java.lang.String 类可能是大家日常用的最多的类,但是对于它是怎么实现的,你真的明白吗? 认真阅读这篇文章,包你一看就明白了。
String 类定义
publicfinalclassStringimplementsjava.io.Serializable,Comparable<String>,CharSequence{}
从源码可以看出,String 是一个用 final 声明的常量类,不能被任何类所继承,而且一旦一个String对象被创建,包含在这个对象中的字符序列是不可改变的,包括该类后续的所有方法都是不能修改该对象的,直至该对象被销毁,这是我们需要特别注意的(该类的一些方法看似改变了字符串,其实内部都是创建一个新的字符串,下面讲解方法时会介绍)。接着实现了 Serializable接口,这是一个序列化标志接口,还实现了 Comparable 接口,用于比较两个字符串的大小(按顺序比较单个字符的ASCII码),后面会有具体方法实现;最后实现了 CharSequence 接口,表示是一个有序字符的集合,相应的方法后面也会介绍。
字段属性
/**用来存储字符串*/privatefinalcharvalue[];/**缓存字符串的哈希码*/privateinthash;//Defaultto0/**实现序列化的标识*/privatestaticfinallongserialVersionUID=-6849794470754667710L;
一个 String 字符串实际上是一个 char 数组。
构造方法
String 类的构造方法很多。可以通过初始化一个字符串,或者字符数组,或者字节数组等等来创建一个 String 对象。
Stringstr1="abc";//注意这种字面量声明的区别,文末会详细介绍Stringstr2=newString("abc");Stringstr3=newString(newchar[]{'a','b','c'});
equals(Object anObject) 方法
publicbooleanequals(ObjectanObject){if(this==anObject){returntrue;}if(anObjectinstanceofString){StringaString=(String)anObject;if(coder()==aString.coder()){returnisLatin1()?StringLatin1.equals(value,aString.value):StringUTF16.equals(value,aString.value);}}returnfalse;}
String 类重写了 equals 方法,比较的是组成字符串的每一个字符是否相同,如果都相同则返回true,否则返回false。
hashCode() 方法
publicinthashCode(){inth=hash;if(h==0&&value.length>0){hash=h=isLatin1()?StringLatin1.hashCode(value):StringUTF16.hashCode(value);}returnh;}
String 类的 hashCode 算法很简单,主要就是中间的 for 循环,计算公式如下:
s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
s 数组即源码中的 val 数组,也就是构成字符串的字符数组。这里有个数字 31 ,为什么选择31作为乘积因子,而且没有用一个常量来声明?主要原因有两个:
1、31是一个不大不小的质数,是作为 hashCode 乘子的优选质数之一。
2、31可以被 JVM 优化,31 * i = (i « 5) - i。因为移位运算比乘法运行更快更省性能。
charAt(int index) 方法
publiccharcharAt(intindex){if(isLatin1()){returnStringLatin1.charAt(value,index);}else{returnStringUTF16.charAt(value,index);}}
我们知道一个字符串是由一个字符数组组成,这个方法是通过传入的索引(数组下标),返回指定索引的单个字符。
intern() 方法
这是一个本地方法:
public native String intern();
当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)
的字符串,则返回该字符串。否则,将此String对象添加到池中,并返回此对象的引用。
这句话什么意思呢?就是说调用一个String对象的intern()
方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中);如果没有,则将该对象添加到池中,并返回池中的引用。
Stringstr1="hello";//字面量只会在常量池中创建对象Stringstr2=str1.intern();System.out.println(str1==str2);//trueStringstr3=newString("world");//new关键字只会在堆中创建对象Stringstr4=str3.intern();System.out.println(str3==str4);//falseStringstr5=str1+str2;//变量拼接的字符串,会在常量池中和堆中都创建对象Stringstr6=str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象System.out.println(str5==str6);//trueStringstr7="hello1"+"world1";//常量拼接的字符串,只会在常量池中创建对象Stringstr8=str7.intern();System.out.println(str7==str8);//true
关于String类里面的众多方法,这里不一一介绍了,下面我们来深入了解一下,String 类不可变型。
面试精选
分析一道经典的面试题:
publicstaticvoidmain(String[]args){StringA="abc";StringB="abc";StringC=newString("abc");System.out.println(A==B);System.out.println(A.equals(B));System.out.println(A==C);System.out.println(A.equals(C));}
答案是:true、true、false、true
对于上面的题目,我们可以先来看一张图,如下:
首先 String A= “abc”,会先到常量池中检查是否有“abc”的存在,发现是没有的,于是在常量池中创建“abc”对象,并将常量池中的引用赋值给A;第二个字面量 String B= “abc”,在常量池中检测到该对象了,直接将引用赋值给B;第三个是通过new关键字创建的对象,常量池中有了该对象了,不用在常量池中创建,然后在堆中创建该对象后,将堆中对象的引用赋值给C,再将该对象指向常量池。
需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,这也就是为什么A.equals(C)返回true的原因了。
注意:看上图红色的箭头,通过 new 关键字创建的字符串对象,如果常量池中存在了,会将堆中创建的对象指向常量池的引用。
再来看一道题目,使用包含变量表达式创建对象:
Stringstr1="hello";Stringstr2="helloworld";Stringstr3=str1+"world";//编译器不能确定为常量(会在堆区创建一个String对象)Stringstr4="hello"+"world";//编译器确定为常量,直接到常量池中引用System.out.println(str2==str3);//fasleSystem.out.println(str2==str4);//trueSystem.out.println(str3==str4);//fasle
str3 由于含有变量str1,编译器不能确定是常量,会在堆区中创建一个String对象。而str4是两个常量相加,直接引用常量池中的对象即可。
String 不可变性
String类是Java中的一个不可变类(immutable class)。
简单来说,不可变类就是实例在被创建之后不可修改。
String不可变这个话题应该是老生长谈了,String自打娘胎一出生就跟他们的兄弟姐妹不一样,好好的娃被戴了一个final的帽子,
以至于byte,int,short,long等基本类型的小伙们都不带它玩。
如果你仔细阅读源码注释,你会发现这样一句话:
大致意思就是String是个常量,从一出生就注定不可变。
首先需要补充一个容易混淆的知识点:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。
String 类是用 final 关键字修饰的,所以我们认为其是不可变对象。但是真的不可变吗?
每个字符串都是由许多单个字符组成的,我们知道其源码是由char[] value
字符数组构成。
/**Thevalueisusedforcharacterstorage.*/privatefinalcharvalue[];/**Cachethehashcodeforthestring*/privateinthash;//Defaultto0
value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。
/**用来存储字符串*/privatefinalcharvalue[];/**缓存字符串的哈希码*/privateinthash;//Defaultto0/**实现序列化的标识*/privatestaticfinallongserialVersionUID=-6849794470754667710L;0
/**用来存储字符串*/privatefinalcharvalue[];/**缓存字符串的哈希码*/privateinthash;//Defaultto0/**实现序列化的标识*/privatestaticfinallongserialVersionUID=-6849794470754667710L;1
通过前后两次打印的结果,我们可以看到 str 值被改变了,但是str的内存地址还是没有改变。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。
不可变的好处
首先,我们应该站在设计者的角度思考问题,而不是觉得这不好,那不合理:
可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销。
我们的程序中大量使用了String字符串,有可能是出于安全性考虑。
当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。
小结
有兴趣的小伙伴也可以去阅读下String的源码,浩浩荡荡的3000+。
String 被new时是要创建对象的,+ 号拼接同理,程序中尽量不要使用 + 拼接,推荐使用StringBuffer或者StringBuilder。