DEV Community

websilvercraft
websilvercraft

Posted on

Java Generics, Union Types, and the “String OR Integer” Problem

The motivating problem

Consider this seemingly simple requirement:

“I want a single method that accepts either an array of Integer or an array of String, 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);
Enter fullscreen mode Exit fullscreen mode

You’re explicitly not allowed to overload methods, so this is illegal:

void printArray(Integer[] a) { }
void printArray(String[] a) { } // ❌ rejected
Enter fullscreen mode Exit fullscreen mode

Naturally, this leads to the question:

“Can I restrict a generic method to only String and Integer?”

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why this works

  • Integer[] and String[] are both subtypes of Object[]
  • Only one method → no overloading violation

Downside

  • Loses compile-time type safety
  • You can now pass anything:
  Object[] mixed = {1, "hello", new Date()};
Enter fullscreen mode Exit fullscreen mode

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) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

But Java does not allow:

<T extends A | B>   // ❌ no union types
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

and then:

<T extends StringOrInteger>
Enter fullscreen mode Exit fullscreen mode

But this fails immediately because:

class MyString extends String {}   // ❌ illegal
class MyInteger extends Integer {} // ❌ illegal
Enter fullscreen mode Exit fullscreen mode

Both String and Integer are final, so they cannot share a custom superclass.

The only common superclass they do share is:

Object
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode
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(); }
}
Enter fullscreen mode Exit fullscreen mode
public <T extends IntOrString> void printArray(T[] array) {
    for (T t : array) {
        System.out.println(t);
    }
}
Enter fullscreen mode Exit fullscreen mode

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) }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Scala supports union types directly.


TypeScript (designed for unions)

function printArray(arr: (string | number)[]) {
    arr.forEach(console.log);
}
Enter fullscreen mode Exit fullscreen mode

This is exactly what you wanted Java to do.


Swift

func printArray(_ arr: [Any]) {
    for item in arr {
        print(item)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)