目录
问题描述
String类型
拼接超长字符串
截取超长字符串
总结
参考文献
因为项目的需要,封装的SM4的加密、解密工具包,最近出了问题,客户反馈说现场有一个15M大小录音文件,在进行加密和解密的时候,方法没有反应,调用超时,失败了。按照最初封装时的考虑,没想过需要加密的入参字符串会有那么大,所以也没有考虑这种情况,今天拿到测试样例数据之后,通过读文件和写文件的方式进行了验证,最终发现并不是加密的算法有问题,也不是方法不能正常执行,而是整个过程中部分代码对String的处理效率低造成的,当对String处理的循环超过100w长度时,需要几个小时才能处理完。通过Debug找到耗时点之后,下面记录下改造内容。
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;i0){
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;ilen){
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的最大长度