Lesson 27Phase 3: Abstraction and design
Inner classes and OOP consolidation
Classes that live inside classes, when nesting helps, and a recap that ties all the object ideas so far together.
By BasithUpdated 55 min
- inner class
- nested class
- static nested
- anonymous class
- consolidation
Every class you have written so far has stood on its own, side by side with the others in the file. That is the usual case, and it stays the usual case. Sometimes, though, a class only makes sense as part of another one: a small helper that nothing else in the program uses. Java lets you write that helper inside the class it serves. This lesson shows the kinds of class you can put inside another, when each one fits, and then steps back to connect the object-oriented ideas you have met so far.
- Say why a helper class is sometimes better placed inside the class it serves.
- Tell a
staticnested class apart from an inner class, and build an object of each. - Explain how an inner-class object reaches the fields of the outer object it belongs to.
- Write an anonymous class that implements an interface on the spot, and read one that extends a class.
- Connect the object-oriented ideas of the course so far into one map.
A helper that has no life of its own#
Picture a class that holds an address: a street, a city and a postcode. On its own, an address is a perfectly good class. Now suppose the only reason it exists in your program is to be part of a Person. No other code makes an address by itself or cares about an address that does not belong to a person. The address is a detail of the person.
You can still write it as a top-levelclass, one written directly in the file and not inside another class. Nothing forces you to nest it. But then the file says “here is an Address that anyone may use”, when the truth is “this only ever belongs to a Person”. Writing the class inside Person says what is true: the helper sits where it is used, and it is plainly a detail of its host.
Key ideaNest a class when it is a detail of another class, not something the rest of the program needs to know about. The nesting groups related code and says “this belongs to me”.
Node class. Java's own LinkedList keeps its node class nested inside, as a private static class, because a node means nothing outside its list. The other classic case is a builder, a helper that collects settings and then builds an object, which you will meet in designing classes well as new Pizza.Builder("large").Two kinds, and the one question that separates them#
When you write a class inside another class, you face one decision, and it is one you already understand from static and final: does this nested class need a connection to a particular object of the outer class, or not?
That single question splits nested classes into two kinds:
- A static nested class is marked
static. It has no link to any outer object. It lives inside the outer class's braces only for grouping. Think of it as an ordinary class that you filed inside another one for tidiness. - An inner class is a nested class without
static. It is tied to one particular object of the outer class. Every inner-class object belongs to an outer object and can use that object's fields directly.
This is the rule from static and final again: a static member belongs to the class itself, while a member without static belongs to one object. A static nested class belongs to the outer class. An inner class belongs to an outer object. Seen that way, the two kinds are one familiar idea used once more, with nothing new to memorize.
static.static keyword.new Outer.Helper().outer.new Inner().Library class has a nested Loan class. A loan's describe() method prints the library's name, an instance field of Library. Which kind of nested class should Loan be?A static nested class#
Here is the address as a static nested class. Step through it and watch the order: the address is built first, on its own, with no Person anywhere, and only then handed to the new person.
public class Main { public static void main(String[] args) { // A static nested class is built on its own, no Person object needed. Person.Address home = new Person.Address("Cairo"); Person p = new Person("Aisha", home); System.out.println(p.name + " lives in " + p.address.city); }} class Person { String name; Address address; Person(String name, Address address) { this.name = name; this.address = address; } // Static nested class: it lives inside Person for tidiness, but it has // no link to any particular Person object. It is just filed here. static class Address { String city; Address(String city) { this.city = city; } }}It prints Aisha lives in Cairo. Read the type name carefully: Person.Address. Because Address lives inside Person, its full name starts with its host, the same way a static field is reached as Goblin.count. The static on the class is what lets you write new Person.Address("Cairo") with no Person object in sight. In the memory view the address is an ordinary object of its own, and the person simply points at it.
An inner class#
Now the other kind. An inner class is tied to an outer object, so it can read that object's fields without being handed them. Below, a BankAccount has an inner Statementclass. A statement only makes sense for one particular account, and it reaches straight into that account's owner and balance.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount("Aisha", 500.0); // An inner class is built FROM an outer object: acc.new Statement(). // The statement is bound to this one account. BankAccount.Statement s = acc.new Statement(); s.print(); }} class BankAccount { String owner; double balance; BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } // Inner class (no static): each Statement belongs to one BankAccount and // can read that account's fields, owner and balance, directly. class Statement { void print() { System.out.println("Statement for " + owner + ": balance is " + balance); } }}It prints Statement for Aisha: balance is 500.0. Look at print(). It says owner and balancewith no dot in front, just like an ordinary method reading its own object's fields. Those fields do not belong to the Statement, though. In the memory view the statement has no fields of its own, only a BankAccount.this row with an arrow to the account. The fields belong to the BankAccount this statement was made from. Reaching into the outer object is the whole point of an inner class, and it is why you build one as acc.new Statement(): the statement needs to know which account it belongs to.
So does a statement remember the balance it saw when it was made? Decide before you run this one.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount("Aisha", 500.0); BankAccount.Statement s = acc.new Statement(); acc.balance = 750.0; s.print(); }} class BankAccount { String owner; double balance; BankAccount(String owner, double balance) { this.owner = owner; this.balance = balance; } class Statement { void print() { System.out.println("Statement for " + owner + ": balance is " + balance); } }}The balance changes after the statement is made. What does s.print() show?
It prints Statement for Aisha: balance is 750.0. The statement never copied the balance. It holds a link to the account, and print()reads the account's balance at the moment it runs, so it sees the new value.
In real code you will rarely write acc.new Statement() yourself. Usually the outer class has a method that makes its inner objects, such as Statement statement() { return new Statement(); }. Inside an instance method of BankAccount, a plain new Statement() is attached to this account automatically. The exercise below uses that pattern.
static. If the nested class never touches an outer object's instance fields, make it a static nested class; it is simpler and carries no hidden link. Drop the staticonly when the helper really needs to reach into one outer object's state.Player belongs to one Team, so Player is an inner class, made by the team's sign method. Finish it: the constructor should add one to the size of the team the player joins, and badge() should return the player's name followed by plays for the and the team's name.Anonymous classes: a one-off class with no name#
There is a third shape, and you will see it in real code often. Sometimes you need a class exactly once, in one place, and giving it a name and a block of its own feels like ceremony. Java lets you define such a class and build an object of it in a single expression, with no name at all. That is an anonymous class.
The typical use is a callback: a small piece of behavior you hand to some other code so that it can call you back later. Think “when the button is clicked, run this”. The “this” is a tiny one-off object whose only job is to carry that one method. A whole named class for it would hide the intent; an anonymous class puts the behavior right where it is used.
public class Main { public static void main(String[] args) { // An anonymous class: this defines a one-off subclass of Greeter AND // builds an object of it, all in one expression. It has no name. Greeter g = new Greeter() { void greet() { System.out.println("Hello from a class with no name"); } }; g.greet(); }} class Greeter { void greet() { System.out.println("a plain greeting"); }}It prints Hello from a class with no name. Read the shape slowly. new Greeter() { ... } does more than build a Greeter. The block of braces right after the parentheses is a new class body, written on the spot, that extends Greeter and overrides greet(). There is no class Something extends Greeter line anywhere, because the class never gets a name. Note the semicolon after the closing brace: the whole thing is one expression inside an ordinary variable declaration.
Greeter g = new Greeter() { void greet() { ... } }; create?Implementing an interface on the spot#
The more common use is with an interface. You cannot write new ClickHandler() on its own, because an interface is only a promise: javac says ClickHandler is abstract; cannot be instantiated. Add a body that keeps the promise, though, and you get an object of a nameless class that implements the interface. Here a button stores whatever handler it is given and calls it when pressed:
public class Main { public static void main(String[] args) { String name = "Sana"; Button ok = new Button("OK"); ok.onClick(new ClickHandler() { public void handle(String label) { System.out.println(name + " pressed " + label); } }); ok.press(); }} interface ClickHandler { void handle(String label);} class Button { String label; ClickHandler handler; Button(String label) { this.label = label; } void onClick(ClickHandler h) { handler = h; } void press() { handler.handle(label); }}It prints Sana pressed OK. The anonymous class is written right inside the call to onClick, as its argument. Its handle method is public because interface methods are public, as you saw in interface vs abstract class. Step through it and notice the order: nothing is printed when the handler is handed over. It runs only later, when press() calls it back.
The handler also uses name, a local variable of main. An anonymous class may read the local variables around it, with one condition: they must never change after they get their value. Java calls such a variable effectively final: it is not marked final, but it could be. Give name a second value and the program no longer compiles:
public class Main { public static void main(String[] args) { String name = "Sana"; name = "Ravi"; ClickHandler h = new ClickHandler() { public void handle(String label) { // error: local variables referenced from an inner class must be final or effectively final System.out.println(name + " pressed " + label); } }; h.handle("OK"); }} interface ClickHandler { void handle(String label);}Countdown with an anonymous class and then runs it, printing 3, 2, 1 and Go! on separate lines.Tap a line, then tap where it should go. Or use the arrows.
label -> System.out.println(name + " pressed " + label). Lambdas, and the single-method interfaces they implement, are the subject of enums, lambdas and functional interfaces, right after this lesson. A lambda follows the same rule about local variables.Consolidation: the object ideas so far, in one map#
You now have the core tools for building programs out of objects. The rest of this phase, enums, records and sealed types, builds on these same tools, so this is a good moment to stop adding and connect. Below is one map that ties together everything from the first object you made to the keywords of the last few lessons.
The foundation: class and object
Everything starts with one move. Bundle the data that belongs together with the actions that work on it, and describe that bundle once as a class. From the class you build many objects with new, each holding its own state. A class gives its objects behavior through methods, and a constructor makes sure every object starts out in a valid state. See why objects and creating classes and objects.
The four big ideas
On that foundation stand the four ideas that object-oriented programming is built from. Each one solves a clear problem.
- Encapsulationhides an object's insides so that outside code cannot put it into a nonsense state. Mark fields
privateand offer controlled methods. Access modifiers and encapsulation. - Inheritance lets one class build on another, the is-a relationship, so shared code lives in a parent and every child reuses it. Inheritance, plus the overriding and super that let a child replace or extend what it inherits.
- Polymorphism lets one parent-typed variable drive different behavior for each object, so a single loop over an
Animal[]can make each animal speak in its own voice. Polymorphism. - Abstraction hides detail behind a clean shape, either through an abstract class that shares code while it demands a contract, or through an interface that is mostly a contract with no code of its own.
Key ideaEncapsulation, inheritance, polymorphism and abstraction share one purpose: to let you build a large program out of small parts, each of which knows its own job and can be reused.
The shaping keywords
Around those four ideas sit the keywords that shape how classes relate and what they promise. They are precise tools for expressing the four ideas above.
abstract class or methodinterface with implementsstaticfinalif/else chain that checks what kind of animal it has. Every new animal means editing the chain. Which idea removes the chain?A small recap you can run
Here is one small program that uses several of these ideas at once. Shape is an abstract base with shared code; Circle and Square are concrete subclasses; one array typed by the parent drives polymorphic calls; and a static counter, owned by the class instead of any object, counts how many shapes were made. Press Run and step through it.
public class Main { public static void main(String[] args) { Shape[] shapes = new Shape[2]; shapes[0] = new Circle(2.0); shapes[1] = new Square(3.0); int i = 0; while (i < shapes.length) { shapes[i].describe(); i = i + 1; } System.out.println("shapes made: " + Shape.count); }} abstract class Shape { static int count = 0; String name; Shape(String name) { this.name = name; count = count + 1; } abstract double area(); void describe() { System.out.println(name + " has area " + area()); }} class Circle extends Shape { double radius; Circle(double r) { super("Circle"); radius = r; } double area() { return 3.14 * radius * radius; }} class Square extends Shape { double side; Square(double s) { super("Square"); side = s; } double area() { return side * side; }}Trace what prints. The first shape is a Circle of radius 2.0, so its area is 3.14 * 2.0 * 2.0, which is 12.56: the line reads Circle has area 12.56. The second is a Square of side 3.0, area 9.0: Square has area 9.0. Two shapes were constructed, so the shared static counter reads 2: shapes made: 2. In one short program you can see an abstract base that demands a method, inheritance with super(...), polymorphic calls through an array, and a static member that belongs to the class.
if checks on type led to polymorphism. Each keyword exists to solve a problem you can feel, and when you remember the problem, the tool comes back with it.Try it
Answer each one in your head or on paper first, then open the answer.
static nested class, and when for an inner class?BankAccount example, print() says owner and balance with no dot in front. Whose fields are those, and how can the inner class see them?ClickHandler prints name, a local variable of main. Why is that allowed, and what one-line change to main would stop it compiling?shapes made: print 2 instead of being stored separately on each shape? Tie your answer to a keyword.Practice
Work through these on your own. Each one builds on this page.
Triangle, with fields base and height and an area() of 0.5 * base * height. Add it to the array, run the program, and check that both its area line and the final shapes made: count change as you expect.Playlist class that contains a nested Song type holding a title and a length in seconds. Decide whether Song should be a static nested class or an inner class, and write one or two sentences justifying the choice. Then write it, make a playlist with three songs, and print the total length.Statement class as a static nested class whose constructor takes the account, so that it is built as new BankAccount.Statement(acc). What has to change inside print(), and why? Run both versions and compare.new Something() { ... } shape into your notes and label, in your own words, which interface or class it implements or extends, which method it overrides, and what behavior it supplies.Further reading#
- Nested Classes on dev.java, covering static nested, inner, local and anonymous classes
- Anonymous Classes in Oracle's Java Tutorials, with the rules for the variables they can use
- Inner classes and enclosing instances in the Java Language Specification, section 8.1.3
Short guides on this topic
Quick answers to what people search for about it, with code to run.
Spotted a mistake, or a part that did not make sense? Report it by email