Java 11 Interview Questions (Free Preview)
Free sample of 15 from 49 questions available
Java 11 Fundamentals
What is the difference between Oracle JDK and OpenJDK starting from Java 11?
The 30-Second Answer: Starting from Java 11, Oracle JDK and OpenJDK became functionally identical in terms of features and APIs, with the main differences being licensing and support. Oracle JDK requires a commercial license for production use and offers paid commercial support, while OpenJDK is free under the GPL license. Oracle also provides Oracle-specific tools and longer commercial support periods, but the core Java platform is the same.
The 2-Minute Answer (If They Want More):
Prior to Java 11, Oracle JDK included proprietary features and commercial tools not available in OpenJDK. However, Oracle made a strategic decision to make Oracle JDK and OpenJDK builds functionally interchangeable starting with Java 11, eliminating technical differences while introducing significant licensing changes.
Licensing differences are now the primary distinction. OpenJDK is licensed under the GPLv2 with Classpath Exception, making it completely free for development, testing, and production use without restrictions. Oracle JDK requires a commercial license (Oracle Technology Network License) for production use, updates, and commercial support. Organizations using Oracle JDK in production without a license or subscription violate the terms. This licensing change prompted widespread migration to OpenJDK distributions.
Support and updates differ significantly. Oracle provides free updates for Oracle JDK only for six months after each release (except LTS versions which get longer commercial support). Oracle offers paid commercial support with extended update periods, critical patch updates, and enterprise-level SLAs. OpenJDK receives updates from the community, but individual builds may have different support timelines. Various vendors (Amazon Corretto, Azul Zulu, Red Hat, AdoptOpenJDK/Eclipse Temurin, Microsoft) provide free long-term supported OpenJDK builds with their own update schedules.
Technical differences are minimal but exist. Oracle JDK includes additional monitoring and management tools like Java Flight Recorder (JFR) and Java Mission Control (JMC), though these are now open-sourced and available in OpenJDK builds from some vendors. Oracle JDK may have slightly different installers and packaging, and some performance characteristics might differ due to build configurations, though functionally they're equivalent.
Binary compatibility and testing also matter. Oracle JDK undergoes Oracle's internal testing and certification processes, while OpenJDK builds from different vendors may have varying levels of testing. However, major OpenJDK distributors like Amazon, Azul, and Red Hat provide production-ready, thoroughly tested builds that match or exceed Oracle JDK quality.
For most organizations, OpenJDK distributions from reputable vendors (Temurin, Corretto, Zulu) are the recommended choice, offering free licensing, long-term support, regular updates, and production-grade quality without Oracle's commercial licensing requirements.
// Example: Checking JDK version and vendor
public class JDKInfo {
public static void main(String[] args) {
System.out.println("Java Version: " +
System.getProperty("java.version"));
System.out.println("Java Vendor: " +
System.getProperty("java.vendor"));
System.out.println("JVM Name: " +
System.getProperty("java.vm.name"));
System.out.println("JVM Vendor: " +
System.getProperty("java.vm.vendor"));
// Runtime version includes build info
Runtime.Version version = Runtime.version();
System.out.println("Runtime Version: " + version);
}
}
/* Output examples:
Oracle JDK 11:
Java Version: 11.0.12
Java Vendor: Oracle Corporation
JVM Name: Java HotSpot(TM) 64-Bit Server VM
OpenJDK 11 (Temurin):
Java Version: 11.0.12
Java Vendor: Eclipse Adoptium
JVM Name: OpenJDK 64-Bit Server VM
Amazon Corretto 11:
Java Version: 11.0.12
Java Vendor: Amazon.com Inc.
JVM Name: OpenJDK 64-Bit Server VM
*/
Common OpenJDK Distributions:
- Eclipse Temurin (formerly AdoptOpenJDK) - Community-driven, broad platform support
- Amazon Corretto - AWS-supported, optimized for cloud, free LTS
- Azul Zulu - Enterprise-ready, multiple support options
- Red Hat OpenJDK - Integrated with RHEL, enterprise support available
- Microsoft Build of OpenJDK - Optimized for Azure, LTS support
References:
- https://blogs.oracle.com/java/post/oracle-jdk-releases-for-java-11-and-later
- https://www.oracle.com/java/technologies/javase/jdk-faqs.html
- https://foojay.io/today/its-time-to-look-past-oracles-jdk/
String API Enhancements
What is the difference between strip() and trim()?
The 30-Second Answer:
The main difference is Unicode support: strip() uses Character.isWhitespace() to identify and remove all Unicode whitespace characters, while trim() only removes characters with code points ≤ U+0020 (space). This means strip() properly handles modern Unicode whitespace like non-breaking spaces, while trim() may miss them. For most modern applications, strip() is the preferred choice.
The 2-Minute Answer (If They Want More):
The trim() method has been part of Java since version 1.0 and uses a simple criterion: it removes all leading and trailing characters whose code point is less than or equal to U+0020 (the space character). This means it removes spaces, tabs, newlines, and other ASCII control characters, but it doesn't recognize many Unicode whitespace characters that have higher code points. This limitation stems from its design in the early days of Java when Unicode support was less comprehensive.
In contrast, strip() (introduced in Java 11) uses the Character.isWhitespace(int) method to determine what constitutes whitespace. This method recognizes a much broader range of whitespace characters defined in the Unicode standard, including non-breaking spaces (U+00A0), various width spaces (like em space U+2003, thin space U+2009), zero-width spaces, and many others. This makes strip() more robust and correct for internationalized applications and modern text processing.
For most practical purposes in modern Java applications, you should prefer strip() over trim() because it provides more accurate whitespace handling. However, trim() is still maintained for backward compatibility and may be slightly faster in performance-critical scenarios where you know you're only dealing with ASCII whitespace. The strip() family also includes stripLeading() and stripTrailing() for more granular control, which have no direct equivalents in the trim API.
It's worth noting that both methods return new String instances (maintaining immutability) and don't modify the original string. If you're working with legacy code or need to maintain exact backward compatibility, you might need to continue using trim(), but for new code, strip() is the recommended approach.
// ASCII whitespace - both work the same
String ascii = " Hello ";
System.out.println("[" + ascii.trim() + "]"); // [Hello]
System.out.println("[" + ascii.strip() + "]"); // [Hello]
// Unicode whitespace - different behavior
String unicode = "\u2002Hello\u2003"; // U+2002: en space, U+2003: em space
System.out.println("[" + unicode.trim() + "]"); // [\u2002Hello\u2003] (not removed!)
System.out.println("[" + unicode.strip() + "]"); // [Hello] (properly removed)
// Non-breaking space (common in HTML/web content)
String nbsp = "\u00A0Hello\u00A0";
System.out.println("[" + nbsp.trim() + "]"); // [\u00A0Hello\u00A0] (not removed!)
System.out.println("[" + nbsp.strip() + "]"); // [Hello] (properly removed)
// Performance consideration (for ASCII-only content)
String simple = " test ";
simple.trim(); // Slightly faster for ASCII-only content
simple.strip(); // More correct, minimal performance difference
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#strip()
- https://stackoverflow.com/questions/51266582/difference-between-string-trim-and-strip-methods-in-java-11
- https://www.baeldung.com/java-11-string-api
Collection Factory Methods
What is the difference between Collections.unmodifiableList() and List.of()?
The 30-Second Answer:
Collections.unmodifiableList() creates an immutable view/wrapper around an existing mutable list, so changes to the original list affect the view. List.of() creates a truly immutable list that cannot be changed by anyone, provides better performance with optimized internal implementations, and doesn't allow null elements.
The 2-Minute Answer (If They Want More):
The key difference lies in how immutability is achieved and guaranteed. Collections.unmodifiableList() implements the decorator pattern - it wraps an existing list and blocks modification methods by throwing UnsupportedOperationException. However, if you or someone else still has a reference to the original underlying list, they can modify it, and those changes will be visible through the unmodifiable wrapper. This makes it more of a "read-only view" rather than a truly immutable data structure.
In contrast, List.of() creates a genuinely immutable list from scratch. There is no underlying mutable list - the data structure itself is immutable. This provides stronger guarantees: once created, the list's contents can never change, regardless of who has references to what. This makes List.of() safer for sharing across threads, using as Map keys, or storing in Sets.
Performance-wise, List.of() is more efficient. It uses specialized internal implementations optimized for different sizes (0, 1, 2, or N elements), reducing memory overhead and improving access speed. Collections.unmodifiableList() adds an extra layer of indirection with the wrapper object.
Null handling also differs significantly. Collections.unmodifiableList() allows null elements if the underlying list contains them, while List.of() explicitly rejects nulls by throwing NullPointerException. This null-safety helps prevent bugs and makes code more predictable. Additionally, the iteration order and serialization behavior may differ between the two approaches.
// Collections.unmodifiableList() - wrapper around mutable list
List<String> mutableList = new ArrayList<>();
mutableList.add("A");
mutableList.add("B");
List<String> unmodifiableView = Collections.unmodifiableList(mutableList);
// The view appears immutable
// unmodifiableView.add("C"); // Throws UnsupportedOperationException
// But the underlying list can still be modified!
mutableList.add("C");
System.out.println(unmodifiableView); // [A, B, C] - changed!
// Nulls are allowed
mutableList.add(null);
System.out.println(unmodifiableView); // [A, B, C, null]
// List.of() - truly immutable
List<String> immutableList = List.of("A", "B");
// Cannot be modified at all
// immutableList.add("C"); // Throws UnsupportedOperationException
// No underlying mutable reference exists to modify
// The list is guaranteed to always contain exactly ["A", "B"]
// Nulls are NOT allowed
// List<String> invalid = List.of("A", null, "B"); // Throws NullPointerException
// Demonstrating the safety difference
public class CollectionExample {
// Unsafe - caller could modify the list
public static List<String> getDataUnsafe() {
List<String> data = new ArrayList<>(List.of("X", "Y", "Z"));
return Collections.unmodifiableList(data); // Still holds reference to 'data'
}
// Safe - truly immutable
public static List<String> getDataSafe() {
return List.of("X", "Y", "Z"); // No mutable reference exists
}
// Thread-safe sharing
private static final List<String> CONSTANTS = List.of("PROD", "DEV", "TEST");
public static void main(String[] args) {
// Multiple threads can safely access CONSTANTS without synchronization
// because it's truly immutable
}
}
Comparison table:
| Feature | Collections.unmodifiableList() | List.of() |
|---|---|---|
| Immutability | View-level (wrapper) | True immutability |
| Underlying list | Can be modified externally | No underlying mutable list |
| Null elements | Allowed | Not allowed (NullPointerException) |
| Performance | Extra wrapper overhead | Optimized internal implementations |
| Memory | Original list + wrapper | Single optimized structure |
| Thread safety | Not guaranteed if underlying list is modified | Fully thread-safe |
| Since | Java 1.2 | Java 9 |
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Collections.html#unmodifiableList(java.util.List)
- https://stackoverflow.com/questions/46579074/what-is-the-difference-between-list-of-and-collections-unmodifiablelist
- https://www.baeldung.com/java-unmodifiable-vs-immutable-collections
HTTP Client API
What is the difference between the old HttpURLConnection and the new HttpClient?
The 30-Second Answer: HttpURLConnection is a legacy, low-level API that's difficult to use, synchronous-only, and lacks HTTP/2 support. The new HttpClient provides a modern, fluent API with HTTP/2 support, asynchronous requests, better error handling, and cleaner code. HttpClient should be used for all new Java 11+ projects.
The 2-Minute Answer (If They Want More): The differences between HttpURLConnection and HttpClient are substantial and represent decades of evolution in Java networking APIs. HttpURLConnection was introduced in JDK 1.1 (1997) and has remained largely unchanged, carrying many design decisions that don't align with modern Java practices. It's verbose, error-prone, requires manual connection management, and forces developers to handle many low-level details.
HttpClient, standardized in Java 11, is designed from the ground up for modern Java development. It uses the builder pattern for creating clients and requests, making code more readable and reducing errors. The API is immutable and thread-safe by design, whereas HttpURLConnection required careful handling to avoid concurrency issues.
One of the most significant differences is asynchronous support. HttpURLConnection is purely synchronous and blocking, requiring manual thread management for concurrent requests. HttpClient provides first-class asynchronous support through CompletableFuture, integrating seamlessly with modern reactive programming patterns and Java's concurrency utilities.
HTTP/2 support is another major differentiator. HttpURLConnection only supports HTTP/1.1, missing out on multiplexing, header compression, and other performance improvements. HttpClient provides native HTTP/2 support with automatic fallback, dramatically improving performance for applications making multiple requests to the same server. The new API also has better security defaults, automatic cookie management, and more intuitive redirect handling.
// OLD API: HttpURLConnection (verbose and error-prone)
URL url = new URL("https://api.github.com/users/octocat");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
int statusCode = connection.getResponseCode();
if (statusCode == 200) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
System.out.println("Response: " + response.toString());
} else {
System.err.println("Error: " + statusCode);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
connection.disconnect();
}
// NEW API: HttpClient (clean and modern)
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/users/octocat"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
try {
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Response: " + response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
// Asynchronous request (not possible with HttpURLConnection)
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(body -> System.out.println("Async: " + body))
.exceptionally(ex -> {
System.err.println("Failed: " + ex.getMessage());
return null;
});
// POST request comparison
// OLD API
HttpURLConnection postConn = (HttpURLConnection)
new URL("https://api.example.com/data").openConnection();
postConn.setRequestMethod("POST");
postConn.setRequestProperty("Content-Type", "application/json");
postConn.setDoOutput(true);
String jsonBody = "{\"name\":\"John\"}";
try (OutputStream os = postConn.getOutputStream()) {
os.write(jsonBody.getBytes());
}
// NEW API
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"John\"}"))
.build();
client.sendAsync(postRequest, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> System.out.println(response.body()));
| Feature | HttpURLConnection (Old) | HttpClient (New) |
|---|---|---|
| Java Version | JDK 1.1+ | Java 11+ |
| API Style | Imperative, low-level | Fluent builder pattern |
| HTTP/2 Support | No (HTTP/1.1 only) | Yes, with HTTP/1.1 fallback |
| Asynchronous Requests | No (manual threading needed) | Yes (CompletableFuture) |
| Immutability | Mutable, not thread-safe | Immutable, thread-safe |
| Code Verbosity | High (lots of boilerplate) | Low (concise and readable) |
| Error Handling | Multiple IOException types | Cleaner exception hierarchy |
| Timeout Configuration | Separate connect/read timeouts | Duration-based timeouts |
| Request Body | Manual OutputStream handling | BodyPublishers API |
| Response Body | Manual InputStream reading | BodyHandlers API |
| Cookie Management | Manual with CookieHandler | Automatic with CookieHandler |
| Redirect Handling | Manual or limited automatic | Configurable redirect policies |
| WebSocket Support | No | Yes (java.net.http.WebSocket) |
| Connection Pooling | Limited | Automatic with HTTP/2 |
| Modern Java Features | No | Yes (Duration, CompletableFuture, etc.) |
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html
- https://openjdk.org/jeps/321
- https://www.baeldung.com/java-9-http-client
What is the new HTTP Client API introduced in Java 11?
The 30-Second Answer: The HTTP Client API (java.net.http) is a modern, standardized API introduced in Java 11 that replaces the legacy HttpURLConnection. It provides native support for HTTP/2, WebSocket, asynchronous programming with CompletableFuture, and a fluent builder pattern for creating HTTP requests and clients.
The 2-Minute Answer (If They Want More): The HTTP Client API was initially introduced as an incubator module in Java 9, refined in Java 10, and finally standardized in Java 11. It addresses many limitations of the old HttpURLConnection API, which was low-level, difficult to use, and lacked modern features.
The new API consists of three main classes: HttpClient for sending requests, HttpRequest for building HTTP requests, and HttpResponse for handling responses. It supports both synchronous and asynchronous request execution, making it suitable for blocking and non-blocking programming models.
One of the most significant improvements is native HTTP/2 support with features like request/response multiplexing, server push, and header compression. The API also provides better security with automatic cookie management, redirect policies, and authentication handling. The builder pattern makes the API more intuitive and type-safe compared to the old approach.
The API integrates seamlessly with Java's modern concurrency utilities, particularly CompletableFuture, enabling reactive programming patterns. It also supports HTTP/1.1 fallback when HTTP/2 is not available, ensuring broad compatibility.
// Basic example of the new HTTP Client API
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.html
- https://openjdk.org/jeps/321
- https://www.baeldung.com/java-9-http-client
Module System (Project Jigsaw)
What is the difference between classpath and module path?
The 30-Second Answer: The classpath is the traditional mechanism where all JARs and classes are loaded into a flat namespace with no encapsulation or dependency verification, while the module path loads modules with strong encapsulation, explicit dependencies, and unique package ownership. The module path enforces module boundaries at compile-time and runtime, preventing split packages and illegal access to internal APIs.
The 2-Minute Answer (If They Want More):
Classpath is the legacy mechanism where all classes and packages are available in a flat namespace. Every public class in every JAR is accessible to all other code, regardless of intended visibility. The classpath doesn't verify dependencies until runtime, leading to NoClassDefFoundError when classes are missing. It allows split packages (same package in multiple JARs), causing version conflicts and unpredictable behavior. The classpath has no concept of encapsulation beyond access modifiers, so internal implementation classes can't be truly hidden if they're public.
Module path introduces structured dependency management. Each module explicitly declares its dependencies and exports, creating a module graph that's verified before the application starts. The module system enforces strong encapsulation - only exported packages are accessible, even for public classes. Split packages are forbidden; each package must belong to exactly one module. The module path enables reliable configuration where missing dependencies are detected early, and conflicting modules are rejected at startup.
You can use both classpath and module path simultaneously. JARs on the classpath become part of the unnamed module, which can read all other modules but cannot be required by named modules. This allows gradual migration from classpath to modules. Automatic modules bridge the gap - JARs on the module path without a module-info.java become automatic modules with derived module names, reading all other modules and exporting all packages.
The performance implications are significant. Module path allows the JVM to optimize module loading and enables jlink to create custom runtime images containing only needed modules. The module system's accessibility checks happen at startup, eliminating repeated runtime checks. Memory footprint is reduced because only required modules are loaded, and the JVM can optimize module boundaries.
// Compilation and execution differences
// Classpath approach (legacy)
javac -classpath lib/commons-lang.jar:lib/gson.jar src/Main.java
java -classpath lib/*:. Main
// Module path approach
javac --module-path mods -d out --module com.example.myapp
java --module-path mods:out --module com.example.myapp/com.example.Main
// Mixed approach (migration scenario)
java --module-path mods \
--add-modules com.example.myapp \
-classpath legacy-libs/* \
com.example.Main
// module-info.java enforces dependencies
module com.example.myapp {
requires java.sql; // Must be on module path
requires commons.lang3; // Automatic module from classpath JAR
exports com.example.api; // Only this package is accessible
// com.example.internal is hidden even if classes are public
}
graph TB
subgraph "Classpath (Legacy)"
CP1[All JARs in flat namespace]
CP2[No encapsulation]
CP3[Runtime errors for missing deps]
CP4[Split packages allowed]
CP5[All public classes accessible]
end
subgraph "Module Path (JPMS)"
MP1[Structured module graph]
MP2[Strong encapsulation]
MP3[Startup verification]
MP4[Unique package ownership]
MP5[Explicit exports only]
end
CP1 -.->|migrates to| MP1
CP2 -.->|replaced by| MP2
CP3 -.->|improved by| MP3
CP4 -.->|prevented by| MP4
CP5 -.->|restricted to| MP5
style CP1 fill:#FF9800
style CP2 fill:#FF9800
style CP3 fill:#FF9800
style MP1 fill:#4CAF50
style MP2 fill:#4CAF50
style MP3 fill:#4CAF50
References:
- https://docs.oracle.com/javase/9/migrate/toc.htm
- https://www.oracle.com/corporate/features/understanding-java-9-modules.html
- https://www.baeldung.com/java-modularity
What is the Java Platform Module System (JPMS) introduced in Java 9?
The 30-Second Answer: The Java Platform Module System (JPMS), also known as Project Jigsaw, is a major feature introduced in Java 9 that adds a higher level of aggregation above packages. It provides strong encapsulation, reliable configuration, and better scalability by allowing developers to organize code into self-contained modules with explicit dependencies, making it easier to build and maintain large applications while improving security and performance.
The 2-Minute Answer (If They Want More): JPMS was introduced to address several critical issues in Java's architecture: classpath hell, weak encapsulation, and the monolithic nature of the JDK. Before Java 9, all code was accessible on the classpath, making it difficult to hide internal APIs and creating dependency management nightmares. JPMS introduces modules as a fundamental building block that sits above packages, allowing developers to explicitly declare what a module exposes and what it depends on.
The module system provides strong encapsulation by allowing you to hide internal implementation details even within public classes. Unlike packages, which could always be accessed if they were on the classpath, modules can explicitly control which packages are exported and to whom. This prevents external code from depending on internal APIs that were never meant to be public.
One of the most significant benefits is the modularization of the JDK itself. The monolithic rt.jar has been broken down into over 90 modules, allowing applications to use only the parts of the JDK they actually need. This enables the creation of smaller runtime images using jlink, which is crucial for containerized applications and microservices. The module system also provides better compile-time and runtime dependency checking, catching missing dependencies early rather than at runtime.
JPMS also improves security by reducing the surface area of the JDK and applications. By explicitly controlling access to internal APIs, it prevents reflection-based attacks on internal implementation details. The module system works seamlessly with the existing classpath mechanism, allowing gradual migration of legacy applications.
// Example: Basic module definition
module com.example.myapp {
requires java.sql;
requires java.logging;
exports com.example.myapp.api;
}
graph TD
A[Application Module] -->|requires| B[java.sql]
A -->|requires| C[java.logging]
B -->|requires| D[java.base]
C -->|requires| D
A -->|exports| E[Public API Package]
A -.->|hides| F[Internal Packages]
style A fill:#4CAF50
style E fill:#2196F3
style F fill:#FF9800
style D fill:#9C27B0
References:
- https://openjdk.org/projects/jigsaw/
- https://www.oracle.com/corporate/features/understanding-java-9-modules.html
- https://docs.oracle.com/javase/9/whatsnew/toc.htm#JSNEW-GUID-C23AFD78-C777-460B-8ACE-58BE5EA681F6
What is module-info.java and how do you define a module?
The 30-Second Answer: module-info.java is a special file placed at the root of the module's source directory that contains the module declaration. It defines the module's name, dependencies (requires), exported packages (exports), services provided or consumed, and reflection permissions (opens), serving as the module's descriptor that controls its visibility and dependencies.
The 2-Minute Answer (If They Want More): The module-info.java file is the cornerstone of JPMS. It must be placed at the root of your module's source directory (alongside your package hierarchy) and uses the module keyword to declare the module. Unlike regular Java files, module-info.java doesn't belong to any package and uses a special syntax for module declarations.
A basic module declaration includes the module name (which should follow reverse-DNS naming conventions) and can include several directives: requires for dependencies, exports for public packages, opens for reflection access, provides for service implementations, and uses for service consumption. The module name should be globally unique and typically matches your package naming convention.
When you compile a module, the module-info.java is compiled into module-info.class, which contains the module descriptor. This descriptor is read by the JVM at runtime to enforce access control and dependency resolution. The module system uses this information to construct the module graph before the application starts, ensuring all dependencies are present and there are no conflicts.
You can have different levels of accessibility: exported packages are accessible to all modules that require your module, qualified exports restrict access to specific modules, and opened packages allow deep reflection (needed for frameworks like Hibernate or Spring). The module system distinguishes between compile-time and runtime requirements using requires static for optional dependencies.
// Simple module
module com.example.simple {
exports com.example.simple.api;
}
// Complete module with various directives
module com.example.myapp {
// Dependencies
requires java.sql;
requires java.logging;
requires static com.google.common; // Optional dependency
requires transitive java.desktop; // Implied readability
// Exports
exports com.example.myapp.api;
exports com.example.myapp.utils to com.example.client; // Qualified export
// Reflection access
opens com.example.myapp.model; // For frameworks like Hibernate
opens com.example.myapp.config to com.fasterxml.jackson.databind;
// Services
uses com.example.myapp.spi.PluginService;
provides com.example.myapp.spi.PluginService
with com.example.myapp.impl.DefaultPlugin;
}
graph LR
A[Source Directory] --> B[module-info.java]
A --> C[com/example/myapp/...]
B -->|compiles to| D[module-info.class]
D --> E[Module Descriptor]
E --> F[Name: com.example.myapp]
E --> G[Requires: java.sql, ...]
E --> H[Exports: api packages]
E --> I[Opens: reflection access]
style B fill:#4CAF50
style D fill:#2196F3
style E fill:#FF9800
References:
- https://docs.oracle.com/javase/specs/jls/se11/html/jls-7.html#jls-7.7
- https://www.baeldung.com/java-9-modularity
- https://www.oracle.com/uk/corporate/features/understanding-java-9-modules.html
Other Java 11 Features
What is the purpose of the Files.readString() and Files.writeString() methods?
The 30-Second Answer:
Files.readString() and Files.writeString() are convenience methods introduced in Java 11 for reading and writing text files in a single line of code. They simplify the common task of reading entire file contents into a String or writing a String to a file, eliminating the need for BufferedReader/Writer boilerplate code.
The 2-Minute Answer (If They Want More):
Before Java 11, reading a text file into a String required multiple lines of code with try-with-resources, BufferedReader, and manual string concatenation or stream operations. The new Files.readString() method handles all of this internally, making file I/O much more concise and readable.
The Files.readString(Path path) method reads all content from a file into a String, decoding from bytes to characters using UTF-8 charset by default. You can also specify a custom charset with Files.readString(Path path, Charset charset). Similarly, Files.writeString(Path path, CharSequence csq, OpenOption... options) writes a CharSequence to a file, creating the file if it doesn't exist or truncating it if it does.
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
public class FilesExample {
public static void main(String[] args) throws IOException {
Path filePath = Path.of("example.txt");
// Write string to file
String content = "Hello, Java 11!\nThis is a new feature.";
Files.writeString(filePath, content);
// Read string from file
String readContent = Files.readString(filePath);
System.out.println(readContent);
// Append to file with custom options
Files.writeString(filePath, "\nAppended line",
StandardOpenOption.APPEND);
// Read with specific charset
String contentUtf8 = Files.readString(filePath,
java.nio.charset.StandardCharsets.UTF_8);
}
}
These methods are particularly useful for reading configuration files, small text files, or when writing test data. However, they're not recommended for very large files as they load the entire content into memory. For large files, traditional streaming approaches with BufferedReader/Writer are still preferable.
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/file/Files.html#readString(java.nio.file.Path)
- https://www.baeldung.com/java-11-new-features
- https://openjdk.org/jeps/323
What is the Pattern.asMatchPredicate() method?
The 30-Second Answer:
Pattern.asMatchPredicate() is a method introduced in Java 11 that returns a Predicate that tests if the entire input sequence matches the pattern. Unlike asPredicate() which finds a match anywhere in the input, asMatchPredicate() requires the entire string to match the pattern, equivalent to using matches().
The 2-Minute Answer (If They Want More):
The Pattern class in Java 11 gained the asMatchPredicate() method to complement the existing asPredicate() method. While asPredicate() returns a predicate that finds a partial match (equivalent to find()), asMatchPredicate() returns a predicate that requires a complete match of the entire input (equivalent to matches()).
This distinction is crucial when working with stream operations and you need to validate that strings conform to a specific format rather than just contain a pattern. The method returns a Predicate<String> that can be used directly in stream filter operations, making regex-based filtering more concise and functional.
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class PatternPredicateExample {
public static void main(String[] args) {
List<String> inputs = List.of(
"12345",
"abc123",
"123",
"67890",
"test"
);
Pattern digitPattern = Pattern.compile("\\d+");
// asPredicate() - finds pattern anywhere in string
List<String> containsDigits = inputs.stream()
.filter(digitPattern.asPredicate())
.collect(Collectors.toList());
System.out.println("Contains digits: " + containsDigits);
// [12345, abc123, 123, 67890]
// asMatchPredicate() - entire string must match pattern
List<String> onlyDigits = inputs.stream()
.filter(digitPattern.asMatchPredicate())
.collect(Collectors.toList());
System.out.println("Only digits: " + onlyDigits);
// [12345, 123, 67890]
// Email validation example
Pattern emailPattern = Pattern.compile(
"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
);
List<String> emails = List.of(
"user@example.com",
"invalid.email",
"test@domain.co.uk",
"@wrong.com",
"correct@test.org"
);
List<String> validEmails = emails.stream()
.filter(emailPattern.asMatchPredicate())
.collect(Collectors.toList());
System.out.println("Valid emails: " + validEmails);
// [user@example.com, test@domain.co.uk, correct@test.org]
}
}
The asMatchPredicate() method enhances the functional programming capabilities of Java by making pattern matching operations more streamlined and composable within the Stream API. It's particularly useful for validation scenarios where you need to ensure data conforms to specific formats.
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Pattern.html#asMatchPredicate()
- https://www.baeldung.com/java-regex-pattern-as-predicate
- https://openjdk.org/jeps/323
Optional API Enhancements
What is the or() method in Optional?
The 30-Second Answer:
The or() method returns the current Optional if a value is present, otherwise it returns an Optional produced by a supplying function. Unlike orElse() which returns an unwrapped value, or() returns another Optional, enabling chaining of multiple Optional sources without unwrapping until necessary.
The 2-Minute Answer (If They Want More):
The or() method accepts a Supplier<? extends Optional<? extends T>> and provides lazy evaluation of alternative Optional values. This is fundamentally different from orElse() and orElseGet(), which return unwrapped values. The method signature Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) makes it clear that you're working with Optional-wrapped values throughout the chain.
This method is particularly powerful when you have multiple potential sources for a value and want to try them in sequence. For example, you might first check a cache, then a local database, and finally a remote API, each returning an Optional. The or() method allows you to compose these lookups elegantly without unwrapping and re-wrapping values.
The lazy evaluation aspect is crucial - the supplier is only invoked if the current Optional is empty. This means expensive operations in fallback sources are avoided when earlier sources succeed. This makes or() ideal for implementing fallback chains, multi-tier caching strategies, and conditional resource loading.
A common pattern is to chain multiple or() calls together, creating a waterfall of potential sources. This results in highly readable code that clearly expresses the priority and fallback logic. The final value is typically extracted using terminal operations like orElse(), orElseGet(), or orElseThrow().
// Chaining multiple Optional sources
public Optional<String> findConfiguration(String key) {
return findInMemory(key)
.or(() -> findInLocalCache(key))
.or(() -> findInDatabase(key))
.or(() -> findInRemoteConfig(key));
}
// Usage with terminal operation
String config = findConfiguration("api.key")
.orElse("default-value");
// Practical example: User lookup from multiple sources
public Optional<User> findUser(String userId) {
return sessionCache.get(userId)
.or(() -> database.findById(userId))
.or(() -> legacySystem.lookupUser(userId));
}
User user = findUser("123")
.orElseThrow(() -> new UserNotFoundException("User 123 not found"));
// Conditional fallback
Optional<String> primary = getPrimaryValue();
Optional<String> secondary = getSecondaryValue();
String result = primary
.or(() -> secondary)
.orElse("default");
// Complex example with different types (using map)
public Optional<Configuration> loadConfig(String env) {
return loadFromFile(env)
.or(() -> loadFromEnvironment(env))
.or(() -> Optional.of(Configuration.getDefault()));
}
// Lazy evaluation demonstration
Optional<String> expensive = Optional.empty()
.or(() -> {
System.out.println("Expensive computation executed");
return Optional.of("expensive result");
});
// "Expensive computation executed" prints only if needed
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Optional.html#or(java.util.function.Supplier)
- https://www.baeldung.com/java-9-optional#or
- https://4comprehension.com/java-9-optional-or-method/
What is orElseThrow() without arguments?
The 30-Second Answer:
The orElseThrow() method without arguments, introduced in Java 10, returns the value if present or throws NoSuchElementException if empty. It serves as a clearer, more semantically meaningful replacement for the get() method, making code more self-documenting and explicit about the throwing behavior.
The 2-Minute Answer (If They Want More):
Prior to Java 10, the get() method was used to extract values from Optional, but its name didn't clearly indicate that it could throw an exception. This led to confusion and runtime errors when developers used get() without first checking isPresent(). The orElseThrow() method without arguments addresses this by having a name that explicitly communicates the throwing behavior, improving code clarity and intent.
The method signature is simply T orElseThrow() and it throws NoSuchElementException when called on an empty Optional. This is functionally identical to get(), but the naming follows the pattern established by other orElseThrow() variants and makes it immediately clear to code readers that an exception is part of the method's contract.
This method should be used when you're confident the Optional contains a value, typically because of prior validation or business logic guarantees. However, it's generally better to use other Optional methods like orElse(), orElseGet(), ifPresent(), or the parameterized orElseThrow(Supplier) for better error handling and more robust code. The no-argument version is best suited for cases where the absence of a value genuinely represents a programming error rather than a recoverable condition.
The Java team has even considered deprecating get() in favor of orElseThrow() to encourage better practices. Using orElseThrow() makes code reviews easier and helps new developers understand that exceptions are intentional rather than overlooked edge cases. It aligns with the principle of making code self-documenting and reducing cognitive load.
// Basic usage
Optional<String> opt = Optional.of("Hello");
String value = opt.orElseThrow(); // Returns "Hello"
Optional<String> empty = Optional.empty();
String error = empty.orElseThrow(); // Throws NoSuchElementException
// Replacing get() method
// Old way (Java 8-9):
String oldWay = optional.get();
// New way (Java 10+):
String newWay = optional.orElseThrow();
// Practical example: After validation
public User getAuthenticatedUser() {
Optional<User> userOpt = getCurrentUser();
if (userOpt.isEmpty()) {
throw new AuthenticationException("Not authenticated");
}
// At this point, we know it's present
return userOpt.orElseThrow();
}
// In stream pipelines
List<String> values = ids.stream()
.map(this::findById)
.filter(Optional::isPresent)
.map(Optional::orElseThrow) // Safe because of filter
.collect(Collectors.toList());
// Better alternative with custom exception
Optional<User> userOpt = findUser(id);
// Instead of:
User user1 = userOpt.orElseThrow();
// Prefer:
User user2 = userOpt.orElseThrow(
() -> new UserNotFoundException("User " + id + " not found")
);
// Comparison with other methods
Optional<String> opt = findValue();
// Throws NoSuchElementException with generic message
String a = opt.orElseThrow();
// Throws custom exception with meaningful message
String b = opt.orElseThrow(
() -> new CustomException("Value not found")
);
// Returns default value (no exception)
String c = opt.orElse("default");
// Lazily computes default (no exception)
String d = opt.orElseGet(() -> computeDefault());
// When to use orElseThrow()
public Configuration getMandatoryConfig(String key) {
// Config must exist or it's a programming error
return configMap.get(key)
.orElseThrow(); // Appropriate use
}
public User getUserOrDefault(String id) {
// User might not exist, handle gracefully
return findUser(id)
.orElse(User.guest()); // Better than orElseThrow()
}
References:
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Optional.html#orElseThrow()
- https://www.baeldung.com/java-optional-or-else-throw
- https://openjdk.org/jeps/323
Local Variable Type Inference (var)
What is local variable type inference (var) introduced in Java 10?
The 30-Second Answer:
Local variable type inference, introduced in Java 10, allows developers to use the var keyword to declare local variables without explicitly specifying their type. The compiler infers the type from the initializer expression, reducing boilerplate code while maintaining Java's static typing and type safety.
The 2-Minute Answer (If They Want More):
Local variable type inference with var is a language feature that simplifies variable declarations by letting the compiler deduce the type from the right-hand side of the assignment. This feature doesn't make Java dynamically typed—it's purely compile-time syntactic sugar. Once the compiler infers the type, the variable is as strongly typed as if you had declared it explicitly.
The feature was introduced in Java 10 (JEP 286) to reduce verbosity and improve code readability, especially when dealing with complex generic types. For example, instead of writing Map<String, List<Customer>> customerMap = new HashMap<>(), you can write var customerMap = new HashMap<String, List<Customer>>().
It's important to understand that var is not a type itself—it's a reserved type name that signals the compiler to infer the actual type. The bytecode generated is identical to explicitly typed code, so there's no runtime performance impact. The feature only works for local variables with initializers; it cannot be used for fields, method parameters, or method return types.
The introduction of var brought Java closer to modern programming language ergonomics while preserving its commitment to static typing and compile-time type safety. It's particularly useful when the type is obvious from the context or when dealing with verbose generic type declarations.
// Traditional explicit typing
HashMap<String, List<Customer>> customerMap = new HashMap<>();
// Using var - compiler infers HashMap<String, List<Customer>>
var customerMap = new HashMap<String, List<Customer>>();
// Clear from context
var customers = getCustomers(); // type inferred from method return type
// Iteration
for (var entry : map.entrySet()) { // type is Map.Entry<K, V>
var key = entry.getKey();
var value = entry.getValue();
}
References:
- https://openjdk.org/jeps/286
- https://docs.oracle.com/en/java/javase/11/language/local-variable-type-inference.html
- https://developer.oracle.com/learn/technical-articles/jdk-10-local-variable-type-inference
Stream API Enhancements
What new Stream methods were added in Java 9?
The 30-Second Answer:
Java 9 added four new Stream methods: takeWhile() and dropWhile() for conditional element processing, ofNullable() for creating streams from potentially null values, and an overloaded iterate() method that accepts a termination predicate. These methods enhance Stream API's expressiveness and make common operations more concise and intuitive.
The 2-Minute Answer (If They Want More):
The Stream API enhancements in Java 9 addressed several gaps in the original API. The takeWhile() method allows you to take elements from a stream while a predicate is true, stopping at the first element that doesn't match. Its counterpart dropWhile() skips elements while the predicate is true and processes everything after. These are particularly useful for ordered streams where you want to process a prefix or skip a prefix based on conditions.
The ofNullable() method solves the common problem of creating a stream from a value that might be null. Before Java 9, you had to use awkward workarounds like Optional.ofNullable(value).map(Stream::of).orElseGet(Stream::empty). Now you can simply use Stream.ofNullable(value) which returns either a single-element stream or an empty stream.
The new iterate() overload adds a hasNext-style predicate, making it work more like a traditional for-loop. The original iterate(seed, unaryOperator) created infinite streams that required limit() to terminate. The new version iterate(seed, predicate, unaryOperator) terminates naturally when the predicate becomes false, which is more intuitive and safer.
These additions make the Stream API more complete and reduce the need for workarounds or custom implementations when dealing with common streaming scenarios.
// takeWhile and dropWhile examples
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Take elements while they're less than 5
List<Integer> taken = numbers.stream()
.takeWhile(n -> n < 5)
.collect(Collectors.toList());
// Result: [1, 2, 3, 4]
// Drop elements while they're less than 5
List<Integer> dropped = numbers.stream()
.dropWhile(n -> n < 5)
.collect(Collectors.toList());
// Result: [5, 6, 7, 8, 9, 10]
// ofNullable example
String value = getUserInput(); // might return null
Stream<String> stream = Stream.ofNullable(value);
// Returns empty stream if value is null, otherwise single-element stream
// New iterate with predicate
Stream.iterate(0, n -> n < 10, n -> n + 1)
.forEach(System.out::println);
// Prints 0 through 9, terminates at 10
References:
- https://docs.oracle.com/javase/9/docs/api/java/util/stream/Stream.html
- https://www.baeldung.com/java-9-stream-api
- https://openjdk.org/jeps/269
Migration and Compatibility
What are the challenges of migrating from Java 8 to Java 11?
The 30-Second Answer:
The main challenges include removed Java EE modules (JAXB, JAX-WS, CORBA), discontinued support for Java applets and Web Start, changes to the internal API (sun.* packages), module system requirements, and updated garbage collector defaults. Additionally, the shift to a six-month release cycle means adjusting to a new versioning and support model.
The 2-Minute Answer (If They Want More):
The most immediate challenge is handling removed Java EE and CORBA modules. Java 11 removed modules that were previously bundled with the JDK, including java.xml.bind (JAXB), java.xml.ws (JAX-WS), java.activation, java.corba, and java.transaction. Applications using these APIs will fail to compile or run unless you add third-party dependencies.
Internal API dependencies pose another significant challenge. Many libraries and frameworks relied on internal APIs like sun.misc.Unsafe or accessed internal packages through reflection. While some internal APIs are still accessible via --add-exports or --add-opens flags, this is a temporary workaround. The strong encapsulation introduced by the module system (Project Jigsaw) restricts access to JDK internals.
Tooling and dependency compatibility can be problematic. Build tools (Maven, Gradle), IDE plugins, testing frameworks, and third-party libraries may need updates. For example, older versions of Maven Surefire plugin, AspectJ, or Mockito might not work correctly with Java 11. You'll need to update dependencies and verify compatibility.
Runtime behavior changes include different garbage collector defaults (G1GC is now default instead of Parallel GC), removed GC options (like CMS), string concatenation optimization changes (via invokedynamic), and stricter SecurityManager policies. These can affect performance characteristics and application behavior in subtle ways.
# Common compilation error when JAXB is missing
Error: package javax.xml.bind does not exist
Error: cannot find symbol class JAXBContext
# Runtime error for missing modules
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
References:
- https://docs.oracle.com/en/java/javase/11/migrate/index.html
- https://blogs.oracle.com/javamagazine/post/oracle-jdk-releases-explained
- https://dzone.com/articles/8-steps-for-migrating-existing-applications-to-jav