PR

Javaの「var」宣言を基礎から理解する

Java

Javaでは従来、変数宣言時に「int」「String」「List<String>」といった具体的なデータ型を明示的に記述してきました。しかしJava 10で登場した「var」を使うと、変数の型推論(type inference)が行われ、他の言語(JavaScriptやKotlinなど)で見られる「型を省略した変数宣言」のように書くことができます。

このページではJava初心者の方に向けて「var」を利用した変数宣言を理解するためのポイントを順を追って紹介します。

スポンサーリンク

1. varとはなにか

  • 型推論を利用するキーワード
    「var」を使うと、コンパイラが「右辺(代入する値)」の型情報から自動的に変数の型を推論してくれます。
var message = "Hello World"; // コンパイラが自動で String 型だと判断
var number  = 10;            // コンパイラが自動で int 型だと判断
  • Javaの「var」と他言語との違い
    C#やJavaScriptのように「言語レベルで動的型付け」をしているわけではなく、あくまでコンパイル時に型をきちんと確定しています。実行時には厳密な型が確定しているため、Javaの静的型付けという特徴はそのままです。
  • Java 10以降で利用可能
    「var」はJava 10から導入されました。したがって、Javaのバージョンが10未満の場合は使えないので注意してください。

Javaはコンパイルしたバイトコードを実行する流れですが、このバイトコードを生成する段階で型が一意に決まります。その意味で実行時に動的に型付けがされるJavaScriptのような言語とは仕組みとして異なります。

2. varが使える範囲

「var」はローカル変数としてしか利用できないという特徴があります。実践に即して具体例を挙げると↓のような位置で使用できます。

  • メソッド内部の変数
public void exampleMethod() {
    var count = 0;
    var name = "Alice";
    ...
}
  • for文&拡張for文のループ変数
// for文
for (var i = 0; i < 10; i++) {
    System.out.println(i);
}

// 拡張for文
var names = List.of("Alice", "Bob", "Carol");
for (var n : names) {
    System.out.println(n);
}

使えない場所としては以下が代表的です。

  • メソッドの引数・戻り値の型
  • フィールド(クラスメンバ変数)の型
  • インスタンス変数やstatic変数の型

「var」はあくまでメソッド内部やブロック内部でしか使えない、と覚えておきましょう。

3. varで宣言するメリット

  • 型宣言の簡略化
    例として、Genericsを伴うコレクションを宣言する場合、従来は長い記述が必要でした。
List<Map<String, Integer>> list = new ArrayList<>();

これが「var」を使うとちょっとだけすっきりします。

var list = new ArrayList<Map<String, Integer>>();

コンストラクタ右辺の「Map<String, Integer>」情報から、コンパイラはList<Map<String, Integer>>型だと推論します。

  • コードの可読性向上
    上の例のように複雑な型を扱う場合、可読性が向上することが多いです。長いGenerics型を全部明示する必要がなくなり、意図が分かりやすくなります。
  • リファクタリングがしやすい
    右辺の型が変わったとしても、変数宣言部分を大きく書き直す必要がありません。メソッドの戻り値型が変更になった場合なども、適切に型推論してくれるので柔軟に対応できます。

4. varのデメリット・注意点

  1. 可読性が落ちるケースがある
    一見、型が推論されるためスッキリするようでいて、逆に「実際にどんな型なのかが分かりにくい」ことがあります。特に右辺のメソッドが複雑なジェネリクス型を返す場合は、エディタの補完やドキュメントを見ないと確信が持てないこともあるでしょう。
  2. 暗黙の型変換は行われない
    「var」はコンパイラが右辺をもとに厳密に型を推論します。例えば、var x = 10;の時点でxはint型となり、その後x = 10.5;のようにdoubleを代入することはできません。
  3. nullだけを代入する宣言はできない
    var test = null; という形では型が決められません(コンパイルエラー)。必ず具体的なオブジェクトやリテラルを右辺に書く必要があります。
  4. 初期化子(右辺)が必須
    「var」で変数を宣言する際は、必ず同じ行で初期化する必要があります。これはコンパイラが型推論をするために必須の手順です。
var s; // コンパイルエラー:初期値がない
s = "Hello";

5. 実践でよくある使いどころ

  • ストリームAPIの途中の変数受け取り
var result = list.stream()
                 .filter(...)
                 .map(...)
                 .collect(Collectors.toList());
  • ストリームの種類や要素型が複雑になっても、とりあえず「var」にしておけば「List」なのか「Set」なのかなどを右辺から推論してくれます。
  • ローカルでの簡易的なデータ構造宣言
    たとえば「Map<String, List<Integer>>」のように型パラメータが入り乱れる場合などは、右辺だけで型情報を提供するほうが可読性が高いことがあります。
  • テストコードやサンプルコード
    型推論のおかげで余分な情報を排除し、「意図のみにフォーカスしたコード」を書きやすくなります。ただし、大規模プロジェクトでは過度なvar利用は混乱を招くこともあるため、使用ルールをチームで明確化することが大切です。

6. 推論される型を確認する方法

6-1. IDEの補助機能を使う

  • EclipseやIntelliJ IDEA、VS Codeなど、モダンなIDEでは「var」で宣言した変数にカーソルを合わせると、内部で推論された型がポップアップ表示されます。
  • チーム開発でソースコードを読むときも、IDEの補完機能を使うことで「このvarは最終的にどの型に推論されているか?」を簡単に把握できます。

6-2. コンパイラのエラーを活用する

  • たとえば「var message = "Hello";」と宣言した後に「message = 100;」と書くとコンパイルエラーが発生します。これは「message」がString型として推論されたからです。
  • こうしたエラーからも「どういう型になっているか」を逆算することができます。

6-3. ツールや静的解析を利用する

  • チームによっては、SonarQubeなどの静的解析ツールを導入し、varを使用する際に何らかのルールを設けているケースがあります(例:「varの多用を禁止する」など)。
  • これらのツールは推論される型を把握しており、レポートとして警告を出してくれることもあるため、コードレビューに役立ちます。

7. ラムダ式での「var」の利用

7-1. ラムダ式のパラメータにvarを使う(Java 11~)

// Java 11 以降
(var s) -> s.toUpperCase()
  • Java 11からはラムダ式の引数にも「var」が使えるようになりました。
  • メリットとしては、ラムダ引数にアノテーションをつける際に記述が統一できることがあります。たとえば、次のように引数にアノテーションを付与したい場合です。ただし、可読性の面からすると、ラムダパラメータに型があったほうが分かりやすいという意見も多く、現場ではあまり多用されていないことも事実です。
(var @NonNull s) -> s.toUpperCase()

7-2. ラムダ式内部でのローカル変数にvarを使う

  • ラムダ式の中の普通のローカル変数については、前編で挙げたように通常のローカル変数宣言と同じ扱いになります。
  • 複雑な型が入り乱れるストリーム処理などで「var」を適度に使うと、コードがすっきり書けるケースもありますが、あまりにも複雑になる場合は「var」を使いすぎて型が分からなくなる可能性もあります。
var list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> {
    var upper = item.toUpperCase();
    System.out.println(upper);
});

8. 匿名クラス・インナークラスとの違い

  • 匿名クラスローカルクラスで変数を宣言する場合にも「var」は使えますが、あくまで「変数宣言がローカルスコープに属している」ならばOKというルールです。
  • ただし、クラスのフィールドをvarで宣言することはできない点に注意しましょう(「var」を使えるのはあくまでローカル変数のみ)。
var thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // ここでのvarは通常通り使える
        var localVar = "test";
        System.out.println(localVar);
    }
});

9. try-with-resources構文とvar

  • Java 9から「try-with-resources」でローカル変数参照を直接使えるようになったため、Java 10以降であれば次のような書き方が可能です。
  • 「try (var br ...)」とすることで、明示的に型を書く必要がなくなり、可読性が向上するケースがあります。
  • ただし、初心者にとっては「このvarが何の型なのか分かりづらい」と感じるかもしれません。チーム方針で「try-with-resourcesだけはvarを使わない」と決めているところもあるため、プロジェクトのコーディング規約を確認しましょう。
try (var br = new BufferedReader(new FileReader("test.txt"))) {
    // br は自動的にCloseableとして扱われる
    System.out.println(br.readLine());
} catch (IOException e) {
    e.printStackTrace();
}

10. 大規模開発における「var」導入のベストプラクティス

  1. チームでルールを決める
    • 例えば「基本的には型を書き、ジェネリクスが煩雑な場合に限りvarを使う」「テストコードでは自由に使ってよい」など、ガイドラインを作ることで、コードベースが「varだらけ」になって混乱するのを防ぎます。
  2. 変数名をわかりやすくする
    • 「var」を使う場合、変数名が型のヒントとなるようにするのが望ましいです。
    • 変数名やメソッド名が「何を返すのか」分かりやすくすることで、IDEがなくてもコードリーディングが可能になります。
  3. 必要以上にvarを使わない
    • 「型が簡潔に書ける場所」や「変数の型が明らかである場所」に限定して使うことで、可読性を損なわずに恩恵だけ享受できます。
    • たとえば、単純なintStringのように明らかに分かりやすい型では、わざわざ「var」を使わないケースが多いです。
  4. 型が複雑すぎるならメソッド分割も検討
    • 「varのおかげでスッキリ書けているけれど、右辺のメソッド呼び出しが極端に長い・複雑」という場合、メソッドを分割して可読性を上げることも検討します。
    • すなわち「varを使う」はあくまで一手段であって、根本的には読みやすいメソッド設計が必要です。

11. 具体的な利用例(まとめ)

ここまでの内容を踏まえ、以下のようなサンプルを通して「var」の使い方を再度整理しましょう。

public class VarSample {

    public static void main(String[] args) {
        // 1. 文字列 (String)
        var greeting = "Hello, var!";
        System.out.println(greeting);         // greetingはString型と推論

        // 2. 数値 (int)
        var count = 10;
        // count = 10.5; // コンパイルエラー:countはint型

        // 3. 複雑なジェネリクス
        var userMap = new HashMap<String, List<Integer>>();
        userMap.put("Alice", List.of(1,2,3));
        userMap.put("Bob",   List.of(4,5));
        System.out.println(userMap);

        // 4. ストリームAPI
        var result = userMap.entrySet().stream()
                            .filter(e -> e.getValue().size() > 2)
                            .map(e -> e.getKey())
                            .toList(); 
        // resultはList<String>と推論される
        System.out.println(result);

        // 5. try-with-resources
        try (var br = new BufferedReader(new FileReader("example.txt"))) {
            System.out.println(br.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 6. 拡張for文
        var list = List.of("Java", "Kotlin", "Scala");
        for (var lang : list) {
            System.out.println(lang);
        }

        // 7. ラムダ式(Java 11~ varパラメータ)
        // (var s) -> s.toLowerCase()
        list.forEach((var item) -> System.out.println(item.toUpperCase()));
    }
}

このように、実際のコードでも「var」がさまざまなシーンで利用可能であることが分かります。

まとめ Javaのvar

  • 「var」はローカル変数の型宣言を省略できる機能であり、Java 10から導入された。
  • メリットとしては、ジェネリクスによる煩雑な型宣言が簡略化され、リファクタリングが容易になる点などが挙げられる。
  • デメリットとしては、どんな型なのか即座に判断しづらくなり、可読性が低下するケースがある点に注意が必要。
  • 導入の際はチームルールを設定し、コードが混乱しないように適度に使うことが望ましい。

以上が、Javaの「var」を初心者から現場レベルまで俯瞰して理解するためのポイントです。特に大規模プロジェクトや長期運用されるシステムでは、むやみに「var」を使わず、可読性とのバランスを常に意識することが重要になります。

タイトルとURLをコピーしました