2

Let's say I have a class with raw type declaration as List (list1). It's just a simple example:

public class Wildcards {
    public boolean contains(List list1, List<?> list2){

      /*for(Object element: list1) {
            if (list2.contains(element)) {
                return true;
            }
        }*/

        list1.add("12sdf34"); //insert String 

        return false;
    }
}

In list1 I insert String value. (If I use unbounded wildcards for list1 as for list2 it would be more secure and it would be compilation error). However here is a raw type.

Now let's use this method as following:

List<Integer> list1 = new ArrayList<Integer>();
List<Double> list2 = new ArrayList<Double>();

System.out.println("Contains? " + (new Wildcards()).contains(list1, list2));
System.out.println("List1 element: " + list1.get(0));

I will not get any errors and receive the following result:

Contains? false

List1 element: 12sdf34

Could anyone explain how it might be as I initialized list1 as List of Integers?

Community
  • 1
  • 1
Kirill Ch
  • 5,496
  • 4
  • 44
  • 65
  • 1
    `list1` is empty. Thus, your `foreach`-loop is never entered. And you do not get an error/exception because [generics are erased after compilation](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html). At runtime, all generic parameters are replaced with their respective upper bound or `Object` if no bound exists. A question regarding your code: why do you use `Boolean` instead of `boolean` as return type of your method? – Turing85 Apr 25 '18 at 13:02

2 Answers2

4

At runtime, generic type parameters are erased, which you can effectively think of as meaning that parameter types are swapped for Object. Thus, if compile has succeeded, you can add any object of any type to any list.

All generic type-checking is done at compile time, and the compiler can't raise an error for raw types, otherwise code written for Java 1.4 (pre-generics) or older wouldn't compile. Hence, instead, it raises the "rawtypes" warning.

user31601
  • 2,482
  • 1
  • 12
  • 22
  • 2
    Note, however, that *getting objects back* from the list, has its chances to fail. As the list is declared List, when getting items from it the compiler will usually attempt to get Integers. That is to say, it will usually cast to Integer the Object obtained from calling get(). In the example given in the question that is not happening because list1.get(0) is used somewhere an Integer isn't required and an Object works fine. Casting to Integer would be an unneeded overhead. However, Integer i = list1.get(0) would trigger a ClassCastException. Which, after I re-read myself, is obvious. – kumesana Apr 25 '18 at 13:16
2

The answer is two-folded. First, generics are erased during compilation. The second part is the actual implementation of ArrayList. Let's start with type erasure.

At compile time, all generic types are replaced with their upper bound. For example a generic parameter <T extends Comparable<T>> collapses to Comparable<T>. If no upper bound is given, it is replaced with Object. This makes generics an efficient tool for type-checking at compile-time, but we loose all type information at runtime. Project Valhalla may or may not fix that in the future. Since your method deals with raw- and unbounded types, the compiler assumes Object as generic type and thus list1.add("12sdf34"); passes type checking.

So why don't you get some exception at runtime? Why does ArrayList not "recognize" that the value you give it is of wrong type? Because ArrayList uses an Object[] as its backing buffer. Next logical question is: Why does ArrayList use an Object[] instead of an T[]? Because of type erasure: we cannot instantiate anything of T or T[] at runtime. Furthermore, the fact that arrays are covariant and retained, while generics are invariant and erased, makes for an explosive product if these two are mixed.

For your program, that means that neither a compilation-error nor a runtime exception will be thrown and thus your ArrayList<Integer> may contain a String. You will, however, get into troubles by writing

...
System.out.println("Contains? " + (new Wildcards()).contains(list1, list2));
System.out.println("List1 element: " + list1.get(0))
int i = list1.get(0);

From the view of the lexer, the code is still valid. But at runtime, the assignment will generate a ClassCastException. This is one of the reasons why raw types should be avoided.

Turing85
  • 18,217
  • 7
  • 33
  • 58