プログラミング/10

出典: CourseWiki

目次

publicとprivate

今まで public や private を解説なしに使ってきましたが,ここで解説しておきます.

Javaでは,クラスの中のフィールド(field)(インスタンス変数とクラス変数のこと),メソッド,コンストラクタのそれぞれに対して,公開する範囲を指定することができます.

公開する範囲は,アクセス修飾子(access modifier)で決めます.

アクセス修飾子 公開範囲
public 全てのクラスに公開
private 非公開 (同一クラスからしかアクセスできない)
付けない 同一パッケージ内からしかアクセスできない

(アクセス修飾子には他に protected がありますが,クラスの継承を学ぶまでおいておきます)

パッケージに関しては後述します.

クラスの外部からは,公開されているフィールドやメソッド等にはアクセスできますが, 公開されていないものにアクセスしようとするとコンパイルエラーになります. 以下に例を示します.

public class Foo {
  private int priv;  // privateなインスタンス変数
  public int pub;   // publicなインスタンス変数
  public void pubMethod() {...} // public なインスタンスメソッド
  private void privMethod() {...} // privateなインスタンスメソッド
}

public class Bar {
  public static void main(String[] args) {
    Foo f = new Foo();
    int p = f.priv; // エラー. priv は Foo の外からアクセスできない
    int q = f.pub;  // OK.pub は public なのでアクセスできる
    f.pub = 0;      // OK.publicな変数は書き換えることも可能.
    f.pubMethod();  // OK. 
    f.privMethod(); // エラー. privMethod は Foo の外からアクセスできない
  }
}

(ちなみに,classに対しては public は付けられますが,private は付けられません).

publicとprivateの使い分け

一人で小規模なプログラムを書いている間はアクセス修飾子を使い分ける意味はそれほどありませんが,複数の人で大規模なプログラムを書く場合は重要になります.

メソッドの場合
他のクラスから呼び出されることを想定しているメソッドは public に,そうでないメソッドは private にします.public なメソッドは,そのクラスの提供する機能を実行する入り口になるので,むやみに変更してはいけません(そのクラスを使っているクラスも変更しないといけなくなるので).

また,必要がないメソッドを public にすることも避けるべきです.

フィールドの場合
一般にフィールドは public にしません.public にすると,上の例にあるように,値をクラス外部から書き換えられる可能性があるためです.public にしてもよいフィールドは final がついたものだけです.final がついたフィールドは,外部から値を書き換えようとしてもエラーになります(値の参照はできます).
コンストラクタの場合
クラスのコンストラクタを private にすると,外部からインスタンスを生成できないクラスを作ることができます (new の呼び出しがエラーになる).ちょっと高度なので,この授業の範囲ではコンストラクタは public であるとしておきます.

publicなメソッドやコンストラクタは,そのクラスの入り口(インタフェース)に相当します.

前回の Fraction クラスでは,最大公約数を求める gcd メソッドは private にしていました. これは,Fractionクラスが(他のクラスに対して)最大公約数を求める機能を提供する必然性がないためです. もし,Fractionクラスをバージョンアップした結果,最大公約数を求める必要性がなくなった場合, gcdメソッドがprivateならば単に削除するだけで済みます (public ならば誰かがその機能を使っているかも知れません).

外部からのクラスメソッドの呼び出し

クラスメソッドは,今まで private static として書いていたので同一クラスからしか呼び出せなかったのですが,実は public なクラスメソッド(public static...)も定義できます.publicなクラスメソッドはクラス外部から呼び出せます.

最初に private static にしていたのはクラスを1つしか使っていなかったため

FooクラスのインスタンスメソッドはFooのインスタンスがないと呼び出すことができませんでしたが,Fooクラスの(publicな)クラスメソッドは,Fooのインスタンスがなくても呼び出すことができます.

publicなクラスメソッドを呼び出すには,クラス名.クラスメソッド名(引数..) と書きます.

public class Foo {
  public static int classMethod() {..} // クラスメソッド
}

public class Bar {
  public static void main(String[] args) {
    int f = Foo.classMethod();         // クラスメソッドの呼び出し
  }
}

今まで Math.random() や Keyboard.intValue() のように書いていましたが,これらは Math クラスや Keyboard クラスのクラスメソッドを呼び出していたのです.

Keyboard.java の中を見て,intValue() などが public なクラスメソッドになっていることを確認してください.

パッケージ

ファイルを整理するためにはディレクトリを作成しますが,Java ではクラスはパッケージ(package)と呼ばれる単位で整理します.

Javaの全てのクラスは何らかのパッケージに所属しています.

Javaの基本的なクラスは java.lang パッケージに所属しています. (ディレクトリの場合は / で区切りましたが,パッケージの場合は . で区切ります)

String クラスの正式名は,先頭にパッケージ名をつけて java.lang.String になります. ただ,毎回プログラム中で java.lang.String と書くのは大変なので,java.lang のクラスはいちいち java.lang をつけなくても良いことになっています.

java.lang 以外のクラス(例えば多倍長整数演算を行う java.math.BigInteger クラス)を使う場合,そのまま java.math.BigInteger と書いてもよいのですが,面倒なので普通は import 文を使います.

import文

プログラムの先頭(クラス定義よりも前)に,以下のように import 文を使って使いたいクラスの正式名を書いておきます.

import java.math.BigInteger;

すると,プログラムの残りの部分(クラス定義の中)では BigInteger と書くだけで java.math.BigInteger クラスを意味することになります.

複数の import 文を書いてもかまいません.

import java.math.BigInteger;
import java.math.BigDecimal;

public class Foo {
  private BigInteger big;
  ...
}

package文を使って自分でパッケージを定義することもできますが,この授業ではこの機能は使いません.package文を使わない場合,定義したクラスはデフォルトパッケージに属していることになります.

本当はデフォルトパッケージの使用は推奨されません.他の人が作ったクラスと名前が衝突する可能性があるためです.

APIドキュメント

Stringクラスには,どのようなコンストラクタやメソッドがあるのでしょうか. Stringクラスの場合,
http://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html
に全てのコンストラクタやメソッドの解説があります.

このようなドキュメントのことを,APIドキュメントと呼びます. (API = Application Program Interface). プログラマーでも全てのクラスやそのメソッドを覚えられるわけはないので,このようなドキュメントを眺めながらプログラムを書くことになります.

APIドキュメントでは,通常 private なものは掲載されていません.private なものはクラスの外部から見えないので,掲載する意味がないためです.

このページは,大きく分けると以下の構造になっています.

クラスの解説 初心者には分かりにくい場合が多いです.
フィールドの概要 フィールド(インスタンス変数とクラス変数).staticがついていればクラス変数,ついていなければインスタンス変数.
コンストラクタの概要 コンストラクタの説明.引数のパターンごとに記述されている.
メソッドの概要 メソッドの説明.static と書いてあるのがクラスメソッドで,そうでないものはインスタンスメソッド.
継承されたメソッド いまのところ気にしない
フィールドの詳細 各フィールドの解説
コンストラクタの詳細 各コンストラクタの解説
メソッドの詳細 各メソッドの解説

コンストラクタとメソッドの詳細では,解説の他に以下の記述があります.

パラメータ
引数の説明
戻り値
returnされる値の説明(voidでないメソッドの場合)
例外
発生する可能性のある例外の説明

他のクラスの解説は,http://docs.oracle.com/javase/jp/8/docs/api/index.html?overview-summary.html から辿ることができます.左上のフレームでパッケージを選ぶと(例: java.lang),左下のフレームにそのパッケージに属するクラスの一覧が表示されます.

APIドキュメントは全体をダウンロードすることも可能です.

演習(APIドキュメント)

Math クラス (java.lang.Math クラス) のAPIドキュメントを探し,random() や abs(..) などがクラスメソッドであることを確認してください.

また,MathクラスとStringクラスのそれぞれで,使ったことがないメソッドを呼び出すプログラムを作成して下さい.

オブジェクト指向プログラミング

例えば,クルマを作る場合,クルマ全体を一つのモノとしてつくるよりも,シャーシ,エンジン,変速機,タイヤ等の様々な部品を組み合わせてつくったほうが簡単です.コンピュータのプログラムも,最初から全部を一枚岩でつくるよりも,いろいろな部品を組み合わせて作る方が簡単です.このように部品(モノ)を組み合わせてプログラミングする方法がオブジェクト指向プログラミング(Object Oriented Programming, OOP)になります.

オブジェクト指向プログラミングでは,プログラムをいくつかのクラスに分割して記述することになります.

Stringクラスは以下の機能を持っています.

  • 文字列を記憶すること
  • 文字列に対する操作を行うこと(「長さを調べる」,「部分文字列を取り出す」,etc.)

このように,オブジェクト指向プログラミングでは,文字列というモノと,文字列に対する操作をペアにして考えます.

車は,ハンドルやアクセル,ブレーキ等の使い方さえを知っていれば運転できます.車を運転するために,ハンドルを回したときのタイヤの回転角度や,アクセルを踏んだ角度に応じたガソリンの噴射量の計算法,エンジンの点火タイミング等を知る必要はありません.オブジェクト指向プログラミングでは,この考え方を取り入れています.

文字列を使うためには,文字列の代入の仕方や長さの求め方等の使い方(各メソッドの意味)だけを知っていれば良く,その文字列がコンピュータの中でどのように表現されているか(フィールドの中身),どのようにして文字列の長さを計算しているのか等(メソッドの中身)は知る必要はありません.これにより,以下のようなメリットがあります.

  • 文字列というモノを,その実装を気にせずに抽象化して扱うことができる
  • (適切なアクセス修飾子を使っていれば)将来クラスの実装を修正しても,クラスを使う側に影響を与えないことを保証できる

課題(ポーカー)

上の解説とそれほど関係のない課題になってしまいました.すみません(安倍)

トランプのカード一揃い(ジョーカーを除く52枚)から5枚のカードをとったときに,ポーカーの役ができる確率をモンテカルロ法で調べるプログラムを作成せよ.

ポーカーを良く知らない人は Wikipedia を参照すること.

以下のクラスを使います.

Cardクラス

1枚のカードを表します.

1枚のカードは,スーツ(HEART, DIAMOND, SPADE, CLUBのいずれか)と,数字(1から13)で表すことができるので,この2つをインスタンス変数(suit, number)で保持します.

/*
 * Card: 1枚のカードを表すクラス
 */
public class Card {
    // 定数の定義
    public final static int HEART = 0;
    public final static int DIAMOND = 1;
    public final static int SPADE = 2;
    public final static int CLUB = 3;
    // スーツの数
    public final static int NSUITS = 4;
    // スーツあたりのカードの数
    public final static int NCARDS_PER_SUIT = 13;
 
    // インスタンス変数
    private int suit;        // スーツ(0-3)
    private int number;    // 番号(1-13)
 
    // コンストラクタ
    public Card(int suit, int number) {
        if (suit != HEART && suit != DIAMOND && suit != SPADE && suit != CLUB) {
            System.err.println("Card: given suit is not valid");
            System.exit(1);
        }
        if (number < 1 || NCARDS_PER_SUIT < number) {
            System.err.println("Card: given number is not valid");
            System.exit(1);
        }
        this.number = number;
        this.suit = suit;
    }
 
    // このカードの数を取得する
    public int getNumber(){
        return number;
    }
 
    // このカードの種類を取得する
    public int getSuit() {
        return suit;
    }
 
    // 文字列に変換する
    public String toString() {
        // 文字コードを UTF-8 にしていて,適切なフォントがあれば以下の
        // スーツのマークが使える
        // String[] name = {"♥", "♦", "♠", "♣"};
        String[] name = {"H", "D", "S", "C"};
        return name[suit] + number;
    }
}
  • マジックナンバーを減らすために,public final static な定数を定義してます.このように定数は全て大文字にする習慣があります.
    • スーツを表す HEART, DIAMOND などは,本当は enum という機能を使って書くべきなのですが,enum を学習していないので int で書いています.
  • 引数がおかしいときに System.exit(1) でプログラムを終了していますが,本来は例外をスローすべきところです.
  • また,エラーメッセージは,System.out.println ではなく System.err.println を使って出力しています.System.out.println は標準出力へ出力しますが,System.err.println は標準エラー出力に出力します.標準出力と標準エラー出力の違いについては,http://oku.edu.mie-u.ac.jp/~okumura/java2/io.html あたりを参照して下さい.

CardStackクラス

カード一揃いを表します.

最初はすべてのカードが揃っていますが,1枚ずつ抜いていくことができるようになっています. 抜いたかどうかを覚えておくために,2次元配列 taken を使っています.

drawメソッドは,カード一揃いから1枚乱数で抜いて,抜いたカードをCardクラスのインスタンスとして返すメソッドです.

  • CardStackクラスから,Card クラスの公開されている(publicな)クラス変数を参照するためには,Card.NSUITS のように書きます

/*
 * CardStack: トランプのカードの集合を表わすクラス.
 * カードを引くとそのカードはカードの集合から消える.
 * Jokerはないものとする.
 */
public class CardStack {
    // カードの残枚数
    private int nCard;
    // 各カードが抜かれていれば true になる2次元配列
    // 最初の次元はスーツ,次の次元は番号(0-12)
    private boolean[][] taken
        = new boolean[Card.NSUITS][Card.NCARDS_PER_SUIT];
 
    // コンストラクタ
    public CardStack() {
        // 残枚数を初期化する
        nCard = Card.NSUITS * Card.NCARDS_PER_SUIT;
    }
 
    // カードを1枚引き,引いたカードを Card 型のインスタンスとして返す.
    // カードがないならば null を返す.
    public Card draw() {
        if (nCard == 0) {
            return null;
        }
 
        // 乱数でカードを選ぶ
        // カードの残り枚数が少なくなってくると効率が悪いが...
        while (true) {
            int s = (int) (Math.random() * Card.NSUITS);
            int n = (int) (Math.random() * Card.NCARDS_PER_SUIT);
            // まだ抜き取られていないカードを選んだかチェック
            if (!taken[s][n]) {
                taken[s][n] = true;
                nCard--;
                // Cardクラスのインスタンスを作って返す
                // + 1 しているのはカードの番号が 1 から始まっているため
                return new Card(s, n + 1);
            }
        }
    }
}

Handクラス

ポーカーの手札(5枚のカード)を表します.

コンストラクタで5枚のカード(Cardの配列)を与えておき,役の判定を行うことができるクラスです.

  • import文を使っています
  • ワンペアとフラッシュの判定メソッドだけ用意してあります
  • 役の判定方法がちょっと難しいかも知れませんが,コメントを頼りにがんばって理解してください
  • getNumList() はクラスの外から呼ぶ必要はないので private メソッドです

// import文の例
import java.util.Arrays;
 
/*
 * ポーカーの手を表すクラス
 */
public class Hand {
    public final static int NCARDS = 5;
    // 手持ちのカード(5枚)
    private Card[] cards;
 
    // コンストラクタ
    public Hand(Card[] cards) {
        if (cards.length != NCARDS) {
            System.err.println("Hand: cards.length != 5");
            System.exit(1);
        }
        this.cards = cards;
    }
    
    // 文字列に変換
    public String toString() {
        return Arrays.toString(cards);
    }
    
    // フラッシュならば true を返す
    public boolean isFlush() {
        // 全てが cards[0] のスーツと同じか調べる
        for (int i = 1; i < NCARDS; i++) {
            if (cards[i].getSuit() != cards[0].getSuit()) {
                return false;
            }
        }
        return true;
    }
    
    // ワンペアならば true を返す
    public boolean isOnePair() {
        int[] nums = getNumList();
        /*
         * count[0] ... (スーツを無視して)手札に存在しない番号の数
         * count[1] ... (スーツを無視して)手札に1枚だけある番号の数
         * count[2] ... (スーツを無視して)手札に同じ数が2枚ある番号の数
         * count[3] ... (スーツを無視して)手札に同じ数が3枚ある番号の数
         * count[4] ... (スーツを無視して)手札に同じ数が4枚ある番号の数
         * 例: 手札が 1, 3, 3, 4, 4 ならば,
         * count[0] = 10, count[1] = 1, count[2] = 2, count[3] = 0
         * count[4] = 0 になる.count[0] が10になる理由は,手札にない番号は
         * 2, 5, 6, 7, 8, 9, 10, 11, 12, 13 なので.
         */
        int[] count = new int[Card.NSUITS + 1];
        for (int i = 0; i < nums.length; i++) {
            count[nums[i]]++;
        }
        // ワンペアの条件
        return (count[2] == 1 && count[3] == 0 && count[4] == 0);
    }
    
    /*
     * それぞれの番号が何枚入っているかを保持する要素数13の配列を返す.
     * スーツの違いは無視する.
     * int[] nums = this.getNumList(); として呼び出すと
     * nums[0] ... 1の枚数  
     * nums[1] ... 2の枚数  
     * ... 
     * nums[12] ... 13の枚数
     * になる.例えば,手札が 1, 3, 3, 4, 4 ならば,
     * nums[0] = 1, nums[2] = 2, nums[3] = 2, その他の要素は 0 
     * という配列を返す.  
     */ 
    private int[] getNumList() {
        int[] nums = new int[Card.NCARDS_PER_SUIT]; 
        for (int i = 0; i < NCARDS; i++) {
            // カードの番号(1-13)を配列のインデックス(0-12)で扱うため -1 している
            nums[cards[i].getNumber() - 1]++;
        }
        return nums;
    }
}

これらのクラスを使って,まずはワンペアとフラッシュの出現確率を求めるプログラム CardGame を書いてください.試行回数は 1,000,000 回とします.

ヒント: Handクラスのコンストラクタには,5要素のCardの配列(Card[]) が必要です. 以下のような感じです.

CardStack stack = new CardStack();
Card[] cards = new Card[5];
for (int i = 0; i < cards.length; i++) {
  cards[i] = stack.draw();
}
Hand hand = new Hand(cards);

次に,ツーペア,スリーカード,フォーカード,フルハウスの役を判定するメソッドを Hand クラスに追加し,これらの確率も求めてください.

余裕がある人版

以下から自分の能力と興味にあわせて選んでください.

  • それぞれの役の確率を解析的に計算して,モンテカルロ法の結果と照合してください.
  • ストレート,ストレートフラッシュ,ロイヤルストレートフラッシュの確率も求めてください.
  • 役の判定の度に getNumList() メソッドを毎回実行するのは無駄なので,これを避ける工夫をしてみてください.isOnePair(),isTwoPair() などは似たような処理になるはずなので,これも簡潔に書くようにして下さい.
  • 一回引いた後に,適当な戦略で何枚か交換(カードを捨てて捨てた枚数分を再度引く)したときに,それぞれの役の確率がどの程度上がるかを求めてください.
    • Handクラスを改造して一部のカードを入れ替える機能をいれてもよいですが,それよりはHandクラスのインスタンスを作り直したほうが簡単.
    • 使った戦略も明記すること.戦略の例:
    • ワンペアならば残りの3枚を交換
    • ツーペアならば残りの1枚を交換
    • スリーカードならば残りの2枚を交換
    • 役無しならば,状況に応じてストレートかフラッシュを狙う

EclipseのTips(技)

  • ソースコードの上で,メソッド呼び出しやフィールドを参照しているところにカーソルを置き,右クリック→Open Declaration (あるいはF3キー) を押すとそのメソッドやフィールドの定義に飛べます.
  • 変数名やメソッド名にカーソルを置き,メニューの Refactor → Rename を選ぶと変数名やメソッド名を変更できます.


Copyright (C) 2002-2015 Kota Abe, Osaka City University

ナビゲーション