你可能也會掉進這個簡單的 String 的坑

點擊上方藍色字體,關注我 ——
java

一個在阿里雲打工的清華學渣!程序員


圖 by:石頭@阿里巴巴飛天園區web

關於做者:程序猿石頭(ID: tangleithu),現任阿里巴巴技術專家,清華學渣,前大疆後端 Leader。歡迎關注,交流和指導!回覆  「0」 送阿里技術大禮包。

背景

石頭同窗是某大公司高級開發工程師,某日收到很多錯誤告警信息,因而便去開始排查。面試

跟蹤日誌發現是某個服務拋出的異常信息,奇怪的是這個服務上線也有一段時間了。以前不多看到相似的錯誤信息,最近偶爾多了起來。算法

後來才定位到是由於服務調用了某外部接口,發現對方對參數長度作了限制,若是輸入參數超過 1000 bytes,就直接拋異常,代碼相似以下:後端

/**
 * @param status
 * @param result, the size should less than 1000 bytes
 * @throws Exception
 */

public XXResult(boolean status, String result) {
    if (result != null && result.getBytes().length > 1000) {
        throw new RuntimeException("result size more than 1000 bytes!");
    }
  ......
}

心想,這還不簡單,我們的 result 也不是什麼關鍵性的東西,你有限制,我直接 trim 一下不就好了?數組

解決方案

因而三下五除二,給搞了個 trim 方法,支持傳不一樣參數按需 trim,代碼以下:微信

/**
 * 將給定的字符串 trim 到指定大小
 * @param input
 * @param trimTo 須要 trim 的字節長度
 * @return trim 後的 String
 */

public static String trimAsByte(String input, int trimTo) {
    if (Objects.isNull(input)) {
        return null;
    }
    byte[] bytes = input.getBytes();
    if (bytes.length > trimTo) {
        byte [] subArray = Arrays.copyOfRange(bytes, 0, trimTo);
        return new String(subArray);
    }
    return input;
}

再在須要調用外部服務的地方,先調用這個 trimAsByte 方法,一頓操做連忙上線,一切完美~app

災難現場

一切完美,石頭哥也是這樣認爲的。而後幸福老是短暫的。less

通過一段時間後(前面也提到,業務場景確實是偶發的),相同的錯誤仍然發生了。

簡直不敢相信,都 trim 了爲啥還會超出?你也幫忙想一想,是哪裏的問題?


看看上面的例子(爲了方便展現,簡單修改文首代碼了下),

trimAsByte("WeChat:tangleithu"8)

輸入字符串 WeChat:tangleithu 太長了,只 trim 到剩下 8 個字節,對應的字節數組是從 [87,101,67,104,97,116,58,116,97,110,103,108,101,105,116,104,117] 變爲了 [87,101,67,104,97,116,58,116],字符串變成了 WeChat:t ,結果正確。

其實在寫這個方法的時候仍是太草率了,本應該很容易想到中文的狀況的,咱們來試試:

trimAsByte("程序猿石頭"8)

看上述截圖,悲劇了,輸入程序猿石頭,3 個字節一個漢字,一共 15 個字節 [-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76],trim 到 8 位,剩下前 8 位 [-25,-88,-117,-27,-70,-113,-25,-116] 也正確。再 new String變成3 個 「中文」 了,雖然第 3 個「中文」,咱也不認識,咱也不敢問到底讀啥,總之再轉換成字節數組,長度多了 1 個,變成 9 了。

問題算是定位到了。

不由要問,爲何?

來看看這個 String 的構造函數,看看上面註釋才發現,其實咱們忽略了一個很重要的概念,就是編碼方式。

/**
 * Constructs a new {@code String} by decoding the specified array of bytes
 * using the platform's default charset.  The length of the new {@code
 * String} is a function of the charset, and hence may not be equal to the
 * length of the byte array.
 *
 * <p> The behavior of this constructor when the given bytes are not valid
 * in the default charset is unspecified.  The {@link
 * java.nio.charset.CharsetDecoder} class should be used when more control
 * over the decoding process is required.
 *
 * @param  bytes
 *         The bytes to be decoded into characters
 *
 * @since  JDK1.1
 */

public String(byte bytes[]) {
    //this(bytes, 0, bytes.length);
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

當咱們用默認的構造函數 new String 的時候,只是用了系統默認的編碼(本文是「UTF-8」)去嘗試解碼,構造出字符串。

因此,當咱們在用字節數組(字節流)來表達具體的語義的時候,必定要約定好以什麼方式進行編碼,本文不具體闡述編碼問題了。下面用一個例子來解釋上文的現象:

[-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76] 仍然用這串字節數組來實驗,這串字節數組,若是用 「UTF-8」 編碼去解釋,那麼其想表達的語義就是中文「程序猿石頭」,從上文標註的 1,2,3 中能夠看出來,沒有寫即用了系統中的默認編碼「UTF-8」。

假設按照 「GBK」 來解釋(標註 4),就是表達的 「紼嬪簭鐚跨煶澶�」,注意看下其中的  是否是似曾相識;

注意標註 5,經過 GBK 解釋構造字符串後,再經過默認的 「UTF-8」 獲取字節數組,長度就變成 24 了,而後還經過 「GBK」 編碼獲得的字節數組長度爲 15(標註 6),再試圖構造字符串(標註 7),其中「程序猿石頭」的「頭」字,已經沒了。說明這個轉換過程當中,其實信息已經被丟了。

上面的  實際上是 UNICODE 編碼方式中的一個特殊的字符,也就是 0xFFFD(65535),實際上是一個佔位符(REPLACEMENT CHARACTER),用來表達未知的、沒辦法表達的東東。上文中在進行編碼轉換過程當中,出現了這個玩意,其實也就是沒辦法準確表達含義,會被替換成這個東西,所以信息也就丟失了。你能夠試試前面的例子,好比把前 8 個字節中的最後一兩個字節隨便改改,都是同樣的。

程序猿石頭:65533 示例

總結

總結一下,其實原本是一個很簡單的問題,卻通過幾回修改才最終解決,說明對 「基礎」 掌握得仍是不夠,一個重要的點是,在處理二進制數據的時候,必定要聯想到 「編碼」 方式。

另外,提醒咱們,看似簡單的問題,咱們每每容易忽略。好比若是單純看到文中提到的這個trim 方法,其實很容易寫個單元測試就能儘早發現有問題;

越是基礎的方法,咱們越應該考慮其代碼的健壯性,在以前的 從一道面試題談談一線大廠碼農應該具有的基本能力 中,我也談到了寫單元測試、測試用例的重要性。

後記

以爲本號分享的文章有價值,記得添加星標哦。周更很累,不要白 piao,須要來點正反饋,安排個 「一鍵三連」(點贊、在看、分享)如何?😝 這將是我持續輸出優質文章的最強動力。


推 薦 閱 讀



程序猿石頭 


程序猿石頭(ID: tangleithu),現任阿里巴巴技術專家,清華學渣,前大疆後端 Leader。用不一樣的視角分享高質量技術文章,以每篇文章都讓人有收穫爲目的,歡迎關注,交流和指導!掃碼回覆關鍵字 「1024」 獲取程序員大廠面試指南


本文分享自微信公衆號 - 程序猿石頭(tangleithu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索