Javaのクラスについて


Javaのクラスには次の役割があります。

  • オブジェクトのひな型
  • 責務の分離
  • 詳細を隠蔽する抽象化

🎉 オブジェクトのライフサイクル管理

オブジェクトのライフサイクルを適切に管理する次の技法があります。

  • 変数のスコープにちゅうして、不要に長い寿命のオブジェクトを減らします
  • 寿命の短いオブジェクトと、寿命の長いオブジェクトを適切に分離します
  • ファクトリパターンやDI(Dependency Injection)など、オブジェクト生成向けの技法を活用します
  • オブジェクトのライフサイクル管理をフレームワークなどの層に隠蔽する

🎂 クラスの設計指針

  • 共通したコードをまとめる
  • 変わりやすい部分と変わりにくい部分を分ける
    • (テンプレートメソッドパターン、ストラテジパターン、パラメータ化)
  • クラス間の依存を減らす。依存関係を単純化する
  • 外部から呼び出しやすいコードにする

🐮 コンストラクタ

コンストラクタはオブジェクトの生成時によばれる処理です。

class Book {
private String title;
private int price;
Book(String title) { //フィールドとパラメータは同名にするのが定石
this(title, 1000); //下のコンストラクタを呼ぶ
}
Book(String title, int price) {
this.title = title;
this.price = price;
}
}

コンストラクタの意義はオブジェクトの生成時に適切な初期化を強制できる点です。
superを使って親クラスのコンストラクタを呼び出すことも可能です。

コンストラクタを書かなかったクラスには暗黙にデフォルトコンストラクタが生成されます。
1つでもコンストラクタを書き足すと自動的にデフォルトコンストラクタは消滅します。

コンストラクタのルール

コンストラクタのルールは次のとおりです。

  • メソッド名をクラス名と同じにすること
  • 戻り値型は記述できない
  • newと一緒にしか使えない(インスタンス生成時以外は呼び出せない)
  • thisを使ってオーバーロードされたコンストラクタを呼び出す場合はその前に別の処理はできない

🐠 複数クラスの共通処理

複数クラスの共通処理をくくりだすには次の方法があります。

  • 共通部をクラスとして、そのオブジェクト参照をフィールドにもち処理を委譲します
  • 共通部を基底クラスとして、クラスを継承して階層管理します

出典:改訂2版 パーフェクトJava

  • 単純に共通処理をくくりだすだけなら、委譲を使ってください
  • 上位型は下位型の汎用型となるような型定義の共通部なら、拡張継承にしてください

👽 拡張継承によるクラスの構成要素の変更

クラスの構成要素が、継承した先でクラスの構成要素を変更できるかを紹介します。

名称 追加 削除 変更
フィールド 可能 不可 隠蔽
クラスフィールド 可能 不可 隠蔽
メソッド 可能 不可 オーバーライド
クラスメソッド 可能 不可 不可
内部クラス 可能 不可 隠蔽
staticなネストしたクラス 可能 不可 隠蔽
staticなネストしたインターフェース 可能 不可 隠蔽
コンストラクタ 可能 不可 不可
初期化ブロック 可能 不可 不可
static初期化ブロック 可能 不可 不可

🐰 フィールド変数の隠蔽

継承した先のクラスで継承元と同名のフィールドを宣言すると、継承元のフィールドを隠蔽します。

class Parent {
final String s = "123";
}
public class Child extends Parent {
final String s = "456"; // Parentのフィールドを隠蔽
public static void main(String... args) {
Parent parent = new Parent();
System.out.println(parent.s); //=> 123
Child child = new Child();
System.out.println(child.s); //=> 456
Parent parent2 = new Child(); // 呼ばれる変数の型でフィールドが決定
System.out.println(parent2.s); //=> 123
}
}

一般的に変数の隠蔽はコードの可読性を落とすため、避けるべきです。

🍮 オーバーライド

オーバーライドとは親クラスのメソッドを子クラスで定義をし直す(上書きする)ことです。

class Book {
private String title = Book title;
public void printTitle() {
System.out.println("Book.printTitle(): " + title);
}
}
public class ProgrammingBook extends Book {
private String title = Programming book title;
@Override // コンパイルエラーになるのでオーバーライドに失敗すると気付ける
public void printTitle() {
System.out.println(ProgrammingBook.printTitle(): + title);
}
public static void main(String[] args) {
Book book = new Book();
book.printTitle(); //=> Book.printTitle(): Book title
ProgrammingBook programmingBook = new ProgrammingBook();
programmingBook.printTitle(); //=> ProgrammingBook.printTitle(): Programming book title
Book book2 = new ProgrammingBook();
book2.printTitle();
}
}

隠蔽では変数の型で呼ばれるフィールドが変わりましたが、オーバーライドでは呼ばれるオブジェクトの型でメソッドが決まります。

オーバーライドのルール

オーバーライドには、次のようなルールがあります。

  1. オーバーライドは、メソッド名、引数リストがまったく同じメソッドをサブクラスで定義すること
  2. 返り値の型は、スーパークラスと同じものか、もしくはその返り値のサブクラスであれば利用可
  3. アクセス制御は「スーパークラスと同じものかそれよりも公開範囲が広ければ利用可

オーバーライドにおける例外処理(throws)について

メソッドをオーバーライドする際に例外処理(throws)を記述する際のルールについてです。

  • throwsには、スーパークラスのメソッドがthrowsに指定した例外クラス(サブクラス)が指定できる
  • throwsには、RuntimeExceptionクラス(サブクラス)も指定可能
  • スーパークラスのメソッドにthrowsがあっても、throwsをしてしなくてもいい

🍣 super参照

オーバーライドされた元メソッドがprivateでなければ、superを通じて元メソッドを呼び出すことができます。

class Book {
final String title = Book title;
public void printTitle() {
System.out.println(book.title: + this.title);
}
}
class CookBook extends Book {
final String title = Cook book title;
@Override
public void printTitle() {
super.printTitle();
System.out.println(CookBook.printTitle: + super.title);
}
public static void main(String[] args) {
CookBook cookBook = new CookBook();
cookBook.printTitle(); //=> book title: Cook book title
}
}

🚕 抽象クラス

abstract修飾子をつけたクラスを抽象クラスと呼び、インスタンス化ができません。
抽象クラスになる条件は次のとおりです。

  • 抽象メソッドをもつクラス
  • interfaceを継承して、interfaceのすべてのメソッドの実装をもたないクラス
  • 抽象クラスを継承して、すべての抽象メソッドの実装をもたないクラス

🎳 ユーティリティクラス

オブジェクト生成を禁止し、クラスの直接利用のみを行うクラスを「ユーティリティクラス」と呼びます。

public final class Math { // final で無用な継承を禁止
private Math() {} // インスタンス化を禁止
public static final double E = 2.7182818284590452354; // staticをつける
}

意味のまとまりのあるメソッドを一ヵ所にまとめることができる点が、ユーティリティクラスの利点です。

😼 匿名クラス

匿名クラスのオブジェクトはメソッドの引数として引き渡すために生成します。

public static void main(String[] args) {
List<String> list = Arrays.asList("xyz", "abc", "defghi");
// sortメソッドの第2引数に匿名オブジェクトを渡す
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
}

🐝 クラス設計に関する用語

DTO:データを運ぶクラス

DTO(Data Transfer Object)クラスは、データを運ぶ役割を分離してモジュール間の関係を単純化するために使います。

DAO:DBとの境界を担うクラス

DAO(Data Access Object)クラスは、データベースとの境界を担うクラスを作ることで役割を分離します。

🐯 補足:JVMがメソッドを探す流れ

JVMが実行すべきメソッドを探すために使う方法は次の3つです。

  • 参照 (どのインスタンスのメソッドなのか)
  • クラス名 (どのクラスに定義されているstaticなメソッドなのか)
  • シグネチャ (メソッド名と引数のセット、オーバーロードがあるため)

🗻 補足:クラス継承で引き継がないもの

クラス継承で引き継がないのは「コンストラクタ」と「privateなフィールドやメソッド」です。

🎃 補足:フィールドはオーバーライドしない

  • Javaではメソッドとフィールドは明確に区別されており、オーバーライドできない
  • 親クラスと小クラスで同名フィールドを定義すると変数の型によってどちらかのフィールドが参照
  • アクセスできないフィールドが発生しかねず、バグの温床になるので絶対に行わない

🐹 補足:継承関係のクラスの同名フィールドの優先順位

次のようなスーパークラスとサブクラスがあったとします。


出典:徹底攻略Java SE 8 Silver問題集

class A {
String val = "A";
void print() {
System.out.print(val);
}
}
class B extends A {
String val = "B";
}
public class Main {
public static void main(String[] args) {
A a = new A();
A b = new B();
System.out.print(a.val); // => A
System.out.print(b.val); // => A
a.print(); // => A
b.print(); // => A
}
}

この場合どちらのフィールドが使われるかは次のルールに従います。

  • フィールドを参照した場合は、変数の型で宣言されたほうを使う
  • メソッドを呼び出した場合は、メソッド内の指示に従う

🗽 継承関係のあるクラスのコンストラクタ

継承関係にあるクラスのコンストラクタについてです。次のような継承関係があるとします。

class A {
public A() {
System.out.println("A");
}
}
class B extends A {
public B() {
super(); // コンパイラが自動で埋め込む
System.out.println("B");
}
}

この場合、

  • スーパークラスのインスタンスがもつコンストラクタが先に実行されなければいけない
  • サブクラスのコンストラクタには、スーパークラスのコンストラクタを呼び出す「super();」がコンパイラによって追加

🐡 参考リンク

📚 おすすめの書籍

🖥 サーバについて

このブログでは「Cloud Garage」さんのDev Assist Program(開発者向けインスタンス無償提供制度)でお借りしたサーバで技術検証しています。 Dev Assist Programは、開発者や開発コミュニティ、スタートアップ企業の方が1GBメモリのインスタンス3台を1年間無料で借りれる心強い制度です!(有償でも1,480円/月と格安)