OOPs With Java
Unit: - 3
Java New Features
Abstract Methods
An abstract method in Java is a method declared without a
body (i.e., no implementation). It only defines the method
signature and must be implemented by subclasses.
The class containing the abstract method must be abstract.
Abstract methods act like placeholders for methods that must
be implemented in the child classes.
Syntax
abstract class Animal {
abstract void makeSound(); // abstract method
}
Interfaces in Java
An interface in Java is a reference type, similar to a class,
that can contain:
Abstract methods (methods without a body),
Default methods (with implementation),
Static methods, and
Constants (public static final variables).
Interfaces are used to define a contract that classes must
follow, without dictating how the methods are
implemented.
Key Features of Interfaces
Cannot be instantiated directly.
All fields are public, static, and final by default.
All abstract methods are public and abstract by default.
A class can implement multiple interfaces, enabling
multiple inheritance (not possible with classes).
Syntax
interface Vehicle {
void start(); // abstract method
}
Functional Interfaces
A functional interface in Java is an interface that contains
exactly one abstract method.
These interfaces are used primarily in lambda
expressions and method references, enabling more
concise and readable code, especially for functional-
style programming introduced in Java 8.
Key Characteristics
Has exactly one abstract method.
Can have default and static methods.
Annotated with @FunctionalInterface (optional but
recommended).
Used with lambda expressions, method references, and
streams API.
Example
@FunctionalInterface
interface MyFunction {
void execute();
}
Functional Interfaces in Java Related to Lambda
Expressions
Functional interfaces and lambda expressions in Java are
closely related—in fact, lambda expressions are only
allowed in contexts where a functional interface is
expected.
Example Without Lambda:
@FunctionalInterface
interface Greeting {
void sayHello();
}
class Hello implements Greeting {
public void sayHello() {
System.out.println("Hello!");
}
}
Example With Lambda:
Greeting greet = () -> System.out.println("Hello!");
greet.sayHello(); // Output: Hello!
Method References in Java
Method references in Java are a shorthand alternative to
lambda expressions.
They allow you to refer to a method directly by its name,
when that method already matches the signature of a
functional interface method.
Syntax
ClassName::methodName
Types of Method References
1. Reference to a Static Method
@FunctionalInterface
interface Calculator {
int compute(int a, int b);
}
class MathOperations {
static int add(int x, int y) {
return x + y;
}
}
public class Test {
public static void main(String[] args) {
Calculator calc = MathOperations::add; // method
reference
System.out.println(calc.compute(5, 3)); //
Output: 8
}
}
2. Reference to an Instance Method of a Particular
Object
class Printer {
void print(String msg) {
System.out.println(msg);
}
}
public class Test {
public static void main(String[] args) {
Printer printer = new Printer();
Consumer<String> printRef = printer::print; //
instance method reference
printRef.accept("Hello"); // Output: Hello
}
}
3. Reference to an Instance Method
List<String> names = Arrays.asList("Alice", "Bob",
"Charlie");
// Equivalent lambda: s -> s.toUpperCase()
names.stream().map(String::toUpperCase).forEach(Sy
stem.out::println);
4. Reference to a Constructor
@FunctionalInterface
interface PersonFactory {
Person create(String name);
}
class Person {
String name;
Person(String name) {
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
PersonFactory factory = Person::new; //
constructor reference
Person p = factory.create("John");
System.out.println(p.name); // Output: John
}
}
Stream API
The Stream API in Java (introduced in Java 8) is used to
process collections of data in a functional and
declarative way—that means you can perform
operations like filtering, mapping, and reducing without
writing boilerplate code like loops.
What is a Stream?
A Stream is a sequence of elements that supports
various operations to process data:
It does not store data itself.
It is not a data structure (like List or Set).
It works with Collections, arrays, or I/O channels.
Default and Static Methods
Java 8 introduced default and static methods in
interfaces, which allowed interfaces to have concrete
(non-abstract) methods for the first time.
1. Default Methods
A default method is a method in an interface that
has a default implementation. It allows you to add
new methods to interfaces without breaking
existing implementations.
Syntax
interface Vehicle {
default void start() {
System.out.println("Vehicle is starting");
}
}
Why Use Default Methods?
To extend interfaces without affecting classes that
implement them.
To provide a common implementation shared across
multiple classes.
2. Static Methods in Interfaces
A static method in an interface belongs to the
interface itself, not to the implementing classes.
It can only be called using the interface name.
Syntax
interface Vehicle {
static void fuelType() {
System.out.println("Petrol or Diesel");
}
}
Base64
Base64 is used to encode binary data (like images or files)
into a text string made up of ASCII characters. It's especially
useful when transmitting data over media that are designed
to deal with text (like JSON, XML, or HTML).
import java.util.Base64;
public class Base64Example {
public static void main(String[] args) {
String original = "Hello, World!";
// Encode
String encoded =
Base64.getEncoder().encodeToString(original.getBytes())
;
System.out.println("Encoded: " + encoded);
// Decode
byte[] decodedBytes =
Base64.getDecoder().decode(encoded);
String decoded = new String(decodedBytes);
System.out.println("Decoded: " + decoded);
}
}
For each Method
In java, the forEach method is a terminal operation
provided by stream API.
It allows you to perform a specified action for each
element in a stream, iterating over the elements
sequentially.
The forEach method accepts a functional interface as an
argument, which defines the action to be performed on
each element of the stream.
try-with-resources statement in java
The try-with-resources statement is a feature introduced
in Java 7 to simplify the management of resources like
files, sockets, streams, etc., that need to be closed after
use.
Features of Try-With-Resources
Feature Description
Automatic closing No need for finally to close
resources.
Supports multiple Declare multiple resources
resources. in try().
Cleaner Syntax Less boilerplate code
Exception Suppression. Handles exceptions
through during both try
and close.
Type Annotations
Type annotations are annotations that can be applied
directly to types — not just declarations like classes,
methods, or fields.
Before Java 8, annotations could only be used on
declarations like:
Classes
Methods
Fields
Parameters
But now, you can annotate any use of a type, for
example:
Inside generics
In type casts
On new expressions
In throws clauses
In method references
Repeating Annotations
Repeating annotations in Java, introduced in Java 8 through
JSR 308, allow developers to apply the same annotation more
than once to a single program element such as a class,
method, or field.
This is done by marking the annotation with @Repeatable
and creating a corresponding container annotation that holds
an array of the repeated annotation.
This feature eliminates the need for grouping annotations
manually in a container, improving code readability and
reducing boilerplate.
It is especially useful in scenarios like defining multiple roles,
permissions, constraints, or event handlers where repeating
the same annotation type with different values is necessary.
Reflection APIs can retrieve these annotations easily using
getAnnotationsByType(), making it powerful and efficient for
metadata handling in large applications.
Java Module System
The java module system provides a way to modularize java
applications by encapsulating code into discrete units called
modules.
This allows developers to organize codebase into logical units
with well-designed dependencies and boundaries.
The java module system offers significant advantages for
developing and maintaining large scale java applications.
Advantages
1. Modularization
2. Encapsulation
3. Improved Performance
4. Isolation and Security
5. Scalability
Diamond Syntax
Diamond syntax in Java, introduced in Java 7, simplifies
the use of generics by allowing the compiler to infer
type parameters automatically.
Before this feature, programmers had to explicitly
specify the generic types on both sides of the
assignment, which was verbose and repetitive.
With diamond syntax, you write the generic type only
once on the left side, and the compiler fills in the rest,
making the code cleaner and easier to read.
This reduces boilerplate and potential errors in
specifying types.
Diamond syntax works with most generic classes like
collections (e.g., ArrayList, HashMap), enhancing code
maintainability and clarity.
Inner Anonymous classes in java
Inner anonymous classes in Java are special types of
inner classes without a name, declared and instantiated
in a single expression.
They are often used to provide quick implementations of
interfaces or abstract classes, especially for event
handling, callbacks, or threading, without creating
separate named classes.
Because they are defined inside methods or expressions,
they can access final or effectively final variables from
the enclosing scope.
Anonymous inner classes simplify code by enabling
inline behaviour definitions. For example, when creating
a thread, you can override the run() method using an
anonymous inner class, allowing concise and readable
code.
Example: -
Without use of anonymous class
interface Person{
void show();
}
class Student implements Person
{
public void show()
{
System.out.println(“show”);
}
}
public class AnonymousEx {
public static void main(String args[])
{
Person p=new Students();
p.show();
}
With use of anonymous class
interface Person{
void show();
}
public class AnonymousEx {
public static void main(String args[])
{
Person p = Person(){
Public void show(){
System.out.println(“show”);
}
};
Local Variable Type Inference
Local Variable Type Inference is a feature in
programming languages (like Java, introduced in Java 10)
that allows the compiler to automatically infer the data
type of a local variable based on the value it is initialized
with.
Instead of explicitly declaring the type, you use a
keyword like var:
Example in Java:
// Before Java 10
String message = "Hello, World!";
int count = 10;
// With local variable type inference (Java 10+)
var message = "Hello, World!";
var count = 10;
Benefits for Code Readability
1. Focuses on logic, not syntax:
o Helps the reader concentrate on what the code is
doing rather than the types.
2. Improves maintainability:
o If you change the right-hand side type, you don’t
need to update the left-hand declaration.
3. Enhances readability in complex generic types:
o Especially helpful when working with deeply nested
generics or lambda expressions.
Switch Statement in Java
A switch statement in Java is a control flow statement
that allows a variable to be tested for equality against a
list of values, called cases.
It's often used as an alternative to a series of if-else
statements.
Syntax (Traditional Switch Statement):
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
default:
System.out.println("Invalid day");
}
Key Points:
break prevents fall-through to the next case.
default is optional and executed when no case matches.
Works with int, char, enum, String (since Java 7), etc.
Switch Expression
A switch expression enhances the traditional switch
statement by:
Allowing it to return a value.
Using arrow (->) syntax to avoid fall-through.
Being more concise and readable.
Syntax:
int day = 3;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
default -> "Invalid day";
};
System.out.println(dayName);
Features:
No need for break.
Returns a value (assignable to a variable).
Allows block bodies with yield:
yield Keyword in Java
The **yield** keyword was introduced in Java 13 (as a
preview) and became a standard part of switch
expressions in Java 14.
It is used inside a switch expression to return a value
from a case block when using block syntax (i.e., {}).
Why is yield needed?
In switch expressions, when you use a code block {}
inside a case, you can’t just use return or an expression;
instead, you use yield to specify the value to return from
that block.
Example:
int day = 2;
String dayType = switch (day) {
case 1, 7 -> "Weekend";
case 2, 3, 4, 5, 6 -> {
System.out.println("Weekday logic here");
yield "Weekday";
}
default -> "Invalid day";
};
System.out.println(dayType);
Text Blocks in Java
A Text Block is a feature introduced in Java 13 (preview)
and standardized in Java 15 that allows you to create
multi-line string literals more easily and readably using
triple double-quotes """.
Syntax of Text Block
String html = """
<html>
<body>
<h1>Hello, World!</h1>
</body>
</html>
""";
Benefits of Text Blocks for String Manipulation
Improved Readability
Less Escaping
Automatic Formatting
Easier Maintenance
Records in Java
Records are a special type of class introduced in Java 14
(preview) and standardized in Java 16 that make it easier
to create immutable data objects.
They are designed to model data carriers—objects
whose main purpose is to hold data, like DTOs (Data
Transfer Objects).
Syntax of a Record
public record Person(String name, int age) {}
This single line automatically generates:
A final class
Private final fields: name and age
A constructor
Getters: name() and age()
equals(), hashCode(), and toString() methods
How Records Simplify Immutable Data Creation
1. Concise Syntax
Records provide a concise syntax for declaring classes
that the primarily used to hold data.
2. Implicit Finality
By default all components of a record are implicitly final.
This ensures that instances if the record are immutable.
3. Compact Initialization
Records generate a compact constructor to initialize the
record’s components. This allows you to create instances
of the record using a concise syntax.
Sealed Classes in Java
Sealed classes (introduced in Java 15 as a preview and
finalized in Java 17) are a powerful feature that lets you
control which classes can extend or implement a given
class or interface.
Purpose of Sealed Classes
Sealed classes provide fine-grained control over
inheritance, improving:
Security: Prevents unexpected or unauthorized
subclasses.
Maintainability: Helps document and enforce an
intended class hierarchy.
Exhaustiveness: Improves compiler checks, especially
with switch expressions over class types.
Syntax of a Sealed Class
public sealed class Vehicle permits Car, Truck {
// common vehicle code
}
Role in Controlling Inheritance Hierarchies
Benefit Explanation
Explicit control Only the classes you list in
permits can extend your
sealed class.
Improved reasoning When you use sealed
classes in pattern
matching or switch
expressions, the compiler
can check for
exhaustiveness —
meaning you handled all
permitted types.
Better encapsulation Limits the number of
subclasses, helping keep
the code predictable and
maintainable.
Useful with pattern Works well with instanceof
matching and future enhancements
in pattern matching for
deconstructing types.