Java系列之 字符串

显而易见,字符串操作是程序设计过程中最常见的行为,本文,将深入学习Java中应用最广泛的String类及其相关的类及工具。

一、String

1、String不可变

String对象是不可变的,在JDK中是这样定义的:

1 public final class String
2     implements java.io.Serializable, Comparable<String>, CharSequence {
3     ...
4 }
View Code

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 }
View Code

输出结果:

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 }
View Code

输出结果:

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 }
View Code

输出结果:

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 }
View Code

 

运行一下这段代码,可以看到两个方法对应的字节码,先看看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 }
View Code

输出结果:

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的反射机制,改变了字符串对象,不过我们一般不会通过这种情况去改变字符串,所以,正常情况下,我们认为字符串时不可变的。

 

posted @ 2017-11-23 20:28  蒙小奇奇  阅读(95)  评论(0)    收藏  举报