Master abstraction in Java by choosing correctly between abstract classes and interfaces, implementing multiple interfaces, using default methods, and designing flexible APIs with sealed types.
Abstraction is the process of hiding implementation details and exposing only what callers need to know. Java provides two mechanisms: abstract classes and interfaces. Getting the choice right is one of the most consequential design decisions you make when building libraries or frameworks.
Interfaces define contracts — what operations a type supports — without any state. Any class can implement any number of interfaces. Abstract classes define partial implementations — they provide common behavior that subclasses inherit and specialize. With Java 8's default methods, the distinction blurred: interfaces can now have concrete method implementations. With sealed interfaces in Java 17, interfaces can define closed hierarchies that the compiler can verify exhaustively. In this lesson you will develop a clear decision framework and see both features applied to real enterprise patterns.
1// Abstract class: cannot be instantiated, may have abstract methods
2public abstract class Vehicle {
3 // Instance state — available to all subclasses
4 protected String make;
5 protected String model;
6 protected int year;
7 private double fuelLevel;
8
9 // Constructor — called by subclass constructors via super()
10 protected Vehicle(String make, String model, int year) {
11 this.make = make;
12 this.model = model;
13 this.year = year;
14 this.fuelLevel = 1.0; // 100%
15 }
16
17 // Abstract methods: subclasses MUST implement these
18 public abstract double fuelEfficiency(); // km per liter
19 public abstract String fuelType();
20 public abstract void start();
21
22 // Concrete method: shared by all subclasses
23 public double range() {
24 return fuelLevel * 60 * fuelEfficiency(); // 60L tank
25 }
26
27 public void refuel(double amount) {
28 fuelLevel = Math.min(1.0, fuelLevel + amount / 60.0);
29 }
30
31 // Template method pattern: concrete flow, abstract steps
32 public final String describe() {
33 return String.format("%d %s %s — %s, range: %.0f km",
34 year, make, model, fuelType(), range());
35 }
36
37 @Override
38 public String toString() { return describe(); }
39}
40
41// Concrete subclasses implement the abstract methods
42public class GasolineCar extends Vehicle {
43 private int engineCc;
44
45 public GasolineCar(String make, String model, int year, int cc) {
46 super(make, model, year);
47 this.engineCc = cc;
48 }
49
50 @Override public double fuelEfficiency() { return 12.5; } // km/L
51 @Override public String fuelType() { return "Gasoline"; }
52 @Override public void start() { System.out.println("Vroom! Engine starts."); }
53}
54
55public class ElectricCar extends Vehicle {
56 private int batteryKwh;
57
58 public ElectricCar(String make, String model, int year, int kwh) {
59 super(make, model, year);
60 this.batteryKwh = kwh;
61 }
62
63 @Override public double fuelEfficiency() { return 6.0; } // km/kWh
64 @Override public String fuelType() { return "Electric"; }
65 @Override public void start() { System.out.println("Silent motor engages."); }
66}
67
68// Vehicle car = new Vehicle(...); // COMPILE ERROR: abstract class
69Vehicle gas = new GasolineCar("Toyota", "Camry", 2023, 2000);
70Vehicle ev = new ElectricCar("Tesla", "Model 3", 2023, 75);
71gas.start();
72System.out.println(ev.describe());1// Interface: defines capabilities (what), not state (how)
2public interface Printable {
3 void print(); // implicitly public abstract
4}
5
6public interface Serializable {
7 String serialize();
8 static Serializable of(String data) { return () -> data; } // static method
9}
10
11public interface Auditable {
12 String getCreatedBy();
13 java.time.LocalDateTime getCreatedAt();
14
15 // Default method: provides implementation (Java 8+)
16 default String auditInfo() {
17 return String.format("Created by %s at %s",
18 getCreatedBy(), getCreatedAt());
19 }
20}
21
22// A class can implement multiple interfaces
23public class Document implements Printable, Serializable, Auditable {
24 private String title;
25 private String content;
26 private String createdBy;
27 private java.time.LocalDateTime createdAt;
28
29 public Document(String title, String content, String createdBy) {
30 this.title = title;
31 this.content = content;
32 this.createdBy = createdBy;
33 this.createdAt = java.time.LocalDateTime.now();
34 }
35
36 // From Printable
37 @Override
38 public void print() {
39 System.out.println("=== " + title + " ===");
40 System.out.println(content);
41 }
42
43 // From Serializable
44 @Override
45 public String serialize() {
46 return title + "|" + content + "|" + createdBy;
47 }
48
49 // From Auditable
50 @Override public String getCreatedBy() { return createdBy; }
51 @Override public java.time.LocalDateTime getCreatedAt() { return createdAt; }
52 // auditInfo() is inherited from Auditable — no override needed
53}
54
55// Interface as type — polymorphism across unrelated class hierarchies
56void printAll(List<Printable> items) {
57 items.forEach(Printable::print); // method reference
58}
59
60void archiveAll(List<Serializable> items) {
61 items.stream()
62 .map(Serializable::serialize)
63 .forEach(System.out::println);
64}1// Default methods enable evolving interfaces without breaking existing implementors
2public interface Collection<E> { // simplified java.util.Collection
3 boolean add(E element);
4 boolean remove(E element);
5 int size();
6 boolean isEmpty();
7
8 // Default method (Java 8+): added after initial release
9 // Existing implementors inherit this — no code changes needed
10 default boolean addAll(Iterable<? extends E> elements) {
11 boolean changed = false;
12 for (E element : elements) {
13 changed |= add(element);
14 }
15 return changed;
16 }
17}
18
19// Functional interfaces: exactly ONE abstract method
20@FunctionalInterface
21public interface Transformer<T, R> {
22 R transform(T input); // single abstract method
23
24 // Default methods don't break the functional interface contract
25 default Transformer<T, R> logBefore() {
26 return input -> {
27 System.out.println("Input: " + input);
28 return this.transform(input);
29 };
30 }
31}
32
33// Lambdas implement functional interfaces concisely
34Transformer<String, Integer> lengthOf = s -> s.length();
35Transformer<String, Integer> loggedLength = lengthOf.logBefore();
36
37System.out.println(lengthOf.transform("hello")); // 5
38System.out.println(loggedLength.transform("hi")); // Input: hi\n2
39
40// Common built-in functional interfaces
41import java.util.function.*;
42
43Function<String, Integer> length = String::length; // T → R
44Predicate<String> nonEmpty = s -> !s.isEmpty(); // T → boolean
45Consumer<String> printer = System.out::println; // T → void
46Supplier<String> greeting = () -> "Hello World"; // () → T
47BiFunction<Integer,Integer,Integer> add = (a, b) -> a + b; // T,U → R
48
49System.out.println(length.apply("Java")); // 4
50System.out.println(nonEmpty.test("")); // false
51printer.accept("Printed!");
52System.out.println(greeting.get());
53System.out.println(add.apply(3, 4)); // 71// USE ABSTRACT CLASS when:
2// 1. Subclasses share state (instance variables)
3// 2. You want to provide partial implementations
4// 3. You need constructors to initialize shared state
5// 4. The hierarchy is tightly coupled (IS-A, same team)
6
7abstract class Report { // has state → abstract class
8 protected String title;
9 protected LocalDate date;
10 protected List<String> lines = new ArrayList<>();
11
12 protected Report(String title) {
13 this.title = title;
14 this.date = LocalDate.now();
15 }
16
17 public abstract void generate(); // must implement
18
19 protected void addLine(String line) { lines.add(line); } // shared
20
21 public void print() { // shared template
22 System.out.println("Report: " + title + " (" + date + ")");
23 lines.forEach(System.out::println);
24 }
25}
26
27// USE INTERFACE when:
28// 1. Defining capabilities across unrelated class hierarchies
29// 2. Classes need to implement multiple contracts
30// 3. No shared state — just behavior contracts
31// 4. Design for extensibility across teams/libraries
32
33interface Exportable { // no state → interface
34 byte[] toBytes();
35 default String toBase64() {
36 return java.util.Base64.getEncoder()
37 .encodeToString(toBytes());
38 }
39}
40
41interface Validatable {
42 List<String> validate();
43 default boolean isValid() { return validate().isEmpty(); }
44}
45
46// A class can only extend ONE abstract class
47// but can implement MULTIPLE interfaces
48class SalesReport extends Report
49 implements Exportable, Validatable, Comparable<SalesReport> {
50
51 private double totalSales;
52
53 public SalesReport(String title, double totalSales) {
54 super(title);
55 this.totalSales = totalSales;
56 }
57
58 @Override public void generate() {
59 addLine("Total Sales: $" + totalSales);
60 }
61
62 @Override public byte[] toBytes() {
63 return title.getBytes();
64 }
65
66 @Override public List<String> validate() {
67 List<String> errors = new ArrayList<>();
68 if (totalSales < 0) errors.add("Sales cannot be negative");
69 return errors;
70 }
71
72 @Override public int compareTo(SalesReport other) {
73 return Double.compare(this.totalSales, other.totalSales);
74 }
75}1// Sealed interface (Java 17+): restricts permitted subtypes
2// All permitted types must be in the same package or module
3public sealed interface Shape permits Circle, Rectangle, Triangle {}
4
5public record Circle(double radius) implements Shape {}
6public record Rectangle(double width, double height) implements Shape {}
7public record Triangle(double base, double height) implements Shape {}
8
9// Exhaustive switch: compiler verifies all permitted types are handled
10// No 'default' needed if all cases are covered
11double area(Shape shape) {
12 return switch (shape) {
13 case Circle c -> Math.PI * c.radius() * c.radius();
14 case Rectangle r -> r.width() * r.height();
15 case Triangle t -> 0.5 * t.base() * t.height();
16 // If you omit Triangle, the compiler gives an ERROR — not just a warning
17 // This is a compile-time guarantee that all shapes are handled
18 };
19}
20
21// Sealed interfaces for domain modeling
22public sealed interface PaymentResult permits Success, Failure, Pending {}
23
24record Success(String transactionId, double amount) implements PaymentResult {}
25record Failure(String reason, int errorCode) implements PaymentResult {}
26record Pending(String referenceId) implements PaymentResult {}
27
28// Process all payment outcomes exhaustively
29String handlePayment(PaymentResult result) {
30 return switch (result) {
31 case Success s -> "Paid: " + s.transactionId();
32 case Failure f -> "Failed: " + f.reason();
33 case Pending p -> "Pending: " + p.referenceId();
34 };
35}A complete payment processing design that uses interfaces for contracts, an abstract class for shared implementation, default methods for extensibility, and sealed types for exhaustive processing.
1import java.time.LocalDateTime;
2import java.util.ArrayList;
3import java.util.List;
4import java.util.UUID;
5
6/**
7 * Payment system demonstrating abstract classes, interfaces, default methods.
8 */
9
10// Interfaces define contracts
11interface Validatable {
12 List<String> validate();
13 default boolean isValid() { return validate().isEmpty(); }
14}
15
16interface Auditable {
17 LocalDateTime getTimestamp();
18 default String toAuditString() {
19 return getClass().getSimpleName() + " at " + getTimestamp();
20 }
21}
22
23// Abstract class provides shared state and partial implementation
24abstract class Payment implements Validatable, Auditable {
25 protected final String id;
26 protected final double amount;
27 protected final String currency;
28 protected final LocalDateTime timestamp;
29 protected String status;
30
31 protected Payment(double amount, String currency) {
32 if (amount <= 0) throw new IllegalArgumentException("Amount must be > 0");
33 this.id = UUID.randomUUID().toString().substring(0, 8);
34 this.amount = amount;
35 this.currency = currency;
36 this.timestamp = LocalDateTime.now();
37 this.status = "PENDING";
38 }
39
40 // Abstract: each payment type has different processing logic
41 public abstract String process();
42 public abstract String getPaymentMethod();
43
44 // Concrete: shared by all payment types
45 public String getId() { return id; }
46 public double getAmount() { return amount; }
47 public String getStatus() { return status; }
48
49 @Override
50 public LocalDateTime getTimestamp() { return timestamp; }
51
52 @Override
53 public String toString() {
54 return String.format("%s{id=%s, amount=%.2f %s, status=%s}",
55 getPaymentMethod(), id, amount, currency, status);
56 }
57}
58
59class CreditCardPayment extends Payment {
60 private final String cardLast4;
61 private final String cardHolder;
62
63 public CreditCardPayment(double amount, String cardLast4, String cardHolder) {
64 super(amount, "USD");
65 this.cardLast4 = cardLast4;
66 this.cardHolder = cardHolder;
67 }
68
69 @Override
70 public String process() {
71 status = "COMPLETED";
72 return String.format("Charged $%.2f to card ending %s", amount, cardLast4);
73 }
74
75 @Override public String getPaymentMethod() { return "CreditCard"; }
76
77 @Override
78 public List<String> validate() {
79 List<String> errors = new ArrayList<>();
80 if (cardLast4.length() != 4) errors.add("Card last4 must be 4 digits");
81 if (cardHolder.isBlank()) errors.add("Card holder cannot be blank");
82 if (amount > 10_000) errors.add("Credit limit exceeded");
83 return errors;
84 }
85}
86
87class BankTransferPayment extends Payment {
88 private final String accountNumber;
89 private final String routingNumber;
90
91 public BankTransferPayment(double amount, String accountNumber, String routingNumber) {
92 super(amount, "USD");
93 this.accountNumber = accountNumber;
94 this.routingNumber = routingNumber;
95 }
96
97 @Override
98 public String process() {
99 status = amount > 5000 ? "PENDING_APPROVAL" : "COMPLETED";
100 return String.format("Bank transfer of $%.2f initiated", amount);
101 }
102
103 @Override public String getPaymentMethod() { return "BankTransfer"; }
104
105 @Override
106 public List<String> validate() {
107 List<String> errors = new ArrayList<>();
108 if (accountNumber.length() < 8) errors.add("Invalid account number");
109 if (routingNumber.length() != 9) errors.add("Routing must be 9 digits");
110 return errors;
111 }
112}
113
114class PaymentProcessor {
115 public static void processAll(List<Payment> payments) {
116 System.out.println("=== Processing Payments ===");
117 for (Payment payment : payments) { // polymorphic — works for all types
118 List<String> errors = payment.validate();
119 if (!payment.isValid()) { // default method from Validatable
120 System.out.println("INVALID: " + payment.getPaymentMethod());
121 errors.forEach(e -> System.out.println(" - " + e));
122 continue;
123 }
124 String result = payment.process(); // dynamic dispatch
125 System.out.println(result);
126 System.out.println(payment.toAuditString()); // default from Auditable
127 }
128 }
129
130 public static void main(String[] args) {
131 List<Payment> payments = new ArrayList<>();
132 payments.add(new CreditCardPayment(150.00, "4242", "Alice Smith"));
133 payments.add(new BankTransferPayment(8000.00, "123456789", "021000021"));
134 payments.add(new CreditCardPayment(15000.00, "9999", "Bob Jones")); // exceeds limit
135
136 processAll(payments);
137 }
138}This payment system illustrates when to use each abstraction mechanism: Validatable and Auditable are interfaces because they define capabilities that any class (not just Payment subclasses) might implement; Payment is an abstract class because it has shared state (id, amount, timestamp) and shared constructor logic; CreditCardPayment and BankTransferPayment override process() and getPaymentMethod() while inheriting the rest; the default isValid() and toAuditString() methods on interfaces provide useful behavior without requiring every implementor to duplicate the logic; and the PaymentProcessor.processAll() loop is completely polymorphic — adding a new payment type (e.g., CryptoPayment) requires zero changes to ProcessAll.
With abstraction mechanisms mastered, the next lesson covers exception handling: the difference between checked and unchecked exceptions, try-catch-finally blocks, multi-catch, try-with-resources for automatic resource management, and how to design meaningful custom exception hierarchies.
Your progress is saved in this browser, on this device only.