Types of Interfaces in Java
Java provides several types of interfaces to define behavior that classes can implement. These include marker interfaces, functional interfaces, and regular interfaces. Each serves a specific purpose and is used in different scenarios.
1. Marker Interface
- Definition:
A marker interface is an empty interface (no methods or fields) used to convey metadata or tagging information to the JVM or compiler. - Purpose:
Marker interfaces indicate that a class possesses a specific property. - Examples:
java.io.Serializablejava.lang.Cloneablejava.util.RandomAccess
How Marker Interfaces Work
- The JVM or libraries check for the presence of the marker interface using the
instanceofoperator or reflection.
Example:
import java.io.*;
class Employee implements Serializable {
private String name;
private int id;
public Employee(String name, int id) {
this.name = name;
this.id = id;
}
}
In this example, Employee can be serialized because it implements the Serializable marker interface.
2. Functional Interface
- Definition:
A functional interface is an interface with a single abstract method (SAM). It can have default and static methods but only one abstract method. - Purpose:
Used as the target for lambda expressions or method references. - Annotation:
Marked with@FunctionalInterfaceto ensure it has exactly one abstract method. - Examples:
java.util.function.Predicatejava.util.function.Functionjava.lang.Runnable
Example:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
System.out.println("Sum: " + add.calculate(10, 20));
}
}
Chaining Functional Interfaces
Functional interfaces can be combined for complex operations.
- Predicate Chaining:
Predicate<Integer> isEven = num -> num % 2 == 0; Predicate<Integer> isPositive = num -> num > 0; Predicate<Integer> isEvenAndPositive = isEven.and(isPositive); System.out.println(isEvenAndPositive.test(4)); // true
- Function Chaining:
Function<Integer, Integer> multiplyBy2 = num -> num * 2; Function<Integer, Integer> square = num -> num * num; Function<Integer, Integer> combined = multiplyBy2.andThen(square); System.out.println(combined.apply(3)); // 36 (3 * 2 = 6, 6^2 = 36)
- Consumer Chaining:
Consumer<String> greet = name -> System.out.print("Hello, " + name);
Consumer<String> end = name -> System.out.println("! Welcome.");
Consumer<String> combinedConsumer = greet.andThen(end);
combinedConsumer.accept("Alice"); // Hello, Alice! Welcome.
3. Regular Interfaces
- Definition:
Interfaces that can contain multiple abstract methods, default methods, and static methods. - Purpose:
Define a contract that implementing classes must follow.
Example:
interface Animal {
void sound();
default void eat() {
System.out.println("Eating food");
}
}
class Dog implements Animal {
@Override
public void sound() {
System.out.println("Barks");
}
}
public class RegularInterfaceDemo {
public static void main(String[] args) {
Animal dog = new Dog();
dog.sound();
dog.eat();
}
}
4. Types of Functional Interfaces in java.util.function Package
- Predicate:
- Represents a boolean-valued function of one argument.
- Methods:
boolean test(T t): Evaluates this predicate on the given argument.default Predicate<T> and(Predicate<? super T> other): Returns a composed predicate that represents a short-circuiting logical AND of this predicate and another.default Predicate<T> or(Predicate<? super T> other): Returns a composed predicate that represents a short-circuiting logical OR of this predicate and another.default Predicate<T> negate(): Returns a predicate that represents the logical negation of this predicate.static <T> Predicate<T> isEqual(Object targetRef): Returns a predicate that tests if two arguments are equal.
- Example:
Predicate<Integer> isEven = num -> num % 2 == 0; Predicate<Integer> isPositive = num -> num > 0; Predicate<Integer> isEvenAndPositive = isEven.and(isPositive).negate(); System.out.println(isEvenAndPositive.test(4)); // false
- BiPredicate:
- Represents a boolean-valued function of two arguments.
- Methods:
boolean test(T t, U u): Evaluates this predicate on the given arguments.default BiPredicate<T,U> and(BiPredicate<? super T,? super U> other): Returns a composed predicate that represents a short-circuiting logical AND of this predicate and another.default BiPredicate<T,U> or(BiPredicate<? super T,? super U> other): Returns a composed predicate that represents a short-circuiting logical OR of this predicate and another.default BiPredicate<T,U> negate(): Returns a predicate that represents the logical negation of this predicate.
- Example:
BiPredicate<String, String> startsWith = (str, prefix) -> str.startsWith(prefix);
BiPredicate<String, String> endsWith = (str, suffix) -> str.endsWith(suffix);
BiPredicate<String, String> startsAndEnds = startsWith.and(endsWith);
System.out.println(startsAndEnds.test("hello", "h")); // false
- Function:
- Represents a function that takes one argument and produces a result.
- Methods:
R apply(T t): Applies this function to the given argument.default <V> Function<T,V> andThen(Function<? super R,? extends V> after): Returns a composed function that first applies this function and then applies another.default <V> Function<V,R> compose(Function<? super V,? extends T> before): Returns a composed function that first applies another function and then applies this function.
- Example:
Function<Integer, Integer> multiplyBy2 = num -> num * 2; Function<Integer, Integer> square = num -> num * num; Function<Integer, Integer> combined = multiplyBy2.andThen(square); System.out.println(combined.apply(3)); // 36
- Consumer:
- Represents an operation that accepts a single input and returns no result.
- Methods:
void accept(T t): Performs this operation on the given argument.default Consumer<T> andThen(Consumer<? super T> after): Returns a composed Consumer that performs, in sequence, this operation followed by another.
- Example:
Consumer<String> print = str -> System.out.print("Hello, " + str);
Consumer<String> exclaim = str -> System.out.println("! Welcome.");
Consumer<String> combined = print.andThen(exclaim);
combined.accept("Alice"); // Hello, Alice! Welcome.
- Supplier:
- Represents a supplier of results with no input.
- Methods:
T get(): Gets a result.
- Example:
Supplier<Double> randomValue = () -> Math.random(); System.out.println(randomValue.get());
- BiFunction:
- Represents a function that takes two arguments and produces a result.
- Methods:
R apply(T t, U u): Applies this function to the given arguments.default <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after): Returns a composed function that first applies this function and then applies another.
- Example:
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; System.out.println(multiply.apply(3, 4)); // 12
5. Differences Between Marker, Functional, and Regular Interfaces
| Feature | Marker Interface | Functional Interface | Regular Interface |
|---|---|---|---|
| Abstract Methods | None | Exactly one | One or more |
| Purpose | Provides metadata | Target for lambdas | Defines a contract |
| Examples | Serializable, Cloneable | Runnable, Callable | List, Map, Set |
Conclusion
Understanding the different types of interfaces in Java is essential for effective programming. Marker interfaces provide metadata, functional interfaces simplify functional programming with lambdas and chaining, and regular interfaces define contracts for implementing classes. Leveraging these appropriately can lead to cleaner, more efficient code.