【问题标题】:Implied anonymous types inside lambdaslambda 中的隐含匿名类型
【发布时间】:2017-10-14 16:46:01
【问题描述】:

this question 中,用户@Holger 提供了an answer,它显示了匿名类的不常见用法,我不知道。

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

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方法链传播的。


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

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

List<Person> people = ...;

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

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<Long, String>(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<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 类中没有名为 field 的成员。

所以我想知道:

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

【问题讨论】:

  • 方法对应效果见here。在这里,泛型类型推断公开了匿名类型,您可以在其中发现字段。在上一个示例中,您专门使用了 java.lang.Object,它没有此类字段。
  • 虽然这不会在 Eclipse 上编译:\
  • @SotiriosDelimanolis 这实际上很棒,但我不认为它们完全相同。当直接调用一个方法时,你有点像chain this - 类型就在那里,你只是无法表达它。在 lambda 创建中,类型将在各个阶段传递。我发现不同
  • 据我所知,没有地方说这应该有效,实际上是相反的;没有地方说表达式的类型应该回退到基类,如果它是匿名的。因此,new Object() { void foo() {} }.foo() 起作用的原因与其他涉及 lambda 的示例起作用的原因相同。但正如所说,“据我所知”,一个反例指向 JLS 中的一个地方说“不不”可能证明我错了。
  • @SotiriosDelimanolis @Eugene 它在 Neon 中为我编译。它甚至显示of 的推断类型:&lt;Main(){}&gt; Optional&lt;Main(){}&gt; java.util.Optional.of(Main(){} value)

标签: java lambda java-8 language-lawyer anonymous-types


【解决方案1】:

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

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

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>

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

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

【讨论】:

  • 感谢您的回答,霍尔格。是的,我已经看到了您提到的捕获值。它们是 lambda 参数的 val$p 和封闭类的 this$0
  • 所以,简而言之,您是说匿名类型在任何地方都是可能的,只要它们由泛型类型变量表示。这是正确的吗?
  • 只要在使用上不需要通过名称来引用它们。泛型类型变量有助于推断它们,但它需要一个像 lambda 表达式这样的结构,它允许创建(参数)变量而不指定它们的类型名称。 Type::name 表单的方法引用不允许引用匿名类型的成员,因为它需要名称,但是,new Object() { void foo() {} }::foo 可以工作。
【解决方案2】:

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

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

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

这就像说:

 Stream<TypeICanUseButTypeICantName> // Stream<YouKnowWho>?

它在您的最后一个示例中不起作用,因为您显然已经告诉编译器类型为:Optional&lt;Object&gt; optional 从而破坏了anonymous type 推理。

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

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

    int test = x.y; 

由于var x 由编译器推断,int test = x.y; 也可以工作

【讨论】:

  • C++ 有 auto 可以推断类型,甚至是 lambda 的匿名类型。 Project Amber 提议在 Java 中添加一个类似的关键字。也许将来我们可以有类似var = Optional.of(AnonymousType) 的东西,其中var 将被推断为Optional&lt;AnonymousType&gt;
  • 类型推断并非 lambda 独有。请参阅 Holger 的示例,singletonList()
  • @JornVernee 我猜未来就在眼前
  • @Eugene 未来仍处于抢先体验阶段;)
  • @shmosel 我真的不知道怎么说...我指的是map(anonymous -&gt; anonymous.field)anonymous 将由编译器推断,你甚至不能在 lambda 表达式之外声明类似的东西,即对匿名内部类的引用
【解决方案3】:

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

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

AND@Holger 已回答的其余问题。

编辑

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

很抱歉,由于我的英语不好,我再也找不到 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$1
     4: dup
     5: aload_0
     6: invokespecial #3     // Method Main$1."<init>":(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

【讨论】:

  • 但是在您的 chain 示例中,您只是返回接收到的参数,而 Stream.map 的签名是 &lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper)。我的意思是,您不是返回参数,而是返回一个新流,其中应用了一个函数。无论如何,我理解你的意思,这是常见的行为。
  • @FedericoPeraltaSchaffner 嗨,Stream&lt;T&gt; 的类型相同,因为 T 的类型是匿名类而不是 Object 类。
  • 所以也许我们可以得出结论,只要匿名类型由泛型类型变量表示,这种构造是可能的?
猜你喜欢
  • 2013-08-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-28
  • 1970-01-01
  • 2014-05-03
  • 1970-01-01
相关资源
最近更新 更多