6日目のゴール
6日目のテーマは
「コンストラクタで“初期化する”だけでなく、“間違えようがない初期化”を設計すること」 です。
ここまでであなたはすでに、
コンストラクタで必須情報を受け取る
おかしな値をコンストラクタで弾く・補正する
関連するオブジェクトもコンストラクタの中でまとめて初期化する
new の1行が「そのオブジェクトの自己紹介」になるように設計する
というところまで来ています。
6日目では、ここからもう一歩踏み込んで、
「間違った使い方をしようとしても、そもそもコンパイルが通らない」
「初期化の順番ミス・設定漏れが起きない形にする」
「“途中の中途半端な状態”を作らせない」
という、“ミスしにくい初期化”の設計を体で覚えていきます。
題材は、現実でもよくある 「注文アプリの注文データ」 です。
今日の題材:注文データの初期化を考える
現実のイメージから整理する
ネットショップをイメージしてみてください。
注文には、注文番号がある
注文には、購入者情報がある
注文には、合計金額がある
「注文番号なし」「購入者なし」「合計金額マイナス」はありえない
これをそのままクラスにすると、まずはこうなります。
public class Order {
String orderId;
String customerName;
int totalAmount;
}
Javaそして、こう使えます。
public class Main {
public static void main(String[] args) {
Order o = new Order();
o.orderId = "A001";
o.customerName = "山田太郎";
o.totalAmount = 5000;
}
}
Java動きます。
でも、ここには「初期化の罠」がたくさんあります。
「中途半端な状態」が簡単に生まれてしまう
ありえない注文が作れてしまう
さっきの Order は、こういうこともできます。
Order o1 = new Order();
o1.orderId = null;
o1.customerName = null;
o1.totalAmount = -1000;
Order o2 = new Order();
// 何も設定しないまま使う
Java現実世界で考えると、
注文番号が null の注文
購入者名が null の注文
合計金額がマイナスの注文
そもそも何も決まっていない注文
どれも「ありえない状態」です。
でも、コンパイルエラーにはなりません。
つまり、
「変な状態の Order が簡単に生まれてしまう」
ということです。
6日目では、
これを「そもそもそんな状態を作れない」方向に持っていきます。
コンストラクタで「必須情報」を強制する
new した瞬間に“最低限の約束”を守らせる
まずは、必須情報をコンストラクタで受け取る形にします。
public class Order {
String orderId;
String customerName;
int totalAmount;
Order(String orderId, String customerName, int totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
}
void show() {
System.out.println("注文番号: " + orderId);
System.out.println("購入者: " + customerName);
System.out.println("合計金額: " + totalAmount + "円");
}
}
Java使い方はこうなります。
public class Main {
public static void main(String[] args) {
Order o = new Order("A001", "山田太郎", 5000);
o.show();
}
}
Javaこれで、
「注文番号なし」「購入者名なし」「合計金額なし」の注文は作れない
(コンストラクタの引数を渡さないとコンパイルエラーになる)
という状態にはなりました。
でも、まだ足りません。
null やマイナスは渡せてしまいます。
コンストラクタの中で「ありえない値」を弾く
初期化のときに“おかしさ”を潰す
Order のコンストラクタを、こう変えます。
public class Order {
String orderId;
String customerName;
int totalAmount;
Order(String orderId, String customerName, int totalAmount) {
if (orderId == null || orderId.isEmpty()) {
throw new IllegalArgumentException("注文番号は必須です。");
}
if (customerName == null || customerName.isEmpty()) {
throw new IllegalArgumentException("購入者名は必須です。");
}
if (totalAmount < 0) {
throw new IllegalArgumentException("合計金額はマイナスにできません。");
}
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
}
void show() {
System.out.println("注文番号: " + orderId);
System.out.println("購入者: " + customerName);
System.out.println("合計金額: " + totalAmount + "円");
}
}
Javaここで新しく出てきたのが IllegalArgumentException です。
「渡された引数(argument)が不正(illegal)だよ」という意味の例外です。
これを投げることで、
「その値で Order を作ること自体を拒否する」
ことができます。
例えば、こう書くと:
Order o = new Order("", "山田太郎", 5000);
Java実行時に例外が発生して、「それはダメ」と教えてくれます。
ここでの本質は、
「コンストラクタの中で、ありえない状態を“なかったことにする”のではなく、“そもそも作らせない”」
という考え方です。
3〜4日目では「補正する」パターンもやりましたが、
6日目では「絶対に許したくない値は、例外で止める」という選択肢も持っておいてほしいんです。
「途中の中途半端な状態」を作らせない
セッターでバラバラに変えさせない設計
ここで、もう一つ大事な視点を足します。
こんな Order を考えてみます。
public class Order {
String orderId;
String customerName;
int totalAmount;
Order() {
}
void setOrderId(String orderId) {
this.orderId = orderId;
}
void setCustomerName(String customerName) {
this.customerName = customerName;
}
void setTotalAmount(int totalAmount) {
this.totalAmount = totalAmount;
}
}
Javaこれを使うと、こうなります。
Order o = new Order();
o.setOrderId("A001");
// ここで何か処理…
o.setCustomerName("山田太郎");
// ここで何か処理…
o.setTotalAmount(5000);
Java一見「柔軟でいいじゃん」と思えますが、
実はかなり危険です。
途中のどこかで処理が止まったら、
注文番号だけ入っている注文
注文番号と購入者名だけ入っている注文
といった「中途半端な状態」が簡単に生まれます。
6日目で強く意識してほしいのは、
「new した瞬間に、もう“完成した状態”であるべきものは、コンストラクタで全部決める」
ということです。
注文データはまさにそれです。
「あとでちょっとずつ埋めていく」ものではなく、
「作るときに全部そろっていてほしい」ものです。
だからこそ、
Order o = new Order("A001", "山田太郎", 5000);
Javaという形が、いちばん自然で安全なんです。
「初期化専用のクラス」を挟むという考え方
入力値のチェックと変換をまとめる
もう一歩だけ踏み込みます。
現実のアプリでは、
ユーザーの入力は「文字列」で来ることが多いです。
注文番号:テキスト
購入者名:テキスト
合計金額:テキスト(”5000″ など)
これをそのまま Order に渡すと、
Order のコンストラクタが「文字列→数値変換」まで抱えることになります。
そこで使えるのが、
「入力を受け取る専用のクラス」を挟む という考え方です。
public class OrderInput {
String orderIdText;
String customerNameText;
String totalAmountText;
OrderInput(String orderIdText, String customerNameText, String totalAmountText) {
this.orderIdText = orderIdText;
this.customerNameText = customerNameText;
this.totalAmountText = totalAmountText;
}
Order toOrder() {
if (orderIdText == null || orderIdText.isEmpty()) {
throw new IllegalArgumentException("注文番号は必須です。");
}
if (customerNameText == null || customerNameText.isEmpty()) {
throw new IllegalArgumentException("購入者名は必須です。");
}
if (totalAmountText == null || totalAmountText.isEmpty()) {
throw new IllegalArgumentException("合計金額は必須です。");
}
int amount;
try {
amount = Integer.parseInt(totalAmountText);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("合計金額は数値で入力してください。");
}
if (amount < 0) {
throw new IllegalArgumentException("合計金額はマイナスにできません。");
}
return new Order(orderIdText, customerNameText, amount);
}
}
Javaここでやっていることは、
文字列として入力を受け取る
必須チェックをする
数値変換をする
変換に成功したら、ちゃんとした Order を new して返す
という流れです。
これにより、
Order は「すでに整った値」を受け取るだけでよくなる
文字列のチェックや変換は、OrderInput に閉じ込められる
という分担になります。
初期化の意味を、さらに一段深く言うなら、
「生の入力から、“ちゃんとしたオブジェクト”に変わるまでの道筋を設計すること」
でもあります。
main から見た“ミスしにくさ”を確認する
使う側のコードが「間違えにくい形」になっているか?
ここまでのクラスを main から使ってみます。
public class Main {
public static void main(String[] args) {
OrderInput input1 = new OrderInput("A001", "山田太郎", "5000");
OrderInput input2 = new OrderInput("", "", "-100");
Order o1 = input1.toOrder();
o1.show();
System.out.println("-----");
try {
Order o2 = input2.toOrder();
o2.show();
} catch (IllegalArgumentException e) {
System.out.println("注文の作成に失敗しました: " + e.getMessage());
}
}
}
Javaこのコードを眺めてみてください。
new OrderInput("A001", "山田太郎", "5000") を見れば、
「ユーザー入力としての注文情報だな」と分かる
input1.toOrder() を見れば、
「ここで“ちゃんとした Order” に変換しているんだな」と分かる
IllegalArgumentException を catch しているのを見れば、
「おかしな入力はここで止めるんだな」と分かる
つまり、
「どこで何が初期化されていて、どこで何がチェックされているか」が読み取りやすい
状態になっています。
6日目で意識してほしい“初期化の3段階”
今日の流れを、3段階で整理してみます。
生の入力(文字列など)
まだ何も保証されていない、ただのデータ。
ここでは「とりあえず受け取る」だけ。
入力チェック・変換(OrderInput)
必須チェック・形式チェック・数値変換などを行う。
ここで「この値なら Order を作っていい」と判断する。
オブジェクトの初期化(Order)
すでに整った値だけを受け取り、
「ありえない状態にならない」ことを保証する。
この3段階を意識できると、
「どこで何を責任持って初期化するか」
を自分で設計できるようになります。
6日目のミニアプリをもう一度まとめて眺める
Order
注文番号・購入者名・合計金額を持つ。
コンストラクタで「null・空文字・マイナス」を拒否する。
一度作られたら、「ありえない状態」にはならない。
OrderInput
文字列としての入力を受け取る。toOrder() でチェックと変換を行い、
問題なければ Order を new して返す。
Main
OrderInput を作るtoOrder() を呼んで Order に変換する
例外が出たら「注文作成に失敗」として扱う
この構造を、自分の言葉で説明できたら、6日目はかなりいい仕上がりです。
6日目で絶対に押さえてほしい本質
今日いちばん大事なのは、
「初期化=“値を入れる”ではなく、“間違えようがない形にする”こと」
だと腹で理解できたかどうかです。
コンストラクタで考えるべきことは、
このクラスは、どんな状態なら“正しい”と言えるか?
その状態を壊すような値は、補正するのか? それとも拒否するのか?
new した瞬間に、「中途半端な状態」が残っていないか?
そして、
「このオブジェクトは、new された瞬間にもう“完成”しているべきか?」
「それとも、段階的に組み立てるべきものか?」
と自分に問いながら、
コンストラクタと“入力を受け取るクラス”の役割を分けていく。
ここまで来たあなたは、
コンストラクタを「書き方を覚えるもの」ではなく、
“バグを生みにくい設計をするための道具”として扱い始めています。
7日目は、この感覚を使って、
小さなアプリ全体の「初期化の流れ」を自分で設計してみる——
そこまで行くと、「初期化の意味」はもう完全にあなたの武器になっています。


