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



自己紹介

名古屋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と異なるが、結果は同じだった。


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


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

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


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

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

  • valueOf(char[] data)
  • valueOf(Object obj)


状況整理

  • ジェネリックスメソッドの結果を、値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;
    }
}


オーバロードの確認

オーバロードされたメソッドの組み合わせを変えて、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) {} //引数の型を変える
}


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;
}