Classes within classes

Learn the basics of nested top-level classes and inner classes

While teaching Java, I often find that students try to declare methods within other methods. However, unlike the Pascal language -- which allows procedures (roughly equivalent to methods) to nest inside other procedures -- Java does not allow methods to nest inside other methods. As a result, the Java compiler generates an error when it encounters the following code fragment, which nests method innerMethod() inside method outerMethod():

void outerMethod ()
{
   void innerMethod ()
   {
   }
}

In contrast to method nesting, Java, beginning with Java Language Specification 1.1, supports class nesting; the Java compiler allows one class to appear inside another class. The following code fragment demonstrates the nesting of class innerClass inside class outerClass:

class outerClass
{
   class innerClass
   {
   }
}

Why does Java support class nesting, and what kinds of nested classes does Java support? This Java 101 installment answers these questions. Once you finish reading this article, you will have gained a good working knowledge of class nesting and can use that knowledge to write powerful Java programs. To begin, let's find out why Java supports class nesting.

Note
With the release of JDK 1.1 (which includes Java 1.1), Sun released the Inner Classes Specification document. That document thoroughly covers nested top-level classes and inner classes. I strongly encourage you to review that document once you finish reading this article.

Why does Java support class nesting?

Java doesn't have to support class nesting. In fact, if you study the Inner Classes Specification document, you will discover workarounds to class nesting. However, there are at least two benefits to Java's support for class nesting:

  • An improvement in source code clarity
  • A reduction in name conflicts

Source code clarity improves with class nesting because you declare a class closer to those objects it must manipulate and allow that class's methods to directly access object fields and call object methods -- even private fields and methods -- of enclosing classes. To understand that benefit, consider a scenario in which a program must iterate over an array of Job objects that exist within an Employee object:

Listing 1. JobIterator1.java

// JobIterator1.java
class Job
{
   private String jobTitle;
   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }
   public String toString ()
   {
      return jobTitle;
   }
}
class Employee
{
   private String name;
   private Job [] jobs;
   private int jobIndex = 0;
   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }
   String getName ()
   {
      return name;
   }
   boolean hasMoreJobs ()
   {
      return jobIndex < jobs.length;
   }
   Job nextJob ()
   {
      return !hasMoreJobs () ? null : jobs [jobIndex++];
   }
}
class JobIterator1
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };
      Employee e = new Employee ("John Doe", jobs);
      System.out.println (e.getName () + " works the following jobs:\n");
      while (e.hasMoreJobs ())
         System.out.println (e.nextJob ());
   }
}

When run, JobIterator1 produces the following output:

John Doe works the following jobs:
Janitor
Delivery Person

JobIterator1 consists of classes Job, Employee, and JobIterator1. Job encapsulates a job title, and Employee encapsulates an employee name and an array of those jobs the employee performs. JobIterator1 contains a main() method that creates Job and Employee objects, and prints the employee's name and jobs.

A close look at Employee reveals methods hasMoreJobs() and nextJob(). Together, those methods make up an iterator. When an Employee object initializes, an internal index in the private jobs array sets to zero. The hasMoreJobs() method returns a Boolean true value if that index's value is less than the jobs array's length. nextJob() uses that index's value to return a Job object from the array at that index -- and increments that index's value so the next call to nextJob() returns a reference to the next Job object.

JobIterator1 does feature problems. First, you can't restart an iteration after it completes. However, you could easily solve that problem by adding some sort of reset() method to Employee that assigns 0 to jobIndex. A second, more serious problem is the inability to create multiple iterators for a single Employee object. That problem arises from hasMoreJobs() and nextJob() being hardcoded within Employee. To overcome both problems, developers typically declare an iterator class whose objects iterate over the jobs array. Once an iterator finishes, a program can start a new iteration by creating a new iterator object. Also, by creating multiple iterator objects, a program can perform multiple iterations over the same Employee object's jobs array. Listing 2 demonstrates the use of an iterator class named JobIterator:

Listing 2. JobIterator2.java

// JobIterator2.java
class Job
{
   private String jobTitle;
   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }
   public String toString ()
   {
      return jobTitle;
   }
}
class Employee
{
   private String name;
   private Job [] jobs;
   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }
   String getName ()
   {
      return name;
   }
   JobIterator getJobIterator ()
   {
      return new JobIterator (jobs);
   }
}
class JobIterator
{
   private Job [] jobs;
   private int jobIndex = 0;
   JobIterator (Job [] jobs)
   {
      this.jobs = jobs;
   }
   boolean hasMoreJobs ()
   {
      return jobIndex < jobs.length;
   }
   Job nextJob ()
   {
      return !hasMoreJobs () ? null : jobs [jobIndex++];
   }
}
class JobIterator2
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };
      Employee e = new Employee ("John Doe", jobs);
      System.out.println (e.getName () + " works the following jobs:\n");
      JobIterator ji = e.getJobIterator ();
      while (ji.hasMoreJobs ())
         System.out.println (ji.nextJob ());
   }
}

JobIterator2 produces the same output as JobIterator1, but the source code differs in that JobIterator2 migrates iterator code from Employee to JobIterator. Also, Employee declares a getJobIterator() method that returns a reference to a new JobIterator object. Notice that JobIterator and Employee are tightly coupled classes: JobIterator's constructor requires a reference to Employee's private jobs array. Keep that coupling in mind, as it offers a clue as to how class nesting works at an internal level (for more information on class nesting internals see the Inner Classes Specification document).

Although JobIterator2 conveniently solves JobIterator1's problems, the new program introduces a new problem: the addition of a new JobIterator class at the same level as Employee prevents the insertion of a future generic JobIterator interface at that same level into the source file. After all, you cannot have two classes/interfaces with the same name at the same level in a source file. Although not a serious problem in our trivial example, in significant programs, situations arise where introducing classes/interfaces with the same names into the same source file is necessary. To make those names coexist, you must recognize that some classes completely depend on other classes. You should be able to declare those dependent classes within the classes they depend on. Listing 3 shows how to declare a dependent JobIterator class within an Employee class -- upon which JobIterator depends:

Listing 3. JobIterator3.java

// JobIterator3.java
class Job
{
   private String jobTitle;
   Job (String jobTitle)
   {
      this.jobTitle = jobTitle;
   }
   public String toString ()
   {
      return jobTitle;
   }
}
class Employee
{
   private String name;
   private Job [] jobs;
   Employee (String name, Job [] jobs)
   {
      this.name = name;
      this.jobs = jobs;
   }
   String getName ()
   {
      return name;
   }
   JobIterator getJobIterator ()
   {
      return new JobIterator ();
   }
   class JobIterator
   {
      private int jobIndex = 0;
      public boolean hasMoreJobs ()
      {
         return jobIndex < jobs.length;
      }
      public Object nextJob ()
      {
         return !hasMoreJobs () ? null : jobs [jobIndex++];
      }
   }
}
class JobIterator3
{
   public static void main (String [] args)
   {
      Job [] jobs = { new Job ("Janitor"), new Job ("Delivery Person") };
      Employee e = new Employee ("John Doe", jobs);
      System.out.println (e.getName () + " works the following jobs:\n");
      Employee.JobIterator eji = e.getJobIterator ();
      while (eji.hasMoreJobs ())
         System.out.println (eji.nextJob ());
   }
}

JobIterator3, which produces the same output as JobIterator1 and JobIterator2, illustrates class nesting: class Employee contains the class JobIterator declaration. That nesting results in JobIterator not requiring a constructor because JobIterator can directly access Employee's private jobs field. Also, that nesting requires JobIterator3's main() method to prefix Employee. to JobIterator whenever main() needs access to JobIterator. Because JobIterator3's JobIterator class no longer requires a constructor or its own jobs field, the source code is somewhat clearer than JobIterator2's source code.

In addition to improved source code clarity, class nesting features a second benefit. When classes nest within other classes, name conflicts reduce. Look carefully at Listing 3. The top-level classes are Job, Employee, and JobIterator3. There is also an Employee.JobIterator class. If we choose to insert a JobIterator interface at the same level as Employee, we would end up with Job, Employee, JobIterator3, and Employee.JobIterator classes, and a JobIterator interface. Because Employee.JobIterator and JobIterator represent two different names, no name conflict results.

Note
If you compile JobIterator3 and examine the resulting class files, you will discover an Employee$JobIterator.class filename. That filename corresponds to the class file containing byte codes for the nested JobIterator class within Employee. Notice the dollar sign character. Java's compiler generates names for nested classes by prefixing their enclosing class names and dollar sign characters to nested class names. Why use a dollar sign? It is a legitimate character that underlying platforms allow for filenames. In contrast, period characters (.) separate the actual name from the file's extension, and are not always valid for use in platform-specific filenames.

What nested classes does Java support?

Java divides nested classes into two main categories: nested top-level classes and inner classes. Furthermore, Java divides inner classes into the instance inner class, local inner class, and anonymous inner class categories. To understand nested classes, you need to understand each category. We begin exploring those categories by focusing on nested top-level classes.

Nested top-level classes

When you declare a class outside any other class, Java regards that class as a top-level class. If you declare a class within a top-level class and prefix keyword static to the nested class declaration, you end up with a nested top-level class. The following code fragment demonstrates a top-level class and a nested top-level class:

class TopLevelClass
{
   static class NestedTopLevelClass
   {
   }
}

Just as a static field and a static method (also known as a class field and a class method) are independent of any objects that you create from their enclosing classes, a nested top-level class is also independent from such objects. Consider the following code fragment:

class TopLevelClass
{
   static int staticField;
   int instanceField;
   static class NestedTopLevelClass
   {
      static
      {
         System.out.println ("Can access staticField " + staticField);
//         System.out.println ("Cannot access instanceField " + instanceField);
      }
      {
         System.out.println ("Can access staticField " + staticField);
//         System.out.println ("Cannot access instanceField " + instanceField);
      }
   }
}

Within the class block and object block initializers of the preceding code fragment's NestedTopLevelClass class, you can access TopLevelClass's staticField variable. However, you cannot access instanceField from within either initializer. Because NestedTopLevelClass cannot access TopLevelClass's instanceField variable, NestedTopLevelClass is independent of any TopLevelClass objects.

Caution
A nested top-level class cannot access any enclosing class's instance members (fields and methods).

Although NestedTopLevelClass cannot access TopLevelClass's instance fields, keyword static does not prevent NestedTopLevelClass from declaring its own instance fields or creating NestedTopLevelClass objects. For proof, check out Listing 4:

Listing 4. NestedTopLevelClassDemo.java

// NestedTopLevelClassDemo.java
class TopLevelClass
{
   static class NestedTopLevelClass
   {
      int myInstanceField;
      NestedTopLevelClass (int i)
      {
         myInstanceField = i;
      }
   }
}
class NestedTopLevelClassDemo
{
   public static void main (String [] args)
   {
      TopLevelClass.NestedTopLevelClass ntlc;
      ntlc = new TopLevelClass.NestedTopLevelClass (5);
      System.out.println (ntlc.myInstanceField);
   }
}
1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more