lambdas中隐含的匿名类型

在这个问题中 ,用户@Holger提供了一个答案 ,显示了匿名类的不常见用法,我不知道。

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

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

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


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

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

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


现在,我可以想象这个function的一个非常有用的用法……很多时候,我需要在Stream管道上发布一些map操作,同时还保留原始元素,即假设我有一个人员列表:

 public class Person { Long id; String name, lastName; // getters, setters, hashCode, equals... } List 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类,或者Pair ,甚至是Tuple ,或者只是使用AbstractMap.SimpleEntry

 people.stream() .map(p -> new Pair(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's intricacies"; Optional 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中有关于此的内容?
  • 这有什么限制,如果有的话?
  • 编写这样的代码实际上是否安全
  • 是否有简写语法,或者这是我们能做的最好的?

JLS中没有提到这种用法,但是,当然,通过列举编程语言提供的所有可能性,规范不起作用。 相反,你必须应用关于类型的forms规则,它们对匿名类型没有例外,换句话说,规范在任何时候都没有说,表达式的类型必须回退到指定的超类型匿名类的情况。

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

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

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

 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) "x"; // e has type CharSequence & Comparable 

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

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

绝对不是答案,但更多的是0.02$

这是可能的,因为lambdas为您提供了一个由编译器推断的变量; 它是从上下文推断出来的。 这就是为什么它只能用于推断的类型,而不是我们可以声明的类型。

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

这就像说:

  Stream // Stream? 

它在你的上一个例子中不起作用,因为你明显告诉编译器类型是: Optionaloptional因此打破了anonymous type推断。

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

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

由于var x是由编译器推断的,因此int test = xy; 也会工作

这是在某处记录的,还是在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 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 chain(T instance) { return instance; } 

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

其余的问题@Holger已经回答了。

编辑

我们可以得出结论,只要匿名类型由generics类型变量表示,这种结构是可行的吗?

对不起,由于我的英语不好,我再也找不到JLS参考了。 但我可以告诉你它确实如此。 您可以使用javap命令查看详细信息。 例如:

 public class Main { void test() { int count = chain(new Object() { int count = 1; }).count; }  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."":(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