使用FileReader采用的默認編碼
FileReader采用的默認編碼
很久以前聽教學(xué)視頻,里面講到Java采用的默認編碼是ISO-8859-1,一直記著。
但是最近重新看IO流的時候,驚訝地發(fā)現(xiàn),在不指定字符編碼的情況下,F(xiàn)ileReader居然可以讀取內(nèi)容為中文的文本文件。要知道ISO-8859-1可是西歐字符集,怎么能包含中文呢?于是百度了一下關(guān)鍵詞“IOS-8859-1顯示中文”,結(jié)果很多人都有這個疑惑。
代碼如下:
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(); } } } }
事情的真相是什么呢?
編碼一般是在構(gòu)造方法處指定的,于是查看一下FileReader的構(gòu)造方法。也是奇葩,以前沒怎么注意過,F(xiàn)ileReader竟然沒有可以指定字符編碼的構(gòu)造方法。而且僅僅是簡單地從InputStreamReader繼承,并沒有重寫或擴展任何方法。這可能是歷史上最吝嗇的子類,完全就是啃老族。
不過好在Java的文檔注釋寫得很給力,在FileReader這個類的開頭有下面一段文檔注釋(中文部分為我劣質(zhì)的翻譯):
/** * 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. * *這是一個很方便的讀取字符文件(文本文件)的類。 *這個類的構(gòu)造方法假設(shè)默認的字符編碼和默認的緩存數(shù)組大小是合適的(滿足需要的)。 *假如你想自己指定字符編碼和緩存數(shù)組的大小, *請使用基于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是設(shè)計為用來讀取字符流的。 *想要讀取原始的字節(jié)流的話,可以考慮使用FileInputStream * @see InputStreamReader * @see FileInputStream * * @author Mark Reinhold * @since JDK1.1 */
所以,設(shè)計者已經(jīng)在文檔注釋中講明白了這么設(shè)計的原因。但是對于我們來說,現(xiàn)在比較重要的是這個所謂的默認的字符編碼是什么。
這個時候我們來看一下我們使用的FileReader中的那個構(gòu)造方法的具體內(nèi)容。
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); }
FileReader繼承自InputStreamReader,調(diào)用了InputStreamReader的接受InputStream類型的形參的構(gòu)造方法,也就是下面這個。
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的這個構(gòu)造方法又調(diào)用了其父類Reader的下面的構(gòu)造方法。
protected Reader(Object lock) { if (lock == null) { throw new NullPointerException(); } this.lock = lock; }
在這里,它只是把得到的InputStream對象賦值給成員變量lock(看lock這個成員變量的文檔注釋的話,大概知道它是用來保證同步的),并沒有說到字符編碼的事。
既然通過super(in)向上查找到父類Reader的構(gòu)造方法也沒有發(fā)現(xiàn)默認字符編碼的蹤跡,那么這條道就到頭了。接下來應(yīng)該看的是super(in)下面的代碼,也就是那個異常捕捉語句塊。主體語句只有下面一行內(nèi)容。
sd = StreamDecoder.forInputStreamReader(in, this, (String)null);
仔細看FileReader和其它IO流的代碼的話會發(fā)現(xiàn),很多輸入流的讀取功能(read及其重載方法)都是通過這個StreamDecoder完成的,這是后話。在Eclipse里面直接查看這個
StreamDecoder的源碼是不行的,需要去openjdk上找。
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/sun/nio/cs/StreamDecoder.java
上面異常捕捉語句塊主體部分調(diào)用的是StreamDecoder的forInputStreamReader方法,對應(yīng)的代碼如下:
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); }
其實調(diào)用的時候,傳遞的第三個參數(shù)是字符串形式的null,這個其實就是我們要找的默認字符編碼。
我們要找的是默認字符編碼,其它代碼不必深究。第一行是說把接收到的第三個參數(shù)賦值給csn(局部變量:字符編碼),當然了,這個是被InputStreamReader的帶字符編碼參數(shù)的構(gòu)造方法調(diào)用的時候才有意義的。沒有指定字符編碼的構(gòu)造方法調(diào)用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; }
這代碼看起來很費勁,而且接著又要看其它代碼。最終結(jié)果是這個所謂的默認字符編碼,其實就是JVM啟動時候的本地編碼。
這個要查看的話,就在對應(yīng)的項目上點擊右鍵,選擇Properties選項,在彈出的屬性窗口中,可以看到當前項目在JVM中運行時候的默認字符編碼。對于咱們中國人來說,一般都是“GBK”,不過可以根據(jù)需要從下拉框選擇。
這代碼看起來很費勁,而且接著又要看其它代碼。最終結(jié)果是這個所謂的默認字符編碼,其實就是JVM啟動時候的本地編碼。
這個要查看的話,就在對應(yīng)的項目上點擊右鍵,選擇Properties選項,在彈出的屬性窗口中,可以看到當前項目在JVM中運行時候的默認字符編碼。對于咱們中國人來說,一般都是“GBK”,不過可以根據(jù)需要從下拉框選擇。
所以開頭那個疑問,完全是因為不知道默認的編碼其實是GBK而產(chǎn)生的誤解。反過來測試一下就好了,先用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(); } } } }
輸出結(jié)果是:
Est-ce possible que tu sois en train de penser ? moi lorsque tu me manques?
大部分都正確還原了,因為法語中大部分也是英文字母。但是那個法語特有的(相比于英語)à 讀出來以后無法識別,變成了問號。
假如默認編碼真的是ISO-8859-1,那么讀取是完全沒有問題的?,F(xiàn)在有問題,正好說明默認編碼不是ISO-8859-1。
基本上到這兒就完事了,但是還要說一句。雖然我們可以很方便地知道在不指定字符編碼的情況下,JVM將會采用什么編碼,但是還是建議采用字符類的時候加上字符編碼,因為寫清楚字符編碼可以讓別人明白你的原意,而且能避免代碼轉(zhuǎn)手后換了一個開發(fā)工具后可能出現(xiàn)的編碼異常問題。
FileReader的編碼問題
有一個UTF-8編碼的文本文件,用FileReader讀取到一個字符串,然后轉(zhuǎn)換字符集:str=new String(str.getBytes(),"UTF-8");結(jié)果大部分中文顯示正常,但最后仍有部分漢字顯示為問號!
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”),此時應(yīng)該還原為文件中的字節(jié)序列了,然后再按UTF-8解碼,生成的字符串按理說應(yīng)該就應(yīng)該是正確的。
為什么結(jié)果中還是有部分亂碼呢?
問題出在FileReader讀取文件的過程中,F(xiàn)ileReader繼承了InputStreamReader,但并沒有實現(xiàn)父類中帶字符集參數(shù)的構(gòu)造函數(shù),所以FileReader只能按系統(tǒng)默認的字符集來解碼,然后在UTF-8 -> GBK -> UTF-8的過程中編碼出現(xiàn)損失,造成結(jié)果不能還原最初的字符。
原因明確了,用InputStreamReader代替FileReader,InputStreamReader isr=new InputStreamReader(new FileInputStream(fileName),"UTF-8");這樣讀取文件就會直接用UTF-8解碼,不用再做編碼轉(zhuǎn)換。
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; }
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot日志進階實戰(zhàn)之Logback配置經(jīng)驗和方法
本文給大家介紹在SpringBoot中使用Logback配置日志的經(jīng)驗和方法,并提供了詳細的代碼示例和解釋,包括:滾動文件、異步日志記錄、動態(tài)指定屬性、日志級別、配置文件等常用功能,覆蓋日常Logback配置開發(fā)90%的知識點,感興趣的朋友跟隨小編一起看看吧2023-06-06Java結(jié)構(gòu)型設(shè)計模式之裝飾模式詳解
裝飾模式(Decorator Pattern)允許向一個現(xiàn)有的對象添加新的功能,同時又不改變其結(jié)構(gòu)。這種類型的設(shè)計模式屬于結(jié)構(gòu)型模式,它是作為現(xiàn)有類的一個包裝。這種模式創(chuàng)建了一個裝飾類,用來包裝原有的類,并在保持類方法簽名完整性的前提下,提供了額外的功能2023-03-03利用JavaFX工具構(gòu)建Reactive系統(tǒng)
這篇文章主要介紹了使用JavaFX構(gòu)建Reactive系統(tǒng),利用JavaFX工具集中的新的超棒特性來構(gòu)建響應(yīng)式的快速應(yīng)用程序,感興趣的小伙伴們可以參考一下2016-02-02引入mybatis-plus報 Invalid bound statement錯誤問題的解決方法
這篇文章主要介紹了引入mybatis-plus報 Invalid bound statement錯誤問題的解決方法,需要的朋友可以參考下2020-05-05Spring Data JPA中的Specification動態(tài)查詢詳解
Specification是一個設(shè)計模式,用于企業(yè)級應(yīng)用開發(fā)中,其主要目的是將業(yè)務(wù)規(guī)則從業(yè)務(wù)邏輯中分離出來,在數(shù)據(jù)查詢方面,Specification可以定義復(fù)雜的查詢,使其更易于重用和測試,這篇文章主要介紹了Spring Data JPA中的Specification動態(tài)查詢詳解,需要的朋友可以參考下2023-07-07RabbitMQ中的channel信道、exchange交換機和queue隊列詳解
這篇文章主要介紹了RabbitMQ中的channel信道、exchange交換機和queue隊列詳解,connection是指物理的連接,一個client與一個server之間有一個連接,一個連接上可以建立多個channel,可以理解為邏輯上的連接,需要的朋友可以參考下2023-08-08Java數(shù)據(jù)結(jié)構(gòu)之復(fù)雜度篇
算法復(fù)雜度分為時間復(fù)雜度和空間復(fù)雜度。其作用:?時間復(fù)雜度是度量算法執(zhí)行的時間長短;而空間復(fù)雜度是度量算法所需存儲空間的大小2022-01-01