Unit 3.2

Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

Java Stream API

The Stream API, introduced in Java 8. It allows for functional-style operations on collections of objects. This
API helps in writing more readable, efficient, and concise code. Streams support various operations such as
filtering, mapping, and reducing.

Characteristics of Streams

Sequence of Elements: Streams represent a sequence of elements of a specific type. This sequence can be
ordered or unordered depending on the source.

Pipelining: Streams support pipelining of multiple operations, where the result of one operation is passed
directly to the next. This allows for a fluent programming style.

Laziness: Streams are lazy, meaning that intermediate operations (like filter, map, etc.) are not executed
until a terminal operation (like collect, forEach, etc.) is invoked. This allows for efficient computation
by optimizing the intermediate operations.

Consumable: Streams can be traversed only once. After a terminal operation is performed, the stream is
considered consumed and cannot be reused. To traverse the data again, a new stream needs to be created.

Creating Streams

Streams can be created from various data sources such as collections, arrays, or specific methods:

// From a Collection
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

// From an Array
Stream<String> stream = Stream.of("a", "b", "c");

// From Values
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

// From a File (Java NIO)


Stream<String> lines = Files.lines(Paths.get("file.txt"));
Intermediate Operations

1. filter(): Filters elements based on a predicate.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);


List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());

2. map(): Transforms elements using a function.

List<String> strings = Arrays.asList("a", "b", "c");


List<String> upperCaseStrings = strings.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());

3. sorted(): Sorts elements in natural order or using a comparator.

List<Integer> numbers = Arrays.asList(5, 3, 1, 4, 2);


List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());

4. distinct(): Removes duplicate elements.

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);


List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
Terminal Operations

1. forEach(): Performs an action for each element.

List<String> list = Arrays.asList("a", "b", "c");


list.stream().forEach(System.out::println);

2. collect(): Collects elements into a collection or other container.

List<String> list = Arrays.asList("a", "b", "c");


List<String> result = list.stream()
.collect(Collectors.toList());

3. reduce(): Reduces elements to a single value using an associative accumulation function.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);


int sum = numbers.stream()
.reduce(0, Integer::sum);

4. count(): Counts the number of elements in the stream.

List<String> list = Arrays.asList("a", "b", "c");


long count = list.stream().count();

Example: create a list of names and filter out the names that are shorter than 5 characters, convert the
remaining names to uppercase, and collect the results into a new list.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class WithStreamExample {


public static void main(String[] args) {
List<String> names = Arrays.asList("Anil", "Amit", " Abhishek");

List<String> result = names.stream()


.filter(name -> name.length() >= 5)
.map(String::toUpperCase)
.collect(Collectors.toList());

System.out.println(result); // Output: [ABHISHEK]


}
}
Annotations
Annotations in Java are a form of metadata that provide data about a program but are not part of the program
itself. They have no direct effect on the code they annotate but can be used by the compiler, development tools,
or during runtime by other programs.

Basic Syntax

Annotations are defined with the @ symbol, followed by the annotation name:

@Override
public String toString() {
return "Example";
}

Built-in Annotations

 @Override
Indicates that a method is overriding a method in a superclass.
Helps catch errors if the method signatures don't match.
 @Deprecated
o Marks a method or class as deprecated, indicating it should no longer be used.
o Generates a warning if the deprecated code is used.
 @SuppressWarnings
o Tells the compiler to suppress specific warnings.

Custom Annotations

You can create your own annotations using the @interface keyword.

Example:

@interface MyAnnotation {
String value();
int number() default 0;
}

Usage:

@MyAnnotation(value = "Test", number = 5)


public void myMethod() {
// method body
}
Meta-Annotations

Meta-annotations are annotations that apply to other annotations. Java provides several built-in meta-
annotations:

1. @Retention
o Specifies how long the annotation should be retained.
o Values: RetentionPolicy.SOURCE, RetentionPolicy.CLASS, RetentionPolicy.RUNTIME.
2. @Target
o Specifies where the annotation can be applied (e.g., method, field, type).
o Values: ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, etc.
3. @Documented
o Indicates that the annotation should be documented by javadoc and similar tools.
4. @Inherited
o Indicates that the annotation type can be inherited from the superclass.

Example:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyRuntimeAnnotation {
String value();
}
Try-with-resources in Java
Try-with-resources is a feature introduced in Java 7 that simplifies handling resources. A resource is any
object that needs to be properly closed after use, such as files, network connections, or databases.

You declare resource variables within parentheses after the try keyword. These variables are automatically
closed at the end of the try block, even in case of exceptions.

Example:

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {


String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} // reader is automatically closed here

Key points:

 You can use multiple resources within parentheses, separated by commas.


 Closing order: Resources are closed in reverse order of their creation.
 Exceptions: If an exception occurs in the try block, resources are still closed after the exception is
handled. Exceptions thrown by closing resources are suppressed (not shown).

Benefits:

 Safer code: Reduces the risk of resource leaks.


 Cleaner code: Less error-prone and easier to read.
 Improved maintainability: Makes code easier to understand and maintain.
Switch Expression

A switch expression is a new way to use the switch statement in Java. It was Introduced in Java 12 and made
standard in Java 14. It allows you to return a value from the switch statement. This makes your code cleaner
and easier to read.

Syntax

Following is the syntax of switch expression:

result = switch (variable) {


case value1 -> expression1;
case value2 -> expression2;
case value3, value4 -> expression3;
default -> expression4;
};

Characteristics

 Conciseness: Switch expressions use the -> syntax, which makes the code shorter and cleaner.
 Exhaustiveness: You must handle all possible cases, either explicitly or with a default case.
 Return Values: Switch expressions return a value that you can directly assign to a variable.
 Multiple Labels: You can group multiple case labels together.
 Yield Statement: You can use the yield statement to return values in more complex scenarios.

Example

Here is a simple example to show how a switch expression works:

public class Example {


public static void main(String[] args) {
String day = "MONDAY";

String typeOfDay = switch (day) {


case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" ->
"Weekday";
case "SATURDAY", "SUNDAY" -> "Weekend";
default -> “Invalid Day”;
};

System.out.println("The type of day is: " + typeOfDay);


}
}

Explanation:

 Input: The variable day is checked against different cases.


 Output: Depending on the value of day, a corresponding string ("Weekday" or "Weekend") is assigned
to typeOfDay.
Use of yield Keyword

The yield keyword is used to return a value from a block of code within a switch expression. It allows for
more complex logic before returning a value.

Example: Days in a Month

Let's create a program that returns the number of days in a month based on the month number:

public class Example {


public static void main(String[] args) {
int month = 2;
int year = 2024;

int daysInMonth = switch (month) {


case 1, 3, 5, 7, 8, 10, 12 -> 31;
case 4, 6, 9, 11 -> 30;
case 2 -> {
if (isLeapYear(year)) {
yield 29;
} else {
yield 28;
}
}
default -> “Invalid Day”; };

System.out.println("Number of days: " + daysInMonth);


}

private static boolean isLeapYear(int year) {


return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
}

Explanation:

 Input: The variable month is checked against different cases.


 Output: Depending on the value of month, the number of days in that month is assigned to
daysInMonth.
 Leap Year Check: For February (month 2), we check if it's a leap year using the isLeapYear method.
If it is, we yield 29 days; otherwise, we yield 28 days.
 Default Case: If month does not match any of the specified cases, the default case is executed.
Local Variable Type Inference (var)
Local Variable Type Inference allows the Java compiler to create the type of a local variable based on the value
assigned to it. It was introduced in Java 10; this feature uses the var keyword.

Why use it?

 Reduces boilerplate code.


 Makes the code more readable.
 Simplifies the variable declaration.

Example:

public class Example {


public static void main(String[] args) {
var message = "Hello, World!"; // Compiler infers String type
var number = 10; // Compiler infers int type

System.out.println(message);
System.out.println(number);

}
}

Key Points:

 var can only be used for local variables (inside methods, initializers, etc.).
 The type of the variable is created at compile-time.
Text Blocks

Text Blocks allow you to write multi-line strings more easily. It was introduced in Java 13.

Text block uses triple double quotes (""") to create the multiline strings.

Why use it?

 Makes multi-line strings more readable and easier to write.


 Reduces the need for escaping characters.

Example:

public class TextBlocksExample {


public static void main(String[] args) {
var data = """
{
"name": "John Doe",
"age": 30,
"city": "New York"
}
""";

System.out.println(data);
}
}

Key Points:

 Text Blocks preserve the format of the text.


 Leading and trailing spaces are ignored if the closing delimiter is on a new line.
Records
Records provide a compact syntax for declaring classes that are intended to be used for holding data. It was
introduced in Java 14,

A record automatically generates several useful methods such as equals(), hashCode(), toString(),
and accessor methods for the fields.

Here is the basic syntax for defining a record:

public record RecordName(Type1 fieldName1, Type2 fieldName2, ...) {


// Optionally, you can add custom methods or a compact constructor.
}

Key Points:

 Record Declaration: The record is declared using the record keyword followed by the record name
and the list of fields (also known as the record components).
 Auto-generated Methods: The compiler automatically generates a constructor, accessor methods (e.g.,
name(), age()), equals(), hashCode(), and toString() methods.
 Immutability: Records are designed to be immutable. The fields are final by default, and there are no
setters.
 Custom Methods: You can add custom methods within the record declaration to provide additional
functionality.
Example

Following is a practical example with a record named Person:

public record Person(String name, int age) {


// You can add custom methods if needed
public String greeting() {
return "Hello, my name is " + name;
}
}

public class Main {


public static void main(String[] args) {
Person person = new Person("Alice", 30);

System.out.println(person.name()); // Accessor method for name


System.out.println(person.age()); // Accessor method for age
System.out.println(person); // Auto-generated toString() method
System.out.println(person.greeting()); // Custom method
}
}

In this example:

 new Person("Alice", 30) creates a new instance of the Person record.


 person.name() and person.age() are the accessor methods that return the values of the fields.
 person.toString() (or just person) prints a string representation of the record.
 person.greeting() is a custom method that returns a greeting message.
Sealed Classes in Java
Sealed classes in Java provide a way to restrict which other classes may inherit the particular class. This feature
was introduced in Java 15.

By using sealed classes, you can have more control over your class hierarchy, ensuring that only specified
classes are allowed to be subclasses.

This helps in maintaining a more predictable and secure inheritance structure.

Syntax

The basic syntax for a sealed class involves the sealed keyword followed by a permits clause, which lists
the classes that are allowed to extend the sealed class.

public sealed class ClassName permits Subclass1, Subclass2, ... {


// class body
}

Example

Let's consider a simple example involving a sealed class hierarchy for shapes:

public sealed class Shape permits Circle, Rectangle, Triangle {


// common methods for all shapes
}

public final class Circle extends Shape {


// specific methods for Circle
}

public final class Rectangle extends Shape {


// specific methods for Rectangle
}

public non-sealed class Triangle extends Shape {


// specific methods for Triangle
}

Key Points:

 Sealed Declaration: The sealed class Shape uses the sealed keyword and the permits clause to
specify which classes can extend it (Circle, Rectangle, and Triangle in this case).
 Subclasses:
o Circle and Rectangle are final, meaning they cannot be further extended.
o Triangle is non-sealed, which means it can be extended by other classes.
Example Usage:

public class SealedClassesExample {


public static void main(String[] args) {
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
Shape shape3 = new Triangle();

// Use shape1, shape2, and shape3


}
}

Detailed Explanation:

1. Sealed Class Declaration:

public sealed class Shape permits Circle, Rectangle, Triangle {


// common methods for all shapes
}

The Shape class is declared as sealed, meaning its subclasses are restricted to those listed in the
permits clause.

2. Final Subclass:

public final class Circle extends Shape {


// specific methods for Circle
}

The Circle class is declared as final, indicating that no further subclasses can extend Circle.

3. Non-Sealed Subclass:

public non-sealed class Triangle extends Shape {


// specific methods for Triangle
}

The Triangle class is declared as non-sealed, meaning it can be extended by other classes.

Benefits:

 Control Over Inheritance: Sealed classes provide better control over which classes can extend a
particular class, leading to a more secure and predictable class hierarchy.
 Enhanced Maintainability: By restricting the inheritance, it becomes easier to manage and understand
the relationships between classes.
 Improved Exhaustiveness Checking: Sealed classes can improve the exhaustiveness checking in
switch expressions and patterns, ensuring that all possible subclasses are accounted for.

You might also like