下面這個(gè)程序看起來是在用一種特殊的方法做一件普通的事。那么,它會打印出什么呢?
public class Greeter{
public static void main(String[] args){
String greeting = "Hello World";
for(int i = 0; i < greeting.length(); i++)
System.out.write(greeting.charAt(i));
}
}
盡管這個(gè)程序有點(diǎn)奇怪,但是我們沒有理由懷疑它會產(chǎn)生不正確的行為。它將“Hello World”寫入了System.out,每次寫一個(gè)字符。你可能會意識到write方法只會使用其輸入?yún)?shù)的低位字節(jié)(lower-order byte)。所以當(dāng)“Hello World”含有任何外來字符的時(shí)候,可能會造成一些麻煩,但這里不會:因?yàn)椤癏ello World”完全是由ASCII字符組成的。無論你是每次打印一個(gè)字符,還是一次全部打印,結(jié)果都應(yīng)該是一樣的:這個(gè)程序應(yīng)該打印Hello World。然而,如果你運(yùn)行該程序,就會發(fā)現(xiàn)它不會打印任何東西。那句問候語到哪里去了?難道是程序認(rèn)為它并不令人愉快?
這里的問題在于System.out是帶有緩沖的。Hello World中的字符被寫入了System.out的緩沖區(qū),但是緩沖區(qū)從來都沒有被刷新(flush)。大多數(shù)的程序員認(rèn)為,當(dāng)有輸出產(chǎn)生的時(shí)候System.out和System.err會自動地進(jìn)行刷新,這并不完全正確。這2個(gè)流都屬于PrintStream類型,在5.0版[Java-API]中,有關(guān)這個(gè)類型的文檔敘述道:
一個(gè)PrintStream可以被創(chuàng)建為自動刷新的;這意味著當(dāng)一個(gè)字節(jié)數(shù)組(byte array)被寫入,或者某個(gè)println方法被調(diào)用,或者一個(gè)換行字符或字節(jié)(‘\n’)被寫入之后,PrintStream類型的flush方法就會被自動地調(diào)用。
System.out和System.err所引用的流確實(shí)是PrintStream的能夠自動刷新的變體,但是上面的文檔中并沒有提及write(int)方法。有關(guān)write(int)方法的文檔敘述道:將指定的byte寫入流。如果這個(gè)byte是一個(gè)換行字符,并且流可以自動刷新,那么flush方法將被調(diào)用[Java-API]。實(shí)際上,write(int)是一個(gè)在自動刷新(automatic flushing)功能開啟的情況下不刷新PrintStream的輸出方法(output method)。
令人好奇的是,如果這個(gè)程序改用print(char)去替代write(int),它就會刷新System.out并打印出Hello World。這種行為與print(char)的文檔是矛盾的,因?yàn)槠湮臋n敘述道[Java-API]:
打印一個(gè)字符:這個(gè)字符將根據(jù)平臺缺省的字符編碼方式被翻譯成為一個(gè)或多個(gè)字節(jié),并且這些字節(jié)將完全按照write(int)方法的方式被寫出。
類似地,如果程序改用print(String),它也會對流進(jìn)行刷新,雖然文檔中是禁止這么做的。相應(yīng)的文檔確實(shí)應(yīng)該被修改為描述該方法的實(shí)際行為,而修改方法的行為則會破壞穩(wěn)定性。
修改這個(gè)程序最簡單的方法就是在循環(huán)之后加上一個(gè)對System.out.flush方法的調(diào)用。經(jīng)過這樣的修改之后,程序就會正常地打印出Hello World。當(dāng)然,更好的辦法是重寫這個(gè)程序,使用我們更熟悉的System.out.println方法在控制臺上產(chǎn)生輸出。
這個(gè)謎題的教訓(xùn)與謎題23一樣:盡可能使用熟悉的慣用法;如果你不得不使用陌生的API,請一定要參考相關(guān)的文檔。這里有3條教訓(xùn)給API的設(shè)計(jì)者們:請讓你們的方法的行為能夠清晰的反映在方法名上;請清楚而詳細(xì)地給出這些行為的文檔;請正確地實(shí)現(xiàn)這些行為。
public class Greeter{
public static void main(String[] args){
String greeting = "Hello World";
for(int i = 0; i < greeting.length(); i++)
System.out.write(greeting.charAt(i));
}
}
盡管這個(gè)程序有點(diǎn)奇怪,但是我們沒有理由懷疑它會產(chǎn)生不正確的行為。它將“Hello World”寫入了System.out,每次寫一個(gè)字符。你可能會意識到write方法只會使用其輸入?yún)?shù)的低位字節(jié)(lower-order byte)。所以當(dāng)“Hello World”含有任何外來字符的時(shí)候,可能會造成一些麻煩,但這里不會:因?yàn)椤癏ello World”完全是由ASCII字符組成的。無論你是每次打印一個(gè)字符,還是一次全部打印,結(jié)果都應(yīng)該是一樣的:這個(gè)程序應(yīng)該打印Hello World。然而,如果你運(yùn)行該程序,就會發(fā)現(xiàn)它不會打印任何東西。那句問候語到哪里去了?難道是程序認(rèn)為它并不令人愉快?
這里的問題在于System.out是帶有緩沖的。Hello World中的字符被寫入了System.out的緩沖區(qū),但是緩沖區(qū)從來都沒有被刷新(flush)。大多數(shù)的程序員認(rèn)為,當(dāng)有輸出產(chǎn)生的時(shí)候System.out和System.err會自動地進(jìn)行刷新,這并不完全正確。這2個(gè)流都屬于PrintStream類型,在5.0版[Java-API]中,有關(guān)這個(gè)類型的文檔敘述道:
一個(gè)PrintStream可以被創(chuàng)建為自動刷新的;這意味著當(dāng)一個(gè)字節(jié)數(shù)組(byte array)被寫入,或者某個(gè)println方法被調(diào)用,或者一個(gè)換行字符或字節(jié)(‘\n’)被寫入之后,PrintStream類型的flush方法就會被自動地調(diào)用。
System.out和System.err所引用的流確實(shí)是PrintStream的能夠自動刷新的變體,但是上面的文檔中并沒有提及write(int)方法。有關(guān)write(int)方法的文檔敘述道:將指定的byte寫入流。如果這個(gè)byte是一個(gè)換行字符,并且流可以自動刷新,那么flush方法將被調(diào)用[Java-API]。實(shí)際上,write(int)是一個(gè)在自動刷新(automatic flushing)功能開啟的情況下不刷新PrintStream的輸出方法(output method)。
令人好奇的是,如果這個(gè)程序改用print(char)去替代write(int),它就會刷新System.out并打印出Hello World。這種行為與print(char)的文檔是矛盾的,因?yàn)槠湮臋n敘述道[Java-API]:
打印一個(gè)字符:這個(gè)字符將根據(jù)平臺缺省的字符編碼方式被翻譯成為一個(gè)或多個(gè)字節(jié),并且這些字節(jié)將完全按照write(int)方法的方式被寫出。
類似地,如果程序改用print(String),它也會對流進(jìn)行刷新,雖然文檔中是禁止這么做的。相應(yīng)的文檔確實(shí)應(yīng)該被修改為描述該方法的實(shí)際行為,而修改方法的行為則會破壞穩(wěn)定性。
修改這個(gè)程序最簡單的方法就是在循環(huán)之后加上一個(gè)對System.out.flush方法的調(diào)用。經(jīng)過這樣的修改之后,程序就會正常地打印出Hello World。當(dāng)然,更好的辦法是重寫這個(gè)程序,使用我們更熟悉的System.out.println方法在控制臺上產(chǎn)生輸出。
這個(gè)謎題的教訓(xùn)與謎題23一樣:盡可能使用熟悉的慣用法;如果你不得不使用陌生的API,請一定要參考相關(guān)的文檔。這里有3條教訓(xùn)給API的設(shè)計(jì)者們:請讓你們的方法的行為能夠清晰的反映在方法名上;請清楚而詳細(xì)地給出這些行為的文檔;請正確地實(shí)現(xiàn)這些行為。