Edited at

Java7からJava8に移行した際にClassCastException発生 ~ジェネリックスとオーバロード~

More than 1 year has passed since last update.



自己紹介

名古屋Javaユーザグループ 2018年4月でLTしました!



実行環境


  • Java 1.8.0_162

  • Java 1.7.0_80

  • Eclipse 4.7.2(Pleiades)



目次


  1. 発生した問題

  2. 原因を調査

  3. 調査した結果



1. 発生した問題



実行するとどうなるでしょうか?


Main.java


public class Main {

public static void main(String[] args) {
String.valueOf(hoge());
}

static <E> E hoge() {
return (E) "hoge";
}

}




Java7は正常終了, Java8はClassCastException発生


  • Java 1.7.0_80:正常終了

  • Java 1.8.0_162:「java.lang.Stringはchar配列にキャストできない」というExceptionが発生


Console

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to [C

at jp.co.sample.Main.main(Main.java:13)

[Cはchar配列

https://docs.oracle.com/javase/specs/jls/se7/html/jls-10.html#jls-10.8



そもそも、このソースは何?


  • 2000年頃に作られたWebシステムの既存コード(発表用にソースは修正した)

  • Java8での老朽更新対応で、「Java8のマイナーバージョン違いで、実行結果が異なる」という問題が発生した


    • エラーが起きなかったJava8のマイナーバージョンを忘れてしまったため、本発表ではJava7とJava8で比較した





このソースの問題点


  • ジェネリックスの使い方が間違っている


    • 型パラメータが不要




  • String.valueOfの意味がない

本発表では、気にしないでください。



2. 原因を調査



Eclipseで確認

コンパイラはECJ(Eclipse Java Compiler)でJDKと異なるが、結果は同じだった。

image



Java7ではhogeの型パラメータはObject

Java7



Java8ではhogeの型パラメータはchar[]

Java8

char[]String.valueOfから来ている。



String.valueOfはオーバロードされている

char配列型 or Object型の引数を受け取れる


  • valueOf(char[] data)

  • valueOf(Object obj)

String.valueOfのオーバロード



状況整理


  • ジェネリックスメソッドの結果を、値Aとする


    • 戻り値は仮型パラメータ

    • 引数がないので、実型パラメータが不明



  • char配列型 or Object型の引数を受け取れるメソッドに、値Aを渡している



ジェネリックメソッドの確認


  • 型パラメータが決まれば(String)、java8でも正常終了する。


MainGenericsMethod.java

public class MainGenericsMethod {

public static void main(String[] args) {
String.valueOf(fuga("fuga"));
}

/** 引数から型パラメータが決まる */
static <E> E fuga(E arg) {
return (E) arg;
}
}


image



オーバロードの確認

オーバロードされたメソッドの組み合わせを変えて、Java8で実行した結果。


  • Objectとchar[] : NG

  • Objectとint[] : NG

  • Objectとchar : OK

  • Objectとint : OK

Object型 or 配列型を受け取るオーバロードされたメソッドが問題。


MainOverload.java

public class MainOverload {

public static void main(String[] args) {
overload(hoge());
}

static <E> E hoge() {
return (E) "hoge";
}

static void overload(Object arg) {}
static void overload(char[] arg) {} //引数の型を変える
}


image



3. 調査した結果



まとめ


  • 型パラメータが不明な値を、配列 or Objectを受け取るオーバロードされたメソッドに渡したときの動きは、Javaのバージョンによって異なる

  • Java1.7.0_80ではObject型を受け取るメソッドが実行される

  • Java1.8.0_162では配列型を受け取るメソッドが実行される



【補足】曖昧なオーバロード

int[] or char[]を受け取るオーバロードされたメソッドを呼び出すと、「メソッド overload(Object) は型 MainOverload であいまいです」というコンパイルエラーが発生する

        //コンパイルエラー

static void overload(int[] arg) {}
static void overload(char[] arg) {}



今回の問題と関係ありそうな記事


So, whereas before Java 8 the method argument site did not receive any inference, defaulting to Object, in Java 8 the most specific applicable type is inferred, in this case String.




【補足】Java8のjavap -verboseで逆アセンブルしてみた

Classfile /C:/Users/yuji3/Desktop/java-test/java8/Main.class

Last modified 2018/04/14; size 476 bytes
MD5 checksum 409af5d1b1986da6e0c4cd431dc50b59
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#20 // java/lang/Object."<init>":()V
#2 = Methodref #6.#21 // Main.hoge:()Ljava/lang/Object;
#3 = Class #22 // "[C"
#4 = Methodref #23.#24 // java/lang/String.valueOf:([C)Ljava/lang/String;
#5 = String #14 // hoge
#6 = Class #25 // Main
#7 = Class #26 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 hoge
#15 = Utf8 ()Ljava/lang/Object;
#16 = Utf8 Signature
#17 = Utf8 <E:Ljava/lang/Object;>()TE;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #8:#9 // "<init>":()V
#21 = NameAndType #14:#15 // hoge:()Ljava/lang/Object;
#22 = Utf8 [C
#23 = Class #27 // java/lang/String
#24 = NameAndType #28:#29 // valueOf:([C)Ljava/lang/String;
#25 = Utf8 Main
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/String
#28 = Utf8 valueOf
#29 = Utf8 ([C)Ljava/lang/String;
{
public Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method hoge:()Ljava/lang/Object;
3: checkcast #3 // class "[C"
6: invokestatic #4 // Method java/lang/String.valueOf:([C)Ljava/lang/String;
9: pop
10: return
LineNumberTable:
line 4: 0
line 5: 10

static <E extends java.lang.Object> E hoge();
descriptor: ()Ljava/lang/Object;
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #5 // String hoge
2: areturn
LineNumberTable:
line 8: 0
Signature: #17 // <E:Ljava/lang/Object;>()TE;
}
SourceFile: "Main.java"



【補足】Java7のjavap -verboseで逆アセンブルしてみた

Classfile /C:/Users/yuji3/Desktop/java-test/java7/Main.class

Last modified 2018/04/14; size 481 bytes
MD5 checksum 72c2eee8d5b013579f81dc56f53458d0
Compiled from "Main.java"
public class Main
SourceFile: "Main.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER

Constant pool:
#1 = Methodref #6.#19 // java/lang/Object."<init>":()V
#2 = Methodref #5.#20 // Main.hoge:()Ljava/lang/Object;
#3 = Methodref #21.#22 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#4 = String #13 // hoge
#5 = Class #23 // Main
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 hoge
#14 = Utf8 ()Ljava/lang/Object;
#15 = Utf8 Signature
#16 = Utf8 <E:Ljava/lang/Object;>()TE;
#17 = Utf8 SourceFile
#18 = Utf8 Main.java
#19 = NameAndType #7:#8 // "<init>":()V
#20 = NameAndType #13:#14 // hoge:()Ljava/lang/Object;
#21 = Class #25 // java/lang/String
#22 = NameAndType #26:#27 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#23 = Utf8 Main
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/String
#26 = Utf8 valueOf
#27 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
{
public Main();
flags: ACC_PUBLIC

Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC

Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method hoge:()Ljava/lang/Object;
3: invokestatic #3 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
6: pop
7: return
LineNumberTable:
line 4: 0
line 5: 7

static <E extends java/lang/Object> E hoge();
flags: ACC_STATIC

Code:
stack=1, locals=0, args_size=0
0: ldc #4 // String hoge
2: areturn
LineNumberTable:
line 8: 0
Signature: #16 // <E:Ljava/lang/Object;>()TE;
}