「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战」
前言
String,相信写过Java的人都不陌生吧,但是不知道关于String的一些底层知识点,大家是不是又都了解呢?下面就带大家过过平时使用String没有注意的一些点吧,希望对大家有帮助。
String和new String
当年真实遇到过该面试题,new一个String,实际创建了多少个对象或直接String赋值一个参数,实际创建了多少个对象,下面基于 JDK8 版本说明
上代码
public static void main(String[] args) {
String s1 = "123";
String s2 = new String("123");
//打印内存地址
System.out.println("s1 = " + System.identityHashCode(s1));
System.out.println("s2 = " + System.identityHashCode(s2));
System.out.println(s1 == s2);
}
结果
s1 = 1735600054
s2 = 21685669
false
其实这就涉及到JVM堆里的字符串常量池
- 在String s1 = "123"中可能
创建一个对象或者不创建对象,- 如果"123"字符串在Java的String常量池不存在,则会在常量池创建一个"123"String对象;
- 如果存在,s1直接reference to这个String池里的对象
- String s2 = new String("123")
至少创建一个对象,也可能两个。- 因为用到new 关键字,会在heap(堆)创建一个 s2 的String 对象,它的value 是 "123"。
- 同时,如果"123"这个字符串在java String池里不存在,会在java String池创建这个一个String对象("123")
所以上方的判断s1 == s2 结果是 false。s1返回的是常量池的对象地址;s2返回的是在堆中的地址,只是这个地址最终又指向了常量池中的"123"
String.intern()
String.intern()设计的初衷是为了重用字符串对象,以便节省内存。
在JKD1.6中
String.intern()先判断常量池中当前字符串是否存在:
- 不存在:将当前字符串复制到常量池,并返回常量池中字符串的引用;
- 存在:不会改变常量池已存在的引用,并返回常量池中的字符串引用;
在JDK1.7+中
String.intern()也是先判断常量池中当前字符串是否存在:
- 不存在:不会再将当前字符串复制到常量池,而是将当前字符串的引用复制到常量池并返回常量池中字符串的引用;
- 存在:不会改变常量池已存在的引用,并返回常量池中的字符串引用;
主要针对的不存在的情况做了改变,我们一般关注JDK1.7+的版本就好,毕竟现在主流是Java1.8。
public class StringTest {
public static void main(String[] args) {
String s1 = "hello";
String s2 = s1.intern();
String s3 = new String("hello");
System.out.println("s1 = " + System.identityHashCode(s1));
System.out.println("s2 = " + System.identityHashCode(s2));
System.out.println("s3 = " + System.identityHashCode(s3));
System.out.println("**********************");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
}
}
运行结果
s1 = 666641942
s2 = 666641942
s3 = 960604060
**********************
true
false
false
- 因为s1直接赋值一个
hello,这时候String常量池没有hello,会新建一个字符串,并返回常量池中字符串的引用 - 当运行到
String s2 = s1.intern();时,这时候常量池已经存在改字符串,所以直接返回常量池中字符串的引用 - 当
String s3 = new String("hello");时,先在堆创建了一个对象,同时该对象指向常量池中字符串
再来看一个例子
public class StringTest {
public static void main(String[] args) {
String s1 = new String("hello") + new String("world");
String s2 = s1.intern();
String s3 = "helloworld";
System.out.println("s1 = " + System.identityHashCode(s1));
System.out.println("s2 = " + System.identityHashCode(s2));
System.out.println("s3 = " + System.identityHashCode(s3));
System.out.println("**********************");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
}
}
运行结果
s1 = 666641942
s2 = 666641942
s3 = 666641942
**********************
true
true
true
- 因为s1是两个new出来的相加,所以此时返回的是一个 "helloworld" 的堆对象的引用,此时常量池并不存在"helloworld"字符串,因为并没有直接new("helloworld")。
- 当运行到
String s2 = s1.intern();时,这时候常量池并不存在字符,所以将当前字符串的引用复制到常量池并返回常量池中字符串的引用;注意;此时复制到常量池的并不是直接为"helloworld",而是"helloworld"的堆对象的引用。 - 当
String s3 = "helloworld";时,此时常量池有一个"helloworld"的引用对象,所以就直接返回给引用对象。
如何改变一个String的值
如何改变一个String字符串的值?这时候肯定有人说,直接赋值不就好了吗?就像这样
String str="hello";
str = "world"
要是面试时这样回答,就真的gg了。
因为我们大家都知道,String是不可变的,是被final修饰的,底层是一个char类型数组;在代码二次赋值时,实际上是在常量池新创建了一个对象,只是将str指向了新对象。
那么String就是不可变的吗?
其实不是的,final在修饰引用数据类型时,就如一个数组,能够保证指向该数组地址的引用不能修改,但是数组本身内的值可以被修改。
final char[] c1={'a','b','c'};
char[] c2={'1','2','3'};
c1=c2;
这时候肯定是编译报错的,因为我们修改了数组c1的地址引用。
final char[] c1={'a','b','c'};
c1[1]='1';
这时候编译不会报错,因为数组c1的引用我们并没有修改,我们修改的是c1本身内的值。
所以这就说明了即使被final修饰,但是直接操作数组里的元素还是允许的。所以我们想改变一个String的值的时候,就要用到以上的思路了,通过反射修改String底层char数组的值
public class StringTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str="Hello";
System.out.println(str + "hashCode: "+ str.hashCode());
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
field.set(str, new char[]{'W','o','r','l','d'});
}
}
运行结果
HellohashCode: 69609650
WorldhashCode: 69609650
这样就完美修改了String的值了。强调一点,这里的Java版本是8,但是在jdk9中,使用了byte数组代替char数组;主要是为了String对象占用的内存减少。
好了,关于String的一些我们平时不太注意的方面就这些了,这是我更文的第28天,给自己点个赞!!我是Liu_Denny,未来再见。