The motivating problem
Consider this seemingly simple requirement:
“I want a single method that accepts either an array of
Integeror an array ofString, but nothing else.”
In a HackerRank-style problem, this shows up as:
Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
myPrinter.printArray(intArray);
myPrinter.printArray(stringArray);
You’re explicitly not allowed to overload methods, so this is illegal:
void printArray(Integer[] a) { }
void printArray(String[] a) { } // ❌ rejected
Naturally, this leads to the question:
“Can I restrict a generic method to only
StringandInteger?”
Let’s explore what is possible, what is not, and why Java behaves this way.
The obvious (working) solutions
1. The generic solution (recommended)
class Printer {
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
What this allows
Integer[]String[]- Any other reference type
Pros
- Type-safe
- Idiomatic Java
- Clean and flexible
Cons
- Does not restrict the types at all
This is the expected answer for most interview and coding-challenge scenarios.
2. The Object[] solution (works, but weaker)
class Printer {
public void printArray(Object[] items) {
for (Object item : items) {
System.out.println(item);
}
}
}
Why this works
-
Integer[]andString[]are both subtypes ofObject[] - Only one method → no overloading violation
Downside
- Loses compile-time type safety
- You can now pass anything:
Object[] mixed = {1, "hello", new Date()};
This is acceptable for printing, but not ideal for real APIs.
The thing people want to write (but can’t)
Union type bound (not supported)
public <T extends String | Integer> void printArray(T[] array) {
...
}
This feels intuitive — but Java does not support union types.
Why this is impossible in Java
1. Java generics do not support OR constraints
Java allows intersection types (AND):
<T extends InterfaceA & InterfaceB>
But Java does not allow:
<T extends A | B> // ❌ no union types
The type system only understands:
- “T must satisfy all of these”
- Never “T must satisfy one of these”
2. String and Integer are both final
You might think:
class StringOrInteger { }
and then:
<T extends StringOrInteger>
But this fails immediately because:
class MyString extends String {} // ❌ illegal
class MyInteger extends Integer {} // ❌ illegal
Both String and Integer are final, so they cannot share a custom superclass.
The only common superclass they do share is:
Object
Which is… too broad to be useful.
What can be done (with trade-offs)
Option 1: Runtime checks (not type-safe)
public void printArray(Object[] items) {
for (Object item : items) {
if (!(item instanceof String || item instanceof Integer)) {
throw new IllegalArgumentException("Only String or Integer allowed");
}
System.out.println(item);
}
}
Pros
- Enforces the rule
Cons
- Errors occur at runtime, not compile time
- Easy to misuse
Option 2: Wrapper + marker interface (type-safe, but heavy)
interface IntOrString { }
class MyString implements IntOrString {
String value;
MyString(String value) { this.value = value; }
public String toString() { return value; }
}
class MyInteger implements IntOrString {
Integer value;
MyInteger(Integer value) { this.value = value; }
public String toString() { return value.toString(); }
}
public <T extends IntOrString> void printArray(T[] array) {
for (T t : array) {
System.out.println(t);
}
}
Pros
- Compile-time restriction
- Clean type system
Cons
- Requires wrapping values
- Overkill for simple tasks
Why Java is like this (design philosophy)
Java’s generics were designed to:
- Be backward compatible
- Avoid runtime overhead
- Keep the type system simple and predictable
Union types introduce:
- Complex overload resolution
- Ambiguous method selection
- Harder type inference
Java chose simplicity and safety over expressiveness here.
How other languages handle this
Kotlin (yes, but indirectly)
fun printArray(arr: Array<out Any>) {
arr.forEach { println(it) }
}
Kotlin still lacks true union types, but:
- Has better variance (
out,in) - Stronger type inference
Scala (much more powerful)
def printArray(arr: Array[String | Int]): Unit =
arr.foreach(println)
Scala supports union types directly.
TypeScript (designed for unions)
function printArray(arr: (string | number)[]) {
arr.forEach(console.log);
}
This is exactly what you wanted Java to do.
Swift
func printArray(_ arr: [Any]) {
for item in arr {
print(item)
}
}
Swift allows this but relies on runtime checks unless you use enums.
The final takeaway
In Java:
- ❌ You cannot restrict generics to “String OR Integer”
- ❌ You cannot create a union superclass
- ❌ You cannot express OR constraints in type bounds
- ✅ You can use unrestricted generics
- ✅ You can enforce rules at runtime
- ✅ You can redesign with wrappers if needed
That’s why the best Java answer to this problem remains:
public <T> void printArray(T[] array)
So remember, if you were thinking about union types, congrats.
You’re already thinking beyond Java’s surface level and into type system design, which is exactly where strong developers end up.
Top comments (0)