lambda 中隐含的匿名类型

Implied anonymous types inside lambdas

this question, user @Holger provided an answer 中显示了匿名 classes 的不常见用法,这是我不知道的。

那个答案使用流,但这个问题不是关于流的,因为这个匿名类型构造可以在其他上下文中使用,即:

String s = "Digging into Java's intricacies";

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

令我惊讶的是,这编译并打印了预期的输出。


注意:我很清楚,自古以来,可以构造一个匿名内部class并使用其成员如下:

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

但是,这不是我在这里要问的。我的情况不同,因为匿名类型是通过 Optional 方法链传播的。


现在,我可以想象这个功能的一个非常有用的用法...很多时候,我需要在 Stream 管道上发出一些 map 操作,同时还要保留原始元素,即假设我有一份人员名单:

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

List<Person> people = ...;

而且我需要在某个存储库中存储我的 Person 实例的 JSON 表示,为此我需要每个 Person 实例的 JSON 字符串,以及每个 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's the ID?

在此示例中,我丢失了 Person.id 字段,因为我已将每个人都转换为相应的 json 字符串。

为了避免这种情况,我看到很多人使用某种 Holder class,或 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 class。如果我们需要通过流传播 3 个值,我认为我们可以使用 Tuple3 class 等。使用数组也是一种选择,但是它不是类型安全的,除非所有值都是同类型。

因此,使用隐含的匿名类型,上面的相同代码可以重写如下:

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's 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 class.

中没有名为 field 的成员

所以我想知道:

绝对不是答案,而是 0.02$.

这是可能的,因为 lambda 表达式为您提供了一个由编译器推断的变量;它是从上下文中推断出来的。这就是为什么只有 推断 的类型才有可能,而我们可以 声明 .

的类型则不行

编译器可以deduce类型作为匿名类型,只是它不能表达它所以我们可以按名称使用它。所以信息在那里,但由于语言限制我们无法获得。

这就像在说:

 Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?

它在你的最后一个例子中不起作用,因为你显然已经告诉编译器类型是:Optional<Object> optional 从而打破了 anonymous type 推断。

这些匿名类型现在(java-10 明智地)也可以以更简单的方式使用:

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

    int test = x.y; 

由于 var x 是由编译器推断的,因此 int test = x.y; 也可以工作

JLS 中没有提到这种用法,但是,当然,该规范不会通过编程语言提供的枚举所有可能性来工作。相反,您必须应用关于类型的正式规则,它们对匿名类型没有例外,换句话说,规范在任何时候都没有说,表达式的类型必须回退到命名的超类型匿名 classes.

的情况

当然,我本可以在规范的深处忽略这样的声明,但对我来说,关于匿名类型的唯一限制源于它们的 anonymous 看起来总是很自然性质,即需要通过名称引用类型的每种语言构造不能直接使用该类型,因此您必须选择一个超类型。

所以如果表达式new Object() { String field; }的类型是包含字段“field”的匿名类型,那么不仅访问new Object() { String field; }.field有效,而且Collections.singletonList(new Object() { String field; }).get(0).field, 除非明确的规则禁止它并且始终如一,这同样适用于 lambda 表达式。

从 Java10 开始,您可以使用 var 声明其类型从初始化程序推断的局部变量。这样,您现在可以声明任意局部变量,而不仅仅是 lambda 参数,具有匿名类型 class。例如,以下作品

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

在这种情况下,我们可以参考 the specification 显示的类似示例,表明这不是疏忽,而是有意为之:

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

另一个暗示变量可能具有不可表示类型的一般可能性:

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

也就是说,我必须警告不要过度使用该功能。除了可读性问题(你自己称它为“不常见用法”)之外,在你使用它的每个地方,你都在创建一个不同的新 class (与“双括号初始化”相比)。它不像实际的元组类型或其他编程语言的未命名类型,它们会平等对待同一组成员的所有出现。

此外,像 new Object() { String field = s; } 这样创建的实例消耗的内存是所需内存的两倍,因为它不仅包含声明的字段,还包含用于初始化字段的捕获值。在 new Object() { Long id = p.getId(); String json = toJson(p); } 示例中,您为三个引用而不是两个引用的存储付费,因为 p 已被捕获。在非静态上下文中,匿名内部 class 也总是捕获周围的 this.

Is this documented somewhere or is there something about this in the JLS?

我认为这不是需要引入 JLS 的匿名 class 中的特例。正如您在问题中提到的,您可以直接访问匿名 class 成员,例如:incr(3)

首先,让我们看一个本地 class 示例,这将说明为什么具有匿名 class 的链可以访问其成员。例如:

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

    Foo it = new Foo();

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

正如我们所见,即使其成员是私有的,也可以在其范围之外访问本地 class 成员。

正如@Holger 在他的回答中提到的那样,编译器将为每个匿名class 创建一个内部class,如EnclosingClass${digit}。所以 Object{...} 有自己的类型,派生自 Object。由于链方法 return 它拥有类型 EnclosingClass${digit} 而不是从 Object 派生的类型。这就是为什么您链接匿名 class 实例可以正常工作的原因。

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

由于我们不能直接引用匿名class,所以当我们将链式方法分成两行时,我们实际上引用了派生自Object的类型。

AND 其余问题@Holger 已回答。

编辑

we can conclude that this construction is possible as long as the anonymous type is represented by a generic type variable?

很抱歉,我的英语不好,找不到 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
     4: dup
     5: aload_0
     6: invokespecial #3     // Method Main."<init>":(LMain;)V
     9: invokevirtual #4    // Method chain:(Ljava/lang/Object;)Ljava/lang/Object;
    12: checkcast     #2    // class Main
    15: getfield      #5    // Field Main.count:I
    18: istore_1
    19: return