1. 看看源码
大家都知道, 被声明为 final,因此它不可被继承。( 等包装类也不能被继承)。我们先来看看 的源码。
在 Java 8 中, 内部使用 char 数组存储数据。
final class
java.io., , {
/** The value is used for . */
final char value[];
在 Java 9 之后, 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
final class
java.io., , {
/** The value is used for . */
final byte[] value;
/** The of the used to the bytes in {@code value}. */
final byte coder;
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 内部没有改变 value 数组的方法,因此可以保证 不可变。
2. 不可变有什么好处呢
2.1 可以缓存 hash 值
因为 的 hash 值经常被使用,例如 用做 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2.2 Pool 的使用
如果一个 对象已经被创建过了,那么就会从 Pool 中取得引用。只有 是不可变的,才可能使用 Pool。
2.3 安全性
经常作为参数, 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 是可变的,那么在网络连接过程中, 被改变,改变 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
2.4 线程安全
不可变性天生具备线程安全,可以在多个线程中安全地使用。
3. 再来深入了解一下
3.1 “+” 连接符
字符串对象可以使用“+”连接其他对象,其中字符串连接是通过 (或 )类及其 方法实现的,对象转换为字符串是通过 方法实现的。可以通过反编译验证一下:
/**
* 测试代码
*/
class Test {
void main([] args) {
int i = 10;
s = “abc”;
.out.(s + i);
/**
* 反编译后
*/
class Test {
void main( args[]) { //删除了默认构造函数和字节码
byte byte0 = 10;
s = “abc”;
.out.((new ()).(s).(byte0).());
由上可以看出,Java中使用”+”连接字符串对象时,会创建一个()对象,并调用()方法将数据拼接,最后调用()方法返回拼接好的字符串。那这个 “+” 的效率怎么样呢?
3.2 “+”连接符的效率
使用“+”连接符时,JVM会隐式创建对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如:
s = “abc”;
for (int i=0; i
s += “abc”;
这样由于大量创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个对象调用()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。
与此之外还有一种特殊情况,也就是当”+”两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如:
.out.(“Hello” + “World”);
/**
* 反编译后
*/
.out.(“”);
4. 字符串常量
4.1 为什么使用字符串常量?
JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于字符串的不可变性,常量池中一定不存在两个相同的字符串。
4.2 实现字符串常量池的基础
实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。
运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。
我们来看个小例子,了解下不同的方式创建的字符串在内存中的位置:
= “abc”; // 常量池
= “abc”; // 常量池
= new (“abc”); // 堆内存
5. 类常见的面试题
5.1 判断字符串s1和s2是否相等
void main([] args) {
s1 = “123”;
s2 = “123”;
s3 = “1234”;
s4 = “12” + “34”;
s5 = s1 + “4”;
s6 = new (“1234”);
.out.(s1 == s2); // true
.out.(s1.(s2)); //true
.out.(s3 == s4); //true
.out.(s3 == s5); // false
.out.(s3.(s5)); //true
.out.(s3 == s6); // false
解析:
s1 = “123”;先是在字符串常量池创建了一个字符串常量“123”,“123”常量是有地址值,地址值赋值给s1。接着声明 s2=“123”,由于s1已经在方法区的常量池创建字符串常量”123″,进入常量池规则:如果常量池中没有这个常量,就创建一个,如果有就不再创建了,故直接把常量”123″的地址值赋值给s2,所以s1==s2为true。
由于类重写了方法,s1.(s2)比较的是字符串的内容,s1和s2的内容都是”123″,故s1.(s2)为true。
s3创建了一个新的字符串”1234″,s4是两个新的字符串”12″和”34″通过”+“符号连接所得,根据Java中常量优化机制, “12” 和”34″两个字符串常量在编译期就连接创建了字符串”1234”,由于字符串”1234″在常量池中存在,故直接把”1234″在常量池的地址赋值给s4,所以s3==s4为true。
s5是由一个变量s1连接一个新的字符串”4″,首先会在常量池创建字符串”4″,然后进行”+“操作,根据字符串的串联规则,s5会在堆内存中创建(或)对象,通过方法拼接s1和字符串常量”4”,此时拼接成的字符串”1234″是(或)类型的对象,通过调用方法转成对象”1234″,所以s5此时实际指向的是堆内存中的”1234″对象,堆内存中对象的地址和常量池中对象的地址不一致,故s3==s5为false。
看下JDK8的API文档里的解释:
Java语言为字符串连接运算符(+)提供特殊支持,并为其他对象转换为字符串。字符串连接是通过 (或 )类及其方法实现的。字符串转换是通过方法来实现,由下式定义和继承由在Java中的所有类。有关字符串连接和转换的其他信息,请参阅,Joy 和,Java 语言规范。
不管是常量池还是堆,只要是使用比较字符串,都是比较字符串的内容,所以s3.(s5)为true。
Java常量优化机制:给一个变量赋值,如果等于号的右边是常量,并且没有一个变量,那么就会在编译阶段计算该表达式的结果,然后判断该表达式的结果是否在左边类型所表示范围内,如果在,那么就赋值成功,如果不在,那么就赋值失败。但是注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制。
s6 = new (“1234”);在堆内存创建一个字符串对象,s6指向这个堆内存的对象地址,而s3指向的是字符串常量池的”1234″对象的地址,故s3==s6为false。
5.2 创建多少个字符串对象?
s0 = “123”;
s1 = new (“123”);
s2 = new (“1” + “2”);
s3 = new (“12”) + “3”;
解析:
字符串常量池对象:“123”,1个;
共1个。
字符串常量池对象:“123”,1个;
堆对象:new (“123”),1个;
共2个。
字符串常量池对象:“12”,1个(Jvm在编译期做了优化,“1” + “2”合并成了 “12”);
堆对象:new (“12”),1个
共2个。
由于s2涉及字符串合并,我们通过命令看下字节码信息:
javac .java //编译源文件得到class文件
javap -c .class // 查看编译结果
得到字节码信息如下:
可以看到,包括在内,共创建了4个对象,字符串”12″和字符串”3″是分开创建的,所以共创建了3个字符串对象。
总结:
new ()是在堆内存创建新的字符串对象,其构造参数中可传入字符串,此字符串一般会在常量池中先创建出来,new ()创建的字符串是参数字符串的副本,看下API中关于构造器的解释:
( )
初始化新创建的对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。
所以new ()的方式创建字符串百分百会产生一个新的字符串对象,而类似于”123″这样的字符串对象则需要在创建之前看常量池中有没有,有的话就不创建,没有则创建新的对象。 “+”操作符连接字符串常量的时候会在编译期直接生成连接后的字符串,若该字符串在常量池已经存在,则不会创建新的字符串;连接变量的话则涉及等字符串构建器的创建,会在堆内存生成新的字符串对象。
———END———
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,永久会员只需109元,全站资源免费下载 点击查看详情
站 长 微 信: nanadh666