Class Types in Java

  • Last Updated: March 3, 2025
  • By: javahandson
  • Series
img

Class Types in Java

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.

1. Introduction

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.

1.1 What This Article Covers

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:

  • What a class type means, and why Java needs several
  • Concrete classes, the ones you write on day one
  • Abstract classes, and the shared code they hold
  • Interfaces, plus default, static, and private methods
  • A side-by-side table of abstract class versus interface
  • Nested, inner, local, and anonymous classes
  • Final classes, enums, records, sealed classes, and generics
  • Common traps, one full walkthrough, and interview questions

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.

2. What a Class Type Really Means

2.1 A Class Is a Blueprint

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.

2.2 Why Java Needs More Than One Kind

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.

2.3 The Five Main Types at a Glance

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.

  • Concrete class – fully written, and you can create objects from it.
  • Abstract class – partly written, so a subclass must finish the job.
  • Interface – a contract that lists what a class must do.
  • Nested class – a class that lives inside another class.
  • Anonymous class – a nameless class you write and use in one go.

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.

3. Concrete Class

3.1 What Makes a Class Concrete

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.

3.2 A Simple Example

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.

3.3 The Rules a Concrete Class Follows

A concrete class enjoys plenty of freedom, but a few rules still apply.

  • It may extend exactly one other class, abstract or not.
  • Any number of interfaces can sit in its implements list.
  • When it extends an abstract class, it must supply a body for every abstract method.
  • Declaring even one abstract method inside it breaks compilation.

That last rule catches beginners often. The moment a class holds an abstract method, the class itself must carry the abstract keyword too.

4. Abstract Class

4.1 Why a Half-Built Class Helps

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.

4.2 Writing an Abstract Class

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().

4.3 Rules to Remember

Abstract classes surprise people in both directions. They allow more than most beginners expect, and less than the rest expect.

  • No object can come straight from an abstract class.
  • Constructors are perfectly legal, and super(...) in a subclass calls them.
  • Zero abstract methods still works, though the class then just blocks direct use.
  • Instance fields, static fields, and final methods all fit inside.
  • Only one abstract class can sit above your class, since Java allows a single parent.

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.

4.4 When to Reach for an Abstract Class

Pick an abstract class when your classes share both a family name and real code.

  • Several classes repeat the same helper method word for word.
  • You want shared fields, such as a name or an id, in one place.
  • A few steps differ per subclass, and the rest stay identical.
  • The relationship reads as a true “is-a”, like Dog is an Animal.

Our deep dive on abstract class in Java covers the template pattern and more edge cases.

5. Interface

5.1 A Pure Contract

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.

5.2 Default and Static Methods

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.

5.3 Private Methods in Java 9

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.

5.4 Functional Interfaces

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.

5.5 When to Reach for an Interface

  • Unrelated classes need the same ability, such as Printable or Comparable.
  • One class must play several roles at once, since Java allows many interfaces.
  • You want to swap implementations later without touching the caller.
  • A lambda would express the whole idea in one line.

For a fuller tour with real examples, read our article on interface in Java.

6. Abstract Class vs Interface

6.1 Side by Side

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

6.2 How to Choose

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.

7. Nested Classes

A nested class simply lives inside another class. Java offers a few flavours, and the differences matter more than they look.

7.1 Static Nested Class

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.

7.2 Inner Class

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.

7.3 Local Class

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.

7.4 Why Nest a Class at All

  • Grouping keeps a helper type right next to the class that uses it.
  • Encapsulation improves, because a private nested class hides from the outside world.
  • Fewer top-level files means less clutter in your package.
  • Readers see the relationship instantly, with no hunting across the project.

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.

8. Anonymous Class

8.1 One Class, One Object

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.

8.2 Capturing Local Variables

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.

8.3 Limits of Anonymous Classes

  • No constructor can appear, since a constructor needs the class name.
  • Only one parent works: either one interface or one class, never both.
  • Reuse is impossible, because you build exactly one object.
  • Long bodies hurt readability fast, so keep them tiny.

An instance initializer block covers the missing constructor. Just write a bare { ... } block inside the anonymous body to set fields up.

8.4 Lambda or Anonymous Class?

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.

9. Other Class Types You Will Meet

The five core types cover most textbooks. Modern Java code adds several more, and interviewers love them.

9.1 Final Class

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 error

Java 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.

9.2 Enum

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.

9.3 Record

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.

9.4 Sealed Class

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 permitted

Each 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.

9.5 Generic Class

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.

10. Common Mistakes and Pitfalls

These five traps catch learners again and again. Spot them early and save yourself an afternoon.

10.1 Marking a Class Abstract With No Abstract Method

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.

10.2 Forgetting public on an Interface Method

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.

10.3 Making Every Nested Class an Inner Class

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.

10.4 Changing a Captured Local Variable

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.

10.5 Picking an Interface When You Need Shared State

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.

11. A Practical Walkthrough

Let us build one tiny notification system. It uses four class types together, which is exactly how real projects look.

11.1 The Contract

First, the promise. Every notifier must send a message. An interface states that in three lines.

interface Notifier {
    void send(String message);
}

11.2 The Shared Base

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.

11.3 The Concrete Types

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.

11.4 A One-Off Notifier

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 shipped

Read 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.

12. Interview Questions

Q: What are the different class types in Java?

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.

Q: What is the difference between an abstract class and an interface?

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.

Q: Can an abstract class have a constructor in Java?

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.

Q: Can an abstract class exist without any abstract method?

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.

Q: What is the difference between a static nested class and an inner class?

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.

Q: Why must local variables be final or effectively final inside an anonymous class?

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.

Q: Can an anonymous class have a constructor?

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.

Q: When should I use a lambda instead of an anonymous class?

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.

Q: Why can an interface not have instance variables?

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.

Q: What is a sealed class in Java?

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.

13. Conclusion

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.

Further Reading

Leave a Comment