• 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

  • 相关阅读:
    poj 3041 Asteroids (最大匹配最小顶点覆盖——匈牙利模板题)
    poj 2060 Taxi Cab Scheme (最小路径覆盖)
    poj 2728 Desert King (最小比例生成树)
    poj 2449 Remmarguts' Date(第K短路问题 Dijkstra+A*)
    poj 3463 Sightseeing( 最短路与次短路)
    研究生flag
    插入排序和堆排序
    根据二叉树的中序遍历和层次遍历还原二叉树
    关于AVL实现的代码记录
    回文数猜想(与6174问题很像)
  • 原文地址:https://www.cnblogs.com/lewisyoung/p/12803686.html
Copyright © 2020-2023  润新知