Class Types in Java
-
Last Updated: March 3, 2025
-
By: javahandson
-
Series
Learn Java in a easy way
Learn the class types in Java with clear examples: concrete, abstract, interface, nested, inner, local, anonymous, final, enum, record, and sealed classes. A friendly beginner guide.
The class types in Java describe the different shapes a class can take. Some classes are ready to use. Others stay half finished on purpose. A few carry no name at all.
Think about a furniture shop. A finished chair sits on the floor, and you can buy it today. Nearby, a half-built frame waits in the workshop for cushions and legs. On the wall hangs a drawing that says what a chair must have, though nobody can sit on paper.
Java works the same way. A concrete class is the finished chair. An abstract class is the half-built frame. An interface is the drawing on paper.
So why does Java bother with all these forms? Because real programs need both freedom and rules. Sometimes you want a class you can use right away. Other times you want to force every future class to follow a shape.
Once you know which form fits which job, your code gets cleaner in a hurry. You stop copying the same method into five classes. You start letting the compiler catch your mistakes for you.
We start with the plain, everyday class. Then we walk up to the trickier forms, one small step at a time. Here is the plan:
You need only a little Java to follow along. If you have written a class with a constructor and a method, you are ready. Every idea comes with a short example you can run.
A class describes what an object will look like. It lists the fields the object holds. It lists the methods the object can run.
Picture a cookie cutter. The cutter itself never becomes a cookie. It only shapes the dough you press through it. A class plays the same role for objects.
When you write new Car(), Java reads the Car blueprint and builds one real object in memory. You can build a thousand cars from that single blueprint. Each car keeps its own field values.
If any of that feels new, our guide on classes and objects in Java walks through the basics first.
A single blueprint style would work, but it would hurt. Say you write Dog, Cat, and Cow. All three need a sound() method. All three also share a breathe() method that does the exact same thing.
Copy breathe() into all three, and you now maintain three copies. Fix a bug in one, and the other two still carry it. That pain grows with every new animal.
Java gives you better tools. An abstract class holds the shared breathe() once and forces each animal to write its own sound(). An interface goes further and holds no code at all, just the promise.
So each class type answers a different question. How much do I want to share? How much do I want to force? Your answer picks the type.
Java developers usually name five core class types. We cover all of them below, then add a few extras you will meet in modern code.
Notice that these labels overlap a little. A nested class can also stay concrete. An inner class can also implement an interface. The labels describe traits, not sealed boxes.
A concrete class carries a body for every single method. Nothing dangles. Nothing waits for a subclass to fill in.
Because of that, Java lets you build objects from it with new. This first class you ever wrote in Java was almost certainly a concrete class.
Most classes in any real project sit in this group. Controllers, services, and helper classes all live here. Abstract classes and interfaces play supporting roles around them.
Here is a small Book class. It holds two fields, takes a constructor, and prints itself.
class Book {
String title;
int pages;
Book(String title, int pages) {
this.title = title;
this.pages = pages;
}
void show() {
System.out.println(title + " has " + pages + " pages");
}
}
public class Main {
public static void main(String[] args) {
Book b = new Book("Effective Java", 412);
b.show(); // Output: Effective Java has 412 pages
}
}Every method here has a body, so Book counts as concrete. The new Book(...) line proves it. Try that same line on an abstract class and the compiler stops you cold.
A concrete class enjoys plenty of freedom, but a few rules still apply.
implements list.That last rule catches beginners often. The moment a class holds an abstract method, the class itself must carry the abstract keyword too.
Go back to Dog, Cat, and Cow. Each animal breathes the same way. Each animal makes a different sound.
An abstract class captures exactly that split. You write breathe() once with a full body. You declare sound() with no body and let each subclass answer it.
Java then guards the deal for you. Forget to write sound() in Dog, and your build fails right away. No silent bug slips into production.
Also note the second half of the deal. Nobody can create a bare Animal, because a plain “animal” has no real sound. The class exists to be extended, not used directly.
Mark the class with abstract. Mark any unfinished method the same way, and end it with a semicolon instead of braces.
abstract class Animal {
String name;
Animal(String name) { // constructors are allowed
this.name = name;
}
abstract void sound(); // no body, subclass must write it
void breathe() { // shared code, written once
System.out.println(name + " is breathing");
}
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override
void sound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog("Bruno");
a.sound(); // Output: Woof!
a.breathe(); // Output: Bruno is breathing
}
}Look at the last block closely. We declare the variable as Animal but point it at a Dog. That trick is the heart of polymorphism in Java.
Now try new Animal("x") on its own line. The compiler refuses, because Animal never finished sound().
Abstract classes surprise people in both directions. They allow more than most beginners expect, and less than the rest expect.
super(...) in a subclass calls them.That constructor point deserves a second look. The abstract class never becomes an object by itself. Its constructor still runs, though, right when a subclass object comes to life.
Pick an abstract class when your classes share both a family name and real code.
Our deep dive on abstract class in Java covers the template pattern and more edge cases.
An interface lists what a class must do. Classically, it says nothing about how.
Think of a wall socket. The socket promises a certain shape and voltage. A lamp, a laptop charger, and a kettle all plug in happily. None of them care what happens behind the wall.
Java interfaces work the same way. Any class that implements Vehicle promises a start() method. Your code can then call start() on anything with that promise.
interface Vehicle {
int WHEELS = 4; // implicitly public static final
void start(); // implicitly public abstract
}
class Car implements Vehicle {
@Override
public void start() { // public is mandatory here
System.out.println("Car starts with " + WHEELS + " wheels");
}
}
public class Main {
public static void main(String[] args) {
Vehicle v = new Car();
v.start(); // Output: Car starts with 4 wheels
}
}Two implicit rules hide in that snippet. Every interface field turns into public static final automatically. Every plain method turns into public abstract.
The second rule explains a classic compile error. Drop public from start() in Car, and Java complains about weaker access. The interface already promised public, so the class cannot narrow it.
Java 8 loosened the “no code” rule. Interfaces gained two method kinds with real bodies.
A default method ships a ready implementation that every implementing class inherits. A static method belongs to the interface itself, much like a static method on a class.
interface Student {
void study(); // abstract
default void submitAssignment() { // Java 8 default method
System.out.println("Assignment submitted");
}
static void schoolInfo() { // Java 8 static method
System.out.println("Java HandsOn School");
}
}
class CollegeStudent implements Student {
@Override
public void study() {
System.out.println("Studying hard");
}
}
public class Main {
public static void main(String[] args) {
CollegeStudent s = new CollegeStudent();
s.study(); // Output: Studying hard
s.submitAssignment(); // Output: Assignment submitted
Student.schoolInfo(); // Output: Java HandsOn School
}
}Why add default methods at all? Backward compatibility drove the change. Java 8 wanted to add forEach() to the huge Iterable interface without breaking every library on earth.
An abstract method would have snapped millions of classes. A default method slipped in quietly, and old code kept compiling.
Default methods created a fresh problem. Two default methods often repeat the same few lines, and there was nowhere private to tuck that logic away.
Java 9 answered with private methods inside interfaces. They stay hidden from implementing classes, so they act as internal helpers only.
interface Logger {
default void info(String msg) {
print("INFO", msg);
}
default void warn(String msg) {
print("WARN", msg);
}
private void print(String level, String msg) { // Java 9
System.out.println("[" + level + "] " + msg);
}
}Notice how print serves both defaults without leaking into any class. Java 9 also permits private static methods for the same reason.
An interface with exactly one abstract method earns a special name. We call it a functional interface.
The count ignores default and static methods entirely. Only abstract methods matter for the rule.
Why care? Because a functional interface accepts a lambda expression. That single rule powers streams, Runnable, Comparator, and most modern Java code.
@FunctionalInterface
interface Greeting {
void greet(String name);
}
public class Main {
public static void main(String[] args) {
Greeting g = name -> System.out.println("Hello, " + name);
g.greet("Suraj"); // Output: Hello, Suraj
}
}The @FunctionalInterface annotation stays optional. Adding it helps, though, because the compiler then guards the one-method rule for you.
For a fuller tour with real examples, read our article on interface in Java.
These two confuse almost everyone at first. The table below puts the real differences in one place.
| Point | Abstract Class | Interface |
|---|---|---|
| Keyword to join | extends |
implements |
| How many | One only | As many as you like |
| Instance fields | Yes | No, constants only |
| Constructors | Yes | No |
| Method bodies | Any method | default, static, private |
| Access modifiers | Any modifier | Members stay public |
| Typical meaning | “is a” relationship | “can do” ability |
| State across subclasses | Shared fields work | No shared state |
Ask one question first. Do these classes share code and state, or only a capability?
Shared code and fields point straight at an abstract class. A shared capability across unrelated types points at an interface.
Dog and Cat belong to a real family, so Animal works well as an abstract class. Now compare Dog with Printer. They share nothing at all, yet both might be Serializable. That is interface territory.
In practice, teams often use both together. The interface names the contract, and an abstract base class handles the boring parts once.
A nested class simply lives inside another class. Java offers a few flavours, and the differences matter more than they look.
Add static to a class inside another class, and you get a static nested class. It behaves like a normal top-level class that happens to live in someone else’s file.
No outer object is required at all. The trade-off? Such a class cannot touch the outer instance fields directly, only the static ones.
class Library {
static String city = "Pune";
static class Book {
void show() {
System.out.println("Book in " + city);
}
}
}
public class Main {
public static void main(String[] args) {
Library.Book b = new Library.Book(); // no Library object needed
b.show(); // Output: Book in Pune
}
}Look at that creation line. You write new Library.Book() and never build a Library. Java’s own Map.Entry follows exactly this pattern.
Drop the static keyword, and the same class turns into an inner class. Now every instance ties itself to one outer object.
That link gives it full access to the outer instance, private fields included. It also means you must create the outer object first.
class Car {
private String model = "Tesla";
class Engine { // inner class, no static
void start() {
System.out.println("Engine of " + model + " starts");
}
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
Car.Engine e = car.new Engine(); // note the outer.new syntax
e.start(); // Output: Engine of Tesla starts
}
}That car.new Engine() syntax looks odd the first time. It reads exactly right, though: this engine belongs to that car.
One modern update helps here. Before Java 16, an inner class could not declare static members. Java 16 lifted that restriction, so static fields and methods now compile fine inside inner classes.
You can even declare a class inside a method. We call that a local class, and its scope ends with the method.
public class Main {
public static void main(String[] args) {
int discount = 10; // effectively final
class Bill { // local class
void print(int amount) {
System.out.println("Pay " + (amount - discount));
}
}
new Bill().print(100); // Output: Pay 90
}
}Local classes stay rare in day-to-day code. They shine when a helper type matters to one method and nothing else.
One habit saves memory and bugs. Prefer static on a nested class unless it truly needs the outer instance. Our guide on nested and inner classes in Java goes deeper on that choice.
Sometimes you need an implementation exactly once. Writing a whole named class for it feels heavy.
An anonymous class solves that. You declare the class and create its single object in one expression. The class never gets a name of its own.
interface Greeting {
void greet();
}
public class Main {
public static void main(String[] args) {
Greeting g = new Greeting() { // anonymous class starts here
@Override
public void greet() {
System.out.println("Java HandsOn");
}
}; // semicolon closes the statement
g.greet(); // Output: Java HandsOn
}
}Two details trip people up. The new Greeting() call does not create an interface object, which nobody can do. It creates an object of a hidden class that implements Greeting on the spot.
The trailing semicolon matters too. The whole thing forms one statement, so the closing brace needs that semicolon after it.
An anonymous class can read local variables from the method around it. Java attaches one condition: the variable must stay final or effectively final.
“Effectively final” means you assign it once and never change it again. You may skip the final keyword, as long as you leave the value alone.
public class Main {
public static void main(String[] args) {
String city = "Mumbai"; // effectively final
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello from " + city);
}
};
r.run(); // Output: Hello from Mumbai
// city = "Delhi"; // uncomment this and the code above fails to compile
}
}Why the rule? The anonymous object may outlive the method call. Java copies the value into the object, so a later change would leave two copies out of sync.
An instance initializer block covers the missing constructor. Just write a bare { ... } block inside the anonymous body to set fields up.
Since Java 8, a lambda often replaces an anonymous class. The lambda version reads far shorter.
// anonymous class version
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
// lambda version, same behaviour
Runnable r2 = () -> System.out.println("Running");So when does the older form still win? A lambda works only for a functional interface, meaning exactly one abstract method.
Reach for an anonymous class when the interface declares two or more methods, when you extend an abstract class, or when you need fields inside the body. Otherwise, take the lambda. Our post on lambda expression in Java 8 explains the syntax in detail.
The five core types cover most textbooks. Modern Java code adds several more, and interviewers love them.
Mark a class final, and nobody can extend it. The class stays concrete, yet the family tree stops there.
final class Currency {
// no class may extend Currency
}
// class Rupee extends Currency { } // compile errorJava uses this trick itself. String, Integer, and the other wrapper classes all carry final. That choice protects their behaviour, since a subclass could otherwise break immutability. See our guide on the final keyword in Java for the full story.
An enum is a special class with a fixed set of objects. Days of the week, order statuses, and card suits all fit perfectly.
enum Status {
ACTIVE, INACTIVE, BANNED
}
public class Main {
public static void main(String[] args) {
Status s = Status.ACTIVE;
System.out.println(s); // Output: ACTIVE
System.out.println(s.ordinal()); // Output: 0
}
}Every enum quietly extends java.lang.Enum, so it cannot extend anything else. It may still implement interfaces, and it may hold fields, constructors, and methods. Our enum in Java article shows those richer forms.
Java 16 made records a permanent feature. A record models plain data with almost no code.
record Point(int x, int y) { }
public class Main {
public static void main(String[] args) {
Point p = new Point(3, 4);
System.out.println(p.x()); // Output: 3
System.out.println(p); // Output: Point[x=3, y=4]
}
}That one line hands you a constructor, accessor methods, equals, hashCode, and toString. Records are implicitly final, and their fields never change after construction.
Java 17 added sealed classes. A sealed class names the exact subclasses that may extend it.
sealed class Shape permits Circle, Square { }
final class Circle extends Shape { }
non-sealed class Square extends Shape { }
// class Triangle extends Shape { } // compile error, not permittedEach permitted subclass must pick one of three words: final, sealed, or non-sealed. This gives you a closed set of types, which pairs beautifully with pattern matching in a switch.
A generic class works with a type you supply later. ArrayList<String> and HashMap<String, Integer> both come from generic classes.
class Box<T> {
private T item;
void put(T item) { this.item = item; }
T get() { return item; }
}
public class Main {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.put("Gift");
System.out.println(box.get()); // Output: Gift
}
}The T acts as a placeholder for a real type. One Box class then serves String, Integer, and every other type, with full compile-time checking.
These five traps catch learners again and again. Spot them early and save yourself an afternoon.
Java permits an abstract class with zero abstract methods. Many people write one by accident, then wonder why new fails.
Ask yourself whether you meant to block direct creation. If not, drop the abstract keyword and move on.
Interface methods carry public access by default. Your implementing class must match that level.
interface Vehicle {
void start(); // public by default
}
class Car implements Vehicle {
void start() { } // compile error: attempting to assign weaker access
}The fix takes one word. Write public void start() and the error disappears.
An inner class holds a hidden reference to its outer object. Keep thousands of them alive, and that reference keeps the outer objects alive too.
Memory leaks in long-running apps often start right here. Add static whenever the nested class ignores outer instance fields.
Anonymous classes, local classes, and lambdas all capture local variables by value. Reassign the variable afterwards and compilation fails.
Two clean fixes exist. Copy the value into a fresh variable, or move the state into a field instead.
Interfaces hold constants, never instance fields. So a counter, a cache, or a name cannot live there.
When several classes must share mutable state, use an abstract class. Better still, combine both, as the walkthrough below shows.
Let us build one tiny notification system. It uses four class types together, which is exactly how real projects look.
First, the promise. Every notifier must send a message. An interface states that in three lines.
interface Notifier {
void send(String message);
}Next, the shared work. Every notifier stamps a channel name onto the message, so we write that once.
abstract class BaseNotifier implements Notifier {
protected final String channel;
BaseNotifier(String channel) {
this.channel = channel;
}
protected String format(String message) { // shared helper
return "[" + channel + "] " + message;
}
}Notice what this class does not do. It never writes send(), so it stays abstract. It only carries the field and the helper that everyone needs.
Now the finished classes. Each one supplies send() and reuses format() from the base.
class EmailNotifier extends BaseNotifier {
EmailNotifier() { super("EMAIL"); }
@Override
public void send(String message) {
System.out.println(format(message));
}
}
class SmsNotifier extends BaseNotifier {
SmsNotifier() { super("SMS"); }
@Override
public void send(String message) {
System.out.println(format(message.toUpperCase()));
}
}Both classes stay tiny. The channel name and the formatting live in one place, so a change there fixes every notifier at once.
Finally, a quick console notifier for a demo. Nobody will reuse it, so an anonymous class fits perfectly.
import java.util.List;
public class Main {
public static void main(String[] args) {
Notifier debug = new Notifier() { // anonymous class
@Override
public void send(String message) {
System.out.println("[DEBUG] " + message);
}
};
List<Notifier> all = List.of(
new EmailNotifier(),
new SmsNotifier(),
debug
);
for (Notifier n : all) {
n.send("Order shipped");
}
}
}
// Output:
// [EMAIL] Order shipped
// [SMS] ORDER SHIPPED
// [DEBUG] Order shippedRead that loop once more. It knows nothing about email, SMS, or debugging. It only knows the Notifier contract, and each object handles the rest.
Adding a push notifier tomorrow costs one small class. The loop never changes. That flexibility is exactly why Java offers so many class types.
A: The five core ones are concrete classes, abstract classes, interfaces, nested classes, and anonymous classes. Modern Java adds final classes, enums, records, sealed classes, and generic classes. Each form answers a different question about how much code you share and how much you force a subclass to write.
A: An abstract class can hold instance fields, constructors, and methods with any access modifier, but a class can extend only one of them. An interface holds only constants plus abstract, default, static, and private methods, and a class can implement many. Use an abstract class for an “is-a” family that shares state, and an interface for a “can-do” ability across unrelated types.
A: Yes. You cannot create an object of the abstract class itself, but its constructor still runs whenever a subclass object comes to life. The subclass calls it through super(…), which is how shared fields such as a name or an id get their initial values.
A: Yes, that compiles fine. The abstract keyword then serves only to stop anyone creating an object directly. The reverse never works, though: any class holding an abstract method must itself carry the abstract keyword.
A: A static nested class needs no outer object, so you create it with new Outer.Nested(). It reaches only the static members of the outer class. An inner class has no static keyword, ties itself to one outer object through outer.new Inner(), and reads every outer member including private ones.
A: The anonymous object can outlive the method that created it. Java therefore copies the local variable’s value into the object rather than sharing it. If you could reassign the original afterwards, the two copies would drift apart, so the compiler blocks the change.
A: No. A constructor needs the class name, and an anonymous class has none. Use an instance initializer block, a bare { … } block inside the body, when you need setup code. You can also pass arguments to the parent constructor in the new expression.
A: Use a lambda when the target is a functional interface, meaning it declares exactly one abstract method. Keep the anonymous class when the interface has several abstract methods, when you extend an abstract class, or when the body needs its own fields.
A: An interface describes behaviour, not state. Every field inside it turns into public static final automatically, so it becomes a shared constant. When several classes must share mutable state, an abstract class is the right tool.
A: Java 17 introduced sealed classes. A sealed class lists its allowed subclasses in a permits clause, and no other class may extend it. Each permitted subclass must declare itself final, sealed, or non-sealed. This closed set works well with pattern matching in a switch.
Let us wrap up what we covered. A concrete class stands finished, so you build objects from it directly with new.
An abstract class sits halfway. It shares fields and real methods, then forces each subclass to finish the missing pieces. An interface goes to the other extreme and states a pure contract, though default, static, and private methods now add limited code.
Nested classes group related types together. Prefer the static form unless the class truly needs its outer object. An anonymous class handles the one-off case, and a lambda often replaces it in modern code.
Beyond those five, keep final classes, enums, records, sealed classes, and generics in your toolkit. Pick the smallest form that says what you mean, and your code stays easy to read for years.