3-1. 継承クラスの作り方

プログラムの抽象度

プログラムの鉄則として、具体的なものをなるべくプログラムに含めない ようにします。 例えば、定数は、定数の名前をつけて定義しておいてから、その名前をプ ログラムで使います。 それが、プログラムの可読性や保守性を向上させます。

Java ではプログラムは必ず Class の中のメソッド内に作ります。 すると、特定のプログラムは クラス名とメソッド名により抽象化されま す。 但し、カプセル化により、外部的にはプログラムそのものが見えるわけで はなく、クラス名、メソッド名により抽象化されます。

クラスを継承すると、継承クラスは親クラスのメソッドを引き継ぎます。


class A {
    public String name(){
        return "A";
    }
}
class B extends A {
}

また、親クラスのメソッドをオーバライドした場合、子クラスのメソッド が有効になります。


class A {
    public String name(){
        return "A";
    }
}
class B extends A {
    @Override
    public String name(){
        return "B";
    }
}

したがって、変数としては親クラスで定義していても子クラスのメソッド を実行することができます。


   B x1 = new B();  // B      
   A x1 = new A();  // A    
   A x1 = new B();  // B    

必ず子クラスでオーバライドするメソッドは、親クラスでメソッドを実装 する必要がありません。


abstract class A { // abstract メソッドがあるクラスは abstract 宣言が必要
    public abstract String name(); //実装しないためには abstract 宣言する
}
class B extends A {
    @Override
    public String name(){
        return "B";
    }
}

同じことを C++ 言語で書くとつぎのようになります。


#include <string>
class A {
public:
  virtual std::string name()=0;
};
class B :public A {
  virtual std::string name(){
    return "B";
  }
};

テンプレートデザインパターン

定数を返す子クラスのメソッドを用いた、子クラス共通のメソッドを親ク ラスに置くことを考えます。 この時、親クラスでは、実装されていない抽象メソッドを用いた式を使っ て、新しいメソッドを作ります。


abstract class A { 
    public abstract String name(); 
    public String iam(){
        return "I am "+name()+".";
    }
}
class B extends A {
    @Override
    public String name(){
        return "B";
    }
}
class C extends A {
    @Override
    public String name(){
        return "C";
    }
}

このように子クラスで実装する抽象メソッドを使って、値を計算するメソッ ドを作ることをテンプレートデザインパターンと言います。

Java 言語の Interface

メソッドを宣言しておけば、実装しなくても変数宣言や、テンプレートデ ザインパターンのような他のメソッドを作ることができます。 Java では public abstract 宣言のみのクラスを interface という宣言 で作ることができます。 Java 7 までは interface にメソッドを作ることはできませんでしたので、 クラスを作る際には、 interface を作り、共通部分のメソッドを実装し たクラスと、それを継承した具体的なクラスの三層構造で作ると良いです。


interface A { 
    String name(); 
    String iam();
}
abstract class AbstractA implements A { 
// name メソッドを実装していないので abstract 宣言
    @Overide
    public String iam(){
        return "I am "+name()+".";
    }
}
class B extends AbstractA {
    @Override
    public String name(){
        return "B";
    }
}
class C extends AbstractA {
    @Override
    public String name(){
        return "C";
    }
}

Java の暗黙

Java はクラス宣言すると、 java.lang.Object クラスを暗黙で継承しま す。


class A {  
}
class A extends java.lang.Object {  
} // 等価な宣言

また、引数なしのコンストラクタが暗黙に宣言されます。


class A {  
}
class A extends java.lang.Object {  
   public A(){}
} // 等価な宣言

さらに、すべてのコンストラクタの冒頭で、親クラスのコンストラクタをし なくても、暗黙で引数抜きで呼び出されます。


class A {  
}
class A extends java.lang.Object {  
    public A(){
        super();
    }
} // 等価な宣言

性質としての interface

interface や class を継承していると(implements, extends)、特定の処理 が可能になります。 したがって、継承はそのクラスの性質を表します。

特にまずよく使われる、オブジェクト同士が比較可能であることを示す java.lang.Comparable<T> インタフェースについて学ぶことにしま す。

演習3-1

作成したプログラムを、学籍番号順に表示するように改良したい。

  1. 	  
    Arrays.stream(list)
    	.sorted()
    	.forEach(System.out::println);
    
    リストを sort で並べ替えてから表示するようにする。
  2. 	  
    package spro2;
    public interface A extends Comparable<A> {
    	String name();
    	String id();
    }
    

    オブジェクトが並び替えられる条件は Java では java.lang.Comparable を実装していることである。

    そのため、インターフェイス A に Comparable を継承させる

  3. 比較を実施するプログラムをすべてのクラスの共通部分である AbstractA クラスに実装する。 実装しなければならないメソッドは以下の 3 つである。

    • bool equals(Object o)
    • int hashCode()
    • int compareTo(A a)
  4. これらのメソッドの雛形を得るために、ダミーで AbstractA の中にフィー ルド Strng a; を仮に作る
  5. compareTo の雛形を作るには、 Comparable のメソッドなので Source→Override/inherit Methods を選び、 compareTo のみをチェックして Ok を押します
  6. Source → Generate hashCode() and equals() を選び Ok を押す
  7. ここでセーブするとエラーがすべて消えることを確認する。
  8. ここで一回 commit しておく

演習3-2

次回、次次回の導入

テスト環境を作ります。

  1. src フォルダを右クリックして new → package で test パッケージ を作る。
  2. A.java を右クリックして new→Other→Java→JUnit→JUnit Test Case を選び next
  3. Package を test にし、 setUp をチェックして next
  4. テストを作るメソッドとして compareTo をチェックして Finish する
  5. 以下のプログラムを入れる
    
    package test;
    import static org.junit.jupiter.api.Assertions.*;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import spro2.A;
    import spro2.A998;
    import spro2.A999;
    class ATest {
    	private A999 a999;
    	private A998 a998;
    
    	@BeforeEach
    	void setUp() throws Exception {
    		a999 = new A999();
    		a998 = new A998();
    	}
    
    	@Test
    	void testCompareTo() {
    		assertTrue(a998.compareTo(a999)<0);
    		assertTrue(a998.compareTo(a998)==0);
    		assertTrue(a999.compareTo(a999)==0);
    		assertTrue(a999.compareTo(a998)>0);
    	}
    	@Test
    	void testEquals() {
    		assertFalse(a998.equals(null));
    		assertFalse(a998.equals("99ec999"));
    		assertTrue(a998.equals(a998));
    		assertFalse(a998.equals(a999));
    	}
    	private boolean hashCodeCondition(A a1, A a2) {
    		return !a1.equals(a2)||(a1.hashCode()==a2.hashCode());
    	}
    	@Test
    	void testHashCode() {
    		assertTrue(hashCodeCondition(a998,a998));
    		assertTrue(hashCodeCondition(a998,a999));
    		assertTrue(hashCodeCondition(a999,a998));
    		assertTrue(hashCodeCondition(a999,a999));
    	}
    }
    
  6. ここで一旦コミットする

3-2. 比較

次にオブジェクトの比較について考えます。 同一メモリアドレス上にある、つまり、まさに参照先が同じかどうかを判定するには == を使用します。

しかし、文字列など、異なるメモリ上にあっても 内容が等しければ等しいと判断したいときがあります。 そのため java.lang.Object には equals というメソッドがあります。 このメソッドを各オブジェクトクラスの性質に応じてオーバライドして使用します。 equals を実装する際もコピーと同様に浅く比較するか、深く比較するかを検 討する必要があります。 なお、配列では java.util.Arrays に静的なメソッドとして equals と deepEquals が実装されています。

equals

equals は数学でいう同値関係を満たす必要があります。つまり次 の三条件を満たさなければなりません。

null でない Object a,b,c に対しては、次を満たします。

反射律
a.equals(a)は true
対称律
a.equals(b)b.equals(a) は一致する
推移律
a.equals(b)b.equals(c) がともに true で あるなら、 a.equals(c) も true

この上、プログラミング的な制約として、オブジェクトが変化しない限りは何 度 equal を呼んでも結果が変化しないという整合性(初回だけ true で 2 回 目以降は false というのはダメ)と、 x.equals(null) は常に false でなければならないという条件が付加されてます。

以上挙げた条件を実際に満たすことはそれほど厳しいことではないので、 いろいろな条件の equals を考えることができます。 例えば、「ある条件が成立する同一グループ」は大抵この性質を持っています。 つまり、例えば数を比較する場合、「値が完全に等しい」という条件の他に、 「3 で割った余りが等しい整数」なども上記の条件を満たします。 但し、 equals の字面は「等しい」を意味しますので、等しさからあまり直感 的に外れた定義をすると、プログラムが作りにくくなったり読みづらくなった りしますので、注意が必要です。

例3-1

オンラインショッピングなどで顧客データを扱う際、通常顧客の ID で全て識 別します。 そのため、 equals の実装としてふさわしいのは次のように顧客の ID の比較 になります。


class Customer {
  private String id;
...
  /**
  * 顧客の等価性を返します
  * @param 顧客オブジェクト
  * @return 同一の顧客かどうか
  */
  public boolean equals(Customer customer){
    return this.id.equals(customer.id);
  }
}

なお、java.lang.Object における equals の引数は java.lang.Object なの で、この equals は Override にはなりません。 厳密に java.lang.Object の equals 関数を Override するためには、引数を Object 型にしなければなりません。 引数を Object に許すと、引数に対してそのまま Customer のメソッドを適用 できなくなります。つまり、キャストする必要があります。


/*
問題あり
*/
class Customer {
  private String id;
...
  @Override
  public boolean equals(Object o){
    Customer customer = (Customer) o;
    return this.id.equals(customer.id);
  }
}

ただし、このままでは equals の引数に null が与えられた場合や Customer クラスとは異なるオブジェクトが与えられた時に false が返りません。 さて、 Eclipse にはこの equals を自動生成する機能があります。 Customer に関して Eclipse に生成させると、次のようなプログラムを生成し ます。


  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    final Customer other = (Customer) obj;
    if (id == null) {
      if (other.id != null)
        return false;
    } else if (!id.equals(other.id))
      return false;
    return true;
  }

このようにかなり複雑な条件式になります。 このうち、最初の条件は単なる高速化のためですが、それ以降は、引数 obj とインスタンス変数 id に関して次のような条件を考えています。

obj == null
false
obj が Customer 以外のオブジェクト
false
ここでやっと obj をキャストしてもエラーにならない。
id == null
なんと obj.id == null を返す。つまり両方共 null なら true、一方だ けが null なら false
id が null でないことが分かる
ここで、やっと id.equals(obj.id) が返せる

なお、 getClass を用いる代わりに instanceof 演算子を使うと、 null のチェックとクラスのチェックが同時にできます。 これを用いて Eclipse の記述をより短くすると次のようになります。


  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Customer)) return false;
    final Customer other = (Customer) obj;
    if (id == null) return other.id==null;
    return id.equals(other.id);
  }

最初の if 文以外はガード文になっていて、そこで return しないと、次のス テップで必ずランタイムエラーが出ることに注意してください。 つまり、obj が Customer クラスのインスタンスで無い限りキャストできませ んし、id が null では equals メソッドを呼べないということです。

hashCode

さて、等しいという条件として別の条件を考えてみましょう。 例えば、顧客 ID は異なっていても、名前と誕生日とお届先が一致して いるような顧客は本当に異なっているのでしょうか? コンピュータは条件さえそろえられれば、大量の顧客リストに関して、同一人 物の可能性のあるような顧客 ID リストなどを簡単に作ることができます。 誕生日自体は個人を特定しませんが、「異なっていれば別人」という情報を持っ ています。 電子マネーの ID や、レシート番号など、直接は個人情報にあたらないもので も大量に入手して処理することで個人を特定できることがあります。 このような情報処理を紐付けと呼びます。 個人情報保護が立法化され、社会的に個人情報の取扱いに注意が払われるよう になりました。 一方で、コンピュータの情報処理により、単独で は無意味な情報が個人の特定に大きく関与するようになりました。 そのため、法的には個人情報に当たらない情報でも、複数そろうと個人を特定で きてしまうこともあります。 そのため、 データベースなどを作る際には、情報を取り扱う責任や障害時の被害の軽減な ども考え、「責任の明確化」や「不必要な情報を採取しない」などの対応を考 える必要があります。

さて、話がそれました。 再び、「等しさ」の話に戻します。 java.lang.Object の equals の説明には hashCode の実装をするように書かれています。 この hashCode(ハッシュ値) というのは各オブジェクトから作成 する長さの決まった値です。 Java では int 型、つまり 32bit になります。 このハッシュ値は equals の性質を一部受け継いでいます。 つまり、Object a,b に対して、 a.equals(b) が true である なら、 a.hashCode() == b.hashCode() も成り立たなければな らないということです。 これは a.equals(b) が false である場合には、何も規定して ません。 対偶を考えると、a.hashCode()b.hashCode() が異なる場合は必ず a.equals(b) は false になるということ です。 これは先ほどお話した誕生日がもつ条件と同じです。 「同一人物であれば誕生日が等しい」ということは、「誕生日が異なれば同一 人物では無い」ということです。 このようにハッシュ値というのはオブジェクトを表す一部の情報になります。 そして、equals は自由に設定できるという説明をしましたが、その場合、そ の equals に連動したハッシュ値、つまり equals が true になるようなオブ ジェクトに対しては常にハッシュ値が等しくなるようにする必要があります。

例3-2

例3-1 では顧客の等価条件を顧客の ID と定めましたので、 hashCode は顧客 ID のハッシュ値を返すようにします。


class Customer {
  private String id;
...
  /**
  * 顧客の等価性を返します
  * @param 顧客オブジェクト
  * @return 同一の顧客かどうか
  */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Customer)) return false;
    final Customer other = (Customer) obj;
    if (id == null) return other.id==null;
    return id.equals(other.id);
  }
  /**
  * 顧客のハッシュ値を返します。
  * これは顧客ID のハッシュ値と等しいです。
  * @return ハッシュ値
  */
  @Override public int hashCode(){
    return id.hashCode();
  }
}

なお、例えば、名前と生年月日が等しい時に equals が true を返すという場 合、どのように hashCode を返せば良いでしょうか? 複数のデータを持つオブジェクトの代表格として文字列型 java.lang.String があります。 このオブジェクトの hashCode はマニュアルによると次のように計算されます。

s 0 * 31 n - 1 + s 1 * 31 n - 2 + ... + s n - 1
= s 0 * 31 + s 1 * 31 + ... * 31 + s n - 1

このように、複数のデータに対してハッシュコードを計算するには、 直前の値 x とデータ値 y に対して 31*x + y という値を与えています。

そのほかの与え方として、 java.lang.Long (64 bit の整数)では、上位 32 bit と下位 32 bit の排他的論理和を取っています。 排他的論理和は一方が 1 bit 異なれば、値になります。しかし、二つの同じ 値に対して排他的論理和をとると 0 になってしまうという性質もあります。 64bit の整数に対して、上位と下位が一致してしまうというのはあまり考えら れない状況ですが、文字列に関しては 1 文字目と 2 文字目が一致することは 十分考えられます。 したがって、複数のオブジェクトのハッシュコードの算出方法は少なくとも次 の二通りが使われています。

オブジェクトの性質 演算 代表的な例
二つが一致する可能性が低い 排他的論理和 x^y java.lang.Long, java.lang.Double
その他 31*x + y java.lang.String など

異なる要素に対してなるべく異なる hashCode を用意すると、hashCode を使 うようなアプリケーションで検索などが高速になります。

なお、 Eclipse ではこのようなルールに基づいて equals と hashCode を自 動的に生成します。

補足

なぜ 31 という特定の値を使うのかですが、 ひとつは 2 の 5 乗 - 1 という計算のしやすい値であることです。 もうひとつは以下の通りです。

まず、 31 の代わりに変数 x を用いて 二つの文字列 s[0,...,n-1] と t[0,...,n-1] に関して hashCode を求めると 次のようになります。

s 0 * x n - 1 + s 1 * x n - 2 + ... + s n - 1
t 0 * x n - 1 + t 1 * x n - 2 + ... + t n - 1

但し、これは高々 32 bit の演算なので、 232 で割った余りを考えることになります。 これが 一致する可能性を考えます。そこで、これに対して辺々引くと次のようになります。

s 0 - t 0 * x n - 1 + s 1 - t 1 * x n - 2 + ... + s n - 1 - t n - 1 0 mod 2 32

hashCode がなるべく重ならないようにするには、 この多項式に対して、多項式の値が 0 になる個数が少ない x を求めることに なります。 まず、 x = 0 や 232 などの場合、全ての多項式が 0 になります。 また、 x が 232 の約数である場合、各項で積が 232 となるような係数は 0 と 232 以外にいくつか存在することになるので、 0 になる個数が増えます。 したがって、まず最初の条件として、 x は 232 と互いに素でなければなりません。

次に多項式の各項について、 x を何乗すると元に戻るかを考えます。 オイラー関数 φ n には次の性質があります。

φ n = n 1 - 1 p1 ... 1 - 1 pk n = p1 e1 p2 e2 ... pk ek のとき
x φ n 1 mod n

ここで、 x φ n は必ず n で割れば 1 余りますが、 k < φ n だからと言って、 x k 1 mod n とは言えません。

ここで、 n が合成数の場合は、 φ n で始めて mod n で 1 となるような要素 x は必ずしも 存在しません (素数なら必ず存在します)。

ここで値を代入します。 n = 2 32 なので、 φ 2 32 = 2 32 1 - 1 2 = 2 31 つまり、 31 が十分大きな k まで全て 31 k 1 mod n を満たしているかどうかを検証すべきです。 しかし、実際は慣習で使われています。 なお、従来は 37 や 17 などが使われていました。

順序

次に、顧客を顧客 ID 順や名前順などに並べる方法を考えましょう。 例で示した顧客のオブジェクトは hashCode が与えられてはいますが、順番に 並べる条件は何も示していません。 このままでは順番に並びません。 そこで順番をあたえます。 Java では順番を与えるには java.lang.Comparable インターフェイスを implement し、 compareTo メソッドを実装します。


package java.lang;
public interface Comparable<T>{
    int compareTo(T o);
}

オブジェクト a, b に対して、 compareTo メソッドは次のような値を返すよ うに実装します。

a<b
a.compareTo(b)<0
a==b
a.compareTo(b)==0
a>b
a.compareTo(b)>0

数学では順序と比較は似たような概念になっています。この compareTo に関 して、これが全順序関係になるには次の条件を満たす必要があり ます。

反射律
a.compareTo(a)は 0
反対称律
任意の要素に対して a.compareTo(b)-b.compareTo(a) の符号は等しい(あるいはともに 0)
推移律
a.compareTo(b)b.compareTo(c) がともに正 なら a.compareTo(c) も正

さらに、オブジェクトが変化しない限りはなんど計算しても同一の答えを出さ なければならない一貫性が求められます。 また、 a.compareTo(b)==0 の時、 a.equals(b) が true であることが推奨されています。

このように全ての要素が比較可能で、上記の全順序の条件が成り立つ場合、必 ず、各要素は数直線のように一次元に並べることができます。 そのため、上記の条件を満たすような比較のことを全順序関係と呼びます。

なお、半順序関係とは全順序の条件に対して比較不能を許すもの です。 この場合、一直線上には並ばなくなります。 例えば、「約数である」という条件は半順序関係です。 この場合、反射律、推移律などは満たしますが、 任意の自然数の組に関して、反対称律は必ずしも成り立ちません。

例3-3

例3-1 では顧客の等価条件を顧客の ID と定めましたので、 顧客の ID を順序として与えます。


class Customer implements Comparable<Customer>{
  private String id;
...
  /**
  * 顧客の順序を与えます。
  * @param 顧客オブジェクト
  * @return 大小関係に応じて 負値(小), 0(等), 正値(大) を返します。
  */
  public int compareTo(Customer customer){
    return this.id.compareTo(customer.id);
  }
}

このように Comparable を実装すると、 TreeSet に格納すると自然に整列さ れる他、配列に対しては java.util.Arrays.sort メソッドで整列できますし、 各 Collection に関しても Collections.sort メソッドで整列可能です。 なお、 TreeSet や TreeMap を使用する場合、compareTo が 0 であることと、 equals が true になることが一致していないと誤動作します。


import java.util.LinkedList;
import java.util.Collections;
  
  final LinkedList<Customer> customerList = new LinkedList<Customer>();
  customerList.add(new Customer("..."));
  ...
  Collections.sort(customerList); // Customer が Comparable なので整列される

比較子(Comparator)

オブジェクトの集まりを比較するとき、もともと与えた順序と異なる順序を考 えたい場合があります。 例えば、顧客番号以外に、名前順、購入金額順、成績順など、 さまざまな順序による整列(sort)は多くの情報処理における基本的な操作です。 オブジェクト指向言語ではこのような整列を行うために、Comparator(比 較子)と呼ばれるオブジェクトを作り、整列のメソッドに渡す手法が一 般的です。

Java で比較子を作るには、 java.util.Comparator インターフェイスを implement し、compare メソッドを実装します。


package java.util;
public interface Comparator<T> {
  int compare(T o1, T o2);
  boolean equals(Object obj);
}

この compare は二つのオブジェクトを引数に取り、左が小さければ負の整 数、等しければ 0、左が大きければ正の整数を返します。

例3-4

例えば、顧客リストに関して生年月日で順序付けを行いたい場合を考えます。 顧客の生年月日は getBirthDay メソッドで java.util.Date 型の誕生日を取 得できます。 また、 java.util.Date 自体が Comparable なので、 compareTo メソッドは 既に実装されています。 そのため、比較子は顧客オブジェクトから誕生日を取り出して、 compare に与えるだけで実現できます。


import java.util.Comparator;
class BirthDayOrder implements Comparator<User> {
  public BirthDayOrder(){}
  public int compare(User u1, User u2){
    return u1.getBirthDay().compareTo(u2.getBirthDay());
  }
}

顧客の配列があったとして、これを誕生日順に並べるには次のようにします。


    User[] userList;
...
   java.util.Arrays.sort(userList,new BirthDayOrder());

なお、 java.util.Comparator インターフェイスには equals メソッドも実装 するように定義されていますが、全てのクラスは java.lang.Object のサブク ラスですので、デフォルトの equals は既に実装されています。 そのため、マニュアルにあるように通常は equals は実装しません。 この equals は要素の等価性を判定するものではなく、比較子の等価性を判定 するものです。

3-3. 演習問題

演習3-3

  1. 科目名(String)ごとに点数(Integer)を持つ 成績(Seiseki)クラスを java.util.HashMap のオブジェクトをコンポジション で持つだけのクラスとして作りなさい。

    実装するメソッドは次のメソッドです。メソッドの詳細は java.util.HashMap と同様ですので API のマニュアルをよく参照して実装しなさい。

    put(String,Integer)
    科目ごとの成績の追加
    get(String)
    科目を指定して成績を得る
    containsKey(String)
    科目を習得しているかどうかを調べる
    entrySet()
    Map の Set ビューを返す
    keySet()
    キーの Set ビューを返す
  2. 1 で作成したクラスに対して、平均点を返す double getAverage() メソッドを実 装しなさい。そして、次のプログラムと結合してテストを行いなさい。
    
    import java.util.Map;
    class Ex1 {
        /** 成績と平均点を表示します。
         * @param Seiseki オブジェクト
         */
        private static void showSeiseki(Seiseki s){
    	for(Map.Entry<String,Integer> item : s.entrySet()){
    	    System.out.println(item.getKey()+": "+item.getValue());
    	}
    	System.out.println("平均: "+s.getAverage());
        }
        public static void main(String[] arg){
    	final Seiseki student1 = new Seiseki();
    	final Seiseki student2 = new Seiseki();
    	student1.put("データ構造とアルゴリズムI",70);
    	student1.put("データ構造とアルゴリズム2",80);
    	student1.put("情報ネットワーク",100);
    	student2.put("データ構造とアルゴリズムI",90);
    	student2.put("データ構造とアルゴリズム2",70);
    	student2.put("情報ネットワーク",60);
    	showSeiseki(student1);
    	showSeiseki(student2);
        }
    }
    

演習3-4

演習3-3 のように 成績と Java のデータ構造である Map の関係は通常は has-a 関係になります が、成績を単なる表を表すオブジェクトとみなす場合、 is-a 関係として解釈できることも あります。 そこで、成績を単なる表だと思って、 HashMap のサブクラスとして実装して みましょう。 科目名(String)ごとに点数(Integer)を持つ 成績(Seiseki)クラスを java.util.HashMap<String,Integer> のサブク ラスとして作りなさい。 そして、 double getAverage() を実装して、演習3-3 の 2 と同じプログラムに結 合しなさい。

演習3-5

演習3-3 において、成績を student という変数名で受けるのには違和感があ ります。 学生ごとに成績があるということは、学生(Student)と成績(Seiseki)がオブジェ クトになり、has-a 関係があることになります。 また、学生には学籍番号があるとします。 そのため、 setId, getId, getSeiseki を実装しなさい。 なお、Student のコンストラクタで成績オブジェクトを初期化しなさい。 また、さらに、 Student クラスでは学籍番号順に扱いたいので、 equals, hashCode, compareTo メソッドを実装しなさい。 そして、下記のプログラムと結合して動作を確かめなさい。 但し、Seiseki クラスは演習 1, 2 のどちらのものでも構わない。


import java.util.Map;
class Ex3 {
    /** 成績と平均点を表示します。
     * @param Seiseki オブジェクト
     */
    private static void showSeiseki(Seiseki s){
	for(Map.Entry<String,Integer> item : s.entrySet()){
	    System.out.println(item.getKey()+": "+item.getValue());
	}
	System.out.println("平均: "+s.getAverage());
    }
    /** Student オブジェクトの配列に対して、 id を表示します
     * @param Student[] 型の配列
     */
    private static void showStudentId(Student[] array){
	for(Student student : array){
	    System.out.println(student.getId());
	}
    }
    public static void main(String[] arg){
	final Student[] students = new Student[3];
	for(int i=0; i < students.length; i++){
	    students[i] = new Student();
	}
	students[0].setId("07ec999");
	students[1].setId("07ec990");
	students[2].setId("07ec995");
	students[0].getSeiseki().put("データ構造とアルゴリズムI",70);
	students[0].getSeiseki().put("データ構造とアルゴリズムII",80);
	students[0].getSeiseki().put("情報ネットワーク",100);
	showSeiseki(students[0].getSeiseki());
	showStudentId(students);
	java.util.Arrays.sort(students);
	showStudentId(students);
    }
}

演習3-6

次のクラス ThreeInt には、コンストラクタで 3 つの要素を持つ int[] 型の 配列を与えて使用します。


import java.util.*;
class ThreeInt {
    private int[] array;
    public ThreeInt(int[] a){
	array = a;
    }
    public int getFirst(){
	return array[0];
    }
    public int getSecond(){
	return array[1];
    }
    public int getThird(){
	return array[2];
    }
    @Override public String toString(){
	return Arrays.toString(array);
    }
}

下記のプログラムにおいて、Arrays.sort を行ったとき、 Third のインスタンスを与えれば 3 番目の要素に対して整列、 First のインスタンスを与えれば 1 番目の要素に対して整列、 Second のインスタンスを与えれば 2 番目の要素に対して整列するように、 それぞれ Comparator<ThreeInt> としてクラスを作りなさい。


class Ex {
    private static void show(ThreeInt[] array){
	for(ThreeInt t : array){
	    System.out.println(t);
	}
	System.out.println("---------");
    }
    public static void main(String[] arg){
	final ThreeInt[] tArray = { new ThreeInt(new int[]{1,2,3}),
			      new ThreeInt(new int[]{2,3,1}),
			      new ThreeInt(new int[]{3,1,2})};
	show(tArray);
	Arrays.sort(tArray, new Third());
	show(tArray);
	Arrays.sort(tArray, new First());
	show(tArray);
	Arrays.sort(tArray, new Second());
	show(tArray);
    }
}
出力例
[1, 2, 3]
[2, 3, 1]
[3, 1, 2]
---------
[2, 3, 1]
[3, 1, 2]
[1, 2, 3]
---------
[1, 2, 3]
[2, 3, 1]
[3, 1, 2]
---------
[3, 1, 2]
[1, 2, 3]
[2, 3, 1]
---------

3-4. 付録

演習2-1のファイル

spro2/A.java

package spro2;
public interface A {
	String name();
	String id();
}

spro2/AbstractA.java

package spro2;
public abstract class AbstractA implements A {
	@Override
	public String toString() {
		return id()+":"+getClass()+":"+name();
	}
}

spro2/A999A.java

package spro2;
public class A999 extends AbstractA {
	@Override
	public String name() {
		return "坂本直志";
	}
	@Override
	public String id() {
		return "99ec999";
	}
}

spro2/Main.java

package spro2;
import java.util.Arrays;
public class Main {
	public static void main(String[] args) {
		A[] list = {
				new A999(),
				};
		Arrays.stream(list)
		.forEach(System.out::println);
	}
}