Objectos Weekly #008: The Java receiver parameter
Welcome to Objectos Weekly issue #008.
I hope you had a great Holiday season.
In the first issue of 2023 we will discuss Java's receiver parameter. A feature of the language I did not know about until very recently.
Let's begin.
The Java receiver parameter
Since Java 8 you can declare an optional and purely syntactic parameter in all instance methods.
For example, the following is a valid toString
method implementation:
public class Example {
@Override
public String toString(Example this) {
return "I'm a receiver parameter example.";
}
}
Let's verify that this is an actual toString
method.
The following code invokes it implicitly:
System.out.println(new Example());
It prints:
I'm a receiver parameter example
This kind of parameter is formally called the receiver parameter.
Type annotations
Java 8 introduced type annotations to the language. Section 9.7.4 of the JLS says the following about them:
A type annotation is an annotation that applies to a type (or any part of a type), and whose annotation interface is applicable in type contexts.
It can be used by static analysis tools to, for example, warn you of possible programming mistakes:
class MyList<E> implements @Unmodifiable List<E> {
...
}
In the example, the List
interface is annotated with a hypothetical Unmodifiable
annotation.
If we use the MyList
like the following, a hypothetical tool might raise a warning or error:
var list = MyList.of("A", "B", "C");
list.add("D");
// ^^^
// tool might raise warning or error
// at compile time as
// list modification is disallowed
And what is their relation to receiver parameters?
The receiver parameter + type annotations
The receiver parameter exists so that one can annotate the type of the object for which the method is invoked. Here's an example:
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) {}
}
It represents a generic service.
Note that it defines two type annotations Started
and Configured
.
To properly use it, you must invoke its methods in the correct order:
-
configure
first; -
start
next; and -
stop
after you are done using it.
A hypothetical static tool could ensure the Service
is used in the correct manner.
In other words, the tool would allow the following:
var service = new Service();
service.configure();
...
service.start();
...
service.stop();
On the other hand, it would disallow the following:
var service = new Service();
service.start();
// ^^^^^
// compilation error:
// must call configure first
Note that this would be done at compile time.
To be honest, I am not sure if a tool could do this kind of static analysis. In any case, this is the best example I could come up with.
A very well-designed solution
The thing to keep in mind is that the receiver parameter is purely a syntactic construct. Its sole purpose is to allow the type to be annotated.
In other words, it is not a formal parameter.
It does not have a name that you can refer to.
Its "name" is a reserved keyword.
In the case of instance methods, the "name" must be the reserved keyword this
.
This solution makes it impossible to misuse it (as far as I can tell).
A let's pretend game
Let's pretend the receiver is a formal parameter:
public class ToString {
@Override
public String toString(ToString this) {
var self = this;
var type = self.getClass();
return "Simple name is: " + type.getSimpleName();
}
}
See?
We have declared a local variable self
and assigned our "parameter" to it.
Now let's remove the parameter declaration and keep the method body the same:
public class ToString {
@Override
public String toString() {
var self = this;
var type = self.getClass();
return "Simple name is: " + type.getSimpleName();
}
}
Can you see that the method is still both:
-
valid Java code? and
-
exactly the same as before?
Cool, isn't it?
Same signature
Again, receiver is purely a syntactic construct. If we try to compile the following class:
public class Signature {
@Override
public String toString() {
return "Parameterless";
}
@Override
public String toString(Signature this) {
return "Receiver";
}
}
Compilations fails as both methods have the same signature:
$ 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
Same bytecode
Let's compare the bytecode generated by javac
of the two ToString
classes of the previous section.
Please note I've edited the output slightly so it is easier to read.
This is the javap
output for the one with the receiver parameter:
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
And here's the output for the one without the receiver parameter:
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
They are the same.
Of course, if we were to annotate the receiver parameter with a non source-retention annotation, the following gets added to the bottom of the output:
RuntimeVisibleTypeAnnotations:
0: #42(): METHOD_RECEIVER
iter4.yea.Anno$Runtime
RuntimeInvisibleTypeAnnotations:
0: #44(): METHOD_RECEIVER
iter4.yea.Anno$Class
But the method body remains the same.
It must be declared before the formal parameters
You can declare a receiver parameter along with formal parameters:
public class Arity {
public void m0(Arity this) {}
public void m1(Arity this, int a) {}
public void m2(Arity this, int a, int b) {}
}
The only catch is that it must be the first one. If you try to compile the following (wrong) class:
public class Wrong {
public void m2(int a, Wrong this, int b) {}
}
Compilation fails with the following error:
$ 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
Type annotations only
Remember that the receiver parameter was introduced to the language because of type annotations.
So, even though its formal name is receiver parameter, you can only annotate it with type annotations. In other words, you cannot annotate it with an annotation that targets parameters exclusively.
The following class does not compile:
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";
}
}
Compilation fails with the following message:
$ 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
To fix the compilation error we need to either:
-
remove the
@Param
annotation; or -
add
TYPE_USE
to its@Target
.
Inner classes' constructors
As a final note, you can also declare a receiver parameter in the constructor of a inner class. Just to be clear, by inner class I mean a non-static nested class.
In the constructor of a inner class the receiver parameter denotes the type of the immediately enclosing type. The syntax is slightly different when compared to instance methods.
The following example illustrates it:
public class Outer {
class Inner {
final Outer outer;
Inner(Outer Outer.this) {
outer = Outer.this;
}
}
}
The "name" of the parameter is Outer.this
.
In other words, it is the name of the enclosing class followed by .this
.
Note that the syntax, once again, makes it impossible to misuse it (as far as I can tell). The previous example is equivalent to the following:
public class Outer {
class Inner {
final Outer outer;
Inner() {
outer = Outer.this;
}
}
}
Note, once again, that the constructor without the receiver parameter is:
-
valid Java code; and
-
equivalent to the constructor declaring a receiver parameter.
Cool, isn't it?
Let's work together
Do you have a legacy Java application that needs some looking at? Perhaps I can help. Let's get in touch.
You can find my contacts on this page. All work will be provided via Objectos Software LTDA based in São Paulo, Brazil.
Until the next issue of Objectos Weekly
So that's it for today. I hope you enjoyed reading.
The source code of all of the examples are in this GitHub repository.
Please send me an e-mail if you have comments, questions or corrections regarding this post.