Static Comparator Fields as Named Orders
Before You Start
Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.
Not sure? Take the 60-second self-check.
1. Static field. What does public static final mean on a class field?
Check
static: the field belongs to the class, not to any particular instance. final: the field cannot be reassigned after initialization. Together: one shared, immutable reference accessible without constructing an object.
2. Comparator.comparing. What does Comparator.comparing(Player::getTeam) produce?
Check
A Comparator<Player> that orders Player objects by calling getTeam() on each and comparing the resulting String values in natural order.
3. Call site difference. Compare Arrays.sort(players, new TeamComparator()) with Arrays.sort(players, Player.BY_TEAM). What is different?
Check
new TeamComparator() allocates a new instance each call. Player.BY_TEAM accesses an already-allocated static field. Both produce the same sort result. Player.BY_TEAM is shorter and avoids repeated allocation.
Try This First
The F19 Lab 1 call site is:
Arrays.sort(nflPlayers, new TeamComparator());
Before reading further, write what this call would look like if BY_TEAM were a public static final Comparator<Player> field on Player instead.
Check
Arrays.sort(nflPlayers, Player.BY_TEAM);
No new, no constructor call, no class name from the comparators package. The ordering is named directly on the type it orders.
What You Need To Walk In With
The Insight: Moving comparators from separate named classes to public static final fields on the type they order is a modernization, not a redesign. The objects are the same; only where they live has changed. The field form is shorter at call sites and self-documenting (the type name and the ordering name appear together: Player.BY_TEAM). The named-class form, which the Steiner labs teach, is still the pedagogically correct starting point because it makes the function-object pattern explicit.
The JDK uses this idiom at the library level: String.CASE_INSENSITIVE_ORDER is declared as public static final Comparator<String> CASE_INSENSITIVE_ORDER directly on the String class, and Comparator.naturalOrder() returns a stable singleton for the same purpose. When you declare Player.BY_TEAM, you are following the same convention that the standard library authors chose.
You can: declare a public static final Comparator<T> field using Comparator.comparing, use it at a sort call site without new, explain why all three modifiers are required, and decide when a named class scales better than a field.
How It Works
The static-field pattern
public class Player implements Comparable<Player>
{
// Named alternate orderings, no separate files needed
public static final Comparator<Player> BY_TEAM =
Comparator.comparing(Player::getTeam);
public static final Comparator<Player> BY_POSITION =
Comparator.comparing(Player::getPosition);
public static final Comparator<Player> BY_GAMES_PLAYED =
Comparator.comparingInt(Player::getGamesPlayed);
// ... natural order, fields, constructors, getters
@Override
public int compareTo(final Player other) { ... }
}
Call sites become:
Arrays.sort(nflPlayers); // natural order
Arrays.sort(nflPlayers, Player.BY_TEAM); // team order
Arrays.sort(nflPlayers, Player.BY_POSITION); // position order
Arrays.sort(nflPlayers, Player.BY_GAMES_PLAYED); // games-played order
Each line is self-documenting: the ordering name appears right after the type name.
Why public static final (all three modifiers)
| Modifier | What happens without it |
|---|---|
public |
Field is inaccessible from the client package that calls Arrays.sort |
static |
Each Player instance would carry its own comparator reference; callers need new Player() to access it |
final |
Any caller can reassign Player.BY_TEAM = someOtherComparator, silently breaking every other caller that relied on the original value |
All three are required. The combination is the standard Java idiom for class-level constant values.
Comparing the two forms
| Aspect | Named class in cscd211Comparators/ |
Static field on Player |
|---|---|---|
| Call site | new TeamComparator() |
Player.BY_TEAM |
| Files | One file per ordering | Zero extra files |
| Discoverability | ls cscd211Comparators/ |
IDE autocomplete on Player. |
| Pedagogy | Function-object pattern explicit | Comparators feel like properties of the type |
| Scale (50+ orderings) | Scales (each ordering is its own file) | Clutters the class |
The Steiner labs teach the named-class form for pedagogical reasons. Production Java uses the static-field form when the number of orderings is small (fewer than five or so).
Quick check
Check your understanding
A Player class declares: public static final Comparator<Player> BY_TEAM = Comparator.comparing(Player::getTeam); Which call site is correct?
Worked Example: Predict Then Check
Refactor F19 Lab 1’s GamesPlayedComparator class into a static field on Player. Write the field declaration and the updated call site.
Reasoning
GamesPlayedComparator orders by getGamesPlayed() ascending using Integer.compare. The static-field form uses Comparator.comparingInt with a method reference.
Show answer
Field declaration (inside the Player class body):
public static final Comparator<Player> BY_GAMES_PLAYED =
Comparator.comparingInt(Player::getGamesPlayed);
Call site (replaces new GamesPlayedComparator()):
Arrays.sort(nflPlayers, Player.BY_GAMES_PLAYED);
The GamesPlayedComparator.java file is no longer needed for this ordering. The named class can be deleted.
Common Misconceptions
Misconception 1: forgetting final on the static comparator field
Wrong mental model: “
public static Comparator<Player> BY_TEAM = ...;(nofinal) is the same.”
Why it breaks: Without final, the field is reassignable. Any class that can see Player.BY_TEAM can replace it:
Player.BY_TEAM = someOtherComparator; // legal if not final; silently breaks callers
Every caller that read Player.BY_TEAM after the reassignment would get the wrong comparator. Global mutable state is a source of unpredictable bugs.
How to correct: Always write public static final. The final keyword enforces that the field can only be assigned once (at declaration or in a static initializer).
Source: Bloch, Effective Java, Item 17.
Quick check
Check your understanding
A student declares: public static Comparator<Player> BYTEAM = Comparator.comparing(Player::getTeam); Later, another class runs Player.BYTEAM = reverseOrder; What happens next?
Misconception 2: keeping the named class file just to initialize the static field
Wrong mental model: “I already have
TeamComparator.java. I will just setPlayer.BY_TEAM = new TeamComparator().”
Why it is redundant: If TeamComparator.java exists only to populate one static field, the class provides no additional value. The static field can be initialized directly with Comparator.comparing(Player::getTeam), and the named class file can be deleted. Two artifacts where one suffices is unnecessary complexity.
Exception: The named class is correct and required in CSCD 211 lab assignments that explicitly ask for it. The “delete the class” advice applies to production code after the pedagogical purpose has been served.
Source: Bloch, Effective Java, Item 43.
Formal Definition and Interface Contract
From Bloch, Effective Java, Item 17 (Minimize mutability):
Make every field final unless there is a compelling reason to make it nonfinal.
The Comparator<Player> value returned by Comparator.comparing is itself immutable (it holds no mutable state), so the final field is also an immutable value. The combination produces a constant that is safe to share across threads, call sites, and contexts.
From Bloch, Effective Java, Item 15 (Minimize accessibility of classes and members):
Make classes and members as inaccessible as possible. [...] For members of public classes,
public static finalfields are the only acceptable form of public static fields.
Mental Model
Think of the static-field form as putting a labeled key on a hook next to the type’s front door. The key (the comparator) was always there; now it has a name and a fixed location. Anyone who needs “the team ordering for Player” reaches for Player.BY_TEAM rather than constructing a new one. The named-class form is the locksmith’s workshop where the keys are made; the static-field form is the key hook where they hang.
Connections
Within CSCD 210/211: The static-field form is the production evolution of what F19 Lab 1 teaches with named classes in cscd211Comparators/. Both forms produce the same ordering behavior at the call site; only the packaging differs.
Looking back: One Class, Many Comparators by Design established that multiple comparators for one class are correct. This lesson shows the modern packaging of those comparators.
Looking ahead: Comparable and Comparator Coexist on the Same Type covers how a class can have both a natural order (via compareTo) and static-field comparators simultaneously.
Go Deeper (optional)
None of this section is needed to write correct code. It is here if you find yourself curious about why things are designed this way.
The companion-class alternative. When a type accumulates many orderings (more than five or six), placing them all as static fields on the main class clutters it. A common production alternative is a companion class: a separate PlayerOrders or PlayerComparators class whose sole job is to hold public static final comparator fields. The main Player class stays clean, and the companion groups all the orderings in one discoverable place.
Reference-type constants are not inlined. For primitive static final int values, the Java compiler inlines the value at every use site, so no field lookup happens at runtime. For reference-type static final fields like Comparator<Player>, the compiler does not inline the object reference: the JVM performs one field-read to get the comparator. This distinction does not matter for correctness or typical performance, but it surfaces when you use reflection or serialization, where the field itself is visible as a program element.
Where this shows up in professional code. Production Java APIs routinely expose comparators as static fields on the type they order. String.CASE_INSENSITIVE_ORDER in the JDK is the most common example. Custom business objects follow the same idiom when multiple orderings are stable across the codebase: a developer who types Invoice. in an IDE and sees BY_DATE, BY_AMOUNT, and BY_CLIENT immediately knows every supported ordering without searching through a comparators package.
Practice
Level 1
Add BY_POSITION as a public static final Comparator<Player> field to the Player class, using Comparator.comparing with a method reference to getPosition() (which returns String).
Show answer
public static final Comparator<Player> BY_POSITION =
Comparator.comparing(Player::getPosition);
Call site: Arrays.sort(nflPlayers, Player.BY_POSITION);
Level 2
A student declares:
public static Comparator<Player> BY_TEAM = Comparator.comparing(Player::getTeam);
Identify the problem and state the exact fix.
Show answer
Problem: final is missing. The field is reassignable; any caller can replace Player.BY_TEAM with a different comparator, silently breaking all other callers.
Fix: Add final:
public static final Comparator<Player> BY_TEAM = Comparator.comparing(Player::getTeam);
Level 3
A Receipt class has compareTo (natural order: by timestamp). Three orderings are needed: by amount, by customer name, and by tax. Compare two implementations: (a) three named classes in cscd211Comparators/ and (b) three static fields on Receipt. State when each is preferable and why.
Show answer
(a) Named classes:
cscd211Comparators/ReceiptByAmountComparator.java
cscd211Comparators/ReceiptByCustomerComparator.java
cscd211Comparators/ReceiptByTaxComparator.java
(b) Static fields:
public static final Comparator<Receipt> BY_AMOUNT = Comparator.comparingDouble(Receipt::getAmount);
public static final Comparator<Receipt> BY_CUSTOMER = Comparator.comparing(Receipt::getCustomer);
public static final Comparator<Receipt> BY_TAX = Comparator.comparingDouble(Receipt::getTax);
When (a) is preferable:
- The ordering is complex (multiple fields, conditional logic) and benefits from a named, testable class.
- The class is assigned in a course that explicitly teaches the named-class form.
- More than five or six orderings exist and the class would become cluttered.
When (b) is preferable:
- The ordering is simple enough to express in one
Comparator.comparingcall. - Call sites should be self-documenting (
Receipt.BY_AMOUNTinstead ofnew ReceiptByAmountComparator()). - The team prefers to minimize the number of files for small orderings.
Both are correct. The choice is a matter of scale and pedagogy.
Check Yourself
Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.
Check your understanding
Which combination of modifiers is required when declaring a comparator as a class-level constant on a type such as Player?
A student writes: public static Comparator<Player> BY_TEAM = Comparator.comparing(Player::getTeam); What is the consequence of the missing modifier?
Given this Player class fragment: public static final Comparator<Player> BYGAMESPLAYED = Comparator.comparingInt(Player::getGamesPlayed); A student writes the call site: Arrays.sort(nflPlayers, new Player.BYGAMESPLAYED()); What is the result?
String.CASEINSENSITIVEORDER is a JDK field declared as: public static final Comparator<String> CASEINSENSITIVEORDER. Which statement best explains why this is the same pattern covered in this topic?
A team already has a TeamComparator.java file. A developer proposes setting Player.BY_TEAM = new TeamComparator() to populate the static field. What does this lesson say about that approach?