1、不可变的String类
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char[] value;
private int hash;
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
}
以上为JDK8的String的定义,String是一个final修饰的类,表示String不可以被继承,上面的有参构造方法显示String内部是用char数组来保存字符串的,并且char数组也是用final修饰的,被final修饰的变量只能赋值一次,所以String的值是不能被修改的。String的substring和replace等方法都是返回一个新String对象。
2、为什么String不可变
1)、保证hashcode的一致性,hashcode可以被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键
2)、类加载器要用到字符串,如果字符串是可修改的会改变加载器需要加载的类,
3)、安全问题:譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接的,如果字符串是可变的,黑客可以更改字符串的指向对象的值,会造成安全漏洞
4)、字符串的不可变保证了字符串常量池的实现
3、String真的不可变吗?
//创建字符串
String s1 = "abc";
//反射获得value成员变量
Field field = s1.getClass().getDeclaredField("value");
//改变vlaue的访问属性
field.setAccessible(true);
//jdk8中String的value是char[],jdk9改为了byte[]
//获取s1对象的value
byte[] chars = (byte[])field.get(s1);
//改变数组的第一个值
chars[0]='b';
System.out.println(s1); //输出bbc
代码中s1一直指向的是同一个引用,内部的value是final修饰的,是不能二次赋值的,但是数组的值的引用是可以更改的,这里把value数组的第一个值从a改为b,所以输出就是bbc;
4、String的常量池
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = "a";
String s5 = "bc";
String s6 = s4 + s5;
String s7 = s4 + "bc";
String s8 = "a" + "bc";
System.out.println(s1 == s2); //true
System.out.println(s1 == s3); //false
System.out.println(s1 == s6); //false
System.out.println(s1 == s7); //false
System.out.println(s1 == s8); //true
s1和s2都指向常量池中的abc,所以相等,s3是在堆中创建了一个abc的对象,所以是s1==s3是fasle,s4+s5因为是引用相加是创建了新的对象,所以s6==s1是false,s7同理,s8=a+bc;再编译时会优化为s8=abc;所以还是指向常量池中的abc,因此s1==s8是true。
5、String的intern方法
String str1 = new String("intern")+ new String("方法"); //1
System.out.println(str1.intern() == str1); //2
System.out.println(str1 == "intern方法"); //3
jdk6:false false jdk7:true true jdk8:true true
第一行中,在常量池创建了“intern”和“方法”两个常量。在堆中创建“intern方法”的对象,
第二行中,jdk6,常量池在方法区,所以str1.intern方法会在常量池创建一个“intern方法”,然后返回它的引用;而在jdk7中,常量池是在堆中,str1.intern方法会直接在常量池存储堆中str1的引用,然后返回引用。在jdk8,常量池已经被移到metaspace中,是直接存在内存中的,和jdk7一样,str1.intern方法会直接在常量池存储str1的引用,然后返回引用。
第三行中,同理第二行。