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 (which are 100% abstract by nature) and through abstract classes (which can be anywhere from 0% to 100% abstract).
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.
Abstract Classes Versus Interfaces
This comparison comes up in nearly every Java interview. Here is the honest breakdown:
An interface represents a contract. It says what a class must be able to do. An abstract class represents a partial implementation. It says both what subclasses must do and provides some shared behavior they all need.
Use an abstract class when your subclasses share actual code, like fields and method implementations, not just a common API. Use an interface when you just want to define a capability that completely unrelated classes might implement.
A class can implement multiple interfaces but can only extend one abstract class. This is another important difference because Java does not allow multiple inheritance of classes.
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
└── ArrayList
└── ... 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
The Object class provides a set of methods that every single class in Java inherits. The three you need to know deeply are toString(), equals(), and hashCode(). Others include getClass(), clone(), wait(), notify(), and notifyAll().
toString()
When you print an object directly, Java calls toString() on it behind the scenes. The default implementation from Object returns the class name followed by the at sign and the object's hash code in hexadecimal. It looks like this: Person@1b6d3586. That is not useful to anyone reading your output.
Override toString() to return something meaningful:
java
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
// Returns a readable description of this object
return "Person{name=" + name + ", age=" + age + "}";
}
}
Person p = new Person("Alice", 30);
System.out.println(p); // prints: Person{name=Alice, age=30}equals()
The default equals() method from Object checks reference equality. It asks: are these two variables pointing to the exact same object in memory? This is the same as using ==.
java
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
// Default equals() from Object checks reference, not content
System.out.println(p1.equals(p2)); // false, even though they look the same
System.out.println(p1 == p2); // also false, different objects in memoryMost of the time you want to compare the content of objects, not their memory addresses. Override equals() to define what makes two objects equal:
java
@Override
public boolean equals(Object obj) {
// Same reference? Definitely equal
if (this == obj) return true;
// Different type? Cannot be equal
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
// Equal if name and age match
return this.age == other.age && this.name.equals(other.name);
}hashCode()
Here is the most important contract in all of Java collections: if you override equals(), you must also override hashCode().
The rule is: if two objects are equal according to equals(), they must return the same hashCode() value. This is not optional. It is a contract that HashMap, HashSet, and every other hash based collection depends on.
If you break this contract, your objects will silently misbehave inside collections. You could put an object into a HashMap, then fail to retrieve it with an identical key because the keys land in different hash buckets.
java
@Override
public int hashCode() {
// Include the same fields you used in equals()
int result = name.hashCode();
result = 31 * result + age;
return result;
}The formula using prime number 31 is a common approach. What matters is that two Person objects with the same name and age return the same hash code.
The wait(), notify(), and notifyAll() methods from Object are used in multi threaded programming. You will encounter them properly when studying Java concurrency and threads.
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. Internally, the compiler adds a hidden field called this$0 to the inner class that holds a reference back to the outer object. This is how the inner class always knows which outer object it belongs to.
The this$0 Hidden Reference
When Java compiles a member inner class, it secretly adds a field named this$0 that holds a reference to the enclosing outer class instance. You never see this in your source code, but it is there in the bytecode. This is why you need an outer object to create an inner object: the inner object must have somewhere to store that this$0 reference.
This is also why inner class instances cannot outlive their outer instances without causing potential issues. Each inner instance holds a strong reference back to the outer instance, which prevents the outer instance from being garbage collected as long as the inner instance is alive.
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
Think about how method memory works. When Java calls a method, it allocates a stack frame for that method. All the method's local variables live in that stack frame. When the method returns, the stack frame is destroyed and the memory is reclaimed.
A local inner class lives inside that stack frame. Its entire existence is bounded by the method call. Once the method returns, the class is gone. You cannot use LocalInner outside of the display() method because it does not exist outside of it.
This is why a local inner class cannot have public, private, or protected access modifiers. Those modifiers control visibility from the outside, but a local inner class has no outside. It is invisible to the entire world except for the block it was defined in. Only the default (package private) access makes conceptual sense, though in practice the modifier is just omitted entirely.
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.
Interview Questions and Pitfalls
Q: Can a top level class be private? No. A top level class can only be public or package private (no modifier). But a nested class can be private, protected, public, or package private. This distinction is important.
Q: Can you create an object of an abstract class? No. You cannot write new AbstractClass(). But you can hold a reference of the abstract class type and assign a concrete subclass object to it. The object is always of the concrete type; the reference variable can be of the abstract type.
Q: What is the parent of a class that does not extend anything? Every such class implicitly extends java.lang.Object. The compiler inserts this automatically. Object is the root of the entire Java class hierarchy.
Q: If you override equals(), what else must you override? You must override hashCode(). The contract is: if a.equals(b) returns true, then a.hashCode() must equal b.hashCode(). Breaking this contract causes silent failures in HashMap, HashSet, and any other hash based collection.
Q: What is the difference between a static nested class and an inner class? A static nested class is associated with the outer class itself and can be instantiated without an outer class object: new OuterClass.StaticNested(). A member inner class is associated with an outer class instance and requires an outer object for instantiation: outer.new InnerClass(). The static nested class can only access static members of the outer class. The inner class can access all members, both static and instance.
Q: What is a local inner class and why can it not be private or public? A local inner class is defined inside a method or block. Its entire scope is limited to that block. Access modifiers like private and public control visibility from the outside, but a local inner class has no meaningful outside. It simply cannot be referenced from anywhere beyond the block it lives in, regardless of any modifier you might try to apply.
Q: What is an anonymous class? An anonymous class is a class with no name given by the programmer. You declare it and instantiate it in a single expression. It is typically used to provide a one time implementation of an abstract class or interface without creating a named file for it. The compiler generates the class name automatically (like ClassName$1.class) and creates a proper class file behind the scenes.
Q: Why does a member inner class hold a this$0 reference? Because the inner class needs to access instance members of the outer class, it must always have a reference to the specific outer object it belongs to. The compiler adds a hidden field called this$0 to the inner class bytecode to hold this reference. This is why creating an inner class object always requires an existing outer object.
Q: What happens if a subclass of an abstract class does not implement all abstract methods? The subclass itself must be declared abstract. The obligation to implement all abstract methods is passed down the chain until the first concrete class in the hierarchy, which must implement every remaining abstract method.
Q: Can an abstract class have a constructor? Yes. An abstract class can and often should have constructors. Even though you cannot call new AbstractClass() directly, subclass constructors call super() which invokes the abstract class constructor. This lets the abstract class initialize its own private fields properly.
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();Putting It All Together
You now have a complete picture of the first half of Java's class taxonomy. Concrete classes are the workhorses you instantiate every day. Abstract classes let you define partial blueprints that enforce a contract on subclasses while sharing common code. The Object class is the silent ancestor providing methods like toString(), equals(), and hashCode() to every class you will ever write. And nested classes come in four forms depending on where they live and whether they need an outer class instance to function.
The nested class types are worth memorizing as a group: static nested (class level, no outer instance), member inner (instance level, outer instance required, has the hidden this$0 reference), local inner (block level, scope limited to the enclosing block), and anonymous (no name, declare and instantiate together, used for one off implementations).
The next set of class types, including generic classes, POJOs, enums, final classes, singleton classes, immutable classes, and wrapper classes, builds on everything covered here. Make sure these concepts are solid before moving on.