参考:
https://www.cnblogs.com/wulouhua/p/3875630.html
https://blog.csdn.net/xiamiflying/article/details/82860721
通过如下几个样例,来理解Java中的String定义,在内存中申请多少个对象。
1、样例1
String str1="abc";
String str2="abc";
如上代码,会创建几个String对象呢? 答案是1个。
这个就涉及到了Java中两个关键内容:
- 字符串池。
在JVM中存在着一个 字符串池,其中保存着很多String对象,且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。
字符串池由String类维护,我们可以调用intern()方法来访问字符串池。 - 文本化创建String对象。(暂且这么叫吧)
解析如上的创建过程,就需了解堆、栈的作用:
- 堆
- 存储的是对象,每个对象都包含一个与之对应的class。
- JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
- 对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定
- 栈
- 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。
- 每个栈中的数据(原始类型和对象引用)都是私有的。
- 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
- 数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失。
- 方法区
- 静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,如class、static变量
- 字符串常量池则存在于方法区。
- 代码:堆栈方法区存储字符串。
如上:
String str1="abc";
字符串池:"abc" ,新建1个String对象
堆:无
引用:str1 :新建1个
(1)首先在 栈 中创建str1变量。
(2)然后判断 "abc"字符串常量在 字符串池 中是否存在,判断依据是String类equals(Object obj)方法的返回值。
* 如果存在,不再创建新的对象,直接返回已存在对象的引用,str1直接指向 字符串池 中的 "abc"(此过程是编译器优化的);
* 如果不存在,在 字符串池 中创建“abc"对象,str1指向 字符串池 中的对象。 (根据上下文,执行前 字符串池 中无此对象,所以此处会创建1个对象)。
如上:
String str2 = "abc";
字符串池:"abc" ,已存在,无新建
堆:无
引用: str2 :新建1个
同如上(1)(2)过程,此时 字符串池中已经有"abc"对象,str2直接指向即可。所以此语句不会创建对象。
2、样例2
String str=new String("abc");
如上代码,会创建几个String对象呢? 答案是2个。
原因:
可以把如上这行代码拆分成几部分看待:String str、=、"abc"和new String()。
(1)String str只是定义了一个名为str的String类型的变量,并没有创建对象;
(2)=是对变量str赋值,将某个对象的引用赋值给它,也没有创建对象;
(3)只剩下new String("abc")了。new String("abc")是如何操作的呢?来看一下String的构造器:
public String(String original) {
//other code ...
}
所以,此部分是通过new调用了String类的上面那个构造器方法创建了一个对象。
同时构造器方法的参数也是一个String对象,这个对象内容是"abc"。所以,是是创建了两个对象。
构造方法的参数的String对象是通过"abc"赋值的,其也是**文本化创建对象**,其也是在 **字符串池** 中创建,然后original变量指向 字符串池 中对象。
So,如上代码是:
* 字符串池:"abc" : 新建1个String对象
* 堆:new String:新建1个String对象
* 引用: str :新建1个
扩展1
String str1 = "abc";
String str2 = new String("abc");
第二句创建几个对象呢?1个。
因为,第一句已经在 字符串池 中创建了"abc"。第二句只在堆中new String创建一个对象。
扩展2
String str2 = new String("ABC") + "ABC" ;
字符串常量池:"ABC" : 1个String对象
堆:new String :1个String对象
引用: str2 :1个
3. 样例3
String a="ab"+"cd";
如上代码,会创建几个String对象呢? 3个吗(“ab”、“cd”、“abcd”)? 答案是1个。
原因:反编译代码后,我们发现代码是
String a = "abcd";
因为 对于静态字符串的连接操作,Java在 编译 时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
JVM执行到这一句时, 就在 字符串池 中找"abcd",找不到会在 字符串池 中创建1个String对象、值为"abcd"。
字符串常量池:"abcd":1个String对象。(java做了静态拼接,所以不是在 字符串池 中"ab"和"cd"分别创建一个对象,“+”连接后又创建了一个"abcd"对象)
堆:无
引用:a ,1个
同理:String s = “a” + “b” + “c” + “d” + “e”; 也是在 字符串池 中创建1个String对象。
扩展:
是不是所有经过 + 连接后的字符串都会放入 字符串池 呢?
我们通过如下代码样例来说明。通过 对象引用 进行 == 对比判断是否是引用同一个String对象。
==有以下两种情况:
(1)如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),是判断它们的值是否相等。
(2)如果比较的是两个对象引用,是判断它们的引用是否指向同一个对象。
public class StringTest {
public static void main(String[] args) {
String a = "ab"; // 创建了一个对象,并加入字符串池中
String b = "cd"; // 创建了一个对象,并加入字符串池中
String c = "abcd"; // 创建了一个对象,并加入字符串池中
String d = "ab" + "cd";
if (d == c) {
System.out.println(""ab"+"cd" 创建的对象 "加入了" 字符串池中");
}
else {
System.out.println(""ab"+"cd" 创建的对象 "没加入" 字符串池中");
}
String e = a + "cd";
if (e == c) {
System.out.println(" a +"cd" 创建的对象 "加入了" 字符串池中");
}
else {
System.out.println(" a +"cd" 创建的对象 "没加入" 字符串池中");
}
String f = "ab" + b;
if (f == c) {
System.out.println(""ab"+ b 创建的对象 "加入了" 字符串池中");
}
else {
System.out.println(""ab"+ b 创建的对象 "没加入" 字符串池中");
}
String g = a + b;
if (g == c) {
System.out.println(" a + b 创建的对象 "加入了" 字符串池中");
}
else {
System.out.println(" a + b 创建的对象 "没加入" 字符串池中");
}
}
}
运行结果如下:
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b 创建的对象 "没加入" 字符串池中
a + b 创建的对象 "没加入" 字符串池中
从上面的结果中看出,只有使用 引号文本方式 使用“+”连接 产生的新对象才会被加入字符串池中。因此提倡大家用 引号文本方式 来创建String对象,以提高效率,实际上这也是我们在编程中常采用的。
4. 样例4:String类型对象通过StringBuilder拼接
String a= "a";
String b= "b";
String c= "c";
String d= "d";
String str = a + b + c + d; 这句创建几个对象呢? 答案是3个对象,但只有1个String对象。
由于编译器的优化,最终代码为通过StringBuilder完成:
StringBuilder builder = new StringBuilder(); //这里创建了1个StringBuilder对象。
builder.append(a);
builder.append(b);
builder.append(c);
builder.append(d);
String str = builder.toString();
我们先看看StringBuilder的构造器
public StringBuilder() {
super(16);
}
看下去
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
可见,构造器分配了一个16字节长度的char数组。
我们看看append的整个过程:
public StringBuilder append(String str) {
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null)
str = “null”;
int len = str.length();
if (len == 0)
return this;
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);
str.getChars(0, len, value, count);
count = newCount;
return this;
}
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > count) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, offset + srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
可见,我们的字符串不会超过16个,所以不会出现扩展value的情况。
而append里面使用了arraycopy的复制方式,也没有产生新的对象。
最后,我们再看StringBuilder的 toString()方法:
public String toString() {
// Create a copy, don’t share the array
return new String(value, 0, count);
}
So,综上所述:三个对象分别为:
StringBuilder builder = new StringBuilder(); //这里创建了1个StringBuilder对象。 //StringBuilder对象的构造器产生了1个new char[capacity]
builder.append(a);
builder.append(b);
builder.append(c);
builder.append(d);
String str = builder.toString(); //这里产生了1个new String。
即,产生了3个对象,其中1个是String对象。
大家注意:如上默认的16容量,如果题目出现了总长度超过16,则会出现如下的再次分配的情况
void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
value = Arrays.copyOf(value, newCapacity);
}
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
可见,expand容量时,增加为当前(长度+1)*2。
注意这里用了Arrays的方法,注意不是前面的System.arraycopy方法。这里产生了一个新的copy的char数组,长度为新的长度。
5. 样例5:String的intern()方法
public native String intern();
这是一个本地方法。在调用此方法时:
(1)在JDK6中,调用String的intern()方法,如果字符串池中存在该字符串,则直接返回已有字符串的引用;如果没有,会把该字符串对象复制一份放到字符串池中,并返回对象引用。
(2)在JDK7及以后中,调用String的intern()方法,如果字符串池中存在该字符串,则直接返回已有字符串的引用;如果没有,会把该字符串对象的引用复制一份放到字符串常量池中,并返回字符串常量池中的该引用。
public class StringExer1 {
public static void main(String[] args) {
String s = new String("a") + new String("b"); //new String("ab")
//在上一行代码执行完以后,字符串常量池中并没有"ab"
String s2 = s.intern(); //jdk6中:在串池中创建一个字符串"ab"
//jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回
System.out.println(s2 == "ab");//jdk6:true jdk8:true
// 需要注意的是这里的"ab"并不会再去常量池中重新创建一个字符串变量。因为在常量池中"ab"已经存在(JDK6中复制的对象,是JDK7及以上,创建的堆空间中"ab"对象的引用),为了节约空间,不会重新创建
System.out.println(s == "ab");//jdk6:false jdk8:true
}
}
通过如上样例可以看出:
(1)因为对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
(2)因此要注意StringBuffer/Builder的适用场合:for循环中大量拼接字符串。(如果不用StringBuffer/Builder会频繁创建String对象)
(3)如果是静态的编译器就能感知到的拼接,采用字符串连接效率更高,不要盲目地去使用StirngBuffer/Builder。
6. 样例6:字符串和整数拼接什么效果
String firStr = "123";
String secStr = firStr + 456;
System.out.println(secStr);
打印结果是123456
public class Demo01 {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a + b);
a += b;//相当于a = a + b;
a -= b;//相当于a = a - b;
System.out.println(a);
System.out.println("" + a + b); //在运算前出现字符串,系统则会将后续出现的变量都转换为字符串进行拼接。 1020
System.out.println(a + b + ""); //在运算后出现字符串,系统则会先运算,将运算结果转换为字符串再与后面都字符串进行拼接。30
}
}
打印结果是:
30
10
1020
30