[Java] 變數裡存的到底是什麼

krun發表於2018-02-26

考慮下面這個例子:

Long l1 = 1L;
Long l2 = 2L;
Long l3 = 3L;
long l4 = 3L;
Long l5 = 1 + 2L;
System.out.println(l3 == 3);
System.out.println(l4 == 3);
System.out.println(l3.equals(3));
System.out.println(l3.equals(l4));
System.out.println(l3.equals(l5));

輸出的結果是

true
true
false
true
true

相信這個例子很多初學者都犯過迷糊:l3l4 不都是 3 嗎,怎麼 l3.equals(3)false 呢。

這裡面有很多點可以講的,我們一個一個來看:

Longlong

Java 裡只有兩種型別: primitive types 原始型別 和 reference types 引用型別。

null 是一種特殊的型別

規範說明:4.1. The Kinds of Types and Values

原始型別裡包括:boolean、byte、short、int、long、char、float、double
引用型別有四種:class、interface、type、array (其中 type 我們平時遇到過的就是泛型 T,詳細內容可以查閱規範 4.4. Type Variables)

所以這裡,long 是原始型別,Long 是引用型別,這很重要,是接下來討論的基礎。

Boxing ConversionUnboxing Conversion

其實這個就是拆箱裝箱,這個知識點應該不陌生吧,就是 Java 會自動幫你把原始數值型別和原始浮點型別轉換為對應的引用型別,如 long 轉換為 Long

舉個栗子:

public void func(Long l) {
    System.out.println(l);
}

func(1L);

這段程式碼是可以跑起來的,但是如果呼叫時是這樣的 func(1),那麼就會報錯,因為 1 是整數型,它即便自動裝箱也是 Integer 而不是 Long

Objects,變數裡存的是什麼

規範中,對於 Obejct 有這麼一句話:

An object is a class instance or an array.
一個 Object 可以是一個類的例項或者是一個陣列 (一個陣列其實是一個 Object,不過這是另一個話題了。)
The reference values (often just references) are pointers to these objects, and a special null reference, which refers to no object.
引用值(通常是引用)是指向這些物件的指標和一個特殊的null引用,它不引用任何物件。

有些學過 C++ 的或是有 引用 這個概念的其他語言的同學,可能在這裡要犯迷糊了:不是說 Java 裡沒有引用嗎,怎麼規範裡又提到這個詞了。

 注意,C 裡面沒有引用只有指標,它跟 Java 一樣是值傳遞的。

其實可以這麼不嚴謹地認為:C++ 裡的 引用 是動詞,Java 裡的 引用 是名詞。

C++ 裡的引用是對一個變數定義一個別名:

int a = 2, int &ra = a;
// a為目標原名稱,ra為目標引用名。給ra賦值:ra = 1; 等價於 a = 1;
C++ 引用

Java 裡的引用就是一個值,一個指向 物件 的值。

public static void func(String s) {
    s = "bar";
}

String s1 = "foo";
func(s1);
System.out.println(s1); // "foo"

在這裡,s1 的值並不是 foo,而是一個指向 其欄位value值為 ['f', 'o', 'o']String 例項 的引用。

比如說,再宣告一個 String s2 = "foo";,然後在 func(s1); 處下斷點,可以看到:

clipboard.png

可以看到,String{@xxx}value:byte[]{@xxx} 都是一樣的,因為它們就是同一個物件,s1s2 這兩個變數的值是 指向了同一個物件(String{@674})引用

如果我們在 func(String s) 裡打斷點,會發現在 func(s1) 的情況下,ss1引用值 是一樣的。

因為 Java 是值傳遞,只不過在引用型別的情況下,傳遞的這個值,就是 引用值

func 內部對這個 s 進行操作後,我們再來看看func內部斷點的情況:

public static void func(String s) {
    s = "bar";
    // 斷點處,此時 s 的引用值已經變為 String{@674}
    // 即此時的 s 的引用值已經不再是 s1 的引用值,自此它們已經指向的是不同的物件了。
}
由於 String 的設計是不可變的,在一個 String 例項上的任何增刪操作都會產生一個新的 String 例項,效果與重新為變數設定新的引用值是一樣的。

我們再看看一個原始型別的斷點情況:

int i = 0;

clipboard.png

對於原始型別的變數而言,它們的值就是本身。

==equals

== 操作符在規範裡其實分了三種情況:

equalsObject 的方法,但是任何類都可以覆寫這個方法來實現自定義的例項間判斷,比如 Long.equals 就改成了這個樣子:

    /**
     * Compares this object to the specified object.  The result is
     * {@code true} if and only if the argument is not
     * {@code null} and is a {@code Long} object that
     * contains the same {@code long} value as this object.
     *
     * @param   obj   the object to compare with.
     * @return  {@code true} if the objects are the same;
     *          {@code false} otherwise.
     */
    public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }

也就是說,只要待判定物件不是 Long 或其子類,那麼就直接返回 false

結合前邊講的 int 在方法呼叫時會被自動裝箱成 Integer(如果引數不顯式要求 int 型別),很顯然,l3.equals(3) 會直接因為 Integer 3 不是 Long 而返回 false

相關文章