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

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

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

    https://www.v2ex.com/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 List<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   ·   实用小工具   ·   839 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 21:20 · PVG 05:20 · LAX 13:20 · JFK 16:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.