Java系列之 字符串
显而易见,字符串操作是程序设计过程中最常见的行为,本文,将深入学习Java中应用最广泛的String类及其相关的类及工具。
一、String
1、String不可变
String对象是不可变的,在JDK中是这样定义的:
1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence { 3 ... 4 }
String是通过final来修饰的,其实,String类中每一个看起来会修改String值的方法,都是创建了一个全新的String对象,用以包含修改后的字符串内容,而最初的String对象却没有做任何改动。
先看一个例子:
1 public class StringDemo { 2 public static void main(String[] args) { 3 String s1 = "Hello World"; 4 System.out.println(s1); 5 s1 = "Hello Java"; 6 System.out.println(s1); 7 } 8 }
输出结果:
Hello World
Hello Java
咦,String对象s1的值看上去发生了变化,那么与我们上面提到的--String类是不可变的是否矛盾呢?
答案是否定的。这需要从内存与堆说起,因为s1只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。
接下来,通过String类中的toUpperCase()方法来证明,
测试用例:
1 public class StringDemo { 2 public static void main(String[] args) { 3 String s1 = "Hello World"; 4 System.out.println(s1); 5 String s2 = upCase(s1); 6 System.out.println(s2); 7 System.out.println(s1); 8 } 9 public static String upCase(String str) { 10 return str.toUpperCase(); 11 } 12 }
输出结果:
Hello World
HELLO WORLD
Hello World
String对象s1调用了String类的toUpperCase方法,将字符串中的每个字符都变成了大写,同时赋值给s2,但是s1本身却没有做任何更改,还是原来的值
由此,我们可以得出结论,String对象是不可变的
2、重载"+"与StringBuilder
操作符"+"可以用来连接String,
测试用例:
1 public class StringDemo { 2 public static void main(String[] args) { 3 String mqq = "mqq"; 4 String s = mqq + "is a " + "doll" + 8; 5 System.out.println(s); 6 } 7 }
输出结果:
mqqis a doll8
那么第4行代码是怎么工作的呢?
别着急,为了说清楚这个问题,需要先在Eclipse中安装一个查看字节码的插件 ByteCode Outline,
安装方法:Help -> Install new Software... -> Work with->Add,在Location中填入URL:http://andrei.gmxhome.de/eclipse/,选择 ByteCode Outline安装
安装完后并不是马上就有,需要手动打开,Eclipse菜单Windows -> Show View -> Other,然后在Java类里找到Bytecode并添加到下方的Tab中。
安装好后,再运行程序,点开Bytecode查看,主要字节码如下:
1 public static main([Ljava/lang/String;)V 2 L0 3 LINENUMBER 10 L0 4 LDC "mqq" 5 ASTORE 1 6 L1 7 LINENUMBER 11 L1 8 NEW java/lang/StringBuilder 9 DUP 10 ALOAD 1 11 INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; 12 INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V 13 LDC "is a " 14 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 15 LDC "doll" 16 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 17 BIPUSH 8 18 INVOKEVIRTUAL java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder; 19 INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String; 20 ASTORE 2 21 L2 22 LINENUMBER 12 L2 23 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 24 ALOAD 2 25 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V 26 L3 27 LINENUMBER 13 L3 28 RETURN
通过分析字节码,我们可以得出以下结论:
- Java程序在通过"+"连接字符串时,会自动引入java.lang.StringBuilder类,并且通过StringBuilder的appen()方法从左到右将每个子串拼接起来,最终调用toString()方法将其转换成String对象。
那么,我们是不是可以随意使用String呢?反正编译器都会自动为我们优化性能,接下来,再看一个例子:
1 public class StringDemo { 2 public static void main(String[] args) { 3 String[] s = {"banana", "apple", "lemen", "orange", "grape"}; 4 useString(s); 5 useStringBuilder(s); 6 } 7 public static void useStringBuilder(String[] s) { 8 StringBuilder result = new StringBuilder(); 9 for (int i = 0; i < s.length; i++) { 10 result.append(s[i] + "\n"); 11 } 12 System.out.println(result.toString()); 13 } 14 public static void useString(String[] s) { 15 String result = ""; 16 for (int i = 0; i < s.length; i++) { 17 result += s[i]; 18 } 19 System.out.println(result); 20 } 21 }
运行一下这段代码,可以看到两个方法对应的字节码,先看看useString()方法:
1 public static useString([Ljava/lang/String;)V 2 L0 3 LINENUMBER 19 L0 4 LDC "" 5 ASTORE 1 6 L1 7 LINENUMBER 20 L1 8 ICONST_0 9 ISTORE 2 10 L2 11 GOTO L3 12 L4 13 LINENUMBER 21 L4 14 FRAME APPEND [java/lang/String I] 15 NEW java/lang/StringBuilder 16 DUP 17 ALOAD 1 18 INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; 19 INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V 20 ALOAD 0 21 ILOAD 2 22 AALOAD 23 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 24 INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String; 25 ASTORE 1 26 L5 27 LINENUMBER 20 L5 28 IINC 2 1 29 L3 30 FRAME SAME 31 ILOAD 2 32 ALOAD 0 33 ARRAYLENGTH 34 IF_ICMPLT L4 35 L6 36 LINENUMBER 23 L6 37 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 38 ALOAD 1 39 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V 40 L7 41 LINENUMBER 24 L7 42 RETURN
其中11-34是一个循环体,注意重点是StringBuilder对象是在循环体内构造的,意味着没循环一次,就会创建一个StringBuilder对象。
接下来是useStringBuilder方法对应的字节码:
1 public static useStringBuilder([Ljava/lang/String;)V 2 L0 3 LINENUMBER 12 L0 4 NEW java/lang/StringBuilder 5 DUP 6 INVOKESPECIAL java/lang/StringBuilder.<init>()V 7 ASTORE 1 8 L1 9 LINENUMBER 13 L1 10 ICONST_0 11 ISTORE 2 12 L2 13 GOTO L3 14 L4 15 LINENUMBER 14 L4 16 FRAME APPEND [java/lang/StringBuilder I] 17 ALOAD 1 18 ALOAD 0 19 ILOAD 2 20 AALOAD 21 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 22 POP 23 L5 24 LINENUMBER 13 L5 25 IINC 2 1 26 L3 27 FRAME SAME 28 ILOAD 2 29 ALOAD 0 30 ARRAYLENGTH 31 IF_ICMPLT L4 32 L6 33 LINENUMBER 16 L6 34 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 35 ALOAD 1 36 INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String; 37 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V 38 L7 39 LINENUMBER 17 L7 40 RETURN
可以看到,不仅循环部分代码更简洁简单,而且只生成了一个StringBuilder对象。
那么,孰好孰坏,可以很直观的得出答案,因此,当我们为一个类编写toString()方法时,如果字符串的操作比较简单,那就可以信赖编译器,它会为我们合理的构造最终的字符串结果,但是,如果我们需要在toString()方法中使用循环,那么,最好自己创建一个StringBuilder对象,用它来构造最终的结果。
3.字符串常量池
JVM为了提高性能并且减少内存开销,内部维护了一个字符串常量池,当创建一个字符串常量时,JVM首先会查找常量池,若常量池中已经存在该字符串对象,则直接将该字符串对象的引用返回,否则,就创建一个新的对象放入常量池中。
但与创建字符串常量方式不同的是,当通过new String()等方式创建字符串对象时,不管内容是否相同,都会在堆内存中创建新的字符串对象,此时,需要通过equals()方法来判断字符串对象的内容是否相同
测试用例:
1 public class StringDemo { 2 public static void main(String[] args) { 3 String s1 = "abc"; 4 String s2 = "abc"; 5 String s3 = new String("abc"); 6 System.out.println(s1 == s2); 7 System.out.println(s1 == s3); 8 System.out.println(s1.equals(s3)); 9 } 10 }
输出结果:
true false true
4.String、StringBuffer与StringBuilder的区别
性能上:StringBuilder > StringBuffer > Stirng
StringBuffer是线程安全的
StringBuilder是线程不安全的
并且,StringBuffer与StringBuilder都是可变的
5.另外
那么,一个字符串真的是永远不可变的吗?
接下来看一个例子:
1 public class StringDemo { 2 public static void main(String[] args) { 3 changeString(); 4 } 5 6 public static void changeString() { 7 String s = "Hello World!"; 8 Field valueField = null; 9 try { 10 valueField = String.class.getDeclaredField("value"); 11 valueField.setAccessible(true); 12 char[] value = (char[]) valueField.get(s); 13 System.out.println(s); 14 value[5] = '_'; 15 System.out.println(s); 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } 19 } 20 }
输出结果:
Hello World!
Hello_World!
可以看见,我们通过Java的反射机制,改变了字符串对象,不过我们一般不会通过这种情况去改变字符串,所以,正常情况下,我们认为字符串时不可变的。

浙公网安备 33010602011771号