我爱编程

Java基础探索之String

2018-04-09  本文已影响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及成员变量存在以下特性:

了解不可变(immutable)对象

什么是不可变对象?

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

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

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

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

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

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

为什么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对象,可以带来如下几个好处:

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

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

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

String、StringBuilder、StringBuffer的区别?

StringBuilder

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中不存在值相同的字符串实例

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

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的属性:

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的情况:

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

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";
上一篇下一篇

猜你喜欢

热点阅读