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.
On this page
🧩 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 seeInstantiationErrororInstantiationException - 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)
- Abstract classes in depth (constructors, template method): covered in Abstract classes in Java
- Interfaces, default and static methods: covered in Interfaces in Java and Static and default methods in interfaces
- The
finalkeyword on fields and methods: covered in The final keyword in Java
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.
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 cis fine, because the object behind it is a concreteCreditCardPayment. - Line 5 doesn't instantiate the interface. It declares an anonymous concrete class that implements
PaymentMethodand instantiates that.javacemits it as a separate class file, something likeCheckout$1.class. - An abstract class still has a constructor. It runs when a concrete subclass calls
super(cardToken), which is howcardTokengets 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.
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:
- Not
abstractand not an interface. That's concreteness. - An accessible constructor with matching arguments.
java.lang.Mathis a concrete, final class, yetnew Math()fails because its only constructor isprivate. Concrete and instantiable are different words for a reason. - 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. - Not an enum. Enums are concrete classes, but
newon 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:
- Start concrete. A concrete class is the simplest thing that works. Services, value objects, DTOs and records are all concrete.
- 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).
- Add an abstract base only when implementations share state or a skeleton algorithm, like
pay()above. - Make the concrete class
finalunless 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,LocalDateand every record are final.
The decision tree below captures the path.
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.
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 |
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 finalis 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
PaymentMethodweresealed ... permits CreditCardPayment, UpiPayment, each permitted class must befinal,sealedornon-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>()andnew ArrayList<Order>()instantiate the same runtime class,ArrayList; type arguments are erased.
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
Further reading (7)
- JLS §8.1.1.1 abstract Classes and §8.1.1.2 final Classes
- JLS §15.9.1 Determining the Class being Instantiated
- JVMS §6.5 new instruction
- The Java Tutorials: Abstract Methods and Classes
- Effective Java, 3rd ed., Item 19: Design and document for inheritance or else prohibit it
- Spring Framework reference: Proxying Mechanisms
- Baeldung: Concrete Class in Java