- 描述
- 关键字段:
-
private final char value[]
:表明String内部实际上就是一个不可变的字符数组,final保证引用不会变,但数组本身可以被修改,所以String把value[]
定义为private,类中也做了控制,所以除反射外String可以认为是不可变的。
-
- 构造函数
- String的构造函数有很多种类。传入空串、字符串、字符数组、字节数组+字符集、字符数组 + 位置截取、StringBuffer、StringBuilder等等。最终目的都是通过转换给value[]赋值。下面列举几种:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
String(char[] value, boolean share) {
this.value = value;
}
- 第一种:因为String类型传入时就是不可变的所以直接赋值即可。
- 第二种:传入的value[]不能直接赋值,传入的对象可能会带外部的引用,外部修改会导致数据被改,所以内部使用System.arraycopy()新建一个对象然后赋值给value[],传入的数组类参数都需要copy后再赋值。
- 第三种:一种特殊的赋值,包内可调,为了提升速度不创建新char[]直接赋值,后面StringBuffer的toString时会遇到。
- isEmpty()
public boolean isEmpty() {
return value.length == 0;
}
- 判断字符串是否为空,value不可变,直接判断长度即可
- equals(Object anObject)、equalsIgnoreCase(String anotherString)
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- equals():先"=="比较地址,如果相等肯定是等的,返回True。然后通过instanceof判断类型是否相同或有继承关系,然后依次判断value[]的字符是否一致,一致则返回true。
- equalsIgnoreCase():equals()的不区分大小版,通过两个字符数组Character.toUpperCase()比较,代码很简单,就不贴了。
- hashCode()
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
- 循环字符数组把之前的结果乘31然后加上当前字符(Unicode低16位)
- Unicode和ASCII的区别(百度):这两种编码的目的都是为了计算机中表示字符,ASCII码占一个字节,包括英文大小写、数字、制表符等等,范围是0x00 - 0xFF一共256个。后来因为要包括其他国家语言而进行扩展,最大到两个字节0x0000 - 0xFFFF,就是Unicode。Unicode包含了ASCII。
- charAt(int index)
- 返回数组下标的元素,代码略。
- split(String regex)、split(String regex, int limit)
- split(String regex); = split(String regex, 0)
- split(String regex, int limit)第二个参数有三种处理方法,直接举例说明传入"a,b,c,,":
- limit大于0:匹配n-1次后停止。例如n = 2; // {"a","b,c,,"}
- limit小于零:完全匹配。例如n = -2; // {"a","b","c","",""}
- limit等于零:完全匹配,而且清除结尾的空串。例如n = 0; // {"a","b","c"}
- replace(CharSequence target, CharSequence replacement)
- 字符串中所有target替换成replacement。
- intern()
- native方法,如果常量池中存在该字符串,就会直接返回常量池中该字符串,如果没有, 会将字符串放入常量池后, 再返回,下面通过对象创建看一下这个方法。
- 字符串对象的创建
- String str = new String("a");创建几个对象?1 or 2
- 堆里一个String对象。常量池里一个"a"常量,如果之前有就直接引用没有就创建。引用路线大概是栈str -> 堆String -> 常量池"a"
- 网上有很多说法,来做个测试,证明一下这个结论,还有字符常量池到底是怎么运作的?(虽然常量池也划在堆中,但测试单独区分方便分析)
- 先来看一下对象是否相同有两种常用方法:
- "=="
- Object.HashCode() 或者 System.identityHashCode():这个可能会有一些歧义,hash码确实不能直接表示对象相等,但Object的hashCode有个特点,if(a==b)则HashCode(a) == HashCode(b),反之则不一定,不一定的原因是大量数据产生的hash冲突,如果只是几个对象的测试,还是可靠的。还有一个问题就是String重写了hashCode()重写后只和value[]有关与对象无关,System.identityHashCode()可以替代。
- 下面通过hash码和intern()通过现象验证一下上述的推测 ,代码分别运行,以免常量池复用影响结果,各段代码和分割线之间的hash码无关,只有在同段代码中能证明对象是不是同一个。网上几乎没有用这种测试方法的,都是"=="判断,所以如果测试有疏漏请及时提醒。
- 对象创建测试
String a = "呵呵";
String a1 = "呵" + "呵";
String a2 = new String("呵呵");
String a3 = a2.intern();
---------------------------------------------------------------------------
String s = new String("嘻嘻");
String s1 = "嘻嘻";
String s3 = s.intern();
---------------------------------------------------------------------------
String s4 = new String("嘻嘻");
String s5 = s.intern();
String s6 = "嘻嘻";
- 第一段代码(直接列出identityHashCode()输出的结果):
- a = 654845766:a会直接放在常量池
- a1 = 654845766:a1这样的声明会经过编译优化,优化后和a的声明完全相同,都指向常量池所以相等
- a2 = 1712536284:new对象的方式会在常量池找有没有"呵呵",如果没有在常量池创建"呵呵"然后创建堆对象指向这个"呵呵",如果有就创建堆对象直接指过去。a2之前已经有了a和a1所以a2声明应该是 "栈里a2的引用" -> "堆里的String对象a2" -> "常量池的字符‘呵呵’ ",但这个hash码是"堆里的String对象a2"的hash码,所以和上面的常量池对象肯定不等。
- a3 = 654845766:根据intern()的解释,常量池已经有常量"呵呵"了,直接返回,所以和a、a1相等。
- 第二段代码(每段代码单独运行,以免污染常量池):
- s = 654845766:后两段代码调整对象生成顺序,可以证明上面new对象”先创建常量池对象,然后堆创建对象最后栈指向“的理论
- s1 = 1712536284
- s2 = 1712536284
- 第三段代码:
- s4 = 654845766
- s5 = 1712536284
- s6 = 1712536284
String o = "嘻嘻";
String b = new String("嘻嘻");
String d = new String("嘻") + new String("嘻");
String e = "嘻" + new String("嘻");
String h = new StringBuilder("嘻").append("嘻").toString();
- 上面证明了字面量声明和直接new对象两种方式,现在来测试一下复杂的情况
- o = 654845766:位于常量池。其他hash码都不一样,所以这些都不是直接指向常量池的而是堆,再写测试代码证明。
String s4 = new String("嘻") + new String("嘻");
String s5 = "嘻嘻";
String s6 = s4.intern();
------------------------------------------------------------------------------
String s7 = new String("嘻") + new String("嘻");
String s8 = s7.intern();
String s9 = "嘻嘻";
- 第一段代码:
- s4 = 379110473:堆
- s5 = 99550389:常量池
- s6 = 99550389:常量池,这段看不出问题,再来对比第二段代码
- 第二段代码:
- s7 = 654845766:堆
- s8 = 654845766:堆?
- s9 = 654845766:堆?为什么一样?这时候我发现了一篇博客说,s7声明时不会在常量池创建"嘻嘻"("嘻"会创建,当然并不需要讨论它),s7的String对象在堆,s8.intern()时发现堆里有s7而且字符内容相同,不创建常量池的"嘻嘻",而是直接指到了s7,intern()时常量池创建的对象会指到堆的"嘻嘻",字面量声明时也会这样,所以最后都指到了堆!
- 剩下的三种
new String + new String / "" + new String / new StringBuilder
的用最后一种方法验证也是一样的就省略了
- String为什么不可变
- 第一个因素是字符常量池,常量池是一个存储可复用字符串的内存空间,位于堆,大致的原理是创建字符串对象时,如果池里有这个字符串则返回引用,没有就在池里创建并返回。如果String是可变的,常量池内容被改所有引用这个字符串的对象都会变,这样常量池就没意义了。
- 第二个是线程安全,线程间修改同个字符串不用再做单独的并发处理。
- 第三个是因为hashCode缓存在对象里,可以避免重复计算。
结语:对象生成测试大多数是根据代码现象、网上资料推测出来的难免有疏漏,欢迎指正!
网友评论