M.Hiroi's Home Page

お気楽 Java プログラミング入門

メソッドの関数的な使い方

[ PrevPage | Java | NextPage ]

はじめに

前回と前々回で Java の基本的なデータ型と制御構造について一通り説明しました。今回はメソッドについて取り上げます。Java はオブジェクト指向プログラミング言語なので、クラスを定義しないとプログラムを作ることができません。このクラスの中で定義された関数がメソッドになります。とりあえずオブジェクト指向のことはおいといて、メソッドの関数的な使い方について説明します。

●関数の基礎知識

プログラミングは模型を組み立てる作業と似ています。簡単な処理は Java の機能やライブラリを使って実現することができます。ところが、模型が大きくなると、一度に全体を組み立てるのは難しくなります。このような場合、全体をいくつかに分割して、まずその部分ごとに作ります。最後に、それを結合して全体を完成させます。

これはプログラミングにも当てはまります。実現しようとする処理が複雑になると、一度に全部作ることは難しくなります。そこで、全体を小さな処理に分割して、一つ一つの処理を作成し、それらを組み合わせて全体のプログラムを完成させます [*1]

分割した処理を作成する場合、それを一つの部品として扱えると便利です。つまり、小さな部品を作り、それを使って大きな部品を作り、最後にそれを組み合わせて全体を完成させます。このとき、もっとも基本となる部品が関数です。Java はオブジェクト指向プログラミング言語なので、厳密な意味での関数はありませんが、メソッドを関数のように使うことは簡単にできます。

-- note --------
[*1] このような方法を分割統治法といいます。

●関数の定義方法

Java の関数 (メソッド) 定義はとても簡単です。本ドキュメントでは、関数的な使い方をするメソッドのことを関数と呼ぶことにします。例題として、数を 2 乗する関数を作ってみましょう。次のリストを見てください。

リスト : 数を 2 乗する関数

public class sample30 {
  static int square(int x) {
    return x * x;
  }

  public static void main(String[] args) {
    System.out.println(square(10));
  }
}

関数定義の構文を図 1 に示します。

static データ型 名前(仮引数名, ...){
  処理A;
  処理B;
  ...
}

図 1 : Java の関数定義

関数の定義は図 2 のように数式と比較するとわかりやすいでしょう。

square が関数名、( ) の中の x が入力データを受け取る引数、ブロック { } の中の return x * x が実行される処理です。関数定義で使用する引数のことを「仮引数」、実際に与えられる引数を「実引数」といいます。square の定義で使用した x が仮引数で、square(10) の 10 が実引数になります。

そして、関数が出力する値を「返り値」といいます。返り値のデータ型は関数名の前で指定します。Java の場合、関数の値は return 文を使って返します。return x * x; とすることで、x * x の計算結果を返します。Java の場合、メソッドはクラスメソッドとインスタンスメソッドの 2 種類があり、static を付けるとクラスメソッドになります。とりあえず、関数的な使い方をする場合は static を付けると覚えておいてください。

それでは実際に実行してみましょう。

$ javac sample30.java
$ java sample30
100
jshell> /open sample30.java

jshell> sample30.square(20)
$2 ==> 400

square はクラスメソッドなので、JShell で sample30.java を読み込み、sample30.square(20) と呼び出すことができます。なお、値を返さない関数も定義することができます。この場合、返り値の型には void を指定してください。

それから、JShell で直接関数を定義する場合、static を付ける必要はありません。

jshell> int square(int x) { return x * x; }
|  次を作成しました: メソッド square(int)

jshell> square(20)
$2 ==> 400

JShell 内部でクラスが自動的に生成され、そのクラスメソッドとして square が定義されます。

●局所変数と大域変数

それでは、ここで変数 x に値が代入されている場合を考えてみましょう。次の例を見てください。

リスト : 局所変数と大域変数

public class sample31 {
  static int x = 10;
  
  static int square(int x) {
    return x * x;
  }

  public static void main(String[] args) {
    System.out.println(x);
    System.out.println(square(5));
    System.out.println(x);
  }
}
$ javac sample31.java
$ java sample31
10
25
10

クラスの先頭で変数 x を定義しています。Java の場合、クラスで定義された変数を「フィールド変数」といいます。通常、フィールド変数はインスタンスを生成するときインスタンスの中に割り当てられます。他のオブジェクト指向言語では「インスタンス変数」と呼びます。

ところが、フィールド変数に static を付けると、インスタンスではなくクラスに割り当てられます。これを「クラス変数」と呼びます。インスタンス変数はインスタンスを生成しないとアクセスすることはできませんが、クラス変数はインスタンスを生成しなくても、同じクラスのメソッドからアクセスすることができます。

最初の println() ではクラス変数の x を参照するので 10 が表示されます。それでは、square(5) の実行結果はどうなると思いますか。x には 10 がセットされているので 10 の 2 乗を計算して返り値は 100 になるのでしょうか。これは 5 の 2 乗を計算して結果は 25 になります。そして、square() を実行したあとでもクラス変数 x の値は変わりません。

square() の仮引数 x は、その関数が実行されている間だけ有効です。このような変数を「ローカル変数 (local variable)」もしくは「局所変数」といいます。これに対し、プログラムのどこからでもアクセスできる変数を「グローバル変数 (golbal variable)」もしくは「大域変数」といいます。

ところで、var による変数の宣言は「ローカル変数の型推論」という機能ですが、名前が示すようにローカル変数の宣言にしか使用することができません。ご注意くださいませ。

Java は変数の値を求めるとき、それが局所変数であればその値を参照します。局所変数でなければ、フィールド変数の値を参照します。Java には厳密な意味での大域変数は存在しませんが、フィールド変数のアクセス権を設定することで、クラス変数を大域変数のように使用することは可能です。ただし、この方法はお勧めしません。

プログラムを作る場合、関数を部品のように使います。ある関数を呼び出す場合、いままで使っていた変数の値が勝手に書き換えられると、呼び出す方が困ってしまいます。部品であるならば、ほかの処理に影響を及ぼさないように、自分自身の中で処理を完結させることが望ましいのです。これを実現するための必須機能が局所変数なのです。

●局所変数の定義と有効範囲

Java の場合、関数の仮引数は局所変数になりますが、それ以外にも関数の中で局所変数が必要になる場合があります。Java は関数内で宣言された変数を局所変数として扱います。今まで main() の中で変数を宣言しましたが、これらの変数はすべて局所変数になります。

簡単な例を示しましょう。

リスト : 局所変数の有効範囲

public class sample32 {
  public static void main(String[] args) {
    int x = 1;
    {
      int y = 2;
      {
        int z = 3;
        System.out.println(x);
        System.out.println(y);
        System.out.println(z);
      }
      System.out.println(x);
      System.out.println(y);
      // System.out.println(z);  z は範囲外 (コンパイルエラー)
    }
    System.out.println(x);
    // System.out.println(y);    y は範囲外 (コンパイルエラー)
    // System.out.println(z);    z は範囲外 (コンパイルエラー)
  }
}
$ javac sample32.java
$ java sample32
1
2
3
1
2
1

局所変数が値を保持する期間のことを、変数の「有効範囲 (scope : スコープ)」といいます。局所変数の有効範囲は変数が定義されているブロックの中だけです。for 文の場合も同じで、初期化処理で宣言された局所変数は、そのあとのブロックが有効範囲になります。

変数 x は main() の一番外側のブロックで定義されているので、main() の処理が終了するまで有効です。変数 y は 2 番目のブロックで、変数 z は 3 番目のブロックで定義されているので、各々のブロックの終わりまでが変数の有効範囲になります。ブロックの実行が終了すると、そのブロックで定義された局所変数は廃棄されます。

したがって、3 番目のブロックの中では変数 x, y, z が有効です。ブロックの処理が終了すると変数 z が廃棄されるので、2 番目のブロックの中では変数 x, y が有効です。そして、そのブロックの処理が終了すると、変数 y が廃棄されるので有効な変数は x と引数の args だけになります。

もう一つ簡単な例を示しましょう。

jshell> int sum(int[] ary){
   ...> int total = 0;
   ...> for (int n: ary) total += n;
   ...> return total;
   ...> }
|  次を作成しました: メソッド sum(int[])

jshell> sum(new int[]{1,2,3,4,5,6,7,8,9,10})
$2 ==> 55

関数 sum() の引数 ary には要素が int の配列を渡します。変数 total は関数内で宣言しているので、局所変数として使用することができます。また、for 文で使う変数 n も局所変数になります。この場合、変数 n の有効範囲は for 文の中だけです。for 文による繰り返しが終了すると、変数 n は廃棄されることに注意してください。

sum() の処理内容は簡単です。最初に変数 total を 0 に初期化します。次に for 文で配列の要素を順番に取り出して変数 n に代入し、n の値を total に加算していきます。最後に total の値を返します。実際に sum() を呼び出すと、配列の合計値を求めることができます。

●可変長引数

仮引数の個数よりも多くの値を受け取りたい場合は、データ型の後ろに ... を付けた仮引数を用意します。これを「可変長引数」といい、JDK 5 から導入された機能です。仮引数に入りきらない値は、配列に格納されて可変長引数に渡されます。これで可変個の引数を受け取る関数を定義することができます。簡単な例を示しましょう。

リスト : 可変長引数

public class sample34 {
  static void foo(int a, int... args) {
    System.out.print(a + ",[");
    for (int n: args) {
      System.out.print(n + ",");
    }
    System.out.println("]");
  }

  static void foo0(int... args) {
    System.out.print("[");
    for (int n: args) {
      System.out.print(n + ",");
    }
    System.out.println("]");
  }

  public static void main(String[] args){
    foo(1);
    foo(1, 2);
    foo(1, 2, 3);
    foo(1, 2, 3, 4);
    foo0();
    foo0(1);
    foo0(1, 2);
    foo0(1, 2, 3);
  }
} 
$ javac sample34.java
$ java sample34
1,[]
1,[2,]
1,[2,3,]
1,[2,3,4,]
[]
[1,]
[1,2,]
[1,2,3,]

可変長引数は通常の仮引数よりも後ろに定義します。関数 foo() は通常の引数が一つしかありません。foo(1) と呼び出すと、引数 a に 1 がセットされます。実引数はもうないので、仮引数 args には空の配列が渡されます。次に foo(1, 2) と呼び出すと、実引数 2 が配列に格納されて仮引数 args に渡されます。同様に、foo(1, 2, 3) は 2 と 3 が配列に格納されて仮引数 args に渡されます。

関数 foo0() は、0 個以上の引数を受け取る関数、つまり、引数があってもなくてもどちらでも動作します。この場合、仮引数は args だけになります。実引数がない場合、引数 args には空の配列 [ ] が渡されます。もし、複数の引数があれば、それらを配列にまとめて仮引数 args に渡します。

●関数の多重定義

Java は同じクラス内で同名の関数 (メソッド) を複数定義することができます。これを「多重定義 (overload)」といいます。ただし、引数のデータ型、個数、並び方などが異なる必要があります。これらがまったく同じメソッドを多重定義することはできません。

簡単な例を示しましょう。

リスト : 多重定義

public class sample35 {
  static int max(int a, int b) {
    return a < b ? b : a;
  }
  
  static double max(double a, double b) {
    return a < b ? b : a;
  }

  public static void main(String[] args) {
    System.out.println(max(1, 2));
    System.out.println(max(1.1, 2.2));
  }
}
$ javac sample35.java
$ java sample35
2
2.2

関数 max は引数 a と b で大きいほうを返します。同じ名前の関数が 2 つ定義されていますが、仮引数のデータ型が異なっています。実引数に int を渡すと int max(int a, int b) が呼び出されます。double を渡すと double max(double a, double b) が呼び出されます。このように、同じ名前の関数を定義することができます。

ところで、メソッド print() と println() は整数、浮動小数点数、文字列などいろいろなデータを出力できますが、これは多重定義されているからできることなのです。なお、max() は Java の Math クラスで多重定義されていて、Math.max() で呼び出すことができます。

●データの探索

それでは簡単な例題として、データの探索処理を作ってみましょう。データの探索とは、データの集まりの中から特定のデータを見つける処理のことです。データの探索はプログラムの中で最も基本的な操作の一つです。たとえば配列からデータを探す場合、いちばん簡単な方法は先頭から順番にデータを比較していくことです。これを「線形探索 (linear searching)」といます。次のリストを見てください。

リスト : データの探索

public class sample36 {
  // n があるか
  static boolean find(int n, int[] ary) {
    for (int x: ary) {
      if (x == n) return true;
    }
    return false;
  }

  // n の位置を求める
  static int position(int n, int[] ary) {
    for (int i = 0; i < ary.length; i++) {
      if (ary[i] == n) return i;
    }
    return -1;
  }

  // n の個数を求める
  static int count(int n, int[] ary){
    int c = 0;
    for (int x: ary) {
      if (x == n) c++;
    }
    return c;
  }

  public static void main(String[] args) {
    int[] a = {1, 2, 3, 1, 2, 3, 4, 5};
    System.out.println(find(4, a));
    System.out.println(find(6, a));
    System.out.println(position(5, a));
    System.out.println(position(7, a));
    System.out.println(count(3, a));
    System.out.println(count(8, a));
  }
}
$ javac sample36.java
$ java sample36
true
false
7
-1
2
0

関数 find() は配列 ary の中から引数 n と等しいデータを探します。for 文で配列の要素を一つずつ順番に取り出して n と比較します。等しい場合は true を返します。for ループが終了する場合は n と等しい要素が見つからなかったので false を返します。関数 position() は、データを見つけた場合はその位置 i を返し、見つからない場合は -1 を返します。

find() と position() は最初に見つけた要素とその位置を返しますが、同じ要素が配列に複数あるかもしれません。関数 count() は等しい要素の個数を数えて返します。局所変数 c を 0 に初期化し、n と等しい要素 x を見つけたら c の値を +1 します。最後に c の値を返します。

このように、線形探索は簡単にプログラムできますが、大きな欠点があります。データ数が多くなると処理に時間がかかるのです。近年、パソコンの性能は著しく向上しているので、線形探索でどうにかなる場合もありますが、データ数が多くて時間かかかるのであれば、次の例題で取り上げる「二分探索」や他の高速な探索アルゴリズム [*2] を使ってみるとよいでしょう。

-- note --------
[*2] 基本的なところでは「ハッシュ法」や「二分探索木」などがあります。

●二分探索

次は「二分探索 (バイナリサーチ:binary searching)」を例題として取り上げます。線形探索の実行時間は要素数 N に比例するので、N が大きくなると時間がかかるようになります。これに対し、二分探索は log2 N に比例する時間でデータを探すことができます。

ただし、探索するデータはあらかじめ昇順に並べておく必要があります。この操作を「ソート (sort)」といいます。二分探索は最初にデータをソートしておかないといけないので、線形探索に比べて準備に時間がかかります。

二分探索の動作を図 3 に示します。


              図 3 : 二分探索

二分探索は探索する区間を半分に分割しながら調べていきます。キーが 66 の場合を考えてみましょう。まず区間の中央値 55 とキーを比較します。データが昇順にソートされている場合、66 は中央値 55 より大きいので区間の前半を調べる必要はありません。したがって、後半部分だけを探索すればいいのです。

あとは、これと同じことを後半部分に対して行います。最後には区間の要素が一つしかなくなり、それとキーが一致すれば探索は成功、そうでなければ探索は失敗です。ようするに、探索するデータ数が 1 / 2 ずつ減少していくわけです。図 3 の場合、線形探索ではデータの比較が 6 回必要になりますが、二分探索であれば 4 回で済みます。また、データ数が 1,000,000 個になったとしても、二分探索を使えば高々 20 回程度の比較で探索を完了することができます。

それでは、配列からデータを二分探索するプログラムを作ってみましょう。Java にはクラス Arrays に二分探索を行うメソッド binarySearch() とソートを行うメソッド sort() が用意されていますが、私達でも繰り返しを使って簡単にプログラムすることができます。次のリストを見てください。

リスト : 二分探索

public class sample37 {
  static boolean binarySearch(int n, int[] ary){
    int low = 0;
    int high = ary.length - 1;
    while (low <= high) {
      int mid = (low + high) / 2;
      if (n == ary[mid])
        return true;
      else if (n > ary[mid])
        low = mid + 1;
      else
        high = mid - 1;
    }
    return false;
  }

  public static void main(String[] args) {
    int[] a = {10, 20, 30, 40, 50, 60, 70, 80};
    System.out.println(binarySearch(20, a));
    System.out.println(binarySearch(60, a));
    System.out.println(binarySearch(45, a));
  }
}
$ javac sample37.java
$ java sample37
true
true
false

最初に、探索する区間を示す変数 low と high を初期化します。配列の長さは ary.length で取得し、最後の要素の位置を high にセットします。次の while ループで、探索区間を半分ずつに狭めていきます。まず、区間の中央値を求めて変数 mid にセットします。if 文で mid の位置にある要素と n を比較し、等しい場合は探索成功です。return で true を返します。

n が大きい場合は区間の後半を調べます。変数 low に mid + 1 をセットします。逆に、n が小さい場合は前半を調べるため、変数 high に mid - 1 をセットします。これを区間が二分割できるあいだ繰り返します。low が high より大きくなったら分割できないので繰り返しを終了し false を返します。

二分探索はデータを高速に探索することができますが、あらかじめデータをソートしておく必要があります。このため、途中でデータを追加するには、データを挿入する位置を求め、それ以降のデータを後ろへ移動する処理が必要になります。つまり、データの登録には時間がかかるのです。

したがって、二分探索はプログラムの実行中にデータを登録し、同時に探索も行うという使い方には向いていません。途中でデータを追加して探索も行う場合は、他の高速な探索アルゴリズムを検討してみてください。

●素数を求める (2)

それでは関数を使って、前回作成した素数を求めるプログラムを書き直して見ましょう。リスト 9 を見てください。

リスト : 素数を求める

public class Prime1 {
  // 素数のチェック
  static boolean isPrime(int n, int primeSize, int[] primeTable) {
    for(int i = 1; i < primeSize; i++){
      int p = primeTable[i];
      if (p * p > n) break;
      if (n % p == 0) return false;
    }
    return true;
  }
  
  public static void main(String[] args) {
    int primeTable[] = new int [100];
    int primeSize = 1;
    primeTable[0] = 2;
    for (int n = 3; n < primeTable.length; n += 2) {
      if (isPrime(n, primeSize, primeTable)) {
        primeTable[primeSize++] = n;
      }
    }
    for (int i = 0; i < primeSize; i++) {
      System.out.print(primeTable[i] + " ");
    }
  }
}
$ javac Prime1.java
$ java Prime1
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

数値 n が素数か判定する処理を関数 isPrime() で行うように変更します。isPrime() は数値 n と素数の個数 primeSize と配列 primeTable を受け取り、n が素数で割り切れれば false を返し、そうでなければ true を返します。

Lisp / Scheme などの関数型言語の世界では、真偽値を返す関数を「述語 (predicate)」といいます。Java の場合、真偽値を返す関数は名前の先頭 (接頭辞) に is, has, can などを付けて表すことが多いようです。

isPrime() を使うと、素数を求める処理は簡単にプログラムすることができます。isPrime() が true を返したら n を配列 primeTable に追加するだけです。素数の判定処理を関数 isPrime() で行うことにより、関数 main() はとてもわかりやすいプログラムになりました。

●値呼びと参照呼び

一般に、関数呼び出しには二つの方法があります。一つが「値呼び (call by value)」、もう一つが「参照呼び (call by reference)」です。近代的なプログラミング言語では「値呼び」が主流です。

値呼びの概念はとても簡単です。

  1. 受け取るデータを格納する変数 (仮引数) を用意する。
  2. データを引数に代入する。
  3. 関数の実行終了後、引数を廃棄する。

値呼びのポイントは 2. です。データを引数に代入するとき、データのコピーが行われるのです。たとえば、変数 a の値が 10 の場合、関数 foo(a) を呼び出すと、実引数 a の値 10 が foo の仮引数にコピーされます。変数に格納されている値そのものを関数に渡すので、「値渡し」とか「値呼び」と呼ばれます。また、値呼びは任意の式の値を実引数として渡すことができます。たとえば foo(a + b) の場合、引数に渡された式 a + b を計算し、その結果が foo の仮引数に渡されます。

値呼びは単純でわかりやすいのですが、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできると便利な場合もあります。仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」です。

Java は「値呼び」です。文字型を含む数や boolean などの「基本データ型」は、仮引数にデータをセットするときデータのコピーが行われます。ただし、配列や文字列などのオブジェクトは「参照データ型」といって、仮引数にデータをセットするとき、オブジェクトのコピーは行われません。Java の変数 (引数) は参照データ型を格納しているのではなく、そのオブジェクトへの参照を格納しているのです。参照はC言語のポインタや Perl のリファレンスのことで、実態はオブジェクトに割り当てられたメモリのアドレスです。図 4 を見てください。

変数 a に文字列 "ab" を代入する場合、Java は "ab" を a に書き込むのではありません。文字列 (オブジェクト) "ab" を生成して、図 4 (1) のようにオブジェクトへの参照を a に書き込みます。a の値を変数 b に代入する場合も、図 4 (2) のように a に格納されているオブジェクトへの参照を b に書き込むだけで、文字列はコピーされません。

したがって、参照データ型の場合、代入はデータに名札を付ける操作と考えることができます。図 4 (2) のように、一つのデータに複数の名札を付けることもできるわけです。これは引数の場合も同じです。参照データ型の場合、実引数に格納されている値はオブジェクトへの参照であり、それが仮引数にコピーされます。つまり、アドレスを値渡ししているわけです。

オブジェクトの同一性は演算子 == で調べることができます。次の例を見てください。

jshell> var a = new String("ab")
a ==> "ab"

jshell> var b = a
b ==> "ab"

jshell> var c = new String("ab")
c ==> "ab"

jshell> a == b
$4 ==> true

jshell> a == c
$5 ==> false

jshell> a.equals(c)
$6 ==> true

new String("ab") は String 型のインスタンスを生成します。インスタンスはオブジェクトのことです。オブジェクト指向についていはあとで詳しく説明します。

変数 a と c には String のオブジェクトを代入し、変数 b に a の値を代入します。変数 a と b は同じオブジェクトを参照しているので、a == b は true になります。a と c は異なるオブジェクトなので、値が同じ文字列 "ab" でも a == c は false になります。値を比較したい場合はメソッド equals() を使います。インスタンスメソッドは次の形式で呼び出します。

object.method(args, ...)

インスタンス object の後ろにドット ( . ) を付けて、その後ろに呼び出すメソッドを指定します。a.equals(c) は a と c の値を比較するので true になります。文字列が等しいか調べる場合は演算子 == ではなくメソッド equals() を使ってください。

ただし、配列のような更新可能なオブジェクトの場合、関数の引数に配列を渡してそれを破壊的に修正すると、呼び出し元の変数の値も書き換えられたかのようにみえます。次の例を見てください。

jshell> void foo(int[] ary) { ary[0] *= 10; }
|  次を作成しました: メソッド foo(int[])

jshell> var d = new int[] {1,2,3,4,5,6,7,8}
d ==> int[8] { 1, 2, 3, 4, 5, 6, 7, 8 }

jshell> foo(d)

jshell> d
d ==> int[8] { 10, 2, 3, 4, 5, 6, 7, 8 }

変数 d に配列 {1, 2, 3, 4, 5, 6, 7, 8} をセットします。関数 foo() は配列 ary の先頭要素を 10 倍します。このとき、配列 ary の内容を破壊的に修正していることに注意してください。foo(d) を呼び出したあと変数 d の値を表示すると、先頭要素が 10 に書き換えられていることがわかります。

この場合、変数 d の値が書き換えられたのではなく、d が参照しているオブジェクトの内容を直接書き換えているだけなのです。元の値をそのままにしておきたい場合は、元の配列をコピーして新しい配列を生成してください。

●問題

double 型の配列に格納されたデータから次の値を求める関数を定義してください。

  1. 合計値を求める関数 sum
  2. 平均値を求める関数 average
  3. 標準偏差を求める関数 sd
  4. 最大値を求める関数 maximum
  5. 最小値を求める関数 minimum

データを \(x_1, x_2, \ldots, x_N\) とすると、総計量 (合計値) \(T\) と平均値 \(M\) は次式で求めることができます。

\(\begin{array}{l} T = x_1 + x_2 + \cdots + x_N = \displaystyle \sum_{i=1}^{N} x_i \\ M = \dfrac{x_1 + x_2 + \cdots + x_N}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} x_i \end{array}\)

平均値が同じ場合でも、データの特徴が異なる場合があります。たとえば、A = {4, 4, 5, 5, 5, 6, 6, 6, 7, 7} と B = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} の平均値は 5.5 になります。A のデータは平均値の近くに集まっていてますが、B のデータはバラバラになっていますね。統計学では、ばらつきの大きさを表すために「分散 (variance)」という値を使います。分散 \(S^2\) の定義を次に示します。

\( S^2 = \dfrac{(x_1 - M)^2 + (x_2 - M)^2 + \cdots + (x_N - M)^2}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} (x_i - M)^2 \)

標準偏差 \(S = \sqrt{S^2}\)

分散の定義からわかるように、平均値から離れたデータが多いほど、分散の値は大きくなります。逆に、平均値に近いデータが多くなると分散は小さな値になります。そして、分散の平方根が「標準偏差 (SD : standard deviation)」になります。

Java の場合、平方根を求めるにはメソッド sqrt() を使います。

Math.sqrt(数値)

sqrt() はパッケージ java.lang のクラス Math に定義されています。このほかにも Math には便利な数学関数が多数用意されています。













●解答

リスト : 解答例

public class Statistic {
  static double height[] = {
    148.7, 149.5, 133.7, 157.9, 154.2, 147.8, 154.6, 159.1, 148.2, 153.1,
    138.2, 138.7, 143.5, 153.2, 150.2, 157.3, 145.1, 157.2, 152.3, 148.3,
    152.0, 146.0, 151.5, 139.4, 158.8, 147.6, 144.0, 145.8, 155.4, 155.5,
    153.6, 138.5, 147.1, 149.6, 160.9, 148.9, 157.5, 155.1, 138.9, 153.0,
    153.9, 150.9, 144.4, 160.3, 153.4, 163.0, 150.9, 153.3, 146.6, 153.3,
    152.3, 153.3, 142.8, 149.0, 149.4, 156.5, 141.7, 146.2, 151.0, 156.5,
    150.8, 141.0, 149.0, 163.2, 144.1, 147.1, 167.9, 155.3, 142.9, 148.7,
    164.8, 154.1, 150.4, 154.2, 161.4, 155.0, 146.8, 154.2, 152.7, 149.7,
    151.5, 154.5, 156.8, 150.3, 143.2, 149.5, 145.6, 140.4, 136.5, 146.9,
    158.9, 144.4, 148.1, 155.5, 152.4, 153.3, 142.3, 155.3, 153.1, 152.3
  };

  static double sum(double[] table) {
    double s = 0.0;
    for (double x: table) s += x;
    return s;
  }

  static double average(double[] table) {
    return sum(table) / table.length;
  }

  static double sd(double[] table) {
    double m = average(table);
    double s = 0.0;
    for (double x: table) s += (x - m) * (x - m);
    return Math.sqrt(s / table.length);
  }

  static double maximum(double[] table) {
    double m = table[0];
    for (int i = 1; i < table.length; i++) {
      if (m < table[i]) m = table[i];
    }
    return m;
  }

  static double minimum(double[] table) {
    double m = table[0];
    for (int i = 1; i < table.length; i++) {
      if (m > table[i]) m = table[i];
    }
    return m;
  }

  public static void main(String[] args) {
    System.out.println("sum = " + sum(height));
    System.out.println("average = " + average(height));
    System.out.println("sd = " + sd(height));
    System.out.println("max = " + maximum(height));
    System.out.println("min = " + minimum(height));
  }
}
$ javac Statistic.java
$ java Statistic
sum = 15062.699999999997
average = 150.62699999999998
sd = 6.433472701426501
max = 167.9
min = 133.7

初版 2009 年 4 月 11 日
改訂 2016 年 11 月 12 日
改訂二版 2021 年 2 月 7 日

Copyright (C) 2009-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Java | NextPage ]