Modern Java adds features that reduce boilerplate while keeping type contracts explicit. Records represent data carriers, sealed types limit permitted subtypes, and pattern matching can combine a type check with a variable binding. The version boundary matters: records, sealed types, and instanceof patterns work on the Java 17 baseline here; finalized pattern matching for switch requires Java 21. This lesson uses only Java 17 syntax in its runnable programs and explains where a newer project can go next.
Records provide a transparent data shape
A record declaration lists components and generates private final fields, component accessors, a canonical constructor, and implementations of equals, hashCode, and toString. The accessor is name(), not getName(). Records are final and extend Record, so they cannot extend another class. They can implement interfaces and define methods. They are useful for a value whose identity is its components, not for every mutable entity or framework model with different lifecycle requirements.
Validate with a compact constructor
A compact constructor lets a record validate or normalize constructor inputs without repeating the full parameter list. Its body runs before the component fields are implicitly assigned. Reject invalid values with a clear exception, and normalize inputs by assigning the component parameter. A record's fields are final, but a component can reference a mutable object such as a list. Copy mutable inputs when the record's contract requires an independent unmodifiable view.
Use sealed types for a closed hierarchy
sealed limits which classes or interfaces can directly extend or implement a type through permits. Permitted subtypes must meet the relevant package or module location rules and declare their own status as final, sealed, or non-sealed. A non-sealed subtype reopens that branch for further extension. The feature expresses an intentionally limited family, such as known result variants; it should not be used simply to prevent inconvenient extension without a meaningful design reason.
Bind a variable with instanceof
An instanceof pattern combines a successful type check with a variable of the checked type. if (value instanceof String text) makes text available in the branch where the condition is true. A null value fails the check rather than producing a bound variable. Scope follows control-flow reasoning, so text is not available wherever the compiler cannot establish the match. This removes a repetitive cast while preserving type safety.
Understand the switch pattern version boundary
Java 17 supports finalized switch expressions for values such as enums and strings, but type patterns in switch were still a preview feature in that release. Java 21 finalized pattern matching for switch, including more expressive exhaustive handling. Do not paste a Java 21 type-pattern switch into a Java 17 project and assume a missing import is the problem. Match examples to the configured release and avoid preview syntax unless the project explicitly adopts it.
Adopt features according to their contracts
A shorter declaration is useful only when it models the intended behavior. A record should not falsely imply deep immutability, and a sealed hierarchy should not hide cases the application actually needs to extend. Use a conventional class when mutable state and identity are central. Keep modern syntax readable to the team and document the minimum Java release in the build. Compiler targeting and runtime testing should agree with those feature choices.
Practice tasks and checks
Add a second valid Parcel and compare records with equal component values using equals. Then create a record containing a mutable list and demonstrate why final component fields do not prevent changes through that list. Replace the stored input with List.copyOf when an unmodifiable independent list is required. Extend the sealed Outcome family with a third permitted record and update a handler to cover its meaning. Finally, compile all examples with --release 17 and explain why introducing a Java 21 type-pattern switch would fail under that target. The exercise should test both the language feature and its design promise rather than celebrating a shorter class declaration without examining its behavior.
Java Summary
- Records provide value-oriented components but not automatic deep immutability.
- Sealed types constrain direct subtype families.
- Match modern syntax to the configured Java release.
- Continue with record foundations when these examples are clear.
Java Examples
Each example shows the code, the output it produces and what each part does.
Validate a record component
Save this program as ValidatedRecord.java. Compile it with javac ValidatedRecord.java and run it with java ValidatedRecord.
Expected output:
The compact constructor validates the weight and reassigns the label parameter to its trimmed version. Implicit component assignment then stores those validated values. The generated accessors expose them without setter methods. This example assumes a non-null label; a production contract accepting null would need an explicit policy. Calling label() reflects the record's component accessor convention rather than JavaBean-style naming.
public class ValidatedRecord {
record Parcel(String label, int grams) {
Parcel {
if (grams <= 0) throw new IllegalArgumentException("Positive weight required");
label = label.trim();
}
}
public static void main(String[] args) {
Parcel parcel = new Parcel(" Books ", 650);
System.out.println(parcel.label());
System.out.println(parcel.grams());
}
}Books
650Declare a sealed family
Save this program as SealedResults.java. Compile it with javac SealedResults.java and run it with java SealedResults.
Expected output:
Outcome permits exactly the two declared direct implementation types. Records are final, satisfying their permitted-subtype status. The instanceof pattern checks the actual result and binds success only in the successful branch. Its accessor supplies Ready. A real result handler should address Missing too; the short program deliberately demonstrates the successful variant without using Java 21 switch syntax.
public class SealedResults {
sealed interface Outcome permits Success, Missing {}
record Success(String value) implements Outcome {}
record Missing(String reason) implements Outcome {}
public static void main(String[] args) {
Outcome result = new Success("Ready");
if (result instanceof Success success) {
System.out.println(success.value());
}
}
}ReadyReplace a separate cast with a pattern
Save this program as PatternLength.java. Compile it with javac PatternLength.java and run it with java PatternLength.
Expected output:
The String argument matches and supplies a typed text variable, so its length is six. The Integer argument does not match, and neither does null, so both calls return the documented fallback zero. This method intentionally treats nonstrings as having no display length; another application could reject them instead. The pattern saves a cast but does not decide the domain policy automatically.
public class PatternLength {
static int length(Object value) {
if (value instanceof String text) return text.length();
return 0;
}
public static void main(String[] args) {
System.out.println(length("Harbor"));
System.out.println(length(12));
System.out.println(length(null));
}
}6
0
0Java FAQ
Are records deeply immutable?
No. Their component fields cannot be reassigned, but a component can refer to mutable data. Make defensive copies or choose immutable component types when the record's contract requires that protection.
Can these programs run on Java 17?
Yes. They use finalized records, sealed types, and instanceof patterns. A type-pattern switch example would require a newer finalized language level such as Java 21 or carefully configured preview support in an older release.
Which modern Java features should I learn first?
Start with the ones you'll read in almost every recent codebase: var for local variables, switch expressions, text blocks, records and pattern matching with instanceof. Sealed types and record patterns become useful once you model data with a fixed set of shapes.
What is a preview feature?
A feature included in a JDK release for feedback before it is final. It only works when you compile and run with --enable-preview, and it may change or be removed in a later version. Avoid preview features in production code; wait until they are finalised.
How do I know which language features my project can use?
Check the release level your build targets, such as <maven.compiler.release>17</maven.compiler.release> in Maven or the Java toolchain in Gradle. Compiling with --release 17 rejects features newer than Java 17 even when you have a newer JDK installed, which keeps the code compatible with servers running that version.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What is the generated accessor for a record component named label?
What does permits describe?
Java Quiz
2 questions to check what you learned.
What happens for null in an instanceof String pattern?
- The match is false
- It binds an empty string
- It throws solely because of instanceof
Which finalized feature needs Java 21 rather than these Java 17 examples?
- Type patterns in switch
- Records
- Instanceof patterns