V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  程序员

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

  •  
  •   amiwrong123 · 2019-09-28 17:01:21 +08:00 · 2972 次点击
    这是一个创建于 1881 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 函数却可以执行成功? ulgDbD.png ulghKf.png

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

    20 条回复    2019-09-30 11:33:08 +08:00
    putin541
        1
    putin541  
       2019-09-28 17:28:41 +08:00
    我的理解是,类型的集合也可以看作是一种类型。? 是所有类型的集合,所以 Long != ?
    realPipiz
        2
    realPipiz  
       2019-09-28 18:06:27 +08:00
    aguesuka
        3
    aguesuka  
       2019-09-28 18:38:50 +08:00
    equals 写得应该有问题
    oneisall8955
        4
    oneisall8955  
       2019-09-28 18:51:32 +08:00 via Android
    https://liuzhicong.cn/index.php/study/extends-T-and-super-T.html

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

    只要是类型安全问题,编译器经量往最安全的地方去检查你的代码
    Mistwave
        5
    Mistwave  
       2019-09-28 20:45:58 +08:00 via iPhone
    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
        6
    amiwrong123  
    OP
       2019-09-28 21:01:20 +08:00
    @putin541
    你的理解貌似也很有道理。但偏偏 wildSubtype 函数接受同样的实参,却不会报编译错误了,这该如何解释呢
    amiwrong123
        7
    amiwrong123  
    OP
       2019-09-28 21:03:08 +08:00
    @realPipiz
    谢谢,看了。介绍泛型的知识很广,但没有我所疑问的点。
    amiwrong123
        8
    amiwrong123  
    OP
       2019-09-28 21:03:25 +08:00
    @aguesuka
    怎么个有问题法呀
    amiwrong123
        9
    amiwrong123  
    OP
       2019-09-28 21:08:09 +08:00
    @oneisall8955
    哈哈哈,上次你就给推荐过啦。看了,介绍了<? extends T> 及 <? super T>这两种引用的合法操作和限制操作,不错。但我这里,主要不懂在于,泛型方法上,涉及了通配符的类型推断,编译器为啥会报这个错==
    (悄悄地说,等会写博客准备把你那个 copy 的 jdk 例子写进去,哈哈哈)
    amiwrong123
        10
    amiwrong123  
    OP
       2019-09-28 21:12:45 +08:00
    @Mistwave
    谢谢,你说这两点就是<? extends T> 及 <? super T>这两种引用的合法操作和限制操作了吧(一种合法的是“读操作”,另一个是“写操作”)。

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

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


    哈哈哈哈英文文章多看就熟练了,没有别的技巧
    secondwtq
        12
    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
        13
    secondwtq  
       2019-09-28 23:56:09 +08:00
    @secondwtq s/假设 X 是 Object,然后我们的 // ...
    aguesuka
        14
    aguesuka  
       2019-09-29 02:14:15 +08:00 via Android
    @amiwrong123 a.equals(b) 和 b.equals(a)的值应该一致。
    amiwrong123
        15
    amiwrong123  
    OP
       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
        16
    amiwrong123  
    OP
       2019-09-29 10:20:38 +08:00
    @secondwtq
    话说大佬可以帮忙解释一下 exact2 函数为什么会有编译错误吗?
    仅仅是因为,两个形参都要求确切的类型,所以两个实参里不能有一个带通配符的吗?
    by73
        17
    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
        18
    YUyu101  
       2019-09-29 16:12:43 +08:00
    编译器不知道上下文,把自己想象成编译器只看这一句语句,上面的代码虽然是你写的,但你当做看不到,?等于占位用的,我不知道是什么类型
    Holder<? extends long>
    可能是 holder<Along extends long>
    也可能是 holder<Blong extends long>
    但实际整个工程里没有任何继承 long 的类,
    YUyu101
        19
    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
        20
    mineV  
       2019-09-30 11:33:08 +08:00 via Android
    ?和? extends Object 是等价的。你用它去当做 exacr2 的参数,就会调用其中的 set 方法,显然这是不可以的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1646 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:52 · PVG 00:52 · LAX 08:52 · JFK 11:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.