在 JSP 頁面中,我們習慣用 out.print() 輸出變量,卻鮮少思考:為什麼 out.print(new Date()) 能直接輸出對象,而 out.write(new Date()) 卻編譯報錯?本文從 Servlet 轉譯原理、JspWriter 繼承體系、Java 多態機制三個維度,徹底揭開這對「雙胞胎」方法的本質差異。
假設 JSP 代碼:
<body>
<h1>現在時間:</h1>
<%= new Date() %>
</body>
經 JSP 引擎(如 Jasper)編譯後,生成的 Servlet _jspService() 方法中 body 部分約為:
out.write("<h1>現在時間:</h1>\n");
out.print(new Date());
關鍵觀察: 靜態模板文本用 write(String),動態表達式用 print(Object)。這不是巧合,是編譯器基於類型系統的刻意選擇。
abstract class JspWriter extends java.io.Writer {
// 從 Writer 繼承
public void write(String s) { ... }
public void write(char[] cbuf, int off, int len) { ... }
public void write(int c) { ... }
// JspWriter 自身新增的 print 系列(重載)
public void print(boolean b) { ... }
public void print(char c) { ... }
public void print(int i) { ... }
public void print(long l) { ... }
public void print(float f) { ... }
public void print(double d) { ... }
public void print(char[] s) { ... }
public void print(String s) { ... }
public void print(Object obj) { ... } // ★ 關鍵:接受 Object
public void println() { ... }
public void println(boolean x) { ... }
// ... 其他 println 重載
public void println(Object x) { ... }
}
Writer 抽象類僅定義 write(String)、write(char[])、write(int) 三種寫入基本類型/字串的方法,沒有 write(Object)。而 JspWriter(及其父類 PrintWriter)擴展了完整的 print/println 重載家族,覆蓋所有基本類型與 Object。
java.io.Writer 是字元輸出流的抽象基類,契約是「寫入字元序列」。其方法簽名刻意不包含 Object,避免在底層流層面引入 toString() 邏輯、異常處理、國際化等高層語義,保持流操作純粹、高效、可組合。
PrintWriter 繼承 Writer,在其之上構建了豐富的 print/println 重載:
String.valueOf())。Object 調用 String.valueOf(obj) → 內部委託 obj.toString(),null 安全返回 "null"。autoFlush)。JspWriter 繼承 PrintWriter 行為,並增加緩衝區管理、clear()/clearBuffer() 等 JSP 生命周期相關能力。
JSP 編譯器看到 <%= new Date() %>:
Date(Object 子類)。out.write(Date) ❌ 無匹配方法(Writer 無 write(Object))。out.print(Date) ✅ 匹配 print(Object)。out.print(new Date())。對於靜態文本 "<h1>...",類型為 String,write(String) 和 print(String) 皆可,編譯器傾向用 write(語義更精確:寫入字元流,無格式化開銷)。
public class PrintVsWriteDemo {
public static void main(String[] args) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
Object obj = new Date(); // 任意對象
// 1. print(Object) - 正常工作
pw.print(obj);
System.out.println("print 輸出: " + sw.toString()); // 如: Mon Jan 14 10:00:00 CST 2024
sw.getBuffer().setLength(0);
// 2. write(String) - 需手動 toString()
pw.write(obj.toString());
System.out.println("write 輸出: " + sw.toString());
// 3. write(Object) - 編譯錯誤
// pw.write(obj); // Error: no suitable method found for write(Object)
}
}
| 場景 | 推薦方法 | 理由 |
|---|---|---|
| 輸出任意對象(含基本類型包裝類) | out.print(obj) / out.println(obj) | 自動 toString()、null 安全、多態分發 |
| 輸出已知字串、字元陣列、單字元 | out.write(str) | 語義精確、避免 PrintWriter 額外檢查開銷、直接寫入緩衝區 |
| 需要格式化輸出(如 printf 風格) | out.printf(fmt, args) | PrintWriter 提供,JSP 2.0+ 支援 |
| 大量字串拼接輸出 | StringBuilder 組裝後 out.write(sb.toString()) | 減少頻繁調用、提升效能 |
out.write() 比 out.print() 快。String 參數,兩者內部最終都寫入同一緩衝區,差異微乎其微。print 只是多了一次 String.valueOf() 空檢查。out.print()。write(),手動改 print() 無益處。out.write(null) 拋異常。Writer.write(null) 拋 NullPointerException;PrintWriter.print(null) 輸出 "null" 字串。這是選擇時的關鍵差異。out.print() 能輸出對象、out.write() 僅限字串,本質是 Java IO 類庫分層設計 的體現:Writer 定義字元流寫入的最小契約,PrintWriter/JspWriter 在此基礎上提供面向對象的格式化輸出外觀。JSP 編譯器根據表達式靜態類型,自動選擇最匹配的方法。理解這一機制,不僅能避免編譯錯誤,更能在效能敏感場景做出正確的 API 選擇。