OOPS USING JAVA Unit-3
OOPS USING JAVA Unit-3
Functional Interfaces are essential in Java because they are used to enable functional
programming features like lambda expressions and method references. These features allow
you to write more concise and expressive code, especially when dealing with tasks that involve
passing behavior as parameters.
In this example:
• We define a Functional Interface Calculator with the abstract method calculate(int a, int
b).
• We use the @FunctionalInterface annotation to ensure that the interface is a Functional
Interface (optional but recommended for clarity).
• Inside the main method, we implement the Calculator interface using lambda
expressions for addition and subtraction operations.
• We then use these implemented methods to perform addition and subtraction and print
the results.
Functional Interfaces simplify code by allowing us to treat functions as objects, making it easier
to work with functional programming paradigms in Java.
Lambda Expression:-
Lambda expressions in Java are like shortcuts for writing small pieces of code, especially when
you need to use functions that don't need a name or when you want to pass a piece of code as
an argument to another method. They're often used in functional programming style.
Here's a breakdown:
Overall, lambda expressions are a powerful tool for writing cleaner, more concise code,
especially in scenarios where you need to pass behavior as an argument or when working with
functional interfaces.
interface MathOperation {
int operate(int a, int b);
}
In this example:
This shows how Lambda Expressions can be used to write more concise and readable code,
especially when working with functional interfaces and passing functions as arguments.
Method References:-
Method References in Java are a way to refer to methods without actually calling them
immediately. It's like having a shortcut to a method that you can use later. There are different
types of method references:
1. **Static Method References:** These refer to static methods in a class. You use the class
name followed by `::` and then the method name. For example, `ClassName::methodName`.
2. **Instance Method References:** These refer to instance methods of an object. You use
the object name, followed by `::`, and then the method name. For example,
`objectName::methodName`.
3. **Constructor References:** These refer to constructors. You use the class name followed
by `::` and then `new`. For example, `ClassName::new`.
- **Static Method Reference:** Imagine you have a class `MathUtils` with a static method
`double square(double x)`. You can create a reference to this method like `MathUtils::square`.
- **Instance Method Reference:** If you have a class `Calculator` with a non-static method
`double add(double a, double b)`, and you have an instance `calc` of `Calculator`, you can
create a reference like `calc::add`.
- **Constructor Reference:** Let's say you have a class `Person` with a constructor that takes
a name as a parameter, like `Person(String name)`. You can create a reference to this
constructor using `Person::new`.
In essence, method references simplify code by providing a concise way to refer to methods
or constructors. They are often used in functional interfaces, where you can pass these
references as arguments to methods that expect functional interfaces, making your code more
readable and compact.
Explanation:
In Java, method references allow you to treat a method as a lambda expression or a functional
interface implementation. Instead of defining a new lambda expression, you can refer to an
existing method by its name.
### Example:
Suppose you have a class `Calculator` with a static method `add` and an instance method
`multiply`:
class Calculator {
public static int add(int a, int b) {
return a + b;
}
import java.util.function.BiFunction;
import java.util.List;
import java.util.function.Supplier;
Method references provide a more concise and readable way to work with functional
interfaces and lambda expressions, making your code more expressive and easier to maintain.
Stream API:-
Sure, let's break down the Stream API in Java in an easy-to-understand way:
2. **Key Concepts:**
- **Stream:** It represents a sequence of elements from a source (like a collection) that
supports various operations.
- **Intermediate Operations:** These are operations that can be chained together to process
the data stream. Examples include `filter`, `map`, `sorted`, `distinct`, etc.
- **Terminal Operations:** These are operations that produce a result or a side-effect and
terminate the stream. Examples include `forEach`, `collect`, `reduce`, `count`, etc.
- **Intermediate Operations:**
- Filtering elements:
```
Stream<Integer> evenNumbers = numberStream.filter(num -> num % 2 == 0);
```
- Mapping elements:
```
Stream<String> numberStrings = numberStream.map(num -> "Number: " + num);
```
- Sorting elements:
```
Stream<Integer> sortedNumbers = numberStream.sorted();
```
- **Terminal Operations:**
- Collecting elements into a list:
```
List<Integer> evenNumbersList = evenNumbers.collect(Collectors.toList());
```
- Getting the count of elements:
```
long count = numberStream.count();
```
4. **Benefits of Streams:**
- **Conciseness:** Streams allow you to write code in a more compact and expressive way
compared to traditional loops.
- **Readability:** Stream operations are chainable and declarative, making your code
easier to understand and maintain.
- **Efficiency:** Streams can leverage parallel processing, automatically handling parallel
execution for improved performance on multi-core processors.
In summary, the Stream API in Java simplifies data processing by providing a set of powerful
operations that can be applied to collections of data, making your code more efficient,
readable, and concise.
1. **Stream Creation**: You can create a stream from a collection using the `stream()`
method. For example:
```
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream = numbers.stream();
```
3. **Terminal Operations**: These operations produce a result or a side effect and terminate
the stream. Common terminal operations include `forEach()`, `collect()`, `reduce()`, `count()`,
and `anyMatch()`. For example:
```
// Print each element in the stream
numbers.stream().forEach(System.out::println);
```
import java.util.Arrays;
import java.util.List;
In this example, the stream is created from a list of numbers, then filtered to keep only even
numbers using the `filter()` method, and finally, the `reduce()` method is used to calculate
their sum.
The Stream API simplifies the processing of collections by providing a fluent and declarative
way to perform operations on them.
Default Methods:-
In Java, default methods are a feature introduced in Java 8 that allows interfaces to have
concrete methods. Before Java 8, interfaces could only have abstract methods, meaning
methods without a body. Default methods were added to interfaces to provide a way to add
new functionality to interfaces without breaking existing implementations.
Default methods in Java's object-oriented programming provide a way to add new methods to
interfaces without breaking existing implementations. Here's a detailed explanation in easy
language:
void stop();
}
Overall, default methods in Java are a powerful feature that promotes code reusability,
flexibility, and maintainability in object-oriented programming.
Static Method:-
What is a Static Method?
A static method in Java is a type of method that belongs to the class itself rather than to any
specific object instance of that class. This means that you can call a static method directly on
the class without creating an object of that class. Static methods are commonly used for
utility functions or operations that are not tied to any particular instance of the class.
In Java, a static method belongs to the class rather than an instance of the class. Here’s a
detailed breakdown:
1. **Belongs to the Class:** When you create a method in a Java class and declare it as
static, it means that this method is associated with the class itself, not with any particular
object (instance) of that class.
2. **Accessing Static Methods:** You can access static methods directly using the class
name, without needing to create an object of that class. For example, if you have a class
called `Calculator` with a static method `add`, you can call it like this: `Calculator.add(5,
10);`
3. **No Access to Instance Variables:** Inside a static method, you cannot directly access
instance variables or use `this` keyword because static methods are not associated with any
specific object. They operate on class-level data and behavior.
4. **Use Cases:** Static methods are commonly used for utility functions that don’t rely on
instance-specific data. For instance, methods to perform calculations, validate data, or format
strings are often declared as static.
5. **Memory Efficiency:** Since static methods are associated with the class and not
individual objects, they save memory because they are loaded into memory only once,
regardless of how many instances of the class are created.
Sure, I'd be happy to explain static methods in Java in an easy-to-understand way with an
example.
1. **Declaration:** To declare a static method in Java, you use the `static` keyword before
the return type of the method. Here's an example:
```
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
In this example, `add` and `square` are static methods of the `MathUtils` class.
2. **Accessing Static Methods:** Since static methods belong to the class itself, you can call
them directly using the class name, without creating an object:
```
public class Main {
public static void main(String[] args) {
int sum = MathUtils.add(5, 3); // Calling a static method
System.out.println("Sum: " + sum);
In this example, `MathUtils.add(5, 3)` calls the static `add` method to add two numbers, and
`MathUtils.square(4.5)` calls the static `square` method to calculate the square of a number.
4. **Limitations:**
- **Accessing Non-Static Members:** Static methods cannot directly access non-static
members (variables or methods) of a class. They operate at the class level, not the instance
level.
- **Inheritance:** Static methods are not overridden in subclasses like instance methods.
They are associated with the class they are defined in.
2. **Encode Data**: To encode data, you use the `encodeToString` method of the `Base64`
class. This method takes a byte array as input and returns the Base64-encoded string.
```
byte[] data = "Hello, world!".getBytes(); // Convert string to byte array
String encodedData = Base64.getEncoder().encodeToString(data);
```
In this example, `encodedData` will contain the Base64-encoded representation of the string
"Hello, world!".
2. **Decode Data**: To decode Base64-encoded data back to its original form, you use the
`decode` method of the `Base64` class. This method takes a Base64-encoded string as input
and returns the decoded byte array.
```
String encodedData = "SGVsbG8sIHdvcmxkIQ=="; // Example Base64-encoded string
byte[] decodedData = Base64.getDecoder().decode(encodedData);
```
In this example, `decodedData` will contain the byte array representing the original string
"Hello, world!" after decoding.
### Why Base64 Encoding?
Base64 encoding is commonly used in scenarios where binary data needs to be represented as
text, such as:
- Sending binary data over text-based protocols like HTTP or SMTP.
- Storing binary data in text-based formats like XML or JSON.
- Encoding binary files (e.g., images, PDFs) for transmission over networks.
```
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
In this example, `image.jpg` is read, encoded to Base64, decoded back, and then written to
`decoded_image.jpg`. This demonstrates the practical use of Base64 encoding and decoding
for binary data.
```
import java.util.Base64;
// Encoding
String encodedString = Base64.getEncoder().encodeToString(bytes);
System.out.println("Encoded String: " + encodedString);
}
}
```
```
import java.util.Base64;
// Decoding
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
String decodedString = new String(decodedBytes);
System.out.println("Decoded String: " + decodedString);
}
}
```
Base64 encoding and decoding are fundamental techniques for handling binary data in text-
based environments, making them essential tools in various applications such as data
transmission, cryptography, and file storage.
ForEach Method:-
The `forEach` method in Java is used to iterate over elements in a collection, such as arrays or
lists, and perform a specified action for each element. It's part of the Stream API introduced in
Java 8, which provides a more functional programming style for working with collections.
1. **Syntax**:
```
collection.forEach(element -> {
// Perform action on each element
});
```
2. **Explanation**:
- `collection`: This represents the collection of elements you want to iterate over, like an array
or a list.
- `forEach`: This is the method used to iterate over each element in the collection.
- `element -> { // Perform action }`: This is a lambda expression that defines the action to be
performed on each element. The `element` variable represents each individual element in the
collection.
3. **Example**:
Let's say you have an array of numbers and you want to print each number multiplied by 2
using `forEach`:
```
import java.util.Arrays;
In this example:
- `Arrays.stream(numbers)` converts the array `numbers` into a stream, which allows us to
use the `forEach` method.
- `forEach(num -> System.out.println(num * 2))` iterates over each element in the stream
(each number in the array) and prints it multiplied by 2.
So, the `forEach` method simplifies iterating over elements in a collection and performing
actions on each element, making your code more concise and readable.
The `forEach` method in Java is used to iterate over elements in a collection, such as an array
or a list. It's part of the Stream API introduced in Java 8 and provides a more concise way to
perform operations on each element of the collection.
1. **Usage**: You typically use `forEach` with streams to apply an action to each element in
the collection.
2. **Lambda Expression**: The `forEach` method takes a lambda expression as its argument.
This lambda expression defines the action to be performed on each element. It's like a short,
inline function that specifies what to do with each element.
Here, `collection` is the name of your array or list, `element` represents each item in the
collection, and `action` is what you want to do with each element.
4. **Example Action**: The `action` part of the lambda expression can be anything you want
to do with each element, such as printing it, modifying it, or performing some calculation based
on it.
5. **No Return Value**: Unlike some other methods in Java, `forEach` doesn't return anything.
It's just a way to perform a specified action on each element without needing a loop.
6. **Conciseness**: One of the benefits of using `forEach` is that it makes your code more
concise and expressive. Instead of writing a loop to iterate over elements, you can use `forEach`
to achieve the same result in a more streamlined manner.
Overall, the `forEach` method is handy for applying a specific action to each element in a
collection, making your code cleaner and easier to read.
Try-with- resources:-
Try-with-resources is a feature in Java that helps manage resources like file streams or database
connections. When you use try-with-resources, you don't need to manually close these
resources. Here's a detailed explanation:
2. **Syntax:** The syntax for try-with-resources is simple. You start with the `try` keyword,
followed by opening parentheses where you declare and initialize your resources. After that,
you write your code block within curly braces `{}` as usual.
3. **Resource Declaration:** Inside the parentheses after `try`, you declare and initialize your
resources. For example, if you're working with a file, you would declare a `FileInputStream`
or `FileOutputStream` within these parentheses.
4. **Automatic Closing:** When the code inside the try block finishes executing, Java
automatically closes the declared resources. This closing happens in reverse order of
declaration, meaning resources are closed from last declared to first.
5. **Exception Handling:** If an exception occurs within the try block, Java automatically
handles the closing of resources. It first executes any `catch` blocks for exception handling and
then closes the resources in reverse order of declaration.
6. **No Explicit Closing Code:** One of the main benefits of try-with-resources is that it
reduces the chance of resource leaks by ensuring resources are closed properly. You don't need
to write explicit closing code, which can be error-prone if not done correctly.
```
FileReader fileReader = null;
try {
fileReader = new FileReader("example.txt");
// Read file contents
} catch (IOException e) {
// Handle exception
} finally {
if (fileReader != null) {
try {
fileReader.close();
} catch (IOException e) {
// Handle close exception
}
}
}
```
2. **Try-with-resources Approach:**
Try-with-resources simplifies this process by automatically closing resources when they are
no longer needed, even if an exception occurs.
```
try (FileReader fileReader = new FileReader("example.txt")) {
// Read file contents
} catch (IOException e) {
// Handle exception
}
```
In this code:
- The `FileReader` is declared and initialized within the `try` block.
- After the `try` block finishes (either normally or due to an exception), the `FileReader` is
automatically closed.
- You don't need a `finally` block to explicitly close the `FileReader`.
The Try-with-resources approach is not only cleaner and less error-prone but also ensures that
resources are properly released, improving the overall reliability of your code.
2. **How It Works:** When you use Try-with-resources, Java automatically takes care of
closing the resource for you after the try block ends, whether it ends normally or due to an
exception.
3. **Automatic Resource Management:** This feature ensures that resources are closed
properly, even if an exception occurs during their use. It's like having a built-in cleanup
mechanism.
**Example:**
Let's say you have a file that you want to read using Try-with-resources:
```
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
In this example:
- We create a `BufferedReader` inside the Try-with-resources block, which reads lines from a
file.
- After the try block ends, Java automatically closes the `BufferedReader`, whether the reading
operation was successful or an exception occurred.
This ensures that we don't have to worry about explicitly closing the `BufferedReader` or
handling any potential exceptions related to resource cleanup.
Type Annotations:-
What are Type Annotations?
Type Annotations are annotations applied to types in Java, such as classes, interfaces, methods,
and fields. They are used to provide information about the types, such as their intended use,
restrictions, or instructions for the compiler or runtime environment. They help the compiler
and other tools understand the intended use of these elements.
1. **@Override:** This annotation is used when you override a method from a superclass in
a subclass. It tells the compiler that you intend to override a method and helps catch errors if
the method signature in the superclass changes.
2. **@Deprecated:** When you mark something as deprecated, you're telling developers that
it's no longer recommended for use. This helps in maintaining code by providing a warning
when deprecated elements are used.
3. **@SuppressWarnings:** Sometimes, you might get warnings from the compiler that you
know are safe to ignore. This annotation allows you to suppress specific warnings, ensuring
cleaner compilation output.
4. **@FunctionalInterface:** This annotation is used for interfaces that have exactly one
abstract method, indicating that they are meant to be used as functional interfaces for lambda
expressions or method references.
5. **@SafeVarargs:** When you use varargs (variable arguments) in a method, this annotation
assures that the method doesn't perform unsafe operations on the varargs parameter, ensuring
type safety.
6. **@Nullable and @NonNull:** These annotations are used for parameters, fields, or return
types to indicate whether they can be null (`@Nullable`) or must not be null (`@NonNull`).
They help catch potential NullPointerExceptions during development.
7. **@Documented:** This annotation is used to indicate that elements with this annotation
should be included in the generated documentation. It's helpful for documenting APIs and
libraries.
Overall, type annotations in Java provide metadata about your code, making it more
understandable, maintainable, and less error-prone.
```
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@interface NonNull {}
class Example {
void process(@NonNull String data) {
// Process the non-null data
}
}
```
In this example:
- `@NonNull` is a custom annotation that we've defined with `@interface`.
- `@Retention(RetentionPolicy.RUNTIME)` specifies that this annotation should be retained
at runtime, allowing runtime processing.
- `@Target(ElementType.PARAMETER)` indicates that this annotation can be applied to
method parameters.
The `process` method in the `Example` class is annotated with `@NonNull`, indicating that the
`data` parameter should not be null when the method is called.
### Summary
Type Annotations in Java are powerful tools for providing metadata and instructions about
types in your code. They can be used to enforce constraints, guide compiler behavior, and
enhance documentation, contributing to more robust and understandable code.
Repeating Annotations:-
Repeating annotations in Java are a feature that allows you to use multiple annotations of the
same type on a declaration. This can be particularly useful when you want to apply the same
annotation multiple times without having to repeat the annotation type.
In essence, repeating annotations provide a more flexible and concise way to apply multiple
instances of the same annotation, enhancing the expressiveness and organization of your code.
Imagine you have an annotation `@Author` that you want to use to specify the authors of a
book, and you want to allow multiple authors for a single book. Here's how you can use
repeating annotations:
```
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Authors.class)
@interface Author {
String name();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Authors {
Author[] value();
}
@Authors({
@Author(name = "John Doe"),
@Author(name = "Jane Smith")
})
public class Book {
// Book details and methods
}
```
In this example:
- We define the `@Author` annotation with a single attribute `name` to represent the author's
name.
- We also define a container annotation `@Authors` that holds an array of `Author` annotations.
This is where the repeating aspect comes into play.
- We apply the `@Authors` annotation to the `Book` class, specifying multiple authors using
repeating `@Author` annotations inside `@Authors`.
Using repeating annotations simplifies the syntax and makes the code more readable, especially
when dealing with multiple instances of the same annotation on a single element.
1. **Modules:** Modules are like mini-projects within your Java application. They
encapsulate related code and resources, making it easier to maintain and reuse them across
different projects.
3. **Encapsulation:** Modules enforce strong encapsulation. This means that only the code
explicitly exported by a module can be accessed by other modules. Everything else remains
hidden, reducing the risk of accidental misuse or interference.
5. **Module Path:** When compiling and running your Java application, you specify a module
path that tells Java where to find modules. This allows Java to resolve dependencies and load
modules correctly.
6. **Benefits:** The Java Module System promotes modularity, which leads to better code
organization, easier maintenance, and improved code reuse. It also helps in creating more
scalable and flexible applications.
Overall, the Java Module System enhances the way Java applications are structured and
maintained, making them more modular, scalable, and easier to manage in larger projects.
### Explanation:
1. **Modules:** Think of modules as containers for your Java code. They help in organizing
your codebase into logical units, making it easier to manage dependencies and improve code
readability.
3. **Exports and Requires:** Inside the `module-info.java` file, you use keywords like
`exports` to specify which packages within your module are accessible to other modules, and
`requires` to declare dependencies on other modules.
4. **Encapsulation:** One of the key benefits of modules is encapsulation. Modules can hide
their internal implementation details, exposing only what's necessary for other modules to use.
This helps in reducing dependencies and improving security.
### Example:
2. **Define `module-info.java`:**
```
module module.example {
exports com.example; // Expose the com.example package
}
```
3. **Create `MyClass.java`:**
```
package com.example;
public class MyClass {
public void sayHello() {
System.out.println("Hello from MyClass!");
}
}
```
In this example:
- We created a module named `module.example`.
- Inside the module, we have a package `com.example` containing a class `MyClass`.
- The `module-info.java` file specifies that the `com.example` package is accessible outside the
module.
- We compiled the module and ran the `MyClass` program using the module system.
This is a basic example, but it showcases how modules help in structuring Java projects and
managing dependencies.
An Inner Anonymous Class, on the other hand, is a class defined within another class without
giving it a name. It's typically used for one-time use and can access the enclosing class's
members.
When you combine Diamond Syntax with an Inner Anonymous Class, you can create instances
of generic classes without specifying the generic type arguments explicitly. This is useful when
the types can be inferred from the context, making your code cleaner and easier to read.
For example, if you have a generic class like `List<String>` and you want to create an instance
using an Inner Anonymous Class, you can use the Diamond Syntax like this:
```
List<String> myList = new ArrayList
<>();
```
Here, the `<>` (diamond operator) is used to indicate that the compiler should infer the type
arguments based on the context, which in this case is `String` because we're assigning the
instance to a `List<String>` variable.
Similarly, if you have a generic class like `Map<Integer, String>` and you want to create an
instance using an Inner Anonymous Class, you can use the Diamond Syntax like this:
```
Map<Integer, String> myMap = new HashMap<>();
```
Again, the compiler infers the type arguments (`Integer` and `String`) based on the context of
the assignment.
In summary, Diamond Syntax with Inner Anonymous Classes in Java allows you to create
instances of generic classes without explicitly specifying the type arguments, making your code
more concise and readable when the types can be inferred from the context.
### Explanation:
Now, let's see how Diamond Syntax with Inner Anonymous Class works with an example:
```java
import java.util.ArrayList;
import java.util.List;
In this example:
- We use Diamond Syntax (`ArrayList<>`) to create a list of integers without specifying the
type again on the right-hand side of the assignment.
- An inner anonymous class `MyConsumer` is defined within the `Main` class to implement
the `Consumer` interface for iterating and printing elements of the list using the `forEach`
method.
This combination of Diamond Syntax with Inner Anonymous Class helps in writing concise
and readable code when working with generics and functional interfaces in Java.
When you use `var`, Java looks at the right side of the assignment to determine the type. This
can make your code more concise and easier to read, especially when the data type is obvious
from the context.
However, it's important to note that this feature doesn't change Java's strong typing nature. The
type is still known at compile time, so you get the benefits of type safety and error checking
during compilation.
Local Variable Type Inference in Java is a feature that allows you to declare variables without
explicitly mentioning their data types. Instead, Java infers the type of the variable based on the
value assigned to it. This feature was introduced in Java 10 to make code more concise and
readable.
### Explanation:
In traditional Java programming, when you declare a variable, you explicitly specify its data
type. For example:
```
String message = "Hello, Java!";
int number = 10;
```
In the above code, `String` and `int` are explicit data type declarations.
With Local Variable Type Inference, you can omit the explicit data type declaration and let Java
infer it based on the assigned value. You use the `var` keyword for this purpose. For example:
```
var message = "Hello, Java!";
var number = 10;
```
In this code, `var` is used instead of `String` and `int`, allowing Java to infer the data types.
### Example:
Let's consider an example where Local Variable Type Inference can be helpful. Suppose you
have a list of names that you want to iterate over using a for-each loop:
```
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
### Benefits:
- **Conciseness:** Reduces boilerplate code by omitting explicit type declarations.
- **Readability:** Focuses on the variable's purpose rather than its data type.
Overall, Local Variable Type Inference streamlines code writing in Java, making it more
expressive and concise.
Switch Expressions:-
Switch Expressions in Java are a way to streamline the traditional switch statement. With
switch expressions, you can assign a value directly to a variable based on different cases,
similar to how you would with if-else statements.
Here's how it works:
1. **Syntax**: In a switch expression, you use the `switch` keyword followed by a value or
expression in parentheses. Inside curly braces `{}`, you list the cases using `case` labels. Each
case ends with a `break` statement or a `yield` statement for returning a value.
2. **Yield**: The `yield` statement is what makes switch expressions powerful. It allows you
to return a value from a case directly, without needing a separate variable assignment. This
makes your code more concise and readable.
3. **Arrow (`->`) Syntax**: Instead of using `break`, you use the arrow `->` to indicate the
value to be returned for a specific case. For example, `case 1 -> "One";` means if the value is
`1`, return `"One"`.
4. **Default Case**: You can also include a default case using `default`, which handles cases
where none of the specified cases match the given value.
5. **Examples of Use**: Switch expressions are handy for situations where you have multiple
conditions and want to assign a value based on those conditions. For instance, you can use them
in calculating grades based on scores or determining actions based on user inputs.
Switch expressions in Java are a way to simplify and enhance traditional switch statements.
They allow you to assign a value based on the result of evaluating different cases. Here's a
detailed explanation in easy language along with an example:
1. **Syntax**:
```
switch (expression) {
case value1 -> result1;
case value2 -> result2;
// more cases as needed
default -> defaultResult;
}
```
2. **Key Points**:
- The `expression` is evaluated once, and its result is compared against each `case` value.
- The arrow (`->`) separates the case value from the result.
- The `default` case is optional and provides a default value if none of the cases match.
3. **Features**:
- **Yield**: Switch expressions can be used as an expression, allowing you to directly assign
their result to a variable or use it in a calculation.
- **Pattern Matching**: You can use more complex patterns in switch expressions, making
them versatile for matching various data structures.
4. **Benefits**:
- **Conciseness**: Switch expressions reduce boilerplate code compared to traditional
switch statements.
- **Readability**: They make code more readable, especially when handling multiple cases
with different results.
### Example:
Let's say you have a program that assigns a grade based on a student's score:
```
int score = 85;
String grade = switch (score / 10) {
case 10, 9 -> "A"; // For scores 90-100, assign grade A
case 8 -> "B"; // For scores 80-89, assign grade B
case 7 -> "C"; // For scores 70-79, assign grade C
case 6 -> "D"; // For scores 60-69, assign grade D
default -> "F"; // For scores below 60, assign grade F
};
In this example:
- The `score / 10` calculates the grade level based on dividing the score by 10.
- Each `case` represents a range of scores and assigns the corresponding grade.
- The `default` case handles scores that don't fall into any specified range.
When you run this code with `score = 85`, it will output:
```
Grade: B
```
This switch expression efficiently determines the grade based on the student's score, making
the code concise and easy to understand.
Yield Keyword:-
In Java, the `yield` keyword is used in the context of concurrency and multithreading. It's
related to the concept of cooperative multitasking, where different tasks or threads cooperate
to share resources and execute efficiently. Here's a detailed explanation of the `yield` keyword:
2. **Using `yield`:** When a thread calls `yield`, it essentially pauses its execution and gives
a hint to the scheduler that it's willing to yield its current execution time. The scheduler can
then choose to switch to another thread if it's ready to run.
3. **Context Switching:** When a thread yields, the scheduler performs a context switch,
which involves saving the current thread's state (like program counter, stack pointer, etc.) and
loading the state of the next thread to run.
5. **Voluntary Action:** It's important to note that `yield` is a voluntary action taken by a
thread. It's not a forced interruption like with thread interruption mechanisms (`interrupt`
method). Threads use `yield` to cooperate with each other, but it's ultimately up to the scheduler
to decide when and which thread gets to run.
6. **Use Cases:** `yield` is often used in scenarios where threads perform non-critical or non-
time-sensitive tasks. For example, in a simulation program, background threads might yield to
a main rendering thread to ensure smooth graphics display.
In Java, the `yield` keyword is used in the context of concurrent programming and is related to
thread scheduling. Here's a detailed explanation in easy language with an example:
**Explanation:**
The `yield` keyword in Java is used to give a hint to the scheduler that the current thread is
willing to pause its execution temporarily to allow other threads to run. It's like saying, "I'm
okay with taking a break so others can do their work."
When a thread calls `yield`, it doesn't guarantee that it will immediately pause or switch to
another thread. It's more of a suggestion to the scheduler, which decides how to handle thread
execution based on various factors.
**Example:**
Let's say we have two threads, Thread A and Thread B, and we want them to alternate their
execution using the `yield` keyword.
```
public class YieldExample {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread A - Count: " + i);
Thread.yield(); // Yield to allow other threads
}
});
threadA.start();
threadB.start();
}
}
```
In this example, Thread A and Thread B both have a loop that counts from 1 to 5. After printing
each count, they call `Thread.yield()` to suggest to the scheduler that they are willing to yield
to other threads.
When you run this program, you may see output like:
```
Thread A - Count: 1
Thread B - Count: 1
Thread A - Count: 2
Thread B - Count: 2
Thread A - Count: 3
Thread B - Count: 3
Thread A - Count: 4
Thread B - Count: 4
Thread A - Count: 5
Thread B - Count: 5
```
The exact behavior may vary depending on the scheduler and system conditions, but the `yield`
keyword allows threads to cooperate and share resources more efficiently in a multi-threaded
environment.
Text Blocks:-
Text Blocks, introduced in Java 13, are a convenient way to write multiline strings in Java
without the need for escaping special characters like newline (\n) or quotes. They are
particularly useful for writing SQL queries, JSON data, or any text that spans multiple lines.
1. **Multiline Text:** Text Blocks allow you to write multiline strings without using escape
sequences like \n for newlines. This makes your code more readable and reduces errors caused
by missing or incorrect escape characters.
2. **No String Concatenation:** With Text Blocks, you don't need to use string concatenation
to create multiline strings. You can simply write the entire text as-is within the Text Block.
3. **Whitespace Preservation:** Text Blocks preserve the whitespace, including leading
spaces and line breaks, exactly as you write them. This is helpful for maintaining the formatting
of your text, especially in cases like SQL queries or JSON data where indentation is important.
4. **Escape Sequences:** While Text Blocks minimize the need for escape sequences within
the text, you can still use them if necessary. For example, you can include escape sequences
like \t for tabs or \" for double quotes within a Text Block.
5. **Block Delimiters:** Text Blocks are delimited by triple double quotes ("""), which
indicate the start and end of the block. These delimiters must appear on their own lines, and
any whitespace before or after them is ignored.
6. **Concatenation with String Methods:** You can concatenate Text Blocks with regular
strings or other Text Blocks using string concatenation methods like `+` or `concat()`. This
allows you to combine dynamic content with static text defined in Text Blocks.
Text Blocks, introduced in Java 13, are a convenient way to write multi-line strings in Java
without the need for escape characters like `\n` for line breaks. Here's a detailed explanation
with an example:
In Java, when you need to write a long string that spans multiple lines, you typically have to
concatenate multiple strings using the `+` operator or use escape characters like `\n` for new
lines. This can make the code less readable and more cumbersome to maintain, especially for
large blocks of text or HTML/XML content.
Text Blocks solve this problem by allowing you to write multi-line strings in a more natural
and readable way. Here's how you can use Text Blocks in Java:
```
public class TextBlocksExample {
public static void main(String[] args) {
String htmlContent = """
<html>
<head>
<title>Hello, Text Blocks!</title>
</head>
<body>
<h1>Welcome to Text Blocks</h1>
<p>This is a demonstration of Text Blocks in Java.</p>
</body>
</html>
""";
System.out.println(htmlContent);
}
}
```
In this example, we have a Java class `TextBlocksExample` with a `main` method. Inside the
`main` method, we define a multi-line string `htmlContent` using Text Blocks. The content of
the HTML document spans multiple lines, and we use triple double-quotes (`"""`) to enclose
the text block.
With Text Blocks, you don't need to worry about escaping new lines or special characters within
the string. The text block starts after the opening `"""` and ends before the closing `"""`. This
makes it much easier to write and maintain long strings, especially when dealing with complex
text formats like HTML, SQL queries, or JSON.
Text Blocks also preserve the formatting of the text, including leading white spaces and line
breaks, making your code more readable and maintainable.
Overall, Text Blocks in Java provide a cleaner and more natural way to work with multi-line
strings, improving code readability and reducing the need for cumbersome escape characters.
Records:-
Records in Java are a special type of class introduced in Java 14 to simplify the creation of
classes that primarily store data. They are similar to traditional classes but come with built-in
features that reduce boilerplate code.
2. **Immutable by Default:**
- Records are immutable by default, meaning once you create a record object, you cannot
modify its component fields directly. This immutability ensures data integrity and reduces the
chances of accidental modifications.
3. **Component Fields:**
- Records have component fields that store data. These fields are specified when defining a
record and represent the different attributes or properties of the record.
4. **Constructor:**
- Records automatically generate a constructor that initializes all component fields. This
constructor allows you to create record objects with specified values for each field.
6. **ToString Method:**
- Records automatically generate a `toString()` method that returns a string representation of
the record, including the values of its component fields. This makes it easy to display record
information in a human-readable format.
7. **Compact Syntax:**
- Records use a compact syntax for defining the record class, making the code more concise
and readable. The syntax includes specifying the record keyword followed by the class name
and component fields.
**Explanation:**
1. **Definition:** A record in Java is a special kind of class that is primarily used to store data.
It automatically generates methods like `equals()`, `hashCode()`, and `toString()` based on its
fields.
2. **Fields:** Records have fields, which are like variables that hold data. Each field in a
record represents a piece of information about an object of that record type.
3. **Immutable:** By default, records are immutable, which means their fields cannot be
changed once the record is created. This helps maintain data integrity.
5. **Simplified Syntax:** Records have a simplified syntax for defining their structure,
making it easier and quicker to create classes that hold data.
**Example:**
Suppose we want to create a record to represent a student with fields for their name and age.
Here's how we would define and use a record in Java:
```
// Define a record named Student
record Student(String name, int age) {
// No need for explicit constructor or methods
}
In this example, we define a record `Student` with fields for `name` and `age`. We then create
two `Student` objects using the record syntax and demonstrate accessing their fields and using
the automatically generated `toString()` method.
Records are especially useful when dealing with data-centric classes where the primary purpose
is to store and manipulate data. They reduce boilerplate code and make the code more concise
and readable.
Sealed Classes:-
In Java, sealed classes are used to restrict the inheritance hierarchy of classes. When a class is
sealed, it means that other classes can only extend it if they are explicitly listed as permitted
subclasses. This restriction helps in creating a more controlled and predictable class hierarchy.
1. **Restricting Inheritance:** Sealed classes restrict which classes can extend them. This is
done by explicitly specifying which classes are allowed to be subclasses.
2. **Permitted Subclasses:** Sealed classes define a list of permitted subclasses using the
`permits` keyword. Only classes listed in this permits list can extend the sealed class.
3. **Enhanced Type Safety:** By limiting the inheritance hierarchy, sealed classes enhance
type safety in your code. This means that you have more control over how subclasses are
structured and used.
4. **Preventing Unwanted Subclasses:** Sealed classes help prevent the creation of unwanted
subclasses that might break the design or behavior of the sealed class.
5. **Maintaining Class Structure:** Sealed classes are useful for maintaining a clear and well-
structured class hierarchy. They can be particularly beneficial in large projects where class
relationships need to be carefully managed.
Sealed classes in Java are a way to restrict the inheritance hierarchy for classes. When a class
is marked as sealed, it means that other classes can only extend it if they are listed as permitted
subclasses. This restriction helps in maintaining code integrity and preventing unexpected
subclassing.
2. **Permitted Subclasses**:
In the above example, `Circle`, `Rectangle`, and `Triangle` are permitted subclasses of the
`Shape` class. This means that no other classes can directly extend `Shape` unless they are
explicitly listed as permitted subclasses.
3. **Restricting Subclassing**:
If another class tries to extend `Shape` without being listed as a permitted subclass, it will
result in a compilation error. This ensures that the inheritance hierarchy remains controlled and
prevents unintended subclassing.
4. **Example**:
Let's consider an example with the `Shape` class and its permitted subclasses:
```
public sealed class Shape permits Circle, Rectangle, Triangle {
// class definition
}
In this example, `Circle`, `Rectangle`, and `Triangle` are permitted to extend `Shape`, but
`Square` is not, leading to a compilation error if you try to extend `Shape` with `Square`.
By using sealed classes, you can control and manage the inheritance hierarchy more effectively,
ensuring that only specified subclasses can extend a sealed class.