Collections in Java

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

  1. 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.

  2. 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.

  3. Querying the Collection

    • boolean contains(Object o): Returns true if the collection contains the specified element.

    • boolean containsAll(Collection<?> c): Returns true if the collection contains all the elements in the specified collection.

    • boolean isEmpty(): Returns true if the collection contains no elements.

    • int size(): Returns the number of elements in the collection.

  4. 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.

  5. 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

  1. 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 an ArrayList to a LinkedList:

        List<Integer> myList = new LinkedList<>();
      
    • This makes your code more maintainable and adaptable to future requirements.

  2. 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.
  3. 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<>());
      
  4. 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>.