天天看點

Java 有值類型嗎?

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

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

胡扯結束,現在來說正事。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 成了引用類型,你卻并不能對它進行引用類型所特有(而值類型沒有)的操作。這樣的操作包括:

deref。就像 C 語言裡的 * 操作符。

成員指派。就像對 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 能做的事情隻有兩種:

讀出它的值。

對它進行指派,使它指向另一個地方。

這兩種事情,就跟你能對值類型能做的兩件事情沒有差別。這就是為什麼你沒法通過對 x 的操作而改變 y 表示的值。是以不管 int 在實作上是傳遞值還是傳遞引用,它們在語義上都是等價的。也就是說,原始類型是值類型還是引用類型,對于程式員來說完全沒有差別。你完全可以把 Java 所有的原始類型都想成引用類型,之後你能對它們做的事情,你的

賣手機号

程式設計思路和方式,都不會是以有任何的改變。

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

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

deref 操作。這使得你可以用 *x = 2 這樣的語句來改變引用指向的内容,導緻共享位址的其它引用看到新的值。你沒法通過 x = 2 讓其他值變量得到新的值,是以你感覺到值類型的存在。

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

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

思考題

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