在众多的编程语言里面,字符串都被广泛的使用。在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 位童鞋阅读过。