10日で覚えるJavaDay 9: コレクションとジェネリクス
books.chapter 910日で覚えるJava

Day 9: コレクションとジェネリクス

今日学ぶこと

  • List(ArrayList, LinkedList)
  • Set(HashSet, TreeSet)
  • Map(HashMap, TreeMap)
  • ジェネリクス
  • Stream API の基礎

コレクションフレームワーク

Javaのコレクションフレームワークは、データの集まりを扱う統一されたAPIです。

flowchart TB
    Collection["«interface»<br>Collection"]
    List["«interface»<br>List"]
    Set["«interface»<br>Set"]
    Map["«interface»<br>Map"]
    ArrayList["ArrayList"]
    LinkedList["LinkedList"]
    HashSet["HashSet"]
    TreeSet["TreeSet"]
    HashMap["HashMap"]
    TreeMap["TreeMap"]
    Collection --> List
    Collection --> Set
    List --> ArrayList
    List --> LinkedList
    Set --> HashSet
    Set --> TreeSet
    Map --> HashMap
    Map --> TreeMap
    style Collection fill:#3b82f6,color:#fff
    style List fill:#22c55e,color:#fff
    style Set fill:#f59e0b,color:#fff
    style Map fill:#8b5cf6,color:#fff

List

順序あり・重複OKのコレクションです。

ArrayList

import java.util.ArrayList;
import java.util.List;

// 作成
List<String> fruits = new ArrayList<>();

// 追加
fruits.add("りんご");
fruits.add("みかん");
fruits.add("ぶどう");
fruits.add("りんご"); // 重複OK

// アクセス
fruits.get(0)         // "りんご"
fruits.size()         // 4
fruits.contains("みかん") // true

// 変更・削除
fruits.set(1, "バナナ");  // インデックス1を変更
fruits.remove("ぶどう");   // 値で削除
fruits.remove(0);          // インデックスで削除

// ループ
for (String fruit : fruits) {
    System.out.println(fruit);
}

不変リスト(Java 9+)

// 変更不可のリスト
List<String> colors = List.of("赤", "青", "緑");
// colors.add("黄"); // ❌ UnsupportedOperationException

主要メソッド

メソッド 説明
add(e) 末尾に追加
add(i, e) 指定位置に挿入
get(i) 取得
set(i, e) 置換
remove(i) / remove(e) 削除
size() 要素数
contains(e) 含むか
indexOf(e) インデックス検索
isEmpty() 空か
sort(comparator) ソート

ArrayList vs LinkedList

特徴 ArrayList LinkedList
ランダムアクセス ✅ 高速 O(1) ❌ 低速 O(n)
先頭への挿入・削除 ❌ 低速 O(n) ✅ 高速 O(1)
メモリ効率 ✅ 良い ❌ オーバーヘッド大
推奨度 通常はこちら 特殊な場合のみ

Set

順序なし・重複NGのコレクションです。

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

// HashSet(順序なし)
Set<String> hashSet = new HashSet<>();
hashSet.add("Java");
hashSet.add("Python");
hashSet.add("Java");    // 重複は無視
System.out.println(hashSet.size()); // 2

// TreeSet(ソート済み)
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(30);
treeSet.add(10);
treeSet.add(20);
System.out.println(treeSet); // [10, 20, 30]

// 不変Set(Java 9+)
Set<String> immutable = Set.of("A", "B", "C");

集合演算

Set<Integer> a = new HashSet<>(Set.of(1, 2, 3, 4));
Set<Integer> b = new HashSet<>(Set.of(3, 4, 5, 6));

// 和集合
Set<Integer> union = new HashSet<>(a);
union.addAll(b);          // {1, 2, 3, 4, 5, 6}

// 積集合(共通部分)
Set<Integer> intersection = new HashSet<>(a);
intersection.retainAll(b); // {3, 4}

// 差集合
Set<Integer> diff = new HashSet<>(a);
diff.removeAll(b);         // {1, 2}

Map

キーと値のペアを格納するコレクションです。

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

// HashMap
Map<String, Integer> scores = new HashMap<>();
scores.put("太郎", 85);
scores.put("花子", 92);
scores.put("次郎", 78);

// アクセス
scores.get("太郎")           // 85
scores.getOrDefault("美咲", 0) // 0(キーがない場合のデフォルト)
scores.containsKey("花子")    // true
scores.size()                 // 3

// 更新・削除
scores.put("太郎", 90);       // 上書き
scores.remove("次郎");        // 削除

// ループ
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.printf("%s: %d点%n", entry.getKey(), entry.getValue());
}

// キーだけ / 値だけ
scores.keySet()   // [太郎, 花子]
scores.values()   // [90, 92]

不変Map(Java 9+)

Map<String, Integer> map = Map.of(
    "one", 1,
    "two", 2,
    "three", 3
);

便利メソッド

// putIfAbsent: キーが存在しない場合のみ追加
scores.putIfAbsent("美咲", 88);

// computeIfAbsent: キーが存在しない場合に計算して追加
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("A", k -> new ArrayList<>()).add("太郎");

// merge: 値のマージ
Map<String, Integer> wordCount = new HashMap<>();
for (String word : words) {
    wordCount.merge(word, 1, Integer::sum);
}

ジェネリクス

型をパラメータ化して、型安全なコードを書けます。

// ジェネリクスなし(危険)
List rawList = new ArrayList();
rawList.add("Hello");
rawList.add(123);
String s = (String) rawList.get(1); // ClassCastException!

// ジェネリクスあり(安全)
List<String> safeList = new ArrayList<>();
safeList.add("Hello");
// safeList.add(123); // ❌ コンパイルエラー
String s = safeList.get(0); // キャスト不要

ジェネリッククラス

class Pair<A, B> {
    private final A first;
    private final B second;

    Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }

    A getFirst() { return first; }
    B getSecond() { return second; }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

// 使用
Pair<String, Integer> nameAge = new Pair<>("太郎", 25);
System.out.println(nameAge); // (太郎, 25)

ジェネリックメソッド

static <T> T getFirst(List<T> list) {
    if (list.isEmpty()) {
        throw new IllegalArgumentException("リストが空です");
    }
    return list.get(0);
}

// 使用
String first = getFirst(List.of("A", "B", "C")); // "A"
Integer num = getFirst(List.of(1, 2, 3));          // 1

境界型パラメータ

// T は Comparable を実装していなければならない
static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

max(3, 7)         // 7
max("apple", "banana") // "banana"

Stream API の基礎

コレクションのデータ処理を宣言的に書けます。

import java.util.List;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// フィルタ + 変換 + 集約
List<Integer> evenDoubled = numbers.stream()
    .filter(n -> n % 2 == 0)        // 偶数を抽出
    .map(n -> n * 2)                 // 2倍にする
    .toList();                       // リストに変換
// [4, 8, 12, 16, 20]

// 合計
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum(); // 55

// 文字列結合
List<String> names = List.of("太郎", "花子", "次郎");
String joined = names.stream()
    .collect(Collectors.joining(", "));
// "太郎, 花子, 次郎"
flowchart LR
    Source["ソース<br>[1,2,3,4,5]"]
    Filter["filter<br>偶数のみ"]
    Map["map<br>2倍"]
    Collect["toList<br>リスト化"]
    Source --> Filter --> Map --> Collect
    style Source fill:#3b82f6,color:#fff
    style Filter fill:#f59e0b,color:#fff
    style Map fill:#22c55e,color:#fff
    style Collect fill:#8b5cf6,color:#fff

よく使うStream操作

操作 種類 説明
filter() 中間 条件でフィルタ
map() 中間 要素を変換
sorted() 中間 ソート
distinct() 中間 重複除去
limit() 中間 件数制限
toList() 終端 リストに変換
forEach() 終端 各要素に処理
count() 終端 件数
reduce() 終端 集約

実践: 生徒管理システム

import java.util.*;
import java.util.stream.Collectors;

record Student(String name, int score, String subject) {}

public class StudentManager {
    public static void main(String[] args) {
        List<Student> students = List.of(
            new Student("太郎", 85, "数学"),
            new Student("花子", 92, "英語"),
            new Student("次郎", 78, "数学"),
            new Student("美咲", 95, "英語"),
            new Student("健太", 67, "数学"),
            new Student("さくら", 88, "英語")
        );

        // 平均点
        double avg = students.stream()
            .mapToInt(Student::score)
            .average()
            .orElse(0);
        System.out.printf("全体平均: %.1f点%n", avg);

        // 科目別平均
        Map<String, Double> subjectAvg = students.stream()
            .collect(Collectors.groupingBy(
                Student::subject,
                Collectors.averagingInt(Student::score)
            ));
        subjectAvg.forEach((subject, average) ->
            System.out.printf("%s平均: %.1f点%n", subject, average));

        // 成績上位3名
        System.out.println("\n=== 成績上位3名 ===");
        students.stream()
            .sorted(Comparator.comparingInt(Student::score).reversed())
            .limit(3)
            .forEach(s -> System.out.printf("%s: %d点%n", s.name(), s.score()));

        // 80点以上の名前リスト
        List<String> highScorers = students.stream()
            .filter(s -> s.score() >= 80)
            .map(Student::name)
            .toList();
        System.out.println("\n80点以上: " + highScorers);

        // 科目別グループ化
        Map<String, List<Student>> bySubject = students.stream()
            .collect(Collectors.groupingBy(Student::subject));
        bySubject.forEach((subject, list) -> {
            System.out.printf("\n%s: ", subject);
            list.forEach(s -> System.out.printf("%s(%d) ", s.name(), s.score()));
        });
    }
}

まとめ

概念 説明
List 順序あり・重複OK
Set 順序なし・重複NG
Map キーと値のペア
ArrayList 最も一般的なList実装
HashMap 最も一般的なMap実装
ジェネリクス 型の安全性を保証
Stream API 宣言的なデータ処理

重要ポイント

  1. 通常は**ArrayListHashMap**を使う
  2. 重複を排除したい場合は**Set**
  3. ジェネリクスで型安全なコードを書く
  4. Stream APIで宣言的にデータを処理する

練習問題

問題1: 基本

Map<String, Integer>で単語の出現回数をカウントするプログラムを作成してください。

問題2: 応用

Stream APIを使って、整数リストから偶数だけを抽出し、降順にソートし、先頭5件を取得してください。

チャレンジ問題

ジェネリッククラスStack<T>を実装してください。push, pop, peek, isEmpty, size メソッドを持ち、内部ではArrayListを使いましょう。


参考リンク


次回予告: Day 10では「総合プロジェクト」に取り組みます。これまで学んだすべてのJava技術を使って、タスク管理アプリケーションを構築しましょう!