你不知道的JAVA系列一 Type Inference
在正式开讲之前先容许我说下写这篇文章的故事背景。前几天我们的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.emptyList
static <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 item
14 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 item
14 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.