字符串优化处理
通常字符串对象或者与其等价的对象(char[]数组),在内存中,总是占据了最大的空间块,因此如何高效的处理字符串,必是提高系统整体性能的关键所在。
String对象是java语言中重要的数据类型,但它并不是java基本的数据类型。在C语言中,对字符串的处理最通常的做法是使用char数组,但这种方式的弊端是显而易见的,数组本身无法封装字符串操作所需的基本方法。而在java语言中,String对象可以认为是char数组的延伸和进一步封装。
java中String类的基本实现,主要由三个部分组成,char数组、偏移量和String长度,char数组表示String的内容,它是String对象所表示字符串的超集。String的真实内容还需要由偏移量和长度在这个char数组中进行定位和截取。
在java语言中,java的设计者对String对象进行了大量的优化,其主要表现在以下三个方面,同时这也是String对象的三个基本特点:
1.不变性
2.针对常量池的优化
3.类的final定义
不变性:
String对象一旦生成,则不能再对它进行改变,String的这个特性可以泛化成不变模式,即一个对象的状态在对象被创建之后就不再发生变化,不变模式的主要作用在于,当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁的等待时间从而大幅提高系统性能。
针对常量池优化:
针对常量池的优化指:当两个String对象拥有相同的值,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
public class Demo {
public static void main(String[] args) throws Exception {
String str1="abc";
String str2="abc";
String str3=new String("abc");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str1==str3.intern());
}
}
结果:
true
false
true
str1和str2引用了相同的引用地址,但是str3缺开辟了一块内存空间。str3在常量池中的位置和str1一样,
也就是说,虽然str3单独占用了堆空间,但是它所指定的实体和str1完全一样。(栈空间存储变量引用,堆
空间存储实例)。用intern()方法,该方法返回了String对象在常量池中的引用:
类的final定义:
除了以上两点,final类型定义也是String对象的重要特点。作为final类的String对象,在系统中不可能有任何子类,这是对系统安全性的保护。
字符串的分割和查找
根据某个字符串切割成一组小字符串:
public String[] split(String regex)
也可以使用正则表达式将字符串进行分割成一组小字符串。
public class Demo {
public static void main(String[] args) throws Exception {
String str="a,b:c;d";
String arr[]=str.split("[,|:|;]");
System.out.println(Arrays.toString(arr));
}
}
String.Split()方法使用简单,功能强大,但是在敏感的系统中频繁使用这个方法是不可取的。
使用效率更高的StringTokenizer类分割字符串
StringTokenizer是JDK专门处理字符串子类的工具类,构造函数如下:
public StringTokenizer(String str,String delim)
其中str参数是要分割处理的字符串,delim是分割符号。当一个StringTokenizer对象生成后,通过它的nextToken()方法便可以得到下一个分割的字符串。通过hasMoreTokens()方法可以知道是否有更多的子字符串需要处理。
public class Demo {
public static void main(String[] args) throws Exception {
String str="a;b;c;d";
StringTokenizer st=new StringTokenizer(str,";");
while(st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
}
}
结果:
a
b
c
d
更优化的字符串分割方式:
我们需要使用String的两个方法indexOf()和subString(),subString是采用时间换取空间的技术,因此它的执行速度相对会很快。indexOf是一个执行速度非常快的方法。
public class Demo {
public static void main(String[] args) throws Exception {
String str="a;b;c;d";
while(true) {
int num=str.indexOf(";");
if(num<0) {
System.out.println(str);
break;
}else {
System.out.println(str.substring(0, num));
str=str.substring(num+1);
}
}
}
}
结果:
a
b
c
d
高效率的charAt()方法:
cahrAt()方法返回指定位置的字符。
我们判断某个字符串的开头或者结尾,会用StartsWith()和endsWith()方法,现在我们单纯的使用charAt()来判断
public class Demo {
public static void main(String[] args) throws Exception {
String str="java is best language";
int len=str.length();
if(str.charAt(0)=='j'&&
str.charAt(1)=='a'&&
str.charAt(2)=='v'&&
str.charAt(3)=='a') {
System.out.println("字符串java成功匹配");
}
if(str.charAt(len-1)=='e'&&
str.charAt(len-2)=='g'&&
str.charAt(len-3)=='a') {
System.out.print("字符串age成功匹配");
}
}
}
结果:
字符串java成功匹配
字符串age成功匹配
StringBufffer和StringBuilder
String字符串对于常量字符串的累加,并没有效率的不足,因为java在编译的时候就做了充足的优化。
public class Demo {
public static void main(String[] args) throws Exception {
long n=System.currentTimeMillis();
for(int i=0;i<50000;i++) {
String str1="aaaaaaaaa"+"bbbbbbb"+"cccccccccc"+"ddddddd";
}
System.out.println(System.currentTimeMillis()-n);
long n1=System.currentTimeMillis();
for(int i=0;i<50000;i++) {
StringBuffer sb=new StringBuffer();
sb.append("aaaaaaaaa");
sb.append("bbbbbbb");
sb.append("cccccccccc");
sb.append("ddddddd");
}
System.out.println(System.currentTimeMillis()-n1);
}
}
结果:
0
15
可以从结果看出来,String相比StringBuffer要快。
这就是因为java编译器优化了String的字符串,因为能确定字符串的值。
String变量的累加操作。
public class Demo {
public static void main(String[] args) throws Exception {
long n=System.currentTimeMillis();
for(int i=0;i<50000;i++) {
String a="aaaaaaaaa";
String b="bbbbbbb";
String c="cccccccccc";
String d="ddddddd";
String str=a+b+c+d;
}
System.out.println(System.currentTimeMillis()-n);
long n1=System.currentTimeMillis();
for(int i=0;i<50000;i++) {
StringBuffer sb=new StringBuffer();
sb.append("aaaaaaaaa");
sb.append("bbbbbbb");
sb.append("cccccccccc");
sb.append("ddddddd");
}
System.out.println(System.currentTimeMillis()-n1);
}
}
结果:
27
17
这是String效率就不行了,因为编译器无法在运行时确定String字符串的具体取值。
虽然数值相差没有想象的大,那是因为累加的数量不够,而且在实际使用时,对于多次累加操作,我们应该尽量
使用StringBuffer或者StringBuilder,因为我们要使用对象优化提升性能,而不是依靠编译器对程序进行
优化。
StringBuffer和StringBuilder选择:
StringBuffer几乎所有的方法都使用了同步操作,所以线程安全,但是也导致了效率没有StringBuilder效率高。选择条件就是线程或者效率之间的抉择。
StringBuffer和StringBuilder在初始化的时候都可以设置一个长度容量参数,设置这个值的好处是:如果可以预见或者估测你的存放大小,预先设置容量值,这样避免了它们容量不够时的自动扩容,因为扩容是将原有的大小翻倍,以新的容量申请内存空间,建立新的char数组,然后将原有数组中的内容赋值到这个新的数组中。因此对于大对象的扩容会涉及大量的内存复制操作。
自动扩容的源码
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}