JDKのバージョンが原因で起きるコンパイルエラーデバッグのヒント教えます(8)

» 2006年08月31日 00時00分 公開
[中越智哉ナレッジエックス]

 お盆を過ぎて暑さもようやく一段落してきましたが、皆さんいかがお過ごしですか。お盆休みで故郷に帰省して、のんびりできた方もいらっしゃるでしょうか。お盆休みを避けて、これから休暇を取られる方や、開発プロジェクトによっては、上期末に納品を控えていて、休みは納品後にまとめて、という方もいらっしゃるかもしれませんね。

 この連載もいよいよラストスパートです。お休みが取れた方も、これからお休みを取られる方も、最後までどうぞお付き合いください。

JDKのバージョンの違いが原因で起きるコンパイルエラー

分類:コンパイルエラー

 今回ご紹介するコンパイルエラーは、ちょっと特殊な環境で発生するエラーです。閉じた環境で開発をしている場合にはなかなか起きないですが、チームで開発をしていたりすると、発生することもありますのでぜひ知っておいてください。

 このコンパイルエラーは、かなり限定的な状況で発生するものなので、エラーを発生する手順を先に紹介していきたいと思います。

 まず、次のような2つのクラスTips4_1_1Tips4_1_2を用意します。

package kx;
public class Tips4_1_1 {
        public static void main(String[] args) {
                Tips4_1_2 t = new Tips4_1_2();
                        t.print();
        }
} 
Tips4_1_1
package kx;
public class Tips4_1_2 {
        public void print() {
                System.out.println("Hello!");
        }
} 
Tips4_1_2

 コードを見ると分かるように、Tips4_1_1は、内部でTips4_1_2のインスタンスを生成し、メソッドを呼び出しているので、Tips4_1_1Tips4_1_2を必要としていることが分かります(クラス図を図1に示します)。

図1 クラス図 図1 クラス図

 では、これらのクラスをJ2SE5.0でコンパイルしてみましょう。これらのクラスには特に文法的な誤りはありませんので、コンパイルエラーなどは起きません。

 次に、Tips4_1_1だけをJ2SE 1.4でコンパイルしてみてください(IDEなどで実行すると再現しないことがあるので、コマンドラインから実行することをお勧めします)。

 すると、次のようなコンパイルエラーが表示されます(画面の様子は図2)。

kx\Tips4_1_1.java:11: kx.Tips4_1_2 にアクセスできません。
クラスファイル .\kx\Tips4_1_2.class は不正です。
クラスファイルのバージョン 49.0 は不正です。48.0 であるべきです。
削除するか、クラスパスの正しいサブディレクトリにあるかを確認してください。
                Tips4_1_2 t = new Tips4_1_2();
                ^
エラー 1 個 
図2 コンパイルエラーが発生した様子 図2 コンパイルエラーが発生した様子

 つまり、Tips4_1_1.javaをコンパイルすると、そのクラス中でTips4_1_2を参照している(インスタンスの生成およびメソッド呼び出し)ので、コンパイラはTips4_1_2のクラスファイルを参照します。Tips4_1_2にはすでにコンパイル済みのクラスファイルが存在していますが、そのクラスファイルはJ2SE5.0でコンパイルしたものなので、コンパイラ自身のバージョン(J2SE1.4)よりも新しいバージョンです。

 このような場合、コンパイラはTips4_1_2.javaをリコンパイルせずに、上記のようなコンパイルエラーを表示するのです(図3を参照)。

  

図3 コンパイルエラーが発生する様子。48.0は、J2SE1.4でコンパイルされたことを示すクラスファイルのバージョン番号、同じく49.0は、J2SE5.0でコンパイルされたことを示すクラスファイルのバージョン番号 図3 コンパイルエラーが発生する様子。48.0は、J2SE1.4でコンパイルされたことを示すクラスファイルのバージョン番号、同じく49.0は、J2SE5.0でコンパイルされたことを示すクラスファイルのバージョン番号

 このコンパイルエラーが出た場合には、以下のいずれかの方法で解消することができます。

  1. 新しいバージョンのクラスファイルを削除してから、再度コンパイルを行う
  2. 依存しているクラスも含めて一度にコンパイルを行う
  3. コンパイラ自身のバージョンを新しいバージョンに合わせたものに変える

「クラスファイルのバージョン番号xx.xは不正です。xx.xであるべきです」が表示されたら=以前コンパイルしたときのJavaのバージョンと、いまコンパイルしようとしているコンパイラのJavaのバージョンが異なっていないかチェックする。

「ClassCastException」で落ちてしまったら

分類:ランタイムエラー

 Javaでは、あらゆるクラスは暗黙のうちにjava.lang.Objectを継承している、というルールがあります。さらに、ポリモルフィズムといって、あるクラスの型がその親クラスであるかのように見せ掛けることもできるようになっています。この2つの性質を利用すると、型の違いに合わせていちいちメソッドを定義しなくても、あらゆるクラスのインスタンスを引数に受け取ったり、戻り値として呼び出し元に渡したりすることができます。

 次の例を見てください。

package kx;
public class Tips4_2_1 {
        private Object anyObj;
        public void setObject(Object newObj) {
                nyObj = newObj;
        }
        public Object getObject() {
                return anyObj;
        }
} 
Tips4_2_1

 このクラスはsetObject、getObjectというgetter/setterを持っています。このgetter/setterによってアクセスされる値はObject型の変数なので、どのようなクラスのインスタンスも渡すことができ、また、受け取ることができます。ですが注意しなければならないのは、次のようなプログラムを書いた場合です。

package kx;
public class Tips4_2_2 {
        public static void main(String[] args) {
                Tips4_2_1 t = new Tips4_2_1();
                String s = "ナレッジエックス";
                t.setObject(s);
                Integer i = (Integer)t.getObject();
        }
}
Tips4_2_2

 このプログラムでは、前述のTips4_2_1クラスのインスタンスに、setObjectメソッドでString型のオブジェクトを渡し、直後にgetObjectメソッドでそのオブジェクトを取り出しています。getObjectメソッドは戻り値がObject型なので、通常は取り出したオブジェクトはキャスト演算子によってキャスト(型変換)をしてから使います。このプログラムでも取り出したオブジェクトをキャストして変数に代入しています。このプログラムをコンパイルしても、コンパイルエラーにはなりませんが、実行すると、ClassCastExceptionが発生します(図4)。

図4 ClassCastExceptionが発生した例 図4 ClassCastExceptionが発生した例

 このプログラムでは、String型のオブジェクトをsetObjectメソッドで渡しているにもかかわらず、取り出したオブジェクトをInteger型に変換しようとしています。ClassCastExceptionは、そのオブジェクトと実際には継承関係がないクラスにキャストをしようとすると発生します。StringとIntegerは、共通の親を持つクラスではありますが、両者には直接の継承関係はありません(図5)。

図5 StringとIntegerの継承関係 図5 StringとIntegerの継承関係

 このようなケースは、ポリモルフィズムの性質を生かして汎用的な操作ができる半面、外部からはどんな型のオブジェクトが入っているか分かりにくいことが問題となります。このような問題に対する1つの解決策として、J2SE 5.0からはGenericsという機能が使えるようになりました。Genericsを使ってクラスを定義しておくと、クラスの変数やメソッドの引数、戻り値の型などを、インスタンスを生成するときに動的に決めることができるようになります。次の例を見てください。

package kx;
public class Tips4_2_3<T> {
        private T anyObj;
        public void setObject(T newObj) {
                anyObj = newObj;
        }
        public T getObject() {
                return anyObj;
        }
} 
Tips4_2_3

 「T」というのが、Genericsの機能によって、後から決めることのできる型名(の代わり)です。このクラスを使って、先ほどと同じようなプログラムを作ってみます。

package kx;
public class Tips4_2_4 {
        public static void main(String[] args) {
                Tips4_2_3<String> t = new Tips4_2_3<String>();
                String s = "ナレッジエックス";
                t.setObject(s);
                Integer i = (Integer)t.getObject();
        }
}
Tips4_2_4

 Genericsの機能を使い、インスタンス生成時に「T」としていた部分をString型に設定しています。これによって、クラスTips4_2_3のgetter/setterでやりとりされるオブジェクトの型は、String型に限定されたことになります。そのため、getObjectメソッドで取り出したオブジェクトをInteger型にキャストしようとする記述は、コンパイル時にコンパイルエラーとなり(図6)、実行前に文法上の誤りとして修正することが可能になります。

図6 Genericsを使って型を指定した場合のコンパイルエラー 図6 Genericsを使って型を指定した場合のコンパイルエラー

ClassCastExceptionの発生=継承関係にないクラスへキャストしようとしていないかを確認。Genericsを活用すれば実行前にコンパイルエラーとして検出させることもできる。


例外に相当する状況なのに例外が出ない

分類:仕様どおり動かない

 プログラムを実行して例外が発生するのは、正常な動作をしていないので望ましくない状況ではありますが、例外が発生すべき状況なのに例外が発生しないというのも、安全なプログラムの実行のためには望ましくない状況です。例えば、次のプログラムを見てください。

package kx;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
public class Tips4_3 {
        public static ArrayList<String> getNames() {
                Connection conn?? = null;
                Statement? stmt?? = null;
                ResultSet? result = null;
                ArrayList<String> list = new ArrayList<String>();
                try {
                        Class.forName("com.mysql.jdbc.Driver");
                        String url ="jdbc:mysql://localhost/mydb";
                        String user = "user";
                        String pass = "passwd";
                        conn = DriverManager.getConnection(url,user,pass);
                        stmt = conn.createStatement();
                        result = stmt.executeQuery("SELECT NAME FROM EMP");
                        while(result.next()) {
                                list.add(result.getString("NAME"));
                        }
                } catch(ClassNotFoundException ex) {
                } catch(SQLException ex) {
                } finally {
                        try {
                                if (result!=null) result.close();
                                if (stmt!=null) stmt.close();
                                if (conn!=null) conn.close();
                        } catch(SQLException ex) {
                        }
                }
                return list;
        }
} 

 このクラスのgetNamesメソッドはJDBCを使ってEMPというテーブルにアクセスし、NAMEカラムの内容をArrayListに格納して返すというものです。EMPテーブルが空であった場合は、ArrayListには何も格納されませんので、中身が空のArrayListオブジェクトが返却されます。しかしこのメソッドでは、例外をcatch節で捕捉しているものの、catch節内に何も処理を書いていないため、データベースアクセスの処理で例外が発生した場合にも、中身が空のArrayListオブジェクトが返されてしまいます。

 try〜catch節によって例外を捕捉している場合は、特別な理由がない限りは、catch節を空のままにしてはいけません。この例のように、正常な状況による処理結果なのか、異常な状況による処理結果なのかが、全く区別できなくなってしまうからです。発生した例外への対処を、どのクラスのどのメソッドが行うべきかは、もちろん設計によって異なりますので、この場合も選択肢は1つではありませんが、例えば、次のような対応が考えられます。

(1)catch節を書かずに、メソッド定義部にthrows SQLException、ClassNotFoundExceptionなどを追加し、例外処理はメソッドの呼び出し元に任せる。

 変更されたコード(getNamesメソッド)は次のようなものです。

public static ArrayList<String> getNames() throws SQLException,ClassNotFoundException {
        (省略)
        try {
                Class.forName("com.mysql.jdbc.Driver");
                (省略)
        } finally {
                (省略)
        }
        return list;
} 

(2)メソッド定義部にユーザー定義の例外クラスを記述する。SQLExceptionやClassNotFoundExceptionをcatch節で捕捉したら、ユーザー定義の例外クラスのインスタンスを生成し、コンストラクタには捕捉した例外オブジェクトを渡してthrowする。

 コードは次のようなものです。

package kx;
public class TipsException extends Exception {
        public TipsException(Throwable t) {
                super(t);
        }
}
ユーザー定義の例外クラス(TipsException)
public static ArrayList<String> getNames() throws TipsException {
        (省略)
        try {
                (省略)
        } catch(Exception ex) {
                throw new TipsException(ex);
        } finally {
                try {
                        (省略)
                } catch(SQLException ex) {
                        throw new TipsException(ex);
                }
        }
        return list;
} 
変更されたgetNamesメソッド

(3)もし、(1)(2)どちらの対処もできない場合は、少なくともcatch節で例外を捕捉したときに、printStackTrace()メソッドで、例外のトレースは出力しておく。

 そうすれば、例外発生時には気が付かなかったとしても、後でログを確認することで例外の状況を知ることができるためです。

例外に相当する状況なのに例外が出ない=例外をcatchしているのに、何も対処をしていない個所がないかを調べる。個所がないかを調べる。



Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。