在众多的编程语言里面,字符串都被广泛的使用。在Java中字符串属于对象,语言提供了String类来创建和操作字符串。
Java提供两种方式来定义字符串,例如:
定义字符使用单引号,定义字符串使用双引号;
// 直接赋值
String str1 = "hello world";
// 构造方法
String str2 = new String("hello world");
通过对String源码的查看:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
……
}
从上面的代码我们可以得出两点结论:
对字符串的每一次操作,例如连接子串都会重新创建一个新的String对象。我们可以从String中的concat方法源码中可以看出这一点,代码如下:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
当被连接的子串的长度为0时,直接返回自身,连接一个长度不为0的子串,通过char数组的系列操作,重新生成一个新的String对象。
所以在此要注意 对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。
上面写了两种定义字符串的方式,不知道大家知道这两种方式的区别和联系么?
// 直接赋值
String str1 = "hello world";
// 构造方法
String str2 = new String("hello world");
String str3 = "hello world";
String str4 = new String("hello world");
System.out.println(str1==str2);
System.out.println(str1==str3);
System.out.println(str2==str4);
你能直接说出上面的执行结果么?如果不能请继续往下看,能的话也请继续往下看。
具体的结果如下:
false true false
在class文件中有一部分来存储编译期间生成的字面常量以及符号引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。在上述的代码中String str1 = "hello world";和String str2 = new String("hello world");都在编译期生成了字面常量和符号引用,运行期间字面常量"hello world"都被存储在运行时常量池。JVM执行引擎会在运行时常量池中查找是否存在相同的字面常量,若有则直接将引用指向已经存在的字面常量;否则在运行时常量池中开辟一个新的空间来存储该字面量,并将引用指向该字面常量,通过这种方式来把String对象跟引用绑定。
通过new关键字生成对象这个过程是在堆heap中进行的,而在堆进行对象生成过程中,不会有检查对象是否已经存在这个行为。因此通过new来创建对象,创建出来的一定是新的对象,即在内存中有着新的内存地址,但字符串的内容是相同的。
下面是Java中不同变量在内存中存放的位置:
| 变量 | 内存位置 |
|---|---|
| new出来的对象 | heap 堆 |
| 局部变量、基本数据类型 | stack 栈 |
| 静态变量、字符串、常量 | data segment 数据区 |
| 代码 | code segment 代码区 |
为什么已经存在了String了,还会出现StringBuffer、StringBuilder?
如果一个字符串需要连接10000次其他的字符串,实现代码如下:
public class Main {
public static void main(String[] args){
String string = "";
for(int i=0;i<10000;i++){
string = string.concat("hello");
}
}
}
上述代码不断的new字符串对象,前面已经说了重要的一点 对String类对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。 ,这种代码将会有多大的内存消耗。这个时候想必大家已经有了点答案。我将上述的代码稍微的修改一下:
public class Main {
public static void main(String[] args){
String string = "";
for(int i=0;i<10000;i++){
string += "hello";
}
}
}
两部分代码看似只有一点差异,其实两者的内存消耗有着天大的差别。我们通过javap命令来反编译.class文件。具体内容如下:
D:/work/javaLearn/out/production/javaLearn>javap -c Main
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: sipush 10000
9: if_icmpge 38
12: new #3 // class java/lang/StringBuilder
15: dup
16: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
19: aload_1
20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: ldc #6 // String hello
25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_1
32: iinc 2, 1
35: goto 5
38: return
}
从上面反编译出来的字节码中可以看出一点门道:string+="hello"的操作事实上会自动被JVM优化成StringBuilder类的append操作。
那么有人会问既然有了StringBuilder类,为什么还需要StringBuffer类?查看源代码便一目了然,事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。
我们来看下面的代码:
public class Main {
public static void main(String[] args){
String str1 = "I "+"love "+"you";
String str2 = "I ";
String str3 = "love ";
String str4 = "you ";
String str5 = str2 + str3 + str4;
}
}
用javap命令来反编译.class文件:
D:/work/javaLearn/out/production/javaLearn>javap -c Main
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String I love you
2: astore_1
3: ldc #3 // String I
5: astore_2
6: ldc #4 // String love
8: astore_3
9: ldc #5 // String you
11: astore 4
13: new #6 // class java/lang/StringBuilder
16: dup
17: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
20: aload_2
21: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_3
25: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: aload 4
30: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore 5
38: return
}
str1在编译之后就被直接赋值为"I love you";str5却没有什么操作。综上所述我们可以得出一些结论:
String a = "hello2"; String b = "hello" + 2; System.out.println((a == b));
结果是true,它String b = "hello" + 2; 被编译器优化成了String b = "hello2"; 所以运行时字符串a和b指向同一个对象。
String a = "hello2"; String b = "hello"; String c = b + 2; System.out.println((a == c));
输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,通过StringBuilder生成了一个新的对象,因此这种方式生成的对象事实上是保存在堆上的。
String a = "hello2"; final String b = "hello"; String c = b + 2; System.out.println((a == c));
输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2;
字符串的故事就暂时说到这里,后续有的话就继续更新。
最后更新于 2018-06-29 09:13:49 并被添加「java Java学习系列文章」标签,已有 0 位童鞋阅读过。