Java 10 - 與“var類型推斷機制”的第一次親密接觸

這裏寫圖片描述

引言

官方消息,Java 10 將在2018年3月20號正式發佈。(我大Java 9 瞬間成了Vista……….)據傳,官方在2017年12月14號已經完成了版本開發的主線合併。 迄今爲止,在官方放出了Java 10少數新特性裏面,局部變量類型推斷(local-variable type inference) 絕對是備受萬衆矚目的。它將我們常常在JS裏面使用的var 變量引入到語言特性中,把我們從那些冗長的變量聲明中解放出來。來吧,舒展你的右手,下面是你以前絕對沒有寫過的代碼:

var users = new ArrayList<User>();

還是禁不住要感嘆Java的偉大,“集大廣成”,沒有什麼詞語能更好地形容了。So,看到這樣的代碼我猜你一定很有興趣想知道更多關於它的信息,這篇文章將討論var 適用於哪裏,它是如何影響可讀性的以及在類型推斷的過程中發生了什麼。

用var替換傳統變量聲明

作爲一名有着麒麟臂的Java開發者,我們日擼月擼的Java是一門強類型語言,也就是要顯式聲明各個變量的確切類型,稍有不慎編譯器就會報錯。如以下代碼,寫一行代碼要輸入兩次類型,一次是引用類型聲明,另一次是構造函數:

URL lovnx = new URL("https://github.com/Lovnx");

我們還經常爲下一行代碼聲明變量類型:

URL lovnx = new URL("https://github.com/Lovnx");
URLConnection connection = lovnx.openConnection();
Reader reader = new BufferedReader(
        new InputStreamReader(connection.getInputStream())
    );

正是由於這樣的原因,我的基友某獒十分鄙夷Java,要不是想學Java的併發處理,這廝估計得一輩子不待見Java。上面這種情況還不是最可怕的,儘管看起來是有些多餘。我們的IDE會幫助我們更好地書寫代碼,好用的快捷鍵,以及自動提示。但是當我們的變量名跳躍性較大的時候,可讀性會受到極大的影響,因爲當變量類型的字符數參差不齊,或者聲明一箇中間變量的時候,往往會使人感到心力交瘁,專注度不能獲得正向的反饋,寫了半天也瞧不出有幾個邏輯。這種語言特性,飽受詬病,尤其是被Python開發者。。。

從Java 10開始,開發者就可選擇通過把變量聲明爲var 來讓編譯器自行推斷其類型:

var lovnx = new URL("https://github.com/Lovnx");
var connection = lovnx.openConnection();
var reader = new BufferedReader(
    new InputStreamReader(connection.getInputStream()));

編譯器在處理var 變量的時候,它會去檢測右側代碼的聲明,並將其類型用於左側,這一過程發生在初始化階段。WTF?甜到憂傷?JIT在編譯成字節碼的時候還是用的推斷後的結果類型。

正如你所看到的,這樣在鍵入代碼的時候可以節省不少字符,更重要的是,可以去除冗餘的信息,使代碼變得清爽,還可以對齊變量的名稱。當然這樣也會付出一些代價,一些變量,比如上文的connection 不是由構造函數直接創建,我們相較以往將不會立刻知道它的實際類型,只能藉助IDE了。

另外,如果你擔心命名成var 的方法與變量衝突,不用擔心,從技術上來講,var 不是一個關鍵字,而是一個保留的類型名稱,也就是說它的作用域只在編譯器推斷類型的範圍內,而在其他地方還是有效的標識符。這樣也限制了一些東西,即類名不能起爲var

局部變量類型推斷看起來像一個簡單的語言特性,但實際上並不簡單,可能在你的心中已經有了一些疑問:

  • 這貨到底是 Java 還是 JavaScript?
  • 該在什麼地方使用?
  • 它真的不會破壞以往強類型的可讀性?
  • 爲什麼又沒有 val 或者 let?

往下看,你會得到答案。

它不是JavaScript

var 絲毫不會影響Java對一個靜態類型的歸類,它做的,僅僅是在編譯器中推斷出變量類型,然後將實際的類型寫入字節碼,就像以往的強類型顯式聲明一樣。

舉個例子,以下代碼就是對上面那段有var 代碼的字節碼的反編譯結果:

URL lovnx = new URL("https://github.com/Lovnx");
URLConnection connection = lovnx.openConnection();
Reader reader = new BufferedReader(
        new InputStreamReader(connection.getInputStream())
    );

事實上,var 的生命週期只存在於編譯器當中,並沒有針對它涉及的運行時組件,所以大可放心沒有性能影響。所以,它並不是JavaScript那樣把它當成一個關鍵字來解析,沒有人能夠一蹴而就。

如果你擔心沒有明確的類型會使代碼變得不好閱讀,那麼你在寫函數式語句的時候就壓根不會有一個變量:

rhetoricalQuestion.answer(yes -> "see my point?");

var的適用範圍

JEP 286’的標題是“局部變量類型推斷”, 看名稱就可以知道使用範圍:局部變量。更加確切的說法是:具有初始化器的局部類型變量聲明。所以下面這種方式是不行的:

//nope
var foo;
foo = "Foo";

必須得是 var foo = "Foo"。上面的例子沒有涵蓋所有不能使用var 的聚集表達式。比如lambdas和方法引用,編譯器會根據預期的類型確定類型,下面這些情況也不行:

//nope
var ints = {0, 1, 2};
var appendSpace = a -> a + " ";
var compareString = String::compareTo

除了局部變量之外,還有一個用處是用在for 循環裏面:

//right
var numbers = List.of("a", "b", "c");
for (var nr : numbers)
    System.out.print(nr + " ");
for (var i = 0; i < numbers.size(); i++)
    System.out.print(numbers.get(i) + " ");

這表示,字段、方法簽名、catch子句仍然需要顯示類型聲明,下面這種是錯誤的:

// nope
private var getFoo() {
    return "foo";
}

避免一些莫名其妙的錯誤

var 只能用於局部變量並不是JDK團隊技術上的侷限,這還是Java語言特性所決定的。就像下面這樣:

// cross fingers that compiler infers List<User>
var users = new ArrayList<User>();
// but it doesn't, so this is a compile error:
users = new LinkedList<>();

編譯器肯定是能夠輕易得知代碼情況的,並可以輕易地推斷出所有的類型,但事實上沒有這樣做。JDK團隊想盡量讓使用者避免一下莫名其妙的錯誤, 不應該改掉一個地方而導致一個看似不相干的錯誤。

下面是一個例子:

// inferred as `int`
var id = 123;
if (id < 100) {
    // very long branch; unfortunately
    // not its own method call
} else {
    // oh boy, much more code...
}

上面的代碼沒有任何問題,現在我們在條件體裏面追加這一行代碼:

id = "124"

會發生什麼?這不是一個浮誇的問題,想一想。

答案是if 條件會拋出一個錯誤,因爲id 不再是一個int 型的變量,不能和< 進行比較。這個錯誤與造成這種錯誤的原因相去甚遠。這顯然是給一個變量賦值而無法預料到的結果。

從這個角度來看,將類型推斷限制在JIT中即時類型的決定是有道理的。

爲什麼不能用於聲明類屬性與方法返回值類型?

屬性與方法的作用域比局部變量大得多,因此稍有不慎,就有可能出現上文那種毫無徵兆的錯誤。在最壞的情況下,更改方法參數的類型可能引起序列化二進制不兼容,從而導致運行時錯誤。這就是改變一些極小細節而帶來的極端後果,並且是毫無徵兆的。

因此,由於非private的屬性和方法是類的靜態組成部分,不允許被瞎改,所以類型推斷就捨棄了它們。當然,對private的屬性與方法理論上是可以使用類型推斷的,但如果強行+1,不免顯得有點奇怪。

歸根結底其基本原因,還是局部變量只是一些實現細節,不能被外部引用,而這就減少了其嚴格,明確和詳細地定義(強類型)其類型的需要。(我看是偷懶吧===)

Java 10 引入var的背景

讓我們從後文找出引入var 類型推斷的的原因,以及它是如何影響代碼可讀性的,爲什麼vallet 沒有隨之一起引入。如果你還是對其他細節頗有興趣,可以參照官方JEP 286, var FAQ問題解答,或者是Amber項目的郵件列表。

But why?!

Java的語法歷來以冗長著稱,尤其是對比一些年輕的語言,這已經成爲開發者最大的痛點之一,你往往會聽到一些初學者與高級開發者對其的詬病與抱怨。Project Amber, var 的原始項目, 致力於孵化出一種“體積更小,面向生產效率”的Java新語言特性,減少一些以往過於累贅的語法規則。

如此,局部變量類型推斷機制(Local-variable type inference)便應運而生了。在編寫代碼的時候,可以很明顯地使變量的聲明變得簡潔,雖然到目前爲止我仍認爲它與IDE的自動生成功能相比是喜憂參半的,比如在重寫過程中,或者寫一個構造方法,抑或是爲方法的返回值聲明一個類型。

var的好處除了使局部變量的聲明更加簡便之外,還能使代碼相得益彰,why?如果你曾經或現在致力於過企業級開發,你會覺得那些命名相當醜陋。下面就是一個典型栗子:

InternationalCustomerOrderProcessor<AnonymousCustomer, SimpleOrder<Book>> orderProcessor = createInternationalOrderProcessor(customer, order);

它就像王大娘的裹腳一般,又臭又長,一個很簡單的功能要寫到吐血,中間還夾敘夾議才能保證語義明確,避免後期維護的時候看不懂寫的什麼。

var orderProcessor = createInternationalOrderProcessor(customer, order);

以往聲明中間變量的愛恨糾葛,在引入了var之後可以徹底地冰釋前嫌了,現在你在方法體內可以什麼也不管,一路var下去,特別是一些嵌套的或者連鎖表達式,它的好處也更加顯而易見。

簡而言之,var就是減少Java累贅語法的一顆語法糖,誰先嚐到誰先甜到憂傷。

And What About 可讀性?

現在來看看可讀性的影響。毫無疑問,使用var勢必會引起變量類型可視化缺失, 這會傷害一部分的可讀性,特別是當你想要知道一些代碼的運行邏輯的時候,能夠目所能及地看到變量類型顯得格外重要,儘管將來的IDE可能會智能顯示所有推斷類型,這是當前唯一可能會受到批評的地方。

針對可讀性缺失,var從其他地方來彌補,其中一種方式就是使變量名稱對其(呵呵00好像是好看了一些):

// with explicit types
No no = new No();
AmountIncrease<BigDecimal> more = new BigDecimalAmountIncrease();
HorizontalConnection<LinePosition, LinePosition> jumping =
    new HorizontalLinePositionConnection();
Variable variable = new Constant(5);
List<String> names = List.of("Max", "Maria");

// with inferred types
var no = new No();
var more = new BigDecimalAmountIncrease();
var jumping = new HorizontalLinePositionConnection();
var variable = new Constant(5);
var names = List.of("Max", "Maria");

變量類型固然重要,但是變量名稱纔是決定性因素。類型描述了Java整個生態系統(JDK)、通用用例(庫或框架),以及業務領域(應用程序)。 但一個又一個渺小的變量名稱纔是串起這一些的樞紐。就像JS那樣,不也風靡世界嗎?

使用var的時候,變量名可以忽略類型名近乎以頂格的方式存在,特別是當你雙擊選中其中一個變量名的時候,不同以往的滿天星的呈現,如今可以排列地整整齊齊。Kotlin的這一點不也廣受歡迎?

如上所述,捨棄掉變量類型顯示聲明,換來了另一種方式的可讀性,我們要做的就是適應。

Finding A Style

當然,使用var固然容易,但是我們需要在可讀性和簡潔性之間取得平衡。甲骨文的Java語言架構師,負責Amber項目的Brian Goetz給了我們啓示:

當我們需要使代碼更清晰,更簡潔的同時不會丟失掉一些重要信息,那就使用var。

爲什麼不用 val/let?

許多使用var爲主變量的語言會爲不可變變量提供一個額外的關鍵字,通常是val或者let,但是我們在Java 10將會使用final var,促成這個結果的原因有以下幾點:

  • 不可變變量比局部變量更加重要。
  • 從Java 8開始,我們Effectively final的概念(局部內部類和匿名內部類訪問的局部變量必須由final修飾,java8開始,可以不加final修飾符,由系統默認添加。java將這個功能稱爲:Effectively final 功能)
  • 引入var的讚揚度很高(74% 強烈支持, 12% 中度支持) 反觀 var/ val 與 var/ let 的組合則含糊不清。

這個結果其實有些令人失望的,讓val或者let替代final var不是也挺好的嗎?

Well, maybe in the future… until then we have to use final var.

總結

在Java 10之後你在聲明局部變量類型的時候可以使用var來告知編譯器進行類型推斷,取代之前的類名或接口名。這僅僅發生在變量初始化的階段,就像 var s = “”;這樣。 此外,for循環中的索引變量類型也可以使用var。它由編譯器推斷類型,然後將推斷出的類型寫入字節碼中,也就是說它對運行時並沒有任何影響,僅僅是一個語法糖,Java仍然是一種靜態語言。

除了局部變量之外,另外在屬性和方法返回值類型中,不能使用var。 這樣做是爲了避免引起一些無法預知的錯誤,使用的時候儘量使需要推斷的變量靠近它聲明的地方,從而緩解可讀性問題。

儘管引入var變量會使代碼可讀性變得更糟,但此次的新特性爲開發者提供了一種在編寫複雜表達式的時候尋求了一個新的契機。

略有改動 原文參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章