使用FileReader采用的默認編碼

FileReader采用的默認編碼

很久以前聽教學視頻,裡面講到Java采用的默認編碼是ISO-8859-1,一直記著。

但是最近重新看IO流的時候,驚訝地發現,在不指定字符編碼的情況下,FileReader居然可以讀取內容為中文的文本文件。要知道ISO-8859-1可是西歐字符集,怎麼能包含中文呢?於是百度瞭一下關鍵詞“IOS-8859-1顯示中文”,結果很多人都有這個疑惑。

代碼如下:

package day170903; 
import java.io.*; 
public class TestDecoder {
	public static void main(String[] args) {
		FileReader fr = null;
		try {
			fr = new FileReader("G:/io/hello.txt");
			int len = 0;
			while((len=fr.read())!=-1) {
				System.out.println((char)len);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(fr!=null) {
					fr.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

事情的真相是什麼呢?

編碼一般是在構造方法處指定的,於是查看一下FileReader的構造方法。也是奇葩,以前沒怎麼註意過,FileReader竟然沒有可以指定字符編碼的構造方法。而且僅僅是簡單地從InputStreamReader繼承,並沒有重寫或擴展任何方法。這可能是歷史上最吝嗇的子類,完全就是啃老族。

不過好在Java的文檔註釋寫得很給力,在FileReader這個類的開頭有下面一段文檔註釋(中文部分為我劣質的翻譯):

/**
 * Convenience class for reading character files.  The constructors of this
 * class assume that the default character encoding and the default byte-buffer
 * size are appropriate.  To specify these values yourself, construct an
 * InputStreamReader on a FileInputStream.
 *
 *這是一個很方便的讀取字符文件(文本文件)的類。
 *這個類的構造方法假設默認的字符編碼和默認的緩存數組大小是合適的(滿足需要的)。
 *假如你想自己指定字符編碼和緩存數組的大小,
 *請使用基於FileInputStream的InputStreamReader類。
 * <p><code>FileReader</code> is meant for reading streams of characters.
 * For reading streams of raw bytes, consider using a
 * <code>FileInputStream</code>.
 *
 *FileReader是設計為用來讀取字符流的。
 *想要讀取原始的字節流的話,可以考慮使用FileInputStream
 * @see InputStreamReader
 * @see FileInputStream
 *
 * @author      Mark Reinhold
 * @since       JDK1.1
 */

所以,設計者已經在文檔註釋中講明白瞭這麼設計的原因。但是對於我們來說,現在比較重要的是這個所謂的默認的字符編碼是什麼。

這個時候我們來看一下我們使用的FileReader中的那個構造方法的具體內容。

    public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
    }

FileReader繼承自InputStreamReader,調用瞭InputStreamReader的接受InputStream類型的形參的構造方法,也就是下面這個。

    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            // The default encoding should always be available
            throw new Error(e);
        }
    }

當然InputStreamReader的這個構造方法又調用瞭其父類Reader的下面的構造方法。

    protected Reader(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }

在這裡,它隻是把得到的InputStream對象賦值給成員變量lock(看lock這個成員變量的文檔註釋的話,大概知道它是用來保證同步的),並沒有說到字符編碼的事。

既然通過super(in)向上查找到父類Reader的構造方法也沒有發現默認字符編碼的蹤跡,那麼這條道就到頭瞭。接下來應該看的是super(in)下面的代碼,也就是那個異常捕捉語句塊。主體語句隻有下面一行內容。

sd = StreamDecoder.forInputStreamReader(in, this, (String)null);

仔細看FileReader和其它IO流的代碼的話會發現,很多輸入流的讀取功能(read及其重載方法)都是通過這個StreamDecoder完成的,這是後話。在Eclipse裡面直接查看這個

StreamDecoder的源碼是不行的,需要去openjdk上找。

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/sun/nio/cs/StreamDecoder.java

上面異常捕捉語句塊主體部分調用的是StreamDecoder的forInputStreamReader方法,對應的代碼如下:

public static StreamDecoder forInputStreamReader(InputStream in,
                                                 Object lock,
                                                 String charsetName)
    throws UnsupportedEncodingException
{
    String csn = charsetName;
    if (csn == null)
        csn = Charset.defaultCharset().name();
    try {
        if (Charset.isSupported(csn))
            return new StreamDecoder(in, lock, Charset.forName(csn));
    } catch (IllegalCharsetNameException x) { }
    throw new UnsupportedEncodingException (csn);
}

其實調用的時候,傳遞的第三個參數是字符串形式的null,這個其實就是我們要找的默認字符編碼。

我們要找的是默認字符編碼,其它代碼不必深究。第一行是說把接收到的第三個參數賦值給csn(局部變量:字符編碼),當然瞭,這個是被InputStreamReader的帶字符編碼參數的構造方法調用的時候才有意義的。沒有指定字符編碼的構造方法調用StreamDecoder的forInputStreamReader的時候傳遞是null。所以接下來的if語句判斷就成立瞭,那麼csn這個變量得到的就是Charset.defaultCharset().name(),見名知意,即默認字符編碼。

接下來就要看Charset這個類的defaultCharset方法的返回值——Charset對象的name()方法的返回值是什麼瞭。說起來有點繞,其實就是找裡面的默認字符編碼。

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

這代碼看起來很費勁,而且接著又要看其它代碼。最終結果是這個所謂的默認字符編碼,其實就是JVM啟動時候的本地編碼。

這個要查看的話,就在對應的項目上點擊右鍵,選擇Properties選項,在彈出的屬性窗口中,可以看到當前項目在JVM中運行時候的默認字符編碼。對於咱們中國人來說,一般都是“GBK”,不過可以根據需要從下拉框選擇。

這代碼看起來很費勁,而且接著又要看其它代碼。最終結果是這個所謂的默認字符編碼,其實就是JVM啟動時候的本地編碼。

這個要查看的話,就在對應的項目上點擊右鍵,選擇Properties選項,在彈出的屬性窗口中,可以看到當前項目在JVM中運行時候的默認字符編碼。對於咱們中國人來說,一般都是“GBK”,不過可以根據需要從下拉框選擇。

所以開頭那個疑問,完全是因為不知道默認的編碼其實是GBK而產生的誤解。反過來測試一下就好瞭,先用OutputStreamWriter往文件中寫入下面一句法語

Est-ce possible que tu sois en train de penser à moi lorsque tu me manques?

我在想你的時候,你會不會也剛好正在想我?

寫入的時候指定字符編碼為ISO-8859-1,然後用InputStreamReader讀取,讀取的時候不指定字符編碼(即采用默認字符編碼)。那麼,假如不能正確還原這句話,就說明默認的字符編碼並不是ISO-8859-1。

package day170903; 
import java.io.*; 
public class TestDefaultCharEncoding {
	public static void main(String[] args) {
		InputStreamReader isr = null;
		OutputStreamWriter osw = null;
		try {
			osw = new OutputStreamWriter(new FileOutputStream("G:/io/ISO-8859-1.txt"),"ISO-8859-1");
			isr = new InputStreamReader(new FileInputStream("G:/io/ISO-8859-1.txt"));
			char[] chars = "Est-ce possible que tu sois en train de penser à moi lorsque tu me manques?".toCharArray();
			osw.write(chars);
			osw.flush();
			int len = 0;
			while((len=isr.read())!=-1) {
				System.out.print((char)len);
			}
		} catch (UnsupportedEncodingException | FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(isr!=null) {
					isr.close();
				}
				if(osw!=null) {
					osw.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

輸出結果是:

Est-ce possible que tu sois en train de penser ? moi lorsque tu me manques?

大部分都正確還原瞭,因為法語中大部分也是英文字母。但是那個法語特有的(相比於英語)à 讀出來以後無法識別,變成瞭問號。

假如默認編碼真的是ISO-8859-1,那麼讀取是完全沒有問題的。現在有問題,正好說明默認編碼不是ISO-8859-1。

基本上到這兒就完事瞭,但是還要說一句。雖然我們可以很方便地知道在不指定字符編碼的情況下,JVM將會采用什麼編碼,但是還是建議采用字符類的時候加上字符編碼,因為寫清楚字符編碼可以讓別人明白你的原意,而且能避免代碼轉手後換瞭一個開發工具後可能出現的編碼異常問題。

FileReader的編碼問題

有一個UTF-8編碼的文本文件,用FileReader讀取到一個字符串,然後轉換字符集:str=new String(str.getBytes(),”UTF-8″);結果大部分中文顯示正常,但最後仍有部分漢字顯示為問號!

public static List<String> getLines( String fileName )  
{  
    List<String> lines = new ArrayList<String>();  
    try  
    {  
        BufferedReader br = new BufferedReader(new FileReader(fileName));  
        String line = null;  
        while( ( line = br.readLine() ) != null )  
            lines.add(new String(line.getBytes("GBK"), "UTF-8"));  
        br.close();  
    }  
    catch( FileNotFoundException e )  
    {  
    }  
    catch( IOException e )  
    {  
    }  
    return lines;  
}  

文件讀入時是按OS的默認字符集即GBK解碼的,我先用默認字符集GBK編碼str.getBytes(“GBK”),此時應該還原為文件中的字節序列瞭,然後再按UTF-8解碼,生成的字符串按理說應該就應該是正確的。

為什麼結果中還是有部分亂碼呢?

問題出在FileReader讀取文件的過程中,FileReader繼承瞭InputStreamReader,但並沒有實現父類中帶字符集參數的構造函數,所以FileReader隻能按系統默認的字符集來解碼,然後在UTF-8 -> GBK -> UTF-8的過程中編碼出現損失,造成結果不能還原最初的字符。

原因明確瞭,用InputStreamReader代替FileReader,InputStreamReader isr=new InputStreamReader(new FileInputStream(fileName),”UTF-8″);這樣讀取文件就會直接用UTF-8解碼,不用再做編碼轉換。

public static List<String> getLines( String fileName )  
{  
    List<String> lines = new ArrayList<String>();  
    try  
    {  
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), "UTF-8"));  
        String line = null;  
        while( ( line = br.readLine() ) != null )  
            lines.add(line);  
        br.close();  
    }  
    catch( FileNotFoundException e )  
    {  
    }  
    catch( IOException e )  
    {  
    }  
    return lines;  
}  

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: