无法理解此Java Stream + Generics示例

时间:2015-11-24 22:25:45

标签: java eclipse generics java-8 java-stream

有人可以帮我理解为什么这段代码的行为与评论

中描述的一样
number++;

它显然与泛型方法中的类型定义有关,它是必须以某种方式提供的信息......但为什么它是强制性的?在语法上的位置和方式,我应该从方法// 1) compiles List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll); /* * 2) does not compile * * Exception in thread "main" java.lang.Error: Unresolved compilation problems: * Type mismatch: cannot convert from Object to <unknown> * The type ArrayList does not define add(Object, Integer) that is applicable here * The type ArrayList does not define addAll(Object, Object) that is applicable here */ Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // 3) compiles Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll); /* * 4) does not compile * * Exception in thread "main" java.lang.Error: Unresolved compilation problems: * Type mismatch: cannot convert from Object to <unknown> * The type ArrayList does not define add(Object, Integer) that is applicable here * The type ArrayList<Integer> does not define addAll(Object, Object) that is applicable here */ Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll); of()的签名中找出它吗?

collect()

3 个答案:

答案 0 :(得分:7)

虽然这不是分析http://download.oracle.com/otndocs/jcp/lambda-0_9_3-fr-eval-spec/index.html上的Lambda规范的答案,但我试图找出它所依赖的内容。

Stream类复制两个方法:

static class Stream2<T> {

    @SafeVarargs
    @SuppressWarnings("varargs") // Creating a stream from an array is safe
    public static<T> Stream2<T> of(T... values) {
        return new Stream2<>();
    }

     public  <R> R collect(  Supplier<R> supplier,
             BiConsumer<R, ? super T> accumulator,
             BiConsumer<R, R> combiner){return null;}

}

编译:

Stream2.of(1,2,3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll );

喜欢OP(2)。

现在通过将第一个参数移动到第三个位置来更改collect方法

     public  <R> R collect(BiConsumer<R, ? super T> accumulator,
             BiConsumer<R, R> combiner,
             Supplier<R> supplier
     ){return null;}

这仍然有效(5):

 Stream2.of(1,2,3).collect(ArrayList::add, ArrayList::addAll,ArrayList::new );

这也有效(6):

 Stream2.of(1,2,3).collect(ArrayList::add, ArrayList::addAll,ArrayList<Integer>::new );

这些不起作用(7,8):

 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList::addAll,ArrayList::new );
 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList<Integer>::addAll,ArrayList::new );

但这再次起作用(9):

 Stream2.of(1,2,3).collect(ArrayList<Integer>::add, ArrayList<Integer>::addAll,ArrayList<Integer>::new );

所以我想当供应商使用显式类型参数进行注释时,它似乎有效。当只有消费者时,却没有。但也许其他人知道为什么这会产生影响。

编辑:尝试使用TestList,它甚至更奇怪:

public class StreamTest2 {

    public static void main(String[] args) {

        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll2);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll3);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll4);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll5);
        Stream.of(1, 2, 3).collect(TestList::new, TestList::add, TestList<Integer>::addAll6);

    }
}

class TestList<T> extends AbstractList<T> {

    @Override
    public T get(int index) {
        return null;
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
        return true;
    }

    public boolean addAll2(TestList<? extends T> c) {
        return true;
    }
    public boolean addAll3(Collection<T> c) {
        return true;
    }

    public boolean addAll4(List<? extends T> c) {
        return true;
    }
    public boolean addAll5(AbstractList<? extends T> c) {
        return true;
    }

    public boolean addAll6(Collection<? extends T> c) {
        return true;
    }

    @Override
    public boolean add(T e) {
        return true;
    }
}

addAll不起作用,但addAll2 - 6可行。即使addAll6有效,与原始addAll具有相同的签名

答案 1 :(得分:1)

每当您为编译器错误而烦恼时,您应该包括您使用的编译器及其版本号。如果您使用的是标准javac以外的编译器,则应尝试javac并编译结果。

写作时

List<Integer> l = Stream.of(1, 2, 3)
    .collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

编译器将使用目标类型List<Integer>来推断类型R(这里与目标类型完全匹配)。没有目标类型,如

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

编译器将推断供应商的类型R并推断ArrayList<Object>。由于ArrayList<Object>能够保留Integer个实例并提供必要的addaddAll方法,因此在使用标准javac时,此构造可以毫无问题地进行编译。我尝试了jdk1.8.0_05jdk1.8.0_20jdk1.8.0_40jdk1.8.0_51jdk1.8.0_60jdk1.9.0b29jdk1.9.0b66,以确保有没有涉及特定版本的错误。我想,您使用的是Eclipse,因为Java 8类型推断存在问题。

同样,使用

Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll);

有效,但现在您的提示会强制R的推断类型为ArrayList<Integer>。相比之下

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

不起作用,因为编译器推断ArrayList<Object>供应商的返回类型与方法ArrayList<Integer>::addAll不兼容。但以下情况可行:

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Object>::addAll);

但是,使用标准javac ...

时,您不需要任何显式类型

答案 2 :(得分:1)

当面对这种情况时,我觉得理解问题的最好方法是纯粹的推理和逻辑。类型推断是一种涵盖entire chapter of the JLS的野兽。让我们暂时忘记ECJ和javac,仔细考虑4个示例,并确定 a 给定的编译器是否能够或应该能够根据JLS编译它。< / p>

因此,请考虑collect的签名:

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);

此签名的问题是:R是什么以及编译器如何确定R是什么?

我们可以争辩说,编译器能够使用我们作为R的参数提供collect的类型。例如,第一个是Supplier<R>,所以如果,e。例如,我们将作为参数() -> new StringBuilder()给出,编译器应该能够将R推断为StringBuilder

让我们考虑以下情况:

List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

在此示例中,collect的3个参数是3个方法引用,我们将结果分配给List<Integer>。编译器可以采用的信息:我们说它使用的类型是Integer

好的,它应该编译吗?让我们分别考虑collect的3个参数:

  • 在这种情况下,第一个是Supplier<ArrayList<Integer>>,我们正在给ArrayList::new。这个方法引用可以引用现有方法(在这种情况下是构造函数)吗?是的,它可以引用ArrayList的空构造函数(作为lambda - () -> new ArrayList<Integer>())因为ArrayList<Integer>可以绑定到List<Integer>。到目前为止一切顺利。
  • 第二个是BiConsumer<ArrayList<Integer>, ? super Integer>。请注意T = Integer,因为Stream由整数文字组成,类型为int。我们正在提供ArrayList::add,可以引用add(e)(作为lambda:(list, element) -> list.add(element))。
  • 第三个是BiConsumer<List<Integer>, List<Integer>>,我们正在给ArrayList::addAll。它还可以引用addAll(c)addAll作为参数Collection<? extends Integer>List<Integer>可以绑定到此类型。

所以基本上,只有推理,这样的表达式应该编译。

现在,让我们考虑你的4个案例:

案例1:

List<Integer> l = Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll);

我们将表达式的结果指定为List<Integer>,因此我们告诉编译器:R此处为List<Integer>。与上述情况的不同之处在于我们提供了方法参考ArrayList<Integer>::addAll。让我们仔细看看这个。此方法引用尝试引用方法名称addAll,该方法名称List<Integer>(我们的R)作为参数,应该应用于ArrayList<Integer>(类型)我们在方法参考中明确地使用了。那么这与我们在上面的推理中得出的结论完全相同;它应该有效:R = List<Integer>可以绑定到ArrayList<Integer>

案例2

Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

与上述情况的不同之处在于我们没有分配表达式的结果。因此,编制者只能根据供应商推断出类型:ArrayList::new,因此应将其推断为ArrayList<Object>

  • ArrayList::add可以绑定到BiConsumer<ArrayList<Object>, ? super Integer>,因为add的{​​{1}}方法可以将ArrayList<Object>作为参数。
  • Integer可以绑定到ArrayList::addAll

所以编译器应该能够编译它。

案例3

BiConsumer<ArrayList<Object>, ArrayList<Object>>

与案例2的区别在于,我们明确告诉编译器供应商提供Stream.of(1, 2, 3).collect(ArrayList<Integer>::new, ArrayList::add, ArrayList::addAll); 个实例,而不仅仅是ArrayList<Integer>。它有什么改变吗?它不应该,案例2中的推理仍然存在于此。所以它也应该编译。

案例4

ArrayList<Object>

与案例2的不同之处在于,这一次,我们正在给予Stream.of(1, 2, 3).collect(ArrayList::new, ArrayList::add, ArrayList<Integer>::addAll); 。根据案例2,我们知道由于供应商(没有特定类型),编译器将ArrayList<Integer>::addAll推断为R。这应该会导致问题:ArrayList<Object>尝试在ArrayList<Integer>::addAll上引用方法addAll,但我们看到,对于编译器,ArrayList<Integer>被推断为this } ArrayList<Object> ArrayList<Object>。所以这不应该编译。

我们可以做些什么来使其编译?

  • 将供应商更改为具有特定ArrayList<Integer>类型。
  • 从方法参考中删除显式Integer
  • 改为将方法参考编写为<Integer>

结论

我使用Eclipse Mars 4.5.1和ArrayList::<Integer> addAll 1.8.0_60测试了这些示例。结果是javac的行为与我们推断的结论完全相同:只有案例4没有编译。

总的来说,Eclipse有一个小bug。