String 源码解读
在了解String类之前,先给大家总结一句话:“所有对String类进行过改变操作的方法,所返回的结果都是一个新的String对象,因为String字符串是不可变的!!!”
由于包子是刚开始写文章,所以对于一些语言描述可能不是特别的通俗易懂,在以后小编会尽量学习使用通俗易懂的语言来为大家解释。对于上边的总结,我们通过下边的内容能够更容易理解。
String 类
java.lang.String 类代表字符串。Java程序中所有的字符串文字(被双引号包裹的内容,例如 "包子" )都可以被看作是实现此类的实例。
String类中有很多方法,例如:
- 用于比较字符串内容的方法
- 搜索字符串的方法
- 提取字符串的方法
- 将字符串转换成全大写或全小写的方法
- 替换字符串内容的方法
- 字符串分割等等
String 字符串的特点:
1、字符串是不可变的,所以他们可以被共享(重点)。因为String底层维护的是一个 final char[] value;也就是一个 final char 类型的数组。大家都知道,被 final 修饰的常量是不可变的。
2、 所有对字符串进行修改的操作,都是操作的字符串的副本(Arrays.copy()),不会影响字符串本身。(这句话是小编自己通过阅读源码总结的,因为我看了下源码,基本上都是靠Arrays.copy()复制一个字符数组,然后对字符数组进行操作,然后返回一个新的String字符串。而且String的底层数组是final修饰的,不可变的,所以小编总结,对字符串进行的操作是对字符串的副本进行操作的,不会影响String本身。如果有错误,欢迎大佬指出。)
String 字符串创建对象的两种方式:
1、String s1 = "aa"; // 在String池中创建一个aa值,然后在栈中声明一个s1变量,最后将s1指向String 池中的 aa值。
2、String s2 = new String("bb"); // 首先在堆中创建一个String类型的变量bb,然后再栈中声明一个变量s2,然后将 s2指向变量bb,s2通过地址来访问bb值。
String 类的常用方法:
- 构造方法:
public String() :初始化新创建的 String对象,以使其表示空字符序列。
public String(char[] value) :通过当前参数中的字符数组来构造新的String。
public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String字符串。
- 字符串判断:
public boolean equals (Object anObject) :将此字符串与指定对象进行比较。
public boolean equalsIgnoreCase (String anotherString) :将此字符串与指定对象进行比较,忽略大小写。
- 字符串获取:
public int length () :返回此字符串的长度。
public String concat (String str) :将指定的字符串连接到该字符串的末尾,返回一个新的String字符串。
public char charAt (int index) :返回指定索引处的 char值。
public int indexOf (String str) :返回指定子字符串第一次出现在该字符串内的索引。
public String substring (int beginIndex) :返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
public String substring (int beginIndex, int endIndex) :返回一个子字符串,从beginIndex到endIndex截取字符串。包含beginIndex,不含endIndex。
- 字符串转换:
public char[] toCharArray () :将此字符串转换为新的字符数组。
public byte[] getBytes () :使用平台的默认字符集将该 String编码转换为新的字节数组。
public String replace (CharSequence target, CharSequence replacement) :将与target匹配的字符串使用replacement字符串替换。
- 字符串分割:
public String[] split(String regex) :将此字符串按照给定的regex(规则)拆分为字符串数组。
String源码解读:
// String底层维护的是一个 final char类型的数组。
private final char value[];
// String的构造方法,创建一个空字符层,字符串长度为0
public String() {
this.value = new char[0];
}
// 显示的初始化一个String字符串对象,该字符串是参数字符串的一个副本,除非需要{@code original }显示的副本,否则没有必要使用它,因为字符串是不可变的。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// 判断char[] value是否为空,为空返回true
public boolean isEmpty() {
return value.length == 0;
}
// 返回字符在数组中的下标,用来查找指定字符在字符串中的位置
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
/**
* 比较两个字符串是否相同,比较的是内容
* 首先比较两个字符串是否指向同一地址,如果指向同一地址返回true,否则向下执行
* 如果两个字符层不指向同一地址,则去比较他们的内容,先判断目标字符串是否是String类型,
* 如果是String类型,比较两个字符层的长度,如果长度相同,则比较两个字符串对应位置的字符是否相同,因为String底层是一个char类型的数组。
* 如果满足 两个字符层指向同一地址 OR (两个对象都是字符串 && 两个字符串长度相同 && 对应位置的字符相同) 这些条件,
* 则证明两个字符串的内容相同,返回true,否则返回false。
* 扩展:String继承类Object类,在Object中的equals方法比较的只是两个对象是否指向同一地址,String类重写类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;
}
/**
* 忽略大小写比较两个字符串的内容是否相同
* equalsIgnoreCase方法内部使用了三元运算,如果两个字符串对象指向同一地址,返回true。
* 如果两个字符串对象不是指向同一地址,则对anotherString进行一下判断:
* 1、非空判断
* 2、两个字符串的长度判断
* 3、使用 regionMatches判断两个字符层的内容。
* regionMatches方法内部将两个字符串都转换成了大写,然后比较两个大写字符串的内容。
*/
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
// 将 当前字符串对象与传入的字符串对象都转换成大写,然后比较两个字符串的内容。
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
if (ignoreCase) {
// If characters don't match but case may be ignored,
// try converting both characters to uppercase.
// If the results match, then the comparison scan should
// continue.
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
// Unfortunately, conversion to uppercase does not work properly
// for the Georgian alphabet, which has strange rules about case
// conversion. So we need to make one last check before
// exiting.
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
// 返回一个子字符串,子串的内容是当前字符串对象下标从 beginIndex开始到endIndex结束的内容,包含beginIndex不包含endIndex(因为数组下标是从0开始的)。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
// 拼接字符串,新产生的String字符串是一个新的对象,因为String字符串是不可变的。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
// 创建一个新的字符数组,长度是当前字符串长度与被拼接字符串长度的总和,然后将被拼接字符串的内容追加到当前字符串的末尾
// 有兴趣的同学可以自行阅读copyOf方法的源码。
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
// 返回一个新的String字符串对象
return new String(buf, true);
}
// 使用新的字符替换当前字符串对象中指定的旧字符,然后返回一个新的String字符串
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;
}
当然,String类中的方法还有很多,有兴趣的同学可以自行阅读源码,String的源码还是很好理解的,下面我们来看一下String在面试中比较常见的面试题:
问题1:以下代码 s1的输出结果是什么?
String s1="abc";
s1+="d";
System.out.println(s1); // "abcd
因为 String 字符串是不可变的,所有内存中会有 "abc"、"abcd"两个对象,只是变量s1从指向 "abc" 改变成指向 "abcd"而已,
此时s1指向的是"abcd","abc"已经没有变量指向它了,在之后如果还没有新的变量指向"abc",它将会被GC垃圾回收器回收。
问题2:内存中有几个 "abc"常量?
String s1="abc";
String s2="abc";
答案是 1个,此时在字符串常量池中只有一个abc,但是在堆中有两个变量 s1和 s2,他们都指向abc。
问题3:以下代码的输出结果是什么?
String s1="abc";
System.out.println(s1=="abc"); // true
首先在String池中开辟一块空间存放常量 "abc";
然后在栈中开辟一块空间存放变量 s1的引用;
然后 s1指向String池中的"abc";
所以 s1所指代的地址就是 "abc"的地址,所以返回true。
问题4:以下代码的输出结果是什么?
String str1 = new String("abc");
System.out.println(str2 == "abc"); // false
首先在堆内存中开辟一块空间存放新建的String 对象(new String("abc"); ),
然后在栈中开辟一块空间存放声明的变量 str1的引用,然后 str1的引用指向 堆中的String对象。
str1指向的是堆中的地址,常量 "abc"是存储在 String pool字符串池中的,所以他们不可能是同一对象,返回false。
问题5:以下代码的输出结果是什么?
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2); // false
首先在堆中开辟一块空间,存储新建 new String("abc"),然后在栈中开辟一块空间存储 str1的引用,将str1引用指向String对象。
然后在堆中再开辟一块空间,存储新建的 new String("abc")对象,这个String对象与str1指向的不是同一个对象,是两个值相同,地址不同的对象, 然后在栈中开辟空间存放 str2的引用,str2指向第二个String对象。
这两个对象只是值相同,但是在内存中的地址是不同的,所以str1==str2返回false。
问题6:以下代码输出结果是什么?
String str1 = "a" + "b";
System.out.println(str1 == "ab"); // true
因为 "a"和"b"都是常量,所以他们都是被存储在 String池中的,根据 JVM的优化功能,会在 String池中开辟一块空间,用来存放两个常量合并后的结果"ab";
然后在栈中开辟一块空间用来存放 str1的引用,然后将 str1指向 "ab",所以 str1指代的就是 "ab"的地址,所以结果为true。
问题7:以下代码输出结果是什么?
final String s = "a";
String str1 = s + "b";
System.out.println(str5 == "ab"); // true
同问题6一样,"a"是常量,放在String池中,s的引用指向"a",而且 s是被 final 修饰的,不可变,所以它只能指向 "a",所以 s也是个常量。
str1 的引用,指向的是 两个常量的结合,也就是 String池中的 "ab",所以打印结果是 true 。
问题8:以下代码输出结果是什么?
String s1 = "a";
String s2 = "b";
String str1 = s1 + s2;
System.out.println(str6 == "ab"); // false
s1、s2是栈中存放的两个引用,s1指向"a",s2指向"b";
s1+s2 是通过 StringBuilder 的 toString() 方法构建的一个新的String对象"ab",这个 "ab"是放在栈中的,str1的引用指向的是栈中的String对象"ab";
str1=="ab" 中的ab是放在String池中的,一个在堆中,一个在String池中,所以返回结果是false。
问题9:以下代码输出结果是什么?
String str1 = "abc".toUpperCase();
System.out.println(str8 == "ABC"); // false
"abc"是String池中的常量,通过String的 toUpperCase()方法,新构建了一个字符串对象"ABC",这个对象是放在堆中的,因为toUpperCase()方法返回的是 return new String(),新构建的字符串对象,所以它是被放在堆中的;
栈中存放的是str1 的引用,它指向的是堆中的String对象"ABC";
而 str8 == "ABC"中的 "ABC"是一个常量,存储在String池中,所以返回结果是 false。
学习了以上内容,同学们是否对String类有了更详细的了解呢?下面我们看看以下代码的执行结果,知道是为什么嘛?如果你能够说出以下代码的结果,那么说明你对String已经了解的非常深刻了呢!
class Test1 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); //false
}
}
class Test2 {
public static void main(String[] args) {
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2); //false
}
}
class Test3 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "ab" + "c";
System.out.println(s1 == s2); //true
}
}
class Test4 {
public static void main(String[] args) {
String s = "a";
String s1 = "abc";
String s2 = s + "bc";
System.out.println(s1 == s2); // false
}
}
class Test5 {
public static void main(String[] args) {
String s1 = "ab";
String s2 = "ab" + getString();
System.out.println(s1 == s2); //fasle
}
// 该方法返回的是一个String对象
private static String getString() {
return "c";
}
}
class Test6 {
public static void main(String[] args) {
String s = "a";
String s1 = "abc";
String s2 = s + "bc";
System.out.println(s1 == s2.intern()); // ture
}
}
包子是边学习边总结,如果以上内容有错误的地方,欢迎大佬指正,希望包子的文章对大家有所帮助,包子会从基础开始,逐渐更新更多的 java技术及工作中遇到的问题总结。希望大家支持,谢谢大家!!!
注:面试题部分引用了灭霸詹大佬的《詹哥秘笈之JVM知识图谱》,注释部分有包子自己的一些理解!!!