Java 8 собирать и уменьшать

Как известно, при накоплении «уменьшить» всегда возвращает новый неизменяемый объект, а «собрать» внесет изменения в изменяемый объект.

Однако, когда я случайно назначаю ссылку на один метод как методу сокращения, так и методу сбора, он компилируется без каких-либо ошибок. Почему?

Взгляните на следующий код:

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);
    }
}

person ning    schedule 28.12.2014    source источник


Ответы (1)


В основном вопрос сводится к следующему: BiConsumer — это функциональный интерфейс, функция которого объявлена ​​так:

void accept(T t, U u)

Вы дали ему ссылку на метод с правильными параметрами, но с неправильным типом возвращаемого значения:

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

[Параметр T на самом деле является объектом this при вызове reduce, поскольку 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 — это тип результата метода функционального интерфейса. Я думаю, что это предложение, которое позволяет использовать непустую ссылку на метод там, где ожидается ссылка на недействительный метод.

(Изменить: Как указал Исмаил в комментариях, JLS 15.13.2 здесь может быть правильным предложением; оно говорит о том, что ссылка на метод соответствует типу функции, и одно из условий для этого заключается в том, что результат типа функции void.)

В любом случае, надеюсь, это объясняет, почему он компилируется. Конечно, компилятор не всегда может сказать, когда вы делаете что-то, что приведет к неправильным результатам.

person ajb    schedule 28.12.2014
comment
Я думаю, что 15.13.2 более уместно в этом случае -- Выражение ссылки на метод совместимо в контексте присваивания [...] с целевым типом T [...], если [...] Результат типа функции [of Т] является недействительным - person Ismail Badawi; 28.12.2014
comment
хорошо, я думаю, объяснение довольно ясно. Не уверен, почему это принято, так как в большинстве случаев это вызовет ошибку. Строгая проверка типов должна предотвращать подобные ошибки. - person ning; 28.12.2014
comment
Вы правы в мотивации. Точно так же, как вы можете вызвать метод и отбросить тип возвращаемого значения, вы можете преобразовать метод, несущий результат, в функциональный интерфейс, возвращающий void. Предыдущий комментатор беспокоится, что эта гибкость приводит к ошибкам, но альтернатива была хуже — вы даже не могли преобразовать aList::add в Consumer, так как add что-то возвращает — и это было бы очень раздражающим, если бы вы не могли сказать x.forEach(list::add)! Любое решение должно было кого-то раздражать. Этот путь считался меньшим из зол. И действительно, это даже близко не стояло. - person Brian Goetz; 28.12.2014
comment
@BrianGoetz Спасибо, что указали на это. Я не думал о функциях, основная цель которых не вернуть значение, а внести какое-то важное изменение, но которые еще и возвращают какой-то статус, или объект для цепочки, или что-то еще. Возможно, какой-нибудь будущий язык будет проводить различие между этими типами функций. (Предварительная версия Ады различала функции, которым не разрешалось иметь побочные эффекты, и процедуры, возвращающие значение. Это не вошло в окончательный стандарт 1983 года.) - person ajb; 29.12.2014