Skip to content

Unit 29: Nested Class

Learning Objectives

After completing this unit, students should be able to:

  • Explain why and when nested classes are used, particularly for encapsulation, information hiding, and logical grouping of tightly coupled classes.
  • Differentiate between the kinds of nested classes in Java (static nested classes, inner classes, local classes, and anonymous classes), including their scoping rules and access to enclosing members.
  • Reason about access control and abstraction barriers when exposing or hiding nested classes, and understand how this affects clients of a class.
  • Apply qualified this correctly to disambiguate between enclosing instances in inner classes.
  • Explain and predict variable capture behavior, including the notion of effectively final variables and its implications for program correctness.
  • Write and use local and anonymous classes appropriately, especially for single-use behaviors such as custom comparators.

Overview

As programs grow in size, a class often requires several helper classes that exist solely to support its internal implementation. Declaring these helpers as top-level classes can clutter the namespace and expose unnecessary implementation details to clients.

Java allows classes to be defined within other classes or even within methods. These nested classes let us group tightly coupled components together, keep helper classes within the abstraction barrier, and express design intent more clearly. Nested classes also interact closely with Java’s access control, static and instance contexts, and scoping rules.

In this unit, we examine the different kinds of nested classes in Java and the rules that govern their access to enclosing state, including variable capture and the requirement for effectively final variables. Understanding these mechanisms will help you write better-encapsulated code and understand common patterns used in Java libraries.

Nested Class

A nested class is a class defined within an enclosing class. For example, the following declaration declares a private nested class named B within the class A.

1
2
3
4
5
class A {
  private class B {
      :
  }
}

Nested classes are used to group logically relevant classes together. Typically, a nested class is tightly coupled with the enclosing class and would have no use outside of the enclosing class. Nested classes can be used to encapsulate implementation details within an enclosing class, for instance, when the implementation of the enclosing class becomes too complex. As such, they are useful for "helper" classes that serve specific purposes.

A nested class is a field of the enclosing class and can access its fields and methods, including those declared as private. We can keep the nested class within the abstraction barrier by declaring the nested class as private if there is no need for it to be exposed to the client outside the barrier.

Since the nested class can access the private fields of the enclosing class, we should introduce a nested class only if the nested class belongs to the same encapsulation as the enclosing class. Otherwise, the enclosing class would leak its implementation details to the nested class.

Take the HashMap<K,V> class for instance. The implementation of HashMap<K,V> contains several nested classes, including HashIterator, which implement an Iterator<E> interface for iterating through the key and value pairs in the map, and an Entry<K,V> class, which encapsulates a key-value pair in the map. Some of these classes are declared private, as they are only used within the HashMap<K,V> class.

Example from CS2030S This Semester

We can take another example from your programming exercise on bank simulation. In one of many possible designs, the subclasses of Event: ArrivalEvent, DepartureEvent, etc. are not used anywhere outside of BankSimulation. They can be safely encapsulated within BankSimulation as inner classes, so that these classes can access the fields within the BankSimulation class, simplifying their implementation.

A nested class can be either static or non-static. Just like static fields and static methods, a static nested class is associated with the enclosing class, NOT an instance. So, it can only access static fields and static methods of the enclosing class. A non-static nested class, on the other hand, carries implicitly a reference to the enclosing object1 and can access all fields and methods of the enclosing instance. A non-static nested class is also known as an inner class. It cannot be instantiated without an enclosing instance.

The example below shows an enclosing class A with two nested classes, a non-static inner class B, and a static nested class C. B can access instance fields, instance methods, class fields, and class methods in A. C can only access the class fields and class methods in A.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class A {
  private int x;
  private static int y;

  private class B {
    void foo() {
      x = 1; // accessing x from A is OK
      y = 1; // accessing y from A is OK
    }
  }

  private static class C {
    void bar() {
      x = 1; // accessing x from A is not OK since C is static
      y = 1; // accessing y is OK
    }
  }
}

Recall that we recommend that all access to instance fields be done through the this reference. In the example above, however, we can't access this.x from within B.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class A {
 private int x;

 private class B {
   void foo() {
     this.x = 1; // error
     x = 1; // ok
   }
 }
}

Since this.x is called within a method of B, this would refer to the instance of B, rather than the instance of A. Java has a piece of syntax called qualified this to resolve this. A qualified this reference is prefixed with the enclosing class name, to differentiate between the this of the inner class and the this of the enclosing class. In the example above, we can access x from A through the A.this reference.

1
2
3
4
5
6
7
8
9
class A {
  private int x;

  private class B {
    void foo() {
      A.this.x = 1; // ok
    }
  }
}

Hiding Nested Classes

When we use nested classes for encapsulating the implementation details of the enclosing class, it is better to declare them as private nested classes, so that they are not exposed across the abstract barrier, adhering to the information hiding principle. For example, consider the following

1
2
3
4
5
6
7
8
class A {
  private class B {
    public void buz() { 
    }
  }
  private static class C {
  }
}

We cannot access A.B or A.C

1
2
A.B b; // compilation error
A.C c; // compilation error

Note that it is still possible to expose instances of private nested classes outside the enclosing class. For example, let's say we have:

1
2
3
4
5
6
7
8
9
class A {
  private class B {
    public void buz() { 
    }
  }
  B foo() {
    return new B();
  }
}

We are allowed to call A::foo and obtain a reference to an instance of B, as long as the type B is not used directly.

1
2
3
A a = new A();
a.foo();         // return an instance of A.B is OK
A.B b = a.foo(); // still not allowed since the type A.B is private

Since the type A.B is private to within A, we cannot call methods of B outside of A as well.

1
2
A a = new A();
a.foo().buz();  // error since `buz` is defined in a private nested class

When a nested class does not expose implementation details and is an essential part of the methods provided by the class, it is useful to expose the nested classes across the abstraction barrier. For example, Java's Map has a nested type Entry<K, V> that is public.

Consider the example below, where A has two public nested classes B and C.

1
2
3
4
5
6
7
8
class A {
  public class B {
    public void buz() { 
    }
  }
  public static class C {
  }
}

We can declare variables of type A.B and A.C

1
2
A.B b;  // ok
A.C c;  // ok

and initialize them like so

1
2
3
A a = new A();
A.B b = a.new B();
A.C c = new A.C();

Note that syntax to create an instance of B is a.new B(), not new a.B().

Local Class

We can also declare a class within a function, just like a local variable.

To motivate this, let's consider how one would use the java.util.Comparator interface.
The Comparator interface allows us to specify how to compare two elements, by implementing this interface with a customized compare() method. compare(o1,o2) should return 0 if the two elements are equal, a negative integer if o1 is "less than" o2, and a positive integer otherwise.

Suppose we have a list of strings, and we want to sort them in the order of their length, we can write the following method:

sortNames v1 with Local Class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void sortNames(List<String> names) {

  class NameComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
      return s1.length() - s2.length();
    }
  }

  names.sort(new NameComparator());
}

This makes the code easier to read since we keep the definition of the class and its usage closer together.

Classes like NameComparator that are declared inside a method (or to be more precise, inside a block of code between { and }) is called a local class. Just like a local variable, a local class is scoped within the method. Like a nested class, a local class has access to the variables of the enclosing class through the qualified this reference. Further, it can access the local variables of the enclosing method.

For example,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class A {
  int x = 1;

  void f() {
    int y = 1;

    class B {
      void g() {
        x = y; // accessing x and y is OK.
      }
    }

    new B().g();
  }
}

Here, B is a local class defined in method f(). It has access to all the local variables accessible from within f, as well as the fields of its enclosing class.

Variable Capture

Recall that when a method returns, all local variables of the methods are removed from the stack. But, an instance of that local class might still exist. Consider the following example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
interface C {
  void g();
}

class A {
  int x = 1;

  C f() {
    int y = 1;

    class B implements C {
      void g() {
        x = y; // accessing x and y is OK.
      }
    }

    B b = new B();
    return b;
  }
}

Calling

1
2
3
A a = new A();
C b = a.f();
b.g();

will give us a reference to an object of type B now. But, if we call b.g(), what is the value of y?

For this reason, although a local class can access local variables in the enclosing method, the local class makes a copy of local variables inside itself. We say that a local class captures the local variables.
Capturing local variables or enclosing instances may extend their lifetime beyond the execution of the method.

Effectively final

Variable captures can be confusing. Consider the following code:

sortNames v2 with Sorting Order (Won't Compile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void sortNames(List<String> names) {
  boolean ascendingOrder = true;
  class NameComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
      if (ascendingOrder)
        return s1.length() - s2.length();
      else
        return s2.length() - s1.length();
    }
  }

  ascendingOrder = false;
  names.sort(new NameComparator());
}

Will sort sorts in ascending order (i.e., use the value of ascendingOrder when the class is declared) or descending order (i.e., use the value of ascendingOrder when the class is instantiated)?

To avoid such confusion, Java only allows a local class to access variables that are explicitly declared final or implicitly final (or effectively final). An effectively final variable is a variable that is assigned only once, even if the final keyword is not used. Therefore, Java saves us from such a hair-pulling situation and disallows such code — ascendingOrder is not effectively final so the code above does not compile.

Breaking the Limitation of Effectively final.    The limitation of effectively final only happens because the value is of a primitive type. So, if we capture the value and forbid re-assigning the value, there is nothing we can do to change the primitive value.

On the other hand, reference types can be mutated. So if we use our own implementation of Bool class below instead of boolean primitive type, we can modify the code above to allow the "value" in variable ascendingOrder to be changed. However, this change is via mutation and not re-assignment to the variable.

sortNames v3 with Mutable Wrapper
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void sortNames(List<String> names) {
  Bool ascendingOrder = new Bool(true);
  class NameComparator implements Comparator<String> {
    public int compare(String s1, String s2) {
      if (ascendingOrder.val)
        return s1.length() - s2.length();
      else
        return s2.length() - s1.length();
    }
  }

  ascendingOrder.val = false;
  names.sort(new NameComparator());
}

class Bool {
  boolean val;
}

The code above compiles correctly now. But, as a result, the behavior of the code is not as intuitive as before. So please exercise this workaround with extreme caution.

Variable Capture in JavaScript

Those of you who did CS1101S or are otherwise familiar with JavaScript might want to note that this is different from JavaScript, which does not enforce the final/effectively final restriction in variable captures. This is because there is no concept of primitive value in JavaScript.

Every single primitive type is automatically boxed in JavaScript. The unboxed variant is not available to the programmer directly. So, if we write x = 1 in JavaScript, the value 1 is boxed and put into the heap. Then, the variable x in the stack points to this box in the heap, unlike Java primitive type.

Anonymous Class

An anonymous class is one where you declare a local class and instantiate it in a single statement. This is syntactic sugar to allow programmers to declare quickly a "single-use" class -- a class that we use only once and never need again. We don't even need to give the class a name (hence, anonymous).

1
2
3
4
5
names.sort(new Comparator<String>() {
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
 }
});

The example above removes the need to declare a class just to compare two strings.

An anonymous class has the following format: new X (arguments) { body }, where:

  • X is a class that the anonymous class extends or an interface that the anonymous class implements. X cannot be empty. This syntax also implies an anonymous class cannot extend another class and implement an interface at the same time. Furthermore, an anonymous class cannot implement more than one interface. Put simply, you cannot have extends and implements keywords in between X and (arguments).
  • arguments are the arguments that you want to pass into the constructor of the anonymous class. If the anonymous class is extending an interface, then there is no constructor, but we still need ().
  • body is the body of the class as per normal, except that we cannot have a constructor for an anonymous class.

The syntax might look overwhelming at first, but we can also write it as:

1
2
3
4
5
6
Comparator<String> cmp = new Comparator<String>() {
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
  }
};
names.sort(cmp);

Line 1 above looks just like what we do when we instantiate a class, except that we are instantiating an interface with a { .. } body.

You can think of anonymous class as a syntactic sugar for the following class.

1
2
3
4
5
class MyComparator implements Comparator<String> {
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
  }
}

Then, the code above can also be rewritten to be the following.

1
2
Comparator<String> cmp = new MyComparator();
names.sort(cmp);

Like a local class, an anonymous class captures the variables of the enclosing scope as well — the same rules to variable access as local classes apply.

Anonymous classes were commonly used before Java 8 for single-use behavior. In many cases, lambda expressions (which you will encounter in the next unit) now provide a clearer alternative, but anonymous classes are still required when state or multiple methods are involved.


  1. Some recent versions of Java optimize away the implicit reference to the enclosing instance if it is not used in the inner class. However, for the purpose of this course, we will always assume the implicit reference always exists.