When both branches of a ternary are reference (object) types rather than numerics, how does Java decide the result type?
answer
- Reference branches → least upper bound (common supertype)
- null branch → result takes the other branch's type
- Static result type drives member access + overload resolution
- ArrayList vs LinkedList → List<String>
- primitive + non-wrapper object → primitive is boxed, then lub
basics
~20 sFor object branches, Java finds a common parent type both branches share — roughly their nearest shared supertype. If one branch is null, the result takes the other branch's type. The result is the most specific type that can hold either value.
solid answer
~50 sWhen both branches are reference types, the ternary's result type is the **least upper bound (lub)** of the two — informally, the most specific common supertype, computed from the classes and interfaces both branches share (including generics). If the two branches are the same type, that is the result. If one branch is the `null` literal and the other is a reference type, the result is that reference type. This matters because the static result type controls which members you can call on the expression without a cast and how overload resolution picks a method. For example `cond ? new ArrayList<String>() : new LinkedList<String>()` has the result type `List<String>` (their common supertype via the interface), so you can assign it to a `List` but calling an `ArrayList`-specific method would need a cast. The lub can be surprisingly broad (sometimes `Object` or an intersection of interfaces like `Serializable & Comparable`), which occasionally produces unexpected overload selection or compile behavior.
code
java · 14 linesboolean cond = true;
List<String> r1 = cond ? new ArrayList<String>()
: new LinkedList<String>(); // type List<String>
String r2 = cond ? null : "hello"; // null branch -> type String
// overload resolution follows the STATIC result type:
// f(Object) vs f(String):
// cond ? "x" : "y" -> String -> calls f(String)
// cond ? "x" : new Object() -> Object -> calls f(Object)
var mixed = cond ? 1 : "x"; // int is boxed to Integer; lub of Integer & String
// is an Object-like intersection typego deeper
Knows that two object branches give some shared/common type and that null takes the other side's type.
Can name the common-supertype idea and predict List<String> for ArrayList/LinkedList branches and String for a null branch.
Explains the least-upper-bound rule, how the static result type drives member access and overload resolution, and the box-then-lub behavior for primitive+non-wrapper.
Discusses intersection types, edge cases where lub collapses to Object, overload-resolution pitfalls in API design, and when to force an explicit type for clarity/compatibility.
## Reference vs primitive branches We saw that *numeric* branches use binary numeric promotion. **Reference (object) branches** follow a different rule. A **reference type** is any class, interface, array, or `null` — as opposed to primitives like `int`. The result type of a ternary whose branches are reference types is determined by finding a type that can hold the value of **either** branch. ## The simple cases first 1. **Same type both sides** → that type. `cond ? "a" : "b"` is `String`. 2. **One side is `null`** → the result type is the *other* side's type. `cond ? null : "x"` is `String`. `cond ? someList : null` is the type of `someList`. 3. **One side is a subtype of the other** → the result is the supertype. `cond ? new ArrayList<String>() : (List<String>) x` is `List<String>`. ## The general rule: least upper bound (lub) When the two types are different and neither is a subtype of the other, Java computes the **least upper bound** — the *most specific* type that is a supertype of both. "Most specific" means: walk up each branch's chain of superclasses and implemented interfaces, and take the lowest common ancestor in that type hierarchy. Example with concrete collections: ```java ArrayList<String> a = new ArrayList<>(); LinkedList<String> l = new LinkedList<>(); var r = cond ? a : l; // r has type List<String> (both implement List<String>) ``` `ArrayList` and `LinkedList` are not subtypes of each other, but both implement `List`, so the lub is `List<String>` (actually a richer intersection, but `List<String>` is the usable part). ## Why the static type matters The **static type** of an expression is the type the *compiler* believes it has, regardless of the actual runtime object. It controls two things: 1. **Which members you can use without a cast.** On a `cond ? a : l` typed as `List<String>`, you can call `.add(...)` (a `List` method) but not `ArrayList`-only methods without casting. 2. **Overload resolution.** When you pass the ternary to an overloaded method, the compiler picks the overload based on this static result type, which can differ from what you'd expect if you thought only about the runtime branch: ```java void f(Object o) { ... } void f(String s) { ... } f(cond ? "x" : "y"); // result type String -> calls f(String) f(cond ? "x" : new Object()); // lub is Object -> calls f(Object) ``` ## Intersection types and Object Sometimes the lub is not a single named type but an **intersection** like `Serializable & Comparable<…>` (e.g. mixing `Integer` and `String` as objects), or it collapses to `Object` when the branches share nothing more specific. You generally cannot name these intersection types, but the compiler uses them, and you may notice the effect when assignment or overloads behave more permissively than you expected. ## Boxing interaction If one branch is a primitive and the other a *reference* type that is **not** its wrapper (e.g. `int` and `String`), the primitive is **boxed** to its wrapper and then the lub is taken over the two reference types — so `cond ? 1 : "x"` is roughly `Serializable & Comparable<…>` (a lub of `Integer` and `String`), an `Object`-ish type, not a number. This is different from the primitive+matching-wrapper case (which unboxes — see the NPE question). ## Practical guidance - For object branches, **expect the common supertype**, and cast if you need a more specific type. - Beware overload resolution surprises; if a method is overloaded, make the intended type explicit. - Prefer keeping both branches the same declared type for clarity.
- What is the result type of cond ? new ArrayList<String>() : new LinkedList<String>()?List<String> (more precisely an intersection of the shared supertypes, but List<String> is the usable part). Since the two concrete classes aren't subtypes of each other, Java takes their least upper bound — the common interface List<String>.
- What type does cond ? null : "hi" have?String. When one branch is the null literal, the result type is simply the type of the other branch.
Picture a family tree. Two cousins (the branches) don't directly descend from each other, so to refer to 'whichever of them shows up,' you name their nearest common ancestor. The ternary does the same: it labels the result with the closest shared ancestor type.
saying these in an interview costs you the question
- Assuming the result type is whichever branch runs at runtime (it is static)
- Thinking null forces the result to Object (it takes the other branch's type)
- Expecting to call subclass-specific methods on the lub result without a cast
- Confusing the box-then-lub case (int + String) with the unbox case (int + Integer)