☕ Java
Interface
An interface is a pure contract — a named set of method signatures that any implementing class promises to support. Interfaces decouple callers from implementations, enable multiple type adoption, and define the roles and capabilities that classes can take on independently of their class hierarchy. Java 8 enriched interfaces with default methods, static methods, and private methods, making them far more powerful while keeping the core contract-definition purpose unchanged. This entry covers interface declaration and implementation, the complete interface member taxonomy, default methods and their purpose, functional interfaces, interface constants, and the design principles that make interfaces effective.
Interface Declaration and Implementation
An interface is declared with the interface keyword. All methods in an interface are implicitly public and abstract unless they are declared default or static. All fields are implicitly public, static, and final — that is, they are constants. A class adopts an interface with the implements keyword and must provide a concrete body for every abstract method in the interface, or must itself be declared abstract.
The implements keyword creates a type relationship: a class that implements Comparable is a Comparable. This type membership is what makes interfaces so powerful — unlike extends, a class can implement any number of interfaces simultaneously. A Dog can be an Animal (class hierarchy), a Comparable<Dog>, a Serializable, and a Cloneable all at once. Each interface represents a different role or capability, and the class commits to all of them.
The interface's methods are the contract that callers depend on. Any class that implements the interface is substitutable wherever the interface type is expected. This substitutability is what enables the dependency inversion principle: code depends on the interface (the abstraction) not on the specific implementing class (the detail). The result is a system where implementations can be swapped — for testing, for different environments, for improved performance — without touching the dependent code.
Java
// ── Interface declaration ─────────────────────────────────────────────
public interface PaymentProcessor {
// ── Abstract methods — all implicitly public abstract ─────────────
PaymentResult charge(PaymentRequest request);
PaymentResult refund(String transactionId, BigDecimal amount);
PaymentStatus getStatus(String transactionId);
// ── Constants — all implicitly public static final ────────────────
int MAX_AMOUNT_CENTS = 1_000_000_00; // $1,000,000.00
String DEFAULT_CURRENCY = "USD";
int TRANSACTION_ID_LENGTH = 36;
}
// ── Implementing the interface ─────────────────────────────────────────
public class StripePaymentProcessor implements PaymentProcessor {
private final StripeClient stripeClient;
public StripePaymentProcessor(StripeClient client) {
this.stripeClient = client;
}
@Override
public PaymentResult charge(PaymentRequest request) {
validateAmount(request.getAmountCents());
Charge charge = stripeClient.charges().create(
ChargeParams.builder()
.amount(request.getAmountCents())
.currency(request.getCurrency())
.source(request.getCardToken())
.build());
return PaymentResult.success(charge.getId());
}
@Override
public PaymentResult refund(String txId, BigDecimal amount) {
Refund refund = stripeClient.refunds().create(
RefundParams.builder()
.charge(txId)
.amount(amount.movePointRight(2).longValue())
.build());
return PaymentResult.success(refund.getId());
}
@Override
public PaymentStatus getStatus(String txId) {
Charge charge = stripeClient.charges().retrieve(txId);
return PaymentStatus.fromStripeStatus(charge.getStatus());
}
private void validateAmount(long cents) {
if (cents <= 0 || cents > PaymentProcessor.MAX_AMOUNT_CENTS)
throw new IllegalArgumentException("Invalid amount: " + cents);
}
}
// ── Multiple interfaces simultaneously ───────────────────────────────
public class PayPalProcessor
implements PaymentProcessor, Auditable, Closeable {
@Override public PaymentResult charge(PaymentRequest r) { /* PayPal API */ return null; }
@Override public PaymentResult refund(String id, BigDecimal a) { return null; }
@Override public PaymentStatus getStatus(String id) { return null; }
@Override public String getAuditDescription() { return "PayPal processor"; }
@Override public Instant getLastModified() { return Instant.now(); }
@Override public String getModifiedBy() { return "system"; }
@Override public void close() { /* release PayPal resources */ }
}Default Methods — Behaviour in Interfaces
Java 8 introduced default methods — methods with a body declared with the default keyword inside an interface. Default methods exist for a specific purpose: evolving existing interfaces without breaking all implementing classes. Before default methods, adding a new method to an interface was a breaking change — every class implementing the interface would fail to compile until it provided an implementation. Default methods allow the interface to provide a fallback implementation that existing classes inherit automatically.
Default methods also enable interface composition. Interfaces can provide utility methods built on top of the abstract methods they define. Comparable's compareTo() is the abstract method; default methods like isGreaterThan(), isLessThan() can be built on top of it without the implementor needing to write them. The Collection interface's stream(), forEach(), and removeIf() are all default methods that were added in Java 8 without breaking the millions of Collection implementations that existed.
When a class implements two interfaces that both provide a default method with the same signature, the class must override the method to resolve the ambiguity — the compiler forces this. When a class extends an abstract class and implements an interface that both provide a method with the same signature, the class's method wins; if the class does not override, the abstract class's method wins (class beats interface).
Java
// ── Default methods — interface with behaviour ───────────────────────
public interface Validator<T> {
// ── Abstract — implementing class defines the rule ─────────────────
boolean isValid(T value);
// ── Default — built on top of the abstract method ─────────────────
default void validate(T value) {
if (!isValid(value)) {
throw new ValidationException(
"Validation failed for: " + value);
}
}
default Validator<T> and(Validator<T> other) {
return value -> this.isValid(value) && other.isValid(value);
}
default Validator<T> or(Validator<T> other) {
return value -> this.isValid(value) || other.isValid(value);
}
default Validator<T> negate() {
return value -> !this.isValid(value);
}
}
// ── Implementing classes only write the rule — get utilities free ─────
public class EmailValidator implements Validator<String> {
@Override
public boolean isValid(String email) {
return email != null && email.matches(
"^[\w.+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$");
}
}
public class LengthValidator implements Validator<String> {
private final int min, max;
public LengthValidator(int min, int max) {
this.min = min; this.max = max;
}
@Override
public boolean isValid(String s) {
return s != null && s.length() >= min && s.length() <= max;
}
}
// ── Composing validators with default methods ─────────────────────────
Validator<String> emailValidator = new EmailValidator();
Validator<String> lengthValidator = new LengthValidator(5, 100);
Validator<String> strictEmail = emailValidator.and(lengthValidator);
strictEmail.validate("alice@example.com"); // passes
strictEmail.validate("a@b"); // throws — too short
// ── Ambiguity resolution when two interfaces have same default ────────
interface A { default String hello() { return "Hello from A"; } }
interface B { default String hello() { return "Hello from B"; } }
class C implements A, B {
@Override
public String hello() {
// MUST override — compiler error otherwise
return A.super.hello() + " and " + B.super.hello();
// or just: return "Hello from C";
}
}Functional Interfaces and Lambda Expressions
A functional interface is an interface with exactly one abstract method. This single-abstract-method contract is what makes an interface a target type for lambda expressions and method references. The @FunctionalInterface annotation declares the intent and causes the compiler to verify the single-abstract-method constraint — a useful guard against accidentally adding a second abstract method and breaking all lambda usages.
Java's java.util.function package provides a comprehensive set of general-purpose functional interfaces: Function<T,R> transforms T to R; Predicate<T> tests T and returns boolean; Consumer<T> accepts T and returns nothing; Supplier<T> takes nothing and returns T; BiFunction<T,U,R> takes two inputs; UnaryOperator<T> transforms T to T; and more. These cover the vast majority of functional patterns without needing custom interfaces.
The power of functional interfaces is that they allow behaviour to be treated as data — passed as arguments, returned from methods, stored in variables, and composed using default methods. Code that previously required an anonymous inner class can be written as a lambda in a fraction of the characters. More importantly, the code communicates intent rather than plumbing: Comparator.comparing(Person::getAge).reversed() reads like a specification; an anonymous Comparator implementation reads like infrastructure.
Java
// ── @FunctionalInterface — one abstract method ───────────────────────
@FunctionalInterface
public interface PriceRule {
BigDecimal apply(BigDecimal basePrice, Customer customer);
// Default methods do NOT count against the single abstract method
default PriceRule andThen(PriceRule next) {
return (price, customer) ->
next.apply(this.apply(price, customer), customer);
}
}
// ── Lambda expression implements PriceRule ────────────────────────────
PriceRule vatRule = (price, cust) ->
price.multiply(BigDecimal.valueOf(1.20)); // +20% VAT
PriceRule loyaltyRule = (price, cust) ->
cust.isLoyaltyMember()
? price.multiply(BigDecimal.valueOf(0.90)) // -10% discount
: price;
PriceRule premiumRule = (price, cust) ->
cust.isPremium()
? price.multiply(BigDecimal.valueOf(0.85)) // -15% discount
: price;
// ── Compose rules using the default method ────────────────────────────
PriceRule fullPricing = vatRule
.andThen(loyaltyRule)
.andThen(premiumRule);
BigDecimal finalPrice = fullPricing.apply(
new BigDecimal("100.00"), customer);
// ── Standard functional interfaces from java.util.function ───────────
Function<String, Integer> strLen = String::length;
Predicate<String> isEmpty = String::isEmpty;
Consumer<String> printer = System.out::println;
Supplier<List<String>> listFactory = ArrayList::new;
BiFunction<String,String,String> concat = String::concat;
UnaryOperator<String> upper = String::toUpperCase;
Comparator<String> byLen = Comparator.comparingInt(String::length);
// ── Method reference forms ────────────────────────────────────────────
// Static method: ClassName::staticMethod
Function<String, Integer> parseInt = Integer::parseInt;
// Instance method on type: ClassName::instanceMethod
Function<String, String> trim = String::trim;
// Instance method on instance: instance::method
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
// Constructor: ClassName::new
Function<String, StringBuilder> sbFactory = StringBuilder::new;Interface Segregation and Good Interface Design
The Interface Segregation Principle states that no class should be forced to implement methods it does not use. Fat interfaces — interfaces with many methods that not all implementors need — force implementors to provide empty or throw-UnsupportedOperationException implementations for the irrelevant methods. These are design signals that the interface is trying to be too many things at once.
The right size for an interface is the minimum set of methods that form a coherent, meaningful contract. If some classes need all those methods and others need only a subset, the interface should be split into smaller, more focused interfaces. Java's own Collections framework demonstrates this: Collection, List, Set, Queue, and Deque each define a specific role. A class implementing List is not forced to implement Queue operations.
Well-designed interfaces are also stable — they change rarely. Because every implementing class must be updated when an interface changes, frequent interface changes create significant maintenance burden. Default methods help here by allowing additions without breaking existing implementations, but the best design keeps the abstract method count low and changes it rarely. Interfaces are contracts: be conservative about what you commit to.
Java
// ── Fat interface — violates Interface Segregation ──────────────────
public interface Animal {
String makeSound();
void fly(); // What about dogs and fish?
void swim(); // What about cats and birds?
void run();
void climb(); // What about fish and birds?
}
// ── Many implementors must provide throw stubs ────────────────────────
public class Dog implements Animal {
@Override public String makeSound() { return "Woof"; }
@Override public void run() { System.out.println("runs"); }
@Override public void fly() { throw new UnsupportedOperationException(); }
@Override public void swim() { throw new UnsupportedOperationException(); }
@Override public void climb(){ throw new UnsupportedOperationException(); }
}
// ── Segregated interfaces — each class implements only what it does ────
public interface SoundMaking { String makeSound(); }
public interface Flying { void fly(); }
public interface Swimming { void swim(); }
public interface Running { void run(); }
public interface Climbing { void climb(); }
// ── Classes implement only relevant capabilities ──────────────────────
public class Dog implements SoundMaking, Running, Swimming {
@Override public String makeSound() { return "Woof"; }
@Override public void run() { System.out.println("runs on 4 legs"); }
@Override public void swim() { System.out.println("paddles"); }
// Does not implement Flying or Climbing — not forced to
}
public class Eagle implements SoundMaking, Flying, Swimming {
@Override public String makeSound() { return "Screech"; }
@Override public void fly() { System.out.println("soars"); }
@Override public void swim() { System.out.println("wades"); }
}
public class Fish implements SoundMaking, Swimming {
@Override public String makeSound() { return "(silent)"; }
@Override public void swim() { System.out.println("glides"); }
}
// ── Code that works with specific capabilities ────────────────────────
public class PetShelter {
private final List<SoundMaking> residents = new ArrayList<>();
public void addResident(SoundMaking animal) {
residents.add(animal);
}
public void morningCallout() {
for (SoundMaking animal : residents) {
System.out.println(animal.makeSound());
}
}
}
// ── Stable, minimal interface — the ideal ────────────────────────────
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void deleteById(ID id);
boolean existsById(ID id);
long count();
}
// Six methods, all related, all needed by every repository — cohesiveRelated Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.