Understanding var, Records, Switch Expressions, and Other Version-Specific Features in Java
Course: Core Java Training
Topic: Modern Java Language Features
Course Link: Core Java Training
Demo Link: Register for Course Demo
Website: JustAcademy
1. Introduction to Modern Java Features
Java has evolved significantly since Java 8, introducing language features that make programs more concise, readable, maintainable, and expressive. These enhancements help developers reduce repetitive code, improve type safety, simplify data handling, and write modern applications.
Some important features include the var keyword, switch expressions, records, text blocks, pattern matching, sealed classes, and newer language constructs introduced in subsequent Java versions.
Understanding the Java version in which a feature became available is important because a program may not compile when the configured JDK or language level does not support that feature.
Learning Objectives
- Understand local variable type inference using
var.
- Learn how records simplify data carrier classes.
- Use switch expressions to return values from conditional logic.
- Understand text blocks and pattern matching.
- Explore sealed classes and record patterns.
- Identify important Java version milestones.
- Apply modern Java features in practical programs.
2. Java Version and Feature Overview
The following table summarizes selected Java language features and the versions in which they became permanent. Some features were previewed in earlier releases before becoming standard language features. Refer to the official Java language changes summary for additional details.
| Java Version |
Feature |
Purpose |
| Java 8 |
Lambda expressions and Stream API |
Support functional-style programming and collection processing. |
| Java 9 |
Module system and private interface methods |
Improve application modularity and interface implementation reuse. |
| Java 10 |
var for local variables |
Infer local variable types from initializers. |
| Java 11 |
var in implicitly typed lambda parameters |
Allow annotations on implicitly typed lambda parameters. |
| Java 14 |
Switch expressions |
Allow switch constructs to return values. |
| Java 15 |
Text blocks |
Simplify multiline string literals. |
| Java 16 |
Records and pattern matching for instanceof |
Simplify data carrier classes and type checks. |
| Java 17 |
Sealed classes |
Restrict which classes or interfaces may extend or implement a type. |
| Java 21 |
Record patterns and pattern matching for switch |
Match and extract structured data more concisely. |
Important: This is a selected overview, not a complete list of all Java releases or APIs. Preview features may require special compiler and runtime options and can change before becoming permanent.
3. Understanding the var Keyword
3.1 What Is var?
The var keyword was introduced in Java 10. It enables local variable type inference, which means the compiler determines the variable's type from its initializer.
Instead of explicitly writing a type when declaring a local variable, you can use var when the type can be inferred from the initialization expression.
Traditional Variable Declaration
String name = "Amit";
int age = 25;
double salary = 45000.50;
Using var
var name = "Amit";
var age = 25;
var salary = 45000.50;
The compiler infers the types as String, int, and double, respectively. The variables remain statically typed; var does not make Java dynamically typed.
3.2 Example Program Using var
public class Main {
public static void main(String[] args) {
var studentName = "Priya";
var studentAge = 21;
var marks = 87.5;
System.out.println("Name: " + studentName);
System.out.println("Age: " + studentAge);
System.out.println("Marks: " + marks);
}
}
Output:
Name: Priya
Age: 21
Marks: 87.5
3.3 Using var With Collections
The var keyword can be useful when working with collections, particularly when the type declaration is lengthy.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
var names = new ArrayList<String>();
names.add("Amit");
names.add("Neha");
names.add("Rahul");
for (var name : names) {
System.out.println(name);
}
}
}
Output:
Amit
Neha
Rahul
Here, the variable names is inferred as ArrayList<String>, and the loop variable name is inferred as String.
3.4 Rules for Using var
- It can be used for local variables with initializers.
- The compiler must be able to infer the type from the initializer.
- It cannot be used to declare an uninitialized local variable.
- It cannot replace parameter or return types in ordinary method declarations.
- It cannot be used as a field type or a constructor parameter type.
- It cannot be used as the type of a lambda parameter in Java 10; Java 11 allows it for implicitly typed lambda parameters.
- It does not mean the variable can later hold a value of an unrelated type.
3.5 Invalid Examples
// Invalid: no initializer
var age;
// Invalid: null does not provide enough type information
var name = null;
// Invalid: var cannot be used for a field
class Student {
// var studentName = "Amit";
}
Each invalid example illustrates a restriction. A local variable declared with var must have an initializer from which the compiler can infer its type.
3.6 var With Lambda Parameters in Java 11
Java 11 permits var in implicitly typed lambda parameters. This can be useful when annotations are needed on lambda parameters.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names =
Arrays.asList("Amit", "Neha", "Rahul");
names.forEach((var name) ->
System.out.println(name.toUpperCase()));
}
}
Output:
AMIT
NEHA
RAHUL
Use either var for all lambda parameters or explicit types for all parameters; do not mix inferred and explicitly declared parameter types in the same lambda expression.
4. Understanding Records in Java
4.1 What Is a Record?
A record is a special kind of Java class designed to model data with a fixed set of components. Records reduce repetitive code by automatically providing component fields, a canonical constructor, accessor methods, and implementations of equals(), hashCode(), and toString().
Records were introduced as a preview feature in Java 14 and became a permanent feature in Java 16.
4.2 Traditional Class Example
public class Student {
private final String name;
private final int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
}
A regular class may require additional code for equality, hashing, and readable string output. A record provides these features automatically based on its declared components.
4.3 Record Syntax
record Student(String name, int age) {}
This declaration creates a record with two components: name and age.
4.4 Example Program Using Records
record Student(String name, int age, double marks) {}
public class Main {
public static void main(String[] args) {
Student student =
new Student("Priya", 21, 89.5);
System.out.println(student.name());
System.out.println(student.age());
System.out.println(student.marks());
System.out.println(student);
}
}
Output:
Priya
21
89.5
Student[name=Priya, age=21, marks=89.5]
4.5 Important Features of Records
- Records provide a concise syntax for declaring data carrier types.
- Component fields are private and final.
- Accessor methods use the component name, such as
name().
- Records automatically provide value-based implementations of
equals() and hashCode().
- Records automatically provide a readable
toString() implementation.
- Records cannot extend ordinary classes because they implicitly extend
java.lang.Record.
- Records can implement interfaces.
- Records are final and cannot be subclassed.
4.6 Record Constructors and Validation
Records can validate component values through a compact constructor. This is useful for enforcing basic rules when an object is created.
record Employee(String name, double salary) {
Employee {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(
"Name cannot be empty"
);
}
if (salary < 0) {
throw new IllegalArgumentException(
"Salary cannot be negative"
);
}
}
}
public class Main {
public static void main(String[] args) {
Employee employee =
new Employee("Amit", 50000);
System.out.println(employee);
}
}
Output:
Employee[name=Amit, salary=50000.0]
The compact constructor validates the values supplied to the record. If a rule is violated, the constructor throws an exception.
4.7 Records and Immutability
Records are shallowly immutable: their component fields cannot be reassigned after construction. However, if a component refers to a mutable object, that object may still be changed unless the program takes additional steps to prevent it.
import java.util.ArrayList;
import java.util.List;
record Team(String name, List<String> members) {}
public class Main {
public static void main(String[] args) {
List<String> members = new ArrayList<>();
members.add("Amit");
Team team = new Team("Developers", members);
members.add("Neha");
System.out.println(team.members());
}
}
Output:
[Amit, Neha]
The record's reference to the list is final, but the list itself is mutable. For stronger immutability, defensive copies and immutable collection views may be appropriate.
4.8 When Should You Use Records?
- For DTOs (Data Transfer Objects).
- For simple API response models.
- For representing coordinates, points, ranges, or measurements.
- For values passed between application layers.
- For data models where component-based equality is appropriate.
A regular class is often more suitable when the object requires mutable state, complex inheritance, or behavior and identity beyond its data components.
5. Understanding Switch Expressions
5.1 What Is a Switch Expression?
A switch expression is a form of switch that produces a value. Switch expressions became a permanent language feature in Java 14 after preview releases in Java 12 and Java 13.
Traditional switch statements are commonly used to execute actions. Switch expressions can assign a result directly to a variable or return it from a method.
5.2 Traditional Switch Statement
public class Main {
public static void main(String[] args) {
int day = 2;
String dayName;
switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
default:
dayName = "Invalid day";
}
System.out.println(dayName);
}
}
5.3 Using a Switch Expression
public class Main {
public static void main(String[] args) {
int day = 2;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Invalid day";
};
System.out.println(dayName);
}
}
Output:
Tuesday
The switch expression returns a value that is assigned directly to dayName.
5.4 Arrow Labels and No Fall-Through
The arrow syntax (->) does not fall through to the next case. This reduces accidental execution of multiple case branches.
String type = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Invalid day";
};
Multiple case labels can share one result. In this example, days 1 through 5 are treated as weekdays, and days 6 and 7 as weekend days.
5.5 Using yield in a Switch Expression
When a switch expression uses a block of statements instead of a single expression, the yield statement supplies the value for that branch.
public class Main {
public static void main(String[] args) {
int marks = 85;
String grade = switch (marks / 10) {
case 10, 9 -> "A";
case 8 -> {
System.out.println("Good performance");
yield "B";
}
case 7 -> "C";
case 6 -> "D";
default -> "Needs improvement";
};
System.out.println("Grade: " + grade);
}
}
Output:
Good performance
Grade: B
The yield statement returns the result of the block to the switch expression. It is different from the yield keyword used in other Java contexts.
5.6 Switch Expressions Must Be Exhaustive
A switch expression must account for every possible input value. For example, a switch expression over an integer generally requires a default branch unless the compiler can establish exhaustiveness through the relevant language rules.
int number = 5;
String result = switch (number) {
case 1 -> "One";
case 2 -> "Two";
default -> "Other number";
};
Without an appropriate branch for the remaining values, the compiler may reject the switch expression.
6. Text Blocks in Java
6.1 What Are Text Blocks?
Text blocks provide a convenient way to represent multiline strings. They became a permanent feature in Java 15 after preview releases in Java 13 and Java 14.
Text blocks are useful when working with JSON, SQL, HTML, multiline messages, or formatted text that would otherwise require numerous escape characters and string concatenations.
6.2 Traditional Multiline String
String html = "<html>\n" +
" <body>\n" +
" <h1>Hello</h1>\n" +
" </body>\n" +
"</html>";
6.3 Using a Text Block
public class Main {
public static void main(String[] args) {
String html = """
<html>
<body>
<h1>Hello</h1>
</body>
</html>
""";
System.out.println(html);
}
}
Text blocks improve readability by preserving a natural multiline layout. The compiler processes incidental indentation and line terminators according to the text block rules.
6.4 Benefits of Text Blocks
- Reduce the need for explicit newline escape sequences.
- Improve readability for multiline strings.
- Make embedded JSON, SQL, and markup easier to maintain.
- Support standard string operations after the text block has been created.
Text blocks do not automatically validate embedded SQL, HTML, or JSON, and they do not replace safe parameterized queries when interacting with databases.
7. Pattern Matching for instanceof
7.1 Traditional Type Checking
Before pattern matching, developers commonly checked an object's type using instanceof and then performed an explicit cast.
Object value = "Hello Java";
if (value instanceof String) {
String text = (String) value;
System.out.println(text.length());
}
7.2 Using Pattern Matching
Pattern matching for instanceof became permanent in Java 16 after being previewed in Java 14 and Java 15.
public class Main {
public static void main(String[] args) {
Object value = "Hello Java";
if (value instanceof String text) {
System.out.println(text.length());
}
}
}
Output:
10
The variable text is introduced by the pattern and is available where the compiler can determine that the pattern matched.
7.3 Benefits
- Reduces explicit casting.
- Makes type checks more concise.
- Improves readability.
- Helps avoid certain casting errors.
8. Sealed Classes and Interfaces
8.1 What Are Sealed Classes?
Sealed classes and interfaces restrict which types may extend or implement them. Sealed classes became permanent in Java 17 after being previewed in earlier releases.
This feature is useful when an application has a controlled set of permitted subtypes, such as different payment methods, notification types, or geometric shapes.
8.2 Example
sealed interface Payment
permits CashPayment, CardPayment {}
final class CashPayment implements Payment {}
final class CardPayment implements Payment {}
public class Main {
public static void main(String[] args) {
Payment payment = new CashPayment();
System.out.println(
payment instanceof CashPayment
);
}
}
Output:
true
The permits clause lists the types allowed to implement the sealed interface. Permitted direct subclasses must follow the relevant rules, typically declaring themselves final, sealed, or non-sealed.
8.3 Common Use Cases
- Modeling a limited set of payment types.
- Representing application-specific result types.
- Building controlled domain models.
- Supporting exhaustive pattern matching in suitable type hierarchies.
9. Pattern Matching for switch and Record Patterns
Java 21 made pattern matching for switch and record patterns permanent features. These features allow developers to select behavior based on an object's type and, when appropriate, extract the components of a record.
9.1 Pattern Matching for switch
public class Main {
static String describe(Object value) {
return switch (value) {
case Integer number -> "Integer: " + number;
case String text -> "String: " + text;
case Double decimal -> "Double: " + decimal;
case null -> "Null value";
default -> "Other type";
};
}
public static void main(String[] args) {
System.out.println(describe(25));
System.out.println(describe("Java"));
System.out.println(describe(null));
}
}
Output:
Integer: 25
String: Java
Null value
This example uses Java 21 pattern matching for switch. The explicit case null handles a null selector instead of allowing the switch to throw a NullPointerException.
9.2 Record Patterns
Record patterns allow a program to match a record and extract its components directly. They became permanent in Java 21.
record Point(int x, int y) {}
public class Main {
static void display(Object value) {
if (value instanceof Point(int x, int y)) {
System.out.println("X: " + x);
System.out.println("Y: " + y);
}
}
public static void main(String[] args) {
display(new Point(10, 20));
}
}
Output:
X: 10
Y: 20
Record patterns are particularly useful for data-oriented code because they allow record components to be extracted without explicitly calling each accessor.
10. Other Useful Version-Specific Features
10.1 Private Methods in Interfaces — Java 9
Java 9 introduced private methods in interfaces. They allow common implementation logic to be shared among default and static methods without exposing the helper method as part of the public interface API.
interface Greeting {
private String message() {
return "Welcome to Java";
}
default void greet() {
System.out.println(message());
}
}
10.2 Standard HTTP Client — Java 11
Java 11 standardized the modern HTTP Client API in java.net.http. It supports HTTP/1.1 and HTTP/2 and provides synchronous and asynchronous request handling.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println(response.statusCode());
}
}
This example makes a real HTTP request when executed. Network availability and the remote server determine the response. Use appropriate error handling and avoid hard-coding sensitive information in production code.
10.3 Virtual Threads — Java 21
Virtual threads became a permanent feature in Java 21. They are lightweight threads managed by the Java runtime and can be useful for applications that handle many concurrent tasks involving blocking operations.
public class Main {
public static void main(String[] args)
throws InterruptedException {
Thread task = Thread.startVirtualThread(() ->
System.out.println("Running in a virtual thread")
);
task.join();
}
}
Virtual threads can simplify high-concurrency applications, particularly those that spend time waiting for I/O. They do not make CPU-intensive work inherently faster.
11. Comparison of var, Records, and Switch Expressions
| Feature |
Introduced as Permanent Feature |
Main Purpose |
Typical Example |
var |
Java 10 |
Infer local variable types. |
var count = 10; |
| Records |
Java 16 |
Declare concise data carrier types. |
record User(String name) {} |
| Switch expressions |
Java 14 |
Return values from switch constructs. |
String result = switch (x) { ... }; |
| Text blocks |
Java 15 |
Represent multiline strings clearly. |
String s = """ ... """; |
| Pattern matching for instanceof |
Java 16 |
Combine type checking and variable binding. |
obj instanceof String s |
| Sealed classes |
Java 17 |
Restrict permitted subtypes. |
sealed interface Result permits ... |
| Record patterns and pattern matching for switch |
Java 21 |
Match types and deconstruct record values. |
case Point(int x, int y) -> ... |
12. Practical Project: Employee Information Processor
Objective: Build a small Java program using records, var, switch expressions, and modern pattern matching to represent and process employee information.
Program
record Employee(String name, String department, double salary) {}
public class Main {
static String salaryCategory(double salary) {
return switch ((int) salary / 10000) {
case 0, 1, 2 -> "Entry Level";
case 3, 4, 5 -> "Mid Level";
default -> "Senior Level";
};
}
static String describe(Object value) {
return switch (value) {
case Employee employee ->
employee.name() + " works in " +
employee.department();
case String text -> "Text value: " + text;
case null -> "No value supplied";
default -> "Unknown data type";
};
}
public static void main(String[] args) {
var employee = new Employee(
"Priya", "Development", 45000
);
System.out.println(describe(employee));
System.out.println(
"Salary category: " +
salaryCategory(employee.salary())
);
System.out.println(describe("Core Java"));
System.out.println(describe(null));
}
}
Output:
Priya works in Development
Salary category: Mid Level
Text value: Core Java
No value supplied
Project Explanation
- The
Employee record stores an employee's name, department, and salary.
- The
var keyword allows the compiler to infer the employee variable's type.
- The
salaryCategory() method uses a switch expression to return a salary category.
- The
describe() method uses Java 21 pattern matching for switch to process different types of input.
- The program demonstrates how modern Java features can work together in a small application.
Version requirement: This complete project uses Java 21 language features, including pattern matching for switch and case null. Compile and run it with a compatible JDK 21 or newer.
13. Common Mistakes and Best Practices
| Common Mistake |
Explanation |
Best Practice |
| Using var without an initializer |
The compiler cannot infer a type. |
Initialize the variable when declaring it. |
| Assuming var makes Java dynamically typed |
The inferred type is fixed at compile time. |
Remember that Java remains statically typed. |
| Using records for every class |
Some classes need mutable state or more complex behavior. |
Use records primarily for data-oriented models. |
| Assuming records are deeply immutable |
Referenced objects may remain mutable. |
Use defensive copies where necessary. |
| Forgetting switch exhaustiveness |
A switch expression must cover all possible cases. |
Include appropriate branches or a default case. |
| Confusing yield and break |
They serve different purposes in switch constructs. |
Use yield to supply a value from a switch-expression block. |
| Using unsupported language features |
Older JDKs may reject newer syntax. |
Check the configured JDK and language level. |
| Treating preview features as permanent |
Preview syntax may change between releases. |
Verify the feature's status for the target JDK. |
14. Practice Exercises
- Use
var to declare a string, an integer, and a list of names.
- Create a
Product record with name, price, and quantity components.
- Add validation to a record constructor so the product price cannot be negative.
- Write a switch expression that converts a month number into a month name.
- Use a switch expression to calculate a student's grade based on marks.
- Create a multiline JSON string using a text block.
- Use pattern matching for
instanceof to process a string stored in an Object variable.
- Create a sealed interface for different payment methods.
- Use Java 21 pattern matching for switch to process integers, strings, and null values.
- Create a record representing a point and use a record pattern to extract its coordinates.
15. Interview Questions and Answers
Q1. What is the var keyword in Java?
var enables local variable type inference. The compiler determines the variable's static type from its initializer.
Q2. Which Java version introduced var?
Local variable type inference using var became permanent in Java 10.
Q3. Can var be used for instance variables?
No. It is not permitted as a field type. It is mainly used for local variable declarations and certain other specifically supported contexts.
Q4. What is a record in Java?
A record is a special class designed to represent data with a fixed set of components and automatically generated data-oriented methods.
Q5. Which Java version made records permanent?
Records became a permanent language feature in Java 16.
Q6. Are records completely immutable?
No. Records are shallowly immutable. Their component fields are final, but referenced mutable objects can still change.
Q7. What is a switch expression?
A switch expression evaluates a selector and produces a value based on the matching branch.
Q8. Which Java version made switch expressions permanent?
Switch expressions became permanent in Java 14.
Q9. What is the difference between yield and break?
yield supplies a value from a block within a switch expression. break is used to exit applicable switch statements and other supported control structures.
Q10. What are text blocks?
Text blocks are multiline string literals that became permanent in Java 15 and help represent formatted text more readably.
Q11. What are sealed classes?
Sealed classes restrict which classes can extend them. Sealed interfaces similarly restrict which classes can implement them.
Q12. What is pattern matching for instanceof?
It combines a type check with a pattern variable, reducing the need for an explicit cast after the check succeeds.
Q13. What are record patterns?
Record patterns allow a program to match a record and extract its components directly. They became permanent in Java 21.
Q14. Why should developers learn Java version-specific features?
These features help developers write more concise and maintainable code while ensuring that applications are compatible with their target JDK.
16. Quick Revision Notes
var: Java 10; infers local variable types at compile time.
- Records: Java 16; concise data carrier classes.
- Switch expressions: Java 14; switch constructs can return values.
- Text blocks: Java 15; multiline string literals.
- Pattern matching for
instanceof: Java 16; type checking with variable binding.
- Sealed classes: Java 17; controlled inheritance.
- Pattern matching for switch: Java 21; type-based switch branches.
- Record patterns: Java 21; deconstruct record values.
- Always verify the target JDK before using a language feature.
- Prefer the feature that makes code clearer without adding unnecessary complexity.
17. Conclusion
Modern Java features make it easier to write concise, readable, and maintainable applications. The var keyword reduces repetitive local type declarations, records simplify data models, and switch expressions improve value-based conditional logic. Text blocks, pattern matching, and sealed classes provide further improvements to everyday Java programming.
Learning when these features became available helps developers select compatible language constructs and prepare for Core Java interviews. Practice the examples using the correct JDK version to understand both the syntax and the behavior of each feature.
Learn Core Java With JustAcademy
Explore Core Java concepts, modern language features, object-oriented programming, collections, exception handling, and practical programming exercises.
Course Link: Explore Core Java Training
Demo Registration: Register for a Course Demo
Official Website: Visit JustAcademy