Ultimate Guide to JAVA Collections

Bhavik Vashi
9 min readApr 23, 2024

--

Why do we need Collection Framework?

There are following data types supported in JAVA.

  1. Primitive Data-types: boolean, char, byte, short, int, long, float & double.
  • When you want to store the only single value of specific data type, then use primitive data type.
  • Example: person age, person name
int num1 = 10;
int num2 = 20;

int sum = num1 + num2;

System.out.println(sum);

2. Non-Primitive Data-types: Classes, Interfaces, Arrays, Collections

  • When you want to store the complex data type (combination of data type), Like Person (id, name, age) then use Class
  • When you want to store the collection of elements of specific data type, then use Array or Collection.

Array: -

→ Arrays are fixed-size data structure that store elements of same data-type sequentially in memory.

→ Arrays have fixed length, which means that you have to specify the size of array while declaration. Hence, in prior you have to determine the size of array.

→ Once you have specify that size of array, you can’t changes it size, if you change it, it will allocate the whole new space & its new array declaration.

→ Arrays are memory efficient than collection because they have a fixe size & does not incure the overhead of dynamic sizing.

int[] numbers = new int[5];

numbers[0] = 10;
numbers[1] = 11;
numbers[2] = 12;
numbers[3] = 13;
numbers[4] = 14;

for(int i = 0; i < numbers.length; i++){
System.out.println(numbers[i]);
}

Accessing / storing the element out of size will give an Exception

int[] numbers = new int[5];

numbers[0] = 10;
numbers[5] = 16;
// total size is 5, range is [index0, ...., index 4]
// updating the 6th element, 5th index => increasing the size of array to 6
// give an exception of ArrayIndexOutOfBoundsException

Collections: -

→ Collections are dynamic data structure they can grow or shrink their size dynamically.

→ Collections allows to store & manipulate the group of data effectively & efficiently.

→ Collections provides helper methods for sorting, searching & filtering.

List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

// in Collection we can store n number of elements
// no need to worry about size of it

Overall, we can say that use Collections when we need flexibility in managing n-number of elements or when you need it perform operations like adding, removing, searching elements effectively whereas use the array when you want to know the exact number of elements that you are going to store.

Hierarchy of Collection Framework
  • As we have seen collection framework allows to store the group of data & manipulate it effectively & efficiently.
  • Collection interface has following 3 types of Interface: I) List, II) Queue, III) Set
  • I) List: When we want to store group of elements & duplication of elements are allowed.
  • II) Queue: When we want to store group of elements & follow First-In-First-Out manner.
  • III) Set: When we want to store group of elements & duplications of elements are not allowed.

LIST

List is a collection interface which allows to store the group of elements & allows to store the duplicate elements.

We have 3 types of List: I) ArrayList, II) LinkedList & IV) Vector

ArrayList vs LinkedList

→ ArrayList internally uses the Array data structure to store the elements & increases it size by 50% when it size exceed to capacity. Whereas LinkedList uses the Doubly Linked List (each node has its value, prev node link, next node link).

→ ArrayList are more memory efficient because it store the elements in sequential manner. LinkedList are less memory efficient because it uses the Doubly LinkedList (data + 2 links).

→ ArrayList are faster in terms of iteration because data is stored in sequential manner. LinkedList is slower in terms of iteration because data is store with links.

→ In ArrayList data manipulation (insertion/ deletion/ updation) is slower because it leads to whole new array creation internally. Whereas LinkedList provides the faster data manipulation (just need to update links)

Array List Program: -

ArrayList<Integer> numbers = new ArrayList<>();
// insert: adding elements
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
System.out.println(numbers); // [10, 20, 30, 40]

// update
numbers.set(2, 50);
System.out.println(numbers); // [10, 20, 50, 40]

// read
Integer numAt2ndIndex = numbers.get(2);
System.out.println(numAt2ndIndex); // 50

// remove
numbers.remove(3); // remove by Index
System.out.println(numbers); // [10, 20, 50]
numbers.remove(new Integer(10)); // remove element by element
System.out.println(numbers); // [20, 50]

LinkedList Program: -

  • LinkedList maintains the links hence it provides the additional method to get/ update/ remove at the begging & end.
LinkedList<Integer> numbers = new LinkedList<>();
// insert: adding elements
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
System.out.println(numbers); // [10, 20, 30, 40]

// update
numbers.set(2, 50);
System.out.println(numbers); // [10, 20, 50, 40]

// read
Integer numAt2ndIndex = numbers.get(2);
System.out.println(numAt2ndIndex); // 50

// remove
numbers.remove(3); // remove by Index
System.out.println(numbers); // [10, 20, 50]
numbers.remove(new Integer(10)); // remove element by element
System.out.println(numbers); // [20, 50]
// LinkedList Additional Methods
LinkedList<Integer> numbers = new LinkedList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
System.out.println(numbers); // [10, 20, 30, 40]


numbers.addFirst(55);
numbers.addLast(111);
System.out.println(numbers); // [55, 10, 20, 30, 40, 111]

numbers.removeFirst();
numbers.removeLast();
System.out.println(numbers); // [10, 20, 30, 40]

System.out.println(numbers.getFirst()); // 10
System.out.println(numbers.getLast()); // 40

ArrayList vs Vector

→ ArrayList is not thread safe because it’s method & property not declared with synchronized keyword. Hence, multiple thread can operate on same ArrayList at a time & may result into in-consistent data.

→ Vector is thread safe because it’s method & propery are declared as synchronized. Hence, once single thread can operate on same Vector at a time.

→ ArrayList is not a legacy class. Vector is as legacy class.

→ ArrayList increases its size by 50% when its size reaches to the capacity. Whereas vector increases its size by 100% when its size reaches to the capacity.

Vector<Integer> numbers = new Vector<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
System.out.println(numbers);

Iteration of Collection

We can iterate the Collection using Iterator or ListIterator or Enumeration depending on the type of collection.

→ Any class which is type of collection, we can use Iterator.

→ Any class which is type of List, we can use ListIterator

→ Enumeration is only used with the Vector.

Iterator : -

→ Iterator only works in forward direction.

→ Iterator can only remove the data.

List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

Iterator<Integer> iterator = numbers.iterator();

while(iterator.hasNext()){
System.out.println(iterator.next());
}
// remove the element using iterator 
// if collection current element is 20 then remove it
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

Iterator<Integer> iterator = numbers.iterator();

while (iterator.hasNext()){
Integer currentElement = iterator.next();
if(currentElement.equals(20)){
iterator.remove();
}
}
System.out.println(numbers);

List Iterator: -

→ ListIterator works in both direction forward & backward.

→ ListIterator can remove, add, update the data.

List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

ListIterator<Integer> iterator = numbers.listIterator();

while (iterator.hasNext()){
Integer currentElement = iterator.next();
System.out.println(currentElement);
}

while(iterator.hasPrevious()){
Integer currentElement = iterator.previous();
System.out.println(currentElement);
}
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

ListIterator<Integer> iterator = numbers.listIterator();

while (iterator.hasNext()){
Integer currentElement = iterator.next();
if(currentElement == 10)
iterator.add(5);
if(currentElement == 20)
iterator.set(22);
if(currentElement == 40)
iterator.remove();
}
System.out.println(numbers); // [10, 5, 22, 30]

Enumeration: -

→ It is used with Vector interface.

→ It is just used to iterate the Vector interface ( No Update/ No Delete/ No Insertion)

Vector<Integer> numbers = new Vector<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);

Enumeration<Integer> elements = numbers.elements();


while (elements.hasMoreElements()){
System.out.println(elements.nextElement());
}
System.out.println(numbers);

Set

List vs Set

// List allows duplicates
List<Integer> numbers = new ArrayList<>();
numbers.addAll(Arrays.asList(10,20,30,40,50, 50));
System.out.println(numbers); // [10, 20, 30, 40, 50, 50]


// Set does not allow the duplicates
Set<Integer> numbers1 = new LinkedHashSet<>();
numbers1.addAll(Arrays.asList(10, 20, 30, 40 , 50 , 50));
System.out.println(numbers1); // [10, 20, 30, 40, 50]
// It will automatically remove the second 50 as its duplicates

→ The main difference between List & Set is that List allows the duplicate values whereas Set does not allow the duplicate values.

List allows to access (list.get(index)) / add (list.add(index, val) )/ update ( list.set(index, newVal) )the element using index , whereas Set does not allow to access/ update/ remove of elements using index.

→ We can iterate the list using Iterator & ListIterator whereas we can iterate the Set using Iterator.

Set we have three types of Set

I) HashSet: It will not maintain the insertion order

II) LinkedHashSet: It will maintain the insertion order

III) TreeSet: It will insert the elements in sorted manner.

// HashSet: Random order data insertion 
HashSet<Integer> set1 = new HashSet<>();
set1.addAll(Arrays.asList(30, 20, 10, 40));
System.out.println(set1); // [20, 40, 10, 30]

// LinkedHashSet: Insertion order maintained
LinkedHashSet<Integer> set2 = new LinkedHashSet<>();
set2.addAll(Arrays.asList(30, 20, 10, 40));
System.out.println(set2); // [30, 20, 10, 40]

// TreeSet: Insertion is done based on Sorting
TreeSet<Integer> set3 = new TreeSet<>();
set3.addAll(Arrays.asList(30, 20, 10, 40));
System.out.println(set3); // [10, 20, 30, 40]

Depth of TreeSet

→ SortedSet extends the Set

→ NavigableSet extends the SortedSet

→ TreeSet is the implementation of NavigableSet

SortedSet

  • It extends the Set.
  • It maintains the elements in Sorted order.
  • It provides the following additional method
TreeSet<Integer> set2 = new TreeSet<>();
set2.addAll(Arrays.asList(30, 20, 10, 40, 50, 60, 70));
System.out.println(set2.subSet(20, 50)); // sub set: [20, 30, 40]
System.out.println(set2.tailSet(40));
// tailSet => greater than or equal to
// tail set [40, 50, 60, 70]

System.out.println(set2.headSet(30));
// headSet => less than
// headSet [10, 20]

NavigableSet

→ It extends the SortedSet.

→ It provides the methods to navigate the set.

TreeSet<Integer> set2 = new TreeSet<>();
set2.addAll(Arrays.asList(30, 20, 10, 40, 50, 60, 70));
System.out.println(set2.lower(20)); // element less than 20
System.out.println(set2.higher(20)); // element greater than 20
System.out.println(set2.floor(20)); // element less than 20 or equal 20
System.out.println(set2.ceiling(20)); // element greater than or equal to 20

MAP

  • If you want to store the key-value pair then use the Map Interface. Basically it is used to map the value with its key, hence you can store the n number of key-value pairs & retrieve the value using key.
    Map<String, Integer> contacts = new HashMap<>();
contacts.put("Person1", 12121212);
contacts.put("Person2", 32323232);

System.out.println(contacts); // {Person1=12121212, Person2=32323232}

// check do we have key present or not
System.out.println(contacts.containsKey("Person2")); // true
System.out.println(contacts.containsKey("Person3")); // false

// get the value from the key
System.out.println(contacts.get("Person2")); //32323232
Map<String, Integer> contacts = new HashMap<>();
contacts.put("Person1", 12121212);
contacts.put("Person2", 32323232);

System.out.println(contacts); // {Person1=12121212, Person2=32323232}

// get all keys: map.keySet():
Set<String> contactNames = contacts.keySet();
// get all values : map.values();
Collection<Integer> phoneNos = contacts.values();
System.out.println("Contact Name: " +contactNames);
// Contact Name: [Person1, Person2]
System.out.println("Phone Nos: " + phoneNos);
// Phone Nos: [12121212, 32323232]


// Iterate Each key manually :
// map.entrySet() & entry.getKey() & entry.getValue()
Set<Map.Entry<String, Integer>> entries = contacts.entrySet();
for(Map.Entry<String, Integer> entry: entries){
System.out.println("The phone number of "
+ entry.getKey() + " is " + entry.getValue());
}
  • There are three type of Map

→ I) HashMap: HashMap Stores the kay-value in random manner

→ II) LinkedHashMap: LinkedHashMap maintains the insertion order

→ III) SortedMap: SortedMap stores the key-value pairs in the sorted order of key.


Map<String, Integer> contacts = new HashMap<>();
contacts.put("Person1", 12121212);
contacts.put("Person4", 32323232);
contacts.put("Person2", 21212121);
System.out.println(contacts);
// HASH MAP : Random order
// {Person1=12121212, Person2=21212121, Person4=32323232}


Map<String, Integer> contacts1 = new LinkedHashMap<>();
contacts1.put("Person1", 12121212);
contacts1.put("Person4", 32323232);
contacts1.put("Person2", 21212121);
System.out.println(contacts1);
// LINKED HASH MAP: Insertion order is maintained
// {Person1=12121212, Person4=32323232, Person2=21212121}


Map<String, Integer> contacts2 = new TreeMap<>();
contacts2.put("Person1", 12121212);
contacts2.put("Person4", 32323232);
contacts2.put("Person2", 21212121);
System.out.println(contacts2);
// TREE MAP: Elements are inserted in the sorted manner of key
// {Person1=12121212, Person2=21212121, Person4=32323232}

Deep Dive of TreeMap

SortedMap

→ SortedMap extends the Map & it provides the functionality of sorting.

TreeMap<Character, Integer> charMap = new TreeMap<>();
charMap.put('A', 1);
charMap.put('B', 2);
charMap.put('C', 3);
charMap.put('D', 4);
charMap.put('E', 5);

System.out.println(charMap.subMap('B', 'D'));
// {B=2, C=3}
// Map : fromKey (inclusive), toKey (exclusive)

System.out.println(charMap.headMap('C'));
// {A=1, B=2}
// Map: toKey exclusive

System.out.println(charMap.tailMap('B'));
// {B=2, C=3, D=4, E=5}
// Map: fromKey inclusive

NavigableMap

→ NavigableMap extends the SortedMap & it provides the functionality of navigation.

→ TreeMap is the implementation of NavigableMap.

TreeMap<Character, Integer> charMap = new TreeMap<>();
charMap.put('A', 1);
charMap.put('B', 2);
charMap.put('C', 3);
charMap.put('D', 4);
charMap.put('E', 5);

// map.higherEntry(key1) : entry which has key value higher than provided key1
System.out.println(charMap.higherEntry('C')); // D=4
// map.lowerEntry(key1) : entry which has key value lower than provided key1
System.out.println(charMap.lowerEntry('C')); // B=2
// map.ceilingEntry(key1) : entry which has key value higher or equal than provided key1
System.out.println(charMap.ceilingEntry('C')); // C=3
// map.floorEntry(key1) : entry which has key value lower or equal than provided key1
System.out.println(charMap.floorEntry('C')); // C=3

--

--