Java String
String 源码(JDK 1.8.0_171)
String
不是基本类型,是一个类。分析一个类,应该从类定义(继承,实现接口等),变量,方法,内部类等等进行分析。
1. 类定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
String
类被final
修饰,意味着这个类不能被继承。问题:那么为什么String
不能被继承?
实现Serializable
序列化接口。
实现Comparable
接口,它其中只用一个方法public int compareTo(T o);
用来比较两个String
对象大小。
实现CharSequence
接口,它其中有int length();
,char charAt(int index);
,public String toString();
等方法。
2. 变量定义
String
类中有两个重要的成员变量value[]
hash
。
/** The value is used for character storage. */ private final char value[];
value
被关键字private
final
修饰,这意味着value
对外不可见,且对修改关闭。这里需要注意的是value
不可修改只是引用地址不可修改,value
地址指向的是堆中的数据。如下所示例子,编译器将会报错。
final int[] value={1,2,3,4}; int[] arr = {5,6,7,8}; value = arr;
但如果是修改value
中的值,如下图所示,输出结果为 10
。
final int[] value={1,2,3,4}; value[0] = 10; System.out.println(value[0]);
如果觉得有点绕,可以看下图所示的堆内的简化版指向图。新建String
对象a
b
。
当a=b
时,只是将a
中的地址引用修改,原来"aa"
还是没有改变。
阅读String
源码会发现,除了在构造方法上对value
赋值外,没有对value
中的值进行任何的修改。而由于value
被private
修饰,对外不可见,所以相当于String
不可变。这里指的不可变不是指String
实例对象不可变,是指字符串常量池中的String
对象不可变。问题:那么为什么要把String
设计成不可变?
/** Cache the hash code for the string */ private int hash; // Default to 0
由于String
的不可变性,所以缓存了hashcode
,减少每次需要hashcode
时而进行一次运算带来的开销。
3. 方法
String
类中提供多种构造方法,本质就是将的参数传递给成员变量value[]
进行初始化。接下来我们讲讲主要的几个方法。
-
equals
方法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
方法第一是先比较两个对象的地址是否相同。再判断参数对象是否是String
实例。如果是String
实例,进行比较两个实例的数组长度。长度相同则开始对两个String
对象的value[]
中的值进行一一比较。如果value[]
中的值都相同则返回true
。 -
concat
方法public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }
concat
方法是将两个字符串连接起来。这个操作并不是在this
这个字符串对象中进行,而是产生一个新的String
对象。Arrays
中包含了操纵数组的各种方法。 -
replace
方法public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(buf, true); } } return this; }
replace
方法是替换操作,主要是将原来字符串中的oldChar
全部替换成newChar
。先找到第一个所要替换的字符串的位置i
,将i
之前的字符直接复制到一个新char
数组。然后从i
开始再对每一个字符进行判断是不是所要替换的字符。 -
split
方法public String[] split(String regex, int limit) { /* fastpath if the regex is a (1)one-char String and this character is not one of the RegEx's meta characters ".$|()[{^?*+\\", or (2)two-char String and the first char is the backslash and the second is not the ascii digit or ascii letter. */ char ch = 0; if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { int off = 0; int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // last one //assert (list.size() == limit - 1); list.add(substring(off, value.length)); off = value.length; break; } } // If no match was found, return this if (off == 0) return new String[]{this}; // Add remaining segment if (!limited || list.size() < limit) list.add(substring(off, value.length)); // Construct result int resultSize = list.size(); if (limit == 0) { while (resultSize > 0 && list.get(resultSize - 1).length() == 0) { resultSize--; } } String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } return Pattern.compile(regex).split(this, limit); }
这个方法看起来比较复杂,但其实我们一般都不会用到那一大串的内容,一般我们用到最后那一句
return Pattern.compile(regex).split(this, limit);
是使用Pattern的正则方式去解析并拆分成字符串数组。
4. 回答问题
结合上面对String
类的了解,我们现在现在来对上面的两个问题 那么为什么String
不能被继承? 那么为什么要把String
设计成不可变? 进行解答。
那么为什么String
不能被继承?
假设String
是可继承的。新建一个ChildString
继承String
类,重写它的lenth()
方法。
public class ChildString extends String { @Override public int length() { return 999999; } }
再写一个方法根据字符串长度创建数组长度。
private static int[] getArray(String string) { return new int[string.length()]; } public static void main(String[] args) { getArray(new String()); getArray(new ChildString()); }
如代码所示,getArray(new ChildString());
创建了长度为999999的数组,造成安全漏洞。在Java
中String
的使用率非常高,并且也可以看出,虽然它是非基本类型,但是开发过程已经将它当做基本类型来使用了。如果String
变为可继承,那引发的安全问题可想而知。所以干脆直接将String
变成不可继承,杜绝此类安全问题的产生。
再有一个就是效率问题,我们都知道实例对象的方法调用是先在当前类中查找该方法,如果没有再去父类中查找,以此类推。String
不可继承,不再需要往父类查找方法消耗时间。
那么为什么要把String
设计成不可变?
上文讲到因为String
的不可继承,成员变量value[]
被final
private
修饰,所以String
实例拥有不可变性。上面问题也提到Java
中String
的使用率非常高,可以说被当做基本类型来使用。String
可变则会引发线程安全问题。如下代码:
StringBuilder s1 = new StringBuilder("a"); StringBuilder s2 = s1; s2.append("c"); System.out.println(s1); // ac
此处用StringBuilder
来模拟String
可变。如代码所示,最后s1
输出结果是ac
。假设如在某个地方设置String
类型的数据库密码,而后有人获取这个String s1
实例给另一个String s2
实例,在不知情的情况下对s2
进行的修改,那在系统中的数据库密码就被修改了,从而导致系统崩溃。
Map<StringBuilder,Object> data = new HashMap<>(); StringBuilder s1 = new StringBuilder("a"); data.put(s1,"1"); StringBuilder s2 = s1; s2.append("c"); data.put(s2,"2"); System.out.println(data); // ac:2
再比如在Map
中put(String,Object)
的值,此处用StringBuilder
来模拟String
可变。由上面可知,由于s2
的修改导致s1
也变成了ac
,最后并没有得到我们想要的结果。
String
不可变,我们将 s1
给s2
,s2
进行修改则会新建一个String
实例,而不会改变s1
指向的值,保证了线程安全。
String
实例在它创建的时候HashCode
就被缓存了,不需要重新计算。这就使得字符串很适合作为Map
中的键,字符串的处理速度要快过其它的键对象。这就是HashMap
中的键往往都使用字符串。
我们都知道JVM
的Heap
中有字符串常量池。其实说白了,要String
不可变就是为了更好的实现字符串常量池。字符串池的实现可以在运行时节约很多Heap
空间,因为不同的字符串变量都指向池中的同一个字符串。不需要考虑被修改的问题。如果可以被改来改去的,字符串常量池就没有存在的意义了。
字符串常量池
由上文可知String
在Java
的使用率非常高,而且很经常使用相同的字符串。为了减少字符串对象的重复创建,JVM
在Heap
区(1.7 开始在Heap
)维护了一个特殊的内存,这段内存被成为字符串常量池。这是一个缓存区域,将已经创建的对象放入缓存中,下次创建相同内容的对象直接将引用赋值给新对象,这样就减少了内存空间的使用,加快运行速度。
字符串进入到常量池中的方法有两种。
- 通过调用
String
intern()
方法 。执行intern()
方法时,若常量池中不存在等值的字符串,JVM
就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。 “”
引起来的内容(字面量)。引号引起来的字符串,首先从常量池中查找是否存在此字符串,如果不存在则在常量池中添加此字符串对象,然后引用此字符串对象。如果存在,则直接引用此字符串。这种在编译时期就会增加到常量池中。
为了更好的理解,看看下面的代码。
//符合条件 2 在常量池中创建 "ab"。 String s0 = "ab"; //s0时已经创建了"ab",直接返回常量池中的地址引用。 String s1 = "ab"; //编译器会优化成 String s2 = "ab"; 与s1情况相同。 String s2 = "a" + "b"; System.out.println(s0 == s1); //true System.out.println(s1 == s2); //true //符合条件 2 在常量池中创建 "cd"。 String s3 = "cd"; //通过new的方式创建时,在常量池中查询是否存在,没有则会创建"cd",然后在堆中创建"cd"。 //s4引用 -> 堆中"cd" String s4 = new String("cd"); //此时 s3引用 -> 常量池中的"cd" s4引用 -> 堆中的"cd" 所以是不相等的。 System.out.println(s3 == s4); //false //符合条件 2 在常量池中创建 "ef"。 String s5 = "ef"; //两个 String 实例的拼接实际上是使用StringBuilder。使用 append() 方法拼接,返会一个新的String对象。但是并没有在常量池中查看创建"ef" //s6引用 -> 堆中"ef" String s6 = new String("e") + new String("f"); //此时 s5引用 -> 常量池中的"ef" s6引用 -> 堆中"ef" 所以是不相等的。 System.out.println(s5 == s6); //false String s7 = new String("mn"); String s8 = new String("mn"); //这个比较好理解,就是比较两个对象地址。 System.out.println(s7 == s8); //false //符合条件 2 在常量池中创建 "op"。 String s9 = "op"; //s10引用 -> 堆中"op" String s10 = new String("op"); //使用 intern()方法,将"op"加入到常量池中。 //如果已存在则直接返回地址引用。 //如果不存在则存储一份堆中"op"的引用,相当于常量池中的"op" == 堆中的 "op"。 //s10引用 -> 常量池中的"op" s10.intern(); //此时 s9引用 -> 堆中的 "op"。 s10引用 -> 常量池中的"op" 不相等 System.out.println(s9 == s10); //false //s11引用 -> 堆中"xy" String s11 = new String("x") + new String("y"); //使用 intern()方法,将"xy"加入到常量池中。 //不存在则存储一份堆中"xy"的引用,常量池中的"xy" = 堆中的 "xy"。 s11.intern(); String s12 = "xy"; //此时 s11引用 -> 堆中的 "xy"。 s12引用 -> 常量池 -> 堆中的 "xy" 相等 System.out.println(s11 == s12); //true
String 、StringBuilder、StringBuffer
由于String
的不可变性,当我们需要可变的操作时String
就无法做到。查看源码StringBuilder
StringBuffer
都继承AbstractStringBuilder
抽象类,成员变量value[]
是包内可见可修改的,保证了可变性。
/** * The value is used for character storage. */ char[] value;
查看StringBuilder
的源码,value[]
的初始容量在不指定的情况下为16
。
public StringBuilder() { super(16); }
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
当容量不足的情况下将需要进行扩容,扩容的大小为当前长度的 2 倍 + 2,如果新扩容的容量还是比实际字符数量小,则直接将扩容至实际字符数量。当然并不是可以无限的扩容下去,当实际字符大小大于Integer.MAX_VALUE
则会内存溢出错误,否则最大为MAX_ARRAY_SIZE
。 扩容中原先的数组复制过来,再丢弃旧的数组。
private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } //MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8 return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }
private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }
在需要大量字符串拼接的时候,不要使用String
来拼接,我们知道String
的不可变性,导致拼接的时候创建的是一个新的String
实例,从而导致资源的浪费。因为StringBuilder
的可变性,使用StringBuilder
来进行字符串拼接,操作的始终都是同一个实例,并不会浪费多余的空间。如果可以确定字符长度,在初始化是指定大小,减少扩容带来的性能消耗。
StringBuffer
的大多数方法都是与StringBuilder
相同,不同的是StringBuffer
中大部分方法被synchronized
修饰,意味着StringBuffer
是线程安全的。三者在不同的场景各有各的优势,在实际开发中,应当选择适合的。
有问题请可以在评论区指正。