首頁 > Java > java教程 > 主體

全面解析Java String類別的內容(附程式碼)

不言
發布: 2018-09-15 16:55:31
原創
2026 人瀏覽過

這篇文章帶給大家的內容是關於全面解析Java String類別的內容(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

開始寫 Java 一年來,一直都是遇到什麼問題再去解決,還沒有主動的深入的去學習過 Java 語言的特性和深入閱讀 JDK 的源碼。既然決定今後靠 Java
吃飯,還是得花些心思在上面,放棄一些打遊戲的時間,系統深入的去學習。

Java String 是 Java 程式設計中最常用的類別之一,也是 JDK 提供的最基礎的類別。所以我決定先從 String 類別入手,深入的研究一番來開個好頭。

類別定義與類別成員

開啟 JDK 中的 String 原始碼,最先應關注 String 類別的定義。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
登入後複製

不可繼承與不可變

寫過 Java 的人都知道, 當 final 關鍵字修飾類別時,代表這類不可繼承。所以 String 類別是不能被外部繼承。這時候我們可能會好奇,String 的設計者
為什麼要把它設計成不可繼承的。我在知乎上找到了相關的問題和討論,
我覺得首位的回答已經說的很明白了。 String 做為 Java 的最基礎的引用資料類型,最重要的一點就是不可變性,所以使用 final 就是為了**禁止繼承
破壞了 String 的不可變的性質**。

實作類別的不可變性,不光是用final 修飾類別這麼簡單,從原始碼可以看到,String 其實是對一個字元陣列的封裝,而字元陣列是私有的,並且沒有提供
任何可以修改字元陣列的方法,所以一旦初始化完成, String 物件就無法被修改。

序列化

從上面的類別定義我們看到了 String 實作了序列化的介面 Serializable,所以 String 是支援序列化和反序列化的。
什麼是Java物件的序列化?相信很多跟我一樣的 Java 菜鳥都有這樣疑問。深入分析Java的序列化與反序列化這篇文章中的這段話
解釋的很好。

Java平台讓我們可以在記憶體中建立可重複使用的Java對象,但一般情況下,
只有當JVM處於執行時,這些物件才可能存在,
即,這些物件的生命週期不會比JVM的生命週期更長。但在現實應用中,
就可能要求在JVM停止運行之後能夠保存(持久化)指定的對象,並在將來重新讀取被保存的對象。
Java物件序列化就能夠幫助我們實現該功能。
使用Java物件序列化,在儲存物件時,會把其狀態儲存為一組位元組,在未來,再將這些位元組組裝成物件。
必須注意地是,物件序列化保存的是物件的”狀態”,即它的成員變數。由此可知,物件序列化不會關注類別中的靜態變數。
除了在持久化物件時會用到物件序列化之外,當使用RMI(遠端方法呼叫),或在網路中傳遞物件時,都會用到物件序列化。
Java序列化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 這個定義則比上一個少見許多,大概猜到是與序列化時的類別成員有關係。為了弄清楚這個欄位的意義,我google 百度齊上,也
僅僅只找到了JDK 文件對類別ObjectStreamField的一丁點描述, `A description of a Serializable field from a Serializable class.
An array of ObjectStreamFields is used to declare the Serializable fields of a class.` 大意是這個類別用來描述序列化類別的一個序列化字段,
如果定義一個此類的數組則可以聲明類別需要被序列化的字段。但是還是沒有找到這個類別的具體用法和作用是怎樣的。後來我仔細看了一下這個字段的定義,
與 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中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!