Introduction
Google Guava is a library containing a lot of utility methods, providing helper methods around collections, string, caching, etc. This post is about the Preconditions in Google Guava.
What are Preconditions
Quoting the definition of a Precondition from Wikipedia
In computer programming, a precondition is a condition or predicate that must always be true just prior to the execution of some section of code or before an operation in a formal specification. If a precondition is violated, the effect of the section of code becomes undefined and thus may or may not carry out its intended work.
Before executing a routine or a method, we expect certain conditions to be true either about the values passed to the routine or the state of the object having the routine. These expected conditions are called are preconditions. If one or more of these conditions are false, then the method cannot be executed. The most common approach is to abort the execution by throwing an unchecked exception.
Read the differences between checked and unchecked exception post to know the differences between a checked and an unchecked exception.
Example of a Precondition
Precondition violation can either happen with the parameters passed to a method or with the object’s state.
In the below example, there is a debit method that will debit the passed amount from the account id passed. One of the precondition is that the amount value must be greater than 0 (else we will credit free money into the account).
private AccountService accountService;
public void debit(long accountId, int amount) {
accountService.debit(accountId, amount);
}
Thus, before execution of the method, we must ensure that amount > 0.
Another precondition regarding the object’s state is that the accountService must be fully initialised and must not be null.
Preconditions in Google Guava
From the example and the explanations it is clear that there will be a lot of times when we have to validate the arguments passed to the function and validate the state of an object. Google Guava offers useful static methods in the Preconditions class that will help us write one-line statements. These can do the precondition validations and throw an unchecked exception if they are not satisfied.
The Preconditions in Google Guava has the following categories of preconditions checking scenarios
- Checking if a value is not null - checkNotNull
- To check the state of the calling instance - checkState
- To check/validate the arguments passed to a method - checkArgument
- Check if an index is a valid element index into a list/string/an array - checkElementIndex
- Check if an index is a valid position index into a list/string/an array - checkPositionIndex
- Verify if a range (start..end) is a valid sub-range of a list/string/an array - checkPositionIndexes
When the preconditions fails, it throws an exception. We will see them with examples.
Variants of the Preconditions checking utilities in Google Guava
The first three methods (checkNotNull, checkState and checkArgument) have three variants. All the methods take a boolean value as the first parameter. It is the condition that we are checking (precondition). This is the first variant - it just takes the condition. If the condition is violated, the thrown exception will not have any error message.
The second variant takes an Object as the second parameter. When throwing an exception (precondition check violation), it includes the toString() representation of the object as the error message.
The third variety takes a String argument, which is the template for the error message. It has a varargs of Object parameters as the third argument. Each of these would be converted to a String and used as substitutes into the %s formatter of the string template. The template argument only allows %s indicators.
Shown below is the three argument variety parameter types
boolean expression,
String errorMessageTemplate,
Object @Nullable ... errorMessageArgs
Error message arguments
When we pass the error message arguments they would get substituted into the %s formatter of the errorMessageTemplate in order. The first errorMessageArgs will be used for the first %s and the second errorMessageArgs will be used for the second %s and so on.
Calling a varargs method in java would make the argument values passed to be wrapped in an array, and passed (implicitly autoboxed to and from an array). This is how Java supports variable number of arguments. Refer to the Varargs documentation to know more. This can have some impact on the performance if done often.
Hence, Google Guava tries to provide an optimization. The checkNotNull, checkState and checkArgument have a large number of overloads that take a lot of combinations of Object parameters and primitives (like int, long). This will avoid the varargs array creation and the primitive boxing. Their idea is that the overloads must be able to support most of the normal use cases. Only when we have a lot of error message arguments, we will end up calling the one taking a varargs.
checkNotNull
Use it to check if a parameter is not null. Used mostly to validate the parameters to a constructor. It throws a NullPointerException if the condition is violated.
There are three varieties as described earlier.
private AccountService accountService;
public TransactionService(AccountService accountService) {
this.accountService = checkNotNull(accountService, "Account Service cannot be null");
}
The above shows the 2nd variation, which takes a boolean condition and an error message that will be used when throwing a NullPointerException.
If we wanted to make use of a template message for the exception message we could do like,
private Character chacter;
private Location location;
public Processor(Character character, Location location) {
this.location = checkNotNull(location, "Location cannot be null");
this.character = checkNotNull(character, "Character is null for location %s", location);
}
Each %s in the message template will be replaced by the toString of the appropriate error message argument (the first %s with the first parameter, the second %s with the second parameter and so on).
The good thing about checkNotNull is that it returns the reference we are checking for. Hence, we can use the same to assign it to the instance variable and do not need a separate assignment statement like below.
checkNotNull(accountService, "Account Service cannot be null");
this.accountService = accountService;
checkArgument
Use this to throw an IllegalArgumentException when the condition is false. Use this to validate the parameters to a method satisfy an expected condition.
In one of the earlier examples, we saw a debit method that expected the amount to be debited to be greater than zero
public void debit(long accountId, int amount) {
checkArgument(amount > 0, "Amount to be debited must be greater than 0 and cannot be %s", amount);
accountService.debit(accountId, amount);
}
If a caller calls the debit method, passing a zero or a negative value for the amount parameter, it will throw an IllegalArgumentException. Say passing -10 will result in,
java.lang.IllegalArgumentException: Amount to be debited must be greater than 0 and cannot be -10
checkState
This is very similar to the checkArgument. We use this to validate the state of an object and not the arguments to a method. It throws an IllegalStateException if the boolean(condition) is false.
public void arrangeGrid(int n) {
int [][] grid = gridConstructor.buildSquareGrid(n);
checkState(grid.length == grid[0].length, "The constructed grid does not have equal rows and columns. It has %s rows and %s columns", grid.length, grid[0].length);
//Use grid
}
In the above example, we ask the gridConstructor to build a 2D square grid. This implies that the returned grid must have the same number of rows and columns. We use a checkState to check if the grid is in a proper state. If the grid constructor returns a m x n grid rather than an n x n grid, it will throw an IllegalStateException as shown (with random row and column numbers for example).
java.lang.IllegalStateException: The constructed grid does not have equal rows and columns. It has 2 rows and 1 columns
Note: It will stop no one from using checkState to validate the arguments passed to a method. It is just a convention to use checkArguments for validating arguments and use checkState for ensuring that the object is in a correct state.
checkElementIndex
We use this method to ensure an index specifies a valid element index in a list, array or string of length size. A valid element index ranges from zero, inclusive, to size, exclusive.
We do not pass the list, array or string directly, we pass its size. It throws an IndexOutOfBoundsException if the index does not point to a valid element.
We use this for validating the index before indexing into the list, array or string.
int[] arr = new int[10];
public int getElementAtIndex(int index) {
if (index < 0 || index >= arr.length) {
throw new IndexOutOfBoundsException("Not a valid index");
}
return arr[index];
}
The above can be simplified using checkElementIndex
int[] arr = new int[10];
public int getElementAtIndex(int index) {
checkElementIndex(index, arr.length);
return arr[index];
}
Passing an invalid (say 100), it throws
java.lang.IndexOutOfBoundsException: index (100) must be less than size (10)
We can even pass a String to identity the index in the exception message like
checkElementIndex(index, arr.length, "The array index");
This results in an exception with the message The array index (100) must be less than size (10). It will use the message we pass as the exception message’s prefix.
Likewise, you can extend this idea for accessing an element in a list or a string.
checkPositionIndex
This is similar to checkElementIndex, but it considers the valid position to be zero to size inclusive. This ensures that a given index is a valid position in a list, array or string of size, size. A valid position index ranges from zero to size, inclusive.
I don’t understand how this method is useful. It would have made more sense if it had been from one to size inclusive. The answer from this stackoverflow question suggests using this to verify if the position is a valid position to insert a new element. That makes sense. Poor Javadoc I would say.
checkPositionIndexes
This method takes a start, an end and the size. We can use to ensure that the start and end specify valid positions in an array, list or a string. A position index may range from zero to size, inclusive. Thus, this checks the following:
- Start is greater than -1.
- Start is before end or equal to end.
- End is not greater than size.
Again this is checking for the position and hence it considers size as inclusive.
But the Google Guava Preconditions wiki page gives a wrong range notation as [start, end) considering the end as exclusive. But, as per the javadoc and implementation end is inclusive.
int[] arr = new int[10];
//valid ones
checkPositionIndexes(0, 8, arr.length);
checkPositionIndexes(0, 10, arr.length);
checkPositionIndexes(0, 11, arr.length);
The above throws
java.lang.IndexOutOfBoundsException: end index (11) must not be greater than size (10)
I’m not sure on the usefulness of this method. It would have been nice if it had been checking for index rather than position.
A word on performance
As called out in the Javadoc, avoid passing message template and arguments that are expensive to compute. They will always be computed as it does not allow to pass a Supplier lambda to compute it lazily. If you have such arguments, use a conventional if/throw pattern. Thus, we will construct the expensive exception message only when the condition is violated.
Google Guava Custom Exception
Google Guava Preconditions does not allow us to throw custom exceptions. Say, after checking the parameter to a method we would want to throw our business/domain exception. But unfortunately, Google Guava Preconditions does not provide such a method. We can roll our own in a utility class.
public static <X extends RuntimeException> void checkArgument(boolean expression,
Supplier<X> exceptionSupplier) {
if (!expression) {
throw exceptionSupplier.get();
}
}
Usage:
checkArgument(a > 0, () -> new InvalidParameterException(String.format("Expected parameter to be greater than 0. It was %d", a)));
Objects requireNonNull vs Preconditions checkNotNull
The Objects class in Java provides a method called requireNonNull. It takes a reference and throws a NullPointerException if the reference is null. It has an overload that takes an exception message too.
But it does not offer an error message template and lots of overloads with different combinations of parameters like Google Guava’s checkNotNull.
But the good thing about requireNonNull is that it accepts a Supplier of String as the exception message provider. Hence, we can make use of it to construct an expensive exception message. This will overcome the performance problem I mentioned earlier.
private AccountService accountService;
public TransactionService(AccountService accountService) {
this.accountService = requireNonNull(accountService, "Account Service cannot be null");
}
Using a lambda for the message supplier
requireNonNull(s, this::constructMessage);
private String constructMessage() {
String message = "";
// Logic to build the message
return message;
}
this::constructMessage is a method reference of the lambda expression () -> constructMessage().
Conclusion
Preconditions are a set of conditions that must hold true prior to the execution of a method. First, we saw an example to understand about preconditions. Second, we learnt about the Preconditions class in Google Guava and the precondition methods from it. Among them the most commonly used ones are checkNotNull, checkArgument and checkState. Last, we saw how requireNonNull from the JDK differs from the checkNotNull in Google Guava.