Java:ラムダ式とStream
1学習の目的
- Stream API で抽出・変換・集計を短く書けるようになる。for文とif文の組み合わせが、1本の流れとして表現できるようになる。
- ラムダ式(->)を読み書きできるようになる。現代のJavaコードを読む上で避けて通れない記法を身につける。
2基礎解説
JV12までは for と if でデータを処理してきました。Stream API を使うと、「何をしたいか」だけを書いて、繰り返しの手続きを省略できます。
| 操作 | 意味 | 例 |
|---|---|---|
| filter | 条件で絞る | p -> p.getStock() > 0 |
| map | 別のものに変換 | p -> p.getName() |
| sorted | 並べ替え | Comparator.comparing… |
| collect | 結果を集める | Collectors.toList() |
- ラムダ式は「引数 -> 処理」。p -> p.getStock() > 0 は「p を受け取って、在庫が0より大きいか判定する」という意味。短い関数をその場で書く記法。
- Stream は stream() で始まり、collect などで終わる。途中の filter や map はつなげて書ける(メソッドチェーン)。
- Product::getName はメソッド参照。p -> p.getName() と同じ意味だが、より短く書ける。
- 数値の集計は mapToInt を挟む。.mapToInt(p -> p.getPrice()).sum() のように書くと、合計・平均・最大が取れる。
- 元のリストは変更されない。Streamの操作は新しい結果を作るだけなので、元のデータは安全に保たれる。
現場使用例:検索結果の絞り込み、一覧の並べ替え、カテゴリ別集計、CSVの加工。現代のJavaコードでは Stream が当たり前に使われている。
import java.util.*;
import java.util.stream.*;
List<Product> items = …;
// ① 絞って、変換して、集める
List<String> names = items.stream()
.filter(p -> p.getStock() > 0) // 在庫があるものだけ
.map(p -> p.getName()) // 名前に変換
.collect(Collectors.toList()); // Listにする
// ② メソッド参照で短く
.map(Product::getName) // p -> p.getName() と同じ
// ③ 数値の集計
int total = items.stream()
.mapToInt(p -> p.getPrice() * p.getStock())
.sum();
double avg = items.stream()
.mapToInt(Product::getPrice)
.average()
.orElse(0); // 空なら0
long count = items.stream()
.filter(p -> p.getPrice() >= 10000)
.count();
// ④ 並べ替え(高い順)
items.stream()
.sorted(Comparator.comparingInt(Product::getPrice).reversed())
.map(Product::getName)
.collect(Collectors.toList());
// ⑤ 最大・最小
String max = items.stream()
.max(Comparator.comparingInt(Product::getPrice))
.map(Product::getName)
.orElse("なし");
// ⑥ グループ化(カテゴリ別の件数)
Map<String, Long> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory, TreeMap::new, Collectors.counting()));
// ⑦ 文字列の連結
String joined = items.stream()
.map(Product::getName)
.collect(Collectors.joining(", "));
// ⑧ 条件の判定
items.stream().anyMatch(p -> p.getStock() == 0); // 1つでも該当するか
items.stream().allMatch(p -> p.getPrice() > 0); // すべて該当するか
3基本ドリル(10問)
出力 == 60
list.stream().mapToInt(n -> n).sum() と書く。
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(10, 20, 30);
int total = nums.stream().mapToInt(n -> n).sum();
System.out.println(total);
}
}for文を書かずに合計できる。mapToInt で数値の流れに変換すると、sum() が使えるようになる。
A. nと2を掛ける宣言 B. nを受け取って2倍を返す関数 C. 変数の宣言 D. 比較演算 選択★☆☆無料
解答 == B
矢印の左が引数、右が処理。
B
「引数 → 処理」を表す短い関数。メソッドを定義するほどではない小さな処理を、その場に書ける。
出力 == [マウス, モニター]
filter(s -> s.length() <= 4) で絞り、collect(Collectors.toList()) で集める。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> items = List.of("ノートPC", "マウス", "モニター");
List<String> result = items.stream()
.filter(s -> s.length() <= 4)
.collect(Collectors.toList());
System.out.println(result);
}
}filter は条件に合うものだけを通す。for文とifを書いていた処理が1行になる。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(1, 2, 3, 4, 5);
List<Integer> even = nums.stream()
.____(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(even);
}
}出力 == [2, 4]
「ふるいにかける」を意味する6文字のメソッド。
filter
filter は条件が true のものだけを残す。ふるいにかけるイメージで覚えるとよい。
A. 変更する B. 変更しない C. 削除する D. 並べ替える 選択★☆☆無料
解答 == B
新しい結果を作るだけ。
B
元のデータは安全に保たれる。Collections.sort が元を並べ替えるのとは対照的。
出力 == [20, 40, 60]
map(n -> n * 2) で各要素を変換する。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(10, 20, 30);
List<Integer> doubled = nums.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(doubled);
}
}map は各要素を別のものに変換する。件数は変わらず、中身だけが変わる——filter(件数が減る)との違いに注意。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> result = List.of("A", "B").stream()
.____(Collectors.toList());
System.out.println(result);
}
}出力 == [A, B]
「集める」を意味する7文字のメソッド。
collect
collect が Stream の終点。これを書かないと処理が実行されず、結果も取り出せない。
A. 変数宣言 B. メソッド参照(p -> p.getName() と同じ) C. 継承 D. 型変換 選択★★☆広告解放
解答 == B
ラムダ式の短縮形。
B
単にメソッドを呼ぶだけなら、この書き方が短い。:: はコロン2つで、クラス名とメソッド名をつなぐ。
出力 == 20.0
mapToInt して average() を呼び、orElse(0) で既定値を指定する。
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> nums = List.of(10, 20, 30);
double avg = nums.stream()
.mapToInt(n -> n)
.average()
.orElse(0);
System.out.println(avg);
}
}average は Optional を返す——空のリストでは平均が存在しないため。orElse で「無いときの値」を指定する。
出力 == A, B, C
collect(Collectors.joining(", ")) と書く。
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> items = List.of("A", "B", "C");
String joined = items.stream()
.collect(Collectors.joining(", "));
System.out.println(joined);
}
}末尾に余計な区切りが付かない。for文で書くと「最後だけカンマを付けない」処理が必要になり、意外と面倒。
4実践シナリオ(5問)
出力 == [ノートPC, マウス, キーボード]
filter で絞り、map で名前に変換する。
import java.util.*;
import java.util.stream.*;
public class Main {
static class Product {
String name;
int stock;
Product(String name, int stock) {
this.name = name;
this.stock = stock;
}
String getName() { return name; }
int getStock() { return stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product("ノートPC", 5),
new Product("マウス", 42),
new Product("モニター", 0),
new Product("キーボード", 18));
List<String> result = items.stream()
.filter(p -> p.getStock() > 0)
.map(Product::getName)
.collect(Collectors.toList());
System.out.println(result);
}
}「絞って、変換して、集める」が Stream の基本形。for文とifで書いていた処理が、3行の流れになる。
出力 == 927400
mapToInt の中で掛け算し、sum() する。
import java.util.*;
public class Main {
static class Product {
int price;
int stock;
Product(int price, int stock) {
this.price = price;
this.stock = stock;
}
int getPrice() { return price; }
int getStock() { return stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product(128000, 5),
new Product(3200, 42),
new Product(45000, 0),
new Product(8500, 18));
int total = items.stream()
.mapToInt(p -> p.getPrice() * p.getStock())
.sum();
System.out.println(total);
}
}合計用の変数を宣言する必要がない。JV05では int total = 0; から始めていたが、Streamなら不要。
出力 == [ノートPC, モニター, キーボード, マウス]
Comparator.comparingInt(...).reversed() で降順になる。
import java.util.*;
import java.util.stream.*;
public class Main {
static class Product {
String name;
int price;
Product(String name, int price) {
this.name = name;
this.price = price;
}
String getName() { return name; }
int getPrice() { return price; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product("ノートPC", 128000),
new Product("マウス", 3200),
new Product("モニター", 45000),
new Product("キーボード", 8500));
List<String> sorted = items.stream()
.sorted(Comparator.comparingInt(Product::getPrice).reversed())
.map(Product::getName)
.collect(Collectors.toList());
System.out.println(sorted);
}
}reversed() を付けるだけで降順になる。JV06では「後ろからループする」という工夫が必要だった。
出力 == {PC=1, 周辺機器=3}
Collectors.groupingBy(分類キー, TreeMap::new, Collectors.counting()) を使う。
import java.util.*;
import java.util.stream.*;
public class Main {
static class Product {
String name;
String category;
Product(String name, String category) {
this.name = name;
this.category = category;
}
String getCategory() { return category; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product("ノートPC", "PC"),
new Product("マウス", "周辺機器"),
new Product("モニター", "周辺機器"),
new Product("キーボード", "周辺機器"));
Map<String, Long> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
TreeMap::new,
Collectors.counting()));
System.out.println(byCategory);
}
}JV12でMapを使って書いた集計が1文になる。getOrDefault でカウントを増やす処理が不要になった。
出力2行。true / true
anyMatch と allMatch を使う。
import java.util.*;
public class Main {
static class Product {
int price;
int stock;
Product(int price, int stock) {
this.price = price;
this.stock = stock;
}
int getPrice() { return price; }
int getStock() { return stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product(128000, 5),
new Product(45000, 0),
new Product(8500, 18));
System.out.println(items.stream().anyMatch(p -> p.getStock() == 0));
System.out.println(items.stream().allMatch(p -> p.getPrice() > 0));
}
}anyMatch は見つかった時点で終了する。1万件あっても、1件目で該当すれば残りは調べない——効率的に動く。
5仕上げ課題
Product クラス:name・category・price・stock(getterを持つ)
データ
・ノートPC / PC / 128000 / 5
・マウス / 周辺機器 / 3200 / 42
・モニター / 周辺機器 / 45000 / 0
・キーボード / 周辺機器 / 8500 / 18
すべて Stream で処理すること
① 在庫がある商品名をカンマ区切りで出力
② 在庫総額(価格×在庫の合計)
③ 平均価格(小数第1位まで)
④ 最も高い商品の名前
⑤ カテゴリ別の合計金額(価格の合計、TreeMapでキー順)
⑥ 品切れ商品があるかの判定
期待される出力(6行)
在庫あり:ノートPC, マウス, キーボード
在庫総額:927,400円
平均価格:46,175.0円
最高額:ノートPC
カテゴリ別:{PC=128000, 周辺機器=56700}
品切れあり:true 総合★★★広告解放
出力6行が完全一致
joining(", ") で連結、mapToInt().sum() で合計、average().orElse(0) で平均、max(Comparator...) で最大、groupingBy(..., TreeMap::new, summingInt(...)) でカテゴリ別集計、anyMatch で判定。
import java.util.*;
import java.util.stream.*;
public class Main {
static class Product {
private String name;
private String category;
private int price;
private int stock;
Product(String name, String category, int price, int stock) {
this.name = name;
this.category = category;
this.price = price;
this.stock = stock;
}
String getName() { return name; }
String getCategory() { return category; }
int getPrice() { return price; }
int getStock() { return stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product("ノートPC", "PC", 128000, 5),
new Product("マウス", "周辺機器", 3200, 42),
new Product("モニター", "周辺機器", 45000, 0),
new Product("キーボード", "周辺機器", 8500, 18));
String inStock = items.stream()
.filter(p -> p.getStock() > 0)
.map(Product::getName)
.collect(Collectors.joining(", "));
System.out.println("在庫あり:" + inStock);
int total = items.stream()
.mapToInt(p -> p.getPrice() * p.getStock())
.sum();
System.out.println("在庫総額:" + String.format("%,d", total) + "円");
double avg = items.stream()
.mapToInt(Product::getPrice)
.average()
.orElse(0);
System.out.println("平均価格:" + String.format("%,.1f", avg) + "円");
String maxName = items.stream()
.max(Comparator.comparingInt(Product::getPrice))
.map(Product::getName)
.orElse("なし");
System.out.println("最高額:" + maxName);
Map<String, Integer> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
TreeMap::new,
Collectors.summingInt(Product::getPrice)));
System.out.println("カテゴリ別:" + byCategory);
boolean hasOutOfStock = items.stream().anyMatch(p -> p.getStock() == 0);
System.out.println("品切れあり:" + hasOutOfStock);
}
}JV12で30行以上かかった集計が、それぞれ3〜5行で書けた。とくにカテゴリ別集計は劇的だ。JV12では Map を2つ用意し、getOrDefault で加算する処理を書いていたが、Stream なら groupingBy の1文で済む。
Stream の利点は「何をしたいか」がそのまま書けることにある。filter → map → collect という並びは「絞って、変換して、集める」と読める。for文とifで書くと、繰り返しの手続きがコードの大半を占め、目的が埋もれてしまう。
ただしStreamが常に優れているわけではない。単純なループなら for のほうが読みやすいことも多く、デバッグもしやすい。複雑な集計や、絞り込みと変換が連続する処理——そういう場面で Stream が力を発揮する。
average().orElse(0) の Optional にも触れておきたい。空のリストに平均は存在しないので、Javaは「値が無いかもしれない」という型で返す。orElse で既定値を指定することで、null チェックを書かずに済む——nullを避ける現代的な設計だ。
次章では日付とユーティリティを学ぶ。実務で必ず使う、日付計算と数値整形を押さえる。