lambdas

时间:2017-05-15 19:22:12

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

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

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

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类,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

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

所以我想知道:

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

3 个答案:

答案 0 :(得分:8)

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

答案 1 :(得分:3)

绝对不是答案,而是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 xint test = x.y;也会起作用

答案 2 :(得分:1)

  

这是在某处记录的,还是在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