考慮下面這個例子:
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
相信這個例子很多初學者都犯過迷糊:l3
、l4
不都是 3
嗎,怎麼 l3.equals(3)
是 false
呢。
這裡面有很多點可以講的,我們一個一個來看:
Long
和 long
在 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 Conversion
和 Unboxing 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);
處下斷點,可以看到:
可以看到,String{@xxx}
和 value:byte[]{@xxx}
都是一樣的,因為它們就是同一個物件,s1
和 s2
這兩個變數的值是 指向了同一個物件(String{@674})的引用。
如果我們在 func(String s)
裡打斷點,會發現在 func(s1)
的情況下,s
和 s1
的 引用值 是一樣的。
因為 Java
是值傳遞,只不過在引用型別的情況下,傳遞的這個值,就是 引用值。
當 func
內部對這個 s
進行操作後,我們再來看看func
內部斷點的情況:
public static void func(String s) {
s = "bar";
// 斷點處,此時 s 的引用值已經變為 String{@674}
// 即此時的 s 的引用值已經不再是 s1 的引用值,自此它們已經指向的是不同的物件了。
}
由於String
的設計是不可變的,在一個String
例項上的任何增刪操作都會產生一個新的String
例項,效果與重新為變數設定新的引用值是一樣的。
我們再看看一個原始型別的斷點情況:
int i = 0;
對於原始型別的變數而言,它們的值就是本身。
==
和 equals
==
操作符在規範裡其實分了三種情況:
- 15.21.1. Numerical Equality Operators == and !=
- 15.21.2. Boolean Equality Operators == and !=
- 15.21.3. Reference Equality Operators == and !=
equals
是 Object
的方法,但是任何類都可以覆寫這個方法來實現自定義的例項間判斷,比如 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
。