Java基础探索之String

String是Java开发中使用非常频繁的类,本文将对String的源码和设计进行探索。

文章来源:陈同学 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";

猜你喜欢

转载自blog.csdn.net/myle69/article/details/79874624