【问题标题】:Java 8 collect vs reduceJava 8 收集与减少
【发布时间】:2015-02-24 18:27:30
【问题描述】:

众所周知,在进行累加时,“reduce”总是返回一个新的不可变对象,而“collect”将对可变对象进行更改。

但是,当我不小心将一个方法引用分配给 reduce 和 collect 方法时,它编译时没有任何错误。为什么?

看看下面的代码:

public class Test {
    @Test
    public void testReduce() {

        BiFunction<MutableContainer,Long,MutableContainer> func =
            MutableContainer::reduce;

        // Why this can compile?
        BiConsumer<MutableContainer,Long> consume =
            MutableContainer::reduce;

        // correct way:
        //BiConsumer<MutableContainer,Long> consume =
        //  MutableContainer::collect;


        long param=10;

        MutableContainer container = new MutableContainer(0);


        consume.accept(container, param);
        // here prints "0",incorrect result,
        // because here we expect a mutable change instead of returning a immutable value
        System.out.println(container.getSum());

        MutableContainer newContainer = func.apply(container, param);
        System.out.println(newContainer.getSum());
    }
}

class MutableContainer {
    public MutableContainer(long sum) {
        this.sum = sum;
    }

    public long getSum() {
        return sum;
    }

    public void setSum(long sum) {
        this.sum = sum;
    }

    private long sum;

    public MutableContainer reduce(long param) {
        return new MutableContainer(param);
    }

    public void collect(long param){
        this.setSum(param);
    }
}

【问题讨论】:

    标签: java java-8 reduce collect


    【解决方案1】:

    基本上,问题归结为:BiConsumer 是一个函数式接口,其函数声明如下:

    void accept(T t, U u)
    

    你给了它一个带有正确参数的方法引用,但返回类型错误:

    public MutableContainer reduce(long param) {
        return new MutableContainer(param);
    }
    

    [T 参数实际上是调用reduce 时的this 对象,因为reduce 是实例方法而不是静态方法。这就是参数正确的原因。] 但是,返回类型是 MutableContainer 而不是 void。那么问题来了,为什么编译器会接受呢?

    直觉上,我认为这是因为方法引用或多或少相当于一个匿名类,如下所示:

    new BiConsumer<MutableContainer,Long>() {
        @Override
        public void accept(MutableContainer t, Long u) {
             t.reduce(u);
         }
    }
    

    注意t.reduce(u) 会返回一个结果。但是,结果被丢弃。由于可以使用结果调用方法并丢弃结果,因此我认为,通过扩展,这就是为什么可以在方法返回结果的地方使用方法引用,对于方法返回 void 的功能接口。

    从法律上讲,我相信原因在于JLS 15.12.2.5。这部分很难,我不完全理解,但在这部分的某处它说

    如果 e 是一个精确的方法引用表达式 ... R2 是 无效。

    如果我没看错的话,R2 是函数式接口方​​法的结果类型。我认为这是允许在需要 void 方法引用的地方使用非 void 方法引用的子句。

    (编辑: 正如 Ismail 在 cmets 中指出的那样,JLS 15.13.2 可能是这里的正确子句;它谈到了与函数类型一致的方法引用,以及以下条件之一这是函数类型的结果是void。)

    无论如何,这应该有望解释为什么它编译。当然,编译器不能总是告诉你什么时候做的事情会产生不正确的结果。

    【讨论】:

    • 我认为15.13.2 在这种情况下更相关——“方法引用表达式在赋值上下文中兼容 [...] 与目标类型 T [...] if [. ..] [T] 的函数类型的结果是 void"
    • 好的,我认为解释很清楚。不知道为什么这被接受,因为它在大多数情况下会导致错误。强类型检查应该可以防止这种错误。
    • 你的动机是正确的。就像您可以调用方法并丢弃返回类型一样,您可以将带有结果的方法转换为返回 void 的功能接口。之前的评论者担心这种灵活性会招致错误,但替代方案更糟——你甚至无法将aList::add 转换为Consumer,因为add 返回了一些东西——如果你不能,这真的很烦人。不要说x.forEach(list::add)!任何一种解决方案都会惹恼某人。这种方式被认为是恶少的。真的,这甚至都算不上一个险情。
    • @BrianGoetz 感谢您指出这一点。我没有考虑过“函数”,其主要目的不是返回值,而是进行一些重要的更改,但它也返回某种状态,或用于链接的对象,或其他东西。也许一些未来的语言会区分这些类型的函数。 (Ada 的初步版本区分了不允许有副作用的“函数”和“返回值的过程”。这没有成为 1983 年的最终标准。)
    猜你喜欢
    • 2014-04-29
    • 2019-02-17
    • 1970-01-01
    • 2015-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多