Q21.

What is exception handling in Java?

Q22.

What is the difference between checked and unchecked exceptions?

Q23.

What is try-with-resources?

Q24.

Explain garbage collection in Java.

Q25.

What is the difference between heap and stack memory?

Q26.

What are Java 8 features?

Q27.

Explain lambda expressions in Java.

Q28.

What is Stream API in Java?

The Java Stream API, introduced in Java 8, provides a powerful and functional approach to process collections of objects. It enables developers to perform complex data manipulation operations in a more declarative, concise, and often parallelizable manner.

What is the Stream API?

The Java Stream API is not a data structure itself; instead, it's a sequence of elements that supports sequential and parallel aggregate operations. Streams work with data sources like collections, arrays, or I/O channels, allowing operations like filtering, mapping, and reducing without modifying the source. It facilitates a functional programming style for processing data.

Key Characteristics

  • Functional in nature: Operations are expressed as lambda expressions or method references.
  • Laziness: Intermediate operations are not executed until a terminal operation is invoked.
  • Pipelining: Multiple intermediate operations can be chained together to form a pipeline.
  • No storage: Streams do not store elements; they are computed on demand from a source.
  • Single-use: A stream can be traversed only once; attempting to reuse it will throw an IllegalStateException.
  • Non-interfering: Stream operations do not modify the underlying data source.
  • Terminal vs. Intermediate operations: Operations are categorized into intermediate (return a new stream) and terminal (produce a result or a side-effect).

Stream Operations

Intermediate Operations

These operations transform a stream into another stream. They are always lazy, meaning they are not executed until a terminal operation is invoked. Multiple intermediate operations can be chained together to form a pipeline.

  • filter(Predicate<T>): Selects elements that match a given predicate.
  • map(Function<T, R>): Transforms each element of the stream by applying a function.
  • flatMap(Function<T, Stream<R>>): Transforms each element into a stream of zero or more other elements, then flattens the resulting streams into a single stream.
  • distinct(): Returns a stream consisting of the distinct elements.
  • sorted() / sorted(Comparator<T>): Returns a stream consisting of the elements sorted according to natural order or a provided Comparator.
  • limit(long maxSize): Returns a stream consisting of the elements of this stream, truncated to be no longer than maxSize.
  • skip(long n): Returns a stream consisting of the remaining elements after discarding the first n elements.

Terminal Operations

These operations produce a non-stream result or a side-effect, and terminate the stream pipeline. Once a terminal operation is performed, the stream is consumed and cannot be reused.

  • forEach(Consumer<T>): Performs an action for each element of this stream.
  • collect(Collector<T, A, R>): Accumulates the elements into a Collection or summary result.
  • reduce(BinaryOperator<T>): Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the result.
  • count(): Returns the count of elements in this stream.
  • min(Comparator<T>) / max(Comparator<T>): Returns an Optional describing the minimum/maximum element of this stream according to the provided Comparator.
  • anyMatch(Predicate<T>), allMatch(Predicate<T>), noneMatch(Predicate<T>): Return a boolean indicating if any, all, or none of the elements match the given predicate.
  • findFirst() / findAny(): Returns an Optional describing the first element of this stream, or any element of this stream, respectively.

Example Usage

Let's consider an example where we filter a list of numbers, double the even ones, and then collect them into a new list.

java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Filter even numbers, double them, and collect into a new list
        List<Integer> evenDoubledNumbers = numbers.stream()
                                                .filter(n -> n % 2 == 0) // Intermediate operation
                                                .map(n -> n * 2)       // Intermediate operation
                                                .collect(Collectors.toList()); // Terminal operation

        System.out.println(evenDoubledNumbers); // Output: [4, 8, 12, 16, 20]
    }
}

Benefits of Stream API

  • Concise and Readable Code: Reduces boilerplate compared to traditional loop-based processing, leading to more compact and understandable code.
  • Declarative Style: Focuses on *what* to do rather than *how* to do it, making the intent of the code clearer.
  • Parallel Processing: Offers built-in support for parallel execution (parallelStream()), which can significantly improve performance on multi-core processors for large datasets.
  • Reduced Boilerplate: Eliminates explicit loops, iterators, and conditional statements for many common data processing tasks.
  • Functional Programming Style: Encourages the use of lambda expressions and method references, promoting a more functional and less imperative programming paradigm.
Q29.

What is Optional in Java?

Java's `Optional` is a container object used to contain a non-null value that may or may not be present. Introduced in Java 8, it provides a clear way to indicate the absence of a value, helping to prevent `NullPointerException`s and write more robust, readable code.

What is Optional?

Before Java 8, representing the absence of a value often involved returning null. This approach led to boilerplate null checks and frequent NullPointerExceptions if developers forgot to handle the null case. Optional addresses this by providing a type-safe way to express the potential absence of a value.

An Optional instance can either contain a non-null value (present) or contain nothing (empty). This forces developers to explicitly consider the case where a value might not be available, leading to more robust and less error-prone code.

Why use Optional?

  • Avoids NullPointerException: By encouraging explicit checks for value presence, it drastically reduces the chances of encountering NPEs.
  • Clearer API Design: Method signatures returning Optional<T> clearly communicate that the method might not return a value, making the API easier to understand and use correctly.
  • Improved Readability: Reduces null checks clutter and allows for more functional-style programming with methods like map(), filter(), and ifPresent().
  • Encourages Better Error Handling: Forces developers to make decisions about what to do when a value is absent, rather than silently propagating null.

Creating Optional Instances

There are three primary ways to create an Optional instance:

  • Optional.empty(): Creates an empty Optional instance.
  • Optional.of(T value): Creates an Optional with the specified non-null value. Throws NullPointerException if the value is null.
  • Optional.ofNullable(T value): Creates an Optional with the specified value, which can be null. If the value is null, it returns an empty Optional.
java
import java.util.Optional;

public class OptionalCreation {
    public static void main(String[] args) {
        // 1. An empty Optional
        Optional<String> emptyOptional = Optional.empty();
        System.out.println("Empty Optional present: " + emptyOptional.isPresent()); // false

        // 2. An Optional with a non-null value
        Optional<String> presentOptional = Optional.of("Hello Java");
        System.out.println("Present Optional value: " + presentOptional.get()); // Hello Java

        // 3. An Optional from a potentially null value
        String nullableString = null;
        Optional<String> maybeString = Optional.ofNullable(nullableString);
        System.out.println("Nullable Optional (null) present: " + maybeString.isPresent()); // false

        String nonNullableString = "Non-null data";
        Optional<String> anotherMaybeString = Optional.ofNullable(nonNullableString);
        System.out.println("Nullable Optional (non-null) value: " + anotherMaybeString.get()); // Non-null data
    }
}

Common Optional Methods

Optional provides several methods for interacting with its encapsulated value:

  • isPresent(): Returns true if a value is present, false otherwise.
  • isEmpty(): Returns true if a value is not present, false otherwise (introduced in Java 11).
  • get(): Returns the value if present, otherwise throws NoSuchElementException. Use with caution, usually after checking isPresent().
  • orElse(T other): Returns the value if present, otherwise returns the specified default value.
  • orElseGet(Supplier<? extends T> other): Returns the value if present, otherwise invokes the Supplier function and returns the result. Useful for expensive default value computations.
  • orElseThrow() (Java 10+), orElseThrow(Supplier<? extends X> exceptionSupplier): Returns the value if present, otherwise throws the specified exception (or NoSuchElementException by default).
  • ifPresent(Consumer<? super T> consumer): If a value is present, performs the given action with the value, otherwise does nothing.
  • ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) (Java 9+): If a value is present, performs the given action, otherwise performs the emptyAction.
  • map(Function<? super T, ? extends U> mapper): If a value is present, applies the mapping function to it and returns an Optional describing the result. Otherwise, returns an empty Optional.
  • flatMap(Function<? super T, Optional<U>> mapper): Similar to map, but the mapping function itself returns an Optional. This prevents nested Optionals.
  • filter(Predicate<? super T> predicate): If a value is present and matches the given predicate, returns an Optional describing the value. Otherwise, returns an empty Optional.
java
import java.util.Optional;

public class OptionalMethods {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Alice");
        Optional<String> emptyName = Optional.empty();

        // isPresent() / isEmpty()
        System.out.println("Name is present: " + name.isPresent()); // true
        System.out.println("Empty name is empty: " + emptyName.isEmpty()); // true (Java 11+)

        // get() - use with caution!
        if (name.isPresent()) {
            System.out.println("Value with get(): " + name.get()); // Alice
        }

        // orElse()
        String result1 = name.orElse("Default"); // result1: Alice
        String result2 = emptyName.orElse("Default"); // result2: Default
        System.out.println("orElse results: " + result1 + ", " + result2);

        // orElseGet()
        String result3 = name.orElseGet(() -> "Generated Default"); // result3: Alice
        String result4 = emptyName.orElseGet(() -> "Generated Default"); // result4: Generated Default
        System.out.println("orElseGet results: " + result3 + ", " + result4);

        // ifPresent()
        name.ifPresent(s -> System.out.println("ifPresent: Hello, " + s)); // Hello, Alice
        emptyName.ifPresent(s -> System.out.println("ifPresent: Hello, " + s)); // no output

        // map()
        Optional<Integer> nameLength = name.map(String::length);
        System.out.println("Name length: " + nameLength.orElse(0)); // 5

        // filter()
        Optional<String> longName = name.filter(s -> s.length() > 3);
        System.out.println("Long name filtered: " + longName.orElse("Too short")); // Alice

        Optional<String> veryLongName = name.filter(s -> s.length() > 10);
        System.out.println("Very long name filtered: " + veryLongName.orElse("Too short")); // Too short
    }
}

Best Practices

  • Do not use Optional as a field type: It's not Serializable and can lead to unexpected behavior.
  • Do not use Optional as a method parameter: Instead, allow null parameters and perform null checks inside the method, or overload the method.
  • Avoid get() without isPresent(): This defeats the purpose of Optional and can still lead to NoSuchElementException. Prefer orElse(), orElseGet(), orElseThrow(), or ifPresent().
  • Prefer functional operations: Leverage map(), flatMap(), filter(), ifPresent() to chain operations and make code more concise.
  • Avoid nested Optionals: Use flatMap() to flatten Optional<Optional<T>> to Optional<T>.
  • Return Optional.empty() instead of null: When a method might not have a result, returning an empty Optional is the idiomatic way.

Example Usage Scenario

java
import java.util.Optional;

public class UserService {

    // Represents a simplified User object
    static class User {
        private long id;
        private String name;
        private String email;

        public User(long id, String name, String email) {
            this.id = id;
            this.name = name;
            this.email = email;
        }

        public String getEmail() {
            return email;
        }
        public String getName() { return name; }
    }

    // Simulates fetching a user by ID from a database
    public Optional<User> findUserById(long id) {
        // In a real application, this would query a database.
        // For this example, we return a user only for ID 1.
        if (id == 1L) {
            return Optional.of(new User(1L, "John Doe", "john.doe@example.com"));
        }
        return Optional.empty(); // User not found
    }

    public void printUserEmail(long userId) {
        Optional<User> userOptional = findUserById(userId);

        userOptional.map(User::getEmail) // If user is present, get their email
                    .ifPresentOrElse( // If email is present, print it. Otherwise, print 'not found'.
                        email -> System.out.println("User email: " + email),
                        () -> System.out.println("User with ID " + userId + " not found.")
                    );
    }

    public static void main(String[] args) {
        UserService service = new UserService();

        System.out.println("--- Searching for user ID 1 ---");
        service.printUserEmail(1L); // Output: User email: john.doe@example.com

        System.out.println("\n--- Searching for user ID 2 ---");
        service.printUserEmail(2L); // Output: User with ID 2 not found.
    }
}

Conclusion

Optional is a powerful feature in Java 8+ that promotes cleaner, more expressive code by explicitly handling the absence of a value. When used correctly, it significantly reduces the likelihood of NullPointerExceptions and improves the overall robustness and readability of applications. It's a key tool for writing functional-style Java code.

Q30.

Explain functional interfaces.

Functional interfaces are a key feature introduced in Java 8 that enable the use of lambda expressions. They are interfaces that contain exactly one abstract method, acting as a contract for lambda expressions and method references.

What are Functional Interfaces?

In Java, a functional interface is an interface that has exactly one abstract method. This single abstract method is known as the Functional Method or Single Abstract Method (SAM). Functional interfaces are designed to be targets for lambda expressions and method references, making code more concise and readable by allowing behavior to be passed as an argument.

Key Characteristics

  • Single Abstract Method (SAM): Must declare exactly one abstract method. Methods inherited from java.lang.Object (like equals, hashCode, toString) do not count towards this single abstract method rule.
  • Default and Static Methods: Can include any number of default and static methods, which were also introduced in Java 8.
  • @FunctionalInterface Annotation: This annotation is optional but highly recommended. It serves as an indicator for the compiler to enforce the single abstract method rule. If an interface annotated with @FunctionalInterface has more or less than one abstract method (excluding Object class methods), the compiler will flag an error.
  • Target for Lambda Expressions: They provide the context or target type required for lambda expressions and method references to work.

Syntax and Example

Defining a functional interface is similar to defining a regular interface, with the addition of the @FunctionalInterface annotation and ensuring only one abstract method. Lambda expressions then provide a concise way to implement this single abstract method.

java
@FunctionalInterface
interface MyFunctionalInterface {
    void performAction(String data); // Single abstract method

    // Optional: default method
    default void logAction(String message) {
        System.out.println("Logging: " + message);
    }

    // Optional: static method
    static void displayInfo() {
        System.out.println("This is a custom functional interface.");
    }
}
java
public class FunctionalInterfaceDemo {
    public static void main(String[] args) {
        // Using a lambda expression to implement the functional interface
        MyFunctionalInterface myAction = (data) -> {
            System.out.println("Action performed with data: " + data.toUpperCase());
        };

        myAction.performAction("hello world"); // Output: Action performed with data: HELLO WORLD
        myAction.logAction("Action completed."); // Output: Logging: Action completed.
        MyFunctionalInterface.displayInfo(); // Output: This is a custom functional interface.

        // Another way using method reference if a compatible method exists
        // MyFunctionalInterface anotherAction = System.out::println;
        // anotherAction.performAction("method reference example");
    }
}

Pre-defined Functional Interfaces

Java provides a rich set of pre-defined functional interfaces in the java.util.function package, eliminating the need to create custom interfaces for common use cases. These include interfaces for functions, consumers, predicates, and suppliers.

InterfaceAbstract MethodDescription
`Predicate<T>``boolean test(T t)`Represents a predicate (boolean-valued function) of one argument.
`Consumer<T>``void accept(T t)`Represents an operation that accepts a single input argument and returns no result.
`Function<T, R>``R apply(T t)`Represents a function that accepts one argument and produces a result.
`Supplier<T>``T get()`Represents a supplier of results. Has no arguments.
`UnaryOperator<T>``T apply(T t)`Represents an operation on a single operand that produces a result of the same type as its operand. Extends `Function<T, T>`.
`BinaryOperator<T>``T apply(T t1, T t2)`Represents an operation upon two operands of the same type, producing a result of the same type as the operands. Extends `BiFunction<T, T, T>`.
`BiPredicate<T, U>``boolean test(T t, U u)`Represents a predicate (boolean-valued function) of two arguments.
`BiConsumer<T, U>``void accept(T t, U u)`Represents an operation that accepts two input arguments and returns no result.
`BiFunction<T, U, R>``R apply(T t, U u)`Represents a function that accepts two arguments and produces a result.

Why Use Functional Interfaces?

  • Enables Lambda Expressions: They are the fundamental building blocks for using lambda expressions, allowing behavior to be passed around as an argument.
  • Concise Code: Reduces boilerplate code significantly compared to traditional anonymous inner classes.
  • Readability: Makes code cleaner and easier to understand, especially for simple operations.
  • Integration with Streams API: Functional interfaces are heavily used in Java's Streams API to perform operations like filtering, mapping, and reducing data collections in a declarative style.
  • Supports Functional Programming: Facilitates a more functional programming style in Java, promoting immutability and side-effect-free operations.