Writing Comparators - Java

  1. Which comparator produces the order "b", "AA", "aA", "ab"?
    Answer: Comparator.comparingInt(String::length).thenComparing(String.CASE_INSENSITIVE_ORDER).thenComparing(Comparator.naturalOrder()) When chaining Java comparators, the order of thenComparing calls matters enormously — each subsequent comparator only breaks ties left by the previous one. The goal here is: sort by length first, then case-insensitively, then by natural order. Option A achieves exactly this. Starting with comparingInt(String::length), the strings group as {"b"} (length 1) and {"AA", "aA", "ab"} (length 2). Within the length-2 group, thenComparing(String.CASE_INSENSITIVE_ORDER) treats "AA", "aA", and "ab" as "aa" vs "ab", placing both "AA" and "aA" before "ab". Finally, thenComparing(Comparator.naturalOrder()) breaks the "AA"/"aA" tie using lexicographic order, where uppercase letters have lower Unicode values, so "AA" comes before "aA". The result: "b", "AA", "aA", "ab" — exactly correct. Option B is wrong because it applies naturalOrder() before CASE_INSENSITIVE_ORDER. This would sort case-sensitively first within each length group, placing "AA" before "aA" before "ab" by Unicode — which accidentally gives the right order here — but the comparison logic is semantically inverted from the requirement, and this approach would fail for other inputs. Option C is wrong because it sorts by the lowercased string value first, completely ignoring length as the primary key. This scrambles the intended priority. Option D is wrong because .reversed() flips the length sort to descending, producing length-2 strings before "b" — the opposite of what's needed. Study tip: When reading comparator chains, trace each level of sorting independently and ask "what does this step see as a tie?" — only tied elements ever reach the next comparator.
  2. Which comparator should be supplied to sort?
    Answer: Comparator.comparingInt(Job::priority) When sorting with a custom comparator in Java, two properties matter: correctness (does it sort in the right order?) and stability (do equal elements keep their original order?). The good news is that ArrayList.sort() — which delegates to Arrays.sort() — is already guaranteed to be a stable sort by the Java specification. That means equal elements automatically stay in their original relative order, and you don't need to do anything extra to preserve it. Comparator.comparingInt(Job::priority) is the correct answer (D). It compares jobs by their priority() value in ascending order — exactly what you want, since smaller numbers mean higher priority. Because sort() is stable, ties are resolved by original insertion order for free. Choice A is a trap that tries to manually enforce stability by breaking ties with System.identityHashCode. This is wrong for two reasons: identity hash codes are not guaranteed to reflect insertion order, and stability is already provided — so this "fix" is both unnecessary and incorrect. Choice B is subtly broken: when priorities are equal, it always returns -1, claiming j1 comes before j2. This violates the comparator contract (which requires antisymmetry: if compare(a, b) < 0, then compare(b, a) must be > 0). An invalid comparator can corrupt the sort entirely. Choice C uses .reversed(), which would sort jobs in descending priority order — meaning lower-priority jobs (larger numbers) would come first. That's the opposite of what the problem requires. Your takeaway: trust Java's stable sort. When you see a question asking you to "preserve relative order for equal elements," remember that List.sort() handles that automatically — no comparator tricks needed.
  3. Which expression reverses only the due-date portion of the ordering?
    Answer: Comparator.comparing(Task::dueDate).reversed().thenComparingInt(Task::priority) When chaining Java Comparator methods, you need to understand precisely what .reversed() flips — it reverses the entire comparator it's called on, not just the last step added. Option B, Comparator.comparing(Task::dueDate).reversed().thenComparingInt(Task::priority), works correctly because .reversed() is called immediately after the due-date comparator is built, flipping only that portion. Then .thenComparingInt(Task::priority) is appended after the reversal, so priority remains in its natural ascending order. This gives you latest-to-earliest dates, then smallest-to-largest priority — exactly what the passage requires. Option A applies .reversed() at the very end of the full chain, which flips both the due-date and priority orderings simultaneously. You'd get earliest-to-latest dates and largest-to-smallest priority — the opposite of what you want on both counts. Option C attempts to reverse only the priority comparator, which is backwards from the goal. The due date would remain in ascending order (earliest first) rather than descending, violating the primary sort requirement. Option D chains two reversals — one on the due-date comparator and one on the priority comparator. Reversing priority gives largest-to-smallest, which contradicts the smallest-to-largest requirement for the tiebreaker. The key strategy to remember: .reversed() wraps and negates everything built so far. If you want to reverse only one part of a multi-level sort, call .reversed() immediately after that specific comparator, before appending the next comparison step with .thenComparing. Think of it as "reverse what I have, then add more on top."
  4. Which comparator safely implements the required nested null handling?
    Answer: Comparator.nullsLast(Comparator.comparing(Product::category, Comparator.nullsLast(Comparator.naturalOrder()))) When you see nested null handling in Java comparators, think in layers: handle the outermost nulls first, then the inner nulls. The Comparator API lets you compose these layers, but you must match each null-handling wrapper to the correct level. Here, you have two levels of nulls: null Product references in the list, and null category() strings within non-null products. The requirement says null products come last, and among non-null products, null categories come last. This means you need nullsLast at the product level (outer) and nullsLast at the category level (inner). D is correct because it wraps the entire comparator with nullsLast(...), which pushes null Product references to the end. Inside, comparing(Product::category, Comparator.nullsLast(Comparator.naturalOrder())) handles the category extraction — when two non-null products are compared, their categories are compared in natural order with null categories sorted last. Both layers match the requirements perfectly. A uses nullsFirst at the outer level, which places null Product references first, directly contradicting the requirement that non-null products come first. B uses nullsFirst for category ordering (inner level), which would push null categories to the front among non-null products — the opposite of what's needed. C doesn't handle null Product references at all. Calling Product::category on a null product reference will immediately throw a NullPointerException before any comparison logic runs. A useful tip: map the requirements to layers before writing any code. Each "null goes first/last" rule corresponds to exactly one nullsFirst/nullsLast wrapper at the matching level.
  5. Which comparator meets all of these requirements?
    Answer: Comparator.comparing(Person::name, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)).thenComparingInt(Person::id) When building multi-criteria comparators in Java, you need to think about two independent decisions: where nulls go (first or last) and how non-null values are compared (natural order vs. case-insensitive). Mixing these up is exactly what the distractors exploit. The requirements call for null names sorted last and non-null names compared case-insensitively, with ID as a tiebreaker. Option C delivers precisely this: Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER) wraps the case-insensitive ordering inside a null-safe wrapper that pushes nulls to the end, then .thenComparingInt(Person::id) breaks ties by ID ascending. Every requirement is satisfied. Option A fails immediately because it uses nullsFirst instead of nullsLast, meaning null names would sort before non-null names — the opposite of what's required. Option B uses nullsLast(Comparator.naturalOrder()). The problem here is that Comparator.naturalOrder() on String performs a case-sensitive, lexicographic comparison, not a case-insensitive one. Names like "alice" and "Alice" would not be treated as equal, violating the case-insensitive requirement. Option D applies the comparators in the wrong order — it sorts by ID first, then name. This means two people with the same ID but different names would be sorted by name, but the primary sort would be numeric rather than alphabetic. The requirements clearly state name is the primary criterion. A useful rule of thumb: read the comparator chain left to right as a priority list — the first criterion dominates. Then double-check your null placement (nullsFirst vs. nullsLast) and ordering strategy (natural vs. case-insensitive) as separate concerns.
  6. Which comparator both implements the required order and obeys the comparator contract?
    Answer: (a, b) -> { int c = Integer.compare(Math.floorMod(b, 2), Math.floorMod(a, 2)); return c != 0 ? c : Integer.compare(a, b); } When writing a comparator for custom sort orders, you need to think about two things: correctness of the ordering logic and the comparator contract (consistency, antisymmetry, and transitivity). The key trap here involves negative numbers. In Java, the % operator returns a result with the sign of the dividend, so -3 % 2 evaluates to -1, not 1. This breaks odd/even detection for negatives. Math.floorMod(a, 2), by contrast, always returns 0 or 1 regardless of sign — making it the right tool for this problem. Option C is correct because it uses Math.floorMod safely, and critically, it compares (b, 2) before (a, 2) in the first comparison: Integer.compare(Math.floorMod(b, 2), Math.floorMod(a, 2)). Since odds have floorMod of 1 and evens have 0, reversing the arguments makes odds sort before evens — exactly what the problem requires. Within each group, Integer.compare(a, b) gives ascending numeric order. Option A fails because a % 2 returns -1 for negative odd numbers, so negative odds appear to be "less than" even numbers, destroying the odd-before-even guarantee. Option B uses Math.floorMod correctly but compares (a, 2) before (b, 2), which places evens (0) before odds (1) — the reverse of what's required. Option D violates the comparator contract by returning -1 whenever the groups differ, meaning compare(odd, even) and compare(even, odd) both return -1, breaking antisymmetry. As a study tip: whenever negatives are involved with modulo in Java, immediately reach for Math.floorMod — and always verify your comparator's antisymmetry by mentally swapping a and b.
  7. Which comparator should be used?
    Answer: Comparator.comparingDouble(Item::price).thenComparing(Item::sku) When working with TreeSet and a custom comparator, remember two things: the comparator defines both the iteration order and what counts as a "duplicate." A TreeSet uses comparator.compare(a, b) == 0 to determine equality — if two elements compare as equal, only one survives in the set. The requirement has two parts: iterate by price ascending first, then by SKU ascending as a tiebreaker, while ensuring same-price items with different SKUs are both retained. Option C, Comparator.comparingDouble(Item::price).thenComparing(Item::sku), satisfies both. It sorts primarily by price (ascending), and when prices are equal, falls back to SKU (ascending). Because SKUs are unique and non-null, no two distinct items will ever compare as zero — so all items are preserved in the set. Option A fails because it only compares by price. Two items sharing the same price would return 0, causing the TreeSet to treat them as duplicates and discard one — violating the requirement that both must remain. Option B sorts by SKU first, then price. This produces the wrong primary sort order (SKU ascending, not price ascending), even though it would technically preserve all items. Option D sorts by price descending (due to .reversed()) and then SKU ascending. The iteration order is wrong — the requirement asks for price ascending, not descending. A useful rule of thumb: whenever a TreeSet must hold elements that are logically distinct but share a natural key, your comparator must include a unique tiebreaker field. Otherwise, the set silently drops elements.
  8. Which comparator implements the specified business ordering?
    Answer: Comparator.comparingInt((Order o) -> rank.get(o.status())).thenComparing(Comparator.comparing(Order::createdAt).reversed()) When chaining Java comparators, precision about where .reversed() applies is everything — it can flip just one stage or the entire comparator depending on placement. The goal here is: (1) sort by status rank ascending (URGENT=0 first), then (2) sort by createdAt descending (newest first). Answer A achieves exactly this. Comparator.comparingInt(o -> rank.get(o.status())) builds an ascending status comparator, then .thenComparing(Comparator.comparing(Order::createdAt).reversed()) appends a secondary comparator that is independently reversed — so createdAt alone goes newest-to-oldest. Each stage is scoped correctly. B is the classic trap. It calls .reversed() after .thenComparing(...), which reverses the entire chained comparator — flipping both the status order (now DEFERRED first) and the time order simultaneously. The reversal is not scoped to createdAt alone. C gets the sort keys backwards. It compares by createdAt first and status second. Even though createdAt is reversed correctly within its scope, the primary/secondary ordering is inverted from the requirements. D reverses the status comparator before chaining, making DEFERRED (rank=2) sort first rather than URGENT (rank=0). Then createdAt is appended ascending (oldest first), so both stages are wrong. The study tip to remember: in Java's Comparator API, .reversed() always acts on the entire comparator it's called on up to that point, not just the last key. To reverse only one key in a chain, wrap it — pass Comparator.comparing(...).reversed() as the argument to .thenComparing(), as answer A demonstrates.
  9. Which comparator implements the required ordering?
    Answer: Comparator.comparing(Employee::department).thenComparing(Comparator.comparingInt(Employee::salary).reversed()).thenComparing(Employee::name) When chaining comparators in Java, you need to think carefully about scope: calling .reversed() on a comparator chain flips the entire chain built so far, not just the last criterion. The correct approach for "department ascending, salary descending, name ascending" is to reverse only the salary comparator before chaining it. Answer B does exactly this: Comparator.comparing(Employee::department) establishes ascending department order, then .thenComparing(Comparator.comparingInt(Employee::salary).reversed()) passes in an already-reversed salary comparator — so only salary is flipped to descending — and finally .thenComparing(Employee::name) adds ascending name ordering. Each criterion is independently controlled. Answer A is a classic trap. Calling .reversed() after .thenComparingInt(Employee::salary) reverses the entire comparator built so far, meaning department becomes descending and salary becomes ascending — the opposite of what you want. The .thenComparing(Employee::name) added afterward also ends up in reversed context, making this doubly wrong. Answer C reverses only the department comparator (making it descending), then chains salary ascending and name ascending. This misplaces the reversal entirely — department should be ascending, not reversed. Answer D correctly sorts department ascending and salary descending so far, but then reverses the name comparator, producing name descending instead of ascending — the opposite of the requirement. The key strategy to remember: when you need one criterion descending in a multi-key sort, wrap that specific comparator in .reversed() before passing it to .thenComparing(). Never call .reversed() at the end of a chain unless you truly want to invert every criterion.
  10. Which comparator reliably sorts players by score from highest to lowest?
    Answer: (p1, p2) -> Integer.compare(p2.score(), p1.score()) When sorting with a Comparator, the contract requires returning a negative number, zero, or positive number to indicate ordering. The subtle danger here is integer overflow: subtracting two int values can silently wrap around when the difference exceeds Integer.MAX_VALUE, producing a completely wrong sign. Integer.compare(a, b) is the safe, overflow-proof tool for this job. It returns -1, 0, or 1 based on whether a is less than, equal to, or greater than b — no arithmetic involved. For highest-to-lowest (descending) order, you reverse the arguments: compare p2 to p1 instead of p1 to p2. That's exactly what C does with Integer.compare(p2.score(), p1.score()) — making it the correct answer. A is the classic overflow trap. If p2.score() is Integer.MAX_VALUE and p1.score() is -1, the subtraction wraps to a negative number, reversing the intended order. This will silently corrupt your sort on extreme values. B uses Integer.compare safely, but in the wrong direction — (p1, p2) order gives ascending (lowest-to-highest), not descending. D attempts to fix overflow by casting to long, then casting back to int. While the subtraction itself is safe as a long, the final cast back to int reintroduces overflow for extreme differences, making it unreliable. As a study tip: whenever you see a comparator using subtraction (p1 - p2 or p2 - p1), treat it as a red flag for overflow. Always prefer Integer.compare(), and remember that swapping the arguments flips ascending to descending.

Start this quiz