イマカラブログ

イマカラメガネの中の人が、芝居と関係あることないこと好き勝手に書くブログ。

Eclipseプラグイン「JavadocDecorator」を作ったよ

いまさらEclipse

InteliJ IDEAが人気でEclipse使いが減ってきてる印象もありますが、SIerのお堅い仕事の現場では全然現役のEclipse

そんなお堅い現場だと、クラス名やメソッド名が「業務を識別する接頭辞+連番」だったりして、名前から内容が推測できず、ストレスフルな開発を強いられたりすることもある(実際に見かけたことはあるけど、運良く、自分が直接関わったことはない)。

でもまあ、名前から内容が推測できないという点では、英語で命名されてても英語が苦手な人にとっては結構しんどいんだよね。あと、ローマ字の母音を省略したやつとかも慣れてない人には辛い。

で、内容がわからないからとりあえずソースコード開いてJavadocをチラ見してすぐ閉じる。ってことを無意識にまあまあ頻繁にやってる(はず)。

そこで、そんなストレスを少しでも軽減できればと思いEclipseプラグイン「JavadocDecorator」を作ってみた。

何ができるプラグインなの?

パッケージエクスプローラなどでツリー表示されてるJava要素(クラスとかメソッドとか)の横っちょに、その要素のJavadocの1行目を表示します。

以上、全機能。

地味。ちょー地味。

でも便利。

英語がわからなくても、

f:id:imakaramegane:20190916194046p:plain

命名規則が英語のソースコード

母音省略された暗号的ローマ字でも、

f:id:imakaramegane:20190916195025p:plain

命名規則が暗号的ローマ字のソースコード

管理都合の名前でも、

f:id:imakaramegane:20190916195629p:plain

すぐにその現場から離れることをお勧めするソースコード

ほら!一目瞭然!

って、こんな適当にこさえたサンプルだけだとイメージしづらいところもあると思うし、何よりも適切に命名されたソースコードでもいい感じになるよというアピールのため、ちょっと規模のあるものをgithubから探して表示してみました。

f:id:imakaramegane:20190916211932p:plain

実際のソースコードでの表示例

※こちらのリポジトリソースコードを表示例にお借りしました。
GitHub - jkazama/ddd-java: Spring Boot + Java [ DDD Sample ]

ぜひ使ってみてください。

インストール方法

プラグインのインストール方法はいくつかありますが、代表的な簡単な方法を書きます。

Eclipse Marketplace からのインストール

下のボタンをEclipseのワークベンチにドラッグ&ドロップしてください。

Drag to your running Eclipse* workspace. *Requires Eclipse Marketplace Client

更新サイトからのインストール

[ヘルプ]メニューの[新規ソフトウェアのインストール...]を選択し、次のURLを入力してください。

https://imkrmgn.github.io/JavadocDecorator

Eclipse Marketplace からもインストール出来るようにしたくて登録したつもりなんだけど、承認メッセージが届かない。地味すぎると却下とかあるのかな。。。

⇒ 半日ぐらいでメールが届いて登録されました。

参考

プラグインソースコードはこちらで公開してます。

github.com

テスト時にプロパティファイルを動的に差し替える

はじめに

この記事は、Java8+JUnit4を使っています。

プロパティファイルを動的に差し替えたい

JUnitを使ってテストしていると、テスト対象のクラス内部でプロパティファイルを読み込んでいると、そのプロパティファイルをテストケース毎に差し替えたいことがある。

具体的にはこんな感じの実装になってるやつ。

public class Hoge{
    public String example() {
        Properties prop = new Properties();
        prop.load(this.getClass().getResourceAsStream("hoge.properties"));
        // 以降、propを使った処理が続く
    }
}

hoge.propertiesの内容によってexampleメソッドの返却値が変わるので、このClass#getResourceAsStreamで返却される内容を差し替えたいんだけど、外から差し替えるポイントがない。

Mavenを使ってる環境なら、src/test/resources配下にテスト用のhoge.propertiesを置いとくこともできるけど、それではバリエーションを持たせたテストが出来ない(いや、target/test-classes配下をテストケースから書き換えれば出来なくもないか?出来たとしてもあまり選択したくない方法だな)。

本来、設計(実装)から見直して欲しいところだけれども、諸事情によりテスト対象のクラスを修正するわけにもいかず、そのままテストせざるを得ない。ライブラリの内部で間接的に使われてるときとかもそう。

今回もそんなケースだったので、プロパティファイルを動的に差し替えるためにクラスローダーでゴニョゴニョした記録を残しておく。なお、良い子はマネしちゃいけません。

クラスローダーを作る

プロパティファイルというか、クラスパス上のリソースを読み込む処理というのは、多くの場合、根本を辿るとClassLoader#getResourceで得たURLオブジェクトを通してリソースを読み込むという動作になる(ここは説明が面倒なので省略)。したがって、自分にとって都合のよいURLオブジェクトがClassLoader#getResourceから返却されるような仕掛けを用意すればよい。

ということで、テスト用のクラスローダーを作成。

以下、ポイントを絞ってコードを抜粋。

public class TestClassLoader extends URLClassLoader {

    private static Map<String, URL> testResourceMap = new HashMap<>();

    /**
     * リソース名とテストリソースのマッピングを追加する
     * @param resourceName テストリソース名
     * @param testResourceURL テストリソースを読み込むためのURL
     */
    public static void addTestResourceMap(String resourceName, URL testResourceURL) {
        testResourceMap.put(resourceName, testResourceURL);
    }

    /**
     * テストリソースのマッピングをクリアする
     */
    public static void clearTestResourceMap() {
        testResourceMap.clear();
    }

    //省略

    /**
     * @param name リソース名
     * @return リソース名にテストリソースがマッピングされていたら、そのリソースを読み込むためのURL。
     *         マッピングされていない場合は、デフォルトの動作で得られたURL。
     */
    @Override
    public URL getResource(String name) {
        if (testResourceMap.containsKey(name)) {
            return testResourceMap.get(name);
        } else {
            return super.getResource(name);
        }
    }
}

ClassLoader#getResouceをオーバーライドして、引数のリソース名がtestResourceMapのキーとして存在したら、そのリソース名にマッピングされたURLオブジェクトを返却している。testResourceMapには、addTestResouceMapというstaticメソッドを通して、リソース名とURLオブジェクトのマッピングを追加できるようにした。clearTestResourceMapは、addTestResourceMapで追加したマッピングを全てクリアする。

このクラスローダーがあることを前提に、JUnitのテストケースが次のように実装できる。

@RunWith(TestRunner.class)
public class HogeTest {

    @Before
    public void setUp() {
        TestClassLoader.clearTestResourceMap();
    }

    @Test
    public void testHoge01() throws IOException {
        TestClassLoader.addTestResourceMap(
                "hoge.properties",
                Paths.get("testdata/hoge_test01.properties").toUri().toURL());

        Hoge hoge = new Hoge();
        String actual = hoge.exampale();
        //...
    }

    @Test
    public void testHoge02() throws IOException {
        TestClassLoader.addTestResourceMap(
                "hoge.properties",
                Paths.get("testdata/hoge_test02.properties").toUri().toURL());

        Hoge hoge = new Hoge();
        String actual = hoge.example();
        //...
    }
}

各テストメソッドの先頭でTestClassLoader#addTestResourceMapを使ってリソース名とリソース(URLオブジェクト)のマッピングを行う。これにより、テスト対象メソッド内部のhoge.propertiesの読み込みは、マッピングしたリソースへの読み込みに置き換えられる。なお、マッピングTestClassLoaderのstatic変数に保持しているので、@Beforeを付けたsetUpメソッドにてマッピングをクリアしている。

以上でプロパティファイルを動的に差し替えることができました。

おわり。

って、説明が足りてないね。

この仕掛けを成立させるには、テスト対象のクラスHogeをテスト用クラスローダーTestClassLoaderにロードさせる必要がある。

自作のクラスローダーを使うときは、javaコマンド実行時にシステムプロパティ「java.system.class.loader」で指定すれば使えるんだけど、この場合、自作クラスローダーがシステムクラスローダーとして動作するだけの実装が必要で、ハードルが高い。また、JUnit実行するときにシステムプロパティを指定するのも手間。

そこで、JUnitが動く仕掛けのなかで自作クラスローダーを差し込むポイントが無いか探った。

テストランナーを作る

JUnitを拡張するならまずは@Rule@ClassRuleだろうと探ってみたけどクラスローダーを差し込むポイントは見つけられなかった。じゃあテストランナー作るしかないなとJUnit4のデフォルトのテストランナーのコードを見たら以外とあっさり見つかった。

で、作ったテストランナーの実装はこれ。

public class TestRunner extends BlockJUnit4ClassRunner {

    private static TestClassLoader testClassLoader = new TestClassLoader();

    public TestRunner(Class<?> testClass) throws InitializationError, ClassNotFoundException {
        super(testClassLoader.loadClass(testClass.getName()));
    }
}

テストランナーのコンストラクタにテストケースのクラス(今回の例だとHogeTest)がJUnitCoreから引き渡される。テストランナーは、このテストケースのクラスのインスタンスをリフレクションで生成して、各テストメソッドを実行している。そこで、このコンストラクタに引き渡されたテストケースのクラスを自作クラスローダーでロードし直して、それが以降の処理で使われるようにした。

あとは、@RunWithでこのテストランナーを使うように指定して完成。

@RunWith(TestRunner.class)
public class HogeTest {
    //...
}

おわりに

クラスローダー周りは面倒なので説明を端折っちゃった。

全コードをgithubに上げておいたので興味のある人はどうぞ。もし、誰かの役に立てばなによりです。

なお、テスト対象がClassLoader#getSystemResouceClassLoader#getSystemResouceAsStreamを使っている場合、この記事の方式では差し替えられません。webアプリの場合、これらのメソッドを使っていることはまあ無いと思うので大丈夫だと思いますが。

オブジェクトの配列の初期化

思い出した

確かJava8になって簡潔に書けるようになってたよなーって思いつつ、結局、思い出せず手癖でループで初期化してたこんな感じのコード。

Object[] array = new Object[10];
for (int i = 0; i < array.length; i++) {
    array[i] = new Object();
}

Java8以降ではこう書ける。

Object[] array = new Object[10];
Arrays.setAll(array, i -> new Object());

わーい。すっきり。

おまけ

この手の奴はいつも性能が気になっちゃうので測定結果。

cpu=8
useFor              :   54   70   35   37   97   38   36   37   36   95 : avg=53.50
useSetAll           :   36   36   37   40   36   58   98   75   35   35 : avg=48.60
useParallelSetAll   :  241  239  238  238  237  249  236  236  235  236 : avg=238.50

測定したときのソース。

import java.util.Arrays;
import java.util.LongSummaryStatistics;

public class Test {

    static final int NUMER_OF_MEASURE = 10;
    static final int NUMBER_OF_RUN = 10000;
    static final int ARRAY_LENGTH = 1000;

    public static void main(String[] args) {
        int cpuNum = Runtime.getRuntime().availableProcessors();
        System.out.printf("cpu=%d%n", cpuNum);

        report("useFor", Test::useFor);
        report("useSetAll", Test::useSetAll);
        report("useParallelSetAll", Test::useParallelSetAll);
    }

    static void report(String name, Runnable runner) {
        measure(runner); // 空回し

        LongSummaryStatistics statics = new LongSummaryStatistics();
        
        System.out.printf("%-20s:", name);
        for (int i = 0; i < NUMER_OF_MEASURE; i++) {
            long time = measure(runner);
            statics.accept(time);
            System.out.printf("%,5d", time);
        }

        System.out.printf(" : avg=%,5.2f%n", statics.getAverage());
    }

    static long measure(Runnable runner) {
        long t = System.currentTimeMillis();
        for (int i = 0; i < NUMBER_OF_RUN; i++) {
            runner.run();
        }
        return System.currentTimeMillis() - t;
    }

    static void useFor() {
        Object[] array = new Object[ARRAY_LENGTH];
        for (int i = 0; i < array.length; i++) {
            array[i] = new Object();
        }
    }

    static void useSetAll() {
        Object[] array = new Object[ARRAY_LENGTH];
        Arrays.setAll(array, i -> new Object());
    }

    static void useParallelSetAll() {
        Object[] array = new Object[ARRAY_LENGTH];
        Arrays.parallelSetAll(array, i -> new Object());
    }

}

DateTimeFormatterで可変幅のミリ秒の解析

おさらい

前回の記事の続きです。

ミリ秒の解析のためにDateTimeFormatterを使ってパターン文字"S"を1文字で指定したら、 解析対象のミリ秒が2桁以上あると解析に失敗した。

// NGな例
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.S");
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt);
// -> java.time.format.DateTimeParseException: Text '2018-08-12T12:34:56.12' could not be parsed, unparsed text found at index 21

原因

日付や時、分、秒はパターン文字を1文字だけ書けば複数桁の解析ができるのになぜ? と調べてDateTimeFormatterBuilder#appendPattern(String)Javadocに次の記述を見つけた。

パターン文字の数によってフォーマットが決まります。ユーザーに焦点を合わせたパターンの説明については、DateTimeFormatterを参照してください。次の表は、パターン文字がどのようにビルダーにマップされるかを定義しています。

Pattern Count Equivalent builder methods
H 1 appendValue(ChronoField.HOUR_OF_DAY)
HH 2 appendValue(ChronoField.HOUR_OF_DAY, 2)
m 1 appendValue(ChronoField.MINUTE_OF_HOUR)
mm 2 appendValue(ChronoField.MINUTE_OF_HOUR, 2)
s 1 appendValue(ChronoField.SECOND_OF_MINUTE)
ss 2 appendValue(ChronoField.SECOND_OF_MINUTE, 2)
S..S 1..n appendFraction(ChronoField.NANO_OF_SECOND, n, n, false)

※表は抜粋

この表を見るとappendPatternに指定したパターン文字"S"は、内部でappendFractionの呼び出しに置き換えられていて、最小幅と最大幅が"S"を書いた文字数(n)に指定されている。

だから、パターン文字"S"を1文字だけ書いて解析対象のミリ秒が1文字でない場合は解析に失敗したのか。

一方、時(H)、分(m)、秒(s)などを1文字だけ書いた場合appendValueが内部で使用され、このメソッドは可変幅の文字も受け付けるので、解析対象が1桁でも複数桁でも解析に失敗しない。

なぜ、ミリ秒だけこんな仕様なんだ?他のフィールドと同じように1文字だけなら可変幅にすればいいじゃないかと疑問に感じたが、 冷静に考えると、解析対象としてミリ秒以外のフィールドを1文字幅に制限するということは通常考えられないが、ミリ秒(秒の小数部)は1文字幅に制限したいという可能性はある。 その可能性を考慮すると、この仕様は確かに妥当かと思い直した。

対策

とりあえず仕様に納得はしたものの困ったことには変わりない。

今やりたいことは「ミリ秒は可変幅で、それ以外のフィールドは固定幅で日時を解析」である。

みんなどうやってるのかググったところ、次のようにやってる人が多そう。

DateTimeFormatterBuilder dtfBldr = new DateTimeFormatterBuilder();
DateTimeFormatter dtf = dtfBldr
        .appendPattern("yyyy-MM-dd'T'HH:mm:ss")
        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 3, true)
        .toFormatter();
LocalDateTime ldt = dtf.parse("2018-08-01T12:34:56.12", LocalDateTime::from);
System.out.println(ldt); // -> 2018-08-01T12:34:56.120

うーん。ちょっとめんどい。

めんどいし、解析対象が小数点で終わっている場合に失敗してくれないのがちょっと嫌だ。

もうちょっとスマートにできないかと調べたら Stack Overflow で次のようにやってるのを見つけた。

DateTimeFormatter dtf = DateTimeFormatter
        .ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS][.SS][.S]");
LocalDateTime ldt = dtf.parse("2018-08-01T12:34:56.12", LocalDateTime::from);
System.out.println(ldt); // -> 2018-08-01T12:34:56.120

なるほど。オプションで長い幅からマッチングさせてるのか。頭いい。

小数部の桁数が多いときは先の方法でやったほうがいいけど、3桁ぐらいならこっちのほうがいいな。 これなら小数点で終わっている場合に解析失敗してくれるし。

とりあえず目先の用途としてはこれでいいか。

SimpleDateFormatのミリ秒

はじめに

この記事で使用してるのはJava8です。

ミリ秒の結果がおかしい

SimpleDateFormatでパースしたら、ミリ秒が期待した結果にならなかった。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
Date date = sdf.parse("2018-08-12T12:34:56.12");
System.out.println(date.toInstant()); // -> 2018-08-12T12:34:56.012Z

ミリ秒が.012で出力されてる。 いやいや。なんで1桁ずれてるのよ。

試しにもう1桁削ってみる。

Date date = sdf.parse("2018-08-12T12:34:56.1");
System.out.println(date.toInstant()); // -> 2018-08-12T12:34:56.001Z

うふふ。2桁ずれた。

逆に増やしてみる。

Date date = sdf.parse("2018-08-12T12:34:56.1234");
System.out.println(date.toInstant()); // -> 2018-08-12T12:34:57.234Z

1秒増えた(^^;

そっか。パターン文字"S"って、Javadocに書いてあるとおり「ミリ秒」なんだ。 「秒の小数部」じゃないのね。 いつも、桁数固定でパースしてたから今まで気付かなかった。

DateTimeFormatterだとどうなるの?

DateTimeFormatterのJavadocでは、パターン文字"S"は「fraction-of-second」って書いてあるから、こっちは期待した結果になりそう。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt);
// ->  java.time.format.DateTimeParseException: Text '2018-08-12T12:34:56.12' could not be parsed at index 20

こけたorz

ああ。SimpleDateFormatと違って桁数厳密なのか。

桁数をあわせて再実行。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SS"); // Sを1文字削って入力の桁数とあわせた
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt); // -> 2018-08-12T12:34:56.120

うん。期待した通りの結果。

しかし、私がパースしたい文字列の小数部の長さは可変なので、パターン文字"S"を1文字だけに修正。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.S"); // Sを1文字だけに修正
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt);
// -> java.time.format.DateTimeParseException: Text '2018-08-12T12:34:56.12' could not be parsed, unparsed text found at index 21

こけた...。

あれ?たしか可変長パースできたよな...。と次のとおり実行してみる。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("y-M-d'T'H:m:s.SS"); // S以外のパース文字を1文字に修正
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt)); // -> 2018-08-12T12:34:56.120

うん。問題ない。

もう一度、ここからパターン文字"S"を1文字だけに修正して実行。

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("y-M-d'T'H:m:s.S"); // Sも1文字に修正
LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from);
System.out.println(ldt);
// -> java.time.format.DateTimeParseException: Text '2018-08-12T12:34:56.12' could not be parsed, unparsed text found at index 21

!?

ここでJavadocを再確認して、色々、試してもわけがわからず時間をだいぶ潰す。。。

で、googleさんに聞いてみたらこんな記事が見つかった。

qiita.com

事象はちょっと違うけど、たぶんこれだな。 Java9で修正されてるっぽいのでJava9で実行してみたけど症状変わらず。 Java10ならどうよ?と、Java10をダウンロードして試しても症状変わらず。。。

え?バグじゃないの?もしかして仕様なの?

もう、眠いから明日調べてみよ。。。

スタンドアロンでJDTのASTを使う

f:id:imakaramegane:20170109201859j:plain

必要なライブラリ

JDTのASTをスタンドアロンで使いたい場合は、 Eclipseプラグインが格納されているフォルダにある、 以下のjarを拾ってきてクラスパスを通せば使える (アスタリスク(*)の部分は、実際にはバージョン番号とかになってる)。

Mavenのセントラルリポジトリにも登録されてるので、そこから拾うのが簡単かな。

pom.xmlはこんな感じ。

  <dependencies>
    <dependency>
      <groupId>org.eclipse.core</groupId>
      <artifactId>org.eclipse.core.contenttype</artifactId>
      <version>3.4.100.v20100505-1235</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.core</groupId>
      <artifactId>org.eclipse.core.jobs</artifactId>
      <version>3.5.100</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.birt.runtime</groupId>
      <artifactId>org.eclipse.core.resources</artifactId>
      <version>3.10.0.v20150423-0755</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.core</groupId>
      <artifactId>org.eclipse.core.runtime</artifactId>
      <version>3.7.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.equinox</groupId>
      <artifactId>org.eclipse.equinox.common</artifactId>
      <version>3.6.0.v20100503</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.equinox</groupId>
      <artifactId>org.eclipse.equinox.preferences</artifactId>
      <version>3.4.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jdt</groupId>
      <artifactId>org.eclipse.jdt.core</artifactId>
      <version>3.10.0</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse</groupId>
      <artifactId>org.eclipse.osgi</artifactId>
      <version>3.8.0.v20120529-1548</version>
    </dependency>
  </dependencies>

基本的な使い方

ASTParser#createASTを使ってASTを作成。 返却されたASTNodeacceptメソッドにASTVisitorを継承した自作クラスの インスタンスを引き渡してツリーを走査する(Visitorパターン)。

    String source = ...;  // 解析するJavaソースの文字列
    ASTParser parser = ASTParser.newParser( AST.JLS8 );
    Map<?, ?> options = JavaCore.getOptions();
    JavaCore.setComplianceOptions( JavaCore.VERSION_1_8, options );
    parser.setCompilerOptions( options );
    parser.setSource( source.toCharArray() );
    ASTNode ast = parser.createAST( null );
    ast.accept( new ASTVisitor() { ... } );

以下、ソースの断片を説明。

ASTParser#newParser

    ASTParser parser = ASTParser.newParser( AST.JLS8 );

ASTParserインスタンスを作成するときにJavaAPIレベルを指定しないといけないんだけど、 AST.JLS8が全バージョンに対応してるので、 固定でAST.JLS8を指定すればOK。 というか他が全部deprecatedになってるので選択の余地はない。

ASTParser#setCompilerOptions

    Map<?, ?> options = JavaCore.getOptions();
    JavaCore.setComplianceOptions( JavaCore.VERSION_1_8, options );
    parser.setCompilerOptions( options );

解析時に使用されるコンパイラオプションを引数に指定している。 ASTParser#setSource(char[])を使って1.5以降のソースを解析する場合、 このメソッドを使って適切なコンパイラオプションを指定する必要がある。

注意点として、ASTParser#setSourceIClassFileおよびICompilationUnitを引き渡した場合、 設定したコンパイラオプションがリセットされるらしい。

あと、optionsの中身を見るとソースのフォーマットに関するオプションも入ってた。 ASTでソースを改変することがあったら、細かく調べてみようかな。

ASTParser#createAST

    parser.setSource( source.toCharArray() );
    ASTNode ast = parser.createAST( null );

ASTParser#setSourceで設定したソースを解析してASTを構築して返却してくれる。 あとは、返却されたASTNodeインスタンスから各ノードを辿って自分の欲しい情報を 取得する。

返却されたASTNodeはデフォルトだとCompilationUnitインスタンスみたい。 ASTParser#setKindで変えることができるようだけど、必要になったときに調べる。

引数にIProgressMonitorインスタンスを渡せば、解析の進捗状況を確認したり、 途中でキャンセルできるみたい。必要ない場合はnullでOK。

おまけ

ソースファイルの中身を丸っと取得するコード。

    String source = new String(
            Files.readAllBytes( Paths.get( "Hoge.java" ) ), "utf-8" );

参考

blog.cles.jp

(2017/1/10 追記) もっと詳しい情報があった。。。 qiita.com

ITypeBindingから取得できる名前

JDTのASTを弄るときに、ITypeBindingから取得できる名前がどの形式で取得できたか毎度忘れちゃうのでメモ。

ITypeBinding#getQualifiedName

わかってるつもりだったんだけど、時々、思ったような結果が得られなくて混乱したので、Javadocを見ながら挙動を確認する。

Returns the fully qualified name of the type represented by this binding if it has one.

このバインディングが持つ型の完全修飾名を返します。

  • For top-level types, the fully qualified name is the simple name of the type preceded by the package name (or unqualified if in a default package) and a ".". Example: "java.lang.String" or "java.util.Collection". Note that the type parameters of a generic type are not included.

    最上位タイプの場合、完全修飾名は、パッケージ名の前にあるタイプの単純名です(デフォルトパッケージの場合は修飾されません)。例: "java.lang.String"または "java.util.Collection"。ジェネリック型の型パラメータは含まれていないことに注意してください。

TypeDeclarationでの取得例(行コメントの右側)。参考までにITypeBinding#getNameの取得結果もあわせて記載(行コメントの左側)。

package sample;
class TopLevelClass1 {} // -> "TopLevelClass1", "sample.TopLevelClass1"
class TopLevelClass2<T> {} // -> "TopLevelClass2", "sample.TopLevelClass2"
  • For members of top-level types, the fully qualified name is the simple name of the type preceded by the fully qualified name of the enclosing type (as computed by this method) and a ".". Example: "java.io.ObjectInputStream.GetField". If the binding is for a member type that corresponds to a particular instance of a generic type arising from a parameterized type reference, the simple name of the type is followed by the fully qualified names of the type arguments (as computed by this method) surrounded by "<>" and separated by ",". Example: "pkg.Outer.Inner<java.lang.String>".

    トップレベル型のメンバの場合、完全修飾名は、(このメソッドで計算された)囲み型の完全修飾名と "。"で始まる型の単純名です。例: "java.io.ObjectInputStream.GetField"。パラメータ化された型参照から生じるジェネリック型の特定のインスタンスに対応するメンバ型のバインディングの場合、その型の単純名の後に(このメソッドによって計算される)型引数の完全修飾名が囲まれます。 "<>"で区切られ、 "、"で区切られます。例: "pkg.Outer.Inner <java.lang.String>"。

TypeDeclarationでの取得例。ネストしたクラスおよびインナークラスにおいてもジェネリクスのパラメタ型が含まれていないことが確認できる。

package sample;
class TopLevelClass {
    static class NestedClass1 {} // -> "NestedClass1", "sample.TopLevelClass.NestedClass1"
    static class NestedClass2<T> {} // -> "NestedClass2", "sample.TopLevelClass.NestedClass2"
    static class NestedClass3<T extends Object> {} // -> "NestedClass3", "sample.TopLevelClass.NestedClass3"
    class InnerClass1{} // -> "InnerClass1", "sample.TopLevelClass.InnerClass1"
    class InnerClass2<T>{} // -> "InnerClass2", "sample.TopLevelClass.InnerClass2"
    class InnerClass3<T extends Object>{} // -> "InnerClass3", "sample.TopLevelClass.InnerClass3"
}

FieldDeclarationおよびMethodDeclaration#getReturnType2での取得例。ジェネリクスの型引数も完全修飾名が付いた状態で取得できるのが確認できる。

package sample;
class TopLevelClass {
    String field1; // -> "String", "java.lang.String"
    List field2; // -> "List", "java.util.List"
    List<?> field3; // -> "List<?>", "java.util.List<?>"
    List<String> field4; // -> "List<String>", "java.util.List<java.lang.String>"
    
    String method1() { return null; } // -> "String", "java.lang.String"
    List method2() { return null; } // -> "List", "java.util.List"
    List<?> method3() { return null; } // -> "List<?>", "java.util.List<?>"
    List<String> method4() { return null; } // -> "List<String>", "java.util.List<java.lang.String>"
}
  • For primitive types, the fully qualified name is the keyword for the primitive type. Example: "int".

    プリミティブ型の場合、完全修飾名はプリミティブ型のキーワードです。例: "int"

  • For the null type, the fully qualified name is the string "null".

    null型の場合、完全修飾名は文字列 "null"です。

書いてあるとおり。わかりやすい。

  • Local types (including anonymous classes) and members of local types do not have a fully qualified name. For these types, and array types thereof, this method returns an empty string.

    ローカル・タイプ(匿名クラスを含む)およびローカル・タイプのメンバーには、完全修飾名はありません。これらの型およびその配列型の場合、このメソッドは空の文字列を返します。

う~ん?配列になってるfield2var2の名前に一貫性なくない?var2が"[]"を返却してるのって、Javadocの説明とあってないのでは?

package sample;
class TopLevelClass {
    void method() {
        class LocalClass {  // -> "LocalClass", ""
            LocalLocalClass field1;  // -> "LocalLocalClass", ""
            LocalLocalClass[] field2; // -> "LocalLocalClass[]", "[]"(?)
            class LocalLocalClass {} // -> "LocalLocalClass", ""
        }
        LocalClass var1;  // -> "LocalClass", ""
        LocalClass[] var2;  // -> ""(?), ""
        new TopLevelClass() {}; // -> "", ""
    }
}
  • For array types whose component type has a fully qualified name, the fully qualified name is the fully qualified name of the component type (as computed by this method) followed by "". Example: "java.lang.String".

    コンポーネント型が完全修飾名を持つ配列型の場合、完全修飾名は([このメソッドで計算される])コンポーネント型の完全修飾名で、その後に""が続きます。例: "java.lang.String "。

上で試したケースで足りてるかな。コンポーネントってなんのこと?

  • For type variables, the fully qualified name is just the name of the type variable (type bounds are not included). Example: "X".

    型変数の場合、完全修飾名は型変数の名前にすぎません(型境界は含まれません)。例: "X"

書いてあるとおり。わかりやすい。

  • For type bindings that correspond to particular instances of a generic type arising from a raw type reference, the fully qualified name is the fully qualified name of the erasure type. Example: "java.util.Collection". Note that the the type parameters are omitted.

    raw type参照から生じるジェネリック型の特定のインスタンスに対応する型バインディングの場合、完全修飾名は消去型の完全修飾名です。例: "java.util.Collection"。型パラメータは省略されていることに注意してください。

これも上で試したケースで足りてるかな。List var;だと"java.util.List"ってことよね?

  • For type bindings that correspond to particular instances of a generic type arising from a parameterized type reference, the fully qualified name is the fully qualified name of the erasure type followed by the fully qualified names of the type arguments surrounded by "<>" and separated by ",". Example: "java.util.Collection<java.lang.String>".

    パラメータ化された型参照から生じるジェネリック型の特定のインスタンスに対応する型バインディングの場合、完全修飾名は、イレージャ型の完全修飾名、続いて "<>"で囲まれた型引数の完全修飾名"、"によって。例: "java.util.Collection <java.lang.String>"。

これも上で試したケースで足りてる。List<String> var;だと"java.util.List<java.lang.String"

  • For wildcard types, the fully qualified name is "?" optionally followed by a single space followed by the keyword "extends" or "super" followed a single space followed by the fully qualified name of the bound (as computed by this method) when present. Example: "? extends java.io.InputStream".

    ワイルドカードタイプの場合、完全修飾名は "?"です。オプションで、単一のスペースの後にキーワード "extends"または "super"が続き、単一のスペースの後に(このメソッドで計算された)完全修飾名が続きます。例: "?extends java.io.InputStream"。

ジェネリクス関連をまとめてお試し。

class TopLevelClass {
    List var1;  // -> "List", "java.lang.List"
    List<Number> var2;  // -> "List<Number>", "java.util.List<java.lang.Number"
    List<?> var3;  // -> "List<?>", "java.util.List<?>"
    List<? extends Number> var4;  // -> "List<? extends Number>", "java.util.List<? extends java.lang.Number>"
    List<? super Number> var5;  // -> "List<? super Number>", "java.util.List<? super java.lang.Number>"
}
  • Capture types do not have a fully qualified name. For these types, and array types thereof, this method returns an empty string.

    キャプチャー・タイプには完全修飾名はありません。これらの型およびその配列型の場合、このメソッドは空の文字列を返します。

キャプチャ・タイプって、下の場合のaddの引数の型のことでいいのかな?確かに空文字列が返却される。

class TopLevelClass {
    void method(List<?> lst) {
        lst.add( null );  // -> "", ""
    }
}

ITypeBinding#getBinaryName

Returns the binary name of this type binding. The binary name of a class is defined in the Java Language Specification 3rd edition, section 13.1. Note that in some cases, the binary name may be unavailable. This may happen, for example, for a local type declared in unreachable code.

この型バインディングバイナリ名を返します。クラスのバイナリ名は、Java言語仕様第3版13.1節で定義されています。 場合によっては、バイナリ名が使用できないことがあります。これは、たとえば、到達不能なコードで宣言されたローカルタイプに対して発生します。

デバッガで見たときに確認できる名前。
基本的にITypeBinding#getQualifiedNameと同じ文字列になるので、異なる文字列になったものだけ下に掲載。

package sample;
class TopLevelClass {
    static class NestedClass1 {} // -> "sample.TopLevelClass$NestedClass1"
    static class NestedClass2<T> {} // -> "sample.TopLevelClass$NestedClass2"
    static class NestedClass3<T extends Object> {} // -> "sample.TopLevelClass$NestedClass3"
    class InnerClass1{} // -> "sample.TopLevelClass$InnerClass1"
    class InnerClass2<T>{} // -> "sample.TopLevelClass$InnerClass2"
    class InnerClass3<T extends Object>{} // -> "sample.TopLevelClass$InnerClass3"
}
class TopLevelClass {
    List<?> field3; // -> "java.util.List"
    List<String> field4; // -> "java.util.List"
    
    List<?> method3() { return null; } // -> "java.util.List"
    List<String> method4() { return null; } // -> "java.util.List"
}
class TopLevelClass {
    void method() {
        class LocalClass {  // -> "sample.TopLevelClass$1LocalClass"
            LocalLocalClass field1;  // -> "sample.TopLevelClass$1LocalClass$LocalLocalClass"
            LocalLocalClass[] field2; // -> "[Lsample.TopLevelClass$1LocalClass$LocalLocalClass"
            class LocalLocalClass {} // -> "sample.TopLevelClass$1LocalClass$LocalLocalClass"
        }
        LocalClass var1;  // -> "sample.TopLevelClass$1LocalClass"
        LocalClass[] var2;  // -> "[Lsample.TopLevelClass$1LocalClass"
        new TopLevelClass() {}; // -> "sample.TopLevelClass$1"
    }
}
class TopLevelClass {
    List var1;  // -> "java.lang.List"
    List<Number> var2;  // -> "java.util.List"
    List<?> var3;  // -> "java.util.List"
    List<? extends Number> var4;  // -> "java.util.List"
    List<? super Number> var5;  // -> "java.util.List"
}

違いとしては、

  • トップレベルのクラスから下の区切り文字が"."の代わりに"$"。
  • 配列の表現。
  • 型パラメータの情報がなくなる。

なんだけど、キャプチャ型の場合、nullが返却された。

class TopLevelClass {
    void method(List<?> lst) {
        lst.add( null );  // -> null
    }
}

一応、ITypeBinding#getErasureを使えば"java.lang.Object"は取得できた。

参考

ジェネリクスの代入互換のカラクリ - プログラマーの脳みそ