🐷

Javaで学ぶOOP基礎(クラス,継承,ポリモーフィズム,カプセル化)

2024/07/15に公開

オブジェクト指向プログラミング(OOP)を理解する

~前書き~

前書き: 私のOOP学習きっかけ

去年私はJavaを少ししか触らずして、kotlinでの開発がはじまりました。
しかし、理解しようとしてもJavaが、OOPが、わかんないからなのか?理解できないと
感じることが多かったです。
そこでやったのが、Javaに戻ってJavaでOOPを改めて学ぶ!
そしてそれはKotlinではどう変化した?KotlinのこれはJavaの何に当たるのか?
というふうに学ぶに変更しました。
(人によってはルート違うかも?いろんな方法がありそう!)

ということでアウトプットとして、OOPについて学んできたことを、
基礎と題して書いていこうと思います!

今回は日常に当てはめながらわかりやすく書きたいと思う!

基本概念(ポイント)

  • 継承(クラス/すでにあるクラスを利用して新しいクラスを定義)
  • カプセル化(オブジェクトの安全性と独立性を高める)
  • ポリモーフィズム
    (オブジェクトの種類により同じメソッドでも違う操作を行わせる)

基本概念①クラス: オブジェクトを生成する雛形(設計図)

まずは、オブジェクトって何?というところからだが、
オブジェクト、は日本語にしたら"もの"だ。
OOPの考え方での"もの"は個別の実体を指している。
(最初は実体という言葉に戸惑う可能性があるがスルーでいい。)

そのオブジェクトを作る雛形(設計図)が、クラスだ。

クラスには、属性と操作を定義することができる。

例えば、犬を表現するクラスで考えてみる。
犬には名前や年齢といった属性(フィールド)があり、吠えるといった操作ができる。

Dog
public class Dog {
    // 属性(Javaでいう"変数"のこと)
    private String name;
    private int age;

    // コンストラクタ
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 操作(Javaでいうメソッド): 吠える!
    public void bark() {
        System.out.println(name + " is barking!");
    }

    // getterメソッド
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

このDogクラスは、犬の一般的な特性(名前、年齢)と動作(吠える)を定義した。
しかし、Dogクラス自体は具体的な犬を表しているわけではない。

ここから具体的に犬を作ろう。
クラスから具体的な実体を作るには、newキーワードを使う。

Main
public class Main {
    public static void main(String[] args) {
        // Dogクラスの具体的なオブジェクトを生成
        Dog myDog = new Dog("Buddy", 3);

        // オブジェクトのメソッドを呼び出す
        myDog.bark(); // Buddy is barking!
        
        // オブジェクトのフィールドを取得する
        System.out.println("Name: " + myDog.getName()); // Name: Buddy
        System.out.println("Age: " + myDog.getAge());   // Age: 3
    }
}
mermaid

このコードで、Dogクラスからnewして、
myDogという名前の具体的実体(ここでは犬)が作られた。"Buddy"という名前で3歳の犬だ。
この具体的な実体を"インスタンス"と呼んでいる。

言い方変えたら、クラスとインスタンスの関係は、
"クラスは種類で、インスタンスは具体的な実体"とも言える。

補足: コンストラクタ

ここまで触れてこなかったが、コンストラクタについても説明する。
上記Dogクラスのコードに以下のようなコードがあったと思う.

Dog
public class Dog {
  :
  :
    // コンストラクタ
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
  :
}

クラスの新しいオブジェクトを生成する際に呼び出される特別なメソッドのことだ。

クラスの属性に値を設定し、オブジェクトが適切に動作するために必要な初期設定を行うもので、
一言で言うならば、"オブジェクトの初期化"だ。

<コンストラクタの特徴>

  • クラスと同じ名前を持つ
  • 戻り値はない: コンストラクタには戻り値がありません(voidも書かない)。
  • オブジェクト生成時に自動的に呼び出される:
    newキーワードを使用してオブジェクトを生成する際に、コンストラクタが呼び出される。

基本概念②,③: 継承とポリモーフィズムって?

ここでも日常に合わせながら簡単に説明を書いてみる。
ポリモーフィズム(polymorphism)は一言で言ったら、
"同じ名前のメソッドの呼び出しに対して異なる機能・動作をさせること"だ。

polymorphism: 日本語にしたら、いろんな形に変わる、という意味!

異なるクラスのオブジェクトが、同じメソッド呼び出しに対して
それぞれ異なる方法で応答する能力
、という言い方もできる。

コードで見てみる。
まずこのAnimalというクラス。このクラスではmakeSound()メソッドの作成を行なっている。

Animal
// 親クラス
public class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

ではAnimalクラスのmakeSound()メソッドを使用して、
犬は犬の鳴き声、猫は猫の鳴き声で応答するようにしよう。

このときはこの親となるクラスを"継承"し、メソッドをオーバーライドすることで実現できる。

Dog
//  Animalクラスの子クラス
public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Bark");
    }
}

DogクラスはAnimalクラスを継承し、
makeSound()メソッドをオーバーライドして、犬の鳴き声(Bark)を出力する。

補足: @Override:
メソッドが親クラスのメソッドをオーバーライドしていることを明示的に示すアノテーション。
このアノテーションはコンパイル時にチェックされ、正しくオーバーライドされていることを保証する

"継承"は一言で言えば、"あるクラスが別のクラスの属性やメソッドを引き継ぐこと"だ。

以下Catクラスも同様、Animalクラスを継承し、
makeSound()メソッドをオーバーライドして、猫の鳴き声(Meow)を出力する。

Cat
// 子クラス
public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow");
    }
}

ではこれらをMainクラスで使用していく。

Main
public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        myDog.makeSound(); // Bark
        myCat.makeSound(); // Meow
    }
}

このコードでは、Animal型の変数(myDog,myCat)にDogCatのインスタンスを
割り当てている。そして、makeSound()メソッドを呼び出すと...
それぞれの実際の型に応じた動作が実行されるようになっている。(コメントアウトのように。)

これが、ポリモーフィズムだ。
親クラスのメソッドを子クラスで異なる方法で実装できるため、
異なるオブジェクトが同じインターフェースを持ちつつ、異なる動作をすることが可能
になる。

一般的なコードを親クラスに定義し、具体的な実装を子クラスに任せることで、
コードの再利用性も高まるし、既存のコードを変更せずに、新しい動作を追加できるため、
システムのメンテナンスが容易になる

今回のこのようにメソッドをオーバーライディングした方法は、
ポリモーフィズムの中でも動的ポリモーフィズム(実行時ポリモーフィズム)と呼ばれる。
具体的にすると、ポリモーフィズムは2種類に分かれて、動的と静的があるが、
ポイントを抑えるには必要ないので、タブに軽くまとめます!

補足:動的/静的ポリモーフィズムについて

補足:動的/静的ポリモーフィズムについて

特徴 動的ポリモーフィズム
(実行時ポリモーフィズム)
静的ポリモーフィズム
(コンパイル時ポリモーフィズム)
説明 実行時にメソッドの呼び出しが決定される。 コンパイル時にメソッドの呼び出しが決定される。
使用方法 メソッドオーバーライディング メソッドオーバーローディング
親クラスのメソッドを
子クラスでオーバーライドし、
実行時に適切なメソッドが呼び出される。
同じメソッド名で
異なる引数リストを持つ
複数のメソッドを定義し、
コンパイル時に適切なメソッドが選ばれる。

動的ポリモーフィズム(実行時ポリモーフィズム)

動的ポリモーフィズムは、
プログラムの実行時にどのメソッドが呼び出されるかが決定されるポリモーフィズムのこと。
メソッドオーバーライディングがこれに該当する。

ここは上記のポリモーフィズムの話なのでコードは省略。

静的ポリモーフィズム(コンパイル時ポリモーフィズム)

静的ポリモーフィズムは、
プログラムのコンパイル時にどのメソッドが呼び出されるかが決定されるポリモーフィズム
メソッドオーバーローディングがこれに該当します。

public class MathUtils {
    
    public static int add(int a, int b) {
        return a + b;
    }

    public static int add(int a, int b, int c) {
        return a + b + c;
    }

    public static double add(double a, double b) {
        return a + b;
    }
}
// メインクラス
public class Main {
    public static void main(String[] args) {
        System.out.println(MathUtils.add(2, 3));          // 5
        System.out.println(MathUtils.add(2, 3, 4));       // 9
        System.out.println(MathUtils.add(2.5, 3.5));      // 6.0
    }
}

この例では、addメソッドが異なる引数リストを持つ複数のバージョンとして定義されていて、
どのaddメソッドが呼び出されるかはコンパイル時に決定される。

そしてもう少し。継承についても補足すると、
継承関係にあるクラスの継承元(親)のクラスのことを"スーパークラス"
継承先の子クラスのことは"サブクラス" と呼ぶ。


クラスをもう少し見てみる..."カプセル化"を知る

大雑把に見てきたので、少し"クラス"の説明に内容を加えていく。上記では、
クラスはオブジェクトを作る雛形(設計図)で、属性(変数)と操作(メソッド)を持っているもの
というふうに書いたが、こんな表現もある。

クラスは「まとめて、隠して、たくさん作る」もの

サブルーチンという言葉に最初は躓くかもしれない!!(昔の私)
難しいことではなくて、"サブルーチン"は一般的に関数やメソッドを指す用語で、
クラス内にまとめられたもの(サブルーチン)を特に"メソッド" と呼んでいる!

補足:サブルーチンとは

補足:サブルーチンとは

サブルーチンとは、
プログラム内の特定のタスクを実行するために再利用可能なコードのブロックのことだ。
"メインの処理(ルーチン)から呼び出される別の処理"という表現もされる。

メソッドや関数として実装され、特定の処理を一度定義すれば、何度でも呼び出して利用可能。
サブルーチンを使うことで、同じコードを繰り返し書く必要がなくなり、
プログラムの可読性や保守性が向上する!

ex.

public class MathUtils {
    // サブルーチン: 二つの数値を加算するメソッド
    public static int add(int a, int b) {
        return a + b;
    }

    // サブルーチン: 二つの数値を乗算するメソッド
    public static int multiply(int a, int b) {
        return a * b;
    }
}

public class Main {
    public static void main(String[] args) {
        int sum = MathUtils.add(5, 3); // サブルーチンを呼び出す
        int product = MathUtils.multiply(4, 2); // サブルーチンを呼び出す

        System.out.println("Sum: " + sum); // Sum: 8
        System.out.println("Product: " + product); // Product: 8
    }
}

いろんな変数があるけど、ここでいう"変数"は、
"グローバル変数をまとめよう"ということで、まとめた後の変数は、"インスタンス変数"と呼ぶ。

インスタンス変数:
クラス内で定義されるインスタンスごとに異なる値を持つ変数
各オブジェクトが独自に持つ変数のことをインスタンス変数とよんでいる。
別クラスからはアクセス不可で、
一旦インスタンスが作られた後は、必要無くなるまでメモリに残る。

"隠す"=カプセル化(Encapsulation)

"クラス内部だけで使う変数やサブルーチンを"隠す""

データの一貫性と安全性を保つために、
クラスの内部実装やロジックを隠し、外部からの不正アクセスや変更を防ぐことが大事で、
このことを"カプセル化"という。

クラスに実装された処理の独立性を高めることとも言います。

"アクセス修飾子でクラスのメンバ(変数やメソッド)へのアクセス範囲が決定される。"ので、
アクセス修飾子については知っている必要がある。

補足: アクセス修飾子によるアクセス範囲

private: クラス内部からのみアクセス可能
public: どこからでもアクセス可能
protected: 同じパッケージ内およびサブクラスからアクセス可能
(デフォルト修飾子): 同じパッケージ内からアクセス可能


OOPの基礎として書きましたが、間違えや
こんなことも書いておく必要があるのでは?などありましたら教えてください~^^

Discussion