okpy

Pythonエンジニア兼テックリーダーが、多くのプロジェクトとチーム運営から得た実践的な知識を共有するブログです。

オブジェクト指向プログラミング(OOP)の基礎固め - Java 編

プログラミングを始めると、よく耳にするキーワードの一つが OOPオブジェクト指向プログラミング) です。しかし、「本当に」オブジェクト指向をよく理解している人は意外に少ないかもしれません。
今回は、オブジェクト指向の基本概念から Java のサンプルコードまで順を追って確認し、なぜオブジェクト指向が重要なのか、そしてどのように適用するのかを学んでいきましょう。

 


1. オブジェクト指向プログラミング(OOP)とは?

オブジェクト指向プログラミング とは、プログラムを「オブジェクト」という単位に分けて設計・実装する手法です。オブジェクトは状態(フィールド/メンバ変数)と振る舞い(メソッド)をあわせ持ち、複数のオブジェクトが相互作用してシステムを構成します。

オブジェクト指向プログラミングの利点

  1. 再利用性: 共通機能を継承やインターフェースを通じて再利用できる
  2. 保守性: コードが分割されているため、修正時の影響範囲が小さい
  3. 拡張性: 新しい機能やクラスを追加しやすい
  4. 抽象化: 複雑なシステムをオブジェクト単位に分割し、理解しやすくする

2. オブジェクト指向の4大特徴

オブジェクト指向を説明するときによく取り上げられる4つの重要な特徴は、
カプセル化(Encapsulation)継承(Inheritance)ポリモーフィズム(Polymorphism)抽象化(Abstraction) です。
これらをしっかり理解することが、オブジェクト指向プログラミングの第一歩となります。

2-1. カプセル化(Encapsulation)

  • 定義: オブジェクトが内部的に管理すべき属性(フィールド/メンバ変数)や機能(メソッド)を、外部から「隠蔽」または「保護」すること。
  • なぜ必要か?
    • 外部からオブジェクトの内部状態をむやみに変更されると、予期しないエラーが起きる可能性がある。
    • オブジェクト内部の実装が変わっても、外部からは公開されたメソッド(インターフェース)さえ使えばよいので影響が少ない。
  • Java の例:
    public class BankAccount {
        private double balance; // privateにして直接アクセスを防止
    
        public BankAccount(double initialBalance) {
            this.balance = initialBalance;
        }
    
        // balance に間接的にアクセスするメソッド
        public void deposit(double amount) {
            if (amount > 0) {
                balance += amount;
            }
        }
    
        public void withdraw(double amount) {
            if (amount > 0 && amount <= balance) {
                balance -= amount;
            }
        }
    
        public double getBalance() {
            return balance;
        }
    }
    

    解説: balance フィールドを privateカプセル化し、depositwithdraw メソッドを通してのみ残高を変更可能にしています。こうすることで、残高の整合性が保証され、外部が直接 balance を不正に操作できません。

2-2. 継承(Inheritance)

  • 定義: 既存のクラス(親)をもとに、新しいクラス(子)を作ること。子クラスは親クラスのフィールドやメソッドを受け継ぎ、必要に応じて機能を拡張したり再定義したりできます。
  • なぜ必要か?
    • 重複コードを削減し、再利用性を向上させる。
    • クラスの階層構造により、オブジェクト同士の関係を明確に表せる。
  • Java の例:
    // 親クラス
    public class Payment {
        protected double amount;
        public Payment(double amount) {
            this.amount = amount;
        }
        public void pay() {
            System.out.println("基本の支払い: " + amount + "円");
        }
    }
    
    // 子クラス
    public class CreditCardPayment extends Payment {
        public CreditCardPayment(double amount) {
            super(amount); // 親クラスのコンストラクタを呼び出し
        }
    
        @Override
        public void pay() {
            System.out.println("クレジットカードで支払い: " + amount + "円");
        }
    }
    

    解説: Payment クラスを親とし、CreditCardPayment がこれを継承しています。子クラスは親クラスの amount フィールドや pay() メソッドを引き継ぎつつ、pay() をオーバーライドしてクレジットカード専用の支払い処理を実装しています。

間違った継承設計の例

  • 継承階層が深すぎたり、無関係な機能を無理やり継承するケース(例:Animal -> Bird -> Eagle -> FighterJet??)。
  • 「継承」が必要ない(特に “has-a” 関係の場合)にもかかわらず、常に継承を使うケース。実際は「合成(Composition)」の方が適切な場合も多い。

2-3. ポリモーフィズム(Polymorphism)

  • 定義: 「1 つのインターフェースに対して複数の実装が存在する」こと。主に継承と組み合わせて使われ、親クラス型の変数で子クラスのオブジェクトを扱えます。
  • なぜ必要か?
    • コードの柔軟性向上:親(もしくはインターフェース)型を使うことで、さまざまな子クラスを処理できる。
    • 保守性向上:新しい子クラスを追加しても、既存のコードを大幅に変更せずに済む。
  • Java の例:
    public class PaymentProcessor {
        public static void process(Payment payment) {
            payment.pay();
        }
    
        public static void main(String[] args) {
            Payment cardPayment = new CreditCardPayment(10000);
            Payment cashPayment = new CashPayment(5000);
    
            // ポリモーフィズムの適用
            process(cardPayment); // クレジットカードで支払い: 10000円
            process(cashPayment); // 現金で支払い: 5000円
        }
    }
    
    public class CashPayment extends Payment {
        public CashPayment(double amount) {
            super(amount);
        }
        @Override
        public void pay() {
            System.out.println("現金で支払い: " + amount + "円");
        }
    }
    

    解説: PaymentProcessor.process() メソッドは Payment 型(親クラス)を引数としますが、実際には CreditCardPaymentCashPayment(子クラス)のオブジェクトを渡しても、ポリモーフィズムによりそれぞれの pay() が呼び出されます。

2-4. 抽象化(Abstraction)

  • 定義: 複雑なシステムの中から本質的な部分だけを取り出し、他の詳細を隠すこと。インターフェースや抽象クラス(abstract class)を使って実現します。
  • なぜ必要か?
    • システム全体の要件を大きくとらえ、必要な機能だけを定義することで柔軟な設計ができる。
    • 具体的な実装(子クラス)側が、自由に拡張して対応できるようになる。
  • Java の例:
    public abstract class Shape {
        // 抽象メソッド:実装は子クラスに任せる
        public abstract double getArea();
        public abstract double getPerimeter();
    }
    
    public class Rectangle extends Shape {
        private double width, height;
    
        public Rectangle(double width, double height) {
            this.width = width;
            this.height = height;
        }
    
        @Override
        public double getArea() {
            return width * height;
        }
    
        @Override
        public double getPerimeter() {
            return 2 * (width + height);
        }
    }
    

    解説: Shape という抽象クラスは「図形は面積と周囲長を求められる」という共通の取り決めだけを提示します。実際の計算ロジックは、RectangleCircle などの子クラスで異なる方法で実装できます。


3. クラス vs. オブジェクト

クラス は「設計図(blueprint)」のようなもので、フィールドやメソッドを定義する抽象的な枠組みです。一方で、
オブジェクト は、その設計図からメモリ上に生成された「実体(インスタンス)」を指します。

例:Person クラス

public class Person {
    // フィールド(属性)
    String name;
    int age;

    // メソッド(振る舞い)
    public void sayHello() {
        System.out.println("Hello, my name is " + name);
    }
}
  • この段階では「設計図」であり、実際に使えるオブジェクトではありません。
  • 下記のようにオブジェクトを生成(インスタンス化)して初めて「実体」が生まれます。
public class Main {
    public static void main(String[] args) {
        Person kim = new Person();
        kim.name = "Kim";
        kim.age = 25;
        kim.sayHello(); // Hello, my name is Kim

        Person lee = new Person();
        lee.name = "Lee";
        lee.age = 30;
        lee.sayHello(); // Hello, my name is Lee
    }
}

メモリ構造(簡単な概念)

  • スタック領域(Stack): メソッド呼び出し時に使われるローカル変数や参照変数が保存される。
  • ヒープ領域(Heap): new キーワードで生成されたオブジェクトが保存される。
  • 参照(Reference): スタック領域にある参照変数が、ヒープ領域に生成されたオブジェクトのアドレスを指す。

4. アクセス修飾子とカプセル化

Java には、private, public, protected, (デフォルト) という4種類のアクセス修飾子があります。これらを使い分けることで、オブジェクトの内部実装を安全に保護し、必要な機能だけを外部に公開できます。

代表的なアクセス修飾子

  1. public: どこからでもアクセス可能
  2. private: 同一クラス内のみアクセス可能
  3. protected: 同一パッケージ内、または子クラスからアクセス可能
  4. デフォルト(指定なし): 同一パッケージ内でのみアクセス可能

カプセル化と Getter/Setter

カプセル化 のため、多くの場合フィールドは private にし、必要に応じて public なメソッドや Getter/Setter を通じて値を取り出したり変更したりします。

public class Student {
    private String name;
    private int grade;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        // 必要ならバリデーションロジックを入れる
        this.name = name;
    }

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        if (grade >= 1 && grade <= 3) {
            this.grade = grade;
        }
    }
}

利点: setGrade() メソッドで学年が 1〜3 の範囲内に収まるよう制限できるため、データの整合性 が保たれます。


5. まとめと今後の学習に向けて

ここまで、オブジェクト指向の基本概念である カプセル化、継承、ポリモーフィズム、抽象化 を取り上げ、
クラスとオブジェクト の違いや、アクセス修飾子 を使ったカプセル化の手法を確認しました。
オブジェクト指向プログラミングにおける最も重要な部分ですので、サンプルコードを何度も書いて身につけていくことをおすすめします。

次のステップ としては、[SOLID の原則] や [デザインパターン] などのオブジェクト指向設計手法を学んでみてください。
こういった基礎を段階的に積み重ねることで、より 柔軟で保守しやすいコード を書けるようになるでしょう。

ご質問や 「こんなトピックを扱ってほしい!」 などありましたら、コメントをお寄せください。
一緒に悩み、そして成長していきましょう!


参考資料

  • [Effective Java - Joshua Bloch]
  • [Head First Object-Oriented Analysis and Design]
  • [Clean Code - Robert C. Martin]