Java Tutorial » Chapter 22 — Inner Classes

Chapter 22 — Java Inner Classes

Mastering the art of nesting classes for better encapsulation and organization.

1. Introduction

What are Inner Classes?

In Java, an inner class is a class defined within another class. The main reason for using inner classes is that they provide a way to logically group classes that are only used in one place, increasing encapsulation and making code more readable and maintainable.

There are several types of inner classes, each with its own specific properties and use cases:

  • Non-static Nested Classes (Inner Classes): Associated with an instance of the outer class.
  • Static Nested Classes: Associated with the outer class itself, not an instance.
  • Method-local Inner Classes: Defined within a method's scope.
  • Anonymous Inner Classes: Unnamed classes declared and instantiated at the same time.
2. Nested Classes

The General Concept

A nested class is simply a class whose definition is inside another class. The outer class is often called the top-level class, and the class inside is the nested class.

public class OuterClass {
    // This is the top-level class
    
    // This is a nested class
    public class NestedClass {
        // Members of the nested class
    }
}

The key distinction is between static and non-static nested classes, which we will explore next.

3. Inner Classes (Non-static)

Associated with an Outer Instance

An inner class (or non-static nested class) is a class defined within another class without the static keyword. An instance of an inner class can only exist with an instance of its outer class.

Key features:

  • The inner class has access to all members (even private) of the outer class.
  • It cannot define any static members itself.
  • To instantiate an inner class from outside, you must first create an instance of the outer class.
public class Outer {
    private String outerMessage = "I am the outer class.";

    // Inner class definition
    public class Inner {
        public void display() {
            // The inner class can access the outer's private member
            System.out.println(outerMessage);
            System.out.println("I am the inner class.");
        }
    }

    public void createInnerAndDisplay() {
        // Must create an Outer instance to create an Inner instance
        Outer outer = new Outer();
        Inner inner = outer.new Inner();
        inner.display();
    }
    
    public static void main(String[] args) {
        Outer outerObj = new Outer();
        outerObj.createInnerAndDisplay();
    }
}
4. Accessing the Private Members

A Powerful Feature of Inner Classes

One of the most powerful features of inner classes is their ability to access the private members of the outer class. This breaks the normal encapsulation rules and is a primary reason for using them.

public class DataHolder {
        private int secretValue = 100;

        public void printSecret() {
            System.out.println("Secret value is: " + secretValue);
        }

        // Inner class can access and modify the private member
        public class ValueModifier {
            public void modifySecret(int newValue) {
                secretValue = newValue; // Direct access to private member
                System.out.println("Secret value changed to: " + secretValue);
            }
        }

        public void modifyValueViaInner() {
            ValueModifier modifier = new ValueModifier();
            modifier.modifySecret(999);
        }
    }

public class PrivateAccessExample {
    public static void main(String[] args) {
        DataHolder holder = new DataHolder();
        holder.printSecret(); // Initial value is 100
        
        holder.modifyValueViaInner();
        holder.printSecret(); // Value is now 999
    }
}
5. Method-local Inner Classes

Classes Defined Inside Methods

You can define a class inside a method. Such a class is known as a method-local inner class. It is not part of the outer class; it's local to the method block where it's defined.

Rules for method-local inner classes:

  • They can access the method's local variables and parameters, but only if they are final or effectively final.
  • They cannot access non-final local variables of the enclosing method.
  • They cannot be marked public, private, static, or synchronized.
interface Greeter {
    void greet();
}

public class MethodLocalInner {
    public void createGreeter(String message) {
        // 'message' is effectively final and can be accessed
        final String finalMessage = message;
        
        // Method-local inner class
        class LocalGreeter implements Greeter {
            @Override
            public void greet() {
                System.out.println(finalMessage);
            }
        }
        
        Greeter greeter = new LocalGreeter();
        greeter.greet();
    }
    
    public static void main(String[] args) {
        MethodLocalInner example = new MethodLocalInner();
        example.createGreeter("Hello from a method-local inner class!");
    }
}
6. Anonymous Inner Classes

Classes Without a Name

An anonymous inner class is a local class without a name. It enables you to declare and instantiate a class at the same time. They are essentially expressions that let you say, "I want an object that does X right here."

They are often used for implementing interfaces or abstract classes with a one-time implementation.

interface HelloWorld {
    void sayHello();
}

public class AnonymousExample {
    public void greet() {
        // Creating an anonymous class that implements the HelloWorld interface
        HelloWorld englishGreeter = new HelloWorld() {
            @Override
            public void sayHello() {
                System.out.println("Hello, World!");
            }
        };
        
        englishGreeter.sayHello();

        // Another anonymous class, this time implementing Runnable
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous thread is running.");
            }
        });
        
        thread.start();
    }
}
7. Anonymous Class as Argument

Passing Implementations on the Fly

You can pass an anonymous class directly to a method that expects an object of an interface or abstract class. This is a very common pattern in GUI programming and event handling.

interface GreetingService {
    void performGreeting(String name);
}

public class AnonymousArgument {
    public void greetUser(String name, GreetingService service) {
        // The method receives an anonymous implementation of GreetingService
        service.performGreeting(name);
    }

    public static void main(String[] args) {
        AnonymousArgument example = new AnonymousArgument();
        
        // Passing an anonymous class as an argument
        example.greetUser("Alice", new GreetingService() {
            @Override
            public void performGreeting(String name) {
                System.out.println("Greetings, " + name + "! Have a great day.");
            }
        });
    }
}
8. Static Nested Classes

Associated with the Class, Not an Instance

A static nested class is a nested class declared with the static keyword. It is associated with its outer class, not with an instance of the outer class.

Key properties:

  • It cannot directly access non-static members (instance variables or methods) of the outer class.
  • It can access the static members of the outer class.
  • To create an instance, you don't need an instance of the outer class.
public class Outer {
    private static int staticValue = 50;
    private int instanceValue = 100;

    // Static nested class
    public static class StaticNested {
        public void displayStatic() {
            // Can access the outer class's static member
            System.out.println("Static value from outer: " + staticValue);
            
            // Cannot access the outer class's instance member
            // System.out.println("Instance value from outer: " + instanceValue); // COMPILE ERROR
        }
    }
    
    public void displayInstance() {
        System.out.println("Instance value from outer: " + instanceValue);
    }

public class StaticNestedExample {
    public static void main(String[] args) {
        // Instantiate the static nested class directly
        Outer.StaticNested nested = new Outer.StaticNested();
        nested.displayStatic();
        
        // To access instance members, you need an Outer instance
        Outer outer = new Outer();
        outer.displayInstance();
    }
}
9. Practice & Challenge

Test Your Skills

  1. Create an outer class with a private field and an inner class that modifies it.
  2. Write a method that returns an instance of a method-local inner class.
  3. Implement an interface using an anonymous inner class and pass it to another method.
  4. Create a class with both a static and a non-static nested class to demonstrate the difference in access.

🏆 Challenge: Custom Linked List

Create a simple generic linked list implementation. Instead of a separate Node class file, define the Node as a static nested class inside your LinkedList class. This is a common pattern in data structures where the node class is only relevant to the list itself.

public class CustomLinkedList {
    // The head of the list
    private Node head;
    
    // Static nested class for the list nodes
    private static class Node {
        E data;
        Node next;
        
        Node(E data) {
            this.data = data;
            this.next = null;
        }
    }
    
    // Method to add an element to the list
    public void add(E data) {
        Node newNode = new Node(data);
        if (head == null) {
            head = newNode;
        } else {
            Node current = head;
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;
        }
    }
    
    // Method to print the list
    public void display() {
        Node current = head;
        while (current != null) {
            System.out.print(current.data + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        CustomLinkedList list = new CustomLinkedList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");
        list.display();
    }
}