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


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

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

List<Person> people = ...;

而且我需要将我的实例的JSON表示形式存储在某个存储库中,为此我需要每个实例的JSON字符串以及每个ID:PersonPersonPerson

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?

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

为了规避这一点,我看到很多人使用某种类,或者,甚至,或者只是:HolderPairTupleAbstractMap.SimpleEntry

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

虽然这对于这个简单的示例来说已经足够好了,但它仍然需要一个泛型类的存在。如果我们需要通过流传播3个值,我认为我们可以使用一个类,等等。使用数组也是一种选择,但它不是类型安全的,除非所有值都属于同一类型。PairTuple3

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

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

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

所以我想知道:

  • 这是否在某个地方被记录下来,还是在JLS中有什么关于此的内容?
  • 如果有的话,这有什么限制?
  • 编写这样的代码真的安全吗?
  • 是否有速记语法,或者这是我们能做的最好的语法?

答案 1

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

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

因此,如果表达式的类型是包含字段 “” 的匿名类型,则不仅访问将起作用,而且 ,除非显式规则禁止它并且一致地,则同样适用于 lambda 表达式。new Object() { String field; }fieldnew Object() { String field; }.fieldCollections.singletonList(new Object() { String field; }).get(0).field

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

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

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

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


答案 2

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

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

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

就像说:

 Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?

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

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

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

    int test = x.y; 

由于是由编译器推断的,也会工作var xint test = x.y;


推荐