Module I: Collection and Generic
Java Collection Framework
The Java Collection Framework provides a set of classes and interfaces to manage groups of
objects in a structured way. It includes a variety of data structures like lists, sets, maps, and
queues, along with various algorithms for data manipulation. Let's break down the main
components of the framework with simple examples.
1. Collection Interfaces
• Collection: The root interface for most collection classes.
• List: An ordered collection that allows duplicate elements.
• Set: A collection that does not allow duplicate elements.
• Queue: A collection used to hold multiple elements prior to processing.
• Map: A collection that maps keys to values, with no duplicate keys allowed.
2. Key Interfaces and Classes
a. List Interface
• ArrayList: A resizable array implementation.
• LinkedList: A doubly-linked list implementation.
• Vector: A synchronized, thread-safe version of ArrayList.
• Stack: A last-in, first-out (LIFO) stack of objects.
Example: ArrayList
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// Creating an ArrayList
ArrayList<String> fruits = new ArrayList<>();
// Adding elements to the ArrayList
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Accessing elements
System.out.println(fruits.get(1)); // Output: Banana
// Iterating through the ArrayList
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
b. Set Interface
• HashSet: Does not maintain any order and allows null elements.
• LinkedHashSet: Maintains insertion order.
• TreeSet: Stores elements in a sorted order.
Example: HashSet
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
// Creating a HashSet
HashSet<String> cars = new HashSet<>();
// Adding elements to the HashSet
cars.add("BMW");
cars.add("Audi");
cars.add("Honda");
cars.add("BMW"); // Duplicate element, will be ignored
// Iterating through the HashSet
for (String car : cars) {
System.out.println(car);
}
}
}
c. Map Interface
• HashMap: A map based on a hash table, allows one null key and multiple null values.
• LinkedHashMap: Maintains insertion order.
• TreeMap: A map that is sorted according to the natural order of its keys.
Example: HashMap
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
// Creating a HashMap
HashMap<Integer, String> students = new HashMap<>();
// Adding key-value pairs to the HashMap
students.put(101, "John");
students.put(102, "Alice");
students.put(103, "Bob");
// Accessing value by key
System.out.println(students.get(102)); // Output: Alice
// Iterating through the HashMap
for (Integer key : students.keySet()) {
System.out.println("Roll No: " + key + ", Name: " + students.get(key));
}
}
}
3. Common Methods
Most collection classes provide common methods like:
• add(element): Adds an element to the collection.
• remove(element): Removes an element from the collection.
• size(): Returns the number of elements in the collection.
• contains(element): Checks if the collection contains the specified element.
• isEmpty(): Checks if the collection is empty.
• clear(): Removes all elements from the collection.
4. Why Use the Collection Framework?
• Flexible Storage: Offers various data structures to suit different needs (ordered,
unordered, sorted).
• Easy to Use: Provides a set of pre-built, reusable data structures.
• Efficiency: Optimized for performance with built-in algorithms.
• Thread Safety: Some classes like Vector and HashTable provide synchronized access.
Basic Operations
The Java Collection Framework provides a set of basic operations that you can perform on
different collection types such as lists, sets, and maps. These operations include adding,
removing, accessing, and iterating over elements. Here are some common operations with
examples:
1. Adding Elements
• add(element): Adds an element to the collection.
• add(index, element): Adds an element at a specific position in a list.
Example: Adding to a List
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
// List Example
ArrayList<String> list = new ArrayList<>();
list.add("Apple"); // Adds "Apple" to the list
list.add("Banana"); // Adds "Banana" to the list
list.add(1, "Orange"); // Adds "Orange" at index 1
System.out.println(list); // Output: [Apple, Orange, Banana]
// Set Example
HashSet<String> set = new HashSet<>();
set.add("Dog"); // Adds "Dog" to the set
set.add("Cat"); // Adds "Cat" to the set
set.add("Dog"); // Duplicate, will be ignored
System.out.println(set); // Output: [Cat, Dog] (no duplicates)
}
}
2. Removing Elements
• remove(element): Removes the specified element from the collection.
• remove(index): Removes the element at the specified position in a list.
• clear(): Removes all elements from the collection.
Example: Removing from a List and Set
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
// List Example
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
list.remove("Banana"); // Removes "Banana"
list.remove(0); // Removes element at index 0 ("Apple")
System.out.println(list); // Output: [Orange]
// Set Example
HashSet<String> set = new HashSet<>();
set.add("Dog");
set.add("Cat");
set.add("Horse");
set.remove("Cat"); // Removes "Cat"
System.out.println(set); // Output: [Dog, Horse]
}
}
3. Accessing Elements
• get(index): Returns the element at the specified position in a list.
• contains(element): Checks if the collection contains the specified element.
• size(): Returns the number of elements in the collection.
Example: Accessing Elements in a List and Set
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
// List Example
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
System.out.println(list.get(1)); // Output: Banana
System.out.println(list.contains("Orange")); // Output: true
System.out.println(list.size()); // Output: 3
// Set Example
HashSet<String> set = new HashSet<>();
set.add("Dog");
set.add("Cat");
set.add("Horse");
System.out.println(set.contains("Cat")); // Output: true
System.out.println(set.size()); // Output: 3
}
}
4. Iterating over Elements
• for loop: Iterate through a collection using a basic for loop (mainly for lists).
• Enhanced for loop: Iterate through all elements in any collection.
• Iterator: An object that provides a way to traverse elements.
Example: Iterating through a List and Set
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
// List Example
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
// Using a simple for loop (only for List)
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// Using an enhanced for loop
for (String fruit : list) {
System.out.println(fruit);
}
// Set Example
HashSet<String> set = new HashSet<>();
set.add("Dog");
set.add("Cat");
set.add("Horse");
// Using an enhanced for loop
for (String animal : set) {
System.out.println(animal);
}
// Using an Iterator
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
5. Other Common Operations
• isEmpty(): Checks if the collection is empty.
• toArray(): Converts the collection into an array.
• retainAll(collection): Retains only the elements in the collection that are also in the
specified collection.
• removeAll(collection): Removes all elements that are contained in the specified
collection.
Example: Checking if a Collection is Empty
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
System.out.println(list.isEmpty()); // Output: true
list.add("Apple");
System.out.println(list.isEmpty()); // Output: false
}
}
Bulk Operations
Bulk operations in the Java Collection Framework allow you to perform operations on multiple
elements at once. These operations are useful for efficiently adding, removing, or retaining
groups of elements across collections. Here are some common bulk operations:
1. Adding All Elements: addAll()
The addAll(Collection<? extends E> c) method adds all elements from one collection to another.
Example: Adding All Elements from One List to Another
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
list1.add("Orange");
ArrayList<String> list2 = new ArrayList<>();
list2.add("Grapes");
list2.add("Pineapple");
// Adding all elements from list2 to list1
list1.addAll(list2);
System.out.println(list1); // Output: [Apple, Banana, Orange, Grapes, Pineapple]
}
}
Example: Adding All Elements from a Set to a List
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Dog");
list.add("Cat");
HashSet<String> set = new HashSet<>();
set.add("Horse");
set.add("Elephant");
// Adding all elements from set to list
list.addAll(set);
System.out.println(list); // Output: [Dog, Cat, Horse, Elephant]
}
}
2. Removing All Elements: removeAll()
The removeAll(Collection<?> c) method removes all elements in the current collection that are
also present in the specified collection.
Example: Removing Common Elements from a List
import java.util.ArrayList;
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
list1.add("Orange");
ArrayList<String> list2 = new ArrayList<>();
list2.add("Banana");
list2.add("Grapes");
// Removing all elements in list1 that are also in list2
list1.removeAll(list2);
System.out.println(list1); // Output: [Apple, Orange]
}
}
Example: Removing Elements of a Set from Another Set
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> set1 = new HashSet<>();
set1.add("Dog");
set1.add("Cat");
set1.add("Horse");
HashSet<String> set2 = new HashSet<>();
set2.add("Cat");
set2.add("Elephant");
// Removing all elements in set1 that are also in set2
set1.removeAll(set2);
System.out.println(set1); // Output: [Dog, Horse]
}
}
3. Retaining Common Elements: retainAll()
The retainAll(Collection<?> c) method retains only the elements in the current collection that are
also present in the specified collection. All other elements are removed.
Example: Retaining Common Elements in Two Lists
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
list1.add("Orange");
ArrayList<String> list2 = new ArrayList<>();
list2.add("Banana");
list2.add("Grapes");
// Retaining only the elements in list1 that are also in list2
list1.retainAll(list2);
System.out.println(list1); // Output: [Banana]
}
}
Example: Retaining Common Elements in Two Sets
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> set1 = new HashSet<>();
set1.add("Dog");
set1.add("Cat");
set1.add("Horse");
HashSet<String> set2 = new HashSet<>();
set2.add("Cat");
set2.add("Elephant");
// Retaining only the elements in set1 that are also in set2
set1.retainAll(set2);
System.out.println(set1); // Output: [Cat]
}
}
4. Checking for Common Elements: containsAll()
The containsAll(Collection<?> c) method checks if the current collection contains all elements
from the specified collection.
Example: Checking if a List Contains All Elements of Another List
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
list1.add("Orange");
ArrayList<String> list2 = new ArrayList<>();
list2.add("Banana");
list2.add("Orange");
// Checking if list1 contains all elements of list2
boolean result = list1.containsAll(list2);
System.out.println(result); // Output: true
}
}
Example: Checking if a Set Contains All Elements of a List
import java.util.HashSet;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("Dog");
set.add("Cat");
set.add("Horse");
ArrayList<String> list = new ArrayList<>();
list.add("Cat");
list.add("Dog");
// Checking if set contains all elements of list
boolean result = set.containsAll(list);
System.out.println(result); // Output: true
}
}
5. Converting to an Array: toArray()
The toArray() method converts the collection into an array.
Example: Converting a List to an Array
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
// Converting the list to an array
String[] array = list.toArray(new String[0]);
// Printing the array
System.out.println(Arrays.toString(array)); // Output: [Apple, Banana, Orange]
}
}
Example: Converting a Set to an Array
import java.util.HashSet;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
set.add("Dog");
set.add("Cat");
set.add("Horse");
// Converting the set to an array
String[] array = set.toArray(new String[0]);
// Printing the array
System.out.println(Arrays.toString(array)); // Output: [Dog, Cat, Horse]
}
}
6. Checking Equality: equals()
The equals(Object o) method checks whether two collections are equal in terms of containing the
same elements in the same order.
Example: Checking Equality of Two Lists
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("Apple");
list1.add("Banana");
ArrayList<String> list2 = new ArrayList<>();
list2.add("Apple");
list2.add("Banana");
// Checking if list1 is equal to list2
boolean result = list1.equals(list2);
System.out.println(result); // Output: true
}
}
Iteration
Iteration is the process of accessing each element of a collection one by one. In Java, there are
several ways to iterate over collections such as lists, sets, and maps. Let's explore the different
methods of iteration with examples.
1. Using for Loop (Basic For Loop)
This is mainly used for iterating over List collections where you need the index of each element.
Example: Iterating Over a List Using a for Loop
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Iterating using a basic for loop
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
}
}
2. Using Enhanced for Loop (For-Each Loop)
The enhanced for loop is a simpler and more concise way to iterate over collections and arrays.
Example: Iterating Over a List Using an Enhanced for Loop
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Iterating using an enhanced for loop
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
Example: Iterating Over a Set Using an Enhanced for Loop
import java.util.HashSet;
public class Main {
public static void main(String[] args) {
HashSet<String> animals = new HashSet<>();
animals.add("Dog");
animals.add("Cat");
animals.add("Elephant");
// Iterating using an enhanced for loop
for (String animal : animals) {
System.out.println(animal);
}
}
}
3. Using Iterator Interface
The Iterator interface provides a way to traverse through a collection sequentially and allows for
safe element removal during iteration.
Common Methods in Iterator:
• hasNext(): Returns true if the iteration has more elements.
• next(): Returns the next element in the iteration.
• remove(): Removes the last element returned by the iterator (optional operation).
Example: Iterating Over a List Using Iterator
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Creating an iterator
Iterator<String> iterator = fruits.iterator();
// Iterating using the iterator
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Example: Removing an Element While Iterating Using Iterator
import java.util.ArrayList;
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Creating an iterator
Iterator<String> iterator = fruits.iterator();
// Removing "Banana" while iterating
while (iterator.hasNext()) {
String fruit = iterator.next();
if (fruit.equals("Banana")) {
iterator.remove(); // Safely removes "Banana"
}
}
System.out.println(fruits); // Output: [Apple, Orange]
}
}
4. Using ListIterator Interface
ListIterator is an extension of Iterator for lists, providing additional methods like hasPrevious()
and previous(). It allows bidirectional traversal.
Common Methods in ListIterator:
• hasPrevious(): Returns true if the list iterator has more elements when traversing
backward.
• previous(): Returns the previous element in the list and moves the cursor position
backward.
• nextIndex() and previousIndex(): Returns the index of the element that would be
returned by a subsequent call to next() or previous().
Example: Iterating Backward and Forward Using ListIterator
import java.util.ArrayList;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Creating a list iterator
ListIterator<String> listIterator = fruits.listIterator();
// Iterating forward
while (listIterator.hasNext()) {
System.out.println("Next: " + listIterator.next());
}
// Iterating backward
while (listIterator.hasPrevious()) {
System.out.println("Previous: " + listIterator.previous());
}
}
}
5. Using forEach() Method
Java 8 introduced the forEach() method in the Iterable interface, which can be used to iterate
over collections using a lambda expression.
Example: Iterating Over a List Using forEach()
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using forEach method with lambda expression
fruits.forEach(fruit -> System.out.println(fruit));
}
}
6. Iterating Over a Map
Maps don't implement the Iterable interface, so they require a different approach to iterate over
keys, values, or entries.
Example: Iterating Over a Map's Key-Value Pairs Using entrySet()
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// Iterating over key-value pairs
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
Example: Iterating Over a Map's Keys Using keySet()
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// Iterating over keys
for (Integer key : map.keySet()) {
System.out.println("Key: " + key);
}
}
}
Example: Iterating Over a Map's Values Using values()
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// Iterating over values
for (String value : map.values()) {
System.out.println("Value: " + value);
}
}
}
List
In Java, the List interface is a part of the Java Collections Framework and represents an ordered
collection of elements. Lists allow duplicate elements and provide control over the position
where elements are inserted or accessed. The most commonly used implementations of the List
interface are ArrayList, LinkedList, and Vector. Let's dive deeper into the List interface and its
common operations with examples.
1. Characteristics of a List
• Ordered Collection: Elements are stored in the order they are inserted.
• Allows Duplicates: Multiple occurrences of the same element are allowed.
• Positional Access: Elements can be accessed by their index.
2. Common Implementations of List
• ArrayList: Backed by a dynamic array, it offers fast random access but slower insertions
and deletions in the middle.
• LinkedList: A doubly linked list implementation, it provides fast insertions and deletions
but slower access.
• Vector: Similar to ArrayList but synchronized (thread-safe).
3. Creating and Initializing a List
You can create a List using its implementations like ArrayList or LinkedList.
Example: Creating and Initializing an ArrayList
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
System.out.println(fruits); // Output: [Apple, Banana, Orange]
}
}
Example: Creating and Initializing a LinkedList
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new LinkedList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
System.out.println(fruits); // Output: [Apple, Banana, Orange]
}
}
4. Basic Operations on a List
The List interface provides various methods to perform operations on the list elements.
4.1 Adding Elements
• add(E element): Adds an element at the end.
• add(int index, E element): Adds an element at a specific index.
Example: Adding Elements to a List
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple"); // Adds "Apple" at the end
fruits.add("Banana"); // Adds "Banana" at the end
fruits.add(1, "Orange"); // Adds "Orange" at index 1
System.out.println(fruits); // Output: [Apple, Orange, Banana]
}
}
4.2 Accessing Elements
• get(int index): Returns the element at the specified index.
Example: Accessing Elements from a List
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Accessing elements
String firstFruit = fruits.get(0); // Gets the element at index 0
String secondFruit = fruits.get(1); // Gets the element at index 1
System.out.println(firstFruit); // Output: Apple
System.out.println(secondFruit); // Output: Banana
}
}
4.3 Updating Elements
• set(int index, E element): Replaces the element at the specified index with the new
element.
Example: Updating an Element in a List
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Updating element at index 1
fruits.set(1, "Grapes");
System.out.println(fruits); // Output: [Apple, Grapes, Orange]
}
}
4.4 Removing Elements
• remove(int index): Removes the element at the specified index.
• remove(Object o): Removes the first occurrence of the specified element.
Example: Removing an Element from a List
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Removing element at index 1
fruits.remove(1);
System.out.println(fruits); // Output: [Apple, Orange]
// Removing "Orange" by element
fruits.remove("Orange");
System.out.println(fruits); // Output: [Apple]
}
}
4.5 Checking the Size
• size(): Returns the number of elements in the list.
Example: Checking the Size of a List
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
int size = fruits.size(); // Size is 3
System.out.println(size); // Output: 3
}
}
4.6 Checking for an Element
• contains(Object o): Returns true if the list contains the specified element.
Example: Checking if a List Contains an Element
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
boolean hasBanana = fruits.contains("Banana"); // true
boolean hasGrapes = fruits.contains("Grapes"); // false
System.out.println(hasBanana); // Output: true
System.out.println(hasGrapes); // Output: false
}
}
5. Iterating Over a List
There are multiple ways to iterate over a List in Java, such as using a for loop, enhanced for loop,
iterator, and forEach.
Example: Iterating Using a Basic For Loop
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using a basic for loop
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
}
}
Example: Iterating Using an Enhanced For Loop
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using an enhanced for loop
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
Example: Iterating Using an Iterator
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using an iterator
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Example: Iterating Using forEach() Method
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using forEach method
fruits.forEach(fruit -> System.out.println(fruit));
}
}
6. Sorting a List
The Collections.sort() method can be used to sort a list of Comparable elements, or a custom
comparator can be provided.
Example: Sorting a List of Strings
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Banana");
fruits.add("Apple");
fruits.add("Orange");
// Sorting the list
Collections.sort(fruits);
System.out.println(fruits); // Output: [Apple, Banana, Orange]
}
}
Difference between linked list and array list
The difference between ArrayList and LinkedList in Java is primarily based on their underlying
data structures, performance characteristics, and the types of operations they optimize. Here’s a
detailed comparison:
1. Underlying Data Structure
• ArrayList:
o Backed by a dynamic array.
o The size of the array can change dynamically as elements are added or removed.
• LinkedList:
o Implemented as a doubly linked list.
o Each element (node) contains a reference to the next and previous node.
2. Memory Allocation
• ArrayList:
o Requires contiguous memory for its elements.
o If the underlying array needs to grow, a new larger array is created, and elements
are copied, which can be expensive in terms of performance.
• LinkedList:
o Nodes can be scattered throughout memory.
o Each node requires additional memory for storing references to the next and
previous nodes.
3. Usage Scenarios
• ArrayList:
o Best for scenarios where you need fast random access to elements (like getting
elements by index).
o Suitable when you frequently add or remove elements at the end of the list.
• LinkedList:
o Ideal for scenarios that require frequent insertions and deletions, especially in the
middle of the list.
o Useful when memory usage is not a critical concern, and the number of elements
varies significantly.
4. Implementation
• ArrayList:
o Simple to implement.
o Provides built-in methods to manage dynamic array resizing.
• LinkedList:
o More complex due to node management.
o Allows for more sophisticated operations, like adding or removing elements from
both ends efficiently.
5. Iterating
• ArrayList:
o Iteration is generally faster due to contiguous memory access.
o Cache-friendly, meaning it can take better advantage of CPU caching.
• LinkedList:
o Iteration is slower due to non-contiguous memory allocation.
o Each access may require following pointers, leading to more cache misses.
6. Thread Safety
• Both ArrayList and LinkedList are not synchronized (not thread-safe). If you need a
thread-safe list, you can use Collections.synchronizedList() or CopyOnWriteArrayList.
Set
In Java, the Set interface is part of the Java Collections Framework and represents a collection of
unique elements. Sets do not allow duplicate elements, and their primary purpose is to provide
operations for manipulating sets of objects, such as adding, removing, and checking for the
presence of elements. Here’s a detailed overview of Set, its characteristics, implementations, and
common operations.
1. Characteristics of a Set
• Uniqueness: A Set does not allow duplicate elements. If you try to add a duplicate, the
operation will not be performed.
• Unordered: The elements in a Set are not stored in any specific order. The order may
change over time.
• Dynamic Size: Sets can grow or shrink in size as elements are added or removed.
2. Common Implementations of Set
• HashSet:
o Backed by a hash table.
o Provides constant time performance for basic operations (add, remove, contains).
o Does not guarantee any specific order of elements.
• LinkedHashSet:
o Extends HashSet and maintains a linked list of the entries.
o Maintains insertion order, meaning elements will be returned in the order they
were added.
• TreeSet:
o Implements the SortedSet interface.
o Backed by a red-black tree, it maintains a sorted order of elements (natural
ordering or a specified comparator).
o Provides logarithmic time performance for basic operations.
3. Creating and Initializing a Set
You can create a Set using its implementations like HashSet, LinkedHashSet, or TreeSet.
Example: Creating and Initializing a HashSet
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Apple"); // Duplicate, will not be added
System.out.println(fruits); // Output: [Banana, Orange, Apple]
}
}
Example: Creating and Initializing a LinkedHashSet
import java.util.LinkedHashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new LinkedHashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Apple"); // Duplicate, will not be added
System.out.println(fruits); // Output: [Apple, Banana, Orange]
}
}
Example: Creating and Initializing a TreeSet
import java.util.TreeSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new TreeSet<>();
fruits.add("Banana");
fruits.add("Apple");
fruits.add("Orange");
fruits.add("Apple"); // Duplicate, will not be added
System.out.println(fruits); // Output: [Apple, Banana, Orange]
}
}
4. Common Operations on a Set
4.1 Adding Elements
• add(E e): Adds an element to the set. Returns true if the set did not already contain the
specified element.
Example: Adding Elements to a Set
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
System.out.println(fruits); // Output: [Apple, Banana]
}
}
4.2 Removing Elements
• remove(Object o): Removes the specified element from the set. Returns true if the set
contained the element.
Example: Removing Elements from a Set
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.remove("Banana"); // Removes "Banana"
System.out.println(fruits); // Output: [Apple]
}
}
4.3 Checking for Elements
• contains(Object o): Returns true if the set contains the specified element.
Example: Checking if a Set Contains an Element
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
boolean hasBanana = fruits.contains("Banana"); // true
boolean hasGrapes = fruits.contains("Grapes"); // false
System.out.println(hasBanana); // Output: true
System.out.println(hasGrapes); // Output: false
}
}
4.4 Getting the Size
• size(): Returns the number of elements in the set.
Example: Checking the Size of a Set
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
System.out.println(fruits.size()); // Output: 2
}
}
4.5 Iterating Over a Set
You can iterate over a Set using various methods like the enhanced for loop, iterator, and
forEach.
Example: Iterating Using Enhanced For Loop
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using an enhanced for loop
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
Example: Iterating Using an Iterator
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using an iterator
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
Example: Iterating Using forEach
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Using forEach method
fruits.forEach(fruit -> System.out.println(fruit));
}
}
5. Set Operations
Sets support various operations like union, intersection, and difference. You can perform these
operations using methods in the Set interface or by utilizing collections.
Example: Union of Two Sets
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> set1 = new HashSet<>();
set1.add("Apple");
set1.add("Banana");
Set<String> set2 = new HashSet<>();
set2.add("Banana");
set2.add("Orange");
// Union
set1.addAll(set2);
System.out.println(set1); // Output: [Apple, Banana, Orange]
}
}
Example: Intersection of Two Sets
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> set1 = new HashSet<>();
set1.add("Apple");
set1.add("Banana");
Set<String> set2 = new HashSet<>();
set2.add("Banana");
set2.add("Orange");
// Intersection
set1.retainAll(set2);
System.out.println(set1); // Output: [Banana]
}
}
Example: Difference of Two Sets
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> set1 = new HashSet<>();
set1.add("Apple");
set1.add("Banana");
Set<String> set2 = new HashSet<>();
set2.add("Banana");
set2.add("Orange");
// Difference
set1.removeAll(set2);
System.out.println(set1); // Output: [Apple]
}
}
Maps
In Java, the Map interface is part of the Java Collections Framework and represents a collection
of key-value pairs. Each key is associated with exactly one value, and a Map does not allow
duplicate keys. The Map interface provides a way to store and retrieve data efficiently based on
keys. Here’s a detailed overview of Map, its characteristics, implementations, and common
operations.
1. Characteristics of a Map
• Key-Value Pair: Each entry in a Map consists of a key and a corresponding value.
• Uniqueness of Keys: Each key in a Map must be unique. You can have duplicate values,
but not duplicate keys.
• Dynamic Size: Maps can grow or shrink as elements are added or removed.
2. Common Implementations of Map
• HashMap:
o Implements the Map interface using a hash table.
o Provides constant time performance for basic operations (put, get, remove) on
average.
o Does not guarantee any specific order of elements.
• LinkedHashMap:
o Extends HashMap and maintains a linked list of the entries.
o Maintains insertion order, meaning elements will be returned in the order they
were added.
• TreeMap:
o Implements the SortedMap interface.
o Backed by a red-black tree, it maintains a sorted order of keys (natural ordering or
specified comparator).
o Provides logarithmic time performance for basic operations.
• Hashtable:
o Similar to HashMap but synchronized (thread-safe).
o Legacy class that has been largely replaced by HashMap.
3. Creating and Initializing a Map
You can create a Map using its implementations like HashMap, LinkedHashMap, or TreeMap.
Example: Creating and Initializing a HashMap
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
System.out.println(fruitCounts); // Output: {Apple=3, Banana=2, Orange=5}
}
}
Example: Creating and Initializing a LinkedHashMap
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new LinkedHashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
System.out.println(fruitCounts); // Output: {Apple=3, Banana=2, Orange=5}
}
}
Example: Creating and Initializing a TreeMap
import java.util.TreeMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new TreeMap<>();
fruitCounts.put("Banana", 2);
fruitCounts.put("Apple", 3);
fruitCounts.put("Orange", 5);
System.out.println(fruitCounts); // Output: {Apple=3, Banana=2, Orange=5}
}
}
4. Common Operations on a Map
4.1 Adding Elements
• put(K key, V value): Adds a key-value pair to the map. If the key already exists, it
updates the value.
Example: Adding Elements to a Map
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
System.out.println(fruitCounts); // Output: {Apple=3, Banana=2, Orange=5}
}
}
4.2 Accessing Values
• get(Object key): Retrieves the value associated with the specified key.
Example: Accessing Values from a Map
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
int appleCount = fruitCounts.get("Apple"); // Gets the value for "Apple"
System.out.println(appleCount); // Output: 3
}
}
4.3 Removing Elements
• remove(Object key): Removes the key-value pair associated with the specified key.
Example: Removing Elements from a Map
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.remove("Banana"); // Removes the entry for "Banana"
System.out.println(fruitCounts); // Output: {Apple=3}
}
}
4.4 Checking for a Key or Value
• containsKey(Object key): Returns true if the map contains the specified key.
• containsValue(Object value): Returns true if the map maps one or more keys to the
specified value.
Example: Checking for a Key or Value
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
boolean hasBanana = fruitCounts.containsKey("Banana"); // true
boolean hasGrapes = fruitCounts.containsValue(5); // false
System.out.println(hasBanana); // Output: true
System.out.println(hasGrapes); // Output: false
}
}
4.5 Getting the Size
• size(): Returns the number of key-value pairs in the map.
Example: Checking the Size of a Map
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
System.out.println(fruitCounts.size()); // Output: 2
}
}
4.6 Iterating Over a Map
You can iterate over a Map using various methods, such as iterating over keys, values, or entries.
Example: Iterating Using Entry Set
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
// Iterating using entry set
for (Map.Entry<String, Integer> entry : fruitCounts.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
5. Map Operations
Maps support various operations like merging, replacing values, and bulk operations.
Example: Merging Two Maps
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map1 = new HashMap<>();
map1.put("Apple", 3);
map1.put("Banana", 2);
Map<String, Integer> map2 = new HashMap<>();
map2.put("Banana", 3);
map2.put("Orange", 5);
// Merging map2 into map1
map2.forEach((key, value) ->
Difference between HashMap, LinkedHashMap, TreeMap
1. Underlying Data Structure
• HashMap:
o Uses a hash table as the underlying data structure.
• LinkedHashMap:
o Extends HashMap and maintains a linked list of the entries to preserve insertion
order.
• TreeMap:
o Uses a red-black tree as the underlying data structure to store the keys in a sorted
order.
2. Order of Elements
• HashMap:
o Does not guarantee any order of its elements. The order may change over time as
elements are added or removed.
• LinkedHashMap:
o Maintains the order of elements based on their insertion order. It can also be
configured to maintain access order.
• TreeMap:
o Stores elements in a sorted order based on the natural ordering of keys or a
specified comparator.
3. Performance (Time Complexity)
Operation HashMap LinkedHashMap TreeMap
Insertion O(1) average (O(n) worst case) O(1) average (O(n) worst case) O(log n)
Access (get) O(1) average (O(n) worst case) O(1) average (O(n) worst case) O(log n)
Removal O(1) average (O(n) worst case) O(1) average (O(n) worst case) O(log n)
Iteration O(n) O(n) O(n)
Sorting Not sorted Not sorted Sorted
4. Null Keys and Values
• HashMap:
o Allows one null key and multiple null values.
• LinkedHashMap:
o Allows one null key and multiple null values.
• TreeMap:
o Does not allow null keys (throws NullPointerException) but allows multiple null
values.
5. Use Cases
• HashMap:
o Best for scenarios where fast access and insertion are required, and order does not
matter.
• LinkedHashMap:
o Useful when you need to maintain the order of entries, such as in caches or when
order of iteration matters.
• TreeMap:
o Ideal for scenarios where you need sorted order of keys, such as range queries or
when implementing sorted maps.
6. Synchronization
• None of these maps are synchronized. If you need a thread-safe map, you can wrap them
using Collections.synchronizedMap() or use ConcurrentHashMap.
1. HashMap Example
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new HashMap<>();
// Adding key-value pairs to the HashMap
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
// Displaying the contents of the HashMap
System.out.println("HashMap: " + fruitCounts); // Order is not guaranteed
// Accessing a value by its key
System.out.println("Count of Apples: " + fruitCounts.get("Apple"));
// Removing a key-value pair
fruitCounts.remove("Banana");
System.out.println("After removing Banana: " + fruitCounts);
}
}
Explanation:
• Creating a HashMap: We instantiate a HashMap to store fruit names as keys and their
counts as values.
• Adding Entries: The put method adds key-value pairs. If a key already exists, the value
is updated.
• Accessing Values: The get method retrieves the count of apples.
• Removing Entries: The remove method deletes the entry for "Banana". The order of
elements is not guaranteed.
2. LinkedHashMap Example
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapExample {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new LinkedHashMap<>();
// Adding key-value pairs to the LinkedHashMap
fruitCounts.put("Apple", 3);
fruitCounts.put("Banana", 2);
fruitCounts.put("Orange", 5);
// Displaying the contents of the LinkedHashMap
System.out.println("LinkedHashMap: " + fruitCounts); // Maintains insertion order
// Accessing a value by its key
System.out.println("Count of Apples: " + fruitCounts.get("Apple"));
// Removing a key-value pair
fruitCounts.remove("Banana");
System.out.println("After removing Banana: " + fruitCounts);
}
}
Explanation:
• Creating a LinkedHashMap: We instantiate a LinkedHashMap, which maintains the
order of insertion.
• Adding Entries: Similar to HashMap, we use the put method to add key-value pairs.
• Accessing Values: The get method retrieves the count of apples. The insertion order is
preserved when displaying.
• Removing Entries: The remove method deletes the "Banana" entry, and the order
remains intact.
3. TreeMap Example
import java.util.TreeMap;
import java.util.Map;
public class TreeMapExample {
public static void main(String[] args) {
Map<String, Integer> fruitCounts = new TreeMap<>();
// Adding key-value pairs to the TreeMap
fruitCounts.put("Banana", 2);
fruitCounts.put("Apple", 3);
fruitCounts.put("Orange", 5);
// Displaying the contents of the TreeMap
System.out.println("TreeMap: " + fruitCounts); // Sorted by keys
// Accessing a value by its key
System.out.println("Count of Apples: " + fruitCounts.get("Apple"));
// Removing a key-value pair
fruitCounts.remove("Banana");
System.out.println("After removing Banana: " + fruitCounts);
}
}
Explanation:
• Creating a TreeMap: We instantiate a TreeMap, which sorts its keys in natural order
(alphabetical in this case).
• Adding Entries: We use the put method to add entries. The keys are sorted as we add
them.
• Accessing Values: The get method retrieves the count of apples.
• Removing Entries: The remove method deletes the "Banana" entry, and the remaining
keys stay sorted.
Summary of Key Differences
• Ordering:
o HashMap has no specific order.
o LinkedHashMap maintains insertion order.
o TreeMap sorts keys naturally.
• Performance:
o HashMap and LinkedHashMap provide O(1) average time complexity for
insertion, retrieval, and removal.
o TreeMap provides O(log n) time complexity for these operations due to its sorted
nature.
• Usage:
o Use HashMap for fast access when order doesn't matter.
o Use LinkedHashMap when you need to maintain insertion order.
o Use TreeMap when you need a sorted map based on keys.
Generics in Java
Generics in Java allow you to write code that can operate on objects of various types while
providing compile-time type safety. It means you can create classes, interfaces, and methods
where the type of data they operate on is specified as a parameter. This helps in reducing runtime
errors by catching type-related issues at compile-time and eliminating the need for casting.
Why Use Generics?
• Type Safety: Ensures that you can only use the specified type, avoiding
ClassCastException.
• Code Reusability: You can write more general and reusable code.
• Elimination of Casts: Reduces the need to use casting.
Generic Class Example
Let’s start with a simple example of a generic class.
Example: A Generic Box Class
// Defining a generic class with a type parameter T
public class Box<T> {
private T item;
// Method to set the item
public void setItem(T item) {
this.item = item;
}
// Method to get the item
public T getItem() {
return item;
}
public static void main(String[] args) {
// Creating a Box to hold Integer type
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123); // No need to cast
System.out.println("Integer Value: " + integerBox.getItem());
// Creating a Box to hold String type
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello Generics");
System.out.println("String Value: " + stringBox.getItem());
}
}
Explanation:
• Box<T>: This declares a generic class where T is a placeholder for the type that the class
will operate on. T can be any reference type.
• setItem(T item): This method sets the item of type T.
• getItem(): This method returns the item of type T.
• Box<Integer> integerBox = new Box<>();: Creates a Box object that will only hold
Integer types.
• Box<String> stringBox = new Box<>();: Creates a Box object that will only hold String
types.
Generic Method Example
You can also create generic methods. A generic method is a method that can handle any data
type.
Example: A Generic Method to Compare Two Values
// A generic method to compare two values of any type
public class GenericMethodExample {
// Method definition with a type parameter <T>
public static <T> boolean areEqual(T first, T second) {
return first.equals(second);
}
public static void main(String[] args) {
// Comparing two integers
boolean result1 = areEqual(10, 20);
System.out.println("Are 10 and 20 equal? " + result1); // false
// Comparing two strings
boolean result2 = areEqual("Hello", "Hello");
System.out.println("Are 'Hello' and 'Hello' equal? " + result2); // true
}
}
Explanation:
• <T>: Declares a generic method. The <T> before the return type means that this method
can accept any type T.
• areEqual(T first, T second): This method takes two parameters of the same type T and
returns whether they are equal.
• areEqual(10, 20): Compares two integers.
• areEqual("Hello", "Hello"): Compares two strings.
Bounded Types in Generics
You can restrict the types that can be used as arguments in a generic class or method.
Example: Bounded Type Parameter
// A generic class with a bounded type parameter T
public class NumericBox<T extends Number> {
private T number;
public void setNumber(T number) {
this.number = number;
}
public T getNumber() {
return number;
}
public static void main(String[] args) {
NumericBox<Integer> intBox = new NumericBox<>();
intBox.setNumber(10); // Works, as Integer is a subclass of Number
NumericBox<Double> doubleBox = new NumericBox<>();
doubleBox.setNumber(5.5); // Works, as Double is a subclass of Number
// NumericBox<String> stringBox = new NumericBox<>(); // Compilation Error
}
}
Explanation:
1. NumericBox<T extends Number>: Restricts T to be a subclass of Number. This means T
can only be Integer, Double, Float, etc., but not String.
2. NumericBox<Integer> intBox = new NumericBox<>();: Works, as Integer is a Number.
3. NumericBox<String> stringBox = new NumericBox<>();: Causes a compilation error
because String is not a Number.
Generic Types in Java
Java generics provide a way to create classes, interfaces, and methods that can operate on
different data types while providing compile-time type safety. They are a powerful feature to
create more flexible and reusable code.
Types of Generics
4. Generic Classes
5. Generic Methods
6. Bounded Type Parameters
7. Wildcard Parameters
Let’s go through each of these in detail with examples.
1. Generic Classes
A generic class is defined with a type parameter in angle brackets <T> immediately following
the class name. This type parameter can be used in the class to define attributes, methods, and
constructors.
Example: A Simple Generic Class
// Defining a generic class with type parameter T
public class Container<T> {
private T value;
// Constructor to set value
public Container(T value) {
this.value = value;
}
// Method to get value
public T getValue() {
return value;
}
// Method to set value
public void setValue(T value) {
this.value = value;
}
public static void main(String[] args) {
// Creating a Container to hold Integer
Container<Integer> intContainer = new Container<>(100);
System.out.println("Integer Value: " + intContainer.getValue());
// Creating a Container to hold String
Container<String> stringContainer = new Container<>("Hello Generics");
System.out.println("String Value: " + stringContainer.getValue());
}
}
Explanation:
• Container<T>: T is a placeholder for the type that will be specified when an object of
this class is created.
• intContainer: This is a Container that holds Integer values.
• stringContainer: This is a Container that holds String values.
• This allows the same class Container to store different types without casting.
2. Generic Methods
A generic method is a method that can operate on any type, specified by the caller at runtime. It
is defined with a type parameter <T> before the return type.
Example: A Generic Method to Print Array Elements
// Class with a generic method
public class GenericMethodExample {
// Generic method that prints array elements
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Using the generic method to print Integer array
Integer[] intArray = {1, 2, 3, 4};
printArray(intArray); // Output: 1 2 3 4
// Using the generic method to print String array
String[] stringArray = {"A", "B", "C"};
printArray(stringArray); // Output: A B C
}
}
Explanation:
• <T>: This declares the method as a generic method with T as the type parameter.
• printArray(T[] array): This method can take an array of any type T and print its
elements.
• The method is used to print both Integer and String arrays without any changes to the
method itself.
3. Bounded Type Parameters
Sometimes you want to restrict the types that can be used as arguments for a type parameter.
This is called a bounded type parameter. It is done using the extends keyword.
Example: Bounded Type Parameter
// Generic class with bounded type parameter
public class NumericBox<T extends Number> {
private T number;
// Constructor to set number
public NumericBox(T number) {
this.number = number;
}
// Method to get the double value of the number
public double doubleValue() {
return number.doubleValue();
}
public static void main(String[] args) {
// NumericBox with Integer
NumericBox<Integer> intBox = new NumericBox<>(10);
System.out.println("Double Value of Integer: " + intBox.doubleValue()); // Output: 10.0
// NumericBox with Double
NumericBox<Double> doubleBox = new NumericBox<>(5.5);
System.out.println("Double Value of Double: " + doubleBox.doubleValue()); // Output: 5.5
// NumericBox<String> stringBox = new NumericBox<>("Hello"); // Error: String is not a
subclass of Number
}
}
Explanation:
• <T extends Number>: This restricts T to be a subclass of Number, so you can only use
numeric types like Integer, Double, etc.
• This ensures that methods like doubleValue() are available for all types T.
4. Wildcard Parameters
Wildcards are used in situations where you don't know the exact type. They are represented by a
question mark ? and are useful in method arguments to make code more flexible.
Types of Wildcards
• Unbounded Wildcard ?: Represents any type.
• Bounded Wildcards:
o Upper Bounded Wildcard ? extends T: Represents any type that is a subclass of
T.
o Lower Bounded Wildcard ? super T: Represents any type that is a superclass of
T.
Example: Using Wildcards
import java.util.List;
public class WildcardExample {
// Method with an upper bounded wildcard
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.print(n + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
// Works with both Integer and Double because they are subclasses of Number
printNumbers(intList); // Output: 1 2 3
printNumbers(doubleList); // Output: 1.1 2.2 3.3
}
}
Explanation:
• List<? extends Number>: This allows the method to accept a list of any type that
extends Number (e.g., Integer, Double).
• This flexibility is useful when you need to process a group of related types.
Summary
• Generic Classes: Allow you to create classes that work with any type.
• Generic Methods: Allow you to create methods that work with any type.
• Bounded Type Parameters: Restrict the types used in generics to a certain range (like
subclasses of a certain class).
• Wildcard Parameters: Provide flexibility in working with unknown or related types.
Parameterized Types in Java
Parameterized types refer to using generics in classes, interfaces, or methods where a specific
type is passed as a parameter. This type parameter is represented by a placeholder, like T, E, K,
V, etc., and allows a class, interface, or method to operate on different data types without
sacrificing type safety.
When you define a class, interface, or method with a type parameter, you are creating a generic
template. When you use this template, you specify the actual data type, thus creating a
parameterized type.
Why Use Parameterized Types?
• Type Safety: Ensures that only the specified type can be used, reducing the chances of
runtime errors.
• Code Reusability: Allows you to use the same code with different data types.
• Readability: Makes the code more understandable by explicitly stating the type it works
with.
Example of Parameterized Types
Generic Class Example
Let's start with a generic class example.
// Defining a generic class with parameterized type T
public class Pair<T, U> {
private T first;
private U second;
// Constructor to initialize both elements
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
// Getter for the first element
public T getFirst() {
return first;
}
// Getter for the second element
public U getSecond() {
return second;
}
public static void main(String[] args) {
// Creating a Pair with Integer and String types
Pair<Integer, String> pair1 = new Pair<>(1, "One");
System.out.println("First: " + pair1.getFirst() + ", Second: " + pair1.getSecond());
// Creating a Pair with String and Double types
Pair<String, Double> pair2 = new Pair<>("Two", 2.0);
System.out.println("First: " + pair2.getFirst() + ", Second: " + pair2.getSecond());
}
}
Explanation:
• Pair<T, U>: A generic class with two type parameters T and U. This class can work with
any two types.
• pair1: A Pair object with Integer as the first type and String as the second type.
• pair2: A Pair object with String as the first type and Double as the second type.
• This way, the same Pair class can be used to store different combinations of data types.
Method Example with Parameterized Types
You can also use parameterized types in generic methods.
// Class with a generic method
public class Utility {
// A generic method that swaps elements in an array
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4};
System.out.println("Before Swap: ");
for (int num : intArray) {
System.out.print(num + " ");
}
// Swapping elements at index 1 and 3
swap(intArray, 1, 3);
System.out.println("\nAfter Swap: ");
for (int num : intArray) {
System.out.print(num + " ");
}
}
}
Explanation:
• <T>: Declares that swap is a generic method with type parameter T.
• swap(T[] array, int i, int j): This method can swap elements in an array of any type.
• The swap method is used to swap elements in an Integer array, but it can also be used for
String, Double, or any other type of array.
Using Parameterized Types in Collections
The most common use of parameterized types is in the Java Collections Framework.
Example: Using Parameterized Types in Collections
import java.util.ArrayList;
import java.util.List;
public class CollectionExample {
public static void main(String[] args) {
// Creating a list of Strings
List<String> stringList = new ArrayList<>();
stringList.add("Apple");
stringList.add("Banana");
stringList.add("Cherry");
// Iterating through the list
for (String fruit : stringList) {
System.out.println(fruit);
}
// Creating a list of Integers
List<Integer> intList = new ArrayList<>();
intList.add(10);
intList.add(20);
intList.add(30);
// Iterating through the list
for (Integer number : intList) {
System.out.println(number);
}
}
}
Explanation:
• List<String>: A parameterized type where the List holds String objects.
• List<Integer>: A parameterized type where the List holds Integer objects.
• This ensures type safety and removes the need for type casting when retrieving elements
from the list.
Bounded Parameterized Types
You can restrict the types that can be used as arguments in a parameterized type using bounded
type parameters.
Example: Bounded Parameterized Type
// A generic class with a bounded parameterized type
public class NumberBox<T extends Number> {
private T number;
// Constructor to set the number
public NumberBox(T number) {
this.number = number;
}
// Method to return the double value of the number
public double doubleValue() {
return number.doubleValue();
}
public static void main(String[] args) {
// Creating a NumberBox with Integer type
NumberBox<Integer> intBox = new NumberBox<>(10);
System.out.println("Double value of Integer: " + intBox.doubleValue());
// Creating a NumberBox with Double type
NumberBox<Double> doubleBox = new NumberBox<>(5.5);
System.out.println("Double value of Double: " + doubleBox.doubleValue());
// NumberBox<String> stringBox = new NumberBox<>("Hello"); // Compilation Error
}
}
Explanation:
• <T extends Number>: Restricts T to subclasses of Number, like Integer, Double, etc.
• This ensures that methods like doubleValue() can be called on T.
Summary
• Parameterized Types allow classes, interfaces, and methods to be more flexible and
reusable by specifying a type parameter.
• They improve code readability and maintainability by clearly stating the types being
used.
• They enhance type safety by preventing runtime type errors.
• Common examples include generic collections (like List<String>) and utility classes or
methods that can operate on various types.
Wildcards in Java
Wildcards in Java generics are special symbols that represent unknown types. They are used to
increase the flexibility of code by allowing methods and classes to operate on different types
while still maintaining type safety. Wildcards are represented by the ? symbol and can be used in
various contexts such as method parameters, return types, and variable declarations.
Why Use Wildcards?
• Flexibility: Wildcards allow methods to accept a range of types rather than a specific
one.
• Readability: They make the intent of the code clearer when a method or class can work
with a variety of types.
• Reusability: Increase code reusability by allowing more general operations on
collections of objects.
Types of Wildcards
• Unbounded Wildcard <?>
• Upper Bounded Wildcard <? extends T>
• Lower Bounded Wildcard <? super T>
Let's look at each type in detail with examples.
1. Unbounded Wildcard <?>
The unbounded wildcard <?> is used when the exact type is unknown or irrelevant. It represents
any type.
Example: Using an Unbounded Wildcard
import java.util.List;
public class UnboundedWildcardExample {
// Method to print elements of any List
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
// List of Integers
List<Integer> intList = List.of(1, 2, 3, 4);
printList(intList); // Prints: 1 2 3 4
// List of Strings
List<String> stringList = List.of("A", "B", "C");
printList(stringList); // Prints: A B C
}
}
Explanation:
• List<?>: The method printList can accept a List of any type (Integer, String, etc.).
• The wildcard <?> indicates that the method can handle any list, regardless of its element
type.
2. Upper Bounded Wildcard <? extends T>
The upper bounded wildcard <? extends T> restricts the unknown type to be a subtype of T (or T
itself). This is useful when you want to read from a structure without modifying it.
Example: Using an Upper Bounded Wildcard
import java.util.List;
public class UpperBoundedWildcardExample {
// Method to calculate the sum of a list of numbers
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
public static void main(String[] args) {
// List of Integers
List<Integer> intList = List.of(1, 2, 3, 4);
System.out.println("Sum of Integers: " + sumOfList(intList)); // Output: 10.0
// List of Doubles
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
System.out.println("Sum of Doubles: " + sumOfList(doubleList)); // Output: 6.6
}
}
Explanation:
• List<? extends Number>: The method sumOfList accepts a list of any type that is
a subclass of Number, such as Integer, Double, Float, etc.
• This allows the method to work with different numeric types while ensuring they all have
a doubleValue() method.
3. Lower Bounded Wildcard <? super T>
The lower bounded wildcard <? super T> restricts the unknown type to be a superclass of T
(or T itself). This is useful when you want to add elements to a structure.
Example: Using a Lower Bounded Wildcard
import java.util.List;
import java.util.ArrayList;
public class LowerBoundedWildcardExample {
// Method to add numbers to a list of any supertype of Integer
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
public static void main(String[] args) {
// List of Numbers
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println("Number List: " + numberList); // Output: [1, 2, 3]
// List of Objects
List<Object> objectList = new ArrayList<>();
addNumbers(objectList);
System.out.println("Object List: " + objectList); // Output: [1, 2, 3]
}
}
Explanation:
• List<? super Integer>: The method addNumbers accepts a list of any type that is
a superclass of Integer, such as Number or Object.
• This allows adding Integer elements to the list, while still being flexible enough to
accommodate various types of lists.
Guidelines for Choosing Wildcards
• Use <?>: When you don’t care about the type and are only reading from a collection.
o Example: void printList(List<?> list)
• Use <? extends T>: When you want to read from a collection of objects of type T or
its subclasses, and don't need to modify the collection.
o Example: double sumOfList(List<? extends Number> list)
• Use <? super T>: When you want to write to a collection of objects of type T or its
superclasses.
o Example: void addNumbers(List<? super Integer> list)
Lambda Expressions in Java
Lambda expressions are a powerful feature introduced in Java 8 that enable you to write more
concise and readable code, especially when working with functional interfaces and the Java
Streams API. They allow you to treat functionality as a method argument, or pass a block of
code around, making your code more flexible and easier to maintain.
What is a Lambda Expression?
A lambda expression is essentially an anonymous function that can be treated as a single
method interface instance. It consists of three parts:
• Parameters: Input to the lambda expression.
• Arrow Token (->): Separates the parameters from the body.
• Body: The code that is executed.
Syntax of Lambda Expressions
(parameters) -> { body }
• Parameters: Can be zero or more.
• Arrow Token (->): Separates parameters and body.
• Body: Can be a single expression or a block of code.
Why Use Lambda Expressions?
• Conciseness: Reduce boilerplate code, making it shorter and easier to read.
• Readability: Code becomes more expressive and easier to understand.
• Functional Programming: Supports functional programming paradigms, enabling
operations like map, filter, and reduce.
• Enhanced APIs: Simplifies the use of APIs like Java Streams.
Functional Interfaces
A functional interface is an interface with exactly one abstract method. Lambda
expressions work seamlessly with functional interfaces.
Example of a Functional Interface
@FunctionalInterface
public interface Greeting {
void sayHello(String name);
Example 1: Basic Lambda Expression
Let's start with a simple example using the Greeting functional interface.
Without Lambda Expression
public class LambdaExample {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
@Override
public void sayHello(String name) {
System.out.println("Hello, " + name);
};
greeting.sayHello("Alice");
With Lambda Expression
public class LambdaExample {
public static void main(String[] args) {
// Using a lambda expression to implement the Greeting interface
Greeting greeting = (name) -> System.out.println("Hello, " + name);
greeting.sayHello("Alice");
}
}
Explanation:
• Without Lambda: Requires creating an anonymous inner class and overriding the
sayHello method.
• With Lambda: Provides a more concise way to implement the sayHello method.
Example 2: Lambda Expressions with Parameters
Functional Interface with Multiple Parameters
@FunctionalInterface
public interface MathOperation {
int operate(int a, int b);
Using Lambda Expressions
public class LambdaParametersExample {
public static void main(String[] args) {
// Addition
MathOperation addition = (a, b) -> a + b;
System.out.println("Addition: " + addition.operate(5, 3)); // Output: 8
// Subtraction
MathOperation subtraction = (a, b) -> a - b;
System.out.println("Subtraction: " + subtraction.operate(5, 3)); // Output: 2
// Multiplication
MathOperation multiplication = (a, b) -> a * b;
System.out.println("Multiplication: " + multiplication.operate(5, 3)); // Output: 15
// Division
MathOperation division = (a, b) -> a / b;
System.out.println("Division: " + division.operate(6, 3)); // Output: 2
Explanation:
• Each lambda expression implements the operate method with different
operations.
• The syntax (a, b) -> a + b represents a method that takes two integers and
returns their sum.
Example 3: Lambda Expressions with No Parameters
Functional Interface with No Parameters
@FunctionalInterface
public interface Printer {
void print();
Using Lambda Expressions
public class LambdaNoParameterExample {
public static void main(String[] args) {
// Using a lambda expression with no parameters
Printer printer = () -> System.out.println("Printing without parameters!");
printer.print();
}
Explanation:
• The lambda expression () -> System.out.println("...") indicates that the
print method takes no arguments and executes the provided code.
Example 4: Using Lambda Expressions with Java Collections
Lambda expressions are particularly useful when working with collections, especially with
methods like forEach, map, filter, etc.
Example: Iterating Over a List
import java.util.Arrays;
import java.util.List;
public class LambdaCollectionExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// Using lambda expression with forEach
names.forEach(name -> System.out.println("Hello, " + name + "!"));
Explanation:
• The forEach method accepts a Consumer functional interface, which can be
implemented using a lambda expression.
• The lambda name -> System.out.println("Hello, " + name + "!") prints
a greeting for each name in the list.
Best Practices for Using Lambda Expressions
• Use Functional Interfaces: Ensure you're using or creating appropriate functional
interfaces.
• Keep Lambdas Simple: For readability, keep lambda expressions concise. If the
logic is complex, consider using a method reference or a regular method.
• Prefer Method References When Possible: They can make the code even more
readable.
o Example: String::toUpperCase instead of s -> s.toUpperCase().
• Avoid Side Effects: Lambdas should ideally be stateless and free from side effects
to maintain predictability.
• Use Meaningful Variable Names: Even though lambdas can be concise, using
clear variable names helps in understanding the code.
Lambda Type Inference in Java
In Java, type inference refers to the compiler's ability to automatically deduce the types of
parameters or return values in a lambda expression, rather than explicitly specifying them.
With lambda expressions, type inference helps reduce boilerplate code, making it more
concise and readable.
The type of a lambda expression is inferred from the context in which it is used,
specifically from the target type, which is usually a functional interface.
How Does Lambda Type Inference Work?
Lambda expressions in Java work closely with functional interfaces, which have a single
abstract method. When a lambda is assigned to a variable or passed to a method that
expects a functional interface, the compiler can infer the types of the lambda's parameters
and return value based on the target interface's method signature.
Example of Type Inference in a Lambda Expression
Consider the following functional interface:
@FunctionalInterface
interface Greeting {
void sayHello(String name);
}
When using this interface in a lambda expression:
Greeting greeting = (name) -> System.out.println("Hello, " + name);
In this example:
• The lambda expression name -> System.out.println("Hello, " + name) is
inferred to match the sayHello method in the Greeting interface.
• The parameter name is inferred to be of type String because the sayHello method
has a parameter of type String.
Here, type inference allows us to omit specifying String name explicitly.
Syntax with and without Type Inference
Without Type Inference (Explicit Types):
Greeting greeting = (String name) -> System.out.println("Hello, " + name);
With Type Inference:
Greeting greeting = (name) -> System.out.println("Hello, " + name);
oth versions are valid, but type inference helps simplify the code by allowing the omission
of explicit parameter types.
Where Does Java Infer Types?
Java can infer types in several places when working with lambda expressions:
• Method Parameters: If a lambda expression is passed as an argument to a
method, Java can infer the types based on the method's signature.
• Generic Functional Interfaces: When working with generics, Java infers the types
based on how the functional interface is used.
• Assignment Context: When a lambda is assigned to a variable of a functional
interface, Java infers the types based on the interface's abstract method.
Example 1: Type Inference in Method Parameters
Consider this method that accepts a Predicate:
public static void filterNames(Predicate<String> predicate) {
// Implementation
}
When calling the method, you can pass a lambda expression like this:
filterNames(name -> name.startsWith("A"));
Here, Java infers that name is of type String because the Predicate interface is
parameterized with String.
Example 2: Type Inference with Generics
Consider the Comparator functional interface, which is a generic interface:
Comparator<String> stringComparator = (s1, s2) -> s1.compareTo(s2);
Here, Java infers that s1 and s2 are both of type String, since the lambda is assigned to a
Comparator<String>.
Lambda Parameters in Java
In Java, lambda expressions are a concise way to represent instances of functional
interfaces. These interfaces contain a single abstract method, and lambdas are used to
implement that method in a simpler syntax. The lambda parameters refer to the input
values that the lambda expression receives, which are passed to the functional interface's
abstract method.
Syntax of Lambda Expression
A lambda expression typically consists of:
• Parameters: These are enclosed in parentheses, similar to method parameters.
• Arrow token ->: This separates the parameters from the body of the lambda
expression.
• Body: The code that implements the functional interface method. It can either be a
single statement or a block of multiple statements.
(parameters) -> expression or { statements }
Example of Lambda Expression with Parameters
Consider a functional interface Calculator:
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Now, using a lambda expression to implement the add method:
Calculator calc = (a, b) -> a + b;
System.out.println(calc.add(5, 3)); // Output: 8
Here:
• The parameters a and b are inferred to be int because the method signature of add
expects two int arguments.
• The body of the lambda expression adds the two parameters.
Lambda Parameter Syntax
Lambda parameters can be specified in several ways, depending on the situation. Let's
explore different cases.
1. No Parameters
If the method in the functional interface does not take any parameters, the lambda
expression simply uses empty parentheses.
Example: No Parameter Lambda
@FunctionalInterface
interface Greet {
void sayHello();
}
Greet greeting = () -> System.out.println("Hello, World!");
greeting.sayHello(); // Output: Hello, World!
Here, the lambda has no parameters and simply prints a message.
2. Single Parameter (With or Without Parentheses)
When the functional interface method has one parameter, parentheses can be omitted
around the parameter if its type is inferred.
Example: Single Parameter Lambda (Without Parentheses)
@FunctionalInterface
interface Printer {
void print(String message);
}
Printer printer = message -> System.out.println(message);
printer.print("Hello, Lambda!"); // Output: Hello, Lambda!
Here:
• The lambda expression takes a single String parameter, message.
• Parentheses around the parameter are optional when there is only one parameter.
If you want, you can also include parentheses:
Example: Single Parameter Lambda (With Parentheses)
Printer printer = (message) -> System.out.println(message);
3. Multiple Parameters
When the lambda expression has more than one parameter, they must be enclosed in
parentheses, and they are separated by commas.
Example: Multiple Parameters Lambda
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
MathOperation addition = (a, b) -> a + b;
MathOperation multiplication = (a, b) -> a * b;
System.out.println(addition.operate(10, 5)); // Output: 15
System.out.println(multiplication.operate(10, 5)); // Output: 50
Here:
• The lambda expressions take two parameters, a and b.
• Parentheses are mandatory when there are multiple parameters.
4. Explicit Parameter Types
In many cases, Java infers the parameter types based on the target functional interface.
However, you can explicitly declare the types if you wish.
Example: Lambda with Explicit Parameter Types
MathOperation addition = (int a, int b) -> a + b;
Explicit parameter types are not usually necessary, but they might be helpful in cases
where type inference does not work or for clarity.
Lambda Function Body in Java
The function body of a lambda expression in Java defines the actions or logic that is
executed when the lambda expression is invoked. The body of a lambda can be either a
single expression or a block of multiple statements enclosed in curly braces {}.
The structure of a lambda expression is as follows:
(parameters) -> { function body }
Types of Lambda Function Bodies
There are two main types of lambda function bodies in Java:
8. Single Expression Body: This is the simplest form of a lambda expression. It
consists of a single expression, and the result of the expression is automatically
returned.
9. Block Body: This type of lambda function body consists of multiple statements
enclosed within curly braces {}. If the lambda is supposed to return a value, the
return statement is required inside a block body.
1. Single Expression Body
In this type, the lambda body consists of a single expression, and the result of that
expression is implicitly returned without needing an explicit return keyword.
Syntax
(parameters) -> expression
Example: Single Expression Lambda
@FunctionalInterface
interface Square {
int calculate(int x);
}
Square square = (x) -> x * x;
System.out.println(square.calculate(5)); // Output: 25
Here:
• The lambda body (x) -> x * x contains a single expression (x * x).
• The result of the expression is automatically returned by the lambda.
2. Block Body
A block body consists of one or more statements. When using a block body, you must use
curly braces {} and, if the lambda returns a value, the return statement is required.
Syntax
(parameters) -> {
// multiple statements
return result; // required if the function returns a value
}
Example: Block Lambda with Multiple Statements
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Calculator addition = (a, b) -> {
System.out.println("Adding " + a + " and " + b);
return a + b;
};
System.out.println(addition.add(10, 20)); // Output: Adding 10 and 20, 30
Here:
• The lambda expression (a, b) -> { ... } contains multiple statements: a
System.out.println() call and a return statement.
• Since the body contains multiple statements, curly braces {} are used, and return
is required to return the result of the addition.
Important Rules for Lambda Function Body
10. Single Expression:
o The result is returned implicitly (no return keyword needed).
o Parentheses {} are not required.
11. Block Body:
o Must be enclosed in curly braces {}.
o If the lambda returns a value, the return keyword must be used.
Example: Lambda Function with Different Function Bodies
Example 1: Single Expression Lambda
@FunctionalInterface
interface Greeter {
String greet(String name);
}
Greeter greeter = name -> "Hello, " + name;
System.out.println(greeter.greet("Rashmi")); // Output: Hello, Rashmi
In this example:
• The lambda expression name -> "Hello, " + name consists of a single
expression.
• The result of the expression ("Hello, " + name) is implicitly returned.
Example 2: Block Lambda with Multiple Statements
Greeter formalGreeter = name -> {
System.out.println("Preparing a formal greeting...");
return "Good evening, " + name;
};
System.out.println(formalGreeter.greet("Rashmi")); // Output: Preparing a formal
greeting... Good evening, Rashmi
Here:
• The lambda body has multiple statements, so it uses a block ({}).
• Since there are multiple statements, the return keyword is explicitly used to return
the result.
Single-Line vs. Multi-Line Lambda Expression
12. Single-line Lambda (Expression Body):
o Used when the lambda has a single statement that returns a value.
o No need for curly braces or return keyword.
Example:
Square square = (x) -> x * x;
Multi-line Lambda (Block Body):
• Used when the lambda has more than one statement.
• Curly braces and return keyword (if applicable) are required.
Example:
Square square = (x) -> {
System.out.println("Calculating square of " + x);
return x * x;
};
Example: Block Lambda with Complex Logic
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
MathOperation complexOperation = (a, b) -> {
if (a > b) {
return a - b;
} else {
return a + b;
}
};
System.out.println(complexOperation.operate(10, 5)); // Output: 5
System.out.println(complexOperation.operate(3, 7)); // Output: 10
In this example:
• The lambda body contains a conditional if-else statement, making the function
body more complex.
• Since the body has multiple statements, curly braces {} are used, and the return
keyword is required.
Returning a Value from a Lambda Expression in Java
In Java, lambda expressions can return values just like methods. Whether the lambda
returns a value implicitly or explicitly depends on the type of lambda expression body:
single expression or block body.
13. Single Expression: In this case, the value is returned implicitly without needing the
return keyword.
14. Block Body: If a lambda has multiple statements, the return keyword must be
used explicitly to return a value.
1. Returning a Value from a Single Expression Lambda
A lambda expression that contains a single expression automatically returns the result of
that expression. There is no need to explicitly use the return keyword.
Example: Single Expression Lambda
@FunctionalInterface
interface Multiply {
int multiply(int a, int b);
}
Multiply multiplier = (a, b) -> a * b;
int result = multiplier.multiply(5, 3);
System.out.println(result); // Output: 15
Here:
• The lambda (a, b) -> a * b is a single expression, and it automatically returns
the product of a and b.
• No return keyword is needed.
2. Returning a Value from a Block Body Lambda
When the lambda body consists of multiple statements, you must use the return keyword
to explicitly return a value. If the lambda returns a value, the body needs to be enclosed in
curly braces {}.
Example: Block Body Lambda
@FunctionalInterface
interface Addition {
int add(int a, int b);
}
Addition adder = (a, b) -> {
System.out.println("Adding " + a + " and " + b);
return a + b;
};
int sum = adder.add(10, 20);
System.out.println(sum); // Output: Adding 10 and 20, 30
Here:
• The lambda (a, b) -> { ... } has multiple statements: it prints a message and
then returns the sum of a and b.
• Since there are multiple statements, curly braces {} are used, and the return
keyword is required to return the result.
Returning Values from Lambda with Conditional Logic
You can also return values conditionally using if-else statements or other control flow
structures.
Example: Lambda with Conditional Logic
@FunctionalInterface
interface Compare {
String compareNumbers(int a, int b);
}
Compare comparator = (a, b) -> {
if (a > b) {
return a + " is greater than " + b;
} else if (a < b) {
return a + " is less than " + b;
} else {
return a + " is equal to " + b;
}
};
System.out.println(comparator.compareNumbers(10, 5)); // Output: 10 is greater than 5
System.out.println(comparator.compareNumbers(2, 8)); // Output: 2 is less than 8
System.out.println(comparator.compareNumbers(4, 4)); // Output: 4 is equal to 4
In this example:
• The lambda (a, b) -> { ... } uses conditional logic to return different strings
based on the comparison of a and b.
Lambdas as Objects in Java
In Java, lambda expressions are a way to create instances of functional interfaces in a
concise way. Even though lambdas are often used as syntactic sugar to implement
functional interfaces, under the hood, they are treated as objects.
Lambdas in Java are treated as instances of functional interfaces and can be assigned to
variables, passed as arguments, or returned from methods just like objects. They are not
regular objects themselves but can be referenced as objects through functional interfaces,
allowing them to behave similarly to objects in most scenarios.
Key Concepts
• Functional Interface: A lambda expression is an implementation of a functional
interface, which is an interface that has exactly one abstract method.
• Object Representation: Internally, lambda expressions are instances of an
anonymous class that implements the functional interface. They are treated as
objects implementing that interface.
• Behavior as Objects: Lambda expressions can be assigned to variables, passed to
methods, returned from methods, and stored in data structures just like any object.
Example of Lambdas as Objects
Consider a simple functional interface, Calculator, with one abstract method:
@FunctionalInterface
interface Calculator {
int operate(int a, int b);
}
You can create a lambda expression that implements this functional interface:
Calculator addition = (a, b) -> a + b;
int result = addition.operate(5, 3);
System.out.println(result); // Output: 8
In this case:
• The lambda expression (a, b) -> a + b is treated as an object that implements
the Calculator interface.
• The lambda is invoked by calling the operate method on the addition object.
Assigning Lambdas to Variables
Just like objects, lambda expressions can be assigned to variables:
Calculator subtraction = (a, b) -> a - b;
System.out.println(subtraction.operate(10, 5)); // Output: 5
Here, the lambda (a, b) -> a - b is assigned to the subtraction variable, and it's
treated like an object implementing the Calculator interface.
Passing Lambdas as Method Arguments
Since lambdas are objects, they can be passed as arguments to methods that accept
functional interfaces.
Example: Passing Lambda as a Method Argument
public static int performOperation(Calculator calculator, int x, int y) {
return calculator.operate(x, y);
}
Calculator multiply = (a, b) -> a * b;
int result = performOperation(multiply, 4, 5);
System.out.println(result); // Output: 20
Here:
• The lambda (a, b) -> a * b is passed as an argument to the
performOperation method, which accepts a Calculator functional interface.
Returning Lambdas from Methods
Lambdas can also be returned from methods, just like objects.
Example: Returning Lambda from a Method
public static Calculator createCalculator() {
return (a, b) -> a + b; // Return a lambda expression
}
Calculator calculator = createCalculator();
System.out.println(calculator.operate(7, 3)); // Output: 10
In this example:
• The method createCalculator() returns a lambda expression, which is assigned
to the calculator variable. It behaves like an object that implements the
Calculator interface.
Treating Lambdas as Object Type
While lambda expressions can be treated as instances of functional interfaces, they
cannot be directly cast to Object or assigned to an Object variable without losing their
functional interface behavior.
Example: Casting Lambda to Object
Object obj = (Calculator) (a, b) -> a + b; // Lambda expression assigned as an object
However, you can't call the lambda's method directly from the Object reference. The
lambda needs to be cast back to its functional interface type to be used.
Calculator calc = (Calculator) obj;
System.out.println(calc.operate(10, 20)); // Output: 30