Abstract Classes in Java Explained — When, Why and How to Use Them
- An abstract class is a partially built type — it mixes concrete working methods with abstract must-override slots. This combination of shared state, shared logic, and enforced contracts is something interfaces cannot fully replicate because interfaces cannot hold instance fields.
- The Template Method pattern is the most powerful real-world use of abstract classes: lock the algorithm sequence in a final concrete method, make each variable step abstract so subclasses must fill it in, and add hook methods with no-op defaults for optional steps.
- The signal to choose abstract class over interface is shared instance state. If your related types share fields and need shared constructor logic, use an abstract class. If you only need a capability contract across potentially unrelated types, use an interface.
- Abstract classes are partially built types — they share concrete logic while mandating that subclasses complete the unfinished parts before the type is usable
- The abstract keyword prevents instantiation and unlocks abstract member declarations simultaneously — both effects matter
- Abstract classes can hold fields, constructors, and concrete methods — interfaces cannot hold instance fields in any Java version
- The Template Method Pattern is the killer use case — lock the algorithm order in a final method, delegate the variable steps to abstract methods that subclasses must implement
- Choosing abstract class over interface is fundamentally about shared STATE — if your related types share fields and constructor logic, use an abstract class; if you only need a capability contract, use an interface
- A class can be declared abstract with zero abstract methods — this is a legitimate design choice to prevent direct instantiation of a logically incomplete type
Compile error — cannot instantiate abstract type
grep -rn 'abstract class' src/main/java/io/thecodeforge/grep -rn 'extends DataExporter\|extends Shape\|extends ReportGenerator' src/main/java/io/thecodeforge/Compile error — subclass does not implement inherited abstract method
grep -n 'abstract' src/main/java/io/thecodeforge/export/DataExporter.javagrep -n '@Override' src/main/java/io/thecodeforge/export/S3JsonExporter.javaNullPointerException in a polymorphic call that should have been provided by a subclass override
javap -p io.thecodeforge.export.DataExporter | grep -E 'abstract|void validate|boolean validate'grep -n 'validate\|transform\|write' src/main/java/io/thecodeforge/export/DataExporter.javaProduction Incident
validate() call.validate(), transform(), and write() declared as regular methods with empty bodies that returned true or void without doing anything. The developer created S3JsonExporter and implemented transform() and write() but forgot validate(). Because the base class used concrete methods with empty bodies instead of abstract methods, the compiler had no way to know anything was missing. At runtime, the empty validate() method returned true for every record, allowing corrupted data through the pipeline on every invocation. The template method export() called validate() first, got true back, and proceeded to transform and write corrupted records as if they were valid.validate(), transform(), and write() to abstract methods. Any concrete subclass that omits any of these now produces a compile error naming the exact missing method — the build breaks before the code ever reaches production.
2. Made the template method export() final to prevent any subclass from reordering the steps or bypassing the validation gate.
3. Added a CI build step using a custom annotation processor that fails the pipeline if any concrete DataExporter subclass in the project has unresolved abstract method obligations.
4. Added integration tests that run each concrete exporter with intentionally malformed input data and assert that validate() returns false and no records are written to the destination.Production Debug GuideCommon symptoms when abstract classes are misused or missing in production Java systems. Most of these produce a clear compiler error — read the message rather than working around it.
Circle() or new Rectangle(). If no concrete subclass exists yet, you need to create one. If you genuinely need to remove the constraint, rethink whether the type should be abstract at all — but usually the right answer is to create the concrete type the caller actually needs.BaseClass.methodName()→The concrete subclass is missing one or more abstract method implementations. The compiler message names the exact method — implement it. Either implement every abstract method from the base class or declare your subclass abstract itself to defer the obligation further down the chain. Use grep -n 'abstract' on the base class file to get the full list of what needs implementing.Every large Java codebase eventually hits the same problem: you have a group of related classes that share some behaviour but differ in the implementation details. If you copy-paste the shared logic into each class, you are one bug-fix away from a maintenance nightmare where the same method exists in six places and three of them are out of date. If you rely purely on interfaces, you lose the ability to share actual working code between related types. Abstract classes sit in the sweet spot between these two extremes, and understanding them — really understanding them, not just the syntax — is the difference between writing beginner Java and writing production-grade Java.
Abstract classes exist to solve the problem of partial implementation sharing. They let you say: here is the code every subclass will use, and here are the slots every subclass must fill in themselves. This prevents code duplication while enforcing a contract on anything that extends the class. It is the backbone of classic design patterns like Template Method, and it appears constantly in production frameworks — Android's Activity lifecycle, Spring's AbstractRoutedDataSource, JDBC's connection management, and virtually every plugin-style architecture you will encounter in enterprise Java.
By the end of this article you will know exactly what makes a class abstract, why the compiler stops you from instantiating one, how to design a real-world hierarchy using abstract classes, and — critically — when to reach for an abstract class instead of an interface. You will also understand the three mistakes that trip up intermediate developers in code reviews and interviews, and how to avoid every one of them.
What the abstract Keyword Actually Does — And Why Both Effects Matter
Putting abstract on a class does two things simultaneously: it prevents the class from being instantiated with new, and it enables you to declare methods that have a signature but no body. Both of these are features, not restrictions.
Preventing instantiation makes sense when a class is conceptually incomplete. A plain Shape object with no defined geometry is meaningless — what would you draw? What area would you calculate? Forcing callers to use Circle or Rectangle instead guarantees they always work with something concrete and well-defined.
Declaring abstract methods is how you enforce a contract downward through your hierarchy. You are telling every subclass: I do not know how you will do this, but you absolutely must do it. The compiler backs you up — any non-abstract subclass that forgets to implement an abstract method will fail to compile with a clear error message naming the exact missing method. That is a compile-time safety net you simply cannot get from documentation, comments, or code review alone.
Crucially, abstract classes can also contain fully implemented concrete methods, instance fields, and constructors. A class can be abstract even if it has zero abstract methods — this is a legitimate design choice when you want to prevent direct instantiation of a logically incomplete type while still providing all the default shared logic. This is what separates abstract classes from interfaces: they carry actual state and working implementation, not just a list of method signatures.
The most important production rule: never use a concrete method with an empty body when you mean abstract. An empty concrete method looks like a default implementation but provides no contract enforcement. A subclass that forgets to override it compiles cleanly, runs at runtime, does nothing, and produces a defect that may not surface for days.
package io.thecodeforge.shape; /** * ShapeHierarchy.java — a self-contained, runnable demonstration. * Shows abstract class with both abstract and concrete members. * * Key design decisions: * - colour is shared state that every shape needs — lives in the abstract class * - calculateArea() is abstract — each shape has a unique formula * - printDetails() is concrete — the formatting logic is identical for all shapes * - The constructor initialises shared state; subclasses call super() */ abstract class Shape { // Concrete field — every shape has a colour. // Lives here so it is initialised once, consistently, for all subclasses. private final String colour; // Abstract classes CAN and SHOULD have constructors. // Subclasses call this via super() to ensure colour is always set. // The abstract class itself is never instantiated — but this constructor // runs every time a concrete subclass is instantiated. protected Shape(String colour) { if (colour == null || colour.isBlank()) { throw new IllegalArgumentException("Shape colour cannot be null or blank"); } this.colour = colour; } /** * ABSTRACT METHOD — no body here. * Every concrete subclass MUST provide its own implementation. * The compiler enforces this — forget to implement it and the build breaks. * There is no sensible default: area depends entirely on the geometry. */ public abstract double calculateArea(); /** * ABSTRACT METHOD — perimeter varies by geometry just as area does. * Declaring both as abstract ensures no half-implemented subclass reaches production. */ public abstract double calculatePerimeter(); /** * CONCRETE METHOD — shared formatting logic that every shape reuses. * This is why abstract classes beat interfaces when shared logic matters: * subclasses inherit this for free without writing a single line. */ public void printDetails() { System.out.printf( "Shape: %-12s | Colour: %-8s | Area: %8.2f | Perimeter: %8.2f%n", getClass().getSimpleName(), colour, calculateArea(), // resolved to the concrete subclass at runtime calculatePerimeter() // same — polymorphism at work ); } public String getColour() { return colour; } } class Circle extends Shape { private final double radius; public Circle(String colour, double radius) { super(colour); // delegates shared state initialisation to the abstract parent if (radius <= 0) throw new IllegalArgumentException("Radius must be positive"); this.radius = radius; } @Override // @Override catches typos and signature mismatches at compile time public double calculateArea() { return Math.PI * radius * radius; } @Override public double calculatePerimeter() { return 2 * Math.PI * radius; } } class Rectangle extends Shape { private final double width; private final double height; public Rectangle(String colour, double width, double height) { super(colour); if (width <= 0 || height <= 0) throw new IllegalArgumentException("Dimensions must be positive"); this.width = width; this.height = height; } @Override public double calculateArea() { return width * height; } @Override public double calculatePerimeter() { return 2 * (width + height); } } public class ShapeHierarchy { public static void main(String[] args) { // Shape redShape = new Shape("Red"); // COMPILE ERROR: Shape is abstract; cannot be instantiated // We store references as the abstract type — polymorphism in action. // The concrete type determines which calculateArea() runs at runtime. Shape[] shapes = { new Circle("Red", 5.0), new Rectangle("Blue", 4.0, 6.0), new Circle("Green", 3.5) }; for (Shape shape : shapes) { shape.printDetails(); } } }
Shape: Rectangle | Colour: Blue | Area: 24.00 | Perimeter: 20.00
Shape: Circle | Colour: Green | Area: 38.48 | Perimeter: 21.99
- The foundation, plumbing, and electrical wiring are done — those are the concrete methods and shared fields that every subclass inherits automatically.
- The kitchen layout, bathroom tiles, and interior design are left to the buyer — those are the abstract methods that each subclass must implement before anyone can move in.
- You cannot move into a half-built house — the compiler prevents instantiation of the abstract class.
- Every buyer (subclass) must complete all unfinished rooms before the house is habitable — or the compiler refuses to sign off on the occupancy permit.
- The architect controls the floor plan and load-bearing walls; the buyer controls everything that happens inside the rooms. The architect does not care which tiles you choose, only that you tile the bathroom.
The Template Method Pattern — The Killer Use Case for Abstract Classes
Once you understand the mechanics, the next question is: when should you actually reach for an abstract class? The clearest, most compelling answer is when you have an algorithm whose overall sequence is fixed but whose individual steps vary by subclass. This is the Template Method pattern, and abstract classes are its natural home in Java.
Imagine a data export pipeline: you always validate the data first, then transform it into the target format, then write it to the destination. That order is non-negotiable — writing before validating is the bug that caused the $47K incident described earlier in this article. But how you validate CSV data is completely different from how you validate Parquet, and writing to S3 is completely different from writing to a local file system.
With an abstract class, you lock the sequence in a final concrete template method that calls the steps in order. The steps themselves are abstract — each concrete subclass provides its own implementation. The sequence can physically never be changed or reordered by a subclass because the template method is final. The only thing a subclass can do is provide the step implementations the abstract class demands.
This is why Android's Activity lifecycle works this way. onCreate(), onResume(), onPause() are abstract methods (or hook methods with empty defaults) that Android's framework calls in a defined, unchangeable sequence. Your Activity subclass fills in the steps. The framework owns the sequence. This is also why Spring's JdbcTemplate, JDBC's connection lifecycle management, and virtually every plugin framework you will encounter in enterprise Java use the same pattern.
Hook methods extend the pattern: if a particular step is optional — something that some exporters need but others do not — provide a concrete no-op implementation in the abstract class. Subclasses that need the step override it; subclasses that do not need it get the no-op for free. This gives you maximum flexibility without losing the safety of the fixed sequence.
package io.thecodeforge.export; /** * DataExportPipeline.java — Template Method pattern with abstract classes. * * Every export pipeline follows the same sequence: * validate -> preProcess (optional hook) -> transform -> write -> postProcess (optional hook) * * The sequence is locked in the final export() method. * Subclasses fill in the required steps; optional hooks have no-op defaults. * * This is the pattern that prevents the 14K record corruption incident: * you cannot call transform() before validate() — the template method enforces it. */ abstract class DataExporter { /** * TEMPLATE METHOD — concrete and final. * Owns the algorithm sequence. Cannot be overridden. * Every export goes through exactly these steps in exactly this order. */ public final void export(String rawData) { System.out.println("--- Starting export via " + getClass().getSimpleName() + " ---"); if (!validate(rawData)) { System.out.println("[" + getClass().getSimpleName() + "] Validation failed — export aborted. No records written."); System.out.println(); return; } preProcess(rawData); // hook — optional step, no-op by default String transformedData = transform(rawData); write(transformedData); postProcess(transformedData); // hook — optional step, no-op by default System.out.println("--- Export complete ---\n"); } // REQUIRED STEPS — abstract, every subclass must provide its own implementation. // Missing any of these causes a compile error. The build catches it, not production. protected abstract boolean validate(String rawData); protected abstract String transform(String rawData); protected abstract void write(String processedData); // HOOK METHODS — concrete with no-op defaults. // Subclasses that need these steps override them; others inherit the no-op silently. protected void preProcess(String rawData) { // Default: do nothing. Override when pre-processing is needed. } protected void postProcess(String processedData) { // Default: do nothing. Override for post-write auditing, metrics emission, etc. } } /** CSV-specific exporter — fills in the three required steps. */ class CsvExporter extends DataExporter { @Override protected boolean validate(String rawData) { boolean isValid = rawData != null && rawData.contains(","); System.out.println("[CSV] Validation: " + (isValid ? "PASSED" : "FAILED — no commas found")); return isValid; } @Override protected String transform(String rawData) { // Wrap each comma-separated value in quotes for RFC 4180 compliance String transformed = '"' + rawData.replace(",", "\",\"") + '"'; System.out.println("[CSV] Transformed: " + transformed); return transformed; } @Override protected void write(String processedData) { System.out.println("[CSV] Writing " + processedData.length() + " chars to output.csv"); } } /** JSON-specific exporter — different logic, same fixed sequence. */ class JsonExporter extends DataExporter { @Override protected boolean validate(String rawData) { boolean isValid = rawData != null && !rawData.isBlank(); System.out.println("[JSON] Validation: " + (isValid ? "PASSED" : "FAILED — empty input")); return isValid; } @Override protected String transform(String rawData) { // Produce a minimal valid JSON object String escaped = rawData.replace("\"", "\\\""); String transformed = "{\"data\": \"" + escaped + "\"}"; System.out.println("[JSON] Transformed: " + transformed); return transformed; } @Override protected void write(String processedData) { System.out.println("[JSON] Writing " + processedData.length() + " chars to output.json"); } // Overrides the optional postProcess hook — JSON exports emit metrics @Override protected void postProcess(String processedData) { System.out.println("[JSON] Emitting export metrics to monitoring system"); } } public class DataExportPipeline { public static void main(String[] args) { DataExporter csvExporter = new CsvExporter(); DataExporter jsonExporter = new JsonExporter(); csvExporter.export("Alice,30,Engineer"); // valid CSV jsonExporter.export("name: Bob, age: 25"); // valid JSON input csvExporter.export("no commas anywhere"); // validation fails — nothing written jsonExporter.export(""); // validation fails — nothing written } }
[CSV] Validation: PASSED
[CSV] Transformed: "Alice","30","Engineer"
[CSV] Writing 22 chars to output.csv
--- Export complete ---
--- Starting export via JsonExporter ---
[JSON] Validation: PASSED
[JSON] Transformed: {"data": "name: Bob, age: 25"}
[JSON] Writing 33 chars to output.json
[JSON] Emitting export metrics to monitoring system
--- Export complete ---
--- Starting export via CsvExporter ---
[CSV] Validation: FAILED — no commas found
[CsvExporter] Validation failed — export aborted. No records written.
--- Starting export via JsonExporter ---
[JSON] Validation: FAILED — empty input
[JsonExporter] Validation failed — export aborted. No records written.
export() is marked final. This is deliberate and important. Without final, a subclass could override export() and bypass the validation gate, reorder the steps, or skip the postProcess hook entirely. The entire point of the Template Method pattern is that the sequence is fixed and cannot drift. final is the enforcement mechanism.
For optional steps that some subclasses need and others do not, use hook methods: declare them as concrete methods with empty bodies in the abstract class. Subclasses that need the hook override it. Subclasses that do not need it inherit the no-op silently. This gives you maximum flexibility without sacrificing the fixed-sequence guarantee.export() defeats the entire design.Abstract Classes vs Interfaces — Choosing the Right Tool for the Right Job
This is the question every Java interview surfaces, and the answer has evolved since Java 8 added default methods to interfaces. Here is the honest take, without the oversimplification.
Use an abstract class when your related types genuinely share state — instance fields — or need shared constructor logic. You cannot put instance fields in an interface, and you cannot have a constructor in an interface. If Circle and Rectangle both need a colour field initialised and validated the same way, that shared initialisation belongs in an abstract class. The shared state is the deciding signal.
Use an interface when you are defining a capability that unrelated types might share. A Flyable interface makes sense on an Eagle, a Bat, and a Boeing 737 — they have nothing in common except the capability to fly. Forcing them into an inheritance hierarchy would be a category error. An interface is the right model for a capability; an abstract class is the right model for a family.
The modern Java rule of thumb that captures this clearly: interfaces define what a type CAN DO; abstract classes define what a type IS. An Eagle IS a Bird — that is an IS-A relationship, the natural territory of an abstract class. An Eagle CAN FLY — that is a capability, the natural territory of an interface. These are complementary, not competing: a class can extend one abstract class and implement any number of interfaces, which gives you the full power of both.
Since Java 8, interfaces can have default methods. Since Java 9, they can have private methods. This has blurred the line, but it has not eliminated it. Default methods are designed for backward-compatible API evolution — adding a new method to a public interface without breaking every existing implementor. They are not designed as a replacement for abstract class design. They still cannot hold instance state. The moment your design needs per-object state shared across methods, you need an abstract class.
A practical heuristic that works in almost every case: if you find yourself writing an abstract class with zero instance fields, no constructor logic worth sharing, and every method is abstract — stop. You have written a verbose interface that also burns the single inheritance slot. Refactor it to an interface.
package io.thecodeforge.behavior; /** * BirdHierarchy.java — combining abstract class and interface. * * Shows why the two tools complement rather than compete: * - Bird (abstract class): what every bird IS — shared identity, shared state, shared behaviour * - Flyable (interface): a capability that SOME birds have and others do not * * This models reality accurately. Forcing a Penguin to implement fly() * just because it IS a Bird would be a design error. The interface lets * the compiler enforce the capability only where it genuinely applies. */ // Interface: defines a CAPABILITY, not a family. // An Eagle, a Bat, and a commercial aircraft could all implement Flyable. // They share nothing else — the interface is purely about the capability. interface Flyable { void fly(); double getMaxAltitudeMetres(); // Default method — available since Java 8. // Provides a sensible fallback without requiring all implementors to override. default String getFlightType() { return "powered flight"; } } // Abstract class: defines what every Bird IS — shared identity, shared state. // ALL birds share these fields and behaviours regardless of flight capability. abstract class Bird { private final String species; // shared state — every bird has a species private final String sound; // shared state — every bird makes a sound // Constructor initialises shared state. // Any subclass that forgets to call super() gets a compile error. protected Bird(String species, String sound) { this.species = species; this.sound = sound; } // Concrete shared behaviour — identical for every bird, no duplication. public void makeSound() { System.out.printf("%s says: %s%n", species, sound); } // Abstract — each bird species moves differently on the ground. // This MUST be implemented in every concrete Bird subclass. public abstract void move(); public String getSpecies() { return species; } } // Eagle IS a Bird AND CAN FLY — inherits Bird state, implements Flyable capability. class Eagle extends Bird implements Flyable { public Eagle() { super("Bald Eagle", "Screech!"); } @Override public void move() { System.out.println("Eagle walks deliberately with powerful talons gripping the ground"); } @Override public void fly() { System.out.println("Eagle soars on thermal currents, banking effortlessly at altitude"); } @Override public double getMaxAltitudeMetres() { return 3000.0; // Bald Eagles have been recorded above 3,000m } // Overrides the default — eagles glide as much as they power-flap @Override public String getFlightType() { return "thermal soaring and gliding"; } } // Penguin IS a Bird but CANNOT FLY — no Flyable implementation. // The compiler prevents anyone from calling penguin.fly() because Penguin // never committed to that capability. Correct by design, not by convention. class Penguin extends Bird { public Penguin() { super("Emperor Penguin", "Squawk!"); } @Override public void move() { System.out.println("Penguin waddles across the ice at 2.5 km/h on stubby legs"); } // No fly() — Penguin does not implement Flyable, so fly() is simply unavailable. // This is not a workaround; it is the correct model of reality. } public class BirdHierarchy { public static void main(String[] args) { Eagle eagle = new Eagle(); Penguin penguin = new Penguin(); System.out.println("=== Eagle ==="); eagle.makeSound(); // inherited from Bird — shared concrete method eagle.move(); // Eagle's own implementation of the abstract method eagle.fly(); // Eagle's Flyable implementation System.out.printf("Max altitude: %.0fm | Flight type: %s%n", eagle.getMaxAltitudeMetres(), eagle.getFlightType()); System.out.println("\n=== Penguin ==="); penguin.makeSound(); // same shared concrete method penguin.move(); // Penguin's own implementation // penguin.fly(); // COMPILE ERROR — Penguin does not implement Flyable // Polymorphism via the interface — any Flyable, not just Eagle System.out.println("\n=== Flyable polymorphism ==="); Flyable flier = eagle; // Eagle satisfies the Flyable contract flier.fly(); System.out.println("Is also a Bird? " + (flier instanceof Bird)); } }
Bald Eagle says: Screech!
Eagle walks deliberately with powerful talons gripping the ground
Eagle soars on thermal currents, banking effortlessly at altitude
Max altitude: 3000m | Flight type: thermal soaring and gliding
=== Penguin ===
Emperor Penguin says: Squawk!
Penguin waddles across the ice at 2.5 km/h on stubby legs
=== Flyable polymorphism ===
Eagle soars on thermal currents, banking effortlessly at altitude
Is also a Bird? true
| Feature / Aspect | Abstract Class | Interface |
|---|---|---|
| Instance fields (shared state) | Yes — any access modifier, any type. This is the primary reason to choose abstract class over interface. | No — only public static final constants. Cannot hold per-object state in any Java version. |
| Constructor | Yes — called by subclasses via super() to initialise shared fields with validation. | No — interfaces have no constructors and no instance state to initialise. |
| Concrete methods | Yes — any number. Shared implementation that all subclasses inherit without reimplementing. | Yes via default methods (Java 8+) and static methods, but no access to instance fields. |
| Abstract methods | Yes — zero or more. Zero abstract methods is valid when you only want to prevent direct instantiation. | All non-default, non-static methods are implicitly abstract. Every implementor must provide an implementation. |
| Multiple inheritance | No — a class can extend exactly one abstract class. Single inheritance is a Java language constraint. | Yes — a class can implement any number of interfaces. This is why interface should be the default choice for capability contracts. |
| Access modifiers on methods | Any — private, protected, public, or package-private. Private methods can be used internally. | Public by default. Private methods allowed since Java 9 for internal interface logic. |
| Best signal to use it | Your related types share instance fields and need shared constructor logic. IS-A relationship is genuine. | Unrelated types share a capability. No shared state required. CAN-DO relationship. |
| Keyword in subclass | extends — a class extends one abstract class | implements — a class implements any number of interfaces |
🎯 Key Takeaways
- An abstract class is a partially built type — it mixes concrete working methods with abstract must-override slots. This combination of shared state, shared logic, and enforced contracts is something interfaces cannot fully replicate because interfaces cannot hold instance fields.
- The Template Method pattern is the most powerful real-world use of abstract classes: lock the algorithm sequence in a final concrete method, make each variable step abstract so subclasses must fill it in, and add hook methods with no-op defaults for optional steps.
- The signal to choose abstract class over interface is shared instance state. If your related types share fields and need shared constructor logic, use an abstract class. If you only need a capability contract across potentially unrelated types, use an interface.
- A class can be declared abstract with zero abstract methods — this is a valid way to prevent direct instantiation of a logically incomplete type when all current methods are fully implemented.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QCan an abstract class have a constructor, and if so, what is it used for since you cannot instantiate the abstract class directly?JuniorReveal
- QWhat is the difference between an abstract class and an interface in Java, and when would you choose one over the other?Mid-levelReveal
- QCan you have an abstract class with no abstract methods? If yes, give a scenario where that is the right design choice.Mid-levelReveal
- QYou are designing a plugin system where third-party developers write data exporters. How would you use abstract classes versus interfaces to enforce the plugin contract, and what trade-offs do you face with single inheritance?SeniorReveal
Frequently Asked Questions
Can an abstract class implement an interface in Java?
Yes, and this is a common, powerful pattern. An abstract class can implement an interface without providing implementations for all the interface's methods — the unimplemented interface methods are simply treated as abstract in the abstract class. A concrete subclass further down the hierarchy provides the actual implementations. This lets you define the capability contract as an interface for maximum flexibility, while an abstract class provides the shared infrastructure that most concrete implementations need.
Can an abstract class have a main method in Java?
Yes. An abstract class can contain a public static void main(String[] args) method and the JVM can run it directly. Static methods belong to the class itself rather than to any instance, so the fact that the abstract class cannot be instantiated does not affect static members at all. You could even demonstrate an abstract class hierarchy from within the abstract class's own main method by instantiating the concrete subclasses there.
What happens if a subclass does not implement all abstract methods of its parent abstract class?
If the subclass is declared concrete (not marked abstract itself), the compiler throws a clear error and refuses to compile — you cannot ship an incomplete concrete class. The error names the exact missing method. The only way to avoid implementing all inherited abstract methods is to declare the subclass as abstract itself, deferring the obligation to the next concrete class in the hierarchy. This is intentional and useful for building multi-level hierarchies where intermediate classes add shared logic while remaining partial implementations.
How does @Override interact with abstract methods in Java?
You should always annotate concrete implementations of abstract methods with @Override. While the annotation is technically optional — the compiler will enforce the override with or without it — using @Override provides two critical benefits: the compiler will immediately catch typos or signature mismatches that would otherwise cause the subclass to silently define a new method rather than overriding the abstract one, and it makes the intent explicit to every developer reading the code. A method marked @Override that does not actually override anything is a compile error. A method without @Override that accidentally fails to override is a subtle runtime bug.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.