What is Type Erasure?
Type Erasure is the process by which the Java compiler removes generic type information during compilation. Generic information is present only at compile time for type-checking purposes; it is not available at runtime.
During type erasure, the compiler:
The main reason for type erasure was to ensure backward compatibility. It allowed generic code to be compatible with older, non-generic legacy code and run on older JVMs that had no knowledge of generics.
A consequence of type erasure is that you cannot do things like `new T()` or `if (obj instanceof List<String>)` at runtime, because the type `T` or `String` has been erased.
What are wildcards in Generics? Explain `? extends T` and `? super T`.
Wildcards (`?`) in generics represent an unknown type. They are used to increase the flexibility of methods that work with generic types.
Upper Bounded Wildcard (`? extends T`):
// This method can accept a List of Number, or a List of Integer, or a List of Double.
public void processNumbers(List<? extends Number> list) {
for (Number num : list) {
// Safe to read as Number
// ...
}
// list.add(new Integer(5)); // COMPILE ERROR!
}
Lower Bounded Wildcard (`? super T`):
// This method can accept a List of Integer, or a List of Number, or a List of Object.
public void addIntegers(List<? super Integer> list) {
list.add(new Integer(5)); // Safe to add an Integer
// Object obj = list.get(0); // Can only read as Object
}
What are new features in Java 9?
Java 9 introduced several significant features. Some of the most important ones are:
What is the `var` keyword introduced in Java 10?
The `var` keyword, introduced in Java 10, allows you to declare a local variable without explicitly specifying its type. This is called Local-Variable Type Inference.
The compiler infers the type of the variable from the type of the initializer on the right-hand side of the assignment.
Before Java 10:
Map<String, List<Integer>> myMap = new HashMap<>();
With Java 10 `var`:
var myMap = new HashMap<String, List<Integer>>();
Important Restrictions:
It is important to note that this is purely a compiler feature (syntactic sugar) to reduce boilerplate code. The variable is still strongly typed; its type is fixed at compile time and cannot be changed.
What is the Reflection API?
The Reflection API is a feature in Java that allows an application to examine or modify the runtime behavior of applications running in the JVM. It provides the ability to inspect classes, interfaces, fields, and methods at runtime, without knowing their names at compile time.
Using reflection, you can:
It is a very powerful tool but should be used with caution because:
Reflection is commonly used in frameworks like Spring and Hibernate for dependency injection and object-relational mapping.
How do you create a custom exception?
You can create a custom exception in Java by extending one of the existing exception classes.
java.lang.Exception
class.java.lang.RuntimeException
class.It is a common practice to provide at least two constructors in your custom exception class:
Example of a Custom Checked Exception:
// 1. Create the custom exception class
public class InvalidAgeException extends Exception {
// 2. Provide constructors
public InvalidAgeException() {
super();
}
public InvalidAgeException(String message) {
super(message);
}
}
// 3. Use the custom exception
public class AgeValidator {
public void validate(int age) throws InvalidAgeException {
if (age < 18) {
throw new InvalidAgeException("User is not old enough.");
}
}
}
What is the difference between `java.io` and `java.nio`?
java.io
(IO) and `java.nio` (NIO - New I/O) are two different APIs for handling input/output operations in Java.
Feature | Java IO (`java.io`) | Java NIO (`java.nio`) |
---|---|---|
I/O Model | Stream-oriented. It reads or writes data one byte at a time sequentially. | Buffer-oriented. Data is first read into a buffer, from which it is then processed. |
Blocking | Blocking I/O. The thread that makes a `read()` or `write()` call is blocked until some data is available or the data is fully written. | Non-blocking I/O. A thread can request to read data from a channel and get whatever is currently available, or nothing at all if no data is available. The thread is not blocked and can go on to do other things. |
Channels and Selectors | Does not have the concept of channels or selectors. | Uses Channels as a medium for I/O operations and Selectors to monitor multiple channels for I/O events (like data available for reading) with a single thread. |
Performance | Generally slower for high-volume network applications due to its blocking nature. | Generally faster and more scalable for I/O-intensive operations, especially in networking, because a single thread can manage multiple connections. |
NIO is more complex to use than IO, but it is the preferred choice for building high-performance, scalable servers.
What is the Builder Design Pattern?
The Builder pattern is a creational design pattern used to construct a complex object step by step. It separates the construction of a complex object from its representation so that the same construction process can create different representations.
It is particularly useful when an object has many constructor parameters, some of which may be optional. Instead of creating many complex constructors (a 'telescoping constructor' anti-pattern), you use a builder.
Steps to Implement:
public class User {
private final String firstName; // required
private final String lastName; // required
private final int age; // optional
private final String phone; // optional
private User(UserBuilder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
}
public static class UserBuilder {
private final String firstName;
private final String lastName;
private int age;
private String phone;
public UserBuilder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public User build() {
return new User(this);
}
}
}
// Usage:
User user = new User.UserBuilder("John", "Doe").age(30).phone("123-456").build();
What is a record in Java?
Records are a new feature introduced as a preview in Java 14 and standardized in Java 16. They provide a compact syntax for declaring classes that are transparent holders for immutable data. They are a special kind of class.
Before records, if you wanted to create a simple data carrier class (like a `Point` with x and y coordinates), you had to write a lot of boilerplate code: private final fields, a constructor, accessor methods (`getters`), and implementations for `hashCode()`, `equals()`, and `toString()`.
With records, you can declare all of this in a single line.
Example using a record:
public record Point(int x, int y) {}
When you declare a record, the Java compiler automatically generates:
Records are implicitly `final` and cannot extend any other class. They are designed to significantly reduce boilerplate code for simple data aggregate classes.
What is the difference between `map` and `flatMap` in the Stream API?
Both `map` and `flatMap` are intermediate operations in the Java Stream API that apply a function to the elements of a stream. The key difference is in how they handle the result of the function.
map(Function<T, R>)
: This operation transforms each element of a stream from type `T` to type `R`. If you have a stream of objects and the mapping function returns a stream for each object, the result will be a stream of streams (e.g., Stream<Stream<R>>
). It performs a one-to-one transformation.flatMap(Function<T, Stream<R>>)
: This operation is a combination of a `map` and a `flatten` operation. It transforms each element into a stream of other objects, and then it flattens all these generated streams into a single, flat stream (Stream<R>>
). It performs a one-to-many transformation.Example: Given a list of words, get a list of all unique characters.
List<String> words = Arrays.asList("Hello", "World");
// Using map: results in Stream<Stream<String>>, which is not what we want.
List<Stream<String>> resultWithMap = words.stream()
.map(word -> Arrays.stream(word.split("")))
.collect(Collectors.toList());
// Using flatMap: correctly results in Stream<String>.
List<String> uniqueChars = words.stream()
.map(word -> word.split("")) // Stream<String[]>
.flatMap(Arrays::stream) // Flattens Stream<String[]> to Stream<String>
.distinct()
.collect(Collectors.toList());
// Result: [H, e, l, o, W, r, d]
In short, use `flatMap` when you need to transform one element into multiple elements and want them all in a single, flat stream.