目次


Java プログラミング入門、第 2 回

実際のアプリケーションに対応するための構成体

より高度な Java 言語機能

Comments

コンテンツシリーズ

このコンテンツは全2シリーズのパート#です: Java プログラミング入門、第 2 回

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Java プログラミング入門、第 2 回

このシリーズの続きに乞うご期待。

はじめに、このチュートリアルに期待すべきこと、そしてチュートリアルを最大限に活用する方法を確認してください。

このチュートリアルについて

この全 2 回からなるチュートリアル「Java プログラミング入門」は、Java テクノロジーに馴染みのないソフトウェア開発者を対象としています。第 1 回と第 2 回の両方に取り組むことで、オブジェクト指向プログラミング (OOP) を理解し、Java 言語およびプラットフォームを使用して実際のアプリケーション開発に取り組めるようになります。

チュートリアル「Java プログラミング入門」の後半となるこの第 2 回では、第 1 回で取り上げた機能よりも高度な Java 言語機能を紹介します。

この記事の目的

Java 言語はほぼあらゆるプログラミング・タスクを達成できるほど、成熟した高度な言語です。このチュートリアルでは、複雑なプログラミング・シナリオに対処しなければならないときに必要となる、Java 言語の機能を紹介します。具体的には、以下の機能です。

  • 例外処理
  • 継承と抽象化
  • インターフェース
  • ネストしたクラス
  • 正規表現
  • ジェネリクス
  • enum
  • 入出力
  • シリアライズ

前提条件

このチュートリアルの内容は、Java 言語の高度な機能に馴染みのない新米の Java プログラマーを対象としています。このチュートリアルでは、読者がすでに「Java プログラミング入門、第 1 回: Java 言語の基本」に取り組んでいて、以下の前提条件を満たしていることを前提とします。

  • Java プラットフォーム上での OOP の基本を理解していること
  • チュートリアルの演習を実行できる開発環境がセットアップされていること
  • プログラミング・プロジェクトにすでに取り掛かって、第 2 回で引き続き開発を進められるようになっていること

システム要件

このチュートリアルの演習に従うには、以下のソフトウェアをインストールして、開発環境をセットアップする必要があります。

  • Oracle の JDK 8
  • Eclipse IDE for Java Developers

上記のダウンロードおよびインストール手順は、第 1 回で説明しています。

推奨されるシステム構成は以下のとおりです。

  • Java SE 8 をサポートする、メモリー容量 2GB 以上のシステム。Java 8 は、Linux、Windows、Solaris、および Mac OS X でサポートされています。
  • ソフトウェア・コンポーネントとサンプル・コードをインストールするための 200MB 以上のディスク・スペース。

オブジェクトを使った次のステップ

このチュートリアルの第 1 回を完了した時点で、Person クラスはかなり有用なものになりましたが、それでもまだ十分とは言えません。今回は Person のようなクラスを拡張する手法を説明します。その出発点として、はじめに以下の手法を取り上げます。

  • メソッドの多重定義
  • メソッドのオーバーライド
  • 2 つのオブジェクトの比較
  • クラス変数とクラス・メソッドの使用

まずは、Person を拡張するために、このクラスのメソッドのうち 1 つを多重定義します。

メソッドを多重定義する

名前は同じでも、使用する引数が異なる (つまり、パラメーターの数または型が異なる) 2 つのメソッドを作成すると、それはメソッドを「多重定義」したことになります。実行時に JRE は渡されたパラメーターに基づいて、多重定義されたメソッドのどのバリエーションを呼び出すのかを決定します。

例えば、Person の現在の状態についての監査を出力するために 2 つのメソッドが必要だとします。これらのメソッドには、両方とも printAudit() という名前を付けます。Eclipse のエディター・ビューで、リスト 1 の多重定義されたメソッドを Person クラスに貼り付けてください。

リスト 1. printAudit(): 多重定義されたメソッド
public void printAudit(StringBuilder buffer) {
   buffer.append("Name=");
   buffer.append(getName());
   buffer.append(",");
   buffer.append("Age=");
   buffer.append(getAge());
   buffer.append(",");
   buffer.append("Height=");
   buffer.append(getHeight());
   buffer.append(",");
   buffer.append("Weight=");
   buffer.append(getWeight());
   buffer.append(",");
   buffer.append("EyeColor=");
   buffer.append(getEyeColor());
   buffer.append(",");
   buffer.append("Gender=");
   buffer.append(getGender());
}

public void printAudit(Logger l) {
   StringBuilder sb = new StringBuilder();
   printAudit(sb);
   l.info(sb.toString());
}

これで、printAudit() の 2 つの多重定義バージョンができました。しかも、一方のバージョンではもう一方のバージョンを使っています。2 つのバージョンを用意することで、呼び出し側にクラスの監査方法の選択肢を与えることになります。Java ランタイムは渡されたパラメーターに応じて、正しいメソッドを呼び出します。

多重定義されたメソッドを使用するときは、2 つの重要な規則があることに注意してください。

  • 戻り値の型を変更するだけでは、メソッドを多重定義できません。
  • 同じ名前の 2 つのメソッドで使用する一連のパラメーターをすべて同じにすることはできません。

上記の規則に違反すると、コンパイラーがエラーを返します。

メソッドをオーバーライドする

サブクラスに、その親クラスのいずれかに定義されているメソッドとは異なる独自の実装を持たせることを、「メソッドをオーバーライドする」と表現します。メソッドのオーバーライドがどのように役立つのかを理解するには、Employee クラスに対して何らかの処理を行う必要があります。このクラスをセットアップしてから、メソッドのオーバーライドがどのような場合に役立つかを説明します。

Employee: Person のサブクラス

第 1 回で説明したように、Person のサブクラス (または子) として Employee クラスを作成すれば、Employee クラスに納税者番号、従業員番号、雇用日、給与などの属性を追加することができます。

Employee クラスを宣言するには、Eclipse 内で com.makotojava.intro パッケージを右クリックして「New (新規)」 > 「Class... (クラス...)」を選択します。これによって、「New Java Class (新規 Java クラス)」ダイアログ・ボックスが開きます (図 1 を参照)。

図 1. 「New Java Class (新規 Java クラス)」ダイアログ・ボックス
プロジェクト・エクスプローラー内で開かれた「New Java Class (新規 Java クラス)」ダイアログ・ボックスのスクリーンショット
プロジェクト・エクスプローラー内で開かれた「New Java Class (新規 Java クラス)」ダイアログ・ボックスのスクリーンショット

クラスの名前として Employee と入力し、そのスーパークラスとして Person と入力します。これで「Finish (完了)」をクリックすると、編集ウィンドウに Employee クラスのコードが表示されます。

明示的にコンストラクターを宣言する必要はないのですが、ここではスーパークラスの両方のコンストラクターを実装します。Employee クラスの編集ウィンドウがフォーカスされた状態で、「Source (ソース)」 > 「Generate Constructors from Superclass... (スーパークラスからコンストラクターを生成...)」を選択します。表示される「Generate Constructors from Superclass (スーパークラスからのコンストラクターの生成)」ダイアログ・ボックスで、両方のコンストラクターを選択して「OK」をクリックします。

図 2. 「Generate Constructors from Superclass (スーパークラスからのコンストラクターの生成)」ダイアログ・ボックス
コンストラクターを作成するためのプロジェクト・パスを示すスクリーンショット
コンストラクターを作成するためのプロジェクト・パスを示すスクリーンショット

Eclipse によって自動的にコンストラクターが生成されます。これで、リスト 2 に示すような Employee クラスがセットアップされました。

リスト 2. Employee クラス
package com.makotojava.intro;

public class Employee extends Person {

  public Employee() {
    super();
    // TODO Auto-generated constructor stub
  }
  
  public Employee(String name, int age, int height, int weight,
  String eyeColor, String gender) {
    super(name, age, height, weight, eyeColor, gender);
    // TODO Auto-generated constructor stub
  }

}

Person の子としての Employee

Employee は親クラス Person の属性と動作を継承しますが、それとは別に、Employee 固有の属性を追加します (リスト 3 の行 7 から行 9 を参照)。

リスト 3. Person の属性を継承する Employee クラス
package com.makotojava.intro;

import java.math.BigDecimal;

public class Employee extends Person {

  private String taxpayerIdentificationNumber;
  private String employeeNumber;
  private BigDecimal salary;

  public Employee() {
    super();
  }
  public String getTaxpayerIdentificationNumber() {
    return taxpayerIdentificationNumber;
  }
  public void setTaxpayerIdentificationNumber(String taxpayerIdentificationNumber) {
    this.taxpayerIdentificationNumber = taxpayerIdentificationNumber;
  }

  // Other getter/setters...
}

新しい属性の getter と setter を生成することも忘れないでください。その方法は、第 1 回の「初めての Java クラスを作成する」のセクションで説明しています。

printAudit() メソッドをオーバーライドする

次は、printAudit() メソッド (リスト 1 を参照) をオーバーライドします。このメソッドは、Person インスタンスの現在の状態をフォーマット化するために使用されているもので、この動作を EmployeePerson から継承しています。Employee をインスタンス化する場合、Employee クラスの属性を設定して printAudit() の多重定義のいずれかを呼び出すと、その呼び出しは成功します。けれども、これによって生成される監査は Employee を完全に表現することにはなりません。Person には Employee に固有の属性に関する情報がないため、printAudit() メソッドでそれらの属性をフォーマット化できないからです。

Employee に固有の属性をフォーマット化するための解決方法として、printAudit() の多重定義のうち、StringBuilder をパラメーターとして取るほうをオーバーライドして、Employee 固有の属性を出力するためのコードを追加します

Employee をエディター・ウィンドウ内で開くか、プロジェクト・エクスプローラー内で選択してから、「Source (ソース)」 > 「Override/Implement Methods... (メソッドをオーバーライド/実装..)」を選択します。「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックス (図 3 を参照) 内で、StringBuilder をパラメーターとして取る printAudit() の多重定義を選択してから「OK」をクリックします。

図 3. 「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックス
「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックスのスクリーンショット
「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックスのスクリーンショット

Eclipse によって自動的にメソッド・スタブが生成されます。残りの部分は、以下のように埋めることができます。

@Override
public void printAudit(StringBuilder buffer) {
  // Call the superclass version of this method first to get its attribute values
  super.printAudit(buffer);

  // Now format this instance's values
  buffer.append("TaxpayerIdentificationNumber=");
  buffer.append(getTaxpayerIdentificationNumber());
  buffer.append(","); buffer.append("EmployeeNumber=");
  buffer.append(getEmployeeNumber());
  buffer.append(","); buffer.append("Salary=");
  buffer.append(getSalary().setScale(2).toPlainString());
}

super.printAudit() の呼び出しに注目してください。ここでは、printAudit() の動作を示すように (Person) スーパークラスに依頼した後、このメソッドに Employee 用の printAudit() の動作を加えています。

super.printAudit() を最初に呼び出さなければならないわけではありませんが、スーパークラスの属性を先に出力したほうが得策だと思ったのです。実際のところ、super.printAudit() を呼び出す必要さえありません。呼び出さないのであれば、Person の属性を Employee.printAudit() メソッド内でフォーマット化する必要があります。そうしないと、それらの属性は監査の出力に含まれなくなります。

オブジェクトを比較する

Java 言語でオブジェクトを比較するには、以下の 2 つの手段があります。

  • == 演算子
  • equals() メソッド

== を使用してオブジェクトを比較する

== 構文は、オブジェクト間の等価性を比較します。例えば、a == b とすると、ab の値が同じである場合にだけ true が返されます。オブジェクトを比較する場合は、2 つのオブジェクトが「同じオブジェクト・インスタンス」を参照していれば、true が返されます。プリミティブ型の場合は、「値が同一」であれば true が返されます。

例えば、Employee の JUnit テストを生成するとします (その方法は、第 1 回の「初めての Java クラスを作成する」のセクションで説明しました)。リスト 4 に Employee の JUnit テストを記載します。

リスト 4. == を使用したオブジェクトの比較
public class EmployeeTest {
  @Test
  public void test() {
    int int1 = 1;
    int int2 = 1;
    Logger l = Logger.getLogger(EmployeeTest.class.getName());
    
    l.info("Q: int1 == int2?           A: " + (int1 == int2));
    Integer integer1 = Integer.valueOf(int1);
    Integer integer2 = Integer.valueOf(int2);
    l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));
    integer1 = new Integer(int1);
    integer2 = new Integer(int2);
    l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));
    Employee employee1 = new Employee();
    Employee employee2 = new Employee();
    l.info("Q: Employee1 == Employee2? A: " + (employee1 == employee2));
  }
}

Eclipse 内でリスト 4 のコードを実行すると (「Project Explorer (プロジェクト・エクスプローラー)」ビュー内で Employee を選択してから、「Run As (実行)」 > 「JUnit Test (JUnit テスト)」を選択)、以下の出力が生成されます。

Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: int1 == int2?           A: true
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Integer1 == Integer2?   A: true
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Integer1 == Integer2?   A: false
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Employee1 == Employee2? A: false

リスト 4 の最初のケースでは、両方のプリミティブ型の値が同じであるため、== 演算子は true を返します。2 番目のケースでは、両方の Integer オブジェクトが同じインスタンスを参照しているため、== 演算子は同じく true を返します。3 番目のケースでは、両方の Integer オブジェクトがラップしている値は同じですが、integer1integer2 は異なるオブジェクトを参照するため、== 演算子は false を返します。== 演算子は、「オブジェクトのインスタンスが同一」であるかどうかのテストだと考えてください。

equals() を使用してオブジェクトを比較する

equals() メソッドはすべての Java オブジェクトが継承する java.lang.Object のインスタンス・メソッドとして定義されているため、何もしなくても、Java 言語のすべてのオブジェクトに備わっています。

equals() を呼び出すには、以下のようにします。

a.equals(b);

上記の文は、オブジェクト b の参照を渡して、オブジェクトの equals() メソッドを呼び出します。デフォルトでは、Java プログラムは == 構文を使って 2 つのオブジェクトが同じであるかどうかをチェックするだけです。一方、equals() はメソッドであるため、オーバーライドすることができます。リスト 4 の JUnit テスト・ケースと、equals() を使って 2 つのオブジェクトを比較するリスト 5 のテスト・ケース (anotherTest() と名付けました) を見比べてください。

リスト 5. equals() を使用したオブジェクトの比較
@Test
public void anotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
  Integer integer1 = Integer.valueOf(1);
  Integer integer2 = Integer.valueOf(1);
  l.info("Q: integer1 == integer2 ? A: " + (integer1 == integer2));
  l.info("Q: integer1.equals(integer2) ? A: " + integer1.equals(integer2));
  integer1 = new Integer(integer1);
  integer2 = new Integer(integer2);
  l.info("Q: integer1 == integer2 ? A: " + (integer1 == integer2));
  l.info("Q: integer1.equals(integer2) ? A: " + integer1.equals(integer2));
  Employee employee1 = new Employee();
  Employee employee2 = new Employee();
  l.info("Q: employee1 == employee2 ? A: " + (employee1 == employee2));
  l.info("Q: employee1.equals(employee2) ? A : " + employee1.equals(employee2));
}

リスト 5 のコードを実行すると、以下の出力が生成されます。

Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1 == integer2 ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1.equals(integer2) ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1 == integer2 ? A: false
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1.equals(integer2) ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: employee1 == employee2 ? A: false
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: employee1.equals(employee2) ? A : false

Integer の比較に関する注意事項

==true を返すのであれば、リスト 5 でも当然、Integerequals() メソッドは true を返すはずです。けれども、2 番目のケースを注目してください。ここでは、別個のオブジェクトを作成して、どちらのオブジェクトでも値 1 をラップしています。この場合、integer1integer2 は異なるオブジェクトを参照しているため、== を使用した場合は false を返しますが、equals()true を返します。

JDK の作成者たちは、Integer については equals() にデフォルトとは異なる意味を持たせることにしたのです (前述のとおり、デフォルトはオブジェクトの参照を比較して、それらが同じオブジェクトを参照しているかどうかをチェックします)。Integer の場合、基礎となる (ボックス化した) int の値が同じであれば、equals()true を返します。

Employee の例では equals() をオーバーライドしなかったので、デフォルトの動作 (== を使用) によって期待したとおりの結果になりました。つまり、employee1employee2 が異なるオブジェクトを参照しているため、false が返されました。

作成するオブジェクトには、作成しているアプリケーションに応じて、適切な equals() の意味を定義することができます。

equals() をオーバーライドする

アプリケーションのオブジェクトに対して equals() がどのような意味を持つかを定義するには、Object.equals() のデフォルトの動作をオーバーライドします。この作業は、Eclipse 内で行うことができます。IDE のソース・ウィンドウ内で Employee がフォーカスされた状態にして、「Source (ソース)」 > 「Override/Implement Methods (メソッドをオーバーライド/実装)」を選択します。これによって、図 4 に示すダイアログ・ボックスが開きます。

図 4.「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックス
Eclipse の「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックスのスクリーンショット
Eclipse の「Override/Implement Methods (メソッドのオーバーライド/実装)」ダイアログ・ボックスのスクリーンショット

ここで実装するのは、Object.equals() スーパークラス・メソッドです。したがって、メソッドのリストで、オーバーライドまたは実装する対象となる Object を見つけて、その equals(Object) メソッドを選択してから「OK」をクリックします。これで、Eclipse によって生成された適切なコードがソース・ファイルに挿入されます。

2 つの Employee オブジェクトの状態が同じ場合、それらのオブジェクトは等しいと考えるのが当然です。つまり、2 つのオブジェクトの値、名前、年齢が同じであれば、同一のオブジェクトと見なすことができます。

equals() を自動生成する

Eclipse では、クラスに定義されたインスタンス変数 (属性) に応じて自動的に equals() メソッドを生成することができます。EmployeePerson のサブクラスなので、最初に Personequals() を生成します。それには、Eclipse の「Project Explorer (プロジェクト・エクスプローラー)」ビュー内で、Person を右クリックして「Generate hashCode() and equals() (hashCode() および equals() を生成)」を選択します。表示されるダイアログ・ボックス (図 5 を参照) 内で、「Select All (すべて選択)」をクリックして hashCode() および equals() メソッドにすべての属性を含めてから、「OK」をクリックします。

図 5. 「Generate hashCode() and equals() (hashCode() および equals() の生成)」ダイアログ・ボックス
hashCode() と equals() を生成するためのダイアログ・ボックスのスクリーンショット
hashCode() と equals() を生成するためのダイアログ・ボックスのスクリーンショット

Eclipse によって、リスト 6 に示すような equals() メソッドが生成されます。

リスト 6. Eclipse によって生成された equals() メソッド
@Override
public boolean equals(Object obj) {
  if (this == obj)
    return true;
  if (obj == null)
    return false;
  if (getClass() != obj.getClass())
    return false;
  Person other = (Person) obj;
  if (age != other.age)
    return false;
  if (eyeColor == null) {
    if (other.eyeColor != null)
      return false;
  } else if (!eyeColor.equals(other.eyeColor))
    return false;
  if (gender == null) {
    if (other.gender != null)
      return false;
  } else if (!gender.equals(other.gender))
    return false;
  if (height != other.height)
    return false;
  if (name == null) {
    if (other.name != null)
      return false;
  } else if (!name.equals(other.name))
    return false;
  if (weight != other.weight)
    return false;
  return true;
}

Eclipse が生成する equals() メソッドは複雑なように見えますが、その処理内容は単純です。equals() メソッドに渡されたオブジェクトがリスト 6 のオブジェクトと同じであれば、true が返されます。渡されたオブジェクトが null であれば (つまり、オブジェクトが欠落している場合) 、false が返されます。

次に、このメソッドは Class オブジェクト同士が同じであるかどうか (つまり、渡されたオブジェクトが Person オブジェクトであること) をチェックします。同じオブジェクトであれば、渡されたオブジェクトの各属性の値をチェックし、値ごとにその特定の Person インスタンスの状態と一致するかどうかを確認します。属性値が null の場合、equals() はできるだけ多くの属性をチェックし、それらの属性がインスタンスの状態と一致すると、2 つのオブジェクトを等しいと見なします。この動作はすべてのプログラムに必要になるというわけではありませんが、ほとんどの目的に役立ちます。

演習

ここで、2 つのガイド付き演習に取り組んで、Eclipse 内で PersonEmployee をさらに処理します。

演習 1: Employee の equals() を生成する

equals() を自動生成する」で説明した手順に従って、Employeeequals() を生成してください。equals() が生成されたら、そのメソッドに以下に記載する (yetAnotherTest() と名付けた) JUnit テスト・ケースを追加します。

@Test
public void yetAnotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
  Employee employee1 = new Employee();
  employee1.setName("J Smith");
  Employee employee2 = new Employee();
  employee2.setName("J Smith");
  l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
  l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));    
}

完成したコードを実行すると、以下の結果が出力されるはずです。

Sep 19, 2015 11:27:23 AM com.makotojava.intro.EmployeeTest yetAnotherTest
INFO: Q: employee1 == employee2?      A: false
Sep 19, 2015 11:27:23 AM com.makotojava.intro.EmployeeTest yetAnotherTest
INFO: Q: employee1.equals(employee2)? A: true

この例の場合、Name が一致するだけで、equals() は 2 つのオブジェクトが等しいと確信しています。このサンプル・コードにさらに属性を追加してみて、どのような結果になるのかを確認してください。

演習 2: toString() をオーバーライドする

このセクションの冒頭で取り上げた printAudit() メソッドを振り返ってください。このメソッドに負担がかかり過ぎていると思ったのなら、それは正解です。オブジェクトの状態を String にフォーマット化するのは極めて一般的なパターンであるため、Java 言語の設計者たちはこのパターンを Object 自体に組み込みました。それが、(当然ながら) toString() という名前のメソッドです。toString() のデフォルト実装はそれほど役に立ちませんが、すべてのオブジェクトにこのデフォルト実装が伴います。この演習では toString() をオーバーライドして、もう少し有用なものにします。

Eclipse で自動的に toString() メソッドを生成できるのではないかと考えているとしたら、その通りです。「Project Explorer (プロジェクト・エクスプローラー)」に戻って、Person クラスを右クリックし、「Source (ソース)」 > 「Generate toString()... (String() を生成...)」を選択してください。表示されるダイアログ・ボックスで、すべての属性を選択してから「OK」をクリックします。Employee についても、同じ手順を繰り返します。リスト 7 に、Employee に対して Eclipseが生成したコードを記載します。

リスト 7. Eclipse によって生成された toString() メソッド
@Override
public String toString() {
  return "Employee [taxpayerIdentificationNumber=" + taxpayerIdentificationNumber + ", 
      employeeNumber=" + employeeNumber + ", salary=" + salary + "]";
}

Eclipse が生成した toString のコードには、スーパークラスの toString() が含まれていません (Employee のスーパークラスは Person です)。この不備は、Eclipse を使って以下のオーバーライドを追加することで簡単に修正できます。

@Override
public String toString() {
  return super.toString() + "Employee [taxpayerIdentificationNumber=" + taxpayerIdentificationNumber + 
    ", employeeNumber=" + employeeNumber + ", salary=" + salary + "]";
}

toString() を追加すると、printAudit() は以下のように遥かに単純になります。

@Override
  public void printAudit(StringBuilder buffer) {
  buffer.append(toString());
}

オブジェクトの現在の状態をフォーマット化するという力仕事を toString() メソッドが引き受けるようになったので、このメソッドから返される結果を StringBuilder に取り込んで返せばよいだけです。

サポート用としてだけでも、クラスには常に toString() を実装することをお勧めします。何らかの時点でアプリケーションの実行中にオブジェクトの状態を確認する必要が生じるのは、事実上、避けられないことです。そのような場合、toString() はオブジェクトの状態を確認するためのフックとして大いに役立ちます。

クラス・メンバー

すべてのオブジェクト・インスタンスには変数とメソッドがありますが、その動作はインスタンスによって多少異なります。変数とメソッドの動作は、それぞれのオブジェクト・インスタンスの状態に基づくためです。PersonEmployee に定義した変数とメソッドはそれぞれ「インスタンス変数」、「インスタンス・メソッド」と呼ばれています。インスタンス変数とインスタンス・メソッドを使用するには、クラスをインスタンス化するか、またはインスタンスの参照を使用します。

クラスにも「クラス変数」と「クラス・メソッド」を使用できます。これらは総称して「クラス・メンバー」と呼ばれています。クラス変数を宣言するには static キーワードを使用します。クラス変数とインスタンス変数の違いは、以下のとおりです。

  • クラスのすべてのインスタンスが、クラス変数の単一のコピーを共有します。
  • クラスのインスタンスがなくても、クラス自体に対してクラス・メソッドを呼び出すことができます。
  • クラス・メソッドはクラス変数にだけアクセスできます。
  • インスタンス・メソッドはクラス変数にアクセスできますが、クラス・メソッドがインスタンス変数にアクセスすることはできません。

クラス変数とクラス・メソッドは、どのような場合に追加すべきなのでしょうか?最も確実な経験則としては、使い過ぎないように、ごくたまに追加することです。そうは言っても、以下の場合には、クラス変数とメソッドを追加するのが得策となります。

  • クラスの任意のインスタンスが使用できる (開発時に値を固定した) 定数を宣言する場合
  • クラスのインスタンスが必要になることは決してないユーティリティー (例えば、Logger.getLogger()) をクラスで使用する場合

クラス変数

クラス変数を作成するには、static キーワードを使用して宣言します。

accessSpecifier static variableName [= initialValue];

注: ここでは、オプションの要素を大括弧で囲んで示しています。これらの括弧は宣言構文には含まれません。

JRE はクラスの各インスタンス変数を保管するために、メモリー内に、クラスのインスタンスごとのスペースを設けます。それとは対照的に、JRE はクラス変数については、インスタンスの数を問わず 1 つのコピーしか作成しません。クラス変数のコピーを作成するタイミングは、クラスを初めてロードする時点 (つまり、JRE がプログラム内でその特定のクラスを初めて検出した時点) です。クラス変数のその単一のコピーを、クラスのすべてのインスタンスが共有します。このことから、すべてのインスタンスから使用できる必要のある定数に使うには、クラス変数が適任です。

例えば、PersonGender 属性は String として定義しましたが、制約は何も設定しませんでした。リスト 8 に、クラス変数の一般的な使い方を示します。

リスト 8. クラス変数の使用
public class Person {
  //. . .
  public static final String GENDER_MALE = "MALE";
  public static final String GENDER_FEMALE = "FEMALE";

  // . . .
  public static void main(String[] args) {
  Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", GENDER_MALE);
    // . . .
  }
  //. . .
}

定数を宣言する

通常、定数は以下のように宣言します。

  • 名前に使用する文字はすべて大文字にします。
  • 複数の単語からなる名前では、各単語を下線で区切ります。
  • final として宣言します (したがって、値を変更することはできません)。
  • public アクセス修飾子を使用して宣言します (したがって、定数の値にアクセスする必要がある他のクラスは、定数の名前を参照することで、該当する定数の値にアクセスできます)。

リスト 8 の場合、Person コンストラクターの呼び出しで MALE の定数を使用するには、その定数の名前を参照するだけで十分です。クラスの外部で定数を使用する場合は、その定数が宣言されているクラスの名前を定数名の前に付加します。

String genderValue = Person.GENDER_MALE;

クラス・メソッド

第 1 回からこのチュートリアルに従っているとしたら、静的 Logger.getLogger() メソッドの呼び出しはすでに何回か行っているはずです (出力をコンソールに書き出すために Logger インスタンスを取得する際は、常にこのメソッドを呼び出しました)。ただし、このメソッドを呼び出す場合、Logger のインスタンスを使用する必要はなかったことを思い出してください。それは、インスタンスを使用するのではなく、Logger クラスを参照したからです。これがすなわち、クラス・メソッドを呼び出すための構文です。クラス変数と同様に、static キーワードによって (この例の場合) Logger はクラス・メソッドとして識別されます。このことから、クラス・メソッドは「静的メソッド」と呼ばれることもあります。

静的変数と静的メソッドについて学んだ知識をつなぎ合わせれば、Employee 上で静的メソッドを作成することができます。まず、Logger を格納する private static final 変数を宣言します。すべてのインスタンスが共有するこの変数は、Employee クラスに対して getLogger() を呼び出すことでアクセスできます。リスト 9 にその方法を記載します。

リスト 9. クラス (静的) メソッドの作成
public class Employee extends Person {
  private static final Logger logger = Logger.getLogger(Employee.class.getName());

  //. . .
  public static Logger getLogger() {
    return logger;
  }

}

リスト 9 では重要なことが 2 つ行われています。

  • Logger インスタンスを private アクセスとして宣言しています。したがって、Employee 外部のクラスがこのインスタンスを直接参照することはできません。
  • クラスのロード時に Logger を初期化します。これは、Java 初期化構文を使用して、このインスタンスに値を設定するためです。

Employee クラスの Logger オブジェクトを取得するには、以下の呼び出しを使用します。

Logger employeeLogger = Employee.getLogger();

例外

完璧に動作し続けるプログラムというものはなく、Java 言語の設計者たちもこのことを認識していました。このセクションでは、コードが計画通りに機能しないという状況に対処するために Java プラットフォームに組み込まれているメカニズムについて説明します。

例外処理の基本

「例外」とは、プログラムの実行中に発生する、プログラムの正常な命令フローを中断させるイベントを指します。例外処理は、Java プログラミングに不可欠の手法です。コードを try ブロックでラップして (「これを試して、例外が発生したら知らせる」ようにすることを意味します)、そのブロックを catch ブロックと一緒に使用することで各種の例外をキャッチします。

例外処理に取り掛かるために、まずはリスト 10 のコードを見てください。

リスト 10. どこにエラーがあるか、わかりますか?
@Test
public void yetAnotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
//    Employee employee1 = new Employee();
  Employee employee1 = null;
  employee1.setName("J Smith");
  Employee employee2 = new Employee();
  employee2.setName("J Smith");
  l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
  l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
}

上記のコードでは、Employee 参照が null に設定されています。このコードを実行すると、以下の結果が出力されます。

java.lang.NullPointerException
  at com.makotojava.intro.EmployeeTest.yetAnotherTest(EmployeeTest.java:49)
  .
  .
  .

上記の出力によると、null 参照 (ポインター) を介してオブジェクトを参照しようとしています。これは重大な開発エラーです (お気付きだと思いますが、Eclipse には潜在的なエラーを警告するために、「Null pointer access: The variable employee1 can only be null at this location (null ポインター・アクセスセス: この場所では変数 employee1 が null にしかなりません)」というメッセージが表示されます。このように、Eclipse はさまざまな潜在的開発エラーについて警告を出します。これも、Java 開発に IDE を使用する利点です)。

幸い、try ブロックと catch ブロック (および finally の多少の助け) を使用することでエラーをキャッチできます。

try、catch、finally を使用する

リスト 11 では、リスト 10 のバグのあるコードを、trycatch、および finally という例外処理の標準的なコード・ブロックを使用してクリーンアップしています。

リスト 11. 例外のキャッチ
@Test
public void yetAnotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());

  //    Employee employee1 = new Employee();
  try {
    Employee employee1 = null;
    employee1.setName("J Smith");
    Employee employee2 = new Employee();
    employee2.setName("J Smith");
    l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
    l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));
  } catch (Exception e) {
    l.severe("Caught exception: " + e.getMessage());
  } finally {
    // Always executes
  }
}

trycatch、および finally ブロックの 3 つを合わせて、例外をキャッチするための網を張ります。まず、try 文で例外をスローする可能性のあるコードをラップします。この場合、例外は直接 catch ブロック (「例外ハンドラー」) に渡されます。try ブロックと catch ブロックのコードがすべて実行された後は、例外が発生したかどうかに関わらず、finally ブロックが実行されます。例外をキャッチした場合は、例外からのグレースフルな回復を試みることも、プログラム (またはメソッド) を終了することもできます。

リスト 11 では、プログラムがエラーから回復すると、以下の例外メッセージを出力します。

Sep 19, 2015 2:01:22 PM com.makotojava.intro.EmployeeTest yetAnotherTest
SEVERE: Caught exception: null

例外の階層

Java 言語には、さまざまなタイプの例外からなる完全な例外階層が統合されています。この階層では、例外が以下の 2 つの主要なカテゴリーにグループ化されています。

  • チェック例外。コンパイラーによってチェックされる例外です (コンパイラーが、コード内のどこかでこの例外が処理されることを確認している例外です)。通常、チェック例外は java.lang.Exception を直接継承するサブクラスです。
  • チェックなし例外。コンパイラーによってチェックされない例外です。「ランタイム例外」とも呼ばれます。これらの例外は、java.lang.RuntimeException のサブクラスです。

プログラムで例外が発生することを、プログラムが例外を「スローする」と表現します。どのメソッドでも、チェック例外をコンパイラーに宣言するには、メソッド・シグニチャーに throws キーワードを含めます。その後に、そのメソッドが実行中にスローする可能性のある例外のコンマ区切りリストを続けます。作成するコードで、1 つ以上のタイプの例外をスローする可能性があると指定しているメソッドを呼び出す場合、何らかの方法でそれに対処するか、メソッド・シグニチャーに throws キーワードを追加して、該当する例外タイプと一緒にコードに渡す必要があります。

例外が発生した場合、Java ランタイムはスタックの上に向かって例外ハンドラーを検索します。スタックの最上部にたどり着くまでに例外ハンドラーが見つからない場合、リスト 10 の場合のように、Java ランタイムはプログラムを急停止します。

複数の catch ブロック

複数の catch ブロックを使用することもできます。けれどもその場合には、特定の方法でブロックが構造化されていなければなりません。いずれかの例外が他の例外のサブクラスである場合、子クラスが親クラスよりも先に配置された形で catch ブロックを順序付けます。リスト 12 に、異なる例外タイプを階層内で正しく順序付けて構造化した例を示します。

リスト 12. 例外階層の例
@Test
public void exceptionTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
  File file = new File("file.txt");
  BufferedReader bufferedReader = null;
  try {
    bufferedReader = new BufferedReader(new FileReader(file));
    String line = bufferedReader.readLine();
    while (line != null) {
      // Read the file
    }
  } catch (FileNotFoundException e) {
    l.severe(e.getMessage());
  } catch (IOException e) {
    l.severe(e.getMessage());
  } catch (Exception e) {
    l.severe(e.getMessage());
  } finally {
    // Close the reader
  }
}

この例では、FileNotFoundExceptionIOException の子クラスであるため、この子クラスの catch ブロックを IOException の catch ブロックよりも先に配置する必要があります。さらに、IOExceptionException の子クラスであるため、Exception の catch ブロックよりも先に配置する必要があります。

try-with-resources ブロック

リスト 12 のコードでは、bufferedReader 参照を格納する変数を宣言しなければならないので、最後の finally ブロック内で bufferedReader を解放する必要があります。

別の方法として、(JDK 7 から使用可能になった) より簡潔な構文で、try ブロックがスコープを外れた時点でリソースを自動的に解放することもできます。リスト 13 に、この新しい構文を記載します。

リスト 13. リソース管理構文
@Test
public void exceptionTestTryWithResources() {
  Logger l = Logger.getLogger(Employee.class.getName());
  File file = new File("file.txt");
    try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
      String line = bufferedReader.readLine();
      while (line != null) {
// Read the file
      }
    } catch (Exception e) {
      l.severe(e.getMessage());
    }
}

基本的に、try に続く括弧内にリソース変数を割り当てると、try ブロックがスコープを外れた時点で、それらのリソースが自動的に解放されます。自動的に解放されるようにするリソースは java.lang.AutoCloseable インターフェースを実装しているものでなければなりません。このインターフェースを実装していないリソース・クラス上でこの構文を使おうとすると、Eclipse が警告を出します。

Java アプリケーションを作成する

このセクションでは、Person を Java アプリケーションとして引き続き拡張していきます。その過程で、オブジェクトまたはオブジェクトのコレクションがアプリケーションへと進化する仕組みについて理解を深めることができます。

アプリケーションのエントリー・ポイント

すべての Java アプリケーションには例外なく、Java ランタイムがコードの実行を開始する場所として認識するエントリー・ポイントが必要となります。そのエントリー・ポイントとなるのが、main() メソッドです。ドメイン・オブジェクト、つまりアプリケーションのビジネス・ドメインの一部となっているオブジェクト (例えば、PersonEmployee はドメイン・オブジェクトに該当します) には通常 main() メソッドは含まれませんが、すべてのアプリケーション内で少なくとも 1 つのクラスにこのメソッドが含まれていなければなりません。

ご存知のとおり、Person とそのサブクラスである Employee は、概念上は人材アプリケーションの一部となります。これから、アプリケーションに新しいクラスを追加して、そのクラスにエントリー・ポイントを割り当てます。

ドライバー・クラスを作成する

「ドライバー・クラス」の目的は、その名前からわかるように、アプリケーションを「駆動」することです。以下に示すように、人材アプリケーションの単純なドライバーに main() メソッドを含めます。

package com.makotojava.intro;
public class HumanResourcesApplication {
  public static void main(String[] args) {
  }
}

まず、Eclipse 内で、PersonEmployee を作成したときと同じ手順に従ってドライバー・クラスを作成します。このクラスには HumanResourcesApplication という名前を付けて、必ず main() メソッドをクラスに追加するための項目を選択してください。あとは、Eclipse によって自動的にクラスが生成されます。

次に、以下のような内容になるように、新しい main() メソッドにコードを追加します。

package com.makotojava.intro;
import java.util.logging.Logger;

public class HumanResourcesApplication {
  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  public static void main(String[] args) {
    Employee e = new Employee();
    e.setName("J Smith");
    e.setEmployeeNumber("0001");
    e.setTaxpayerIdentificationNumber("123-45-6789");
    e.setSalary(BigDecimal.valueOf(45000.0));
    e.printAudit(log);
  }
}

最後に、HumanResourcesApplication クラスを起動して、その実行状況を観察します。以下の出力が表示されるはずです。

Sep 19, 2015 7:59:37 PM com.makotojava.intro.Person printAudit
INFO: Name=J Smith,Age=0,Height=0,Weight=0,EyeColor=null,Gender=null TaxpayerIdentificationNumber=123-45-6789,EmployeeNumber=0001,Salary=45000.00

これだけの作業で、単純な Java アプリケーションが作成されました。次のセクションでは、より複雑なアプリケーションを開発する際に利用できる構文とライブラリーをいくつか取り上げて詳細を調べます。

継承

継承の例は、このチュートリアルですでに何度か目にしています。このセクションでは第 1 回での継承に関する説明内容を復習し、継承の仕組みをさらに詳しく調べるために、継承の階層、コンストラクターと継承、そして継承の抽象化について説明します。

継承の仕組み

Java コードで使用するクラスは、階層構造になっています。ある特定のクラスの上位階層に位置するクラスは、その特定のクラスの「スーパークラス」です。その特定のクラスは、上位階層に位置するすべてのクラスの「サブクラス」であり、サブクラスはそのスーパークラスを継承します。クラス階層の最上位に位置するのは java.lang.Object クラスです、したがって、すべての Java クラスは Object クラスのサブクラスであり、このクラスを継承することになります。

例えば、リスト 14 に示すような Person クラスがあるとします。

リスト 14. public Person クラス
public class Person {
  
  public static final String STATE_DELIMITER = "~";
  
  public Person() {
    // Default constructor
  }
  
  public enum Gender {
    MALE,
    FEMALE,
    UNKNOWN
  }
  
  public Person(String name, int age, int height, int weight, String eyeColor, Gender gender) {
    this.name = name;
    this.age = age;
    this.height = height;
    this.weight = weight;
    this.eyeColor = eyeColor;
    this.gender = gender;
  }


  private String name;
  private int age;
  private int height;
  private int weight;
  private String eyeColor;
  private Gender gender;

リスト 14 の Person クラスは、暗黙的に Object を継承しています。すべてのクラスは Object を継承することが前提となるため、定義するすべてのクラスに対して extends Object を入力しなければならないわけではありません。けれども、クラスがそのスーパークラスを継承するとは何を意味するのでしょうか?それは単に、Person がそのスーパークラス内の公開されている変数とメソッドにアクセスできることを意味します。この例の場合、Person には Object の public および protected として指定されたすべてのメソッドと変数が可視になり、Person はこれらのメソッドと変数を使用することができます。

クラス階層を定義する

次は、Person を継承する Employee クラスの例を取り上げます。Employee クラスは、以下のように定義されているとします。

public class Employee extends Person {

  private String taxpayerIdentificationNumber;
  private String employeeNumber;
  private BigDecimal salary;
  // . . .
}

すべてのスーパークラスに対する Employee の継承関係 (継承図) から、EmployeePerson の public および protected として指定されたすべてのメソッドと変数にアクセスできること (EmployeePerson を直接継承するため)、さらに Object の public および protected として指定されたメソッドと変数にもアクセスできることを読み取れます (Employee は間接的ではあっても Object も継承するため)。一方、EmployeePerson は同じパッケージに含まれていることから、EmployeePerson の「パッケージ・プライベート」(または「フレンドリー」) 変数およびメソッドにもアクセスできます。

クラス階層をさらに 1 ステップ深くするには、例えば以下のような Employee を継承する 3 番目のクラスを作成するという方法があります。

public class Manager extends Employee {
  // . . .
}

Java 言語では、いずれのクラスも直接継承できるスーパークラスは 1 つだけですが、サブクラスについては、いくらでも持つことができます。この点が、Java 言語での継承階層について覚えておかなければならない最も重要なことです。

単一継承と多重継承

C++ のような言語では、多重継承の概念をサポートしています。多重継承は、階層の任意の箇所でクラスが 1 つ以上のクラスを直接継承できるという概念です。java 言語でサポートしている継承は、単一継承のみです。つまり、extends キーワードは単一のクラスでしか使用できません。したがって、Java クラスのクラス階層には java.lang.Object に達するまで一切の分岐がありません。ただし、次のメイン・セクション「インターフェース」で説明するように、Java 言語では単一のクラス内に複数のインターフェースを実装できるようになっているため、あらゆる類の単一継承の次善策としてインターフェースを使用することができます。

コンストラクターと継承

コンストラクターは本格的なオブジェクト指向のメンバーではないため、コンストラクターを継承することはできません。したがって、サブクラス内に明示的にコンストラクターを実装する必要があります。これについて詳しく説明する前に、コンストラクターの定義および呼び出し方法についての基本的な規則を復習しましょう。

コンストラクターの基本

コンストラクターの名前はそのコンストラクターを使用して構成するクラスの名前と同じであること、そしてコンストラクターには戻り値の型がないことを思い出してください。以下に一例を記載します。

public class Person {
  public Person() {
  }
}

すべてのクラスには少なくとも 1 つのコンストラクターがあります。クラスのコンストラクターを明示的に定義しなければ、コンパイラーによって自動的に「デフォルト・コンストラクター」という名前のコンストラクターが生成されます。上記のクラス定義と以下のクラス定義は、機能に関してはまったく同じです。

public class Person {
}

スーパークラス・コンストラクターを呼び出す

デフォルト・コンストラクター以外のスーパークラス・コンストラクターを呼び出すには、明示的に呼び出す必要があります。例えば、Person のコンストラクターは、作成する Person オブジェクトの名前だけをパラメーターとして取るとします。Employee のデフォルト・コンストラクターからは Person コンストラクターを呼び出すには、例えばリスト 15 に示す方法を使用できます。

リスト 15. 新規 Employee の初期化
public class Person {
  private String name;
  public Person() {
  }
  public Person(String name) {
    this.name = name;
  }
}

// Meanwhile, in Employee.java
public class Employee extends Person {
  public Employee() {
    super("Elmer J Fudd");
  }
}

けれども、このような方法で新しい Employee オブジェクトを初期化したいとは決して思わないでしょう。オブジェクト指向の概念と一般的な Java 構文を使い慣れるまでは、スーパークラス・コンストラクターをサブクラスに実装するのは、そうしなければならないことが確実な場合に限るのが賢明です。リスト 16 では、Person に定義されているコンストラクターと一致するよう、Employee に同じようなコンストラクターを定義しています。保守の観点からは、この手法のほうが遥かに混乱しにくくなります。

リスト 16. スーパークラスの呼び出し
public class Person {
  private String name;
  public Person(String name) {
    this.name = name;
  }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
  public Employee(String name) {
    super(name);
  }
}

コンストラクターを宣言する

コンストラクター内の最初の行で、別のコンストラクターを呼び出さない限り、コンストラクターは真っ先にそのクラスが直接継承するスーパークラスのデフォルト・コンストラクターを呼び出します。例えば、以下の 2 つの宣言は機能的にはまったく同じです。

public class Person {
  public Person() {
  }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
  public Employee() {
  }
}
public class Person {
  public Person() {
  }
}
// Meanwhile, in Employee.java
public class Employee extends Person {
  public Employee() {
  super();
  }
}

引数なしのコンストラクター

デフォルト・コンストラクターの代わりとなるコンストラクターを指定するには、デフォルト・コンストラクターを明示的に指定する必要があります。そうしなければ、代替コンストラクターを使用できません。例えば、以下のコードではコンパイラー・エラーが発生します。

public class Person {
  private String name;
  public Person(String name) {
    this.name = name;
  }
}
// Meanwhile, in Employee.java
public class Employee extends Person {

  public Employee() {
  }
}

上記の例の Person クラスにはデフォルト・コンストラクターがありません。なぜなら、デフォルト・コンストラクターを明示的に含めることなく、代替コンストラクターを指定しているためです。

あるコンストラクターから別のコンストラクターを呼び出す方法

コンストラクターは同じクラスに含まれる別のコンストラクターを呼び出すことができます。それには、this キーワードと併せて引数リストを使用します。super() と同じく、this() の呼び出しはコンストラクター内の先頭行で行う必要があります (以下の例を参照)。

public class Person {
  private String name;
  public Person() {
    this("Some reasonable default?");
  }
  public Person(String name) {
    this.name = name;
  }

}

この慣用的なコード・ブロックは頻繁に目にすることになります。つまり、あるコンストラクターが別のコンストラクターに処理を任せ、その別のコンストラクターが呼び出された場合にはデフォルト値を渡すという手法です。この手法はまた、新しいコンストラクターをクラスに追加する一方、元のコンストラクターをすでに使用しているコードへの影響を最小限に抑えるのにも大いに役立ちます。

コンストラクターのアクセス・レベル

コンストラクターにも任意のアクセス・レベルを指定できますが、可視性に関する特定の規則が適用されます。表 1 に、コンストラクターのアクセス・ルールを要約します。

表 1. コンストラクターのアクセス・ルール
コンストラクターのアクセス修飾子説明
public任意のクラスがコンストラクターを呼び出すことができます。
protected同じパッケージ内のクラスまたは任意のサブクラスがコンストラクターを呼び出すことができます。
修飾子なし (パッケージ・プライベート) 同じパッケージ内の任意のクラスがコンストラクターを呼び出すことができます。
privateコンストラクターが定義されているクラスだけがコンストラクターを呼び出すことができます。

コンストラクターを protected として宣言する使用ケース、さらにはパッケージ・プライベートとして宣言する使用ケースもいろいろと思い付くかもしれませんが、private コンストラクターについてはどうでしょうか?これが役に立つ場合を考えられますか?私はこれまで、例えば Factory パターンを実装するときに、new キーワードを使って直接オブジェクトを作成できないようにするために private コンストラクターを使用してきました。この場合、クラスのインスタンスを作成するために静的メソッドを使用しています。この (そのクラスに含まれる) 静的メソッドからは、private コンストラクターを呼び出すことが許可されます。

継承と抽象化

サブクラスがスーパークラスのメソッドをオーバーライドすると、そのメソッドは基本的に隠されることになります。なぜなら、サブクラスの参照によってメソッドを呼び出すと、スーパークラスのメソッドではなく、サブクラスのバージョンのメソッドが呼び出されるからです。それでもまだ、スーパークラス・メソッドのメソッドにアクセスすることは可能です。サブクラスでスーパークラスのメソッドを呼び出すには、そのメソッドの名前の先頭に super キーワードを付加します (コンストラクターの規則での場合とは異なり、この呼び出しは、サブクラス・メソッド内の任意の行から行うことができます。さらに、まったく別のメソッドから呼び出すことさえできます)。Java プログラムはデフォルトでは、サブクラスの参照によってメソッドが呼び出される場合は、サブクラスのメソッドを呼び出します。

この機能は、呼び出し側が変数にアクセスできる場合は変数にも適用されます (つまり、変数は、それにアクセスしようとするコードから可視になります)。この細かな仕様は、Java プログラムに習熟してきたプログラマーにとって尽きない苦悩の種になりますが、Eclipse を使用していれば十分な警告が出されます。例えば、スーパークラスの変数が隠されている場合やメソッド呼び出しが目的のメソッドを呼び出さない場合などは、Eclipse から警告が出されます。

OOP のコンテキストでは、「抽象化」とは継承階層内でデータと動作を現在のクラスよりも上位のタイプに一般化することを意味します。変数やメソッドをサブクラスからスーパークラスに移す場合、それはそれらのメンバーを「抽象化する」と表現されます。抽象化する主な理由は、共通のコードをできる限り階層の上位に移して再利用するためです。しかも、共通のコードを 1 箇所に集めれば、保守しやすくなります。

抽象クラスと抽象メソッド

場合によっては、抽象化としての役割だけを果たし、必ずしもインスタンス化する必要がないクラスを作成しなければならないことがあります。そのようなクラスは「抽象クラス」と呼ばれます。同様に、サブクラスごとに特定のメソッドをスーパークラスとは異なる方法で実装する必要がある場合もあります。そのようなメソッドは「抽象メソッド」と呼ばれます。抽象クラスおよびメソッドの基本的な規則は以下のとおりです。

  • 任意のクラスを abstract として宣言できます。
  • 抽象クラスを継承することはできません。
  • 抽象メソッドにメソッド本体を含めることはできません。
  • 抽象クラスを持つクラスはいずれも abstract として宣言する必要があります。

抽象化を使用する

例えば、Employee クラスを直接インスタンス化できないようにする必要があるとします。その場合、abstract キーワードを使ってクラスを宣言するだけで、その目的を果たすことができます。

public abstract class Employee extends Person {
  // etc.
}

以下のコードを実行しようとすると、コンパイル・エラーが発生します。

public void someMethodSomwhere() {
  Employee p = new Employee();// compile error!!
}

コンパイラーは、Employee が抽象クラスであり、インスタンス化できないことに対してエラーを出しているというわけです。

抽象化の力

次の例として、Employee オブジェクトの状態を調べて、オブジェクトが有効であることを確認するメソッドが必要だとします。このメソッドは、すべての Employee オブジェクトに共通して必要であるように思えますが、潜在的なサブクラスの 1 つごとにメソッドの動作が変わってくることから、再利用できる可能性はまったくありません。そのような場合は、この validate() メソッドを abstract として宣言します (すべてのサブクラスに、このメソッドを実装するよう強制します)。

public abstract class Employee extends Person {
  public abstract boolean validate();
}

これで、Employee を直接継承するサブクラス (Manager) はいずれも validate() メソッドを実装しなければならなくなります。けれども、サブクラスがいったん validate() メソッドを実装した後は、そのサブクラスのサブクラスがこのメソッドを実装する必要はありません。

例えば、Executive オブジェクトを Manager が継承する場合、以下の定義は有効です。

public class Executive extends Manager {
  public Executive() {
  }
}

抽象化するべき (するべきでない) 場合: 2 つの規則

第一の経験則として、初期設計では抽象化を使用しないでください。設計の初期段階で抽象クラスを使用すると、いずれはアプリケーションに制限が課せられることになります。常に、共通の動作 (抽象クラスを使用する要点そのもの) を階層図の上のほうにリファクタリングするようにしてください。また、ほぼ必ずと言ってよいほど、リファクタリングするのは、その必要性を見つけてからにするのが賢明です。Eclipse には素晴らしいリファクタリングのサポートが備わっています。

第二に、抽象クラスは非常に役立ちますが、なるべく使用しないようにしてください。複数のスーパークラスの動作があまりにも共通していて、それらのスーパークラスのそれぞれに動作を持たせても意味がないという場合を除き、クラスを抽象化するのは禁物です。継承図のレベルが深くなると、コードを保守するのが困難になります。クラスのサイズと保守しやすいコードとの間のトレードオフを検討する必要があります。

代入: クラス

あるクラスの参照を、別のクラスに属する型の変数に代入することは可能ですが、特定の規則が適用されます。以下の例を見てください。

Manager m = new Manager();
Employee e = new Employee();
Person p = m; // okay
p = e; // still okay
Employee e2 = e; // yep, okay
e = m; // still okay
e2 = p; // wrong!

代入先の変数は、代入元の参照に属するクラスのスーパータイプでなければなりません。そうでないと、コンパイラーがエラーを出します。代入の右辺にくるものが何であれ、それはサブクラスであるか、左辺にあるものと同じクラスでなければなりません。別の言い方をすると、目的という点で、サブクラスはスーパークラスより具体的であるため、サブクラスはスーパークラスより範囲が狭いと考えられます。また、より汎用的なスーパークラスは、サブクラスより範囲が広いと考えられます。規則となるのは、参照の範囲を狭めるような代入は行ってはならないことです。

以下の例を見てください。

Manager m = new Manager();
Manager m2 = new Manager();
m = m2; // Not narrower, so okay
Person p = m; // Widens, so okay
Employee e = m; // Also widens
Employee e = p; // Narrows, so not okay!

EmployeePerson ですが、必ずしも Manager であるとはいえません。コンパイラーはこの区別を実施します。

インターフェース

このセクションで、インターフェースについて基礎から学び、Java コードで実際にインターフェースを使い始めてください。

インターフェース: 何に役立つのか?

前のセクションで学んだように、抽象メソッドは仕様として (メソッド名、パラメーター、戻り値の型という手段で)「規約」を指定しますが、それによってコードが再利用可能になることはありません。(抽象クラスに定義された) 抽象メソッドは、動作の実装方法が、抽象クラスのサブクラスの間で変わる可能性がある場合に役立ちます。

アプリケーション内に、まとめて 1 つのグループにして名前を付けられるものの、複数の実装が存在する一連の共通の動作が見つかった場合 (java.util.List の例を考えてください)、その動作を「インターフェース」を使用して定義することを検討してください。インターフェースという機能は、そのために Java 言語に用意されているのです。ただし、このかなり高度な機能は (私が身をもって体験してきたように) 誤用されたり、分かりにくくされたり、憎むべき形に歪められたりしがちです。そのため、インターフェースは慎重を期して使用する必要があります。

インターフェースの捉え方としては、インターフェースは抽象メソッドだけが含まれる抽象クラスのようなものであると考えると役に立つかもしれません。つまり、インターフェースはメソッドの規約だけを定義し、実装は一切定義しないのです。

インターフェースを定義する

インターフェースを定義するための構文は、以下のように単純明快です。

public interface InterfaceName {
    returnType methodName(argumentList);
  }

インターフェースの宣言は、interface キーワードを使用するという点を除き、クラスの宣言と同様です。インターフェースには (言語の規則に従って) 任意の名前を付けることができますが、慣例により、クラス名に似たインターフェース名を指定します。

インターフェース内に定義するメソッドには、メソッド本体がありません。メソッドの本体を提供するのは、インターフェースを実装する開発者の責任です (この点は、抽象クラスの場合と同じです)。

インターフェースの階層はクラスの階層と同じように定義しますが、クラスごとに必要な数のインターフェースをいくらでも実装できるという点が異なります。クラスで継承できるクラスは 1 つだけであることを思い出してください。あるクラスが別のクラスを継承して 1 つまたは複数のインターフェースを実装する場合は、以下のように、継承されるクラスの後にインターフェースのリストを続けます。

public class Manager extends Employee implements BonusEligible, StockOptionRecipient {
  // And so on
}

インターフェースに本体を含める必要は一切ありません。例えば、以下の定義にはまったく問題がありません。

public interface BonusEligible {
}

一般的に言えば、このようなインターフェースはそのインターフェースを実装するクラスをマークするだけで、特に明示的な動作を提供するわけではありません。このことから、この類のインターフェースは「マーカー・インターフェース」と呼ばれます。

以上の知識があれば、実際にインターフェースを定義するのは簡単なことです。

public interface StockOptionRecipient {
  void processStockOptions(int numberOfOptions, BigDecimal price);
}

インターフェースを実装する

作成するクラスでインターフェースを定義するには、インターフェースを「実装」する必要があります。つまり、インターフェースの規約を履行する動作を可能にするメソッド本体を用意するということです。インターフェースを実装するには、implements キーワードを使用します。

public class ClassName extends SuperclassName implements InterfaceName {
  // Class Body
}

例えば、Manager クラスで StockOptionRecipient インターフェースを実装するとします (リスト 17 を参照)。

リスト 17. インターフェースの実装
public class Manager extends Employee implements StockOptionRecipient {
  public Manager() {
  }
  public void processStockOptions (int numberOfOptions, BigDecimal price) {
    log.info("I can't believe I got " + number + " options at $" +
    price.toPlainString() + "!"); 
  }
}

インターフェースを実装する際は、インターフェース上にメソッド (複数可) の動作を実装します。それには、インターフェース上のシグニチャーと一致するシグニチャーを使用してメソッドを実装し、そのメソッドに public アクセス修飾子を付加する必要があります。

特定のインターフェースを実装することを宣言するクラスとしては抽象クラスを使用できますが、抽象クラスでそのインターフェース上に定義されたメソッドのすべてを実装しなければならないわけではありません。抽象クラスの場合、そのクラスが実装すると宣言するメソッドのすべてを実装する必要はないためです。ただし、最初の具象クラス (つまり、インスタンス化できる最初のクラス) が、クラス階層に実装されていないすべてのメソッドを実装する必要があります。

注: インターフェースを実装する具象クラスのサブクラスには、そのインターフェースの固有の実装は必要ありません (インターフェースに定義されたメソッドは、すでにスーパークラスによって実装されているためです)。

Eclipse 内でインターフェースを生成する

クラスのいずれかでインターフェースを実装すべきだと判断した場合は、Eclipse で簡単に正しいメソッド・シグニチャーを生成できます。そのために必要なのは、インターフェースを実装するようにクラスのシグニチャーを変更することだけです。すると、そのクラスにはインターフェース上のメソッドがないため、Eclipse がクラスの下に赤の波線を引いて、その変更が誤っているというフラグを立てます。クラス名をクリックして Ctrl + 1 を押すと、Eclipse が「クイック・フィックス」候補を提示します。これらの候補のうち、「Add Unimplemented Methods (実装されていないメソッドを追加)」を選択すると、Eclipse がそれらのメソッドを自動的に生成して、ソース・ファイルの末尾に追加してくれます。

インターフェースを使用する

インターフェースを参照するには、インターフェースが定義する新しい「参照型」というデータ型を使用します。クラスを参照するような任意の場所で、参照型を使用して、そのインターフェースを参照できます。参照型変数を宣言するとき、つまり、ある型から別の型にキャストするときにも、この機能を使用できます。

リスト 18. StockOptionEligible 参照への新規 Manager インスタンスの割り当て
package com.makotojava.intro;
import java.math.BigDecimal;
import org.junit.Test;
public class ManagerTest {
  @Test
  public void testCalculateAndAwardStockOptions() {
    StockOptionEligible soe = new Manager();// perfectly valid
    calculateAndAwardStockOptions(soe);
    calculateAndAwardStockOptions(new Manager());// works too
    }
    public static void calculateAndAwardStockOptions(StockOptionEligible soe) {
    BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
    int numberOfOptions = 10000;
    soe.awardStockOptions(numberOfOptions, reallyCheapPrice);
  }
}

ご覧のように、新しい Manager インスタンスを StockOptionEligible 参照に代入することも、StockOptionEligible 参照を期待するメソッドに新しい Manager インスタンスを渡すこともできます。

代入: インターフェース

インターフェースを実装するクラスからインターフェース型の変数に参照を代入することはできますが、その場合には特定の規則が適用されます。リスト 18 からわかるように、Manager インスタンスを StockOptionEligible 変数の参照に代入することはできます。なぜなら、Manager クラスはそのインターフェースを実装するからです。一方、以下の代入は無効です。

 Manager m = new Manager();
  StockOptionEligible soe = m; //okay
  Employee e = soe; // Wrong!

EmployeeManager のスーパータイプであることから、上記のコードは一見すると問題がなさそうですが、実はそうではありません。なぜなら、ManagerStockOptionEligible を実装している一方、Employee はこのインターフェースを実装していないためです。

このようなインターフェースの代入には、「継承」のセクションで説明した代入のルールが適用されます。クラスの場合と同じく、インターフェースの参照を代入できるのは、同じ型またはスーパーインターフェース型の変数に限られます。

ネストしたクラス

このセクションでは、ネストしたクラスの概要、そしてクラスをネストする場合とその方法について説明します。

ネストしたクラスを使用する場合

その名前からわかるように、「ネストしたクラス」とは、別のクラス内に定義されたクラスのことです (「内部クラス」とも呼ばれます)。

public class EnclosingClass {
  . . .
  public class NestedClass {
  . . .

  }
}

メンバー変数とメンバー・メソッドと同じように、Java のクラスは publicprivateprotected を含む任意のスコープを指定して定義できます。ネストしたクラスが役立つのは、オブジェクト指向の方法でクラス内での処理が必要になる一方、その機能を対象のクラスだけに制限しなければならない場合です。

通常、クラスとそのクラス内で定義するクラスを密結合する必要がある場合は、ネストしたクラスを使用します。内部クラスは外側のクラスに含まれるプライベート・データにアクセスできますが、この構造には、ネストしたクラスを扱い始めたばかりでは明らかにはわからない副次作用が伴います。

ネストしたクラスでのスコープ

ネストしたクラスにはスコープが設定されるため、ネストしたクラスはスコープの規則に縛られます。例えば、メンバー変数にはそのクラス (オブジェクト) のインスタンスからでなければアクセスできませんが、このことは、ネストしたクラスにも当てはまります。

一例として、DirectReports というネストしたクラスがあり、ManagerDirectReports (この Manager に報告する Employee のコレクション) の間に以下の関係があるとします。

public class Manager extends Employee {
  private DirectReports directReports;
  public Manager() {
    this.directReports = new DirectReports();
  }
  . . .
  private class DirectReports {
  . . .
  }
}

Manager オブジェクトが固有の人間を表すのと同様に、DirectReports オブジェクトはマネージャーに報告する実際の従業員の集合を表します。DirectReports の内容は、Manager によって異なります。この場合、ネストしたクラス DirectReports は外側の Manager インスタンスのコンテキストでのみ参照するのが当然なので、このネストしたクラスを private クラスにしています。

ネストした public クラス

DirectReportsprivate であるため、そのインスタンスを作成できるのは Manager のみです。その一方、外部エンティティーが DirectReports のインスタンスを作成できる必要のある場合を考えてください。その場合、DirectReports クラスのスコープを public として指定し、任意の外部コードで DirectReports インスタンスを作成するという方法で対処できるように思えます (リスト 19 を参照)。

リスト 19. DirectReportsインスタンスの作成: 最初の試み
public class Manager extends Employee {
  public Manager() {
  }
  . . .
  public class DirectReports {
  . . .
  }
}
//
public static void main(String[] args) {
  Manager.DirectReports dr = new Manager.DirectReports();// This won't work!
}

リスト 19 のコードでは上手くいきません。どうしてなのか首をかしげるかもしれませんが、問題は (そしてそのソリューションは) Manager 内での DirectReports の定義方法とスコープの規則にあります。

スコープの規則 (復習)

Manager のメンバー変数を使用する場合、Manager オブジェクトの参照がない状態でその変数を参照しようとすると、コンパイラーがエラーを出すことは理解できるはずです。これと同じことは、DirectReports にも当てはまります。少なくともリスト 19 でのように定義されたネストしたクラスには、このスコープの規則が適用されます。

ネストした public クラスのインスタンスを作成するには、new 演算子の特殊なバージョンを使用します。外部クラスを収容する外側のインスタンスの参照と new の組み合わせが、ネストしたクラスのインスタンスを作成する手段となります。

public class Manager extends Employee {
  public Manager() {
  }
  . . .
  public class DirectReports {
  . . .
  }
  }
// Meanwhile, in another method somewhere...
public static void main(String[] args) {
  Manager manager = new Manager();
  Manager.DirectReports dr = manager.new DirectReports();
}

行 12 の構文では、外側のインスタンスを参照する部分の後にドットと new キーワードを続け、その後に作成するクラスを指定する必要があることに注意してください。

静的内部クラス

場合によっては、クラスに (概念上は) 密結合されているクラスを作成するとしても、スコープの規則を幾分緩和して、外側のインスタンスの参照を使用する必要をなくさなければならないこともあります。そのような場合には、「静的」内部クラスが役立ちます。これが役立つ典型的な例の 1 つは、Comparator を実装する場合です。Comparator は、通常はクラスを順序付ける (ソートする) 目的で同じクラスの 2 つのインスタンスを比較するために使用されます。

public class Manager extends Employee {
  . . .
  public static class ManagerComparator implements Comparator<Manager> {
  . . .
  }
  }
// Meanwhile, in another method somewhere...
public static void main(String[] args) {
  Manager.ManagerComparator mc = new Manager.ManagerComparator();
  . . .
}

この場合、外側のインスタンスは必要になりません。静的内部クラスは通常の Java クラスの内部クラスのように機能するため、クラスとその定義を密結合する必要がある場合にだけ使用してください。明らかに、ManagerComparator のようなユーティリティー・クラスの場合には、外部クラスを作成する必要はありません。作成すると、コード・ベースが外部クラスによって雑然としてしまう可能性があります。そのようなクラスは、静的内部クラスとして定義するべきです。

匿名内部クラス

Java 言語では、抽象クラスとインターフェースを至って簡単に実装できます。しかも、実装する場所は自由です。必要であれば、メソッドの中央に実装することも、さらにはクラスの名前を指定せずに実装することもできます。この機能は基本的にコンパイラーがなせる技ですが、匿名内部クラスを使用すると重宝な場合もあります。

リスト 20 ではリスト 17 を拡張し、StockOptionEligible ではない Employee タイプを処理するデフォルト・メソッドを追加しています。このリストは、HumanResourcesApplication のメソッドがストック・オプションを処理するところから始まり、続いて JUnit テストを使用してメソッドを駆動します。

リスト 20. StockOptionEligible ではない Employee タイプの処理
// From HumanResourcesApplication.java
public void handleStockOptions(final Person person, StockOptionProcessingCallback callback) {
  if (person instanceof StockOptionEligible) {
    // Eligible Person, invoke the callback straight up
    callback.process((StockOptionEligible)person);
  } else if (person instanceof Employee) {
    // Not eligible, but still an Employee. Let's cobble up a
    /// anonymous inner class implementation for this
    callback.process(new StockOptionEligible() {
      @Override
      public void awardStockOptions(int number, BigDecimal price) {
        // This employee is not eligible
        log.warning("It would be nice to award " + number + " of shares at $" +
            price.setScale(2, RoundingMode.HALF_UP).toPlainString() +
            ", but unfortunately, Employee " + person.getName() + 
            " is not eligible for Stock Options!");
      }
    });
  } else {
    callback.process(new StockOptionEligible() {
      @Override
      public void awardStockOptions(int number, BigDecimal price) {
        log.severe("Cannot consider awarding " + number + " of shares at $" +
            price.setScale(2, RoundingMode.HALF_UP).toPlainString() +
            ", because " + person.getName() + 
            " does not even work here!");
      }
    });
  }
}
// JUnit test to drive it (in HumanResourcesApplicationTest.java):
@Test
public void testHandleStockOptions() {
  List<Person> people = HumanResourcesApplication.createPeople();

  StockOptionProcessingCallback callback = new StockOptionProcessingCallback() {
    @Override
    public void process(StockOptionEligible stockOptionEligible) {
      BigDecimal reallyCheapPrice = BigDecimal.valueOf(0.01);
      int numberOfOptions = 10000;
      stockOptionEligible.awardStockOptions(numberOfOptions, reallyCheapPrice);
    }
  };
  for (Person person : people) {
    classUnderTest.handleStockOptions(person, callback);
  }
}

リスト 20 の例には、匿名内部クラスを使用する 2 つのインターフェースの実装を示しています。最初の 2 つのインターフェースは StockOptionEligible の別個の実装であり、一方は Employees 用、もう一方は Persons 用です (このインターフェースに従った実装)。その後に続く StockOptionProcessingCallback の実装は、Manager インスタンスのストック・オプションを処理するために使用します。

匿名内部クラスの概念を理解するには時間がかかるかもしれませんが、匿名内部クラスは非常に重宝します。私が Java コードを作成するときは、いつも匿名内部クラスを使用しています。Java 開発者として成長するにつれ、皆さんも同じく匿名内部クラスを使うようになるはずです。

正規表現

「正規表現」とは基本的に、同じパターンを共有する一連のストリングを記述するパターンのことです。Perl プログラマーにとっては、Java 言語で使用する正規表現 (regex) パターンの構文は馴染み深く感じられることでしょう。けれどもこれを使ったことがないプログラマーの目には、正規表現は奇異に映るかもしれません。このセクションでは、Java プログラムで正規表現を使用する方法を説明します。

regex API

以下のストリングには、いくつかの共通点があります。

  • A string
  • A longer string
  • A much longer string

上記のストリングはいずれも「A」で始まり、「string」で終わっています。Java regex API は、共通の要素の抽出、要素間のパターンの検出、収集した情報に対する関心を持った処理の実行に役立ちます。

regex API には、皆さんがほぼ常に使用することになる、以下の 3 つのコア・クラスがあります。

  • Pattern。ストリング・パターンを記述します。
  • Matcher。ストリングをテストしてパターンに一致するかどうかを調べます。
  • PatternSyntaxException。定義しようとしているパターンに許容されない点があることを知らせてくれます。

この後、以上のクラスを使用した単純な正規表現パターンを扱いますが、その前に、正規表現パターンの構文を確認してください。

正規表現パターンの構文

「正規表現パターン」は、入力ストリング内で見つけようとするストリングの構造を表現するものです。正規表現パターンの構文は、初心者には奇妙に見えますが、いったん構文を理解すれば、正規表現パターンは簡単に解釈できることがわかるはずです。表 2 に、パターン・ストリング内で最もよく使う正規表現構成体を抜粋します。

表 2. よく使われる正規表現構成体
正規表現構成体一致条件
.任意の文字
?直前の文字が 0 回または 1 回出現
*直前の文字が 0 回以上出現
+直前の文字が 1 回以上出現
[]文字または数字の範囲
^後に続く文字の否定 (つまり、「出現しない」)
¥d任意の数字 ([0-9] と同じ)
¥D数字以外 ([^0-9] と同じ)
¥s任意の空白文字 ([¥n¥t¥f¥r] と同じ)
¥S任意の非空白文字 ([^¥n¥t¥f¥r] と同じ)
¥w任意の単語 ([a-zA-Z_0-9] と同じ)
¥W単語以外 ([^¥w] と同じ)

表の最初に記載されているいくつかの構成体は、直前の内容を数値で表すことから、「数量子」と呼ばれます。¥d などの構成体は、事前定義された文字クラスです。パターンの中で特別な意味を持たないすべての文字はリテラルであり、その文字自体と一致します。

パターン・マッチ

表 2 に記載されているパターンの構文を知っていれば、Java regex API に含まれているクラスを使用したリスト 21 の単純な例を理解できるはずです。

リスト 21. 正規表現を使用したパターン・マッチ
Pattern pattern = Pattern.compile("[Aa].*string");
  Matcher matcher = pattern.matcher("A string");
  boolean didMatch = matcher.matches();
  Logger.getAnonymousLogger().info (didMatch);
  int patternStartIndex = matcher.start();
  Logger.getAnonymousLogger().info (patternStartIndex);
  int patternEndIndex = matcher.end();
  Logger.getAnonymousLogger().info (patternEndIndex);

リスト 21 ではまず、compile() (Pattern の静的メソッド) を呼び出して Pattern クラスを作成しています。このクラスには、一致対象のパターンを表現するストリング・リテラルが含まれていて、そのリテラルで正規表現パターンの構文が使用されています。この例のパターンは、以下のように翻訳されます。

大文字 A または小文字 a の後にゼロ個以上の文字が続き、最後が string で終わるストリングを検索する。

パターン・マッチに使用するメソッド

リスト 21 では Pattern クラスを作成した後、このクラスに対して matcher() を呼び出します。この呼び出しにより、Matcher インスタンスが作成されます。Matcher は自身に渡されたストリングを調べて、Pattern を作成したときに使用したパターン・ストリングとの一致を探します。

Java 言語のすべてのストリングはインデックス付きの文字集合です。インデックス値は 0 から始まり、ストリング長から 1 を引いた値で終わります。Matcher はインデックス値 0 の位置にあるストリングから解析し、パターン・ストリングとの一致を探します。このプロセスが完了した時点で、Matcher には、入力ストリング内で見つかった (または見つからなかった) 一致に関する情報が含まれることになります。その情報にアクセスするには、Matcher に対して以下のメソッドを呼び出します。

  • matches()。入力シーケンス全体がパターンとの完全一致であるかどうかを調べることができます。
  • start()。ストリングの中で、一致したストリングが開始した位置のインデックス値を取得できます。
  • end()。ストリングの中で、一致したストリングが終了した位置のインデックス値に 1 を足した値を取得できます。

リスト 21 では、インデックス値 0 で開始してインデックス値 7 で終了する一致が 1 つだけ検出されます。したがって、matches() を呼び出すと true が返され、start() を呼び出すと 0 が返され、end() を呼び出すと 8 が返されます。

lookingAt() と matches()

検索対象のパターンに含まれる文字数を超える要素が含まれるストリングの場合には、matches() ではなく、lookingAt() を使用できます。lookingAt() メソッドは、サブストリングと指定されたパターンとの一致を検索します。例えば、以下のストリングがあるとします。

a string with more than just the pattern.

このストリングで a.*string パターンを検索する場合、lookingAt() を使用すると一致が見つかります。一方、matches() を使用すると false が返されます。それは、このストリングにはパターンに含まれる文字数より多くの要素があるためです。

正規表現での複合パターン

regex のクラスを使用すると単純な検索を簡単に行えるだけでなく、regex API では非常に高度な処理にも対応できます。

wiki は、ほぼ完全に正規表現に基づいています。ユーザーが入力したストリングに基づく wiki の内容は、正規表現を使用して解析され、フォーマット化されます。wiki ワードを入力すれば、どのユーザーでも wiki 内の別のトピックへのリンクを作成することができます。一般に、wiki ワードは一連の連結された単語であり、各単語は大文字で始まります。以下はその一例です。

MyWikiWord

例えば、ユーザーが以下のストリングを入力したとします。

Here is a WikiWord followed by AnotherWikiWord, then YetAnotherWikiWord.

上記のストリング内で wiki ワードを検索するには、正規表現パターンを以下のように使用します。

[A-Z][a-z]*([A-Z][a-z]*)+

以下のコードは、wiki ワードを検索します。

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
  Logger.getAnonymousLogger().info("Found this wiki word: " + matcher.group());
}

このコードを実行すると、コンソールに 3 つの wiki ワードが表示されます。

ストリングを置換する

一致を検索する機能は重宝しますが、ストリングの一致を見つけた後に、それらのストリングを操作することも可能です。ストリングを操作するには、一致したストリングを別のストリングで置換します。これは、文書処理プログラムでテキストを検索して、そのテキストを別のテキストで置き換えるのと同じことです。Matcher にはストリング要素を置換するための以下のメソッドがあります。

  • replaceAll()。すべての一致を、指定したストリングで置き換えます。
  • replaceFirst()。最初の一致だけを、指定したストリングで置き換えます。

Matcherreplace メソッドを使うのは簡単です。

String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("replacement");
Logger.getAnonymousLogger().info("After: " + result);

このコードは前の例のように wiki ワードを検索します。Matcher は一致を検出すると、その wiki ワードのテキストを置換テキストで置き換えます。このコードを実行すると、コンソールに以下の結果が表示されます。

Before: Here is WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is replacement followed by replacement, then replacement.

replaceFirst() を使用するとしたら、以下の結果になります。

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is a replacement followed by AnotherWikiWord, then SomeWikiWord.

グループのパターン・マッチと操作

正規表現パターンとの一致を検索すると、検出結果に関する情報を取得できます。この機能の一部は、Matcher での start() および end() メソッドですでに確認しましたが、グループをキャプチャーすることで、一致を参照することもできます。

パターン内でグループを作成するには、一般に、パターンの構成部分を括弧で囲みます。グループには左から右への方向に番号が 1 から付けられます (グループ 0 は一致全体を表します)。リスト 22 のコードは、各 wiki ワードを、そのワードを「ラップ」するストリングで置き換えます。

リスト 22. グループのパターン・マッチ
String input = "Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.";
Pattern pattern = Pattern.compile("[A-Z][a-z]*([A-Z][a-z]*)+");
Matcher matcher = pattern.matcher(input);
Logger.getAnonymousLogger().info("Before: " + input);
String result = matcher.replaceAll("blah$0blah");
Logger.getAnonymousLogger().info("After: " + result);

リスト 22 のコードを実行すると、コンソールに以下の結果が表示されます。

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
  then blahSomeWikiWordblah.

リスト 22 では、一致全体を参照するために、置換ストリングに $0 を組み込んでいます。$int という形の置換ストリングは、いずれの部分も、整数で識別されたグループを参照します ($1 はグループ 1 を参照するなどといった具合です)。つまり、$0matcher.group(0); に相当します。

この置換と同じ目標を、別のメソッドを使って達成することもできます。例えば、replaceAll() を呼び出すのではなく、以下のような置換を行うことも可能です。

StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
  matcher.appendReplacement(buffer, "blah$0blah");
}
matcher.appendTail(buffer);
Logger.getAnonymousLogger().info("After: " + buffer.toString());

上記のコードでも、同じ結果になります。

Before: Here is a WikiWord followed by AnotherWikiWord, then SomeWikiWord.
  After: Here is a blahWikiWordblah followed by blahAnotherWikiWordblah,
  then blahSomeWikiWordblah.

ジェネリクス

JDK 5.0 (2004 年にリリース) でのジェネリクスの導入により、Java 言語は大きな飛躍を遂げました。C++ のテンプレートを使用したことがあるとしたら、Java 言語でのジェネリクスはそれと似ていますが、まったく同じというわけではありません。C++ のテンプレートを使用したことがないとしても、心配する必要はありません。このセクションで、Java 言語でのジェネリクスの概要を説明します。

ジェネリクスとは何か?

JDK 5.0 で「ジェネリック型」(「ジェネリクス」) とこれに関連する構文が Java 言語に導入された際に、かつてお馴染みだった JDK クラスの一部が、それぞれに相当するジェネリクスで置き換えられました。ジェネリクスとはコンパイラー・メカニズムの 1 つであり、共通のコードを取り入れる一方で、残りは「パラメーター化」(または「テンプレート化」) することによって、汎用のクラスとインターフェース、そしてメソッドを作成 (および使用) するための手段です。このプログラミング手法は、「ジェネリック・プログラミング」と呼ばれています。

ジェネリクスの動作

ジェネリクスがどのような変化をもたらすかを確認するために、長きに渡って JDK に含まれているクラスの 1 つを例に取り上げます。それは、java.util.ArrayList という、配列に支えられた ObjectList です。

リスト 23 に、java.util.ArrayList をインスタンス化する方法を示します。

リスト 23. ArrayList のインスタンス化
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.

ご覧のように、ArrayList は異種混合のコレクションであり、2 つの String 型と 1 つの Integer 型が含まれています。JDK 5.0 がリリースされる前は、Java 言語にはこのような動作を制約するものは何もなかったため、コーディングの誤りが多発していました。例えばリスト 23 のコードには今のところ問題はなさそうですが、リスト 24 でのように ArrayList の要素にアクセスするとどうなるでしょうか?

リスト 24. ArrayList 内の要素へのアクセス試行
ArrayList arrayList = new ArrayList();
arrayList.add("A String");
arrayList.add(new Integer(10));
arrayList.add("Another String");
// So far, so good.
processArrayList(arrayList);
// In some later part of the code...
private void processArrayList(ArrayList theList) {
  for (int aa = 0; aa < theList.size(); aa++) {
    // At some point, this will fail...
    String s = (String)theList.get(aa);
  }
}

ArrayList の内容が事前にわからなければ、アクセスしたい要素をチェックして、その型を処理できるかどうかを確認する必要があります。そうでないと、ClassCastException がスローされる可能性があります。

ジェネリクスでは、ArrayList にすでに格納されている要素の型を指定できます。リスト 25 に、誤った型のオブジェクトを追加しようとする例 (行 3) とその結果を示します。

リスト 25. ジェネリクスを使用した 2 回目の試行
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A String");
arrayList.add(new Integer(10));// compiler error!
arrayList.add("Another String");
// So far, so good.
processArrayList(arrayList);
// In some later part of the code...
private void processArrayList(ArrayList<String> theList) {
  for (int aa = 0; aa < theList.size(); aa++) {
    // No cast necessary...
    String s = theList.get(aa);
  }
}

ジェネリクスを使用した繰り返し処理

Java 言語を拡張するジェネリクスの特殊な構文では、一般に要素を 1 つずつ処理していく必要がある List などのエンティティーに対処できます。例えば ArrayList を繰り返し処理するとしたら、リスト 25 のコードを以下のように作り直すことができます。

private void processArrayList(ArrayList<String> theList) {
  for (String s : theList) {
    String s = theList.get(aa);
  }
}

この構文は、Iterable であるオブジェクト (つまり、Iterable インターフェースを実装するオブジェクト) であれば、どのタイプのオブジェクトにも有効です。

パラメーター化されたクラス

パラメーター化されたクラスは、コレクションで威力を発揮します。したがって、以降の例では、コレクションがコンテキストとなります。一例として、順序付けされたオブジェクトのコレクションを表す List インターフェースを考えてください。最もよくある使用ケースでは、項目を List に追加した後、それらの項目にアクセスするために、インデックスを使用するか、あるいは List を繰り返し処理します。

クラスをパラメーター化することを考えているとしたら、以下の基準が当てはまるかどうかを検討する必要があります。

  • コアとなるクラスは、何らかのラッパーの中心にあること。クラスの中心にある「もの」は多様であり、「もの」を取り囲む機能 (例えば、属性) が同一であることを意味します。
  • 動作が共通していること。クラスの中心にある「もの」が何であろうと、ほとんど同じ処理を行うことを意味します。

この 2 つの基準を適用すると、コレクションは以下の必要条件を満たすことがわかります。

  • 「もの」が、コレクションを構成するクラスであること。
  • コレクションを構成するオブジェクトが何であろうと、処理 (追加削除サイズ調整、クリアなど) はほとんど同じであること。

パラメーター化されたリスト

ジェネリクスの構文では、List を作成するコードは以下のようになります。

List<E> listReference = new concreteListClass<E>();

Element を表す E が、前述の「もの」に相当します。concreteListClass は JDK に含まれるクラスであり、このクラスをインスタンス化します。JDK には複数の List<E> 実装がありますが、ここでは ArrayList<E> を使用します。ジェネリック・クラスとしては Class<T> も見かけるはずです。ここで、T は Type を表します。Java コードの中で E が使用されている場合、それは一般にある種のコレクションを参照しています。また、T が使用されている場合、それはパラメーター化されたクラスを表します。

したがって、例えば java.lang.IntegerArrayList を作成するとしたら、以下のようにします。

List<Integer> listOfIntegers = new ArrayList<Integer>();

SimpleList: パラメーター化されたクラス

次は、SimpleList というパラメーター化された独自のクラスを作成するとします。このクラスには、以下の 3 つのメソッドがあります。

  • add()。要素を SimpleList の末尾に追加します。
  • size()SimpleList に現在格納されている要素の数を返します。
  • clear()SimpleList の中身を完全にクリアします。

リスト 26 に、SimpleList をパラメーター化する構文を記載します。

リスト 26. SimpleList のパラメーター化
package com.makotojava.intro;
import java.util.ArrayList;
import java.util.List;
public class SimpleList<E> {
  private List<E> backingStore;
  public SimpleList() {
    backingStore = new ArrayList<E>();
  }
  public E add(E e) {
    if (backingStore.add(e))
    return e;
    else
    return null;
  }
  public int size() {
    return backingStore.size();
  }
  public void clear() {
    backingStore.clear();
  }
}

SimpleList は任意の Object サブクラスを使用してパラメーター化できます。例えば java.math.BigDecimal オブジェクトの SimpleList を作成して使用するには、以下のようにします。

package com.makotojava.intro;
import java.math.BigDecimal;
import java.util.logging.Logger;
import org.junit.Test;
public class SimpleListTest {
  @Test
  public void testAdd() {
    Logger log = Logger.getLogger(SimpleListTest.class.getName());
    
    SimpleList<BigDecimal> sl = new SimpleList<>();
    sl.add(BigDecimal.ONE);
    log.info("SimpleList size is : " + sl.size());
    sl.add(BigDecimal.ZERO);
    log.info("SimpleList size is : " + sl.size());
    sl.clear();
    log.info("SimpleList size is : " + sl.size());
  }
}

上記のコードからは以下の出力が生成されます。

Sep 20, 2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 1 Sep 20, 2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 2 Sep 20, 
2015 10:24:33 AM com.makotojava.intro.SimpleListTest testAdd 
INFO: SimpleList size is: 0

パラメーター化したメソッド

クラス全体をパラメーター化するのではなく、クラスのメソッドのうち 1 つか 2 つだけをパラメーター化したい場合もあります。その場合には、「ジェネリック・メソッド」を作成します。リスト 27 の例を見てください。ここでは、formatArray メソッドを使用して、配列に含まれる要素のストリング表現を作成しています。

リスト 27. ジェネリック・メソッド
public class MyClass {
// Other possible stuff... ignore...
  public <E> String formatArray(E[] arrayToFormat) {
    StringBuilder sb = new StringBuilder();

    int index = 0;
    for (E element : arrayToFormat) {
      sb.append("Element ");
      sb.append(index++);
      sb.append(" => ");
      sb.append(element);
      sb.append('\n');
    }

    return sb.toString();
  }
// More possible stuff... ignore...
}

MyClass をパラメーター化するのではなく、任意の要素型に使用できる一貫したストリング表現を作成するために使用する 1 つのメソッドだけをジェネリック・メソッドにします。

実務では、メソッドよりもパラメーター化したクラスとインターフェースを使用する場合のほうが多くなることに気づくと思いますが、これで、必要な場合にはパラメーター化の機能を利用できることがわかったはずです。

enum 型

JDK 5.0 で、Java 言語に追加された新しいデータ型には、(java.util.Enumeration と混同しないように) enum という名前が付けられています。enum 型が表す不変のオブジェクトのセットでは、すべてのオブジェクトが 1 つの特定の概念に関連する一方、それぞれのオブジェクトがそのセット内で固有の定数値を表します。enum が導入される前の Java 言語では、ある概念 (例えば、性別) の定数値のセットを以下のように定義していました。

public class Person {
  public static final String MALE = "male";
  public static final String FEMALE = "female";
  public static final String OTHER = "other";
}

特定の定数値を参照する必要があるコードを作成するには、以下の方法が採られていました。

public void myMethod() {
  //. . .
  String genderMale = Person.MALE;
  //. . .
}

enum を使用して定数を定義する場合

enum 型を使用すると、定数の定義方法がより形式的になります。つまり、より効果的に定数を定義できるということです。以下に、enum で定義した Gender のバージョンを記載します。

public enum Gender {
  MALE,
  FEMALE,
  OTHER
}

この例は、enum の機能を表面的にかじっているだけに過ぎません。実際、enum はクラスとよく似ていて、コンストラクター、属性、メソッドを持つことができます。

package com.makotojava.intro;

public enum Gender {
  MALE("male"),
  FEMALE("female"),
  OTHER("other");

  private String displayName;
  private Gender(String displayName) {
    this.displayName = displayName;
  }

  public String getDisplayName() {
    return this.displayName;
  }
}

クラスと enum の唯一の違いは、enum のコンストラクターは private として宣言する必要があり、enum の間での継承はできないという点です。ただし、enum でインターフェースを実装することはできます

インターフェースを実装する enum

例えば、以下の Displayable インターフェースを定義するとします。

package com.makotojava.intro;
public interface Displayable {
  public String getDisplayName();
}

Gender の enum (および、わかりやすい表示名を生成する必要のあるその他すべての enum) では、上記のインターフェースを以下のように実装できます。

package com.makotojava.intro;

public enum Gender implements Displayable {
  MALE("male"),
  FEMALE("female"),
  OTHER("other");

  private String displayName;
  private Gender(String displayName) {
    this.displayName = displayName;
  }
  @Override
  public String getDisplayName() {
    return this.displayName;
  }
}

入出力

このセクションでは、java.io パッケージの概要を説明します。このパッケージに含まれるツールを使用して、各種のソースからデータを収集し、処理する方法を学んでください。

外部データを処理する

大抵、Java プログラム内で使用するデータは外部データ・ソースにあります。例えばデータベースからデータを収集したり、ソケットを介してバイトを直接転送したり、ファイル・ストレージから取り出したりするなどです。外部データを収集および処理するための Java ツールのほとんどは、java.io パッケージに含まれています。

ファイル

Java アプリケーションで使用できるすべてのデータ・ソースのうち、最もよく使われていて、通常は最も扱いやすいのはファイルです。アプリケーション内でファイルを読み取るには、「ストリーム」を使用して、そこに着信したバイトを Java 言語の型に解析する必要があります。

java.io.File は、ファイル・システム上のリソースを定義して、そのリソースを抽象的に表現するクラスです。以下のように、File オブジェクトは簡単に作成することができます。

File f = new File("temp.txt");

File f2 = new File("/home/steve/testFile.txt");

File コンストラクターは、作成するファイルの名前を取ります。上記の最初の呼び出しでは、指定のディレクトリー内に temp.txt という名前のファイルを作成します。2 番目の呼び出しでは、Linux システム上の特定の場所にファイルを作成します。File のコンストラクターには、使用しているオペレーティング・システムにとって有効なファイル名である限り、任意の String を渡すことができます。その String で参照しているファイルが存在しているかどうかも問われません。

以下のコードは、新しく作成された File オブジェクトに対し、該当するファイルが存在するかどうかを照会します。

File f2 = new File("/home/steve/testFile.txt");
if (f2.exists()) {
  // File exists. Process it...
} else {
  // File doesn't exist. Create it...
  f2.createNewFile();
}

java.io.File には、他にも以下の目的で使用できる重宝なメソッドがあります。

  • ファイルを削除する
  • ディレクトリーを作成する (File のコンストラクターにディレクトリー名を引数として渡します)
  • リソースがファイルであるか、ディレクトリーであるか、あるいはシンボリック・リンクであるかを判断する
  • その他多数

Java I/O の主なアクションは、データ・ソースに対する書き込みおよび読み取り処理の際に実行されます。そこで必要になってくるのが、ストリームです。

Java I/O 内でストリームを使用する

ファイル・システム上のファイルには、ストリームを使用することでアクセスできます。最も下位のレベルでは、ストリームはプログラムがソースからのバイトを受信すること、または宛先に出力を送信することを可能にします。一部のストリームはあらゆる類の 16 ビット文字を処理します (Reader 型と Writer 型)。そのほかのストリームが処理するのは 8 ビットのバイトだけです (InputStream 型と OutputStream 型)。これらの階層に含まれる各種のフレーバーのストリームはすべて、java.io パッケージにあります。

バイト・ストリームは 8 ビットのバイトの読み取り (InputStream とそのサブクラス) および書き込み (OutputStream とそのサブクラス) を処理します。つまり、バイト・ストリームは、そのままの形に近いストリームであると見なすことができます。以下に、よく使われる 2 つのバイト・ストリームとそれぞれの用途を要約します。

  • FileInputStream / FileOutputStream: ファイルからバイトを読み取り、ファイルにバイトを書き込みます。
  • ByteArrayInputStream / ByteArrayOutputStream: インメモリー配列からバイトを読み取り、インメモリー配列にバイトを書き込みます。

文字ストリーム

文字ストリームは 16 ビット文字の読み取り (Reader とそのサブクラス) および書き込み (Writer とそのサブクラス) を処理します。以下に、文字ストリームを抜粋し、それぞれの用途を説明します。

  • StringReader / StringWriter: メモリー内の String に対して文字の読み取り/書き込みを行います。
  • InputStreamReader / InputStreamWriter (および FileReader / FileWriter サブクラス): バイト・ストリームと文字ストリームとの間の橋渡し役を果たします。Reader のフレーバーは、バイト・ストリームからバイトを読み取り、それらのバイトを文字に変換します。Writer のフレーバーは、文字をバイトに変換して、バイト・ストリームに乗せられるようにします。
  • BufferedReader / BufferedWriter: 別のストリームの読み取り/書き込み処理中にデータをバッファーに入れて、読み取り/書き込み処理をより効率的に行えるようにします。

ここではストリームのすべてを取り上げるのではなく、ファイルの読み取りと書き込み処理に使用するのに推奨されるストリームに焦点を絞ります。ほとんどの場合、それに該当するのは文字ストリームです。

ファイルからの読み取り

ファイルから読み取る方法はいくつかありますが、そのうち最も単純なのは、ほぼ間違いなく以下の方法です。

  1. 読み取る対象の File に対して InputStreamReader を作成します。
  2. ファイルの終わりに達するまで、read() を呼び出して文字を 1 つずつ読み取ります。

リスト 28 に、File から読み取る例を記載します。

リスト 28. ファイルからの読み取り
public List<Employee> readFromDisk(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file))) {

    StringBuilder sb = new StringBuilder();
    int numberOfEmployees = 0;
    int character = reader.read();
    while (character != -1) {
        sb.append((char)character);
        character = reader.read();
    }
    log.info("Read file: \n" + sb.toString());
    int index = 0;
    while (index < sb.length()-1) {
      StringBuilder line = new StringBuilder();
      while ((char)sb.charAt(index) != '\n') {
        line.append(sb.charAt(index++));
      }
      StringTokenizer strtok = new StringTokenizer(line.toString(), Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      index++;
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

ファイルへの書き込み

File から読み取る場合と同じく、File に書き込むにはいくつかの方法があります。書き込みの場合も、以下の方法が最も簡単です。

  1. 書き込む対象の File に対して FileOutputStream を作成します。
  2. write() を呼び出して文字を順に書き込みます。

リスト 29 に、File に書き込む例を記載します。

リスト 29. ファイルへの書き込み
public boolean saveToDisk(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

ストリームのバッファリング

文字ストリームを 1 文字ずつ読み取ったり書き込んだりするのは非効率的なので、ほとんどの場合はバッファーに入れられた I/O を使用することになるはずです。バッファー I/O を使用してファイルから読み取る方法はリスト 28 に示した方法と基本的に同じですが、リスト 30 に示されているように、InputStreamReaderBufferedReader 内にラップするという点が異なります。

リスト 30. バッファー I/O を使用したファイルからの読み取り
public List<Employee> readFromDiskBuffered(String filename) {
  final String METHOD_NAME = "readFromDisk(String filename)";
  List<Employee> ret = new ArrayList<>();
  File file = new File(filename);
  try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
    String line = reader.readLine();
    int numberOfEmployees = 0;
    while (line != null) {
      StringTokenizer strtok = new StringTokenizer(line, Person.STATE_DELIMITER);
      Employee employee = new Employee();
      employee.setState(strtok);
      log.info("Read Employee: " + employee.toString());
      ret.add(employee);
      numberOfEmployees++;
      // Read next line

      line = reader.readLine();
    }
    log.info("Read " + numberOfEmployees + " employees from disk.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

バッファー I/O を使用してファイルに書き込む場合も同様です。つまり、リスト 31 に示されているように、OutputStreamWriterBufferedWriter 内にラップします。

リスト 31. バッファー I/O を使用したファイルへの書き込み
public boolean saveToDiskBuffered(String filename, List<Employee> employees) {
  final String METHOD_NAME = "saveToDisk(String filename, List<Employee> employees)";
  
  boolean ret = false;
  File file = new File(filename);
  try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) {
    log.info("Writing " + employees.size() + " employees to disk (as String)...");
    for (Employee employee : employees) {
      writer.write(employee.getState()+"\n");
    }
    ret = true;
    log.info("Done.");
  } catch (FileNotFoundException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
  } catch (IOException e) {
    log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
  }
  return ret;
}

Java シリアライズ

Java シリアライズも、Java プラットフォームに不可欠なライブラリーの 1 つです。シリアライズを使用する目的は、主にオブジェクトの永続化とオブジェクトのリモート処理です。この 2 つの使用ケースでは、オブジェクトの状態のスナップショットを取り、後で再構築できるようにする必要があります。このセクションでは、Java Serialization API の特徴を紹介し、プログラムでこの API を使用する方法を説明します。

オブジェクトのシリアライズとは何か?

「シリアライズ」とは、オブジェクトの状態とメタデータ (オブジェクトのクラス名や属性の名前など) を特殊なバイナリー・フォーマットで保管するプロセスのことです。オブジェクトをこの特殊なフォーマットに整形する (シリアライズする) ことで、必要に応じていつでもオブジェクトを再構築 (デシリアライズ) するために必要なすべての情報を保存します。

オブジェクト・シリアライズの主な使用ケースには、以下の 2 つがあります。

  • オブジェクトの永続化: オブジェクトの状態を恒久的な永続化メカニズム (データベースなど) に保管します。
  • オブジェクトのリモート処理: オブジェクトを別のコンピューターやシステムに送信します。

java.io.Serializable

シリアライズを機能させる際の最初のステップは、オブジェクトがこのメカニズムを使用できるようにすることです。シリアライズ可能にするすべてのオブジェクトは、以下のように java.io.Serializable というインターフェースを実装している必要があります。

import java.io.Serializable;
public class Person implements Serializable {
  // etc...
}

上記の例では、Serializable インターフェースがランタイムに対し、Person クラス (および Person クラスのすべてのサブクラス) のオブジェクトを serializable としてマークしています。

Java ランタイムはオブジェクトをシリアライズするときに、オブジェクトがシリアライズ可能でないと、そのオブジェクトの属性に対して NotSerializableException をスローします。この動作は、transient キーワードを使って特定の属性をシリアライズしないようランタイムに指示することで管理できます。その場合には、オブジェクトが正常に機能するよう、開発者が自己責任で、それらの属性が (必要に応じて) 復元されることを確実にしなければなりません。

オブジェクトをシリアライズする

ここで、これまでに学んだ Java I/O の知識とシリアライズについて学んだ知識を 1 つにまとめた例に取り組みます。

Employee オブジェクトの List を作成して、そこに要素を取り込んだ後、その List を (この例ではファイルへの) OutputStream にシリアライズするとします。このプロセスをリスト 32 に示します。

リスト 32. オブジェクトのシリアライズ
public class HumanResourcesApplication {
  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();
  
  public static List<Employee> createEmployees() {
    List<Employee> ret = new ArrayList<Employee>();
    Employee e = new Employee("Jon Smith", 45, 175, 75, "BLUE", Gender.MALE, 
       "123-45-9999", "0001", BigDecimal.valueOf(100000.0));
    ret.add(e);
    //
    e = new Employee("Jon Jones", 40, 185, 85, "BROWN", Gender.MALE, "223-45-9999", 
       "0002", BigDecimal.valueOf(110000.0));
    ret.add(e);
    //
    e = new Employee("Mary Smith", 35, 155, 55, "GREEN", Gender.FEMALE, "323-45-9999", 
       "0003", BigDecimal.valueOf(120000.0));
    ret.add(e);
    //
    e = new Employee("Chris Johnson", 38, 165, 65, "HAZEL", Gender.UNKNOWN, 
       "423-45-9999", "0004", BigDecimal.valueOf(90000.0));
    ret.add(e);
    // Return list of Employees
    return ret;
  }
  
  public boolean serializeToDisk(String filename, List<Employee> employees) {
    final String METHOD_NAME = "serializeToDisk(String filename, List<Employee> employees)";
    
    boolean ret = false;// default: failed
    File file = new File(filename);
    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) {
      log.info("Writing " + employees.size() + " employees to disk (using Serializable)...");
      outputStream.writeObject(employees);
      ret = true;
      log.info("Done.");

    } catch (IOException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
    }
    return ret;
  }

最初のステップは、オブジェクトを作成することです。そのために、いくつかの属性値を設定するように特化された Employee のコンストラクターを使用して、createEmployees() 内でオブジェクトを作成しています。続いて、OutputStream (この例では FileOutputStream) を作成し、そのストリームに対して writeObject() を呼び出します。writeObject() は、Java シリアライズを使用してオブジェクトをストリームにシリアライズするメソッドです。

この例では、List オブジェクト (およびそこに含まれる Employee オブジェクト) をファイルに保管していますが、どのタイプのシリアライズでも、これと同じ手法を使用します。

リスト 32 のコードを動作させるには、以下のように JUnit テストを使用するという方法があります。

public class HumanResourcesApplicationTest {

  private HumanResourcesApplication classUnderTest;
  private List<Employee> testData;
  
  @Before
  public void setUp() {
    classUnderTest = new HumanResourcesApplication();
    testData = HumanResourcesApplication.createEmployees();
  }
  @Test
  public void testSerializeToDisk() {
    String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser";
    boolean status = classUnderTest.serializeToDisk(filename, testData);
    assertTrue(status);
  }

}

オブジェクトをデシリアライズする

オブジェクトのシリアライズにおいて最も重要な点は、オブジェクトを再構築 (デシリアライズ) できるようにすることにあります。リスト 33 のコードは、シリアライズしたファイルを読み取り、その内容をデシリアライズするために、Employee オブジェクトからなる List の状態を復元します。

リスト 33. オブジェクトのデシリアライズ
public class HumanResourcesApplication {

  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();
  
  @SuppressWarnings("unchecked")
  public List<Employee> deserializeFromDisk(String filename) {
    final String METHOD_NAME = "deserializeFromDisk(String filename)";
    
    List<Employee> ret = new ArrayList<>();
    File file = new File(filename);
    int numberOfEmployees = 0;
    try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
      List<Employee> employees = (List<Employee>)inputStream.readObject();
      log.info("Deserialized List says it contains " + employees.size() + 
         " objects...");
      for (Employee employee : employees) {
        log.info("Read Employee: " + employee.toString());
        numberOfEmployees++;
      }
      ret = employees;
      log.info("Read " + numberOfEmployees + " employees from disk.");
    } catch (FileNotFoundException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
         file.getName() + ", message = " + e.getLocalizedMessage(), e);
    } catch (IOException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
    } catch (ClassNotFoundException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "ClassNotFoundException, 
         message = " + e.getLocalizedMessage(), e);
    }
    return ret;
  }
  
}

この場合も、リスト 33 のコードを動作させるには、以下に示すような JUnit テストを使用できます。

public class HumanResourcesApplicationTest {
  
  private HumanResourcesApplication classUnderTest;
  
  private List<Employee> testData;
  
  @Before
  public void setUp() {
    classUnderTest = new HumanResourcesApplication();
  }
  
  @Test
  public void testDeserializeFromDisk() {
    String filename = "employees-Junit-" + System.currentTimeMillis() + ".ser";
    int expectedNumberOfObjects = testData.size();
    classUnderTest.serializeToDisk(filename, testData);
    List<Employee> employees = classUnderTest.deserializeFromDisk(filename);
    assertEquals(expectedNumberOfObjects, employees.size());
  }

}

ほとんどのアプリケーションで、シリアライズに関して注意が必要となるのは、オブジェクトを serializable としてマークすることだけです。オブジェクトを明示的にシリアライズおよびデシリアライズしなければならない場合には、リスト 32リスト 33 に示した手法を使用できます。けれどもアプリケーションのオブジェクトが発展して、オブジェクトの属性を追加したり削除したりするうちに、シリアライズはアプリケーションに新しい複雑さを重ねることになります。

serialVersionUID

初期の頃のミドルウェアおよびリモート・オブジェクトの通信では、オブジェクトの「ワイヤー・フォーマット」を制御する責任は主に開発者にあったため、テクノロジーが進化し始めてからというもの、開発者の頭痛の種が尽きることはありませんでした。

例えば、ある属性をオブジェクトに追加してからコードを再コンパイルし、そのコードをアプリケーション・クラスター内のすべてのコンピューターに再配布したとします。オブジェクトが保管されているコンピューター上ではシリアライズ・コードの 1 つのバージョンだけを使用しているとしても、そのオブジェクトにアクセスする他のコンピューターは別のバージョンのシリアライズ・コードを使用している可能性があります。それらのコンピューターがオブジェクトをデシリアライズしようとすると、ほとんどの場合はエラーが発生します。

Java シリアライズの高度なメタデータ (バイナリー・シリアライズ・フォーマットで組み込まれる情報) は、初期のミドルウェア開発者を悩ませていた問題の多くを解決します。けれども、これによってすべての問題を解決できるわけではありません。

Java シリアライズでは serialVersionUID という名前のプロパティーを使用して、シリアライズのシナリオ内で異なるバージョンのオブジェクトを処理できるようになっています。このプロパティーをオブジェクト上で宣言する必要はありません。デフォルトでは、Java プラットフォームが使用するアルゴリズムによって、クラスの属性、クラス名、そしてローカル・クラスターでの位置を基に、このプロパティーの値が計算されます。このアルゴリズムは大抵の場合は問題なく機能しますが、属性を追加または削除して、動的に生成される値が変わると、Java ランタイムが InvalidClassException をスローします。

このような事態を防ぐために、serialVersionUID を明示的に宣言する習慣を身につけてください。これを宣言する方法は以下のとおりです。

import java.io.Serializable;
  public class Person implements Serializable {
  private static final long serialVersionUID = 20100515;
  // etc...
  }

serialVersionUID バージョン番号には、何らかの方式を採用することを推奨します (上記の例では、現在の日付を使用しました)。また、serialVersionUIDprivate static final として宣言し、型を long に指定する必要があります。

どのような場合にこのプロパティーを変更すべきなのかと言うと、その簡潔な答えは、互換性のない変更をクラスに加えたときは必ずこのプロパティーを変更する必要がある、です。これは通常、属性を追加または変更した場合を意味します。あるコンピューター上に、属性が追加または削除されたバージョンのオブジェクトがあり、その属性が追加されていない、または削除されていないバージョンのオブジェクトを使用しているリモートのコンピューター上にこのオブジェクト型のデータを送ると、問題のある事態になります。そこで役立つのが、Java プラットフォームに組み込まれている serialVersionUID チェックです。

経験則として、クラスの機能 (つまり属性やその他のインスタンス・レベルの状態変数) を追加または削除するときは、必ずそのクラスの serialVersionUID を変更することをお勧めします。互換性のないクラスの変更によってアプリケーションにバグが発生するよりも、通信先で java.io.InvalidClassException エラーを受け取るほうがましです。

第 2 回のまとめ

この「Java プログラミング入門」チュートリアルでは、Java 言語のかなりの部分を取り上げましたが、Java 言語は壮大な言語です。1 つのチュートリアルでそのすべてを網羅することはできません。

Java 言語とプラットフォームについて学習を続ける中で、正規表現やジェネリクス、Java シリアライズなどの特定のトピックをさらに詳しく調査したいと思うようになるはずです。最終的には、この入門者向けチュートリアルでは取り上げなかったトピックについても詳しく調べることをお勧めします。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=1049938
ArticleTitle=Java プログラミング入門、第 2 回: 実際のアプリケーションに対応するための構成体
publish-date=09212017