Static classes and inner classes in Java

how-to
Aug 29, 202419 mins
JavaProgramming LanguagesSoftware Development

Learn how to use the four types of nested classes to better organize your Java code.

Programming concept, person at a keyboard.
Credit: TippaPatt / Shutterstock

Nested classes are classes that are declared as members of other classes or scopes. Nesting classes is one way to better organize your code. For example, say you have a non-nested class (also known as a top-level class) that stores objects in a resizable array, followed by an iterator class that returns each object. Rather than pollute the top-level class’s namespace, you could declare the iterator class as a member of the resizable array collection class. This works because the two are closely related.

In Java, nested classes are categorized as either static member classes or inner classes. Inner classes are non-static member classes, local classes, or anonymous classes. In this tutorial you’ll learn how to work with static member classes and the three types of inner classes in your Java programs.

What you’ll learn in this Java tutorial

  • About static classes in Java
  • About the three types of inner classes:
    • Non-static member classes
    • Local classes
    • Anonymous classes
  • Examples of using nested classes in your Java programs

Static classes in Java

In Classes and objects in Java, you learned how to declare static fields and static methods as members of a class, and in Class and object initialization in Java, you learned how to declare static initializers as members of a class. Now you’ll learn how to declare static classes. Formally known as static member classes, these are nested classes that you declare at the same level as these other static entities, using the static keyword. Here’s an example of a static member class declaration:


class C
{
   static int f;

   static void m() {}

   static
   {
      f = 2;
   }

   static class D
   {
      // members
   }
}

This example introduces top-level class C with static field f, static method m(), a static initializer, and static member class D. Notice that D is a member of C. The static field f, static method m(), and the static initializer are also members of C. Since all these elements belong to class C, it is known as the enclosing class. Class D is known as the enclosed class.

Enclosure and access rules

Although it is enclosed, a static member class 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. To demonstrate, Listing 1 declares an EnclosingClass with a nested SMClass.

Listing 1. Declaring a static member class (EnclosingClass.java, version 1)


class EnclosingClass
{
   private static String s;

   private static void m1()
   {
      System.out.println(s);
   }

   static void m2()
   {
      SMClass.accessEnclosingClass();
   }

   static class SMClass
   {
      static void accessEnclosingClass()
      {
         s = "Called from SMClass's accessEnclosingClass() method";
         m1();
      }

      void accessEnclosingClass2()
      {
         m2();
      }
   }
}

Listing 1 declares a top-level class named EnclosingClass with class field s, class methods m1() and m2(), and static member class SMClass. SMClass declares class method accessEnclosingClass() and instance method accessEnclosingClass2(). Note the following:

  • m2()‘s invocation of SMClass‘s accessEnclosingClass() method requires the SMClass. prefix because accessEnclosingClass() is declared static.
  • accessEnclosingClass() is able to access EnclosingClass‘s s field and call its m1() method, even though both have been declared private.

Listing 2 presents the source code to an SMCDemo application class that demonstrates how to invoke SMClass‘s accessEnclosingClass() method. It also demonstrates how to instantiate SMClass and invoke its accessEnclosingClass2() instance method.

Listing 2. Invoking a static member class’s methods (SMCDemo.java)


public class SMCDemo
{
   public static void main(String[] args)
   {
      EnclosingClass.SMClass.accessEnclosingClass();
      EnclosingClass.SMClass smc = new EnclosingClass.SMClass();
      smc.accessEnclosingClass2();
   }
}

As shown in Listing 2, if you want to invoke a top-level class’s method from within an enclosed class, you must prefix the enclosed class’s name with the name of its enclosing class. Likewise, in order to instantiate an enclosed class you must prefix the name of that class with the name of its enclosing class. You can then invoke the instance method in the normal manner.

Compile Listings 1 and 2 as follows:


javac *.java

When you compile an enclosing class that contains a static member class, the compiler creates a class file 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. In this case, compiling results in EnclosingClass$SMCClass.class and EnclosingClass.class.

Run the application as follows:


java SMCDemo

You should observe the following output:


Called from SMClass's accessEnclosingClass() method
Called from SMClass's accessEnclosingClass() method

Example: Static classes and Java 2D

Java’s standard class library is a runtime library of class files, which store compiled classes and other reference types. The library includes numerous examples of static member classes, some of which are found in the Java 2D geometric shape classes located in the java.awt.geom package. (You’ll learn about packages in the next Java 101 tutorial.)

The Ellipse2D class found in java.awt.geom describes an ellipse, which is defined by a framing rectangle in terms of an (x,y) upper-left corner along with width and height extents. The following code fragment shows that this class’s architecture is based on Float and Double static member classes, which both subclass Ellipse2D:


public abstract class Ellipse2D extends RectangularShape
{
   public static class Float extends Ellipse2D implements Serializable
   {
      public float x, y, width, height;

      public Float()
      {
      }

      public Float(float x, float y, float w, float h)
      {
         setFrame(x, y, w, h);
      }

      public double getX()
      {
         return (double) x;
      }

      // additional instance methods
   }

   public static class Double extends Ellipse2D implements Serializable
   {
      public double x, y, width, height;

      public Double()
      {
      }

      public Double(double x, double y, double w, double h)
      {
         setFrame(x, y, w, h);
      }

      public double getX()
      {
         return x;
      }

      // additional instance methods
   }

   public boolean contains(double x, double y)
   {
      // ...
   }

   // additional instance methods shared by Float, Double, and other
   // Ellipse2D subclasses
}

The Float and Double classes extend Ellipse2D, providing floating-point and double precision floating-point Ellipse2D implementations. Developers use Float to reduce memory consumption, particularly because you might need thousands or more of these objects to construct a single 2D scene. We use Double when greater accuracy is required.

You cannot instantiate the abstract Ellipse2D class, but you can instantiate either Float or Double. You also can extend Ellipse2D to describe a custom shape that’s based on an ellipse.

As an example, let’s say you want to introduce a Circle2D class, which isn’t present in the java.awt.geom package. The following code fragment shows how you would create an Ellipse2D object with a floating-point implementation:


Ellipse2D e2d = new Ellipse2D.Float(10.0f, 10.0f, 20.0f, 30.0f);

The next code fragment shows how you would create an Ellipse2D object with a double-precision floating-point implementation:


Ellipse2D e2d = new Ellipse2D.Double(10.0, 10.0, 20.0, 30.0);

You can now invoke any of the methods declared in Float or Double by invoking the method on the returned Ellipse2D reference (e.g., e2d.getX()). In the same manner, you could invoke any of the methods that are common to Float and Double, and which are declared in Ellipse2D. An example is:


e2d.contains(2.0, 3.0)

That completes the introduction to static member classes. Next we’ll look at inner classes, which are non-static member classes, local classes, or anonymous classes. You’ll learn how to work with all three inner class types.

download
Download the source code for examples in this tutorial. Created by Jeff Friesen.

Inner class type 1: Non-static member classes

You’ve learned previously in the Java 101 series how to declare non-static (instance) fields, methods, and constructors as members of a class. You can also declare non-static member classes, which are nested non-static classes that you declare at the same level as instance fields, methods, and constructors. Consider this example:


class C
{
   int f;

   void m() {}

   C()
   {
      f = 2;
   }

   class D
   {
      // members
   }
}

Here, we introduce top-level class C with instance field f, instance method m(), a constructor, and non-static member class D. All of these entities are members of class C, which encloses them. However, unlike in the previous example, these instance entities are associated with instances of C and not with the C class itself.

Each instance of the non-static member class is implicitly associated with an instance of its enclosing class. The non-static member class’s instance methods can call the enclosing class’s instance methods and access its instance fields. To demonstrate this access, Listing 3 declares an EnclosingClass with a nested NSMClass.

Listing 3. Declare an enclosing class with a nested non-static member class (EnclosingClass.java, version 2)


class EnclosingClass
{
   private String s;

   private void m()
   {
      System.out.println(s);
   }

   class NSMClass
   {
      void accessEnclosingClass()
      {
         s = "Called from NSMClass's accessEnclosingClass() method";
         m();
      }
   }
}

Listing 3 declares a top-level class named EnclosingClass with instance field s, instance method m(), and non-static member class NSMClass. Furthermore, NSMClass declares instance method accessEnclosingClass().

Because accessEnclosingClass() is non-static, NSMClass must be instantiated before this method can be called. This instantiation must take place via an instance of EnclosingClass, as shown in Listing 4.

Listing 4. NSMCDemo.java


public class NSMCDemo
{
   public static void main(String[] args)
   {
      EnclosingClass ec = new EnclosingClass();
      ec.new NSMClass().accessEnclosingClass();
   }
}

Listing 4’s main() method first instantiates EnclosingClass and saves its reference in local variable ec. The main() method then uses the EnclosingClass reference as a prefix to the new operator, in order to instantiate NSMClass. The NSMClass reference is then used to call accessEnclosingClass().

Compile Listings 3 and 4 as follows:


javac *.java

When you compile an enclosing class that contains a non-static member class, the compiler creates a class file for the non-static member class whose name consists of its enclosing class’s name, a dollar-sign character, and the non-static member class’s name. In this case, compiling results in EnclosingClass$NSMCClass.class and EnclosingClass.class.

Run the application as follows:


java NSMCDemo

You should observe the following output:


Called from NSMClass's accessEnclosingClass() method

Example: Non-static member classes in HashMap

The standard class library includes non-static member classes as well as static member classes. For this example, we’ll look at the HashMap class, which is part of the Java Collections Framework in the java.util package. HashMap, which describes a hash table-based implementation of a map, includes several non-static member classes.

For example, the KeySet non-static member class describes a set-based view of the keys contained in the map. The following code fragment relates the enclosed KeySet class to its HashMap enclosing class:


public class HashMap<K,V> extends AbstractMap<K,V>
                          implements Map<K,V>, Cloneable, Serializable
{
   // various members

   final class KeySet extends AbstractSet<K>
   {
      // various members
   }

   // various members
}

The <K,V> and <K> syntaxes are examples of generics, a suite of related language features that help the compiler enforce type safety. I’ll introduce generics in an upcoming Java 101 tutorial. For now, you just need to know that these syntaxes help the compiler enforce the type of key objects that can be stored in the map and in the keyset, and the type of value objects that can be stored in the map.

HashMap provides a keySet() method that instantiates KeySet when necessary and returns this instance or a cached instance. Here’s the complete method:


public Set<K> keySet()
{
   Set<K> ks = keySet;
   if (ks == null)
   {
      ks = new KeySet();
      keySet = ks;
   }
   return ks;
}

Notice that the enclosed class’s (KeySet‘s) constructor is called from within the enclosing class’s (HashSet‘s) keySet() instance method. This illustrates a common practice, especially because prefixing the new operator with an enclosing class reference is rare.

Inner class type 2: Local classes

It’s occasionally helpful to declare a class in a block, such as a method body or sub-block. For example, you might declare a class that describes an iterator in a method that returns an instance of this class. Such classes are known as local classes because (as with local variables) they are local to the methods in which they are declared. Here is an example:


interface I
{
   // members
}

class C
{
   I m() // or even static I m()
   {
      class D implements I
      {
         // members
      }
      return new D();
   }
}

Top-level class C declares instance method m(), which returns an instance of local class D, which is declared in this method. Notice that m()‘s return type is interface I, which D implements. The interface is necessary because giving m() return type D would result in a compiler error–D isn’t accessible outside of m()‘s body.

A local class can be associated with an instance of its enclosing class, but only when used in a non-static context. Also, a local class can be declared anywhere that a local variable can be declared, and has the same scope as a local variable. It can access the surrounding scope’s local variables and parameters, which must be declared final. Consider Listing 5.

Listing 5. Declaring a local class within an enclosing class instance method (EnclosingClass.java, version 3)


class EnclosingClass
{
   void m(final int x)
   {
      final int y = x * 3;
      class LClass
      {
         int m = x;
         int n = y;
      }
      LClass lc = new LClass();
      System.out.println(lc.m);
      System.out.println(lc.n);
   }
}

Listing 5 declares EnclosingClass with instance method m(), declaring a local class named LClass. LClass declares a pair of instance fields (m and n). When LClass is instantiated, the instance fields are initialized to the values of final parameter x and final local variable y, as shown in Listing 6.

Listing 6. A local class declares and initializes a pair of instance fields (LCDemo.java)


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

Listing 6’s main() method first instantiates EnclosingClass. It then invokes m(5) on this instance. The called m() method multiplies this argument by 3, instantiates LClass, whose <init>() method assigns the argument and the tripled value to its pair of instance fields and outputs LClass‘s instance fields. (Note that in this case the local class uses the <init>() method instead of a constructor to interact with its instance fields.)

Compile Listings 5 and 6 as follows:


javac *.java

When you compile a class whose method contains a local class, the compiler creates a class file for the local class whose name consists of its enclosing class’s name, a dollar-sign character, a 1-based integer, and the local class’s name. In this case, compiling results in EnclosingClass$1LClass.class and EnclosingClass.class.

A note about local class file name

When generating a name for a local class’s class file, the compiler adds an integer to the generated name. This integer is probably generated to distinguish a local class’s class file from a non-static member class’s class file. If two local classes have the same name, the compiler increments the integer to avoid conflicts. Consider the following example:


public class EnclosingClass
{
    public void m1()
    {
       class LClass
       {
       }
    }

    public void m2()
    {
       class LClass
       {
       }
    }

    public void m3()
    {
       class LClass2
       {
       }
    }
}

EnclosingClass declares three instance methods that each declare a local class. The first two methods generate two different local classes with the same name. The compiler generates the following class files:


EnclosingClass$1LClass.class
EnclosingClass$1LClass2.class
EnclosingClass$2LClass.class
EnclosingClass.class

Run the application as follows:


java LCDemo

You should observe the following output:


5
15

Example: Using local classes in regular expressions

The standard class library includes examples of local class usage. For example, the Matcher class, in java.util.regex, provides a results() method that returns a stream of match results. This method declares a MatchResultIterator class for iterating over these results:


public Stream<MatchResult> results()
{
   class MatchResultIterator implements Iterator<MatchResult>
   {
      // members
   }
   return StreamSupport.
          stream(Spliterators.spliteratorUnknownSize(new MatchResultIterator(),
                                                     Spliterator.ORDERED |
                                                     Spliterator.NONNULL),
                 false);
}

Note the instantiation of MatchResultIterator() following the class declaration. Don’t worry about parts of the code that you don’t understand; instead, think about the usefulness in being able to declare classes in the appropriate scopes (such as a method body) to better organize your code.

Inner class type 3: Anonymous classes

Static member classes, non-static member classes, and local classes have names. In contrast, anonymous classes are unnamed nested classes. You introduce them in the context of expressions that involve the new operator and the name of either a base class or an interface that is implemented by the anonymous class:


// subclass the base class

abstract class Base
{
   // members
}

class A
{
   void m()
   {
      Base b = new Base()
               {
                 // members
               };
   }
}

// implement the interface

interface I
{
   // members
}

class B
{
   void m()
   {
      I i = new I()
            {
               // members
            };
   }
}

The first example demonstrates an anonymous class extending a base class. Expression new Base() is followed by a pair of brace characters that signify the anonymous class. The second example demonstrates an anonymous class implementing an interface. Expression new I() is followed by a pair of brace characters that signify the anonymous class.

Anonymous classes are useful for expressing functionality that’s passed to a method as its argument. For example, consider a method for sorting an array of integers. You want to sort the array in ascending or descending order, based on comparisons between pairs of array elements. You might duplicate the sorting code, with one version using the less than (<) operator for one order, and the other version using the greater than (>) operator for the opposite order. Alternatively, as shown below, you could design the sorting code to invoke a comparison method, then pass an object containing this method as an argument to the sorting method.

Listing 7. Using an anonymous class to pass functionality as a method argument (Comparer.java)


public abstract class Comparer
{
   public abstract int compare(int x, int y);
}

The compare() method is invoked with two integer array elements and returns one of three values: a negative value if x is less than y, 0 if both values are the same, and a positive value if x is greater than y. Listing 8 presents an application whose sort() method invokes compare() to perform the comparisons.

Listing 8. Sorting an array of integers with the Bubble Sort algorithm (ACDemo.java)


public class ACDemo
{
   public static void main(String[] args)
   {
      int[] a = { 10, 30, 5, 0, -2, 100, -9 };
      dump(a);
      sort(a, new Comparer()
                  {
                     public int compare(int x, int y)
                     {
                        return x - y;
                     }
                  });
      dump(a);
      int[] b = { 10, 30, 5, 0, -2, 100, -9 };
      sort(b, new Comparer()
                  {
                     public int compare(int x, int y)
                     {
                        return y - x;
                     }
                  });
      dump(b);
   }

   static void dump(int[] x)
   {
      for (int i = 0; i < x.length; i++)
         System.out.print(x[i] + " ");
      System.out.println();
   }

   static void sort(int[] x, Comparer c)
   {
      for (int pass = 0; pass < x.length - 1; pass++)
         for (int i = x.length - 1; i > pass; i--)
            if (c.compare(x[i], x[pass]) < 0)
            {
               int temp = x[i];
               x[i] = x[pass];
               x[pass] = temp;
            }
   }
}

The main() method reveals two calls to its companion sort() method, which sorts an array of integers via the Bubble Sort algorithm. Each call receives an integer array as its first argument, and a reference to an object created from an anonymous Comparer subclass as its second argument. The first call achieves an ascending order sort by subtracting y from x; the second call achieves a descending order sort by subtracting x from y.

Compile Listings 7 and 8 as follows:


javac *.java

When you compile a class whose method contains an anonymous class, the compiler creates a class file for the anonymous class whose name consists of its enclosing class’s name, a dollar-sign character, and an integer that uniquely identifies the anonymous class. In this case, compiling results in ACDemo$1.class and ACDemo$2.class in addition to ACDemo.class.

Run the application as follows:


java ACDemo

You should observe the following output:


10 30 5 0 -2 100 -9
-9 -2 0 5 10 30 100
100 30 10 5 0 -2 -9

Example: Using anonymous classes with an AWT event handler

Anonymous classes can be used with many packages in the standard class library. For this example, we’ll use an anonymous class as an event handler in the Abstract Windowing Toolkit or Swing Windowing Toolkit. The following code fragment registers an event handler with Swing’s JButton class, which is located in the javax.swing package. JButton describes a button that performs an action (in this case printing a message) when clicked.


JButton btnClose = new JButton("close");
btnClose.addActionListener(new ActionListener()
                               {
                                  public void actionPerformed(ActionEvent ae)
                                  {
                                     System.out.println("close button clicked");
                                  }
                               });

The first line instantiates JButton, passing close as the button label to JButton‘s constructor. The second line registers an action listener with the button. The action listener’s actionPerformed() method is invoked whenever the button is clicked. The object passed to addActionListener() is instantiated from an anonymous class that implements the java.awt.event.ActionListener interface.

Conclusion

Java’s nesting capabilities help you organize non-top-level reference types. For top-level reference types, Java provides packages. The next Java 101 tutorial introduces you to packages and static imports in Java.

More from this author