lambda 表达式中隐含的匿名类型 [英] Implied anonymous types inside lambdas

查看:27
本文介绍了lambda 表达式中隐含的匿名类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

String s = "深入研究 Java 的复杂性";Optional.of(new Object() { String field = s; }).map(anonymous ->anonymous.field)//匿名隐含类型.ifPresent(System.out::println);

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

<小时>

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

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

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

<小时>

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

public class Person {长ID;字符串名称,姓氏;//getter、setter、hashCode、equals...}列表<人>人 = ...;

而且我需要在某个存储库中存储我的 Person 实例的 JSON 表示,为此我需要每个 Person 实例的 JSON 字符串,以及每个人员 id:

public static String toJson(Object obj) {字符串 json = ...;//使用一些 JSON 库序列化 obj返回json;}人流().map(person -> toJson(person)).forEach(json -> repository.add(ID, json));//身份证在哪里?

在这个例子中,我丢失了 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 = "深入研究 Java 的复杂性";可选的<对象>optional = Optional.of(new Object() { String field = s; });optional.map(匿名 -> 匿名.field).ifPresent(System.out::println);

我收到一个编译错误:

错误:java:找不到符号符号:变量字段位置:java.lang.Object 类型的变量匿名

这是意料之中的,因为在 Object 类中没有名为 field 的成员.

所以我想知道:

  • 这是否记录在某处,或者 JLS 中是否有相关内容?
  • 这有什么限制(如果有)?
  • 写这样的代码真的安全吗?
  • 是否有这方面的速记语法,或者这是我们能做的最好的吗?

解决方案

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

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

所以如果表达式的类型 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;字符串 s = "等等";};目标.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 具有匿名类的类型

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

<块引用>

var e = (CharSequence & Comparable) "x";//e 的类型为 CharSequence &可比较的<字符串>

<小时>

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

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

In this question, user @Holger provided an answer that shows an uncommon usage of anonymous classes, which I wasn't aware of.

That answer uses streams, but this question is not about streams, since this anonymous type construction can be used in other contexts, i.e.:

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

To my surprise, this compiles and prints the expected output.


Note: I'm well aware that, since ancient times, it is possible to construct an anonymous inner class and use its members as follows:

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

However, this is not what I'm asking here. My case is different, because the anonymous type is propagated through the Optional method chain.


Now, I can imagine a very useful usage for this feature... Many times, I've needed to issue some map operation over a Stream pipeline while also preserving the original element, i.e. suppose I have a list of people:

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

List<Person> people = ...;

And that I need to store a JSON representation of my Person instances in some repository, for which I need the JSON string for every Person instance, as well as each 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?

In this example, I have lost the Person.id field, since I've transformed every person to its corresponding json string.

To circumvent this, I've seen many people use some sort of Holder class, or Pair, or even Tuple, or just AbstractMap.SimpleEntry:

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

While this is good enough for this simple example, it still requires the existence of a generic Pair class. And if we need to propagate 3 values through the stream, I think we could use a Tuple3 class, etc. Using an array is also an option, however it's not type safe, unless all the values are of the same type.

So, using an implied anonymous type, the same code above could be rewritten as follows:

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

It is magic! Now we can have as many fields as desired, while also preserving type safety.

While testing this, I wasn't able to use the implied type in separate lines of code. If I modify my original code as follows:

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

I get a compilation error:

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

And this is to be expected, because there's no member named field in the Object class.

So I would like to know:

  • Is this documented somewhere or is there something about this in the JLS?
  • What limitations does this have, if any?
  • Is it actually safe to write code like this?
  • Is there a shorthand syntax for this, or is this the best we can do?

解决方案

This kind of usage has not been mentioned in the JLS, but, of course, the specification doesn’t work by enumerating all possibilities, the programming language offers. Instead, you have to apply the formal rules regarding types and they make no exceptions for anonymous types, in other words, the specification doesn’t say at any point, that the type of an expression has to fall back to the named super type in the case of anonymous classes.

Granted, I could have overlooked such a statement in the depths of the specification, but to me, it always looked natural that the only restriction regarding anonymous types stems from their anonymous nature, i.e. every language construct requiring referring to the type by name, can’t work with the type directly, so you have to pick a supertype.

So if the type of the expression new Object() { String field; } is the anonymous type containing the field "field", not only the access new Object() { String field; }.field will work, but also Collections.singletonList(new Object() { String field; }).get(0).field, unless an explicit rule forbids it and consistently, the same applies to lambda expressions.

Starting with Java 10, you can use var to declare local variables whose type is inferred from the initializer. That way, you can now declare arbitrary local variables, not only lambda parameters, having the type of an anonymous class. E.g., the following works

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

Likewise, we can make the example of your question work:

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

In this case, we can refer to the specification showing a similar example indicating that this is not an oversight but intended behavior:

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

and another one hinting at the general possibility that a variable may have a non-denotable type:

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


That said, I have to warn about overusing the feature. Besides the readability concerns (you called it yourself an "uncommon usage"), each place where you use it, you are creating a distinct new class (compare to the "double brace initialization"). It’s not like an actual tuple type or unnamed type of other programming languages that would treat all occurrences of the same set of members equally.

Also, instances created like new Object() { String field = s; } consume twice as much memory as needed, as it will not only contain the declared fields, but also the captured values used to initialize the fields. In the new Object() { Long id = p.getId(); String json = toJson(p); } example, you pay for the storage of three references instead of two, as p has been captured. In a non-static context, anonymous inner class also always capture the surrounding this.

这篇关于lambda 表达式中隐含的匿名类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆