Skip to main content
Object Oriented Design, Analysis and Programming

Java Collections Framework and Generics

Published: 2026-08-19
Level: postgraduate
Audience: Postgraduate students in Object Oriented Design, Analysis and Programming

Prerequisite Knowledge

This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.

Previously Covered in This Subject

  • Arrays — Fundamentals, Declaration, Initialization and Access — covered in Lecture 18: Arrays, Strings and String Handling in Java
  • Interfaces — covered in Lecture 1: Object-Oriented Analysis and Design
  • Interfaces Versus Abstract Classes — Hierarchy Freedom — covered in Lecture 11: GRASP Patterns and Design Principles — Polymorphism, Indirection, Fabrication and Protected Variations
  • Constructors — Purpose, Rules and Automatic Invocation — covered in Lecture 17: Constructors, Static Members, this, final and Software Development Life Cycle
  • Classes — The Blueprint Idea — covered in Lecture 16: Introduction to Java and Object-Oriented Programming Foundations

# Java Collections Framework and Generics

22.1 Collections Framework — Overview and Foundational Idea

22.1.1 What the Collections Framework Is

Hook: Why does every large Java program — from an airline reservation system to an Android app — import java.util on its very first line? Because without a common way to hold many objects, every developer would reinvent lists, queues and maps from scratch.

A collections framework is a unified hierarchy of interfaces and classes that manages a group of objects as a single unit. The phrase group of objects as a single unit is central: instead of writing separate variables and loops for each element, the framework lets a program treat the whole group through one uniform set of operations such as add, remove and size. The hierarchy part means parent interfaces define common behaviour and child classes provide concrete storage strategies.

Intuition — the toolbox analogy: Think of the framework as a well-organized toolbox. The top shelf holds a master label — "container" — that promises every container can be opened, counted and emptied. Individual drawers below hold specific tools: a flat tray for arrays, a chain of linked hooks for linked lists, a ticket-counter lane for queues. Every drawer looks different inside, but the handles are identical, so you never need to relearn how to open a new drawer. Where the analogy breaks: real drawers do not automatically resize or sort themselves, whereas ArrayList grows and SortedSet orders on its own.

Formal idea: A collection is any object that groups multiple elements into a single unit, where each element is an Object (or, since JDK 5, a parameterized type E). The framework standardizes how those groups are declared, created and manipulated, so changing from one storage strategy to another requires changing only the implementing class, not the logic that uses it.

Different storage strategies solve the same high-level problem but organize memory differently:

  • An array is a collection of similar data types stored in a fixed block of continuous memory. The lecture pictured a fixed block that can hold exactly six integers, or six strings, or six floating-point numbers. Once allocated, its length never changes — to store a seventh element you must allocate a new array and copy.
  • A linked list stores data in a distributed form. One element might sit at address 0x1000, the next at 0x84A0, the next at 0x3200. Links connect them: a head pointer to node 1, node 1 points to node 2, node 2 to node 3, and so on. The storage is not continuous, yet the logical view is a single ordered sequence.
  • A queue works like a movie ticket line or a railway reservation counter — the professor's own motivating picture. The counter is at one end. The person at the front is served first and leaves, while new persons join at the far end. It follows a first-in, first-out order, written as FIFO.
  • Other structures include trees (hierarchical, branched) and maps (key-value tables where one value is looked up through its key).

Without standardization, each structure needs its own methods because the internal functioning differs. Array operations differ from linked list operations, which differ from queue operations. A developer must learn and remember each set of methods, classes and syntax, and must update knowledge whenever a different organization is chosen. The collections framework standardizes this process: it provides one hierarchy through which arrays, linked lists, queues, maps, trees and other structures can be handled in a similar manner, lifting the burden of remembering separate syntaxes and supplying standard implementations that can be used directly.

Visual intuition: imagine a bar chart comparing developer effort without vs with the framework. The x-axis is the task (store list, store queue, store map); the y-axis is lines of custom code needed. Without the framework the bars are tall and uneven — arrays need manual resizing code, linked lists need pointer management, queues need head-tail tracking. With the framework all bars collapse to the same short height — one line List<String> l = new ArrayList<>() or Queue<String> q = new LinkedList<>() and the same add call. The horizontal line at the bottom is the takeaway: uniform interfaces collapse diverse internals into one learning curve.

Scope and assumptions: The framework assumes you want to manage references to objects, not isolated primitives (use wrapper types or primitive-specialized collections otherwise). It applies when the collection size or ordering requirement is not known at compile time. It breaks down when you need guaranteed memory locality for ultra-low-latency systems — there a raw array still wins — or when you implement a specialized structure such as a B-tree that the provided classes do not offer out of the box.

Pitfalls: (1) Confusing framework with a single class — it is the whole hierarchy, not just ArrayList. (2) Treating every collection as interchangeable — a Set silently drops duplicates while a List keeps them; swapping the interface changes behaviour even though the method name add stays the same. (3) Ignoring the import — every framework type lives in java.util; forgetting import java.util.* causes an immediate compile error that beginners misdiagnose as a missing class.

Recap: The collections framework is a parent-child hierarchy that turns many storage strategies into one uniform API for handling groups of objects. It solves the "different syntax per structure" problem by standardizing method names and supplying ready-made implementations. This sets up why generic types are needed to make that uniform API type-safe — the subject of 22.2.

Real-world and domain connection: In enterprise Java, teams never ship a hand-rolled linked list for a customer order history — they use ArrayList for fast random access or LinkedList for frequent head insertions, both from java.util. In data engineering pipelines, Queue buffers incoming sensor readings in FIFO order while HashMap indexes them for lookup, all through the same framework without reinventing traversal logic. The broader placement: this is the foundation of object-oriented reuse — standard libraries replace duplicated ad-hoc code across the industry.

22.1.2 Goals That the Framework Was Designed to Meet

Four explicit design goals were visible on the opening overview slide and they still explain every interface choice today:

  • High performance. Operations on collections should be fast in both time and memory, with implementations tuned by library authors, not rewritten per project.
  • Efficient implementation of fundamental collections. Human-written implementations tend to contain non-standard patterns — dead code inside loops, repeated constant evaluation inside a loop that could be hoisted, or naive resizing by one. Standard implementations avoid these inefficiencies and supply optimized code for the common organizations: arrays, linked lists, trees and hash tables.
  • Allowing different collections to work in a similar manner. Whether a program uses an ArrayList, a LinkedList, a Set, a Map or a Queue, the core operations behave uniformly. A single method name such as add works across types, even though the underlying storage differs.
  • Providing standard implementations that users can directly use. ArrayList and LinkedList are needed in almost every application. Instead of forcing each programmer to write them from scratch in a non-standard way, the framework supplies ready, tested and optimized implementations.

Before the framework, Java provided ad-hoc classes such as Dictionary, Vector, Stack and Properties for storing and manipulating groups of objects. Those classes were useful but suffered from non-uniformity: each had its own underlying methods and there was no central unifying theme. Handling a dictionary required different mechanisms than handling a vector or a stack. The collections framework was introduced as the solution to that lack of a central theme by unifying everything under one hierarchy and retrofitting the legacy classes where possible.

Visual intuition: picture a before-and-after timeline. The x-axis is calendar time; the y-axis is API uniformity. Before roughly 1998 the curve is jagged — four separate APIs at different heights. At the framework introduction the curves merge into a single smooth trunk (CollectionList/Set/Queue) with branches for concrete classes. Landmark: the trunk narrows learning effort while branches widen choice.

Assumptions and scope: These goals assume general-purpose application development where developer productivity and correctness outweigh hand-tuned micro-optimisation. If you are building a real-time embedded system with fixed memory budgets, the "grow automatically" goal may be a liability — you will bound capacity explicitly instead.

Pitfalls: Do not cite Dictionary/Vector/Stack/Properties as modern choices in an exam answer — the question expects you to name them as pre-framework examples whose non-uniformity motivated the framework. Also, do not claim Vector is deprecated — it was retrofitted to implement List and remains available but is not preferred for new code because it is synchronized and slower.

Recap: The framework was built for speed, for correctness through tuned implementations, for uniformity through one hierarchy, and for reuse through ready-made classes. Those four goals explain both the old Vector/Hashtable history and the modern ArrayList/HashSet landscape.

Industrial note: Library designers measured that in-house ArrayList clones frequently missed growth policies such as doubling capacity and wasted cycles — the framework codifies the efficient policy once, so every application benefits.

22.1.3 Evolution in J2SE 5 — Generics and the For-Each Loop

Hook: A single release — J2SE 5, released in September 2004 (described in the lecture as 1998 to mark the early discussion era) — turned every collection from a bag of Object into a type-safe container. Two lines of syntax made raw ArrayList almost obsolete.

Two additions were highlighted:

  • Generics — adding parameterized types to collections, so ArrayList<String> holds only strings.
  • For-each style loop — a different loop syntax for traversing elements of a collection, with a form that differs from a simple for loop with an index.

After this release, all collections became generic, and many methods that operate on collections take generic type parameters. This change also helps avoid runtime mismatch errors, because the allowed type is declared up front — a ClassCastException that once appeared only when running the program now appears as a compile-time error in the editor.

Scope: J2SE 5 (JDK 5.0, also branded as Java 5) is the watershed — code compiled without generics still runs through raw types for backward compatibility, but new code should always use the parameterized form. The for-each loop, written as for (T x : collection), works only for types implementing Iterable, which every Collection does.

Pitfalls: Beginners read the history year and quote 1998 in exams — the generics/for-each change is J2SE 5, September 2004 (JDK 1.5). Also, do not write for (int i : list) when the list holds String — the loop variable type must match the collection's type parameter.

Recap: Generics and the enhanced for loop, both arriving with J2SE 5, made the framework type-safe and traversal-simple. Because every later example writes ArrayList<String> and for (String s : names), generics is introduced next as a preliminary requirement — see 22.2.

Applied note: Modern IDEs flag every raw-type declaration such as ArrayList arr = new ArrayList() with a warning — teams treat that warning as an error because the generic form ArrayList<String> arr = new ArrayList<>() catches type mismatches before the code ever runs. The same for-each idiom now scans Kafka consumer batches and risk-engine position lists with identical syntax.

22.2 Generics — Parameterized Types

22.2.1 The Core Idea: A Type as a Parameter

Hook: How can one single class hold an Integer today, a String tomorrow, and a custom Employee next week — without rewriting it three times and without risky casts at every retrieval?

A generic — meaning a parameterized type — is a class, interface or method that receives a type as a parameter. The intuition is deliberately parallel to ordinary functions where arguments are passed as parameters. With generics the same idea is lifted to the type system: a type itself is passed inside angle brackets and then used throughout the body.

Intuition — function-parameter analogy, made explicit: An ordinary function int add(int a, int b) says "I will work with whatever int values you pass at call time." A generic class Gen<T> says "I will work with whatever type you pass at declaration time." If you think of T as a blank slot, writing Gen<Integer> fills the slot with Integer, while Gen<String> fills it with String. The class body never changes — only the filler for the slot changes. Where the analogy breaks: function parameters are values chosen at runtime; type parameters are types checked at compile time and then erased, so at runtime there is only one Gen class.

Formalize: A parameterized type is written as a name followed by a type-parameter list in angle brackets. The general form for a generic class is:

where each is a type parameter — a slot for a real reference type that will be supplied when an object is created. Inside the class, may appear as a field type, a constructor parameter type, a method return type, or a method parameter type. Instantiation supplies type arguments:

After this, every occurrence of inside that instance behaves as ConcreteType. A generic class is also called a parameterized class. The same syntax applies to generic interfaces and generic methods, with the type-parameter list appearing after the interface or method name.

A simple mental picture from the lecture: there is a class named ABC with a body. Writing it as

means the class now receives a parameter written inside angle brackets as . Within the class, a variable can be declared as followed by a name, for example

Here is not a fixed type such as int or String; it is the type parameter that was passed to the class, and at instantiation it becomes the actual type supplied when the class is created. The class gains flexibility: the first time it is instantiated it can be told to behave like an integer holder, the second time like a string holder, or like a Long, Float, Double, or even another user-defined class, depending on what the programmer passes.

A crucial language rule: generics work only with reference types. You cannot write Gen<int> — primitive types are illegal as type arguments. Use wrapper types instead: Gen<Integer> holds an int via autoboxing. This is not a serious restriction because autoboxing and auto-unboxing make the wrapper transparent.

Visual intuition: picture two columns. Left column: generic definition Gen<T> with a single blank stencil T appearing in three places — field, constructor, getter. Right column: two stamped copies. Copy 1 stamps Integer into every blank; Copy 2 stamps String into every blank. The stencil shape is identical; only the ink colour changes. The x-axis is "place in class," the y-axis is "type used." The takeaway: one definition, many typed incarnations, checked at compile time.

Scope and assumptions: Generics assume you work with reference types and that compile-time type checking is desired. The compiler enforces that all uses of are consistent for a given instantiation and removes generic type information after checking — a process called erasure. This means at runtime there is really only one Gen class, with necessary casts inserted automatically. If you need runtime reflection on the actual type argument, erasure is a boundary — you must pass a Class<T> token explicitly.

Pitfalls: (1) Using a primitive as a type argument — Gen<int> fails to compile. (2) Believing the compiler creates separate classes per type — it does not; erasure leaves one class. (3) Mixing raw and parameterized forms without understanding that raw Gen disables type safety and reintroduces casts and ClassCastException risks.

Recap: Generics means parameterized types: a type parameter inside lets one class, interface or method work safely with many data types, with the actual type supplied at instantiation and checked at compile time. This idea is the key that makes every later collection declaration such as ArrayList<String> readable.

Real-world: Without generics, a stack implementation using Object references required (String) stack.pop() and could throw ClassCastException at 2 a.m. in production. With generics, Stack<String> rejects stack.push(42) at compile time, a pattern now standard across financial position-keeping and e-commerce catalog code.

22.2.2 Single-Parameter Generic Class — The Identity Example

Declaration and members: The lecture walks through a concrete generic class named Identity in full detail:

Line by line: the class is called Identity. After its name there is , the type parameter passed to the class. Inside, a field obj is declared with type . The constructor receives an argument of type and assigns it to the field. A method getObject returns a value of type . Every occurrence of is a slot for whatever actual type is supplied at instantiation. Companion reference (T6 §14, Gen<T> example) confirms this is the canonical form — field T ob, constructor Gen(T o), getter T getob() — identical in spirit to Identity.

Usage in the main method was shown as:

The first time the class is called, is received as Long, so every inside the class behaves as Long. The second time, is received as String, so every behaves as String. Imagine replacing each with Long in the first case and with String in the second — that replacement view explains execution, even though the compiler actually implements it via erasure and inserted casts. The output after running prints the stored Long value for the first instance and the stored String value for the second. A canonical textbook trace (using Gen<Integer>(88) and Gen<String>("Generics Test")) produces:

and analogously Identity<Long> / Identity<String> prints the long and string values stored.

Worked trace — Identity with Long and String: Start from empty. (1) new Identity<Long>(123L) — the constructor receives a Long value 123L, stores it in obj which is now typed as Long. Calling getObject() returns 123L of type Long without any cast. Internally showType() would report java.lang.Long. (2) new Identity<String>("Hello") — constructor receives "Hello", stores it in obj now typed as String. Calling getObject() returns "Hello" of type String. No cast is needed in either case. If you tried Identity<String> s = new Identity<Long>(123L) the compiler would reject it — this type mismatch is the compile-time safety generics adds. Sense-check: the two instances are unrelated types; assigning one to the other fails at compile time, precisely preventing the ClassCastException that plagued the Object-based NonGen version.

A frequent confusion point clarified in the lecture: the bracket with a type inside is not syntax noise; it signals generics. Whenever that form appears after a class, method or interface name, the same regular behaviour applies, but generically: the behaviour can be customized per instantiation instead of being fixed to one type.

Visual intuition: draw the class as a template with a transparent window labelled T. Overlay a Long card and the window shows Long through every hole — field type, parameter type, return type. Swap in a String card and all holes instantly show String. The single template with swappable cards is the takeaway versus the old NonGen approach which had one opaque Object window requiring manual cast glasses to read through.

Assumptions and scope: The single-parameter form assumes one type varies; if two independent types vary you need two parameters (see 22.2.3). The showType() idiom using ob.getClass().getName() assumes obj is non-null — calling it on a null field throws NullPointerException.

Pitfalls: Forgetting the type argument on the right-hand side before Java 7's diamond operator — older code writes new Identity<Long>(...) with the argument repeated; using new Identity<>(...) is allowed only from JDK 7 onward. Also, writing Identity<int> fails — use Identity<Integer>.

22.2.3 Multiple Type Parameters

Two-parameter form: Generics is not limited to one parameter. A class is declared with two slots, conventionally and (or and style names), such as:

Inside, the first field is of type and the second of type . Any reference type can fill each slot independently. The textbook counterpart (T6 §14, TwoGen<T,V>) writes class TwoGen<T,V> { T ob1; V ob2; TwoGen(T o1, V o2) { ob1=o1; ob2=o2; } T getob1(){return ob1;} V getob2(){return ob2;} } — same structure with names T and V instead of T and U.

Two instantiations were demonstrated:

  • Instance : is supplied as String, as Integer. Concrete values such as "Generics" and 88 are passed to initialize obj1 and obj2.
  • Instance : the choice is reversed — as Integer, as String.

It is the user's choice which type goes where. In , obj1 holds the String and obj2 the Integer; in , obj1 holds the Integer and obj2 the String. A method printObject (or showTypes() in the textbook TwoGen example) then prints obj1 and obj2 for each instance. The output differs only by which type was bound to which position, confirming that both parameters are respected independently.

Worked example — Pair with swapped bindings: Define Pair<String,Integer> p1 = new Pair<>("Hello", 42) and Pair<Integer,String> p2 = new Pair<>(99, "World"). For p1, obj1 is "Hello" of type String and obj2 is 42 of type Integer; p1.getObj1() returns String, p1.getObj2() returns Integer. For p2, obj1 is 99 of type Integer and obj2 is "World" of type String; return types swap accordingly. Attempting p1.getObj1().intValue() would fail to compile for p1 (it is a String) but succeed for p2.getObj1().intValue() (it is an Integer) — the compiler tracks position-specific types. Both bindings can also be the same type, e.g., TwoGen<String,String> x = new TwoGen<>("A","B"), which is valid but makes two parameters unnecessary.

Supporting depth — bounded types (from companion T6 §14): Sometimes you need to restrict what can fill a slot, such as a Stats<T> that calls nums[i].doubleValue(). Without a bound, the compiler complains because T might be String. The fix is an upper bound:

Now T can only be Number or a subclass (Integer, Double, Float), so doubleValue() is guaranteed and Stats<String> is rejected at compile time. Bounds can also involve interfaces, e.g., class Gen<T extends MyClass & MyInterface>. For wildcards (see also 22.3), Stats<?> matches any Stats object, and Coords<? extends ThreeD> matches Coords<ThreeD> or Coords<FourD> but not Coords<TwoD> — a pattern the map hierarchy exploits with addAll(Collection<? extends E> c).

Pitfalls: Assuming Pair<String,Integer> is assignable to Pair<Object,Object> — it is not; generic types are invariant even if String extends Object. Forgetting that both slots are independent — Pair<String,Integer> and Pair<Integer,String> are different types.

Recap: Two parameters T and U give two independently swappable type slots; swapping the arguments swaps which field gets which type. Bounds and wildcards (supporting material from T6 §14) let you constrain those slots when operations require a common superclass such as Number.

22.2.4 Generic Interfaces

The same parameterized idea applies to interfaces, which are collections of method declarations with return types and parameter types. The lecture's example interface was named DemoInterface and declared as:

Two parameters and are passed to the interface. The first method returns and receives ; the second does the reverse, returning and receiving . Each can be any type. This mirrors the textbook MinMax<T extends Comparable<T>> and MinMax<T> examples, confirming that interfaces declare type parameters exactly like classes.

In the main demonstration, is bound to String and to Integer, so the implementing class contains methods such as

If the binding were swapped — as Integer, as String — the signatures would swap accordingly. This illustrates that generic code lets the return type and parameter type of interface methods be chosen per use. Whether a class, a method or an interface, the signal is the same: when a name is followed by , generics is in play, and the type inside dictates how the body behaves without changing the normal logic of classes, methods and interfaces.

Interface implementation trace: Declare class DemoImpl implements DemoInterface<String,Integer> { public Integer doSomeOperation(String t){ return t.length(); } public String doOtherOperation(Integer t){ return "Value: "+t; } }. Calling new DemoImpl().doSomeOperation("Hi") returns 2 as an Integer; calling doOtherOperation(5) returns "Value: 5" as a String. Swap the declaration to DemoInterface<Integer,String> and the implementing methods must swap their signatures — String doSomeOperation(Integer t) and Integer doOtherOperation(String t) — otherwise compilation fails. Sense-check: the compiler enforces that the implementing class be generic enough to pass the type arguments to the interface; a class implementing MinMax<T extends Comparable<T>> must itself declare T extends Comparable<T>.

Supporting note — generic methods and constructors: A method can introduce its own type parameters independent of its class, e.g., static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y). A constructor can be generic even if its class is not: <T extends Number> GenCons(T arg) { val = arg.doubleValue(); }.

Pitfalls: Forgetting the contract that a class implementing a generic interface must itself be generic enough to supply the type argument — class MyClass implements MinMax<T> without declaring T is illegal. Writing implements MinMax<T extends Comparable<T>> in the implements clause is also wrong — once T extends Comparable<T> is declared on the class, it is passed as implements MinMax<T> without repetition.

Recap: Generic interfaces DemoInterface<T1,T2> let return and parameter types swap per binding, proving that generics applies uniformly to classes, interfaces and methods. The bracket is the reliable signal for this behaviour.

22.2.5 Intuition and Usage Notes

Several pedagogical points were emphasized: the parameterized type can be any reference type — String, Integer, Long, Float, Double, or another user-defined class — and the convenience is that the same generic definition serves all scenarios. Seeing repeatedly in upcoming collections syntax should not cause confusion; it simply means the same underlying class works generically and its behaviour is modified as per the requirement of the user.

A closing conceptual bridge from the lecture: generics is a preliminary requirement for understanding collections and appears repeatedly in the syntax of every collection, so it is introduced first even though the word does not appear throughout the remaining module title. Recognizing Collection<E>, List<E>, Map<K,V> and Identity<T> as the same idea with different letters is the fluency that lets you read ArrayList<String> without hesitation.

Common exam confusion: Do not read as "less-than T greater-than" or as a comparison — it is a type-parameter bracket. In handwritten answers, write it as angle brackets with care; confusing it with parentheses () is a frequent marking-point loss.

Recap and bridge: One generic definition serves every type; changing the type argument customizes behaviour without rewriting logic. This is why every collection from ArrayList to TreeSet will declare or — the pattern introduced here is the alphabet of the next section, where Collection<E> declares the uniform methods of the framework.

Real-world domain link: Large codebases such as Spring or Android SDKs publish APIs like Response<T>, Optional<T> and LiveData<T> — all generic wrappers that guarantee the caller receives the expected type without casts, exactly the holder pattern that Identity<T> teaches in miniature.

22.3 Collections Hierarchy, Core Interfaces and Standard Methods

22.3.1 Collection as a Generic Interface

Hook: If every list, queue and set promises the same verbs — add, remove, contains — where is that promise written once so you never have to relearn it?

At the heart of the hierarchy, Collection itself is a generic interface. Its declaration is:

where specifies the type of objects the collection will hold — stands for Element — exactly like the in the generics examples. Because it is generic, every collection declares what kind of elements it stores; Collection<String> holds strings, Collection<Integer> holds integers. Collection declares the core methods that all collections will have — the reason the framework achieves standardization. Whether the concrete type is an ArrayList, a Queue, a LinkedList, a Set or a map-related view, the same core method names apply.

Why this matters: By placing methods such as add, size and contains in Collection<E> itself, the framework guarantees that any class implementing Collection inherits the same method signatures. Companion text T6 §19 confirms Collection<E> extends Iterable<E> — so every collection is also traversable with the enhanced for loop for (E e : collection). The single declaration above is therefore the source of uniformity across the entire tree below it.

Visual intuition: picture a pyramid. The apex is Iterable. Directly below is Collection<E>. From its base, three broad pillars drop — List, Queue, Set. The apex label is tiny but load-bearing: without Collection<E> holding add(E) and size(), each pillar would need its own incompatible verbs. The takeaway: one interface at the centre saves you from learning three vocabularies.

Scope: Collection<E> is the root for collections that hold single elements. Map<K,V> does not extend Collection — it holds pairs, so it sits alongside, not underneath, even though it belongs to the overall framework organization.

Recap: Collection<E> is the generic root that declares the uniform verbs every collection shares and, by extending Iterable, makes every collection traversable with for (E e : c).

22.3.2 The Hierarchy: Interfaces and Classes

Intuition — family tree with two colours: Picture a family tree where blue boxes are interfaces (promises) and green boxes are classes (concrete fulfillments). An arrow from List to Collection means "List extends Collection — it inherits and specializes the promise." An arrow from ArrayList to List means "ArrayList implements List — it delivers a real storage engine for the promise."

Java provides support for manipulating collections through both interfaces and classes — an entire hierarchy that the lecture distinguished with colour coding.

Key levels (aligned with textbook T6 §19 table):

  • Iterable sits at the top. Collection extends Iterable (so every collection can be used in a for-each loop).
  • Three major sub-interfaces extend Collection: List for sequences where order matters and duplicates are allowed, Queue for first-in-first-out handling where removal is only from the head, and Set for sets without duplicates. SortedSet further specializes Set to keep elements sorted in ascending order. NavigableSet and Deque are further refinements built on these.
  • Map and SortedMap (implemented by HashMap/TreeMap) are also part of the overall collections organization for key-value associations, even though they conceptually store pairs rather than single elements. They are handled via their own interfaces outside the Collection line.

Concrete classes that implement these interfaces include:

  • For List: ArrayList (dynamic array), LinkedList (linked nodes), Vector, Stack — legacy classes retrofitted to implement List.
  • For Queue / Deque: PriorityQueue (heap-ordered), ArrayDeque (described in the lecture as "array queue" / ArrayQueue).
  • For Set: HashSet (hash table), LinkedHashSet (hash table plus insertion-order linked list), TreeSet (balanced tree, sorted).
  • For Map: HashMap, LinkedHashMap, TreeMap (sorted by keys), Hashtable (legacy).

Different platforms and diagrams show minor variations — some include AbstractCollection or AbstractList helpers — and some internal classes exist that are not always drawn. That variation is normal; the general shape above is what is followed.

Reading the diagram: Suppose you need a sorted, duplicate-free list of names. You look at the blue column: SortedSet promises "no duplicates + sorted." Moving to the green column, TreeSet is the concrete class that delivers it: SortedSet<String> s = new TreeSet<>(). If you instead need ordered storage that allows duplicates and fast indexed access, you stay on the List pillar and pick ArrayList. The interface type on the left determines the contract; the class on the right determines performance.

Visual intuition: imagine a subway map. The central hub is Collection. Three main lines branch out — blue List line, green Queue line, orange Set line — each with concrete stations (ArrayList, LinkedList on the List line; PriorityQueue on the Queue line; HashSet → LinkedHashSet → TreeSet on the Set line). A separate loop line is Map with stations HashMap → TreeMap. All lines share the same ticketing rules (add, contains, size) because they all passed through the central hub.

Pitfalls: (1) Claiming Map extends Collection in an exam — it does not; it is a separate hierarchy that can provide a collection-view via entrySet(). (2) Confusing PriorityQueue with FIFO — PriorityQueue orders by comparator / natural ordering, not by insertion time. (3) Over-memorizing every helper class — AbstractCollection, AbstractList and AbstractSequentialList are skeletal helpers; focus on the blue interfaces and the five most-used green classes: ArrayList, LinkedList, HashSet, TreeSet, HashMap.

22.3.3 Core Methods Declared by Collection

Formal set of uniform verbs: Several standard methods were listed as examples of the core behaviour all collections share. Per companion T6 §19 Table 19-1, the full core includes:

  • — adds an object at the end of the invoking collection (appends for lists; may reject for sets that forbid duplicates). Returns true if the collection changed.
  • — adds all elements of to the end of the invoking collection. The wildcard ? extends E means you can add a Collection<Integer> to a Collection<Number> safely.
  • — removes all elements and sets size to zero.
  • — searches for the object in the invoking collection, returning true if present, false otherwise.
  • — true if every element of c is present.
  • — equality depends on collection type (ordered for lists, unordered for sets).
  • — returns true if size is zero.
  • — returns an iterator for the invoking collection.
  • — removes one instance, returns true if removed.
  • — returns the number of elements.
  • Additional methods: removeAll, retainAll, toArray, stream/parallelStream/spliterator (JDK 8+).

These methods are defined in the classes that implement the Collection hierarchy, and because they are declared at the Collection level they can be used uniformly with different concrete collections. Additional similar methods exist beyond this short list.

Specific exception and optional-operation rules (from T6 §19): methods that modify a collection may throw UnsupportedOperationException if the collection is unmodifiable; ClassCastException on incompatible types; NullPointerException if null is not allowed; IllegalArgumentException on invalid arguments; and IllegalStateException for fixed-length collections that are full. All built-in collections are modifiable, so you rarely see UnsupportedOperationException with ArrayList or HashSet.

Uniform verbs, different engines — tiny demo: Collection<String> c1 = new ArrayList<>(); c1.add("A"); and Collection<String> c2 = new HashSet<>(); c2.add("A"); use the identical call add("A"). Both report size()==1 and contains("A")==true. Yet c1.add("A") a second time makes size()==2 (list allows duplicates) while c2.add("A") a second time leaves size()==1 and returns false (set rejects duplicates). Sense-check: the call site is identical; the behaviour difference comes from the implementing class, which is exactly the "same name, different behaviour" benefit the lecture highlights.

Assumptions and scope: Uniform names do not mean uniform semantics — return values and duplicate handling differ by subinterface (add returns false on duplicate for Set, true for List). Also, collection-view of a map (map.entrySet()) is a Set, not a Collection of pairs directly.

Recap: The verbs add, addAll, clear, contains, containsAll, isEmpty, iterator, remove, size and friends live once in Collection<E> and work everywhere. Learning their signatures once lets you manipulate every concrete collection.

Exam note: recognizing that Collection is generic and that it declares core uniform methods is foundational for interpreting every later example that uses add or size — expect to translate add(E) and size() calls across any implementing class without relearning syntax.

22.3.4 Collection Classes and the AbstractCollection Note

The framework contains roughly five major collection classes that were mentioned at a high level, though more exist internally. One internal helper is AbstractCollection, which implements most of the Collection interface with skeletal code. It does not always appear in overview diagrams, and platform-specific hierarchies may vary slightly precisely because of such helper classes. Similarly, AbstractList and AbstractSequentialList provide skeletal List implementations so concrete classes need only fill in a few methods. In practice the classes encountered most frequently are ArrayList, LinkedList and queue-related classes, plus HashSet/TreeSet and HashMap/TreeMap.

Scope: Treat AbstractCollection and its siblings as convenience scaffolding, not as types you instantiate directly. They exist to make creating a custom collection easier — a point the textbook emphasizes: "Some of the classes provide full implementations that can be used as-is. Others are abstract, providing skeletal implementations."

Recap and bridge: The theoretical side — which interfaces exist, which classes implement them, and which standard methods they share — sets up the detailed study of individual classes that follows. The next section, ArrayList, is the first concrete List examined: a dynamic array that grows automatically while exposing exactly the add/get/size verbs declared here.

Domain connection: Interviewers and architects routinely ask "Program to an interface: would you declare List<String> list = new ArrayList<>() or ArrayList<String> list = new ArrayList<>()?" The correct answer uses the interface on the left (List) because it was defined by this hierarchy — it lets you swap ArrayList for LinkedList later without changing any method calls, since both share the Collection/List verbs.

22.4 ArrayList — Dynamic Arrays

22.4.1 Fixed Arrays versus Dynamic Arrays

Hook: A plain array forces you to guess its final size on the day you write the code. What if a web request brings 5 orders today and 5,000 tomorrow — do you allocate for 5,000 every time?

A normal array such as with five elements occupies a fixed size. Once allocated for five integers or strings or floats — e.g., int[] arr = new int[5] — its length cannot be increased to hold a sixth element nor decreased after deleting elements; the allocated size remains fixed at five, which is a limitation. To grow you must create a new larger array and copy all elements manually.

An ArrayList supports dynamic arrays. Think of dynamic as stretchable or shrinkable as required: elements can be added and the size grows automatically, and it can shrink when elements are removed. This is achieved through the ArrayList class that is part of the Collection hierarchy. Under the hood an ArrayList still uses an array, but it hides fixed-size management and enlarges itself automatically when capacity is exceeded, typically by allocating a larger backing array and copying.

Formal definition: ArrayList<E> extends AbstractList<E> implements List<E> — a resizable-array implementation of the List interface. It stores elements in continuous storage like an array for fast indexed access, but exposes the collection verbs add, get, set, remove, size so the caller never manages capacity by hand.

Fixed vs dynamic in numbers: Plain array String[] a = new String[3]a[0]="A"; a[1]="B"; a[2]="C"; a[3]="D" throws ArrayIndexOutOfBoundsException — the physical length is 3, period. ArrayList<String> al = new ArrayList<>(); al.add("A"); al.add("B"); al.add("C"); al.add("D"); succeeds — after the third add the internal array (say capacity 3) is transparently replaced by a capacity 6 array and size() becomes 4. Sense-check: al.size() reports logical element count (4), not physical capacity (6).

Visual intuition: draw two rectangles. Left: a rigid box labelled "array[5]" with five cells; trying to push a sixth item makes it overflow outside the border with a red cross. Right: a stretchable balloon labelled ArrayList that starts with 5 cells and, on the sixth add, grows a dotted extension to 10 cells with an arrow indicating copy. The x-axis is number of elements, y-axis is required manual code — the array curve spikes when you cross capacity, the ArrayList curve stays flat.

Scope and assumptions: ArrayList assumes that most operations are appends or indexed gets (both amortized O(1)). It breaks down for frequent insertions or deletions in the middle — each requires shifting elements, costing O(n). In that scenario LinkedList or a tree-based structure is preferable.

Pitfalls: Confusing capacity (physical backing array length) with size (logical element count returned by size()). A list with new ArrayList<>(100) still reports size()==0 until you add.

Recap: Fixed arrays trap you at a compile-time length; ArrayList virtualizes that length so you work with logical size while the framework manages physical capacity.

22.4.2 Constructors

ArrayLists are created with an initial size and enlarge automatically when that size is exceeded. Three constructors exist (per T6 §19), but for learning purposes two were emphasized:

  • Zero-argument default constructor: — creates an empty list with default initial capacity (10 in the JDK implementation). No elements yet, size()==0.
  • Capacity constructor: — for example creates a list with initial capacity 20, meaning it is prepared to grow efficiently up to that threshold before further enlargement. Formally ArrayList(int capacity) builds an empty list whose backing array length is 20.
  • Collection-copy constructor: — builds a list initialized with the elements of collection c in iteration order.

Why would you pass capacity? Performance. If you know you will store about 10,000 rows from a database, new ArrayList<>(10000) avoids the repeated doubling and copying that would occur if you started at 10 and grew incrementally. Conversely, ensureCapacity(int cap) can increase capacity after construction, and trimToSize() shrinks the backing array to exactly size() if you want to save memory.

Pitfalls: Passing a capacity does not pre-fill elements — indices 0 to 19 do not exist until you add to them; get(0) on a fresh new ArrayList<>(20) still throws IndexOutOfBoundsException.

22.4.3 Worked Example — Unparameterized ArrayList (Heterogeneous Storage)

This example deliberately uses an unparameterized (raw) ArrayList, analogous to a plain class with no . Because no type parameter is supplied, the list can hold heterogeneous objects — integer, string, double, boolean in the same collection. The point of starting here is to contrast with the bounded case later and to show that raw types abandon compile-time safety.

Setup:

  • Required import: ; all collection interfaces and classes come from .
  • Class with .
  • Two objects:

The first uses the zero-argument constructor, the second the capacity constructor with 20. In modern code both would be ArrayList<Object> or ArrayList<String>, but the raw form illustrates pre-generics behaviour.

Step-by-step execution with index bookkeeping:

  1. Size check before insertion: and both return 0. For arr1, even though capacity 20 was requested, size remains 0 until elements are added — capacity is not size. Printing sizes therefore shows 0 and 0.
  1. Adding elements one by one via the standard method:

At this point indices are 0:10, 1:"a", 2:12.56, 3:true. Autoboxing wraps 10 into Integer, 12.56 into Double, true into Boolean.

  1. Overloaded add with index: — insert value 30 at index 1. Existing elements at indices 1 and beyond shift one position right. The bookkeeping becomes:
  • index 0: 10 (unchanged)
  • index 1: 30 (new)
  • index 2: "a" (shifted from 1)
  • index 3: 12.56 (shifted from 2)
  • index 4: true (shifted from 3)

So the list is [10, 30, a, 12.56, true] and prints exactly that: 10, 30, a, 12.56, true in list notation (toString() inherited from AbstractCollection). A notable convenience compared to plain arrays is that printing the list name directly shows all contents, whereas a plain array requires an explicit for loop or Arrays.toString().

  1. Illegal index illustration: attempting when the highest valid index is 4 (size 5) tries to skip to an index that does not exist. This throws an IndexOutOfBoundsException and terminates execution. The rule is: with indexed add, the index must be between 0 and size() inclusive — far gaps are not allowed. Without an index, add simply appends sequentially and expands as needed.
  1. Bulk addition: appends the entire collection arr to the end of arr1. Since arr1 was still empty, after this call arr1 holds the same five elements as arr: [10, 30, a, 12.56, true]. Contents of arr1 printed are therefore identical to arr. This demonstrates addAll(Collection<? extends E> c) from Collection.

Final state: arr = [10, 30, a, 12.56, true] with size()==5; arr1 = [10, 30, a, 12.56, true] after addAll. Sense-check: heterogeneous storage works only because the raw type treats every element as Object; with ArrayList<String> the same add(10) would have been rejected at compile time.

Visual intuition: imagine five labelled slots on a paper strip. Step 2 fills them left to right. Step 3 cuts the strip between slots 0 and 1, inserts a new card "30", and slides every card to the right — the cut-and-slide animation is the takeaway for indexed insertion cost.

This walkthrough underscores standardization: the same method name works here and later for LinkedList, Queue, Set and others, even though internal handling differs.

Pitfalls: Raw ArrayList silences the compiler — arr.add("hello"); int x = (Integer) arr.get(0); compiles but throws ClassCastException at runtime if index 0 holds a String. Always prefer ArrayList<String> to get the error at compile time.

22.4.4 Worked Example — Parameterized ArrayList<String> with Set, Remove and Index Queries

Now the list is bounded via generics, analogous to replacing a raw with and fixing . The example uses:

Because of , this list can store only strings. Attempting to add an integer or double produces a compile-time error — names.add(42) fails — which is the safety generics provides. The List methods used here (add, add(int,E), set, remove, indexOf, lastIndexOf) are the List-specific additions summarized in T6 Table 19-2.

Insertion sequence with shifting shown at each step. Starting from empty:

  • — appended at index 0:
  • — Mic at index 1, just after Java:
  • — Rahul at index 0, existing elements shift right:
  • — Object at index 2, Mic shifts to 3:
  • — appended at end (index 4):

At this snapshot, is 5 and printing yields Rahul, Java, Object, Mic, Fortran with the order derived exactly by the shifting logic above. Remembering the order after each indexed insertion is key to following the next operations.

Continuing from that snapshot — set versus add and removals:

  • Set (update without size change): replaces the element at index 2. Indices are 0:Rahul,1:Java,2:Object,3:Mic,4:Fortran. After the call, index 2 becomes Testing, size stays 5: . This differs from , which would increase size to 6 and shift; overwrites and returns the old value "Object".
  • Appending more: , , append at successive ends: — 8 elements. Note the duplicate "Java" is allowed because List permits duplicates.
  • Remove by index: — the overloaded E remove(int index) — removes element at index 2 (Testing) and returns it. The list shrinks to 7 and elements right of index 2 shift left: .
  • Remove by object: — the boolean remove(Object) form — removes the string Fortran wherever it occurs (first occurrence) and returns true: — now 6 elements.
  • Index queries: returns first occurrence index, returns last occurrence. In the current list indices are 0:Rahul,1:Java,2:Mic,3:C,4:C++,5:Java. So and . Printing them shows 1 then 5. If the element is absent both return -1. Java appears twice, so the example shows both methods distinct from contains.

Sense-check: set never changes size(), add(index, E) always increases it by 1, remove(int) decreases it by 1 and shifts left — verifying these invariants on the trace above confirms the bookkeeping.

Visual intuition: consider a timeline where each operation is a frame. The strip of cards shifts right on add(0, "Rahul"), stays same length but repaints one card on set(2, "Testing"), then compresses left on remove(2). The animation direction (expand vs repaint vs compress) is the visual cue for which method was called.

Pitfalls to flag (professor's warning): Far-index add(10, val) when size()==6 throws IndexOutOfBoundsException — gaps are illegal. remove(2) vs remove("2") confusion: the former removes by index and returns E, the latter (on List<String>) removes by object and returns boolean; autoboxing on List<Integer> makes remove(1) ambiguous — cast to (Integer) 1 or use (Object) 1 to force object removal.

22.4.5 Traversal — Simple For Loop and For-Each Loop

Two traversal styles were compared using the same parameterized ArrayList<String> with elements like Java, Mic, Rahul, Object, Fortran (in whatever order currently held).

Simple for loop, forward:

The loop control variable starts at 0, checks , retrieves each iteration and prints. This walks from first to last. Under the hood get(i) on ArrayList is O(1) via direct array indexing.

Simple for loop, backward: reverse the initialization and condition:

Starting at (last index) and decrementing prints in reverse order — Fortran, Object, Rahul, Mic, Java if iterating the 5-element snapshot.

For-each loop (enhanced for), introduced after J2SE 5:

No integer index is needed. The loop variable takes the type of the collection (String) and is initialized with the collection object in the syntax . Internally, points to the first element, then in each iteration moves to the next element and prints it — Rahul, then Java, then each remaining element in traversal order — until the end. The description emphasized its simplicity: no explicit size check, no index management, just a traversal variable bound to the collection that increments automatically each loop pass. It is described as a specialized loop for collections and is convenient alongside the traditional for loop. Under the hood the compiler translates it to an Iterator.

Side-by-side on names = [Rahul, Java, Testing, Mic, Fortran]: Indexed loop prints Rahul(0) → Java(1) → Testing(2) → Mic(3) → Fortran(4) by incrementing i. For-each prints the same sequence as s takes those values without ever exposing i. Reversing the indexed loop prints Fortran(4) → Mic(3) → Testing(2) → Java(1) → Rahul(0). The enhanced for cannot naturally go backward — that needs an indexed loop or a ListIterator (see 22.7). Sense-check: all three loops report names.size() unchanged — traversal does not modify the list.

Scope and pitfalls: The for-each loop assumes you will not structurally modify the list during traversal — adding or removing inside it triggers ConcurrentModificationException. For modification during iteration use an explicit Iterator with remove() (see 22.6). Also, for LinkedList the indexed get(i) inside a for loop is O(n) per access (must walk nodes), making the for-each/iterator O(n) total versus O(n²) for the indexed loop — a deliberate design trade-off.

Recap: Forward indexed traversal gives full control including reverse; the J2SE 5 for-each for (String s : names) trades control for simplicity and works on any Iterable, with the compiler hiding the iterator.

Real-world and domain connection: Using a bounded directly corresponds to common practice where a list of names or identifiers is held as strings, and the compile-time check prevents accidental insertion of a numeric value. In web backends, ArrayList<Order> buffers a page of database results — indexed loops paginate, for-each loops render — both relying on the dynamic-array guarantees taught here. The broader placement: mastering ArrayList mechanics (capacity vs size, add shift vs set repaint, remove compress) is prerequisite for choosing between ArrayList and LinkedList in the next section.

22.5 LinkedList

22.5.1 Structure and Contract

Hook: If ArrayList slides every element on an insert, how can a structure add to the front a million times without moving anything?

A LinkedList arranges elements as a linked list — distributed storage rather than continuous. Visualize one element at address 0x10A0, the next at 0x7FE0, another at 0x2030, and so on, with a starting head pointer to node 1, node 1 pointing to node 2, node 2 to node 3, node 3 to node 4. Despite physical distribution, the logical view remains a single ordered collection where position 0, 1, 2 have a clear order. Internally each node holds data and two links (next and previous) because the JDK implements a doubly-linked list.

In the hierarchy, LinkedList extends AbstractSequentialList and implements the List and Deque/Queue interfaces, which is why it supports both list-style indexed operations (add(index,E), get, set) and queue/deque-style end operations that add or peek at the head or tail. Companion T6 §19 confirms: class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Queue<E>. The AbstractSequentialList parent signals that sequential access is the efficient path.

Formal contract: LinkedList<E> promises ordered, duplicate-allowed storage with O(1) insertion/removal at the ends and O(n) indexed access in the middle, because reaching index i requires walking from the nearer end. The same List verbs as ArrayList are honoured, but performance characteristics invert: ArrayList is fast by index, LinkedList is fast at the ends.

Visual intuition: draw two memory maps. Top: ArrayList cells packed tightly in one block, labelled index 0-5 contiguously. Bottom: LinkedList nodes scattered across the page with arrows chaining them; inserting at the front just redirects the head arrow — no sliding of existing nodes. The dotted arrow that redirects is the takeaway: links replace shifts.

Scope and assumptions: LinkedList assumes you frequently insert/remove at the head or tail, or need deque behaviour (stack + queue). If your workload is dominated by get(i) in a loop, ArrayList is the better choice — LinkedList.get(i) must traverse.

Q: Can we use loops to add elements taken from the user?

A: Yes. Use the standard add method. Take input from the user into a variable — e.g., via Scanner sc = new Scanner(System.in); String input = sc.nextLine(); — and pass that variable to add; whatever the user provides will be added to the collection. The same add method used in the lecture examples works for user-driven insertion, and wrapping it in a loop lets you collect an arbitrary number of user entries:

There is no special "user input add" — the uniform verb handles it.

22.5.2 Specific Methods

Beyond the standard Collection methods, LinkedList exposes deque-end methods that reflect its doubly-linked structure and are self-explanatory:

  • — adds element as the first element of the list (head insertion, O(1)).
  • — adds element as the last element (tail insertion, O(1)).
  • — retrieves the first element without removing; throws NoSuchElementException if empty.
  • — retrieves the last element similarly.
  • — removes and returns the first element.
  • — removes and returns the last element.

These complement the generic (which appends) and indexed that shift elements exactly as in ArrayList from the caller's perspective — the caller sees the same shifting semantics, though internally LinkedList walks and re-links rather than array-copying. Additional Deque aliases offerFirst/offerLast/peekFirst/pollFirst provide non-throwing variants (return null/false instead of exception).

Pitfalls: getFirst() on an empty list throws — check isEmpty() first or use peekFirst() which returns null. add(E) on LinkedList is defined as addLast(E) — appending is the default.

22.5.3 Worked Examples

Example 1 — Parameterized LinkedList<String> with addFirst / addLast and bounds check:

Elements are added with the same standard add used for ArrayList:

  • at index 0, then , , , , appended sequentially, yielding indices 0:F, 1:B, 2:D, 3:E, 4:C, 5:Z.
  • inserts A at the front: — head pointer redirected, no sliding of existing nodes needed internally.
  • with no index appends to the tail (conceptually addLast): where the final element is the newly appended value (shown in lowercase to distinguish from the leading A). Indices are now 0:A, 1:F, 2:B, 3:D, 4:E, 5:C, 6:Z, 7:a, printed in that order via System.out.println(L1).

An illegal index case was reiterated: attempting when size is 8 tries to access a far index that does not yet exist and throws an IndexOutOfBoundsException. Valid indexed inserts are only at existing indices — exactly at size means append, anything beyond is illegal. Sense-check: L1.size() is 8 before the illegal call; add(10, x) requests a gap of two empty slots, which no List permits.

Example 2 — Parameterized LinkedList<Integer> with head insertion and indexed removal:

Because of , only integer values are allowed — L2.add("hi") would fail at compile time.

The walkthrough adds integers such as 101, 102, 103 etc. Concretely, after initial appends at indices 0,1,2,3 the list holds values like 101, 102, 103 together with the subsequently described operations:

  • and appended as further integers,
  • inserts 25 at the front,
  • appends at the end,
  • removes element at index 1.

Before removal, indices were 0:25, 1:101, 2:102, 3:103, 4:1, 5:2, 6:104 and so on; after the element at index 1 (101) is gone and later elements shift left logically, leaving . The textbook LinkedListDemo trace confirms the same pattern: start A, A2, D, E, C, Zremove("F")remove(2)removeFirst()removeLast()get(2)/set — each operation's index arithmetic matches. Printed result matches style sequencing, confirming that indexed add and remove behave identically to the ArrayList case from the caller's view (append by default, insert at index shifts right logically, remove shrinks and shifts left). Once the ArrayList mechanics are understood, LinkedList examples are quick to grasp because the method names and shifting semantics are standardized, even though the internal engine differs (pointer rerouting vs array copy).

Bookkeeping summary for L2 after removes: size() decreases by 1 per remove; getFirst() would now return 25; getLast() returns 104.

Visual intuition for the Integer example: animate the chain where node 25 is spliced at the head — only two arrows change (new head to 25, 25 to old head). Contrast with an ArrayList where six elements would have slid. The cheap head-insert is the selling point.

Pitfalls and scope note: Do not use indexed loops with LinkedList.get(i) for bulk processing — for (int i=0;i<list.size();i++) list.get(i) on LinkedList re-walks from the head each time (O(n²) total). Prefer for-each or an Iterator (O(n) total). Also, LinkedList allows null entries but many queue usages forbid them — check the concrete policy before inserting null.

Recap and bridge: LinkedList is the doubly-linked, deque-capable realization of List: same add/remove/get verbs as ArrayList from the outside, but O(1) at the ends and O(n) by index inside. That performance inversion explains why the next abstraction — Iterator — exists: it gives a single traversal idiom that works efficiently for both ArrayList and LinkedList without exposing their opposite cost models.

Real-world connection: Browser history (back/forward links) and music player playlists are textbook doubly-linked use cases — constant-time insertion at either end and bidirectional navigation. In backend systems, LinkedList as a Queue buffers tasks in LinkedList<Task> queue; queue.offer(task); queue.poll(); with the same addFirst/removeFirst primitives taught here.

22.6 Iterator Interface

22.6.1 Purpose — Uniform Traversal

Hook: ArrayList loves get(i), LinkedList hates it. How do you write one loop that is efficient for both without asking "what kind of list are you?" every time?

Iterator is an interface near the top of the hierarchy (above Collection via Iterable) used for more convenient, standardized retrieval. Instead of coding size checks and index arithmetic, a program creates an iterator object associated with a concrete collection and uses iterator methods. The same iterator pattern works for ArrayList, LinkedList and other collections, which is the uniformity benefit emphasized.

Formal contract: Defined in java.util as interface Iterator<E>, it declares three core methods (plus JDK 8 defaults):

Every Collection provides one via Iterator<E> iterator() inherited from Collection. Because Iterator is bound to the collection at creation time, it knows whether to walk an array or follow links — the caller just calls hasNext/next without caring.

Intuition — bookmark analogy: Think of an iterator as a bookmark that starts before the first page. hasNext() asks "is there a page after the bookmark?" next() slides the bookmark forward one page and reads it. remove() tears out the page the bookmark just stepped onto. The same bookmark works for a stapled booklet (ArrayList) or a loose-leaf chain (LinkedList); you never count pages yourself. Where the analogy breaks: a real bookmark does not fail if someone adds pages behind it — an iterator does, throwing ConcurrentModificationException.

Visual intuition: draw two lists side by side — a packed array and a scattered chain. A single yellow arrow labelled itr sits just before index 0 in both. Each next() advances the arrow one step; hasNext() lights up while the arrow is not past the last element. The arrow's shape is identical for both storages — the takeaway is one idiom per collection type.

Scope and assumptions: Iterator provides only forward traversal. For backward traversal you need ListIterator. It also assumes you will not structurally modify the collection except through the iterator's own remove() — any external add between next and remove violates the contract.

22.6.2 Traversal Worked Examples

ArrayList example — forward scan:

Indices are 0:a,1:b,2:x,3:y,4:z.

An iterator is created and bound:

Traversal loop:

The iterator starts before the first element (blank), next() moves to the immediate first element and returns it for printing, then next iteration moves to second, third and so on until hasNext() is false. Output is a, b, x, y, z in insertion order. Internally ArrayList's iterator advances an array index; the caller never sees the index.

LinkedList example — identical verbs, different engine: same pattern with

with elements F,B,D,E,C,Z etc., and

Starting from blank, first next yields F, then B, then D and so on, printing F, B, D, E, C, Z in order. The method names hasNext and next are identical across both collections, showing a single standardized way to retrieve elements irrespective of whether internal storage is array-based or linked. Internally LinkedList's iterator follows node links. Sense-check: both loops never call size() or get(i) — element count is implicit in hasNext.

Pitfalls: Calling next() when hasNext() is false throws NoSuchElementException. Also, writing for (Iterator<String> it = al.iterator(); it.hasNext(); ) without advancing inside the loop body creates an infinite loop.

22.6.3 Remove via Iterator — Rules and Exceptions

Iterator also provides to delete from the underlying collection, but strict rules govern its use, reflecting the fail-fast design of the framework:

Contract rules for :

  • via the iterator instance can be invoked only once per call to (or when available on ListIterator). Repeated remove() without an intervening next() is illegal.
  • After the last call to or \(\text{previous}()`, (structural modification via the collection, not via iterator) must not have been executed in between before the . In other words, the sequence must be next then remove, with no intervening external add, otherwise the contract is violated and the iterator is fail-fast.

If either rule is broken, IllegalStateException (rule 1) or ConcurrentModificationException (rule 2) is thrown.

Three illustrative programs were walked through, each on a list such as [Java, Object, Fortran, Pascal, Python]:

Example 1 — remove without prior next (violates rule 1, throws IllegalStateException):

Because next was never called, the iterator has no current element to remove — the cursor is still before the first element. Result: IllegalStateException. Trace: hasNext()==true but no element has been "returned" yet, so remove has nothing to target.

Example 2 — correct next then remove:

Same list and iterator binding, but now

Here next moves to the first element Java, then remove deletes that current element. Printing after shows with Java removed and size() decremented from 5 to 4 — the expected behaviour when the rule is followed. A second remove() without another next() would now throw IllegalStateException.

Example 3 — add between next and remove (violates rule 2, throws ConcurrentModificationException):

Even though next was called, an external add occurred between next and remove, modifying modCount behind the iterator's back and violating the second rule. The program throws ConcurrentModificationException (described in lecture as a concurrent-modification style exception). The lesson: for iterator-based removal, only a direct nextremove sequence is allowed; no add or other structural modification may intervene. Modern Iterator also offers forEachRemaining and removeIf defaults, but the strict nextremove pairing remains the exam focus.

Sense-check on all three: only Example 2 satisfies both "one remove per next" and "no external modification between next and remove" — hence only it succeeds.

Visual intuition: draw a pointer itr with a trailing lastReturned slot. After next, lastReturned is filled. remove empties it and deletes that node. A second remove finds lastReturned empty and throws. An external add stamps a mismatch flag on the collection, so the next remove sees the flag and throws.

Pitfalls: Mixing collection remove with iteration — for (String s : list) { if (s.equals("Java")) list.remove(s); } throws ConcurrentModificationException every time; use Iterator.remove() or list.removeIf(s -> s.equals("Java")) instead. Also, calling remove before next inside a while (it.hasNext()) that has not yet entered the body is a frequent exam trap.

Recap and bridge: Iterator delivers uniform forward traversal (hasNext/next) plus a tightly constrained remove — once per next, with no intervening add. Mastering this prepares the bidirectional extension: ListIterator adds backward movement with hasPrevious/previous.

Real-world connection: Data-cleaning pipelines routinely iterate a List<Transaction> and drop flagged entries with iterator.remove() — the same pattern that filters spam messages from a LinkedList<Message> buffer without writing index-shift arithmetic.

22.7 ListIterator Interface

22.7.1 Bidirectional Traversal as a Sub-Interface

Hook: Iterator can march forward — but how do you step backward to re-examine the previous element without rebuilding the loop from scratch?

ListIterator is a sub-interface of Iterator that allows traversal in both directions. While Iterator moves only forward via hasNext/next, ListIterator adds reverse movement with hasPrevious/previous. This avoids writing custom index-reversal logic by providing ready-made methods and works for any List.

Formal declaration: Per T6 §19 Table 19-2, List declares:

and ListIterator<E> extends Iterator<E> adds:

Because it is specialized for lists, it is particularly natural with ArrayList and LinkedList — the only collections that have a positional index.

Key operations mentioned in the lecture:

  • returns a ListIterator instance pointing at the beginning of the list (before index 0).
  • returns a ListIterator starting at the given index — useful for resuming mid-list.
  • Forward: and .
  • Backward: and .

The cursor model is precise: a ListIterator sits between elements. next() steps the cursor forward and returns the element ahead; previous() steps backward and returns the element behind. This is why the same iterator that traversed forward can immediately traverse backward — no new object is needed.

Visual intuition: draw a strip a | b | x | y | z with tick marks between slots. A diamond cursor starts before a (between start and a). next() hops right over a and yields a; previous() hops left over z and yields z. Forward arrows point right, backward arrows point left, both originating from the same diamond — the bidirectional cursor is the takeaway.

Scope: ListIterator exists only for List implementations. You cannot obtain one from a Set or Queue — they have no positional order. Also, ListIterator permits add and set during iteration (unlike plain Iterator which only has remove), but the same fail-fast rule applies — external structural modification outside the iterator still invalidates it.

22.7.2 Worked Examples — Forward and Backward

Forward traversal example:

No size variable or loop counter is needed. The loop checks hasNext and prints next each iteration, exactly like the Iterator forward case but through the ListIterator type. Trace: cursor starts before a; hasNext=true → next returns a; hasNext=true → next returns b; hasNext=true → next returns x; then y, then z; finally hasNext=false and loop ends. Output is a,b,x,y,z in forward order.

Backward traversal example: Same list and binding — importantly the iterator must be positioned at the end before backward traversal makes sense. Two equivalent ways were noted:

Alternatively, directly position at the end:

In either case the loop checks hasPrevious (whether a previous element exists behind the cursor) and each iteration calls previous to move backward one step and print. Trace from the end: cursor after z; hasPrevious=true → previous returns z; hasPrevious=true → previous returns y; then x, b, a. Traversal therefore yields z,y,x,b,a — the reverse sequence. The discussion emphasized how little extra code is needed: the same listIterator object that traversed forward can traverse backward by switching to hasPrevious/previous, with no manual index reversal logic such as for (int i=size-1; i>=0; i--).

Sense-check: forward yields size 5 elements; backward yields the same 5 in opposite order; a fresh listIterator(2) starting at index 2 would first return x on next() and b on previous(), demonstrating the between-elements cursor precisely.

Pitfalls: Starting backward traversal from listIterator() without first advancing to the end yields zero backward steps — hasPrevious() is immediately false at the start. This is the most common exam trap: forgetting to position at size() or traverse forward first. Also, mixing Iterator and ListIterator types — hasPrevious does not exist on plain Iterator.

Recap and bridge: ListIterator is Iterator plus hasPrevious/previous (and nextIndex/previousIndex/set/add) for bidirectional, index-aware traversal of any List. This completes the traversal story: Iterator for all collections forward, ListIterator for lists both ways — the next interface, RandomAccess, asks when to prefer indexed get(i) at all.

Real-world: Text editors use ListIterator<Character> to model a cursor inside a line — next() types forward, previous() backspaces, add inserts, remove deletes — all through the same iterator without managing indices manually. Undo stacks for such editors walk the same list backward with hasPrevious.

22.8 RandomAccess Interface

22.8.1 A Marker Interface

Hook: How can a method know, just by looking at a list's type, whether calling get(5000) will be instant or will walk through 5,000 links?

RandomAccess is a marker interface — an interface with no members (no methods or fields). Implementing it signals that the collection supports efficient random access to its elements. Marker here means the presence of the interface is itself the signal; the type tag carries the information.

Defined in java.util, it extends nothing and declares no methods. A class writes class ArrayList<E> implements List<E>, RandomAccess to announce "indexed get(i) is O(1)." LinkedList does not implement it, announcing "indexed access is O(n)." Client code can test this at runtime with if (list instanceof RandomAccess) to choose a strategy. Companion T6 §19 states: "By implementing RandomAccess, a list indicates that it supports efficient, random access to its elements."

History note: marker interfaces were the pre-annotation way to tag capabilities; modern Java might use annotations, but RandomAccess and Serializable remain as classic examples.

Visual intuition: imagine a property tag clipped to each list object. ArrayList wears a green "FAST INDEX" tag; LinkedList wears no tag. A sorting algorithm glances at the tag before deciding whether to use index swapping (green tag present) or iterator stepping (tag absent). The tag itself does no work — it just informs — which is the defining trait of a marker.

Scope: RandomAccess is only meaningful for List implementations. Applying the tag to a Set would be meaningless because a Set has no get(index) to optimize. It also does not guarantee parallelism or synchronization — only access cost.

22.8.2 Traditional Indexed Loop versus Iterator Retrieval

Two contrasting code patterns were compared, and the point was that speed is situational, not absolute.

Code pattern 1 — traditional indexed loop:

Here size is checked in every iteration as the loop condition, and get(i) fetches by index. For ArrayList (backed by an array) each get(i) is a direct array access — O(1) per iteration, O(n) total.

Code pattern 2 — iterator-based loop:

Using the standard hasNext/next methods. For LinkedList the iterator holds a node pointer and advances by one link per step — O(1) per iteration, O(n) total. For ArrayList it advances an internal index similarly.

The statement made in the lecture was that pattern 1 will run faster than pattern 2 under certain conditions — it is situational. If the list size is very large and the underlying system parameters efficiently support indexed collection access, the traditional indexed form wins on ArrayList; otherwise the iterator form may be comparable and is always correct for LinkedList. Invoking a RandomAccess tag signals that efficient random access is available, but the actual choice of retrieval path in the background can depend on system resources, JIT optimization, and how the program occupies them. It is not a strict policy that using the collections framework forces iterator-based access; the implementation may use the traditional path when it is more efficient, and still satisfy the efficiency promise that RandomAccess announces.

Concrete cost table:

List type get(i) per iteration Indexed loop total (n elements) Iterator loop total
ArrayList (RandomAccess) O(1) array access O(n) — fast, cache-friendly O(n) — similar
LinkedList (no RandomAccess) O(n) walk from nearest end O(n²) — catastrophically slow O(n) — single walk

The framework's Collections utility class embodies this: many algorithms check if (list instanceof RandomAccess) and switch between an indexed algorithm and an iterator algorithm automatically.

Choosing by marker — conceptual trace: Suppose n = 10,000. On ArrayList, indexed loop for (i=0;i<list.size();i++) sum += list.get(i) does 10,000 array reads — about microseconds. On LinkedList the same loop does node steps — roughly 50 million steps — visibly slower. Replacing it with for (int v : list) or iterator while(it.hasNext()) follows 10,000 links once — again microseconds. The instanceof RandomAccess test predicts this before you run.

Visual intuition: plot execution time vs list size n. For ArrayList, both indexed and iterator curves are shallow, nearly overlapping lines. For LinkedList, the iterator curve is shallow but the indexed loop is a steep parabola shooting upward — the gap between the two curves is the penalty for ignoring RandomAccess.

Pitfalls: (1) Assuming RandomAccess makes iteration automatically parallel — it does not; it only describes indexed cost. (2) Writing for (int i=0; i<list.size(); i++) on a LinkedList because it "looks simpler" — simpler code that is O(n²) is not simpler in production. (3) Checking instanceof RandomAccess on a non-List such as HashSet — always false, but the check is meaningless there.

Recap: RandomAccess is an empty marker that tags ArrayList (fast get(i)) and not LinkedList (slow get(i)). Use the tag to decide between an indexed loop (good for RandomAccess lists) and an iterator/for-each loop (essential for sequential-access lists). This distinction guides every bulk algorithm in the framework.

Real-world and domain connection: In high-frequency trading risk engines, portfolios as ArrayList<Position> are scanned with indexed loops for maximum throughput — the RandomAccess tag guarantees that choice is safe. In workflow engines, task chains stored as LinkedList<Step> are traversed with iterators to avoid quadratic slowdown. The broader lesson: the same traversal logic can hide two orders of magnitude in performance, and the marker lets generic code adapt without knowing the concrete class upfront.

22.9 Map and SortedMap Interfaces

22.9.1 Map — Association of Keys and Values

Hook: A list remembers where you stored something (index 3). But how do you store an employee's ID "C" and later ask "what is C's salary?" without searching the whole list?

ArrayList and LinkedList store single values — one element per position addressed by an integer index. Map stores associations between keys and values : each entry is a pair, and the key is the lookup handle. Visualize a two-column table:

  • Key A maps to value 100
  • Key B maps to value 200
  • Key C maps to value 300
  • and so on.

The mapping means a key can be used to retrieve its value later — provide key C and obtain 300 — because the pair was stored together. If you store key-value pair ("C", 1003), a later map.get("C") returns 1003. This is the functioning of the Map interface.

Formal idea: Map<K,V> is a generic interface that maps keys of type to values of type . Unlike Collection<E>, it does not extend Collection; it lives alongside it. Core verbs use put not add, because you store a pair:

Each key maps to at most one value; putting the same key again overwrites the previous value. Companion T6 §19 notes: "Maps store key/value pairs. Although maps are part of the Collections Framework, they are not collections in the strict use of the term. You can, however, obtain a collection-view of a map."

Non-technical framing from the lecture: the same map idea appears in a dictionary where a word (key) maps to a definition (value), or a phone book where a name maps to a number.

Visual intuition: draw a map as two vertical columns with horizontal arrows. Left column: keys A, B, C, D stacked vertically. Right column: values 1001-1004 aligned. Each arrow from a key lands on its value. Adding a pair draws a new arrow; overwriting a key reroutes its arrow. The takeaway: lookup follows the arrow from key to value, not a linear scan.

Scope: Keys should be immutable and must implement equals/hashCode correctly for hash-based maps (HashMap). If you mutate a key object after insertion, get may fail to find it.

22.9.2 Worked Example — Map<String,Integer>

Implementation notes:

  • Import as always.
  • Declaration uses two type parameters — for keys, for values:

String is the key type, Integer the value type, analogous to fixing to String and to Integer in earlier generics on Pair<T,U>.

  • Adding pairs uses , not , because a whole key-value pair is stored, not a single element:

Picture a virtual table:

Key Value
A 1001
B 1002
C 1003
D 1004

Output after printing the map directly (System.out.println(mp)) shows each key mapped to its value in that paired form, though HashMap does not guarantee iteration order.

Traversal via entrySet and for-each:

An entry object associated with the map's entrySet is used — entrySet() returns a Set<Map.Entry<String,Integer>> view over the pairs. Each iteration prints the key and its value via e.getKey() and e.getValue(). Results printed are:

in some order for HashMap (insertion order for LinkedHashMap). Sense-check: mp.get("C") returns 1003 as an Integer; mp.containsKey("Z") returns false; mp.size() is 4 after the four puts.

Overwrite trace: If you then call mp.put("C", 9999), get("C") becomes 9999 and size() stays 4 — the map has no duplicate keys, only value replacement.

Pitfalls: Writing mp.add("A",1001) — there is no add on Map; the correct verb is put. Confusing keySet() (returns Set<K>) with entrySet() (returns pairs). Also, using == to compare Integer values beyond the cached range -127..128 may fail — use equals.

22.9.3 SortedMap — Same Pairs but Sorted by Keys

SortedMap works on the same key-value principle but guarantees that entries are maintained in ascending order based on keys (not values). Formally:

with additional methods K firstKey(), K lastKey(), SortedMap<K,V> headMap(K toKey), tailMap, subMap. Keys are ordered by natural ordering (Comparable) or by a supplied Comparator.

If pairs are inserted in the order D 1001, A 1002, B 1003, C 1004, internal storage (typically a balanced tree in TreeMap) reorders them as A 1002, B 1003, C 1004, D 1001 — sorted by the key alphabetically, with values following their keys. The sorted order is preserved regardless of insertion order.

Worked illustration:

Insertion order was D, A, B, C, but printing the map (System.out.println(smp)) shows

in ascending key order (lexicographic for strings). Note values did not sort — value 1001 moved with its key D to the last position. Sense-check: smp.firstKey() returns "A" and smp.lastKey() returns "D"; smp.get("B") still returns 1003.

Iteration: for (Map.Entry<String,Integer> e : smp.entrySet()) now visits A, B, C, D guaranteed, unlike HashMap where order is unspecified.

The discussion stressed that unlike ArrayList/LinkedList where add was used, Map and SortedMap must use put because the underlying organization stores a pair, and the difference between Map and SortedMap is solely the ordering guarantee on keys — the verb put and the two-slot generic Map<K,V> are shared. Keys were emphasized as String and values as Integer to keep the two typed slots clear, and the same put method is used for both interfaces.

Visual intuition: animate values dropping into a TreeMap. Each new key slides into its alphabetically correct slot, like inserting a name card into an already sorted rolodex — the rolodex never looks unsorted. Contrast with HashMap where cards are tossed into buckets by hash code and appear jumbled.

Scope and pitfalls: SortedMap keys must be mutually comparable — putting a String and an Integer into the same TreeMap (via raw type dodge) throws ClassCastException at runtime. Null keys are not allowed in TreeMap (no natural order). HashMap allows one null key; TreeMap does not.

Recap: Map<K,V> is the key-value table using put/get; SortedMap<K,V> adds the guarantee that entrySet() iterates in ascending key order (implemented by TreeMap). Both are the framework's answer to associative lookup, complementary to the positional world of List.

Real-world connection: Configuration maps (Map<String,String> config), session caches (Map<SessionId,User>), and symbol tables in compilers are all HashMap uses. When you need to list configuration keys alphabetically or iterate date-keyed trades in chronological order, swapping HashMap for TreeMap (a SortedMap) gives sorted iteration for free — a one-line declaration change that the two-slot generic Map<K,V> enables.

22.10 Set and SortedSet Interfaces

22.10.1 Set — The No-Duplicates Rule

Hook: What if a bug that inserts the same customer ID twice could silently corrupt a billing run — could the collection itself prevent the second insert instead of you writing an if (!list.contains(id)) before every add?

In mathematics, a set is a collection in which duplicate values cannot be stored. If a collection holds A, B, C, D, A, E, mapping it to a set yields A, B, C, D, E — the repeated A appears only once. Set in the collections framework embodies exactly this rule. It inherits methods from Collection and adds the restriction that duplicate elements are rejected on insertion.

Formal contract: Per T6 §19, interface Set<E> extends Collection<E> declares no new methods (except the refined add contract). Its boolean add(E obj) returns false if the element is already present — the set is unchanged — and true if it was newly added. Equality is based on equals/hashCode (for HashSet) or on comparison (for TreeSet). The interface documentation states: a set contains no pair of elements e1, e2 such that e1.equals(e2).

The business motivation makes the value concrete: if customer IDs must not duplicate, writing manual duplicate-check logic (comparisons before each insert) is overhead that can be eliminated by directly using Set. The moment Set is used, the worry about duplicates is handled by the framework itself. No explicit check code is needed; the standard add method takes care of it — returning false on a duplicate is the signal.

Visual intuition: picture a bouncer at a club door with a guest list Set. Each name is checked once; the first A enters and is crossed onto an inside list; the second A is stopped at the door with a polite "already inside" and add returning false. A List bouncer, by contrast, lets every A through and counts them.

Scope: Set assumes that element equality is well-defined via equals/hashCode. If you store a custom class without overriding both, two logically equal objects may be treated as distinct and duplicates will slip through.

22.10.2 SortedSet — No Duplicates plus Sorted Order

SortedSet builds on Set with two rules combined:

  • Rule 1: No duplicate elements (inherited from Set).
  • Rule 2: Elements stored in sorted order (ordered collection, ascending).

Formally:

with additional ordered-view methods E first(), E last(), SortedSet<E> headSet(E to), tailSet, subSet.

Its definition in the lecture is that it contains methods inherited from Set and adds the feature that all elements are kept in sorted order. For strings, sorted means alphabetical ascending ("C" < "Java" < "Python"); for numbers, numeric ascending (1 < 2 < 10). The ordering is by natural ordering (Comparable) or a supplied Comparator. The framework again supplies the guarantee without requiring the programmer to write sorting or deduplication logic — TreeSet is the classic implementation (balanced tree).

Rule-check helper: to test understanding, apply both rules in order — first deduplicate, then sort. Many students sort then deduplicate and get confused; the framework does deduplication on insertion, then maintains sorted order automatically.

Scope and assumptions: All elements in a SortedSet must be mutually comparable — adding String and Integer (via raw-type escape) throws ClassCastException. Null is generally disallowed in TreeSet because there is no ordering for null. HashSet makes no ordering promise at all — its iteration order is hash-dependent and may appear random.

22.10.3 Worked Examples

Set example — HashSet deduplication, iteration order not guaranteed:

Step-by-step trace:

  • add("C") → true, set becomes {C}, size 1.
  • add("C++") → true, {C, C++}, size 2.
  • add("Java") → true, {C, C++, Java}, size 3.
  • add("Python") → true, {C, C++, Java, Python}, size 4.
  • add("Python") → false, duplicate rejected, set stays {C, C++, Java, Python}, size still 4.
  • add("Java") → false, duplicate rejected, size still 4.

Elements added are C, C++, Java, Python, Python (duplicate), Java (duplicate). Printing setObj (System.out.println(setObj)) shows only four distinct elements: C, C++, Java, Python, in some order determined by hashes (e.g., [Java, C++, C, Python] is possible — hash-based ordering means the exact order is hash-dependent and not sorted). The repeated Python and Java are discarded automatically. Note the same method name add is used here as with ArrayList and LinkedList, yet its behaviour differs — here it enforces deduplication — which vividly demonstrates the uniform-name, different-behaviour standardization that is the beauty of the framework. Simply printing the set name shows a clean list with no duplicates, and no explicit duplicate-check code was written. setObj.contains("Java") would return true; setObj.contains("Ruby") false.

Sense-check: setObj.size()==4 confirms exactly two inserts were rejected; setObj.add("C") again would still return false.

SortedSet example — TreeSet deduplication plus sorting:

Step-by-step:

  • add("C") → true, tree {C}.
  • add("Python") → true, {C, Python} — Python sorts after C.
  • add("Java") → true, {C, Java, Python} — Java inserts between C and Python (C < Java < Python lexicographically; compare character-by-character: 'C' (67) < 'J' (74) < 'P' (80)).
  • add("C") → false, duplicate removed, tree unchanged.

Input values are C, Python, Java, C (duplicate C). Applying Rule 1 removes the duplicate C, leaving three distinct values. Applying Rule 2 sorts them alphabetically ascending: C comes first, then Java, then Python. The printed result is — guaranteed sorted order for TreeSet. Additional SortedSet queries would give first() = C, last() = Python, headSet("Python") = [C, Java]. The same add method that appended in ArrayList/LinkedList and deduplicated in Set now also sorts in SortedSet, again without any explicit sorting code. Sense-check: setObj2.size()==3 confirms deduplication, and setObj2.toString() order C, Java, Python confirms sorting — both rules satisfied together.

Visual intuition: draw two columns. Left: HashSet bucket diagram — elements drop into buckets by hashCode, appearing scattered; duplicates bounce off. Right: TreeSet balanced tree — each new element walks down the tree to its sorted leaf position; duplicates hit an existing node and bounce. Iteration of the left is bucket order; iteration of the right is inorder (sorted).

Pitfalls: (1) Relying on HashSet iteration order — it is not sorted and may change across runs; use LinkedHashSet for insertion order or TreeSet for sorted order. (2) Defining equals without hashCodeHashSet will then fail to deduplicate. (3) Expecting SortedSet to sort by insertion order — it sorts by comparison, not by time. (4) Adding null to TreeSet — throws NullPointerException because null has no comparison.

Recap: Set is the no-duplicates guarantee (add returns false on duplicate); SortedSet is Set plus automatic ascending order. Both reuse the same add name that meant "append" on lists but now means "deduplicate (and sort)" — the framework's uniform verbs, context-sensitive behaviour, distilled to one example. Choosing HashSet (hash table, O(1) average), LinkedHashSet (hash plus insertion-order linked list), or TreeSet (balanced tree, O(log n), sorted) is the implementation decision; the interface Set/SortedSet on the left keeps calling code interchangeable.

Real-world connection: Fraud detection deduplicates transaction IDs with HashSet<String> seen; once seen, add returning false flags a duplicate. Leaderboards use TreeSet<Score> to keep scores sorted and unique automatically — inserting a new score places it in rank order without an explicit sort step. The broader placement: Set/SortedSet alongside Map/SortedMap completes the framework's taxonomy — List for ordered duplicates-allowed, Set for unordered duplicates-forbidden, SortedSet for ordered duplicates-forbidden, Map for associative lookup.

Exam Guidance Summary

No explicit mark distribution, question type, or exam-date information was stated in this session. The lecture instead emphasized recurring mechanics and contracts that are highly examinable. Treat the implicit guidance below as your revision checklist.

Exam note: Generics is flagged as a preliminary requirement for collections even though the word may not appear in the module title — understanding , , forms and how is replaced per instantiation is necessary to read every later collection declaration. Be fluent translating ArrayList<String> to "list of strings" and Map<K,V> to "map from K to V" without hesitation.

  • Standard method names are uniform and examinable. Expect to be able to use the same names across ArrayList, LinkedList, Set, SortedSet, Map and to explain how the same name behaves differently per interface: add appends on List, deduplicates on Set, sorts + deduplicates on SortedSet; put is the outlier for Map because it stores a pair. Other standard verbs that recur are addAll, clear, contains, isEmpty, size, put for maps, set/remove/indexOf/lastIndexOf for lists, hasNext/next/remove for iterators, hasPrevious/previous for ListIterator, plus first/last on SortedSet/SortedMap.
  • ArrayList index mechanics are a favourite for implementation questions. Practice the step-by-step index walks: indexed add(index,E) shifts right and increases size by 1; set(index,E) overwrites without changing size; remove(index) shifts left and decreases size by 1; far-index add(6,val) when size==5 throws IndexOutOfBoundsException — valid indices are 0..size inclusive, gaps are illegal. Be ready to trace the Rahul/Java/Object/Mic/Fortran sequence and the subsequent set, remove(2), remove("Fortran"), indexOf/lastIndexOf walk.
  • Hierarchy relationships are conceptual exam staples. Know: Collection extends Iterable; List, Queue and Set extend Collection; SortedSet extends Set; Map/SortedMap sit outside Collection but belong to the framework; concrete classes (ArrayList, LinkedList, Vector, Stack, PriorityQueue/ArrayDeque, HashSet, LinkedHashSet, TreeSet, HashMap, TreeMap) and the marker nature of RandomAccess (empty interface, ArrayList implements it, LinkedList does not, instanceof RandomAccess chooses indexed vs iterator algorithm). Minor diagram variations across platforms were noted as normal — focus on the stable trunk, not transient layout differences.
  • Iterator/ListIterator contract nuances are frequently tested as true/false. Remember: Iterator starts before first element; hasNext/next forward only; remove allowed once per next with no intervening external add (otherwise IllegalStateException / ConcurrentModificationException); ListIterator adds hasPrevious/previous and must be positioned at size() before hasPrevious becomes true; for-each cannot safely remove via the collection.
  • Practical advice given explicitly: always import java.util.* before using collections, and use the generic bounded form such as rather than raw ArrayList when a single type is intended to get compile-time safety and avoid ClassCastException.

How to study: For each interface, write its declaration, its core methods, one correct instantiation, and one trace of add/remove behaviour. Then close the book and redraw the hierarchy from Iterable down to concrete classes with blue/green colour coding. Finally, time yourself tracing two code snippets: one on ArrayList<String> with set/remove/indexOf, one on TreeSet/HashMap ordering — these traces mirror the implementation-question format hinted in the lecture.

Key Industry Applications

The collections framework is not an academic exercise — it is the daily toolkit for building large Java systems where reuse, type safety and correctness at scale matter. The key patterns from this lecture appear directly in production code.

  • Rapid development with standard implementations: Direct use of ArrayList, LinkedList, Vector, Stack, PriorityQueue, ArrayDeque, HashSet, LinkedHashSet and TreeSet from java.util avoids writing and maintaining custom versions of arrays, linked lists, queues, sets and maps, with uniform method names (add, addAll, clear, contains, isEmpty, size) working across all of them. In enterprise onboarding, a new hire productive on day one because add means the same everywhere — no team-specific "insertElement" dialect to learn.
  • Data organization choice without syntax tax: Arrays (fixed continuous blocks), linked lists (distributed nodes with links), queues (FIFO ticket-counter model), trees and maps (key-value tables) represent different storage strategies that the framework unifies — the same add pattern can feed any of them, letting the developer choose based on the problem (random access vs head-insertion vs lookup speed) rather than syntax burden. A logistics planner switches from ArrayList<Route> to LinkedList<Route> for frequent reordering without touching loop logic.
  • Generics for type safety at codebase scale: Using parameterized types such as , , , and enables compile-time guarantees that a holds only strings or a holds string keys with integer values, preventing runtime type mismatches in codebases with millions of lines. The nightly build catches list.add(42) on a List<String> before it reaches testing, saving costly production ClassCastException incidents that once plagued Object-based collections.
  • Deduplication and ordering without custom logic: Set automatically rejects duplicate entries (e.g., ensuring customer IDs are unique) and SortedSet additionally maintains sorted order (e.g., alphabetical listings), removing the need for manual duplicate checks or explicit sorting code. Fraud pipelines deduplicate event IDs with HashSet; HR dashboards present employees in TreeSet<Employee> sorted by name with a comparator — both with a single add call.
  • Key-value mapping as the backbone of lookup: Map and SortedMap store key-value pairs such as identifiers mapped to numeric values (A->1001, B->1002 ...) with put/entrySet iteration, and SortedMap guarantees ascending key order — the same pattern underpins lookup tables, configuration maps, JSON-like stores, and in-memory caches in applications. Swapping HashMap for TreeMap turns an unsorted cache into a chronologically ordered audit log with zero logic change beyond the declaration.
  • Uniform traversal for polymorphic processing: Iterator with hasNext/next/remove and ListIterator with hasPrevious/previous provide a single traversal idiom across ArrayList and LinkedList, while RandomAccess as a marker signals when indexed get(i) access will be efficient — a pattern used when iterating over large in-memory collections. Batch processors expose Collection<Transaction> transactions and iterate without knowing whether the underlying source was a list or a set.
  • For-each loop for clean, modern iteration: The enhanced for loop introduced in J2SE 5 simplifies scanning any generic collection — for (Order o : orders) process(o); — a widely adopted idiom in contemporary Java codebases that hides iterator boilerplate and reduces off-by-one errors. Combined with generics, it is now the default traversal taught in style guides from Android to Spring.

Together these applications show the framework's central promise: write once against interfaces (List, Set, Map), instantiate the engine that fits the performance profile (ArrayList vs LinkedList, HashSet vs TreeSet, HashMap vs TreeMap), and let the uniform verbs and type-safe generics carry the same code across products, teams and years.

OODAP Lecture 22 notes · Java Collections Framework and Generics

Object Oriented Design, Analysis and Programming Architecture· postgraduate· 2026-08-19

Sections Breakdown

122.1 Collections Framework — Overview and Foundational Idea

Defines the framework as a uniform hierarchy for groups of objects and its four design goals, contrasting pre-framework ad-hoc classes with the J2SE 5 generics/for-each evolution.

222.2 Generics — Parameterized Types

Introduces generics as parameterized types T, single-parameter Identity<T>, two-parameter Pair<T,U>, generic interfaces DemoInterface<T1,T2>, plus supporting depth on reference-type restriction, erasure, and wildcards/bounds.

322.3 Collections Hierarchy, Core Interfaces and Standard Methods

Collection<E> as generic root extending Iterable, blue-interface/green-class hierarchy with List/Queue/Set and Map family, standard methods add/addAll/clear/contains/isEmpty/size/iterator, and skeletal helpers AbstractCollection/AbstractList.

422.4 ArrayList — Dynamic Arrays

Contrasts fixed arrays with resizable ArrayList, constructors and capacity vs size, raw heterogeneous list versus ArrayList<String> with full index-shift traces for add/set/remove/indexOf and forward/backward/for-each traversal.

522.5 LinkedList

Doubly-linked distributed storage implementing List+Queue/Deque; addFirst/addLast/getFirst/getLast/removeFirst/removeLast; String and Integer traces with identical add/remove shifting semantics to ArrayList but O(1) ends, plus user-input loop Q&A.

622.6 Iterator Interface

Uniform forward traversal via hasNext/next/remove; ArrayList and LinkedList traversal traces showing identical verbs over different storage; strict remove-once-per-next and no-intervening-add rules with IllegalState/ConcurrentModification failures.

722.7 ListIterator Interface

Bidirectional extension of Iterator for List only; listIterator() vs listIterator(index) cursor model; forward hasNext/next trace a→z and backward hasPrevious/previous trace z→a on ArrayList [a,b,x,y,z].

822.8 RandomAccess Interface

Marker interface with no members signalling efficient indexed access; ArrayList implements it, LinkedList does not; situational speed comparison shows indexed get(i) O(1) on ArrayList but O(n²) loop on LinkedList where iterator is O(n).

922.9 Map and SortedMap Interfaces

Map<K,V> as key-value table using put/get/entrySet vs Collection's add; HashMap example A->1001..D->1004 with entrySet for-each; SortedMap/TreeMap guarantees ascending key order reordering D,A,B,C to A,B,C,D.

1022.10 Set and SortedSet Interfaces

Set as no-duplicates via add returning false; SortedSet adds ascending sorted order; HashSet trace C/C++/Java/Python deduplicates to 4 unordered, TreeSet trace C/Python/Java/C deduplicates and sorts to [C, Java, Python].

11Exam Guidance Summary

Implicit exam guidance emphasizing uniform verbs, index-shift mechanics, hierarchy, iterator contracts, and import/generic practice.

12Key Industry Applications

Industrial uses of collections, generics, sets/maps and iterators for rapid development, deduplication, lookup and uniform traversal.

Postgraduate students in Object Oriented Design, Analysis and Programming

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Collections Framework — Overview and Foundational Idea

Must-know: Framework = hierarchy of interfaces+classes for group-of-objects as single unit; four goals are high performance, efficient fundamental impls, uniform working via same method names, and ready standard impls; pre-framework classes Dictionary/Vector/Stack/Properties were non-uniform.

⚠️ Top pitfall: Confusing framework with a single class; assuming List and Set behave identically with add (List appends, Set deduplicates).

Self-check: Name the four framework goals and one pre-framework ad-hoc class they replaced.

Connects to: 22.2, 22.3

Generics — Parameterized Types

Must-know: Generics = type as parameter in <>; T works only with reference types; single class Identity<T> behaves as Long or String per instantiation; Pair<T,U> has two independent slots; interface DemoInterface<T1,T2> swaps return/param types.

⚠️ Top pitfall: Writing Gen<int> with primitive; thinking compiler creates separate classes (it uses erasure); confusing <> with comparison operators.

Self-check: If class Pair<T,U> is instantiated as Pair<String,Integer> and as Pair<Integer,String>, what types do obj1 and obj2 have in each case?

Connects to: 22.3, 22.4

Collections Hierarchy, Core Interfaces and Standard Methods

Must-know: Collection<E> extends Iterable; List/Queue/Set extend Collection, Map/SortedMap separate; blue=interfaces green=classes; core methods add(E), addAll(Collection), clear, contains, isEmpty, iterator, remove, size; Map does NOT extend Collection.

⚠️ Top pitfall: Saying Map extends Collection; confusing PriorityQueue with FIFO; over-focus on Abstract helpers instead of five main concrete classes.

Self-check: Does Map extend Collection? Name the three direct subinterfaces of Collection and one concrete class for each.

Connects to: 22.4, 22.5, 22.9, 22.10

ArrayList — Dynamic Arrays

Must-know: ArrayList = resizable List, extends AbstractList; capacity != size; add(E) appends, add(index,E) shifts right, set(index,E) overwrites, remove(index) shifts left; indexOf/lastIndexOf return first/last; traversal via index loop or for (T x : list).

⚠️ Top pitfall: Far index add(6,val) when size=5 throws IndexOutOfBoundsException; confusing add vs set size semantics; confusing remove(int) vs remove(Object) on List<Integer>.

Self-check: Starting from [Rahul, Java, Object, Mic, Fortran], what does set(2,Testing), then remove(2), then remove("Fortran") produce, and what are indexOf/lastIndexOf of Java?

Connects to: 22.5, 22.6

LinkedList

Must-know: LinkedList extends AbstractSequentialList implements List+Deque/Queue; distributed nodes; addFirst/addLast O(1), get(i) O(n); same add/index shifting semantics as ArrayList; illegal add(10) when size=8 throws IndexOutOfBoundsException.

⚠️ Top pitfall: Using indexed get(i) loop on LinkedList gives O(n²); confusing add(E) which is addLast; calling getFirst on empty list throws NoSuchElementException.

Self-check: Why does LinkedList implement both List and Queue? What is the cost of get(500) in a 1000-element LinkedList versus ArrayList?

Connects to: 22.4, 22.6

Iterator Interface

Must-know: Iterator<E> methods hasNext/next/remove; iterator starts before first element; same traversal for ArrayList and LinkedList; remove allowed once per next with no external add between.

⚠️ Top pitfall: Calling remove without next throws IllegalStateException; add between next and remove throws ConcurrentModificationException; enhanced for cannot safely remove via collection.

Self-check: On [Java,Object,Fortran], trace iterator.next()->add()->remove() — what exception and why? What is the correct next->remove sequence?

Connects to: 22.4, 22.7

ListIterator Interface

Must-know: ListIterator extends Iterator, List-only; adds hasPrevious/previous plus nextIndex/previousIndex/set/add; cursor sits between elements; listIterator(size) starts at end for backward traversal.

⚠️ Top pitfall: Calling hasPrevious immediately after listIterator() at start returns false; confusing Iterator (forward only) with ListIterator (both ways).

Self-check: On [a,b,x,y,z], what does listIterator(2).next() return? What does hasPrevious return at start vs after advancing to end?

Connects to: 22.6, 22.8

RandomAccess Interface

Must-know: RandomAccess is empty marker; ArrayList supports efficient random access, LinkedList does not; indexed for-loop O(n) on ArrayList but O(n²) on LinkedList; check instanceof RandomAccess to choose strategy.

⚠️ Top pitfall: Thinking marker adds methods; using indexed loop on LinkedList for large n; expecting RandomAccess on Set.

Self-check: Why does ArrayList implement RandomAccess but LinkedList does not, and what does that imply for a 10k-element traversal loop?

Connects to: 22.4, 22.5, 22.6

Map and SortedMap Interfaces

Must-know: Map<K,V> does not extend Collection, uses put(K,V)/get(key)/entrySet; HashMap unordered, TreeMap SortedMap iterates by ascending key; two generic slots K for key, V for value.

⚠️ Top pitfall: Calling add on Map instead of put; thinking SortedMap sorts by value; inserting incomparable keys into TreeMap throws ClassCastException.

Self-check: Insert D 1001, A 1002, B 1003, C 1004 into TreeMap — what order does entrySet iterate and what does get("C") return?

Connects to: 22.2, 22.10

Set and SortedSet Interfaces

Must-know: Set = no duplicates (add returns false on dup); SortedSet = Set + sorted ascending; HashSet unordered hash, TreeSet sorted tree; same add name behaves as append on List, deduplicate on Set, sort+deduplicate on SortedSet.

⚠️ Top pitfall: Assuming HashSet iteration is sorted; forgetting equals/hashCode contract for custom types; adding null to TreeSet throws NPE; adding incomparable types to TreeSet throws ClassCastException.

Self-check: Add C, Python, Java, C to TreeSet — what are size, iteration order, first() and last()? How does HashSet differ?

Connects to: 22.3, 22.9

Exam Guidance Summary

Must-know: Uniform verbs across collections; index arithmetic for add/set/remove; hierarchy List/Queue/Set vs Map; iterator remove rules; always import java.util and use generic forms.

⚠️ Top pitfall: Forgetting that far-index add throws IndexOutOfBoundsException; confusing Map with Collection hierarchy.

Self-check: Trace an ArrayList indexed add/set/remove sequence and state valid index range.

Connects to: None

Key Industry Applications

Must-know: Reuse via standard impls, data-organization choice, generics type safety, Set deduplication, Map lookup, iterator traversal patterns.

⚠️ Top pitfall: Reimplementing what the framework already provides; using raw types in large codebases.

Self-check: Name one production use for HashSet and one for TreeMap sorted iteration.

Connects to: None

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.