ローカルクラスとしてrecordを使う
こちらで書いた「Local record classes」を使う時の注意。
触ってると案外使う機会が出てきそうな感触があったので、気をつけるところを書いておきます。
ローカルクラス
そもそもローカルクラスってあまり使わないものなんで存在も認識されてなかったりします。メソッド内に書くクラスなんですが……
class Hoge { void fuga() { class Piyo { } // これ } }
書いたら「こんなとこにクラス書けるの?」な反応もしばしば。
用途は特殊だし代替手段もあるので無理に使う必要性はないです。知らなくても生きてけるし、知ってたからと言って 知識マウントに使うにも微妙で 嬉しいことってそんなにない。
サンプルコード書く時にちょっと楽なくらいです。
ローカルレコードクラスの使い所
と言う感触だったんですが、これがStream APIに加えてrecord
で若干変わりました。recordのネスト、ついでに他も でコンストラクタ全部出力するコードでも使ってます。
- 名前付きのPair(やタプル)として作成できる
- classだからメソッドも持たせられる
あたりが素敵です。
ローカルクラスでもできたんですけど、やろうとするとフィールドとコンストラクタを作らなきゃなので、メソッド内に書くには大袈裟でした。なのでまともにクラスを作るか、いろんなことに目を瞑って Object[]
を使ってきました。record
だとこの辺要らないので、メソッド内でもノイズがない手触りが良いです。
クラスを作りたくない理由は、そこまで広いコンテキストで使いたくないから。パッケージプライベートでも広いし、クラスにネストしたprivateクラスでもまだ広いかなって思うことがある。名前汚染みたいなことも気になっちゃう。 メソッドのローカルに閉じられると名前もメソッド内に特化したもので良くなるし、多少雑にやっても他を汚さないから取り扱い易いんです。
ローカルクラスとの違い
record
はクラスではあるんですが、class
とイコールではないです。
特にrecordのネスト、ついでに他も で書いたstatic
の扱い。
これがローカルの場合も入ってきます。
void method() { record LocalRecord() { } class LocalClass { } }
コンパイルは通ります。同じように見えますが、LocalRecord
は static
で、LocalClass
は違います。
これが何を意味するかと言うと、「それぞれから外側にアクセスできるか」です。
void method() { var object = new Object(); record LocalRecord() { void method() { object.toString(); // コンパイルエラー } } class LocalClass { void method() { object.toString(); } } }
これが本稿の主題です。ローカルクラスと同じ場所でローカルレコードクラスを使った場合、ローカルクラスと同等には使用できません。
「ローカルクラス」としてではなく「メソッド内に閉じたrecord
」と認識する必要があります。
当然、メソッドの一時変数だけでなく、外側のクラスのインスタンスメンバにもrecord
の内側からはアクセスできません。
record
で使用するものはレコードコンポーネントとして明示的に渡してあげる必要があります。
「ローカルレコードクラスでメソッドを使わなければ問題ない」ではありますが、メソッドを持てるのはただのデータ構造に対するせっかくのメリット。 取りまとめて名前までつけた情報の組み合わせには固有のメソッドがある。はず。たぶん。
もうちょい詳しく
エラーメッセージは Non-static variable 'object' cannot be referenced from a static context
です。 static
なんて書いてないけど、record
は static context
ということ。
この辺は javap
で出てきたりします。以下は javap -v
の抜粋。
SourceFile: "Sample.java" NestMembers: Sample$1LocalClass Sample$1LocalRecord InnerClasses: #20= #15; // LocalClass=class Sample$1LocalClass static final #21= #17; // LocalRecord=class Sample$1LocalRecord
同じように書いてるのに LocalRecord
のほうは static final
がついてます。
明示したい
「暗黙に任せずに明示したい」と思うかもしれませんが、以下はいずれもコンパイルエラーです。
void method() { static record LocalRecord() { } static class LocalClass { } }
ローカルクラスにstatic
はつけれないので、 static record
は書けません。「デフォルトがどっち」とかではなく「強制的にこっち」なので明示云々じゃないんですね。
たぶんrecord
はレコードコンポーネント以外を持たないのを優先したんでしょう。外側にアクセスするためには外側からインスタンスを受け取るようにコンストラクタを変えなきゃですし。
record
としては正しいと思うけど、インナークラスとかに対する半端な知識があると混乱ポイントかなと思います。
蛇足: Stream APIのLambda内レコード
これってどうなんだ?と思いつつ。
Stream.of("abc", "defg") .map(s -> { record Pair(String value, int length) {} return new Pair(s, s.length()); }) .forEach(System.out::println);
StreamAPIの途中でrecord
を宣言もできます。static
なんで後続処理にインスタンス渡してもインナークラスインスタンスを渡した時のようにメモリリークの原因にもならないはず。
後続処理(ここではforEach
のSystem.out::println
)がPair
をコンパイルレベルで必要としなければ普通にPair
として動作します。
Pair
を以下のように書けば、当然出力も変わります。
record Pair(String value, int length) { @Override public String toString() { return "きゃー"; } }
もちろんですが、Pair
独自のメソッドは後続ではコンパイルできません。
Stream.of("abc", "defg") .map(s -> { record Pair(String value, int length) {} return new Pair(s, s.length()); }) .map(Pair::value) // コンパイルエラー .forEach(System.out::println);
record
定義を前に出せば当然いけます。
record Pair(String value, int length) {} Stream.of("abc", "defg") .map(s -> { return new Pair(s, s.length()); }) .map(Pair::value) .forEach(System.out::println);
けどこれは面白くない・・・あ、そうだ。java.util.function
のインタフェースを使おう。
Stream.of("abc", "defg") .map(s -> { record Pair(String value, int length) implements Supplier<String> { @Override public String get() { return "さぷらい"; } } return new Pair(s, s.length()); }) .map(Supplier::get) .forEach(System.out::println);
いけた。よし!
toString
が呼べたからObject
とかの識別はできてたんだろけど、前段のPair
がSupplier
ってのをmap
の時にわかるんですね。すごいなぁ。
……できるのは分かったけど、どう見てもわかりづらいし、お蔵入りかなー。
おまけ: ローカルenum
書けたのか……いやこれこそ書いてどうすんだ?
void method() { enum MyLocalEnum { HOO } }
説明は省きますが、これもローカルレコードクラスと同じくstatic-contextになります。 「書いてどうすんだ?」とか書いたけど、ちょっと思いついてしまった。いつか試す。
recordのネスト、ついでに他も
record Outer(int a) { record Nest(int b) { } }
こんなことしたらどうなるんだろうと気になりまして。
record
が単にクラスのシンタックスシュガーであれば、Nest
はインナークラスになるのでOuter
のインスタンス変数、ここでは a
にアクセスできるんですが、単なるネストクラスであればアクセスできません。
結論としてはNest
からa
にはアクセスできず、Nest
はOuter
のインスタンスを持たない、static
が省略されていると言うことになります。
ネストだのインナーだのは 匿名クラスとかローカルクラスとか を参照ください。
確認はNest
の方にメソッド生やしてOuter
のインスタンスメンバにアクセスしてみようとすればいいんですが、他にもNest
をjavap
かリフレクションでコンストラクタやフィールドを見て、Outer
があるかどうかを見ればわかります。
そいや他でもどうなるんだろ?とこんなの書いて確認。
class OuterClass { class NestClass { } interface NestInterface { } enum NestEnum { } record NestRecord() { } static class StaticNestClass { } static interface StaticNestInterface { } static record StaticNestRecord() { } static enum StaticNestEnum { } } enum OuterEnum { CONSTANT; class NestClass { } interface NestInterface { } enum NestEnum { } record NestRecord() { } static class StaticNestClass { } static interface StaticNestInterface { } static record StaticNestRecord() { } static enum StaticNestEnum { } } public interface OuterInterface { class NestClass { } interface NestInterface { } enum NestEnum { } record NestRecord() { } static class StaticNestClass { } static interface StaticNestInterface { } static record StaticNestRecord() { } static enum StaticNestEnum { } } record OuterRecord(String s) { class NestClass { } interface NestInterface { } enum NestEnum { } record NestRecord() { } static class StaticNestClass { } static interface StaticNestInterface { } static record StaticNestRecord() { } static enum StaticNestEnum { } }
で、全部のコンストラクタ舐めて出力。インタフェースはコンストラクタ無いから出ないけどね。
String[] targets = {"Class", "Interface", "Enum", "Record"}; record OuterAndNest(String outer, String nest) { Class<?> nestClass() { try { return Class.forName(nest); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } } Arrays.stream(targets) .map(outer -> "Outer" + outer) .peek(System.out::println) .flatMap(outer -> Stream.concat( Arrays.stream(targets).map(nest -> new OuterAndNest(outer, outer + "$Nest" + nest)), Arrays.stream(targets).map(nest -> new OuterAndNest(outer, outer + "$StaticNest" + nest)) )) .forEach(e -> { Class<?> nestClass = e.nestClass(); System.out.println(" " + nestClass); Arrays.stream(nestClass.getDeclaredConstructors()) .forEach(constructor -> { System.out.println(" " + constructor); }); });
結果。
OuterClass class OuterClass$NestClass OuterClass$NestClass(OuterClass) interface OuterClass$NestInterface class OuterClass$NestEnum private OuterClass$NestEnum(java.lang.String,int) class OuterClass$NestRecord OuterClass$NestRecord() class OuterClass$StaticNestClass OuterClass$StaticNestClass() interface OuterClass$StaticNestInterface class OuterClass$StaticNestEnum private OuterClass$StaticNestEnum(java.lang.String,int) class OuterClass$StaticNestRecord OuterClass$StaticNestRecord() OuterInterface class OuterInterface$NestClass public OuterInterface$NestClass() interface OuterInterface$NestInterface class OuterInterface$NestEnum private OuterInterface$NestEnum(java.lang.String,int) class OuterInterface$NestRecord public OuterInterface$NestRecord() class OuterInterface$StaticNestClass public OuterInterface$StaticNestClass() interface OuterInterface$StaticNestInterface class OuterInterface$StaticNestEnum private OuterInterface$StaticNestEnum(java.lang.String,int) class OuterInterface$StaticNestRecord public OuterInterface$StaticNestRecord() OuterEnum class OuterEnum$NestClass OuterEnum$NestClass(OuterEnum) interface OuterEnum$NestInterface class OuterEnum$NestEnum private OuterEnum$NestEnum(java.lang.String,int) class OuterEnum$NestRecord OuterEnum$NestRecord() class OuterEnum$StaticNestClass OuterEnum$StaticNestClass() interface OuterEnum$StaticNestInterface class OuterEnum$StaticNestEnum private OuterEnum$StaticNestEnum(java.lang.String,int) class OuterEnum$StaticNestRecord OuterEnum$StaticNestRecord() OuterRecord class OuterRecord$NestClass OuterRecord$NestClass(OuterRecord) interface OuterRecord$NestInterface class OuterRecord$NestEnum private OuterRecord$NestEnum(java.lang.String,int) class OuterRecord$NestRecord OuterRecord$NestRecord() class OuterRecord$StaticNestClass OuterRecord$StaticNestClass() interface OuterRecord$StaticNestInterface class OuterRecord$StaticNestEnum private OuterRecord$StaticNestEnum(java.lang.String,int) class OuterRecord$StaticNestRecord OuterRecord$StaticNestRecord()
わかりづらいから flatMap
の後で適当に filter
しとく。
.filter(e -> Arrays.stream(e.nestClass().getDeclaredConstructors()) .anyMatch(constructor -> Arrays.stream(constructor.getParameterTypes()) .anyMatch(parameterType -> parameterType.getName().equals(e.outer()))))
結果。
OuterClass class OuterClass$NestClass OuterClass$NestClass(OuterClass) OuterInterface OuterEnum class OuterEnum$NestClass OuterEnum$NestClass(OuterEnum) OuterRecord class OuterRecord$NestClass OuterRecord$NestClass(OuterRecord)
エンクロージングクラスのインスタンスを受けるようにコンストラクタが弄られるのは class
の時だけ、と言うことで。
きっかけ
- IntelliJ IDEA 2021.2.3
IntelliJ IDEAさんが警告してくれなかったのよね。StaticNestInterface
やStaticNestEnum
はstatic
要らないって灰色で、NestClass
はstatic
にできるよって黄色で。
「あってもなくても意味ないのなら灰色になるんじゃないかなぁ、出ないってことは違いあるのかなぁ」って思ったのだけど、黄色にもなってないので。
ああ、一律あってもなくても意味ないのに灰色にしてるわけじゃなく、「enum
の不要な修飾子」みたいな括りなのか。なるほど。
なおinterface
にネストした場合は全部のstaticが灰色になってくれます。これはinterface
のメンバに対する不要な修飾子で引っかかってる模様。
満足。(言語仕様とか見に行く気力は残ってない)
IntelliJ IDEAさんは IDEA-266665 でfix済だった。 2021.3
になったら期待してる通りになりそう。
IntelliJ IDEAで複数のクラスをマージする
- IntelliJ IDEA 2021.2.3
値オブジェクトなクラスを呼吸をするように作っていると「あれ、これ同じじゃ?」となることがたまにあります。 使用箇所が少なければ手作業でマージしてもいいのですが、多くの箇所で使っているクラスだと大変。 文字列の一括置換でやれなくはないんですが、正規表現力が試されますし、誤置換が怖いです。import文とかあるし、同じ名前を部分的に使っているクラスやメソッドを置換してしまうと困ったことになります。(こないだ事故った。)
そういう時、Inline Super Class(親クラスのインライン化)が使えます。
例
ちょっと長いですが、全体を貼ります。試してみたいかたはどうぞ。
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 が同じでよさそう」と気づいたとしましょう。(登場場面もメソッドも違うから違うでよさそうなんだけど、今回はクラスのマージの紹介なので目を瞑ってください。)
実行ステップ
- テストが通るのを確認する
- 片方でもう片方を継承する
- テストが通るのを確認する
Inline Super Class...
を実行する- テストが通るのを確認する
- 気に入らないメソッドの並び順や変数名を直す
- テストが通るのを確認する
- コミットする
操作自体は1分かかりません。ファイル数によってはインライン化にそこそこかかるかもしれません。テストの実行時間は努力してください。
初期状態
片方でもう片方を継承する
マージなんでどっちがどっちでも構いません。
寄せたい方があればそちらを子クラスにすればいいですが、どちらにせよマージ後に名前変更すればいいだけの話です。
extends
して、コンストラクタで super
呼ぶようにして、フィールドを消す。
-class OrderNumber { +class OrderNumber extends ReceiptNumber { - String value; OrderNumber(String value) { - this.value = value; + super(value); } - String value() { - return value; - }
この作業は手作業になります。なのでテスト必須。
フィールドが private
だと使用しているメソッドがコンパイルエラーになりますが、一時的にprotected
にするとか、アクセサメソッドを介するなどで対応できます。
Inline Super Class...
を実行する
extends
の後ろ(今回は ReceiptNumber
)にカーソルを持って行って Inline Super Class...
を実行します。
私はRefactor ThisにHotkeyを入れてるので冒頭の画像のようになりますが、コンテキストメニューからならこの辺です。(⌥⌘N
なんて覚えてません。なぜか指は勝手に使うけど。)
選択ダイアログでは、クラスをマージしたいので親クラスに消えてもらう方を選びます。keepじゃなくremoveの方。
結果、以下のような 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()
を削除しなかった場合)、警告ダイアログが出てきます。どうとでもなるので、いい感じにしましょう。
一気に変わってますが、個別には以下になります。なおメンバのpush downはリファクタリングメニューから個別に実行できます。
やりたいのは「使用クラスの変更」なんですが、これを安全に行うのが本稿の内容になります。それなりの規模で根底のドメインを変えると、数百ファイル同時に編集されたりします。手作業では面倒で不安。
これ書いた動機
型とIDEの機能を使えば素早く安定してコードをこねくり回せます。その一例。
値オブジェクトを作ると「クラスがたくさんになり過ぎて困るのでは」みたいなのがありますが、その困ることのひとつに「違うと思ってたけど実は同じだった」というのがあります。この時に時間と精神力を消費して必ず誤る手作業(酷い表現だ)や、祈りの文字列置換を行うのでは気が重いため、作ろうとした時に「違うように見えるけど本当は同じなのでは?」と穿ってみる必要が出てきます。
一塊のものを分けるより、分かれているものをくっつけるほうがよほど簡単です。
もちろん例外はあるけど。一塊のものを分離するのはなかなか機械的にはできません。いくつかできるテクニックはあります(それはまたそのうち……と言って書いてないのどんだけあるだろう)が、いつもできるわけではありません。 一方、分かれているものをくっつけるのはほぼほぼ機械的にできますし、機械的にできるものは今回のようにIDEで一発だったりします。
てことで、また知識が十分でない時は「なんか違うものっぽいなぁ」と感じたらじゃんじゃんクラスを作っていくといいと思います。同じかどうかを判断するのは後からでもできる。「同じと扱っていたものを後から分ける」より、「違うと思っていたものが実は同じだったからくっつける」の方が楽だと思います。
ツイートのリンク。まぁなんだ、要はバランスですよ、はい。
おまけ: 簡単で不安定な別解
少々乱暴ですが、以下の方法もあります。
- 2つのクラスの内容を一致させる。(コピペ。)
- 片方のクラスをファイルごと削除する。(使用箇所があるとか警告されますが無視して実行。コンパイルエラーになる。)
- 残った方の名前を削除した方の名前に変換する。(リファクタリング機能を使用する。)
コンパイルエラー状態でのリファクタリング機能は精度が落ちるので推奨はしません。最近はだいぶいい感じに動きますが、安心して使えるとは言い難いです。 この手順では、2番目でファイルを削除しなかったら「ファイル名が一致する」とかで怒られます。
さらに派生で削除する代わりに参照先モジュールに持っていくことでコンパイルエラーを回避するとかもありますが、マルチモジュール構成でなければできませんし、大掛かりになるので他の事故要因も出てきます。どうしても必要な時にって感じですね。
蛇足: recordのとき
recordを使うと継承が使用できないので、素直にはできなくなります。現段階では一旦recordをclassに書き換えて上記の手順を実施、その後recordに戻す……かなぁと思ってます。IntelliJ IDEAでclassとrecordの変換は相互にできるので、これも別に手間じゃないかなって。
あわせてよみたい?
ライブコーディング中に使ったIntelliJ IDEAの機能を適当に紹介してます。今回書いたのは意識して使う機能ですが、リンク先で挙げてるのはほんと無意識に使うレベルのもの。