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
的成员
所以我想知道:
- 这是否在某处记录或在 JLS 中有关于此的内容?
- 这有什么限制,如果有的话?
- 这样写代码真的安全吗?
- 是否有 shorthand 语法,或者这是我们能做的最好的?
绝对不是答案,而是 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
在 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
的成员
所以我想知道:
- 这是否在某处记录或在 JLS 中有关于此的内容?
- 这有什么限制,如果有的话?
- 这样写代码真的安全吗?
- 是否有 shorthand 语法,或者这是我们能做的最好的?
绝对不是答案,而是 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