Java超長String處理遇到的一些問題記錄

目錄

問題描述

String類型

拼接超長字符串

截取超長字符串

總結

參考文獻


問題描述

因爲項目的需要,封裝的SM4的加密、解密工具包,最近出了問題,客戶反饋說現場有一個15M大小錄音文件,在進行加密和解密的時候,方法沒有反應,調用超時,失敗了。按照最初封裝時的考慮,沒想過需要加密的入參字符串會有那麼大,所以也沒有考慮這種情況,今天拿到測試樣例數據之後,通過讀文件和寫文件的方式進行了驗證,最終發現並不是加密的算法有問題,也不是方法不能正常執行,而是整個過程中部分代碼對String的處理效率低造成的,當對String處理的循環超過100w長度時,需要幾個小時才能處理完。通過Debug找到耗時點之後,下面記錄下改造內容。

String類型

String類型到底能存儲多長的內容?這取決於你怎麼用。編譯階段和運行階段String內存可存儲的內容長度是不一樣的。參考這篇博文的描述:

編譯階段:

在我們使用字符串字面量直接定義String的時候,會把字符串在常量池中存儲一份。常量池中的每一項常量都是一個表,都有自己對應的類型。String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值,注意:該表只存儲文字字符串值,不存儲符號引用。

JVM的常量池最多可放65535個項。第0項不用。最後一項最多隻能是65534(下標值)。而每一項中,若是放一個UTF-8的常量串,其長度最長是:65535個字節(不是字符)。

運行時階段:

String內部是以char數組的形式存儲,數組的長度是int類型,那麼String允許的最大長度就是Integer.MAX_VALUE了,2147483647;又由於java中的字符是以16位存儲的,因此大概需要4GB的內存才能存儲最大長度的字符串。

也就是說,如果你定義一個String類型的常量,它的長度最多不能超過65535個字節。這裏插播一下不同編碼下英文字符和中文對應字節的個數不同,參考這篇博文。我們以UTF-8編碼爲例,全英文字符的話,最大隻能定義65535長度的字符。

但是如果是在運行階段,給String類型的變量賦值,例如:我們從文件裏讀取文件的內容,將文件的內容賦值給String變量,這時候屬於運行階段,運行階段String可存儲的內容就長很多,理論上可以存儲4G大小的內容。

拼接超長字符串

先看下出現耗時的問題代碼,代碼如下:

    public static String sm4Eecode(String strs) throws Exception{
        int mm1[] = getInput(strs);
        mm1 = encode(mm1);
        String rstr="";
        for(int i=0;i< mm1.length;i++){
            rstr+ =  StringUtils.leftPad(Integer.toHexString(mm1[i]), 8, '0');
        }
        return rstr;
    }

mm1是加密後的int數組,這個加密過程很快,我們重點看下for循環,乍一看,這段代碼沒什麼問題,就是通過循環把數組裏的內容轉換成16進制的字符串,然後拼接到一起。如果mm1的長度超過100w,這塊for循環的代碼就會執行好幾個小時。問題出在哪裏呢?rstr的內容每次都會重新改變和調整,隨着字符串長度的變化,每次都要在內存中重新創建,這是極其耗時的。對於字符串的拼接,我們一般使用StringBuffer或者StringBuilder。StringBuffer和StringBuilder最大區別在於StringBuffer是線程安全的,但是效率比StringBuilder低一些。這裏不涉及多線程,改造後的代碼如下:

   public static String sm4Eecode(String strs) throws Exception{
        int mm1[] = getInput(strs);
        mm1 = encode(mm1);
        StringBuilder sb = new StringBuilder();
        for(int i=0;i< mm1.length;i++){
            String str =  StringUtils.leftPad(Integer.toHexString(mm1[i]), 8, '0');
            sb.append(str);
        }
        return sb.toString();
    }

改造之後,處理速度有非常明顯的提升,100w循環基本上2秒之內就能處理完。

截取超長字符串

在對加密後內容進行解密的時候,涉及到字符串的截取,每次取8位。通過調試發現這塊也是影響處理性能耗時的點之一,先看下問題代碼,如下:

    private static int[] getOutPut(String contents) {
        int len = contents.length()/8;
        int pv = contents.length()%8;
        int rt[];
        if(pv>0){
             rt = new int[len+1];
        }else{
             rt = new int[len];
        }

        for(int i=0;i<len;i++){
            String     st = contents.substring(0,8);
            BigInteger bi = new BigInteger(st, 16);
            rt[i] = bi.intValue();
            contents=contents.substring(8);
        }
        if(!"".equals(contents) && contents.length()>0){
            BigInteger bi = new BigInteger(contents, 16);
            rt[len] = bi.intValue();
        }
        return rt;
    }

問題也是出在for循環的地方,這個地方是通過substring的方式,每次調整字符串的長度,每次去掉已處理開頭的8個字符,將剩餘的字符串重新賦值給contents。這裏遇到的問題和上面基本上是一樣,我一開始以爲是substring的性能問題,其實不是,主要還是因爲String的內容每次都在調整,每次都會重新申請內存空間,這是極其耗時的。這塊可以通過調整substring的being和end下標,持續從contents裏取值,而不改變content內容的方式來做,改造後代碼如下:

   private static int[] getOutPut(String contents) {
        int len = contents.length()/8;
        int pv = contents.length()%8;
        int rt[];
        if(pv>0){
            rt = new int[len+1];
        }else{
            rt = new int[len];
        }

        int ib=0;
        int ie=8;
        for(int i=0;i<len;i++){
            String     st = contents.substring(ib,ie);
            BigInteger bi = new BigInteger(st, 16);
            rt[i] = bi.intValue();
            ib=ib+8;
            ie=ie+8;
        }
        if(rt.length>len){
            String str = contents.substring(ib);
            BigInteger bi = new BigInteger(str, 16);
            rt[len] = bi.intValue();
        }
        return rt;
    }

改造後,執行效率提升明顯。

總結

Java中對String的操作,在涉及到循環或者賦值頻率比較高的時候,一定要注意,String類型的變量內容發生變化後,每次都會進行新的內存空間申請,如果過於頻繁,這會嚴重影響性能。

參考文獻

【1】java中String類型的最大長度

【2】一個字符佔幾個字節

【3】java 字符串String的最大長度

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章