Подтвердить что ты не робот

Нехватывающая лямбда, тем не менее, захватывает охватывающий экземпляр

Я написал этот код:

import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws Exception {
        new Main();
    }

    private Main() throws Exception {
        Supplier<Thread> supplier = (Supplier<Thread> & Serializable) () -> new Thread() {};
        new ObjectOutputStream(System.out).writeObject(supplier);
    }
}

Если я запустил его, я получу исключение:

Exception in thread "main" java.io.NotSerializableException: Main
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1378)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1174)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.<init>(Main.java:28)

Тем не менее, я понимаю, что я правильно сделал лямбда Serializable, и я объявил это так, что он не относится к окружающему контексту, поэтому он не захватывает лямбда. Тем не менее экземпляр Main фиксируется, и результат выражения лямбда не может быть сериализован. Я понимаю, что объявляю анонимный класс внутри лямбда, но я ожидал бы, что сам экземпляр лямбда будет его охватывающим экземпляром, а не окружающим типом Main.

Является ли это поведение ожидаемым спецификацией языка Java и, если да, то как?

4b9b3361

Ответ 1

В вашем элементе лямбда-выражения у вас есть объявление анонимного класса new Thread() {}, и вы не находитесь в контексте static, поэтому это выражение неявно захватывает this, которое имеет такое же значение в выражении лямбда, что и вне it, per JLS §15.27.2, Lambda Body:

В отличие от кода, появляющегося в объявлениях анонимного класса, значение имен и ключевых слов this и super, входящих в тело лямбда, наряду с доступностью ссылочных объявлений, такое же, как в окружающем контексте (за исключением того, что параметры лямбда вводят новые имена).

Прозрачность this (как явная, так и неявная) в теле выражения лямбда - то есть, рассматривая его так же, как и в окружающем контексте, - обеспечивает большую гибкость для реализаций и предотвращает значение неквалифицированных имен в тело зависит от разрешения перегрузки.

Поскольку окружающий контекст определяет поведение анонимного класса, вы можете легко исправить проблему, используя контекст static для создания вложенного класса:

import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws Exception {
        write();
    }

    static void write() throws Exception {
        Supplier<Thread> supplier = (Supplier<Thread> & Serializable)() -> new Thread() {};
        new ObjectOutputStream(System.out).writeObject(supplier);
    }
}

Тогда никакой окружающий экземпляр не будет захвачен.

Обратите внимание, что это можно рассматривать как общую идею лямбда-выражений, чтобы определить функции как выражения, имеющие то же значение, что и в контексте, который они пишут, за исключением введения параметров функции. Генерация экземпляра функционального интерфейса - это всего лишь средство, позволяющее получить эту концепцию на языке программирования Java совместимым и бесшумным способом, но не концепцией влияния на значение лямбда-выражения.

Ответ 2

Проще говоря:

  • анонимные классы всегда ссылаются на экземпляр экземпляра при создании в нестационарном контексте
  • this в lambda - экземпляр, охватывающий лямбда (не сам лямбда)

Из этого следует, что анонимный класс внутри выражения лямбда захватывает экземпляр, охватывающий выражение лямбда. В вашем примере он захватывает экземпляр Main, который не является сериализуемым.

Возможные решения:

  • создать лямбда в статическом контексте: в статическом методе или назначить статическое поле
  • заменить анонимный класс на static вложенный класс:
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) throws Exception {
        new Main();
    }

    private Main() throws Exception {
        Supplier<Thread> supplier = (Supplier<Thread> & Serializable) MyThread::new;
        new ObjectOutputStream(System.out).writeObject(supplier);
    }

    private static class MyThread extends Thread {
    }
}