Javaのオブジェクト指向がわかる!4大概念をコード例で完全解説

Javaの書き方は一通り学んだ。クラスも書けるし、newでインスタンスを作るのも何となくできる。なのに「オブジェクト指向って結局何のためにあるの?」という疑問が頭に引っかかったまま進めている。そういう感覚がある方は少なくないはずです。「書けているのに理解できていない」という状態は、Javaを学ぶ人がほぼ全員通る壁でもあります。

結論から言うと、オブジェクト指向は「変更に強いコードを書くため」に生まれた考え方です。コードを大量に書いたあとで仕様が変わったとき、手続き型では1箇所を直すだけで10箇所が壊れる、という経験をした開発者たちが「もっと管理しやすい書き方を」と考えた末にたどり着いたのがこのアプローチです。概念を先に覚えるより、「どんな問題を解くために生まれたか」を知る方が、ずっと腑に落ちやすくなります。

この記事では、オブジェクト指向がなぜ必要になったか(手続き型との違い)、クラスとオブジェクト・インスタンスの関係、4大概念(カプセル化・継承・ポリモーフィズム・抽象化)のコード例、getter/setterや継承への素直な疑問への回答、そしてオブジェクト指向を理解すると実務・転職でどう変わるか、の5点を順に説明します。

この記事でわかること

  • オブジェクト指向がなぜ必要になったか(手続き型との違い)
  • クラス・オブジェクト・インスタンスの関係
  • 4大概念(カプセル化・継承・ポリモーフィズム・抽象化)のコード例
  • getter/setterや継承への素直な疑問への回答
  • オブジェクト指向を理解すると実務・転職でどう変わるか

Javaでオブジェクト指向が難しく感じる3つの理由

「クラスの作り方はわかった。でもなぜこう書くのかがわからない」という感覚、これはJavaを独学している多くの人が感じていることです。このセクションでは、なぜオブジェクト指向がわかりにくいのかをあらかじめ整理しておきます。自分のつまずきポイントを言語化してから先に進むと、以降の説明がぐっと入りやすくなります。

「書けるけど意味がわからない」という感覚の正体

Javaの入門書やオンライン講座では、「クラスを作ってnewでインスタンスを作る」という操作を最初に教わります。コードのコピーと改変を繰り返せば、見た目上は「書けている」状態になれます。ところがその段階では、「なぜこうするのか」がまだ身についていません。

この状態のまま進むと、少し違うコードを書こうとしたとき手が止まります。書き方は知っているのに、何をどこに書けばよいか判断できないからです。「書けるけど意味がわからない」の正体は、操作の模倣と概念の理解のあいだにあるギャップです。このギャップは、「なぜこの書き方をするのか」という問いに一度でも向き合うことで、驚くほど埋まります。

4大概念が一気に出てきて混乱する

オブジェクト指向を調べると、カプセル化・継承・ポリモーフィズム・抽象化という4つの概念がほぼ同時に出てきます。しかもそれぞれが独立した説明で、互いにどう関係しているのかがよくわからない。これが混乱のもとです。

実際には、4大概念は「別々に暗記するもの」ではなく、「変更に強いコードを書く」という同じ目的に向かった4つのアプローチです。それぞれが独立した問題解決の道具として存在しています。この前提を知ってから見ていくと、4つの関係がずっと掴みやすくなります。

「なぜこう書くのか」に答えてくれる記事が少ない

ネット上のJava解説記事の多くは、「カプセル化とはデータとメソッドを一まとめにすることです」「継承とは親クラスのプロパティを引き継ぐことです」という定義から入ります。間違いではありませんが、「だからどうすればいいのか」まで踏み込んでいる記事は多くありません。

「定義はわかった。でも自分のコードにどう活かすかがわからない」という状態になるのは、「何のためにその概念があるのか」という動機が見えていないからです。この記事では次のH2からその動機の説明を先に行います。手続き型で何が困るのかを知ってから4大概念を見ると、「なるほど、これが解決策か」という流れが自然につながります。

▲ 目次に戻る

オブジェクト指向が生まれた理由:手続き型の限界

オブジェクト指向の話をするとき、概念の定義より先に「なぜそれが必要になったか」を知る方が理解の土台になります。このセクションでは、手続き型プログラミングでどんな問題が起きるかをコードで確認し、オブジェクト指向がその問題をどう解消するかを同じコードの書き換えで見ていきます。

手続き型プログラミングで起きる困りごと

手続き型プログラミングでは、データを変数に入れ、処理を上から順番に書いていきます。シンプルな処理なら問題ありませんが、コードが大きくなったり仕様が変わったりすると困った事態が起きてきます。

たとえばユーザーの年齢を管理するコードを考えてみます。

// 手続き型の例(変更すると広範囲に影響)

int userAge = 25;



// プログラムの別の場所でも直接変更できてしまう

userAge = -5; // 不正な値も何のエラーも出さずに入れられる



// さらに別の場所でも

userAge = 200; // これも止めようがない

「年齢に不正な値が入ってはいけない」というルールがあっても、手続き型では変数に直接アクセスできてしまうため、どこかでうっかり無効な値が代入されるリスクが常にあります。コードが数百行・数千行になると、どこで値が変わったかを追うだけでも大変な作業になります。「1箇所直したら別の場所が壊れた」という状況が、手続き型の大規模開発でよく起きる問題です。

オブジェクト指向がその問題をどう解決するか

同じ「ユーザーの年齢管理」をオブジェクト指向で書き直してみます。

// OOPの例(クラスで管理)

class User {

    private int age; // privateにすることで外から直接触れなくする



    public void setAge(int age) {

        // 値を設定する前にバリデーション(検証)を入れられる

        if (age < 0 || age > 120) {

            throw new IllegalArgumentException("不正な年齢です");

        }

        this.age = age;

    }



    public int getAge() {

        return age;

    }

}



// 使う側

User user = new User();

user.setAge(25);  // OK:正常に設定される

// user.setAge(-5); // エラー:不正な値はsetAgeで弾かれる

クラスの中にデータとそのデータを扱うルールをセットで入れることで、「不正な値が入れられない」仕組みを作れます。外部からは必ずsetAgeというメソッドを経由する形になるため、どこで年齢が変わったかを追うのも簡単になります。これがオブジェクト指向の「変更に強い」という意味の核心です。データを守る仕組みと、そのデータを操作するルールを一か所にまとめておく、という発想です。

▲ 目次に戻る

クラスとオブジェクト(インスタンス)の関係を整理する

4大概念を理解するうえで、「クラスとインスタンスの関係」は絶対に押さえておきたい土台です。ここが曖昧なまま継承やポリモーフィズムに進むと、コードの意味が掴めなくなります。日常のたとえとコードをセットで確認しておきましょう。

クラスは「設計図」、オブジェクトは「実体」

クラスとオブジェクトの関係は、「車の設計図」と「実際に走っている車」の関係に似ています。設計図(クラス)は「色・速度・走るという動作」などの情報の定義だけを持っています。実際に色がついて走れる車(オブジェクト)は、その設計図をもとに作られた「実体」です。

設計図は1枚でも、そこから赤い車・青い車・白い車と、何台でも実体を作れます。これがJavaにおける「1つのクラスから複数のインスタンスを作れる」という仕組みの直感的なイメージです。「オブジェクト」と「インスタンス」はほぼ同じ意味で使われますが、Javaでは「クラスからnewで生成したもの」をインスタンスと呼ぶことが多いです。

Javaでクラスとインスタンスを書いてみる

実際のコードで確認します。Carクラスを定義して、インスタンスを2つ作ってみます。

// クラス(設計図)の定義

class Car {

    String color;  // フィールド(データ)

    int speed;



    // メソッド(動作)

    void run() {

        System.out.println(color + "の車が時速" + speed + "kmで走ります");

    }

}



// インスタンス(実体)を作る

Car redCar = new Car();   // 赤い車を1台作る

redCar.color = "赤";

redCar.speed = 60;

redCar.run();  // 出力:赤の車が時速60kmで走ります



Car blueCar = new Car();  // 青い車をもう1台作る

blueCar.color = "青";

blueCar.speed = 80;

blueCar.run(); // 出力:青の車が時速80kmで走ります

「new Car()」と書くたびに、Carクラスの設計図をもとにした新しい実体(インスタンス)が作られます。redCarとblueCarはそれぞれ独立していて、片方のcolorを変えてももう片方には影響しません。クラスが「共通の設計図」で、インスタンスが「その設計図から作られた個別の実体」、これがJavaのオブジェクト指向の最初の入口です。

▲ 目次に戻る

Javaの4大概念をコード例で理解する

カプセル化・継承・ポリモーフィズム・抽象化の4つは、それぞれ「変更に強いコードを書く」という同じ目的に向かった別々のアプローチです。一度に全部覚えようとせず、1つずつ「これはどんな問題を解くためのものか」を確認しながら読み進めてください。順番に見ていけば、必ずつながりが見えてきます。

カプセル化(なぜprivateにするのか)

カプセル化とは、データ(フィールド)とそのデータを操作するメソッドを1つのクラスにまとめ、外部から直接フィールドにアクセスできないようにすることです。「なぜprivateにするのか」という疑問は、前のセクションで見た「不正な値を防ぐ」という動機から来ています。

class User {

    private int age; // privateにすることで外部から直接触れなくする



    // 値を設定するメソッド(setter)

    public void setAge(int age) {

        if (age < 0 || age > 120) {

            throw new IllegalArgumentException("不正な年齢です");

        }

        this.age = age;

    }



    // 値を取得するメソッド(getter)

    public int getAge() {

        return age;

    }

}



// 使う側

User user = new User();

user.setAge(25);  // setterを通してのみ設定できる

System.out.println(user.getAge()); // 出力:25

// user.age = -5; // コンパイルエラー:privateなので直接アクセスできない

privateにすることで、ageという値を変更する場所がsetAgeメソッド1か所だけに限定されます。バリデーションを入れておけば、どこからageを変更しようとしても必ずチェックが走ります。コードが大きくなっても「ageはここで管理している」という明確な責任の場所ができる、それがカプセル化の意義です。

継承(is-a関係とコードの再利用)

継承は、既存のクラスの性質と機能を別のクラスが引き継ぐ仕組みです。「DogはAnimalの一種だ(Dog is-a Animal)」という関係があるとき、AnimalクラスをDogクラスが継承することで、Animal側に書いた共通の処理をDogで書き直す必要がなくなります。

// 親クラス(スーパークラス)

class Animal {

    String name;



    void eat() {

        System.out.println(name + "が食事しています");

    }

}



// 子クラス(サブクラス):Animalを継承

class Dog extends Animal {

    void bark() {

        System.out.println(name + "がワンと吠えます");

    }

}



class Cat extends Animal {

    void meow() {

        System.out.println(name + "がニャアと鳴きます");

    }

}



// 使う側

Dog dog = new Dog();

dog.name = "ポチ";

dog.eat();  // 出力:ポチが食事しています(Animalから引き継いだメソッド)

dog.bark(); // 出力:ポチがワンと吠えます(Dog独自のメソッド)

Dogクラスにはeatメソッドを書いていませんが、Animalを継承しているので使えます。「共通の処理を親クラスにまとめ、子クラスで再利用する」という発想が継承の核心です。Dog・Cat以外に新しい動物クラスを追加するときも、Animalを継承させれば共通処理を毎回書く手間が省けます。

ポリモーフィズム(if文削減と拡張性)

ポリモーフィズム(多態性)は、異なるクラスのオブジェクトに対して同じメソッド名で呼び出しを行い、それぞれのクラスに応じた動作をさせる仕組みです。「たくさんのif文を1行に置き換える」と覚えておくと実感しやすいです。

before/afterで比べてみます。

// Before:動物が増えるたびにif文を追加しなければならない

String type = "dog";

if (type.equals("dog")) {

    dog.bark(); // 犬のとき

} else if (type.equals("cat")) {

    cat.meow(); // 猫のとき

}

// 新しい動物が増えるたびに、このif文を探して追記する必要がある

// After:ポリモーフィズムを使う

class Animal {

    String name;

    void speak() {

        // 共通の鳴き声メソッド(子クラスで上書きする)

    }

}



class Dog extends Animal {

    void speak() { System.out.println(name + ":ワン"); } // オーバーライド

}



class Cat extends Animal {

    void speak() { System.out.println(name + ":ニャア"); } // オーバーライド

}



// Animal型のリストに入れて、まとめて処理できる

List<Animal> animals = new ArrayList<>();

animals.add(new Dog());

animals.add(new Cat());



for (Animal a : animals) {

    a.speak(); // どのクラスのインスタンスかに応じて自動的に正しいspeakが呼ばれる

}

// 新しい動物クラスを追加するときは、そのクラスを追加するだけでよい

ポリモーフィズムがあると、新しい動物を追加するときにfor文の中身を変える必要がありません。「機能を追加するときに既存コードを変えなくてよい」という性質を開放閉鎖原則(OCP)と呼び、保守性の高いコード設計の基本になっています。

観点 Before(if文で型チェック) After(ポリモーフィズム)
動物を追加するとき if文を探して追記が必要 新クラスを追加するだけでOK
コードの変更範囲 呼び出し側も変更が必要 呼び出し側は変更不要
読みやすさ 分岐が増えるほど複雑になる シンプルなループのまま保てる
バグのリスク 追記漏れで動かないケースが出る 既存コードに触れないため低い

抽象化(抽象クラスとインターフェースの使い分け)

抽象化とは「共通のルール・約束だけを定義して、具体的な実装は各クラスに任せる」という考え方です。Javaでは抽象クラス(abstract class)とインターフェース(interface)の2つの方法があります。

// インターフェース:「泳げる」という約束だけを定義

interface Swimmable {

    void swim(); // 実装は書かない、約束だけ

}



// 抽象クラス:共通処理を持ちつつ、一部を子クラスに委ねる

abstract class Animal {

    String name;



    void eat() {

        System.out.println(name + "が食事しています"); // 共通処理は実装しておける

    }



    abstract void speak(); // 鳴き声は各動物ごとに違うので、実装は子クラスに任せる

}



// DuckはAnimalを継承しつつ、Swimmableも実装できる

class Duck extends Animal implements Swimmable {

    public void speak() {

        System.out.println(name + ":ガアガア");

    }

    public void swim() {

        System.out.println(name + "が泳いでいます");

    }

}

使い分けの目安はシンプルです。共通の状態(フィールド)や処理を持たせたい場合は抽象クラス、「この機能が使える」という能力の宣言だけをしたい場合はインターフェースを使います。Javaではクラスの継承は1つしかできませんが、インターフェースは複数実装できるため、「泳げる」「飛べる」など複数の能力を持たせたいときにインターフェースが活きます。

▲ 目次に戻る

「なぜこう書くの?」初心者の3つの素直な疑問に答える

オブジェクト指向を学んでいると、「なぜわざわざこんな書き方をするのか」という疑問が必ず出てきます。参考書や入門記事のほとんどは「何を書くか」は教えてくれますが、「なぜそう書くか」まで踏み込んでいることは少ない。このセクションでは、初心者が持ちやすい3つの素直な疑問に直接答えます。

getter/setterはなぜ必要か(publicにしてはいけない理由)

「フィールドをpublicにすれば直接読み書きできるのに、なぜわざわざgetterとsetterを作るの?」という疑問は、Javaを学び始めた人がほぼ全員持ちます。面倒なだけに見えるこの仕組みには、しっかりした理由があります。

publicフィールドの問題は、「どこからでも、どんな値でも代入できてしまう」点です。

// publicフィールドの危険性

class User {

    public int age; // publicだと外部から直接変更できる

}



User user = new User();

user.age = -100; // 不正な値でも何も起きない

user.age = 999;  // これも止められない

一方、privateフィールド+setterにすると、値を設定する前に必ずバリデーション(入力チェック)を入れられます。前のH2-4カプセル化のコードで見た通りです。setterで「0以上120以下の整数でなければエラー」というルールを書いておけば、コードのどこから呼ばれても不正な値は入りません。

もう一つの理由として、将来の変更への対応があります。フィールドをpublicにしていると、後から「年齢をintではなくAgeという専用クラスで管理したい」と変えたくなったとき、user.ageと書いている箇所をプログラム全体から探して修正しなければなりません。getAge()というメソッド経由にしておけば、getAge()の中身だけ変えれば済むため、変更の影響が最小限に抑えられます。

継承とコピペの違いは何か

「同じコードはコピペすればよくない?継承って余計に複雑じゃない?」という疑問も自然です。小さいプログラムならコピペでも動きます。ただし、コードが育ってくると深刻な問題が出てきます。

コピペで同じeatメソッドをDog・Cat・Birdに貼り付けたとします。あとで「食事の表示を「食べています」から「食事中です」に変えたい」となった場合、3か所すべてを探して書き直す必要があります。どこかに変更漏れがあれば、動物によって表示が変わる不整合バグが生まれます。

継承を使っていればAnimalのeatメソッドを1か所直すだけで、全クラスに変更が反映されます。「10か所に分散していたバグの修正箇所が1か所になる」というのが、コピペと継承の決定的な違いです。コードの行数は増えるように見えても、変更コストとバグリスクは大幅に下がります。

ポリモーフィズムはいつ役立つのか

「ポリモーフィズムの概念はわかった。でも自分が書くコードで使う場面が想像できない」という感想もよく聞きます。実感しやすい場面を一つ挙げます。

たとえばECサイトで「支払い方法」を追加する場合を考えてみます。最初はクレジットカードだけでした。あとでコンビニ払い・銀行振込・QRコード決済を追加することになりました。

ポリモーフィズムを使わないと、支払い処理のロジックにif文を追加し続けることになります。新しい支払い方法が増えるたびに既存のコードを変更するため、テスト範囲も広がりバグのリスクが高まります。

ポリモーフィズムを使って「支払える(pay)」というインターフェースを定義しておけば、新しい支払い方法は「Payableを実装した新クラスを追加する」だけで済みます。既存の支払い処理ロジックには一切手を触れません。これがポリモーフィズムの「機能追加しても既存コードが壊れない」という価値の実体です。Spring BootなどのフレームワークもこのパターンをDIコンテナという形で大規模に活用しています。

▲ 目次に戻る

オブジェクト指向が分かると何が変わるか

4大概念を理解することは、Javaの文法を知っていることとは少し違う意味を持ちます。「なぜこう書くか」がわかると、コードを読む力も書く力も変わります。転職・実務・フレームワーク学習、それぞれの場面でどう変わるかを具体的に整理します。

転職面接では、「Javaのオブジェクト指向を説明してください」という質問が高い確率で出ます。定義を暗記して答えるのと、「変更に強いコードを書くために生まれた考え方で、たとえばカプセル化を使うと…」と文脈から答えられるのでは、面接官の印象がまったく違います。Javaの難易度が高いと言われる5つの理由でも触れていますが、オブジェクト指向の「なぜ」を答えられることは、転職評価の中でも特に差がつきやすいポイントです。

実務では、Spring Bootなどのフレームワークがオブジェクト指向の概念を前提に設計されています。DIコンテナ(依存性の注入)はポリモーフィズムを活用した仕組みですし、Repositoryパターンは抽象化そのものです。4大概念を理解してからSpring Bootのコードを見ると、「なぜこう書くのか」が自然に読み取れるようになり、ドキュメントの理解速度も上がります。

オブジェクト指向を学んだあとの次のステップについては、【初心者向け】最短でJavaを習得!学習ロードマップでJava全体の学習の流れを確認してみてください。独学で進める方は、Java独学の始め方と挫折しないコツ|87.5%が挫折する理由も参考になります。

▲ 目次に戻る

まとめ:Javaのオブジェクト指向、これで迷わない

この記事で押さえた内容を振り返ります。

  • オブジェクト指向は「変更に強いコードを書くため」に生まれた。手続き型で起きる「1か所直したら10か所壊れる」問題への解決策として生まれた考え方
  • クラスは設計図、オブジェクト(インスタンス)はその設計図から作られた実体。1つのクラスから何個でも作れる
  • 4大概念はそれぞれ独立した問題解決の道具。カプセル化は「不正な値を防ぐ」、継承は「共通処理を再利用する」、ポリモーフィズムは「if文を排除して拡張しやすくする」、抽象化は「約束だけ決めて実装を任せる」
  • getter/setterや継承には「面倒でも使う理由」がある。コードが育ったときの変更コストとバグリスクを大幅に下げる仕組み
  • 4大概念を理解すると、Spring Bootなどのフレームワーク学習がスムーズになり、転職面接でも自分の言葉で答えられるようになる

オブジェクト指向は、ルールをすべて暗記してから実践するものではありません。コードを書きながら「なぜこう設計するのか」を少しずつ実感していくものです。まずはカプセル化だけでも、自分のコードに取り入れてみてください。「privateにしてsetterを作る」という一歩が、オブジェクト指向を体で理解する入口になります。

次に何を学ぶかは、Javaロードマップ記事で学習の全体像を確認しながら決めてみてください。スクールで体系的に学びたい場合は、初心者向けJavaが習得できるおすすめプログラミングスクールも参考になります。