Chapter     5

Mastering Advanced Language Features, Part 1

In Chapters 2 through 4, I laid a foundation for learning the Java language. In Chapter 5, I will add to this foundation by introducing you to some of Java’s more advanced language features, specifically those features related to nested types, packages, static imports, and exceptions. Additional advanced language features are covered in Chapter 6.

Mastering Nested Types

Classes that are declared outside of any class are known as top-level classes. Java also supports nested classes, which are classes that are 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.

In this section, I will introduce you to static member classes and inner classes. For each kind of nested class, I will provide a brief introduction, an abstract example, and a more practical example. I will then briefly examine the topics of inner classes and memory leaks as well as nesting interfaces within classes and nesting classes within interfaces.

Static Member Classes

A static member class is astatic member of an enclosing class. Although enclosed, it doesn’t 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 5-1 presents a static member class declaration.

Listing 5-1. Declaring a Static Member Class

class EnclosingClass
{
   private static int i;
 
   private static void m1()
   {
      System.out.println(i);
   }
 
   static void m2()
   {
      EnclosedClass.accessEnclosingClass();
   }
 
   static class EnclosedClass
   {
      static void accessEnclosingClass()
      {
         i = 1;
         m1();
      }
 
      void accessEnclosingClass2()
      {
         m2();
      }
   }
}

Listing 5-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 be prefixed with EnclosedClass and the member access operator to call this method.

Listing 5-2 presents the source code to an application class that demonstrates how to invoke EnclosedClass’s accessEnclosingClass() class method and instantiate EnclosedClass and invoke its accessEnclosingClass2() instance method.

Listing 5-2. Invoking a Static Member Class’s Class and Instance Methods

public class SMCDemo
{
   public static void main(String[] args)
   {
      EnclosingClass.EnclosedClass.accessEnclosingClass(); // Output: 1
      EnclosingClass.EnclosedClass ec = new EnclosingClass.EnclosedClass();
      ec.accessEnclosingClass2(); // Output: 1
   }
}

Listing 5-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 5-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.

Listing 5-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 5-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 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 5-4.

Listing 5-4. Creating and Using Different Rectangle Implementations

public 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 5-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 5-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

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. Additional examples include java.util.AbstractMap.SimpleEntry and java.io.ObjectInputStream.GetField.

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 5-1 and you will discover EnclosingClass$EnclosedClass.class in addition to 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 5-5 presents a nonstatic member class declaration.

Listing 5-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 5-5 declares a top-level class named EnclosingClass with instance field i, instance method m1(), 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 5-6 accomplishes these tasks.

Listing 5-6. Calling a Nonstatic Member Class’s Instance Method

public class NSMCDemo
{
   public static void main(String[] args)
   {
      EnclosingClass ec = new EnclosingClass();
      ec.new EnclosedClass().accessEnclosingClass(); // Output: 1
   }
}

Listing 5-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 5-7’s ToDo class to implement these items.

Listing 5-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();
   }
}

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 don’t know how many instances will be stored, and Java arrays have fixed lengths. See Listing 5-8.

Listing 5-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;
      }
   }
}

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 5-9 demonstrates these methods.

Listing 5-9. Creating and Iterating Over a ToDoList of ToDo Instances

public 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 = #1, Desc = Do laundry.
Name = #2, Desc = Buy groceries.
Name = #3, Desc = Vacuum apartment.
Name = #4, Desc = Write report.
Name = #5, Desc = 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 9.)

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 anonymous implementation of an interface) and instantiated any place where it is legal to specify an expression. Listing 5-10 demonstrates an anonymous class declaration and instantiation.

Listing 5-10. Declaring and Instantiating an Anonymous Class That Extends a Class

abstract class Speaker
{
   abstract void speak();
}
 
public 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 5-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 when there are no arguments.

An anonymous class doesn’t have a constructor (because the anonymous class doesn’t 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 virtual machine, it lets an anonymous class instance only access local variables and parameters that are declared final (see Listing 5-10). On 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 (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 5-11 demonstrates an alternative anonymous class declaration and instantiation.

Listing 5-11. Declaring and Instantiating an Anonymous Class That Implements an Interface

interface Speakable
{
   void speak();
}
 
public 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 5-11 is very similar to Listing 5-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 5-11 behaves like Listing 5-10.

Although an anonymous class doesn’t 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");
                   }
                });

However, keep in mind that there is a downside to using anonymous classes. Because they are anonymous, you cannot reuse anonymous classes in other parts of your applications.

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.

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 5-12’s local class declaration accesses a final parameter and a final local variable.

Listing 5-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 5-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 5-13 demonstrates this local class.

Listing 5-13. Demonstrating a Local Class

public class LCDemo
{
   public static void main(String[] args)
   {
      EnclosingClass ec = new EnclosingClass();
      ec.m(10);
   }
}

After instantiating EnclosingClass, Listing 5-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 5-14 declares an Iterator interface and a ToDoList class whose iterator() method returns an instance of its local Iter class as an Iterator instance (because Iter implements Iterator).

Listing 5-14. The Iterator Interface and the 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 5-15 demonstrates Iterator, the refactored ToDoList class, and Listing 5-7’s ToDo class.

Listing 5-15. Creating and Iterating Over a ToDoList of ToDo Instances with a Reusable Iterator

public 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."));
      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 5-9.

Inner Classes and Memory Leaks

Instances of inner classes contain implicit references to their outer classes. Prolonging the existence of an inner class instance (such as storing its reference in a static variable) can result in a memory leak in which the outer instance may be referencing a large graph of objects that cannot be garbage collected because of the prolonged inner class reference. Consider Listing 5-16’s example.

Listing 5-16. Demonstrating a Memory Leak in the Context of a Local Class

public class InnerLeakDemo
{
   public static void main(String[] args)
   {
      class Outer
      {
         String s = "outer string";
 
         class Inner
         {
            String s = "inner string";
 
            void print()
            {
               System.out.println(s);
               System.out.println(Outer.this.s);
            }
         }
      }
      Outer o = new Outer();
      Outer.Inner oi = o.new Inner();
      oi.print();
   }
}

Listing 5-16’s main() method declares a local class named Outer, which declares a nonstatic member class named Inner. Each of Outer and Inner declares a String instance field named s, and Inner also declares a print() method.

After declaring Outer, main() instantiates this local class and then instantiates its nested Inner member. Finally, it invokes Inner’s print() method.

Compile Listing 5-16 (javac InnerLeakDemo.java) and run this application (java InnerLeakDemo). You should observe the following output:

inner string
outer string

This output reveals that Outer’s s field is present for as long as the reference to its Inner class exists. This is an example of a memory leak.

Although this example is trivial, you’ll find it more profoundly demonstrated in an Android context, in which the inner class can outlive its activity outer class and delay the activity from being garbage collected and its resources released. To learn more about this problem, check out Alex Lockwood’s “How to Leak a Context: Handlers & Inner Classes” blog post (www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html).

Interfaces within Classes and Classes within Interfaces

Interfaces can be nested within classes. Once declared, an interface is considered to be static even when it is not declared static. For example, Listing 5-17 declares an enclosing class named X along with two nested static interfaces named A and B.

Listing 5-17. Declaring a Pair of Interfaces Within a Class

class X
{
   interface A
   {
   }
 
   static interface B
   {
   }
}

You would access Listing 5-17’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 by nested classes. Collectively, these types are nested because they cannot (as in Listing 5-14’s Iter local class) or need not appear at the same level as a top-level class and pollute its package namespace. The java.util.Map.Entry interface and HashMap class is a good example.

Classes can be nested within interfaces. Once declared, a class is considered to be static even when it is not declared static. Although nowhere near as common as nesting an interface within a class, nested classes have a potential use, which Listing 5-18 demonstrates.

Listing 5-18. Tightly Binding a Class to an Interface

public interface Addressable
{
   class Address
   {
      private String boxNumber;
      private String street;
      private String city;
 
      public Address(String boxNumber, String street, String city)
      {
         this.boxNumber = boxNumber;
         this.street = street;
         this.city = city;
      }
 
      public String getBoxNumber()
      {
         return boxNumber;
      }
 
      public String getStreet()
      {
         return street;
      }
 
      public String getCity()
      {
         return city;
      }
 
      @Override
      public String toString()
      {
         return boxNumber+" - "+street+" - "+city;
      }
   }
 
   Address getAddress();
}

Listing 5-18 declares an Addressable interface that describes any entity associated with an address, such as a letter, parcel, or postcard. This interface declares an Address class to store the address components, and an Address getAddress() method that returns the address.

Assuming the existence of Letter, Parcel, and Postcard classes whose constructors take Address arguments, the following code fragment shows you how you could construct an array of addressables and then iterate over it, obtaining and printing each address:

Addressable[] addressables =
{
   new Letter(new Addressable.Address("10", "AnyStreet", "AnyTown")),
   new Parcel(new Addressable.Address("20", "Doe Street", "NewTown")),
   new Postcard(new Addressable.Address("30", "Ender Avenue", "AnyCity"))
};
 
for (int i = 0; i < addressables.length; i++)
   System.out.println(addressables[i].getAddress());

Notice that you must specify Addressable.Address to access the nested Address class. (You can use the static imports feature discussed in Chapter 6 to save yourself from typing the Addressable. prefix.)

Why nest Address instead of making it a separate top-level class? The idea here is that there exists a tight relationship between Addressable and Address, and you want to capture this relationship. Also, you might have a different top-level Address class and want to prevent a name conflict.

Nesting a class in an interface is considered by many to be a bad practice, because it goes against the ideas of object-oriented programming and the notion of an interface. However, you should be aware of this capability because you may encounter it in practice.

Mastering 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 don’t 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.

In this section, I will introduce you to packages. After defining this term and explaining why package names must be unique, I will present the package and import statements. I will next explain how the virtual machine searches for packages and types and then I will present an example that shows you how to work with packages. I will close this section 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 (they have main() methods), you should consider storing your types (especially when they are reusable) in packages. Get into the habit now because you’ll use packages extensively when developing Android apps. Each Android app must be stored in its own unique package.

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.

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.

Note  Each of Oracle’s and Google Android’s standard class libraries 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.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 doesn’t have an area() method, the compiler will report an error.

This scenario illustrates the importance of choosing unique package names. Specifically, the top-level 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. When 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.

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  When a source file doesn’t 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 doesn’t 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 when it encounters a Circle class, a Rectangle class, or even an Employee class (if Employee hasn’t 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.

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 doesn’t 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’s 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 virtual machine 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 virtual machine. 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 is specified when starting the virtual machine (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 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 (or -cp) option used to start the virtual machine or, when not present, the CLASSPATH environment variable.

Runtime Search

When the compiler or any other Java application runs, the virtual machine will encounter types and must load their associated classfiles via special code known as a classloader. The virtual machine will use the previously stored package information that is associated with the encountered type in a search for that type’s classfile.

The virtual machine 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/-cp 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 Mac OS X, 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 5-19 presents the Logger interface, which describes objects that log messages.

Listing 5-19. 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 5-20 presents the LoggerFactory abstract class.

Listing 5-20. 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 object for logging messages to an appropriate destination. It uses the varargs (variable arguments) feature (see Chapter 3) to optionally accept an extra String argument for those destination types that require the argument. For example, FILE requires a filename.

Listing 5-21 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 5-21. Logging Messages to the Console

package logging;
 
class Console implements Logger
{
   private String dstName;
 
   Console(String dstName)
   {
      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 5-22 presents the package-private File class.

Listing 5-22. 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;
   }
 
   @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 11, 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 5-23 presents an application that tests this library.

Listing 5-23. Testing the Logging Library

import logging.Logger;
import logging.LoggerFactory;
 
public 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");
   }
}

Follow the 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 5-19 to a file named Logger.java in the logging directory.
  4. Copy Listing 5-20 to a file named LoggerFactory.java in the logging directory.
  5. Copy Listing 5-21 to a file named Console.java in the logging directory.
  6. Copy Listing 5-22 to a file named File.java in the logging directory.
  7. Copy Listing 5-23 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 these steps, 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 virtual machine not finding the logging package and its LoggerFactory classfile.

You can solve this problem by specifying -classpath/-cp when running the java tool or by adding the location of the logging package to the CLASSPATH environment variable. For example, I chose to use -classpath (which I find more convenient) 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 virtual machine complains about not finding the TestLogger classfile.

Tip  If you discover an error message where the virtual machine reports that it cannot find an application classfile, try appending a period character to the classpath. Doing so will probably fix the problem.

Packages and JAR Files

The JDK provides a jar tool that is used to archive classfiles in JAR (Java ARchive) 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, you 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 the following command:
    jar cf logger.jar logging*.class

    The c option stands for “create new archive” and the f option stands for “specify archive filename.”

    You could alternatively execute the following command:

    jar cf logger.jar logging/*.class

You should now find a logger.jar file in the current directory. To prove to yourself that this file contains the four classfiles, execute the following command, where the t option stands for “list table of contents”:

jar tf logger.jar

You can run TestLogger.class by adding logger.jar to the classpath. For example, you can run TestLogger under Windows via the following command:

java -classpath logger.jar;. TestLogger

Note  Although you can create your own logging framework, doing so is a waste of time. Instead, you should leverage the java.util.logging package (see Chapter 16) that’s included in the standard class library.

Mastering 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 5-24 presents an example.

Listing 5-24. 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 5-25).

Listing 5-25. Implementing a Constant Interface

public class TrafficFlow implements Directions
{
   public static void main(String[] args)
   {
      showDirection((int) (Math.random()* 4));
   }
 
   static void showDirection(int dir)
   {
      switch (dir)
      {
         case NORTH: System.out.println("Moving north"); break;
         case SOUTH: System.out.println("Moving south"); break;
         case EAST : System.out.println("Moving east"); break;
         case WEST : System.out.println("Moving west");
      }
   }
}

Listing 5-25’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 don’t have to qualify them with their class names. It’s implemented via a small modification to the import statement, as follows:

import staticpackagespec.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 doesn’t know whether PI belongs to Math or to Circle.

Caution  Overuse of static imports can make your code unreadable and unmaintainable. Anyone reading your code could have a hard time finding out which class a static member comes from, especially when importing all static member names from a class. Also, static imports pollute the code’s namespace with all of the static members you import. Eventually, you may run into name conflicts that are hard to resolve.

Mastering 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 virtual machine never runs out of memory when the application needs to instantiate objects.

In contrast, real-world applications occasionally attempt to open files that don’t exist, attempt to connect to remote computers that are unable to communicate with them, and require more memory than the virtual machine 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, I will look at representing exceptions in source code. I will then examine the topics of throwing and handling exceptions and conclude 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 when the file doesn’t exist.

This example illustrates an exception that cannot be prevented. However, a workaround is possible. For example, the application can detect that the file doesn’t 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 virtual machine 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’s impossible (or at least inadvisable) to work around; the application must terminate, presenting a message to the user that explains why it’s 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 will introduce you to Java’s exception and error class hierarchy, emphasizing the difference between checked and runtime exceptions. I will close my discussion on representing exceptions in source code by discussing custom exception classes.

Error Codes vs. 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 5-19’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 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 virtual machine 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 [such as the virtual machine 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 virtual machine 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-evident, 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). Figure 5-1 reveals Throwable and its immediate subclasses.

9781430264545_Fig05-01.jpg

Figure 5-1. The exceptions hierarchy is rooted in the Throwable class

Exception is the root class for all exception-oriented throwables. Similarly, Error is the root class for all error-oriented throwables.

Table 5-1 identifies and describes most of Throwable’s constructors and methods.

Table 5-1. Throwable’s Constructors and Methods

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.

Throwable(Throwable cause)

Create a throwable whose detail message is the string representation of a nonnull cause or null.

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 7.)

Throwable getCause()

Return the cause of this throwable. When 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 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’s 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 store the cause. If you don’t call either constructor, you can alternatively call initCause() directly, but you must do so immediately after creating the throwable. Call the getCause() method to return the cause.

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 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 (see http://en.wikipedia.org/wiki/Remote_procedure_call) 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 11.)

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 four 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 4), java.lang.IOException (discussed in Chapter 11), and other classes. Similarly, Error is itself subclassed by java.lang.AssertionError (discussed in Chapter 6), java.lang.OutOfMemoryError, and other classes.

Caution  Never instantiate Throwable, Exception, or Error. The resulting objects are meaningless because they are too generic.

Checked Exceptions vs. 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 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 doesn’t 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, which is thrown when you attempt to access an array element with a negative index or an index that is greater than or equal to the length of the array. (In hindsight, RuntimeException should have been named UncheckedException because all exceptions occur at runtime.)

Note  Many developers are unhappy 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 the 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. When 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.

Suppose you are creating a Media class whose static methods are to perform 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 doesn’t 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 5-26 presents this class’s declaration.

Listing 5-26. 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).

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 5-27.

Listing 5-27. 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 5-27 declares the Media class to be final because this utility 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 must be handled by some other method. It consists of reserved word throws followed by a comma-separated 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 InvalidMediaException or IOException instance to the virtual machine.

convert() also demonstrates the throw statement, which consists of reserved word throw followed by an instance of Throwable or a subclass. (You will typically instantiate an Exception subclass.) This statement throws the instance to the virtual machine, 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. For example, you cannot pass null filenames to convert().

The second use of the throw statement is to throw a media.InvalidMediaFormatException instance when the expected media format doesn’t 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:

  • 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 virtual machine 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 doesn’t 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 doesn’t 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 doesn’t also handle the exception or list the exception in its throws clause.
  • If at all possible, don’t 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.
  • 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 single-parameter 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 virtual machine will search for a handler. It first examines 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 virtual machine 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 virtual machine 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 indicates that an ArithmeticException 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.

Handling Multiple Exception Types

You can specify multiple catch blocks after a try block. For example, Listing 5-27’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 5-28.

Listing 5-28. Handling Different Kinds of Exceptions

import java.io.FileNotFoundException;
import java.io.IOException;
 
import media.InvalidMediaFormatException;
import media.Media;
 
public 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 5-28 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.

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 5-28 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 5-28’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, don’t create an empty catch block. It swallows exceptions and you don’t 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.

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:

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 an ancestor method can finish handling it. Another possibility is to log the exception (for later analysis), which is demonstrated in the following example:

catch (FileNotFoundException fnfe)
{
   logger.log(fnfe);
   throw fnfe; // Rethrow the exception here.
}

Performing Cleanup

In some situations, you might want to execute cleanup code before execution leaves a method following a thrown exception. 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 may be handled (and possibly rethrown) before finally executes. In the latter case, the exception is handled (and possibly rethrown) after finally executes.

Listing 5-29 demonstrates the first scenario in the context of a simulated file-copying application’s main() method.

Listing 5-29. Cleaning Up by Closing Files After Handling a Thrown Exception

import java.io.IOException;
 
public class Copy
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy srcFile dstFile");
         return;
      }
 
      int fileHandleSrc = 0;
      int fileHandleDst = 1;
      try
      {
         fileHandleSrc = open(args[0]);
         fileHandleDst = create(args[1]);
         copy(fileHandleSrc, fileHandleDst);
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
         return;
      }
      finally
      {
         close(fileHandleSrc);
         close(fileHandleDst);
      }
   }
 
   static int open(String filename)
   {
      return 1; // Assume that filename is mapped to integer.
   }
 
   static int create(String filename)
   {
      return 2; // Assume that filename is mapped to integer.
   }
 
   static void close(int fileHandle)
   {
      System.out.println("closing file: " + fileHandle);
   }
 
   static void copy(int fileHandleSrc, int fileHandleDst) throws IOException
   {
      System.out.println("copying file " + fileHandleSrc + " to file " +
                         fileHandleDst);
      if (Math.random() < 0.5)
         throw new IOException("unable to copy file");
   }
}

Listing 5-29 presents a Copy application class that simulates the copying of bytes from a source file to a destination file. The try block invokes the open() method to open the source file and the create() method to create the destination file. Each method returns an integer-based file handle that uniquely identifies the file.

Next, this block calls the copy() method to perform the copy. After outputting a suitable message, copy() invokes the Math class’s random() method (officially discussed in Chapter 7) to return a random number between 0 and 1. When this method returns a value less than 0.5, which simulates a problem (perhaps the disk is full), the IOException class is instantiated and this instance is thrown.

The virtual machine locates the catch block that follows the try block and causes its handler to execute, which outputs a message. Then, the code in the finally block that follows the catch block is allowed to execute. Its purpose is to close both files by invoking the close() method on the passed file handle.

Compile this source code (javac Copy.java) and run the application with two arbitrary arguments (java Copy x.txt x.bak). You should observe the following output when there is no problem:

copying file 1 to file 2
closing file: 1
closing file: 2

When something goes wrong, you should observe the following output:

copying file 1 to file 2
I/O error: unable to copy file
closing file: 1
closing file: 2

Whether or not an I/O error occurs, notice that the finally block is the final code to execute. The finally block executes even though the catch block ends with a return statement.

This example illustrates finally block execution after a thrown exception is handled. However, you might want to perform cleanup before the exception is handled. Listing 5-30 presents a variation of the Copy application that demonstrates this alternative.

Listing 5-30. Cleaning Up by Closing Files Before Handling a Thrown Exception

import java.io.IOException;
 
public class Copy
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 2)
      {
         System.err.println("usage: java Copy srcFile dstFile");
         return;
      }
 
      int fileHandleSrc = 0;
      int fileHandleDst = 1;
      try
      {
         fileHandleSrc = open(args[0]);
         fileHandleDst = create(args[1]);
         copy(fileHandleSrc, fileHandleDst);
      }
      finally
      {
         close(fileHandleSrc);
         close(fileHandleDst);
      }
   }
 
   static int open(String filename)
   {
      return 1; // Assume that filename is mapped to integer.
   }
 
   static int create(String filename)
   {
      return 2; // Assume that filename is mapped to integer.
   }
 
   static void close(int fileHandle)
   {
      System.out.println("closing file: " + fileHandle);
   }
 
   static void copy(int fileHandleSrc, int fileHandleDst) throws IOException
   {
      System.out.println("copying file " + fileHandleSrc + " to file " +
                         fileHandleDst);
      if (Math.random() < 0.5)
         throw new IOException("unable to copy file");
   }
}

Listing 5-30 is nearly identical to Listing 5-29. The only difference is the throws clause appended to the main() method header and the removal of the catch block. When IOException is thrown, the finally block executes before execution leaves the main() method. This time, Java’s default exception handler executes printStackTrace() and you observe output similar to the following:

copying file 1 to file 2
closing file: 1
closing file: 2
Exception in thread "main" java.io.IOException: unable to copy file
        at Copy.copy(Copy.java:48)
        at Copy.main(Copy.java:19)

EXERCISES

The following exercises are designed to test your understanding of Chapter 5’s content.

  1. What is a nested class?
  2. Identify the four kinds of nested classes.
  3. Which nested classes are also known as inner classes?
  4. True or false: A static member class has an enclosing instance.
  5. How do you instantiate a nonstatic member class from beyond its enclosing class?
  6. When is it necessary to declare local variables and parameters final?
  7. True or false: An interface can be declared within a class or within another interface.
  8. Define package.
  9. How do you ensure that package names are unique?
  10. What is a package statement?
  11. True or false: You can specify multiple package statements in a source file.
  12. What is an import statement?
  13. How do you indicate that you want to import multiple types via a single import statement?
  14. During a runtime search, what happens when the virtual machine cannot find a classfile?
  15. How do you specify the user classpath to the virtual machine?
  16. Define constant interface.
  17. Why are constant interfaces used?
  18. Why are constant interfaces bad?
  19. What is a static import statement?
  20. How do you specify a static import statement?
  21. What is an exception?
  22. In what ways are objects superior to error codes for representing exceptions?
  23. What is a throwable?
  24. What does the getCause() method return?
  25. What is the difference between Exception and Error?
  26. What is a checked exception?
  27. What is a runtime exception?
  28. Under what circumstance would you introduce your own exception class?
  29. True or false: You use a throw statement to identify exceptions that are thrown from a method by appending this statement to a method’s header.
  30. What is the purpose of a try statement, and what is the purpose of a catch block?
  31. What is the purpose of a finally block?
  32. A 2D graphics package supports two-dimensional drawing and transformations (rotation, scaling, translation, etc.). These transformations require a 3-by-3 matrix (a table). Declare a G2D class that encloses a private Matrix nonstatic member class. 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).
  33. Extend the logging package to support a null device in which messages are thrown away.
  34. 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.
  35. Modify TestLogger to respond appropriately to thrown CannotConnectException and NotConnectedException objects.

Summary

Classes that are declared outside of any class are known as top-level classes. Java also supports nested classes, which are classes that are declared as members of other classes or scopes.

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.

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 you must check 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 don’t 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 virtual machine, which searches for an appropriate handler. When 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 an exception is thrown or not, and before a thrown exception leaves the method.

Chapter 6 continues to explore the Java language by focusing on assertions, annotations, generics, and enums.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.198.36