Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

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

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);

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 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);
        }
    }
}

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[] 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()};

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.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.