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

本日の内容


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

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

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

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

例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);
   }
}

なお、引数なしのコンストラクタが暗黙のうちに自動的に定義されます (デフォルトコンストラクタ)。 したがって、コンストラクタを定義しなくてもオブジェクトを生成し使用する ことができますが、この機能を使うのはお勧めしません。 デフォルトのコンストラクタを使用したい場合も、その旨を定義すべきです。

例3-2


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);
   }
}


class User {
   private String id;
   private String name;
   public User(){} //デフォルトコンストラクタ
   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 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 な関数として集められたク ラスになってます。

さて、ここで、コンストラクタを private にして、静的関数を使ってインス タンスを生成する例をいくつか示します。 このようなクラスの例としては 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());
    }
}

以下、実際にコンストラクタを private 修飾し、インスタンスを生成する静 的関数を作る例を示します。

例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();
    }
}

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

例3-7

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


class Ex {
    private static int num = 0;
    private Ex(){} 
    public static Ex getInstance(){
       num++;
       return new Ex();
    }
    public static int getNum(){
       return num;
    }
}
class Rei {
    public static void main(String[] arg){
        Ex e1 = Ex.getInstance();
        Ex e2 = Ex.getInstance();
        System.out.println(Ex.getNum());
    }
}

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

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


class Ex {
    private static Ex ex = 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-8


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

継承

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

例3-9


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

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

オーバライド

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

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

例3-10


class A {
    public A(){}
    public void show(){
	System.out.println("Aです");
    }
}
class B extends A {
    public B(){}
    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-11


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です";
    }
}
class Rei {
    public static void main(String[] arg){
	A a1 = new A(10);
	A a2 = new B(20);
	System.out.println(a1+" "+a1.getValue());
	System.out.println(a2+" "+a2.getValue());
    }
}

抽象クラス

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

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

例3-12


abstract class A {
    final public static String desu="です";
    public abstract void show();
}
class B extends A {
    public B(){}
    public void show(){
	System.out.println("B"+desu);
    }
}
class C extends A {
    public C(){}
    public void show(){
	System.out.println("C"+desu);
    }
}
class Rei {
    public static void main(String[] arg){
	A a1 = new B(); // A の変数を作ることはできる
	A a2 = new C();
	a1.show();
	a2.show();
    }
}
参考

なお、 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(){
  A *a1 = new B(); // A の変数は作れないが、ポインタは作れる
  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-13


interface A {
    String desu="です";
    void show();
}
class B implements A {
    public B(){}
    public void show(){
	System.out.println("B"+desu);
    }
}
class C implements A {
    public C(){}
    public void show(){
	System.out.println("C"+desu);
    }
}
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-14


class ExArray {
    Object[] oArray;
    int length;
    public ExArray(int length){
	this.length = 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 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){
	ExArray intArray = new ExArray(3);
	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-15

Java 1.4 の形でなんでも入れられるオブジェクトを作りました。


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

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

例3-16


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

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

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

例3-17


class ExArray<E> {
    Object[] oArray;
    int length;
    public ExArray(int length){
	this.length = length;
	oArray = new Object[length]; // ここで E[length] とできない
    }
    public void set(int n, E o){
	oArray[n] = o;
    }
    public E get(int n){
	return (E) oArray[n]; // ダウンキャストしないといけない
    }
    public int length(){
	return 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){
	ExArray<Integer> intArray = new ExArray<Integer>(3);
	ExArray<String> strArray = new ExArray<String>(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 では、コンパイラによる型 チェックができます。 但し、ExArray クラス内での型チェックはできません。 また、 get メソッドの部分でコンパイル時にワーニングが出ます。 さらに、この例でもわかるように、 Generics では基本型を入れることはでき ませんので、必ずラッパークラスを使用します。

アノテーション

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

上記の例では次のように記述することで、ワーニングを消すことができます。


  @SuppressWarnings({"unchecked"})
  public E get(int n) {
    return (E) oArray[n];
  }

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

参考

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

  1. C++ では template で int 型を直接扱え、ラッパークラスが不要
  2. 配列型と言うのが無く、ポインタで配列を扱う
  3. template 引数でオブジェクトが作れる
  4. コンストラクタでインスタンス変数の初期化の書式が違う
  5. オブジェクト型の仮引数の型は 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 Ex {
    public static void main(String[] arg){
	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 Ex {
    public static void main(String[] arg){
	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 Ex {
    public static void main(String[] arg){
	Money y1 = new Yen(100);
	System.out.println(y1.getValue());
	System.out.println(y1.getSign());
	y1.setValue(200);
	System.out.println(y1);
	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>
東京電機大学工学部情報通信工学科