lambdas中隐含的匿名类型

Fed*_*ner 21 java lambda anonymous-types language-lawyer java-8

这个问题中,用户@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);
Run Code Online (Sandbox Code Playgroud)

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


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

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

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


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

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

List<Person> people = ...;
Run Code Online (Sandbox Code Playgroud)

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

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?
Run Code Online (Sandbox Code Playgroud)

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

为了避免这种情况,我看到很多人使用某种HolderPair,或者甚至Tuple,或者只是AbstractMap.SimpleEntry:

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

虽然这对于这个简单的例子已经足够好了,但仍然需要存在泛型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));
Run Code Online (Sandbox Code Playgroud)

真是太神奇了!现在我们可以根据需要拥有尽可能多的字段,同时还可以保持类型安全.

在测试时,我无法在单独的代码行中使用隐含类型.如果我修改我的原始代码如下:

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);
Run Code Online (Sandbox Code Playgroud)

我收到编译错误:

Error: java: cannot find symbol
  symbol:   variable field
  location: variable anonymous of type java.lang.Object
Run Code Online (Sandbox Code Playgroud)

这是可以预料的,因为没有命名的成员fieldObject类.

所以我想知道:

  • 这是在某处记录的,还是在JLS中有关于此的内容?
  • 这有什么限制,如果有的话?
  • 编写这样的代码实际上是否安全
  • 是否有简写语法,或者这是我们能做的最好的?

Hol*_*ger 9

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; String s = "blah"; };
obj.i += 10;
System.out.println(obj.s);
Run Code Online (Sandbox Code Playgroud)

同样,我们可以使您的问题的示例工作:

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

在这种情况下,我们可以参考显示类似示例的规范,该示例表明这不是一种疏忽而是预期的行为:

var d = new Object() {};  // d has the type of the anonymous class
Run Code Online (Sandbox Code Playgroud)

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

var e = (CharSequence & Comparable<String>) "x";
                          // e has type CharSequence & Comparable<String>
Run Code Online (Sandbox Code Playgroud)

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

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

  • 只要用法不需要​​通过名称引用它们.通用类型变量有助于推断它们,但它需要一个像lambda表达式这样的构造,它允许创建(参数)变量而不指定它们的类型名称.`Type :: name`表单的方法引用不允许引用匿名类型的成员,因为它需要名称,但是,`new Object(){void foo(){}} :: foo`会工作. (3认同)