日々常々

ふつうのプログラマがあたりまえにしたいこと。

IntelliJ IDEAで複数のクラスをマージする

値オブジェクトなクラスを呼吸をするように作っていると「あれ、これ同じじゃ?」となることがたまにあります。 使用箇所が少なければ手作業でマージしてもいいのですが、多くの箇所で使っているクラスだと大変。 文字列の一括置換でやれなくはないんですが、正規表現力が試されますし、誤置換が怖いです。import文とかあるし、同じ名前を部分的に使っているクラスやメソッドを置換してしまうと困ったことになります。(こないだ事故った。)

そういう時、Inline Super Class(親クラスのインライン化)が使えます。

f:id:irof:20211021121346p:plain

ちょっと長いですが、全体を貼ります。試してみたいかたはどうぞ。

class OrderService {

    OrderNumber 新規注文(OrderDetail orderDetail) {
        // ... 何らかの処理
        OrderNumber orderNumber = new OrderNumber("dummy");

        return orderNumber;
    }

    Order 注文詳細(ReceiptNumber receiptNumber) {
        if (!receiptNumber.isValid()) {
            throw new IllegalArgumentException();
        }

        // ... 何らかの処理
        Order order = new Order(receiptNumber, new OrderDetail());

        return order;
    }

    BillingNumber 請求(OrderNumber orderNumber) {
        // ... 何らかの処理
        BillingNumber billingNumber = orderNumber.toBillingNumber();

        return billingNumber;
    }
}

/** 受付番号 */
class ReceiptNumber {
    String value;

    ReceiptNumber(String value) {
        this.value = value;
    }

    String value() {
        return value;
    }

    boolean isValid() {
        return !value.isBlank();
    }
}

/** 注文番号 */
class OrderNumber {
    String value;

    OrderNumber(String value) {
        this.value = value;
    }

    String value() {
        return value;
    }

    BillingNumber toBillingNumber() {
        // 何らかのルールに従って生成する
        return new BillingNumber();
    }
}

/** 注文 */
record Order(ReceiptNumber receiptNumber, OrderDetail orderDetail) {
}

/** 注文内容 */
class OrderDetail {
}

/** 請求番号 */
class BillingNumber {
}

これの「ReceiptNumber と OrderNumber が同じでよさそう」と気づいたとしましょう。(登場場面もメソッドも違うから違うでよさそうなんだけど、今回はクラスのマージの紹介なので目を瞑ってください。)

実行ステップ

  1. テストが通るのを確認する
  2. 片方でもう片方を継承する
  3. テストが通るのを確認する
  4. Inline Super Class... を実行する
  5. テストが通るのを確認する
  6. 気に入らないメソッドの並び順や変数名を直す
  7. テストが通るのを確認する
  8. コミットする

操作自体は1分かかりません。ファイル数によってはインライン化にそこそこかかるかもしれません。テストの実行時間は努力してください。

初期状態

f:id:irof:20211021143405p:plain

片方でもう片方を継承する

マージなんでどっちがどっちでも構いません。 寄せたい方があればそちらを子クラスにすればいいですが、どちらにせよマージ後に名前変更すればいいだけの話です。 extends して、コンストラクタで super 呼ぶようにして、フィールドを消す。

-class OrderNumber {
+class OrderNumber extends ReceiptNumber {

-    String value;
 
     OrderNumber(String value) {
-        this.value = value;
+        super(value);
     }

-    String value() {
-        return value;
-    }

この作業は手作業になります。なのでテスト必須。

フィールドが private だと使用しているメソッドがコンパイルエラーになりますが、一時的にprotectedにするとか、アクセサメソッドを介するなどで対応できます。

f:id:irof:20211021143447p:plain

Inline Super Class... を実行する

extends の後ろ(今回は ReceiptNumber)にカーソルを持って行って Inline Super Class... を実行します。 私はRefactor ThisにHotkeyを入れてるので冒頭の画像のようになりますが、コンテキストメニューからならこの辺です。(⌥⌘Nなんて覚えてません。なぜか指は勝手に使うけど。)

f:id:irof:20211021123405p:plain

選択ダイアログでは、クラスをマージしたいので親クラスに消えてもらう方を選びます。keepじゃなくremoveの方。

f:id:irof:20211021123201p:plain

結果、以下のような OrderNumber になります。

class OrderNumber {

    String value;

    OrderNumber(String value) {
        this.value = value;
    }

    BillingNumber toBillingNumber() {
        // 何らかのルールに従って生成する
        return new BillingNumber();
    }

    String value() {
        return value;
    }

    boolean isValid() {
        return !value.isBlank();
    }
}

ReceiptNumber にあったメンバが持ってこられてます。privateメソッドやstaticメソッドがあっても持ってきてくれます。

これに加え、ReceiptNumberを使用しているコードもすべてOrderNumberに置き換えられます。型の使用箇所で処理してくれるので、文字列置換とは安心感が段違いです。

super呼び出しがインライン化されるため、子クラスのコンストラクタはsuper呼び出し以外をしないこと。他の処理をいれていると挙動が変わることになります。また、同じメソッドが定義されていた場合(今回だと value() を削除しなかった場合)、警告ダイアログが出てきます。どうとでもなるので、いい感じにしましょう。

f:id:irof:20211021143719p:plain

一気に変わってますが、個別には以下になります。なおメンバのpush downはリファクタリングメニューから個別に実行できます。

f:id:irof:20211021144728p:plain

やりたいのは「使用クラスの変更」なんですが、これを安全に行うのが本稿の内容になります。それなりの規模で根底のドメインを変えると、数百ファイル同時に編集されたりします。手作業では面倒で不安。

これ書いた動機

型とIDEの機能を使えば素早く安定してコードをこねくり回せます。その一例。

値オブジェクトを作ると「クラスがたくさんになり過ぎて困るのでは」みたいなのがありますが、その困ることのひとつに「違うと思ってたけど実は同じだった」というのがあります。この時に時間と精神力を消費して必ず誤る手作業(酷い表現だ)や、祈りの文字列置換を行うのでは気が重いため、作ろうとした時に「違うように見えるけど本当は同じなのでは?」と穿ってみる必要が出てきます。

一塊のものを分けるより、分かれているものをくっつけるほうがよほど簡単です。

もちろん例外はあるけど。一塊のものを分離するのはなかなか機械的にはできません。いくつかできるテクニックはあります(それはまたそのうち……と言って書いてないのどんだけあるだろう)が、いつもできるわけではありません。 一方、分かれているものをくっつけるのはほぼほぼ機械的にできますし、機械的にできるものは今回のようにIDEで一発だったりします。

てことで、また知識が十分でない時は「なんか違うものっぽいなぁ」と感じたらじゃんじゃんクラスを作っていくといいと思います。同じかどうかを判断するのは後からでもできる。「同じと扱っていたものを後から分ける」より、「違うと思っていたものが実は同じだったからくっつける」の方が楽だと思います。

f:id:irof:20211021132551p:plain

ツイートのリンク。まぁなんだ、要はバランスですよ、はい。

おまけ: 簡単で不安定な別解

少々乱暴ですが、以下の方法もあります。

  1. 2つのクラスの内容を一致させる。(コピペ。)
  2. 片方のクラスをファイルごと削除する。(使用箇所があるとか警告されますが無視して実行。コンパイルエラーになる。)
  3. 残った方の名前を削除した方の名前に変換する。(リファクタリング機能を使用する。)

コンパイルエラー状態でのリファクタリング機能は精度が落ちるので推奨はしません。最近はだいぶいい感じに動きますが、安心して使えるとは言い難いです。 この手順では、2番目でファイルを削除しなかったら「ファイル名が一致する」とかで怒られます。

さらに派生で削除する代わりに参照先モジュールに持っていくことでコンパイルエラーを回避するとかもありますが、マルチモジュール構成でなければできませんし、大掛かりになるので他の事故要因も出てきます。どうしても必要な時にって感じですね。

蛇足: recordのとき

recordを使うと継承が使用できないので、素直にはできなくなります。現段階では一旦recordをclassに書き換えて上記の手順を実施、その後recordに戻す……かなぁと思ってます。IntelliJ IDEAでclassとrecordの変換は相互にできるので、これも別に手間じゃないかなって。

あわせてよみたい?

irof.hateblo.jp

ライブコーディング中に使ったIntelliJ IDEAの機能を適当に紹介してます。今回書いたのは意識して使う機能ですが、リンク先で挙げてるのはほんと無意識に使うレベルのもの。

IntelliJ IDEAのGradleが使うJDKバージョン設定箇所

IntelliJ IDEAでGradleのタスク実行しようとしたら意図したバージョンで動作しなくて文句言われる場合の設定。複数のJDKを設定していなかったら起こらないと思います。

あ、Mavenも大体一緒です。

設定する場所

Build, Execution, DevelopmentBuild ToolsGradle にある Gradle JVM でGradleが使うJVMを選べます。

f:id:irof:20211019140803p:plain

このウィンドウはMacだとPreferences、WindowsだとSettingsです。なぜ違うかは知らない。

プルダウンを開くと使えるのがずらっと(複数のJDKを設定していればですが)。

f:id:irof:20211019141032p:plain

設定次第で色々ありますが Project SDK にしておくのがおすすめです。コード書くときとビルド時のJDKを変えたいなら選べるんですが、そいうケースはそんなに多くないでしょう。

JDK自体の設定は以下を参照。

irof.hateblo.jp

設定のツリー辿るの面倒

項目を覚えていればツリーを辿れますが、そんなことに記憶容量は割きたくありません。

IntelliJ IDEAさんは設定画面の検索窓にやりたい設定の項目をタイピングすればだいたい連れて行ってくれます。入力したワードを黄色の枠で強調してくれる。

f:id:irof:20211019142305p:plain

設定探すとき超便利。

とは言え今回は「Gradleの設定」ってわかっていて、絞り込みとかタイピングも面倒。一番すぐ出せるのはGradleのToolWindowのレンチボタン(🔧)から Gradle Settings... です。

f:id:irof:20211019142130p:plain

これクリックしたら最初の画面を一発で開いてくれる。

雑記

IntelliJ IDEA自体を動かすJDK(普段は存在を気にしないかもですが、メモリやfile.encodingを設定する時に「あーJDKで動いてるんだー」と気付きます。)、コード書くときに使うJDK(ProjectウィンドウのExternal Librariesに表示されます。一番目に入るし、意識するものかな。)、mainメソッドやJUnit実行時に使うJDK(▶️ボタンで実行する時の設定。通常はProject SDKですが他のも選べます。)Gradleタスクを実行する時に使うJDK(今回設定したもの)などがあり、それぞれ独立して設定、制御できます。

全部同じのを使えばいいと思われるかもしれませんが、別プロセスで動作するものですし、すなわちバージョンやメモリ量などJVMのオプションを変えられるということでもあります。これらが別に存在するのはある程度知識があれば認識できるようになりますし、別に存在するなら設定できるはず、設定できるなら設定箇所があるはず、と想像できるかと思います。

これらはいちいち気にしていたら脳のメモリが足りなくなるので、普段の開発では意識の外に置いておくのがいいことではあります。

とは言えトラブル時(よくあるのはエンコーディング違いによる文字化けやOOMEなど)にどれを設定すればいいかに当たりをつけて適切に処置するには必須の知識ですし、今後はおそらく避けては通れない「Javaのバージョンアップ」や「Javaのバージョン違いを検証する」際にどこをいじればいいかもわかってきます。IDEや開発環境はこの辺の複雑さをある程度隠すのが仕事とは言え、ちょっと意識を向けてあげてもいいのかな、と思います。

IDEJDKの話は以下のセッションでも触れています。

irof.hateblo.jp

3種類とか言ってますが「作る時に使うもの」はコード書く時に使うものと実行時に使うものに分けれますね。

Recordを使ってく上で気にしとくこと

Java16で導入されたRecordですが、Java17リリースによりこれから一気に使われていくことかと思います。

Java17雑感で「データクラスを新しく作るならRecordを使ってみる」とか書いたんで「よしRecordを使おう!……ところで何気にしなきゃなんだっけ?」な私向けに、現時点で「これくらい知っとくといいんじゃないかな」ってことを書いておきます。だらだら書いたんで順番とか内容の濃淡がひどいかもしれない。

ちゃんとした知識

とか。

あと、うらがみさんとこの「Javaのレコードについてメモ」

・・・・あれ、気になって調べたことが全部書いてある。まいっか。

書き方

record EmptyRecord() { }

record 名前 (レコードコンポーネントのリスト) {ボディー} ですね。ちゃんとしたのは言語仕様の方を。 空っぽでも () とか {} とかは必要。空のRecordの存在意義は知りませんが。

素直に書くとこんなの。

record Point(int x, int y) { }

シンプルで良い。けど { } がちょっとダサい。なくしたらセミコロン書かなきゃいけなさそうだし、仕方ないか(そんな理由じゃないはず)。

  • record って名前はパッケージ名とか変数名とかには使える。
    • クラス名には使えない(Java14以降)
      • f:id:irof:20210924003433p:plain
      • 小文字クラス名とかやらないだろから問題ないでしょ。
  • int x, int y とかを「レコードコンポーネント」と呼ぶらしい。
    • 呼び方は個人的に一番重要なとこだったりする。
    • record宣言で書くのは「レコードコンポーネント」であり、フィールドでもプロパティでもない。
  • レコードコンポーネントに対応したコンストラクタ(CanonicalConstructor)とフィールド、アクセサメソッドが自動的に作られる。
    • 型と名前はレコードコンポーネントと同じ。
    • 自分で定義したら作られない。デフォルトコンストラクタとかと同じ感じ。
  • アクセサメソッドは自分でも定義できる。
    • レコードコンポーネントと同じ型と名前。
    • 引数なし、throwsなしである必要がある。
    • 後述のアノテーションの問題もあるし、自分では作らないほうがいいかも。
  • フィールドは static だけ。インスタンスフィールドを書いたらコンパイルエラー。
  • 他のメソッドは好きに作れる。
  • コンストラクタも好きに作れる。
    • CanonicalConstructorも作れる。型と名前をレコードコンポーネントに合わせる必要がある。
    • CanonicalConstructorを呼ぶようにしなきゃいけないってくらい。 super() は呼べなかった。
  • あと native メソッドは書けない。書かないからいいけど。

CanonicalConstructorとかコンストラクタがちょっと特別なくらいで、他は難しい構文でもないと思います。

お約束

R copy = new R(r.c1(), r.c2(), ..., r.cn()); とやったとき r.equals(copy)true にならなきゃいけない。ってJavadocに書いてる。 アクセサメソッドやCanonicalConstructorを自分で実装せず生成されるものに任せればいい話。

こんな変なことをするなってことです。(コンパイルは通っちゃう)

record NgRecord(int hoge, int fuga) {
    NgRecord(int hoge, int fuga) {
        this.fuga = hoge;
        this.hoge = fuga;
    }
}

アクセサメソッドで計算してもいいけど、その計算結果をコンストラクタで受けたらアクセサメソッドで同じ値を返すようになってなきゃいけない。

これは「お約束」であって崩せるわけだけど、崩しちゃいけない。equals/hashcodeのように実装するなら意識しとけよってものです。「 equalsが同じになるならhashcodeが同じにならないといけない」ってやつね。 equals/hashcodeよりかは何もしなければ守られるものなので、多少ましかな。似たり寄ったりではあるけど。

今あるのをRecordにする?

無理にする必要はないと思うけど……

f:id:irof:20210923234111p:plain

完全コンストラクタを使って全フィールドが final だとIntelliJ IDEAさんは「Recordにしようぜ!」と言ってきます(Weak Warning)。で、サクッと置き換えてくれます。

さすがにアクセサメソッドを置き換えてはくれないので、「recordのアクセサメソッドを使うように置き換えてインライン化」とかはしなきゃな感じ。

自分でツール作ろうと思ったけど、IntelliJ IDEAさんマジIntelliJ IDEAさんだわ。

アノテーションとリフレクション

アプリケーションプログラミングレベルだと特に問題なく使える確信はあります。おそらくイミュータブルなデータコンテナを扱えるかが最大のハードル。 そんなのはRecordに限った話ではないので、気になるのは足回りです。リフレクションとかバイトコードとかです。 バイトコードはどうせJIG対応する時にしっかりみなきゃなので、ここではリフレクションとか、フレームワークやライブラリ目線で。。。

  • private final でも setAccessible(true) したら上書きできるんだけど、recordで作ったクラスだとできない。
    • リフレクションAPIの中で判定入ってる。
    • リフレクションで値をセットしているライブラリはそのままじゃ置き換えられない と言う意味。
      • なんとかしてCanonicalConstructorを使うように変更する必要がある。
      • Java16以降で対応なら次のgetRecordComponent使えるけど、15以下も同じコードで扱うならそれなりに面倒。
  • Class#getRecordComponents() とか java.lang.reflect.RecordComponent とかが追加されてる。
  • Class#isRecord() はRecordかどうかの判定メソッド。親クラスが java.lang.Record かとか見てる。

書けるアノテーション

@Target(ElementType.FIELD)
public @interface FieldAnnotation { }

と言う感じのアノテーションを全ElementTypeの文作った上でこんなコードを書いて、リフレクションで取ってみるなど。

@TypeAnnotation
@TypeUseAnnotation
record AnnotatedRecord(
        @FieldAnnotation
        @MethodAnnotation
        @ParameterAnnotation
        @RecordComponentAnnotation
        @TypeUseAnnotation
        int a,
        int b
) {

    @ConstructorAnnotation
    @TypeUseAnnotation
    AnnotatedRecord {
    }

    @MethodAnnotation
    @TypeUseAnnotation
    public int b() {
        return b;
    }
}
  • @Target(ElementType.FIELD)@Target(ElementType.METHOD) もレコードコンポーネントのとこに書ける。
  • これはそれぞれ生成されるフィールドやメソッドに引き継がれる。
    • clz.getDeclaredFields()*.getAnnotations()a@FieldAnnotation が取れる。他のアノテーションはない。
    • clz.getDeclaredMethods()*.getAnnotations()a()@MethodAnnotation が取れる。他のアノテーションはない。
    • なお アクセサメソッドを自分で定義したらアノテーションは引き継がれない。自分でメソッド書くなら自分でアノテーションも書けということ。
  • ちなみに clz.getRecordComponents()*.getAnnotations() では @RecordComponentAnnotation しか取れない。
    • 他の4つは行方不明。
  • コンストラクタのアノテーションを書きたかったらCompact Canonical Constructorsを作る。
    • record@ConstructorAnnotation 書いたらいけるかな?とか思ったけどそんなことなかった。

CanonicalConstructorの見分けかた

java.lang.reflect.Constructor からは特になさそう。javap で見えるコンストラクタも特別なフラグとか持ってない。

Class#getRecordComponents() で型を特定してコンストラクタ取るのが確実かな。

var parameterTypes = Arrays.stream(clz.getRecordComponents())
        .map(RecordComponent::getType)
        .toArray(Class[]::new);
var constructor = clz.getDeclaredConstructor(parameterTypes);

とはいえこれも生成されたのと手で書いたのの区別はつかないわけだけど。 まぁ「お約束」もあるし、名前とか一致してたらいいでしょ。

2021-10-22T23:16 追記 Javadocにまんま書いてた

Class.html#getRecordComponents()

static <T extends Record> Constructor<T> getCanonicalConstructor(Class<T> cls)
     throws NoSuchMethodException {
   Class<?>[] paramTypes =
     Arrays.stream(cls.getRecordComponents())
           .map(RecordComponent::getType)
           .toArray(Class<?>[]::new);
   return cls.getDeclaredConstructor(paramTypes);
 }

javadocにメソッド書かず Class にメソッド生やしておいてください(真顔

リフレクションでのインスタンスコピー

CanonicalConstructorの識別がなぜ必要かと言うと、リフレクションで特定のレコードコンポーネントだけ値を変えたインスタンスを作りたいとかなった場合。

  • CanonicalConstructorを見つける(上記)
  • レコードコンポーネントの値を取得
    • 前述の「お約束」があるので、フィールドからでもアクセサメソッドからでもどっちからでも良い。
    • けど private 無理矢理見るより、素直にアクセサメソッドかな。
  • 必要な値を準備
  • CanonicalConstructorでインスタンス生成

って感じかしら。たぶんBeanUtilsかなんかで実装してるのもうあるだろし、そのうち見とこう。。

2021-09-24T10:30追記

やりたくはないんや……

「各Recordに手を入れず」「一律」となると厳しい感。自分でならwith作るんだけど、目の届かない領域のある開発体制組まれると、どこかで必要になってくる。 そいやLombokはrecordにいい感じのwithを生やしてくれたりするんだろか。

リフレクション関連で思ったこと

フィールドやメソッドにレコードコンポーネントに書いたアノテーションを持ってってくれるのが良い。 これなら既存のアノテーションベースのライブラリもそれなりに素直に対応できるんじゃなかろーか。

Field#set(Object, Object)accessible = true でも拒否されたのはびっくりしました。 recordじゃないfinal フィールドも蹴るようにしたいけど、そうしたらいろんなライブラリが動かなくなりそうねぇ。

Local record classes

JEPに書いてて面白いと思ったのがLocal record classes。コードを転載。

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

ローカルクラスなんて使わないよとか昔から思ってたわけですが、これはありな気がしなくもなくもなく。

ちなみにRecordは型パラメーターを持てるので、こんなTupleは書ける。

record Tuple<A, B>(A a, B b) { }

そして「こんなの作らずにちゃんと名前つけようよ!!」って書いてる。

A central aspect of Java's design philosophy is that names matter.

御意。。

ところでjava.lang.Recordクラス

コード見ての通りequalshashcodetoStringabstract でOverrideされているだけのクラスです。あとコンストラクタが protected だったりします。 extendsimplementsはありません。 Serializable も実装していないんだなーそりゃそうかーと見てたら、「実装したら serializable recordとして特別扱いする。具体的にはシリアライズにreadObjectやwriteObjectは使わない」とか書いてるので、もしシリアライズするなら注意がいるかもです。うらがみさんが試してるのでそっち参照。

ところでrecordで生成されるクラス

enumextends java.lang.Enum になるように、 recordextends java.lang.Recordになります。 これは Recordに独自の基底クラスを作れない と言う意味になります。あと final にもなるので record で作ったクラスを継承もできません。

Recordの主な用途であるDTOですが、共通項目を基底クラスで宣言して実装継承をしているような用途だと、そのままは代替できません。インタフェースは実装できるのでそちらで共通項目を強制するようなことは可能ですが、レコードコンポーネントは記述する必要はあります。

DTOの実装継承で省力化してたら辛いことありそうですね。

javapのMethodParameters

コンパイルする時に -parameters をつけるとメソッドの仮引数名がクラスファイルに残って、リフレクションとかで使えるようになります。( spring-boot-starter-parentを使ったら有効 になってたりします。)

つけなかったらコンパイル時に消えてリフレクションや javap とかで仮引数名が見えなくなるんですけど、CanonicalConstructorは -parameters をつけなくても MethodParameters が入ってたりします。シリアライズの時に使ってたりするのかな。

思ったこと

素直にDTOの代わりに使うのは思ったよりハードルありそうだなー。