Почему лямбда-выражения в Java 8 требуют, чтобы переменные, используемые внутри него, использовали модификатор final, но не при использовании ссылки на метод?

Рассмотрим следующий класс:

class Foo<T> {

    void handle(T t) {
        System.out.println("handling " + t);
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);

        Foo<Integer> f = new Foo<>();

        list.forEach(f::handle);             // compiles fine
        //list.forEach(obj -> f.handle(obj));// compilation error

        f = new Foo<>(); // reassign f

    }
}

Почему я получаю ошибку компиляции для obj -> f.handle(obj), но не для f::handle?


person Mateus Viccari    schedule 22.12.2014    source источник
comment
Гм, обе версии должны работать без модификатора final. (Однако он все еще должен быть эффективно окончательным.)   -  person Louis Wasserman    schedule 22.12.2014
comment
Да, вопрос был, почему вторая версия не требует модификатора final.   -  person Mateus Viccari    schedule 22.12.2014
comment
Ни одна из версий не требует финальной версии.   -  person Louis Wasserman    schedule 22.12.2014
comment
Вы присваиваете новое значение s после вызова forEach?   -  person Pshemo    schedule 22.12.2014
comment
@MateusViccari Я пытался упростить твой вопрос. Не стесняйтесь отменить мое редактирование, если вы не чувствуете, что это то, что вы хотели спросить.   -  person Pshemo    schedule 22.12.2014
comment
Мне больше нравится, что stackoverflow.com/a/33053161/94687 принят ответ на аналогичный вопрос, потому что он немного больше подробно и понятно. Хотя тот вопрос более свежий (молодой), а этот старше. Итак, если закрыть дубликаты, этот должен выжить в соответствии со своим возрастом.   -  person imz -- Ivan Zakharyaschev    schedule 29.07.2021


Ответы (2)


Это две разные конструкции, которые делают две разные вещи. В первом случае вы получаете ссылку на метод конкретного объекта: это нужно сделать только один раз, после чего JVM имеет свою собственную ссылку (по сути, окончательную) на объект f и может вызывать метод handle. Во втором случае при каждом вызове JVM должна разрешать ссылку f и поэтому жалуется, что f должно быть final. Вы можете легко написать код, который устанавливает f в null во время работы forEach и, таким образом, вызывает NPE.

person Giovanni Botta    schedule 22.12.2014
comment
Я сделал небольшой пример, который может подчеркнуть разницу между лямбда-выражением и ссылкой на метод: pastebin.com/4W0ivi7U - person vbezhenar; 23.12.2014
comment
Вы также можете написать final Foo<Integer> f = null; и вызвать NPE. Это не объясняет, почему f должен быть окончательным. - person a better oliver; 23.12.2014
comment
Какой-то парень только что изменил весь мой вопрос, поэтому я не знаю, вы говорите о его вопросе или о моем. Но я думаю, что я понял вашу точку зрения, спасибо. - person Mateus Viccari; 23.12.2014
comment
@MateusViccari Я какой-то парень. Извините, если я слишком сильно изменил ваш вопрос, пытался предотвратить его закрытие, поскольку неясно, что вы спрашиваете, опубликовав пример, который позволил бы нам легко воспроизвести вашу проблему. Если я неправильно понял проблему, с которой вы столкнулись, рассмотрите возможность публикации другого вопроса, но на этот раз предоставьте больше информации с кодом, который позволит другим воспроизвести ошибку, описанную в вопросе. - person Pshemo; 23.12.2014
comment
@zeroflagL Да, вы могли бы, но с точки зрения компилятора важно то, что ссылка не меняется, и поэтому программа имеет более детерминированное поведение. Компилятор не помешает вам охотно выстрелить себе в ногу! :) - person Giovanni Botta; 23.12.2014

Чтобы добавить иллюстрацию к ответу Джованни, мы можем выделить разницу между f::handle и obj -> f.handle(obj), если заменим f вызовом метода:

static Set<String> f() {
    System.out.println("  f() called");
    return new HashSet<>();
}

public static void main(String[] args) {
    List<String> empty = Collections.emptyList();
    List<String> strings = Arrays.asList("foo", "bar");

    System.out.println("method reference, no invocations");
    empty.forEach(f()::add);

    System.out.println("method reference, 2 invocations");
    strings.forEach(f()::add);

    System.out.println("lambda, no invocations");
    empty.forEach(str -> f().add(str));

    System.out.println("lambda, 2 invocations");
    strings.forEach(str -> f().add(str));
}   

Выход:

method reference, no invocations
  f() called
method reference, 2 invocations
  f() called
lambda, no invocations
lambda, 2 invocations
  f() called
  f() called

Итак, как вы видите, .forEach(f()::add) сразу оценит f(), а затем вызовет add(...) для результата столько раз, сколько вызывается лямбда.

С другой стороны, str -> f().add(str) ничего не делает заранее, но будет вызывать f() каждый раз, когда вызывается лямбда.

person Misha    schedule 23.12.2014