Core and Web-based Java
Collections, Ordering, Generics and Reflection
PGCP-AC
Collections organize groups of values, generics express the permitted element types, ordering defines how values are compared and reflection inspects types at runtime. These facilities meet in everyday code: a sorted map needs a valid comparator, a hash set depends on equality, a generic method must state what it reads or writes and a framework may discover annotated classes reflectively. Correct choices begin with the required behaviour rather than a familiar implementation name.
1. The collection hierarchy
Collection<E> is the root interface for groups of elements. Its main subinterfaces include List, Set, Queue and Deque. Map<K,V> belongs to the collections framework but does not extend Collection because it stores key–value associations rather than individual elements.
| Abstraction | Central promise | Typical use |
|---|---|---|
List | Ordered sequence; duplicates allowed | Rows, history, indexed values |
Set | Unique elements | Membership, deduplication |
Queue | Elements waiting for processing | Tasks, breadth-first traversal |
Deque | Insert and remove at both ends | Stack or double-ended queue |
Map | Unique keys mapped to values | Lookup tables, indexes |
Program variables should normally use the least specific interface that expresses the required operations:
List<String> names = new ArrayList<>();
Set<String> tags = new HashSet<>();
Map<Long, Student> students = new HashMap<>();
This keeps the implementation replaceable and prevents code from depending accidentally on irrelevant details.
2. ArrayList and LinkedList
ArrayList stores references in a resizable array. Indexed access is constant time, appending is amortized constant time and middle insertion or removal shifts later elements.
“Amortized O(1)” means that occasional growth requires copying, but the average cost across a sequence of appends remains constant. It does not promise that every individual append has identical cost.
LinkedList stores linked nodes and implements both List and Deque. Adding or removing at a known end is constant time, but locating an index requires traversal. It also uses extra memory per node and has poor memory locality. Consequently, ArrayList is the better default for most list workloads. Use a deque implementation such as ArrayDeque for frequent end operations.
Vector is a legacy synchronized resizable list. Its individual operations are synchronized, but a multi-step action still needs an atomic design.
3. Set implementations
HashSet offers hash-based membership without an iteration-order guarantee. It uses equals and hashCode, so elements must obey their contract and should not mutate equality-defining state while stored.
LinkedHashSet adds predictable encounter order, normally insertion order, while preserving hash-based uniqueness. It is useful when deduplication must retain the first-seen sequence.
TreeSet maintains sorted order with a balanced tree. Its add, remove and lookup operations are typically O(log n). It uses natural ordering or a supplied Comparator. A comparison result of zero means the set considers the values equivalent for its operations, even if equals says otherwise.
Set<String> codes = new LinkedHashSet<>();
codes.add("B");
codes.add("A");
codes.add("B");
System.out.println(codes); // [B, A]
4. Map implementations
HashMap supplies expected constant-time basic operations with sound hashing. It permits one null key and multiple null values, but it promises no iteration order.
LinkedHashMap maintains a linked encounter order. Its default is insertion order; an access-order configuration can support least-recently-used cache logic when combined with controlled eviction.
TreeMap maintains keys in natural or comparator order and supports range views such as subMap, headMap and tailMap. Comparison equality determines key equivalence.
Hashtable is a legacy synchronized map that rejects null keys and values. ConcurrentHashMap also rejects nulls, but provides concurrency-oriented operations and better parallel access rather than simply copying Hashtable's locking model.
5. Hashing and key correctness
A hash structure first uses hashCode to find a region and then equals to distinguish candidates. Equal objects must have equal hash codes. Unequal objects may collide.
record ProductCode(String value) {}
Map<ProductCode, Integer> stock = new HashMap<>();
stock.put(new ProductCode("P10"), 8);
System.out.println(stock.get(new ProductCode("P10"))); // 8
Records derive value equality and matching hashes from components. For ordinary key classes, override both methods using the same stable fields. A mutable key can become unreachable through normal lookup after its hash-relevant state changes.
HashMap iteration order must never be treated as insertion or sorted order, even if a small example appears stable.
6. Comparable and natural ordering
Comparable<T> defines a type's natural ordering:
record Student(int rollNumber, String name)
implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return Integer.compare(rollNumber, other.rollNumber);
}
}
A comparison returns a negative value, zero or a positive value. Do not subtract numeric fields because overflow can reverse the result. A natural ordering should be antisymmetric, transitive and consistent in sign. Ideally, comparison zero agrees with equals, especially when values enter sorted sets or maps.
Natural order should represent the most obvious stable ordering for the type. A class need not implement Comparable if no single order is natural.
7. Comparator strategies
Comparator<T> defines an external ordering without changing the class:
Comparator<Student> byNameThenRoll =
Comparator.comparing(Student::name)
.thenComparingInt(Student::rollNumber);
List<Student> students = new ArrayList<>();
students.sort(byNameThenRoll);
Useful methods include reversed, thenComparing, naturalOrder, nullsFirst and nullsLast. A comparator used by TreeSet or TreeMap must remain stable while elements or keys are stored. If comparison depends on a mutable field, changing it can violate tree placement.
A PriorityQueue uses a comparator to keep its head as the least element, but iteration over the queue is not sorted. Repeated poll() operations reveal priority order.
8. Queue and Deque operations
Queue operations come in exception-throwing and special-value forms:
| Purpose | Throws on failure | Special-value form |
|---|---|---|
| Insert | add(e) | offer(e) |
| Remove head | remove() | poll() |
| Examine head | element() | peek() |
poll and peek return null for an empty queue, so queues using these conventions generally should not store null.
ArrayDeque efficiently supports both ends. Use addFirst, removeFirst, addLast and removeLast for deque behaviour. It also replaces the legacy Stack: push, pop and peek operate at the front.
9. Iteration and modification
The enhanced for loop uses an iterator. Many ordinary collection iterators are fail-fast: unsupported structural modification outside the iterator may trigger ConcurrentModificationException.
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
if (iterator.next().isBlank()) {
iterator.remove();
}
}
Fail-fast behaviour is a bug-detection aid, not a concurrency guarantee. removeIf expresses common filtering more directly. Concurrent collections define their own iterator consistency rules.
ListIterator supports bidirectional traversal and controlled addition or replacement. Index-based removal in a forward loop can skip shifted elements unless the index is adjusted.
10. Backed views and immutable factories
map.keySet(), values() and entrySet() usually return views backed by the map. Removing a key through the key-set view removes the corresponding map entry. Adding directly to that key set is unsupported because no value is available.
subList is also a backed view, so structural changes require care. Arrays.asList(array) creates a fixed-size list backed by the array: set is supported, while add and remove are not.
List.of, Set.of and Map.of create unmodifiable collections that reject null. “Unmodifiable” does not make mutable element objects immutable. Collections.unmodifiableList(source) is a read-only view; changes made through the original source remain visible.
11. Collection algorithms and utilities
Collections is a utility class containing algorithms and wrappers; Collection is an interface. Common operations include sort, binarySearch, reverse, shuffle, min, max and unmodifiable or synchronized wrappers.
Binary search requires data already sorted under the same ordering used by the search. Violating this prerequisite gives an undefined insertion-position result rather than performing a linear fallback.
List.copyOf and related copy factories create unmodifiable snapshots of elements, while wrapper methods create views. The distinction determines whether later source changes are visible.
12. Generic classes and methods
Generics express relationships between types at compile time:
class Box<T> {
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
Box<String> box = new Box<>();
box.set("Java");
A generic method declares its type parameter before the return type:
static <T> T first(List<T> values) {
if (values.isEmpty()) throw new NoSuchElementException();
return values.get(0);
}
Bounds state required capabilities:
static <T extends Comparable<? super T>> T maximum(List<T> values) {
return Collections.max(values);
}
Multiple bounds place a class first, followed by interfaces: <T extends Base & Runnable>.
13. Invariance
Generic types are invariant. Although Integer extends Number, List<Integer> is not a subtype of List<Number>.
If it were, this unsafe sequence would become possible:
// Hypothetical and illegal:
List<Integer> integers = new ArrayList<>();
// List<Number> numbers = integers;
// numbers.add(3.14); // would corrupt the integer list
A List<Number> may contain Integer, Double or other Number values. A List<Integer> permits only Integer values. Wildcards express safe variance at a use site.
14. Upper-bounded wildcards
? extends Number means “some unknown type that is Number or a subtype.” Values can be read as Number:
static double total(List<? extends Number> values) {
double sum = 0;
for (Number value : values) sum += value.doubleValue();
return sum;
}
The method accepts List<Integer>, List<Double> and similar lists. It cannot safely add an Integer because the actual list might be a List<Double>. It may add null, though doing so is rarely useful.
The mnemonic “producer extends” describes a structure from which the method consumes produced values.
15. Lower-bounded and unbounded wildcards
? super Integer means an unknown type that is Integer or one of its supertypes:
static void addDefaults(List<? super Integer> destination) {
destination.add(0);
destination.add(1);
}
It accepts List<Integer>, List<Number> or List<Object>. Reading yields Object because the exact element type is unknown. “Consumer super” describes a destination that consumes Integer values.
List<?> means a list of an unknown type. It is useful when operations do not depend on the element type, such as size, iteration as Object or clearing. It is safer than raw List because the unknown type remains enforced.
16. Erasure and generic restrictions
Java implements most generics through type erasure. Type arguments guide compilation, while ArrayList<String> and ArrayList<Integer> normally share the same runtime class. The compiler inserts casts and may generate bridge methods to preserve polymorphism.
Consequences include:
- code cannot use
new T()because the runtime constructor is unknown; - code cannot create
new T[10]directly; instanceof List<String>is illegal because the argument is erased;- a class cannot implement the same generic interface with two different arguments;
- overloads whose signatures erase to the same form clash.
Generic type arguments must be reference types, so primitives use wrappers. Varargs with non-reifiable generic types can cause heap-pollution warnings.
17. Raw types and heap pollution
A raw type omits a generic argument:
List raw = new ArrayList<String>();
raw.add(42); // warning
List<String> text = raw; // unchecked warning
String value = text.get(0); // ClassCastException
Raw types exist mainly for compatibility with code written before generics. They disable useful compile-time checks and can move a type error far from its cause. Do not silence unchecked warnings until the operation has been proven safe and isolated.
18. Reflection foundations
Reflection begins with a Class<?> object:
Class<String> a = String.class;
Class<?> b = "text".getClass();
Class<?> c = Class.forName("java.lang.String");
Class literals do not need a string lookup. Class.forName can fail with ClassNotFoundException and may initialize the class.
Reflection can inspect constructors, methods, fields, modifiers, interfaces, superclass information, generic metadata and runtime-retained annotations. Methods named getMethods expose public methods including inherited ones, while getDeclaredMethods exposes methods declared directly in the class, regardless of access, but excludes inherited declarations.
19. Reflective construction and invocation
Constructor<Service> constructor =
Service.class.getDeclaredConstructor(String.class);
Service service = constructor.newInstance("production");
Method method = Service.class.getDeclaredMethod("start");
method.invoke(service);
Lookup and invocation are separate stages. Invocation wraps an exception thrown by the target method in InvocationTargetException; inspect its cause for the underlying failure.
Access checks, module boundaries, security policies and encapsulation still apply. Deep reflective access can fail even if setAccessible(true) is requested. Reflection also weakens refactoring safety and shifts errors to runtime, so ordinary typed calls are preferable when the type is known.
20. Annotations, enums and runtime discovery
Reflection sees an annotation only if it has RUNTIME retention:
if (handlerClass.isAnnotationPresent(Command.class)) {
Command command = handlerClass.getAnnotation(Command.class);
registry.put(command.value(), handlerClass);
}
Frameworks use this pattern for dependency injection, persistence mapping, validation and test discovery. Metadata alone performs nothing; framework code must scan, interpret and act.
Enums expose a fixed set of named instances. Reflective code can use Class.isEnum() and getEnumConstants(), but ordinary enum APIs are clearer when the type is statically known.
Practical considerations
| Mistake | Correct approach |
|---|---|
| Choosing LinkedList for indexed access | Prefer ArrayList |
| Assuming HashMap order | Choose LinkedHashMap or TreeMap explicitly |
| Giving TreeSet an inconsistent comparator | Define stable intended equivalence |
| Iterating a PriorityQueue to obtain sorted output | Repeatedly poll a copy |
| Mutating a hash key | Use stable equality state |
| Treating List<Integer> as List<Number> | Use a suitable wildcard |
Adding to ? extends T | Read from it; use ? super T for writes |
| Using raw collections | Supply type arguments |
| Treating reflection as an access bypass | Respect access and module boundaries |
Worked collection trace
List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(1);
numbers.add(3);
numbers.sort(Integer::compareTo);
System.out.println(numbers);
List permits duplicates and retains all three elements. The comparator orders integers ascending, so the result is [1, 3, 3]. If the values were placed in a TreeSet instead, comparison equality would eliminate the duplicate and iteration would produce [1, 3].
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.