关于 Java 泛型方法定义的疑惑 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ak1ak
V2EX    Java

关于 Java 泛型方法定义的疑惑

  •  
  •   ak1ak 2022-06-13 10:42:26 +08:00 3593 次点击
    这是一个创建于 1235 天前的主题,其中的信息可能已经有所发展或是发生改变。

    t/858692 里讨论了 "? super T 和 ? extends T" 的问题。我现在有一个疑惑,在设计接口的时候,应该如何正确使用泛型通配符。

    借用引用该回答里的定义,有 3 个类:A1 、A2 、A3 ,A2 继承 A1 ,A3 继承 A2 ,那么有:A1>A2>A3 。如果想设计一个工具方法,接收有继承自 A1 的元素,以及一个对元素操作的方法。理论上可以这个写:

     interface Util { void process(List<? extends A1> list, Function<? extends A1, Boolean> function); } 

    但是实际上使用的时候,下面代码会报错:

     Util util = (l, f) -> { l.forEach(e -> { f.apply(e); // 这里会报错 }); }; 

    在报错的地方,IDE 提示如下:

    Required type: capture of ? extends A1 Provided: capture of ? extends A1 

    这里该如何理解呢?

    13 条回复    2022-06-13 16:25:41 +08:00
    ak1ak
        1
    ak1ak  
    OP
       2022-06-13 10:51:48 +08:00
    如果这样定义的话,就没有问题:
    ```java
    // 定义工具类
    interface Util<T extends A1> {
    void process(List<T> list, Function<T, Boolean> function);
    }

    //使用工具类
    Util<A1> util = (l, f) -> {
    l.forEach(e -> {
    Boolean flag = f.apply(e);
    });
    };
    ```
    这样使用是没有问题的,那如果改一下需求,Util 类需要接收所有是 A3 父类的 List 和一个 List 元素操作的方法又要如何设计呢? Java 里不允许定义 `T super A3`。
    nothingistrue
        2
    nothingistrue  
       2022-06-13 11:09:54 +08:00
    泛型必须有泛型参数,或者模板参数,这样才能在使用的时候将模板参数替换成实际内容。你标题里面的定义缺少了模板参数,这样定义的时候没问题,但是使用的时候因为没有传递模板参数(也无法传递)导致没法替换。换成你回复里面的定义方式,加上了模板参数,这样使用的时候 “Util<A1> util” 这就把 A1 这个参数传进去了,就能用。
    ak1ak
        3
    ak1ak  
    OP
       2022-06-13 11:30:20 +08:00
    @nothingistrue 请问有没有一种方式可以实现类似 process(List<? super A3> list, Function<? super A3, Boolean> function) 这样的功能。
    nothingistrue
        4
    nothingistrue  
       2022-06-13 11:38:47 +08:00
    interface Util2<T> {
    void process(List<? super T> list, Function<T, Boolean> function);
    }
    chendy
        5
    chendy  
       2022-06-13 12:11:18 +08:00
    1.
    因为前面的 ? 和 后面的 ? 不一定是一样的类型,所以不行
    换成同一个类型参数就可以了:<T> void process(List<T extends A1> list, Function<T extends A1, Boolean> function)
    2. super 同上,用同一个类型参数就行。另外 super 一般约束返回,拿来约束参数有点没想好是要什么效果
    GuuJiang
        6
    GuuJiang  
       2022-06-13 12:35:29 +08:00 via iPhone
    @nothingistrue 如果你真正看懂了我在隔壁的回答就不会有这个疑问了,你在#1 和#3 说的这种场景是不可能实现的,不是 Java 语法的限制,而是你假想的这个需求本身就有问题,我们姑且先忽略掉 List<? super A3>是不能进行 get 操作的这一点,退一步讲,哪怕允许 get 了,简化一下需求,要定义一个方法,其参数可能是 A3 及其父类,那你在这个方法的内部能够把这个参数当成什么类型呢?唯一的选择就只有 Object 了,这里的 A3 没有提供任何信息量,因此这样的方法没有任何的意义,也不可能存在,也不可能具有实际应用场景
    和你试图假定的这个场景最接近的应该是下面这个
    interface Util<T extends A1> {
    void process(List<? super A3> list, Supplier<? extends A3> function);
    }
    这里的 Supplier<? extends A3>也可以换成 Function<T, ? extends A3>,其中的 T 是任意一个具体类型
    然后在你的 process 内部也不能像你想象的那样从 list 中 get 然后交给 function 处理,而只能调用 function 然后将返回值 add 到 list
    坦白说这确实是演示 PECS 原则的一个很好的例子
    nothingistrue
        7
    nothingistrue  
       2022-06-13 12:40:50 +08:00
    class Scratch {
    public static void main(String[] args) {
    Util<A2> util = new Util<>();

    List<A3> a3List= new ArrayList<>();
    util.getAndProcess(a3List,a3 -> {return true;});

    List<A1> a1List = new ArrayList<>();
    util.supplyAndSet(a1List,A2::new );
    }
    }

    class Util<T> {
    public List<? extends T> getAndProcess(List<? extends T> list, Function<T, Boolean> function) {
    list.forEach(e->function.apply(e));
    return list;
    }

    public List<? super T> supplyAndSet(List<? super T> list, Supplier<T> supplier) {
    list.add(supplier.get());
    return list;
    }
    }


    class A1 {
    }

    class A2 extends A1 {
    }

    class A3 extends A2 {
    }
    nothingistrue
        8
    nothingistrue  
       2022-06-13 12:57:07 +08:00
    运行起来才发现怪怪的,楼主定义的 Util 是个函数式接口,但它的具体方法又继续用函数式接口,这样嵌套下来的场景,貌似我不好举例。所以就把 Util 换成工具类了。然后实际运行中,静态方法无法使用模板参数,所以 Util 又给改成对象类型的。

    代码看上面,最终的效果是。A2 的工具类,可以从 A3 的 List 中做读方向处理,可以往 A1 的 List 中做写方向处理。

    对于楼主 1 楼的需求,如果是这样,Util 类是个函数式接口,模板参数是“A3 的父类”,这是绝对不行的,因为这样的效果等同于方法的形参定义成了“某某或它的父类”,而这是违反面向对象基本原则的。如果是这样,Util 类是带模板参数的普通类,它的其中一个方法的参数限制为“模板参数的父类”,这是可以的,实际效果就看我上面的代码。
    nothingistrue
        9
    nothingistrue  
       2022-06-13 13:52:07 +08:00
    1 楼的需求,变通一下,也是可以实现的。变通后的需求是:接受一个对象,将之转换,然后将转换后的结果加入到 指定类的的父类的 list 。

    interface ProcessAndSet<T>{
    void processAndSet(T element, Function<T, A3> function, List<? super A3> list);
    }

    ProcessAndSet processAndSet = (e,f,l)->{
    l.add(f.apply(e));
    };
    List<A1> a1List = new ArrayList<>();

    这个变通需求与原始需求的区别是:原始需求中 “A3 父类的 List” 作为模板参数,要跟函数式接口一并定义,变通后,“A3 父类的 List” 是传入参数而不再是模板参数,不再一起定义,而是分开定义。
    chonh
        10
    chonh  
       2022-06-13 14:06:53 +08:00 via Android
    PECS: producer extend consumer super.

    Function 改为? super A1 即可。
    详细解释可以搜 so
    nothingistrue
        11
    nothingistrue  
       2022-06-13 14:47:07 +08:00   1
    回到楼主的最初疑问上,有必要对泛型标记做一个区分。

    泛型说到本质,就是模板替换。而模板替换,需要首先定义两个东西:一个是替换什么,即模板变量;一个是在哪里替换,即引用模板变量的地方。
    举例来说一下:
    public interface Listlt;E> { boolean add(E e); } 左边的<E> 是模板变量,右边的那个 E 是模板变量的引用。
    <T> T[] toArray(T[] a) ;(该方法同样在 List 中) 昨天的<T> 是模板变量,右边的那个 T 是模板变量的引用。

    上面只是定义了模板,到了使用的时候,你还得再定义第三个东西:替换成什么。
    举例:
    ArrayList<String> = new ArrayList<>(); 这里就定了了将相关的 E 替换成 String 。


    通配符,只能用在第二个定义,即模板变量的引用那里。模板变量,和模板要替换的值,都必须是确定的,故不能用通配符。这里有一个特殊的地方,返回值那里可以使用<?>通配符,但此时这个<?>等同于<Object>,是个假的通配符。



    当上面区分好之后,再看楼主的需求。

    主贴当中之所以错误,是因为没有定义模板变量。

    1 楼不允许定义`T super A3`的原因,因为这是模板变量,虽然跟普通变量不一样,但也要遵循一样的原则:你只能将变量的类型限定成具体的。T 可以,这相当于 Object 类型,T extend Base 可以,相当于 Base 类型。T extend Base & SomeInterface 也可以,仍然相当于 Base 类型,只不过额外要求实现了 SomeInterface 。T super Child 不可以,因为无法确定这代表哪种类型。

    3 楼的需求,想要的效果本质上是:定义一个方法,方法的参数类型是 A3 的父类。这跟泛型都没关系了,已经违反基本准则了,显然是不可实现的。
    ak1ak
        12
    ak1ak  
    OP
       2022-06-13 15:33:58 +08:00
    感谢各位的回答,总结一下,本质上就两点容易纠结的地方:

    @nothingistrue 说的模板,就是在使用定义的方法时,能够让编译器能够推导出具体的类型( type reference ),`? super A3` 确实无法推导出一个具体的类型,因此我之前的方法定义是无意义的,现实中也不会有这种需求。

    @GuuJiang 说的意思应该是:定义通用泛型方法时,需要考虑到 PECS 原则,结合 RednaxelaFX
    在知乎上的回答「 PECS 原则背后的原理,通俗来说就是八字箴言:宽于律人,严于律己。」以 Java Stream<T> 接口为例:map(Function<? super T, ? extends R> mapper) 方法里,目标是完成 T-> R 的转换。因为 T 在消费方( in/consumer ),允许传入所有的 T 以及 T 的父类型的元素,R 在生产方( out/producer ),允许返回所有 R 以及 R 子类型的元素。
    dk7952638
        13
    dk7952638  
       2022-06-13 16:25:41 +08:00
    年轻人,老夫奉劝你不要对 JAVA 的泛型有过高的期望,尤其是灵活性方面,过多的技巧最终你会发现小丑竟是你自己,泛型的尽头就是 @SuppressWarnings("unchecked")
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3060 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 12:37 PVG 20:37 LAX 05:37 JFK 08:37
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86