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) {}
}
它代表了一种通用服务。请注意,它定义了两个类型注释Started
和Configured
。要正确使用它,您必须以正确的顺序调用它的方法:
- 先
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
暂无评论