JavaSlugger
JavaSlugger

Reputation: 56

How do java generic methods constrain the method type arguments?

I've been reading about generic methods, and thought I understood how the generic type argument would constrain the method parameter types, but when I tested out some of the ideas with actual code, I got unexpected results. Here is a simple generic method that I don't understand:

private static <T> void foo(T[] t1, T[] t2){  
    t2[0] = t1[0];
}
...
String[] stringArray = new String[]{"1", "2", "3"};
Integer[] integerArray = new Integer[]{4,5,6};
foo(stringArray, integerArray);

I would have thought that this generic method was constrained so that the two arrays had to be of the same type T, but in practice the code above compiles just fine, even though one array is of type String and the other is of type Integer. When the program runs, it generates a run-time exception (ArrayStoreException).

Upvotes: 3

Views: 1646

Answers (4)

Gary
Gary

Reputation: 7257

As @Bozho has demonstrated the problem can be fixed, however to demonstrate what is going on consider this code:

public class Main {


  // This is the original version that fails because of type erasure in arrays
  private static <T> void foo(T[] t1, T[] t2) {

    t2[0] = t1[0];
  }

  // The same method as foo() but with the type erasure demonstrated
  private static void foo2(Object[] t1, Object[] t2) {

    // Integer[] should not contain String 
    t2[0] = t1[0];
  }


  public static void main(String[] args) {

    String[] stringArray = new String[]{"1", "2", "3"};
    Integer[] integerArray = new Integer[]{4, 5, 6};

    foo2(stringArray, integerArray);
  }

}

This is all to do with the fact that arrays in Java are covariant, whereas generics are not. There is an interesting article about it here. A quick quote says it all:

Arrays in the Java language are covariant -- which means that if Integer extends Number (which it does), then not only is an Integer also a Number, but an Integer[] is also a Number[], and you are free to pass or assign an Integer[] where a Number[] is called for. (More formally, if Number is a supertype of Integer, then Number[] is a supertype of Integer[].) You might think the same is true of generic types as well -- that List is a supertype of List, and that you can pass a List where a List is expected. Unfortunately, it doesn't work that way.

It turns out there's a good reason it doesn't work that way: It would break the type safety generics were supposed to provide. Imagine you could assign a List to a List. Then the following code would allow you to put something that wasn't an Integer into a List:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Because ln is a List, adding a Float to it seems perfectly legal. But if ln were aliased with li, then it would break the type-safety promise implicit in the definition of li -- that it is a list of integers, which is why generic types cannot be covariant.

Upvotes: 1

johnstok
johnstok

Reputation: 98260

The compiler has inferred the type T for you and probably chosen Object.

You can specify the type if necessary using the following syntax:

MyClass.<String>foo(stringArray, integerArray); // Compiler error.

Combining arrays and generics is universally terrifying. By way of illustration I present Angelika Langer's Generics FAQ: http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html

Upvotes: 0

meriton
meriton

Reputation: 70574

A not-so-well-known fact about Java is that String[] is a subtype of Object[] (in contrast to List<String> not being a subtype of List<Object>). Hence the compiler can infer T = Object, making the method signature

foo(Object[] t1, Object[] t2)

which can be called with foo(stringArray, integerArray).

If you try the same with Lists:

<T> void foo(List<T> t1, List<T> t2) { ... }

you will find that

foo(new ArrayList<String>(), new ArrayList<Integer>())

doesn't compile - because there is no T such that both List<String> and List<Integer> are subtypes of List<T>. However, if instead you used wildcard types to declare the method:

void foo(List<?> t1, List<?> t2) { ... }

the method could be invoked (but the method body won't compile, because the compiler knows that the ? may refer to incompatible types).

Upvotes: 1

Bozho
Bozho

Reputation: 597234

In this example the inferred type is ? extends Object[], which fits both types.

In order to achieve what you want, you'd need:

private static <T> void foo(Class<T> clazz, T[] t1, T[] t2);

then

foo(String.class, stringArray, stringArray); // compiles
foo(String.class, stringArray, integerArray); // fails

Upvotes: 1

Related Questions