Appearance
Java Classes in Depth: Concrete, Abstract, Object, and Nested Classes
By now you have used classes constantly in Java. You have written them, instantiated them, and extended them. But do you actually know how many different types of classes Java has? There are concrete classes, abstract classes, the special Object class, nested classes of four distinct varieties, POJOs, enums, final classes, singleton classes, immutable classes, and wrapper classes. This article covers the first group: concrete, abstract, Object, and all four nested class types. Every concept here is something that comes up in interviews, and every one of them has tripped up developers who assumed they understood it.
What a Concrete Class Is
A concrete class is the most ordinary type of class in Java. The name sounds fancy but the definition is simple: a concrete class is any class from which you can create an instance using the new keyword.
If you have a class and you can write new ClassName() and it compiles and runs, that is a concrete class. Every method in a concrete class must have a full implementation. There are no gaps, no empty method bodies left for someone else to fill in later.
java
// A plain concrete class. Every method is fully implemented.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void greet() {
System.out.println("Hello, my name is " + name);
}
}
// You can create an object because Person is concrete
Person p = new Person("Alice", 30);
p.greet();A concrete class can also be a class that implements an interface. The class that provides the actual implementation is the concrete one, not the interface itself.
java
interface Shape {
double area();
}
// Rectangle provides the implementation, so it is concrete
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
// You can create an object of Rectangle because it is concrete
Rectangle r = new Rectangle(5.0, 3.0);Access Modifiers for Top Level Classes
Here is something that catches a lot of people off guard. For a top level class, meaning a class defined at the file level and not inside another class, you only get two access modifier options: public or nothing (package private, also called default).
You cannot make a top level class private. You cannot make it protected. If you try, the compiler will throw an error immediately. The reason is logical: private means only visible inside the enclosing class, but a top level class has no enclosing class. There is nothing to be private to. And protected only makes sense in terms of inheritance hierarchies within packages, which again does not apply to a top level class declaration.
java
// ALLOWED: public top level class
public class Car { }
// ALLOWED: package private (no modifier)
class Engine { }
// COMPILE ERROR: private is not allowed for top level classes
// private class Wheel { }
// COMPILE ERROR: protected is not allowed for top level classes
// protected class Door { }Keep this rule in your memory because it is a classic interview question. However, there is an important exception you will see later: nested classes can be private or protected. The restriction only applies to top level classes.
Abstract Classes: The Incomplete Blueprint
Now here is where things get more interesting. An abstract class is a class that you cannot instantiate. You cannot write new AbstractClassName() because the compiler will not allow it. But why would you ever want a class you cannot create objects from?
Think of a car as an analogy. The concept of a car is real. A car has a brake, a throttle, and a steering wheel. But "car" in general is an abstract idea. You cannot drive the concept of a car. You drive a specific Toyota, or a specific BMW, or a specific Tesla. Each of those is a concrete realization of the abstract idea of a car.
In Java, an abstract class represents exactly that: a conceptual blueprint that is not yet complete enough to instantiate. It defines what something should be able to do, but leaves the specifics of how to the concrete subclasses.
Achieving Abstraction
Abstraction in Java means hiding implementation details from the person using your code and only exposing what they need to know. When you press a brake pedal in a car, you do not need to understand hydraulic fluid pressure or brake pad friction. You just press the pedal and the car slows down. The how is hidden. The what is exposed.
You can achieve abstraction in two ways in Java: through interfaces and through abstract classes. An abstract class can be anywhere from 0% to 100% abstract: it can mix abstract methods with fully implemented ones. (Interfaces are covered in their own chapters.)
java
// An abstract class for Car
public abstract class Car {
// Abstract method: defines what, but not how
// Subclasses MUST provide the implementation
public abstract void pressBrake();
public abstract void pressClutch();
// Concrete method: this one IS implemented here
// Subclasses inherit this directly
public void decreaseSpeed() {
System.out.println("Reducing engine power");
}
}An abstract method is a method with no body. It ends with a semicolon instead of a pair of curly braces. It tells subclasses: you must implement this. If you do not, you are also abstract.
Extending an Abstract Class
Any class that extends an abstract class must either implement all the abstract methods or declare itself abstract as well.
java
// LuxuryCar extends Car but does not implement everything
// It adds its own abstract method and implements one from Car
public abstract class LuxuryCar extends Car {
// Implements one parent abstract method
@Override
public void pressBrake() {
System.out.println("Standard brake applied");
}
// Adds a new abstract method
// This means LuxuryCar itself must be abstract
public abstract void pressDualBrakeSystem();
}java
// Audi is a concrete class. It is NOT abstract.
// It must implement ALL remaining abstract methods from the hierarchy
public class Audi extends LuxuryCar {
// Inherited from Car, not yet implemented in LuxuryCar
@Override
public void pressClutch() {
System.out.println("Clutch pressed on Audi");
}
// Declared in LuxuryCar
@Override
public void pressDualBrakeSystem() {
System.out.println("Dual brake system engaged");
}
}The chain of responsibility is clear. Every abstract method must eventually be implemented by the first concrete class in the hierarchy. Audi is that first concrete class, so Audi handles everything that was left unimplemented above it.
What You Can and Cannot Do With Abstract Classes
You cannot instantiate an abstract class:
java
// COMPILE ERROR
Car c = new Car();
// COMPILE ERROR
LuxuryCar lc = new LuxuryCar();
// ALLOWED: Audi is concrete
Audi audi = new Audi();
// ALSO ALLOWED: store a concrete object in an abstract reference
Car myCar = new Audi(); // Car reference, Audi object
LuxuryCar luxury = new Audi(); // LuxuryCar reference, Audi objectYou can store a reference to a concrete object using the abstract class as the type. The reference type determines what methods you can call on the variable, but the actual object created is always the concrete subclass.
The Object Class: The Silent Parent of Everything
Here is an interview question that surprises people: what is the parent class of a class that does not extend anything?
java
// Person does not say "extends" anything
public class Person {
private String name;
}Your instinct might be "it has no parent." But you are wrong. In Java, every class that does not explicitly extend another class automatically extends java.lang.Object. The compiler inserts extends Object on your behalf whether you write it or not.
This means Object is the parent of every class in Java. It sits at the very top of the inheritance tree. Every class you have ever written inherits from it.
Object
├── Person
├── Car
│ └── LuxuryCar
│ └── Audi
├── String
└── AbstractCollection ... ArrayList (indirectly)
└── ... and every other classWhy This Matters Practically
Because every class inherits from Object, you can store any object in an Object reference variable:
java
Person p = new Person("Alice", 30);
Audi a = new Audi();
// Both can be held in an Object reference
Object obj1 = p; // Works because Person extends Object
Object obj2 = a; // Works because Audi also ultimately extends Object
// You can ask what class an object actually belongs to at runtime
System.out.println(obj1.getClass()); // prints: class Person
System.out.println(obj2.getClass()); // prints: class AudiThis is useful when you want to store different types of objects somewhere and you do not know the type in advance.
Methods That Come From Object
Because every class extends Object, every class inherits its common methods, such as toString(), equals(), clone(), getClass(), wait(), notify(), and notifyAll(). wait() and notify() are used with threads and are covered in the multithreading chapters.
Nested Classes: Four Distinct Types
A nested class is a class defined inside another class. That is the entire definition. The outer class and the inner class share a file but can have very different relationships depending on how the inner class is declared.
Here is the full family tree of nested classes:
Nested Classes
├── Static Nested Class
└── Non Static (Inner Classes)
├── Member Inner Class
├── Local Inner Class
└── Anonymous Inner ClassWhen to use a nested class at all? The guiding principle is simple: if you have a class that will only ever be used by one other class, there is no reason to put it in its own file. Put it inside the class that uses it as a nested class. This keeps logically related code together in one file.
Type 1: Static Nested Class
A static nested class is declared inside another class with the static keyword. The key characteristic: it is associated with the outer class itself, not with any particular instance of the outer class.
Think of the relationship between a static nested class and its outer class the same way you think about static variables. A static variable belongs to the class. A static nested class belongs to the class. Neither needs an object to exist.
java
public class OuterClass {
// Instance variable: belongs to an object
private int instanceVariable = 10;
// Static variable: belongs to the class itself
private static int classVariable = 20;
// Static nested class
public static class StaticNestedClass {
public void display() {
// Can access static members of the outer class
System.out.println("Class variable: " + classVariable);
// CANNOT access instance members: no outer object exists
// System.out.println(instanceVariable); // COMPILE ERROR
}
}
}How to Instantiate a Static Nested Class
Because it is associated with the class and not an instance, you do not need an object of the outer class to create a static nested class object. You use the outer class name directly:
java
// No OuterClass object needed
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
nested.display();Compare this to how you would call a static method on the outer class:
java
// Static method: OuterClass.someStaticMethod()
// Static nested class: new OuterClass.StaticNestedClass()The pattern is the same. Static things are accessed through the class name, not through an object.
Nested Classes Can Be Private
Here is the other key interview point. Earlier you learned that top level classes cannot be private. But nested classes are different. A nested class is a member of the outer class, just like a field or a method. And fields and methods can be private. So nested classes can be private too.
java
public class OuterClass {
// This private nested class cannot be accessed from outside OuterClass
private static class PrivateNested {
public void doSomething() {
System.out.println("Inside private nested class");
}
}
// To use it, OuterClass itself creates the instance
public void usePrivateNested() {
PrivateNested pn = new PrivateNested();
pn.doSomething();
}
}From outside OuterClass, no one can see or touch PrivateNested. If external code needs its behavior, it goes through a public method on OuterClass that internally creates and uses PrivateNested.
A nested class can also be public, protected, or package private. All four access levels are available, unlike top level classes where only public and package private are options.
Type 2: Member Inner Class (Non Static)
A member inner class is a non static class defined at the member level of the outer class. The critical difference from a static nested class: a member inner class is associated with an instance of the outer class, not with the outer class itself.
Because it belongs to an instance, a member inner class has full access to all fields and methods of the outer class, including private ones. The instance already exists, so all instance state is reachable.
java
public class OuterClass {
private int instanceVariable = 10;
private static int classVariable = 20;
// Member inner class (no static keyword)
public class InnerClass {
public void display() {
// Can access BOTH instance and static members of outer class
System.out.println("Instance variable: " + instanceVariable);
System.out.println("Class variable: " + classVariable);
}
}
}How to Instantiate a Member Inner Class
This is where the syntax looks unusual. Because the inner class is tied to an outer class instance, you must first create an outer class object. Then you create the inner class object through that outer class object.
java
// Step 1: create an object of the outer class
OuterClass outer = new OuterClass();
// Step 2: use that outer object to create the inner class object
OuterClass.InnerClass inner = outer.new InnerClass();
// Now you can use the inner object
inner.display();The outer.new InnerClass() syntax is unique to member inner classes. It makes the binding between the inner instance and the outer instance explicit.
Type 3: Local Inner Class
A local inner class is defined inside a method or a block, not at the class member level. It is completely local to that block.
java
public class OuterClass {
private int instanceVariable = 10;
private static int classVariable = 20;
public void display() {
int methodVariable = 30; // local variable in this method
// Local inner class defined inside the method body
class LocalInner {
public void print() {
// Can access outer instance variable
System.out.println("Instance var: " + instanceVariable);
// Can access outer static variable
System.out.println("Class var: " + classVariable);
// Can access effectively final local variables from enclosing scope
System.out.println("Method var: " + methodVariable);
}
}
// Can only be used inside this block
LocalInner local = new LocalInner();
local.print();
}
}Why Its Scope is Limited
Like a local variable, the name LocalInner can only be used inside the block that declares it. You cannot write new LocalInner() anywhere else. (The objects themselves are ordinary Heap objects; only the name is limited to the block.)
That is why a local inner class cannot be public, private, or protected: those modifiers control visibility from outside, and the class name is never visible outside its block anyway. It can use the method's local variables as long as they are not changed after being set.
To use a local inner class from outside the method, you would have it call a public method of the outer class, which internally creates the local inner class object and delegates work to it.
Type 4: Anonymous Inner Class
An anonymous inner class is a class with no name. That is the entire definition. You create the class and instantiate it at the same time, in a single expression.
Anonymous classes are most useful when you want to provide a one time implementation of an abstract class or interface without creating a named subclass file for it.
Here is the situation that motivates anonymous classes:
java
public abstract class Car {
public abstract void pressBrake();
}Normally you would create a named subclass like Audi extends Car in its own file just to implement pressBrake(). But what if you need this implementation only in one place and it is simple enough that creating an entire named class feels like overkill?
java
// Traditional approach: create a named subclass
public class Audi extends Car {
@Override
public void pressBrake() {
System.out.println("Audi brake applied");
}
}
// Then use it:
Car myCar = new Audi();
myCar.pressBrake();java
// Anonymous class approach: no separate file, no named class
Car myAnonymousCar = new Car() {
// The curly brace opens the anonymous class body
@Override
public void pressBrake() {
System.out.println("Anonymous brake applied");
}
// The curly brace closes the anonymous class body
}; // Semicolon here: the whole thing is a statement
myAnonymousCar.pressBrake();That expression new Car() { ... } does several things simultaneously: it declares a new class (with no name of your choosing), it extends Car, it provides the implementation of pressBrake(), and it creates an instance of that new class. All in one expression.
What the Compiler Does Behind the Scenes
This is the part interviewers love to ask about. When the compiler sees an anonymous inner class, it creates a real class file for it. The name is generated automatically by the compiler: if your outer class is named Test, the anonymous class becomes something like Test$1.class. If you have another anonymous class in the same file, it becomes Test$2.class.
So the "anonymous" part only means anonymous to you as the programmer. The compiler gives it a name and produces a real class file. That class extends the abstract class (or implements the interface) you specified, provides the implementation you wrote in the block, and the compiler creates an object of it and assigns it to your reference variable.
java
// What you write:
Car audiCar = new Car() {
@Override
public void pressBrake() {
System.out.println("Brake applied");
}
};
// What the compiler effectively generates (in simplified pseudocode):
// class Test$1 extends Car {
// public void pressBrake() {
// System.out.println("Brake applied");
// }
// }
// Car audiCar = new Test$1();Inheritance With Nested Classes
Inheritance works with nested classes too, and while you may rarely use this in production code, interview questions touch on it. Here are the key scenarios.
Inner Class Inheriting From Another Inner Class
Two inner classes within the same outer class can have an inheritance relationship:
java
public class OuterClass {
class InnerClass1 {
protected int value = 100;
public void display() {
System.out.println("InnerClass1: " + value);
}
}
// InnerClass2 inherits from InnerClass1
class InnerClass2 extends InnerClass1 {
public void show() {
display(); // inherited from InnerClass1
System.out.println("InnerClass2 also sees value: " + value);
}
}
}
// Usage:
OuterClass outer = new OuterClass();
OuterClass.InnerClass2 inner2 = outer.new InnerClass2();
inner2.show();An External Class Inheriting From a Static Nested Class
A completely separate external class can extend a static nested class. Because the nested class is static, this is straightforward:
java
public class OuterClass {
public static class StaticNested {
public void display() {
System.out.println("StaticNested display");
}
}
}
// SomeOtherClass extends the static nested class
public class SomeOtherClass extends OuterClass.StaticNested {
public void extraMethod() {
display(); // inherited from StaticNested
}
}An External Class Inheriting From a Member Inner Class
This is the tricky one. A member inner class is tied to an outer class instance. So when another class tries to extend it, the constructor of the extending class must first create an outer class object, because the super constructor (the inner class constructor) cannot run without one.
java
public class OuterClass {
public class InnerClass {
public void print() {
System.out.println("InnerClass print");
}
}
}
public class SomeOtherClass extends OuterClass.InnerClass {
// Must explicitly create an OuterClass object to call super
SomeOtherClass() {
new OuterClass().super(); // creates outer object, then calls inner constructor
}
public void extra() {
print(); // inherited from InnerClass
}
}The new OuterClass().super() syntax looks strange but it makes sense once you understand why it is there. The inner class constructor requires an outer instance. This line creates a fresh outer instance on the spot and uses it to satisfy that requirement before calling super.
Quick Reference: Nested Class Syntax
java
// Static nested class instantiation: no outer object needed
OuterClass.StaticNested obj1 = new OuterClass.StaticNested();
// Member inner class instantiation: outer object required
OuterClass outer = new OuterClass();
OuterClass.InnerClass obj2 = outer.new InnerClass();
// Local inner class: defined and used inside a method block only
public void someMethod() {
class LocalInner {
void doWork() { System.out.println("local work"); }
}
LocalInner li = new LocalInner();
li.doWork();
}
// Anonymous inner class: no name, declared and instantiated together
AbstractClass ref = new AbstractClass() {
@Override
public void abstractMethod() {
System.out.println("one time implementation");
}
};
ref.abstractMethod();Check yourself
- Which access modifiers can a top-level class have? And a nested class?
- Can you instantiate an abstract class? What can a subclass of an abstract class do?
- What is the parent of
class Person {}? - You stored objects of different classes in
Objectvariables. How do you find out which class each one is? - How do you create an instance of a static nested class versus a member inner class?
- Where can a local inner class be used, and why can it not be
publicorprivate? - What does the compiler generate for an anonymous class?
Answers
- Top-level:
publicor package-private only. Nested:public,protected, package-private, orprivate. - No,
new Car()is not allowed, though aCarreference can hold anAudiobject. A subclass either implements the abstract methods or is itself abstract and may add more abstract methods; the first concrete class must implement everything that is left. java.lang.Object, inserted implicitly.- Call
getClass(), e.g.obj1.getClass()printsclass Person. new Outer.StaticNested()needs no outer object.outer.new Inner()needs one.- Only inside the block (method, loop,
if) that declares it. Its name is not visible outside that block, so access modifiers have nothing to control. - A real class named like
Outer$1, which extends the class or implements the interface you named, plus the code to create one instance of it.