The branches do not have to be the same type, because the compiler computes a common type for the whole expression. This is where the original explanations of this operator usually go wrong, since the rules are not simply "both branches must be compatible".
For **numeric** operands, binary numeric promotion applies, which is the same widening rule the arithmetic operators use.
| Branch types | Result type | Example |
|---|
int and int | int | c ? 1 : 2 |
int and long | long | c ? 1 : 2L becomes a Long |
double and float | double | c ? 1.0d : 2.0f becomes a Double |
byte and short | short | Not int, the wider of the pair wins |
char and an int constant | char | c ? 'a' : 2 becomes a Character |
The last two rows surprise people, so they are worth running. They are a special case belonging to the conditional operator itself rather than plain widening: when one branch is a byte, short or char and the other is an int *constant*, the result keeps the narrow type instead of widening to int. Assigning to Object is the easiest way to see the resulting type.
boolean c = true;
char ch = 'a';
Object a = c ? ch : 2;
// Character, not Integer
Object b = c ?(byte)1 :(short)2;
// Short
System.out.println(a.getClass().getSimpleName() + " " + b.getClass().getSimpleName());
The constant still has to fit in the narrow type. A value beyond its range promotes the result to int, which is the safety net that stops the narrow rule from silently wrapping.
short s = 1;
Object p = c ? s : 1000;
// Short, 1000 fits in a short
Object q = c ? s : 100000;
// Integer, 100000 does not fit
For **reference** types, the result is the nearest common supertype, that is the *least upper bound* of the two.
| Branch types | Result type |
|---|
String and int | Rejected, no common supertype other than Object in an assignment |
String and null | String, the null adopts the other side |
ArrayList and LinkedList | List, or Object here |
StringBuilder and String | Rejected, unrelated classes |
int and Integer | int, the wrapper is unboxed |
Two branches with nothing in common is a compile error, not a silent widening.
boolean c = true;
int bad = c ? 1 : "x";
// error: incompatible types: bad type in conditional expression
The null case is the useful one, because it lets you write a null-returning expression without a cast.
String found = lookup(id);
int nameLength = found != null ? found.length() : 0;
// result type is int, not Integer
Mixing a primitive with its wrapper unboxes, which is why the whole expression gets a numeric type rather than becoming an Object.
Integer boxed = c ? 5 : Integer.valueOf(7);
System.out.println(boxed.getClass().getSimpleName());
// Integer