ホームページ  >  記事  >  Java  >  Java String クラスの内容の包括的な分析 (コード付き)

Java String クラスの内容の包括的な分析 (コード付き)

不言
不言オリジナル
2018-09-15 16:55:312065ブラウズ

この記事は Java String クラスの包括的な分析に関するものです (コード付き)。必要な方は参考にしていただければ幸いです。

Java を書き始めてからこの 1 年間、私は遭遇した問題を解決しようと努めてきましたが、Java 言語の機能を深く学習したり、Java 言語のソース コードを読んだりすることはしていませんでした。 JDKの詳細。将来的には Java
に依存して生きていくと決めたのですから、やはり Java

について少し考え、ゲームをする時間を諦めて、システムを徹底的に研究する必要があります。

Java String は、Java プログラミングで最も一般的に使用されるクラスの 1 つであり、JDK によって提供される最も基本的なクラスです。そこで、良いスタートを切るために、String クラスから始めて徹底的に研究することにしました。

クラス定義とクラス メンバー

JDK で String ソース コードを開くと、まず String クラスの定義に注目する必要があります。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence

非継承および不変


Java を書いたことがある人なら誰でも、final キーワードがクラスを変更するとき、それはこのクラスが継承不可能であることを意味することを知っています。したがって、String クラスを外部に継承することはできません。現時点では、String の設計者がなぜ String を継承できないように設計したのか疑問に思うかもしれません。 Zhihu で関連する質問とディスカッションを見つけました。
最初の回答で非常に明確になったと思います。 Java の最も基本的な参照データ型である String の最も重要な点は不変であるため、final を使用すると **継承を禁止することになります**。これにより、String の不変の性質**が破壊されます。

クラスの不変性を実現するには、final を使用してクラスを変更するだけでは済みません。ソース コードから、String が実際には文字配列と文字のカプセル化であることがわかります。配列はプライベートであり、

文字配列を変更できるメソッドを提供しないため、初期化が完了すると String オブジェクトを変更できません。

Serialization

上記のクラス定義から、String はシリアル化インターフェイス Serializable を実装しているため、String はシリアル化と逆シリアル化をサポートしていることがわかります。

Java オブジェクトのシリアル化とは何ですか?私のような多くの Java 初心者がこの疑問を抱いていると思います。 Java のシリアル化と逆シリアル化の詳細な分析 この記事のこの段落
で非常に詳しく説明されています。
Java プラットフォームを使用すると、メモリ内に再利用可能な Java オブジェクトを作成できますが、一般に、

これらのオブジェクトは、JVM が実行されているときにのみ存在します。
つまり、これらのオブジェクトのライフサイクルは存在しません。 JVM よりもライフサイクルが長いです。しかし、実際のアプリケーションでは、
では、JVM の実行が停止した後に指定されたオブジェクトを保存 (永続化) でき、保存されたオブジェクトを将来再読み取りできることが必要な場合があります。
Java オブジェクトのシリアル化は、この機能の実現に役立ちます。
Java オブジェクトのシリアル化を使用すると、オブジェクトを保存するときにその状態がバイトのセットとして保存され、将来的にはこれらのバイトがオブジェクトに組み立てられます。
オブジェクトのシリアル化では、オブジェクトの「状態」、つまりそのメンバー変数が保存されることに注意する必要があります。オブジェクトのシリアル化では、クラス内の静的変数に注意が払われないことがわかります。
オブジェクトのシリアル化は、オブジェクトを永続化するときに使用されるだけでなく、RMI (リモート メソッド呼び出し) を使用するときや、ネットワーク上でオブジェクトを渡すときにも使用されます。
Java Serialization API は、オブジェクトのシリアル化を処理するための標準メカニズムを提供します。この API はシンプルで使いやすいです。
String ソース コードでは、シリアル化をサポートするクラス メンバーの定義も確認できます。

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
serialVersionUID はシリアル化のバージョン番号です。Java はこの UID を使用して、逆シリアル化中のバイト ストリームとローカル クラスの一貫性を判断します。それらが同じである場合、それらは一貫しているとみなされ、

逆シリアル化できます。 . 異なる場合は例外がスローされます。

serialPersistentFields この定義は、前の定義よりもはるかにまれであり、おそらくシリアル化中のクラス メンバーに関連しています。このフィールドの意味を理解するために、Baidu で Google 検索したところ、

JDK ドキュメント内でクラス ObjectStreamField についての短い説明 (「シリアル化可能なクラスからのシリアル化可能なフィールドの説明。
ObjectStreamFields の配列」) しか見つかりませんでした。は、クラスのシリアル化可能なフィールドを宣言するために使用されます。` 一般的な考え方は、このクラスはシリアル化されたクラスのシリアル化されたフィールドを記述するために使用されるということです。
この型の配列を定義する場合、フィールドを宣言できます。クラスをシリアル化する必要があります。しかし、このクラスの具体的な使い方や機能はまだわかりません。その後、このフィールドの定義を詳しく調べて、
と SerialVersionUID も特定のフィールド名を通じてさまざまなルールを定義する必要があります。その後、serialPersistentFields というキーワードを直接検索して、その特定の役割を見つけました。
つまり、**デフォルトのシリアル化カスタマイズには、キーワード transient と静的フィールド名 SerialPersistentFields が含まれています。transient は、デフォルトでシリアル化されないフィールドを指定するために使用されます。
serialPersistentFields は、どのフィールドをシリアル化する必要があるかを指定します。デフォルト。 SerialPersistentFields と transient の両方が定義されている場合、transient は無視されます。 **
私も自分でテストしましたが、確かにこの効果があります。

知道了 serialPersistentFields 的作用以后,问题又来了,既然这个静态字段是用来定义参与序列化的类成员的,那为什么在 String 中这个数组的长度定义为0?
经过一番搜索查找资料以后,还是没有找到一个明确的解释,期待如果有大佬看到能解答一下。

可排序

String 类还实现了 Comparable 接口,Comparable接口只有一个方法 public int compareTo(T o),实现了这个接口就意味着该类支持排序,
即可用 Collections.sort 或 Arrays.sort 等方法对该类的对象列表或数组进行排序。

在 String 中我们还可以看到这样一个静态变量,

 public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

从上面的源码中可以看出,这个静态成员是一个实现了 Comparator 接口的类的实例,而实现这个类的作用是比较两个忽略大小写的 String 的大小。

那么 Comparable 和 Comparator 有什么区别和联系呢?同时 String 又为什么要两个都实现一遍呢?

第一个问题这里就不展开了,总结一下就是,Comparable 是类的内部实现,一个类能且只能实现一次,而 Comparator 则是外部实现,可以通过不改变
类本身的情况下,为类增加更多的排序功能。
所以我们也可以为 String 实现一个 Comparator使用。

String 实现了两种比较方法的意图,实际上是一目了然的。实现 Comparable 接口为类提供了标准的排序方案,同时为了满足大多数排序需求的忽略大小写排序的情况,
String 再提供一个 Comparator 到公共静态类成员中。如果还有其他的需求,那就只能我们自己实现了。

类方法

String 的方法大致可以分为以下几类。

  • 构造方法

  • 功能方法

  • 工厂方法

  • intern方法

关于 String 的方法的解析,这篇文章已经解析的够好了,所以我这里也不再重复的说一遍了。不过
最后的 intern 方法值得我们去研究。

intern方法

字符串常量池

String 做为 Java 的基础类型之一,可以使用字面量的形式去创建对象,例如 String s = "hello"。当然也可以使用 new 去创建 String 的对象,
但是几乎很少看到这样的写法,久而久之我便习惯了第一种写法,但是却不知道背后大有学问。下面一段代码可以看出他们的区别。

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello world";
        String s2 = new String("hello world");
        String s3 = "hello world";
        String s4 = new String("hello world");
        String s5 = "hello " + "world";
        String s6 = "hel" + "lo world";
        String s7 = "hello";
        String s8 = s7 + " world";
        
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2) );
        System.out.println("s1.equals(s2): " + String.valueOf(s1.equals(s2)));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
        System.out.println("s1.equals(s3): " + String.valueOf(s1.equals(s3)));
        System.out.println("s2 == s4: " + String.valueOf(s2 == s4));
        System.out.println("s2.equals(s4): " + String.valueOf(s2.equals(s4)));
        System.out.println("s5 == s6: " + String.valueOf(s5 == s6));
        System.out.println("s1 == s8: " + String.valueOf(s1 == s8));
    }
}
/* output
s1 == s2: false
s1.equals(s2): true
s1 == s3: true
s1.equals(s3): true
s2 == s4: false
s2.equls(s4): true
s5 == s6: true
s1 == s8: false
 */

从这段代码的输出可以看到,equals 比较的结果都是 true,这是因为 String 的 equals 比较的值( Object 对象的默认 equals 实现是比较引用,
String 对此方法进行了重写)。== 比较的是两个对象的引用,如果引用相同则返回 true,否则返回 false。s1==s2: false和 s2==s4: false
说明了 new 一个对象一定会生成一个新的引用返回。s1==s3: true 则证明了使用字面量创建对象同样的字面量会得到同样的引用。

s5 == s6 实际上和 s1 == s3 在 JVM 眼里是一样的情况,因为早在编译阶段,这种常量的简单运算就已经完成了。我们可以使用 javap 反编译一下 class 文件去查看
编译后的情况。

➜ ~ javap -c StringConstPool.class
Compiled from "StringConstPool.java"
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello world
       2: astore_1
       3: return
}

看不懂汇编也没关系,因为注释已经很清楚了......

s1 == s8 的情况就略复杂,s8 是通过变量的运算而得,所以无法在编译时直接算出其值。而 Java 又不能重载运算符,所以我们在 JDK 的源码里也
找不到相关的线索。万事不绝反编译,我们再通过反编译看看实际上编译器对此是否有影响。

public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String  world
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_2
      23: return
}

通过反编译的结果可以发现,String 的变量运算实际上在编译后是由 StringBuilder 实现的,s8 = s7 + " world" 的代码等价于
(new StringBuilder(s7)).append(" world").toString()。Stringbuilder 是可变的类,通过 append 方法 和 toString 将两个 String 对象聚合
成一个新的 String 对象,所以到这里就不难理解为什么 s1 == s8 : false 了。

之所以会有以上的效果,是因为有字符串常量池的存在。字符串对象的分配和其他对象一样是要付出时间和空间代价,而字符串又是程序中最常用的对象,JVM
为了提高性能和减少内存占用,引入了字符串的常量池,在使用字面量创建对象时, JVM 首先会去检查常量池,如果池中有现成的对象就直接返回它的引用,如果
没有就创建一个对象,并放到池里。因为字符串不可变的特性,所以 JVM 不用担心多个变量引用同一个对象会改变对象的状态。同时运行时实例创建的全局
字符串常量池中有一个表,总是为池中的每个字符串对象维护一个引用,所以这些对象不会被 GC 。

intern 方法的作用

上面说了很多都没有涉及到主题 intern 方法,那么 intern 方法到作用到底是什么呢?首先查看一下源码。

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

Oracle JDK 中,intern 方法被 native 关键字修饰并且没有实现,这意味着这部分到实现是隐藏起来了。从注释中看到,这个方法的作用是如果常量池
中存在当前字符串,就会直接返回当前字符串,如果常量池中没有此字符串,会将此字符串放入常量池中后再返回。通过注释的介绍已经可以明白这个方法的作用了,
再用几个例子证明一下。

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = s2.intern();
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
    }
}
/* output
s1 == s2: false
s1 == s3: true
*/

这里就很容易的了解 intern 实际上就是把普通的字符串对象也关联到常量池中。

以上がJava String クラスの内容の包括的な分析 (コード付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。