連載
» 2006年05月20日 00時00分 公開

デバッグのヒント教えます(2):スタックトレースからデバッグのヒントを読み取る

[中越智哉,ナレッジエックス]

 ランタイムエラーが発生したときに、コンソールなどによく表示されるものとして、「スタックトレース」があります。これは、例外の発生状況と発生個所を詳細に示すもので、デバッグ時には大いに役に立ちます。

 しかし、読み方が分からないと何から手を付けたらよいか分からないのも事実です。特にWeb系のプログラミングをされている方は、非常に長いスタックトレースや2組のスタックトレースが同時に表示されてしまい何をどう見ればよいのか分からないこともあるのではないでしょうか。

 今回は、まずスタックトレースの読み方から解説しましょう。

スタックトレースの読み方

分類:ランタイムエラー

 スタックトレースは、その例外に関連しているクラスの数によって長さはまちまちですが、その書式は一定です。

書式:
 (1)例外クラス名:詳細メッセージ
 (2) at クラス名.メソッド名(ソースファイル名:行番号)
 (3)(2)の呼び出し元メソッドに関する(2)と同様の表示の繰り返し 

スタックトレースの例

 例えば、次のようなクラスを見てみましょう。このプログラムはServletクラスです。このプログラムを、Tomcat(バージョンは5.5.12を想定)で実行してみます(web.xmlの記述や、デプロイの方法については割愛します)。実行時に、URLにnumberというパラメータを付けると、その数値を2乗した値が表示されますが、パラメータの内容が数値でない場合は、例外が発生し、ブラウザにはスタックトレースの内容が表示されます。

URLに「?number=123」を付けた場合の実行結果 URLに「?number=123」を付けた場合の実行結果
URLに「?number=aaa」を付けた場合の実行結果 URLに「?number=aaa」を付けた場合の実行結果
package kx;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Tips1_7Servlet extends HttpServlet {
    protected void doGet
            (HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String numberStr = request.getParameter("number");
        int numberInt = Integer.parseInt(numberStr);
        int result = numberInt * numberInt;
        response.setContentType("text/html;charset=Windows-31J");
        PrintWriter out = response.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Debug Sample</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>入力された数値の2乗は"+result+"です");
        out.close();
    }
} 

 では、スタックトレースの内容を見てみましょう。先ほどの画面に「原因のすべてのスタックトレースは、Apache Tomcat/5.5.12のログに記録されています」とあるとおり、ブラウザにはトレースの一部しか表示されていませんので、Tomcatのログに表示されるトレースを見てみることにします。

java.lang.NumberFormatException: For input string: "aaa"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at kx.Tips1_7Servlet.doGet(Tips1_7Servlet.java:16)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:689)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain
.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(
ApplicationFilterChain.java:173)
at org.apache.catalina.core.StandardWrapperValve.invoke(
StandardWrapperValve.java:213)
at org.apache.catalina.core.StandardContextValve.invoke(
StandardContextValve.java:178)
at org.apache.catalina.core.StandardHostValve.invoke(
StandardHostValve.java:126)
at org.apache.catalina.valves.ErrorReportValve.invoke(
ErrorReportValve.java:105)
at org.apache.catalina.core.StandardEngineValve.invoke(
StandardEngineValve.java:107)
at org.apache.catalina.connector.CoyoteAdapter.service(
CoyoteAdapter.java:148)
at org.apache.coyote.http11.Http11Processor.process(
Http11Processor.java:868)
at org.apache.coyote.http11.Http11BaseProtocol$
Http11ConnectionHandler.processConnection(
Http11BaseProtocol.java:663)
at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(
PoolTcpEndpoint.java:527)
at org.apache.tomcat.util.net.LeaderFollowerWorkerThread.runIt
(LeaderFollowerWorkerThread.java:80)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(
ThreadPool.java:684)
at java.lang.Thread.run(Unknown Source) 

 かなり大量のトレースが表示されていますが、まずは先頭から順に見ていきましょう。トレースの先頭は、例外クラス名とその詳細メッセージです。

java.lang.NumberFormatException: For input string: "aaa" 

 java.lang.NumberFormatExceptionは、数値のフォーマットに関する例外です。詳細メッセージの「For input string: "aaa"」から、正確な意味が分からなくても、何となく"aaa"という文字列に関連して何か原因がありそうだということが分かります。

 では次にこの例外がどこで発生しているかを見てみましょう。スタックトレースにはかなり多くのクラス名が列挙されていますが、大半のクラス名は見る必要がありません(注)。このスタックトレースで見るべきクラス名は、「自分が作成したクラス」です。この例では、

at kx.Tips1_7Servlet.doGet(Tips1_7Servlet.java:16) 

が、自分の作成したクラスです。()内にある行番号を見れば、実際のソース上での例外発生個所が分かりますので、あとはその個所で例外となり得る原因を考え、対処を施すことになります。この例ではInteger.parseInt()を実行する際に、数値以外の文字列が入ってしまう可能性があるため、その場合に例外が発生するのです。

注:実際には、スタックトレースは例外が発生した直接の原因となるメソッドから順に、呼び出し元のメソッドの個所が連なって表示されます。ですから、上位に表示されたメソッドほど直接の例外の発生原因に近いのですが、それがAPIやフレームワークなどのクラスのメソッドである場合、それら自身のバグでない限りは、まず自分のクラスを疑うのが原則です。

「スタックトレースの読み方」=「(1)先頭が例外の種類と内容、(2)2行目以降から自分が作成したクラスを探してみる」


「処理されない例外の型 Exception」が出てしまった

分類:コンパイルエラー

 このコンパイルエラーは、例外処理を扱うプログラミングをしている場合によく遭遇するものです。次の例を見てください。

package kx;
public class Tips1_5 {
    int age = 0;
    
    public void setAge(int newAge) {
        if (newAge >= 0) {
            age = newAge;
        } else {
            throw new Exception("年齢に負数は代入できません");
        }
    }
}

 この例では、リストの15行目「throw new Exception("...");」の個所で、コンパイルエラー「処理されない例外の型 Exception」が表示されます。

注:J2SE 5.0のjavacでは、「例外 java.lang.Exception は報告されません。スローするにはキャッチまたは、スロー宣言をしなければなりません。」と表示されます。

 このコンパイルエラーの対処を知るには、例外処理の仕組みについて少々知っておく必要があります。

 まず、java.lang.Exceptionを基底とする例外クラスを自分のプログラムから送出(throw)する場合には、メソッド定義部にthrows節を用いて送出される可能性のある例外クラスをあらかじめ定義しておく必要があります。もしくは、そのメソッドから例外を送出するつもりがない場合は、try〜catch構文によってその例外を補足し、例外が送出されないようにしなければなりません。

 throws節を追加し、前者の対応を行う場合は、setAgeメソッドの定義は次のようになります。

public void setAge(int newAge) throws Exception {
    if (newAge >= 0) {
        age = newAge;
    } else {
        throw new Exception("年齢に負数は代入できません");
    }
}

 こうすることで、正しく例外送出ができるように定義することができます。後者の対応が必要な例を見てみましょう。

package kx;
package kx;
public class Tips1_6 {
    public static void main(String[] args) {
        Class.forName("com.mysql.Driver");
        
    }
    
}

 この例では、MySQL用のJDBCドライバクラスである com.mysql.Driverクラスを、Class.forName()によってロードしようとしていますが、コンパイルすると、「処理されない例外の型 ClassNotFoundException」が表示されます。そこで、try〜catch節を使ってこの例外を捕捉できるようにしてみましょう。

try {
        Class.forName("com.mysql.Driver");
    } catch(ClassNotFoundException ex) {
        ex.printStackTrace();
        System.out.println("ドライバクラスのロードに失敗しました");
    } 

 こうすることで、例外を正しく捕捉できるように定義することができます。

「処理されない例外の型」=「throws節をメソッドに定義するか、try〜catch節でその例外を捕捉する」


「NullPointerException」が発生した

分類:ランタイムエラー

 NullPointerExceptionは、ランタイムエラーの中でも非常によく見かけるものの1つです。J2SE 5.0のJavadocでNullPointerExceptionを調べてみると、

オブジェクトが必要な場合に、アプリケーションが null を使おうとするとスローされます。例えば、以下のような場合があります。

* null オブジェクトのインスタンスメソッドの呼び出し

* null オブジェクトのフィールドに対するアクセスまたは変更

* null の長さを配列であるかのように取得

* null のスロットを配列であるかのようにアクセスまたは修正

* null をThrowable値であるかのようにスロー


とあります。中身がnullのオブジェクトに対して何か操作をしようとすると起きる例外ということですね。では例を見てみましょう。

package kx;
public class Tips1_7 {
    public static void main(String[] args) {
        String str = null;
        System.out.println("変数strの長さは"+str.length()+"です");
        
    }
    
}

 このプログラムを実行すると、NullPointerExceptionが発生します。

 この例は直前の行で変数strにnullを代入しているので非常に分かりやすいのですが、もっと複雑なコードでは、その個所を見ただけではそのオブジェクトがnullなのかどうかは分からないことが多いのです。発生個所からコードをさかのぼっていき、オブジェクトにnullが代入されたり、nullに初期化したままになっていないかどうかを調べてみましょう。

「NullPointerExceptionの発生」=「発生個所にあるオブジェクトのどれかがnullになっていないかを調べよう」



Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

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

メールマガジン登録

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