java程序性能优化第三章

String 类的3个基本的特点:1.不变性;2.针对常量池的优化3.类的final定义
String对象的内部结构:1.char数组2.offset偏移3.count长度
图(暂略)
不变性:String对象一旦生成不能再对其做出改变,String这个特性可以泛化成不变模式。不变模式的主要作用在于当一个对象需要被多线程共享,并且频繁访问时,可以省略同步和锁等待的时间,从而大幅提高系统性能。不变模式是一个可以提高多线程程序的性能、降低多线程复杂度的设计模式。
针对常量池的优化:当两个String对象拥有相同的值时,它们只引用常量池中的同一个拷贝。当一个字符串反复出现时,可以大幅度的节省内存空间。
String str1 = “abc”;
String str2 = “abc”;
String str3 = new String(“abc”);
System.out.println(str1 == str2);//返回true
System.out.println(str1 == str3);//返回false
System.out.println(str1 == str3.intern());//返回true
图:(暂略)
类的final定义:作为final类的String对象在系统中不可能有任何子类,这是对系统安全性的保护。
subString()方法存在内存泄漏:
public String subString(int beginIndex,int endIndex){
if(beginIndex < 0){
throw new StringIndexOutOfBoundsException(beginIndex);
}
if(endIndex > count){
throw new StringIndexOutOfBoundsException(endIndex);
}
return ((beginIndex == 0)&&(endIndex == count))? this :
new String(offset + beginIndex,endIndex - beginIndex,value);

在方法的最后返回了一个新建的String对象。查看该对象的构造函数:
//Package private constructor which shares value array for speed.
String(int offset,int count,char value[]){
this.value = value;
this.offset = offset;
this.count = count;
}

这一包作用域的构造函数其目的就是为了能高效且快速地共享String内的char数组对象,但在这种通过偏移量来截取字符串的方法中,String的原生内容value数组被复制到新的子字符串中,这种以时间换空间的策略浪费了内存空间提高了字符串的生成速度。
实例代码:

public static void main(String[]args){
List<Sting>handler = new ArrayList<String>();
/**
 *HugeStr不到1000次就会内存溢出
 *但是ImprovedHuge却不会
 */
 for(int i = 0;i < 1000;i++){
 //HugeStr str = new HugeStr(); //抛内存溢出异常
 ImprovedHugeStr str =  new ImprovedHugeStr();
 handler.add(h.getSubString(1,5));
   }
 }
 static class HugeStr{
 private String str = new String(new char[100000]);
 public String getSubString(int beginIndex,int endIndex){
 return str.subString(beginIndex,endIndex);
  }
 }
 static class ImprovedHugeStr{
 private String str = new String(new char[100000]);
 public String getSubString(int beginIndex,int endIndex){
 return new String(str.subString(beginIndex,int endIndex));
  }
 }

subString是一个包内私有的构造函数,也就是说应用程序无法使用它。但我们也要担心java.lang包内的对象对它的调用是否会引起内存泄漏。
下面是一些使用subString()构造函数的方法:
1.Integer.toString(int);
2.Long.toString(long);
3.String: concat(String),replace(char,char),subString(int,int),toLowerCase(Locale),toUpperCase(Locale),valueOf(char)

chapter3.13

“a;b,c:d”.split(“[; | , | :]”);
对简单字符串的分割性能较差,在性能敏感的系统中频繁的使用这个方法是不可取的。
使用效率更高的StringTokenizer类分割字符串
StringTokenizer类是JDK中提供的专门用来处理字符串分割子串的工具类,构造函数如下:
public StringTokenizer(String str,String delim);
其中str是要分隔处理的字符串,delim是分隔符号;

//orgStr是由;分割的一连串数字
StringTokenizer str = new StringTokenizer(orgStr,";");
for(int i = 0;i < 10000;i++){
while(st.hasMoreTokens()){
st.nextToken();
 }
 st = new StringTokenizer(orgStr,";");
 }

StringTokenizer是出于兼容性的原因而被保留的遗留类,建议使用java.util.regex
自定义分割字符串的方法:

String str = orgStr;
for(int i = 0;i < 10000;i++){
while(true){
String splitStr = null;
int j = tmp.indexOf(';');
if(j < 0) break;
splitStr = tmp.subString(0,j);
tmp = tmp.subString(j + 1);
}
tmp = orgStr;
}

三种方法splite方法功能强大效率最差,StringTokenizer性能稍优于splite方法但现已基本不用,自己实现的方法性能最好,但不易维护,只有当系统性能问题成为主要矛盾时,才推荐使用该方法。
charAt方法的性能要高于startsWith()和endsWith();
由于String对象是不可变的对象,因此需要对字符串进行修改操作时(如字符串的连接、替换),String对象总是会生成新的对象,所以性能较差。JDK专门提供了创建和修改字符串的工具,这就是StringBuffer和StringBuider类。

String result = "String" + "and" + "String"+"append";
StringBuilder result = new StringBuilder();
result.append("String");
result.append("and");
result.append("String");
result.append("append");

第一段代码前两个字符串连接生成一个新的字符串,然后再依次与后面的字符串连接。但Java对第一段代码在编译时就做了充分的优化,对于这些在编译时就能确定的取值的字符串的操作,在编译时就做了计算,因此并没有想象中的生成了大量的String实例。所以第一段字符串的效率才会与预期的相反,执行效率比第二段快。
对于静态字符串的连接操作,java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。

String str1 = "String";
String str2 = "and";
String str3 = "String";
String str4 = "append";
String result = str1  + str2 + str3 + str4;
String str1 = "String";
String str2 = "and";
String str3 = "String";
String str4 = "append";
String s = (new StringBuilder(String.valueof(str1))).append(str2).append(str3).append(str4).toString();

两段代码的执行效率几乎一样,由此可见Java对变量字符串的累加也做了相应的优化操作,使用了StringBuilder对象来实现字符串的累加。

构建超大的String对象

代码A:
for(int i = 0;i < 10000;i++){
str = str + i;
}
代码段B:
for(int i = 0;i < 10000;i++){
result = result.concat(String.valueOf(i));
}
代码段C:
StringBuilder str = new StringBuilder();
for(int i = 0;i < 10000;i++){
str.append(i);
}
代码C的耗时最短几乎0ms,代码B的次之,代码A最长。
看代码A的反编译代码:

for(int i = 0;i < CIRCLE;i++){
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

说明编译器不太够聪明,每次都生成了新的StringBuilder实例。
StringBuilder和StringBuffer是一对孪生兄弟,它们都实现了AbstractStringBuilder抽象类,拥有几乎相同的对外接口,两者最大的不同在于StringBuffer对几乎所有的方法都做了同步,而StringBuilder并没有做任何同步。同步需要耗时,而StringBuilder无法保证线程安全,在多线程中不能使用。
无论是StringBuffer还是StringBuilder,在初始化时都可以设置一个容量参数。对应的构造函数如下:
public StringBuilder(int capacity);
public StringBuffer(int capacity);
在不指定容量参数时,默认是16个字节。此参数指定了StringBuilder和StringBuffer的初始大小。
AbstractStringBuilder(int capacity){
value = new char[capacity];
}
在容量不够时需要扩容:
void expandCapicity(int minimumCapacity){
int newCapacity = (value.length + 1) * 2;
if(newCapacity < 0){
newCapaity = Integer.MAX_VALUE;
}else if(minimumCapacity > newCapacity){
newCapacity = minimumCapicity;
}
value = Arrays.copyof(value,newCapicity);
}
扩容的策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的char数组,然后将原数组的内容复制到这个新的数组中,因此对于大对象的扩容会涉及到大量的内存复制的操作。所以如果能预先评估StringBuilder的大小,将能够有效地节省这些操作,从而提高系统的性能。

3.2核心数据结构

JDK提供了一组数据结构实现如List、Map、Set等都继承自java.util.Collection接口,并位于java.util包内。
List有三种实现:ArrayList、Vector和LinkedList
一、List接口
类图关系如下所示:
(暂略)
三种List均来自AbstractList的实现,而AbstactorList直接实现了List接口并扩展自AbstractCollection.
在这三种不同的实现中,ArrayList和Vector使用了数组实现,可以认为,ArrayList或者Vector封装了对内部数组的操作。比如向数组中添加、删除、插入新的元素或者数组的扩展和重定义。对ArrayList或者Vector操作相当于对内部对象数据的操作。
ArrayList和Vector几乎是用了相同的算法。它们唯一的区别就是对多线程的支持。ArrayList没有对任何一个方法做线程同步,因此不是线程安全的。从理论上说没有实现线程同步的ArrayList要稍好于Vector,但实际表现并不是非常明显。
LinkedList使用了循环双向链表的数据结构。与基于数组工作的List相比,这两种截然不同的实现技术决定了它们将适用于不同的工作场景。
LinkedList列表项的组成:1.元素内容2.驱表项3.后驱表项
在JDK的实现中无论LinkedList是否为空,链表内都有一个header表项,它既表示链表的开始,也表示链表的结尾。表项header的后驱表项便是链表的第一个元素,表项header的前驱表项便是链表的最后一个元素。

增加元素到列表尾端

在ArrayList中增加元素到队列的尾端的代码如下:
public boolean add(E e){
ensureCapacity(size + 1);//确保内部数组有足够的空间
elementData[size++] = e;
return true;
}
ArrayList中add()方法的性能取决于ensureCapacity()方法。其实现如下:

public void ensureCapacity(int minCapacity){
modCount++;
int oldCapacity = elementData.length;
if(minCapacity > oldCapacity){
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3) / 2 + 1;//扩容到原始容量的1.5倍
if(newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyof(elementData,newCapacity);
}
}

数组复制时,最终将调用System.arraycopy()方法,因此,add()操作的效率还是相当高的。

public boolean add(E e) {
addBefore(e, header);//将元素增加到header的前面
return true;
}

header元素的前驱正是List中的最后一个元素;因此将元素增加到header之前,就相当于在List的最后插入该元素。其中addBefore()方法实现如下:

private Entry<E> addBefore(E e,Entry<E> entry){
Entry< E> newEntry = new Entry<E>(e,entry,entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}

LinkedList由于使用了链表的结构,因此不需要维护容量的大小。它比ArrayList有一定的性能优势。然而,每次元素的增加都需要新建一个Entry对象,并进行更多的赋值操作。
分别使用ArrayList和LinkedList运行以下代码(-Xmx512M -Xms512M):

Object obj = new Object();
for(int i = 0;i < 500000;i++){ //循环50万次
list.add(obj);
}

使用-Xmx512M -Xms512M的目的是屏蔽GC对程序执行速度测量的干扰。不间断的生成新的对象还是占用了一定的系统资源。而因为数组的连续性,因此总是在尾端增加元素时,只有在空间不足时才产生数组扩容和数组复制,所以绝大部分的追加操作效率是非常高的。
若不使用-Xmx512M -Xms512M参数,而使用JVM的默认堆大小,ArrayList和LinkedList在本次测试中的性能差异会更大(分别相对耗时78ms和266ms),可见,使用LinkedList对堆内存和GC的要求更高。

增加元素到列表任意位置

除了提供了增加元素到List的尾端,List接口还提供了在任意位置插入元素的方法。

void add(int index,E element);

由于实现上的不同,ArrayList和LinkedList在这个方法上存在一定的性能的差异。由于ArrayList是基于数组实现的,而数组是一块连续的内存空间,如果在数组的任意位置插入元素,必然会导致在该位置后的所有元素需要重新排列,因此,其效率相对较低。
以下代码是ArrayList中的实现:

public void add(int index,E element) {
if(index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index :" + index + ", Size: "+size);
ensureCapacity(size + 1);
System.arraycopy(elementData,index,elementData,index + 1,size - index);
elementData[index] = element;
size++;
}

可以看到,每次插入操作,都会进行一次数组复制。而这个操作在增加元素到List尾端的时候是不存在的。大量的数组重组操作会导致系统性能低下。并且,插入的元素在List中的位置越靠前,数组重组的开销也越大。尽量将元素插入到List的尾端附近。

public void add(int index,E element) {
addBefore(element,(index==size ?header : entry(index)));
}

如果在系统应用中,List对象需要经常在任意的位置插入元素,则可以考虑用LinkedList代替ArrayList

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值