Functional Interfaces
Definition
A functional interface is an interface that contains exactly one abstract method. It can have any number of default or static methods. Functional interfaces are the foundation for lambda expressions in Java.
Key Points
- Contains exactly one abstract method
- Can have multiple default and static methods
- Annotated with
@FunctionalInterface(optional but recommended) - Can be used as the target for lambda expressions
- Introduced in Java 8
// Custom functional interface
@FunctionalInterface
interface Calculator {
int calculate(int a, int b); // Single abstract method
// Default method (allowed)
default void printResult(int result) {
System.out.println("Result: " + result);
}
// Static method (allowed)
static void info() {
System.out.println("Calculator Interface");
}
}
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
// Using lambda expression
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;
Calculator multiply = (a, b) -> a * b;
System.out.println("5 + 3 = " + add.calculate(5, 3));
System.out.println("5 - 3 = " + subtract.calculate(5, 3));
System.out.println("5 * 3 = " + multiply.calculate(5, 3));
// Using default and static methods
add.printResult(add.calculate(10, 20));
Calculator.info();
}
}
Built-in Functional Interfaces (java.util.function)
| Interface | Method | Description |
|---|---|---|
| Predicate<T> | boolean test(T t) | Tests a condition, returns boolean |
| Function<T, R> | R apply(T t) | Transforms T to R |
| Consumer<T> | void accept(T t) | Consumes T, returns nothing |
| Supplier<T> | T get() | Supplies T, takes no input |
| BiFunction<T, U, R> | R apply(T t, U u) | Takes two inputs, returns R |
| UnaryOperator<T> | T apply(T t) | Same input and output type |
| BinaryOperator<T> | T apply(T t1, T t2) | Two inputs of same type, same output |
import java.util.function.*;
public class BuiltInFunctionalInterfaces {
public static void main(String[] args) {
// Predicate - tests a condition
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println("Is 4 even? " + isEven.test(4)); // true
// Function - transforms input to output
Function<String, Integer> length = s -> s.length();
System.out.println("Length: " + length.apply("Hello")); // 5
// Consumer - consumes input, no output
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello, Consumer!");
// Supplier - supplies output, no input
Supplier<Double> random = () -> Math.random();
System.out.println("Random: " + random.get());
// BiFunction - two inputs, one output
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println("Sum: " + add.apply(5, 3)); // 8
// UnaryOperator - same type input/output
UnaryOperator<Integer> square = n -> n * n;
System.out.println("Square: " + square.apply(5)); // 25
}
}
Exam Tip
The @FunctionalInterface annotation is optional but recommended. It causes a compile error if the interface has more than one abstract method.
Lambda Expressions
Definition
A lambda expression is a concise way to represent an anonymous function that can be passed as an argument or stored in a variable. It provides a clear and compact syntax for implementing functional interfaces.
Syntax
// Basic syntax
(parameters) -> expression
// With block body
(parameters) -> {
statements;
return value;
}
// Examples
() -> 42 // No parameters
x -> x * x // Single parameter (no parentheses needed)
(x, y) -> x + y // Multiple parameters
(String s) -> s.length() // Explicit parameter type
(x, y) -> { return x + y; } // Block body with return
import java.util.*;
public class LambdaDemo {
public static void main(String[] args) {
// Without lambda (anonymous class)
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello from anonymous class");
}
};
// With lambda
Runnable r2 = () -> System.out.println("Hello from lambda");
r1.run();
r2.run();
// Sorting with lambda
List<String> names = Arrays.asList("John", "Alice", "Bob");
// Without lambda
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
// With lambda
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
// Even simpler with method reference
Collections.sort(names, String::compareTo);
System.out.println(names);
}
}
Variable Capture
public class VariableCapture {
public static void main(String[] args) {
// Variables used in lambda must be effectively final
int multiplier = 5; // effectively final
Function<Integer, Integer> multiply = n -> n * multiplier;
System.out.println(multiply.apply(10)); // 50
// This would cause error:
// multiplier = 10; // Cannot modify - not effectively final
}
}
Note
Lambda expressions can access variables from the enclosing scope, but those variables must be effectively final (not modified after initialization).
Method References
Definition
A method reference is a shorthand notation for a lambda expression that calls an existing method. It uses the :: operator to separate the class or object name from the method name.
Types of Method References
| Type | Syntax | Example |
|---|---|---|
| Static method | ClassName::staticMethod | Math::max |
| Instance method of object | object::instanceMethod | str::toUpperCase |
| Instance method of class | ClassName::instanceMethod | String::length |
| Constructor | ClassName::new | ArrayList::new |
import java.util.*;
import java.util.function.*;
public class MethodReferenceDemo {
public static void main(String[] args) {
// 1. Static method reference
// Lambda: (a, b) -> Math.max(a, b)
BiFunction<Integer, Integer, Integer> max = Math::max;
System.out.println("Max: " + max.apply(5, 10)); // 10
// 2. Instance method of particular object
String str = "Hello";
// Lambda: () -> str.toUpperCase()
Supplier<String> upper = str::toUpperCase;
System.out.println(upper.get()); // HELLO
// 3. Instance method of class type
// Lambda: s -> s.length()
Function<String, Integer> length = String::length;
System.out.println("Length: " + length.apply("Java")); // 4
// 4. Constructor reference
// Lambda: () -> new ArrayList()
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
// Practical example with forEach
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda
names.forEach(name -> System.out.println(name));
// Method reference
names.forEach(System.out::println);
}
// Custom static method
public static boolean isEven(int n) {
return n % 2 == 0;
}
}
Stream API
Definition
The Stream API, introduced in Java 8, is used to process collections of objects in a functional style. A stream is a sequence of elements that supports sequential and parallel aggregate operations.
Key Points
- Streams do not store elements; they carry values from a source
- Operations on streams are lazy; computation happens when terminal operation is invoked
- Streams can be processed only once
- Streams support functional-style operations
Stream Operations
| Intermediate Operations | Terminal Operations |
|---|---|
| filter(), map(), sorted() | forEach(), collect(), reduce() |
| distinct(), limit(), skip() | count(), min(), max() |
| flatMap(), peek() | findFirst(), findAny(), anyMatch() |
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter - keep elements matching condition
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Even: " + evenNumbers); // [2, 4, 6, 8, 10]
// map - transform elements
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("Squares: " + squares);
// reduce - combine elements
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum); // 55
// Chaining operations
int sumOfEvenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // Keep even numbers
.map(n -> n * n) // Square them
.reduce(0, Integer::sum); // Sum them
System.out.println("Sum of even squares: " + sumOfEvenSquares); // 220
// sorted, distinct, limit
List<Integer> nums = Arrays.asList(5, 3, 8, 3, 1, 9, 5);
List<Integer> result = nums.stream()
.distinct() // Remove duplicates
.sorted() // Sort
.limit(4) // Take first 4
.collect(Collectors.toList());
System.out.println("Result: " + result); // [1, 3, 5, 8]
}
}
Collectors
import java.util.*;
import java.util.stream.*;
public class CollectorsDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// Collect to List
List<String> upperList = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// Collect to Set
Set<String> upperSet = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toSet());
// Join strings
String joined = names.stream()
.collect(Collectors.joining(", "));
System.out.println("Joined: " + joined); // Alice, Bob, Charlie, David
// Counting
long count = names.stream()
.filter(n -> n.length() > 3)
.count();
System.out.println("Count: " + count);
// Grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("Grouped by length: " + byLength);
// Partitioning (split into two groups)
Map<Boolean, List<String>> partitioned = names.stream()
.collect(Collectors.partitioningBy(n -> n.length() > 4));
System.out.println("Partitioned: " + partitioned);
}
}
Exam Tip
Remember: Intermediate operations (filter, map, sorted) return a stream and are lazy. Terminal operations (collect, forEach, reduce) produce a result and trigger processing.
Default and Static Methods in Interfaces
Definition
Java 8 introduced default and static methods in interfaces. Default methods provide a default implementation that implementing classes can override. Static methods belong to the interface and cannot be overridden.
interface Vehicle {
// Abstract method (must be implemented)
void start();
// Default method (has implementation)
default void honk() {
System.out.println("Honking...");
}
// Static method (belongs to interface)
static void description() {
System.out.println("This is a vehicle interface");
}
}
class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starting...");
}
// Optional: Override default method
@Override
public void honk() {
System.out.println("Car honking: Beep Beep!");
}
}
class Bike implements Vehicle {
@Override
public void start() {
System.out.println("Bike starting...");
}
// Uses default honk() implementation
}
public class DefaultStaticDemo {
public static void main(String[] args) {
Car car = new Car();
car.start();
car.honk(); // Overridden
Bike bike = new Bike();
bike.start();
bike.honk(); // Default implementation
// Static method called on interface
Vehicle.description();
}
}
Diamond Problem Resolution
interface A {
default void display() {
System.out.println("A");
}
}
interface B {
default void display() {
System.out.println("B");
}
}
// Class must override to resolve conflict
class C implements A, B {
@Override
public void display() {
// Can call specific interface's method
A.super.display(); // Calls A's display
B.super.display(); // Calls B's display
System.out.println("C");
}
}
Base64 Encoding and Decoding
Definition
Base64 is a binary-to-text encoding scheme that represents binary data in an ASCII string format. Java 8 introduced the java.util.Base64 class for encoding and decoding.
import java.util.Base64;
public class Base64Demo {
public static void main(String[] args) {
String original = "Hello, Java Base64!";
// Basic encoding
String encoded = Base64.getEncoder()
.encodeToString(original.getBytes());
System.out.println("Encoded: " + encoded);
// Basic decoding
byte[] decodedBytes = Base64.getDecoder().decode(encoded);
String decoded = new String(decodedBytes);
System.out.println("Decoded: " + decoded);
// URL-safe encoding (uses - and _ instead of + and /)
String urlEncoded = Base64.getUrlEncoder()
.encodeToString(original.getBytes());
System.out.println("URL Encoded: " + urlEncoded);
// MIME encoding (for email, with line breaks)
String mimeEncoded = Base64.getMimeEncoder()
.encodeToString(original.getBytes());
System.out.println("MIME Encoded: " + mimeEncoded);
}
}
forEach Method
Definition
The forEach method is a default method in the Iterable interface that performs an action for each element. It accepts a Consumer functional interface.
import java.util.*;
public class ForEachDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Traditional for loop
for (String name : names) {
System.out.println(name);
}
// forEach with lambda
names.forEach(name -> System.out.println(name));
// forEach with method reference
names.forEach(System.out::println);
// forEach with Map
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
map.forEach((key, value) ->
System.out.println(key + " = " + value));
}
}
try-with-resources Statement
Definition
The try-with-resources statement (introduced in Java 7) automatically closes resources that implement the AutoCloseable interface when the try block exits.
import java.io.*;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// Without try-with-resources (old way)
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// With try-with-resources (recommended)
try (BufferedReader reader = new BufferedReader(
new FileReader("file.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// reader is automatically closed
// Multiple resources
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Note
Resources declared in try-with-resources must implement AutoCloseable or Closeable interface. They are closed in reverse order of their creation.
Modern Java Features
Annotations (Introduction)
Definition: Annotations provide metadata about the program. They do not directly affect program semantics but can be used by the compiler and runtime.
@Override // Indicates method overrides superclass method
@Deprecated // Marks element as deprecated
@SuppressWarnings("unchecked") // Suppresses compiler warnings
@FunctionalInterface // Marks interface as functional
Local Variable Type Inference (var) - Java 10
public class VarDemo {
public static void main(String[] args) {
// Type is inferred from the right-hand side
var message = "Hello"; // String
var number = 42; // int
var decimal = 3.14; // double
var list = new ArrayList<String>(); // ArrayList<String>
// Works in for loops
for (var i = 0; i < 5; i++) {
System.out.println(i);
}
// Enhanced for loop
var names = List.of("A", "B", "C");
for (var name : names) {
System.out.println(name);
}
// Cannot use var for:
// - Fields
// - Method parameters
// - Method return types
// - Without initialization
}
}
Switch Expressions (Java 14)
public class SwitchExpressions {
public static void main(String[] args) {
int day = 3;
// Traditional switch
String dayName;
switch (day) {
case 1: dayName = "Monday"; break;
case 2: dayName = "Tuesday"; break;
case 3: dayName = "Wednesday"; break;
default: dayName = "Unknown";
}
// Switch expression with arrow syntax
String dayType = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Invalid";
};
// Switch expression with yield
String result = switch (day) {
case 1 -> {
System.out.println("Monday logic");
yield "Start of week";
}
case 5 -> {
System.out.println("Friday logic");
yield "End of work week";
}
default -> "Regular day";
};
}
}
Text Blocks (Java 15)
public class TextBlocks {
public static void main(String[] args) {
// Traditional multi-line string
String html1 = "<html>\n" +
" <body>\n" +
" <p>Hello</p>\n" +
" </body>\n" +
"</html>";
// Text block (Java 15+)
String html2 = """
<html>
<body>
<p>Hello</p>
</body>
</html>
""";
// JSON example
String json = """
{
"name": "John",
"age": 30,
"city": "New York"
}
""";
System.out.println(json);
}
}
Records (Java 16)
// Record - immutable data class
record Person(String name, int age) {
// Compact constructor for validation
public Person {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
// Additional method
public String greeting() {
return "Hello, I am " + name;
}
}
public class RecordsDemo {
public static void main(String[] args) {
Person p = new Person("John", 25);
// Automatic getter methods (no 'get' prefix)
System.out.println(p.name()); // John
System.out.println(p.age()); // 25
// Automatic toString, equals, hashCode
System.out.println(p); // Person[name=John, age=25]
Person p2 = new Person("John", 25);
System.out.println(p.equals(p2)); // true
// Custom method
System.out.println(p.greeting());
}
}
Sealed Classes (Java 17)
// Sealed class - restricts which classes can extend it
sealed class Shape permits Circle, Rectangle, Triangle {
abstract double area();
}
final class Circle extends Shape {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
final class Rectangle extends Shape {
private final double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
}
non-sealed class Triangle extends Shape {
private final double base, height;
Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
double area() {
return 0.5 * base * height;
}
}
// Usage
public class SealedClassesDemo {
public static void main(String[] args) {
Shape circle = new Circle(5);
System.out.println("Circle area: " + circle.area());
}
}
Exam Tip
Records are immutable data carriers with automatic constructors, getters, equals(), hashCode(), and toString(). Sealed classes control inheritance hierarchy using permits keyword.