信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
Java配列の全て ─ 仕組み・メモリ・共変性・試験の急所まで完全理解
Javaの配列をゼロから完全解説。JVMメモリモデル、オブジェクトとしての正体、共変性の罠、多次元配列、Arraysユーティリティ、Silver試験頻出パターンまで網羅。
一言結論
Java の配列は『固定長・型安全・参照型オブジェクト』。C言語のポインタ演算を排除しつつハードウェアに近いパフォーマンスを実現するために設計された、Java最古のデータ構造である。
Java配列の全て ─ 仕組み・メモリ・共変性・試験の急所まで完全理解
この記事を読めば: 配列の宣言・初期化から、JVMがメモリ上でどう配置するか、
Objectを継承するとはどういうことか、共変性がなぜ危険か、Silver試験でどう問われるかまで、一本の線でつながる。
なぜ配列が存在するのか ─ 歴史と設計意図
C/C++ からの遺産
Java は 1995 年に Sun Microsystems の James Gosling が設計した言語だ。当時のプログラマの多くは C/C++ を使っていた。C の配列は「連続したメモリ領域へのポインタ」であり、高速だが危険だった。
// C言語の配列 ─ 境界チェックがない
int arr[3] = {10, 20, 30};
arr[5] = 99; // 範囲外アクセス ─ コンパイルも通るし、実行時に何が起きるか分からない
C ではこの「範囲外アクセス」がバッファオーバーフロー攻撃の温床になった。Java の設計者はこの問題を解決するために、配列に3つの制約を入れた。
- 固定長: 生成時にサイズが決まり、後から変更できない
- 型安全:
int[]にはintしか入れられない - 境界チェック: 範囲外アクセスは必ず
ArrayIndexOutOfBoundsExceptionで止まる
この3つにより、C の配列の速度を可能な限り残しつつ、安全性を確保した。配列はJavaで最も原始的で、最もハードウェアに近いデータ構造だ。
なぜ固定長なのか
配列はメモリ上に連続した領域として確保される。これが高速なランダムアクセス(O(1))を可能にする理由だ。
メモリのイメージ(int[] arr = {10, 20, 30}):
アドレス 値
0x1000 10 ← arr[0]
0x1004 20 ← arr[1](intは4バイトなので +4)
0x1008 30 ← arr[2](さらに +4)
arr[i] のアドレスは「先頭アドレス + i × 要素サイズ」で一発計算できる。途中に要素を挿入したり、サイズを変えたりすると、この連続性が壊れる。だから配列は固定長なのだ。
配列は「オブジェクト」である ─ 参照型としての正体
Java の型の2大分類をおさらい
Java のすべての値は「プリミティブ型(8種)」か「参照型」のどちらか。配列は参照型に属する。
int x = 42; // プリミティブ型 ─ 変数に値そのものが入る
int[] arr = {1, 2}; // 参照型 ─ 変数にはオブジェクトのアドレスが入る
配列は Object のサブクラス
意外に思うかもしれないが、Java の配列はすべて java.lang.Object を継承したオブジェクトだ。クラスファイルには int[] というクラス定義は存在しないが、JVMが実行時に配列クラスを自動生成する。
int[] nums = {1, 2, 3};
// Object のメソッドが使える
System.out.println(nums.getClass().getName()); // "[I"(int配列の内部名)
System.out.println(nums.getClass().getSuperclass()); // class java.lang.Object
System.out.println(nums instanceof Object); // true
// toString() は Object のデフォルト実装(ハッシュ値)
System.out.println(nums.toString()); // "[I@1b6d3586" のような文字列
System.out.println(nums); // 同じ ─ println が内部で toString() を呼ぶ
// 中身を表示したいなら Arrays.toString() を使う
System.out.println(Arrays.toString(nums)); // "[1, 2, 3]"
JVMの内部名称:
| 配列の型 | 内部名 | 意味 |
|---|---|---|
int[] | [I | [ = 配列、I = int |
double[] | [D | D = double |
String[] | [Ljava.lang.String; | L = 参照型 |
int[][] | [[I | [ が2つ = 2次元配列 |
この内部名は JVM仕様(JVMS §4.3.2)で定義されている。試験で直接問われることは少ないが、「配列はオブジェクトである」という理解の裏付けになる。
length はフィールドであってメソッドではない
int[] arr = {10, 20, 30};
// ✅ フィールドアクセス(括弧なし)
System.out.println(arr.length); // 3
// ❌ メソッド呼び出し(括弧あり)── コンパイルエラー
// System.out.println(arr.length()); // エラー!
// 比較: String の length() はメソッド
String s = "hello";
System.out.println(s.length()); // 5(括弧あり)
なぜフィールドなのか? 配列の長さは生成時に決まって二度と変わらない。JVMは配列オブジェクトのヘッダに長さ情報を埋め込んでおり、メソッド呼び出しのオーバーヘッドなしに直接読める。パフォーマンスのための設計判断だ。
Silver試験の定番: 「
arr.length()とarr.lengthのどちらが正しいか」は頻出。配列は.length(フィールド)、String は.length()(メソッド)、コレクションは.size()(メソッド)。この3つの違いを混同させる問題が出る。
宣言・生成・初期化 ─ 3段階を区別する
配列の利用には「宣言」「生成」「初期化」の3つのステップがある。これを混同するとコンパイルエラーや NullPointerException の原因になる。
宣言 ─ 変数を作るだけ
int[] a; // 推奨スタイル(型に [] を付ける)
int b[]; // C言語スタイル(変数名に [] を付ける)── コンパイルは通るが非推奨
int[] c, d; // c も d も int[] 型
int e[], f; // e は int[] 型、f は int 型 ── 混乱の元!
試験の罠:
int e[], f;でfがint型になることを問う問題がある。[]を変数名に付けるスタイルを使うとこの罠にハマる。だから型に付けるint[]スタイルが推奨される。
宣言しただけでは配列オブジェクトは存在しない。変数はまだ何も指していない。
int[] a;
// System.out.println(a.length); // コンパイルエラー!ローカル変数は初期化が必要
// System.out.println(a[0]); // 同上
生成 ─ new でヒープにオブジェクトを作る
int[] a = new int[5]; // 長さ5の int 配列をヒープに生成
このとき、JVMは以下を行う。
- ヒープメモリに
int × 5 = 20バイト+ ヘッダ分の連続領域を確保 - 全要素をデフォルト値(
intなら0)で埋める - そのオブジェクトへの参照を変数
aに格納
初期化 ─ 値を入れる
// 方法1: new で生成した後に1つずつ代入
int[] a = new int[3];
a[0] = 10;
a[1] = 20;
a[2] = 30;
// 方法2: 初期化リスト(宣言と同時にのみ使える)
int[] b = {10, 20, 30};
// 方法3: 無名配列(メソッド引数など、宣言と分離した場面で使う)
int[] c;
c = new int[]{10, 20, 30}; // OK
// c = {10, 20, 30}; // ❌ コンパイルエラー!宣言と同時でないと {} は使えない
試験の罠:
c = {10, 20, 30}が通らないことを問う問題は頻出。{}構文(配列初期化子)は変数宣言文の中でしか使えない。
デフォルト値 ─ 配列を new で作ったら何が入るか
| 要素の型 | デフォルト値 |
|---|---|
byte, short, int, long | 0 |
float, double | 0.0 |
char | '�'(null文字、表示されない) |
boolean | false |
参照型(String, クラス, 配列など) | null |
String[] names = new String[3];
System.out.println(names[0]); // null
// System.out.println(names[0].length()); // ❌ NullPointerException!
この「参照型配列のデフォルトが null」は初心者が最もハマるポイントの一つ。配列を new しただけでは中身のオブジェクトは生成されない。箱は用意されたが、中は空っぽだ。
メモリモデル ─ スタックとヒープで何が起きているか
配列を理解するには、JVMのメモリモデルを知る必要がある。
スタック vs ヒープ
┌─────────────────┐ ┌──────────────────────────┐
│ スタック │ │ ヒープ │
│ (変数の箱) │ │ (オブジェクトの実体) │
├─────────────────┤ ├──────────────────────────┤
│ arr ─────────────┼────→│ [10] [20] [30] │
│ │ │ 配列オブジェクト │
│ x = 42 │ │ length = 3 │
└─────────────────┘ └──────────────────────────┘
- スタック: メソッドのローカル変数が置かれる。配列変数
arrはここにある。中身はアドレス(参照)。 - ヒープ:
newで生成されたオブジェクトが置かれる。配列の実体(要素データ + lengthフィールド)はここにある。
代入は「参照のコピー」
int[] a = {1, 2, 3};
int[] b = a; // b は a と同じオブジェクトを指す
b[0] = 99;
System.out.println(a[0]); // 99 ── a も影響を受ける!
スタック ヒープ
┌───────┐ ┌──────────────┐
│ a ────┼──┬─→│ [99] [2] [3] │
│ b ────┼──┘ └──────────────┘
└───────┘
a と b は同じ実体を指す2枚のカード。カードをコピーしても、カードが指す先は1つ。
配列の中身をコピーする方法
同じ実体を指すのではなく、別の配列として独立させたい場合はコピーが必要。
int[] src = {1, 2, 3};
// 方法1: Arrays.copyOf(最も一般的)
int[] copy1 = Arrays.copyOf(src, src.length);
// 方法2: Arrays.copyOfRange(範囲指定)
int[] copy2 = Arrays.copyOfRange(src, 0, 2); // [1, 2]
// 方法3: System.arraycopy(低レベル、高速)
int[] copy3 = new int[src.length];
System.arraycopy(src, 0, copy3, 0, src.length);
// 方法4: clone()(配列は Cloneable を実装している)
int[] copy4 = src.clone();
// どの方法でも、コピー後は独立した別オブジェクト
copy1[0] = 99;
System.out.println(src[0]); // 1(影響なし)
注意: 参照型配列の「浅いコピー」
StringBuilder[] orig = {new StringBuilder("A"), new StringBuilder("B")};
StringBuilder[] copy = orig.clone();
// 配列自体は別物だが、中身のオブジェクトは同じ実体を指す(浅いコピー)
copy[0].append("!");
System.out.println(orig[0]); // "A!" ── orig にも影響!
// 配列の要素(参照)を差し替えれば独立する
copy[0] = new StringBuilder("Z");
System.out.println(orig[0]); // "A!"(影響なし)
プリミティブ型配列なら clone() で完全コピーになるが、参照型配列は「配列の枠はコピーされるが、中身のオブジェクトは共有」という浅いコピーになる。
共変性(Covariance)─ Java配列の設計ミス
配列は共変
「共変(covariant)」とは、Dog extends Animal なら Dog[] extends Animal[] も成り立つという性質だ。
// Dog は Animal のサブクラスとする
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
Dog[] dogs = {new Dog(), new Dog()};
Animal[] animals = dogs; // ✅ コンパイル通る ── 配列は共変だから
// ところが……
animals[0] = new Cat(); // ✅ コンパイルは通る(Cat は Animal だから)
// ❌ 実行時 ArrayStoreException!(実体は Dog[] なのに Cat を入れようとした)
なぜこの設計ミスが起きたか
Java 1.0(1996年)にはジェネリクスが存在しなかった。配列に多態性がないと、汎用的なソートメソッドすら書けなかった。
// ジェネリクスがない時代、こう書きたかった
public static void sort(Object[] arr) {
// 配列の要素を比較してソート
}
// String[] も Integer[] も渡したい
sort(new String[]{"B", "A"}); // String[] → Object[] への代入が必要
sort(new Integer[]{3, 1, 2}); // Integer[] → Object[] への代入が必要
配列が共変でなければ、String[] を Object[] に渡せない。だから「コンパイル時は許可して、型が合わなければ実行時に例外」という設計にした。Java 5(2004年)でジェネリクスが入り、この問題はコレクション側では解決された(List<Dog> は List<Animal> に代入できない = 不変)。
試験では:
ArrayStoreExceptionが起きるコードを見せて「何が出力されるか」を問うパターンがある。「コンパイルは通るが実行時に落ちる」タイプの問題だ。
多次元配列 ─ 「配列の配列」という真実
Java に真の多次元配列は存在しない
C# や Fortran には「2次元配列」というネイティブな概念がある。Java にはない。Java の int[][] は「int[] を要素とする配列」、つまり「配列の配列」だ。
int[][] matrix = new int[3][4];
このコードが作るのは以下の構造。
スタック ヒープ
┌──────────┐ ┌───────────────┐
│ matrix ──┼───→│ [ref0] │ ← 長さ3の配列(要素は int[] への参照)
│ │ │ [ref1] │
│ │ │ [ref2] │
│ │ └───┬───┬───┬───┘
│ │ ↓ ↓ ↓
│ │ ┌──────┐ ┌──────┐ ┌──────┐
│ │ │0 0 0 0│ │0 0 0 0│ │0 0 0 0│ ← 各行は独立した int[4]
│ │ └──────┘ └──────┘ └──────┘
└──────────┘
外側の配列(長さ3)の各要素が、内側の配列(長さ4)への参照を持っている。合計4つの配列オブジェクトが作られる。
ジャグ配列 ─ 行ごとに長さが違う配列
「配列の配列」だからこそ、各行の長さを変えられる。
int[][] jagged = new int[3][]; // 外側だけ生成(内側はまだ null)
jagged[0] = new int[2]; // 1行目: 2要素
jagged[1] = new int[5]; // 2行目: 5要素
jagged[2] = new int[1]; // 3行目: 1要素
System.out.println(jagged.length); // 3(外側の長さ)
System.out.println(jagged[1].length); // 5(2行目の長さ)
// System.out.println(jagged[0][3]); // ❌ ArrayIndexOutOfBoundsException(1行目は2要素)
試験の罠:
int[][] arr = new int[3][]の直後にarr[0][0]にアクセスすると NullPointerException。内側の配列がまだ生成されていない(null)からだ。
多次元配列の初期化リスト
int[][] grid = {
{1, 2, 3},
{4, 5, 6}
};
// ジャグ配列の初期化リスト
int[][] jagged = {
{1},
{2, 3},
{4, 5, 6}
};
length の使い分け
int[][] matrix = new int[3][4];
System.out.println(matrix.length); // 3(行数 = 外側の配列の長さ)
System.out.println(matrix[0].length); // 4(列数 = 内側の配列の長さ)
for-each ループと配列
基本的な使い方
int[] nums = {10, 20, 30};
// 従来の for ループ
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
// for-each(拡張 for 文)── インデックスが不要なとき
for (int n : nums) {
System.out.println(n);
}
for-each の制約
int[] nums = {1, 2, 3};
// ❌ for-each ではインデックスにアクセスできない
for (int n : nums) {
// n はコピーされた値。元の配列を書き換えることはできない
n = 99; // nums には影響しない
}
System.out.println(Arrays.toString(nums)); // [1, 2, 3](変わらない)
for-each のループ変数はコピーなので、プリミティブ型配列の場合、ループ変数を書き換えても元の配列には反映されない。
多次元配列の for-each
int[][] matrix = {{1, 2}, {3, 4}, {5, 6}};
for (int[] row : matrix) { // 外側: 各行(int[] 型)
for (int cell : row) { // 内側: 各要素(int 型)
System.out.print(cell + " ");
}
System.out.println();
}
// 出力:
// 1 2
// 3 4
// 5 6
Arrays クラス ─ 配列操作のスイスアーミーナイフ
java.util.Arrays は配列を操作する static メソッドの集合。配列自体にはほとんどメソッドがないため、このクラスが補完する。
ソート
int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr); // 元の配列を直接変更する(in-place ソート)
System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
// 範囲指定ソート
int[] arr2 = {5, 3, 1, 4, 2};
Arrays.sort(arr2, 1, 4); // インデックス 1〜3 の範囲だけソート
System.out.println(Arrays.toString(arr2)); // [5, 1, 3, 4, 2]
// 参照型はカスタム Comparator を渡せる
String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words, Comparator.reverseOrder()); // 降順
System.out.println(Arrays.toString(words)); // [cherry, banana, apple]
二分探索
int[] sorted = {1, 2, 3, 4, 5};
int idx = Arrays.binarySearch(sorted, 3); // 2(見つかった位置)
int missing = Arrays.binarySearch(sorted, 6); // 負の値: -(挿入位置)-1 = -6
前提条件: 配列がソート済みであること。ソートされていない配列に対して使うと結果は不定。
試験の罠: ソートせずに
binarySearchを呼ぶコードが出題される。「結果は不定」が正解。
比較
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = a;
System.out.println(a == b); // false(別オブジェクト)
System.out.println(a == c); // true(同じオブジェクト)
System.out.println(Arrays.equals(a, b)); // true(内容が等しい)
// 多次元配列の比較
int[][] m1 = {{1, 2}, {3, 4}};
int[][] m2 = {{1, 2}, {3, 4}};
System.out.println(Arrays.equals(m1, m2)); // false!(内側の配列の参照が異なる)
System.out.println(Arrays.deepEquals(m1, m2)); // true(中身まで再帰的に比較)
// 表示も同様
System.out.println(Arrays.toString(m1)); // "[[I@xxx, [I@yyy]"(内側は参照表示)
System.out.println(Arrays.deepToString(m1)); // "[[1, 2], [3, 4]]"(中身まで表示)
試験の罠: 多次元配列に
Arrays.equals()を使うとfalseになる。deepEquals()を使わないと中身まで比較されない。
その他の便利メソッド
// fill: 全要素を同じ値で埋める
int[] arr = new int[5];
Arrays.fill(arr, 7);
System.out.println(Arrays.toString(arr)); // [7, 7, 7, 7, 7]
// copyOf: 新しい配列にコピー(長さ変更可)
int[] original = {1, 2, 3};
int[] longer = Arrays.copyOf(original, 5); // [1, 2, 3, 0, 0](余りは0で埋まる)
int[] shorter = Arrays.copyOf(original, 2); // [1, 2]
// copyOfRange: 範囲指定コピー
int[] range = Arrays.copyOfRange(original, 1, 3); // [2, 3](開始含む、終了含まない)
// mismatch (Java 9+): 最初に異なるインデックスを返す
int[] x = {1, 2, 3, 4};
int[] y = {1, 2, 5, 4};
System.out.println(Arrays.mismatch(x, y)); // 2(インデックス2で初めて異なる)
配列とメソッド ─ 引数と戻り値
配列をメソッドに渡すと「参照渡し」のように振る舞う
正確には「参照の値渡し」だが、実務上は「元の配列を直接操作できる」と覚えてよい。
static void doubleAll(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] *= 2;
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
doubleAll(nums);
System.out.println(Arrays.toString(nums)); // [2, 4, 6] ── 元の配列が変わっている
}
可変長引数(varargs)は配列の糖衣構文
static int sum(int... nums) {
// nums は int[] 型として扱える
int total = 0;
for (int n : nums) total += n;
return total;
}
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(new int[]{4, 5})); // 9(配列を直接渡すことも可能)
int... nums は内部的に int[] nums と同じ。コンパイラが呼び出し側の引数を配列に変換する。
試験の罠: 可変長引数はメソッドの最後のパラメータにしか置けない。
void foo(int... a, String b)はコンパイルエラー。
配列とコレクションの橋渡し
配列 → List
String[] arr = {"A", "B", "C"};
// Arrays.asList: 固定サイズ List(元の配列と連動する)
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "Z"); // OK ── arr[0] も "Z" に変わる
// fixed.add("D"); // ❌ UnsupportedOperationException
// List.of (Java 9+): 完全不変 List
List<String> immutable = List.of(arr);
// immutable.set(0, "X"); // ❌ UnsupportedOperationException
// 可変 List にしたい場合
List<String> mutable = new ArrayList<>(Arrays.asList(arr));
mutable.add("D"); // OK
List → 配列
List<String> list = List.of("X", "Y", "Z");
// toArray: 配列に変換
String[] arr1 = list.toArray(new String[0]); // 推奨パターン(サイズ0の配列を渡す)
String[] arr2 = list.toArray(String[]::new); // Java 11+ のメソッド参照パターン
// 引数なし toArray() は Object[] を返す
Object[] objArr = list.toArray();
// String[] bad = (String[]) list.toArray(); // ❌ ClassCastException!
toArray(new String[0])のサイズが0でよい理由: 内部で十分なサイズの配列を新規作成するため、渡す配列のサイズは関係ない。実はサイズ0の方がJVMの最適化が効いてわずかに高速という測定結果もある。
Silver試験の急所 ─ 配列で問われるパターン
パターン1: 宣言スタイルの罠
// 何が int[] で何が int か?
int[] a, b; // a → int[], b → int[]
int c[], d; // c → int[], d → int(!)
パターン2: 初期化の可否
int[] a = {1, 2, 3}; // ✅
int[] b;
b = {1, 2, 3}; // ❌ コンパイルエラー
b = new int[]{1, 2, 3}; // ✅
パターン3: 範囲外アクセス
int[] arr = new int[3];
System.out.println(arr[3]); // ❌ ArrayIndexOutOfBoundsException(有効インデックスは 0〜2)
パターン4: length の使い方
int[] arr = {1, 2, 3};
System.out.println(arr.length); // ✅ 3
// System.out.println(arr.length()); // ❌ コンパイルエラー
パターン5: 配列の比較
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println(a == b); // false
System.out.println(a.equals(b)); // false(Object.equals = 参照比較)
System.out.println(Arrays.equals(a, b)); // true
a.equals(b) が false になるのは、配列が equals() をオーバーライドしていないから。Object のデフォルト実装(== と同じ参照比較)が使われる。
パターン6: 共変性と ArrayStoreException
Object[] objs = new String[3]; // コンパイル通る
objs[0] = "hello"; // OK
objs[1] = 42; // ❌ ArrayStoreException(実体は String[])
パターン7: 多次元配列の null
int[][] arr = new int[3][];
System.out.println(arr[0]); // null
// System.out.println(arr[0][0]); // ❌ NullPointerException
まとめ
| 特性 | 配列 |
|---|---|
| サイズ | 固定(生成時に決定、変更不可) |
| 型 | 参照型(Object のサブクラス) |
| パフォーマンス | ランダムアクセス O(1)、最速のデータ構造 |
| 型安全 | コンパイル時チェック + 実行時チェック(共変性による例外あり) |
| ジェネリクス | 使えない(new T[] は不可) |
| プリミティブ格納 | 可能(int[] 等。コレクションにはない利点) |
| サイズ変更 | 不可。必要なら Arrays.copyOf で新配列を作る |
| 長さの取得 | .length(フィールド。括弧なし) |
| 内容の比較 | Arrays.equals() / Arrays.deepEquals() |
| 文字列化 | Arrays.toString() / Arrays.deepToString() |
配列は Java の最も基本的なデータ構造であり、コレクションフレームワーク(ArrayList など)の内部実装でも使われている。配列を理解することは、Java のメモリモデルとオブジェクト指向の土台を理解することに等しい。