Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ambiguous behaviour in casting

I was teaching students the old-school Generics and came across an unseen! behavior while I was presenting! :(

I have a simple class

public class ObjectUtility {

  public static void main(String[] args) {
    System.out.println(castToType(10,new HashMap<Integer,Integer>()));
  }

  private static <V,T> T castToType(V value, T type){
    return (T) value;
  }

}

This gives output as 10,without any error!!! I was expecting this to give me a ClassCastException, with some error like Integer cannot be cast to HashMap.

Curious and Furious, I tried getClass() on the return value, something like below

System.out.println(castToType(10,new HashMap<Integer,Integer>()).getClass());

which is throwing a ClassCastException as I expected.

Also, when I break the same statement into two, something like

Object o = castToType(10,new HashMap<Integer,Integer>());
System.out.println(o.getClass());

It is not throwing any error and prints class java.lang.Integer

All are executed with

openjdk version "1.7.0_181"
OpenJDK Runtime Environment (Zulu 7.23.0.1-macosx) (build 1.7.0_181-b01)
OpenJDK 64-Bit Server VM (Zulu 7.23.0.1-macosx) (build 24.181-b01, mixed mode)

Can someone point me in the right direction on Why this behaviour is happening?

like image 825
Mohamed Anees A Avatar asked Feb 03 '23 17:02

Mohamed Anees A


2 Answers

T doesn't exist at runtime. It resolves to the lower bound of the constraint. In this case, there are none, so it resolves to Object. Everything can be cast to Object, so no class cast exception.

If you were to do change the constraint to this

private static <V,T extends Map<?,?>> T castToType(V value, T type){
    return (T) value;
}

then the cast to T becomes a cast to the lower bound Map, which obviously Integer is not, and you get the class cast exception you're expecting.


Also, when I break the same statement into two, something like

Object o = castToType(10,new HashMap<Integer,Integer>());
System.out.println(o.getClass());

It is not throwing any error

castToType(10,new HashMap<Integer,Integer>()).getClass() 

This throws a class cast exception because it statically links to the method HashMap::getClass (not Object::getClass) since the signature says to expect HashMap as a return value. This necessitates an implicit cast to HashMap which fails because castToType returns an Integer at runtime.

When you use this first

Object o = castToType(10,new HashMap<Integer,Integer>());

you are now statically linking against Object::getClass which is fine regardless of what's actually returned.

The "unsplit" version is equivalent to this

final HashMap<Integer, Integer> map = castToType(10, new HashMap<>());
System.out.println(map.getClass());

which hopefully demonstrates the difference

like image 115
Michael Avatar answered Feb 06 '23 11:02

Michael


You could see the differences using javap tool.

The compiling process by default makes code optimizations that changes the Generic types into the primitive ones

First code:

public class ObjectUtility {

  public static void main(String[] args) {
    System.out.println(castToType(10,new java.util.HashMap<Integer,Integer>()));
  }

  private static <V,T> T castToType(V value, T type){
    return (T) value;
  }

}

Real PseudoCode:

Compiled from "ObjectUtility.java"
public class ObjectUtility {
  public ObjectUtility();
    descriptor: ()V
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       8: new           #4                  // class java/util/HashMap
      11: dup
      12: invokespecial #5                  // Method java/util/HashMap."<init>":()V
      15: invokestatic  #6                  // Method castToType:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      21: return
    LineNumberTable:
      line 4: 0
      line 5: 21

  private static <V, T> T castToType(V, T);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
    Code:
       0: aload_0
       1: areturn
    LineNumberTable:
      line 8: 0
}

The calls of the Generic types are changed to Object and an Integer.valueOf is added on the System out print.

Second code:

public class ObjectUtility {

  public static void main(String[] args) {
    System.out.println(castToType(10,new java.util.HashMap<Integer,Integer>()).getClass());
  }

  private static <V,T> T castToType(V value, T type){
    return (T) value;
  }

}

Real Pseudo Code:

Compiled from "ObjectUtility.java"
public class ObjectUtility {
  public ObjectUtility();
    descriptor: ()V
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        10
       5: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       8: new           #4                  // class java/util/HashMap
      11: dup
      12: invokespecial #5                  // Method java/util/HashMap."<init>":()V
      15: invokestatic  #6                  // Method castToType:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      18: checkcast     #4                  // class java/util/HashMap
      21: invokevirtual #7                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      27: return
    LineNumberTable:
      line 4: 0
      line 5: 27

  private static <V, T> T castToType(V, T);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
    Code:
       0: aload_0
       1: areturn
    LineNumberTable:
      line 8: 0
}

The checkcast is invoqued over HashMap but the signature is changed to Object and the returnt is the value as int without the cast inside castToType. The "int" primitive type causes an invalid cast

Third Code:

public class ObjectUtility {

  public static void main(String[] args) {
    Object o = castToType(10,new java.util.HashMap<Integer,Integer>());
    System.out.println(o.getClass());
  }

  private static <V,T> T castToType(V value, T type){
    return (T) value;
  }

}

Real Pseudo Code:

Compiled from "ObjectUtility.java"
public class ObjectUtility {
  public ObjectUtility();
    descriptor: ()V
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: bipush        10
       2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: new           #3                  // class java/util/HashMap
       8: dup
       9: invokespecial #4                  // Method java/util/HashMap."<init>":()V
      12: invokestatic  #5                  // Method castToType:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      15: astore_1
      16: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: aload_1
      20: invokevirtual #7                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
      23: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      26: return
    LineNumberTable:
      line 4: 0
      line 5: 16
      line 6: 26

  private static <V, T> T castToType(V, T);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
    Code:
       0: aload_0
       1: areturn
    LineNumberTable:
      line 9: 0
}

At this case the method is similar to the first one. castToType returns the first parameter without change.

As you can see the java compiler mades some "performance" changes that could affect in some cases. The Generics are an "invention" of the source code that are finally converted to the real type required in any case.

like image 27
Dubas Avatar answered Feb 06 '23 11:02

Dubas