By Enabling Functional Programming concepts java programmer can write
concise code. Lambda Expression is used to enable functional
programming.
Lambda Expression
It is an anonymous(nameless) function.
Without Return type.
Without modifiers.
Means this is a function having no name, no return type and no modifier.
Normal function :Exp-1
public void m1(){
System.out.println(“Hellooooo”);
}
Lambda Expression for same above function:
()->System.out.println(“Hellooooo”);
Exp-2:
Normal Function:
public void m1(int a, int b)
{
System.out.println(a+b);
}
Lambda Function:
(a,b)->System.out.println(a+b);
Exp-3:
Normal Function:
public int squareIt(int n){
return n*n;
}
Lambda Function:
n->n*n;
Exp-4:
Normal Function:
public void m1(String s){
return s.length(); }
Lambda Function:
s->s.length();
Functional Interface(FI)
To call a lambda expression we use Functional Interface.
A Functional Interface contains exactly Single Abstract Method(SAM).Functional
Interface may contains any number of default and static method but should contain
exactly one abstract method.
Runnable= run()
Comparable compareTo()
Comparator compare()
ActionListener actionPerformed()
Callable call()
Example
Exp-1:
interface Interf
{
public void m1();
}
Exp-2:
@FunctionalInterface
interface Interf
{
public void m1();}
default void m2(){}
public static void m3(){}
}
Example
Exp-3
@FunctionalInterface
interface A
{
public void m1();
}
@FunctionalInterface
interface B extends A
{}
Exp-4:
@FunctionalInterface
interface A
{
public void m1();}
@FunctionalInterface
interface B extends A
{
public void m2(); // Will Give Error
}
Example
Exp-5:
@FunctionalInterface
interface A
{
public void m1();}
interface B extends A
{
public void m2();
}
Calling Lambda Expression
To implement the abstract method of interface we use a separate class.
To implement this Functional interface we can use Lambda Expression also and in
that case the separate implementation class is not required.
As we know for below method
public void m1()
{
System.out.println(“Hello”);
}
Lambda expression for above method is ()->System.out.println(“Hello”);
So, in place of providing separate implementation class we can write
Interf i= ()->System.out.println(“Hello”);
So, now the new Code is :
Exp-1: Calling method of an FI
interface Interf
{
public void m1();
}
class Demo implements Interf
{
public void m1()
{
System.out.println(“Hello”);
}}
class Test
{
public static void main(String args[])
{
Demo d=new Demo();
//Interf i=new Demo();
d.m1();
//i.demo();}
Using Same Code with Lambda
Expression
interface Interf
{
public void m1();
}
class Test
{
public static void main(String args[])
{
Interf i= ()->System.out.println(“Hello”);
i.m1();
}
Exp-2: Calling method of an interface
interface Interf
{
public void add(int a,int b);
}
class Demo implements Interf
{
public void add(int a,int b)
{
System.out.println(“Sum=“+(a+b));
}}
class Test
{
public static void main(String args[])
{
Demo d=new Demo();
//Interf i=new Demo();
d.add(5,10);
//i.add(20,30);}
Using Same Code with Lambda
Expression
interface Interf
{
public void add(int a, int b);
}
class Test
{
public static void main(String args[])
{
Interf i= (a,b)->System.out.println(“Sum=”+(a+b));
i.add(10,20);
}
Exp-3: Calling method of an interface
interface Interf
{
public int squareIt(int n);
}
class Demo implements Interf
{
public int squareIt(int n)
{
System.out.println(“Square=“+(n*n));
}}
class Test
{
public static void main(String args[])
{
Demo d=new Demo();
//Interf i=new Demo();
d.squareIt(5);
//i.add(20,30);}
Using Same Code with Lambda
Expression
interface Interf
{
public int squareIt(int n);
}
class Test
{
public static void main(String args[])
{
Interf i= n->System.out.println(“Square=”+(n*n));
i.squareIt(5);
}
Multithreading by Runnable
interface(Normal)
class MyRunnable implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(“Child Thread”);
}}}
class Test{
public static void main(String args[]){
MyRunnable r=new MyRunnable();
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread”);
}}}
Multithreading by Runnable interface(Lambda)
class Test{
public static void main(String args[]){
Runnable r=()->{
for(int i=0;i<10;i++){
System.out.println(“Child Thread 1”);
}
};
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread 1”);
}}}
// Define a functional interface with a single abstract method
@FunctionalInterface
interface ArithmeticOperation {
int operate(int a, int b);
}
public class Main {
public static void main(String[] args) {
// Implement the interface using a lambda expression for addition
ArithmeticOperation add = (a, b) -> a + b;
// Implement the interface using a lambda expression for subtraction
ArithmeticOperation subtract = (a, b) -> a - b;
// Implement the interface using a lambda expression for multiplication
ArithmeticOperation multiply = (a, b) -> a * b;
// Implement the interface using a lambda expression for division
ArithmeticOperation divide = (a, b) -> {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
};
// Using the implemented lambda expressions
System.out.println("Addition: " + add.operate(5,3));
System.out.println("Subtraction: " + subtract.operate(5, 3));
System.out.println("Multiplication: " + multiply.operate(5, 3));
System.out.println("Division: " + divide.operate(5, 3));
}
}
ArrayList Example Without Lambda Expression
import java.util.*;
class MyComparator implements Comparator<integer>{
public int compare(Integer I1,Integer I2){
if(I1<I2){return -1;}
else if((I1>I2){return +1;}
else {return 0;}
//return (I1<I2)?-1 : (I1>I2)?1:0;
//Lambda Expression (I1,I2)->(I1<I2)?-1 : (I1>I2)?1:0;}}
class Test{
public static void main(String args[]){
ArrayList<Integer> l=new ArrayList<Integer>();
l.add(20);l.add(10); l.add(25);l.add(5); l.add(30);l.add(0); l.add(15);
Sop(l);
Collections.sort(l,new MyComparator());
Sop(l);
}}
ArrayList Example With Lambda Expression
Import java.util.*;import java.util.stream.*;
class Test{
public static void main(String args[]){
ArrayList<Integer> l=new ArrayList<Integer>();
l.add(20);l.add(10); l.add(25);l.add(5); l.add(30);l.add(0); l.add(15);
Sop(l);
Comparator<Integer> c= (I1,I2)->(I1<I2)?-1 : (I1>I2)?1:0;
Collections.sort(l,c));
Sop(l);
l.stream().forEach(System.out::println();
List<Integer> l2=l.stream().filter(i->i%2==0).collect(Collectors.toList());
Sop(l2);
}}
Employee Example1 With Lambda Expression
Import java.util.*;import java.util.stream.*;
Class Employee{
String name;int eno;
Employee(String name,int eno){this.name=name;this.eno=eno;}
Public string toString(){
Return name +”:”+eno;}
class Test{
public static void main(String args[]){
ArrayList<Employee> l=new ArrayList<Employeer>();
l.add(new Employee(“Diksha”,8472425));l.add(new Employee(“Ashok”,21234));
l.add(new Employee(“Santosh”,12456));l.add(new Employee(“Zempa”,784523)); l.add(new
Employee(“Bunny”,25348));
Sop(l);
Collections.sort(l,(e1,e2)->(e1.eno<e2.eno)?-1 : (e1.eno>e2.eno)?1 : 0);
Sop(l);}}
Employee Example2 With Lambda Expression
Import java.util.*;import java.util.stream.*;
Class Employee{
String name;int eno;
Employee(String name,int eno){this.name=name;this.eno=eno;}
Public string toString(){
Return name +”:”+eno;}
class Test{
public static void main(String args[]){
ArrayList<Employee> l=new ArrayList<Employeer>();
l.add(new Employee(“Diksha”,8472425));l.add(new Employee(“Ashok”,21234));
l.add(new Employee(“Santosh”,12456));l.add(new Employee(“Zempa”,784523)); l.add(new
Employee(“Bunny”,25348));
Sop(l);
Collections.sort(l,(e1,e2)->(e1.name.compareTo(e2.name));
Sop(l);}}
Using Predicates
Import java.util.*;import java.util.function.*;
Class Employee{
String name;double salary;
Employee(String name,double salary){this.name=name;this.salary=salary;}
class Test{
public static void main(String args[]){
ArrayList<Employee> l=new ArrayList<Employeer>();
l.add(new Employee(“Diksha”,72425));l.add(new Employee(“Ashok”,21234));
l.add(new Employee(“Santosh”,12456));l.add(new Employee(“Zempa”,84523)); l.add(new
Employee(“Bunny”,25348));
Predicate<Employee> p=e->e.salary>30000;
for(Employee e1:l)
{
If(p.test(e1)
{
Sop(e1.name+”:”+e1.salary);
}
}
}}
Method Reference
► Method references are a feature of Java 8 that allows you to refer to
a method without having to call it. This can be useful in a number of
situations, such as when you need to pass a method as an argument
to another method. It is an alternative syntax for lambda expression.
It enhances the reusability of code.
A method reference is described using the :: (double colon) symbol. The general syntax is
one of the following forms:
► Reference to a static method: ContainingClass::staticMethodName
► Reference to an instance method of a particular object: instance::instanceMethodName
► Reference to a constructor: ClassName::new
Multithreading by Runnable interface(Lambda)
class Test{
public static void main(String args[]){
Runnable r=()->{
for(int i=0;i<10;i++){
System.out.println(“Child Thread 1”);
}
};
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread 1”);
}}}
Example-1
class Test{
public static void m1()
{
for(int i=0;i<10;i++){
System.out.println(“Child Thread 1”);
}}
public static void main(String args[]){
Runnable r=Test::m1;
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread 1”);
}}}
Note:FI method can be mapped to our specified method by :: operator. This is called Method
Reference. Above Runnable interface run method is referring Test class m1 method.
Example-2
class Test{
public void m1()
{
for(int i=0;i<10;i++){
System.out.println(“Child Thread 1”);
}}
public static void main(String args[]){
Test t1=new Test();
Runnable r=t1::m1;
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread 1”);
}}}
Example-3
Note: In method reference arguments must be same. Like m1() and run() both have same
arguments. Except argument we can change return type, modifier etc.
interface interf
{
public void add(int a,int b);
}}
class Test{
public static void sum(int x,int y)
{
System.out.println(“The Sum=“+(a+b));
}
public static void main(String[] args){
interf i=Test::sum;
i.add(100,200);
i.add(10,20);
}}
Example-4 Constructor Reference
Note:
class Sample{
Sample(){
System.out.println(“Sample class Constructor”);
}}
interface interf
{
public Sample get();
}
class Test{
public static void main(String[] args){
interf i=Sample::new;
Sample s1=i.get();
}}
Example-4 Constructor Reference with arguments
Note:
class Sample{
Sample(String s){
System.out.println(“Sample class Constructorwith arguments ”+s);
}}
interface interf
{
public Sample get(String s);
}
class Test{
public static void main(String[] args){
interf i=Sample::new;
Sample s1=i.get(“Akgec”);
}}
What is the Stream API?
► The Stream API is a framework introduced in Java 8 that enables us to process the
stream of objects. It express operations on collections (lists, arrays, etc.) as a series
of transformations. It's designed to make your code more concise, readable, and
potentially more efficient by leveraging parallelism.
Key Concepts
► Stream: A sequence of elements from a source (e.g., collection, array, file) that
supports aggregate operations. It doesn't store the elements themselves.
► Pipeline: A sequence of stream operations (intermediate and terminal) that are
chained together.
► Intermediate Operations: These operations (like filter, map, sorted) transform a
stream into another stream. They're lazily evaluated, meaning they don't execute
until a terminal operation is called.
► Terminal Operations: These operations (like forEach, collect, count) produce a
final result or side effect, consuming the stream in the process.
Creating Streams
► From Collections: List<String> names = ...; Stream<String> nameStream =
names.stream();
► From Arrays: int[] numbers = ...; IntStream numberStream =
Arrays.stream(numbers);
► From Values: Stream<String> stream = Stream.of("Java", "Python", "C++");
► From a Function: Stream.iterate(0, n -> n + 2).limit(10); (Creates a stream of
even numbers)
Stream API
► Stream operations are expressed in a declarative manner—they describe what
to do rather than how to do it.
► Key Features of Stream API
• Declarative Programming: Allows you to express operations on data
sequences clearly.
• Pipeline Processing: Streams can be pipelined to achieve greater efficiency.
Operations can be executed lazily to avoid processing all data until it is
necessary.
• Parallelism: Streams support parallel processing, which can lead to significant
performance improvements in multi-core environments.
Stream API Working
How does Stream Work Internally?
In streams,
❑ To filter out from the objects we do have a function named filter()
❑ To impose a condition we do have a logic of predicate which is nothing but a
functional interface. Here function interface can be replaced by a random
expression. Hence, we can directly impose the condition check-in our
predicate.
❑ To collect elements we will be using Collectors.toList() to collect all the
required elements.
❑ Lastly, we will store these elements in a List and display the outputs on the
console.
Java 8 Stream API // First lets print the collection
System.out.println("Printing the collection : "+ al);
// Java Program to illustrate FILTER
// & COLLECT Operations // Printing new line for better output
import java.io.*; readability
import java.util.*; System.out.println();
import java.util.stream.*;
public class GFG {
public static void main(String[] args) List<Integer> ls
{ = al.stream()
// Creating an ArrayList object of integer type .filter(i -> i % 2 == 0)
ArrayList<Integer> al = new .collect(Collectors.toList());
ArrayList<Integer>();
System.out.println(
// Inserting elements to ArrayList class object "Printing the List after stream operation : "+ ls);
al.add(2); }
al.add(6); }
al.add(9);
al.add(4);
al.add(20);
Streams
Various Core Operations Over Streams
There are broadly 3 types of operations that are carried over streams namely as
follows as depicted from the image shown above:
► Intermediate operations
► Terminal operations
► Short-circuit operations
Streams
1. Intermediate Operations:
Intermediate operations transform a stream into another stream. Some common
intermediate operations include:
filter(): Filters elements based on a specified condition.
map(): Transforms each element in a stream to another value.
sorted(): Sorts the elements of a stream.
2. Terminal Operations
Terminal Operations are the operations that on execution return a final result as
an absolute value.
collect(): It is used to return the result of the intermediate operations performed
on the stream.
forEach(): It iterates all the elements in a stream.
reduce(): It is used to reduce the elements of a stream to a single value.
3. Short Circuit Operations
Short-circuit operations provide performance benefits by avoiding unnecessary
computations when the desired result can be obtained early. They are
particularly useful when working with large or infinite streams.
anyMatch(): it checks the stream if it satisfies the given condition.
findFirst(): it checks the element that matches a given condition and stops
processing when it finds it.
Components of the Stream API
► The Stream API consists of a source, zero or more intermediate operations,
and a terminal operation:
► Source: Streams can be created from collections, arrays, files, and other data
sources.
► Intermediate Operations: Transform the stream into another one, such as filter,
map, and sorted.
► Terminal Operations: Produce a result or side-effect, such as forEach, collect, or
reduce.
Example: Using the Stream API
import java.util.*; public class Stream {
import java.util.stream.*; public static void main(String[] args) {
class Person { List<Person> people = Arrays.asList(
private String name; new Person("John", 30),
private int age; new Person("Jane", 25),
public Person(String name, int age) { new Person("Greg", 18),
this.name = name; new Person("Sara", 20),
this.age = age; new Person("Harold", 40)
} );
public String getName() {
return name;
public int getAge() {
return age;
}
► // Example 1: Filter and print names of people ► // Example 3: Check if there is anyone younger
older than 25 than 18
► System.out.println("People older than ► boolean anyYoungerThan18 =
25:"); people.stream()
► people.stream() ► .anyMatch(p ->
p.getAge() < 18);
► .filter(p -> p.getAge() > 25)
► System.out.println("Any person younger
► .forEach(p -> than 18: " + anyYoungerThan18);
System.out.println(p.getName()));
► // Example 4: Collect names into a list
► // Example 2: Calculate average age of all
people ► List<String> names = people.stream()
► double averageAge = people.stream() ► .map(Person::getName)
► ►
.mapToInt(Person::getAge) .collect(Collectors.toList());
► .average() ► System.out.println("Names: " + names);
► .getAsDouble();
► System.out.println("Average age: " +
averageAge);
► // Example 5: Get a list of all ages sorted ► People older than 25:
List<Integer> ages = people.stream() ► John
.map(Person::getAge) ► Harold
.sorted() ► Average age: 26.6
.collect(Collectors.toList()); ► Any person younger than 18:
false
System.out.println("Sorted ages: " + ages);
► Names: [John, Jane, Greg,
}
Sara, Harold]
}
► Sorted ages: [18, 20, 25, 30,
40]
Example
import java.util.*; List<Product> productsList = new ArrayList<Product>();
import java.util.stream.Collectors; //Adding Products
class Product{ productsList.add(new Product(1,"HP Laptop",25000f));
int id; productsList.add(new Product(2,"Dell Laptop",30000f));
String name; productsList.add(new Product(3,"Lenevo Laptop",28000f));
float price; productsList.add(new Product(4,"Sony Laptop",28000f));
public Product(int id, String name, float price) productsList.add(new Product(5,"Apple Laptop",90000f));
{
this.id = id; List<Float> productPriceList2 =productsList.stream()
this.name = name; .filter(p -> p.price > 30000)// filtering data
this.price = price; .map(p->p.price) // fetching price
} .collect(Collectors.toList()); // collecting as list
}
System.out.println(productPriceList2);
public class JavaStreamExample {
}
public static void main(String[] args) {
}
Output: [90000.0]
Base64 Encoding and Decoding
► Base64 is a binary-to-text encoding scheme that represents binary data in a string of
ASCII characters.
► It's commonly used for encoding email attachments, image data in web applications,
etc.
► Purpose: To represent binary data (images, audio, etc.) as a text string that can be
safely transmitted over channels designed for text (email, URLs).
► Algorithm:
► Grouping: The binary data is divided into 6-bit chunks.
► Mapping: Each 6-bit chunk is translated into a corresponding character from the Base64
alphabet (A-Z, a-z, 0-9, +, /).
► Padding: If the input length is not a multiple of 3 bytes, padding characters (=) are added
to ensure the encoded output length is a multiple of 4.
► Reversible: Decoding is the reverse process, converting the Base64 string back into
its original binary form.
Types of Java Base64 Encoding
• Simple Encoding
• URL and filename Encoding
• MIME Encoding
Simple Encoding
It uses the Base64 alphabet specified by Java in RFC 4648 and RFC 2045 for
encoding and decoding operations. The encoder does not add any line separator
character. The decoder rejects data that contains characters outside the base64
alphabet.
URL and Filename Encoding
It uses the Base64 alphabet specified by Java in RFC 4648 for encoding and
decoding operations. The encoder does not add any line separator character. The
decoder rejects data that contains characters outside the base64 alphabet.
MIME Encoding
It uses the Base64 alphabet as specified in RFC 2045 for encoding and decoding
operations. The encoded output must be represented in lines of no more than 76
characters each and uses a carriage return '\r' followed immediately by a linefeed
'\n' as the line separator. No line separator is added to the end of the encoded
output. All line separators or other characters not found in the base64 alphabet table
are ignored in decoding operation.
Let's encode the string "Man" using
Base64
1. Binary Representation: Convert each character into its ASCII binary value:
1. M: 01001101
2. a: 01100001
3. n: 01101110
2. Grouping: Group the bits into 6-bit chunks:
1. 010011 010110 000101 101110
3. Mapping: Find the corresponding Base64 character for each chunk:
1. 010011: T
2. 010110: W
3. 000101: F
4. 101110: u
4. Padding: Since we have a complete multiple of 3 bytes (no leftover bits), no padding is needed.
► Therefore, the Base64 encoded representation of "Man" is "TWFu".
To show the ASCII values corresponding to each
character in the string "Vikas", we can look up each
character in the ASCII table. Here are the ASCII
values
• 'V' = 86
for each character in "Vikas":
• 'i' = 105
• 'k' = 107
• 'a' = 97
• 's' = 115
► Each of these values represents the numerical representation of the character
in the ASCII encoding system, which is a standard for representing text in
computers and other electronic devices.
► To represent the characters in the string "Vikas" in binary form, you convert each ASCII value to
binary. Here are the binary representations corresponding to each ASCII value for "Vikas":
• 'V' = 86 in ASCII, which is 01010110 in binary
• 'i' = 105 in ASCII, which is 01101001 in binary
• 'k' = 107 in ASCII, which is 01101011 in binary
• 'a' = 97 in ASCII, which is 01100001 in binary
• 's' = 115 in ASCII, which is 01110011 in binary
► These binary sequences represent how each character is stored in memory in a typical computer
system using the ASCII encoding standard.
► To group the binary representations of the characters in "Vikas" into 6-bit
chunks, we first concatenate all the binary strings:
• 'V' = 01010110
• 'i' = 01101001
• 'k' = 01101011
• 'a' = 01100001
• 's' = 01110011
0101011001101001011010110110000101110011
► Now, let's break this into 6-bit chunks:010101 (V)100110 (i)100101 (k)101011 (k)011000
(a)010111 (s)0011 (remaining bits)
► We notice that there are 4 bits left after the final division into 6-bit chunks.
► To complete the last chunk in base64 encoding, these bits would typically be padded with two
zeros on the right side to make it a full 6-bit chunk, resulting in 001100.
► Thus, we end up with 6 full 6-bit chunks and no bits left after padding the final group.
► This code will take the Base64 encoded string "VmlrYXM=" and convert it back to the original
string "Vikas"
Purpose of Padding
► Data Integrity: Padding ensures that the data can be divided evenly according
to the encoding's requirements.
► In Base64 encoding, for instance, the input data needs to be split into 6-bit
chunks.
► If the total number of bits in the input isn't a multiple of 6, padding is added
to make up the difference.
► Uniform Length: Some encoding schemes, including Base64, require data
lengths that fit their encoding blocks perfectly.
► Padding helps achieve this by filling out the last block if necessary.
Base64 Padding
► Base64 specifics: Base64 encoding converts each set of three bytes into four
encoded characters, each representing 6 bits of data.
► If the input byte sequence does not divide evenly into sets of three bytes,
fewer than 24 bits are available for the final group.
► Padding mechanism: Base64 adds padding using the = character to account
for these missing bytes:
► If the final chunk has only 16 bits (two bytes), one = is added.
► If the final chunk has only 8 bits (one byte), two = characters are added.
► This padding ensures that when the data is decoded, the decoding process can
accurately reconstruct the original byte sequence, including the correct
number of bytes, by ignoring the extra padding bits.
► Original bit string without padding: 0011
► To make it a full 6-bit chunk, two padding bits (00) were added, making it 001100.
► If these bits were part of a Base64 encoding process, additional characters might also be added
to the encoded string itself as = to indicate padding, depending on the specifics of the final byte
grouping.
Encoding "Vikas" to Base64
► import java.util.Base64;
► public class Base64Example {
► public static void main(String[] args) {
► String originalInput = "Vikas";
► String encodedString =
Base64.getEncoder().encodeToString(originalInput.getBytes());
► System.out.println(encodedString);
► }
► }
Decoding from Base64 to "Vikas"
► import java.util.Base64;
► public class Base64Example {
► public static void main(String[] args) {
► String encodedString = "VmlrYXM=";
► byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
► String decodedString = new String(decodedBytes);
► System.out.println(decodedString);
► }
► }
Use Cases and Scenarios
► Email Attachments: Attachments are encoded in Base64 to ensure they can
be included directly in email messages without corruption.
► Data URLs (Data URIs): Images and other media can be embedded directly
into HTML or CSS using Base64-encoded data URLs. This reduces the number
of HTTP requests and can improve page load times.
► Web Tokens (e.g., JWT): JSON Web Tokens often use Base64 to encode the
header and payload for transmission.
► Basic Authentication: Passwords are encoded in Base64 along with usernames
when using Basic Authentication over HTTP.
► Considerations:
► Size Increase: Base64 encoding increases the size of data by roughly 33%.
► Security: Base64 is not encryption; anyone can easily decode it. Use
encryption for sensitive data.
import java.util.Base64;
public class Base64Example {
public static void main(String[] args) {
String text = "Hello, Base64!";
byte[] data = text.getBytes();
// Encoding
String encodedString = Base64.getEncoder().encodeToString(data);
System.out.println("Encoded: " + encodedString);
// Decoding
byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
String decodedString = new String(decodedBytes);
System.out.println("Decoded: " + decodedString);
}
}
► Import the Base64 Class:
► import java.util.Base64; brings in the necessary class from the Java standard library for Base64
operations.
► Prepare the Input Data:
► String text = "Hello, Base64!";: This is the original text we want to encode.
► byte[] data = text.getBytes();: The getBytes() method converts the text into an array of bytes, as Base64
operates on binary data.
► Encoding:
► Base64.getEncoder(): This gets a Base64 encoder object.
► .encodeToString(data): The encodeToString() method takes the byte array (data) and encodes it into a
Base64 string (encodedString).
► Decoding:
► Base64.getDecoder(): This gets a Base64 decoder object.
► .decode(encodedString): The decode() method takes the Base64-encoded string and decodes it back into
a byte array (decodedBytes).
► new String(decodedBytes): This creates a new String object from the decoded byte array, effectively
retrieving the original text.
► Output:
► The code then prints both the encoded string (encodedString) and the decoded string (decodedString) to
the console.
► Encoded: SGVsbG8sIEJhc2U2NCEx
► Decoded: Hello, Base64!
► Key Takeaways
► Encoding: Converts binary data into a Base64-encoded string (safe for text transmission).
► Decoding: Converts a Base64 string back into its original binary data.
► Java's Base64 Class: Provides convenient methods for both encoding and decoding.
► Use Cases: Email attachments, data URLs in web pages, web tokens, and Basic Authentication.
URL Encoding and Decoding
import java.util.Base64;
public class Base64BasicEncryptionExample {
public static void main(String[] args) {
// Getting encoder
Base64.Encoder encoder = Base64.getUrlEncoder();
// Encoding URL
String eStr = encoder.encodeToString("http://www.Mysite.com/index.php/".ge
tBytes()); System.out.println("Encoded URL: "+eStr);
// Getting decoder
Base64.Decoder decoder = Base64.getUrlDecoder();
// Decoding URl
String dStr = new String(decoder.decode(eStr));
System.out.println("Decoded URL: "+dStr);
}
} Encoded URL: aHR0cDovL3d3dy5qYXZhdHBvaW50LmNvbS9qYXZhLXR1dG9yaWFsLw==
MIME Encoding and Decoding
package Base64Encryption;
import java.util.Base64;
public class Base64BasicEncryptionExample {
public static void main(String[] args) {
// Getting MIME encoder
Base64.Encoder encoder = Base64.getMimeEncoder();
String message = "Hello, \nYou are informed regarding your inconsistency of work";
String eStr = encoder.encodeToString(message.getBytes());
System.out.println("Encoded MIME message: "+eStr);
// Getting MIME decoder
Base64.Decoder decoder = Base64.getMimeDecoder();
// Decoding MIME encoded message
String dStr = new String(decoder.decode(eStr));
System.out.println("Decoded message: "+dStr);
} }
Encoded MIME message: SGVsbG8sIApZb3UgYXJlIGluZm9ybWVkIHJlZ2FyZGluZyB5b3VyIGluY29uc2lzdGVuY3kgb2Yg d29yaw==
Decoded message: Hello, You are informed regarding your inconsistency of work
foreach method in java
► In Java, the forEach method is a way to iterate over elements in a collection
or a stream.
► It was introduced in Java 8 as a more concise and functional alternative to
traditional for loops.
► The forEach method takes a lambda expression or a method reference as an
argument.
► This lambda expression defines the action to be performed on each element
of the collection.
► import java.util.ArrayList; ► An ArrayList named fruits is created and
populated with some strings.
► import java.util.List;
► The forEach method is called on the fruits list.
► public class ForEach {
► Inside the forEach, a lambda expression fruit ->
► public static void main(String[] args) { System.out.println(fruit) is passed.
► List<String> fruits = new ArrayList<>(); ► This expression takes each element (fruit) from
► fruits.add("Apple"); the list and prints it to the console.
► fruits.add("Banana"); ► Alternatively, a method reference
System.out::println can be used to achieve the
► fruits.add("Orange"); same result.
► // Using lambda expression Output
► fruits.forEach(fruit -> ► Apple
System.out.println(fruit));
► Banana
► // Using method reference
► Orange
► fruits.forEach(System.out::println);
► Apple
► }
► Banana
► }
► Orange
► import java.util.Arrays; ► Number: 1
► import java.util.List; ► Number: 2
► import java.util.function.Consumer; ► Number: 3
► public class CustomForEach { ► Number: 4
► public static void main(String[] args) { ► Number: 5
► List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); ► Number (lambda): 1
► // Custom Consumer implementation ► Number (lambda): 2
► Consumer<Integer> action = new Consumer<Integer>() { ► Number (lambda): 3
► @Override ► Number (lambda): 4
► public void accept(Integer number) { ► Number (lambda): 5
► System.out.println("Number: " + number); ► The new Consumer<Integer>() { ... } syntax creates an
anonymous class that implements the Consumer interface.
► }
► The }; after the overridden accept method signifies the end of
► }; the anonymous class definition.
► // Using the custom action ► Anonymous Class: When you implement an interface using an
anonymous class, you need to define the methods within a
► numbers.forEach(action); block. The }; signifies the end of this block.
► // Using lambda expression ► Lambda Expression: A lambda expression provides a shorthand
► numbers.forEach(number -> System.out.println("Number for the same functionality without the need for explicit class
(lambda): " + number)); and method definitions, hence no }; is required.
► }
► }
Streams and forEach: The forEach method is also
commonly used with Java Streams to process collections
► import java.util.Arrays; ► Alice
► import java.util.List; ► In this example, the forEach method is used after
filtering the stream to print only the names that
► public class StreamForEach { start with "A".
► public static void main(String[] args) {
► List<String> names = Arrays.asList("Alice", ► The forEach method provides a convenient and
"Bob", "Charlie"); concise way to iterate over elements in a
collection and perform operations on them.
► // Using Stream and forEach
► names.stream()
► .filter(name -> name.startsWith("A"))
► .forEach(name ->
System.out.println("Filtered Name: " + name));
► }
► }
Try-with resources
► Try-with-resources, introduced in Java 7, is a powerful mechanism to automatically
manage and close resources like files, streams, or network connections.
► It simplifies resource management and helps prevent resource leaks, making your
code cleaner and safer.
► The try-with-resources allows you to automatically close resources (such as files,
streams, sockets, etc.) when they are no longer needed.
► This helps to avoid resource leaks by ensuring that each resource is closed at the
end of the statement.
► The try-with-resources statement ensures that each resource is closed at the end of
the statement.
► Any object that implements java.lang.AutoCloseable, which includes all objects that
implement java.io.Closeable, can be used as a resource.
► Here is the syntax of the try-with-resources statement:
► try (ResourceType resource = new ResourceType()) {
► // Use the resource
► } catch (ExceptionType e) {
► // Handle exception
► }
Example with File I/O
► try (BufferedReader reader = new BufferedReader(new
FileReader("file.txt"))) {
► String line; A BufferedReader is created to read from
► while ((line = reader.readLine()) != null) { "file.txt". This is the resource.
The code reads lines from the file and
► System.out.println(line); prints them.
► } When the try block ends (either after
reading the whole file or if an error
► } catch (IOException e) { occurs), the reader is automatically
► System.err.println("Error reading file: " + closed, ensuring the file is released.
e.getMessage());
► }
Example with multiple resources:
► try (FileInputStream in = new ► The resource classes used must
FileInputStream("input.txt"); implement the AutoCloseable
interface (or the older Closeable
► FileOutputStream out = new
interface).
FileOutputStream("output.txt")) {
► Most standard Java IO classes like
► // Code using both in and out
BufferedReader, BufferedWriter,
► } catch (IOException e) { FileInputStream, etc., already do.
► // Exception handling ► Exceptions thrown during resource
closing are "suppressed" if the main
► } try block also throws an exception.
► You can access suppressed
exceptions using
Throwable.getSuppressed().
Annotations in Java
► Annotations in Java are a form of syntactic metadata that can be added to Java
source code.
► They provide data about a program that is not part of the program itself.
► Annotations have no direct effect on the operation of the code they annotate,
but they can be used by tools such as compilers and IDEs to provide
information about the code.
► Annotations can be applied to classes, interfaces, methods, fields, and local
variables.
► Annotations can be used to provide information about a wide variety of things, such as:
• The author of a class or method
• The version of a class or method
• The parameters of a method
• The return type of a method
• Whether a method is deprecated
• Whether a method is thread-safe
• Whether a class is a singleton
Annotations are used to provide supplemental information about a program.
• Annotations start with ‘@’.
• Annotations do not change the action of a compiled program.
• Annotations help to associate metadata (information) to the program elements i.e.
instance variables, constructors, methods, classes, etc.
• Annotations are not pure comments as they can change the way a program is treated by
the compiler.
• Annotations basically are used to provide additional information, so could be an
alternative to XML and Java marker interfaces.
► The @Override annotation is used to indicate that a method overrides or replaces the behavior of an inherited
method.
► @SuppressWarnings indicates we want to ignore certain warnings from a part of the code.
► The @SafeVarargs annotation also acts on a type of warning related to using varargs.
► The @Deprecated annotation can be used to mark an API as not intended for use anymore.
► Single Abstract Method interfaces are a big part of this. If we intend a SAM interface to be used by lambdas,
we can optionally mark it as such with @FunctionalInterface
Annotation Syntax:
1.Declaration Syntax
@interface Annotation_Name
{
Data_type member_name()[default value];
}
2.Utilization Syntax
We can use annotation in any programming element like variables, methods, classes
@annotation_name(member1=value1,member2=value, …….)
Programming element
Type on Annotation on the basis of members available in annotation
1.Marker Annotation(Annotation without any member)
@Override
2.Single Valued Annotation(Contains only one members)
@SupressWarnings(“Unchecked”);
@SupressWarnings(value=“Unchecked”);
3. Multi Valued Annotation
@WebServlet(name=“MS”, urlPattern=“/abc/xyz”)
Hierarchy of Annotations in Java
Example: Using@Override
class Abc{
void show(){
System.out.println(“Hello Abc”);
}}
class Xyz extends Abc{
@Override
void show(){
System.out.println(“Hello Xyz”);
}
public static void main(String args[]){
Abc obj=new Xyz();
obj.show();
}
}
Example: Using @SuppressWarnings
import java.util.*;
class TestAnnotation2{
@SuppressWarnings("unchecked")
public static void main(String args[]){
ArrayList list=new ArrayList();
list.add("sonoo");
list.add("vimal");
list.add("ratan");
for(Object obj:list)
System.out.println(obj);
}}
Example: Using @Depricated
class A{
void m(){System.out.println("hello m");}
@Deprecated
void n(){System.out.println("hello n");}
}
class TestAnnotation3{
public static void main(String args[]){
A a=new A();
a.n();
}}
Meta-Annotations
Annotation about annotation (exp @Inherited)
By Default annotations are not inheritable from super class to subclass.
@Inherited
@interface Persistable
{
}
Meta-Annotations
@Percistable
Class Employee
{
String eid;
String ename;
}
Class Manager extends Employee
{
Void getManagerDetails(){
S.o.p(eid);
S.o.p(ename);
}}
@Documented : The purpose of this annotation is to make any annotation as a
documentable annotation as annotations are by default not documentable.
@Target: The purpose of this annotation is to define target elements to which we
wants to apply annotations. In java programming we can apply annotation for
variables, methods, packages, constructors, local variables and for another
annotation.
The purpose of Target annotation is to define to which set of programming element
we want to apply annotation.
Syntax:@Target(ElementTpe val)
Element Types Where the annotation can be applied
TYPE class, interface or enumeration
FIELD fields
METHOD methods
CONSTRUCTOR constructors
LOCAL_VARIABLE local variables
ANNOTATION_TYPE annotation type
PARAMETER parameter
Example
@Target(ElementType.TYPE, ElementType.FIELD, ElementType.METHOD)
@interface Persistable
{
}
@Retention annotation is used to specify to what level annotation will be
available
RetentionPolicy Availability
RetentionPolicy.SOURCE refers to the source code, discarded
during compilation. It will not be
@Retention(RetentionPolicy.SOURCE) available in the compiled class.
@interface Persistable
{ RetentionPolicy.CLASS refers to the .class file, available to
java compiler but not to JVM . It is
} included in the class file.
RetentionPolicy.RUNTIME refers to the runtime, available to java
compiler and JVM .
► @Retention
► Some annotations are meant to be used as hints for the compiler, while others are used at runtime.
► We use the @Retention annotation to say where in our program’s lifecycle our annotation applies.
► To do this, we need to configure @Retention with one of three retention policies:
1. RetentionPolicy.SOURCE: The annotation is retained only in the source code and discarded during
compilation.
2. RetentionPolicy.CLASS: The annotation is retained in the class file but not available at runtime.
3. RetentionPolicy.RUNTIME: The annotation is retained in the class file and available at runtime.
If no @Retention annotation is present on the annotation declaration, then the retention policy defaults
to RetentionPolicy.CLASS.
Custom Annotations
1. Define user defined annotation
2. Utilize the user defined annotation in java application
3. Access data from user defined annotation.
Define user defined annotation
import java.lang.annotation.*;
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Course
{
String cid() default “c-111”;
String cname() default “Java Programming”;
String ccost() default 10000;
}
Custom Annotations
1. Define user defined annotation
2. Utilize the user defined annotation in java application
3. Access data from user defined annotation.
Define user defined annotation
import java.lang.annotation.*;
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Course
{
String cid() default “c-111”;
String cname() default “Java Programming”;
String ccost() default 10000;
}
@Course(cid=“C-333”,cname=“C”,ccost=5000)
public class Student
{
String sid;
String sname;
String saddr;
public Student(String sid, String sname, String saddr
){
this.sid=sid;this.sname=sname;this.saddr=saddr;
}
Public void getStudentDetails()
{
S.o.p(“student Details”);
S.o.p(“-------------”);
S.o.p(“student Id ”+sid);
S.o.p(“student Name ”+sname);
}
}
import java.lang.annotation.*;
class Test{
public static void main(String[] args) throws Exception{
Student s=new Student(“S-111”,”Sam”,”Delhi”);
s.getStudentDetails();
Class c=s.getClass();
Annotation ann=c.getAnnotation(Course.class);
Course crs=(Course)Ann;
S.o.p(“student Course Details”);
S.o.p(“-------------”);
S.o.p(“Course Id ”+crs.cid);
S.o.p(“Course Name ”+crs.cname);
}
}
► @Inherited
► In some situations, we may need a subclass to have the annotations bound to a parent class.
► We can use the @Inherited annotation to make our annotation propagate from an annotated class to its
subclasses.
► @Documented
► By default, Java doesn’t document the usage of annotations in Javadocs.
► @Repeatable
► Sometimes it can be useful to specify the same annotation more than once on a given Java element.
► Before Java 7, we had to group annotations together into a single container annotation
4. Type Annotations
► Java 8 has included new features type annotations in its prior annotations topic.
► In early Java versions, you can apply annotations only to declarations.
► After releasing of Java SE 8 , annotations can be applied to any type use. It means that annotations can be
used anywhere you use a type.
► For example, if you want to avoid NullPointerException in your code, you can declare a string variable like
this:
@NonNull String str;
Note - Java created type annotations to support improved analysis of Java programs. It supports way of ensuring
stronger type checking.
Examples of type annotations:
► @NonNull List<String>
► List<@NonNull String> str
► Arrays<@NonNegative Integer> sort
► @Encrypted File file
► @Open Connection connection
► void divideInteger(int a, int b) throws @ZeroDivisor ArithmeticException
These annotations can be applied to any place where a type is being used. For example, we can annotate the return
type of a method. These are declared annotated with @Target annotation.
Syntax
► @Encrypted String data; // Annotating a variable type
► List<@NonNull String> strings; // Annotating a type parameter
► myGraph = (@Immutable Graph) tmpGraph; // Annotating a cast
Type annotations bring several
benefits
1. Stronger Type Checking: They enable more sophisticated static analysis tools
to verify code correctness and catch potential errors at compile time.
2. Improved Code Readability: Annotations can convey additional information
about the intended use of types, making code more self-explanatory.
3. Enhanced Tooling: Type annotations provide a foundation for building
powerful tools like linters and static analyzers that can help enforce coding
standards and detect bugs.
4. Customizable Analysis: You can create your own type annotations and
associated tools to perform specialized code analysis tailored to your specific
needs.
Common Use Cases
Nullability Annotations:
• To indicate whether a variable, return type, or parameter can be null.
• @Nullable String nullableString;
• @NonNull String nonNullString;
► Checker Framework:
• Used for various checks like nullness, immutability, and other custom checks.
► Example
► import org.checkerframework.checker.nullness.qual.NonNull;
► import org.checkerframework.checker.nullness.qual.Nullable;
► public class Example {
► @Nullable String mightBeNull() {
► return null;
► }
► void exampleMethod(@NonNull String param) {
► // implementation
► }
► }
► Concurrency Annotations:
• To indicate how a variable should be accessed in concurrent programming.
► @GuardedBy("this") private int counter;
Applying Type Annotations
► Type annotations can be used in a variety of places. Here are some examples:
1. Variable Declarations:
► @NonNull String text;
2.Generic Type Parameters:
List<@NonNull String> nonNullList;
3.Type Casts:
String str = (@NonNull String) nullableString;
► Method Return Types:
► @NonNull String getText() {
► return text;
► }
► Method Parameters:
► void setText(@NonNull String text) {
► this.text = text;
► }
Example: Here is a complete example
demonstrating the use of type annotations
► import ► this.lastName = lastName;
org.checkerframework.checker.nullness.qual.
NonNull; ► }
► import ► public @NonNull String getName() {
org.checkerframework.checker.nullness.qual. ► return name;
Nullable;
► }
► public class Person {
► public @Nullable String getMiddleName() {
► private final @NonNull String name;
► return middleName;
► private final @Nullable String middleName;
► }
► private final @NonNull String lastName;
► public @NonNull String getLastName() {
► public Person(@NonNull String name,
@Nullable String middleName, @NonNull ► return lastName;
String lastName) {
► }
► this.name = name;
► }
► this.middleName = middleName;
5. Repeating Annotations
► Repeating annotations in Java allow you to apply the same annotation
multiple times to a single element.
► This feature was introduced in Java 8 and can be particularly useful when you
need to provide multiple pieces of related metadata to an element.
Defining Repeating Annotations: To define a repeating annotation, you need to
use two annotations:-
1. Container Annotation: This annotation acts as a container for the repeated
annotations.
2. Repeatable Annotation: This is the annotation that you want to repeat.
Define the repeatable annotation
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
String day();
String time();
}
Define the container annotation
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value();
}
Example
import java.lang.annotation.*;
@Repeatable(Schedules.class)
@interface Schedule {
String dayOfMonth() default "first"; In this example:@Schedule is the repeatable
String dayOfWeek() default "Mon"; annotation.@Schedules is the containing annotation.
You can now apply the @Schedule annotation multiple times to
int hour() default 12;
the doPeriodicCleanup method.
} How It Works:
@interface Schedules {
Schedule[] value(); When you compile the code, the Java compiler automatically
wraps the repeated @Schedule annotations into a single
}
@Schedules annotation.
public class Example {
@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri", hour = 23)
public void doPeriodicCleanup() { // ... }}
Benefits of Repeating Annotations
► Improved Code Readability: You can express multiple values of the same
annotation more clearly and concisely.
► Reduced Boilerplate: You no longer need to create artificial container
annotations for repeated annotations.
► Backward Compatibility: The mechanism is designed to work seamlessly with
older code.
Java Module System
► Java Module System is a major change in Java 9 version. Java added this feature
to collect Java packages and code into a single unit called module.
► In earlier versions of Java, there was no concept of module to create modular
Java applications, that why size of application increased and difficult to move
around. Even JDK itself was too heavy in size, in Java 8, rt.jar file size is around
64MB.
► To deal with situation, Java 9 restructured JDK into set of modules so that we
can use only required module for our project.
► Apart from JDK, Java also allows us to create our own modules so that we can
develop module based application.
Java Module System
The module system includes various tools and options that are given below.
► Includes various options to the Java tools javac, jlink and java where we can specify module paths that
locates to the location of module.
► Modular JAR file is introduced. This JAR contains module-info.class file in its root folder.
► JMOD format is introduced, which is a packaging format similar to JAR except it can include native code
and configuration files.
► The JDK and JRE both are reconstructed to accommodate modules. It improves performance, security and
maintainability.
► Java defines a new URI scheme for naming modules, classes and resources.
Java 9 Module
Module is a collection of Java programs or softwares. To describe a module, a Java file module-info.java is
required. This file also known as module descriptor and defines the following
► Module name
► What does it export
► What does it require
How to create Java module
Creating Java module required the following steps.
► Create a directory structure
► Create a module declarator
► Java source code
Create a Directory Structure
To create module, it is recommended to follow given directory structure, it is same
as reverse-domain-pattern, we do to create packages / project-structure in Java.
Create a file module-info.java, inside this file, declare a module by
using module identifier and provide module name same as the directory name that
contains it. In our case, our directory name is com.javatpoint.
module com.javatpoint{
}
Leave module body empty, if it does not has any module dependency. Save this file
inside src/com.javatpoint with module-info.java name.
Java Source Code
Now, create a Java file to compile and execute module. In our example, we have
a Hello.java file that contains the following code.
class Hello{
public static void main(String[] args){
System.out.println("Hello from the Java module");
}
}
Save this file inside src/com.javatpoint/com/javatpoint/ with Hello.java name.
Compile Java Module
To compile the module use the following command.
► javac -d mods --module-source-path src/ --module com.javatpoint
► After compiling, it will create a new directory that contains the following structure.
After compiling, it will create a new directory that contains the following structure.
Now, we have a compiled module that can be just run.
java --module-path mods/ --module com.javatpoint/com.javatpoint.Hello
Creating a Module
► module com.example.mymodule {
► exports com.example.mymodule.api; // Expose the API package
► requires com.example.othermodule; // Require another module
► }
Compiling and Running Modules
► You'll use javac and java with module-specific options:
► # Compile
► javac -d mods --module-source-path src -m
com.example.mymodule,com.example.othermodule
► # Run
► java --module-path mods -m
com.example.mymodule/com.example.mymodule.Main
What is Generics?
► Generics are a facility of generic programming that was added to the Java
programming language in 2004 as part of J2SE 5.0.
► They allow a type or method to operate on objects of various types while
providing compile-time type safety.
► A generic type is a generic class or interface that is parameterized over
types.
A generic class is defined with the
following format:
► class name <T1, T2, ….,Tn>{ /* ... ... */ }
► The type parameter section, delimited by angle brackets (<>), follows the
class name.
► It specifies the type parameters (also called type variables) T1, T2, ..., and
T n.
► Generic Interface are defined in the same way
Type Parameter Naming Conventions
► By convention, type parameter names are single, uppercase letters. Without
this convention, it would be difficult to tell the difference between a type
variable and an ordinary class or interface name.
► The most commonly used type parameter names are:
► E- Element (used extensively by the Java Collections Framework)
► K- Key N-Number
► T- Type
► V- Value
► S,U,V etc.- 2nd, 3rd, 4th types
A Generic Class
public class Box <T>
{
private T t; // T stands for "Type“
public void add(T t)
{
this.t= t;
}
public T get()
{
return t;
}
}
Why Use Generics?
► Stronger type checks at compile time.
► A Java compiler applies strong type checking to generic code and issues errors if
the code violates type safety.
► Fixing compile-time errors is easier than fixing runtime errors, which can be
difficult to find.
► Elimination of casts. The following code snippet without generics requires
casting:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
► When re-written to use generics, the code does not require casting: List list = new
ArrayList();
list.add("hello");
String s = list.get(0); // no cast
► Widening(automatic): byte -> short -> char -> int -> long -> float -> double
► Narrowing (manual): double -> float -> long -> int -> char -> short -> byte
Before JDK 1.5 Since JDK 1.5
► The methods operate on Object types, ► The methods use generics, represented by
meaning they can store any type of objects. <E>, which allows for type safety. The E
For example, the add method takes an stands for the element type of the ArrayList.
Object as a parameter.
► The add method takes an element of type E,
► The get method returns an Object, so type and the get method returns an element of
casting is necessary when retrieving type E, eliminating the need for type
elements. casting.
► ArrayList list = new ArrayList(); ► ArrayList<String> list = new ArrayList<>();
► list.add("Hello"); ► list.add("Hello");
► String s = (String) list.get(0); // Type casting ► String s = list.get(0); // No type casting
required required
Specific Changes
► add(o: Object): void -> add(o: E): void
► get(index: int): Object -> get(index: int): E
► set(index: int, o: Object): Object -> set(index: int, o: E): E
► Using generics improves code safety and readability by ensuring that only
objects of the specified type can be added to the collection, and type casting
is no longer necessary when retrieving elements.
Declaring Generic Classes and
Interfaces
► UML Diagram: GenericStack<E>
► Field:-
► list: java.util.ArrayList<E>: This is a private field of type ArrayList<E> used to store the elements of the
stack.
► Constructors and Methods:
► +GenericStack(): This is a public constructor that initializes an empty stack.
► +getSize(): int: This public method returns the number of elements in the stack.
► +peek(): E: This public method returns the top element of the stack without removing it.
► +pop(): E: This public method returns and removes the top element of the stack.
► +push(o: E): void: This public method adds a new element to the top of the stack.
► +isEmpty(): boolean: This public method returns true if the stack is empty, otherwise false.
public class GenericStack<E> { public void push(E o) {
private java.util.ArrayList<E> list = new list.add(o);
java.util.ArrayList<>(); }
public GenericStack() { public boolean isEmpty() {
} return list.isEmpty();
public int getSize() { }
return list.size(); public static void main(String[] args) {
GenericStack<Integer> stack = new
}
GenericStack<>();
public E peek() { stack.push(1);
return list.get(list.size() - 1); stack.push(2);
} stack.push(3);
public E pop() { System.out.println(stack.pop()); // Outputs 3
E o = list.get(list.size() - 1); System.out.println(stack.peek()); // Outputs 2
list.remove(list.size() - 1); System.out.println(stack.getSize()); // Outputs 2
return o; System.out.println(stack.isEmpty()); // Outputs
false
}
}
Generic Student Class
► class Student<T extends Comparable<T>> implements public class TestComparable {
► Comparable<Student<T>> { public static void main(String[] args) {
► private T GPA; Student<Double> student1 = new Student<>(2.0);
Student<Double> student2 = new Student<>(3.2);
► public Student(T GPA) {
► this.GPA = GPA;
// Compare the two students' GPAs
► } System.out.println(student1.compareTo(student2)
► @Override // Outputs -1
► public int compareTo(Student<T> student) { Student<Integer> student3 = new Student<>(90);
► return this.GPA.compareTo(student.GPA); Student<Integer> student4 = new Student<>(85);
► } // Compare the two students' GPAs with Integer
type
► public T getGPA() {
System.out.println(student3.compareTo(student4)
► return GPA;
// Outputs 1
► } }
► } }
Double GPA Integer GPA
► Student<Double> student1 = new ► Student<Integer> student3 = new
Student<>(2.0); Student<>(90);
► Student<Double> student2 = new ► Student<Integer> student4 = new
Student<>(3.2); Student<>(85);
► System.out.println(student1.compareTo(stud ► System.out.println(student3.compareTo(stud
ent2)); ent4));
► Outputs -1 ► Outputs 1
In both cases, the Student class can store the GPA as a Double or an Integer, and it can compare the
GPA values correctly because both Double and Integer implement the Comparable interface.
► Override Annotation:
► The @Override annotation is added to the compareTo method to indicate that this method overrides a
method from the Comparable interface.
► Constructor:
► The constructor initializes the GPA field, and the field's type is double.Main
► Method:
► Two Student objects are created with different GPAs.
► The compareTo method is used to compare the GPAs, and the result is printed.
► The output will be -1 because student1 has a lower GPA than student2.
► The output will be 1 because student1 has a greater GPA than student2.
Benefits
► Flexibility: The Student class is not limited to a specific type for the GPA. It
can work with any type that implements Comparable, such as Double,
Integer, Float, etc.
► Type Safety: Using generics ensures that the Student class will only accept
types that are comparable, preventing runtime errors related to type
incompatibility.
► Code Reusability: The same Student class can be reused with different types,
reducing code duplication and improving maintainability.
Inner Classes
Java inner class or nested class is a class that is declared inside the class or interface.
We use inner classes to logically group classes and interfaces in one place to be more readable
and maintainable.
Additionally, it can access all the members of the outer class, including private data members and
methods.
Syntax of Inner class
class University{
//code
class Department{
//code
} }
Advantages: it can access all the members (data members and methods) of the outer
class, including private.
used to develop more readable and maintainable code because it logically group classes and
interfaces in one place only.
Code Optimization: It requires less code to write.
Types of Inner Class: Based on position and behaviour inner classes are of four types.
1. Normal or Local Inner class
2. Method Local inner class
3. Anonymous Inner Class
4. Static Nested classes
1.Normal or Local Inner class:
Outer.java
class Outer{
//code
class Inner{
//code
} }
Inside Inner class we can not declare any static member including main method. Main method
may my in outer class.
It will generate Outer.class and Outer$Inner.class
To Run java Outer$Inner
Accesing Inner Class Method:
Class Outer{
Class Inner{
Public void m1(){
SOP(“Inner class method”);}}
P s v main(String args[]){
Outer o=new Outer();
Outer.Inner i=o.new Inner();
//Outer.Inner i=new Outer.new Inner();
i.m1();
// new Outer.new Inner().m1();
}
Accesing Inner Class Method:
Class Outer{
Class Inner{
Public void m1()
{
SOP(“Inner class method”);
}}
Public void m2()
{
Inner i=new Inner();
i.m1();
}
P s v main(String args[]){
Outer o=new Outer();
Outer.Inner i=o.new Inner();
//Outer.Inner i=new Outer.new Inner();
i.m1();
// new Outer.new Inner().m1();
}
2. Method Local inner class:
Declaring nested method in java is not allowed. But if our requirement is method
within method then we can declare that nested method within inner class that is
defined inside a class.
Example:
Class Test{
Int x=10;static int y=20;
Public void m1(){
Class inner{
public void sum(int x,int y){
Sop(“Sum=“+(x+y));}
Inner i=new Inner();
i.sum(10,20);i.sum(100,200);
}
P s v m(String args[]){
Test t=new Test();
t.m1();
}}
3. Anonymous inner class:
Declaring inner class without name is called Anonymous inner class. Classes created
for instant use is called anonymous class. Based on declaration and behaviour there
are three types of Anonymous inner class.
a. Anonymous inner class that extends a class.
b. Anonymous inner class that implements an interface.
c. Anonymous inner class that defined inside arguments
Syntax:
Popcorn p=new Popcorn();
Popcorn p=new Popcorn()
{
};
Here, we are creating a class without name that extends Popcorn class. And creatin an
object of that child class.
Thread t=new Thread();
Thread t=new Thread()
{
};
Thread t=new Thread();
Thread t=new Thread()
{
};
Here, we are creating a class without name that extends Thread class. And creating
an object of that child class.
Runnable r= new Runnable()
{
};
Here we are creating a class without name that implements Runnable interface and
Creating object of that child class.
Example:
Class Popcorn{
Public void taste()
{
Sop(“Salty”);
}
//100 more methods
}
Class SubPopcorn extends Popcorn{
Public void taste()
{
Sop(“Spicy”);
//100 more methods
}}
When we have one time requirement of a method of class then it is not preferable to
go for permanent class in spite of this we will prefer anonymous class as given in next
slide…
Example:
Class Test
{
Public static void main(String args[]){
Popcorn p=new Popcorn()
{
Public void taste(){
Sop(“Salty”);
` }
};
p.taste();
Popcorn p1=new Popcorn();
P1.taste();
Popcorn p2=new Popcorn()
{
Public void taste(){
Sop(“Sweet”);
` }
};
P2.taste();
}
}
Example of Multithreading without Anonymous inner class:
Class Mythread extends Thread
{
Public void run(){
for(int i=0;i<10;i++)
{
Sop(“Child thread”);
` }
}
}
Class ThreadDemo{
P s v m(String[] args){
Mythread t=new Mythread();
t.start();
for(int i=0;i<10;i++)
{
Sop(“main thread”);
` }
}
}
Example of Multithreading with Anonymous inner class:
Class ThreadDemo{
P s v m(String[] args){
Thread t=new Thread()
Public void run(){
for(int i=0;i<10;i++)
{
Sop(“Child thread”);
` }
}
};
t.start();
for(int i=0;i<10;i++)
{
Sop(“main thread”);
` }
}
}
Multithreading using Runnable interface:
class MyRunnable implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println(“Child Thread”);
}}}
class Test{
public static void main(String args[]){
MyRunnable r=new MyRunnable();
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread”);
}}}
Anonymous inner class that implements an interface:
class Test{
public static void main(String args[]){
Runnable r=new Runnable()
{
public void run(){
for(int i=0;i<10;i++){
System.out.println(“Child Thread”);
}}
};
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread”);
}}}
Anonymous inner class that defined inside arguments(Below
example will be
coverted to anonymous inner class that defined inside arguments
in next slide):
class Test{
public static void main(String args[]){
Runnable r=new Runnable()
{
public void run(){
for(int i=0;i<10;i++){
System.out.println(“Child Thread”);
}}
};
Thread t=new Thread(r);
t.start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread”);
}}}
Anonymous inner class that defined inside arguments:
class Test{
public static void main(String args[]){
Thread t=new Thread(new Runnable()
{
public void run(){
for(int i=0;i<10;i++){
System.out.println(“Child Thread”);
}}
}).start();
for(int i=0;i<10;i++){
System.out.println(“Main Thread”);
}}}
4.Static nested classes
Inner classes declared with static modifier are called static nested classes.
Example=1
class Outer{
static class Nested{
public void m1()
{
Sop(“Static nested class method”);
}
Public static void main(String[] args){
Nested n=new Nested();
N.m1();
}
}
Note: If we want to create nested class object from outside of Outer class then we
can create as follows.
Outer.Nested n=new Outer.Nested();
Example=2
Inner classes declared inner class with static modifier are called static nested classes.
class Outer{
static class Nested
{
public static void main()
{
Sop(“Inner class main method”);
}
}
public static void main()
{
Sop(“Outer class main method”);
}
}
Java Test //Output: Inner class main method
Java Test$Nested //Output: Outer class main method
Difference between Normal inner class and Static nested class:
Normal inner class Static nested class
1. Strongly connected 1.Not strongly connected
2. Static members not allowed 2.Static member allowed
3. We can not declare main method 3.Can declare main method
4. Access both static and non static members of outer class 4.Only static member
outer class may be access, instance
members cannot be accessed
Diamond Syntax with Inner Anonymous
Class in java
► Diamond Operator: Introduced in Java 7 to simplify the use of generics. It
allows you to omit the generic type on the right-hand side of an object
creation expression, like this:
► List<String> names = new ArrayList<>(); // Diamond operator used here
► Anonymous Inner Classes: Classes without a name, defined and instantiated
in a single expression. They're often used for implementing interfaces or
extending classes.
► Prior to Java 9, you couldn't use the diamond operator with anonymous inner
classes. This was because the compiler couldn't reliably infer the type
arguments.
Why Use Diamond Syntax with
Anonymous Inner Classes?
• Improved Readability: Code becomes more concise and easier to understand.
• Reduced Verbosity: Avoids unnecessary repetition of type arguments.
► Caveats
► Type Inference: The diamond operator with anonymous classes works best when
the type arguments can be easily inferred from the surrounding context. If the
types are ambiguous, you might need to explicitly specify them.
► Java 9 and Above: This feature is available only in Java 9 and later versions.
Diamond Operator with Anonymous Inner Classes
abstract class XYZ<T> {
abstract T add(T num1, T num2);
}
public class Geeks {
public static void main(String[] args)
{
XYZ<Integer> obj = new XYZ<>() {
Integer add(Integer n1, Integer n2)
{
return (n1 + n2);
}
};
Integer result = obj.add(10, 20);
System.out.println("Addition of two numbers: " + result);
}}
Switch Expressions
► Switch expressions in Java were introduced in Java 12 as a preview feature
and became a standard feature in Java 14.
► They provide a more concise and flexible way to handle multiple conditions
compared to traditional switch statements.
► Switch expressions allow you to use both case labels with an arrow
► (->) and the traditional colon (:),
► but the arrow syntax is preferred.
Switch Expression
Switch Expression
Switch Expression
Switch Expression
Switch Expression(Final)
Switch Expression (yield keyword)
► The yield keyword in Java was introduced with switch expressions to provide
a way to return a value from a switch expression block.
► It is used within a block to yield a value that the switch expression will
evaluate to.
► This is especially useful when the computation of the result requires more
than a single expression or when you need to perform multiple statements
before returning the value.
► We don’t need a break after yield as it automatically terminates the switch
expression.
► Purpose:
► Yield is used to return a value from a switch expression.
► It helps handle complex logic within a case block, making switch expressions more versatile and
powerful.
► Syntax:
► Yield is used within curly braces {} which define a block.
► The value after yield is returned as the result of the switch expression for that particular case.
► Usage:
► When a switch label needs to execute multiple statements or complex logic before determining the
result, yield can be used to return the final value.
Switch Expression(yield keyword)
Local Variable Type Inference
► Local Variable Type Inference is one of the most evident change to language available from Java 10
onwards. It allows to define a variable using var and without specifying the type of it. The compiler infers
the type of the variable using the value provided. This type inference is restricted to local variables.
► Old way of declaring local variable.
String name = "Welcome to tutorialspoint.com";
► New Way of declaring local variable.
var name = "Welcome to tutorialspoint.com";
Points for Local Variable Type Inference
► No type inference in case of member variable, method parameters, return values.
► Local variable should be initialized at time of declaration otherwise compiler will
not be infer and will throw error.
► Local variable inference is available inside initialization block of loop statements.
► No runtime overhead. As compiler infers the type based on value provided, there is
no performance loss.
► No dynamic type change. Once type of local variable is inferred it cannot be
changed.
► Complex boilerplate code can be reduced using local variable type inference.
Inference Rules
► The compiler determines the type of the variable based on the initializer.
► The type must be explicitly known at compile time.
► Example:
var numbers = List.of(1, 2, 3); // Inferred as List<Integer>
Limitations
► var can only be used for local variables with initializers.
► It cannot be used for method parameters, instance variables, or return types.
► Example
var count; // Compilation error: cannot use 'var' on variable without initializer
Best Practices
► Use var where the type is obvious from the context to improve readability.
► Avoid using var if it makes the code less clear or if the inferred type is not
immediately obvious.
► Example
var map = new HashMap<String, List<String>>(); // Clear and concise
► Benefits of Local Variable Type Inference
1. Conciseness:
Reduces boilerplate code by removing redundant type declarations.
Example
var list = new ArrayList<String>(); // Instead of ArrayList<String> list = new ArrayList<>();
2. Enhanced Readability:
When the type is clear from the context, var can make the code more readable.
Example
var name = "Alice"; // Type is clearly String
3. Consistency:
Encourages a consistent way to declare variables, especially in complex generic types.
Example
var complexMap = new HashMap<String, List<Map<Integer, String>>>();
Text block
► A text block is an alternative form of Java string representation that can be
used anywhere a traditional double-quoted string literal can be used. Text
blocks begin with a “”” (3 double-quote marks) observed through
non-obligatory white spaces and a newline. For example:
// Using a literal string
String text1 = “Hello abc";
// Using a text block
String text2 = """ Hello
abc """;
Example:
public class Geeks {
public static void main(String[] args) {
String textBlock1 = " This is a" + // primitive string declaration
" multiline "
+ "text block."
;
System.out.println(textBlock1);
String textBlock = ""“ //new text block
This is a
multiline
text block.
""";
System.out.println(textBlock);
}
}
output
► In this example, the textBlock variable contains a multiline string defined using triple double
quotes (""").
► The content of the text block starts after the opening """ and ends before the closing """.
► The indentation before the closing """ is considered as part of the string content and will be
preserved.
► Text blocks can also be used for various purposes like writing HTML or SQL queries, JSON strings,
and more, making code easier to read and maintain.
Java Sealed Classes
► Java 15 introduced the concept of sealed classes.
► Java sealed classes and interfaces restrict that which classes and interfaces may extend or implement them.
► In other words, we can say that the class that cannot be inherited but can be instantiated is known as the
sealed class. It allows classes and interfaces to have more control over their permitted subtypes. It is useful
both for general domain modeling and for building a more secure platform for libraries.
Sealed classes work well with the following:
► Java Reflection API
► Java Records
► Pattern Matching
Sealed class
► Sealed classes are used to restrict the users from inheriting the class.
► A class can be sealed by using the sealed keyword.
► The keyword tells the compiler that the class is sealed, and therefore, cannot be
extended.
► No class can be derived from a sealed class.
The following is the syntax of a sealed class :
sealed class class_name
{
// data members
// methods
.
.
.
► The declaration of a sealed class is not much complicated. If we want to declare a class as sealed, add
a sealed modifier to its declaration. After the class declaration and extends and implements clause,
add permits clause. The clause denotes the classes that may extend the sealed class.
► It presents the following modifiers and clauses:
• sealed: It can only be extended by its permitted subclasses.
• non-sealed: It can be extended by unknown subclasses; a sealed class cannot prevent its permitted
subclasses from doing this.
• permits: It allows the subclass to inherit and extend.
• final: The permitted subclass must be final because it prevents further extensions.
Example
► public sealed class Shape permits Circle, Rectangle {
► // common properties and methods
► }
► final class Circle extends Shape {
► // Circle-specific properties and methods
► }
► final class Rectangle extends Shape {
► // Rectangle-specific properties and methods
► }
Records:
► In Java, records are a feature introduced in Java 14 as a preview feature and
then made a permanent feature starting from Java 16.
► Records provide a concise way to declare classes that are essentially data
carriers, where the primary purpose is to store and retrieve data.
► They are immutable by default, meaning their state cannot be changed after
they are created.
// Define a record representing a Person
public record Person(String name, int age) {} This line defines a record named Person with two
components: name (a String) and age (an int).
public class Main {
public static void main(String[] args) {
// Creating a new Person object using the
record constructor
Person person1 = new Person("Alice", 30); These lines create two Person objects using the
automatically generated constructor.
Person person2 = new Person("Bob", 25);
// Accessing fields of the Person object
System.out.println("Name: " + This prints the name and age of person1. The
person1.name()); compiler provides getter methods (name() and
System.out.println("Age: " + person1.age()); age()) for accessing the components.
// Records provide a toString()
implementation by default
This line demonstrates the automatically generated
System.out.println("Person 1: " + person1); toString() method, which produces a string
representation of the Person object (e.g.,
Person[name=Alice, age=30]).
// Records are immutable, so we cannot change
their state
// person1.name = "Charlie"; // Compilation This commented-out line shows that you cannot change
error: cannot assign a value to a final variable the values of record components.
// Records provide structural equality
System.out.println("Are person1 and person2 This checks if person1 and person2 are equal based on
equal? " + (person1.equals(person2))); their component values.
// Creating a new Person object using the copy
constructor
Person person3 = new Person(person1.name(), This creates a new Person object (person3) as a copy of
person1.age()); person1.
System.out.println("Person 3: " + person3);
// Records allow for deconstruction
This extracts the name and age components from
String name = person1.name();
person1.
int age = person1.age();
System.out.println("Deconstructed: Name=" +
name + ", Age=" + age);
}
}
Output: