Beginning Java 7

Beginning Java 7
BOOKS FOR PROFESSIONALS BY PROFESSIONALS ®
Friesen
RELATED
Beginning Java 7
Get coding with Beginning Java 7. This definitive guide to Oracle’s latest release of the
popular Java language and platform details the many APIs and tools that you’ll need to
master to become an accomplished Java developer.
Author Jeff Friesen first gives you a comprehensive guided tour of the Java language and shows you how to start programming with the JDK and NetBeans. He then
takes you through all the major APIs, from math to concurrency by way of wrappers,
reference, reflection, string handling, threading, and collections. Next, he explains how
to build graphical user interfaces; tells you everything you need to know about interacting with filesystems, networks, and databases; and details parsing, creating, and
transforming XML documents as well as working with web services. You’ll even see
how Java extends to Android, from architecture to development tools.
With Beginning Java 7, you’ll learn:
• The entire Java language, including new Java 7 features such as switch
on string, try-with-resources, final rethrow, multicatch, and SafeVarargs
• A huge assortment of APIs, including Java 7-specific APIs such as the
Fork/Join Framework, Objects, JLayer, and NIO.2
• Essential Java 7 tools, starting with the javac compiler and java
application launcher
• How to develop Android apps
Each chapter features exercises that help you test what you learned along the way.
In addition, the book walks you through the development of a simple application,
giving you essential first-hand experience and practical tips that will aid you in all
your future Java 7 projects.
Shelve in
Programming Languages / Java
User level:
Beginning–Intermediate
SOURCE CODE ONLINE
www.apress.com
This book was purchased by [email protected]
For your convenience Apress has placed some of the front
matter material after the index. Please use the Bookmarks
and Contents at a Glance links to access them.
Contents at a Glance
 About the Author.................................................................................................. xiv
 About the Technical Reviewer .............................................................................. xv
 Acknowledgments ............................................................................................... xvi
 Introduction ........................................................................................................ xvii
 Chapter 1: Getting Started with Java......................................................................1
 Chapter 2: Discovering Classes and Objects ........................................................51
 Chapter 3: Exploring Advanced Language Features ...........................................131
 Chapter 4: Touring Language APIs .....................................................................227
 Chapter 5: Collecting Objects..............................................................................319
 Chapter 6: Touring Additional Utility APIs ..........................................................401
 Chapter 7: Creating and Enriching Graphical User Interfaces ............................435
 Chapter 8: Interacting with Filesystems.............................................................511
 Chapter 9: Interacting with Networks and Databases ........................................585
 Chapter 10: Parsing, Creating, and Transforming XML Documents ...................663
 Chapter 11: Working with Web Services ............................................................751
 Chapter 12: Java 7 Meets Android .....................................................................831
 Index ...................................................................................................................873
iii
CHAPTER 1
Getting Started with Java
Welcome to Java. This chapter launches you on a tour of this technology by focusing on fundamentals.
First, you receive an answer to the “What is Java?” question. If you have not previously encountered Java,
the answer might surprise you. Next, you are introduced to some basic tools that will help you start
developing Java programs, and to the NetBeans integrated development environment, which simplifies
the development of these programs. Finally, you explore fundamental language features.
What Is Java?
Java is a language for describing programs, and Java is a platform on which to run programs written in
Java and other languages (e.g., Groovy, Jython, and JRuby). This section introduces you to Java the
language and Java the platform.
 Note To discover Java’s history, check out Wikipedia’s “Java (programming language)”
(http://en.wikipedia.org/wiki/Java_(programming_language)#History) and “Java (software platform)”
(http://en.wikipedia.org/wiki/Java_(software_platform)#History) entries.
Java Is a Language
Java is a general-purpose, class-based, and object-oriented language patterned after C and C++ to make
it easier for existing C/C++ developers to migrate to this language. Not surprisingly, Java borrows
elements from these languages. The following list identifies some of these elements:
•
Java supports the same single-line and multiline comment styles as found in
C/C++ for documenting source code.
•
Java provides the if, switch, while, for, and other reserved words as found in the
C and C++ languages. Java also provides the try, catch, class, private, and other
reserved words that are found in C++ but not in C.
•
As with C and C++, Java supports character, integer, and other primitive types.
Furthermore, Java shares the same reserved words for naming these types; for
example, char (for character) and int (for integer).
1
CHAPTER 1  GETTING STARTED WITH JAVA
•
Java supports many of the same operators as C/C++: the arithmetic operators (+, -,
*, /, and %) and conditional operator (?:) are examples.
•
Java also supports the use of brace characters { and } to delimit blocks of
statements.
Although Java is similar to C and C++, it also differs in many respects. The following list itemizes
some of these differences:
•
Java supports an additional comment style known as Javadoc.
•
Java provides transient, synchronized, strictfp, and other reserved words not
found in C or C++.
•
Java’s character type has a larger size than the version of this type found in C and
C++, Java’s integer types do not include unsigned variants of these types (Java has
no equivalent of the C/C++ unsigned long integer type, for example), and Java’s
primitive types have guaranteed sizes, whereas no guarantees are made for the
equivalent C/C++ types.
•
Java doesn’t support all of the C/C++ operators. For example, there is no sizeof
operator. Also, Java provides some operators not found in C/C++. For example,
>>> (unsigned right shift) and instanceof are exclusive to Java.
•
Java provides labeled break and continue statements. These variants of the C/C++
break and continue statements provide a safer alternative to C/C++’s goto
statement, which Java doesn’t support.
 Note Comments, reserved words, types, operators, and statements are examples of fundamental language
features, which are discussed later in this chapter.
A Java program starts out as source code that conforms to Java syntax, rules for combining symbols
into meaningful entities. The Java compiler translates the source code stored in files that have the
“.java” file extension into equivalent executable code, known as bytecode, which it stores in files that
have the “.class” file extension.
 Note The files that store compiled Java code are known as classfiles because they often store the runtime
representation of Java classes, a language feature discussed in Chapter 2.
The Java language was designed with portability in mind. Ideally, Java developers write a Java
program’s source code once, compile this source code into bytecode once, and run the bytecode on any
platform (e.g., Windows, Linux, and Mac OS X) where Java is supported, without ever having to change
the source code and recompile. Portability is achieved in part by ensuring that primitive types have the
same sizes across platforms. For example, the size of Java’s integer type is always 32 bits.
2
CHAPTER 1  GETTING STARTED WITH JAVA
The Java language was also designed with robustness in mind. Java programs should be less
vulnerable to crashes than their C/C++ counterparts. Java achieves robustness in part by not
implementing certain C/C++ features that can make programs less robust. For example, pointers
(variables that store the addresses of other variables) increase the likelihood of program crashes, which
is why Java doesn’t support this C/C++ feature.
Java Is a Platform
Java is a platform that executes Java-based programs. Unlike platforms with physical processors (e.g., an
Intel processor) and operating systems (e.g., Windows 7), the Java platform consists of a virtual machine
and execution environment.
A virtual machine is a software-based processor with its own set of instructions. The Java Virtual
Machine (JVM)’s associated execution environment consists of a huge library of prebuilt functionality,
commonly known as the standard class library, that Java programs can use to perform routine tasks (e.g.,
open a file and read its contents). The execution environment also consists of “glue” code that connects
the JVM to the underlying operating system.
 Note The “glue” code consists of platform-specific libraries for accessing the operating system’s windowing,
networking, and other subsystems. It also consists of code that uses the Java Native Interface (JNI) to bridge
between Java and the operating system. I discuss the JNI in Appendix C. You might also want to check out
Wikipedia’s “Java Native Interface” entry (http://en.wikipedia.org/wiki/Java_Native_Interface) to learn
about the JNI.
When a Java program launcher starts the Java platform, the JVM is launched and told to load a Java
program’s starting classfile into memory, via a component known as a classloader. After the classfile has
loaded, the following tasks are performed:
•
The classfile’s bytecode instruction sequences are verified to ensure that they
don’t compromise the security of the JVM and underlying environment.
Verification ensures that a sequence of instructions doesn’t find a way to exploit
the JVM to corrupt the environment and possibly steal sensitive information. The
component that handles this task is known as the bytecode verifier.
•
The classfile’s main sequence of bytecode instructions is executed. The
component that handles this task is known as the interpreter because instructions
are interpreted (identified and used to select appropriate sequences of native
processor instructions to carry out the equivalent of what the bytecode
instructions mean). When the interpreter discovers that a bytecode instruction
sequence is executed repeatedly, it informs the Just-In-Time (JIT) compiler
component to compile this sequence into an equivalent sequence of native
instructions. The JIT helps the Java program achieve faster execution than would
be possible through interpretation alone. Note that the JIT and the Java compiler
that compiles source code into bytecode are two separate compilers with two
different goals.
3
CHAPTER 1  GETTING STARTED WITH JAVA
During execution, a classfile might refer to another classfile. In this situation, a classloader is used to
load the referenced classfile, the bytecode verifier then verifies the classfile’s bytecodes, and the
interpreter/JIT executes the appropriate bytecode sequence in this other classfile.
The Java platform was designed with portability in mind. By providing an abstraction over the
underlying operating system, bytecode instruction sequences should execute consistently across Java
platforms. However, this isn’t always borne out in practice. For example, many Java platforms rely on
the underlying operating system to schedule threads (discussed in Chapter 4), and the thread scheduling
implementation varies from operating system to operating system. As a result, you must be careful to
ensure that the program is designed to adapt to these vagaries.
The Java platform was also designed with security in mind. As well as the bytecode verifier, the
platform provides a security framework to help ensure that malicious programs don’t corrupt the
underlying environment on which the program is running. Appendix C discusses Java’s security
framework.
Installing and Working with JDK 7
Three software development kits (SDKs) exist for developing different kinds of Java programs:
•
The Java SE (Standard Edition) Software Development Kit (known as the JDK) is
used to create desktop-oriented standalone applications and web browserembedded applications known as applets. You are introduced to standalone
applications later in this section. I don’t discuss applets because they aren’t as
popular as they once were.
•
The Java ME (Mobile Edition) SDK is used to create applications known as
MIDlets and Xlets. MIDlets target mobile devices, which have small graphical
displays, simple numeric keypad interfaces, and limited HTTP-based network
access. Xlets typically target television-oriented devices such as Blu-ray Disc
players. The Java ME SDK requires that the JDK also be installed. I don’t discuss
MIDlets or Xlets.
•
The Java EE (Enterprise Edition) SDK is used to create component-based
enterprise applications. Components include servlets, which can be thought of as
the server equivalent of applets, and servlet-based Java Server Pages (JSPs). The
Java EE SDK requires that the JDK also be installed. I don’t discuss servlets.
This section introduces you to JDK 7 (also referred to as Java 7, a term used in later chapters) by first
showing you how to install this latest major Java SE release. It then shows you how to use JDK 7 tools to
develop a simple standalone application—I’ll use the shorter application term from now on.
Installing JDK 7
Point your browser to http://www.oracle.com/technetwork/java/javase/downloads/index-jsp138363.html and follow the instructions on the resulting web page to download the appropriate JDK 7
installation exe or gzip tarball file for your Windows, Solaris, or Linux platform.
Following the download, run the Windows executable or unarchive the Solaris/Linux gzip tarball,
and modify your PATH environment variable to include the resulting home directory’s bin subdirectory so
that you can run JDK 7 tools from anywhere in your filesystem. For example, you might include the
C:\Program Files\Java\jdk1.7.0 home directory in the PATH on a Windows platform. You should also
update your JAVA_HOME environment variable to point to JDK 7’s home directory, to ensure that any Javadependent software can find this directory.
4
CHAPTER 1  GETTING STARTED WITH JAVA
JDK 7’s home directory contains several files (e.g., README.html and LICENSE) and subdirectories. The
most important subdirectory from this book’s perspective is bin, which contains various tools that we’ll
use throughout this book. The following list identifies some of these tools:
•
jar: a tool for packaging classfiles and resource files into special ZIP files with
“.jar” file extensions
•
java: a tool for running applications
•
javac: a tool that launches the Java compiler to compile one or more source files
•
javadoc: a tool that generates special HTML-based documentation from Javadoc
comments
The JDK’s tools are run in a command-line environment. You establish this by launching a
command window (Windows) or shell (Linux/Solaris), which presents to you a sequence of prompts for
entering commands (program names and their arguments). For example, a command window (on
Windows platforms) prompts you to enter a command by presenting a drive letter and path
combination (e.g., C:\).
You respond to the prompt by typing the command, and then press the Return/Enter key to tell the
operating system to execute the command. For example, javac x.java followed by a Return/Enter key
press causes the operating system to launch the javac tool, and to pass the name of the source file being
compiled (x.java) to this tool as its command-line argument. If you specified the asterisk (*) wildcard
character, as in javac *.java, javac would compile all source files in the current directory. To learn
more about working at the command line, check out Wikipedia’s “Command-line interface” entry
(http://en.wikipedia.org/wiki/Command-line_interface).
Another important subdirectory is jre, which stores the JDK’s private copy of the Java Runtime
Environment (JRE). The JRE implements the Java platform, making it possible to run Java programs.
Users interested in running (but not developing) Java programs would download the public JRE.
Because the JDK contains its own copy of the JRE, developers do not need to download and install the
public JRE.
 Note JDK 7 comes with external documentation that includes an extensive reference to Java’s many APIs (see
http://en.wikipedia.org/wiki/Application_programming_interface to learn about this term). You can
download the documentation archive from
http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html so that you can view
this documentation offline. However, because the archive is fairly large, you might prefer to view the
documentation online at http://download.oracle.com/javase/7/docs/index.html.
Working with JDK 7
An application consists of a class with an entry-point method named main. Although a proper discussion
of classes and methods must wait until Chapter 2, it suffices for now to just think of a class as a factory
for creating objects (also discussed in Chapter 2), and to think of a method as a named sequence of
instructions that are executed when the method is called. Listing 1-1 introduces you to your first
application.
5
CHAPTER 1  GETTING STARTED WITH JAVA
Listing 1-1. Greetings from Java
class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, world!");
}
}
Listing 1-1 declares a class named HelloWorld that provides a framework for this simple application.
It also declares a method named main within this class. When you run this application, and you will learn
how to do so shortly, it is this entry-point method that is called and its instructions that are executed.
The main() method includes a header that identifies this method and a block of code located
between an open brace character ({) and a close brace character (}). As well as naming this method, the
header provides the following information:
•
public: This reserved word makes main() visible to the startup code that calls this
method. If public wasn’t present, the compiler would output an error message
stating that it could not find a main() method.
•
static: This reserved word causes this method to associate with the class instead
of associating with any objects created from this class. Because the startup code
that calls main() doesn’t create an object from the class in order to call this
method, it requires that the method be declared static. Although the compiler
will not report an error if static is missing, it will not be possible to run
HelloWorld, which will not be an application if the proper main() method doesn’t
exist.
•
void: This reserved word indicates that the method doesn’t return a value. If you
change void to a type’s reserved word (e.g., int) and then insert a statement that
returns a value of this type (e.g., return 0;), the compiler will not report an error.
However, you won’t be able to run HelloWorld because the proper main() method
would not exist.
•
(String[] args): This parameter list consists of a single parameter named args of
type String[]. Startup code passes a sequence of command-line arguments to
args, which makes these arguments available to the code that executes within
main(). You’ll learn about parameters and arguments in Chapter 2.
The block of code consists of a single System.out.println("Hello, world!"); method call. From left
to write, System identifies a standard class of system utilities, out identifies an object variable located in
System whose methods let you output values of various types optionally followed by a newline character
to the standard output device, println identifies a method that prints its argument followed by a
newline character to standard output, and "Hello, world!" is a string (a sequence of characters
delimited by double quote " characters and treated as a unit) that is passed as the argument to println
and written to standard output (the starting " and ending " double quote characters are not written;
these characters delimit but are not part of the string).
6
CHAPTER 1  GETTING STARTED WITH JAVA
 Note All desktop Java/nonJava applications can be run at the command line. Before graphical user interfaces
with their controls for inputting and outputting values (e.g., textfields), these applications obtained their input and
generated their output with the help of Standard I/O, an input/output mechanism that originated with the Unix
operating system, and which consists of standard input, standard output, and standard error devices.
The user would input data via the standard input device (typically the keyboard, but a file could be specified
instead—Unix treats everything as files). The application’s output would appear on the standard output device
(typically a computer screen, but optionally a file or printer). Output messages denoting errors would be output to
the standard error device (screen, file, or printer) so that these messages could be handled separately.
Now that you understand how Listing 1-1 works, you’ll want to create this application. Complete the
following steps to accomplish this task:
1.
Copy Listing 1-1 to a file named HelloWorld.java.
2.
Execute javac HelloWorld.java to compile this source file. javac will complain
if you do not specify the “.java” file extension.
If all goes well, you should see a HelloWorld.class file in the current directory. Now execute java
HelloWorld to run this classfile’s main() method. Don’t specify the “.class” file extension or java will
complain. You should observe the following output:
Hello, world!
Congratulations! You have run your first Java-based application. You’ll have an opportunity to run
more applications throughout this book.
Installing and Working with NetBeans 7
For small projects, it’s no big deal to work at the command line with JDK tools. Because you’ll probably
find this scenario tedious (and even unworkable) for larger projects, you should consider obtaining an
Integrated Development Environment (IDE) tool.
Three popular IDEs for Java development are Eclipse (http://www.eclipse.org/), IntelliJ IDEA
(http://www.jetbrains.com/idea/), which is free to try but must be purchased if you want to continue to
use it, and NetBeans (http://netbeans.org/). I focus on the NetBeans 7 IDE in this section because of its
JDK 7 support. (IntelliJ IDEA 10.5 also supports JDK 7.)
 Note For a list of NetBeans 7 IDE enhancements that are specific to JDK 7, check out the page at
http://wiki.netbeans.org/NewAndNoteworthyNB70#JDK7_support.
7
CHAPTER 1  GETTING STARTED WITH JAVA
This section shows you how to install the NetBeans 7 IDE. It then introduces you to this IDE while
developing HelloWorld.
 Note NetBeans is more than an IDE. It’s also a platform framework that lets developers create applications
much faster by leveraging the modular NetBeans architecture.
Installing NetBeans 7
Point your browser to http://netbeans.org/downloads/ and perform the following tasks:
1.
Select an appropriate IDE language (English is the default).
2.
Select an appropriate platform (Windows is the default).
3.
Click the Download button underneath the next-to-leftmost (Java EE) column
to initiate the download process for the appropriate installer file. I chose to
download the English Java EE installer for the Windows platform, which is a
file named netbeans-7.x-ml-javaee-windows.exe. (Because I don’t explore Java
EE in Beginning Java 7, it might seem pointless to install the Java EE version of
NetBeans. However, you might as well install this software now in case you
decide to explore Java EE after reading this book.)
Run the installer. After configuring itself, the installer presents a Welcome dialog that gives you the
option of choosing which application servers you want to install with the IDE. Ensure that both the
GlassFish Server and Apache Tomcat checkboxes remain checked (you might want to play with both
application servers when exploring Java EE), and click the Next button.
On the resulting License Agreement dialog, read the agreement, indicate its acceptance by checking
the checkbox, and click Next. Repeat this process on the subsequent JUnit License Agreement dialog.
The resulting NetBeans IDE 7.0 Installation dialog presents the default location where NetBeans will
be installed (C:\Program Files\NetBeans 7.0 on my platform) and the JDK 7 home directory location
(C:\Program Files\Java\jdk1.7.0 on my platform). Change these locations if necessary and click Next.
The resulting GlassFish 3.1 Installation dialog box presents the default location where the GlassFish
application server will be installed (C:\Program Files\glassfish-3.1 on my platform). Change this
location if necessary and click Next.
The resulting Apache Tomcat 7.0.11 Installation dialog presents the default location where the
Apache Tomcat application server will be installed (C:\Program Files\Apache Software
Foundation\Apache Tomcat 7.0.11 on my platform). Change this location if necessary and click Next.
The resulting Summary dialog presents your chosen options as well as the combined installation
size for all software being installed. After reviewing this information, click the Install button to begin
installation.
Installation takes a few minutes and culminates in a Setup Complete dialog. After reviewing this
dialog’s information, click the Finish button to complete installation.
Assuming a successful installation, start this IDE. NetBeans first presents a splash screen while it
performs various initialization tasks, and then presents a main window similar to that shown in Figure 11.
8
This book was purchased by [email protected]
CHAPTER 1  GETTING STARTED WITH JAVA
Figure 1-1. The NetBeans 7 IDE’s main window initially presents a Start Page tab.
If you’ve worked with previous versions of the NetBeans IDE, you might want to click the Take a
Tour button to learn how version 7 differs from its predecessors. You are taken to a web page that
provides video tours of the IDE, such as NetBeans IDE 7.0 Overview.
Working with NetBeans 7
NetBeans presents a user interface whose main window is divided into a menu bar, a toolbar, a
workspace, and a status bar. The workspace presents a Start Page tab for learning about NetBeans,
accessing your NetBeans projects, and more.
To help you get comfortable with this IDE, I’ll show you how to create a HelloWorld project that
reuses Listing 1-1’s source code. I’ll also show you how to compile and run the HelloWorld application.
Complete the following steps to create the HelloWorld project:
1.
Select New Project from the File menu.
2.
Make sure that Java is the selected category and Java Application is the
selected Project in their respective Categories and Projects lists on the
resulting New Project dialog box’s Choose Project pane. Click Next.
3.
On the resulting Name and Location pane, enter HelloWorld into the Project
Name textfield. Notice that helloworld.HelloWorld appears in the textfield to
the right of the Create Main Class checkbox (which must be checked). The
9
CHAPTER 1  GETTING STARTED WITH JAVA
helloworld portion of this string refers to a package that stores the HelloWorld
class portion of this string. (Packages are discussed in Chapter 3.) Click Finish.
NetBeans spends a few moments creating the HelloWorld project. Once it finishes, NetBeans
presents the workspace shown in Figure 1-2.
Figure 1-2. The workspace is organized into multiple work areas.
After creating HelloWorld, NetBeans organizes the workspace into projects, editor, navigator, and
tasks work areas. The projects area helps you manage your projects and is organized into the following
tabs:
•
The Projects tab is the main entry point to your project’s source and resource files.
It presents a logical view of important project contents.
•
The Files tab presents a directory-based view of your projects. This view includes
any files and folders not shown on the Projects tab.
•
The Services tab presents a logical view of resources registered with the IDE, for
example, servers, databases, and web services.
The editor area helps you edit a project’s source files. Each file is associated with its own tab, which
is labeled with the filename. For example, Figure 1-2 reveals a HelloWorld.java tab that provides a
skeletal version of this source file’s contents.
10
CHAPTER 1  GETTING STARTED WITH JAVA
The navigator area presents the Navigator tab, which offers a compact view of the currently selected
file, and which simplifies navigation between various parts of the file (e.g., class and method headers).
Finally, the task area presents a Tasks tab that reveals a to-do list of items that need to be resolved
for the project’s various files. Each item consists of a description, a filename, and the location within the
file where resolution must take place.
Replace the HelloWorld.java tab’s contents with Listing 1-1, keeping the package helloworld;
statement at the top of the file to prevent NetBeans from complaining about an incorrect package.
Continuing, select Run Main Project from the Run menu to compile and run this application. Figure 13’s Output tab shows HelloWorld’s greeting.
Figure 1-3. An Output tab appears to the left of Tasks and shows HelloWorld’s greeting.
 Tip To pass command-line arguments to an application, first select Project Properties from the File menu. On
the resulting Project Properties dialog box, select Run in the Categories tree, and enter the arguments (separated
by spaces; for example, first second third) in the Arguments textfield on the resulting pane.
For more information on the NetBeans 7 IDE, study the tutorials via the Start Page tab, access IDE
help via the Help menu, and explore the NetBeans knowledge base at http://netbeans.org/kb/.
11
CHAPTER 1  GETTING STARTED WITH JAVA
Java Language Fundamentals
Most computer languages support comments, identifiers, types, variables, expressions, and statements.
Java is no exception, and this section introduces you to these fundamental language features from Java’s
perspective.
Comments
A program’s source code needs to be documented so that you (and any others who have to maintain it)
can understand it, now and later. Source code should be documented while being written and whenever
it is modified. If these modifications impact existing documentation, the documentation must be
updated so that it accurately explains the code.
Java provides the comment feature for embedding documentation in source code. When the source
code is compiled, the Java compiler ignores all comments—no bytecodes are generated. Single-line,
multiline, and Javadoc comments are supported.
Single-Line Comments
A single-line comment occupies all or part of a single line of source code. This comment begins with the
// character sequence and continues with explanatory text. The compiler ignores everything from // to
the end of the line in which // appears. The following example presents a single-line comment:
int x = (int) (Math.random()*100); // Obtain a random x coordinate from 0 through 99.
Single-line comments are useful for inserting short but meaningful explanations of source code into
this code. Don’t use them to insert unhelpful information. For example, when declaring a variable, don’t
insert a meaningless comment such as // this variable is an integer.
Multiline Comments
A multiline comment occupies one or more lines of source code. This comment begins with the /*
character sequence, continues with explanatory text, and ends with the */ character sequence.
Everything from /* through */ is ignored by the compiler. The following example demonstrates a
multiline comment:
static boolean isLeapYear(int year)
{
/*
A year is a leap year if it is divisible by 400, or divisible by 4 but
not also divisible by 100.
*/
if (year%400 == 0)
return true;
else
if (year%100 == 0)
return false;
else
if (year%4 == 0)
return true;
else
12
CHAPTER 1  GETTING STARTED WITH JAVA
return false;
}
This example introduces a method for determining whether or not a year is a leap year. The
important part of this code to understand is the multiline comment, which clarifies the expression that
determines whether year’s value does or doesn’t represent a leap year.
 Caution You cannot place one multiline comment inside another. For example, /*/* Nesting multiline
comments is illegal! */*/ is not a valid multiline comment.
Javadoc Comments
A Javadoc comment (also known as a documentation comment) occupies one or more lines of source
code. This comment begins with the /** character sequence, continues with explanatory text, and ends
with the */ character sequence. Everything from /** through */ is ignored by the compiler. The
following example demonstrates a Javadoc comment:
/**
* Application entry point
*
* @param args array of command-line arguments passed to this method
*/
public static void main(String[] args)
{
// TODO code application logic here
}
This example begins with a Javadoc comment that describes the main() method. Sandwiched
between /** and */ is a description of the method, which could (but doesn’t) include HTML tags (such
as <p> and <code>/</code>), and the @param Javadoc tag (an @-prefixed instruction).
The following list identifies several commonly used tags:
•
@author identifies the source code’s author.
•
@deprecated identifies a source code entity (e.g., a method) that should no longer
be used.
•
@param identifies one of a method’s parameters.
•
@see provides a see-also reference.
•
@since identifies the software release where the entity first originated.
•
@return identifies the kind of value that the method returns.
Listing 1-2 presents our HelloWorld application with documentation comments that describe the
HelloWorld class and its main() method.
13
CHAPTER 1  GETTING STARTED WITH JAVA
Listing 1-2. Greetings from Java with documentation comments
/**
A simple class for introducing a Java application.
@author Jeff Friesen
*/
class HelloWorld
{
/**
Application entry point
@param args array of command-line arguments passed to this method
*/
public static void main(String[] args)
{
System.out.println("Hello, world!");
}
}
We can extract these documentation comments into a set of HTML files by using the JDK’s javadoc
tool, as follows:
javadoc -private HelloWorld.java
javadoc defaults to generating HTML-based documentation for public classes and
public/protected members of these classes—you’ll learn about these concepts in Chapter 2. Because
HelloWorld is not public, specifying javadoc HelloWorld.java causes javadoc to complain that no public
or protected classes were found to document. The remedy is to specify javadoc’s -private commandline option.
javadoc responds by outputting the following messages:
Loading source file HelloWorld.java...
Constructing Javadoc information...
Standard Doclet version 1.7.0
Building tree for all the packages and classes...
Generating \HelloWorld.html...
Generating \package-frame.html...
Generating \package-summary.html...
Generating \package-tree.html...
Generating \constant-values.html...
Building index for all the packages and classes...
Generating \overview-tree.html...
Generating \index-all.html...
Generating \deprecated-list.html...
Building index for all classes...
Generating \allclasses-frame.html...
Generating \allclasses-noframe.html...
Generating \index.html...
Generating \help-doc.html...
14
CHAPTER 1  GETTING STARTED WITH JAVA
It also generates several files, including the index.html entry-point file. Point your browser to this
file and you should see a page similar to that shown in Figure 1-4.
Figure 1-4. The entry-point page into HelloWorld’s javadoc provides easy access to the documentation.
 Note JDK 7’s external documentation has a similar appearance and organization to Figure 1-4 because this
documentation was also generated by javadoc.
Identifiers
Source code entities such as classes and methods need to be named so that they can be referenced from
elsewhere in the code. Java provides the identifiers feature for this purpose.
An identifier consists of letters (A-Z, a-z, or equivalent uppercase/lowercase letters in other human
alphabets), digits (0-9 or equivalent digits in other human alphabets), connecting punctuation
characters (e.g., the underscore), and currency symbols (e.g., the dollar sign $). This name must begin
with a letter, a currency symbol, or a connecting punctuation character; and its length cannot exceed the
line in which it appears.
Examples of valid identifiers include i, counter, loop10, border$color and _char. Examples of invalid
identifiers include 50y (starts with a digit) and first#name (# is not a valid identifier symbol).
15
CHAPTER 1  GETTING STARTED WITH JAVA
 Note Java is a case-sensitive language, which means that identifiers differing only in case are considered
separate identifiers. For example, salary and Salary are separate identifiers.
Almost any valid identifier can be chosen to name a class, method, or other source code entity.
However, some identifiers are reserved for special purposes; they are known as reserved words. Java
reserves the following identifiers: abstract, assert, boolean, break, byte, case, catch, char, class, const,
continue, default, do, double, enum, else, extends, false, final, finally, float, for, goto, if, implements,
import, instanceof, int, interface, long, native, new, null, package, private, protected, public, return,
short, static, strictfp, super, switch, synchronized, this, throw, throws, transient, true, try, void,
volatile, and while. The compiler outputs an error message if you attempt to use any of these reserved
words outside of their usage contexts.
 Note Most of Java’s reserved words are also known as keywords. The three exceptions are false, null, and
true, which are examples of literals (values specified verbatim).
Types
Programs process different types of values such as integers, floating-point values, characters, and
strings. A type identifies a set of values (and their representation in memory) and a set of operations that
transform these values into other values of that set. For example, the integer type identifies numeric
values with no fractional parts and integer-oriented math operations, such as adding two integers to
yield another integer.
 Note Java is a strongly typed language, which means that every expression, variable, and so on has a type
known to the compiler. This capability helps the compiler detect type-related errors at compile time rather than
having these errors manifest themselves at runtime. Expressions and variables are discussed later in this chapter.
Java classifies types as primitive types, user-defined types, and array types.
Primitive Types
A primitive type is a type that is defined by the language and whose values are not objects. Java supports
the Boolean, character, byte integer, short integer, integer, long integer, floating-point, and double
precision floating-point primitive types. They are described in Table 1-1.
16
CHAPTER 1  GETTING STARTED WITH JAVA
Table 1-1. Primitive Types
Primitive Type
Reserved Word
Size
Min Value
Max Value
Boolean
boolean
--
--
--
Character
char
16-bit
Unicode 0
Unicode 216 - 1
Byte integer
byte
8-bit
-128
+127
Short integer
short
16-bit
-215
+215 - 1
Integer
int
32-bit
-231
+231 - 1
Long integer
long
64-bit
-2
63
+263 - 1
Floating-point
float
32-bit
IEEE 754
IEEE 754
Double precision floating-point
double
64-bit
IEEE 754
IEEE 754
Table 1-1 describes each primitive type in terms of its reserved word, size, minimum value, and
maximum value. A “--” entry indicates that the column in which it appears is not applicable to the
primitive type described in that entry’s row.
The size column identifies the size of each primitive type in terms of the number of bits (binary
digits—each digit is either 0 or 1) that a value of that type occupies in memory. Except for Boolean
(whose size is implementation dependent—one Java implementation might store a Boolean value in a
single bit, whereas another implementation might require an eight-bit byte for performance efficiency),
each primitive type’s implementation has a specific size.
The minimum value and maximum value columns identify the smallest and largest values that can
be represented by each type. Except for Boolean (whose only values are true and false), each primitive
type has a minimum value and a maximum value.
The minimum and maximum values of the character type refer to Unicode, which is a standard for
the consistent encoding, representation, and handling of text expressed in most of the world's writing
systems. Unicode was developed in conjunction with the Universal Character Set, a standard for
encoding the various symbols making up the world’s written languages. Unicode 0 is shorthand for “the
first Unicode code point”—a code point is an integer that represents a symbol (e.g., A) or a control
character (e.g., newline or tab), or that combines with other code points to form a symbol. Check out
Wikipedia’s “Unicode” entry (http://en.wikipedia.org/wiki/Unicode) to learn more about this
standard, and Wikipedia’s “Universal Character Set” entry
(http://en.wikipedia.org/wiki/Universal_Character_Set) to learn more about this standard.
 Note The character type’s limits imply that this type is unsigned (all character values are positive). In contrast,
each numeric type is signed (it supports positive and negative values).
17
CHAPTER 1  GETTING STARTED WITH JAVA
The minimum and maximum values of the byte integer, short integer, integer, and long integer
types reveal that there is one more negative value than positive value (0 is typically not regarded as a
positive value). The reason for this imbalance has to do with how integers are represented.
Java represents an integer value as a combination of a sign bit (the leftmost bit—0 for a positive
value and 1 for a negative value) and magnitude bits (all remaining bits to the right of the sign bit). If the
sign bit is 0, the magnitude is stored directly. However, if the sign bit is 1, the magnitude is stored using
twos-complement representation in which all 1s are flipped to 0s, all 0s are flipped to 1s, and 1 is added
to the result. Twos-complement is used so that negative integers can naturally coexist with positive
integers. For example, adding the representation of -1 to +1 yields 0. Figure 1-5 illustrates byte integer 2’s
direct representation and byte integer -2’s twos-complement representation.
Figure 1-5. The binary representation of two byte integer values begins with a sign bit.
The minimum and maximum values of the floating-point and double precision floating-point types
refer to IEEE 754, which is a standard for representing floating-point values in memory. Check out
Wikipedia’s “IEEE 754-2008” entry (http://en.wikipedia.org/wiki/IEEE_754) to learn more about this
standard.
 Note Developers who argue that Java should only support objects are not happy about the inclusion of primitive
types in the language. However, Java was designed to include primitive types to overcome the speed and memory
limitations of early 1990s-era devices, to which Java was originally targeted.
User-Defined Types
A user-defined type is a type that is defined by the developer using a class, an interface, an enum, or an
annotation type; and whose values are objects. For example, Java’s String class defines the string userdefined type; its values describe strings of characters, and its methods perform various string operations
such as concatenating two strings together. Chapter 2 discusses classes, interfaces, and methods.
Chapter 3 discusses enums and annotation types.
User-defined types are also known as reference types because a variable of that type stores a
reference (a memory address or some other identifier) to a region of memory that stores an object of that
type. In contrast, variables of primitive types store the values directly; they don’t store references to
these values.
Array Types
An array type is a special reference type that signifies an array, a region of memory that stores values in
equal-size and contiguous slots, which are commonly referred to as elements.
18
CHAPTER 1  GETTING STARTED WITH JAVA
This type consists of the element type (a primitive type or a user-defined type) and one or more
pairs of square brackets that indicate the number of dimensions (extents). A single pair of brackets
signifies a one-dimensional array (a vector), two pairs of brackets signify a two-dimensional array (a
table), three pairs of brackets signify a one-dimensional array of two-dimensional arrays (a vector of
tables), and so on. For example, int[] signifies a one-dimensional array (with int as the element type),
and double[][] signifies a two-dimensional array (with double as the element type).
Variables
This book was purchased by [email protected]
Programs manipulate values that are stored in memory, which is symbolically represented in source
code through the use of the variables feature. A variable is a named memory location that stores some
type of value. Variables that store references are often referred to as reference variables.
Variables must be declared before they are used. A declaration minimally consists of a type name,
optionally followed by a sequence of square bracket pairs, followed by a name, optionally followed by a
sequence of square bracket pairs, and terminated with a semicolon character (;). Consider the following
examples:
int counter;
double temperature;
String firstName;
int[] ages;
char gradeLetters[];
float[][] matrix;
The first example declares an integer variable named counter, the second example declares a double
precision floating-point variable named temperature, the third example declares a string variable named
firstName, the fourth example declares a one-dimensional integer array variable named ages, the fifth
example declares a one-dimensional character array variable named gradeLetters, and the sixth
example declares a two-dimensional floating-point array variable named matrix. No string is yet
associated with firstName, and no arrays are yet associated with ages, gradeLetters, and matrix.
 Caution Square brackets can appear after the type name or after the variable name, but not in both places. For
example, the compiler reports an error when it encounters int[] x[];. It is common practice to place the square
brackets after the type name (as in int[] ages;) instead of after the variable name (as in char
gradeLetters[];).
You can declare multiple variables on one line by separating each variable from its predecessor with
a comma, as demonstrated by the following example:
int x, y[], z;
This example declares three variables named x, y, and z. Each variable shares the same type, which
happens to be integer. Unlike x and z, which store single integer values, y[] signifies a one-dimensional
array whose element type is integer – each element stores an integer value. No array is yet associated
with y.
The square brackets must appear after the variable name when the array is declared on the same
line as the other variables. If you place the square brackets before the variable name, as in int x, []y,
19
CHAPTER 1  GETTING STARTED WITH JAVA
z;, the compiler reports an error. If you place the square brackets after the type name, as in int[] x, y,
z;, all three variables signify one-dimensional arrays of integers.
Expressions
The previously declared variables were not explicitly initialized to any values. As a result, they are either
initialized to default values (e.g., 0 for int and 0.0 for double) or remain uninitialized, depending upon
the contexts in which they appear (declared within classes or declared within methods). Chapter 2
discusses variable contexts in terms of fields, local variables, and parameters.
Java provides the expressions feature for initializing variables and for other purposes. An expression
is a combination of literals, variable names, method calls, and operators. At runtime, it evaluates to a
value whose type is referred to as the expression’s type. If the expression is being assigned to a variable,
the expression’s type must agree with the variable’s type; otherwise, the compiler reports an error.
Java classifies expressions as simple expressions and compound expressions.
Simple Expressions
A simple expression is a literal (a value expressed verbatim), a variable name (containing a value), or a
method call (returning a value). Java supports several kinds of literals: string, Boolean true and false,
character, integer, floating-point, and null.
 Note A method call that doesn’t return a value—the called method is known as a void method—is a special
kind of simple expression; for example, System.out.println("Hello, World!");. This standalone expression
cannot be assigned to a variable. Attempting to do so (as in int i = System.out.println("X");) causes the
compiler to report an error.
A string literal consists of a sequence of Unicode characters surrounded by a pair of double quotes;
for example, "The quick brown fox jumps over the lazy dog." It might also contain escape sequences,
which are special syntax for representing certain printable and nonprintable characters that otherwise
cannot appear in the literal. For example, "The quick brown \"fox\" jumps over the lazy dog." uses
the \" escape sequence to surround fox with double quotes.
Table 1-2 describes all supported escape sequences.
20
CHAPTER 1  GETTING STARTED WITH JAVA
Table 1-2. Escape Sequences
Escape Syntax
Description
\\
Backslash
\"
Double quote
\'
Single quote
\b
Backspace
\f
Form feed
\n
Newline (also
referred to as line
feed)
\r
Carriage return
\t
Horizontal tab
Finally, a string literal might contain Unicode escape sequences, which are special syntax for
representing Unicode characters. A Unicode escape sequence begins with \u and continues with four
hexadecimal digits (0-9, A-F, a-f) with no intervening space. For example, \u0041 represents capital letter
A, and \u20ac represents the European Union’s euro currency symbol.
A Boolean literal consists of reserved word true or reserved word false.
A character literal consists of a single Unicode character surrounded by a pair of single quotes ('A'
is an example). You can also represent, as a character literal, an escape sequence ('\'', for example) or a
Unicode escape sequence (e.g., '\u0041').
An integer literal consists of a sequence of digits. If the literal is to represent a long integer value, it
must be suffixed with an uppercase L or lowercase l (L is easier to read). If there is no suffix, the literal
represents a 32-bit integer (an int).
Integer literals can be specified in the decimal, hexadecimal, octal, and binary formats:
•
The decimal format is the default format; for example, 127.
•
The hexadecimal format requires that the literal begin with 0x or 0X and continue
with hexadecimal digits (0-9, A-F, a-f); for example, 0x7F.
•
The octal format requires that the literal be prefixed with 0 and continue with octal
digits (0-7); for example, 0177.
•
The binary format requires that the literal be prefixed with 0b or 0B and continue
with 0s and 1s; for example, 0b01111111.
To improve readability, you can insert underscores between digits; for example, 204_555_1212.
Although you can insert multiple successive underscores between digits (as in 0b1111__0000), you
cannot specify a leading underscore (as in _123) because the compiler would treat the literal as an
21
CHAPTER 1  GETTING STARTED WITH JAVA
identifier. Also, you cannot specify a trailing underscore (as in 123_). A floating-point literal consists of
an integer part, a decimal point (represented by the period character [.]), a fractional part, an exponent
(starting with letter E or e), and a type suffix (letter D, d, F, or f). Most parts are optional, but enough
information must be present to differentiate the floating-point literal from an integer literal. Examples
include 0.1 (double precision floating-point), 89F (floating-point), 600D (double precision floatingpoint), and 13.08E+23 (double precision floating-point). As with integer literals, you can make floatingpoint literals easier to read by placing underscores between digits (3.141_592_654, for example).
Finally, the null literal is assigned to a reference variable to indicate that the variable does not refer
to an object.
The following examples use literals to initialize the previously presented variables:
int counter = 10;
double temperature = 98.6; // Assume Fahrenheit scale.
String firstName = "Mark";
int[] ages = { 52, 28, 93, 16 };
char gradeLetters[] = { 'A', 'B', 'C', 'D', 'F' };
float[][] matrix = { { 1.0F, 2.0F, 3.0F }, { 4.0F, 5.0F, 6.0F }};
int x = 1, y[] = { 1, 2, 3 }, z = 3;
The last four examples use array initializers to initialize the ages, gradeletters, matrix, and y arrays.
An array initializer consists of a brace-and-comma-delimited list of expressions, which (as the matrix
example shows) may themselves be array initializers. The matrix example results in a table that looks
like the following:
1.0F 2.0F 3.0F
4.0F 5.0F 6.0F
ORGANIZING VARIABLES IN MEMORY
Perhaps you’re curious about how variables are organized in memory. Figure 1-6 presents one possible
high-level organization for the counter, ages, and matrix variables, along with the arrays assigned to
ages and matrix.
Figure 1-6. The counter variable stores a four-byte integer value, whereas ages and matrix store four-byte
references to their respective arrays.
Figure 1-6 reveals that each of counter, ages, and matrix is stored at a memory address (starting at a
fictitious 20001000 value in this example) and divisible by four (each variable stores a four-byte value),
that counter’s four-byte value is stored at this address, and that each of the ages and matrix four-byte
22
CHAPTER 1  GETTING STARTED WITH JAVA
memory locations stores the 32-bit address of its respective array (64-bit addresses would most likely be
used on 64-bit JVMs). Also, a one-dimensional array is stored as a list of values, whereas a twodimensional array is stored as a one-dimensional row array of addresses, where each address identifies a
one-dimensional column array of values for that row.
Although Figure 1-6 implies that array addresses are stored in ages and matrix, which equates references
with addresses, a Java implementation might equate references with handles (integer values that identify
slots in a list). This alternative is presented in Figure 1-7 for ages and its referenced array.
Figure 1-7. A handle is stored in ages, and the list entry identified by this handle stores the address of the
associated array.
Handles make it easy to move around regions of memory during garbage collection (discussed in Chapter
2). If multiple variables referenced the same array via the same address, each variable’s address value
would have to be updated when the array was moved. However, if multiple variables referenced the array
via the same handle, only the handle’s list entry would need to be updated. A downside to using handles is
that accessing memory via these handles can be slower than directly accessing this memory via an
address. Regardless of how references are implemented, this implementation detail is hidden from the
Java developer in order to promote portability.
The following example shows a simple expression where one variable is assigned the value of
another variable:
int counter1 = 1;
int counter2 = counter1;
Finally, the following example shows a simple expression that assigns the result of a method call to a
variable named isLeap:
boolean isLeap = isLeapYear(2011);
The previous examples have assumed that only those expressions whose types are the same as the
types of the variables that they are initializing can be assigned to those variables. However, under certain
circumstances, it’s possible to assign an expression having a different type. For example, Java permits
you to assign certain integer literals to short integer variables, as in short s = 20;, and assign a short
integer expression to an integer variable, as in int i = s;.
Java permits the former assignment because 20 can be represented as a short integer (no
information is lost). In contrast, Java would complain about short s = 40000; because integer literal
40000 cannot be represented as a short integer (32767 is the maximum positive integer that can be stored
in a short integer variable). Java permits the latter assignment because no information is lost when Java
converts from a type with a smaller set of values to a type with a wider set of values.
Java supports the following primitive type conversions via widening conversion rules:
23
CHAPTER 1  GETTING STARTED WITH JAVA
•
Byte integer to short integer, integer, long integer, floating-point, or double
precision floating-point
•
Short integer to integer, long integer, floating-point, or double precision floatingpoint
•
Character to integer, long integer, floating-point, or double precision floatingpoint
•
Integer to long integer, floating-point, or double precision floating-point
•
Long integer to floating-point or double precision floating-point
•
Floating-point to double precision floating-point
 Note When converting from a smaller integer to a larger integer, Java copies the smaller integer’s sign bit into
the extra bits of the larger integer.
Chapter 2 discusses the widening conversion rules for performing type conversions in the context of
user-defined and array types.
Compound Expressions
A compound expression is a sequence of simple expressions and operators, where an operator (a
sequence of instructions symbolically represented in source code) transforms its operand expression
value(s) into another value. For example, -6 is a compound expression consisting of operator - and
integer literal 6 as its operand. This expression transforms 6 into its negative equivalent. Similarly, x+5 is
a compound expression consisting of variable name x, integer literal 5, and operator + sandwiched
between these operands. Variable x’s value is fetched and added to 5 when this expression is evaluated.
The sum becomes the value of the expression.
 Note If x’s type is byte integer or short integer, this variable’s value is widened to an integer. However, if x’s
type is long integer, floating-point, or double precision floating-point, 5 is widened to the appropriate type. The
addition operation is performed after the widening conversion takes place.
Java supplies a wide variety of operators that are classified by the number of operands they take. A
unary operator takes only one operand (unary minus [-] is an example), a binary operator takes two
operands (addition [+] is an example), and Java’s single ternary operator (conditional [?:]) takes three
operands.
Operators are also classified as prefix, postfix, and infix. A prefix operator is a unary operator that
precedes its operand (as in -6), a postfix operator is a unary operator that trails its operand (as in x++),
and an infix operator is a binary or ternary operator that is sandwiched between the binary operator’s
24
CHAPTER 1  GETTING STARTED WITH JAVA
two or the ternary operator’s three operands (as in x+5).Table 1-3 presents all supported operators in
terms of their symbols, descriptions, and precedence levels—the concept of precedence is discussed at
the end of this section. Various operator descriptions refer to “integer type,” which is shorthand for
specifying any of byte integer, short integer, integer, or long integer unless “integer type” is qualified as a
32-bit integer. Also, “numeric type” refers to any of these integer types along with floating-point and
double precision floating-point.
Table 1-3. Operators
Operator
Symbol
Description
Precedence
Addition
+
Given operand1 + operand2, where each operand
must be o f character or numeric type, add
operand2 to operand1 and return the sum.
10
Array index
[]
Given variable[index], where index must be of
integer type, read value from or store value into
variable’s storage element at location index.
13
Assignment
=
Given variable = operand, which must be
assignment-compatible (their types must agree),
store operand in variable.
0
Bitwise AND
&
Given operand1 & operand2, where each operand
must be of character or in teger type, bitwise
AND their correspo nding bits and return the
result. A result bit is set to 1 if each o perand’s
corresponding bit is 1. Otherwise, the result bit is
set to 0.
6
Bitwise
complement
~
Given ~operand, where operand must be of
character or integer type, flip operand’s bits (1s to
0s and 0s to 1s) and return the result.
12
Bitwise
exclusive OR
^
Given operand1 ^ operand2, where each operand
must be of character or in teger type, bitwise
exclusive OR their corresponding bits and return
the result. A result bit is set to 1 if one operand’s
corresponding bit is 1 and t he other oper and’s
corresponding bit is 0. Otherwise, the result bit is
set to 0.
5
Bitwise
inclusive OR
|
Given operand1 | operand2, which must be of
character or integer type, bitwis e inclusive OR
their corresponding bits and return the res ult. A
result bit is set to 1 if eith er (or both) of the
operands’ corresponding bits is 1. Otherwise, the
result bit is set to 0.
4
25
CHAPTER 1  GETTING STARTED WITH JAVA
Cast
26
(type)
Given (type) operand, convert operand to an
equivalent value that can be represented by
type. For example, you could use this operator to
convert a floating-point value to a 32-bit integer
value.
12
Compound
assignment
+=, -=, *=,
/=, %=, &=,
|=, ^=, <<=,
>>=, >>>=
Given variable operator operand, where
operator is one of the listed compound operator
symbols, and where operand is assignmentcompatible with variable, perform the indicated
operation using variable’s value as operator’s
left operand value, and store the resulting value
in variable.
0
Conditional
?:
Given operand1 ? operand2 : operand3, where
operand1 must be of Boolean type, re turn
operand2 if operand1 is true or operand3 if
operand1 is false. The types of
operand2 and
operand3 must agree.
1
Conditional
AND
&&
Given operand1 && operand2, where each
operand must be of Boolea n type, r eturn true if
both operands are true. Ot herwise, return false.
If operand1 is false, operand2 is not examined.
This is known as short-circuiting.
3
Conditional OR
||
Given operand1 || operand2, where each
operand must be of Boolea n type, r eturn true if
at least one operand is true. Otherwise, return
false. If operand1 is true, operand2 is not
examined. This is known as short-circuiting.
2
Division
/
Given operand1 / operand2, where each operand
must be o f character or numeric type, divide
operand1 by operand2 and return the quotient.
11
Equality
==
Given operand1 == operand2, where both
operands must be co mparable (you cannot
compare an integer with a string li teral, for
example), compare both operands for equality.
Return true if thes e operands are equa l.
Otherwise, return false.
7
Inequality
!=
Given operand1 != operand2, where both
operands must be co mparable (you cannot
compare an integer with a string li teral, for
example), compare both operands for inequality.
Return true if these operands are not equal.
7
CHAPTER 1  GETTING STARTED WITH JAVA
Otherwise, return false.
Left shift
<<
Given operand1 << operand2, where each
operand must be o f character or integer type,
shift operand1’s binary representation left by the
number of bits that operand2 specifies. For ea ch
shift, a 0 is shifted into the rightmost bit and the
leftmost bit is discarded. Only the f ive low-order
bits of operand2 are used wh en shifting a 32-bit
integer (to prevent shifting more than the
number of bits in a 32- bit integer). Only the si x
low-order bits of operand2 are used when
shifting a 64-bit integer (to prevent shifting more
than the number of bits in a 64-bit integer). The
shift preserves negative values. Furthermore, it is
equivalent to (but faster than) multiplying by a
multiple of 2.
9
Logical AND
&
Given operand1 & operand2, where each operand
must be of Boolea n type, return true if both
operands are true. Otherwise, return false. In
contrast to conditional AND, logical AND does
not perform short-circuiting.
6
Logical
complement
!
Given !operand, where operand must be of
Boolean type, flip operand’s value (true to false or
false to true) and return the result.
12
Logical
exclusive OR
^
Given operand1 ^ operand2, where each operand
must be of Boolea n type, return true if one
operand is true and t he other operand is false.
Otherwise, return false.
5
Logical
inclusive OR
|
Given operand1 | operand2, where each operand
must be of Boolea n type, return true if a t least
one operand is true. Otherwise, return false. In
contrast to conditional OR, logical inclusive OR
does not perform short-circuiting.
4
Member access
.
Given identifier1.identifier2, access
identifier2 member of identifer1.
the
13
Method call
()
Given identifier(argument list), call the
method identified by identifier and matching
parameter list.
13
Multiplication
*
Given operand1 * operand2, where each operand
must be o f character or numeric type, multiply
11
27
CHAPTER 1  GETTING STARTED WITH JAVA
operand1 by operand2 and return the product.
28
Object creation
new
Given new identifier(argument list), allocate
memory for object and
call constructor
(discussed in C hapter 2) specified as
identifier(argument
list).
Given
new
identifier[integer size], allocate a onedimensional array of values.
12
Postdecrement
--
Given variable--, where variable must be of
character or numeric type, subtract 1 from
variable’s value (storing the res ult in variable)
and return the original value.
13
Postincrement
++
Given variable++, where variable must be of
character or numeric type, add 1 to variable’s
value (storing the result in variable) and return
the original value.
13
Predecrement
--
Given --variable, where variable must be of
character or numeric type, subtract 1 fro m its
value, store the result in variable, and return
this value.
12
Preincrement
++
Given ++variable, where variable must be of
character or num eric type, a dd 1 to its value,
store the result in variable, and return this
value.
12
Relational
greater than
>
Given operand1 > operand2, where each operand
must be o f character or numeric type, return
true if operand1 is greater than operand2.
Otherwise, return false.
8
Relational
greater than or
equal to
>=
Given operand1 >= operand2, where each
operand must be o f character or numeric type,
return true if operand1 is greater than or equal to
operand2. Otherwise, return false.
8
Relational less
than
<
Given operand1 < operand2, where each operand
must be o f character or numeric type, return
true if operand1 is less t han operand2. Otherwise,
return false.
8
Relational less
than or equal to
<=
Given operand1 <= operand2, where each
operand must be o f character or numeric type,
return true if operand1 is less than or equal to
operand2. Otherwise, return false.
8
CHAPTER 1  GETTING STARTED WITH JAVA
instanceof
Given operand1 instanceof operand2, where
operand1 is an object and operand2 is a class (o r
other user-defined type), return true if operand1
is an instance of operand2. Otherwise, return
false.
8
Remainder
%
Given operand1 % operand2, where each operand
must be o f character or numeric type, divide
operand1 by operand2 and return the remainder.
11
Signed right
shift
>>
Given operand1 >> operand2, where each
operand must be o f character or integer type,
shift operand1’s binary repre sentation right by
the number of bits that operand2 specifies. For
each shift, a copy of the sign bit (the leftmost bit)
is shifted to t he right a nd the rightmost bit is
discarded. Only the fi ve low-order b its of
operand2 are used when shifting a 32-bit integer
(to prevent s hifting more than the num ber of
bits in a 32-bit integer). Only the s ix low-order
bits of operand2 are used wh en shifting a 64-bit
integer (to prevent shifting more than the
number of bi ts in a 64-bit integer). The shift
preserves negative values. Furthermore, it is
equivalent to (but fast er than) dividing by a
multiple of 2.
9
String
concatenation
+
Given operand1 + operand2, where at least one
operand is of String type, append operand2’s
string representation to operand1’s string
representation and return the concatenated
result.
10
Subtraction
-
Given operand1 - operand2, where each operand
must be o f character or numeric type, subtract
operand2 from operand1 and return the
difference.
10
Unary minus
-
Given -operand, where operand must be of
character or numeric type, return operand’s
arithmetic negative.
12
Unary plus
+
Like its predecessor, but return operand. Rarely
used.
12
>>>
Given operand1 >>> operand2, where each
operand must be o f character or integer type,
shift operand1’s binary repre sentation right by
the number of bits that operand2 specifies. For
9
This book was purchased by [email protected]
Relational type
checking
Unsigned right
shift
29
CHAPTER 1  GETTING STARTED WITH JAVA
each shift, a zero is shifted into the leftm ost bit
and the rightmost bit is discarded. Only the five
low-order bits of operand2 are used when
shifting a 32-bit integer (to prevent shifting more
than the number of bits in a 32-bit integer). Only
the six low-order bits of operand2 are used when
shifting a 64-bit integer (to prevent shifting more
than the number of bits in a 64-bit integer). The
shift does not pres erve negative values.
Furthermore, it is equivalent to (but faster than)
dividing by a multiple of 2.
Table 1-3’s operators can be classified as additive, array index, assignment, bitwise, cast,
conditional, equality, logical, member access, method call, multiplicative, object creation, relational,
shift, and unary minus/plus.
Additive Operators
The additive operators consist of addition (+), subtraction (-), postdecrement (--), postincrement (++),
predecrement (--), preincrement (++), and string concatenation (+). Addition returns the sum of its
operands (e.g., 6+4 returns 10), subtraction returns the difference between its operands (e.g., 6-4 returns
2 and 4-6 returns 2), postdecrement subtracts one from its variable operand and returns the variable’s
prior value (e.g., x--), postincrement adds one to its variable operand and returns the variable’s prior
value (e.g., x++), predecrement subtracts one from its variable operand and returns the variable’s new
value (e.g., --x), preincrement adds one to its variable operand and returns the variable’s new value (e.g.,
++x), and string concatenation merges its string operands and returns the merged string (e.g., "A"+"B"
returns "AB").
The addition, subtraction, postdecrement, postincrement, predecrement, and preincrement
operators can yield values that overflow or underflow the limits of the resulting value’s type. For
example, adding two large positive 32-bit integer values can produce a value that cannot be represented
as a 32-bit integer value. The result is said to overflow. Java does not detect overflows and underflows.
Java provides a special widening conversion rule for use with string operands and the string
concatenation operator. If either operand is not a string, the operand is first converted to a string prior to
string concatenation. For example, when presented with "A"+5, the compiler generates code that first
converts 5 to "5" and then performs the string concatenation operation, resulting in "A5".
Array Index Operator
The array index operator ([]) accesses an array element by presenting the location of that element as an
integer index. This operator is specified after an array variable’s name; for example, ages[0].
Indexes are relative to 0, which implies that ages[0] accesses the first element, whereas ages[6]
accesses the seventh element. The index must be greater than or equal to 0 and less than the length of
the array; otherwise, the JVM throws ArrayIndexOutOfBoundsException (consult Chapter 3 to learn about
exceptions).
An array’s length is returned by appending “.length” to the array variable. For example,
ages.length returns the length of (the number of elements in) the array that ages references. Similarly,
matrix.length returns the number of row elements in the matrix two-dimensional array, whereas
matrix[0].length returns the number of column elements assigned to the first row element of this array.
30
CHAPTER 1  GETTING STARTED WITH JAVA
Assignment Operators
The assignment operator (=) assigns an expression’s result to a variable (as in int x = 4;). The types of
the variable and expression must agree; otherwise, the compiler reports an error.
Java also supports several compound assignment operators that perform a specific operation and
assign its result to a variable. For example, the += operator evaluates the numeric expression on its right
and adds the result to the contents of the variable on its left. The other compound assignment operators
behave in a similar way.
Bitwise Operators
The bitwise operators consist of bitwise AND (&), bitwise complement (~), bitwise exclusive OR (^), and
bitwise inclusive OR (|). These operators are designed to work on the binary representations of their
character or integral operands. Because this concept can be hard to understand if you haven’t previously
worked with these operators in another language, the following example demonstrates these operators:
~0B00000000000000000000000010110101 results in 11111111111111111111111101001010
0B00011010&0B10110111 results in 00000000000000000000000000010010
0B00011010^0B10110111 results in 00000000000000000000000010101101
0B00011010|0B10110111 results in 00000000000000000000000010111111
The &, ^, and | operators in the last three lines first convert their byte integer operands to 32-bit
integer values (through sign bit extension, copying the sign bit’s value into the extra bits) before
performing their operations.
Cast Operator
The cast operator—(type)—attempts to convert the type of its operand to type. This operator exists
because the compiler will not allow you to convert a value from one type to another in which
information will be lost without specifying your intention do so (via the cast operator). For example,
when presented with short s = 1.65+3;, the compiler reports an error because attempting to convert a
double precision floating-point value to a short integer results in the loss of the fraction .65—s would
contain 4 instead of 4.65.
Recognizing that information loss might not always be a problem, Java permits you to explicitly
state your intention by casting to the target type. For example, short s = (short) 1.65+3; tells the
compiler that you want 1.65+3 to be converted to a short integer, and that you realize that the fraction
will disappear.
The following example provides another demonstration of the need for a cast operator:
char c = 'A';
byte b = c;
The compiler reports an error about loss of precision when it encounters byte b = c;. The reason is
that c can represent any unsigned integer value from 0 through 65535, whereas b can only represent a
signed integer value from -128 through +127. Even though 'A' equates to +65, which can fit within b’s
range, c could just have easily been initialized to '\u0323', which would not fit.
The solution to this problem is to introduce a (byte) cast, as follows, which causes the compiler to
generate code to cast c’s character type to byte integer:
byte b = (byte) c;
Java supports the following primitive type conversions via cast operators:
31
CHAPTER 1  GETTING STARTED WITH JAVA
•
Byte integer to character
•
Short integer to byte integer or character
•
Character to byte integer or short integer
•
Integer to byte integer, short integer, or character
•
Long integer to byte integer, short integer, character, or integer
•
Floating-point to byte integer, short integer, character, integer, or long integer
•
Double precision floating-point to byte integer, short integer, character, integer,
long integer, or floating-point
A cast operator is not always required when converting from more to fewer bits, and where no data
loss occurs. For example, when it encounters byte b = 100;, the compiler generates code that assigns
integer 100 to byte integer variable b because 100 can easily fit into the 8-bit storage location assigned to
this variable.
Conditional Operators
The conditional operators consist of conditional AND (&&), conditional OR (||), and conditional (?:). The
first two operators always evaluate their left operand (a Boolean expression that evaluates to true or
false) and conditionally evaluate their right operand (another Boolean expression). The third operator
evaluates one of two operands based upon a third Boolean operand.
Conditional AND always evaluates its left operand and evaluates its right operand only when its left
operand evaluates to true. For example, age > 64 && stillWorking first evaluates age > 64. If this
subexpression is true, stillWorking is evaluated, and its true or false value (stillWorking is a Boolean
variable) serves as the value of the overall expression. If age > 64 is false, stillWorking is not evaluated.
Conditional OR always evaluates its left operand and evaluates its right operand only when its left
operand evaluates to false. For example, value < 20 || value > 40 first evaluates value < 20. If this
subexpression is false, value > 40 is evaluated, and its true or false value serves as the overall
expression’s value. If value < 20 is true, value > 40 is not evaluated.
Conditional AND and conditional OR boost performance by preventing the unnecessary evaluation
of subexpressions, which is known as short-circuiting. For example, if its left operand is false, there is no
way that conditional AND’s right operand can change the fact that the overall expression will evaluate to
false.
If you aren’t careful, short-circuiting can prevent side effects (the results of subexpressions that
persist after the subexpressions have been evaluated) from executing. For example, age > 64 &&
++numEmployees > 5 increments numEmployees for only those employees whose ages are greater than 64.
Incrementing numEmployees is an example of a side effect because the value in numEmployees persists after
the subexpression ++numEmployees > 5 has evaluated.
The conditional operator is useful for making a decision by evaluating and returning one of two
operands based upon the value of a third operand. The following example converts a Boolean value to its
integer equivalent (1 for true and 0 for false):
boolean b = true;
int i = b ? 1 : 0; // 1 assigns to i
32
CHAPTER 1  GETTING STARTED WITH JAVA
Equality Operators
The equality operators consist of equality (==) and inequality (!=). These operators compare their
operands to determine whether they are equal or unequal. The former operator returns true when equal;
the latter operator returns true when unequal. For example, each of 2 == 2 and 2 != 3 evaluates to true,
whereas each of 2 == 4 and 4 != 4 evaluates to false.
When it comes to object operands (discussed in Chapter 2), these operators do not compare their
contents. For example, "abc" == "xyz" does not compare a with x. Instead, because string literals are
really String objects stored in memory (Chapter 4 discusses this concept further), == compares the
references to these objects.
Logical Operators
The logical operators consist of logical AND (&), logical complement (!), logical exclusive OR (^), and
logical inclusive OR (|). Although these operators are similar to their bitwise counterparts, whose
operands must be integer/character, the operands passed to the logical operators must be Boolean. For
example, !false returns true. Also, when confronted with age > 64 & stillWorking, logical AND
evaluates both subexpressions. This same pattern holds for logical exclusive OR and logical inclusive OR.
Member Access Operator
The member access operator (.) is used to access a class’s members or an object’s members. For
example, String s = "Hello"; int len = s.length(); returns the length of the string assigned to
variable s. It does so by calling the length() method member of the String class. Chapter 2 discusses this
topic in more detail.
Arrays are special objects that have a single length member. When you specify an array variable
followed by the member access operator, followed by length, the resulting expression returns the
number of elements in the array as a 32-bit integer. For example, ages.length returns the length of (the
number of elements in) the array that ages references.
Method Call Operator
The method call operator—()—is used to signify that a method (discussed in Chapter 2) is being called.
Furthermore, it identifies the number, order, and types of arguments that are passed to the method, to
be picked up by the method’s parameters. System.out.println("Hello"); is an example.
Multiplicative Operators
The multiplicative operators consist of multiplication (*), division (/), and remainder (%). Multiplication
returns the product of its operands (e.g., 6*4 returns 24), division returns the quotient of dividing its left
operand by its right operand (e.g., 6/4 returns 1), and remainder returns the remainder of dividing its left
operand by its right operand (e.g., 6%4 returns 2).
The multiplication, division, and remainder operators can yield values that overflow or underflow
the limits of the resulting value’s type. For example, multiplying two large positive 32-bit integer values
can produce a value that cannot be represented as a 32-bit integer value. The result is said to overflow.
Java does not detect overflows and underflows.
Dividing a numeric value by 0 (via the division or remainder operator) also results in interesting
behavior. Dividing an integer value by integer 0 causes the operator to throw an ArithmeticException
33
CHAPTER 1  GETTING STARTED WITH JAVA
object (Chapter 3 covers exceptions). Dividing a floating-point/double precision floating-point value by
0 causes the operator to return +infinity or -infinity, depending on whether the dividend is positive or
negative. Finally, dividing floating-point 0 by 0 causes the operator to return NaN (Not a Number).
Object Creation Operator
The object creation operator (new) creates an object from a class and also creates an array from an
initializer. These topics are discussed in Chapter 2.
Relational Operators
The relational operators consist of relational greater than (>), relational greater than or equal to (>=),
relational less than (<), relational less than or equal to (<=), and relational type checking (instanceof).
The former four operators compare their operands and return true if the left operand is (respectively)
greater than, greater than or equal to, less than, or less than or equal to the right operand. For example,
each of 5.0 > 3, 2 >= 2, 16.1 < 303.3, and 54.0 <= 54.0 evaluates to true.
The relational type-checking operator is used to determine whether an object belongs to a specific
type. This topic is discussed in Chapter 2.
Shift Operators
The shift operators consist of left shift (<<), signed right shift (>>), and unsigned right shift (>>>). Left shift
shifts the binary representation of its left operand leftward by the number of positions specified by its
right operand. Each shift is equivalent to multiplying by 2. For example, 2 << 3 shifts 2’s binary
representation left by 3 positions; the result is equivalent to multiplying 2 by 8.
Each of signed and unsigned right shift shifts the binary representation of its left operand rightward
by the number of positions specified by its right operand. Each shift is equivalent to dividing by 2. For
example, 16 >> 3 shifts 16’s binary representation right by 3 positions; the result is equivalent to
dividing 16 by 8.
The difference between signed and unsigned right shift is what happens to the sign bit during the
shift. Signed right shift includes the sign bit in the shift, whereas unsigned right shift ignores the sign bit.
As a result, signed right shift preserved negative numbers, but unsigned right shift does not. For
example, -4 >> 1 (the equivalent of -4/2) evaluates to -2, whereas –4 >>> 1 evaluates to 2147483646.
 Tip The shift operators are faster than multiplying or dividing by powers of 2.
Unary Minus/Plus Operators
Unary minus (-) and unary plus (+) are the simplest of all operators. Unary minus returns the negative of
its operand (such as -5 returns -5 and --5 returns 5), whereas unary plus returns its operand verbatim
(such as +5 returns 5 and +-5 returns -5). Unary plus is not commonly used, but is present for
completeness.
34
CHAPTER 1  GETTING STARTED WITH JAVA
Precedence and Associativity
When evaluating a compound expression, Java takes each operator’s precedence (level of importance)
into account to ensure that the expression evaluates as expected. For example, when presented with the
expression 60+3*6, we expect multiplication to be performed before addition (multiplication has higher
precedence than addition), and the final result to be 78. We do not expect addition to occur first, yielding
a result of 378.
 Note Table 1-3’s rightmost column presents a value that indicates an operator’s precedence: the higher the
number, the higher the precedence. For example, addition’s precedence level is 10 and multiplication’s
precedence level is 11, which means that multiplication is performed before addition.
Precedence can be circumvented by introducing open and close parentheses, ( and ), into the
expression, where the innermost pair of nested parentheses is evaluated first. For example, 2*((60+3)*6)
results in (60+3) being evaluated first, (60+3)*6 being evaluated next, and the overall expression being
evaluated last. Similarly, in the expression 60/(3-6), subtraction is performed before division.
During evaluation, operators with the same precedence level (e.g., addition and subtraction, which
both have level 10) are processed according to their associativity (a property that determines how
operators having the same precedence are grouped when parentheses are missing).
For example, expression 9*4/3 is evaluated as if it was (9*4)/3 because * and / are left-to-right
associative operators. In contrast, expression x=y=z=100 is evaluated as if it was x=(y=(z=100))—100 is
assigned to z, z’s new value (100) is assigned to y, and y’s new value (100) is assigned to x – because = is a
right-to-left associative operator.
Most of Java’s operators are left-to-right associative. Right-to-left associative operators include
assignment, bitwise complement, cast, compound assignment, conditional, logical complement, object
creation, predecrement, preincrement, unary minus, and unary plus.
 Note Unlike languages such as C++, Java doesn’t let you overload operators. However, Java overloads the +,
++, and -- operator symbols.
Statements
Statements are the workhorses of a program. They assign values to variables, control a program’s flow by
making decisions and/or repeatedly executing other statements, and perform other tasks. A statement
can be expressed as a simple statement or as a compound statement:
•
A simple statement is a single standalone source code instruction for performing
some task; it’s terminated with a semicolon.
35
CHAPTER 1  GETTING STARTED WITH JAVA
•
A compound statement is a (possibly empty) sequence of simple and other
compound statements sandwiched between open and close brace delimiters—a
delimiter is a character that marks the beginning or end of some section. A
method body (e.g., the main() method’s body) is an example. Compound
statements can appear wherever simple statements appear and are alternatively
referred to as blocks.
This section introduces you to many of Java’s statements. Additional statements are covered in later
chapters. For example, Chapter 2 discusses the return statement.
Assignment Statements
The assignment statement is an expression that assigns a value to a variable. This statement begins with a
variable name, continues with the assignment operator (=) or a compound assignment operator (such as
+=), and concludes with an expression and a semicolon. Below are three examples:
x = 10;
ages[0] = 25;
counter += 10;
The first example assigns integer 10 to variable x, which is presumably of type integer as well. The
second example assigns integer 25 to the first element of the ages array. The third example adds 10 to the
value stored in counter and stores the sum in counter.
 Note Initializing a variable in the variable’s declaration (e.g., int counter = 1;) can be thought of as a special
form of the assignment statement.
Decision Statements
The previously described conditional operator (?:) is useful for choosing between two expressions to
evaluate, and cannot be used to choose between two statements. For this purpose, Java supplies three
decision statements: if, if-else, and switch.
If Statement
The if statement evaluates a Boolean expression and executes another statement when this expression
evaluates to true. This statement has the following syntax:
if (Boolean expression)
statement
If consists of reserved word if, followed by a Boolean expression in parentheses, followed by a
statement to execute when Boolean expression evaluates to true.
The following example demonstrates this statement:
if (numMonthlySales > 100)
wage += bonus;
36
CHAPTER 1  GETTING STARTED WITH JAVA
If the number of monthly sales exceeds 100, numMonthlySales > 100 evaluates to true and the wage
+= bonus; assignment statement executes. Otherwise, this assignment statement does not execute.
If-Else Statement
The if-else statement evaluates a Boolean expression and executes one of two statements depending on
whether this expression evaluates to true or false. This statement has the following syntax:
if (Boolean expression)
statement1
else
statement2
If-else consists of reserved word if, followed by a Boolean expression in parentheses, followed by a
statement1 to execute when Boolean expression evaluates to true, followed by a statement2 to execute
when Boolean expression evaluates to false.
The following example demonstrates this statement:
if ((n&1) == 1)
System.out.println("odd");
else
System.out.println("even");
This example assumes the existence of an int variable named n that has been initialized to an
integer. It then proceeds to determine whether the integer is odd (not divisible by 2) or even (divisible by
2).
The Boolean expression first evaluates n&1, which bitwise ANDs n’s value with 1. It then compares
the result to 1. If they are equal, a message stating that n’s value is odd outputs; otherwise, a message
stating that n’s value is even outputs.
The parentheses are required because == has higher precedence than &. Without these parentheses,
the expression’s evaluation order would change to first evaluating 1 == 1 and then trying to bitwise AND
the Boolean result with n’s integer value. This order results in a compiler error message because of a type
mismatch: you cannot bitwise AND an integer with a Boolean value.
You could rewrite this if-else statement example to use the conditional operator, as follows:
System.out.println((n&1) == 1 ? "odd" : "even");. However, you cannot do so with the following
example:
if ((n&1) == 1)
odd();
else
even();
This example assumes the existence of odd() and even() methods that don’t return anything.
Because the conditional operator requires that each of its second and third operands evaluates to a
value, the compiler reports an error when attempting to compile (n&1) == 1 ? odd() : even().
You can chain multiple if-else statements together, resulting in the following syntax:
if (Boolean expression1)
statement1
else
if (Boolean expression2)
statement2
else
37
CHAPTER 1  GETTING STARTED WITH JAVA
…
else
statementN
If Boolean expression1 evaluates to true, statement1 executes. Otherwise, if Boolean expression2
evaluates to true, statement2 executes. This pattern continues until one of these expressions evaluates to
true and its corresponding statement executes, or the final else is reached and statementN (the default
statement) executes.
The following example demonstrates this chaining:
if (testMark >= 90)
{
gradeLetter = 'A';
System.out.println("You aced the test.");
}
else
if (testMark >= 80)
{
gradeLetter = 'B';
System.out.println("You did very well on this test.");
}
else
if (testMark >= 70)
{
gradeLetter = 'C';
System.out.println("Not bad, but you need to study more for future tests.");
}
else
if (testMark >= 60)
{
gradeLetter = 'D';
System.out.println("Your test result suggests that you need a tutor.");
else
{
gradeLetter = 'F';
System.out.println("Your test result is pathetic; you need summer school.");
}
DANGLING-ELSE PROBLEM
When if and if-else are used together, and the source code is not properly indented, it can be difficult to
determine which if associates with the else. For example:
if (car.door.isOpen())
if (car.key.isPresent())
car.start();
else car.door.open();
38
CHAPTER 1  GETTING STARTED WITH JAVA
Did the developer intend for the else to match the inner if, but improperly formatted the code to make it
appear otherwise? For example:
if (car.door.isOpen())
if (car.key.isPresent())
car.start();
else
car.door.open();
If car.door.isOpen() and car.key.isPresent() each return true, car.start() executes. If
car.door.isOpen() returns true and car.key.isPresent() returns false, car.door.open(); executes.
Attempting to open an open door makes no sense.
This book was purchased by [email protected]
The developer must have wanted the else to match the outer if, but forgot that else matches the nearest if.
This problem can be fixed by surrounding the inner if with braces, as follows:
if (car.door.isOpen())
{
if (car.key.isPresent())
car.start();
}
else
car.door.open();
When car.door.isOpen() returns true, the compound statement executes. When this method returns
false, car.door.open(); executes, which makes sense.
Forgetting that else matches the nearest if and using poor indentation to obscure this fact is known as the
dangling-else problem.
Switch Statement
The switch statement lets you choose from among several execution paths in a more efficient manner
than with equivalent chained if-else statements. This statement has the following syntax:
switch (selector expression)
{
case value1: statement1 [break;]
case value2: statement2 [break;]
…
case valueN: statementN [break;]
[default: statement]
}
Switch consists of reserved word switch, followed by a selector expression in parentheses,
followed by a body of cases. The selector expression is any expression that evaluates to an integer,
character, or string value. For example, it might evaluate to a 32-bit integer or to a 16-bit character.
39
CHAPTER 1  GETTING STARTED WITH JAVA
Each case begins with reserved word case, continues with a literal value and a colon character (:),
continues with a statement to execute, and optionally concludes with a break statement, which causes
execution to continue after the switch statement.
After evaluating the selector expression, switch compares this value with each case’s value until it
finds a match. If there is a match, the case’s statement is executed. For example, if the selector
expression’s value matches value1, statement1 executes.
The optional break statement (anything placed in square brackets is optional), which consists of
reserved word break followed by a semicolon, prevents the flow of execution from continuing with the
next case’s statement. Instead, execution continues with the first statement following switch.
 Note You will usually place a break statement after a case’s statement. Forgetting to include break can lead to
a hard-to-find bug. However, there are situations where you want to group several cases together and have them
execute common code. In such a situation, you would omit the break statement from the participating cases.
If none of the cases’ values match the selector expression’s value, and if a default case (signified by
the default reserved word followed by a colon) is present, the default case’s statement is executed.
The following example demonstrates this statement:
switch (direction)
{
case 0: System.out.println("You are travelling
case 1: System.out.println("You are travelling
case 2: System.out.println("You are travelling
case 3: System.out.println("You are travelling
default: System.out.println("You are lost.");
}
north."); break;
east."); break;
south."); break;
west."); break;
This example assumes that direction stores an integer value. If this value is in the range 0-3, an
appropriate direction message is output; otherwise, a message about being lost is output.
 Note This example hardcodes values 0, 1, 2, and 3, which is not a good idea in practice. Instead, constants
should be used. Chapter 2 introduces you to constants.
Loop Statements
It’s often necessary to repeatedly execute a statement, and this repeated execution is called a loop. Java
provides three kinds of loop statements: for, while, and do-while. This section first discusses these
statements. It then examines the topic of looping over the empty statement. Finally, the section
discusses the break, labeled break, continue, and labeled continue statements for prematurely ending all
or part of a loop.
40
CHAPTER 1  GETTING STARTED WITH JAVA
For Statement
The for statement lets you loop over a statement a specific number of times, or even indefinitely. This
statement has the following syntax:
for ([initialize]; [test]; [update])
statement
For consists of reserved word for, followed by a header in parentheses, followed by a statement to
execute. The header consists of an optional initialize section, followed by an optional test section,
followed by an optional update section. A nonoptional semicolon separates each of the first two sections
from the next section.
The initialize section consists of a comma-separated list of variable declarations or variable
assignments. Some or all of these variables are typically used to control the loop’s duration, and are
known as loop-control variables.
The test section consists of a Boolean expression that determines how long the loop executes.
Execution continues as long as this expression evaluates to true.
Finally, the update section consists of a comma-separated list of expressions that typically modify
the loop-control variables.
For is perfect for iterating (looping) over an array. Each iteration (loop execution) accesses one of
the array’s elements via an array[index] expression, where array is the array whose element is being
accessed, and index is the zero-based location of the element being accessed.
The following example uses the for statement to iterate over the array of command-line arguments
that is passed to the main() method:
public static void main(String[] args)
{
for (int i = 0; i < args.length; i++)
switch (args[i])
{
case "-v":
case "-V": System.out.println("version 1.0");
break;
default : showUsage();
}
}
For’s initialization section declares variable i for controlling the loop, its test section compares i’s
current value to the length of the args array to ensure that this value is less than the array’s length, and
its update section increments i by 1. The loop continues until i’s value equals the array’s length.
Each iteration accesses one of the array’s values via the args[i] expression. This expression returns
this array’s ith value (which happens to be a String object in this example). The first value is stored in
args[0].
The args[i] expression serves as the switch statement’s selector expression. If this String object
contains -V, the second case is executed, which calls System.out.println() to output a version number
message. The subsequent break statement keeps execution from falling into the default case, which calls
showUsage() to output usage information when main() is called with unexpected arguments.
If this String object contains -v, the lack of a break statement following the first case causes
execution to fall through to the second case, calling System.out.println(). This example demonstrates
the occasional need to group cases to execute common code.
41
CHAPTER 1  GETTING STARTED WITH JAVA
 Note Although I’ve named the array containing command-line arguments args, this name isn’t mandatory. I
could as easily have named it arguments (or even some_other_name).
The following example uses the for statement to output the contents of the previously declared
matrix array, which is redeclared here for convenience:
float[][] matrix = { { 1.0F, 2.0F, 3.0F }, { 4.0F, 5.0F, 6.0F }};
for (int row = 0; row < matrix.length; row++)
{
for (int col = 0; col < matrix[row].length; col++)
System.out.print(matrix[row][col]+" ");
System.out.print("\n");
}
Expression matrix.length returns the number of rows in this tabular array. For each row, expression
matrix[row].length returns the number of columns for that row. This latter expression suggests that
each row can have a different number of columns, although each row has the same number of columns
in the example.
System.out.print() is closely related to System.out.println(). Unlike the latter method,
System.out.print() outputs its argument without a trailing newline.
This example generates the following output:
1.0 2.0 3.0
4.0 5.0 6.0
While Statement
The while statement repeatedly executes a statement while its Boolean expression evaluates to true. This
statement has the following syntax:
while (Boolean expression)
statement
While consists of reserved word while, followed by a parenthesized Boolean expression header,
followed by a statement to repeatedly execute.
The while statement first evaluates the Boolean expression. If it is true, while executes the other
statement. Once again, the Boolean expression is evaluated. If it is still true, while re-executes the
statement. This cyclic pattern continues.
Prompting the user to enter a specific character is one situation where while is useful. For example,
suppose that you want to prompt the user to enter a specific uppercase letter or its lowercase equivalent.
The following example provides a demonstration:
int ch = 0;
while (ch != 'C' && ch != 'c')
{
System.out.println("Press C or c to continue.");
ch = System.in.read();
}
42
CHAPTER 1  GETTING STARTED WITH JAVA
This example begins by initializing variable ch. This variable must be initialized; otherwise, the
compiler will report an uninitialized variable when it tries to read ch’s value in the while statement’s
Boolean expression.
This expression uses the conditional AND operator (&&) to test ch’s value. This operator first
evaluates its left operand, which happens to be expression ch != 'C'. (The != operator converts 'C' from
16-bit unsigned char type to 32-bit signed int type prior to the comparison.)
If ch does not contain C (it does not at this point—0 was just assigned to ch), this expression
evaluates to true.
The && operator next evaluates its right operand, which happens to be expression ch != 'c'.
Because this expression also evaluates to true, conditional AND returns true and while executes the
compound statement.
The compound statement first outputs, via the System.out.println() method call, a message that
prompts the user to press the C key with or without the Shift key. It next reads the entered keystroke via
System.in.read(), saving its integer value in ch.
From left to right, System identifies a standard class of system utilities, in identifies an object located
in System that provides methods for inputting one or more bytes from the standard input device, and
read() returns the next byte (or -1 when there are no more bytes).
Following this assignment, the compound statement ends and while re-evaluates its Boolean
expression.
Suppose ch contains C’s integer value. Conditional AND evaluates ch != 'C', which evaluates to
false. Seeing that the expression is already false, conditional AND short circuits its evaluation by not
evaluating its right operand, and returns false. The while statement subsequently detects this value and
terminates.
Suppose ch contains c’s integer value. Conditional AND evaluates ch != 'C', which evaluates to
true. Seeing that the expression is true, conditional AND evaluates ch != 'c', which evaluates to false.
Once again, the while statement terminates.
43
CHAPTER 1  GETTING STARTED WITH JAVA
 Note A for statement can be coded as a while statement. For example,
for (int i = 0; i < 10; i++)
System.out.println(i);
is equivalent to
int i = 0;
while (i < 10)
{
System.out.println(i);
i++;
}
Do-While Statement
The do-while statement repeatedly executes a statement while its Boolean expression evaluates to true.
Unlike the while statement, which evaluates the Boolean expression at the top of the loop, do-while
evaluates the Boolean expression at the bottom of the loop. This statement has the following syntax:
do
statement
while(Boolean expression);
Do-while consists of the do reserved word, followed by a statement to repeatedly execute, followed
by the while reserved word, followed by a parenthesized Boolean expression header, followed by a
semicolon.
The do-while statement first executes the other statement. It then evaluates the Boolean expression.
If it is true, do-while executes the other statement. Once again, the Boolean expression is evaluated. If it
is still true, do-while re-executes the statement. This cyclic pattern continues.
The following example demonstrates do-while prompting the user to enter a specific uppercase
letter or its lowercase equivalent:
int ch;
do
{
System.out.println("Press C or c to continue.");
ch = System.in.read();
}
while (ch != 'C' && ch != 'c');
44
CHAPTER 1  GETTING STARTED WITH JAVA
This example is similar to its predecessor. Because the compound statement is no longer executed
prior to the test, it’s no longer necessary to initialize ch – ch is assigned System.in.read()’s return value
prior to the Boolean expression’s evaluation.
Looping Over the Empty Statement
Java refers to a semicolon character appearing by itself as the empty statement. It’s sometimes
convenient for a loop statement to execute the empty statement repeatedly. The actual work performed
by the loop statement takes place in the statement header. Consider the following example:
for (String line; (line = readLine()) != null; System.out.println(line));
This example uses for to present a programming idiom for copying lines of text that are read from
some source, via the fictitious readLine() method in this example, to some destination, via
System.out.println() in this example. Copying continues until readLine() returns null. Note the
semicolon (empty statement) at the end of the line.
 Caution Be careful with the empty statement because it can introduce subtle bugs into your code. For example,
the following loop is supposed to output the string Hello on ten lines. Instead, only one instance of this string is
output, because it is the empty statement and not System.out.println() that’s executed ten times:
for (int i = 0; i < 10; i++); // this ; represents the empty statement
System.out.println("Hello");
Break and Labeled Break Statements
What do for(;;);, while(true); and do;while(true); have in common? Each of these loop statements
presents an extreme example of an infinite loop (a loop that never ends). An infinite loop is something
that you should avoid because its unending execution causes your application to hang, which is not
desirable from the point of view of your application’s users.
 Caution An infinite loop can also arise from a loop header’s Boolean expression comparing a floating-point
value against a nonzero value via the equality or inequality operator, because many floating-point values have
inexact internal representations. For example, the following code fragment never ends because 0.1 does not have
an exact internal representation:
for (double d = 0.0; d != 1.0; d += 0.1)
System.out.println(d);
45
CHAPTER 1  GETTING STARTED WITH JAVA
However, there are times when it is handy to code a loop as if it were infinite by using one of the
aforementioned programming idioms. For example, you might code a while(true) loop that repeatedly
prompts for a specific keystroke until the correct key is pressed. When the correct key is pressed, the
loop must end. Java provides the break statement for this purpose.
The break statement transfers execution to the first statement following a switch statement (as
discussed earlier) or a loop. In either scenario, this statement consists of reserved word break followed
by a semicolon.
The following example uses break with an if decision statement to exit a while(true)-based infinite
loop when the user presses the C or c key:
int ch;
while (true)
{
System.out.println("Press C or c to continue.");
ch = System.in.read();
if (ch == 'C' || ch == 'c')
break;
}
The break statement is also useful in the context of a finite loop. For example, consider a scenario
where an array of values is searched for a specific value, and you want to exit the loop when this value is
found. The following example reveals this scenario:
int[] employeeIDs = { 123, 854, 567, 912, 224 };
int employeeSearchID = 912;
boolean found = false;
for (int i = 0; i < employeeIDs.length; i++)
if (employeeSearchID == employeeIDs[i])
{
found = true;
break;
}
System.out.println((found) ? "employee "+employeeSearchID+" exists"
: "no employee ID matches "+employeeSearchID);
The example uses for and if to search an array of employee IDs to determine whether a specific
employee ID exists. If this ID is found, if’s compound statement assigns true to found. Because there is
no point in continuing the search, it then uses break to quit the loop.
The labeled break statement transfers execution to the first statement following the loop that’s
prefixed by a label (an identifier followed by a colon). It consists of reserved word break, followed by an
identifier for which the matching label must exist. Furthermore, the label must immediately precede a
loop statement.
Labeled break is useful for breaking out of nested loops (loops within loops). The following example
reveals the labeled break statement transferring execution to the first statement that follows the outer for
loop:
46
CHAPTER 1  GETTING STARTED WITH JAVA
outer:
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
if (i == 1 && j == 1)
break outer;
else
System.out.println("i="+i+", j="+j);
System.out.println("Both loops terminated.");
When i’s value is 1 and j’s value is 1, break outer; is executed to terminate both for loops. This
statement transfers execution to the first statement after the outer for loop, which happens to be
System.out.println("Both loops terminated.");.
The following output is generated:
i=0,
i=0,
i=0,
i=1,
Both
j=0
j=1
j=2
j=0
loops terminated.
Continue and Labeled Continue Statements
The continue statement skips the remainder of the current loop iteration, re-evaluates the header’s
Boolean expression, and performs another iteration (if true) or terminates the loop (if false). Continue
consists of reserved word continue followed by a semicolon.
Consider a while loop that reads lines from a source and processes nonblank lines in some manner.
Because it should not process blank lines, while skips the current iteration when a blank line is detected,
as demonstrated in the following example:
String line;
while ((line = readLine()) != null)
{
if (isBlank(line))
continue;
processLine(line);
}
This example employs a fictitious isBlank() method to determine whether the currently read line is
blank. If this method returns true, if executes the continue statement to skip the rest of the current
iteration and read the next line whenever a blank line is detected. Otherwise, the fictitious processLine()
method is called to process the line’s contents.
Look carefully at this example and you should realize that the continue statement is not needed.
Instead, this listing can be shortened via refactoring (rewriting source code to improve its readability,
organization, or reusability), as demonstrated in the following example:
String line;
while ((line = readLine()) != null)
{
if (!isBlank(line))
processLine(line);
}
47
CHAPTER 1  GETTING STARTED WITH JAVA
This example’s refactoring modifies if’s Boolean expression to use the logical complement operator
(!). Whenever isBlank() returns false, this operator flips this value to true and if executes processLine().
Although continue isn’t necessary in this example, you’ll find it convenient to use this statement in more
complex code where refactoring isn’t as easy to perform.
The labeled continue statement skips the remaining iterations of one or more nested loops and
transfers execution to the labeled loop. It consists of reserved word continue, followed by an identifier
for which a matching label must exist. Furthermore, the label must immediately precede a loop
statement.
Labeled continue is useful for breaking out of nested loops while still continuing to execute the
labeled loop. The following example reveals the labeled continue statement terminating the inner for
loop’s iterations:
outer:
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
if (i == 1 && j == 1)
continue outer;
else
System.out.println("i="+i+", j="+j);
System.out.println("Both loops terminated.");
When i’s value is 1 and j’s value is 1, continue outer; is executed to terminate the inner for loop
and continue with the outer for loop at its next value of i. Both loops continue until they finish.
The following output is generated:
i=0,
i=0,
i=0,
i=1,
i=2,
i=2,
i=2,
Both
j=0
j=1
j=2
j=0
j=0
j=1
j=2
loops terminated.
EXERCISES
The following exercises are designed to test your understanding of applications and language
fundamentals:
48
1.
Declare an EchoArgs class whose main() method outputs its command-line
arguments, one argument per line. Store this class in a file named
EchoArgs.java. Compile this source code (javac EchoArgs.java) and run the
application; for example, java EchoArgs A B C. You should see each of A, B, and
C appearing on a separate line.
2.
Declare a Circle class whose main() method declares a double precision
floating-point variable named PI that’s initialized to 3.14159, declares a double
precision floating-point variable named radius that’s initialized to 15, calculates
and outputs the circle’s circumference (PI times the diameter), and calculates and
CHAPTER 1  GETTING STARTED WITH JAVA
outputs the circle’s area (PI times the square of the radius). Compile and run this
application.
3.
Declare an Input class whose main() method is declared as follows: public
static void main(String[] args) throws java.io.IOException—don’t
worry about throws java.io.IOException; you’ll learn about this language
feature in Chapter 3. Continuing, insert the “loop until C or c is input” example
from the “Break and Labeled Break Statements” section into the main() method.
Compile and run this application. When prompted, type a key and press the
Enter/Return key. What happens when you type multiple keys (abc, for example)
and press Enter/Return?
This book was purchased by [email protected]
4. Declare a Triangle class whose main() method uses a pair of nested for
statements along with System.out.print() to output a 10-row triangle of
asterisks, where each row contains an odd number of asterisks (1, 3, 5, 7, and so
on), as follows:
*
***
*****
*******
*********
***********
*************
***************
*****************
*******************
Compile and run this application.
5.
Declare an OutputReversedInt class whose main() method declares an int
variable named x that’s assigned a positive integer. This declaration is followed by
a while loop that outputs this integer’s digits in reverse. For example, 876432094
outputs as 490234678.
Summary
Java is a language for describing programs. This general-purpose, class-based, and object-oriented
language is patterned after C and C++ to make it easier for existing C/C++ developers to migrate to Java.
Java is also a platform on which to run programs written in Java and other languages (e.g., Groovy,
Jython, and JRuby). Unlike platforms with physical processors (e.g., an Intel processor) and operating
systems (e.g., Windows 7), the Java platform consists of a virtual machine and execution environment.
Before you can develop Java programs, you need to determine what kind(s) of programs you want to
develop and then install the appropriate software. Use the JDK to develop standalone applications and
applets, the Java ME SDK to develop MIDlets and Xlets, and the Java EE SDK to develop servlets and
JSPs.
For small projects, it’s no big deal to work at the command line with JDK tools. Because you’ll
probably find this scenario tedious (and even unworkable) for larger projects, you should also consider
obtaining an IDE such as NetBeans 7, which includes support for those language features introduced by
JDK 7.
49
CHAPTER 1  GETTING STARTED WITH JAVA
Most computer languages support comments, identifiers, types, variables, expressions, and
statements. Comments let you document your source code; identifiers name things (e.g., classes and
methods); types identify sets of values (and their representations in memory) and sets of operations that
transform these values into other values of that set; variables store values; expressions combine
variables, method calls, literals, and operators; and statements are the workhorses of a program, and
include assignment, decision, loop, break and labeled break, and continue and labeled continue.
Now that you possess a basic understanding of Java’s fundamental language features, you’re ready
to learn about Java’s language support for classes and objects. Chapter 2 introduces you to this support.
50
CHAPTER 2
Discovering Classes and Objects
Chapter 1 gently introduced you to the Java language by focusing mainly on fundamental language
features ranging from comments to statements. Using only these features, you can create simple
applications (such as HelloWorld and the applications mentioned in the chapter’s exercises) that are
reminiscent of those written in structured programming languages such as C.
■ Note Structured programming is a programming paradigm that enforces a logical structure on programs
through data structures (named aggregates of data items), functions (named blocks of code that return values to
the code that calls [passes program execution to] them), and procedures (named blocks of code that don’t return
values to their callers). Structured programs use sequence (one statement follows another statement),
selection/choice (if/switch), and repetition/iteration (for/while/do) programming constructs; use of the potentially
harmful GOTO statement (see http://en.wikipedia.org/wiki/GOTO) is discouraged.
Structured programs separate data from behaviors. This separation makes it difficult to model realworld entities (such as a bank accounts and employees) and often leads to maintenance headaches
when programs become complex. In contrast, classes and objects combine data and behaviors into
program entities; programs based on classes and objects are typically easier to understand and
maintain.
Chapter 2 takes you deeper into the Java language by focusing on its support for classes and objects.
You first learn how to declare classes and create objects from these classes, and then learn how to
encapsulate state and behaviors into these program entities through fields and methods. After learning
about class and object initialization, you move beyond this object-based programming model and dive
into object-oriented programming, by exploring Java’s inheritance- and polymorphism-oriented
language features.
At this point, the chapter presents one of Java’s more confusing language features: interfaces. You
learn what interfaces are, how they relate to classes, and what makes them so useful.
Java programs create objects that occupy memory. To reduce the possibility of running out of
memory, the Java Virtual Machine (JVM)’s garbage collector occasionally performs garbage collection by
locating objects that are no longer being used and removing this garbage to free up memory. Chapter 2
concludes by introducing you to the garbage collection process.
51
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Declaring Classes and Creating Objects
Structured programs create data structures that organize and store data items, and manipulate the data
stored in these data structures via functions and procedures. The fundamental units of a structured
program are its data structures and the functions or procedures that manipulate them. Although Java
lets you create applications in a similar fashion, this language is really about declaring classes and
creating objects from these classes. These program entities are the fundamental units of a Java program.
This section first shows you how to declare a class, and then shows you how to create objects from
this class with the help of the new operator and a constructor. The section then shows you how to specify
constructor parameters and local variables. Finally, you learn how to create arrays using the same new
operator that’s used to create an object from a class.
Declaring Classes
A class is a template for manufacturing objects (named aggregates of code and data), which are also
known as class instances, or instances for short. Classes generalize real-world entities, and objects are
specific manifestations of these entities at the program level. You might think of classes as cookie cutters
and objects as the cookies that cookie cutters create.
Because you cannot instantiate objects from a class that does not exist, you must first declare the
class. The declaration consists of a header followed by a body. At minimum, the header consists of
reserved word class followed by a name that identifies the class (so that it can be referred to from
elsewhere in the source code). The body starts with an open brace character ({) and ends with a close
brace (}). Sandwiched between these delimiters are various kinds of declarations. Consider Listing 2-1.
Listing 2-1. Declaring a skeletal Image class
class Image
{
// various member declarations
}
Listing 2-1 declares a class named Image, which presumably describes some kind of image for
displaying on the screen. By convention, a class’s name begins with an uppercase letter. Furthermore,
the first letter of each subsequent word in a multiword class name is capitalized. This is known as
camelcasing.
Creating Objects with the new Operator and a Constructor
Image is an example of a user-defined type from which objects can be created. You create these objects
by using the new operator with a constructor, as follows:
Image image = new Image();
The new operator allocates memory to store the object whose type is specified by new’s solitary
operand, which happens to be Image() in this example. The object is stored in a region of memory
known as the heap.
The parentheses (round brackets) that follow Image signify a constructor, which is a block of code for
constructing an object by initializing it in some manner. The new operator invokes (calls) the constructor
immediately after allocating memory to store the object.
When the constructor ends, new returns a reference (a memory address or other identifier) to the
object so that it can be accessed elsewhere in the program. Regarding the newly created Image object, its
52
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
reference is stored in a variable named image whose type is specified as Image. (It’s common to refer to
the variable as an object, as in the image object, although it stores only an object’s reference and not the
object itself.)
■ Note new’s returned reference is represented in source code by keyword this. Wherever this appears, it
represents the current object. Also, variables that store references are called reference variables.
Image does not explicitly declare a constructor. When a class does not declare a constructor, Java
implicitly creates a constructor for that class. The created constructor is known as the default
noargument constructor because no arguments (discussed shortly) appear between its ( and ) characters
when the constructor is invoked.
■ Note Java does not create a default noargument constructor when at least one constructor is declared.
Specifying Constructor Parameters and Local Variables
You explicitly declare a constructor within a class’s body by specifying the name of the class followed by
a parameter list, which is a round bracket-delimited and comma-separated list of zero or more
parameter declarations. A parameter is a constructor or method variable that receives an expression
value passed to the constructor or method when it is called. This expression value is known as an
argument.
Listing 2-2 enhances Listing 2-1’s Image class by declaring three constructors with parameter lists
that declare zero, one, or two parameters; and a main() method for testing this class.
Listing 2-2. Declaring an Image class with three constructors and a main() method
class Image
{
Image()
{
System.out.println("Image() called");
}
Image(String filename)
{
this(filename, null);
System.out.println("Image(String filename) called");
}
Image(String filename, String imageType)
{
System.out.println("Image(String filename, String imageType) called");
if (filename != null)
{
System.out.println("reading "+filename);
53
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
if (imageType != null)
System.out.println("interpreting "+filename+" as storing a "+
imageType+" image");
}
// Perform other initialization here.
}
public static void main(String[] args)
{
Image image = new Image();
System.out.println();
image = new Image("image.png");
System.out.println();
image = new Image("image.png", "PNG");
}
}
Listing 2-2’s Image class first declares a noargument constructor for initializing an Image object to
default values (whatever they may be). This constructor simulates default initialization by invoking
System.out.println() to output a message signifying that it’s been called.
Image next declares an Image(String filename) constructor whose parameter list consists of a single
parameter declaration—a parameter declaration consists of a variable’s type followed by the variable’s
name. The java.lang.String parameter is named filename, signifying that this constructor obtains
image content from a file.
■ Note Throughout this book’s chapters, I typically prefix the first use of a predefined type (such as String) with
the package hierarchy in which the type is stored. For example, String is stored in the lang subpackage of the
java package. I do so to help you learn where types are stored so that you can more easily specify import
statements for importing these types (without having to first search for a type’s package) into your source code—
you don’t have to import types that are stored in the java.lang package, but I still prefix the java.lang package
to the type name for completeness. I will have more to say about packages and the import statement in Chapter 3.
Some constructors rely on other constructors to help them initialize their objects. This is done to
avoid redundant code, which increases the size of an object, and unnecessarily takes memory away from
the heap that could be used for other purposes. For example, Image(String filename) relies on
Image(String filename, String imageType) to read the file’s image content into memory.
Although it appears otherwise, constructors don’t have names (although it is common to refer to a
constructor by specifying the class name and parameter list). A constructor calls another constructor by
using keyword this and a round bracket-delimited and comma-separated list of arguments. For
example, Image(String filename) executes this(filename, null); to execute Image(String filename,
String imageType).
54
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Caution You must use this to call another constructor—you cannot use the class’s name, as in Image(). The
this() constructor call (if present) must be the first code that is executed within the constructor. This rule
prevents you from specifying multiple this() constructor calls in the same constructor. Finally, you cannot specify
this() in a method—constructors can be called only by other constructors and during object creation. (I will
discuss methods later in this chapter.)
When present, the constructor call must be the first code that is specified within a constructor;
otherwise, the compiler reports an error. For this reason, a constructor that calls another constructor
can only perform additional work after the other constructor has finished. For example, Image(String
filename) executes System.out.println("Image(String filename) called"); after the invoked
Image(String filename, String imageType) constructor finishes.
The Image(String filename, String imageType) constructor declares an imageType parameter that
signifies the kind of image stored in the file—a Portable Network Graphics (PNG) image, for example.
Presumably, the constructor uses imageType to speed up processing by not examining the file’s contents
to learn the image format. When null is passed to imageType, as happens with the Image(String
filename) constructor, Image(String filename, String imageType) examines file contents to learn the
format. If null was also passed to filename, Image(String filename, String imageType) wouldn’t read
the file, but would presumably notify the code attempting to create the Image object of an error
condition.
After declaring the constructors, Listing 2-2 declares a main() method that lets you create Image
objects and view output messages. main() creates three Image objects, calling the first constructor with
no arguments, the second constructor with argument "image.png", and the third constructor with
arguments "image.png" and "PNG".
■ Note The number of arguments passed to a constructor or method, or the number of operator operands is
known as the constructor’s, method’s, or operator’s arity.
Each object’s reference is assigned to a reference variable named image, replacing the previously
stored reference for the second and third object assignments. (Each occurrence of
System.out.println(); outputs a blank line to make the output easier to read.)
The presence of main() changes Image from only a class to an application. You typically place main()
in classes that are used to create objects in order to test such classes. When constructing an application
for use by others, you usually declare main() in a class where the intent is to run an application and not
to create an object from that class—the application is then run from only that class. See Chapter 1’s
HelloWorld class for an example.
After saving Listing 2-2 to Image.java, compile this file by executing javac Image.java at the
command line. Assuming that there are no error messages, execute the application by specifying java
Image. You should observe the following output:
Image() called
Image(String filename, String imageType) called
55
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
reading image.png
Image(String filename) called
Image(String filename, String imageType) called
reading image.png
interpreting image.png as storing a PNG image
The first output line indicates that the noargument constructor has been called. Subsequent output
lines indicate that the second and third constructors have been called.
In addition to declaring parameters, a constructor can declare variables within its body to help it
perform various tasks. For example, the previously presented Image(String filename, String
imageType) constructor might create an object from a (hypothetical) File class that provides the means
to read a file’s contents. At some point, the constructor instantiates this class and assigns the instance’s
reference to a variable, as demonstrated in the following:
Image(String filename, String imageType)
{
System.out.println("Image(String filename, String imageType) called");
if (filename != null)
{
System.out.println("reading "+filename);
File file = new File(filename);
// Read file contents into object.
if (imageType != null)
System.out.println("interpreting "+filename+" as storing a "+
imageType+" image");
else
// Inspect image contents to learn image type.
; // Empty statement is used to make if-else syntactically valid.
}
// Perform other initialization here.
}
As with the filename and imageType parameters, file is a variable that is local to the constructor,
and is known as a local variable to distinguish it from a parameter. Although all three variables are local
to the constructor, there are two key differences between parameters and local variables:
56
•
The filename and imageType parameters come into existence at the point where
the constructor begins to execute and exist until execution leaves the constructor.
In contrast, file comes into existence at its point of declaration and continues to
exist until the block in which it is declared is terminated (via a closing brace
character). This property of a parameter or a local variable is known as lifetime.
•
The filename and imageType parameters can be accessed from anywhere in the
constructor. In contrast, file can be accessed only from its point of declaration to
the end of the block in which it is declared. It cannot be accessed before its
declaration or after its declaring block, but nested subblocks can access the local
variable. This property of a parameter or a local variable is known as scope.
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Note The lifetime and scope (also known as visibility) properties also apply to classes, objects, and fields
(discussed later). Classes come into existence when loaded into memory and cease to exist when unloaded from
memory, typically when an application exits. Also, loaded classes are typically visible to other classes, but this isn’t
always the case—Appendix C will have more to say about this issue when it presents classloaders.
An object’s lifetime ranges from its creation via the new operator until the moment when it is removed from
memory by the garbage collector. Its scope depends on various factors, such as when its reference is assigned to
a local variable or to a field. I discuss fields later in this chapter.
The lifetime of a field depends upon whether it is an instance field or a class field. If the field belongs to an object,
it comes into existence when the object is created and dies when the object disappears from memory. If the field
belongs to a class, the field begins its existence when the class is loaded and disappears when the class is
removed from memory. As with an object, a field’s scope depends upon various factors, such as whether the field
is declared to have private access or not—you’ll learn about private access later in this chapter.
A local variable cannot have the same name as a parameter because a parameter always has the
same scope as the local variable. However, a local variable can have the same name as another local
variable provided that both variables are located within different scopes (that is, within different blocks).
For example, you could specify int x = 1; within an if-else statement’s if block and specify double x =
2.0; within the statement’s corresponding else block, and each local variable would be distinct.
■ Note The discussion of constructor parameters, arguments, and local variables also applies to method
parameters, arguments, and local variables—I discuss methods later in this chapter.
Creating Arrays with the new Operator
The new operator is also used to create an array of objects in the heap, and is an alternative to the array
initializer presented in Chapter 1.
■ Note An array is implemented as a special Java object whose read-only length field contains the array’s size
(the number of elements). You’ll learn about fields later in this chapter.
57
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
When creating the array, specify new followed by a name that identifies the type of values that are
stored in the array, followed by one or more pairs of square brackets that signify the number of
dimensions occupied by the array. The leftmost pair of square brackets must contain an integral
expression that specifies the size of the array (the number of elements), whereas remaining pairs contain
integral expressions or are empty.
For example, you can use new to create a one-dimensional array of object references, as
demonstrated by the following example, which creates a one-dimensional array that can store ten Image
object references:
Image[] imArray = new Image[10];
When you create a one-dimensional array, new zeros the bits in each array element’s storage
location, which you interpret at the source code level as literal value false, '\u0000', 0, 0L, 0.0, 0.0F, or
null (depending on element type). In the previous example, each of imArray’s elements is initialized to
null, which represents the null reference (a reference to no object).
After creating an array, you need to assign object references to its elements. The following example
demonstrates this task by creating Image objects and assigning their references to imArray elements:
for (int i = 0; i < imArray.length; i++)
imArray[i] = new Image("image"+i+".png"); // image0.png, image1.png, and so on
The "image"+i+".png" expression uses the string concatenation operator (+) to combine image with
the string equivalent of the integer value stored in variable i with .png. The resulting string is passed to
Image’s Image(String filename) constructor.
■ Caution Use of the string concatenation operator in a loop context can result in a lot of unnecessary String
object creation, depending on the length of the loop. I will discuss this topic in Chapter 4 when I introduce you to
the String class.
You can also use new to create arrays of primitive type values (such as integers or double precision
floating-point numbers). For example, suppose you want to create a two-dimensional three-row-bytwo-column array of double precision floating-point temperature values. The following example
accomplishes this task:
double[][] temperatures = new double[3][2];
After creating a two-dimensional array, you will want to populate its elements with suitable values.
The following example initializes each temperatures element, which is accessed as
temperatures[row][col], to a randomly generated temperature value via Math.random(), which I’ll
explain in Chapter 4:
for (int row = 0; row < temperatures.length; row++)
for (int col = 0; col < temperatures[row].length; col++)
temperatures[row][col] = Math.random()*100;
You can subsequently output these values in a tabular format by using a for loop, as demonstrated
by the following example—the code makes no attempt to align the temperature values in perfect
columns:
for (int row = 0; row < temperatures.length; row++)
58
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
for (int col = 0; col < temperatures[row].length; col++)
System.out.print(temperatures[row][col]+" ");
System.out.println();
}
Java provides an alternative for creating a multidimensional array in which you create each
dimension separately. For example, to create a two-dimensional array via new in this manner, first create
a one-dimensional row array (the outer array), and then create a one-dimensional column array (the
inner array), as demonstrated here:
// Create the row array.
double[][] temperatures = new double[3][]; // Note the extra empty pair of brackets.
// Create a column array for each row.
for (int row = 0; row < temperatures.length; row++)
temperatures[row] = new double[2]; // 2 columns per row
This book was purchased by [email protected]
This kind of an array is known as a ragged array because each row can have a different number of
columns; the array is not rectangular, but is ragged.
■ Note When creating the row array, you must specify an extra pair of empty brackets as part of the expression
following new. (For a three-dimensional array—a one-dimensional array of tables, where this array’s elements
reference row arrays—you must specify two pairs of empty brackets as part of the expression following new.)
You can combine new with Chapter 1’s array initialization syntax if desired. For example, Image[]
imArray = new Image[] { new Image("image0.png"), new Image("image1.png") }; creates a pair of
Image objects and a two-element Image array object initialized to the Image objects’ references, and
assigns the array’s reference to imArray.
When you create an array in this manner, you are not permitted to specify an integral expression
between the square brackets. For example, the compiler reports an error when it encounters Image[]
imArray = new Image[2] { new Image("image0.png"), new Image("image1.png") };. To correct this
error, remove the 2 from between the square brackets.
Encapsulating State and Behaviors
Classes model real-world entities from a template perspective; for example, car and savings account.
Objects represent specific entities; for example, John’s red Toyota Camry (a car instance) and Cuifen’s
savings account with a balance of twenty thousand dollars (a savings account instance).
Entities have attributes, such as color red, make Toyota, model Camry, and balance twenty
thousand dollars. An entity’s collection of attributes is referred to as its state. Entities also have
behaviors, such as open car door, drive car, display fuel consumption, deposit, withdraw, and show
account balance.
A class and its objects model an entity by combining state with behaviors into a single unit—the
class abstracts state whereas its objects provide concrete state values. This bringing together of state and
behaviors is known as encapsulation. Unlike structured programming, where the developer focuses on
modeling behaviors through structured code, and modeling state through data structures that store data
items for the structured code to manipulate, the developer working with classes and objects focuses on
59
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
templating entities by declaring classes that encapsulate state and behaviors, instantiating objects with
specific state values from these classes to represent specific entities, and interacting with objects
through their behaviors.
This section first introduces you to Java’s language features for representing state, and then
introduces you to its language features for representing behaviors. Because some state and behaviors
support the class’s internal architecture, and should not be visible to those wanting to use the class, this
section concludes by presenting the important concept of information hiding.
Representing State via Fields
Java lets you represent state via fields, which are variables declared within a class’s body. Entity
attributes are described via instance fields. Because Java also supports state that’s associated with a class
and not with an object, Java provides class fields to describe this class state.
You first learn how to declare and access instance fields and then learn how to declare and access
class fields. After discovering how to declare read-only instance and class fields, you review the rules for
accessing fields from different contexts.
Declaring and Accessing Instance Fields
You can declare an instance field by minimally specifying a type name, followed by an identifier that
names the field, followed by a semicolon character (;). Listing 2-3 presents a Car class with three
instance field declarations.
Listing 2-3. Declaring a Car class with make, model, and numDoors instance fields
class Car
{
String make;
String model;
int numDoors;
}
Listing 2-3 declares two String instance fields named make and model. It also declares an int
instance field named numDoors. By convention, a field’s name begins with a lowercase letter, and the first
letter of each subsequent word in a multiword field name is capitalized.
When an object is created, instance fields are initialized to default zero values, which you interpret
at the source code level as literal value false, '\u0000', 0, 0L, 0.0, 0.0F, or null (depending on element
type). For example, if you were to execute Car car = new Car();, make and model would be initialized to
null and numDoors would be initialized to 0.
You can assign values to or read values from an object’s instance fields by using the member access
operator (.); the left operand specifies the object’s reference and the right operand specifies the instance
field to be accessed. Listing 2-4 uses this operator to initialize a Car object’s make, model, and numDoors
instance fields.
Listing 2-4. Initializing a Car object’s instance fields
class Car
{
String make;
String model;
60
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
int numDoors;
public static void main(String[] args)
{
Car car = new Car();
car.make = "Toyota";
car.model = "Camry";
car.numDoors = 4;
}
}
Listing 2-4 presents a main() method that instantiates Car. The car instance’s make instance field is
assigned the "Toyota" string, its model instance field is assigned the "Camry" string, and its numDoors
instance field is assigned integer literal 4. (A string’s double quotes delimit a string’s sequence of
characters but are not part of the string.)
You can explicitly initialize an instance field when declaring that field to provide a nonzero default
value, which overrides the default zero value. Listing 2-5 demonstrates this point.
Listing 2-5. Initializing Car’s numDoors instance field to a default nonzero value
class Car
{
String make;
String model;
int numDoors = 4;
Car()
{
}
public static void main(String[] args)
{
Car johnDoeCar = new Car();
johnDoeCar.make = "Chevrolet";
johnDoeCar.model = "Volt";
}
}
Listing 2-5 explicitly initializes numDoors to 4 because the developer has assumed that most cars
being modeled by this class have four doors. When Car is initialized via the Car() constructor, the
developer only needs to initialize the make and model instance fields for those cars that have four doors.
It is usually not a good idea to directly initialize an object’s instance fields, and you will learn why
when I discuss information hiding (later in this chapter). Instead, you should perform this initialization
in the class’s constructor(s)—see Listing 2-6.
Listing 2-6. Initializing Car’s instance fields via constructors
class Car
{
String make;
String model;
int numDoors;
Car(String make, String model)
{
this(make, model, 4);
61
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
}
Car(String make, String model, int nDoors)
{
this.make = make;
this.model = model;
numDoors = nDoors;
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
Car yourCar = new Car("Mazda", "RX-8", 2);
}
}
Listing 2-6’s Car class declares Car(String make, String model) and Car(String make, String
model, int nDoors) constructors. The first constructor lets you specify the make and model, whereas the
second constructor lets you specify values for the three instance fields.
The first constructor executes this(make, model, 4); to pass the values of its make and model
parameters, along with a default value of 4 to the second constructor. Doing so demonstrates an
alternative to explicitly initializing an instance field, and is preferable from a code maintenance
perspective.
The Car(String make, String model, int numDoors) constructor demonstrates another use for
keyword this. Specifically, it demonstrates a scenario where constructor parameters have the same
names as the class’s instance fields. Prefixing a variable name with “this.” causes the Java compiler to
create bytecode that accesses the instance field. For example, this.make = make; assigns the make
parameter’s String object reference to this (the current) Car object’s make instance field. If make = make;
was specified instead, it would accomplish nothing by assigning make’s value to itself; a Java compiler
might not generate code to perform the unnecessary assignment. In contrast, “this.” isn’t necessary for
the numDoors = nDoors; assignment, which initializes the numDoors field from the nDoors parameter
value.
Declaring and Accessing Class Fields
In many situations, instance fields are all that you need. However, you might encounter a situation
where you need a single copy of a field no matter how many objects are created.
For example, suppose you want to track the number of Car objects that have been created, and
introduce a counter instance field (initialized to 0) into this class. You also place code in the class’s
constructor that increases counter’s value by 1 when an object is created. However, because each object
has its own copy of the counter instance field, this field’s value never advances past 1. Listing 2-7 solves
this problem by declaring counter to be a class field, by prefixing the field declaration with the static
keyword.
Listing 2-7. Adding a counter class field to Car
class Car
{
String make;
String model;
int numDoors;
static int counter;
62
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Car(String make, String model)
{
this(make, model, 4);
}
Car(String make, String model, int numDoors)
{
this.make = make;
this.model = model;
this.numDoors = numDoors;
counter++;
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
Car yourCar = new Car("Mazda", "RX-8", 2);
System.out.println(Car.counter);
}
}
Listing 2-7’s static prefix implies that there is only one copy of the counter field, not one copy per
object. When a class is loaded into memory, class fields are initialized to default zero values. For
example, counter is initialized to 0. (As with instance fields, you can alternatively assign a value to a class
field in its declaration.) Each time an object is created, counter will increase by 1 thanks to the counter++
expression in the Car(String make, String model, int numDoors) constructor.
Unlike instance fields, class fields are normally accessed directly via the member access operator.
Although you could access a class field via an object reference (as in myCar.counter), it is conventional to
access a class field by using the class’s name, as in Car.counter. (It is also easier to tell that the code is
accessing a class field.)
■ Note Because the main() method is a member of Listing 2-7’s Car class, you could access counter directly,
as in System.out.println(counter);. To access counter in the context of another class’s main() method,
however, you would have to specify Car.counter.
If you run Listing 2-7, you will notice that it outputs 2, because two Car objects have been created.
Declaring Read-Only Instance and Class Fields
The previously declared fields can be written to as well as read from. However, you might want to
declare a field that is read-only; for example, a field that names a constant value such as pi (3.14159…).
Java lets you accomplish this task by providing reserved word final.
Each object receives its own copy of a read-only instance field. This field must be initialized, as part
of the field’s declaration or in the class’s constructor. If initialized in the constructor, the read-only
instance field is known as a blank final because it does not have a value until one is assigned to it in the
constructor. Because a constructor can potentially assign a different value to each object’s blank final,
these read-only variables are not truly constants.
63
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
If you want a true constant, which is a single read-only value that is available to all objects, you need
to create a read-only class field. You can accomplish this task by including the reserved word static with
final in that field’s declaration.
Listing 2-8 shows you how to declare a read-only class field.
Listing 2-8. Declaring a true constant in the Employee class
class Employee
{
final static int RETIREMENT_AGE = 65;
}
Listing 2-8’s RETIREMENT_AGE declaration is an example of a compile-time constant. Because there is
only one copy of its value (thanks to the static keyword), and because this value will never change
(thanks to the final keyword), the compiler is free to optimize the compiled code by inserting the
constant value into all calculations where it is used. The code runs faster because it doesn’t have to
access a read-only class field.
Reviewing Field-Access Rules
The previous examples of field access may seem confusing because you can sometimes specify the
field’s name directly, whereas you need to prefix a field name with an object reference or a class name
and the member access operator at other times. The following rules dispel this confusion by giving you
guidance on how to access fields from the various contexts:
•
Specify the name of a class field as is from anywhere within the same class as the
class field declaration. Example: counter
•
Specify the name of a class field’s class, followed by the member access operator,
followed by the name of the class field from outside the class. Example:
Car.counter
•
Specify the name of an instance field as is from any instance method, constructor,
or instance initializer (discussed later) in the same class as the instance field
declaration. Example: numDoors
•
Specify an object reference, followed by the member access operator, followed by
the name of the instance field from any class method or class initializer (discussed
later) within the same class as the instance field declaration, or from outside the
class. Example: Car car = new Car(); car.numDoors = 2;
Although the latter rule might seem to imply that you can access an instance field from a class
context, this is not the case. Instead, you are accessing the field from an object context.
The previous access rules are not exhaustive because there exist two more field-access scenarios to
consider: declaring a local variable (or even a parameter) with the same name as an instance field or as a
class field. In either scenario, the local variable/parameter is said to shadow (hide or mask) the field.
If you find that you have declared a local variable or a parameter that shadows a field, you can
rename the local variable/parameter, or you can use the member access operator with reserved word
this (instance field) or class name (class field) to explicitly identify the field. For example, Listing 2-6’s
Car(String make, String model, int nDoors) constructor demonstrated this latter solution by
specifying statements such as this.make = make; to distinguish an instance field from a same-named
parameter.
64
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Representing Behaviors via Methods
Java lets you represent behaviors via methods, which are named blocks of code declared within a class’s
body. Entity behaviors are described via instance methods. Because Java also supports behaviors that are
associated with a class and not with an object, Java provides class methods to describe these class
behaviors.
You first learn how to declare and invoke instance methods, and then learn how to create instance
method call chains. Next, you discover how to declare and invoke class methods, encounter additional
details about passing arguments to methods, and explore Java’s return statement. After learning how to
invoke methods recursively as an alternative to iteration, and how to overload methods, you review the
rules for invoking methods from different contexts.
Declaring and Invoking Instance Methods
You can declare an instance method by minimally specifying a return type name, followed by an
identifier that names the method, followed by a parameter list, followed by a brace-delimited body.
Listing 2-9 presents a Car class with a printDetails() instance method.
Listing 2-9. Declaring a printDetails() instance method in the Car class
class Car
{
String make;
String model;
int numDoors;
Car(String make, String model)
{
this(make, model, 4);
}
Car(String make, String model, int numDoors)
{
this.make = make;
this.model = model;
this.numDoors = numDoors;
}
void printDetails()
{
System.out.println("Make = "+make);
System.out.println("Model = "+model);
System.out.println("Number of doors = "+numDoors);
System.out.println();
}
public static void main(String[] args)
{
Car myCar = new Car("Toyota", "Camry");
myCar.printDetails();
Car yourCar = new Car("Mazda", "RX-8", 2);
yourCar.printDetails();
}
}
65
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-9 declares an instance method named printDetails(). By convention, a method’s name
begins with a lowercase letter, and the first letter of each subsequent word in a multiword method name
is capitalized.
Methods are like constructors in that they have parameter lists. You pass arguments to these
parameters when you call the method. Because printDetails() does not take arguments, its parameter
list is empty.
■ Note A method’s name and the number, types, and order of its parameters are known as its signature.
When a method is invoked, the code within its body is executed. In the case of printDetails(), this
method’s body executes a sequence of System.out.println() method calls to output the values of its
make, model, and numDoors instance fields.
Unlike constructors, methods are declared to have return types. A return type identifies the kind of
values returned by the method (e.g., int count() returns 32-bit integers). If a method does not return a
value (and printDetails() does not), its return type is replaced with keyword void, as in void
printDetails().
■ Note Constructors don’t have return types because they cannot return values. If a constructor could return an
arbitrary value, how would that value be returned? After all, the new operator returns a reference to an object, and
how could new also return a constructor value?
A method is invoked by using the member access operator; the left operand specifies the object’s
reference and the right operand specifies the method to be called. For example, the
myCar.printDetails() and yourCar.printDetails() expressions invoke the printDetails() instance
method on the myCar and yourCar objects.
Compile Listing 2-9 (javac Car.java) and run this application (java Car). You should observe the
following output, whose different instance field values prove that printDetails() associates with an
object:
Make = Toyota
Model = Camry
Number of doors = 4
Make = Mazda
Model = RX-8
Number of doors = 2
When an instance method is invoked, Java passes a hidden argument to the method (as the leftmost
argument in a list of arguments). This argument is the reference to the object on which the method is
invoked, and is represented at the source code level via reserved word this. You don’t need to prefix an
instance field name with “this.” from within the method whenever you attempt to access an instance
field name that isn’t also the name of a parameter because “this.” is assumed in this situation.
66
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
METHOD-CALL STACK
Method invocations require a method-call stack (also known as a method-invocation stack) to keep track
of the statements to which execution must return. Think of the method-call stack as a simulation of a pile
of clean trays in a cafeteria—you pop (remove) the clean tray from the top of the pile and the dishwasher
will push (insert) the next clean tray onto the top of the pile.
When a method is invoked, the JVM pushes its arguments and the address of the first statement to
execute following the invoked method onto the method-call stack. The JVM also allocates stack space for
the method’s local variables. When the method returns, the JVM removes local variable space, pops the
address and arguments off the stack, and transfers execution to the statement at this address.
Chaining Together Instance Method Calls
Two or more instance method calls can be chained together via the member access operator, which
results in more compact code. To accomplish instance method call chaining, you need to re-architect
your instance methods somewhat differently, as Listing 2-10 reveals.
Listing 2-10. Implementing instance methods so that calls to these methods can be chained together
class SavingsAccount
{
int balance;
SavingsAccount deposit(int amount)
{
balance += amount;
return this;
}
SavingsAccount printBalance()
{
System.out.println(balance);
return this;
}
public static void main(String[] args)
{
new SavingsAccount().deposit(1000).printBalance();
}
}
Listing 2-10 shows that you must specify the class’s name as the instance method’s return type.
Each of deposit() and printBalance() must specify SavingsAccount as the return type. Also, you must
specify return this; (return current object’s reference) as the last statement—I discuss the return
statement later.
For example, new SavingsAccount().deposit(1000).printBalance(); creates a SavingsAccount
object, uses the returned SavingsAccount reference to invoke SavingsAccount’s deposit() instance
method, to add one thousand dollars to the savings account (I’m ignoring cents for convenience), and
finally uses deposit()’s returned SavingsAccount reference (which is the same SavingsAccount instance)
to invoke SavingsAccount’s printBalance() instance method to output the account balance.
67
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Declaring and Invoking Class Methods
In many situations, instance methods are all that you need. However, you might encounter a situation
where you need to describe a behavior that is independent of any object.
For example, suppose you would like to introduce a utility class (a class consisting of static [class]
methods) whose methods perform various kinds of conversions (such as converting from degrees
Celsius to degrees Fahrenheit). You don’t want to create an object from this class in order to perform a
conversion. Instead, you simply want to call a method and obtain its result. Listing 2-11 addresses this
requirement by presenting a Conversions class with a pair of class methods. These methods can be
called without having to create a Conversions object.
Listing 2-11. A Conversions utility class with a pair of class methods
class Conversions
{
static double c2f(double degrees)
{
return degrees*9.0/5.0+32;
}
static double f2c(double degrees)
{
return (degrees-32)*5.0/9.0;
}
}
Listing 2-11’s Conversions class declares c2f() and f2c() methods for converting from degrees
Celsius to degrees Fahrenheit and vice versa, and returning the results of these conversions. Each
method header (method signature and other information) is prefixed with keyword static to turn the
method into a class method.
To execute a class method, you typically prefix its name with the class name. For example, you can
execute Conversions.c2f(100.0); to find out the Fahrenheit equivalent of 100 degrees Celsius, and
Conversions.f2c(98.6); to discover the Celsius equivalent of the normal body temperature. You don’t
need to instantiate Conversions and then call these methods via that instance, although you could do so
(but that isn’t good form).
■ Note Every application has at least one class method. Specifically, an application must specify public static
void main(String[] args) to serve as the application’s entry point. The static reserved word makes this
method a class method. (I will explain reserved word public later in this chapter.)
Because class methods are not called with a hidden argument that refers to the current object,
c2f(), f2c(), and main() cannot access an object’s instance fields or call its instance methods. These
class methods can only access class fields and call class methods.
68
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Passing Arguments to Methods
A method call includes a list of (zero or more) arguments being passed to the method. Java passes
arguments to methods via a style of argument passing called pass-by-value, which the following example
demonstrates:
This book was purchased by [email protected]
Employee emp = new Employee("John ");
int recommendedAnnualSalaryIncrease = 1000;
printReport(emp, recommendAnnualSalaryIncrease);
printReport(new Employee("Cuifen"), 1500);
Pass-by-value passes the value of a variable (the reference value stored in emp or the 1000 value
stored in recommendedAnnualSalaryIncrease, for example) or the value of some other expression (such as
new Employee("Cuifen") or 1500) to the method.
Because of pass-by-value, you cannot assign a different Employee object’s reference to emp from
inside printReport() via the printReport() parameter for this argument. After all, you have only passed
a copy of emp’s value to the method.
Many methods (and constructors) require you to pass a fixed number of arguments when they are
called. However, Java also can pass a variable number of arguments—such methods/constructors are
often referred to as varargs methods/constructors. To declare a method (or constructor) that takes a
variable number of arguments, specify three consecutive periods after the type name of the
method’s/constructor’s rightmost parameter. The following example presents a sum() method that
accepts a variable number of arguments:
double sum(double... values)
{
int total = 0;
for (int i = 0; i < values.length; i++)
total += values[i];
return total;
}
sum()’s implementation totals the number of arguments passed to this method; for example,
sum(10.0, 20.0) or sum(30.0, 40.0, 50.0). (Behind the scenes, these arguments are stored in a onedimensional array, as evidenced by values.length and values[i].) After these values have been totaled,
this total is returned via the return statement.
Returning from a Method via the Return Statement
The execution of statements within a method that does not return a value (its return type is set to void)
flows from the first statement to the last statement. However, Java’s return statement lets a method (or a
constructor) exit before reaching the last statement. As Listing 2-12 shows, this form of the return
statement consists of reserved word return followed by a semicolon.
69
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-12. Using the return statement to return prematurely from a method
class Employee
{
String name;
Employee(String name)
{
setName(name);
}
void setName(String name)
{
if (name == null)
{
System.out.println("name cannot be null");
return;
}
else
this.name = name;
}
public static void main(String[] args)
{
Employee john = new Employee(null);
}
}
Listing 2-12’s Employee(String name) constructor invokes the setName() instance method to
initialize the name instance field. Providing a separate method for this purpose is a good idea because it
lets you initialize the instance field at construction time and also at a later time. (Perhaps the employee
changes his or her name.)
■ Note When you invoke a class’s instance or class method from a constructor or method within the same class,
you specify only the method’s name. You don’t prefix the method invocation with the member access operator and
an object reference or class name.
setName() uses an if statement to detect an attempt to assign a null reference to the name field. When
such an attempt is detected, it outputs the “name cannot be null” error message and returns
prematurely from the method so that the null value cannot be assigned (and replace a previously
assigned name).
■ Caution When using the return statement, you might run into a situation where the compiler reports an
“unreachable code” error message. It does so when it detects code that will never be executed and occupies
memory unnecessarily. One area where you might encounter this problem is the switch statement. For example,
70
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
suppose you specify case "-v": printUsageInstructions(); return; break; as part of this statement. The
compiler reports an error when it detects the break statement following the return statement because the break
statement is unreachable; it never can be executed.
The previous form of the return statement is not legal in a method that returns a value. For such
methods, Java provides an alternate version of return that lets the method return a value (whose type
must match the method’s return type). The following example demonstrates this version:
double divide(double dividend, double divisor)
{
if (divisor == 0.0)
{
System.out.println("cannot divide by zero");
return 0.0;
}
return dividend/divisor;
}
divide() uses an if statement to detect an attempt to divide its first argument by 0.0, and outputs an
error message when this attempt is detected. Furthermore, it returns 0.0 to signify this attempt. If there
is no problem, the division is performed and the result is returned.
■ Caution You cannot use this form of the return statement in a constructor because constructors do not have
return types.
Invoking Methods Recursively
A method normally executes statements that may include calls to other methods, such as
printDetails() invoking System.out.println(). However, it is occasionally convenient to have a
method call itself. This scenario is known as recursion.
For example, suppose you need to write a method that returns a factorial (the product of all the
positive integers up to and including a specific integer). For example, 3! (the ! is the mathematical
symbol for factorial) equals 3×2×1 or 6.
Your first approach to writing this method might consist of the code presented in the following
example:
int factorial(int n)
{
int product = 1;
for (int i = 2; i <= n; i++)
product *= i;
return product;
}
71
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Although this code accomplishes its task (via iteration), factorial() could also be written according
to the following example’s recursive style.
int factorial(int n)
{
if (n == 1)
return 1; // base problem
else
return n*factorial(n-1);
}
The recursive approach takes advantage of being able to express a problem in simpler terms of itself.
According to this example, the simplest problem, which is also known as the base problem, is 1! (1).
When an argument greater than 1 is passed to factorial(), this method breaks the problem into a
simpler problem by calling itself with the next smaller argument value. Eventually, the base problem will
be reached.
For example, calling factorial(4) results in the following stack of expressions:
4*factorial(3)
3*factorial(2)
2*factorial(1)
This last expression is at the top of the stack. When factorial(1) returns 1, these expressions are
evaluated as the stack begins to unwind:
•
2*factorial(1) now becomes 2*1 (2)
•
3*factorial(2) now becomes 3*2 (6)
•
4*factorial(3) now becomes 4*6 (24)
Recursion provides an elegant way to express many problems. Additional examples include
searching tree-based data structures for specific values and, in a hierarchical file system, finding and
outputting the names of all files that contain specific text.
■ Caution Recursion consumes stack space, so make sure that your recursion eventually ends in a base problem;
otherwise, you will run out of stack space and your application will be forced to terminate.
Overloading Methods
Java lets you introduce methods with the same name but different parameter lists into the same class.
This feature is known as method overloading. When the compiler encounters a method invocation
expression, it compares the called method’s arguments list with each overloaded method’s parameter
list as it looks for the correct method to invoke.
Two same-named methods are overloaded when their parameter lists differ in number or order of
parameters. For example, Java’s String class provides overloaded public int indexOf(int ch) and
public int indexOf(int ch, int fromIndex) methods. These methods differ in parameter counts. (I
explore String in Chapter 4.)
72
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Two same-named methods are overloaded when at least one parameter differs in type. For example,
Java’s java.lang.Math class provides overloaded public static double abs(double a) and public
static int abs(int a) methods. One method’s parameter is a double; the other method’s parameter is
an int. (I explore Math in Chapter 4.)
You cannot overload a method by changing only the return type. For example, double
sum(double... values) and int sum(double... values) are not overloaded. These methods are not
overloaded because the compiler does not have enough information to choose which method to call
when it encounters sum(1.0, 2.0) in source code.
Reviewing Method-Invocation Rules
The previous examples of method invocation may seem confusing because you can sometimes specify
the method’s name directly, whereas you need to prefix a method name with an object reference or a
class name and the member access operator at other times. The following rules dispel this confusion by
giving you guidance on how to invoke methods from the various contexts:
•
Specify the name of a class method as is from anywhere within the same class as
the class method. Example: c2f(37.0);
•
Specify the name of the class method’s class, followed by the member access
operator, followed by the name of the class method from outside the class.
Example: Conversions.c2f(37.0); (You can also invoke a class method via an
object instance, but that is considered bad form because it hides from casual
observation the fact that a class method is being invoked.)
•
Specify the name of an instance method as is from any instance method,
constructor, or instance initializer in the same class as the instance method.
Example: setName(name);
•
Specify an object reference, followed by the member access operator, followed by
the name of the instance method from any class method or class initializer within
the same class as the instance method, or from outside the class. Example: Car car
= new Car("Toyota", "Camry"); car.printDetails();
Although the latter rule might seem to imply that you can call an instance method from a class
context, this is not the case. Instead, you call the method from an object context.
Also, don’t forget to make sure that the number of arguments passed to a method, along with the
order in which these arguments are passed, and the types of these arguments agree with their parameter
counterparts in the method being invoked.
■ Note Field access and method call rules are combined in expression System.out.println();, where the
leftmost member access operator accesses the out class field (of type java.io.PrintStream) in the
java.lang.System class, and where the rightmost member access operator calls this field’s println() method.
You’ll learn about PrintStream in Chapter 8 and System in Chapter 4.
73
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Hiding Information
Every class X exposes an interface (a protocol consisting of constructors, methods, and [possibly] fields
that are made available to objects created from other classes for use in creating and communicating with
X’s objects).
An interface serves as a one-way contract between a class and its clients, which are the external
constructors, methods, and other (initialization-oriented) class entities (discussed later in this chapter)
that communicate with the class’s instances by calling constructors and methods, and by accessing
fields (typically public static final fields, or constants). The contract is such that the class promises to
not change its interface, which would break clients that depend upon the interface.
X also provides an implementation (the code within exposed methods along with optional helper
methods and optional supporting fields that should not be exposed) that codifies the interface. Helper
methods are methods that assist exposed methods and should not be exposed.
When designing a class, your goal is to expose a useful interface while hiding details of that
interface’s implementation. You hide the implementation to prevent developers from accidentally
accessing parts of your class that do not belong to the class’s interface, so that you are free to change the
implementation without breaking client code. Hiding the implementation is often referred to as
information hiding. Furthermore, many developers consider implementation hiding to be part of
encapsulation.
Java supports implementation hiding by providing four levels of access control, where three of these
levels are indicated via a reserved word. You can use the following access control levels to control access
to fields, methods, and constructors, and two of these levels to control access to classes:
•
Public: A field, method, or constructor that is declared public is accessible from
anywhere. Classes can be declared public as well.
•
Protected: A field, method, or constructor that is declared protected is accessible
from all classes in the same package as the member’s class, as well as subclasses of
that class regardless of package. (I will discuss packages in Chapter 3.)
•
Private: A field, method, or constructor that is declared private cannot be
accessed from beyond the class in which it is declared.
•
Package-private: In the absence of an access-control reserved word, a field,
method, or constructor is only accessible to classes within the same package as
the member’s class. The same is true for non-public classes. The absence of
public, protected, or private implies package-private.
■ Note A class that is declared public must be stored in a file with the same name. For example, a public Image
class must be stored in Image.java. A source file can declare one public class only.
You will often declare your class’s instance fields to be private and provide special public instance
methods for setting and getting their values. By convention, methods that set field values have names
starting with set and are known as setters. Similarly, methods that get field values have names with get
(or is, for Boolean fields) prefixes and are known as getters. Listing 2-13 demonstrates this pattern in the
context of an Employee class declaration.
74
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-13. Separation of interface from implementation
public class Employee
{
private String name;
public Employee(String name)
{
setName(name);
}
public void setName(String empName)
{
name = empName; // Assign the empName argument to the name field.
}
public String getName()
{
return name;
}
}
Listing 2-13 presents an interface consisting of the public Employee class, its public constructor, and
its public setter/getter methods. This class and these members can be accessed from anywhere. The
implementation consists of the private name field and constructor/method code, which is only
accessible within the Employee class.
It might seem pointless to go to all this bother when you could simply omit private and access the
name field directly. However, suppose you are told to introduce a new constructor that takes separate first
and last name arguments and new methods that set/get the employee’s first and last names into this
class. Furthermore, suppose that it has been determined that the first and last names will be accessed
more often than the entire name. Listing 2-14 reveals these changes.
Listing 2-14. Revising implementation without affecting existing interface
public class Employee
{
private String firstName;
private String lastName;
public Employee(String name)
{
setName(name);
}
public Employee(String firstName, String lastName)
{
setName(firstName+" "+lastName);
}
public void setName(String name)
{
// Assume that the first and last names are separated by a
// single space character. indexOf() locates a character in a
// string; substring() returns a portion of a string.
setFirstName(name.substring(0, name.indexOf(' ')));
setLastName(name.substring(name.indexOf(' ')+1));
}
public String getName()
75
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
return getFirstName()+" "+getLastName();
}
public void setFirstName(String empFirstName)
{
firstName = empFirstName;
}
public String getFirstName()
{
return firstName;
}
public void setLastName(String empLastName)
{
lastName = empLastName;
}
public String getLastName()
{
return lastName;
}
}
Listing 2-14 reveals that the name field has been removed in favor of new firstName and lastName
fields, which were added to improve performance. Because setFirstName() and setLastName() will be
called more frequently than setName(), and because getFirstName() and getLastName() will be called
more frequently than getName(), it is more performant (in each case) to have the first two methods
set/get firstName’s and lastName’s values rather than merging either value into/extracting this value
from name’s value.
Listing 2-14 also reveals setName() calling setFirstName() and setLastName(), and getName() calling
getFirstName() and getLastName(), rather than directly accessing the firstName and lastName fields.
Although avoiding direct access to these fields is not necessary in this example, imagine another
implementation change that adds more code to setFirstName(), setLastName(), getFirstName(), and
getLastName(); not calling these methods will result in the new code not executing.
Client code (code that instantiates and uses a class, such as Employee) will not break when Employee’s
implementation changes from that shown in Listing 2-13 to that shown in Listing 2-14, because the
original interface remains intact, although the interface has been extended. This lack of breakage results
from hiding Listing 2-13’s implementation, especially the name field.
■ Note setName() invokes the String class’s indexOf() and substring() methods. You’ll learn about these
and other String methods in Chapter 4.
Java provides a little known information hiding-related language feature that lets one object (or
class method/initializer) access another object’s private fields or invoke its private methods. Listing 215 provides a demonstration.
76
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-15. One object accessing another object’s private field
class PrivateAccess
{
private int x;
PrivateAccess(int x)
{
this.x = x;
}
boolean equalTo(PrivateAccess pa)
{
return pa.x == x;
}
public static void main(String[] args)
{
PrivateAccess pa1 = new PrivateAccess(10);
PrivateAccess pa2 = new PrivateAccess(20);
PrivateAccess pa3 = new PrivateAccess(10);
System.out.println("pa1 equal to pa2: "+pa1.equalTo(pa2));
System.out.println("pa2 equal to pa3: "+pa2.equalTo(pa3));
System.out.println("pa1 equal to pa3: "+pa1.equalTo(pa3));
System.out.println(pa2.x);
}
}
Listing 2-15’s PrivateAccess class declares a private int field named x. It also declares an equalTo()
method that takes a PrivateAccess argument. The idea is to compare the argument object with the
current object to determine if they are equal.
The equality determination is made by using the == operator to compare the value of the argument
object’s x instance field with the value of the current object’s x instance field, returning Boolean true
when they are the same. What may seem baffling is that Java lets you specify pa.x to access the argument
object’s private instance field. Also, main() is able to directly access x, via the pa2 object.
I previously presented Java’s four access-control levels and presented the following statement
regarding the private access-control level: “A field, method, or constructor that is declared private
cannot be accessed from beyond the class in which it is declared.” When you carefully consider this
statement and examine Listing 2-15, you will realize that x is not being accessed from beyond the
PrivateAccess class in which it is declared. Therefore, the private access-control level is not being
violated.
The only code that can access this private instance field is code located within the PrivateAccess
class. If you attempted to access x via a PrivateAccess object that was created in the context of another
class, the compiler would report an error.
Being able to directly access x from within PrivateAccess is a performance enhancement; it is faster
to directly access this implementation detail than to call a method that returns its value.
Compile PrivateAccess.java (javac PrivateAccess.java) and run the application (java
PrivateAccess). You should observe the following output:
pa1 equal to pa2: false
pa2 equal to pa3: false
pa1 equal to pa3: true
20
77
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Tip Get into the habit of developing useful interfaces while hiding implementations because it will save you
much trouble when maintaining your classes.
Initializing Classes and Objects
Classes and objects need to be properly initialized before they are used. You’ve already learned that class
fields are initialized to default zero values after a class loads, and can be subsequently initialized by
assigning values to them in their declarations via class field initializers; for example, static int counter
= 1;. Similarly, instance fields are initialized to default values when an object’s memory is allocated via
new, and can be subsequently initialized by assigning values to them in their declarations via instance
field initializers; for example, int numDoors = 4;.
Another aspect of initialization that’s already been discussed is the constructor, which is used to
initialize an object, typically by assigning values to various instance fields, but is also capable of
executing arbitrary code, such as code that opens a file and reads the file’s contents.
Java provides two additional initialization features: class initializers and instance initializers. After
introducing you to these features, this section discusses the order in which all of Java’s initializers
perform their work.
Class Initializers
Constructors perform initialization tasks for objects. Their counterpart from a class initialization
perspective is the class initializer.
A class initializer is a static-prefixed block that is introduced into a class body. It is used to initialize
a loaded class via a sequence of statements. For example, I once used a class initializer to load a custom
database driver class. Listing 2-16 shows the loading details.
Listing 2-16. Loading a database driver via a class initializer
class JDBCFilterDriver implements Driver
{
static private Driver d;
static
{
// Attempt to load JDBC-ODBC Bridge Driver and register that
// driver.
try
{
Class c = Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
d = (Driver) c.newInstance();
DriverManager.registerDriver(new JDBCFilterDriver());
}
catch (Exception e)
{
System.out.println(e);
}
}
78
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
//...
}
Listing 2-16’s JDBCFilterDriver class uses its class initializer to load and instantiate the class that
describes Java’s JDBC-ODBC Bridge Driver, and to register a JDBCFilterDriver instance with Java’s
database driver. Although this listing’s JDBC-oriented code is probably meaningless to you right now,
the listing illustrates the usefulness of class initializers. (I discuss JDBC in Chapter 9.)
A class can declare a mix of class initializers and class field initializers, as demonstrated in Listing 217.
This book was purchased by [email protected]
Listing 2-17. Mixing class initializers with class field initializers
class C
{
static
{
System.out.println("class initializer 1");
}
static int counter = 1;
static
{
System.out.println("class initializer 2");
System.out.println("counter = "+counter);
}
}
Listing 2-17 declares a class named C that specifies two class initializers and one class field
initializer. When the Java compiler compiles into a classfile a class that declares at least one class
initializer or class field initializer, it creates a special void <clinit>() class method that stores the
bytecode equivalent of all class initializers and class field initializers in the order they occur (from top to
bottom).
■ Note <clinit> is not a valid Java method name, but is a valid name from the runtime perspective. The angle
brackets were chosen as part of the name to prevent a name conflict with any clinit() methods that you might
declare in the class.
For class C, <clinit>() would first contain the bytecode equivalent of System.out.println("class
initializer 1");, it would next contain the bytecode equivalent of static int counter = 1;, and it
would finally contain the bytecode equivalent of System.out.println("class initializer 2");
System.out.println("counter = "+counter);.
When class C is loaded into memory, <clinit>() executes immediately and generates the following
output:
class initializer 1
class initializer 2
counter = 1
79
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Instance Initializers
Not all classes can have constructors, as you will discover in Chapter 3 when I present anonymous
classes. For these classes, Java supplies the instance initializer to take care of instance initialization tasks.
An instance initializer is a block that is introduced into a class body, as opposed to being introduced
as the body of a method or a constructor. The instance initializer is used to initialize an object via a
sequence of statements, as demonstrated in Listing 2-18.
Listing 2-18. Initializing a pair of arrays via an instance initializer
class Graphics
{
double[] sines;
double[] cosines;
{
sines = new double[360];
cosines = new double[sines.length];
for (int i = 0; i < sines.length; i++)
{
sines[i] = Math.sin(Math.toRadians(i));
cosines[i] = Math.cos(Math.toRadians(i));
}
}
}
Listing 2-18’s Graphics class uses an instance initializer to create an object’s sines and cosines
arrays, and to initialize these arrays’ elements to the sines and cosines of angles ranging from 0 through
359 degrees. It does so because it’s faster to read array elements than to repeatedly call Math.sin() and
Math.cos() elsewhere; performance matters. (Chapter 4 introduces Math.sin() and Math.cos().)
A class can declare a mix of instance initializers and instance field initializers, as shown in Listing 219.
Listing 2-19. Mixing instance initializers with instance field initializers
class C
{
{
System.out.println("instance initializer 1");
}
int counter = 1;
{
System.out.println("instance initializer 2");
System.out.println("counter = "+counter);
}
}
Listing 2-19 declares a class named C that specifies two instance initializers and one instance field
initializer. When the Java compiler compiles a class into a classfile, it creates a special void <init>()
method representing the default noargument constructor when no constructor is explicitly declared;
otherwise, it create an <init>() method for each encountered constructor. Furthermore, it stores in
each constructor the bytecode equivalent of all instance initializers and instance field initializers in the
order they occur (from top to bottom).
80
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Note <init> is not a valid Java method name, but is a valid name from the runtime perspective. The angle
brackets were chosen as part of the name to prevent a name conflict with any init() methods that you might
declare in the class.
For class C, <init>() would first contain the bytecode equivalent of System.out.println("instance
initializer 1");, it would next contain the bytecode equivalent of int counter = 1;, and it would
finally contain the bytecode equivalent of System.out.println("instance initializer 2");
System.out.println("counter = "+counter);.
When new C() executes, <init>() executes immediately and generates the following output:
instance initializer 1
instance initializer 2
counter = 1
■ Note You should rarely need to use the instance initializer, which is not commonly used in industry.
Initialization Order
A class’s body can contain a mixture of class field initializers, class initializers, instance field initializers,
instance initializers, and constructors. (You should prefer constructors to instance field initializers,
although I am guilty of not doing so consistently, and restrict your use of instance initializers to
anonymous classes.) Furthermore, class fields and instance fields initialize to default values.
Understanding the order in which all of this initialization occurs is necessary to preventing confusion, so
check out Listing 2-20.
Listing 2-20. A complete initialization demo
class InitDemo
{
static double double1;
double double2;
static int int1;
int int2;
static String string1;
String string2;
static
{
System.out.println("[class] double1 = "+double1);
System.out.println("[class] int1 = "+int1);
System.out.println("[class] string1 = "+string1);
System.out.println();
}
{
81
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
System.out.println("[instance] double2 = "+double2);
System.out.println("[instance] int2 = "+int2);
System.out.println("[instance] string2 = "+string2);
System.out.println();
}
static
{
double1 = 1.0;
int1 = 1000000000;
string1 = "abc";
}
{
double2 = 1.0;
int2 = 1000000000;
string2 = "abc";
}
InitDemo()
{
System.out.println("InitDemo() called");
System.out.println();
}
static double double3 = 10.0;
double double4 = 10.0;
static
{
System.out.println("[class] double3 = "+double3);
System.out.println();
}
{
System.out.println("[instance] double4 = "+double3);
System.out.println();
}
public static void main(String[] args)
{
System.out.println ("main() started");
System.out.println();
System.out.println("[class] double1 = "+double1);
System.out.println("[class] double3 = "+double3);
System.out.println("[class] int1 = "+int1);
System.out.println("[class] string1 = "+string1);
System.out.println();
for (int i = 0; i < 2; i++)
{
System.out.println("About to create InitDemo object");
System.out.println();
InitDemo id = new InitDemo();
System.out.println("id created");
System.out.println();
System.out.println("[instance] id.double2 = "+id.double2);
System.out.println("[instance] id.double4 = "+id.double4);
System.out.println("[instance] id.int2 = "+id.int2);
System.out.println("[instance] id.string2 = "+id.string2);
82
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
System.out.println();
}
}
}
Listing 2-20’s InitDemo class declares two class fields and two instance fields for the double
precision floating-point primitive type, one class field and one instance field for the integer primitive
type, and one class field and one instance field for the String reference type. It also introduces one
explicitly initialized class field, one explicitly initialized instance field, three class initializers, three
instance initializers, and one constructor. If you compile and run this code, you will observe the
following output:
[class] double1 = 0.0
[class] int1 = 0
[class] string1 = null
[class] double3 = 10.0
main() started
[class]
[class]
[class]
[class]
double1 = 1.0
double3 = 10.0
int1 = 1000000000
string1 = abc
About to create InitDemo object
[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null
[instance] double4 = 10.0
InitDemo() called
id created
[instance]
[instance]
[instance]
[instance]
id.double2 = 1.0
id.double4 = 10.0
id.int2 = 1000000000
id.string2 = abc
About to create InitDemo object
[instance] double2 = 0.0
[instance] int2 = 0
[instance] string2 = null
[instance] double4 = 10.0
InitDemo() called
83
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
id created
[instance]
[instance]
[instance]
[instance]
id.double2 = 1.0
id.double4 = 10.0
id.int2 = 1000000000
id.string2 = abc
As you study this output in conjunction with the aforementioned discussion of class initializers and
instance initializers, you will discover some interesting facts about initialization:
•
Class fields initialize to default or explicit values just after a class is loaded.
Immediately after a class loads, all class fields are zeroed to default values. Code
within the <clinit>() method performs explicit initialization.
•
All class initialization occurs prior to the <clinit>() method returning.
•
Instance fields initialize to default or explicit values during object creation. When
new allocates memory for an object, it zeroes all instance fields to default values.
Code within an <init>() method performs explicit initialization.
•
All instance initialization occurs prior to the <init>() method returning.
Additionally, because initialization occurs in a top-down manner, attempting to access the contents
of a class field before that field is declared, or attempting to access the contents of an instance field
before that field is declared causes the compiler to report an illegal forward reference.
Inheriting State and Behaviors
We tend to categorize stuff by saying things like “cars are vehicles” or “savings accounts are bank
accounts.” By making these statements, we really are saying that cars inherit vehicular state (e.g., make
and color) and behaviors (e.g., park and display mileage), and that savings accounts inherit bank
account state (e.g., balance) and behaviors (e.g., deposit and withdraw). Car, vehicle, savings account,
and bank account are examples of real-world entity categories, and inheritance is a hierarchical
relationship between similar entity categories in which one category inherits state and behaviors from at
least one other entity category. Inheriting from a single category is called single inheritance, and
inheriting from at least two categories is called multiple inheritance.
Java supports single inheritance and multiple inheritance to facilitate code reuse—why reinvent the
wheel? Java supports single inheritance in a class context, in which a class inherits state and behaviors
from another class through class extension. Because classes are involved, Java refers to this kind of
inheritance as implementation inheritance.
Java supports multiple inheritance only in an interface context, in which a class inherits behavior
templates from one or more interfaces through interface implementation, or in which an interface
inherits behavior templates from one or more interfaces through interface extension. Because interfaces
are involved, Java refers to this kind of inheritance as interface inheritance. (I discuss interfaces later in
this chapter.)
This section introduces you to Java’s support for implementation inheritance by first focusing on
class extension. It then introduces you to a special class that sits at the top of Java’s class hierarchy. After
introducing you to composition, which is an alternative to implementation inheritance for reusing code,
this section shows you how composition can be used to overcome problems with implementation
inheritance.
84
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Extending Classes
Java provides the reserved word extends for specifying a hierarchical relationship between two classes.
For example, suppose you have a Vehicle class and want to introduce Car and Truck classes that extend
Vehicle. Listing 2-21 uses extends to cement these relationships.
Listing 2-21. Relating classes via extends
class
{
//
}
class
{
//
}
class
{
//
}
Vehicle
member declarations
Car extends Vehicle
member declarations
Truck extends Vehicle
Member declarations
Listing 2-21 codifies relationships that are known as “is-a” relationships: a car or a truck is a kind of
vehicle. In this relationship, Vehicle is known as the base class, parent class, or superclass; and each of
Car and Truck is known as the derived class, child class, or subclass.
■ Caution You cannot extend a final class. For example, if you declared Vehicle as final class Vehicle, the
compiler would report an error upon encountering class Car extends Vehicle or class Truck extends
Vehicle. Developers declare their classes final when they do not want these classes to be extended (for security
or other reasons).
As well as being capable of providing its own member declarations, each of Car and Truck is capable
of inheriting member declarations from its Vehicle superclass. As Listing 2-22 shows, non-private
inherited members become accessible to members of the Car and Truck classes.
Listing 2-22. Inheriting members
class Vehicle
{
private String make;
private String model;
private int year;
Vehicle(String make, String model, int year)
{
this.make = make;
this.model = model;
this.year = year;
85
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
}
String getMake()
{
return make;
}
String getModel()
{
return model;
}
int getYear()
{
return year;
}
}
class Car extends Vehicle
{
private int numWheels;
Car(String make, String model, int year, int numWheels)
{
super(make, model, year);
this.numWheels = numWheels;
}
public static void main(String[] args)
{
Car car = new Car("Toyota", "Camry", 2011, 4);
System.out.println("Make = "+car.getMake());
System.out.println("Model = "+car.getModel());
System.out.println("Year = "+car.getYear());
System.out.println("Number of wheels = "+car.numWheels);
System.out.println();
car = new Car("Aptera Motors", "Aptera 2e/2h", 2012, 3);
System.out.println("Make = "+car.getMake());
System.out.println("Model = "+car.getModel());
System.out.println("Year = "+car.getYear());
System.out.println("Number of wheels = "+car.numWheels);
}
}
class Truck extends Vehicle
{
private boolean isExtendedCab;
Truck(String make, String model, int year, boolean isExtendedCab)
{
super(make, model, year);
this.isExtendedCab = isExtendedCab;
}
public static void main(String[] args)
{
Truck truck = new Truck("Chevrolet", "Silverado", 2011, true);
System.out.println("Make = "+truck.getMake());
System.out.println("Model = "+truck.getModel());
System.out.println("Year = "+truck.getYear());
System.out.println("Extended cab = "+truck.isExtendedCab);
86
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
}
}
Listing 2-22’s Vehicle class declares private fields that store a vehicle’s make, model, and year; a
constructor that initializes these fields to passed arguments; and getter methods that retrieve these
fields’ values.
The Car subclass provides a private numWheels field, a constructor that initializes a Car object’s
Vehicle and Car layers, and a main() class method for testing this class. Similarly, the Truck subclass
provides a private isExtendedCab field, a constructor that initializes a Truck object’s Vehicle and Truck
layers, and a main() class method for testing this class.
Car’s and Truck’s constructors use reserved word super to call Vehicle’s constructor with Vehicleoriented arguments, and then initialize Car’s numWheels and Truck’s isExtendedCab instance fields,
respectively. The super() call is analogous to specifying this() to call another constructor in the same
class, but invokes a superclass constructor instead.
■ Caution The super() call can only appear in a constructor. Furthermore, it must be the first code that is
specified in the constructor. If super() is not specified, and if the superclass does not have a noargument
constructor, the compiler will report an error because the subclass constructor must call a noargument superclass
constructor when super() is not present.
Car’s main() method creates two Car objects, initializing each object to a specific make, model, year,
and number of wheels. Four System.out.println() method calls subsequently output each object’s
information. Similarly, Truck’s main() method creates a single Truck object, and also initializes this
object to a specific make, model, year, and flag (Boolean true/false value) indicating that the truck is an
extended cab. The first three System.out.println() method calls retrieve their pieces of information by
calling a Car or Truck instance’s inherited getMake(), getModel(), and getYear() methods.
The final System.out.println() method call directly accesses the instance’s numWheels or
isExtendedCab instance field. Although it’s generally not a good idea to access an instance field directly
(because it violates information hiding), each of the Car and Truck class’s main() methods, which
provides this access, is present only to test these classes and would not exist in a real application that
uses these classes.
Assuming that Listing 2-22 is stored in a file named Vehicle.java, execute javac Vehicle.java to
compile this source code into Vehicle.class, Car.class, and Truck.class classfiles. Then execute java
Car to test the Car class. This execution results in the following output:
Make = Toyota
Model = Camry
Year = 2011
Number of wheels = 4
Make = Aptera Motors
Model = Aptera 2e/2h
Year = 2012
Number of wheels = 3
Continuing, execute java Truck to test the Truck class. This execution results in the following
output:
87
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Make = Chevrolet
Model = Silverado
Year = 2011
Extended cab = true
■ Note A class whose instances cannot be modified is known as an immutable class. Vehicle is an example. If
Car’s and Truck’s main() methods, which can directly read/write numWheels or isExtendedCab, were not
present, Car and Truck would also be examples of immutable classes. Also, a class cannot inherit constructors,
nor can it inherit private fields and methods. For example, Car does not inherit Vehicle’s constructor, nor does it
inherit Vehicle’s private make, model, and year fields.
A subclass can override (replace) an inherited method so that the subclass’s version of the method is
called instead. Listing 2-23 shows you that the overriding method must specify the same name,
parameter list, and return type as the method being overridden.
Listing 2-23. Overriding a method
class Vehicle
{
private String make;
private String model;
private int year;
Vehicle(String make, String model, int year)
{
this.make = make;
this.model = model;
this.year = year;
}
void describe()
{
System.out.println(year+" "+make+" "+model);
}
}
class Car extends Vehicle
{
private int numWheels;
Car(String make, String model, int year, int numWheels)
{
super(make, model, year);
}
void describe()
{
System.out.print("This car is a "); // Print without newline – see Chapter 1.
super.describe();
}
public static void main(String[] args)
88
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
Car car = new Car("Ford", "Fiesta", 2009, 4);
car.describe();
}
}
Listing 2-23’s Car class declares a describe() method that overrides Vehicle’s describe() method to
output a car-oriented description. This method uses reserved word super to call Vehicle’s describe()
method via super.describe();.
This book was purchased by [email protected]
■ Note Call a superclass method from the overriding subclass method by prefixing the method’s name with
reserved word super and the member access operator. If you don’t do this, you end up recursively calling the
subclass’s overriding method. Use super and the member access operator to access non-private superclass
fields from subclasses that mask these fields by declaring same-named fields.
If you were to compile Listing 2-23 (javac Vehicle.java) and run the Car application (java Car), you
would discover that Car’s overriding describe() method executes instead of Vehicle’s overridden
describe() method, and outputs This car is a 2009 Ford Fiesta.
■ Caution You cannot override a final method. For example, if Vehicle’s describe() method was declared as
final void describe(), the compiler would report an error upon encountering an attempt to override this
method in the Car class. Developers declare their methods final when they do not want these methods to be
overridden (for security or other reasons). Also, you cannot make an overriding method less accessible than the
method it overrides. For example, if Car’s describe() method was declared as private void describe(), the
compiler would report an error because private access is less accessible than the default package access.
However, describe() could be made more accessible by declaring it public, as in public void describe().
Suppose you were to replace Listing 2-23’s describe() method with the method shown here:
void describe(String owner)
{
System.out.print("This car, which is owned by "+owner+", is a ");
super.describe();
}
The modified Car class now has two describe() methods, the preceding explicitly declared method
and the method inherited from Vehicle. The void describe(String owner) method does not override
Vehicle’s describe() method. Instead, it overloads this method.
The Java compiler helps you detect an attempt to overload instead of override a method at compile
time by letting you prefix a subclass’s method header with the @Override annotation, as shown below—I
discuss annotations in Chapter 3:
89
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
@Override
void describe()
{
System.out.print("This car is a ");
super.describe();
}
Specifying @Override tells the compiler that the method overrides another method. If you overload
the method instead, the compiler reports an error. Without this annotation, the compiler would not
report an error because method overloading is a valid feature.
■ Tip Get into the habit of prefixing overriding methods with the @Override annotation. This habit will help you
detect overloading mistakes much sooner.
I previously presented the initialization order of classes and objects, where you learned that class
members are always initialized first, and in a top-down order (the same order applies to instance
members). Implementation inheritance adds a couple more details:
•
A superclass’s class initializers always execute before a subclass’s class initializers.
•
A subclass’s constructor always calls the superclass constructor to initialize an
object’s superclass layer before initializing the subclass layer.
Java’s support for implementation inheritance only permits you to extend a single class. You cannot
extend multiple classes because doing so can lead to problems. For example, suppose Java supported
multiple implementation inheritance, and you decided to model a flying horse (from Greek mythology)
via the class structure shown in Listing 2-24.
Listing 2-24. A fictional demonstration of multiple implementation inheritance
class Horse
{
void describe()
{
// Code that outputs a description of a horse's appearance and behaviors.
}
}
class Bird
{
void describe()
{
// Code that outputs a description of a bird's appearance and behaviors.
}
}
class FlyingHorse extends Horse, Bird
{
public static void main(String[] args)
{
90
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
FlyingHorse pegasus = new FlyingHorse();
pegasus.describe();
}
}
This class structure reveals an ambiguity resulting from each of Horse and Bird declaring a
describe() method. Which of these methods does FlyingHorse inherit? A related ambiguity arises from
same-named fields, possibly of different types. Which field is inherited?
The Ultimate Superclass
A class that does not explicitly extend another class implicitly extends Java’s Object class (located in the
java.lang package—I will discuss packages in the next chapter). For example, Listing 2-1’s Image class
extends Object, whereas Listing 2-21’s Car and Truck classes extend Vehicle, which extends Object.
Object is Java’s ultimate superclass because it serves as the ancestor of every other class, but does
not itself extend any other class. Object provides a common set of methods that other classes inherit.
Table 2-1 describes these methods.
Table 2-1. Object’s Methods
Method
Description
Object clone()
Create and return a copy of the current object.
boolean equals(Object obj)
Determine whether the current object is equal to the
object identified by obj.
void finalize()
Finalize the current object.
Class<?> getClass()
Return the current object’s Class object.
int hashCode()
Return the current object’s hash code.
void notify()
Wake up one of the threads that are waiting on the
current object’s monitor.
void notifyAll()
Wake up all threads that are waiting on the current
object’s monitor.
String toString()
Return a string representation of the current object.
void wait()
Cause the current thread to wait on the current
object’s monitor until it is woken up via notify() or
notifyAll().
void wait(long timeout)
Cause the current thread to wait on the current
object’s monitor until it is woken up via notify() or
notifyAll(), or until the specified timeout value (in
91
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
milliseconds) has elapsed, whichever comes first.
void wait(long timeout, int nanos)
Cause the current thread to wait on the current
object’s monitor until it is woken up via notify() or
notifyAll(), or until the specified timeout value (in
milliseconds) plus nanos value (in nanoseconds) has
elapsed, whichever comes first.
I will discuss the clone(), equals(), finalize(), hashCode(), and toString() methods shortly, but
defer a discussion of getClass(), notify(), notifyAll(), and the wait() methods to Chapter 4.
■ Note Chapter 6 introduces you to the java.util.Objects class, which provides several null-safe or nulltolerant class methods for comparing two objects, computing the hash code of an object, requiring that a
reference not be null, and returning a string representation of an object.
Cloning
The clone() method clones (duplicates) an object without calling a constructor. It copies each primitive
or reference field’s value to its counterpart in the clone, a task known as shallow copying or shallow
cloning. Listing 2-25 demonstrates this behavior.
Listing 2-25. Shallowly cloning an Employee object
class Employee implements Cloneable
{
String name;
int age;
Employee(String name, int age)
{
this.name = name;
this.age = age;
}
public static void main(String[] args) throws CloneNotSupportedException
{
Employee e1 = new Employee("John Doe", 46);
Employee e2 = (Employee) e1.clone();
System.out.println(e1 == e2); // Output: false
System.out.println(e1.name == e2.name); // Output: true
}
}
Listing 2-25 declares an Employee class with name and age instance fields, and a constructor for
initializing these fields. The main() method uses this constructor to initialize a new Employee object’s
copies of these fields to John Doe and 46.
92
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Note A class must implement the java.lang.Cloneable interface or its instances cannot be shallowly cloned
via Object’s clone() method—this method performs a runtime check to see if the class implements Cloneable.
(I will discuss interfaces later in this chapter.) If a class does not implement Cloneable, clone() throws
java.lang.CloneNotSupportedException. (Because CloneNotSupportedException is a checked exception, it is
necessary for Listing 2-25 to satisfy the compiler by appending throws CloneNotSupportedException to the
main() method’s header. I will discuss exceptions in the next chapter.) String is an example of a class that does
not implement Cloneable; hence, String objects cannot be shallowly cloned.
After assigning the Employee object’s reference to local variable e1, main() calls the clone() method
on this variable to duplicate the object, and then assigns the resulting reference to variable e2. The
(Employee) cast is needed because clone() returns Object.
To prove that the objects whose references were assigned to e1 and e2 are different, main() next
compares these references via == and outputs the Boolean result, which happens to be false. To prove
that the Employee object was shallowly cloned, main() next compares the references in both Employee
objects’ name fields via == and outputs the Boolean result, which happens to be true.
■ Note Object’s clone() method was originally specified as a public method, which meant that any object
could be cloned from anywhere. For security reasons, this access was later changed to protected, which means
that only code within the same package as the class whose clone() method is to be called, or code within a
subclass of this class (regardless of package) can call clone().
Shallow cloning is not always desirable because the original object and its clone refer to the same
object via their equivalent reference fields. For example, each of Listing 2-25’s two Employee objects
refers to the same String object via its name field.
Although not a problem for String, whose instances are immutable, changing a mutable object via
the clone’s reference field causes the original (noncloned) object to see the same change via its reference
field. For example, suppose you add a reference field named hireDate to Employee. This field is of type
Date with year, month, and day instance fields. Because Date is intended to be mutable, you can change
the contents of these fields in the Date instance assigned to hireDate.
Now suppose you plan to change the clone’s date, but want to preserve the original Employee
object’s date. You cannot do this with shallow cloning because the change is also visible to the original
Employee object. To solve this problem, you must modify the cloning operation so that it assigns a new
Date reference to the Employee clone’s hireDate field. This task, which is known as deep copying or deep
cloning, is demonstrated in Listing 2-26.
Listing 2-26. Deeply cloning an Employee object
class Date
{
int year, month, day;
93
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Date(int year, int month, int day)
{
this.year = year;
this.month = month;
this.day = day;
}
}
class Employee implements Cloneable
{
String name;
int age;
Date hireDate;
Employee(String name, int age, Date hireDate)
{
this.name = name;
this.age = age;
this.hireDate = hireDate;
}
@Override
protected Object clone() throws CloneNotSupportedException
{
Employee emp = (Employee) super.clone();
if (hireDate != null) // no point cloning a null object (one that does not exist)
emp.hireDate = new Date(hireDate.year, hireDate.month, hireDate.day);
return emp;
}
public static void main(String[] args) throws CloneNotSupportedException
{
Employee e1 = new Employee("John Doe", 46, new Date(2000, 1, 20));
Employee e2 = (Employee) e1.clone();
System.out.println(e1 == e2); // Output: false
System.out.println(e1.name == e2.name); // Output: true
System.out.println(e1.hireDate == e2.hireDate); // Output: false
System.out.println(e2.hireDate.year+" "+e2.hireDate.month+" "+
e2.hireDate.day); // Output: 2000 1 20
}
}
Listing 2-26 declares Date and Employee classes. The Date class declares year, month, and day fields
and a constructor.
Employee overrides the clone() method to deeply clone the hireDate field. This method first calls
Object’s clone() method to shallowly clone the current Employee object’s instance fields, and then stores
the new object’s reference in emp. It next assigns a new Date object’s reference to emp’s hireDate field; this
object’s fields are initialized to the same values as those in the original Employee object’s hireDate
instance.
At this point, you have an Employee clone with shallowly cloned name and age fields, and a deeply
cloned hireDate field. The clone() method finishes by returning this Employee clone.
94
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Note If you are not calling Object’s clone() method from an overriding clone() method (because you prefer
to deeply clone reference fields and do your own shallow copying of nonreference fields), it isn’t necessary for the
class containing the overriding clone() method to implement Cloneable, but it should implement this interface
for consistency. String does not override clone(), so String objects cannot be deeply cloned.
Equality
The == and != operators compare two primitive values (such as integers) for equality (==) or inequality
(!=). These operators also compare two references to see whether they refer to the same object or not.
This latter comparison is known as an identity check.
You cannot use == and != to determine whether two objects are logically the same (or not). For
example, two Truck objects with the same field values are logically equivalent. However, == reports them
as unequal because of their different references.
■ Note Because == and != perform the fastest possible comparisons, and because string comparisons need to be
performed quickly (especially when sorting a huge number of strings), the String class contains special support
that allows literal strings and string-valued constant expressions to be compared via == and !=. (I will discuss this
support when I present String in Chapter 4.) The following statements demonstrate these comparisons:
System.out.println("abc" == "abc"); // Output: true
System.out.println("abc" == "a"+"bc"); // Output: true
System.out.println("abc" == "Abc"); // Output: false
System.out.println("abc" != "def"); // Output: true
System.out.println("abc" == new String("abc")); // Output: false
Recognizing the need to support logical equality in addition to reference equality, Java provides an
equals() method in the Object class. Because this method defaults to comparing references, you need to
override equals() to compare object contents.
Before overriding equals(), make sure that this is necessary. For example, Java’s
java.lang.StringBuffer class (discussed in Chapter 4) does not override equals(). Perhaps this class’s
designers did not think it necessary to determine if two StringBuffer objects are logically equivalent.
You cannot override equals() with arbitrary code. Doing so will probably prove disastrous to your
applications. Instead, you need to adhere to the contract that is specified in the Java documentation for
this method, and which I present next.
The equals() method implements an equivalence relation on nonnull object references:
•
It is reflexive: For any nonnull reference value x, x.equals(x) returns true.
95
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
•
It is symmetric: For any nonnull reference values x and y, x.equals(y) returns true
if and only if y.equals(x) returns true.
•
It is transitive: For any nonnull reference values x, y, and z, if x.equals(y) returns
true and y.equals(z) returns true, then x.equals(z) returns true.
•
It is consistent: For any nonnull reference values x and y, multiple invocations of
x.equals(y) consistently return true or consistently return false, provided no
information used in equals() comparisons on the objects is modified.
•
For any nonnull reference value x, x.equals(null) returns false.
Although this contract probably looks somewhat intimidating, it is not that difficult to satisfy. For
proof, take a look at the implementation of the equals() method in Listing 2-27’s Point class.
Listing 2-27. Logically comparing Point objects
class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
@Override
public boolean equals(Object o)
{
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
public static void main(String[] args)
{
Point p1 = new Point(10, 20);
Point p2 = new Point(20, 30);
Point p3 = new Point(10, 20);
// Test reflexivity
System.out.println(p1.equals(p1)); // Output: true
// Test symmetry
System.out.println(p1.equals(p2)); // Output: false
System.out.println(p2.equals(p1)); // Output: false
// Test transitivity
System.out.println(p2.equals(p3)); // Output: false
System.out.println(p1.equals(p3)); // Output: true
// Test nullability
System.out.println(p1.equals(null)); // Output: false
// Extra test to further prove the instanceof operator's usefulness.
System.out.println(p1.equals("abc")); // Output: false
}
96
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
}
Listing 2-27’s overriding equals() method begins with an if statement that uses the instanceof
operator to determine whether the argument passed to parameter o is an instance of the Point class. If
not, the if statement executes return false;.
The o instanceof Point expression satisfies the last portion of the contract: For any nonnull
reference value x, x.equals(null) returns false. Because the null reference is not an instance of any class,
passing this value to equals() causes the expression to evaluate to false.
The o instanceof Point expression also prevents a java.lang.ClassCastException instance from
being thrown via expression (Point) o in the event that you pass an object other than a Point object to
equals(). (I will discuss exceptions in the next chapter.)
Following the cast, the contract’s reflexivity, symmetry, and transitivity requirements are met by
only allowing Points to be compared with other Points, via expression p.x == x && p.y == y.
The final contract requirement, consistency, is met by making sure that the equals() method is
deterministic. In other words, this method does not rely on any field value that could change from
method call to method call.
■ Tip You can optimize the performance of a time-consuming equals() method by first using == to determine if
o’s reference identifies the current object. Simply specify if (o == this) return true; as the equals()
method’s first statement. This optimization is not necessary in Listing 2-27’s equals() method, which has
satisfactory performance.
It is important to always override the hashCode() method when overriding equals(). I did not do so
in Listing 2-27 because I have yet to formally introduce hashCode().
Finalization
Finalization refers to cleanup via the finalize() method, which is known as a finalizer. The finalize()
method’s Java documentation states that finalize() is “called by the garbage collector on an object
when garbage collection determines that there are no more references to the object. A subclass overrides
the finalize() method to dispose of system resources or to perform other cleanup.”
Object’s version of finalize() does nothing; you must override this method with any needed
cleanup code. Because the JVM might never call finalize() before an application terminates, you
should provide an explicit cleanup method, and have finalize() call this method as a safety net in case
the method is not otherwise called.
■ Caution Never depend on finalize() for releasing limited resources such as graphics contexts or file
descriptors. For example, if an application object opens files, expecting that its finalize() method will close
them, the application might find itself unable to open additional files when a tardy JVM is slow to call finalize().
What makes this problem worse is that finalize() might be called more frequently on another JVM, resulting in
97
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
this too-many-open-files problem not revealing itself. The developer might thus falsely believe that the application
behaves consistently across different JVMs.
If you decide to override finalize(), your object’s subclass layer must give its superclass layer an
opportunity to perform finalization. You can accomplish this task by specifying super.finalize(); as
the last statement in your method, which the following example demonstrates:
@Override
protected void finalize() throws Throwable
{
try
{
// Perform subclass cleanup.
}
finally
{
super.finalize();
}
}
The example’s finalize() declaration appends throws Throwable to the method header because the
cleanup code might throw an exception. If an exception is thrown, execution leaves the method and, in
the absence of try-finally, super.finalize(); never executes. (I will discuss exceptions and try-finally in
Chapter 3.)
To guard against this possibility, the subclass’s cleanup code executes in a block that follows
reserved word try. If an exception is thrown, Java’s exception-handling logic executes the block
following the finally reserved word, and super.finalize(); executes the superclass’s finalize()
method.
The finalize() method has often been used to perform resurrection (making an unreferenced
object referenced), to implement object pools that recycle the same objects when these objects are
expensive (time-wise) to create (database connection objects are an example).
Resurrection occurs when you assign this (a reference to the current object) to a class or instance
field (or to another long-lived variable). For example, you might specify r = this; within finalize() to
assign the unreferenced object identified as this to a class field named r.
Because of the possibility for resurrection, there is a severe performance penalty imposed on the
garbage collection of an object that overrides finalize(). You’ll learn about this penalty and a better
alternative to overriding finalize() in Chapter 4.
■ Note A resurrected object’s finalizer cannot be called again.
Hash Codes
The hashCode() method returns a 32-bit integer that identifies the current object’s hash code, a small
value that results from applying a mathematical function to a potentially large amount of data. The
calculation of this value is known as hashing.
98
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
This book was purchased by [email protected]
You must override hashCode() when overriding equals(), and in accordance with the following
contract, which is specified in hashCode()’s Java documentation:
•
Whenever it is invoked on the same object more than once during an execution of
a Java application, the hashCode() method must consistently return the same
integer, provided no information used in equals(Object) comparisons on the
object is modified. This integer need not remain consistent from one execution of
an application to another execution of the same application.
•
If two objects are equal according to the equals(Object) method, then calling the
hashCode() method on each of the two objects must produce the same integer
result.
•
It is not required that if two objects are unequal according to the equals(Object)
method, then calling the hashCode() method on each of the two objects must
produce distinct integer results. However, the programmer should be aware that
producing distinct integer results for unequal objects might improve the
performance of hash tables.
Fail to obey this contract and your class’s instances will not work properly with Java’s hash-based
Collections Framework classes, such as java.util.HashMap. (I will discuss HashMap and other Collections
Framework classes in Chapter 5.)
If you override equals() but not hashCode(), you most importantly violate the second item in the
contract: The hash codes of equal objects must also be equal. This violation can lead to serious
consequences, as demonstrated in the following example:
java.util.Map<Point, String> map = new java.util.HashMap<>();
map.put(p1, "first point");
System.out.println(map.get(p1)); // Output: first point
System.out.println(map.get(new Point(10, 20))); // Output: null
Assume that the example’s statements are appended to Listing 2-27’s main() method—the
java.util. prefix, <Point, String>, and <> have to do with packages and generics, which I discuss in
Chapter 3.
After main() creates its Point objects and calls its System.out.println() methods, it executes this
example’s statements, which perform the following tasks:
•
The first statement instantiates HashMap, which is in the java.util package.
•
The second statement calls HashMap’s put() method to store Listing 2-27’s p1
object key and the "first point" value in the hashmap.
•
The third statement retrieves the value of the hashmap entry whose Point key is
logically equal to p1 via HashMap’s get() method.
•
The fourth statement is equivalent to the third statement, but returns the null
reference instead of "first point".
Although objects p1 and Point(10, 20) are logically equivalent, these objects have different hash
codes, resulting in each object referring to a different entry in the hashmap. If an object is not stored (via
put()) in that entry, get() returns null.
Correcting this problem requires that hashCode() be overridden to return the same integer value for
logically equivalent objects. I’ll show you how to accomplish this task when I discuss HashMap in Chapter
5.
99
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
String Representation
The toString() method returns a string-based representation of the current object. This representation
defaults to the object’s class name, followed by the @ symbol, followed by a hexadecimal representation
of the object’s hash code.
For example, if you were to execute System.out.println(p1); to output Listing 2-27’s p1 object, you
would see a line of output similar to [email protected] (System.out.println() calls p1’s inherited toString()
method behind the scenes.)
You should strive to override toString() so that it returns a concise but meaningful description of
the object. For example, you might declare, in Listing 2-27’s Point class, a toString() method that is
similar to the following:
@Override
public String toString()
{
return "("+x+", "+y+")";
}
This time, executing System.out.println(p1); results in more meaningful output, such as (10, 20).
Composition
Implementation inheritance and composition offer two different approaches to reusing code. As you
have learned, implementation inheritance is concerned with extending a class with a new class, which is
based upon an “is-a” relationship between them: a Car is a Vehicle, for example.
On the other hand, composition is concerned with composing classes out of other classes, which is
based upon a “has-a” relationship between them. For example, a Car has an Engine, Wheels, and a
SteeringWheel.
You have already seen examples of composition in this chapter. For example, Listing 2-3’s Car class
includes String make and String model fields. Listing 2-28’s Car class provides another example of
composition.
Listing 2-28. A Car class whose instances are composed of other objects
class Car extends Vehicle
{
private Engine engine;
private Wheel[] wheels;
private SteeringWheel steeringWheel;
}
Listing 2-28 demonstrates that composition and implementation inheritance are not mutually
exclusive. Although not shown, Car inherits various members from its Vehicle superclass, in addition to
providing its own engine, wheels, and steeringwheel instance fields.
The Trouble with Implementation Inheritance
Implementation inheritance is potentially dangerous, especially when the developer does not have
complete control over the superclass, or when the superclass is not designed and documented with
extension in mind.
100
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
The problem is that implementation inheritance breaks encapsulation. The subclass relies on
implementation details in the superclass. If these details change in a new version of the superclass, the
subclass might break, even if the subclass is not touched.
For example, suppose you have purchased a library of Java classes, and one of these classes
describes an appointment calendar. Although you do not have access to this class’s source code, assume
that Listing 2-29 describes part of its code.
Listing 2-29. An appointment calendar class
public class ApptCalendar
{
private final static int MAX_APPT = 1000;
private Appt[] appts;
private int size;
public ApptCalendar()
{
appts = new Appt[MAX_APPT];
size = 0; // redundant because field automatically initialized to 0
// adds clarity, however
}
public void addAppt(Appt appt)
{
if (size == appts.length)
return; // array is full
appts[size++] = appt;
}
public void addAppts(Appt[] appts)
{
for (int i = 0; i < appts.length; i++)
addAppt(appts[i]);
}
}
Listing 2-29’s ApptCalendar class stores an array of appointments, with each appointment described
by an Appt instance. For this discussion, Appt’s details are irrelevant—it could be as trivial as class Appt
{}.
Suppose you want to log each appointment in a file. Because a logging capability is not provided,
you extend ApptCalendar with Listing 2-30’s LoggingApptCalendar class, which adds logging behavior in
overriding addAppt() and addAppts() methods.
101
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-30. Extending the appointment calendar class
public class LoggingApptCalendar extends ApptCalendar
{
// A constructor is not necessary because the Java compiler will add a
// noargument constructor that calls the superclass's noargument
// constructor by default.
@Override
public void addAppt(Appt appt)
{
Logger.log(appt.toString());
super.addAppt(appt);
}
@Override
public void addAppts(Appt[] appts)
{
for (int i = 0; i < appts.length; i++)
Logger.log(appts[i].toString());
super.addAppts(appts);
}
}
Listing 2-30’s LoggingApptCalendar class relies on a Logger class whose void log(String msg) class
method logs a string to a file (the details are unimportant). Notice the use of toString() to convert an
Appt object to a String object, which is then passed to log().
Although this class looks okay, it does not work as you might expect. Suppose you instantiate this
class and add a few Appt instances to this instance via addAppts(), in the following manner:
LoggingApptCalendar lapptc = new LoggingApptCalendar();
lapptc.addAppts(new Appt[] {new Appt(), new Appt(), new Appt()});
If you also add a System.out.println(msg); method call to Logger’s log(String msg) method, to
output this method’s argument to standard output, you will discover that log() outputs a total of six
messages; each of the expected three messages (one per Appt object) is duplicated.
When LoggingApptCalendar’s addAppts() method is called, it first calls Logger.log() for each Appt
instance in the appts array that is passed to addAppts(). This method then calls ApptCalendar’s
addAppts() method via super.addAppts(appts);.
ApptCalendar’s addAppts() method calls LoggingApptCalendar’s overriding addAppt() method for
each Appt instance in its appts array argument. addAppt() executes Logger.log(appt.toString()); to log
its appt argument’s string representation, and you end up with three additional logged messages.
If you did not override the addAppts() method, this problem would go away. However, the subclass
would be tied to an implementation detail: ApptCalendar’s addAppts() method calls addAppt().
It is not a good idea to rely on an implementation detail when the detail is not documented. (I
previously stated that you do not have access to ApptCalendar’s source code.) When a detail is not
documented, it can change in a new version of the class.
Because a base class change can break a subclass, this problem is known as the fragile base class
problem. A related cause of fragility that also has to do with overriding methods occurs when new
methods are added to a superclass in a subsequent release.
For example, suppose a new version of the library introduces a new public void addAppt(Appt
appt, boolean unique) method into the ApptCalendar class. This method adds the appt instance to the
102
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
calendar when unique is false, and, when unique is true, it adds the appt instance only if it has not
previously been added.
Because this method has been added after the LoggingApptCalendar class was created,
LoggingApptCalendar does not override the new addAppt() method with a call to Logger.log(). As a
result, Appt instances passed to the new addAppt() method are not logged.
Here is another problem: You introduce a method into the subclass that is not also in the superclass.
A new version of the superclass presents a new method that matches the subclass method signature and
return type. Your subclass method now overrides the superclass method, and probably does not fulfill
the superclass method’s contract.
There is a way to make these problems disappear. Instead of extending the superclass, create a
private field in a new class, and have this field reference an instance of the “superclass.” This task
demonstrates composition because you are forming a “has-a” relationship between the new class and
the “superclass.”
Additionally, have each of the new class’s instance methods call the corresponding “superclass”
method via the “superclass” instance that was saved in the private field, and also return the called
method’s return value. This task is known as forwarding, and the new methods are known as forwarding
methods.
Listing 2-31 presents an improved LoggingApptCalendar class that uses composition and forwarding
to forever eliminate the fragile base class problem and the additional problem of the unanticipated
method overriding.
Listing 2-31. A composed logging appointment calendar class
public class LoggingApptCalendar
{
private ApptCalendar apptCal;
public LoggingApptCalendar(ApptCalendar apptCal)
{
this.apptCal = apptCal;
}
public void addAppt(Appt appt)
{
Logger.log(appt.toString());
apptCal.addAppt(appt);
}
public void addAppts(Appt[] appts)
{
for (int i = 0; i < appts.length; i++)
Logger.log(appts[i].toString());
apptCal.addAppts(appts);
}
}
Listing 2-31’s LoggingApptCalendar class does not depend upon implementation details of the
ApptCalendar class. You can add new methods to ApptCalendar and they will not break
LoggingApptCalendar.
103
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
■ Note Listing 2-31’s LoggingApptCalendar class is an example of a wrapper class, a class whose instances
wrap other instances. Each LoggingApptCalendar instance wraps an ApptCalendar instance.
LoggingApptCalendar is also an example of the Decorator design pattern, which is presented on page 175 of
Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson,
and John Vlissides (Addison-Wesley, 1995; ISBN: 0201633612).
When should you extend a class and when should you use a wrapper class? Extend a class when an
“is-a” relationship exists between the superclass and the subclass, and either you have control over the
superclass or the superclass has been designed and documented for class extension. Otherwise, use a
wrapper class.
What does “design and document for class extension” mean? Design means provide protected
methods that hook into the class’s inner workings (to support writing efficient subclasses), and ensure
that constructors and the clone() method never call overridable methods. Document means clearly
state the impact of overriding methods.
■ Caution Wrapper classes should not be used in a callback framework, an object framework in which an object
passes its own reference to another object (via this) so that the latter object can call the former object’s methods
at a later time. This “calling back to the former object’s method” is known as a callback. Because the wrapped
object does not know of its wrapper class, it passes only its reference (via this), and resulting callbacks do not
involve the wrapper class’s methods.
Changing Form
Some real-world entities can change their forms. For example, water (on Earth as opposed to interstellar
space) is naturally a liquid, but it changes to a solid when frozen, and it changes to a gas when heated to
its boiling point. Insects such as butterflies that undergo metamorphosis are another example.
The ability to change form is known as polymorphism, and is useful to model in a programming
language. For example, code that draws arbitrary shapes can be expressed more concisely by
introducing a single Shape class and its draw() method, and by invoking that method for each Circle
instance, Rectangle instance, and other Shape instance stored in an array. When Shape’s draw() method
is called for an array instance, it is the Circle’s, Rectangle’s or other Shape instance’s draw() method that
gets called. We say that there are many forms of Shape’s draw() method, or that this method is
polymorphic.
Java supports four kinds of polymorphism:
104
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
•
Coercion: An operation serves multiple types through implicit type conversion. For
example, division lets you divide an integer by another integer, or divide a
floating-point value by another floating-point value. If one operand is an integer
and the other operand is a floating-point value, the compiler coerces (implicitly
converts) the integer to a floating-point value, to prevent a type error. (There is no
division operation that supports an integer operand and a floating-point
operand.) Passing a subclass object reference to a method’s superclass parameter
is another example of coercion polymorphism. The compiler coerces the subclass
type to the superclass type, to restrict operations to those of the superclass.
•
Overloading: The same operator symbol or method name can be used in different
contexts. For example, + can be used to perform integer addition, floating-point
addition, or string concatenation, depending on the types of its operands. Also,
multiple methods having the same name can appear in a class (through
declaration and/or inheritance).
•
Parametric: Within a class declaration, a field name can associate with different
types and a method name can associate with different parameter and return
types. The field and method can then take on different types in each class
instance. For example, a field might be of type java.lang.Integer and a method
might return an Integer reference in one class instance, and the same field might
be of type String and the same method might return a String reference in another
class instance. Java supports parametric polymorphism via generics, which I will
discuss in Chapter 3.
•
Subtype: A type can serve as another type’s subtype. When a subtype instance
appears in a supertype context, executing a supertype operation on the subtype
instance results in the subtype’s version of that operation executing. For example,
suppose that Circle is a subclass of Point, and that both classes contain a draw()
method. Assigning a Circle instance to a variable of type Point, and then calling
the draw() method via this variable, results in Circle’s draw() method being
called. Subtype polymorphism partners with implementation inheritance.
Many developers do not regard coercion and overloading as valid kinds of polymorphism. They see
coercion and overloading as nothing more than type conversions and syntactic sugar (syntax that
simplifies a language, making it “sweeter” to use). In contrast, parametric and subtype are regarded as
valid kinds of polymorphism.
This section introduces you to subtype polymorphism through upcasting and late binding. We then
move on to abstract classes and abstract methods, downcasting and runtime type identification, and
covariant return types.
Upcasting and Late Binding
Listing 2-27’s Point class represents a point as an x-y pair. Because a circle (in this example) is an x-y pair
denoting its center, and has a radius denoting its extent, you can extend Point with a Circle class that
introduces a radius field. Check out Listing 2-32.
Listing 2-32. A Circle class extending the Point class
class Circle extends Point
{
private int radius;
105
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
int getRadius()
{
return radius;
}
}
Listing 2-32’s Circle class describes a Circle as a Point with a radius, which implies that you can
treat a Circle instance as if it was a Point instance. Accomplish this task by assigning the Circle instance
to a Point variable, as demonstrated here:
Circle c = new Circle(10, 20, 30);
Point p = c;
The cast operator is not needed to convert from Circle to Point because access to a Circle instance
via Point’s interface is legal. After all, a Circle is at least a Point. This assignment is known as upcasting
because you are implicitly casting up the type hierarchy (from the Circle subclass to the Point
superclass). It is also an example of covariance in that a type with a wider range of values (Circle) is
being converted to a type with a narrower range of values (Point).
After upcasting Circle to Point, you cannot call Circle’s getRadius() method because this method
is not part of Point’s interface. Losing access to subtype features after narrowing it to a superclass seems
useless, but is necessary for achieving subtype polymorphism.
In addition to upcasting the subclass instance to a variable of the superclass type, subtype
polymorphism involves declaring a method in the superclass and overriding this method in the subclass.
For example, suppose Point and Circle are to be part of a graphics application, and you need to
introduce a draw() method into each class to draw a point and a circle, respectively. You end with the
class structure shown in Listing 2-33.
Listing 2-33. Declaring a graphics application’s Point and Circle classes
class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
@Override
public String toString()
106
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
return "("+x+", "+y+")";
}
void draw()
{
System.out.println("Point drawn at "+toString ());
}
}
class Circle extends Point
{
private int radius;
Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
int getRadius()
{
return radius;
}
@Override
public String toString()
{
return ""+radius;
}
@Override
void draw()
{
System.out.println("Circle drawn at "+super.toString()+
" with radius "+toString());
}
}
Listing 2-33’s draw() methods will ultimately draw graphics shapes, but simulating their behaviors
via System.out.println() method calls is sufficient during the early testing phase of the graphics
application.
Now that you have temporarily finished with Point and Circle, you want to test their draw()
methods in a simulated version of the graphics application. To achieve this objective, you write Listing
2-34’s Graphics class.
Listing 2-34. A Graphics class for testing Point’s and Circle’s draw() methods
class Graphics
{
public static void main(String[] args)
{
Point[] points = new Point[] { new Point(10, 20),
new Circle(10, 20, 30) };
for (int i = 0; i < points.length; i++)
points[i].draw();
}
}
107
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-34’s main() method first declares an array of Points. Upcasting is demonstrated by first
having the array’s initializer instantiate the Circle class, and then by assigning this instance’s reference
to the second element in the points array.
Moving on, main() uses a for loop to call each Point element’s draw() method. Because the first
iteration calls Point’s draw() method, whereas the second iteration calls Circle’s draw() method, you
observe the following output:
Point drawn at (10, 20)
Circle drawn at (10, 20) with radius 30
How does Java “know” that it must call Circle’s draw() method on the second loop iteration?
Should it not call Point’s draw() method because Circle is being treated as a Point thanks to the upcast?
At compile time, the compiler does not know which method to call. All it can do is verify that a
method exists in the superclass, and verify that the method call’s arguments list and return type match
the superclass’s method declaration.
In lieu of knowing which method to call, the compiler inserts an instruction into the compiled code
that, at runtime, fetches and uses whatever reference is in points[1] to call the correct draw() method.
This task is known as late binding.
Late binding is used for calls to non-final instance methods. For all other method calls, the
compiler knows which method to call, and inserts an instruction into the compiled code that calls the
method associated with the variable’s type (not its value). This task is known as early binding.
You can also upcast from one array to another provided that the array being upcast is a subtype of
the other array. Consider Listing 2-35.
Listing 2-35. Demonstrating array upcasting
class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
}
class ColoredPoint extends Point
{
private int color;
ColoredPoint(int x, int y, int color)
{
super(x, y);
this.color = color;
}
int getColor() { return color; }
}
class UpcastArrayDemo
{
public static void main(String[] args)
{
ColoredPoint[] cptArray = new ColoredPoint[1];
108
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
cptArray[0] = new ColoredPoint(10, 20, 5);
Point[] ptArray = cptArray;
System.out.println(ptArray[0].getX()); // Output: 10
System.out.println(ptArray[0].getY()); // Output: 20
System.out.println(ptArray[0].getColor()); // Illegal
//
}
}
Listing 2-35’s main() method first creates a ColoredPoint array consisting of one element. It then
instantiates this class and assigns the object’s reference to this element. Because ColoredPoint[] is a
subtype of Point[], main() is able to upcast cptArray’s ColoredPoint[] type to Point[] and assign its
reference to ptArray. main() then invokes the ColoredPoint instance’s getX() and getY() methods via
ptArray[0]. It cannot invoke getColor() because ptArray has narrower scope than cptArray. In other
words, getColor() is not part of Point’s interface.
This book was purchased by [email protected]
Abstract Classes and Abstract Methods
Suppose new requirements dictate that your graphics application must include a Rectangle class. Also,
this class must include a draw() method, and this method must be tested in a manner similar to that
shown in Listing 2-34’s Graphics class.
In contrast to Circle, which is a Point with a radius, it does not make sense to think of a Rectangle
as a being a Point with a width and height. Rather, a Rectangle instance would probably be composed of
a Point indicating its origin and a Point indicating its width and height extents.
Because circles, points, and rectangles are examples of shapes, it makes more sense to declare a
Shape class with its own draw() method than to specify class Rectangle extends Point. Listing 2-36
presents Shape’s declaration.
Listing 2-36. Declaring a Shape class
class Shape
{
void draw()
{
}
}
Listing 2-36’s Shape class declares an empty draw() method that only exists to be overridden and to
demonstrate subtype polymorphism.
You can now refactor Listing 2-33’s Point class to extend Listing 2-36’s Shape class, leave Circle as is,
and introduce a Rectangle class that extends Shape. You can then refactor Listing 2-34’s Graphics class’s
main() method to take Shape into account. Check out the following main() method:
public static void main(String[] args)
{
Shape[] shapes = new Shape[] { new Point(10, 20), new Circle(10, 20, 30),
new Rectangle(20, 30, 15, 25) };
for (int i = 0; i < shapes.length; i++)
shapes[i].draw();
}
Because Point and Rectangle directly extend Shape, and because Circle indirectly extends Shape by
extending Point, main() responds to shapes[i].draw(); by calling the correct subclass’s draw() method.
109
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Although Shape makes the code more flexible, there is a problem. What is to stop someone from
instantiating Shape and adding this meaningless instance to the shapes array, as follows?
Shape[] shapes = new Shape[] { new Point(10, 20), new Circle(10, 20, 30),
new Rectangle(20, 30, 15, 25), new Shape() };
What does it mean to instantiate Shape? Because this class describes an abstract concept, what does
it mean to draw a generic shape? Fortunately, Java provides a solution to this problem, which is
demonstrated in Listing 2-37.
Listing 2-37. Abstracting the Shape class
abstract class Shape
{
abstract void draw(); // semicolon is required
}
Listing 2-37 uses Java’s abstract reserved word to declare a class that cannot be instantiated. The
compiler reports an error should you try to instantiate this class.
■ Tip Get into the habit of declaring classes that describe generic categories (e.g., shape, animal, vehicle, and
account) abstract. This way, you will not inadvertently instantiate them.
The abstract reserved word is also used to declare a method without a body—the compiler reports
an error when you supply a body or omit the semicolon. The draw() method does not need a body
because it cannot draw an abstract shape.
■ Caution The compiler reports an error when you attempt to declare a class that is both abstract and final. For
example, abstract final class Shape is an error because an abstract class cannot be instantiated and a final
class cannot be extended. The compiler also reports an error when you declare a method to be abstract but do not
declare its class to be abstract. For example, removing abstract from the Shape class’s header in Listing 2-37
results in an error. This removal is an error because a non-abstract (concrete) class cannot be instantiated when
it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all
the abstract class’s abstract methods, or else the extending class must itself be declared to be abstract;
otherwise, the compiler will report an error.
An abstract class can contain non-abstract methods in addition to or instead of abstract methods.
For example, Listing 2-22’s Vehicle class could have been declared abstract. The constructor would still
be present, to initialize private fields, even though you could not instantiate the resulting class.
110
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Downcasting and Runtime Type Identification
Moving up the type hierarchy via upcasting causes loss of access to subtype features. For example,
assigning a Circle instance to Point variable p means that you cannot use p to call Circle’s getRadius()
method.
However, it is possible to once again access the Circle instance’s getRadius() method by
performing an explicit cast operation; for example, Circle c = (Circle) p;. This assignment is known
as downcasting because you are explicitly moving down the type hierarchy (from the Point superclass to
the Circle subclass). It is also an example of contravariance in that a type with a narrower range of
values (Point) is being converted to a type with a wider range of values (Circle).
Although an upcast is always safe (the superclass’s interface is a subset of the subclass’s interface),
the same cannot be said of a downcast. Listing 2-38 shows you what kind of trouble you can get into
when downcasting is used incorrectly.
Listing 2-38. The trouble with downcasting
class A
{
}
class B extends A
{
void m() {}
}
class DowncastDemo
{
public static void main(String[] args)
{
A a = new A();
B b = (B) a;
b.m();
}
}
Listing 2-38 presents a class hierarchy consisting of a superclass named A and a subclass named B.
Although A does not declare any members, B declares a single m() method.
A third class named DowncastDemo provides a main() method that first instantiates A, and then tries to
downcast this instance to B and assign the result to variable b. The compiler will not complain because
downcasting from a superclass to a subclass in the same type hierarchy is legal.
However, if the assignment is allowed, the application will undoubtedly crash when it tries to
execute b.m();. The crash happens because the JVM will attempt to call a method that does not exist—
class A does not have an m() method.
Fortunately, this scenario will never happen because the JVM verifies that the cast is legal. Because
it detects that A does not have an m() method, it does not permit the cast by throwing an instance of the
ClassCastException class.
The JVM’s cast verification illustrates runtime type identification (or RTTI, for short). Cast
verification performs RTTI by examining the type of the cast operator’s operand to see whether the cast
should be allowed. Clearly, the cast should not be allowed.
A second form of RTTI involves the instanceof operator. This operator checks the left operand to
see whether it is an instance of the right operand, and returns true if this is the case. The following
example introduces instanceof to Listing 2-38 to prevent the ClassCastException:
if (a instanceof B)
111
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
B b = (B) a;
b.m();
}
The instanceof operator detects that variable a’s instance was not created from B and returns false
to indicate this fact. As a result, the code that performs the illegal cast will not execute. (Overuse of
instanceof probably indicates poor software design.)
Because a subtype is a kind of supertype, instanceof will return true when its left operand is a
subtype instance or a supertype instance of its right operand supertype. The following example
demonstrates:
A a = new A();
B b = new B();
System.out.println(b instanceof A); // Output: true
System.out.println(a instanceof A); // Output: true
This example assumes the class structure shown in Listing 2-38 and instantiates superclass A and
subclass B. The first System.out.println() method call outputs true because b’s reference identifies an
instance of a subclass of A; the second System.out.println() method call outputs true because a’s
reference identifies an instance of superclass A.
You can also downcast from one array to another provided that the array being downcast is a
supertype of the other array, and whose elements types are those of the subtype. Consider Listing 2-39.
Listing 2-39. Demonstrating array downcasting
class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX() { return x; }
int getY() { return y; }
}
class ColoredPoint extends Point
{
private int color;
ColoredPoint(int x, int y, int color)
{
super(x, y);
this.color = color;
}
int getColor() { return color; }
}
class DowncastArrayDemo
{
public static void main(String[] args)
{
ColoredPoint[] cptArray = new ColoredPoint[1];
cptArray[0] = new ColoredPoint(10, 20, 5);
112
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Point[] ptArray = cptArray;
System.out.println(ptArray[0].getX()); // Output: 10
System.out.println(ptArray[0].getY()); // Output: 20
System.out.println(ptArray[0].getColor()); // Illegal
if (ptArray instanceof ColoredPoint[])
{
ColoredPoint cp = (ColoredPoint) ptArray[0];
System.out.println(cp.getColor());
}
//
}
}
Listing 2-39 is similar to Listing 2-35 except that it also demonstrates downcasting. Notice its use of
instanceof to verify that ptArray’s referenced object is of type ColoredPoint[]. If this operator returns
true, it is safe to downcast ptArray[0] from Point to ColoredPoint and assign the reference to
ColoredPoint.
So far, you have encountered two forms of RTTI. Java also supports a third form that is known as
reflection. I will introduce you to this form of RTTI when I cover reflection in Chapter 4.
Covariant Return Types
A covariant return type is a method return type that, in the superclass’s method declaration, is the
supertype of the return type in the subclass’s overriding method declaration. Listing 2-40 demonstrates
this feature.
Listing 2-40. A demonstration of covariant return types
class SuperReturnType
{
@Override
public String toString()
{
return "superclass return type";
}
}
class SubReturnType extends SuperReturnType
{
@Override
public String toString()
{
return "subclass return type";
}
}
class Superclass
{
SuperReturnType createReturnType()
{
return new SuperReturnType();
}
}
class Subclass extends Superclass
{
113
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
@Override
SubReturnType createReturnType()
{
return new SubReturnType();
}
}
class CovarDemo
{
public static void main(String[] args)
{
SuperReturnType suprt = new Superclass().createReturnType();
System.out.println(suprt); // Output: superclass return type
SubReturnType subrt = new Subclass().createReturnType();
System.out.println(subrt); // Output: subclass return type
}
}
Listing 2-40 declares SuperReturnType and Superclass superclasses, and SubReturnType and
Subclass subclasses; each of Superclass and Subclass declares a createReturnType() method.
Superclass’s method has its return type set to SuperReturnType, whereas Subclass’s overriding method
has its return type set to SubReturnType, a subclass of SuperReturnType.
Covariant return types minimize upcasting and downcasting. For example, Subclass’s
createReturnType() method does not need to upcast its SubReturnType instance to its SubReturnType
return type. Furthermore, this instance does not need to be downcast to SubReturnType when assigning
to variable subrt.
In the absence of covariant return types, you would end up with Listing 2-41.
Listing 2-41. Upcasting and downcasting in the absence of covariant return types
class SuperReturnType
{
@Override
public String toString()
{
return "superclass return type";
}
}
class SubReturnType extends SuperReturnType
{
@Override
public String toString()
{
return "subclass return type";
}
}
class Superclass
{
SuperReturnType createReturnType()
{
return new SuperReturnType();
}
}
114
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
class Subclass extends Superclass
{
@Override
SuperReturnType createReturnType()
{
return new SubReturnType();
}
}
class CovarDemo
{
public static void main(String[] args)
{
SuperReturnType suprt = new Superclass().createReturnType();
System.out.println(suprt); // Output: superclass return type
SubReturnType subrt = (SubReturnType) new Subclass().createReturnType();
System.out.println(subrt); // Output: subclass return type
}
}
In Listing 2-41, the first bolded code reveals an upcast from SubReturnType to SuperReturnType, and
the second bolded code uses the required (SubReturnType) cast operator to downcast from
SuperReturnType to SubReturnType, prior to the assignment to subrt.
Formalizing Class Interfaces
In my introduction to information hiding, I stated that every class X exposes an interface (a protocol
consisting of constructors, methods, and [possibly] fields that are made available to objects created from
other classes for use in creating and communicating with X’s objects).
Java formalizes the interface concept by providing reserved word interface, which is used to
introduce a type without implementation. Java also provides language features to declare, implement,
and extend interfaces. After looking at interface declaration, implementation, and extension, this section
explains the rationale for using interfaces.
Declaring Interfaces
An interface declaration consists of a header followed by a body. At minimum, the header consists of
reserved word interface followed by a name that identifies the interface. The body starts with an open
brace character and ends with a close brace. Sandwiched between these delimiters are constant and
method header declarations. Consider Listing 2-42.
Listing 2-42. Declaring a Drawable interface
interface Drawable
{
int RED = 1;
// For simplicity, integer constants are used. These
int GREEN = 2; // constants are not that descriptive, as you will see.
int BLUE = 3;
int BLACK = 4;
void draw(int color);
}
115
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-42 declares an interface named Drawable. By convention, an interface’s name begins with
an uppercase letter. Also, the first letter of each subsequent word in a multiword interface name is
capitalized.
■ Note Many interface names end with the able suffix. For example, Java’s standard class library includes
interfaces named Adjustable, Callable, Comparable, Cloneable, Iterable, Runnable, and Serializable. It’s
not mandatory to use this suffix; the standard class library also provides interfaces named CharSequence,
Collection, Composite, Executor, Future, Iterator, List, Map, and Set.
Drawable declares four fields that identify color constants. Drawable also declares a draw() method
that must be called with one of these constants to specify the color used to draw something.
■ Note You can precede interface with public, to make your interface accessible to code outside of its
package. (I will discuss packages in Chapter 3.) Otherwise, the interface is only accessible to other types in its
package. You can also precede interface with abstract, to emphasize that an interface is abstract. Because an
interface is already abstract, it is redundant to specify abstract in the interface’s declaration. An interface’s fields
are implicitly declared public, static, and final. It is therefore redundant to declare them with these reserved
words. Because these fields are constants, they must be explicitly initialized; otherwise, the compiler reports an
error. Finally, an interface’s methods are implicitly declared public and abstract. Therefore, it is redundant to
declare them with these reserved words. Because these methods must be instance methods, do not declare them
static or the compiler will report errors.
Drawable identifies a type that specifies what to do (draw something) but not how to do it. It leaves
implementation details to classes that implement this interface. Instances of such classes are known as
drawables because they know how to draw themselves.
■ Note An interface that declares no members is known as a marker interface or a tagging interface. It associates
metadata with a class. For example, the Cloneable marker/tagging interface states that instances of its
implementing class can be shallowly cloned. RTTI is used to detect that an object’s class implements a
marker/tagging interface. For example, when Object’s clone() method detects, via RTTI, that the calling
instance’s class implements Cloneable, it shallowly clones the object.
116
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Implementing Interfaces
By itself, an interface is useless. To be of any benefit to an application, the interface needs to be
implemented by a class. Java provides the implements reserved word for this task. Listing 2-43
demonstrates using implements to implement the aforementioned Drawable interface.
Listing 2-43. Implementing the Drawable interface
class Point implements Drawable
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
@Override
public String toString()
{
return "("+x+", "+y+")";
}
@Override
public void draw(int color)
{
System.out.println("Point drawn at "+toString()+" in color "+color);
}
}
class Circle extends Point implements Drawable
{
private int radius;
Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
int getRadius()
{
return radius;
}
@Override
public String toString()
{
return ""+radius;
}
117
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
@Override
public void draw(int color)
{
System.out.println("Circle drawn at "+super.toString()+
" with radius "+toString()+" in color "+color);
}
}
Listing 2-43 retrofits Listing 2-33’s class hierarchy to take advantage of Listing 2-42’s Drawable
interface. You will notice that each of classes Point and Circle implements this interface by attaching
the implements Drawable clause to its class header.
To implement an interface, the class must specify, for each interface method header, a method
whose header has the same signature and return type as that in the interface’s method header, and a
code body to go with the method header.
■ Caution When implementing a method, do not forget that the interface’s methods are implicitly declared
public. If you forget to include public in the implemented method’s declaration, the compiler will report an error
because you are attempting to assign weaker access to the implemented method.
When a class implements an interface, the class inherits the interface’s constants and method
headers, and overrides the method headers by providing implementations (hence the @Override
annotation). This is known as interface inheritance.
It turns out that Circle’s header does not need the implements Drawable clause. If this clause is not
present, Circle inherits Point’s draw() method, and is still considered to be a Drawable, whether or not it
overrides this method.
An interface specifies a type whose data values are the objects whose classes implement the
interface, and whose behaviors are those specified by the interface. This fact implies that you can assign
an object’s reference to a variable of the interface type, provided that the object’s class implements the
interface. The following example provides a demonstration:
public static void main(String[] args)
{
Drawable[] drawables = new Drawable[] { new Point(10, 20),
new Circle(10, 20, 30) };
for (int i = 0; i < drawables.length; i++)
drawables[i].draw(Drawable.RED);
}
Because Point and Circle instances are drawables by virtue of these classes implementing the
Drawable interface, it is legal to assign Point and Circle instance references to variables (including array
elements) of type Drawable.
When you run this method, it generates the following output:
Point drawn at (10, 20) in color 1
Circle drawn at (10, 20) with radius 30 in color 1
118
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-42’s Drawable interface is useful for drawing a shape’s outline. Suppose you also need to fill
a shape’s interior. You might attempt to satisfy this requirement by declaring Listing 2-44’s Fillable
interface.
Listing 2-44. Declaring a Fillable interface
interface Fillable
{
int RED = 1;
int GREEN = 2;
int BLUE = 3;
int BLACK = 4;
void fill(int color);
}
This book was purchased by [email protected]
Given Listings 2-42 and 2-44, you can declare that the Point and Circle classes implement both
interfaces by specifying class Point implements Drawable, Fillable and class Circle implements
Drawable, Fillable. You can then modify the main() method to also treat the drawables as fillables so
that you can fill these shapes, as follows:
public static void main(String[] args)
{
Drawable[] drawables = new Drawable[] { new Point(10, 20),
new Circle(10, 20, 30) };
for (int i = 0; i < drawables.length; i++)
drawables[i].draw(Drawable.RED);
Fillable[] fillables = new Fillable[drawables.length];
for (int i = 0; i < drawables.length; i++)
{
fillables[i] = (Fillable) drawables[i];
fillables[i].fill(Fillable.GREEN);
}
}
After invoking each drawable’s draw() method, main() creates a Fillable array of the same length as
the Drawable array. It then proceeds to copy each Drawable array element to a Fillable array element,
and then invoke the fillable’s fill() method. The (Fillable) cast is necessary because a drawable is not
a fillable. This cast operation will succeed because the Point and Circle instances being copied
implement Fillable as well as Drawable.
■ Tip You can list as many interfaces as you need to implement by specifying a comma-separated list of interface
names after implements.
Implementing multiple interfaces can lead to name collisions, and the compiler will report errors.
For example, suppose that you attempt to compile Listing 2-45’s interface and class declarations.
119
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-45. Colliding interfaces
interface A
{
int X = 1;
void foo();
}
interface B
{
int X = 1;
int foo();
}
class Collision implements A, B
{
@Override
public void foo();
@Override
public int foo() { return X; }
}
Each of Listing 2-45’s A and B interfaces declares a constant named X. Despite each constant having
the same type and value, the compiler will report an error when it encounters X in Collision’s second
foo() method because it does not know which X is being inherited.
Speaking of foo(), the compiler reports an error when it encounters Collision’s second foo()
declaration because foo() has already been declared. You cannot overload a method by changing only
its return type.
The compiler will probably report additional errors. For example, the Java 7 compiler has this to say
when told to compile Listing 2-45:
Collision.java:16: error: foo() is already defined in Collision
public int foo() { return X; }
^
Collision.java:11: error: Collision is not abstract and does not override abstract method
foo() in B
class Collision implements A, B
^
Collision.java:14: error: foo() in Collision cannot implement foo() in B
public void foo();
^
return type void is not compatible with int
Collision.java:16: error: reference to X is ambiguous, both variable X in A and variable X
in B match
public int foo() { return X; }
^
4 errors
Extending Interfaces
Just as a subclass can extend a superclass via reserved word extends, you can use this reserved word to
have a subinterface extend a superinterface. This, too, is known as interface inheritance.
120
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
For example, the duplicate color constants in Drawable and Fillable lead to name collisions when
you specify their names by themselves in an implementing class. To avoid these name collisions, prefix a
name with its interface name and the member access operator, or place these constants in their own
interface, and have Drawable and Fillable extend this interface, as demonstrated in Listing 2-46.
Listing 2-46. Extending the Colors interface
interface Colors
{
int RED = 1;
int GREEN = 2;
int BLUE = 3;
int BLACK = 4;
}
interface Drawable extends Colors
{
void draw(int color);
}
interface Fillable extends Colors
{
void fill(int color);
}
The fact that Drawable and Fillable each inherit constants from Colors is not a problem for the
compiler. There is only a single copy of these constants (in Colors) and no possibility of a name collision,
and so the compiler is satisfied.
If a class can implement multiple interfaces by declaring a comma-separated list of interface names
after implements, it seems that an interface should be able to extend multiple interfaces in a similar way.
This feature is demonstrated in Listing 2-47.
Listing 2-47. Extending a pair of interfaces
interface A
{
int X = 1;
}
interface B
{
double X = 2.0;
}
interface C extends A, B
{
}
Listing 2-47 will compile even though C inherits two same-named constants X with different return
types and initializers. However, if you implement C and then try to access X, as in Listing 2-48, you will
run into a name collision.
121
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Listing 2-48. Discovering a name collision
class Collision implements C
{
public void output()
{
System.out.println(X); // Which X is accessed?
}
}
Suppose you introduce a void foo(); method header declaration into interface A, and an int
foo(); method header declaration into interface B. This time, the compiler will report an error when you
attempt to compile the modified Listing 2-47.
Why Use Interfaces?
Now that the mechanics of declaring, implementing, and extending interfaces are out of the way, we can
focus on the rationale for using them. Unfortunately, newcomers to Java’s interfaces feature are often
told that this feature was created as a workaround to Java’s lack of support for multiple implementation
inheritance. While interfaces are useful in this capacity, this is not their reason for existence. Instead,
Java’s interfaces feature was created to give developers the utmost flexibility in designing their
applications, by decoupling interface from implementation. You should always code to the interface.
Those who are adherents to agile software development (a group of software development
methodologies based on iterative development that emphasizes keeping code simple, testing frequently,
and delivering functional pieces of the application as soon as they are deliverable) know the importance
of flexible coding. They cannot afford to tie their code to a specific implementation because a change in
requirements for the next iteration could result in a new implementation, and they might find
themselves rewriting significant amounts of code, which wastes time and slows development.
Interfaces help you achieve flexibility by decoupling interface from implementation. For example,
the main() method following Listing 2-36 creates an array of objects from classes that subclass the Shape
class, and then iterates over these objects, calling each object’s draw() method. The only objects that can
be drawn are those that subclass Shape.
Suppose you also have a hierarchy of classes that model resistors, transistors, and other electronic
components. Each component has its own symbol that allows the component to be shown in a
schematic diagram of an electronic circuit. Perhaps you want to add a drawing capability to each class
that draws that component’s symbol.
You might consider specifying Shape as the superclass of the electronic component class hierarchy.
However, electronic components are not shapes (although they have shapes) so it makes no sense to
place these classes in a class hierarchy rooted in Shape.
However, you can make each component class implement the Drawable interface, which lets you
add expressions that instantiate these classes to the drawables array in the main() method appearing
prior to Listing 2-44 (so you can draw their symbols). This is legal because these instances are drawables.
Wherever possible, you should strive to specify interfaces instead of classes in your code, to keep
your code adaptable to change. This is especially true when working with Java’s Collections Framework,
which I will discuss at length in Chapter 5.
For now, consider a simple example that consists of the Collections Framework’s java.util.List
interface, and its java.util.ArrayList and java.util.LinkedList implementation classes. The following
example presents inflexible code based on the ArrayList class:
ArrayList<String> arrayList = new ArrayList<String>();
122
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
void dump(ArrayList<String> arrayList)
{
// suitable code to dump out the arrayList
}
This example uses the generics-based parameterized type language feature (which I will discuss in
Chapter 3) to identify the kind of objects stored in an ArrayList instance. In this example, String objects
are stored.
The example is inflexible because it hardwires the ArrayList class into multiple locations. This
hardwiring focuses the developer into thinking specifically about array lists instead of generically about
lists.
Lack of focus is problematic when a requirements change, or perhaps a performance issue brought
about by profiling (analyzing a running application to check its performance), suggests that the
developer should have used LinkedList.
The example only requires a minimal number of changes to satisfy the new requirement. In
contrast, a larger code base might need many more changes. Although you only need to change
ArrayList to LinkedList, to satisfy the compiler, consider changing arrayList to linkedList, to keep
semantics (meaning) clear—you might have to change multiple occurrences of names that refer to an
ArrayList instance throughout the source code.
The developer is bound to lose time while refactoring the code to adapt to LinkedList. Instead, the
developer could have saved time by writing this example to use the equivalent of constants. In other
words, the example could have been written to rely on interfaces, and to only specify ArrayList in one
place. The following example shows you what the resulting code would look like:
List<String> list = new ArrayList<String>();
void dump(List<String> list)
{
// suitable code to dump out the list
}
This example is much more flexible than the previous example. If a requirements or profiling
change suggests that LinkedList should be used instead of ArrayList, simply replace Array with Linked
and you are done. You do not even have to change the parameter name.
INTERFACES VERSUS ABSTRACT CLASSES
Java provides interfaces and abstract classes for describing abstract types (types that cannot be
instantiated). Abstract types represent abstract concepts (drawable and shape, for example), and instances
of such types would be meaningless.
Interfaces promote flexibility through lack of implementation—Drawable and List illustrate this flexibility.
They are not tied to any single class hierarchy, but can be implemented by any class in any hierarchy.
Abstract classes support implementation, but can be genuinely abstract (Listing 2-37’s abstract Shape
class, for example). However, they are limited to appearing in the upper levels of class hierarchies.
Interfaces and abstract classes can be used together. For example, the Collections Framework’s
java.util package provides List, Map, and Set interfaces; and AbstractList, AbstractMap, and
AbstractSet abstract classes that provide skeletal implementations of these interfaces.
123
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
The skeletal implementations make it easy for you to create your own interface implementations, to
address your unique requirements. If they do not meet your needs, you can optionally have your class
directly implement the appropriate interface.
Collecting Garbage
Objects are created via reserved word new, but how are they destroyed? Without some way to destroy
objects, they will eventually fill up the heap’s available space and the application will not be able to
continue. Java does not provide the developer with the ability to remove them from memory. Instead,
Java handles this task by providing a garbage collector, which is code that runs in the background and
occasionally checks for unreferenced objects. When the garbage collector discovers an unreferenced
object (or multiple objects that reference each other, and where there are no other references to each
other—only A references B and only B references A, for example), it removes the object from the heap,
making more heap space available.
An unreferenced object is an object that cannot be accessed from anywhere within an application.
For example, new Employee("John", "Doe"); is an unreferenced object because the Employee reference
returned by new is thrown away. In contrast, a referenced object is an object where the application stores
at least one reference. For example, Employee emp = new Employee("John", "Doe"); is a referenced
object because variable emp contains a reference to the Employee object.
A referenced object becomes unreferenced when the application removes its last stored reference.
For example, if emp is a local variable that contains the only reference to an Employee object, this object
becomes unreferenced when the method in which emp is declared returns. An application can also
remove a stored reference by assigning null to its reference variable. For example, emp = null; removes
the reference to the Employee object that was previously stored in emp.
Java’s garbage collector eliminates a form of memory leakage in C++ implementations that do not
rely on a garbage collector. In these C++ implementations, the developer must destroy dynamically
created objects before they go out of scope. If they vanish before destruction, they remain in the heap.
Eventually, the heap fills and the application halts.
Although this form of memory leakage is not a problem in Java, a related form of leakage is
problematic: continually creating objects and forgetting to remove even one reference to each object
causes the heap to fill up and the application to eventually come to a halt. This form of memory leakage
typically occurs in the context of collections (object-based data structures that store objects), and is a
major problem for applications that run for lengthy periods of time—a web server is one example. For
shorter-lived applications, you will normally not notice this form of memory leakage.
Consider Listing 2-49.
Listing 2-49. A memory-leaking stack
public class Stack
{
private Object[] elements;
private int top;
public Stack(int size)
{
elements = new Object[size];
top = -1; // indicate that stack is empty
}
public void push(Object o)
{
124
n
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
if (top+1 == elements.length)
{
System.out.println("stack is full");
return;
}
elements[++top] = o;
}
public Object pop()
{
if (top == -1)
{
System.out.println("stack is empty");
return null;
}
Object element = elements[top--];
//
elements[top+1] = null;
return element;
}
public static void main(String[] args)
{
Stack stack = new Stack(2);
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
}
}
Listing 2-49 describes a collection known as a stack, a data structure that stores elements in last-in,
first-out order. Stacks are useful for remembering things, such as the instruction to return to when a
method stops executing and must return to its caller.
Stack provides a push() method for pushing arbitrary objects onto the top of the stack, and a pop()
method for popping objects off the stack’s top in the reverse order to which they were pushed.
After creating a Stack object that can store a maximum of two objects, main() invokes push() three
times, to push three String objects onto the stack. Because the stack’s internal array can store two
objects only, push() outputs an error message when main() tries to push "C".
At this point, main() attempts to pop three Objects off of the stack, outputting each object to the
standard output device. The first two pop() method calls succeed, but the final method call fails and
outputs an error message because the stack is empty when it is called.
When you run this application, it generates the following output:
stack is full
B
A
stack is empty
null
There is a problem with the Stack class: it leaks memory. When you push an object onto the stack,
its reference is stored in the internal elements array. When you pop an object off the stack, the object’s
reference is obtained and top is decremented, but the reference remains in the array (until you invoke
push()).
125
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
Imagine a scenario where the Stack object’s reference is assigned to a class field, which means that
the Stack object hangs around for the life of the application. Furthermore, suppose that you have
pushed three 50-megabyte Image objects onto the stack, and then subsequently popped them off the
stack. After using these objects, you assign null to their reference variables, thinking that they will be
garbage collected the next time the garbage collector runs. However, this won’t happen because the
Stack object still maintains its references to these objects, and so 150 megabytes of heap space will not
be available to the application, and maybe the application will run out of memory.
The solution to this problem is for pop() to explicitly assign null to the elements entry prior to
returning the reference. Simply uncomment the elements[top+1] = null; line in Listing 2-49 to make
this happen.
You might think that you should always assign null to reference variables when their referenced
objects are no longer required. However, doing so often does not improve performance or free up
significant amounts of heap space, and can lead to thrown instances of the
java.lang.NullPointerException class when you’re not careful. (I discuss NullPointerException in the
context of Chapter 3’s coverage of Java’s exceptions-oriented language features). You typically nullify
reference variables in classes that manage their own memory, such as the aforementioned Stack class.
■ Note Garbage collection is a complex process and has resulted in various garbage collectors being developed
for the JVM. If you want to learn more about garbage collection, I recommend that you start by reading the
“Memory Management in the Java HotSpot Virtual Machine” whitepaper at
http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf.
Next, you will want to learn about the Garbage-First collector, which is new in Java 7. Check out “The GarbageFirst Garbage Collector” whitepaper (http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp135488.html) to learn about this garbage collector. For additional information on Java’s garbage collection
process, you can explore the other whitepapers that are accessible from Oracle’s “Java HotSpot Garbage
Collection” page at http://www.oracle.com/technetwork/java/javase/tech/index-jsp-140228.html.
Chapter 4 pursues garbage collection further by introducing you to Java’s Reference API, which lets
your application receive notifications when objects are about to be finalized or have been finalized.
■ Note Throughout this book, I often refer to API in both broad and narrow contexts. On the one hand, I refer to
Reference as an API, but I also refer to the individual classes of Reference as APIs themselves.
126
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
EXERCISES
The following exercises are designed to test your understanding of classes and objects:
1.
Listing 2-2 presents an Image class with three constructors and a main() method
for testing this class. Expand Image by introducing private int fields named width
and height, and a private one-dimensional byte array field named image. Refactor
the Image() constructor to invoke the Image(String filename) constructor via
this(null). Refactor the Image(String filename, String imageType)
constructor such that, when the filename reference is not null, it creates a byte
array of arbitrary size, perhaps with the help of an expression such as (int)
(Math.random()*100000) (return a randomly generated integer between 0 and
99999 inclusive), and assigns this array’s reference to the image field. Similarly, it
assigns an arbitrary width to the width field and an arbitrary height to the height
field. If filename contains null, it assigns -1 to each of width and height.
Continuing, introduce getWidth(), getHeight(), and getImage() methods that
return the values of their respective fields, and introduce a getSize() method that
returns the length of the array assigned to the image field (or 0 if image contains
the null reference). Finally, refactor the main() method such that, for each
constructor, the following sequence of method calls occurs:
System.out.println("Image = "+image.getImage());
System.out.println("Size = "+image.getSize());
System.out.println("Width = "+image.getWidth());
System.out.println("Height = "+image.getHeight());.
2.
Model part of an animal hierarchy by declaring Animal, Bird, Fish,
AmericanRobin, DomesticCanary, RainbowTrout, and SockeyeSalmon classes:
•
Animal is public and abstract, declares private String-based kind and
appearance fields, declares a public constructor that initializes these fields to
passed-in arguments, declares public and abstract eat() and move() methods
that take no arguments and whose return type is void, and overrides the
toString() method to output the contents of kind and appearance.
•
Bird is public and abstract, extends Animal, declares a public constructor that
passes its kind and appearance parameter values to its superclass constructor,
overrides its eat() method to output eats seeds and insects (via
System.out.println()), and overrides the move() method to output flies
through the air.
•
Fish is public and abstract, extends Animal, declares a public constructor that
passes its kind and appearance parameter values to its superclass constructor,
overrides its eat() method to output eats krill, algae, and insects, and
overrides its move() method to output swims through the water.
127
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
•
AmericanRobin is public, extends Bird, and declares a public noargument
constructor that passes "americanrobin" and "red breast" to its superclass
constructor.
•
DomesticCanary is public, extends Bird, and declares a public noargument
constructor that passes "domesticcanary" and "yellow, orange, black,
brown, white, red" to its superclass constructor.
•
RainbowTrout is public, extends Fish, and declares a public noargument
constructor that passes "rainbowtrout" and "bands of brilliant speckled
multicolored stripes running nearly the whole length of its body" to
its superclass constructor.
•
SockeyeSalmon is public, extends Fish, and declares a public noargument
constructor that passes "sockeyesalmon" and "bright red with a green
head" to its superclass constructor.
For brevity, I have omitted from the Animal hierarchy abstract Robin, Canary,
Trout, and Salmon classes that generalize robins, canaries, trout, and salmon.
Perhaps you might want to include these classes in the hierarchy.
Although this exercise illustrates the accurate modeling of a natural scenario using
inheritance, it also reveals the potential for class explosion—too many classes
may be introduced to model a scenario, and it might be difficult to maintain all
these classes. Keep this in mind when modeling with inheritance.
3.
Continuing from the previous exercise, declare an Animals class with a main()
method. This method first declares an animals array that is initialized to
AmericanRobin, RainbowTrout, DomesticCanary, and SockeyeSalmon objects.
The method then iterates over this array, first outputting animals[i] (which
causes toString() to be called), and then calling each object’s eat() and
move() methods (demonstrating subtype polymorphism).
4.
Continuing from the previous exercise, declare a public Countable interface with
a String getID() method. Modify Animal to implement Countable and have this
method return kind’s value. Modify Animals to initialize the animals array to
AmericanRobin, RainbowTrout, DomesticCanary, SockeyeSalmon,
RainbowTrout, and AmericanRobin objects. Also, introduce code that computes a
census of each kind of animal. This code will use the Census class that is declared
in Listing 2-50.
Listing 2-50. The Census class stores census data on four kinds of animals
public class Census
{
public final static int SIZE = 4;
private String[] IDs;
private int[] counts;
public Census()
128
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
{
This book was purchased by [email protected]
IDs = new String[SIZE];
counts = new int[SIZE];
}
public String get(int index)
{
return IDs[index]+" "+counts[index];
}
public void update(String ID)
{
for (int i = 0; i < IDs.length; i++)
{
// If ID not already stored in the IDs array (which is indicated by
// the first null entry that is found), store ID in this array, and
// also assign 1 to the associated element in the counts array, to
// initialize the census for that ID.
if (IDs[i] == null)
{
IDs[i] = ID;
counts[i] = 1;
return;
}
// If a matching ID is found, increment the associated element in
// the counts array to update the census for that ID.
if (IDs[i].equals(ID))
{
counts[i]++;
return;
}
}
}
}
Summary
Structured programs create data structures that organize and store data items, and manipulate the data
stored in these data structures via functions and procedures. The fundamental units of a structured
program are its data structures and the functions or procedures that manipulate them. Although Java
lets you create applications in a similar fashion, this language is really about declaring classes and
creating objects from these classes.
A class is a template for manufacturing objects (named aggregates of code and data), which are also
known as class instances, or instances for short. Classes generalize real-world entities, and objects are
specific manifestations of these entities at the program level.
Classes model real-world entities from a template perspective. Objects represent specific entities.
Entities have attributes. An entity’s collection of attributes is referred to as its state. Entities also have
behaviors.
A class and its objects model an entity by combining state with behaviors into a single unit—the
class abstracts state whereas its objects provide concrete state values. This bringing together of state and
behaviors is known as encapsulation. Unlike structured programming, where the developer focuses on
129
CHAPTER 2  DISCOVERING CLASSES AND OBJECTS
modeling behaviors through structured code, and modeling state through data structures that store data
items for the structured code to manipulate, the developer working with classes and objects focuses on
templating entities by declaring classes that encapsulate state and behaviors expressed as fields and
methods, instantiating objects with specific field values from these classes to represent specific entities,
and interacting with objects by invoking their methods.
We tend to categorize stuff by saying things like “cars are vehicles” or “savings accounts are bank
accounts.” By making these statements, we really are saying that cars inherit vehicular state (such as
make and color) and behaviors (such as park and display mileage), and similarly are saying that savings
accounts inherit bank account state (such as balance) and behaviors (such as deposit and withdraw).
Car, vehicle, savings account, and bank account are examples of real-world entity categories, and
inheritance is a hierarchical relationship between similar entity categories in which one category
inherits state and behaviors from at least one other entity category. Inheriting from a single category is
called single inheritance, and inheriting from at least two categories is called multiple inheritance.
Java supports single inheritance and multiple inheritance to facilitate code reuse—why reinvent the
wheel? Java supports single inheritance in a class context, in which a class inherits fields and methods
from another class through class extension. Because classes are involved, Java refers to this kind of
inheritance as implementation inheritance.
Java supports multiple inheritance only in an interface context, in which a class inherits method
templates from one or more interfaces through interface implementation, or in which an interface
inherits method templates from one or more interfaces through interface extension. Because interfaces
are involved, Java refers to this kind of inheritance as interface inheritance.
Some real-world entities can change their forms. For example, water is naturally a liquid, but it
changes to a solid when frozen, and it changes to a gas when heated to its boiling point. Insects such as
butterflies that undergo metamorphosis are another example.
The ability to change form is known as polymorphism, and is useful to model in a programming
language. For example, code that draws arbitrary shapes can be expressed more concisely by
introducing a single Shape class and its draw() method, and by invoking that method for each Circle
instance, Rectangle instance, and other Shape instance stored in an array. When Shape’s draw() method
is called for an array instance, it is the Circle’s, Rectangle’s or other Shape instance’s draw() method that
gets called. We say that there are many forms of Shape’s draw() method, or that this method is
polymorphic.
Every class X exposes an interface (a protocol consisting of constructors, methods, and [possibly]
fields that are made available to objects created from other classes for use in creating and
communicating with X’s objects). Java formalizes the interface concept by providing reserved word
interface, which is used to introduce a type without implementation. Although many believe that this
language feature was created as a workaround to Java’s lack of support for multiple implementation
inheritance, this is not the real reason for its existence. Instead, Java’s interfaces feature was created to
give developers the utmost flexibility in designing their applications, by decoupling interface from
implementation.
Objects are created via reserved word new, but how are they destroyed? Without some way to destroy
objects, they will eventually fill up the heap’s available space and the application will not be able to
continue. Java does not provide the developer with the ability to remove them from memory. Instead,
Java handles this task by providing a garbage collector, which is code that runs in the background and
occasionally checks for unreferenced objects. When the garbage collector discovers an unreferenced
object (or multiple objects that reference each other, and where there are no other references to each
other—only A references B and only B references A, for example), it removes the object from the heap,
making more heap space available.
Now that you understand Java’s support for classes and objects, you’re ready to explore this
language’s support for more advanced features such as packages and generics. Chapter 3 introduces you
to Java’s support for these and other advanced language features.
130
CHAPTER 3
Exploring Advanced Language
Features
Chapters 1 and 2 introduced you to Java’s fundamental language features along with its support for
classes and objects. Chapter 3 builds onto this foundation by introducing you to Java’s advanced
language features, specifically those features related to nested types, packages, static imports,
exceptions, assertions, annotations, generics, and enums.
Nested Types
Classes that are declared outside of any class are known as top-level classes. Java also supports nested
classes, which are classes declared as members of other classes or scopes. Nested classes help you
implement top-level class architecture.
There are four kinds of nested classes: static member classes, nonstatic member classes,
anonymous classes, and local classes. The latter three categories are known as inner classes.
This section introduces you to static member classes and inner classes. For each kind of nested
class, I provide you with a brief introduction, an abstract example, and a more practical example. The
section then briefly examines the topic of nesting interfaces within classes.
Static Member Classes
A static member class is a static member of an enclosing class. Although enclosed, it does not have an
enclosing instance of that class, and cannot access the enclosing class’s instance fields and invoke its
instance methods. However, it can access the enclosing class’s static fields and invoke its static
methods, even those members that are declared private. Listing 3-1 presents a static member class
declaration.
Listing 3-1. Declaring a static member class
class EnclosingClass
{
private static int i;
private static void m1()
{
System.out.println(i);
}
static void m2()
131
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
{
EnclosedClass.accessEnclosingClass();
}
static class EnclosedClass
{
static void accessEnclosingClass()
{
i = 1;
m1();
}
void accessEnclosingClass2()
{
m2();
}
}
}
Listing 3-1 declares a top-level class named EnclosingClass with class field i, class methods m1()
and m2(), and static member class EnclosedClass. Also, EnclosedClass declares class method
accessEnclosingClass() and instance method accessEnclosingClass2().
Because accessEnclosingClass() is declared static, m2() must prefix this method’s name with
EnclosedClass and the member access operator to invoke this method.
Listing 3-2 presents the source code to an application that demonstrates how to invoke
EnclosedClass’s accessEnclosingClass() class method, and instantiate EnclosedClass and invoke its
accessEnclosingClass2() instance method.
Listing 3-2. Invoking a static member class’s class and instance methods
class SMCDemo
{
public static void main(String[] args)
{
EnclosingClass.EnclosedClass.accessEnclosingClass(); // Output: 1
EnclosingClass.EnclosedClass ec = new EnclosingClass.EnclosedClass();
ec.accessEnclosingClass2(); // Output: 1
}
}
Listing 3-2’s main() method reveals that you must prefix the name of an enclosed class with the
name of its enclosing class to invoke a class method; for example,
EnclosingClass.EnclosedClass.accessEnclosingClass();.
This listing also reveals that you must prefix the name of the enclosed class with the name of its
enclosing class when instantiating the enclosed class; for example, EnclosingClass.EnclosedClass ec =
new EnclosingClass.EnclosedClass();. You can then invoke the instance method in the normal manner;
for example, ec.accessEnclosingClass2();.
Static member classes have their uses. For example, Listing 3-3’s Double and Float static member
classes provide different implementations of their enclosing Rectangle class. The Float version occupies
less memory because of its 32-bit float fields, and the Double version provides greater accuracy because
of its 64-bit double fields.
132
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-3. Using static member classes to declare multiple implementations of their enclosing class
abstract class Rectangle
{
abstract double getX();
abstract double getY();
abstract double getWidth();
abstract double getHeight();
static class Double extends Rectangle
{
private double x, y, width, height;
Double(double x, double y, double width, double height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
double getX() { return x; }
double getY() { return y; }
double getWidth() { return width; }
double getHeight() { return height; }
}
static class Float extends Rectangle
{
private float x, y, width, height;
Float(float x, float y, float width, float height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
double getX() { return x; }
double getY() { return y; }
double getWidth() { return width; }
double getHeight() { return height; }
}
// Prevent subclassing. Use the type-specific Double and Float
// implementation subclass classes to instantiate.
private Rectangle() {}
boolean contains(double x, double y)
{
return (x >= getX() && x < getX()+getWidth()) &&
(y >= getY() && y < getY()+getHeight());
}
}
Listing 3-3’s Rectangle class demonstrates nested subclasses. Each of the Double and Float static
member classes subclass the abstract Rectangle class, providing private floating-point or double
133
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
precision floating-point fields, and overriding Rectangle’s abstract methods to return these fields’ values
as doubles.
Rectangle is abstract because it makes no sense to instantiate this class. Because it also makes no
sense to directly extend Rectangle with new implementations (the Double and Float nested subclasses
should be sufficient), its default constructor is declared private. Instead, you must instantiate
Rectangle.Float (to save memory) or Rectangle.Double (when accuracy is required), as demonstrated by
Listing 3-4.
Listing 3-4. Instantiating nested subclasses
class SMCDemo
{
public static void main(String[] args)
{
Rectangle r = new Rectangle.Double(10.0, 10.0, 20.0, 30.0);
System.out.println("x = "+r.getX());
System.out.println("y = "+r.getY());
System.out.println("width = "+r.getWidth());
System.out.println("height = "+r.getHeight());
System.out.println("contains(15.0, 15.0) = "+r.contains(15.0, 15.0));
System.out.println("contains(0.0, 0.0) = "+r.contains(0.0, 0.0));
System.out.println();
r = new Rectangle.Float(10.0f, 10.0f, 20.0f, 30.0f);
System.out.println("x = "+r.getX());
System.out.println("y = "+r.getY());
System.out.println("width = "+r.getWidth());
System.out.println("height = "+r.getHeight());
System.out.println("contains(15.0, 15.0) = "+r.contains(15.0, 15.0));
System.out.println("contains(0.0, 0.0) = "+r.contains(0.0, 0.0));
}
}
Listing 3-4 first instantiates Rectangle’s Double subclass via new Rectangle.Double(10.0, 10.0,
20.0, 30.0) and then invokes its various methods. Continuing, Listing 3-4 instantiates Rectangle’s Float
subclass via new Rectangle.Float(10.0f, 10.0f, 20.0f, 30.0f) before invoking Rectangle methods on
this instance.
Compile both listings (javac SMCDemo.java or javac *.java) and run the application (java SMCDemo).
You will then observe the following output:
x = 10.0
y = 10.0
width = 20.0
height = 30.0
contains(15.0, 15.0) = true
contains(0.0, 0.0) = false
x = 10.0
y = 10.0
width = 20.0
height = 30.0
contains(15.0, 15.0) = true
contains(0.0, 0.0) = false
134
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Java’s class library contains many static member classes. For example, the java.lang.Character
class encloses a static member class named Subset whose instances represent subsets of the Unicode
character set. java.util.AbstractMap.SimpleEntry, java.io.ObjectInputStream.GetField, and
java.security.KeyStore.PrivateKeyEntry are other examples.
■ Note When you compile an enclosing class that contains a static member class, the compiler creates a classfile
for the static member class whose name consists of its enclosing class’s name, a dollar-sign character, and the
static member class’s name. For example, compile Listing 3-1 and you will discover
EnclosingClass$EnclosedClass.class as well as EnclosingClass.class. This format also applies to nonstatic
member classes.
Nonstatic Member Classes
A nonstatic member class is a non-static member of an enclosing class. Each instance of the nonstatic
member class implicitly associates with an instance of the enclosing class. The nonstatic member class’s
instance methods can call instance methods in the enclosing class and access the enclosing class
instance’s nonstatic fields. Listing 3-5 presents a nonstatic member class declaration.
Listing 3-5. Declaring a nonstatic member class
class EnclosingClass
{
private int i;
private void m()
{
System.out.println(i);
}
class EnclosedClass
{
void accessEnclosingClass()
{
i = 1;
m();
}
}
}
Listing 3-5 declares a top-level class named EnclosingClass with instance field i, instance method
m(), and nonstatic member class EnclosedClass. Furthermore, EnclosedClass declares instance method
accessEnclosingClass().
Because accessEnclosingClass() is nonstatic, EnclosedClass must be instantiated before this
method can be called. This instantiation must take place via an instance of EnclosingClass. Listing 3-6
accomplishes these tasks.
135
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-6. Calling a nonstatic member class’s instance method
class NSMCDemo
{
public static void main(String[] args)
{
EnclosingClass ec = new EnclosingClass();
ec.new EnclosedClass().accessEnclosingClass(); // Output: 1
}
}
Listing 3-6’s main() method first instantiates EnclosingClass and saves its reference in local variable
ec. Then, main() uses this reference as a prefix to the new operator, to instantiate EnclosedClass, whose
reference is then used to call accessEnclosingClass(), which outputs 1.
■ Note Prefixing new with a reference to the enclosing class is rare. Instead, you will typically call an enclosed
class’s constructor from within a constructor or an instance method of its enclosing class.
Suppose you need to maintain a to-do list of items, where each item consists of a name and a
description. After some thought, you create Listing 3-7’s ToDo class to implement these items.
Listing 3-7. Implementing to-do items as name-description pairs
class ToDo
{
private String name;
private String desc;
ToDo(String name, String desc)
{
this.name = name;
this.desc = desc;
}
String getName()
{
return name;
}
String getDesc()
{
return desc;
}
@Override
public String toString()
{
return "Name = "+getName()+", Desc = "+getDesc();
}
}
136
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
You next create a ToDoList class to store ToDo instances. ToDoList uses its ToDoArray nonstatic
member class to store ToDo instances in a growable array – you do not know how many instances will be
stored, and Java arrays have fixed lengths. See Listing 3-8.
Listing 3-8. Storing a maximum of two ToDo instances in a ToDoArray instance
class ToDoList
{
private ToDoArray toDoArray;
private int index = 0;
ToDoList()
{
toDoArray = new ToDoArray(2);
}
boolean hasMoreElements()
{
return index < toDoArray.size();
}
ToDo nextElement()
{
return toDoArray.get(index++);
}
void add(ToDo item)
{
toDoArray.add(item);
}
private class ToDoArray
{
private ToDo[] toDoArray;
private int index = 0;
ToDoArray(int initSize)
{
toDoArray = new ToDo[initSize];
}
void add(ToDo item)
{
if (index >= toDoArray.length)
{
ToDo[] temp = new ToDo[toDoArray.length*2];
for (int i = 0; i < toDoArray.length; i++)
temp[i] = toDoArray[i];
toDoArray = temp;
}
toDoArray[index++] = item;
}
ToDo get(int i)
{
return toDoArray[i];
}
int size()
{
return index;
137
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
}
}
As well as providing an add() method to store ToDo instances in the ToDoArray instance, ToDoList
provides hasMoreElements() and nextElement() methods to iterate over and return the stored instances.
Listing 3-9 demonstrates these methods.
Listing 3-9. Creating and iterating over a ToDoList of ToDo instances
class NSMCDemo
{
public static void main(String[] args)
{
ToDoList toDoList = new ToDoList();
toDoList.add(new ToDo("#1", "Do laundry."));
toDoList.add(new ToDo("#2", "Buy groceries."));
toDoList.add(new ToDo("#3", "Vacuum apartment."));
toDoList.add(new ToDo("#4", "Write report."));
toDoList.add(new ToDo("#5", "Wash car."));
while (toDoList.hasMoreElements())
System.out.println(toDoList.nextElement());
}
}
Compile all three listings (javac NSMCDemo.java or javac *.java) and run the application (java
NSMCDemo). You will then observe the following output:
Name
Name
Name
Name
Name
=
=
=
=
=
#1,
#2,
#3,
#4,
#5,
Desc
Desc
Desc
Desc
Desc
=
=
=
=
=
Do laundry.
Buy groceries.
Vacuum apartment.
Write report.
Wash car.
Java’s class library presents many examples of nonstatic member classes. For example, the
java.util package’s HashMap class declares private HashIterator, ValueIterator, KeyIterator, and
EntryIterator classes for iterating over a hashmap’s values, keys, and entries. (I will discuss HashMap in
Chapter 5.)
■ Note Code within an enclosed class can obtain a reference to its enclosing class instance by qualifying reserved
word this with the enclosing class’s name and the member access operator. For example, if code within
accessEnclosingClass() needed to obtain a reference to its EnclosingClass instance, it would specify
EnclosingClass.this.
Anonymous Classes
An anonymous class is a class without a name. Furthermore, it is not a member of its enclosing class.
Instead, an anonymous class is simultaneously declared (as an anonymous extension of a class or as an
138
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
anonymous implementation of an interface) and instantiated any place where it is legal to specify an
expression. Listing 3-10 demonstrates an anonymous class declaration and instantiation.
This book was purchased by [email protected]
Listing 3-10. Declaring and instantiating an anonymous class that extends a class
abstract class Speaker
{
abstract void speak();
}
class ACDemo
{
public static void main(final String[] args)
{
new Speaker()
{
String msg = (args.length == 1) ? args[0] : "nothing to say";
@Override
void speak()
{
System.out.println(msg);
}
}
.speak();
}
}
Listing 3-10 introduces an abstract class named Speaker and a concrete class named ACDemo. The
latter class’s main() method declares an anonymous class that extends Speaker and overrides its speak()
method. When this method is called, it outputs main()’s first command-line argument or a default
message if there are no arguments; for example, java ACDemo Hello outputs Hello.
An anonymous class does not have a constructor (because the anonymous class does not have a
name). However, its classfile does contain an <init>() method that performs instance initialization. This
method calls the superclass’s noargument constructor (prior to any other initialization), which is the
reason for specifying Speaker() after new.
Anonymous class instances should be able to access the surrounding scope’s local variables and
parameters. However, an instance might outlive the method in which it was conceived (as a result of
storing the instance’s reference in a field), and try to access local variables and parameters that no longer
exist after the method returns.
Because Java cannot allow this illegal access, which would most likely crash the Java Virtual
Machine (JVM), it lets an anonymous class instance only access local variables and parameters that are
declared final. Upon encountering a final local variable/parameter name in an anonymous class
instance, the compiler does one of two things:
•
If the variable’s type is primitive (int or double, for example), the compiler
replaces its name with the variable’s read-only value.
•
If the variable’s type is reference (java.lang.String, for example), the compiler
introduces, into the classfile, a synthetic variable (a manufactured variable) and
code that stores the local variable’s/parameter’s reference in the synthetic
variable.
Listing 3-11 demonstrates an alternative anonymous class declaration and instantiation.
139
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-11. Declaring and instantiating an anonymous class that implements an interface
interface Speakable
{
void speak();
}
class ACDemo
{
public static void main(final String[] args)
{
new Speakable()
{
String msg = (args.length == 1) ? args[0] : "nothing to say";
@Override
public void speak()
{
System.out.println(msg);
}
}
.speak();
}
}
Listing 3-11 is very similar to Listing 3-10. However, instead of subclassing a Speaker class, this
listing’s anonymous class implements an interface named Speakable. Apart from the <init>() method
calling java.lang.Object() (interfaces have no constructors), Listing 3-11 behaves like Listing 3-10.
Although an anonymous class does not have a constructor, you can provide an instance initializer to
handle complex initialization. For example, new Office() {{addEmployee(new Employee("John
Doe"));}}; instantiates an anonymous subclass of Office and adds one Employee object to this instance
by calling Office’s addEmployee() method.
You will often find yourself creating and instantiating anonymous classes for their convenience. For
example, suppose you need to return a list of all filenames having the “.java” suffix. The following
example shows you how an anonymous class simplifies using the java.io package’s File and
FilenameFilter classes to achieve this objective:
String[] list = new File(directory).list(new FilenameFilter()
{
@Override
public boolean accept(File f, String s)
{
return s.endsWith(".java");
}
});
Local Classes
A local class is a class that is declared anywhere that a local variable is declared. Furthermore, it has the
same scope as a local variable. Unlike an anonymous class, a local class has a name and can be reused.
Like anonymous classes, local classes only have enclosing instances when used in nonstatic contexts.
140
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
A local class instance can access the surrounding scope’s local variables and parameters. However,
the local variables and parameters that are accessed must be declared final. For example, Listing 3-12’s
local class declaration accesses a final parameter and a final local variable.
Listing 3-12. Declaring a local class
class EnclosingClass
{
void m(final int x)
{
final int y = x*2;
class LocalClass
{
int a = x;
int b = y;
}
LocalClass lc = new LocalClass();
System.out.println(lc.a);
System.out.println(lc.b);
}
}
Listing 3-12 declares EnclosingClass with its instance method m() declaring a local class named
LocalClass. This local class declares a pair of instance fields (a and b) that are initialized to the values of
final parameter x and final local variable y when LocalClass is instantiated: new
EnclosingClass().m(10);, for example.
Listing 3-13 demonstrates this local class.
Listing 3-13. Demonstrating a local class
class LCDemo
{
public static void main(String[] args)
{
EnclosingClass ec = new EnclosingClass();
ec.m(10);
}
}
After instantiating EnclosingClass, Listing 3-13’s main() method invokes m(10). The called m()
method multiplies this argument by 2, instantiates LocalClass, whose <init>() method assigns the
argument and the doubled value to its pair of instance fields (in lieu of using a constructor to perform
this task), and outputs the LocalClass instance fields. The following output results:
10
20
Local classes help improve code clarity because they can be moved closer to where they are needed.
For example, Listing 3-14 declares an Iterator interface and a refactored ToDoList class whose
iterator() method returns an instance of its local Iter class as an Iterator instance (because Iter
implements Iterator).
141
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-14. The Iterator interface and the refactored ToDoList class
interface Iterator
{
boolean hasMoreElements();
Object nextElement();
}
class ToDoList
{
private ToDo[] toDoList;
private int index = 0;
ToDoList(int size)
{
toDoList = new ToDo[size];
}
Iterator iterator()
{
class Iter implements Iterator
{
int index = 0;
@Override
public boolean hasMoreElements()
{
return index < toDoList.length;
}
@Override
public Object nextElement()
{
return toDoList[index++];
}
}
return new Iter();
}
void add(ToDo item)
{
toDoList[index++] = item;
}
}
Listing 3-15 demonstrates Iterator, the refactored ToDoList class, and Listing 3-7’s ToDo class.
Listing 3-15. Creating and iterating over a ToDoList of ToDo instances with a reusable iterator
class LCDemo
{
public static void main(String[] args)
{
ToDoList toDoList = new ToDoList(5);
toDoList.add(new ToDo("#1", "Do laundry."));
toDoList.add(new ToDo("#2", "Buy groceries."));
toDoList.add(new ToDo("#3", "Vacuum apartment."));
142
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
toDoList.add(new ToDo("#4", "Write report."));
toDoList.add(new ToDo("#5", "Wash car."));
Iterator iter = toDoList.iterator();
while (iter.hasMoreElements())
System.out.println(iter.nextElement());
}
}
The Iterator instance that is returned from iterator() returns ToDo items in the same order as
when they were added to the list. Although you can only use the returned Iterator object once, you can
call iterator() whenever you need a new Iterator object. This capability is a big improvement over the
one-shot iterator presented in Listing 3-9.
Interfaces Within Classes
Interfaces can be nested within classes. Once declared, an interface is considered to be static, even if it is
not declared static. For example, Listing 3-16 declares an enclosing class named X along with two
nested static interfaces named A and B.
Listing 3-16. Declaring a pair of interfaces within a class
class X
{
interface A
{
}
static interface B
{
}
}
You would access Listing 3-16’s interfaces in the same way. For example, you would specify class C
implements X.A {} or class D implements X.B {}.
As with nested classes, nested interfaces help to implement top-level class architecture by being
implemented via nested classes. Collectively, these types are nested because they cannot (as in Listing 314’s Iter local class) or need not appear at the same level as a top-level class and pollute its package
namespace.
■ Note Chapter 2’s introduction to interfaces showed you how to declare constants and method headers in the
body of an interface. You can also declare interfaces and classes in an interface’s body. Because there are few
good reasons to do this (java.util.Map.Entry, which is discussed in Chapter 5, is one exception), it is probably
best to avoid nesting interfaces and/or classes within interfaces.
143
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Packages
Hierarchical structures organize items in terms of hierarchical relationships that exist between those
items. For example, a filesystem might contain a taxes directory with multiple year subdirectories,
where each subdirectory contains tax information pertinent to that year. Also, an enclosing class might
contain multiple nested classes that only make sense in the context of the enclosing class.
Hierarchical structures also help to avoid name conflicts. For example, two files cannot have the
same name in a nonhierarchical filesystem (which consists of a single directory). In contrast, a
hierarchical filesystem lets same-named files exist in different directories. Similarly, two enclosing
classes can contain same-named nested classes. Name conflicts do not exist because items are
partitioned into different namespaces.
Java also supports the partitioning of top-level user-defined types into multiple namespaces, to
better organize these types and to also prevent name conflicts. Java uses packages to accomplish these
tasks.
This section introduces you to packages. After defining this term and explaining why package names
must be unique, the section presents the package and import statements. It next explains how the JVM
searches for packages and types, and then presents an example that shows you how to work with
packages. This section closes by showing you how to encapsulate a package of classfiles into JAR files.
■ Tip Except for the most trivial of top-level types and (typically) those classes that serve as application entry
points, you should consider storing your types (especially if they are reusable) in packages.
What Are Packages?
A package is a unique namespace that can contain a combination of top-level classes, other top-level
types, and subpackages. Only types that are declared public can be accessed from outside the package.
Furthermore, the constants, constructors, methods, and nested types that describe a class’s interface
must be declared public to be accessible from beyond the package.
■ Note Throughout this book, I typically don’t declare top-level types and their accessible members public,
unless I’m creating a package.
Every package has a name, which must be a nonreserved identifier. The member access operator
separates a package name from a subpackage name, and separates a package or subpackage name from
a type name. For example, the two member access operators in graphics.shapes.Circle separate
package name graphics from the shapes subpackage name, and separate subpackage name shapes from
the Circle type name.
144
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
■ Note The standard class library organizes its many classes and other top-level types into multiple packages.
Many of these packages are subpackages of the standard java package. Examples include java.io (types related
to input/output operations), java.lang (language-oriented types), java.lang.reflect (reflection-oriented
language types), java.net (network-oriented types), and java.util (utility types).
Package Names Must Be Unique
Suppose you have two different graphics.shapes packages, and suppose that each shapes subpackage
contains a Circle class with a different interface. When the compiler encounters
System.out.println(new Circle(10.0, 20.0, 30.0).area()); in the source code, it needs to verify that
the area() method exists.
The compiler will search all accessible packages until it finds a graphics.shapes package that
contains a Circle class. If the found package contains the appropriate Circle class with an area()
method, everything is fine; otherwise, if the Circle class does not have an area() method, the compiler
will report an error.
This scenario illustrates the importance of choosing unique package names. Specifically, the toplevel package name must be unique. The convention in choosing this name is to take your Internet
domain name and reverse it. For example, I would choose ca.tutortutor as my top-level package name
because tutortutor.ca is my domain name. I would then specify
ca.tutortutor.graphics.shapes.Circle to access Circle.
■ Note Reversed Internet domain names are not always valid package names. One or more of its component
names might start with a digit (6.com), contain a hyphen (-) or other illegal character (aq-x.com), or be one of
Java’s reserved words (int.com). Convention dictates that you prefix the digit with an underscore (com._6),
replace the illegal character with an underscore (com.aq_x), and suffix the reserved word with an underscore
(com.int_).
The Package Statement
The package statement identifies the package in which a source file’s types are located. This statement
consists of reserved word package, followed by a member access operator-separated list of package and
subpackage names, followed by a semicolon.
For example, package graphics; specifies that the source file’s types locate in a package named
graphics, and package graphics.shapes; specifies that the source file’s types locate in the graphics
package’s shapes subpackage.
By convention, a package name is expressed in lowercase. If the name consists of multiple words,
each word except for the first word is capitalized.
Only one package statement can appear in a source file. When it is present, nothing apart from
comments must precede this statement.
145
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
■ Caution Specifying multiple package statements in a source file or placing anything apart from comments
above a package statement causes the compiler to report an error.
Java implementations map package and subpackage names to same-named directories. For
example, an implementation would map graphics to a directory named graphics, and would map
graphics.shapes to a shapes subdirectory of graphics. The Java compiler stores the classfiles that
implement the package’s types in the corresponding directory.
■ Note If a source file does not contain a package statement, the source file’s types are said to belong to the
unnamed package. This package corresponds to the current directory.
The Import Statement
Imagine having to repeatedly specify ca.tutortutor.graphics.shapes.Circle or some other lengthy
package-qualified type name for each occurrence of that type in source code. Java provides an
alternative that lets you avoid having to specify package details. This alternative is the import statement.
The import statement imports types from a package by telling the compiler where to look for
unqualified type names during compilation. This statement consists of reserved word import, followed
by a member access operator-separated list of package and subpackage names, followed by a type name
or * (asterisk), followed by a semicolon.
The * symbol is a wildcard that represents all unqualified type names. It tells the compiler to look
for such names in the import statement’s specified package, unless the type name is found in a
previously searched package. (Using the wildcard does not have a performance penalty or lead to code
bloat, but can lead to name conflicts, as you will see.)
For example, import ca.tutortutor.graphics.shapes.Circle; tells the compiler that an unqualified
Circle class exists in the ca.tutortutor.graphics.shapes package. Similarly, import
ca.tutortutor.graphics.shapes.*; tells the compiler to look in this package if it encounters a Rectangle
class, a Triangle class, or even an Employee class (if Employee has not already been found).
■ Tip You should avoid using the * wildcard so that other developers can easily see which types are used in
source code.
Because Java is case sensitive, package and subpackage names specified in an import statement
must be expressed in the same case as that used in the package statement.
When import statements are present in source code, only a package statement and comments can
precede them.
146
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
■ Caution Placing anything other than a package statement, import statements, static import statements
(discussed shortly), and comments above an import statement causes the compiler to report an error.
You can run into name conflicts when using the wildcard version of the import statement because
any unqualified type name matches the wildcard. For example, you have graphics.shapes and geometry
packages that each contain a Circle class, the source code begins with import geometry.*; and import
graphics.shape.*; statements, and it also contains an unqualified occurrence of Circle. Because the
compiler does not know if Circle refers to geometry’s Circle class or graphics.shape’s Circle class, it
reports an error. You can fix this problem by qualifying Circle with the correct package name.
■ Note The compiler automatically imports the String class and other types from the java.lang package, which
is why it is not necessary to qualify String with java.lang.
Searching for Packages and Types
Newcomers to Java who first start to work with packages often become frustrated by “no class definition
found” and other errors. This frustration can be partly avoided by understanding how the JVM searches
for packages and types.
This section explains how the search process works. To understand this process, you need to realize
that the compiler is a special Java application that runs under the control of the JVM. Furthermore, there
are two different forms of search.
Compile-Time Search
When the compiler encounters a type expression (such as a method call) in source code, it must locate
that type’s declaration to verify that the expression is legal (a method exists in the type’s class whose
parameter types match the types of the arguments passed in the method call, for example).
The compiler first searches the Java platform packages (which contain class library types). It then
searches extension packages (for extension types). If the -sourcepath command-line option was
specified when starting the JVM (via javac), the compiler searches the indicated path’s source files.
■ Note Java platform packages are stored in rt.jar and a few other important JAR files. Extension packages are
stored in a special extensions directory named ext.
Otherwise, the compiler searches the user classpath (in left-to-right order) for the first user classfile
or source file containing the type. If no user classpath is present, the current directory is searched. If no
147
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
package matches or the type still cannot be found, the compiler reports an error. Otherwise, the
compiler records the package information in the classfile.
■ Note The user classpath is specified via the -classpath option used to start the JVM or, if not present, the
CLASSPATH environment variable.
Runtime Search
When the compiler or any other Java application runs, the JVM will encounter types and must load their
associated classfiles via special code known as a classloader (discussed in Appendix C). The JVM will use
the previously stored package information that is associated with the encountered type in a search for
that type’s classfile.
The JVM searches the Java platform packages, followed by extension packages, followed by the user
classpath (in left-to-right order) for the first classfile that contains the type. If no user classpath is
present, the current directory is searched. If no package matches or the type cannot be found, a “no class
definition found” error is reported. Otherwise, the classfile is loaded into memory.
■ Note Whether you use the -classpath option or the CLASSPATH environment variable to specify a user
classpath, there is a specific format that must be followed. Under Windows, this format is expressed as
path1;path2;..., where path1, path2, and so on are the locations of package directories. Under Unix and Linux,
this format changes to path1:path2:....
Playing with Packages
Suppose your application needs to log messages to the console, to a file, or to another destination. It can
accomplish this task with the help of a logging library. My implementation of this library consists of an
interface named Logger, an abstract class named LoggerFactory, and a pair of package-private classes
named Console and File.
■ Note The logging library that I present is an example of the Abstract Factory design pattern, which is presented
on page 87 of Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm,
Ralph Johnson, and John Vlissides (Addison-Wesley, 1995; ISBN: 0201633612).
Listing 3-17 presents the Logger interface, which describes objects that log messages.
148
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-17. Describing objects that log messages via the Logger interface
package logging;
public interface Logger
{
boolean connect();
boolean disconnect();
boolean log(String msg);
}
Each of the connect(), disconnect(), and log() methods returns true upon success, and false upon
failure. (Later in this chapter, you will discover a better technique for dealing with failure.) These
methods are not declared public explicitly because an interface’s methods are implicitly public.
Listing 3-18 presents the LoggerFactory abstract class.
This book was purchased by [email protected]
Listing 3-18. Obtaining a logger for logging messages to a specific destination
package logging;
public abstract class LoggerFactory
{
public final static int CONSOLE = 0;
public final static int FILE = 1;
public static Logger newLogger(int dstType, String... dstName)
{
switch (dstType)
{
case CONSOLE: return new Console(dstName.length == 0 ? null
: dstName[0]);
case FILE
: return new File(dstName.length == 0 ? null
: dstName[0]);
default
: return null;
}
}
}
newLogger() returns a Logger instance for logging messages to an appropriate destination. It uses
the variable number of arguments feature (see Chapter 2) to optionally accept an extra String argument
for those destination types that require the argument. For example, FILE requires a filename.
Listing 3-19 presents the package-private Console class – this class is not accessible beyond the
classes in the logging package because reserved word class is not preceded by reserved word public.
Listing 3-19. Logging messages to the console
package logging;
class Console implements Logger
{
private String dstName;
Console(String dstName)
149
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
{
this.dstName = dstName;
}
@Override
public boolean connect()
{
return true;
}
@Override
public boolean disconnect()
{
return true;
}
@Override
public boolean log(String msg)
{
System.out.println(msg);
return true;
}
}
Console’s package-private constructor saves its argument, which most likely will be null because
there is no need for a String argument. Perhaps a future version of Console will use this argument to
identify one of multiple console windows.
Listing 3-20 presents the package-private File class.
Listing 3-20. Logging messages to a file (eventually)
package logging;
class File implements Logger
{
private String dstName;
File(String dstName)
{
this.dstName = dstName;
}
@Override
public boolean connect()
{
if (dstName == null)
return false;
System.out.println("opening file "+dstName);
return true;
}
@Override
public boolean disconnect()
{
if (dstName == null)
return false;
System.out.println("closing file "+dstName);
return true;
150
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
@Override
public boolean log(String msg)
{
if (dstName == null)
return false;
System.out.println("writing "+msg+" to file "+dstName);
return true;
}
}
Unlike Console, File requires a nonnull argument. Each method first verifies that this argument is
not null. If the argument is null, the method returns false to signify failure. (In Chapter 8, I refactor File
to incorporate appropriate file-writing code.)
The logging library allows us to introduce portable logging code into an application. Apart from a
call to newLogger(), this code will remain the same regardless of the logging destination. Listing 3-21
presents an application that tests this library.
Listing 3-21. Testing the logging library
import logging.Logger;
import logging.LoggerFactory;
class TestLogger
{
public static void main(String[] args)
{
Logger logger = LoggerFactory.newLogger(LoggerFactory.CONSOLE);
if (logger.connect())
{
logger.log("test message #1");
logger.disconnect();
}
else
System.out.println("cannot connect to console-based logger");
logger = LoggerFactory.newLogger(LoggerFactory.FILE, "x.txt");
if (logger.connect())
{
logger.log("test message #2");
logger.disconnect();
}
else
System.out.println("cannot connect to file-based logger");
logger = LoggerFactory.newLogger(LoggerFactory.FILE);
if (logger.connect())
{
logger.log("test message #3");
logger.disconnect();
}
else
System.out.println("cannot connect to file-based logger");
}
151
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
Follow these steps (which assume that the JDK has been installed) to create the logging package and
TestLogger application, and to run this application:
1.
Create a new directory and make this directory current.
2.
Create a logging directory in the current directory.
3.
Copy Listing 3-17 to a file named Logger.java in the logging directory.
4.
Copy Listing 3-18 to a file named LoggerFactory.java in the logging directory.
5.
Copy Listing 3-19 to a file named Console.java in the logging directory.
6.
Copy Listing 3-20 to a file named File.java in the logging directory.
7.
Copy Listing 3-21 to a file named TestLogger.java in the current directory.
8.
Execute javac TestLogger.java, which also compiles logger’s source files.
9.
Execute java TestLogger.
After completing the final step, you should observe the following output from the TestLogger
application:
test message #1
opening file x.txt
writing test message #2 to file x.txt
closing file x.txt
cannot connect to file-based logger
What happens when logging is moved to another location? For example, move logging to the root
directory and run TestLogger. You will now observe an error message about the JVM not finding the
logging package and its LoggerFactory classfile.
You can solve this problem by specifying -classpath when running the java tool, or by adding the
location of the logging package to the CLASSPATH environment variable. You’ll probably find it more
convenient to use the former option, as demonstrated in the following Windows-specific command line:
java -classpath \;. TestLogger
The backslash represents the root directory in Windows. (I could have specified a forward slash as
an alternative.) Also, the period represents the current directory. If it is missing, the JVM complains
about not finding the TestLogger classfile.
■ Tip If you discover an error message where the JVM reports that it cannot find an application classfile, try
appending a period character to the classpath. Doing so will probably fix the problem.
152
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Packages and JAR Files
Chapter 1 briefly introduced you to the JDK’s jar tool, which is used to archive classfiles in JAR files, and
is also used to extract a JAR file’s classfiles. It probably comes as no surprise that you can store packages
in JAR files, which greatly simplify the distribution of your package-based class libraries.
To show you how easy it is to store a package in a JAR file, we will create a logger.jar file that
contains the logging package’s four classfiles (Logger.class, LoggerFactory.class, Console.class, and
File.class). Complete the following steps to accomplish this task:
1.
Make sure that the current directory contains the previously created logging
directory with its four classfiles.
2.
Execute jar cf logger.jar logging\*.class. You could alternatively execute
jar cf logger.jar logging/*.class. (The c option stands for “create new
archive” and the f option stands for “specify archive filename.”)
You should now find a logger.jar file in the current directory. To prove to yourself that this file
contains the four classfiles, execute jar tf logger.jar. (The t option stands for “list table of contents.”)
You can run TestLogger.class by adding logger.jar to the classpath. For example, you can run
TestLogger under Windows via java -classpath logger.jar;. TestLogger.
Static Imports
An interface should only be used to declare a type. However, some developers violate this principle by
using interfaces to only export constants. Such interfaces are known as constant interfaces, and Listing 322 presents an example.
Listing 3-22. Declaring a constant interface
interface Directions
{
int NORTH = 0;
int SOUTH = 1;
int EAST = 2;
int WEST = 3;
}
Developers who resort to constant interfaces do so to avoid having to prefix a constant’s name with
the name of its class (as in Math.PI, where PI is a constant in the java.lang.Math class). They do this by
implementing the interface—see Listing 3-23.
Listing 3-23. Implementing a constant interface
class TrafficFlow implements Directions
{
public static void main(String[] args)
{
showDirection((int)(Math.random()*4));
}
static void showDirection(int dir)
{
switch (dir)
153
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
{
case
case
case
case
NORTH:
SOUTH:
EAST :
WEST :
System.out.println("Moving
System.out.println("Moving
System.out.println("Moving
System.out.println("Moving
north"); break;
south"); break;
east"); break;
west");
}
}
}
Listing 3-23’s TrafficFlow class implements Directions for the sole purpose of not having to specify
Directions.NORTH, Directions.SOUTH, Directions.EAST, and Directions.WEST.
This is an appalling misuse of an interface. These constants are nothing more than an
implementation detail that should not be allowed to leak into the class’s exported interface, because they
might confuse the class’s users (what is the purpose of these constants?). Also, they represent a future
commitment: even when the class no longer uses these constants, the interface must remain to ensure
binary compatibility.
Java 5 introduced an alternative that satisfies the desire for constant interfaces while avoiding their
problems. This static imports feature lets you import a class’s static members so that you do not have
to qualify them with their class names. It is implemented via a small modification to the import
statement, as follows:
import static packagespec . classname . ( staticmembername | * );
The static import statement specifies static after import. It then specifies a member access
operator-separated list of package and subpackage names, which is followed by the member access
operator and a class’s name. Once again, the member access operator is specified, followed by a single
static member name or the asterisk wildcard.
■ Caution Placing anything apart from a package statement, import/static import statements, and comments
above a static import statement causes the compiler to report an error.
You specify a single static member name to import only that name:
import static java.lang.Math.PI; // Import the PI static field only.
import static java.lang.Math.cos; // Import the cos() static method only.
In contrast, you specify the wildcard to import all static member names:
import static java.lang.Math.*;
// Import all static members from Math.
You can now refer to the static member(s) without having to specify the class name:
System.out.println(cos(PI));
Using multiple static import statements can result in name conflicts, which causes the compiler to
report errors. For example, suppose your geom package contains a Circle class with a static member
named PI. Now suppose you specify import static java.lang.Math.*; and import static
geom.Circle.*; at the top of your source file. Finally, suppose you specify System.out.println(PI);
somewhere in that file’s code. The compiler reports an error because it does not know if PI belongs to
Math or Circle.
154
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Exceptions
In an ideal world, nothing bad ever happens when an application runs. For example, a file always exists
when the application needs to open the file, the application is always able to connect to a remote
computer, and the JVM never runs out of memory when the application needs to instantiate objects.
In contrast, real-world applications occasionally attempt to open files that do not exist, attempt to
connect to remote computers that are unable to communicate with them, and require more memory
than the JVM can provide. Your goal is to write code that properly responds to these and other
exceptional situations (exceptions).
This section introduces you to exceptions. After defining this term, the section looks at representing
exceptions in source code. It then examines the topics of throwing and handling exceptions, and
concludes by discussing how to perform cleanup tasks before a method returns, whether or not an
exception has been thrown.
What Are Exceptions?
An exception is a divergence from an application’s normal behavior. For example, the application
attempts to open a nonexistent file for reading. The normal behavior is to successfully open the file and
begin reading its contents. However, the file cannot be read if the file does not exist.
This example illustrates an exception that cannot be prevented. However, a workaround is possible.
For example, the application can detect that the file does not exist and take an alternate course of action,
which might include telling the user about the problem. Unpreventable exceptions where workarounds
are possible must not be ignored.
Exceptions can occur because of poorly written code. For example, an application might contain
code that accesses each element in an array. Because of careless oversight, the array-access code might
attempt to access a nonexistent array element, which leads to an exception. This kind of exception is
preventable by writing correct code.
Finally, an exception might occur that cannot be prevented, and for which there is no workaround.
For example, the JVM might run out of memory, or perhaps it cannot find a classfile. This kind of
exception, known as an error, is so serious that it is impossible (or at least inadvisable) to work around;
the application must terminate, presenting a message to the user that explains why it is terminating.
Representing Exceptions in Source Code
An exception can be represented via error codes or objects. After discussing each kind of representation
and explaining why objects are superior, I introduce you to Java’s exception and error class hierarchy,
emphasizing the difference between checked and runtime exceptions. I close my discussion on
representing exceptions in source code by discussing custom exception classes.
Error Codes Versus Objects
One way to represent exceptions in source code is to use error codes. For example, a method might
return true on success and false when an exception occurs. Alternatively, a method might return 0 on
success and a nonzero integer value that identifies a specific kind of exception.
Developers traditionally designed methods to return error codes; I demonstrated this tradition in
each of the three methods in Listing 3-17’s Logger interface. Each method returns true on success, or
returns false to represent an exception (unable to connect to the logger, for example).
Although a method’s return value must be examined to see if it represents an exception, error codes
are all too easy to ignore. For example, a lazy developer might ignore the return code from Logger’s
155
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
connect() method and attempt to call log(). Ignoring error codes is one reason why a new approach to
dealing with exceptions has been invented.
This new approach is based on objects. When an exception occurs, an object representing the
exception is created by the code that was running when the exception occurred. Details describing the
exception’s surrounding context are stored in the object. These details are later examined to work
around the exception.
The object is then thrown, or handed off to the JVM to search for a handler, code that can handle the
exception. (If the exception is an error, the application should not provide a handler because errors are
so serious [e.g., the JVM has run out of memory] that there’s practically nothing that can be done about
them.) When a handler is located, its code is executed to provide a workaround. Otherwise, the JVM
terminates the application.
■ Caution Code that handles exceptions can be a source of bugs because it’s often not thoroughly tested. Always
make sure to test any code that handles exceptions.
Apart from being too easy to ignore, an error code’s Boolean or integer value is less meaningful than
an object name. For example, fileNotFound is self-explanatory, but what does false mean? Also, an
object can contain information about what led to the exception. These details can be helpful to a
suitable workaround.
The Throwable Class Hierarchy
Java provides a hierarchy of classes that represent different kinds of exceptions. These classes are rooted
in java.lang.Throwable, the ultimate superclass for all throwables (exception and error objects—
exceptions and errors, for short—that can be thrown). Table 3-1 identifies and describes most of
Throwable’s constructors and methods.
Table 3-1. Throwable’s Constructors and Methods
156
Method
Description
Throwable()
Create a throwable with a null detail message
and cause.
Throwable(String message)
Create a throwable with the specified detail
message and a null cause.
Throwable(String message, Throwable
cause)
Create a throwable with the specified detail
message and cause.
protected Throwable(String message,
Throwable cause, boolean
enableSuppression, boolean
writableStackTrace)
Create a throwable with the specified detail
message, cause, suppression enabled or
disabled, and writable stack trace enabled or
disabled.
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Throwable(Throwable cause)
Create a throwable whose detail message is the
string representation of a nonnull cause, or null.
void addSuppressed(Throwable
exception)
Append the specified exception to the
exceptions that were suppressed in order to
deliver this exception.
Throwable fillInStackTrace()
Fill in the execution stack trace. This method
records information about the current state of
the stack frames for the current thread within
this throwable. (I discuss threads in Chapter 4.)
Throwable getCause()
Return the cause of this throwable. If there is no
cause, null is returned.
String getMessage()
Return this throwable’s detail message, which
might be null.
StackTraceElement[] getStackTrace()
Provide programmatic access to the stack trace
information printed by printStackTrace() as an
array of stack trace elements, each representing
one stack frame.
Throwable[] getSuppressed()
Return an array containing all exceptions that
were suppressed (typically by the try-withresources statement, discussed later) in order to
deliver this exception.
Throwable initCause(Throwable cause)
Initialize the cause of this throwable to the
specified value.
void printStackTrace()
Print this throwable and its backtrace of stack
frames to the standard error stream.
void setStackTrace(StackTraceElement[]
stackTrace)
Set the stack trace elements that will be returned
by getStackTrace() and printed by
printStackTrace() and related methods.
It is not uncommon for a class’s public methods to call helper methods that throw various
exceptions. A public method will probably not document exceptions thrown from a helper method
because they are implementation details that often should not be visible to the public method’s caller.
However, because this exception might be helpful in diagnosing the problem, the public method
can wrap the lower-level exception in a higher-level exception that is documented in the public
method’s contract interface. The wrapped exception is known as a cause because its existence causes the
higher-level exception to be thrown. A cause is created by invoking the Throwable(Throwable cause) or
Throwable(String message, Throwable cause) constructor, which invoke the initCause() method to
157
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
store the cause. If you do not call either constructor, you can alternatively call initCause() directly, but
must do so immediately after creating the throwable. Call the getCause() method to return the cause.
When one exception causes another exception, the first exception is usually caught and then the
second exception is thrown in response. In other words, there is a causal connection between the two
exceptions. In contrast, there are situations where two independent exceptions can be thrown in sibling
code blocks; for example, in the try block of a try-with-resources statement (discussed later in this
chapter) and the compiler-generated finally block that closes the resource. In these situations, only one
of the thrown exceptions can be propagated.
In the try-with-resources statement, when there are two such exceptions, the exception originating
from the try block is propagated and the exception from the finally block is added (via the
addSuppressed() method) to the list of exceptions suppressed by the exception from the try block. As an
exception unwinds the stack, it can accumulate multiple suppressed exceptions. An array of the
suppressed expressions can be retrieved by calling getSuppressed().
When an exception is thrown, it leaves behind a stack of unfinished method calls. Throwable’s
constructors call fillInStackTrace() to record this stack trace information, which is output by calling
printStackTrace().
The getStackTrace() method provides programmatic access to the stack trace by returning this
information as an array of java.lang.StackTraceElement instances – each instance represents one stack
entry. StackTraceElement provides methods to return stack trace information. For example, String
getMethodName() returns the name of an unfinished method.
The setStackTrace() method is designed for use by Remote Procedure Call (RPC) frameworks (RPC
is briefly discussed in Chapter 11) and other advanced systems, allowing the client to override the
default stack trace that is generated by fillInStackTrace() when a throwable is constructed, or
deserialized when a throwable is read from a serialization stream. (I will discuss serialization in Chapter
8.)
Except for Throwable(String message, Throwable cause, boolean enableSuppression, boolean
writableStackTrace), each Throwable constructor always treats suppression as being enabled, and
always calls fillInStackTrace(). In contrast, this constructor lets you disable suppression by passing
false to enableSuppression, and prevent fillInStackTrace() from being called by passing false to
writableStackTrace. Pass false to writableStackTrace when you plan to override the default stack trace
and want to avoid the unnecessary fillInStackTrace() method calls. Similarly, pass false to
enableSuppression when repeatedly catching and rethrowing the same exception object (to implement
control flow between two subsystems, for example) or in other exceptional circumstances.
You will notice that Throwable(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) is signified as a protected constructor. Also, its Java documentation
includes the following sentence: “Subclasses of Throwable should document any conditions under which
suppression is disabled and document conditions under which the stack trace is not writable.” This is an
example of “design and document for class extension,” which I discuss in Chapter 2.
Moving down the throwable hierarchy, you encounter the java.lang.Exception and
java.lang.Error classes, which respectively represent exceptions and errors. Each class offers five
constructors that pass their arguments to their Throwable counterparts, but provides no methods apart
from those that are inherited from Throwable.
Exception is itself subclassed by java.lang.CloneNotSupportedException (discussed in Chapter 2),
java.io.IOException (discussed in Chapter 8), and other classes. Similarly, Error is itself subclassed by
java.lang.AssertionError (discussed later in this chapter), java.lang.OutOfMemoryError, and other
classes.
158
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
■ Caution Never instantiate Throwable, Exception, or Error. The resulting objects are meaningless because
they are too generic.
This book was purchased by [email protected]
Checked Exceptions Versus Runtime Exceptions
A checked exception is an exception that represents a problem with the possibility of recovery, and for
which the developer must provide a workaround. The compiler checks (examines) the code to ensure
that the exception is handled in the method where it is thrown, or is explicitly identified as being
handled elsewhere.
Exception and all subclasses except for java.lang.RuntimeException (and its subclasses) describe
checked exceptions. For example, the CloneNotSupportedException and IOException classes describe
checked exceptions. (CloneNotSupportedException should not be checked because there is no runtime
workaround for this kind of exception.)
A runtime exception is an exception that represents a coding mistake. This kind of exception is also
known as an unchecked exception because it does not need to be handled or explicitly identified—the
mistake must be fixed. Because these exceptions can occur in many places, it would be burdensome to
be forced to handle them.
RuntimeException and its subclasses describe unchecked exceptions. For example,
java.lang.ArithmeticException describes arithmetic problems such as integer division by zero. Another
example is java.lang.ArrayIndexOutOfBoundsException. (In hindsight, RuntimeException should have
been named UncheckedException because all exceptions occur at runtime.)
■ Note Many developers are not happy with checked exceptions because of the work involved in having to handle
them. This problem is made worse by libraries providing methods that throw checked exceptions when they
should throw unchecked exceptions. As a result, many modern languages support only unchecked exceptions.
Custom Exception Classes
You can declare your own exception classes. Before doing so, ask yourself if an existing exception class in
Java’s standard class library meets your needs. If you find a suitable class, you should reuse it. (Why
reinvent the wheel?) Other developers will already be familiar with the existing class, and this knowledge
will make your code easier to learn.
If no existing class meets your needs, think about whether to subclass Exception or
RuntimeException. In other words, will your exception class be checked or unchecked? As a rule of
thumb, your class should subclass RuntimeException if you think that it will describe a coding mistake.
■ Tip When you name your class, follow the convention of providing an Exception suffix. This suffix clarifies that
your class describes an exception.
159
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Suppose you are creating a Media class whose static methods perform various media-oriented utility
tasks. For example, one method converts sound files in non-MP3 media formats to MP3 format. This
method will be passed source file and destination file arguments, and will convert the source file to the
format implied by the destination file’s extension.
Before performing the conversion, the method needs to verify that the source file’s format agrees
with the format implied by its file extension. If there is no agreement, an exception must be thrown.
Furthermore, this exception must store the expected and existing media formats so that a handler can
identify them when presenting a message to the user.
Because Java’s class library does not provide a suitable exception class, you decide to introduce a
class named InvalidMediaFormatException. Detecting an invalid media format is not the result of a
coding mistake, and so you also decide to extend Exception to indicate that the exception is checked.
Listing 3-24 presents this class’s declaration.
Listing 3-24. Declaring a custom exception class
package media;
public class InvalidMediaFormatException extends Exception
{
private String expectedFormat;
private String existingFormat;
public InvalidMediaFormatException(String expectedFormat,
String existingFormat)
{
super("Expected format: "+expectedFormat+", Existing format: "+
existingFormat);
this.expectedFormat = expectedFormat;
this.existingFormat = existingFormat;
}
public String getExpectedFormat()
{
return expectedFormat;
}
public String getExistingFormat()
{
return existingFormat;
}
}
InvalidMediaFormatException provides a constructor that calls Exception’s public
Exception(String message) constructor with a detail message that includes the expected and existing
formats. It is wise to capture such details in the detail message because the problem that led to the
exception might be hard to reproduce.
InvalidMediaFormatException also provides getExpectedFormat() and getExistingFormat()
methods that return these formats. Perhaps a handler will present this information in a message to the
user. Unlike the detail message, this message might be localized, expressed in the user’s language
(French, German, English, and so on).
160
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Throwing Exceptions
Now that you have created an InvalidMediaFormatException class, you can declare the Media class and
begin to code its convert() method. The initial version of this method validates its arguments, and then
verifies that the source file’s media format agrees with the format implied by its file extension. Check out
Listing 3-25.
Listing 3-25. Throwing exceptions from the convert() method
package media;
import java.io.IOException;
public final class Media
{
public static void convert(String srcName, String dstName)
throws InvalidMediaFormatException, IOException
{
if (srcName == null)
throw new NullPointerException(srcName+" is null");
if (dstName == null)
throw new NullPointerException(dstName+" is null");
// Code to access source file and verify that its format matches the
// format implied by its file extension.
//
// Assume that the source file’s extension is RM (for Real Media) and
// that the file’s internal signature suggests that its format is
// Microsoft WAVE.
String expectedFormat = "RM";
String existingFormat = "WAVE";
throw new InvalidMediaFormatException(expectedFormat, existingFormat);
}
}
Listing 3-25 declares the Media class to be final because this class will only consist of class methods
and there’s no reason to extend it.
Media’s convert() method appends throws InvalidMediaFormatException, IOException to its
header. A throws clause identifies all checked exceptions that are thrown out of the method, and which
must be handled by some other method. It consists of reserved word throws followed by a commaseparated list of checked exception class names, and is always appended to a method header. The
convert() method’s throws clause indicates that this method is capable of throwing an
InvalidMediaFormatException or IOException instance to the JVM.
convert() also demonstrates the throw statement, which consists of reserved word throw followed
by an instance of Throwable or a subclass. (You typically instantiate an Exception subclass.) This
statement throws the instance to the JVM, which then searches for a suitable handler to handle the
exception.
The first use of the throw statement is to throw a java.lang.NullPointerException instance when a
null reference is passed as the source or destination filename. This unchecked exception is commonly
thrown to indicate that a contract has been violated via a passed null reference. (Chapter 6’s discussion
of the java.util.Objects class presents an alternative approach to dealing with null references passed to
parameters.) For example, you cannot pass null filenames to convert().
161
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The second use of the throw statement is to throw a media.InvalidMediaFormatException instance
when the expected media format does not match the existing format. In the contrived example, the
exception is thrown because the expected format is RM and the existing format is WAVE.
Unlike InvalidMediaFormatException, NullPointerException is not listed in convert()’s throws
clause because NullPointerException instances are unchecked. They can occur so frequently that it is
too big a burden to force the developer to properly handle these exceptions. Instead, the developer
should write code that minimizes their occurrences.
Although not thrown from convert(), IOException is listed in this method’s throws clause in
preparation for refactoring this method to perform the conversion with the help of file-handling code.
NullPointerException is one kind of exception that is thrown when an argument proves to be
invalid. The java.lang.IllegalArgumentException class generalizes the illegal argument scenario to
include other kinds of illegal arguments. For example, the following method throws an
IllegalArgumentException instance when a numeric argument is negative:
public static double sqrt(double x)
{
if (x < 0)
throw new IllegalArgumentException(x+" is negative");
// Calculate the square root of x.
}
There are a few additional items to keep in mind when working with throws clauses and throw
statements:
162
•
You can append a throws clause to a constructor and throw an exception from the
constructor when something goes wrong while the constructor is executing. The
resulting object will not be created.
•
When an exception is thrown out of an application’s main() method, the JVM
terminates the application and calls the exception’s printStackTrace() method to
print, to the console, the sequence of nested method calls that was awaiting
completion when the exception was thrown.
•
If a superclass method declares a throws clause, the overriding subclass method
does not have to declare a throws clause. However, if the subclass method does
declare a throws clause, the clause must not include the names of checked
exception classes that are not also included in the superclass method’s throws
clause, unless they are the names of exception subclasses. For example, given
superclass method void foo() throws IOException {}, the overriding subclass
method could be declared as void foo() {}, void foo() throws IOException {},
or void foo() throws FileNotFoundException—the
java.io.FileNotFoundException class subclasses IOException.
•
A checked exception class name does not need to appear in a throws clause when
the name of its superclass appears.
•
The compiler reports an error when a method throws a checked exception and
does not also handle the exception or list the exception in its throws clause.
•
Do not include the names of unchecked exception classes in a throws clause.
These names are not required because such exceptions should never occur.
Furthermore, they only clutter source code, and possibly confuse someone who is
trying to understand that code.
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
•
You can declare a checked exception class name in a method’s throws clause
without throwing an instance of this class from the method. (Perhaps the method
has yet to be fully coded.) However, Java requires that you provide code to handle
this exception, even though it is not thrown.
Handling Exceptions
A method indicates its intention to handle one or more exceptions by specifying a try statement that
includes one or more appropriate catch blocks. The try statement consists of reserved word try followed
by a brace-delimited body. You place code that throws exceptions into this block.
A catch block consists of reserved word catch, followed by a round bracket-delimited singleparameter list that specifies an exception class name, followed by a brace-delimited body. You place
code that handles exceptions whose types match the type of the catch block’s parameter list’s exception
class parameter in this block.
A catch block is specified immediately after a try block. When an exception is thrown, the JVM
searches for a handler by first examining the catch block to see whether its parameter type matches or is
the superclass type of the exception that has been thrown.
If the catch block is found, its body executes and the exception is handled. Otherwise, the JVM
proceeds up the method-call stack, looking for the first method whose try statement contains an
appropriate catch block. This process continues unless a catch block is found or execution leaves the
main() method.
The following example illustrates try and catch:
try
{
int x = 1/0;
}
catch (ArithmeticException ae)
{
System.out.println("attempt to divide by zero");
}
When execution enters the try block, an attempt is made to divide integer 1 by integer 0. The JVM
responds by instantiating ArithmeticException and throwing this exception. It then detects the catch
block, which is capable of handling thrown ArithmeticException objects, and transfers execution to this
block, which invokes System.out.println() to output a suitable message—the exception is handled.
Because ArithmeticException is an example of an unchecked exception type, and because
unchecked exceptions represent coding mistakes that must be fixed, you typically don’t catch them, as
demonstrated previously. Instead, you would fix the problem that led to the thrown exception.
■ Tip You might want to name your catch block parameters using the abbreviated style shown in the preceding
section. Not only does this convention result in more meaningful exception-oriented parameter names (ae implies
that an ArithmeticException object has been thrown), it can help reduce compiler errors. For example, it is
common practice to name a catch block’s parameter e, for convenience. (Why type a long name?) However, the
compiler will report an error when a previously declared local variable or parameter also uses e as its name—
multiple same-named local variables and parameters cannot exist in the same scope.
163
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Handling Multiple Exception Types
You can specify multiple catch blocks after a try block. For example, Listing 3-25’s convert() method
specifies a throws clause indicating that convert() can throw InvalidMediaFormatException, which is
currently thrown, and IOException, which will be thrown when convert() is refactored. This refactoring
will result in convert() throwing IOException when it cannot read from the source file or write to the
destination file, and throwing FileNotFoundException (a subclass of IOException) when it cannot open
the source file or create the destination file. All these exceptions must be handled, as demonstrated in
Listing 3-26.
Listing 3-26. Handling different kinds of exceptions
import java.io.FileNotFoundException;
import java.io.IOException;
import media.InvalidMediaFormatException;
import media.Media;
class Converter
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println("usage: java Converter srcfile dstfile");
return;
}
try
{
Media.convert(args[0], args[1]);
}
catch (InvalidMediaFormatException imfe)
{
System.out.println("Unable to convert "+args[0]+" to "+args[1]);
System.out.println("Expecting "+args[0]+" to conform to "+
imfe.getExpectedFormat()+" format.");
System.out.println("However, "+args[0]+" conformed to "+
imfe.getExistingFormat()+" format.");
}
catch (FileNotFoundException fnfe)
{
}
catch (IOException ioe)
{
}
}
}
The call to Media’s convert() method in Listing 3-26 is placed in a try block because this method is
capable of throwing an instance of the checked InvalidMediaFormatException, IOException, or
FileNotFoundException class—checked exceptions must be handled or be declared to be thrown via a
throws clause that is appended to the method.
164
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The catch (InvalidMediaFormatException imfe) block’s statements are designed to provide a
descriptive error message to the user. A more sophisticated application would localize these names so
that the user could read the message in the user’s language. The developer-oriented detail message is
not output because it is not necessary in this trivial application.
■ Note A developer-oriented detail message is typically not localized. Instead, it is expressed in the developer’s
language. Users should never see detail messages.
Although not thrown, a catch block for IOException is required because this checked exception type
appears in convert()’s throws clause. Because the catch (IOException ioe) block can also handle
thrown FileNotFoundException instances (because FileNotFoundException subclasses IOException), the
catch (FileNotFoundException fnfe) block isn’t necessary at this point, but is present to separate out
the handling of a situation where a file cannot be opened for reading or created for writing (which will be
addressed once convert() is refactored to include file code).
Assuming that the current directory contains Listing 3-26 and a media subdirectory containing
InvalidMediaFormatException.java and Media.java, compile this listing (javac Converter.java), which
also compiles media’s source files, and run the application, as in java Converter A B. Converter
responds by presenting the following output:
Unable to convert A to B
Expecting A to conform to RM format.
However, A conformed to WAVE format.
Listing 3-26’s empty FileNotFoundException and IOException catch blocks illustrate the often-seen
problem of leaving catch blocks empty because they are inconvenient to code. Unless you have a good
reason, do not create an empty catch block. It swallows exceptions and you do not know that the
exceptions were thrown. (For brevity, I don’t always code catch blocks in this book’s examples.)
■ Caution The compiler reports an error when you specify two or more catch blocks with the same parameter
type after a try body. Example: try {} catch (IOException ioe1) {} catch (IOException ioe2) {}. You
must merge these catch blocks into one block.
Although you can write catch blocks in any order, the compiler restricts this order when one catch
block’s parameter is a supertype of another catch block’s parameter. The subtype parameter catch block
must precede the supertype parameter catch block; otherwise, the subtype parameter catch block will
never be executed.
For example, the FileNotFoundException catch block must precede the IOException catch block. If
the compiler allowed the IOException catch block to be specified first, the FileNotFoundException catch
block would never execute because a FileNotFoundException instance is also an instance of its
IOException superclass.
165
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Multicatch
Suppose you have two or more catch blocks whose code is identical or nearly identical. To eliminate this
redundancy, you might be tempted to refactor this code into a single catch block with a common
superclass exception type (such as catch (Exception e) {}). However, catching overly broad exceptions
is not a good idea because doing so masks the purpose for the handler (what exceptions are handled by
catch (Exception e) {}, for example). Also, the single catch block might inadvertently handle thrown
exceptions that should be handled elsewhere. (Perhaps these exceptions are thrown as a result of
refactored code.)
Java provides the multicatch language feature to avoid redundancy and also the problems inherent
with catching overly broad exceptions. Multicatch lets you specify multiple exception types in a catch
block where each successive type is separated from its predecessor by placing a vertical bar (|) between
these types. Consider the following example:
try
{
Media.convert(args[0], args[1]);
}
catch (InvalidMediaFormatException | UnsupportedMediaFormatException imfeumfe)
{
// common code to respond to these similar exceptions
}
This example assumes that convert() is also capable of throwing
media.UnsupportedMediaFormatException when it detects a media format that it cannot handle (such as a
video format). When convert() throws either InvalidMediaFormatException or
UnsupportedMediaFormatException, the catch block will handle either exception.
When multiple exception types are listed in a catch block’s single parameter list, the parameter is
implicitly regarded as final. As a result, you cannot change the parameter’s value. For example, you
cannot change the reference stored in the example’s imfeumfe parameter.
Multicatch is not always necessary. For example, you do not need to specify catch
(FileNotFoundException | IOException fnfeioe) { /* suitable common code */ } to handle
FileNotFoundException and IOException because catch (IOException ioe) accomplishes the same task,
by catching FileNotFoundException as well as IOException. For this reason, the compiler reports an error
when it detects a catch block whose parameter list exception types include a supertype and a subtype.
■ Note The bytecode resulting from compiling a catch block that handles multiple exception types will be smaller
than compiling several catch blocks that each handle only one of the listed exception types. A catch block that
handles multiple exception types contributes no duplicate bytecode during compilation. In other words, the
bytecode doesn’t contain replicated exception handlers.
Rethrowing Exceptions
While discussing the Throwable class, I discussed wrapping lower-level exceptions in higher-level
exceptions. This activity will typically take place in a catch block, and is illustrated in the following
example:
166
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
catch (IOException ioe)
{
throw new ReportCreationException(ioe);
}
This example assumes that a helper method has just thrown a generic IOException instance as the
result of trying to create a report. The public method’s contract states that ReportCreationException is
thrown in this case. To satisfy the contract, the latter exception is thrown. To satisfy the developer who is
responsible for debugging a faulty application, the IOException instance is wrapped inside the
ReportCreationException instance that is thrown to the public method’s caller.
Sometimes, a catch block might not be able to fully handle an exception. Perhaps it needs access to
information provided by some ancestor method in the method-call stack. However, the catch block
might be able to partly handle the exception. In this case, it should partly handle the exception, and then
rethrow the exception so that a handler in the ancestor method can finish handling the exception. This
scenario is demonstrated in the following example:
catch (FileNotFoundException fnfe)
{
// Provide code to partially handle the exception here.
throw fnfe; // Rethrow the exception here.
}
Final Rethrow
Java 7’s compiler analyzes rethrown exceptions more precisely than its predecessors, but only when no
assignments are made to the rethrown exception’s catch block parameter (the parameter is effectively
final). When an exception originates from the preceding try block and is a supertype/subtype of the
parameter’s type, the compiler throws the actual type of the caught exception instead of throwing the
type of the parameter (as is done in previous Java versions).
The purpose of this final rethrow feature is to facilitate adding a try statement around a block of
code to intercept, process, and rethrow an exception without affecting the statically determined set of
exceptions thrown from the code. Also, this feature lets you provide a common exception handler to
partly handle the exception close to where it’s thrown, and provide more precise handlers elsewhere that
handle the rethrown exception. Consider Listing 3-27.
Listing 3-27. A pressure simulation
class PressureException extends Exception
{
PressureException(String msg)
{
super(msg);
}
}
class TemperatureException extends Exception
{
TemperatureException(String msg)
{
super(msg);
}
}
167
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
class MonitorEngine
{
public static void main(String[] args)
{
try
{
monitor();
}
catch (Exception e)
{
if (e instanceof PressureException)
System.out.println("correcting pressure problem");
else
System.out.println("correcting temperature problem");
}
}
static void monitor() throws Exception
{
try
{
if (Math.random() < 0.1)
throw new PressureException("pressure too high");
else
if (Math.random() > 0.9)
throw new TemperatureException("temperature too high");
else
System.out.println("all is well");
}
catch (Exception e)
{
System.out.println(e.getMessage());
throw e;
}
}
}
Listing 3-27 simulates the testing of an experimental rocket engine to see if the engine’s pressure or
temperature exceeds a safety threshold. It performs this testing via the monitor() helper method.
monitor()’s try block throws PressureException when it detects a pressure extreme, and throws
TemperatureException when it detects a temperature extreme. (Because this is only a simulation,
random numbers are used. I’ll have more to say about random numbers in Chapter 4.) The try block is
followed by a catch block, which is designed to partly handle the exception by outputting a warning
message. This exception is then rethrown so that monitor()’s calling method can finish handling the
exception.
Before Java 7, you couldn’t specify PressureException and TemperatureException in monitor()’s
throws clause because the catch block’s e parameter is of type Exception and rethrowing an exception
was treated as throwing the parameter’s type. Starting with Java 7, you can specify these exception types
in the throws clause because the compiler determines that the exception thrown by throw e came from
the try block, and only PressureException and TemperatureException can be thrown from this block.
Because you can now specify static void monitor() throws PressureException,
TemperatureException, you can provide more precise handlers where monitor() is called, as the
following example demonstrates:
168
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
try
{
monitor();
}
catch (PressureException pe)
{
System.out.println("correcting pressure problem");
}
catch (TemperatureException te)
{
System.out.println("correcting temperature problem");
}
Because of the improved type checking offered by final rethrow, source code that compiled under
previous versions of Java might fail to compile under Java 7. For example, consider Listing 3-28.
This book was purchased by [email protected]
Listing 3-28. Demonstrating code breakage as a result of final rethrow
class SuperException extends Exception
{
}
class SubException1 extends SuperException
{
}
class SubException2 extends SuperException
{
}
class BreakageDemo
{
public static void main(String[] args) throws SuperException
{
try
{
throw new SubException1();
}
catch (SuperException se)
{
try
{
throw se;
}
catch (SubException2 se2)
{
}
}
}
}
Listing 3-28 compiles under Java 6 and earlier. However, it fails to compile under Java 7, whose
compiler detects and reports the fact that SubException2 is never thrown in the body of the
corresponding try statement.
169
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Although unlikely to occur, it’s possible to run into this problem. Instead of grumbling about the
breakage, consider the value in having the compiler detect a source of redundant code whose removal
results in cleaner source code and a smaller classfile.
Performing Cleanup
In some situations, you might want to prevent an exception from being thrown out of a method before
the method’s cleanup code is executed. For example, you might want to close a file that was opened, but
could not be written, possibly because of insufficient disk space. Java provides the finally block for this
situation.
The finally block consists of reserved word finally followed by a body, which provides the cleanup
code. A finally block follows either a catch block or a try block. In the former case, the exception is
handled (and possibly rethrown) before finally executes. In the latter case, finally executes before the
exception is thrown and handled.
Listing 3-29 demonstrates the finally block in the context of a file-copying application.
Listing 3-29. Cleaning up after handling a thrown exception
import
import
import
import
java.io.FileInputStream;
java.io.FileOutputStream;
java.io.FileNotFoundException;
java.io.IOException;
class Copy
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println("usage: java Copy srcfile dstfile");
return;
}
FileInputStream fis = null;
try
{
fis = new FileInputStream(args[0]);
FileOutputStream fos = null;
try
{
fos = new FileOutputStream(args[1]);
int b; // I chose b instead of byte because byte is a reserved word.
while ((b = fis.read()) != -1)
fos.write(b);
}
catch (FileNotFoundException fnfe)
{
String msg = args[1]+" could not be created, possibly because "+
"it might be a directory";
System.err.println(msg);
}
catch (IOException ioe)
170
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
{
String msg = args[0]+" could not be read, or "+args[1]+
" could not be written";
System.err.println(msg);
}
finally
{
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
System.err.println("unable to close "+args[1]);
}
}
}
catch (FileNotFoundException fnfe)
{
String msg = args[0]+" could not be found or might be a directory";
System.err.println(msg);
}
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
System.err.println("unable to close "+args[0]);
}
}
}
}
■ Note Do not be concerned if you find this listing’s file-oriented code difficult to grasp; I will formally introduce
I/O and the listing’s file-oriented types in Chapter 8. I’m presenting this code here because file copying provides a
perfect example of the finally block.
Listing 3-29 presents an application that copies bytes from a source file to a destination file via a
nested pair of try blocks. The outer try block uses a java.io.FileInputStream object to open the source
file for reading; the inner try block uses a java.io.FileOutputStream object to create the destination file
for writing, and also contains the file-copying code.
171
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
If the fis = new FileInputStream(args[0]); expression throws FileNotFoundException, execution
flows into the outer try statement’s catch (FileNotFoundException fnfe) block, which outputs a
suitable message to the user. Execution then enters the outer try statement’s finally block.
The outer try statement’s finally block closes an open source file. However, when
FileNotFoundException is thrown, the source file is not open—no reference was assigned to fis. The
finally block uses if (fis != null) to detect this situation, and does not attempt to close the file.
If fis = new FileInputStream(args[0]); succeeds, execution flows into the inner try block, which
executes fos = new FileOutputStream(args[1]);. If this expression throws FileNotFoundException,
execution moves into the inner try’s catch (FileNotFoundException fnfe) block, which outputs a
suitable message to the user.
This time, execution continues with the inner try statement’s finally block. Because the destination
file was not created, no attempt is made to close this file. In contrast, the open source file must be
closed, and this is accomplished when execution moves from the inner finally block to the outer finally
block.
FileInputStream’s and FileOutputStream’s close() methods throw IOException when a file is not
open. Because IOException is checked, these exceptions must be handled; otherwise, it would be
necessary to append a throws IOException clause to the main() method header.
You can specify a try statement with only a finally block. You would do so when you are not
prepared to handle an exception in the enclosing method (or enclosing try statement, if present), but
need to perform cleanup before the thrown exception causes execution to leave the method. Listing 3-30
provides a demonstration.
Listing 3-30. Cleaning up before handling a thrown exception
import
import
import
import
java.io.FileInputStream;
java.io.FileOutputStream;
java.io.FileNotFoundException;
java.io.IOException;
class Copy
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println("usage: java Copy srcfile dstfile");
return;
}
try
{
copy(args[0], args[1]);
}
catch (FileNotFoundException fnfe)
{
String msg = args[0]+" could not be found or might be a directory,"+
" or "+args[1]+" could not be created, "+
"possibly because "+args[1]+" is a directory";
System.err.println(msg);
}
catch (IOException ioe)
{
172
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
String msg = args[0]+" could not be read, or "+args[1]+
" could not be written";
System.err.println(msg);
}
}
static void copy(String srcFile, String dstFile) throws IOException
{
FileInputStream fis = new FileInputStream(srcFile);
try
{
FileOutputStream fos = new FileOutputStream(dstFile);
try
{
int b;
while ((b = fis.read()) != -1)
fos.write(b);
}
finally
{
try
{
fos.close();
}
catch (IOException ioe)
{
System.err.println("unable to close "+dstFile);
}
}
}
finally
{
try
{
fis.close();
}
catch (IOException ioe)
{
System.err.println("unable to close "+srcFile);
}
}
}
}
Listing 3-30 provides an alternative to Listing 3-29 that attempts to be more readable. It
accomplishes this task by introducing a copy() method that uses a nested pair of try-finally constructs to
perform the file-copy operation, and also close each open file whether an exception is or is not thrown.
If the FileInputStream fis = new FileInputStream(srcFile); expression results in a thrown
FileNotFoundException, execution leaves copy() without entering the outer try statement. This
statement is only entered after the FileInputStream object has been created, indicating that the source
file was opened.
If the FileOutputStream fos = new FileOutputStream(dstFile); expression results in a thrown
FileNotFoundException, execution leaves copy() without entering the inner try statement. However,
173
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
execution leaves copy() only after entering the finally block that is mated with the outer try block. This
finally block closes the open source file.
If the read() or write() method in the inner try statement’s body throws an IOException object, the
finally block associated with the inner try block is executed. This finally block closes the open
destination file. Execution then flows into the outer finally block, which closes the open source file, and
continues on out of copy().
■ Caution If the body of a try statement throws an exception, and if the finally block results in another exception
being thrown, this new exception replaces the previous exception, which is lost.
Despite Listing 3-30 being somewhat more readable than Listing 3-29, there is still a lot of
boilerplate thanks to each finally block requiring a try statement to close a file. This boilerplate is
necessary; its removal results in a new IOException possibly being thrown from the catch block, which
would mask a previously thrown IOException.
Automatic Resource Management
Listings 3-29 and 3-30 are hideous because of the amount of code that’s necessary to ensure that each
file is closed. However, you don’t have to code this way. Instead, you can use Java’s try-with-resources
statement to automatically close resources (objects that must be closed when they are no longer needed)
on your behalf.
The try-with-resources statement minimally consists of a try block that features the following
syntax:
try ([resource declaration; ...] resource declaration)
{
// code to execute
}
Reserved word try is followed by a round bracket-delimited and semicolon-separated list of
resource declarations. Each of the declared resources is to be closed when execution leaves the try block,
either normally or via a thrown exception. The following example uses try-with-resources to shorten
Listing 3-30’s copy() method considerably:
static void copy(String srcFile, String dstFile) throws IOException
{
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(dstFile))
{
int b;
while ((b = fis.read()) != -1)
fos.write(b);
}
}
The example’s try-with-resources statement declares two file resources that must be closed; the
resource declarations are separated with a mandatory semicolon. When the copy() method ends
174
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
(normally or via a thrown exception), fis’s and fos’s close() methods are called, but in the opposite
order to which these resources were created (fis was created before fos). Hence, fos.close() is called
before fis.close().
Suppose that fos.write(buffer, 0, n) throws an IOException instance. Now suppose that the
behind-the-scenes fos.close() method call results in a thrown IOException instance. This latter
exception is suppressed, and the exception thrown by fos.write(buffer, 0, n) is the exception thrown
out of the copy() method. The suppressed exception can be retrieved by calling Throwable’s Throwable[]
getSuppressed() method, which I previously presented.
■ Note A try-with-resources statement can include catch and finally. These blocks are executed after all declared
resources have been closed.
To take advantage of try-with-resources with your own classes, keep in mind that a resource class
must implement the java.lang.AutoCloseable interface or its java.lang.Closeable subinterface. Each
interface provides a close() method that performs the close operation.
Unlike Closeable’s close() method, which is declared to throw only IOException (or a subtype),
AutoCloseable’s close() method is declared to throw Exception. As a result, classes that implement
AutoCloseable, Closeable, or a subinterface can declare their close() methods to throw any kind of
exception. The close() method should be declared to throw a more specific exception, or (as with
java.util.Scanner’s close() method) to not throw an exception if the method cannot fail.
■ Note Implementations of Closeable’s close() method are idempotent; subsequent calls to close() have no
effect on the resource. In contrast, implementations of AutoCloseable’s close() method are not required to be
idempotent, but making them idempotent is recommended.
Assertions
Writing source code is not an easy task. All too often, bugs (defects) are introduced into the code. When a
bug is not discovered before compiling the source code, it makes it into runtime code, which will
probably fail unexpectedly. At this point, the cause of failure can be very difficult to determine.
Developers often make assumptions about application correctness, and some developers think that
specifying comments that state their beliefs about what they think is true at the comment locations is
sufficient for determining correctness. However, comments are useless for preventing bugs because the
compiler ignores them.
Many languages address this problem by providing a language feature called assertions that lets the
developer codify assumptions about application correctness. When the application runs, and if an
assertion fails, the application terminates with a message that helps the developer diagnose the failure’s
cause.
This section introduces you to Java’s assertions language feature. After defining this term, showing
you how to declare assertions, and providing examples, the section looks at using and avoiding
175
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
assertions. Finally, you learn how to selectively enable and disable assertions via the javac compiler
tool’s command-line arguments.
Declaring Assertions
An assertion is a statement that lets you express an assumption of program correctness via a Boolean
expression. If this expression evaluates to true, execution continues with the next statement. Otherwise,
an error that identifies the cause of failure is thrown.
There are two forms of the assertion statement, each of which begins with reserved word assert:
assert expression1 ;
assert expression1 : expression2 ;
In both forms of this statement, expression1 is the Boolean expression. In the second form,
expression2 is any expression that returns a value. It cannot be a call to a method whose return type is
void.
When expression1 evaluates to false, this statement instantiates the AssertionError class. The first
statement form calls this class’s noargument constructor, which does not associate a message
identifying failure details with the AssertionError instance.
The second form calls an AssertionError constructor whose type matches the type of expression2’s
value. This value is passed to the constructor and its string representation is used as the error’s detail
message.
When the error is thrown, the name of the source file and the number of the line from where the
error was thrown are output to the console as part of the thrown error’s stack trace. In many situations,
this information is sufficient for identifying what led to the failure, and the first form of the assertion
statement should be used.
Listing 3-31 demonstrates the first form of the assertion statement.
Listing 3-31. Throwing an assertion error without a detail message
class AssertionDemo
{
public static void main(String[] args)
{
int x = 1;
assert x == 0;
}
}
When assertions are enabled (I discuss this task later), running the previous application results in
the following output:
Exception in thread "main" java.lang.AssertionError
at AssertionDemo.main(AssertionDemo.java:6)
In other situations, more information is needed to help diagnose the cause of failure. For example,
suppose expression1 compares variables x and y, and throws an error when x’s value exceeds y’s value.
Because this should never happen, you would probably use the second statement form to output these
values so you could diagnose the problem.
Listing 3-32 demonstrates the second form of the assertion statement.
176
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-32. Throwing an assertion error with a detail message
class AssertionDemo
{
public static void main(String[] args)
{
int x = 1;
assert x == 0: x;
}
}
Once again, it is assumed that assertions are enabled. Running the previous application results in
the following output:
Exception in thread "main" java.lang.AssertionError: 1
at AssertionDemo.main(AssertionDemo.java:6)
The value in x is appended to the end of the first output line, which is somewhat cryptic. To make
this output more meaningful, you might want to specify an expression that also includes the variable’s
name: assert x == 0: "x = "+x;, for example.
Using Assertions
There are many situations where assertions should be used. These situations organize into internal
invariant, control-flow invariant, and design-by-contract categories. An invariant is something that does
not change.
Internal Invariants
An internal invariant is expression-oriented behavior that is not expected to change. For example,
Listing 3-33 introduces an internal invariant by way of chained if-else statements that output the state of
water based on its temperature.
Listing 3-33. Discovering that an internal invariant can vary
class IIDemo
{
public static void main(String[] args)
{
double temperature = 50.0; // Celsius
if (temperature < 0.0)
System.out.println("water has solidified");
else
if (temperature >= 100.0)
System.out.println("water is boiling into a gas");
else
{
// temperature > 0.0 and temperature < 100.0
assert(temperature > 0.0 && temperature < 100.0): temperature;
System.out.println("water is remaining in its liquid state");
177
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
}
}
A developer might specify only a comment stating an assumption as to what expression causes the
final else to be reached. Because the comment might not be enough to detect the lurking < 0.0
expression bug, an assertion statement is necessary.
Another example of an internal invariant concerns a switch statement with no default case. The
default case is avoided because the developer believes that all paths have been covered. However, this is
not always true, as Listing 3-34 demonstrates.
Listing 3-34. Another buggy internal invariant
class IIDemo
{
final static int NORTH = 0;
final static int SOUTH = 1;
final static int EAST = 2;
final static int WEST = 3;
public static void main(String[] args)
{
int direction = (int) (Math.random()*5);
switch (direction)
{
case NORTH: System.out.println("travelling
case SOUTH: System.out.println("travelling
case EAST : System.out.println("travelling
case WEST : System.out.println("travelling
default
: assert false;
}
}
}
north"); break;
south"); break;
east"); break;
west"); break;
Listing 3-34 assumes that the expression tested by switch will only evaluate to one of four integer
constants. However, (int) (Math.random()*5) can also return 4, causing the default case to execute
assert false;, which always throws AssertionError. (You might have to run this application a few times
to see the assertion error, but first you need to learn how to enable assertions, which I discuss later in
this chapter.)
■ Tip When assertions are disabled, assert false; does not execute and the bug goes undetected. To always
detect this bug, replace assert false; with throw new AssertionError(direction);.
Control-Flow Invariants
A control-flow invariant is a flow of control that is not expected to change. For example, Listing 3-34 uses
an assertion to test an assumption that switch’s default case will not execute. Listing 3-35, which fixes
Listing 3-34’s bug, provides another example.
178
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
This book was purchased by [email protected]
Listing 3-35. A buggy control-flow invariant
class CFDemo
{
final static int NORTH = 0;
final static int SOUTH = 1;
final static int EAST = 2;
final static int WEST = 3;
public static void main(String[] args)
{
int direction = (int)(Math.random()*4);
switch (direction)
{
case NORTH: System.out.println("travelling
case SOUTH: System.out.println("travelling
case EAST : System.out.println("travelling
case WEST : System.out.println("travelling
default
: assert false;
}
}
}
north"); break;
south"); break;
east"); break;
west");
Because the original bug has been fixed, the default case should never be reached. However, the
omission of a break statement that terminates case WEST causes execution to reach the default case. This
control-flow invariant has been broken. (Again, you might have to run this application a few times to see
the assertion error, but first you need to learn how to enable assertions, which I discuss later in this
chapter.)
■ Caution Be careful when using an assertion statement to detect code that should never be executed. If the
assertion statement cannot be reached according to the rules set forth in The Java Language Specification, Third
Edition, by James Gosling, Bill Joy, Guy Steele, and Gilad Bracha (Addison-Wesley, 2005; ISBN: 0321246780) (also
available at (http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html), the compiler will
report an error. For example, for(;;); assert false; causes the compiler to report an error because the
infinite for loop prevents the assertion statement from executing.
Design-by-Contract
Design-by-Contract (see http://en.wikipedia.org/wiki/Design_by_contract) is a way to design software
based on preconditions, postconditions, and invariants (internal, control-flow, and class). Assertion
statements support an informal design-by-contract style of development.
179
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Preconditions
A precondition is something that must be true when a method is called. Assertion statements are often
used to satisfy a helper method’s preconditions by checking that its arguments are legal. Listing 3-36
provides an example.
Listing 3-36. Verifying a precondition
class Lotto649
{
public static void main(String[] args)
{
// Lotto 649 requires that six unique numbers be chosen.
int[] selectedNumbers = new int[6];
// Assign a unique random number from 1 to 49 (inclusive) to each slot
// in the selectedNumbers array.
for (int slot = 0; slot < selectedNumbers.length; slot++)
{
int num;
// Obtain a random number from 1 to 49. That number becomes the
// selected number if it has not previously been chosen.
try_again:
do
{
num = rnd(49)+1;
for (int i = 0; i < slot; i++)
if (selectedNumbers[i] == num)
continue try_again;
break;
}
while (true);
// Assign selected number to appropriate slot.
selectedNumbers[slot] = num;
}
// Sort all selected numbers into ascending order and then print these
// numbers.
sort(selectedNumbers);
for (int i = 0; i < selectedNumbers.length; i++)
System.out.print(selectedNumbers[i]+" ");
}
static int rnd(int limit)
{
// This method returns a random number (actually, a pseudorandom number)
// ranging from 0 through limit-1 (inclusive).
assert limit > 1: "limit = "+limit;
return (int) (Math.random()*limit);
}
static void sort(int[] x)
{
// This method sorts the integers in the passed array into ascending
// order.
for (int pass = 0; pass < x.length-1; pass++)
180
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
for (int i = x.length-1; i > pass; i--)
if (x[i] < x[pass])
{
int temp = x[i];
x[i] = x[pass];
x[pass] = temp;
}
}
}
Listing 3-36’s application simulates Lotto 6/49, one of Canada’s national lottery games. The rnd()
helper method returns a randomly chosen integer between 0 and limit-1. An assertion statement
verifies the precondition that limit’s value must be 2 or higher.
■ Note The sort() helper method sorts (orders) the selectedNumbers array’s integers into ascending order by
implementing an algorithm (a recipe for accomplishing some task) called Bubble Sort.
Bubble Sort works by making multiple passes over the array. During each pass, various comparisons and swaps
ensure that the next smallest element value “bubbles” toward the top of the array, which would be the element at
index 0.
Bubble Sort is not efficient, but is more than adequate for sorting a six-element array. Although I could have used
one of the efficient sort() methods located in the java.util package’s Arrays class (for example,
Arrays.sort(selectedNumbers); accomplishes the same objective as Listing 3-36’s sort(selectedNumbers);
method call, but does so more efficiently), I chose to use Bubble Sort because I prefer to wait until Chapter 5
before getting into the Arrays class.
Postconditions
A postcondition is something that must be true after a method successfully completes. Assertion
statements are often used to satisfy a helper method’s postconditions by checking that its result is legal.
Listing 3-37 provides an example.
Listing 3-37. Verifying a postcondition as well as preconditions
class MergeArrays
{
public static void main(String[] args)
{
int[] x = { 1, 2, 3, 4, 5 };
int[] y = { 1, 2, 7, 9 };
int[] result = merge(x, y);
for (int i = 0; i < result.length; i++)
181
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
System.out.println(result[i]);
}
static int[] merge(int[] a, int[] b)
{
if (a == null)
throw new NullPointerException("a is null");
if (b == null)
throw new NullPointerException("b is null");
int[] result = new int[a.length+b.length];
// Precondition
assert result.length == a.length+b.length: "length mismatch";
for (int i = 0; i < a.length; i++)
result[i] = a[i];
for (int i = 0; i < b.length; i++)
result[a.length+i-1] = b[i];
// Postcondition
assert containsAll(result, a, b): "value missing from array";
return result;
}
static boolean containsAll(int[] result, int[] a, int[] b)
{
for (int i = 0; i < a.length; i++)
if (!contains(result, a[i]))
return false;
for (int i = 0; i < b.length; i++)
if (!contains(result, b[i]))
return false;
return true;
}
static boolean contains(int[] a, int val)
{
for (int i = 0; i < a.length; i++)
if (a[i] == val)
return true;
return false;
}
}
Listing 3-37 uses an assertion statement to verify the postcondition that all the values in the two
arrays being merged are present in the merged array. The postcondition is not satisfied, however,
because this listing contains a bug.
Listing 3-37 also shows preconditions and postconditions being used together. The solitary
precondition verifies that the merged array length equals the lengths of the arrays being merged prior to
the merge logic.
Class Invariants
A class invariant is a kind of internal invariant that applies to every instance of a class at all times, except
when an instance is transitioning from one consistent state to another.
For example, suppose instances of a class contain arrays whose values are sorted in ascending
order. You might want to include an isSorted() method in the class that returns true if the array is still
182
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
sorted, and verify that each constructor and method that modifies the array specifies assert
isSorted(); prior to exit, to satisfy the assumption that the array is still sorted when the
constructor/method exists.
Avoiding Assertions
Although there are many situations where assertions should be used, there also are situations where
they should be avoided. For example, you should not use assertions to check the arguments that are
passed to public methods, for the following reasons:
•
Checking a public method’s arguments is part of the contract that exists between
the method and its caller. If you use assertions to check these arguments, and if
assertions are disabled, this contract is violated because the arguments will not be
checked.
•
Assertions also prevent appropriate exceptions from being thrown. For example,
when an illegal argument is passed to a public method, it is common to throw
IllegalArgumentException or NullPointerException. However, AssertionError is
thrown instead.
You should also avoid using assertions to perform work required by the application to function
correctly. This work is often performed as a side effect of the assertion’s Boolean expression. When
assertions are disabled, the work is not performed.
For example, suppose you have a list of Employee objects and a few null references that are also
stored in this list, and you want to remove all the null references. It would not be correct to remove these
references via the following assertion statement:
assert employees.removeAll(null);
Although the assertion statement will not throw AssertionError because there is at least one null
reference in the employees list, the application that depends upon this statement executing will fail when
assertions are disabled.
Instead of depending on the former code to remove the null references, you would be better off
using code similar to the following:
boolean allNullsRemoved = employees.removeAll(null);
assert allNullsRemoved;
This time, all null references are removed regardless of whether assertions are enabled or disabled,
and you can still specify an assertion to verify that nulls were removed.
Enabling and Disabling Assertions
The compiler records assertions in the classfile. However, assertions are disabled at runtime because
they can affect performance. An assertion might call a method that takes awhile to complete, and this
would impact the running application’s performance.
You must enable the classfile’s assertions before you can test assumptions about the behaviors of
your classes. Accomplish this task by specifying the -enableassertions or -ea command-line option
when running the java application launcher tool.
The -enableassertions and -ea command-line options let you enable assertions at various
granularities based upon one of the following arguments (except for the noargument scenario, you must
use a colon to separate the option from its argument):
183
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
•
No argument: Assertions are enabled in all classes except system classes.
•
PackageName...: Assertions are enabled in the specified package and its
subpackages by specifying the package name followed by ....
•
...: Assertions are enabled in the unnamed package, which happens to be
whatever directory is current.
•
ClassName: Assertions are enabled in the named class by specifying the class name.
For example, you can enable all assertions except system assertions when running the MergeArrays
application via java -ea MergeArrays. Also, you could enable any assertions in this chapter’s logging
package by specifying java -ea:logging TestLogger.
Assertions can be disabled, and also at various granularities, by specifying either of the disableassertions or -da command-line options. These options take the same arguments as enableassertions and -ea.
For example, java -ea -da:loneclass mainclass enables all assertions except for those in
loneclass. (loneclass and mainclass are placeholders for the actual classes that you specify.)
The previous options apply to all classloaders (discussed in Appendix C). Except when taking no
arguments, they also apply to system classes. This exception simplifies the enabling of assertion
statements in all classes except for system classes, which is often desirable.
To enable system assertions, specify either -enablesystemassertions or -esa; for example, java -esa
-ea:logging TestLogger. Specify either -disablesystemassertions or -dsa to disable system assertions.
Annotations
While developing a Java application, you might want to annotate various application elements, or
associate metadata (data that describes other data) with them. For example, you might want to identify
methods that are not fully implemented so that you will not forget to implement them. Java’s
annotations language feature lets you accomplish this task.
This section introduces you to annotations. After defining this term and presenting three kinds of
compiler-supported annotations as examples, the section shows you how to declare your own
annotation types and use these types to annotate source code. Finally, you discover how to process your
own annotations to accomplish useful tasks.
■ Note Java has always supported ad hoc annotation mechanisms. For example, the java.lang.Cloneable
interface identifies classes whose instances can be shallowly cloned via Object’s clone() method, the
transient reserved word marks fields that are to be ignored during serialization (discussed in Chapter 8), and the
@deprecated javadoc tag documents methods that are no longer supported. Java 6 formalized the need for
annotations by introducing the annotations language feature.
184
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Discovering Annotations
An annotation is an instance of an annotation type and associates metadata with an application
element. It is expressed in source code by prefixing the type name with the @ symbol. For example,
@Readonly is an annotation and Readonly is its type.
■ Note You can use annotations to associate metadata with constructors, fields, local variables, methods,
packages, parameters, and types (annotation, class, enum, and interface).
The compiler supports the Override, Deprecated, SuppressWarnings, and SafeVarargs annotation
types. These types are located in the java.lang package.
@Override annotations are useful for expressing that a subclass method overrides a method in the
superclass, and does not overload that method instead. The following example reveals this annotation
being used to prefix the overriding method:
@Override
public void draw(int color)
{
// drawing code
}
@Deprecated annotations are useful for indicating that the marked application element is deprecated
(phased out) and should no longer be used. The compiler warns you when a deprecated application
element is accessed by nondeprecated code.
In contrast, the @deprecated javadoc tag and associated text warns you against using the deprecated
item, and tells you what to use instead. The following example demonstrates that @Deprecated and
@deprecated can be used together:
/**
* Allocates a <code>Date</code> object and initializes it so that
* it represents midnight, local time, at the beginning of the day
* specified by the <code>year</code>, <code>month</code>, and
* <code>date</code> arguments.
*
* @param
year
the year minus 1900.
* @param
month
the month between 0-11.
* @param
date
the day of the month between 1-31.
* @see
java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.set(year + 1900, month, date)</code>
* or <code>GregorianCalendar(year + 1900, month, date)</code>.
*/
@Deprecated
public Date(int year, int month, int date)
{
this(year, month, date, 0, 0, 0);
}
185
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
This example excerpts one of the constructors in Java’s Date class (located in the java.util
package). Its Javadoc comment reveals that Date(int year, int month, int date) has been deprecated
in favor of using the set() method in the Calendar class (also located in the java.util package). (I
explore Date and Calendar in Appendix C.)
The compiler suppresses warnings when a compilation unit (typically a class or interface) refers to a
deprecated class, method, or field. This feature lets you modify legacy APIs without generating
deprecation warnings, and is demonstrated in Listing 3-38.
Listing 3-38. Referencing a deprecated field from within the same class declaration
class Employee
{
/**
* Employee’s name
* @deprecated New version uses firstName and lastName fields.
*/
@Deprecated
String name;
String firstName;
String lastName;
public static void main(String[] args)
{
Employee emp = new Employee();
emp.name = "John Doe";
}
}
Listing 3-38 declares an Employee class with a name field that has been deprecated. Although
Employee’s main() method refers to name, the compiler will suppress a deprecation warning because the
deprecation and reference occur in the same class.
Suppose you refactor this listing by introducing a new UseEmployee class and moving Employee’s
main() method to this class. Listing 3-39 presents the resulting class structure.
Listing 3-39. Referencing a deprecated field from within another class declaration
class Employee
{
/**
* Employee’s name
* @deprecated New version uses firstName and lastName fields.
*/
@Deprecated
String name;
String firstName;
String lastName;
}
class UseEmployee
{
public static void main(String[] args)
{
Employee emp = new Employee();
emp.name = "John Doe";
186
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
}
If you attempt to compile this source code via the javac compiler tool, you will discover the
following messages:
Note: Employee.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
You will need to specify -Xlint:deprecation as one of javac’s command-line arguments (as in javac
-Xlint:deprecation Employee.java) to discover the deprecated item and the code that refers to this
item:
Employee.java:17: warning: [deprecation] name in Employee has been deprecated
emp.name = "John Doe";
^
1 warning
@SuppressWarnings annotations are useful for suppressing deprecation or unchecked warnings via a
"deprecation" or "unchecked" argument. (Unchecked warnings occur when mixing code that uses
generics with pre-generics legacy code. I discuss generics and unchecked warnings later in this chapter.)
For example, Listing 3-40 uses @SuppressWarnings with a "deprecation" argument to suppress the
compiler’s deprecation warnings when code within the UseEmployee class’s main() method accesses the
Employee class’s name field.
Listing 3-40. Suppressing the previous deprecation warning
class Employee
{
/**
* Employee’s name
* @deprecated New version uses firstName and lastName fields.
*/
@Deprecated
String name;
String firstName;
String lastName;
}
class UseEmployee
{
@SuppressWarnings("deprecation")
public static void main(String[] args)
{
Employee emp = new Employee();
emp.name = "John Doe";
}
}
187
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
■ Note As a matter of style, you should always specify @SuppressWarnings on the most deeply nested element
where it is effective. For example, if you want to suppress a warning in a particular method, you should annotate
that method rather than its class.
Finally, @SafeVarargs annotations are useful for asserting that the body of the annotated method or
constructor does not perform potentially unsafe operations on its variable number of arguments
parameter. I’ll have more to say about this annotation when I present generics later in this chapter.
Declaring Annotation Types and Annotating Source Code
Before you can annotate source code, you need annotation types that can be instantiated. Java supplies
many annotation types as well as Override, Deprecated, SuppressWarnings, and SafeVarargs. Java also
lets you declare your own types.
You declare an annotation type by specifying the @ symbol, immediately followed by reserved word
interface, followed by the type’s name, followed by a body. For example, Listing 3-41 uses @interface to
declare an annotation type named Stub.
Listing 3-41. Declaring the Stub annotation type
public @interface Stub
{
}
Instances of annotation types that supply no data apart from a name – their bodies are empty – are
known as marker annotations because they mark application elements for some purpose. As Listing 3-42
reveals, @Stub is used to mark empty methods (stubs).
Listing 3-42. Annotating a stubbed-out method
public class Deck // Describes a deck of cards.
{
@Stub
public void shuffle()
{
// This method is empty and will presumably be filled in with appropriate
// code at some later date.
}
}
Listing 3-42’s Deck class declares an empty shuffle() method. This fact is indicated by instantiating
Stub and prefixing shuffle()’s method header with the resulting @Stub annotation.
■ Note Although marker interfaces (introduced in Chapter 2) appear to have been replaced by marker annotations,
this is not the case, because marker interfaces have advantages over marker annotations. One advantage is that a
188
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
marker interface specifies a type that is implemented by a marked class, which lets you catch problems at
compile time. For example, if a class does not implement the Cloneable interface, its instances cannot be
shallowly cloned via Object’s clone() method. If Cloneable had been implemented as a marker annotation, this
problem would not be detected until runtime.
Although marker annotations are useful (@Override and @Deprecated are good examples), you will
typically want to enhance an annotation type so that you can store metadata via its instances. You
accomplish this task by adding elements to the type.
An element is a method header that appears in the annotation type’s body. It cannot have
parameters or a throws clause, and its return type must be a primitive type (such as int), String, Class,
an enum, an annotation type, or an array of the preceding types. However, it can have a default value.
Listing 3-43 adds three elements to Stub.
This book was purchased by [email protected]
Listing 3-43. Adding three elements to the Stub annotation type
public @interface Stub
{
int id(); // A semicolon must terminate an element declaration.
String dueDate();
String developer() default "unassigned";
}
The id() element specifies a 32-bit integer that identifies the stub. The dueDate() element specifies
a String-based date that identifies when the method stub is to be implemented. Finally, developer()
specifies the String-based name of the developer responsible for coding the method stub.
Unlike id() and dueDate(), developer() is declared with a default value, "unassigned". When you
instantiate Stub and do not assign a value to developer() in that instance, as is the case with Listing 3-44,
this default value is assigned to developer().
Listing 3-44. Initializing a Stub instance’s elements
public class Deck
{
@Stub
(
id = 1,
dueDate = "12/21/2012"
)
public void shuffle()
{
}
}
Listing 3-44 reveals one @Stub annotation that initializes its id() element to 1 and its dueDate()
element to "12/21/2012". Each element name does not have a trailing (), and the comma-separated list
of two element initializers appears between ( and ).
Suppose you decide to replace Stub’s id(), dueDate(), and developer() elements with a single
String value() element whose string specifies comma-separated ID, due date, and developer name
values. Listing 3-45 shows you two ways to initialize value.
189
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-45. Initializing each Stub instance’s value() element
public class Deck
{
@Stub(value = "1,12/21/2012,unassigned")
public void shuffle()
{
}
@Stub("2,12/21/2012,unassigned")
public Card[] deal(int ncards)
{
return null;
}
}
Listing 3-45 reveals special treatment for the value() element. When it is an annotation type’s only
element, you can omit value()’s name and = from the initializer. I used this fact to specify
@SuppressWarnings("deprecation") in Listing 3-40.
Using Meta-Annotations in Annotation Type Declarations
Each of the Override, Deprecated, and SuppressWarnings annotation types is itself annotated with metaannotations (annotations that annotate annotation types). For example, Listing 3-46 shows you that the
SuppressWarnings annotation type is annotated with two meta-annotations.
Listing 3-46. The annotated SuppressWarnings type declaration
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings
The Target annotation type, which is located in the java.lang.annotation package, identifies the
kinds of application elements to which an annotation type applies. @Target indicates that
@SuppressWarnings annotations can be used to annotate types, fields, methods, parameters,
constructors, and local variables.
Each of TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, and LOCAL_VARIABLE is a member of the
ElementType enum, which is also located in the java.lang.annotation package.
The { and } characters surrounding the comma-separated list of values assigned to Target’s value()
element signify an array—value()’s return type is String[]. Although these braces are necessary (unless
the array consists of one item), value= could be omitted when initializing @Target because Target
declares only a value() element.
The Retention annotation type, which is located in the java.lang.annotation package, identifies the
retention (also known as lifetime) of an annotation type’s annotations. @Retention indicates that
@SuppressWarnings annotations have a lifetime that is limited to source code—they do not exist after
compilation.
SOURCE is one of the members of the RetentionPolicy enum (located in the java.lang.annotation
package). The other members are CLASS and RUNTIME. These three members specify the following
retention policies:
•
190
CLASS: The compiler records annotations in the classfile, but the JVM does not
retain them (to save memory space). This policy is the default.
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
•
RUNTIME: The compiler records annotations in the classfile, and the JVM retains
them so that they can be read via the Reflection API (discussed in Chapter 4) at
runtime.
•
SOURCE: The compiler discards annotations after using them.
There are two problems with the Stub annotation types shown in Listings 3-41 and 3-43. First, the
lack of an @Target meta-annotation means that you can annotate any application element @Stub.
However, this annotation only makes sense when applied to methods and constructors. Check out
Listing 3-47.
Listing 3-47. Annotating undesirable application elements
@Stub("1,12/21/2012,unassigned")
public class Deck
{
@Stub("2,12/21/2012,unassigned")
private Card[] cardsRemaining = new Card[52];
@Stub("3,12/21/2012,unassigned")
public Deck()
{
}
@Stub("4,12/21/2012,unassigned")
public void shuffle()
{
}
@Stub("5,12/21/2012,unassigned")
public Card[] deal(@Stub("5,12/21/2012,unassigned") int ncards)
{
return null;
}
}
Listing 3-47 uses @Stub to annotate the Deck class, the cardsRemaining field, and the ncards parameter as
well as annotating the constructor and the two methods. The first three application elements are
inappropriate to annotate because they are not stubs.
You can fix this problem by prefixing the Stub annotation type declaration with
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) so that Stub only applies to methods and
constructors. After doing this, the javac compiler tool will output the following error messages when you
attempt to compile Listing 3-47:
Deck.java:1: error: annotation type not applicable to this kind of declaration
@Stub("1,12/21/2012,unassigned")
^
Deck.java:4: error: annotation type not applicable to this kind of declaration
@Stub("2,12/21/2012,unassigned")
^
Deck.java:15: error: annotation type not applicable to this kind of declaration
public Card[] deal(@Stub("5,12/21/2012,unassigned") int ncards)
^
3 errors
191
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The second problem is that the default CLASS retention policy makes it impossible to process @Stub
annotations at runtime. You can fix this problem by prefixing the Stub type declaration with
@Retention(RetentionPolicy.RUNTIME).
Listing 3-48 presents the Stub annotation type with the desired @Target and @Retention metaannotations.
Listing 3-48. A revamped Stub annotation type
import
import
import
import
java.lang.annotation.ElementType;
java.lang.annotation.Retention;
java.lang.annotation.RetentionPolicy;
java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Stub
{
String value();
}
■ Note Java also provides Documented and Inherited meta-annotation types in the java.lang.annotation
package. Instances of @Documented-annotated annotation types are to be documented by javadoc and similar
tools, whereas instances of @Inherited-annotated annotation types are automatically inherited. According to
Inherited’s Java documentation, if “the user queries the annotation type on a class declaration, and the class
declaration has no annotation for this type, then the class’s superclass will automatically be queried for the
annotation type. This process will be repeated until an annotation for this type is found, or the top of the class
hierarchy (Object) is reached. If no superclass has an annotation for this type, then the query will indicate that the
class in question has no such annotation.”
Processing Annotations
It is not enough to declare an annotation type and use that type to annotate source code. Unless you do
something specific with those annotations, they remain dormant. One way to accomplish something
specific is to write an application that processes the annotations. Listing 3-49’s StubFinder application
does just that.
Listing 3-49. The StubFinder application
import java.lang.reflect.Method;
class StubFinder
{
public static void main(String[] args) throws Exception
{
192
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
if (args.length != 1)
{
System.err.println("usage: java StubFinder classfile");
return;
}
Method[] methods = Class.forName(args[0]).getMethods();
for (int i = 0; i < methods.length; i++)
if (methods[i].isAnnotationPresent(Stub.class))
{
Stub stub = methods[i].getAnnotation(Stub.class);
String[] components = stub.value().split(",");
System.out.println("Stub ID = "+components[0]);
System.out.println("Stub Date = "+components[1]);
System.out.println("Stub Developer = "+components[2]);
System.out.println();
}
}
}
StubFinder loads a classfile whose name is specified as a command-line argument, and outputs the
metadata associated with each @Stub annotation that precedes each public method header. These
annotations are instances of Listing 3-48’s Stub annotation type.
StubFinder next uses a special class named Class (in the java.lang package) and its forName() class
method to load a classfile. Class also provides a getMethods() method that returns an array of Method
objects describing the loaded class’s public methods.
For each loop iteration, a Method object’s isAnnotationPresent() method is called to determine if
the method is annotated with the annotation described by the Stub class (referred to as Stub.class).
If isAnnotationPresent() returns true, Method’s getAnnotation() method is called to return the
annotation Stub instance. This instance’s value() method is called to retrieve the string stored in the
annotation.
Next, String’s split() method is called to split the string’s comma-separated list of ID, date, and
developer values into an array of String objects. Each object is then output along with descriptive text.
Class’s forName() method is capable of throwing various exceptions that must be handled or
explicitly declared as part of a method’s header. For simplicity, I chose to append a throws Exception
clause to the main() method’s header.
■ Caution There are two problems with throws Exception. First, it is better to handle the exception and present
a suitable error message than to “pass the buck” by throwing it out of main(). Second, Exception is generic—it
hides the names of the kinds of exceptions that are thrown. However, it is convenient to specify throws
Exception in a throwaway utility.
Do not be concerned if you do not understand Class, forName(), getMethods(), Method,
isAnnotationPresent(), .class, getAnnotation(), and split(). You will learn about these items in
Chapter 4.
193
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
After compiling StubFinder (javac StubFinder.java), Stub (javac Stub.java), and Listing 3-45’s
Deck class (javac Deck.java), run StubFinder with Deck as its single command-line argument (java
StubFinder Deck). You will observe the following output:
Stub ID = 2
Stub Date = 12/21/2012
Stub Developer = unassigned
Stub ID = 1
Stub Date = 12/21/2012
Stub Developer = unassigned
If you expected the output to reflect the order of appearance of @Stub annotations in Deck.java, you
are probably surprised by the output’s unsorted order. This lack of order is caused by getMethods().
According to this method’s Java documentation, “the elements in the array returned are not sorted and
are not in any particular order.”
■ Note Java 5 introduced an apt tool for processing annotations. This tool’s functionality has been integrated into
the compiler beginning with Java 6 – apt is being phased out. My “Java Tools Annotation Processors” tutorial
(http://tutortutor.ca/cgi-bin/makepage.cgi?/tutorials/ct/jtap) provides a tutorial on using the Java
compiler to process annotations.
Generics
Java 5 introduced generics, language features for declaring and using type-agnostic classes and
interfaces. When working with Java’s Collections Framework (which I introduce in Chapter 5), these
features help you avoid thrown instances of the java.lang.ClassCastException class.
■ Note Although the main use for generics is the Collections Framework, Java’s class library also contains
generified (retrofitted to make use of generics) classes that have nothing to do with this framework:
java.lang.Class, java.lang.ThreadLocal, and java.lang.ref.WeakReference are three examples.
This section introduces you to generics. You first learn how generics promote type safety in the
context of the Collections Framework classes, and then you explore generics in the contexts of generic
types and generic methods. After learning about generics in the context of arrays, you learn how to use
the SafeVarargs annotation type.
194
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Collections and the Need for Type Safety
Java’s Collections Framework makes it possible to store objects in various kinds of object containers
(known as collections) and later retrieve those objects. For example, you can store objects in a list, a set,
or a map. You can then retrieve a single object, or iterate over the collection and retrieve all objects.
Before Java 5 overhauled the Collections Framework to take advantage of generics, there was no way
to prevent a collection from containing objects of mixed types. The compiler did not check an object’s
type to see if it was suitable before it was added to a collection, and this lack of static type checking led to
ClassCastExceptions.
Listing 3-50 demonstrates how easy it is to generate a ClassCastException.
Listing 3-50. Lack of type safety leading to a ClassCastException at runtime
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class Employee
{
private String name;
Employee(String name)
{
this.name = name;
}
String getName()
{
return name;
}
}
class TypeSafety
{
public static void main(String[] args)
{
List employees = new ArrayList();
employees.add(new Employee("John Doe"));
employees.add(new Employee("Jane Smith"));
employees.add("Jack Frost");
Iterator iter = employees.iterator();
while (iter.hasNext())
{
Employee emp = (Employee) iter.next();
System.out.println(emp.getName());
}
}
}
Listing 3-50’s main() method first instantiates java.util.ArrayList, and then uses this list collection
object’s reference to add a pair of Employee objects to the list. It then adds a String object, which violates
the implied contract that ArrayList is supposed to store only Employee objects.
Moving on, main() obtains a java.util.Iterator instance for iterating over the list of Employees. As
long as Iterator’s hasNext() method returns true, its next() method is called to return an object stored
in the array list.
195
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The Object that next() returns must be downcast to Employee so that the Employee object’s
getName() method can be called to return the employee’s name. The string that this method returns is
then output to the standard output device via System.out.println().
The (Employee) cast checks the type of each object returned by next() to make sure that it is
Employee. Although this is true of the first two objects, it is not true of the third object. Attempting to cast
"Jack Frost" to Employee results in a ClassCastException.
The ClassCastException occurs because of an assumption that a list is homogenous. In other words,
a list stores only objects of a single type or a family of related types. In reality, the list is heterogeneous in
that it can store any Object.
Listing 3-51’s generics-based homogenous list avoids ClassCastException.
Listing 3-51. Lack of type safety leading to a compiler error
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
class Employee
{
private String name;
Employee(String name)
{
this.name = name;
}
String getName()
{
return name;
}
}
class TypeSafety
{
public static void main(String[] args)
{
List<Employee> employees = new ArrayList<Employee>();
employees.add(new Employee("John Doe"));
employees.add(new Employee("Jane Smith"));
employees.add("Jack Frost");
Iterator<Employee> iter = employees.iterator();
while (iter.hasNext())
{
Employee emp = iter.next();
System.out.println(emp.getName());
}
}
}
Listing 3-51’s refactored main() method illustrates the central feature of generics, which is the
parameterized type (a class or interface name followed by an angle bracket-delimited type list identifying
what kinds of objects are legal in that context).
For example, List<Employee> indicates only Employee objects can be stored in the List. As shown,
the <Employee> designation can be repeated with ArrayList, as in Arraylist<Employee>, which is the
collection implementation that stores the Employees. Because the compiler can figure out this type
196
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
argument from the context, you can omit the redundant Employee type name from between ArrayList’s
< and > characters, resulting in List<Employee> employees = new ArrayList<>();.
■ Note Because of its appearance, many developers refer to the <> character sequence as the diamond operator.
I don’t regard <> as a true operator, which is why I don’t include it in Table 1-3’s list of Java operators.
Also, Iterator<Employee>—you cannot use the diamond operator in this context—indicates that
iterator() returns an Iterator whose next() method returns only Employee objects. It is not necessary
to cast iter.next()’s returned value to Employee because the compiler inserts the cast on your behalf.
If you attempt to compile this listing, the compiler will report an error when it encounters
employees.add("Jack Frost");. The error message will tell you that the compiler cannot find an
add(java.lang.String) method in the java.util.List<Employee> interface.
Unlike in the pre-generics List interface, which declares an add(Object) method, the generified
List interface’s add() method parameter reflects the interface’s parameterized type name. For example,
List<Employee> implies add(Employee).
Listing 3-50 revealed that the unsafe code causing the ClassCastException (employees.add("Jack
Frost");) and the code that triggers the exception ((Employee) iter.next()) are quite close. However,
they are often farther apart in larger applications.
Rather than having to deal with angry clients while hunting down the unsafe code that ultimately
led to the ClassCastException, you can rely on the compiler saving you this frustration and effort by
reporting an error when it detects this code during compilation. Detecting type safety violations at
compile time is the benefit of using generics.
Generic Types
A generic type is a class or interface that introduces a family of parameterized types by declaring a formal
type parameter list (a comma-separated list of type parameter names between angle brackets). This
syntax is expressed as follows:
class identifier<formal_type_parameter_list> {}
interface identifier<formal_type_parameter_list> {}
For example, List<E> is a generic type, where List is an interface and type parameter E identifies the
list’s element type. Similarly, Map<K, V> is a generic type, where Map is an interface and type parameters K
and V identify the map’s key and value types.
■ Note When declaring a generic type, it is conventional to specify single uppercase letters as type parameter
names. Furthermore, these names should be meaningful. For example, E indicates element, T indicates type, K
indicates key, and V indicates value. If possible, you should avoid choosing a type parameter name that is
meaningless where it is used. For example, List<E> means list of elements, but what does List<S> mean?
197
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Parameterized types instantiate generic types. Each parameterized type replaces the generic type’s
type parameters with type names. For example, List<Employee> (List of Employee) and List<String>
(List of String) are examples of parameterized types based on List<E>. Similarly, Map<String,
Employee> is an example of a parameterized type based on Map<K, V>.
The type name that replaces a type parameter is known as an actual type argument. Generics
supports five kinds of actual type arguments:
•
Concrete type: The name of a class or interface is passed to the type parameter. For
example, List<Employee> employees; specifies that the list elements are Employee
instances.
•
Concrete parameterized type: The name of a parameterized type is passed to the
type parameter. For example, List<List<String>> nameLists; specifies that the
list elements are lists of strings.
•
Array type: An array is passed to the type parameter. For example, List<String[]>
countries; specifies that the list elements are arrays of Strings, possibly city
names.
•
Type parameter: A type parameter is passed to the type parameter. For example,
given class declaration class X<E> { List<E> queue; }, X’s type parameter E is
passed to List’s type parameter E.
•
Wildcard: The ? is passed to the type parameter, indicating an unknown actual
type argument. For example, List<?> list; specifies that the list elements are
unknown. You will learn about wildcards later in the chapter.
A generic type also identifies a raw type, which is a generic type without its type parameters. For
example, List<Employee>’s raw type is List. Raw types are nongeneric and can hold any Object.
■ Note Java allows raw types to be intermixed with generic types to support the vast amount of legacy code that
was written prior to the arrival of generics. However, the compiler outputs a warning message whenever it
encounters a raw type in source code.
Declaring and Using Your Own Generic Types
It is not difficult to declare your own generic types. In addition to specifying a formal type parameter list,
your generic type specifies its type parameter(s) throughout its implementation. For example, Listing 352 declares a Queue<E> generic type.
Listing 3-52. Declaring and using a Queue<E> generic type
class Queue<E>
{
private E[] elements;
private int head, tail;
@SuppressWarnings("unchecked")
Queue(int size)
198
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
{
This book was purchased by [email protected]
if (size < 2)
throw new IllegalArgumentException(""+size);
elements = (E[]) new Object[size];
head = 0;
tail = 0;
}
void insert(E element) throws QueueFullException
{
if (isFull())
throw new QueueFullException();
elements[tail] = element;
tail = (tail+1)%elements.length;
}
E remove() throws QueueEmptyException
{
if (isEmpty())
throw new QueueEmptyException();
E element = elements[head];
head = (head+1)%elements.length;
return element;
}
boolean isEmpty()
{
return head == tail;
}
boolean isFull()
{
return (tail+1)%elements.length == head;
}
public static void main(String[] args)
throws QueueFullException, QueueEmptyException
{
Queue<String> queue = new Queue<>(6);
System.out.println("Empty: "+queue.isEmpty());
System.out.println("Full: "+queue.isFull());
System.out.println("Adding A");
queue.insert("A");
System.out.println("Adding B");
queue.insert("B");
System.out.println("Adding C");
queue.insert("C");
System.out.println("Adding D");
queue.insert("D");
System.out.println("Adding E");
queue.insert("E");
System.out.println("Empty: "+queue.isEmpty());
System.out.println("Full: "+queue.isFull());
System.out.println("Removing "+queue.remove());
System.out.println("Empty: "+queue.isEmpty());
System.out.println("Full: "+queue.isFull());
System.out.println("Adding F");
199
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
queue.insert("F");
while (!queue.isEmpty())
System.out.println("Removing "+queue.remove());
System.out.println("Empty: "+queue.isEmpty());
System.out.println("Full: "+queue.isFull());
}
}
class QueueEmptyException extends Exception
{
}
class QueueFullException extends Exception
{
}
Listing 3-52 declares Queue, QueueEmptyException, and QueueFullException classes. The latter two
classes describe checked exceptions that are thrown from methods of the former class.
Queue implements a queue, a data structure that stores elements in first-in, first-out order. An
element is inserted at the tail and removed at the head. The queue is empty when the head equals the
tail, and full when the tail is one less than the head. As a result, a queue of size n can store a maximum of
n-1 elements.
Notice that Queue<E>’s E type parameter appears throughout the source code. For example, E
appears in the elements array declaration to denote the array’s element type. E is also specified as the
type of insert()’s parameter and as remove()’s return type.
E also appears in elements = (E[]) new Object[size];. (I will explain later why I specified this
expression instead of specifying the more compact elements = new E[size]; expression.)
The E[] cast results in the compiler warning about this cast being unchecked. The compiler is
concerned that downcasting from Object[] to E[] might result in a violation of type safety because any
kind of object can be stored in Object[].
The compiler’s concern is not justified in this example. There is no way that a non-E object can
appear in the E[] array. Because the warning is meaningless in this context, it is suppressed by prefixing
the constructor with @SuppressWarnings("unchecked").
■ Caution Be careful when suppressing an unchecked warning. You must first prove that a ClassCastException
cannot occur, and then you can suppress the warning.
When you run this application, it generates the following output:
Empty: true
Full: false
Adding A
Adding B
Adding C
Adding D
Adding E
Empty: false
Full: true
Removing A
Empty: false
200
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Full: false
Adding F
Removing B
Removing C
Removing D
Removing E
Removing F
Empty: true
Full: false
Type Parameter Bounds
List<E>’s E type parameter and Map<K, V>’s K and V type parameters are examples of unbounded type
parameters. You can pass any actual type argument to an unbounded type parameter.
It is sometimes necessary to restrict the kinds of actual type arguments that can be passed to a type
parameter. For example, you might want to declare a class whose instances can only store instances of
classes that subclass an abstract Shape class (such as Circle and Rectangle).
To restrict actual type arguments, you can specify an upper bound, a type that serves as an upper
limit on the types that can be chosen as actual type arguments. The upper bound is specified via
reserved word extends followed by a type name.
For example, ShapesList<E extends Shape> identifies Shape as an upper bound. You can specify
ShapesList<Circle>, ShapesList<Rectangle>, and even ShapesList<Shape>, but not ShapesList<String>
because String is not a subclass of Shape.
You can assign more than one upper bound to a type parameter, where the first bound is a class or
interface, and where each additional upper bound is an interface, by using the ampersand character (&)
to separate bound names. Consider Listing 3-53.
Listing 3-53. Assigning multiple upper bounds to a type parameter
abstract class Shape
{
}
class Circle extends Shape implements Comparable<Circle>
{
private double x, y, radius;
Circle(double x, double y, double radius)
{
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public int compareTo(Circle circle)
{
if (radius < circle.radius)
return -1;
else
if (radius > circle.radius)
return 1;
else
201
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
return 0;
}
@Override
public String toString()
{
return "("+x+", "+y+", "+radius+")";
}
}
class SortedShapesList<S extends Shape&Comparable<S>>
{
@SuppressWarnings("unchecked")
private S[] shapes = (S[]) new Shape[2];
private int index = 0;
void add(S shape)
{
shapes[index++] = shape;
if (index < 2)
return;
System.out.println("Before sort: "+this);
sort();
System.out.println("After sort: "+this);
}
private void sort()
{
if (index == 1)
return;
if (shapes[0].compareTo(shapes[1]) > 0)
{
S shape = (S) shapes[0];
shapes[0] = shapes[1];
shapes[1] = shape;
}
}
@Override
public String toString()
{
return shapes[0].toString()+" "+shapes[1].toString();
}
}
class SortedShapesListDemo
{
public static void main(String[] args)
{
SortedShapesList<Circle> ssl = new SortedShapesList<>();
ssl.add(new Circle(100, 200, 300));
ssl.add(new Circle(10, 20, 30));
}
}
Listing 3-53’s Circle class extends Shape and implements the java.lang.Comparable interface, which
is used to specify the natural ordering of Circle objects. The interface’s compareTo() method implements
this ordering by returning a value to reflect the order:
202
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
•
A negative value is returned if the current object should precede the object passed
to compareTo() in some fashion.
•
A zero value is returned if the current and argument objects are the same.
•
A positive value is returned if the current object should succeed the argument
object.
Circle’s overriding compareTo() method compares two Circle objects based on their radii. This
method orders a Circle instance with the smaller radius before a Circle instance with a larger radius.
The SortedShapesList class specifies <S extends Shape&Comparable<S>> as its parameter list. The
actual type argument passed to the S parameter must subclass Shape, and it must also implement the
Comparable interface.
■ Note A type parameter bound that includes the type parameter is known as a recursive type bound. For
example, Comparable<S> in S extends Shape&Comparable<S> is a recursive type bound. Recursive type bounds
are rare and typically show up in conjunction with the Comparable interface, for specifying a type’s natural
ordering.
Circle satisfies both criteria: it subclasses Shape and implements Comparable. As a result, the
compiler does not report an error when it encounters the main() method’s SortedShapesList<Circle>
ssl = new SortedShapesList<>(); statement.
An upper bound offers extra static type checking that guarantees that a parameterized type adheres
to its bounds. This assurance means that the upper bound’s methods can be called safely. For example,
sort() can call Comparable’s compareTo() method.
If you run this application, you will discover the following output, which shows that the two Circle
objects are sorted in ascending order of radius:
Before sort: (100.0, 200.0, 300.0) (10.0, 20.0, 30.0)
After sort: (10.0, 20.0, 30.0) (100.0, 200.0, 300.0)
■ Note Type parameters cannot have lower bounds. Angelika Langer explains the rationale for this restriction in
her “Java Generics FAQs” (see
http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ107).
Type Parameter Scope
A type parameter’s scope (visibility) is its generic type except where masked (hidden). This scope
includes the formal type parameter list of which the type parameter is a member. For example, the scope
of S in SortedShapesList<S extends Shape&Comparable<S>> is all of SortedShapesList and the formal type
parameter list.
203
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
It is possible to mask a type parameter by declaring a same-named type parameter in a nested type’s
formal type parameter list. For example, Listing 3-54 masks an enclosing class’s T type parameter.
Listing 3-54. Masking a type variable
class EnclosingClass<T>
{
static class EnclosedClass<T extends Comparable<T>>
{
}
}
EnclosingClass’s T type parameter is masked by EnclosedClass’s T type parameter, which specifies
an upper bound where only those types that implement the Comparable interface can be passed to
EnclosedClass. Referencing T from within EnclosedClass refers to the bounded T and not the unbounded
T passed to EnclosingClass.
If masking is undesirable, it is best to choose a different name for the type parameter. For example,
you might specify EnclosedClass<U extends Comparable<U>>. Although U is not as meaningful a name as
T, this situation justifies this choice.
The Need for Wildcards
Suppose that you have created a List of String and want to output this list. Because you might create a
List of Employee and other kinds of lists, you want this method to output an arbitrary List of Object. You
end up creating Listing 3-55.
Listing 3-55. Attempting to output a List of Object
import java.util.ArrayList;
import java.util.List;
class OutputList
{
public static void main(String[] args)
{
List<String> ls = new ArrayList<>();
ls.add("first");
ls.add("second");
ls.add("third");
outputList(ls);
}
static void outputList(List<Object> list)
{
for (int i = 0; i < list.size(); i++)
System.out.println(list.get(i));
}
}
Now that you’ve accomplished your objective (or so you think), you compile Listing 3-55 via javac
OutputList.java. Much to your surprise, you receive the following error message:
204
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
OutputList.java:12: error: method outputList in class OutputList cannot be applied to given
types;
outputList(ls);
^
required: List<Object>
found: List<String>
reason: actual argument List<String> cannot be converted to List<Object> by method
invocation conversion
1 error
This error message results from being unaware of the fundamental rule of generic types: for a given
subtype x of type y, and given G as a raw type declaration, G<x> is not a subtype of G<y>.
To understand this rule, you must refresh your understanding of subtype polymorphism (see
Chapter 2). Basically, a subtype is a specialized kind of supertype. For example, Circle is a specialized
kind of Shape and String is a specialized kind of Object. This polymorphic behavior also applies to
related parameterized types with the same type parameters (e.g., List<Object> is a specialized kind of
java.util.Collection<Object>).
However, this polymorphic behavior does not apply to multiple parameterized types that differ only
in regard to one type parameter being a subtype of another type parameter. For example, List<String>
is not a specialized kind of List<Object>. The following example reveals why parameterized types
differing only in type parameters are not polymorphic:
List<String> ls = new ArrayList<>();
List<Object> lo = ls;
lo.add(new Employee());
String s = ls.get(0);
This example will not compile because it violates type safety. If it compiled, a ClassCastException
would be thrown at runtime because of the implicit cast to String on the final line.
The first line instantiates a List of String and the second line upcasts its reference to a List of
Object. The third line adds a new Employee object to the List of Object. The fourth line obtains the
Employee object via get() and attempts to assign it to the List of String reference variable. However,
ClassCastException is thrown because of the implicit cast to String—an Employee is not a String.
■ Note Although you cannot upcast List<String> to List<Object>, you can upcast List<String> to the raw
type List in order to interoperate with legacy code.
The aforementioned error message reveals that List of String is not also List of Object. To call
Listing 3-55’s outputList() method without violating type safety, you can only pass an argument of
List<Object> type, which limits the usefulness of this method.
However, generics offer a solution: the wildcard argument (?), which stands for any type. By
changing outputList()’s parameter type from List<Object> to List<?>, you can call outputList() with a
List of String, a List of Employee, and so on.
205
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Generic Methods
Suppose you need a method to copy a List of any kind of object to another List. Although you might
consider coding a void copyList(List<Object> src, List<Object> dest) method, this method would
have limited usefulness because it could only copy lists whose element type is Object. You couldn’t copy
a List<Employee>, for example.
If you want to pass source and destination lists whose elements are of arbitrary type (but their
element types agree), you need to specify the wildcard character as a placeholder for that type. For
example, you might consider writing the following copyList() class method that accepts collections of
arbitrary-typed objects as its arguments:
static void copyList(List<?> src, List<?> dest)
{
for (int i = 0; i < src.size(); i++)
dest.add(src.get(i));
}
This method’s parameter list is correct, but there is another problem: the compiler outputs the
following error message when it encounters dest.add(src.get(i));:
CopyList.java:18: error: no suitable method found for add(Object)
dest.add(src.get(i));
^
method List.add(int,CAP#1) is not applicable
(actual and formal argument lists differ in length)
method List.add(CAP#1) is not applicable
(actual argument Object cannot be converted to CAP#1 by method invocation conversion)
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error
This error message assumes that copyList() is part of a class named CopyList. Although it appears
to be incomprehensible, the message basically means that the dest.add(src.get(i)) method call
violates type safety. Because ? implies that any type of object can serve as a list’s element type, it’s
possible that the destination list’s element type is incompatible with the source list’s element type.
For example, suppose you create a List of String as the source list and a List of Employee as the
destination list. Attempting to add the source list’s String elements to the destination list, which expects
Employees, violates type safety. If this copy operation were allowed, a ClassCastException instance would
be thrown when trying to obtain the destination list’s elements.
You could avoid this problem by specifying void copyList(List<String> src, List<String> dest),
but this method header limits you to copying only lists of String objects. Alternatively, you might restrict
the wildcard argument, which is demonstrated here:
static void copyList(List<? extends String> src,
List<? super String> dest)
{
for (int i = 0; i < src.size(); i++)
dest.add(src.get(i));
}
This method demonstrates a feature of the wildcard argument: You can supply an upper bound or
(unlike with a type parameter) a lower bound to limit the types that can be passed as actual type
206
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
arguments to the generic type. Specify an upper bound via extends followed by the upper bound type
after the ?, and a lower bound via super followed by the lower bound type after the ?.
You interpret ? extends String to mean that any actual type argument that is String or a subclass
can be passed, and you interpret ? super String to imply that any actual type argument that is String or
a superclass can be passed. Because String cannot be subclassed, this means that you can only pass
source lists of String and destination lists of String or Object.
The problem of copying lists of arbitrary element types to other lists can be solved through the use
of a generic method (a class or instance method with a type-generalized implementation). Generic
methods are syntactically expressed as follows:
<formal_type_parameter_list> return_type identifier(parameter_list)
The formal_type_parameter_list is the same as when specifying a generic type: it consists of type
parameters with optional bounds. A type parameter can appear as the method’s return_type, and type
parameters can appear in the parameter_list. The compiler infers the actual type arguments from the
context in which the method is invoked.
You’ll discover many examples of generic methods in the Collections Framework. For example, its
java.util.Collections class provides a public static <T extends Object & Comparable<? super T>> T
min(Collection<? extends T> coll) method for returning the minimum element in the given
Collection according to the ordering specified by the supplied java.util.Comparator instance.
You can easily convert copyList() into a generic method by prefixing the return type with <T> and
replacing each wildcard with T. The resulting method header is <T> void copyList(List<T> src,
List<T> dest), and Listing 3-56 presents its source code as part of an application that copies a List of
Circle to another List of Circle.
Listing 3-56. Declaring and using a copyList() generic method
import java.util.ArrayList;
import java.util.List;
class Circle
{
private double x, y, radius;
Circle(double x, double y, double radius)
{
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public String toString()
{
return "("+x+", "+y+", "+radius+")";
}
}
class CopyList
{
public static void main(String[] args)
{
List<String> ls = new ArrayList<String>();
ls.add("A");
ls.add("B");
207
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
ls.add("C");
outputList(ls);
List<String> lsCopy = new ArrayList<String>();
copyList(ls, lsCopy);
outputList(lsCopy);
List<Circle> lc = new ArrayList<Circle>();
lc.add(new Circle(10.0, 20.0, 30.0));
lc.add(new Circle (5.0, 4.0, 16.0));
outputList(lc);
List<Circle> lcCopy = new ArrayList<Circle>();
copyList(lc, lcCopy);
outputList(lcCopy);
}
static <T> void copyList(List<T> src, List<T> dest)
{
for (int i = 0; i < src.size(); i++)
dest.add(src.get(i));
}
static void outputList(List<?> list)
{
for (int i = 0; i < list.size(); i++)
System.out.println(list.get(i));
System.out.println();
}
}
The generic method’s type parameters are inferred from the context in which the method was
invoked. For example, the compiler determines that copyList(ls, lsCopy); copies a List of String to
another List of String. Similarly, it determines that copyList(lc, lcCopy); copies a List of Circle to
another List of Circle.
When you run this application, it generates the following output:
A
B
C
A
B
C
(10.0, 20.0, 30.0)
(5.0, 4.0, 16.0)
(10.0, 20.0, 30.0)
(5.0, 4.0, 16.0)
Arrays and Generics
After presenting Listing 3-52’s Queue<E> generic type, I mentioned that I would explain why I specified
elements = (E[]) new Object[size]; instead of the more compact elements = new E[size];
expression. Because of Java’s generics implementation, it isn’t possible to specify array-creation
expressions that involve type parameters (e.g., new E[size] or new List<E>[50]) or actual type
208
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
arguments (e.g., new Queue<String>[15]). If you attempt to do so, the compiler will report a generic
array creation error message.
Before I present an example that demonstrates why allowing array-creation expressions that involve
type parameters or actual type arguments is dangerous, you need to understand reification and
covariance in the context of arrays, and erasure, which is at the heart of how generics are implemented.
Reification is representing the abstract as if it was concrete —for example, making a memory
address available for direct manipulation by other language constructs. Java arrays are reified in that
they’re aware of their element types (an element type is stored internally) and can enforce these types at
runtime. Attempting to store an invalid element in an array causes the JVM to throw an instance of the
java.lang.ArrayStoreException class.
Listing 3-57 teaches you how array manipulation can lead to an ArrayStoreException:
This book was purchased by [email protected]
Listing 3-57. How an ArrayStoreException arises
class Point
{
int x, y;
}
class ColoredPoint extends Point
{
int color;
}
class ReificationDemo
{
public static void main(String[] args)
{
ColoredPoint[] cptArray = new ColoredPoint[1];
Point[] ptArray = cptArray;
ptArray[0] = new Point();
}
}
Listing 3-57’s main() method first instantiates a ColoredPoint array that can store one element. In
contrast to this legal assignment (the types are compatible), specifying ColoredPoint[] cptArray = new
Point[1]; is illegal (and won’t compile) because it would result in a ClassCastException at runtime—the
array knows that the assignment is illegal.
■ Note If it’s not obvious, ColoredPoint[] cptArray = new Point[1]; is illegal because Point instances have
fewer members (only x and y) than ColoredPoint instances (x, y, and color). Attempting to access a Point
instance’s nonexistent color field from its entry in the ColoredPoint array would result in a memory violation
(because no memory has been assigned to color) and ultimately crash the JVM.
The second line (Point[] ptArray = cptArray;) is legal because of covariance (an array of supertype
references is a supertype of an array of subtype references). In this case, an array of Point references is a
supertype of an array of ColoredPoint references. The nonarray analogy is that a subtype is also a
supertype. For example, a Throwable instance is a kind of Object instance.
209
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Covariance is dangerous when abused. For example, the third line (ptArray[0] = new Point();)
results in ArrayStoreException at runtime because a Point instance is not a ColoredPoint instance.
Without this exception, an attempt to access the nonexistent member color crashes the JVM.
Unlike with arrays, a generic type’s type parameters are not reified. They’re not available at runtime
because they’re thrown away after the source code is compiled. This “throwing away of type
parameters” is a result of erasure, which also involves inserting casts to appropriate types when the code
isn’t type correct, and replacing type parameters by their upper bounds (such as Object).
■ Note The compiler performs erasure to let generic code interoperate with legacy (nongeneric) code. It
transforms generic source code into nongeneric runtime code. One consequence of erasure is that you cannot use
the instanceof operator with parameterized types apart from unbounded wildcard types. For example, it’s illegal
to specify List<Employee> le = null; if (le instanceof ArrayList<Employee>) {}. Instead, you must
change the instanceof expression to le instanceof ArrayList<?> (unbounded wildcard) or le instanceof
ArrayList (raw type, which is the preferred use).
Suppose you could specify an array-creation expression involving a type parameter or an actual
type argument. Why would this be bad? For an answer, consider the following example, which should
generate an ArrayStoreException instead of a ClassCastException but doesn’t do so:
List<Employee>[] empListArray = new List<Employee>[1];
List<String> strList = new ArrayList<>();
strList.add("string");
Object[] objArray = empListArray;
objArray[0] = strList;
Employee e = empListArray[0].get(0);
Let’s assume that the first line, which creates a one-element array where this element stores a List
of Employee, is legal. The second line creates a List of String, and the third line stores a single String
object in this list.
The fourth line assigns empListArray to objArray. This assignment is legal because arrays are
covariant and erasure converts List<Employee>[] to the List runtime type, and List subtypes Object.
Because of erasure, the JVM doesn’t throw ArrayStoreException when it encounters objArray[0] =
strList;. After all, we’re assigning a List reference to a List[] array at runtime. However, this exception
would be thrown if generic types were reified because we’d then be assigning a List<String> reference
to a List<Employee>[] array.
However, there is a problem. A List<String> instance has been stored in an array that can only hold
List<Employee> instances. When the compiler-inserted cast operator attempts to cast
empListArray[0].get(0)’s return value ("string") to Employee, the cast operator throws a
ClassCastException object.
Perhaps a future version of Java will reify type parameters, making it possible to specify arraycreation expressions that involve type parameters or actual type arguments.
210
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Varargs and Generics
When you invoke a varargs (variable number of arguments) method whose parameter is declared to be a
parameterized type (as in List<String>), the compiler emits a warning message at the point of call. This
message can be confusing and tends to discourage the use of varargs in third-party APIs.
The warning message is related to heap pollution, which occurs when a variable of a parameterized
type refers to an object that is not of that parameterized type. Heap pollution can only occur when an
application performs an operation that would give rise to an unchecked warning at compile time. (The
Java Language Specification, Third Edition discusses the concept of heap pollution
[http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.12.2.1]).
Unchecked warnings occur in calls to varargs methods whose parameter types are not reifiable. In
other words, the parameter’s type information cannot be completely expressed at runtime because of
erasure.
Varargs are implemented via arrays and arrays are reified. In other words, an array’s element type is
stored internally and used when required for various runtime type checks. However, this stored type
information cannot include information required to represent a parameterized type that is nonreifiable.
This mismatch between a reified array passing nonreified (and nonreifiable) parameterized types to
a method is at the heart of the unchecked warning when the method is called.
In Java 5, calling one of these methods causes a compile-time warning; declaring such a method
doesn’t result in a similar warning. Although the existence of such a varargs method doesn’t cause heap
pollution, its existence contributes to heap pollution by offering an easy way to cause heap pollution to
occur. Furthermore, it influences heap pollution by offering the method to be called. For this reason,
method declarations that contribute to heap pollution deserve a compiler warning, just as this warning
is already present for method calls that cause heap pollution.
The Java 7 compiler outputs warnings in both locations, and Listing 3-58 presents a scenario that
leads to these warnings.
Listing 3-58. Merging a variable number of Lists of Strings
import java.util.ArrayList;
import java.util.List;
class SafeVarargsDemo
{
public static void main(String[] args)
{
List<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
List<String> list2 = new ArrayList<>();
list2.add("C");
list2.add("D");
list2.add("E");
System.out.println(merge(list1, list2)); // Output: [A, B, C, D, E]
}
//@SafeVarargs
static List<String> merge(List<String>... lists)
{
List<String> mergedLists = new ArrayList<>();
for (int i = 0; i < lists.length; i++)
mergedLists.addAll(lists[i]);
211
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
return mergedLists;
}
}
Listing 3-58 declares a merge() method whose purpose is to merge a variable number of List of
String arguments into a single List of String that this method returns. Because erasure converts the
method’s List<String> parameter type to List, there is a potential for this array parameter to refer to a
List that doesn’t store String objects, which is an example of heap pollution. For this reason, the
compiler emits the following warnings when you compile Listing 3-58 via javac -Xlint:unchecked
SafeVarargsDemo.java:
SafeVarargsDemo.java:15: warning: [unchecked] unchecked generic array creation for varargs
parameter of type List<String>[]
System.out.println(merge(list1, list2)); // Output: [A, B, C, D, E]
^
SafeVarargsDemo.java:18: warning: [unchecked] Possible heap pollution from parameterized
vararg type List<String>
static List<String> merge(List<String>... lists)
^
2 warnings
The merge() method does nothing that can lead to a ClassCastException. Therefore, these warning
messages are spurious and can be ignored by annotating merge() with @SafeVarargs to assert that the
body of the merge() method does not perform potentially unsafe operations on its varargs parameter.
Uncomment //@SafeVarargs in Listing 3-58 and recompile. You’ll discover that these warning
messages disappear.
■ Note Various standard class library methods, such as the Arrays class’s public static <T> List<T>
asList(T... a) method, are annotated @SafeVarargs because they don’t throw ClassCastExceptions when
their varargs array arguments are created by the compiler using proper type inference.
Enums
An enumerated type is a type that specifies a named sequence of related constants as its legal values. The
months in a calendar, the coins in a currency, and the days of the week are examples of enumerated
types.
Java developers have traditionally used sets of named integer constants to represent enumerated
types. Because this form of representation has proven to be problematic, Java 5 introduced the enum
alternative.
This section introduces you to enums. After discussing the problems with traditional enumerated
types, the section presents the enum alternative. It then introduces you to the Enum class, from which
enums originate.
212
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The Trouble with Traditional Enumerated Types
Listing 3-59 declares a Coin enumerated type whose set of constants identifies different kinds of coins in
a currency.
Listing 3-59. An enumerated type identifying coins
class Coin
{
final static
final static
final static
final static
}
int
int
int
int
PENNY = 0;
NICKEL = 1;
DIME = 2;
QUARTER = 3;
Listing 3-60 declares a Weekday enumerated type whose set of constants identifies the days of the
week.
Listing 3-60. An enumerated type identifying weekdays
class Weekday
{
final static
final static
final static
final static
final static
final static
final static
}
int
int
int
int
int
int
int
SUNDAY = 0;
MONDAY = 1;
TUESDAY = 2;
WEDNESDAY = 3;
THURSDAY = 4;
FRIDAY = 5;
SATURDAY = 6;
Listing 3-59’s and 3-60’s approach to representing an enumerated type is problematic, where the
biggest problem is the lack of compile-time type safety. For example, you can pass a coin to a method
that requires a weekday and the compiler will not complain.
You can also compare coins to weekdays, as in Coin.NICKEL == Weekday.MONDAY, and specify even
more meaningless expressions, such as Coin.DIME+Weekday.FRIDAY-1/Coin.QUARTER. The compiler does
not complain because it only sees ints.
Applications that depend upon enumerated types are brittle. Because the type’s constants are
compiled into an application’s classfiles, changing a constant’s int value requires you to recompile
dependent applications or risk them behaving erratically.
Another problem with enumerated types is that int constants cannot be translated into meaningful
string descriptions. For example, what does 4 mean when debugging a faulty application? Being able to
see THURSDAY instead of 4 would be more helpful.
■ Note You could circumvent the previous problem by using String constants. For example, you might specify
final static String THURSDAY = "THURSDAY";. Although the constant value is more meaningful, Stringbased constants can impact performance because you cannot use == to efficiently compare just any old strings (as
213
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
you will discover in Chapter 4). Other problems related to String-based constants include hard-coding the
constant’s value ("THURSDAY") instead of the constant’s name (THURSDAY) into source code, which makes it
difficult to change the constant’s value at a later time; and misspelling a hard-coded constant ("THURZDAY"), which
compiles correctly but is problematic at runtime.
The Enum Alternative
Java 5 introduced enums as a better alternative to traditional enumerated types. An enum is an
enumerated type that is expressed via reserved word enum. The following example uses enum to declare
Listing 3-59’s and 3-60’s enumerated types:
enum Coin { PENNY, NICKEL, DIME, QUARTER }
enum Weekday { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
Despite their similarity to the int-based enumerated types found in C++ and other languages, this
example’s enums are classes. Each constant is a public static final field that represents an instance of
its enum class.
Because constants are final, and because you cannot call an enum’s constructors to create more
constants, you can use == to compare constants efficiently and (unlike string constant comparisons)
safely. For example, you can specify c == Coin.NICKEL.
Enums promote compile-time type safety by preventing you from comparing constants in different
enums. For example, the compiler will report an error when it encounters Coin.PENNY ==
Weekday.SUNDAY.
The compiler also frowns upon passing a constant of the wrong enum kind to a method. For
example, you cannot pass Weekday.FRIDAY to a method whose parameter type is Coin.
Applications depending upon enums are not brittle because the enum’s constants are not compiled
into an application’s classfiles. Also, the enum provides a toString() method for returning a more useful
description of a constant’s value.
Because enums are so useful, Java 5 enhanced the switch statement to support them. Listing 3-61
demonstrates this statement switching on one of the constants in the previous example’s Coin enum.
Listing 3-61. Using the switch statement with an enum
class EnhancedSwitch
{
enum Coin { PENNY, NICKEL, DIME, QUARTER }
public static void main(String[] args)
{
Coin coin = Coin.NICKEL;
switch (coin)
{
case PENNY : System.out.println("1 cent"); break;
case NICKEL : System.out.println("5 cents"); break;
case DIME
: System.out.println("10 cents"); break;
case QUARTER: System.out.println("25 cents"); break;
default
: assert false;
}
}
214
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
}
Listing 3-61 demonstrates switching on an enum’s constants. This enhanced statement only allows
you to specify the name of a constant as a case label. If you prefix the name with the enum, as in case
Coin.DIME, the compiler reports an error.
Enhancing an Enum
You can add fields, constructors, and methods to an enum – you can even have the enum implement
interfaces. For example, Listing 3-62 adds a field, a constructor, and two methods to Coin to associate a
denomination value with a Coin constant (such as 1 for penny and 5 for nickel) and convert pennies to
the denomination.
Listing 3-62. Enhancing the Coin enum
enum Coin
{
PENNY(1),
NICKEL(5),
DIME(10),
QUARTER(25);
private final int denomValue;
Coin(int denomValue)
{
this.denomValue = denomValue;
}
int denomValue()
{
return denomValue;
}
int toDenomination(int numPennies)
{
return numPennies/denomValue;
}
}
Listing 3-62’s constructor accepts a denomination value, which it assigns to a private blank final
field named denomValue—all fields should be declared final because constants are immutable. Notice
that this value is passed to each constant during its creation (PENNY(1), for example).
■ Caution When the comma-separated list of constants is followed by anything other than an enum’s closing
brace, you must terminate the list with a semicolon or the compiler will report an error.
Furthermore, this listing’s denomValue() method returns denomValue, and its toDenomination()
method returns the number of coins of that denomination that are contained within the number of
pennies passed to this method as its argument. For example, 3 nickels are contained in 16 pennies.
215
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-63 shows you how to use the enhanced Coin enum.
Listing 3-63. Exercising the enhanced Coin enum
class Coins
{
public static void main(String[] args)
{
if (args.length == 1)
{
int numPennies = Integer.parseInt(args[0]);
System.out.println(numPennies+" pennies is equivalent to:");
int numQuarters = Coin.QUARTER.toDenomination(numPennies);
System.out.println(numQuarters+" "+Coin.QUARTER.toString()+
(numQuarters != 1 ? "s," : ","));
numPennies -= numQuarters*Coin.QUARTER.denomValue();
int numDimes = Coin.DIME.toDenomination(numPennies);
System.out.println(numDimes+" "+Coin.DIME.toString()+
(numDimes != 1 ? "s, " : ","));
numPennies -= numDimes*Coin.DIME.denomValue();
int numNickels = Coin.NICKEL.toDenomination(numPennies);
System.out.println(numNickels+" "+Coin.NICKEL.toString()+
(numNickels != 1 ? "s, " : ", and"));
numPennies -= numNickels*Coin.NICKEL.denomValue();
System.out.println(numPennies+" "+Coin.PENNY.toString()+
(numPennies != 1 ? "s" : ""));
}
System.out.println();
System.out.println("Denomination values:");
for (int i = 0; i < Coin.values().length; i++)
System.out.println(Coin.values()[i].denomValue());
}
}
Listing 3-63 describes an application that converts its solitary “pennies” command-line argument to
an equivalent amount expressed in quarters, dimes, nickels, and pennies. In addition to calling a Coin
constant’s denomValue() and toDenomValue() methods, the application calls toString() to output a string
representation of the coin.
Another called enum method is values(). This method returns an array of all Coin constants that are
declared in the Coin enum (value()’s return type, in this example, is Coin[]). This array is useful when
you need to iterate over these constants. For example, Listing 3-63 calls this method to output each
coin’s denomination.
When you run this application with 119 as its command-line argument (java Coins 119), it
generates the following output:
119 pennies is equivalent to:
4 QUARTERs,
1 DIME,
1 NICKEL, and
4 PENNYs
Denomination values:
216
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
1
5
10
25
The output shows that toString() returns a constant’s name. It is sometimes useful to override this
method to return a more meaningful value. For example, a method that extracts tokens (named
character sequences) from a string might use a Token enum to list token names and, via an overriding
toString() method, values – see Listing 3-64.
Listing 3-64. Overriding toString() to return a Token constant’s value
enum Token
{
IDENTIFIER("ID"),
INTEGER("INT"),
LPAREN("("),
RPAREN(")"),
COMMA(",");
private final String tokValue;
Token(String tokValue)
{
this.tokValue = tokValue;
}
@Override
public String toString()
{
return tokValue;
}
public static void main(String[] args)
{
System.out.println("Token values:");
for (int i = 0; i < Token.values().length; i++)
System.out.println(Token.values()[i].name()+" = "+
Token.values()[i]);
}
}
Listing 3-64’s main() method calls values() to return the array of Token constants. For each
constant, it calls the constant’s name() method to return the constant’s name, and implicitly calls
toString() to return the constant’s value. If you were to run this application, you would observe the
following output:
Token values:
IDENTIFIER = ID
INTEGER = INT
LPAREN = (
RPAREN = )
COMMA = ,
Another way to enhance an enum is to assign a different behavior to each constant. You can
accomplish this task by introducing an abstract method into the enum and overriding this method in an
anonymous subclass of the constant. Listing 3-65’s TempConversion enum demonstrates this technique.
217
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Listing 3-65. Using anonymous subclasses to vary the behaviors of enum constants
enum TempConversion
{
C2F("Celsius to Fahrenheit")
{
@Override
double convert(double value)
{
return value*9.0/5.0+32.0;
}
},
F2C("Fahrenheit to Celsius")
{
@Override
double convert(double value)
{
return (value-32.0)*5.0/9.0;
}
};
TempConversion(String desc)
{
this.desc = desc;
}
private String desc;
@Override
public String toString()
{
return desc;
}
abstract double convert(double value);
public static void main(String[] args)
{
System.out.println(C2F+" for 100.0 degrees = "+C2F.convert(100.0));
System.out.println(F2C+" for 98.6 degrees = "+F2C.convert(98.6));
}
}
When you run this application, it generates the following output:
Celsius to Fahrenheit for 100.0 degrees = 212.0
Fahrenheit to Celsius for 98.6 degrees = 37.0
The Enum Class
The compiler regards enum as syntactic sugar. When it encounters an enum type declaration (enum Coin
{}), it generates a class whose name (Coin) is specified by the declaration, and which also subclasses the
abstract Enum class (in the java.lang package), the common base class of all Java-based enumeration
types.
If you examine Enum’s Java documentation, you will discover that it overrides Object’s clone(),
equals(), finalize(), hashCode(), and toString() methods:
218
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
•
clone() is overridden to prevent constants from being cloned so that there is
never more than one copy of a constant; otherwise, constants could not be
compared via ==.
•
equals() is overridden to compare constants via their references—constants with
the same identities (==) must have the same contents (equals()), and different
identities imply different contents.
•
finalize() is overridden to ensure that constants cannot be finalized.
•
hashCode() is overridden because equals() is overridden.
•
toString() is overridden to return the constant’s name.
This book was purchased by [email protected]
Except for toString(), all of the overriding methods are declared final so that they cannot be
overridden in a subclass.
Enum also provides its own methods. These methods include the final compareTo(), (Enum
implements Comparable), getDeclaringClass(), name(), and ordinal() methods:
•
compareTo() compares the current constant with the constant passed as an
argument to see which constant precedes the other constant in the enum, and
returns a value indicating their order. This method makes it possible to sort an
array of unsorted constants.
•
getDeclaringClass() returns the Class object corresponding to the current
constant’s enum. For example, the Class object for Coin is returned when calling
Coin.PENNY.getDeclaringClass() for enum Coin { PENNY, NICKEL, DIME, QUARTER
}. Also, TempConversion is returned when calling
TempConversion.C2F.getDeclaringClass() for Listing 3-65’s TempConversion enum.
The compareTo() method uses Class’s getClass() method and Enum’s
getDeclaringClass() method to ensure that only constants belonging to the same
enum are compared. Otherwise, a ClassCastException is thrown. (I will discuss
Class in Chapter 4.)
•
name() returns the constant’s name. Unless overridden to return something more
descriptive, toString() also returns the constant’s name.
•
ordinal() returns a zero-based ordinal, an integer that identifies the position of
the constant within the enum type. compareTo() compares ordinals.
Enum also provides the public static <T extends Enum<T>> T valueOf(Class<T> enumType, String
name) method for returning the enum constant from the specified enum with the specified name:
•
enumType identifies the Class object of the enum from which to return a constant.
•
name identifies the name of the constant to return.
For example, Coin penny = Enum.valueOf(Coin.class, "PENNY"); assigns the Coin constant whose
name is PENNY to penny.
You will not discover a values() method in Enum’s Java documentation because the compiler
synthesizes (manufactures) this method while generating the class.
219
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
Extending the Enum Class
Enum’s generic type is Enum<E extends Enum<E>>. Although the formal type parameter list looks ghastly, it
is not that hard to understand. But first, take a look at Listing 3-66.
Listing 3-66. The Coin class as it appears from the perspective of its classfile
final class Coin extends Enum<Coin>
{
public static final Coin PENNY = new Coin("PENNY", 0);
public static final Coin NICKEL = new Coin("NICKEL", 1);
public static final Coin DIME = new Coin("DIME", 2);
public static final Coin QUARTER = new Coin("QUARTER", 3);
private static final Coin[] $VALUES = { PENNY, NICKEL, DIME, QUARTER };
public static Coin[] values()
{
return Coin.$VALUES.clone();
}
public static Coin valueOf(String name)
{
return Enum.valueOf(Coin.class, "Coin");
}
private Coin(String name, int ordinal)
{
super(name, ordinal);
}
}
Behind the scenes, the compiler converts enum Coin { PENNY, NICKEL, DIME, QUARTER } into a class
declaration that is similar to Listing 3-66.
The following rules show you how to interpret Enum<E extends Enum<E>> in the context of Coin
extends Enum<Coin>:
•
Any subclass of Enum must supply an actual type argument to Enum. For example,
Coin’s header specifies Enum<Coin>.
•
The actual type argument must be a subclass of Enum. For example, Coin is a
subclass of Enum.
•
A subclass of Enum (such as Coin) must follow the idiom that it supplies its own
name (Coin) as an actual type argument.
The third rule allows Enum to declare methods—compareTo(), getDeclaringClass(), and valueOf()—
whose parameter and/or return types are specified in terms of the subclass (Coin), and not in terms of
Enum. The rationale for doing this is to avoid having to specify casts. For example, you do not need to cast
valueOf()’s return value to Coin in Coin penny = Enum.valueOf(Coin.class, "PENNY");.
■ Note You cannot compile Listing 3-66 because the compiler will not compile any class that extends Enum. It will
also complain about super(name, ordinal);.
220
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
EXERCISES
The following exercises are designed to test your understanding of nested types, packages, static imports,
exceptions, assertions, annotations, generics, and enums:
1.
A 2D graphics package supports two-dimensional drawing and transformations
(rotation, scaling, translation, and so on). These transformations require a 3-by-3
matrix (a table). Declare a G2D class that encloses a private Matrix nonstatic
member class. In addition to declaring a Matrix(int nrows, int ncols)
constructor, Matrix declares a void dump() method that outputs the matrix
values to standard output in a tabular format. Instantiate Matrix within G2D’s
noargument constructor, and initialize the Matrix instance to the identity matrix (a
matrix where all entries are 0 except for those on the upper-left to lower-right
diagonal, which are 1. Then invoke this instance’s dump() method from the
constructor. Include a main() method to test G2D.
2.
Extend the logging package to support a null device in which messages are
thrown away.
3.
Continuing from Exercise 1, introduce the following matrix-multiplication method
into Matrix:
Matrix multiply(Matrix m)
{
Matrix result = new Matrix(matrix.length, matrix[0].length);
for (int i = 0; i < matrix.length; i++)
for (int j = 0; j < m.matrix[0].length; j++)
for (int k = 0; k < m.matrix.length; k++)
result.matrix[i][j] = result.matrix[i][j]+
matrix[i][k]*m.matrix[k][j];
return result;
}
Next, declare a void rotate(double angle) method in G2D. This method’s first
task is to negate its angle argument (to ensure counterclockwise rotation), which
specifies a rotation angle in degrees. It then creates a 3-by-3 rotation Matrix and
initializes the following (row, column) entries: (0, 0) to the cosine of the angle, (1,
0) to the sine of the angle, (0, 1) to the negative of the angle’s sine, (1, 1) to the
cosine of the angle, and (2, 2) to 1.0. Statically import all necessary Math class
methods. Finally, rotate() multiplies the identity matrix created in G2D’s
constructor by this rotation matrix, and invokes dump() to dump the result. Test
rotate() from the main() method by executing G2D g2d = new G2D();
g2d.rotate(45);. You should observe the following output:
1.0 0.0 0.0
0.0 1.0 0.0
0.0 0.0 1.0
0.7071067811865476 0.7071067811865475 0.0
221
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
-0.7071067811865475 0.7071067811865476 0.0
0.0 0.0 1.0
4.
Modify the logging package so that Logger’s connect() method throws
CannotConnectException when it cannot connect to its logging destination, and
the other two methods each throw NotConnectedException when connect()
was not called or when it threw CannotConnectException. Modify TestLogger to
respond appropriately to thrown CannotConnectException and
NotConnectedException objects.
5.
Continuing from Exercise 3, use an assertion to verify the class invariant that the
transformation matrix is initialized to the identity matrix before G2D’s constructor
ends.
6.
Declare a ToDo marker annotation type that annotates only type elements, and
that also uses the default retention policy.
7.
Rewrite the StubFinder application to work with Listing 3-43’s Stub annotation
type (with appropriate @Target and @Retention annotations) and Listing 3-44’s
Deck class.
8.
Implement a Stack<E> generic type in a manner that is similar to Listing 3-52’s
Queue class. Stack must declare push(), pop(), and isEmpty() methods (it could
also declare an isFull() method but that method is not necessary in this
exercise), push() must throw a StackFullException instance when the stack is
full, and pop() must throw a StackEmptyException instance when the stack is
empty. (You must create your own StackFullException and
StackEmptyException helper classes because they are not provided for you in
Java’s standard class library.) Declare a similar main() method, and insert two
assertions into this method that validate your assumptions about the stack being
empty immediately after being created and immediately after popping the last
element.
9.
Declare a Compass enum with NORTH, SOUTH, EAST, and WEST members. Declare a
UseCompass class whose main() method randomly selects one of these constants
and then switches on that constant. Each of the switch statement’s cases should
output a message such as heading north.
Summary
Java supports advanced language features related to nested types, packages, static imports, exceptions,
assertions, annotations, generics, and enums.
Classes that are declared outside of any class are known as top-level classes. Java also supports
nested classes, which are classes declared as members of other classes or scopes, and which help you
implement top-level class architecture.
There are four kinds of nested classes: static member classes, nonstatic member classes,
anonymous classes, and local classes. The latter three categories are known as inner classes.
Java supports the partitioning of top-level types into multiple namespaces, to better organize these
types and to also prevent name conflicts. Java uses packages to accomplish these tasks.
222
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
The package statement identifies the package in which a source file’s types are located. The import
statement imports types from a package by telling the compiler where to look for unqualified type
names during compilation.
An exception is a divergence from an application’s normal behavior. Although it can be represented
by an error code or object, Java uses objects because error codes are meaningless and cannot contain
information about what led to the exception.
Java provides a hierarchy of classes that represent different kinds of exceptions. These classes are
rooted in Throwable. Moving down the throwable hierarchy, you encounter the Exception and Error
classes, which represent nonerror exceptions and errors.
Exception and its subclasses, except for RuntimeException (and its subclasses), describe checked
exceptions. They are checked because the compiler checks the code to ensure that an exception is
handled where thrown or identified as being handled elsewhere.
RuntimeException and its subclasses describe unchecked exceptions. You do not have to handle
these exceptions because they represent coding mistakes (fix the mistakes). Although the names of their
classes can appear in throws clauses, doing so adds clutter.
The throw statement throws an exception to the JVM, which searches for an appropriate handler. If
the exception is checked, its name must appear in the method’s throws clause, unless the name of the
exception’s superclass is listed in this clause.
A method handles one or more exceptions by specifying a try statement and appropriate catch
blocks. A finally block can be included to execute cleanup code whether or not an exception is thrown,
and before a thrown exception leaves the method.
An assertion is a statement that lets you express an assumption of application correctness via a
Boolean expression. If this expression evaluates to true, execution continues with the next statement;
otherwise, an error that identifies the cause of failure is thrown.
There are many situations where assertions should be used. These situations organize into internal
invariant, control-flow invariant, and design-by-contract categories. An invariant is something that does
not change.
Although there are many situations where assertions should be used, there also are situations where
they should be avoided. For example, you should not use assertions to check the arguments that are
passed to public methods.
The compiler records assertions in the classfile. However, assertions are disabled at runtime
because they can affect performance. You must enable the classfile’s assertions before you can test
assumptions about the behaviors of your classes.
Annotations are instances of annotation types and associate metadata with application elements.
They are expressed in source code by prefixing their type names with @ symbols. For example, @Readonly
is an annotation and Readonly is its type.
Java supplies a wide variety of annotation types, including the compiler-oriented Override,
Deprecated, SuppressWarnings, and SafeVarargs types. However, you can also declare your own
annotation types by using the @interface syntax.
Annotation types can be annotated with meta-annotations that identify the application elements
they can target (such as constructors, methods, or fields), their retention policies, and other
characteristics.
Annotations whose types are assigned a runtime retention policy via @Retention annotations can be
processed at runtime using custom applications or Java’s apt tool, whose functionality has been
integrated into the compiler starting with Java 6.
Java 5 introduced generics, language features for declaring and using type-agnostic classes and
interfaces. When working with Java’s Collections Framework, these features help you avoid
ClassCastExceptions.
223
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
A generic type is a class or interface that introduces a family of parameterized types by declaring a
formal type parameter list. The type name that replaces a type parameter is known as an actual type
argument.
There are five kinds of actual type arguments: concrete type, concrete parameterized type, array
type, type parameter, and wildcard. Furthermore, a generic type also identifies a raw type, which is a
generic type without its type parameters.
Many type parameters are unbounded in that they can accept any actual type argument. To restrict
actual type arguments, you can specify an upper bound, a type that serves as an upper limit on the types
that can be chosen as actual type arguments. The upper bound is specified via reserved word extends
followed by a type name. However, lower bounds are not supported.
A type parameter’s scope is its generic type except where masked. This scope includes the formal
type parameter list of which the type parameter is a member.
To preserve type safety, you are not allowed to violate the fundamental rule of generic types: for a
given subtype x of type y, and given G as a raw type declaration, G<x> is not a subtype of G<y>. In other
words, multiple parameterized types that differ only in regard to one type parameter being a subtype of
another type parameter are not polymorphic. For example, List<String> is not a specialized kind of
List<Object>.
This restriction can be ameliorated without violating type safety by using wildcards. For example,
where a void output(List<Object> list) method can only output a List that contains Objects (to
adhere to the aforementioned rule), a void output(List<?> list) method can output a List of arbitrary
objects.
Wildcards alone cannot solve the problem where you want to copy one List to another. The
solution is to use a generic method, a static or non-static method with a type-generalized
implementation. For example, a <T> void copyList(List<T> src, List<T> dest) method can copy a
source List of arbitrary objects (whose type is specified by T) to another List of arbitrary objects (having
the same type). The compiler infers the actual type arguments from the context in which the method is
invoked.
Reification is representing the abstract as if it was concrete -- for example, making a memory
address available for direct manipulation by other language constructs. Java arrays are reified in that
they’re aware of their element types (an element type is stored internally) and can enforce these types at
runtime. Attempting to store an invalid element in an array causes the JVM to throw an instance of the
ArrayStoreException class.
Unlike with arrays, a generic type’s type parameters are not reified. They’re not available at runtime
because they’re thrown away after the source code is compiled. This “throwing away of type
parameters” is a result of erasure, which also involves inserting casts to appropriate types when the code
isn’t type correct, and replacing type parameters by their upper bounds (such as Object).
When you invoke a varargs method whose parameter is declared to be a parameterized type (as in
List<String>), the compiler emits a warning message at the point of call. This message can be confusing
and tends to discourage the use of varargs in third-party APIs.
The warning message is related to heap pollution, which occurs when a variable of a parameterized
type refers to an object that is not of that parameterized type.
If a varargs method is declared such that this warning message occurs, and if the varargs method
doesn’t perform a potentially unsafe operation on its varargs parameter, you can annotate the method
@SafeVarargs, and eliminate the warning message.
An enumerated type is a type that specifies a named sequence of related constants as its legal
values. Java developers have traditionally used sets of named integer constants to represent enumerated
types.
Because sets of named integer constants have proven to be problematic, Java 5 introduced the
enum alternative. An enum is an enumerated type that is expressed via reserved word enum.
224
CHAPTER 3  EXPLORING ADVANCED LANGUAGE FEATURES
You can add fields, constructors, and methods to an enum—you can even have the enum
implement interfaces. Also, you can override toString() to provide a more useful description of a
constant’s value, and subclass constants to assign different behaviors.
The compiler regards enum as syntactic sugar for a class that subclasses Enum. This abstract class
overrides various Object methods to provide default behaviors (usually for safety reasons), and provides
additional methods for various purposes.
This chapter largely completes our tour of the Java language. However, there are a few more
advanced language features to explore. You will encounter a couple of these minor features in Chapter 4,
which begins a multichapter exploration of additional types that are located in Java’s standard class
library.
225
CHAPTER 4
Touring Language APIs
Java’s standard class library provides various language-oriented APIs. Most of these APIs reside in the
java.lang package and its subpackages, although a few APIs reside in java.math. Chapter 4 first
introduces you to the java.lang/subpackage Math and StrictMath, Package, Primitive Type Wrapper
Class, Reference, Reflection, String, StringBuffer and StringBuilder, System, and Threading APIs. This
chapter then introduces you to java.math’s BigDecimal and BigInteger APIs.
Math and StrictMath
The java.lang.Math class declares double constants E and PI that represent the natural logarithm base
value (2.71828...) and the ratio of a circle’s circumference to its diameter (3.14159...). E is initialized to
2.718281828459045 and PI is initialized to 3.141592653589793. Math also declares assorted class methods
to perform various math operations. Table 4-1 describes many of these methods.
Table 4-1. Math Methods
Method
Description
double abs(double d)
Return the absolute value of d. There are four special cases:
abs(-0.0) = +0.0, abs(+infinity) = +infinity, abs(infinity) = +infinity, and abs(NaN) = NaN.
float abs(float f)
Return the absolute value of f. There are four special cases:
abs(-0.0) = +0.0, abs(+infinity) = +infinity, abs(infinity) = +infinity, and abs(NaN) = NaN.
int abs(int i)
Return the absolute value of i. There is one special case: the
absolute value of Integer.MIN_VALUE is Integer.MIN_VALUE.
long abs(long l)
Return the absolute value of l. There is one special case: the
absolute value of Long.MIN_VALUE is Long.MIN_VALUE.
double acos(double d)
Return angle d’s arc cosine within the range 0 through PI.
There are three special cases: acos(anything > 1) = NaN,
acos(anything < -1) = NaN, and acos(NaN) = NaN.
227
CHAPTER 4  TOURING LANGUAGE APIS
228
double asin(double d)
Return angle d’s arc sine within the range -PI/2 through PI/2.
There are three special cases: asin(anything > 1) = NaN,
asin(anything < -1) = NaN, and asin(NaN) = NaN.
double atan(double d)
Return angle d’s arc tangent within the range -PI/2 through
PI/2. There are five special cases: atan(+0.0) = +0.0, atan(0.0) = -0.0, atan(+infinity) = +PI/2, atan(-infinity) =
-PI/2, and atan(NaN) = NaN.
double ceil(double d)
Return the smallest value (closest to negative infinity) that is
not less than d and is equal to an integer. There are six
special cases: ceil(+0.0) = +0.0, ceil(-0.0) = -0.0,
ceil(anything > -1.0 and < 0.0) = -0.0, ceil(+infinity)
= +infinity, ceil(-infinity) = -infinity, and ceil(NaN) =
NaN.
double cos(double d)
Return the cosine of angle d (expressed in radians). There are
three special cases: cos(+infinity) = NaN, cos(-infinity) =
NaN, and cos(NaN) = NaN.
double exp(double d)
Return Euler’s number e to the power d. There are three
special cases: exp(+infinity) = +infinity, exp(-infinity)
= +0.0, and exp(NaN) = NaN.
double floor(double d)
Return the largest value (closest to positive infinity) that is
not greater than d and is equal to an integer. There are five
special cases: floor(+0.0) = +0.0, floor(-0.0) = -0.0,
floor(+infinity) = +infinity, floor(-infinity) = infinity, and floor(NaN) = NaN.
double log(double d)
Return the natural logarithm (base e) of d. There are six
special cases: log(+0.0) = -infinity, log(-0.0) = infinity, log(anything < 0) = NaN, log(+infinity) =
+infinity, log(-infinity) = NaN, and log(NaN) = NaN.
double log10(double d)
Return the base 10 logarithm of d. There are six special cases:
log10(+0.0) = -infinity, log10(-0.0) = -infinity,
log10(anything < 0) = NaN, log10(+infinity) = +infinity,
log10(-infinity) = NaN, and log10(NaN) = NaN.
double max(double d1, double
d2)
Return the most positive (closest to positive infinity) of d1
and d2. There are four special cases: max(NaN, anything) =
NaN, max(anything, NaN) = NaN, max(+0.0, -0.0) = +0.0,
and max(-0.0, +0.0) = +0.0.
float max(float f1, float
f2)
Return the most positive (closest to positive infinity) of f1
and f2. There are four special cases: max(NaN, anything) =
NaN, max(anything, NaN) = NaN, max(+0.0, -0.0) = +0.0,
CHAPTER 4  TOURING LANGUAGE APIS
and max(-0.0, +0.0) = +0.0.
int max(int i1, int i2)
Return the most positive (closest to positive infinity) of i1
and i2.
long max(long l1, long l2)
Return the most positive (closest to positive infinity) of l1
and l2.
double min(double d1, double
d2)
Return the most negative (closest to negative infinity) of d1
and d2. There are four special cases: min(NaN, anything) =
NaN, min(anything, NaN) = NaN, min(+0.0, -0.0) = -0.0,
and min(-0.0, +0.0) = -0.0.
float min(float f1, float
f2)
Return the most negative (closest to negative infinity) of f1
and f2. There are four special cases: min(NaN, anything) =
NaN, min(anything, NaN) = NaN, min(+0.0, -0.0) = -0.0,
and min(-0.0, +0.0) = -0.0.
int min(int i1, int i2)
Return the most negative (closest to negative infinity) of i1
and i2.
long min(long l1, long l2)
Return the most negative (closest to negative infinity) of l1
and l2.
double random()
Return a pseudorandom number between 0.0 (inclusive)
and 1.0 (exclusive).
long round(double d)
Return the result of rounding d to a long integer. The result is
equivalent to (long) Math.floor(d+0.5). There are seven
special cases: round(+0.0) = +0.0, round(-0.0) = +0.0,
round(anything > Long.MAX_VALUE) = Long.MAX_VALUE,
round(anything < Long.MIN_VALUE) = Long.MIN_VALUE,
round(+infinity) = Long.MAX_VALUE, round(-infinity) =
Long.MIN_VALUE, and round(NaN) = +0.0.
int round(float f)
Return the result of rounding f to an integer. The result is
equivalent to (int) Math.floor(f+0.5). There are seven
special cases: round(+0.0) = +0.0, round(-0.0) = +0.0,
round(anything > Integer.MAX_VALUE) =
Integer.MAX_VALUE, round(anything < Integer.MIN_VALUE)
= Integer.MIN_VALUE, round(+infinity) =
Integer.MAX_VALUE, round(-infinity) = Integer.MIN_VALUE,
and round(NaN) = +0.0.
double signum(double d)
Return the sign of d as -1.0 (d less than 0.0), 0.0 (d equals 0.0),
and 1.0 (d greater than 0.0). There are five special cases:
signum(+0.0) = +0.0, signum(-0.0) = -0.0,
signum(+infinity) = +1.0, signum(-infinity) = -1.0, and
229
CHAPTER 4  TOURING LANGUAGE APIS
This book was purchased by [email protected]
signum(NaN) = NaN.
float signum(float f)
Return the sign of f as -1.0 (f less than 0.0), 0.0 (f equals 0.0),
and 1.0 (f greater than 0.0). There are five special cases:
signum(+0.0) = +0.0, signum(-0.0) = -0.0,
signum(+infinity) = +1.0, signum(-infinity) = -1.0, and
signum(NaN) = NaN.
double sin(double d)
Return the sine of angle d (expressed in radians). There are
five special cases: sin(+0.0) = +0.0, sin(-0.0) = -0.0,
sin(+infinity) = NaN, sin(-infinity) = NaN, and sin(NaN)
= NaN.
double sqrt(double d)
Return the square root of d. There are five special cases:
sqrt(+0.0) = +0.0, sqrt(-0.0) = -0.0, sqrt(anything < 0)
= NaN, sqrt(+infinity) = +infinity, and sqrt(NaN) = NaN.
double tan(double d)
Return the tangent of angle d (expressed in radians). There
are five special cases: tan(+0.0) = +0.0, tan(-0.0) = -0.0,
tan(+infinity) = NaN, tan(-infinity) = NaN, and tan(NaN)
= NaN.
double toDegrees(double
angrad)
Convert angle angrad from radians to degrees via expression
angrad*180/PI. There are five special cases: toDegrees(+0.0)
= +0.0, toDegrees(-0.0) = -0.0, toDegrees(+infinity) =
+infinity, toDegrees(-infinity) = -infinity, and
toDegrees(NaN) = NaN.
double toRadians(angdeg)
Convert angle angdeg from degrees to radians via expression
angdeg/180*PI. There are five special cases: toRadians(+0.0)
= +0.0, toRadians(-0.0) = -0.0, toRadians(+infinity) =
+infinity, toRadians(-infinity) = -infinity, and
toRadians(NaN) = NaN.
Table 4-1 reveals a wide variety of useful math-oriented methods. For example, each abs() method
returns its argument’s absolute value (number without regard for sign).
abs(double) and abs(float) are useful for comparing double precision floating-point and floatingpoint values safely. For example, 0.3 == 0.1+0.1+0.1 evaluates to false because 0.1 has no exact
representation. However, you can compare these expressions with abs() and a tolerance value, which
indicates an acceptable range of error. For example, Math.abs(0.3-(0.1+0.1+0.1)) < 0.1 returns true
because the absolute difference between 0.3 and 0.1+0.1+0.1 is less than a 0.1 tolerance value.
Previous chapters demonstrated other Math methods. For example, Chapter 2 demonstrated Math’s
random(), sin(), cos(), and toRadians() methods.
As Chapter 3’s Lotto649 application revealed, random() (which returns a number that appears to be
randomly chosen but is actually chosen by a predictable math calculation, and hence is pseudorandom)
is useful in simulations (as well as in games and wherever an element of chance is needed). However, its
double precision floating-point range of 0.0 through (almost) 1.0 isn’t practical. To make random() more
useful, its return value must be transformed into a more useful range, perhaps integer values 0 through
230
CHAPTER 4  TOURING LANGUAGE APIS
49, or maybe -100 through 100. You will find the following rnd() method useful for making these
transformations:
static int rnd(int limit)
{
return (int) (Math.random()*limit);
}
rnd() transforms random()’s 0.0 to (almost) 1.0 double precision floating-point range to a 0 through
limit-1 integer range. For example, rnd(50) returns an integer ranging from 0 through 49. Also, 100+rnd(201) transforms 0.0 to (almost) 1.0 into -100 through 100 by adding a suitable offset and passing
an appropriate limit value.
■ Caution Do not specify (int) Math.random()*limit because this expression always evaluates to 0. The
expression first casts random()’s double precision floating-point fractional value (0.0 through 0.99999. . .) to
integer 0 by truncating the fractional part, and then multiplies 0 by limit, resulting in 0.
The sin() and cos() methods implement the sine and cosine trigonometric functions—see
http://en.wikipedia.org/wiki/Trigonometric_functions. These functions have uses ranging from the
study of triangles to modeling periodic phenomena (such as simple harmonic motion—see
http://en.wikipedia.org/wiki/Simple_harmonic_motion).
We can use sin() and cos() to generate and display sine and cosine waves. Listing 4-1 presents the
source code to an application that does just this.
Listing 4-1. Graphing sine and cosine waves
class Graph
{
final static int ROWS = 11; // Must be odd
final static int COLS= 23;
public static void main(String[] args)
{
char[][] screen = new char[ROWS][];
for (int row = 0; row < ROWS; row++)
screen[row] = new char[COLS];
double scaleX = COLS/360.0;
for (int degree = 0; degree < 360; degree++)
{
int row = ROWS/2+
(int) Math.round(ROWS/2*Math.sin(Math.toRadians(degree)));
int col = (int) (degree*scaleX);
screen[row][col] = 'S';
row = ROWS/2+
(int) Math.round(ROWS/2*Math.cos(Math.toRadians(degree)));
screen[row][col] = (screen[row][col] == 'S') ? '*' : 'C';
}
for (int row = ROWS-1; row >= 0; row--)
231
CHAPTER 4  TOURING LANGUAGE APIS
{
for (int col = 0; col < COLS; col++)
System.out.print(screen[row][col]);
System.out.println();
}
}
}
Listing 4-1 introduces a Graph class that first declares a pair of constants: NROWS and NCOLS. These
constants specify the dimensions of an array on which the graphs are generated. NROWS must be assigned
an odd integer; otherwise, an instance of the java.lang.ArrayIndexOutOfBoundsException class is
thrown.
■ Tip It’s a good idea to use constants wherever possible. The source code is easier to maintain because you only
need to change the constant’s value in one place instead of having to change each corresponding value
throughout the source code.
Graph next declares its main() method, which first creates a two-dimensional screen array of
characters. This array is used to simulate an old-style character-based screen for viewing the graphs.
main() next calculates a horizontal scale value for scaling each graph horizontally so that 360
horizontal (degree) positions fit into the number of columns specified by NCOLS.
Continuing, main() enters a for loop that, for each of the sine and cosine graphs, creates (row,
column) coordinates for each degree value, and assigns a character to the screen array at those
coordinates. The character is S for the sine graph, C for the cosine graph, and * when the cosine graph
intersects the sine graph.
The row calculation invokes toRadians() to convert its degree argument to radians, which is
required by the sin() and cos() methods. The value returned from sin() or cos() (-1 to 1) is then
multiplied by ROWS/2 to scale this value to half the number of rows in the screen array. After rounding the
result to the nearest long integer via the long round(double d) method, a cast is used to convert from
long integer to integer, and this integer is added to ROW/2 to offset the row coordinate so that it’s relative
to the array’s middle row. The column calculation is simpler, multiplying the degree value by the
horizontal scale factor.
The screen array is dumped to the standard output device via a pair of nested for loops. The outer
for loop inverts the screen so that it appears right side up—row number 0 should output last.
Compile Listing 4-1 (javac Graph.java) and run the application (java Graph). You observe the
following output:
232
CHAPTER 4  TOURING LANGUAGE APIS
CC SSSS
CC
CSSS SS
CC
S*C
SS
CC
S CC
SS
CC
SS CC
SS
CC
S
CC
S
CC
S
C
SS
C
SS
CC
SS CC
S
CC
SCC
SS
CC
CSS SSS
CCCCC SSSS
■ Note When I created the screen array, I took advantage of the fact that every element is initialized to 0, which
is interpreted as the null character. When a System.out.print() or System.out.println() method detects this
character, it outputs a space character instead.
Table 4-1 also reveals a few curiosities starting with +infinity, -infinity, +0.0, -0.0, and NaN (Not a
Number).
Java’s floating-point calculations are capable of returning +infinity, -infinity, +0.0, -0.0, and NaN
because Java largely conforms to IEEE 754 (http://en.wikipedia.org/wiki/IEEE_754), a standard for
floating-point calculations. The following are the circumstances under which these special values arise:
•
+infinity returns from attempting to divide a positive number by 0.0. For example,
System.out.println(1.0/0.0); outputs Infinity.
•
-infinity returns from attempting to divide a negative number by 0.0. For example,
System.out.println(-1.0/0.0); outputs -Infinity.
•
NaN returns from attempting to divide 0.0 by 0.0, attempting to calculate the
square root of a negative number, and attempting other strange operations. For
example, System.out.println(0.0/0.0); and System.out.println(Math.sqrt(1.0)); each output NaN.
•
+0.0 results from attempting to divide a positive number by +infinity. For example,
System.out.println(1.0/(1.0/0.0)); outputs +0.0.
•
-0.0 results from attempting to divide a negative number by +infinity. For
example, System.out.println(-1.0/(1.0/0.0)); outputs -0.0.
Once an operation yields +infinity, -infinity, or NaN, the rest of the expression usually equals that
special value. For example, System.out.println(1.0/0.0*20.0); outputs Infinity. Also, an expression
that first yields +infinity or -infinity might devolve into NaN. For example, 1.0/0.0*0.0 yields +infinity
(1.0/0.0) and then NaN (+infinity*0.0).
Another curiosity is Integer.MAX_VALUE, Integer.MIN_VALUE, Long.MAX_VALUE, and Long.MIN_VALUE.
Each of these items is a primitive type wrapper class constant that identifies the maximum or minimum
value that can be represented by the class’s associated primitive type. (I discuss primitive type wrapper
classes later in this chapter.)
233
CHAPTER 4  TOURING LANGUAGE APIS
Finally, you might wonder why the abs(), max(), and min() overloaded methods do not include byte
and short versions, as in byte abs(byte b) and short abs(short s). There is no need for these methods
because the limited ranges of bytes and short integers make them unsuitable in calculations. If you need
such a method, check out Listing 4-2.
Listing 4-2. Obtaining absolute values for byte integers and short integers
class AbsByteShort
{
static byte abs(byte b)
{
return (b < 0) ? (byte) -b : b;
}
static short abs(short s)
{
return (s < 0) ? (short) -s : s;
}
public static void main(String[] args)
{
byte b = -2;
System.out.println(abs(b)); // Output: 2
short s = -3;
System.out.println(abs(s)); // Output: 3
}
}
Listing 4-2’s (byte) and (short) casts are necessary because -b converts b’s value from a byte to an
int, and -s converts s’s value from a short to an int. In contrast, these casts are not needed with (b < 0)
and (s < 0), which automatically cast b’s and s’s values to an int before comparing them with intbased 0.
■ Tip Their absence from Math suggests that byte and short are not very useful in method declarations.
However, these types are useful when declaring arrays whose elements store small values (such as a binary file’s
byte values). If you declared an array of int or long to store such values, you would end up wasting heap space
(and might even run out of memory).
While searching through the Java documentation for the java.lang package, you will probably
encounter a class named StrictMath. Apart from a longer name, this class appears to be identical to
Math. The differences between these classes can be summed up as follows:
234
•
StrictMath’s methods return exactly the same results on all platforms. In contrast,
some of Math’s methods might return values that vary ever so slightly from
platform to platform.
•
Because StrictMath cannot utilize platform-specific features such as an extendedprecision math coprocessor, an implementation of StrictMath might be less
efficient than an implementation of Math.
CHAPTER 4  TOURING LANGUAGE APIS
For the most part, Math’s methods call their StrictMath counterparts. Two exceptions are
toDegrees() and toRadians(). Although these methods have identical code bodies in both classes,
StrictMath’s implementations include reserved word strictfp in the method headers:
public static strictfp double toDegrees(double angrad)
public static strictfp double toRadians(double angdeg)
Wikipedia’s “strictfp” entry (http://en.wikipedia.org/wiki/Strictfp) mentions that strictfp
restricts floating-point calculations to ensure portability. This reserved word accomplishes portability in
the context of intermediate floating-point representations and overflows/underflows (generating a value
too large or small to fit a representation).
■ Note The previously cited “strictfp” article states that Math contains public static strictfp double
abs(double); and other strictfp methods. If you check out this class’s source code under Java 7, you will not
find strictfp anywhere in the source code. However, many Math methods (such as sin()) call their StrictMath
counterparts, which are implemented in a platform-specific library, and the library’s method implementations are
strict.
Without strictfp, an intermediate calculation is not limited to the IEEE 754 32-bit and 64-bit
floating-point representations that Java supports. Instead, the calculation can take advantage of a larger
representation (perhaps 128 bits) on a platform that supports this representation.
An intermediate calculation that overflows/underflows when its value is represented in 32/64 bits
might not overflow/underflow when its value is represented in more bits. Because of this discrepancy,
portability is compromised. strictfp levels the playing field by requiring all platforms to use 32/64 bits
for intermediate calculations.
When applied to a method, strictfp ensures that all floating-point calculations performed in that
method are in strict compliance. However, strictfp can be used in a class header declaration (as in
public strictfp class FourierTransform) to ensure that all floating-point calculations performed in
that class are strict.
■ Note Math and StrictMath are declared final so that they cannot be extended. Also, they declare private
empty noargument constructors so that they cannot be instantiated. Finally, Math and StrictMath are examples of
utility classes because they exist as placeholders for static methods.
Package
The java.lang.Package class provides access to information about a package (see Chapter 3 for an
introduction to packages). This information includes version details about the implementation and
specification of a Java package, the name of the package, and an indication of whether or not the
package has been sealed (all classes that are part of a package are archived in the same JAR file).
Table 4-2 describes some of Package’s methods.
235
CHAPTER 4  TOURING LANGUAGE APIS
Table 4-2. Package Methods
236
Method
Description
String
getImplementationTitle()
Return the title of this package’s implementation, which might
be null. The format of the title is unspecified.
String
getImplementationVendor()
Return the name of the vendor or organization that provides
this package’s implementation. This name might be null. The
format of the name is unspecified.
String
getImplementationVersion()
Return the version number of this package’s implementation,
which might be null. This version string must be a sequence of
positive decimal integers separated by periods and might have
leading zeros.
String getName()
Return the name of this package in standard dot notation; for
example, java.lang.
static Package
getPackage(String
packageName)
Return the Package object that is associated with the package
identified as packageName, or null when the package identified
as packageName cannot be found. This method throws
java.lang.NullPointerException when packageName is null.
static Package[]
getPackages()
Return an array of all Package objects that are accessible to this
method’s caller.
String
getSpecificationTitle()
Return the title of this package’s specification, which might be
null. The format of the title is unspecified.
String
getSpecificationVendor()
Return the name of the vendor or organization that provides
the specification that is implemented by this package. This
name might be null. The format of the name is unspecified.
String
getSpecificationVersion()
Return the version number of the specification of this
package’s implementation, which might be null. This version
string must be a sequence of positive decimal integers
separated by periods, and might have leading zeros.
boolean
isCompatibleWith(String
desired)
Check this package to determine if it is compatible with the
specified version string, by comparing this package’s
specification version with the desired version. Return true
when this package’s specification version number is greater
than or equal to the desired version number (this package is
compatible); otherwise, return false. This method throws
NullPointerException when desired is null, and
java.lang.NumberFormatException when this package’s version
CHAPTER 4  TOURING LANGUAGE APIS
number or the desired version number is not in dotted form.
boolean isSealed()
Return true when this package has been sealed; otherwise,
return false.
I have created a PackageInfo application that demonstrates most of Table 4-2’s Package methods.
Listing 4-3 presents this application’s source code.
Listing 4-3. Obtaining information about a package
class PackageInfo
{
public static void main(String[] args)
{
if (args.length == 0)
{
System.err.println("usage: java PackageInfo packageName [version]");
return;
}
Package pkg = Package.getPackage(args[0]);
if (pkg == null)
{
System.err.println(args[0]+" not found");
return;
}
System.out.println("Name: "+pkg.getName());
System.out.println("Implementation title: "+
pkg.getImplementationTitle());
System.out.println("Implementation vendor: "+
pkg.getImplementationVendor());
System.out.println("Implementation version: "+
pkg.getImplementationVersion());
System.out.println("Specification title: "+
pkg.getSpecificationTitle());
System.out.println("Specification vendor: "+
pkg.getSpecificationVendor());
System.out.println("Specification version: "+
pkg.getSpecificationVersion());
System.out.println("Sealed: "+pkg.isSealed());
if (args.length > 1)
System.out.println("Compatible with "+args[1]+": "+
pkg.isCompatibleWith(args[1]));
}
}
After compiling Listing 4-3 (javac PackageInfo.java), specify at least a package name on the
command line when you run this application. For example, java PackageInfo java.lang returns the
following output under Java 7:
Name: java.lang
Implementation title: Java Runtime Environment
237
CHAPTER 4  TOURING LANGUAGE APIS
Implementation vendor: Oracle Corporation
Implementation version: 1.7.0
Specification title: Java Platform API Specification
Specification vendor: Oracle Corporation
Specification version: 1.7
Sealed: false
PackageInfo also lets you determine if the package’s specification is compatible with a specific
version number. A package is compatible with its predecessors.
For example, java PackageInfo java.lang 1.7 outputs Compatible with 1.7: true, whereas java
PackageInfo java.lang 1.8 outputs Compatible with 1.8: false.
You can also use PackageInfo with your own packages, which you learned to create in Chapter 3. For
example, that chapter presented a logging package.
Copy PackageInfo.class into the directory containing the logging package directory (which
contains the compiled classfiles), and execute java PackageInfo logging.
PackageInfo responds by displaying the following output:
logging not found
This error message is presented because getPackage() requires at least one classfile to be loaded
from the package before it returns a Package object describing that package.
The only way to eliminate the previous error message is to load a class from the package.
Accomplish this task by merging the following code fragment into Listing 4-3.
if (args.length == 3)
try
{
Class.forName(args[2]);
}
catch (ClassNotFoundException cnfe)
{
System.err.println("cannot load "+args[2]);
return;
}
This code fragment, which must precede Package pkg = Package.getPackage(args[0]);, loads the
classfile named by the revised PackageInfo application’s third command-line argument. (I’ll discuss
Class.forName() later in this chapter.)
Run the new PackageInfo application via java PackageInfo logging 1.5 logging.File and you will
observe the following output—this command line identifies logging’s File class as the class to load:
Name: logging
Implementation title: null
Implementation vendor: null
Implementation version: null
Specification title: null
Specification vendor: null
Specification version: null
Sealed: false
Exception in thread "main" java.lang.NumberFormatException: Empty version string
at java.lang.Package.isCompatibleWith(Package.java:228)
at PackageInfo.main(PackageInfo.java:42)
238
CHAPTER 4  TOURING LANGUAGE APIS
It is not surprising to see all of these null values because no package information has been added to
the logging package. Also, NumberFormatException is thrown from isCompatibleWith() because the
logging package does not contain a specification version number in dotted form (it is null).
Perhaps the simplest way to place package information into the logging package is to create a
logging.jar file in a similar manner to the example shown in Chapter 3. But first, you must create a
small text file that contains the package information. You can choose any name for the file. Listing 4-4
reveals my choice of manifest.mf.
Listing 4-4. manifest.mf containing the package information
Implementation-Title: Logging Implementation
Implementation-Vendor: Jeff Friesen
Implementation-Version: 1.0a
Specification-Title: Logging Specification
Specification-Vendor: Jeff Friesen
Specification-Version: 1.0
Sealed: true
■ Note Make sure to press the Return/Enter key at the end of the final line (Sealed: true). Otherwise, you will
probably observe Sealed: false in the output because this entry will not be stored in the logging package by
the JDK’s jar tool—jar is a bit quirky.
Execute the following command line to create a JAR file that includes logging and its files, and
whose manifest, a special file named MANIFEST.MF that stores information about the contents of a JAR file,
contains the contents of Listing 4-4:
jar cfm logging.jar manifest.mf logging/*.class
This command line creates a JAR file named logging.jar (via the c [create] and f [file] options). It
also merges the contents of manifest.mf (via the m [manifest] option) into MANIFEST.MF, which is stored in
the package’s META-INF directory.
■ Note To learn more about a JAR file’s manifest, read the “JAR Manifest” section of the JDK documentation’s
“JAR File Specification” page
(http://download.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#JAR Manifest).
Assuming that the jar tool presents no error messages, execute the following Windows-oriented
command line (or a command line suitable for your platform) to run PackageInfo and extract the
package information from the logging package:
java -cp logging.jar;. PackageInfo logging 1.0 logging.File
This time, you should see the following output:
239
CHAPTER 4  TOURING LANGUAGE APIS
Name: logging
Implementation title: Logging Implementation
Implementation vendor: Jeff Friesen
Implementation version: 1.0a
Specification title: Logging Specification
Specification vendor: Jeff Friesen
Specification version: 1.0
Sealed: true
Compatible with 1.0: true
Primitive Type Wrapper Class
The java.lang package includes Boolean, Byte, Character, Double, Float, Integer, Long, and Short. These
classes are known as primitive type wrapper classes because their instances wrap themselves around
values of primitive types.
This book was purchased by [email protected]
■ Note The primitive type wrapper classes are also known as value classes.
Java provides these eight primitive type wrapper classes for two reasons:
•
The Collections Framework (discussed in Chapter 5) provides lists, sets, and maps
that can only store objects; they cannot store primitive values. You store a
primitive value in a primitive type wrapper class instance and store the instance in
the collection.
•
These classes provide a good place to associate useful constants (such as
MAX_VALUE and MIN_VALUE) and class methods (such as Integer’s parseInt()
methods and Character’s isDigit(), isLetter(), and toUpperCase() methods)
with the primitive types.
This section introduces you to each of these primitive type wrapper classes and a java.lang class
named Number.
Boolean
Boolean is the smallest of the primitive type wrapper classes. This class declares three constants,
including TRUE and FALSE, which denote precreated Boolean objects. It also declares a pair of
constructors for initializing a Boolean object:
•
Boolean(boolean value) initializes the Boolean object to value.
•
Boolean(String s) converts s’s text to a true or false value and stores this value in
the Boolean object.
The second constructor compares s’s value with true. Because the comparison is case-insensitive,
any uppercase/lowercase combination of these four letters (such as true, TRUE, or tRue) results in true
being stored in the object. Otherwise, the constructor stores false in the object.
240
CHAPTER 4  TOURING LANGUAGE APIS
■ Note Boolean’s constructors are complemented by boolean booleanValue(), which returns the wrapped
Boolean value.
Boolean also declares or overrides the following methods:
•
int compareTo(Boolean b) compares the current Boolean object with b to
determine their relative order. The method returns 0 when the current object
contains the same Boolean value as b, a positive value when the current object
contains true and b contains false, and a negative value when the current object
contains false and b contains true.
•
boolean equals(Object o) compares the current Boolean object with o and returns
true when o is not null, o is of type Boolean, and both objects contain the same
Boolean value.
•
static boolean getBoolean(String name) returns true when a system property
(discussed later in this chapter) identified by name exists and is equal to true.
•
int hashCode() returns a suitable hash code that allows Boolean objects to be used
with hash-based collections (discussed in Chapter 5).
•
static boolean parseBoolean(String s) parses s, returning true if s equals
"true", "TRUE", "True", or any other uppercase/lowercase combination.
Otherwise, this method returns false. (Parsing breaks a sequence of characters
into meaningful components, known as tokens.)
•
String toString() returns "true" when the current Boolean instance contains
true; otherwise, this method returns "false".
•
static String toString(boolean b) returns "true" when b contains true;
otherwise, this method returns "false".
•
static Boolean valueOf(boolean b) returns TRUE when b contains true or FALSE
when b contains false.
•
static Boolean valueOf(String s) returns TRUE when s equals "true", "TRUE",
"True", or any other uppercase/lowercase combination of these letters. Otherwise,
this method returns FALSE.
■ Caution Newcomers to the Boolean class often think that getBoolean() returns a Boolean object’s true/false
value. However, getBoolean() returns the value of a Boolean-based system property—I discuss system
properties later in this chapter. If you need to return a Boolean object’s true/false value, use the booleanValue()
method instead.
241
CHAPTER 4  TOURING LANGUAGE APIS
It is often better to use TRUE and FALSE than to create Boolean objects. For example, suppose you
need a method that returns a Boolean object containing true when the method’s double argument is
negative, or false when this argument is zero or positive. You might declare your method like the
following isNegative() method:
Boolean isNegative(double d)
{
return new Boolean(d < 0);
}
Although this method is concise, it unnecessarily creates a Boolean object. When the method is
called frequently, many Boolean objects are created that consume heap space. When heap space runs
low, the garbage collector runs and slows down the application, which impacts performance.
The following example reveals a better way to code isNegative():
Boolean isNegative(double d)
{
return (d < 0) ? Boolean.TRUE : Boolean.FALSE;
}
This method avoids creating Boolean objects by returning either the precreated TRUE or FALSE object.
■ Tip You should strive to create as few objects as possible. Not only will your applications have smaller memory
footprints, they’ll perform better because the garbage collector will not run as often.
Character
Character is the largest of the primitive type wrapper classes, containing many constants, a constructor,
many methods, and a trio of nested classes (Subset, UnicodeBlock, and UnicodeScript).
■ Note Character’s complexity derives from Java’s support for Unicode
(http://en.wikipedia.org/wiki/Unicode). For brevity, I ignore much of Character’s Unicode-related
complexity, which is beyond the scope of this chapter.
Character declares a single Character(char value) constructor, which you use to initialize a
Character object to value. This constructor is complemented by char charValue(), which returns the
wrapped character value.
When you start writing applications, you might codify expressions such as ch >= '0' && ch <= '9'
(test ch to see if it contains a digit) and ch >= 'A' && ch <= 'Z' (test ch to see if it contains an uppercase
letter). You should avoid doing so for three reasons:
•
242
It is too easy to introduce a bug into the expression. For example, ch > '0' && ch
<= '9' introduces a subtle bug that does not include '0' in the comparison.
CHAPTER 4  TOURING LANGUAGE APIS
•
The expressions are not very descriptive of what they are testing.
•
The expressions are biased toward Latin digits (0-9) and letters (A-Z and a-z). They
do not take into account digits and letters that are valid in other languages. For
example, '\u0beb' is a character literal representing one of the digits in the Tamil
language.
Character declares several comparison and conversion class methods that address these concerns.
These methods include the following:
•
static boolean isDigit(char ch) returns true when ch contains a digit (typically
0 through 9, but also digits in other alphabets).
•
static boolean isLetter(char ch) returns true when ch contains a letter
(typically A-Z or a-z, but also letters in other alphabets).
•
static boolean isLetterOrDigit(char ch) returns true when ch contains a letter
or digit (typically A-Z, a-z, or 0-9; but also letters or digits in other alphabets).
•
static boolean isLowerCase(char ch) returns true when ch contains a lowercase
letter.
•
static boolean isUpperCase(char ch) returns true when ch contains an
uppercase letter.
•
static boolean isWhitespace(char ch) returns true when ch contains a
whitespace character (typically a space, a horizontal tab, a carriage return, or a
line feed).
•
static char toLowerCase(char ch) returns the lowercase equivalent of ch’s
uppercase letter; otherwise, this method returns ch’s value.
•
static char toUpperCase(char ch) returns the uppercase equivalent of ch’s
lowercase letter; otherwise, this method returns ch’s value.
For example, isDigit(ch) is preferable to ch >= '0' && ch <= '9' because it avoids a source of
bugs, is more readable, and returns true for non-Latin digits (e.g., '\u0beb') as well as Latin digits.
Float and Double
Float and Double store floating-point and double precision floating-point values in Float and Double
objects, respectively. These classes declare the following constants:
•
MAX_VALUE identifies the maximum value that can be represented as a float or
double.
•
MIN_VALUE identifies the minimum value that can be represented as a float or
double.
•
NaN represents 0.0F/0.0F as a float and 0.0/0.0 as a double.
•
NEGATIVE_INFINITY represents -infinity as a float or double.
•
POSITIVE_INFINITY represents +infinity as a float or double.
243
CHAPTER 4  TOURING LANGUAGE APIS
Float and Double also declare the following constructors for initializing their objects:
•
Float(float value) initializes the Float object to value.
•
Float(double value) initializes the Float object to the float equivalent of value.
•
Float(String s) converts s’s text to a floating-point value and stores this value in
the Float object.
•
Double(double value) initializes the Double object to value.
•
Double(String s) converts s’s text to a double precision floating-point value and
stores this value in the Double object.
Float’s constructors are complemented by float floatValue(), which returns the wrapped
floating-point value. Similarly, Double’s constructors are complemented by double doubleValue(),
which returns the wrapped double precision floating-point value.
Float declares several utility methods as well as floatValue(). These methods include the following:
•
static int floatToIntBits(float value) converts value to a 32-bit integer.
•
static boolean isInfinite(float f) returns true when f’s value is +infinity or infinity. A related boolean isInfinite() method returns true when the current
Float object’s value is +infinity or -infinity.
•
static boolean isNaN(float f) returns true when f’s value is NaN. A related
boolean isNaN() method returns true when the current Float object’s value is
NaN.
•
static float parseFloat(String s) parses s, returning the floating-point
equivalent of s’s textual representation of a floating-point value, or throwing
NumberFormatException when this representation is invalid (contains letters, for
example).
Double declares several utility methods as well as doubleValue(). These methods include the
following:
•
static long doubleToLongBits(double value) converts value to a long integer.
•
static boolean isInfinite(double d) returns true when d’s value is +infinity or infinity. A related boolean isInfinite() method returns true when the current
Double object’s value is +infinity or -infinity.
•
static boolean isNaN(double d) returns true when d’s value is NaN. A related
boolean isNaN() method returns true when the current Double object’s value is
NaN.
•
static double parseDouble(String s) parses s, returning the double precision
floating-point equivalent of s’s textual representation of a double precision
floating-point value, or throwing NumberFormatException when this representation
is invalid.
The floatToIntBits() and doubleToIntBits() methods are used in implementations of the equals()
and hashCode() methods that must take float and double fields into account. floatToIntBits() and
doubleToIntBits() allow equals() and hashCode() to respond properly to the following situations:
244
CHAPTER 4  TOURING LANGUAGE APIS
•
equals() must return true when f1 and f2 contain Float.NaN (or d1 and d2 contain
Double.NaN). If equals() was implemented in a manner similar to f1.floatValue()
== f2.floatValue() (or d1.doubleValue() == d2.doubleValue()), this method
would return false because NaN is not equal to anything, including itself.
•
equals() must return false when f1 contains +0.0 and f2 contains -0.0 (or viceversa), or d1 contains +0.0 and d2 contains -0.0 (or vice-versa). If equals() was
implemented in a manner similar to f1.floatValue() == f2.floatValue() (or
d1.doubleValue() == d2.doubleValue()), this method would return true because
+0.0 == -0.0 returns true.
These requirements are needed for hash-based collections (discussed in Chapter 5) to work
properly. Listing 4-5 shows how they impact Float’s and Double’s equals() methods:
Listing 4-5. Demonstrating Float’s equals() method in a NaN context and Double’s equals() method in a
+/-0.0 context
class FloatDoubleDemo
{
public static void main(String[] args)
{
Float f1 = new Float(Float.NaN);
System.out.println(f1.floatValue());
Float f2 = new Float(Float.NaN);
System.out.println(f2.floatValue());
System.out.println(f1.equals(f2));
System.out.println(Float.NaN == Float.NaN);
System.out.println();
Double d1 = new Double(+0.0);
System.out.println(d1.doubleValue());
Double d2 = new Double(-0.0);
System.out.println(d2.doubleValue());
System.out.println(d1.equals(d2));
System.out.println(+0.0 == -0.0);
}
}
Compile Listing 4-5 (javac FloatDoubleDemo.java) and run this application (java FloatDoubleDemo).
The following output proves that Float’s equals() method properly handles NaN and Double’s equals()
method properly handles +/-0.0:
NaN
NaN
true
false
0.0
-0.0
false
true
245
CHAPTER 4  TOURING LANGUAGE APIS
■ Tip If you want to test a float or double value for equality with +infinity or -infinity (but not both), do not use
isInfinite(). Instead, compare the value with NEGATIVE_INFINITY or POSITIVE_INFINITY via ==. For example,
f == Float.NEGATIVE_INFINITY.
You will find parseFloat() and parseDouble() useful in many contexts. For example, Listing 4-6 uses
parseDouble() to parse command-line arguments into doubles.
Listing 4-6. Parsing command-line arguments into double precision floating-point values
class Calc
{
public static void main(String[] args)
{
if (args.length != 3)
{
System.err.println("usage: java Calc value1 op value2");
System.err.println("op is one of +, -, *, or /");
return;
}
try
{
double value1 = Double.parseDouble(args[0]);
double value2 = Double.parseDouble(args[2]);
if (args[1].equals("+"))
System.out.println(value1+value2);
else
if (args[1].equals("-"))
System.out.println(value1-value2);
else
if (args[1].equals("*"))
System.out.println(value1*value2);
else
if (args[1].equals("/"))
System.out.println(value1/value2);
else
System.err.println("invalid operator: "+args[1]);
}
catch (NumberFormatException nfe)
{
System.err.println("Bad number format: "+nfe.getMessage());
}
}
}
Specify java Calc 10E+3 + 66.0 to try out the Calc application. This application responds by
outputting 10066.0. If you specified java Calc 10E+3 + A instead, you would observe Bad number
format: For input string: "A" as the output, which is in response to the second parseDouble() method
call’s throwing of a NumberFormatException object.
246
CHAPTER 4  TOURING LANGUAGE APIS
Although NumberFormatException describes an unchecked exception, and although unchecked
exceptions are often not handled because they represent coding mistakes, NumberFormatException does
not fit this pattern in this example. The exception does not arise from a coding mistake; it arises from
someone passing an illegal numeric argument to the application, which cannot be avoided through
proper coding. Perhaps NumberFormatException should have been implemented as a checked exception
type.
Integer, Long, Short, and Byte
Integer, Long, Short, and Byte store 32-bit, 64-bit, 16-bit, and 8-bit integer values in Integer, Long, Short,
and Byte objects, respectively.
Each class declares MAX_VALUE and MIN_VALUE constants that identify the maximum and minimum
values that can be represented by its associated primitive type. These classes also declare the following
constructors for initializing their objects:
•
Integer(int value) initializes the Integer object to value.
•
Integer(String s) converts s’s text to a 32-bit integer value and stores this value
in the Integer object.
•
Long(long value) initializes the Long object to value.
•
Long(String s) converts s’s text to a 64-bit integer value and stores this value in
the Long object.
•
Short(short value) initializes the Short object to value.
•
Short(String s) converts s’s text to a 16-bit integer value and stores this value in
the Short object.
•
Byte(byte value) initializes the Byte object to value.
•
Byte(String s) converts s’s text to an 8-bit integer value and stores this value in
the Byte object.
Integer’s constructors are complemented by int intValue(), Long’s constructors are
complemented by long longValue(), Short’s constructors are complemented by short shortValue(),
and Byte’s constructors are complemented by byte byteValue(). These methods return wrapped
integers.
These classes declare various useful integer-oriented methods. For example, Integer declares the
following utility methods for converting a 32-bit integer to a java.lang.String instance according to a
specific representation (binary, hexadecimal, octal, and decimal):
•
static String toBinaryString(int i) returns a String object containing i’s
binary representation. For example, Integer.toBinaryString(255) returns a
String object containing 11111111.
•
static String toHexString(int i) returns a String object containing i’s
hexadecimal representation. For example, Integer.toHexString(255) returns a
String object containing ff.
•
static String toOctalString(int i) returns a String object containing i’s octal
representation. For example, toOctalString(64) returns a String object
containing 100.
247
CHAPTER 4  TOURING LANGUAGE APIS
•
static String toString(int i) returns a String object containing i’s decimal
representation. For example, toString(255) returns a String object containing
255.
It is often convenient to prepend zeros to a binary string so that you can align multiple binary
strings in columns. For example, you might want to create an application that displays the following
aligned output:
11110001
+
00000111
-------11111000
Unfortunately, toBinaryString() does not let you accomplish this task. For example,
Integer.toBinaryString(7) returns a String object containing 111 instead of 00000111. Listing 4-7’s
toAlignedBinaryString() method addresses this oversight.
Listing 4-7. Aligning binary strings
class AlignBinary
{
public static void main(String[] args)
{
System.out.println(toAlignedBinaryString(7, 8));
System.out.println(toAlignedBinaryString(255, 16));
System.out.println(toAlignedBinaryString(255, 7));
}
static String toAlignedBinaryString(int i, int numBits)
{
String result = Integer.toBinaryString(i);
if (result.length() > numBits)
return null; // cannot fit result into numBits columns
int numLeadingZeros = numBits-result.length();
String zerosPrefix = "";
for (int j = 0; j < numLeadingZeros; j++)
zerosPrefix += "0";
return zerosPrefix+result;
}
}
The toAlignedBinaryString() method takes two arguments: the first argument specifies the 32-bit
integer that is to be converted into a binary string, and the second argument specifies the number of bit
columns in which to fit the string.
After calling toBinaryString() to return i’s equivalent binary string without leading zeros,
toAlignedBinaryString() verifies that the string’s digits can fit into the number of bit columns specified
by numBits. If they do not fit, this method returns null. (You will learn about length() and other String
methods later in this chapter.)
Moving on, toAlignedBinaryString() calculates the number of leading "0"s to prepend to result,
and then uses a for loop to create a string of leading zeros. This method ends by returning the leading
zeros string prepended to the result string.
Although using the compound string concatenation with assignment operator (+=) in a loop to build
a string looks okay, it is very inefficient because intermediate String objects are created and thrown
248
CHAPTER 4  TOURING LANGUAGE APIS
away. However, I employed this inefficient code so that I can contrast it with the more efficient code that
I present later in this chapter.
When you run this application, it generates the following output:
00000111
0000000011111111
null
Number
Each of Float, Double, Integer, Long, Short, and Byte provides the other classes’ xValue() methods as
well as its own xValue() method. For example, Float provides doubleValue(), intValue(), longValue(),
shortValue(), and byteValue() as well as floatValue().
All six methods are members of Number, which is the abstract superclass of Float, Double, Integer,
Long, Short, and Byte—Number’s floatValue(), doubleValue(), intValue(), and longValue() methods are
abstract. Number is also the superclass of java.math.BigDecimal and java.math.BigInteger (discussed
later in this chapter), and a pair of concurrency-related classes (one of these classes is presented in
Chapter 6).
Number exists to simplify iterating over a collection of Number subclass objects. For example, you can
declare a variable of java.util.List<Number> type and initialize it to an instance of
java.util.ArrayList<Number> (or ArrayList<>, for short). You can then store a mixture of Number
subclass objects in the collection, and iterate over this collection by calling a subclass method
polymorphically.
Reference
Chapter 2 introduced you to garbage collection, where you learned that the garbage collector removes
an object from the heap when there are no more references to the object. This statement isn’t
completely true, as you will shortly discover.
Chapter 2 also introduced you to java.lang.Object’s finalize() method, where you learned that
the garbage collector calls this method before removing an object from the heap. The finalize()
method gives the object an opportunity to perform cleanup.
This section continues from where Chapter 2 left off by introducing you to Java’s Reference API.
After acquainting you with some basic terminology, it introduces you to the API’s Reference and
ReferenceQueue classes, followed by the API’s SoftReference, WeakReference, and PhantomReference
classes. These classes let applications interact with the garbage collector in limited ways.
■ Note As well as this section, you will find Brian Goetz’s “Java theory and practice: Plugging memory leaks with
soft references” (http://www.ibm.com/developerworks/java/library/j-jtp01246/index.html) and “Java
theory and practice: Plugging memory leaks with weak references”
(http://www.ibm.com/developerworks/java/library/j-jtp11225/index.html) tutorials to be helpful in
understanding the Reference API.
249
CHAPTER 4  TOURING LANGUAGE APIS
Basic Terminology
When an application runs, its execution reveals a root set of references, a collection of local variables,
parameters, class fields, and instance fields that currently exist and that contain (possibly null)
references to objects. This root set changes over time as the application runs. For example, parameters
disappear after a method returns.
Many garbage collectors identify this root set when they run. They use the root set to determine if an
object is reachable (referenced, also known as live) or unreachable (not referenced). The garbage
collector cannot collect reachable objects. Instead, it can only collect objects that, starting from the root
set of references, cannot be reached.
This book was purchased by [email protected]
■ Note Reachable objects include objects that are indirectly reachable from root-set variables, which means
objects that are reachable through live objects that are directly reachable from those variables. An object that is
unreachable by any path from any root-set variable is eligible for garbage collection.
Beginning with Java 1.2, reachable objects are classified as strongly reachable, softly reachable,
weakly reachable, and phantom reachable. Unlike strongly reachable objects, softly, weakly, and
phantom reachable objects can be garbage collected.
Going from strongest to weakest, the different levels of reachability reflect the life cycle of an object.
They are defined as follows:
250
•
An object is strongly reachable if it can be reached from some thread without
traversing any Reference objects. A newly created object (such as the object
referenced by d in Double d = new Double(1.0);) is strongly reachable by the
thread that created it. (I will discuss threads later in this chapter.)
•
An object is softly reachable if it is not strongly reachable but can be reached by
traversing a soft reference (a reference to the object where the reference is stored in
a SoftReference object). The strongest reference to this object is a soft reference.
When the soft references to a softly reachable object are cleared, the object
becomes eligible for finalization (discussed in Chapter 2).
•
An object is weakly reachable if it is neither strongly reachable nor softly
reachable, but can be reached by traversing a weak reference (a reference to the
object where the reference is stored in a WeakReference object). The strongest
reference to this object is a weak reference. When the weak references to a weakly
reachable object are cleared, the object becomes eligible for finalization. (Apart
from the garbage collector being more eager to clean up the weakly reachable
object, a weak reference is exactly like a soft reference.)
•
An object is phantom reachable if it is neither strongly, softly, nor weakly
reachable, it has been finalized, and it is referred to by some phantom reference (a
reference to the object where the reference is stored in a PhantomReference object).
The strongest reference to this object is a phantom reference.
CHAPTER 4  TOURING LANGUAGE APIS
•
Finally, an object is unreachable, and therefore eligible for removal from memory
during the next garbage collection cycle, when it is not reachable in any of the
above ways.
The object whose reference is stored in a SoftReference, WeakReference, or PhantomReference object
is known as a referent.
Reference and ReferenceQueue
The Reference API consists of five classes located in the java.lang.ref package. Central to this package
are Reference and ReferenceQueue.
Reference is the abstract superclass of this package’s concrete SoftReference, WeakReference, and
PhantomReference subclasses.
ReferenceQueue is a concrete class whose instances describe queue data structures. When you
associate a ReferenceQueue instance with a Reference subclass object (Reference object, for short), the
Reference object is added to the queue when the referent to which its encapsulated reference refers
becomes garbage.
■ Note You associate a ReferenceQueue object with a Reference object by passing the ReferenceQueue object
to an appropriate Reference subclass constructor.
Reference is declared as generic type Reference<T>, where T identifies the referent’s type. This class
provides the following methods:
•
void clear() assigns null to the stored reference; the Reference object on which
this method is called is not enqueued (inserted) into its associated reference queue
(if there is an associated reference queue). (The garbage collector clears references
directly; it does not call clear(). Instead, this method is called by applications.)
•
boolean enqueue() adds the Reference object on which this method is called to the
associated reference queue. This method returns true when this Reference object
has become enqueued; otherwise, this method returns false—this Reference
object was already enqueued or was not associated with a queue when created.
(The garbage collector enqueues Reference objects directly; it does not call
enqueue(). Instead, this method is called by applications.)
•
T get() returns this Reference object’s stored reference. The return value is null
when the stored reference has been cleared, either by the application or by the
garbage collector.
•
boolean isEnqueued() returns true when this Reference object has been
enqueued, either by the application or by the garbage collector. Otherwise, this
method returns false—this Reference object was not associated with a queue
when created.
251
CHAPTER 4  TOURING LANGUAGE APIS
■ Note Reference also declares constructors. Because these constructors are package-private, only classes in
the java.lang.ref package can subclass Reference. This restriction is necessary because instances of
Reference’s subclasses must work closely with the garbage collector.
ReferenceQueue is declared as generic type ReferenceQueue<T>, where T identifies the referent’s type.
This class declares the following constructor and methods:
•
ReferenceQueue() initializes a new ReferenceQueue instance.
•
Reference<? extends T> poll() polls this queue to check for an available
Reference object. If one is available, the object is removed from the queue and
returned. Otherwise, this method returns immediately with a null value.
•
Reference<? extends T> remove() removes the next Reference object from the
queue and returns this object. This method waits indefinitely for a Reference
object to become available, and throws java.lang.InterruptedException when
this wait is interrupted.
•
Reference<? extends T> remove(long timeout) removes the next Reference
object from the queue and returns this object. This method waits until a Reference
object becomes available or until timeout milliseconds have elapsed—passing 0 to
timeout causes the method to wait indefinitely. If timeout’s value expires, the
method returns null. This method throws java.lang.IllegalArgumentException
when timeout’s value is negative, or InterruptedException when this wait is
interrupted.
SoftReference
The SoftReference class describes a Reference object whose referent is softly reachable. As well as
inheriting Reference’s methods and overriding get(), this generic class provides the following
constructors for initializing a SoftReference object:
•
SoftReference(T r) encapsulates r’s reference. The SoftReference object behaves
as a soft reference to r. No ReferenceQueue object is associated with this
SoftReference object.
•
SoftReference(T r, ReferenceQueue<? super T> q) encapsulates r’s reference.
The SoftReference object behaves as a soft reference to r. The ReferenceQueue
object identified by q is associated with this SoftReference object. Passing null to
q indicates a soft reference without a queue.
SoftReference is useful for implementing caches of objects that are expensive timewise to create
(e.g., a database connection) and/or occupy significant amounts of heap space, such as large images. An
image cache keeps images in memory (because it takes time to load them from disk) and ensures that
duplicate (and possibly very large) images are not stored in memory.
The image cache contains references to image objects that are already in memory. If these
references were strong, the images would remain in memory. You would then need to figure out which
images are no longer needed and remove them from memory so that they can be garbage collected.
252
CHAPTER 4  TOURING LANGUAGE APIS
Having to manually remove images duplicates the work of a garbage collector. However, if you wrap
the references to the image objects in SoftReference objects, the garbage collector will determine when
to remove these objects (typically when heap memory runs low) and perform the removal on your
behalf.
Listing 4-8 shows how you could use SoftReference to cache an image.
Listing 4-8. Caching an image
import java.lang.ref.SoftReference;
class Image
{
private byte[] image;
private Image(String name)
{
image = new byte[1024*1024*100];
}
static Image getImage(String name)
{
return new Image(name);
}
}
class ImageCache
{
public static void main(String[] args)
{
Image image = Image.getImage("large.png");
System.out.println("caching image");
SoftReference<Image> cache = new SoftReference<>(image);
image = null;
byte[] b = new byte[1024];
while (cache.get() != null)
{
System.out.println("image is still cached");
b = new byte[b.length*10];
}
System.out.println("image is no longer cached");
b = null;
System.out.println("reloading and recaching image");
cache = new SoftReference<>(Image.getImage("large.png"));
int counter = 0;
while (cache.get() != null && ++counter != 7)
System.out.println("image is still cached");
}
}
Listing 4-8 declares an Image class that simulates loading a large image, and an ImageCache class that
demonstrates the SoftReference-based caching of an Image object.
The main() method first creates an Image instance by calling the getImage() class method; the
instance’s private image array occupies 100MB of memory.
253
CHAPTER 4  TOURING LANGUAGE APIS
main() next creates a SoftReference object that is initialized to an Image object’s reference, and
clears the strong reference to the Image object by assigning null to image. If this strong reference is not
removed, the Image object will be cached always and the application will most likely run out of memory.
After creating a byte array that’s used to demonstrate SoftReference, main() enters the application’s
main loop, which keeps looping as long as cache.get() returns a nonnull reference (the Image object is
still in the cache). For each loop iteration, main() outputs a message stating that the Image object is still
cached, and doubles the size of the byte array.
At some point, the array doubling will exhaust the heap space. However, before it throws an
instance of the java.lang.OutOfMemoryError class, the Java Virtual Machine (JVM) will attempt to obtain
sufficient memory by clearing the SoftReference object’s Image reference, and removing the Image object
from the heap.
The next loop iteration will detect this situation by discovering that get() returns null. The loop
ends and main() outputs a suitable message confirming that the Image object is no longer cached.
main() now assigns null to b to ensure that there will be sufficient memory to reload the large image
(via getImage()) and once again store it in a SoftReference-based cache.
Finally, main() enters a finite loop to demonstrate that the reloaded Image object is still in the cache.
Compile Listing 4-8 (javac ImageCache.java) and run the application (java ImageCache). You should
discover output that’s similar to that shown here:
caching image
image is still cached
image is still cached
image is still cached
image is still cached
image is still cached
image is no longer cached
reloading and recaching image
image is still cached
image is still cached
image is still cached
image is still cached
image is still cached
image is still cached
WeakReference
The WeakReference class describes a Reference object whose referent is weakly reachable. As well as
inheriting Reference’s methods, this generic class provides the following constructors for initializing a
WeakReference object:
•
WeakReference(T r) encapsulates r’s reference. The WeakReference object behaves
as a weak reference to r. No ReferenceQueue object is associated with this
WeakReference object.
•
WeakReference(T r, ReferenceQueue<? super T> q) encapsulates r’s reference.
The WeakReference object behaves as a weak reference to r. The ReferenceQueue
object identified by q is associated with this WeakReference object. Passing null to
q indicates a weak reference without a queue.
WeakReference is useful for preventing memory leaks related to hashmaps. A memory leak occurs
when you keep adding objects to a hashmap and never remove them. The objects remain in memory
because the hashmap stores strong references to them.
254
CHAPTER 4  TOURING LANGUAGE APIS
Ideally, the objects should only remain in memory when they are strongly referenced from
elsewhere in the application. When an object’s last strong reference (apart from hashmap strong
references) disappears, the object should be garbage collected.
This situation can be remedied by storing weak references to hashmap entries so they are discarded
when no strong references to their keys exist. Java’s java.util.WeakHashMap class (discussed in Chapter
5), whose private Entry static member class extends WeakReference, accomplishes this task.
■ Note Reference queues are more useful with WeakReference than they are with SoftReference. In the context
of WeakHashMap, these queues provide notification of weakly referenced keys that have been removed. Code
within WeakHashMap uses the information provided by the queue to remove all hashmap entries that no longer
have valid keys so that the value objects associated with these invalid keys can be garbage collected. However, a
queue associated with SoftReference can alert the application that heap space is beginning to run low.
PhantomReference
The PhantomReference class describes a Reference object whose referent is phantom reachable. As well as
inheriting Reference’s methods and overriding get(), this generic class provides a single constructor for
initializing a PhantomReference object:
•
PhantomReference(T r, ReferenceQueue<? super T> q) encapsulates r’s
reference. The PhantomReference object behaves as a phantom reference to r. The
ReferenceQueue object identified by q is associated with this PhantomReference
object. Passing null to q makes no sense because get() is overridden to return null
and the PhantomReference object will never be enqueued.
Although you cannot access a PhantomReference object’s referent (its get() method returns null),
this class is useful because enqueuing the PhantomReference object signals that the referent has been
finalized but its memory space has not yet been reclaimed. This signal lets you perform cleanup without
using the finalize() method.
The finalize() method is problematic because the garbage collector requires at least two garbage
collection cycles to determine if an object that overrides finalize() can be garbage collected. When the
first cycle detects that the object is eligible for garbage collection, it calls finalize(). Because this
method might perform resurrection (see Chapter 2), which makes the unreachable object reachable, a
second garbage collection cycle is needed to determine if resurrection has happened. This extra cycle
slows down garbage collection.
If finalize() is not overridden, the garbage collector does not need to call that method, and
considers the object to be finalized. Hence, the garbage collector requires only one cycle.
Although you cannot perform cleanup via finalize(), you can still perform cleanup via
PhantomReference. Because there is no way to access the referent (get() returns null), resurrection
cannot happen.
Listing 4-9 shows how you might use PhantomReference to detect the finalization of a large object.
Listing 4-9. Detecting a large object’s finalization
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
255
CHAPTER 4  TOURING LANGUAGE APIS
class LargeObject
{
private byte[] memory = new byte[1024*1024*50]; // 50 megabytes
}
class LargeObjectDemo
{
public static void main(String[] args)
{
ReferenceQueue<LargeObject> rq;
rq = new ReferenceQueue<LargeObject>();
PhantomReference<LargeObject> pr;
pr = new PhantomReference<LargeObject>(new LargeObject(), rq);
byte[] b = new byte[1024];
while (rq.poll() == null)
{
System.out.println("waiting for large object to be finalized");
b = new byte[b.length*10];
}
System.out.println("large object finalized");
System.out.println("pr.get() returns "+pr.get());
}
}
Listing 4-9 declares a LargeObject class whose private memory array occupies 50MB. If your JVM
throws OutOfMemoryError when you run LargeObject, you might need to reduce the array’s size.
The main() method first creates a ReferenceQueue object describing a queue onto which a
PhantomReference object that initially contains a LargeObject reference will be enqueued.
main() next creates the PhantomReference object, passing a reference to a newly created LargeObject
object and a reference to the previously created ReferenceQueue object to the constructor.
After creating a byte array that’s used to demonstrate PhantomReference, main() enters a polling
loop.
The polling loop begins by calling poll() to detect the finalization of the LargeObject object. As long
as this method returns null, meaning that the LargeObject object is still unfinalized, the loop outputs a
message and doubles the size of the byte array.
At some point, heap space will exhaust and the garbage collector will attempt to obtain sufficient
memory, by first clearing the PhantomReference object’s LargeObject reference and finalizing the
LargeObject object prior to its removal from the heap. The PhantomReference object is then enqueued
onto the rq-referenced ReferenceQueue; poll() returns the PhantomReference object.
main() now exits the loop, outputs a message confirming the large object’s finalization, and outputs
pr.get()’s return value, which is null proving that you cannot access a PhantomReference object’s
referent. At this point, any additional cleanup operations related to the finalized object (such as closing a
file that was opened in the file’s constructor but not otherwise closed) could be performed.
Compile Listing 4-9 and run the application. You should see output that’s similar to that shown
here:
waiting for large object
waiting for large object
waiting for large object
waiting for large object
waiting for large object
large object finalized
256
to
to
to
to
to
be
be
be
be
be
finalized
finalized
finalized
finalized
finalized
CHAPTER 4  TOURING LANGUAGE APIS
pr.get() returns null
■ Note For a more useful example of PhantomReference, check out Keith D Gregory’s “Java Reference Objects”
blog post (http://www.kdgregory.com/index.php?page=java.refobj).
Reflection
Chapter 2 referred to reflection (also known as introspection) as a third form of runtime type
identification (RTTI). Java’s Reflection API lets applications learn about loaded classes, interfaces,
enums (a kind of class), and annotation types (a kind of interface). It also lets applications load classes
dynamically, instantiate them, find a class’s fields and methods, access fields, call methods, and perform
other tasks reflectively.
Chapter 3 presented a StubFinder application that used part of the Reflection API to load a class and
identify all the loaded class’s public methods that are annotated with @Stub annotations. This tool is one
example where using reflection is beneficial. Another example is the class browser, a tool that
enumerates the members of a class.
■ Caution Reflection should not be used indiscriminately. Application performance suffers because it takes longer
to perform operations with reflection than without reflection. Also, reflection-oriented code can be harder to read,
and the absence of compile-time type checking can result in runtime failures.
The java.lang package’s Class class is the entry point into the Reflection API, whose types are
mainly stored in the java.lang.reflect package. Class is generically declared as Class<T>, where T
identifies the class, interface, enum, or annotation type that is being modeled by the Class object. T can
be replaced by ? (as in Class<?>) when the type being modeled is unknown.
Table 4-3 describes some of Class’s methods.
Table 4-3. Class Methods
Method
Description
static Class<?>
forName(String typename)
Return the Class object that is associated with typename, which
must include the type’s qualified package name when the type
is part of a package (java.lang.String, for example). If the class
or interface type has not been loaded into memory, this
method takes care of loading (reading the classfile’s contents
into memory), linking (taking these contents and combining
them into the runtime state of the JVM so that they can be
executed), and initializing (setting class fields to default values,
running class initializers, and performing other class
initialization) prior to returning the Class object. This method
257
CHAPTER 4  TOURING LANGUAGE APIS
throws java.lang.ClassNotFoundException when the type
cannot be found, java.lang.LinkageError when an error
occurs during linkage, and
java.lang.ExceptionInInitializerError when an exception
occurs during a class’s static initialization.
258
Annotation[]
getAnnotations()
Return an array (that’s possibly empty) containing all
annotations that are declared for the class represented by this
Class object.
Class<?>[] getClasses()
Return an array containing Class objects representing all
public classes and interfaces that are members of the class
represented by this Class object. This includes public class and
interface members inherited from superclasses, and public
class and interface members declared by the class. This method
returns a zero-length array when this Class object has no
public member classes or interfaces. This method also returns a
zero-length array when this Class object represents a primitive
type, an array class, or void.
Constructor[]
getConstructors()
Return an array containing java.lang.reflect.Constructor
objects representing all public constructors of the class
represented by this Class object. A zero-length array is returned
when the represented class has no public constructors, this
Class object represents an array class, or this Class object
represents a primitive type or void.
Annotation[]
getDeclaredAnnotations()
Return an array containing all annotations that are directly
declared on the class represented by this Class object—
inherited annotations are not included. The returned array
might be empty.
Class<?>[]
getDeclaredClasses()
Return an array of Class objects representing all classes and
interfaces declared as members of the class represented by this
Class object. This includes public, protected, default (package)
access, and private classes and interfaces declared by the class,
but excludes inherited classes and interfaces. This method
returns a zero-length array when the class declares no classes
or interfaces as members, or when this Class object represents
a primitive type, an array class, or void.
Constructor[]
getDeclaredConstructors()
Return an array of Constructor objects representing all
constructors declared by the class represented by this Class
object. These are public, protected, default (package) access,
and private constructors. The returned array’s elements are not
sorted and are not in any order. If the class has a default
constructor, it is included in the returned array. This method
returns a zero-length array when this Class object represents
an interface, a primitive type, an array class, or void.
CHAPTER 4  TOURING LANGUAGE APIS
Field[] getDeclaredFields()
Return an array of java.lang.reflect.Field objects
representing all fields declared by the class or interface
represented by this Class object. This array includes public,
protected, default (package) access, and private fields, but
excludes inherited fields. The returned array’s elements are not
sorted and are not in any order. This method returns a zerolength array when the class/interface declares no fields, or
when this Class object represents a primitive type, an array
class, or void.
Method[]
getDeclaredMethods()
Return an array of java.lang.reflect.Method objects
representing all methods declared by the class or interface
represented by this Class object. This array includes public,
protected, default (package) access, and private methods, but
excludes inherited methods. The elements in the returned array
are not sorted and are not in any order. This method returns a
zero-length array when the class or interface declares no
methods, or when this Class object represents a primitive type,
an array class, or void.
Field[] getFields()
Return an array containing Field objects representing all
public fields of the class or interface represented by this Class
object, including those public fields inherited from
superclasses and superinterfaces. The elements in the returned
array are not sorted and are not in any order. This method
returns a zero-length array when this Class object represents a
class or interface with no accessible public fields, or when this
Class object represents an array class, a primitive type, or void.
Method[] getMethods()
Return an array containing Method objects representing all
public methods of the class or interface represented by this
Class object, including those public methods inherited from
superclasses and superinterfaces. Array classes return all the
public member methods inherited from the Object class. The
elements in the returned array are not sorted and are not in any
order. This method returns a zero-length array when this Class
object represents a class or interface that has no public
methods, or when this Class object represents a primitive type
or void. The class initialization method <clinit> (see Chapter
2) is not included in the returned array.
int getModifiers()
Returns the Java language modifiers for this class or interface,
encoded in an integer. The modifiers consist of the JVM's
constants for public, protected, private, final, static,
abstract and interface; they should be decoded using the
methods of class java.lang.reflect.Modifier.
If the underlying class is an array class, then its public, private
and protected modifiers are the same as those of its
259
CHAPTER 4  TOURING LANGUAGE APIS
This book was purchased by [email protected]
component type. If this Class object represents a primitive type
or void, its public modifier is always true, and its protected and
private modifiers are always false. If this Class object
represents an array class, a primitive type or void, then its final
modifier is always true and its interface modifier is always
false. The values of its other modifiers are not determined by
this specification.
String getName()
Return the name of the class represented by this Class object.
Package getPackage()
Return a Package object—I presented Package earlier in this
chapter—that describes the package in which the class
represented by this Class object is located, or null when the
class is a member of the unnamed package.
Class<? super T>
getSuperclass()
Return the Class object representing the superclass of the
entity (class, interface, primitive type, or void) represented by
this Class object. When the Class object on which this method
is called represents the Object class, an interface, a primitive
type, or void, null is returned. When this object represents an
array class, the Class object representing the Object class is
returned.
boolean isAnnotation()
Return true when this Class object represents an annotation
type. If this method returns true, isInterface() also returns
true because all annotation types are also interfaces.
boolean isEnum()
Return true if and only if this class was declared as an enum in
the source code.
boolean isInterface()
Return true when this Class object represents an interface.
T newInstance()
Create and return a new instance of the class represented by
this Class object. The class is instantiated as if by a new
expression with an empty argument list. The class is initialized
when it has not already been initialized. This method throws
java.lang.IllegalAccessException when the class or its
noargument constructor is not accessible;
java.lang.InstantiationException when this Class object
represents an abstract class, an interface, an array class, a
primitive type, or void, or when the class does not have a
noargument constructor (or when instantiation fails for some
other reason); and ExceptionInInitializerError when
initialization fails because the object threw an exception during
initialization.
Table 4-3’s description of the forName() method reveals one way to obtain a Class object. This
method loads, links, and initializes a class or interface that is not in memory, and returns a Class object
260
CHAPTER 4  TOURING LANGUAGE APIS
that represents the class or interface. Listing 4-10 demonstrates forName() and additional methods
described in this table.
Listing 4-10. Using reflection to decompile a type
import
import
import
import
java.lang.reflect.Constructor;
java.lang.reflect.Field;
java.lang.reflect.Method;
java.lang.reflect.Modifier;
class Decompiler
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java Decompiler classname");
return;
}
try
{
decompileClass(Class.forName(args[0]), 0);
}
catch (ClassNotFoundException cnfe)
{
System.err.println("could not locate "+args[0]);
}
}
static void decompileClass(Class<?> clazz, int indentLevel)
{
indent(indentLevel*3);
System.out.print(Modifier.toString(clazz.getModifiers())+" ");
if (clazz.isEnum())
System.out.println("enum "+clazz.getName());
else
if (clazz.isInterface())
{
if (clazz.isAnnotation())
System.out.print("@");
System.out.println(clazz.getName());
}
else
System.out.println(clazz);
indent(indentLevel*3);
System.out.println("{");
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++)
{
indent(indentLevel*3);
System.out.println("
"+fields[i]);
}
Constructor[] constructors = clazz.getDeclaredConstructors();
261
CHAPTER 4  TOURING LANGUAGE APIS
if (constructors.length != 0 && fields.length != 0)
System.out.println();
for (int i = 0; i < constructors.length; i++)
{
indent(indentLevel*3);
System.out.println("
"+constructors[i]);
}
Method[] methods = clazz.getDeclaredMethods();
if (methods.length != 0 &&
(fields.length != 0 || constructors.length != 0))
System.out.println();
for (int i = 0; i < methods.length; i++)
{
indent(indentLevel*3);
System.out.println("
"+methods[i]);
}
Method[] methodsAll = clazz.getMethods();
if (methodsAll.length != 0 &&
(fields.length != 0 || constructors.length != 0 ||
methods.length != 0))
System.out.println();
if (methodsAll.length != 0)
{
indent(indentLevel*3);
System.out.println("
ALL PUBLIC METHODS");
System.out.println();
}
for (int i = 0; i < methodsAll.length; i++)
{
indent(indentLevel*3);
System.out.println("
"+methodsAll[i]);
}
Class<?>[] members = clazz.getDeclaredClasses();
if (members.length != 0 && (fields.length != 0 ||
constructors.length != 0 || methods.length != 0 ||
methodsAll.length != 0))
System.out.println();
for (int i = 0; i < members.length; i++)
if (clazz != members[i])
{
decompileClass(members[i], indentLevel+1);
if (i != members.length-1)
System.out.println();
}
indent(indentLevel*3);
System.out.println("}");
}
static void indent(int numSpaces)
{
for (int i = 0; i < numSpaces; i++)
System.out.print(' ');
}
262
CHAPTER 4  TOURING LANGUAGE APIS
}
Listing 4-10 presents the source code to a decompiler tool that uses reflection to obtain information
about this tool’s solitary command-line argument, which must be a Java reference type (such as a class).
The decompiler lets you output the type and name information for a class’s fields, constructors,
methods, and nested types; it also lets you output the members of interfaces, enums, and annotation
types.
After verifying that one command-line argument has been passed to this application, main() calls
forName() to try to return a Class object representing the class or interface identified by this argument. If
successful, the returned object’s reference is passed to decompileClass(), which decompiles the type.
forName() throws an instance of the checked ClassNotFoundException class when it cannot locate
the class’s classfile (perhaps the classfile was erased prior to executing the application). It also throws
LinkageError when a class’s classfile is malformed, and ExceptionInInitializerError when a class’s
static initialization fails.
■ Note ExceptionInInitializerError is often thrown as the result of a class initializer throwing an unchecked
exception. For example, the class initializer in the following FailedInitialization class results in
ExceptionInInitializerError because someMethod() throws NullPointerException:
class FailedInitialization
{
static
{
someMethod(null);
}
static void someMethod(String s)
{
int len = s.length(); // s contains null
System.out.println(s+"'s length is "+len+" characters");
}
public static void main(String[] args)
{
}
}
Much of the printing code is concerned with making the output look nice. For example, this code
manages indentation, and only allows a newline character to be output to separate one section from
another; a newline character is not output unless content appears before and after the newline.
Listing 4-10 is recursive in that it invokes decompileClass() for every encountered nested type.
263
CHAPTER 4  TOURING LANGUAGE APIS
Compile Listing 4-10 (javac Decompiler.java) and run this application with java.lang.Boolean as
its solitary command line argument (java Decompiler java.lang.Boolean). You will observe the
following output:
public final class java.lang.Boolean
{
public static final java.lang.Boolean java.lang.Boolean.TRUE
public static final java.lang.Boolean java.lang.Boolean.FALSE
public static final java.lang.Class java.lang.Boolean.TYPE
private final boolean java.lang.Boolean.value
private static final long java.lang.Boolean.serialVersionUID
public java.lang.Boolean(java.lang.String)
public java.lang.Boolean(boolean)
public int java.lang.Boolean.hashCode()
public boolean java.lang.Boolean.equals(java.lang.Object)
public java.lang.String java.lang.Boolean.toString()
public static java.lang.String java.lang.Boolean.toString(boolean)
public static int java.lang.Boolean.compare(boolean,boolean)
public int java.lang.Boolean.compareTo(java.lang.Object)
public int java.lang.Boolean.compareTo(java.lang.Boolean)
public static java.lang.Boolean java.lang.Boolean.valueOf(boolean)
public static java.lang.Boolean java.lang.Boolean.valueOf(java.lang.String)
public boolean java.lang.Boolean.booleanValue()
public static boolean java.lang.Boolean.getBoolean(java.lang.String)
public static boolean java.lang.Boolean.parseBoolean(java.lang.String)
private static boolean java.lang.Boolean.toBoolean(java.lang.String)
ALL PUBLIC METHODS
public int java.lang.Boolean.hashCode()
public boolean java.lang.Boolean.equals(java.lang.Object)
public java.lang.String java.lang.Boolean.toString()
public static java.lang.String java.lang.Boolean.toString(boolean)
public static int java.lang.Boolean.compare(boolean,boolean)
public int java.lang.Boolean.compareTo(java.lang.Object)
public int java.lang.Boolean.compareTo(java.lang.Boolean)
public static java.lang.Boolean java.lang.Boolean.valueOf(boolean)
public static java.lang.Boolean java.lang.Boolean.valueOf(java.lang.String)
public boolean java.lang.Boolean.booleanValue()
public static boolean java.lang.Boolean.getBoolean(java.lang.String)
public static boolean java.lang.Boolean.parseBoolean(java.lang.String)
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException
}
264
CHAPTER 4  TOURING LANGUAGE APIS
The output reveals the difference between calling getDeclaredMethods() and getMethods(). For
example, the output associated with getDeclaredMethods() includes the private toBoolean() method.
Also, the output associated with getMethods() includes Object methods that are not overridden by
Boolean; getClass() is an example.
One of Table 4-3’s methods not demonstrated in Listing 4-10 is newInstance(), which is useful for
instantiating a dynamically loaded class, provided that the class has a noargument constructor.
Suppose you plan to create a viewer application that lets the user view different kinds of files. For
example, the viewer can view the instruction sequence of a disassembled Windows EXE file, the
graphical contents of a PNG file, or the contents of some other file. Furthermore, the user can choose to
view this content in its normal state (disassembly versus graphical image, for example), in an
informational manner (descriptive labels and content; for example, EXE HEADER: MZ), or as a table of
hexadecimal values.
The viewer application will start out with a few viewers, but you plan to add more viewers over time.
You don’t want to integrate the viewer source code with the application source code because you would
have to recompile the application and all of its viewers every time you added a new viewer (for example,
a viewer that lets you view the contents of a Java classfile).
Instead, you create these viewers in a separate project, and distribute their classfiles only. Also, you
design the application to enumerate its currently accessible viewers when the application starts running
(perhaps the viewers are stored in a JAR file), and present this list to the user. When the user selects a
specific viewer from this list, the application loads the viewer’s classfile and instantiates this class via its
Class object. The application can then invoke the object’s methods.
Listing 4-11 presents the Viewer superclass that all viewer classes must extend.
Listing 4-11. Abstracting a viewer
abstract class Viewer
{
enum ViewMode { NORMAL, INFO, HEX };
abstract void view(byte[] content, ViewMode vm);
}
Viewer declares an enum to describe the three viewing modes. It also declares a view() method that
displays the content of its byte array argument according to the viewer mode specified by its vm
argument.
Listing 4-12 presents a Viewer subclass for viewing an EXE file’s contents.
Listing 4-12. A viewer for viewing EXE content
class ViewerEXE extends Viewer
{
@Override
void view(byte[] content, ViewMode vm)
{
switch (vm)
{
case NORMAL:
System.out.println("outputting EXE content normally");
break;
case INFO:
System.out.println("outputting EXE content informationally");
break;
265
CHAPTER 4  TOURING LANGUAGE APIS
case HEX:
System.out.println("outputting EXE content in hexadecimal");
}
}
}
ViewerEXE’s view() method demonstrates using the switch statement to switch on an enum
constant. For brevity, I’ve limited this method to printing messages to standard output. Also, I don’t
present the corresponding ViewPNG class, which has a similar structure.
Listing 4-13 presents an application that dynamically loads ViewerEXE or ViewerPNG, instantiates the
loaded class via newInstance(), and invokes the view() method.
Listing 4-13. Loading, instantiating, and using Viewer subclasses
class ViewerDemo
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage : java ViewerDemo filetype");
System.err.println("example: java ViewerDemo EXE");
return;
}
try
{
Class<?> clazz = Class.forName("Viewer"+args[0]);
Viewer viewer = (Viewer) clazz.newInstance();
viewer.view(null, Viewer.ViewMode.HEX);
}
catch (ClassNotFoundException cnfe)
{
System.err.println("Class not found: "+cnfe.getMessage());
}
catch (IllegalAccessException iae)
{
System.err.println("Illegal access: "+iae.getMessage());
}
catch (InstantiationException ie)
{
System.err.println("Unable to instantiate loaded class");
}
}
}
Assuming that you’ve compiled all source files (javac *.java, for example), execute java
ViewerDemo EXE. You should observe the following output:
outputting EXE content in hexadecimal
If you were to execute java ViewerDemo PNG, you should see similar output.
266
CHAPTER 4  TOURING LANGUAGE APIS
Suppose you attempted to load and instantiate the abstract Viewer class via java ViewerDemo "".
Although this class would load, newInstance() would throw an instance of the InstantiationException
class, and you would see the following output:
Unable to instantiate loaded class
Table 4-3’s descriptions of the getAnnotations() and getDeclaredAnnotations() methods reveal that
each method returns an array of Annotation, an interface that is located in the java.lang.annotation
package. Annotation is the superinterface of Override, SuppressWarnings, and all other annotation types.
Table 4-3’s method descriptions also refer to Constructor, Field, and Method. Instances of these
classes represent a class’s constructors and a class’s or an interface’s fields and methods.
Constructor represents a constructor and is generically declared as Constructor<T>, where T
identifies the class in which the constructor represented by Constructor is declared. Constructor
declares various methods, including the following methods:
•
Annotation[] getDeclaredAnnotations() returns an array of all annotations
declared on the constructor. The returned array has zero length when there are no
annotations.
•
Class<T> getDeclaringClass() returns a Class object that represents the class in
which the constructor is declared.
•
Class[]<?> getExceptionTypes() returns an array of Class objects representing
the types of exceptions listed in the constructor’s throws clause. The returned
array has zero length when there is no throws clause.
•
String getName() returns the constructor’s name.
•
Class[]<?> getParameterTypes() returns an array of Class objects representing
the constructor’s parameters. The returned array has zero length when the
constructor does not declare parameters.
■ Tip If you want to instantiate a class via a constructor that takes arguments, you cannot use Class’s
newInstance() method. Instead, you must use Constructor’s T newInstance(Object... initargs) method to
perform this task. Unlike Class’s newInstance() method, which bypasses the compile-time exception checking
that would otherwise be performed by the compiler, Constructor’s newInstance() method avoids this problem
by wrapping any exception thrown by the constructor in an instance of the
java.lang.reflect.InvocationTargetException class.
Field represents a field and declares various methods, including the following getter methods:
•
Object get(Object object) returns the value of the field for the specified object.
•
boolean getBoolean(Object object) returns the value of the Boolean field for the
specified object.
•
byte getByte(Object object) returns the value of the byte integer field for the
specified object.
267
CHAPTER 4  TOURING LANGUAGE APIS
•
char getChar(Object object) returns the value of the character field for the
specified object.
•
double getDouble(Object object) returns the value of the double precision
floating-point field for the specified object.
•
float getFloat(Object object) returns the value of the floating-point field for the
specified object.
•
int getInt(Object object) returns the value of the integer field for the specified
object.
•
long getLong(Object object) returns the value of the long integer field for the
specified object.
•
short getShort(Object object) returns the value of the short integer field for the
specified object.
get() returns the value of any type of field. In contrast, the other listed methods return the values of
specific types of fields. These methods throw NullPointerException when object is null and the field is
an instance field, IllegalArgumentException when object is not an instance of the class or interface
declaring the underlying field (or not an instance of a subclass or interface implementor), and
IllegalAccessException when the underlying field cannot be accessed (it is private, for example).
Listing 4-14 demonstrates Field’s getInt(Object) method along with its void setInt(Object obj,
int i) counterpart.
Listing 4-14. Reflectively getting and setting the values of instance and class fields
import java.lang.reflect.Field;
class X
{
public int i = 10;
public static final double PI = 3.14;
}
class FieldAccessDemo
{
public static void main(String[] args)
{
try
{
Class<?> clazz = Class.forName("X");
X x = (X) clazz.newInstance();
Field f = clazz.getField("i");
System.out.println(f.getInt(x)); // Output: 10
f.setInt(x, 20);
System.out.println(f.getInt(x)); // Output: 20
f = clazz.getField("PI");
System.out.println(f.getDouble(null)); // Output: 3.14
f.setDouble(x, 20);
System.out.println(f.getDouble(null)); // Never executed
}
catch (Exception e)
268
CHAPTER 4  TOURING LANGUAGE APIS
{
System.err.println(e);
}
}
}
Listing 4-14 declares classes X and FieldAccessDemo. I’ve included X’s source code with
FieldAccessDemo’s source code for convenience. However, you can imagine this source code being
stored in a separate source file.
FieldAccessDemo’s main() method first attempts to load X, and then tries to instantiate this class via
newInstance(). If successful, the instance is assigned to reference variable x.
main() next invokes Class’s Field getField(String name) method to return a Field instance that
represents the public field identified by name, which happens to be i (in the first case) and PI (in the
second case). This method throws java.lang.NoSuchFieldException when the named field doesn’t exist.
Continuing, main() invokes Field’s getInt() and setInt() methods (with an object reference) to get
the instance field’s initial value, change this value to another value, and get the new value. The initial
and new values are output.
At this point, main() demonstrates class field access in a similar manner. However, it passes null to
getInt() and setInt() because an object reference isn’t required to access a class field. Because PI is
declared final, the call to setInt() results in a thrown instance of the IllegalAccessException class.
■ Note I’ve specified catch (Exception e) to avoid having to specify multiple catch blocks. You could also use
multicatch (see Chapter 3) where appropriate.
Method represents a method and declares various methods, including the following methods:
•
int getModifiers() returns a 32-bit integer whose bit fields identify the method’s
reserved word modifiers (such as public, abstract, or static). These bit fields
must be interpreted via the Modifier class. For example, you might specify
(method.getModifiers()&Modifier.ABSTRACT) == Modifier.ABSTRACT to find out if
the method (represented by the Method object whose reference is stored in method)
is abstract—this expression evaluates to true when the method is abstract.
•
Class<?> getReturnType() returns a Class object that represents the method’s
return type.
•
Object invoke(Object receiver, Object... args) calls the method on the object
identified by receiver (which is ignored when the method is a class method),
passing the variable number of arguments identified by args to the called method.
The invoke() method throws NullPointerException when receiver is null and the
method being called is an instance method, IllegalAccessException when the
method is not accessible (it is private, for example), IllegalArgumentException
when an incorrect number of arguments are passed to the method (and other
reasons), and InvocationTargetException when an exception is thrown from the
called method.
•
boolean isVarArgs() returns true when the method is declared to receive a
variable number of arguments.
269
CHAPTER 4  TOURING LANGUAGE APIS
Listing 4-15 demonstrates Method’s invoke(Object, Object...) method.
Listing 4-15. Reflectively invoking instance and class methods
import java.lang.reflect.Method;
This book was purchased by [email protected]
class X
{
public void objectMethod(String arg)
{
System.out.println("Instance method: "+arg);
}
public static void classMethod()
{
System.out.println("Class method");
}
}
class MethodInvocationDemo
{
public static void main(String[] args)
{
try
{
Class<?> clazz = Class.forName("X");
X x = (X) clazz.newInstance();
Class[] argTypes = { String.class };
Method method = clazz.getMethod("objectMethod", argTypes);
Object[] data = { "Hello" };
method.invoke(x, data); // Output: Instance method: Hello
method = clazz.getMethod("classMethod", (Class<?>[]) null);
method.invoke(null, (Object[]) null); // Output: Class method
}
catch (Exception e)
{
System.err.println(e);
}
}
}
Listing 4-15 declares classes X and MethodInvocationDemo. MethodInvocationDemo’s main() method
first attempts to load X, and then tries to instantiate this class via newInstance(). If successful, the
instance is assigned to reference variable x.
main() next creates a one-element Class array that describes the types of objectMethod()’s
parameter list. This array is used in the subsequent call to Class’s Method getMethod(String name,
Class<?>... parameterTypes) method to return a Method object for invoking a public method named
objectMethod with this parameter list. This method throws java.lang.NoSuchMethodException when the
named method doesn’t exist.
Continuing, main() creates an Object array that specifies the data to be passed to the method’s
parameters; in this case, the array consists of a single String argument. It then reflectively invokes
objectMethod() by passing this array along with the object reference stored in x to the invoke() method.
At this point, main() shows you how to reflectively invoke a class method. The (Class<?>[]) and
(Object[]) casts are used to suppress warning messages that have to do with variable numbers of
270
CHAPTER 4  TOURING LANGUAGE APIS
arguments and null references. Notice that the first argument passed to invoke() is null when invoking
a class method.
The java.lang.reflect.AccessibleObject class is the superclass of Constructor, Field, and Method.
This superclass provides methods for reporting a constructor’s, field’s, or method’s accessibility (is it
private?) and making an inaccessible constructor, field, or method accessible. AccessibleObject’s
methods include the following:
•
T getAnnotation(Class<T> annotationType) returns the constructor’s, field’s, or
method’s annotation of the specified type when such an annotation is present;
otherwise, null returns.
•
boolean isAccessible() returns true when the constructor, field, or method is
accessible.
•
boolean isAnnotationPresent(Class<? extends Annotation> annotationType)
returns true when an annotation of the type specified by annotationType has been
declared on the constructor, field, or method. This method takes inherited
annotations into account.
•
void setAccessible(boolean flag) attempts to make an inaccessible constructor,
field, or method accessible when flag is true.
■ Note The java.lang.reflect package also includes an Array class whose class methods make it possible to
reflectively create and access Java arrays.
I previously showed you how to obtain a Class object via Class’s forName() method. Another way to
obtain a Class object is to call Object’s getClass() method on an object reference; for example, Employee
e = new Employee(); Class<? extends Employee> clazz = e.getClass();. The getClass() method does
not throw an exception because the class from which the object was created is already present in
memory.
There is one more way to obtain a Class object, and that is to employ a class literal, which is an
expression consisting of a class name, followed by a period separator, followed by reserved word class.
Examples of class literals include Class<Employee> clazz = Employee.class; and Class<String> clazz
= String.class.
Perhaps you are wondering about how to choose between forName(), getClass(), and a class literal.
To help you make your choice, the following list compares each competitor:
•
forName() is very flexible in that you can dynamically specify any reference type by
its package-qualified name. If the type is not in memory, it is loaded, linked, and
initialized. However, lack of compile-time type safety can lead to runtime failures.
•
getClass() returns a Class object describing the type of its referenced object. If
called on a superclass variable containing a subclass instance, a Class object
representing the subclass type is returned. Because the class is in memory, type
safety is assured.
271
CHAPTER 4  TOURING LANGUAGE APIS
•
A class literal returns a Class object representing its specified class. Class literals
are compact and the compiler enforces type safety by refusing to compile the
source code when it cannot locate the literal’s specified class.
■ Note You can use class literals with primitive types, including void. Examples include int.class,
double.class, and void.class. The returned Class object represents the class identified by a primitive type
wrapper class’s TYPE field or java.lang.Void.TYPE. For example, each of int.class == Integer.TYPE and
void.class == Void.TYPE evaluates to true.
You can also use class literals with primitive type-based arrays. Examples include int[].class and
double[].class. For these examples, the returned Class objects represent Class<int[]> and
Class<double[]>.
String
String is the first predefined reference type presented in this book (in Chapter 1). Instances of this type
represent sequences of characters, or strings.
Unlike other reference types, the Java language treats the String class specially, by providing
syntactic sugar that simplifies working with strings. For example, Java recognizes String favLanguage =
"Java"; as the assignment of string literal "Java" to String variable favLanguage. Without this sugar, you
would have to specify String favLanguage = new String("Java");. The Java language also overloads the
+ and += operators to perform string concatenation.
Table 4-4 describes some of String’s constructors and methods for initializing String objects and
working with strings.
Table 4-4. String Constructors and Methods
272
Method
Description
String(char[] data)
Initialize this String object to the data array’s characters.
Modifying data after initializing this String object has no effect
on the object.
String(String s)
Initialize this String object to s’s string.
char charAt(int index)
Return the character located at the zero-based index in this
String object’s string. This method throws
java.lang.StringIndexOutOfBoundsException when index is
less than 0 or greater than or equal to the length of the string.
String concat(String s)
Return a new String object containing this String object’s
string followed by the s argument’s string.
CHAPTER 4  TOURING LANGUAGE APIS
boolean endsWith(String
suffix)
Return true when this String object’s string ends with the
characters in the suffix argument, when suffix is empty
(contains no characters), or when suffix contains the same
character sequence as this String object’s string. This method
performs a case-sensitive comparison (a is not equal to A, for
example), and throws NullPointerException when suffix is
null.
boolean equals(Object
object)
Return true when object is of type String and this argument’s
string contains the same characters (and in the same order) as
this String object’s string.
boolean
equalsIgnoreCase(String s)
Return true when s and this String object contain the same
characters (ignoring case). This method returns false when the
character sequences differ or when null is passed to s.
int indexOf(int c)
Return the zero-based index of the first occurrence (from the
start of the string to the end of the string) of the character
represented by c in this String object’s string. Return -1 when
this character is not present.
int indexOf(String s)
Return the zero-based index of the first occurrence (from the
start of the string to the end of the string) of s’s character
sequence in this String object’s string. Return -1 when s is not
present. This method throws NullPointerException when s is
null.
String intern()
Search an internal table of String objects for an object whose
string is equal to this String object’s string. This String object’s
string is added to the table when not present. Return the object
contained in the table whose string is equal to this String
object’s string. The same String object is always returned for
strings that are equal.
int lastIndexOf(int c)
Return the zero-based index of the last occurrence (from the
start of the string to the end of the string) of the character
represented by c in this String object’s string. Return -1 when
this character is not present.
int lastIndexOf(String s)
Return the zero-based index of the last occurrence (from the
start of the string to the end of the string) of s’s character
sequence in this String object’s string. Return -1 when s is not
present. This method throws NullPointerException when s is
null.
int length()
Return the number of characters in this String object’s string.
String replace(char oldChar,
Return a new String object whose string matches this String
273
CHAPTER 4  TOURING LANGUAGE APIS
char newChar)
object’s string except that all occurrences of oldChar have been
replaced by newChar.
String[] split(String expr)
Split this String object’s string into an array of String objects
using the regular expression (a string whose pattern [template]
is used to search a string for substrings that match the pattern)
specified by expr as the basis for the split. This method throws
NullPointerException when expr is null and
java.util.regex.PatternSyntaxException when expr’s syntax
is invalid.
boolean startsWith(String
prefix)
Return true when this String object’s string starts with the
characters in the prefix argument, when prefix is empty
(contains no characters), or when prefix contains the same
character sequence as this String object’s string. This method
performs a case-sensitive comparison (a is not equal to A, for
example), and throws NullPointerException when prefix is
null.
String substring(int start)
Return a new String object whose string contains this String
object’s characters beginning with the character located at
start. This method throws StringIndexOutOfBoundsException
when start is negative or greater than the length of this String
object’s string.
char[] toCharArray()
Return a character array that contains the characters in this
String object’s string.
String toLowerCase()
Return a new String object whose string contains this String
object’s characters where uppercase letters have been
converted to lowercase. This String object is returned when it
contains no uppercase letters to convert.
String toUpperCase()
Return a new String object whose string contains this String
object’s characters where lowercase letters have been
converted to uppercase. This String object is returned when it
contains no lowercase letters to convert.
String trim()
Return a new String object that contains this String object’s
string with whitespace characters (characters whose Unicode
values are 32 or less) removed from the start and end of the
string, or this String object if there is no leading/trailing
whitespace.
Table 4-4 reveals a couple of interesting items about String. First, this class’s String(String s)
constructor does not initialize a String object to a string literal. Instead, it behaves similarly to the C++
copy constructor by initializing the String object to the contents of another String object. This behavior
suggests that a string literal is more than what it appears to be.
274
CHAPTER 4  TOURING LANGUAGE APIS
In reality, a string literal is a String object. You can prove this to yourself by executing
System.out.println("abc".length()); and System.out.println("abc" instanceof String);. The first
method call outputs 3, which is the length of the "abc" String object’s string, and the second method call
outputs true ("abc" is a String object).
■ Note String literals are stored in a classfile data structure known as the constant pool. When a class is loaded, a
String object is created for each literal and is stored in an internal table of String objects.
The second interesting item is the intern() method, which interns (stores a unique copy of) a
String object in an internal table of String objects. intern() makes it possible to compare strings via
their references and == or !=. These operators are the fastest way to compare strings, which is especially
valuable when sorting a huge number of strings.
By default, String objects denoted by literal strings ("abc") and string-valued constant expressions
("a"+"bc") are interned in this table, which is why System.out.println("abc" == "a"+"bc"); outputs
true. However, String objects created via String constructors are not interned, which is why
System.out.println("abc" == new String("abc")); outputs false. In contrast,
System.out.println("abc" == new String("abc").intern()); outputs true.
■ Caution Be careful with this string comparison technique (which only compares references) because you can
easily introduce a bug when one of the strings being compared has not been interned. When in doubt, use the
equals() or equalsIgnoreCase() method.
Table 4-4 also reveals the charAt() and length() methods, which are useful for iterating over a
string’s characters. For example, String s = "abc"; for (int i = 0; i < s.length(); i++)
System.out.println(s.charAt(i)); returns each of s’s a, b, and c characters and outputs each character
on a separate line.
Finally, Table 4-4 presents split(), a method that I employed in Chapter 3’s StubFinder application
to split a string’s comma-separated list of values into an array of String objects. This method uses a
regular expression that identifies a sequence of characters around which the string is split. (I discuss
regular expressions in Appendix C.)
■ Note StringIndexOutOfBoundsException and ArrayIndexOutOfBoundsException are sibling classes that
share a common java.lang.IndexOutOfBoundsException superclass.
275
CHAPTER 4  TOURING LANGUAGE APIS
StringBuffer and StringBuilder
String objects are immutable: you cannot modify a String object’s string. The various String methods
that appear to modify the String object actually return a new String object with modified string content
instead. Because returning new String objects is often wasteful, Java provides the
java.lang.StringBuffer and java.lang.StringBuilder classes as a workaround. These classes are
identical apart from the fact that StringBuffer can be used in the context of multiple threads (discussed
later in this chapter), and that StringBuilder is faster than StringBuffer but cannot be used in the
context of multiple threads without explicit synchronization (also discussed later in this chapter).
Table 4-5 describes some of StringBuffer’s constructors and methods for initializing StringBuffer
objects and working with string buffers. StringBuilder’s constructors and methods are identical.
Table 4-5. StringBuffer Constructors and Methods
276
Method
Description
StringBuffer()
Initialize this StringBuffer object to an empty array with an
initial capacity of 16 characters.
StringBuffer(int capacity)
Initialize this StringBuffer object to an empty array with an
initial capacity of capacity characters. This constructor throws
java.lang.NegativeArraySizeException when capacity is
negative.
StringBuffer(String s)
Initialize this StringBuffer object to an array containing s’s
characters. This object’s initial capacity is 16 plus the length of
s. This constructor throws NullPointerException when s is
null.
StringBuffer append(boolean
b)
Append “true” to this StringBuffer object’s array when b is
true and “false” to the array when b is false, and return this
StringBuffer object.
StringBuffer append(char ch)
Append ch’s character to this StringBuffer object’s array, and
return this StringBuffer object.
StringBuffer append(char[]
chars)
Append the characters in the chars array to this StringBuffer
object’s array, and return this StringBuffer object. This
method throws NullPointerException when chars is null.
StringBuffer append(double
d)
Append the string representation of d’s double precision
floating-point value to this StringBuffer object’s array, and
return this StringBuffer object.
StringBuffer append(float f)
Append the string representation of f’s floating-point value to
this StringBuffer object’s array, and return this StringBuffer
object.
CHAPTER 4  TOURING LANGUAGE APIS
StringBuffer append(int i)
Append the string representation of i’s integer value to this
StringBuffer object’s array, and return this StringBuffer
object.
StringBuffer append(long l)
Append the string representation of l’s long integer value to
this StringBuffer object’s array, and return this StringBuffer
object.
StringBuffer append(Object
obj)
Call obj’s toString() method and append the returned string’s
characters to this StringBuffer object’s array. Append “null” to
the array when null is passed to obj. Return this StringBuffer
object.
StringBuffer append(String
s)
Append s’s string to this StringBuffer object’s array. Append
“null” to the array when null is passed to s. Return this
StringBuffer object.
int capacity()
Return the current capacity of this StringBuffer object’s array.
char charAt(int index)
Return the character located at index in this StringBuffer
object’s array. This method throws
StringIndexOutOfBoundsException when index is negative or
greater than or equal to this StringBuffer object’s length.
void ensureCapacity(int min)
Ensure that this StringBuffer object’s capacity is at least that
specified by min. If the current capacity is less than min, a new
internal array is created with greater capacity. The new capacity
is set to the larger of min and the current capacity multiplied by
2, with 2 added to the result. No action is taken when min is
negative or zero.
int length()
Return the number of characters stored in this StringBuffer
object’s array.
StringBuffer reverse()
Return this StringBuffer object with its array contents
reversed.
void setCharAt(int index,
char ch)
Replace the character at index with ch. This method throws
StringIndexOutOfBoundsException when index is negative or
greater than or equal to the length of this StringBuffer object’s
array.
void setLength(int length)
Set the length of this StringBuffer object’s array to length. If
the length argument is less than the current length, the array’s
contents are truncated. If the length argument is greater than
or equal to the current length, sufficient null characters
('\u0000') are appended to the array. This method throws
277
CHAPTER 4  TOURING LANGUAGE APIS
StringIndexOutOfBoundsException when length is negative.
String substring(int start)
Return a new String object that contains all characters in this
StringBuffer object’s array starting with the character located
at start. This method throws
StringIndexOutOfBoundsException when start is less than 0 or
greater than or equal to the length of this StringBuffer object’s
array.
String toString()
Return a new String object whose string equals the contents of
this StringBuffer object’s array.
A StringBuffer or StringBuilder object’s internal array is associated with the concepts of capacity
and length. Capacity refers to the maximum number of characters that can be stored in the array before
the array grows to accommodate additional characters. Length refers to the number of characters that
are already stored in the array.
The toAlignedBinaryString() method presented earlier in this chapter included the following
inefficient loop in its implementation:
int numLeadingZeros = numBits-result.length();
String zerosPrefix = "";
for (int j = 0; j < numLeadingZeros; j++)
zerosPrefix += "0";
This loop is inefficient because each of the iterations creates a StringBuilder object and a String
object. The compiler transforms this code fragment into the following fragment:
int numLeadingZeros = 3;
String zerosPrefix = "";
for (int j = 0; j < numLeadingZeros; j++)
zerosPrefix = new StringBuilder().append(zerosPrefix).append("0").toString();
A more efficient way to code the previous loop involves creating a StringBuffer/StringBuilder
object prior to entering the loop, calling the appropriate append() method in the loop, and calling
toString() after the loop. The following code fragment demonstrates this more efficient scenario:
int numLeadingZeros = 3;
StringBuilder sb = new StringBuilder();
for (int j = 0; j < numLeadingZeros; j++)
sb.append("0");
String zerosPrefix = sb.toString();
■ Caution Avoid using the string concatenation operator in a lengthy loop because it results in the creation of
many unnecessary StringBuilder and String objects.
278
CHAPTER 4  TOURING LANGUAGE APIS
System
The java.lang.System class provides access to system-oriented resources, including standard input,
standard output, and standard error.
System declares in, out, and err class fields that support standard input, standard output, and
standard error, respectively. The first field is of type java.io.InputStream, and the last two fields are of
type java.io.PrintStream. (I will formally introduce these classes in Chapter 8.)
System also declares various static methods, including those methods that are described in Table 46.
Table 4-6. System Methods
Method
Description
void arraycopy(Object src,
int srcPos, Object dest, int
destPos, int length)
Copy the number of elements specified by length from the src
array starting at zero-based offset srcPos into the dest array
starting at zero-based offset destPos. This method throws
NullPointerException when src or dest is null,
ArrayIndexOutOfBoundsException when copying causes access
to data outside array bounds, and
java.lang.ArrayStoreException when an element in the src
array could not be stored into the dest array because of a type
mismatch.
long currentTimeMillis()
Return the current system time in milliseconds since January 1,
1970 00:00:00 UTC.
void gc()
Inform the JVM that now would be a good time to run the
garbage collector. This is only a hint; there is no guarantee that
the garbage collector will run.
String getProperty(String
prop)
Return the value of the system property (platform-specific
attribute, such as a version number) identified by prop or null
when no such property exists.
void runFinalization()
Inform the JVM that now would be a good time to perform any
outstanding object finalizations. This is only a hint; there is no
guarantee that outstanding object finalizations will be
performed.
void setErr(PrintStream err)
Reassign the standard error stream to err. This is equivalent to
specifying, for example, java Application 2>errlog on
Windows XP.
void setIn(InputStream in)
Reassign the standard input stream to in. This is equivalent to
specifying, for example, java Application <input on Windows
XP.
279
CHAPTER 4  TOURING LANGUAGE APIS
void setOut(PrintStream out)
Reassign the standard output stream to out. This is equivalent
to specifying, for example, java Application >output on
Windows XP.
Listing 4-16 demonstrates the arraycopy(), currentTimeMillis(), and getProperty() methods.
Listing 4-16. Experimenting with System methods
This book was purchased by [email protected]
class SystemTasks
{
public static void main(String[] args)
{
int[] grades = { 86, 92, 78, 65, 52, 43, 72, 98, 81 };
int[] gradesBackup = new int[grades.length];
System.arraycopy(grades, 0, gradesBackup, 0, grades.length);
for (int i = 0; i < gradesBackup.length; i++)
System.out.println(gradesBackup[i]);
System.out.println("Current time: "+System.currentTimeMillis());
String[] propNames =
{
"java.vendor.url",
"java.class.path",
"user.home",
"java.class.version",
"os.version",
"java.vendor",
"user.dir",
"user.timezone",
"path.separator",
"os.name",
"os.arch",
"line.separator",
"file.separator",
"user.name",
"java.version",
"java.home"
};
for (int i = 0; i < propNames.length; i++)
System.out.println(propNames[i]+": "+
System.getProperty(propNames[i]));
}
}
Listing 4-16’s main() method begins by demonstrating arraycopy(). It uses this method to copy the
contents of a grades array to a gradesBackup array.
280
CHAPTER 4  TOURING LANGUAGE APIS
■ Tip The arraycopy() method is the fastest portable way to copy one array to another. Also, when you write a
class whose methods return a reference to an internal array, you should use arraycopy() to create a copy of the
array, and then return the copy’s reference. That way, you prevent clients from directly manipulating (and possibly
screwing up) the internal array.
main() next calls currentTimeMillis() to return the current time as a milliseconds value. Because
this value is not human-readable, you might want to use the java.util.Date class (discussed in
Appendix C). The Date() constructor calls currentTimeMillis() and its toString() method converts this
value to a readable date and time.
main() concludes by demonstrating getProperty() in a for loop. This loop iterates over all of Table
4-6’s property names, outputting each name and value.
When I run this application on my platform, it generates the following output:
86
92
78
65
52
43
72
98
81
Current time: 1312236551718
java.vendor.url: http://java.oracle.com/
java.class.path: .
user.home: C:\Documents and Settings\Jeff Friesen
java.class.version: 51.0
os.version: 5.1
java.vendor: Oracle Corporation
user.dir: C:\prj\dev\bj7\ch04\code\SystemTasks
user.timezone:
path.separator: ;
os.name: Windows XP
os.arch: x86
line.separator:
file.separator: \
user.name: Jeff Friesen
java.version: 1.7.0
java.home: C:\Program Files\Java\jdk1.7.0\jre
■ Note line.separator stores the actual line separator character/characters, not its/their representation (such
as \r\n), which is why a blank line appears after line.separator:.
281
CHAPTER 4  TOURING LANGUAGE APIS
Threading
Applications execute via threads, which are independent paths of execution through an application’s
code. When multiple threads are executing, each thread’s path can differ from other thread paths. For
example, a thread might execute one of a switch statement’s cases, and another thread might execute
another of this statement’s cases.
■ Note Applications use threads to improve performance. Some applications can get by with only the default main
thread to carry out their tasks, but other applications need additional threads to perform time-intensive tasks in the
background, so that they remain responsive to their users.
The JVM gives each thread its own method-call stack to prevent threads from interfering with each
other. Separate stacks let threads keep track of their next instructions to execute, which can differ from
thread to thread. The stack also provides a thread with its own copy of method parameters, local
variables, and return value.
Java supports threads via its Threading API. This API consists of one interface (Runnable) and four
classes (Thread, ThreadGroup, ThreadLocal, and InheritableThreadLocal) in the java.lang package. After
exploring Runnable and Thread (and mentioning ThreadGroup during this exploration), this section
explores thread synchronization, ThreadLocal, and InheritableThreadLocal.
■ Note Java 5 introduced the java.util.concurrent package as a high-level alternative to the low-level
Threading API. (I will discuss this package in Chapter 6.) Although java.util.concurrent is the preferred API for
working with threads, you should also be somewhat familiar with Threading because it is helpful in simple
threading scenarios. Also, you might have to analyze someone else’s source code that depends on Threading.
Runnable and Thread
Java provides the Runnable interface to identify those objects that supply code for threads to execute via
this interface’s solitary void run() method—a thread receives no arguments and returns no value.
Classes implement Runnable to supply this code, and one of these classes is Thread.
Thread provides a consistent interface to the underlying operating system’s threading architecture.
(The operating system is typically responsible for creating and managing threads.) Thread makes it
possible to associate code with threads, as well as start and manage those threads. Each Thread instance
associates with a single thread.
Thread declares several constructors for initializing Thread objects. Some of these constructors take
Runnable arguments: you can supply code to run without having to extend Thread. Other constructors do
not take Runnable arguments: you must extend Thread and override its run() method to supply the code
to run.
For example, Thread(Runnable runnable) initializes a new Thread object to the specified runnable
whose code is to be executed. In contrast, Thread() does not initialize Thread to a Runnable argument.
282
CHAPTER 4  TOURING LANGUAGE APIS
Instead, your Thread subclass provides a constructor that calls Thread(), and the subclass also overrides
Thread’s run() method.
In the absence of an explicit name argument, each constructor assigns a unique default name
(starting with Thread-) to the Thread object. Names make it possible to differentiate threads. In contrast
to the previous two constructors, which choose default names, Thread(String threadName) lets you
specify your own thread name.
Thread also declares methods for starting and managing threads. Table 4-7 describes many of the
more useful methods.
Table 4-7. Thread Methods
Method
Description
static Thread
currentThread()
Return the Thread object associated with the thread that calls
this method.
String getName()
Return the name associated with this Thread object.
Thread.State getState()
Return the state of the thread associated with this Thread
object. The state is identified by the Thread.State enum as one
of BLOCKED (waiting to acquire a lock, discussed later), NEW
(created but not started), RUNNABLE (executing), TERMINATED (the
thread has died), TIMED_WAITING (waiting for a specified amount
of time to elapse), or WAITING (waiting indefinitely).
void interrupt()
Set the interrupt status flag in this Thread object. If the
associated thread is blocked or waiting, clear this flag and wake
up the thread by throwing an instance of the
InterruptedException class.
static boolean interrupted()
Return true when the thread associated with this Thread object
has a pending interrupt request. Clear the interrupt status flag.
boolean isAlive()
Return true to indicate that this Thread object’s associated
thread is alive and not dead. A thread’s lifespan ranges from
just before it is actually started within the start() method to
just after it leaves the run() method, at which point it dies.
boolean isDaemon()
Return true when the thread associated with this Thread object
is a daemon thread, a thread that acts as a helper to a user
thread (nondaemon thread) and dies automatically when the
application’s last nondaemon thread dies so the application
can exit.
boolean isInterrupted()
Return true when the thread associated with this Thread object
has a pending interrupt request.
void join()
The thread that calls this method on this Thread object waits for
the thread associated with this object to die. This method
283
CHAPTER 4  TOURING LANGUAGE APIS
throws InterruptedException when this Thread object’s
interrupt() method is called.
void join(long millis)
The thread that calls this method on this Thread object waits for
the thread associated with this object to die, or until millis
milliseconds have elapsed, whichever happens first. This
method throws InterruptedException when this Thread
object’s interrupt() method is called.
void setDaemon(boolean
isDaemon)
Mark this Thread object’s associated thread as a daemon thread
when isDaemon is true. This method throws
java.lang.IllegalThreadStateException when the thread has
not yet been created and started.
void setName(String
threadName)
Assign threadName’s value to this Thread object as the name of
its associated thread.
static void sleep(long time)
Pause the thread associated with this Thread object for time
milliseconds. This method throws InterruptedException when
this Thread object’s interrupt() method is called while the
thread is sleeping.
void start()
Create and start this Thread object’s associated thread. This
method throws IllegalThreadStateException when the thread
was previously started and is running or has died.
Listing 4-17 introduces you to the Threading API via a main() method that demonstrates Runnable,
Thread(Runnable runnable), currentThread(), getName(), and start().
Listing 4-17. A pair of counting threads
class CountingThreads
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
String name = Thread.currentThread().getName();
int count = 0;
while (true)
System.out.println(name+": "+count++);
}
};
Thread thdA = new Thread(r);
Thread thdB = new Thread(r);
thdA.start();
284
CHAPTER 4  TOURING LANGUAGE APIS
thdB.start();
}
}
According to Listing 4-17, the default main thread that executes main() first instantiates an
anonymous class that implements Runnable. It then creates two Thread objects, initializing each object to
the runnable, and calls Thread’s start() method to create and start both threads. After completing these
tasks, the main thread exits main() and dies.
Each of the two started threads executes the runnable’s run() method. It calls Thread’s
currentThread() method to obtain its associated Thread instance, uses this instance to call Thread’s
getName() method to return its name, initializes count to 0, and enters an infinite loop where it outputs
name and count, and increments count on each iteration.
■ Tip To stop an application that does not end, press the Ctrl and C keys simultaneously (at least on Windows
platforms).
I observe both threads alternating in their execution when I run this application on the Windows XP
platform. Partial output from one run appears here:
Thread-0:
Thread-0:
Thread-0:
Thread-0:
Thread-0:
Thread-0:
Thread-0:
Thread-0:
Thread-1:
Thread-1:
Thread-1:
Thread-1:
0
1
2
3
4
5
6
7
0
1
2
3
The operating system assigns a separate thread to each processor or core so the threads execute
concurrently (at the same time). When a computer does not have enough processors and/or cores, a
thread must wait its turn to use the shared processor/core.
The operating system uses a scheduler (http://en.wikipedia.org/wiki/Scheduling_(computing)) to
determine when a waiting thread executes. The following list identifies three different schedulers:
•
Linux 2.6 through 2.6.22 uses the O(1) scheduler
(http://en.wikipedia.org/wiki/O(1)_scheduler) .
•
Linux 2.6.23 uses the Completely Fair Scheduler
(http://en.wikipedia.org/wiki/Completely_Fair_Scheduler).
•
Windows NT-based operating systems (e.g., NT, 2000, XP, Vista, and 7) use a
multilevel feedback queue scheduler
(http://en.wikipedia.org/wiki/Multilevel_feedback_queue).
285
o
CHAPTER 4  TOURING LANGUAGE APIS
The previous output from the counting threads application resulted from running this application
via Windows XP’s multilevel feedback queue scheduler. Because of this scheduler, both threads take
turns executing.
■ Caution Although this output indicates that the first thread starts executing, never assume that the thread
associated with the Thread object whose start() method is called first is the first thread to execute. While this
might be true of some schedulers, it might not be true of others.
A multilevel feedback queue and many other thread schedulers take the concept of priority (thread
relative importance) into account. They often combine preemptive scheduling (higher priority threads
preempt—interrupt and run instead of—lower priority threads) with round robin scheduling (equal
priority threads are given equal slices of time, which are known as time slices, and take turns executing).
Thread supports priority via its void setPriority(int priority) method (set the priority of this
Thread object’s thread to priority, which ranges from Thread.MIN_PRIORITY to Thread.MAX_PRIORITY—
Thread.NORMAL_PRIORITY identifies the default priority) and int getPriority() method (return the
current priority).
■ Caution Using the setPriority() method can impact an application’s portability across platforms because
different schedulers can handle a priority change in different ways. For example, one platform’s scheduler might
delay lower priority threads from executing until higher priority threads finish. This delaying can lead to indefinite
postponement or starvation because lower priority threads “starve” while waiting indefinitely for their turn to
execute, and this can seriously hurt the application’s performance. Another platform’s scheduler might not
indefinitely delay lower priority threads, improving application performance.
Listing 4-18 refactors Listing 4-17’s main() method to give each thread a nondefault name, and to
put each thread to sleep after outputting name and count.
Listing 4-18. A pair of counting threads revisited
class CountingThreads
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
String name = Thread.currentThread().getName();
int count = 0;
286
CHAPTER 4  TOURING LANGUAGE APIS
while (true)
{
System.out.println(name+": "+count++);
try
{
Thread.sleep(100);
}
catch (InterruptedException ie)
{
}
}
}
};
Thread thdA = new Thread(r);
thdA.setName("A");
Thread thdB = new Thread(r);
thdB.setName("B");
thdA.start();
thdB.start();
}
}
Listing 4-18 reveals that Threads A and B execute Thread.sleep(100); to sleep for 100 milliseconds.
This sleep results in each thread executing more frequently, as the following partial output reveals:
A:
B:
A:
B:
A:
B:
A:
B:
0
0
1
1
2
2
3
3
A thread will occasionally start another thread to perform a lengthy calculation, download a large
file, or perform some other time-consuming activity. After finishing its other tasks, the thread that
started the worker thread is ready to process the results of the worker thread and waits for the worker
thread to finish and die.
It is possible to wait for the worker thread to die by using a while loop that repeatedly calls Thread’s
isAlive() method on the worker thread’s Thread object and sleeps for a certain length of time when this
method returns true. However, Listing 4-19 demonstrates a less verbose alternative: the join() method.
Listing 4-19. Joining the default main thread with a background thread
class JoinDemo
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
287
CHAPTER 4  TOURING LANGUAGE APIS
System.out.println("Worker thread is simulating "+
"work by sleeping for 5 seconds.");
try
{
Thread.sleep(5000);
}
catch (InterruptedException ie)
{
}
System.out.println("Worker thread is dying");
}
};
Thread thd = new Thread(r);
thd.start();
System.out.println("Default main thread is doing work.");
try
{
Thread.sleep(2000);
}
catch (InterruptedException ie)
{
}
System.out.println("Default main thread has finished its work.");
System.out.println("Default main thread is waiting for worker thread "+
"to die.");
try
{
thd.join();
}
catch (InterruptedException ie)
{
}
System.out.println("Main thread is dying");
}
}
Listing 4-19 demonstrates the default main thread starting a worker thread, performing some work,
and then waiting for the worker thread to die by calling join() via the worker thread’s thd object. When
you run this application, you will discover output similar to the following (message order might differ
somewhat):
Default main thread is doing work.
Worker thread is simulating work by sleeping for 5 seconds.
Default main thread has finished its work.
Default main thread is waiting for worker thread to die.
Worker thread is dying
Main thread is dying
Every Thread object belongs to some ThreadGroup object; Thread declares a ThreadGroup
getThreadGroup() method that returns this object. You should ignore thread groups because they are not
that useful. If you need to logically group Thread objects, you should use an array or collection instead.
288
CHAPTER 4  TOURING LANGUAGE APIS
■ Caution Various ThreadGroup methods are flawed. For example, int enumerate(Thread[] threads) will not
include all active threads in its enumeration when its threads array argument is too small to store their Thread
objects. Although you might think that you could use the return value from the int activeCount() method to
properly size this array, there is no guarantee that the array will be large enough because activeCount()’s return
value fluctuates with the creation and death of threads.
However, you should still know about ThreadGroup because of its contribution in handling
exceptions that are thrown while a thread is executing. Listing 4-20 sets the stage for learning about
exception handling by presenting a run() method that attempts to divide an integer by 0, which results
in a thrown java.lang.ArithmeticException instance.
Listing 4-20. Throwing an exception from the run() method
class ExceptionThread
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
int x = 1/0;
}
};
Thread thd = new Thread(r);
thd.start();
}
}
Run this application and you will see an exception trace that identifies the thrown
ArithmeticException:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at ExceptionThread$1.run(ExceptionThread.java:10)
at java.lang.Thread.run(Thread.java:722)
When an exception is thrown out of the run() method, the thread terminates and the following
activities take place:
•
The JVM looks for an instance of Thread.UncaughtExceptionHandler installed via
Thread’s void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler
eh) method. When this handler is found, it passes execution to the instance’s void
uncaughtException(Thread t, Throwable e) method, where t identifies the Thread
object of the thread that threw the exception, and e identifies the thrown
exception or error—perhaps an OutOfMemoryError instance was thrown. If this
method throws an exception/error, the exception/error is ignored by the JVM.
289
CHAPTER 4  TOURING LANGUAGE APIS
•
Assuming that setUncaughtExceptionHandler() was not called to install a handler,
the JVM passes control to the associated ThreadGroup object’s
uncaughtException(Thread t, Throwable e) method. Assuming that ThreadGroup
was not extended, and that its uncaughtException() method was not overridden to
handle the exception, uncaughtException() passes control to the parent
ThreadGroup object’s uncaughtException() method when a parent ThreadGroup is
present. Otherwise, it checks to see if a default uncaught exception handler has
been installed (via Thread’s static void
setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler
handler) method.) If a default uncaught exception handler has been installed, its
uncaughtException() method is called with the same two arguments. Otherwise,
uncaughtException() checks its Throwable argument to determine if it is an
instance of java.lang.ThreadDeath. If so, nothing special is done. Otherwise, as
Listing 4-20’s exception message shows, a message containing the thread's name,
as returned from the thread's getName() method, and a stack backtrace, using the
Throwable argument’s printStackTrace() method, is printed to the standard error
stream.
This book was purchased by [email protected]
Listing 4-21 demonstrates Thread’s setUncaughtExceptionHandler() and
setDefaultUncaughtExceptionHandler() methods.
Listing 4-21. Demonstrating uncaught exception handlers
class ExceptionThread
{
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
int x = 1/0;
}
};
Thread thd = new Thread(r);
Thread.UncaughtExceptionHandler uceh;
uceh = new Thread.UncaughtExceptionHandler()
{
public void uncaughtException(Thread t,
{
System.out.println("Caught throwable
}
};
thd.setUncaughtExceptionHandler(uceh);
uceh = new Thread.UncaughtExceptionHandler()
{
public void uncaughtException(Thread t,
{
System.out.println("Default uncaught
System.out.println("Caught throwable
}
290
Throwable e)
"+e+" for thread "+t);
Throwable e)
exception handler");
"+e+" for thread "+t);
CHAPTER 4  TOURING LANGUAGE APIS
};
thd.setDefaultUncaughtExceptionHandler(uceh);
thd.start();
}
}
When you run this application, you will observe the following output:
Caught throwable java.lang.ArithmeticException: / by zero for thread Thread[Thread-0,5,main]
You also will not see the default uncaught exception handler’s output because the default handler is
not called. To see that output, you must comment out thd.setUncaughtExceptionHandler(uceh);. If you
also comment out thd.setDefaultUncaughtExceptionHandler(uceh);, you will see Listing 4-20’s output.
■ Caution Thread declares several deprecated methods, including stop() (stop an executing thread). These
methods have been deprecated because they are unsafe. Do not use these deprecated methods. (I will show you
how to safely stop a thread later in this chapter.) Also, you should avoid the static void yield() method, which
is intended to switch execution from the current thread to another thread, because it can affect portability and hurt
application performance. Although yield() might switch to another thread on some platforms (which can improve
performance), yield() might only return to the current thread on other platforms (which hurts performance
because the yield() call has only wasted time).
Thread Synchronization
Throughout its execution, each thread is isolated from other threads because it has been given its own
method-call stack. However, threads can still interfere with each other when they access and manipulate
shared data. This interference can corrupt the shared data, and this corruption can cause an application
to fail.
For example, consider a checking account in which a husband and wife have joint access. Suppose
that the husband and wife decide to empty this account at the same time without knowing that the other
is doing the same thing. Listing 4-22 demonstrates this scenario.
Listing 4-22. A problematic checking account
class CheckingAccount
{
private int balance;
CheckingAccount(int initialBalance)
{
balance = initialBalance;
}
boolean withdraw(int amount)
{
if (amount <= balance)
{
try
291
CHAPTER 4  TOURING LANGUAGE APIS
{
Thread.sleep((int)(Math.random()*200));
}
catch (InterruptedException ie)
{
}
balance -= amount;
return true;
}
return false;
}
public static void main(String[] args)
{
final CheckingAccount ca = new CheckingAccount(100);
Runnable r = new Runnable()
{
public void run()
{
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++)
System.out.println (name+" withdraws $10: "+
ca.withdraw(10));
}
};
Thread thdHusband = new Thread(r);
thdHusband.setName("Husband");
Thread thdWife = new Thread(r);
thdWife.setName("Wife");
thdHusband.start();
thdWife.start();
}
}
This application lets more money be withdrawn than is available in the account. For example, the
following output reveals $110 being withdrawn when only $100 is available:
Wife withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Wife withdraws $10: true
Wife withdraws $10: false
Wife withdraws $10: false
292
CHAPTER 4  TOURING LANGUAGE APIS
Wife withdraws $10: false
Wife withdraws $10: false
Wife withdraws $10: false
The reason why more money is withdrawn than is available for withdrawal is that a race condition
exists between the husband and wife threads.
■ Note A race condition is a scenario in which multiple threads update the same object at the same time or nearly
at the same time. Part of the object stores values written to it by one thread, and another part of the object stores
values written to it by another thread.
The race condition exists because the actions of checking the amount for withdrawal to ensure that
it is less than what appears in the balance and deducting the amount from the balance are not atomic
(indivisible) operations. (Although atoms are divisible, atomic is commonly used to refer to something
being indivisible.)
■ Note The Thread.sleep() method call that sleeps for a variable amount of time (up to a maximum of 199
milliseconds) is present so that you can observe more money being withdrawn than is available for withdrawal.
Without this method call, you might have to execute the application hundreds of times (or more) to witness this
problem, because the scheduler might rarely pause a thread between the amount <= balance expression and the
balance -= amount; expression statement—the code executes rapidly.
Consider the following scenario:
•
The Husband thread executes withdraw()’s amount <= balance expression, which
returns true. The scheduler pauses the Husband thread and lets the Wife thread
execute.
•
The Wife thread executes withdraw()’s amount <= balance expression, which
returns true.
•
The Wife thread performs the withdrawal. The scheduler pauses the Wife thread
and lets the Husband thread execute.
•
The Husband thread performs the withdrawal.
This problem can be corrected by synchronizing access to withdraw() so that only one thread at a
time can execute inside this method. You synchronize access at the method level by adding reserved
word synchronized to the method header prior to the method’s return type; for example, synchronized
boolean withdraw(int amount).
As I demonstrate later, you can also synchronize access to a block of statements by specifying
synchronized(object) { /* synchronized statements */ }, where object is an arbitrary object
293
CHAPTER 4  TOURING LANGUAGE APIS
reference. No thread can enter a synchronized method or block until execution leaves the
method/block; this is known as mutual exclusion.
Synchronization is implemented in terms of monitors and locks. A monitor is a concurrency
construct for controlling access to a critical section, a region of code that must execute atomically. It is
identified at the source code level as a synchronized method or a synchronized block.
A lock is a token that a thread must acquire before a monitor allows that thread to execute inside a
monitor’s critical section. The token is released automatically when the thread exits the monitor, to give
another thread an opportunity to acquire the token and enter the monitor.
■ Note A thread that has acquired a lock does not release this lock when it calls one of Thread’s sleep()
methods.
A thread entering a synchronized instance method acquires the lock associated with the object on
which the method is called. A thread entering a synchronized class method acquires the lock associated
with the class’s Class object. Finally, a thread entering a synchronized block acquires the lock associated
with the block’s controlling object.
■ Tip Thread declares a static boolean holdsLock(Object o) method that returns true when the calling
thread holds the monitor lock on object o. You will find this method handy in assertion statements, such as assert
Thread.holdsLock(o);.
The need for synchronization is often subtle. For example, Listing 4-23’s ID utility class declares a
getNextID() method that returns a unique long-based ID, perhaps to be used when generating unique
filenames. Although you might not think so, this method can cause data corruption and return duplicate
values.
Listing 4-23. A utility class for returning unique IDs
class ID
{
private static long nextID = 0;
static long getNextID()
{
return nextID++;
}
}
There are two lack-of-synchronization problems with getNextID(). Because 32-bit JVM
implementations require two steps to update a 64-bit long integer, adding 1 to nextID is not atomic: the
scheduler could interrupt a thread that has only updated half of nextID, which corrupts the contents of
this variable.
294
CHAPTER 4  TOURING LANGUAGE APIS
■ Note Variables of type long and double are subject to corruption when being written to in an unsynchronized
context on 32-bit JVMs. This problem does not occur with variables of type boolean, byte, char, float, int, or
short; each type occupies 32 bits or less.
Assume that multiple threads call getNextID(). Because postincrement (++) reads and writes the
nextID field in two steps, multiple threads might retrieve the same value. For example, thread A executes
++, reading nextID but not incrementing its value before being interrupted by the scheduler. Thread B
now executes and reads the same value.
Both problems can be corrected by synchronizing access to nextID so that only one thread can
execute this method’s code. All that is required is to add synchronized to the method header prior to the
method’s return type; for example, static synchronized int getNextID().
Synchronization is also used to communicate between threads. For example, you might design your
own mechanism for stopping a thread (because you cannot use Thread’s unsafe stop() methods for this
task). Listing 4-24 shows how you might accomplish this task.
Listing 4-24. Attempting to stop a thread
class ThreadStopping
{
public static void main(String[] args)
{
class StoppableThread extends Thread
{
private boolean stopped = false;
@Override
public void run()
{
while(!stopped)
System.out.println("running");
}
void stopThread()
{
stopped = true;
}
}
StoppableThread thd = new StoppableThread();
thd.start();
try
{
Thread.sleep(1000); // sleep for 1 second
}
catch (InterruptedException ie)
{
}
thd.stopThread();
}
}
295
CHAPTER 4  TOURING LANGUAGE APIS
Listing 4-24 introduces a main() method with a local class named StoppableThread that subclasses
Thread. StoppableThread declares a stopped field initialized to false, a stopThread() method that sets
this field to true, and a run() method whose infinite loop checks stopped on each loop iteration to see if
its value has changed to true.
After instantiating StoppableThread, the default main thread starts the thread associated with this
Thread object. It then sleeps for one second and calls StoppableThread’s stop() method before dying.
When you run this application on a single-processor/single-core machine, you will probably observe the
application stopping.
You might not see this stoppage when the application runs on a multiprocessor machine or a
uniprocessor machine with multiple cores. For performance reasons, each processor or core probably
has its own cache with its own copy of stopped. When one thread modifies its copy of this field, the other
thread’s copy of stopped is not changed.
Listing 4-25 refactors Listing 4-24 to guarantee that the application will run correctly on all kinds of
machines.
Listing 4-25. Guaranteed stoppage on a multiprocessor/multicore machine
class ThreadStopping
{
public static void main(String[] args)
{
class StoppableThread extends Thread
{
private boolean stopped = false;
@Override
public void run()
{
while(!isStopped())
System.out.println("running");
}
synchronized void stopThread()
{
stopped = true;
}
private synchronized boolean isStopped()
{
return stopped;
}
}
StoppableThread thd = new StoppableThread();
thd.start();
try
{
Thread.sleep(1000); // sleep for 1 second
}
catch (InterruptedException ie)
{
}
thd.stopThread();
}
}
296
CHAPTER 4  TOURING LANGUAGE APIS
Listing 4-25’s stopThread() and isStopped() methods are synchronized to support thread
communication (between the default main thread that calls stopThread() and the started thread that
executes inside run()). When a thread enters one of these methods, it is guaranteed to access a single
shared copy of the stopped field (not a cached copy).
Synchronization is necessary to support mutual exclusion or mutual exclusion combined with
thread communication. However, there is an alternative to synchronization when the only purpose is to
communicate between threads. This alternative is reserved word volatile, which Listing 4-26
demonstrates.
Listing 4-26. The volatile alternative to synchronization for thread communication
class ThreadStopping
{
public static void main(String[] args)
{
class StoppableThread extends Thread
{
private volatile boolean stopped = false;
@Override
public void run()
{
while(!stopped)
System.out.println("running");
}
void stopThread()
{
stopped = true;
}
}
StoppableThread thd = new StoppableThread();
thd.start();
try
{
Thread.sleep(1000); // sleep for 1 second
}
catch (InterruptedException ie)
{
}
thd.stopThread();
}
}
Listing 4-26 declares stopped to be volatile; threads that access this field will always access a single
shared copy (not cached copies on multiprocessor/multicore machines). As well as generating code that
is less verbose, volatile might offer improved performance over synchronization.
When a field is declared volatile, it cannot also be declared final. If you’re depending on the
semantics (meaning) of volatility, you still get those from a final field. In his “Java theory and practice:
Fixing the Java Memory Model, Part 2” article (http://www.ibm.com/developerworks/library/jjtp03304/), Brian Goetz has this to say about this issue: “The new JMM [Java Memory Model] also seeks
to provide a new guarantee of initialization safety—that as long as an object is properly constructed
(meaning that a reference to the object is not published before the constructor has completed), then all
threads will see the values for its final fields that were set in its constructor, regardless of whether or not
297
CHAPTER 4  TOURING LANGUAGE APIS
synchronization is used to pass the reference from one thread to another. Further, any variables that can
be reached through a final field of a properly constructed object, such as fields of an object referenced by
a final field, are also guaranteed to be visible to other threads as well. This means that if a final field
contains a reference to, say, a LinkedList, in addition to the correct value of the reference being visible
to other threads, also the contents of that LinkedList at construction time would be visible to other
threads without synchronization. The result is a significant strengthening of the meaning of final—that
final fields can be safely accessed without synchronization, and that compilers can assume that final
fields will not change and can therefore optimize away multiple fetches.”
■ Caution You should only use volatile in the context of thread communication. Also, you can only use this
reserved word in the context of field declarations. Although you can declare double and long fields volatile, you
should avoid doing so on 32-bit JVMs because it takes two operations to access a double or long variable’s value,
and mutual exclusion via synchronization is required to access their values safely.
Object’s wait(), notify(), and notifyAll() methods support a form of thread communication
where a thread voluntarily waits for some condition (a prerequisite for continued execution) to arise, at
which time another thread notifies the waiting thread that it can continue. wait() causes its calling
thread to wait on an object’s monitor, and notify() and notifyAll() wake up one or all threads waiting
on the monitor.
■ Caution Because the wait(), notify(), and notifyAll() methods depend on a lock, they cannot be called
from outside of a synchronized method or synchronized block. If you fail to heed this warning, you will encounter a
thrown instance of the java.lang.IllegalMonitorStateException class. Also, a thread that has acquired a lock
releases this lock when it calls one of Object’s wait() methods.
A classic example of thread communication involving conditions is the relationship between a
producer thread and a consumer thread. The producer thread produces data items to be consumed by
the consumer thread. Each produced data item is stored in a shared variable.
Imagine that the threads are not communicating and are running at different speeds. The producer
might produce a new data item and record it in the shared variable before the consumer retrieves the
previous data item for processing. Also, the consumer might retrieve the contents of the shared variable
before a new data item is produced.
To overcome those problems, the producer thread must wait until it is notified that the previously
produced data item has been consumed, and the consumer thread must wait until it is notified that a
new data item has been produced. Listing 4-27 shows you how to accomplish this task via wait() and
notify().
Listing 4-27. The producer-consumer relationship
class PC
298
CHAPTER 4  TOURING LANGUAGE APIS
{
public static void main(String[] args)
{
Shared s = new Shared();
new Producer(s).start();
new Consumer(s).start();
}
}
class Shared
{
private char c = '\u0000';
private boolean writeable = true;
synchronized void setSharedChar(char c)
{
while (!writeable)
try
{
wait();
}
catch (InterruptedException e) {}
this.c = c;
writeable = false;
notify();
}
synchronized char getSharedChar()
{
while (writeable)
try
{
wait();
}
catch (InterruptedException e) {}
writeable = true;
notify();
return c;
}
}
class Producer extends Thread
{
private Shared s;
Producer(Shared s)
{
this.s = s;
}
@Override
public void run()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
synchronized(s)
{
s.setSharedChar(ch);
299
CHAPTER 4  TOURING LANGUAGE APIS
System.out.println(ch+" produced by producer.");
}
}
}
This book was purchased by [email protected]
}
class Consumer extends Thread
{
private Shared s;
Consumer(Shared s)
{
this.s = s;
}
@Override
public void run()
{
char ch;
do
{
synchronized(s)
{
ch = s.getSharedChar();
System.out.println(ch+" consumed by consumer.");
}
}
while (ch != 'Z');
}
}
The application creates a Shared object and two threads that get a copy of the object's reference. The
producer calls the object's setSharedChar() method to save each of 26 uppercase letters; the consumer
calls the object’s getSharedChar() method to acquire each letter.
The writeable instance field tracks two conditions: the producer waiting on the consumer to
consume a data item, and the consumer waiting on the producer to produce a new data item. It helps
coordinate execution of the producer and consumer. The following scenario, where the consumer
executes first, illustrates this coordination:
1.
The consumer executes s.getSharedChar() to retrieve a letter.
2.
Inside of that synchronized method, the consumer calls wait() because
writeable contains true. The consumer now waits until it receives notification
from the producer.
3.
The producer eventually executes s.setSharedChar(ch);.
4.
When the producer enters that synchronized method (which is possible
because the consumer released the lock inside of the wait() method prior to
waiting), the producer discovers writeable’s value to be true and does not call
wait().
5.
300
The producer saves the character, sets writeable to false (which will cause the
producer to wait on the next setSharedChar() call when the consumer has not
consumed the character by that time), and calls notify() to awaken the
consumer (assuming the consumer is waiting).
CHAPTER 4  TOURING LANGUAGE APIS
6.
The producer exits setSharedChar(char c).
7.
The consumer wakes up (and reacquires the lock), sets writeable to true
(which will cause the consumer to wait on the next getSharedChar() call when
the producer has not produced a character by that time), notifies the producer
to awaken that thread (assuming the producer is waiting), and returns the
shared character.
Although the synchronization works correctly, you might observe output (on some platforms) that
shows multiple producing messages before a consuming message. For example, you might see A
produced by producer., followed by B produced by producer., followed by A consumed by consumer., at
the beginning of the application’s output.
This strange output order is caused by the call to setSharedChar() followed by its companion
System.out.println() method call not being atomic, and by the call to getSharedChar() followed by its
companion System.out.println() method call not being atomic. The output order is corrected by
wrapping each of these method call pairs in a synchronized block that synchronizes on the s-referenced
Shared object.
When you run this application, its output should always appear in the same alternating order, as
shown next (only the first few lines are shown for brevity):
A
A
B
B
C
C
D
D
produced
consumed
produced
consumed
produced
consumed
produced
consumed
by
by
by
by
by
by
by
by
producer.
consumer.
producer.
consumer.
producer.
consumer.
producer.
consumer.
■ Caution Never call wait() outside of a loop. The loop tests the condition (!writeable or writeable in the
previous example) before and after the wait() call. Testing the condition before calling wait() ensures liveness. If
this test was not present, and if the condition held and notify() had been called prior to wait() being called, it is
unlikely that the waiting thread would ever wake up. Retesting the condition after calling wait() ensures safety. If
retesting did not occur, and if the condition did not hold after the thread had awakened from the wait() call
(perhaps another thread called notify() accidentally when the condition did not hold), the thread would proceed
to destroy the lock’s protected invariants.
Too much synchronization can be problematic. If you are not careful, you might encounter a
situation where locks are acquired by multiple threads, neither thread holds its own lock but holds the
lock needed by some other thread, and neither thread can enter and later exit its critical section to
release its held lock because some other thread holds the lock to that critical section. Listing 4-28’s
atypical example demonstrates this scenario, which is known as deadlock.
Listing 4-28. A pathological case of deadlock
class Deadlock
301
CHAPTER 4  TOURING LANGUAGE APIS
{
private Object lock1 = new Object();
private Object lock2 = new Object();
void instanceMethod1()
{
synchronized(lock1)
{
synchronized(lock2)
{
System.out.println("first thread in instanceMethod1");
// critical section guarded first by
// lock1 and then by lock2
}
}
}
void instanceMethod2()
{
synchronized(lock2)
{
synchronized(lock1)
{
System.out.println("second thread in instanceMethod2");
// critical section guarded first by
// lock2 and then by lock1
}
}
}
public static void main(String[] args)
{
final Deadlock dl = new Deadlock();
Runnable r1 = new Runnable()
{
@Override
public void run()
{
while(true)
dl.instanceMethod1();
}
};
Thread thdA = new Thread(r1);
Runnable r2 = new Runnable()
{
@Override
public void run()
{
while(true)
dl.instanceMethod2();
}
};
Thread thdB = new Thread(r2);
thdA.start();
thdB.start();
302
CHAPTER 4  TOURING LANGUAGE APIS
}
}
Listing 4-28’s thread A and thread B call instanceMethod1() and instanceMethod2(), respectively, at
different times. Consider the following execution sequence:
1.
Thread A calls instanceMethod1(), obtains the lock assigned to the lock1referenced object, and enters its outer critical section (but has not yet acquired
the lock assigned to the lock2-referenced object).
2.
Thread B calls instanceMethod2(), obtains the lock assigned to the lock2referenced object, and enters its outer critical section (but has not yet acquired
the lock assigned to the lock1-referenced object).
3.
Thread A attempts to acquire the lock associated with lock2. The JVM forces
the thread to wait outside of the inner critical section because thread B holds
that lock.
4.
Thread B attempts to acquire the lock associated with lock1. The JVM forces
the thread to wait outside of the inner critical section because thread A holds
that lock.
5.
Neither thread can proceed because the other thread holds the needed lock.
We have a deadlock situation and the program (at least in the context of the
two threads) freezes up.
Although the previous example clearly identifies a deadlock state, it is often not that easy to detect
deadlock. For example, your code might contain the following circular relationship among various
classes (in several source files):
•
Class A’s synchronized method calls class B’s synchronized method.
•
Class B’s synchronized method calls class C’s synchronized method.
•
Class C’s synchronized method calls class A’s synchronized method.
If thread A calls class A’s synchronized method and thread B calls class C’s synchronized method,
thread B will block when it attempts to call class A’s synchronized method and thread A is still inside of
that method. Thread A will continue to execute until it calls class C’s synchronized method, and then
block. Deadlock results.
■ Note Neither the Java language nor the JVM provides a way to prevent deadlock, and so the burden falls on
you. The simplest way to prevent deadlock from happening is to avoid having either a synchronized method or a
synchronized block call another synchronized method/block. Although this advice prevents deadlock from
happening, it is impractical because one of your synchronized methods/blocks might need to call a synchronized
method in a Java API, and the advice is overkill because the synchronized method/block being called might not
call any other synchronized method/block, so deadlock would not occur.
303
CHAPTER 4  TOURING LANGUAGE APIS
You will sometimes want to associate per-thread data (such a user ID) with a thread. Although you
can accomplish this task with a local variable, you can only do so while the local variable exists. You
could use an instance field to keep this data around longer, but then you would have to deal with
synchronization. Thankfully, Java supplies ThreadLocal as a simple (and very handy) alternative.
Each instance of the ThreadLocal class describes a thread-local variable, which is a variable that
provides a separate storage slot to each thread that accesses the variable. You can think of a thread-local
variable as a multi-slot variable in which each thread can store a different value in the same variable.
Each thread sees only its value and is unaware of other threads having their own values in this variable.
ThreadLocal is generically declared as ThreadLocal<T>, where T identifies the type of value that is
stored in the variable. This class declares the following constructor and methods:
•
ThreadLocal() creates a new thread-local variable.
•
T get() returns the value in the calling thread’s storage slot. If an entry does not
exist when the thread calls this method, get() calls initialValue().
•
T initialValue() creates the calling thread’s storage slot and stores an initial
(default) value in this slot. The initial value defaults to null. You must subclass
ThreadLocal and override this protected method to provide a more suitable initial
value.
•
void remove() removes the calling thread’s storage slot. If this method is followed
by get() with no intervening set(), get() calls initialValue().
•
void set(T value) sets the value of the calling thread’s storage slot to value.
Listing 4-29 shows you how to use ThreadLocal to associate a different user ID with each of two
threads.
Listing 4-29. Different user IDs for different threads
class ThreadLocalDemo
{
private static volatile ThreadLocal<String> userID =
new ThreadLocal<String>();
public static void main(String[] args)
{
Runnable r = new Runnable()
{
@Override
public void run()
{
String name = Thread.currentThread().getName();
if (name.equals("A"))
userID.set("foxtrot");
else
userID.set("charlie");
System.out.println(name+" "+userID.get());
}
};
Thread thdA = new Thread(r);
thdA.setName("A");
Thread thdB = new Thread(r);
304
CHAPTER 4  TOURING LANGUAGE APIS
thdB.setName("B");
thdA.start();
thdB.start();
}
}
After instantiating ThreadLocal and assigning the reference to a volatile class field named userID
(the field is volatile because it is accessed by different threads, which might execute on a
multiprocessor/multicore machine), the default main thread creates two more threads that store
different String objects in userID and output their objects.
When you run this application, you will observe the following output (possibly not in this order):
A foxtrot
B charlie
Values stored in thread-local variables are not related. When a new thread is created, it gets a new
storage slot containing initialValue()’s value. Perhaps you would prefer to pass a value from a parent
thread, a thread that creates another thread, to a child thread, the created thread. You accomplish this
task with InheritableThreadLocal.
InheritableThreadLocal is a subclass of ThreadLocal. As well as declaring an
InheritableThreadLocal() constructor, this class declares the following protected method:
•
T childValue(T parentValue) calculates the child’s initial value as a function of
the parent’s value at the time the child thread is created. This method is called
from the parent thread before the child thread is started. The method returns the
argument passed to parentValue and should be overridden when another value is
desired.
Listing 4-30 shows you how to use InheritableThreadLocal to pass a parent thread’s Integer object
to a child thread.
Listing 4-30. Different user IDs for different threads
class InheritableThreadLocalDemo
{
private static volatile InheritableThreadLocal<Integer> intVal =
new InheritableThreadLocal<Integer>();
public static void main(String[] args)
{
Runnable rP = new Runnable()
{
@Override
public void run()
{
intVal.set(new Integer(10));
Runnable rC = new Runnable()
{
public void run()
{
Thread thd;
thd = Thread.currentThread();
String name = thd.getName();
System.out.println(name+" "+
305
CHAPTER 4  TOURING LANGUAGE APIS
intVal.get());
}
};
Thread thdChild = new Thread(rC);
thdChild.setName("Child");
thdChild.start();
}
};
new Thread(rP).start();
}
}
After instantiating InheritableThreadLocal and assigning it to a volatile class field named intVal,
the default main thread creates a parent thread, which stores an Integer object containing 10 in intVal.
The parent thread creates a child thread, which accesses intVal and retrieves its parent thread’s Integer
object.
When you run this application, you will observe the following output:
Child 10
BigDecimal
Chapter 2 introduced you to a SavingsAccount class with a balance field. I declared this field to be of type
int, and mentioned that balance represents the number of dollars that can be withdrawn. Alternatively, I
could have stated that balance represents the number of pennies that can be withdrawn.
Perhaps you are wondering why I did not declare balance to be of type double or float. That way,
balance could store values such as 18.26 (18 dollars in the whole number part and 26 pennies in the
fraction part). I did not declare balance to be a double or float for the following reasons:
•
Not all floating-point values that can represent monetary amounts (dollars and
cents) can be stored exactly in memory. For example, 0.1 (which you might use to
represent 10 cents) has no exact storage representation. If you executed double
total = 0.1; for (int i = 0; i < 50; i++) total += 0.1;
System.out.println(total);, you would observe 5.099999999999998 instead of the
correct 5.1 as the output.
•
The result of each floating-point calculation needs to be rounded to the nearest
cent. Failure to do so introduces tiny errors that can cause the final result to differ
from the correct result. Although Math supplies a pair of round() methods that you
might consider using to round a calculation to the nearest cent, these methods
round to the nearest integer (dollar).
Listing 4-31’s InvoiceCalc application demonstrates both problems. However, the first problem is
not serious because it contributes very little to the inaccuracy. The more serious problem occurs from
failing to round to the nearest cent after performing a calculation.
Listing 4-31. Floating-point-based invoice calculations leading to confusing results
import java.text.NumberFormat;
class InvoiceCalc
{
306
CHAPTER 4  TOURING LANGUAGE APIS
final static double DISCOUNT_PERCENT = 0.1; // 10%
final static double TAX_PERCENT = 0.05; // 5%
public static void main(String[] args)
{
double invoiceSubtotal = 285.36;
double discount = invoiceSubtotal*DISCOUNT_PERCENT;
double subtotalBeforeTax = invoiceSubtotal-discount;
double salesTax = subtotalBeforeTax*TAX_PERCENT;
double invoiceTotal = subtotalBeforeTax+salesTax;
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
System.out.println("Subtotal: "+currencyFormat.format(invoiceSubtotal));
System.out.println("Discount: "+currencyFormat.format(discount));
System.out.println("SubTotal after discount: "+
currencyFormat.format(subtotalBeforeTax));
System.out.println("Sales Tax: "+currencyFormat.format(salesTax));
System.out.println("Total: "+currencyFormat.format(invoiceTotal));
}
}
Listing 4-31 relies on the NumberFormat class (located in the java.text package) and its format()
method to format a double precision floating-point value into a currency—I discuss NumberFormat in the
Internationalization section of Appendix C. When you run InvoiceCalc, you will discover the following
output:
Subtotal: $285.36
Discount: $28.54
SubTotal after discount: $256.82
Sales Tax: $12.84
Total: $269.67
This output reveals the correct subtotal, discount, subtotal after discount, and sales tax. In contrast,
it incorrectly reveals 269.67 instead of 269.66 as the final total. The customer will not appreciate paying
an extra penny, even though 269.67 is the correct value according to the floating-point calculations:
Subtotal: 285.36
Discount: 28.536
SubTotal after discount: 256.824
Sales Tax: 12.8412
Total: 269.6652
The problem arises from not rounding the result of each calculation to the nearest cent before
performing the next calculation. As a result, the 0.024 in 256.824 and 0.0012 in 12.84 contribute to the
final value, causing NumberFormat’s format() method to round this value to 269.67.
■ Caution Never using float or double to represent monetary values.
Java provides a solution to both problems in the form of a BigDecimal class. This immutable class (a
BigDecimal instance cannot be modified) represents a signed decimal number (such as 23.653) of
307
CHAPTER 4  TOURING LANGUAGE APIS
arbitrary precision (number of digits) with an associated scale (an integer that specifies the number of
digits after the decimal point).
BigDecimal declares three convenience constants: ONE, TEN, and ZERO. Each constant is the
BigDecimal equivalent of 1, 10, and 0 with a zero scale.
■ Caution BigDecimal declares several ROUND_-prefixed constants. These constants are largely obsolete and
should be avoided, along with the BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
and BigDecimal setScale(int newScale, int roundingMode) methods, which are still present so that
dependent legacy code continues to compile.
BigDecimal also declares a variety of useful constructors and methods. A few of these constructors
and methods are described in Table 4-8.
Table 4-8. BigDecimal Constructors and Methods
Method
308
Description
BigDecimal(int val)
Initialize the BigDecimal instance to val’s digits. Set the scale
to 0.
BigDecimal(String val)
Initialize the BigDecimal instance to the decimal equivalent
of val. Set the scale to the number of digits after the decimal
point, or 0 if no decimal point is specified. This constructor
throws NullPointerException when val is null, and
NumberFormatException when val’s string representation is
invalid (contains letters, for example).
BigDecimal abs()
Return a new BigDecimal instance that contains the absolute
value of the current instance’s value. The resulting scale is
the same as the current instance’s scale.
BigDecimal add(BigDecimal
augend)
Return a new BigDecimal instance that contains the sum of
the current value and the argument value. The resulting
scale is the maximum of the current and argument scales.
This method throws NullPointerException when augend is
null.
BigDecimal divide(BigDecimal
divisor)
Return a new BigDecimal instance that contains the quotient
of the current value divided by the argument value. The
resulting scale is the difference of the current and argument
scales. It might be adjusted when the result requires more
digits. This method throws NullPointerException when
divisor is null, or ArithmeticException when divisor
represents 0 or the result cannot be represented exactly.
CHAPTER 4  TOURING LANGUAGE APIS
BigDecimal max(BigDecimal
val)
Return either this or val, whichever BigDecimal instance
contains the larger value. This method throws
NullPointerException when val is null.
BigDecimal min(BigDecimal
val)
Return either this or val, whichever BigDecimal instance
contains the smaller value. This method throws
NullPointerException when val is null.
BigDecimal
multiply(BigDecimal
multiplicand)
Return a new BigDecimal instance that contains the product
of the current value and the argument value. The resulting
scale is the sum of the current and argument scales. This
method throws NullPointerException when multiplicand is
null.
BigDecimal negate()
Return a new BigDecimal instance that contains the negative
of the current value. The resulting scale is the same as the
current scale.
int precision()
Return the precision of the current BigDecimal instance.
BigDecimal
remainder(BigDecimal
divisor)
Return a new BigDecimal instance that contains the
remainder of the current value divided by the argument
value. The resulting scale is the difference of the current
scale and the argument scale. It might be adjusted when the
result requires more digits. This method throws
NullPointerException when divisor is null, or
ArithmeticException when divisor represents 0.
int scale()
Return the scale of the current BigDecimal instance.
BigDecimal setScale(int
newScale, RoundingMode
roundingMode)
Return a new BigDecimal instance with the specified scale
and rounding mode. If the new scale is greater than the old
scale, additional zeros are added to the unscaled value. In
this case no rounding is necessary. If the new scale is smaller
than the old scale, trailing digits are removed. If these
trailing digits are not zero, the remaining unscaled value has
to be rounded. For this rounding operation, the specified
rounding mode is used. This method throws
NullPointerException when roundingMode is null, and
ArithmeticException when roundingMode is set to
RoundingMode.ROUND_UNNECESSARY but rounding is necessary
based on the current scale.
BigDecimal
subtract(BigDecimal
subtrahend)
Return a new BigDecimal instance that contains the current
value minus the argument value. The resulting scale is the
maximum of the current and argument scales. This method
throws NullPointerException when subtrahend is null.
309
CHAPTER 4  TOURING LANGUAGE APIS
String toString()
Return a string representation of this BigDecimal instance.
Scientific notation is used when necessary.
Table 4-8 refers to java.math.RoundingMode, which is an enum containing various rounding mode
constants. These constants are described in Table 4-9.
This book was purchased by [email protected]
Table 4-9. RoundingMode Constants
Constant
Description
CEILING
Round toward positive infinity.
DOWN
Round toward zero.
FLOOR
Round toward negative infinity.
HALF_DOWN
Round toward the “nearest neighbor” unless both neighbors
are equidistant, in which case round down.
HALF_EVEN
Round toward the “nearest neighbor” unless both neighbors
are equidistant, in which case, round toward the even
neighbor.
HALF_UP
Round toward “nearest neighbor” unless both neighbors are
equidistant, in which case round up. (This is the rounding
mode commonly taught at school.)
UNNECESSARY
Rounding is not necessary because the requested operation
produces the exact result.
UP
Positive values are rounded toward positive infinity and
negative values are rounded toward negative infinity.
The best way to get comfortable with BigDecimal is to try it out. Listing 4-32 uses this class to
correctly perform the invoice calculations that were presented in Listing 4-31.
Listing 4-32. BigDecimal-based invoice calculations not leading to confusing results
import java.math.BigDecimal;
import java.math.RoundingMode;
class InvoiceCalc
{
public static void main(String[] args)
{
BigDecimal invoiceSubtotal = new BigDecimal("285.36");
BigDecimal discountPercent = new BigDecimal("0.10");
BigDecimal discount = invoiceSubtotal.multiply(discountPercent);
310
CHAPTER 4  TOURING LANGUAGE APIS
discount = discount.setScale(2, RoundingMode.HALF_UP);
BigDecimal subtotalBeforeTax = invoiceSubtotal.subtract(discount);
subtotalBeforeTax = subtotalBeforeTax.setScale(2, RoundingMode.HALF_UP);
BigDecimal salesTaxPercent = new BigDecimal("0.05");
BigDecimal salesTax = subtotalBeforeTax.multiply(salesTaxPercent);
salesTax = salesTax.setScale(2, RoundingMode.HALF_UP);
BigDecimal invoiceTotal = subtotalBeforeTax.add(salesTax);
invoiceTotal = invoiceTotal.setScale(2, RoundingMode.HALF_UP);
System.out.println("Subtotal: "+invoiceSubtotal);
System.out.println("Discount: "+discount);
System.out.println("SubTotal after discount: "+subtotalBeforeTax);
System.out.println("Sales Tax: "+salesTax);
System.out.println("Total: "+invoiceTotal);
}
}
Listing 4-32’s main() method first creates BigDecimal objects invoiceSubtotal and discountPercent
that are initialized to 285.36 and 0.10, respectively. It then multiplies invoiceSubtotal by
discountPercent and assigns the BigDecimal result to discount.
At this point, discount contains 28.5360. Apart from the trailing zero, this value is the same as that
generated by invoiceSubtotal*DISCOUNT_PERCENT in Listing 4-31. The value that should be stored in
discount is 28.54. To correct this problem before performing another calculation, main() calls discount’s
setScale() method with these arguments:
•
2: Two digits after the decimal point
•
RoundingMode.HALF_UP: The conventional approach to rounding
After setting the scale and proper rounding mode, main() subtracts discount from invoiceSubtotal,
and assigns the resulting BigDecimal instance to subtotalBeforeTax. main() calls setScale() on
subtotalBeforeTax to properly round its value before moving on to the next calculation.
main() next creates a BigDecimal object named salesTaxPercent that is initialized to 0.05. It then
multiplies subtotalBeforeTax by salesTaxPercent, assigning the result to salesTax, and calls setScale()
on this BigDecimal object to properly round its value.
Moving on, main() adds salesTax to subtotalBeforeTax, saving the result in invoiceTotal, and
rounds the result via setScale(). The values in these objects are sent to the standard output device via
System.out.println(), which calls their toString() methods to return string representations of the
BigDecimal values.
When you run this new version of InvoiceCalc, you will discover the following output:
Subtotal: 285.36
Discount: 28.54
SubTotal after discount: 256.82
Sales Tax: 12.84
Total: 269.66
■ Caution BigDecimal declares a BigDecimal(double val) constructor that you should avoid using if at all
possible. This constructor initializes the BigDecimal instance to the value stored in val, making it possible for this
instance to reflect an invalid representation when the double value cannot be stored exactly. For example,
311
CHAPTER 4  TOURING LANGUAGE APIS
BigDecimal(0.1) results in 0.1000000000000000055511151231257827021181583404541015625 being stored in
the instance. In contrast, BigDecimal("0.1") stores 0.1 exactly.
BigInteger
BigDecimal stores a signed decimal number as an unscaled value with a 32-bit integer scale. The
unscaled value is stored in an instance of the BigInteger class.
BigInteger is an immutable class that represents a signed integer of arbitrary precision. It stores its
value in two’s complement format (all bits are flipped—1s to 0s and 0s to 1s—and 1 is added to the result
to be compatible with the two’s complement format used by Java’s byte integer, short integer, integer,
and long integer types).
■ Note Check out Wikipedia’s “Two’s complement” entry (http://en.wikipedia.org/wiki/Two's_complement)
to learn more about two’s complement.
BigInteger declares three convenience constants: ONE, TEN, and ZERO. Each constant is the
BigInteger equivalent of 1, 10, and 0.
BigInteger also declares a variety of useful constructors and methods. A few of these constructors
and methods are described in Table 4-10.
Table 4-10. BigInteger Constructors and Methods
312
Method
Description
BigInteger(byte[] val)
Initialize the BigInteger instance to the integer that is stored
in the val array, with val[0] storing the integer’s most
significant (leftmost) eight bits. This constructor throws
NullPointerException when val is null, and
NumberFormatException when val.length equals 0.
BigInteger(String val)
Initialize the BigInteger instance to the integer equivalent of
val. This constructor throws NullPointerException when
val is null, and NumberFormatException when val’s string
representation is invalid (contains letters, for example).
BigInteger abs()
Return a new BigInteger instance that contains the absolute
value of the current instance’s value.
BigInteger add(BigInteger
augend)
Return a new BigInteger instance that contains the sum of
the current value and the argument value. This method
throws NullPointerException when augend is null.
CHAPTER 4  TOURING LANGUAGE APIS
BigInteger divide(BigInteger
divisor)
Return a new BigInteger instance that contains the quotient
of the current value divided by the argument value. This
method throws NullPointerException when divisor is null,
and ArithmeticException when divisor represents 0 or the
result cannot be represented exactly.
BigInteger max(BigInteger
val)
Return either this or val, whichever BigInteger instance
contains the larger value. This method throws
NullPointerException when val is null.
BigInteger min(BigInteger
val)
Return either this or val, whichever BigInteger instance
contains the smaller value. This method throws
NullPointerException when val is null.
BigInteger
multiply(BigInteger
multiplicand)
Return a new BigInteger instance that contains the product
of the current value and the argument value. This method
throws NullPointerException when multiplicand is null.
BigInteger negate()
Return a new BigInteger instance that contains the negative
of the current value.
BigInteger
remainder(BigInteger
divisor)
Return a new BigInteger instance that contains the
remainder of the current value divided by the argument
value. This method throws NullPointerException when
divisor is null, and ArithmeticException when divisor
represents 0.
BigInteger
subtract(BigInteger
subtrahend)
Return a new BigInteger instance that contains the current
value minus the argument value. This method throws
NullPointerException when subtrahend is null.
String toString()
Return a string representation of this BigInteger.
■ Note BigInteger also declares several bit-oriented methods, such as BigInteger and (BigInteger val),
BigInteger flipBit(int n), and BigInteger shiftLeft(int n). These methods are useful for when you
need to perform low-level bit manipulation.
The best way to get comfortable with BigInteger is to try it out. Listing 4-33 uses this class in a
factorial() method comparison context.
Listing 4-33. Comparing factorial() methods
import java.math.BigInteger;
313
CHAPTER 4  TOURING LANGUAGE APIS
class FactComp
{
public static void main(String[] args)
{
System.out.println(factorial(12));
System.out.println();
System.out.println(factorial(20L));
System.out.println();
System.out.println(factorial(170.0));
System.out.println();
System.out.println(factorial(new BigInteger("170")));
System.out.println();
System.out.println(factorial(25.0));
System.out.println();
System.out.println(factorial(new BigInteger("25")));
}
static int factorial(int n)
{
if (n == 0)
return 1;
else
return n*factorial(n-1);
}
static long factorial(long n)
{
if (n == 0)
return 1;
else
return n*factorial(n-1);
}
static double factorial(double n)
{
if (n == 1.0)
return 1.0;
else
return n*factorial(n-1);
}
static BigInteger factorial(BigInteger n)
{
if (n.equals(BigInteger.ZERO))
return BigInteger.ONE;
else
return n.multiply(factorial(n.subtract(BigInteger.ONE)));
}
}
Listing 4-33 compares four versions of the recursive factorial() method. This comparison reveals
the largest argument that can be passed to each of the first three methods before the returned factorial
value becomes meaningless, because of limits on the range of values that can be accurately represented
by the numeric type.
314
CHAPTER 4  TOURING LANGUAGE APIS
The first version is based on int and has a useful argument range of 0 through 12. Passing any
argument greater than 12 results in a factorial that cannot be represented accurately as an int.
You can increase the useful range of factorial(), but not by much, by changing the parameter and
return types to long. After making these changes, you will discover that the upper limit of the useful
range is 20.
To further increase the useful range, you might create a version of factorial() whose parameter
and return types are double. This is possible because whole numbers can be represented exactly as
doubles. However, the largest useful argument that can be passed is 170.0. Anything higher than this
value results in factorial() returning +infinity.
It is possible that you might need to calculate a higher factorial value, perhaps in the context of
calculating a statistics problem involving combinations or permutations. The only way to accurately
calculate this value is to use a version of factorial() based on BigInteger.
When you run this application, as in java FactComp, it generates the following output:
479001600
2432902008176640000
7.257415615307994E306
7257415615307998967396728211129263114716991681296451376543577798900561843401706157852350749242
6174595114909912378385207766660225654427530253289007732075109024004302800582956039666125996582
5710439855829425756896631343961226257109494680671120556888045719334021266145280000000000000000
0000000000000000000000000
1.5511210043330986E25
15511210043330985984000000
The first three values represent the highest factorials that can be returned by the int-based, longbased, and double-based factorial() methods. The fourth value represents the BigInteger equivalent of
the highest double factorial.
Notice that the double method fails to accurately represent 170! (! is the math symbol for factorial).
Its precision is simply too small. Although the method attempts to round the smallest digit, rounding
does not always work—the number ends in 7994 instead of 7998. Rounding is only accurate up to
argument 25.0, as the last two output lines reveal.
■ Note RSA encryption, BigDecimal, and factorial are practical examples of BigInteger’s usefulness. However,
you can also use BigInteger in unusual ways. For example, my February 2006 JavaWorld article titled “Travel
Through Time with Java” (http://www.javaworld.com/javaworld/jw-02-2006/jw-0213-funandgames.html), a
part of my Java Fun and Games series, used BigInteger to store an image as a very large integer. The idea was
to experiment with BigInteger methods to look for images (possibly by discovering mathematical patterns) of
people and places that existed in the past, will exist in the future, or might never exist. If this craziness appeals to
you, check out my article.
315
CHAPTER 4  TOURING LANGUAGE APIS
EXERCISES
The following exercises are designed to test your understanding of Java’s language APIs:
316
1.
A prime number is a positive integer greater than 1 that is evenly divisible by 1
and itself. Create a PrimeNumberTest application that determines if its solitary
integer argument is prime or not prime, and outputs a suitable message. For
example, java PrimeNumberTest 289 should output the message 289 is not
prime. A simple way to check for primality is to loop from 2 through the square
root of the integer argument, and use the remainder operator in the loop to
determine if the argument is divided evenly by the loop index. For example,
because 6/2 yields a remainder of 0 (2 divides evenly into 6), integer 6 is not a
prime number.
2.
Reflection is useful in a device driver context, where an application needs to
interact with different versions of a driver. If an older version is detected, the
application invokes its methods. If a newer version is detected, the application can
invoke the older methods or invoke newer versions of those methods. Create two
versions of a Driver class. The first version declares a String
getCapabilities() method that returns "basic capabilities", and the second
version declares this method along with a String getCapabilitiesEx() method
that returns "extended capabilities". Create a DriverDemo class that uses
reflection to determine if the current Driver.class classfile supports
getCapabilitiesEx(), and invoke that method if it does. If the method does not
exist, use reflection to determine if it supports getCapabilities(), and invoke
that method if that is the case. Otherwise, output an error message.
3.
Java arrays have fixed lengths. Create a growable array class, GArray<E>, whose
instances store objects of the type specified by the actual type argument passed
to E. This class declares a GArray(int initCapacity) constructor that creates
an internal array with the number of elements specified by initCapacity. Also,
this class declares E get(int index) and void set(int index, E value)
methods that respectively return the object at the index position within the
internal array, and store the specified value in the array at the index position. The
get() method must throw ArrayIndexOutOfBoundsException when the
argument passed to index is out of range (negative or greater than/equal to the
array’s length). The set() method must throw the same exception when the
argument passed to index is negative. However, when the argument is positive, it
must create a new internal array whose size is twice that of the old array, copy
elements from the old array to the new array via System.arraycopy(), and store
the new value at the index position. This class also declares an int size()
method that returns the array’s size. Test this class with the GArrayDemo
application described in Listing 4-34.
CHAPTER 4  TOURING LANGUAGE APIS
Listing 4-34. Demonstrating a growable array
import ca.tutortutor.collections.GArray;
class GArrayDemo
{
public static void main(String[] args)
{
GArray<String> ga = new GArray<>(10);
System.out.println("Size = "+ga.size());
ga.set(3, "ABC");
System.out.println("Size = "+ga.size());
ga.set(22, "XYZ");
System.out.println("Size = "+ga.size());
System.out.println(ga.get(3));
System.out.println(ga.get(22));
System.out.println(ga.get(20));
ga.set(20, "PQR");
System.out.println(ga.get(20));
System.out.println("Size = "+ga.size());
}
}
When you run this application, it should generate the following output:
Size
Size
Size
ABC
XYZ
null
PQR
Size
= 0
= 4
= 23
= 23
4.
Modify Listing 4-17’s CountingThreads application by marking the two counting
threads as daemon threads. What happens when you run the resulting
application?
5.
Modify Listing 4-17’s CountingThreads application by adding logic to stop both
counting threads when the user presses the Enter key. The default main thread
should call System.in.read() prior to terminating, and assign true to a variable
named stopped after this method call returns. Each counting thread should test
this variable to see if it contains true at the start of each loop iteration, and only
continue the loop when the variable contains false.
Summary
Java’s standard class library provides various language-oriented APIs via the java.lang and java.math
packages. These APIs include Math and StrictMath, Package, Primitive Type Wrapper Class, Reference,
Reflection, String, StringBuffer and StringBuilder, System, Threading, BigDecimal, and BigInteger.
317
CHAPTER 4  TOURING LANGUAGE APIS
The Math and StrictMath classes offer a wide variety of useful math-oriented methods for calculating
trigonometric values, generating pseudorandom numbers, and so on. StrictMath differs from Math by
ensuring that all of these mathematical operations yield the same results on all platforms.
The Package class provides access to package information. This information includes version details
about the implementation and specification of a Java package, the package’s name, and an indication of
whether the package is sealed or not.
Instances of the Boolean, Byte, Character, Double, Float, Integer, Long, and Short primitive type
wrapper classes wrap themselves around values of primitive types. These classes are useful for storing
primitive values in collections, and for providing a good place to associate useful constants (such as
MAX_VALUE and MIN_VALUE) and class methods (such as Integer’s parseInt() methods and Character’s
isDigit(), isLetter(), and toUpperCase() methods) with the primitive types.
The Reference API makes it possible for an application to interact with the garbage collector in
limited ways. This API consists of classes Reference, ReferenceQueue, SoftReference, WeakReference, and
PhantomReference.
SoftReference is useful for implementing image caches, WeakReference is useful for preventing
memory leaks related to hashmaps, and PhantomReference is useful for learning when an object has been
finalized so that its resources can be cleaned up.
The Reflection API lets applications learn about loaded classes, interfaces, enums (a kind of class),
and annotation types (a kind of interface). It also lets applications load classes dynamically, instantiate
them, find a class’s fields and methods, access fields, call methods, and perform other tasks reflectively.
The entry point into the Reflection API is a special class named Class. Additional classes include
Constructor, Field, Method, AccessibleObject, and Array.
The String class represents a string as a sequence of characters. Because instances of this class are
immutable, Java provides StringBuffer and StringBuilder for building a string more efficiently. The
former class can be used in a multithreaded context, whereas the latter class is more performant.
The System class provides access to standard input, standard output, standard error, and other
system-oriented resources. For example, System provides the arraycopy() method as the fastest portable
way to copy one array to another.
Java supports threads via its low-level Threading API. This API consists of one interface (Runnable)
and four classes (Thread, ThreadGroup, ThreadLocal, and InheritableThreadLocal).
Throughout its execution, each thread is isolated from other threads because it has been given its
own method-call stack. However, threads can still interfere with each other when they access and
manipulate shared data. This interference can corrupt the shared data, and this corruption can cause an
application to fail. Java provides a thread-synchronization mechanism to prevent this interference.
Money must never be represented by floating-point and double precision floating-point variables
because not all monetary values can be represented exactly. In contrast, the BigDecimal class lets you
accurately represent and manipulate these values.
BigDecimal relies on the BigInteger class for representing its unscaled value. A BigInteger instance
describes an integer value that can be of arbitrary length (subject to the limits of the JVM’s memory).
This chapter briefly referred to the Collections Framework while introducing the Primitive Type
Wrapper Class API. Chapter 5 introduces you to this broad utility API for collecting objects.
318
CHAPTER 5
Collecting Objects
Applications often must manage collections of objects. Although you can use arrays for this purpose,
they are not always a good choice. For example, arrays have fixed sizes, making it tricky to determine an
optimal size when you need to store a variable number of objects. Also, arrays can be indexed by integers
only, which make them unsuitable for mapping arbitrary objects to other objects.
Java’s standard class library provides the Collections Framework and legacy APIs to manage
collections on behalf of applications. Chapter 5 first presents this framework, and then introduces you to
these legacy APIs (in case you encounter them in legacy code). Because the framework and legacy APIs
may not satisfy specific needs, this chapter lastly focuses on creating special-purpose collections APIs.
■ Note Java’s concurrency utilities (discussed in Chapter 6) extend the Collections Framework.
The Collections Framework
The Collections Framework is a standard architecture for representing and manipulating collections,
which are groups of objects stored in instances of classes designed for this purpose. After presenting an
overview of this framework’s architecture, this section introduces you to the various types (mainly
located in the java.util package) that contribute to this architecture.
Architecture Overview
The Collection Framework’s architecture is divided into three sections:
•
Core interfaces: The framework provides core interfaces for manipulating
collections independently of their implementations.
•
Implementation classes: The framework provides classes that provide different
core interface implementations to address performance and other requirements.
•
Utility classes: The framework provides utility classes whose methods let you sort
arrays, obtain synchronized collections, and perform other operations.
The core interfaces include java.lang.Iterable, Collection, List, Set, SortedSet, NavigableSet,
Queue, Deque, Map, SortedMap, and NavigableMap. Collection extends Iterable; List, Set, and Queue each
extend Collection; SortedSet extends Set; NavigableSet extends SortedSet; Deque extends Queue;
SortedMap extends Map; and NavigableMap extends SortedMap.
319
CHAPTER 5  COLLECTING OBJECTS
Figure 5-1 illustrates the core interfaces hierarchy (arrows point to parent interfaces).
This book was purchased by [email protected]
Figure 5-1. The Collections Framework is based on a hierarchy of core interfaces.
The framework’s implementation classes include ArrayList, LinkedList, TreeSet, HashSet,
LinkedHashSet, EnumSet, PriorityQueue, ArrayDeque, TreeMap, HashMap, LinkedHashMap, IdentityHashMap,
WeakHashMap, and EnumMap. The name of each concrete class ends in a core interface name, identifying the
core interface on which it is based.
■ Note Additional implementation classes are part of the concurrency utilities.
The framework’s implementation classes also include the abstract AbstractCollection,
AbstractList, AbstractSequentialList, AbstractSet, AbstractQueue, and AbstractMap classes. These
classes offer skeletal implementations of the core interfaces to facilitate the creation of concrete
implementation classes.
Finally, the framework provides two utility classes: Arrays and Collections.
Comparable Versus Comparator
A collection implementation stores its elements in some order (arrangement). This order may be
unsorted, or it may be sorted according to some criterion (such as alphabetical, numerical, or
chronological).
A sorted collection implementation defaults to storing its elements according to their natural
ordering. For example, the natural ordering of String objects is lexicographic or dictionary (also known
as alphabetical) order.
A collection cannot rely on equals() to dictate natural ordering because this method can only
determine if two elements are equivalent. Instead, element classes must implement the
java.lang.Comparable<T> interface and its int compareTo(T o) method.
320
CHAPTER 5  COLLECTING OBJECTS
■ Note According to Comparable’s Java documentation, this interface is considered to be part of the Collections
Framework, even though it is a member of the java.lang package.
A sorted collection uses compareTo() to determine the natural ordering of this method’s element
argument o in a collection. compareTo() compares argument o with the current element (which is the
element on which compareTo() was called) and does the following:
•
It returns a negative value when the current element should precede o.
•
It returns a zero value when the current element and o are the same.
•
It returns a positive value when the current element should succeed o.
When you need to implement Comparable’s compareTo() method, there are some rules that you must
follow. These rules, listed next, are similar to those shown in Chapter 2 for implementing the equals()
method:
•
compareTo() must be reflexive: For any nonnull reference value x, x.compareTo(x)
must return 0.
•
compareTo() must be symmetric: For any nonnull reference values x and y,
x.compareTo(y) == -y.compareTo(x) must hold.
•
compareTo() must be transitive: For any nonnull reference values x, y, and z, if
x.compareTo(y) > 0 is true, and if y.compareTo(z) > 0 is true, then x.compareTo(z)
> 0 must also be true.
Also, compareTo() should throw NullPointerException when the null reference is passed to this
method. However, you do not need to check for null because this method throws NullPointerException
when it attempts to access a null reference’s members.
■ Note Before Java 5 and its introduction of generics, compareTo()’s argument was of type java.lang.Object
and had to be cast to the appropriate type before the comparison could be made. The cast operator would throw a
java.lang.ClassCastException instance when the argument’s type was not compatible with the cast.
You might occasionally need to store in a collection objects that are sorted in some order that differs
from their natural ordering. In this case, you would supply a comparator to provide that ordering.
A comparator is an object whose class implements the Comparator interface. This interface, whose
generic type is Comparator<T>, provides the following pair of methods:
•
int compare(T o1, T o2) compares both arguments for order. This method
returns 0 when o1 equals o2, a negative value when o1 is less than o2, and a
positive value when o1 is greater than o2.
321
CHAPTER 5  COLLECTING OBJECTS
•
boolean equals(Object o) returns true when o “equals” this Comparator in that o is
also a Comparator and imposes the same ordering. Otherwise, this method returns
false.
■ Note Comparator declares equals() because this interface places an extra condition on this method’s
contract. Additionally, this method can return true only if the specified object is also a comparator and it imposes
the same ordering as this comparator. You do not have to override Object’s equals() method, but doing so may
improve performance by allowing programs to determine that two distinct comparators impose the same order.
Chapter 3 provided an example that illustrated implementing Comparable, and you will discover
another example later in this chapter. Also, this chapter will present examples of implementing
Comparator.
Iterable and Collection
Most of the core interfaces are rooted in Iterable and its Collection subinterface. Their generic types
are Iterable<T> and Collection<E>.
Iterable describes any object that can return its contained objects in some sequence. This interface
declares an Iterator<T> iterator() method that returns an Iterator instance for iterating over all the
contained objects.
Collection represents a collection of objects that are known as elements. This interface provides
methods that are common to the Collection subinterfaces on which many collections are based. Table
5-1 describes these methods.
Table 5-1. Collection Methods
Method
Description
boolean add(E e)
Add element e to this collection. Return true if this collection
was modified as a result; otherwise, return false. (Attempting
to add e to a collection that does not permit duplicates and
already contains a same-valued element results in e not
being added.) This method throws
java.lang.UnsupportedOperationException when add() is
not supported, ClassCastException when e’s class is not
appropriate for this collection,
java.lang.IllegalArgumentException when some property
of e prevents it from being added to this collection,
java.lang.NullPointerException when e contains the null
reference and this collection does not support null elements,
and java.lang.IllegalStateException when the element
cannot be added at this time because of insertion
restrictions.
IllegalStateException signals that a method has been
322
CHAPTER 5  COLLECTING OBJECTS
invoked at an illegal or inappropriate time. In other words,
the Java environment or Java application is not in an
appropriate state for the requested operation. It is often
thrown when you try to add an element to a bounded queue
(a queue with a maximum length) and the queue is full.
boolean addAll(Collection<?
extends E> c)
Add all elements of collection c to this collection. Return
true if this collection was modified as the result; otherwise,
return false. This method throws
UnsupportedOperationException when this collection does
not support addAll(), ClassCastException when the class of
one of c’s elements is inappropriate for this collection,
IllegalArgumentException when some property of an
element prevents it from being added to this collection,
NullPointerException when c contains the null reference or
when one of its elements is null and this collection does not
support null elements, and IllegalStateException when not
all the elements can be added at this time because of
insertion restrictions.
void clear()
Remove all elements from this collection. This method
throws UnsupportedOperationException when this collection
does not support clear().
boolean contains(Object o)
Return true when this collection contains o; otherwise,
return false. This method throws ClassCastException when
the class of o is inappropriate for this collection, and
NullPointerException when o contains the null reference
and this collection does not support null elements.
boolean
containsAll(Collection<?> c)
Return true when this collection contains all the elements
that are contained in the collection specified by c; otherwise,
return false. This method throws ClassCastException when
the class of one of c’s elements is inappropriate for this
collection, and NullPointerException when c contains the
null reference or when one of its elements is null and this
collection does not support null elements.
boolean equals(Object o)
Compare o with this collection and return true when o
equals this collection; otherwise, return false.
int hashCode()
Return this collection’s hash code. Equal collections have
equal hash codes.
boolean isEmpty()
Return true when this collection contains no elements;
otherwise, return false.
Iterator<E> iterator()
Return an Iterator instance for iterating over all of the
elements contained in this collection. There are no
323
CHAPTER 5  COLLECTING OBJECTS
guarantees concerning the order in which the elements are
returned (unless this collection is an instance of some class
that provides a guarantee). This Iterable method is
redeclared in Collection for convenience.
boolean remove(Object o)
Remove the element identified as o from this collection.
Return true when the element is removed; otherwise, return
false. This method throws UnsupportedOperationException
when this collection does not support remove(),
ClassCastException when the class of o is inappropriate for
this collection, and NullPointerException when o contains
the null reference and this collection does not support null
elements.
boolean
removeAll(Collection<?> c)
Remove all the elements from this collection that are also
contained in collection c. Return true when this collection is
modified by this operation; otherwise, return false. This
method throws UnsupportedOperationException when this
collection does not support removeAll(),
ClassCastException when the class of one of c’s elements is
inappropriate for this collection, and NullPointerException
when c contains the null reference or when one of its
elements is null and this collection does not support null
elements.
boolean
retainAll(Collection<?> c)
Retain all the elements in this collection that are also
contained in collection c. Return true when this collection is
modified by this operation; otherwise, return false. This
method throws UnsupportedOperationException when this
collection does not support retainAll(),
ClassCastException when the class of one of c’s elements is
inappropriate for this collection, and NullPointerException
when c contains the null reference or when one of its
elements is null and this collection does not support null
elements.
int size()
Return the number of elements contained in this collection,
or java.lang.Integer.MAX_VALUE when there are more than
Integer.MAX_VALUE elements contained in the collection.
Object[] toArray()
Return an array containing all the elements stored in this
collection. If this collection makes any guarantees as to what
order its elements are returned in by its iterator, this method
returns the elements in the same order.
The returned array is “safe” in that no references to it are
maintained by this collection. (In other words, this method
allocates a new array even when this collection is backed by
an array.) The caller can safely modify the returned array.
324
CHAPTER 5  COLLECTING OBJECTS
<T> T[] toArray(T[] a)
Return an array containing all the elements in this
collection; the runtime type of the returned array is that of
the specified array. If the collection fits in the specified array,
it is returned in the array. Otherwise, a new array is allocated
with the runtime type of the specified array and the size of
this collection. This method throws NullPointerException
when null is passed to a, and
java.lang.ArrayStoreException when a’s runtime type is
not a supertype of the runtime type of every element in this
collection.
Table 5-1 reveals three exceptional things about various Collection methods. First, some methods
can throw instances of the UnsupportedOperationException class. For example, add() throws
UnsupportedOperationException when you attempt to add an object to an immutable (unmodifiable)
collection (discussed later in this chapter).
Second, some of Collection’s methods can throw instances of the ClassCastException class. For
example, remove() throws ClassCastException when you attempt to remove an entry (also known as
mapping) from a tree-based map whose keys are Strings, but specify a non-String key instead.
Finally, Collection’s add() and addAll() methods throw IllegalArgumentException instances when
some property (attribute) of the element to be added prevents it from being added to this collection. For
example, a third-party collection class’s add() and addAll() methods might throw this exception when
they detect negative Integer values.
■ Note Perhaps you are wondering why remove() is declared to accept any Object argument instead of
accepting only objects whose types are those of the collection. In other words, why is remove() not declared as
boolean remove(E e)? Also, why are containsAll(), removeAll(), and retainAll() not declared with an
argument of type Collection<? extends E>, to ensure that the collection argument only contains elements of
the same type as the collection on which these methods are called? The answer to these questions is the need to
maintain backward compatibility. The Collections Framework was introduced before Java 5 and its introduction of
generics. To let legacy code written before version 5 continue to compile, these four methods were declared with
weaker type constraints.
Iterator and the Enhanced For Statement
By extending Iterable, Collection inherits that interface’s iterator() method, which makes it possible
to iterate over a collection. iterator() returns an instance of a class that implements the Iterator
interface, whose generic type is expressed as Iterator<E> and which declares the following three
methods:
•
boolean hasNext() returns true when this Iterator instance has more elements to
return; otherwise, this method returns false.
325
CHAPTER 5  COLLECTING OBJECTS
•
E next() returns the next element from the collection associated with this
Iterator instance, or throws java.util.NoSuchElementException when there are
no more elements to return.
•
void remove() removes the last element returned by next() from the collection
associated with this Iterator instance. This method can be called only once per
next() call. The behavior of an Iterator instance is unspecified when the
underlying collection is modified while iteration is in progress in any way other
than by calling remove(). This method throws UnsupportedOperationException
when it is not supported by this Iterator, and IllegalStateException when
remove() has been called without a previous call to next() or when multiple
remove() calls occur with no intervening next() calls.
The following example shows you how to iterate over a collection after calling iterator() to return
an Iterator instance:
Collection<String> col = ... // This code does not compile because of the “...”.
// Add elements to col.
Iterator iter = col.iterator();
while (iter.hasNext())
System.out.println(iter.next());
The while loop repeatedly calls the iterator’s hasNext() method to determine whether or not
iteration should continue, and (if it should continue) the next() method to return the next element from
the associated collection.
Because this idiom is commonly used, Java 5 introduced syntactic sugar to the for statement to
simplify iteration in terms of the idiom. This sugar makes this statement appear like the foreach
statement found in languages such as Perl, and is revealed in the following simplified equivalent of the
previous example:
Collection<String> col = ... // This code does not compile because of the “...”.
// Add elements to col.
for (String s: col)
System.out.println(s);
This sugar hides col.iterator(), a method call that returns an Iterator instance for iterating over
col’s elements. It also hides calls to Iterator’s hasNext() and next() methods on this instance. You
interpret this sugar to read as follows: “for each String object in col, assign this object to s at the start of
the loop iteration.”
■ Note The enhanced for statement is also useful in an arrays context, in which it hides the array index variable.
Consider the following example:
String[] verbs = { "run", "walk", "jump" };
for (String verb: verbs)
System.out.println(verb);
326
CHAPTER 5  COLLECTING OBJECTS
This example, which reads as “for each String object in the verbs array, assign that object to verb at the start of
the loop iteration,” is equivalent to the following example:
String[] verbs = { "run", "walk", "jump" };
for (int i = 0; i < verbs.length; i++)
System.out.println(verbs[i]);
The enhanced for statement is limited in that you cannot use this statement where access to the
iterator is required to remove an element from a collection. Also, it is not usable where you must replace
elements in a collection/array during a traversal, and it cannot be used where you must iterate over
multiple collections or arrays in parallel.
■ Tip To have your classes support the enhanced for statement, design these classes to implement the
java.lang.Iterable interface.
Autoboxing and Unboxing
Developers who believe that Java should support only reference types have complained about Java’s
support for primitive types. One area where the dichotomy of Java’s type system is clearly seen is the
Collections Framework: you can store objects but not primitive type-based values in collections.
Although you cannot directly store a primitive type-based value in a collection, you can indirectly
store this value by first wrapping it in an object created from a primitive type wrapper class (see Chapter
4) such as Integer, and then storing this primitive type wrapper class instance in the collection—see the
following example:
Collection<Integer> col = ...; // This code does not compile because of the “...”.
int x = 27;
col.add(new Integer(x)); // Indirectly store int value 27 via an Integer object.
The reverse situation is also tedious. When you want to retrieve the int from col, you must invoke
Integer’s intValue() method (which, if you recall, is inherited from Integer’s java.lang.Number
superclass). Continuing on from this example, you would specify int y =
col.iterator().next().intValue(); to assign the stored 32-bit integer to y.
To alleviate this tedium, Java 5 introduced autoboxing and unboxing, which are a pair of
complementary syntactic sugar-based language features that make primitive values appear more like
objects. (This “sleight of hand” is not complete because you cannot specify expressions such as
27.doubleValue().)
Autoboxing automatically boxes (wraps) a primitive value in an object of the appropriate primitive
type wrapper class whenever a primitive type is specified but a reference is required. For example, you
could change the example’s third line to col.add(x); and have the compiler box x into an Integer object.
327
CHAPTER 5  COLLECTING OBJECTS
Unboxing automatically unboxes (unwraps) a primitive value from its wrapper object whenever a
reference is specified but a primitive type is required. For example, you could specify int y =
col.iterator().next(); and have the compiler unbox the returned Integer object to int value 27 prior
to the assignment.
Although autoboxing and unboxing were introduced to simplify working with primitive values in a
collections context, these language features can be used in other contexts, and this arbitrary use can lead
to a problem that is difficult to understand without knowledge of what is happening behind the scenes.
Consider the following example:
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // Output: true
System.out.println(i1 < i2); // Output: false
System.out.println(i1 > i2); // Output: false
System.out.println(i1+i2); // Output: 254
i1 = 30000;
i2 = 30000;
System.out.println(i1 == i2); // Output: false
System.out.println(i1 < i2); // Output: false
System.out.println(i1 > i2); // Output: false
i2 = 30001;
System.out.println(i1 < i2); // Output: true
System.out.println(i1+i2); // Output: 60001
With one exception, this example’s output is as expected. The exception is the i1 == i2 comparison
where each of i1 and i2 contains 30000. Instead of returning true, as is the case where each of i1 and i2
contains 127, i1 == i2 returns false. What is causing this problem?
Examine the generated code and you will discover that Integer i1 = 127; is converted to Integer
i1 = Integer.valueOf(127); and Integer i2 = 127; is converted to Integer i2 =
Integer.valueOf(127);. According to valueOf()’s Java documentation, this method takes advantage of
caching to improve performance.
■ Note valueOf() is also used when adding a primitive value to a collection. For example, col.add(27) is
converted to col.add(Integer.valueOf(27)).
Integer maintains an internal cache of unique Integer objects over a small range of values. The low
bound of this range is -128, and the high bound defaults to 127. However, you can change the high
bound by assigning a different value to system property java.lang.Integer.IntegerCache.high (via the
java.lang.System class’s String setProperty(String prop, String value) method—I demonstrated
this method’s getProperty() counterpart in Chapter 4).
■ Note Each of Byte, Long, and Short also maintains an internal cache of unique Byte, Long, and Short objects,
respectively.
328
CHAPTER 5  COLLECTING OBJECTS
Because of the cache, each Integer.valueOf(127) call returns the same Integer object reference,
which is why i1 == i2 (which compares references) evaluates to true. Because 30000 lies outside of the
default range, each Integer.valueOf(30000) call returns a reference to a new Integer object, which is
why i1 == i2 evaluates to false.
In contrast to == and !=, which do not unbox the boxed values prior to the comparison, operators
such as <, >, and + unbox these values before performing their operations. As a result, i1 < i2 is
converted to i1.intValue() < i2.intValue() and i1+i2 is converted to i1.intValue()+i2.intValue().
■ Caution Don’t assume that autoboxing and unboxing are used in the context of the == and != operators.
List
A list is an ordered collection, which is also known as a sequence. Elements can be stored in and accessed
from specific locations via integer indexes. Some of these elements may be duplicates or null (when the
list’s implementation allows null elements). Lists are described by the List interface, whose generic type
is List<E>.
List extends Collection and redeclares its inherited methods, partly for convenience. It also
redeclares iterator(), add(), remove(), equals(), and hashCode() to place extra conditions on their
contracts. For example, List’s contract for add() specifies that it appends an element to the end of the
list, rather than adding the element to the collection.
List also declares Table 5-2’s list-specific methods.
Table 5-2. List-specific Methods
Method
Description
void add(int index, E e)
Insert element e into this list at position index. Shift the
element currently at this position (if any) and any
subsequent elements to the right. This method throws
UnsupportedOperationException when this list does not
support add(), ClassCastException when e’s class is
inappropriate for this list, IllegalArgumentException when
some property of e prevents it from being added to this list,
NullPointerException when e contains the null reference
and this list doesn’t support null elements, and
java.lang.IndexOutOfBoundsException when index is less
than 0 or index is greater than size().
boolean addAll(int index,
Collection<? extends E> c)
Insert all c’s elements into this list starting at position index
and in the order that they are returned by c's iterator. Shift
the element currently at this position (if any) and any
subsequent elements to the right. This method throws
UnsupportedOperationException when this list does not
support addAll(), ClassCastException when the class of one
of c’s elements is inappropriate for this list,
IllegalArgumentException when some property of an
329
CHAPTER 5  COLLECTING OBJECTS
This book was purchased by [email protected]
element prevents it from being added to this list,
NullPointerException when c contains the null reference or
when one of its elements is null and this list does not
support null elements, and IndexOutOfBoundsException
when index is less than 0 or index is greater than size().
330
E get(int index)
Return the element stored in this list at position index. This
method throws IndexOutOfBoundsException when index is
less than 0 or index is greater than or equal to size().
int indexOf(Object o)
Return the index of the first occurrence of element o in this
list, or -1 when this list does not contain the element. This
method throws ClassCastException when o’s class is
inappropriate for this list, and NullPointerException when o
contains the null reference and this list does not support null
elements.
int lastIndexOf(Object o)
Return the index of the last occurrence of element o in this
list, or -1 when this list does not contain the element. This
method throws ClassCastException when o’s class is
inappropriate for this list, and NullPointerException when o
contains the null reference and this list does not support null
elements.
ListIterator<E>
listIterator()
Return a list iterator over the elements in this list. The
elements are returned in the same order as they appear in
the list.
ListIterator<E>
listIterator(int index)
Return a list iterator over this list’s elements starting with the
element at index. Elements are returned in the same order as
they appear in the list. IndexOutOfBoundsException is thrown
when index is less than 0 or index is greater than size().
E remove(int index)
Remove the element at position index from this list, shift any
subsequent elements to the left, and return this element.
UnsupportedOperationException is thrown when this list
does not support remove(); IndexOutOfBoundsException is
thrown when index is less than 0, or greater than or equal to
size().
E set(int index, E e)
Replace the element at position index in this list with
element e and return the element previously stored at this
position. This method throws
UnsupportedOperationException when this list does not
support set(), ClassCastException when e’s class is
inappropriate for this list, IllegalArgumentException when
some property of e prevents it from being added to this list,
NullPointerException when e contains the null reference
and this list does not support null elements, and
CHAPTER 5  COLLECTING OBJECTS
IndexOutOfBoundsException when index is less than 0 or
index is greater than or equal to size().
List<E> subList(int
fromIndex, int toIndex)
Return a view (discussed later) of the portion of this list
between fromIndex (inclusive) and toIndex (exclusive). (If
fromIndex and toIndex are equal, the returned list is empty.)
The returned list is backed by this list, so nonstructural
changes in the returned list are reflected in this list and viceversa. The returned list supports all the optional list methods
(those methods that can throw
UnsupportedOperationException) supported by this list. This
method throws IndexOutOfBoundsException when fromIndex
is less than 0, toIndex is greater than size(), or fromIndex is
greater than toIndex.
Table 5-2 refers to the ListIterator interface, which is more flexible than its Iterator
superinterface in that ListIterator provides methods for iterating over a list in either direction,
modifying the list during iteration, and obtaining the iterator’s current position in the list.
■ Note The Iterator and ListIterator instances that are returned by the iterator() and listIterator()
methods in the ArrayList and LinkedList List implementation classes are fail-fast: when a list is structurally
modified (by calling the implementation’s add() method to add a new element, for example) after the iterator is
created, in any way except through the iterator’s own add() or remove() methods, the iterator throws
ConcurrentModificationException. Therefore, in the face of concurrent modification, the iterator fails quickly
and cleanly, rather than risking arbitrary, nondeterministic behavior at some time in the future.
ListIterator declares the following methods:
•
void add(E e) inserts e into the list being iterated over. This element is inserted
immediately before the next element that would be returned by next(), if any, and
after the next element that would be returned by previous(), if any. This method
throws UnsupportedOperationException when this list iterator does not support
add(), ClassCastException when e’s class is inappropriate for this list, and
IllegalArgumentException when some property of e prevents it from being added
to this list.
•
boolean hasNext() returns true when this list iterator has more elements when
traversing the list in the forward direction.
•
boolean hasPrevious() returns true when this list iterator has more elements
when traversing the list in the reverse direction.
•
E next() returns the next element in this list and advances the cursor position.
This method throws NoSuchElementException when there is no next element.
331
CHAPTER 5  COLLECTING OBJECTS
•
int nextIndex() returns the index of the element that would be returned by a
subsequent call to next(), or the size of the list when at the end of the list.
•
E previous() returns the previous element in this list and moves the cursor
position backwards. This method throws NoSuchElementException when there is
no previous element.
•
int previousIndex() returns the index of the element that would be returned by a
subsequent call to previous(), or -1 when at the beginning of the list.
•
void remove() removes from the list the last element that was returned by next()
or previous(). This call can be made only once per call to next() or previous().
Furthermore, it can be made only when add() has not been called after the last call
to next() or previous(). This method throws UnsupportedOperationException
when this list iterator does not support remove(), and IllegalStateException
when neither next() nor previous() has been called, or remove() or add() has
already been called after the last call to next() or previous().
•
void set(E e) replaces the last element returned by next() or previous() with
element e. This call can be made only when neither remove() nor add() has been
called after the last call to next() or previous(). This method throws
UnsupportedOperationException when this list iterator does not support set(),
ClassCastException when e’s class is inappropriate for this list,
IllegalArgumentException when some property of e prevents it from being added
to this list, and IllegalStateException when neither next() nor previous() has
been called, or remove() or add() has already been called after the last call to
next() or previous().
A ListIterator instance does not have the concept of a current element. Instead, it has the concept
of a cursor for navigating through a list. The nextIndex() and previousIndex() methods return the cursor
position, which always lies between the element that would be returned by a call to previous() and the
element that would be returned by a call to next(). A list iterator for a list of length n has n+1 possible
cursor positions, as illustrated by each caret (^) as shown here:
Element(0)
cursor positions:
^
Element(1)
^
Element(2)
^
... Element(n-1)
^
^
The remove() and set() methods are not defined in terms of the cursor position; they are defined to
operate on the last element returned by a call to next() or previous().
■ Note You can mix calls to next() and previous() as long as you are careful. Keep in mind that the first call to
previous() returns the same element as the last call to next(). Furthermore, the first call to next() following a
sequence of calls to previous() returns the same element as the last call to previous().
Table 5-2’s description of the subList() method refers to the concept of a view, which is a list that is
backed by another list. Changes that are made to the view are reflected in this backing list. The view can
cover the entire list or, as subList()’s name implies, only part of the list.
332
CHAPTER 5  COLLECTING OBJECTS
The subList() method is useful for performing range-view operations over a list in a compact
manner. For example, list.subList(fromIndex, toIndex).clear(); removes a range of elements from
list where the first element is located at fromIndex and the last element is located at toIndex-1.
■ Caution A view’s meaning becomes undefined when changes are made to the backing list. Therefore, you
should only use subList() temporarily, whenever you need to perform a sequence of range operations on the
backing list.
ArrayList
The ArrayList class provides a list implementation that is based on an internal array (see Chapters 1 and
2). As a result, access to the list’s elements is fast. However, because elements must be moved to open a
space for insertion or to close a space after deletion, insertions and deletions of elements is slow.
ArrayList supplies three constructors:
•
ArrayList() creates an empty array list with an initial capacity (storage space) of
ten elements. Once this capacity is reached, a larger array is created, elements
from the current array are copied into the larger array, and the larger array
becomes the new current array. This process repeats as more elements are added
to the array list.
•
ArrayList(Collection<? extends E> c) creates an array list containing c’s
elements in the order in which they are returned by c’s iterator.
NullPointerException is thrown when c contains the null reference.
•
ArrayList(int initialCapacity) creates an empty array list with an initial
capacity of initialCapacity elements. IllegalArgumentException is thrown when
initialCapacity is negative.
Listing 5-1 demonstrates an array list.
Listing 5-1. A demonstration of an array-based list
import java.util.ArrayList;
import java.util.List;
class ArrayListDemo
{
public static void main(String[] args)
{
List<String> ls = new ArrayList<>();
String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
for (String weekDay: weekDays)
ls.add(weekDay);
dump("ls:", ls);
ls.set(ls.indexOf("Wed"), "Wednesday");
dump("ls:", ls);
333
CHAPTER 5  COLLECTING OBJECTS
ls.remove(ls.lastIndexOf("Fri"));
dump("ls:", ls);
}
static void dump(String title, List<String> ls)
{
System.out.print(title+" ");
for (String s: ls)
System.out.print(s+" ");
System.out.println();
}
}
The List<String> ls = new ArrayList<>(); assignment reveals a couple of items to note:
•
I’ve declared variable ls to be of List<String> interface type, and have assigned to
this variable a reference to an instance of the ArrayList class that implements this
interface. When working with the Collections Framework, it is common practice to
declare variables to be of interface type. Doing so eliminates extensive code
changes when you need to work with a different implementation class; for
example, List<String> ls = new LinkedList<>();. Check out Chapter 2’s “Why
Use Interfaces?” section for more information about this practice.
•
The diamond operator <> (which is new in Java 7) reduces verbosity by forcing the
compiler to infer actual type arguments for the constructors of generic classes.
Without this operator, I would need to specify String as the actual type argument
passed to ArrayList<E>, resulting in the more verbose List<String> ls = new
ArrayList<String>(); instead of the shorter List<String> ls = new
ArrayList<>();. (I don’t regard the diamond operator as a true operator, which is
why I don’t include it in Chapter 1’s table of operators—Table 1-3.)
The dump() method’s enhanced for statement uses iterator(), hasNext(), and next() behind the
scenes.
When you run this application, it generates the following output:
ls: Sun Mon Tue Wed Thu Fri Sat
ls: Sun Mon Tue Wednesday Thu Fri Sat
ls: Sun Mon Tue Wednesday Thu Sat
LinkedList
The LinkedList class provides a list implementation that is based on linked nodes. Because links must
be traversed, access to the list’s elements is slow. However, because only node references need to be
changed, insertions and deletions of elements is fast. (I will introduce you to nodes later in this chapter.)
LinkedList supplies two constructors:
•
LinkedList() creates an empty linked list.
•
LinkedList(Collection<? extends E> c) creates a linked list containing c’s
elements in the order in which they are returned by c’s iterator.
NullPointerException is thrown when c contains the null reference.
Listing 5-2 demonstrates a linked list.
334
CHAPTER 5  COLLECTING OBJECTS
Listing 5-2. A demonstration of a linked list of nodes
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
class LinkedListDemo
{
public static void main(String[] args)
{
List<String> ls = new LinkedList<>();
String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
for (String weekDay: weekDays)
ls.add(weekDay);
dump("ls:", ls);
ls.add(1, "Sunday");
ls.add(3, "Monday");
ls.add(5, "Tuesday");
ls.add(7, "Wednesday");
ls.add(9, "Thursday");
ls.add(11, "Friday");
ls.add(13, "Saturday");
dump("ls:", ls);
ListIterator<String> li = ls.listIterator(ls.size());
while (li.hasPrevious())
System.out.print(li.previous()+" ");
System.out.println();
}
static void dump(String title, List<String> ls)
{
System.out.print(title+" ");
for (String s: ls)
System.out.print(s+" ");
System.out.println();
}
}
This application demonstrates that each successive add() method call must increase its index by 2
to account for the previously added element when adding longer weekday names to the list. It also
shows you how to output a list in reverse order: return a list iterator with its cursor initialized past the
end of the list and repeatedly call previous().
When you run this application, it generates the following output:
ls: Sun Mon Tue Wed Thu Fri Sat
ls: Sun Sunday Mon Monday Tue Tuesday Wed Wednesday Thu Thursday Fri Friday Sat Saturday
Saturday Sat Friday Fri Thursday Thu Wednesday Wed Tuesday Tue Monday Mon Sunday Sun
Set
A set is a collection that contains no duplicate elements. In other words, a set contains no pair of
elements e1 and e2 such that e1.equals(e2) returns true. Furthermore, a set can contain at most one
null element. Sets are described by the Set interface, whose generic type is Set<E>.
335
CHAPTER 5  COLLECTING OBJECTS
Set extends Collection and redeclares its inherited methods, for convenience and also to add
stipulations to the contracts for add(), equals(), and hashCode(), to address how they behave in a set
context. Also, Set’s documentation states that all constructors of implementation classes must create
sets that contain no duplicate elements.
Set does not introduce new methods.
TreeSet
The TreeSet class provides a set implementation that is based on a tree data structure. As a result,
elements are stored in sorted order. However, accessing these elements is somewhat slower than with
the other Set implementations (which are not sorted) because links must be traversed.
■ Note Check out Wikipedia’s “Tree (data structure)” entry
(http://en.wikipedia.org/wiki/Tree_(data_structure)) to learn about trees.
TreeSet supplies four constructors:
•
TreeSet() creates a new, empty tree set that is sorted according to the natural
ordering of its elements. All elements inserted into the set must implement the
Comparable interface.
•
TreeSet(Collection<? extends E> c) creates a new tree set containing c’s
elements, sorted according to the natural ordering of its elements. All elements
inserted into the new set must implement the Comparable interface. This
constructor throws ClassCastException when c’s elements do not implement
Comparable or are not mutually comparable, and NullPointerException when c
contains the null reference.
•
TreeSet(Comparator<? super E> comparator) creates a new, empty tree set that is
sorted according to the specified comparator. Passing null to comparator implies
that natural ordering will be used.
•
TreeSet(SortedSet<E> s) creates a new tree set containing the same elements and
using the same ordering as s. (I discuss sorted sets later in this chapter.) This
constructor throws NullPointerException when s contains the null reference.
Listing 5-3 demonstrates a tree set.
Listing 5-3. A demonstration of a tree set with String elements sorted according to their natural ordering
import java.util.Set;
import java.util.TreeSet;
class TreeSetDemo
{
public static void main(String[] args)
{
336
CHAPTER 5  COLLECTING OBJECTS
Set<String> ss = new TreeSet<>();
String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis"};
for (String fruit: fruits)
ss.add(fruit);
dump("ss:", ss);
}
static void dump(String title, Set<String> ss)
{
System.out.print(title+" ");
for (String s: ss)
System.out.print(s+" ");
System.out.println();
}
}
Because String implements Comparable, it is legal for this application to use the TreeSet()
constructor to insert the contents of the fruits array into the set.
When you run this application, it generates the following output:
ss: apples bananas grapes kiwis pears
HashSet
The HashSet class provides a set implementation that is backed by a hashtable data structure
(implemented as a HashMap instance, discussed later, which provides a quick way to determine if an
element has already been stored in this structure). Although this class provides no ordering guarantees
for its elements, HashSet is much faster than TreeSet. Furthermore, HashSet permits the null reference to
be stored in its instances.
■ Note Check out Wikipedia’s “Hash table” entry (http://en.wikipedia.org/wiki/Hash_table) to learn about
hashtables.
HashSet supplies four constructors:
•
HashSet() creates a new, empty hashset where the backing HashMap instance has
an initial capacity of 16 and a load factor of 0.75. You will learn what these items
mean when I discuss HashMap later in this chapter.
•
HashSet(Collection<? extends E> c) creates a new hashset containing c’s
elements. The backing HashMap has an initial capacity sufficient to contain c’s
elements and a load factor of 0.75. This constructor throws NullPointerException
when c contains the null reference.
•
HashSet(int initialCapacity) creates a new, empty hashset where the backing
HashMap instance has the capacity specified by initialCapacity and a load factor
of 0.75. This constructor throws IllegalArgumentException when
initialCapacity’s value is less than 0.
337
CHAPTER 5  COLLECTING OBJECTS
•
HashSet(int initialCapacity, float loadFactor) creates a new, empty hashset
where the backing HashMap instance has the capacity specified by initialCapacity
and the load factor specified by loadFactor. This constructor throws
IllegalArgumentException when initialCapacity is less than 0 or when
loadFactor is less than or equal to 0.
Listing 5-4 demonstrates a hashset.
Listing 5-4. A demonstration of a hashset with String elements unordered
import java.util.HashSet;
import java.util.Set;
class HashSetDemo
{
public static void main(String[] args)
{
Set<String> ss = new HashSet<>();
String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis",
"pears", null};
for (String fruit: fruits)
ss.add(fruit);
dump("ss:", ss);
}
static void dump(String title, Set<String> ss)
{
System.out.print(title+" ");
for (String s: ss)
System.out.print(s+" ");
System.out.println();
}
}
In Listing 5-3’s TreeSetDemo application, I did not add null to the fruits array because TreeSet
throws NullPointerException when it detects an attempt to add this element. In contrast, HashSet
permits null to be added, which is why Listing 5-4 includes null in HashSetDemo’s fruits array.
When you run this application, it generates unordered output such as the following:
ss: null grapes bananas kiwis pears apples
Suppose you want to add instances of your classes to a hashset. As with String, your classes must
override equals() and hashCode(); otherwise, duplicate class instances can be stored in the hashset. For
example, Listing 5-5 presents the source code to an application whose Planet class overrides equals()
but fails to also override hashCode().
Listing 5-5. A custom Planet class not overriding hashCode()
import java.util.HashSet;
import java.util.Set;
class CustomClassAndHashSet
{
public static void main(String[] args)
338
CHAPTER 5  COLLECTING OBJECTS
{
Set<Planet> sp = new HashSet<>();
sp.add(new Planet("Mercury"));
sp.add(new Planet("Venus"));
sp.add(new Planet("Earth"));
sp.add(new Planet("Mars"));
sp.add(new Planet("Jupiter"));
sp.add(new Planet("Saturn"));
sp.add(new Planet("Uranus"));
sp.add(new Planet("Neptune"));
sp.add(new Planet("Fomalhaut b"));
Planet p1 = new Planet("51 Pegasi b");
sp.add(p1);
Planet p2 = new Planet("51 Pegasi b");
sp.add(p2);
System.out.println(p1.equals(p2));
System.out.println(sp);
}
}
class Planet
{
private String name;
Planet(String name)
{
this.name = name;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Planet))
return false;
Planet p = (Planet) o;
return p.name.equals(name);
}
String getName()
{
return name;
}
@Override
public String toString()
{
return name;
}
}
Listing 5-5’s Planet class declares a single name field of type String. Although it might seem pointless
to declare Planet with a single String field because I could refactor this listing to remove Planet and
work with String, I might want to introduce additional fields to Planet (perhaps to store a planet’s mass
and other characteristics) in the future.
When you run this application, it generates unordered output such as the following:
true
339
CHAPTER 5  COLLECTING OBJECTS
[Venus, Fomalhaut b, Uranus, Mars, Neptune, Jupiter, Earth, Mercury, Saturn, 51 Pegasi b, 51
Pegasi b]
This output reveals two 51 Pegasi b elements in the hashset. Although these elements are equal
from the perspective of the overriding equals() method (the first output line, true, proves this point),
overriding equals() is not enough to avoid duplicate elements being stored in a hashset: you must also
override hashCode().
The easiest way to override hashCode() in Listing 5-5’s Planet class is to have the overriding method
call the name field’s hashCode() method and return its value. (This technique only works with a class
whose single reference field’s class provides a valid hashCode() method.) Listing 5-6 presents this
overriding hashCode() method.
Listing 5-6. A custom Planet class overriding hashCode()
import java.util.HashSet;
import java.util.Set;
This book was purchased by [email protected]
class CustomClassAndHashSet
{
public static void main(String[] args)
{
Set<Planet> sp = new HashSet<>();
sp.add(new Planet("Mercury"));
sp.add(new Planet("Venus"));
sp.add(new Planet("Earth"));
sp.add(new Planet("Mars"));
sp.add(new Planet("Jupiter"));
sp.add(new Planet("Saturn"));
sp.add(new Planet("Uranus"));
sp.add(new Planet("Neptune"));
sp.add(new Planet("Fomalhaut b"));
Planet p1 = new Planet("51 Pegasi b");
sp.add(p1);
Planet p2 = new Planet("51 Pegasi b");
sp.add(p2);
System.out.println(p1.equals(p2));
System.out.println(sp);
}
}
class Planet
{
private String name;
Planet(String name)
{
this.name = name;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Planet))
return false;
Planet p = (Planet) o;
340
CHAPTER 5  COLLECTING OBJECTS
return p.name.equals(name);
}
String getName()
{
return name;
}
@Override
public int hashCode()
{
return name.hashCode();
}
@Override
public String toString()
{
return name;
}
}
Compile Listing 5-6 (javac CustomClassAndHashSet.java) and run the application (java
CustomClassAndHashSet). You will observe output (similar to that shown below) that reveals no duplicate
elements:
true
[Saturn, Earth, Uranus, Fomalhaut b, 51 Pegasi b, Venus, Jupiter, Mercury, Mars, Neptune]
■ Note LinkedHashSet is a subclass of HashSet that uses a linked list to store its elements. As a result,
LinkedHashSet’s iterator returns elements in the order in which they were inserted. For example, if Listing 5-4
had specified Set<String> ss = new LinkedHashSet<>();, the application’s output would have been ss:
apples pears grapes bananas kiwis null. Also, LinkedHashSet offers slower performance than HashSet and
faster performance than TreeSet.
EnumSet
Chapter 3 introduced you to traditional enumerated types and their enum replacement. (An enum is an
enumerated type that is expressed via reserved word enum.) The following example demonstrates the
traditional enumerated type:
static
static
static
static
static
static
static
final
final
final
final
final
final
final
int
int
int
int
int
int
int
SUNDAY = 1;
MONDAY = 2;
TUESDAY = 4;
WEDNESDAY = 8;
THURSDAY = 16;
FRIDAY = 32;
SATURDAY = 64;
341
CHAPTER 5  COLLECTING OBJECTS
Although the enum has many advantages over the traditional enumerated type, the traditional
enumerated type is less awkward to use when combining constants into a set; for example, static final
int DAYS_OFF = SUNDAY | MONDAY;.
DAYS_OFF is an example of an integer-based, fixed-length bitset, which is a set of bits where each bit
indicates that its associated member belongs to the set when the bit is set to 1, and is absent from the set
when the bit is set to 0.
■ Note An int-based bitset cannot contain more than 32 members because int has a size of 32 bits. Similarly, a
long-based bitset cannot contain more than 64 members because long has a size of 64 bits.
This bitset is formed by bitwise inclusive ORing the traditional enumerated type’s integer constants
together via the bitwise inclusive OR operator (|): you could also use +. Each constant must be a unique
power of two (starting with one) because otherwise it is impossible to distinguish between the members
of this bitset.
To determine if a constant belongs to the bitset, create an expression that involves the bitwise AND
operator (&). For example, ((DAYS_OFF&MONDAY) == MONDAY) bitwise ANDs DAYS_OFF (3) with MONDAY (2),
which results in 2. This value is compared via == with MONDAY (2), and the result of the expression is true:
MONDAY is a member of the DAYS_OFF bitset.
You can accomplish the same task with an enum by instantiating an appropriate Set
implementation class and calling the add() method multiple times to store the constants in the set.
Listing 5-7 illustrates this more awkward alternative.
Listing 5-7. Creating the Set equivalent of DAYS_OFF
import java.util.Set;
import java.util.TreeSet;
enum Weekday
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
class DaysOff
{
public static void main(String[] args)
{
Set<Weekday> daysOff = new TreeSet<>();
daysOff.add(Weekday.SUNDAY);
daysOff.add(Weekday.MONDAY);
System.out.println(daysOff);
}
}
When you run this application, it generates the following output:
[SUNDAY, MONDAY]
342
CHAPTER 5  COLLECTING OBJECTS
■ Note The constants’ ordinals and not their names are stored in the tree set, which is why the names appear
unordered even though the constants are stored in sorted order of their ordinals.
As well as being more awkward to use (and verbose) than the bitset, the Set alternative requires
more memory to store each constant and is not as fast. Because of these problems, EnumSet was
introduced.
The EnumSet class provides a Set implementation that is based on a bitset. Its elements are constants
that must come from the same enum, which is specified when the enum set is created. Null elements are
not permitted; any attempt to store a null element results in a thrown NullPointerException.
Listing 5-8 demonstrates EnumSet.
Listing 5-8. Creating the EnumSet equivalent of DAYS_OFF
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Set;
enum Weekday
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}
class EnumSetDemo
{
public static void main(String[] args)
{
Set<Weekday> daysOff = EnumSet.of(Weekday.SUNDAY, Weekday.MONDAY);
Iterator<Weekday> iter = daysOff.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}
}
EnumSet, whose generic type is EnumSet<E extends Enum<E>>, provides various class methods for
conveniently constructing enum sets. For example, <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
returns an EnumSet instance consisting of elements e1 and e2. In this example, those elements are
Weekday.SUNDAY and Weekday.MONDAY.
When you run this application, it generates the following output:
SUNDAY
MONDAY
■ Note As well as providing several overloaded of() methods, EnumSet provides other methods for conveniently
creating enum sets. For example, allOf() returns an EnumSet instance that contains all of an enum’s constants,
where this method’s solitary argument is a class literal that identifies the enum:
343
CHAPTER 5  COLLECTING OBJECTS
Set<Weekday> allWeekDays = EnumSet.allOf(Weekday.class);
Similarly, range() returns an EnumSet instance containing a range of an enum’s elements (with the range’s limits
as specified by this method’s two arguments):
for (WeekDay wd : EnumSet.range(WeekDay.MONDAY, WeekDay.FRIDAY))
System.out.println(wd);
SortedSet
TreeSet is an example of a sorted set, which is a set that maintains its elements in ascending order, sorted
according to their natural ordering or according to a comparator that is supplied when the sorted set is
created. Sorted sets are described by the SortedSet interface.
SortedSet, whose generic type is SortedSet<E>, extends Set. With two exceptions, the methods it
inherits from Set behave identically on sorted sets as on other sets:
•
The Iterator instance returned from iterator() traverses the sorted set in
ascending element order.
•
The array returned by toArray() contains the sorted set’s elements in order.
■ Note Although not guaranteed, the toString() methods of SortedSet implementations in the Collections
Framework (such as TreeSet) return a string containing all the sorted set’s elements in order.
SortedSet’s documentation requires that an implementation must provide the four standard
constructors that I presented in my discussion of TreeSet. Furthermore, implementations of this
interface must implement the methods that are described in Table 5-3.
Table 5-3. SortedSet-specific Methods
344
Method
Description
Comparator<? super E>
comparator()
Return the comparator used to order the elements in this
set, or null when this set uses the natural ordering of its
elements.
E first()
Return the first (lowest) element currently in this set, or
throw a NoSuchElementException instance when this set is
empty.
CHAPTER 5  COLLECTING OBJECTS
SortedSet<E> headSet(E
toElement)
Return a view of that portion of this set whose elements are
strictly less than toElement. The returned set is backed by
this set, so changes in the returned set are reflected in this
set and vice versa. The returned set supports all optional set
operations that this set supports. This method throws
ClassCastException when toElement is not compatible with
this set’s comparator (or, when the set has no comparator,
when toElement does not implement Comparable),
NullPointerException when toElement is null and this set
does not permit null elements, and
IllegalArgumentException when this set has a restricted
range and toElement lies outside of this range’s bounds.
E last()
Return the last (highest) element currently in this set, or
throw a NoSuchElementException instance when this set is
empty.
SortedSet<E> subSet(E
fromElement, E toElement)
Return a view of the portion of this set whose elements range
from fromElement, inclusive, to toElement, exclusive. (When
fromElement and toElement are equal, the returned set is
empty.) The returned set is backed by this set, so changes in
the returned set are reflected in this set and vice versa. The
returned set supports all optional set operations that this set
supports. This method throws ClassCastException when
fromElement and toElement cannot be compared to one
another using this set’s comparator (or, when the set has no
comparator, using natural ordering), NullPointerException
when fromElement or toElement is null and this set does not
permit null elements, and IllegalArgumentException when
fromElement is greater than toElement or when this set has a
restricted range and fromElement or toElement lies outside of
this range’s bounds.
SortedSet<E> tailSet(E
fromElement)
Return a view of that portion of this set whose elements are
greater than or equal to fromElement. The returned set is
backed by this set, so changes in the returned set are
reflected in this set and vice versa. The returned set supports
all optional set operations that this set supports. This
method throws ClassCastException when fromElement is not
compatible with this set’s comparator (or, when the set has
no comparator, when fromElement does not implement
Comparable), NullPointerException when fromElement is
null and this set does not permit null elements, and
IllegalArgumentException when this set has a restricted
range and fromElement lies outside of the range’s bounds.
The set-based range views returned from headSet(), subSet(), and tailSet() are analogous to the
list-based range view returned from List’s subList() method except that a set-based range view remains
345
CHAPTER 5  COLLECTING OBJECTS
valid even when the backing sorted set is modified. As a result, a set-based range view can be used for a
lengthy period of time.
■ Note Unlike a list-based range view whose endpoints are elements in the backing list, the endpoints of a setbased range view are absolute points in element space, allowing a set-based range view to serve as a window
onto a portion of the set’s element space. Any changes made to the set-based range view are written back to the
backing sorted set and vice versa.
Each range view returned by headSet(), subSet(), or tailSet() is half open because it does not
include its high endpoint (headSet() and subSet()) or its low endpoint (tailSet()). For the first two
methods, the high endpoint is specified by argument toElement; for the last method, the low endpoint is
specified by argument fromElement.
■ Note You could also regard the returned range view as being half closed because it includes only one of its
endpoints.
Listing 5-9 demonstrates a sorted set based on a tree set.
Listing 5-9. A sorted set of fruit and vegetable names
import java.util.SortedSet;
import java.util.TreeSet;
class SortedSetDemo
{
public static void main(String[] args)
{
SortedSet<String> sss = new TreeSet<>();
String[] fruitAndVeg =
{
"apple", "potato", "turnip", "banana", "corn", "carrot", "cherry",
"pear", "mango", "strawberry", "cucumber", "grape", "banana",
"kiwi", "radish", "blueberry", "tomato", "onion", "raspberry",
"lemon", "pepper", "squash", "melon", "zucchini", "peach", "plum",
"turnip", "onion", "nectarine"
};
System.out.println("Array size = "+fruitAndVeg.length);
for (String fruitVeg: fruitAndVeg)
sss.add(fruitVeg);
dump("sss:", sss);
System.out.println("Sorted set size = "+sss.size());
System.out.println("First element = "+sss.first());
346
CHAPTER 5  COLLECTING OBJECTS
System.out.println("Last element = "+sss.last());
System.out.println("Comparator = "+sss.comparator());
dump("hs:", sss.headSet("n"));
dump("ts:", sss.tailSet("n"));
System.out.println("Count of p-named fruits & vegetables = "+
sss.subSet("p", "q").size());
System.out.println("Incorrect count of c-named fruits & vegetables = "+
sss.subSet("carrot", "cucumber").size());
System.out.println("Correct count of c-named fruits & vegetables = "+
sss.subSet("carrot", "cucumber\0").size());
}
static void dump(String title, SortedSet<String> sss)
{
System.out.print(title+" ");
for (String s: sss)
System.out.print(s+" ");
System.out.println();
}
}
When you run this application, it generates the following output:
Array size = 29
sss: apple banana blueberry carrot cherry corn cucumber grape kiwi lemon mango melon
nectarine onion peach pear pepper plum potato radish raspberry squash strawberry tomato
turnip zucchini
Sorted set size = 26
First element = apple
Last element = zucchini
Comparator = null
hs: apple banana blueberry carrot cherry corn cucumber grape kiwi lemon mango melon
ts: nectarine onion peach pear pepper plum potato radish raspberry squash strawberry tomato
turnip zucchini
Count of p-named fruits & vegetables = 5
Incorrect count of c-named fruits & vegetables = 3
Correct count of c-named fruits & vegetables = 4
This output reveals that the sorted set’s size is less than the array’s size because a set cannot contain
duplicate elements: the duplicate banana, turnip, and onion elements are not stored in the sorted set.
The comparator() method returns null because the sorted set was not created with a comparator.
Instead, the sorted set relies on the natural ordering of String elements to store them in sorted order.
The headSet() and tailSet() methods are called with argument "n" to return, respectively, a set of
elements whose names begin with a letter that is strictly less than n, and a letter that is greater than or
equal to n.
Finally, the output shows you that you must be careful when passing an upper limit to subSet(). As
you can see, ss.subSet("carrot", "cucumber") does not include cucumber in the returned range view
because cucumber is subSet()’s high endpoint.
To include cucumber in the range view, you need to form a closed range or closed interval (both
endpoints are included). With String objects, you accomplish this task by appending \0 to the string. For
example, ss.subSet("carrot", "cucumber\0") includes cucumber because it is less than cucumber\0.
347
CHAPTER 5  COLLECTING OBJECTS
This same technique can be applied wherever you need to form an open range or open interval
(neither endpoint is included). For example, ss.subSet("carrot\0", "cucumber") does not include
carrot because it is less than carrot\0. Furthermore, it does not include high endpoint cucumber.
■ Note When you want to create closed and open ranges for elements created from your own classes, you need
to provide some form of predecessor() and successor() methods that return an element’s predecessor and
successor.
You need to be careful when designing classes that work with sorted sets. For example, the class
must implement Comparable when you plan to store the class’s instances in a sorted set where these
elements are sorted according to their natural ordering. Consider Listing 5-10.
Listing 5-10. A custom Employee class not implementing Comparable
import java.util.SortedSet;
import java.util.TreeSet;
class CustomClassAndSortedSet
{
public static void main(String[] args)
{
SortedSet<Employee> sse = new TreeSet<>();
sse.add(new Employee("Sally Doe"));
sse.add(new Employee("Bob Doe")); // ClassCastException thrown here
sse.add(new Employee("John Doe"));
System.out.println(sse);
}
}
class Employee
{
private String name;
Employee(String name)
{
this.name = name;
}
@Override
public String toString()
{
return name;
}
}
When you run this application, it generates the following output:
Exception in thread "main" java.lang.ClassCastException: Employee cannot be cast to
java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1188)
348
CHAPTER 5  COLLECTING OBJECTS
at java.util.TreeMap.put(TreeMap.java:531)
at java.util.TreeSet.add(TreeSet.java:255)
at CustomClassAndSortedSet.main(CustomClassAndSortedSet.java:9)
The ClassCastException instance is thrown during the second add() method call because the sorted
set implementation, an instance of TreeSet, is unable to call the second Employee element’s compareTo()
method, because Employee does not implement Comparable.
The solution to this problem is to have the class implement Comparable, which is exactly what is
revealed in Listing 5-11.
Listing 5-11. Making Employee elements comparable
import java.util.SortedSet;
import java.util.TreeSet;
class CustomClassAndSortedSet
{
public static void main(String[] args)
{
SortedSet<Employee> sse = new TreeSet<>();
sse.add(new Employee("Sally Doe"));
sse.add(new Employee("Bob Doe"));
Employee e1 = new Employee("John Doe");
Employee e2 = new Employee("John Doe");
sse.add(e1);
sse.add(e2);
System.out.println(sse);
System.out.println(e1.equals(e2));
}
}
class Employee implements Comparable<Employee>
{
private String name;
Employee(String name)
{
this.name = name;
}
@Override
public int compareTo(Employee e)
{
return name.compareTo(e.name);
}
@Override
public String toString()
{
return name;
}
}
Listing 5-11’s main() method differs from Listing 5-10 in that it also creates two Employee objects
initialized to "John Doe", adds these objects to the sorted set, and compares these objects for equality via
349
CHAPTER 5  COLLECTING OBJECTS
equals(). Furthermore, Listing 5-11 declares Employee to implement Comparable, introducing a
compareTo() method into Employee.
When you run this application, it generates the following output:
[Bob Doe, John Doe, Sally Doe]
false
This book was purchased by [email protected]
This output shows that only one "John Doe" Employee object is stored in the sorted set. After all, a
set cannot contain duplicate elements. However, the false value (resulting from the equals()
comparison) also shows that the sorted set’s natural ordering is inconsistent with equals(), which
violates SortedSet’s contract:
The ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be
consistent with equals() if the sorted set is to correctly implement the Set interface. This is so because the
Set interface is defined in terms of the equals() operation, but a sorted set performs all element
comparisons using its compareTo() (or compare()) method, so two elements that are deemed equal by this
method are, from the standpoint of the sorted set, equal.
Because the application works correctly, why should SortedSet’s contract matter? Although the
contract does not appear to matter with respect to the TreeSet implementation of SortedSet, perhaps it
will matter in the context of a third-party class that implements this interface.
Listing 5-12 shows you how to correct this problem and make Employee instances work with any
implementation of a sorted set.
Listing 5-12. A contract-compliant Employee class
import java.util.SortedSet;
import java.util.TreeSet;
class CustomClassAndSortedSet
{
public static void main(String[] args)
{
SortedSet<Employee> sse = new TreeSet<>();
sse.add(new Employee("Sally Doe"));
sse.add(new Employee("Bob Doe"));
Employee e1 = new Employee("John Doe");
Employee e2 = new Employee("John Doe");
sse.add(e1);
sse.add(e2);
System.out.println(sse);
System.out.println(e1.equals(e2));
}
}
class Employee implements Comparable<Employee>
{
private String name;
Employee(String name)
{
this.name = name;
}
@Override
public int compareTo(Employee e)
{
350
CHAPTER 5  COLLECTING OBJECTS
return name.compareTo(e.name);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Employee))
return false;
Employee e = (Employee) o;
return e.name.equals(name);
}
@Override
public String toString()
{
return name;
}
}
Listing 5-12 corrects the SortedSet contract violation by overriding equals(). Run the resulting
application and you will observe [Bob Doe, John Doe, Sally Doe] as the first line of output and true as
the second line: the sorted set’s natural ordering is now consistent with equals().
■ Note Although it is important to override hashCode() whenever you override equals(), I did not override
hashCode() (although I overrode equals()) in Listing 5-12’s Employee class to emphasize that tree-based sorted
sets ignore hashCode().
NavigableSet
TreeSet is an example of a navigable set, which is a sorted set that can be iterated over in descending
order as well as ascending order, and which can report closest matches for given search targets.
Navigable sets are described by the NavigableSet interface, whose generic type is NavigableSet<E>,
which extends SortedSet, and which is described in Table 5-4.
Table 5-4. NavigableSet-specific Methods
Method
Description
E ceiling(E e)
Return the least element in this set greater than or equal to e,
or null when there is no such element. This method throws
ClassCastException when e cannot be compared with the
elements currently in the set, and NullPointerException
when e is null and this set does not permit null elements.
Iterator<E>
descendingIterator()
Return an iterator over the elements in this set, in
descending order. Equivalent in effect to
descendingSet().iterator().
351
CHAPTER 5  COLLECTING OBJECTS
352
NavigableSet<E>
descendingSet()
Return a reverse order view of the elements contained in this
set. The descending set is backed by this set, so changes to
the set are reflected in the descending set and vice versa. If
either set is modified (except through the iterator’s own
remove() operation) while iterating over the set, the results
of the iteration are undefined.
E floor(E e)
Return the greatest element in this set less than or equal to e,
or null when there is no such element. This method throws
ClassCastException when e cannot be compared with the
elements currently in the set, and NullPointerException
when e is null and this set does not permit null elements.
NavigableSet<E> headSet(E
toElement, boolean
inclusive)
Return a view of the portion of this set whose elements are
less than (or equal to, when inclusive is true) toElement.
The returned set is backed by this set, so changes in the
returned set are reflected in this set and vice versa. The
returned set supports all optional set operations that this set
supports. This method throws ClassCastException when
toElement is not compatible with this set’s comparator (or,
when the set has no comparator, when toElement does not
implement Comparable), NullPointerException when
toElement is null and this set does not permit null elements,
and IllegalArgumentException when this set has a restricted
range and toElement lies outside of this range’s bounds.
E higher(E e)
Return the least element in this set strictly greater than the
given element, or null when there is no such element. This
method throws ClassCastException when e cannot be
compared with the elements currently in the set, and
NullPointerException when e is null and this set does not
permit null elements.
E lower(E e)
Return the greatest element in this set strictly less than the
given element, or null when there is no such element. This
method throws ClassCastException when e cannot be
compared with the elements currently in the set, and
NullPointerException when e is null and this set does not
permit null elements.
E pollFirst()
Return and remove the first (lowest) element from this set,
or return null when this set is empty.
E pollLast()
Return and remove the last (highest) element from this set,
or return null when this set is empty.
NavigableSet<E> subSet(E
fromElement, boolean
fromInclusive, E toElement,
Return a view of the portion of this set whose elements range
from fromElement to toElement. (When fromElement and
toElement are equal, the returned set is empty unless
CHAPTER 5  COLLECTING OBJECTS
boolean toInclusive)
fromInclusive and toInclusive are both true.) The returned
set is backed by this set, so changes in the returned set are
reflected in this set and vice versa. The returned set supports
all optional set operations that this set supports. This
method throws ClassCastException when fromElement and
toElement cannot be compared to one another using this
set’s comparator (or, when the set has no comparator, using
natural ordering), NullPointerException when fromElement
or toElement is null and this set does not permit null
elements, and IllegalArgumentException when fromElement
is greater than toElement or when this set has a restricted
range and fromElement or toElement lies outside of this
range’s bounds.
NavigableSet<E> tailSet(E
fromElement, boolean
inclusive)
Return a view of the portion of this set whose elements are
greater than (or equal to, when inclusive is true)
fromElement. The returned set is backed by this set, so
changes in the returned set are reflected in this set and vice
versa. The returned set supports all optional set operations
that this set supports. This method throws
ClassCastException when fromElement is not compatible
with this set’s comparator (or, when the set has no
comparator, when fromElement does not implement
Comparable), NullPointerException when fromElement is
null and this set does not permit null elements, and
IllegalArgumentException when this set has a restricted
range and fromElement lies outside of this range’s bounds.
Listing 5-13 demonstrates a navigable set based on a tree set.
Listing 5-13. Navigating a set of integers
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.TreeSet;
class NavigableSetDemo
{
public static void main(String[] args)
{
NavigableSet<Integer> ns = new TreeSet<>();
int[] ints = { 82, -13, 4, 0, 11, -6, 9 };
for (int i: ints)
ns.add(i);
System.out.print("Ascending order: ");
Iterator iter = ns.iterator();
while (iter.hasNext())
System.out.print(iter.next()+" ");
System.out.println();
System.out.print("Descending order: ");
353
CHAPTER 5  COLLECTING OBJECTS
iter = ns.descendingIterator();
while (iter.hasNext())
System.out.print(iter.next()+" ");
System.out.println("\n");
outputClosestMatches(ns, 4);
outputClosestMatches(ns.descendingSet(), 12);
}
static void outputClosestMatches(NavigableSet<Integer> ns, int i)
{
System.out.println("Element < "+i+" is "+ns.lower(i));
System.out.println("Element <= "+i+" is "+ns.floor(i));
System.out.println("Element > "+i+" is "+ns.higher(i));
System.out.println("Element >= "+i+" is "+ns.ceiling(i));
System.out.println();
}
}
Listing 5-13 creates a navigable set of Integer elements. It takes advantage of autoboxing to ensure
that ints are converted to Integers.
When you run this application, it generates the following output:
Ascending order: -13 -6 0 4 9 11 82
Descending order: 82 11 9 4 0 -6 -13
Element
Element
Element
Element
< 4 is 0
<= 4 is 4
> 4 is 9
>= 4 is 4
Element
Element
Element
Element
< 12 is 82
<= 12 is 82
> 12 is 11
>= 12 is 11
The first four output lines beginning with Element pertain to an ascending-order set where the
element being matched (4) is a member of the set. The second four Element-prefixed lines pertain to a
descending-order set where the element being matched (12) is not a member.
As well as letting you conveniently locate set elements via its closest-match methods (ceiling(),
floor(), higher(), and lower()), NavigableSet lets you return set views containing all elements within
certain ranges, as demonstrated by the following examples:
•
ns.subSet(-13, true, 9, true): Return all elements from -13 through 9.
•
ns.tailSet(-6, false): Return all elements greater than -6.
•
ns.headSet(4, true): Return all elements less than or equal to 4.
Finally, you can return and remove from the set the first (lowest) element by calling pollFirst() and
the last (highest) element by calling pollLast(). For example, ns.pollFirst() removes and returns -13,
and ns.pollLast() removes and returns -82.
354
CHAPTER 5  COLLECTING OBJECTS
Queue
A queue is a collection in which elements are stored and retrieved in a specific order. Most queues are
categorized as one of the following:
•
First-in, first-out (FIFO) queue: Elements are inserted at the queue’s tail and
removed at the queue’s head.
•
Last-in, first-out (LIFO) queue: Elements are inserted and removed at one end of
the queue such that the last element inserted is the first element retrieved. This
kind of queue behaves as a stack.
•
Priority queue: Elements are inserted according to their natural ordering, or
according to a comparator that is supplied to the queue implementation.
Queue, whose generic type is Queue<E>, extends Collection, redeclaring add() to adjust its contract
(insert the specified element into this queue if it is possible to do so immediately without violating
capacity restrictions), and inheriting the other methods from Collection. Table 5-5 describes add() and
the other Queue-specific methods.
Table 5-5. Queue-specific Methods
Method
Description
boolean add(E e)
Insert element e into this queue if it is possible to do so
immediately without violating capacity restrictions. Return
true on success; otherwise, throw IllegalStateException
when the element cannot be added at this time because no
space is currently available. This method also throws
ClassCastException when e’s class prevents e from being
added to this queue, NullPointerException when e contains
the null reference and this queue does not permit null
elements to be added, and IllegalArgumentException when
some property of e prevents it from being added to this
queue.
E element()
Return but do not also remove the element at the head of
this queue. This method throws NoSuchElementException
when this queue is empty.
boolean offer(E e)
Insert element e into this queue if it is possible to do so
immediately without violating capacity restrictions. Return
true on success; otherwise, return false when the element
cannot be added at this time because no space is currently
available. This method throws ClassCastException when e’s
class prevents e from being added to this queue,
NullPointerException when e contains the null reference
and this queue does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this queue.
355
CHAPTER 5  COLLECTING OBJECTS
E peek()
Return but do not also remove the element at the head of
this queue. This method returns null when this queue is
empty.
E poll()
Return and also remove the element at the head of this
queue. This method returns null when this queue is empty.
E remove()
Return and also remove the element at the head of this
queue. This method throws NoSuchElementException when
this queue is empty. This is the only difference between
remove() and poll().
Table 5-5 reveals two sets of methods: in one set, a method (such as add()) throws an exception
when an operation fails; in the other set, a method (such as offer()) returns a special value (false or null)
in the presence of failure. The methods that return a special value are useful in the context of capacityrestricted Queue implementations where failure is a normal occurrence.
■ Note The offer() method is generally preferable to add() when using a capacity-restricted queue because
offer() does not throw IllegalStateException.
Java supplies many Queue implementation classes, where most of these classes are members of the
java.util.concurrent package: LinkedBlockingQueue, LinkedTransferQueue, and SynchronousQueue are
examples. In contrast, the java.util package provides LinkedList and PriorityQueue as its Queue
implementation classes.
■ Caution Many Queue implementation classes do not allow null elements to be added. However, some classes
(such as LinkedList) permit null elements. You should avoid adding a null element because null is used as a
special return value by the peek() and poll() methods to indicate that a queue is empty.
PriorityQueue
The PriorityQueue class provides an implementation of a priority queue, which is a queue that orders its
elements according to their natural ordering or by a comparator provided when the queue is
instantiated. Priority queues do not permit null elements, and do not permit insertion of non-Comparable
objects when relying on natural ordering.
The element at the head of the priority queue is the least element with respect to the specified
ordering. If multiple elements are tied for least element, one of those elements is arbitrarily chosen as
the least element. Similarly, the element at the tail of the priority queue is the greatest element, which is
arbitrarily chosen when there is a tie.
356
CHAPTER 5  COLLECTING OBJECTS
Priority queues are unbounded, but have a capacity that governs the size of the internal array that is
used to store the priority queue’s elements. The capacity value is at least as large as the queue’s length,
and grows automatically as elements are added to the priority queue.
PriorityQueue (whose generic type is PriorityQueue<E>) supplies six constructors:
•
PriorityQueue() creates a PriorityQueue instance with an initial capacity of 11
elements, and which orders its elements according to their natural ordering.
•
PriorityQueue(Collection<? extends E> c) creates a PriorityQueue instance
containing c’s elements. If c is a SortedSet or PriorityQueue instance, this priority
queue will be ordered according to the same ordering. Otherwise, this priority
queue will be ordered according to the natural ordering of its elements. This
constructor throws ClassCastException when c’s elements cannot be compared to
one another according to the priority queue’s ordering, and NullPointerException
when c or any of its elements contain the null reference.
•
PriorityQueue(int initialCapacity) creates a PriorityQueue instance with the
specified initialCapacity, and which orders its elements according to their
natural ordering. This constructor throws IllegalArgumentException when
initialCapacity is less than 1.
•
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
creates a PriorityQueue instance with the specified initialCapacity, and which
orders its elements according to the specified comparator. Natural ordering is used
when comparator contains the null reference. This constructor throws
IllegalArgumentException when initialCapacity is less than 1.
•
PriorityQueue(PriorityQueue<? extends E> pq) creates a PriorityQueue instance
containing pq’s elements. This priority queue will be ordered according to the
same ordering as pq. This constructor throws ClassCastException when pq’s
elements cannot be compared to one another according to pq’s ordering, and
NullPointerException when pq or any of its elements contains the null reference.
•
PriorityQueue(SortedSet<? extends E> ss) creates a PriorityQueue instance
containing ss’s elements. This priority queue will be ordered according to the
same ordering as ss. This constructor throws ClassCastException when
sortedSet’s elements cannot be compared to one another according to ss’s
ordering, and NullPointerException when sortedSet or any of its elements
contains the null reference.
Listing 5-14 demonstrates a priority queue.
Listing 5-14. Adding randomly generated integers to a priority queue
import java.util.PriorityQueue;
import java.util.Queue;
class PriorityQueueDemo
{
public static void main(String[] args)
{
Queue<Integer> qi = new PriorityQueue<>();
for (int i = 0; i < 15; i++)
357
CHAPTER 5  COLLECTING OBJECTS
qi.add((int) (Math.random()*100));
while (!qi.isEmpty())
System.out.print(qi.poll()+" ");
System.out.println();
}
}
After creating a priority queue, the main thread adds 15 randomly generated integers (ranging from
0 through 99) to this queue. It then enters a while loop that repeatedly polls the priority queue for the
next element and outputs that element until the queue is empty.
When you run this application, it outputs a line of 15 integers in ascending numerical order from left
to right. For example, I observed the following output from one run:
11 21 29 35 40 53 66 70 72 75 80 83 87 88 89
Because poll() returns null when there are no more elements, I could have coded this loop as
follows:
Integer i;
while ((i = qi.poll()) != null)
System.out.print(i+" ");
Suppose you want to reverse the order of the previous application’s output so that the largest
element appears on the left and the smallest element appears on the right. As Listing 5-15 demonstrates,
you can achieve this task by passing a comparator to the appropriate PriorityQueue constructor.
Listing 5-15. Using a comparator with a priority queue
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
class PriorityQueueDemo
{
final static int NELEM = 15;
public static void main(String[] args)
{
Comparator<Integer> cmp;
cmp = new Comparator<Integer>()
{
public int compare(Integer e1, Integer e2)
{
return e2-e1;
}
};
Queue<Integer> qi = new PriorityQueue<>(NELEM, cmp);
for (int i = 0; i < NELEM; i++)
qi.add((int) (Math.random()*100));
while (!qi.isEmpty())
System.out.print(qi.poll()+" ");
System.out.println();
}
}
358
CHAPTER 5  COLLECTING OBJECTS
Listing 5-15 is similar to Listing 5-14, but there are some differences. First, I have declared an NELEM
constant so that I can easily change both the priority queue’s initial capacity and the number of
elements inserted into the priority queue by specifying the new value in one place.
Second, Listing 5-15 declares and instantiates an anonymous class that implements Comparator. Its
compareTo() method subtracts element e2 from element e1 to achieve descending numerical order. The
compiler handles the task of unboxing e2 and e1 by converting e2-e1 to e2.intValue()-e1.intValue().
Finally, Listing 5-15 passes an initial capacity of NELEM elements and the instantiated comparator to
the PriorityQueue(int initialCapacity, Comparator<? super E> comparator) constructor. The priority
queue will use this comparator to order these elements.
Run this application and you will now see a single output line of 15 integers shown in descending
numerical order from left to right. For example, I observed this output line:
90 86 78 74 65 53 45 44 30 28 18 9 9 7 5
Deque
A deque (pronounced deck) is a double-ended queue in which element insertion or removal occurs at its
head or tail. Deques can be used as queues or stacks.
Deque, whose generic type is Deque<E>, extends Queue, in which the inherited add(E e) method
inserts e at the deque’s tail. Table 5-6 describes Deque-specific methods.
Table 5-6. Deque-specific Methods
Method
Description
void addFirst(E e)
Insert e at the head of this deque if it is possible to do so
immediately without violating capacity restrictions. When
using a capacity-restricted deque, it is generally preferable to
use method offerFirst(). This method throws
IllegalStateException when e cannot be added at this time
because of capacity restrictions, ClassCastException when
e’s class prevents e from being added to this deque,
NullPointerException when e contains the null reference
and this deque does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this deque.
void addLast(E e)
Insert e at the tail of this deque if it is possible to do so
immediately without violating capacity restrictions. When
using a capacity-restricted deque, it is generally preferable to
use method offerLast(). This method throws
IllegalStateException when e cannot be added at this time
because of capacity restrictions, ClassCastException when
e’s class prevents e from being added to this deque,
NullPointerException when e contains the null reference
and this deque does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this deque.
Iterator<E>
Return an iterator over the elements in this deque in reverse
359
This book was purchased by [email protected]
CHAPTER 5  COLLECTING OBJECTS
360
descendingIterator()
sequential order. The elements will be returned in order
from last (tail) to first (head). The inherited Iterator<E>
iterator() method returns elements from the head to the
tail.
E element()
Retrieve but do not remove the first element of this deque (at
the head). This method differs from peek() only in that it
throws NoSuchElementException when this deque is empty.
This method is equivalent to getFirst().
E getFirst()
Retrieve but do not remove the first element of this deque.
This method differs from peekFirst() only in that it throws
NoSuchElementException when this deque is empty.
E getLast()
Retrieve but do not remove the last element of this deque.
This method differs from peekLast() only in that it throws
NoSuchElementException when this deque is empty.
boolean offer(E e)
Insert e at the tail of this deque if it is possible to do so
immediately without violating capacity restrictions,
returning true upon success and false when no space is
currently available. When using a capacity-restricted deque,
this method is generally preferable to the add() method,
which can fail to insert an element only by throwing an
exception. This method throws ClassCastException when e’s
class prevents e from being added to this deque,
NullPointerException when e contains the null reference
and this deque does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this deque. This method is
equivalent to offerLast().
boolean offerFirst(E e)
Insert the specified element at the head of this deque unless
it would violate capacity restrictions. When using a capacityrestricted deque, this method is generally preferable to the
addFirst() method, which can fail to insert an element only
by throwing an exception. This method throws
ClassCastException when e’s class prevents e from being
added to this deque, NullPointerException when e contains
the null reference and this deque does not permit null
elements to be added, and IllegalArgumentException when
some property of e prevents it from being added to this
deque.
boolean offerLast(E e)
Insert e at the tail of this deque unless it would violate
capacity restrictions. When using a capacity-restricted
deque, this method is generally preferable to the addLast()
method, which can fail to insert an element only by throwing
an exception. This method throws ClassCastException when
CHAPTER 5  COLLECTING OBJECTS
e’s class prevents e from being added to this deque,
NullPointerException when e contains the null reference
and this deque does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this deque.
E peek()
Retrieve but do not remove the first element of this deque (at
the head), or return null when this deque is empty. This
method is equivalent to peekFirst().
E peekFirst()
Retrieve but do not remove the first element of this deque (at
the head), or return null when this deque is empty.
E peekLast()
Retrieve but do not remove the last element of this deque (at
the tail), or return null when this deque is empty.
E poll()
Retrieve and remove the first element of this deque (at the
head), or return null when this deque is empty. This method
is equivalent to pollFirst().
E pollFirst()
Retrieve and remove the first element of this deque (at the
head), or return null when this deque is empty.
E pollLast()
Retrieve and remove the last element of this deque (at the
tail), or return null when this deque is empty.
E pop()
Pop an element from the stack represented by this deque. In
other words, remove and return the first element of this
deque. This method is equivalent to removeFirst().
void push(E e)
Push e onto the stack represented by this deque (in other
words, at the head of this deque) if it is possible to do so
immediately without violating capacity restrictions,
returning true upon success and throwing
IllegalStateException when no space is currently available.
This method also throws ClassCastException when e’s class
prevents e from being added to this deque,
NullPointerException when e contains the null reference
and this deque does not permit null elements to be added,
and IllegalArgumentException when some property of e
prevents it from being added to this deque. This method is
equivalent to addFirst().
E remove()
Retrieve and remove the first element of this deque (at the
head). This method differs from poll() only in that it throws
NoSuchElementException when this deque is empty. This
method is equivalent to removeFirst().
361
CHAPTER 5  COLLECTING OBJECTS
E removeFirst()
Retrieve and remove the first element of this deque. This
method differs from pollFirst() only in that it throws
NoSuchElementException when this deque is empty.
boolean
removeFirstOccurrence(Object
o)
Remove the first occurrence of o from this deque. If the
deque does not contain o, it is unchanged. Return true when
this deque contained o (or equivalently, when this deque
changed as a result of the call). This method throws
ClassCastException when o’s class prevents o from being
added to this deque, and NullPointerException when o
contains the null reference and this deque does not permit
null elements to be added. The inherited boolean
remove(Object o) method is equivalent to this method.
E removeLast()
Retrieve and remove the last element of this deque. This
method differs from pollLast() only in that it throws
NoSuchElementException when this deque is empty.
boolean
removeLastOccurrence(Object
o)
Remove the last occurrence of o from this deque. If the
deque does not contain o, it is unchanged. Return true when
this deque contained o (or equivalently, when this deque
changed as a result of the call). This method throws
ClassCastException when o’s class prevents o from being
added to this deque, and NullPointerException when o
contains the null reference and this deque does not permit
null elements to be added.
As Table 5-6 reveals, Deque declares methods to access elements at both ends of the deque. Methods
are provided to insert, remove, and examine the element. Each of these methods exists in two forms: one
throws an exception when the operation fails, the other returns a special value (either null or false,
depending on the operation). The latter form of the insert operation is designed specifically for use with
capacity-restricted Deque implementations; in most implementations, insert operations cannot fail.
Figure 5-2 reveals a table from Deque’s Java documentation that nicely summarizes both forms of the
insert, remove, and examine methods for both the head and the tail.
362
CHAPTER 5  COLLECTING OBJECTS
Figure 5-2. Deque declares twelve methods for inserting, removing, and examining elements at the head or
tail of a deque.
When a deque is used as a queue, FIFO (First-In-First-Out) behavior results. Elements are added at
the end of the deque and removed from the beginning. The methods inherited from the Queue interface
are precisely equivalent to the Deque methods as indicated in Table 5-7.
Table 5-7. Queue and equivalent Deque Methods
Queue Method
Equivalent Deque Method
add(e)
addLast(e)
offer(e)
offerLast(e)
remove()
removeFirst()
poll()
pollFirst()
element()
getFirst()
peek()
peekFirst()
Finally, deques can also be used as LIFO (Last-In-First-Out) stacks. When a deque is used as a stack,
elements are pushed and popped from the beginning of the deque. Because a stack’s push(e) method
would be equivalent to Deque’s addFirst(e) method, its pop() method would be equivalent to Deque’s
removeFirst() method, and its peek() method would be equivalent to Deque’s peekFirst() method,
Deque declares the E peek(), E pop(), and void push(E e) stack-oriented convenience methods.
ArrayDeque
The ArrayDeque class provides a resizable-array implementation of the Deque interface. It prohibits null
elements from being added to a deque, and its iterator() method returns fail-fast iterators.
ArrayDeque supplies three constructors:
•
ArrayDeque() creates an empty array list with an initial capacity of 16 elements.
363
CHAPTER 5  COLLECTING OBJECTS
•
ArrayDeque(Collection<? extends E> c) creates an array deque containing c’s
elements in the order in which they are returned by c’s iterator. (The first element
returned by c’s iterator becomes the first element, or front of the deque.)
NullPointerException is thrown when c contains the null reference.
•
ArrayDeque(int numElements) creates an empty array deque with an initial
capacity sufficient to hold numElements elements. No exception is thrown when the
argument passed to numElements is less than or equal to zero.
Listing 5-16 demonstrates an array deque.
Listing 5-16. Using an array deque as a stack
import java.util.ArrayDeque;
import java.util.Deque;
class ArrayDequeDemo
{
public static void main(String[] args)
{
Deque<String> stack = new ArrayDeque<>();
String[] weekdays = { "Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday" };
for (String weekday: weekdays)
stack.push(weekday);
while (stack.peek() != null)
System.out.println(stack.pop());
}
}
When you run this application, it generates the following output:
Saturday
Friday
Thursday
Wednesday
Tuesday
Monday
Sunday
Map
A map is a group of key/value pairs (also known as entries). Because the key identifies an entry, a map
cannot contain duplicate keys. Furthermore, each key can map to at most one value. Maps are described
by the Map interface, which has no parent interface, and whose generic type is Map<K,V> (K is the key’s
type; V is the value’s type).
Table 5-8 describes Map’s methods.
364
CHAPTER 5  COLLECTING OBJECTS
Table 5-8. Map-specific Methods
Method
Description
void clear()
Remove all elements from this map, leaving it empty. This
method throws UnsupportedOperationException when
clear() is not supported.
boolean containsKey(Object
key)
Return true when this map contains an entry for the
specified key; otherwise, return false. This method throws
ClassCastException when key is of an inappropriate type for
this map, and NullPointerException when key contains the
null reference and this map does not permit null keys.
boolean containsValue(Object
value)
Return true when this map maps one or more keys to value.
This method throws ClassCastException when value is of an
inappropriate type for this map, and NullPointerException
when value contains the null reference and this map does
not permit null values.
Set<Map.Entry<K,V>>
entrySet()
Return a Set view of the entries contained in this map.
Because this map backs the view, changes that are made to
the map are reflected in the set and vice versa.
boolean equals(Object o)
Compare o with this map for equality. Return true when o is
also a map and the two maps represent the same entries;
otherwise, return false.
V get(Object key)
Return the value to which key is mapped, or null when this
map contains no entry for key. If this map permits null
values, then a return value of null does not necessarily
indicate that the map contains no entry for key; it is also
possible that the map explicitly maps key to the null
reference. The containsKey() method may be used to
distinguish between these two cases. This method throws
ClassCastException when key is of an inappropriate type for
this map, and NullPointerException when key contains the
null reference and this map does not permit null keys.
int hashCode()
Return the hash code for this map. A map’s hash code is
defined to be the sum of the hash codes for the entries in the
map’s entrySet() view.
boolean isEmpty()
Return true when this map contains no entries; otherwise,
return false.
Set<K> keySet()
Return a Set view of the keys contained in this map. Because
this map backs the view, changes that are made to the map
365
CHAPTER 5  COLLECTING OBJECTS
are reflected in the set and vice versa.
366
V put(K key,V value)
Associate value with key in this map. If the map previously
contained an entry for key, the old value is replaced by
value. This method returns the previous value associated
with key, or null when there was no entry for key. (The null
return value can also indicate that the map previously
associated the null reference with key, if the implementation
supports null values.) This method throws
UnsupportedOperationException when put() is not
supported, ClassCastException when key’s or value’s class is
not appropriate for this map, IllegalArgumentException
when some property of key or value prevents it from being
stored in this map, and NullPointerException when key or
value contains the null reference and this map does not
permit null keys or values.
void putAll(Map<? extends
K,? extends V> m)
Copy all the entries from map m to this map. The effect of this
call is equivalent to that of calling put(k, v) on this map
once for each mapping from key k to value v in map m. This
method throws UnsupportedOperationException when
putAll() is not supported, ClassCastException when the
class of a key or value in map m is not appropriate for this
map, IllegalArgumentException when some property of a
key or value in map m prevents it from being stored in this
map, and NullPointerException when m contains the null
reference or when m contains null keys or values and this
map does not permit null keys or values.
V remove(Object key)
Remove key’s entry from this map if it is present. This
method returns the value to which this map previously
associated with key, or null when the map contained no
entry for key. If this map permits null values, then a return
value of null does not necessarily indicate that the map
contained no entry for key; it is also possible that the map
explicitly mapped key to null. This map will not contain an
entry for key once the call returns. This method throws
UnsupportedOperationException when remove() is not
supported, ClassCastException when the class of key is not
appropriate for this map, and NullPointerException when
key contains the null reference and this map does not permit
null keys.
int size()
Return the number of key/value entries in this map. If the
map contains more than Integer.MAX_VALUE entries, this
method returns Integer.MAX_VALUE.
CHAPTER 5  COLLECTING OBJECTS
Collection<V> values()
Return a Collection view of the values contained in this
map. Because this map backs the view, changes that are
made to the map are reflected in the collection and vice
versa.
Unlike List, Set, and Queue, Map does not extend Collection. However, it is possible to view a map as
a Collection instance by calling Map’s keySet(), values(), and entrySet() methods, which respectively
return a Set of keys, a Collection of values, and a Set of key/value pair entries.
■ Note The values() method returns Collection instead of Set because multiple keys can map to the same
value, and values() would then return multiple copies of the same value.
The Collection views returned by these methods (recall that a Set is a Collection because Set
extends Collection) provide the only means to iterate over a Map. For example, suppose you declare
Listing 5-17’s Color enum with its three Color constants, RED, GREEN, and BLUE.
Listing 5-17. A colorful enum
enum Color
{
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
private int r, g, b;
private Color(int r, int g, int b)
{
this.r = r;
this.g = g;
this.b = b;
}
@Override
public String toString()
{
return "r = "+r+", g = "+g+", b = "+b;
}
}
The following example declares a map of String keys and Color values, adds several entries to the
map, and iterates over the keys and values:
Map<String, Color> colorMap = ...; // ... represents creation of a Map implementation
colorMap.put("red", Color.RED);
colorMap.put("blue", Color.BLUE);
colorMap.put("green", Color.GREEN);
colorMap.put("RED", Color.RED);
for (String colorKey: colorMap.keySet())
367
CHAPTER 5  COLLECTING OBJECTS
System.out.println(colorKey);
Collection<Color> colorValues = colorMap.values();
for (Iterator<Color> it = colorValues.iterator(); it.hasNext();)
System.out.println(it.next());
When running this example against a hashmap implementation (discussed later) of colorMap, you
should observe output similar to the following:
red
blue
green
RED
r = 255,
r = 0, g
r = 0, g
r = 255,
g
=
=
g
= 0,
0, b
255,
= 0,
b
=
b
b
= 0
255
= 0
= 0
The first four output lines identify the map’s keys; the second four output lines identify the map’s
values.
The entrySet() method returns a Set of Map.Entry objects. Each of these objects describes a single
entry as a key/value pair and is an instance of a class that implements the Map.Entry interface, where
Entry is a nested interface of Map. Table 5-9 describes Map.Entry’s methods.
Table 5-9. Map.Entry Methods
368
Method
Description
boolean equals(Object o)
Compare o with this entry for equality. Return true when o is
also a map entry and the two entries have the same key and
value.
K getKey()
Return this entry’s key. This method optionally throws
IllegalStateException when this entry has previously been
removed from the backing map.
V getValue()
Return this entry’s value. This method optionally throws
IllegalStateException when this entry has previously been
removed from the backing map.
int hashCode()
Return this entry’s hash code.
V setValue(V value)
Replace this entry’s value with value. The backing map is
updated with the new value. This method throws
UnsupportedOperationException when setValue() is not
supported, ClassCastException when value’s class prevents it
from being stored in the backing map, NullPointerException
when value contains the null reference and the backing map
does not permit null, IllegalArgumentException when some
property of value prevents it from being stored in the backing
map, and (optionally) IllegalStateException when this entry
has previously been removed from the backing map.
CHAPTER 5  COLLECTING OBJECTS
The following example shows you how you might iterate over the previous example’s map entries:
for (Map.Entry<String, Color> colorEntry: colorMap.entrySet())
System.out.println(colorEntry.getKey()+": "+colorEntry.getValue());
When running this example against the previously mentioned hashmap implementation, you would
observe the following output:
red: r = 255, g = 0, b = 0
blue: r = 0, g = 0, b = 255
green: r = 0, g = 255, b = 0
RED: r = 255, g = 0, b = 0
TreeMap
The TreeMap class provides a map implementation that is based on a red-black tree. As a result, entries
are stored in sorted order of their keys. However, accessing these entries is somewhat slower than with
the other Map implementations (which are not sorted) because links must be traversed.
■ Note Check out Wikipedia’s “Red-black tree” entry (http://en.wikipedia.org/wiki/Red-black_tree) to
learn about red-black trees.
TreeMap supplies four constructors:
•
TreeMap() creates a new, empty tree map that is sorted according to the natural
ordering of its keys. All keys inserted into the map must implement the Comparable
interface.
•
TreeMap(Comparator<? super K> comparator) creates a new, empty tree map that
is sorted according to the specified comparator. Passing null to comparator implies
that natural ordering will be used.
•
TreeMap(Map<? extends K, ? extends V> m) creates a new tree map containing m’s
entries, sorted according to the natural ordering of its keys. All keys inserted into
the new map must implement the Comparable interface. This constructor throws
ClassCastException when m’s keys do not implement Comparable or are not
mutually comparable, and NullPointerException when m contains the null
reference.
•
TreeMap(SortedMap<K, ? extends V> sm) creates a new tree map containing the
same entries and using the same ordering as sm. (I discuss sorted maps later in this
chapter.) This constructor throws NullPointerException when sm contains the null
reference.
Listing 5-18 demonstrates a tree map.
369
CHAPTER 5  COLLECTING OBJECTS
Listing 5-18. Sorting a map’s entries according to the natural ordering of their String-based keys
import java.util.Map;
import java.util.TreeMap;
This book was purchased by [email protected]
class TreeMapDemo
{
public static void main(String[] args)
{
Map<String, Integer> msi = new TreeMap<>();
String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis"};
int[] quantities = {10, 15, 8, 17, 30};
for (int i = 0; i < fruits.length; i++)
msi.put(fruits[i], quantities[i]);
for (Map.Entry<String, Integer> entry: msi.entrySet())
System.out.println(entry.getKey()+": "+entry.getValue());
}
}
When you run this application, it generates the following output:
apples: 10
bananas: 17
grapes: 8
kiwis: 30
pears: 15
HashMap
The HashMap class provides a map implementation that is based on a hashtable data structure. This
implementation supports all Map operations, and permits null keys and null values. It makes no
guarantees on the order in which entries are stored.
A hashtable maps keys to integer values with the help of a hash function. Java provides this function
in the form of Object’s hashCode() method, which classes override to provide appropriate hash codes.
A hash code identifies one of the hashtable’s array elements, which is known as a bucket or slot. For
some hashtables, the bucket may store the value that is associated with the key. Figure 5-3 illustrates this
kind of hashtable.
370
CHAPTER 5  COLLECTING OBJECTS
Figure 5-3. A simple hashtable maps keys to buckets that store values associates with those keys.
The hash function hashes Bob Doe to 0, which identifies the first bucket. This bucket contains ACCTS,
which is Bob Doe’s employee type. The hash function also hashes John Doe and Sally Doe to 1 and 2
(respectively) whose buckets contain SALES.
A perfect hash function hashes each key to a unique integer value. However, this ideal is very
difficult to meet. In practice, some keys will hash to the same integer value. This nonunique mapping is
referred to as a collision.
To address collisions, most hashtables associate a linked list of entries with a bucket. Instead of
containing a value, the bucket contains the address of the first node in the linked list, and each node
contains one of the colliding entries. See Figure 5-4.
Figure 5-4. A complex hashtable maps keys to buckets that store references to linked lists whose node
values are hashed from the same keys.
371
CHAPTER 5  COLLECTING OBJECTS
When storing a value in a hashtable, the hashtable uses the hash function to hash the key to its hash
code, and then searches the appropriate linked list to see if an entry with a matching key exists. If there is
an entry, its value is updated with the new value. Otherwise, a new node is created, populated with the
key and value, and appended to the list.
When retrieving a value from a hashtable, the hashtable uses the hash function to hash the key to its
hash code, and then searches the appropriate linked list to see if an entry with a matching key exists. If
there is an entry, its value is returned. Otherwise, the hashtable may return a special value to indicate
that there is no entry, or it might throw an exception.
The number of buckets is known as the hashtable’s capacity. The ratio of the number of stored
entries divided by the number of buckets is known as the hashtable’s load factor. Choosing the right load
factor is important for balancing performance with memory use:
•
As the load factor approaches 1, the probability of collisions and the cost of
handling them (by searching lengthy linked lists) increase.
•
As the load factor approaches 0, the hashtable’s size in terms of number of buckets
increases with little improvement in search cost.
•
For many hashtables, a load factor of 0.75 is close to optimal. This value is the
default for HashMap’s hashtable implementation.
HashMap supplies four constructors:
•
HashMap() creates a new, empty hashmap with an initial capacity of 16 and a load
factor of 0.75.
•
HashMap(int initialCapacity) creates a new, empty hashmap with a capacity
specified by initialCapacity and a load factor of 0.75. This constructor throws
IllegalArgumentException when initialCapacity’s value is less than 0.
•
HashMap(int initialCapacity, float loadFactor) creates a new, empty hashmap
with a capacity specified by initialCapacity and a load factor specified by
loadFactor. This constructor throws IllegalArgumentException when
initialCapacity is less than 0 or when loadFactor is less than or equal to 0.
•
HashMap(Map<? extends K, ? extends V> m) creates a new hashmap containing
m’s entries. This constructor throws NullPointerException when m contains the
null reference.
Listing 5-19 demonstrates a hashmap.
Listing 5-19. Using a hashmap to count command-line arguments
import java.util.HashMap;
import java.util.Map;
class HashMapDemo
{
public static void main(String[] args)
{
Map<String, Integer> argMap = new HashMap<>();
for (String arg: args)
{
Integer count = argMap.get(arg);
372
CHAPTER 5  COLLECTING OBJECTS
argMap.put(arg, (count == null) ? 1 : count+1);
}
System.out.println(argMap);
System.out.println("Number of distinct arguments = "+argMap.size());
}
}
HashMapDemo creates a hashmap of String keys and Integer values. Each key is one of the commandline arguments passed to this application, and its value is the number of occurrences of that argument
on the command line.
For example, java HashMapDemo how much wood could a woodchuck chuck if a woodchuck could
chuck wood generates the following output:
{wood=2, could=2, how=1, if=1, chuck=2, a=2, woodchuck=2, much=1}
Number of distinct arguments = 8
Because the String class overrides equals() and hashCode(), Listing 5-19 can use String objects as
keys in a hashmap. When you create a class whose instances are to be used as keys, you must ensure that
you override both methods.
Listing 5-6 showed you that a class’s overriding hashCode() method can call a reference field’s
hashCode() method and return its value, provided that the class declares a single reference field (and no
primitive type fields).
More commonly, classes declare multiple fields, and a better implementation of the hashCode()
method is required. The implementation should try to generate hash codes that minimize collisions.
There is no rule on how to best implement hashCode(), and various algorithms (recipes for
accomplishing tasks) have been created. My favorite algorithm appears in Effective Java Second Edition,
by Joshua Bloch (Addison-Wesley, 2008; ISBN: 0321356683).
The following algorithm, which assumes the existence of an arbitrary class that is referred to as X,
closely follows Bloch’s algorithm, but is not identical:
1.
Initialize int variable hashCode (the name is arbitrary) to an arbitrary nonzero
integer value, such as 19. This variable is initialized to a nonzero value to
ensure that it takes into account any initial fields whose hash codes are zeros.
If you initialize hashCode to 0, the final hash code will be unaffected by such
fields and you run the risk of increased collisions.
2.
For each field f that is also used in X’s equals() method, calculate f’s hash
code and assign it to int variable hc as follows:
a.
If f is of Boolean type, calculate hc = f?1:0.
b.
If f is of byte integer, character, integer, or short integer type, calculate hc =
(int) f. The integer value is the hash code.
c.
If f is of long integer type, calculate hc = (int) (f^(f>>>32)). This expression
exclusive ORs the long integer’s least significant 32 bits with its most
significant 32 bits.
d.
If f is of type floating-point, calculate hc = Float.floatToIntBits(f). This
method takes +infinity, -infinity, and NaN into account.
e.
If f is of type double precision floating-point, calculate long l =
Double.doubleToLongBits(f); hc = (int) (l^(l>>>32)).
f.
If f is a reference field with a null reference, calculate hc = 0.
373
CHAPTER 5  COLLECTING OBJECTS
g.
If f is a reference field with a nonnull reference, and if X’s equals() method
compares the field by recursively calling equals() (as in Listing 5-12’s Employee
class), calculate hc = f.hashCode(). However, if equals() employs a more
complex comparison, create a canonical (simplest possible) representation of
the field and call hashCode() on this representation.
h.
If f is an array, treat each element as a separate field by applying this algorithm
recursively and combining the hc values as shown in the next step.
3.
Combine hc with hashCode as follows: hashCode = hashCode*31+hc. Multiplying
hashCode by 31 makes the resulting hash value dependent on the order in
which fields appear in the class, which improves the hash value when a class
contains multiple fields that are similar (several ints, for example). I chose 31
to be consistent with the String class’s hashCode() method.
4.
Return hashCode from hashCode().
■ Tip Instead of using this or another algorithm to create a hash code, you might find it easier to work with the
HashCodeBuilder class (see http://commons.apache.org/lang/api2.4/org/apache/commons/lang/builder/HashCodeBuilder.html for an explanation of this class). This class,
which follows Bloch’s rules, is part of the Apache Commons Lang component, which you can download from
http://commons.apache.org/lang/.
In Chapter 2, Listing 2-27’s Point class overrides equals() but does not override hashCode(). I later
presented a small code fragment that must be appended to Point’s main() method to demonstrate the
problem of not overriding hashCode(). I restate this problem here:
Although objects p1 and Point(10, 20) are logically equivalent, these objects have different hash
codes, resulting in each object referring to a different entry in the hashmap. If an object is not stored (via
put()) in that entry, get() returns null.
Listing 5-20 modifies Listing 2-27’s Point class by declaring a hashCode() method. This method uses
the aforementioned algorithm to ensure that logically equivalent Point objects hash to the same entry.
Listing 5-20. Using a hashmap to count command-line arguments
import java.util.HashMap;
import java.util.Map;
class Point
{
private int x, y;
Point(int x, int y)
{
this.x = x;
this.y = y;
}
int getX()
374
CHAPTER 5  COLLECTING OBJECTS
{
return x;
}
int getY()
{
return y;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
@Override
public int hashCode()
{
int hashCode = 19;
int hc = x;
hashCode = hashCode*31+hc;
hc = y;
hashCode = hashCode*31+hc;
return hc;
}
public static void main(String[] args)
{
Point p1 = new Point(10, 20);
Point p2 = new Point(20, 30);
Point p3 = new Point(10, 20);
// Test reflexivity
System.out.println(p1.equals(p1)); // Output: true
// Test symmetry
System.out.println(p1.equals(p2)); // Output: false
System.out.println(p2.equals(p1)); // Output: false
// Test transitivity
System.out.println(p2.equals(p3)); // Output: false
System.out.println(p1.equals(p3)); // Output: true
// Test nullability
System.out.println(p1.equals(null)); // Output: false
// Extra test to further prove the instanceof operator's usefulness.
System.out.println(p1.equals("abc")); // Output: false
Map<Point, String> map = new HashMap<Point, String>();
map.put(p1, "first point");
System.out.println(map.get(p1)); // Output: first point
System.out.println(map.get(new Point(10, 20))); // Output: null
}
}
Listing 5-20’s hashCode() method is a little verbose in that it assigns each of x and y to local variable
hc, rather than directly using these fields in the hash code calculation. However, I decided to follow this
approach to more closely mirror the hash code algorithm.
375
CHAPTER 5  COLLECTING OBJECTS
When you run this application, its last two lines of output are of the most interest. Instead of
presenting first point followed by null on two separate lines, the application now correctly presents
first point followed by first point on these lines.
■ Note LinkedHashMap is a subclass of HashMap that uses a linked list to store its entries. As a result,
LinkedHashMap’s iterator returns entries in the order in which they were inserted. For example, if Listing 5-19 had
specified Map<String, Integer> argMap = new LinkedHashMap<>();, the application’s output for java
HashMapDemo how much wood could a woodchuck chuck if a woodchuck could chuck wood would have
been {how=1, much=1, wood=2, could=2, a=2, woodchuck=2, chuck=2, if=1} followed by Number of
distinct arguments = 8.
IdentityHashMap
The IdentityHashMap class provides a Map implementation that uses reference equality (==) instead of
object equality (equals()) when comparing keys and values. This is an intentional violation of Map’s
general contract, which mandates the use of equals() when comparing elements.
IdentityHashMap obtains hash codes via System’s static int identityHashCode(Object x) method
instead of via each key’s hashCode() method. identityHashCode() returns the same hash code for x as
returned by Object’s hashCode() method, whether or not x’s class overrides hashCode(). The hash code
for the null reference is zero.
These characteristics give IdentityHashMap a performance advantage over other Map
implementations. Also, IdentityHashMap supports mutable keys (objects used as keys and whose hash
codes change when their field values change while in the map). Listing 5-21 contrasts IdentityHashMap
with HashMap where mutable keys are concerned.
Listing 5-21. Contrasting IdentityHashMap with HashMap in a mutable key context
import java.util.IdentityHashMap;
import java.util.HashMap;
import java.util.Map;
class IdentityHashMapDemo
{
public static void main(String[] args)
{
Map<Employee, String> map1 = new IdentityHashMap<>();
Map<Employee, String> map2 = new HashMap<>();
Employee e1 = new Employee("John Doe", 28);
map1.put(e1, "SALES");
System.out.println(map1);
Employee e2 = new Employee("Jane Doe", 26);
map2.put(e2, "MGMT");
System.out.println(map2);
System.out.println("map1 contains key e1 = "+map1.containsKey(e1));
System.out.println("map2 contains key e2 = "+map2.containsKey(e2));
376
CHAPTER 5  COLLECTING OBJECTS
e1.setAge(29);
e2.setAge(27);
System.out.println(map1);
System.out.println(map2);
System.out.println("map1 contains key e1 = "+map1.containsKey(e1));
System.out.println("map2 contains key e2 = "+map2.containsKey(e2));
}
}
class Employee
{
private String name;
private int age;
Employee(String name, int age)
{
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Employee))
return false;
Employee e = (Employee) o;
return e.name.equals(name) && e.age == age;
}
@Override
public int hashCode()
{
int hashCode = 19;
hashCode = hashCode*31+name.hashCode();
hashCode = hashCode*31+age;
return hashCode;
}
void setAge(int age)
{
this.age = age;
}
void setName(String name)
{
this.name = name;
}
@Override
public String toString()
{
return name+" "+age;
}
}
Listing 5-21’s main() method creates IdentityHashMap and HashMap instances that each store an
entry consisting of an Employee key and a String value. Because Employee instances are mutable (because
of setAge() and setName()), main() changes their ages while these keys are stored in their maps. These
changes result in the following output:
377
CHAPTER 5  COLLECTING OBJECTS
{John Doe 28=SALES}
{Jane Doe 26=MGMT}
map1 contains key e1
map2 contains key e2
{John Doe 29=SALES}
{Jane Doe 27=MGMT}
map1 contains key e1
map2 contains key e2
= true
= true
= true
= false
The last four lines show that the changed entries remain in their maps. However, map2’s
containsKey() method reports that its HashMap instance no longer contains its Employee key (which
should be Jane Doe 27), whereas map1’s containsKey() method reports that its IdentityHashMap instance
still contains its Employee key, which is now John Doe 29.
■ Note IdentityHashMap’s documentation states that “a typical use of this class is topology-preserving object
graph transformations, such as serialization or deep copying.” (I discuss serialization in Chapter 8.) It also states
the following: “another typical use of this class is to maintain proxy objects.” Also, developers responding to
stackoverflow’s “Use Cases for Identity HashMap” topic (http://stackoverflow.com/questions/838528/usecases-for-identity-hashmap) mention that it is much faster to use IdentityHashMap than HashMap when the
keys are Class objects.
WeakHashMap
The WeakHashMap class provides a Map implementation that is based on weakly reachable keys. Because
each key object is stored indirectly as the referent of a weak reference, the key is automatically removed
from the map only after the garbage collector clears all weak references to the key (inside and outside of
the map).
■ Note Check out Chapter 4’s “Reference API” section to learn about weakly reachable and weak references.
In contrast, value objects are stored via strong references (and should not strongly refer to their own
keys, either directly or indirectly, because doing so prevents their associated keys from being discarded).
When a key is removed from a map, its associated value object is also removed.
Listing 5-22 provides a simple demonstration of the WeakHashMap class.
Listing 5-22. Detecting a weak hashmap entry’s removal
import java.util.Map;
import java.util.WeakHashMap;
class LargeObject
378
CHAPTER 5  COLLECTING OBJECTS
{
private byte[] memory = new byte[1024*1024*50]; // 50 megabytes
}
class WeakHashMapDemo
{
public static void main(String[] args)
{
Map<LargeObject, String> map = new WeakHashMap<>();
LargeObject lo = new LargeObject();
map.put(lo, "Large Object");
System.out.println(map);
lo = null;
while (!map.isEmpty())
{
System.gc();
new LargeObject();
}
System.out.println(map);
}
}
Listing 5-22’s main() method stores a 50MB LargeObject key and a String value in the weak
hashmap, and then removes the key’s strong reference by assigning null to lo. main() next enters a while
loop that executes until the map is empty (map.isEmpty() returns true).
Each loop iteration begins with a System.gc() method call, which may or may not cause a garbage
collection to take place (depending upon platform). To encourage a garbage collection, the iteration
then creates a LargeObject object and throws away its reference. This activity should eventually cause
the garbage collector to run and remove the map’s solitary entry.
When I run this application on my Windows XP platform, I observe the following output—you might
need to modify the code if you find that the application is in an infinite loop:
{[email protected]=Large Object}
{}
■ Note WeakHashMap is useful for avoiding memory leaks, as explained in Brian Goetz’s article “Java Theory and
Practice: Plugging Memory Leaks with Weak References”
(http://www.ibm.com/developerworks/java/library/j-jtp11225/).
EnumMap
The EnumMap class provides a Map implementation whose keys are the members of the same enum. Null
keys are not permitted; any attempt to store a null key results in a thrown NullPointerException.
Because an enum map is represented internally as an array, an enum map approaches an array in terms
of performance.
EnumMap supplies the following constructors:
379
CHAPTER 5  COLLECTING OBJECTS
•
EnumMap(Class<K> keyType) creates an empty enum map with the specified
keyType. This constructor throws NullPointerException when keyType contains
the null reference.
•
EnumMap(EnumMap<K,? extends V> m) creates an enum map with the same key type
as m, and with m’s entries. This constructor throws NullPointerException when m
contains the null reference.
•
EnumMap(Map<K,? extends V> m) creates an enum map initialized with m’s entries.
If m is an EnumMap instance, this constructor behaves like the previous constructor.
Otherwise, m must contain at least one entry in order to determine the new enum
map’s key type. This constructor throws NullPointerException when m contains
the null reference, and IllegalArgumentException when m is not an EnumMap
instance and is empty.
Listing 5-23 demonstrates EnumMap.
Listing 5-23. An enum map of Coin constants
This book was purchased by [email protected]
import java.util.EnumMap;
import java.util.Map;
enum Coin
{
PENNY, NICKEL, DIME, QUARTER
}
class EnumMapDemo
{
public static void main(String[] args)
{
Map<Coin, Integer> map = new EnumMap<>(Coin.class);
map.put(Coin.PENNY, 1);
map.put(Coin.NICKEL, 5);
map.put(Coin.DIME, 10);
map.put(Coin.QUARTER, 25);
System.out.println(map);
Map<Coin,Integer> mapCopy = new EnumMap<>(map);
System.out.println(mapCopy);
}
}
When you run this application, it generates the following output:
{PENNY=1, NICKEL=5, DIME=10, QUARTER=25}
{PENNY=1, NICKEL=5, DIME=10, QUARTER=25}
SortedMap
TreeMap is an example of a sorted map, which is a map that maintains its entries in ascending order,
sorted according to the keys’ natural ordering or according to a comparator that is supplied when the
sorted map is created. Sorted maps are described by the SortedMap interface.
380
CHAPTER 5  COLLECTING OBJECTS
SortedMap, whose generic type is SortedMap<K,V>, extends Map. With two exceptions, the methods it
inherits from Map behave identically on sorted maps as on other maps:
•
The Iterator instance returned by the iterator() method on any of the sorted
map’s Collection views traverses the collections in order.
•
The arrays returned by the Collection views’ toArray() methods contain the keys,
values, or entries in order.
■ Note Although not guaranteed, the toString() methods of the Collection views of SortedSet
implementations in the Collections Framework (such as TreeMap) return a string containing all of the view’s
elements in order.
SortedMap’s documentation requires that an implementation must provide the four standard
constructors that I presented in my discussion of TreeMap. Furthermore, implementations of this
interface must implement the methods that are described in Table 5-10.
Table 5-10. SortedMap-specific Methods
Method
Description
Comparator<? super K>
comparator()
Return the comparator used to order the keys in this map, or
null when this map uses the natural ordering of its keys.
Set<Map.Entry<K,V>>
entrySet()
Return a Set view of the mappings contained in this map.
The set’s iterator returns these entries in ascending key
order. Because this map backs the view, changes that are
made to the map are reflected in the set and vice versa.
K firstKey()
Return the first (lowest) key currently in this map, or throw a
NoSuchElementException instance when this map is empty.
SortedMap<K,V> headMap(K
toKey)
Return a view of that portion of this map whose keys are
strictly less than toKey. Because this map backs the returned
map, changes in the returned map are reflected in this map
and vice versa. The returned map supports all optional map
operations that this map supports. This method throws
ClassCastException when toKey is not compatible with this
map’s comparator (or, when the map has no comparator,
when toKey does not implement Comparable),
NullPointerException when toKey is null and this map does
not permit null keys, and IllegalArgumentException when
this map has a restricted range and toKey lies outside of this
range’s bounds.
381
CHAPTER 5  COLLECTING OBJECTS
Set<K> keySet()
Return a Set view of the keys contained in this map. The
set’s iterator returns the keys in ascending order. Because
the map backs the view, changes that are made to the map
are reflected in the set and vice versa.
K lastKey()
Return the last (highest) key currently in this map, or throw a
NoSuchElementException instance when this map is empty.
SortedMap<K,V> subMap(K
fromKey, K toKey)
Return a view of the portion of this map whose keys range
from fromKey, inclusive, to toKey, exclusive. (When fromKey
and toKey are equal, the returned map is empty.) Because
this map backs the returned map, changes in the returned
map are reflected in this map and vice versa. The returned
map supports all optional map operations that this map
supports. This method throws ClassCastException when
fromKey and toKey cannot be compared to one another using
this map’s comparator (or, when the map has no
comparator, using natural ordering), NullPointerException
when fromKey or toKey is null and this map does not permit
null keys, and IllegalArgumentException when fromKey is
greater than toKey or when this map has a restricted range
and fromKey or toKey lies outside its bounds.
SortedMap<K,V> tailMap(K
fromKey)
Return a view of that portion of this map whose keys are
greater than or equal to fromKey. Because this map backs the
returned map, changes in the returned map are reflected in
this map and vice versa. The returned map supports all
optional map operations that this map supports. This
method throws ClassCastException when fromKey is not
compatible with this map’s comparator (or, when the map
has no comparator, when fromKey does not implement
Comparable), NullPointerException when fromKey is null and
this map does not permit null keys, and
IllegalArgumentException when this map has a restricted
range and fromKey lies outside of the range’s bounds.
Collection<V> values()
Return a Collection view of the values contained in this
map. The collection’s iterator returns the values in
ascending order of the corresponding keys. Because the map
backs the collection, changes that are made to the map are
reflected in the collection and vice versa.
Listing 5-24 demonstrates a sorted map based on a tree map.
Listing 5-24. A sorted map of office supply names and quantities
import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;
382
CHAPTER 5  COLLECTING OBJECTS
class SortedMapDemo
{
public static void main(String[] args)
{
SortedMap<String, Integer> smsi = new TreeMap<>();
String[] officeSupplies =
{
"pen", "pencil", "legal pad", "CD", "paper"
};
int[] quantities =
{
20, 30, 5, 10, 20
};
for (int i = 0; i < officeSupplies.length; i++)
smsi.put(officeSupplies[i], quantities[i]);
System.out.println(smsi);
System.out.println(smsi.headMap("pencil"));
System.out.println(smsi.headMap("paper"));
SortedMap<String, Integer> smsiCopy;
Comparator<String> cmp;
cmp = new Comparator<String>()
{
public int compare(String key1, String key2)
{
return key2.compareTo(key1); // descending order
}
};
smsiCopy = new TreeMap<String, Integer>(cmp);
smsiCopy.putAll(smsi);
System.out.println(smsiCopy);
}
}
When you run this application (java SortedMapDemo), it generates the following output:
{CD=10, legal pad=5, paper=20, pen=20, pencil=30}
{CD=10, legal pad=5, paper=20, pen=20}
{CD=10, legal pad=5}
{pencil=30, pen=20, paper=20, legal pad=5, CD=10}
NavigableMap
TreeMap is an example of a navigable map, which is a sorted map that can be iterated over in descending
order as well as ascending order, and which can report closest matches for given search targets.
Navigable maps are described by the NavigableMap interface, whose generic type is NavigableMap<K,V>,
which extends SortedMap, and which is described in Table 5-11.
383
CHAPTER 5  COLLECTING OBJECTS
Table 5-11. NavigableMap-specific Methods
384
Method
Description
Map.Entry<K,V>
ceilingEntry(K key)
Return the key-value mapping associated with the least key
greater than or equal to key, or null when there is no such
key. This method throws ClassCastException when key
cannot be compared with the keys currently in the map, and
NullPointerException when key is null and this map does
not permit null keys.
K ceilingKey(K key)
Return the least key greater than or equal to key, or null
when there is no such key. This method throws
ClassCastException when key cannot be compared with the
keys currently in the map, and NullPointerException when
key is null and this map does not permit null keys.
NavigableSet<K>
descendingKeySet()
Return a reverse order navigable set-based view of the keys
contained in this map. The set’s iterator returns the keys in
descending order. This map backs the set, so changes to the
map are reflected in the set and vice versa. If the map is
modified (except through the iterator’s own remove()
operation) while iterating over the set, the results of the
iteration are undefined.
NavigableMap<K,V>
descendingMap()
Return a reverse order view of the mappings contained in
this map. This map backs the descending map, so changes
to the map are reflected in the descending map and vice
versa. If either map is modified while iterating over a
collection view of either map (except through the iterator’s
own remove() operation), the results of the iteration are
undefined.
Map.Entry<K,V> firstEntry()
Return a key-value mapping associated with the least key in
this map, or null when the map is empty.
Map.Entry<K,V> floorEntry(K
key)
Return a key-value mapping associated with the greatest key
less than or equal to key, or null when there is no such key.
This method throws ClassCastException when key cannot
be compared with the keys currently in the map, and
NullPointerException when key is null and this map does
not permit null keys.
K floorKey(K key)
Return the greatest key less than or equal to key, or null
when there is no such key. This method throws
ClassCastException when key cannot be compared with the
keys currently in the map, and NullPointerException when
key is null and this map does not permit null keys.
CHAPTER 5  COLLECTING OBJECTS
NavigableMap<K,V> headMap(K
toKey, boolean inclusive)
Return a view of the portion of this map whose keys are less
than (or equal to, when inclusive is true) toKey. This map
backs the returned map, so changes in the returned map are
reflected in this map and vice versa. The returned map
supports all optional map operations that this map supports.
This method throws ClassCastException when toKey is not
compatible with this map’s comparator (or, when the map
has no comparator, when toMap does not implement
Comparable), NullPointerException when toMap is null and
this map does not permit null keys, and
IllegalArgumentException when this map has a restricted
range and toKey lies outside of this range’s bounds.
Map.Entry<K,V> higherEntry(K
key)
Return a key-value mapping associated with the least key
strictly greater than key, or null when there is no such key.
This method throws ClassCastException when key cannot
be compared with the keys currently in the map, and
NullPointerException when key is null and this map does
not permit null keys.
K higherKey(K key)
Return the least key strictly greater than key, or null when
there is no such key. This method throws
ClassCastException when key cannot be compared with the
keys currently in the map, and NullPointerException when
key is null and this map does not permit null keys.
Map.Entry<K,V> lastEntry()
Return a key-value mapping associated with the greatest key
in this map, or null when the map is empty.
Map.Entry<K,V> lowerEntry(K
key)
Return a key-value mapping associated with the greatest key
strictly less than key, or null when there is no such key. This
method throws ClassCastException when key cannot be
compared with the keys currently in the map, and
NullPointerException when key is null and this map does
not permit null keys.
K lowerKey(K key)
Return the greatest key strictly less than key, or null when
there is no such key. This method throws
ClassCastException when key cannot be compared with the
keys currently in the map, and NullPointerException when
key is null and this map does not permit null keys.
NavigableSet<K>
navigableKeySet()
Return a navigable set-based view of the keys contained in
this map. The set’s iterator returns the keys in ascending
order. This map backs the set, so changes to the map are
reflected in the set and vice versa. If the map is modified
while iterating over the set (except through the iterator’s
own remove() operation), the results of the iteration are
385
CHAPTER 5  COLLECTING OBJECTS
undefined.
Map.Entry<K,V>
pollFirstEntry()
Remove and return a key-value mapping associated with the
least key in this map, or null when the map is empty.
Map.Entry<K,V>
pollLastEntry()
Remove and return a key-value mapping associated with the
greatest key in this map, or null when the map is empty.
NavigableMap<K,V> subMap(K
fromKey, boolean
fromInclusive, K toKey,
boolean toInclusive)
Return a view of the portion of this map whose keys range
from fromKey to toKey. (When fromKey and toKey are equal,
the returned map is empty unless fromInclusive and
toInclusive are both true.) This map backs the returned
map, so changes in the returned map are reflected in this
map and vice versa. The returned map supports all optional
map operations that this map supports. This method throws
ClassCastException when fromKey and toKey cannot be
compared to one another using this map’s comparator (or,
when the map has no comparator, using natural ordering),
NullPointerException when fromKey or toKey is null and this
map does not permit null elements, and
IllegalArgumentException when fromKey is greater than
toKey or when this map has a restricted range and fromKey or
toMap lies outside of this range’s bounds.
NavigableMap<K,V> tailMap(K
fromKey, boolean inclusive)
Return a view of the portion of this map whose keys are
greater than (or equal to, when inclusive is true) fromKey.
This map backs the returned map, so changes in the
returned map are reflected in this map and vice versa. The
returned map supports all optional map operations that this
map supports. This method throws ClassCastException
when fromKey is not compatible with this map’s comparator
(or, when the map has no comparator, when fromKey does
not implement Comparable), NullPointerException when
fromKey is null and this map does not permit null keys, and
IllegalArgumentException when this map has a restricted
range and fromKey lies outside of this range’s bounds.
Table 5-11’s methods describe the NavigableMap equivalents of the NavigableSet methods presented
in Table 5-4, and even return NavigableSet instances in two instances.
Listing 5-25 demonstrates a navigable map based on a tree map.
Listing 5-25. Navigating a map of (bird, count within a small acreage) entries
import
import
import
import
java.util.Iterator;
java.util.NavigableMap;
java.util.NavigableSet;
java.util.TreeMap;
class NavigableMapDemo
386
CHAPTER 5  COLLECTING OBJECTS
{
public static void main(String[] args)
{
NavigableMap<String,Integer> nm = new TreeMap<>();
String[] birds = { "sparrow", "bluejay", "robin" };
int[] ints = { 83, 12, 19 };
for (int i = 0; i < birds.length; i++)
nm.put(birds[i], ints[i]);
System.out.println("Map = "+nm);
System.out.print("Ascending order of keys: ");
NavigableSet<String> ns = nm.navigableKeySet();
Iterator iter = ns.iterator();
while (iter.hasNext())
System.out.print(iter.next()+" ");
System.out.println();
System.out.print("Descending order of keys: ");
ns = nm.descendingKeySet();
iter = ns.iterator();
while (iter.hasNext())
System.out.print(iter.next()+" ");
System.out.println();
System.out.println("First entry = "+nm.firstEntry());
System.out.println("Last entry = "+nm.lastEntry());
System.out.println("Entry < ostrich is "+nm.lowerEntry("ostrich"));
System.out.println("Entry > crow is "+nm.higherEntry("crow"));
System.out.println("Poll first entry: "+nm.pollFirstEntry());
System.out.println("Map = "+nm);
System.out.println("Poll last entry: "+nm.pollLastEntry());
System.out.println("Map = "+nm);
}
}
Listing 5-25’s System.out.println("Map = "+nm); method calls rely on TreeMap’s toString() method
to obtain the contents of a navigable map.
When you run this application, you observe the following output:
Map = {bluejay=12, robin=19, sparrow=83}
Ascending order of keys: bluejay robin sparrow
Descending order of keys: sparrow robin bluejay
First entry = bluejay=12
Last entry = sparrow=83
Entry < ostrich is bluejay=12
Entry > crow is robin=19
Poll first entry: bluejay=12
Map = {robin=19, sparrow=83}
Poll last entry: sparrow=83
Map = {robin=19}
387
CHAPTER 5  COLLECTING OBJECTS
Utilities
The Collections Framework would not be complete without its Arrays and Collections utility classes.
Each class supplies various class methods that implement useful algorithms in the contexts of arrays and
collections.
Following is a sampling of the Arrays class’s array-oriented utility methods:
•
static <T> List<T> asList(T... a) returns a fixed-size list backed by array a.
(Changes to the returned list “write through” to the array.) For example,
List<String> birds = Arrays.asList("Robin", "Oriole", "Bluejay"); converts
the three-element array of Strings (recall that a variable sequence of arguments is
implemented as an array) to a List whose reference is assigned to birds.
•
static int binarySearch(int[] a, int key) searches array a for entry key using
the binary search algorithm (explained following this list). The array must be
sorted before calling this method; otherwise, the results are undefined. This
method returns the index of the search key, if it is contained in the array;
otherwise, (-(insertion point)-1) is returned. The insertion point is the point at
which key would be inserted into the array (the index of the first element greater
than key, or a.length if all elements in the array are less than key) and guarantees
that the return value will be greater than or equal to 0 if and only if key is found.
For example, Arrays.binarySearch(new String[] {"Robin", "Oriole",
"Bluejay"}, "Oriole") returns 1, "Oriole"’s index.
•
static void fill(char[] a, char ch) stores ch in each element of the specified
character array. For example, Arrays.fill(screen[i], ' '); fills the ith row of a
2D screen array with spaces.
•
static void sort(long[] a) sorts the elements in long integer array a into
ascending numerical order; for example, long lArray = new long[] { 20000L,
89L, 66L, 33L}; Arrays.sort(lArray);.
•
static <T> void sort(T[] a, Comparator<? super T> c) sorts the elements in
array a using comparator c to order them. For example, when given
Comparator<String> cmp = new Comparator<String>() { public int
compare(String e1, String e2) { return e2.compareTo(e1); } }; String[]
innerPlanets = { "Mercury", "Venus", "Earth", "Mars" };,
Arrays.sort(innerPlanets, cmp); uses cmp to help in sorting innerPlanets into
descending order of its elements: Venus, Mercury, Mars, Earth is the result.
There are two common algorithms for searching an array for a specific element. Linear search
searches the array element by element from index 0 to the index of the searched-for element or the end
of the array. On average, half of the elements must be searched; larger arrays take longer to search.
However, the arrays do not need to be sorted.
In contrast, binary search searches ordered array a’s n items for element e in a much faster amount
of time. It works by recursively performing the following steps:
388
1.
Set low index to 0.
2.
Set high index to n-1.
3.
If low index > high index, then Print “Unable to find ” e. End.
CHAPTER 5  COLLECTING OBJECTS
4.
Set middle index to (low index+high index)/2.
5.
If e > a[middle index], then set low index to middle index+1. Go to 3.
6.
If e < a[middle index], then set high index to middle index-1. Go to 3.
7.
Print “Found ” e “ at index ” middle index.
The algorithm is similar to optimally looking for a name in a phone book. Start by opening the book
to the exact middle. If the name is not on that page, proceed to open the book to the exact middle of the
first half or the second half, depending on in which half the name occurs. Repeat until you find the name
(or not).
Applying a linear search to 4,000,000,000 elements results in approximately 2,000,000,000
comparisons (on average), which takes time. In contrast, applying a binary search to 4,000,000,000
elements results in a maximum of 32 comparisons. This is why Arrays contains binarySearch() methods
and not also linearSearch() methods.
Following is a sampling of the Collections class’s collection-oriented class methods:
•
static <T extends Object&Comparable<? super T>> T min(Collection<? extends
T> c) returns the minimum element of collection c according to the natural
ordering of its elements. For example,
System.out.println(Collections.min(Arrays.asList(10, 3, 18, 25))); outputs
3. All of c’s elements must implement the Comparable interface. Furthermore, all
elements must be mutually comparable. This method throws
NoSuchElementException when c is empty.
•
static void reverse(List<?> l) reverses the order of list l’s elements. For
example, List<String> birds = Arrays.asList("Robin", "Oriole", "Bluejay");
Collections.reverse(birds); System.out.println(birds); results in [Bluejay,
Oriole, Robin] as the output.
•
static <T> List<T> singletonList(T o) returns an immutable list containing
only object o. For example, list.removeAll(Collections.singletonList(null));
removes all null elements from list.
•
static <T> Set<T> synchronizedSet(Set<T> s) returns a synchronized (threadsafe) set backed by set s; for example, Set<String> ss =
Collections.synchronizedSet(new HashSet<String>());. In order to guarantee
serial access, it is critical that all access to the backing set is accomplished through
the returned set.
•
static <K,V> Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m)
returns an unmodifiable view of map m; for example, Map<String, Integer> msi =
Collections.synchronizedMap(new HashMap<String, Integer>());. Query
operations on the returned map “read through” to the specified map, and
attempts to modify the returned map, whether direct or via its collection views,
result in an UnsupportedOperationException.
■ Note For performance reasons, collections implementations are unsynchronized—unsynchronized collections
have better performance than synchronized collections. To use a collection in a multithreaded context, however,
389
CHAPTER 5  COLLECTING OBJECTS
you need to obtain a synchronized version of that collection. You obtain that version by calling a method such as
synchronizedSet().
You might be wondering about the purpose for the various “empty” class methods in the
Collections class. For example, static final <T> List<T> emptyList() returns an immutable empty
list, as in List<String> ls = Collections.emptyList();. These methods are present because they offer a
useful alternative to returning null (and avoiding potential NullPointerExceptions) in certain contexts.
Consider Listing 5-26.
Listing 5-26. Empty and nonempty Lists of Birds
import
import
import
import
java.util.ArrayList;
java.util.Collections;
java.util.Iterator;
java.util.List;
This book was purchased by [email protected]
class Birds
{
private List<String> birds;
Birds()
{
birds = Collections.emptyList();
}
Birds(String... birdNames)
{
birds = new ArrayList<String>();
for (String birdName: birdNames)
birds.add(birdName);
}
@Override
public String toString()
{
return birds.toString();
}
}
class EmptyListDemo
{
public static void main(String[] args)
{
Birds birds = new Birds();
System.out.println(birds);
birds = new Birds("Swallow", "Robin", "Bluejay", "Oriole");
System.out.println(birds);
}
}
390
CHAPTER 5  COLLECTING OBJECTS
Listing 5-26 declares a Birds class that stores the names of various birds in a list. This class provides
two constructors, a noargument constructor and a constructor that takes a variable number of String
arguments identifying various birds.
The noargument constructor invokes emptyList() to initialize its private birds field to an empty
List of String—emptyList() is a generic method and the compiler infers its return type from its context.
If you’re wondering about the need for emptyList(), look at the toString() method. Notice that this
method evaluates birds.toString(). If we did not assign a reference to an empty List<String> to birds,
birds would contain null (the default value for this instance field when the object is created), and a
NullPointerException instance would be thrown when attempting to evaluate birds.toString().
When you run this application (java EmptyListDemo), it generates the following output:
[]
[Swallow, Robin, Bluejay, Oriole]
The emptyList() method is implemented as follows: return (List<T>) EMPTY_LIST;. This statement
returns the single List instance assigned to the EMPTY_LIST class field in the Collections class.
You might want to work with EMPTY_LIST directly, but you’ll run into an unchecked warning message
if you do, because EMPTY_LIST is declared to be of the raw type List, and mixing raw types with generic
types leads to such messages. Although you could suppress the warning, you’re better off using the
emptyList() method.
Suppose you add a void setBirds(List<String> birds) method to Birds, and pass an empty list to
this method, as in birds.setBirds(Collections.emptyList());. The compiler will respond with an error
message stating that it requires the argument to be of type List<String>, but instead the argument is of
type List<Object>. It does so because the compiler cannot figure out the proper type from this context,
and so it chooses List<Object>.
There is a way to solve this problem, which will probably look very strange. Specify
birds.setBirds(Collections.<String>emptyList());, where the formal type parameter list and its actual
type argument appear after the member access operator and before the method name. The compiler will
now know that the proper type argument is String, and that emptyList() is to return List<String>.
Legacy Collections APIs
Java 1.2 introduced the Collections Framework. Prior to the framework’s inclusion in Java, developers
had two choices where collections were concerned: create their own frameworks, or use the Vector,
Enumeration, Stack, Dictionary, Hashtable, Properties, and BitSet types, which were introduced by Java
1.0.
Vector is a concrete class that describes a growable array, much like ArrayList. Unlike an ArrayList
instance, a Vector instance is synchronized. Vector has been generified and also retrofitted to support
the Collections Framework, which makes statements such as List<String> list = new
Vector<String>(); legal.
The Collections Framework provides Iterator for iterating over a collection’s elements. In contrast,
Vector’s elements() method returns an instance of a class that implements the Enumeration interface for
enumerating (iterating over and returning) a Vector instance’s elements via Enumeration’s
hasMoreElements() and nextElement() methods.
Vector is subclassed by the concrete Stack class, which represents a LIFO data structure. Stack
provides an E push(E item) method for pushing an object onto the stack, an E pop() method for
popping an item off the top of the stack, and a few other methods, such as boolean empty() for
determining whether or not the stack is empty.
Stack is a good example of bad API design. By inheriting from Vector, it is possible to call Vector’s
void add(int index, E element) method to add an element anywhere you wish, and violate a Stack
391
CHAPTER 5  COLLECTING OBJECTS
instance’s integrity. In hindsight, Stack should have used composition in its design: use a Vector
instance to store a Stack instance’s elements.
Dictionary is an abstract superclass for subclasses that map keys to values. The concrete Hashtable
class is Dictionary’s only subclass. As with Vector, HashTable instances are synchronized, HashTable has
been generified, and HashTable has been retrofitted to support the Collections Framework.
Hashtable is subclassed by Properties, a concrete class representing a persistent set of properties
(String-based key/value pairs that identify application settings). Properties provides Object
setProperty(String key, String value) for storing a property, and public String getProperty(String
key) for returning a property’s value.
■ Note Application’s use properties for various purposes. For example, if your application has a graphical user
interface, you might persist its main window’s screen location and size to a file via a Properties object so that
the application can restore the window’s location and size when it next runs.
Properties is another good example of bad API design. By inheriting from Hashtable, you can call
Hashtable’s V put(K key, V value) method to store an entry with a non-String key and/or a non-String
value. In hindsight, Properties should have leveraged composition: store a Properties instance’s
elements in a Hashtable instance.
■ Note Chapter 2 discusses wrapper classes, which is how Stack and Properties should have been
implemented.
Finally, BitSet is a concrete class that describes a variable-length set of bits. This class’s ability to
represent bitsets of arbitrary length contrasts with the previously described integer-based, fixed-length
bitset that is limited to a maximum number of members: 32 members for an int-based bitset, or 64
members for a long-based bitset.
BitSet provides a pair of constructors for initializing a BitSet instance: BitSet() initializes the
instance to initially store an implementation-dependent number of bits, whereas BitSet(int nbits)
initializes the instance to initially store nbits bits. BitSet also provides various methods, including the
following:
392
•
void and(BitSet bs) bitwise ANDs this bitset with bs. This bitset is modified such
that a bit is set to 1 when it and the bit at the same position in bs are 1.
•
void andNot(BitSet bs) sets all the bits in this bitset to 0 whose corresponding
bits are set to 1 in bs.
•
void clear() sets all the bits in this bitset to 0.
•
Object clone() clones this bitset to produce a new bitset. The clone has exactly
the same bits set to one as this bitset.
CHAPTER 5  COLLECTING OBJECTS
•
boolean get(int bitIndex) returns the value of this bitset’s bit, as a Boolean
true/false value (true for 1, false for 0) at the zero-based bitIndex. This method
throws IndexOutOfBoundsException when bitIndex is less than 0.
•
int length() returns the “logical size” of this bitset, which is the index of the
highest 1 bit plus 1, or 0 when this bitset contains no 1 bits.
•
void or(BitSet bs) bitwise inclusive ORs this bitset with bs. This bitset is
modified such that a bit is set to 1 when it or the bit at the same position in bs is 1,
or when both bits are 1.
•
void set(int bitIndex, boolean value) sets the bit at the zero-based bitIndex to
value (true is converted to 1; false is converted to 0). This method throws
IndexOutOfBoundsException when bitIndex is less than 0.
•
int size() returns the number of bits that are being used by this bitset to
represent bit values.
•
String toString() returns a string representation of this bitset in terms of the
positions of bits that are 1; for example, {4, 5, 9, 10}.
•
void xor(BitSet set) bitwise exclusive ORs this bitset with bs. This bitset is
modified such that a bit is set to 1 when either it or the bit at the same position in
bs (but not both) is 1.
Listing 5-27 presents an application that demonstrates some of these methods, and gives you more
insight into how the bitwise AND (&), bitwise inclusive OR (|), and bitwise exclusive OR (^) operators
work.
Listing 5-27. Working with variable-length bitsets
import java.util.BitSet;
class BitSetDemo
{
public static void main(String[] args)
{
BitSet bs1 = new BitSet();
bs1.set(4, true);
bs1.set(5, true);
bs1.set(9, true);
bs1.set(10, true);
BitSet bsTemp = (BitSet) bs1.clone();
dumpBitset("
", bs1);
BitSet bs2 = new BitSet();
bs2.set(4, true);
bs2.set(6, true);
bs2.set(7, true);
bs2.set(9, true);
dumpBitset("
", bs2);
bs1.and(bs2);
dumpSeparator(Math.min(bs1.size(), 16));
dumpBitset("AND (&) ", bs1);
393
CHAPTER 5  COLLECTING OBJECTS
System.out.println();
bs1 = bsTemp;
dumpBitset("
", bs1);
dumpBitset("
", bs2);
bsTemp = (BitSet) bs1.clone();
bs1.or(bs2);
dumpSeparator(Math.min(bs1.size(), 16));
dumpBitset("OR (|) ", bs1);
System.out.println();
bs1 = bsTemp;
dumpBitset("
", bs1);
dumpBitset("
", bs2);
bsTemp = (BitSet) bs1.clone();
bs1.xor(bs2);
dumpSeparator(Math.min(bs1.size(), 16));
dumpBitset("XOR (^) ", bs1);
}
static void dumpBitset(String preamble, BitSet bs)
{
System.out.print(preamble);
int size = Math.min(bs.size(), 16);
for (int i = 0; i < size; i++)
System.out.print(bs.get(i) ? "1" : "0");
System.out.print(" size("+bs.size()+"), length("+bs.length()+")");
System.out.println();
}
static void dumpSeparator(int len)
{
System.out.print("
");
for (int i = 0; i < len; i++)
System.out.print("-");
System.out.println();
}
}
Why did I specify Math.min(bs.size(), 16) in dumpBitset(), and pass a similar expression to
dumpSeparator()? I wanted to display exactly 16 bits and 16 dashes (for aesthetics), and needed to
account for a bitset’s size being less than 16. Although this does not happen with the JDK’s BitSet class,
it might happen with a non-JDK variant.
When you run this application, it generates the following output:
0000110001100000
0000101101000000
---------------AND (&) 0000100001000000
size(64), length(11)
size(64), length(10)
0000110001100000
0000101101000000
---------------0000111101100000
size(64), length(11)
size(64), length(10)
0000110001100000
size(64), length(11)
OR (|)
394
size(64), length(10)
size(64), length(11)
CHAPTER 5  COLLECTING OBJECTS
0000101101000000
---------------XOR (^) 0000011100100000
size(64), length(10)
size(64), length(11)
■ Caution Unlike Vector and Hashtable, BitSet is not synchronized. You must externally synchronize access to
this class when using BitSet in a multithreaded context.
The Collections Framework has made Vector, Stack, Dictionary, and Hashtable obsolete. These
types continue to be part of the standard class library to support legacy code.
The framework’s Iterator interface has largely obsoleted the Enumeration interface. However,
because the java.util.StringTokenizer class (which is somewhat useful, and which is briefly discussed
in Chapter 6) uses Enumeration, this interface still has some credibility.
The Preferences API (see Appendix C) has made Properties largely obsolete. However, the standard
class library still uses Properties in various places (such as in the context of XSLT, discussed in Chapter
10). You’ll probably have a few uses for this class as well.
Because BitSet is still relevant, this class continues to be improved. For example, Java 7 introduces
new valueOf() class methods (such as static BitSet valueOf(byte[] bytes)) and instance methods
(such as int previousSetBit(int fromIndex)) into this class.
■ Note It is not surprising that BitSet is still being improved (as recently as Java 7 at time of writing) when you
realize the usefulness of variable-length bitsets. Because of their compactness and other advantages, variablelength bitsets are often used to implement an operating system’s priority queues and facilitate memory page
allocation. Unix-oriented file systems also use bitsets to facilitate the allocation of inodes (information nodes) and
disk sectors. And bitsets are useful in Huffman coding, a data-compression algorithm for achieving lossless data
compression.
Creating Your Own Collections
Arrays, the Collections Framework, and legacy classes such as BitSet are suitable for organizing groups
of objects (or, in the case of BitSet, sets of bits that are interpreted as Boolean true/false values), and you
should use them wherever possible before creating your own collection APIs. After all, why “reinvent the
wheel?”
The Collections Framework supports lists, sets, queues, deques, and maps. If your collection
requirement can fit into one of these categories, then go with this framework. Keep in mind that you can
also take advantage of trees in TreeSet and TreeMap implementation contexts, and stacks in deque
contexts.
Perhaps you need a different implementation of one of the Collections Framework core interfaces. If
so, you can extend this framework by implementing the interface, or by subclassing one of the more
convenient “Abstract” classes, such as AbstractQueue. For example, author Cay Horstmann
demonstrates extending this class to implement a circular array queue (see
395
CHAPTER 5  COLLECTING OBJECTS
http://www.java2s.com/Code/Java/Collections-DataStructure/Howtoextendthecollectionsframework.htm).
OBEYING CONTRACTS
When you implement a core interface or extend one of the Abstract classes, you should ensure that your
implementation class doesn’t deviate from the various contracts described in the Java documentation for
these interfaces. For example, List places the following stipulation on the hashCode() method that it
inherits from Collection:
The hash code of a list is defined to be the result of the following calculation:
int hashCode = 1;
for (E e: list)
hashCode = 31*hashCode+(e==null ? 0 : e.hashCode());
This calculation ensures that list1.equals(list2) implies that list1.hashCode() ==
list2.hashCode() for any two lists, list1 and list2, as required by the general contract of
Object.hashCode().
The AbstractList class, which partially implements List, has this to say about hashCode():This
implementation uses exactly the code that is used to define the list hash function in the documentation for
the List.hashCode() method.
When it comes to lists, you should also be aware of the RandomAccess interface:
ArrayList implements the RandomAccess interface, which is a marker interface used by List
implementation classes to indicate that they support fast (generally constant time) random access. The
primary purpose of this interface is to allow generic algorithms to alter their behavior to provide good
performance when applied to either random or sequential access lists.
The best algorithms for manipulating random access List implementations (such as ArrayList) can
produce quadratic behavior when applied to sequential access List implementations (such as
LinkedList). Generic list algorithms are encouraged to check whether the given list is an instance of this
interface (via instanceof) before applying an algorithm that would provide poor performance if it were
applied to a sequential access list, and to alter their behavior if necessary to guarantee acceptable
performance.
The distinction between random and sequential access is often fuzzy. For example, some List
implementations provide asymptotically linear (a line whose distance to a given curve tends to zero) access
times if they get huge, but constant access times in practice. Such a List implementation class should
generally implement this interface. As a rule of thumb, a List implementation class should implement this
interface if, for typical instances of the class, the following loop:
for (int i=0, n=list.size(); i < n; i++) list.get(i);
runs faster than the following loop:
for (Iterator i=list.iterator(); i.hasNext();) i.next();
396
CHAPTER 5  COLLECTING OBJECTS
Keep these advices in mind and you should find it easier to extend the Collections Framework.
You might require a collection that isn’t supported by the Collections Framework (or perhaps you
only think it isn’t supported). For example, you might want to model a sparse matrix, a table where many
or most of its elements are zeros (see http://en.wikipedia.org/wiki/Sparse_matrix). A sparse matrix is
a good data structure for implementing a spreadsheet, for example.
If the elements represent bits, you could use BitSet to represent the matrix. If the elements are
objects, you might use an array. The problem with either approach is scalability and the limits of heap
space. For example, suppose you need a table with 100,000 rows and 100,000 columns, yielding a
maximum of 10 billion elements.
You can forget about using BitSet (assuming that each entry occupies a single bit) because
10,000,000,000 is too large to pass to the BitSet(int nbits) constructor; some information will be lost
when you cast this long integer to an integer. You can also forget about using an array because you’ll
exhaust the JVM’s memory and obtain a java.lang.OutOfMemoryError at runtime.
Because you’re dealing with a sparse matrix, assume that no more than 25,000 table entries are
nonzero at any one time. After all, a sparse matrix has a sparse number of nonzero entries. This is a lot
more manageable.
You won’t use BitSet to represent this matrix because you’ll assume that each matrix entry is an
object. You can’t use a two-dimensional array to store these objects because the array would need
100,000 rows by 100,000 columns to properly index the sparse matrix, and you would exhaust memory
by being extremely wasteful in storing zero (or null, in the case of object) values.
There is another way to represent this matrix, and that is to create a linked list of nodes.
A node is an object consisting of value and link fields. Unlike an array, where each element stores a
single value of the same primitive type or reference supertype, a node can store multiple values of
different types. It can also store links (references to other nodes).
Consider Listing 5-28’s Node class:
Listing 5-28. A node consists of value fields and link fields
class Node
{
// value field
String name;
// link field
Node next;
}
Node describes simple nodes where each node consists of a single name value field and a single next
link field. Notice that next is of the same type as the class in which it is declared. This arrangement lets a
node instance store a reference to another node instance (which is the next node) in this field. The
resulting nodes are linked together.
Listing 5-29 presents a Nodes class that demonstrates connecting Nodes together into a linked list,
and then iterating over this list to output the values of the name fields.
Listing 5-29. Creating and iterating over a linked list of nodes
class Nodes
{
public static void main(String[] args)
{
Node top = new Node();
397
CHAPTER 5  COLLECTING OBJECTS
top.name = "node 1";
top.next = new Node();
top.next.name = "node 2";
top.next.next = new Node();
top.next.next.name = "node 3";
top.next.next.next = null;
Node temp = top;
while (temp != null)
{
System.out.println(temp.name);
temp = temp.next;
}
}
}
Listing 5-29 demonstrates the creation of a singly linked list (a list where each node consists of a
single link field). The first Node instance is pointed to by reference variable top, which identifies the top
of the list. Each subsequent node in this linked list is referenced from its predecessor’s next field. The
final next field is set to null to signify the end of the linked list. (This explicit initialization is unnecessary
because the field defaults to the null reference during instance initialization, but is present for clarity).
Figure 5-5 reveals this three-node linked list.
Figure 5-5. Reference variable top points to the first node in this three-node linked list.
Listing 5-29 also shows you how to traverse this singly linked list by following each Node object’s next
field. Prior to the traversal, top’s reference is assigned to variable temp, to preserve the start of this linked
list so that further manipulations (node insertions, removals, updates) and searches can be performed.
The while loop iterates until temp contains the null reference, outputting each node’s name field and
assigning the reference in the current node’s next field to temp.
When you run this application, it generates the following output:
node 1
node 2
node 3
You might declare the following Cell class to represent a sparse matrix node for a spreadsheet,
which is known as a cell:
class Cell
{
int row;
int col;
Object value;
Node next;
}
When called upon to update the spreadsheet on the screen, your spreadsheet application’s
rendering code traverses its linked list of Cell nodes. For each cell, it first examines (row, col) to learn if
the cell is visible and should be rendered. If the cell is visible, the instanceof operator is used to
determine value’s type, and value is then displayed. As soon as null is encountered, the rendering code
knows that there are no more spreadsheet elements to render.
398
CHAPTER 5  COLLECTING OBJECTS
Before creating your own linked list class to store Cell instances, you should realize that doing so
isn’t necessary. Instead, you can leverage the Collection Framework’s LinkedList class to store Cell
instances (without the unnecessary next fields). Although you might occasionally need to create your
own node-based collections, the moral of this exercise is that you should always think about using
arrays, the Collections Framework, or a legacy class such as BitSet before inventing your own API to
collect objects.
EXERCISES
The following exercises are designed to test your understanding of collections:
1.
As an example of array list usefulness, create a JavaQuiz application that
presents a multiple-choice-based quiz on Java features. The JavaQuiz class’s
main() method first populates the array list with the entries in a QuizEntry array
(e.g., new QuizEntry("What was Java's original name?", new String[] {
"Oak", "Duke", "J", "None of the above" },'A')). Each entry consists of a
question, four possible answers, and the letter (A, B, C, or D) of the correct
answer. main() then uses the array list’s iterator() method to return an
Iterator instance, and this instance’s hasNext() and next() methods to iterate
over the list. Each of the iterations outputs the question and four possible answers,
and then prompts the user to enter the correct choice. After the user enters A, B,
C, or D (via System.in.read()), main() outputs a message stating whether or not
the user made the correct choice.
2.
Create a word-counting application (WC) that reads words from the standard input
(via System.in.read()) and stores them in a map along with their frequency
counts. For this exercise, a word consists of letters only; use the
java.lang.Character class’s isLetter() method to make this determination.
Also, use Map’s get() and put() methods and take advantage of autoboxing to
record a new entry or update an existing entry’s count—the first time a word is
seen, its count is set to 1. Use Map’s entrySet() method to return a Set of
entries, and iterate over these entries, outputting each entry to the standard
output.
3.
Collections provides the static int frequency(Collection<?> c, Object
o) method to return the number of collection c elements that are equal to o.
Create a FrequencyDemo application that reads its command-line arguments and
stores all arguments except for the last argument in a list, and then calls
frequency() with the list and last command-line argument as this method’s
arguments. It then outputs this method’s return value (the number of occurrences
of the last command-line argument in the previous command-line arguments). For
example, java FrequencyDemo should output Number of occurrences of null
= 0, and java FrequencyDemo how much wood could a woodchuck chuck if
a woodchuck could chuck wood wood should output Number of occurrences
of wood = 2.
399
CHAPTER 5  COLLECTING OBJECTS
Summary
This book was purchased by [email protected]
The Collections Framework is a standard architecture for representing and manipulating collections,
which are groups of objects stored in instances of classes designed for this purpose. This framework
largely consists of core interfaces, implementation classes, and utility classes.
The core interfaces make it possible to manipulate collections independently of their
implementations. They include Iterable, Collection, List, Set, SortedSet, NavigableSet, Queue, Deque,
Map, SortedMap, and NavigableMap. Collection extends Iterable; List, Set, and Queue each extend
Collection; SortedSet extends Set; NavigableSet extends SortedSet; Deque extends Queue; SortedMap
extends Map; and NavigableMap extends SortedMap.
The framework’s implementation classes include ArrayList, LinkedList, TreeSet, HashSet,
LinkedHashSet, EnumSet, PriorityQueue, ArrayDeque, TreeMap, HashMap, LinkedHashMap, IdentityHashMap,
WeakHashMap, and EnumMap. The name of each concrete class ends in a core interface name, identifying the
core interface on which it is based.
The framework’s implementation classes also include the abstract AbstractCollection,
AbstractList, AbstractSequentialList, AbstractSet, AbstractQueue, and AbstractMap classes. These
classes offer skeletal implementations of the core interfaces to facilitate the creation of concrete
implementation classes.
The Collections Framework would not be complete without its Arrays and Collections utility
classes. Each class supplies various class methods that implement useful algorithms in the contexts of
arrays and collections.
Before Java 1.2’s introduction of the Collections Framework, developers had two choices where
collections were concerned: create their own frameworks, or use the Vector, Enumeration, Stack,
Dictionary, Hashtable, Properties, and BitSet types, which were introduced by Java 1.0.
The Collections Framework has made Vector, Stack, Dictionary, and Hashtable obsolete. The
framework’s Iterator interface has largely obsoleted the Enumeration interface. The Preferences API has
made Properties largely obsolete. Because BitSet is still relevant, this class continues to be improved.
Arrays, the Collections Framework, and legacy classes such as BitSet are suitable for organizing
groups of objects (or, in the case of BitSet, sets of bits that are interpreted as Boolean true/false values),
and you should use them wherever possible before creating your own collection APIs.
However, you might need a different implementation of one of the Collections Framework core
interfaces. If so, you can extend this framework by implementing the interface, or by subclassing one of
the more convenient “Abstract” classes, such as AbstractQueue.
You might require a collection that isn’t supported by the Collections Framework (or perhaps you
only think it isn’t supported). For example, you might want to model a sparse matrix, a table where
many or most of its elements are zeros. A sparse matrix is a good data structure for implementing a
spreadsheet, for example.
To model a spreadsheet or other sparse matrix, you can work with nodes, which are objects
consisting of value and link fields. Unlike an array, where each element stores a single value of the same
primitive type or reference supertype, a node can store multiple values of different types. It can also
store references to other nodes, which are known as links.
You can connect nodes together into linked lists, but (at least for singly linked lists) there is no need
to do so because you can take advantage of the Collections Framework’s LinkedList class for this task.
After all, you should not “reinvent the wheel.”
Broadly speaking, the Collections Framework is an example of a utility API. Chapter 6 continues to
focus on utility APIs by introducing you to Java’s concurrency utilities, which extend the Collections
Framework, the java.util.Objects class, and more.
400
CHAPTER 6
Touring Additional Utility APIs
Chapter 5 introduced you to the Collections Framework, which is a collection of utility APIs. Chapter 6
introduces you to additional utility APIs, specifically the concurrency utilities, Objects, and Random.
Concurrency Utilities
Java 5 introduced the concurrency utilities, which are classes and interfaces that simplify the
development of concurrent (multithreaded) applications. These types are located in the
java.util.concurrent package and in its java.util.concurrent.atomic and
java.util.concurrent.locks subpackages.
The concurrency utilities leverage the low-level Threading API (see Chapter 4) in their
implementations and provide higher-level building blocks to simplify creating multithreaded
applications. They are organized into executor, synchronizer, concurrent collection, lock, atomic
variable, and additional utility categories.
Executors
Chapter 4 introduced the Threading API, which lets you execute runnable tasks via expressions such as
new Thread(new RunnableTask()).start();. These expressions tightly couple task submission with the
task’s execution mechanics (run on the current thread, a new thread, or a thread arbitrarily chosen from
a pool [group] of threads).
 Note A task is an object whose class implements the java.lang.Runnable interface (a runnable task) or the
java.util.concurrent.Callable interface (a callable task).
The concurrency utilities provide executors as a high-level alternative to low-level Threading API
expressions for executing runnable tasks. An executor is an object whose class directly or indirectly
implements the java.util.concurrent.Executor interface, which decouples task submission from taskexecution mechanics.
401
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
 Note The executor framework’s use of interfaces to decouple task submission from task-execution mechanics
is analogous to the Collections Framework’s use of core interfaces to decouple lists, sets, queues, deques, and
maps from their implementations. Decoupling results in flexible code that is easier to maintain.
Executor declares a solitary void execute(Runnable runnable) method that executes the runnable
task named runnable at some point in the future. execute() throws java.lang.NullPointerException
when runnable is null, and java.util.concurrent.RejectedExecutionException when it cannot execute
runnable.
 Note RejectedExecutionException can be thrown when an executor is shutting down and does not want to
accept new tasks. Also, this exception can be thrown when the executor does not have enough room to store the
task (perhaps the executor uses a bounded blocking queue to store tasks and the queue is full—I discuss blocking
queues later in this chapter).
The following example presents the Executor equivalent of the aforementioned new Thread(new
RunnableTask()).start(); expression:
Executor executor = ...; // ... represents some executor creation
executor.execute(new RunnableTask());
Although Executor is easy to use, this interface is limited in various ways:
•
Executor focuses exclusively on Runnable. Because Runnable’s run() method does
not return a value, there is no convenient way for a runnable task to return a value
to its caller.
•
Executor does not provide a way to track the progress of executing runnable tasks,
cancel an executing runnable task, or determine when the runnable task finishes
execution.
•
Executor cannot execute a collection of runnable tasks.
•
Executor does not provide a way for an application to shut down an executor
(much less to properly shut down an executor).
These limitations are addressed by the java.util.concurrent.ExecutorService interface, which
extends Executor, and whose implementation is typically a thread pool (a group of reusable threads).
Table 6-1 describes ExecutorService’s methods.
402
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Table 6-1. ExecutorService Methods
Method
Description
boolean
awaitTermination(long
timeout, TimeUnit unit)
Block (wait) until all tasks have finished after a shutdown
request, the timeout (measured in unit time units) expires,
or the current thread is interrupted, whichever happens first.
Return true when this executor has terminated, and false
when the timeout elapses before termination. This method
throws java.lang.InterruptedException when interrupted.
<T> List<Future<T>>
invokeAll(Collection<?
extends Callable<T>> tasks)
Execute each callable task in the tasks collection, and return
a java.util.List of java.util.concurrent.Future instances
that hold task statuses and results when all tasks complete—
a task completes through normal termination or by throwing
an exception. The List of Futures is in the same sequential
order as the sequence of tasks returned by tasks’ iterator.
This method throws InterruptedException when it is
interrupted while waiting, in which case unfinished tasks are
canceled, NullPointerException when tasks or any of its
elements is null, and RejectedExecutionException when any
one of tasks’ tasks cannot be scheduled for execution.
<T> List<Future<T>>
invokeAll(Collection<?
extends Callable<T>> tasks,
long timeout, TimeUnit unit)
Execute each callable task in the tasks collection, and return
a List of Future instances that hold task statuses and results
when all tasks complete—a task completes through normal
termination or by throwing an exception—or the timeout
(measured in unit time units) expires. Tasks that are not
completed at expiry are canceled. The List of Futures is in
the same sequential order as the sequence of tasks returned
by tasks’ iterator. This method throws
InterruptedException when it is interrupted while waiting,
in which case unfinished tasks are canceled. It also throws
NullPointerException when tasks, any of its elements, or
unit is null; and throws RejectedExecutionException when
any one of tasks’ tasks cannot be scheduled for execution.
<T> T invokeAny(Collection<?
extends Callable<T>> tasks)
Execute the given tasks, returning the result of an arbitrary
task that has completed successfully (i.e., without throwing
an exception), if any does. Upon normal or exceptional
return, tasks that have not completed are canceled. This
method throws InterruptedException when it is interrupted
while waiting, NullPointerException when tasks or any of
its elements is null, java.lang.IllegalArgumentException
when tasks is empty,
java.util.concurrent.ExecutionException when no task
completes successfully, and RejectedExecutionException
when none of the tasks can be scheduled for execution.
403
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
404
<T> T invokeAny(Collection<?
extends Callable<T>> tasks,
long timeout, TimeUnit unit)
Execute the given tasks, returning the result of an arbitrary
task that has completed successfully (i.e., without throwing
an exception), if any does before the timeout (measured in
unit time units) expires—tasks that are not completed at
expiry are canceled. Upon normal or exceptional return,
tasks that have not completed are canceled. This method
throws InterruptedException when it is interrupted while
waiting, NullPointerException when tasks, any of its
elements, or unit is null, IllegalArgumentException when
tasks is empty, java.util.concurrent.TimeoutException
when the timeout elapses before any task successfully
completes, ExecutionException when no task completes
successfully, and RejectedExecutionException when none of
the tasks can be scheduled for execution.
boolean isShutdown()
Return true when this executor has been shut down;
otherwise, return false.
boolean isTerminated()
Return true when all tasks have completed following
shutdown; otherwise, return false. This method will never
return true prior to shutdown() or shutdownNow() being
called.
void shutdown()
Initiate an orderly shutdown in which previously submitted
tasks are executed, but no new tasks will be accepted. Calling
this method has no effect after the executor has shut down.
This method does not wait for previously submitted tasks to
complete execution. Use awaitTermination() if waiting is
necessary.
List<Runnable> shutdownNow()
Attempt to stop all actively executing tasks, halt the
processing of waiting tasks, and return a list of the tasks that
were awaiting execution. There are no guarantees beyond
best-effort attempts to stop processing actively executing
tasks. For example, typical implementations will cancel via
Thread.interrupt(), so any task that fails to respond to
interrupts may never terminate. This method does not wait
for actively executing tasks to terminate. Use
awaitTermination() if waiting is necessary.
<T> Future<T>
submit(Callable<T> task)
Submit a callable task for execution and return a Future
instance representing task’s pending results. The Future
instance’s get() method returns task’s result upon
successful completion. This method throws
RejectedExecutionException when task cannot be
scheduled for execution, and NullPointerException when
task is null. If you would like to immediately block while
waiting for a task to complete, you can use constructions of
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
the form result = exec.submit(aCallable).get();.
Future<?> submit(Runnable
task)
Submit a runnable task for execution and return a Future
instance representing task’s pending results. The Future
instance’s get() method returns task’s result upon
successful completion. This method throws
RejectedExecutionException when task cannot be
scheduled for execution, and NullPointerException when
task is null.
<T> Future<T>
submit(Runnable task, T
result)
Submit a runnable task for execution and return a Future
instance whose get() method returns result upon
successful completion. This method throws
RejectedExecutionException when task cannot be
scheduled for execution, and NullPointerException when
task is null.
Table 6-1 refers to java.util.concurrent.TimeUnit, an enum that represents time durations at given
units of granularity: DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS, and SECONDS.
Furthermore, TimeUnit declares methods for converting across units (e.g., long toHours(long
duration)), and for performing timing and delay operations (e.g., void sleep(long timeout)) in these
units.
Table 6-1 also refers to callable tasks, which are analogous to runnable tasks. Unlike Runnable,
whose void run() method cannot throw checked exceptions, Callable<V> declares a V call() method
that returns a value, and which can throw checked exceptions because call() is declared with a throws
Exception clause.
Finally, Table 6-1 refers to the Future interface, which represents the result of an asynchronous
computation. Future, whose generic type is Future<V>, provides methods for canceling a task, for
returning a task’s value, and for determining whether or not the task has finished. Table 6-2 describes
Future’s methods.
Table 6-2. Future Methods
Method
Description
boolean cancel(boolean
mayInterruptIfRunning)
Attempt to cancel execution of this task, and return true
when the task was canceled; otherwise, return false (perhaps
the task completed normally before this method was called).
The cancellation attempt fails when the task has completed,
has already been canceled, or could not be canceled for
some other reason. If successful and this task had not started
when cancel() was called, the task should never run. If the
task has already started, then mayInterruptIfRunning
determines whether (true) or not (false) the thread executing
this task should be interrupted in an attempt to stop the
task. After this method returns, subsequent calls to isDone()
always return true. Subsequent calls to isCancelled() always
return true when cancel() returns true.
405
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
V get()
Wait if necessary for the task to complete and return the
result. This method throws
java.util.concurrent.CancellationException when the
task was canceled prior to this method being called,
ExecutionException when the task threw an exception, and
InterruptedException when the current thread was
interrupted while waiting.
V get(long timeout, TimeUnit
unit)
Wait at most timeout units (as specified by unit) for the task
to complete and then return the result (if available). This
method throws CancellationException when the task was
canceled prior to this method being called,
ExecutionException when the task threw an exception,
InterruptedException when the current thread was
interrupted while waiting, and TimeoutException when this
method’s timeout value expires (the wait times out).
boolean isCancelled()
Return true when this task was canceled before it completed
normally; otherwise, return false.
boolean isDone()
Return true when this task completed; otherwise, return
false. Completion may be due to normal termination, an
exception, or cancellation—this method returns true in all
these cases.
Suppose you intend to write an application whose graphical user interface (GUI) lets the user enter
a word. After the user enters the word, the application presents this word to several online dictionaries
and obtains each dictionary’s entry. These entries are subsequently displayed to the user.
Because online access can be slow, and because the user interface should remain responsive
(perhaps the user might want to end the application), you offload the “obtain word entries” task to an
executor that runs this task on a separate thread. The following example employs ExecutorService,
Callable, and Future to accomplish this objective:
ExecutorService executor = ...; // ... represents some executor creation
Future<String[]> taskFuture = executor.submit(new Callable<String[]>()
{
public String[] call()
{
String[] entries = ...;
// Access online dictionaries
// with search word and populate
// entries with their resulting
// entries.
return entries;
}
});
// Do stuff.
String entries = taskFuture.get();
406
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
After obtaining an executor in some manner (you will learn how shortly), the example’s main thread
submits a callable task to the executor. The submit() method immediately returns with a reference to a
Future object for controlling task execution and accessing results. The main thread ultimately calls this
object’s get() method to get these results.
 Note The java.util.concurrent.ScheduledExecutorService interface extends ExecutorService and
describes an executor that lets you schedule tasks to run once or to execute periodically after a given delay.
Although you could create your own Executor, ExecutorService, and ScheduledExecutorService
implementations (such as class DirectExecutor implements Executor { public void
execute(Runnable r) { r.run(); } }—run executor directly on the calling thread), the concurrency
utilities offer a simpler alternative: java.util.concurrent.Executors.
 Tip If you intend to create your own ExecutorService implementations, you will find it helpful to work with the
java.util.concurrent.AbstractExecutorService and java.util.concurrent.FutureTask classes.
The Executors utility class declares several class methods that return instances of various
ExecutorService and ScheduledExecutorService implementations (and other kinds of instances). This
class’s static methods accomplish the following tasks:
•
Create and return an ExecutorService instance that is configured with commonly
used configuration settings.
•
Create and return a ScheduledExecutorService instance that is configured with
commonly used configuration settings.
•
Create and return a “wrapped” ExecutorService or ScheduledExecutorService
instance that disables reconfiguration of the executor service by making
implementation-specific methods inaccessible.
•
Create and return a java.util.concurrent.ThreadFactory instance for creating
new threads.
•
Create and return a Callable instance out of other closure-like forms so that it can
be used in execution methods requiring Callable arguments (e.g.,
ExecutorService’s submit(Callable) method). (Check out Wikipedia’s “Closure
(computer science)” entry
[http://en.wikipedia.org/wiki/Closure_(computer_science)] to learn about
closures.)
For example, static ExecutorService newFixedThreadPool(int nThreads) creates a thread pool
that reuses a fixed number of threads operating off a shared unbounded queue. At most, nThreads
threads are actively processing tasks. If additional tasks are submitted when all threads are active, they
wait in the queue for an available thread.
407
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
If any thread terminates because of a failure during execution before the executor shuts down, a
new thread will take its place when needed to execute subsequent tasks. The threads in the pool will
exist until the executor is explicitly shut down. This method throws IllegalArgumentException when you
pass zero or a negative value to nThreads.
 Note Threads pools are used to eliminate the overhead from having to create a new thread for each submitted
task. Thread creation is not cheap, and having to create many threads could severely impact an application’s
performance.
You would commonly use executors, runnables, callables, and futures in an input/output context. (I
discuss Java’s support for filesystem input/output in Chapter 8.) Performing a lengthy calculation offers
another scenario where you could use these types. For example, Listing 6-1 uses an executor, a callable,
and a future in a calculation context of Euler’s number e (2.71828…).
Listing 6-1. Calculating Euler’s number e
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import
import
import
import
import
java.util.concurrent.Callable;
java.util.concurrent.ExecutionException;
java.util.concurrent.ExecutorService;
java.util.concurrent.Executors;
java.util.concurrent.Future;
class CalculateE
{
final static int LASTITER = 17;
public static void main(String[] args)
{
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<BigDecimal> callable;
callable = new Callable<BigDecimal>()
{
public BigDecimal call()
{
MathContext mc = new MathContext(100,
RoundingMode.HALF_UP);
BigDecimal result = BigDecimal.ZERO;
for (int i = 0; i <= LASTITER; i++)
{
BigDecimal factorial = factorial(new BigDecimal(i));
BigDecimal res = BigDecimal.ONE.divide(factorial, mc);
result = result.add(res);
}
return result;
408
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
}
public BigDecimal factorial(BigDecimal n)
{
if (n.equals(BigDecimal.ZERO))
return BigDecimal.ONE;
else
return n.multiply(factorial(n.subtract(BigDecimal.ONE)));
}
};
Future<BigDecimal> taskFuture = executor.submit(callable);
try
{
while (!taskFuture.isDone())
System.out.println("waiting");
System.out.println(taskFuture.get());
}
catch(ExecutionException ee)
{
System.err.println("task threw an exception");
System.err.println(ee);
}
catch(InterruptedException ie)
{
System.err.println("interrupted while waiting");
}
executor.shutdownNow();
}
}
The main thread that executes Listing 6-1’s main() method first obtains an executor by calling
Executors’ newFixedThreadPool() method. It then instantiates an anonymous class that implements
Callable and submits this task to the executor, receiving a Future instance in response.
After submitting a task, a thread typically does some other work until it needs to obtain the task’s
result. I have chosen to simulate this work by having the main thread repeatedly output a waiting
message until the Future instance’s isDone() method returns true. (In a realistic application, I would
avoid this looping.) At this point, the main thread calls the instance’s get() method to obtain the result,
which is then output.
 Caution It is important to shut down the executor after it completes; otherwise, the application might not end.
The application accomplishes this task by calling shutdownNow().
The callable’s call() method calculates e by evaluating the mathematical power series e =
1/0!+1/1!+1/2!+…. This series can be evaluated by summing 1/n!, where n ranges from 0 to infinity.
call() first instantiates java.math.MathContext to encapsulate a precision (number of digits) and a
rounding mode. I chose 100 as an upper limit on e’s precision and HALF_UP as the rounding mode.
409
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
 Tip Increase the precision as well as LASTITER’s value to converge the series to a lengthier and more accurate
approximation of e.
call() next initializes a java.math.BigDecimal local variable named result to BigDecimal.ZERO. It
then enters a loop that calculates a factorial, divides BigDecimal.ONE by the factorial, and adds the
division result to result.
The divide() method takes the MathContext instance as its second argument to ensure that the
division does not result in a nonterminating decimal expansion (the quotient result of the division
cannot be represented exactly—0.3333333…, for example), which throws
java.lang.ArithmeticException (to alert the caller to the fact that the quotient cannot be represented
exactly), which the executor rethrows as ExecutionException.
When you run this application, you should observe output similar to the following:
This book was purchased by [email protected]
waiting
waiting
waiting
waiting
2.71828182845904507051604779584860506117897963525103269890073500406522504250484331405588797434
4245741730039454062711
Synchronizers
The Threading API offers synchronization primitives for synchronizing thread access to critical sections.
Because it can be difficult to correctly write synchronized code that is based on these primitives, the
concurrency utilities include synchronizers, classes that facilitate common forms of synchronization.
Five commonly used synchronizers are countdown latches, cyclic barriers, exchangers, phasers, and
semaphores:
410
•
A countdown latch lets one or more threads wait at a “gate” until another thread
opens this gate, at which point these other threads can continue. The
java.util.concurrent.CountDownLatch class implements this synchronizer.
•
A cyclic barrier lets a group of threads wait for each other to reach a common
barrier point. The java.util.concurrent.CyclicBarrier class implements this
synchronizer, and makes use of the
java.util.concurrent.BrokenBarrierException class. CyclicBarrier instances are
useful in applications involving fixed sized parties of threads that must
occasionally wait for each other. CyclicBarrier supports an optional Runnable,
known as a barrier action, which runs once per barrier point after the last thread
in the party arrives but before any threads are released. This barrier action is
useful for updating shared state before any of the parties continue.
•
An exchanger lets a pair of threads exchange objects at a synchronization point.
The java.util.concurrent.Exchanger class implements this synchronizer. Each
thread presents some object on entry to Exchanger’s exchange() method, matches
with a partner thread, and receives its partner’s object on return. Exchangers may
be useful in applications such as genetic algorithms (see
http://en.wikipedia.org/wiki/Genetic_algorithm) and pipeline designs.
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
•
A phaser is a reusable synchronization barrier that is similar in functionality to
CyclicBarrier and CountDownLatch, but offers more flexibility. For example, unlike
with other barriers, the number of threads that register to synchronize on a phaser
may vary over time. The java.util.concurrent.Phaser class implements this
synchronizer. Phaser may be used instead of a CountDownLatch to control a oneshot action that serves a variable number of parties. It may also be used by tasks
executing in the context of the Fork/Join Framework, discussed later in this
chapter.
•
A semaphore maintains a set of permits for restricting the number of threads that
can access a limited resource. The java.util.concurrent.Semaphore class
implements this synchronizer. Each call to one of Semaphore’s acquire() methods
blocks if necessary until a permit is available, and then takes it. Each call to
release() adds a permit, potentially releasing a blocking acquirer. However, no
actual permit objects are used; the Semaphore instance only keeps a count of the
number of available permits and acts accordingly. Semaphores are often used to
restrict the number of threads than can access some (physical or logical) resource.
Consider the CountDownLatch class. Each of its instances is initialized to a nonzero count. A thread
calls one of CountDownLatch’s await() methods to block until the count reaches zero. Another thread
calls CountDownLatch’s countDown() method to decrement the count. Once the count reaches zero, the
waiting threads are allowed to continue.
 Note After waiting threads are released, subsequent calls to await() return immediately. Also, because the
count cannot be reset, a CountDownLatch instance can be used only once. When repeated use is a requirement,
use the CyclicBarrier class instead.
We can use CountDownLatch to ensure that worker threads start working at approximately the same
time. For example, check out Listing 6-2.
Listing 6-2. Using a countdown latch to trigger a coordinated start
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class CountDownLatchDemo
{
final static int NTHREADS = 3;
public static void main(String[] args)
{
final CountDownLatch startSignal = new CountDownLatch(1);
final CountDownLatch doneSignal = new CountDownLatch(NTHREADS);
Runnable r = new Runnable()
{
public void run()
{
411
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
try
{
report("entered run()");
startSignal.await(); // wait until told to proceed
report("doing work");
Thread.sleep((int)(Math.random()*1000));
doneSignal.countDown(); // reduce count on which
// main thread is waiting
}
catch (InterruptedException ie)
{
System.err.println(ie);
}
}
void report(String s)
{
System.out.println(System.currentTimeMillis()+": "+
Thread.currentThread()+": "+s);
}
};
ExecutorService executor = Executors.newFixedThreadPool(NTHREADS);
for (int i = 0; i < NTHREADS; i++)
executor.execute(r);
try
{
System.out.println("main thread doing something");
Thread.sleep(1000);
// sleep for 1 second
startSignal.countDown(); // let all threads proceed
System.out.println("main thread doing something else");
doneSignal.await();
// wait for all threads to finish
executor.shutdownNow();
}
catch (InterruptedException ie)
{
System.err.println(ie);
}
}
}
Listing 6-2’s main thread first creates a pair of countdown latches. The startSignal countdown
latch prevents any worker thread from proceeding until the main thread is ready for them to proceed.
The doneSignal countdown latch causes the main thread to wait until all worker threads have finished.
The main thread next creates a runnable whose run() method is executed by subsequently created
worker threads.
The run() method first outputs an initial message and then calls startSignal’s await() method to
wait for this countdown latch’s count to read zero before it can proceed. Once this happens, run()
outputs a message that indicates work is being done, and sleeps for a random period of time (0 through
999 milliseconds) to simulate this work.
At this point, run() invokes doneSignal’s countDown() method to decrement this latch’s count. Once
this count reaches zero, the main thread waiting on this signal will continue, shutting down the executor
and terminating the application.
412
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
After creating the runnable, the main thread obtains an executor that’s based on a thread pool of
NTHREADS threads, and then calls the executor’s execute() method NTHREADS times, passing the runnable
to each of the NTHREADS pool-based threads. This action starts the worker threads, which enter run().
Next, the main thread outputs a message and sleeps for one second to simulate doing additional
work (giving all the worker threads a chance to have entered run() and invoke startSignal.await()),
invokes startSignal’s countdown() method to cause the worker threads to start running, outputs a
message to indicate that it is doing something else, and invokes doneSignal’s await() method to wait for
this countdown latch’s count to reach zero before it can proceed.
When you run this application, you will observe output similar to the following:
main thread doing something
1312936533890: Thread[pool-1-thread-1,5,main]:
1312936533890: Thread[pool-1-thread-2,5,main]:
1312936533890: Thread[pool-1-thread-3,5,main]:
1312936534890: Thread[pool-1-thread-1,5,main]:
1312936534890: Thread[pool-1-thread-2,5,main]:
1312936534890: Thread[pool-1-thread-3,5,main]:
main thread doing something else
entered run()
entered run()
entered run()
doing work
doing work
doing work
You might observe the main thread doing something else message appearing between the last
“entered run()” message and the first “doing work” message.
 Note For brevity, I have avoided examples that demonstrate CyclicBarrier, Exchanger, Phaser, and
Semaphore. Instead, I refer you to the Java documentation for these classes. Each class’s documentation provides
an example that shows you how to use the class.
Concurrent Collections
The java.util.concurrent package includes several interfaces and classes that are concurrency-oriented
extensions to the Collections Framework (see Chapter 5):
•
BlockingDeque is a subinterface of BlockingQueue and java.util.Deque that also
supports blocking operations that wait for the deque to become nonempty before
retrieving an element, and wait for space to become available in the deque before
storing an element. The LinkedBlockingDeque class implements this interface.
•
BlockingQueue is a subinterface of java.util.Queue that also supports blocking
operations that wait for the queue to become nonempty before retrieving an
element, and wait for space to become available in the queue before storing an
element. Each of the ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque,
LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, and
SynchronousQueue classes implements this interface.
•
ConcurrentMap is a subinterface of java.util.Map that declares additional atomic
putIfAbsent(), remove(), and replace() methods. The ConcurrentHashMap class
(the concurrent equivalent of java.util.HashMap) and the ConcurrentSkipListMap
class implement this interface.
413
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
•
ConcurrentNavigableMap is a subinterface of ConcurrentMap and
java.util.NavigableMap. The ConcurrentSkipListMap class implements this
interface.
•
TransferQueue is a subinterface of BlockingQueue and describes a blocking queue
in which producers may wait for consumers to receive elements. The
LinkedTransferQueue class implements this interface.
•
ConcurrentLinkedDeque is an unbounded concurrent deque based on linked
nodes.
•
ConcurrentLinkedQueue is an unbounded thread-safe FIFO implementation of the
Queue interface.
•
ConcurrentSkipListSet is a scalable concurrent NavigableSet implementation.
•
CopyOnWriteArrayList is a thread-safe variant of java.util.ArrayList in which all
mutative (nonimmutable) operations (add, set, and so on) are implemented by
making a fresh copy of the underlying array.
•
CopyOnWriteArraySet is a java.util.Set implementation that uses an internal
CopyOnWriteArrayList instance for all its operations.
Listing 6-3 uses BlockingQueue and ArrayBlockingQueue in an alternative to Listing 4-27’s producerconsumer application (PC).
Listing 6-3. The blocking queue equivalent of Listing 4-27’s PC application
import
import
import
import
java.util.concurrent.ArrayBlockingQueue;
java.util.concurrent.BlockingQueue;
java.util.concurrent.ExecutorService;
java.util.concurrent.Executors;
class PC
{
public static void main(String[] args)
{
final BlockingQueue<Character> bq;
bq = new ArrayBlockingQueue<Character>(26);
final ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable producer;
producer = new Runnable()
{
public void run()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
try
{
bq.put(ch);
System.out.println(ch+" produced by producer.");
}
catch (InterruptedException ie)
414
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
{
assert false;
}
}
}
};
executor.execute(producer);
Runnable consumer;
consumer = new Runnable()
{
public void run()
{
char ch = '\0';
do
{
try
{
ch = bq.take();
System.out.println(ch+" consumed by consumer.");
}
catch (InterruptedException ie)
{
assert false;
}
}
while (ch != 'Z');
executor.shutdownNow();
}
};
executor.execute(consumer);
}
}
Listing 6-3 uses BlockingQueue’s put() and take() methods, respectively, to put an object on the
blocking queue and to remove an object from the blocking queue. put() blocks when there is no room to
put an object; take() blocks when the queue is empty.
Although BlockingQueue ensures that a character is never consumed before it is produced, this
application’s output may indicate otherwise. For example, here is a portion of the output from one run:
Y
Y
Z
Z
consumed
produced
consumed
produced
by
by
by
by
consumer.
producer.
consumer.
producer.
Chapter 4’s PC application overcame this incorrect output order by introducing an extra layer of
synchronization around setSharedChar()/System.out.println() and an extra layer of synchronization
around getSharedChar()/System.out.println(). The next section shows you an alternative in the form of
locks.
415
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Locks
The java.util.concurrent.locks package provides interfaces and classes for locking and waiting for
conditions in a manner that is distinct from built-in synchronization and monitors.
This package’s most basic lock interface is Lock, which provides more extensive locking operations
than can be achieved via the synchronized reserved word. Lock also supports a wait/notification
mechanism through associated Condition objects.
 Note The biggest advantage of Lock objects over the implicit locks that are obtained when threads enter critical
sections (controlled via the synchronized reserved word) is their ability to back out of an attempt to acquire a
lock. For example, the tryLock() method backs out when the lock is not available immediately or when a timeout
expires (if specified). Also, the lockInterruptibly() method backs out when another thread sends an interrupt
before the lock is acquired.
ReentrantLock implements Lock, describing a reentrant mutual exclusion Lock implementation with
the same basic behavior and semantics as the implicit monitor lock accessed via synchronized, but with
extended capabilities.
Listing 6-4 demonstrates Lock and ReentrantLock in a version of Listing 6-3 that ensures that the
output is never shown in incorrect order (a consumed message appearing before a produced message).
Listing 6-4. Achieving synchronization in terms of locks
import
import
import
import
java.util.concurrent.ArrayBlockingQueue;
java.util.concurrent.BlockingQueue;
java.util.concurrent.ExecutorService;
java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class PC
{
public static void main(String[] args)
{
final Lock lock = new ReentrantLock();
final BlockingQueue<Character> bq;
bq = new ArrayBlockingQueue<Character>(26);
final ExecutorService executor = Executors.newFixedThreadPool(2);
Runnable producer;
producer = new Runnable()
{
public void run()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
try
416
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
{
lock.lock();
try
{
while (!bq.offer(ch))
{
lock.unlock();
Thread.sleep(50);
lock.lock();
}
System.out.println(ch+" produced by producer.");
}
catch (InterruptedException ie)
{
assert false;
}
}
finally
{
lock.unlock();
}
}
}
};
executor.execute(producer);
Runnable consumer;
consumer = new Runnable()
{
public void run()
{
char ch = '\0';
do
{
try
{
lock.lock();
try
{
Character c;
while ((c = bq.poll()) == null)
{
lock.unlock();
Thread.sleep(50);
lock.lock();
}
ch = c; // unboxing behind the scenes
System.out.println(ch+" consumed by consumer.");
}
catch (InterruptedException ie)
{
assert false;
}
417
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
}
finally
{
lock.unlock();
}
}
while (ch != 'Z');
executor.shutdownNow();
}
};
executor.execute(consumer);
}
}
Listing 6-4 uses Lock’s lock() and unlock() methods to obtain and release a lock. When a thread
calls lock() and the lock is unavailable, the thread is disabled (and cannot be scheduled) until the lock
becomes available.
This listing also uses BlockingQueue’s offer() method instead of put() to store an object in the
blocking queue, and its poll() method instead of take() to retrieve an object from the queue. These
alternative methods are used because they do not block.
If I had used put() and take(), this application would have deadlocked in the following scenario:
1.
The consumer thread acquires the lock via its lock.lock() call.
2.
The producer thread attempts to acquire the lock via its lock.lock() call and is
disabled because the consumer thread has already acquired the lock.
3.
The consumer thread calls take() to obtain the next java.lang.Character
object from the queue.
4.
Because the queue is empty, the consumer thread must wait.
5.
The consumer thread does not give up the lock that the producer thread
requires before waiting, so the producer thread also continues to wait.
 Note If I had access to the private lock used by BlockingQueue implementations, I would have used put() and
take(), and also would have called Lock’s lock() and unlock() methods on that lock. The resulting application
would then have been identical (from a lock perspective) to Listing 4-27’s PC application, which used
synchronized twice for each of the producer and consumer threads.
Run this application and you will discover that it generates the same output as Listing 4-27’s PC
application.
418
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Atomic Variables
The java.util.concurrent.atomic package provides Atomic-prefixed classes (e.g., AtomicLong) that
support lock-free, thread-safe operations on single variables. Each class declares methods such as get()
and set() to read and write this variable without the need for external synchronization.
Listing 4-23 declared a small utility class named ID for returning unique long integer identifiers via
ID’s getNextID() method. Because this method was not synchronized, multiple threads could obtain the
same identifier. Listing 6-5 fixes this problem by including reserved word synchronized in the method
header.
Listing 6-5. Returning unique identifiers in a thread-safe manner via synchronized
class ID
{
private static long nextID = 0;
static synchronized long getNextID()
{
return nextID++;
}
}
Although synchronized is appropriate for this class, excessive use of this reserved word in more
complex classes can lead to deadlock, starvation, or other problems. Listing 6-6 shows you how to avoid
these assaults on a concurrent application’s liveness (the ability to execute in a timely manner) by
replacing synchronized with an atomic variable.
Listing 6-6. Returning unique IDs in a thread-safe manner via AtomicLong
import java.util.concurrent.atomic.AtomicLong;
class ID
{
private static AtomicLong nextID = new AtomicLong(0);
static long getNextID()
{
return nextID.getAndIncrement();
}
}
In Listing 6-6, I have converted nextID from a long to an AtomicLong instance, initializing this object
to 0. I have also refactored the getNextID() method to call AtomicLong’s getAndIncrement() method,
which increments the AtomicLong instance’s internal long integer variable by 1 and returns the previous
value in one indivisible step.
Additional Concurrency Utilities
As well as supporting the Java 5-introduced concurrency utilities, Java 7 introduces a pair of concurrency
utilities that improve performance, which is achieved in part by taking full advantage of multiple
processors/cores. These utilities consist of the java.util.concurrent.ThreadLocalRandom class and the
Fork/Join Framework.
419
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
ThreadLocalRandom
The ThreadLocalRandom class describes a random number generator that is isolated to the current thread.
In other words, it can be accessed from the current thread only.
As with the global random number generator used by the java.lang.Math class (and which I discuss
later in this chapter), a ThreadLocalRandom instance is initialized with an internally generated seed
(starting value) that may not otherwise be modified. When applicable, use of ThreadLocalRandom rather
than calls to Math.random() in concurrent programs will typically result in much less overhead and
contention.
To use this class, first invoke ThreadLocalRandom’s static ThreadLocalRandom current() method to
return the current thread’s ThreadLocalRandom instance. Continue by invoking one of
ThreadLocalRandom’s “next” methods, such as double nextDouble(double n), which returns a
pseudorandom, uniformly distributed double value between 0 (inclusive) and the specified value n
(exclusive). The argument passed to n is the upper bound on the random number to be returned and
must be positive; otherwise, IllegalArgumentException is thrown.
The following example provides a demonstration via another “next” method:
int r = ThreadLocalRandom.current().nextInt(20, 40);
This book was purchased by [email protected]
This example invokes ThreadLocalRandom’s int nextInt(int least, int bound) method to return a
pseudorandom, uniformly distributed value between the given least value (inclusive) and bound
(exclusive). In this example, least is 20, which is the smallest value that can be returned, and bound is
40, which is one integer greater than the highest value (39) that can be returned.
 Note ThreadLocalRandom leverages thread-local variables, which I discussed in Chapter 4’s coverage of Java’s
Threading API.
Fork/Join Framework
There is always a need for code to execute faster. Historically, this need was addressed by increasing
microprocessor speeds and/or by supporting multiple processors. However, somewhere around 2003,
microprocessor speeds stopped increasing because of natural limits. To compensate, processor
manufacturers started to add multiple processing cores to their processors, to increase speed through
massive parallelism.
 Note Parallelism refers to running threads/tasks simultaneously through some combination of multiple
processors and cores. In contrast, concurrency is a more generalized form of parallelism in which threads run
simultaneously or appear to run simultaneously through task switching, also known as virtual parallelism. Some
people further characterize concurrency as a property of a program or operating system and parallelism as the
run-time behavior of executing multiple tasks simultaneously.
420
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Java supports concurrency via the Threading API and concurrency utilities such as thread pools.
The problem with concurrency is that it doesn’t maximize the use of available processor/core resources.
For example, suppose you have created a sorting algorithm that divides an array into two halves, assigns
two threads to sort each half, and merges the results after both threads finish.
Let’s assume that each thread runs on a different processor. Because different amounts of element
reordering may occur in each half of the array, it’s possible that one thread will finish before the other
thread and must wait before the merge can happen. In this case, a processor resource is wasted.
This problem (and the related problems of the code being verbose and harder to read) can be solved
by recursively breaking a task into subtasks and combining results. These subtasks run in parallel and
complete approximately at the same time (if not at the same moment), where their results are merged
and passed up the stack to the previous layer of subtasks. Hardly any processor time is wasted through
waiting, and the recursive code is less verbose and (usually) easier to understand. Java provides the
Fork/Join Framework to implement this scenario.
Fork/Join consists of a special executor service and thread pool. The executor service makes a task
available to the framework, and this task is broken down into smaller tasks that are forked (executed by
different threads) from the pool. A task waits until joined (its subtasks finish).
Fork/Join uses work stealing to minimize thread contention and overhead. Each worker thread from
a pool of worker threads has its own double-ended work queue and pushes new tasks to this queue. It
reads the task from the head of the queue. If the queue is empty, the worker thread tries to get a task
from the tail of another queue. Stealing is infrequent because worker threads put tasks into their queues
in a last-in, first-out (LIFO) order, and the size of work items gets smaller as a problem is divided into
subproblems. You start by giving the tasks to a central worker and it keeps dividing them into smaller
tasks. Eventually all the workers have something to do with minimal synchronization.
Fork/Join largely consists of the java.util.concurrent package’s ForkJoinPool, ForkJoinTask,
ForkJoinWorkerThread, RecursiveAction, and RecursiveTask classes:
•
ForkJoinPool is an ExecutorService implementation for running ForkJoinTasks. A
ForkJoinPool instance provides the entry point for submissions from nonForkJoinTask clients, as well as providing management and monitoring
operations.
•
ForkJoinTask is the abstract base class for tasks that run within a ForkJoinPool
context. A ForkJoinTask instance is a thread-like entity that is much lighter weight
than a normal thread. Huge numbers of tasks and subtasks may be hosted by a
small number of actual threads in a ForkJoinPool, at the price of some usage
limitations.
•
ForkJoinWorkerThread describes a thread managed by a ForkJoinPool instance,
which executes ForkJoinTasks.
•
RecursiveAction describes a recursive resultless ForkJoinTask.
•
RecursiveTask describes a recursive result-bearing ForkJoinTask.
The Java documentation provides examples of RecursiveAction-based tasks (such as sorting) and
RecursiveTask-based tasks (such as computing Fibonacci numbers). You can also use RecursiveAction
to accomplish matrix multiplication (see http://en.wikipedia.org/wiki/Matrix_multiplication).
For example, suppose that you’ve created Listing 6-7’s Matrix class to represent a matrix consisting
of a specific number of rows and columns.
421
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Listing 6-7. A class for representing a two-dimensional table
class Matrix
{
private double[][] matrix;
Matrix(int nrows, int ncols)
{
matrix = new double[nrows][ncols];
}
int getCols()
{
return matrix[0].length;
}
int getRows()
{
return matrix.length;
}
double getValue(int row, int col)
{
return matrix[row][col];
}
void setValue(int row, int col, double value)
{
matrix[row][col] = value;
}
}
Listing 6-8 demonstrates the single-threaded approach to multiplying two Matrix instances:
Listing 6-8. Multiplying two Matrix instances via the standard matrix-multiplication algorithm
class MatMult
{
public static void main(String[]
{
Matrix a = new Matrix(1, 3);
a.setValue(0, 0, 1); // | 1 2
a.setValue(0, 1, 2);
a.setValue(0, 2, 3);
dump(a);
Matrix b = new Matrix(3, 2);
b.setValue(0, 0, 4); // | 4 7
b.setValue(1, 0, 5); // | 5 8
b.setValue(2, 0, 6); // | 6 9
b.setValue(0, 1, 7);
b.setValue(1, 1, 8);
b.setValue(2, 1, 9);
dump(b);
dump(multiply(a, b));
}
static void dump(Matrix m)
422
args)
3 |
|
|
|
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
{
for (int i = 0; i < m.getRows(); i++)
{
for (int j = 0; j < m.getCols(); j++)
System.out.print(m.getValue(i, j)+" ");
System.out.println();
}
System.out.println();
}
static Matrix multiply(Matrix a, Matrix b)
{
if (a.getCols() != b.getRows())
throw new IllegalArgumentException("rows/columns mismatch");
Matrix result = new Matrix(a.getRows(), b.getCols());
for (int i = 0; i < a.getRows(); i++)
for (int j = 0; j < b.getCols(); j++)
for (int k = 0; k < a.getCols(); k++)
result.setValue(i, j, result.getValue(i, j)+a.getValue(i, k)*
b.getValue(k, j));
return result;
}
}
Listing 6-8’s MatMult class declares a multiply() method that demonstrates matrix multiplication.
After verifying that the number of columns in the first Matrix (a) equals the number of rows in the
second Matrix (b), which is essential to the algorithm, multiply() creates a Matrix named result and
enters a sequence of nested loops to perform the multiplication.
The essence of these loops is as follows: For each row in a, multiply each of that row’s column values
by the corresponding column’s row values in b. Add together the results of the multiplications, and store
the overall total in result at the location specified via the row index (i) in a and the column index (j) in
b.
When you run this application, it generates the following output, which indicates that a 1-row-by-3column matrix multiplied by a 3-row-by-2 column matrix results in a 1-row-by-2-column matrix:
1.0 2.0 3.0
4.0 7.0
5.0 8.0
6.0 9.0
32.0 50.0
3
Computer scientists classify this algorithm as O(n ), which is read “big-oh of n-cubed” or
“approximately n-cubed.” This notation is an abstract way of classifying the algorithm’s performance
(without being bogged down in specific details such as microprocessor speed). A O(n3) classification
indicates very poor performance, and this performance worsens as the sizes of the matrixes being
multiplied increase.
The performance can be improved (on multiprocessor and/or multicore platforms) by assigning
each row-by-column multiplication task to a separate thread-like entity. Listing 6-9 shows you how to
accomplish this scenario in the context of the Fork/Join Framework.
423
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Listing 6-9. Multiplying two matrixes via the Fork/Join Framework
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
class MatMult extends RecursiveAction
{
private Matrix a, b, c;
private int row;
MatMult(Matrix a, Matrix b, Matrix c)
{
this(a, b, c, -1);
}
MatMult(Matrix a, Matrix b, Matrix c, int row)
{
if (a.getCols() != b.getRows())
throw new IllegalArgumentException("rows/columns mismatch");
this.a = a;
this.b = b;
this.c = c;
this.row = row;
}
@Override
public void compute()
{
if (row == -1)
{
List<MatMult> tasks = new ArrayList<>();
for (int row = 0; row < a.getRows(); row++)
tasks.add(new MatMult(a, b, c, row));
invokeAll(tasks);
}
else
multiplyRowByColumn(a, b, c, row);
}
static void multiplyRowByColumn(Matrix a, Matrix b, Matrix c, int row)
{
for (int j = 0; j < b.getCols(); j++)
for (int k = 0; k < a.getCols(); k++)
c.setValue(row, j, c.getValue(row, j)+a.getValue(row, k)*
b.getValue(k, j));
}
static void dump(Matrix m)
{
for (int i = 0; i < m.getRows(); i++)
{
for (int j = 0; j < m.getCols(); j++)
System.out.print(m.getValue(i, j)+" ");
424
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
System.out.println();
}
System.out.println();
}
public static void main(String[] args)
{
Matrix a = new Matrix(2, 3);
a.setValue(0, 0, 1); // | 1 2 3 |
a.setValue(0, 1, 2); // | 4 5 6 |
a.setValue(0, 2, 3);
a.setValue(1, 0, 4);
a.setValue(1, 1, 5);
a.setValue(1, 2, 6);
dump(a);
Matrix b = new Matrix(3, 2);
b.setValue(0, 0, 7); // | 7 1 |
b.setValue(1, 0, 8); // | 8 2 |
b.setValue(2, 0, 9); // | 9 3 |
b.setValue(0, 1, 1);
b.setValue(1, 1, 2);
b.setValue(2, 1, 3);
dump(b);
Matrix c = new Matrix(2, 2);
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new MatMult(a, b, c));
dump(c);
}
}
Listing 6-9 presents a MatMult class that extends RecursiveAction. To accomplish meaningful work,
RecursiveAction’s void compute() method is overridden.
 Note Although compute() is normally used to subdivide a task into subtasks recursively, I’ve chosen to handle
the multiplication task somewhat differently (for brevity and simplicity).
After creating Matrixes a and b, Listing 6-9’s main() method creates Matrix c and instantiates
ForkJoinPool. It then instantiates MatMult, passing these three Matrix instances as arguments to the
MatMult(Matrix a, Matrix b, Matrix c) constructor, and calls ForkJoinPool’s T
invoke(ForkJoinTask<T> task) method to start running this initial task. This method does not return
until the initial task and all of its subtasks complete.
The MatMult(Matrix a, Matrix b, Matrix c) constructor invokes the MatMult(Matrix a, Matrix b,
Matrix c, int row) constructor, specifying -1 as row’s value. This value is used by compute(), which is
invoked as a result of the aforementioned invoke() method call, to distinguish between the initial task
and subtasks.
When compute() is initially called (row equals -1), it creates a List of MatMult tasks and passes this
List to RecursiveAction’s Collection<T> invokeAll(Collection<T> tasks) method (inherited from
ForkJoinTask). This method forks all the List collection’s tasks, which will start to execute. It then waits
425
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
until the invokeAll() method returns (which also joins to all these tasks), which happens when the
boolean isDone() method (also inherited from ForkJoinTask) returns true for each task.
Notice the tasks.add(new MatMult(a, b, c, row)); method call. This call assigns a specific row
value to a MatMult instance. When invokeAll() is called, each task’s compute() method is called and
detects a different value (other than -1) assigned to row. It then executes multiplyRowByColumn(a, b, c,
row); for its specific row.
When you run this application (java MatMult), it generates the following output:
1.0 2.0 3.0
4.0 5.0 6.0
7.0 1.0
8.0 2.0
9.0 3.0
50.0 14.0
122.0 32.0
Objects
Java 7’s new java.util.Objects class consists of class methods for operating on objects. These utilities
include null-safe or null-tolerant methods for comparing two objects, computing the hash code of an
object, requiring that a reference not be null, and returning a string for an object.
Table 6-3 describes Objects’ class methods.
Table 6-3. Objects Methods
426
Method
Description
<T> int compare(T a, T b,
Comparator<? super T> c)
Return 0 when the first two arguments are identical
(including the case where both arguments are the null
reference), and the result of invoking c.compare(a, b)
otherwise. An instance of the NullPointerException class
may or may not be thrown depending on the
java.util.Comparator argument’s ordering policy for null
references (if there is such a policy). (I discussed Comparator
in Chapter 5.)
boolean deepEquals(Object a,
Object b)
Return true when the passed arguments are deeply equal
(discussed later). Otherwise, this method returns false. Two
null references are considered to be deeply equal. If both
arguments are arrays, the algorithm followed by
Arrays.deepEquals() is used to determine equality.
Otherwise, equality is determined by calling the first
argument’s equals() method. (I introduced
java.util.Arrays in Chapter 5.)
boolean equals(Object a,
Object b)
Return true when the passed arguments are equal to each
other (including the scenario where both arguments are
null). Otherwise, this method returns false (including
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
scenarios where only one argument is null). If neither
argument is null, equality is determined by calling the first
argument’s equals() method.
int hash(Object... values)
Generate a hash code for a sequence of object arguments.
The hash code is generated as if all arguments were put into
an array and that array was hashed by calling
Arrays.hashCode(Object[]).When a single object is passed
to values, hash()’s returned value does not equal the hash
code of that object. To obtain a single object’s hash code,
call hashCode(Object).
int hashCode(Object o)
Return the hash code of a nonnull argument and 0 for the
null argument.
<T> T requireNonNull(T obj)
Test the passed object reference for nullness. It either
returns the nonnull reference stored in obj or throws
NullPointerException when obj contains the null reference.
<T> T requireNonNull(T obj,
String message)
Test the passed object reference for nullness. It either
returns the nonnull reference stored in obj or throws
NullPointerException when obj contains the null reference.
The thrown NullPointerException instance contains the
message provided by message.
String toString(Object o)
Return the result of calling toString() for a nonnull
argument and "null" for a null argument.
String toString(Object o,
String nullDefault)
Return the result of calling toString() on the first argument
(passed to o) when that argument is not null; otherwise, this
method returns the second argument (passed to
nullDefault).
Objects implements the null-tolerant compare() method to first compare its arguments for object
identity by using == before calling the provided Comparator.
The equals() and deepEquals() methods define equivalence relations over object references. Unlike
Object.equals(Object o), Objects.equals(Object a, Object b) handles null values, returning true
when both arguments are null, or when the first argument is nonnull and a.equals(b) returns true.
The deepEquals() method is used in the context of arrays (including nested arrays) to determine if
two arrays are deeply equal (they are both null or they contain the same number of elements and all
corresponding pairs of elements in the two arrays are deeply equal).
This method’s two (possibly null) arguments, denoted by e1 and e2 below, are deeply equal when
any of the following conditions hold:
•
e1 and e2 are arrays of object reference types, and Arrays.deepEquals(e1, e2)
would return true
•
e1 and e2 are arrays of the same primitive type, and the appropriate overloading of
Arrays.equals(e1, e2) would return true.
427
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
•
e1 == e2
•
e1.equals(e2) would return true.
Equality implies deep equality, but the converse isn’t necessarily true. In the following example, x
and y are deeply equal but are not equal:
Object common = "string";
Object[] x = {"string"};
Object[] y = {"string"};
System.out.println("x == y: "+(x == y)); // false (two different references)
System.out.println("Objects.equals(x, y): "+Objects.equals(x, y)); // false
System.out.println("Objects.deepEquals(x, y): "+Objects.deepEquals(x, y)); // true
Arrays x and y are not equal because they contain two different references and Objects.equals() is
using reference equality (comparing their references) in this context. (Object equality, or comparing
object contents, occurs when a class overrides Object’s equals() method.) However, these arrays are
deeply equal because x and y are both arrays of object reference types and Arrays.deepEquals(x, y)
would return true.
 Note Unlike the java.lang.Object class, which is automatically imported because of its java.lang prefix, you
must explicitly import Objects into your source code (import java.util.Objects;) when you want to avoid
having to specify the java.util prefix.
The Java documentation for the requireNonNull() methods states that they are designed primarily
for doing parameter validation in methods and constructors. The idea is to check a method’s or a
constructor’s parameter values for null references before attempting to use these references later in the
method or constructor, and avoid potential NullPointerExceptions. Listing 6-10 provides a
demonstration.
Listing 6-10. Testing constructor parameters for null reference arguments
import java.util.Objects;
class Employee
{
private String firstName, lastName;
Employee(String firstName, String lastName)
{
try
{
firstName = Objects.requireNonNull(firstName);
lastName = Objects.requireNonNull(lastName,
"lastName shouldn't be null");
lastName = Character.toUpperCase(lastName.charAt(0))+
lastName.substring(1);
this.firstName = firstName;
this.lastName = lastName;
428
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
}
catch (NullPointerException npe)
{
// In lieu of a more sophisticated logging mechanism, and also for
// brevity, I output the exception's message to standard output.
System.out.println(npe.getMessage());
}
}
String getName()
{
return firstName+" "+lastName;
}
public static void main(String[] args)
{
Employee e1 = new Employee(null, "Doe");
Employee e2 = new Employee("John", null);
Employee e3 = new Employee("John", "doe");
System.out.println(e3.getName());
}
}
Listing 6-10’s Employee constructor first invokes Objects.requireNonNull() on each argument value
passed to its firstName and lastName parameters. If either argument value is the null reference,
NullPointerException is instantiated and thrown; otherwise, the requireNonNull() method returns the
argument value, which is guaranteed to be nonnull.
It is now safe to invoke lastName.charAt(), which returns the first character from the string on
which this method is called. This character is passed to Character’s toUpperCase() utility method, which
returns the character when it does not represent a lowercase letter, or the uppercase equivalent of the
lowercase letter. After toUpperCase() returns, the (potentially uppercased) letter is prepended to the rest
of the string, resulting in a last name starting with an uppercase letter. (Assume that the name consists of
letters only.)
Listing 6-10’s Objects.requireNonNull() method calls offer a more compact alternative to the
following example, which demonstrates how requireNonNull(T obj, String message)’s message
parameter is used:
if (firstName == null)
throw new NullPointerException();
if (lastName == null)
throw new NullPointerException("lastName shouldn't be null");
Compile Listing 6-10 (javac Employee.java) and run the resulting application (java Employee). You
should observe the following output:
null
lastName shouldn't be null
John Doe
As Listing 6-10 reveals, the Objects class’s methods were introduced to promote null safety by
reducing the likelihood of a NullPointerException being thrown unintentionally. As another example,
Employee e = null; String s = e.toString(); results in a thrown NullPointerException instance
because you cannot invoke toString() on the null reference stored in e. In contrast, Employee e = null;
String s = Objects.toString(e); doesn’t result in a thrown NullPointerException instance because
Objects.toString() returns "null" when it detects that e contains the null reference. Rather than having
429
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
to explicitly test a reference for null, as in if (e != null) { String s = e.toString(); /* other code
here */ }, you can offload the null-checking to the Objects class’s various methods.
These methods were also introduced to avoid the “reinventing the wheel” syndrome. Many
developers have repeatedly written methods that perform similar operations, but do so in a null-safe
manner. The inclusion of Objects in Java’s standard class library standardizes this common
functionality.
Random
Chapter 4 introduced you to the Math class’s random() method. If you were to investigate this method’s
source code, you would discover the following implementation:
This book was purchased by [email protected]
private static Random randomNumberGenerator;
private static synchronized Random initRNG()
{
Random rnd = randomNumberGenerator;
return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd;
}
public static double random()
{
Random rnd = randomNumberGenerator;
if (rnd == null) rnd = intRNG();
return rnd.nextDouble();
}
This implementation, which demonstrates lazy initialization (not initializing something until it is
first needed, in order to improve performance), shows you that Math’s random() method is implemented
in terms of a class named Random, which is located in the java.util package. Random instances generate
sequences of random numbers and are known as random number generators.
 Note These numbers are not truly random because they are generated from a mathematical algorithm. As a
result, they are often referred to as pseudorandom numbers. However, it is often convenient to drop the “pseudo”
prefix and refer to them as random numbers.
Random generates its sequence of random numbers by starting with a special 48-bit value that is
known as a seed. This value is subsequently modified by a mathematical algorithm, which is known as a
linear congruential generator.
 Note Check out Wikipedia’s “Linear congruential generator” entry
(http://en.wikipedia.org/wiki/Linear_congruential_generator) to learn about this algorithm for
generating random numbers.
430
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Random declares a pair of constructors:
•
Random() creates a new random number generator. This constructor sets the seed
of the random number generator to a value that is very likely to be distinct from
any other call to this constructor.
•
Random(long seed) creates a new random number generator using its seed
argument. This argument is the initial value of the random number generator’s
internal state, which the protected int next(int bits) method maintains.
 Note The next() method, which is used by the other methods, is protected so that subclasses can change
the generator implementation from that shown below
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed*multiplier+addend)&mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int) (nextseed >>> (48-bits));
}
to something different. For a subclassing example, check out “Subclassing java.util.Random”
(http://www.javamex.com/tutorials/random_numbers/java_util_random_subclassing.shtml).
Because Random() does not take a seed argument, the resulting random number generator always
generates a different sequence of random numbers. This explains why Math.random() generates a
different sequence each time an application starts running.
 Tip Random(long seed) gives you the opportunity to reuse the same seed value, allowing the same sequence
of random numbers to be generated. You will find this capability useful when debugging a faulty application that
involves random numbers.
Random(long seed) calls the void setSeed(long seed) method to set the seed to the specified value.
If you call setSeed() after instantiating Random, the random number generator is reset to the state that it
was in immediately after calling Random(long seed).
The previous code fragment demonstrates Random’s double nextDouble() method, which returns the
next pseudorandom, uniformly distributed double precision floating-point value between 0.0 and 1.0 in
this random number generator’s sequence.
431
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
Random also declares the following methods for returning other kinds of values:
•
boolean nextBoolean() returns the next pseudorandom, uniformly distributed
Boolean value in this random number generator’s sequence. Values true and false
are generated with (approximately) equal probability.
•
void nextBytes(byte[] bytes) generates pseudorandom byte integer values and
stores them in the bytes array. The number of generated bytes is equal to the
length of the bytes array.
•
float nextFloat() returns the next pseudorandom, uniformly distributed
floating-point value between 0.0 and 1.0 in this random number generator’s
sequence.
•
double nextGaussian() returns the next pseudorandom, Gaussian (“normally”)
distributed double precision floating-point value with mean 0.0 and standard
deviation 1.0 in this random number generator’s sequence.
•
int nextInt() returns the next pseudorandom, uniformly distributed integer
32
value in this random number generator’s sequence. All 2 possible integer values
are generated with (approximately) equal probability.
•
int nextInt(int n) returns a pseudorandom, uniformly distributed integer value
between 0 (inclusive) and the specified value (exclusive), drawn from this random
number generator’s sequence. All n possible integer values are generated with
(approximately) equal probability.
•
long nextLong() returns the next pseudorandom, uniformly distributed long
integer value in this random number generator’s sequence. Because Random uses a
seed with only 48 bits, this method will not return all possible 64-bit long integer
values.
The java.util.Collections class declares a pair of shuffle() methods for shuffling the contents of a
list. In contrast, the Arrays class does not declare a shuffle() method for shuffling the contents of an
array. Listing 6-11 addresses this omission.
Listing 6-11. Shuffling an array of integers
import java.util.Random;
class Shuffler
{
public static void main(String[] args)
{
Random r = new Random();
int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
for (int i = 0; i < array.length; i++)
{
int n = r.nextInt(array.length);
// swap array[i] with array[n]
int temp = array[i];
array[i] = array[n];
array[n] = temp;
432
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
}
for (int i = 0; i < array.length; i++)
System.out.print(array[i]+" ");
System.out.println();
}
}
Listing 6-11 presents a simple recipe for shuffling an array of integers—this recipe could be
generalized. For each array entry from the start of the array to the end of the array, this entry is swapped
with another entry whose index is chosen by int nextInt(int n).
When you run this application, you will observe a shuffled sequence of integers that is similar to the
following sequence that I observed:
7 1 5 2 9 8 6 4 3 0
EXERCISES
The following exercises are designed to test your understanding of the concurrency utilities, Objects, and
Random:
1.
The Java documentation for the Semaphore class presents a Pool class that
demonstrates how a semaphore can control access to a pool of items. Because
Pool is incomplete, introduce a single resource (replace protected Object[]
items = ... with an array containing this resource in its single entry) and then
demonstrate Pool’s getItem() and putItem() methods in the context of a pair of
threads launched from the main() method of a SemaphoreDemo class.
2.
Create an EqualsDemo application to play with Objects’ deepEquals() method.
As well as an EqualsDemo class, this application declares Car and Wheel classes.
A Car instance contains (typically four) Wheel instances, and a Wheel instance
contains a brand name. Each of Car and Wheel must override Object’s equals()
method but does not have to override hashCode() in this example. Your main()
method should contain the following code and generate the output shown in the
comments:
Car[]
Car[]
Car[]
Car[]
Car[]
cars1
cars2
cars3
cars4
cars5
=
=
=
=
=
{
{
{
{
{
new Car(4, "Goodyear"), new Car(4, "Goodyear") };
new Car(4, "Goodyear"), new Car(4, "Goodyear") };
new Car(4, "Michelin"), new Car(4, "Goodyear") };
new Car(3, "Goodyear"), new Car(4, "Goodyear") };
new Car(4, "Goodyear"), new Car(4, "Goodyear"),
new Car(3, "Michelin") };
System.out.println(Objects.deepEquals(cars1, cars2)); // Output: true
System.out.println(Objects.deepEquals(cars1, cars3)); // Output: false
System.out.println(Objects.deepEquals(cars1, cars4)); // Output: false
System.out.println(Objects.deepEquals(cars1, cars5)); // Output: false
The comments reveal that two arrays are deeply equal when they contain the same number of
equal elements.
433
CHAPTER 6  TOURING ADDITIONAL UTILITY APIS
3.
Create a Die application that uses Random to simulate the role of a die (a single
dice). Output the value.
Summary
Java 5 introduced the concurrency utilities to simplify the development of concurrent applications. The
concurrency utilities are organized into executor, synchronizer, concurrent collection, lock, atomic
variable, and additional utilities categories, and leverage the low-level Threading API in their
implementations.
An executor decouples task submission from task-execution mechanics and is described by the
Executor, ExecutorService, and ScheduledExecutorService interfaces. A synchronizer facilitates
common forms of synchronization: countdown latches, cyclic barriers, exchangers, phasers, and
semaphores are commonly used synchronizers.
A concurrent collection is an extension to the Collections Framework. A lock supports high-level
locking and can associate with conditions in a manner that is distinct from built-in synchronization and
monitors. An atomic variable encapsulates a single variable, and supports lock-free, thread-safe
operations on that variable.
Java 7’s new ThreadLocalRandom class describes a random number generator that is isolated to the
current thread, and its new Fork/Join Framework lets you recursively break a task into subtasks and
combine results to make maximum use out of multiple processors and/or processor cores.
The new Objects class consists of class methods for operating on objects. These utilities include
null-safe or null-tolerant methods for comparing two objects, computing the hash code of an object,
requiring that a reference not be null, and returning a string for an object.
The Math class’s random() method is implemented in terms of the Random class, whose instances are
known as random number generators. Random generates a sequence of random numbers by starting with
a special 48-bit seed. This value is subsequently modified via a mathematical algorithm that is known as
a linear congruential generator.
The examples in this chapter and its predecessors have leveraged the underlying platform’s
Standard I/O facility to create character-based user interfaces. However, Java also lets you create GUIs to
achieve more compelling user interfaces. Chapter 7 introduces you to Java’s APIs for creating and
enriching GUIs.
434
CHAPTER 7
Creating and Enriching Graphical
User Interfaces
The applications presented in previous chapters featured Standard I/O-based user interfaces. Although
these simple character-oriented user interfaces are convenient for demonstrating Java features or for
interacting with small utility applications (e.g., Chapter 3’s StubFinder application), they are inadequate
for more sophisticated needs, such as filling out forms or viewing HTML pages. However, Java also
provides APIs that let you create and enrich more sophisticated graphical user interfaces (GUIs).
Abstract Window Toolkit (AWT) is Java’s original GUI-oriented API. After introducing AWT to Java,
Sun Microsystems introduced Java Foundation Classes (JFC) as an AWT superset with many new
capabilities. JFC’s main APIs are Swing (for creating more sophisticated GUIs), Accessibility (for
supporting assistive technologies), Java 2D (for creating high-quality graphics), and Drag and Drop (for
dragging and dropping AWT/Swing GUI components, such as buttons or textfields).
Chapter 7 continues to explore the standard class library by introducing you to AWT, Swing, and
Java 2D. Appendix C introduces you to Accessibility and Drag and Drop.
Abstract Window Toolkit
Abstract Window Toolkit (AWT) is Java’s original windowing system-independent API for creating GUIs
that are based on components, containers, layout managers, and events. AWT also supports graphics,
colors, fonts, images, data transfer, and more.
The standard class library organizes AWT’s many types into the java.awt package and subpackages.
However, not all java.awt types and subpackages belong to AWT. For example, java.awt.Graphics2D and
java.awt.geom belong to Java 2D. This arrangement exists because the java.awt-based package structure
provides a natural fit for various non-AWT types. (AWT is often viewed as part of JFC nowadays.)
This section introduces you to AWT by first presenting toolkits. It then explores components,
containers, layout managers, and events. After exploring graphics, colors, and fonts, the section focuses
on images. It closes by discussing AWT’s support for data transfer.
AWT HISTORY
Before JDK 1.0’s release (on January 23, 1996), developers at Sun Microsystems were tasked with
abstracting the various windowing systems of the day and their attendant widgets (GUI controls, such as
buttons—Java refers to GUI controls as components) into a portable windowing system that Java
applications could target. AWT was born and was included in JDK 1.0. (Legend has it [see
435
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
http://www.cs.jhu.edu/~scott/oos/java/doc/TIJ3/html/TIJ316.htm, for example] that the first
AWT version had to be designed and implemented in one month.)
The JDK 1.0.1 and 1.0.2 releases corrected various AWT bugs, and JDK 1.1 offered an improved eventhandling model that greatly simplified how applications respond to GUI events (such as button clicks and
key presses). Subsequent JDK releases brought about additional improvements. For example, JDK 1.2
introduced JFC, JDK 6 introduced the Desktop, Splash Screen, and System Tray APIs, and JDK 7
standardized the support for translucent and shaped windows first introduced in JDK 6 update 10 (build
12).
Appendix C covers Desktop, Splash Screen, System Tray, and translucent/shaped windows.
Toolkits
AWT uses toolkits to abstract over windowing systems. A toolkit is a concrete implementation of AWT’s
abstract java.awt.Toolkit class. AWT provides a separate toolkit for each windowing system used by the
Windows, Solaris, Linux, and Mac OS platforms.
Toolkit declares various methods that AWT calls to obtain information about the platform’s
windowing system, and to perform various windowing system-specific tasks. For example, void beep()
emits an audio beep.
Most applications should not call any of Toolkit’s methods directly; they are intended for use by
AWT. However, you might occasionally find it helpful to call some of these methods.
For example, you might want your application to sound one or more beeps when a long-running
task finishes, to alert the user who might not be looking at the screen. You can accomplish this task by
specifying code that’s similar to the following:
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0 ; i < 5; i++)
{
toolkit.beep();
try { Thread.sleep(200); } catch (InterruptedException ie) {}
}
This example reveals that you must obtain a Toolkit instance before you can call a Toolkit method,
and that you do so by calling Toolkit’s Toolkit getDefaultToolkit() class method. It also reveals that
you might want to place a small delay between successive beeps to ensure that each beep is distinct.
Components, Containers, Layout Managers, and Events
AWT lets you create GUIs that are based on components, containers, layout managers, and events.
A component is a graphical widget that appears in a window on the screen; a label, a button, or a
textfield is an example. A window is represented by a special component known as a container.
A layout manager is an object that organizes components and containers within a container. It is
used to create useful GUIs (e.g., a form consisting of labels, textfields, and buttons).
An event is an object describing a button click or other GUI interaction. Applications register event
listener objects with components to listen for specific events so that application code can respond to
them.
436
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
Components Overview
AWT provides a wide variety of component classes in the java.awt package. Figure 7-1 presents the class
hierarchy for AWT’s nonmenu component classes.
Figure 7-1. AWT’s nonmenu component class hierarchy is rooted in java.awt.Component.
AWT’s abstract Component class is the root class for all AWT nonmenu components (and Swing
components). Directly beneath Component are Button, Canvas, Checkbox, Choice, Container, Label, List,
Scrollbar, and TextComponent:
•
Button describes a clickable label.
•
Canvas describes a blank rectangular area. You would subclass Canvas to introduce
your own AWT components.
•
Checkbox describes a true/false choice. You can use Checkbox with
java.awt.CheckboxGroup to create a set of mutually exclusive radio buttons.
•
Choice describes a drop-down list (also known as a pop-up menu) of strings.
•
Container describes a component that stores other components. This nesting
capability lets you create GUIs of arbitrary complexity and is very powerful. (Being
able to represent containers as components is an example of the Composite design
pattern, which is presented on page 163 of Design Patterns: Elements of Reusable
Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and
John Vlissides [Addison-Wesley, 1995; ISBN: 0201633612].)
•
Label describes a single line of static text as a visual aid to the user.
•
List describes a non-drop-down list of strings.
437
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
•
Scrollbar describes a range of values.
•
TextComponent describes any component that inputs text. Its TextArea subclass
describes a text component for inputting multiple lines of text, whereas its
TextField subclass describes a text component for inputting a single line of text.
Figure 7-2 presents the class hierarchy for menu component classes.
Figure 7-2. AWT’s menu component class hierarchy is rooted in java.awt.MenuComponent.
AWT’s abstract MenuComponent class (which doesn’t extend Component) is the root class for all AWT
menu components. Directly beneath MenuComponent are MenuBar and MenuItem:
•
MenuBar encapsulates the windowing system concept of a menubar bound to a
frame window. It contains a sequence of Menu components, where each Menu
component contains a sequence of MenuItem components.
•
MenuItem describes a single menuitem. Its CheckboxMenuItem subclass describes a
menuitem that’s implemented via a checkbox. Its Menu subclass describes a pulldown menu component that’s deployed from a menu bar. (Menu extends MenuItem
to create arbitrarily complex menus.) Menu is subclassed by PopupMenu to describe a
menu that can be dynamically popped up at a specified position within a
component.
Component declares many nonmenu component-oriented methods. For example, Component declares
the following methods to inform the caller about the component’s displayable, visible, and showing
status:
438
•
boolean isDisplayable() returns true when a component is in the displayable
state (the component is connected to a native screen resource [defined shortly],
typically by being added to a container).
•
boolean isVisible() returns true when a component is in the visible state (the
component appears on the screen). The companion void setVisible(boolean b)
method lets you show (b is true) or hide (b is false) a component.
•
boolean isShowing() returns true when a component is in the showing state (the
component is visible and is contained in a container that is also visible and
showing). This method is useful for determining whether or not a component has
been obscured by another component. It returns false when obscured, whereas
isVisible() would continue to return true.
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
MenuComponent’s repertoire of methods is much shorter. However, it shares some commonality with
Component. For example, both classes declare a method for specifying the component’s font.
Some of Component’s and MenuComponent’s methods have been deprecated and should not be used.
For example, Component declares java.awt.peer.ComponentPeer getPeer() and MenuComponent declares
java.awt.peer.MenuComponentPeer getPeer(). Both deprecated methods hint at how AWT implements
its predefined components.
AWT leverages the platform’s windowing system to create various components. When you add a
component to a container, AWT creates a peer object whose class implements a ComponentPeer or
MenuComponentPeer subinterface. For example, AWT creates a java.awt.peer.ButtonPeer instance when
you add a Button component class instance to a container.
 Note Each AWT toolkit implementation includes its own set of peer interface implementations.
Behind the scenes, the component object communicates with the peer object, which communicates
with native code in a JDK library. This code communicates with the platform’s windowing system, which
manages the native screen resource (a native window) that appears on the screen.
For example, when you add a Button instance to a container, AWT calls Component’s void
addNotify() method, which obtains the current toolkit and calls its ButtonPeer createButton(Button
target) method to create this toolkit’s Button peer.
Ultimately, the windowing system is asked to create a button native screen resource. For example,
on a 32-bit Windows operating system, the native screen resource could be obtained via a call to the
CreateWindow() or CreateWindowEx() Win32 API function.
AWT components except for those created from nonpredefined classes that directly extend
Component or Container are known as heavyweight components because of their corresponding peer
interfaces and native screen resources. Components created from custom Component and Container
subclasses are known as lightweight components because they do not have peer interfaces and native
screen resources (they reuse their closest ancestor’s peer, which is how Swing works). You can call
Component’s boolean isLightweight() method to determine if a component is lightweight.
 Note Heavyweight and lightweight components can be mixed in a single component hierarchy provided that the
entire hierarchy is valid (noncontainer components are correctly sized; container components have their contained
components laid out). When the hierarchy is invalidated (e.g., after changing component bounds [width, height,
and location relative to the component’s parent container], such as when changing a button’s text, or after
adding/removing components to/from containers), AWT validates it by invoking Container’s void validate()
method on the top-most invalid container of the hierarchy.
As you explore the JDK documentation for the various component classes, you’ll discover many
useful constructors and methods. For example, Button declares a Button(String label) constructor for
initializing a button to the specified label text. Alternatively, you could call the Button() constructor to
create a Button with no label. Regardless of which constructor you use, you can always call Button’s void
setLabel(String label) and String getLabel() methods to specify and retrieve the label text that is
439
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
displayed on the button. (Changing a button’s displayed text invalidates the button; AWT then performs
validation, which causes the component hierarchy to be re-laid out.)
Components are easy to create, as demonstrated by the following example, which creates a Yes
button:
Button btnYes = new Button("Yes");
 Note I like to prefix a component variable to indicate its kind. For example, I prefix buttons with btn.
Containers Overview
This book was purchased by [email protected]
Buttons, labels, textfields, and other components cannot be placed directly on the screen; they need to
be placed in a container window that is placed directly on the screen.
AWT provides several container classes in the java.awt package. Figure 7-3 presents their hierarchy.
Figure 7-3. AWT’s container class hierarchy is rooted in Container.
AWT’s Container class is the root class for all AWT containers. Directly beneath Container are Panel,
ScrollPane, and Window:
•
Panel is the simplest container. It provides space in which an application can
attach any other component, including other panels.
•
ScrollPane implements automatic horizontal and/or vertical scrolling for a single
child (contained) component. A container that contains a component is referred
to as that component’s parent.
•
Window is a top-level window with no borders. Its Dialog subclass describes a
dialog box (a window for soliciting input from the user) and its Frame subclass
describes a frame window (a top-level window with borders, including a titlebar).
Dialog’s FileDialog subclass describes a dialog box for selecting a file.
Container declares many container-oriented methods. For example, Component add(Component
comp) appends component comp to the container, Component[] getComponents() returns an array of the
container’s components, and int getComponentCount() returns the number of components in the
container.
Window declares a void pack() method for making a top-level window just large enough to display all
its components at their preferred (natural) sizes. Also, pack() makes the window (and any owner of the
window—dialog boxes are typically owned by other windows) displayable when not already displayable.
440
CHAPTER 7  CREATING AND ENRICHING GRAPHICAL USER INTERFACES
Window also declares a void setSize(int width, int height) method that lets you size a window to
a specific size (in pixels).
Continuing from the previous example, suppose you want to add the Yes button to a panel (which
might also contain a No button). The following example shows you how to accomplish this task:
Panel pnl = new Panel();
pnl.add(btnYes);
Layout Managers Overview
Containers can contain components but cannot lay them out on the screen (e.g., in rows, in a grid, or in
some other arrangement). Layout managers handle this task. A layout manager is typically associated
with a container to lay out the container’s components.
 Note Layout managers provide a screen size-independent way to display a GUI. Without them, an application
would have to obtain the current screen size and adapt container/component sizes to account for the screen size.
Doing so could involve writing hundreds of lines of code, a tedious proposition at best.
AWT provides several layout managers in the java.awt package: BorderLayout (lay out no more than
five components in a container’s north, south, east, west, and center areas), CardLayout (treat each
contained component as a card; only one card is visible at a time, and the container acts as a stack of
cards), FlowLayout (arrange components in a horizontal row), GridBagLayout (l