From assertions in JDK 1.4 to the forthcoming lambdas in Java 8, the Java language has evolved considerably since its inception. The next several Java 101 articles present a toolbox of essential Java language features, starting this week with assertions and generics.
The Java language is one of the most widely used programming languages in the world. The next several articles in Java 101: The next generation will focus on features added to the Java language from Java 1.4 to Java 8 (and even touch on Java 9). My goal is to introduce you to a toolbox of essential Java language features, with examples demonstrating both why and how they’re used in Java programs.
This first article is all about assertions, which were added in Java 1.4, and generics, the first of a handful of important new features introduced in Java 5.
Assertions in Java 1.4
Assertions, introduced in Java 1.4, remain one of the most useful and important additions to the Java language. Assertions are used to codify the requirements that render a program correct or not. Assertions test conditions (aka Boolean expressions) for true values, notifying the developer when such conditions are false. Using assertions can greatly increase your confidence in the correctness of your code.
The Java Q&A blog
Do you have questions about Java programming? Get reliable answers from an experienced Java tutor. Jeff Friesen’s Java Q&A blog is published weekly to address common programming questions from Java beginners and more experienced developers.
Assertions are compilable entities that execute at runtime, assuming you’ve enabled them for program testing. You can program assertions to notify you of bugs at the points where the bugs occur, which can greatly reduce the amount of time you would otherwise spend debugging a failing program.
Before Java 1.4, developers mostly used comments to document assumptions about code correctness. While useful for documenting code, comments are inferior to assertions as a testing and debugging mechanism. Because the compiler ignores comments, there is no way to use them for bug notification. It’s also common for comments to be unchanged, even when code changes.
Implementing assertions
Assertions are implemented via the assert
statement and java.lang.AssertionError
class. This statement begins with the keyword assert
and continues with a Boolean expression. An assert
statement is expressed syntactically as follows:
assert BooleanExpr;
If BooleanExpr
evaluates to true, nothing happens and execution continues. If the expression evaluates to false, however, AssertionError
is instantiated and thrown, as demonstrated in Listing 1.
Listing 1. AssertDemo.java (version 1)
public class AssertDemo
{
public static void main(String[] args)
{
int x = -1;
assert x >= 0;
}
}
The assertion in Listing 1 indicates the developer’s belief that variable x
contains a value that is greater than or equal to 0. However, this is clearly not the case; the assert
statement’s execution results in a thrown AssertionError
.
Compile Listing 1 (javac AssertDemo.java
) and run it with assertions enabled (java -ea AssertDemo
). You should observe the following output:
Exception in thread “main” java.lang.AssertionError
at AssertDemo.main(AssertDemo.java:6)
This message is somewhat cryptic in that it doesn’t identify what caused the AssertionError
to be thrown. If you want a more informative message, use the assert
statement expressed below:
assert BooleanExpr : expr;
Here, expr
is any expression (including a method invocation) that can return a value—you cannot invoke a method with a void
return type. A useful expression is a string literal that describes the reason for failure, as demonstrated in Listing 2.
Listing 2. AssertDemo.java (version 2)
public class AssertDemo
{
public static void main(String[] args)
{
int x = -1;
assert x >= 0: “x < 0”;
}
}
Compile Listing 2 (javac AssertDemo.java
) and run it with assertions enabled (java -ea AssertDemo
). This time, you should observe the following slightly expanded output, which includes the reason for the thrown AssertionError
:
Exception in thread “main” java.lang.AssertionError: x < 0
at AssertDemo.main(AssertDemo.java:6)
For either example, running AssertDemo
without the -ea
(enable assertions) option results in no output. When assertions are not enabled, they are not executed, although they are still present in the classfile.
Testing preconditions and postconditions with assertions
Assertions are often used to test a program’s preconditions and postconditions:
- A precondition is a condition that must evaluate to true before the execution of some code sequence. Preconditions ensure that callers keep their contracts with callees.
- A postcondition is a condition that must evaluate to true after the execution of some code sequence. Postconditions ensure that callees keep their contracts with callers.
Preconditions
You can enforce preconditions on public constructors and methods by making explicit checks and throwing exceptions when necessary. For private helper methods, you can enforce preconditions by specifying assertions. Consider Listing 3.
Listing 3. AssertDemo.java (version 3)
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
class PNG
{
/**
* Create a PNG instance, read specified PNG file, and decode
* it into suitable structures.
*
* @param filespec path and name of PNG file to read
*
* @throws NullPointerException when <code>filespec</code>
is
* <code>null</code>
*/
PNG(String filespec) throws IOException
{
// Enforce preconditions in non-private constructors and
// methods.
if (filespec == null)
throw new NullPointerException(“filespec is null”);
try (FileInputStream fis = new FileInputStream(filespec))
{
readHeader(fis);
}
}
private void readHeader(InputStream is) throws IOException
{
// Confirm that precondition is satisfied in private
// helper methods.
assert is != null : “null passed to is”;
}
}
public class AssertDemo
{
public static void main(String[] args) throws IOException
{
PNG png = new PNG((args.length == 0) ? null : args[0]);
}
}
The PNG
class in Listing 3 is the minimal beginning of a library for reading and decoding PNG (portable network graphics) image files. The constructor explicitly compares filespec
with null
, throwing NullPointerException
when this parameter contains null
. The point is to enforce the precondition that filespec
not contain null
.
It’s not appropriate to specify assert filespec != null;
because the precondition mentioned in the constructor’s Javadoc wouldn’t (technically) be honored when assertions were disabled. (In fact, it would be honored because FileInputStream()
would throw NullPointerException
, but you shouldn’t depend on undocumented behavior.)
However, assert
is appropriate in the context of the private readHeader()
helper method, which will be completed eventually to read and decode a PNG file’s 8-byte header. The precondition that is
always be passed a nonnull value will always hold.
Postconditions
Postconditions are typically specified via assertions, regardless of whether or not the method (or constructor) is public. Consider Listing 4.
Listing 4. AssertDemo.java (version 4)
public class AssertDemo
{
public static void main(String[] args)
{
int[] array = { 20, 91, -6, 16, 0, 7, 51, 42, 3, 1 };
sort(array);
for (int element: array)
System.out.printf(“%d “, element);
System.out.println();
}
private static boolean isSorted(int[] x)
{
for (int i = 0; i < x.length-1; i++)
if (x[i] > x[i+1])
return false;
return true;
}
private static void sort(int[] x)
{
int j, a;
// For all integer values except the leftmost value ...
for (int i = 1; i < x.length; i++)
{
// Get integer value a.
a = x[i];
// Get index of a. This is the initial insert position, which is
// used if a is larger than all values in the sorted section.
j = i;
// While values exist to the left of a’s insert position and the
// value immediately to the left of that insert position is
// numerically greater than a’s value ...
while (j > 0 && x[j-1] > a)
{
// Shift left value—x[j-1]—one position to its right —
// x[j].
x[j] = x[j-1];
// Update insert position to shifted value’s original position
// (one position to the left).
j—;
}
// Insert a at insert position (which is either the initial insert
// position or the final insert position), where a is greater than
// or equal to all values to its left.
x[j] = a;
}
assert isSorted(x): “array not sorted”;
}
}
Listing 4 presents a sort()
helper method that uses the insertion sort algorithm to sort an array of integer values. I’ve used assert
to check the postcondition of x
being sorted before sort()
returns to its caller.
The example in Listing 4 demonstrates an important characteristic of assertions, which is that they’re typically expensive to execute. For this reason, assertions are usually disabled in production code. In Listing 4, isSorted()
must scan through the entire array, which can be time-consuming in the case of a lengthy array.
Generics in Java 5
In addition to the Java Concurrency Utilities (profiled in June 2013 for this series), Java 5 added eight new language features: generics, typesafe enums, annotations, autoboxing and unboxing, enhanced for loop, static imports, varargs, and covariant return types. I’ll cover all of these Java 5 features over the next two articles, starting here with generics.
Generics is a suite of language features that allow types or methods to operate on objects of various types while providing compile-time type safety. Generics address the problem of java.lang.ClassCastException
s thrown at runtime due to code that is not type safe.
Generics in the Java class library
Although generics are widely used in the Java Collections Framework, they are not exclusive to it. Generics are also used in other parts of Java’s standard class library, including java.lang.Class
, java.lang.Comparable
, java.lang.ThreadLocal
, and java.lang.ref.WeakReference
.
Consider the following code fragment, which demonstrates the lack of type safety that was common in Java code before generics were introduced:
List doubleList = new LinkedList();
doubleList.add(new Double(3.5));
Double d = (Double) doubleList.iterator().next();
Although the goal of the above program is to store only java.lang.Double
objects in the list, nothing prevents other kinds of objects from being stored. For example, you could specify doubleList.add(“Hello”);
to add a java.lang.String
object. However, when storing another kind of object, the final line’s (Double)
cast operator causes ClassCastException
to be thrown when confronted with a non-Double
object.
Because this lack of type safety isn’t detected until runtime, a developer might not be aware of the problem, leaving it to the client to discover. Clearly it would be better to have the compiler detect the problem. Generics aid the compiler by letting the developer mark the list as containing a specific type of object, as follows:
List<Double> doubleList = new LinkedList<Double>();
doubleList.add(new Double(3.5));
Double d = doubleList.iterator().next();
List<Double>
now reads “List
of Double
”. List
is a generic interface, expressed as List<E>
, that takes a Double
type argument, which is also specified when creating the actual object. The compiler can now enforce type correctness when adding an object to the list—for instance, the list could store Double
s only. This enforcement removes the need for the (Double)
cast.
Discovering generic types
A generic type is a class or interface that introduces a set of parameterized types via a formal type parameter list, which is a comma-separated list of type parameter names between a pair of angle brackets. Generic types adhere to the following syntax:
class identifier<formalTypeParameterList>
{
// class body
}
interface identifier<formalTypeParameterList>
{
// interface body
}
The Java Collections Framework offers many examples of generic types and their parameter lists. For example, java.util.Set<E>
is a generic type with <E>
as its formal type parameter list and E
as this list’s solitary type parameter. java.util.Map<K, V>
is another example.
Naming type parameters
Java programming convention dictates that type parameter names be single uppercase letters, such as E
for element, K
for key, V
for value, and T
for type. If possible, avoid using a meaningless name like “P
”—java.util.List<E>
means a list of elements, but what could you possibly mean by List<P>
?
A parameterized type is a generic type instance where the generic type’s type parameters are replaced with actual type arguments (type names). For example, Set<String>
is a parameterized type where String
is the actual type argument replacing type parameter E
.
The Java language supports the following kinds of actual type arguments: