finalを付けるのをやめてみた
Javaの話ね。バージョンは8以降の実質的final(effectively final)があるものとします。7以前は匿名クラス(この呼び方は 匿名クラスとかローカルクラスとか参照)でローカル変数を使うにはfinal
が必要なので文脈変わります。
前提の整理
final
は色々なところにつけられます。
例えばこんな感じ。
final class FooClass { final Object barField = new Object(); final void bazMethod(final Object quxParameter) { final Object corgeLocalVariable; } }
このエントリで対象にするのは変数。フィールド barField
、パラメータ quxParameter
、ローカル変数 corgeLocalVariable
です。
以下を前提にします。
- 自分たちだけが使用するコードです。不特定多数から使用されるライブラリなどではありません。
- クラスやメソッドの
final
は対象外です。意味も違うし。 - アクセス修飾子も今回は存在自体を忘れてください。
- XXX(某JVM言語や某ライブラリ)使えば? は堪えてください。言いたくなる気持ちはわかるつもりですが、そっちに脱線すると帰ってこれなくなりますので。
- Java 11の
var
も別の話。
final
をつける意義と実際
再代入不可になり、コンパイルレベルでチェックしてくれます。これにより再代入が原因となる不具合が防げます。
再代入を許容すると状態が変わるため「この時点ではどうなるんだっけ?」と悩むことになります。
無用な状態遷移に悩まされないように final
をつけることを推奨するコーディング規約や、静的解析ツールも存在します。
で、実際のとこどうなのよと言うと、こんな感じになります。
- フィールド: 可能なら
final
にする。 - パラメータ: まちまち。 周囲が付けてたら合わせて付ける。
- ローカル変数: わざわざ付けない。そう言う規約なら従う。けど付いてるのをわざわざ外したりもしない。
新しくプロジェクトに参画して、借りてきた猫モードしてる時はだいたいこうします。
フィールドを final
にするのはイミュータブルが好ましいと言う最近の傾向や、完全コンストラクタに対応するバインディングライブラリなどの後押しもあります。
こんなクラスですね。
// 完全コンストラクタの例(イミュータブルの例ではない) class Hoge { final Object field; Hoge(Object field) { this.field = field; } }
フィールドはわかりやすいのですが、パラメータやローカル変数は様々です。
「パラメータは絶対上書きなんてしてはいけない。だから final
を付ける。」と言うのは、前半は完全に同意。後半もまあいいかな、と言う感じです。付ける派のほうが強い意思を持つものなので、わざわざ喧嘩する必要もないかなって。
ローカル変数は、まあ、スコープが広い(数千行に及ぶ)とかなら、付けるのもわかるかも……。
ともかくコンテキストを絞らずに驚き最小の法則を適用するなら、先に挙げたような感じになると思います。
この辺りの話は ふつうのJavaコーディングの初段と四段 あたりが該当します。 で、次が九段の「強い意思でルールを破る」です。
final
を付けるのをやめた
最近は final
を付けません。
だからと言っても、再代入を推奨するわけではありません。
final
なんて付けなくても、 どうせ再代入なんてしない のです。
あってもなくても一緒ならあるだけノイズです。
もし final
によりうっかり再代入してしまうことを防いでるのだとしたら、うっかり再代入をする方が問題です。
うっかり再代入を検出して、なぜ再代入をすべきでないかをチームで共有するべきでしょう。そんなうっかりさんがいたらどんなコード書いてくるか気になって仕方ないので、しばらくペアプロしたい。
検出に静的解析ツールを使ってもいいと思いますが、それは再代入を検出するべきで、final
が付いていないことを対象にはしません。
フィールドが final
付いてないからって再代入するようなのは、それをしちゃう考え方の方が危うい感じがします。
また、再代入を行うフィールドが存在した場合、選択的に final
を外すのも手間です。一度外したのを付け直すとかも面倒だし、そう言うときはコンパイルチェックがかからないから付け直し忘れとかが起こりうる。「なんでこれ final
付いてないんだっけ……」のような、答えGitのログにしか無いようなことに悩みたく無いです。
私の観測上、ローカル変数の使い回しが行われる主因は「汎用的な変数名」です。例えば str
とか。
適切な名前をつければ使い回しをする気なんて起きません。名付けの問題なのに「 final
を付けてないから使い回しちゃうんだ、使い回せないように final
をつけるルールにしよう」なんてするのは、臭いものの蓋ではないでしょうか。ルール敷いたところで str1
str2
とかが生まれるだけです。蓋の中でどんどん増殖しちゃう。
また、ローカル変数に final
を付ける要因として先に挙げた「スコープが広い」がありますが、これは「狭くしましょう」としか言いようがありません。
変数のスコープを狭くする。わかりやすくはメソッドの分割ですね。十分にスコープが狭ければ、「このローカル変数は再代入されたりしてないだろうか?」と疑う必要はありません。数行手前に目を走らせれば良いだけです。これも final
を付けて対策としてはいけないものだと思うのです。
パラメータのfinal
不要も、メソッドが十分小さければ再代入なんてしたらすぐ違和感に気づくはず。
そしてメソッドが十分に小さい、つまり文字数が少ない時に、パラメータやローカル変数に final
が付いてたら、意味のない final
の全体の文字数に対する割合が大きくなってしまう。
まあそんなわけで、実質的 final
、実質的イミュータブルで十分かなーって感じです。
それでもfinal
にする場面
と言うのもあります。
フィールドは、そのクラスのインスタンスを自分の与り知らぬ誰かが使用する場合ですね。
この記事の前提に反しますが、例えばライブラリなどを開発している場合です。
まあ final
にしたところで無理矢理変えることはできるんだけど、気休めは気が休まるので重要です。
パラメータやローカル変数は final
付いていようがなかろうがメソッドが十分小さくできてたら要らないと思う。対レガシーコードで手が出せなかったら事故防止のために付けるのを許容する、かも。
あとはチーム規模が大きくて(大きくするなよ)、誰がどんなコード書くかわからない(それもどうなの)……なんて時でしょうか。
まとめ
「final
を付けるルール(暗黙含む)」によって、本来の問題が隠されてはいませんか?
一回やめてみてもいいのではないでしょうか。私は今の所 final
がないことによる問題には遭遇しておらず、コードの見通しがよくなったくらいです。
final
を付けることで定数ならコンパイル時にインライン化されるなどのメリットはありますが、そういうのを考慮してもまあ要らないかなって。(とはいえ定数なら final
付けるけど、定数自体最近ほとんど使わないなぁ。)
最後になりますが、デフォルトで final
がいいです。わざわざ書きたくない。再代入可能の方にだけ宣言したい。と言う願望で締めとします。
追記: final
を付けるべきだと言う話 (2019-09-28T09:25)
私自身も final
を付けるべきだと思ってたりもします。以下のようなのは大いに同意しますし、なんならそれを理由に「 final
を付けるべきだ!」と言う役目を負ってもいいくらい。
読み手に変更する意思がないことを明示する
わかります。コードに意図を込めることは重要です。 レビュアーや未来の自分を含め今後変更する際のコードを読む負荷を下げられるなら付けます。 でも「よほどのことがない限り再代入なんてしない」なら、付けるほうが読む量増えるのに情報量が増えないので、やめてみました。
IDEが自動でつけるから書く手間はかからない
わかります。わざわざ final
(スペース含め6文字)をタイプすることはないですし、変数宣言をIDEに生成させた時に自動でつくオプションもあります。とても便利。
手間をすっ飛ばして残るのはやはり可読性。「 final
をつけることでコードを読む速度が上がるか?」で、大体の場合答えはNO。なので、やめてみました。
ちなみに最近の私は、コードを書く手間は殆ど考慮していません。コードは読み物なんで、書く時の一瞬の負荷に振り回されるべきではないです。 ふつうのJavaコーディング や コードをどまんなかになどをご参照ください。
再代入する必要のある変数を目立たせたいというのがあるんですよね……。🍥Reassignable みたいなアノテーションが有ればいいのかな……?
がくぞさんのツイートより。 私が本当に欲しいもの。情報量が増えて読む時間の短縮、使い方も誤りにくくなると期待できます。 まあでも使ってみたら案外役に立たないかもしれないし、導入するほどでもないんですよね……。 言語の標準機能であるならいいんですが、このために他のものを積む気にはならないんです。
まあでもフィールド用で、パラメータやローカル変数には使いませんね。後者はやっぱ小さいメソッドで片付けられると思うのです。
作るプロダクトや組織体制とかを踏まえて、どうするか考えるのがいいと思うのですよ。final
を付けるべきかどうか自体は枝葉に過ぎないかな。
JUnit 5の拡張機能を完全にマスターしたぃ
本日9/19の関ジャバ'19 9月度でJUnit 5のExtensionの話をしました。
スライド
JUnit 5.5.2 のUser Guide を合わせてご参照ください。なお5.3ならoohiraさんの日本語訳もあります。とても助かってる。
ちなみに表紙で言及してるBtoWは ゼルダの伝説 ブレス オブ ザ ワイルド - Switch で、ロード画面がこんな感じなのです。
背景
9月度のセッションは、以前の関ジャバにて登壇枠で申し込みいただいたお二人です。
- 業務システムの概要とその選択肢 / tunemageさん
- みんなのSelenium:The Road to WebDriver / woosyumeさん
他のイベントでの登壇などから特に心配はないのですが、流石に初登壇の方だけにお任せするのもあれなので私も話すことにしてみました。
Seleniumに絡めてテストの話、と考えてExtensionになったわけです。 ちなみにJJUG CCC 2019 FallのCfP出して落ちたネタです。
今回は我ながら珍しく、エモさほぼなしのテクニカルのみな内容になっています。
実際
お二人のセッション共に期待していた以上でした。ありがとうございました。
結局開始直前まで資料作成してた私が一番不安要素だったというか予想通りというか、はい。
woosyumeさんが私のセッションがJUnit5だからと、スケルトンをJUnit5に更新してくれたと聞きいたのを受け、Extensionの話をするんだからとWebDriverの初期化と引数解決できるExtensionを即興で作ってみました。自分のセッションに間に合って実例を見せれました。
いいネタをありがとうございました。
BotWは面白いです。これのためだけにSwitch買って良かった。関ジャバ関係ない。
- 出版社/メーカー: 任天堂
- 発売日: 2017/03/03
- メディア: Video Game
- この商品を含むブログ (37件) を見る
2019-09-20T00:52 追記
2016-12-03に 「どうしようJUnit 5」ってセッション をしてるんですが、その65ページで「詳しい話はそのうち」って言ってたんですよね。今回のはその「詳しい話」になります。 3年弱でようやく。正直忘れてた。
配列引数と可変長引数のクラスファイルでの違い
明日の関ジャバの資料作ってたら気になったので。(全く関係ない)
javac 11.0.4
可変長引数のメソッドを持つVaragsクラスとそれを呼び出すInvokerクラスがあってさ。
class Varargs { static void methodX(Object... args) {} static void methodY(Object[] args) {} } class Invoker { void methodA() { Varargs.methodX(); } void methodB() { Varargs.methodX(new Object()); } }
Varargs.methodX
と Varargs.methodY
の違いって、可変長で作ってたら引数が空とか配列にしなくても渡せたりとかするわけだけど、外から見えるコンパイル結果に違いがないと、そんな区別ってつかないはずで。メソッド内は配列で扱ってるのは間違いないのだけど。
なので javap
してみて。
static void methodX(java.lang.Object...); descriptor: ([Ljava/lang/Object;)V flags: (0x0088) ACC_STATIC, ACC_VARARGS Code: stack=0, locals=1, args_size=1 0: return static void methodY(java.lang.Object[]); descriptor: ([Ljava/lang/Object;)V flags: (0x0008) ACC_STATIC Code: stack=0, locals=1, args_size=1 0: return
descriptor
は同じで、 ACC_VARARGS
がついてる。classファイルのフォーマット眺める。
The ACC_VARARGS flag indicates that this method takes a variable number of arguments at the source code level. A method declared to take a variable number of arguments must be compiled with the ACC_VARARGS flag set to 1. All other methods must be compiled with the ACC_VARARGS flag set to 0.
なるほど(ふいんき)。
あとInvokerも javap
って眺める。
void methodA(); descriptor: ()V flags: (0x0000) Code: stack=1, locals=1, args_size=1 0: iconst_0 1: anewarray #2 // class java/lang/Object 4: invokestatic #3 // Method Varargs.methodX:([Ljava/lang/Object;)V 7: return void methodB(); descriptor: ()V flags: (0x0000) Code: stack=5, locals=1, args_size=1 0: iconst_1 1: anewarray #2 // class java/lang/Object 4: dup 5: iconst_0 6: new #2 // class java/lang/Object 9: dup 10: invokespecial #1 // Method java/lang/Object."<init>":()V 13: aastore 14: invokestatic #3 // Method Varargs.methodX:([Ljava/lang/Object;)V 17: return
呼び出し側で配列作って渡してる。やっぱそうだよね。
現実逃避終わり。