JavaにおけるOOP:クラス、オブジェクト、カプセル化、継承、抽象化

Javaは常に世界トップ3の人気言語の1つです。エンタープライズソフトウェア開発、Androidモバイルアプリ、大規模Webアプリケーションなどの分野での採用は類を見ません。強力な型システム、広範なエコシステム、そして「一度書いてどこでも実行」の能力は、頑丈でスケーラブルなシステムを構築する際に特に魅力的です。この記事では、Javaのオブジェクト指向プログラミング機能が開発者にこれらの機能を効果的に利用させ、適切なコードの組織化と再利用を通じて保守可能かつスケーラブルなアプリケーションを構築できるようにする方法を探ります。

A Note on Organizing And Running Java Code

コードを書き始める前に、セットアップを行いましょう。

構文と同様に、Javaはコードの組織化について厳格なルールを持っています。

最初に、各publicクラスは独自のファイルに配置する必要があります。ファイル名はクラスとまったく同じである必要があり、拡張子は.javaです。つまり、Laptopクラスを作成したい場合、ファイル名はLaptop.javaとなります。ファイル名は大文字小文字を区別します。同じファイル内に非publicクラスを含めることもできますが、別々にすることがベストです。クラスを書く前にクラスをどこに配置するかの大まかな考えを持っておくのは良いアイデアです。

すべてのJavaプロジェクトには、Main.java ファイルが必要で、Main クラスが含まれている必要があります。ここで、クラスをテストするためにオブジェクトを作成します。

Javaコードを実行するには、 IntelliJ IDEA、人気のあるJava IDEを使用します。IntelliJをインストールした後:

  1. 新しいJavaプロジェクトを作成します(ファイル>新規>プロジェクト)
  2. srcフォルダを右クリックして、Main.javaファイルを作成し、以下の内容を貼り付けます:
public class Main { public static void main(String[] args) { // ここでオブジェクトを作成してテストします } }

クラスについて話すときは、Main.javaファイル以外の他のファイルにコードを書きます。しかし、オブジェクトの作成とテストについて話すときは、Main.javaに切り替えます。

プログラムを実行するには、メインメソッドの隣にある緑の再生ボタンをクリックしてください:

出力は、下部の実行ツールウィンドウに表示されます。

Javaに全く新しい場合は、ぜひ当社の Java入門コースをご覧ください。このコースでは、基本的なデータ型と制御フローを学んだ後に進むことができます。

さあ、早速始めましょう。

Javaクラスとオブジェクト

では、クラスとは正確に何ですか?

クラスは、Javaにおけるプログラミング構造で、現実の概念を表現するためのものです。例えば、このMenuItemクラスを考えてみてください(IDEでこのクラスを書くためのファイルを作成します):

public class MenuItem { public String name; public double price; }

このクラスは、レストランのさまざまなメニューアイテムを表現するための青写真またはテンプレートを提供します。クラスの2つの属性、name, および priceを変更することで、ハンバーガーやサラダなど、無限のメニュー オブジェクトを作成することができます。

Javaでクラスを作成するには、クラスのアクセスレベルを示す行を開始します(privatepublic、または protected)の後にクラス名が続きます。括弧の直後に、クラスの属性を概説します。

しかし、このクラスに属するオブジェクトをどのように作成すればよいのでしょうか? Javaはこれをコンストラクタメソッドを介して可能にします:

public class MenuItem { public String name; public double price; // コンストラクタ public MenuItem(String name, double price) { this.name = name; this.price = price; } }

コンストラクタは、クラスから新しいオブジェクトを作成する際に呼び出される特別なメソッドです。それは提供された値でオブジェクトの属性を初期化します。上記の例では、コンストラクタは名前と価格のパラメータを取り、これらを将来のオブジェクトインスタンスを参照するために ‘this’ キーワードを使用してオブジェクトのフィールドに割り当てます。

コンストラクタの構文は、戻り値の型を指定する必要がないため、他のクラスメソッドとは異なります。また、コンストラクタの名前はクラスと同じでなければならず、クラス定義の後に宣言した属性と同じ数の属性を持っている必要があります。上記では、クラス定義の後に2つ宣言したため、コンストラクタは2つの属性を作成しています: nameprice

クラスとそのコンストラクタを書いた後は、メインメソッドでインスタンス(オブジェクト)を作成できます:

public class Main { public static void main(String[] args) { // ここでオブジェクトを作成します MenuItem burger = new MenuItem("Burger", 3.5); MenuItem salad = new MenuItem("Salad", 2.5); System.out.println(burger.name + ", " + burger.price); } }

出力:

Burger, 3.5

上記では、2つのMenuItemオブジェクトを変数burgersaladに作成しています。Javaでは、変数のタイプを宣言する必要があり、これはMenuItemです。次に、クラスのインスタンスを作成するために、newキーワードを記述し、コンストラクタメソッドを呼び出します。

コンストラクタ以外にも、クラスに動作を追加するための通常のメソッドを作成できます。たとえば、以下では税金を加算した合計価格を計算するメソッドを追加しています:

public class MenuItem { public String name; public double price; // コンストラクタ public MenuItem(String name, double price) { this.name = name; this.price = price; } // 税金を加算するメソッド public double getPriceAfterTax() { double taxRate = 0.08; // 8%の税率 return price + (price * taxRate); } }

これで、税金を含む合計価格を計算できます:

public class Main { public static void main(String[] args) { MenuItem burger = new MenuItem("Burger", 3.5); System.out.println("Price after tax: $" + burger.getPriceAfterTax()); } }

出力:

Price after tax: $3.78

カプセル化

クラスの目的は、オブジェクトを作成するための設計図を提供することです。これらのオブジェクトは、他のスクリプトやプログラムによって使用されます。たとえば、当社のMenuItemオブジェクトは、ユーザーインターフェースによって、名前、価格、画像が画面上に表示されます。

このため、インスタンスが意図した方法でのみ使用されるようにクラスを設計する必要があります。現時点では、MenuItemクラスは非常に基本的でエラーが発生しやすいです。人々は、価格がマイナスのリンゴパイや100万ドルのサンドイッチなど、ばかげた属性を持つオブジェクトを作成する可能性があります:

// Main.java内 MenuItem applePie = new MenuItem("Apple Pie", -5.99); // マイナスの価格! MenuItem sandwich = new MenuItem("Sandwich", 1000000); // 過剰な高額 System.out.println("Apple pie price: $" + applePie.price); System.out.println("Sandwich price: $" + sandwich.price);

クラスを作成した後の最初の作業は、属性を保護して作成およびアクセス方法を制限することです。まず、priceには正の値のみを許可し、誤って非常に高価なアイテムが表示されるのを防ぐために最大値を設定したいと考えています。

Javaは、setterメソッドを使用してこれを実現できます:

public class MenuItem { private String name; private double price; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { this.name = name; setPrice(price); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

上記のコードブロックで新しい点を見てみましょう:

1. 属性をプライベートにするために、private キーワードを追加しました。これにより、それらはMenuItemクラス内でのみアクセスできるようになります。カプセル化はこの重要なステップから始まります。

2. 新しい定数 MAX_PRICE を追加しました。これは次のようになります:

  • プライベート (クラス内でのみアクセス可能)
  • 静的(すべてのインスタンスで共有)
  • 最終的(初期化後に変更不可)
  • $100.0を合理的な最大価格として設定

3. setPrice() メソッドを追加しました:

  • 価格パラメータを受け取ります
  • 価格が負でないことを検証します
  • 価格がMAX_PRICEを超えないことを検証します
  • 検証に失敗した場合、説明メッセージ付きのIllegalArgumentExceptionをスローします
  • すべての検証が通った場合のみ価格を設定します

4. コンストラクタを修正して setPrice() を使用して価格を直接割り当てるのではなく、オブジェクトの作成時に価格の検証が行われるようにしました。

私たちは、良いオブジェクト指向設計の基本的な柱の1つを実装しました — カプセル化。このパラダイムはデータの隠蔽とオブジェクト属性への制御されたアクセスを強制し、内部の実装の詳細が外部からの干渉から保護され、よく定義されたインターフェースを通じてのみ変更できるようにします。

これを実証するために、名前属性にカプセル化を適用しましょう。ラテ、カプチーノ、エスプレッソ、アメリカーノ、モカだけを提供するコーヒーショップを想像してください。

したがって、メニューアイテム名はこのリスト内のアイテムのみにする必要があります。これをコードでどのように強制できるかを以下に示します:

// クラスの残りの部分 ... private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private String name; public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); }

上記のコードはコーヒーショップのメニューアイテムの名前の検証を実装しています。以下で詳細に説明します:

1. まず、許可されている飲み物の名前であるラテ、カプチーノ、エスプレッソ、アメリカーノ、モカを含むプライベートで静的な最終配列VALID_NAMESを定義します。この配列は:

  • private:クラス内でのみアクセス可能
  • static:すべてのインスタンスで共有
  • 最終: 初期化後に変更できない

2. ドリンク名を格納するためのプライベートなString名前フィールドを宣言します

3. setName()メソッドは検証ロジックを実装します:

  • String名前パラメータを取ります
  • 比較を大文字と小文字を区別しないようにするために、それを小文字に変換します
  • VALID_NAMES配列をループします
  • 一致が見つかった場合、名前を設定して返します
  • 一致が見つからない場合は、すべての有効なオプションをリストアップした説明メッセージとともにIllegalArgumentExceptionをスローします

これまでのクラス全体は次のとおりです:

public class MenuItem { private String name; private double price; private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"}; private static final double MAX_PRICE = 100.0; public MenuItem(String name, double price) { setName(name); setPrice(price); } public void setName(String name) { String lowercaseName = name.toLowerCase(); for (String validName : VALID_NAMES) { if (validName.equals(lowercaseName)) { this.name = name; return; } } throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES)); } public void setPrice(double price) { if (price < 0) { throw new IllegalArgumentException("Price cannot be negative"); } if (price > MAX_PRICE) { throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE); } this.price = price; } }

属性の作成方法を保護した後、アクセス方法も保護したいと考えています。これはgetterメソッドを使用して行われます:

public class MenuItem { // ここにコードの残りがあります ... public String getName() { return name; } public double getPrice() { return price; } }

Getterメソッドはクラスのプライベート属性への制御されたアクセスを提供します。直接属性へのアクセスの問題を解決し、望ましくない変更を防ぎ、カプセル化を壊すことを防ぎます。

例えば、ゲッターを使用しない場合、属性に直接アクセスすることがあります:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.name; // 属性への直接アクセス item.name = "INVALID"; // 検証をバイパスして直接変更することができます

ゲッターを使用することで、適切なアクセスを強制します:

MenuItem item = new MenuItem("Latte", 4.99); String name = item.getName(); // ゲッターを通じた制御されたアクセス // item.name = "INVALID"; // 許可されていません - setName()を使用する必要があります。これは検証を行います

このカプセル化:

  1. 無効な変更を防ぐことでデータの整合性を保護します
  2. クラスを使用するコードに影響を与えずに内部実装を変更できるようにします
  3. 追加のロジックが必要な場合にも対応できる単一のアクセス点を提供します
  4. コードをより保守しやすくし、バグが発生しにくくします

継承

私たちのクラスは良くなってきていますが、いくつかの問題があります。例えば、多くの種類の料理や飲み物を提供する大きなレストランにとって、このクラスは十分に柔軟ではありません。

異なる種類の食べ物を追加しようとすると、いくつかの課題に直面します。一部の料理はテイクアウト用に準備できますが、他の料理は即時消費が必要です。メニュー項目には異なる価格や割引があるかもしれません。料理は温度管理や特別な保存が必要な場合があります。飲み物はホットまたはコールドで、カスタマイズ可能な材料が含まれることがあります。アイテムにはアレルゲン情報やポーションの選択肢が必要な場合があります。現在のシステムはこれらの異なる要件に対応していません。

継承はこれらすべての問題に対する優れた解決策を提供します。共通の属性を持つ基本的なMenuItemクラスを定義し、それを継承してユニークな機能を追加しながら、メニューアイテムの専門バージョンを作成できます。

たとえば、温度オプションを持つ飲み物用のDrinkクラス、即座の消費が必要なアイテム用のFoodクラス、特別な保存方法が必要なアイテム用のDessertクラスなどがあります。これらはすべて、コアメニューアイテムの機能を継承しています。

クラスの拡張

それらのアイデアを実装してみましょう。Drink:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { this.name = name; this.price = price; this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

親から継承する子クラスを定義するには、子クラス名の後に親クラスに続いてextendsキーワードを使用します。クラス定義の後に、この子クラスが持つ新しい属性を定義し、コンストラクタを実装します。

ただし、nameおよびpriceを初期化する必要があることに注意してくださいisCold。これは理想的ではありません。親クラスには数百もの属性があるかもしれません。また、上記のコードはコンパイル時にエラーをスローします。これは親クラスの属性を初期化する正しい方法ではないからです。正しい方法はsuperキーワードを使用することです:

public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } public boolean getIsCold() { return isCold; } public void setIsCold(boolean isCold) { this.isCold = isCold; } }

superキーワードは親クラスのコンストラクタを呼び出すために使用されます。この場合、super(name, price)MenuItemクラスのコンストラクタを呼び出してこれらの属性を初期化し、コードの重複を回避します。新しいisCold属性を初期化する必要がありますDrinkクラスに固有の属性のみ。

キーワードは非常に柔軟です。子クラスの任意の場所で親クラスを参照するために使用できます。たとえば、親メソッドを呼び出すには、super.methodName() を使用します。一方、属性には super.attributeName が使われます。

メソッドのオーバーライド

さて、例として、クラスに新しいメソッドを追加して税込みの合計金額を計算したいとします。異なるメニューアイテムには異なる税率が適用される場合(たとえば、調理済み食品とパッケージされた飲み物)、各子クラスで特定の税金計算を実装するためにメソッドのオーバーライドを使用し、親クラスで共通のメソッド名を維持します。

これは次のようになります:

public class MenuItem { // MenuItemクラスの残り public double calculateTotalPrice() { // デフォルトの税率は10% return price * 1.10; } } public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { // 食品には15%の税金があります return super.getPrice() * 1.15; } } public class Drink extends MenuItem { private boolean isCold; public Drink(String name, double price, boolean isCold) { super(name, price); this.isCold = isCold; } @Override public double calculateTotalPrice() { // 飲料には8%の税金があります return super.getPrice() * 1.08; } }

この例では、メソッドオーバーライドにより、各サブクラスが独自の calculateTotalPrice()の実装を提供できます:

ベース MenuItem クラスはデフォルトの税計算を10%と定義しています。

いつ FoodDrink クラスが MenuItem を拡張する場合、彼らは独自の税率を実装するためにこのメソッドをオーバーライドします:

  • 食品アイテムには15%の高い税率があります
  • 飲み物は8%の低い税率が適用されます

The @Overrideアノテーションは、これらのメソッドが親クラスのメソッドをオーバーライドしていることを明示的に示すために使用されます。これにより、メソッドのシグネチャが親クラスと一致しない場合にエラーを検出するのに役立ちます。

各サブクラスは、super.getPrice()を使用して親クラスの価格にアクセスできます。これは、オーバーライドされたメソッドが親クラスの機能を利用しながら独自の動作を追加できることを示しています。

要するに、メソッドのオーバーライドは、サブクラスが親クラスで定義されたメソッドの実装を提供できるようにする継承の重要な部分であり、同じメソッドシグネチャを保持しながらより具体的な振る舞いを可能にします。

抽象クラス

私たちのMenuItemクラス階層は機能していますが、問題があります:誰でも単なるMenuItemオブジェクトを作成できるべきでしょうか?結局のところ、私たちのレストランでは、すべてのメニューアイテムが食べ物または飲み物のいずれかであり、「一般的なメニューアイテム」というものは存在しません。

これを防ぐことができます。MenuItemを抽象クラスにすることで。抽象クラスは基本的な設計図を提供します – 継承のための親クラスとしてのみ使用され、直接インスタンス化されることはありません。

抽象的なMenuItem を作成するには、そのアクセス修飾子の後に abstract キーワードを追加します:

public abstract class MenuItem { private String name; private double price; public MenuItem(String name, double price) { setName(name); setPrice(price); } // 既存のgetter/setterはそのままです // このメソッドを抽象化する - すべてのサブクラスはそれを実装する必要があります public abstract double calculateTotalPrice(); }

抽象クラスは、上記のように calculateTotalPrice() のような抽象メソッドを持つこともできます。これらの抽象メソッドは、サブクラスに実装を提供することを強制する契約として機能します。言い換えれば、抽象クラス内の任意の抽象メソッドは、子クラスによって実装されなければなりません。

したがって、次のように書き直しましょう:食べ物および飲み物をこれらの変更を考慮して書き直します:

public class Food extends MenuItem { private boolean isVegetarian; public Food(String name, double price, boolean isVegetarian) { super(name, price); this.isVegetarian = isVegetarian; } @Override public double calculateTotalPrice() { return getPrice() * 1.15; // 15% 税 } } public class Drink extends MenuItem { private boolean hasCaffeine; public Drink(String name, double price, boolean hasCaffeine) { super(name, price); this.hasCaffeine = hasCaffeine; } @Override public double calculateTotalPrice() { return getPrice() * 1.10; // 10% 税 } }

このメニューシステムの実装を通じて、抽象化と継承がどのように協力して柔軟で保守しやすいコードを作成し、異なるビジネス要件に簡単に適応できるかを見てきました。

結論

今日は、Javaがオブジェクト指向プログラミング言語としてどのようなことができるのかを垣間見ました。クラス、オブジェクト、そしてOOPの重要な柱であるカプセル化、継承、抽象化をレストランのメニューシステムを通じて基本的な部分をカバーしました。

このシステムを商用準備が整った状態にするためには、インターフェース(抽象化の一部)、ポリモーフィズム、OOPデザインパターンなど、まだ学ぶべきことがたくさんあります。これらの概念について詳しく学ぶには、私たちの JavaにおけるOOP入門コースを参照してください。

Javaの知識をテストしたい場合は、Java インタビューの質問記事にあるいくつかの質問に答えてみてください。

Source:
https://www.datacamp.com/tutorial/oop-in-java