プログラミング入門(金曜1・2限)講義資料(2008年後期)

プログラミング入門 第9回

前回の復習

クラスの継承

弁当屋さん経営ゲームでは,Playerクラス(人のプレイヤー)とCompPlayerクラス(コンピュータプレイヤー)の違いはprepareBentoメソッド(弁当を作る数を決めるメソッド)だけです.他のメソッドやインスタンス変数は全て共通です.

プログラミングでは,このように似ている(けれども少し違う)部品を使うことがよくあります.

Player.javaをCompPlayer.javaにコピーして,prepareBentoメソッドだけを書き換えるという方法だと,Player.javaに修正を加える度に,CompPlayer.javaも修正する必要があります.

オブジェクト指向言語では,このような場合,クラスの継承という機能を使うことができます.クラスを継承すると,既に存在するクラスを修正して別のクラスを作ることができます.つまり異なるところだけを記述すればいいわけです(差分記述).

例えば,バス(Bus)もトラック(Truck)も車(Car)なので,基本的な機能はほぼ共通しています.そこで,まず Car クラスを作って,車としての共通の機能を作成します.次に,Car クラスを継承して Busクラスを作り,バスに特有の機能を作成します.同様に,Car クラスを継承して Truck クラスを作り,トラックに特有な機能を作成します.こうすると,車の基本機能に何か追加や変更があったときも,Car クラスを変更するだけ済みます.

クラスAをベースにクラスBを作る場合(クラスBがクラスAを継承する場合)は,クラスBではクラスAの全てのメソッドや変数を引き継ぎます.つまり,クラスBではクラスAのメソッドや変数を最初から備えていることになります.

継承するときに,別のメソッドを付け加えたり特定のメソッドの定義を変更することができます.これが継承の強力な機能です.これによって,既存のクラスの振る舞いをちょっと変更したり,機能を付け加えたりすることが可能です.

継承を使ってクラスを作るには以下のように書きます.

// クラスAを引き継いでクラスBを作る
class B extends A {
  クラス定義
}

クラス定義の部分には,普通のクラス定義と同じように,変数やメソッドなどを書きます.新しくここに書いた変数やメソッドは,クラスAのものに追加されます.また,同じ名前かつ同じ引数のメソッドは,そのメソッドに置き換わります.

クラスAが以下のように定義されている場合,

class A {
  private int var_a;

  void method1() {
    ...;
  }
  void method2(int a) {
    ...;
  }
}

クラスBを以下のように定義すると,method1を再定義(入れ替え)し,method3を追加することになります.

class B extends A {
  private int var_b;  // 変数も追加できる.

  void method1() {    // method1の再定義
    ...;
  }
  int method3() {     // method3の追加
    ...;
  }
}

つまり,クラスBでは,void method1(), void method2(int a),void method3() という3つのメソッドが使用できることになります.

CompPlayerクラス

先週の弁当屋さん経営ゲームの,人のプレイヤー(Player)も,コンピュータプレイヤー(CompPlayer)も,提供する機能はほとんど同じなので,クラスの継承を使うとすっきりと書くことができます.

本来,このような場合は,両者に共通する機能を集めたクラスをまず作り,そこから人のプレイヤーのクラス(Player)とコンピュータプレイヤーのクラス(CompPlayer)を継承させる方法が正しいとされていますが,今回は時間の都合上CompPlayerクラスはPlayerクラスから継承させることにします.

CompPlayer が Player と異なるところは prepareBento メソッドだけなので,prepareBento メソッドだけを記述すればよいはずです.

public class CompPlayer extends Player {
    public void prepareBento(int kousui) {
      // コンピュータ側の思考ルーチン
      // 降水確率から作る弁当の個数を決定し,zaikoに代入する
    }
    // 他のメソッドは書く必要がない
}

prepareBentoメソッドでは,moneyやzaiko等のインスタンス変数にアクセスする必要があります.しかし,これらの変数はPlayerクラスの中でprivateと宣言されているので,CompPlayerクラスから利用することはできません.かといってpublicにしてしまうと危険です.

このために,Javaではprivate, publicの他にprotectedが用意されています.protectedで宣言された変数は,継承したクラスからはアクセスできますが,クラスの利用者からはアクセスできません.

つまり,Playerクラスのzaikoやmoneyをprivateからprotectedに変更すれば,CompPlayerクラスから使用することができるようになります.CompPlayerクラスで新たにzaikoやmoneyを宣言する必要はありません.

課題9-1 (完全版弁当屋さん経営ゲーム)

CompPlayerクラスを完成させて下さい.Playerクラスのmoneyとzaikoをprivateからprotectedに変更することを忘れずに.

コンピュータ側の戦略は好きなようにして構いませんが,降水確率が大きければ弁当を沢山作った方が良いことを忘れずに.例えば,降水確率に応じて500個,300個,100個から決める等.

もちろん,所持金で作れる限界以上の弁当を作ってはいけません.

さらに,SimpleBentoGame.javaを修正してBentoGame.javaを作って下さい.

人とコンピュータのどちらかのプレイヤーの所持金が 100000 を越えた時点でwhile文を抜けます.両方同時に 100000 を越えることがありうることに注意してください.最終的には所持金が大きい方が勝ちです.

1. Player.java,2. CompPlayer.java,3. BentoGame.java,4. 実行結果,5. 苦労したところや改善すべき点,感想 を書くこと.



ネットワーク入門

次は,ネットワークを使って他のコンピュータと通信してみましょう.そのためにはネットワークに関する基礎知識が必要です.

ここでは,インターネットで使われているTCP/IPでの通信の方法をごく簡単に解説します.

クライアントとサーバ

インターネットにはたくさんのコンピュータが接続されていて,Webや電子メール等,様々なサービスを利用することができます.

Webの場合,利用者はWebブラウザを使ってWebページにアクセスします.どのページを閲覧するかはURLと呼ばれる文字列によって決まります.Webブラウザは,URLからインターネット上のどのコンピュータと通信すれば良いかを知り,そのコンピュータとやりとりして欲しい情報(HTMLドキュメント)を取得します.

サービスを提供する側をサーバ,サービスを提供される側をクライアントと呼びます.Webの例だと,Webブラウザがクライアントに相当します.Webブラウザが通信する相手がWebサーバです.Webサーバは,クライアントから要求があったHTMLドキュメントを送り返す処理を行っています.

IPアドレスとポート番号

ネットワーク上で通信するためには,どのコンピュータと通信するかを指定する必要があります.指定するためにはある種のアドレスが必要です.電話でいう電話番号に相当するものです.TCP/IPではIPアドレスと呼ばれるもので指定します.インターネットに接続されたコンピュータには,全て異なるIPアドレスが付与されています.

IPアドレスは,4つの8ビットの数(0〜255の数)からできています.普通は160.193.3.1 のように間に . を挟んだ10進数で表現します.

しかし,IPアドレスは覚えにくいので,わかりやすいホスト名をつけることができるようになっています.ホスト名とは www.media.osaka-cu.ac.jp のような文字列です.学術情報総合センターの教育用システムのコンピュータには x9a01.ex.media.osaka-cu.ac.jp のようなホスト名がついています.ホスト名からIPアドレスを検索するシステム(DNS)が動いていて,インターネットを支えています.

さて,一つのコンピュータで,複数のサーバを動かすことができます.例えばWebサーバと電子メールサーバ等です.WebブラウザがWebページを取得しようとしたら電子メールサーバに繋がってしまった,というようなことが起きると困りますから,何らかの方法で区別する必要があります.

このため,サービス毎に特定の数値(ポート番号と呼びます)を割り当てます.例えば,Webは80番ポート,電子メールは25番ポートと決まっています.クライアントが通信を始めるときには「IPアドレス160.193.3.1の80番ポートに繋いでください」というように指定します.(ポート番号は16ビットの整数です).

コネクション

電話を使うときは,まず電話を掛けて(電話回線を繋ぐことで)話ができるようにします.これと同じように,クライアントとサーバが通信する時にも,仮想的な通信路を作って.その上で通信を行います.この通信路のことをコネクションと呼びます.通信する前には,コネクションを確立する必要があります.(コネクションを確立しないタイプの通信もあるが,ここでは触れません.)

一度コネクションを確立したら,コネクションにバイト列を流し込むことができます.クライアント側でコネクションにバイト列を流し込むと,それがそのままサーバ側で読めます.反対にサーバ側でバイト列を流し込むと,それはクライアント側で読むことができます.

つまり,コネクションはクライアントからサーバ方向と,サーバからクライアント方向の2本できることになります.

また,コネクションは不要になったら閉じることができます.コネクションは,両方の側で閉じる必要があります.両方で閉じると,コネクションは無くなります.

Webブラウザを使わずにWebサーバと通信してみる実験

UNIXのtelnetコマンドは,もともとネットワークを使って他のコンピュータにログインするためのコマンドですが,任意のホストの任意のポートにコネクションを確立して通信するために使うこともできます.ホスト名とポート番号を指定します.これを使ってwww.ex.media.osaka-cu.ac.jp の Web サービス (80番ポート) に接続してみましょう.

コマンドラインから,以下のように実行してみてください.

$ telnet www.ex.media.osaka-cu.ac.jp 80
Trying 160.193.113.20...
Connected to www.ex.media.osaka-cu.ac.jp (1)
Escape character is '^]'.
GET /(改行) (2)
....


(...HTMLが続く...)

Connection closed by foreign host.

Webサーバとの間にコネクションを確立し(1),コマンドを送る(2)と,結果がサーバから送り返されたと思います.上の例では http://www.ex.media.osaka-cu.ac.jp/ のページを取得しています.

Webの裏側でどのようなやり取りが行われているか,感じ取ることができたでしょうか?

Javaでネットワークプログラミング

Javaで,ネットワークを扱うためには,java.net.Socketクラスとjava.net.ServerSocketクラスを使います.

Socketクラスを使うと,任意のホストにコネクションを張ることができます.ServerSocketクラスは,サーバを作る(外からのコネクションを受け入れる)ために使います.

Socketクラス

Socketクラスのコンストラクタでホスト名とポート番号を指定すると,コネクションを確立します.

コネクションはファイルと同じようにバイト列ですから,Javaではファイルと同じように扱います.つまり,java.io.BufferedReaderクラスとjava.io.PrintWriterクラスを使うことができます.

telnet で行ったことを,Javaのプログラムで書いてみましょう.


このファイルをダウンロード ■ UNIX用(EUC版) ■ Windows用(SJIS版)
(上のどちらかのリンクを右ボタンでクリックして「リンク先を名前をつけて保存」して下さい)

// WebClient.java
// Webサーバに接続し,トップページを取得する
import java.net.*;
import java.io.*;

public class WebClient {
    public static void main(String[] args) throws Exception {
	// ホスト名とポート番号を指定してコネクションを確立する
	Socket sock = new Socket("www.ex.media.osaka-cu.ac.jp", 80);

	// 文字列を受け取るためのオブジェクトを作る
	BufferedReader in =
	    new BufferedReader(new InputStreamReader(sock.getInputStream()));
	// 文字列を送るためのオブジェクトを作る
	PrintWriter out =
	    new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
	
	// 文字列をサーバに送る
	out.println("GET /");

	// PrintWriterはバッファリングするので,flushを呼ぶまでは送信されず,
	// メモリにたまっている.送信しないと結果を受け取れないので,
	// ここで flush する.
	out.flush();

	while (true) {
	    String s = in.readLine();
	    if (s == null) {
		break;
	    }
	    System.out.println(s);
	}

	in.close();
	out.close();
	sock.close();
    }
}


SocketクラスのgetInputStreamメソッドは,1バイトづつ読むことができるjava.io.InputStreamクラスのオブジェクトを返します.それをjava.io.InputStreamReaderによってバイトからUnicodeに変換し,さらにそれをjava.io.BufferedReaderによってバッファリングします.

SocketクラスのgetOutputStreamメソッドは,1バイトづつ書き込むことができるjava.io.OutputStreamクラスのオブジェクトを返します.java.io.OutputStreamWriterを使ってUnicodeで書けるようにし,さらにjava.io.PrintWriterによってprintln等のメソッドが使えるようにします.

ネットワークコネクションを扱うのは,ほとんどファイルの読み書きと同じということに気が付くでしょう.ファイルをオープンするかわりに,コネクションを張ります.

flush メソッドを忘れると「送ったつもり」という状態になっている可能性があるので注意しましょう.

コネクションが相手側から閉じられると,readLineメソッドは null を返します.これはファイルの終わりに到達したときの動作と同じです.

closeメソッドによってこちら側のコネクションも閉じます.

ServerSocketクラス

サーバの側では,外からやってくるコネクションを受け入れるためにServerSocketクラスを使います.

ServerSocketクラスのコンストラクタでは,待ち受けたいポートの番号を指定します.

その後acceptメソッドによって,外からそのポートにコネクションが確立されるまで待ちます.acceptメソッドは,コネクションが確立したらSocketオブジェクトへの参照を返すので,後はそのSocketオブジェクトを使って通信を行います.

プロトコル

Webサーバでは,"GET /" という文字列を送信することで,トップページの内容を得ることができました.WebクライアントとWebサーバとの間で,どのような文字列を送信すると,どのような意味になるかということは,予め決められています.このような取り決めをプロトコルと呼びます.WebサーバやWebクライアントはHTTP(Hyper Text Transport Protocol)というプロトコルに従って通信を行っています.他にもいろいろなプロトコルが使われています.例えば電子メールの送信にはSMTP(Simple Mail Transfer Protocol)というプロトコルが使われています.

対戦型しりとりゲーム

2人で対戦できるしりとりゲームを作りましょう.サーバとクライアントを作る必要があります.サーバ側にいる人と,クライアント側にいる人でしりとりをします.

まず,サーバ側のプログラム(ShiriServer)を実行します.引数として待ち受けるポート番号を指定します.実行すると,クライアントが接続してくるのを待ちます.

クライアント側では,ShiriClient を実行します.引数としてサーバのホスト名とポート番号を指定します.コネクションが確立されるとゲームが始まります.

ここでは,同じコンピュータでサーバとクライアントを実行してみます.(もちろん,1つのコンピュータの中でも通信することができます).そのためには自分自身のコンピュータを表すホスト名 localhost を使います.

同じコンピュータでサーバとクライアントを実行するために,ターミナル(mltermやコマンドプロンプトなど)を2つ起動します.一つのターミナルでサーバを実行して,もう一つのターミナルでクライアントを実行することになります.

サーバ側の画面

$ java ShiriServer 10000
相手プレイヤーが接続するのを待っています...
接続されました
しりとりゲームをします.最初はしりとりの「り」です.
あなたの番です(「り」から始まる単語): りんご
相手の応答を待っています...
相手の単語: ごま
あなたの番です(「ま」から始まる単語): まり
相手の応答を待っています...
相手の単語: りす
あなたの番です(「す」から始まる単語): すうどん
あなたの負け!
$

クライアント側の画面

$ java ShiriClient localhost 10000
サーバからのメッセージを待っています...
あなたの番です(相手の言葉は「りんご」): ごま

サーバからのメッセージを待っています...
あなたの番です(相手の言葉は「まり」): りす

サーバからのメッセージを待っています...
あなたの勝ち!(すうどん)
$

サーバ側のプログラム


このファイルをダウンロード ■ UNIX用(EUC版) ■ Windows用(SJIS版)
(上のどちらかのリンクを右ボタンでクリックして「リンク先を名前をつけて保存」して下さい)

// ShiriServer.java
// しりとりサーバプログラム

import java.net.*;
import java.io.*;

public class ShiriServer {
    public static void main(String[] args) throws Exception {
	if (args.length != 1) {
	    System.out.println("引数として ポート番号 が必要です.");
	    // プログラムを途中で終了させる
	    System.exit(1);
	}

	// ポート番号を文字列から数値に変換
	int port = Integer.parseInt(args[0]);

	// 待ち受けるための ServerSocket を作る
	ServerSocket serv = new ServerSocket(port);
	System.out.println("相手プレイヤーが接続するのを待っています...");
	// 接続されたら Socket オブジェクトができる
	Socket sock = serv.accept();
	System.out.println("接続されました");

	// 文字列を受け取るためにBufferedReaderオブジェクトを作る
	BufferedReader in =
	    new BufferedReader(new InputStreamReader(sock.getInputStream()));
	// 文字列を送るためにPrintWriterオブジェクトを作る
	PrintWriter out =
	    new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
	
	System.out.println("しりとりゲームをします.最初はしりとりの「り」です.");

	// 入力する単語は,この文字からはじまる必要がある.
	String last = "り";

	while (true) {
	    // サーバ側のプレイヤーの処理
	    // last から始まる言葉をキーボードから入力
	    String mine = getWordFromKeyboard(last);
	    
	    // 最後の文字を取り出す
	    last = mine.substring(mine.length() - 1,
				  mine.length());

	    // んで終わる?
	    if (last.equals("ん")) {
		System.out.println("あなたの負け!");
		// ゲームオーバの通知
		out.println("GAMEOVER");
		out.println("あなたの勝ち!(" + mine + ")");
		out.flush();
		break;
	    }

	    // クライアント側プレイヤーの処理
	    // last から始まる言葉をキーボードから入力
	    String opponent = getWordFromClient(mine, last, in, out);

	    // 最後の文字を取り出す
	    last = opponent.substring(opponent.length() - 1,
				      opponent.length());

	    // んで終わる?
	    if (last.equals("ん")) {
		System.out.println("相手の負け!");
		// ゲームオーバの通知
		out.println("GAMEOVER");
		out.println("あなたの負け!(" + opponent + ")");
		out.flush();
		break;
	    }
	}

	// 最後にクローズ
	in.close();
	out.close();
	sock.close();
    }

    // キーボードから last で始まる単語を入力するためのメソッド
    //
    // 入力された単語(String)を返す.
    //
    private static String getWordFromKeyboard(String last) {
	String word;
	while (true) {
	    System.out.print("あなたの番です(「" + last
			     + "」から始まる単語): ");
	    word = Keyboard.stringValue();
	    // もし空だったら
	    if (word.equals("")) {
		// ループ(while文)の先頭にジャンプする (continue文)
		continue;
	    }
	    // 入力した最初の文字が last から始まっているかチェック
	    String first = word.substring(0, 1);
	    if (first.equals(last)) {
		break;
	    }
	}
	return word;
    }

    // クライアントから,last で始まる単語を入力するためのメソッド
    //
    // String myword     サーバ側の単語
    // String last       サーバ側の単語の最後の文字
    // BufferedReader in クライアントからの入力用コネクション
    // PrintWriter out   クライアントへの出力用コネクション
    //
    // 入力された単語(String)を返す.
    //
    private static String getWordFromClient(String myword, String last,
		    BufferedReader in, PrintWriter out) throws Exception {
	String word;
	while (true) {
	    System.out.println("相手の応答を待っています...");
	    // 相手の番!
	    out.println("YOURTURN");
	    out.println(myword);
	    out.flush();

	    // 相手の単語を受けとる
	    word = in.readLine();
	    System.out.println("相手の単語: " + word);
	    // もし空だったら
	    if (word.equals("")) {
		// ループの先頭にジャンプする
		continue;
	    }
	    // ちゃんと最初の文字が last と同じかチェックする
	    String first = word.substring(0, 1);
	    if (first.equals(last)) {
		break;
	    }
	}
	return word;
    }
}


簡単にするため,すでに登場した単語がもう一度使われたかどうかはチェックしていません.

continue文

continue文は,break文のようなループ制御文の一つです.while や for 文の中で使います.

continueは,ループの残り部分の実行をスキップして,いきなりループの継続条件のチェックにジャンプする命令です.

  while (条件) {
    ☆;
    ☆;
    ☆;
    if (...) {
        continue;  // ★ の実行をスキップし,条件のチェックに飛ぶ
    }
    ★;
    ★;
    ★;
  }

ループ実行中にある条件が満たされたら,残りの部分を実行する必要がないというような場合に使います.

文字列から数値への変換

サーバで待ち受けるポート番号はプログラムへの引数から取得します(プログラムへの引数のことを忘れてしまった人は第7回講義資料の「クラスとは(その2)」を読み返してください).main(String[]args)の args[0] が最初の引数ですから,ここにポート番号が入りますが,これは文字列(例えば "10000")なので数値(int)に変換しないといけません.

Stringをintに変換するには,java.lang.Integerクラスの助けを借ります.Integerクラスは,基本型のint型を補助するためのクラスです.

以下のようにIntegerクラスのクラスメソッドparseIntを使うと,文字列からintに変換できます.

  String s = "10000";
  int port = Integer.parseInt(s); // port = 10000 になる

他にも,Doubleクラス,Floatクラス,Longクラス,Booleanクラスなどがあります.

プロトコル

しりとりゲームにも何らかのプロトコルが必要です.ここでは以下のように決めてみました.

サーバからクライアントへの送られる命令は YOURTURN と GAMEOVER の2つ.

このプロトコルは,telnet で確かめることができます.サーバを動作させてから,telnet を実行してみましょう.

  
$ telnet localhost 10000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
YOURTURN
りす
すいか
GAMEOVER
あなたの勝ち!(かだん)
Connection closed by foreign host.

課題9-2 (しりとりクライアント)

telnetを使った実行方法はあまり親切とは言えません.もっと簡単にしりとりできるように,クライアント側のプログラム ShiriClient を書いてください.

実行例

だいたい,以下のような構造になるでしょう.

1. ソースプログラム,2. 実行結果,3. 苦労したところや改善すべき点,感想 を書くこと.なにかプロトコルに変更を加えたら,それも書くこと.