メソッド(関数)

基本的な呼び出し方.

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 tamada Java KSU_cse_AP
tamada -> TAMADA
Java -> JAVA
KSU_cse_AP -> KSU_CSE_AP

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

参考コード

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

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

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

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

例題1の解答例

メソッドの定義方法

メソッドを定義する方法は,基本的には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というキーワードに対してメソッドを呼び出します.詳細は暗黙の定数thisで説明しています.

例題2の解答例

例題3

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

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

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

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

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

public class LeapYear{
  void run(String[] argsForRun){
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[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型配列のargsForRunと 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.getDate());
    }
    Date updateDate3(Date d){
        // dの時刻と同じ時刻の Date 型の実体が作成される.
        Date d2 = new Date(d.getYear() + 1, d.getMonth(),
                           d.getDate());
        return d2;
    }
    // mainメソッドは省略.
}

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

変数のスコープ

変数はスコープ(scope)を持っています.スコープとは変数の有効範囲のことです. 有効範囲の外からその変数の参照,代入は行えません. プログラム中の {} で囲まれた範囲をブロックと呼びます. スコープは変数が宣言されて以降,宣言されたブロック内でのみ有効です.

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

まず,runメソッドの仮引数である argsForRun のスコープを確認しましょう. 仮引数はそのメソッド内で有効で,メソッドの外側では例え同じ名前であっても違う変数として扱われます. 次に,ループ制御変数であるi,ループ内で宣言された Integer型の year, 最後に,Boolean型のleapYearFlagのスコープを確認してください. 引き続き,isLeapYearメソッドの仮引数であるyprintLearpYearメソッドの仮引数y2lyfのスコープを確認してください. 図をクリックすると,それぞれの有効範囲が出てきます.

このように,変数の有効範囲は基本的には,{から}の範囲内で,宣言されて以降ということがわかると思います. 変数の有効範囲はできるだけ短く,というのが定石です. 有効範囲が広いと,どこからでもアクセスできて,どこで更新されているのかがわからなくなるので,避けるべき,ということです.Q&A変数のスコープが短い方が良いのはなぜですか?も参考にしてください.

暗黙の定数this

各メソッドでは,暗黙的に宣言されたthisという変数が利用できます. thisは,その型の実体が代入されています. 例えば,次の例で考えてみましょう(メソッドの仮引数と実引数の再掲).

public class LeapYear{
  void run(String[] argsForRun){
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[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);
  }
  public static void main(String[] args){
    LeapYear ly = new LeapYear();
    ly.run(args);
  }
}

mainメソッドでLearpYearをnewして実体を作成し,lyという変数に代入しています. そして,lyrunメソッドを20行目で呼び出しています(ly.run(args);の行). この呼び出しにより,runメソッド内に処理が移ります. このrunの中でLeapYearの実体を参照する変数がthisです.

5行目と6行目で this が使われており,this.isLearpYear(year)でからisLeapYearメソッドを, this.printLeapYear(year, leapYearFlag)printLeapYearメソッドをそれぞれ呼び出しています.

なお,thisに値を代入しようとするとコンパイルエラーが発生します.

  void run(String[] argsForRun){
    LeapYear ly = new LeapYear();
    for(Integer i = 0; i < argsForRun.length; i++){
      Integer year = Integer.valueOf(argsForRun[i]);
      Boolean leapYearFlag = ly.isLeapYear(year);
      ly.printLeapYear(year, leapYearFlag);
    }
  }

例えば,runを上のようにし,thisの代わりにly経由でisLeapYearなどを呼び出したとしても,同じ結果を得られます. ただし,lythisは異なる実体です(ly != this). このようなとき,lyを改めて作成し直す必要はありません.

mainメソッドのlyrunメソッド内のthisは同じ実体ですが, mainメソッドとrunメソッドのlyは異なる実体であることに注意しましょう.