0% found this document useful (0 votes)
1 views9 pages

Chapter 13

This document covers various advanced features of Java, including Type Annotations, Repeating Annotations, the Java Module System, and enhancements in Java 7 to 17 such as the Diamond Operator, Local Variable Type Inference, Switch Expressions, Text Blocks, Records, and Sealed Classes. It provides examples and explanations for each feature, emphasizing their benefits for code readability, maintainability, and safety. The document serves as a comprehensive guide for developers looking to leverage these features in their Java programming.

Uploaded by

qjgfb66tvw
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
1 views9 pages

Chapter 13

This document covers various advanced features of Java, including Type Annotations, Repeating Annotations, the Java Module System, and enhancements in Java 7 to 17 such as the Diamond Operator, Local Variable Type Inference, Switch Expressions, Text Blocks, Records, and Sealed Classes. It provides examples and explanations for each feature, emphasizing their benefits for code readability, maintainability, and safety. The document serves as a comprehensive guide for developers looking to leverage these features in their Java programming.

Uploaded by

qjgfb66tvw
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 9

Object Oriented Programming With JAVA

Chapter-13

Annota ons in JAVA

Type Annotations

Java 8 introduced Type Annotations, allowing developers to apply annotations


wherever a type is used, including within the Stream API. It is mostly useful for static analysis,
null-safety, immutability, or custom validation tools. Normally, annotations are placed above
declarations:
@Override
public String toString() { ... }
They're typically used with:
 Streams
 Collection Generics
 Type Casts
 Object creation
 Return types
 Lambda expression
When working with the Stream API, type annotations can enhance code readability and
safety. Here’s how you can integrate them effectively:
1. Type Annotations for Collections: Ensure type safety in streams by applying type
annotations to the generic types in collections.
List<@NonNull String> names = Arrays.asList("Java", "Stream", "API");
names.stream().forEach(System.out::println);
2. Type Annotations in Lambda Expressions: Improve type inference within lambda
expressions to specify types explicitly.
Stream<@NonNegative Integer> numberStream = Stream.of(1, 2, 3, 4).
filter((@NonNegative Integer n) -> n > 0);
3. Enhancing Stream Operations: Use type annotations in advanced stream operations to
ensure consistency and detection of potential issues.
List<@ReadOnly String> readOnlyList =
Collections.unmodifiableList(Arrays.asList("Java", "Stream"));
readOnlyList.stream().map((@ReadOnly String s) -> s.toUpperCase())
.forEach(System.out::println);

Repeating Annotations
With Java 8 and later versions, you can use Repeating Annotations to apply the same
annotation multiple times to a single element, enhancing your code's flexibility. Although
Repeating Annotations aren't directly linked to the Stream API, they can be effectively used
Object Oriented Programming With JAVA
on classes, methods, or parameters that work with streams, boosting your Java programming
capabilities. Java allows you to repeat an annotation on the same declaration:
Syntax:
@Schedule(day = "Monday")
@Schedule(day = "Friday")
public void streamTask() {
// do something with streams
}
To effectively work with repeatable annotations, you'll need two components:
1. Container Annotation: This serves as a holder for the repeated annotations.
2. Main Annotation with @Repeatable: This is the primary annotation that can be used
multiple times. By having both of these elements, you can efficiently utilize repeatable
annotations in your code.
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Arrays;

// Step 1: Define the container annotation


@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
Schedule[] value();
}

// Step 2: Define the repeatable annotation


@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Schedule {
String day();
}
public class MyProg {
@Schedule(day = "Monday")
@Schedule(day = "Wednesday")
@Schedule(day = "Friday")
public void processStream() {
List<String> names = Arrays.asList("Ram", "Shyam", "Deepak");
names.stream().filter(name -> name.startsWith("S"))
.forEach(System.out::println);
}
public static void main(String[] args) throws Exception {
Schedule[] sch = MyProg.class.getMethod("processStream")
.getAnnotationsByType(Schedule.class);
for (Schedule s : sch) {
System.out.println("Scheduled on: " + s.day());
}
}
}
Output:
Object Oriented Programming With JAVA

Java Project Module System (JPMS)

The Java Module System, introduced in Java 9, enables developers to organize


applications into well-structured modules. These modules serve as robust, encapsulated
units of code with clearly defined dependencies.
Java Stream API is a powerful tool for processing sequences of elements, such as
collections, in a functional style. Mastering its structure can greatly enhance the efficiency
and readability of your code. Below, we outline the key components of a module using Java
Stream API.
1. Source: Every stream begins with a source. This is the collection or array from
which the stream will be created. Common sources include Lists, Sets, Maps, and arrays. For
example:
List<String> strings = Arrays.asList("apple", "orange", "banana");
Stream<String> stream = strings.stream();
2. Intermediate Operations: These operations transform a stream into another
stream. They are lazy, meaning they only execute when a terminal operation is invoked.
Some common intermediate operations are:
a) filter: Filters elements based on a condition.
b) map: Transforms each element of the stream.
c) sorted: Sorts the elements.
Example:
Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"));
3. Terminal Operations: These operations produce a result or a side-effect and mark
the end of the stream. Once a terminal operation is executed, the stream is consumed and
cannot be used again. Examples include:
a) collect: Converts the elements of the stream into a collection.
b) for Each: Performs an action for each element.
c) count: Returns the count of elements.
Example:
List<String> result = filteredStream.collect(Collectors.toList());
4.Parallel Stream: For performance optimization, streams can be processed in
parallel to leverage multi-core processors. Simply replace .stream() with .parallelStream() to
enable this feature:

List<String> result = strings.parallelStream() .filter(s ->


s.startsWith("a")).collect(Collectors.toList());

Example Basic Structure of a module using Stream API


mod-stream-app/
Object Oriented Programming With JAVA
├── module-info.java
├── com/
│ └── example/
│ └── streamapp/
│ └── Main.java

module-info.java
module mod.stream.app {
requires java.base; // optional (java.base is required by default)
}

Main.java
package com.example.streamapp;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Ram", "Shyam", "Deepak");
names.stream().filter(name -> name.startsWith("D"))
.map(String::toUpperCase).forEach(System.out::println);
}
}

Diamond Operator (<>)


The diamond syntax, introduced in Java 7, simplifies the instantiation of generic
types. It allows you to omit explicit type arguments on the right side of a statement, enabling
the compiler to infer them from the variable declaration on the left side. For instance:
List<String> list = new ArrayList<>(); // Type inferred as ArrayList<String>
Here, the diamond operator <> tells the compiler to use the type parameter declared on the
left side, i.e., String.
Using Diamond Syntax with Inner Anonymous Classes
An anonymous class is a class without a name that is instantiation-ready at the point
of declaration. Sometimes, you'll need to create an instance of an anonymous class while
utilizing generics. Here's how you can combine this with diamond syntax:
import java.util.*;
public class MyProg {
public static void main(String[] args) {
// List using diamond syntax
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
for (String fruit : fruits) {
System.out.println(fruit);
}
// Map using diamond syntax
Map<String, Integer> stock = new HashMap<>();
stock.put("Apple", 50);
stock.put("Banana", 30);
System.out.println("Stock: " + stock);
Object Oriented Programming With JAVA
}
}
Output:

In this example, an anonymous subclass of HashMap is created. The diamond syntax


applies to the HashMap() creation, allowing the compiler to infer the Integer and String types
without explicitly rewriting them.
Benefits-
1. Simplified Code: The diamond operator reduces redundancy, making code cleaner and
easier to read.
2. Type Safety: It enhances the type safety of the code by enabling the compiler to verify
the type compatibility.
3. Enhanced Maintainability: By minimizing verbosity, your codebase becomes more
maintainable, reducing the likelihood of errors during updates.
Local Variable Type Inference
Java 10 introduced local variable type inference with the var keyword. We cannot use
var as initializer, null initializer, Lambda expressions, Fields, method parameters and return
types. This lets the compiler infer the type of a local variable based on its assigned value.
Syntax:
var variableName = expression;// The type of variableName is inferred at compile
time, based on the expression.
Example:
var name = "Alice”; // Inferred as String
var age = 30; // Inferred as int
var list = new ArrayList<String>(); // Inferred as ArrayList<String>
Another Example with Stream API
var names = List.of("Ram", "Shyam", "Deepak");
var filtered = names.stream().filter(n -> n.startsWith("A")) .toList();
filtered.forEach(System.out::println);
Another Example with Loop
var numbers = List.of(1, 2, 3, 4, 5);
for (var num : numbers) {
System.out.println(num);
}

Switch Expressions
In Java 14, the traditional switch statement has been upgraded to a more robust
switch expression. a switch expression evaluates a single variable or expression and matches
it against multiple possible values. When a match is found, the corresponding code block is
executed. Unlike the conventional switch statements, switch expressions can return values,
making them more functional and versatile.
Object Oriented Programming With JAVA
This new feature offers improved conciseness and readability while allowing you to return
values directly from a switch, making it more efficient and powerful.

Traditional Switch Switch Expressions


String result; String result = switch (day) {
switch (day) { case "MONDAY" -> "Start of week";
case "MONDAY": case " SUNDAY " -> "End of week";
result = "Start of week"; default -> "Midweek";
break; };
case "SUNDAY":
result = "End of week";
break;
default:
result = "Midweek";
}

Switch with Enums


The use of switch statements with enums can greatly enhance your coding efficiency
and readability. Enums, short for enumerations, are a special type in many programming
languages that allow developers to define a set of named constants, making your code more
intuitive and error-free.

public class MyProg {


enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public static void main(String[] args) {
Day day = Day.MONDAY;
String type = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
case SATURDAY, SUNDAY -> "Weekend";
};
System.out.println("Day type: " + type);
}
}

Switch with Yield


yield replaces break and returns a value from the block. 'yield' is used within switch
expressions to return a value. It provides a structured way to assign a value to a variable
based on different cases in the switch statement. Use yield when the result of a case block
involves more logic:
Object Oriented Programming With JAVA
public class MyProg {
public static void main(String[] args) {
String day = "WEDNESDAY";
String result = switch (day) {
case "MONDAY", "TUESDAY" -> "Early week";
case "WEDNESDAY" -> {
System.out.println("Midweek logic");
yield "Midweek";
}
default -> "Later week";
};
System.out.println("Result: " + result);
}
}

Text Blocks in Java


Java Text Blocks (introduced in Java 13 as preview and finalized in Java 15) are a
feature that allows you to write multi-line strings in a clean, readable way using triple
quotes (""").
Traditional (Verbose & Ugly) Text Block (Clean)
String query = "SELECT * FROM users String query = """
WHERE age > 18\n" + SELECT * FROM users WHERE age > 18
"AND status = 'active'"; AND status = 'active'
""";

Example:
public class MyProg {
public static void main(String[] args) {
String json = """
{
"name": "Ram",
"age": 20,
"city": "Delhi"
}
""";
System.out.println(json);
}
}
Object Oriented Programming With JAVA

Records in Java
Records (introduced in Java 14 as a preview and finalized in Java 16) are a special
class type designed for immutable data-holding objects with minimal boilerplate. There are
several reasons to use it.
1. Automatic Getters-Java Records provide automatic getter methods, eliminating
the need to manually write methods like getX(). This feature streamlines your code and
improves its readability.

2. Built-in toString(), equals(), and hashCode()-With Records, there's no need to


manually implement toString(), equals(), or hashCode() methods. These are auto-generated,
ensuring a consistent and error-free implementation every time.

3. Immutability-Fields in Java Records are inherently final, making them immutable


by default. This ensures data integrity and simplifies the design of your application by
reducing unforeseen side-effects associated with mutable objects.

4. Conciseness-With Java Records, there's no requirement to write constructors,


setters, or overrides, allowing you to focus on the core functionality of your application while
maintaining clean and concise code.
Example:
record Person(String name, int age) {}
public class Main {
public static void main(String[] args) {
Person p = new Person("Ram", 25);
System.out.println("Name: " + p.name()); // Accessor method
System.out.println("Age: " + p.age());
System.out.println("Record: " + p); // Calls toString()
}
}

Sealed Classes
Sealed classes (introduced in Java 15 as a preview and finalized in Java 17) allow you
to restrict which classes can extend or implement them. Sealed classes allow you to specify
Object Oriented Programming With JAVA
which classes are permitted to extend or implement them. By using this feature, developers
can reduce the risk of unintended subclassing, making the code easier to manage and
understand.
To create a sealed class in Java, use the sealed modifier in your class declaration,
followed by the permits keyword to specify the subclasses. Here’s a simple example:
public class MyProg {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(4, 3);
System.out.println("Circle Area: " + s1.area());
System.out.println("Rectangle Area: " + s2.area());
}
}
public abstract sealed class Shape permits Circle, Rectangle {
public abstract double area();
}
final class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
final class Rectangle extends Shape {
private double length, width;
public Rectangle(double l, double w) {
this.length = l;
this.width = w;
}
@Override
public double area() {
return length * width;
}
}

In this example, only the Car and Truck classes are permitted to extend the Vehicle
class, providing a clear, predefined hierarchy.
Keyword Meaning
sealed Limits subclasses to only those in permits.
final No further subclassing allowed.
non-sealed Allows further subclassing beyond those in permits.

You might also like