Unit III: Modern Java Features

Explore functional programming and modern language features in Java

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

FunctionalInterfaceDemo.java
// 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
BuiltInFunctionalInterfaces.java
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

Lambda 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
LambdaDemo.java
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

VariableCapture.java
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
MethodReferenceDemo.java
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

Stream Operations

Intermediate Operations Terminal Operations
filter(), map(), sorted() forEach(), collect(), reduce()
distinct(), limit(), skip() count(), min(), max()
flatMap(), peek() findFirst(), findAny(), anyMatch()
StreamDemo.java
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

CollectorsDemo.java
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.

DefaultStaticDemo.java
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

DiamondProblem.java
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.

Base64Demo.java
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.

ForEachDemo.java
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.

TryWithResourcesDemo.java
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.

Common Annotations
@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

VarDemo.java
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)

SwitchExpressions.java
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)

TextBlocks.java
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)

RecordsDemo.java
// 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)

SealedClassesDemo.java
// 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.