你不知道的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.

时间: 2024-11-06 07:49:49

你不知道的JAVA系列一 Type Inference的相关文章

Java系列笔记(2) - Java RTTI和反射机制

目录 前言 传统的RTTI 反射 反射的实现方式 反射的性能 反射与设计模式 前言 并不是所有的Class都能在编译时明确,因此在某些情况下需要在运行时再发现和确定类型信息(比如:基于构建编程,),这就是RTTI(Runtime Type Information,运行时类型信息). 在java中,有两种RTTI的方式,一种是传统的,即假设在编译时已经知道了所有的类型:还有一种,是利用反射机制,在运行时再尝试确定类型信息. 本文主要讲反射方式实现的RTTI,建议在阅读本文之前,先了解类的加载机制(

Java系列笔记(1) - Java 类加载与初始化

目录 类加载器 动态加载 链接 初始化 示例 类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class),类加载器实质上是一条类加载器链,一般的,我们只会用到一个原生的类加载器,它只加载Java API等可信类,通常只是在本地磁盘中加载,这些类一般就够我们使用了.如果我们需要从远

Java系列(1)----Java特性

一.Java的关键特性1.简单性2.安全性3.可移植性4.面向对象5.健壮性6.多线程7.体系结构中立8.解释执行9.高性能10.分布式11.动态性 二.J2SE 5的主要特性1.泛型2.注解(annotation)3.自动装箱和自动拆箱4.枚举5.增强for-each风格的for循环6.可变长度参数7.静态导入8.格式化I/O9.并发实用工具 三.J2SE 7的重要特性1.String现在能够控制switch语句2.二进制整型字面值3.数值字面值下的下划线4.扩展的try语句,称为带资源的tr

java系列课程精品推荐

深入学习ExtJs4.1的布局及常用控件 http://edu.ibeifeng.com/view-index-id-129.html 专题-深入Java OOP编程 http://edu.ibeifeng.com/view-index-id-88.html 案例:Red5 & Flex聊天室架构与实现 http://edu.ibeifeng.com/view-index-id-84.html 深入学习Java图表组件JFreeChart开发统计图表 http://edu.ibeifeng.co

[Java系列1]Eclipse配置,常用插件

java可视化编程-eclipse安装windowbuilder插件 http://blog.csdn.net/jason0539/article/details/21219043 [Java系列1]Eclipse配置,常用插件,码迷,mamicode.com

跟老杨学java系列(五) JDK的安装与配置

跟老杨学java系列(五) JDK的安装与配置 提示:本节内容对于java入门是非常关键的,对于刚接触java的同学一定要认真学习,欢迎大家留言探讨技术问题.其他问题概不回复. (书接上回)上节课程我们简单介绍了java项目的开发过程及常用的开发工具,这节课我们详细讲解一下JDK的安装与配置.根据上一节的学习,我们知道编写完java代码后,需要先对java代码进行编译,然后再执行.而java程序的编译与执行都是通过JDK来完成的.所以做java开发,首先我们需要学会安装和配置JDK.下面我们就来

【转载】Java系列笔记(1) - Java 类加载与初始化

Java系列笔记(1) - Java 类加载与初始化 原文地址:http://www.cnblogs.com/zhguang/p/3154584.html 目录 类加载器 动态加载 链接 初始化 示例 类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class),类加载器实质上是一

跟老杨学java系列(二) PNP训练法介绍

跟老杨学java系列(二) PNP训练法介绍 声明:以下内容可能会有做广告的嫌疑,哈哈~~.大家不想看的忽略就好..欢迎大家留言探讨技术问题.其他问题概不回复. (书接上回)PNP训练法是国内java培训机构-乐橙谷推出的一种针对java培训的学习训练方法.该训练法摒弃了传统的以章节知识点为线索的教学方式,改为以完整的项目开发过程为线索.以对功能点的分析设计驱动出的知识点为授课内容.以迭代为教学手段,学习难度由浅入深,让学生在充分了解软件开发过程的基础上,熟练掌握项目开发经验.深入掌握相关开发技

Java系列笔记(6) - 并发(上)

目录 1,基本概念 2,volatile 3,atom 4,ThreadLocal 5,CountDownLatch和CyclicBarrier 6,信号量 7,Condition 8,Exchanger 在Java中,JVM.并发.容器.IO/NIO是我认为最重要的知识点,本章将介绍其中的并发,这也是从“会Java”到精通Java所必须经历的一步.本章承接上一张<Java系列笔记(5) - 线程>,其中介绍了Java线程的相关知识,是本章介绍内容的基础,如果对于线程不熟悉的,可以先阅读以下这