Yet 4 More Techniques for Writing Better Java
Want to write better Java? Here's how to improve your coding with argument validation, in-depth knowledge of the Object class, jshell, and reading good code.
Join the DZone community and get the full member experience.
Join For FreeProgramming skills are like many other skills in life and require constant improvement: If we are not going forward, we are going backward. Standing still is not an option. In this third installation of the 4 Techniques for Writing Better Java series, we cover four important topics: (1) Validating arguments using methods provided by the standard Java library, (2) understanding the important Object
class, (3) experimenting and learning by playing with jshell, and (4) finding and reading the most well-written code we can, in both books and in the source code of Java itself.
Some of these techniques are purely programming techniques that can help in a specific pinch while others focus on the tools and environment surrounding the Java ecosystem. Regardless of the individual nature of each technique, when applied with diligence and sound judgment, each can help improve the Java code written by developers, both novice and expert.
1. Validate Arguments With Standard Methods
Validating input is an inevitable part of any program. For example, if we pass an object as an argument to a method and expect to call a method on that object, we must first validate that the supplied object is not null. Additionally, we may pass this object to another method (possibly one that we did not develop) and this second method may expect its arguments to not be null, causing an error if a null argument is passed.
This situation gets more involved when the execution of an invalid statement may cause an error in a different point in the execution of a program than where an object was supplied. Even worse, the error may occur with no evidence of the cause anywhere in the stack trace. For example, if we create an immutable class that stores an object and a method that uses this object is called in another thread, a NullPointerException
(NPE) may be thrown in the calling thread, with no sign as to where the assignment of the object occurred. An example of such a class is depicted below.
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void setRpm(int rpm) {
engine.setRpm(rpm);
}
}
Since the error may occur in a disparate location from the initial assignment, it is imperative that we validate the arguments at the assignment site and fail-fast if invalid arguments are supplied. To do this, we can add a null check to ensure that if a null argument is passed to the assignment location, it is immediately rejected and results in an NPE being thrown:
public class Car {
private final Engine engine;
public Car(Engine engine) {
if (engine == null) {
throw new NullPointerException("Engine cannot be null");
}
this.engine = engine;
}
public void setRpm(int rpm) {
engine.setRpm(rpm);
}
}
If the supplied argument is not null, no exception is thrown and the class functions as normal. Although this is a simple solution to the problem, its deficiencies are highlighted when more than one argument must be validated. For example, if we supply an Engine
and a Transmission
object to the Car
constructor, our class grows to the following:
public class Car {
private final Engine engine;
private final Transmission transmission;
public Car(Engine engine, Transmission transmission) {
if (engine == null) {
throw new NullPointerException("Engine cannot be null");
}
if (transmission == null) {
throw new NullPointerException("Transmission cannot be null");
}
this.engine = engine;
this.transmission = transmission;
}
public void setRpm(int rpm) {
engine.setRpm(rpm);
}
public void setGear(int gear) {
transmission.setGear(gear);
}
}
As the number of null checks grows, the clarity of our code begins to diminish. This is such as a common problem that as of Java Development Kit (JDK) 7, a new class was added (called Objects
) which includes the requireNonNull
method that allows developers to check that an object is not null. If the object supplied to the requireNonNull
method is null, an NPE is thrown. This method also returns the supplied object which allows for compact assignments to be made: If the supplied object is null, an NPE is thrown, but if the supplied object is not null, it is returned and can be assigned to a variable. Using this JDK method, we can reduce the null checking logic of the Car
constructor to the following:
public class Car {
private final Engine engine;
private final Transmission transmission;
public Car(Engine engine, Transmission transmission) {
this.engine = Objects.requireNonNull(engine, "Engine cannot be null");
this.transmission = Objects.requireNonNull(transmission, "Transmission cannot be null");
}
public void setRpm(int rpm) {
engine.setRpm(rpm);
}
public void setGear(int gear) {
transmission.setGear(gear);
}
}
Using this standard method, the intent of our code is much clearer: Store the supplied Engine
and Transmission
objects if they are not null. The requireNonNull
method is also flexible enough to allow for a customized message to be supplied and has a close cousin, requireNonNullElse
(available in JDK 9), that allows for a default value to be supplied. The requireNonNullElse
will return the supplied default value in the event that the supplied object is null, rather than throwing an NPE. In total, there are three overloadings of the requireNotNull
method and two overloadings of the requireNotNullElse
method:
requireNonNull(T obj)
: Throws an NPE if the supplied object is nullrequireNonNull(T obj, String message)
: Throws an NPE with the supplied message if the supplied argument is nullrequireNonNull(T obj, Supplier<String> messageSupplier)
: Throws an NPE with a message generated by themessageSupplier
argument if the supplied object is null; the message is generated at the time the NPE is thrown; this method should be used when the message for the exception can be costly to create (and should therefore only be created if an NPE is thrown)requireNonNullElse(T obj, T defaultObj)
: Returns the supplied object if it is not null or returns the supplied default value otherwiserequireNonNullElseGet(T obj, Supplier<? extends T> supplier)
: Returns the supplied object if it is not null or generates a default value and returns it otherwise; the default value is generated only if the supplied object is null; this method should be used when the default value may be costly to create (and should only be generated when the supplied object is null)
JDK 7 also includes two methods, isNull
and nonNull
, that are close counterparts to the above methods but instead return true
and false
, respectively, if the object supplied to them are null. These boolean-based methods should be used whenever an NPE is not desired and some custom exception or handling logic should be used. Note that the customary behavior in the Java environment is to throw an NPE when a supplied argument is null (rather than an IllegalArgumentException
or some custom exception) and throwing an exception of a different type should be done with proper diligence and caution.
With the release of JDK 9, three more methods were introduced that allow developers to check that a supplied index or set of indices are within bounds:
checkFromIndexSize(int fromIndex, int size, int length)
: Throws anIndexOutOfBoundsException
(IOOBE) if the sum of the suppliedfromIndex
(inclusive) andsize
(exclusive) is within the range of0
tolength
(exclusive) or returnsfromIndex
if valid; this method is useful for validating that accessing n elements (size
), starting inclusively at thefromIndex
, is valid for a collection or array with a givenlength
checkFromToIndex(int fromIndex, int toIndex, int length)
: Throws an IOOBE if the suppliedfromIndex
(inclusive) to the suppliedtoIndex
(exclusive) is within the range0
tolength
(exclusive) or returnsfromIndex
if valid; this method is useful for validating that some range, inclusively fromfromIndex
exclusively totoIndex
, is valid for a collection or array of a givenlength
checkIndex(int index, int length)
: Throws an IOOBE if the suppliedindex
is less than0
or greater than or equal to the suppliedlength
, or returnsindex
if valid; this message is useful for validating that a givenindex
is valid for a collection or array of a givenlength
We can use these index checking methods to ensure that a supplied index is correct for a given collection of objects, as depicted in the listing below:
public class Garage {
private final List<Car> cars = new ArrayList<>();
public void addCar(Car car) {
cars.add(car);
}
public Car getCar(int index) {
int validIndex = Objects.checkIndex(index, cars.size());
return cars.get(validIndex);
}
}
Unfortunately, the index checking methods do not allow for a custom exception or even a custom exception message to be supplied. In some cases, the low abstraction level of an IOOBE is ill-suited for an application and a more high-level exception is needed. For example, depending on the context, we may not want clients of the Garage
class to know we are storing the Car
objects in a list (as opposed to a database or some remote service) and thus, throwing an IOOBE may reveal too much information or tie our interface too closely with its implementation. Instead, a NoSuchElementException
may be more appropriate (or a custom exception if needed).
Taking these shortcomings into account, we can devise the following rule with regards to null checking and index checking of method (including constructor) arguments:
Use the JDK standard null checking and index checking methods when possible. Bare in mind that the abstraction level of the exceptions thrown by the standard index checking methods may be inappropriate.
2. Get to Know the Object Class
One of the most common first-day lessons for object-orientation in Java is the default superclass for all classes: Object
. This class constitutes the root of the entire Java type hierarchy and includes methods that are common among all Java types, both user-defined and those contained in the standard Java library. While these basics are nearly universal among the repertoire of Java developers, many of the details fall through the cracks. As a matter of fact, many of the details go unlearned, even for intermediate and advanced Java developers.
In total, the Object
class has eleven methods that are inherited by all classes in the Java environment. While some of these methods, such as finalize
, are deprecated and should never be overridden or explicitly called, other such as equals
and hashCode
are essential to daily programming in Java. While the depth of intricacies of the Object
class are outside the scope of this article, we will focus on two of the most important methods in this ultimate class: equals
and hashCode
.
equals
The equals
method is a simple method in theory and a much more nuanced one in practice. This method allows for equality comparison between two objects, returning true
if the objects are equal and false
otherwise. Although this concept may sound simple, it is actually far from it. For example, can two objects of a different type be equal? Can two objects that are stored at different locations in memory (i.e. are different instances) be equal if their state is equal? How does equality effect other methods and characteristics of the Object
class?
By default, the equals method returns true
if two instances are equal. This is obvious if we look at the JDK 9 implementation of the Object#equals
method:
public boolean equals(Object obj) {
return (this == obj);
}
While this definition is strikingly simple, it hides some important characteristics of the equals method. In general, the entire Java environments makes five basic assumptions about how the equals method is implemented for any class, including user-defined classes, and these assumptions are recorded in the documentation for the Object
class. These assumptions, as quoted from the aforementioned documentation, are as follows:
- It is reflexive: for any non-null reference value
x
,x.equals(x)
should returntrue
. - It is symmetric: for any non-null reference values
x
andy
,x.equals(y)
should returntrue
if and only ify.equals(x)
returnstrue
. - It is transitive: for any non-null reference values
x
,y
, andz
, ifx.equals(y)
returnstrue
andy.equals(z)
returnstrue
, thenx.equals(z)
should returntrue
. - It is consistent: for any non-null reference values
x
andy
, multiple invocations ofx.equals(y)
consistently returntrue
or consistently returnfalse
, provided no information used in equals comparisons on the objects is modified. - For any non-null reference value
x
,x.equals(null)
should returnfalse
.
It should be noted that these restrictions are compounded with the purpose of equals: Return true
if two objects are considered equal or false
otherwise. In most cases, the default equals implementation will suffice, but there may be cases in which a more fine-tuned implementation is needed. For example, if we create an immutable class, two objects of this class should be equal if all of their fields are equal. In practice, overriding the equals method results in the following implementation structure:
- Check if the supplied object is this object
- Check if the supplied object has the same type as this object
- Check if the fields of the supplied object are the same as the fields of this object
For example, if we wanted to create an immutable Exam
class that records the grade received by a student on a specific exam, we can define the class, along with its equals
method, as follows:
public class Exam {
private final int id;
private final int score;
public Exam(int id, int score) {
this.id = id;
this.score = score;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
else if (!(o instanceof Exam)) {
return false;
}
else {
Exam other = (Exam) o;
return other.id == id && other.score == score;
}
}
}
This implementation ensures that the following results are obtained:
Exam x = new Exam(1, 97);
Exam y = new Exam(1, 97);
Exam z = new Exam(1, 97);
Exam different = new Exam(5, 89);
// Difference comparison
System.out.println(x.equals(different)); // False
// Reflexive
System.out.println(x.equals(x)); // True
// Symmetric
System.out.println(x.equals(y)); // True
System.out.println(y.equals(x)); // True
// Transitive
System.out.println(x.equals(y)); // True
System.out.println(y.equals(z)); // True
System.out.println(x.equals(z)); // True
// Consistent
System.out.println(x.equals(y)); // True
System.out.println(x.equals(y)); // True
// Null
System.out.println(x.equals(null)); // False
There is more to the equals
method than first meets the eye and intermediate and advanced Java developers should become familiar with this important method by reading its official documentation. An in-depth look at the equals
method, including many of the idiosyncrasies associated with custom implementations, can be found in Item 10 (pp. 37-49) of Effective Java, 3rd Edition by Joshua Bloch.
hashCode
The second pair in the Object
tandem is the hashCode
method, which generates an integer hash code that corresponds to an object. This hash code is used as the hash digest when making insertions in hash-based data structures, such as HashMap
. Just like the equals
method, the entirety of the Java environment makes assumptions about the behavior of the hashCode
method that are not reflected programmatically:
- Hash codes for an object must be constant while the data that is factored into the hash code remains unchanged; generally, this means that the hash code for an object remains constant if the state of the object is unchanged
- Hash codes must be equal for objects that are equal according to the
equals
method - Hash codes for two objects are not required to be unequal if the two objects are unequal according to their
equals
methods, although algorithms and data structures that rely on hash codes usually perform better when unequal objects result in unequal hash codes
Hash codes are usually some ordered summation of the values of each field in an object. This ordering is usually achieved through multiplication of each component in the summation. As explained in Effective Java, 3rd Edition (pp. 52), the multiplicative factor 31 is selected:
The number 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, because multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance on some architectures:
31 * i = (i << 5) - i
.
For example, the hash code for some arbitrary set of fields is usually computed in practice using the following series of calculations:
int result = field1.hash();
result = (31 * result) + field2.hash();
result = (31 * result) + field3.hash();
// ...
return result;
In code, this results hashCode
definitions that resemble the following:
public class Exam {
private final int id;
private final int score;
// ...existing class definition...
@Override
public int hashCode() {
return (31 * id) + score;
}
}
In order to reduce the tediousness of implementing the hashCode
method for a class with numerous fields, the Objects
class includes a static method hash
that allows for an arbitrary number of values to be hashed together:
public class Exam {
private final int id;
private final int score;
// ...existing class definition...
@Override
public int hashCode() {
return Objects.hash(id, score);
}
}
While the Objects#hash
method reduces the clutter of the hashCode
method and improves its readability, it does not come with a price: Since the hash
method uses variable arguments, the Java Virtual Machine (JVM) creates an array to hold its arguments and requires boxing of arguments that are of a primitive type. All things considered, the hash method is a good default when overriding the the hashCode
method, but if better performance is required, the manual multiply-and-sum operation should be implemented or the hash code should be cached. Lastly, due to the conjoined constraints of the equals
and hashCode
methods (i.e. hash codes much be equal if the equals
method returns true
), whenever one of the methods is overridden, the other should be overridden as well.
While this section covers many of the major aspects of the Object
class, it only scratches the surface on the nuance of this important class. For more information, consult the official Object class documentation. In summary, the following rule should be adhered to:
Get to know theObject
class: Every class inherits from it and the Java environment makes serious expectations for its methods. Be sure to follow these rules when overridding eitherequals
orhashCode
and be sure never to override one without overriding the other.
3. Experiment With jshell
There are countless times when a developer gets curious about how a statement or class will work at runtime in his or her application and does not want to try it in the real application. For example, if we loop with these indices, how many times will this loop get executed? Or, if we use this conditional, will this logic ever be executed? Sometimes, the curiosity may be more general, such as wondering what the return value of a single statement will be or questioning how a language feature looks in practice (i.e. what happens if I supply a null value to Objects#requireNonNull
?).
Accompanying many of the other important features in JDK 9, a Read-Evaluate-Print Loop (REPL) tool called jshell was introduced. jshell is a command-line tool that allows for Java statements to be executed and evaluated, displaying the result of the statement. To start jshell (assuming that the bin/
directory for a JDK 9 installation is on the operating system path), simply execute the jshell
command as follows (the version number will depend on the version of the JDK installed on the machine):
$ jshell
| Welcome to JShell -- Version 9.0.4
| For an introduction type: /help intro
jshell>
Once the jshell>
prompt is displayed, we can type in any executable Java statement and see its evaluation. Note that single statements do not require trailing semicolons, although they can be included if desired. For example, we can see how Java would sum 4 and 5 with the following command in jshell:
jshell> 4 + 5
$4 ==> 9
While this may be simple, it is important to grasp that we are able to execute Java code without creating an entirely new project and writing a boilerplate public static void main
method. Whatsmore, we can also execute relatively complex logic with jshell, as seen below.
jshell> public class Car {
...> private final String name;
...> public Car(String name) {
...> this.name = name;
...> }
...> public String getName() {
...> return name;
...> }
...> }
| created class Car
jshell> Car myCar = new Car("Ferrari 488 Spider");
myCar ==> Car@5e5792a0
jshell> myCar.getName()
$3 ==> "Ferrari 488 Spider"
Being that jshell a swift alternative to creating an entire project, it no longer becomes cumbersome to evaluate small pieces of code (ones that would take seconds to write and minutes to create an runnable project for). For example, if we were curious to see how the Objects#requireNonNull
methods would respond to various arguments (technique 1), we could try them out and see the actual results using jshell, as depicted below.
jshell> Objects.requireNonNull(null, "This argument cannot be null")
| java.lang.NullPointerException thrown: This argument cannot be null
| at Objects.requireNonNull (Objects.java:246)
| at (#5:1)
jshell> Objects.requireNonNull(new Object(), "This argument cannot be null")
$6 ==> java.lang.Object@210366b4
It is important to note that although we can execute single statements without trailing semicolons, statements with a scope (i.e. those surrounded by curly braces, single-line conditional bodies, etc.) must include trailing semicolons. For example, leaving off a trailing semicolon in the middle of a class definition causes a syntax error in jshell:
jshell> public class Foo {
...> private final String bar
...> }
| Error:
| ';' expected
| private final String bar
|
Although jshell is much more capable than the examples in this section give it credit for, a full exposé of its features is beyond the scope of this article. The curious reader can find a wealth of information in the Java Shell User's Guide. Notwithstanding the simplicity of the examples in this section, the ease of use and power of jshell provide us with a handy technique for improving how we develop applications in Java:
Use jshell to evaulate the runtime behavior of a Java statement or groups of statements. Don't be shy: Taking a few seconds to see how a statement is actually evaluated can save value minutes or hours.
4. Read Well-Written Code
One of the best ways to become a skilled craftsman is to watch a more experienced craftsman at work. For example, in order to be a better painter, an aspiring artist can watch a professional painter in a video (i.e. The Joy of Painting by Bob Ross) or even study existing paintings by masters such as Rembrandt or Monet. Likewise, a hockey player can study video on how the best National Hockey League (NHL) players skate or still handle during a game or hire an experienced player as a coach.
Programming is no different. It can be easy to forget that programming is a skill that must be honed and one of the best ways to improve this skill is to look towards the historically best programmers. In the realm of Java, this means looking into how the original designers of the language use the language. For example, if we wish to know how to write clean, simple code, we can look at the JDK source code and find out how the inventors of Java write Java code. (Note that the JDK source code can be found lib/src.zip
under a standard JDK installation in Windows or downloaded from OpenJDK on any operating system.)
We can also garner a great deal of information about how a particular class works by looking at its implementation. For example, suppose we are concerned about how a Collection
removes an element using the AbstractCollection#remove(Object)
method. Instead of guessing about the implementation, we can go straight to the source and see the implementation (as depicted below).
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
By simply looking at the source code for this method, we can see that if a null Object
is passed to this method, the first null found in the Collection
(using the Iterator
for the Collection
) is removed. Otherwise, the equals
method is used to find a matching element and if present, it is removed from the Collection
. If any alterations are made to the Collection
, true
is returned; otherwise, false
is returned. While we can understand what the method is doing from its associated JavaDocs, we can see how it is accomplished by looking directly at the source for the method.
Apart from seeing how specific methods work, we can also see how some of the most experienced Java developers write their code. For example, we can see how to effectively concatenate strings by looking at the AbstractCollection#toString
method:
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
Many new Java developers may have used simple string concatenation, but the developer of the AbstractCollection#toString
(who happens to be one of the original Java forerunners) decided to use a StringBuilder
. This should at least beg the question: Why? Is there something that this developer knows that we do not? (It is likely since it's not too common to find a bug or typo in the JDK source code.)
It should be noted, however, that just because code is written a certain way in the JDK does not necessarily mean that it is written that way in most Java applications. Many times, idioms are used by a wide array of Java developers but are not present in the JDK (some JDK code has been written long ago). Likewise, the developers of the JDK may not have made the correct decision (even some of the original Java developers admit that some original implementations were a mistake, but these implementations are used in too many different applications to go back and alter them) and it would be wise not to repeat these mistakes, but rather, learn from them.
As a supplement to the JDK source code, an experienced Java developer should read as much code written by famous Java developers as possible. For example, reading the code written by Martin Fowler in Refactoring can be quite eye-opening. There may be ways of writing code that we had never thought of but are common to the most well-seasoned practitioners. Although it is nearly impossible to devise a comprehensive list of the books that contain the most-written code (being that well-written is very subjective), a few of the most well-known books are as follows:
Although there are countless others, these books provide a good foundation, encompassing some of the most prolific Java developers in history. Just as with the JDK source code, the code written in each of the above books is written in the particular style of its author. Each developer is an individual and each will have his or her own style (i.e. some swear by opening braces at the end of a line while others demand that they are placed on a new line all by themselves), but the point is not to get bogged down in minutia. Instead, we should learn how some of the best Java developers write their code and should aspire to write code that is just as simple and just as clean to read.
In summary, this technique can be distilled down into the following:
Read as much code written by experienced Java developers as possible. Each developer has his or her own style, and each are human and can make poor choices, but on the whole, developers should emulate the code written by many of the original authors of Java and many of its most prolific practitioners.
Conclusion
There are countless techniques for improving both the skill level of a developer, as well as the code he or she developers. In this third installation of the 4 More Techniques for Writing Better Java series, we covered validating method arguments using the standard methods provided by the Objects
class, understanding the Object
class, experimenting with jshell, and maintaining an insatiable appetite for resources with the best-written code. Using these techniques, paired with a healthy dosage of good judgment, can lead to better Java for developers at any level, from novice to expert.
Opinions expressed by DZone contributors are their own.
Comments