Skip to main content

Java 10以降のローカル型推論チートシート!

著者

Stuart Marks

2018年4月26日

0 分で読めます
コード例を使って3つの原則と7つのガイドラインを解説する、Javaのローカル型推論に関するSnykのチートシート。

Snykブログでお届けする、新しいチートシートシリーズの第1回へようこそ。開発者として成長するために印刷して掲示できるコンテンツをお届けします。第1回では、Java 10のリリースに続き、話題のローカル変数の型推論を取り上げます。上の画像をクリックするか、こちらのリンクからPDF版をダウンロードできます。ローカル型推論機能の基本的な考え方はとてもシンプルです。宣言時に明示していた型を、新しい予約語「var」に置き換えると、型が推論されます。たとえば、次のように置き換えられます。

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

次のようになります。

var outputStream = new ByteArrayOutputStream();

すると、outputStreamの型はByteArrayOutputStreamと推論されます。ちょっと待ってください。Javaで動的型付けが使えるようになったということですか?もちろん違います。型推論はすべてコンパイル時に行われ、明示的な型はコンパイラによってバイトコードに組み込まれます。実行時のJavaは、これまでどおり静的型付けです。使い方自体はとてもシンプルなので、このチートシートではローカル型推論の最も重要な側面、つまり実践的な使い方に焦点を当てます。明示的な型指定を使うべきときと、型推論を検討すべきときについて説明します。

このチートシートを作ろうと思っていたところ、OracleのJDKエンジニアであるStuart Marksが、型推論の使用に関する原則と指針を解説した素晴らしい記事を公開しました。そこでチートシートを作るにあたり、Stuartに連絡して、彼の考えを盛り込み、開発者が掲示して日々活用できる形にまとめられないか相談しました。ぜひStuartの記事全文もお読みください。読む価値があります。

原則

1. コードを書くことより、読むことが大切。

1行のコードを書くのに10分かかろうと10日かかろうと、そのコードはほぼ間違いなく、その後何年にもわたって読み返されます。コードが将来も保守しやすく理解しやすいものであるためには、明確で簡潔であり、何よりもその目的を理解するために必要な情報がすべて含まれていなければなりません。目指すのは、理解しやすさを最大限に高めることです。

2. コードは、その場で読んで理解できるようにする。

何が起きているのかを理解するために、コードベースのあちこちを調べなくて済むよう、できる限り多くの情報をコードに含めましょう。メソッド名や変数名を工夫するのもその方法です。

3. コードの読みやすさをIDEに依存させない。

IDEはとても便利です。本当に便利です。開発者の生産性や正確性を高めてくれます。しかし、IDEに頼らなくてもコードを読んで理解できる必要があります。コードはIDE以外の場所で読まれることもよくあります。また、IDEによって、読み手に提供される情報の量が異なる場合もあります。コードはそれ自体で内容が伝わるべきです。ツールの助けがなくても、一目で理解できるようにしましょう。

判断するのはあなたです。

変数に明示的な型を指定するか、Javaコンパイラに型を推論させるかは、トレードオフです。一方では、余分な記述や定型コード、形式的な記述を減らしたいでしょう。もう一方では、コードの理解しやすさを損ないたくありません。型宣言だけが読み手に情報を伝える手段ではありません。変数名や初期化式からも情報を伝えられます。各変数について、明示的な型指定を省いてよいか判断する際は、利用できる情報源をすべて考慮しましょう。

ガイドライン

1. 役立つ情報を伝える変数名を付ける。

これは一般的に良い習慣ですが、varを使う場合は特に重要です。varを使った宣言では、変数の意味や用途を変数名で伝えられます。明示的な型をvarに置き換えるときは、変数名もより分かりやすくするのがよいでしょう。場合によっては、変数名に型を含めると便利です。たとえば、次のようになります。

List<Customer> x = dbconn.executeQuery(query);

✅ var custList = dbconn.executeQuery(query);

2. ローカル変数のスコープを最小限にする。

ローカル変数のスコープを制限することは、『Effective Java』(第3版)の項目57で解説されています。varを使う場合は、なおさら重要です。問題になるのは、変数のスコープが広い場合です。つまり、変数の宣言から使用までの間に多くのコード行がある状態です。コードの保守中に型などが変更されると、動作が変わってしまうことがあります。たとえば、ListからSetに変更するのは問題なさそうに見えても、同じスコープ内の後続処理で順序に依存していないでしょうか。同じインターフェースを実装している型でも、実装の微妙な違いが思わぬ問題を招くことがあります。このような場合、単にvarを避けるのではなく、ローカル変数のスコープを狭めてからvarで宣言しましょう。次のコードを見てください。

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

このコードにはバグがあります。Setには反復順序が定義されていないためです。ただし、items変数の使用箇所が宣言のすぐ近くにあるので、プログラマーはこのバグにすぐ気づいて修正できるでしょう。では、このコードが大きなメソッドの一部で、items変数のスコープもそれに応じて広い場合を考えてみましょう。

var items = new HashSet<Item>(...);

// ... 100 lines of code ...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }

この場合、Setの末尾に項目を追加しようとする行が型宣言から離れているため、バグの発見はずっと難しくなります。

3. 初期化式から十分な情報が読み取れる場合は、varを検討する。

ローカル変数は、コンストラクターで初期化されることがよくあります。生成するクラス名は、左辺の明示的な型として繰り返し記述されることが少なくありません。型名が長い場合、varを使えば情報を失わずに記述を簡潔にできます。

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

✅ var outputStream = new ByteArrayOutputStream();

Files.newBufferedReader(…)のようなメソッド呼び出しや、List stringList = List.of("a", "b", "c")のような場合にvarを使うのも妥当です。

4. varを使って、連鎖した式や入れ子になった式をローカル変数に分割する。

文字列のコレクションを受け取り、最も頻繁に出現する文字列を見つけるコードを考えてみましょう。次のように書けます。

return strings.stream()
              .collect(groupingBy(s -> s, counting()))
              .entrySet()
              .stream()
              .max(Map.Entry.comparingByValue())
              .map(Map.Entry::getKey);

このコードは正しく動作しますが、複数の文に分けたほうが読みやすくなります。次のように文を分けるときに問題となるのは、

Map<String, Long> freqMap = strings.stream()
                                   .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
                                                       .stream()
                                                       .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

しかし、明示的な型指定が非常に煩雑で、重要なコードから注意がそれるため、作者は分割をためらったのかもしれません。varを使えば、中間変数の型を明示するという大きな負担をかけずに、より自然な形でコードを表現できます。

✅
var freqMap = strings.stream()
                     .collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
                         .stream()
                         .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

メソッド呼び出しを1つの長い連鎖にした最初のコードを、正当な理由から好む人もいるでしょう。ただし、長いメソッドチェーンを分割したほうがよい場合もあります。

5. ローカル変数で「インターフェースに対するプログラミング」を過度に気にしない。

Javaでは、具象型のインスタンスを生成しながら、インターフェース型の変数に代入するのが一般的です。たとえば、次のようになります。

List<String> list = new ArrayList<>();

しかし、varを使うと、インターフェース型ではなく具象型が推論されます。

// Inferred type of list is ArrayList<String>.
✅ var list = new ArrayList<String>();

list変数を使うコードは、具象実装に依存するようになる可能性があります。将来、変数の初期化式が変更されると、推論される型も変わり、後続のコードでエラーやバグが発生するおそれがあります。

ガイドライン2に従ってローカル変数のスコープを小さくしていれば、これはそれほど問題になりません。具象実装が「漏れ出して」後続コードに影響するリスクを抑えられるためです。

6. ダイヤモンド演算子やジェネリックメソッドでvarを使う場合は注意する。

varと「ダイヤモンド」機能はどちらも、すでにある情報から型を導き出せる場合に、明示的な型情報を省略できます。しかし、両方を併用すると、コンパイラが推論する型を正しく絞り込むために必要な情報をすべて省略してしまうことがあります。

次の例を見てみましょう。

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
✅ var itemQueue = new PriorityQueue<Item>();

// DANGEROUS: infers as PriorityQueue<Object>
❌ var itemQueue = new PriorityQueue<>();

ジェネリックメソッドでは型推論がうまく活用されており、プログラマーが型引数を明示することはほとんどありません。ジェネリックメソッドの型推論では、十分な型情報を与える実引数がない場合、ターゲット型に依存します。varによる宣言にはターゲット型がないため、ダイヤモンド演算子と同様の問題が起こることがあります。たとえば、

// DANGEROUS: infers as List<Object>
❌ var list = List.of();

ダイヤモンド演算子でもジェネリックメソッドでも、コンストラクターやメソッドに実引数を渡すことで型情報を追加でき、意図した型を推論できます。間接的な要素が1つ増えますが、それでも予測可能です。つまり、次のようになります。

// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
✅ var itemQueue = new PriorityQueue<>(comp);

7. リテラルでvarを使う場合は注意する。

型名は一般に短いため、リテラルでvarを使っても大きなメリットは期待できません。ただし、変数名をそろえる場合など、varが便利なこともあります。

boolean、文字、long、文字列のリテラルには問題ありません。これらのリテラルから推論される型は明確なので、varの意味も曖昧になりません。初期化式が数値、特に整数リテラルの場合は十分に注意してください。左辺に明示的な型を指定すると、数値がint以外の型に暗黙的に拡張または縮小されることがあります。varを使うと値はintと推論されますが、それが意図した型ではない可能性があります。

// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
byte flags = 0;
short mask = 0x7fff;
long base = 17;

✅ var ready = true;
✅ var ch    = '\ufffd';
✅ var sum   = 0L;
✅ var label = "wombat";

// DANGEROUS: all infer as int
❌ var flags = 0;
❌ var mask = 0x7fff;
❌ var base = 17;

まとめ

宣言にvarを使うと、余分な記述が減り、より重要な情報が目立つようになるため、コードの改善につながります。一方、むやみにvarを使うと、かえってコードが分かりにくくなることがあります。適切に使えば、理解しやすさを損なうことなくコードを短く明確にし、質の高いコードをさらに改善できます。varを使うときは、コードが曖昧になっていないか、詳しく調べなくても明確に理解できるかを確認しましょう。

今すぐチートシートをダウンロード!

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。

feature insights context
Blog

予防は、本質的に解決済みの問題なのでしょうか?

エージェントが生成するコードの予防策はアーキテクチャ上解決されていますが、開発を遅らせることなくセキュリティを守る制御を選ぶことが、依然として課題です。