What Abstraction Means
Abstraction is showing what something does while hiding how it does it. You already use it constantly outside programming: you drive a car by turning a steering wheel without knowing anything about the rack and pinion underneath, and the interface stays the same whether the car is petrol or electric.
In Java, abstraction is about declaring behaviour without committing to an implementation. It differs from encapsulation, and the two get confused in interviews. Encapsulation hides data — it is about protecting fields with private. Abstraction hides implementation — it is about defining a method that says what happens, and letting each class decide how.
Java gives you two tools for it. An abstract class is a partly-finished class: it can hold fields, constructors and working methods, and can also declare methods with no body that every subclass must fill in. An interface is a pure list of what a class must be able to do, with no state of its own.
The reason to bother is the same one from the polymorphism lesson. When your program is written against an abstraction, it does not need to change when you add a new implementation. A notification system written against a Notifier abstraction handles email, SMS and push notifications today, and WhatsApp next month, without a single line changing in the code that sends notifications.
// A method declared with no body: the 'what', without the 'how'
public abstract class Notifier {
protected final String recipient;
protected Notifier(String recipient) {
this.recipient = recipient;
}
// Abstract: every subclass MUST provide this
public abstract void send(String message);
// Concrete: shared by everyone, written once
public void sendUrgent(String message) {
send("[URGENT] " + message);
}
}
public class SmsNotifier extends Notifier {
public SmsNotifier(String phone) { super(phone); }
@Override
public void send(String message) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
public class EmailNotifier extends Notifier {
public EmailNotifier(String email) { super(email); }
@Override
public void send(String message) {
System.out.println("Email to " + recipient + ": " + message);
}
}
// Code written against the abstraction never changes
List<Notifier> channels = List.of(
new SmsNotifier("9876543210"),
new EmailNotifier("ananya@example.com")
);
for (Notifier n : channels) {
n.sendUrgent("Exam postponed to Monday");
} - Abstraction and encapsulation are often asked about together. One sentence that separates them cleanly: encapsulation is about what you can touch, abstraction is about what you need to know.
Abstract Classes: the Rules
A class marked abstract cannot be instantiated. new Notifier("...") is a compile error, and that is the point — "a notifier" with no delivery mechanism is not a real thing, only an idea shared by real things.
An abstract method has a signature and a semicolon instead of a body. Any class extending an abstract class must implement every abstract method it inherits, or must itself be declared abstract and pass the obligation down. Forget one and the compiler tells you precisely which method is missing.
Everything else about an abstract class is ordinary. It can have fields, including private ones. It can have constructors — and it needs them, because subclasses call super(...), even though nobody can call new on it directly. It can have fully written concrete methods that all subclasses inherit unchanged. That mix is exactly what makes it useful: shared implementation in one place, plus a few holes each subclass fills in.
This pattern has a name you will meet again: the template method. The abstract class writes the overall algorithm as a concrete method, calling abstract methods for the steps that vary. Subclasses supply the steps but cannot change the order — which is often precisely the guarantee you want.
public abstract class Vehicle {
protected final String brand;
private int kilometresDriven = 0; // abstract classes can have state
protected Vehicle(String brand) { // and constructors
this.brand = brand;
}
// Abstract: no body, subclass must supply one
public abstract void start();
public abstract double costPerKm();
// Concrete: inherited as-is
public void drive(int km) {
kilometresDriven += km;
System.out.printf("%s drove %d km, cost Rs %.2f%n",
brand, km, km * costPerKm());
}
// Template method: fixed sequence, variable steps
public final void trip(int km) {
start(); // subclass decides how
drive(km);
System.out.println("Trip complete");
}
}
public class ElectricCar extends Vehicle {
public ElectricCar(String brand) { super(brand); }
@Override public void start() {
System.out.println(brand + " starts silently");
}
@Override public double costPerKm() { return 1.2; }
}
public class PetrolCar extends Vehicle {
public PetrolCar(String brand) { super(brand); }
@Override public void start() {
System.out.println(brand + " engine turns over");
}
@Override public double costPerKm() { return 6.5; }
}
// Vehicle v = new Vehicle("Generic");
// error: Vehicle is abstract; cannot be instantiated
Vehicle v = new ElectricCar("Nexon EV"); // reference of the abstract type
v.trip(40);
// Forgetting a method:
// class Scooter extends Vehicle { }
// error: Scooter is not abstract and does not override
// abstract method costPerKm() in Vehicle - An abstract class may contain no abstract methods at all. That is legal and occasionally useful — it is a way of saying "this class is only ever meant to be extended, never used directly", even when every method already works.
Interfaces: a Contract Without Implementation
An interface declares what a class must be able to do, with no fields of its own and no constructor. A class then declares implements and provides the bodies. The word contract is the right one: the interface is a promise, and the compiler enforces it.
Two implicit rules catch people out. Interface methods are automatically public and abstract, so you do not write those keywords — and this means an implementing class must declare them public, since an override cannot narrow access. Forgetting the public in the implementing class gives attempting to assign weaker access privileges, an error that is baffling until you know the method was already public. Any variables declared in an interface are automatically public static final — constants, not fields.
The reason interfaces exist alongside abstract classes is that a class may implement as many interfaces as it likes, while it may extend only one class. That single fact drives most design decisions. A Report can be both printable and exportable and comparable, with no inheritance conflict, because each of those is an interface.
Interfaces are also how the standard library asks you to plug into it. Implement Comparable and Collections.sort() will sort your objects. Implement Runnable and a thread will run yours. You do not modify the library; you satisfy a contract it already understands.
public interface Printable {
void print(); // implicitly public abstract
}
public interface Exportable {
String export(String format);
int MAX_SIZE_MB = 25; // implicitly public static final
}
// One class, several contracts
public class Report implements Printable, Exportable, Comparable<Report> {
private final String title;
private final String content;
public Report(String title, String content) {
this.title = title;
this.content = content;
}
@Override
public void print() { // 'public' is required here
System.out.println(title + ": " + content);
}
@Override
public String export(String format) {
return String.format("[%s] %s", format, title);
}
@Override
public int compareTo(Report other) {
return this.title.compareTo(other.title);
}
}
// Because it implements Comparable, the library can sort it
List<Report> reports = new ArrayList<>(List.of(
new Report("Physics", "..."), new Report("Chemistry", "...")
));
Collections.sort(reports); // works with no extra code
// A variable can be declared by any interface the class implements
Printable p = new Report("Maths", "...");
p.print();
// p.export("pdf"); // not visible through a Printable reference - You cannot write
new Printable()— an interface has no implementation to run. You can, however, create an object that implements it on the spot with an anonymous class, or, if it has a single abstract method, with a lambda. The Lambda Expressions lesson covers that.
default and static Methods, and the Diamond
Before Java 8, adding a method to an interface broke every class that implemented it, because all of them suddenly failed to satisfy the contract. That made interfaces in widely used libraries effectively frozen forever.
Java 8 solved it with default methods: an interface method that carries a body. Existing implementations inherit it and keep compiling; classes that want different behaviour override it. This is exactly how forEach was added to Iterable and stream() to Collection without breaking the world. Use them the same way — for a sensible general implementation most implementers will accept, and for convenience methods built on top of the abstract ones.
Interfaces can also hold static methods, called as InterfaceName.method(), which is a natural home for factory methods and helpers related to the type. Since Java 9 they may also have private methods, purely so that several default methods can share code without exposing it.
Default methods reintroduce a small version of the classic multiple-inheritance problem. If a class implements two interfaces that both provide a default method with the same signature, Java cannot choose, so it refuses to compile until you choose. You override the method in your class, and if you want one of the inherited versions you name it explicitly with InterfaceName.super.method(). This is a deliberately narrow, explicit escape hatch rather than a hidden rule.
public interface Exportable {
String export(String format); // abstract
default String exportAsJson() { // default: has a body
return export("json");
}
default String exportAsCsv() {
return export("csv");
}
static Exportable empty() { // static factory on the interface
return format -> "";
}
}
// ---- The diamond ----
interface Walker {
default String move() { return "walking"; }
}
interface Swimmer {
default String move() { return "swimming"; }
}
class Duck implements Walker, Swimmer {
// Without this override:
// error: class Duck inherits unrelated defaults for move()
// from types Walker and Swimmer
@Override
public String move() {
return Walker.super.move() + " and " + Swimmer.super.move();
}
}
System.out.println(new Duck().move()); // walking and swimming
// One more rule worth knowing: a method inherited from a CLASS always
// wins over a default method inherited from an interface. - Default methods are a compatibility tool, not a way to smuggle state into interfaces. An interface still cannot have instance fields, so a default method can only work with the other methods of the interface — it has nothing of its own to read.
Abstract Class or Interface?
This is one of the most common Java interview questions, and the usual answer — a list of syntactic differences — misses the point. The differences matter because they tell you what each tool is for.
An abstract class says "these things are a kind of the same thing, and here is the shared machinery". It can hold state, run constructor logic, and provide protected helpers, so it is the right choice when subclasses genuinely share implementation and not merely a shape. The cost is that it uses up the single extends slot.
An interface says "these things can all do this", regardless of what they are. A Bird and an Aeroplane have nothing in common as objects, but both can fly. Interfaces cost nothing to add and a class can take as many as it needs, so they are the default choice in modern Java. The common advice is: start with an interface, and introduce an abstract class only when you find real shared code to put in it — often as a companion, the way the library pairs List with AbstractList.
- Instance fields and constructors — only an abstract class
- Multiple inheritance of type — only interfaces (one
extends, manyimplements) - Method bodies — an abstract class freely; an interface only via
default,staticandprivatemethods - Access modifiers on members — anything in an abstract class; interface members are effectively public
- "is a kind of" relationship with shared code — abstract class
- "is capable of" relationship across unrelated types — interface
- Plugging into a library (
Comparable,Runnable,AutoCloseable) — always an interface - Exactly one abstract method, intended for a lambda — interface, marked
@FunctionalInterface
// "can do" — unrelated classes, same capability
interface Flyable {
void fly();
}
class Bird implements Flyable {
@Override public void fly() { System.out.println("Flapping wings"); }
}
class Aeroplane implements Flyable {
@Override public void fly() { System.out.println("Jet engines engaged"); }
}
// "is a kind of", with shared machinery — abstract class
abstract class Employee {
protected final String name;
private final List<String> payslips = new ArrayList<>(); // shared state
protected Employee(String name) { this.name = name; }
public abstract double monthlySalary(); // varies by type
public final void runPayroll() { // shared behaviour
double amount = monthlySalary();
payslips.add(String.format("%s: Rs %.2f", name, amount));
}
}
class SalariedEmployee extends Employee {
private final double annual;
SalariedEmployee(String name, double annual) { super(name); this.annual = annual; }
@Override public double monthlySalary() { return annual / 12; }
}
// And a class can do both at once
class Pilot extends Employee implements Flyable {
Pilot(String name) { super(name); }
@Override public double monthlySalary() { return 150000; }
@Override public void fly() { System.out.println(name + " is flying the aircraft"); }
} - An interface with exactly one abstract method is a functional interface and can be implemented by a lambda expression. Marking it
@FunctionalInterfacemakes the compiler reject a second abstract method being added later, which protects every lambda already written against it.Runnable,ComparatorandPredicateall work this way.
