2016-10-06 第3回目 Java言語の基礎3

本日のテーマ

Java言語の基礎3

メソッド(関数)

基本的な呼び出し方.

C言語で言う関数をJava言語ではメソッド(method)と呼びます. メソッドは必ず何らかの実体に含まれています. そのため,メソッドを呼び出すときは,どの実体に対して呼び出すのかを指定しなければいけません. 例えば,クラス定義の基本形 に示した次のコードを見てみます.

public static void main(String[] args){
    GivenClassName application = new GivenClassName();
    application.run();
}

このコードの3行目は,application.run() とメソッドを呼び出しています. これは,application という実体に対して,run というメソッドを呼び出していることになります. このように,必ず,何らかの実体を経由しなければメソッドは呼び出せません.

例題1 定義済みメソッドの呼び出し

コマンドライン引数で与えられた文字列を全て大文字に変換するプログラム,ToUpper を書いてみましょう. 以下のように出力されます.

$ java ToUpper tamada
TAMADA
$ java ToUpper sagisaka Java KSU_cse_AP
SAGISAKA
JAVA
KSU_CSE_AP

String 型の値を大文字に変更するには,toUpperCase メソッドを String 型変数に対して呼び出します. すると,toUpperCaseの返り値として,全てが大文字に変更された文字列が返ってきます. なお,String型の実体は不変(immutable)です.内容の変更はできませんので,元の変数の内容は変わりません.

参考コード

以下のコード片と ArgsPrinterが参考になるでしょう.

String original;
original = "tamada";
String upper;
upper = original.toUpperCase(); 
// => original に代入されている文字が全て大文字になって,upperに代入される.

このプログラムは,String型変数であるoriginal に対して,toUpper を呼び出し, その返り値をupper というString型変数に代入しています. Javaのあらゆるメソッドはこのように,何らかのものに対して呼び出します.

動作イメージを次の画像で確認してください.クリックするたびに画像が更新されます.

メソッドの定義方法

メソッドを定義する方法は,基本的にはC言語と同じです.異なる点は,Java言語の場合, メソッド宣言は必ずクラス宣言の内側でなければならない,という点です.

メソッドの典型的な宣言は次の通りです.

// クラス宣言
public class Xxxxx{
    // ReturnType, ArgumentType は適切な型に読み替えてください.
    ReturnType methodName(ArgumentType argumentName, ...){
    }
}

methodName はメソッド名を表しています.この部分はある程度好きな名前を付けられます. 処理の内容が後から見てわかるように付けましょう.ローマ字でも構いません.

また,ReturnTypeは返り値の型を表しており,適切な型に読み替えてください. 例えば,そのメソッドが返り値を持つ場合は,StringInteger など適切な型を宣言しましょう. そのメソッドが何も値を返さない場合は,voidとしてください.省略はできません.

ArgumentType も適切な型に読み替えてください. C言語の場合,引数に何も受け取らない場合,voidと書けましたが,Java言語では書けません. 引数に何も受け取らない場合は,methodName() と書いてください.

なお,この ReturnType methodName(ArgumentType argumentName, ...) の部分をメソッドのシグネチャと呼びます. このメソッドの{から} の間をメソッドのボディと呼びます. そして,メソッドのシグネチャとメソッドのボディを書くことをメソッドを定義する,もしくはメソッドを宣言すると言います.

メソッドは細かく分けましょう. 目安として,メソッド内の処理が5行以上になれば,別のメソッドに分けることを考えましょう.

引数, 返り値のないメソッド

引数も帰り値もないメソッドは,フィールドの値を利用したり, フィールドに値を作成したり,常に同じ値を返すような場合に用いられます.

例えば,次のコードは呼び出された時点の日付,時刻で,フィールドのcurrentDateの値を更新します.

import java.util.Date;
public class DateSample{
    Date currentDate = new Date();
    // ... today 以外のメソッドは省略.
    void updateCurrentDate(){
        this.currentDate = new Date();
    }
}

引数あり, 返り値のないメソッド

引数の値を用いて,何らかの処理を行うメソッドの場合に用いられます. 処理の結果は,System.out.printで出力されたり,フィールドに代入することが多いです.

引数なし, 返り値のあるメソッド

String型の文字列を全て大文字にする toUpperCase や全て小文字にする toLowerCase などのように, フィールドの値を外に返すために用いられることが多いです.

例えば,次のメソッドは常に,呼び出し時点の時刻を返します.

import java.util.Date;
public class DateSample{
    // ... today 以外のメソッドは省略.
    Date today(){
        return new Date();
    }
}

引数, 返り値のあるメソッド

値を引数として受け取り,何らかの加工を行った結果を返す場合が多いです.

例題2

前回の練習問題1. HelloWorld改をメソッドを使って書き換えてください. クラス名は,HelloWorld2r としてください.

コマンドライン引数で与えられた人に,挨拶しましょう. もし,誰も指定されない場合は, "Hello, World"と出力してください.

public class HelloWorld2r{
    void run(String[] args){
        if(/* 条件は前回の練習問題と同じ */){
            this.greet(/* 引数に何を渡すか考えてください. */);
        }
        else{
            this.greet(/* 引数に何を渡すか考えてください. */);
        }
    }
    void greet(/* 引数となる変数を宣言してください */){
        System.out.printf("Hello, %s%n", /* ここに何が来るでしょうか. */);
    }
}

なお,runメソッドが実行している実体と同じ実体に対してメソッドを呼び出すときは, 上記のように,thisというキーワードに対してメソッドを呼び出します.

例題3

前回の練習問題3. 階乗をメソッドを使って書き換えてください.

public class Factorial2{
    void run(String[] args){
        Integer number = new Integer(args[0]);
        Integer factorial = this.factorial(/* 引数に何を渡すか考えてください */);
        // number の階乗を計算する.
        System.out.printf("%d! = %d%n", number, factorial);
    }
    // ここに factorialメソッドを定義してください.
}

メソッドの仮引数と実引数

メソッドの引数を理解するために,仮引数と実引数の2つの概念があることを区別しましょう. 仮引数(parameter)とは,メソッド定義時に使用される引数のことを指し, 実引数(argument)は,メソッド呼び出し時に用いる引数のことである.

次の例で考えてみましょう.

public class LeapYear{
    void run(String[] argsOfRun){
        for(Integer i = 0; i < argsOfRun.length; i++){
            Integer year = new Integer(argsOfRun[i]);
            Boolean leapYearFlag = this.isLeapYear(year);
            this.printLeapYear(year, leapYearFlag);
        }
    }
    Boolean isLeapYear(Integer y){
        return y % 400 == 0 && (y % 100 != 0 || y % 400 == 0);
    }
    void printLeapYear(Integer y2, Boolean lyf){
        if(lyf)
            System.out.printf("%d年はうるう年です.%n", y2);
        else
            System.out.printf("%d年はうるう年ではありません.%n", y2);
    }
}

このプログラムには,実引数,仮引数がそれぞれ存在します. 仮引数は,2行目 runメソッド宣言時の String型配列のargsOfRunと 9行目のisLeapYearメソッドのInteger型のy,そして,12行目の printLeapYearメソッドの y2lyfです.

実引数は,5行目の this.isLeapYearメソッドに渡している変数year, そして,6行目のthis.printLeapYearに渡している yearと,leapYearFlagです.

仮引数は変数を定義する場所,実引数は,既に宣言された変数を利用する場所であると区別しましょう.

メソッド内での引数の更新

次のプログラムを考えてみましょう.updateDate1updateDate2updateDate3の3つのメソッドそれぞれで 年に1を追加しているように見えます. 動作結果を考えて実行してみましょう. また,なぜこのような結果になるのかを考えてみましょう.

public class DateConfusion{
    void run(){
        Date date;
        date = new Date();
        this.updateDate1(date);
        System.out.println(date);

        date = new Date();
        this.updateDate2(date);
        System.out.println(date);

        date = new Date();
        Date date2 = this.updateDate3(date);
        System.out.println(date);
        System.out.println(date2);
    }
    void updateDate1(Date d){
        d.setYear(d.getYear() + 1);
    }
    void updateDate2(Date d){
        d = new Date(d.getYear() + 1, d.getMonth(), d.getDay());
    }
    Date updateDate3(Date d){
        // dの時刻と同じ時刻の Date 型の実体が作成される.
        Date d2 = new Date(d.getYear() + 1, d.getMonth(), d.getDay());
        return d2;
    }
    // mainメソッドは省略.
}

メソッドの中で参照先が変わったとしてもメソッドの呼び出し元では全く影響を受けません. 一方で,メソッドの中で参照先の実体の状態を変更すると,呼び出し元にも影響を受けます.= による代入は参照先の変更であることをしっかりと理解しましょう.

変数のスコープ

変数はスコープ(scope)を持っています.スコープとは変数の有効範囲のことです. 有効範囲の外からその変数の参照,代入は行えません. スコープは変数が宣言されて以降,一番内側の閉じ括弧までです.

以下のプログラム(メソッドの仮引数と実引数の再掲)のrunメソッド内の変数のスコープを確認してみましょう.

まず,実引数である argsOfRun のスコープを確認しましょう. 次に,ループ制御変数であるi,ループ内で宣言された Integer型の year, 最後に,Boolean型のleapYearFlagのスコープを確認してください. 図をクリックすると,それぞれの有効範囲が出てきます.

このように,変数の有効範囲は基本的には,{から}の範囲内(これをブロックと呼びます)で,宣言されて以降ということがわかると思います. 変数の有効範囲はできるだけ短く,というのが定石です. 有効範囲が広いと,どこからでもアクセスできて,どこで更新されているのかがわからなくなるので,避けるべき,ということです.

ページのトップに戻る

練習問題

1. 与えられた文字列のソート

以下の条件を満たすように,コマンドライン引数 で与えられた複数の文字列をソートして出力するプログラムを作成してください (ソートアルゴリズムを自分で書く必要はありません).

ソートは Arraysに対して,sortメソッドを呼び出すと渡した配列の要素がソートされます. ただし,Arraysを利用する場合は,import java.util.Arrays;という一文が クラス宣言の前に必要です.

public class ArgsSorter{
    void run(String[] args){
        // ここで,printArray を呼び出し,"before"の一行を出力する.
        // argsの内容をソートするため,Arrays.sortメソッドを呼び出す.
        Arrays.sort(args); // <= args がソート済みになる.
        // ここで,printArray を呼び出し,"after"の一行を出力する.
    }
    // printArrayメソッドをここに書く.
    // mainメソッドは省略.
}

実行例

$ java ArgsSorter one two three four five six
before: one, two, three, four, five, six, 
after: five, four, one, six, three, two, 
$ java ArgsSorter time flies like an arrow
before: time, flies, like, an, arrow, 
after: an, arrow, flies, like, time, 
$ java ArgsSorter 2016 10 6
before: 2016, 10, 6, 
after: 10, 2016, 6, 

このように,アルファベット順にソートされていることが確認できます. 最後の例は,間違いではありません."2016""10""6"という文字列でソートしている,つまり, 1桁目の文字("2", "1", "6")でソートしているので,この順で正しいです.

2. 学生証番号の正当性を検証するプログラム

本学の学生証番号(6桁)は,各桁を足し合わせると,10の倍数となるようになっています. コマンドライン引数で与えられた学生証番号が正しいものであるかを判定するプログラムStudentIdValidatorを作成してください. 正しい学生証番号であれば,与えられた学生証番号と共に,valid,正しくない学生証番号であれば,invalid, 与えられた文字列が6桁の学生証番号でなければ,not student idと出力してください.

それぞれのメソッドの処理は次の通りです。 この処理通りにしなければいけないわけではありませんが、 このような処理にすると各メソッドの役割がわかりやすいと思います。

ヒント

文字列の長さを取得するには,String型の変数に対して,length()メソッドを呼び出してください.

実行例

$ java StudentIdValidator 024509
024509: valid
$ java StudentIdValidator 123456
123456: invalid
$ java StudentIdValidator 123455 123456
123455: valid
123456: invalid
$ java StudentIdValidator 123509 244409 1024509
123509: valid
244409: invalid
1024509: not student id

3. 二次方程式の解

2次方程式の解を解の公式を用いて求めるプログラム作成してください. その際,実数解,重解,虚数解を区別して出力するようにしましょう. ただし,以下の条件を満たしてください.

なお,解の公式は$\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$,判別式$D=b^2 -4ac$が正の場合は実数解,0の場合は,重解,負の場合は虚数解です.

平方根

平方根は,次のコードで求められます.

Integer two = 2;
Double sqrtOfTwo = Math.sqrt(two); // => 1.41421356...

実行例

$ java QuadraticEquation 1 -4 4
answer = 2.000000
$ java QuadraticEquation 1 -4 8
answer = 2.000000 + 2.000000 i, 2.000000 - 2.000000 i
$ java QuadraticEquation 1 0 -4
answer = 2.00000, -2.00000

4. モンテカルロ法による$\pi$の計算

モンテカルロ法により,$\pi$を計算しましょう. モンテカルロ法とは,乱数を用いて統計計算を行う方法です. ここでは,$\pi$を求めます.次のアルゴリズムに従って実装してください.

  1. 0.0〜1.0の乱数を2つ取得し,$x, y$とします.
  2. 原点と$(x, y)$の距離を計算します.距離は,$\sqrt{x^2+y^2}$で求められます.
  3. 計算した距離が1よりも小さい場合,ヒットしたとみなします
    • 距離が1以下の場合,点は円の内側に存在します(ヒットした).
    • 距離が1よりも大きな場合,点は円の外側に存在します.
  4. 1〜3を規定回数繰り返します.
  5. ヒットした回数の割合を計算します.
  6. この割合が$\frac{\pi}{4}$になるので,4を掛け$\pi$を計算します.

下図のように半径1の円の第1象限に注目します. 0.0から1.0の乱数を2つ取得して,$x, y$とします. 下図の点が得られた乱数値から求められた座標とします.

求められた座標と原点との距離を計算します.距離は,$\sqrt{x^2+y^2}$で求められます. この距離が1よりも小さい場合,円の内側に存在するため,ヒットしたものとします(1回目のクリックの時の画像).

一方,円の外側にある場合は,ヒットしないものとします(次のクリックの時の画像). これを規定回数(デフォルトは1000回とします.)繰り返してください. このとき,ヒットした確率は $\frac{\pi}{4}$に近付いていくため, ヒットした確率に 4をかけると$\pi$の近似値が得られます.

クラス名は,MonteCarloPiとします. 引数に値が指定された場合,その値だけ繰り返してください. 値が何も指定されない場合は,1,000回の繰り返しとします.

実行例

計算結果は必ずしもこの通りではありません.

$ java MonteCarloPi
pi = 3.10800
$ java MonteCarloPi
pi = 3.17200
$ java MonteCarloPi 10000
pi = 3.13916
$ java MonteCarloPi 100000
pi = 3.14776
$ java MonteCarloPi 1000000
pi = 3.14127
$ java MonteCarloPi 10000000
pi = 3.14229

5. 台形公式による積分計算を利用した$\pi$の計算

台形公式を用いて$\pi$を計算してください. まず,半径1の円を表す式$x^2+y^2=1$を考えましょう. 式を変形すると,$y=\sqrt{1−x^2}$となります. これの$0\leq x \leq 1$の範囲を考えます.

下図のように,この式で描かれるグラフに内接するいくつかの台形を考えます. 下図を1度クリックすると,2つの台形(1つは三角形のように見えますが,上底(右側の高さ)が0に近い台形と考えてください)があり, 横幅は$x_i−x_{i−1}$,高さは$h_{i−1}, h_i$です($h_{i−1}\ge h_i$). このとき,一つの台形の面積は,$\frac{(x_i−x_{i−1})\times(h_{i−1}+h_i)}{2}$です. 高さを求めるには,その時の$y=\sqrt{1−x^2}$にその時の$x_i$値を代入することで求められます.

これをすべての台形について求めます.そして,求めたすべての台形の面積を足し合わせると, $\frac{\pi}{4}$の近似値が得られます. 得られた値に4を掛けると$\pi$が得られます. なお,すべての$i$において,横幅の間隔 ($x_i−x_{i−1}$) は同じ値(等間隔)です.

さらに,横幅により狭い値を指定することで,値を$\pi$に近づけられるようになります. さて,この方法で,$\pi$を求めるプログラムを作成しましょう. クラス名は, TrapezoidalRulePiとします. 作成する際に,コマンドライン引数で台形の横幅を指定できるようにしてください. もし,コマンドライン引数で値が指定されなければ,0.001が指定されたものとしてください.

実行例

計算結果は必ずしもこの通りでなくても構いません.

$ java TrapezoidalRulePi 0.01
pi = 3.1375956845831103
$ java TrapezoidalRulePi 
pi = 3.141591477698228
$ java TrapezoidalRulePi 0.001
pi = 3.1414660465553967
$ java TrapezoidalRulePi 0.0000001
pi = 3.141592654152668

ページのトップに戻る

まとめ