Javaで語を数える

Table of Contents

1 目標

与えられたファイル中の単語の出現回数をカウントし, 多いものから順に表示する.

2 クラスライブラリ

以下のクラスライブラリを利用する.

2.1 コレクション

2.2 ファイル入出力

2.3 文字列処理

文字列のクラス java.lang.String の以下のメソッドを利用する

3 コレクション

まずArrayList等のコレクションクラスの利用方法を説明する.

3.1 ArrayListの利用方法

ArrayListList の一種であり, リスト構造を実現している.

基本的な利用方法は以下の通り.

  • Stringを要素とするリストを作成する.
    ArrayList<String> list = new ArrayList<String>();
    
  • リストの最後に要素を追加する.
    list.add("abc");
    
  • リストの i 番目の要素を取り出す.
    int i = 0;
    String s = list.get(i);
    
  • リストの要素数を取得する.
    int size = list.size();
    
  • リストが空かどうかテストする.
    if (list.isEmpty()) {
      .....
    }
    
  • リストの全要素を表示する.
    for (String s : list) {
      System.out.println(s);
    }
    

    以下のようにしても良い.

    for (int i = 0; i < list.size(); i++) {
      System.out.println(list.get(i));
    }
    
  • リストの要素をソートする.
    Collections.sort(list);
    
  • 比較関数を指定してソートする.
    Collections.sort(list, new Comparator<String>() {
      public int compare(String s1, String s2) {
        int c = -1 か 0 か 1 の値;
        return c;
      }
    });
    

3.1.1 ArrayListのプログラム例

 1:  import java.util.ArrayList;
 2:  
 3:  public class ArrayListTest {
 4:    public static void main(String[] args) {
 5:      ArrayList<String> list = new ArrayList<String>();
 6:      list.add("abc");
 7:      list.add("def");
 8:      list.add("ghi");
 9:      String s = list.get(0);
10:      System.out.println("first element is " + s);
11:      if (list.isEmpty()) {
12:        System.out.println("list is empty");
13:      }
14:      int size = list.size();
15:      System.out.println("list size is " + size);
16:    }
17:  }

3.2 HashSetの利用方法

HashSetSet の一種であり, 集合を実現している.

基本的な利用方法は以下の通り.

  • Stringを要素とする集合を作成する.
    HashSet<String> set = new HashSet<String>();
    
  • 集合に要素を追加する.
    set.add("abc");
    
  • 集合に要素が含まれているかどうかテストする.
    if (set.contains("abc")) {
      .....
    }
    
  • 集合の要素数を取得する.
    int size = set.size();
    
  • 集合が空かどうかテストする.
    if (set.isEmpty()) {
      .....
    }
    
  • 集合からリストに変換する.
    list = new ArrayList<String>(set);
    
  • リストから集合に変換する.
    set = new HashSet<String>(list);
    

3.2.1 HashSetのプログラム例

 1:  import java.util.HashSet;
 2:  
 3:  public class HashSetTest {
 4:    public static void main(String[] args) {
 5:      HashSet<String> set = new HashSet<String>();
 6:      set.add("abc");
 7:      set.add("def");
 8:      set.add("abc");
 9:      if (set.contains("abc")) {
10:        System.out.println("set containts abc");
11:      }
12:      if (set.isEmpty()) {
13:        System.out.println("set is empty");
14:      }
15:      int size = set.size();
16:      System.out.println("set size is " + size);
17:    }
18:  }

3.3 HashMapの利用方法

HashMapMap の一種であり, ハッシュ表を実現している.

基本的な利用方法は以下の通り.

  • StringをキーとしIntegerを値とするハッシュ表を作成する.
    HashMap<String,Integer> map = new HashMap<String,Integer>();
    
  • ハッシュ表にデータを登録する.
    map.put("abc", 123);
    
  • ハッシュ表にキーが含まれているかどうかテストする.
    if (map.containsKey("abc")) {
      .....
    }
    
  • ハッシュ表の値を取得する.
    int x = map.get("abc");
    
  • ハッシュ表の要素数を取得する.
    int size = map.size();
    
  • ハッシュ表のキーの集合を取得する.
    Set<String> set = map.keySet();
    

3.3.1 HashMapのプログラム例

 1:  import java.util.HashMap;
 2:  
 3:  public class HashMapTest {
 4:    public static void main(String[] args) {
 5:      HashMap<String,Integer> map = new HashMap<String,Integer>();
 6:      map.put("abc", 1);
 7:      map.put("def", 1);
 8:      if (map.containsKey("abc")) {
 9:        int x = map.get("abc");
10:        System.out.println("map contains " + x + " for abc");
11:      }
12:      if (map.isEmpty()) {
13:        System.out.println("map is empty");
14:      }
15:      int size = map.size();
16:      System.out.println("map size is " + size);
17:    }
18:  }

4 ファイル入力

ファイル入力には, 以下のように FileReaderBufferedReader を利用する.

 1:  String fileName = ファイル名;
 2:  try {
 3:    BufferedReader in = new BufferedReader(new FileReader(fileName));
 4:    while (true) {
 5:      String line = in.readLine();
 6:      if (line == null) {
 7:        break;
 8:      }
 9:      line の処理;
10:    }
11:    in.close();
12:  } catch (FileNotFoundException e) {
13:    e.printStackTrace(); // ファイルが存在しなかったという例外
14:  } catch (IOException e) {
15:    e.printStackTrace(); // 入出力エラーの例外
16:  }
  • FileReader は文字ファイルを読み込むためのクラスである.
  • BufferedReader は文字入力をバッファリングするクラスである.
  • バッファリングすることで,一行単位での入力 ( readLine() )が可能になる.
  • readLine() は一行を読み込む. 入力が終わった場合は null が返る.
  • close() は入力バッファ(および入力ファイル)を閉じる.
  • try-catch は,例外処理のための構文である.
  • tryブロック中の実行中に, FileNotFoundException が生じると, tryブロックの実行を中断し,該当するcatchブロックが実行される.
  • tryブロック中の実行中に, IOException が生じると, tryブロックの実行を中断し,該当するcatchブロックが実行される.
  • e.printStackTrace() は,実行トレースを出力する.

5 文字列処理

文字列のクラス String には文字列処理のための様々なメソッドが用意されている.

5.1 大文字への変換

大文字への変換には toUpperCase() メソッドを利用する.

line = line.toUpperCase();

5.2 文字列の置換

指定された 正規表現 にマッチする部分文字列を探索し, それらを指定された文字列に置換するには replaceAll メソッドを利用する.

  • 文字 a, b のいずれかにマッチする部分文字列を x に置換する.
    s = "abracadabra";
    t = s.replaceAll("[ab]", "x");
    

    t は "xxrxcxdxxrx" になる.

  • 英大文字にマッチする部分文字列を x に置換する.
    s = "Kobe University";
    t = s.replaceAll("[A-Z]", "x");
    

    t は "xobe xniversity" になる.

  • 英大文字にマッチしない部分文字列を x に置換する.
    s = "Kobe University";
    t = s.replaceAll("[^A-Z]", "x");
    

    t は "KxxxxUxxxxxxxxx" になる.

5.3 文字列の分割

指定された 正規表現 にマッチする部分で文字列を分割するには split メソッドを利用する.

たとえば,以下のようなプログラムでは, 文字列sは文字 a にマッチする部分で分割される.

s = "abracadabra";
String p[] = s.split("a");

分割された結果は配列であり,以下のようになる.

p[0]=""
p[1]="br"
p[2]="c"
p[3]="d"
p[4]="br"

空白文字が一文字以上連続している場所で分割するには 以下のように書く.

s = "Kobe University    Japan";
String p[] = s.split("\\s+");

空白文字の正規表現は \s だが, ダブルクォーテーション中にバックスラッシュを記述する場合, \\ と書かないといけないため \\s となる点に注意する. また + は一文字以上連続することを表す正規表現での記法である.

6 クラス設計

与えられた問題を解くには, 以下のようなクラスCountTableがあれば便利だろう.

1:  // 文字列の回数を数える表を作成
2:  CountTable table = new CountTable();
3:  // 文字列を登録し,回数を数える
4:  table.add("abc");
5:  table.add("def");
6:  table.add("abc");
7:  // 文字列の回数を取得
8:  int count = table.get("abc");  // 2が返る
  • このようなクラスは,標準のJavaでは用意されていないので, 自分で作ることにする.

6.1 インターフェイスを決める

どのようなメソッドを提供するかを設計する.

  • new CountTable()
    新しいカウンタ表を作成する.
  • void add(String s)
    文字列 s をカウンタ表に登録する. 同じ文字列が登録された回数を覚える.
  • int get(String s)
    カウンタ表に文字列 s が登録された回数を返す. 一度も登録されていない場合は,0を返す.
  • List<String> getKeysByCount()
    カウンタ表に登録されている文字列を, 登録回数の降順でソートしたリストを返す.
  • int size()
    カウンタ表に登録されている文字列の個数を返す.
  • boolean isEmpty()
    カウンタ表が空なら真を返す.
  • void clear()
    カウンタ表を空にする.

6.2 シナリオを考える

使い方のシナリオを考える(テストにも有効).

1:  CountTable table = new CountTable();
2:  table.add("abc");
3:  table.add("def");
4:  table.add("abc");
5:  List<String> list = table.getKeysByCount();
6:  for (String s : list) {
7:    int count = table.get(s);
8:    System.out.println(count + " " + s);
9:  }

7 クラス実装

7.1 実装に利用できるクラスを探す

CountTableクラスを実装には, HashMapクラスを利用するのがよさそうだ.

 1:  // StringをキーとしIntegerを値とするハッシュ表を作成
 2:  HashMap<String,Integer> map = new HashMap<String,Integer>();
 3:  // ハッシュ表にデータを登録
 4:  map.put("abc", 123);
 5:  // ハッシュ表にキーが含まれているかどうかのテスト
 6:  if (map.containsKey("abc")) {
 7:    .....
 8:  }
 9:  // ハッシュ表の値を取得
10:  int x = map.get("abc");
11:  // ハッシュ表のキーの集合を取得し,リストに変換
12:  Set<String> set = map.keySet();
13:  List<String> list = new ArrayList<String>(set);

7.2 実装方法の選択

他クラスのオブジェクトを利用するクラスを実装するには, 以下の二種類の方法がある.

  • is-aによる実装
    • 他クラスの拡張クラスとして実装する.
    • HashMap<String,Integer> クラスの拡張クラスとして CountTableクラスを実装する.
  • has-aによる実装
    • 他クラスのオブジェクトをメンバ(インスタンス変数)として実装する.
    • HashMap<String,Integer> クラスのオブジェクトを, CountTableクラスのメンバ(インスタンス変数)として実装する.

7.2.1 is-aによる実装例

以下に is-aによる実装例 CountTable1.java の一部を示す.

 1:  public class CountTable1 extends HashMap<String,Integer> {
 2:  
 3:    public int get(String s) {
 4:      int count = 0;
 5:      if (containsKey(s)) {
 6:        count = super.get(s);
 7:      }
 8:      return count;
 9:    }
10:  
11:    public void add(String s) {
12:      int count = get(s);
13:      put(s, count + 1);
14:    }
15:  
16:    public List<String> getKeysByCount() {
17:      .....
18:    }
19:  }
  • size(), isEmpty(), clear() は HashMapから継承されるので,実装は不要.

7.2.2 has-aによる実装例

以下に has-aによる実装例 CountTable2.java の一部を示す.

 1:  public class CountTable2 {
 2:    HashMap<String,Integer> map = new HashMap<String,Integer>();
 3:  
 4:    public int get(String s) {
 5:      int count = 0;
 6:      if (map.containsKey(s)) {
 7:        count = map.get(s);
 8:      }
 9:      return count;
10:    }
11:  
12:    public void add(String s) { ..... }
13:  
14:    public void clear() { ..... }
15:  
16:    public int size() { ..... }
17:  
18:    public boolean isEmpty() { ..... }
19:  
20:    public List<String> getKeysByCount() { ..... }
21:  }
  • すべてを実装する必要がある.

7.2.3 is-aによる実装とhas-aによる実装の比較

  • is-aによる実装
    • 親クラスのメソッドを継承できるので,実装が楽.
    • 親クラスのメソッドを隠そうとすると,面倒.
    • 親クラスのメソッドを利用されていると, 実装方法を変更したくなっても対応が困難.
  • has-aによる実装
    • 親クラスのメソッドを継承できないので,実装が面倒.
    • 公開するメソッドを限定できるので, モジュールとしての独立性が高まり, 実装方法を変更したくなった場合の対応が容易.

将来の変更を想像して,実装方法を決める.

8 単体テスト

シナリオにしたがってテストしてみる.

1:  CountTable1 table = new CountTable1();
2:  table.add("abc");
3:  table.add("def");
4:  table.add("abc");
5:  List<String> list = table.getKeysByCount();
6:  for (String s : list) {
7:    int count = table.get(s);
8:    System.out.println(count + " " + s);
9:  }

以下が出力されればOK.

2 abc
1 def

9 プログラム作成

最終的なプログラムは以下のようになる ( WordCount.java ).

 1:  import java.io.BufferedReader;
 2:  import java.io.FileReader;
 3:  import java.io.FileNotFoundException;
 4:  import java.io.IOException;
 5:  
 6:  public class WordCount {
 7:    public static void main(String[] args) {
 8:      String fileName = "hamlet.txt";
 9:      if (args.length > 0) fileName = args[0];
10:      try {
11:        CountTable1 table = new CountTable1();
12:        BufferedReader in = new BufferedReader(new FileReader(fileName));
13:        while (true) {
14:          String line = in.readLine();
15:          if (line == null) {
16:            break;
17:          }
18:          line = line.toUpperCase();
19:          line = line.replaceAll("[^A-Z]", " ");
20:          for (String s : line.split("\\s+")) {
21:            if (! s.equals("")) {
22:              table.add(s);
23:            }
24:          }
25:        }
26:        in.close();
27:        for (String s : table.getKeysByCount()) {
28:          int count = table.get(s);
29:          System.out.println(count + "\t" + s);
30:        }
31:      } catch (FileNotFoundException e) {
32:        e.printStackTrace();
33:      } catch (IOException e) {
34:        e.printStackTrace();
35:      }
36:    }
37:  }

コンパイルして,以下のように実行する.

$ java WordCount 入力ファイル名

Date: 2017-09-29 21:44:41 JST

Author: 田村直之

Validate XHTML 1.0