Java基础复习(二、String相关)

Java基础复习

目录

二、String相关

本章主要介绍了 String 的主要内容,包括 String 的实现和重要方法源码解读、String 的特性以及用处、常量池、老生常谈的 String、StringBuilder、StringBuffer 的异同等。

String 的内部存储

在 Java 8中,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,因此不能被继承。并且内部使用 final 修饰 char 数组来存储数据,说明 String 是不可变的。
在 Java 9中,String 内部使用 final 修饰的 byte 数组保存,同时使用 coder 来标识使用了哪种编码。

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

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

String 重要方法

在 String 类型中,我们常用的方法有很多,这边只讲几个值得注意的方法。
equals方法
equals 方法是 Object 类的方法,String 重写了 equals 方法,代码如下:

public boolean equals(Object anObject) {
	    // 先比较比较两个 String 对象内存地址,如果内存地址相同那么必定相等
        if (this == anObject) {
            return true;
        }
        // 判断参数对象类型,如果不是 String 那么肯定不相等,否则进行 char 数组元素比较
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

compareTo
compareTo 方法比较了两个字符串的“大小”,这个“大小”比较的代码如下:

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
        	// 逐个比较 char 的大小,char 的大小由 ascii 码决定
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        // 如果还没一个字符串比完了还没比完,就返回长度的比较
        return len1 - len2;
    }

由这段代码就可以知道为什么下面代码的输出是这样子了:

	String s0 = "2";
	String s1 = "10";
	String s2 = "abcd";
	String s3 = "abcde";
	System.out.println(s0.compareTo(s1));    // 1
	System.out.println(s2.compareTo(s3));    // -1

hashCode
我们知道 String 常常用来作为 HashMap 的键,因为 String 不可变因此 hash 值也不变。这里我们看一下 Java8 中 hash 值的计算方法:

	public int hashCode() {
        int h = hash;
        // 如果 hash 值存在或者 String 长度为0,则直接返回计算好的 hash 值,避免多次计算
        // 否则,hash 值的计算公式为 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        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;
    }

intern
这是个非常厉害的方法,而且我觉得面试经常会问到。
首先是代码:

	public native String intern();

这是一个 native 方法,虚拟机会去调用 C++。因此具体实现我们就不看了,我们就看看这个方法干了什么。其实这个方法干的事情很简单,就是将 String 对象放到常量池,并返回一个对它的引用。(常量池内容介绍
接下来是几段常问的代码:

	String s1 = new String("123");
	String s2 = new String("123");
	System.out.println(s1 == s2);    // false
	String s3 = s1.intern();
	System.out.println(s3 == "123");    // true
	String s4 = s2.intern();
	System.out.println(s3 == s4);    // true
    String s5 = new String("123") + new String("123");
	String s6 = new String("123123");
	String s7 = "123123";
	String s8 = s3 + s4;
	System.out.println(s5 == s6);    // false
	System.out.println(s6 == s7);    // false
	System.out.println(s7 == s8);    // false
	final String s9 = "123";
	final String s10 = "123";
	String s11 = s9 + s10;
	System.out.println(s7 == s11);    //true

s1 != s2,因为两个分别指向堆中的不同对象;
s3 == "123",因为 s3 是 s1 intern()的结果,指向常量池中 "123" 对象的地址。故相等
s3 == s4,两者都指向常量池中 "123"的地址
s5 != s6,这里我们反编译一下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=3, args_size=1
         0: new           #16                 // class java/lang/StringBuilder
         3: dup
         4: new           #18                 // class java/lang/String
         7: dup
         8: ldc           #20                 // String 123
        10: invokespecial #22                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
        13: invokestatic  #25                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
        16: invokespecial #29                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        19: new           #18                 // class java/lang/String
        22: dup
        23: ldc           #20                 // String 123
        25: invokespecial #22                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #30                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: ldc           #38                 // String 123123
        37: astore_2
        38: getstatic     #40                 // Field java/lang/System.out:Ljava/io/PrintStream;
		...
}

可以发现,String操作所谓的 + 号,其实是新建了一个 StringBuilder 并调用 append 方法和 toString 方法。因此返回的是新的 String 对象,故显然s5 != s6
s6 != s7,诶?这个我为什么要再做一遍?算了,s6指向堆中新建的对象,s7指向常量池中的对象
s7 != s8,这个反编译后跟 s5 与 s6 的原理相同
s7 == s11,这个厉害了。反编译后代码如下:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: ldc           #16                 // String 123123
         2: astore_1
         3: ldc           #18                 // String 123
         5: astore_2
         6: ldc           #18                 // String 123
         8: astore_3
         9: ldc           #16                 // String 123123
        11: astore        4
        13: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
        16: aload_1
        17: aload         4
        19: if_acmpne     26
        22: iconst_1
        23: goto          27
        26: iconst_0
        27: invokevirtual #26                 // Method java/io/PrintStream.println:(Z)V
        30: return
        LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            3      28     1    s7   Ljava/lang/String;
            6      25     2    s9   Ljava/lang/String;
            9      22     3   s10   Ljava/lang/String;
           13      18     4   s11   Ljava/lang/String;

可以看到,用 final 修饰的 String 相加在编译后并不是调用 StringBuilder 的 append 方法,而是直接用了常量池中的值!神奇的编译器。

String 的不可变性

不可变性的原因
String 的不可变性相信大家都知道,一问,很多人会说,“因为 String 保存的时候 char 数组是用 final 修饰的”。其实这么说并不准确。String 的不可变性是 Java 工程师数据类型构造能力的体现,而不仅仅是一个 final。比如说,虽然 char 数组是用 final 修饰的,但这仅仅保证了 value[] 不会再指向其他数据的地址而已,如果我们强行 value[0] = 'a',这样还是可以做到对 String 的改变的。
String 不可变性的原因包括:

  • 类型使用 final 修饰,保证了类不会被继承,避免了别人修改 String 类型的能力;
  • value 数组使用 final 修饰,保证数组不会指向别的数组;
  • value 数组使用 private 修饰,保证别人无法直接使用数组进行赋值;
  • String 类的方法中避免了对 value 内容的直接暴露,避免了别人趁机修改。
    看到了这里,真的是服了。。。

不可变性的好处
String 类型不可变性的好处主要有以下两点:

  • 安全:可以用作多线程中、可以放心用作如 HashMap、HashSet 的键值
  • 效率:不用反复创建相同的对象
  • 节约空间:同上

String 常量池

先在这里声明,网上一些博客说,字符串常量池是在运行时常量池中的,而运行时常量池是在方法区中的,因此字符串常量池也是在方法区中的,这个说法没有问题,因为虚拟机是在进化的!!在 Java 7 以前,字符串常量池与运行时常量池还没有分家,在 7 以后,字符串常量池就被放到了堆当中(我们知道,堆是放对象和数组的,这部分空间比方法区大多了)。
解决了在哪里的问题,我们来说说是什么。在写程序时,我们写了一大堆代码,其中有部分内容是不会变的,比如说类型、方法名、修饰符、常量等。在一个类还没有被加载时,这个类的相关的不会变的内容都放在静态常量池中(即.class中),加载之后,相关的内容会被加载在运行时常量池(在方法区)中,而有关 String 的常量,就被放到了堆中(上面说了 Java 7 以后字符串常量池被放到了堆中)。
解决了是什么,我们再来说说为什么。建立字符串常量池的原因,还是因为代码中 String 用的太多了,建立常量池的好处就在于节省了空间和避免反复创建。
下面来一个问题:

    // 下面这段代码发生了啥?
    String s1 = new String("123");

首先在堆上建立了一个对象,然后查找常量池中是否存在 String "123",有的话就把这个对象作为参数,调用构造函数 String()。
空口无凭,反编译为证:

  Compiled from "Test.java"
public class Test
 ...
Constant pool:
   ...
  #16 = Class              #17            // java/lang/String
  #17 = Utf8               java/lang/String
  #18 = String             #19            // 123
  #19 = Utf8               123
  #20 = Methodref          #16.#21        // java/lang/String."<init>":(Ljava/lang/String;)V
  #21 = NameAndType        #5:#22         // "<init>":(Ljava/lang/String;)V
  ...
{
  ...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #16                 // class java/lang/String
         3: dup
         4: ldc           #18                 // String 123
         6: invokespecial #20                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
           10       1     1    s1   Ljava/lang/String;
}

在 Constant Pool 中,#19 存储这字符串字面量 "123",#18 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。在 main 方法中,0: 行使用 new #16 在堆中创建一个字符串对象 #17,并且使用 ldc #18 将 String Pool 中的字符串对象作为 String 构造函数的参数。

以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。

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

String、StringBuilder 和 StringBuffer 的比较

1、可变性

  • String 不可变
  • StringBuffer 和 StringBuilder可变
    2、线程安全
  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

对于三者使用的总结
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

posted @ 2020-04-29 17:52  LewisYoung  阅读(118)  评论(0编辑  收藏  举报