Introduction to Object-Oriented Design in Java - Introducing Java - Java in a Nutshell, 6th Edition (2015)

Java in a Nutshell, 6th Edition (2015)

Part I. Introducing Java

Chapter 5. Introduction to Object-Oriented Design in Java

In this chapter, we’ll look at how to work with Java’s objects, covering the key methods of Object, aspects of object-oriented design, and implementing exception handling schemes. Throughout the chapter, we will be introducing some design patterns—essentially best practices for solving some very common situations that arise in software design. Towards the end of the chapter, we’ll also consider the design of safe programs—those that are designed so as not to become inconsistent over time. We’ll get started by considering the subject of Java’s calling and passing conventions and the nature of Java values.

Java Values

Java’s values, and their relationship to the type system, are quite straightforward. Java has two types of values—primitives and object references.

NOTE

Some books refer to primitives as “value types”—this makes it confusing to think of object references as a value in Java. For this reason, we stick to the term primitive when discussing any of Java’s eight nonreference types.

These two kinds of values are the only things that can be put into variables. In fact, that’s one way to define a value: “a thing that can be put into a variable or passed to a method.” For C++ and C programmers, note that object contents cannot be put into variables—so there is no equivalent of a dereference operator or a struct.

The key difference between primitive values and references is that primitive values cannot be altered—the value 2 is always the same value. By contrast, the contents of object references can usually be changed—often referred to as mutation of object contents.

Java tries to simplify a concept that often confused C++ programmers—the difference between “contents of an object” and “reference to an object.” Unfortunately, it’s not possible to completely hide the difference, and so it is necessary for the programmer to understand how reference values work in the platform.

IS JAVA “PASS BY REFERENCE”?

Java handles objects “by reference,” but we must not confuse this with the phrase “pass by reference.” “Pass by reference” is a term used to describe the method-calling conventions of some programming languages. In a pass-by-reference language, values—even primitive values—are not passed directly to methods. Instead, methods are always passed references to values. Thus, if the method modifies its parameters, those modifications are visible when the method returns, even for primitive types.

Java does not do this; it is a “pass-by-value” language. However, when a reference type is involved, the value that is passed is a copy of the reference (as a value). But this is not the same as pass by reference. If Java were a pass-by-reference language, when a reference type is passed to a method, it would be passed as a reference to the reference.

The fact that Java is pass by value can be demonstrated very simply. The following code shows that even after the call to manipulate(), the value contained in variable c is unaltered—it is still holding a reference to a Circle object of radius 2. If Java was a pass-by-reference language, it would instead be holding a reference to a radius 3 Circle:

public void manipulate(Circle circle) {

circle = new Circle(3);

}

Circle c = new Circle(2);

manipulate(c);

System.out.println("Radius: "+ c.getRadius());

If we’re scrupulously careful about the distinction, and about referring to object references as one of Java’s possible kinds of values, then some otherwise surprising features of Java become obvious. Be careful—some older texts are ambiguous on this point. We will meet this concept of Java’s values again when we discuss memory and garbage collection in Chapter 6.

Important Methods of java.lang.Object

As we’ve noted, all classes extend, directly or indirectly, java.lang.Object. This class defines a number of useful methods that were designed to be overridden by classes you write. Example 5-1 shows a class that overrides these methods. The sections that follow this example document the default implementation of each method and explain why you might want to override it.

The example uses a lot of the extended features of the type system that we met last chapter. First, it implements a parameterized, or generic, version of the Comparable interface. Second, the example uses the @Override annotation to emphasize (and have the compiler verify) that certain methods override Object.

Example 5-1. A class that overrides important Object methods

// This class represents a circle with immutable position and radius.

public class Circle implements Comparable<Circle> {

// These fields hold the coordinates of the center and the radius.

// They are private for data encapsulation and final for immutability

private final int x, y, r;

// The basic constructor: initialize the fields to specified values

public Circle(int x, int y, int r) {

if (r < 0) throw new IllegalArgumentException("negative radius");

this.x = x; this.y = y; this.r = r;

}

// This is a "copy constructor"--a useful alternative to clone()

public Circle(Circle original) {

x = original.x; // Just copy the fields from the original

y = original.y;

r = original.r;

}

// Public accessor methods for the private fields.

// These are part of data encapsulation.

public int getX() { return x; }

public int getY() { return y; }

public int getR() { return r; }

// Return a string representation

@Override public String toString() {

return String.format("center=(%d,%d); radius=%d", x, y, r);

}

// Test for equality with another object

@Override public boolean equals(Object o) {

// Identical references?

if (o == this) return true;

// Correct type and non-null?

if (!(o instanceof Circle)) return false;

Circle that = (Circle) o; // Cast to our type

if (this.x == that.x && this.y == that.y && this.r == that.r)

return true; // If all fields match

else

return false; // If fields differ

}

// A hash code allows an object to be used in a hash table.

// Equal objects must have equal hash codes. Unequal objects are

// allowed to have equal hash codes as well, but we try to avoid that.

// We must override this method because we also override equals().

@Override public int hashCode() {

int result = 17; // This hash code algorithm from the book

result = 37*result + x; // _Effective Java_, by Joshua Bloch

result = 37*result + y;

result = 37*result + r;

return result;

}

// This method is defined by the Comparable interface. Compare

// this Circle to that Circle. Return a value < 0 if this < that

// Return 0 if this == that. Return a value > 0 if this > that.

// Circles are ordered top to bottom, left to right, and then by radius

public int compareTo(Circle that) {

// Smaller circles have bigger y

long result = (long)that.y - this.y;

// If same compare l-to-r

if (result==0) result = (long)this.x - that.x;

// If same compare radius

if (result==0) result = (long)this.r - that.r;

// We have to use a long value for subtraction because the

// differences between a large positive and large negative

// value could overflow an int. But we can't return the long,

// so return its sign as an int.

return Long.signum(result);

}

}

toString()

The purpose of the toString() method is to return a textual representation of an object. The method is invoked automatically on objects during string concatenation and by methods such as System.out.println(). Giving objects a textual representation can be quite helpful for debugging or logging output, and a well-crafted toString() method can even help with tasks such as report generation.

The version of toString() inherited from Object returns a string that includes the name of the class of the object as well as a hexadecimal representation of the hashCode() value of the object (discussed later in this chapter). This default implementation provides basic type and identity information for an object but is not usually very useful. The toString() method in Example 5-1 instead returns a human-readable string that includes the value of each of the fields of the Circle class.

equals()

The == operator tests two references to see if they refer to the same object. If you want to test whether two distinct objects are equal to one another, you must use the equals() method instead. Any class can define its own notion of equality by overriding equals(). TheObject.equals() method simply uses the == operator: this default method considers two objects equal only if they are actually the very same object.

The equals() method in Example 5-1 considers two distinct Circle objects to be equal if their fields are all equal. Note that it first does a quick identity test with == as an optimization and then checks the type of the other object with instanceof: a Circle can be equal only to anotherCircle, and it is not acceptable for an equals() method to throw a ClassCastException. Note that the instanceof test also rules out null arguments: instanceof always evaluates to false if its left-hand operand is null.

hashCode()

Whenever you override equals(), you must also override hashCode(). This method returns an integer for use by hash table data structures. It is critical that two objects have the same hash code if they are equal according to the equals() method. It is important (for efficient operation of hash tables) but not required that unequal objects have unequal hash codes, or at least that unequal objects are unlikely to share a hash code. This second criterion can lead to hashCode() methods that involve mildly tricky arithmetic or bit manipulation.

The Object.hashCode() method works with the Object.equals() method and returns a hash code based on object identity rather than object equality. (If you ever need an identity-based hash code, you can access the functionality of Object.hashCode() through the static methodSystem.identityHashCode().)

TIP

When you override equals(), you must always override hashCode() to guarantee that equal objects have equal hash codes. Failing to do this can cause subtle bugs in your programs.

Because the equals() method in Example 5-1 bases object equality on the values of the three fields, the hashCode() method computes its hash code based on these three fields as well. It is clear from the code that if two Circle objects have the same field values, they will have the same hash code.

Note that the hashCode() method in Example 5-1 does not simply add the three fields and return their sum. Such an implementation would be legal but not efficient because two circles with the same radius but whose x and y coordinates were reversed would then have the same hash code. The repeated multiplication and addition steps “spread out” the range of hash codes and dramatically reduce the likelihood that two unequal Circle objects have the same code. Effective Java by Joshua Bloch (Addison Wesley) includes a helpful recipe for constructing efficient hashCode()methods like this one.

Comparable::compareTo()

Example 5-1 includes a compareTo() method. This method is defined by the java.lang.Comparable interface rather than by Object, but it is such a common method to implement that we include it in this section. The purpose of Comparable and its compareTo() method is to allow instances of a class to be compared to each other in the way that the <, <=, >, and >= operators compare numbers. If a class implements Comparable, we can say that one instance is less than, greater than, or equal to another instance. This also means that instances of a Comparable class can be sorted.

Because compareTo() is not declared by the Object class, it is up to each individual class to determine whether and how its instances should be ordered and to include a compareTo() method that implements that ordering. The ordering defined by Example 5-1 compares Circle objects as if they were words on a page. Circles are first ordered from top to bottom: circles with larger y coordinates are less than circles with smaller y coordinates. If two circles have the same y coordinate, they are ordered from left to right. A circle with a smaller x coordinate is less than a circle with a larger x coordinate. Finally, if two circles have the same x and y coordinates, they are compared by radius. The circle with the smaller radius is smaller. Notice that under this ordering, two circles are equal only if all three of their fields are equal. This means that the ordering defined bycompareTo() is consistent with the equality defined by equals(). This is very desirable (but not strictly required).

The compareTo() method returns an int value that requires further explanation. compareTo() should return a negative number if the this object is less than the object passed to it. It should return 0 if the two objects are equal. And compareTo() should return a positive number if thisis greater than the method argument.

clone()

Object defines a method named clone() whose purpose is to return an object with fields set identically to those of the current object. This is an unusual method for two reasons. First, it works only if the class implements the java.lang.Cloneable interface. Cloneable does not define any methods (it is a marker interface) so implementing it is simply a matter of listing it in the implements clause of the class signature. The other unusual feature of clone() is that it is declared protected. Therefore, if you want your object to be cloneable by other classes, you must implement Cloneable and override the clone() method, making it public.

The Circle class of Example 5-1 does not implement Cloneable; instead it provides a copy constructor for making copies of Circle objects:

Circle original = new Circle(1, 2, 3); // regular constructor

Circle copy = new Circle(original); // copy constructor

It can be difficult to implement clone() correctly, and it is usually easier and safer to provide a copy constructor. To make the Circle class cloneable, you would add Cloneable to the implements clause and add the following method to the class body:

@Override public Object clone() {

try { return super.clone(); }

catch(CloneNotSupportedException e) { throw new AssertionError(e); }

}

Aspects of Object-Oriented Design

In this section, we will consider several techniques relevant to object-oriented design in Java. This is a very incomplete treatment and merely intended to showcase some examples—the reader is encouraged to consult additional resources, such as the aforementioned Effective Java by Joshua Bloch.

We start by considering good practices for defining constants in Java, before moving on to discuss different approaches to using Java’s object-oriented capabilities for modeling and domain object design. At the end of the section, we conclude by covering the implementation of some common design patterns in Java.

Constants

As noted earlier, constants can appear in an interface definition. Any class that implements an interface inherits the constants it defines and can use them as if they were defined directly in the class itself. Importantly, there is no need to prefix the constants with the name of the interface or provide any kind of implementation of the constants.

When a set of constants is used by more than one class, it is tempting to define the constants once in an interface and then have any classes that require the constants implement the interface. This situation might arise, for example, when client and server classes implement a network protocol whose details (such as the port number to connect to and listen on) are captured in a set of symbolic constants. As a concrete example, consider the java.io.ObjectStreamConstants interface, which defines constants for the object serialization protocol and is implemented by bothObjectInputStream and ObjectOutputStream.

The primary benefit of inheriting constant definitions from an interface is that it saves typing: you don’t need to specify the type that defines the constants. Despite its use with ObjectStreamConstants, this is not a recommended technique. The use of constants is an implementation detail that is not appropriate to declare in the implements clause of a class signature.

A better approach is to define constants in a class and use the constants by typing the full class name and the constant name. You can save typing by importing the constants from their defining class with the import static declaration. See “Packages and the Java Namespace” for details.

Interfaces Versus Abstract Classes

The advent of Java 8 has fundamentally changed Java’s object-oriented programming model. Before Java 8, interfaces were pure API specification and contained no implementation. This could often lead to duplication of code if the interface had many implementations.

In response, a coding pattern developed. This pattern takes advantage of the fact that an abstract class does not need to be entirely abstract; it can contain a partial implementation that subclasses can take advantage of. In some cases, numerous subclasses can rely on method implementations provided by an abstract superclass.

The pattern consists of an interface that contains the API spec for the basic methods, paired with a primary implementation as an abstract class. A good example would be java.util.List, which is paired with java.util.AbstractList. Two of the main implementations of List that ship with the JDK (ArrayList and LinkedList) are subclasses of AbstractList. As another example:

// Here is a basic interface. It represents a shape that fits inside

// of a rectangular bounding box. Any class that wants to serve as a

// RectangularShape can implement these methods from scratch.

public interface RectangularShape {

void setSize(double width, double height);

void setPosition(double x, double y);

void translate(double dx, double dy);

double area();

boolean isInside();

}

// Here is a partial implementation of that interface. Many

// implementations may find this a useful starting point.

public abstract class AbstractRectangularShape

implements RectangularShape {

// The position and size of the shape

protected double x, y, w, h;

// Default implementations of some of the interface methods

public void setSize(double width, double height) {

w = width; h = height;

}

public void setPosition(double x, double y) {

this.x = x; this.y = y;

}

public void translate (double dx, double dy) { x += dx; y += dy; }

}

The arrival of default methods in Java 8 changes this picture considerably. Interfaces can now contain implementation code, as we saw in “Default Methods”. This means that when defining an abstract type (e.g., Shape) that you expect to have many subtypes (e.g., Circle, Rectangle,Square), you are faced with a choice between interfaces and abstract classes. Because they now have very similar features, it is not always clear which to use.

Remember that a class that extends an abstract class cannot extend any other class, and that interfaces still cannot contain any nonconstant fields. This means that there are still some restrictions on how we can use object orientation in our Java programs.

Another important difference between interfaces and abstract classes has to do with compatibility. If you define an interface as part of a public API and then later add a new mandatory method to the interface, you break any classes that implemented the previous version of the interface—in other words, any new interface methods must be declared as default and an implementation provided. If you use an abstract class, however, you can safely add nonabstract methods to that class without requiring modifications to existing classes that extend the abstract class.

NOTE

In both cases, adding new methods can cause a clash with subclass methods of the same name and signature—with the subclass methods always winning. For this reason, think carefully when adding new methods—especially when the method names are “obvious” for this type, or where the method could have several possible meanings.

In general, the suggested approach is to prefer interfaces when an API specification is needed. The mandatory methods of the interface are nondefault, as they represent the part of the API that must be present for an implementation to be considered valid. Default methods should be used only if a method is truly optional, or if they are really only intended to have a single possible implementation. This latter example is the case for the functional composition present in java.util.function.Function—functions will only ever be composed in the standard way, and it is highly implausible that any sane override of the default compose() method could exist.

Finally, the older technique of merely documenting which methods of an interface are considered “optional” and just throwing a java.lang.UnsupportedOperationException if the programmer does not want to implement them is fraught with problems, and should not be used in new code.

Instance Methods or Class Methods?

Instance methods are one of the key features of object-oriented programming. That doesn’t mean, however, that you should shun class methods. In many cases, it is perfectly reasonable to define class methods.

TIP

Remember that in Java, class methods are declared with the static keyword, and the terms static method and class method are used interchangeably.

For example, when working with the Circle class you might find that you often want to compute the area of a circle with a given radius but don’t want to bother creating a Circle object to represent that circle. In this case, a class method is more convenient:

public static double area(double r) { return PI * r * r; }

It is perfectly legal for a class to define more than one method with the same name, as long as the methods have different parameters. This version of the area() method is a class method, so it does not have an implicit this parameter and must have a parameter that specifies the radius of the circle. This parameter keeps it distinct from the instance method of the same name.

As another example of the choice between instance methods and class methods, consider defining a method named bigger() that examines two Circle objects and returns whichever has the larger radius. We can write bigger() as an instance method as follows:

// Compare the implicit "this" circle to the "that" circle passed

// explicitly as an argument and return the bigger one.

public Circle bigger(Circle that) {

if (this.r > that.r) return this;

else return that;

}

We can also implement bigger() as a class method as follows:

// Compare circles a and b and return the one with the larger radius

public static Circle bigger(Circle a, Circle b) {

if (a.r > b.r) return a;

else return b;

}

Given two Circle objects, x and y, we can use either the instance method or the class method to determine which is bigger. The invocation syntax differs significantly for the two methods, however:

// Instance method: also y.bigger(x)

Circle biggest = x.bigger(y);

Circle biggest = Circle.bigger(x, y); // Static method

Both methods work well, and, from an object-oriented design standpoint, neither of these methods is “more correct” than the other. The instance method is more formally object oriented, but its invocation syntax suffers from a kind of asymmetry. In a case like this, the choice between an instance method and a class method is simply a design decision. Depending on the circumstances, one or the other will likely be the more natural choice.

A word about System.out.println()

We’ve frequently encountered the method System.out.println()—it’s used to display output to the terminal window or console. We’ve never explained why this method has such a long, awkward name or what those two periods are doing in it. Now that you understand class and instance fields and class and instance methods, it is easier to understand what is going on: System is a class. It has a public class field named out. This field is an object of type java.io.PrintStream, and it has an instance method named println().

We can use static imports to make this a bit shorter with import static java.lang.System.out;—this will enable us to refer to the printing method as out.println() but as this is an instance method, we cannot shorten it any further.

Composition Versus Inheritance

Inheritance is not the only technique at our disposal in object-oriented design. Objects can contain references to other objects, so a larger conceptual unit can be aggregated out of smaller component parts—this is known as composition. One important related technique is delegation, where an object of a particular type holds a reference to a secondary object of a compatible type, and forwards all operations to the secondary object. This is frequently done using interface types, as shown in this example where we model the employment structure of software companies:

public interface Employee {

void work();

}

public class Programmer implements Employee {

public void work() { /* program computer */ }

}

public class Manager implements Employee {

private Employee report;

public Manager(Employee staff) {

report = staff;

}

public Employee setReport(Employee staff) {

report = staff;

}

public void work() {

report.work();

}

}

The Manager class is said to delegate the work() operation to their direct report, and no actual work is performed by the Manager object. Variations of this pattern involve some work being done in the delegating class, with only some calls being forwarded to the delegate object.

Another useful, related technique is called the decorator pattern—this provides the capability to extend objects with new functionality, including at runtime. The slight overhead is some extra work needed at design time. Let’s look at an example of the decorator pattern as applied to modeling burritos for sale at a taqueria. To keep things simple, we’ve only modeled a single aspect to be decorated—the price of the burrito:

// The basic interface for our burritos

interface Burrito {

double getPrice();

}

// Concrete implementation—standard size burrito

public class StandardBurrito implements Burrito {

private static final double BASE_PRICE = 5.99;

public double getPrice() {

return BASE_PRICE;

}

}

// Larger, super-size burrito

public class SuperBurrito implements Burrito {

private static final double BASE_PRICE = 6.99;

public double getPrice() {

return BASE_PRICE;

}

}

These cover the basic burritos that can be offered—two different sizes, at different prices. Let’s enhance this by adding some optional extras—jalapeño chilies and guacamole. The key design point here is to use an abstract base class that all of the optional decorating components will subclass:

/*

* This class is the Decorator for Burrito—it represents optional

* extras that the burrito may or may not have.

*/

public abstract class BurritoOptionalExtra implements Burrito {

private final Burrito burrito;

private final double price;

// This constructor is protected to protect against the default

// constructor and to prevent rogue client code from directly

// instantiating the base class.

protected BurritoOptionalExtra(Burrito toDecorate,

double myPrice) {

burrito = toDecorate;

price = myPrice;

}

public final double getPrice() {

return (burrito.getPrice() + price);

}

}

NOTE

The combination of an abstract base, BurritoOptionalExtra, and a protected constructor means that the only valid way to get a BurritoOptionalExtra is to construct an instance of one of the subclasses, as they have public constructors (which also hide the setup of the price of the component from client code).

Let’s test the implementation out:

Burrito lunch = new Jalapeno(new Guacamole(new SuperBurrito()));

// The overall cost of the burrito is the expected $8.09.

System.out.println("Lunch cost: "+ lunch.getPrice());

The decorator pattern is very widely used—not least in the JDK utility classes. When we discuss Java I/O in Chapter 10, we will see more examples of decorators in the wild.

Field Inheritance and Accessors

Java offers multiple potential approaches to the design issue of the inheritance of state. The programmer can choose to mark fields as protected and allow them to be accessed directly by subclasses (including writing to them). Alternatively, we can provide accessor methods to read (and write, if desired) the actual object fields, while retaining encapsulation, and leaving the fields as private.

Let’s revisit our earlier PlaneCircle example from the end of Chapter 9 and explicitly show the field inheritance:

public class Circle {

// This is a generally useful constant, so we keep it public

public static final double PI = 3.14159;

protected double r; // State inheritance via a protected field

// A method to enforce the restriction on the radius

protected void checkRadius(double radius) {

if (radius < 0.0)

throw new IllegalArgumentException("radius may not < 0");

}

// The non-default constructor

public Circle(double r) {

checkRadius(r);

this.r = r;

}

// Public data accessor methods

public double getRadius() { return r; }

public void setRadius(double r) {

checkRadius(r);

this.r = r;

}

// Methods to operate on the instance field

public double area() { return PI * r * r; }

public double circumference() { return 2 * PI * r; }

}

public class PlaneCircle extends Circle {

// We automatically inherit the fields and methods of Circle,

// so we only have to put the new stuff here.

// New instance fields that store the center point of the circle

private final double cx, cy;

// A new constructor to initialize the new fields

// It uses a special syntax to invoke the Circle() constructor

public PlaneCircle(double r, double x, double y) {

super(r); // Invoke the constructor of the superclass

this.cx = x; // Initialize the instance field cx

this.cy = y; // Initialize the instance field cy

}

public double getCentreX() {

return cx;

}

public double getCentreY() {

return cy;

}

// The area() and circumference() methods are inherited from Circle

// A new instance method that checks whether a point is inside the

// circle Note that it uses the inherited instance field r

public boolean isInside(double x, double y) {

double dx = x - cx, dy = y - cy;

// Pythagorean theorem

double distance = Math.sqrt(dx*dx + dy*dy);

return (distance < r); // Returns true or false

}

}

Instead of the preceding code, we can rewrite PlaneCircle using accessor methods, like this:

public class PlaneCircle extends Circle {

// Rest of class is the same as above The field r in

// the superclass Circle can be made private because

// we no longer access it directly here

// Note that we now use the accessor method getRadius()

public boolean isInside(double x, double y) {

double dx = x - cx, dy = y - cy; // Distance from center

double distance = Math.sqrt(dx*dx + dy*dy); // Pythagorean theorem

return (distance < getRadius());

}

}

Both approaches are legal Java, but they have some differences. As we discussed in “Data Hiding and Encapsulation”, fields that are writable outside of the class are usually not a correct way to model object state. In fact, as we will see in “Safe Java Programming” and again in “Java’s Support for Concurrency”, they can damage the running state of a program irreparably.

It is therefore unfortunate that the protected keyword in Java allows access to fields (and methods) from both subclasses and classes in the same packages as the declaring class. This, combined with the ability for anyone to write a class that belongs to any given package (except system packages), means that protected inheritance of state is potentially flawed in Java.

TIP

Java does not provide a mechanism for a member to be visible only in the declaring class and its subclasses.

For all of these reasons, it is usually better to use accessor methods (either public or protected) to provide access to state for subclasses—unless the inherited state is declared final, in which case protected inheritance of state is perfectly permissible.

Singleton

The singleton pattern is another well-known design pattern. It is intended to solve the design issue where only a single instance of a class is required or desired. Java provides a number of different possible ways to implement the singleton pattern. In our discussion, we will use a slightly more verbose form, that has the benefit of being very explicit in what needs to happen for a safe singleton:

public class Singleton {

private final static Singleton instance = new Singleton();

private static boolean initialized = false;

// Constructor

private Singleton() {

super();

}

private void init() {

/* Do initialization */

}

// This method should be the only way to get a reference

// to the instance

public static synchronized Singleton getInstance() {

if (initialized) return instance;

instance.init();

initialized = true;

return instance;

}

}

The crucial point is that for the singleton pattern to be effective, it must be impossible to create more than one of them, and it must be impossible to get a reference to the object in an uninitialized state (see later in this chapter for more on this important point). To achieve this, we require aprivate constructor, which is only called once. In our version of Singleton, we only call the constructor when we initialize the private static variable instance. We also separate out the creation of the only Singleton object from its initialization—which occurs in the private methodinit().

With this mechanism in place, the only way to get a reference to the lone instance of Singleton is via the static helper method, getInstance(). This method checks the flag initialized to see if the object is already in an active state. If it is, then a reference to the singleton object is returned. If not, then getInstance() calls init() to activate the object, and flicks the flag to true, so that next time a reference to the Singleton is requested, further initialization will not occur.

Finally, we also note that getInstance() is a synchronized method. See Chapter 6 for full details of what this means, and why it is necessary, but for now, know that it is present to guard against unintended consequences if Singleton is used in a multithreaded program.

TIP

Singleton, being one of the simplest patterns, is often overused. When used correctly, it can be a useful technique, but too many singleton classes in a program is a classic sign of badly engineered code.

The singleton pattern has some drawbacks—in particular, it can be hard to test and to separate out from other classes. It also requires care when used in mulithreaded code. Nevertheless, it is important that developers are familiar with, and do not accidentally reinvent it. The singleton pattern is often used in configuration management, but modern code will typically use a framework (often a dependency injection) to provide the programmer with singletons automatically, rather than via an explicit Singleton (or equivalent) class.

Exceptions and Exception Handling

We met checked and unchecked exceptions in “Checked and Unchecked Exceptions”. In this section, we discuss some additional aspects of the design of exceptions, and how to use them in your own code.

Recall that an exception in Java is an object. The type of this object is java.lang.Throwable, or more commonly, some subclass of Throwable that more specifically describes the type of exception that occurred. Throwable has two standard subclasses: java.lang.Error andjava.lang.Exception. Exceptions that are subclasses of Error generally indicate unrecoverable problems: the virtual machine has run out of memory, or a class file is corrupted and cannot be read, for example. Exceptions of this sort can be caught and handled, but it is rare to do so—these are the unchecked exceptions previously mentioned.

Exceptions that are subclasses of Exception, on the other hand, indicate less severe conditions. These exceptions can be reasonably caught and handled. They include such exceptions as java.io.EOFException, which signals the end of a file, andjava.lang.ArrayIndexOutOfBoundsException, which indicates that a program has tried to read past the end of an array. These are the checked exceptions from Chapter 2 (except for subclasses of RuntimeException, which are also a form of unchecked exception). In this book, we use the term “exception” to refer to any exception object, regardless of whether the type of that exception is Exception or Error.

Because an exception is an object, it can contain data, and its class can define methods that operate on that data. The Throwable class and all its subclasses include a String field that stores a human-readable error message that describes the exceptional condition. It’s set when the exception object is created and can be read from the exception with the getMessage() method. Most exceptions contain only this single message, but a few add other data. The java.io.InterruptedIOException, for example, adds a field named bytesTransferred that specifies how much input or output was completed before the exceptional condition interrupted it.

When designing your own exceptions, you should consider what other additional modeling information is relevant to the exception object. This is usually situation-specific information about the aborted operation, and the exceptional circumstance that was encountered (as we saw withjava.io.InterruptedIOException).

There are some trade-offs in the use of exceptions in application design. Using checked exceptions means that the compiler can enforce the handling (or propagation up the call stack) of known conditions that have the potential of recovery or retry. It also means that it’s more difficult to forget to actually handle errors—thus reducing the risk that a forgotten error condition causes a system to fail in production.

On the other hand, some applications will not be able to recover from certain conditions—even conditions that are theoretically modelled by checked exceptions. For example, if an application requires a config file to be placed at a specific place in the filesystem and is unable to locate it at startup, there may be very little it can do except print an error message and exit—despite the fact that java.io.FileNotFoundException is a checked exception. Forcing exceptions that cannot be recovered from to be either handled or propagated is, in these circumstances, bordering on perverse.

When designing exception schemes, there are some good practices that you should follow:

§ Consider what additional state needs to be placed on the exception—remember that it’s also an object like any other.

§ Exception has four public constructors—under normal circumstances, custom exception classes should implement all of them—to initialize the additional state, or to customize messages.

§ Don’t create many fine-grained custom exception classes in your APIs—the Java I/O and reflection APIs both suffer from this and it needlessly complicates working with those packages.

§ Don’t overburden a single exception type with describing too many conditions—for example, the Nashorn JavaScript implementation (new with Java 8) originally had overly coarse-grained exceptions, although this was fixed before release.

Finally, two exception handling antipatterns that you should avoid:

// Never just swallow an exception

try {

someMethodThatMightThrow();

} catch(Exception e){

}

// Never catch, log and rethrow an exception

try {

someMethodThatMightThrow();

} catch(SpecificException e){

log(e);

throw e;

}

The former of these two just ignores a condition that almost certainly required some action (even if just a notification in a log). This increases the likelihood of failure elsewhere in the system—potentially far from the original, real source.

The second one just creates noise—we’re logging a message but not actually doing anything about the issue—we still require some other code higher up in the system to actually deal with the problem.

Safe Java Programming

Programming languages are sometimes described as being type safe—however, this term is used rather loosely by working programmers. There are a number of different viewpoints and definitions when discussing type safety, not all of which are mutually compatible. The most useful view for our purposes is that type safety is the property of a programming language that prevents the type of data being incorrectly identified at runtime. This should be thought of as a sliding scale—it is more helpful to think of languages as being more (or less) type safe than each other, rather than a simple binary property of safe / unsafe.

In Java, the static nature of the type system helps prevent a large class of possible errors, by producing compilation errors if, for example, the programmer attempts to assign an incompatible value to a variable. However, Java is not perfectly type safe, as we can perform a cast between any two reference types—this will fail at runtime with a ClassCastException if the value is not compatible.

In this book, we prefer to think of safety as inseparable from the broader topic of correctness. This means that we should think in terms of programs, rather than languages. This emphasizes the point that safe code is not guaranteed by any widely used language, and instead considerable programmer effort (and adherence to rigorous coding discipline) must be employed if the end result is to be truly safe and correct.

We approach our view of safe programs by working with the state model abstraction as shown in Figure 5-1. A safe program is one in which:

§ All objects start off in a legal state after creation

§ Externally accessible methods transition objects between legal states

§ Externally accessible methods must not return with object in an inconsistent state

§ Externally accessible methods must reset object to a legal state before throwing

In this context, “externally accessible” means public, package-private, or protected. This defines a reasonable model for safety of programs, and as it is bound up with defining our abstract types in such a way that their methods ensure consistency of state, it’s reasonable to refer to a program satisfying these requirements as a “safe program,” regardless of the language in which such a program is implemented.

WARNING

Private methods do not have to start or end with object in a legal state, as they cannot be called by an external piece of code.

As you might imagine, actually engineering a substantial piece of code so that we can be sure that the state model and methods respect these properties, can be quite an undertaking. In languages such as Java, in which programmers have direct control over the creation of preemptively multitasked execution threads, this problem is a great deal worse.

JN6 0501

Figure 5-1. Program state transitions

Moving on from our introduction of object-oriented design, there is one final aspect of the Java language and platform that needs to be understood for a sound grounding. That is the nature of memory and concurrency—one of the most complex of the platform, but also one that rewards careful study with large dividends. It is the subject of our next chapter and concludes the first part of this book.