こちらで書いた「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になります。 「書いてどうすんだ?」とか書いたけど、ちょっと思いついてしまった。いつか試す。