パパの勉強部屋

勉強の記録をつけています。Java、ネットワーク、Excel、etc.

スッキリわかるJava入門 第10章 カプセル化③

第10章 カプセル化

getter/setterの存在価値

// カプセル化を行う前
public class Hero {
	String name;
}

// カプセル化を行った後
public class Hero {
	private String name;

	public String getName(){
		return this.name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}
メリット1:Read Only、Write Onlyのフィールドを実現できる
  • setName()メソッドを削除すれば、外部から読めるが書き換えられない(Read Only)フィールドにできる。よく多用されるテクニック。
  • getName()メソッドを削除すれば、外部から自由に書き換えできるが、読めない(Write Only)フィールドにできる。あまり使われない。
メリット2:フィールドの名前など、クラスの内部設計を自由に変更できる
  • たとえば、何らかの理由でnameというフィールド名をnewNameに変更したい場合。
  • getter/setterを準備せず、他のクラスからnameフィールドを直接読み書きしていた場合、他のクラスすべての開発者にアクセスするフィールド名を変更してもらう必要がある。
  • nameフィールドを隠し、外部からはgetter/setter経由で読み書きさせるなら、フィールド名の変更は自由。getter/setterの中身でフィールド名を使っている箇所だけ修正すれば良い。
メリット3:フィールドへのアクセスを検査できる
  • クラス外部からnameフィールドの値を書き換えたい場合、setterを使う必要がある=setterを実行せずに、nameフィールドを書き換える事は不可能。
  • これを利用し、setterで「設定されようとしている値が妥当かを検査する」こともJavaプログラミングの定石。
  • 以下のsetName()メソッドは、nameフィールドへの値を変更しようとするたびに検査を行う。

public class Hero {
	private String name;

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		// 代入前に、代入しようとしている値が妥当かを検査する
		if (name == null) { // nullが設定されそうになった!
			throw new IllegalArgumentException("名前にnullが設定されそうになりました。処理を中断します。");
		}
		if (name.length() <= 1) { // 文字列長が異常(短すぎる)
			throw new IllegalArgumentException(
					"1文字以下の名前が設定されそうになりました。処理を中断します。");
		}
		if (name.length() >= 8) { // 文字列長が異常(長すぎる)
			throw new IllegalArgumentException(
					"8文字以上の名前が設定されそうになりました。処理を中断します。");
		}
		this.name = name; // 検査完了!代入しても大丈夫。
	}
}

クラスに対するアクセス制御

2つのアクセス制御レベル
名前 Javaでの記述 許可する範囲 制限
package private (何も書かない) 自分と同じパッケージに属するクラス 厳しい
public public すべてのクラス 緩い
  • クラス宣言の前にはpublicをつけると丸暗記していたが、何も書かない場合、そのクラスは同一パッケージに属するクラスからのアクセスのみ許可される。
  • 他のパッケージに属するクラスからのアクセスが禁止される。イメージとしては「他のパッケージに属するクラスから、そのクラスの存在自体が見えなくなる」と捉えた方がよい。

非publicクラスとソースファイル

  • 別パッケージのクラスから見えない代わりに、publicクラスでは許可されない次の2つが許可されている。
  • ①クラスの名前はソースファイル名と異なってもよい。
  • ②1つのソースファイルに複数宣言してもよい。

カプセル化を支えている考え方

メソッドでフィールドを保護する
  • 「外部から直接触られないよう、メソッドという殻(カプセル)によってフィールドが保護されている」ように見える事から、カプセル化という名前が付いている。
  • なぜカプセル化ではメソッドではなくフィールドを保護しようとするのか?それは、メソッドよりフィールドの方が異常な状態(不具合)になりやすいから。
  • メソッドの処理内容は、プログラミング段階で決定し、一度コンパイルされればプログラム実行中に変化することはない。
  • フィールドの中身は、プログラムが動作する間に逐次、変化していく。動作中に異常な値になる危険性もある。結果的に不具合の多くは「フィールドに予期しない値が入る」という形で発現する。
  • 「プログラムの不具合を減らすためには、メソッドよりもフィールドを保護する事が重要」。
カプセル化の本質
  • 不具合とは:そもそもバグとは、現実世界と仮想世界が食い違ってしまうこと。
  • カプセル化を使えば、どのように利用されてもフィールドに不正な値が入ってしまうことがない「現実の登場人物と矛盾することがないクラス」を作ることができる。
  • 「現実の登場人物と矛盾することがないクラス」を集めてプログラムを作れば、「現実世界と矛盾することがないプログラム」になるという考えがカプセル化の本当の狙い。



スッキリわかるJava入門 第10章 カプセル化②

第10章 カプセル化

publicやpackage privateを利用する

  • 勇者は戦うのが仕事なので、いろいろなクラスからattack()メソッドが呼び出される可能性がある。
  • どのようなクラスからでも呼び出せるようにpublic指定を付けておく。
  • sleep()メソッドにはpublicを付けない。この場合package privateを指定したと見なされ、同じパッケージに属するクラスからの呼び出しのみ可能となる。

public class Hero {
	// 省略
	
	void sleep() {		
		// 省略
	}

	public void attack(Matango m){
		// 省略
	}

メンバに関するアクセス修飾の定石

  • フィールドはすべてprivate
  • メソッドはすべてpublic
  • クラスはすべてpublic(メンバではないが、クラスの定石)
  • 基本はこのパターンに沿ってアクセス修飾を行い、その後クラス内部だけで利用するメソッドのみをprivateに指定し直すような「微調整」をすればよい。

getterとsetter メソッドを経由したフィールド操作

  • 基本的にフィールドは、メソッド経由でアクセスする
  • 他のクラスの開発者がバグを含んだコードを書いても、勇者のHPをマイナス100にする事は不可能。
  • HPに異常な値が設定される不具合に直面しても、そのときはメソッド(attack(),sleep())どちらかのバグという事が想定できる。

単純にフィールド値を取り出すだけのメソッド

  • 王様クラスが勇者クラスの名前を知るには?
  • HeroクラスにgetNameメソッドを追加して、王様が勇者の名前を知ることができるようにする。

public class Hero {
	private String name;

	public String getName(){
		return this.name;
	}
}
  • Kingクラスでは、nameフィールドにアクセスしている部分を、getName()を呼び出すように修正する。

public class King {
	void talk(Hero h) {
		System.out.println("王様:ようこそ我が国へ、勇者" + h.getName() + "よ。");	
	}
}

getterメソッドの書き方

getterメソッドの書き方の定石
// 定石
public 値を取り出すフィールドの型 getフィールド名(){
	return this.フィールド名;
}

// サンプル
public String getName(){
	return this.name;
}
}
メソッド名の慣習的なルール
  • メソッド名の最初の3文字を「get」にし、それに続けて「フィールド名の先頭を大文字にしたもの」にする。
  • フィールド名がnameならgetName()となる。
  • 例外として戻り値がboolean型の場合のみisXxxx()というメソッド名にすることがある。
  • このメソッド名の付け方はJava開発者の間で常識になっている風習みたいなもの。

単純にフィールドに値を代入するだけのメソッド

  • getメソッドとは逆で、フィールドに指定された値を単に代入するだけのメソッドは、setter(セッター)メソッドと言う。
setterメソッドの書き方の定石
// 定石
public void setフィールド名(フィールド名の型 任意の変数名){
	this.フィールド名 = 任意の変数名;
}

// サンプル
public void setName(String name) {
	this.name = name;
}
}



スッキリわかるJava入門 第10章 カプセル化①

第10章 カプセル化

カプセル化とは?

  • フィールドへの読み書きや、メソッドの呼び出しを制限する機能。
  • 情報へのアクセスや動作の実施について「誰に何を許すか」を定めて制限することを、アクセス制御(access control)と言う。
  • Javaにおけるカプセル化とは、大切な情報(フィールド)や操作(メソッド)についてアクセス制御をかけることにより、悪意や間違いによるメンバの利用を予防し、想定しない利用が発生したならば、その原因箇所を特定しやすくするための仕組み。

アクセス制御されない怖さ

不具合
  • Heroクラスを使ったゲームのテスト中の不具合
  • ①一度もモンスターと戦っていないのに勇者のHPがマイナス100になっている。
  • 別開発者の「宿屋クラス」の不具合で「h.hp = -100;」とコーディングされていた。(コンパイルエラーにならない)
  • ②冒険中にお城で会話をすると、なぜか勇者が理由もなく急死してゲームオーバーになる。
  • 別開発者の「王様クラス」の不具合で「bye()」ではなく「die()」メソッドが呼ばれていた。
必要なアクセス制御
  • ①Heroクラス以外からはhpフィールドに値を設定できない
  • ②die()メソッドを呼べるのはHeroクラスだけ

4つのアクセス制御レベル

  • Javaでは、それぞれのメンバ(フィールド及びメソッド)に対してアクセス制御の設定を行うことができる。
  • ですが、それぞれのメンバに「Aクラスからの利用は許す」のように細かく指定すると、とても手間がかかってしまう。
  • ざっくりと4段階からアクセス制御の方法を選ぶようになっている。
Javaにおけるアクセス制御の範囲と指定方法(メンバ編)
名前 プログラム中の指定方法 アクセスを許可する範囲
private private 自分自身のクラスのみ
package private (何も書かない) 自分と同じパッケージに属するクラス
protected protected 自分と同じパッケージに属するか、自分を継承した子クラス
public public すべてのクラス
  • privateやpublicなどはアクセス修飾子(access modifier)と呼ばれ、フィールドやメソッドを宣言する際、先頭に記述する事でアクセス制御が可能となる。

privateを利用する

必要なアクセス制御①の対応として、HPはprivateにする。

public class Hero {
	private int hp;	
	String name;
	Sword sword;
	static int money;	
	
	Hero(){			
		this("ダミー");	
	}

	Hero(String name){		
		this.hp = 100;	
		this.name = name;	
	}

	void sleep() {		
		this.hp = 100;
		System.out.println(this.name + "は、眠って回復した!");
	}
}
  • hpフィールドにprivateを指定したため、宿屋クラスのcheckIn()メソッドでは「hpフィールドにアクセスできない」コンパイルエラーが発生する。
  • 勇者のhpフィールドが、いっさい変更できなくなる訳ではない。
  • atack()やsleep()のように、同じheroクラスのメソッドからはhpフィールドにアクセスできる。
  • 宿屋クラスのcheckIn()メソッドからは、勇者のsleep()メソッドを呼ぶように修正すればよい。
  • privateであっても、this.~での読み書きは可能。
必要なアクセス制御②の対応として、die()はprivateにする。
  • die()メソッドも、王様などほかのクラスからみだりに呼び出される事がないようにprivateにする。
  • 外部のクラスからは呼び出せなくなるが、同じクラス内にあるattack()メソッドからの呼び出しは問題ない。

public class Hero {
	private int hp;	
	String name;
	Sword sword;
	static int money;	
	
	Hero(){			
		this("ダミー");	
	}

	Hero(String name){		
		this.hp = 100;	
		this.name = name;	
	}

	void bye(){
		System.out.println("勇者は別れを告げた");
	}

	private void die() {
		System.out.println(this.name + "は死んでしまった!");
		System.out.println("GAME OVERです。");
	}
}



スッキリわかるJava入門 第9章 さまざまなクラス機構(練習問題)

第9章 さまざまなクラス機構(練習問題)

練習9

public class Cleric {
	
	// コンストラクタ
	public Cleric(String name, int hp, int mp) {
		this.name = name;
		this.hp = hp;
		this.mp = mp;
	}
	public Cleric(String name, int hp) {
		this(name, hp, MAX_MP);
	}
	public Cleric(String name) {
		this(name, MAX_HP, MAX_MP);
	}
	
	// 名前
	String name;
	// HP
	int hp = 50;
	// 最大HP
	static final int MAX_HP = 50; // staticを追加
	// MP
	int mp = 10;
	// 最大MP
	static final int MAX_MP = 10; // staticを追加
	
	// 魔法_セルフエイド
	public void selfAid() {
		System.out.println(name + "は魔法「セルフエイド」を唱えた!");
		
		// MPを5消費
		this.mp -= 5;
		
		// HPを最大HPまで回復
		this.hp = this.MAX_HP;
		
		System.out.println("最大HPまで回復した.");
	}
	
	// 祈る
	public int pray(int sec) {
		System.out.println(name + "は、" + sec + "秒祈った!");
		
		// ランダムで0~2ポイント補正
		int correction = new java.util.Random().nextInt(3);
		System.out.println("**  補正ポイント:" + correction);
		
		// MP回復量(仮)
		int tempAidPoint = sec + correction;
		
		// 最大MPより回復しないよう制御
		//(MP回復量(仮)またはMP減少値の最小値を採用)
		int aidPoint = Math.min(tempAidPoint, this.MAX_MP - this.mp);
		
		System.out.println("**  回復前のMP:" + this.mp);
		
		// MP回復
		this.mp += aidPoint;
		System.out.println("MPが" + aidPoint + "ポイント回復した.");
		System.out.println("**  回復後のMP:" + this.mp);
		
		return aidPoint;
	}
}

public class Main {
	public static void main (String[] args) {
		
		// コンストラクタ1
		Cleric clericShuto = new Cleric("シュウトウ");
		System.out.println("名前:" + clericShuto.name);
		System.out.println("HP:" + clericShuto.hp);
		System.out.println("MP:" + clericShuto.mp);
		
		// コンストラクタ2
		Cleric clericKondoh = new Cleric("コンドウ", 30);
		System.out.println("名前:" + clericKondoh.name);
		System.out.println("HP:" + clericKondoh.hp);
		System.out.println("MP:" + clericKondoh.mp);
		
		// コンストラクタ3
		Cleric clericGita = new Cleric("ギータ", 40, 9);
		System.out.println("名前:" + clericGita.name);
		System.out.println("HP:" + clericGita.hp);
		System.out.println("MP:" + clericGita.mp);
		
		// コンストラクタ3
		Cleric clericKuri = new Cleric("クリハラ", 100, 100);
		System.out.println("名前:" + clericKuri.name);
		System.out.println("HP:" + clericKuri.hp);
		System.out.println("MP:" + clericKuri.mp);
	}
}

実行結果
C:\work\ex91>java Main
名前:シュウトウ
HP:50
MP:10
名前:コンドウ
HP:30
MP:10
名前:ギータ
HP:40
MP:9
名前:クリハラ
HP:100
MP:100

改善①:コンストラクタの重複コードが最小限となるように修正

修正前

	// コンストラクタ
	public Cleric(String name) {
		this.name = name;
	}
	public Cleric(String name, int hp) {
		this(name);		// コンストラクタ1を呼出し
		this.hp = hp;
	}
	public Cleric(String name, int hp, int mp) {
		this(name, hp);	// コンストラクタ2を呼出し
		this.mp = mp;
	}

修正後

	// コンストラクタ
	public Cleric(String name, int hp, int mp) {
		this.name = name;
		this.hp = hp;
		this.mp = mp;
	}
	public Cleric(String name, int hp) {
		this(name, hp, this.MAX_MP);
	}
	public Cleric(String name) {
		this(name, this.MAX_HP, this.MAX_MP);
	}

改善②:コンパイルエラーを修正(修正後の「 this.MAX_HP」「this.MAX_MP」)

  • コンストラクタはインスタンス生成前なので、thisを用いて「自分自身のインスタンスのメンバ」は利用できない。thisは「自分自身のインスタンス」を意味している。
  • 静的フィールドへのアクセスに変更すればOK(this.→Cleric.)。静的フィールドであれば、インスタンスを1つも生み出さなくても利用可能になる。(金型の上に作られる箱)
コンパイルエラー
C:\work\ex91>javac Main.java Cleric.java
Cleric.java:10: エラー: スーパータイプのコンストラクタの呼出し前はthisを参照できません
                this(name, hp, this.MAX_MP);
                               ^
Cleric.java:13: エラー: スーパータイプのコンストラクタの呼出し前はthisを参照できません
                this(name, this.MAX_HP, this.MAX_MP);
                           ^
Cleric.java:13: エラー: スーパータイプのコンストラクタの呼出し前はthisを参照できません
                this(name, this.MAX_HP, this.MAX_MP);
                                        ^
エラー3
修正後

	// コンストラクタ
	public Cleric(String name, int hp, int mp) {
		this.name = name;
		this.hp = hp;
		this.mp = mp;
	}
	public Cleric(String name, int hp) {
		this(name, hp, Cleric.MAX_MP);
	}
	public Cleric(String name) {
		this(name, Cleric.MAX_HP, Cleric.MAX_MP);
	}

改善③:引数で指定したHP/MPが最大値を超えないように制御

修正後

	// コンストラクタ
	public Cleric(String name, int hp, int mp) {
		this.name = name;
		this.hp = Math.min(hp, Cleric.MAX_HP);
		this.mp = Math.min(mp, Cleric.MAX_MP);
	}
	public Cleric(String name, int hp) {
		this(name, hp, Cleric.MAX_MP);
	}
	public Cleric(String name) {
		this(name, Cleric.MAX_HP, Cleric.MAX_MP);
	}

スッキリわかるJava入門 第9章 さまざまなクラス機構③

第9章 さまざまなクラス機構③

静的メンバ クラス上に準備されるフィールド

  • インスタンスの独立性…newによって生成される個々のインスタンスは基本的に独立した存在。
  • たとえば勇者3名がパーティを組んで冒険するRPGを作る場合、次のようなクラスを書くと、各フィールド(name,hp,money)を各インスタンスが別々に持つことになる。

public class Hero {
  String name;
  int hp;
  int money;
}

  • しかし、プログラムを開発していると「各インスタンスで共有したい情報」が出てくることがある。
  • たとえばRPGはパーティで行動しているので「パーティで1つの財布」を設定したいなど。
  • 同じクラスから生成されたインスタンスでフィールドを共有したい場合、フィールド宣言の先頭にstaticキーワードを追加する。

public class Hero {
  String name;
  int hp;
  static int money;
}
  • staticキーワードが指定してあるフィールドは特に静的フィールド(static field)といわれ、下記の3つの特殊な効果をもたらす。
①フィールド変数の実体がクラスに準備される
  • 通常、フィールドが格納される箱(領域)は個々のインスタンスごとに用意されるが、静的フィールドの箱はインスタンスではなく、クラスに対して1つだけ用意される。
  • イメージは、「勇者の金型」の上にmoney箱が準備されるイメージ。
  • Heroクラスに準備された箱(静的フィールド「money」)を読み書きするには、「Hero.money」という表記を使う。
静的フィールドへのアクセス方法
  • クラス名.フィールド名

public class Main {
  public static void main(String[] args) {
    Hero h1 = new Hero();
    Hero h2 = new Hero();
    System.out.println(h1.hp);
    System.out.println(Hero.money);
  }
}
②全インスタンスに、箱の分身が準備される
  • 共通財産である金額が格納される変数(Hero.money)は、あくまで金型に作られる。
  • しかし、同時にh1やh2といった各インスタンスにもmoneyという名前で「箱の分身」が準備され、金型の箱の別名として機能するようになる。
  • つまり、「h1.money」や「h2.money」という分身の箱に値を代入すれば、本物の箱Hero.moneyにその値が代入される。(インスタンス(h1,h2)経由で、静的フィールド(Hero.money)へのアクセスが可能となる)
  • 実質的に「h1.money」「h2.money」「Hero.money」はどれも同一の箱を指すことになる。
  • このようすは「h1とh2がmoneyフィールドを共有している」と考えることもできるので「静的フィールドを用いれば、インスタンス間でフィールドを共有できる」と解説されることもある。
静的フィールドへの別名によるアクセス
  • インスタンスが含まれる変数名.静的フィールド名」と書いても良いが、「クラス名.静的フィールド名」と同じ意味。

public class Main {
  public static void main(String[] args) {
    Hero h1 = new Hero();
    Hero h2 = new Hero();
    Hero.money = 100;
    System.out.println(Hero.money); -- 100と表示
    System.out.println(h1.money);   -- 100と表示
    h1.money = 300;
    System.out.println(h2.money);   -- 300と表示
  }
}
インスタンスを1つも生み出さなくても箱が利用可能になる
  • 「Hero.money」は金型の上に作られる箱。よって、1つの実態もまだ生み出されていない状況にあっても利用することができる。

public class Main {
  public static void main(String[] args) {
    Hero.money = 100;
    System.out.println(Hero.money); -- 100と表示
  }
}
  • 静的フィールドはクラス(金型)にフィールド(箱)が所属するという特徴からクラス変数と言われることもある。
public static final コンビネーション
  • 多くの場合、staticはfinalやpublicと一緒に利用され、「変化しない定数を各インスタンスで共有するため」に利用される。

静的メソッド

  • Heroクラスに「勇者たちの所持金をランダムに設定する」setRandomMoney()メソッドを追加する場合。

public class Hero {
  String name;
  int hp;
  static int money;
  
  static void setRandomMoney() {
    Hero.money = (int)(Math.random() * 1000);
  }
}
  • staticキーワードがついているメソッドは、静的メソッド(static method)またはクラスメソッド(class method)と言う。
  • 静的フィールドとあわせて静的メンバ(static member)と総称される。
  • 静的メソッドを定義すると、静的フィールドと同様に以下の3つの効果が現れる。
①メソッド自体がクラスに属するようになる
  • 静的メソッドは、その実体が各インスタンスではなくクラスに属し、「クラス名.メソッド名();」で呼び出せる。
インスタンスにメソッドの分身が準備される
  • 静的メソッドは「インスタンス変数名.メソッド名();」でも呼び出せる。
インスタンスを1つも生み出さなくても箱が利用可能になる
  • 静的メソッドは、1つもインスタンスを生み出していない状況でも呼び出せる。

public class Main {
  public static void main(String[] args) {
    Hero.setRandomMoney();
    System.out.println(Hero.money); -- ランダムな金額が表示
    Hero h1 = new Hero();
    System.out.println(h1.money);   -- 同じ額を表示
  }
}
mainメソッドがなぜstaticでなければならないか?
  • mainメソッドが呼び出されるとき、仮想世界にはまだ1つもインスタンスが存在していない。
  • mainメソッドが属するクラス(Mainクラスなど)さえも、まだインスタンス化されていない状況で、mainは呼び出される必要がある。

静的メソッドの制約

  • 静的メソッドの中に記述するコードでは、staticが付いていないフィールドやメソッドは利用できない。

public class Hero {
  String name;
  int hp;
  static int money;
  
  static void setRandomMoney() {
    Hero.money = (int)(Math.random() * 1000);
    System.out.println(this.name + "たちの所持金を初期化しました"); -- this.nameでコンパイルエラー
  }
}
  • 静的メソッドsetRandomMoney()は、まだ1つも勇者インスタンスが存在しない状況でも呼び出されることがあるメソッド。
  • しかし「this.name」は、thisを用いて「自分自身のインスタンスのメンバ」を利用しようとしている。
  • もし仮想世界に1つも勇者インスタンスがない状況でsetRandomMoney()が動いたら、「this.name」は処理できない。



スッキリわかるJava入門 第9章 さまざまなクラス機構②

第9章 さまざまなクラス機構②

コンストラクタ 生まれたてのインスタンスの状態

  • クラスを用いたプログラミングを始めると、ある「めんどうくささ」を感じる。

public class Main {
  public static void main(String[] args) {
    Hero h1 = new Hero();
    h1.name = "ミナト";
    h1.hp = 100;
    Hero h2 = new Hero();
    h2.name = "アサカ";
    h2.hp = 100;
    Wizard w = new Wizard();
    w.name = "スガワラ";
    w.hp = 50;
    w.heal(h1);    // ミナトを回復させる(HP: 100 -> 110)
    w.heal(h2);    // アサカを回復させる(HP: 100 -> 110)
    w.heal(h2);    // アサカを回復させる(HP: 110 -> 120)
  }
}

  • newでインスタンスを生成した直後に、必ずフィールドの初期代入をしている。
  • newで生み出されたばかりのインスタンスのフィールド(nameやhp)には、まだ何も入っていないから。
フィールドの初期値
int型,short型,long型 等の数値の型 0
char型(文字) \u0000
boolean型 false
int[]などの配列型 null
String型などのクラス型 null

フィールド初期値を自動設定する

  • 実際の開発現場において、大規模な開発を行う場合、1人ですべてを開発することは、まずあり得ない。
  • クラスを作るにあたっては、自分以外の開発者がHeroクラスを利用することも考えておかなければならない。
  • その開発者が正しくHPに100を代入してくれるとは限らない。
  • 「勇者のHPが100であること」は、勇者自身に関する事で、Heroクラスの開発者が一番よく知っていること。(Heroクラス側で責任を持つこと)
  • javaでは「インスタンスが生まれた直後に自動実行される処理」をプログラミングできるようになっている。

public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
  public Hero() {
    this.hp = 100;    // hpフィールドを100で初期化
  }
}

  • 9行目のHero()メソッドは、このクラスがnewされると、newと同時にメソッド内部で定義しておいた処理が自動的に行われる。
  • このようなメソッドをコンストラクタ(constructor)と呼ぶ。
  • newされると自動的に実行されてHPに100が代入される。そのためmainメソッド側でHPに初期値を代入する必要はない。

コンストラクタの定義条件

  • 「自動実行されるメソッドの条件」を満たすように定義する。
コンストラクタとみなされる条件
  • ①メソッド名がクラス名と完全に等しい
  • ②メソッド宣言に戻り値が記述されていない(voidもダメ)

コンストラクタに情報を渡す

  • HPフィールドは「100」という固定の値で初期化すればよいため、単純なコンストラクタで済んだ。
  • 勇者の名前は生み出すインスタンスによって異なる。
  • コンストラクタが「毎回異なる追加情報」を引数で受け取れるように宣言する。

public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
  public Hero(String name) {
    this.hp = 100;
    this.name = name;    // 引数の値でnameフィールドを初期化
  }
}

  • このようなHeroクラスを利用する場合は、コンストラクタが実行される際に渡して欲しい引数をnewする際に指定する。

public class Main {
  public static void main(String[] args) {
    Hero h = new Hero("ミナト");
    
    System.out.println(h.hp);
    System.out.println(h.name);
  }
}

2つ以上の同名コンストラクタを定義する

  • 現在のHeroクラスには「文字列引数を1つ受け取るコンストラクタ」が定義されている。
  • このコンストラクタを作ったことによって、インスタンスを生成するときには、名前を指定する必要が生じた。(new Hero();を実行するとエラーとなる)
  • この問題は「引数を受け取らないコンストラクタ」も同時に定義することで解決できる。

public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
  public Hero(String name) {
    this.hp = 100;
    this.name = name;    // 引数の値でnameフィールドを初期化
  }
  public Hero() {
    this.hp = 100;
    this.name = "ダミー";
  }
}

  • 「同じ名前だが引数が異なるメソッドを複数定義」するオーバーロードは、コンストラクタでも可能。

public class Main {
  public static void main(String[] args) {
    Hero h1 = new Hero("ミナト");
    System.out.println(h1.name);
    Hero h2 = new Hero();
    System.out.println(h2.name);
  }
}

複数のコンストラクタが定義されていた場合
  • newするときに渡した引数の型・数・順番に対応するコンストラクタが動作する。(複数のコンストラクタが定義されていても、1つだけしか動作しない)

暗黙のコンストラク

  • コンストラクタが定義されていなかった頃のHeroクラスは「new Hero();」だけで生成できたのに、引数ありコンストラクタを定義しただけで同様のことが不可能になった。
  • Javaでは、すべてのクラスはインスタンス化に際して必ず何らかのコンストラクタを実行することになっている。
  • 本来すべてのクラスは「引数を何もとらず、何も処理をしないコンストラクタ」でよいので、最低でも1つ以上のコンストラクタ定義を持っていなければならない。
  • わざわざダミーでコンストラクタを定義するのはめんどうなので、Javaでは以下の特例を設けている。
コンストラクタの特例
  • クラスに1つもコンストラクタが定義されていない場合に限って、「引数なし、処理内容なし」のコンストラクタ(デフォルトコンストラクタ)の定義がコンパイル時に自動的に追加される。

他のコンストラクタを呼び出す

  • 2つ以上のコンストラクタを定義していると、そのうち「重複する処理」が気になる。
  • そこで思いつくのが、コンストラクタ②の中で、コンストラクタ①を呼び出す方法。
  • Javaではコンストラクタを直接呼び出すことができないため、以下のコードはコンパイルエラーとなる。

public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
  public Hero(String name) {    // コンストラクタ①
    this.hp = 100;
    this.name = name;
  }
  public Hero() {    // コンストラクタ②
    this.Hero("ダミー");
  }
}

別コンストラクタの呼び出しに関するルール
  • 「コンストラクタの先頭で、別のコンストラクタを専用の文法を用いて呼び出す場合に限って」特別に許される。
  • 「this.クラス名(引数);」と記述する事はできない。
  • その代わりに「this(引数);」と記述する。

public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
  public Hero(String name) {    // コンストラクタ①
    this.hp = 100;
    this.name = name;
  }
  public Hero() {    // コンストラクタ②
    this("ダミー");
  }
}



スッキリわかるJava入門 第9章 さまざまなクラス機構①

第9章 さまざまなクラス機構①

仮想世界の真の姿

  • ここまで「Java仮想世界」と表現してきたものは、実際には「コンピュータのメモリ領域」。
  • この領域はJavaのプログラムを実行する際にJVMが大量にメモリ領域を準備(通常は数十MB~数GB)するもので、ヒープ(heap)と言う。
  • newを用いてインスタンスを生み出すたびにヒープの一部の領域(通常は数十~数百バイト)が確保され、インスタンスの情報を格納するために利用される。そのため多くの属性を持った大きなクラスをインスタンス化すると多くのヒープ領域を消費する。
  • インスタンスとは「ヒープの中に確保されたメモリ領域」。★

クラス型変数とその中身

public class Main{
  public static void main(String[] args) {
    Hero h;
    h = new Hero();
    h.hp = 100;
  }
}

Step1:Hero型変数の確保
  • 最初に動くのは3行目「Hero h;」。JVMは「Hero型の変数h」をメモリ内に準備する。
  • JVMは広いヒープ領域の中から現在利用していないメモリ領域を探し出して、自動的に確保してくれる。
  • 仮に1928番地が確保されたとする。Hero型の箱が準備されただけ。
Step2:インスタンスの生成
  • 4行目「h = new Hero();」は代入文。代入の場合は左辺より先に右辺が評価される。
  • 「new Hero()」が実行されると、JVMはヒープ領域から必要な量のメモリを確保する。
  • 仮に3299番地から24バイト分(3299~3945番地)が確保されたとする。
Step3:参照の代入
  • 右辺の実行が終了した後、4行目は「h = 右辺の実行結果」という状態になる。
  • 右辺の実行結果とは、newを実行することによって生成されたインスタンスのために確保されたメモリの先頭番地。
  • 変数hには3299という数字が代入される。変数hに入っているアドレス情報を参照と言う。(配列型と同じ)
  • クラス型と配列型は総称して「参照型」と呼ばれる。
Step4:フィールドへの値の代入
  • 5行目の「h.hp = 100」では変数hに格納されている勇者のHPを100に設定する。
  • JVMの解釈は以下の通り。
  • ①変数hの中身を調べる。→「3299番地を参照せよ」
  • ②メモリ内の3299番地にあるインスタンスのメモリ領域にアクセスし、その中のhpフィールド部分を100に書き換える。
  • このような①→②のJVMの動作を「参照の解決」「アドレス解決」と言う。

同一インスタンスを指す変数

public class Main {
  public static void main(String[] args) {
    Hero h1;
    h1 = new Hero();
    h1.hp = 100;
    Hero h2;
    h2 = h1;
    h2.hp = 200;
    System.out.println(h1.hp);
  }
}

  • ポイントは7行目の「h2 = h1」。ここでコピーされているのは「勇者インスタンスそのもの」ではなく、「3299」などの番地情報。
  • h1とh2はどちらも「まったく同じ1人の勇者インスタンス」を指している。
  • h1.hp、h2.hpどちらのフィールドに代入しても、結局は同じ勇者インスタンスのHPに代入することになる。

クラス型をフィールドに用いる

// まず、Swordクラスを定義しておく
public class Sword {
  String name;
  int damage;
}

// 次にHeroクラスを定義する
public class Hero {
  String name;
  int hp;
  Sword sword;
  public void attack() {
    System.out.println(this.name + "は攻撃した!");
    System.out.println("敵に5ポイントのダメージをあたえた!");
  }
}

  • Heroクラスの5行目「Sword sword」。このようにフィールドにクラス型を使っても問題ない。
  • あるクラスが別のクラスをフィールドとして利用している関係を「has-aの関係」と言う。
  • Hero has-a Sword(勇者は剣を持っている)

public class Main {
  public static void main(String[] args) {
    Sword s = new Sword();
    s.name = "炎の剣";
    s.damage = 10;
    Hero h = new Hero();
    h.name = "ミナト";
    h.hp = 100;
    h.sword = s;
    System.out.println("現在の武器は" + h.sword.name);
  }
}

  • 3行目「Sword s = new Sword()」でswordフィールドに生成済みの剣インスタンスの番地を、9行目「h.sword = s」で代入。
  • 10行目の「h.sword.name」は、勇者「の」剣「の」名前を出力する。

クラス型をメソッド引数や戻り値に用いる

  • クラス型はフィールドの型として用いることができるだけではなく、メソッドの引数や戻り値の型として利用することもできる。

String型の真実

  • String型は、int型やdouble型の仲間ではなく、Hero型と同じ「クラス型」。
  • String型を、int型と同じように扱える理由は、Java言語が作られた時に次のような特別の配慮がなされたため。
java.lang パッケージに宣言してある:
  • java.lang パッケージに所属するクラスを利用する際は、特例としてimport文を記述する必要がない。
  • 本来「java.lang.String s;」と宣言する必要があるところを、単に「String s;」と書けば済む。
二重引用符で文字列を囲めばインスタンスを生成・利用できる:
  • 通常、インスタンスを生成するにはnew演算子を利用する必要がある。
  • 文字列はプログラムの中で多用されるため、その都度newを書いていてはソースコードがnewだらけになる。
  • そこで「二重引用符で文字列を囲めば、その文字列情報を持ったStringインスタンスを利用できる」という特例が設けられた。