page7(update:2017/11/21[例題7a]


[ prev | next | index ]

7. クラスとインスタンス

目次

 目次

7.1 Java言語におけるオブジェクト

Java言語ではクラス(class)をプログラムとして書きます。クラスには、クラスに所属するインスタンス(instance)の構造(フィールド,変数)や振る舞い(メソッド,関数)を記述します。また,プログラムを起動するメソッドとしてmain関数をクラスメソッドとして書くこともできます。

 

7.1.1 クラスとインスタンスの関係

オブジェクト指向モデルの考え方では、本来は、まずオブジェクトが有り、これを分類した類としてクラスがある。 「オブジェクト太郎や花子は人類クラスに所属する」 の様にクラスはクラス分けに使う。 逆にクラスは 明確な分類のために所属するオブジェクトを定義したものとも言える。

javaのクラスではインスタンスが持つフィールド(変数)とメソッド(関数)を定義し,ソースプログラムをコンパイルするとclassファイルが作られます。

実行時には,classファイルがメモリー上に展開されてクラスオブジェクトができます。これに対して,インスタンスオブジェクトはプログラムの実行中にクラスに記述されたインスタンスの構造に従って動的に生成されるオブジェクトです。 このためインスタンスは自動的にそのクラスに分類できる。しかし、インスタンスは所属するクラスを変ることができない

※オブジェクト指向プログラミングを可能にするため、構造体を拡張しクラスを作った。

本来オブジェクトは多様なクラスに分類することが可能なはずです。例えば花子は人クラスの一員で受付係クラスにも属するとか無数に考えられ る。受付係りクラスのインスタンス花子さんが受付係りから会計係りに移動する場合を考えてみよう。分類上は花子さんを受付係クラスから会計係クラスに移すことになるが、java言語では受付係クラスのインスタンス花子を会計係クラスにすることは不可能だ。この様な状況を想定したプログラムでは工夫が必要です。

java言語はインスタンスとその構造を記述したクラスで、オブジェクト指向モデルのオブジェクトとクラスの関係を記述しやすいようにした。しかし、オブジェクト指向モデルがそのままjavaプログラムにできるほどの対応にはなっていない。モデルからプログラムへの変換では、様々な工夫が必要になることを覚えておこう

java言語ではクラスもメンバを持てる

java言語ではインスタンスだけでなくクラスもオブジェクトとしての構造を持つことが可能です。例えば、javaのアプリケーションプログラムはクラスのmainメソッドから実行開始される。このmainメソッドはクラスのメンバである。インスタンスを作らなくても呼び出せる。メソッドだけでなくクラスのフィールドもstaticを付けて定義できる。

java言語のクラスはクラスのメンバを持てるので、クラス単独でオブジェクト としてふるまうことが可能だ。 このため「オブジェクト」がクラスなのかインスタンスなのか混乱しやすい。

以後はjava言語のクラスとインスタンスは共にオブジェクトとして話を進めることにする。

「オブジェクト」は一般的な概念として取っておくことにする。インスタンスはオブジェクトの実装の1つの形です。

7.1.2 UMLでのクラスとインスタンスの図

UMLの作図ツールを使って複素数を表現したComplexクラスのクラスとインスタンスを図に描いてみた。

クラス

左側の3つの枠をもつ四角がクラスの図です。1段目はクラス名Complexが書かれ,2段目はクラスとインスタンスのフィールド(変数),3段目はクラスとインスタンスのメソッド(関数)を書く。クラスのメンバとインスタンスのメンバは下線で区別され,下線がついているのがクラスのメンバです。

インスタンス

右にある2つの枠を持つ2個の四角がインスタンスの図です。 1段目のz1:Complexz2:Complexはインスタンスの名前で,名前は「インスタンス名:クラス名」のように 表記する。インスタンスの図ではインスタンス名に下線を引いて,インスタンスの名前であることを示す。

インスタンスのメソッドはこちらには書かない。実際,クラスに1個だけ用意したインスタンスのメソッドをインスタンスで供用するから,インスタンスごとにメソッドを重複して書くと意味がない。

2段目はフィールドを記述する。インスタンスのフィールドre,imを格納する場所はインスタンスごとに用意される。

クラスのフィールドの I もフィールド枠に書いているが、実際の格納場所はクラスに1個用意されるだけで、個々のインスタンスで重複して格納することはしない。しかし,下のインスタンス図を描いたツールではインスタンス側のフィールドにもクラスフィールドが重複して書かれた。

 

※UMLの図では型と変数が変数名:型名とCやjava言語とは逆になっています。(ALGOLと呼ばれるアルゴリズムを記述する言語の様式に倣った書き方)

±の記号は可視性で  +は公開(public)  -は非公開(private)。

対応するJavaのプログラムは次のようになります。

public class Complex
{
    public static final Complex I=new Complex(0,1);//クラスのフィールドとして虚数単位を定義

    private double re;//インスタンスのフィールドを宣言(インスタンスが生成されたときにメモリーが割り当てられる)
    private double im;//インスタンスのフィールドを宣言
    
    public static void main(String args[])//クラスのメソッドとしてmain関数を定義
    {
        Complex z1=new Complex(3,2);
        Complex z2=new Complex(5,4);

        System.out.println("Z1の実部は"+z1.getReal());
    }

    public Complex(double r,double i)//インスタンスのメソッドを定義(インスタンスから共用できるのでここで定義)
        re=r;im=i;
    }
    public double getReal()//インスタンスのメソッドを定義
    {
        retuirn re;
    }
    public double getImaginary()//インスタンスのメソッドを定義
    {
        return im;
    }
} 
 

 

目次

7.2 可視性とカプセル化

page2で書いたように、オブジェクトのメ ンバはアク セス管理の為に可視性と呼ばれる特性を持ち、可視性には public(公開)、private(非公開)などが有る、

オブジェクトのメンバに可視性を付けて外部からは公開部分を通してのみアクセスを許す様にすることをカプセル化と言う。

フィールドを非公開にすると変数の値を変更したり読み出しができないと思うかもしれません?。しかし、値を変更したり読み出したりするメソッドを用意して公開すればいいのです。javaのプログラムでは非公開の変数Xを読み書きする為にsetX()やgetX()等のメソッド が頻繁に現れます。

変数の値を見るのにいちいち関数を呼ぶなんて効率が悪くて面倒と思うかもしれません。しかし、getX()の様な関数を呼ぶ必要があるのはクラスの外から呼ぶときだけです。さらに直接変数を参照させないことで、その変数に間違った値が入らないように監視することができます。

※C言語にはデータをまとめる構造体が用意されていますが、可視性を加えたカプセ化ができません。 このためデータの読み書きは全ての場所から可能になり、プログラム全体にデータを書き換える可能性があります。 さらにデータの変更はプログラム全体に影響与える可能性があり、この為、規模の大きなプログラムになると更新やバグ取りが非常に難しいものになってしまいます。

以下にUMLに定義された可視性とjava言語の対応を示します。protectedやpackageについては後で説明します。

UMLでの可視性 UMLでの略号 java言語での表記 java言語での意味
public + public 公開(外部に公開)
private - private 非公開(自クラスとそのインスタンスにのみ公開)
protected # protected javaでは同じパッケージ内と継承先にのみ公開
UMLでは継承先にのみ公開
package ~ 既定値 同じパッケージ内にのみ公開

UML(Unified Modeling Language)はオブジェクト指向の開発で使われる図式の統一を目指して公開されたものです。下記に関連URLを示す

UMLTM Resource Page http://www.omg.org/uml/

 目次

7.3 インスタンスの生成、利用、消去

インスタンスは生成されるまで存在していません。
インスタンスには生成、利用、消去のライフサイクルがあります

7.3.1 インスタンスの生成

インスタンスの生成はnew演算子に続いてコンストラクタの呼び出しを記述して行います。

※コンストラクタ: クラスと同じ名前で,戻り値の指定がないインスタンス・メソッド。インスタンスの初期化を行うために存在する。

1)始めに、インスタンス用のメモリーを確保し、フィールドの初期値が書きこまれます。
初期値の指定が無い場合は0クリアを行います。

2)次にコンストラクタと呼ばれるメソッドによる初期化が続きます。

ここで、引数の異なる複数のコンストラクタを用意すれば、newに続くコンストラクタの呼び出しにおいて引数並びが一致するコンストラクタを選択して使うことが可能 です。

new Complex(1,2); // 1+2iで初期化されたインスタンスを生成
new Complex(1); // 1+0iで初期化されたインスタンスを生成

3)newはインスタンスへの参照を返します。参照型変数に参照を記憶しておくことで、これらのインスタンスを利用できます。

Complex z1 = new Complex(1,2); // 1+2iで初期化されたインスタンスを生成
Complex z2 = new Complex(1); // 1+0iで初期化されたインスタンスを生成

コンストラクタ(構築子、Constructor)

インスタンスを生成する 際はnewでインスタンス用のメモリーを確保しゼロクリアします、次にコンストラクタと呼ばれるメソッドを呼んでインスタンスを初期化 します。

コンストラクタと呼ばれるメソッドは少なくとも1個は存在します。プログラムにコンストラクタが一つも記述されていない場合にはコンパイラが何もしないコンストラクタを追加します。

コンストラクタは戻り値が記述されていないクラス名と同じ名前のメソッドです。引数の違いで複数のコンストラクタを区別することが可能です。以下はComplexクラスの例で2つのコンストラクタを用意した。

public class Complex
{
    /*−−−−インスタンスメンバ(インスタンスの雛形)−−−−*/
    //インスタンスのフィールド(変数) 非公開とした
    private double re;//実部
    private double im;//虚部

    //インスタンスのメソッド(関数)
 
    //コンストラクタ インスタンス生成時に初期化を行うメソッド
    public Complex(double r, double i)
    {
         re=r;im=i;
    }
    //引数並びを変えれば複数のコンストラクタを定義できる
    public Complex(double r)
    {
         re=r;im=0;
    }

    //値参照のためのメソッド
    public double getReal()
    {
        return re;
    }
    public double getImaginary()
    {
        return im;
    }
    public void setReal(double r)
    {
        re=r;
    }
    public void setImaginary(double i)
    {
        im=i;
    }
    public void setComplex(double r,double i)
    {
        re=r;im=i;
    }
    
    //絶対値を計算するメソッド
    public double abs()
    {
        return Math.sqrt(re*re+im*im);
    }
    //値をプリントするメソッド
    public void print()
    {
        System.out.print("("+re+","+im+")");
    }
}

 

7.3.2 インスタンスメンバの参照

メンバの参照はインスタンスを参照する変数を介して行う。例えば「参照変数名.メンバ名」 のようにドット演算子を用いて記述する。 (C++言語の対応する記述は「ポインタ変数名->メンバ名」となる。)

System.out.println("z1の実部は"+z1.getReal()+" 虚部は"+z1.getImaginary()+" 絶対値は"+z1.abs());
z2.print();//z2の書き出し

this

インスタンスメソッドの中ではそのインスタンスを指すthisという特別の変数が使える

メンバ名が関数の引数や変数の名前と重複する場合には、名前は内側で定義された引数や変数の名前が優先する。このとき、メンバを名前で参照するためには 「thisメンバ名」とthisを付けて名前がメンバの名前であることを示す必要がある。次に引数名とメンバ名が同じ場合の例を示す。

public void setReal(double re)
{
    this.re=re;//this.reはインスタンスのメンバのre 引数のreと区別するための記述
}

7.3.3 インスタンスの消去

javaでは、どの変数から参照をたどっても繋がらないインスタンスはガベージコレクションにより自動的にメモリーから消去される。

そこで、要らなくなったインスタンスは、これを参照している変数にnullを代入して参照を切り、通常は自動的なガベージコレクションに処理を任せます。C++言語の様に個別のインスタンスの消去を命令することは、javaではできません。

※C++ではnewで生成しdeleteで消去するが、javaにはnewは有ってもdeleteに相当するものが無い。

ユーザープログラムからの意図したガベージコレクションの実行は下記のSystemクラスのメソッドを呼んで行います。


System.gc();

ガベージコレクションが終了した時点で関数から戻りますから処理時間がかかるかもしれません。

※ガベージコレクションは並列処理でjavaVMにより自動実行されています。
javaVMが自動的にガベージコレクションは行っていますが、ユーザーが時間を取ってもいいのでガベージコレクションを実行して欲しいところに、System.gc( ); と記述します。

 

目次

7.4 クラスの生成、利用、消去

クラスもクラスメンバを持っているのでオブジェクトとして振舞えます。そして、クラスにも生成、利用、消去のライフサイクルがあります

クラスのメンバstatic修飾子をつけることで 区別します。mainメソッドにはstaticが付けられていたことを思い出してください。

7.4.1 クラスのロード

クラスの生成はjavaのclassファイルをメモリーに読み込んで初期化することで行われます。この処理を行うオブジェクトをクラスローダーと言います。クラスが必要になった時点で自動的にクラスローダーが呼ばれクラスの読み込みが行われます。

初期化に関係する記述はクラスフィールドの定義静的初期化子です。プログラムの上から下へ書かれた順番に初期化は行われます

静的初期化子 static initializers

staticのみが指定された戻り値、名前、引数が無いメソッド(複数記述可能)でクラスの初期化を行います。

下に例を示します。 実行の順番はクラスフィールドの記述と共に上から順番に実行されます。例のコメントに付けた番号1〜3の順です。

class Sample{
    static int integer1000[]=null;//1.クラスフィールド参照変数integer1000の初期化子

    static{ //2.静的初期化子 要素数1000個の配列インスタンスを生成し要素の値を初期化する。
        integer1000=new int[1000];
        for(int i=0;i<1000;i++)
            integer1000[i]=i;
    }

    static{ //3.静的初期化子 配列integer1000の要素の値を出力する。
        for(int i=0;i<1000;i++)
            System.out.println(integer1000[i]);
    }

    static public void main(String args[ ])
    {
        System.out.println("ABCD");
    }
   
}

※このSampleは実行を行うと0から999までの数字を書き出した後で、main関数で文字列「ABCD」を書き出します。つまり、初期化はmain関数を呼ぶ前に行われていることが解ります。

7.4.2 クラスメンバの参照

クラスメンバ の参照は「クラス名.メンバ名」 とクラスとメンバを指定する形に記述します。 ただし、同じクラスの内側からはメンバ名のみで呼び出すことが可能です。クラスのインスタンスからはクラスメンバが常に参照可能なので、インスタンスが共用するフィールドやメソッドとしてクラスメンバを利用する のが通例です。

※逆にクラスメンバからのインスタンスメンバの参照は、インスタンスの参照がないと不可能です。 

理解度の確認のために

下記のプログラムについて

1.掛算の関数productをインスタンスメソッドとして記述すると、どのようなプログラムになりますか?

/*自身の値に引数aを掛けた値を格納した 新しいインスタンスの参照を戻す関数*/
public Complex product(Complex a){

}
例題7a 下記は掛け算を行うクラスメソッドを記述していますが、上記の問いはインスタンスメソッドの記述を考えてほしいということです。
/*Complex.java*/
public class Complex
{
    /*−−−−インスタンスメンバ(インスタンスの雛形)−−−−*/
    private double re;//実部
    private double im;//虚部
    public Complex(double r, double i)
    {
        re=r;im=i;
    }
    public Complex(double r)
    {
        re=r;im=0;
    }
    //以下を圧縮 この書き方はお勧めできません。
    public double getReal(){ return re;}
    public double getImaginary(){ return im;}
    public void setReal(double r){ re=r;}
    public void setImaginary(double i){ im=i;}
    public void setComplex(double r,double i){ re=r;im=i;}
    public double abs(){ return Math.sqrt(re*re+im*im);}
    public void print(){ System.out.print("("+re+","+im+")");}

    /*−−−−クラスメンバ−−−−*/
    //かけ算 新しいインスタンスの参照を戻すクラスメソッド
    static public Complex product(Complex a, Complex b)
    {
        return new Complex(a.re*b.re-a.im*b.im, a.re*b.im + a.im*b.re);
    }
        
    //テスト用のmain
    static public void main(String args[])
    {
         System.out.println("\nインスタンスメンバのテスト");
         Complex z1=new Complex(1,2);
         Complex z2=new Complex(1);
         System.out.println("z1の実部は"+z1.getReal()+" 虚部は"+z1.getImaginary()+" 絶対値は"+z1.abs());
         z2.print();

         System.out.println("\nクラスメンバのテスト");
         Complex x=new Complex(1, 2);//共役複素数を生成
         Complex y=new Complex(1,-2);
         Complex z;
         z=Complex.product(x,y);//クラスのメソッドでかけ算
         z.print();//インスタンスのメソッドで結果をプリント
    }
} 

7.4.3 クラスの消去

クラスの消去もインスタンスと基本的には同様です。しかし、クラスはクラスを読み込んだクラスローダとの関係が在るために、クラスローダを消去しないと消すことはできません。

通常システム側のクラスローダーを止めるとプログラムが止まりますから、消すかもしれないクラスの場合、消してもよい専用のクラスローダを用意し、このクラスローダでクラスを読み込む必要があります。

消す場合は専用クラスローダと、これで読み込んだクラスとインスタンスへの参照を全て切ってからガベージコレクションの処理を実行します。

※この仕組みを利用するとプログラムを実行しながら、クラスを入れ替えることも可能です。グラフを描画するプログラムを動かした状態で、表示する関数クラスを書いてコンパイルしグラフに描画する。そんなことも可能です。

ReloadableClass <サンプルプログラム

 目次] 

7.5 課題6(初期ファイルP6.java

P6.javaを作ってText.java EditorFrame2.java をレポートツールJReportt.jarと同じフォルダにおいてコンパイルを行い、実行の時にはsample.svgもレポートツールJReportt.jarと同じ場所に置いてください。

とりあえず動作を確認したい場合は、実行可能ファイルP6.jarsample.svgの2つを同じ場所においてP6.jarをダブルクリックで実行してみてください。

XMLを使って図形を表現する形式のひとつにSVGがあります。最近のドロー系のソフトにはSVG形式のエディターとして使えるものもあるようですが、ここではSVGのファイルを読み込んで、ブラウザからアクセスしてみました。

オプションのメニューでfirefoxを起動し、ローカルIPの127.0.0.1にアクセスし、このツールで受けてテキストをブラウザに返信できるようにしてみました。

p6.javaのソースコード

Channelクラスの一部分だけ写経すればOK コメントは省いてください。

\は¥に読み替えること。

import java.net.*;
import java.io.*;
import java.util.*;
import javax.swing.*;

public class P6{
    public static void main(String args[]){
        EditorFrame2 window=new EditorFrame2();
        window.load("sample.svg");
        window.setSize(600,300); 
        window.setVisible(true); 
    }
}
class ChannelManeger  implements Runnable
{
    int port = 80;
    Channel channel=null;
    Thread thread=null;
    public ChannelManeger(Channel channel)
    {
        this.channel=channel;
        thread=new Thread(this);
        thread.start();
    }
    public void run()
    {
        while(thread!=null){
            ServerSocket s_socket=null;
            Socket socket=null;
            try{
                s_socket = new ServerSocket(port);//ポート番号のサーバソケットを作成
                s_socket.setSoTimeout(1000);//accept()のブロック時間に1秒のtimeoutを設定
                try{
                    socket = s_socket.accept();
                }catch(SocketTimeoutException timeout){continue;}
                if(!channel.isBusy()){
                    channel.startChannel(socket);
                }else{
                    socket.close();
                    socket=null;
                }
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                try{
                    if(socket != null)socket.close();
                    if(s_socket != null)s_socket.close();
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }
    }
}
class Channel
{
    /////////////////////////////////////////////////////////
    //クラスのメンバー
    static final String CRLF="\r\n";//HTTPでは改行は\r\n
    static final String charset="UTF-8";//出力の文字コードを一般性は無いがSJISにしておUTF-8がいいのかも
    static Hashtable<String,String> contentTypeTable=new Hashtable<String,String>();
    //さまざまなファイルの拡張子とコンテントタイプの組をcontentTypeTableに登録
    static{
        //HyperText
        contentTypeTable.put("html",  "text/html");
        contentTypeTable.put("htm",   "text/html");
        //絵
        contentTypeTable.put("svg",   "image/svg+xml");
    }
    //ファイル名から、その拡張子に対応したコンテントタイプを戻す
    static public String getContentType(String name)
    {
        int pos=name.lastIndexOf(".");//ピリオドの位置を探して
        String type=null;
        if(pos>-1){//ピリオドがあれば
            type = name.substring(pos+1);//ピリオドの後の拡張子を切り出し
            type = type.toLowerCase();//全て小文字に変換
            type = (String) contentTypeTable.get(type);//上で作ったテーブルから検索
         }
        if(type==null){//ピリオドが無ければ
            type = "text/plain";//見つからない場合とりあえずテキストとする
        }
        return type;
    }
    
    /////////////////////////////////////////////////
    //以下はインスタンスのメンバ
    boolean busy=false;
    Socket socket=null;
    Text text;
    public Channel(Text text)
    {
        this.text=text;
    }
    public boolean isBusy()
    {
        return busy;
    }
    public void startChannel(Socket socket)
    {
        busy=true;
        this.socket = socket;
        //要求を聞いてあげないと切れてしまうのでデバッグ用を兼ねて標準出力へ要求を書き出す
        
        BufferedReader input=null;
        try{
            input=new BufferedReader(
                new InputStreamReader(socket.getInputStream(),"JISAutoDetect")
            );
            try{Thread.sleep(100);}catch(Exception ex){ex.printStackTrace();}
            while(input.ready())System.out.println(input.readLine());
            //要求を聞き終わってから送信
            send(text.getText(),charset,getContentType(text.getName()));
            //送信が終わってからinputをクローズしないとダメ
            input.close();input=null;
        }catch(IOException e){
            e.printStackTrace();
            try{
                if(input!=null) input.close();
            }catch(IOException e2){e2.printStackTrace();}
        }
        busy=false;
    }
    void send(String body,String charset,String contentType)
    {
        System.out.println("ACCEPT:"+socket);
        
        StringBuffer buffer=new StringBuffer();
        buffer.append("HTTP/1.0 200 OK\n");
        buffer.append("Connection: close\n");
        buffer.append("Content-Type: "+contentType+";charset=\""+charset+"\"\n");
        
        byte content[]=toByteArray(body,charset);
        buffer.append("Content-Length: "+content.length+"\n");
        buffer.append("\n");
        
        byte header[]=toByteArray(buffer.toString(),charset);
        
        //ここから送信
        OutputStream output=null;
        try{
            output=socket.getOutputStream();
            output.write(header);
            output.write(content);
            output.close();output=null;
        }catch(IOException e){
            e.printStackTrace();
            try{
                if(output!=null) output.close();
            }catch(IOException e2){e2.printStackTrace();}
        }
        return;
    }
    //Unicode文字列をHTML用のバイト列に変換
    //同時に改行コードを\nから\r\nに置き換える
    byte[] toByteArray(String string,String charset)
    {
        ByteArrayOutputStream byteStream=new ByteArrayOutputStream();
        PrintWriter output=null;
        try{
            output=new PrintWriter(new OutputStreamWriter(byteStream,charset));
            int length=string.length();
            for(int i=0;i<length;i++){
                char c=string.charAt(i);
                switch(c){
                    case '\r':break;
                    case '\n':output.print("\r\n");break;
                    default:output.print(c);
                }
            }
            output.close();
        }catch(Exception e){e.printStackTrace();}
        return byteStream.toByteArray();
    }
} 

[ prev | next | index ]