Course contents

Concrete classes in Java

What makes a class concrete, how it differs from abstract classes and interfaces, the exact rules for instantiating one, when to keep a class concrete and final, and how frameworks like Spring and Mockito react to that choice.

FreeOften askedLesson12 min readWarm-upReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: from job description to finished product
  2. Concrete classes under the hood
  3. Real world: final concrete classes and your frameworks
  4. Advanced: the edge cases interviewers probe
  5. How to say it in the interview

🧩 What is a concrete class?

A concrete class is a class that has a body for every method it declares or inherits and isn't marked abstract, so the compiler lets you create instances of it with new. For example, PaymentMethod might be an interface and AbstractCardPayment a half-finished abstract base, but CreditCardPayment is concrete: new CreditCardPayment("tok_4242") compiles and gives you a working object.

Most classes you write are concrete, which is why this looks too simple to be an interview topic. It usually arrives disguised: "can you instantiate an abstract class?", "does an abstract class need an abstract method?", "why is String final?", or "why did my @Transactional stop working when I made the class final?". Each of those is really a question about where the line between concrete and not-concrete sits, and what the JVM and frameworks do on either side of it.

What you’ll learn

  • Define a concrete class precisely and tell it apart from an abstract class and an interface
  • List the instantiation rules: not abstract, an accessible constructor, an outer instance for inner classes, and why enums are the exception
  • Explain what the JVM checks on new, and when you see InstantiationError or InstantiationException
  • Decide between interface, abstract base and concrete class, and when to make a concrete class final
  • Predict how final classes interact with Spring CGLIB proxies, Mockito and the JIT

For this lesson, we'll focus on the spectrum from interface to abstract class to concrete class to final concrete class, the exact rules for instantiation, what the JVM checks when it runs new, and how to decide what a class should be.

Below the line (out of scope)

The 30-second answer

A concrete class is a non-abstract class whose every method, declared or inherited, has an implementation. That's what lets you instantiate it with new, provided its constructor is accessible. An interface is a pure contract (plus default methods), an abstract class is a partial implementation that can hold state and constructors but can't be instantiated, and a concrete class is the complete thing. If a non-abstract class misses an inherited abstract method, it doesn't compile. My default is to write concrete classes, put an interface in front only when I need several implementations or a seam, use an abstract base only for genuinely shared skeleton code, and make concrete classes final unless I've designed them for subclassing.

Interview radar

Asked very often: What's the difference between a concrete class, an abstract class and an interface, and can you instantiate an abstract class? Senior rounds probe: Should classes be final by default, can an abstract class have no abstract methods, and what happens at the bytecode level if you new an abstract class? Production angle: Making a Spring bean class or method final and watching @Transactional or @Cacheable silently stop working, because CGLIB proxies need to subclass it.

01Picture it first: from job description to finished product

Think of hiring for a role. The interface is the job description: "must be able to pay(amount)". It says what, never how. The abstract class is a half-trained apprentice: knows the shared routine (validate, log, call the gateway) but has a gap where the specific skill goes. The concrete class is a fully trained employee you can actually put to work. A final concrete class is that same employee under a contract that says "no one gets to retrain me".

You can only hire the complete employee. That's the instantiation rule in one sentence. The diagram below maps it onto a payment hierarchy.

Fig. 1

PaymentMethod interface at the top, AbstractCardPayment abstract class below it, and two concrete classes at the bottom, CreditCardPayment and a final UpiPayment; red badges mark the top two as not instantiable and green badges mark the bottom two as instantiable with new

PaymentMethod interface at the top, AbstractCardPayment abstract class below it, and two concrete classes at the bottom, CreditCardPayment and a final UpiPayment; red badges mark the top two as not instantiable and green badges mark the bottom two as instantiable with new

Notice that concreteness flows downward. Each level fills in more, and only the levels with nothing missing get a green new badge. UpiPayment implements the interface directly, which is fine: a concrete class doesn't need an abstract parent.

02Concrete classes under the hood

Here's the hierarchy in code. I'm using long paise for money to keep the example small.

public interface PaymentMethod {
    Receipt pay(long amountPaise);                  // abstract: no body
    default String label() { return getClass().getSimpleName(); }
}

public record Receipt(String id, long amountPaise) {}

public abstract class AbstractCardPayment implements PaymentMethod {
    protected final String cardToken;

    protected AbstractCardPayment(String cardToken) {   // abstract classes have constructors
        this.cardToken = cardToken;
    }

    @Override
    public final Receipt pay(long amountPaise) {        // shared skeleton
        if (amountPaise <= 0) throw new IllegalArgumentException("amount must be positive");
        return charge(amountPaise);
    }

    protected abstract Receipt charge(long amountPaise); // the gap
}

public class CreditCardPayment extends AbstractCardPayment {
    public CreditCardPayment(String cardToken) { super(cardToken); }

    @Override
    protected Receipt charge(long amountPaise) {
        return new Receipt("cc-" + cardToken.hashCode(), amountPaise);
    }
}

public final class UpiPayment implements PaymentMethod {
    private final String vpa;
    public UpiPayment(String vpa) { this.vpa = vpa; }

    @Override
    public Receipt pay(long amountPaise) { return new Receipt("upi-" + vpa, amountPaise); }
}

Which lines compile?

Try it yourself first

Before reading on, mark each of the five lines as compiling or not.

PaymentMethod a = new PaymentMethod();                      // 1
AbstractCardPayment b = new AbstractCardPayment("tok");     // 2
PaymentMethod c = new CreditCardPayment("tok_4242");        // 3
PaymentMethod d = new UpiPayment("asha@okbank");            // 4
PaymentMethod e = new PaymentMethod() {                     // 5
    @Override public Receipt pay(long p) { return new Receipt("test", p); }
};

Lines 1 and 2 fail with PaymentMethod is abstract; cannot be instantiated and the same message for AbstractCardPayment. Lines 3, 4 and 5 compile. Notice:

  • The variable's type can be abstract; the object's class can't. PaymentMethod c is fine, because the object behind it is a concrete CreditCardPayment.
  • Line 5 doesn't instantiate the interface. It declares an anonymous concrete class that implements PaymentMethod and instantiates that. javac emits it as a separate class file, something like Checkout$1.class.
  • An abstract class still has a constructor. It runs when a concrete subclass calls super(cardToken), which is how cardToken gets set.

The compiler's completeness check

The rule that makes a class concrete is enforced at compile time. Forget one method and you get one of the most common Java errors:

public class DebitCardPayment extends AbstractCardPayment {
    public DebitCardPayment(String token) { super(token); }
    // forgot charge(long)
}
// error: DebitCardPayment is not abstract and does not override
//        abstract method charge(long) in AbstractCardPayment

You have exactly two ways out: implement charge, or declare DebitCardPayment abstract and push the gap further down.

What new checks at run time

The compiler catches almost every mistake, but the JVM double-checks, because class files can be compiled at different times. The diagram below shows what the new bytecode instruction does before your constructor ever runs.

Fig. 2

Five numbered steps for the new instruction: resolve the class reference, reject interfaces and abstract classes with InstantiationError, check access, initialise the class if needed, then allocate zeroed memory, followed by invokespecial calling the constructor

Five numbered steps for the new instruction: resolve the class reference, reject interfaces and abstract classes with InstantiationError, check access, initialise the class if needed, then allocate zeroed memory, followed by invokespecial calling the constructor

Step 2 is the interesting one. If Checkout was compiled when CreditCardPayment was concrete, and someone later ships a jar where it's abstract, the old Checkout.class still contains new CreditCardPayment. The JVM refuses at run time with java.lang.InstantiationError. Reflection has its own version: AbstractCardPayment.class.getDeclaredConstructor(String.class).newInstance("tok") throws the checked InstantiationException.

Instantiation rules, all of them

Being concrete is necessary but not sufficient for new X() to compile at a given call site. The full list:

  1. Not abstract and not an interface. That's concreteness.
  2. An accessible constructor with matching arguments. java.lang.Math is a concrete, final class, yet new Math() fails because its only constructor is private. Concrete and instantiable are different words for a reason.
  3. An enclosing instance for inner classes. A non-static nested class needs order.new LineItem(), because every instance carries a hidden reference to its outer object.
  4. Not an enum. Enums are concrete classes, but new on an enum is always a compile error; the constants are the only instances.

Here are the three exceptions side by side:

public class Order {
    public class LineItem {}                      // inner: needs an Order
    public static class Builder {}                // static nested: does not
}

enum OrderStatus { NEW, PAID, SHIPPED }

Order order = new Order();
Order.LineItem item = order.new LineItem();       // ✅ outer instance supplied
Order.Builder builder = new Order.Builder();      // ✅
// new Order.LineItem();    ❌ an enclosing instance that contains Order.LineItem is required
// new Math();              ❌ constructor Math() is private, so javac can't use it
// new OrderStatus();       ❌ enum classes may not be instantiated

03Real world: final concrete classes and your frameworks

Here's the path I take when I create a class:

  1. Start concrete. A concrete class is the simplest thing that works. Services, value objects, DTOs and records are all concrete.
  2. Add an interface when there's a real second implementation or a boundary (a payment gateway, a clock, a repository you'll fake in tests).
  3. Add an abstract base only when implementations share state or a skeleton algorithm, like pay() above.
  4. Make the concrete class final unless you've designed it for subclassing. Every overridable method is a promise about when it's called and what it may assume. That's Effective Java Item 19, and the JDK follows it: String, Integer, LocalDate and every record are final.

The decision tree below captures the path.

Fig. 3

A decision tree: if you need several interchangeable implementations, use an interface with concrete implementations; if they share state or a skeleton, add an abstract base; otherwise write a concrete class, and make it final unless it is designed and documented for subclassing

A decision tree: if you need several interchangeable implementations, use an interface with concrete implementations; if they share state or a skeleton, add an abstract base; otherwise write a concrete class, and make it final unless it is designed and documented for subclassing

final has one practical cost: tools that work by subclassing your class at run time.

In the real world

A team marks a Spring service final after a code review comment about immutability. The app still starts, but @Transactional on its methods silently stops working. Why? When a bean has no interface (or proxyTargetClass is on, the Spring Boot default), Spring creates a CGLIB proxy, which is a generated subclass that overrides each public method to open a transaction. A final class can't be subclassed, so proxy creation fails with "Cannot subclass final class"; a final method can't be overridden, so the proxy silently calls the original and the transaction never starts. The same applies to @Configuration classes, which must not be final. This is also why Kotlin projects use the kotlin-spring (all-open) compiler plugin: Kotlin classes are final by default.

Testing used to be the other complaint, but Mockito 5 made the inline mock maker the default, so it mocks final classes and methods out of the box. "We can't make it final because of mocks" is no longer a valid reason on a modern stack.

04Advanced: the edge cases interviewers probe

A comparison table is the fastest way to answer "concrete vs abstract vs interface" without rambling.

Interface Abstract class Concrete class Final concrete class
new it directly ❌ ❌ ✅ (if constructor accessible) ✅
Constructors ❌ ✅ ✅ ✅
Instance fields ❌ (only constants) ✅ ✅ ✅
Abstract methods ✅ (implicitly) ✅ (optional) ❌ ❌
Can be extended implemented by many ✅ ✅ ❌
A class can inherit from many one one one
Where candidates slip

Many candidates say "an abstract class must have at least one abstract method". It doesn't. abstract on the class alone is enough to forbid new. The classic example is javax.servlet.http.HttpServlet: abstract, yet every method has a body, because the authors wanted you to subclass it and override doGet or doPost rather than instantiate a servlet that does nothing.

More edge cases:

  • abstract final is illegal. The compiler rejects the combination: abstract means "must be extended to be useful", final means "can't be extended".
  • A concrete method can be re-abstracted. An abstract subclass may override a concrete method with an abstract one, for example public abstract String toString();, forcing every concrete descendant to write its own.
  • Concrete subclasses of sealed types must pick a modifier. If PaymentMethod were sealed ... permits CreditCardPayment, UpiPayment, each permitted class must be final, sealed or non-sealed. Records and enums are implicitly final, so they fit naturally. See Sealed classes and interfaces.
  • Generic concrete classes are still one class. new ArrayList<String>() and new ArrayList<Order>() instantiate the same runtime class, ArrayList; type arguments are erased.
Performance angle

Don't make a class final to make it faster. HotSpot uses class hierarchy analysis to inline calls on a non-final class that currently has no loaded subclass, and inline caches to handle call sites that see one or two receiver types. A virtual call that gets inlined costs about the same as a final one, close to 0 ns. The real difference is subtle: with a non-final class, loading a new subclass later can deoptimise code that relied on "only one implementation", which shows up as a brief CPU blip, not a steady-state cost. Make classes final for design reasons; the performance is a rounding error.

05How to say it in the interview

When I ask about concrete classes, I'm listening for a precise definition (non-abstract, every method implemented), the difference between concrete and instantiable (private constructors, enums, inner classes), and a design opinion about final. If you can add what the JVM does at new and why Spring proxies care about final, you've turned a warm-up into a senior signal.

In the interview

"A concrete class is a non-abstract class where every method, including inherited abstract ones, has an implementation, so the compiler allows new on it as long as the constructor is accessible. An interface is a contract, an abstract class is a partial implementation with state and constructors, and neither can be instantiated directly; an anonymous class only looks like it, it's a new concrete subclass. Concrete isn't the same as instantiable: Math has a private constructor and enums can't be newed at all. At run time the new instruction rejects abstract classes with InstantiationError. My default is concrete and final unless I've designed for inheritance, with interfaces for real seams, keeping in mind that Spring's CGLIB proxies need non-final classes and methods."

On the whiteboard

interface   = contract (what)        new ❌
abstract    = partial (state + gaps)  new ❌  ctor ✅ (via super)
concrete    = complete                new ✅ if ctor accessible
final conc. = complete + closed       String, Integer, records
compile: "X is not abstract and does not override abstract method m()"
runtime: new on abstract -> InstantiationError
         reflection      -> InstantiationException
concrete != instantiable: private ctor (Math), enum, inner needs outer.new
abstract w/o abstract methods: legal (HttpServlet); abstract final: illegal
default: concrete -> + interface for seams -> + abstract for shared skeleton
final unless designed for extension (EJ 19); CGLIB proxies need non-final
Practise this topicClasses & objects: interview questions & scenarios
Further reading (7)