What is a Functional Interface?
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 the basis for lambda expressions and method references in Java.
Characteristics:
• Can have multiple default and static methods.
• Annotated with @FunctionalInterface (optional but recommended).
• Used by built-in Java functional APIs (Predicate, Function, etc.)
Example: Custom Functional Interface
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
Common Built-in Functional Interfaces (from java.util.function)
Interface Method Purpose
Predicate<T> boolean test(T) Boolean test for filtering
Function<T,R> R apply(T) Converts T to R
Consumer<T> void accept(T) Performs action on T
Supplier<T> T get() Supplies a value
UnaryOperator<T> T apply(T) Operates on and returns same type
BinaryOperator<T> T apply(T,T) Combines two of same type
What is a Lambda Expression in Java?
A lambda expression in Java is a concise way to represent an instance of a functional interface. It
enables you to write inline implementations of interfaces with a single abstract method — making
your code cleaner and more readable.
Lambda expressions were introduced in Java 8 to support functional programming.
Syntax of Lambda Expression
(parameters) -> expression
Or for multiple statements:
(parameters) -> {
// multiple statements
return result;
Example: Basic Lambda with Custom Functional Interface
Step 1: Define a Functional Interface
@FunctionalInterface
interface MyOperation {
int operate(int a, int b);
Step 2: Use Lambda to Implement It
public class Main {
public static void main(String[] args) {
MyOperation addition = (a, b) -> a + b;
MyOperation subtraction = (a, b) -> a - b;
System.out.println("Add: " + addition.operate(10, 5)); // Output: 15
System.out.println("Subtract: " + subtraction.operate(10, 5)); // Output: 5
3. Method References – A Shorter Form of Lambda
A method reference is a shorthand for a lambda expression that calls a method.
Syntax:
ClassName::staticMethod
instance::instanceMethod
ClassName::new // constructor reference
Example:
List<String> names = Arrays.asList("Bob", "Alice", "John");
// Using lambda
names.forEach(name -> System.out.println(name));
// Using method reference
names.forEach(System.out::println);
When to Use:
• When lambda just calls a method
• Improves readability
4. Stream API – Functional Style Processing of Collections
The Stream API lets you perform complex operations on data collections (like List, Set, etc.) in a
declarative, functional manner.
Common Stream Operations:
Operation Type Description
filter Intermediate Filters elements based on predicate
map Intermediate Transforms elements
sorted Intermediate Sorts elements
collect Terminal Gathers results into a collection
forEach Terminal Performs action for each element
Full Example: Using Everything Together
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Amanda", "Steve");
// Using Stream, Lambda, and Method Reference
names.stream()
.filter(name -> name.startsWith("A")) // Lambda
.map(String::toUpperCase) // Method reference
.forEach(System.out::println); // Method reference
}
▶ Output:
ALICE
AMANDA
1. Default Methods in Interfaces
A default method is a method in an interface that has a body (implementation) and uses the default
keyword.
Purpose:
• Add new functionality to interfaces without breaking existing classes that implement them.
• Enables multiple inheritance of behavior (not state).
Syntax:
interface MyInterface {
default void show() {
System.out.println("Default show() method");
Example:
interface A {
default void sayHello() {
System.out.println("Hello from A");
class B implements A {
// Inherits sayHello() from A
public class Main {
public static void main(String[] args) {
B b = new B();
b.sayHello(); // Output: Hello from A
⚠ Note on Conflict:
If two interfaces provide default methods with the same signature, the implementing class must
override it.
interface A {
default void greet() { System.out.println("Hello from A"); }
interface B {
default void greet() { System.out.println("Hello from B"); }
class C implements A, B {
public void greet() {
System.out.println("Hello from C");
2. Static Methods in Interfaces
A static method in an interface is associated with the interface itself — not the instance — and
cannot be overridden.
Syntax:
interface Utility {
static void printMessage() {
System.out.println("Static method in interface");
}
Usage:
public class Main {
public static void main(String[] args) {
Utility.printMessage(); // Call through interface name
1. Statement
1. Base64 Encode and Decode
Definition:
Base64 encoding is a way to convert binary data (e.g., files, images, strings) into ASCII characters. It's
commonly used to transmit data over media that are designed to deal with textual data (e.g., email,
JSON, URLs).
Java 8 introduced the java.util.Base64 class to handle encoding and decoding operations.
2. forEach() Method
Definition:
The forEach() method is a default method introduced in Java 8 for the Iterable and Stream
interfaces. It allows performing an action on each element of a collection using lambda expressions
or method references.
Example with List:
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name)); // Using lambda
names.forEach(System.out::println); // Using method reference
}
3. Try-With-Resources
Definition:
The try-with-resources statement (introduced in Java 7) automatically closes resources such as files,
sockets, or database connections once they are no longer needed. Resources must implement the
AutoCloseable interface.
This feature avoids manual closing and prevents memory leaks.
Example:
import java.io.*;
public class Main {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line = reader.readLine();
System.out.println("File line: " + line);
} catch (IOException e) {
e.printStackTrace();
Type Annotations in Java
Definition:
Type annotations are annotations that can be applied wherever a type is used — not just on
declarations like classes, methods, or variables.
They were introduced in Java 8 (JSR 308) to enable stronger type checking and more powerful
compile-time and runtime analysis.
Why Use Type Annotations?
• To provide additional type information to tools, compilers, or frameworks
• Useful in static code analysis, null-checking, taint analysis, etc.
• Allows frameworks like Checker Framework, FindBugs, or SpotBugs to validate code more
deeply
Example:
➤ Define a custom type annotation:
• import java.lang.annotation.*;
•
• @Target(ElementType.TYPE_USE)
• @Retention(RetentionPolicy.RUNTIME)
• @interface NonNull {}
➤ Apply it:
• public class Demo {
• public @NonNull String getName() {
• return "ChatGPT";
• }
•
• public void printList(List<@NonNull String> names) {
• names.forEach(System.out::println);
• }
•
• public void checkCast(Object obj) {
• String str = (@NonNull String) obj;
• }
• }
The @NonNull annotation here means the object should not be null — tools
can catch violations.
Repeating Annotations in Java
Definition:
Repeating annotations allow you to apply the same annotation multiple times to
the same declaration or type use.
This feature was introduced in Java 8 to improve clarity and support use cases where
multiple values of the same annotation are needed.
How It Works:
1. You create a repeatable annotation using @Repeatable.
2. You define a container annotation that holds an array of the repeated
annotations.
Java Module System (Jigsaw Project)
Definition:
The Java Module System was introduced in Java 9 under the Jigsaw Project. It
allows you to define modules within your application, enabling better encapsulation,
modularization, and dependency management for large applications.
A module is a group of related packages that are treated as a single unit. It defines
its dependencies on other modules and can specify which of its packages should be
visible to other modules.
Why Use the Java Module System?
1. Modularization of large applications into smaller, manageable parts.
2. Improved security and encapsulation by exposing only the necessary parts of
your application.
3. Reduced complexity when managing dependencies.
4. Better performance by enabling the JVM to load only the necessary parts of an
application.
Anonymous Class in Java
Definition:
An anonymous class in Java is a class without a name that is used to instantiate and
define a class that implements an interface or extends a class on the fly, often in a
single expression. These are commonly used for one-time implementations of
classes or interfaces, especially when you don't need to create a separate named class.
Examples of Anonymous Classes
. Anonymous Class Implementing an Interface
One of the most common uses of anonymous classes is implementing interfaces
without creating a new named class.
public class Main {
public static void main(String[] args) {
// Anonymous class implementing Runnable interface
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello from the anonymous class!");
};
Thread thread = new Thread(runnable);
thread.start();
Explanation:
Here, an anonymous class implements the Runnable interface and overrides the run()
method.
We don't need to create a separate Runnable implementation class; instead, we define
it inline where it's needed.
Diamond Syntax with Inner Anonymous Class
The diamond syntax (<>) in Java was introduced in Java 7 and provides a simpler
way to define the type of a generic object without needing to specify the type on both
sides of the assignment. It works by letting the compiler infer the type on the right-
hand side based on the left-hand side.
List<String> list = new ArrayList<>();
Local Variable Type Inference (Java 10)
Definition:
Local variable type inference was introduced in Java 10 with the var keyword. It
allows the compiler to infer the type of a local variable based on the context,
eliminating the need to explicitly specify the variable's type. The type of the variable
is determined at compile-time and cannot be changed later.
In simpler terms, you can use var to let the compiler figure out the type of a variable,
improving code readability and reducing verbosity
var message = "Hello, Java 10!"; // Inferred type: String
var count = 10; // Inferred type: int
Introduction to Switch Expressions:
The switch statement has been a part of Java since its early versions, but Java 12
introduced switch expressions, which enhance the traditional switch statement by
providing more flexibility, conciseness, and safety.
Switch expressions allow returning a value from a switch block, using multiple
labels for a case, and provide the option for fall-through behavior to be controlled
more easily.
Before Java 12, the switch statement was used only for executing statements based
on a variable’s value, but now with switch expressions, you can return a value
directly from the switch.
Basic Example of Switch Expression:
public class Main {
public static void main(String[] args) {
int day = 3;
String result = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
case 7 -> "Sunday";
default -> "Invalid day";
};
System.out.println(result); // Output: Wednesday
yield Keyword in Java (Switch Expressions)
Introduction to yield Keyword:
The yield keyword was introduced in Java 12 alongside switch expressions. It is
used within a switch expression to return a value from a case block. Unlike the
traditional switch statement, which doesn't allow returning a value directly, switch
expressions allow returning values, and yield is the mechanism that makes it possible
when more than one statement is needed in a case block.
Basic Example Using yield:
public class Main {
public static void main(String[] args) {
int day = 3;
String result = switch (day) {
case 1 -> "Monday"; // Single statement, return immediately
case 2 -> "Tuesday"; // Single statement, return immediately
case 3 -> {
// Multiple statements
String message = "Wednesday is the middle of the week!";
yield message; // Return value using yield
default -> "Invalid day";
};
System.out.println(result); // Output: Wednesday is the middle of the week!
}
Text Blocks in Java (Java 13)
Introduction to Text Blocks:
Introduced in Java 13, Text Blocks provide a more readable and convenient way to
work with multi-line strings. Before text blocks, multi-line strings in Java were often
cumbersome and required handling escape sequences like \n for new lines and \t for
indentation. Text blocks offer a cleaner, more intuitive syntax for defining multi-line
string literals.
Basic Syntax of Text Blocks:
Text blocks in Java are defined using triple double quotes """, which allows for multi-
line strings to be written directly in the source code.
String textBlock = """
This is a
multi-line string
using text blocks.
""";
Records in Java (Java 14)
Introduction to Records:
Introduced in Java 14 as a preview feature and finalized in Java 16, records are a
special kind of class in Java designed to model immutable data in a compact and
concise way. Records simplify the creation of data-carrier classes by automatically
generating several useful methods (e.g., toString(), equals(), hashCode(), and getters)
based on the fields of the class.
In traditional Java, for classes that were just used to hold data, developers had to
manually write boilerplate code for constructors, getters, toString(), equals(), and
hashCode() methods. With records, this repetitive task is automated, leading to more
readable and maintainable code.
🔹 Sealed Classes in Java (Java 15)
✅ Introduction to Sealed Classes:
Sealed classes were introduced in Java 15 as a preview feature and finalized
in Java 17. They allow developers to restrict which other classes or interfaces can
extend or implement them. This concept provides more control over the inheritance
hierarchy, improving security, maintainability, and predictability of the code.
A sealed class is a class that cannot be subclassed freely. Instead, you can define a
set of specific subclasses (or implementers) that are allowed to extend or implement
the sealed class.
✅ Declaring a Sealed Class:
To declare a class as sealed, you use the sealed modifier followed by
the permits keyword, which lists the allowed subclasses.
public sealed class Shape permits Circle, Rectangle {
// Common functionality for all shapes }
public final class Circle extends Shape {
// Circle-specific code }
public final class Rectangle extends Shape {
// Rectangle-specific code }