第3講 Java言語の基礎4

この講で利用するプログラム

第3講 Java言語の基礎4のサブセクション

配列とList

Listとは

第1講で,Javaでは,配列ではなく List を使うように述べました. ここでは,Listの使い方を学びます.

Java言語に限らずListとはデータ構造の一つで,順序を持つ複数のデータを扱います. 要するに配列を置き換えるものです.Listの特徴は次の通りです.

  • 格納された要素をインデックスにより参照できる.
  • 要素を追加・削除できる.
  • 生成時にサイズを決める必要はない.
  • サイズは必要に応じて自動的に増加,縮小する.
  • 途中の要素を削除しても,間が詰められる.

基本的な特徴は配列と同じです. しかし,配列にはない特徴もあります.例えば,サイズを生成時に決める必要がない, やサイズが自動的に増減するなどです.Listは配列よりも優れた特徴を持つ型ですから, 配列ではなく,Listを積極的に使っていきましょう.

Listの種類

Java言語では順序を持つデータの集合をListと呼びます. その実現方法は配列を用いる方法,リンクリストを用いる方法の2種類があります. それぞれの実現方法には向いている点,向いていない点が存在します. そのため,Javaにはそれぞれの実現方法でListを実現する型が 存在します.ArrayListLinkedListの2つの型です.どちらを使っても処理の違いはありませんが, 処理内容によっては実行速度が違ってくる可能性があります. とはいえ,その違いが効いてくるのは,もっと大規模で,実行速度が重視される場合ですから,今は気にしなくても良いでしょう.

リンクリストの詳細については,FAQリンクリストとは何ですかを参照してください.

基本的にArrayListLinkedListも使い方に違いはありません. 以下の例では,ArrayListで説明していますが,ArrayListLinkedListに置き換えても 同じ説明が成り立ちますので,適宜読み替えてください.

なお,ArrayListを使うときには,import文が必要です.import java.util.ArrayList;クラス宣言の前に書きましょう.

Listの宣言方法

Java言語でListを使うには,今までとは少し異なる宣言方法が必要です. データ構造にどのような型の変数を格納するかを型宣言に含める必要があります. 例えば,ArrayListString型や,Integer型を格納しようとすると,次のような宣言が必要になります.

// String型を格納する ArrayList.
ArrayList<String> listForStrings =
    new ArrayList<String>();
// Integer型を格納する ArrayList.
ArrayList<Integer> listForIntegers =
    new ArrayList<Integer>();

上記のように,ArrayList<格納する型> という型として宣言しなければならず,また, 実体を作成するときも,new ArrayList<格納する型>() のように作成しなければいけません. 格納する型が異なるとコンパイルエラーが発生します.

なお,実体を作成するときの格納する型は,下のように省略できます.以下のコードは上に挙げたコードと全く同じ結果になります.

ArrayList<String> listForStrings = new ArrayList<>();
ArrayList<Integer> listForIntegers = new ArrayList<>();

Listの操作

一般的に Listに対して行える操作は4種類です. その操作は,CRUDと呼ばれます.作成(Create),読み取り(Read),更新(Update),削除(Delete)の4種類です. 以降の説明は,次に示すArrayList型の変数に対してメソッドを呼び出すものとして読み進めてください.

ArrayList<String> list = new ArrayList<>();
// ArrayList<String> list = new ArrayList<String>();

Listにデータを追加する (add)

String value1 = // ...
list.add(value1);
list.add("Haruaki Tamada");
// list.add(9); // => コンパイルエラーが発生する.
    // 9 は String型ではないため.
list.add("9"); // => OK"9"は文字列

データを追加するには,上のサンプルのようにaddメソッドを用います. データ集合の最後に追加されていきます.用意されている長さを超えて追加しようとすると,自動的に長さが伸びていくため, 理論上は,無制限にデータを追加できます.

Listにあるデータを取得する (get)

String item1 = list.get(0);
    // => listのインデックスも配列と同じように0から始まる.
String item2 = list.get(1);
String item100 = list.get(100);
    // => 範囲を超えてアクセスしようとすると,実行時に
    // IndexOutOfBoundsException というエラーが発生する

上のサンプルのように,getメソッドを呼び出すことで,List内の特定の要素を取得できます. 返り値の型は,ArrayList型の変数の宣言時に指定した型でなければいけません.

Listにあるデータを更新する (set)

list.set(1, "TAMADA, Haruaki");

特定の要素を置き換える場合は,setメソッドが利用できます. インデックスと置き換え後のデータを指定すると,データの更新が可能です. 指定したインデックスに要素が存在しない場合は,IndexOutOfBoundsExceptionというエラーが発生します.

Listにあるデータを削除する (remove)

list.remove(1);

指定したインデックスの要素を削除したい場合は,removeメソッドを 利用します.removeを使って削除したあと,後ろの要素は詰められます. すなわち,次のコードで全ての要素を順番に削除できます.

while(!list.isEmpty()){ // listが空じゃない間繰り返す.
    list.remove(0);     // 一番最初の要素を削除する.
}

Listのサイズを取得する (size)

List の現在のサイズ(長さ)を取得する場合は,sizeメソッドを利用しましょう. 文字列の長さ,配列の長さと混同しやすいため,注意してください.

  • 文字列(String型)の長さ
    • string.length()(メソッド呼び出しで取得するため括弧が必要).
  • 配列の長さ
    • array.length(変数アクセスのため,括弧は不要).

Listの要素の繰り返し (Iterator)

Java言語での Listの繰り返しは,次の3種類が利用できます.

典型的な方法

一番典型的な方法です.ただし,ループの途中で listの要素数を変化させることは 混乱の元になるので,ループ内での Listへの値の追加・削除は行わない方が良いでしょう.

for(Integer i = 0; i < list.size(); i++){
    String item = list.get(i);
    // ここに繰り返しの処理を書く.
}

Iterator

Iterator 型を利用する方法.Javaらしい書き方.

for(Iterator<String> iterator = list.iterator();
        iterator.hasNext(); ){
    String item = iterator.next();
    // ここに繰り返しの処理を書く.
}

拡張for文

拡張for文と呼ばれる書き方.実質的には,Iteartor型を利用する方法と同じ. コンパイラがIterator型を利用する方法に変換してコンパイルする.最近はこの書き方が多い.

for(String item: list){
    // ここに繰り返しの処理を書く.
}

例題1 argsArrayListに変換する

コマンドライン引数に受け取ったString型の値を全て ArrayListに入れ,ArrayListから順に取り出し,出力するプログラムを書きましょう.

import java.util.ArrayList;
public class ArgsPrinter2{
  void run(String[] args){
    ArrayList<String> list = this.buildList(args);
    this.printList(list);
  }
  ArrayList<String> buildList(String[] array){
    ArrayList<String> arrayList = new ArrayList<>();
    for(Integer i = 0; i < array.length; i++){
      arrayList.add(array[i]);
    }
    return arrayList;
  }
  void printList(ArrayList<String> arrayList){
    for(String item: arrayList){
      System.out.println(item);
    }
  }
  public static void main(String[] args){
    ArgsPrinter2 printer = new ArgsPrinter2();
    printer.run(args);
  }
}

このプログラムを書き,コンパイル,実行してみましょう. 実行時にコマンドライン引数に値を指定して実行してみましょう.

例題2 乱数の生成

50個のDouble型の 0〜1の乱数をArrayListに入れて,出力してみましょう. クラス名は,DoubleValuePrinterとしてください. 乱数の発生方法は,Big & Smallを参照してください. なお,リストを生成する部分,出力する部分を別のメソッドにしてみましょう. クラス名はDoulbeValuePrinterとしましょう. 

完成すれば,コマンドライン引数で発生させる乱数の個数を指定できるようにしてください. 指定されなければ,50個としてください.

出力例

$ java DoubleValuePrinter
  1: 0.24279591112755294
  2: 0.7216985840426494
  3: 0.5978665614812361
... 途中省略
 48: 0.15288776496056167
 49: 0.8335019950136539
 50: 0.8114170360899468
$ java DoubleValuePrinter 3
  1: 0.20585052641970603
  2: 0.578743233682112
  3: 0.107553196759134
import java.util.ArrayList;

public class DoubleValuePrinter{
    void run(String[] args){
        ArrayList<Double> list = this.buildList(args); // リスト作って
        this.printList(list);                          // 出力する.
    }
    ArrayList<Double> buildList(String[] args){
        Integer count = this.parseCount(args); // 生成する個数を決める.
        ArrayList<Double> arrayList = new ArrayList<>(); // 格納するリストを生成する.
        for(Integer i = 0; i < count; i++){
            arrayList.add(Math.random()); // 実際に格納する.
        }
        return arrayList; // リストを返す.
    }
    Integer parseCount(String[] args){
        if(args.length == 0){ // コマンドライン引数で何も与えられなかった場合
            return 50;
        }
        return Integer.valueOf(args[0]); // コマンドライン引数で数が与えられた場合
    }
    void printList(ArrayList<Double> arrayList){
        for(Integer i = 0; i < arrayList.size(); i++){
            System.out.printf("%2d: %f%n", i, arrayList.get(i));
        }
    }
    public static void main(String[] args){
        DoubleValuePrinter printer = new DoubleValuePrinter();
        printer.run(args);
    }
}

ユーザ定義型

ユーザ定義の型を定義する

Javaでは,自分で型を作成することもできます. まずは,人の氏名(姓,名)を持つ型 Person を作成してみましょう.

Person型

public class Person{
    String givenName;
    String familyName;
}

これで,Person型の作成は完了です.Person 型は2つの String 型のフィールド変数 を持っています.givenName(名)とfamilyName(姓)です.

このプログラムだけでは何もできませんので,Person型を利用するプログラムPersonManagerを作成しましょう.

PersonManager型

public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

以上のプログラムを作成し,実行してみましょう. なお,Person.javaPersonManager.javaは同じディレクトリに置いておく必要があります.

例題1 独自型の実体を作成する.

上で作成した PersonManager 型を以下の指示に従って拡張しましょう.PersonManager.javarunメソッドに以下の処理を追加してください. その際,メモリの状態を想像しながらプログラムを書きましょう.

フィールドの参照・代入もメソッド呼び出しと同じように,どの実体のフィールドなのかを 意識して指定してください.Person型のフィールドgivenNameには,上記のperson.givenName = name1 のように,Person型の変数を介してしかアクセスできません.

  1. Person 型の変数 person2 を宣言してください.
  2. person2 に自分の名前を持つ Person 型の実体を作成して,代入してください.
  3. person2 の情報を画面に出力してください.
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);

    Person person2 = createPerson("名前", "自分の");
    this.printPerson(person2);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

PersonManager.javaのコンパイル,実行には,同じディレクトリにPerson.javaを置いておく必要があります.

例題2 実体の代入

次の処理を PersonManagerrun メソッドに追加していきましょう.

  1. Person 型の変数 person3 を宣言してください.
  2. person3person1 を代入してください.
  3. person3familyName を全て大文字にしてください.
    • person3.familyName = person3.familyName.toUpperCase();
  4. person1 の情報を画面に出力してください.
メモリ状態
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");
    this.printPerson(person1);

    Person person2 = createPerson("名前", "自分の");
    this.printPerson(person2);

    Person person3 = person1;
    person3.familyName = person3.familyName.toUpperCase(); // <= person3 の値を更新する.
    this.printPerson(person1); // <= person1 を出力する.
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPerson(Person person){
    System.out.printf("%s, %s%n",
        person.familyName, person.givenName);
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

例題3 独自の型をListに入れる

上で作成したPerson型の実体 person1person3ArrayListに入れて出力部分をまとめてみましょう.

ArrayListには,Integer 型以外でもどのような型でも入れられますので, 上記のように,ArrayList にどのような型を入れるのか,入っている型が何かをしっかりと認識しましょう.

ヒント

    void run(){
        // 今までの処理内容.
        // ただし,printPersonの行は全て削除しておく.
        ArrayList<何を格納するのか> list = new ArrayList<>();
        // listに person1 〜 person3 を追加する.
        this.printPersons(list);
    }
    void printPersons(...){ // <= printPerson を変更する.
        // 引数で与えられた list から要素を一つずつ取り出し,出力する.
    }
public class PersonManager{
  void run(){
    Person person1 =
        this.createPerson("Haruaki", "Tamada");

    Person person2 = createPerson("名前", "自分の");

    Person person3 = person1;
    person3.familyName = person3.familyName.toUpperCase(); // <= person3 の値を更新する.

    ArrayList<Person> list = new ArrayList<>();
    printPersons(list);
  }
  Person createPerson(String name1, String name2){
    Person person = new Person();
    person.givenName = name1;
    person.familyName = name2;
    return person;
  }
  void printPersons(ArrayList<Person> persons){
    for(Person person: persons){
      System.out.printf("%s, %s%n",
          person.familyName, person.givenName);
    }
  }
  public static void main(String[] args){
    PersonManager app = new PersonManager();
    app.run();
  }
}

練習問題

ここに挙げている問題では配列の使用は mainメソッドの引数として与えられた変数 argsのみとし, その他は,List (ArrayListもしくはLinkedList)を使用してください.argsを 受け渡す分には配列を使って構いませんが,新たに配列を作成しないようにしてください.

1. 乱数値100個の統計

0以上,1000未満の乱数を100個取得してください. それらの合計値,最大値,最小値,平均を求めてください. 出力は,まず,合計,最大値,最小値,平均を1行で出力してください. その次に,得られた乱数値を出力してください.ただし,10個出力するごとに改行を入れてください. クラス名は,StatsValues としてください.

平均をDouble型として求めるには,Integer型の合計値sumDouble型に変換する必要があります. Double.valueOf(sum)Double型に変換でき,それ以降Double型として扱えるようになります.

整数乱数を生成する

0以上,1000未満の乱数値を取得するには,Random型を利用します.Random型の 利用には,import java.util.Randomがクラス宣言の前に必要です. 以下のコードで1000未満の正数乱数が得られます.

なお,Random型の初期化は最初に1度だけ行うようにしてください. 乱数を得るたびにRandom型を初期化すると乱数にならない場合があります.

Random random = new Random();
Integer randomValue = random.nextInt(1000);
    // => 0以上1000未満の正の乱数値が得られる

出力例

乱数ですので,必ずしもこの通りの結果にはなりません.

$ java StatsValues
合計: 45262, 最大値: 995, 最小値: 11, 平均値: 452.620000
252  37 553 448 504 144 969 928 177 262 
836  15 198 496 650 977 102 630 348 351 
820  59 288 435 622 677 103 588 576 683 
916 138 154 528 179 411 578 740 715 372 
351 105  41 203 596 746 195 153 469 328 
166 189 754 862 541  84 165 428 567  76 
848 730 947 439 376 258 330 365 896 144 
688  27 380 573 377 752  19  70 965 605 
551 355 103 860 629 660 528 465 995 134 
154 574  11 763 268 443 723 872 233 674

2. 素数の一覧

コマンドライン引数で与えられた値までの素数を出力してください. なお,素数を10個出力するごとに改行を入れてください. クラス名は Primes とします. コマンドライン引数で値が指定されなかった場合は,200までの素数を求めましょう.

素数を求めるには,エラトステネスのふるいを使うと良いでしょう. 次の画像は,0以上100未満の素数を求める時のエラトステネスのふるいのアルゴリズムを表したものです. 順に最後まで繰り返して残った値が素数です. 画像をクリックすると順に素数ではない数値が消えてきます.参考にしてください.

エラトステネスのふるい

まず ArrayListに求めたい値だけ,素数であるフラグ(Boolean型を使えば良いでしょう)をあらかじめ追加しましょう. そして,そこから,素数でないものを順に除外して来ましょう. その後,素数のみを納める別のArrayListを作成し,素数フラグのリストを順に見て素数のリストに格納していきましょう. その素数リストを返すArrayList<Integer> generatePrimes(Integer max) メソッドを作成しましょう.

出力例

$ java Primes 
    2     3     5     7    11    13    17    19    23    29
   31    37    41    43    47    53    59    61    67    71
   73    79    83    89    97   101   103   107   109   113
  127   131   137   139   149   151   157   163   167   173
  179   181   191   193   197   199 
$ java Primes 59
    2     3     5     7    11    13    17    19    23    29
   31    37    41    43    47    53    59

ヒント

ArrayList<Integer> generatePrimes(Integer max){
  ArrayList<Boolean> primes = new ArrayList<>();
  for(Integer i = 0; i <= max; i++){
    primes.add(true); // 仮に全てのiが素数であるとする.
  }
  primes.set(0, false); // 0は素数ではない.
  primes.set(1, false); // 1は素数ではない.

  for(Integer i = 2; i < primes.size(); i++){ 
        // 最小の値である2から始める.
    if(!primes.get(i)){ // iが素数ではなかったら何も行わない.
      continue;
    }
    // j = i * 2 から始めて j += i のインデックスを false にする.
  }
  return primesList(primes);
}
ArrayList<Integer> primesList(ArrayList<Boolean> primes){
  ArrayList<Integer> returnList = new ArrayList<>();
  for(Integer i = 2; i < primes.size(); i++){
    if(primes.get(i)){ // 素数なら returnList に追加する.
      returnList.add(i);
    }
  }
  return returnList;
}

3. 素因数分解

コマンドライン引数で与えられた整数値の素因数分解を行ってください. クラス名は,Factorizerとしてください.

素因数分解を行うには,素数で割り切れなくなるまで,その素数を素因数として記録します. そして,次の小さな素数で同じことを繰り返します.

  • 例えば,12 を素因数分解するとき,一番小さな素数である2で割り切れるかを確認します.
  • 1回目は割り切れ,2を素因数に追加し,割った後の数は6です.
  • 次に,62で割り切れます.再度,素因数に2 を追加します.割った後の数は3です.
  • 32では割り切れませんので,次の小さな素数で割り切れるかを確認します.
  • 33で割り切れますので,素因数として3を追加します.割った後の数は1ですので,これで終了です.

素数のリストを得るには,素数の一覧で作成した PrimesgeneratePrimes メソッドを 利用すれば良いでしょう.Primes.javaFactorizer.javaを同じディレクトリに置いてください. 先ほどの練習問題で適切にプログラムが書けていれば,次のプログラムで100までの素数の一覧が取得できます. なお,この場合,100以上の数を素因数に持つ数値は素因数分解できません. どのような値を generatePrimes メソッドに渡せば良いかに注意してください(定数を利用するのはやめましょう).

Primes primes = new Primes();
ArrayList<Integer> list = primes.generatePrimes(100);

出力例

$ java Factorizer 12
   12: 2 x 2 x 3
$ java Factorizer 24 50
   24: 2 x 2 x 2 x 3
   50: 2 x 5 x 5
$ java Factorizer 123 127
  123: 3 x 41
  127: 127

ヒント

4. 試験成績の分析

import java.util.Random;
public class ExamAnalyzer{
  ExamScore createRandomScore(String name){
    Random random = new Random();
    Integer math = random.nextInt(101);
    Integer physics = random.nextInt(101);
    Integer english = random.nextInt(101);
    return this.createExamScore(math, physics, english, name);
  }
}

上記のプログラムを踏まえて,以下のプログラムを作成してください(各クラスを別のファイルに分けて,両方のソースファイルを提出してください).

  1. ExamScore型を作成してください.
    • Integer型のmath
    • Integer型のphysics
    • Integer型のenglish
    • String型のnameをフィールドに持ちます.
  2. ExamAnalyzerに次のメソッド,処理を追加してください.
    • runメソッドを作成してください.引数,返り値はなしで構いません.
    • 3つのInteger型,1つのString型を受け取り,ExamScoreを返す createExamScoreメソッドを定義してください.
      • メソッドのボディで,ExamScoreの実体を作成し,引数の値を作成したExamScoreの実体のフィールドに代入してください.
      • 代入が終了した ExamScoreの実体を返してください.
    • runメソッド内で次の処理を行ってください.
      • ExamScore型の実体を格納するArrayList実体を作成してください.
      • createRandomScoreメソッドを用いて,ランダムな成績を10個作成してください.
        • 名前は数値の連番("0""9")にしましょう.
        • Integer型変数intValueString型に変換するには,"" + intValueもしくは,intValue.toString()としてください.
      • 作成したArrayListに上記で作成したランダムな成績を追加してください.
      • 全員のmathphysicsenglishごとに平均値,最大値,最小値を求めてください.
      • それぞれExamScoreの実体に対して,mathphysicsenglishの平均値を求めてください.
      • 求めた値を出力してください.

出力例

$ java ExamAnalyzer
       math   phys   eng    ave
ave   51.200 53.100 52.500
max       81     75     99
min       26      0      3
  0       72     53     11 45.333
  1       36     71     65 57.333
  2       33     26      3 20.667
  3       81      0     99 60.000
  4       42     75     36 51.000
  5       26     40     13 26.333
  6       64     57     55 58.667
  7       43     66     90 66.333
  8       50     72     84 68.667
  9       65     71     69 68.333

上記のように Double型の小数点を整形して出力するには,System.out.printf の フォーマット指定を%6.3fとしてください.%6.3f6が 全ての桁数の指定,.3が小数点以下の桁数を指定しています.

まとめ

まとめ

Random random = new Random();
Integer integerRandomValue1 = random.nextInt(100); // 0以上100未満の乱数を返す.
Integer integerRandomValue2 = random.nextInt();    // Integer型の乱数を返す