応用プログラミング 第3回


この授業の目的

オブジェクト指向言語の基礎(抽象クラス,インターフェース)について説明します.


出席メール

出席メールで「授業中の課題の実施」を確認します.メールを出した時刻,PCの場所(アドレス)を確認します.

to::jhoshino@esys.tsukuba.ac.jp
subject: 応用プログラミング 第3回.氏名,学籍番号
本文: なし(書かなくて結構です)


理論

抽象クラス

クラスの継承を実現する途中で、 「抽象クラス(abstract class)」と呼ばれるクラスが現れることがあります。 通常のクラスの目的は、もちろんオブジェクトを生成し活用することです。 ところが、抽象クラスは直接自分自身のオブジェクトを生成しません。 他のクラスのスーパークラスになることを主要な目的としています。

たとえば「哺乳類」というクラスを考えてみましょう。 「哺乳類」のサブクラスには「象」、「馬」、「キリン」などが存在します。 具体的な一匹一匹の個体をオブジェクトと考えます。 この場合、 そして「哺乳類」に含まれるオブジェクトは必ずサブクラスのいずれかになります。 「哺乳類」自身の直接のオブジェクトは存在しません。 ただし「哺乳類」のクラスに何も情報が定義されていないというわけではありません。 たとえば「乳を飲ませて子供を育てる」という共通の手続きは、 「哺乳類」のクラスで定義されることになるでしょう。 クラスの継承関係を数学の「集合」のような形式で 表現で記述してみると理解しやすいかもしれません。


このように抽象クラスは、 「共通の性質を持つクラスをグループ分けする」という働きをします。 そして、そのグループに共通する性質を定義を記述することで、 個々のサブクラスの記述を簡略化することが可能になります。

次に Javaにおける抽象クラスの取り扱いを調べてみましょう。 まず「キーワード abstract」と 「abstract宣言されたメソッド」から説明します。 Javaでは次のように名前と型の情報だけで、 定義の部分が存在しないメソッドを記述することができます。

public abstract void hasNothig( int x, int y );

もちろん定義がないままでは何も処理が実行できません。 このままでは、このメソッドを呼び出しても何の意味もないでしょう。 では、このようなメソッドはどうやったら利用できるのでしょうか?  あるクラスの中では abstract宣言されたメソッドとして存在していても、 そのサブクラスで再定義することによって処理の中身を持つことが可能です。 abstract宣言されたメソッドも最終的には中身を持つことになるわけです。このように「クラスにあらかじめ予定されている機能と、 その実際の内容(実装)を分離する」という考え方は、 最初のうちは馴染みにくいかもしれません。 しかし Javaでクラスを設計する場合にはしばしば用いられる重要なテクニックです。

 


/*
  AbstractSample.java  abstractの実装
*/

//ゲームという抽象概念を宣言するクラス
//通常,abstractクラスはそれを継承するサブクラスに必要な機能を
//宣言するために思量するここではGameという抽象概念は
//タイトルと価格を持つとする

abstract class Game{
  String Title;
  abstract int Price(); //メソッド名の定義.このクラスでは実装しない.
}


class DQ7 extends Game{
  DQ7(){
    Title = "DQ7";
  }
  int Price(){  //Gameクラスで定義したメソッドの実装
    return 7800;
  }
}

class FFX extends Game{
  FFX(){
    Title = "FFX";
  }
  int Price(){
    return 8800;
  }
}

class AbstractSample{
  public static void main(String args[]){
    DQ7 dq = new DQ7();
    FFX ff = new FFX();
    System.out.println("Title "+dq.Title+" Price "+dq.Price());
    System.out.println("Title "+ff.Title+" Price "+ff.Price());
  }
}
 

 

 

 

 

←Gameという抽象クラスを定義する.

 

←Gameクラスを継承する.

 Price()メソッドを定義する.

 

 

実用面での利点としては,Gameクラスを継承していれば,そのサブクラスでは必ずPrice()というメソッドが利用可能であることが分かります.

このように,異なるクラスの間で呼び出し方を統一すると,プログラムが大きくなったときに,整理しやすくなります.

 

 

 

インターフェイスの実装

単純なクラスの継承の場合には、 直接のスーパークラスは1つしか存在しません。 しかし、 実際の財産の相続では複数の相手から財産を受け取る場合があり得ます。 また生物の進化の場合でも両親という複数の親から情報を受け継ぎます。 「複数のクラスをスーパークラスとして継承を行えないか?」 という考えが出てくるのもごく自然なことでしょう。 これを実現しようというのが「多重継承( multiple inheritance )」です。 これは非常に魅力的なアイディアです。 もし実現できれば、次のようなことが可能となるでしょう。

ただし多重継承はいくつか新しい問題を発生させます。 たとえば、 スーパークラスに選択された複数のクラスの中に 同じ名称の情報が含まれていた場合です。 新しいクラスは、 どちらから優先的に情報を受け継いだらいいのでしょうか?  なんらかのルールで優先順位を決めるにせよ、 そのような衝突が生じないように予防するにせよ、 言語は何か新しい仕組みを導入しなくてはいけないでしょう。



多重継承による混乱

また、多重継承によって継承関係が一挙に複雑化します。 単純な継承の場合にはクラス間の関係は、一方が他方のスーパークラスであるか、 サブクラスであるか、あるいは全く無関係である、この3通りの場合しかあり得ません。しかし、多重継承の場合には一筋縄ではいきません。 たとえば A, B, C という3つのクラスがあったとしましょう。 これらから、Aのみを継承したサブクラス、AとBのみを継承したサブクラス、 AとCのみを継承したサブクラス、・・・と全部で7種類のサブクラスが発生します。 これらのうち Aのみを継承したサブクラスと BとCのみを継承したサブクラスの 両者をスーパークラスとして新しいサブクラスを設計することも可能です。 ただし、Aのみを継承したサブクラスと AとBのみを継承したサブクラスの両者を スーパークラスとするのは、明らかに二重にスーパークラスを継承することになります。クラスの継承が何世代にも渡るとスーパークラスの数自体が膨大になりますから、 こうしたチェックだけでも大変です。

Javaにおける多重継承を説明するためには、 「インターフェイス(interface)」の概念を避けて通ることはできません。インターフェースは抽象クラスの考え方と類似していて,機能の定義(メソッドの定義)だけを行います.常識的に考えれば何も内容がないクラスをわざわざ設計するというのは 不思議な気がすると思います。 しかし抽象クラスのところで説明したように、 abstract宣言されたメソッドはあらかじめ機能を分離して記述しておくという 意味がありました。 インターフェイスは 「機能のみを完全に分離して記述されたクラス」ということができます。

なお、インターフェイスの定義を記述する場合、 文法的には通常のクラスと多少異なる取り扱いを受けます。 まずインターフェイスの定義の始まりを表すキーワードは、 class ではなく interface です。もう一つ非常にささいな違いですが、 インターフェイスの定義の中ではメソッドが abstractであることは自明なため、 abstractの修飾子を省略してかまわないということがあります。 したがって、典型的なインターフェイスの定義は次のようになります。

public interface FirstInterface {
   public void firstProcess();
}

Javaのインターフェイスを通じた継承の具体例を見てみましょう。 Javaでの最も一般的な継承の記述は以下のような形式です。

public class SecondClass extends FirstClass implements FirstInterface, SecondInterface ...{


implements という新しいキーワードが用いられています。 この後に継承すべきインターフェイスを指定します。 extendsの後には任意のクラス(インターフェイスでも可)が指定できます。 ただし、インターフェイスではないクラスは1つしか指定できません。 これに対して implementsの後にはインターフェイスしか指定できません。 ただし、複数のインターフェイスを指定することが可能です。 インターフェイスの中ではメソッドは全て abstract宣言されています。 したがってサブクラスがオブジェクトを生成できるクラスになるためには、 インターフェイスから受け継いだメソッドをすべて再定義して内容を用意する 必要が生じます。これは「クラスへのインターフェイスの実装」と言われます。 extends でスーパークラスに指定されたクラスも、 implementsでスーパークラスに指定されたクラスも、 どちらもスーパークラスとして取り扱われる点では全く同等です。

インターフェイスの実装


/*
  InterfaceSample.java  インターフェイスの実装
*/
//abstractクラスは変数の宣言やメソッドの実装もできる
abstract class PSGame{
  String Title;
  abstract int Price();  //メソッド名の定義
  String Hardware(){
    return "PS";
  }
}

//interfaceでは定数やメソッドの仕様のみを宣言できる
interface Media{
  String CD = "CD-ROM";//定数
  //String CD; のように変数を定義するとError

  String Media(); //メソッド名の定義
}

class DQ7 extends PSGame implements Media{  
  DQ7(){
    Title = "DQ7";
  }
  int Price(){  //PSGameクラスで定義したメソッドの実装
    return 7800;
  }
  public String Media(){ //Mediaインターフェイスで定義したメソッドの実装
    return CD;
  }
}

class FF9 extends PSGame implements Media{
  FF9(){
    Title = "FF9";
  }
  int Price(){
    return 7800;
  }
  public String Media(){
    return CD;
  }
}

class InterfaceSample{
  public static void main(String args[]){
    DQ7 dq = new DQ7();
    FF9 ff = new FF9();

    System.out.println("Title "+dq.Title+" Price "+dq.Price()
                      +" Hardware "+dq.Hardware()+" Media "+dq.Media());

    System.out.println("Title "+ff.Title+" Price "+ff.Price()
                      +" Hardware "+ff.Hardware()+" Media "+ff.Media());
  }
}

 

メソッド,コンストラクタのオーバーロード

オーバーロードとは,同じ名前のメソッドやコンストラクタを,複数定義できる機能です.例えば,println()メソッドでは,boolean, char, int, float, doubleなどの様々なデータ型を渡すことができますが,データ型によって表示する機能が異なるのに,一つの名前で呼び出すことができます.クラスライブラリの内部ではデータ型によって異なるメソッドが定義されているのですが,オーバーロードの機能を利用して同じ名前で呼び出すことができます.


/*
  OverloadSample.java  オーバーロードの利用
*/
class StrClass{
  String  subject;  //主語
  String  verb;     //動詞
  String  object;   //目的語
  String  str;


  StrClass(){}  //コンストラクタ(1)
  StrClass(String subject,String verb,String object){   //コンストラクタ(2)
    this.subject = subject;
    this.verb    = verb;
    this.object  = object;
  }


  void combine(){   //メソッド(1)
    str = subject+verb+object;
  }
  void combine(String subject,String verb,String object){   //メソッド(2)
    str = subject+verb+object;
  }
}

class OverloadSample{
  public static void main(String args[]){
    StrClass sa[] = new StrClass[2];


    sa[0] = new StrClass();    //コンストラクタ(1)を呼び出している
    sa[1] = new StrClass("They ","arrived at  ","the airport.");   //コンストラクタ(2)を呼び出している

    for(int i=0;i<2;i++)
      System.out.println(i+" Subject "+sa[i].subject+" Verb "+sa[i].verb
                          +" Object "+sa[i].object);

    sa[0].combine("They ","arrived at ","the airport.");  //メソッド(2)を呼び出している
    sa[1].combine(); //メソッド(1)を呼び出している

    for(int i=0;i<2;i++) System.out.println(i+" Str     "+sa[i].str);
  }
}


演習問題

1)AbstractSample.javaとInterfaceSample.javaを実行して出力結果を確認して下さい.また,その出力結果が得られる理由について考察して下さい.

2)第2回に出てきたClass PlayStationを抽象クラスとして定義して,Class PlayStation2で継承して下さい.Class Playstationのメソッド(getCPU, getMedia)の機能をClass PlayStation2 の中で定義して下さい.

提出先のアドレスは:jhoshino@esys.tsukuba.ac.jpです.Subject欄に「応用プログラミング 第3回課題,氏名,学籍番号」を書いて下さい.本文にソースコード,実行結果,考察を記載して下さい.

締め切りは次の授業の開始時まで.ソースコードはメール本文へのコピーあるいは添付どちらでも結構です.

 


参考資料

 クラス MediaTracker

 Graphics2Dクラス仕様

 Java2D(パッケージ java.awt.geom)仕様


 

ラップクラスの紹介

オブジェクト指向言語では,もともと全てのデータをオブジェクトとして扱うことになっています.SmallTalkなどの古典的なオブジェクト指向言語では,数値データもオブジェクトとして扱います.ところが,複雑な数値計算を全てオブジェクトのメソッドで記述することは面 倒ですので,JAVAではC言語のように算術式で数値計算ができるようになっています.

ラップクラスは,このような古典的なオブジェクト指向言語に備わっていた,数値をオブジェクトとして扱うという機能を引き継いだものです.int,float,double等のデータ型に対応したラップクラスがあります.JAVAでは数値計算は算術式で記述することができますので,通 常のプログラミングでは,主にString(文字列)と数値データの変換,逆変換の機能が使われています.

サンプルプログラムでは文字列からintへの変換は
String str = "string";
Integer intObj = Integer.valueOf(str);
int i = intObj.intValue();

となっていますが
int i = Integer.valueOf(str).intValue();  (教科書p54 )
や int i = Integer.parseInt(str); と書くこともできます.

/*
  VariousLapClass.java  ラップクラスの紹介
*/
class VariousLapClass{
  public static void main(String args[]){


    String str[] = new String[3];
    str[0] = "100";  //数値ではなく文字列であることに注意する
    str[1] = "100.11";

  //文字列を数値に変換する
    Integer intObj  = Integer.valueOf(str[0]); //指定された文字列が表す値に初期化されるIntegerオブジェクトを返す
    int     i       = intObj.intValue();       //現在のオブジェクトの持つ値をint型で返す
    //int     i       = Integer.parseInt(str[0]);も上記2行と同じ意味を持つ

    Float   floObj  = Float.valueOf(str[1]);   ///指定された文字列が表す値に初期化されるFloatオブジェクトを返す
    float   f       = floObj.floatValue();     //現在のオブジェクトの持つ値をfloat型で返す

    Double  douObj  = Double.valueOf(str[1]);  //指定された文字列が表す値に初期化されるDoubleオブジェクトを返す
    double  d       = douObj.doubleValue();    //現在のオブジェクトの持つ値をdouble型で返す

    //i,f,dの値が文字列として扱っていた数値と同じであることを確認する
    System.out.println("int "+i+" float "+f+" double "+d);


  //数値を文字列に変換する
    str[0] = Integer.toString(50);             //intの文字列表現を作成
    str[1] = Float.toString(50.55f);           //floatの文字列表現を作成
    str[2] = Double.toString(50.55);           //doubleの文字列表現を作成

    //数値として代入した値が文字列として扱えることを確認する
    System.out.println("intstr "+str[0]+" floatstr "+str[1]+" doublestr "+str[2]);

  //数値間のデータ型を変更する
    intObj  = new Integer(50);
    f       = intObj.floatValue();              //このIntegerオブジェクトの値を float 値として返す
    //Javaでもキャストは可能なので f = (float)50;やf = (float)intObj.intValue();も上記と同じ意味を持つ

    floObj  = new Float(50.55f);
    d       = floObj.doubleValue();             //このFloatオブジェクトの値を double 値として返す

    douObj  = new Double(50.55);
    i       = douObj.intValue();                //このFloatオブジェクトの値を int 値として返す
    System.out.println("int "+i+" float "+f+" double "+d);
  }
}

実行結果
int 100 float 100.11 double 100.11
intstr 50 floatstr 50.55 doublestr 50.55
int 50 float 50.0 double 50.54999923706055

 


静的変数/インスタンス変数,静的メソッド/インスタンスメソッドの比較

オブジェクトの変数には,インスタンス変数と静的変数の2種類があります.インスタンス変数はこれまでに説明に出てきたオブジェクトの変数です.これに対して,静的変数とは,そのクラスの全てのオブジェクトから参照することができる変数です.

/*
  CompareInSt.java  静的変数、インスタンス変数、静的メソッド、インスタンスメソッドの比較
*/
class CompareClass{
  static int  staticValue;   //静的変数
  int         instanceValue; //インスタンス変数


  CompareClass(){
    staticValue = 0;
    instanceValue = 0;
  }


  //インスタンス変数に1を足すインスタンスメソッド
  int addInstanceValue(){
    return ++instanceValue;
  }
  //error 静的メソッドからインスタンス変数を変更することはできない
  /*
  //インスタンス変数に1を足す静的メソッド
  static int addInstanceValueS(){
    return ++instanceValue;
  }
  */
 
  //静的変数に1を足すインスタンスメソッド
  int addStaticValue(){
    return ++staticValue;
  }

  //静的変数に1を足す静的メソッド
  static int addStaticValueS(){
    return ++staticValue;
  }
}


//変数の情報を見るためのクラス
class Info{
  CompareClass sc[] = new CompareClass[2];  //このクラスのためのCompareClassのオブジェクトを生成

  //コンストラクタのmain関数で定義したCompareClassオブジェクトの参照を受け取る
  Info(CompareClass sc[]){
    for(int i=0;i<2;i++) this.sc[i] = sc[i];
  }

  //インスタンス変数の値を出力する
  void instanceInfo(){
    System.out.println("sc[0]In "+sc[0].instanceValue+" sc[1]In "+sc[1].instanceValue);
  }

  //静的変数の値を出力する
  void staticInfo(){
    System.out.println("sc[0]St "+sc[0].staticValue+" sc[1]St "+sc[1].staticValue
                                +" CompSt "+CompareClass.staticValue);
  }
}

class CompareInSt{
  public static void main(String args[]){
    CompareClass sc[] = new CompareClass[2];
    for(int i=0;i<2;i++) sc[i] = new CompareClass();  //CompareClassのオブジェクトを生成
    Info info = new Info(sc);  //Infoクラスのオブジェクトを生成

    info.instanceInfo();  //インスタンス変数の値を出力する
    info.staticInfo();   //静的変数の値を出力する

    System.out.println("sc[0]In "+sc[0].addInstanceValue());  //sc[0]のインスタンス変数に1を足す
    info.instanceInfo();                                      //sc[1]のインスタンス変数には1が足されていないことを確認する

    System.out.println("sc[0]St "+sc[0].addStaticValue());    //sc[0]からCompareClassの静的変数に1を足す
    info.staticInfo();                                        //静的変数に1が足された結果どこから静的変数を見ても同じ値になっていることを確認する

    System.out.println("sc[1]St "+sc[1].addStaticValueS());   //sc[1]からCompareClassの静的変数に1を足す
    info.staticInfo();                                        //静的変数に1が足された結果どこから静的変数を見ても同じ値になっていることを確認する

    System.out.println("CompSt "+(++CompareClass.staticValue));//CompareClassの静的変数に1を足す
    info.staticInfo();                                        //静的変数に1が足された結果どこから静的変数を見ても同じ値になっていることを確認する
  }
}

実行結果

sc[0]In 0 sc[1]In 0
sc[0]St 0 sc[1]St 0 CompSt 0
sc[0]In 1
sc[0]In 1 sc[1]In 0
sc[0]St 1
sc[0]St 1 sc[1]St 1 CompSt 1
sc[1]St 2
sc[0]St 2 sc[1]St 2 CompSt 2
CompSt 3
sc[0]St 3 sc[1]St 3 CompSt 3



ThreadDemo3.java
初期値0の整数に1秒間隔で1を足していくプログラムの作成
(コンストラクタとの組み合わせ)
*/
class RunnableY implements Runnable {
 int i;
 RunnableY(){
  i = 0;
 }
 public void run() {
  try {
   while(true) {
    Thread.sleep(1000);
    System.out.println(i++);
   }
  }
  catch(InterruptedException ex) {
   ex.printStackTrace();
   }
  }
 }

class ThreadDemo3 {
  public static void main(String args[]) {
  RunnableY ry = new RunnableY();
  Thread t = new Thread(ry);
  t.start();
 }
}