JAVA NEW FEATURE
Functional Interface in Java
A functional interface in Java is an interface that contains exactly one abstract method. It
can have any number of default or static methods, but only one abstract method.
Functional interfaces are used extensively in lambda expressions, method references, and
streams API.
How to Declare a Functional Interface
You can use the @FunctionalInterface annotation (optional but recommended):
@FunctionalInterface
interface MyFunctionalInterface {
void execute(); // only one abstract method
}
Built-in Functional Interfaces (from java.util.function package)
Interface Abstract Method Description
Function<T,R> R apply(T t) Takes one input and returns a result
Consumer<T> void accept(T t) Takes one input, returns nothing
Supplier<T> T get() Takes no input, returns a result
Predicate<T> boolean test(T t) Returns a boolean based on a test condition
BiFunction<T,U,R> R apply(T t, U u) Two inputs, one result
Example: Functional Interface with Static Method
// 1. Define the functional interface
@FunctionalInterface
interface Printer {
void print(String message); // Only one abstract method
// Static method in interface
static void printInfo() {
System.out.println("Printer is a functional interface with one abstract method.");
}
// 2. Implement the interface using a regular class
class ConsolePrinter implements Printer {
public void print(String message) {
System.out.println("Printing: " + message);
// 3. Main class to use the interface
public class Main {
public static void main(String[] args) {
// Calling the static method of the interface
Printer.printInfo();
// Creating an instance of the class that implements the interface
Printer printer = new ConsolePrinter();
printer.print("Hello from functional interface!");
}
Output:
Printer is a functional interface with one abstract method.
Printing: Hello from functional interface!
Functional Interface with Default Method
// Define the functional interface
@FunctionalInterface
interface Greeter {
// One abstract method (required)
void greet(String name);
// One default method (optional)
default void sayGoodbye() {
System.out.println("Goodbye!");
// Implement the interface with a regular class
class EnglishGreeter implements Greeter {
@Override
public void greet(String name) {
System.out.println("Hello, " + name + "!");
// Main class to run the program
public class Main {
public static void main(String[] args) {
// Create an instance of the implementing class
Greeter greeter = new EnglishGreeter();
// Call the abstract method
greeter.greet("Alice");
// Call the default method
greeter.sayGoodbye();
Output:
Hello, Alice!
Goodbye!
Lambda Expression in Java
A lambda expression is a concise way to represent an instance of a functional interface. It’s
a major feature introduced in Java 8 that enables functional programming in Java.
Syntax of Lambda Expression
(parameters) -> expression
// or
(parameters) -> { statements }
Simple Example
@FunctionalInterface
interface Greeting {
void sayHello(String name);
public class Main {
public static void main(String[] args) {
// Lambda expression to implement the Greeting interface
Greeting greet = (name) -> System.out.println("Hello, " + name + "!");
// Call the method
greet.sayHello("Alice");
Output:
Hello, Alice!
Why Use Lambda Expressions?
Less boilerplate code
More readable and expressive
Enables functional-style operations (especially in collections/streams)
Built-in Example: Using Predicate with Lambda
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<String> isLong = str -> str.length() > 5;
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.test("Functional")); // true
Output:
false
true
lambda expression with a custom functional interface
@FunctionalInterface
interface Greeting {
void sayHello();
public class Main {
public static void main(String[] args) {
Greeting greet = () -> System.out.println("Hello, ");
greet.sayHello();
OUtPUT
Hello,
Program: Addition Using Functional Interface + Lambda
@FunctionalInterface
interface Adder {
int add(int a, int b);
public class Main {
public static void main(String[] args) {
// Lambda expression to implement the add method
Adder adder = (a, b) -> a + b;
// Perform addition
int result = adder.add(10, 20);
System.out.println("Sum: " + result);
OUTPUT:
Sum: 30
Method Reference in Java
Method Reference is a shorthand syntax for a lambda expression that simply calls an
existing method. It improves code readability and conciseness.
Introduced in Java 8, method references work only with functional interfaces (i.e.,
interfaces with a single abstract method).
Syntax
ClassName::methodName
Types of Method References
Java supports four types of method references:
Type Syntax Example Equivalent Lambda
1. Reference
(a, b) ->
to a static ClassName::staticMethod Math::max
Math.max(a, b)
method
2. Reference
to an
instance () ->
instance::instanceMethod str::toUpperCase
method of a str.toUpperCase()
particular
object
3. Reference
to an
instance
method of s ->
ClassName::instanceMethod String::toLowerCase
an arbitrary s.toLowerCase()
object of a
particular
type
4. Reference
() -> new
to a ClassName::new ArrayList::new
ArrayList<>()
constructor
Code Examples
1. Static Method Reference
@FunctionalInterface
interface Adder {
int add(int a, int b);
public class Main {
public static int sum(int a, int b) {
return a + b;
}
public static void main(String[] args) {
Adder adder = Main::sum; // Method reference to static method
System.out.println("Sum: " + adder.add(5, 10));
2. Instance Method Reference (Specific Object)
@FunctionalInterface
interface Printer {
void print();
public class Main {
public void sayHello() {
System.out.println("Hello!");
public static void main(String[] args) {
Main obj = new Main();
Printer printer = obj::sayHello;
printer.print();
3. Instance Method Reference (Arbitrary Object)
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, String> toLower = String::toLowerCase;
System.out.println(toLower.apply("JAVA")); // Output: java
4. Constructor Reference
import java.util.function.Supplier;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();
list.add("Java");
System.out.println(list); // Output: [Java]
Stream API in Java
The Stream API in Java (introduced in Java 8) provides a modern, declarative way to process
collections (like List, Set, etc.). Instead of writing complex for loops, you can chain stream
operations to filter, map, sort, and collect data.
Key Concepts
Stream: A sequence of elements supporting sequential and parallel operations.
Intermediate operations: Return a stream (e.g., filter(), map(), sorted()).
Terminal operations: Return a result or produce a side-effect (e.g., collect(),
forEach(), count()).
General Syntax of Stream API in Java
The general syntax of the Stream API follows a pipeline pattern:
collection.stream() // 1. Create a stream from a collection
.intermediateOperation1() // 2. Optional intermediate operations
.intermediateOperation2()
...
.terminalOperation(); // 3. Terminal operation (required)
Components Explained
Step Description Examples
1. Stream Creation Start with a collection or array list.stream()
2. Intermediate Transform or filter data (returns a filter(), map(), sorted(),
Operations stream) distinct()
3. Terminal Produces a result or side effect collect(), forEach(),
Operation (ends the stream) count(), reduce()
Example: Stream API with List
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jake", "Jill", "Tom", "Tim");
// Filter names starting with 'J', convert to uppercase, sort, and collect to list
List<String> result = names.stream()
.filter(name -> name.startsWith("J")) // Intermediate
.map(String::toUpperCase) // Intermediate
.sorted() // Intermediate
.collect(Collectors.toList()); // Terminal
System.out.println(result);
}
}
Output:
[JAKE, JANE, JILL, JOHN]
Common Stream Methods
Method Description
filter(Predicate) Filters elements
map(Function) Transforms elements
sorted() Sorts elements
distinct() Removes duplicates
limit(n) Limits to first n elements
skip(n) Skips first n elements
collect(Collectors.toList()) Converts stream to a list
forEach(Consumer) Performs an action on each element
count() Counts elements
General Syntax Stream API Program
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
// Step 1: Create a collection (List, Set, etc.)
List<DataType> list = Arrays.asList(value1, value2, value3, ...);
// Step 2: Process the data using Stream API
ResultType result = list.stream() // create stream
.filter(element -> condition) // optional: filter
.map(element -> transformation) // optional: map
.sorted() // optional: sort
.distinct() // optional: remove duplicates
.collect(Collectors.toList()); // or another terminal operation
// Step 3: Display the result
System.out.println(result);
}
}
Example of Working Version (Based on the Template)
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5, 10, 15, 20, 10, 5);
List<Integer> result = list.stream()
.filter(n -> n > 10)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
}
}
OUTPUT:
[15, 20]
Example 1: Count Strings Starting with 'A'
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Andrew", "Angela", "Brian");
long count = names.stream()
.filter(name -> name.startsWith("A"))
.count();
System.out.println("Names starting with 'A': " + count);
}
}
Output:
Names starting with 'A': 3
Example 2: Square Each Number and Collect Results
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("Squares: " + squares);
}
}
Output:
Squares: [1, 4, 9, 16, 25]
Example 3: Filter and Print Even Numbers Using forEach
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(10, 15, 20, 25, 30);
System.out.print("Even numbers: ");
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.print(n + " "));
}
}
OUTPUT:
Even numbers: 10 20 30
Program: Using reduce() to Sum All Elements
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 10, 15, 20);
// Using reduce to calculate the sum
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum of all numbers: " + sum);
}
}
Output:
Sum of all numbers: 50
Grouping Person by City
import java.util.*;
import java.util.stream.*;
class Person {
String name, city;
Person(String name, String city) {
this.name = name;
this.city = city;
}
public String getCity() {
return city;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name; // Simplify output to just names
}
}
public class Main {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", "Delhi"),
new Person("Bob", "Mumbai"),
new Person("Charlie", "Delhi")
);
Map<String, List<Person>> groupedByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));
System.out.println(groupedByCity);
}
}
Output:
{Mumbai=[Bob], Delhi=[Alice, Charlie]}
Example: Group Students by Grade
import java.util.*;
import java.util.stream.*;
class Student {
String name;
String grade;
Student(String name, String grade) {
this.name = name;
this.grade = grade;
}
public String getGrade() {
return grade;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name; // Show only student names in output
}
}
public class Main {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("John", "A"),
new Student("Emma", "B"),
new Student("Sophia", "A"),
new Student("Mike", "C"),
new Student("Chris", "B")
);
Map<String, List<Student>> groupedByGrade = students.stream()
.collect(Collectors.groupingBy(Student::getGrade));
System.out.println(groupedByGrade);
}
}
Output:
{A=[John, Sophia], B=[Emma, Chris], C=[Mike]}
Default Methods
Used to provide a default implementation inside an interface.
Helps in adding new methods to interfaces without breaking existing code.
Static Methods
Belong to the interface itself, not to instances.
Can be called using the interface name.
Example: Interface with Default and Static Methods
interface MyInterface {
// Default method
default void showMessage() {
System.out.println("Default method in interface");
}
// Static method
static void showStatic() {
System.out.println("Static method in interface");
}
}
// Implementing the interface
public class Main implements MyInterface {
public static void main(String[] args) {
Main obj = new Main();
// Calling default method using object
obj.showMessage();
// Calling static method using interface name
MyInterface.showStatic();
}
}
Output:
Default method in interface
Static method in interface
What is Base64 Encoding and Decoding in Java?
Base64 is a binary-to-text encoding scheme that encodes binary data (like bytes or files) into
an ASCII string using 64 characters. It's commonly used to safely transmit data over media
that are designed to handle text (like JSON, XML, HTTP).
Why Use Base64?
To encode binary data (e.g., images, files) for transmission in text-based protocols.
To embed binary content (like images) directly into HTML, XML, or JSON.
For basic authentication in HTTP headers.
Java Base64 Support
Since Java 8, the class java.util.Base64 provides built-in encoding and decoding.
Common Methods in Base64 Class
Method Description
Base64.getEncoder() Returns a standard Base64 encoder
Base64.getDecoder() Returns a standard Base64 decoder
Base64.getUrlEncoder() Returns a URL-safe encoder (replaces + and /)
Base64.getMimeEncoder() Returns a MIME-compliant encoder (adds line breaks)
encodeToString(byte[]) Encodes bytes into a Base64 string
decode(String) Decodes a Base64 string into bytes
EXAMPLE
import java.util.Base64;
import java.util.Scanner;
public class Base64EncodeDecodeExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// --- Encoding ---
System.out.print("Enter a string to encode: ");
String input = scanner.nextLine();
String encoded = Base64.getEncoder().encodeToString(input.getBytes());
System.out.println("Encoded: " + encoded);
// --- Decoding ---
System.out.print("Enter a Base64 encoded string to decode: ");
String encodedInput = scanner.nextLine();
try {
byte[] decodedBytes = Base64.getDecoder().decode(encodedInput);
String decoded = new String(decodedBytes);
System.out.println("Decoded: " + decoded);
} catch (IllegalArgumentException e) {
System.out.println("Invalid Base64 input!");
scanner.close();
OutPut:
Enter a string to encode: Btech Section A2 CSE
Encoded: QnRlY2ggU2VjdGlvbiBBMiBDU0U=
Enter a Base64 encoded string to decode: QnRlY2ggU2VjdGlvbiBBMiBDU0U=
Decoded: Btech Section A2 CSE
What is forEach() in Java?
The forEach() method in Java is a default method added in Java 8 to the Iterable and
Stream interfaces.
It is used to iterate over elements in a collection or stream in a more concise and
functional way.
Syntax
collection.forEach(action);
Where action is typically a lambda expression or method reference.
Example with List
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Using lambda expression
names.forEach(name -> System.out.println(name));
}
}
Output:
Alice
Bob
Charlie
Example with Method Reference
names.forEach(System.out::println);
Example with Stream
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
What is try-with-resources in Java?
try-with-resources is a Java feature (introduced in Java 7) that allows you to automatically
close resources (like files, streams, connections) when you're done with them — without
writing explicit finally blocks.
It works with any class that implements the AutoCloseable interface (like FileReader,
BufferedReader, Scanner, Connection, etc.).
Syntax
try (ResourceType resource = new ResourceType(...)) {
// Use the resource
} catch (Exception e) {
// Handle exception
}
Example: Reading a File Using try-with-resources
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
String filePath = "example.txt"; // Make sure this file exists
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
Output:
Hello, World!
Welcome to try-with-resources in Java.
Benefits of try-with-resources:
Advantage Description
No finally needed Automatically closes resources even if exceptions occur
Cleaner code Less boilerplate, no need to explicitly call close()
Works with many classes Any class that implements AutoCloseable
What are Annotations in Java?
Annotations in Java are metadata (data about data) that provide information to the
compiler, tools, or runtime. They do not directly affect program logic but can influence how
code is processed, especially in frameworks (like Spring, JUnit) or tools (like Lombok).
Common Use Cases
Compile-time instructions (e.g., @Override)
Code generation (e.g., Lombok)
Runtime behavior control (e.g., Spring, JPA)
Documentation (e.g., @Deprecated)
Built-in Annotations in Java
Annotation Description
@Override Indicates a method overrides a superclass method
@Deprecated Marks a method/class as outdated
@SuppressWarnings Instructs the compiler to ignore warnings
@FunctionalInterface Ensures the interface has exactly one abstract method
@SafeVarargs Suppresses warnings for generic varargs
@Retention / @Target Used when creating custom annotations
Example 1: @Override
class Animal {
void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
Output:
Bark
Example 2: @Deprecated
@Deprecated
void oldMethod() {
System.out.println("This method is deprecated");
}
Example 3: Custom Annotation
import java.lang.annotation.*;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation {
String value();
}
public class Main {
@MyAnnotation("This is custom annotation")
public void demo() {
System.out.println("Annotated method");
}
public static void main(String[] args) throws Exception {
Main obj = new Main();
Method method = obj.getClass().getMethod("demo");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println("Annotation value: " + annotation.value());
}
obj.demo(); // call the method too
}
}
Output:
Annotation value: This is custom annotation
Annotated method
What is a Type Annotation in Java?
Type annotations were introduced in Java 8 (via JSR 308). They allow annotations to be
applied wherever a type is used, not just on declarations.
Type Annotation (Java 8 and above)
Can be used on:
Instantiations
Casts
implements / extends
Generic type parameters
Method return types and parameters
Example of Type Annotations
import java.lang.annotation.*;
import java.util.*;
@Target({ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@interface NonNull {
}
// Using type annotation
public class Main {
public static void main(String[] args) {
@NonNull String name = "Java";
List<@NonNull String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (@NonNull String str : list) {
System.out.println(str);
}
}
}
Output:
A
B
Where Type Annotations Can Be Used
Context Example
Variable declarations @NonNull String name;
Generics List<@NonNull String>
Method return types public @NonNull String getName()
Casts (@NonNull String) obj
Implements clause class MyClass implements @NonNull Runnable
What are Repeating Annotations in Java?
Repeating annotations (introduced in Java 8) allow you to apply the same annotation
multiple times to a single element (e.g., class, method, field).
Why It's Needed
Before Java 8, you couldn't apply the same annotation more than once to the same element.
Now, with repeating annotations, it's possible — with a bit of setup.
How to Create Repeating Annotations
You need two annotations:
1. The main annotation (e.g., @Hint)
2. A container annotation (e.g., @Hints)
import java.lang.annotation.*;
import java.lang.reflect.Method;
// 1. Define repeatable annotation
@Repeatable(Hints.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Hint {
String value();
}
// 2. Define the container annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Hints {
Hint[] value();
}
// 3. Use the repeating annotation
public class Main {
@Hint("Hint One")
@Hint("Hint Two")
public void show() {
System.out.println("Method with multiple hints");
}
public static void main(String[] args) throws Exception {
Method method = Main.class.getMethod("show");
// Access all annotations of type Hint
Hint[] hints = method.getAnnotationsByType(Hint.class);
for (Hint hint : hints) {
System.out.println("Hint: " + hint.value());
}
}
}
Output:
Hint: Hint One
Hint: Hint Two
What is the Java Module System?
The Java Module System (also known as Project Jigsaw) was introduced in Java 9 to
modularize the Java platform and your own applications.
It brings a structured way to organize large codebases into reliable and encapsulated
components, called modules.
Why Use Java Modules?
Before Java 9:
Applications used packages and JARs without strong boundaries.
Everything was accessible, which caused conflicts, security issues, and classpath
hell.
With Java Modules:
You explicitly define dependencies.
You control which packages are accessible.
You hide internal implementations (strong encapsulation).
Core Concepts
Term Description
Module A self-contained unit of code (like a JAR)
module-info.java Special file that declares module name, dependencies, and exports
Exports Specifies which packages are accessible to other modules
Requires Specifies which modules this one depends on
Benefits of Java Module System
Feature Benefit
Encapsulation Hide internal APIs by default
Reliable dependencies Compiler checks for missing modules
Better tooling IDEs and build tools get better structure
Smaller JDK Enables custom runtimes via jlink
Security Prevents illegal access to internal APIs
Diamond Syntax (<>) with Inner Anonymous Classes in Java
What is Diamond Syntax?
The diamond operator (<>), introduced in Java 7, infers generic types on the right-hand
side of object creation.
Example:
List<String> list = new ArrayList<>(); // diamond syntax
Limitation with Anonymous Inner Classes
You cannot use diamond syntax with anonymous inner classes that use generics, because
the Java compiler can't infer the type for the anonymous subclass.
What is Local Variable Type Inference in Java?
Local Variable Type Inference is a feature introduced in Java 10 that allows you to declare
local variables using the reserved word var instead of explicitly writing the type.
Syntax:
var variableName = expression;
Example:
var name = "Java"; // inferred as String
var number = 10; // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>
example
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// Using 'var' for type inference
var name = "Java"; // inferred as String
var number = 42; // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>
list.add("Hello");
list.add("World");
System.out.println("Name: " + name);
System.out.println("Number: " + number);
for (var item : list) { // 'item' inferred as String
System.out.println(item);
Output:
Name: Java
Number: 42
Hello
World
Switch Expression in Java
Switch expressions, introduced in Java 14 (finalized), are an enhanced version of the
traditional switch statement. They return values and support more concise syntax.
General Syntax of a Switch Expression in Java
Switch expressions were introduced in Java 14 (as a standard feature) to simplify and
improve upon the traditional switch statement. Here's the general syntax:
1. Using Arrow (->) Syntax:
Type variable = switch (expression) {
case value1 -> result1;
case value2, value3 -> result2;
default -> defaultResult;
};
2. Using yield in Block:
Type variable = switch (expression) {
case value1 -> result1;
case value2 -> {
// Some logic
yield result2; // must use yield in block
default -> {
// Default case logic
yield defaultResult;
};
Use yield when you have multi-line blocks.
What is yield in Java?
The yield keyword in Java is used inside a switch expression to return a value from a
case block when that block contains multiple statements.
Why yield?
In a switch expression, when using block syntax (i.e., { ... }), you can't just write a value
directly.
Instead, you must use yield to return the result from that block.
1⃣ Simple Switch Expression
public class SimpleSwitch {
public static void main(String[] args) {
int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Unknown";
};
System.out.println(dayName); // Output: Wednesday
}
Output:
Wednesday
Switch Expression with Multiple Labels
public class MultiLabelSwitch {
public static void main(String[] args) {
String fruit = "Apple";
String category = switch (fruit) {
case "Apple", "Banana", "Mango" -> "Fruit";
case "Carrot", "Potato" -> "Vegetable";
default -> "Unknown";
};
System.out.println(category); // Output: Fruit
}
Output:
Fruit
Switch Expression with yield Keyword
public class YieldSwitch {
public static void main(String[] args) {
int score = 85;
String grade = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> {
System.out.println("Good performance");
yield "B"; // Use yield to return from block
case 7 -> "C";
default -> "F";
};
System.out.println("Grade: " + grade); // Output: Good performance\nGrade: B
}
Output:
Good performance\nGrade: B
What is a Text Block in Java?
Text Blocks are a feature introduced in Java 13 (as a preview) and standardized in Java 15
to simplify writing multi-line string literals.
They allow you to write strings spanning multiple lines without needing to escape newlines,
quotes, or other characters.
Benefits of Text Blocks:
Easy to write and read multiline strings (like JSON, HTML, SQL).
No need for explicit newline (\n) characters.
Preserves formatting and indentation nicely.
Syntax:
String text = """
This is a
multi-line
text block.
""";
Example:
public class TextBlockExample {
public static void main(String[] args) {
String json = """
"name": "Alice",
"age": 30,
"city": "New York"
""";
System.out.println(json);
}
Output:
"name": "Alice",
"age": 30,
"city": "New York"
}
Notes:
The opening """ must be followed by a newline.
The closing """ defines the indentation baseline.
You can use \s to add a trailing space if needed.
Supports escape sequences like \n, \t if you want explicit control.
What are Records in Java?
Records were introduced in Java 14 (preview) and standardized in Java 16 as a compact
way to declare classes that are primarily used to store immutable data — like simple data
carriers or DTOs (Data Transfer Objects).
Why Records?
Boilerplate-free data classes.
Automatically generate:
o private final fields
o Constructor
o equals(), hashCode()
o toString()
Immutable by default (fields are final).
Useful for modeling simple data aggregates.
Syntax:
public record Person(String name, int age) { }, this line is equivalent to:
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
// equals(), hashCode(), toString() auto-generated
}
Example
public class RecordExample {
public static void main(String[] args) {
Person p = new Person("Alice", 30);
System.out.println(p.name()); // prints: Alice
System.out.println(p.age()); // prints: 30
System.out.println(p); // prints: Person[name=Alice, age=30]
record Person(String name, int age) {}
Output:
Alice
30
Person[name=Alice, age=30]
Key Points:
Feature Description
Immutable fields All fields are final
Compact syntax Less code compared to regular class
Accessors Auto-generated getter methods named after fields ( name() for name)
No setters Objects are immutable
Can have methods You can add extra methods if needed
What are Sealed Classes in Java?
Sealed classes were introduced in Java 15 (preview) and standardized in Java 17. They
provide a way to restrict which other classes or interfaces may extend or implement
them.
Why use Sealed Classes?
Control inheritance explicitly.
Improve security and maintainability.
Enable exhaustive pattern matching (useful with switch).
Define a fixed set of subclasses.
Syntax:
public sealed class Shape permits Circle, Rectangle, Square {
// class body
}
The permits clause lists all allowed subclasses.
Subclasses must declare themselves as final, sealed, or non-sealed.
When to use?
When you want a controlled hierarchy, like shapes, events, or domain models.
When enabling exhaustive checks in switch or instanceof.
Key Points:
Feature Description
Restricts subclasses Only classes listed in permits can extend
Subclasses’ modifiers Must be final, sealed, or non-sealed
Supports better pattern matching Because all subclasses are known
Improves design clarity Clear hierarchy and limited extensibility
EXAMPLE
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
Animal cat = new Cat();
dog.speak(); // Output: Woof!
cat.speak(); // Output: Meow!
// ✅ Sealed class (must NOT be public in online compilers)
sealed class Animal permits Dog, Cat {
public void speak() {
System.out.println("Some animal sound");
}
// ✅ Final subclass 1
final class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof!");
// ✅ Final subclass 2
final class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow!");
Output:
Woof!
Meow!
Benefits of Sealed Classes in Java
Sealed classes, introduced in Java 15 (preview) and finalized in Java 17, offer several
powerful benefits for designing safe, controlled, and maintainable class hierarchies.
1. Controlled Inheritance
2. Better Modeling of Domain Logic
3. Enables Exhaustive Pattern Matching
4. Better Maintainability
5. Documentation and Code Clarity
6. Improved IDE Support & Tooling
Benefit Description
Controlled hierarchy Only allowed classes can extend
Better modeling Clear, domain-specific types
Pattern matching support Enables compiler to check all cases
Safer refactoring Prevents accidental subclassing
Clear documentation Shows which classes belong to the hierarchy