A Complete Guide to Java Nested Classes


1. Introduction

In this article, we'll look at nested classes and how to use them in Java. As a bonus, I'll present a cheat sheet to ace your Java interviews and exams.

2. What are Nested Classes

In short, nested classes are classes created inside a top-level class. We can also add nested classes to other nested classes, although that is considered a code smell.

Java supports four types of nested classes: inner classes, static nested classes, local classes, and anonymous classes. Each one has its own set of rules of usage in code and applies to a different use case.

Nested classes are only accessible through its top-level class. Thus, a good software design suggests that nested classes should be closely related to their top-level class.

One good example of that relationship is between the Map interface and its static nested class Entry. The Map contains many entries of pairs of keys and values. Thus, each Entry is associated with the Map interface, and no other classes should directly access it.

2.1. Defining an Inner Class

An inner class is a non-static class defined in the same scope as the fields and methods of its top-level class. It looks exactly like a regular standalone class except that it is located inside another class. Let's see one example of an inner class:

 1public class TopLevelClass {
 3  private int topLevelField = 9;
 5  private class NestedClass {
 7	protected class NestedNestedClass {
 9      public static int nestedField;
11      public int doSomething() {
12          return nestedField * topLevelField;
13      }
14    }
15  }

Inner classes have the following properties:

  • They can access any field of the top-level class, including the private ones.
  • They can have any access modifier, like protected, default, public, or private.
  • Can extend any class or implement any interface.
  • It can be marked as final or abstract. An inner class can't be an interface.

Note: Define a static field or method in a nested class like in _nestedField_is only available at Java version after 16. Before that, the compiler throws an error in that case.

Note: Like mentioned before, defining a nested class for another nested class is typically considered a code smell. Try to avoid that as much as possible.

2.1.1. Instantiating an Object of an Inner Class

To call an instance of an inner class, we also need to instantiate its top-level class. Thus, in our example, to instantiate a NestedNestedClass, we also need two instantiate its top-level classes NestedClass and TopLevelClass. Let's check how to call the doSomething method:

1public static void main (String[] args) {
2    TopLevelClass.NestedClass.NestedNestedClass nested = new TopLevelClass().new NestedClass().new NestedNestedClass();
3    int result = nested.doSomething();
4    System.out.println(result);
5  }

The code provided is only to exemplify how to instantiate nested classes. Make sure to use inner classes properly, and avoid weird syntaxes like the one above.

2.2. Writing a static Nested Class

Nested classes differ from inner classes because they can be declared static. And like in static methods, we can't reference instance variables inside a static class.

 1public class TopLevelClass {
 3  public static class NestedClass {
 5      public static int nestedField = 9;
 7      public static int doSomething() {
 8          return nestedField * 9;
 9      }
10  }

To use the static method doSomething of NestedClass from a standalone top-level class, we call it directly, as follows:

1  public static void main(String[] args) {
2    System.out.println(TopLevelClass.NestedClass.doSomething()); // -> outputs 81
3  }

The static nested classes help reduce the number of constructor calls in the caller code since we access them directly. Use them when its methods don't use instance fields of its top-level class.

2.3. Creating a Local Class

We can define classes like local variables inside methods. The compiler creates a local class when we invoke the method, and its definition goes out of scope as soon the method finishes. For example, let's add a local class inside NestedClass from the previous code snippet:

 1public class TopLevelClass {
 3  public static class NestedClass {
 5    public static int doSomething() {
 7      class LocalClass {
 8          public static final int localField = 9;
 9      }
11      return LocalClass.localField * 9;
12    }
13  }

The LocalClass defined above is like a local variable inside the doSomething method. When that method finishes, the LocalClass definition is gone and can't be accessed outside. That means we can only create instances of LocalClass inside the doSomething method.

Here are the properties about local classes: They can access any field or method of the top-level class. They can only access final and effectively final local variables of the method they are defined. They should not specify any access modifier. Otherwise, the compiler throws an error. They can be declared final or abstract.

2.4. Constructing an Anonymous Class

Anonymous classes are local classes without a name that exists only inside the method. The nuance is that they need to implement an interface or extend a class. We can implement an interface or abstract class method using the new operator, instantiate the anonymous class, and override its super-class methods. Anonymous classes are useful in scenarios where we need an interface or abstract class implementation that only exists in the current scope.

Let's create a TopLevelClass which contains a interface called Calculator that defines one method doSomething:

 1public class TopLevelClass {
 3  public interface Calculator{
 4      public int doSomething();
 5  }
 7  public int calculate() {
 8      Calculator clazz = new Calculator(){
 9          @Override
10          public int doSomething(){
11              int a = 9;
12              return a * 9;
13          }
14      };
16      return clazz.doSomething();
17  }

The calculate method anonymously creates an implementation of the Calculator interface and overrides its doSomething method.

3. Thoughts and Opinions on Nested Classes

As previously mentioned, it is possible to write various nested class types. Java gives us four flavors of them. We can use each one of them in a specific scenario. However, I suggest making only a minor effort to learn two of them in modern Java applications. That's the case for local classes and anonymous classes. But, for exams and interviews, you'll probably need them.

Local Classes are rarely used in real-world applications because there are better practices than writing classes inside methods. You can achieve almost the same with a local variable or a private helper method with a more transparent code. We can use a private inner class if a local variable or method doesn't work. It's hard to find a good use case for a local class, so don't put much effort into profoundly understanding this one.

Java 8 introduced lambda expressions that implement the method of a Functional Interface using a particular syntax. Lambdas only work with functional interfaces that define a single abstract method and partially substitute anonymous classes. It is rare nowadays to find classes that define two abstract methods and don't have a standalone implementation.

In Java 8, interfaces with very different behaviors should be Functional Interfaces. See for example the Function, Supplier, Predicate, and Consumer interfaces. Each one of them can be implemented in a infinity different ways by the programmer. Making them a Functional Interface helps to create much lesser code and make the programmer's life much easier.

That left us with the Inner and Static Nested Classes for real-world applications. Those two are still very relevant today if you apply them correctly. Use them primarily as helper classes that doesn't have any utility outside its top-level class scope. If you need to access instance members of the top-level class, use a inner class. If you only want to access static members, use a static nested class.

3.1. Pros of Nested Classes

One benefit of nested classes is that the relationship where they are accessible only through their top-level class, which evicts code changes unrelated to them and makes the code more maintainable and secure to changes. That relationship also creates fewer dependencies between classes in our application.

3.2. Cons of Nested Classes

One drawback of nested classes is when we don't follow the Single Responsibility Principle. If the class becomes big, we might introduce unnecessary dependencies to the top-level class only to access the nested class and cause code readability problems. Think about nested classes as a helper class to only one class, and try to keep them as simple as possible.

4. Cheat Sheet for Interviews and Exams

Let's visualize the four types of nested classes and how they differ from each other in a table format:

  1. Regarding class modifiers allowed for each class:
Modifier Inner Static Nested Local Anonymous
abstract Yes Yes Yes No
final Yes Yes Yes No
private Yes Yes No No
public Yes Yes No No
protected Yes Yes No No
default Yes Yes No No
  1. Regarding access rules to other classes:
Inner Static Nested Local Anonymous
Can extend/implement any no. of classes/interfaces Yes Yes Yes No - Just one class or interface
Can access instance variables of top-level class Yes No Yes, for non-static methods Yes, for non-static methods
Can access variable of top-level method Not applicable Not applicable Yes, for constants Yes, for constants

5. Conclusion

In this post, we've seen in detail what nested classes are and their applications in Java. We also summarized what is typically asked in interviews and exam questions.