Java:総合演習:在庫管理システム
1学習の目的
- JV01〜JV17で学んだすべてを組み合わせ、実務水準の在庫管理システムを作り上げる。
- 「どの技術をどこで使うか」を自分で判断できるようになる。18章かけて積み上げたものが、1つの形になる。
2基礎解説
| 層 | 役割 | 使う技術(学んだ章) |
|---|---|---|
| ① データ | 商品を表現する | クラス・カプセル化(JV08・09) |
| ② 種類 | 違いを吸収する | 継承・インターフェース(JV10・11) |
| ③ 操作 | 集計・検索する | List/Map・Stream(JV12・16) |
| ④ 結果 | 失敗を表現する | Result型・例外(JV13・15) |
- まず扱うデータをクラスにする。商品には何の情報が必要か——名前・価格・在庫。フィールドは private にし、検証つきの setter を用意する(JV09)。
- 種類ごとの違いを見つける。書籍だけ送料が違うなら、継承で shippingFee() を上書きする(JV10)。
- 「できること」はインターフェースで揃える。配送できる・割引できるといった性質は implements で表現する(JV11)。
- 集計は Stream で書く。合計・グループ化・並べ替えは、ループより Stream のほうが意図が明確になる(JV16)。
- 失敗の表現を決める。想定内の失敗(在庫不足)は Result 型、異常事態(不正な値)は例外——使い分けが設計の質を決める。
この章の題材:商品マスタ・在庫管理・注文処理を備えた在庫管理システム。実務で最も多い業務システムの型です。
// ① インターフェース(できること)
interface Shippable {
int shippingFee();
}
// ② 親クラス(共通のデータと処理)
static class Product implements Shippable {
private final String name; // 変更不可
private final String category;
private int price;
private int stock;
Product(String name, String category, int price, int stock) {
this.name = name;
this.category = category;
setPrice(price); // 検証を通す
setStock(stock);
}
public void setStock(int stock) {
if (stock < 0) {
throw new IllegalArgumentException("在庫は0以上");
}
this.stock = stock;
}
public int shippingFee() { return 600; }
public int stockValue() { return price * stock; }
public String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
public boolean reduce(int n) {
if (n > stock) return false;
stock -= n;
return true;
}
}
// ③ 子クラス(違いだけ書く)
static class Book extends Product {
Book(String name, int price, int stock) {
super(name, "書籍", price, stock);
}
@Override
public int shippingFee() { return 0; }
}
// ④ Result型(失敗を値として返す)
static Result<Integer> order(Product p, int qty) {
if (qty <= 0) return Result.fail("数量が不正です");
if (!p.reduce(qty)) {
return Result.fail("在庫不足です(残り" + p.getStock() + "個)");
}
return Result.ok(p.getPrice() * qty + p.shippingFee());
}
// ⑤ Stream で集計
int total = items.stream().mapToInt(Product::stockValue).sum();
Map<String, Integer> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory, TreeMap::new,
Collectors.summingInt(Product::stockValue)));
3基本ドリル(10問)
出力 == 5
private final String name; と setStock で負の値を拒否する。
public class Main {
static class Product {
private final String name;
private int stock;
Product(String name, int stock) {
this.name = name;
setStock(stock);
}
public void setStock(int stock) {
if (stock < 0) {
throw new IllegalArgumentException("在庫は0以上");
}
this.stock = stock;
}
public int getStock() {
return stock;
}
}
public static void main(String[] args) {
System.out.println(new Product("ノートPC", 5).getStock());
}
}コンストラクタから setter を呼ぶことで、生成時にも検証が働く(JV09)。これが不正なデータを防ぐ第一歩。
A. 例外を投げる B. Result型で返す C. -1を返す D. null を返す 選択★☆☆無料
解答 == B
在庫不足は異常事態か、想定内の結果か。
B
在庫不足は日常的に起こることなので、例外ではなく戻り値で表現する。異常事態(不正な値)だけを例外にする。
出力2行。600 / 0
@Override で shippingFee を上書きする(JV10)。
public class Main {
static class Product {
public int shippingFee() { return 600; }
}
static class Book extends Product {
@Override
public int shippingFee() { return 0; }
}
public static void main(String[] args) {
System.out.println(new Product().shippingFee());
System.out.println(new Book().shippingFee());
}
}違う部分だけを子クラスに書く。名前や価格の処理は親に1回書けば済む。
public class Main {
static String label(int stock) {
if (stock == 0) return "品切れ";
if (stock < 10) ____ "残りわずか";
return "在庫あり";
}
public static void main(String[] args) {
System.out.println(label(5));
}
}出力 == 残りわずか
値を返すキーワード(6文字)。
return
早期リターンで条件を段階的に絞る(JV07)。else を書かずに済み、インデントが深くならない。
A. 二重ループ B. Stream の groupingBy C. 配列を複数用意する D. if文で分岐 選択★☆☆無料
解答 == B
JV16で学んだ集計方法。
B
groupingBy なら1文で書ける。カテゴリが何種類あるか事前に分からなくても対応できる。
出力 == 796800
mapToInt で計算し sum() する(JV16)。
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 stockValue() { return price * stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product(128000, 5),
new Product(3200, 42),
new Product(2800, 8));
System.out.println(items.stream().mapToInt(Product::stockValue).sum());
}
}メソッド参照で簡潔に書ける。Product::stockValue は p -> p.stockValue() と同じ。
public class Main {
static class Product {
private int stock = 5;
public boolean reduce(int n) {
if (n > stock) return ____;
stock -= n;
return true;
}
}
public static void main(String[] args) {
System.out.println(new Product().reduce(100));
}
}出力 == false
失敗を表す真偽値(5文字)。
false
失敗しても在庫は変わらない。先に判定してから減らすのが、状態を壊さない書き方(JV09)。
A. 速くなる B. 「生成後は変わらない」という業務ルールを表現する C. 必須だから D. メモリ節約 選択★★☆広告解放
解答 == B
設計の意図をコードで示す。
B
final は意図の表明。「この値は変わらない」と宣言すれば、誤って変更するコードはコンパイルで止められる。
出力2行。256000 / 数量が不正です
Result.ok と Result.fail を使い分ける(JV15)。
public class Main {
static class Result<T> {
private T value;
private String error;
private Result(T value, String error) {
this.value = value;
this.error = error;
}
static <T> Result<T> ok(T value) { return new Result<>(value, null); }
static <T> Result<T> fail(String e) { return new Result<>(null, e); }
boolean isOk() { return error == null; }
T getValue() { return value; }
String getError() { return error; }
}
static Result<Integer> order(int price, int qty) {
if (qty <= 0) {
return Result.fail("数量が不正です");
}
return Result.ok(price * qty);
}
public static void main(String[] args) {
for (int qty : new int[]{2, 0}) {
Result<Integer> r = order(128000, qty);
System.out.println(r.isOk() ? r.getValue() : r.getError());
}
}
}戻り値の型に「失敗しうる」ことが現れている。呼び出し側は isOk() を確認せざるをえない。
出力 == {PC=128000, 周辺機器=48200}
groupingBy に TreeMap::new と summingInt を渡す(JV16)。
import java.util.*;
import java.util.stream.*;
public class Main {
static class Product {
String category;
int price;
Product(String category, int price) {
this.category = category;
this.price = price;
}
String getCategory() { return category; }
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));
Map<String, Integer> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
TreeMap::new,
Collectors.summingInt(Product::getPrice)));
System.out.println(byCategory);
}
}JV12でMapを手で組み立てた集計が1文になる。カテゴリの種類を意識する必要がない。
4実践シナリオ(5問)
出力3行
toString の中で stockValue() と label() を呼ぶ。
import java.util.*;
public class Main {
static class Product {
private String name;
private int price;
private int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
int stockValue() { return price * stock; }
String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
@Override
public String toString() {
return String.format("%s:%,d円 × %d個 = %,d円(%s)",
name, price, stock, stockValue(), label());
}
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product("ノートPC", 128000, 5),
new Product("マウス", 3200, 42),
new Product("モニター", 45000, 0));
items.forEach(System.out::println);
}
}表示の責任をクラス自身が持つ。items.forEach(System.out::println) だけで一覧が出力できる。
出力 == 1200
ポリモーフィズムで、それぞれの shippingFee が呼ばれる(JV10)。
import java.util.*;
public class Main {
static class Product {
public int shippingFee() { return 600; }
}
static class Book extends Product {
@Override
public int shippingFee() { return 0; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product(), new Book(), new Product());
System.out.println(items.stream()
.mapToInt(Product::shippingFee).sum());
}
}if文で種類を判定していない。ポリモーフィズムのおかげで、リストに何が入っていても正しく計算される。
出力2行。true 3 / false 3
reduce メソッドの戻り値と getStock を組み合わせる。
public class Main {
static class Product {
private int stock;
Product(int stock) { this.stock = stock; }
public int getStock() { return stock; }
public boolean reduce(int n) {
if (n > stock) return false;
stock -= n;
return true;
}
}
public static void main(String[] args) {
Product p = new Product(5);
System.out.println(p.reduce(2) + " " + p.getStock());
System.out.println(p.reduce(100) + " " + p.getStock());
}
}失敗した操作では状態が変わらない。この性質があるから、呼び出し側は安心して再試行できる。
出力3行
成功時は price * qty + shippingFee を返す。
public class Main {
static class Result<T> {
private T value;
private String error;
private Result(T v, String e) { value = v; error = e; }
static <T> Result<T> ok(T v) { return new Result<>(v, null); }
static <T> Result<T> fail(String e) { return new Result<>(null, e); }
boolean isOk() { return error == null; }
T getValue() { return value; }
String getError() { return error; }
}
static class Product {
int price = 128000;
int stock = 5;
int shippingFee() { return 600; }
boolean reduce(int n) {
if (n > stock) return false;
stock -= n;
return true;
}
}
static Result<Integer> order(Product p, int qty) {
if (qty <= 0) return Result.fail("数量が不正です");
if (!p.reduce(qty)) return Result.fail("在庫不足です");
return Result.ok(p.price * qty + p.shippingFee());
}
public static void main(String[] args) {
Product p = new Product();
for (int qty : new int[]{0, 100, 2}) {
Result<Integer> r = order(p, qty);
System.out.println(r.isOk()
? "成功:" + String.format("%,d", r.getValue()) + "円"
: "失敗:" + r.getError());
}
}
}3つの結果が同じ型で返る。呼び出し側は isOk() を見るだけでよく、例外処理を書き分ける必要がない。
出力 == 2026/09/08 時点:796,800円
LocalDate と DateTimeFormatter を使う(JV17)。
import java.time.*;
import java.time.format.DateTimeFormatter;
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 stockValue() { return price * stock; }
}
public static void main(String[] args) {
List<Product> items = List.of(
new Product(128000, 5),
new Product(3200, 42),
new Product(2800, 8));
int total = items.stream().mapToInt(Product::stockValue).sum();
LocalDate date = LocalDate.of(2026, 9, 8);
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy/MM/dd");
System.out.println(date.format(f) + " 時点:"
+ String.format("%,d", total) + "円");
}
}レポートには必ず基準日を入れる。「いつ時点の数字か」が分からない報告書は、後から見返したとき役に立たない。
5仕上げ課題
■ インターフェース Shippable
・int shippingFee()
■ クラス Product implements Shippable
・private final String name, category/private int price, stock
・コンストラクタから検証つき setter を呼ぶ(負の値は IllegalArgumentException)
・shippingFee() → 600
・stockValue() → price × stock
・label() → 0なら「品切れ」、10未満「残りわずか」、以上「在庫あり」
・boolean reduce(int n) → 在庫不足なら false(変更しない)
・toString() → 「ノートPC:128,000円 × 5個 = 640,000円(残りわずか)」
■ クラス Book extends Product:カテゴリ「書籍」固定、送料0円
■ クラス Result<T>:ok / fail / isOk / getValue / getError
■ メソッド order(Product p, int qty)(Result<Integer> を返す)
・数量0以下 → 「数量が不正です」
・在庫不足 → 「在庫不足です(残りN個)」
・成功 → 価格 × 数量 + 送料
■ main の処理
① 商品4件を一覧表示
② 在庫総額(Stream)
③ カテゴリ別の在庫金額(Stream・TreeMap)
④ 注文3件を処理(成功 / 数量不正 / 在庫不足)
⑤ 不正な商品の生成を試みて例外を捕まえる
データ:ノートPC/PC/128000/5、マウス/周辺機器/3200/42、Java入門(Book)/2800/8、モニター/周辺機器/45000/0
期待される出力(11行)
ノートPC:128,000円 × 5個 = 640,000円(残りわずか)
マウス:3,200円 × 42個 = 134,400円(在庫あり)
Java入門:2,800円 × 8個 = 22,400円(残りわずか)
モニター:45,000円 × 0個 = 0円(品切れ)
---
在庫総額:796,800円
カテゴリ別:{PC=640000, 周辺機器=134400, 書籍=22400}
---
成功:256,600円
失敗:数量が不正です
失敗:在庫不足です(残り0個) 総合★★★広告解放
出力11行が完全一致
注文はノートPC×2(成功・送料600込み)、マウス×0(数量不正)、モニター×1(在庫不足)。カテゴリ別は groupingBy に TreeMap::new と summingInt(Product::stockValue) を渡す。最後の例外は catch するが出力はしない(11行に含まれないため、捕まえるだけでよい)。
import java.util.*;
import java.util.stream.*;
public class Main {
interface Shippable {
int shippingFee();
}
static class Product implements Shippable {
private final String name;
private final String category;
private int price;
private int stock;
Product(String name, String category, int price, int stock) {
this.name = name;
this.category = category;
setPrice(price);
setStock(stock);
}
public String getName() { return name; }
public String getCategory() { return category; }
public int getPrice() { return price; }
public int getStock() { return stock; }
public void setPrice(int price) {
if (price < 0) {
throw new IllegalArgumentException("価格は0以上にしてください");
}
this.price = price;
}
public void setStock(int stock) {
if (stock < 0) {
throw new IllegalArgumentException("在庫は0以上にしてください");
}
this.stock = stock;
}
public int shippingFee() {
return 600;
}
public int stockValue() {
return price * stock;
}
public String label() {
if (stock == 0) return "品切れ";
if (stock < 10) return "残りわずか";
return "在庫あり";
}
public boolean reduce(int n) {
if (n > stock) return false;
stock -= n;
return true;
}
@Override
public String toString() {
return String.format("%s:%,d円 × %d個 = %,d円(%s)",
name, price, stock, stockValue(), label());
}
}
static class Book extends Product {
Book(String name, int price, int stock) {
super(name, "書籍", price, stock);
}
@Override
public int shippingFee() {
return 0;
}
}
static class Result<T> {
private T value;
private String error;
private Result(T value, String error) {
this.value = value;
this.error = error;
}
static <T> Result<T> ok(T value) { return new Result<>(value, null); }
static <T> Result<T> fail(String e) { return new Result<>(null, e); }
boolean isOk() { return error == null; }
T getValue() { return value; }
String getError() { return error; }
}
static Result<Integer> order(Product p, int qty) {
if (qty <= 0) {
return Result.fail("数量が不正です");
}
if (!p.reduce(qty)) {
return Result.fail("在庫不足です(残り" + p.getStock() + "個)");
}
return Result.ok(p.getPrice() * qty + p.shippingFee());
}
public static void main(String[] args) {
List<Product> items = new ArrayList<>(List.of(
new Product("ノートPC", "PC", 128000, 5),
new Product("マウス", "周辺機器", 3200, 42),
new Book("Java入門", 2800, 8),
new Product("モニター", "周辺機器", 45000, 0)));
items.forEach(System.out::println);
int total = items.stream().mapToInt(Product::stockValue).sum();
Map<String, Integer> byCategory = items.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
TreeMap::new,
Collectors.summingInt(Product::stockValue)));
System.out.println("---");
System.out.println("在庫総額:" + String.format("%,d", total) + "円");
System.out.println("カテゴリ別:" + byCategory);
System.out.println("---");
int[][] requests = {{0, 2}, {1, 0}, {3, 1}};
for (int[] req : requests) {
Result<Integer> r = order(items.get(req[0]), req[1]);
System.out.println(r.isOk()
? "成功:" + String.format("%,d", r.getValue()) + "円"
: "失敗:" + r.getError());
}
try {
new Product("不正商品", "PC", -100, 1);
} catch (IllegalArgumentException e) {
// 生成が拒否されることを確認(出力はしない)
}
}
}これがJava編の到達点だ。この1本のプログラムに、18章で学んだほぼすべてが入っている。
カプセル化(JV09)がフィールドを守り、継承(JV10)が書籍の送料無料を表現し、インターフェース(JV11)が「配送できる」という性質を定義する。Stream(JV16)が集計を1文にまとめ、ジェネリクス(JV15)の Result型 が失敗を値として返す。例外(JV13)は不正な生成だけに使い、日常的な失敗と区別している。
とくに失敗の表現を2つに使い分けた点に注目してほしい。在庫不足や数量不正はResult型で返し、価格が負というあってはならない値は例外で拒否している。「起きて当然のこと」と「起きてはいけないこと」を区別する——この判断が、システムの堅牢さを決める。
items.forEach(System.out::println) のシンプルさも、積み重ねの成果だ。toString() が表示を、label() が判定を、stockValue() が計算を担っているから、呼び出し側は1行で済む。JV05では main の中にすべてが詰まっていたことを思い出してほしい。
——JV01では System.out.println("Hello, World!") と書くところから始まった。環境構築に手こずり、セミコロンを忘れ、printIn と打ち間違えたかもしれない。それが18章を経て、カプセル化・継承・ジェネリクス・Streamを備えた在庫管理システムになった。
ここまで来たあなたは、もう「Javaを勉強している人」ではありません。Javaでシステムを設計できる人です。次に必要なのは、教材ではなく実際に作りたいものを作ることだ。手を動かした分だけ、確実に上手くなる。
お疲れさまでした。