美文网首页我爱编程
Java基础探索之String

Java基础探索之String

作者: 码代码的陈同学 | 来源:发表于2018-04-09 23:25 被阅读0次

    String是Java开发中使用非常频繁的类,本文将对String的源码和设计进行探索。
    关键字:java、String、immutable、intern、hash

    文章来源:陈同学 Java基础探索之String

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        private final char value[];
        private int hash; // Default to 0
    }    
    

    直观来看,String及成员变量存在以下特性:

    • String被final修饰,即String不可被继承
    • String的实际存储为字符数组value,且value被final修饰,即value指向的数组引用不可变更

    了解不可变(immutable)对象

    什么是不可变对象?

    如果一个对象在创建之后,不能再改变它的状态,那么这个对象就是不可变的。

    状态指对象的成员变量,有以下情况:

    • 成员变量是基础数据类型,那么其值不可改变
    • 成员变量是引用类型,那么引用指向的对象不可改变(引用不能指向其他对象,但对象本身还是可以改变的)

    为什么要设计不可变对象?

    immutable对象存在以下几个优点:

    • 线程安全,因为不可改变(read-only),因此可以被多个线程安全使用。
    • 基于线程安全特性,可以在任何地方重复使用,提高性能。
    • 保证了hashcode的唯一性,如果缓存hashcode,在频繁使用对象时可以不用重复计算hashcode

    怎么创建一个不可变对象?

    本质上就是不提供任何方式改变对象的状态,以下是一些细节:

    • 使用final修饰类,确保没有任何子类可以变更其不可变的特性
    • 成员变量使用final修饰
    • 不提供任何改变对象状态的方法
    • 若对对象进行修改,使用拷贝的方式,不要返回对象本身

    为什么String要设计成immutable?

    Why do we need immutable class?

    这个问题应该拓展至一门语言为什么需要设计immutable数据类型?为什么基础数据类型往往都是immutable的?

    程序是通过代码逻辑操纵内存中的数据并最终持久化下来。内存中的数据呈现显示为各程序设计语言中的数据类型,而持久化的存储可以是DB、磁盘等。

    在程序运行时,数据会在程序中不断进行传递和变更,将基础数据类型设计成immutable类型可以使得软件构建变更容易,因为不用担心数据会在处理过程中发生变更。

    Java中为8中基础数据类型和String都设计了缓存池。

    String对象真的不可变吗?

    因为String对象实际存储是字符串数组,虽然无法直接变更引用所指向的对象,但是可以直接变更对象。

    也就是说:String对象的引用不可变,但是引用指向的对象可以被改变

        public static void main(String[] args) throws Exception {
    
            // 创建字符串"hello", 将引用s指向对象"hello". s用final修饰,无法显示将s指向其他对象
            final String s = "hello";
            System.out.println(s.hashCode());
    
            // 获取String对象中的value字段
            Field valueFiled = String.class.getDeclaredField("value");
    
            // 将value字段设置为可访问的
            valueFiled.setAccessible(true);
    
            // 通过反射获取s引用指向的对象的"value"字段的值
            char[] value = (char[]) valueFiled.get(s);
    
            // 变更对象"hello"中的第一个字符为"H", s引用还是指向这个对象
            value[0] = 'H';
    
            System.out.println(s);
            System.out.println(s.hashCode());
        }
    

    输入的结果:

    通过反射变更了实际的对象,但是没有改变引用值,引用还是指向这个对象。由于String缓存了hashcode,所以即使值变了,但是hashcode却没变。

    99162322
    Hello
    99162322
    

    设计字符串常量池的意义在哪?

    Guide to Java String Pool

    What are the benefits of string pool in Java?

    程序开发中涉及到许多池的概念,如:线程池、数据库连接池、字符串池、Spring容器管理的单例对象以及其它的一些Buffer设计等。设计这些概念并实现,无非是出于提高性能、节约资源(如内存)之类的考虑。

    由于String是immutable对象,天然的具备线程安全特性,因为可以作为全局共享对象。

    最为重要的是,String是Java中使用最为广泛的类型,与8大基础数据类型并列。实际应用中,往往String的使用更为频繁。因此设计常量池缓存String对象,可以带来如下几个好处:

    • 节约内存,遇到大量重复String时节约内存

    • 提高效率,遇到大量重复String时不用频繁创建对象

    字符串常量池在不同JDK版本中如何存储?

    • JDK1.6以前

    常量池位于方法区中的Perm区

    • jdk1.7以后

    常量池位于堆中,JDK1.8时JVM内存模型已经移除了Perm区,由Metaspace代替

    String、StringBuilder、StringBuffer的区别?

    StringBuilder

    • 利用char[]作为buffer存储数据,每次append时都是通过System.arraycopy做数据拷贝
    • char[]的长度默认16,不够时将会自动扩容
    • 非线程安全,适合单线程下字符串拼接场景

    StringBuffer

    特性和StringBuilder一样,但是每个操作方法中都用了synchronized做同步处理

    适合多线程下字符串拼接场景

    String其他的有趣点

    intern()函数的作用?

    JDK源码注释:When the intern method is invoked, if the pool already contains a string equal to this object as determined by the method,then the string from the pool is returned;Otherwise, this object is added to the pool and a reference to this object is returned.

    当调用intern方法时时,如果常量池已存在该字符串,则返回其引用;否则先将该字符串加入常量池,再返回其引用。

    特别注意:JDK1.6及以下和JDK及以上版本对于intern的处理逻辑有变化。

    假设:str为指向堆中字符串实例的引用,且String Pool中不存在值相同的字符串实例

    • JDK1.6:调用str.intern()时,若str在String Pool不存在,将拷贝一份到String Pool
    • JDK1.7:调用str.intern()时,若str在String Pool不存在,String Pool将存储堆中对象的引用

    下面通过一段代码加两张图来解释下:

    String s1 = new String("H") + new String("i");
    String s2 = s1.intern();
    String s3 = "Hi";
    
    System.out.println(s1 == s2); 
    System.out.println(s1 == s3);
    System.out.println(s2 == s3); 
    
    jdk1.6 jdk1.7

    执行s1.intern()时,String Pool也只会保存一个指向堆中对象的引用,不再像1.6那样复制一个对象实例到String Pool中

    理解String的加法运算

    开发中经常会碰到String变量之间的加法操作,那JVM实际上是如何处理的呢?

    下面看一个例子:

    String s1 = "Hello";
    String s2 = "Kitty";
    String s3 = s1 + s2;
    System.out.println(s3 == "HelloKitty"); // false
    

    s3 = s1 + s2 到底会怎么处理?我们看下这段代码对应的字节码指令:

    0: ldc           #2                  // String Hello
    2: astore_1
    3: ldc           #3                  // String Kitty
    5: astore_2
    6: new           #4                  // class java/lang/StringBuilder
    9: dup
    10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
    13: aload_1
    14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    17: aload_2
    18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    24: astore_3
    25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
    28: aload_3
    29: ldc           #9                  // String HelloKitty
    31: if_acmpne     38
    34: iconst_1
    35: goto          39
    38: iconst_0
    39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
    

    本质上,String变量的加法运算是通过StringBuilder来处理。s3 = s1 + s2实际对应的代码应该是:

    new StringBuilder().append("Hello").append("Kitty").toString();
    

    而StringBuilder的toString()源码为:

    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
    

    因此String变量的加法操作实际上是在堆中创建了一个String对象,同时返回对象在堆中的引用。

    String的hashCode函数为何要设计缓存?

    String作为使用最频繁的类型,其很多细微的设计都非常有趣。先回顾下String的属性:

    • value[] 字符数组用于存储数据
    • hash用来缓存hash code
    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
    }
    

    hashCode的源码:

    如果hash码已计算好,将不再进行再次计算,直接返回cache的hash.

    这样设计主要是考虑到一方面String使用非常频繁,另一方面String经常所为一些数据结构的检索字段,例如:Map. 缓存hash可以提升性能

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
    
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    

    String的immutable特性

    String是不可变对象,前面我们也探讨了怎么样设计一个不可变对象。这里来看看String的情况:

    • 类命、实际存储数据的字符数组都用final修饰
    • 没有提供任何setter方法
    • 操作性的函数都是拷贝新的对象

    针对第三点,我们看几个实际的函数:

    replace:返回的是new String

    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            ...
            return new String(buf, true);
            ...
        }
        return this;
    }
    

    substring: 返回的是new String

    public String substring(int beginIndex, int endIndex) {
        ...
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }
    

    编译器对于String的优化

    现在编译器会对String的操作做些基本的优化,下面用代码举例:

    编译前的Java代码

    String s1 = "H" + "i"; 
    final String s2 = "H";
    String s3 = s2 + "i";
    String s4 = s1 + s2;
    

    编译后的class进行反编译

    String s1 = "Hi";
    String s2 = "H";
    String s3 = "Hi";
    String s4 = s1 + "H";
    

    相关文章

      网友评论

        本文标题:Java基础探索之String

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