新人エンジニア研修で知っておきたいstaticメソッドの使い方
なぜ、staticメソッドが存在するのか、その理由
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回は文字と文字列の扱いについて解説しました。クラスやインスタンスという言葉が出てきて、いよいよオブジェクト指向らしくなっていましたね。
今回はstaticメソッドについて解説します。まずはそもそもメソッドとは何かというところからお話を始めたいと思います。
メソッドを一言で説明すると一連の処理に名前をつけたものということができます。例えば、毎朝、①起きて、②顔を洗い、③歯を磨いて、④朝ごはんを食べている、とします。この4つの動作に「朝のルーティーン」という名前をつけて、毎朝、このルーティンを実行するようにすれば人生楽ですね。メソッドはこのように一連の処理に名前をつけて呼び出せるようにしたものです。
1.メソッドとは
プログラムが長くなると全体の見通しが悪くなることがあります。プログラム全体で何をやっているのか分からなくなる訳です。また、あちらこちらで同じコードを書いているときがあります。そうすると無駄に記述量ばかり増えて、かつ、修正が必要になった際にはすべてのコードを修正しなければならなくなります。プログラムには繰り返しがないほうが望ましいのです。このことは、DRY(Don't Repeat Yourself:繰り返しを避けよ)原則として知られています。命令文を分けた方がプログラムを読んだ人にも分かりやすくなり、後々のメンテナンスも容易になります。そのような時にメソッドを使います。
【method】とは、直訳すれば「方法、やり方」といった意味です。メソッドはJava以外の言語では関数と呼ばれることもあります。
直線を表す
y = 2x + 1
という関数のようにxに何かをインプットしたらyがアウトプットされるブラックボックスのようなものが関数だ、と捉えてください。
一番シンプルなメソッドの形は、インプット(引数)、アウトプット(戻り値)ともになく、処理だけをするという、以下のようなものです。
<構文>
void メソッド名() {
命令文;
}
voidは戻り値が“ない”という意味でした。
メソッドの例として、以下、Example01では、10数える(1~10までの整数を表示する)という処理を持ったcount10()というメソッドを作成しています。
package chap08;
public class Example01 {
static void count10() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
public static void main(String[] args) {
count10();
}
}
<実行結果>
1 2 3 4 5 6 7 8 9 10 |
main()メソッドについては、この連載の1回目でお話ししました。プログラムのエントリーポイント、つまり開始点でしたね。
main()メソッドでは、
count10();
とだけ書かれており、ここでメソッドを呼び出しています。以降はメソッドの中に処理が移り、処理が終わるとメインメソッドに処理が戻ります。ですから、count10()メソッドとmain()メソッドの位置関係を逆にして、以下のように書いても同じことです。
package chap08;
public class Example02 {
public static void main(String[] args) {
count10();
}
static void count10() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
なお、一つのクラスに複数のメソッドがあるとき、 main()メソッド はたいてい一番下か一番上に書かれるようです。
これからは、メインメソッドは本の目次のように使います。具体的な処理の内容は個々のメソッドに書いて、メインメソッドには大きな処理の流れしか書かないようにしていきます。
メソッドには、引数と戻り値があるのでした。引数とは、メソッドに渡す値のことです。戻り値とは、その名の通り、メソッドの呼び出し元に戻す値のことです。命令を実行した結果の値のことです。呼び出し元とは上記サンプルプログラムの場合はmain()メソッドのことです。上記の例では、引数は"なし"、戻り値もvoidなので"なし"、というわけです。
デバッガを使ってメソッド呼び出しの動きを確認しておいてください。
では、次に引数のあるメソッドを見てみましょう。
2.引数のあるメソッド
引数のあるメソッドは以下のような形をしています。
<構文>
void メソッド名(型 変数名) {
命令文;
}
()カッコの中に型と変数名を書きます。
この変数名を仮の引数という意味で「仮引数」と呼びます。なぜなら、実際に渡される値はまだ決まっていないからです。このメソッドのブロックの中では、この変数名を使って処理を記述することができるようになります。
以下のExample03では引数のあるメソッドを説明しています。先のサンプルプログラムに手を加えて、毎回10まで数えあげるのではなく、()内で与えられた任意の数まで数えあげることができるようにしました。
package chap08;
public class Example03 {
static void count(int num) {
for (int i = 1; i <= num; i++) {
System.out.print(i + " ");
}
}
public static void main(String[] args) {
count(5);
System.out.println();
count(10);
}
}
<実行結果>
1 2 3 4 5 1 2 3 4 5 6 7 8 9 10 |
ここで、count(5)の5やcount(10)の10という()内で与えられる値は実際の引数という意味で「実引数」と呼びます。仮引数【parameter】と、実引数【argument】という用語は対にして覚えましょう。
3.戻り値のあるメソッド
戻り値のあるメソッドは以下のような形をしています。
<構文>
戻り値の型 メソッド名(仮引数列) {
命令文;
retrun 戻り値;
}
ポイントは、以下の3点です。
- 戻り値の型をメソッド名の前に書きます。
- return文を使って戻り値を戻します。
- 最終的に戻すことができる値は1つだけです。
例として以下のExample04は戻り値のあるメソッドの例です。一辺の長さを与えると正方形の面積を返すメソッドgetAreaOfSquere()を持っています。
※なお、メソッド名でも変数名と同様に単語の連なりを使いたい場合は、上記のように2つ目以降の単語の頭文字を大文字にします。このような命名方法をラクダの形に見立ててキャメルケース(camel case)と言うのでしたね。
良い名前付けは本当に大切です。codic: プログラマーのためのネーミング辞書を是非活用してください。
package chap08;
public class Example04 {
public static void main(String[] args) {
double area = getAreaOfSquere(3.8);
System.out.println(area);
}
static double getAreaOfSquere(double length) {
return length * length;
}
}
<実行結果>
14.44 |
先ほど、「3.最終的に戻すことができる値は1つだけです」と書きました。この意味は、if文などの条件分岐でreturn文は複数あっても、最終的には戻り値は一つに決まるという意味です。
サンプルプログラムで確認してみます。
与えられた引数が偶数かどうかを判定するisEven()というメソッドです。return文が2つありますが、if文によってどちらか一方しか実行されないため問題ありません。
package chap08;
public class Example05 {
public static void main(String[] args) {
System.out.println(isEven(71));
}
static boolean isEven(int num) {
if (num % 2 == 0) {
return true;
} else {
return false;
}
}
}
<実行結果>
false |
なお、参考までに上記のisEven()メソッドは簡略化して以下のように書くこともできます。
static boolean isEven(int num) {
return num % 2 == 0;
}
戻り値のboolean型はtrueかfalseのいずれかの値を持つものであればOKだからです。
さらにreturn文には「打ち切りreturn文」と呼ばれる処理の流れを制御する使われ方があります。
打ち切りreturn文とは、戻り値がvoidであるメソッドの実行中に条件を満たした場合に、その時点でメソッドを終了し、戻り値を返すためのreturn文のことを指します。
例えば、以下のメソッドは、引数として与えられた整数が正の場合に限り、その2倍の値を出力し、それ以外の場合は何も出力せずにメソッドを打ち切ります。以前学んだbreak文と似ていると感じると感じる新人エンジニアの方もいらっしゃるかもしれませんが、break文がブロックを抜けるのに対して、return文はメソッドを抜けます。
public static void printDoublePositiveNumber(int num) {
if (num <= 0) {
return;
} System.out.println(num * 2);
}
4.メソッドのオーバーロード
オーバーロードを使うと同じメソッド名で引数の数や型、並び順が違うメソッドを定義できます。
下図がオーバーロードのイメージです。
ところで、ここまで学んできて、「あれ、変だな?」と思ったことはありませんでしたか?
Javaは型にうるさい言語なのに、、、実引数の型を気にしたことが無かったことに。
例えば、次のExample06はメソッドのオーバーロードの例です。
package chap08;
public class Example06 {
public static void main(String[] args) {
System.out.println("Hello");
System.out.println('A');
System.out.println(256);
System.out.println(3.14);
}
}
おなじみのprintln()メソッドです。渡している実引数の型はそれぞれ、String, char, int, doubleですね。では、println()メソッドの定義はどうなっているのでしょうか?
IDEをお使いの方はコントロールキーを押しながらprintln()メソッドをクリックしてみてください。Javaのソースコードに飛びますが、4つのprintln()メソッドのうちのどれを押すかによって飛び先が違いますね。例えば、doubleの場合は以下のようにPrintSteamクラスの990行目、
Java17標準APIpublic void println(double x) {(以下省略)
という定義に飛びます。
文字列の場合は、同じクラスの1026行目です。
Java17標準APIpublic void println(String x) {(以下省略)
仮引数の型が違うだけですね。
メソッド名が同じで、引数の型や数、並び順が違うメソッドを複数定義することをオーバーロード【overload】といいます。日本語では多重定義と訳されます。オーバーロードの定義では、戻り値の型だけが違っていてもオーバーロードではありません。
public void println(String x)
に対して、
public String println(String x)
はオーバーロードではありません。
メソッドの入り口を広げるのがオーバーロードですからね。
本来、引数の型や数が違えば別のメソッド定義が必要となるわけですが、もし、そのためだけに違ったメソッド名を用意しなければならないとすれば大変です。メソッドを使う側も引数の型や数に気を付けて呼び出す必要が出てきてしまいますね。しかし、オーバーロードによってそのようなプログラマの負担は無くなるのです。
皆さんも今までprintln()メソッドに渡す実引数の型など、気にもしなかったのではないでしょうか?
そしてこの仕組みは皆さんも利用することができます。
サンプルプログラムを見てください。
package chap08;
public class Example07 {
public static void method() {
System.out.println("引数なしのmethodが呼ばれました。");
}
public static void method(int i) {
System.out.println("引数にint型をとるmethodが呼ばれ" + i + "を受け取りました。");
}
public static void method(double d) {
System.out.println("引数にdouble型をとるmethodが呼ばれ" + d + "を受け取りました。");
}
public static void method(String s) {
System.out.println("引数に文字列型をとるmethodが呼ばれ" + s + "を受け取りました。");
}
public static void main(String[] args) {
method();
method(2);
method(3.14);
method("Goodby.");
}
}
<実行結果>
引数なしのmethodが呼ばれました。 引数にint型をとるmethodが呼ばれ2を受け取りました。 引数にdouble型をとるmethodが呼ばれ3.14を受け取りました。 引数に文字列型をとるmethodが呼ばれGoodby.を受け取りました。 |
オーバーロードとは、同じクラスの中でメソッド名が同じで、引数の型や数、並び順が違うメソッドを複数定義することをいいましたね。呼び出し時に指定される引数によって実行されるメソッドが区別されるという仕組みでした。注意点としては、戻り値の型が違っても、それだけではオーバーロードにならないという点です。
①メソッド名、②引数の型、③引数の数の3つの要素をシグネチャと呼びます。
オーバーロードは、①が同じで、②または③が異なる場合と引数の並び順が違う場合に有効なのでした。(下図参照)
なお、シグネチャは署名という意味ですのでシグネチャが全く同じメソッドを複数宣言することはできません。このシグネチャのお話は、また、継承のオーバーライド【override】という話題でも出てきます。ややこしいのですが、オーバーロードとオーバーライド、2文字違いですが全く違う概念ですので気をつけましょう。
5.メソッドのメリット
メソッドを使うメリットは以下の2点です。
- 定形処理の再利用
- メッセージ性を高める
以下、順に説明します。
1.定形処理の再利用
先ほどの「10まで数える処理」を3回、メソッドなしに書くとこうなります。
package chap08;
public class Example08 {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
<実行結果>
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10 |
しかし、メソッドを活用すればExample09のようになります。
package chap08;
public class Example09 {
static void count10() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
public static void main(String[] args) {
count10();
count10();
count10();
}
}
<結果は同じ>
main()メソッド内の処理の流れが見やすいのはどちらかは明らかですね。
また、これまで10まで数えていたものが仕様変更により11まで数えることとなったとすれば、メソッドを利用していなかった場合は3箇所修正しなければならないのに対して、メソッドを利用していた場合は1箇所で済みます。
2.メッセージ性を高める
定型の処理の再利用のほかにもメソッドを使うメリットがあります。それは、メソッドを使うことでメッセージ性が高まるというものです。
例えば、
if (age >= 20){
system.out.println("酒を飲む");
}
というコードよりも以下のようにisAdultというメソッドを使った方が「成人判定をしている」ということがより分かりやすくなります。(ここでは飲酒が可能な年齢を成人としています)
package chap08;
public class Example10 {
public static void main(String[] args) {
int age = 23;
if (isAdult(age)) {
System.out.println("酒を飲む");
}
}
private static boolean isAdult(int age) {
return age >= 20;
}
}
<実行結果>
酒を飲む |
もっともこの例ではメソッドの処理が単純すぎて恩恵を感じられないかもしれません。実際には以下のようにメソッド無しに書くことも可能ですので。
package chap08;
public class Example11 {
public static void main(String[] args) {
int age = 23;
boolean isAdult = age >= 20;
if (isAdult) {
System.out.println("酒を飲む");
}
}
}
<結果は同じ>
ただし、もう少しメソッド内の処理が複雑になると、別処理にして名前をつけることの効果が出てきます。
では、メソッドを使うことのデメリットは何でしょうか?
それは、プログラムを上から順に追えなくなることです。これまでのサンプルプログラムでは、メインメソッドの中を上から順にみていけばプログラムを読み解けました。これをアルゴリズムでは順次と呼んでいましたね。しかし、ここからは処理があちらこちらに飛ぶので慣れないうちは大変だと思います。
※もっとも処理が別クラスに分かれて、クラス間で引数と戻り値をやり取りするようになると、ますます複雑になるのですが。。。
特に次の再帰処理などは初学者が理解困難な部分です。しかし、理解できればメソッドの動きはよく理解できるようになりますので説明してみましょう。
6.メソッドの再帰処理
では、ここで1回目でも見た再帰処理をもう少し実用的な形でやってみましょう。
再帰とは、再び帰ると書きますが、再び自分自身に戻る式のことです。例えば、階乗【factorial】の計算です。
例えば
5!=5*4*3*2*1=120
という計算はみなさんも学生時代に学んだのではないでしょうか?
この計算を再帰を使って解くことを考えます。ただし、話を単純にするために3!にします。
この計算の過程をたどると以下のようになります。
3! = 3 * 2!
2! = 2 * 1!
1! = 1
この計算を下から上にたどると
1! = 1
2! = 2
3! = 6
答えは6になります。
この計算をnを使って一般化すると以下の式になります。
n! = n * (n-1)!
この形は、式の中に自分自身の式がある。つまり、再帰的になっていますね。
以下のサンプルコードを見て下さい。
package chap08;
public class Example12 {
static int factorial(int n) {
if (n <= 1) {
return n;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
int ans = factorial(3);
System.out.println(ans);
}
}
factorial()メソッドの中でfactorial()メソッドを呼んでいます。
ただし、第一回でスタックオーバーフローさせた時と違い今回は、
n <= 1
のとき、1を返して終わりますからスタック・オーバーフローにはなりません。
<実行結果>
6 |
このプログラムを実行しているときのメモリのスタック領域の様子を図示すると下図のようになります。
ちなみに、"stack"とは「積み上げる」という意味で、メソッドはこの領域にロードされるのでしたね。
※上記の図で文字色が薄くなっている部分は実行されないコードです。
①→②→③と3回メソッドが呼ばれていって、「return 1」まで来たら今度は④→⑤→⑥とreturn文で戻っていって答えが求められる様子がイメージできますか?
この再帰処理と相性がいいのがフラクタルです。興味があれば以下のサイトも参照下さい。
https://saycon.co.jp/archives/neta/fractal
7.メソッドチェーン
メソッドの戻り値に対してメソッドをつなげていく書き方を今後、目にすることがあると思います。メソッドチェーン【method chain】といいます。メソッドの鎖という意味ですね。
具体例を見ましょう。
以下のExample13では、文字列"hello"を大文字にしたうえでその先頭から4文字を取り出してコンソール出力しています。
package chap08;
public class Example13 {
public static void main(String[] args) {
System.out.println("hello".toUpperCase().subSequence(0, 4));
}
}
<実行結果>
HELL |
左から順に処理されて、"hello".toUpperCase()によって、戻り値の"HELLO"を得、"HELLO".subSequence(0, 4)によって、戻り値の"HELL"を得ています。"hello"という文字列は実体(インスタンス)なのでメソッドを持っているのですね。(例えば、IDEで編集中にプリミティブの1の後ろに.『ドット』をつけても何も起こりません)
メソッドチェーンは、あまり多用すると読みにくいコードになるので注意が必要ですが、便利な書き方なのでしばしば使われます。その効果としては、使い捨ての変数を用意する必要がなくなり見やすくなること、一連の処理であることを読み取りやすくなることが挙げられます。
メソッドチェーンはJava以外の多くのプログラミング言語においても使用される概念です。例えば、JavaScript, Ruby, Python などの言語でもメソッドチェーンは用いられていますのでこの機会にマスターしておきましょう。
ちなみに、このときの2つの引数、"beginIndex"と"endIndex"の考え方は下図のとおりです。
なお、参考までにtoUpperCase()やtoLowerCase()(小文字にする)というメソッドは、大文字小文字関係なく文字列を扱いたい場合、例えば文字列を大文字・小文字関係なく検索したい場合などに有効なメソッドです。
8.staticキーワードの意味
staticメソッドにはstaticキーワードがついていました。staticは静的という意味でした。つまり、staticメンバは実行時にクラスがロードされた時、一度だけメモリー上にコピーされるのです。また、staticメンバはメモリ上で唯一無二の存在になります。このあとstaticがついていないメソッドも出てくるので、区別しましょう。
以下の図のようにstaticメンバは保存されるメモリ上の領域も今まで出てきたスタック領域やヒープ領域とは違うのです。
一方、インスタンスはプロジェクトの実行中に必要な時に必要なだけヒープ領域に作られるというのが大きな特徴でした。つまり必要な時になって初めてメモリにロードされるのです。これを動的【dynamic】に作ると表現することもあります。静的【static】と対にして理解しておきましょう。
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「staticメソッドで処理を再利用する」方法について見てきました。
次回は、「インスタンスでデータと処理を再利用可能部品にする」です。
いよいよ、オブジェクト指向らしいプログラムになってきます。
クラスを元にインスタンスを作成してそれぞれのインスタンスのメソッドを使う方法について学びましょう。