3个月前 (07-15)  Java系列 |   抢沙发  48 
文章评分 0 次,平均分 0.0

receiver parameter 接收器参数

从Java 8开始,您可以在所有实例方法中声明一个可选的纯语法参数。例如,以下是一个有效的toString方法实现:

public class Example {
  @Override
  public String toString(Example this) {
    return "I'm a receiver parameter example.";
  }
}

让我们验证一下这是一个实际的toString方法。以下代码隐式调用它:

System.out.println(new Example());

将输出:

I'm a receiver parameter example

这种参数形式上称为接收器参数。

类型注解

Java 8为该语言引入了类型注释(https://docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html)JLS第9.7.4节对它们进行了如下描述:类型注释是应用于类型(或类型的任何部分)的注释,其注释接口适用于类型上下文。

例如,静态分析工具可以使用它来警告您可能的编程错误:

class MyList<E> implements @Unmodifiable List<E> {
  ...
}

在该示例中,List使用假设的不可修改注释进行注释。如果我们像下面这样使用MyList,一个假设的工具可能会发出警告或错误:

var list = MyList.of("A", "B", "C");
list.add("D");
//   ^^^
// tool might raise warning or error
// at compile time as
// list modification is disallowed

它们与接收器参数有什么关系?

接收器参数+类型注释

接收器参数的存在是为了注释调用该方法的对象的类型。这里有一个例子:

public class Service {
  @Target(ElementType.TYPE_USE) public @interface Started {}
  
  @Target(ElementType.TYPE_USE) public @interface Configured {}

  public void configure() {}

  public void start(@Configured Service this) {}

  public void stop(@Started Service this) {}
}

它代表了一种通用服务。请注意,它定义了两个类型注释StartedConfigured。要正确使用它,您必须以正确的顺序调用它的方法:

  • configure
  • 下一步 start
  • 用完后就 stop 停下来

假设的静态工具可以确保以正确的方式使用服务。换句话说,该工具将允许以下操作:

var service = new Service();
service.configure();
...
service.start();
...
service.stop();

另一方面,它将禁止以下行为:

var service = new Service();
service.start();
//      ^^^^^
// compilation error:
// must call configure first

请注意,这将在编译时完成

说实话,我不确定一个工具是否可以进行这种静态分析。无论如何,这是我能想到的最好的例子。

精心设计的解决方案

需要记住的是,接收者参数纯粹是一个句法结构。它的唯一目的是允许对类型进行注释。

换句话说,它不是一个形式参数。它没有可以引用的名称。它的“name”是一个保留关键字。对于实例方法,“name”必须是保留关键字this

据我所知,这个解决方案不可能被滥用。

让我们假设接收器是一个形式参数:

public class ToString {
  @Override
  public String toString(ToString this) {
    var self = this;
    var type = self.getClass();
    return "Simple name is: " + type.getSimpleName();
  }
}

看见我们声明了一个局部变量self,并为其分配了“参数”。
现在,让我们删除参数声明并保持方法体不变:

public class ToString {
  @Override
  public String toString() {
    var self = this;
    var type = self.getClass();
    return "Simple name is: " + type.getSimpleName();
  }
}

你能看出这个方法仍然是:

  • 有效的Java代码?
  • 和以前完全一样吗?

相同的签名

再者,接受者纯粹是一种句法结构。如果我们尝试编译以下类:

public class Signature {
  @Override
  public String toString() {
    return "Parameterless";
  }

  @Override
  public String toString(Signature this) {
    return "Receiver";
  }
}

编译失败,因为两种方法具有相同的签名:

$ javac -d /tmp src/main/java/iter3/Signature.java 
src/main/java/iter3/Signature.java:25: error: 
    method toString() is already defined in class Signature
  public String toString(Signature this) {
                ^
1 error

相同的字节码

让我们比较一下上一节中两个ToString类的javac生成的字节码。请注意,我稍微编辑了输出,这样更容易阅读。

这是带有receiver参数的javap输出:

public class iter3.yea.ToString
(...)
  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: aload_0
         1: astore_1
         2: aload_1
         3: invokevirtual #22
         6: astore_2
         7: aload_2
         8: invokevirtual #26
        11: invokedynamic #32,  0
        16: areturn
      LineNumberTable:
        line 25: 0
        line 26: 2
        line 27: 7

这是没有接收器参数的输出:

public class iter3.nay.ToString
(...)
  public java.lang.String toString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: aload_0
         1: astore_1
         2: aload_1
         3: invokevirtual #22
         6: astore_2
         7: aload_2
         8: invokevirtual #26
        11: invokedynamic #32,  0
        16: areturn
      LineNumberTable:
        line 25: 0
        line 26: 2
        line 27: 7

它们是一样的。

当然,如果我们用非源保留注释来注释接收器参数,则以下内容将添加到输出的底部:

RuntimeVisibleTypeAnnotations:
      0: #42(): METHOD_RECEIVER
        iter4.yea.Anno$Runtime
    RuntimeInvisibleTypeAnnotations:
      0: #44(): METHOD_RECEIVER
        iter4.yea.Anno$Class

但方法主体保持不变。

它必须在形式参数之前声明

您可以声明接收器参数以及形式参数:

public class Arity {
  public void m0(Arity this) {}
  public void m1(Arity this, int a) {}
  public void m2(Arity this, int a, int b) {}
}

唯一的问题是它必须是第一个。如果你试图编译以下(错误的)类:

public class Wrong {
  public void m2(int a, Wrong this, int b) {}
}

编译失败,出现以下错误:

$ javac -d /tmp src/main/java/iter5/Wrong.java
src/main/java/iter5/Wrong.java:9: error: 
    as of release 8, 
    'this' is allowed as the parameter name
    for the receiver type only
  public void m2(int a, Wrong this, int b) {}
                              ^
  which has to be the first parameter, 
  and cannot be a lambda parameter
1 error

请记住,由于类型注释,接收器参数被引入到语言中。

因此,即使它的正式名称是接收器参数,您也只能用类型注释对其进行注释。换句话说,您不能使用专门针对参数的注释对其进行注释。

以下类无法编译:

public class TypeUseOnly {
  @Target(ElementType.PARAMETER)
  public @interface Param {}

  @Target(ElementType.TYPE_USE)
  public @interface Type {}

  @Override
  public String toString(@Param @Type TypeUseOnly this) {
    // compilation error ^^^^^^
    return "Only TYPE_USE annotations";
  }
}

编译失败,并显示以下消息:

$ javac -d /tmp src/main/java/iter5/TypeUseOnly.java 
src/main/java/iter5/TypeUseOnly.java:29: error: 
    annotation @Param not applicable in this type context
  public String toString(@Param @Type TypeUseOnly this) {
                         ^
1 error

要修复编译错误,我们需要:

  • 删除@Param注释
  • 将TYPE_USE添加到其@Target

内部类的构造函数

最后,您还可以在内部类的构造函数中声明接收器参数。为了清楚起见,我所说的内部类是指非静态嵌套类。
在内部类的构造函数中,receiver参数表示直接封闭类型的类型。与实例方法相比,语法略有不同。

以下示例对此进行了说明:

public class Outer {  
  class Inner {
    final Outer outer;

    Inner(Outer Outer.this) {
      outer = Outer.this;
    }
  }
}

参数的“名称”是Outer.this。换句话说,它是封闭类的名称,后跟.this
请注意,语法再次使其不可能被误用(据我所知)。前面的示例等效于以下内容:

public class Outer {  
  class Inner {
    final Outer outer;

    Inner() {
      outer = Outer.this;
    }
  }
}

原文地址:https://www.objectos.com.br/blog/objectos-weekly-008-the-java-receiver-parameter.html

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2991.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册