文章目录
- 一、String实例化的两种方式
- (1)两种方式
- (2)举例
- 1、案例1
- 2、案例2
- (3)内存分配
- (4)面试题
- 1、题1
- 2、题2
- 二、String的连接操作+
- (1)案例
- 1、案例剖析
- 2、intern方法
- (2)总结
- 三、练习
- (1)练习类型1:拼接
- 1、题1
- 2、题2
- (2)练习类型2:new
- 1、说明
- 2、练习
- (3)练习类型3:intern()
- 1、说明
- 2、题1
- 3、题2
- 4、题3
- 5、题4
一、String实例化的两种方式
(1)两种方式
🔥两种方式
第1种方式(字面量的方式):String s1 = "hello";
第2种方式(String作为类):String s2 = new String("hello");
(2)举例
1、案例1
🌱代码
@Test
public void test1() {String s1 = "hello";String s2 = "hello";String s3 = new String("hello");String s4 = new String("hello");System.out.println(s1 == s2); //trueSystem.out.println(s1 == s3); //falseSystem.out.println(s1 == s4); //falseSystem.out.println(s3 == s4); //false
}
🍺输出
false
表示两个地址值不一样。
2、案例2
🌱代码
@Test
public void test1() {String s1 = "hello";String s2 = "hello";String s3 = new String("hello");String s4 = new String("hello");System.out.println(s1.equals(s2)); //trueSystem.out.println(s2.equals(s3)); //true
}
🍺输出
equals
比较的是内容,所以都是true。
(3)内存分配
🎲内存是如何分配的呢?
①String s="hello";
:在字符串常量池中有一个hello,将地址给了s。
字符串有一个属性叫value,value里面存储核心数据hello,hello是一个数组,所以value这里是一个引用。
②String s=new String("hello");
在内存中是如何分配空间的?在内存中创建了几个对象?
🚗内存图
String s=new String("hello");
在内存空间中创建了两个对象。
一个是在堆空间里面new的对象;一个是在常量池里面的String类型的hello,这个hello也有一个地址值。(因为字符串比较特别,在new的同时还要往字符串常量池里面放一个字面量)
两个对象如下:
这两个对象里面value
的属性都指向同一个char型的数组,所以它们里面保留的地址(0X5566)是一样的,但外面来看它们的地址是不一样的(0X3344和0X1122)。
比如:
s1与s3的地址值就不一样,但是它们value属性的地址一致。
之前说过String
内部声明的属性:
- jdk8中:
private final char value[];
//存储字符串数据的容器final
: 指明此value数组一旦初始化,其地址就不可变。
- jdk9开始:为了节省内存空间,做了优化
private final byte[] value;
//存储字符串数据的容器。
可以看到,声明value的时候,前面加了一个private
,也就是value不对外暴露。
所以,我们可以不用过多关注s1与s3它们内部的value指向的地址一致,只需要关注外面的地址不一致就好。
(4)面试题
1、题1
🌋String s2 = new String("hello");
在内存中创建了几个对象?
两个!
一个是堆空间中new的对象。另一个是在字符串常量池中生成的字面量。
☕注意
有的小伙伴会有这个疑问,执行第一句代码的时候,已经在字符串常量池中造了一个了。
那么在执行第二句的时候就造一个堆空间里面的不就行了。
其实一般在问的时候,不考虑在字符串常量池中已经有的情况了。
直接问的时候,就说创建了两个对象即可。
2、题2
🌋下面代码输出结果是?
🌱代码
public class StringDemo1 {@Testpublic void test2(){Person p1=new Person();Person p2=new Person();p1.name="Tom";p2.name="Tom";p1.name="Jerry";System.out.println(p2.name); //Tom}
}class Person{String name;
}
🍺输出
☕分析
若是按照以往的思路,我们会觉得p1与p2的name是各自一份,下面修改了p1的name,不会影响p2的name,所以打印输出p2的name还是Tom。
现在来看的话,p1与p2在内存中指向的是同一个位置。
<1> 对于p1:
Person p1=new Person();p1.name="Tom";
首先在堆里面new了一个Person,属性是name。然后用的是“字面量”的方式给它做的赋值,在字符串常量池里面有一个字符串Tom(数组中存放),它的地址赋值给了value(String的一个属性),然后String的地址(比如0x1122)赋值给了name。
最后Person的地址(比如0x3344)给了p1。
如下:
<2> 对于p2:
Person p2=new Person();p2.name="Tom";
同样的,在堆空间中新创建了一个对象,也会有一个地址值(比如0x5566),将它赋给了p2。
堆空间的对象有一个属性name,指向字符串常量池中的String(也是0x1122),value指向Tom。
可与看到,p1与p2它们在内存中使用了同一个Tom。
以前我们画的是简略图
:
<3> 接下来,用p1调用name,并将它改为Jerry。
p1.name="Jerry";
此时详细的过程应该是:字符串常量池里面有一个“Jerry”,它的地址给了String的value,整一个新的地址。
然后p1的name指向的String的地址变成了0x7788。
如下:
<4> 现在用p2调用name,还是"Tom",不受影响。
System.out.println(p2.name);
这样设计的目的是为了节省内存空间。
当不同的属性(比如p1.name和p2.name)值是相同(比如"Tom")的时候,就让他们在字符串常量池里面共用同一个。
🎲为什么String
要有不可变性?
我们看到的像是各自持有一份,但实际上用的是同一个。
假设现在有1000个用的都是Tom,是同一个Tom,若是其中一个想要改,不能在原有的地方改(要不然其他的都要一起变),要改的话自己新造一个,其他的不改就还用之前的那个。
这就极大节省了内存空间。
对于我们平常使用者来说,感受不到底层它们使用的是同一份。
只知道它们都有一个一样的值,其中一个修改不会影响其他的改变。
共用一个节省了空间,在这个基础之上,谁想要改谁就自己重新造一个,不影响现有的对象使用当前的结果。
二、String的连接操作+
(1)案例
1、案例剖析
🌱代码
//测试String的连接符:+
@Test
public void test3(){String s1="hello";String s2="world";String s3="helloworld";String s4="hello"+"world";String s5=s1+"world";String s6="hello"+s2;String s7=s1+s2;System.out.println(s3==s4); //trueSystem.out.println(s3==s5); //falseSystem.out.println(s3==s6); //falseSystem.out.println(s3==s7); //falseSystem.out.println(s4==s5); //falseSystem.out.println(s4==s6); //falseSystem.out.println(s4==s7); //falseSystem.out.println(s5==s6); //falseSystem.out.println(s5==s7); //falseSystem.out.println(s6==s7); //false
}
🍺输出
🍰分析
false
表示两者的地址是不一样的。
看一下反编译文件:
s3与s4已经写成一样的了。
在字节码文件当中,它就已经做了合并了。看到的是两个不同的字面量,其实编译之后就是一个拼接起来的。
所以s3==s4
输出的是true。
"hello"和"world"相当于是两个常量,常量做拼接(“hello”+“world”)和直接写这个常量(“helloworld”)没有区别。
`
s5、s6、s7
中都有变量参与,变量参与的话该如何实现呢?
之前其实已经说过了,如下:
s2+="world"
就相当于s2=s2+"world"
。
像String s5=s1+"world"
这种结构,其实是底层给new了一个对象。
看一下下面的解释(字节码文件):
总之,就是调用了StringBuilder
的toString
,看一下:
public String toString(){//Create a copy,don't share the arrayreturn new String(value,0,count);
}
可以看到,这里面返回的是一个new了之后的对象。
所以,这里其实是新new的对象:(通过查看字节码文件发现,调用了StringBuilder
的toString()
方法–>new String()
)
String s5=s1+"world"; //新new的对象
String s6="hello"+s2; //新new的对象
String s7=s1+s2; //新new的对象
因此,s5、s6、s7都是新new的对象,它们之间作比较,肯定都是不一样的。
s3
是常量池里面的,跟堆空间里面new的对象显然也不是一致的,所以结果是false。
2、intern方法
🗳️补充
intern()
:调用这个方法后会得到一个新的字符串,返回的是字符串常量池中字面量的地址。
//测试String的连接符:+
@Test
public void test3(){String s1="hello";String s2="world";String s3="helloworld";String s4="hello"+"world";String s5=s1+"world"; //通过查看字节码文件发现,调用了StringBuilder的toString()方法-->new String()//...String s8=s5.intern(); //intern:返回的是字符串常量池中字面量的地址System.out.println(s3==s8); //true
}
输出结果:
intern
返回的是字符串常量池中字面量的地址,此时字符串常量池中已经有"helloworld"了,所以就将已有的地址返回给了s8。
因此,s3、s4、s8的地址是一样的。
看一下内存图:
(2)总结
【String的连接操作:+
】
①情况1
常量 + 常量
:结果仍然存储在字符串常量池中,返回此字面量的地址。
注:此时的常量可能是字面量,也可能是final
修饰的常量。
比如:
🌱代码
String s1="hello"; //字面量,存储在常量池中
String s2="world"; //字面量String s3="helloworld"; //字面量
String s4="hello"+"world"; //常量+常量,结果还在字符串常量池中System.out.println(s3==s4); //true
再比如final
修饰的常量:
🌱代码
public void test4(){final String s1="hello"; //字面量,存储在常量池中String s2="world"; //字面量String s3="helloworld"; //字面量String s4="hello"+"world"; //常量+常量,结果还在字符串常量池中String s5=s1+"world"; //通过查看字节码文件发现,调用了StringBuilder的toString()方法-->new String()String s6="hello"+s2;String s7=s1+s2;System.out.println(s3==s5); //trueSystem.out.println(s3==s6); //false
}
在s1前面加了一个final
,就让它变成了常量。
此时s5就跟s4类似了。
🍺输出
这种考的场景比较少。
②情况2
常量 + 变量
或 变量 + 变量
:都会通过new的方式创建一个新的字符串,返回堆空间中此字符串对象的地址。
🌱代码
String s1="hello"; //字面量,存储在常量池中
String s2="world"; //字面量String s5=s1+"world"; //通过查看字节码文件发现,调用了StringBuilder的toString()方法-->new String()
String s6="hello"+s2;System.out.println(s5==s6); //false
③情况3
调用字符串的intern()
:返回的是字符串常量池中字面量的地址。
🌱代码
String s1="hello";
String s3="helloworld"; //字面量
String s5=s1+"world";String s8=s5.intern(); //intern():返回的是字符串常量池中字面量的地址System.out.println(s3==s5); //false
System.out.println(s3==s8); //true
如果字符串常量池当中字面量不存在,就会新创建一个然后返回。
如果存在,就会返回已有的。(字符串常量池中不允许存放两个相同的字符串常量)
④(了解)情况4
concat(xxx)
:不管是常量调用此方法,还是变量调用,同样不管参数是常量还是变量,总之,调用完concat()方法都返回一个新new的对象。
🌱代码
@Test
public void test5(){String s1="hello";String s2="world";String s3=s1.concat(s2); //s1连接s2String s4="hello".concat("world"); //常量连接常量String s5=s1.concat("world");String s6="hello".concat(s2);System.out.println(s3==s4);System.out.println(s3==s5);System.out.println(s3==s6);System.out.println(s4==s5);System.out.println(s4==s6);System.out.println(s5==s6);}
🍺输出
三、练习
(1)练习类型1:拼接
1、题1
🌱代码
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true
// 内存中只有一个"hello"对象被创建,同时被s1和s2共享。
🍺输出
🍰分析
对应内存结构为:(以下内存结构以JDK6为例
绘制):
进一步:
2、题2
🌱代码
@Test
public void test6(){Person p1 = new Person();p1.name = "Tom";Person p2 = new Person();p2.name = "Tom";System.out.println(p1.name.equals( p2.name)); //trueSystem.out.println(p1.name == p2.name); //trueSystem.out.println(p1.name == "Tom"); //true
}
🍰分析
🍺输出
(2)练习类型2:new
1、说明
String str1 = “abc”;
与 String str2 = new String(“abc”);
的区别?
str2 首先指向堆中的一个字符串对象,然后堆中字符串的value数组指向常量池中常量对象的value数组。
- 字符串常量存储在字符串常量池,目的是共享。
- 字符串非常量对象存储在堆中。
2、练习
🌱代码
@Test
public void test7(){String s1 = "javaEE";String s2 = "javaEE";String s3 = new String("javaEE");String s4 = new String("javaEE");System.out.println(s1 == s2);//trueSystem.out.println(s1 == s3);//falseSystem.out.println(s1 == s4);//falseSystem.out.println(s3 == s4);//false
}
🍰分析
🍺输出
🎲问:String str2 = new String("hello");
在内存中创建了几个对象?
两个。
(3)练习类型3:intern()
1、说明
①
String s1 = "a";
说明:在字符串常量池中创建了一个字面量为"a"的字符串。
②
s1 = s1 + "b";
说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+“b”(也就是"ab")。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。
③
String s2 = "ab";
说明:直接在字符串常量池中创建一个字面量为"ab"的字符串。
④
String s3 = "a" + "b";
说明:s3指向字符串常量池中已经创建的"ab"的字符串。
⑤
String s4 = s1.intern();
说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的"ab"字符串赋值给s4。
2、题1
🌱代码
@Test
public void test8(){String s1 = "hello";String s2 = "world";String s3 = "hello" + "world";String s4 = s1 + "world";String s5 = s1 + s2;String s6 = (s1 + s2).intern();System.out.println(s3 == s4); //falseSystem.out.println(s3 == s5); //falseSystem.out.println(s4 == s5); //falseSystem.out.println(s3 == s6); //true
}
🍺输出
结论:
(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。
(2)常量与变量 或 变量与变量:结果在堆中
(3)拼接后调用intern方法:返回值在常量池中
3、题2
🌱代码
@Test
public void test01(){String s1 = "hello";String s2 = "world";String s3 = "helloworld";String s4 = s1 + "world";//s4字符串内容也helloworld,s1是变量,"world"常量,变量 + 常量的结果在堆中String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是变量,变量 + 变量的结果在堆中String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果System.out.println(s3 == s4);//falseSystem.out.println(s3 == s5);//falseSystem.out.println(s3 == s6);//true
}@Test
public void test02(){final String s1 = "hello";final String s2 = "world";String s3 = "helloworld";String s4 = s1 + "world";//s4字符串内容也helloworld,s1是常量,"world"常量,常量+常量结果在常量池中String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是常量,常量+ 常量 结果在常量池中String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果System.out.println(s3 == s4);//trueSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s6);//true
}@Test
public void test01(){String s1 = "hello";String s2 = "world";String s3 = "helloworld";String s4 = (s1 + "world").intern();//把拼接的结果放到常量池中String s5 = (s1 + s2).intern();System.out.println(s3 == s4);//trueSystem.out.println(s3 == s5);//true
}
4、题3
🌱代码
public class TestString {public static void main(String[] args) {String str = "hello";String str2 = "world";String str3 ="helloworld";String str4 = "hello".concat("world");String str5 = "hello"+"world";System.out.println(str3 == str4);//falseSystem.out.println(str3 == str5);//true}
}
concat方法拼接,哪怕是两个常量对象拼接,结果也是在堆。
5、题4
🌱代码
public class StringTest {String str = new String("good");char[] ch = { 't', 'e', 's', 't' };public void change(String str, char ch[]) {str = "test ok";ch[0] = 'b';}public static void main(String[] args) {StringTest ex = new StringTest();ex.change(ex.str, ex.ch);System.out.print(ex.str + " and ");//System.out.println(ex.ch);}
}
🍺输出