流與字符流轉(zhuǎn)換)
1. 一次線上亂碼事故關(guān)于字節(jié)流與字符流的轉(zhuǎn)換很多坑其實(shí)都是這里埋下的上一份工作里線上出了個(gè)典型的“錕斤拷”事故。簡(jiǎn)單說是一個(gè)定時(shí)任務(wù)去讀取某個(gè)合作方同步過來的GBK編碼文件然后做數(shù)據(jù)解析入庫結(jié)果所有中文字段全部變成了“錕斤拷錕斤拷”。定位后發(fā)現(xiàn)問題就出在讀取文件時(shí)直接用了FileReader而線上JVM默認(rèn)編碼是UTF-8源文件卻是GBK編碼兩邊沒對(duì)上解碼全亂。這類問題在Java開發(fā)里實(shí)在太常見了。Java的IO體系明明很簡(jiǎn)單但一旦牽扯到字節(jié)流和字符流的轉(zhuǎn)換很多人就開始迷糊InputStreamReader和OutputStreamWriter是干什么的為什么不能直接拿FileInputStream讀文本為什么同樣的代碼本地跑得好好的換臺(tái)機(jī)器就亂碼如果你也有過這些疑問或者正在準(zhǔn)備Java基礎(chǔ)面試那這篇文章就是給你準(zhǔn)備的。下面我會(huì)從字節(jié)流和字符流的設(shè)計(jì)邏輯說起把轉(zhuǎn)換原理、實(shí)操代碼、亂碼排查完整過一遍。內(nèi)容不繞彎盡量用我在實(shí)際項(xiàng)目里踩過的坑來對(duì)照著講。2. 字節(jié)流與字符流的分工邏輯同樣是讀寫為什么非要兩套體系2.1 字節(jié)流以字節(jié)為最小單位解決一切二進(jìn)制數(shù)據(jù)的讀寫Java的IO流從設(shè)計(jì)上就分了兩條線一條是操作字節(jié)的InputStream/OutputStream另一條是操作字符的Reader/Writer。先看字節(jié)流它代表的是最底層的數(shù)據(jù)傳輸數(shù)據(jù)單位是byte也就是8位二進(jìn)制數(shù)。圖片、音頻、壓縮包、Word文檔這些文件本質(zhì)上都是二進(jìn)制內(nèi)容讀取的時(shí)候必須以字節(jié)為單位原樣搬運(yùn)不做任何編碼轉(zhuǎn)換才能保證內(nèi)容不被破壞。InputStream和OutputStream是所有字節(jié)流的頂層抽象JDK里常見的子類有很多FileInputStream用來讀文件、FileOutputStream用來寫文件、BufferedInputStream給文件讀加緩沖、FilterInputStream提供裝飾能力。字節(jié)流的核心方法很簡(jiǎn)單就是read()和write(byte[])讀出來一個(gè)byte就處理一個(gè)byte寫進(jìn)去一個(gè)byte就傳走一個(gè)byte。它不關(guān)心這些字節(jié)是什么含義字符也好、像素也好、壓縮數(shù)據(jù)也好對(duì)它來說都是二進(jìn)制。打個(gè)比方字節(jié)流就像物流公司里的搬運(yùn)工只管按照包裹數(shù)量來搬運(yùn)不關(guān)心包裹里面裝的是衣服還是書。這個(gè)設(shè)計(jì)在操作系統(tǒng)向下傳輸數(shù)據(jù)時(shí)非常合理因?yàn)榇疟P、網(wǎng)卡、內(nèi)存對(duì)外呈現(xiàn)的都是流式的字節(jié)序列字節(jié)流是跟底層打交道最直接的方式。2.2 字符流面向人類可讀文本天然要處理編碼字符流則是Reader/Writer體系的產(chǎn)物它解決的是“人類可讀文本”的讀寫問題。Java里一個(gè)char占據(jù)兩個(gè)字節(jié)也就是UTF-16編碼下的一個(gè)編碼單元。但文本文件在磁盤上卻不是以char為單位存放的而是按某種字符集編碼后的字節(jié)序列。比如UTF-8編碼下英文字母占1個(gè)字節(jié)中文漢字占3個(gè)字節(jié)GBK編碼下中文占2個(gè)字節(jié)。字符流的作用就是把這些編碼后的字節(jié)序列翻譯成一個(gè)個(gè)Java能夠識(shí)別的char。這個(gè)“翻譯”動(dòng)作在底層需要依賴一個(gè)字符集也就是Charset。當(dāng)你從文件里讀文本時(shí)操作系統(tǒng)給你的是一堆字節(jié)字符流拿著字節(jié)按照你指定的字符集去解碼變成char給你用。寫出去的時(shí)候反過來把char按字符集編碼成字節(jié)再通過底層字節(jié)流寫出去。很多初學(xué)者會(huì)覺得字符流比字節(jié)流高級(jí)這不算錯(cuò)。但要注意字符流同樣離不開字節(jié)流因?yàn)樽罱K數(shù)據(jù)落盤、走網(wǎng)絡(luò)都必須變成字節(jié)。這就是為什么字符流內(nèi)部往往包裝了一個(gè)字節(jié)流比如FileReader你以為它直接操作文件其實(shí)它內(nèi)部就是把FileInputStream包了一層再加默認(rèn)字符集解碼。2.3 我的選型原則與對(duì)比總結(jié)項(xiàng)目的業(yè)務(wù)場(chǎng)景決定了你該用哪條流。我的經(jīng)驗(yàn)是凡是文本內(nèi)容優(yōu)先選字符流凡是二進(jìn)制內(nèi)容必須選字節(jié)流。判斷標(biāo)準(zhǔn)也很簡(jiǎn)單如果文件打開后你能直接用文本編輯器讀懂內(nèi)容那就用字符流如果打開后是一堆亂碼、特殊符號(hào)或者文件本身就是圖片、視頻、音視頻、壓縮包等格式那就老老實(shí)實(shí)用字節(jié)流。下面這組對(duì)比是我在帶新人的時(shí)候經(jīng)常給他們做的總結(jié)對(duì)比維度字節(jié)流字符流頂層抽象InputStream / OutputStreamReader / Writer最小數(shù)據(jù)單位byte8位char16位是否處理編碼完全不處理按字節(jié)原樣讀寫必須指定或隱式依賴字符集典型場(chǎng)景圖片、音視頻、壓縮包、網(wǎng)絡(luò)傳輸文本文件、JSON、日志、配置文件緩沖包裝類BufferedInputStream / BufferedOutputStreamBufferedReader / BufferedWriter常用文件類FileInputStream / FileOutputStreamFileReader / FileWriter這里要特別提醒FileReader和FileWriter看起來方便但它們存在一個(gè)我踩過很多次坑的設(shè)計(jì)問題——構(gòu)造方法里不提供指定字符集的參數(shù)只能使用JVM默認(rèn)字符集。也就是說同樣的代碼在Windows上跑用GBK在Linux上跑用UTF-8環(huán)境一變讀同一個(gè)文件的輸出就完全不一樣。所以如果代碼要在多平臺(tái)部署或者文件編碼不確定我更推薦用InputStreamReader、OutputStreamWriter這兩個(gè)轉(zhuǎn)換流顯式聲明編碼避免隱患。3. 轉(zhuǎn)換橋梁的原理InputStreamReader與OutputStreamWriter到底做了什么3.1 為什么說轉(zhuǎn)換流是字節(jié)與字符之間的“翻譯官”字節(jié)流和字符流是兩套體系但數(shù)據(jù)總要從一個(gè)流向另一個(gè)。于是InputStreamReader和OutputStreamWriter就成了中間那座橋。InputStreamReader本質(zhì)上是一個(gè)字符流因?yàn)樗^承了Reader但它構(gòu)造時(shí)接收的是一個(gè)InputStream對(duì)象內(nèi)部再通過CharsetDecoder把從字節(jié)流里讀出來的字節(jié)解碼成字符。反過來OutputStreamWriter繼承自Writer構(gòu)造時(shí)接收一個(gè)OutputStream寫入字符時(shí)內(nèi)部用CharsetEncoder把字符編碼成字節(jié)再交給字節(jié)流處理。看一下JDK源碼里的字段就一目了然public class InputStreamReader extends Reader { private final CharsetDecoder decoder; private final InputStream in; // ... } public class OutputStreamWriter extends Writer { private final CharsetEncoder encoder; private final OutputStream out; // ... }轉(zhuǎn)換流的核心價(jià)值在于它讓你在寫業(yè)務(wù)代碼時(shí)不需要關(guān)心底層字節(jié)是怎么排列的只需要面對(duì)字符。你讀到的是一行一行或一個(gè)字符一個(gè)字符的字符串寫出的也是字符串編碼轉(zhuǎn)換細(xì)節(jié)全部被封裝在內(nèi)部了。這個(gè)過程和“翻譯官”很像。兩個(gè)不同語言的人要對(duì)話翻譯官先把甲的話翻譯成乙的語言乙回答后再翻譯回去。翻譯官?zèng)Q定了兩端能不能順利溝通而轉(zhuǎn)換流里的Charset就是雙方共同認(rèn)定的“翻譯規(guī)則”兩端必須約定同一套字符編碼翻譯才準(zhǔn)確不然就是雞同鴨講。3.2 Charset 與 CharsetDecoder/CharsetEncoder 的關(guān)系Charset是字符集的抽象類常見實(shí)現(xiàn)有UTF-8、UTF-16、GBK、ISO-8859-1、US-ASCII等。每個(gè)Charset對(duì)象都提供了兩個(gè)關(guān)鍵方法newDecoder()返回一個(gè)CharsetDecodernewEncoder()返回一個(gè)CharsetEncoder。解碼和編碼的具體工作實(shí)際上是由Decoder和Encoder完成的。以讀取為例當(dāng)你調(diào)用InputStreamReader.read()時(shí)它會(huì)從底層的InputStream里拿到一批字節(jié)然后交給CharsetDecoder去解析。CharsetDecoder內(nèi)部維護(hù)了狀態(tài)對(duì)于UTF-8這種變長(zhǎng)編碼它會(huì)持續(xù)緩沖字節(jié)直到能夠組成完整的字符才返回一個(gè)char。如果遇到非法字節(jié)序列還會(huì)觸發(fā)CodingErrorAction默認(rèn)是REPLACE也就是替換成\uFFFD這個(gè)特殊字符。這就是為什么有些文件讀出來會(huì)出現(xiàn)一些奇怪的問號(hào)或替換符。寫方向也是同樣道理。OutputStreamWriter.write()內(nèi)部拿到char數(shù)組后會(huì)根據(jù)當(dāng)前字符集把這些字符編碼成字節(jié)再通過底層的OutputStream寫出。這個(gè)過程同樣有編碼錯(cuò)誤處理如果某個(gè)字符在當(dāng)前字符集里不存在比如把中文寫進(jìn)ISO-8859-1編碼的流里默認(rèn)會(huì)替換成問號(hào)。還有一點(diǎn)要特別注意InputStreamReader和OutputStreamWriter的默認(rèn)構(gòu)造函數(shù)會(huì)使用Charset.defaultCharset()也就是JVM啟動(dòng)時(shí)根據(jù)file.encoding系統(tǒng)屬性確定的字符集。這個(gè)默認(rèn)值不跨平臺(tái)也不跨啟動(dòng)方式。所以在生產(chǎn)環(huán)境里我基本不會(huì)用無參構(gòu)造方法而是一定顯式傳入字符集。這個(gè)習(xí)慣幫我擋掉了很多后續(xù)的麻煩。3.3 標(biāo)準(zhǔn)轉(zhuǎn)換模板與包裝順序下面給出一個(gè)常見的轉(zhuǎn)換組合方式。模板雖然簡(jiǎn)單但包裝順序很關(guān)鍵我拆開來說一下。讀取方向InputStream in new FileInputStream(source.txt); Reader reader new InputStreamReader(in, StandardCharsets.UTF_8); BufferedReader bufferedReader new BufferedReader(reader); String line; while ((line bufferedReader.readLine()) ! null) { System.out.println(line); }寫入方向OutputStream out new FileOutputStream(target.txt); Writer writer new OutputStreamWriter(out, StandardCharsets.UTF_8); BufferedWriter bufferedWriter new BufferedWriter(writer); bufferedWriter.write(你好Java); bufferedWriter.newLine(); bufferedWriter.write(第二行);包裝順序是字節(jié)流在外層還是內(nèi)層以讀取為例最內(nèi)側(cè)必須是字節(jié)流因?yàn)閿?shù)據(jù)源頭是字節(jié)中間用InputStreamReader做字節(jié)到字符的轉(zhuǎn)換最外層再加一層BufferedReader目的是提供readLine()、減少磁盤IO次數(shù)、提高讀寫性能。很多初學(xué)者搞不清為什么還要多包一層Buffer。原因很簡(jiǎn)單如果直接一個(gè)字符一個(gè)字符地調(diào)read()每調(diào)一次就可能觸發(fā)一次操作系統(tǒng)IO開銷非常大。加了BufferedReader之后它會(huì)一次性從底層流中讀入一大塊數(shù)據(jù)到內(nèi)存緩沖區(qū)之后業(yè)務(wù)代碼在緩沖區(qū)里取字符速度要快很多。寫方向同理BufferedWriter會(huì)先攢一批字符再一次性刷到底層。還有一個(gè)很多人忽略的點(diǎn)FileReader和FileWriter其實(shí)并不是一個(gè)獨(dú)立的新流實(shí)現(xiàn)。翻源碼就能看到FileReader不過是InputStreamReader的一個(gè)簡(jiǎn)化子類讀文件時(shí)自動(dòng)構(gòu)造一個(gè)FileInputStream但字符集用的是JVM默認(rèn)值。FileWriter對(duì)應(yīng)OutputStreamWriter。所以你在用FileReader時(shí)等于隱式地做了一次轉(zhuǎn)換只是沒得選字符集這就是它容易埋雷的原因。4. 從文件讀到內(nèi)存再寫回一次完整的轉(zhuǎn)換實(shí)操4.1 一個(gè)常見的實(shí)際場(chǎng)景前陣子有個(gè)朋友問我一個(gè)問題合作方給了一個(gè)CSV文件文件是GBK編碼的里面有幾萬條中文數(shù)據(jù)他們希望轉(zhuǎn)成UTF-8編碼后再導(dǎo)入數(shù)據(jù)庫因?yàn)閿?shù)據(jù)庫連接層統(tǒng)一用的是UTF-8直接導(dǎo)GBK文件會(huì)亂碼。這個(gè)需求本質(zhì)上就是一次“讀GBK字節(jié)流 - 轉(zhuǎn)字符 - 轉(zhuǎn)UTF-8字節(jié)流”的過程。我給他寫的示例大概是這樣public void convertGbkToUtf8(String sourcePath, String targetPath) { try (BufferedReader reader new BufferedReader( new InputStreamReader( new FileInputStream(sourcePath), Charset.forName(GBK))); BufferedWriter writer new BufferedWriter( new OutputStreamWriter( new FileOutputStream(targetPath), StandardCharsets.UTF_8))) { String line; while ((line reader.readLine()) ! null) { // 這里可以對(duì)每行數(shù)據(jù)做清洗或轉(zhuǎn)換 writer.write(line); writer.newLine(); } } catch (IOException e) { e.printStackTrace(); } }這段代碼里有兩個(gè)字符集參數(shù)讀取側(cè)用Charset.forName(GBK)寫入側(cè)用StandardCharsets.UTF_8。為什么不能統(tǒng)一因?yàn)樵次募褪荊BK編碼的字節(jié)序列你必須用GBK去解碼才能把字節(jié)正確地映射成字符。而目標(biāo)文件要求UTF-8編碼所以寫入時(shí)用UTF-8重新編碼。這里就牽扯到一個(gè)關(guān)鍵認(rèn)知亂碼并不一定是文件壞了而往往是“寫入時(shí)用的編碼”和“讀取時(shí)用的解碼”不一致。編碼和解碼就是一對(duì)密鑰密鑰不匹配內(nèi)容就會(huì)錯(cuò)亂。我第一次排查亂碼時(shí)總以為是文件內(nèi)容損壞后來才明白文件里的字節(jié)本沒有錯(cuò)錯(cuò)的是我用了錯(cuò)誤的字符集去解釋它。4.2 BufferedReader.readLine與中文邊界問題很多人用BufferedReader.readLine()時(shí)很放心覺得按行讀文本永遠(yuǎn)不會(huì)出問題。但有一個(gè)細(xì)節(jié)值得想一想當(dāng)文件里某個(gè)中文字符恰好跨越緩沖區(qū)邊界時(shí)流內(nèi)部怎么保證不把一個(gè)完整的多字節(jié)字符拆開答案是InputStreamReader內(nèi)部維護(hù)了一個(gè)字節(jié)緩沖區(qū)。當(dāng)它發(fā)現(xiàn)當(dāng)前累積的字節(jié)還不構(gòu)成一個(gè)完整字符時(shí)它會(huì)繼續(xù)向底層流讀取更多字節(jié)直到能解碼出一個(gè)完整的char再交給上層BufferedReader。這個(gè)過程對(duì)上層是透明的所以readLine()不會(huì)在字符中間截?cái)?。但這也就意味著如果原始文件里有非法字節(jié)序列CharsetDecoder在REPLACE模式下會(huì)把它們各自替換為\uFFFD再返回給你。處理返回的字符串時(shí)要注意可能出現(xiàn)的替換符。另外要提醒BufferedReader.readLine()會(huì)丟棄行尾的換行符所以在寫回文件時(shí)一定要記得補(bǔ)newLine()就像上面示例代碼里那樣。4.3 小文件場(chǎng)景下更直接的轉(zhuǎn)換方式如果文件不大也可以用Files工具類配合String做一步到位轉(zhuǎn)換這種方式代碼更短也更適合快速驗(yàn)證byte[] gbkBytes Files.readAllBytes(Paths.get(sourcePath)); String content new String(gbkBytes, Charset.forName(GBK)); byte[] utf8Bytes content.getBytes(StandardCharsets.UTF_8); Files.write(Paths.get(targetPath), utf8Bytes);new String(byte[], charset)完成的是解碼String.getBytes(charset)完成的是編碼本質(zhì)上和轉(zhuǎn)換流的邏輯完全一致。這段代碼唯一的問題是一次性把整個(gè)文件讀入內(nèi)存如果文件非常大比如幾個(gè)GB內(nèi)存會(huì)扛不住。所以大文件必須走流式逐行處理小批量可以這么寫。Files.readAllBytes還有個(gè)潛在問題如果文件帶BOM讀出來的字符串開頭會(huì)多一個(gè)\uFEFF字符。導(dǎo)入數(shù)據(jù)庫或?qū)懟匚募r(shí)這個(gè)字符可能會(huì)引發(fā)后續(xù)處理異常。遇到帶BOM的UTF-8文件時(shí)建議先做一次剝離if (content.startsWith(\uFEFF)) { content content.substring(1); }4.4 流的關(guān)閉順序?yàn)槭裁匆謨?nèi)外再聊一個(gè)高頻面試題多個(gè)流嵌套時(shí)關(guān)閉順序是怎么樣的關(guān)鍵原則是“先關(guān)外層再關(guān)內(nèi)層”。外層是后包裝的流內(nèi)層是更原始的流。比如BufferedReader - InputStreamReader - FileInputStream這個(gè)鏈條最外層是BufferedReader。關(guān)閉它的時(shí)候它會(huì)把內(nèi)部持有的InputStreamReader也關(guān)閉而InputStreamReader關(guān)閉時(shí)又會(huì)把FileInputStream關(guān)閉。所以實(shí)際業(yè)務(wù)代碼里你只需要關(guān)最外層的流整個(gè)鏈條的資源就都會(huì)被釋放。如果非要從內(nèi)層往外層手動(dòng)關(guān)反而會(huì)出現(xiàn)問題比如先關(guān)FileInputStream再調(diào)用外層流的read()就會(huì)拋IOException因?yàn)榈讓拥淖止?jié)輸入源頭已經(jīng)斷了。在Java 7之后最好的做法其實(shí)是try-with-resources上面示例代碼就是這個(gè)寫法。它會(huì)保證在多資源對(duì)象按逆序自動(dòng)關(guān)閉不需要自己在finally里寫一堆close()判斷。我建議項(xiàng)目中新寫的代碼一律用try-with-resources既簡(jiǎn)潔又不容易泄漏資源。5. 亂碼排查與避坑實(shí)錄5.1 常見亂碼問題速查表整理一下我在工作中最常遇到的亂碼現(xiàn)象、原因和對(duì)策這張表可以收藏備用現(xiàn)象可能原因解決思路文件打開是亂碼“錕斤拷”、“燙燙燙”讀取用的字符集和文件實(shí)際編碼不一致先確認(rèn)文件真實(shí)編碼再改用正確字符集解碼中文字符變成“??”寫入字符集不支持中文常見于ISO-8859-1寫入流顯式指定UTF-8或GBK中文變成“???”等怪符號(hào)UTF-8字節(jié)被當(dāng)成GBK或Latin-1解碼統(tǒng)一讀取和寫入的字符集控制臺(tái)亂碼但文件本身正常IDE或控制臺(tái)默認(rèn)編碼問題檢查IDE文件編碼、控制臺(tái)輸出編碼設(shè)置文件開頭出現(xiàn)單獨(dú)亂碼字符UTF-8文件帶BOM且代碼沒處理解碼后剝離\uFEFF某些字符變成“”替換符字節(jié)序列非法Decoder默認(rèn)替換用正確編碼讀取或用CodingErrorAction.REPORT定位5.2 排查亂碼的正確思路排查亂碼的第一原則不要靠猜先確認(rèn)文件真實(shí)編碼。在Windows下可以用Notepad或VS Code打開文件看右下角編碼提示在Linux下用file -i命令file -i input.csv輸出結(jié)果類似text/plain; charsetgbk或text/plain; charsetutf-8。如果拿不到類似信息可以用十六進(jìn)制工具查看文件頭或者某段文本的字節(jié)內(nèi)容再對(duì)照字符集的編碼規(guī)則判斷。比如UTF-8編碼的“你”在十六進(jìn)制下是E4 BD A0GBK編碼下是C4 E3。確認(rèn)源文件編碼之后再看JVM啟動(dòng)時(shí)的默認(rèn)編碼??梢詫懸恍写a打印System.out.println(Charset.defaultCharset());這一步能幫你判斷是不是代碼沒顯式指定字符集而讓JVM默認(rèn)值去代勞了。很多時(shí)候排查到這一步就會(huì)發(fā)現(xiàn)問題所在。5.3 我踩過的幾次坑與調(diào)整記錄第一個(gè)坑是參數(shù)傳遞導(dǎo)致的編碼丟失。早期項(xiàng)目里有人把從接口拿到的文件名直接拼到路徑里然后用FileInputStream讀文件再丟給一個(gè)沒有指定字符集的InputStreamReader。本地開發(fā)Windows默認(rèn)GBK沒問題部署到Linux就亂碼。那次之后我定了個(gè)規(guī)矩凡是構(gòu)造字符流強(qiáng)制要求顯式傳入Charset參數(shù)代碼評(píng)審時(shí)發(fā)現(xiàn)無參構(gòu)造直接打回。第二個(gè)坑是嵌套流轉(zhuǎn)發(fā)時(shí)的誤編碼。有一次把某個(gè)二進(jìn)制文件上傳到對(duì)象存儲(chǔ)先用InputStreamReader包裝了InputStream再轉(zhuǎn)成字符串保存。結(jié)果文件上傳后下載下來完全打不開。問題就在于對(duì)象存儲(chǔ)需要的是原始字節(jié)流而我多此一舉把字節(jié)解碼成字符再編碼回字節(jié)中間走了個(gè)不需要的Charset轉(zhuǎn)換內(nèi)容雖然沒丟但對(duì)二進(jìn)制文件來說完全是多余的。后來我就把這類接口全部改成直接傳InputStream不再做字符轉(zhuǎn)換。第三個(gè)坑和CodingErrorAction有關(guān)。默認(rèn)情況下Decoder遇到非法字節(jié)會(huì)把問題字符替換成\uFFFD程序不報(bào)錯(cuò)數(shù)據(jù)悄悄變臟。如果數(shù)據(jù)質(zhì)量要求高建議自定義解碼策略InputStreamReader reader new InputStreamReader( new FileInputStream(data.txt), Charset.forName(UTF-8).newDecoder() .onMalformedInput(CodingErrorAction.REPORT) );這樣設(shè)置之后一旦碰到非法字節(jié)序列讀取過程會(huì)直接拋出MalformedInputException而不是默默替換。雖然可能讓程序中斷但至少不會(huì)在不知情的情況下把臟數(shù)據(jù)寫進(jìn)數(shù)據(jù)庫。5.4 給新人的幾個(gè)實(shí)用建議最后分享幾條我這些年攢下來的經(jīng)驗(yàn)可能比上面的代碼更值錢第一寫工具類時(shí)務(wù)必把Charset作為參數(shù)暴露出來不要自己在內(nèi)部寫死字符集也不要用默認(rèn)值。這樣同一個(gè)工具類既能處理UTF-8也能處理GBK將來換需求不用改代碼。第二讀取文本后如果要做字符串處理盡量一次性完成不要多次反復(fù)解碼編碼。每經(jīng)過一次“字節(jié)-字符-字節(jié)”的轉(zhuǎn)換都可能引入未知風(fēng)險(xiǎn)尤其是遇到特殊字符時(shí)。第三如果項(xiàng)目是基于Spring Boot的注意配置文件里的server.servlet.encoding和HTTP請(qǐng)求響應(yīng)的編碼設(shè)置。很多Web項(xiàng)目里的亂碼并不是IO流的問題而是HTTP報(bào)文頭里沒有正確聲明Content-Type的charset兩者容易混淆排查時(shí)要先分清層次。第四能不用FileReader就不用FileReader。這個(gè)類在設(shè)計(jì)上確實(shí)沒有提供指定字符集的構(gòu)造方法在生產(chǎn)代碼里用它的唯一后果就是讓編碼變得不可控。InputStreamReader多寫幾個(gè)字母換來的確定性是完全值得的。字節(jié)流與字符流的轉(zhuǎn)換表面上就是一個(gè)橋接模式的應(yīng)用真正難的是理解你手上這份數(shù)據(jù)到底是誰、從哪來、要往哪去。編碼一致亂碼四散編碼錯(cuò)位千奇百怪。把這些原理吃透以后遇到任何亂碼問題排查起來都會(huì)快很多。