When sorting complex data structures in Java, especially when dealing with dynamic or variable-length sorting criteria, leveraging custom comparators provides fine-grained control over ordering logic. This approach is particularly useful when replicating SQL-style ORDER BY behavior in memory.
Handling Fixed-Key Sorting
For a known set of sort keys, such as ordering books by author (ascending), price (descending), and publisher (ascending), a traditional Comparator can be implemented with cascading comparisons:
Collections.sort(bookList, new Comparator<Map<String, Object>>() {
@Override
public int compare(Map<String, Object> a, Map<String, Object> b) {
// Primary: author (ascending)
int authorComp = ((String) a.get("author")).compareTo((String) b.get("author"));
if (authorComp != 0) return authorComp;
// Secondary: price (descending)
BigDecimal priceA = new BigDecimal(a.get("price").toString());
BigDecimal priceB = new BigDecimal(b.get("price").toString());
int priceComp = priceA.compareTo(priceB);
if (priceComp != 0) return -priceComp; // Reverse for descending
// Tertiary: publisher (ascending)
return ((String) a.get("publisher")).compareTo((String) b.get("publisher"));
}
});
This pattern ensures that each subsequent key is only evaluated when the previous one yields equality. The descending order for price is achieved by negating the comparison result.
Dynamic Multi-Key Sorting
When the number and order of sort fields are determined at runtime — for example, via user configuration or API parameters — a flexible solution is required. Here’s a scalable approach using configuration-driven sorting:
public void dynamicSort(List<Map<String, Object>> records, List<Map<String, Object>> sortConfig) {
// Sort configuration by priority (sortPosition)
sortConfig.sort((cfg1, cfg2) ->
Integer.compare(
Integer.parseInt(cfg1.get("sortPosition").toString()),
Integer.parseInt(cfg2.get("sortPosition").toString())
)
);
// Extract sort criteria into arrays for efficiency
String[] fieldNames = new String[sortConfig.size()];
int[] directions = new int[sortConfig.size()];
for (int i = 0; i < sortConfig.size(); i++) {
Map<String, Object> config = sortConfig.get(i);
fieldNames[i] = config.get("code").toString();
directions[i] = "asc".equalsIgnoreCase(config.get("sortDirection").toString()) ? 1 : -1;
}
// Apply dynamic sorting
records.sort((record1, record2) -> {
for (int i = 0; i < fieldNames.length; i++) {
Object val1 = record1.get(fieldNames[i]);
Object val2 = record2.get(fieldNames[i]);
int comparison = 0;
if (val1 instanceof String && val2 instanceof String) {
comparison = ((String) val1).compareTo((String) val2);
} else {
BigDecimal num1 = new BigDecimal(val1.toString());
BigDecimal num2 = new BigDecimal(val2.toString());
comparison = num1.compareTo(num2);
}
if (comparison != 0) {
return comparison * directions[i];
}
}
return 0; // Equal if all fields match
});
}
In this implementation:
- Sort rules are defined as a list of maps, each specifying the feild name, priority (position), and direction.
- The configuration is pre-sorted by position to ensure correct evaluation order.
- Each field is compared in sequence, and the first non-zero result determines the final order, adjusted by its direction (1 for ascending, -1 for descending).
- Only basic types (String and numeric types convertible to BigDecimal) are supported, but the structure can be extended for dates, booleans, etc.
Considerations
- This method is suitable for datasets under 10,000 records where latency is not critical.
- Type handling is minimal; production systems should validate and cast types explicitly.
- For high-throughput scenarios, consider pre-sorting at the database layer or using indexed collections.
- String comparisons use Unicode ordering, which may differ from database collation rules (e.g., MySQL’s case-insensitive defaults).