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

Подразумеваемые анонимные типы внутри lambdas

В этот вопрос пользователь @Holger предоставил ответ, который показывает необычное использование анонимных классов, о котором я не знал.

В этом ответе используются потоки, но этот вопрос касается не потоков, поскольку эта анонимная конструкция типа может использоваться в других контекстах, то есть:

String s = "Digging into Java intricacies";

Optional.of(new Object() { String field = s; })
    .map(anonymous -> anonymous.field) // anonymous implied type 
    .ifPresent(System.out::println);

К моему удивлению, это компилирует и печатает ожидаемый результат.


Примечание. Мне хорошо известно, что с древних времен можно построить анонимный внутренний класс и использовать его элементы следующим образом:

int result = new Object() { int incr(int i) {return i + 1; } }.incr(3);
System.out.println(result); // 4

Однако это не то, о чем я прошу здесь. Мой случай отличается, потому что анонимный тип распространяется по цепочке методов Optional.


Теперь я могу представить очень полезное использование этой функции... Много раз мне нужно было выполнить некоторую операцию map по конвейеру Stream, а также сохранить исходный элемент, т.е. предположим, что у меня есть список людей:

public class Person {
    Long id;
    String name, lastName;
    // getters, setters, hashCode, equals...
}

List<Person> people = ...;

И мне нужно сохранить JSON-представление моих экземпляров Person в каком-то репозитории, для которого мне нужна строка JSON для каждого экземпляра Person, а также каждый Person id:

public static String toJson(Object obj) {
    String json = ...; // serialize obj with some JSON lib 
    return json;
}        

people.stream()
    .map(person -> toJson(person))
    .forEach(json -> repository.add(ID, json)); // where the ID?

В этом примере я потерял поле Person.id, так как я преобразовал каждого человека в соответствующую строку json.

Чтобы обойти это, я видел, что многие люди используют какой-то класс Holder или Pair, или даже Tuple, или просто AbstractMap.SimpleEntry:

people.stream()
    .map(p -> new Pair<Long, String>(p.getId(), toJson(p)))
    .forEach(pair -> repository.add(pair.getLeft(), pair.getRight()));

Хотя это достаточно хорошо для этого простого примера, для него все еще требуется наличие общего класса Pair. И если нам нужно распространять 3 значения через поток, я думаю, что мы могли бы использовать класс Tuple3 и т.д. Использование массива также является опцией, однако оно не является безопасным, если все значения одного типа.

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

people.stream()
    .map(p -> new Object() { Long id = p.getId(); String json = toJson(p); })
    .forEach(it -> repository.add(it.id, it.json));

Это волшебство! Теперь мы можем иметь как можно больше полей, а также сохранять безопасность типов.

Во время тестирования я не смог использовать подразумеваемый тип в отдельных строках кода. Если я изменю свой исходный код следующим образом:

String s = "Digging into Java intricacies";

Optional<Object> optional = Optional.of(new Object() { String field = s; });

optional.map(anonymous -> anonymous.field)
    .ifPresent(System.out::println);

Я получаю ошибку компиляции:

Error: java: cannot find symbol
  symbol:   variable field
  location: variable anonymous of type java.lang.Object

И этого следует ожидать, потому что в классе Object нет члена с именем field.

Итак, я хотел бы знать:

  • Является ли это документированным где-то или есть что-то об этом в JLS?
  • Какие существуют ограничения, если таковые имеются?
  • Действительно ли можно написать такой код?
  • Есть ли сокращенный синтаксис для этого, или это лучшее, что мы можем сделать?
4b9b3361

Ответ 1

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

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

Итак, если тип выражения new Object() { String field; } является анонимным типом, содержащим поле "field", будет работать не только доступ new Object() { String field; }.field, но и Collections.singletonList(new Object() { String field; }).get(0).field, если явное правило не запрещает его и не будет последовательно, то же самое относится к лямбда-выражениям.

Начиная с Java 10, вы можете использовать var для объявления локальных переменных, тип которых выводится из инициализатора. Таким образом, теперь вы можете объявить произвольные локальные переменные, а не только параметры лямбда, имеющие тип анонимного класса. Например, следующие работы

var obj = new Object() { int i = 42; String s = "blah"; };
obj.i += 10;
System.out.println(obj.s);

Аналогично, мы можем привести пример вашего вопроса:

var optional = Optional.of(new Object() { String field = s; });
optional.map(anonymous -> anonymous.field).ifPresent(System.out::println);

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

var d = new Object() {};  // d has the type of the anonymous class

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

var e = (CharSequence & Comparable<String>) "x";
                          // e has type CharSequence & Comparable<String>

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

Кроме того, экземпляры, созданные как new Object() { String field = s; }, потребляют вдвое больше памяти по мере необходимости, так как они будут содержать не только объявленные поля, но также и захваченные значения, используемые для инициализации полей. В примере new Object() { Long id = p.getId(); String json = toJson(p); } вы платите за хранение трех ссылок вместо двух, поскольку p был захвачен. В нестационарном контексте анонимный внутренний класс также всегда захватывает окружающие this.

Ответ 2

Абсолютно не ответ, а больше 0.02$.

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

Компилятор может deduce использовать тип как анонимный, просто он не может выразить его, чтобы мы могли использовать его по имени. Таким образом, информация есть, но из-за ограничений языка мы не можем добраться до нее.

Мне нравится говорить:

 Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?

Это не работает в вашем последнем примере, потому что вы, очевидно, сказали компилятору, что это тип: Optional<Object> optional, нарушив anonymous type вывод.

Эти анонимные типы теперь (java-10 wise) доступны в гораздо более простом виде:

    var x = new Object() {
        int y;
        int z;
    };

    int test = x.y; 

Так как var x выводится компилятором, int test = x.y; будет работать также

Ответ 3

Является ли это документированным где-то или есть что-то об этом в JLS?

Я думаю, что это не особый случай в анонимном классе, который нужно ввести в JLS. как вы упомянули в своем вопросе, вы можете напрямую обратиться к анонимным членам класса, например: incr(3).

Сначала рассмотрим пример локального класса, это будет означать, почему цепочка с анонимным классом может получить доступ к своим членам. например:

@Test
void localClass() throws Throwable {
    class Foo {
        private String foo = "bar";
    }

    Foo it = new Foo();

    assertThat(it.foo, equalTo("bar"));
}

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

Как отметил в своем ответе @Holger в своем ответе, компилятор создаст внутренний класс, например EnclosingClass${digit} для каждого анонимного класса. поэтому Object{...} имеет собственный тип, полученный из Object. из-за методов цепи возвращают собственный тип EnclosingClass${digit}, а не тип, полученный из Object. вот почему вы подключаете экземпляр анонимного класса, который отлично работает.

@Test
void chainingAnonymousClassInstance() throws Throwable {
    String foo = chain(new Object() { String foo = "bar"; }).foo;

    assertThat(foo,equalTo("bar"));
}

private <T> T chain(T instance) {
    return instance;
}

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

И остальной вопрос @Holger ответил.

Изменить

можно заключить, что эта конструкция возможна, если анонимный тип представлен переменной общего типа?

Извините, я не могу найти ссылку на JLS снова, так как мой английский плохой. но я могу сказать вам, что так оно и есть. вы можете использовать команду javap для просмотра деталей. например:

public class Main {

    void test() {
        int count = chain(new Object() { int count = 1; }).count;
    }

    <T> T chain(T it) {
        return it;
    }
}

и вы увидите, что команда checkcast вызывается ниже:

void test();
descriptor: ()V
     0: aload_0
     1: new           #2      // class Main$1
     4: dup
     5: aload_0
     6: invokespecial #3     // Method Main$1."<init>":(LMain;)V
     9: invokevirtual #4    // Method chain:(Ljava/lang/Object;)Ljava/lang/Object;
    12: checkcast     #2    // class Main$1
    15: getfield      #5    // Field Main$1.count:I
    18: istore_1
    19: return