Nested Classes in Java
Published on May 7, 2024 • 12 min read

In Java, a class can contain other types within its body, such as other classes, interfaces, enums, and records. These are referred to as nested types or nested classes.
When to Use Nested Classes?
Nested classes are useful when your classes are tightly coupled, meaning their functionality is interwoven. They can help encapsulate related functionality and improve code organization.
Important Restrictions for nested classes were removed in JDK16: Before JDK16, only static nested classes were allowed to have static methods. As of JDK16, all four types of nested classes can have static members of any type, including static methods.
Nested Classes
Nested classes are classes defined within another class. They can be of various types:
- Static Nested Classes
- Local Classes
- Inner Classes
- Anonymous Classes
1. Static Nested Classes
Features:
- Static nested classes are classes declared within another class and marked as static.
- They require the outer class name when accessed externally.
- Static nested classes can access private attributes of the outer class, and vice versa.
- static nested classes are declared within the body of the outer class.
- They are useful for encapsulating related functionality within a class.
- Static nested classes offer access to private members of the outer class without requiring explicit getters.
- static nested class can be generic and accept any type compatible with its declaration.
Usage:
- Nested classes aid in organizing and encapsulating code, enhancing readability and maintainability.
- Utilizing static nested classes can improve code modularity and reduce external dependencies.
- They offer flexibility in sorting and comparing objects within the context of the outer class.
Code:
Before seeing the code, let's understand the problem statement first. We have one Employee
class with 3 private fields name
, id
, and salary
. We also want to have another class called EmployeeComparator
, which we can use to sort a list of employees.
Employee class:
package nested_classes_usage;
//Employee.java
public class Employee {
private String name;
private int id;
private int salary;
public Employee() {
}
public Employee(String name, int id, int salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", id=" + id +
", salary=" + salary +
'}';
}
}
EmployeeComparator class looks like:
package nested_classes_usage;
//EmployeeComparator.java
import java.util.Comparator;
public class EmployeeComparator<T extends Employee> implements Comparator<Employee> {
@Override
public int compare(Employee o1, Employee o2) {
return o1.getName().compareTo(o2.getName());
}
}
Main class:
package nested_classes_usage;
//Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Employee> empList = new ArrayList<>(List.of(
new Employee("John", 1003, 20000),
new Employee("Bob", 1001, 19500),
new Employee("Alice", 1008, 30000),
new Employee("Ozan", 1005, 50000),
new Employee("Lola", 1004, 25000)
));
var empComparator = new EmployeeComparator<>();
empList.sort(empComparator);
for (var emp : empList){
System.out.println(emp);
}
}
}
Running the main method will give us the following result: Employees are sorted based on their names.
Employee{name='Alice', id=1008, salary=30000}
Employee{name='Bob', id=1001, salary=19500}
Employee{name='John', id=1003, salary=20000}
Employee{name='Lola', id=1004, salary=25000}
Employee{name='Ozan', id=1005, salary=50000}
Now we want to sort the employees based on salary, so we can modify the EmployeeComparator class as follows (and we will get error, during compile time itself):
package nested_classes_usage;
//EmployeeComparator.java
import java.util.Comparator;
public class EmployeeComparator<T extends Employee> implements Comparator<Employee> {
@Override
public int compare(Employee o1, Employee o2) {
return o1.salary.compareTo(o2.salary); // Error: // 'salary' has private access in 'nested_classes_usage.Employee'
}
}
But notice we can't really do that. As salary
has a private access modifier, we cannot use it here directly (same for the id
field). So we have two options:
- To add another getter methods for
salary
andid
(This is not something we want to do every time) - Better solution would be to use a nested static class in this kind of situation (which we will discuss below)
using static nested class. Let's modiy our Employee
class below:
package nested_classes_usage;
import java.util.Comparator;
//Employee.java
public class Employee {
public static class EmployeeComparator<T extends Employee> implements Comparator<Employee> {
private String sortType;
public EmployeeComparator() {
this.sortType = "name";
}
public EmployeeComparator(String sortType) {
this.sortType = sortType;
}
@Override
public int compare(Employee o1, Employee o2) {
if (this.sortType == "salary"){
return Integer.valueOf(o1.salary).compareTo(Integer.valueOf(o2.salary));
}
return o1.getName().compareTo(o2.getName());
}
}
private String name;
private int id;
private int salary;
public Employee() {
}
public Employee(String name, int id, int salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", id=" + id +
", salary=" + salary +
'}';
}
}
we have added the following part of the code:
public static class EmployeeComparator<T extends Employee> implements Comparator<Employee> {
private String sortType;
public EmployeeComparator() {
this.sortType = "name";
}
public EmployeeComparator(String sortType) {
this.sortType = sortType;
}
@Override
public int compare(Employee o1, Employee o2) {
if (this.sortType == "salary"){
return Integer.valueOf(o1.salary).compareTo(Integer.valueOf(o2.salary));
}
return o1.getName().compareTo(o2.getName());
}
}
Here, we have added a static nested class named EmployeeComparator
within the Employee
class. This nested class implements the Comparator<Employee>
interface, allowing us to define custom comparison logic for sorting employees.
- The
EmployeeComparator
class has two constructors: one without parameters and another with a sortType parameter. The default constructor sets the sortType to "name". - The compare method is overridden to compare two Employee objects based on the specified sortType. If the sortType is "salary", it compares the salaries of the two employees. Otherwise, it compares their names using the getName method.
- This approach allows us to sort employees based on either their names or salaries, depending on the sortType specified when creating an instance of
EmployeeComparator
.
And, we have to change little bit in the main method also:
package nested_classes_usage;
//Main.java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Employee> empList = new ArrayList<>(List.of(
new Employee("John", 1003, 20000),
new Employee("Bob", 1001, 19500),
new Employee("Alice", 1008, 30000),
new Employee("Ozan", 1005, 50000),
new Employee("Lola", 1004, 25000)
));
var empNameComparator = new Employee.EmployeeComparator<>();
empList.sort(empNameComparator);
for (var emp : empList){
System.out.println(emp);
}
System.out.println("---");
var empSalaryComparator = new Employee.EmployeeComparator<>();
empList.sort(empSalaryComparator.reversed());
for (var emp : empList){
System.out.println(emp);
}
}
}
What is happening here?
- Initialized a list of
Employee
objects with sample data. - Created an instance of
EmployeeComparator
without specifying sorting criteria, defaulting to sorting by employee names. - Sorted the
empList
based on employee names using the custom comparison logic. - Iterated over the sorted
empList
and printed each employee's details. - Created another
EmployeeComparator
instance to sortempList
by salary in descending order usingreversed
. - Sorted
empList
again based on salary. - Iterated over the sorted
empList
once more and printed each employee's details.
Final output will look like:
Employee{name='Alice', id=1008, salary=30000}
Employee{name='Bob', id=1001, salary=19500}
Employee{name='John', id=1003, salary=20000}
Employee{name='Lola', id=1004, salary=25000}
Employee{name='Ozan', id=1005, salary=50000}
---
Employee{name='Ozan', id=1005, salary=50000}
Employee{name='Lola', id=1004, salary=25000}
Employee{name='John', id=1003, salary=20000}
Employee{name='Bob', id=1001, salary=19500}
Employee{name='Alice', id=1008, salary=30000}
2. Inner Classes
Features:
- Inner classes are non-static classes declared within an enclosing class.
- They can have any valid access modifier: public, private, protected, or package private.
- Inner classes have access to instance members, including private ones, of the enclosing class.
- As of JDK16, static members of all types are supported on inner classes
- Inner classes differ from static nested classes in their instantiation method.
- To instantiate an inner class, an instance of the enclosing class is required.
EnclosingClass outerClass = new EnclosingClass();
EnclsoingClass.Innerclass innerclass = outerClass.new InnerClass();
//Alternatively, in single line
EnclosingClass.Innerclass innerClass = new EnclsoingClass().new InnerClass();
- Inner classes are accessed using a special syntax on the outer class instance, followed by
.new
. - The syntax for instantiating an inner class may seem unusual but is essential for accessing inner classes.
Example on how to call static inner class and inner class
public class OuterClass {
private static String staticField = "Static Field in OuterClass";
public static class StaticInnerClass {
public void printStaticField() {
System.out.println(staticField);
}
}
public class InnerClass {
public void printStaticField() {
System.out.println(staticField);
}
}
public static void main(String[] args) {
// Calling static inner class
OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
staticInner.printStaticField();
// Calling inner class
OuterClass outerObj = new OuterClass();
OuterClass.InnerClass inner = outerObj.new InnerClass();
inner.printStaticField();
}
}
Explanation:
- The
OuterClass
contains both a static inner classStaticInnerClass
and a non-static inner classInnerClass
. - To call the static inner class, we directly reference it using the enclosing class name:
OuterClass.StaticInnerClass
. - To call the inner class, we first need to create an instance of the enclosing class (
OuterClass
), then use that instance to create an instance of the inner class:outerObj.new InnerClass()
.
3. Local Classes
Features:
- Local classes are inner classes declared directly within a code block, usually within a method body.
- They don't have access modifiers and are only accessible within the method in which they are declared.
- Like inner classes, they have access to all fields and methods of the enclosing class.
- They can also access local variables and method arguments that are final or effectively final.
- Local classes can be used to perform specialized operations or create computed attributes for specific purposes.
- A local class can extend a class and implement interfaces, just like any other class.
- Local variables used in a local class must be final or effectively final.
- JDK 16 introduced support for creating local record, interface, and enum types within a method block.
Example:
Suppose we have a method performOperation that performs various mathematical operations on two operands. Instead of implementing each operation separately in the method body, we can define local classes for each operation within the method. This approach encapsulates the logic for calculating the outcomes of different operations.
package nested_classes_usage;
public class MathOperation {
public static void main(String[] args) {
performOperation(5, 3); // Passing values to perform addition
performOperation(10, 7); // Passing values to perform subtraction
}
public static void performOperation(double operand1, double operand2) {
// Local class for addition operation
class Addition {
public double calculate() {
return operand1 + operand2;
}
}
// Local class for subtraction operation
class Subtraction {
public double calculate() {
return operand1 - operand2;
}
}
// Local class for multiplication operation
class Multiplication {
public double calculate() {
return operand1 * operand2;
}
}
// Local class for division operation
class Division {
public double calculate() {
if (operand2 != 0) {
return operand1 / operand2;
} else {
System.out.println("Error: Division by zero.");
return Double.NaN; // Not a Number
}
}
}
// Perform addition
Addition addition = new Addition();
System.out.println("Addition result: " + addition.calculate());
// Perform subtraction
Subtraction subtraction = new Subtraction();
System.out.println("Subtraction result: " + subtraction.calculate());
// Perform multiplication
Multiplication multiplication = new Multiplication();
System.out.println("Multiplication result: " + multiplication.calculate());
// Perform division
Division division = new Division();
System.out.println("Division result: " + division.calculate());
}
}
In this example, the performOperation
method defines four local classes (Addition
, Subtraction
, Multiplication
, and Division
), each encapsulating the logic for a specific mathematical operation. The method instantiates objects of these local classes and calls their calculate method to perform the corresponding operation on the provided operands. This approach keeps the code organized and modular, making it easier to maintain and extend.
Usage:
- Local classes are useful when you need to create a class for a specific purpose within a method and don't want it to be accessible outside of that method.
- They can be handy for creating derived fields or performing specialized operations on data that is only relevant within that method scope.
4. Anonymous Classes
Features:
- An anonymous class is a local class that doesn't have a name.
- Unlike nested classes created with class declarations, anonymous classes are instantiated as part of an expression.
- Since the introduction of Lambda Expressions in JDK 8, the use of anonymous classes has decreased.
- However, there are still situations where anonymous classes might be preferable, and they are commonly found in older codebases. Understanding anonymous classes can lead to a better understanding of lambda expressions.
- Anonymous classes are instantiated and assigned in a single statement using the new keyword followed by either a superclass or an interface.
- They are useful for creating on-the-fly custom functionality and passing it as a method argument.
- While lambda expressions have largely replaced anonymous classes in modern Java development, understanding anonymous classes remains important for dealing with legacy code and gaining insights into lambda expressions.
Example:
package nested_classes_usage;
import java.util.Arrays;
import java.util.Comparator;
public class AnonymousClass {
public static void main(String[] args) {
String[] names = {"John", "Alice", "Bob", "David", "Eva"};
// Sorting the array using an anonymous class comparator
Comparator<String> anonymousClassComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s2.compareTo(s1); // Reverse order
}
};
Arrays.sort(names, anonymousClassComparator);
System.out.println("Sorted using anonymous class comparator:");
System.out.println(Arrays.toString(names)); //[John, Eva, David, Bob, Alice]
}
}
In this example, the Comparator
interface is implemented anonymously within the Arrays.sort()
method call. The compare()
method is overridden to sort the array of strings in reverse order.