歡迎光臨
每天分享高質量文章

Java 有值型別嗎

(點擊上方公眾號,可快速關註)


來源:正義的花生 ,

www.jianshu.com/p/f8a2ec530852

有人看了我之前的文章『Swift 語言的設計錯誤』,問我:“你說 Java 只有取用型別(reference type),但是根據 Java 的官方文件,Java 也有值型別(value type)和取用型別的區別的。比如 int,boolean 等原始型別就是值型別。” 現在我來解釋一下這個問題。

Swift 語言的設計錯誤

http://www.yinwang.org/blog-cn/2016/06/06/swift

官方文件

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

Java 有值型別,原始型別 int,boolean 等是值型別,其實是長久以來的一種誤解,它混淆了實現和語意的區別。不要以為 Java 的官方文件那樣寫就是權威定論,就可以說“王垠不懂” 🙂 當你認為王垠不懂一個初級問題的時候,都需要三思,因為他可能是大智若愚…… 看了我下麵的論述,也許你會發現自己應該懷疑的是,Java 的設計者到底有沒有搞明白這個問題。

胡扯結束,現在來說正事。Java,Scheme 等語言的原始型別,比如 char,int,boolean,double 等,在“實現”上確實是通過值(而不是取用,或者叫指標)直接傳遞的,然而這完全是一種為了效率的優化(叫做 inlining)。這種優化對於程式員應該是不可見的。Java 繼承了 Scheme/Lisp 的衣缽,它們在“語意”上其實是沒有值型別的。

這不是天方夜譚,為了理解這一點,你可以做一個很有意思的思維實驗。現在你把 Java 裡面所有的原始型別都“想象”成取用型別,也就是說,所有的 int, boolean 等原始型別的變數都不包含實際的資料,而是取用(或者叫指標),指向堆上分配的資料。然後你會發現這樣“改造後”的 Java,仍然符合現有 Java 代碼里能看到的一切現象。也就是說,原始型別被作為值型別還是取用型別,對於程式員完全沒有區別。

舉個簡單的例子,如果我們把 int 的實現變成完全的取用,然後來看這段代碼:

int x = 1;    // x指向記憶體地址A,內容是整數1

int y = x;    // y指向同樣的記憶體地址A,內容是整數1

x = 2;        // x指向另一個記憶體地址B,內容是整數2。y仍然指向地址A,內容是1。

由於我們改造後的 Java 裡面 int 變數全部是取用,所以第一行定義的 x 並不包含一個整數,而是一個取用,它指向堆里分配的一塊記憶體,這個空間的內容是整數 1。在第二行,我們定 int 變數 y,當然它也是一個取用,它的值跟 x 一樣,所以 y 也指向同一個地址,裡面的內容是同一個整數:1。在第三行,我們對 x 這個取用賦值。你會發現一個很有意思的現象,雖然 x 指向了 2,y 卻仍然指向 1。對 x 賦值並沒能改變 y 指向的內容,這種情況就跟 int 是值型別的時候一模一樣!所以現在雖然 int 變數全部是取用,你卻不能實現共享地址的取用能做的事情:對 x 進行某種操作,導致 y 指向的內容也發生改變。

出現這個現象的原因是,雖然現在 int 成了取用型別,你卻並不能對它進行取用型別所特有(而值型別沒有)的操作。這樣的操作包括:

  1. deref。就像 C 語言里的 * 運算子。

  2. 成員賦值。就像對 C struct 成員的 x.foo = 2 。

在 Java 里,你沒法寫像 C 語言的 *x = 2 這樣的代碼,因為 Java 沒有提供 deref 運算子 *。你也沒法通過 x.foo = 2 這樣的陳述句改變 x 所指向的記憶體資料(內容是1)的一部分,因為 int 是一個原始型別。最後你發現,你只能寫 x = 2,也就是改變 x 這個取用本身的指向。x = 2 執行之後,原來數字 1 所在的記憶體空間並沒有變成 2,只不過 x 指向了新的地址,那裡裝著數字 2 而已。指向 1 的其它取用變數比如 y,不會因為你進行了 x = 2 這個操作而看到 2,它們仍然看到原來那個1……

在這種 int 是取用的 Java 里,你對 int 變數 x 能做的事情只有兩種:

  1. 讀出它的值。

  2. 對它進行賦值,使它指向另一個地方。

這兩種事情,就跟你能對值型別能做的兩件事情沒有區別。這就是為什麼你沒法通過對 x 的操作而改變 y 表示的值。所以不管 int 在實現上是傳遞值還是傳遞取用,它們在語意上都是等價的。也就是說,原始型別是值型別還是取用型別,對於程式員來說完全沒有區別。你完全可以把 Java 所有的原始型別都想成取用型別,之後你能對它們做的事情,你的編程思路和方式,都不會因此有任何的改變。

從這個角度來看,Java 在語意上是沒有值型別的。值型別和取用型別如果同時並存,程式員必須能夠在語意上感覺到它們的不同,然而不管原始型別是值型別還是取用型別,作為程式員,你無法感覺到任何的不同。所以你完全可以認為 Java 只有取用型別,把原始型別全都當成取用型別來用,雖然它們確實是用值實現的。

一個在語意上有值型別的語言(比如 C#,Go 和 Swift)必須具有以下兩種特性之一(或者兩者都有),程式員才能感覺到值型別的存在:

  1. deref 操作。這使得你可以用 *x = 2 這樣的陳述句來改變取用指向的內容,導致共享地址的其它取用看到新的值。你沒法通過 x = 2 讓其他值變數得到新的值,所以你感覺到值型別的存在。

  2. 像 struct 這樣的“值組合型別”。你可以通過 x.foo = 2 這樣的成員賦值改變取用資料(比如 class object)的一部分,使得共享地址的其它取用看到新的值。你沒法通過成員賦值讓另一個 struct 變數得到新的值,所以你感覺到值型別的存在。

實際上,所有的資料都是取用型別就是 Scheme 和 Java 最初的設計原理。原始型別用值來傳遞資料只是一種性能優化(叫做 inlining),它對於程式員應該是透明(看不見)的。那些在面試時喜歡問“Java 是否所有資料都是取用”,然後當你回答“是”的時候糾正你說“int,boolean 是值型別”的人,都是本本主義者。

思考題

有人指出,Java 的取用型別可以是 null,而原始型別不行,所以取用型別和值型別還是有區別的。但是其實這並不能否認本文指出的觀點,你可以想想這是為什麼嗎?

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

赞(0)

分享創造快樂