锁定老帖子 主题:关于Java泛型 与 类型推断
精华帖 (0) :: 良好帖 (3) :: 新手帖 (1) :: 隐藏帖 (12)
|
|
---|---|
作者 | 正文 |
发表时间:2012-02-15
最后修改:2012-02-15
amoszhou 写道 RednaxelaFX今天还没出现?
所以说你太着急了… 诶…看英文的太累的话这行很难混。有些词我不太清楚该如何翻译为中文所以为了避免混乱我宁可不翻译而用原文。 =========================================================== 先举例说说covariance。用不太严谨的语言来举例: ·如果我们在整数集上能定义“<”(小于)关系, ·然后有一个函数f(x),定义域是整数集,值域我们可以不关心是什么,不过为了方便举例这里假设值域也是整数集 这样,如果在整数集上有 x < y 如果能保证 f(x) < f(y) 也成立,那么“f”作为一种变换,对“<”关系就是covariant的。 如果用“顺序”来想像,原本整数集可以按“<”关系排序,经过“f”的变换之后这个排序没有变化,这就是covariant的变化;如果排序关系反过来了,就是contravariant。 那么看类型系统的例子。假设有一种关系叫做“子类型”关系,记为“<:”。这个关系是自反和传递的,也就是说, T <: T 成立 如果 T <: U, U <: V,那么 T <: V 也成立。 然后再看一种变换,叫“泛型”。泛型类型的定义域是类型,值域也是类型,也就是一种从类型到类型的变换。假如说记为“G[T]”。 这样,如果有 T <: U 此时如果 G[T] <: G[U],那么G这种变换就是covariant的; 反之,如果 G[U] <: G[T],那么这种变化就是contravariant的。 而在Java的泛型设计中,G[T]与G[U]不构成任何<:关系,所以说G与“子类型关系”是invariant(不变)的。 =========================================================== 这里所说的declaration-site就是“类型本身的声明的地方”。举例说的话, public class String { ... } 这里就是String类的declaration-site。或者可以翻译为“声明点”。 而 String s = ...; 这里就是String类型的一个use-site。或者可以翻译为“使用点”“使用地点”之类。 =========================================================== 接下来看看use-site variance与declaration-site variance。 这两种都是表达variance(co-或contra-variance)的方式。 先看declaration-site variance,因为实际上这种更直观一些。 在C# 4里,泛型接口上的泛型参数可以声明为covariant或者contravariant的。=> 参考资料 covariant的泛型参数用out关键字来修饰,contravariant的泛型参数用in关键字来修饰。 public interface IEnumerable<out T> { ... } 例如说这样,就声明了一个接收一个covariant的泛型参数的、名为IEnumerable的接口。variance直接在“类型声明”的地方就清楚的写明了。 这样,下面的赋值就能够成立: IEnumerable<string> strIter = ...; IEnumerable<object> objIter = strIter; 因为IEnumerable<string>是IEnumerable<object>的子类型,所以赋值匹配。 Java里的Iterable接口跟这个IEnumerable是对应的。但下面这段代码就无法成立,因为Java的泛型对子类型关系是invariant的: Iterable<String> strIter = ...; Iterable<Object> objIter = strIter; =========================================================== 然后看use-site variance。上面的Java代码如何改改就能成立呢? Iterable<? extends Object> objIter = strIter; 就行了。为什么?因为在Java的泛型设计里,? extends T用于表达covariance。 <? extends Object>说的是,实际赋值过来的那个类型是Object的任意子类型都可以匹配。所以X<String>是X<? extends Object>的子类型,X<Object>也是X<? extends Object>的子类型。 我们只能在use-site使用这种记法而无法在Iterable类型的声明上使用,所以把这种记法叫做use-site variance。 于是,Java里泛型类型声明的地方虽然无法指定variance,但实际使用这些泛型类型时却可以任意指定所需要的variance,看起来是不是比declaration-site variance要更灵活? 这里太灵活正是很多不好理解的东西的来源。 =========================================================== 继续讨论Java泛型与variance之前,先看看Java(与C#)的数组。Java的数组是covariant的,而且不是静态安全的,必须带有一些额外的运行时检查才可以保持类型系统的安全。 因为covariant,所以下面的赋值可以成立: String[] strArr = new String[10]; Object[] objArr = strArr; 这里看起来都很正常。 当我们要从数组里取出东西的时候, Object o = objArr[1]; 这样也没问题,类型都是匹配的。 但当我们要往数组里放入元素的时候,事情就不一定这么明显了: objArr[1] = new Fruit(); objArr引用的是一个String[]的实例,它只能装String类型的引用,不能装别的。 但Java源码被静态编译的时候,语言规范没有要求编译器发现objArr实际上引用的是String[]类型的实例;编译器只需要知道objArr的静态类型是Object[]。 objArr[1]的静态类型是Object,那么任意Object的子类型自然都应该能赋值匹配。所以这句赋值能通过编译。 然后到运行时数组的赋值会做子类型检查,发现Fruit不是String的子类型,然后抛出java.lang.ArrayStoreException。 =========================================================== 回到泛型与variance。 在C# 4里,covariant的泛型参数只能出现在方法的返回值位置上,而不能出现在参数位置上。 前面举的IEnumerable接口的例子,里面有一个方法,挑出跟这例子相关的来看: public interface IEnumerable<out T> { IEnumerator<T> GetEnumerator(); } 可以看到T出现在了GetEnumerator()方法的返回值的类型里。这个符合C# 4的规范。 但如果我们再加一个方法到该接口里,void Foo(T arg),就通不过编译。 C# 4里的 IList<T> 接口上的T就既不是covariant也不是contravariant的,因为它既要出现在参数位置上也要出现在返回值位置上。 类似的,contravariant的泛型参数只能出现在方法的参数位置上,而不能出现在返回值位置上。 从上面数组的covariance的情况,不难理解为什么需要做这样的限制:如果covariant泛型参数出现在了参数位置上,那就难以通过静态类型检查来保证类型安全,而必须添加运行时子类型检查。 =========================================================== Java的情况。List<? extends Fruit>类型的变量能够引用任意的Fruit类型的子类型的List<E>,例如说List<Fruit>、List<Apple>、List<Banana>之类。 如果有: List<Apple> apples = new ArrayList<Apple>(); apples.add(new Apple()); List<? extends Fruit> fruits = apples; 那么跟数组的情况相似,我们从fruits里拿出东西的时候肯定没问题,肯定是Fruit的子类型。 Fruit f = fruits.get(0); 但如果要往fruits里放入东西,就开始有问题了:add(E e)方法的E,这里应该被替换为? extends Fruit。但这不是一个确定的类型;它用于表达“非确定的任意Fruit的子类型”,于是任意确定的Fruit的子类型反而无法跟它赋值匹配。 可以想像,你传入一个Banana实例,然后E说“我这个时候想要Apple”(任意Fruit的子类型);你换成传入Apple的实例,它又说“我现在想要Orange”了 这样的捉迷藏伤不起吧? 如果我们写: fruits.add(new Banana()); Java该如何知道这个操作到底安不安全呢?很简单,它干脆就不让这段代码编译,这样就保证安全了。 实际上,Java语言的规定让covariant的泛型参数只能出现在返回值位置上才允许调用;上面add(E e)方法在参数位置上出现了泛型参数E,当E被替换为? extends X的时候,这个方法就不允许调用了。 类似的,contravariant泛型参数只能出现在参数位置上才允许调用。 PECS口诀是producer-extends, consumer-super。producer是“生产者”,自然应该“返回”某些东西;consumer是“消费者”,自然应该“接收”某些东西。 结合前面的例子,这个口诀应该能让人很容易记住extends是用来表示covariance的,而super是用来表示contravariance的。 =========================================================== Java的use-site variance有时候会跟泛型类型的类型推导产生冲突。不过楼主举的例子却还没到类型推导那一步就已经有问题了。 |
|
返回顶楼 | |
发表时间:2012-02-15
RednaxelaFX 写道 amoszhou 写道 RednaxelaFX今天还没出现?
所以说你太着急了… 诶…看英文的太累的话这行很难混。有些词我不太清楚该如何翻译为中文所以为了避免混乱我宁可不翻译而用原文。 =========================================================== 先举例说说covariance。用不太严谨的语言来举例: ·如果我们在整数集上能定义“<”(小于)关系, ·然后有一个函数f(x),定义域是整数集,值域我们可以不关心是什么,不过为了方便举例这里假设值域也是整数集 这样,如果在整数集上有 x < y 如果能保证 f(x) < f(y) 也成立,那么“f”作为一种变换,对“<”关系就是covariant的。 如果用“顺序”来想像,原本整数集可以按“<”关系排序,经过“f”的变换之后这个排序没有变化,这就是covariant的变化;如果排序关系反过来了,就是contravariant。 那么看类型系统的例子。假设有一种关系叫做“子类型”关系,记为“<:”。这个关系是自反和传递的,也就是说, T <: T 成立 如果 T <: U, U <: V,那么 T <: V 也成立。 然后再看一种变换,叫“泛型”。泛型类型的定义域是类型,值域也是类型,也就是一种从类型到类型的变换。假如说记为“G[T]”。 这样,如果有 T <: U 此时如果 G[T] <: G[U],那么G这种变换就是covariant的; 反之,如果 G[U] <: G[T],那么这种变化就是contravariant的。 而在Java的泛型设计中,G[T]与G[U]不构成任何<:关系,所以说G与“子类型关系”是invariant(不变)的。 =========================================================== 这里所说的declaration-site就是“类型本身的声明的地方”。举例说的话, public class String { ... } 这里就是String类的declaration-site。或者可以翻译为“声明点”。 而 String s = ...; 这里就是String类型的一个use-site。或者可以翻译为“使用点”“使用地点”之类。 =========================================================== 接下来看看use-site variance与declaration-site variance。 这两种都是表达variance(co-或contra-variance)的方式。 先看declaration-site variance,因为实际上这种更直观一些。 在C# 4里,泛型接口上的泛型参数可以声明为covariant或者contravariant的。=> 参考资料 covariant的泛型参数用out关键字来修饰,contravariant的泛型参数用in关键字来修饰。 public interface IEnumerable<out T> { ... } 例如说这样,就声明了一个接收一个covariant的泛型参数的、名为IEnumerable的接口。variance直接在“类型声明”的地方就清楚的写明了。 这样,下面的赋值就能够成立: IEnumerable<string> strIter = ...; IEnumerable<object> objIter = strIter; 因为IEnumerable<string>是IEnumerable<object>的子类型,所以赋值匹配。 Java里的Iterable接口跟这个IEnumerable是对应的。但下面这段代码就无法成立,因为Java的泛型对子类型关系是invariant的: Iterable<String> strIter = ...; Iterable<Object> objIter = strIter; =========================================================== 然后看use-site variance。上面的Java代码如何改改就能成立呢? Iterable<? extends Object> objIter = strIter; 就行了。为什么?因为在Java的泛型设计里,? extends T用于表达covariance。 <? extends Object>说的是,实际赋值过来的那个类型是Object的任意子类型都可以匹配。所以X<String>是X<? extends Object>的子类型,X<Object>也是X<? extends Object>的子类型。 我们只能在use-site使用这种记法而无法在Iterable类型的声明上使用,所以把这种记法叫做use-site variance。 于是,Java里泛型类型声明的地方虽然无法指定variance,但实际使用这些泛型类型时却可以任意指定所需要的variance,看起来是不是比declaration-site variance要更灵活? 这里太灵活正是很多不好理解的东西的来源。 =========================================================== 继续讨论Java泛型与variance之前,先看看Java(与C#)的数组。Java的数组是covariant的,而且不是静态安全的,必须带有一些额外的运行时检查才可以保持类型系统的安全。 因为covariant,所以下面的赋值可以成立: String[] strArr = new String[10]; Object[] objArr = strArr; 这里看起来都很正常。 当我们要从数组里取出东西的时候, Object o = objArr[1]; 这样也没问题,类型都是匹配的。 但当我们要往数组里放入元素的时候,事情就不一定这么明显了: objArr[1] = new Fruit(); objArr引用的是一个String[]的实例,它只能装String类型的引用,不能装别的。 但Java源码被静态编译的时候,语言规范没有要求编译器发现objArr实际上引用的是String[]类型的实例;编译器只需要知道objArr的静态类型是Object[]。 objArr[1]的静态类型是Object,那么任意Object的子类型自然都应该能赋值匹配。所以这句赋值能通过编译。 然后到运行时数组的赋值会做子类型检查,发现Fruit不是String的子类型,然后抛出java.lang.ArrayStoreException。 =========================================================== 回到泛型与variance。 在C# 4里,covariant的泛型参数只能出现在方法的返回值位置上,而不能出现在参数位置上。 前面举的IEnumerable接口的例子,里面有一个方法,挑出跟这例子相关的来看: public interface IEnumerable<out T> { IEnumerator<T> GetEnumerator(); } 可以看到T出现在了GetEnumerator()方法的返回值的类型里。这个符合C# 4的规范。 但如果我们再加一个方法到该接口里,void Foo(T arg),就通不过编译。 C# 4里的 IList<T> 接口上的T就既不是covariant也不是contravariant的,因为它既要出现在参数位置上也要出现在返回值位置上。 类似的,contravariant的泛型参数只能出现在方法的参数位置上,而不能出现在返回值位置上。 从上面数组的covariance的情况,不难理解为什么需要做这样的限制:如果covariant泛型参数出现在了参数位置上,那就难以通过静态类型检查来保证类型安全,而必须添加运行时子类型检查。 =========================================================== Java的情况。List<? extends Fruit>类型的变量能够引用任意的Fruit类型的子类型的List<E>,例如说List<Fruit>、List<Apple>、List<Banana>之类。 如果有: List<Apple> apples = new ArrayList<Apple>(); apples.add(new Apple()); List<? extends Fruit> fruits = apples; 那么跟数组的情况相似,我们从fruits里拿出东西的时候肯定没问题,肯定是Fruit的子类型。 Fruit f = fruits.get(0); 但如果要往fruits里放入东西,就开始有问题了:add(E e)方法的E,这里应该被替换为? extends Fruit。但这不是一个确定的类型;它用于表达“非确定的任意Fruit的子类型”,于是任意确定的Fruit的子类型反而无法跟它赋值匹配。 可以想像,你传入一个Banana实例,然后E说“我这个时候想要Apple”(任意Fruit的子类型);你换成传入Apple的实例,它又说“我现在想要Orange”了 这样的捉迷藏伤不起吧? 如果我们写: fruits.add(new Banana()); Java该如何知道这个操作到底安不安全呢?很简单,它干脆就不让这段代码编译,这样就保证安全了。 实际上,Java语言的规定让covariant的泛型参数只能出现在返回值位置上才允许调用;上面add(E e)方法在参数位置上出现了泛型参数E,当E被替换为? extends X的时候,这个方法就不允许调用了。 类似的,contravariant泛型参数只能出现在参数位置上才允许调用。 PECS口诀是producer-extends, consumer-super。producer是“生产者”,自然应该“返回”某些东西;consumer是“消费者”,自然应该“接收”某些东西。 结合前面的例子,这个口诀应该能让人很容易记住extends是用来表示covariance的,而super是用来表示contravariance的。 =========================================================== Java的use-site variance有时候会跟泛型类型的类型推导产生冲突。不过楼主举的例子却还没到类型推导那一步就已经有问题了。 哈哈,说的很详细。现在终于有一句话,说到点上了。就是我想表达的: Java的use-site variance有时候会跟泛型类型的类型推导产生冲突。 List<Apple> apples = new ArrayList<Apple>(); apples.add(new Apple()); List<? extends Fruit> fruits = apples; 这段代码其实我们是不是也可以理解成这样: List<? extends Fruit> fruits =new ArrayList<Apple>(); 按类型推导来说: 编译器应该可以推导 ? extends Fruit 就是Apple类型,而不是Fruit,Banana。 按你所说:? extends Fruit 会替换List<E> 接口上的E 。那么E 应该就会被替换成Apple 。 所以我就觉得:类型推导跟泛型协变有一些不符合的地方,或者说Java的类型推导功能还是不够强大。 可以这么理解么? |
|
返回顶楼 | |
发表时间:2012-02-15
接上楼, 那么fruits里面,add()方法应该就可以增加Apple 是不是列?
另外还有一点问题,很容易想混淆。 也盼大神讲一下怎么个区分法。。 List<? extends Fruit> 是协变。 那么List<T extends Fruit> 列? 虽然两者有着很明显的区别。?代表不定,但是T仅能代表一个类型,但是在人的逻辑上来说:T extends Fruit 表示Fruit的一个子类型,其实也可以理解成一个集合,一个不定的群体。 |
|
返回顶楼 | |
发表时间:2012-02-15
amoszhou 写道 现在终于有一句话,说到点上了。就是我想表达的:
Java的use-site variance有时候会跟泛型类型的类型推导产生冲突。 List<Apple> apples = new ArrayList<Apple>(); apples.add(new Apple()); List<? extends Fruit> fruits = apples; 这段代码其实我们是不是也可以理解成这样: List<? extends Fruit> fruits =new ArrayList<Apple>(); 按类型推导来说: 编译器应该可以推导 ? extends Fruit 就是Apple类型,而不是Fruit,Banana。 按你所说:? extends Fruit 会替换List<E> 接口上的E 。那么E 应该就会被替换成Apple 。 所以我就觉得:类型推导跟泛型协变有一些不符合的地方,或者说Java的类型推导功能还是不够强大。 可以这么理解么? 不能。因为对Java来说? extends Fruit就是一个不会再进一步做推导的类型了。它本身就是“非确定”的一个类型范围而不是某个“确定”的类型,所以无法直接用某个确定的类型去替换。当它出现在返回值位置上的时候Fruit <- ? extends Fruit肯定能成立;当它出现在参数位置上的时候,? extends Fruit <- Fruit的任意确定子类型按照规定不成立。 如果到这一步你仍然在纠结类型推导,这说明你没读过规范去了解Java到底做了什么类型推导,而又不做什么类型推导。于是你需要的是语言规范:http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf。5.1.10里有关于capture conversion的讨论。15.12.2.7有关于类型推导的规定。 最简单的来说,在Java里只有当你不写<>里的内容的时候,它才会做类型推导。 例如说java.util.Arrays.asList(): public static <E> List<E> asList(E... args) { ... } 如果你在代码里写: Arrays.asList(1, 2, 3, 4); 那么Java编译器就需要推导出这个使用点上的E是Integer。 你也可以显式指定E的绑定: Arrays.<Number>asList(1, 2, 3, 4); 此时就不需要做任何推导,只要检查Number跟int经过各种隐式转换后是否赋值匹配。 从Java 7开始,只写<>而不写里面的内容也是一种请求编译器做类型推导的方式。 List<String> strs = new ArrayList<>(); 这里也要推导右手边的<>里到底是什么。 而List<? extends Fruit>的<>里已经有东西了,按照现在的Java的设计编译器就不需要做任何推导了。 有很多事情“可以做”,但一个系统设计的时候不是一定会把所有“可以做”的东西都做掉的。有很多取舍。在C# 3里可以写 var i = 1; var d = i + 3.0; 为啥Java不做?它就是不做。 在Haskell里可以写: foo x y = x + y 而当我们调用它的时候Haskell自动能推导出x和y是Int类型的: foo 1 2 但用Java来写就得写成public static int foo(int x, int y) { return x + y; } 为什么Java不做?它就是不做。 |
|
返回顶楼 | |
发表时间:2012-02-15
不要被java规范束缚死嘛,觉得不爽可以自己让“java代码”有自动推导能力嘛,总的来说,只要不涉及IO,代码里的类型都可以编译期确定,就像C++Template那样
于是你可以写自己爽的java代码,只要你先写一个转换器,把自动类型推导的伪java代码,转化成标准java代码,就ok了 |
|
返回顶楼 | |
发表时间:2012-02-15
好。谢谢。 现在明白了,不用纠结了。~
|
|
返回顶楼 | |
发表时间:2012-02-15
类型推断与动态语言是两码子事情;类型推断是一种约定行为,当不指定类型时,可以通过上下文以及语言约定本身来确定它是什么类型;而动态语言本身是不关心类型。类型推断有助于简化代码,但它本质上还是有类型,可以在编译期间作检查,跟动态语言无法作类型检查根本区别。
|
|
返回顶楼 | |
发表时间:2012-02-15
最后修改:2012-02-15
---追问---
以List<E>为例子,与Fruit相比,List<E>是一种参数化的类型,E可以说是类型List<E>的类型参数,E也是一种类型,E不能是int,float...基本类型; E属于预定义的类型(请允许我这么说),因为我们不能在代码里直接写List<E> foo=new ArrayList<Fruit>();之类的代码; 假设Apple是子类,Fruit是父类;那么: ------------------------------------------ List<Apple> =nothing=> List<Fruit> Apple =extends=> Fruit ------------------------------------------ List<Apple> =extends=> List<? extends Fruit> Apple =extends=> ? extends Fruit ------------------------------------------ 对于List<Apple>来说,预定义类型E是Apple类型; 对于List<? extends Fruit>来说,E是"? extends Fruit"类型; 注意,"? extends Fruit"已经是一种类型了,其中?并不表示一种类型; 而如下: public interface MyList<T extends Fruit> { public T get(); public void set(T fruit); } MyList<T extends Fruit>里的"T extends Fruit",其中extends Fruit是限定T的,而"T extends Fruit"并不表示一种类型,T表是一种类型。 使用时的List<? extends Fruit> 与定义时的MyList<T extends Fruit>常令我混淆; 请问撒迦,这样理解对否? |
|
返回顶楼 | |
发表时间:2012-02-15
mwei 写道 List<Apple> =extends=> List<? extends Fruit> Apple =extends=> ? extends Fruit 这个不对。第二行不是这样的。是 这个成立 ? extends Fruit =extends=> Fruit 但这个不成立 Apple =extends=> ? extends Fruit ================ 没有wildcard(那个问号)的extends/super就是普通的constraint而已。T extends Fruit其实就是T(一个确定类型),但受到了约束:T必须是Fruit的子类型。并不参与variance。 |
|
返回顶楼 | |
发表时间:2012-02-15
RednaxelaFX 写道 mwei 写道 List<Apple> =extends=> List<? extends Fruit>
Apple =extends=> ? extends Fruit 这个不对。第二行不是这样的。是 这个成立 ? extends Fruit =extends=> Fruit 但这个不成立 Apple =extends=> ? extends Fruit ================ 没有wildcard(那个问号)的extends/super就是普通的constraint而已。T extends Fruit其实就是T(一个确定类型),但受到了约束:T必须是Fruit的子类型。并不参与variance。 就是说java的List不支持协变,即使使用wildcard也只能使List表现的像是在协变,实际上使用wildcard后List也不是真正的协变。 |
|
返回顶楼 | |