In this supplementary material we discuss some of the new language features available in Java 5. We do this in the context of a sample of projects to be found in the second edition of Objects First with Java - A Practical Introduction using BlueJ, Pearson Education, 2005 (in the USA: Prentice Hall, 2005), ISBN 0-131-24933-9.
The following topics are discussed:
Collection classes such as ArrayList
, LinkedList
, and
HashMap
permit objects of any class to be stored within
them.
This feature makes them extremely useful for storing arbitrary numbers of
objects in many different application contexts.
One drawback, however, is that storing an object into a collection effectively 'loses' details of an object's type, so that retrieval requires the use of a cast:
Lot selectedLot = (Lot) lots.get(lotNumber - 1);
This is understandable, because the compiler has no way of knowing what type of objects have been stored into the collection. Indeed, a particular collection might have objects of several different types stored within it at any one time.
In Java 5, the new Generics feature offers a way around this 'type loss' problem and casting requirement through its support of typed collections.
If the notebook1 project of Chapter 4 is compiled with a Java 5 compiler, the following warning message is produced:
Note: ...\projects\chapter04\notebook1\Notebook.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
The message suggests recompiling with an additional compiler option: -Xlint. When this option is added (e.g., via the bluej.defs configuration file) the more detailed explanation is:
Notebook.java:30: warning: [unchecked] unchecked call to add(E) as a member of the raw type java.util.ArrayList notes.add(note); ^
This warning is indicating that the compiler cannot check whether or not it is
appropriate to add an object of type String
to the
notes
collection.
Java 5 encourages programmers to use typed collections - collections
that store objects of a known type -
rather than untyped collections - collections that can store objects of mixed
types.
In this particular case, we are encouraged to indicate to the compiler that
notes
will store objects of type String
.
Java 5 introduces a new piece of syntax to allow us to do this.
Instead of declaring notes
as follows:
private ArrayList notes;
we can now declare it as
private ArrayList<String> notes;
We have to make a corresponding change where the ArrayList object is created in the constructor. Instead of:
notes = new ArrayList();
we now write:
notes = new ArrayList<String>();
We can read the angle brackets <..> roughly as "of". Thus, we can read the new
construct "ArrayList<String>
" as
"ArrayList of Strings". This new definition
declares that all elements of this ArrayList will be of type String. The Java
compiler will then enforce this by allowing only String objects to be added.
We say that typed collections are parameterized, but note that the parameter it takes is a type rather than a value. We shall see in the discussion of typed HashMaps, below, that parameterized types can take multiple type parameters.
A typed collection does not require a cast to be used when objects are retrieved from it, since the type of its elements is now known. The following is acceptable for accessing an individual element:
String note = notes.get(noteNumber);
Using typed collections has a knock-on effect with the use of iterators.
Consider the Lot
class of the auction project of Chapter 4,
where we currently iterate over the untyped lots
collection
as follows:
/** * Show the full list of lots in this auction. */ public void showLots() { Iterator it = lots.iterator(); while(it.hasNext()) { Lot lot = (Lot) it.next(); System.out.println(lot.toString()); } }
If we replace the untyped collection with a typed collection:
private ArrayList<Lot> lots;
then we can obviously simplify lot retrieval in getLot
to
Lot selectedLot = lots.get(lotNumber - 1);
However, we cannot immediately simplify retrieval in showLots
because it is via an untyped Iterator rather than via the typed collection.
The solution is to type the Iterator, because a typed collection will return
a typed Iterator:
/** * Show the full list of lots in this auction. */ public void showLots() { Iterator<Lot> it = lots.iterator(); while(it.hasNext()) { Lot lot = it.next(); System.out.println(lot.toString()); } }
See also details of the enhanced for loop for further changes to iteration.
In a HashMap
, we do not enter single elements,
we enter key/value pairs instead.
When a typed HashMap is used, it is necessary to supply
two type parameters in both the variable declaration and the object creation.
The first is the type for the key, and the second is the type for the value.
Here are the fields and constructor from the Responder
class of the tech-support-complete project of Chapter 5,
to illustrate how this is done:
public class Responder { private HashMap<String, String> responseMap; private ArrayList<String> defaultResponses; private Random randomGenerator; /** * Construct a Responder */ public Responder() { responseMap = new HashMap<String, String>(); defaultResponses = new ArrayList<String>(); fillResponseMap(); fillDefaultResponses(); randomGenerator = new Random(); } ... }
Adding to this HashMap is then done as before, and - as with the ArrayList - the cast can be left out when retrieving objects.
Typed collections are just one part of the generics feature introduced in Java 5. They are probably the most useful addition to the language and we recommend that they be used as standard whenever you program using collections. Because they allow more rigorous type checking to be applied at compile time, they make use of collections more type safe at run time than was previously possible.
Prior to Java 5, the standard way to iterate over the complete contents of an array would be as follows:
for(int index = 0; index < array.length; index++) { Type element = array[index]; // Use element in some way. ... }
Java 5 introduces an additional syntax with for loops that simplifies the structure of the loop header when we want to do something with each element of an array. It has the advantage that it helps to avoid common errors with getting the loop terminating condition correct. The new syntax for array iteration is:
for(Type element : array) { // Use element in some way. ... }
It helps to read the new for loop if we read the for
keyword as "for each" and the colon (:
) as "in".
The loop pattern above then becomes, "For each element in
array do ..."
Note that the new style for loop's header does not contain an index variable. The variable declared there successively stores the value of the array elements.
This example sums the number of items in an array of Products:
public int totalQuantity(Product[] products) { int total = 0; for(Product p : products) { total += p.getQuantity(); } return total; }
This new style of loop is not only available for arrays, but also for all other sorts of collections, such as lists and sets. This style does not replace the existing style, because there is no index variable available in the body of the loop. All initialization, testing and incrementation of the index variable has been made implicit. In some cases, an index variable is needed explicitly, and then the original style of for loop must be used.
The enhanced loop syntax is also available for use with Iterators, which means that the following three ways of iterating through a collection are equivalent to one another:
Original while loop
Iterator it = entries.iterator(); while(it.hasNext()) { System.out.println(it.next()); }
Original for loop
for(Iterator it = entries.iterator(); it.hasNext(); ) { System.out.println(it.next()); }
Enhanced for loop (assumes entries
is a typed
collection)
for(LogEntry entry : entries) { System.out.println(entry); }
Notice that the new style means that there is no explicit Iterator object.
The enhanced for-loop style is a useful addition to the language in so far as it simplifies the syntax of basic iteration over an array or collection, and reduces the opportunity for making errors in the loop's condition. It does not entirely replace the original style because it provides no access to an index variable or Iterator object, which is sometimes required. We recommend that it be used wherever the basic style of iteration is needed.
In Chapter 8, wrapper classes such as Integer
were discussed as a way of
storing primitive-type values in object collections:
int i = 18; Integer iwrap = new Integer(i); myCollection.add(iwrap); ... Integer element = (Integer) myCollection.get(0); int value = element.intValue();
Java 5 allows primitive-type values to be stored into and retrieved from collections without explicitly wrapping them, via a process called autoboxing. In effect, autoboxing means that the wrapping and unwrapping is performed by the compiler rather than the programmer. This means that we could write the example above as simply:
int i = 18; myCollection.add(i); ... int value = (Integer) myCollection.get(0);
When a primitive-type value is passed to a method such as add
that
expects an object type, the value is automatically wrapped in an appropriate
wrapper type. Similarly, when a wrapper-type value is stored into a primitive-type
variable, the value is automatically unboxed.
Of course, if myCollection
in the example above is a typed
collection, then the following retrieval:
int value = (Integer) myCollection.get(0);
could simply have been written as:
int value = myCollection.get(0);
Autoboxing is applied whenever a primitive-type value is passed as a parameter to a method that expects a wrapper type, and when a primitive-type value is stored in a wrapper-type variable. Similarly, unboxing is applied when a wrapper-type value is passed as a parameter to a method that expects a primitive-type value, and when stored in a primitive-type variable.
We make no particular recommendation over the use of autoboxing as it will tend to occur naturally anyway, for instance where it is necessary to maintain an arbitrary-size collection of primitive-type data.
In Java 5, a new syntax is introduced to allow the creation of enumerated types. The full syntax is very close to that of class definitions, but here we shall only explore a basic subset of it.
The following class definition illustrates a common way to associate distinct numerical values with symbolic names without using the new enum construct:
/** * Maintain a level attribute. */ public class Control { // Three possible level values. public static final int LOW = 0, MEDIUM = 1, HIGH = 2; // The current level. private int level; /** * Initialise the level to be LOW. */ public Control() { // initialise instance variables level = LOW; } /** * @return The current level (LOW, MEDIUM or HIGH) */ public int getLevel() { return level; } /** * Set the current level. * @param level The level to be set (LOW, MEDIUM or HIGH) */ public void setLevel(int level) { this.level = level; } }
The idea is that a Control
object will have its level
field store a value of 0, 1 or 2, but that we can refer to those particular
values as LOW, MEDIUM or HIGH, respectively, because the particular numbers
chosen have no significance in themselves (we might equally well have chosen
values of 0, 50 and 100, for instance).
There is a major problem with this particular style of coding
related values and this is illustrated by the setLevel
method.
Because these values are ordinary integers, we have
no guarantee that a value passed in to setLevel
will definitely
match one of the three legitimate values.
Of course, we could add tests of the parameter value to ensure that it
is one of the three approved values, but
enumerated types provide a neat alternative solution that is much better.
In its simplest form, an enumerated type allows us to create a dedicated type for a set of names, such as LOW, MEDIUM and HIGH:
/** * Enumerate the set of available levels for a Control object. */ public enum Level { LOW, MEDIUM, HIGH, }
Values of this type are referred to as Level.LOW
,
Level.MEDIUM
, and Level.HIGH
.
The Control
class can now use this new type in place of
the integer type:
public class Control { // The current level. private Level level; /** * Initialise the level to be Level.LOW. */ public Control() { // initialise instance variables this(Level.LOW); } /** * @return The current level (Level.LOW, Level.MEDIUM or Level.HIGH) */ public Level getLevel() { return level; } /** * Set the current level. * @param level The level to be set (Level.LOW, Level.MEDIUM or Level.HIGH) */ public void setLevel(Level level) { this.level = level; } }
Notice, in particular, that the type of the parameter to setLevel
is now Level
rather than int
.
It is important to appreciate that enumerated types are completely
separate from the int
type.
This means that the setLevel
method is
now protected
from receiving an inappropriate value, because their actual parameter's value
must correspond to one of the names listed in the enumerated type.
Similarly, any client calling getLevel
cannot
treat the returned value as an ordinary integer.
values
Method
Every enumerated type defines a public static method called values
that returns an array whose elements contain the values of the type.
The following statements use this method in order to
print out all available levels:
Level[] availableLevels = Level.values(); for(int i = 0; i < availableLevels.length; i++) { System.out.println(availableLevels[i]); }
or, written with the new for loop syntax:
for(Level level : Level.values()) { System.out.println(level); }
Typesafe enums are a useful addition to the language where it is desired to
use meaningful names for a related but distinct set of values.
Enums mean that it is no longer necessary to associate arbitrary integer
values with such names.
The fact that enums are distinct from the int
type adds
an important degree of type safety to their use.
We recommend that enums are used in these circumstances.
Static imports are a relatively minor addition in Java 5. They make it possible to import static methods and variables so that they can be used without use of their qualifying class name.
The following method (taken from the Location
class in the taxi-company-stage-one project of Chapter 14)
uses the static abs
and max
methods
of the Math
class to calculate the
distance between two locations:
/** * Determine the number of movements required to get * from here to the destination. * @param destination The required destination. * @return The number of movement steps. */ public int distance(Location destination) { int xDist = Math.abs(destination.getX() - x); int yDist = Math.abs(destination.getY() - y); return Math.max(xDist, yDist); }
If the following static import statements are included at the top of the source file:
import static java.lang.Math.abs; import static java.lang.Math.max;
then the body of the method could be slightly simplied to:
int xDist = abs(destination.getX() - x); int yDist = abs(destination.getY() - y); return max(xDist, yDist);
The full set of static members of a class may be imported using the form:
import static java.lang.Math.*;
In practice, the static import feature only really saves a little typing effort when there are many references to the static members of another class. We make no particular recommendation for using it.
The new Scanner
class in the java.util
package
provides a way to read and process input that substitutes for what was
often previously done using a combination of BufferedReader
,
StringTokenizer
(or
String.split
),
and the parse
methods of the wrapper classes.
Here is an excerpt from a method of the LoglineTokenizer
class of the
weblog-analyzer project in Chapter 4.
It
splits a String containing integers into separate int values and stores
them in an array:
// Split logline where there are spaces. StringTokenizer tokenizer = new StringTokenizer(logline); int numTokens = tokenizer.countTokens(); if(numTokens == dataLine.length) { for(int i = 0; i < numTokens; i++) { String number = tokenizer.nextToken(); dataLine[i] = Integer.parseInt(number); } } else { System.out.println("Invalid log line: " + logline); }
The example creates a StringTokenizer to split the line, and then repeatedly
requests the next 'token' to be returned as a String. As long as the String
contains a valid integer, the parseInt
method of Integer will
return it as an int value.
The Scanner class slightly simplifies this process because it is able both to split a String into tokens, and to return those tokens as properly typed values. Here is how the process above might be performed with a Scanner:
// Scan the logline for integers. Scanner tokenizer = new Scanner(logline); for(int i = 0; i < dataLine.length; i++) { dataLine[i] = tokenizer.nextInt(); }
The Scanner class can also be used to read and process
input from files or the keyboard. Here is how the Parser
class
of the zuul projects of Chapter 7 might use a Scanner
to read text lines containing up to two words:
import java.util.Scanner; /** * ... * * This parser reads user input and tries to interpret it as an "Adventure" * command. Every time it is called it reads a line from the terminal and * tries to interpret the line as a two word command. It returns the command * as an object of class Command. * * ... */ public class Parser { private CommandWords commands; // holds all valid command words private Scanner reader; // returns user input. /** * Create a Parser for reading user input. */ public Parser() { commands = new CommandWords(); // Scan standard input. reader = new Scanner(System.in); } /** * Parse the next user command. * @return The user's command. */ public Command getCommand() { String inputLine = ""; // will hold the full input line String word1; String word2; System.out.print("> "); // print prompt String line = reader.nextLine(); Scanner scan = new Scanner(line); if(scan.hasNext()) word1 = scan.next(); // get first word else word1 = null; if(scan.hasNext()) word2 = scan.next(); // get second word else word2 = null; // note: we just ignore the rest of the input line. // Create a command with it. return new Command(word1, word2); } /** * Print out a list of valid command words. */ public void showCommands() { commands.showAll(); } }
Notice that this class uses two Scanners. The nextLine
method of reader
is used to return a line of text,
and the hasNext
and next
methods of scan
are
used to examine and parse a line.
The Scanner class slightly simplifies the parsing and type conversion of arbitrary textual input, when compared with alternatives. We recommend that it be used where appropriate.
There are some significant and useful new features in the Java 5 release. In particular, typed collections, the enhanced for loop and enumerated types are likely to be those of most use to the majority of programmers.
Copyright 2004-2005, David J. Barnes and Michael Kölling.