Java 17 Interview Questions (Free Preview)
Free sample of 15 from 47 questions available
Text Blocks
What is incidental whitespace versus essential whitespace?
The 30-Second Answer: Incidental whitespace is the indentation added to align the text block with surrounding code, which is automatically removed by the compiler. Essential whitespace is meaningful whitespace that should be preserved in the final string value, such as formatting within the content itself.
The 2-Minute Answer (If They Want More): Understanding the distinction between incidental and essential whitespace is crucial for effectively using text blocks. Incidental whitespace exists purely for code formatting purposes—it's the indentation you add so that your text block aligns nicely with the surrounding Java code structure. Essential whitespace, on the other hand, is part of the actual string content and must be preserved in the final runtime value.
The Java compiler automatically identifies and removes incidental whitespace by analyzing the indentation pattern of all lines in the text block. It finds the minimum common indentation (the smallest number of leading spaces on any non-blank line, including the closing delimiter line) and strips that amount from every line. Any whitespace beyond this minimum is considered essential and is preserved.
This intelligent handling allows you to write naturally formatted code without worrying about unwanted indentation creeping into your string values. For example, if you're defining a JSON string inside a deeply nested method, you can indent the entire text block to match your code's indentation level, and the compiler will automatically remove that structural indentation while preserving the JSON's internal formatting.
Trailing whitespace on each line is generally considered essential and is preserved by default. However, if you want to ensure trailing whitespace is visible in your code editor, you can use the \s escape sequence to make it explicit and prevent it from being accidentally removed by editor auto-formatting.
public class WhitespaceExample {
public void demonstrateWhitespace() {
// Example showing incidental vs essential whitespace
String poem = """
The road goes ever on and on
Down from the door where it began
Now far ahead the road has gone
And I must follow if I can
""";
// All lines have 22 spaces of incidental indentation (removed)
// Line 2 and 4 have 2 additional spaces of essential indentation (preserved)
// Explicit trailing whitespace using \s escape
String withTrailing = """
Line one\s
Line two\s\s
Line three
""";
// \s represents a space and won't be stripped by editors
// Blank lines preserve their position but not indentation
String withBlankLines = """
First paragraph
Second paragraph
""";
// Visual indentation guide using closing delimiter
String sql = """
SELECT
customer_id,
order_date,
total_amount
FROM orders
WHERE status = 'completed'
"""; // Closing delimiter position determines incidental whitespace
// Demonstrating stripIndent() - manually strip incidental whitespace
String raw = " Line 1\n Line 2\n Line 3";
String stripped = raw.stripIndent();
// Result: "Line 1\n Line 2\nLine 3"
// Demonstrating indent(n) - add indentation
String base = """
Line 1
Line 2
""";
String indented = base.indent(4);
// Each line gets 4 additional spaces
}
}
References:
- https://openjdk.org/jeps/378
- https://docs.oracle.com/en/java/javase/17/language/text-blocks.html
- https://cr.openjdk.org/~jlaskey/Strings/TextBlocksGuide_v9.html
Java 17 LTS Overview
What is the difference between Java 17 and Java 11 LTS?
The 30-Second Answer: Java 17 adds sealed classes, pattern matching for switch, records (finalized), text blocks, and improved switch expressions compared to Java 11. It removes deprecated APIs like Applet and Nashorn JavaScript engine, strengthens JDK encapsulation, and includes significant performance improvements and garbage collector enhancements.
The 2-Minute Answer (If They Want More): The gap between Java 11 (2018) and Java 17 (2021) spans three years and includes features from Java 12-17. Major language enhancements include Records (JEP 395) for immutable data carriers, Text Blocks (JEP 378) for multi-line strings, enhanced Switch Expressions (JEP 361), and Sealed Classes for controlled inheritance. These features significantly improve code readability and reduce boilerplate.
Performance and runtime improvements are substantial. Java 17 includes ZGC and Shenandoah GC improvements, better G1 collector performance, and various JVM optimizations. The Nashorn JavaScript engine was removed (JEP 372), and strong encapsulation of JDK internals became the default, which may break code using reflection to access internal APIs.
API additions include helpful NullPointerException messages showing which variable was null, Stream.toList() for simpler collection conversions, and improved java.time APIs. Security enhancements include better deserialization filtering and removal of weak cryptographic algorithms. The migration from Java 11 to 17 is generally smoother than previous LTS transitions due to better backward compatibility focus.
// Java 17 features not in Java 11
// Records (immutable data classes)
public record Person(String name, int age) {}
// Text blocks
String json = """
{
"name": "John",
"age": 30
}
""";
// Enhanced switch expressions
String result = switch (day) {
case MONDAY, FRIDAY -> "Work day";
case SATURDAY, SUNDAY -> "Weekend";
default -> "Midweek";
};
// Stream.toList() - simpler than collect(Collectors.toList())
List<String> names = people.stream()
.map(Person::name)
.toList();
// Helpful NullPointerException messages
// Before Java 14: NullPointerException
// Java 17: NullPointerException: Cannot invoke "String.length()" because "name" is null
String name = null;
int length = name.length(); // Clear message about which variable is null
References:
- https://docs.oracle.com/en/java/javase/17/migrate/getting-started.html
- https://www.baeldung.com/java-11-vs-17
- https://advancedweb.hu/a-categorized-list-of-all-java-and-jvm-features-since-jdk-8-to-17/
Sealed Classes and Interfaces
What is the difference between sealed, non-sealed, and final modifiers?
The 30-Second Answer:
sealed allows only specified subclasses to extend a class, final prevents any subclassing entirely, and non-sealed reopens the hierarchy allowing any class to extend. These modifiers give you granular control over inheritance at each level of a class hierarchy.
The 2-Minute Answer (If They Want More): These three modifiers represent different levels of inheritance control in Java's type system, each serving distinct purposes in object-oriented design. Understanding when to use each is crucial for creating maintainable and type-safe class hierarchies.
The final modifier is the most restrictive—it completely prevents inheritance. When you mark a class as final, you're declaring that this is the end of the line in the inheritance chain. This is useful for classes that shouldn't be extended for security, performance, or design reasons (like String, Integer, and other Java wrapper classes). In the context of sealed classes, a final permitted subclass represents a leaf node in your type hierarchy.
The sealed modifier offers controlled inheritance—it allows subclassing, but only by classes you explicitly permit. This gives you the benefits of polymorphism while maintaining complete control over the set of possible implementations. When a permitted subclass is itself sealed, you're creating a multi-level controlled hierarchy where each level specifies its own set of permitted subclasses. This is perfect for modeling domain concepts with nested categorizations.
The non-sealed modifier is the escape hatch—it reopens the hierarchy at a specific point, allowing unrestricted inheritance from that class forward. When you mark a permitted subclass as non-sealed, you're saying "I control the hierarchy up to this point, but from here on, anyone can extend this class." This is useful when you want a controlled core hierarchy but need flexibility at the edges, or when you're migrating existing code to use sealed classes but need to maintain backward compatibility with existing subclasses.
// Demonstrating all three modifiers in action
public sealed class Employee
permits FullTimeEmployee, Contractor, Intern {
protected String name;
protected String employeeId;
protected Employee(String name, String employeeId) {
this.name = name;
this.employeeId = employeeId;
}
public abstract double calculatePay();
}
// FINAL: No further subclassing allowed
// Use when: This is a leaf node with complete implementation
final class FullTimeEmployee extends Employee {
private double annualSalary;
public FullTimeEmployee(String name, String employeeId, double annualSalary) {
super(name, employeeId);
this.annualSalary = annualSalary;
}
@Override
public double calculatePay() {
return annualSalary / 12;
}
}
// SEALED: Controlled subclassing - only specified classes can extend
// Use when: You want another level of type refinement with control
sealed class Contractor extends Employee
permits OnSiteContractor, RemoteContractor {
protected double hourlyRate;
protected Contractor(String name, String employeeId, double hourlyRate) {
super(name, employeeId);
this.hourlyRate = hourlyRate;
}
@Override
public double calculatePay() {
return hourlyRate * 160; // Assuming 160 hours/month
}
}
final class OnSiteContractor extends Contractor {
private double parkingAllowance;
public OnSiteContractor(String name, String employeeId, double hourlyRate, double parkingAllowance) {
super(name, employeeId, hourlyRate);
this.parkingAllowance = parkingAllowance;
}
@Override
public double calculatePay() {
return super.calculatePay() + parkingAllowance;
}
}
final class RemoteContractor extends Contractor {
private double internetAllowance;
public RemoteContractor(String name, String employeeId, double hourlyRate, double internetAllowance) {
super(name, employeeId, hourlyRate);
this.internetAllowance = internetAllowance;
}
@Override
public double calculatePay() {
return super.calculatePay() + internetAllowance;
}
}
// NON-SEALED: Opens the hierarchy - anyone can extend
// Use when: You need flexibility or backward compatibility
non-sealed class Intern extends Employee {
protected double monthlyStipend;
public Intern(String name, String employeeId, double monthlyStipend) {
super(name, employeeId);
this.monthlyStipend = monthlyStipend;
}
@Override
public double calculatePay() {
return monthlyStipend;
}
}
// This is allowed because Intern is non-sealed
class SummerIntern extends Intern {
private int projectCount;
public SummerIntern(String name, String employeeId, double monthlyStipend, int projectCount) {
super(name, employeeId, monthlyStipend);
this.projectCount = projectCount;
}
@Override
public double calculatePay() {
return super.calculatePay() + (projectCount * 100); // Bonus per project
}
}
// This is also allowed - open hierarchy continues
class ResearchIntern extends Intern {
public ResearchIntern(String name, String employeeId, double monthlyStipend) {
super(name, employeeId, monthlyStipend);
}
}
// Practical example: HTTP Response modeling
sealed interface HttpResponse
permits SuccessResponse, ErrorResponse {
int statusCode();
}
// Final - concrete success responses
final class SuccessResponse implements HttpResponse {
private final int code;
private final String body;
public SuccessResponse(int code, String body) {
this.code = code;
this.body = body;
}
@Override
public int statusCode() {
return code;
}
public String body() {
return body;
}
}
// Sealed - further categorize error types
sealed class ErrorResponse implements HttpResponse
permits ClientError, ServerError {
protected final int code;
protected final String message;
protected ErrorResponse(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public int statusCode() {
return code;
}
}
final class ClientError extends ErrorResponse {
public ClientError(int code, String message) {
super(code, message);
}
}
final class ServerError extends ErrorResponse {
public ServerError(int code, String message) {
super(code, message);
}
}
classDiagram
class Employee {
<<sealed>>
#name String
#employeeId String
+calculatePay() double
}
class FullTimeEmployee {
<<final>>
-annualSalary double
+calculatePay() double
}
class Contractor {
<<sealed>>
#hourlyRate double
+calculatePay() double
}
class OnSiteContractor {
<<final>>
-parkingAllowance double
+calculatePay() double
}
class RemoteContractor {
<<final>>
-internetAllowance double
+calculatePay() double
}
class Intern {
<<non-sealed>>
#monthlyStipend double
+calculatePay() double
}
class SummerIntern {
-projectCount int
+calculatePay() double
}
class ResearchIntern
class AnyOtherInternType {
<<open hierarchy>>
}
Employee <|-- FullTimeEmployee : permits & final
Employee <|-- Contractor : permits & sealed
Employee <|-- Intern : permits & non-sealed
Contractor <|-- OnSiteContractor : permits & final
Contractor <|-- RemoteContractor : permits & final
Intern <|-- SummerIntern : open
Intern <|-- ResearchIntern : open
Intern <|-- AnyOtherInternType : open
References:
- https://docs.oracle.com/en/java/javase/17/language/sealed-classes-and-interfaces.html
- https://www.baeldung.com/java-sealed-classes-interfaces
- https://blogs.oracle.com/javamagazine/post/java-sealed-classes-fight-ambiguity
Records
What is the difference between records and traditional POJOs?
The 30-Second Answer: Records are immutable, transparent data carriers with automatically generated boilerplate code, while traditional POJOs are flexible, often mutable classes where you must manually write all code. Records emphasize data modeling and value semantics, whereas POJOs support any object-oriented pattern including inheritance, mutable state, and encapsulation of implementation details.
The 2-Minute Answer (If They Want More): The fundamental difference between records and traditional POJOs lies in their design philosophy and use cases. Records are specifically designed for transparent, immutable data modeling where the class is primarily a carrier for a fixed set of values. Traditional POJOs (Plain Old Java Objects) are general-purpose classes that support the full range of object-oriented programming patterns, including inheritance, encapsulation, and mutable state.
From a code perspective, records dramatically reduce boilerplate. A one-line record declaration replaces 30-50 lines of POJO code including constructors, getters, equals(), hashCode(), and toString(). The compiler generates all these methods automatically based on the record's components. In contrast, POJOs require you to write (or generate with IDE/tools) all this code manually, giving you complete control but at the cost of verbosity.
Semantically, records are immutable by default with all fields being final. This makes them thread-safe and suitable for use as map keys, set elements, and in functional programming contexts. POJOs can be mutable or immutable depending on how you design them, offering flexibility but requiring discipline to ensure correctness. Records also provide value-based semantics: two records are equal if all their components are equal, making them ideal for value objects and DTOs.
Regarding extensibility, records cannot be extended and cannot extend other classes, though they can implement interfaces. This limitation is intentional: records are meant to be simple, final data carriers. POJOs support full inheritance hierarchies, allowing for polymorphism and code reuse through inheritance. When you need inheritance, complex behavior, or mutable state, traditional POJOs are the right choice. When you need simple, immutable data carriers with value semantics, records are ideal.
// Traditional POJO
public class PersonPOJO {
private String name;
private int age;
public PersonPOJO() {} // Default constructor
public PersonPOJO(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and setters (mutable)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonPOJO that = (PersonPOJO) o;
return age == that.age && Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "PersonPOJO{name='" + name + "', age=" + age + "}";
}
}
// Equivalent Record (immutable, concise)
public record PersonRecord(String name, int age) {}
// Usage comparison
PersonPOJO pojo = new PersonPOJO("Alice", 30);
pojo.setAge(31); // Mutable - can change state
PersonRecord record = new PersonRecord("Alice", 30);
// record.age = 31; // Compilation error - immutable
// To "change" a record, create a new instance
PersonRecord updated = new PersonRecord(record.name(), 31);
// POJOs support inheritance
class Employee extends PersonPOJO {
private String employeeId;
// Additional fields and methods
}
// Records cannot be extended
// class Employee extends PersonRecord {} // Compilation error
// Records can implement interfaces
interface Named {
String name();
}
record PersonWithInterface(String name, int age) implements Named {
// name() method already provided by record
}
| Aspect | Records | Traditional POJOs |
|---|---|---|
| Mutability | Immutable (all fields final) | Mutable or immutable (your choice) |
| Boilerplate | Auto-generated (1 line) | Manual (30+ lines) |
| Inheritance | Cannot extend or be extended | Full inheritance support |
| Extensibility | Final by design | Can be abstract, final, or open |
| Accessors | component() methods |
getComponent() methods |
| Purpose | Data carriers, value objects | General-purpose objects |
| Thread Safety | Always thread-safe | Depends on implementation |
| Equality | Value-based (automatic) | Reference or value (manual) |
| Use Cases | DTOs, API responses, value objects | Business logic, entities, complex behavior |
References:
- https://www.baeldung.com/java-record-vs-lombok
- https://blogs.oracle.com/javamagazine/post/records-come-to-java
- https://openjdk.org/jeps/395
What is a compact constructor in records?
The 30-Second Answer: A compact constructor is a special constructor syntax for records that allows you to add validation or normalization logic without explicitly declaring parameters or assignments. In a compact constructor, you don't list parameters (they're implicit) or assign values to fields (done automatically after the constructor body), making the code more concise and focused on validation logic.
The 2-Minute Answer (If They Want More): Records provide a canonical constructor automatically, but sometimes you need to add validation or transformation logic. Java offers two ways to customize initialization: explicit canonical constructors and compact constructors. The compact constructor is the more elegant option when you only need to validate or normalize the input data.
In a compact constructor, you write the constructor body without declaring parameters or making field assignments. The parameters are implicitly available with the same names as the record components, and after your constructor body executes, the assignments happen automatically. This makes the code cleaner and more maintainable because you focus solely on the validation or transformation logic.
Compact constructors are particularly useful for implementing defensive copying, null checks, range validation, and data normalization. They're syntactically simpler than explicit constructors and clearly communicate that the constructor is primarily about validation rather than complex initialization logic. The compact form eliminates redundancy and makes the record's intent more obvious.
It's important to note that in a compact constructor, you can reassign the implicit parameters to modify the values that will be stored in the record's fields. This allows for normalization (like trimming strings or clamping values) before the automatic assignment occurs.
// Compact constructor with validation
public record Rectangle(double width, double height) {
// Compact constructor - no parameters or assignments needed
public Rectangle {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(
"Width and height must be positive"
);
}
}
}
// Compact constructor with normalization
public record Email(String address) {
public Email {
// Normalize the email before assignment
address = address.toLowerCase().trim();
if (!address.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
// Equivalent explicit canonical constructor (more verbose)
public record Rectangle(double width, double height) {
public Rectangle(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(
"Width and height must be positive"
);
}
this.width = width;
this.height = height;
}
}
// Usage
Rectangle rect = new Rectangle(10.0, 5.0); // OK
Email email = new Email(" USER@Example.COM ");
System.out.println(email.address()); // user@example.com
// Rectangle invalid = new Rectangle(-5.0, 10.0); // Throws exception
References:
- https://docs.oracle.com/en/java/javase/17/language/records.html#GUID-6699E26F-4A9B-4393-A08B-1E47D4B2D263
- https://www.baeldung.com/java-record-constructor-validation
- https://openjdk.org/jeps/395#Compact-constructors
Switch Expressions
What is the yield keyword and when do you use it?
The 30-Second Answer:
The yield keyword is used in switch expressions to return a value from a block of code when you need multiple statements before producing a result. It's similar to return but specifically designed for switch expressions and can be used with both arrow and colon syntax.
The 2-Minute Answer (If They Want More):
The yield keyword was introduced in Java 13 (as a preview) and finalized in Java 14 as part of the switch expression enhancement. It serves as a way to "yield" or produce a value from within a switch case when you have a code block with multiple statements. While single-expression cases can directly return their value, multi-statement blocks need yield to specify what value the switch expression should evaluate to.
You must use yield in these scenarios:
- When using a code block
{ }with arrow syntax that contains multiple statements - When using traditional colon syntax in a switch expression (even for single statements)
- When you need to perform complex logic before determining the return value
The yield keyword transfers control back to the switch expression with a value. Unlike return, which exits the entire method, yield only exits the switch expression. This makes switch expressions composable—you can use them anywhere an expression is expected, including as arguments to method calls or as parts of larger expressions.
Code Examples:
// Using yield with arrow syntax and code blocks
int value = switch (status) {
case ACTIVE -> {
System.out.println("Processing active status");
int baseValue = 100;
int bonus = 50;
yield baseValue + bonus; // yield required for multi-statement block
}
case INACTIVE -> 0; // Single expression, no yield needed
case PENDING -> {
System.out.println("Calculating pending value");
yield 25; // yield required even though this could be simplified
}
};
// Using yield with traditional colon syntax
String description = switch (errorCode) {
case 404:
System.out.println("Resource not found");
yield "Page Not Found"; // yield required with colon syntax
case 500:
System.out.println("Internal error");
yield "Server Error";
case 200:
yield "Success";
default:
yield "Unknown Status";
};
// Complex logic with yield
double price = switch (membershipLevel) {
case BRONZE -> {
double basePrice = 100.0;
double discount = 0.05;
yield basePrice * (1 - discount);
}
case SILVER -> {
double basePrice = 100.0;
double discount = 0.10;
double bonusDiscount = isPremium() ? 0.05 : 0.0;
yield basePrice * (1 - discount - bonusDiscount);
}
case GOLD -> {
if (isPremium()) {
yield 75.0;
} else {
yield 80.0;
}
}
default -> 100.0; // Arrow with single expression, no yield needed
};
// yield in nested structures
int result = switch (type) {
case A -> {
if (condition) {
yield 1;
} else {
yield 2;
}
}
case B -> {
for (int i = 0; i < 10; i++) {
if (checkCondition(i)) {
yield i;
}
}
yield -1; // All paths must yield
}
default -> 0;
};
When to Use yield:
| Scenario | Need yield? | Example |
|---|---|---|
| Arrow syntax, single expression | No | case A -> 42; |
| Arrow syntax, code block | Yes | case A -> { yield 42; } |
| Colon syntax, any case | Yes | case A: yield 42; |
| Multiple statements before value | Yes | case A -> { log(); yield 42; } |
| Conditional logic in case | Yes | case A -> { if(x) yield 1; else yield 2; } |
Common Mistakes:
// WRONG: Missing yield in block
int x = switch (val) {
case 1 -> {
int result = compute();
result; // Compile error: not a statement
}
};
// CORRECT: Use yield
int x = switch (val) {
case 1 -> {
int result = compute();
yield result;
}
};
// WRONG: Using return instead of yield
int x = switch (val) {
case 1 -> {
return 42; // Compile error: return exits method, not switch
}
};
// CORRECT: Use yield
int x = switch (val) {
case 1 -> {
yield 42;
}
};
References:
- https://openjdk.org/jeps/361
- https://docs.oracle.com/en/java/javase/17/language/switch-expressions.html
- https://www.baeldung.com/java-switch-pattern-matching
What is exhaustiveness in switch expressions?
The 30-Second Answer: Exhaustiveness in switch expressions means that all possible input values must be handled—either through explicit cases or a default clause. The compiler enforces this requirement, ensuring that the switch expression can always produce a value and preventing runtime errors from unhandled cases.
The 2-Minute Answer (If They Want More):
Exhaustiveness is a compile-time guarantee that a switch expression will always produce a value for any possible input. Unlike traditional switch statements (which can silently do nothing if no case matches), switch expressions must account for every possible value of the selector expression. This is a critical safety feature that prevents bugs and makes code more robust.
The exhaustiveness requirement varies based on the type of the selector:
For enums: You must either list all enum constants explicitly or provide a default case. If you add a new enum constant later and don't update the switch, you'll get a compile-time error.
For primitive types and strings: You must provide a default case since there are infinite possible values.
For sealed types (Java 17+): You only need to cover all permitted subtypes; no default is required if all types are handled.
For reference types: You must provide a default case to handle null and any other possible values.
The compiler tracks which cases have been covered and will produce an error if any path through the switch expression might not yield a value. This is especially powerful with enums and sealed classes, where the compiler knows the complete set of possible values and can verify that your code handles them all.
Code Examples:
// EXHAUSTIVE: Enum with all cases covered
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
String dayType = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
case SATURDAY, SUNDAY -> "Weekend";
// No default needed - all enum values covered
};
// EXHAUSTIVE: Enum with default case
String dayType = switch (day) {
case SATURDAY, SUNDAY -> "Weekend";
default -> "Weekday"; // Covers all other enum values
};
// COMPILE ERROR: Non-exhaustive enum switch
String dayType = switch (day) {
case MONDAY, TUESDAY -> "Early week";
case SATURDAY, SUNDAY -> "Weekend";
// ERROR: Missing cases for WEDNESDAY, THURSDAY, FRIDAY
};
// EXHAUSTIVE: Primitive types require default
int value = switch (number) {
case 1 -> 100;
case 2 -> 200;
default -> 0; // Required for int since infinite possible values
};
// EXHAUSTIVE: String requires default
String result = switch (input) {
case "yes", "y" -> "Confirmed";
case "no", "n" -> "Denied";
default -> "Unknown"; // Required for String
};
// EXHAUSTIVE: Sealed classes (Java 17+)
sealed interface Shape permits Circle, Rectangle, Triangle {}
final class Circle implements Shape { double radius; }
final class Rectangle implements Shape { double width, height; }
final class Triangle implements Shape { double base, height; }
// No default needed - all permitted types covered
double area = switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case Rectangle r -> r.width * r.height;
case Triangle t -> 0.5 * t.base * t.height;
};
// With pattern matching and guards (Java 21+)
String classify = switch (shape) {
case Circle c when c.radius > 10 -> "Large circle";
case Circle c -> "Small circle";
case Rectangle r when r.width == r.height -> "Square";
case Rectangle r -> "Rectangle";
case Triangle t -> "Triangle";
};
Exhaustiveness Checking Table:
| Selector Type | Exhaustiveness Requirement | Example |
|---|---|---|
| Enum | All constants OR default | All Day values covered |
| Primitive (int, long, etc.) | Must have default | Infinite values possible |
| String | Must have default | Infinite values possible |
| Sealed class/interface | All permitted types OR default | All subclasses covered |
| Regular class | Must have default | Unknown subclasses possible |
| Wrapper types (Integer, etc.) | Must have default | Includes null |
Benefits of Exhaustiveness:
// Example: Adding new enum value shows benefit
enum Status { ACTIVE, INACTIVE, PENDING }
// Original code
String message = switch (status) {
case ACTIVE -> "Running";
case INACTIVE -> "Stopped";
case PENDING -> "Waiting";
};
// Later, team adds new enum value
enum Status { ACTIVE, INACTIVE, PENDING, SUSPENDED }
// Compiler immediately flags ALL switch expressions
// that need updating - prevents runtime bugs!
String message = switch (status) {
case ACTIVE -> "Running";
case INACTIVE -> "Stopped";
case PENDING -> "Waiting";
// COMPILE ERROR: Missing case for SUSPENDED
};
// Fix by adding the case
String message = switch (status) {
case ACTIVE -> "Running";
case INACTIVE -> "Stopped";
case PENDING -> "Waiting";
case SUSPENDED -> "On Hold"; // Now exhaustive again
};
Exhaustiveness with Null Handling (Java 17+):
// Modern switch can handle null explicitly
String result = switch (value) {
case null -> "No value";
case "A", "B" -> "Letter";
default -> "Other";
};
// Without null case, null throws NullPointerException
String result = switch (value) {
case "A", "B" -> "Letter";
default -> "Other"; // Doesn't catch null!
};
References:
- https://openjdk.org/jeps/361
- https://docs.oracle.com/en/java/javase/17/language/switch-expressions.html
- https://www.baeldung.com/java-14-nullpointerexception
Strong Encapsulation of JDK Internals
What is strong encapsulation of JDK internals in Java 17?
The 30-Second Answer:
Strong encapsulation of JDK internals is a Java Platform Module System (JPMS) feature that prevents access to internal JDK APIs by default, making internal packages like sun.* and com.sun.* inaccessible via reflection. This was introduced incrementally from Java 9 and became mandatory in Java 17, eliminating the --illegal-access=permit option that previously allowed warnings-only access to internal APIs.
The 2-Minute Answer (If They Want More):
Strong encapsulation is a critical security and maintainability feature of the Java Platform Module System. Prior to Java 9, developers could access internal JDK APIs like sun.misc.Unsafe, com.sun.management.*, and other internal packages through reflection, leading to brittle code that broke when JDK internals changed between versions. The module system introduced the concept of strong encapsulation where modules explicitly declare what they export, and internal packages are not exported by default.
The migration happened in phases: Java 9-15 used --illegal-access=permit by default, allowing reflective access with warnings. Java 16 changed the default to --illegal-access=deny, and Java 17 removed the flag entirely, making strong encapsulation mandatory. This means any code attempting to use reflection to access internal APIs will fail with IllegalAccessException unless explicitly opened using command-line flags.
The primary impact is on libraries and frameworks that relied on internal APIs for functionality like serialization (sun.reflect.ReflectionFactory), performance monitoring (com.sun.management.ThreadMXBean), or low-level operations (sun.misc.Unsafe). Developers must either migrate to public APIs, use command-line flags as a temporary workaround, or wait for library updates that comply with strong encapsulation.
Benefits include improved security (internal APIs can't be exploited), better maintainability (JDK developers can change internals without breaking user code), and clearer contracts between the JDK and applications. The JDK team has provided replacement APIs for most common use cases, such as VarHandles replacing some Unsafe operations and java.lang.management APIs for monitoring.
References:
- https://openjdk.org/jeps/403 (JEP 403: Strongly Encapsulate JDK Internals)
- https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html
- https://openjdk.org/jeps/396 (JEP 396: Strongly Encapsulate JDK Internals by Default)
Pattern Matching for instanceof
What is pattern matching for instanceof in Java 17?
The 30-Second Answer: Pattern matching for instanceof is a Java feature that combines type checking and casting into a single operation, automatically creating a pattern variable when the type test succeeds. Instead of separately checking a type with instanceof and then casting, you can declare a pattern variable directly in the instanceof expression that's automatically cast and available in the appropriate scope.
The 2-Minute Answer (If They Want More): Pattern matching for instanceof was introduced as a preview feature in Java 14 and became a standard feature in Java 16. It eliminates the traditional three-step dance of type checking, casting, and variable assignment by condensing them into a single, more readable expression. This is the first implementation of pattern matching in Java, which is a broader programming paradigm that allows checking a value against a pattern and extracting components from it.
The feature works by allowing you to specify both the type test and a binding variable in the same instanceof expression. When the instanceof test evaluates to true, the binding variable is automatically initialized with the cast value and is in scope for the appropriate code paths. This eliminates manual casting and the associated risks of ClassCastException when done incorrectly.
Pattern matching for instanceof is part of Java's broader pattern matching initiative (Project Amber), which aims to extend pattern matching to other constructs like switch statements and expressions. It represents a significant step toward more expressive and less error-prone code, reducing boilerplate while maintaining Java's type safety guarantees.
Before (Traditional approach):
public String getAnimalSound(Object obj) {
if (obj instanceof Dog) {
Dog dog = (Dog) obj; // Explicit cast required
return dog.bark();
} else if (obj instanceof Cat) {
Cat cat = (Cat) obj; // Another explicit cast
return cat.meow();
}
return "Unknown animal";
}
After (Pattern matching):
public String getAnimalSound(Object obj) {
if (obj instanceof Dog dog) { // Type test and binding in one
return dog.bark();
} else if (obj instanceof Cat cat) {
return cat.meow();
}
return "Unknown animal";
}
References:
- https://openjdk.org/jeps/394
- https://docs.oracle.com/en/java/javase/17/language/pattern-matching-instanceof-operator.html
- https://www.baeldung.com/java-pattern-matching-instanceof
Pattern Matching for switch (Preview)
What is pattern matching for switch introduced as preview in Java 17?
The 30-Second Answer: Pattern matching for switch, introduced as a preview feature in Java 17 (JEP 406), extends the switch statement to allow patterns in case labels instead of just constants. This enables switching on object types and deconstructing values in one step, making type testing and casting more concise and less error-prone compared to traditional instanceof chains.
The 2-Minute Answer (If They Want More): Before Java 17, switch statements only supported exact constant values like integers, strings, and enums. Pattern matching for switch fundamentally changes this by allowing type patterns, null handling, and more sophisticated matching logic directly in case labels. This preview feature builds on the pattern matching for instanceof introduced in Java 16.
The primary benefit is eliminating verbose instanceof-and-cast chains. Instead of multiple if-else blocks with instanceof checks followed by explicit casts, you can express the same logic more naturally with a switch expression. The compiler guarantees type safety and ensures that once a pattern matches, the variable is automatically cast to the matched type within that case block.
For example, traditional code would require:
// Old approach with instanceof chains
String formatted;
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
} else {
formatted = obj.toString();
}
With pattern matching for switch (Java 17+):
// New approach with pattern matching
String formatted = switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
This feature also introduces null handling directly in switch statements. Previously, switch would throw NullPointerException for null values, but now you can have a case null label. Pattern matching for switch evolved through preview in Java 17, second preview in Java 18, third preview in Java 19, and fourth preview in Java 20, before becoming a standard feature in Java 21 with additional refinements.
References:
- https://openjdk.org/jeps/406
- https://docs.oracle.com/en/java/javase/17/language/pattern-matching-switch-expressions-and-statements.html
- https://www.baeldung.com/java-switch-pattern-matching
What is exhaustiveness checking in switch pattern matching?
The 30-Second Answer: Exhaustiveness checking is a compile-time verification that ensures a switch expression or statement covers all possible values of the selector expression. When using pattern matching, the compiler analyzes all case patterns to guarantee that every possible input will match at least one case, either through explicit patterns or a default label, preventing potential runtime errors from unhandled cases.
The 2-Minute Answer (If They Want More): Exhaustiveness checking provides compile-time safety by ensuring that switch expressions always evaluate to a value and never fail due to an unmatched case. This is particularly important for switch expressions (as opposed to statements) because they must always produce a result. The compiler performs sophisticated analysis of type patterns to determine if all possible types are covered.
For sealed types, exhaustiveness checking becomes even more powerful. Since sealed types restrict which classes can implement or extend them, the compiler knows the complete set of possible subtypes. This means you can write switch expressions without a default case if you explicitly handle all permitted subtypes:
// Sealed interface with known subtypes
sealed interface Vehicle permits Car, Truck, Motorcycle {}
record Car(String model, int doors) implements Vehicle {}
record Truck(String model, int capacity) implements Vehicle {}
record Motorcycle(String model, boolean hasSidecar) implements Vehicle {}
// Exhaustive switch without default - compiler knows all possibilities
String describe(Vehicle vehicle) {
return switch (vehicle) {
case Car c -> "Car: " + c.model() + " with " + c.doors() + " doors";
case Truck t -> "Truck: " + t.model() + " capacity " + t.capacity() + "kg";
case Motorcycle m -> "Motorcycle: " + m.model() +
(m.hasSidecar() ? " with sidecar" : "");
// No default needed - all cases covered!
};
}
For non-sealed types, exhaustiveness typically requires a default case since new subtypes could be added later. However, patterns like case Object obj can serve as a catch-all pattern that explicitly handles remaining cases instead of using default:
String formatValue(Object obj) {
return switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
case Object o -> "Other: " + o.toString(); // Explicit catch-all
};
}
The compiler also checks for unreachable patterns. If a more general pattern appears before a more specific one, the compiler generates an error because the specific pattern could never match. This prevents subtle bugs from case ordering issues. For example, case Object o must come after all specific type patterns, not before them.
Exhaustiveness checking works with guarded patterns as well, though guards complicate the analysis. Since guards are runtime conditions, the compiler cannot determine if guards make a switch exhaustive. Therefore, if all cases have guards, you typically still need a default case or an unguarded catch-all pattern to ensure exhaustiveness.
References:
- https://openjdk.org/jeps/406
- https://docs.oracle.com/en/java/javase/17/language/pattern-matching-switch-expressions-and-statements.html
- https://www.baeldung.com/java-21-pattern-matching
Foreign Function and Memory API
What is the Foreign Function and Memory API introduced in Java 17?
The 30-Second Answer: The Foreign Function and Memory API (FFM API) is a preview feature in Java 17 that provides a safe, efficient way to interact with native code and memory outside the Java heap. It replaces the unsafe and error-prone JNI (Java Native Interface) with a pure Java solution that offers better performance, safety, and developer experience.
The 2-Minute Answer (If They Want More):
The Foreign Function and Memory API consists of two main components: the Foreign Function API for calling native functions, and the Foreign Memory API for managing off-heap memory. This API was developed under Project Panama and represents a significant improvement over traditional approaches to native interoperability.
The API provides several key advantages. First, it eliminates the need for writing C/C++ glue code, as all native interactions can be defined in pure Java. Second, it offers compile-time and runtime safety checks that prevent many common errors like memory leaks and buffer overflows. Third, it delivers performance comparable to or better than JNI by avoiding the overhead of native method calls and manual memory management.
The FFM API uses a declarative approach where you describe the native function signatures and data structures in Java, and the runtime handles the marshalling and invocation. Memory segments provide a safe abstraction over native memory that integrates with Java's automatic resource management, ensuring proper cleanup.
In Java 17, the API is in preview status, meaning it requires --add-modules jdk.incubator.foreign and --enable-preview flags to use. It was refined through several incubation rounds before being finalized in Java 22 as the Foreign Function & Memory API (JEP 454).
Code Example:
import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
public class FFMExample {
public static void main(String[] args) throws Throwable {
// 1. Find the C library function
CLinker linker = CLinker.systemCLinker();
SymbolLookup stdlib = CLinker.systemLookup();
// Find strlen function from C standard library
MemoryAddress strlenAddress = stdlib.lookup("strlen").orElseThrow();
// 2. Define the function signature
FunctionDescriptor strlenDescriptor = FunctionDescriptor.of(
CLinker.C_LONG, // return type (size_t)
CLinker.C_POINTER // parameter (char*)
);
// 3. Create a method handle for the native function
MethodHandle strlen = linker.downcallHandle(
strlenAddress,
strlenDescriptor
);
// 4. Allocate native memory and call the function
try (ResourceScope scope = ResourceScope.newConfinedScope()) {
MemorySegment nativeString = CLinker.toCString("Hello, FFM!", scope);
long length = (long) strlen.invoke(nativeString.address());
System.out.println("String length: " + length); // Output: 11
}
// Memory automatically freed when scope closes
}
}
// More complex example: Calling a custom native function
class MathLibraryExample {
public static void main(String[] args) throws Throwable {
System.loadLibrary("mathlib");
CLinker linker = CLinker.systemCLinker();
SymbolLookup mathLib = SymbolLookup.loaderLookup();
// Native function: double calculate(double x, double y)
MemoryAddress calculateAddr = mathLib.lookup("calculate").orElseThrow();
FunctionDescriptor calculateDesc = FunctionDescriptor.of(
CLinker.C_DOUBLE,
CLinker.C_DOUBLE,
CLinker.C_DOUBLE
);
MethodHandle calculate = linker.downcallHandle(
calculateAddr,
calculateDesc
);
double result = (double) calculate.invoke(10.5, 20.3);
System.out.println("Result: " + result);
}
}
References:
- https://openjdk.org/jeps/412
- https://docs.oracle.com/en/java/javase/17/docs/api/jdk.incubator.foreign/jdk/incubator/foreign/package-summary.html
- https://www.baeldung.com/java-foreign-memory-access
New APIs and Enhancements
What is the HexFormat class introduced in Java 17?
The 30-Second Answer:
HexFormat is a new utility class in Java 17 (JEP 356) that provides a standardized, efficient way to convert between byte arrays and hexadecimal strings. It replaces various third-party libraries and custom implementations with a built-in, configurable API supporting uppercase/lowercase output, delimiters, and prefixes/suffixes.
The 2-Minute Answer (If They Want More):
Before Java 17, converting between bytes and hexadecimal strings required either third-party libraries like Apache Commons Codec, or writing custom utility methods using bit manipulation. The HexFormat class standardizes this common operation with a clean, performant API that's part of the core JDK.
The class provides both simple static factory methods for common use cases and a builder pattern for customized formatting. You can configure the output format with delimiters (like colons or spaces between hex pairs), prefixes (like "0x"), suffixes, and case (uppercase or lowercase hex digits). The API is designed to be both simple for basic use cases and flexible for advanced formatting needs.
HexFormat is particularly useful for debugging, logging, cryptographic operations, network protocol implementations, and any scenario where you need to display or parse binary data in a human-readable hexadecimal format. The class is thread-safe and can be shared across multiple threads, making it suitable for use as a static constant in your applications.
The class provides methods for parsing hex strings back to bytes, formatting byte arrays to hex strings, and working with individual bytes or specific ranges. It also includes validation and error handling for malformed hex strings.
import java.util.HexFormat;
public class HexFormatExample {
public static void main(String[] args) {
byte[] data = {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE};
// Basic usage - default lowercase hex
HexFormat hex = HexFormat.of();
String hexString = hex.formatHex(data);
System.out.println(hexString); // "cafebabe"
// Parse hex string back to bytes
byte[] parsed = hex.parseHex("cafebabe");
// Uppercase hex
HexFormat upperHex = HexFormat.ofDelimiter("").withUpperCase();
System.out.println(upperHex.formatHex(data)); // "CAFEBABE"
// With delimiter (colon-separated)
HexFormat colonHex = HexFormat.ofDelimiter(":");
System.out.println(colonHex.formatHex(data)); // "ca:fe:ba:be"
// With prefix and uppercase (like 0xCAFEBABE format)
HexFormat prefixHex = HexFormat.ofDelimiter("")
.withPrefix("0x")
.withUpperCase();
System.out.println(prefixHex.formatHex(data)); // "0xCAFEBABE"
// Format with spaces and uppercase (common in memory dumps)
HexFormat memoryDump = HexFormat.ofDelimiter(" ").withUpperCase();
System.out.println(memoryDump.formatHex(data)); // "CA FE BA BE"
// Format individual bytes
HexFormat simple = HexFormat.of();
System.out.println(simple.toHexDigits((byte) 255)); // "ff"
// Practical example: formatting a cryptographic hash
byte[] sha256Hash = new byte[32]; // Simulated hash
for (int i = 0; i < sha256Hash.length; i++) {
sha256Hash[i] = (byte) i;
}
HexFormat hashFormat = HexFormat.of();
System.out.println("SHA-256: " + hashFormat.formatHex(sha256Hash));
// Parse with validation
try {
HexFormat parser = HexFormat.of();
byte[] result = parser.parseHex("invalid");
} catch (IllegalArgumentException e) {
System.out.println("Invalid hex string: " + e.getMessage());
}
}
}
References:
- https://openjdk.org/jeps/356
- https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/HexFormat.html
- https://www.baeldung.com/java-hexformat
Migration and Best Practices
What are the best practices for writing modern Java 17 code?
The 30-Second Answer: Modern Java 17 code leverages records for data carriers, sealed classes for controlled hierarchies, pattern matching for concise conditionals, and text blocks for multi-line strings. Embrace immutability, use the Stream API for collections, leverage var for local variables, and adopt switch expressions. Write code that's readable, type-safe, and takes advantage of new language features.
The 2-Minute Answer (If They Want More):
Use modern language features appropriately. Replace traditional data classes with records for immutable data carriers—they're more concise and convey intent clearly. Use sealed classes when you want to restrict inheritance hierarchies, making your domain model more maintainable. Pattern matching (instanceof patterns, switch patterns) eliminates boilerplate and makes code more readable. Text blocks improve readability of SQL, JSON, and HTML embedded in Java code.
// Modern record instead of traditional class
public record Customer(String name, String email, LocalDate registrationDate) {
// Compact constructor for validation
public Customer {
Objects.requireNonNull(name, "Name cannot be null");
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
// Sealed class for controlled hierarchy
public sealed interface Payment permits CreditCard, PayPal, BankTransfer {
BigDecimal amount();
}
// Pattern matching in switch (Java 17 preview, finalized in 21)
String processPayment(Payment payment) {
return switch (payment) {
case CreditCard cc -> "Processing card: " + cc.cardNumber();
case PayPal pp -> "Processing PayPal: " + pp.email();
case BankTransfer bt -> "Processing transfer: " + bt.accountNumber();
};
}
Embrace functional programming and immutability. Use the Stream API for collection operations instead of traditional loops—it's more expressive and can be parallelized easily. Prefer immutable collections (List.of(), Map.of(), Collections.unmodifiableX()) and final variables. Use Optional to handle nullability explicitly rather than returning null. The var keyword improves readability when the type is obvious from the right-hand side.
// Functional approach with streams
List<String> activeCustomerNames = customers.stream()
.filter(Customer::isActive)
.filter(c -> c.registrationDate().isAfter(cutoffDate))
.map(Customer::name)
.sorted()
.toList(); // Java 16+ - returns immutable list
// Use Optional to handle absence
Optional<Customer> findCustomer(String email) {
return customers.stream()
.filter(c -> c.email().equals(email))
.findFirst();
}
Follow modern API and performance practices. Use the improved NullPointerException messages (enabled by default in Java 17) to debug issues faster. Leverage compact number formatting, the new DateTimeFormatter patterns, and enhanced pseudo-random number generators. For HTTP clients, use the standard HttpClient API introduced in Java 11. Enable helpful warnings during compilation (-Xlint:all) and use jlink to create custom runtime images for deployment. Structure your code with the module system (module-info.java) for larger applications to enforce encapsulation.
References:
- https://www.oracle.com/java/technologies/javase/17-relnote-issues.html
- https://www.baeldung.com/java-record-keyword
- https://blogs.oracle.com/javamagazine/post/java-sealed-classes-interfaces
Garbage Collection
What garbage collectors are available in Java 17?
The 30-Second Answer: Java 17 offers several production-ready garbage collectors: Serial GC (for single-threaded apps), Parallel GC (throughput-focused), G1 GC (default, balanced low-latency), ZGC (ultra-low latency, sub-millisecond pauses), Shenandoah GC (low-latency alternative), and Epsilon GC (no-op collector for testing). G1 GC is the default collector, optimized for applications requiring predictable pause times while maintaining good throughput.
The 2-Minute Answer (If They Want More):
Java 17 provides a mature set of garbage collectors, each designed for specific application requirements. The Serial GC (-XX:+UseSerialGC) uses a single thread for both young and old generation collection, making it suitable for small applications with heaps under 100MB or single-processor environments. The Parallel GC (-XX:+UseParallelGC) uses multiple threads for garbage collection to maximize throughput, ideal for batch processing applications where pause times are less critical than overall performance.
G1 GC (-XX:+UseG1GC) became the default collector in Java 9 and remains so in Java 17. It divides the heap into regions and performs mostly concurrent marking and compaction, targeting specific pause time goals (default 200ms) while maintaining good throughput. G1 is designed for multi-processor machines with large heaps (4GB+) and provides predictable pause times, making it suitable for most server applications.
ZGC (-XX:+UseZGC) is a scalable low-latency garbage collector that performs all expensive work concurrently, keeping pause times below 10ms even with multi-terabyte heaps. It's production-ready since Java 15 and ideal for applications requiring consistent low latency. Shenandoah GC (-XX:+UseShenandoahGC) is another low-latency collector that reduces pause times by performing concurrent evacuation, offering similar benefits to ZGC with different implementation tradeoffs.
Epsilon GC (-XX:+UseEpsilonGC) is a unique "no-op" collector that handles memory allocation but performs no actual garbage collection. It's designed for performance testing, extremely short-lived jobs, or applications that guarantee no memory allocation after initialization. Once the heap is exhausted, the JVM terminates, making it useful for measuring application memory footprint and allocation rates.
| Garbage Collector | Flag | Best For | Pause Time | Throughput | Heap Size |
|---|---|---|---|---|---|
| Serial GC | -XX:+UseSerialGC |
Small apps, single CPU | High | Low | < 100MB |
| Parallel GC | -XX:+UseParallelGC |
Batch processing, throughput priority | High | High | Any |
| G1 GC (default) | -XX:+UseG1GC |
General-purpose server apps | Medium (predictable) | Good | 4GB+ |
| ZGC | -XX:+UseZGC |
Ultra-low latency requirements | Very Low (< 10ms) | Good | Any (scales to TB) |
| Shenandoah GC | -XX:+UseShenandoahGC |
Low latency alternative | Very Low | Good | Medium to Large |
| Epsilon GC | -XX:+UseEpsilonGC |
Testing, short-lived jobs | N/A (no GC) | Maximum | Any |
Common Configuration Flags:
# G1 GC Configuration
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # Target pause time goal
-XX:G1HeapRegionSize=16m # Region size (1-32MB)
-XX:InitiatingHeapOccupancyPercent=45 # Concurrent cycle trigger
# ZGC Configuration
-XX:+UseZGC
-XX:ZCollectionInterval=120 # Proactive GC interval (seconds)
-XX:ZAllocationSpikeTolerance=2 # Allocation spike tolerance
# Parallel GC Configuration
-XX:+UseParallelGC
-XX:ParallelGCThreads=8 # Number of GC threads
-XX:MaxGCPauseMillis=100 # Desired pause time
# General Heap Settings
-Xms4g # Initial heap size
-Xmx4g # Maximum heap size
-XX:+AlwaysPreTouch # Touch all pages at startup
References:
- https://docs.oracle.com/en/java/javase/17/gctuning/available-collectors.html
- https://www.oracle.com/technical-resources/articles/java/g1gc.html
- https://wiki.openjdk.org/display/zgc/Main