本篇文章我先通过经典面试题,筛选需要观看本篇文章的朋友,然后咱们介绍String的基本特性,通过基本特性就可以找到面试题的答案。最后咱们再深入每个面试题,通过字节码、编译原理、基本特性深入剖析所有的面试题,让大家最终能通过原理理解所有的面试题,而不是死记硬背。
1、经典面试题
如果你可以全部答对并且是从原理上答对的,那您是真的完全理解String了,如果全部答对但是都是死记硬背或者未全部答对,则通过本篇文章相信您完全可以不用死记硬背就能答对,所有题目。
import org.junit.Test;
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringTest {
@Test
public void test1() {
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);
}
@Test
public void test2() {
String str1 = "abc";
String str2 = "abc";
str2 = "efg";
System.out.println(str1 == str2);
}
@Test
public void test3() {
String str1 = "abc";
String str2 = "abc";
str2 += "efg";
System.out.println(str1 == str2);
}
@Test
public void test4() {
String str1 = "abc";
String str2 = str1.replace('a', 'e');
System.out.println(str1 == str2);
}
@Test
public void test5() {
String str1 = "a" + "b" + "c";
String str2 = "abc";
System.out.println(str1 == str2);
}
@Test
public void test6() {
String str1 = "Hello";
String str2 = "String";
String str3 = "HelloString";
String str4 = "Hello" + "String";
String str5 = str1 + "String";
String str6 = "Hello" + str2;
String str7 = str1 + str2;
System.out.println(str3 == str4);
System.out.println(str3 == str5);
System.out.println(str3 == str6);
System.out.println(str3 == str7);
System.out.println(str5 == str6);
System.out.println(str5 == str7);
System.out.println(str6 == str7);
String str8 = str6.intern();
System.out.println(str3 == str8);
}
}
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringExer {
String str = "hello";
char[] ch = {'a', 'b'};
public void change(String str, char[] ch) {
str = "string";
ch[0] = 'e';
}
public static void main(String[] args) {
StringExer exer = new StringExer();
exer.change(exer.str, exer.ch);
//请问 输出值 分别是什么
System.out.println(exer.str);
System.out.println(exer.ch);
}
}
/**
* @author liuchao
* @date 2023/3/3
*/
public class Test3 {
@Test
public void test1() {
String str = new String("ab");
//问 new String("ab") 会创建几个对象呢?
}
@Test
public void test2(){
String str = new String("a") + new String("b");
//问 new String("a") + new String("b"); 会创建几个对象呢?
}
}
2、String基本特性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
①、String:字符串,使用一对""引起来
②、String类被声明为final的,不可继承
③、String实现了Serializable接口:表示字符串是支持序列化的;实现Comparable接口:表示String可以比较大小
④、String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte[] 存储字符串数据。
这里说下为什么jdk9要改为byte[]数组
官网解释:https://openjdk.org/jeps/254
官网解释翻译:
动机(为什么要更改)
目前String类的实现将字符存储在一个char数组中,每个字符使用两个字节(16位)。从许多不同的应用中收集到的数据表明,字符串是堆使用的主要组成部分,此外,大多数字符串对象只包含Latin-1(拼音之类的字符,一个拼音等于一个byte,用char存储就有点浪费了)字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有被使用。
说明
我们建议将String类的内部表示方法从UTF-16字符数组改为字节数组加编码标志域。新的String类将根据字符串的内容,以ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的方式存储字符编码。编码标志将表明使用的是哪种编码。
⑤、String:代表不可变的字符序列。简称不可变性。
当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
当调用String的replace()方法修改指定字符或者字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
⑥、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值什么在字符串常量池中。
⑦、字符串常量池是不会存储相同内容的字符串的。
String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
使用-XX:StringTableSize可设置StringTable的长度
在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize设置没有要求
在jdk7中,StringTable的长度默认值是60013,StringTablesize设置没有要求
在jdk8中,设置StringTable长度的话,1009是可以设置的最小值
3、String内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接存储在常量池中。 eg: String test = "test";
如果不是用双引号声明的String对象,可以使用String提供的intern()方法。动态创建字符串放入常量池
Java 6及以前,字符串常量池存放在永久代
Java 7中 Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
Java8元空间,字符串常量在堆
4、字符串拼接操作
①、常量与常量的拼接结果在常量池,原理是编译期优化
②、常量池中不会存在相同内容的变量
③、只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
④、如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
5、通过字节码分析每种题目
5.1、常量池不允许相同字符串重复存在
代码:
import org.junit.Test;
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringTempTest {
@Test
public void test() {
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);
}
}
通过字节码指令可以看出,加载的是同一个常量在地址
注:使用插件为jclasslib,可以访问https://blog.csdn.net/u011837804/article/details/129064876 查看使用方式
5.2、常量与常量的拼接结果在常量池,原理是编译期优化佐证
代码:
import org.junit.Test;
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringTempTest {
@Test
public void test() {
String str1 = "a" + "b" + "c";
String str2 = "abc";
System.out.println(str1 == str2);
}
}
javac编译后的.class文件反编译看下,证明常量与常量的拼接结果在常量池,原理是编译期优化。
5.3、只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
代码:
import org.junit.Test;
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringTempTest {
@Test
public void test() {
String str1 = "Hello";
String str3 = "HelloString";
String str5 = str1 + "String";
System.out.println(str5 == str3);
}
}
代码和字节码对比图片
咱们对字节码逐行分析
# 对应代码11行
0 ldc #2 <Hello>
2 astore_1
# 对应代码12行
3 ldc #3 <HelloString>
# 对应代码14行开始
# 咱们从字节码不难看出,这里先是new StringBuilder() 然后又调用append()方法,
# 最后调用了toString()方法
5 astore_2
6 new #4 <java/lang/StringBuilder>
9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init> : ()V>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
17 ldc #7 <String>
19 invokevirtual #6 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
22 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
25 astore_3
# 对应代码14行结束
26 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
29 aload_3
30 aload_2
31 if_acmpne 38 (+7)
34 iconst_1
35 goto 39 (+4)
38 iconst_0
39 invokevirtual #10 <java/io/PrintStream.println : (Z)V>
42 return
咱们在看看StringBuilder方法的toString()方法具体信息
咱们发现toString()方法实际就是new String()对象,咱们知道通过关键字 new创建的对象,对象实例都是存储在堆上的(非字符串常量池的一块区域),是不是就佐证了,上述代码结果为false
5.4、String intern()方法
通过翻译方法注释,佐证此方法结果是将对象值,在字符串常量池创建一份
a string that has the same contents as this string, but is
guaranteed to be from a pool of unique strings.
翻译:与此字符串具有相同内容,但保证来自唯一字符串池的字符串
5.5、会创建几个对象
通过字节码查看创建了几个对象
6、面试题答案公布
相信大家看完上述的理论+佐证,可以很简单的理解面试题,并且很快解答出来,咱们来看看所有题目的答案。
import org.junit.Test;
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringTest {
@Test
public void test1() {
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);//true
}
@Test
public void test2() {
String str1 = "abc";
String str2 = "abc";
str2 = "efg";
System.out.println(str1 == str2);//不变性,所以为 false
}
@Test
public void test3() {
String str1 = "abc";
String str2 = "abc";
str2 += "efg";
System.out.println(str1 == str2);//不变性,所以 false
}
@Test
public void test4() {
String str1 = "abc";
String str2 = str1.replace('a', 'e');
System.out.println(str1 == str2);//不变性,所以 false
}
@Test
public void test5() {
String str1 = "a" + "b" + "c";
String str2 = "abc";
System.out.println(str1 == str2);//编译优化 所以 true
}
@Test
public void test6() {
String str1 = "Hello";
String str2 = "String";
String str3 = "HelloString";
String str4 = "Hello" + "String";
String str5 = str1 + "String";
String str6 = "Hello" + str2;
String str7 = str1 + str2;
System.out.println(str3 == str4);//编译优化 所以 true
System.out.println(str3 == str5);//两个变量存储的位置不一致,所以 false
System.out.println(str3 == str6);//两个变量存储的位置不一致,所以 false
System.out.println(str3 == str7);//两个变量存储的位置不一致,所以 false
System.out.println(str5 == str6);//都未存储在字符串常量池中,所以地址不一致 false
System.out.println(str5 == str7);//都未存储在字符串常量池中,所以地址不一致 false
System.out.println(str6 == str7);//都未存储在字符串常量池中,所以地址不一致 false
String str8 = str6.intern();
System.out.println(str3 == str8);//intern 将str6内容重新在常量池中创建一份,但是常量池中不允许重复值,所以true
}
}
/**
* @author liuchao
* @date 2023/3/2
*/
public class StringExer {
String str = "hello";
char[] ch = {'a', 'b'};
public void change(String str, char[] ch) {
str = "string";
ch[0] = 'e';
}
public static void main(String[] args) {
StringExer exer = new StringExer();
exer.change(exer.str, exer.ch);
System.out.println(exer.str);//hello
System.out.println(exer.ch);//eb
}
}
/**
* @author liuchao
* @date 2023/3/3
*/
public class Test3 {
@Test
public void test1() {
String str = new String("ab");
//问 new String("ab") 会创建几个对象呢?
/**
* 答案:2个对象
* 对象1:new String()
* 对象2: 常量池中的"ab"
*/
}
@Test
public void test2(){
String str = new String("a") + new String("b");
//问 new String("a") + new String("b"); 会创建几个对象呢?
/**
* 答案: 6个对象
* 对象1:new StringBuilder()
* 对象2:new String("a")
* 对象3:常量池中的"a"
* 对象4:new String("b)
* 对象5:常量池中的"b"
* 对象6:StringBuilder.toString()底层 又 new String() 了一个对象
* 这里注意:StringBuilder.toString() 虽然 new 了一个Stirng,
* 但是 字符串常量池中是没有生成”ab“
*/
}
}
最后:希望对大家的面试有帮助,如果有写错的地方,欢迎大家留言指正,谢谢。