Pricing Engine: Default Conflicts, Function Order and a Mutable Record Key
A pricing engine defines interface Discount { BigDecimal apply(BigDecimal p); default String label() { return labelWith("discount"); } private String labelWith(String s) { return "[" + s + "]"; } } and interface Loyalty { default String label() { return "loyalty"; } }. class Combo implements Discount, Loyalty { public BigDecimal apply(BigDecimal p) { return p; } } is rejected by javac. A cache uses record QuoteKey(String customerId, List tags) as a HashMap key; after code appends a tag to a key's list and looks the same QuoteKey up, get() returns null while the map now holds what looks like a duplicate entry. A pipeline declares Function<Order,String> toCustomer = Order::customerId; Function<String,Integer> len = String::length; the developer writes toCustomer.compose(len) and gets a compile error, then toCustomer.andThen(len) and gets the expected Integer. A retry helper has a lambda capturing an int attempts local that is incremented in a loop body, and that file does not compile. A teammate believes a private interface method cannot be called from a default method.
1. Why is Combo rejected, what are the resolution rules for inherited default methods, and is the private interface method legal?
Read the worked answer
Combo inherits two unrelated default String label() methods, one from Discount and one from Loyalty, and no class in the hierarchy provides an implementation. That is a compile-time error: "types Discount and Loyalty are incompatible; class Combo inherits unrelated defaults for label()".
The resolution rules, in order:
- A concrete method declared in the class (or a superclass) wins over any interface default.
- A default declared in a subinterface wins over one inherited from its superinterface.
- Two unrelated defaults at the same level are a conflict and must be resolved by an explicit override.
class Combo implements Discount, Loyalty {
public BigDecimal apply(BigDecimal p) { return p; }
@Override public String label() {
return Discount.super.label() + "+" + Loyalty.super.label();
}
}
X.super.m() is only legal when X is a direct superinterface of the class. The private labelWith is completely legal: interfaces may declare private and private static methods (Java 9+), typically to share code between default and static methods. Such a method is not inherited, is invisible to implementors, cannot be abstract, and cannot be called as Combo.labelWith(...).
2. Explain why toCustomer.compose(len) fails to compile while toCustomer.andThen(len) succeeds, and state the ordering each one applies.
Read the worked answer
The signatures carry the constraint. For Function<T,R>:
andThen(Function<? super R, ? extends V> after)requires the argument's input type to be a supertype ofR.compose(Function<? super V, ? extends T> before)requires the argument's output type to be a subtype ofT.
Here toCustomer is Function<Order,String>, so T = Order, R = String; len is Function<String,Integer>.
toCustomer.andThen(len): String is a supertype of String, and Integer is a valid V. Result: Function<Order,Integer>. Compiles.
toCustomer.compose(len): the compiler needs len to be a Function<? super V, ? extends Order>, i.e. it must produce an Order. It produces an Integer, and Integer is not an Order, so the types are incompatible - a compile error, not a runtime surprise.
Ordering: f.andThen(g) computes g(f(x)); f.compose(g) computes f(g(x)). So f.andThen(g) and g.compose(f) are equivalent, and f.compose(g) is the mirror image. Also remember a bare method reference has no type on its own: var f = Order::customerId; does not compile because there is no target functional interface to infer from.
3. Explain the cache failure with the record key and the retry helper's compile error, and give fixes for both.
Read the worked answer
Record key mutation. A record generates equals and hashCode from all its components. QuoteKey's hashCode therefore depends on List.hashCode, which depends on the list's contents. Appending a tag changes the key's hash. HashMap stores the entry in the bucket chosen at insertion time; a later get with an equal-but-differently-hashed key probes a different bucket, finds nothing, and returns null. A subsequent put also computes the new hash and inserts a second entry - the map genuinely holds both. The fix is to make the key immutable at construction, which a compact constructor does well (its body runs before the fields are assigned):
record QuoteKey(String customerId, List<String> tags) {
QuoteKey {
tags = List.copyOf(tags); // defensive copy; NPE on null elements
}
}
Retry helper. A lambda may capture only final or effectively final local variables; attempts++ reassigns the variable, so it is no longer effectively final and the capture is rejected. Use a single-element array (int[] attempts = {0}) or an AtomicInteger field, or restructure the retry as a plain loop. The array holder is not thread-safe, so for concurrent retries prefer AtomicInteger.