Java系列之字符串

来源:互联网 发布:node sass下载失败 编辑:程序博客网 时间:2024/06/06 14:01

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

一、String

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

public final class String    implements java.io.Serializable, Comparable<String>, CharSequence {    ...}

String是通过final来修饰的,其实,String类中每一个看起来会修改String值的方法,都是创建了一个全新的String对象,用以包含修改后的字符串内容,而最初的String对象却没有做任何改动。
先看一个例子:

public class StringDemo {    public static void main(String[] args) {        String s1 = "Hello World";        System.out.println(s1);        s1 = "Hello Java";        System.out.println(s1);    }}

输出结果:

Hello WorldHello Java

咦,String对象s1的值看上去发生了变化,那么与我们上面提到的–String类是不可变的是否矛盾呢?

答案是否定的。这需要从内存与堆说起,因为s1只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。

接下来,通过String类中的toUpperCase()方法来证明,
测试用例:

public class StringDemo {    public static void main(String[] args) {        String s1 = "Hello World";        System.out.println(s1);        String s2 = upCase(s1);        System.out.println(s2);        System.out.println(s1);    }    public static String upCase(String str) {        return str.toUpperCase();    }}

输出结果:

Hello WorldHELLO WORLDHello World

String对象s1调用了String类的toUpperCase方法,将字符串中的每个字符都变成了大写,同时赋值给s2,但是s1本身却没有做任何更改,还是原来的值。
由此,我们可以得出结论,String对象是不可变的。

2、重载”+”与StringBuilder
操作符”+”可以用来连接String,
测试用例:

public class StringDemo {    public static void main(String[] args) {        String mqq = "mqq";        String s = mqq + "is a " + "doll" + 8;        System.out.println(s);    }}

输出结果:

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查看,主要字节码如下:

public static main([Ljava/lang/String;)V   L0    LINENUMBER 10 L0    LDC "mqq"    ASTORE 1   L1    LINENUMBER 11 L1    NEW java/lang/StringBuilder    DUP    ALOAD 1    INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V    LDC "is a "    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;    LDC "doll"    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;    BIPUSH 8    INVOKEVIRTUAL java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder;    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;    ASTORE 2   L2    LINENUMBER 12 L2    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;    ALOAD 2    INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V   L3    LINENUMBER 13 L3    RETURN

通过分析字节码,我们可以得出以下结论:

Java程序在通过”+”连接字符串时,会自动引入java.lang.StringBuilder类,并且通过StringBuilder的appen()方法从左到右将每个子串拼接起来,最终调用toString()方法将其转换成String对象。
那么,我们是不是可以随意使用String呢?反正编译器都会自动为我们优化性能,接下来,再看一个例子:

public class StringDemo {    public static void main(String[] args) {        String[] s = {"banana", "apple", "lemen", "orange", "grape"};        useString(s);        useStringBuilder(s);    }    public static void useStringBuilder(String[] s) {        StringBuilder result = new StringBuilder();        for (int i = 0; i < s.length; i++) {            result.append(s[i] + "\n");        }        System.out.println(result.toString());    }    public static void useString(String[] s) {        String result = "";        for (int i = 0; i < s.length; i++) {            result += s[i];        }        System.out.println(result);    }}

运行一下这段代码,可以看到两个方法对应的字节码,先看看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 210    L211     GOTO L312    L413     LINENUMBER 21 L414    FRAME APPEND [java/lang/String I]15     NEW java/lang/StringBuilder16     DUP17     ALOAD 118     INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;19     INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V20     ALOAD 021     ILOAD 222     AALOAD23     INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;24     INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;25     ASTORE 126    L527     LINENUMBER 20 L528     IINC 2 129    L330    FRAME SAME31     ILOAD 232     ALOAD 033     ARRAYLENGTH34     IF_ICMPLT L435    L636     LINENUMBER 23 L637     GETSTATIC java/lang/System.out : Ljava/io/PrintStream;38     ALOAD 139     INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V40    L741     LINENUMBER 24 L742     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 L110     ICONST_011     ISTORE 212    L213     GOTO L314    L415     LINENUMBER 14 L416    FRAME APPEND [java/lang/StringBuilder I]17     ALOAD 118     ALOAD 019     ILOAD 220     AALOAD21     INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;22     POP23    L524     LINENUMBER 13 L525     IINC 2 126    L327    FRAME SAME28     ILOAD 229     ALOAD 030     ARRAYLENGTH31     IF_ICMPLT L432    L633     LINENUMBER 16 L634     GETSTATIC java/lang/System.out : Ljava/io/PrintStream;35     ALOAD 136     INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;37     INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V38    L739     LINENUMBER 17 L740     RETURN

可以看到,不仅循环部分代码更简洁简单,而且只生成了一个StringBuilder对象。

那么,孰好孰坏,可以很直观的得出答案,因此,当我们为一个类编写toString()方法时,如果字符串的操作比较简单,那就可以信赖编译器,它会为我们合理的构造最终的字符串结果,但是,如果我们需要在toString()方法中使用循环,那么,最好自己创建一个StringBuilder对象,用它来构造最终的结果。

3.字符串常量池

JVM为了提高性能并且减少内存开销,内部维护了一个字符串常量池,当创建一个字符串常量时,JVM首先会查找常量池,若常量池中已经存在该字符串对象,则直接将该字符串对象的引用返回,否则,就创建一个新的对象放入常量池中。

但与创建字符串常量方式不同的是,当通过new String()等方式创建字符串对象时,不管内容是否相同,都会在堆内存中创建新的字符串对象,此时,需要通过equals()方法来判断字符串对象的内容是否相同

测试用例:

public class StringDemo {    public static void main(String[] args) {        String s1 = "abc";        String s2 = "abc";        String s3 = new String("abc");        System.out.println(s1 == s2);        System.out.println(s1 == s3);        System.out.println(s1.equals(s3));    }}

输出结果:

truefalsetrue

4.String、StringBuffer与StringBuilder的区别

性能上:StringBuilder > StringBuffer > Stirng

StringBuffer是线程安全的

StringBuilder是线程不安全的

并且,StringBuffer与StringBuilder都是可变的

5.另外

那么,一个字符串真的是永远不可变的吗?

接下来看一个例子:

public class StringDemo {    public static void main(String[] args) {        changeString();    }    public static void changeString() {        String s = "Hello World!";        Field valueField = null;        try {            valueField = String.class.getDeclaredField("value");            valueField.setAccessible(true);            char[] value = (char[]) valueField.get(s);            System.out.println(s);            value[5] = '_';            System.out.println(s);        } catch (Exception e) {            e.printStackTrace();        }    }}

输出结果:

Hello World!Hello_World!

可以看见,我们通过Java的反射机制,改变了字符串对象,不过我们一般不会通过这种情况去改变字符串,所以,正常情况下,我们认为字符串时不可变的。


参考资料:Java编程思想(第4版)

原创粉丝点击