Java Interview Questions
💡 Click Show Answer to generate an AI-powered answer instantly.
What is exception handling in Java?
What is the difference between checked and unchecked exceptions?
What is try-with-resources?
Explain garbage collection in Java.
What is the difference between heap and stack memory?
What are Java 8 features?
Explain lambda expressions in Java.
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 thanmaxSize.skip(long n): Returns a stream consisting of the remaining elements after discarding the firstnelements.
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.
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.
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(), andifPresent(). - 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 emptyOptionalinstance.Optional.of(T value): Creates anOptionalwith the specified non-null value. ThrowsNullPointerExceptionif the value isnull.Optional.ofNullable(T value): Creates anOptionalwith the specified value, which can benull. If the value isnull, it returns an emptyOptional.
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(): Returnstrueif a value is present,falseotherwise.isEmpty(): Returnstrueif a value is not present,falseotherwise (introduced in Java 11).get(): Returns the value if present, otherwise throwsNoSuchElementException. Use with caution, usually after checkingisPresent().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 theSupplierfunction 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 (orNoSuchElementExceptionby 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 givenaction, otherwise performs theemptyAction.map(Function<? super T, ? extends U> mapper): If a value is present, applies the mapping function to it and returns anOptionaldescribing the result. Otherwise, returns an emptyOptional.flatMap(Function<? super T, Optional<U>> mapper): Similar tomap, but the mapping function itself returns anOptional. This prevents nestedOptionals.filter(Predicate<? super T> predicate): If a value is present and matches the given predicate, returns anOptionaldescribing the value. Otherwise, returns an emptyOptional.
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
Optionalas a field type: It's notSerializableand can lead to unexpected behavior. - Do not use
Optionalas a method parameter: Instead, allownullparameters and perform null checks inside the method, or overload the method. - Avoid
get()withoutisPresent(): This defeats the purpose ofOptionaland can still lead toNoSuchElementException. PreferorElse(),orElseGet(),orElseThrow(), orifPresent(). - Prefer functional operations: Leverage
map(),flatMap(),filter(),ifPresent()to chain operations and make code more concise. - Avoid nested
Optionals: UseflatMap()to flattenOptional<Optional<T>>toOptional<T>. - Return
Optional.empty()instead ofnull: When a method might not have a result, returning an emptyOptionalis the idiomatic way.
Example Usage Scenario
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.
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(likeequals,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.
@FunctionalInterfaceAnnotation: 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@FunctionalInterfacehas 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.
@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.");
}
}
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.
| Interface | Abstract Method | Description |
|---|---|---|
| `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.