1. Overview
In this article, we’ll explain how to use the @Contract annotation. Thanks to this annotation, we can define a contract that our methods must match. JetBrains, the brand behind IntelliJ IDEA, introduced the annotation. It allows the text editor to directly troubleshoot potential problems with the calling methods in our code.
2. Setup Maven Dependency
The latest version of the annotations library can be found in the Maven Central Repository. Let’s add the dependency in our pom.xml:
<dependency>
<groupId>org.jetbrains</groupId
<artifactId>annotations</artifactId>
<version>24.0.1</version>
</dependency>
3. The Value Attribute
The @Contract annotation has two attributes: value and pure. The value attribute contains clauses describing the relationship between the methods’ inputs and output. It’s the primary functionality of the annotation and was first introduced in IntelliJ 14.
3.1. Contract Grammar
A contract is a set of causality clauses of the form: “A -> B”. This means that providing A to the method will always give B as a result. For instance, “_ -> null” means that the method returns null for any input value.
The possible constraints on the input are the following:
- _: any value
- null: a null value
- !null: a non-null value
- true: a true boolean value
- false: a false boolean value
The return value supports the same constraints as long as the following ones:
- fail: the method throws an Exception
- new: the method returns a new Object. This Object must be non-null and different from any other Object already present in the heap
- this: the method returns its qualifier value. It can’t be applied to static methods
- param1, param2, ..: the method returns the value of its first (respectively second…) parameter
The new, this, and param1 keywords have only been supported since IntelliJ 2018.2.
Additionally, let’s point out that various constraints can be accumulated as long as they don’t collide.
3.2. Writing Our First Contracts
Let’s now showcase a first example: we’ll create a Person class with a lone name attribute. We’ll add a builder method that will set the name of the Person and return the Person instance:
public class Person {
String name;
@Contract("_ -> this")
Person withName(String name) {
this.name = name;
return this;
}
}
As we can see, whatever the input is, the object itself is returned, so we annotated this method with the valid @Contract(“_ -> this”) annotation.
Let’s now write a method with a more complex signature. This method concatenates two Strings only if the second one is not null and returns null otherwise. Thus, we can give it a contract that states that:
- if the second argument is null, the result is null and doesn’t depend on the value of the first argument
- if the first argument is null, the result is the value of the second argument
- if the second argument isn’t null, then the result isn’t null
In a nutshell, we can annotate our method with the following contract:
@Contract("_, null -> null; null, _ -> param2; _, !null -> !null")
String concatenateOnlyIfSecondArgumentIsNotNull(String head, String tail) {
if (tail == null) {
return null;
}
if (head == null) {
return tail;
}
return head + tail;
}
3.3. Code Inspection
Writing a contract can result in two types of errors:
- the contract isn’t written correctly
- a calling method has some unreachable code
IntelliJ can run code analysis and highlight both kinds of errors. To run the code inspection, we can open the Code menu and then choose Inspect Code:
For instance, let’s write a method that doesn’t do anything. We’ll add a wrong contract that states that it should always fail:
@Contract(" -> fail")
void doNothingWithWrongContract() {}
The Problems tab opens, and the editor warns us about a probable bug in the code:
Those hints that concern contract correctness are useful, but the inspection tool really shines when it helps to remove dead or redundant code. For example, let’s write a piece of code that will call the concatenateOnlyIfSecondArgumentIsNotNull method for two non-null Strings. Then, we’ll want to print the result if it’s not null. Naively, we could write:
String concatenation = concatenateOnlyIfSecondArgumentIsNotNull("1234", "5678");
if (concatenation != null) {
System.out.println(concatenation);
}
Let’s run the code inspection tool. The following message appears:
Condition 'concatenation != null' is always 'true'
Our non-null check is indeed redundant because calling concatenateOnlyIfSecondArgumentIsNotNull on two non-null arguments will always return a non-null result. And IntelliJ was able to spot that thanks to the @Contract annotation!
Last but not least, let’s mention that IntelliJ is also able to carefully look at our libraries’ bytecode and infer contract annotations. For instance, IntelliJ automatically annotates the isEmpty() method with @Contract(“null->true”).
4. The Pure Attribute
The pure attribute specifies that the method has no visible side effects. Thus, if the return value of a pure method isn’t used, we’ll be able to safely remove the call without changing the overall result of our code. Printing to the standard output isn’t considered a visible side effect. On the other hand, methods that don’t seem to have a side effect but could allow changes happening in other threads to be visible in their thread after their execution can’t be marked as pure.
For instance, replacing some characters in a String is a pure operation:
@Contract(pure = true)
String replace(String string, char oldChar, char newChar) {
return string.replace(oldChar, newChar);
}
Furthermore, it is possible to use the pure attribute alongside the value attribute. For instance, we can write a not method that returns the opposite of its boolean parameter with the following contract:
@Contract(value = "true -> false; false -> true", pure = true)
boolean not(boolean input) {
return !input;
}
As of version 2023.1, IntelliJ doesn’t run any relevant code inspection based on the pure attribute. The default value for the pure attribute is false.
6. Conclusion
In this tutorial, we’ve seen how to use the @Contract annotation. Adding this annotation to our toolbox can significantly impact our code quality. In particular, we can easily spot dead code thanks to the value attributes. However, we need to point out that the annotation is just here for description purposes and doesn’t have any consequence on the compiled code.
As always, the code is available over on GitHub.