You have a Shape concept, and you know every real shape can report its area() — but there is no such thing as the area of a generic shape. The formula only exists once you know it is a square or a circle. At the same time, you want every shape to share a describe() method that prints its area, so you are not copying that line into every subclass.
Java's answer is the abstract class: a class that is deliberately incomplete. It can declare a method with no body — abstract double area(); — and leave the implementation to each subclass, while still shipping concrete methods and fields that all subclasses reuse. You cannot instantiate an abstract class directly, because by design it is missing pieces.
flowchart TD A["abstract class Shape"] --> B["area — abstract, no body"] A --> C["describe — concrete, shared"] B --> S["subclass must implement area"] style A fill:#c2410c,color:#fff style B fill:#b45309,color:#fff
What an abstract class may contain
An abstract class can hold a mix of three things: abstract methods (a signature with no body, which every non-abstract subclass must implement), concrete methods (ordinary methods with bodies, shared as-is by subclasses), and fields plus a constructor (run when a subclass object is created, through super).
Marking the class abstract does two jobs at once: it permits method signatures without bodies, and it forbids new Shape() — you may only build a concrete subclass such as new Square(4). The shared concrete method is where abstract classes earn their keep. describe() can call area() even though Shape never defines area's body, because dispatch finds the subclass's version at runtime. One shared method, many correct areas.
public class Main {
abstract static class Shape {
abstract double area();
String describe() {
return "Shape with area " + area();
}
}
static class Square extends Shape {
private double side;
Square(double side) {
this.side = side;
}
@Override
double area() {
return side * side;
}
}
public static void main(String[] args) {
Shape s = new Square(4);
System.out.println(s.describe());
}
}
Default methods: a body inside an interface
Since Java 8, an interface can give a method a body too, by marking it default. A class that implements the interface inherits that body unless it overrides it. This lets you add a method to an interface without breaking the thousands of classes that already implement it — they simply pick up the default.
The mental model is optional, overridable behaviour. A class that is happy with the default does nothing; a class that needs something different overrides it. An interface still cannot hold instance state (no instance fields) and cannot be instantiated on its own — those remain the exclusive privileges of a class. Reach for a default method when several unrelated classes would otherwise copy the same trivial behaviour into each one.
flowchart LR I["interface Greeter"] --> D["default greet — has a body"] D --> U["implementing class inherits it unless it overrides"] style I fill:#3776ab,color:#fff
public class Main {
interface Greeter {
default String greet() {
return "Hello from interface default";
}
}
static class Polite implements Greeter {
@Override
public String greet() {
return "Hello from class";
}
}
static class Lazy implements Greeter {
}
public static void main(String[] args) {
Greeter[] gs = { new Polite(), new Lazy() };
for (Greeter g : gs) {
System.out.println(g.greet());
}
}
}
Choosing between them
Reach for an abstract class when your types are genuinely the same kind of thing — they share identity, state, and meaningful code. A Square really is a Shape, and both benefit from shared fields and a constructor. Reach for an interface when unrelated types merely share a capability: a Reporter and a Logger may both be Closeable without being related at all.
The hard rule that often decides it: a class can extends only one class but implements many interfaces. If you spend your one inheritance slot on shared code, you have nowhere left to hang the capabilities your class also needs. Interfaces compose freely; abstract classes do not. When in doubt, prefer the interface and let a helper or abstract class supply shared code alongside it.
flowchart TD
Q["Child extends Base, implements Extra"] --> W{"which greet wins?"}
W -->|"class wins"| B["Base.greet — the superclass method"]
W -->|"no superclass method"| D["Extra default"]
style B fill:#c2410c,color:#fff
public class Main {
static class Base {
public String greet() {
return "Hello from Base";
}
}
interface Extra {
default String greet() {
return "Hello from Extra default";
}
}
static class Child extends Base implements Extra {
}
public static void main(String[] args) {
Child c = new Child();
System.out.println(c.greet());
}
}
What does this program print? An abstract class with a concrete method that calls the abstract one. Predict both lines.
public class Main {
abstract static class Vehicle {
abstract int wheels();
String describe() {
return "has " + wheels() + " wheels";
}
}
static class Car extends Vehicle {
@Override
int wheels() { return 4; }
}
static class Bike extends Vehicle {
@Override
int wheels() { return 2; }
}
public static void main(String[] args) {
Vehicle[] vs = { new Car(), new Bike() };
for (Vehicle v : vs) {
System.out.println(v.describe());
}
}
}
describe()is concrete and shared by both subclasses.It calls
wheels(), which dispatches to each subclass's own return value.Cargives 4,Bikegives 2 — and they print in array order.
Why can you NOT write new Shape() when Shape is an abstract class?
An abstract class deliberately leaves some methods without bodies, so a direct instance would have unimplemented behaviour. Java forbids new Shape() and forces you to instantiate a concrete subclass that fills in the missing pieces.
Default method check. One class overrides tag(), the other inherits the default. Predict both lines.
public class Main {
interface Logger {
default String tag() {
return "LOG";
}
}
static class PlainLogger implements Logger {
}
static class AuditLogger implements Logger {
@Override
public String tag() {
return "AUDIT";
}
}
public static void main(String[] args) {
Logger[] ls = { new AuditLogger(), new PlainLogger() };
for (Logger l : ls) {
System.out.println(l.tag());
}
}
}
AuditLoggeroverridestag(), so its own body runs.PlainLoggeroverrides nothing, so it inherits the interface's default body.They print in array order: AuditLogger first, then PlainLogger.
Class-wins trap. Robot extends a class with status() AND implements an interface whose default is also status(). What prints?
public class Main {
static class Machine {
public String status() {
return "machine: idle";
}
}
interface Reportable {
default String status() {
return "report: idle";
}
}
static class Robot extends Machine implements Reportable {
}
public static void main(String[] args) {
Robot r = new Robot();
System.out.println(r.status());
}
}
Both a superclass method and an interface default provide
status().A concrete superclass method always beats an interface default.
So
Machine.status()wins, even thoughRobotoverrides neither.
A class implements two interfaces, and BOTH declare a default method ping() with a body. The class does not override ping(). What happens?
Two equally-valid default methods leave no correct automatic choice, so the compiler refuses to pick. You must override ping() in the class and disambiguate, optionally calling one side with Iface.super.ping().
Recap
- An abstract class can mix abstract methods (no body) with concrete ones, fields, and a constructor — and it cannot be instantiated directly.
- A default method gives an interface method a body; implementers inherit it unless they override it.
- When a superclass method and an interface default collide, the class always wins — no error is raised.
- When two interface defaults collide, the class must override to disambiguate, or the build fails.
- Prefer interfaces for shared capabilities across unrelated types; reserve abstract classes for genuinely related types that share state and code.
Next you will see why leaning on inheritance for everything leads to brittle designs, and how composition — holding and delegating to other objects — often produces code that is far easier to change.