Day 6: Collection

It is 30 minutes to 8 in the night, dated 1st of June.
Let’s dive into Collection!
Collections API
Any group of individual objects that are represented as a single unit is known as a Java Collection of Objects.
In Java, a separate framework named the "Collection Framework" has been defined in JDK 1.2 which holds all the Java Collection Classes and Interface in it.
In Java, the Collection interface (java.util.Collection) and Map interface (java.util.Map) are the two main “root” interfaces of Java collection classes.
Need for Collection Framework
Before the Collection Framework(or before JDK 1.2) was introduced, the standard methods for grouping Java objects (or collections) were Arrays or Vectors, or Hashtables.
All of these collections had no common interface. Therefore, though the main aim of all the collections is the same, the implementation of all these collections was defined independently and had no correlation among them.
And also, it is very difficult for the users to remember all the different methods, syntax, and constructors present in every collection class.
Advantages of Java Collection Framework
Consistent API
Reduces Programming Effort
Hierarchy in Collection Framework
Collection vs Collections
Collection is an Interface, while Collections is an utility class.
Collection interface is extended by different interfaces like List, Set and Queue, which are then implemented by classes like
ArrayList, Vector, Stack, LinkedList → List
HashSet, LinkedHashSet → Set
PriorityQueue → Queue
ArrayDeque → Deque
While Collections is a class which has methods (like shuffle( ), reverse( ), sort( )) to work with objects of these classes.
Collection Framework has Map Interface which does not extend Collection interface but their design and working is similar and thus included with Collection.
ArrayList
A resizable array implementation of the List interface that maintains insertion order.
Allows duplicate elements and supports random access using indices.
Common methods: .add(), .get(), .remove(), .size(), .sort(), .forEach().
import java.util.ArrayList;
import java.util.Comparator;
public class Demo{
public static void main(String[] args) {
ArrayList<String> weaponTier = new ArrayList<>();
weaponTier.add("F Class");
weaponTier.add("S Class");
weaponTier.add("A Class");
weaponTier.add("G Class");
weaponTier.forEach(System.out::println);
weaponTier.sort(
new Comparator<String>() {
public int compare(String o1, String o2){
return o1.compareTo(o2);
}
});
System.out.println(weaponTier);
System.out.println(weaponTier.get(2));
}
}
//Output
F Class
S Class
A Class
G Class
[A Class, F Class, G Class, S Class]
G Class
Here we observe:
.add( ) is used to add the elements to the ArrayList weaponTier.
.forEach( ) accesses each element which is then used to print.
System.out :: println is a method reference which means that println method of System.in be applied.
.sort( ) method needs a Comparator which defines how to compare the elements in its compare( ) method. Hence, we need to define it how to behave.
.compareTo( ) method returns -1 if the string is lexicographically smaller or 1 if its lexicographically larger or 0 if its the same.
ArrayLists support indexing, get( ) is used to get the element of that index.
Even in compare ( ) method of Comparator we need to return -1 if no swapping is required, 1 if swap is required or 0 if both values are same. Thus, we end up returning just the compareTo value.
Hence, instead of creating an anonymous object of Comparator we can shorten it to:
weaponTier.sort(String::compareTo);
HashSet
Implements the Set interface and stores unique elements only (no duplicates).
Does not maintain any order of insertion.
Common methods: .add(), .remove(), .contains(), .size(), .addAll().
import java.util.ArrayList;
import java.util.HashSet;
public class Demo{
public static void main(String[] args) {
HashSet<String> seenMonsters = new HashSet<String>();
seenMonsters.add("Slime");
seenMonsters.add("Red Wolf");
seenMonsters.add("Slime");
seenMonsters.add("Ogre");
ArrayList<String> dungeonMonster = new ArrayList<String>();
dungeonMonster.add("Jellyfish");
dungeonMonster.add("Shark");
seenMonsters.addAll(dungeonMonster);
Object cloneSeenMonsters = seenMonsters.clone();
for(String s: seenMonsters)
System.out.println(s);
System.out.println(seenMonsters.contains("Slime"));
System.out.println(seenMonsters.equals(cloneSeenMonsters));
}
}
//Output
Jellyfish
Red Wolf
Shark
Slime
Ogre
true
true
Here we observe:
HashSet values are always unique, therefore we got only one Slime element stored in seenMonsters.
.addAll ( ) adds any Collection (in this case ArrayList dungeonMonster) to the existing HashSet.
.clone ( ) returns an shallow copy of the HashSet as an Object.
.contains ( ) returns boolean value if a particular element is present.
.equals ( ) compares two objects and returns boolean value.
HashMap
Stores key-value pairs; keys must be unique, values can be duplicated.
Provides constant-time performance for basic operations like get and put.
Common methods: .put(), .get(), .containsKey(), .keySet(), .values(), .forEach().
import java.util.ArrayList;
import java.util.HashMap;
class UniqueArea{
private String areaName;
private String areaType;
public UniqueArea(String areaName, String areaType){
this.areaName = areaName;
this.areaType = areaType;
}
public ArrayList<String> getDetails(){
ArrayList<String> a = new ArrayList<String>();
a.add(areaName);
a.add(areaType);
return a;
}
}
class UniqueMonster{
private String monsterName;
public UniqueMonster(String monsterName){
this.monsterName = monsterName;
}
public String getMonsterName(){
return this.monsterName;
}
}
public class Demo{
public static void main(String[] args) {
UniqueArea iceLand = new UniqueArea("Frozen Land", "Ice");
UniqueArea forest = new UniqueArea("Terrainza", "Grass");
UniqueMonster icoolza = new UniqueMonster("Icoolza");
UniqueMonster blackMonkey = new UniqueMonster("Black Monkey");
HashMap<UniqueMonster, UniqueArea> monsterAreaMap = new HashMap<>();
monsterAreaMap.put(icoolza, iceLand);
monsterAreaMap.put(blackMonkey, forest);
monsterAreaMap.forEach((um, ua) ->
System.out.println(um.getMonsterName() + " : " + ua.getDetails()));
}
}
//Output
Black Monkey : [Terrainza, Grass]
Icoolza : [Frozen Land, Ice]
Stream
Used for processing collections in a functional, pipeline-based style. We can perform only one operation on a stream.
Supports operations like filtering, mapping, and reducing.
Common methods: .stream(), .filter(), .map(), .reduce(), .collect(), .forEach().
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,2,3,4,5,6);
Predicate<Integer> p = new Predicate<Integer>() {
public boolean test(Integer n){
return n%2==0;
}
};
Function<Integer, Integer> f = new Function<Integer, Integer>() {
public Integer apply(Integer n){
return n*2;
}
};
BinaryOperator<Integer> b = new BinaryOperator<Integer>() {
public Integer apply(Integer c, Integer e){
return c+e;
}
};
Stream<Integer> s1 = nums.stream();
Stream<Integer> s2 = s1.filter(p);
Stream<Integer> s3 = s2.map(f);
int result = s3.reduce(0, b);
System.out.println(result);
}
}
//Output
24
Here we observe:
.stream( ): Returns a sequential Stream with this collection as its source.
.filter( ): Returns a stream consisting of the elements of this stream that match the given predicate.
Predicate: Represents a predicate (boolean-valued function) of one argument. It just represents the test which filter method applies on the elements.
.map( ): Returns a stream consisting of the results of applying the given function to the elements of this stream.
Function: Represents a function that accepts one argument and produces a result.
.reduce( ): Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value.
Binary Operator or accumulation function: Represents an operation upon two operands of the same type, producing a result of the same type as the operands.'
The whole example can be reduced as follows:
import java.util.Arrays;
import java.util.List;
public class Demo{
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,2,3,4,5,6);
int result = nums.stream()
.filter(n -> n%2==0)
.map(n -> n*2)
.reduce(0, ((c,e) -> c+e));
System.out.println(result);
}
}
//Output
24
Here we observe:
Lambda expressions replaced verbose anonymous classes, making the code concise:
n -> n % 2 == 0
,n -> n * 2
, and(c, e) -> c + e
.
Stream chaining combines
stream → filter → map → reduce
in a single pipeline.
Some new features of Java
LVTI - Local Variable Type Interface
LVTI (Local Variable Type Inference) is a feature introduced in Java 10 using the var keyword. It allows the compiler to infer the type of a local variable from the context, making code cleaner and more concise.
Only works for local variables (inside methods, loops, etc.), not for class fields or parameters.
Improves readability for complex types but should be used carefully to maintain code clarity.
import java.util.Arrays;
public class Demo{
public static void main(String[] args) {
var nums = Arrays.asList(1,2,3,4,5,6);
System.out.println(nums);
}
}
//Output
[1, 2, 3, 4, 5, 6]
Sealed Class
Sealed Classes (introduced in Java 15, stable in Java 17) allow you to restrict which classes can extend or implement a class or interface.
Declared using the sealed keyword, followed by permits to specify allowed subclasses.
Subclasses must be marked as final, sealed, or non-sealed.
Helps in creating more controlled and secure class hierarchies, useful in domain modeling.
sealed class Magic permits Mage, Wizard{
}
non-sealed class Mage extends Magic{
}
final class Wizard extends Magic{
}
public class Demo{
public static void main(String[] args) {
}
}
Record Class
A record is a special type of class in Java used to model immutable data carriers.
It automatically generates the constructor,
getters
,equals()
,hashCode()
, andtoString()
methods.Records reduce boilerplate and are ideal when your class is just storing data.
They're ideal for DTOs, API responses, config objects, etc.
Below is an example of typical data carrier class:
import java.util.Arrays;
import java.util.List;
class CityFood{
private String cityName;
private List<String> foodItems;
public CityFood(String cityName, List<String> foodItems){
this.cityName = cityName;
this.foodItems = foodItems;
}
public String getCityName(){
return this.cityName;
}
public List<String> getFoodItems(){
return this.foodItems;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((cityName == null) ? 0 : cityName.hashCode());
result = prime * result + ((foodItems == null) ? 0 : foodItems.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CityFood other = (CityFood) obj;
if (cityName == null) {
if (other.cityName != null)
return false;
} else if (!cityName.equals(other.cityName))
return false;
if (foodItems == null) {
if (other.foodItems != null)
return false;
} else if (!foodItems.equals(other.foodItems))
return false;
return true;
}
@Override
public String toString() {
return "CityFood [cityName=" + cityName + ", foodItems=" + foodItems + "]";
}
}
public class Demo{
public static void main(String[] args) {
CityFood firstCity = new CityFood("First City", Arrays.asList("Apple Pie", "Banna Cherry drink"));
CityFood secondCity = new CityFood("Second City", Arrays.asList("Coconut halwa", "Lady finger margarita"));
System.out.println(firstCity + "\n" + secondCity);
System.out.println(firstCity.equals(secondCity));
}
}
//Output
CityFood [cityName=First City, foodItems=[Apple Pie, Banna Cherry drink]]
CityFood [cityName=Second City, foodItems=[Coconut halwa, Lady finger margarita]]
false
Below is the same class implemented as a record:
import java.util.Arrays;
import java.util.List;
record CityFood(String cityName, List<String> foodItems) { };
public class Demo{
public static void main(String[] args) {
CityFood firstCity = new CityFood("First City", Arrays.asList("Apple Pie", "Banna Cherry drink"));
CityFood secondCity = new CityFood("Second City", Arrays.asList("Coconut halwa", "Lady finger margarita"));
System.out.println(firstCity + "\n" + secondCity);
System.out.println(firstCity.equals(secondCity));
System.out.println(firstCity.cityName() + " : " + firstCity.foodItems());
}
}
//Output
CityFood[cityName=First City, foodItems=[Apple Pie, Banna Cherry drink]]
CityFood[cityName=Second City, foodItems=[Coconut halwa, Lady finger margarita]]
false
First City : [Apple Pie, Banna Cherry drink]
Note: We can add methods, static blocks, custom constructors inside a record class.
With that we come to an end with our Java Core part of our Java Journey!!
Our next adventure will be on JUnit!
Subscribe to my newsletter
Read articles from Nagraj Math directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
