Consider the following code:

import java.util.List;
import java.util.ArrayList;

public class UncheckedChecker {
    List list = new ArrayList();
}

When compiled with javac -Xlint (nonstardard option for turning on all warnings), the following output is spit out to console:

UncheckedChecker.java:5: warning: [rawtypes] found raw type: List
List list = new ArrayList();
^
missing type arguments for generic class List
where E is a type-variable:
E extends Object declared in interface List
UncheckedChecker.java:5: warning: [rawtypes] found raw type: ArrayList
List list = new ArrayList();
^
missing type arguments for generic class ArrayList
where E is a type-variable:
E extends Object declared in class ArrayList
2 warnings

Java generics was introduced in Java 1.5 and is heavily used in modern Java. The question is: why should we always use types in generic classes?

Let’s illustrate this using an code snippet from Effective Java:

import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

public class UncheckedChecker2 {
    public static void main(String[] args) {
        // This collection will ONLY hold a collection
        // of objects of type Stamp!
        // Please, don't be cruel and do not insert Coins
        // into this collection!
        List stamps = new ArrayList();
        stamps.add(new Stamp(1998, "JFK"));
        stamps.add(new Coin(1954, 20));
        //...
        for(Iterator i = stamps.iterator(); i.hasNext();) {
            Stamp st = (Stamp) i.next();
            System.out.println(
                "Stamp from the year " 
                    + st.year 
                    + " and of value " 
                    + st.name);
        }
    }
    public static final class Stamp {
        public final int year;
        public final String name;
        public Stamp(int year, String name) {
            this.year = year;
            this.name = name;
        }
    }
    public static final class Coin {
        public final int year;
        public final int value;
        public Coin(int year, int value) {
            this.year = year;
            this.value = value;
        }
    }
}

As you can imagine, compiling and running this results in the following:

$ javac UncheckedChecker2.java

 Note: UncheckedChecker2.java uses unchecked or unsafe operations.
 Note: Recompile with -Xlint:unchecked for details.

$ java UncheckedChecker2

 Stamp from the year 1998 and of value JFK
 Exception in thread "main" java.lang.ClassCastException: UncheckedChecker2$Coin cannot be cast to UncheckedChecker2$Stamp
   at UncheckedChecker2.main(UncheckedChecker2.java:15)

In Angelika Langer’s FAQ, another possible type of warning is pointed out to be possible to happen:

class Wrapper<T> {
  private T wrapped ;
  public Wrapper (T arg) {wrapped = arg;}

  ...
  public Wrapper <T> clone() {
    Wrapper<T> clon = null;
     try {
       clon = (Wrapper<T>) super.clone(); // unchecked warning
     } catch (CloneNotSupportedException e) {
       throw new InternalError();
     }
     try {
       Class<?> clzz = this.wrapped.getClass();
       Method   meth = clzz.getMethod("clone", new Class[0]);
       Object   dupl = meth.invoke(this.wrapped, new Object[0]);
       clon.wrapped = (T) dupl; // unchecked warning
     } catch (Exception e) {}
     return clon;
  }
}

She explains that “A cast whose target type is either a (concrete or bounded wildcard) parameterized type or a type parameter is unsafe, if a dynamic type check at runtime is involved. At runtime, only the type erasure is available, not the exact static type that is visible in the source code. As a result, the runtime part of the cast is performed based on the type erasure, not on the exact static type.”

In the example, the cast to Wrapper would check whether the object returned from super.clone is a Wrapper, not whether it is a wrapper with a particular type. Similarly, the casts to the type parameter T are cast to type Object at runtime, and probably optimized away altogether. Due to type erasure, the runtime system is unable to perform more useful type checks at runtime.

Another point to this is to mention how generics in Java are implemented. Java language implements generics via principle called type erasure. This is basically a dirty hack which is done at compile time - all the generics are erased by the compiler (and compiler adds casts to the generic type where necessary). To demonstrate this further, consider this Java source code and its appropriate bytecode:

import java.util.List;
import java.util.ArrayList;

public class CheckedChecker {
    public static void main(String[] args) {
        List<String> strs = new ArrayList<>();
        strs.add("ahoj");
        strs.add("svete");
        for(String str : strs) {
            System.out.println(str);
        }
    }
}

Output of javap -c UncheckedChecker:

public class CheckedChecker {
  public static void main(java.lang.String[]);
    Code:
       0: new             #2                // class java/util/ArrayList
       3: dup
       4: invokespecial   #3                // Method java/util/ArrayList.<init>:()V
       7: astore_1
       8: aload_1
       9: ldc             #4                // String ahoj
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc             #6                // String svete
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: invokeinterface #7,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      32: astore_2
      33: aload_2
      34: invokeinterface #8,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z
      39: ifeq            62
      42: aload_2
      43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      48: checkcast       #10               // class java/lang/String
      51: astore_3
      52: getstatic       #11               // Field java/lang/System.out:Ljava/io/PrintStream;
      55: aload_3
      56: invokevirtual   #12               // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      59: goto            33
      62: return
}

What we naturally expect from compiler when type erasure is present in the language architecture is that compiler refuses to compile code with generics which breaks type safety. Consider this simple example:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MyStamps {
   public static final class Stamp {
        public final int year;
        public final String name;
        public Stamp(int year, String name) {
            this.year = year;
            this.name = name;
        }
    }
   public static final List<Stamp> STAMPS =
       Arrays.asList(
            new Stamp(1938, "TGM"),
            new Stamp(2008, "Special edition")
        );
   public static void main(String... args) {
       STAMPS.add("My new stamp");
   }
}

It should be obvious that adding object of type java.lang.String should be an illegal operation as java.lang.String is not a subtype of Stamp.

For completeness sake, here is the compiler output:

MyStamps.java:21: error: no suitable method found for add(String)
 STAMPS.add("My new stamp");
 ^
 method Collection.add(Stamp) is not applicable
 (argument mismatch; String cannot be converted to Stamp)
 method List.add(Stamp) is not applicable
 (argument mismatch; String cannot be converted to Stamp)
 Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output

Further reading: