概述
字符串操作优化
字符串对象
字符串对象或者其等价对象 (如 char 数组),在内存中总是占据最大的空间块,因此如何高效地处理字符串,是提高系统整体性能的关键。
String 对象可以认为是 char 数组的延伸和进一步封装,它主要由 3 部分组成:char 数组、偏移量和 String 的长度。char 数组表示 String 的内容,它是 String 对象所表示字符串的超集。String 的真实内容还需要由偏移量和长度在这个 char 数组中进行定位和截取。
String 有 3 个基本特点:
1. 不变性;
2. 针对常量池的优化;
3. 类的 final 定义。
不变性指的是 String 对象一旦生成,则不能再对它进行改变。String 的这个特性可以泛化成不变 (immutable) 模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。
针对常量池的优化指的是当两个 String 对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
下面代码 str1、str2、str4 引用了相同的地址,但是 str3 却重新开辟了一块内存空间,虽然 str3 单独占用了堆空间,但是它所指向的实体和 str1 完全一样。代码如下清单 1 所示。
清单 1. 示例代码
输出如清单 2 所示。
清单 2. 输出结果
SubString 使用技巧
String 的 substring 方法源码在最后一行新建了一个 String 对象,new String(offset+beginIndex,endIndex-beginIndex,value);该行代码的目的是为了能高效且快速地共享 String 内的 char 数组对象。但在这种通过偏移量来截取字符串的方法中,String 的原生内容 value 数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了速度却浪费了空间。
下面代码演示了使用 substring 方法在一个很大的 string 独享里面截取一段很小的字符串,如果采用 string 的 substring 方法会造成内存溢出,如果采用反复创建新的 string 方法可以确保正常运行。
清单 3.substring 方法演示
输出结果如清单 4 所示。
清单 4. 输出结果
ImprovedHugeStr 可以工作是因为它使用没有内存泄漏的 String 构造函数重新生成了 String 对象,使得由 substring() 方法返回的、存在内存泄漏问题的 String 对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。
String 的 split 方法支持传入正则表达式帮助处理字符串,但是简单的字符串分割时性能较差。
对比 split 方法和 StringTokenizer 类的处理字符串性能,代码如清单 5 所示。
切分字符串方式讨论
String 的 split 方法支持传入正则表达式帮助处理字符串,操作较为简单,但是缺点是它所依赖的算法在对简单的字符串分割时性能较差。清单 5 所示代码对比了 String 的 split 方法和调用 StringTokenizer 类来处理字符串时性能的差距。
清单 5.String 的 split 方法演示
输出如清单 6 所示:
清单 6. 运行输出结果
当一个 StringTokenizer 对象生成后,通过它的 nextToken() 方法便可以得到下一个分割的字符串,通过 hasMoreToken 方法可以知道是否有更多的字符串需要处理。对比发现 split 的耗时非常的长,采用 StringTokenizer 对象处理速度很快。我们尝试自己实现字符串分割算法,使用 substring 方法和 indexOf 方法组合而成的字符串分割算法可以帮助很快切分字符串并替换内容。
由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
以上实例运行结果差异较大的原因是 split 算法对每一个字符进行了对比,这样当字符串较大时,需要把整个字符串读入内存,逐一查找,找到符合条件的字符,这样做较为耗时。而 StringTokenizer 类允许一个应用程序进入一个令牌(tokens),StringTokenizer 类的对象在内部已经标识化的字符串中维持了当前位置。一些操作使得在现有位置上的字符串提前得到处理。 一个令牌的值是由获得其曾经创建 StringTokenizer 类对象的字串所返回的。
清单 7.split 类源代码
split 借助于数据对象及字符查找算法完成了数据分割,适用于数据量较少场景。
合并字符串
由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。针对超大的 String 对象,我们采用 String 对象连接、使用 concat 方法连接、使用 StringBuilder 类等多种方式,代码如清单 8 所示。
清单 8. 处理超大 String 对象的示例代码
虽然第一种方法编译器判断 String 的加法运行成 StringBuilder 实现,但是编译器没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例从而大大降低了系统性能。
StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎相同的对外借口,两者的最大不同在于 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有任何同步。由于方法同步需要消耗一定的系统资源,因此,StringBuilder 的效率也好于 StringBuffer。 但是,在多线程系统中,StringBuilder 无法保证线程安全,不能使用。代码如清单 10 所示。
清单 10.StringBuilderVSStringBuffer
StringBuilder 数据并没有按照预想的方式进行操作。StringBuilder 和 StringBuffer 的扩充策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。如果能够预先评估大小,会提高性能。
数据定义、运算逻辑优化
使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。
清单 12. 局部变量 VS 静态变量
以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。
位运算代替乘除法
位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。
清单 14. 算数运算
两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。
替换 switch
关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。
清单 16.Switch 示例
运行输出如清单 17 所示。
清单 17. 运行结果
1
2
172
93
使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。
一维数组代替二维数组
JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量将二维数组转化为一维数组再进行处理,以提高系统的响应速度。
清单 18. 数组方式对比
最后
以上就是苹果墨镜为你收集整理的java string 截取字节_用java 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串?...的全部内容,希望文章能够帮你解决java string 截取字节_用java 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串?...所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复