你不知道的JAVA系列一 Type Inference

來源:互聯網
上載者:User

標籤:tostring   成功   inf   pen   int   r++   make   integer   eof   

在正式開講之前先容許我說下寫這篇文章的故事背景。前幾天我們的production下的一個tool突然莫名其妙的報錯,那部分功能已經很久沒有改動過了,按理說是不應該出現問題的,代碼在做反射調用method的時候出現了ClassCastException。我先是以為可能是什麼小問題就把任務分給我同事了,他分析下來告訴我不知道什麼問題,莫名其妙的就突然拋異常了;那找不到問題我們就只能怪JAVA Compiler了  原來最近我們做了一次JDK的升級,從7升級到了8,起先以為是reflect的Method類有所改動,結果比較以後一模一樣,兩眼一抹黑,完蛋。。。。 好了,謎底我會在最後揭露。

Knowledge lets you deduce the right thing to do; Expertise makes the right thing a reflex.
                                        - 《Unix 編程藝術》

程式員最重要的是思想,知其然知其所以然。

下面進入今天的主題 Type Inference.

一, Type Inference

什麼是Type Inference,官方給出的定義是:
Type inference is a Java compiler‘s ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

大意就是:
類型推斷是一個Java編譯器來查看每一個方法調用和相應的聲明,以確定型別參數(或參數),使調用能夠正常實現。推理演算法確定參數類型,如果類型推斷成功,那麼方法返回的值就是那個類型的。最後,推理演算法試圖找到與所有的變數工作的最具體類型。

如何理解這段話呢,我們先把這段話拆分成幾個概念:

  • Generic Method - 泛型方法。
  • Type Parameters - 型別參數,也就是Generic Type Parameter
  • Method invocation - 方法調用,類型推斷主要發生在方法調用的時候。
  • Target Type - 目標類型
  • Inference algorithm - 類型推斷演算法,接下來會用執行個體來說明這個類型推算到底是如何工作的。

二, Generic Method

要想把Type Inference說清楚了還是要先從Generic Method說起的,什麼是Generic Method? 看下面的例子
The Util class includes a generic method, compare, which compares two Pair objects:

public class Util {public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {    return p1.getKey().equals(p2.getKey()) &&        p1.getValue().equals(p2.getValue());}}public class Pair<K, V> {    private K key;    private V value;    public Pair(K key, V value) {    this.key = key;    this.value = value;    }    public void setKey(K key) { this.key = key; }    public void setValue(V value) { this.value = value; }    public K getKey() { return key; }    public V getValue() { return value; }}

這就是一個經典的Generic Method,這個方法叫做 compare方法接受2個型別參數為K和V的Pair類型(這裡就不細說泛型了)。Generic Method 有幾部分組成:
1. Type Parameter, 角括弧包住的部分
2. 一個相同的<Type Parameter>出現在傳回型別前,如果是static method那麼這個<Type Parameter>是必須出現的。
3. 一個傳回型別,可以是Type Parameter對應的那個類型,也可以不是。

那麼到底如何去確定K和V的類型呢,這個類型要在方法調用的時候才能確定下來,這就要來說type inference了.

三, Type Inference 執行個體解說

假設我有一個Generic Method, T在這裡就是type argument,這個方法接受2個T類型的參數,返回一個T類型的結果。
1. 

static <T> T pick(T a1, T a2) { return a2; }

現在去call這個method,

Serializable s = pick("d", new ArrayList<String>());

我們來拆分一下:
1. 第一個a1參數傳入”d”,類型是String.
2. 第二個a2參數傳入ArrayList<String>, 類型是ArrayList.
3. String和ArrayList都是interface Serializable的實現,所以pick method的傳回值被infers成Serializable 類型。

2. 再來看一個Generic Method的例子

public class BoxDemo {    public static <U> void addBox(U u,                                  java.util.List<Box<U>> boxes) {        Box<U> box = new Box<>();        box.set(u);        boxes.add(box);    }    public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {        int counter = 0;        for (Box<U> box: boxes) {            U boxContents = box.get();            System.out.println("Box #" + counter + " contains [" +                    boxContents.toString() + "]");            counter++;        }    }    public static void main(String[] args) {        java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =                new java.util.ArrayList<>();        BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);        BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);        BoxDemo.outputBoxes(listOfIntegerBoxes);    }}

The following is the output from this example:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
addBox是一個Generic Method接受一個U類型的參數,當然這個方法是接受2個參數的,第一個是U類型的參數,第2個是U類型的Box類型的List, 大家可以看到在main方法裡我們用了2種方式來調用addBox,第一種是顯示的告訴Java Compiler我要用Integer這個類型,第二種就是類型推斷,我並沒有顯示的指定我要用Integer, Java Compile根據傳入參數的類型來推斷出method應該使用哪種類型.

四, Target Type 目標類型

Java Compiler 會根據你指定的目標類型來推斷(infers)出method該返回哪種類型的結果,例如:

Collections.emptyListstatic <T> List<T> emptyList(); //這個方法沒有參數,只有一個T類型的傳回型別,那麼我不能傳入參數這個方法是如何知道用什麼傳回型別的呢,這就是target type
List<String> listOne = Collections.emptyList(); 
listOne是一個List<String>類型的變數,Java Compiler會根據這個目標類型來推斷出emptyList method應該返回這個類型,這種類型推斷依賴於assignment context,也就是說我要賦給哪個變數,它是什麼類型我就返回什麼類型。


我們再考慮一種情況,現在我有一個方法接受一個List<String>的參數。

void processStringList(List<String> stringList) {// process stringList}//現在調用:processStringList(Collections.emptyList());

這個在Java 7裡是不會編譯的,因為Java 7不支援 method 類型推斷,T類型預設就是Object,然後就出現了編譯錯誤
List<Object> cannot be converted to List<String>。

五, Context

上面說到method類型推斷,什麼是method類型推斷,那就要說下這個context的概念了,Java Compiler在做類型推斷的時候主要依靠的就是上下文。目前有2種context.

  • Assignment Context
  • Method Context

Assignment Context就是賦值上下文,也很好理解了就是依靠指派陳述式左邊的類型來推斷generic method的具體類型。

Method Context顧名思義就是method上下文了,這個概念不像assignment來的那麼直接,而且JDK 7沒有method infers這個東西。Method上下文就是根據接受參數的method的參數類型來推斷被傳入調用method的類型。

上面的例子放在JDK 8裡就變得有意義了

void processStringList(List<String> stringList) {// process stringList}//現在調用:processStringList(Collections.emptyList());

JDK 8引入了method infers,也就是說Java Compiler會根據當前method的上下文來決定那個T類型到底應該是什麼類型,在這裡就是String類型。

說到JAVA 8那就不能不說lambda了

六, Lambda & Stream

等等這裡不是說Type Inference嗎為什麼要說Lambda? Lambda很重要的一個核心概念就是類型推斷。
先看一個列子:

Predicate<Integer> predicate = (var) -> var > 0; //P.S. 第一眼看上去還是很cool的

要說JAVA的lambda那就要說functional interface, functional interface就是只有一個抽象方法的介面,這樣的介面都可以叫做functional interface.
那麼這個lambda expression到底和type inference有什麼關係呢,首先我們來看一下Predicate介面的方法聲明
boolean test(T t);
上面的lambda expression之所以能夠成功就是因為這個方法的定義,接受一個T類型的參數,返回一個boolean值,這裡面牽涉到一個function descriptor 這裡就不細說了,以後有機會單做一期Lambda;再來看上面的指派陳述式,單看(var) -> var > 0根本不知道這個var是什麼類型的,當這個expression賦值給Perdicate<Integer>的時候按照assignment context的類型推斷這個var就是一個Integer的類型。
java.util.function package下的所有預先定義好的functional interface都全部依賴type inference.
下面看一個關於Stream的例子:

List<String> threeHighCaloricDishNames =menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).limit(3).collect(Collectors.toList());


filter方法接受一個Perdicate<T>的參數, map 方法接受一個Function<T, R>的參數,collect接受一個類型為Collect的參數,這個Collect是由Collectors這個utility class來構造的,如果你翻看Collectors的源碼的話你會發現幾乎所有的方法都用到了generic method。 所有的這一切都源之於 method context的類型推斷。
如果沒有JAVA 8的method context類型推斷你根本就無法使用這種chain的結構,也無法寫出這麼簡潔的代碼.

七, 遺留的問題

最後來說一下開篇的時候留下的問題,在確定是類型推斷問題之前我一度以為JDK 8存在bug,也確實有人遇到了同樣的問題並且在OpenJDK裡報了bug [url]https://bugs.openjdk.java.net/browse/JDK-8072919[/url], 但問題被resolve了並且說這不是一個bug,好吧我承認這確實不是一個bug。

問題是這樣的, 現在有2個方法分別如下:

 1 //方法1: 2 void invoke(Object obj, Object… objs) 3 //接受2個參數一個Object, 一個 Object[],其實就是來至於reflect的Method.invoke 4 //方法2: 5 <T> T readValue() { 6 List<String> list = new Arraylist<>(); 7 list.add(“test”); 8 return (T) list; 9 }10 //用2個方法調用invoke方法, 11 //1.12  invoke(“1231”, readValue());13 //2.14  List<String> list = readValue(); invoke(“12312”, list);

我們來分析一下2種不同的調用方法:
1. 第一種方法直接把readValue()的傳回值當做參數傳入invoke方法,這時候就需要用method context來infers readValue的傳回型別,invoke method的第二個參數是Object...也就是Object[],那麼通過infers就確定了readValue 的類型是Object[], oooooops, 這段代碼會拋出一個ClassCastException, 因為 ArrayList can not cast to Object[], java.utils.ArrayList cannot be cast to [Ljava.lang.Object;
2. 第二個方法可以正確執行,因為我們先調用readValue方法然後賦值給list variable,這時候就有了target type, Java Compiler通過infers決定返回List<String>的值, 再把list這個變數傳入invoke這個method就沒有問題了.
3. 注意:這2種不同的調用方法在JDK 7的時候都能執行成功,因為JDK 7沒有method context的類型推斷,所以T被當成了Object,那麼在readValue內部類型轉換就沒有問題了,因為所有的類都繼承了Object。

為了更直觀的看下JVM到底做了什麼,我寫了一個簡單的小例子,然後我們看一下Java Compiler 對class位元組碼到底做了什麼。
e.g. 1:

 1     static void test(Object... o) { 2         System.out.println(o); 3     } 4  5     public static void main(String[] args) { 6 // List</String> a = gen(); 7 // test(adf, a); 8         test(gen()); 9     }10 11     static <T> T gen() {12         List<T> list = new ArrayList<>();13 //add list item14         return (T)list;15     }

這是會出現ClassCastException的代碼 
java.lang.ClassCastException: java.util.ArrayList cannot be cast to [Ljava.lang.Object;

e.g. 2: 這是可以執行的代碼:

 1     static void test(Object... o) { 2         System.out.println(o); 3     } 4  5     public static void main(String[] args) { 6         List<String> list = gen(); 7         test(list); 8 // test(gen()); 9     }10 11     static <T> T gen() {12         List<T> list = new ArrayList<>();13 //add list item14         return (T)list;15     }

這2個不同的調用方式在這個位元組碼裡體現的很清楚,第2個調用方法產生了一個類型為list的local variable,並且是一個型別參數為String的list,參考astore_1 iconst_1, aload_1指令。


總結一下:文章寫的好不好,總結很重要 

1. Type Inference就是類型推斷,根據當前調用method的上下文來推斷出具體的類型。
2. 如果method有一個T類型的參數,那麼T的類型就由傳入參數的類型決定。
3. 如果method沒有型別參數,但卻有一個T類型的返回,那麼就要考慮context,target type,是assignment context還是method context。
4. JDK 8引入了method context的概念來實現method infers type parameters。functional interface以及Stream API大量使用method類型推斷。(如果大家有興趣,我會單獨做一期關於Lambda和Stream的文章。

希望我解釋的足夠清楚能夠協助大家理解透徹Type Inference.

 

你不知道的JAVA系列一 Type Inference

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.