Collections in Java
Table of contents
A collection allows a group of objects to be treated as a single unit.
The Java Collections Framework provides a set of standard utility classes for managing various kinds of collections. The core framework is provided in the java.util
package and comprises three main parts:
The core interfaces
A set of implementations (concrete classes) that are specific implementations of the core interfaces
Static utility methods found in the Collections and Arrays classes that can be used to perform various operations on collections and arrays, such as sorting and searching, or creating customized collections
Interfaces and Implementations
Collection Interface "extends" Iterable interface (interfaces can only "extend" each other)
-
Collection Interface has a Iterator (uni-directional iteration)
List Interface is a Collection Interface but also has a ListIterator(bi-directional iteration)
Dequeue Interface extends Queue Interface
ArrayList class implements a List
LinkedList class implements both Dequeue Interface and List Interface
ArrayDequeue class implements Dequeue interface
PriorityQueue class implements Queue
HashSet class implements Set Interface and LinkedHashSet extends HashSet
TreeSet implements Navigable Set/Sorted Set
SortedMap interface extends Map Interface
HashMap class implements Map Interface
LinkedHashMap extends HashMap
TreeMap class implements Navigable/Sorted Map
HashTable implements Map
Generics in Java
In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types.
Code that uses generics has many benefits over non-generic code:
Stronger type checks at compile time.
A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.Elimination of casts.
The following code snippet without generics requires casting:List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
When re-written to use generics, the code does not require casting:
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast
Enabling programmers to implement generic algorithms.
By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.A generic can NEVER be of primitive type - wrapper classes are used for each primitive type
Examples:
Creating generic classes
Creating generic Interfaces
Creating generics for BASE type/CHILD type data , unlike regular types a base type generic CANNOT point to child type generic, for this case we would have to use WILDCARD GENERICS OR UPPER/LOWER BOUND GENERICS
-
<? extends Human> means either Human or Child of Human (upper bound generic)
<? super Human> means either Human or Parent of Human (lower bound generic)
Iterable Interface
Used to make any classes iterable with proper OOPS compliance
-
for any class that implements an iterable we can use a for each loop
Seeing our interface/class hierarchy we can see that Map interface does not extend Iterable
Note : To print a custom class using System.out.println we simply overwrite the toString() method
Collection Interface
The Collection
interface in Java is the root interface of the Java Collections Framework. It represents a group of objects, known as elements. The Collection
interface is part of the java.util
package and provides a set of methods that all collections must implement.
Key Subinterfaces
List: An ordered collection (sequence) that allows duplicate elements.
Set: A collection that does not allow duplicate elements.
Queue: A collection used to hold multiple elements prior to processing.
Deque: A double-ended queue that allows elements to be added or removed from both ends.
Common Methods
Adding Elements
boolean add(E e)
: Adds the specified element to the collection.boolean addAll(Collection<? extends E> c)
: Adds all the elements in the specified collection to this collection.
Removing Elements
boolean remove(Object o)
: Removes a single instance of the specified element from the collection.boolean removeAll(Collection<?> c)
: Removes all the elements in the specified collection from this collection.void clear()
: Removes all the elements from the collection.
Querying the Collection
boolean contains(Object o)
: Returnstrue
if the collection contains the specified element.boolean containsAll(Collection<?> c)
: Returnstrue
if the collection contains all the elements in the specified collection.boolean isEmpty()
: Returnstrue
if the collection contains no elements.int size()
: Returns the number of elements in the collection.
Iteration
Iterator<E> iterator()
: Returns an iterator over the elements in the collection.Object[] toArray()
: Returns an array containing all the elements in the collection.<T> T[] toArray(T[] a)
: Returns an array containing all the elements in the collection, with the runtime type of the specified array.
Modifying the Collection
boolean retainAll(Collection<?> c)
: Retains only the elements in the collection that are contained in the specified collection.
List Interface (add/get/set)
ListIterator Interface (extends Iterator Interface)
it extends iterator so obviously has next() and hasNext()
on top of that , this iterator also has previous() and hasPrevious()
It is a bidirectional iterator as internally this is a Doubly linked list
ArrayList (implements List)
dynamic array, used when size is not known
LinkedList (implements List and Dequeue)
dynamic array used for frequent random index insertions and deletions
Vector (implements List)
- thread safe dynamic array, used when size is not known
Queue Interface (offer/peek/poll)
Queue also has the add method but offer method should preferred as it automatically handles exceptions
Queue also has the get method but peek method should preferred as it automatically handles exceptions
Queue also has the remove method but poll method should preferred as it automatically handles exceptions
PriorityQueue class implements Queue
Internally uses a heap data structure
By default a min-heap (Unlike C++)
Dequeue Interface extends Queue Interface
extends Queue to allow double ended queues
used for both FIFO-QUEUE and LIFO-STACK operations
LinkedList class implements Dequeue
Can be used to implement FIFO- QUEUE (recommended implementation for queue problems)
ArrayDequeue class implements Dequeue
Usually used to implement a Dequeue
Note: We also have the Stack Class in java, which has all the usual functions like push/pop (recommended for stack problems)
NOTE: Collections utility class, Comparable vs Comparator
Collection is an interface , collections is class
public class Collections extends Object This class consists exclusively of static methods that operate on or return collections. It contains polymorphic algorithms that operate on collections, "wrappers", which return a new collection backed by a specified collection, and a few other odds and ends.
but when we have to use the Collections function with custom data type for example below class
in above case to use the Collections.sort we would need to provide a comparator or comparable
comparator functional interface -> compare method override , comparable functional interface -> compareTo method override
Comparator is used when the target class is not accessible
Purpose: Used to define an external ordering of objects.
Funtional Interface:
java.util.Comparator<T>
Method:
int compare(T o1, T o2)
Usage: A class does not need to implement
Comparator
itself; comparators can be used to sort instances of any class.
Comparable can we be used when we have access to the target class
Purpose: Used to define the natural ordering of objects.
Functional Interface:
java.lang.Comparable<T>
Method:
int compareTo(T o)
Usage: A class implements
Comparable
to indicate that its instances can be compared to each other.
Key Differences between comparable and comparator
Comparable:
The class itself must implement the
Comparable
interface.Defines a single natural order.
The comparison logic is within the class.
Comparator:
A separate class or lambda expression can implement
Comparator
.Allows multiple different comparisons (e.g., by age, name).
The comparison logic is external to the class.
Example: How to implement a PriorityQueue of custom data type class ?
OPTION 1- implement COMPARABLE interface in the target class
OPTION 2 - COMPARATOR
Priority queue during initialization takes as input an instance of class implementing comparator functional interface
Since its functional interface we can use Lambda
If both options are implemented - the COMPARATOR ordering wins
Set Interface (add/remove/contain/retain)
HashSet class implements Set Interface
- The order among keys is not maintained
- To create a set of custom class we will have to override the equals() and hashCode() method (Intellij helps here) and also the compareTo method or provide it a comparator
LinkedHashSet class extends HashSet class
- same utility functions as HashSet but the order of insertion is maintained
SortedSet / NavigableSet interfaces extends Set Interface
TreeSet class implements SortedSet / NavigableSet interface
- We can even create set of custom types
Map Interface (put/get/contains/remove)
as we can see the Map Interface is not in the Iterable hierarchy, we iterate it using the Map.Entry Interface
HashMap class implements Map Interface
- implement hashCode and equals method to compare custom classes
HashTable class implements Map Interface
basically HashMap but thread safe
LinkedHashMap class extends HashMap class
- basically a HashMap but order of insertion of keys in maintained in the map
SortedMap / NavigableMap interfaces extends Map Interface
TreeMap implements SortedMap / NavigableMap interfaces
- We can even specify ordering of TreeMap via comparator
Notes
Coding against interfaces
Coding against interfaces is a widely accepted best practice in software development, including when working with collections in Java. This approach offers several significant advantages:
Advantages of Coding Against Interfaces
Flexibility and Maintainability:
By declaring a variable as an interface type, such as
List<Integer> myList = new ArrayList<>();
, you can easily change the implementation without modifying the rest of your code. For example, you can switch from anArrayList
to aLinkedList
:List<Integer> myList = new LinkedList<>();
This makes your code more maintainable and adaptable to future requirements.
Abstraction:
- Interfaces provide a way to define a contract for what a collection can do, without specifying how it does it. This allows you to focus on the behavior you need rather than the implementation details.
Polymorphism:
Coding to interfaces allows you to take advantage of polymorphism. You can pass different implementations of an interface to methods without changing the method's code. For example:
public void processList(List<Integer> list) { // Process the list } // Can call with different implementations processList(new ArrayList<>()); processList(new LinkedList<>());
Interchangeability:
- When your code relies on interfaces, you can interchange implementations at runtime, possibly based on configuration or specific needs. This is particularly useful in dependency injection and designing flexible APIs.
Example: Why Use List Instead of ArrayList
Consider the following example:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Example {
public static void main(String[] args) {
// Declaring using the interface type
List<Integer> myList = new ArrayList<>();
myList.add(1);
myList.add(2);
myList.add(3);
System.out.println(myList);
// Changing the implementation to LinkedList without changing the rest of the code
myList = new LinkedList<>(myList);
System.out.println(myList);
}
}
In this example, the type of myList
is declared as List<Integer>
, which allows you to switch from ArrayList
to LinkedList
without altering any code that uses myList
. This flexibility would not be possible if myList
were declared as ArrayList<Integer>
.
Subscribe to my newsletter
Read articles from Paras kaushik directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Paras kaushik
Paras kaushik
Hello, I'm Paras Kaushik! ๐ I'm a dedicated software engineer based in India, specializing in C++ and proficient in the MERN stack. ๐ค Interested in collaborating on innovative projects that require my technical expertise. ๐ฌ Passionate about participating in discussions related to software architecture and best practices. ๐ง Feel free to reach out to me via email: [paraskaushik12@gmail.com] ๐ Connect with me on LinkedIn: [https://www.linkedin.com/in/the-paras-kaushik/]