カテゴリー
C

C(Step1-3)

変数を利用した表示

ここまでのプログラムでは、計算式を書いてただ表示するだけでしたが、ここからはより効率的な書き方を学びます。

変数を準備する

何度も同じ値を使う時や、複雑な計算をするときに一時的な値を入れておく箱をイメージしてください。これを用いることで無駄な部分が無くなります。値を記憶する箱のことを変数(variable)と言います。

C言語では、どのように利用するか以下のサンプルで確認しましょう。

#include <stdio.h>

int main(){
    /*変数を用意*/
    int total; //①

    total = (10*8+15*5)*105/100; //②

    /*10000でのお釣り表示*/
    printf("10000: %d\n", 10000-total); //③
    /*5000でのお釣り表示*/
    printf("5000: %d\n", 5000-total);
    /*1000でのお釣り表示*/
    printf("1000: %d\n", 1000-total);

    return(0);
}
sample1-3.1実行結果

①変数を宣言する

ここの部分は、変数宣言と呼ばれる部分で、値を記憶する箱である変数を記述する部分です。以下のように記述します。

変数宣言の記述方法

[変数の種類] [変数の名前];

このプログラムでは、totalというのが「変数の名前」にあたり、作成者が名前をつけます。「変数の種類」は、入れたい値が整数なのか、小数なのか文字なのかを指定するものです。種類のことをまたはデータ型といいます。ここでは、整数値なので、intという整数の型を指定しています。他にも、実数を扱うfloatやdoubleといった型があります。

同じデータ型の変数を複数用意したいときには、それぞれ別の行に記述してもいいですが、カンマで区切って記述することもできます。

■同じデータ型の変数を複数用意する場合

[変数の種類] [変数の名前1], [変数の名前2], [‥‥‥];

このように変数に値を代入する前には変数宣言をしなくてはいけません。

②変数に値を代入

②の部分は、①で宣言した変数に値を入れて記憶させています。「変数名=数値」と書くことで、変数に値を記憶させることができます。これを、変数に値を代入するといいます。「=」は数学では、イコール記号と同じですが、プログラムでは、代入するという意味になります。例えば、以下のように書くとします。

a = 1000;
a = a + 1000;

この場合、最初のaには1000を代入しますが、2行目のaは、変数aに記憶されている1000に1000を足してそれを、左辺のaに代入しなおす形になります。「値に記憶している変数にさらに値を足す」という記述です。よく使われる記述なので、覚えておくと良いでしょう。

③変数を使って計算し、結果を表示する

③の部分は、printf()で、カンマ(,)のあとに「1000-変数名」と記述しています。変数totalには計算した整数値が入っています。変数に入っている値で計算が処理され、%dの位置に結果が表示されます。

データ型の種類と詳細

ここでは、C言語の型についてまとめています。また、printf()での変換仕様と共に表にしています。

型の名称と読み方変数の取り得る値printf()での変換仕様備考
int
(イント)
整数値%d最大(小)値はコンパイラによって異なる
long int
(ロングイント)
整数値%ld最大(小)値はコンパイラによって異なる
float
(フロート)
実数値
(小数を含む)
%f
double
(ダブル)
実数値
(小数を含む)
%lf値の精度はfloatの約2倍
char
(キャラ)
文字%c1文字を記憶できる
short int
(ショートイント)
整数値%dlong int よりも小さい範囲の値を格納できる
unsigned int
(アンサインドイント)
0以上の正の整数%d最大値は、intの約2倍
unsigned long int
(アンサインドロングイント)
0以上の正の整数%ld最大値は、long intの約2倍
変数のデータ型と変換仕様

printf()で数値を表示するときには、桁を揃えて表示することや、整数部何桁+小数部何桁といった表示する桁の調整も可能です。

整数の桁を揃える方法

整数の桁を揃えるときには、何桁を表示するのかあらかじめ決めれます。3桁表示するなら、以下のように記述します。

printf("%3d", 99);

%とdの間に数値が入っていますが、表示する桁数を表しています。3桁の表示をして、99のように2桁以下の数値を出力した場合、左に空白が入ります。指定の桁以上の表示をするときは、右に溢れて表示されるので、注意が必要です。

実数の桁の調整

小数部値を含んだ値ー実数を表示するときは、整数部何桁+小数部何桁と表示する場合があります。例えば、整数3桁+小数2桁と表示したいときは、全体の桁数を小数点を1と数え、1+3+2=6として記述します。

printf("%6.2lf", 32.456);

表示する数値が指定の桁数未満なら、左に空白が入ります。小数部分が指定の桁以上ある場合は、その下の数値は四捨五入して表示されます。桁数以上の表示では、右にあふれてしまいます。

プログラムを実行中に結果を得る

プログラムを実行しているときに、値を入力し、その入力された値を利用する場合があります。そのような処理について説明していきます。

プログラムで値の入力をする

プログラム実行中に値を入力できるようにするには、「変数に値を読み込む」という処理を行います。まずは、整数値を入力し、入力した値を表示するサンプルを実行しましょう。

#include <stdio.h>

int main(){

    /*整数値の読み込み*/
    int num;

    scanf("%d", &num); //整数値を変数numに読み込む
    printf("result[%d]\n", num); //numの値を表示する

    return(0);
}
sample1-3.2実行結果

このプログラムを実行すると、入力待ち状態になり、そこで数値を入力、改行すると、実行結果のように値を表示してくれます。

入力を受ける関数scanf()の詳細

ここでは、scanf()の使い方について説明していきます。

scanf()の使い方
scanf("%○", &変数名,‥‥‥, &変数名);

・%○にはprintf()で利用した変換仕様%dや%fなどをつけて記述する
・変数名を読み込むときは、変数名の先頭に&をつけて記述する

実際のコードを見ながら考えていきましょう。

int num1, num2; //int型の変数num1とnum2を用意
float num3; //float型の変数num3を用意
double num4; //double型の変数num4を用意

scanf("%d %d", &num1, &num2); //①
scanf("%d:%d", &num1, &num2); //②
scanf("%f %lf", &num3, &num4); //③

①の部分は、2つの整数値を読み込む場合の記述です。%dと%dの間に空きを入れて記述しなければなりません。[整数値][空き][整数値]のようにする必要があります。

②の部分も、2つの整数値を読み込む場合の記述です。この記述の場合は、%dと%dの空きを[整数値][:][整数値]としなければなりません。

③の部分は、2つの実数値を読み込む場合の記述です。実数値を扱う%fと%lfを使っており、他同様、間には空きを記述しています。

キャストの利用

色々な計算をする場合に、はじめは整数型で用意していた変数を、ここの計算のときだけ、小数を含んだ計算に利用したいということがあると思います。そんな時に「そのときだけこの値を小数とみなす」とする記述方法があります。以下のような場合です。

total = (int)((double)a * 110 / 100);

上の例では、「(double)a」とすると、変数aを一時的にdouble型で宣言されたものとして扱えます。これをさらに応用して「(int)(式)」という外側の囲みでは、式の計算結果の「値」を整数に変換しています。ただし、実数から整数に変換したときは小数以下は四捨五入されます。

このように「(変数の型)値」とする記述方法をキャスト(cast)といい、複雑な計算をするときに便利です。

カテゴリー
C

C(Step1-2)

プログラムの詳細

前回、実行環境を設定してプログラムを実行させました。ここからより詳細に説明していきます。

C言語の構成と記述規則

まずは、基本的なことから覚えましょう。以下が簡単な規則になります。

  • 基本的には上から下に処理が順番に進む
  • { と }は対応しており、囲まれた部分はひとつの処理のまとまりとなる
  • 処理ひとつひとつは、;{セミコロン}によって区切られる
  • 空白、改行、タブは、関数{命令}の間に好きなだけ入れられる

現段階では、上記を意識してプログラムを書いていきましょう。次にプログラムの内容の意味について考えていきます。

#include <stdio.h> //①

int main() //②
{
  /*表示をする ③*/
    printf("");  //④
    printf("");

    return(0); //⑤
}

①インクルード部

この部分はプログラムの中で使う関数がどのファイルに定義しているかを指定しています。このファイルをヘッダーファイルまたは、インクルードファイルといいます。#includeのあとに<>で囲んだファイル名を記述します。この部分をインクルード部といいます。

<>でファイル名を指定する時は、コンパイラで指定されている場所にあるファイルしか指定できません。ここで使っている「stdio.h」は、「standard input output(標準入出力)」の略で、キーボードからの入力、ディスプレイへの出力を行う関数の定義が書かれているファイルです。そのため、printf()を使って画面に表示させるには必ず必要になります。

②プログラムの処理の開始

main()は、C言語のプログラムに絶対に欠かせません。これは、main(メイン)関数といい、C言語では必ずひとつのmain関数が必要です。処理は関数(function)というまとまりで構成されていますが、main関数はここからプログラムの処理を開始することを表しています。

main()
{
    //処理内容
}

ソースプログラムでmain()の位置は先頭とは限りませんが、実行されるときはここから実行されます。

③コメント部

C言語のプログラムは、/*と*/で囲まれた部分はプログラムの内容とは無関係な「解説の部分」となります。これはコメント部と呼ばれ、プログラムに不可欠な記述ではありません。しかし、あとで自分が見返す時や他人の書いた処理を見たときに分かりやすいように様々な部分の処理の説明に「コメント」を書いておきましょう。日本語で書くことができるので、分かりやすさを常に意識しましょう。

④プログラムの処理内容を記述

main()の中には、{}で囲んでプログラムの処理内容や手順を記述していきます。この例では、printf関数を使って画面へ結果を表示する処理を行っています。

⑤関数の終了

ここは関数の処理の終了をあらわす部分です。C言語では、main関数から他の関数を呼び出し、さらにそれが他の関数を呼び出し…というように処理を記述していきます。ここで、return(x);が出現したら、いまの処理を終了し、「x」という結果を持って、呼び出された関数に戻ります。main()の中にあった場合、プログラムの処理終了を意味し、この記述をreturn文といいます。

printf()の詳細

文字列を画面に表示させる処理には、printf()という関数で実現していました。ここでは、より詳しく把握していきましょう。

Printf()の使い方… printf(“XXXXX”);

  • “XXXXX”には、画面に表示したい文字を記述する
  • 改行をしたい場所に「\n」を記述する
  • “XXXXX”の記述において、「%, \, “, {, }, ;」は特殊な役割がある。これらの文字を表示したいときは、その文字の前に「\」をつけて記述する。

特殊な役割を持つものは以下のように使われます。

  • \ は改行を表す「\n」などで使われる
  • ” は表示する文字列の範囲を表す
  • { } はC言語の処理のまとまりをあらわす
  • ; は処理の終わりをあらわす

これまで「%」については説明していませんでしたが、もう1つの使い方をサンプルを書いて理解しましょう。

以下のコードを書いてみてください。

#include <stdio.h>

int main()
{
    /*2種類の方法で数値の表示をする*/
    printf("1+2=3\n");
    printf("1+2=%d\n", 1+2);

    return(0);
}

このプログラムを実行すると以下のようになります。

サンプル実行結果

プログラムでは、2種類の記述をしていますが、どちらも同じ実行結果になります。ここで「%d」という記述は、「カンマ(,)の後ろに記述した値が、文字列中の%dの位置に整数として入ります」ということを表しています。

補足…C言語では、足し算は「+」、引き算は「-」、掛け算は「*」、割り算は「/」で表せ、()を使い数学と同じように計算式が書けます。

次のサンプルプログラムでは、整数値や小数値を含む様々な結果を得る方法を紹介します。

#include <stdio.h>

int main(){

   /*printfの様々な使いかた*/
   printf("10/3=%d\n", 10/3);//①
   printf("10/3=%f\n", 10.0/3.0);//②
   printf("10/3=%lf\n", 10.0/3.0);//③

   printf("7+1=%3d\n", 7+1);//④
   printf("7*9=%3d\n", 7*9);//⑤
   printf("7/3=%.3f\n", 7.0/3.0);//⑥

   return(0); 
}
サンプル実行結果

①②③は、算数でいう割り算を実行し、その結果を求めています。しかし、①と②、③の値は異なっています。それは各記述に違いがあるからです。違いは以下のようになります。

  1. 文字列中の「%d」の位置に整数(10進数)として画面に表示させる
  2. 文字列中の「%f」の位置に実数として画面に表示させる
  3. 文字列中の「%lf」の位置に精度の高い実数として画面に表示させる

*C言語では、「1,2,3」としたら整数、実数とするなら、「1.0,2.0,3.0」と明示的に記述しなくてはなりません。また、「整数」と「実数」の計算では、「実数」となります。

④⑤⑥は、計算結果を成形して表示しています。「%3d」とした場合、3桁の整数が右揃えで表示されます。「%.3f」とすれば、小数点以下何桁まで表示するか指定できます。

このように%dや%fなどのように%ではじまる記述を変換仕様(convertion    spesification)といいます。これ以外にもありますが、コードを各過程で覚えていきましょう。

カテゴリー
C

C(Step1-1)

C言語の開発環境設定

まず、始めに開発できる環境を整えましょう。Cの開発環境を今回はVisual Studio Codeを使って説明していきます。必要になるソフトは以下の2つです。

  • エディタ(Visual Studio Code)
  • C言語コンパイラ(gcc コンパイラ)

コンパイラのインストール

C言語のコンパイラには様々な種類がありますが、フリーで導入しやすいgccコンパイラをインストールしていきましょう。MinGWというものを使います。

MinGW(ミン・ジー・ダブリュー、Minimalist GNU for Windows)はGNUツールチェーンのWindows移植版である。MinGWはWindows APIのためのヘッダファイルを含んでおり、フリーのコンパイラであるGCCを、Windowsアプリケーションの開発のために利用できる。

ウィキペディア(Wikipedia)

MinGWのダウンロード

まずは、公式のサイトに行きましょう。

http://mingw-w64.org/

以下のような画像のサイトにたどり着くはずです。

MinGWサイト

この画面になったら、画面内の赤丸で囲まれたダウンロードを選択してください。

選択すると、次の画面に移ります。MinGWが入っているパッケージが表示されると思います。自分のOSに合ったパッケージを選択してダウンロードして頂ければ大丈夫です。

パッケージ選択画面

ちなみに私はWindowsなので、「Msys2」のパッケージを選択しました。今回はこの流れで説明していきます。ダウンロードして頂いて画面の指示に沿っていくと、次の画面が立ち上がると思います。

コマンドプロンプトの画面をイメージして頂けると分かりやすいです。もし、立ち上がらない場合は、プログラムメニューから MSYS2 の MSYS Shell を選択しましょう。この画面で、まず以下のコマンドを入力してください。

pacman -Syuu

もし、次のメッセージが出ていたら、画面を再度開いてもう一度実行しましょう。

警告: terminate MSYS2 without returning to shell and check for updates again
警告: for example close your terminal window instead of calling exit

続いて、以下のコマンドを入力しましょう。

pacman -S base-devel
pacman -S mingw-w64-x86_64-toolchain

これは、MinGW-w64 (64bit) の開発環境をインストールするためのコマンドになります。もし、32bitの場合は、次のように変えてください。

pacman -S mingw-w64-i686-toolchain

途中、インストールを続けますか?と表示されるので、Yes or No を入力します。基本的にはYと入力して続けて構いません。

MinGWインストール中画面

最後まで進んだら、以上でコンパイラのインストールは完了になります。

MinGWをコマンドラインから使うための設定

次にMinGWをコマンドプロンプトなどから使うためのPathの設定を行います。Path(パス)というのは、アプリケーションなどの所在をOSに教えるための仕組みになっています。コントロールパネルからシステム情報を確認できるページに飛びましょう。システムの詳細設定を選択すると環境変数(N)…の項目をクリックします。

システムのプロパティ

下のPathを編集して、新しくMinGWをインストールしたフォルダ内のbinフォルダの場所を入力します。

システム環境変数の設定

今までのインストールが成功しているなら、以下のように入力すると、Pathが通るようになります。

MinGWのPath

Pathの確認を行うのであれば、コマンドプロンプトを開き、「gcc -v」と入力してください。以下のようにgccのバージョンが表示されれば成功になります。

コマンドプロンプト「gcc -v」の実行

Visual Studio Codeの設定

拡張機能を利用してC言語を導入していきましょう。

Cの機能拡張

まずは、C言語の関数など予測して表示してくれるサポートとして「C/C++」の機能拡張を導入しましょう。VsCodeを起動して次の拡張ボタンをクリックします。

C/C++導入

検索欄から「C/C++」と検索し、インストールしましょう。続いて「Code Runner」をインストールします。C言語のプログラムはそのまま実行することはできないので、コンパイルが必要になります。この面倒な作業を簡単にしてコンパイルと実行を同時にやってくれる機能になります。

同様に、検索してインストールしましょう。

Code Runner導入

インストールが完了したら、拡張機能の設定を行います。

表示された中から「Executor Map」という項目を探し出し、settings.jsonで編集の部分を選択します。

開いたら、”code-runner.executorMap”: { 以下の項目を探します。下の赤線で引かれているように文字列「gcc」の後に「-fexec-charset=CP932」という文字列を追加しましょう。それが終わったら、settings.jsonファイルを上書き保存し閉じます。

Executor Map」の項目と同じように、設定ウインドウを下にスクロールして「Run In Terminal 」という項目を見つけてください。初期状態では項目にチェックが入っていないので、クリックしてチェックを入れてください。

Code Runnerの設定は以上になります。

これで2つの設定が完了したので、C言語ファイルを作って動作を確認してみましょう。新しく「.c」ファイルを作成し、以下のプログラムを書いてみてください。

/* 画面に何か表示するプログラム*/
#include <stdio.h>
int main(void)
{
    printf("Hello World!!\n");
    return 0;
}

そして、VsCodeの右上にある再生ボタンを押して実行します。

そうすると、画面下のターミナルに以下のように出力されます。

次からはCのプログラムを本格的に書いていきましょう。

カテゴリー
Java

Java(Step11-1)

抽象クラス

抽象クラス

オブジェクト指向のプログラムの基本から応用的なことについて説明していきます。

抽象クラス

クラスの派生を応用して、図形を表すクラス群を作っていきます。

最初に考える図形は「点」と「長方形」です。両方のクラスに描画のためのメソッドdrawを持たせることにします。2つのクラスは以下のように設計します。

■点クラスPoint

点を表すクラスで、フィールドは持ちません。メソッドdrawは、以下のように実現することで、記号文字’+’を1個だけ表示します。

void draw(){
        System.out.println('+');
}

■長方形クラスRectangle

長方形を表すクラスで、幅と高さを表すint型のフィールドwidthとheightを持たせます。メソッドdrawは以下のように実現します。

void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }

個別に定義されたクラスでメソッドdrawを作っても、それらは無関係なものになります。そこで多相性を有効に使い設計しましょう。

「図形」クラスから「点」と「長方形」を派生する

■図形クラスShape

図形を表すクラスです。点や長方形などのクラスは、このクラスから直接的、間接的に派生することにします。クラスShapeは、図形の設計図というよりも図形のという概念を表す抽象的なものになります。

  • インスタンスを生成できない、または生成すべきではない。
  • メソッドの本体が定義できない、サブクラスで具体化すべきである。

といった性質をもつクラスを表すのが抽象クラス(abstract class)です。抽象クラスとして定義すると以下のようになります。

abstract class Shape {
    abstract void draw();
}

3つのクラスの階層図は、以下のようになります。

図形クラス群のクラス階層図

クラスShapeを抽象クラスとしてそこからクラスPointとRectangleを派生するプログラムを以下に示します。

//図形クラス群

//図形
abstract class Shape {
    abstract void draw();
}
//点
class Point extends Shape{
    Point(){}

    void draw(){
        System.out.println('+');
    }
}
//長方形
class Rectangle extends Shape{
    private int width;
    private int height;

    Rectangle(int width, int height){
        this.width = width;
        this.height = height;
    }

    void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }
}
抽象メソッド

クラスShapeのメソッドdrawの先頭にabstractが付いています。このように宣言されたメソッドは、抽象メソッド(abstract method)となります。メソッドの前に付けられたabstractは、メソッドの実体をここでは定義できないので、派生したクラスで定義してください。という意味を持ちます。

クラスPointとクラスRectangleでは、メソッドdrawをオーバーライドして本体を定義しています。このように抽象クラスから派生したクラスで、抽象メソッドをオーバーライドして本体を定義することを、メソッドを実装する(implement)といいます。

抽象クラス

クラスShapeのように、抽象メソッドを1個でも有するクラスは、必ず抽象クラスとして宣言しなければならないことになっています。クラスを抽象クラスとして宣言するには、classの前にabstractを置くことです。ただ、抽象メソッドが1つもなくても抽象クラスにできます。

図形クラス群をテストするプログラムを以下に示します。

//ShapeTester.java
class ShapeTester {
    public static void main(String[] args){
        //以下の宣言はエラー:抽象クラスはインスタンス化できない
        //Shape s = new Shape();

        Shape[] a = new Shape[2];
        a[0] = new Point();
        a[1] = new Rectangle(4, 3);

        for(Shape s : a){
            s.draw();
            System.out.println();
        }
    }
}
ShapeTester.java実行結果

■抽象クラスのインスタンスは生成できない。

sの宣言がエラーとなることに注目します(コメントアウト)。抽象クラスは、具体定義のないメソッドを持っているため、new Shape()によってインスタンスを生成できません。

■抽象クラスと多相性

aは、Shape型の配列です。各要素a[0]とa[1]はShape型のクラス型変数であってShapeから派生したクラスのインスタンスを参照しています。

拡張for文では、配列a中の要素に対してメソッドdrawを呼び出します。先頭の要素に対しては、クラスPointのメソッドdrawが呼び出され、2番目の要素に対しては、クラスRectangleのメソッドdrawが呼び出されることが実行結果から確認できます。

抽象性をもつ非推奨メソッドの設計

抽象メソッドと非推奨メソッドとが入り組んだ複雑な構造のメソッドを説明します。

図形クラス群の改良

図形クラス群に対して、以下の変更・追加を行っていきます。

  • toStringメソッドの追加
  • 直線クラスの追加
  • 情報解説付き描画メソッドの追加
toStringメソッドの追加

文字列を返すtoStringメソッドの宣言を示したのが以下の図です。

図形と点と長方形におけるtoStringメソッドの実装

ここでは、非常に重要な点に着目しましょう。

クラスShapeでは、toStringメソッドを抽象メソッドとして宣言している。

具体的な図形ではないクラスShapeは、適切な文字列として表現できないからです。toStringメソッドは、Javaの全クラスの親玉であるObjectクラス内で定義されたメソッドです。また、extendsを与えられずに宣言されたクラスは、暗黙の内にObjectクラスから派生しています。

よって、クラスShapeは、スーパークラスであるObjectの非抽象メソッドを、抽象メソッドとしてオーバーライドしていることになります。toStringメソッドを抽象メソッドと宣言することは、そのクラスの下位クラスに対して、toStringメソッドの実装を強要する役割を持っています。

直線クラスの追加

次に直線クラスの追加について考えてみましょう。水平直線クラスHorzLineと垂直直線クラスVirtLineは、クラスShapeから派生しており次の図のようになります。

抽象クラスから水平直線クラスと垂直直線クラスの派生

これらのクラスに対して、長さを表すフィールドlengthのアクセッサを追加していくことを考えてみましょう。値を取得するgetLengthとセットするsetLengthは、両クラスとも全く同じものになります。水平直線と垂直直線の共通部を直線クラスとして独立させ、そのクラスから水平直線クラスと垂直直線クラスを派生した方がよいです。

情報解説付きメソッドの追加

メソッドprintでは、以下の2つを実装します。

  • toStringメソッドが返す文字列を表示する。
  • メソッドdrawによる描画を行う。

点クラスPoint2と長方形クラスRectamgle2を例にとると、以下のようになります。

点と長方形におけるメソッドprint

ここで注意するのは以下の点です。

両クラスのメソッドprintの定義が、まったく同じである。

このように無駄な部分は、スーパークラスでくくりだすべきです。図形クラスShapeの中でメソッドprintを定義するように書きだすと良いです。

改良した図形クラス

ここまでの設計を元に作成した図形クラス群のクラス階層図は以下になります。

図形クラス群のクラス階層図

さらに各クラスのプログラムを以下に示します。

//Shape2.java
//クラスShapeは、図形の概念を表す抽象クラス
//具体的な図形クラスはこのクラスから派生します。

public abstract class Shape2 {
    
    /*図形情報を表す文字列を返却するメソッド
    クラスShapeから派生し、このメソッド本体を実装*/
    public abstract String toString();

    /*メソッドdrawは、図形を描画するためのメソッド
    クラスShapeから派生し、このメソッド本体を実装*/
    public abstract void draw();

    //メソッドprintは、図形情報の表示と図形の表示を行う
    public void print(){
        System.out.println(toString());
        draw();
    }
}
//Point2.java
//クラスPointは、点を表すクラス
//Shapeから派生しています
public class Point2 extends Shape2 {
    
    /*点を生成するコンストラクタ
    受け取る引き数はなし*/
    public Point2(){
        //何も行わない
    }

    /*メソッドtoStringは点に関する図形情報を表す文字列を返却
    返却する文字列はPointです*/
    public String toString(){
        return "Point";
    }

    /*メソッドdrawは、点を描画するメソッド
    プラス記号を1つ表示して改行*/
    public void draw(){
        System.out.println('+');
    }
}
//AbstLine.java
//クラスAbstLineは直線を表す抽象クラス
//Shapeクラスから派生したクラス
public abstract class AbstLine extends Shape2{
    //直線の長さを表すint型のフィールド
    private int length;

    /*直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public AbstLine(int length){
        setLength(length);
    }
    /*直線の長さを取得*/
    public int getLength(){
        return length;
    }
    /*直線の長さを設定*/
    public void setLength(int length){
        this.length = length;
    }
    /*メソッドtoStringは、直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "AbstLine(length:" + length + ")";
    }
}
//HorzLine.java
//クラスHorzLineは水平直線を表すクラス
//AbstLineから派生したクラス
public class HorzLine extends AbstLine {

    /*水平直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public HorzLine(int length){super(length);}

    /*メソッドtoStringは、水平直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "HorzLine(length:" + getLength() + ")";
    }
    /*メソッドdrawは、水平直線を描画
    マイナス記号を横に並べます*/
    public void draw(){
        for(int i = 1; i <= getLength(); i++)
            System.out.print('-');
        System.out.println();
    }
}
//VirtLine.java
//クラスVirtLineは垂直直線を表すクラス
//AbstLineから派生したクラス
public class VirtLine extends AbstLine {

    /*垂直直線を生成するコンストラクタ
    長さを引数として受け取る*/
    public VirtLine(int length){super(length);}

    /*メソッドtoStringは、垂直直線に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "VirtLine(length:" + getLength() + ")";
    }
    /*メソッドdrawは、垂直直線を描画
    縦線記号を横に並べます*/
    public void draw(){
        for(int i = 1; i <= getLength(); i++)
            System.out.println('|');
    }
}
//Rectangle2.java
//クラスRectangle2は、長方形を表すクラスです。
public class Rectangle2 extends Shape2{
    //長方形の幅を表すint型のフィールド
    private int width;
    //長方形の高さを表すint型のフィールド
    private int height;

    /*長方形を生成するコンストラクタ
    幅と高さを引数として受け取る*/
    public Rectangle2(int width, int height){
        this.width = width;
        this.height = height;
    }
    /*メソッドtoStringは、長方形に関する図形情報を表す文字列を返却*/
    public String toString(){
        return "Rectangle(width:" + width + ", height:" + height + ")";
    }
    /*メソッドdrawは、長方形を描画
    描画はアスタリスクを並べます*/
    public void draw(){
        for(int i = 1; i <= height; i++){
            for(int j = 1; j <= width; j++)
                System.out.print('*');
            System.out.println();
        }
    }
}

図形クラス群を利用するプログラムを例を以下に示します。このプログラムでは、多相性の効果を確認するためにクラスShape型の配列を利用しています。拡張for文では、全ての要素に対してメソッドprintを起動しています。正しく実行されていることが実行結果から分かります。

//ShapeTester2.java
class ShapeTester2 {
    public static void main(String[] args){
        Shape2[] p = new Shape2[4];

        p[0] = new Point2();
        p[1] = new HorzLine(5);
        p[2] = new VirtLine(3);
        p[3] = new Rectangle2(4, 4);

        for(Shape2 s : p){
            s.print();
            System.out.println();
        }
    }    
}
ShapeTester2.java実行結果
カテゴリー
Java

Java(Step10-2)

クラスの派生と多相性

多相性

メソッドのオーバーライド

次のプログラムを見てみましょう。このプログラムには、2つのクラスが定義されており、クラスPetとRobotPetです。

//Pet.java
//ペットクラス
public class Pet {
    private String name;
    private String masterName;
    //コンストラクタ
    public Pet(String name, String masterName){
        this.name = name;
        this.masterName = masterName;
    }

    //ペット名を調べる
    public String getName(){return name;}
    //持ち主名を調べる
    public String getMasterName(){return masterName;}

    //自己紹介
    public void introduce(){
        System.out.println("■私の名前は" + name + "です。");
        System.out.println("■持ち主は" + masterName + "です。");
    }
}

class RobotPet extends Pet{
    //コンストラクタ
    public RobotPet(String name, String masterName){
        super(name, masterName); //スーパークラスのコンストラクタ
    }

    //自己紹介
    public void introduce(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }

    //家事を行う
    public void work(int sw){
        switch(sw){
            case 0: System.out.println("掃除します。"); break;
            case 1: System.out.println("洗濯します。"); break;
            case 2: System.out.println("炊事します。"); break;
        }
    }
}

■クラスPet

フィールド

  • name…ペットの名前
  • mastername…主人の名前

コンストラクタ

  • Pet…ペットと主人の名前を設定

メソッド

  • getName…ペットの名前を調べるメソッド
  • getMasterName…主人の名前を調べるメソッド
  • introduce…自己紹介するメソッド

■クラスRobotPet

フィールド

クラスPetのフィールドを継承しています。

コンストラクタ

  • RobotPet…ペットと主人の名前を設定します。設定作業は、super(…)を呼び出すことによって、スーパークラスPetのコンストラクタに任せます。

メソッド

  • introduce…自己紹介メソッド(クラスPetのものを上書きしています)
  • work…家事をするメソッド。家事の種類は、引数でも0,1,2の値として指定します。

メソッドintroduceのように、スーパークラスのメソッドと同形式のメソッドに、サブクラスで別の定義を与える事を「オーバーライドする(override)」と表現します。

多相性

上記2つのクラスをテストするプログラムを以下に示します。

//PetTester.java
//ペットクラスの利用例

class PetTester {
    public static void main(String[]args){
        Pet tarou = new Pet("Tarou", "あなた");
        tarou.introduce();
        System.out.println();

        RobotPet r2d2 = new RobotPet("R2D2", "ジェダイ");
        r2d2.introduce();
        System.out.println();

        Pet p  =r2d2;
        p.introduce();
    }    
}
PetTester.java実行結果
  1. クラスPetのインスタンスTarouを生成して、自己紹介を行わせます。呼び出されるのは、クラスPetに所属するメソッドintroduceです。
  2. クラスRobotPetのインスタンスr2d2を生成して、自己紹介を行わせます。呼び出されるのは、クラスRobotPetに所属するメソッドintroduceです。
  3. Pet型の変数pが、RobotPet型のインスタンスを参照するように初期化しています。

ここで、メソッド呼び出しp.introduce()に着目しましょう。このメソッド呼び出しには、以下のA方式とB方式のどちらに解釈されるでしょうか?

A.ペットPet用の自己紹介メソッドが呼び出される。

変数pの型がPetであるため、Pet型の自己紹介用メソッドintroduceが呼び出される。

B.ロボット型ペットRobotPet用の自己紹介メソッドが呼び出される。

参照先のインスタンスがRobotPet型であるため、RobotPet型の自己紹介用メソッドintroduceが呼び出される。

A方式では、呼び出すメソッドがコンパイル時に決定し、B方式では、呼び出すメソッドが実行時に決定します。実際に生成されるコードはB方式です。実行結果からもわかると思います。

クラス型変数が、派生関係にある様々なクラス型のインスタンスを参照できることを、多相性=ポリモーフィズム(polymorphism)と呼びます。

多相性が絡んだメソッド呼び出しでは、プログラム実行時に呼び出すメソッドが決定されます。そのメリットは、以下の通りです。

  • 異なるクラス型のインスタンスに対して同一のメッセージを送れる
  • メッセージを受け取ったインスタンスは自分自身の型が何であるかを知っており、適切な行動を起こす。

A方式は、呼び出すメソッドをコンパイル時に決定できるため、その呼び出しメカニズムは、静的結合(static binding)や早期結合(early binding)と呼ばれます。一方で、Javaで採用されているB方式は、呼び出すメソッドが実行時に決定されるため、その呼び出しメカニズムは動的結合(dynamic binding)や遅延結合(late binding)と呼ばれます。

動的結合をメソッドの引数に応用したプログラム例を以下に示します。

//PetTester2.java
//ペットクラスの利用例

class PetTester2 {
    //pが参照するインスタンスに自己紹介させる
    static void intro(Pet p){
        p.introduce();
    }    

    public static void main(String[] args){
        Pet[] a = {
            new Pet("Tarou", "あなた"),
            new RobotPet("R2D2", "ジェダイ"),
            new Pet("T-800", "未来のあなた"),
        };

        for(Pet p : a){
            intro(p);  //pが参照するインスタンスに自己紹介させる
            System.out.println();
        }
    }
}
PetTester2.java実行結果

オブジェクト指向の3大要素

ここまでで、継承と多相性について説明していきましたが、以下の3つはオブジェクト指向の3大要素と呼びます。

  • クラス
  • 継承
  • 多相性

参照型のキャスト

引き続き、クラスPetとクラスRobotPetを考えます。以下に示すように、スーパークラス型の変数はサブクラスのインスタンスを参照できます。

Pet p = new RobotPet("R2D2", "ジェダイ");

この時、RobotPet型への参照が、Pet型への参照へと暗黙の内にキャストされています。ここで行われる型変換は、参照型の拡大変換(widening reference conversion)またはアップキャスト(up cast)と呼ばれます。

一方、サブクラス型の変数はスーパークラスのインスタンスを参照できません。以下のようにキャスト演算子を明示的に適用すれば、型変換は可能になります。

Pet p = (Pet)new RobotPet("R2D2", "ジェダイ");

ここで行われる型変換は、参照型の縮小変換(narrowing reference conversion)または、ダウンキャスト(down cast)と呼ばれます。

instanceof 演算子

クラス型の変数は、そのクラス型のインスタンスだけでなく、上位クラスのインスタンスを参照することも、下位クラスのインスタンスを参照することもできます。そうすると、変数が参照しているのがどのクラスか調べる事も必要になります。それを行うプログラム例を以下に示します。

//PetInstanceOf.java
//instanceof演算子の利用例

class PetInstanceOf {
    public static void main(String[] args){
        Pet[] a = {
            new Pet("Tarou", "あなた"),
            new RobotPet("R2D2", "ジェダイ"),
            new Pet("T-800", "未来のあなた"),
        };

        for(int i = 0; i < a.length; i++){
            System.out.println("a[" + i + "]");
            if(a[i] instanceof RobotPet)
                ((RobotPet)a[i]).work(0);
            else
                a[i].introduce();
        }
    }
}
PetInstanceOf.java実行結果

初登場のinstanceof演算子を演算子を利用しています。これは、以下の形式で利用する関係演算子の1種です。

クラス型変数名 instanceof クラス名

x instanceof t変数xが型tに暗黙の内にキャストできる下位クラスであればtrueを、そうでなければ、falseを生成。
instanceof演算子

@Override アナテイション

以下に示すプログラム部分を見てください。クラスRobotPetと自己紹介用メソッドの部分です。introduceとすべきメソッド名を、introductionに間違えています。

class RobotPet {
    //中略
    public void introduction(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

コンパイラでは、メソッドintroductionが新規に宣言されているとみなされてしまいます。そのため、スーパークラスで定義されたintroduceはそのまま継承され、定義されたintroductionは新しく追加されたメソッドとして扱われます。

このようなミスを防ぐのに有効なのが、アナテイション(annotation)です。プログラムの読み手に伝えるものをコメントとして記述しますが、アナテイションは、人にもコンパイラにも伝える注釈になります。

使い方は簡単で、以下のようにメソッド宣言の名前に前に @override と付けるだけです。

class RobotPet {
    //中略
    @Override public void introduction(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

このアナテイションは、以下のことを示します。

「これから宣言するのは、上位クラスのメソッドをオーバーライドするメソッドです。本クラスで新しく追加するメソッドではないですよ。」

この場合、スーパークラスPetにメソッドintroductionが無いため、コンパイラはエラーを吐きます。これで、ミスに気づき、以下のようにプログラムの修正ができます。

class RobotPet {
    //中略
    @Override public void introduce(){
        System.out.println("◇私の名前は" + getName() + "です。");
        System.out.println("◇持ち主は" + getMasterName() + "です。");
    }
    //中略
}

継承とアクセス性

クラスの派生において、フィールドやメソッドは継承されるが、コンストラクタは継承されません。それらを明確にしていきたいと思います。

メンバ

クラスの派生で継承されるのは、クラスのメンバ(member)に限られます。以下にメンバを示します。

  • フィールド
  • メソッド
  • クラス
  • インタフェース

スーパークラスのメンバは、原則としてそのまま継承されます。ただし、非公開アクセス性をもつメンバ、privateで宣言されたメンバは継承されません。

メンバでない、クラスの資産には以下のものがあります。

  • インスタンス初期化子
  • 静的初期化子
  • コンストラクタ

これらは継承されません。

finalなクラスとメソッド

final付きで宣言されたクラスやメンバは、派生において特別な扱いになります。

finalクラス

finalクラスから派生を行うことはできません。つまり、finalクラスをスーパークラスとして作ることはできません。たとえば、文字列を表すStringクラスは、finalクラスになります。以下のように、Stringクラスを拡張したクラスを作ることはできません。

class DeluxeString extends String{  //エラー
   // ……
}

これは、以下の原則を示しています。

拡張すべきでないクラスは、finalクラスとして宣言せよ。

finalメソッド

finalメソッドは、サブクラスでオーバーライドすることができません。以下のようにメソッドの先頭にfinalを付けて宣言します。

final void f(){/* …… */}

オーバーライドとメソッドのアクセス性

メソッドをオーバーライドする際は、アクセス性と関連する以下の規則を必ず知っておかなければなりません。

アクセス制限の強さ、弱さの関係を示したのが以下の図になります。

メソッドのアクセス制限

下位クラスで上位クラスのものよりも強いアクセス制限の修飾子を与えることができません。その規則をまとめたのが以下の表です。

A/B公開限定公開パッケージ非公開
公開×××
限定公開××
パッケージ×
非公開××××
アクセス制限のリスト

Javaの全ての親玉であるObjectクラスは、toStringがpublicメソッドとして定義されています。toStringクラスをオーバーライドする際は、必ずpublicを付ける必要があります。

カテゴリー
Java

Java(Step10-1-2)

クラスの派生と多相性

継承②

メソッドの上書きとsuperの正体

スーパークラスのコンストラクタを呼びだすための super の正体は、そのクラスに部分として含まれるスーパークラス部への参照です。そのため、次の規則があります。

super. メンバ名によって、スーパークラスのメンバをアクセスできる。

このことを、以下に示すプログラムで確認しましょう。

//SuperTester.java
//スーパークラスとサブクラス

//スーパークラス
class Base{
    protected int x; //限定公開

    Base(){this.x = 0;}
    Base(int x){this.x = x;}

    void print(){System.out.println("Base.x = " + x);}
}
//サブクラス
class Derived extends Base{
    int x; //スーパークラスと同一名のフィールド

    Derived(int x1, int x2){super.x = x1; this.x = x2;}
    //スーパークラスのメソッドを上書き(オーバーライド)
    void print(){super.print(); System.out.println("Derived.x = " + x);}
}

public class SuperTester {
    public static void main(String[] args){
        Base a = new Base(10);
        System.out.println("- a -"); a.print();
        Base b = new Derived(20, 30);
        System.out.println("- b -"); b.print();
    }    
}

■クラスBase

このクラスで宣言されたフィールドxは、protected付きで宣言されています。このように宣言されたメンバは、たとえパッケージの外であってもサブクラスからアクセスできる限定公開アクセスになります。

■クラスDerived

このクラスでは、フィールドxを宣言しています。クラスBaseのフィールドxと同名ですが、クラスBaseから継承したものとは別物で扱われます。

クラスの派生とsuper

コンストラクタに注目すると、super.xは、スーパークラスBaseから継承したフィールドxのことであり、this.xは、自分自身のクラスDerivedで宣言されたフィールドxのことです。

メソッドprintは、引数を受け取らず値を返却しないので、クラスBaseのものと同じ形式になります。このメソッド中のsuper.print()は、スーパークラスBaseに所属するメソッドprintの呼び出しです。

クラス階層

ここまでの例は、あるクラスから別のクラスを派生する例でしたが、派生したクラスからさらに派生を行うこともできます。その例が以下になります。

クラスの派生

クラスAからクラスBを派生し、クラスBからクラスC、Dへ派生しています。クラスBはAのです。そして、クラスCとDは、Bのであると同時に、Aのです。すべてのクラスに血縁関係があります。

親を含めた上側のクラスを祖先、子を含めた下側のクラスを子孫と呼ぶことにすると、上位クラス・下位クラスは以下のように定義されます。

  • 上位クラス(super class)…祖先クラス
  • 下位クラス(sub class)…子孫クラス
  • 直接上位クラス(direct super class)…親クラス
  • 直接下位クラス(direct sub class)…子クラス

より詳細に示した表も併せて以下に示します。

名称定義
スーパークラス派生の元になったクラス(親)
サブクラス派生によって作られたクラス(子)
上位クラス親を含めた祖先クラス
下位クラス子を含めた子孫クラス
間接上位クラス親を除いた祖先クラス
間接下位クラス子を除いた子孫クラス
各クラスの定義

Javaでは、複数のクラスから派生を行う多重継承がサポートされていません。そのため、以下に示すクラスはコンパイルエラーになります。

Javaは多重継承をサポートしない

Objectクラス

以下の図を見てみましょう。この図では、Objectというクラスが登場します。クラスAのように、extendsを付けずに宣言されたクラスは、Objectクラスのサブクラスになります。

クラスの派生

したがって、今まで作成してきたextendsを伴わないクラスは、objectクラスのサブクラスだったということです。

差分プログラミング

銀行口座クラスに対し、定期預金を追加する例を別のクラスとして作り直すのではなく、派生によってクラスを作ることにしましょう。それが以下のクラスです。

//TimeAccount2.java
class TimeAccount2 extends Account2 {
    private long timeBalance;

    TimeAccount2(String name, String no, long balance, long timeBalance){
        super(name, no, balance);
        this.timeBalance = timeBalance;
    }

    long getTimeBalance(){
        return timeBalance;
    }

    void cancel(){
        deposit(timeBalance);
        timeBalance = 0;
    }
}

本クラスでは、フィールド・コンストラクタ・2つのメソッドのみを宣言します。それ以外のフィールドとメソッドは、スーパークラスAccount2から継承しているので、新たに宣言する必要がありません。

継承のメリットの1つは、「既存プログラムに対する必要最低限の追加。修正だけで新しいプログラムが完成する」という差分プログラミング(incremental programming)が行われることです。プログラム開発時の効率アップや保守性の向上が図れます。

is-Aの関係とインスタンスへの参照

TimeAccount2は、Account2の子供であって、Account2家に属していると考えられます。このことはis-Aの関係と呼ばれ、以下のように表現します。

TimeAccount2は、一種のAccount2である。

この関係の逆は成立しないことに注意しましょう。この関係をプログラム上で活用した例を以下に示します。

//TimeAccount2Tester.java
//is-Aの関係とインスタンスへの参照
class TimeAccount2Tester{
    public static void main(String[]args){
        Account2 tarou = new Account2("山田太郎", "12345", 1000);
        TimeAccount2 hanako = new TimeAccount2("山田花子", "67890", 200, 500);

        Account2 x;
        x = tarou; //自分自身の型のインスタンス参照可
        x = hanako; //下位クラス型のインスタンス参照可

        System.out.println("xの預金残高:" + x.getBalance());

        TimeAccount2 y;
        //y = tarou; //上位クラス型のインスタンス参照不可
        y = hanako; //自分自身の型のインスタンス参照可

        System.out.println("yの預金残高:" + y.getBalance());
        System.out.println("yの定期預金残高" + y.getTimeBalance());
    }
}
TimeAccount2Tester.java実行結果

mainメソッドの冒頭で、以下に示す2つのインスタンスを生成しています。

  • tarou…銀行口座クラスAccount2型のインスタンス
  • hanako…定期預金付き銀行口座クラスTimeAccount2型のインスタンス

その後で宣言されている変数xは、Account2型のクラス型変数で、変数yはTimeAccount型のクラス型変数です。

次に考えるのは、以下のプログラムです。

//TimeAccount2Tester2.java
//is-Aの関係とインスタンスへの参照

class TimeAccount2Tester2 {
    //どちらの預金残高が多いか
    static int compBalance(Account2 a, Account2 b){
        if(a.getBalance() > b.getBalance()) //aの方が多い
            return 1;
        else if(a.getBalance() < b.getBalance()) //bの方が多い
            return -1;
        return 0; //aとbは同じ
    }
    
    public static void main(String[]args){
        Account2 tarou = new Account2("山田太郎", "12345", 1000);
        TimeAccount2 hanako = new TimeAccount2("山田花子", "56789", 200, 500);

        switch(compBalance(tarou, hanako)){
            case 0: System.out.println("二人の預金残高は同じ。"); break;
            case 1: System.out.println("太郎君の預金残高が多い。"); break;
            case -1: System.out.println("花子ちゃんの預金残高が多い。"); break;
        }
    }
}
TimeAccount2Tester2.java実行結果

仮引数aとbの型はAccount2型です。mainメソッドでは、それらに対して、Account2型インスタンスへの参照とTimeAccount2型への参照を渡しています。これが上手くいくのは、Account2型の変数が、Account2型のインスタンスと、TimeAccount2型インスタンスのいずれも参照できるためです。

※メソッドのクラス型引数に対しては、そのクラス型のインスタンスへの参照だけでなく、そのクラスの下位クラス型のインスタンスへの参照を渡すことができる。

カテゴリー
Java

Java(Step10-1-1)

クラスの派生と多相性

継承①

既存クラスの資産を継承しつつ、新しくクラスを作るための技術である、クラスの派生について説明していきます。

銀行口座クラス改良

以前、銀行口座クラスを作成しましたが、ここではさらに改良して「定期預金」を表せるように変更することを考えます。以下に示すフィールドとメソッドを追加しましょう。

  • 定期預金の残高を表すフィールド
  • 定期預金の残高を調べるメソッド
  • 定期預金を解約して全額を普通預金に移すメソッド

上記のフィールドとメソッドを追加したクラスを以下に示します。

//TimeAccount.java
//定期預金付き銀行口座クラス

class TimeAccount {
    private String name; //口座名義
    private String no;  //口座番号
    private long balance;  //預金残高
    private long timeBalance; //預金残高

    //コンストラクタ
    TimeAccount(String n, String num, long z, long timeBalance){
        name = n;
        no = num;
        balance = z;
        this.timeBalance = timeBalance;
    }
    
    //口座名義を調べる
    String getName(){
        return name;
    }

    //口座番号を調べる
    String getNo(){
        return no;
    }

    //預金残高を調べる
    long getBalance(){
        return balance;
    }

    //定期預金残高を調べる
    long getTimeBalance(){
        return timeBalance;
    }

    //k円預ける
    void deposit(long k){
        balance += k;
    }

    //k円おろす
    void withdraw(long k){
        balance -= k;
    }

    //定期預金を解約して全額普通預金に移す
    void cancel(){
        balance += timeBalance;
        timeBalance = 0;
    }
}

クラス TimeAccount は、「定期預金を扱う」という目的は満たせます。しかし、普通預金だけの銀行口座クラスとの互換性は失われます。そのことを以下に示すメソッドで考えましょう。これは、2つの口座の預金残高を比較して、その大小関係を整数値として返すメソッドです。

//どちらの預金残高が多いか
    static int compBalance(Account2 a, Account2 b){
        if(a.getBalance() > b.getBalance()) //aの方が多い
            return 1;
        else if(a.getBalance() < b.getBalance()) //bの方が多い
            return -1;
        return 0; //aとbは同じ
    }

このメソッドに対して、TimeAccount 型インスタンスを渡すことは不可能です。クラス TimeAccount と Account2 クラスは別物だからです。

派生と継承

このような問題を解決するのが、クラスの派生(derive)です。派生とは、既存クラスのフィールドやメソッドなどの資産を継承(inheritance)した新しいクラスを作り出すことです。なお、派生の際は、資産を継承するだけでなくフィールドやメソッドを追加・上書きできます。

例えば、Baseというクラスがあって、その資産を継承したクラスの Derived を派生するイメージを下の図に表します。派生によって新しく作るクラスの宣言では、extends に続いて派生元のクラス名を書きます。

クラスの派生

派生元になるクラスと、派生によって作られたクラスのことを以下のように表現します。

  • 派生元のクラス…親クラス/上位クラス/基底クラス/スーパークラス
  • 派生したクラス…子クラス/下位クラス/派生クラス/サブクラス

各クラスの資産は次のようになっています。

■クラス Base

フィールドは、a、b の2個でメソッドは、a、bのセッタとゲッタの4個です。

■クラス Derived

このクラスで宣言しているのはフィールド c と、そのセッタとゲッタです。さらにクラス Baseのフィールドとメソッドを継承しているので、それらを合わせると、フィールドは3個、メソッドは6個になります。

サブクラスは、スーパークラスのフィールドやメソッドなどの資産を継承しますので、スーパークラスのメンバはサブクラス中に含まれます。そのため、スーパークラスとサブクラスの資産は図に示す関係になります。

派生による資産の継承とクラス階層図

派生とコンストラクタ

派生において継承されない資産があります。その1つがコンストラクタです。コンストラクタについては、クラスの派生と関連して、いくつかの点を必ず押さえる必要があります。まずは、派生を行う具体的なプログラムを見てみましょう。

//PointTester.java
//2次元座標クラスと3次元座標クラス

//2次元座標クラス
class Point2D{
    int x;
    int y;

    Point2D(int x, int y){this.x = x; this.y = y;}

    void setX(int x){this.x = x;}
    void setY(int y){this.y = y;}
    int getX(){return x;}
    int getY(){return y;}
}
//3次元座標クラス
class Point3D extends Point2D{
    int z;

    Point3D(int x, int y, int z){super(x, y); this.z = z;}

    void setZ(int z){this.z = z;}
    int getZ(){return z;}
}
public class PointTester {
    public static void main(String[] args){
        Point2D a = new Point2D(5, 10);
        Point3D b = new Point3D(15, 20 ,25);

        System.out.printf("a = (%d, %d)\n", a.getX(), a.getY());
        System.out.printf("b = (%d, %d, %d)\n", b.getX(), b.getY(), b.getZ());

    }
}

①スーパークラスのコンストラクタはsuper(…)によって呼び出せる。

このプログラムでは、2次元座標クラス Point2D、3次元座標クラス Point3D、それらをテストするクラスが定義されています。

  • クラス Point2D…2次元座標クラス(X座標/Y座標)

座標を表す2個のフィールドと、x、yと、4個のセッタ・ゲッタと、コンストラクタから構成されるクラスです。コンストラクタは仮引数x、yに受け取ったX座標とY座標の値を、フィールドxとyに設定します。

  • クラス Point3D…3次元座標クラス(X座標/Y座標/Z座標)

2次元座標クラスPoint2Dクラスから派生したクラスです。X座標とY座標に関するフィールドとメソッドは、そのまま継承します。新しく追加されたのは、Z座標のフィールドzと、そのセッタ、ゲッタです。

コンストラクタ内の super(x, y); に注目してみましょう。この式は、スーパークラスのコンストラクタの呼び出しになります。super(x, y)の呼び出しを行うのは、仮引数x、y に受け取った値をフィールドとxとyに代入する作業を、スーパークラスのコンストラクタに委ねるためです。その結果、クラスPoint3Dのコンストラクタ内で直接値を代入するのは、新たに追加されたZ座標用フィールドzだけですみます。ただし、super(…)の呼び出しは、コンストラクタの先頭でのみ行えます。

②サブクラスのコンストラクタ内では、スーパークラスに所属する「引数を受け取らないコンストラクタ」が自動的に呼び出される。

クラスPoint3Dのコンストラクタからsuper(…)の呼び出しを削除して、以下のように書きかえてみましょう。そうすると、コンパイルエラーが発生します。

//コンパイルエラー
Point3D(int x, int y, int z){this.x = x; this.y = y; this.z = z;}

このようなsuper(…)を明示的に呼び出さないコンストラクタには、スーパークラスに所属する「引数を受け取らないコンストラクタ」の呼び出し、すなわちsuper()の呼び出しがコンパイラによって自動的に挿入歌されます。以下のように書きかえられます。

//コンパイルエラー
Point3D(int x, int y, int z){super(); this.x = x; this.y = y; this.z = z;}

コンパイルエラーとなる理由は単純で、スーパークラスPoint2Dに「引数を受け取らないコンストラクタ」が存在せず、それを呼び出せないからです。

③コンストラクタを1つも定義しなければ、super()を呼び出すだけのデフォルトコンストラクタが自動的に定義される。

コンストラクタを1個も定義しないクラス X に対して、何も行わない空のコンストラクタであるデフォルトコンストラクタが、コンパイラによって以下の形式で自動的に定義されますと以前説明しました。

X(){}

そこでの解説は、厳密にいうと説明不足で、自動生成されるデフォルトコンストラクタは本当は以下のようになります。

X(){super();}  //コンパイラによって生成されるデフォルトコンストラクタ

このことを検証する具体的なプログラム例を以下に示します。

//DefaultConstructor.java
//スーパークラスとサブクラス

//スーパークラス
class A{
    private int a;
    A(){a = 50;}
    int getA(){return a;}
}
//サブクラス
class B extends A{
    //デフォルトコンストラクタが生成
}

public class DefaultConstructor {
    public static void main(String[] args){
        B x = new B();
        System.out.println("x.getA() = " + x.getA());
    }
}
DefaultConstructor.java実行結果

クラスAが持つ唯一のフィールドが、int型のaです。コンストラクタは、そのフィールドaに50を代入します。メソッドはgetAは、その値のゲッタです。クラスBは、クラスAのサブクラスです。コンストラクタが1個も定義されないため、デフォルトコンストラクタがコンパイラで定義されます。

注意点としては、クラスにコンストラクタを定義しない場合、そのスーパークラスが、「引数を受け取らないコンストラクタ」を持っていなければならない。

カテゴリー
Java

Java(Step9-2)

パッケージ②

パッケージの宣言

パッケージを作る側に立って、パッケージを構築していきましょう。

パッケージ

まずは、パッケージ宣言(package declation)です。以下の形式で行います。

package パッケージ名;

この宣言は、インポート宣言やクラス宣言よりも前に置かなければなりません。そのため、翻訳単位(translation unit)の構文図は以下のようになります。

翻訳単位の構文図

この構文図は、パッケージ宣言について以下のことを示しています。

  • パッケージ宣言はなくてもよい。
  • パッケージ宣言を2個以上置くことはできない。

■無名パッケージ

ソースファイルにパッケージ宣言が無い場合には、そのソースファイル中で定義したクラスは無名パッケージ(unnamed package) に所属します。無名パッケージに所属するクラスの完全限定名は、単純名と一致します。無名パッケージをディレクトリに例えると、ルートディレクトリのようなものになります。

■パッケージ宣言

パッケージ宣言が置かれたソースプログラム中で宣言されたすべてのクラスは、そのパッケージに所属することになります。例として以下のコードを見てみましょう。

package shape;

class Triangle{
   //…
}

class Rectangle{
  //…
}

この場合は、2つのクラスがパッケージに所属することになります。その結果、各クラスの単純名と完全限定名は以下のようになります。

  • 単純名 Triangle 完全限定名 shape.Triangle
  • 単純名 Rectangle 完全限定名 shape.Rectangle

互いのクラスを単純名でアクセスすることができます。パッケージとクラスの命名に当たっては注意が必要です。それは、1つのパッケージ中に、同一名のパッケージとクラスが存在することが許されないです

パッケージとディレクトリ

パッケージを作成する場合は、パッケージ名と同一名のディレクトリ中にソースファイルとクラスファイルを置くのが基本的な構成です。その例がを示したのが以下の図です。

パッケージとディレクトリの構成

パッケージ名が a なので、a という名前のディレクトリを作成して、その中にソースファイル A1.java を格納します。クラス a.A1 を利用しているのが、以下の2つです。

  • X.java…クラス A1 を完全限定名 a.A1 として利用します。
  • Y.java…a.A1 をインポートする宣言した上でクラス A1 を単純名で利用します。

実際にパッケージを作ってみましょう。クラスRandId をパッケージ id に所属させましょう。

//RandId.java
//識別番号クラス
package id;

import java.util.Random;

public class RandId {
    private static int counter;
    
    private int id;

    static{
        Random rand = new Random();
        counter = rand.nextInt(10) * 100;
    }
    //コンストラクタ
    public RandId(){
        id = ++counter;
    }
    //識別番号取得
    public int getId(){
        return id;
    }
}
  1. パッケージの宣言です。import 宣言よりも前に置かなければなりません。このソースプログラム内で定義されるクラス RandId は、パッケージ id に所属します。
  2. クラス RandId の宣言です。単純名は RandId で、完全限定名は、id.Randidになります。

このクラスを利用するプログラムを以下に示します。package宣言がないので、クラス RandIdTester は無名パッケージに所属します。

//RandIdTester.java
//識別番号クラスRandIdの利用例

import id.RandId;

public class RandIdTester {
    public static void main(String[] args){
        RandId a = new RandId();
        RandId b = new RandId();
        RandId c = new RandId();

        System.out.println("aの識別番号:" + a.getId());
        System.out.println("bの識別番号:" + b.getId());
        System.out.println("cの識別番号:" + c.getId());
    }
}
RandIdTester.java実行結果

2つのプログラムのディレクトリ構成の例を示したのが、次の図になります。

識別番号クラスのパッケージとディレクトリ構成
一意なパッケージ名

自作したクラス群をパッケージ化して広く公開する際には、誰もが使いそうなパッケージ名を与えるわけにはいきません。パッケージには一意な名前を与える必要があります。そのために推奨されているのが、インターネットのアドレスを逆順に並べるという方法です。

クラスとメンバのアクセス性

パッケージによるカプセル化について言及していきます。

クラスのアクセス制御

  • public クラス
  • 非 public クラス
パッケージとクラスのアクセス性

■public クラス

キーワード public付きで宣言されたクラスになります。パッケージとは無関係に利用できます。クラスのアクセス性は、公開アクセス(public access)となります。

■非 public クラス

キーワード publicを付けずに宣言されたクラスです。そのクラスが所属するパッケージ内からは利用できますが、他のパッケージからは利用できません。クラスのアクセス性は、パッケージアクセス(package access)になります。なお、パッケージアクセスは、デフォルトアクセス(default access)と呼ばれます。

メンバのアクセス制御

メンバのアクセス性には、以下の4種類があります。

  • 公開(public)アクセス
  • 限定公開(protected)アクセス
  • パッケージ(default)アクセス
  • 非公開(private)アクセス

上の4つの規則を簡単にまとめたのが以下の表になります。

キーワード/クラスpublicクラス非 publicクラス
public公開アクセスパッケージアクセス
protected限定公開アクセス×
(なし)パッケージアクセス×
private非公開アクセス×
メンバの宣言とアクセス性

これらのアクセス性を図を見ながら理解を深めましょう。

パッケージとメンバのアクセス性

■公開(public)アクセス…メソッド m1

パッケージの中からも外からも利用できます。同一パッケージに所属するクラスPからも、異なるパッケージに所属するクラスQからも、メソッドm1を呼び出せます。

■限定公開(protected)アクセス…メソッド m2

パッケージの中からのみ利用できます。そのため、異なるパッケージに所属するクラスQからメソッドm2を呼び出すことはできません。

■パッケージ(default)アクセス…メソッド m3

同じパッケージ中からのみ利用できます。そのため、異なるパッケージに所属するクラスQからメソッドm3を呼び出すことはできません。

■非公開(private)アクセス…メソッド m4

クラス内部でのみ利用できます。そのため、同じパッケージに所属するクラスPからのメソッドm4を呼び出すことはできません。

カテゴリー
Java

Java(Step9-1)

パッケージ①

パッケージとインポート宣言

データとメソッドを集めて包んだものがクラスでありますが、クラスを集めて包んだものがパッケージになります。

パッケージ

あるプログラマが作ったクラスと別のプログラマが作ったクラスの名前が同じで1つのプログラムで使うとどうなるでしょうか?しかし、エラーの心配等はいりません。パッケージ(package)という論理的な名前空間で、名前の通用する範囲を自由に制御できるからです。パッケージのイメージを具体的にすると以下の図のようになります。

パッケージとクラス名

パッケージ p に所属するクラス Type は、p.Type と表記します。そのため、図の2つのクラスは、それぞれorigin.rectangleInfoとanother.rectangleInfo となります。このようにして、名前の衝突回避・使い分けをします。このとき、origin.rectangleInfo のような型のフルネームを完全限定名(fully qualified name)と呼び、単なるクラス名 rectangleInfo を単純名(simple name)と呼びます。また、パッケージは階層化でき、パソコンのディレクトリによく似ています。

パッケージの主な役割は以下の3つです。

  • 名前の衝突回避
  • カテゴリによる分類
  • カプセル化(アクセス制御)

型インポート宣言

型のフルネームを表す完全限定名は、綴りが長くなります。パッケージ名を省略した単純名だけで型を利用可能にするのが、型インポート宣言(type import declaration)です。

単一型インポート宣言

以下の形式で単一の型をインポートします。

import 完全限定名;

この形式でインポートされた型名は、そのソースプログラム内で単純名だけで利用できます。この形式の宣言を行うプログラムが以下になります。

//Circle3.java
//円の面積を求める
import java.util.Scanner;

public class Circle3 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);

        System.out.println("円の面積を求めます。");
        System.out.print("半径:");
        double r = stdIn.nextDouble();
        System.out.println("面積は" + (Math.PI * r * r) + "です。");
        stdIn.close();
    }
}
Circle3.java実行結果

java.util.Scanner のインポート宣言しているので、ソースプログラムでは、 Scanner を単純名だけで表せます。インポート宣言がなければ、以下のように完全限定名を使う必要があります。

java.util.Scanner stdIn = new java.util.Scanner(System.in);
オンデマンド型インポート宣言

ソースファイル中で利用する全クラスに対して単一型インポート宣言を行う作業は大変になります。そのため、以下の簡易的なインポート方法があります。

import パッケージ名.*;

この宣言を行ったプログラムでは、パッケージ名で指定されたパッケージに所属する型名を、単純名だけで利用できます。

//NumGussing.java
//数当てゲーム(0-99)
import java.util.*;

public class NumGussing {
    public static void main(String[] args){
        Random rand = new Random();
        Scanner stdIn = new Scanner(System.in);

        int num = rand.nextInt(100);

        System.out.println("数当てゲームスタート");
        System.out.println("0~99の値を当ててください。");
    
        int x;
        do{
            System.out.println("数を入力してください。:");
            x = stdIn.nextInt();

            if(x > num)
                System.out.println("もっと小さい数です。");
            else if(x < num)
                System.out.println("もっと大きい数です。");
        }while(x != num);
        System.out.println("正解です。");
        stdIn.close();
    }
}
NumGussing.java実行結果
java.lang パッケージの自動インポート

java.lang パッケージには、Java言語に密接に関連したクラスが集められています。そのため、このパッケージ内で宣言されている型名は、自動的にインポートされることになっています。Javaのソースプログラムでは、あたかも次に宣言するかのように扱われます。

import java.lang.*; //すべてのJavaプログラムで行われる暗黙の宣言

java.langパッケージに含まれる主な型名の一覧を以下の表に記しておきます。

インタフェースAppendable CharSequence Cloneable Compareable<T> Iterable<T> Readable Runable Thread. UncaughtExceptionHandler
クラスBoolean Byte Character Character.Subset Character.UnicodeBlock Class<T> ClassLoader Compiler Double Enum<E extends Enum<E>> Float InheritableThreadLocal<T> Integer Long Math Number Object Package Process ProcessBuilder Runtime RuntimePermission SecurityManager Short StackTraceElement StrictMath String StringBuffer StringBuilder SystemThread ThreadGroup ThreadLocal<T> Throwable Void
アナテイションDeprecated OverRide SuppressWarnings
java.langパッケージの主な型名

静的インポート宣言

クラスの型だけでなく、静的なメンバである以下の2つもインポートできます。

  • クラス変数(静的フィールド)
  • クラスメソッド(静的メソッド)

これらのインポートを行うのが、静的インポート(static import)と呼ばれるインポートです。型インポート宣言と同様に、静的インポートの宣言にも2種類があります。

import static 型名.識別名;  //単一静的インポート宣言
import static 型名.*;  //オンデマンド静的インポート宣言

静的インポートを利用したプログラム例を以下に示します。

//Circle4.java
//円の面積を求める(円周率Math.PIを静的インポート)
import java.util.Scanner;
import static java.lang.Math.PI;

public class Circle4 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);

        System.out.println("円の面積を求めます。");
        System.out.print("半径:");
        double r = stdIn.nextDouble();
        System.out.println("面積は" + (PI * r * r) + "です。");
        stdIn.close();
    }
}
Circle4.java実行結果

このプログラムでは、java.lang.Math クラスに所属する変数PIを静的インポートした上で、単純名としてアクセスしています。

画面への表示とキーボードからの読み込みで利用するSystem.outとSystem.inは、Systemクラスに所属します。これらを静的インポートした場合のプログラムを以下に示します。

//Circle5.java
//円の面積を求める(System.inとSystem.outを静的インポート)
import java.util.Scanner;
import static java.lang.Math.PI;
import static java.lang.System.in;
import static java.lang.System.out;

public class Circle5 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(in);

        out.println("円の面積を求めます。");
        out.print("半径:");
        double r = stdIn.nextDouble();
        out.println("面積は" + (PI * r * r) + "です。");
        stdIn.close();
    }
}
Circle5.java実行結果
カテゴリー
Java

Java(Step8-2)

クラスの基本

四角形のクラス

6つのフィールドを持つ四角形のクラスを作成しながら、クラスに対する理解を深めましょう。

クラスの独立

前記事で作成した銀行口座クラスと、それをテストするクラスは、単一のソースファイルに納められていました。しかし、小規模でない限り、クラス宣言のプログラムとそれを宣言するプログラムを単一のソースファイルに収めることは少ないです。個々のクラスを独立したソースファイルで実現するように作成しましょう。

四角形のクラスでは、以下のデータをフィールドとして持たせます。

  • 名前
  • 高さ
  • 奥行き
  • 現在位置のX座標
  • 現在位置のY座標

図で表すと下のようになります。

四角形のデータ
class RectangleInfo {
    private String name; //四角形名前
    private int width; //幅
    private int height; //高さ
    private int depth; //奥行き
    private double x; //現在位置X座標
    private double y; //現在位置Y座標
}

各フィールドの値が、状態を表します。すべてのフィールドは外部からアクセスできないように「非公開」とします。

クラスには、フィールドの他にコンストラクタとメソッドが必要です。概要は以下のようにします。

■コンストラクタ

現在位置の座標を(0, 0)にセットします。座標以外のフィールドには、引数に受け取った値を設定します。

■メソッド

  • 現在位置のX座標を調べる
  • 現在位置のY座標を調べる
  • 四角形の情報を表示する
  • 四角形の位置を動かす
this参照

まずは、コンストラクタを作りましょう。座標を原点(0, 0)にセットして、座標以外のフィールドには引数に渡された値を設定します。仮引数の名前は、フィールドの名前を使うと分かりやすくなります。宣言は以下のようになります。

RectangleInfo(String name, int width, int height, int depth){
        this.name = name; this.width = width; this.height = height;
        this.depth = depth; x = y = 0.0;
}

これらで大事なのは this です。this は、自分自身のインスタンスへの参照です。自分自身を操作するリモコンのイメージとして用います。このテクニックは以下のメリットがあります。

  • 仮引数の名前を何にするのか悩む必要が無い
  • どのフィールドに値を指定するための引数であるのが分かりやすい

ただし、コンストラクタやメソッドの中で this. を書き忘れると、フィールドではなく仮引数を表すことになってしまうので、 this. を書き忘れないように細心の注意が必要です

コンストラクタとメソッドを追加した四角形クラスのプログラムを以下に示します。

//RectangleInfo.java
//四角形管理クラス

public class RectangleInfo {
    private String name; //四角形名前
    private int width; //幅
    private int height; //高さ
    private int depth; //奥行き
    private double x; //現在位置X座標
    private double y; //現在位置Y座標

    //コンストラクタ
    RectangleInfo(String name, int width, int height, int depth){
        this.name = name; this.width = width; this.height = height;
        this.depth = depth; x = y = 0.0;
    }

    double getX(){return x;} //現在位置X座標を取得
    double getY(){return y;} //現在位置Y座標を取得

    //スペック表示
    void putSpec(){
        System.out.println("名前:" + name);
        System.out.println("幅:" + width + "cm");
        System.out.println("高さ" + height + "cm");
        System.out.println("奥行き" + depth + "cm");
    }

    //四角形の移動
    boolean move(double dx, double dy){
        double dist = Math.sqrt(dx * dx + dy * dy); //移動距離
        if(dist == 0)
            return false; //移動なし
        else{
            x += dx;
            y += dy;
            return true; //移動あり
        }
    }
}

各メソッドは、次のようになっています。

・メソッドgetX、getY

現在位置の座標を調べるためのメソッドです。各X座標、Y座標の数値を返します。

・メソッドputSpec

四角形の名前と幅、高さ、奥行きを表示するメソッドです。

・メソッドmove

四角形をX方向に dx、Y方向に dyだけ移動させるメソッドです。移動距離が0の場合はfalse、それ以外ならtrueを返します。

四角形クラスを利用するプログラム例を以下に示します。単純な表示をするだけです。

//RectangleTester1.java
//四角形クラスの利用その1

public class RectangleTester1 {
    public static void main(String[] args){
        RectangleInfo rect1 = new RectangleInfo("四角形1", 6, 4, 3);
        RectangleInfo rect2 = new RectangleInfo("四角形2", 5, 5, 5);

        rect1.putSpec(); //rect1のスペックを表示
        System.out.println();
        rect2.putSpec(); //rect2のスペックを表示
    }    
}
RectangleTester1.java実行結果

ソースファイルは、RectangleInfo.javaと同じディレクトリに入れておきましょう。そうすることで、同一ディレクトリ上のクラスファイルから自動的に読み込まれます。

次に、座標を移動するようにプログラムを作りましょう。順序は以下に沿って行います。

  1. 四角形の幅などのデータを読み込みます。
  2. 読み込んだ値を元にクラス RectangleInfo 型のインスタンス newRect を構築します。コンストラクタの働きによって、名前や幅などが読み込んだ値にセットされ、座標が(0, 0)にセットされます。続くwhile文では、現在位置の移動を対話的に繰り返します。
  3. 四角形をX方向にdx、Y方向にdyだけ移動します。移動距離が0の場合はfalseが返され、移動なしと表示されます。
//RectangleTester2.java
//四角形クラスの利用その2
import java.util.Scanner;

public class RectangleTester2 {
    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);

        System.out.println("四角形のデータを入力してください。");
        System.out.print("名前は:"); String name = stdIn.next();
        System.out.print("幅は:"); int width = stdIn.nextInt();
        System.out.print("高さは:"); int height = stdIn.nextInt();
        System.out.print("奥行きは:"); int depth = stdIn.nextInt();

        RectangleInfo newRect = new RectangleInfo(name, width, height, depth);

        while(true){
            System.out.println("現在の座標(" + newRect.getX() + ", " + newRect.getY() + ")");
            System.out.print("移動しますか[0…NO/1…YES]:");
            if(stdIn.nextInt() == 0) break;

            System.out.print("X方向の移動距離:");
            double dx = stdIn.nextDouble();
            System.out.print("Y方向の移動距離:");
            double dy = stdIn.nextDouble();

            if(!newRect.move(dx, dy))
                System.out.println("移動なし");
        }
        stdIn.close();
    }
}
RectangleTester2.java実行結果