はじめに
プログラミング講師をしていると、「コピーしたはずなのに、片方を変更したらもう片方も変わってしまいました!」という質問を頻繁に受けます。これは参照型の理解不足が原因で起こる、初心者が必ず一度はハマる落とし穴です。
本記事では、RPGゲームの「アイテム複製システム」と「装備の譲渡システム」を例に、参照型・シャローコピー・ディープコピーの違いを徹底的に解説します。
前回までのおさらい:
- 第1回:オブジェクト指向って何?RPGで理解する超入門
- 第2回:オブジェクト指向って何?RPGで理解する超入門 (配列とループ)
- 第3回:オブジェクト指向って何?RPGで理解する超入門 (フィールドとメソッド)
- 第4回:オブジェクト指向って何?RPGで理解する超入門 (コンストラクタ)
- 第5回:オブジェクト指向って何?RPGで理解する超入門 (カプセル化)
この記事で学べること
- 参照型とメモリ構造の詳細な理解
- シャローコピーとディープコピーの違い
- null問題と適切な対処法
- ==とequals()の使い分け
- 実践的なゲームシステムの実装
対象読者
- Javaの基本文法を学んだ方
- クラスとオブジェクトの基礎を理解している方
- 参照型で不思議な挙動に遭遇した方
- メモリの仕組みをしっかり理解したい方
1. 参照型とメモリ構造を理解する
1.1 参照型とは何か?
Javaのデータ型には、基本型(プリミティブ型)と参照型(リファレンス型) の2種類があります。
基本型(8種類)
変数そのものに「値」が入る
- 整数型:
byte,short,int,long - 浮動小数点型:
float,double - 文字型:
char - 論理型:
boolean
参照型
変数には「値の置き場所(参照)」が入る
- クラス
- インターフェース(
List,Map,Setなど) - 配列
- 列挙型
- レコードクラス
- ラッパークラス(
Integer,Double,Booleanなど)
1.2 メモリ構造:スタックとヒープ
Javaのメモリは大きくスタック領域とヒープ領域に分かれています。
スタック領域(Stack Area)
- ローカル変数や参照が保存される
- メソッド呼び出しごとに作成・削除される
- 高速だが容量は小さい
- 自動的に管理される
ヒープ領域(Heap Area)
- 実際のオブジェクトが保存される
-
new演算子で作成される - ガベージコレクタが不要なオブジェクトを削除
- 容量は大きいが、スタックより遅い
/**
* メモリ構造のデモンストレーション
*/
public class MemoryDemo {
public static void main(String[] args) {
// スタックに変数が作られる
int damage = 10; // 基本型:値そのものが保存
String name = "勇者"; // 参照型:アドレスが保存
// ヒープにオブジェクトが作られる
Player hero = new Player("勇者", 100);
// heroはスタックにあり、Playerオブジェクトへのアドレスを保持
// 実際のPlayerオブジェクトはヒープにある
}
}
1.3 参照の代入とは
分かりやすく言うとこうです
- 参照型の変数には「オブジェクトの住所(参照)」が入ります(実体は別にあります)
- 代入するときは「住所」がコピーされるだけでオブジェクト自体は新しく作られません
/**
* 参照のコピーのデモ
*/
public class ReferenceCopyDemo {
public static void main(String[] args) {
// オリジナルのプレイヤー作成
// ここで変数 hero にオブジェクトの住所(参照)が入る
Player hero = new Player("勇者", 100);
// ここで変数「anotherHero」 に 変数「hero」 の住所(参照) を代入
// 同じオブジェクトを指すようになる
Player anotherHero = hero;
// 変数「anotherHero」の HP を設定すると...
anotherHero.setHp(50);
// 変数「hero」の HP も変わってしまう!
System.out.println(hero.getHp()); // 出力: 50
// なぜなら、hero と anotherHero は同じオブジェクトを指しているから
System.out.println(hero == anotherHero); // 出力: true
}
}
ポイント:
- 代入するときは 住所(参照) がコピーされるだけ
オブジェクト自体は新しく作られません - 両方の変数が同じオブジェクトを指す
- 片方を変更すると、もう片方も影響を受ける
- 新しい別のオブジェクトを作りたい場合は、別の new Player(...) などを使う
2. シャローコピー:参照だけをコピーする
2.1 シャローコピーとは
シャローコピー(Shallow Copy) は、オブジェクトの表面だけをコピーする方法です。オブジェクト内の参照型フィールドは、参照(アドレス)がコピーされるだけで、実体は共有されます。
2.2 シャローコピーの実装例
/**
* 武器クラス
*/
public class Weapon {
private String name;
private int attack;
/**
* コンストラクタ
* @param name 武器名
* @param attack 攻撃力
*/
public Weapon(String name, int attack) {
this.name = name;
this.attack = attack;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAttack() { return attack; }
public void setAttack(int attack) { this.attack = attack; }
}
/**
* プレイヤークラス(シャローコピー版)
*/
public class Player {
private String name;
private int hp;
private Weapon weapon; // 参照型フィールド
/**
* コンストラクタ
* @param name プレイヤー名
* @param hp HP
* @param weapon 装備武器
*/
public Player(String name, int hp, Weapon weapon) {
this.name = name;
this.hp = hp;
this.weapon = weapon;
}
/**
* シャローコピーを作成
* @return コピーされたPlayerオブジェクト
*/
public Player shallowCopy() {
// 基本型はコピーされるが、参照型は参照がコピーされる
return new Player(this.name, this.hp, this.weapon);
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getHp() { return hp; }
public void setHp(int hp) { this.hp = hp; }
public Weapon getWeapon() { return weapon; }
public void setWeapon(Weapon weapon) { this.weapon = weapon; }
}
2.3 シャローコピーの問題点
/**
* シャローコピーの問題を示すデモ
*/
public class ShallowCopyProblem {
public static void main(String[] args) {
// オリジナルの勇者を作成
Weapon sword = new Weapon("鉄の剣", 30);
Player hero = new Player("勇者", 100, sword);
// シャローコピーで仲間を作成
Player ally = hero.shallowCopy();
// 仲間の名前とHPを変更(問題なし)
ally.setName("戦士");
ally.setHp(120);
System.out.println("勇者のHP: " + hero.getHp()); // 出力: 100(変わらない)
System.out.println("戦士のHP: " + ally.getHp()); // 出力: 120
// しかし、武器を強化すると...
ally.getWeapon().setAttack(50);
// 勇者の武器も変わってしまう!
System.out.println("勇者の武器攻撃力: " + hero.getWeapon().getAttack()); // 出力: 50
System.out.println("戦士の武器攻撃力: " + ally.getWeapon().getAttack()); // 出力: 50
// 同じWeaponオブジェクトを共有している
System.out.println(hero.getWeapon() == ally.getWeapon()); // 出力: true
}
}
問題点:
- 基本型フィールド(name, hp)は独立してコピーされる
- 参照型フィールド(weapon)は住所(参照)がコピーされるだけ
- 片方のweaponを変更すると、もう片方も影響を受ける
3. ディープコピー:完全なコピーを作る
3.1 ディープコピーとは
ディープコピー(Deep Copy) は、オブジェクト全体を完全にコピーする方法です。参照型フィールドも、新しいオブジェクトとして複製されます。
👉 オブジェクトの中身まで、全部そっくり新しく作るコピー のことです。
3.2 ディープコピーの実装例
/**
* 武器クラス(コピー機能付き)
*/
public class Weapon {
private String name;
private int attack;
/**
* コンストラクタ
* @param name 武器名
* @param attack 攻撃力
*/
public Weapon(String name, int attack) {
this.name = name;
this.attack = attack;
}
/**
* ディープコピーを作成
* @return 新しいWeaponオブジェクト
*/
public Weapon deepCopy() {
return new Weapon(this.name, this.attack);
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAttack() { return attack; }
public void setAttack(int attack) { this.attack = attack; }
}
/**
* プレイヤークラス(ディープコピー版)
*/
public class Player {
private String name;
private int hp;
private Weapon weapon;
/**
* コンストラクタ
* @param name プレイヤー名
* @param hp HP
* @param weapon 装備武器
*/
public Player(String name, int hp, Weapon weapon) {
this.name = name;
this.hp = hp;
this.weapon = weapon;
}
/**
* ディープコピーを作成
* @return 完全にコピーされたPlayerオブジェクト
*/
public Player deepCopy() {
// 参照型フィールドもディープコピーする
Weapon copiedWeapon = (this.weapon != null) ? this.weapon.deepCopy() : null;
return new Player(this.name, this.hp, copiedWeapon);
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getHp() { return hp; }
public void setHp(int hp) { this.hp = hp; }
public Weapon getWeapon() { return weapon; }
public void setWeapon(Weapon weapon) { this.weapon = weapon; }
}
3.3 ディープコピーの動作確認
/**
* ディープコピーのデモ
*/
public class DeepCopyDemo {
public static void main(String[] args) {
// オリジナルの勇者を作成
Weapon sword = new Weapon("鉄の剣", 30);
Player hero = new Player("勇者", 100, sword);
// ディープコピーで仲間を作成
Player ally = hero.deepCopy();
// 仲間の情報を変更
ally.setName("戦士");
ally.setHp(120);
ally.getWeapon().setAttack(50);
// 勇者の情報は変わらない!
System.out.println("勇者のHP: " + hero.getHp()); // 出力: 100
System.out.println("戦士のHP: " + ally.getHp()); // 出力: 120
System.out.println("勇者の武器攻撃力: " + hero.getWeapon().getAttack()); // 出力: 30
System.out.println("戦士の武器攻撃力: " + ally.getWeapon().getAttack()); // 出力: 50
// 別々のWeaponオブジェクト
System.out.println(hero.getWeapon() == ally.getWeapon()); // 出力: false
}
}
ポイント:
- 参照型フィールドも新しいオブジェクトとしてコピー
- 完全に独立した2つのオブジェクトが作られる
- 片方を変更しても、もう片方は影響を受けない
4. null問題と対策
4.1 NullPointerExceptionとは
nullは「何も参照していない」状態を表す特別な値。
nullのオブジェクトのメソッドを呼び出すと、NullPointerExceptionが発生します。
- 参照型(例えば String や自作のクラスの変数)にだけ使える
- つまり「この変数には“オブジェクトの住所”がまだ入っていない」という状態
4.2 null問題の例
/**
* null問題のデモ
*/
public class NullProblemDemo {
public static void main(String[] args) {
// 武器を持っていないプレイヤー
Player hero = new Player("勇者", 100, null);
// 武器の攻撃力を取得しようとすると...
try {
int power = hero.getWeapon().getAttack(); // ❌ NullPointerException!
System.out.println("攻撃力: " + power);
} catch (NullPointerException e) {
System.out.println("エラー: 武器が装備されていません");
}
}
}
4.3 安全に使うコツ
- 数値は primitive 型は
nullにできない
nullにしたいときはIntegerなどのラッパークラスを使う - 使う前に
nullかどうかを確認する
方法1: if文でチェック
/**
* if文によるnullチェック
* @param player プレイヤー
* @return 武器の攻撃力(武器がない場合は0)
*/
public int getWeaponPower(Player player) {
if (player.getWeapon() != null) {
return player.getWeapon().getAttack();
} else {
return 0; // デフォルト値
}
}
方法2: 三項演算子によるnullチェック
/**
* 三項演算子によるnullチェック
* @param player プレイヤー
* @return 武器の攻撃力(武器がない場合は0)
*/
public int getWeaponPower(Player player) {
return (player.getWeapon() != null) ? player.getWeapon().getAttack() : 0;
}
方法3: Optionalの利用(Java 8以降)
Optional.ofNullable(x) は「x が null かもしれないときに、安全に包む箱」です。
null:何も入ってない段ボール
Optional:フタに「中身あり/なし」が書いてある箱
import java.util.Optional;
/**
* Optionalによるnullチェック
*/
public class OptionalDemo {
/**
* Optionalを使った安全な取得
* @param player プレイヤー
* @return 武器の攻撃力(武器がない場合は0)
*/
public int getWeaponPower(Player player) {
return Optional.ofNullable(player.getWeapon())
.map(Weapon::getAttack)
.orElse(0);
}
}
4.4 nullを避けるベストプラクティス
/**
* nullを避ける設計例
*/
public class BestPracticeDemo {
/**
* デフォルト武器を用意する
*/
private static final Weapon DEFAULT_WEAPON = new Weapon("素手", 5);
/**
* nullを返さないコンストラクタ
* @param name プレイヤー名
* @param hp HP
* @param weapon 武器(nullの場合はデフォルト武器)
*/
public BestPracticeDemo(String name, int hp, Weapon weapon) {
this.name = name;
this.hp = hp;
// nullの場合はデフォルト武器を設定
this.weapon = (weapon != null) ? weapon : DEFAULT_WEAPON;
}
private String name;
private int hp;
private Weapon weapon;
// getter/setter
public Weapon getWeapon() {
// 常にnullでない値を返す
return weapon;
}
public void setWeapon(Weapon weapon) {
// nullを受け取ったらデフォルト武器を設定
this.weapon = (weapon != null) ? weapon : DEFAULT_WEAPON;
}
}
デフォルト値について:
- クラスのフィールドとして宣言しただけの場合、初期値は
nullのことが多い - ローカル変数には初期値が自分で設定されるまで何も入っていない状態(コンパイルは通らないことが多い)
nullを安全に扱うルール:
- 初期化時にnullを避ける - デフォルト値を設定
- 使用前に必ずチェック - if文やOptionalで確認
- 戻り値にnullを返さない - 空のオブジェクトやOptionalを使う
- Optionalを活用 - Java 8以降の推奨方法
5. ==とequals()の違い
5.1 ==演算子:参照の比較
==演算子は、参照(アドレス)が同じかどうかを比較します。
/**
* ==演算子のデモ
*/
public class EqualsOperatorDemo {
public static void main(String[] args) {
Player hero1 = new Player("勇者", 100, null);
Player hero2 = new Player("勇者", 100, null);
Player hero3 = hero1;
// 異なるオブジェクト
System.out.println(hero1 == hero2); // 出力: false
// 同じオブジェクト
System.out.println(hero1 == hero3); // 出力: true
}
}
5.2 equals()メソッド:内容の比較
equals()メソッドは、オブジェクトの内容が同じかどうかを比較します(正しくオーバーライドされている場合)。
5.3 equals()のオーバーライド
/**
* equals()とhashCode()を実装したPlayerクラス
*/
public class Player {
private String name;
private int hp;
private Weapon weapon;
// コンストラクタ、getter/setterは省略
/**
* オブジェクトの内容を比較
* @param obj 比較対象
* @return 内容が同じならtrue
*/
@Override
public boolean equals(Object obj) {
// 1. 同じ参照ならtrue
if (this == obj) {
return true;
}
// 2. nullまたは異なるクラスならfalse
if (obj == null || getClass() != obj.getClass()) {
return false;
}
// 3. 各フィールドを比較
Player other = (Player) obj;
// 名前の比較
if (!this.name.equals(other.name)) {
return false;
}
// HPの比較
if (this.hp != other.hp) {
return false;
}
// 武器の比較(nullチェック付き)
if (this.weapon == null) {
return other.weapon == null;
} else {
return this.weapon.equals(other.weapon);
}
}
/**
* ハッシュコードを生成
* @return ハッシュコード
*/
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + hp;
result = 31 * result + (weapon != null ? weapon.hashCode() : 0);
return result;
}
}
5.4 equals()の使用例
/**
* equals()のデモ
*/
public class EqualsDemo {
public static void main(String[] args) {
Weapon sword1 = new Weapon("鉄の剣", 30);
Weapon sword2 = new Weapon("鉄の剣", 30);
Player hero1 = new Player("勇者", 100, sword1);
Player hero2 = new Player("勇者", 100, sword2);
Player hero3 = hero1;
// ==で比較(参照の比較)
System.out.println("== で比較:");
System.out.println("hero1 == hero2: " + (hero1 == hero2)); // 出力: false
System.out.println("hero1 == hero3: " + (hero1 == hero3)); // 出力: true
// equals()で比較(内容の比較)
System.out.println("\nequals() で比較:");
System.out.println("hero1.equals(hero2): " + hero1.equals(hero2)); // 出力: true
System.out.println("hero1.equals(hero3): " + hero1.equals(hero3)); // 出力: true
}
}
参照型でもデフォルトの equals() の挙動:
- Object クラスのデフォルトの equals() は「同じオブジェクトかどうか(参照の同一性)」を比べます
- クラスを作るときに content の比較をしたい場合は、equals() をオーバーライドします
例: 自分のクラス Point を作っても、特に何もしなければ equals() は同じオブジェクトかどうかを比べます。
配列の比較:
- 配列自体の equals() は参照の同一性を比べるだけです(オブジェクトの配列なら中身は比較しません)
- 中身まで比較したいときは Arrays.equals(arr1, arr2) を使います
数字の配列なら中身が同じかどうかを判定してくれます
比較演算子の使い分け:
- 値そのものを比べたいときは equals()(クラスに応じてオーバーライドされている前提)を使用します
- 同じオブジェクトを指しているかどうかだけ知りたいときは == を使用します
- 文字列やコレクションの中身を比較したい場合は、基本的には equals() を使い、配列は Arrays.equals() で比較します
- primitive 型(int, long, double など)は == がそのまま値の比較になります
6. 総合演習:アイテム管理システム
これまで学んだ内容を活用して、完全なアイテム管理システムを実装しましょう。
6.1 Itemクラス
/**
* アイテムクラス
*/
public class Item {
private String name;
private int value;
private String description;
/**
* コンストラクタ
* @param name アイテム名
* @param value 価値
* @param description 説明
*/
public Item(String name, int value, String description) {
this.name = (name != null) ? name : "不明なアイテム";
this.value = Math.max(value, 0);
this.description = (description != null) ? description : "";
}
/**
* ディープコピーを作成
* @return 新しいItemオブジェクト
*/
public Item deepCopy() {
return new Item(this.name, this.value, this.description);
}
/**
* オブジェクトの内容を比較
* @param obj 比較対象
* @return 内容が同じならtrue
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Item other = (Item) obj;
return this.value == other.value &&
this.name.equals(other.name) &&
this.description.equals(other.description);
}
/**
* ハッシュコードを生成
* @return ハッシュコード
*/
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + value;
result = 31 * result + description.hashCode();
return result;
}
/**
* 文字列表現を返す
* @return 文字列表現
*/
@Override
public String toString() {
return String.format("%s (価値: %d) - %s", name, value, description);
}
// getter
public String getName() { return name; }
public int getValue() { return value; }
public String getDescription() { return description; }
}
6.2 Inventoryクラス
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* インベントリ(所持品)クラス
*/
public class Inventory {
private List<Item> items;
private int maxSize;
/**
* コンストラクタ
* @param maxSize 最大所持数
*/
public Inventory(int maxSize) {
this.maxSize = Math.max(maxSize, 1);
this.items = new ArrayList<>();
}
/**
* アイテムを追加
* @param item 追加するアイテム
* @return 追加成功ならtrue
*/
public boolean addItem(Item item) {
if (item == null) {
System.out.println("nullのアイテムは追加できません");
return false;
}
if (items.size() >= maxSize) {
System.out.println("インベントリが満杯です");
return false;
}
items.add(item);
return true;
}
/**
* アイテムを削除
* @param item 削除するアイテム
* @return 削除成功ならtrue
*/
public boolean removeItem(Item item) {
return items.remove(item);
}
/**
* 名前でアイテムを検索
* @param name アイテム名
* @return 見つかったアイテム(Optional)
*/
public Optional<Item> findItemByName(String name) {
if (name == null) {
return Optional.empty();
}
return items.stream()
.filter(item -> item.getName().equals(name))
.findFirst();
}
/**
* インベントリのディープコピーを作成
* @return 新しいInventoryオブジェクト
*/
public Inventory deepCopy() {
Inventory copy = new Inventory(this.maxSize);
for (Item item : this.items) {
copy.addItem(item.deepCopy());
}
return copy;
}
/**
* 所持アイテム一覧を表示
*/
public void displayItems() {
System.out.println("=== 所持アイテム (" + items.size() + "/" + maxSize + ") ===");
if (items.isEmpty()) {
System.out.println("アイテムがありません");
} else {
for (int i = 0; i < items.size(); i++) {
System.out.printf("%d. %s%n", i + 1, items.get(i));
}
}
}
// getter
public List<Item> getItems() { return new ArrayList<>(items); }
public int getMaxSize() { return maxSize; }
public int getSize() { return items.size(); }
}
6.3 テストプログラム
/**
* アイテム管理システムのテスト
*/
public class ItemManagementTest {
public static void main(String[] args) {
System.out.println("=== アイテム管理システムのテスト ===\n");
// 1. アイテム作成
Item potion = new Item("回復ポーション", 50, "HPを50回復する");
Item sword = new Item("鉄の剣", 300, "攻撃力+15");
Item shield = new Item("木の盾", 200, "防御力+10");
// 2. インベントリ作成
Inventory inventory = new Inventory(5);
// 3. アイテムを追加
inventory.addItem(potion);
inventory.addItem(sword);
inventory.addItem(shield);
System.out.println("【初期状態】");
inventory.displayItems();
// 4. ディープコピーのテスト
System.out.println("\n【ディープコピーのテスト】");
Inventory copyInventory = inventory.deepCopy();
// オリジナルからアイテムを削除
inventory.removeItem(potion);
System.out.println("\nオリジナル:");
inventory.displayItems();
System.out.println("\nコピー(影響なし):");
copyInventory.displayItems();
// 5. nullチェックのテスト
System.out.println("\n【nullチェックのテスト】");
inventory.addItem(null); // エラーメッセージが表示される
// 6. アイテム検索のテスト
System.out.println("\n【アイテム検索のテスト】");
Optional<Item> foundItem = inventory.findItemByName("鉄の剣");
foundItem.ifPresentOrElse(
item -> System.out.println("見つかりました: " + item),
() -> System.out.println("見つかりませんでした")
);
// 7. equals()のテスト
System.out.println("\n【equals()のテスト】");
Item potion2 = new Item("回復ポーション", 50, "HPを50回復する");
System.out.println("potion == potion2: " + (potion == potion2));
System.out.println("potion.equals(potion2): " + potion.equals(potion2));
}
}
実行結果:
=== アイテム管理システムのテスト ===
【初期状態】
=== 所持アイテム (3/5) ===
1. 回復ポーション (価値: 50) - HPを50回復する
2. 鉄の剣 (価値: 300) - 攻撃力+15
3. 木の盾 (価値: 200) - 防御力+10
【ディープコピーのテスト】
オリジナル:
=== 所持アイテム (2/5) ===
1. 鉄の剣 (価値: 300) - 攻撃力+15
2. 木の盾 (価値: 200) - 防御力+10
コピー(影響なし):
=== 所持アイテム (3/5) ===
1. 回復ポーション (価値: 50) - HPを50回復する
2. 鉄の剣 (価値: 300) - 攻撃力+15
3. 木の盾 (価値: 200) - 防御力+10
【nullチェックのテスト】
nullのアイテムは追加できません
【アイテム検索のテスト】
見つかりました: 鉄の剣 (価値: 300) - 攻撃力+15
【equals()のテスト】
potion == potion2: false
potion.equals(potion2): true
基本問題
問題1: シャローコピーとディープコピーの違い
次のコードの出力を予測し、理由を説明してください。
class Box {
int value;
Box(int value) { this.value = value; }
}
class Container {
Box box;
Container(Box box) { this.box = box; }
Container shallowCopy() {
return new Container(this.box);
}
Container deepCopy() {
return new Container(new Box(this.box.value));
}
}
public class Question1 {
public static void main(String[] args) {
Container c1 = new Container(new Box(10));
Container c2 = c1.shallowCopy();
Container c3 = c1.deepCopy();
c1.box.value = 20;
System.out.println(c1.box.value);
System.out.println(c2.box.value);
System.out.println(c3.box.value);
}
}
解答を見る
出力:
20
20
10
理由:
-
c1.box.value = 20で、c1のboxの値を変更 -
c2はシャローコピーなので、c1と同じboxオブジェクトを共有 →20 -
c3はディープコピーなので、別のboxオブジェクトを持つ →10(変更なし)
問題2: nullチェックの実装
次のメソッドを完成させてください。
/**
* プレイヤーの総攻撃力を計算
* @param player プレイヤー(nullの可能性あり)
* @return 総攻撃力(プレイヤーまたは武器がnullなら0)
*/
public int calculateTotalAttack(Player player) {
// ここに実装
}
解答例を見る
public int calculateTotalAttack(Player player) {
if (player == null) {
return 0;
}
int baseAttack = player.getAttack();
if (player.getWeapon() == null) {
return baseAttack;
}
return baseAttack + player.getWeapon().getAttack();
}
// またはOptionalを使った方法
public int calculateTotalAttack(Player player) {
return Optional.ofNullable(player)
.map(p -> p.getAttack() +
Optional.ofNullable(p.getWeapon())
.map(Weapon::getAttack)
.orElse(0))
.orElse(0);
}
応用問題
問題3: ディープコピーの実装
次のクラスにディープコピーメソッドを実装してください。
class Armor {
private String name;
private int defense;
// コンストラクタ、getter/setterは省略
}
class Equipment {
private Weapon weapon;
private Armor armor;
// コンストラクタ、getter/setterは省略
/**
* ディープコピーを作成
* @return 新しいEquipmentオブジェクト
*/
public Equipment deepCopy() {
// ここに実装
}
}
解答例を見る
class Armor {
private String name;
private int defense;
public Armor(String name, int defense) {
this.name = name;
this.defense = defense;
}
public Armor deepCopy() {
return new Armor(this.name, this.defense);
}
// getter/setter
}
class Equipment {
private Weapon weapon;
private Armor armor;
public Equipment(Weapon weapon, Armor armor) {
this.weapon = weapon;
this.armor = armor;
}
public Equipment deepCopy() {
Weapon copiedWeapon = (this.weapon != null) ? this.weapon.deepCopy() : null;
Armor copiedArmor = (this.armor != null) ? this.armor.deepCopy() : null;
return new Equipment(copiedWeapon, copiedArmor);
}
// getter/setter
}
問題4: equals()とhashCode()の実装
次のWeaponクラスに、equals()とhashCode()を正しく実装してください。
class Weapon {
private String name;
private int attack;
private String type; // "剣", "斧", "槍"など
// コンストラクタ、getter/setterは省略
@Override
public boolean equals(Object obj) {
// ここに実装
}
@Override
public int hashCode() {
// ここに実装
}
}
解答例を見る
@Override
public boolean equals(Object obj) {
// 同じ参照ならtrue
if (this == obj) {
return true;
}
// nullまたは異なるクラスならfalse
if (obj == null || getClass() != obj.getClass()) {
return false;
}
// 各フィールドを比較
Weapon other = (Weapon) obj;
if (this.attack != other.attack) {
return false;
}
if (!this.name.equals(other.name)) {
return false;
}
return this.type.equals(other.type);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + attack;
result = 31 * result + type.hashCode();
return result;
}
問題5: 参照のトレース
次のコードの実行後、メモリの状態を図で表してください。
Player p1 = new Player("勇者", 100, new Weapon("剣", 30));
Player p2 = p1;
Player p3 = p1.deepCopy();
p2.setName("戦士");
p3.getWeapon().setAttack(50);
解答を見る
メモリの状態:
スタック領域:
-
p1→ Playerオブジェクト1のアドレス -
p2→ Playerオブジェクト1のアドレス(p1と同じ) -
p3→ Playerオブジェクト2のアドレス
ヒープ領域:
- Playerオブジェクト1: name="戦士", hp=100, weapon → Weaponオブジェクト1
- Playerオブジェクト2: name="勇者", hp=100, weapon → Weaponオブジェクト2
- Weaponオブジェクト1: name="剣", attack=30
- Weaponオブジェクト2: name="剣", attack=50
ポイント:
- p1とp2は同じオブジェクトを指す(シャローコピー)
- p3は独立したオブジェクト(ディープコピー)
- p3の武器の変更は、p1/p2に影響しない
本記事では、参照型の深い理解とコピーの違いについて解説しました。
重要ポイント
-
参照型とメモリ
- スタック領域:変数と参照を保持
- ヒープ領域:実際のオブジェクトを保持
- 参照型の代入は、アドレスのコピー
-
シャローコピー
- 参照だけがコピーされる
- オブジェクトは共有される
- 片方の変更が、もう片方に影響する
-
ディープコピー
- オブジェクト全体がコピーされる
- 完全に独立した2つのオブジェクト
- 片方の変更は、もう片方に影響しない
-
null対策
- 初期化時にnullを避ける
- 使用前に必ずチェック
- Optionalを活用
-
==とequals()
-
==: 参照(アドレス)の比較 -
equals(): オブジェクトの内容の比較 - 必要に応じてequals()をオーバーライド
-
次回は「第7回:継承の基礎」をお届けします!
- 継承とは何か
- スーパークラスとサブクラス
- オーバーライドの使い方
- ポリモーフィズムの基礎
お楽しみに!
- 第1回:オブジェクト指向って何?RPGで理解する超入門
- 第2回:オブジェクト指向って何?RPGで理解する超入門 (配列とループ)
- 第3回:オブジェクト指向って何?RPGで理解する超入門 (フィールドとメソッド)
- 第4回:オブジェクト指向って何?RPGで理解する超入門 (コンストラクタ)
- 第5回:オブジェクト指向って何?RPGで理解する超入門 (カプセル化)
- 第6回:オブジェクト指向って何?RPGで理解する超入門 (参照型を理解する)(本記事)
- 第7回:継承(次回)
- 第8回:ポリモーフィズム
- 第9回:抽象クラスとインターフェース
- 第10回:総合演習 - RPGバトルシステム構築
© 2024 Java Quest - オブジェクト指向って何?RPGで理解する超入門
All Rights Reserved.
本記事は教育目的で作成されています。RPGの例を通じてプログラミング概念を楽しく学習できることを目指しています。









