Unlocking Java’s Potential: A Comprehensive Exploration of Inner and Nested Classes for Superior Code Structure

Table of Contents

In the world of Java programming, the concept of classes is central to the object-oriented paradigm. But did you know that classes can be nested within other classes? This unique feature is known as inner classes, and it opens up a whole new realm of possibilities in terms of code organization, encapsulation, and design patterns. In this blog post, we’ll delve into the fascinating world of inner classes, exploring their types, use cases, and benefits.

Introduction to Inner Classes

Sometimes, we can put a class inside another class. These are called “inner classes.” They were introduced in Java version 1.1 to fix problems with how events are handled in graphical interfaces. But because inner classes have useful features, programmers began using them in regular coding too.

We use inner classes when one type of object can’t exist without another type. For example, a university has departments. If there’s no university, there are no departments. So, we put the department class inside the university class.

Java
class University {  // Outer class
   class Department {  // Inner class
   }
}

Similarly, a car needs an engine to exist. Since an engine can’t exist on its own without a car, we put the engine class inside the car class.

Java
class Car {  // Outer class
  class Engine {  // Inner class
  }
}

Also, think of a map that has pairs of keys and values. Each pair is called an entry. Since entries depend on maps, we define an entry interface inside the map interface.

Java
interface Map {  // Outer interface
  interface Entry {  // Inner interface
  }
}

Remember, inner classes can’t exist without the outer class. This relationship is not “is-a,” but more like “has-a” (composition or aggregation).

Types of Inner Classes

There are four types of inner classes based on where they’re declared and their behavior:

Normal or Regular Inner Classes:

  • These are named classes declared inside another class without the static modifier.
  • They can access both static and instance members of the outer class.
  • Example:
Java
class Outer {
  class Inner {
  }
}

Method Local Inner Classes:

  • These classes are defined within a method.
  • They have access only to final variables of the method.
  • They are used to implement a specific functionality within a method.
  • Example:
Java
class Outer {
  void someMethod() {
    class Inner {
    }
  }
}

Anonymous Inner Classes:

  • These classes are defined without a name.
  • They are often used for implementing interfaces or extending classes on-the-fly.
  • Example:
Java
class Outer {
  interface MyInterface {
    void doSomething();
  }

  MyInterface obj = new MyInterface() {
    public void doSomething() {
    }
  };
}

Static Nested Classes:

  • These are declared as static inside another class.
  • They can access only static members of the outer class.
  • Example:
Java
class Outer {
  static class Nested {
  }
}

Remember:

  • Normal inner classes can access both static and instance members of the outer class.
  • Method local inner classes are declared inside methods and can only access final variables.
  • Anonymous inner classes are often used for implementing interfaces or extending classes without creating separate files.
  • Static nested classes are like regular classes but are defined within another class and can access only static members of the outer class.

When working with inner classes

Normal or Regular Inner Classes:

  • These are named classes declared within another class without the static keyword.
  • Compiling the below example generates two .class files: Outer.class and Outer$Inner.class.
  • Example:
Java
class Outer {
  class Inner {
  }
}

Running Inner Classes:

  • You can’t directly run an inner class from the command prompt unless it has a main method.
  • Attempting to run java Outer or java Outer$Inner without a main method leads to “NoSuchMethodError:main”.

Main Method Inside Outer Class:

  • By adding a main method in the outer class, you can run it.
  • Now, if we run below code java Outer will produce “Outer class main method”.
  • Example:
Java
class Outer {
  class Inner {
  }
  public static void main(String[] args) {
    System.out.println("Outer class main method");
  }
}

Static Members in Inner Classes:

  • Inner classes can’t include static members, such as main methods.
  • Trying to place a main method inside an inner class results in a compile error: “Inner classes cannot have static declarations”.

In short, normal inner classes are named classes within another class, they can’t have static members, and their ability to be run directly depends on the presence of a main method.

Accessing Inner class code

Case 1: Accessing Inner Class Code from Static Area of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
  
  public static void main(String[] args) {
    Outer o = new Outer();
    Outer.Inner i = o.new Inner();
    i.m1();
    // Alternatively:
    // 1. Outer.Inner i = new Outer().new Inner();
    // 2. new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class contains an inner class named Inner.
  • Inside the main method, we create an instance of Outer called o.
  • We then create an instance of the inner class using o.new Inner(), and call the m1() method on it.
  • The two alternative ways to create the inner class instance are shown as comments.
  • Running this code will print “Inner class method” to the console.

Case 2: Accessing Inner Class Code from Instance Area of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
  
  public void m2() {
    Inner i = new Inner();
    i.m1();
  }
  
  public static void main(String[] args) {
    Outer o = new Outer();
    o.m2();
  }
}

In this code:

  • The Outer class has an inner class named Inner.
  • The m2() method in the Outer class creates an instance of the inner class (Inner i = new Inner();) and calls the m1() method on it.
  • The main method in the Outer class creates an instance of Outer called o, and then calls the m2() method on it using o.m2().
  • Running this code will also print “Inner class method” to the console, just like the previous example.

Case 3: Accessing Inner Class Code from Outside of Outer Class

Java
class Outer {
  class Inner {
    public void m1() {
      System.out.println("Inner class method");
    }
  }
}

class Test {
  public static void main(String[] args) {
    Outer o = new Outer();
    Outer.Inner i = o.new Inner();
    i.m1();
  }
}

In this code:

  • The Outer class contains an inner class named Inner.
  • The Test class is separate from the Outer class and has its own main method.
  • Inside the main method of the Test class, we create an instance of Outer called o.
  • We then create an instance of the inner class using o.new Inner(), and call the m1() method on it.
  • Running the Test class will also print “Inner class method” to the console.

Case 4: Accessing Inner Class Code

a) From Static Area of Outer Class or Outside of Outer Class:
  • Create an instance of the outer class.
  • Use that instance to create an instance of the inner class.
  • Call methods on the inner class instance.
Java
Outer o = new Outer();
Outer.Inner i = o.new Inner();
i.m1();
b) From Instance Area of Outer Class
  • Directly create an instance of the inner class.
  • Call methods on the inner class instance.
Java
Inner i = new Inner();
i.m1();

Remember, the approach you choose depends on where you are accessing the inner class from and the context in which you want to use it.


Normal inner class / Regular inner class

1. In a normal or regular inner class, you can access both static and non-static members of the outer class directly. This makes it convenient to use and interact with the outer class’s members from within the inner class.

Java
class Outer {
  int x = 10;
  static int y = 20;
  
  class Inner {
    public void m1() {
      System.out.println(x);  // Accessing non-static member of outer class
      System.out.println(y);  // Accessing static member of outer class
    }
  }

  public static void main(String[] args) {
    new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class has an instance variable x and a static variable y.
  • The Inner class within Outer can directly access both x and y from the outer class.
  • Inside the m1() method of Inner, the non-static member x and the static member y are both printed.
  • When you run the main method, the output will be:
Java
10
20

This demonstrates how a normal inner class can freely access both static and non-static members of its enclosing outer class.

2. In an inner class, the keyword this refers to the current instance of the inner class itself. If you want to refer to the instance of the outer class, you can use the syntax OuterClassName.this. This is particularly useful when there might be naming conflicts or when you explicitly want to access the outer class’s instance.

Java
class Outer {
  int x = 10;

  class Inner {
    int x = 100;

    public void m1() {
      int x = 1000;
      System.out.println(x);           // 1000
      System.out.println(this.x);      // 100 (Inner class's x)
      System.out.println(Outer.this.x); // 10 (Outer class's x)
    }
  }

  public static void main(String[] args) {
    new Outer().new Inner().m1();
  }
}

In this code:

  • The Outer class contains an instance variable x with a value of 10.
  • Inside the Outer class, there’s an Inner class with its own instance variable x set to 100.
  • The m1() method inside the Inner class has a local variable x set to 1000.
  • Printing x will show the value of the local variable (1000).
  • Printing this.x inside the Inner class refers to the x within the Inner class (100).
  • Printing Outer.this.x refers to the x within the Outer class (10).
  • When you run the main method, the output will be:
Java
1000
100
10

This code demonstrates the different levels of scope and how you can use this and OuterClassName.this to access variables from various contexts within an inner class.

Applicable access modifiers for both outer and inner classes in Java

For outer classes:

  • The access modifiers that can be applied are public, default (no modifier), final, abstract, and strictfp.

For inner classes:

  • The access modifiers that can be applied are: private, protected, and static.

Nesting of inner classes

Nesting of inner classes is possible, which means you can define one inner class inside another inner class. This creates a hierarchical structure of classes within classes. This is also known as nested inner classes.

Here’s an example to illustrate this concept:

Java
class A {
  class B {
    class C {
      public void m1() {
        System.out.println("Innermost class method");
      }
    }
  }
}

class Test {
  public static void main(String[] args) {
    A a = new A();
    A.B b = a.new B();
    A.B.C c = b.new C();
    c.m1();
  }
}

In this example:

  • Class A has an inner class B, which itself has an inner class C.
  • The m1() method in class C prints “Innermost class method”.
  • In the main method of the Test class, you create instances step by step: A -> A.B -> A.B.C.
  • Then you call the m1() method on the instance of class C.
  • Running this code will print “Innermost class method” to the console.

The above code effectively demonstrates the concept of nested inner classes and how to work with them.

Let’s see one more example:

Java
class Outer {
  int outerVar = 10;

  class Inner {
    int innerVar = 20;

    class NestedInner {
      int nestedVar = 30;

      public void display() {
        System.out.println("NestedVar: " + nestedVar);
        System.out.println("InnerVar: " + innerVar);
        System.out.println("OuterVar: " + outerVar);
      }
    }
  }

  public static void main(String[] args) {
    Outer outer = new Outer();
    Outer.Inner inner = outer.new Inner();
    Outer.Inner.NestedInner nestedInner = inner.new NestedInner();

    nestedInner.display();
  }
}

In this example, we have an Outer class with an Inner class inside it, and within the Inner class, there’s a NestedInner class. You can create instances of each class and access their members accordingly.

When you run the code, it will display:

Java
NestedVar: 30
InnerVar: 20
OuterVar: 10

This shows that nesting of inner classes allows you to organize your code in a structured manner and access members at different levels of nesting.

Method Local Inner Classes

Method local inner classes are inner classes that are defined within a method’s scope. They are only accessible within that specific method and provide a way to encapsulate functionality that is needed only within that method. This type of inner class is particularly useful when you want to confine a class’s scope to a specific method, keeping the code organized and localized.

Main Purpose of Method Local Inner Classes:

  • Method local inner classes are intended to define functionality that is specific to a particular method.
  • They encapsulate code that is required repeatedly within that method.
  • Method local inner classes are well-suited for addressing nested, localized requirements within a method’s scope.
  • Method local inner classes can only be accessed within the method where they are defined.
  • They have a limited scope and aren’t accessible outside of that method.
  • Method local inner classes are the least commonly used type of inner classes.
  • They are employed when specific circumstances demand a highly localized class definition.

Example:

Java
class Test {
  public void m1() {
    class Inner {
      public void sum(int x, int y) {
        System.out.println("The Sum: " + (x + y));
      }
    }

    Inner i = new Inner();
    i.sum(10, 20);
    // More code...
    i.sum(100, 200);
    // More code...
    i.sum(1000, 2000);
  }

  public static void main(String[] args) {
    Test t = new Test();
    t.m1();
  }
}

In this example:

  • The Test class has a method m1() that contains a method local inner class named Inner.
  • The Inner class has a method sum() that calculates and prints the sum of two numbers.
  • Within m1(), you create an instance of the Inner class and call its sum() method multiple times.
  • Running the code produces the following output:
Java
The Sum: 30
The Sum: 300
The Sum: 3000

The above code effectively demonstrates how method local inner classes can be used to encapsulate functionality within a specific method’s scope.

We can declare a method-local inner class inside both instance and static methods.

  • If we declare an inner class inside an instance method, we can access both static and non-static members of the outer class directly from that method-local inner class.
  • On the other hand, If we declare an inner class inside a static method, we can access only static members of the outer class directly from that method-local inner class.

Example

Java
class Test {
  int x = 10;
  static int y = 20;

  public void m1() {
    class Inner {
      public void m2() {
        System.out.println(x); // Accessing instance member of outer class
        System.out.println(y); // Accessing static member of outer class
      }
    }

    Inner i = new Inner();
    i.m2();
  }

  public static void main(String[] args) {
    Test t = new Test();
    t.m1();
  }
}

Now, when we run this code, the output will be:

Java
10
20

This demonstrates that method local inner classes can access both instance and static members of the outer class within the context of an instance method.

Now, If we declare the m1() method as static, you will indeed get a compilation error at line 1 where you’re trying to access the non-static variable x from a static context. Here’s how the code would look with the error:

Java
class Test {
  int x = 10;
  static int y = 20;

  public static void m1() {
    class Inner {
      public void m2() {
        System.out.println(x); // Compilation error: non-static variable x cannot be referenced from a static context
        System.out.println(y);
      }
    }

    Inner i = new Inner();
    i.m2();
  }

  public static void main(String[] args) {
    Test.m1();
  }
}

In this version of the code, since m1() is declared as static, it can’t access instance variables like x directly. The compilation error mentioned in a comment will occur at the line where you’re trying to access x from the method local inner class’s m2() method. The y variable, being static, can still be accessed without an issue.

We will now look at a Very Important Concept in Inner Classes.

From a method-local inner class, we can’t access local variables of the method in which we declare the inner class. However, if the local variable is declared as final, then we can access it.

Java
class Test {
  public void m1() {
    final int x = 10; // Declaring a final local variable 'x' with a value of 10
    class Inner {
      public void m2() {
        System.out.println(x); // Accessing the final local variable 'x' within the inner class
      }
    }
    Inner i = new Inner(); // Creating an instance of the inner class
    i.m2(); // Calling the method of the inner class to print the value of 'x'
  }
   
  public static void main(String[] args) {
    Test t = new Test(); // Creating an instance of the outer class
    t.m1(); // Calling the method of the outer class
  }
}

Explanation:

  1. In the m1() method of the Test class, a local variable x is declared and initialized with the value 10. The variable x is marked as final, indicating that its value cannot be changed after initialization.
  2. Inside the m1() method, an inner class named Inner is defined. This inner class contains a method m2().
  3. The m2() method of the Inner class prints the value of the final local variable x. Since x is declared as final, it can be accessed within the inner class.
  4. Back in the m1() method, an instance of the Inner class is created using Inner i = new Inner();.
  5. The m2() method of the inner class is called using the instance i, which prints the value of the final local variable x.
  6. In the main method, an instance of the Test class is created (Test t = new Test();).
  7. The m1() method of the outer class is called using the instance t, which triggers the creation of an instance of the inner class and the printing of the value of the final local variable x.

Output: When you run the code, the output will be

Java
10

This output confirms that the inner class is able to access the final local variable x.

Now Few Questions: Consider the following code

Java
class Test {
    int i = 10;
    static int j = 20;

    public void m1() {
        int k = 30;
        final int m = 40;

        class Inner {
            public void m2() {
                // Line 1
            }
        }
    }
}

a) At Line 1, which of the following variables can we access directly? i, j, k, m

Answer → We can access all variables except ‘k’ directly.

b) If we declare m1() as static, then at Line 1, which variables can we access directly? i, j, k, m

Answer –> We can access only ‘j’ and ‘m’.

c) If we declare m2() as static, then at Line 1, which variables can we access directly? i, j, k, m

Answer –> We will get a compilation error (CE) because we cannot declare static members inside inner classes.

Note → The only applicable modifiers for method-local inner classes are final, abstract, and strictfp. If we try to apply any other modifier, we will get a compilation error (CE).


Anonymous Inner Class

Sometimes, inner classes can be declared without a name. Such inner classes are called ‘anonymous inner classes.’ The main purpose of anonymous inner classes is for instant use, typically for one-time usage.

Anonymous Inner Classes:

  • Anonymous inner classes are inner classes declared without a name.
  • They are primarily used for instant (one-time) usage.
  • Anonymous inner classes can be categorized into three types based on their declaration and behavior.

Types of Anonymous Inner Classes

Based on their declaration and behavior, there are three types of anonymous inner classes:

1. Anonymous Inner Class that Extends a Class

  • An anonymous inner class can extend an existing class.
  • It provides an implementation for the methods of the superclass or overrides them.

2. Anonymous Inner Class that Implements an Interface

  • An anonymous inner class can implement an interface.
  • It provides implementations for the methods declared in the interface.

3. Anonymous Inner Class Defined Inside Arguments

  • An anonymous inner class can be defined as an argument to a method.
  • It’s often used for callbacks or event handling.

Anonymous Inner class that extends a class

Java
class PopCorn {
    public void taste() {
        System.out.println("salty");
    }
}

class Test {
    public static void main(String[] args) {
        PopCorn p = new PopCorn() {
            public void taste() {
                System.out.println("spicy");
            }
        };
        p.taste();    // spicy

        PopCorn p1 = new PopCorn();
        p1.taste();    // salty

        PopCorn p2  = new PopCorn() {
            public void taste() {
                System.out.println("sweet");
            }
        };
        p2.taste(); // sweet
        System.out.println(p.getClass().getName());  // Test$1
        System.out.println(p1.getClass().getName()); // PopCorn
        System.out.println(p2.getClass().getName()); // Test$2
    }
}

The generated .class files are:

  • PopCorn.class: Compiled class file for the PopCorn class.
  • Test.class: Compiled class file for the Test class.
  • Test$1.class: Compiled class file for the first anonymous inner class within the Test class.
  • Test$2.class: Compiled class file for the second anonymous inner class within the Test class.

This is a common naming convention used by Java to generate class files for inner classes.

Analysis:

1. PopCorn p = new PopCorn();

  • In this line, you are creating an instance of the PopCorn class using its constructor.

2. PopCorn p = new PopCorn() { }

  • In this case, you are declaring an anonymous inner class that extends PopCorn (anonymously).
  • You’re creating an object of the anonymous inner class using the PopCorn reference p.

3. PopCorn p = new PopCorn() { public void taste() { … } };

Java
PopCorn p = new PopCorn() {
    public void taste() {
        System.out.println("Spicy");
    }
};
  • You’re declaring an anonymous inner class that extends PopCorn.
  • You’re overriding the taste() method within this anonymous inner class.
  • You’re creating an object of this anonymous inner class using the PopCorn reference p.

Different approaches to working with threads

Normal Class Approach:

Java
// Example using a normal class that extends Thread
class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner Class Approach:

Java
// Example using an anonymous inner class extending Thread
class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        };
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner class that implements an Interface

Normal Class Approach:

Java
class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r); // where r is target runnable
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner Class Implementing an Interface:

Note -> Defining a thread by implementing a runnable interface

Java
// Example using an anonymous inner class implementing Runnable interface
class ThreadDemo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        };

        Thread t = new Thread(r);
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Anonymous Inner class that defines inside arguments

Java
// Example using an anonymous inner class inside arguments
class ThreadDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Child Thread");
                }
            }
        }).start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

All the above code examples effectively illustrate the different ways to work with threads using both normal classes and anonymous inner classes


Normal Java Class Vs Anonymous Inner Class

The differences between normal Java classes and anonymous inner classes when it comes to extending classes, implementing interfaces, and defining constructors.

Extending a Class:

  • A normal Java class can extend only one class at a time.
  • An anonymous inner class can also extend only one class at a time.

Implementing Interfaces:

  • A normal Java class can implement any number of interfaces simultaneously.
  • An anonymous inner class can implement only one interface at a time.

Combining Extension and Interface Implementation:

  • A normal Java class can extend a class and implement any number of interfaces simultaneously.
  • An anonymous inner class can either extend a class or implement an interface, but not both simultaneously.

Constructors:

  • A normal Java class can have multiple constructors.
  • Anonymous inner classes cannot have explicitly defined constructors, primarily because they don’t have a specific name. The name of the class and the constructor must match, which is not feasible for anonymous classes.

Note: If the requirement is standard and required several times, then we should go for a normal top-level class. If the requirement is temporary and required only once (for instant use), then we should go for an anonymous inner class.

Where exactly Anonymous inner classes are used?

We can use anonymous inner classes frequently in GUI-based applications to implement event handling.

Anonymous inner classes are often used in GUI-based applications to implement event handling. Event handling in GUI applications involves responding to user interactions such as button clicks, mouse movements, and keyboard inputs. Anonymous inner classes provide a concise way to define event listeners and handlers directly inline within the code, making the code more readable and reducing the need for separate classes for each event.

Java
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MyGUIFrame extends JFrame {
    private JButton b1, b2, b3;

    public MyGUIFrame() {
        // Initialize components
        b1 = new JButton("Button 1");
        b2 = new JButton("Button 2");
        b3 = new JButton("Button 3");

        // Add buttons to the frame
        add(b1);
        add(b2);
        add(b3);

        // Attach anonymous action listeners to buttons
        b1.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 1 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 1 clicked!");
            }
        });

        b2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 2 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 2 clicked!");
            }
        });

        b3.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                // Button 3 specific functionality
                JOptionPane.showMessageDialog(MyGUIFrame.this, "Button 3 clicked!");
            }
        });

        // Set layout and size
        setLayout(new FlowLayout());
        setSize(300, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new MyGUIFrame());
    }
}

In this example, an ActionListener is implemented as an anonymous inner class for each button (b1, b2, b3) to handle their click events. The JOptionPane is used to show a message dialog when each button is clicked. The SwingUtilities.invokeLater() is used to ensure the GUI is created on the Event Dispatch Thread.

Remember to import the necessary classes (JFrame, JButton, ActionEvent, ActionListener, JOptionPane, SwingUtilities, etc.) from the appropriate packages.


Static Nested Classes

Sometimes, we can declare an inner class with the static modifier. Such types of inner classes are called static nested classes.

In the case of a normal or regular inner class, without an existing outer class object, there is no chance of an existing inner class object. That is, the inner class object is strongly associated with the outer class object.

However, in the case of static nested classes, without an existing outer class object, there may be a chance of an existing nested class object. Hence, a static nested class object is not strongly associated with the outer class object.

Java
class Outer 
{
   static class Nested 
   {
     public void m1()
     {
       System.out.println("Static nested class method");
     }
   }

   public static void main(String[] args)
   {
     Nested n = new Nested();
     n.m1();
   }
}

If you want to create a nested class object from outside of the outer class, you can do so as follows:

Java
Outer.Nested n = new Outer.Nested();

Static Method Declaration

In normal or regular inner classes, we can’t declare static members. However, in static nested classes, we can declare static members, including the main method. This allows us to invoke a static nested class directly from the command prompt.

Java
class Test
{
  static class Nested
  {
    public static void main(String[] args)
    {
      System.out.println("Static nested class main method");
    }
  }
 
  public static void main(String[] args)
  {
    System.out.println("Outer class main method");
  }
}

Explanation:

  1. The main method of the outer class (Test) will be invoked when you execute java Test.
  2. The main method of the static nested class (Nested) will be invoked when you execute java Test$Nested because the nested class is essentially a separate class named Test$Nested.

Output:

  • Running java Test will output: Outer class main method
  • Running java Test$Nested will output: Static nested class main method

Accessing static and non-static members from outer classes

In normal or regular inner classes, we can directly access both static and non-static members of the outer class. However, in static nested classes, we can only directly access the static members of the outer class and cannot access non-static members.

Java
class Test
{
  int x = 10;
  static int y = 20;
  static class Nested 
  {
    public void m1()
    {
      // Compilation Error: non-static variable x cannot be referenced from a static context 
      // System.out.println(x);
      System.out.println(y); // valid
    }
  }
}

Explanation:

  • You cannot directly access the non-static variable x from the static method m1() in the static nested class because the static nested class and its methods are associated with the class itself, not an instance of the outer class.
  • However, you can access the static variable y since static members are associated with the class itself and can be accessed from both static and non-static contexts.

Differences between normal or regular inner class and static nested class

There are several significant differences between normal or regular inner classes and static nested classes. These differences revolve around aspects such as their association with the outer class, member accessibility, and more.

Normal or Regular Inner Class:

  1. Without an existing outer class object, there is no chance of an existing inner class object. In other words, the inner class object is strongly associated with the outer class object.
  2. In normal or regular inner classes, we can’t declare static members.
  3. Normal or regular inner classes cannot declare a main method, thus we cannot directly invoke the inner class from the command prompt.
  4. From normal or regular inner classes, we can directly access both static and non-static members of the outer class.

Static Nested Classes:

  1. Without an existing outer class object, there may be a chance of an existing static nested class object. However, the static nested class object is not strongly associated with the outer class object.
  2. In static nested classes, we can declare static members.
  3. In static nested classes, we can declare a main method, allowing us to invoke the nested class directly from the command prompt.
  4. From static nested classes, we can access only the static members of the outer class.

Various combinations of nested classes and interfaces

Case 1: Class Inside a Class

When there is no possibility of one type of object existing without another type of object, we can declare a class inside another class. For instance, consider a university that consists of several departments. Without the existence of a university, the concept of a department cannot exist. Therefore, it’s appropriate to declare the ‘Department’ class within the ‘University’ class.

Java
class University
{
   class Department
   {
    
   }
}

Case 2: Interface Inside a Class

When there is a need for multiple implementations of an interface within a class, and all these implementations are closely related to that particular class, defining an interface inside the class becomes advantageous. This approach helps encapsulate the interface implementations within the context of the class.

Java
class VehicleTypes
{
  interface Vehicle
  {
    int getNoOfWheels();
  }
 
  class Bus implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 6;
    }
  }

  class Auto implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 3;
    }
  }
 
  // Other classes and implementations can follow...
}

Case 3: Interface Inside an Interface

We can declare an interface inside another interface. For instance, consider a ‘Map’ which is a collection of key-value pairs. Each key-value pair is referred to as an ‘Entry.’ Since the existence of an ‘Entry’ object is reliant on the presence of a ‘Map’ object, it’s logical to define the ‘Entry’ interface inside the ‘Map’ interface. This approach helps encapsulate the relationship between the two interfaces.

Java
interface Map
{
   interface Entry
   {
     // Define methods and members for the Entry interface
     // ...
     // ...
     // ...
   }
}

Any interface declared inside another interface is always implicitly public and static, regardless of whether we explicitly declare them as such. This means that we can directly implement an inner interface without necessarily implementing the outer interface. Similarly, when implementing the outer interface, there’s no requirement to also implement the inner interface. In essence, outer and inner interfaces can be implemented independently.

Java
interface Outer 
{
  void m1();
  interface Inner 
  {
    void m2();
  }
}

class Test1 implements Outer 
{
  public void m1()
  {
    System.out.println("Outer interface method implementation");
  }
}

class Test2 implements Outer.Inner
{
  public void m2()
  {
    System.out.println("Inner interface method implementation");
  }
}

public class Test
{
  public static void main(String[] args)
  {
    Outer t1 = new Test1();
    t1.m1();

    Outer.Inner t2 = new Test2();
    t2.m2();
  }
}

Case 4: Class Inside an Interface

When a particular functionality of a class is closely associated with an interface, it is highly recommended to declare that class inside the interface. This approach helps maintain a strong relationship between the class and the interface, emphasizing the specialized functionality encapsulated within the interface.

Java
interface EmailService
{
  void sendMail(EmailDetails e);
 
  class EmailDetails
  {
    String to_list;
    String cc_list;
    String subject;
    String body;
  }
}

In the given example, the EmailDetails class is specifically required only for the EmailService interface and is not used elsewhere. Thus, it’s recommended to declare the EmailDetails class inside the EmailService interface. This approach ensures that the class is tightly associated with the interface it serves.

Furthermore, class declarations inside interfaces can also be used to provide default implementations for methods defined in the interface, contributing to the interface’s flexibility and usability.

Java
interface Vehicle
{
  int getNoOfWheels();
  
  class DefaultVehicle implements Vehicle
  {
    public int getNoOfWheels()
    {
      return 2;
    }
  }
}

class Bus implements Vehicle
{
  public int getNoOfWheels()
  {
    return 6;
  }
}

public class Test
{
  public static void main(String[] args)
  {
    Vehicle.DefaultVehicle d = new Vehicle.DefaultVehicle();
    System.out.println(d.getNoOfWheels());  // 2
   
    Bus b = new Bus();
    System.out.println(b.getNoOfWheels());  // 6
  }
}

In the above example, the DefaultVehicle class serves as the default implementation of the Vehicle interface, while the Bus class provides a customized implementation of the same interface.

It’s worth noting that a class declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such. As a result, it’s possible to create an instance of the inner class directly without needing an instance of the outer interface.

Conclusions

1. In Java, both classes and interfaces can be declared inside each other, allowing for a flexible and versatile approach to structuring and organizing code.

Declaring a class inside a class:

Java
class A
{
  class B
  {
  } 
}

Declaring an interface inside a class:

Java
class A
{
  interface B
  {
  }
}

Declaring an interface inside an interface:

Java
interface A
{
  interface B
  {
  }
}

Declaring a class inside an interface:

Java
interface A
{
  class B
  {
  }
}

2. The interface declared inside an interface is always implicitly public and static, regardless of whether we explicitly declare them as such.

Java
interface A {
    interface B {
        // You can add methods and other members here
    }
}

3. The class which is declared inside an interface is always public and static, whether we explicitly declare it as such or not.

Java
interface A {
    class B {
        // You can add fields, methods, and other members here
    }
}

4. The interface declared inside a class is always implicitly static, but it doesn’t need to be declared as public.

Java
class A {
    interface B {
        // You can add methods and other members here
    }
}

Conclusion

Inner classes are a powerful and versatile feature in Java, enabling you to create complex relationships and encapsulate functionality with elegance. Whether you’re organizing code, implementing event handling, or providing default implementations, inner classes offer a rich toolkit to tackle a variety of scenarios. By understanding the types of inner classes and their benefits, you can wield this feature to enhance code readability, maintainability, and design patterns.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!