In Java, abstract classes and abstract methods offer a powerful way to define common behavior while leaving some parts incomplete—forcing subclasses to fill in the details. This approach provides a flexible blueprint for creating related classes with shared features, without allowing direct instantiation of the abstract class itself.
An abstract class is a class that cannot be instantiated directly. It can contain both concrete methods (with full implementations) and abstract methods (without implementations). Abstract classes are declared with the keyword abstract
.
public abstract class Vehicle {
// Abstract method (no implementation)
public abstract void move();
// Concrete method (implemented)
public void start() {
System.out.println("Vehicle is starting");
}
}
Because Vehicle
is abstract, you cannot create objects of type Vehicle
:
Vehicle v = new Vehicle(); // Compilation error!
An abstract method declares a method signature but no body. It tells subclasses, "You must provide your own implementation of this method."
Example:
public abstract void move();
Every concrete subclass must override and provide an implementation for all abstract methods inherited from its abstract superclass unless the subclass is also abstract.
Use abstract classes when:
If all methods are abstract and no state (fields) is needed, consider using an interface instead (covered later).
Vehicle
Class and Subclasses// Abstract superclass
public abstract class Vehicle {
public abstract void move(); // Abstract method
public void start() { // Concrete method
System.out.println("Vehicle is starting");
}
}
// Concrete subclass Car
public class Car extends Vehicle {
@Override
public void move() {
System.out.println("Car is driving on the road");
}
}
// Concrete subclass Bike
public class Bike extends Vehicle {
@Override
public void move() {
System.out.println("Bike is pedaling on the path");
}
}
public class Main {
public static void main(String[] args) {
Vehicle myCar = new Car();
Vehicle myBike = new Bike();
myCar.start();
myCar.move();
myBike.start();
myBike.move();
}
}
Output:
Vehicle is starting
Car is driving on the road
Vehicle is starting
Bike is pedaling on the path
Notice that even though myCar
and myBike
are both declared as Vehicle
references, the correct subclass implementations of move()
are called. This is polymorphism working hand-in-hand with abstraction.
abstract class Vehicle {
public abstract void move(); // Abstract method
public void start() { // Concrete method
System.out.println("Vehicle is starting");
}
}
class Car extends Vehicle {
@Override
public void move() {
System.out.println("Car is driving on the road");
}
}
class Bike extends Vehicle {
@Override
public void move() {
System.out.println("Bike is pedaling on the path");
}
}
public class Main {
public static void main(String[] args) {
Vehicle myCar = new Car();
Vehicle myBike = new Bike();
myCar.start();
myCar.move();
myBike.start();
myBike.move();
}
}
start()
) in one place.Vehicle
).Try creating your own abstract class and subclasses to practice how abstract methods enforce behavior while allowing flexible implementations.
In Java, interfaces define a contract that classes agree to follow by implementing specific methods. Unlike classes, interfaces only specify what should be done, not how to do it. This helps create flexible, loosely coupled code where multiple classes can share common behaviors without inheriting from the same superclass.
An interface is like a blueprint for classes. It declares method signatures that implementing classes must provide. This ensures consistency and allows code to rely on these common methods without needing to know the exact class details.
The syntax for defining an interface is:
public interface Flyable {
void fly(); // abstract method
}
All methods in an interface are implicitly public
and abstract (before Java 8). Classes that implement this interface must provide a concrete fly()
method.
To implement an interface, a class uses the implements
keyword:
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
The class must implement all methods declared in the interface unless the class is abstract.
Java allows a class to implement multiple interfaces, enabling the class to have diverse capabilities:
public interface Swimmable {
void swim();
}
public class Fish implements Swimmable {
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
public class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("Duck is flying");
}
@Override
public void swim() {
System.out.println("Duck is swimming");
}
}
Since Java 8, interfaces can include default methods — methods with a body that implementing classes inherit automatically:
public interface Flyable {
void fly();
default void takeOff() {
System.out.println("Taking off");
}
}
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
The Bird
class can call takeOff()
even if it doesn't override it, providing flexibility and backward compatibility when evolving interfaces.
Flyable
and Swimmable
public class Main {
public static void main(String[] args) {
Flyable bird = new Bird();
Swimmable fish = new Fish();
Duck duck = new Duck();
bird.fly(); // Bird is flying
fish.swim(); // Fish is swimming
duck.fly(); // Duck is flying
duck.swim(); // Duck is swimming
duck.takeOff(); // Taking off (from Flyable default method)
}
}
interface Swimmable {
void swim();
}
interface Flyable {
void fly();
default void takeOff() {
System.out.println("Taking off");
}
}
class Fish implements Swimmable {
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("Duck is flying");
}
@Override
public void swim() {
System.out.println("Duck is swimming");
}
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
public class Main {
public static void main(String[] args) {
Flyable bird = new Bird();
Swimmable fish = new Fish();
Duck duck = new Duck();
bird.fly(); // Bird is flying
fish.swim(); // Fish is swimming
duck.fly(); // Duck is flying
duck.swim(); // Duck is swimming
duck.takeOff(); // Taking off (from default method)
}
}
implements
to adopt an interface’s contract.Try defining your own interfaces and implementing them in different classes to see how interfaces empower polymorphism and clean architecture in Java.
In Java, both interfaces and abstract classes help define abstract types that other classes can build upon. However, they serve different purposes and have important differences. Understanding when to use each is key to designing clean, maintainable code.
Feature | Interface | Abstract Class |
---|---|---|
Multiple Inheritance | Supports multiple inheritance (a class can implement many interfaces) | Does not support multiple inheritance (a class can extend only one class) |
Method Implementation | Before Java 8, methods were abstract only; since Java 8, interfaces can have default and static methods with implementations | Can have both abstract and fully implemented methods |
Fields/State | Can only have public static final constants (no instance variables) |
Can have instance variables (state) |
Constructors | No constructors (cannot instantiate) | Can have constructors to initialize state |
Purpose | Specifies capabilities or contracts that classes agree to implement | Provides a base partial implementation with shared code |
Usage Scenario | Use when unrelated classes share common behavior, or to define capabilities like Comparable , Runnable |
Use when classes share code, fields, or default behavior and belong to the same family |
Inheritance Requirement | Classes must implement interface methods or be declared abstract | Subclasses inherit abstract methods to implement and concrete methods to reuse |
Flyable
, Serializable
).Example:
public interface Flyable {
void fly();
default void takeOff() {
System.out.println("Taking off");
}
}
Vehicle
superclass).Example:
public abstract class Vehicle {
protected int speed;
public Vehicle(int speed) {
this.speed = speed;
}
public abstract void move();
public void stop() {
System.out.println("Vehicle stopped");
}
}
Example:
abstract class Vehicle {
protected int speed;
public Vehicle(int speed) {
this.speed = speed;
}
public abstract void move();
public void stop() {
System.out.println("Vehicle stopped");
}
}
class Car extends Vehicle {
public Car(int speed) {
super(speed);
}
@Override
public void move() {
System.out.println("Car is moving at " + speed + " km/h");
}
}
class Bike extends Vehicle {
public Bike(int speed) {
super(speed);
}
@Override
public void move() {
System.out.println("Bike is cruising at " + speed + " km/h");
}
}
public class Main {
public static void main(String[] args) {
Vehicle myCar = new Car(100);
Vehicle myBike = new Bike(25);
myCar.move();
myCar.stop();
myBike.move();
myBike.stop();
}
}
Experiment with both interfaces and abstract classes in your projects. Seeing their differences firsthand will help you decide which tool fits your design goals.
Java 8 introduced a powerful new feature called lambda expressions, which bring functional programming concepts into Java. At the heart of this feature are functional interfaces—interfaces with exactly one abstract method—that enable writing concise, expressive code.
A functional interface is an interface with a single abstract method (SAM). It represents a single behavior or action, making it a perfect target for lambda expressions.
Java provides the @FunctionalInterface
annotation to explicitly declare an interface as functional, which helps the compiler catch mistakes:
@FunctionalInterface
public interface Greeting {
void sayHello(String name);
}
A lambda expression provides a clear, concise way to implement the single abstract method of a functional interface without writing an entire class or anonymous class.
General syntax:
(parameters) -> expression
or
(parameters) -> { statements; }
Runnable
Before Java 8, implementing Runnable
required an anonymous inner class:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Task running...");
}
};
new Thread(task).start();
With lambdas:
Runnable task = () -> System.out.println("Task running...");
new Thread(task).start();
This is shorter and easier to read.
Comparator
Comparator<T>
is a functional interface used to compare objects:
Anonymous class way:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
};
With lambda:
Comparator<String> comp = (s1, s2) -> s1.length() - s2.length();
Functional interfaces are key to the Java Stream API, which enables powerful data processing pipelines.
Example: Filtering and printing a list of names that start with "J":
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Jack", "Bob");
names.stream()
.filter(name -> name.startsWith("J")) // Lambda as Predicate functional interface
.forEach(name -> System.out.println(name)); // Lambda as Consumer functional interface
}
}
Output:
John
Jack
java.util.function
Java provides many built-in functional interfaces:
Interface | Abstract Method | Description |
---|---|---|
Runnable |
void run() |
Represents a task to run |
Comparator<T> |
int compare(T o1, T o2) |
Compares two objects |
Predicate<T> |
boolean test(T t) |
Tests a condition on input |
Consumer<T> |
void accept(T t) |
Performs an action on input |
Function<T,R> |
R apply(T t) |
Transforms input to output |
Supplier<T> |
T get() |
Supplies a value without input |
These interfaces are designed to be used with lambdas and method references.
Runnable
and Comparator
to get comfortable.Give it a try: rewrite your anonymous classes using lambdas and explore how functional interfaces simplify your code!