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

jdk8 lambda 表达式推断问题

  •  
  •   beijiaxu · 2019-01-15 16:52:03 +08:00 · 3791 次点击
    这是一个创建于 2139 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前用的是 jdk8 144 版本,写了如下一个方法

    Map<String, Object> map = new HashMap<>();
    map.put("a", new ArrayList<String>());
    
    methodA((Collection) map.get("a")).stream()
        .forEach(p -> System.out.println(p.getClass()));
    methodA((Collection<String>) map.get("a")).stream()
        .forEach(p -> System.out.println(p.getClass()));
    

    如下为 methodA

    List<String> l = new ArrayList<>();
    l.add("a");
    l.add("b");
    return l;
    

    两端输出都是 String 类型, 然而推断类型的时候,第一个调用的参数 p 为 Object,第二个为 String。。。

    第 1 条附言  ·  2019-01-17 16:13:33 +08:00

    我感觉写的代码有点误导大家了。。我再重新写下
    定义一个对象Aoo

    class Aoo {
      int a;
      public int getA() {
        return a;
      }
    }
    

    再定义一个方法

    public List<Aoo> methodA(Collection<String> c) {
        List<Aoo> l = new ArrayList<>();
        l.add(new Aoo());
        return l;
    }
    

    然后调用methodA

    Map<String, Object> map = new HashMap<>();
    map.put("a", new ArrayList<String>());
    
    //第一种调用
    methodA((Collection) map.get("a")).stream()
        .forEach(p -> //这里编译器认为p是Object);
    //第二种调用
    methodA((Collection<String>) map.get("a")).stream()
        .forEach(p -> //这里编译器认为p是Aoo);
    

    这样第一种调用是没法直接调用方法getA()的,
    所以我就疑问为什么方法签名的泛型指定会影响到方法返回值的类型

    23 条回复    2019-01-18 13:52:35 +08:00
    openthinks
        1
    openthinks  
       2019-01-15 17:22:00 +08:00
    楼主期望输出是什么?
    String s1 = "xx";
    Object o1="xx";
    s1.getClass()
    o1.getClass()
    mazai
        2
    mazai  
       2019-01-15 17:31:32 +08:00
    没明白你的意思
    anzu
        3
    anzu  
       2019-01-15 17:44:07 +08:00
    没问题呀
    rizon
        4
    rizon  
       2019-01-15 18:10:18 +08:00
    你没贴 methodA 的返回值类型啊。方法返回值决定了后续操作的对象类型啊
    bumz
        5
    bumz  
       2019-01-15 18:42:17 +08:00
    你的 methodA 是这样的吗?

    ```
    private static <E> Collection<E> methodA(Collection<E> a) {
    List<String> l = new ArrayList<>();
    l.add("a");
    l.add("b");
    return (Collection<E>) l;
    }
    ```

    这样就重现了你描述中的情景
    然而这不是很正常吗
    beijiaxu
        6
    beijiaxu  
    OP
       2019-01-15 20:27:49 +08:00
    @rizon methodA 返回的 List<String> 类型
    beijiaxu
        7
    beijiaxu  
    OP
       2019-01-15 20:29:05 +08:00
    @openthinks 不是什么期望输出,只是我在 lambda 函数里变量推导的类型可能是 Object,可能是具体的我要的类型。。
    beijiaxu
        8
    beijiaxu  
    OP
       2019-01-15 20:35:22 +08:00
    可能大家都没太懂,我再写下。。
    首先有个如下方法 methodA
    List<String> methodA(Collection<String> c) {return ...}

    然后调用该方法,使用 map 来获得变量
    Map<String, Object> map = new HashMap<>();
    map.put("a", new ArrayList<String>());

    第一种方式:强转类型不加泛型类型
    methodA( (Collection) map.get("a") )
    .stream().forEach(p -> 这里的参数 p 推导类型为 Object )
    第二种方式:强转类型有泛型类型
    methodA( (Collection<String>) map.get("a") )
    .stream().forEach(p -> 这里的参数 p 推导类型为 String )

    我不明白的是,为什么方法签名的泛型会影响到 lambda 函数推导方法返回值的类型,我已经在方法返回值里指定了泛型类型了呀。
    beijiaxu
        9
    beijiaxu  
    OP
       2019-01-15 20:38:50 +08:00
    因为今天正好碰到了这个问题,我偷懒没个方法签名加集合的泛型,导致 lambda 里推导出的参数调用方法编译错误,然后我加了给集合加了个<String>, 就能正常工作了,感觉好奇怪。
    chocotan
        10
    chocotan  
       2019-01-15 20:49:03 +08:00
    楼主所说的"推断类型"是指啥?
    编译错误? IDE 的提示?
    cyspy
        11
    cyspy  
       2019-01-15 20:52:22 +08:00
    Collection 不加参数默认是 Collection<Object> ,编译器不知道里面的类型
    m2276699
        12
    m2276699  
       2019-01-15 20:59:13 +08:00
    符合预期
    WangYanjie
        13
    WangYanjie  
       2019-01-15 23:33:09 +08:00
    interface Collection<?>{} 是 generic type.

    Collection<String> 是 parameterized type, an instance of generic type `Collection`
    Collection 是 raw type, java 5 之前的产物
    Collection<?> 是 unbound wildcard parameterized type
    type erasure 的 **一部分** 是指编译器会在编译期间,把 parameterized type 中的 type parameter 都消去,
    Collection<String>, Collection, Collection<?> 应该都是对应的一种运行时的类

    尝试了以下并没有你这样的效果。

    import java.util.*;

    public class Demo {
    public static List<String> methodA(Collection<String> cs) {
    List<String> stringList = new ArrayList<>();
    for (String s : cs) {
    stringList.add(s);
    }
    return stringList;
    }

    public static void main(String args[]) {
    Map<String, Object> map = new HashMap<>();
    List<String> stringList = new ArrayList<>();
    stringList.add("s");
    map.put("A", stringList);
    methodA((Collection) map.get("A")).stream().forEach(p -> System.out.println(p.getClass()));
    methodA((Collection<String>) map.get("A")).stream().forEach(p -> System.out.println(p.getClass()));
    }
    }


    class java.lang.String
    class java.lang.String

    ➜ ~ java -version
    java version "1.8.0_181"
    Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
    Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
    cassia
        14
    cassia  
       2019-01-15 23:46:09 +08:00
    类型擦除,了解下
    a123321456b
        15
    a123321456b  
       2019-01-16 08:36:22 +08:00 via Android
    Collection 可以当做 Collection<Object> 里面的 p 自然也是 Object 因为 java 有类型擦除所以得不到原始的类型 可以运行时判断 p instanceof String == true
    mazai
        16
    mazai  
       2019-01-16 08:49:48 +08:00 via iPhone
    第一种你 get 到的是 object 类型,强转的时候也没有指明,所以是 object
    beijiaxu
        17
    beijiaxu  
    OP
       2019-01-16 09:21:58 +08:00
    @WangYanjie 并不是期望输出 class type...这个 2 种调用输出都一样。
    想问的是在 lambda 函数里参数 p 的编译时推导的类型,一个是 Object,一个是 String,所以参数 p 调用方法时若不指定 methodA 签名里的泛型,会需要用到强转,否则编译错误。给出的提示可用方法只有 Object 的方法,String 的方法一个都没有。
    mezi04
        18
    mezi04  
       2019-01-16 10:12:33 +08:00
    建议楼主看看 collection 的源码。methodB 指定了 Collection 泛型的类型,编译器自然可以推断出 p 的类型了
    WangYanjie
        19
    WangYanjie  
       2019-01-16 21:55:09 +08:00
    @beijiaxu 看错题了,尴尬

    我理解的,明天去学习下能不能看到编译后的代码的
    1 泛型在编译期间会有 type erasure 的过程,会导致 Collection<String> 等价于 Collection<Object>,
    2 在 type erasure 的过程中,编译器会按需要自动加上
    WangYanjie
        20
    WangYanjie  
       2019-01-16 22:28:04 +08:00
    @WangYanjie
    类型转换
    3 Collection 是 raw type, 不参与 type erasure

    一种猜想是 type ensure 的过程中加了 type cast,
    一种猜想是 type ensure 的过程中加了 bridge method.

    我倾向与后者
    当使用 Collection 时,lambda 编译时对应的是 A
    interface Consumer<T> {
    accept(T o)
    }
    class A implements Consumer {
    accept(Object o) {}
    }
    编译后
    interface Consume {
    accept(Object o)
    }
    class A implements Consumer {
    accept(Object o) {}
    }

    当使用 Collection<String> 时,lambda 编译时对应的是 A
    class Consumer<T> {
    accept(Object o)
    }
    class A implements Consumer<String> {
    accept(String o) {}
    }
    编译后会增加 i 一个 bridge method
    class Consumer {
    accept(Object o)
    }
    class A implements Consumer<String> {
    accept(String o) {}
    accept(Object o) {this.accept((String) o)}
    }

    主要的信息来源是 Java Generic FAQs: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102

    从头阅读风味更佳
    beijiaxu
        21
    beijiaxu  
    OP
       2019-01-17 16:15:29 +08:00
    @WangYanjie 我又重新写了下例子,感觉都误导到大家了。。。
    j2gg0s
        22
    j2gg0s  
       2019-01-18 00:47:26 +08:00
    @beijiaxu 我发现我有两个号,另一个叫 WangYanjie
    昨天是我发现我后面不能自圆其说了,今天仔细看了下。
    直接的原因是因为:调用 methodA 时如果有 unchecked conversation,会导致返回类型为定义返回类型的擦除后类型。
    所以 methodA(Collection<String>) 返回 List<String>; methodA(Collection) 返回 List ;
    具体可以看 https://segmentfault.com/a/1190000017935037,我整理在此了
    beijiaxu
        23
    beijiaxu  
    OP
       2019-01-18 13:52:35 +08:00
    @j2gg0s 谢谢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2434 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 02:06 · PVG 10:06 · LAX 18:06 · JFK 21:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.