String
一.字符串是什么
Java字符串其实就是Unicode字符序列,例如“abc\u2122”是由四个Unicode字符 a、b、c和 ™ 组成。Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类String。每个用双引号括起来的字符串都是String类的一个实例。
String s = ""; // 没有内容的字符串实例
String s2 = "Hello";
二.字符串的存储原理
由于需要通过String类来定义和操作字符串,因此它属于引用数据类型,它的存储原理也符合一般引用数据类型的存储原理。
String s = new String("Hello"); // 标准构造对象方式
上例中,代码 new String(“Hello”) 在堆内存中开辟了两个字节的内存空间,创建了一个内容是“Hello“的字符串实例A。系统还在栈内存中创建了一个String类型的引用s,指向堆内存中的字符串实例A。
String s = “Hello”; // 字面量形式
而这种方式创建字符串实例的过程是这样的。首先会到字符串常量池中看是否有内容是“Hello“的字符串实例,没有则创建一个,返回引用,有则直接返回引用。
可见,不同于一般的引用型数据类型,Java系统对于字符串使用了系统级别的缓存处理。
有关字符串常量池的内容会另起章节详细讲解。
三.不可改变的字符串
1. “不可改变”的含义
String类被设计成不可变类(immutable),就是其所有对象都是不可改变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个新的String对象,用以放置改变值后的字符串内容,而最初的String对象则丝毫未变。
String s = "ABC";
s.toLowerCase();
toLowerCase()方法只是另外创建了一个字符串对象“abc”,原本的对象“ABC”仍然存在。
2. 为什么是不可改变的
首先,只有字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多堆内存空间,因为不同的字符串变量都指向池中的同一个字符串对象。如果字符串可以改变,那某变量改变了它的值,其他指向该值的变量都会得到错误的值。
由于字符串对象是不可变的,那么拷贝内容时,就只需要复制它的地址,而不需要复制它本身。复制地址只需要很小的内存,通常只有一个指针大小。
不可变对象对于多线程是安全的。并发环境下,一个可变对象的值很可能被其他线程改变,这会造成不可预期的后果,但使用不可变对象就可以避免这种情况。
字符串的不可变性还可以避免一些安全问题。比如数据库的用户名密码都是以字符串的形式传入来获取数据库连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。如果字符串的值可以改变,那黑客就可以改变字符串指向的对象的值,造成安全漏洞。
四.字符串的相加和比较
1. 字符串相加的本质
在Java中,程序员并不允许重载操作符,而用于String的符号“+”和“+=”是仅有的两个被重载过的操作符。
使用符号“+”可以连接字符串。如果你反编译程序的代码会发现,系统会创建StringBuilder,使用其append()方法构造拼接后的字符串,然后通过toString()方法返回结果。需要注意的是,StringBuilder的toString()方法,是通过调用String的构造方法创建字符串对象。
String s1 = "abc";
String s2 = "a";
String s3 = "bc";
String s4 = s2 + s3;
s1和s4是不是同一个字符串对象?答案是否,它们内容一样,都是“abc”,s1是字面量形式,指向的对象在常量池中,而s4是拼接时通过StringBuilder的toString()方法创建的新的字符串对象,指向的对象在堆内存中。
如果都是字符常量的相加呢?
String s = "a" + "b" + "c" + "d";
思考一下上面这句代码创建了几个字符串对象。你可能要掰着手指说有四五六个,其实只有一个。原因是编译器进行了优化,它在编译期碰到字符串常量直接相加的表达式的话,就去掉其中的加号,直接将其编译成一个相连的结果“abcd”,而不必等到运行期再做加法运算处理。
2. 字符串的比较
字符串的比较又分为字符的比较和字符串对象的比较。
String的equals()方法可以比较字符串的内容。而如果需要判断两个字符串变量指向的对象是否是同一个的话,就需要使用“==”操作符。对于引用类型,“==”比较的是两个变量指向的内存地址。
String a = new String("hello");
String b = new String("hello");
System.out.println(a.equals(b)); // 比较内容
System.out.println(a == b); // 比较内存地址
/** 输出:true false */
字符串a和b的内容是相同的,都是“hello”,但它们是通过构造函数的方式,分别在堆内存中创建的字符串对象,因此不是同一个对象,内存地址也不相同。
五.字符串的使用
1. 字符串的操作方法

总的来说,如果字符串的内容需要改变的话,会创建一个新的字符串对象,修改内容并返回;而如果内容不需要改变,则返回原本对象的引用。这样可以减少内存耗用。
2. intern()方法的使用
intern()方法一般很少用到,但它的作用很重要。
文档注释对intern()方法作用的描述:如果常量池中存在当前字符串, 就会直接返回当前字符串;如果常量池中没有此字符串, 会将此字符串放入常量池中后再返回。
我们知道,用new String() 的方式声明的字符串,是通过构造函数在堆内存中创建的字符串对象。因此在程序运行时需要处理上百万条字符数据时,如果都通过构造函数的方式动态创建字符串对象,那会耗费大量堆内存空间,甚至导致内存溢出。而如果使用 new String().intern() 的方式,会复制字符串对象到常量池中,这样创建同样的字符串时就直接从常量池中获取引用,而不用再在堆空间中创建对象。这样可以大大减少内存消耗,提高效率。
要注意的是,字符串常量池是一个固定大小的Hashtable,默认长度是1009,如果放进常量池的字符串非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后的直接影响就是当调用String.intern时性能会大幅下降,因为要一个一个找。
在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=10240
六.StringBuffer和StringBuilder
StringBuffer、StringBuilder和String一样,可以用来存储和操作字符序列。但它们和String最大的区别,就是对内容的修改,都是修改对象自己,而不是像String一样重新创建新的对象。
从这点看,如果需要频繁地对字符串做追加、插入、替换等操作的话,使用StringBuffer和StringBuilder更能减少内存消耗。
StringBuffer是线程安全的,而StringBuilder则不是。因此如果在非并发环境下,使用StringBuilder速度更快。
浙公网安备 33010602011771号