この記事では、ChatGPT / Claude / Gemini / Copilot などの主要AIに
同じエンジニアリング課題を3つ投げて比較してみます。
- 曖昧要件 → どう設計するか(=設計思想)
- バグ入り Java コード → どう直すか(=修正戦略・安全性)
- シンプルな関数 → どれだけ読みやすいコードを書くか(=表現力)
比較対象のAIモデル
| AI | 使用モデル |
|---|---|
| ChatGPT | GPT-4.1 |
| Gemini | Gemini 3 Pro |
| Claude | Opus 4.5 |
① 設計タスク: 最小構成のToDoサービス設計
プロンプト(共通):
ユーザーが登録・ログインできて、自分のタスクを追加・一覧できる
ToDoサービスの「最小構成」を設計してください。
ChatGPT の回答(要約)
- 必要機能を明確に絞った MVP 設計
- users / tasks の2テーブルに限定
- 認証はセッション + Cookieを推奨
- API も必要最小限だけ定義
- 画面イメージまで必要十分に記述
→ 業務アプリとして最小限の構成を優先する設計判断が特徴
ChatGPT の実際の回答(クリックして展開)
こんな感じの「本当に必要最低限」の構成にするとシンプルでわかりやすいです。
ユーザーが登録・ログインでき、タスクを追加・一覧できる ToDo サービス(最小構成の設計)
1. 機能要件の整理(MVP)
必須機能だけに絞ります:
- ユーザー登録(サインアップ)
- ログイン / ログアウト
- 自分のタスクを追加
- 自分のタスク一覧表示
やらないこと(MVP では):
- タスク編集・削除
- タスクの期限・タグ・優先度
- パスワードリセットメール
- SNS ログイン
- 管理者画面
2. システム構成(最小構成)
コンポーネント
-
フロントエンド
-
シンプルな Web ページ(SPA でも SSR でも OK)
-
画面:
- サインアップ / ログイン画面
- タスク一覧 + 追加フォーム画面
-
-
バックエンド API
- REST で十分
- 認証付きエンドポイントでタスクの追加・一覧
-
データベース
- RDB(PostgreSQL / MySQL / SQLite など)
- テーブルは基本 2 つだけ:
users,tasks
3. データモデル設計(テーブル設計)
users テーブル
| カラム名 | 型 | 説明 |
|---|---|---|
| id | bigint (PK) | ユーザー ID |
| varchar(unique) | ログイン用メールアドレス | |
| password_hash | varchar | ハッシュ化したパスワード |
| created_at | datetime | 作成日時 |
| updated_at | datetime | 更新日時 |
- パスワードは平文保存不可 → BCrypt などでハッシュ化
tasks テーブル
| カラム名 | 型 | 説明 |
|---|---|---|
| id | bigint (PK) | タスク ID |
| user_id | bigint (FK) | 所有ユーザー ID(users.id) |
| title | varchar | タスクのタイトル |
| created_at | datetime | 作成日時 |
- 最小構成ではタイトルのみ
-
user_idでユーザーごとのタスクに紐づけ
4. 認証方式(最小構成)
最もシンプルなのは セッション + Cookie 認証。
認証の流れ(例)
-
/auth/loginに email / password を POST - バックエンドで認証成功 → セッション作成
- セッション ID を Cookie にセット
- クライアントは Cookie を保持して API にアクセス
- バックエンドは Cookie のセッション ID からユーザー特定
5. API 設計(エンドポイント)
認証系
1. ユーザー登録
-
POST /auth/signup -
Body:
{ "email": "...", "password": "..." } -
処理:
- メール重複チェック
- パスワードをハッシュ化して保存
- 成功 → 201
2. ログイン
-
POST /auth/login -
Body:
{ "email": "...", "password": "..." } -
処理:
- email からユーザー取得
- password とハッシュを照合
- 成功 → セッション発行 + Cookie 返却
3. ログアウト
POST /auth/logout- セッション無効化 + Cookie 削除
タスク系(認証必須)
4. タスク一覧
-
GET /tasks -
処理:
- セッションから user_id を取得
- そのユーザーのタスクを返す
例レスポンス:
[
{ "id": 1, "title": "牛乳を買う", "created_at": "2025-12-17T10:00:00Z" },
{ "id": 2, "title": "レポートを提出する", "created_at": "2025-12-17T09:00:00Z" }
]
5. タスク追加
-
POST /tasks -
Body:
{ "title": "..." } -
処理:
- セッション user_id を取得
- tasks に INSERT
レスポンス例:
{ "id": 3, "title": "新しいタスク", "created_at": "..." }
6. 画面イメージ(ざっくり)
ログイン / サインアップ画面
- メール入力
- パスワード入力
- 「ログイン」ボタン
- 下に「アカウントをお持ちでない方はこちら」
タスク一覧画面
-
ヘッダー:メールアドレス表示 + ログアウトボタン
-
タスク追加フォーム:
- タイトル入力
- 「追加」ボタン
-
タスク一覧(新しい順)
- 牛乳を買う
- レポートを提出する
7. 技術スタック例(参考)
- フロント:HTML + JS(または React / Vue)
- バックエンド:Express / Rails / Django / Spring など
- DB:SQLite(開発)→ PostgreSQL(本番推奨)
8. まとめ
「ユーザー」「タスク」の 2 テーブルと、認証付き API(5本)だけで動く最小構成の ToDo サービス。
MVP の次ステップとして:
- タスクの編集/削除
- 完了フラグ・期限
- ユーザープロフィール
- テストやエラーハンドリングの追加
なども柔軟に追加できます。
必要ならこの設計のまま ER 図 / API スキーマ / 画面ワイヤーフレーム / 具体的な実装例(Express, Rails, Django など) も作れます!
Gemini の回答(要約)
- UI / API / DB を分離した三層構造を前提にする
- Tasks に status 等の属性を持たせるなど、将来拡張を意識したデータ設計
- 認証設計は JWT / Cookie どちらにも対応可能な形で整理
- フロント/UIの構成まで示し、Next.js / Supabase など具体的技術案を提示
- 「MVPとして削るもの」も明確に分離
→ MVP でありつつ現代的フルスタック構成を意識した“拡張性と整合性のある設計
Gemini の実際の回答(クリックして展開)
ユーザー登録・ログイン機能を含んだToDoサービスの「最小構成(MVP: Minimum Viable Product)」を設計します。
ここでは、**「最も素早く実装でき、かつ将来的な拡張にも耐えられる」**という観点から、現代的なWeb開発のスタンダードに基づいた設計を提案します。
1. 全体アーキテクチャ概要
最小構成とはいえ、セキュリティとUX(ユーザー体験)を考慮すると、以下の3層構造が基本となります。
2. データベース設計 (ER図)
データモデルは極限までシンプルにします。必要なのは「誰が(User)」と「何を(Task)」の2つのテーブルだけです。
テーブル定義
1. Users テーブル (ユーザー情報)
ユーザーを識別し、認証するためのテーブルです。
| カラム名 | データ型 | 説明 |
|---|---|---|
id |
UUID / Integer | 主キー (PK) |
email |
String | ログインID、ユニーク制約 |
password_hash |
String | 暗号化されたパスワード |
created_at |
DateTime | 登録日時 |
2. Tasks テーブル (ToDoタスク)
タスクそのものを管理します。ユーザーと紐付けるための外部キーが必要です。
| カラム名 | データ型 | 説明 |
|---|---|---|
id |
UUID / Integer | 主キー (PK) |
user_id |
UUID / Integer | 外部キー (FK) - Usersテーブルのid |
title |
String | タスクの内容 (例: 「牛乳を買う」) |
status |
Boolean / Enum | 完了/未完了 (デフォルト: 未完了) |
created_at |
DateTime | 作成日時 (並び替え用) |
設計のポイント:
user_idカラムにより、1:N(1人のユーザーに対し、N個のタスク)の関係を構築します。これにより、「自分のタスクだけを表示する」クエリが可能になります。
3. API設計 (エンドポイント)
フロントエンドとバックエンドをつなぐインターフェースです。RESTful API形式での最小セットは以下の4つです。
認証系 (Auth)
-
POST
/api/register -
機能: ユーザー登録
-
入力: email, password
-
処理: パスワードをハッシュ化してDBに保存
-
POST
/api/login -
機能: ログイン
-
入力: email, password
-
出力: アクセストークン (JWTなど) またはセッションCookie
タスク操作系 (Tasks)
※ 全て「ログイン済み(認証トークンあり)」であることが前提です。
-
GET
/api/tasks -
機能: 自分のタスク一覧取得
-
クエリ:
WHERE user_id = {現在のユーザーID} -
POST
/api/tasks -
機能: タスク追加
-
入力: title
-
処理:
user_idを現在のユーザーにしてDBに保存
4. 画面・UI設計 (ワイヤーフレーム)
画面遷移も最小限にします。
- ランディング / ログイン画面
- 「ログイン」と「新規登録」のフォームのみ。
- ダッシュボード (メイン画面)
- 上部: 新規タスク入力フォーム + 「追加」ボタン
- 下部: タスク一覧リスト (完了/未完了のチェックボックス付き)
- ヘッダー: ログアウトボタン
5. 推奨技術スタック (2025年基準)
「最小構成」を「最短時間で作る」場合、以下の2つのパターンがおすすめです。
パターンA:フルスタック・フレームワーク (学習・拡張性重視)
すべてをJavaScript/TypeScriptで完結させる構成です。
- Framework: Next.js (フロントエンド + APIルート)
- Database: PostgreSQL (Prisma ORMなどを使用)
- Auth: NextAuth.js (Auth.js)
パターンB:BaaS利用 (爆速開発重視)
認証とDBの構築・管理をクラウドに任せる構成です。
- Frontend: React (Vite) または Next.js
- Backend/DB/Auth: Supabase または Firebase
- メリット: ログイン機能とDBAPIが最初から用意されており、バックエンドのコードをほぼ書かずに済みます。
6. 今回の構成から「あえて削った」もの
「最小構成」を維持するために、以下の機能はVer 1.0では実装しません。
- タスクの編集・削除機能 (間違えたら完了にして隠す運用でカバー)
- 期限日 (Due Date) の設定
- パスワードリセット機能
- ソーシャルログイン (Googleログインなど)
- タスクのカテゴリ分け・タグ付け
この設計に基づけば、熟練したエンジニアであれば数時間〜1日でプロトタイプを作成可能です。
次はどちらのアクションをご希望ですか?
- パターンB (Supabase + Next.js) を採用した、具体的な実装コード(ファイル構成と主要コード)を生成する。
- この設計をもとに、テーブル作成のSQL文を作成する。
Claude の回答(要約)
- アーキテクチャ図から入り、層構造を明確化
- 仕様・制約・整合性を意識したデータモデルの記述
- 認証は JWT/bcrypt/HTTPS などセキュリティ基準を初期段階から含める
- DB設計は型・制約まで整理
- 認証フローの説明が体系的
→ アーキテクチャとセキュリティ要件を最初から含めた構成
Claude の実際の回答(クリックして展開)
最小構成 ToDo サービス設計
1. システムアーキテクチャ
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Frontend │────▶│ Backend │────▶│ Database │
│ (SPA) │◀────│ (REST API) │◀────│ (SQL) │
└─────────────┘ └─────────────┘ └─────────────┘
│
JWT認証で保護
2. データベース設計
| users テーブル | 型 | 説明 |
|---|---|---|
| id | UUID / SERIAL | 主キー |
| VARCHAR(255) | ユニーク制約 | |
| password_hash | VARCHAR(255) | bcrypt等でハッシュ化 |
| created_at | TIMESTAMP | 登録日時 |
| tasks テーブル | 型 | 説明 |
|---|---|---|
| id | UUID / SERIAL | 主キー |
| user_id | FK → users.id | 所有者 |
| title | VARCHAR(255) | タスク名 |
| is_completed | BOOLEAN | 完了フラグ |
| created_at | TIMESTAMP | 作成日時 |
3. API 設計
認証 API
| メソッド | エンドポイント | 説明 | リクエスト |
|---|---|---|---|
| POST | /api/auth/register |
ユーザー登録 | { email, password } |
| POST | /api/auth/login |
ログイン | { email, password } |
レスポンス例(ログイン成功):
{ "token": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "...", "email": "..." } }
タスク API(要認証)
| メソッド | エンドポイント | 説明 | リクエスト |
|---|---|---|---|
| GET | /api/tasks |
タスク一覧取得 | - |
| POST | /api/tasks |
タスク追加 | { title } |
ヘッダー: Authorization: Bearer <JWT>
4. 認証フロー
1. 登録/ログイン → サーバーがJWT発行 → クライアントがlocalStorageに保存
2. API呼び出し → Authorizationヘッダーに付与 → サーバーで検証
5. 推奨技術スタック(最小構成)
| レイヤー | 選択肢 |
|---|---|
| Frontend | React + Vite / Next.js / Vue |
| Backend | Node.js (Express) / Python (FastAPI) / Go (Gin) |
| Database | SQLite(開発)/ PostgreSQL(本番) |
| 認証 | JWT (jsonwebtoken / PyJWT) |
| パスワード | bcrypt |
6. セキュリティ要件(最低限)
- パスワードは必ずハッシュ化して保存(bcrypt, argon2)
- JWTには有効期限を設定(例: 24時間)
- HTTPS必須(本番環境)
- SQLインジェクション対策(プリペアドステートメント使用)
この設計で、登録・ログイン・タスク追加・一覧という基本機能が実現できます。拡張する場合は、タスクの編集・削除・完了切替などのエンドポイントを追加していく形になります。
具体的な実装コードや、特定の技術スタックでのサンプルが必要であればお知らせください。
3モデルの設計アプローチの違いまとめ
| 観点 | ChatGPT | Gemini | Claude |
|---|---|---|---|
| 設計スタイル | 必要最小限の構成を整理し、MVP を優先 | 三層構造+拡張性を意識したMVP | アーキテクチャ視点で層構造・制約まで定義 |
| 認証方式の扱い | セッション+Cookie を現実的な選択肢として提示 | JWT/セッション両対応前提 | JWT+bcrypt+HTTPS を重視 |
| データモデル | users / tasks の最小構成 | status など実務属性を含める | 型・制約レベルで定義し責務を明確化 |
| API 範囲 | 必要最低限のみ定義(MVP志向) | REST 4本を整理し標準的構成 | 認証/APIフローを体系的に記述 |
| UI | 必要画面のみ | 画面遷移(ログイン→一覧)まで具体化 | 設計思想に沿う説明 |
| 設計の特徴 | 実務で最小構成を作る際の判断に近い | 仕様書として読みやすい構造化 | アーキテクト的で厳密な設計思想 |
② 修正タスク: バグ入り Java コードをどう直すか
プロンプト(共通):
以下のJavaコードには意図的に複数の問題があります。
【依頼内容】
- 問題点を箇条書きで列挙してください
- 修正方針を短く説明してください
- 最小限の修正で、改善後のコードを提示してください
- 変更理由も簡潔に書いてください
使用したコード(プロンプトではエラーに関するコメントを削除しています):
package example.todo;
import java.time.LocalDateTime;
import java.util.*;
public class TaskService {
private final Map<Long, List<Task>> tasksByUser = new HashMap<>();
public List<Task> getTasks(Long userId) {
if (!tasksByUser.containsKey(userId)) {
tasksByUser.put(userId, new ArrayList<>());
}
List<Task> tasks = tasksByUser.get(userId);
// 元のリストをそのままソートしてしまう
tasks.sort(Comparator.comparing(Task::getCreatedAt));
return tasks; // 内部リストをそのまま返している
}
public void addTask(Long userId, String title) {
// title が null のときに NPE
if (title.isEmpty()) {
return;
}
// getTasks を呼んでいないと null の可能性あり
List<Task> tasks = tasksByUser.get(userId);
tasks.add(new Task(title, LocalDateTime.now())); // ここで NPE の可能性
}
}
// 想定される Task クラス(イメージ)
public class Task {
private String title;
private LocalDateTime createdAt;
public Task(String title, LocalDateTime createdAt) {
this.title = title;
this.createdAt = createdAt;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
// getter / setter などは省略
}
ChatGPT の回答(要約)
- 問題点を広く列挙(null、内部状態の公開、副作用など)
- 修正方針は 防御的コピー+nullチェック+computeIfAbsent の併用
- 修正量は最小限
- タスククラスの不変性改善も提案
→ 安全性・実務性のバランスが良く、丁寧な指摘が特徴。
ChatGPT の実際の回答(全文)
まず問題点からいきますね。
1. 問題点の列挙
-
addTaskで NPE の可能性(タイトル)-
if (title.isEmpty())はtitle == nullの場合にNullPointerExceptionを投げる。
-
-
addTaskで NPE の可能性(タスクリスト)-
List<Task> tasks = tasksByUser.get(userId);
userIdがまだ登録されていないとtasksはnullになり、
続くtasks.add(...)でNullPointerException。
-
-
getTasksが内部コレクションをそのまま返している-
tasksByUserに保持しているList<Task>をそのまま返しているため、
呼び出し側がListを変更すると内部状態が壊れる(カプセル化の破れ)。
-
-
getTasksで内部リストを毎回ソートしている-
tasks.sort(...)により内部リストを都度ソートし、副作用がある。
呼び出し側の変更と相まって状態が読みにくくなる。
-
-
存在しないユーザーに対して空リストを Map に永続化している
-
getTasks内のif (!tasksByUser.containsKey(userId)) { tasksByUser.put(userId, new ArrayList<>()); }により、
「タスクをまだ持たないユーザー」が呼び出されるたびに Map に登録される(不要な副作用)。
-
※ Task クラスは「イメージ」と書かれているので、コンパイル上の細かい問題(別ファイルにすべきなど)はここでは無視しています。
2. 修正方針(短く)
-
addTask-
titleの null / 空文字を防御的にチェックする。 -
tasksByUserからリスト取得時にcomputeIfAbsent等で必ず非 null を確保する。
-
-
getTasks- Map に余計な空リストを保存しない。
- 内部のリストを直接返さず、コピーをソートして返す。
3. 最小限の修正を加えた改善後コード
package example.todo;
import java.time.LocalDateTime;
import java.util.*;
public class TaskService {
private final Map<Long, List<Task>> tasksByUser = new HashMap<>();
public List<Task> getTasks(Long userId) {
// Map に持っているリストはそのまま使うが、呼び出し側にはコピーを返す
List<Task> tasks = tasksByUser.getOrDefault(userId, Collections.emptyList());
List<Task> result = new ArrayList<>(tasks);
result.sort(Comparator.comparing(Task::getCreatedAt));
return result;
}
public void addTask(Long userId, String title) {
// null / 空文字チェックを追加
if (title == null || title.isEmpty()) {
return;
}
// ユーザーごとのリストを必ず非nullで取得
List<Task> tasks = tasksByUser.computeIfAbsent(userId, k -> new ArrayList<>());
tasks.add(new Task(title, LocalDateTime.now()));
}
}
// 想定される Task クラス(イメージ)
public class Task {
private final String title;
private final LocalDateTime createdAt;
public Task(String title, LocalDateTime createdAt) {
this.title = title;
this.createdAt = createdAt;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
// getter / setter などは省略
}
4. 変更理由(簡潔に)
-
title == null || title.isEmpty()に変更
→nullが渡されてもNullPointerExceptionを防ぎ、値として不正なものは早期リターン。 -
computeIfAbsent(userId, k -> new ArrayList<>())を使用
→ ユーザーのタスクリストを安全に初期化でき、tasksが null になることを防ぐ。 -
getOrDefault(userId, Collections.emptyList())+ コピー作成
→ 存在しないユーザーに対して Map に副作用を与えずに、空リスト相当を扱える。
→ 内部リストを直接返さずnew ArrayList<>(tasks)とすることで、呼び出し側からの変更が内部状態に影響しない。 -
コピー側
resultをソートして返却
→ ソートという副作用を内部状態に与えず、「取得結果の並び順」だけを整える。 -
Taskのフィールドをfinalに変更
→ Task インスタンスを基本的に不変オブジェクトとして扱うことで、扱いやすくし、状態の予期せぬ変更を防ぐ。
Gemini の回答(要約)
- 問題点を自然文で構造化し、NPE・不変性破壊・状態不整合を丁寧に説明
- 修正は computeIfAbsent に統一し、初期化ロジックを一元化
- trim().isEmpty() により空白文字列も不正と判断
- getTasks は防御的コピーを返却し、不変性を確保
- 修正コードは標準的で読みやすく、コメントも丁寧
→ 実装と仕様整理を両立させた“安定した修正方針”
Gemini の実際の回答(全文)
提示されたコードは、一見動作しそうに見えますが、NullPointerExceptionの発生や**副作用(意図しないデータの書き換え)**など、重大な欠陥が含まれています。
以下に分析と修正案を提示します。
1. 問題点の列挙
-
致命的な
NullPointerException(addTaskメソッド) -
addTask内でtasksByUser.get(userId)を行っていますが、そのユーザーがまだ登録されていない場合、nullが返ります。その直後のtasks.add(...)でぬるぽ(NPE)が発生します。 -
Getterによる状態の破壊(副作用)
-
getTasks内でtasks.sort(...)を実行しています。これは Map 内に保存されているオリジナルのリストの順序を永続的に変更してしまいます。参照のみのメソッドであるはずが、内部状態を変えてしまっています。 -
カプセル化の欠如
-
getTasksが Map 内のリストの参照そのものを返しています。呼び出し元が返されたリストを操作(clearやadd)すると、TaskService内部のデータも変わってしまいます。 -
不十分なバリデーション
-
titleがnullの場合、title.isEmpty()でNullPointerExceptionが発生します。 -
スレッドセーフではない (潜在的リスク)
-
Serviceクラスは通常シングルトンで扱われますが、標準のHashMapやArrayListはスレッドセーフではありません。同時アクセスでデータが破損します(今回は「最小限の修正」のため、ロジックエラーの修正を優先します)。
2. 修正方針
-
Map操作の安全化:
computeIfAbsentなどを使い、リストが存在しない場合の初期化を一元管理します。 -
防御的コピー:
getTasksでは、内部リストのコピーを作成し、そのコピーをソートして返すことで、内部状態と外部への戻り値を分離します。 -
Nullチェック: タイトルの
nullチェックを追加します。
3. 修正後のコード (最小限の修正)
package example.todo;
import java.time.LocalDateTime;
import java.util.*;
public class TaskService {
// 補足: マルチスレッド環境なら ConcurrentHashMap を推奨
private final Map<Long, List<Task>> tasksByUser = new HashMap<>();
public List<Task> getTasks(Long userId) {
// 存在しない場合は空リストを扱う(Mapへの副作用なし)
List<Task> tasks = tasksByUser.getOrDefault(userId, Collections.emptyList());
// 修正: 内部リストを守るため、コピーを作成してソートする
List<Task> copyTasks = new ArrayList<>(tasks);
copyTasks.sort(Comparator.comparing(Task::getCreatedAt));
return copyTasks;
}
public void addTask(Long userId, String title) {
// 修正: nullチェックを追加
if (title == null || title.isEmpty()) {
return;
}
// 修正: キーが存在しない場合は生成し、存在する場合は取得する (NPE回避)
tasksByUser.computeIfAbsent(userId, k -> new ArrayList<>())
.add(new Task(title, LocalDateTime.now()));
}
}
4. 変更理由
| 変更箇所 | 理由 |
|---|---|
computeIfAbsent の使用 |
addTask 時にリストが未作成でも自動生成され、NullPointerException を防ぐため。コードも簡潔になります。 |
getTasks でのコピー作成 |
tasks.sort が内部データを直接書き換えるのを防ぎ、かつ呼び出し元によるリスト操作が TaskService 内部に影響しないようにするため(カプセル化)。 |
getOrDefault の使用 |
getTasks を呼ぶだけで Map に空リストのエントリーが増える(メモリの無駄遣い)のを防ぐため。 |
title == null 追加 |
タイトルが null の場合に落ちるのを防ぐため。 |
次のステップ
マルチスレッド環境(Webアプリケーションなど)での利用を想定されていますか?その場合は、HashMap を ConcurrentHashMap に変更し、リスト操作の同期化も検討する必要があります。ご希望であればスレッドセーフ版も提示できます。
Claude の回答(要約)
- 問題点を最小限に圧縮して整理(NPE・不変性・入力検証)
- 修正方針は computeIfAbsent に一本化
- title の検証を isBlank() とするなど Java 11+ の特性を自然に取り入れる
- 修正コードは最も短く、設計意図が一貫している
→ 厳密さを保ちつつ最短の修正で整合性を確保する判断
Claude の実際の回答(全文)
1. 問題点の列挙
-
addTaskでNullPointerException:tasksByUser.get(userId)がnullを返す可能性があるが、チェックなしでaddを呼んでいる -
titleのnullチェック漏れ:title.isEmpty()の前にnullチェックがなく、NPEが発生しうる -
内部リストの直接公開:
getTasksが内部リストをそのまま返しており、外部から変更可能(カプセル化違反) -
空白のみの文字列が許容される:
isEmpty()は" "のような空白文字列を検出しない
2. 修正方針
| 問題 | 方針 |
|---|---|
| addTaskのNPE |
computeIfAbsentでリスト取得と初期化を一元化 |
| titleのnullチェック |
title == nullを条件に追加 |
| 内部リスト公開 | 防御的コピー(new ArrayList<>())を返す |
| 空白文字列 |
isBlank()(Java 11+)に変更 |
3. 改善後のコード
package example.todo;
import java.time.LocalDateTime;
import java.util.*;
public class TaskService {
private final Map<Long, List<Task>> tasksByUser = new HashMap<>();
public List<Task> getTasks(Long userId) {
List<Task> tasks = tasksByUser.computeIfAbsent(userId, k -> new ArrayList<>());
tasks.sort(Comparator.comparing(Task::getCreatedAt));
return new ArrayList<>(tasks); // 防御的コピー
}
public void addTask(Long userId, String title) {
if (title == null || title.isBlank()) { // null + 空白チェック
return;
}
List<Task> tasks = tasksByUser.computeIfAbsent(userId, k -> new ArrayList<>());
tasks.add(new Task(title, LocalDateTime.now()));
}
}
4. 変更理由
| 変更箇所 | 理由 |
|---|---|
computeIfAbsent |
存在チェックと初期化を1行で行い、NPEを防止。両メソッドで統一 |
title == null || title.isBlank() |
null参照と空白のみの文字列を同時に弾く |
new ArrayList<>(tasks) |
呼び出し元が内部状態を破壊できないようにする |
3モデルの“直し方の違い”まとめ
| 観点 | ChatGPT | Gemini | Claude |
|---|---|---|---|
| 問題点の網羅性 | 広く細かい) | 丁寧に整理・構造化 | 必要最小限に圧縮 |
| 初期化方針 |
getOrDefault + computeIfAbsent
|
双方 computeIfAbsent で統一 |
computeIfAbsent |
| 内部リスト処理 | コピー返却 | コピー返却 | 内部ソート後コピー |
| 文字列検証 | null + isEmpty | null + trim().isEmpty | null + isBlank |
| 設計判断 | 実務改善を幅広く適用 | 標準的・丁寧な整理 | 厳密で短い修正 |
③ コード品質タスク: ISO週番号メソッドの比較
プロンプト(共通):
次の処理を行う Java メソッドを作成してください。
【要件】
- 引数:日付文字列 (YYYY-MM-DD)
- 戻り値:その日付が属する ISO 週番号 (week-of-year)
- Java 標準APIを使用すること
- 必要に応じて入力値の検証も行ってよい
【依頼内容】
- まず、どのような方針で実装するかを簡潔に説明してください
- 次に、可読性が高く、安全性も考慮した「最小限のコード」を書いてください
- 入力が不正な場合の扱い(例外 or Optional など)を明記してください
- 使用した API(LocalDate, DateTimeFormatter など)の理由を説明してください
【禁止事項】
- 過度に複雑な設計にしないでください
- 独自のパーサや正規表現で日付を手作業で解析しないでください
ChatGPT の回答(要約)
- Optional を用いた安全な返却方式
→ 不正入力時はOptional.empty()を返す - DateTimeFormatter によりフォーマットを明示し、外部入力の曖昧性を排除
- 例外は Optional で扱い、呼び出し側に影響させない設計
→ 実務寄りで、API使用の明示性や入力処理の安全度が高い
ChatGPT の実際の回答(全文)
以下にご要望どおり回答します。
1. 実装方針(簡潔)
-
LocalDateとDateTimeFormatterを使って日付文字列を安全にパースする -
WeekFields.ISOを利用して ISO-8601 準拠の週番号を取得する - 入力が不正な場合は Optional.empty() を返す(例外を表に出さない安全設計)
2. 可読性・安全性を意識した最小限のコード
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.WeekFields;
import java.util.Locale;
import java.util.Optional;
public class IsoWeekCalculator {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static Optional<Integer> getIsoWeekNumber(String dateStr) {
try {
LocalDate date = LocalDate.parse(dateStr, FORMATTER);
int week = date.get(WeekFields.ISO.weekOfWeekBasedYear());
return Optional.of(week);
} catch (DateTimeParseException e) {
return Optional.empty(); // 入力が不正
}
}
}
3. 入力が不正な場合の扱い
-
DateTimeParseExceptionをキャッチし、Optional.empty() を返す - これにより、呼び出し側は
isPresent()でチェックでき、例外制御に依存しない安全なコードとなる
4. 使用 API の理由
LocalDate
- ISOベースの日付演算を正確に扱える
- 不正日付(例: 2024-13-99)を自動的に検出して例外を投げてくれる
DateTimeFormatter
- 指定したフォーマット (
yyyy-MM-dd) で安全にパース可能 - 独自の正規表現などを書かずに済む(要件に適合)
WeekFields.ISO
- ISO-8601 に準拠した週番号(週は月曜開始、週1は年最初の木曜を含む週)を取得可能
-
get(weekOfWeekBasedYear())で ISO week-of-year を正確に取得できる
Optional
- 入力不正時に例外を外に漏らさず、呼び出し側が安全に扱えるデザインのため
必要なら以下も作成できます:
- 例外を投げるバージョン
- 年+週番号を返すバージョン
- JUnit テストコード
何か追加しますか?
Gemini の回答(要約)
- LocalDate.parse による ISO 形式のパースを採用し、フォーマッタは省略(ISO形式前提)
- ISO週番号は WeekFields.ISO を使用して取得
- 不正入力は IllegalArgumentException を投げる
- 例外にはパース例外(DateTimeParseException)をラップして詳細を含める
- 入力検証と API 選択理由を明確に説明している
→ API仕様と入力妥当性を明確にし、実装意図を文章で整理するスタイルが特徴的
Gemini の実際の回答(全文)
ご提示いただいた要件に基づき、Java標準API(java.time パッケージ)を使用したメソッドを作成します。
1. 実装方針
-
パース処理:
LocalDate.parse()を使用します。これはデフォルトでYYYY-MM-DD(ISO-8601) 形式を受け入れるため、標準的かつ安全です。 -
週番号の取得: 単純な「年間の何週目か」ではなく、ISO-8601規格(月曜始まり、最初の週は4日以上など)に準拠するため、
WeekFields.ISOを使用します。 -
エラー処理: 入力が
nullまたは不正なフォーマットの場合、呼び出し元に原因を明確に伝えるため、実行時例外(IllegalArgumentException)をスローする設計とします。
2. Java コード
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.WeekFields;
public class DateUtils {
/**
* 指定された日付文字列 (YYYY-MM-DD) の ISO 週番号を取得します。
*
* @param dateStr 日付文字列 (例: "2023-12-25")
* @return ISO 週番号 (1〜53)
* @throws IllegalArgumentException 日付形式が不正、または null の場合
*/
public static int getIsoWeekNumber(String dateStr) {
if (dateStr == null) {
throw new IllegalArgumentException("Input date string must not be null.");
}
try {
// 1. 文字列を LocalDate にパース (ISO_LOCAL_DATE形式)
LocalDate date = LocalDate.parse(dateStr);
// 2. ISO-8601 規格に基づいて週番号を取得
return date.get(WeekFields.ISO.weekOfWeekBasedYear());
} catch (DateTimeParseException e) {
// 解析失敗時は、原因を含めた例外として再送出
throw new IllegalArgumentException("Invalid date format: " + dateStr, e);
}
}
}
3. 解説
入力が不正な場合の扱い
IllegalArgumentException をスローします。
-
理由: メソッドの事前条件(正しい日付形式であること)を満たしていないことを明確にするためです。
Optional.empty()を返す設計も可能ですが、「計算不能」ではなく「入力ミス(バグ)」の可能性が高いため、例外で通知する方が呼び出し元でのハンドリング漏れを防げます。
使用した API の理由
| API | 採用理由 |
|---|---|
LocalDate |
日付のみ(時刻なし)を扱うための不変でスレッドセーフな標準クラスです。YYYY-MM-DD 形式のパース処理を内部で厳密に行ってくれます。 |
WeekFields.ISO |
最も重要なポイントです。 通常の ChronoField.ALIGNED_WEEK_OF_YEAR などでは、ロケール依存や定義の違いにより ISO-8601 準拠(月曜始まりなど)にならない場合があります。WeekFields.ISO を明示することで、環境に依存せず正確な ISO 週番号を算出できます。 |
weekOfWeekBasedYear() |
「その年」の週番号ではなく、「週ベースの年(Week-based Year)」における週番号を取得するためです。年末年始には、日付上の年と週の所属する年が異なるケース(例:1月1日が前年の53週目になる等)があるため、このフィールド指定が必須です。 |
ご希望であれば、**「年末年始の境界値(例:2023-01-01 など)を用いたJUnitテストコード」**も作成できますが、いかがなさいますか?
Claude の回答(要約)
- Optional ではなく例外を投げる方式
-
LocalDate.parseの ISO 対応を前提にフォーマッタ省略 -
IsoFieldsを採用し、ISO週番号の定義への依存を明確化 - Optional版との比較まで記述
→ 入力値の正当性を例外で強制し、API 仕様に基づく厳密な選択を行う
Claude の実際の回答(全文)
実装方針
基本方針:
-
LocalDate.parse()で文字列をパース(YYYY-MM-DD は ISO 8601 形式なのでデフォルトで対応) -
IsoFields.WEEK_OF_WEEK_BASED_YEARで ISO 週番号を取得 - 不正入力は 例外をスロー する方式を採用(呼び出し側で明示的に処理を強制)
実装コード
import java.time.LocalDate;
import java.time.DateTimeException;
import java.time.temporal.IsoFields;
public class IsoWeekCalculator {
/**
* 日付文字列から ISO 週番号を取得する。
*
* @param dateString 日付文字列(YYYY-MM-DD 形式)
* @return ISO 週番号(1〜53)
* @throws IllegalArgumentException 入力が null または不正な形式の場合
*/
public static int getIsoWeekNumber(String dateString) {
if (dateString == null) {
throw new IllegalArgumentException("日付文字列が null です");
}
try {
LocalDate date = LocalDate.parse(dateString);
return date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
} catch (DateTimeException e) {
throw new IllegalArgumentException(
"不正な日付形式です: " + dateString, e);
}
}
}
使用例
public static void main(String[] args) {
System.out.println(getIsoWeekNumber("2024-01-01")); // 1
System.out.println(getIsoWeekNumber("2024-12-31")); // 1(2025年の第1週に属する)
System.out.println(getIsoWeekNumber("2023-01-01")); // 52(2022年の第52週に属する)
// 不正な入力
getIsoWeekNumber("invalid"); // IllegalArgumentException
getIsoWeekNumber(null); // IllegalArgumentException
}
不正入力の扱い
| 入力パターン | 挙動 |
|---|---|
null |
IllegalArgumentException をスロー |
| 空文字・不正形式 |
IllegalArgumentException をスロー(原因として DateTimeException を保持) |
| 存在しない日付(例: 2024-02-30) |
IllegalArgumentException をスロー |
API 選定理由
| API | 理由 |
|---|---|
LocalDate |
日付のみを扱う(時刻不要)ため最適。イミュータブルでスレッドセーフ |
LocalDate.parse() |
YYYY-MM-DD は ISO 8601 のデフォルト形式なので DateTimeFormatter 指定不要 |
IsoFields.WEEK_OF_WEEK_BASED_YEAR |
ISO 8601 準拠の週番号を直接取得できる標準フィールド |
IllegalArgumentException |
入力値の問題を示す標準的な非検査例外。呼び出し側の柔軟性を確保 |
補足:Optional 版が必要な場合
例外よりも Optional で返したい場合は以下のように変更できます:
import java.util.Optional;
public static Optional<Integer> getIsoWeekNumberSafe(String dateString) {
if (dateString == null) {
return Optional.empty();
}
try {
LocalDate date = LocalDate.parse(dateString);
return Optional.of(date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR));
} catch (DateTimeException e) {
return Optional.empty();
}
}
どちらを選ぶかは、呼び出し側で「不正入力を例外的状況として扱うか」「通常の制御フローで処理したいか」によって決めてください。
3モデルの「コード品質」の違いまとめ
| 観点 | ChatGPT | Gemini | Claude |
|---|---|---|---|
| 不正入力 | Optional.empty | IllegalArgumentException | IllegalArgumentException |
| 入力検証 | null+isEmpty | null のみ(空文字・空白は parse で例外) | null+isBlank |
| ISO週番号API | WeekFields.ISO | WeekFields.ISO(説明が丁寧) | IsoFields(厳密) |
| パース方法 | フォーマッタ指定 | ISO形式前提でフォーマッタ省略 | ISO形式前提でフォーマッタ省略 |
| コード量 | 標準的 | 標準的+説明重視 | 最小限・厳密 |
まとめ
今回の 3 つの比較タスク(設計/バグ修正/コード品質)から、
各モデルの技術判断の傾向は明確に分かれた。
ChatGPT
- 要件範囲で最小限の実装を優先する
- 安全性と実務性のバランスを取りやすい
- 入力検証・内部状態管理などを網羅的にカバーする
Gemini
- 設計や仕様を整理しながら構造的にまとめる
- 必要に応じて入力やデータ構造を少し広めに扱い、実用性とのバランスを取る
- 設計やコードの背景や意図を文章で明確に説明する傾向がある
Claude
- 厳密な API 選定・例外処理・責務分離を重視する
- 最小コードで整合性を取る
- 仕様の前提(ISO 規格など)を正確に扱う
使い分け
以下は、今回の比較結果を踏まえた、私個人の主観的な使い分けイメージです。
ChatGPT
- まず動く設計やコードのたたき台がほしい
- 過度に仕様を広げず、最小限の改善だけ欲しい
- 実務アプリにそのまま適用できるコードが必要
Gemini
- 設計や仕様を整理しながら進めたい
- コードや設計の背景となる意図を文章で把握したい
- 必要に応じて少し広めに判断してほしい(属性の追加・妥当性の補強など)
Claude
- API 選定・例外設計など、厳密な判断を確認したい
- シンプルながら整合性のある設計・修正を見たい
- セキュリティ・仕様遵守が重要な領域を扱う
おわりに
短いプロンプトでもAIごとに思った以上に判断が分かれ、とても興味深い結果となりました。
同じ課題でも別の視点が返ってくるため、複数モデルを併用する価値を改めて実感できる比較となりました。
どなたでも簡単に再現できる内容なので、ぜひ皆さんも色んなプロンプトで試してみてください。
