第 3 回 オブジェクトとプログラミング

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

3-1. 要点

  1. オブジェクトとプログラミング
    1. オブジェクトクラスの名前は大文字で始める
    2. オブジェクトクラスには、フィールド(メンバ変数)、メソッドの他に、 クラスごとに一つしかない static フィールド(クラス変数)と、 static メソッド(クラス名を指定するメソッド)を含むことができる
    3. オブジェクト指向の定石をデザインパターンと言う
    4. ファクトリ、シングルトンデザインパターン
  2. 継承:親クラスのメソッド、フィールドを利用する。親クラス型の変 数は子クラスのオブジェクトを参照できる
    1. オーバーライド:親クラスと同じ名前のメソッドを上書きする。親ク ラス型の変数から参照されていても、メソッドは子クラスの実装が実行さ れる
    2. 抽象クラス:子クラスにオーバーライドされるメソッドは名前だけ定義する
    3. interface:メソッドの名前だけを定義する(但しJava8から仕様変更あり)
    4. Generics:様々な型のオブジェクトを同じように扱いたい時、その型を型 引数として与えてプログラムを作る

3-2. オブジェクトとプログラミング

Java ではオブジェクトを作るにはクラスを定義します。 クラスの基本的な用法は、複数のデータをひとまとまりにして管理する方法で す。 これを実現するには、 private なフィールド変数と、 public なコンストラクタ と、public な getter, setter と public な toString メソッドを作ります。 なお、この「メソッドを作る」ということを実装(implement)する とも言います。

なお、プログラミングするにあたって、変数名などに特別な コーディングルール(プログラミング上の慣習)があります。 変数名、メソッド名は小文字、クラス名は大文字で始めます。 また、複数の単語からなる名前に関しては、nextWord のように単語の区切り では次の単語を大文字にして結合します。

例3-1


class User {
   private String id;
   private String name;
   public User(String id, String name){
      this.id = id;
      this.name = name;
   }
   public void setId(String id){
      this.id = id;
   }
   public void setName(String name){
      this.name = name;
   }
   public String getId(){
      return id;
   }
   public String getName(){
      return name;
   }
   public String toString(){
      return "id: "+id+", name: "+name;
   }
}
class Rei {
   public static void main(String[] arg){
      User u = new User("07ec999", "坂本");
      u.setName(u.getName()+"直志");
      System.out.println(u);
   }
}

なお、クラスを作成し、コンストラクタが定義されていないとき、引数なしの コンストラクタが暗黙のうちに自動 的に定義されます(デフォルトコンストラクタ)。 デフォルトコンストラクタは親クラス(無ければjava.lang.Object)のコンスト ラクタだけを呼びます。 また、アクセス修飾子はクラス定義と同等で、 public なクラスだと public になり、無指定のクラスでは無指定になります。 デフォルトコンストラクタの存在は、常に意識する必要があります。 但し、 Java8 からは省略するのが標準的な記法となります。

以下のプログラムは同じ意味になります。

例3-2


public class User {
   private String id;
   private String name;
   public void setId(String id){
      this.id = id;
   }
   public void setName(String name){
      this.name = name;
   }
   public String getId(){
      return id;
   }
   public String getName(){
      return name;
   }
   public String toString(){
      return "id: "+id+", name: "+name;
   }
}
class Rei {
   public static void main(String[] arg){
      User u = new User(); //暗黙のコンストラクタ
      u.setId("07ec999");
      u.setName("坂本直志");
      System.out.println(u);
   }
}


public class User {
   private String id;
   private String name;
   public User(){super();} //引数なしコンストラクタの明示
   public void setId(String id){
      this.id = id;
   }
   public void setName(String name){
      this.name = name;
   }
   public String getId(){
      return id;
   }
   public String getName(){
      return name;
   }
   public String toString(){
      return "id: "+id+", name: "+name;
   }
}
class Rei {
   public static void main(String[] arg){
      User u = new User();
      u.setId("07ec999");
      u.setName("坂本直志");
      System.out.println(u);
   }
}

クラスに含まれるもの

クラスには基本的にフィールド変数、メソッド、コンストラクタが含まれます。 そして、コンストラクタを使用してオブジェクトが作られます。 クラスから生成された個々のオブジェクトのことを インスタンス と呼びます。 インスタンスに対して、メソッドを使って、情報を格納、取り出しを行います。

しかし、クラスの用法はこれだけに限りません。

まず、特定のひとまとまりの関数の実行を管理するために、それらをメソッド としてオブジェクトに含み、フィールド変数を持たないクラスが考えられます。

例3-3


class Greeting {
    public void hello(){
        System.out.println("Hello.");
    }
    public void bye(){
        System.out.println("Bye.");
    }
}
class Rei {
    public static void main(String[] arg){
        Greeting g = new Greeting();
        g.hello();
        g.bye();
    }
}

このように、クラスにはフィールド変数とメソッドを含むことができますが、さら に既に述べたように、静的関数(クラスメソッド、 static メソッ ド)を含むことができます。 static メソッドは public 宣言すると外部から「クラス名.メソッド名」で呼 び出すことができます。 このようにすると、クラスを関数の集まりとして使うこともできます。

例3-4


class Greeting {
    public static void hello(){
        System.out.println("Hello.");
    }
    public static void bye(){
        System.out.println("Bye.");
    }
}
class Rei {
    public static void main(String[] arg){
        Greeting.hello();
        Greeting.bye();
    }
}

java.lang.Math はこのように数学関数が static な関数として集められたク ラスになってます。

ファクトリ

さて、ここで、他のクラスのオブジェクトを生成したり、いろいろな条件を与 えてからオブジェクトを生成するような場合を考えます。 この様な場合、オブジェクトを生成するのに、単純に public なコンストラク タを使用せずに、public な関数を用いる場合があります。 このようなオブジェクトを生成するような関数をファクトリと言 います。 ここではファクトリの例をいくつか示します。

コンストラクタを使わずにオブジェクトを生成するクラスの例としては java.net.InetAddress というインターネットのアドレスを管理するクラスが あります。 これのコンストラクタは使用できず、次のようにして使用します。

例3-5


import java.net.*;
class Samp {
    private static byte code(int x){
	return (byte)(x & 0xff);
    }
    public static void main(String[] arg) throws UnknownHostException {
	byte[] iaddr = {code(133), 20, code(160), 1};
	// byte は -128 から 127 まで
	InetAddress ia = InetAddress.getByAddress(iaddr);
	System.out.println(ia.getHostAddress());
    }
}

このようにすると、 4 Byte のアドレスを与えると java.net.Inet4Address のオブジェクトを生成し、 16 Byte のアドレスを与えると java.net.Inet6Address のオブジェクトを生成します。

例3-6

単純なファクトリの例を示します。


class Ex {
    private Ex(){} // 引数なしのコンストラクタを private 宣言すると
                   //  外部から勝手にインスタンスを作れなくなる
    public static Ex getInstance(){
       return new Ex();  // コンストラクタを呼び出してオブジェクトを作り
                         // できたオブジェクトを返す
    }
}
class Rei {
    public static void main(String[] arg){
        Ex e = Ex.getInstance();
    }
}

この例ではコンストラクタを使用するのと変わりません。 しかし、getInstance メソッドにオブジェクトを生成する時に一緒に行う処理 を付加することができるようになります。 特に、あらかじめ作成しておいたオブジェクトを渡すような場合など、呼び出 された必ずオブジェクトを生成するわけでないような処理で有効です。


クラス変数(静的変数, static 変数)という、クラスに属す る唯一の変数を定義できます。 これでクラスの管理などを行うことができます。

例3-7

オブジェクトを生成した個数を数えるには次のようにします。


class Ex {
    private static int num = 0; //クラス変数
    public Ex(){
       num++;
    } 
    public static int getNum(){
       return num;
    }
}
class Rei {
    public static void main(String[] arg){
        Ex e1 = new EX();
        Ex e2 = new Ex();
        System.out.println(Ex.getNum());
    }
}

シングルトンデザインパターン

アプリケーションの主画面や、データベースのコントロール、定数的なオブジェ クトを共有する場合等で、対象となるオブジェクトを常に一つだけ生成したい 場合があります。 このような時、オブジェクトの設計における定石が存在します。 オブジェクトの設計の定石のことを デザインパターン と言いま す。 このオブジェクトを常に一つにしておくデザインパターンを シングルトンデザインパターンと言います。 これは次のようにクラス変数を有効に使います。

例3-8


class Ex {
    private static Ex ex = null; // null で初期化
    private Ex(){}               // 外部からのアクセス禁止
    public static Ex getInstance(){
       if(ex == null){
          ex = new Ex();
       }
       return ex;
    }
}
class Rei {
    public static void main(String[] arg){
        Ex e1 = Ex.getInstance();
        Ex e2 = Ex.getInstance();
        if(e1.equals(e2)){
          System.out.println("equal!");
        }
    }
}

クラス変数は初期化の指定ができます。 これに final 修飾子 をつけて、再代入を禁止しておくと、定数 として使用することができます。

例3-9


class User {
   final public static String MAXID = "08ec999";
}

3-3. 継承

オブジェクト指向言語では、しばしば元となるクラスを拡張した別のクラスを 作成することができます。 拡張したクラスのことをサブクラスと言います。 一方、元のクラスを親クラスと言ったり スーパークラスと言ったりしま す。 そして、サブクラスを作ることを継承すると言います。

例3-10


class A {
    public void show(){
	System.out.println("Aです");
    }
}
class B extends A { // A のサブクラス
}
class Rei {
    public static void main(String[] arg){
	A a = new A();
	B b = new B();
        a.show();
        b.show();
    }
}

なお、継承において private なフィールドは継承されません。 外部には公開したくないけど、継承はさせたいものに関して は protected 修飾をします。

オーバライド

サブクラスで同名のメソッドを定義することができます。 これを オーバーライド と言います。

さて、この継承において重要な性質があります。 それは、サブクラスのインスタンスは親クラスの変数で参照が可能であること です。 親クラスの変数型で参照したオブジェクトでは、サブクラスで追加されたメソッ ドは使用できなくなります。 しかし、オーバライドされたメソッドは、変数の型が親のクラスであって も親クラス側ではなくサブクラス側のメソッドを使用できます

例3-11


class A {
    public void show(){
	System.out.println("Aです");
    }
}
class B extends A {
    public void show(){ // オーバーライド
	System.out.println("Bです");
    }
}
class Rei {
    public static void main(String[] arg){
	A a1 = new A();
	A a2 = new B(); // 親クラス型
        a1.show();
        a2.show();  // B の show が呼ばれる
    }
}

なお、子クラスから親クラスのメソッドを呼び出すときは super を使います。特に親クラスのコンストラクタを呼ぶ時は 単純に super() などを使用します。 これは、親クラスのフィールド変数をコンストラクタで初期化したいときなどに使 用します。

例3-12


class A {
    private int value;
    public A(int n){
	value = n;
    }
    public int getValue(){
	return value;
    }
    public String toString(){
	return "Aです。";
    }
}
class B extends A {
    public B(int n){
	super(n); // 親クラスのコンストラクタの呼び出し
    }
    public String toString(){
	return "Bです。親は"+super.toString();
    }
}
class Rei {
    public static void main(String[] arg){
	final A a1 = new A(10);
	final A a2 = new B(20);
	System.out.println(a1+" "+a1.getValue());
	System.out.println(a2+" "+a2.getValue());
    }
}

抽象クラス

親クラスが専らサブクラスを作るためだけに設計され、必ずオーバーライドさ れるメソッドがあったとします。 この時、その親クラスのメソッドを定義をするのは無駄になります。 こういうケースに対して、 abstract 宣言という機能があります。 これは、宣言は必要でも実装したくないメソッドに対して、 abstract 宣言して 実装をせずに ;(セミコロン) でメソッド宣言を終えることえ無意味な実装を する必要がなくなります。 但し、ひとつでも abstract 宣言をしたメソッドを持つクラスは、クラスにも abstract 宣言をする必要があります。 さらに abstract 宣言したクラスはインスタンスを生成できません。 一方、 abstract 宣言したクラスの変数は作ることができ、サブクラスのイン スタンスを参照できます。 又、 abstract 宣言されていてサブクラスでは実装されているメソッドを呼び 出すことができます。

例3-13


abstract class A {
    final public static String desu="です";
    public abstract void show();
}
class B extends A {
    public void show(){ // オーバーライドしないとエラー
	System.out.println("B"+desu);
    }
}
class C extends A {
    public void show(){
	System.out.println("C"+desu);
    }
}
class Rei {
    public static void main(String[] arg){
	final A a1 = new B(); // A の変数を作ることはできる
	final A a2 = new C();
	a1.show();
	a2.show();
    }
}

なお、このように abstract 宣言をしたメソッドはサブクラスで実装されない 限り、オブジェクトをインスタンス化できません。 つまり、 abstract 宣言はサブクラスでのメソッドの実装を強要します。 これを契約と言うことがあります。

例3-14

abstract 宣言したクラスでも、 private なインスタンス変数を持つ 場合があります。 この場合、サブクラスのコンストラクタで、そのインスタンス変数を初期化す る必要が生じます。 そのため、インスタンスが作成できなくても、サブクラスから super で呼び 出すためにコンストラクタを作る場合があります。


abstract class A {
  private int num;
  protected A(int n){
    num = n;
  }
  protected int getValue(){
    return num;
  }
  abstract void show();
}
class B extends A {
  public B(){
    super(1);
  }
  public void show(){
    System.out.println(getValue());
  }
}
参考

なお、 C++ では変数宣言時にコンストラクタが呼ばれるので、完全仮想関数 を含むクラスでは変数宣言できません。 但し、ポインタは宣言できますので、A のポインタが B や C のオブジェクト を参照するようにすることは可能です。


#include <iostream>
#include <string>
class A {
public:
  static const std::string desu;
  virtual void show() const = 0; // 完全仮想メンバ関数 
};
class B : public A {
public:
  B(){}
  void show() const ; // プロトタイプ宣言
};
class C : public A {
public:
  C(){}
  void show() const ;
};
const std::string A::desu= "です"; // C++ ではクラス宣言と実装は分ける
void B::show() const {
  std::cout << "B" << desu << std::endl;
}
void C::show() const {
  std::cout << "C" << desu << std::endl;
}
int main(){
  const A *a1 = new B(); // A の変数は作れないが、ポインタは作れる
  const A *a2 = new C();
  a1->show(); // ポインタからのメソッドの呼び出し
  a2->show();
  return 0;
}

interface

もし、 abstract クラスに含まれるのが public な定数と public abstract メソッドのみの場合、abstract class 宣言の代わりに interface 宣言にすることができます。 interface 宣言では 「final public static」と「public abstract」を省略 することができます。 また、 interface 宣言をしたクラスを継承する場合、 extends の代わり に implements で指定します。 なお、 interface はインスタンスを作れませんが、変数を作ることはできま す。 そして、その変数はサブクラスを参照できます。

例3-15


interface A {
    String desu="です"; // public final static String と同じ
    void show();        // public abstract void show() と同じ
}
class B implements A {
    public void show(){
	System.out.println("B"+desu);
    }
}
class C implements A {
    public void show(){
	System.out.println("C"+desu);
    }
}
class Rei {
    public static void main(String[] arg){
	final A a1 = new B(); // 変数は作れる
	final A a2 = new C();
	a1.show();
	a2.show();
    }
}

なお、interface にもプログラムを記述することができます。 但し、すべて、public 扱いになります。 まず static メソッドを記述できます。 また、default 修飾子を付加することで、インスタンスメソッド もプログラム を記述できます。 なお、interface にプログラムを記述する場合、フィールド変数をつかえない ことには注意が必要です。

例3-16


interface A {
    static String desu(){
        return "です";
    } 
    String getName();
    default void show(){
	System.out.println(getName()+desu);
    }
}
class B implements A {
    @Override
    public String getName(){
	return "B";
    }
}
class C implements A {
    @Override
    public String getName(){
	return "C";
    }
}
class Rei {
    public static void main(String[] arg){
	A a1 = new B();
	A a2 = new C();
	a1.show();
	a2.show();
    }
}

継承などの詳しい用法に関しては章を改めて説明します。

Generics

全ての Java のクラスは暗黙的に java.lang.Object クラスのサブクラスになっ ています。 そのため、あらゆるオブジェクトを java.lang.Object 型の変数で参照できま す。 例えば、 Object o = new Integer(1) などとすることは可能 です。 Java 1.4 まではこの手法を任意のオブジェクトの格納などの用途に使ってい ました。 例えば簡単な配列を含むクラスを考えましょう。

例3-17


class ExArray {
    private Object[] oArray;
    public ExArray(int length){
	oArray = new Object[length];
    }
    public void set(int n, Object o){
	oArray[n] = o;
    }
    public Object get(int n){
	return oArray[n];
    }
    public int length(){
	return oArray.length;
    }
}
class Rei {
    private static int sum(ExArray ea){
	int sum=0;
	for(int i=0; i<ea.length(); i++){
	    sum += ((Integer) ea.get(i)).intValue(); // Integer を仮定
	}
	return sum;
    }
    public static void main(String[] arg){
	final ExArray intArray = new ExArray(3);
	final ExArray strArray = new ExArray(3);
	intArray.set(0,new Integer(1));
	intArray.set(1,new Integer(3));
	intArray.set(2,new Integer(2));
	System.out.println(sum(intArray));
	strArray.set(0,"abc");
	strArray.set(1,"def");
	strArray.set(2,"ghu");
	System.out.println(sum(strArray)); // コンパイルエラーは出ない
                                           // しかし実行時に例外が発生
    }
}

この例ではなんでも入る配列に一方は Integer、もう一方には String を入れ ています。 そして、 Integer が入っていると仮定した関数 sum を用意して合計を求めて います。 ここで、誤って String の入っている配列を sum に与えてもコンパイル時に エラーになりません。 この本質は get で取り出す値が java.lang.Object であることによります。 取り出したオブジェクトを元のオブジェクトとして扱うには元のクラスの型でキャ ストする必要があります。 このサブクラスの型にキャストすることを ダウンキャスト と呼 びます。 Java 1.4 まではこのように Object 型にオブジェクトを入れ、取り出すとき にダウンキャストするというのが普通のテクニックでした。 しかし、上記の例のようにダウンキャストが成功するかどうかは実行してみな いとわからないため、上記のようにコンパイル時にエラーは出ず、実行時にキャ ストが失敗する例外が発生してしまいます。

そこで、なんでも入れられるオブジェクトという機能を残したまま、インスタ ンスを生成するときには、入れるものを指定するような仕組が必要になります。 この機能は日本語では総称などと呼ぶ機能です。 Java では version 5 から Generics と呼ばれる機能拡張が行われました。 クラス宣言やメソッドの宣言で型を仮引数として宣言し、使用するときに具体 的な型を指定するものです。 簡単な例を見ましょう。

例3-18

Java 1.4 のスタイルで、なんでも入れられるオブジェクトの例


class Samp {
    private Object value;
    public void set(Object o){
	value=o;
    }
    public Object get(){
	return value;
    }
}
class Rei {
    public static void main(String[] arg){
	final Samp e1 = new Samp();
	final Samp e2 = new Samp();
	e1.set(new Integer(1));
	final int d1=((Integer)e1.get()).intValue(); // ダウンキャスト
	e2.set("abc");
	final String d2=(String)e2.get();
    }
}

これに対して、 Java 5 以降のスタイルで、 Generics を使用して、入れるも のをインスタンスの宣言時に指定するのが次の例です。

例3-19


class Samp<E> {
    private E value;
    public void set(E e){
	value=e;
    }
    public E get(){
	return value;
    }
}
class Rei {
    public static void main(String[] arg){
	final Samp<Integer> e1 = new Samp<Integer>();
	final Samp<String> e2 = new Samp<String>();
	e1.set(new Integer(1));
	final int d1=e1.get().intValue();
	e2.set("abc");
	final String d2=e2.get();
    }
}

このようにすると、set や get など値が受渡しされるところでコンパイラは 型のチェックをします。 そのため、型の不一致による実行時のエラーを防ぐことができます。

ただ、この記法は変数宣言でも、コンストラクタでも型引数を記述するのが煩 瑣です。 そのため、コンストラクタ側の型引数を省略して <>(ダイヤモンド) で表すことができます。 特に、Java8からは省略するのが標準の記法となります。

例3-20


class Samp<E> {
    private E value;
    public void set(E e){
	value=e;
    }
    public E get(){
	return value;
    }
}
class Rei {
    public static void main(String[] arg){
	final Samp<Integer> e1 = new Samp<>();
	final Samp<String> e2 = new Samp<>();
	e1.set(new Integer(1));
	final int d1=e1.get().intValue();
	e2.set("abc");
	final String d2=e2.get();
    }
}
参考

なお、 C++ では記述可能であればあらゆる記述を処理するような雰囲気があ りますが、 Java の Generics はコンパイル時に従来のバイナリと互換性があ るようにコンパイルされるそうです。 このため、指定した型のインスタンスを生成できないなど、いくつかの制限が あります。 先に示した単純な配列のオブジェクトに関しても、指定した型の配列が作れな いので、従来のコードを一部ひきずるようなプログラムになります。

例3-21


class ExArray<E> {
    E[] oArray;
    public ExArray(int length){
	oArray = (E[]) new Object[length]; // ここで E[length] とできない
    }
    public void set(int n, E o){
	oArray[n] = o;
    }
    public E get(int n){
	return oArray[n];
    }
    public int length(){
	return oArray.length;
    }
}
class Rei {
    private static int sum(ExArray<Integer> ea){ // 引数に Integer を指定
	int sum=0;
	for(int i=0; i<ea.length(); i++){
	    sum += ea.get(i).intValue();
	}
	return sum;
    }
    public static void main(String[] arg){
	final ExArray<Integer> intArray = new ExArray<>(3);
	final ExArray<String> strArray = new ExArray<>(3);
	intArray.set(0,new Integer(1));
	intArray.set(1,new Integer(3));
	intArray.set(2,new Integer(2));
	System.out.println(sum(intArray));
	strArray.set(0,"abc");
	strArray.set(1,"def");
	strArray.set(2,"ghu");
	System.out.println(sum(strArray)); // 今度はちゃんとコンパイルエラーが出る
    }
}

これで少なくとも実際にインスタンスを使用する main では、コンパイラによる型 チェックができます。 但し、コンパイル時にワーニングが出ます。 さらに、この例でもわかるように、 Generics では基本型を入れることはでき ませんので、必ずラッパークラスを使用します。

例3-22

次に関数に対する Generics を与えます。 関数に関しては戻り値の型の前に型引数 <E> を表示し、関数の引数の 型に依存した処理をすることが可能です。


class Rei {
  private static <E extends Integer> int sum(E[] ea){
    int sum=0;
    for(int i=0; i<ea.length; i++){
      sum += ea[i].intValue();
    }
    return sum;
  }
  private static <E extends String> String sum(E[] ea){
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<ea.length; i++){
      sb.append(ea[i]);
    }
    return sb.toString();
  }
  public static void main(String[] arg){
    System.out.println(sum(new Integer[]{4,5,6}));
    System.out.println(sum(new String[]{"abc", "def", "ghi"}));
  }
}

なお、この例において、同じ関数名で引数の型の違う二つの関数 sum を与え ました。 このような定義を多重定義あるいはオーバーロードと 呼びます。

ただし、上記のオーバーロードは Generics 込みの複雑なものですが、下記の ような単純なオーバーロードの方が、この場合はふさわしいです。


class Rei {
  private static int sum(Integer[] ea){
    int sum=0;
    for(int i=0; i<ea.length; i++){
      sum += ea[i].intValue();
    }
    return sum;
  }
  private static String sum(String[] ea){
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<ea.length; i++){
      sb.append(ea[i]);
    }
    return sb.toString();
  }
  public static void main(String[] arg){
    System.out.println(sum(new Integer[]{4,5,6}));
    System.out.println(sum(new String[]{"abc", "def", "ghi"}));
  }
}

なお、この Generics は少なくとも Java 8 までは、コンパイラに与えるメモ でしかなく、実行時に動的に型を決定するものではありません。 従って、Generics の型に応じたオーバーロードはできません。

だめな例

以下は、 ExArray 型に対する関数を与えます。 関数の引数として配列を取り、その配列の型に応じて ExArray を作成し、値 をコピーして返す getExArray 関数を与えます。 しかし、関数の引数の型が ExArray<Integer> と ExArray<String> では、変数の値の内容を元に関数を選ぶことができな いので動作しません。 (しかし、実は以下のコードは Java6 のコンパイラではコンパイラのバグでコ ンパイルできました)


class Rei {
  private static <E> ExArray<E> getExArray(E[] a){
    ExArray<E> e = new ExArray<>(a.length);
    for(int i=0; i<a.length; i++){
      e.set(i,a[i]);
    }
    return e;
  }
  private static int sum(ExArray<Integer> ea){
    int sum=0;
    for(int i=0; i<ea.length(); i++){
      sum += ea.get(i).intValue();
    }
    return sum;
  }
  private static String sum(ExArray<String> ea){
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<ea.length(); i++){
      sb.append(ea.get(i));
    }
    return sb.toString();
  }
  public static void main(String[] arg){
    System.out.println(sum(getExArray(new Integer[]{4,5,6})));
    System.out.println(sum(getExArray(new String[]{"abc", "def", "ghi"})));
  }
}

アノテーション

Generics で説明したように、現行の Java ではコンパイル時に必ずワーニン グが出てしまうプログラムを書かないといけない場合が生じます。 このような状況を放置すると、プログラミングミスによるワーニングと区別が 付かなくなり、ワーニングの意味をなさなくなります。 そのため、 Java 言語ではコンパイラに助言をしてワーニングのコントロール をすることができる アノテーション という機能があります。

上記の例では次のように記述することで、ダウンキャストにおけるワーニング を消すことができます。


  @SuppressWarnings({"unchecked"})
  public ExArray(int length){
    oArray = (E[]) new Object[length];
  }

アノテーションには、この他にオーバライドを意図していることをコンパイラ に伝える @Override と、使用を推奨しないようなメソッドに対して使ったと きに警告を出させる @Depricated があります。 そのため、例えば、 今後、 toString メソッドなどを定義するときは @Override を付加すること にします。 このようにすると、メソッドの綴ミスによるオーバーライドの失敗を防げます。

例3-23


class A {
    private int value;
    public A(int n){
	value = n;
    }
    public int getValue(){
	return value;
    }
    @Override
    public String toString(){
	return "Aです";
    }
}

なお、 interface 内のメソッドには @Override を付けると、Java5ではエラー になり、 Java6 以降ではチェックを行います。

参考

例3-21の処理を C++ で作成すると下記のようになります。 C++ では Object のようなスーパークラスを使用せずに(存在もしませんが)き れいに書けます。

  1. template で int 型を直接扱え、ラッパークラスが不要
  2. 配列型と言う独立した型は無く、ポインタで配列を扱う
  3. template 引数でオブジェクトが作れる
  4. コンストラクタでのインスタンス変数の初期化の書式が違う
  5. オブジェクト型 E を仮引数で参照するための型は const E& が基本
  6. const へのこだわり
  7. デストラクタというオブジェクトが不要になった際のメモリ管理が必要

#include <iostream>
#include <string>
template <typename E>
class ExArray {
private:
    int len;
    E *oArray;
public:
  ExArray(int l): len(l), oArray(new E[l]) {} // E の配列が作れる
  void set(int n, const E& o){ // E& は E の参照型
    oArray[n] = o;
  }
  E get(int n) const { // この const はメンバ変数を変えないという意味
    return oArray[n];
  }
  int length() const {
    return len;
  }
  ~ExArray(){ // デストラクタ
    delete[] oArray;
  }
};
int sum(const ExArray<int>& ea){
  int sum=0;
  for(int i=0; i<ea.length(); ++i){
    sum += ea.get(i);
  }
  return sum;
}
int main(){
  ExArray<int> intArray(3); // 基本型を指定できる
  ExArray<std::string> strArray(3);
  intArray.set(0,1);
  intArray.set(1,3);
  intArray.set(2,2);
  std::cout << sum(intArray) << std::endl;
  strArray.set(0,"abc");
  strArray.set(1,"def");
  strArray.set(2,"ghu");
  //  std::cout << sum(strArray) << std::endl;
  // コメントを外すとエラー
  return 0;
}

演習問題

演習3-1

日本円を入れるクラス Yen を作りなさい。 内部には、金額と貨幣記号を持ち、それぞれ getValue() , getSign() で取り 出せるようにしなさい。 また、金額はコンストラクタ、または setValue() で指定できるようにしなさ い。 また、 toString メソッドをオーバライドして、例えば「¥100」などと文字 列を返すようにしなさい。 そして、次のプログラムと結合して動作させなさい。


class Ex31 {
    public static void main(String[] arg){
	final Yen y1 = new Yen(100);
	System.out.println(y1.getValue());
	System.out.println(y1.getSign());
	y1.setValue(200);
	System.out.println(y1);
    }
}

演習3-2

アメリカドルを入れるクラス Dollar を作りなさい。 内部には、金額と貨幣記号を持ち、それぞれ getValue() , getSign() で取り 出せるようにしなさい。 また、金額はコンストラクタ、または setValue() で指定できるようにしなさ い。 また、 toString メソッドをオーバライドして、例えば「$100」などと文字 列を返すようにしなさい。 そして、次のプログラムと結合して動作させなさい。


class Ex32 {
    public static void main(String[] arg){
	final Dollar d1 = new Dollar(100);
	System.out.println(d1.getValue());
	System.out.println(d1.getSign());
	d1.setValue(200);
	System.out.println(d1);
    }
}

演習3-3

前述の Yen クラスと Dollar クラスは共通部分が多いので、これに対して、 抽象親クラス Money を作って下さい。 但し、 Money クラスは abstract なメソッドのみで良いです。 そして、次のプログラムと結合して下さい。


abstract class Money {
...
}
class Yen extends Money {
...
}
class Dollar extends Money {
...
}
class Ex33 {
    public static void main(String[] arg){
	final Money y1 = new Yen(100);
	System.out.println(y1.getValue());
	System.out.println(y1.getSign());
	y1.setValue(200);
	System.out.println(y1);
	final Money d1 = new Dollar(100);
	System.out.println(d1.getValue());
	System.out.println(d1.getSign());
	d1.setValue(200);
	System.out.println(d1);
    }
}

演習3-4

Yen と Dollar のメソッドにおいて、 toString はほとんど同じです。 Money において、 abstract 宣言されている getSign や getValue を使用して、 toString を実装しなさい。 そして、 Yen, Dollar から toString を消去しなさい。 前の演習と同様に上記のプログラムで動作するようにしなさい。

演習3-5

Yen と Dollar の getValue や setValue メソッドもほとんど同じです。 そこで、インスタンス変数も Money 側にしておき、getValue, setValue を Money クラスで扱うように改造しなさい。 そして上記のプログラムで同様に動作するようにしなさい。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科