美文网首页
Java String

Java String

作者: mrjunwang | 来源:发表于2018-07-13 12:49 被阅读0次

    String即是一串字符串。在Java中,String对象是不可变对象,不可变体现在它一旦被创建便不能被改变。这可以从String的源代码看出来,首先先看String的类声明

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
        ...
    

    这里指明了String类是final的,即不能被继承。存储字符串的成员变量叫value,是一个char的数组,String所有public方法中除了构造函数之外,没有其他函数能修改value的值。这样一来保证了String对象一旦被创建,没有办法对其进行修改。

    创建一个String对象有两种办法:

    1. 创建String常量
    2. 使用new来创建
    String str1 = "Welcome";
    

    这里"Welcome"是一个String常量,编译器会创建一个String对象值为这个常量,=将这个对象赋值给了变量str1str1是一个指向这个String对象的引用。str1这个引用的内容(即"Welcome")不能被修改,但是可以指向别的常量或者String对象,比如

    String str1 = "Welcome";
    str1 = "Hello World";
    

    使用new来创建String对象比较少见,但是可行

    String str2 = new String("Welcome");
    

    存储

    String在Java中是一个非常特殊的类,它的所有实例对象的值(即value)分配的内存统一在一片叫“常量池”的内存区域。在常量池中,相同的String常量(相同是指equals返回true)只会被创建一遍。

    再回到String对象的两种创建方式,第一种String str1 = "Welcome";这行代码在执行时包含以下几步:

    1. 编译器在常量池寻找值为"Welcome"的String对象,找不到则创建一个
    2. 将这个在常量池的String对象的引用赋值给变量str1

    这就很好解释如果再执行String str3 = "Welcome";便不会再创建新的String对象,而是将原来的常量池中的"Welcome"的引用赋值给str3

    再来看String str2 = new String("Welcome");这行代码为什么不常用,因为这行代码会创建一个新的String对象,而其值却是跟"Welcome"常量的值一样。看一下String对应这行的构造函数

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    

    该构造函数将original的值和哈希赋给新的String对象,即,通过new创建的String对象会在堆(Heap)里分配内存,但其值跟常量池中对应的String对象值是一样的而且是同一片内存,即value的内存在常量池中。

    Java编译器还会对String的拼接做一些常量的优化,如

    String str4 = "str";
    String str5 = "ing";
    
    String str6 = "str" + "ing";
    String str7 = str4 + str5;
    System.out.println(str6 == str7);//false
    
    String str8 = "string";
    System.out.println(str6 == str8);//true
    

    这里为什么str6和str7不是同一个而str6和str8却是呢?答案是编译器如果能在编译阶段就能决定一个String对象的值那么会将String对象统一放到常量池中,如果只有在运行时(这里指有普通变量参与时)才能决定的String对象则不会放到常量池中。str6是由两个常量"str"和"ing"拼接,编译器能够在不执行这行代码的情况下就能决定str6的值是"string",所以代码String str6 = "str" + "ing";对于编译器来讲等价于String str6 = "string";,这便解释了str6和str8是同一个对象。而String str7 = str4 + str5;不是由常量拼接起来,对编译器来讲需要去推演str4和str5的值是多少,所以在不执行这段代码的情况下不能决定str7的值,所以str7所指的对象会是一个新创建的对象,并不在常量池中。

    字符串常量拼接有一种特殊的情况:

    public static final String A = "ab"; // 常量A
    public static final String B = "cd"; // 常量B
    public static void main(String[] args) {
        String s = A + B;  // 将两个常量用+连接对s进行初始化
        String t = "abcd";
        if (s == t) {   
            System.out.println("s等于t,它们是同一个对象");   
        } else {   
            System.out.println("s不等于t,它们不是同一个对象");   
        }   
    }
    

    以上代码输出结果是"s等于t,它们是同一个对象"。A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于 String s="ab"+"cd";

    但是

    public static final String A; // 常量A
    public static final String B;    // 常量B
    static {   
    A = "ab";   
    B = "cd";   
    }   
    public static void main(String[] args) {   
        // 将两个常量用+连接对s进行初始化   
        String s = A + B;   
        String t = "abcd";   
        if (s == t) {   
            System.out.println("s等于t,它们是同一个对象");   
        } else {   
            System.out.println("s不等于t,它们不是同一个对象");   
        }
    }
    

    输出结果为"s不等于t,它们不是同一个对象"。A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。

    运行时常量池具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

    public static void main(String[] args) {    
        String s1 = new String("计算机");
        String s2 = s1.intern();
        String s3 = "计算机";
        System.out.println("s1 == s2? " + (s1 == s2));
        System.out.println("s3 == s2? " + (s3 == s2));
    }
    

    输出结果为

    s1 == s2? false
    s3 == s2? true
    

    Unicode

    在上一小节可以看到,String常量里是可以有中文的,Java的字符串使用的编码是Unicode,每个字符的长度通常是16 bit(0x10000 到 0x10FFFF 的Unicode会占用32 bit,详见Unicode),每个字符可以用一个char类型表示。所以在和String中的每个字符打交道的时候要时刻注意字符的编码格式,比如需要将String转化为byte数组的时候需要指定编码:

    import java.io.*;
    
    public class StringConverter {
        public static String byteToHex(byte b) {
            // Returns hex String representation of byte b
            char hexDigit[] = {
                    '0', '1', '2', '3', '4', '5', '6', '7',
                    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
            };
            char[] array = { hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f] };
            return new String(array);
        }
    
        public static void printBytes(byte[] array, String name) {
            for (int k = 0; k < array.length; k++) {
                System.out.println(name + "[" + k + "] = " + "0x" +
                        byteToHex(array[k]));
            }
        }
    
        public static void main(String[] args) {
    
            System.out.println(System.getProperty("file.encoding"));
            String original = new String("A" + "\u4f60" + "\u4eec"
                    + "\u597d" + "C");
    
            System.out.println("original = " + original);
            System.out.println();
    
            try {
                byte[] utf8Bytes = original.getBytes("UTF8");
                byte[] defaultBytes = original.getBytes();
    
                String roundTrip = new String(utf8Bytes, "UTF8");
                System.out.println("roundTrip = " + roundTrip);
    
                System.out.println();
                printBytes(utf8Bytes, "utf8Bytes");
                System.out.println();
                printBytes(defaultBytes, "defaultBytes");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
    
        } // main
    
    }
    

    输出

    UTF-8
    original = A你们好C
    
    roundTrip = A你们好C
    
    utf8Bytes[0] = 0x41
    utf8Bytes[1] = 0xe4
    utf8Bytes[2] = 0xbd
    utf8Bytes[3] = 0xa0
    utf8Bytes[4] = 0xe4
    utf8Bytes[5] = 0xbb
    utf8Bytes[6] = 0xac
    utf8Bytes[7] = 0xe5
    utf8Bytes[8] = 0xa5
    utf8Bytes[9] = 0xbd
    utf8Bytes[10] = 0x43
    
    defaultBytes[0] = 0x41
    defaultBytes[1] = 0xe4
    defaultBytes[2] = 0xbd
    defaultBytes[3] = 0xa0
    defaultBytes[4] = 0xe4
    defaultBytes[5] = 0xbb
    defaultBytes[6] = 0xac
    defaultBytes[7] = 0xe5
    defaultBytes[8] = 0xa5
    defaultBytes[9] = 0xbd
    defaultBytes[10] = 0x43
    

    StringBuilder 和 StringBuffer

    既然String是一个创建了就不能修改的对象,那有些场景下需要动态拼接出来一个字符串变得就不是那么高效了,比如

    String num = "";
    for (int i=0; i<10; i++) {
        num = num + Integer.valueOf(i).toString();
    }
    System.out.println(num);
    

    这段代码企图将0到9十个数字拼接成"0123456789",然而在整个过程中每次执行 num = num + ...时都会创建一个新的String对象,而最终这些中间对象并不会被使用。当这个重复次数过多时,频繁创建这些小对象就变得耗时耗内存了。

    StringBuilder便很好的解决了这样的困境。StringBuilder正如其名,是用来build string的,上面拼接的代码可以写成

    StringBuilder sb = new StringBuilder();
    for (int i=0; i<10; i++) {
        sb.append(i);
    }
    System.out.println(sb.toString());
    

    这段代码有什么不一样呢?首先一个StringBuilder内部存储字符串的是一个普通数组,如果不指定大小默认长度为16,调用append方法会将元素直接填入StringBuilder的数组的对应位置,不会构建String对象。最终调用toString()方法的时候才将最终的字符串数组生成一个String对象。

    StringBuilder只能用于单线程的场景中,如果在多线程场景中使用,多个线程同时去append会有可能丢失或者覆盖一些元素,这就导致错误结果了。

    StringBuffer是一个线程安全的可变字符串类,跟StringBuilder不同的是它保证了多线程的安全性,可以由多个线程同时使用。但也因为如此,它的效率比StringBuilder要低(因为需要做加锁和解锁的操作)。所以如果不涉及多线程场景,推荐使用StringBuilder。

    引用前人的总结
    Java常量池理解与总结
    到底创建了几个String对象

    相关文章

      网友评论

          本文标题:Java String

          本文链接:https://www.haomeiwen.com/subject/sfpjpftx.html