コンテンツにスキップ

Java/基礎/変数と代入演算

出典: フリー教科書『ウィキブックス(Wikibooks)』

変数と代入

[編集]

変数

[編集]

プログラミングにおける変数は、値やデータを格納するための記号的な名前です。変数は、メモリ内の特定の場所を参照し、その場所に値を保持します。これにより、プログラム内でデータを操作したり処理したりすることができます。

変数は、プログラム内で使用されるデータの値を表現し、名前を介してそれらの値にアクセスする手段を提供します。また、変数は値を保持するだけでなく、値を変更することもできます。これにより、プログラムが動的に振る舞うことが可能になります。

例えば、プログラムがユーザーの名前や年齢などの情報を取得し、それを後で使用する必要がある場合、変数を使用してその情報を格納し、必要に応じて変更や処理を行うことができます。

Javaにおける変数

[編集]

Javaにおける変数とは、データを格納する箱のようなものです。変数は、プログラム内で使用されるデータを保持し、それに名前を付けることができます。これにより、プログラム内で値を簡単に参照したり変更したりできます。

Javaでは、変数を使用する前に、その変数の型(データの種類)を宣言する必要があります。例えば、整数を格納する変数の場合はint型、小数を格納する変数の場合はdouble型、文字列を格納する変数の場合はString型などがあります。

宣言

[編集]

変数の宣言は次のように行います:

// 整数を格納する変数の宣言
int age;

// 小数を格納する変数の宣言
double height;

// 文字列を格納する変数の宣言
String name;

これにより、ageheightnameという名前の変数が宣言されます。ただし、この時点ではまだ値は割り当てられていません。

代入

[編集]

変数に値を代入するには、以下のようにします:

age = 25;
height = 1.75;
name = "Alice";

これにより、それぞれの変数に値が割り当てられます。

初期化

[編集]

また、変数の宣言と初期化を同時に行うこともできます:

int age = 25;
double height = 1.75;
String name = "Alice";

これにより、変数の宣言と初期化を一度に行うことができます。

再代入

[編集]

Javaにおいて、再代入とは変数に新しい値を割り当てることを指します。Javaでは、再代入が可能な変数は、その型がプリミティブ型であるか、または参照型であるかによって挙動が異なります。

  1. プリミティブ型の変数の再代入: プリミティブ型の変数は、その型に対応する値を直接保持します。再代入すると、変数が新しい値に更新されます。
    int x = 5; // int型の変数xに初期値5を代入
    x = 10;    // xに新しい値10を再代入
    
  2. 参照型の変数の再代入: 参照型の変数は、オブジェクトへの参照を保持します。再代入すると、変数が新しいオブジェクトへの参照に更新されますが、元のオブジェクトは影響を受けません。
    String str1 = "Hello"; // String型の変数str1に文字列"Hello"を代入
    str1 = "World";        // str1に新しい文字列"World"を再代入
    

変数に再代入された値の型は、変数が宣言された際に指定された型と一致する必要があります。たとえば、int型の変数には整数値を、double型の変数には浮動小数点数を再代入する必要があります。再代入された値の型が異なる場合、コンパイルエラーが発生します。

文字列

[編集]

Javaにおける文字列は、文字のシーケンスを表現するためのデータ型です。Javaでは、文字列はjava.lang.Stringクラスを使用して表現されます。文字列は、ダブルクォーテーション(”)で囲まれた文字のシーケンスとして表現されます。

文字列はイミュータブル(変更不能)であり、一度作成されると変更することができません。つまり、文字列を変更する操作は、新しい文字列を作成することになります。

以下は、Javaで文字列を宣言する例です:

String greeting = "Hello, world!";

この例では、greetingという名前の変数に文字列"Hello, world!"が代入されています。

final 修飾された変数

[編集]

Javaでは、final修飾子を使って宣言された変数は、イミュータブル(不変)です。final修飾子を使用すると、その変数に対する再代入ができなくなります。つまり、一度初期化された後はその値を変更することができません。

以下は、final修飾子を使って宣言されたイミュータブルな変数の例です:

final int AGE = 30;
final String NAME = "John";

これらの変数AGENAMEは、一度初期化された後に再代入することができません。このような変数は、プログラム内で定数として使用されることがあります。

final 修飾された参照型変数

[編集]

Javaにおいて、final修飾された参照型変数は、変数の参照先(オブジェクトへの参照)が不変であることを示します。つまり、一度初期化された後は、その変数が参照するオブジェクトを変更することはできませんが、オブジェクト自体の内容は変更可能です。

以下は、final修飾子を使って宣言された参照型変数の例です:

final StringBuilder builder = new StringBuilder("Hello");

builder.append(", world!"); // オブジェクトの内容を変更する操作

System.out.println(builder.toString()); // 出力: Hello, world!

この例では、final修飾子が付けられたbuilder変数は、一度StringBuilderオブジェクトに初期化された後、その参照先を変更することができません。ただし、StringBuilderオブジェクト自体の内容を変更することは可能です。

キャスト

[編集]

Javaにおけるキャスト(cast)は、データ型を変換する操作を指します。Javaでは、異なるデータ型の間で変換を行う際にキャストを使用します。キャストは、変換元のデータ型を指定し、変換後のデータ型に変換するための演算子です。

キャストは基本的に2つの形式があります:

  1. 明示的なキャスト(Explicit cast): 明示的なキャストは、変換元のデータ型を指定して行われます。キャストの前に、変換先のデータ型を括弧で囲んで指定します。
    double d = 10.5;
    int i = (int) d; // 明示的なキャスト
    
    この例では、double型の変数dint型の変数iに明示的にキャストしています。このキャストにより、dの小数部分が切り捨てられてiには整数部分のみが格納されます。
  2. 暗黙的なキャスト(Implicit cast): 暗黙的なキャストは、Javaの型変換ルールに従って、自動的に行われます。暗黙的なキャストは、より小さいデータ型からより大きいデータ型への変換に使用されます。
    int i = 10;
    double d = i; // 暗黙的なキャスト
    
    この例では、int型の変数idouble型の変数dに代入される際に、暗黙的なキャストが行われます。Javaはint型からdouble型への変換を自動的に行い、iの値をdに代入します。

キャストを行う際には、変換元のデータ型と変換後のデータ型が互換性があるかどうかを確認する必要があります。 たとえば、数値型同士のキャストや、参照型の継承関係に基づくキャストが可能ですが、互換性のない型同士のキャストはコンパイルエラーを引き起こします。

型推論 (var)

[編集]

Java 10以降、Javaにはローカル変数の型推論機能が追加されました。これは、varキーワードを使用して変数を宣言する際に、コンパイラが変数の型を自動的に推論する機能です。この機能により、変数の型を明示的に指定する必要がなくなり、コードの冗長性を減らすことができます。

変数の宣言時に初期化子が提供される場合、その初期化子の型に基づいて変数の型が推論されます。推論された型は静的な型であり、実行時に変更されることはありません。

例えば、以下のようにvarを使用して変数を宣言することができます:

var i = 1;

varを使用することで、変数の型が明確になる場合には冗長な型の記述を省略できます。ただし、可読性を損なわない範囲での使用が推奨されます。また、varを使用する場合でも、適切な変数名やコメントを追加することで、コードの理解を助けることが重要です。

ループ変数

[編集]

Java 10以降、Javaにはループ変数の型推論機能が追加されました。 これにより、forループや拡張forループ(拡張for文、拡張forループ)で、ループ変数の型を自動的に推論することができます。 これにより、コードの冗長性を減らし、可読性を向上させることができます。

具体的には、forループの初期化部でループ変数を宣言し、その型をvarキーワードで指定することができます。 この際、初期化式によって型が明示的に指定される場合、その型を推論してループ変数の型を決定します。

拡張forループでは、コレクションや配列を反復処理する際に、ループ変数の型を指定せずに、varキーワードを使用してループ変数を宣言することができます。この場合、コレクションや配列の要素の型がループ変数の型として推論されます。

以下は、forループと拡張forループでのループ変数の型推論の例です:

// forループでの型推論
for (var i = 0; i < 5; i++) {
    System.out.println(i); // iの型はintと推論される
}

// 拡張forループでの型推論
List<String> strings = List.of("foo", "bar", "baz");
for (var str : strings) {
    System.out.println(str); // strの型はStringと推論される
}

varを使用することで、ループ変数の型を省略し、コードをよりシンプルにすることができます。ただし、適切な変数名やコメントを追加して、コードの可読性を確保することが重要です。

Javaにおける型推論の変遷
Javaにおける型推論は、言語仕様の進化とともに次第に強化されてきました。以下にその変遷をまとめます。
  1. Java SE 5(2004年):ジェネリクスの導入
    • 背景: Java SE 5でジェネリクスが導入され、型安全なコレクション操作が可能になりました。
    • 型推論の始まり:
      ダイヤモンド演算子なし
      List<String> list = new ArrayList<String>();
      
      • 宣言部分とインスタンス化部分で型を明示的に指定する必要がありました。
  2. Java SE 7(2011年):ダイヤモンド演算子の導入
    ダイヤモンド演算子(<>によって型推論が可能に
    List<String> list = new ArrayList<>();
    
    • コンパイラがコンテキストから型を推論できるようになり、コードの簡潔性が向上しました。
  3. Java SE 8(2014年):ラムダ式とターゲット型
    • ラムダ式:
      ラムダ式では、コンパイラがコンテキスト(ターゲット型)から型を推論します。
      List<String> list = Arrays.asList("a", "b", "c");
      list.forEach(item -> System.out.println(item)); // 型推論
      
      上記では、itemの型がStringであることを推論します。
      明示的に型を書くことも可能
      list.forEach((String item) -> System.out.println(item));
      
    • メソッド参照:
      型推論がメソッド参照にも適用されました。
  4. Java SE 9(2017年):<>の改善
    • 匿名クラスのダイヤモンド演算子対応:
      Java SE 8までは、匿名クラスのインスタンス化ではダイヤモンド演算子が使えませんでしたが、Java SE 9で対応しました。
      List<String> list = new ArrayList<>() {
          // 匿名クラスの内容
      };
      
  5. Java SE 10(2018年):varの導入
    • ローカル変数型推論:
      varキーワードによってローカル変数の型をコンパイラに推論させることが可能になりました。
      var list = new ArrayList<String>();
      list.add("Hello");
      
    • 制約:
      • ローカル変数でのみ使用可能(クラスフィールドやメソッド引数には使用不可)。
      • 型が明確に推論できない場合にはエラーになる。
  6. Java SE 11(2018年):varのラムダ式引数への拡張
    • ラムダ式の引数でvarを使用可能に:
      list.forEach((var item) -> System.out.println(item));
      
      これにより、ラムダ引数に注釈を付与しつつ型推論を利用できるようになりました。
      list.forEach((@NonNull var item) -> System.out.println(item));
      
  7. 型推論の限界と注意点
    • 読みやすさの問題:
      過度に型推論に頼ると、コードの可読性が低下する場合があります。
      var map = new HashMap<>();
      
      • 上記の例では、型が明示されていないため、読み手が型を推測する必要があります。
    • 複雑な型:
      • 型推論が有効でないケースもあります(特に複雑なジェネリクスや無名クラス)。
Javaの型推論は、ダイヤモンド演算子やvarの導入を通じて徐々に拡張され、コードの簡潔性を向上させました。ただし、型推論が有効である反面、コードの可読性や明確性を意識して使用することが重要です。

ラムダ式

[編集]

ラムダ式の型推論とは、Javaでラムダ式を使用する際に、コンパイラがラムダ式のパラメータの型や戻り値の型を自動的に推論する機能を指します。つまり、ラムダ式のパラメータや戻り値の型を明示的に指定せずに、コンパイラがコードの文脈から型を推測することができます。

var x = (int i) -> i * i;

例えば、次のコードでは、ラムダ式 (int i) -> i * i のパラメータ i の型が int として指定されています。しかし、Javaのコンパイラは、このコードの文脈から i の型が int 型であることを推論することができます。

var キーワードを使用することで、変数 x の型を明示的に指定せずに、コンパイラがラムダ式の型を推論することができます。そのため、コードをよりシンプルに記述することができます。

varキーワードを使わずに、ラムダ式の型を明示的に指定する場合は、次のようになります:

IntUnaryOperator x = (int i) -> i * i;

このコードでは、IntUnaryOperatorインターフェースを使用してラムダ式を宣言しています。IntUnaryOperatorは、int型の引数を受け取り、int型の値を返す関数型インターフェースです。ラムダ式の引数がint型であるため、このインターフェースを使ってラムダ式を宣言しています。

IntUnaryOperatorインターフェース
IntUnaryOperator インターフェースは、Javaの標準ライブラリである java.util.function パッケージに含まれています。このパッケージは、関数型プログラミングをサポートするためのインターフェースやクラスが定義されています。
IntUnaryOperator インターフェースは、引数として整数型を受け取り、整数型の値を返す関数型インターフェースです。具体的には、int applyAsInt(int operand) メソッドを持ちます。これにより、整数型の引数を受け取り、整数型の結果を返す関数を表現することができます。
Javaの関数型インターフェースは、java.util.function パッケージに含まれているため、import java.util.function.IntUnaryOperator; を使用してインポートすることで利用することができます。

varによるラムダ式の型推論をつかうと、この調べごとをする必要がなくなります。

複雑な型推論

[編集]

複雑な型を返すメソッドを受け取る場合にも、型推論は有用です。以下の例では、flatMap() メソッドを使用して、文字列のリストを1つの文字列に変換しています。

List<String> list = Arrays.asList("hello", "world");
var result = list.stream()
        .flatMap(s -> Arrays.stream(s.split("")))
        .collect(Collectors.joining());

最後に、ループを用いた具体例を示します。以下の例では、リストを String[] に変換しています。

List<String> list = Arrays.asList("hello", "world");
var array = new String[list.size()];
var index = 0;
for (var element : list) {
    array[index++] = element;
}
Javaにおける型推論の変遷
Javaのラムダ式は、Java SE 8で導入され、関数型プログラミングの要素をJavaに取り入れる重要な機能として進化しました。その後のJavaバージョンでの関連機能の改善や拡張を含め、以下に変遷をまとめます。
  1. Java SE 8(2014年):ラムダ式の導入
    背景
    • 従来の匿名クラスを簡潔に記述する手段としてラムダ式が導入されました。
    • 関数型インターフェース(1つの抽象メソッドを持つインターフェース)を基にした記述が可能に。
    基本構文
      (引数) -> { 処理 }
    
    従来の匿名クラス
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello, World!");
        }
    };
    
    ラムダ式
    Runnable r = () -> System.out.println("Hello, World!");
    
    標準ライブラリへの影響
    • java.util.function パッケージに豊富な関数型インターフェース(Predicate, Consumer, Supplier, Functionなど)が追加されました。
      Stream API でラムダ式を活用したデータ操作が可能に
      List<String> list = Arrays.asList("a", "b", "c");
      list.stream()
          .filter(s -> s.startsWith("a"))
          .forEach(System.out::println);
      
  2. Java SE 9(2017年):小規模な改良
    改良点
    • 型推論やラムダ式の適用範囲に直接影響はないものの、Java SE 9ではStream APIに新機能が追加され、ラムダ式の適用例が増加しました。
    StreamtakeWhiledropWhileメソッドの導入。
    List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
    numbers.stream()
           .takeWhile(n -> n < 4)
           .forEach(System.out::println); // 1, 2, 3
    
    匿名クラスの型推論改善
    • 匿名クラスの中でジェネリクス型を省略可能に(ダイヤモンド演算子の改善)。
  3. Java SE 10(2018年):varキーワードの導入
    ラムダ式内のローカル変数型推論
    • varを利用してローカル変数の型推論が可能に。
    var list = List.of(1, 2, 3);
    list.forEach(n -> System.out.println(n));
    
    ラムダ式の影響は限定的
    • ラムダ式自体に新機能は追加されませんでしたが、varによる型推論の利用によりコードが簡潔に記述可能になりました。
  4. Java SE 11(2018年):varのラムダ引数への適用
    拡張内容
    • ラムダ式の引数でvarを使用可能に。
    List<String> list = List.of("a", "b", "c");
    list.forEach((var item) -> System.out.println(item));
    
    メリット
    • 型推論と注釈(例えば、@NonNull)の組み合わせが可能になりました。
    list.forEach((@NonNull var item) -> System.out.println(item));
    
    注意点
    • ラムダ式の引数全体で一貫してvarを使用する必要があります。
    (var a, b) -> { ... } // コンパイルエラー
    
  5. Java SE 17(2021年):ラムダ式とレコード型
    背景
    • Java SE 14で導入されたレコード型(record)は、関数型プログラミングとの相性が良いデータ構造。
    • ラムダ式を使用したコードでレコード型が使いやすくなりました。
    record Point(int x, int y) {}
    
    List<Point> points = List.of(new Point(1, 2), new Point(3, 4));
    points.stream()
          .filter(p -> p.x > 2)
          .forEach(System.out::println);
    
  6. Java SE 21(2023年):関数型プログラミングの強化
    シーリングクラスとパターンマッチングの改良
    • パターンマッチングや型チェックでラムダ式の利用がさらに直感的になりました。
    例(パターンマッチングとラムダ式の併用)
    Stream.of(1, "text", 3.0)
          .map(o -> switch (o) {
              case Integer i -> i * 2;
              case String s -> s.length();
              case Double d -> d.intValue();
              default -> 0;
          })
          .forEach(System.out::println);
    
ラムダ式の利点と限界
利点
  • 匿名クラスより簡潔で読みやすい。
  • スレッドやイベント処理などの非同期プログラミングに適している。
  • Stream APIとの組み合わせで高い生産性を発揮。
限界
  • 過度な使用はコードの可読性を低下させる可能性がある。
  • 直感的でない場合や複雑な処理では依然として従来の構文が適している。
Javaのラムダ式は、Java SE 8での導入以降、さまざまな改良や拡張を経てきました。特に、型推論の向上や新しいAPIの追加により、簡潔で直感的なコードが書きやすくなっています。これにより、Javaがよりモダンなプログラミングスタイルを取り入れ、他の関数型言語とのギャップを縮めています。

附録

[編集]

チートシート

[編集]
public class Main {
    public static void main(String[] args) {
        // 変数の宣言と初期化
        int num1 = 10;
        int num2 = 20;
        String name = "John";
        boolean flag = true;
        
        // 変数の使用と演算
        System.out.println("num1 + num2 = " + (num1 + num2)); // 30
        System.out.println("My name is " + name); // My name is John
        System.out.println("flag is " + flag); // flag is true
        
        // プリミティブ型と参照型
        int[] array = {1, 2, 3}; // 参照型の例
        char ch = 'a'; // プリミティブ型の例
        
        // プリミティブ型と参照型の違い
        int num3 = num1; // num1の値をコピーしてnum3に代入(値渡し)
        int[] array2 = array; // arrayの参照をコピーしてarray2に代入(参照渡し)
        array[0] = 100; // arrayの値を変更
        
        System.out.println("num3 = " + num3); // num3 = 10
        System.out.println("array[0] = " + array[0]); // array[0] = 100
        System.out.println("array2[0] = " + array2[0]); // array2[0] = 100
        
        // プリミティブ型と参照型の比較
        boolean isEqual = num1 == num3; // プリミティブ型同士の比較
        boolean isSameArray = array == array2; // 参照型同士の比較
        
        System.out.println("num1 == num3 : " + isEqual); // num1 == num3 : true
        System.out.println("array == array2 : " + isSameArray); // array == array2 : true
    }
}

用語集

[編集]
  1. 変数 (Variable): プログラミングにおける変数は、値やデータを格納するための記号的な名前です。プログラム内でデータを操作したり処理したりする際に使用されます。
  2. 宣言 (Declaration): 変数の宣言とは、変数をプログラム内で使用する準備をすることです。変数の型と名前を指定して、コンピュータにその変数を識別するための領域を割り当てます。
  3. 代入 (Assignment): 代入とは、変数に値を格納する操作です。変数に値を代入することで、その変数がその値を保持することができます。
  4. 初期化 (Initialization): 初期化とは、変数に初期値を設定する操作です。変数を宣言と同時に初期値を設定することで、その変数が宣言された時点で値を持つことができます。
  5. 再代入 (Reassignment): 再代入とは、変数に新しい値を割り当てる操作です。変数には、同じ型の新しい値を代入することができます。
  6. イミュータブル (Immutable): イミュータブルとは、変更不可能なことを指します。Javaでは、final修飾子を使用して宣言された変数や、文字列などの一部のオブジェクトはイミュータブルです。
  7. キャスト (Cast): キャストとは、データ型を変換する操作です。Javaでは、異なるデータ型の間で変換を行う際にキャストが使用されます。
  8. 型推論 (Type Inference): 型推論とは、コンパイラがコードの文脈から変数の型を推測する機能です。Java 10以降では、varキーワードを使用して型推論を行うことができます。
  9. ラムダ式 (Lambda Expression): ラムダ式とは、無名関数を表現するための記法です。Javaでは、関数型インターフェースの実装として使用されます。
  10. ループ変数 (Loop Variable): ループ変数とは、forループや拡張forループで使用される反復変数のことを指します。Java 10以降では、ループ変数の型推論機能が追加されました。
  11. 複雑な型推論 (Complex Type Inference): 複雑な型を返すメソッドを受け取る場合にも、型推論が有用です。Javaでは、flatMap()メソッドなどでの複雑な型推論が行われます。