第1章:クラスの基本とインスタンス化
第1章:クラスの基本とインスタンス化
1. はじめに
プログラムを書く目的は、コンピュータを動かすことだけではありません。プログラムは人間が読むものでもあり、他人や未来の自分が理解しやすいことが重要です。
したがって、コードは「動く手順書」であると同時に「思考の設計図」である必要があります。
この授業では、コードの可読性(readability)を重視します。単に動けばよいコードではなく、**「何をしているのか」「なぜそう書かれているのか」**を重要視します。
コーディングで大切な3つの原則
- 変数やメソッドの名前に意味を込めること(命名は意図を伝える最初の道具です)
- 「なぜそう書いたのか」をコメントで残すこと(事実の説明ではなく設計意図を補うコメント)
- 状態の変化を明示すること(読み手が「今どの値がどこにあるか」を追いやすくする)
これらを徹底することで、コードは「ただ動くだけの指示」から「設計思想を伝える表現」へと進化します。
実務においては、コードは一度書いて終わりではなく、修正・拡張・レビュー・再利用といった過程を経て維持・進化していく。
そのためには「動く」こと以上に、「何をしているか」「なぜそう書かれているのか」が読み取れること、すなわち**「可読性(readability)」が不可欠となります。**
この可読性を軸にコードを書くための思想を体系的に示した書籍が、『The Art of Readable Code』である。本講義では、そのエッセンスを各回の文法・構造の中に織り交ぜながら学んでいきます。
プログラムの変数は単なる記号ではなく、処理の意図を表現するための言語的なツールである。したがって「その値が何を意味しているか」をコード上で説明できるような名前を与えるべきであると言える。
このような命名の工夫は、リーダブルコード(読みやすいコード)を書くための最初の一歩となります。
本章で学ぶ内容
本日のテーマは クラスの基本とインスタンス化 です。特に、次の二点に焦点を当てます。
- 値渡し(プリミティブ型の引数)がどのように働くのか。
- フィールド(特にstaticを用いたクラス変数)がどのように状態を保持するのか。
この二つを比較することで、「状態はどこに存在するのか?」という設計上の問いを立てることができます。これは後に学ぶ「インスタンス化」への重要なステップとなります。
2. 値渡しの仕組み(Smp01_1)
最初に、値渡しの仕組みを学びます。Javaでは、プリミティブ型(intやdoubleなど)をメソッドに渡すとき、変数そのものではなく「値のコピー」が渡されます。これを理解することは、プログラムの挙動を正しく説明できるようになる第一歩です。
public class Smp01_1 {
public static void add(int num){
num += 10;
System.out.println("メソッド内のnum: " + num);
}
// mainメソッドは後ほど示します
}
ここで登場した num
は「仮引数(formal parameter)」と呼ばれます。呼び出し側から渡す「実引数(actual parameter)」の値がコピーされ、この仮引数に入ります。重要なのは、呼び出し側の変数そのものを参照しているのではないという点です。
呼び出し側のコード
public static void main(String[] args){
int num = 10;
System.out.println("呼び出し前のnum: " + num);
add(num);
System.out.println("呼び出し後のnum: " + num);
}
これを実行すると、次のような出力になります。
値渡しの実行モデル
- mainメソッドでnumに10を代入します。
- add(num)が呼び出されるとき、値10がコピーされて仮引数numに渡されます。
- メソッド内のnumはコピー先なので、+= 10により20になります。
- しかし、その変更はコピーに対して行われたため、呼び出し元のnumには影響を与えません。
一見すると、メソッドの中でnumに10を足しているのに、呼び出し後には元の値に戻っているように見えます。この挙動を正しく説明するには、「値渡し」という概念を理解する必要があります。
実行モデルと用語の整理
この動作を理解するためには、メモリ上の仕組みに触れる必要があります。
- mainで宣言されたnumは スタック領域(stack area) に格納されます。
- add(num)が呼び出されると、実引数の値がコピーされ、新しい仮引数numが別のスタック領域に用意されます。
- メソッド内でコピーを変更しても、呼び出し元の変数には影響がありません。
このように、プリミティブ型は「値渡し(call by value)」 であり、あくまでコピーを操作していることを理解してください。
設計思想の観点から
ここで強調したいのは、「どこの状態に対して操作しているのか」を常に意識することです。**値渡しは「コピーを操作する」というモデルです。**この設計を理解していれば、「メソッドで変更したのに戻ると変わっていない」という誤解を避けられます。
また、変数名にも注意しましょう。サンプルでは学習用に単にnumとしていますが、実務では「加算対象の点数」であればscore、「基準値」であればbaseValueなど、意図を明示した命名を行うべきです。これがリーダブルコードの基本です。
Smp01_1
で扱った変数は、メソッドが終了すると消えてしまう「その場限りの」ものです。しかし、オブジェクトが自身の情報を記憶し続けるためには、より寿命の長い変数が必要です。それがフィールド (field) です。
-
フィールドとは?
クラスの直下に宣言される変数のことです。オブジェクトの「状態」や「属性」(名前、価格、残高等)を保持するために使われます。 -
ローカル変数との決定的な違い
- 寿命: ローカル変数はメソッド内でのみ生存しますが、フィールドはオブジェクトが存在し続ける限り値を保持します。
- スコープ(有効範囲): ローカル変数は宣言されたメソッド内からしかアクセスできませんが、フィールドはクラス内の複数のメソッドからアクセスできます。
3. フィールド(クラス変数)(Smp01_2)
次に、staticフィールド(クラス変数) を使った例を見てみましょう。これは「クラスにひとつだけ存在する状態」であり、すべての呼び出しから共有されます。
以下のサンプルコードを見てください。
public class Smp01_2 {
// フィールド(クラス変数)
public static int num = 10;
public static void add(){
num += 10;
System.out.println("メソッド内のnum: " + num);
}
// mainメソッドは後ほど示します
}
ここでの num は クラス変数(class variable) です。staticを付けると、変数は**メソッド領域(method area, またはstatic領域)**に確保され、プログラム中でただ一つ存在します。(これは2回目に詳しく説明します)
呼び出し側のコード
public static void main(String[] args){
System.out.println("呼び出し前のnum: " + num);
add();
System.out.println("呼び出し後のnum: " + num);
}
クラス変数の実行モデル
- プログラム開始時にnumがクラスにひとつだけ用意され、値10が入る。
- add()を呼び出すと、そのnumに対して10を足す。
- その変更はクラスにひとつしかないnumに直接作用するため、呼び出し後も変更が残る。
実行結果
このように、クラス変数は全体で共有される状態です。
ここで大切なのは、「共有すべき状態かどうか」を判断することです。クラス全体で共通の情報(例:定数、統計データ、設定値)であればstaticは適切ですが、個々のインスタンスが独立して持つべき情報をstaticにしてしまうと、設計は破綻します。
また、変数名についても、単なるnumではなくtotalScoreやglobalCountなど、共有の性質を示す名前をつけることが望ましいでしょう。
4. インスタンスごとの状態(Smp01_3)
ここまでに、値渡し(コピーとして渡る)とクラス変数(全体で共有される状態)を確認しました。
次に取り上げるのは「インスタンスごとの状態」です。
これはオブジェクト指向の最初の一歩であり、クラスから実体を生み出す「インスタンス化」の意味を体験的に理解できる題材です。
インスタンスとは何か?
クラスが「設計図」だとすれば、インスタンスは、その設計図を元に作られた**「実体(モノ)」**そのものです。
日常での例えるなら:バスの「車種」と、実際に走っている「バス」
- クラス (Class): バスの「車種」または「設計図」 🚌
自動車メーカーが作る「シティバス モデルA」の設計図を想像してください。この設計図には「座席は50席」「そのうち、横向きの優先座席が3席」といった仕様がすべて書かれています。
しかし、設計図そのものは、乗客を乗せて走ることはできません。あくまで「こういうバスを作る」という定義です。
- インスタンス (Instance): 実際に街を走っている、一台一台のバス
工場では、その一つの設計図(クラス)を使って、たくさんの「シティバス モデルA」(インスタンス)を製造します。
「大阪駅前へ向かうバス」と「十三駅前に向かうバス」は、同じ設計図から作られた同じ車種ですが、それぞれが独立した別々のバスです。
- インスタンス変数 (Instance Variable): 現在の「乗客数」
今、この瞬間に、「大阪駅前へ向かうバス」には15人の乗客が、「十三駅前に向かうバス」には8人の乗客が乗っているかもしれません。
この**「乗客数」**が、インスタンスごとに異なる値を持つフィールド(インスタンス変数)にあたります。
大阪駅前へ向かうバスに新しい乗客が1人乗車しても、十三駅前に向かうバスが変わることはありませんよね。
このように、インスタンスごとに状態(データ)は完全に独立しています。
今回の例では、身近な「交通系ICカード」をモデルにしてみましょう。ICカードは同じ会社のカードであっても、AさんのカードとBさんのカードは別々に残高を持っています。チャージや支払いはカードごとに独立して行われるのが当然です。これをプログラムで表現すると、インスタンスごとに状態を保持する、という設計になります。
4.1 クラスの定義
まず、ICカードを表すクラスを作ります。ここでは「残高」をフィールドに持ち、利用したときに料金を差し引くpayメソッドを備えています。
// 補助クラス:ICカード
class ICCard {
int balance; // インスタンスごとの残高
// コンストラクタ:発行時に初期残高を設定
ICCard(int initial) {
balance = initial;
}
// 支払い:このインスタンスの残高を減らす
void pay(int amount) {
balance -= amount;
}
}
ここで注目してほしいのは、balanceがstaticではないことです。
つまり、このフィールドはクラス全体で共有されるのではなく、インスタンスごとに別々に存在します。
AのカードとBのカードがあれば、それぞれが独立したbalanceを持ちます。
4.2 実際の利用例(メインクラス)
次に、実際に2枚のICカードを発行して利用してみましょう。
// Smp01_3「ICカード残高」
public class Smp01_3 {
// 定数(Osaka MetroのICカード料金)
static final int INITIAL_BALANCE = 1500; // チャージ額
static final int TRAIN_FARE = 190; // 1区料金(例:梅田駅~淀屋橋駅)
static final int BUS_FARE = 210; // 大阪メトロバス料金(定額)
public static void main(String[] args) {
// 2枚のICカードを発行(インスタンス生成)
ICCard a = new ICCard(INITIAL_BALANCE);
ICCard b = new ICCard(INITIAL_BALANCE);
System.out.println("[発行] ICカード A: " + a.balance + "円, B: " + b.balance + "円");
System.out.println("---");
// Aのカードで地下鉄を2回利用(Bは未使用)
a.pay(TRAIN_FARE); // 1回目の乗車
System.out.println("[乗車1] A: 梅田 → 淀屋橋 (" + TRAIN_FARE + "円)");
a.pay(BUS_FARE); // 2回目の乗車
System.out.println("[乗車2] A: 大阪駅前 → なんば駅前 (" + BUS_FARE + "円)");
System.out.println("---");
// 最終確認:Aだけ残高が減り、Bは変わらない
System.out.println("[確認] A: " + a.balance + "円, B: " + b.balance + "円");
}
}
// 補助クラス:ICカード(package-private)
class ICCard {
int balance; // インスタンスごとの残高
// コンストラクタ:発行時に初期残高を設定
ICCard(int initial) {
balance = initial;
}
// 支払い:このインスタンスの残高を減らす
void pay(int amount) {
balance -= amount;
}
}
実行モデルと用語の整理
- new演算子でインスタンスを生成すると、そのデータは**ヒープ領域(heap area)**に確保されます。
- aとbはそれぞれ異なるインスタンスを参照しており、別々のbalanceを持ちます。
- したがって、Aの残高を変更してもBの残高には影響しません。
実行結果
このように、インスタンス変数はオブジェクトごとに独立して存在する状態です。
4.3 静的構造の可視化:UMLクラス図
Smp01_3
では、Smp01_3
クラスとICCard
クラスという2つのクラスが連携して動作しました。
このようなクラス間の関係性や、各クラスの内部構造を視覚的に表現するための標準的な手法が、
UML (Unified Modeling Language) のクラス図 (Class Diagram) です。
クラス図は、プログラムのソースコードを直接読むことなく、システムの静的構造 (static structure) を俯瞰的に理解するための設計図です。
Smp01_3
のクラス図
Smp01_3
とICCard
の関係をクラス図で表現すると、以下のようになります。
クラス図の構成要素
-
クラス (Class):
- 長方形のボックスがクラスを表します。3段に分かれており、上からクラス名、属性(フィールド)、**操作(メソッド)**を記述します。
-
属性 (Attribute):
-
可視性 名称 : 型
の形式で記述します。 -
~ balance : int
は、balance
という名前のint
型フィールドがpackage-private
(~
で表現)であることを示します。
-
-
操作 (Operation):
-
可視性 名称(引数) : 戻り値の型
の形式で記述します。 -
{static}
という記述は、そのメンバーが**静的メンバー(クラス変数やクラスメソッド)**であることを示します。
-
-
関係 (Relationship):
- クラス間を結ぶ線は、クラス同士の関係性を表します。
-
Smp01_3 ..> ICCard
の点線の矢印は、依存 (Dependency) 関係を示します。これは、Smp01_3
クラスがそのメソッド(ここではmain
)の中で、ICCard
クラスをインスタンス化して利用している (uses
) ことを意味します。ICCard
クラスは単体で存在できますが、Smp01_3
のmain
メソッドはICCard
無しにはコンパイル・実行できません。
このように、クラス図を用いることで、ソースコードの詳細な実装に立ち入る前に、プログラム全体の設計やクラス間の役割分担を視覚的に把握し、議論することが可能になります。
Discussion