2016年度 発展プログラミング演習 第4講 図書館システム 2日目

目次

  1. 図書館システムに本を追加できるようにする.
  2. 図書館システムが持つ本の一覧を表示する.
  3. 図書館システムの本のリストから本を検索する.
  4. 図書館システムから本を削除する.
  1. 練習問題
  2. 本日のまとめ

4. 図書館システムに本を追加する.

4-1. 変数の有効範囲.

変数shelfの宣言場所をメソッド の中ではなく,クラス宣言 の中(メソッド宣言の外側)で宣言しましょう. 変数の宣言場所によって,変数が利用できる範囲が異なります.一般的に, 変数はその変数が宣言された一番内側の波括弧内で有効になります. 波括弧の外側では変数が参照できません.この変数の有効範囲を スコープと呼びます.

スコープの有効範囲を下図の例を用いて説明します.
スコープの有効範囲の例

この例では,一番内側の波括弧のforで,strが宣言されています. strの宣言は,forの内部で行われているため, strの有効範囲は,forの波括弧内です. forの外側でstrを参照しようとすると, 「シンボルが見つかりません」というコンパイルエラーが起こります. また,variable2method1内で宣言されているため, 有効範囲は,method1内のみに限定されます. method1外で参照しようとすると,先ほどと同じく 「シンボルが見つかりません」というエラーでコンパイルに失敗します. これらの変数は,ローカル変数と呼ばれます.

一方のlist1は,Library内のあらゆるメソッドで利用可能です. 実際に,strを利用しているfor内でも参照されています. また,メソッド宣言の外側で宣言された変数(このlist1のような場所で宣言された変数)は, フィールドと呼ばれます.フィールドは 実体が代入された変数を経由して参照できます.Bookを思い出してください. Bookで宣言された title, authors, publisher, publishYearは,Book の実体を代入した変数を介して代入しています.

4-2. shelfの宣言場所を変更する.

さて,話を元に戻します.変数shelfは現在,メソッド内で宣言されています. このままだと,メソッドの外側(他のメソッド)から参照できません. この変数は,本が格納されている変数ですので,様々なメソッドから参照される必要があります. そこで,変数shelfを,フィールドに変更して,クラス内のメソッドから参照できるようにします.

public class Library {
    List<Book> shelf = new ArrayList<Book>();

    void run(){
        // ....
    }
    void printBook(Book book){
        // ....
    }
    void addBooks(List<Book> shelf){
        // ....
    }
    Book createBook(String title, String authors, String publisher, Integer publishYear){
        // ....
    }
    void printWelcomeMessage(){
        System.out.println("ようこそ図書館システムへ");
    }
}

5. 図書館システムが持つ本の一覧を表示する.

5-1. 本の要素での繰り返し.

では,本の一覧を画面に表示してみましょう. 表示するためには,格納している本にアクセスしなければいけません. 本はshelfという名前のList型の変数に格納されていましたね. List型に格納されている要素を一つずつ取り出していきましょう. そのためには,C言語のときと同じく,繰り返しを利用します.

原理的には,C言語と同じく,0番目の要素から,参照するインデックスを1ずつ増やして Listの要素を順に取り出します. しかし,配列やListなど,型によって要素の長さを取得する方法が少しずつ違っています. 配列であれば,args.lengthですし,Listであれば, list.size()です. 型を意識しないと繰り返しを書けないよりは,どんな型であっても 複数の要素を持つ型であれば,同じように各要素を取得できる方が望ましいのは当然です.

5-2. 拡張for文.

Javaはそのような要望に応えるため,拡張for文というものが導入されています(Java SE 5から). 拡張for文は次のように,for(型 変数名: コンテナ変数)のように書くと, コンテナ変数内に格納されている要素を順に取り出せるようになります. コンテナ変数とはList型や配列のような複数の要素を持てる変数を指します.

    for(Book book: shelf){
        // some operation...
    }

この拡張for文のコンテナ変数として扱えるかはBoolean flag = 変数 instanceof Iterable で確認できます.しかし,よく使う型はコンテナ変数であるか否かは覚えてしまいましょう. 今のところ,配列やList, Set がコンテナ変数として扱えることを覚えておけば良いでしょう.

では,shelfというコンテナ変数に格納されているBook 型の変数を一つずつ取り出し,情報を標準出力に出力するメソッド,listを作成しましょう. また,一つのBook型変数を受け取り, 情報を標準出力に出力するメソッドprintBookを作成し, listメソッド内から呼び出してみましょう.

class Library {
    // ....
    void list() {
        for(Book book: shelf) {
            this.printBook(book);
        }
    }
    void printBook(Book book){
        System.out.printf(
            "%s,%s,%s,%d%n", book.title, book.authors,
            book.publisher, book.publishYear
        );
    }
}

6. 図書館システムの本のリストから本を検索する.

6-1. タイトルから本を検索する.

では,特定のタイトルを持つ本を検索できるようにしましょう.Libraryrunメソッドに検索するプログラムを書いてみましょう. 検索するメソッドの名前はfindで,1つのString型の変数を受け取り, Book型の変数を返します.

    void run(){
        // addBooksメソッドは前回の練習問題で行った内容です.
        this.addBooks(shelf);
          
        // ここに,findメソッドを使って,本を検索する処理を追記してください.
        // 本が見つかれば,printBookを使って画面に出力しましょう.
    }

次に検索するfindメソッドを作成しましょう. runメソッドの中で呼び出したように, findメソッドは,返り値の型がBook,引数がString型です. メソッドを宣言しましょう.

public class Library {
    // ....
    // 以下の形式で find メソッドを作成する.
    // 返り値の型 find(引数の型 引数名){
    // }
}

では,findメソッドの中身を作成しましょう. まず,本を検索するためには,shelfの要素を順に調べていく必要があります. shelfの要素を繰り返しで調べていきましょう. 繰り返しは,「5. 図書館システムが持つ本の一覧を表示する」 と同じ方法が利用できます. forの内容は,要素となるBook 型の変数が持つタイトルと引数で与えられたタイトルが一致するかを確認すれば良いですね.

6-2. 変数の比較.

型が違えば比較できません.比較するときは型に注意しましょう. なお,Javaでは文字列の比較は,一般的には,==では行えません. Javaでは実際の値はメモリ上にあり,変数はメモリ上の値を参照しています(C言語で言う所のポインタ変数です). ==による比較は,参照先が同じであるかを判定しており,参照先の値が同じか否かは判定しません.

次の例を見てみましょう.プログラムの枠内のs1, s2, s3, s4String型の変数であると思ってください. メモリの枠内には,具体的な値があり,変数がどの値を指しているかを表しています.
文字列の変数と値
次のようなプログラムコードであると思ってください.

public class StringTest{
    public void run(){
        String s1 = "string1";
        String s2 = s1;
        String s3 = "string2";
        String s4 = "string1";
    }
}

この例では,s1s2はメモリ上の同じ場所を指していますが, s3s4は異なる場所を指しています. この場合,s1 == s2trueとなりますが, s1 == s4s2 == s4falseとなります. 文字列の中身が同じであったとしても,メモリ上の異なる場所を指し示しているためです.

では,文字列の中身が一致しているかを調べるにはどうすれば良いでしょうか. それには,equalsメソッドを利用します. 上記の例では,s1.equals(s2)や,s2.equals(s4)trueになります.異なる場所を指し示している場合でも, 文字列の中身が一致しているかで判定するためです.文字列の内容自体が異なるs3 の場合,s1.equals(s3)s3.equals(s4)falseとなります.

しかし,どこも参照していない変数があればどうなるでしょう.どこも参照していないということはすなわち, nullであることです.nullの変数に対して比較しようとすると NullPointerExceptionというエラーが発生します. そのため,比較するときには,nullでないことの確認が必須です. 例えば,次のようなプログラムです.

    if(s1 != null && s1.equals(s3)){
        // 一致した場合の処理を書く.     
    }

ここで,s1s3の両方がnull であった場合など,単に文字列の内容自体を比較したいのに,4通りも確認するのは面倒です. そこで,近年のJavaでは,Objects.equals(s1, s3) と比較すれば良いようになっています.なお,この比較を行う場合は,プログラムの最初に import java.util.*;という一文が必要です.

では,本の検索に戻りましょう.引数で与えられた文字列と,Book 型の変数が持つtitleが一致するかを判定し, 一致した場合,その要素を返しましょう.一方,全ての要素を調べても見つからなかった場合は, nullを返すようにしましょう.

プログラムが書ければ,検索条件を変えて何度か検索してみましょう.

public class Library {
    // ....
    Book find(String title){
        for(Book book: shelf){
            if(Objects.equals(title, book.title)) {
                return book;
            }
        }
        return null;
    }
}

6-3. 本を絞り込んで検索する.

次に,Book型の変数が持つ4つの変数, titleauthorspublisherpublishYearのどの情報でも絞り込んで調べられるようにしましょう. つまり,publisherpublishYearで絞り込んで, 「とある出版社のこの年に発行された本一覧が欲しい」という要望に対応します. 絞り込みに不要な情報はnullで渡されるものとします. このように与えた条件の全てが一致するように検索する方法をAND検索と呼びます.

この検索では,複数の結果が返される可能性があります.タイトルから本を検索する では,タイトルから本が一意に決まります (現実世界では一意に定まらない場合もありますが,この課題の範囲では定まるものとします). 一方,ここでは,タイトルが指定されない場合もあり,検索結果が複数になる場合があります. そのため,返り値はBookでは表せないことになります.

では,複数のBookを返すにはどのような型を使えば良いのでしょうか. 複数のBook型の変数を格納できるものはどんなものがあったでしょうか. List型が利用できますね.

どのように検索すれば良いでしょうか.ここでは,isMatch メソッドを定義し,検査対象の本と検索条件が引数として渡されます. そして,その本が検索に一致すればtrueを返し, 一致しなければfalseが返されるものとします. ですから,逆に言えば,nullでない条件が一つでも一致しなければ falseを返すようにすれば実現できます.では,実装してみましょう.

4つの変数が条件として指定される可能性があります. 条件が指定されるということは,nullでない値が入っていることを意味します. そして,nullでない条件全てが満たされた場合にisMatchメソッドは trueを返します. 逆に考えれば,条件が一つでも一致しなければ,全体がfalseとなるわけです. ですから,titleが指定された時に,本のタイトルと一致しない場合, isMatchfalseとなります. 他の条件についても同様に考え,全ての条件で判定してみましょう.

プログラムが書ければ,検索条件として与える値を変えて,何度か試してみましょう.

public class Library {
    // ....
    List<Book> findAnd(String title, String authors, String publisher, Integer publishYear){
        List<Book> result = new ArrayList<Book>();

        for(Book book: shelf){
            if(this.isMatch(book, title, authors, publisher, publishYear)) {
                result.add(book);
            }
        }
        return result;
    }
    Boolean isMatch(Book book, String title, String authors,
                    String publisher, Integer publishYear){
        // 条件は,title, authors, publisher, publishYearの4種類.
        // null以外が指定された場合,検索条件として指定されたものとする.
        // ここに,検索条件として与えられた条件全てを満たすか否かを判定する処理を書く.
        // 検索条件を満たしていれば,trueを返し,満たしていなければfalseを返す.
    }
}

7. 図書館システムから本を削除する.

7-1. removeメソッド

ここでは,格納された本のうち,一部の本を登録から削除する機能を追加します. List型に含まれている要素を削除する方法です. 要素を追加するのは,List型の変数に対して, addを呼び出せば実現できました. 削除も同じように,List型の変数に対して, removeを呼び出せば実現できます. addでは,追加したい要素を引数に渡せば,その値が追加されました. removeには何を渡せば良いでしょうか.

7-2. removeメソッドの引数

実は,removeには,要素そのもの,もしくは,インデックスの2種類の値が渡せます. どちらを利用しても良いので,時と場合によって使い分けましょう. では,Libraryremoveメソッドを追加しましょう. ここでは,removeメソッドの引数には削除したい本を渡しましょう. つまり,Book型の変数を渡すようにします. 削除する本は,findメソッドで見つけてきた本を指定します.

public class Library {
    // ....
    void run(){
        // ...
        this.addBooks(shelf);
        // findで本を見つける処理が書かれているはずです.
        // ここで,findで見つかった本を削除するよう,removeメソッドを呼び出しましょう.
        // 最後に,本当に削除されたか,本棚の内容を出力してみましょう.
    }

    void remove(Book book){
        // shelfから引数で与えられた本を削除する処理を書きます.
    }
}

では,実際に上記のremoveメソッドの中身を実装していきましょう. Book型の変数bookfindで見つかった本を shelfに格納されているはずです.

練習問題

1. 検索して表示する

本の一覧を表示するでは,この図書館システムが持つ全ての本が出力されました. この練習問題では,findAndの結果として得られたList に含まれるBookの情報を出力するメソッド,findAndPrintを作成しましょう.

findAndPrintメソッドは,引数として,本のタイトル,著者,出版社,出版年を受け取ります. 返り値の型は,検索結果を返すため,List<Book>としましょう. メソッドの中身では,引数に受け取った情報をそのままfindAndに渡して下さい. そして,findAndの返り値であるList<Book>を変数に代入しましょう. そして,forを使って,得られた検索結果に含まれるBookprintBookを使い,一つずつ画面に出力しましょう.

以上のプログラムがかければ,runメソッドの最後に, 複数の検索結果になるような検索条件を与えて,findAndPrintメソッドを呼び出しましょう. 想定通りの動作になっているかを確認しましょう.

2. OR検索を実装する

6.2 本を絞り込んで検索するで,AND検索を行いました. この練習問題では,OR検索を実現するメソッドを追加して,動作を確認してみましょう. OR検索を実現するメソッド,findOrを作成し,動作を確認しましょう. findOrの引数は,findAndと同じとします. isMatchメソッドは利用できません.同じようなメソッドも作成して findOrを実現してください.

3. 本を削除する?

図書館システムから本を削除するでは,find で見つけた本をList型のremove メソッドに渡すことで削除を実現していました.

では,削除したい本の情報がわかっているものとして, 新たに本をnewすることで,削除するようにしてみましょう. つまり,次のメソッドを実行してみて,実行結果がどうなるか確認してみましょう. また,その理由も考えてみましょう.

public class Library {
    // ...
    void remove2(String title, String author, String publisher, Integer publishYear){
        Book book = createBook(title, author, publisher, publishYear);
        shelf.remove(book);
    }
    // ...
}

本日のまとめ