Java Inner Classes
Updated 10 min read Aimerscrew
A Java inner class is a class written inside another class. Until now, every class, record and enum in this chapter sat side by side at the top of a file. Sometimes, though, a class only makes sense as a part of another one, like a cursor that moves through one particular playlist. Java gives you four ways to nest classes. In this lesson, you'll learn static nested classes, inner classes, local classes and anonymous classes, and you'll get a simple rule for choosing between them.
🌍 Real-life example
Think of the parking spots in a multi-story garage. Spot 12 doesn't mean much on its own: which garage? A spot belongs to one garage and knows whether that garage is open. A library card is different: it's issued by the library system, but you can carry it around without needing a particular building. Java's inner classes and static nested classes mirror exactly this difference.
What is a nested class in Java?
A nested class is any class declared inside another class, which is called the outer class (or enclosing class). There are four kinds:
| Kind | Where it's declared | Tied to an outer object? |
|---|---|---|
| Static nested class | Inside a class, with static | No |
| Inner class | Inside a class, without static | Yes |
| Local class | Inside a method | Yes, if the method isn't static |
| Anonymous class | Inside an expression, with no name | Yes, if created in a non-static method |
Strictly speaking, Java calls the last three "inner classes" and the first one a "static nested class", but most people use "inner class" loosely for all of them.
Static nested classes
A static nested class is a normal class that simply lives inside another class's name, like a folder. It does not belong to any object of the outer class. You create it with new Outer.Nested(...):
public class LibraryDesk {
public static void main(String[] args) {
Library.Card card = new Library.Card("Ana Costa", 1042); // no Library object needed
System.out.println(card.describe());
System.out.println("Max loans: " + Library.Card.MAX_LOANS);
}
}
class Library {
private String branch = "Central";
static class Card { // static nested class
static final int MAX_LOANS = 5;
private String holder;
private int number;
Card(String holder, int number) {
this.holder = holder;
this.number = number;
}
String describe() {
return "Card #" + number + " belongs to " + holder;
// 'branch' is NOT available here: a Card has no Library object
}
}
String getBranch() {
return branch;
}
}
Card #1042 belongs to Ana Costa
Max loans: 5
Because a Card has no Library object attached, it can't read the instance field branch. Trying gives the same error you saw in the static lesson: non-static variable branch cannot be referenced from a static context. Static nested classes are great for helper types that belong with their outer class by name, such as Map.Entry in Java's own library.
Inner classes (non-static)
An inner class is declared without static. Each inner object is created for one specific outer object and keeps a hidden reference to it. That's why it can use the outer object's fields and methods directly, even private ones:
public class MusicApp {
public static void main(String[] args) {
Playlist roadTrip = new Playlist("Road Trip", new String[] { "Sunrise Drive", "Open Highway", "Coastline" });
Playlist.Cursor cursor = roadTrip.cursor();
System.out.println(cursor.current());
cursor.next();
System.out.println(cursor.current());
cursor.next();
cursor.next(); // wraps around to the first song
System.out.println(cursor.current());
}
}
class Playlist {
private String name;
private String[] songs;
Playlist(String name, String[] songs) {
this.name = name;
this.songs = songs;
}
Cursor cursor() {
return new Cursor(); // this Cursor belongs to this Playlist
}
class Cursor { // inner class (not static)
private int index = 0;
void next() {
index = (index + 1) % songs.length; // reads the outer object's field
}
String current() {
return name + " > " + songs[index]; // name and songs come from Playlist
}
}
}
Road Trip > Sunrise Drive
Road Trip > Open Highway
Road Trip > Sunrise Drive
Inside Cursor, the names songs and name really mean Playlist.this.songs and Playlist.this.name. Playlist.this is the hidden reference to the outer object. You rarely need to write it, except when an inner field has the same name as an outer one.
Since an inner object needs an outer object, new Playlist.Cursor() from main fails with an enclosing instance that contains Playlist.Cursor is required. The usual approach is a method in the outer class, like cursor(), that creates it. (The rarely used direct form is roadTrip.new Cursor().)
💡 Tip
If your nested class doesn't use any instance fields or methods of the outer class, make it static. It's simpler, and it doesn't keep the outer object alive in memory.
Local classes
A local class is declared inside a method, just like a local variable. It exists only within that method, so no other code can use it. It can read the method's parameters and local variables, as long as they are never changed:
public class ReceiptPrinter {
public static void main(String[] args) {
printReceipt("EUR", new String[] { "Croissant", "Cappuccino" }, new double[] { 2.40, 3.10 });
}
static void printReceipt(String currency, String[] items, double[] prices) {
class Line { // local class: exists only in this method
private String item;
private double price;
Line(String item, double price) {
this.item = item;
this.price = price;
}
String format() {
return String.format("%-12s %s %.2f", item, currency, price); // uses a local variable
}
}
double total = 0;
for (int i = 0; i < items.length; i++) {
Line line = new Line(items[i], prices[i]);
System.out.println(line.format());
total += prices[i];
}
System.out.println(new Line("TOTAL", total).format());
}
}
Croissant EUR 2.40
Cappuccino EUR 3.10
TOTAL EUR 5.50
The Line class uses currency, a parameter of the method. That works because currency is effectively final: it's never reassigned after it gets its value. If you added currency = "USD"; anywhere in the method, the compiler would complain: local variables referenced from an inner class must be final or effectively final.
Local classes are fairly rare in practice. They're handy when a small helper type is needed in one long method only.
Anonymous classes
An anonymous class is a class with no name that you declare and instantiate in one expression. You write new, an interface or class name, and then a class body in curly braces. It's perfect for a one-off implementation of an interface:
public class Checkout {
public static void main(String[] args) {
DiscountRule blackFriday = new DiscountRule() { // anonymous class
@Override
public double apply(double price) {
return price * 0.70;
}
@Override
public String label() {
return "Black Friday (30% off)";
}
};
DiscountRule loyalty = new DiscountRule() { // another one
@Override
public double apply(double price) {
return Math.max(0, price - 5.00);
}
@Override
public String label() {
return "Loyalty ($5 off)";
}
};
double price = 40.00;
for (DiscountRule rule : new DiscountRule[] { blackFriday, loyalty }) {
System.out.printf("%s: $%.2f%n", rule.label(), rule.apply(price));
}
}
}
interface DiscountRule {
double apply(double price);
String label();
}
Black Friday (30% off): $28.00
Loyalty ($5 off): $35.00
Read new DiscountRule() { ... } as "create an object of a brand-new, unnamed class that implements DiscountRule, with this body". Notice the semicolon after the closing brace: the whole thing is one statement. We never had to invent names like BlackFridayRule for classes used only once.
Anonymous classes with Comparator
A classic use is telling Arrays.sort() (from the Arrays class) how to order things. Comparator is a built-in interface whose compare(a, b) method returns a negative number if a should come first, a positive number if b should, and 0 if they tie:
import java.util.Arrays;
import java.util.Comparator;
public class SortSongs {
public static void main(String[] args) {
String[] songs = { "Coastline", "Open Highway", "Rain", "Sunrise Drive" };
Arrays.sort(songs, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length(); // shorter titles first
}
});
System.out.println(Arrays.toString(songs));
}
}
[Rain, Coastline, Open Highway, Sunrise Drive]
The <String> part says this comparator compares strings; you'll learn about it in Java generics.
📝 Note
An anonymous class that implements an interface with just one abstract method can usually be replaced by a much shorter lambda expression, such as (a, b) -> a.length() - b.length(). You'll learn lambdas in Java lambda expressions. Anonymous classes are still needed when the interface has several methods, like DiscountRule above.
Which kind of nested class should you use?
| Use a... | When | Example |
|---|---|---|
| Top-level class | The class is useful on its own | Book, Customer |
| Static nested class | A helper that belongs with the outer class by name but needs no outer object | Library.Card, Map.Entry |
| Inner class | Each object must belong to one outer object and use its data | Playlist.Cursor, Garage.Spot |
| Local class | A named helper needed inside one method only | Line in printReceipt() |
| Anonymous class | A one-off implementation used in a single place | A Comparator, a DiscountRule |
When in doubt, prefer a static nested class over an inner class, and a top-level class over both.
Common mistakes with inner classes
1. Creating an inner object without an outer object
new Playlist.Cursor() fails because a cursor must belong to a playlist. Create it through an outer object, for example with a method like roadTrip.cursor(), or make the class static if it doesn't need the outer data.
2. Using outer instance fields in a static nested class
A static nested class has no outer object, so outer instance fields aren't available. Pass the data in through the constructor, or make it an inner class if it truly belongs to one object.
3. Changing a variable used by a local or anonymous class
Local variables captured by these classes must be effectively final. If you need a changing value, store it in a field instead.
4. Forgetting the semicolon after an anonymous class
DiscountRule r = new DiscountRule() { ... } is a variable declaration, so it ends with };. Leaving out the semicolon is a very common compile error.
Practice exercise
🏋️ Try it yourself
Create a Garage class with a private name and a private boolean open that starts as true, plus a close() method. Add an inner class Spot with a number and a method label() that returns something like Downtown-12 (open), using the outer garage's fields. Add a method spot(int number) that creates a spot. Make spot 12 in "Downtown" and spot 7 in "Airport", close Downtown, then print both labels.
Show solution
public class ParkingGarage {
public static void main(String[] args) {
Garage downtown = new Garage("Downtown");
Garage airport = new Garage("Airport");
Garage.Spot a = downtown.spot(12);
Garage.Spot b = airport.spot(7);
downtown.close();
System.out.println(a.label());
System.out.println(b.label());
}
}
class Garage {
private String name;
private boolean open = true;
Garage(String name) {
this.name = name;
}
Spot spot(int number) {
return new Spot(number);
}
void close() {
open = false;
}
class Spot {
private int number;
Spot(int number) {
this.number = number;
}
String label() {
return name + "-" + number + (open ? " (open)" : " (closed)");
}
}
}
Downtown-12 (closed)
Airport-7 (open)
Spot a was created while Downtown was open, but it prints "closed": it reads its garage's current state through the hidden link. Spot b belongs to a different garage, so it isn't affected.
What's next?
Congratulations on finishing the chapter on inheritance and interfaces! You can now design class families, contracts, enums, records and nested helpers. In the next chapter, you'll learn what happens when things go wrong at run time, and how to handle it gracefully. Start with Java exceptions.
Key takeaways
- A nested class is declared inside another class; Java has static nested, inner, local and anonymous classes.
- A static nested class needs no outer object and is created with new Outer.Nested(); it cannot use outer instance fields.
- An inner (non-static) class object belongs to one outer object and can use its fields, even private ones, through the hidden Outer.this reference.
- A local class is declared inside a method and can use that method's effectively final variables.
- An anonymous class declares and creates a one-off, unnamed class in a single expression, often to implement an interface such as Comparator.
- Prefer static nested classes over inner classes unless the nested object really needs its outer object.
- Single-method anonymous classes can usually be replaced with lambda expressions.
Frequently asked questions
What is an inner class in Java?
An inner class is a non-static class declared inside another class. Each inner object is linked to an object of the outer class, so it can access that object's fields and methods, including private ones.
What is the difference between a static nested class and an inner class?
A static nested class is not tied to any outer object and cannot use the outer class's instance fields. An inner class is tied to one outer object, keeps a hidden reference to it, and can use its instance fields.
What is an anonymous class in Java?
An anonymous class is a class without a name that is declared and instantiated in one expression, such as new Comparator<String>() { ... }. It is used for one-off implementations of an interface or subclasses of a class.
Can an inner class access private members of the outer class?
Yes. Inner classes, and static nested classes too, can access private members of their outer class. A static nested class still needs an outer object to reach instance members.
Should I use an anonymous class or a lambda?
If the interface has exactly one abstract method, a lambda is shorter and clearer. Use an anonymous class when the interface has several abstract methods, when you extend a class, or when you need fields inside the object.