Java 泛型方法与通配符 其中的类型推断该如何理解?

2019-09-28 17:01:21 +08:00
 amiwrong123
class Holder<T> {
    private T value;
    public Holder() {}
    public Holder(T val) { value = val; }
    public void set(T val) { value = val; }
    public T get() { return value; }
    public boolean equals(Object obj) {
        return value.equals(obj);
    }
}
public class Wildcards {

    static <T> T exact2(Holder<T> holder, T arg) {
        holder.set(arg);
        T t = holder.get();
        return t;
    }
    // 有界 extends 通配符的参数:
    static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
        // holder.set(arg); // 编译错误,不能写操作
        T t = holder.get();//只可以读操作
        return t;
    }
    // 有界 super 通配符的参数:
    static <T> void wildSupertype(Holder<? super T> holder, T arg) {
        holder.set(arg);//只可以写操作
        // T t = holder.get();  // 编译错误,不能读操作

        // 本来是不能读操作的,但 super 有上限 Object,所以这是唯一合法的读操作:
        Object obj = holder.get();
    }
    
    public static void main(String[] args) {
        Holder raw = new Holder<Long>();
        // 上下两行都一样,反正泛型是伪泛型,重要的还是引用的类型
        raw = new Holder();
        Holder<Long> qualified = new Holder<Long>();
        Holder<?> unbounded = new Holder<Long>();
        Holder<? extends Long> bounded = new Holder<Long>();
        Long lng = 1L;

        Long r5 = exact2(raw, lng); // unchecked 警告:
        //   类型推断为了 Long,所以第一个参数会有 unchecked 的警告
        Long r6 = exact2(qualified, lng);
        //Long r7 = exact2(unbounded, lng); // 编译错误
        //Long r8 = exact2(bounded, lng); // 编译错误


        Long r9 = wildSubtype(raw, lng); // unchecked 警告
        Long r10 = wildSubtype(qualified, lng);
        // 只能返回给 Object。因为传递进入的实参类型是无界通配符
        Object r11 = wildSubtype(unbounded, lng);
        Long r12 = wildSubtype(bounded, lng);

        wildSupertype(raw, lng); // unchecked 警告
        wildSupertype(qualified, lng);
        //wildSupertype(unbounded, lng); // 编译错误
        //wildSupertype(bounded, lng); // 编译错误
    }
} ///:~

此例来自于 java 编程思想,在主函数中 exact2 和 wildSupertype 的两处调用都会有编译错误,看了提示,感觉不是特别理解。而且相对的,wildSubtype 函数却可以执行成功?

两个报错都说了,Long 不能转换为?。这个该怎么理解,就是说,一个形参推断出来为 Long,一个形参推断出来为?,就不可以呗? exact2 函数还多说了句,两个形参推断出来的边界不一样。

2784 次点击
所在节点    程序员
20 条回复
putin541
2019-09-28 17:28:41 +08:00
我的理解是,类型的集合也可以看作是一种类型。? 是所有类型的集合,所以 Long != ?
realPipiz
2019-09-28 18:06:27 +08:00
aguesuka
2019-09-28 18:38:50 +08:00
equals 写得应该有问题
oneisall8955
2019-09-28 18:51:32 +08:00
https://liuzhicong.cn/index.php/study/extends-T-and-super-T.html

之前我写的可能相关,太长的话,看最后的引用,stackoverflow 里面的第一第二个解答

只要是类型安全问题,编译器经量往最安全的地方去检查你的代码
Mistwave
2019-09-28 20:45:58 +08:00
wildSubtype
holder 里是 T 的 subtype,所以你不能 set 一个 T 给 holder。就好比 holder 要持有 Dog,T 是 Animal,不是所有的 T 都能给 holder 去持有,比如 Duck 就不行。


wildSupertype
T 是 holder 里的 subtype,从 holder 里 get,当然不能赋值给一个 T。好比 T 是 Dog,holder 里是 Animal,当然是不能 holder.get()然后复制给 t 的。

可以看看这篇讲协变和逆变的文章
http://duanyifu.com/2019/08/30/variance/
amiwrong123
2019-09-28 21:01:20 +08:00
@putin541
你的理解貌似也很有道理。但偏偏 wildSubtype 函数接受同样的实参,却不会报编译错误了,这该如何解释呢
amiwrong123
2019-09-28 21:03:08 +08:00
@realPipiz
谢谢,看了。介绍泛型的知识很广,但没有我所疑问的点。
amiwrong123
2019-09-28 21:03:25 +08:00
@aguesuka
怎么个有问题法呀
amiwrong123
2019-09-28 21:08:09 +08:00
@oneisall8955
哈哈哈,上次你就给推荐过啦。看了,介绍了<? extends T> 及 <? super T>这两种引用的合法操作和限制操作,不错。但我这里,主要不懂在于,泛型方法上,涉及了通配符的类型推断,编译器为啥会报这个错==
(悄悄地说,等会写博客准备把你那个 copy 的 jdk 例子写进去,哈哈哈)
amiwrong123
2019-09-28 21:12:45 +08:00
@Mistwave
谢谢,你说这两点就是<? extends T> 及 <? super T>这两种引用的合法操作和限制操作了吧(一种合法的是“读操作”,另一个是“写操作”)。

你这篇文章也不错,话说你们都喜欢看英文文档啊,我要是能看看 java 官网文档就不错了。。。

主要还是不懂在于,泛型方法上,涉及了通配符(可能是形参涉及了、也可能是实参涉及了)的类型推断,编译器为啥会报这个错==
Mistwave
2019-09-28 22:38:38 +08:00
@amiwrong123 这个很直观的:
编译器报错==类型不安全
那么什么情况下类型不安全?
在这个地方,子类型替换就安全,别的就不安全


哈哈哈哈英文文章多看就熟练了,没有别的技巧
secondwtq
2019-09-28 23:51:11 +08:00
@amiwrong123
我也是最近也开始研究 subtyping,我发现一个规律是 covariance 和 contravariance 一般是对称的,这个问题可以从这个角度出发理解:
Holder<?>,这里的 ? 可能是任何类型 X

在 T wildSubtype(Holder<? extends T> holder, T arg) 中,可以直接把 T 推断为 Object,注意 Object 是 Top Type,是任何类型的 supertype (包括它自己),X <: T 这个 constraint 对任意 Holder<X> 都成立,X 既可以是 Integer,也可以是 InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState

在 T void wildSupertype(Holder<? super T> holder, T arg) 中,没有合适的 ?:
假设 X 是 Object,然后我们的 constraint 是 T <: X,然后既然 X 是任意的类型,那么这个 T 应该直接推断成 Bottom Type,也就是 null,所以 wildSupertype(unbounded, null) 是可以过编译的
但是你给的 lng,无论把 T 推断成 Long/Number/Object 等等都是错的,比如 T 是 Object,那么 X 就只能是 Object,T 是 Long,那 X 就只能是 Long/Number/Object/Serializable ... 无论哪种情况,如果 我的 Holder<?> 实际上是一个 Holder<HasThisTypePatternTriedToSneakInSomeGenericOrParameterizedTypePatternMatchingStuffAnywhereVisitor>,T <: X 就不成立了
secondwtq
2019-09-28 23:56:09 +08:00
@secondwtq s/假设 X 是 Object,然后我们的 // ...
aguesuka
2019-09-29 02:14:15 +08:00
@amiwrong123 a.equals(b) 和 b.equals(a)的值应该一致。
amiwrong123
2019-09-29 10:18:29 +08:00
@secondwtq
谢谢层主,刚看还有点没看大东,多看了几遍大概懂了。话说你是从哪里知道这些知识,感觉我又学到了很多。

首先我才意识到 bottom type 是 null,以前一直以为 java 有上限,没下限。

可能你的分析过程我理解得比较浅显:首先编译器总会找到最合适的推断类型出来,使得泛型方法可调用。且<?>的范围代表了所有类型。

1. wildSubtype 中,由于一个形参推断出来为 Long,一个形参推断出来为?,所以 T 可以被推断为 Long/Number/Object。但为了形参<? extends T>的范围大于等于实参<?>的范围,T 必须被推断为 Object,才可以。

2.wildSupertype 中,同样,由于一个形参推断出来为 Long,一个形参推断出来为?,所以 T 可以被推断为 Long/Number/Object。但形参是<? super T>,无论 T 被推断哪个,其形参的范围都无法大于等于实参<?>的范围。(说得形象点,此函数中,形参的范围是从 Long 到 top,而实参的范围是从 bottom 到 top )
amiwrong123
2019-09-29 10:20:38 +08:00
@secondwtq
话说大佬可以帮忙解释一下 exact2 函数为什么会有编译错误吗?
仅仅是因为,两个形参都要求确切的类型,所以两个实参里不能有一个带通配符的吗?
by73
2019-09-29 14:34:44 +08:00
@amiwrong123 `exact2` 这个,因为形参用的是 `Holder<T>` 这样的 concrete type,传入实参 `Holder<? extends T>` 是一个“集合”,编译器无法判断到底该该采用哪一个类型传进去。

先改下 `bounded` 的声明为 `Holder<? extends Number>`;那么如果我调用 `exact2(bounded, 2.0)` 时编译器把类型 capture 为 `Holder<Double>`,但实际上你 `bounded` 的真正类型应该是 `Holder<Long>`,这就引发了矛盾。因此编译器不允许 wildcard type 和 concrete type 之间的转换。

> The capture of a wildcard is compatible to a corresponding wildcard, never to a concrete type. Correspondingly, the capture of a bounded wildcard is compatible solely to other wildcards, but never to the bound.
>
> 来源:<http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ503>
YUyu101
2019-09-29 16:12:43 +08:00
编译器不知道上下文,把自己想象成编译器只看这一句语句,上面的代码虽然是你写的,但你当做看不到,?等于占位用的,我不知道是什么类型
Holder<? extends long>
可能是 holder<Along extends long>
也可能是 holder<Blong extends long>
但实际整个工程里没有任何继承 long 的类,
YUyu101
2019-09-29 16:24:26 +08:00
假设你习惯把 Long 的子类起名成 XXLong
Holder<? extends Long>编译器就当成了 Holder<XXLong>
推断 exact2 就是 static <XXLong> XXLong exact2(Holder<XXLong> holder, XXLong arg)
XXLong 是 Long 的子类,你后面一个参数传了 Long,Long 不能向下转型成 XXLong
但实际上并没有 XXLong 这个类。
mineV
2019-09-30 11:33:08 +08:00
?和? extends Object 是等价的。你用它去当做 exacr2 的参数,就会调用其中的 set 方法,显然这是不可以的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/605124

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX