文档章节

Java 程序优化:字符串操作、基本运算方法等优化策略(一)

M
 Mysoft
发布于 2015/09/25 11:00
字数 2790
阅读 59
收藏 3

针对 Java 程序编写过程中的实际问题,本文分为两部分,首先对字符串相关操作、数据切分、处理超大 String 对象等提出解决方案及优化建议,并给出具体代码示例;然后对数据定义、运算逻辑优化等方面提出解决方案及优化建议,并给出具体代码示例。 由于本文所尝试的实验都是基于联想 L430 笔记本,i5-3320CPU,4GB 内存基础上的,其他机器上运行代码可能结果有所不同,请以自己的实验环境为准。

字符串操作优化

字符串对象

字符串对象或者其等价对象 (如 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. 示例代码
public class StringDemo {
 public static void main(String[] args){
 String str1 = "abc";
 String str2 = "abc";
 String str3 = new String("abc");
 String str4 = str1;
 System.out.println("is str1 = str2?"+(str1==str2));
 System.out.println("is str1 = str3?"+(str1==str3));
 System.out.println("is str1 refer to str3?"+(str1.intern()==str3.intern()));
 System.out.println("is str1 = str4"+(str1==str4));
 System.out.println("is str2 = str4"+(str2==str4));
 System.out.println("is str4 refer to str3?"+(str4.intern()==str3.intern()));
 }
}

输出如清单 2 所示。

清单 2. 输出结果
is str1 = str2?true
is str1 = str3?false
is str1 refer to str3?true
is str1 = str4true
is str2 = str4true
is str4 refer to str3?true

SubString 使用技巧

String 的 substring 方法源码在最后一行新建了一个 String 对象,new String(offset+beginIndex,endIndex-beginIndex,value);该行代码的目的是为了能高效且快速地共享 String 内的 char 数组对象。但在这种通过偏移量来截取字符串的方法中,String 的原生内容 value 数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空 间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了速度却浪费了空间。

下面代码演示了使用 substring 方法在一个很大的 string 独享里面截取一段很小的字符串,如果采用 string 的 substring 方法会造成内存溢出,如果采用反复创建新的 string 方法可以确保正常运行。

清单 3.substring 方法演示
import java.util.ArrayList;
import java.util.List;

public class StringDemo {
 public static void main(String[] args){
 List<String> handler = new ArrayList<String>();
 for(int i=0;i<1000;i++){
 HugeStr h = new HugeStr();
 ImprovedHugeStr h1 = new ImprovedHugeStr();
 handler.add(h.getSubString(1, 5));
 handler.add(h1.getSubString(1, 5));
 }
 }
 
 static class HugeStr{
 private String str = new String(new char[800000]);
 public String getSubString(int begin,int end){
 return str.substring(begin, end);
 }
 }
 
 static class ImprovedHugeStr{
 private String str = new String(new char[10000000]);
 public String getSubString(int begin,int end){
 return new String(str.substring(begin, end));
 }
 }
}

输出结果如清单 4 所示。

清单 4. 输出结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.StringValue.from(Unknown Source)
at java.lang.String.<init>(Unknown Source)
at StringDemo$ImprovedHugeStr.<init>(StringDemo.java:23)
at StringDemo.main(StringDemo.java:9)

ImprovedHugeStr 可以工作是因为它使用没有内存泄漏的 String 构造函数重新生成了 String 对象,使得由 substring() 方法返回的、存在内存泄漏问题的 String 对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。

String 的 split 方法支持传入正则表达式帮助处理字符串,但是简单的字符串分割时性能较差。

对比 split 方法和 StringTokenizer 类的处理字符串性能,代码如清单 5 所示。

切分字符串方式讨论

String 的 split 方法支持传入正则表达式帮助处理字符串,操作较为简单,但是缺点是它所依赖的算法在对简单的字符串分割时性能较差。清单 5 所示代码对比了 String 的 split 方法和调用 StringTokenizer 类来处理字符串时性能的差距。

清单 5.String 的 split 方法演示
import java.util.StringTokenizer;


public class splitandstringtokenizer {
 public static void main(String[] args){
 String orgStr = null;
 StringBuffer sb = new StringBuffer();
 for(int i=0;i<100000;i++){
 sb.append(i);
 sb.append(",");
 }
 orgStr = sb.toString();
 long start = System.currentTimeMillis();
 for(int i=0;i<100000;i++){
 orgStr.split(",");
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 String orgStr1 = sb.toString();
 StringTokenizer st = new StringTokenizer(orgStr1,",");
 for(int i=0;i<100000;i++){
 st.nextToken();
 }
 st = new StringTokenizer(orgStr1,",");
 end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 String orgStr2 = sb.toString();
 String temp = orgStr2;
 while(true){
 String splitStr = null;
 int j=temp.indexOf(",");
 if(j<0)break;
 splitStr=temp.substring(0, j);
 temp = temp.substring(j+1);
 }
 temp=orgStr2;
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

输出如清单 6 所示:

清单 6. 运行输出结果
39015
16
15

当一个 StringTokenizer 对象生成后,通过它的 nextToken() 方法便可以得到下一个分割的字符串,通过 hasMoreToken 方法可以知道是否有更多的字符串需要处理。对比发现 split 的耗时非常的长,采用 StringTokenizer 对象处理速度很快。我们尝试自己实现字符串分割算法,使用 substring 方法和 indexOf 方法组合而成的字符串分割算法可以帮助很快切分字符串并替换内容。

由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。

以 上实例运行结果差异较大的原因是 split 算法对每一个字符进行了对比,这样当字符串较大时,需要把整个字符串读入内存,逐一查找,找到符合条件的字符,这样做较为耗时。而 StringTokenizer 类允许一个应用程序进入一个令牌(tokens),StringTokenizer 类的对象在内部已经标识化的字符串中维持了当前位置。一些操作使得在现有位置上的字符串提前得到处理。 一个令牌的值是由获得其曾经创建 StringTokenizer 类对象的字串所返回的。

清单 7.split 类源代码
import java.util.ArrayList;


public class Split {
public String[] split(CharSequence input, int limit) { 
int index = 0; 
boolean matchLimited = limit > 0; 
ArrayList<String> matchList = new ArrayList<String>(); 
Matcher m = matcher(input); 
// Add segments before each match found 
while(m.find()) { 
if (!matchLimited || matchList.size() < limit - 1) { 
String match = input.subSequence(index, m.start()).toString(); 
matchList.add(match); 
index = m.end(); 
} else if (matchList.size() == limit - 1) { 
// last one 
String match = input.subSequence(index,input.length()).toString(); 
matchList.add(match); 
index = m.end(); 
} 
} 
// If no match was found, return this 
if (index == 0){ 
return new String[] {input.toString()}; 
}
// Add remaining segment 
if (!matchLimited || matchList.size() < limit){ 
matchList.add(input.subSequence(index, input.length()).toString()); 
}
// Construct result 
int resultSize = matchList.size(); 
if (limit == 0){ 
while (resultSize > 0 && matchList.get(resultSize-1).equals("")) 
resultSize--; 
 String[] result = new String[resultSize]; 
 return matchList.subList(0, resultSize).toArray(result); 
}
}


}

split 借助于数据对象及字符查找算法完成了数据分割,适用于数据量较少场景。

合并字符串

由 于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。针对超大的 String 对象,我们采用 String 对象连接、使用 concat 方法连接、使用 StringBuilder 类等多种方式,代码如清单 8 所示。

清单 8. 处理超大 String 对象的示例代码
public class StringConcat {
 public static void main(String[] args){
 String str = null;
 String result = "";
 
 long start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 str = str + i;
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 result = result.concat(String.valueOf(i));
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 StringBuilder sb = new StringBuilder();
 for(int i=0;i<10000;i++){
 sb.append(i);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

输出如清单 9 所示。

清单 9. 运行输出结果
375
187
0

虽然第一种方法编译器判断 String 的加法运行成 StringBuilder 实现,但是编译器没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例从而大大降低了系统性能。

StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎相同的对外借口,两者的最大不同在于 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有任何同步。由于方法同步需要消耗一定的系统资源,因此,StringBuilder 的效率也好于 StringBuffer。 但是,在多线程系统中,StringBuilder 无法保证线程安全,不能使用。代码如清单 10 所示。

清单 10.StringBuilderVSStringBuffer
public class StringBufferandBuilder {
public StringBuffer contents = new StringBuffer(); 
public StringBuilder sbu = new StringBuilder();

public void log(String message){ 
for(int i=0;i<10;i++){ 
/*
contents.append(i); 
contents.append(message); 
contents.append("\n"); 
*/
contents.append(i);
contents.append("\n");
sbu.append(i);
sbu.append("\n");
} 
} 
public void getcontents(){ 
//System.out.println(contents); 
System.out.println("start print StringBuffer");
System.out.println(contents); 
System.out.println("end print StringBuffer");
}
public void getcontents1(){ 
//System.out.println(contents); 
System.out.println("start print StringBuilder");
System.out.println(sbu); 
System.out.println("end print StringBuilder");
}

 
 
 public static void main(String[] args) throws InterruptedException { 
StringBufferandBuilder ss = new StringBufferandBuilder(); 
runthread t1 = new runthread(ss,"love");
runthread t2 = new runthread(ss,"apple");
runthread t3 = new runthread(ss,"egg");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
 
}

class runthread extends Thread{ 
String message; 
StringBufferandBuilder buffer; 
public runthread(StringBufferandBuilder buffer,String message){ 
this.buffer = buffer;
this.message = message; 
} 
public void run(){ 
while(true){ 
buffer.log(message); 
//buffer.getcontents();
buffer.getcontents1();
try {
sleep(5000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} 
} 


}

输出结果如清单 11 所示。

清单 11. 运行结果
start print StringBuffer
0123456789
end print StringBuffer
start print StringBuffer
start print StringBuilder
01234567890123456789
end print StringBuffer
start print StringBuilder
01234567890123456789
01234567890123456789
end print StringBuilder
end print StringBuilder
start print StringBuffer
012345678901234567890123456789
end print StringBuffer
start print StringBuilder
012345678901234567890123456789
end print StringBuilder

StringBuilder 数据并没有按照预想的方式进行操作。StringBuilder 和 StringBuffer 的扩充策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。如果能够预先评估大小,会提高性能。

本文转载自:http://www.ibm.com/developerworks/cn/java/j-lo-basic-types/index.html

M
粉丝 4
博文 58
码字总数 24535
作品 0
广州
高级程序员
私信 提问
05《Java核心技术》之三种字符串类有什么区别?

一、提出问题 今天,我们来聊聊日常使用的字符串,别看它似乎很简单,但其实字符串几乎在所有编程语言里都是个特殊的存在,因为不管是数量还是体积,字符串都是大多数应用中的重要组成。 今天...

飞鱼说编程
2018/09/24
52
0
一起学Java7新功能扩展——深入历险分享(一)

特此声明:因网友疑问,这里声明一个重要的安全,就是大家所知的java惊现0day漏洞!8月30日,Oralce紧急发布了新版本的JDK和JRE,原因是发现了一个严重的0day漏洞CVE-2012-4681,远程攻击者可...

Beyond-Bit
2012/09/03
4.2K
26
JVM参数调优:Eclipse启动实践

JVM参数调优:Eclipse启动实践 本文主要参考自《深入理解 Java 虚拟机》。 这本书是国人写的难得的不是照搬代码注释的且不是废话连篇的技术书,内容涵盖了 Java 从源码到字节码到执行的整个过...

天天顺利
2015/01/13
280
0
编写高性能 Java 代码的最佳实践

摘要:本文首先介绍了负载测试、基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践。最后研究了JVM特定的调优技巧、数据库端的优化和架构方面的调整。以下是译...

这篇文章
2018/06/20
0
0
Java程序员进化为架构师需要掌握的知识

Java程序员进化为架构师掌握的知识: 一:Java知识 1、进制转换 2、Java基本数据类型 面向对象相关知识 3、类、接口、抽象类 this关键字、static关键字、final关键字 方法的参数传递机制 Ja...

andogo
2014/05/16
1K
3

没有更多内容

加载失败,请刷新页面

加载更多

centos7 安装 mysql5.7 版本(全)

centos 安装 版本说明 :centos7,mysql5.7 ,不是 centos7 可能有些命令不兼容 安装 mysql-server # 下载并安装 mysql yum wget -i -c http://dev.mysql.com/get/mysql57-community-relea......

sanri1993
26分钟前
3
0
Spring3.x升级到Spring4.x-5.x时关于MappingJacksonHttpMessageConverter的报错问题

在Spring4.x或者以上的版本强使用(不然会报错): org.springframework.http.converter.json.MappingJackson2HttpMessageConverter 如果是Spring4.0获者以下的版本可以使用MappingJacksonH...

code-ortaerc
28分钟前
3
0
OSG 渲染状态污染到其它节点怎么解决?

在根节点补上初始状态

洛克人杰洛
30分钟前
3
0
grid 布局 设置行列间距

本文转载于:专业的前端网站➪grid 布局 设置行列间距 <!DOCTYPE html><html lang="zh"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="widt......

前端老手
42分钟前
3
0
spring-data-elasticsearch 和 Jackson 配合使用的bug

下面先简单描述项目。 项目依赖: dependencies { implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-elasticsearch', version: '2.1.0.RELEASE'......

Landas
43分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部