この記事は初心者が理解を深める目的で書いています。
誤りなどありましたらコメントでご指摘頂けますと幸いです。
オブジェクト指向でつまづく
Javaを学ぶ方の多くが、クラス・メソッド・コンストラクタ・thisあたりの観念でつまづいたのではないでしょうか。私もそうです。
よくある「設計図」「実体」などの説明ではよくわからなかったので、自分なりに理解をまとめてみました。
そもそもなぜクラスやメソッドがあるのか
「ゲームをするよ!」と言われて、以下のルールを説明されたとします。
このゲームに本来人数制限はありませんが、今回は1対1の場合に絞ります。
それぞれのプレイヤーは3種類の行動が可能です。
グー:すべての指を折り拳を作る
チョキ:人差し指と中指のみ立て、あとは折る
パー:すべての指を立てる
これらはすべていわゆる三すくみの状態となっており、それぞれ勝てる手、負ける手があります。
etc...
じゃんけんって言ってよ。 そう思いませんでしたか。
それがクラスやメソッドを取り扱うメリットと直結します。
とても大雑把に言うと、 「よく使うものには名前をつけておくと、それを呼ぶだけでどういうことをするのかわかるから、そのあとの説明とか行動がしやすいよね」 というのが、オブジェクト指向のメリットです。
じゃんけんを知らない人にじゃんけんを説明しようと思った場合、いちいちすべて説明しなければいけませんが、知っていれば「じゃんけんしよ」で済むわけです。
具体的なコードを見る
なんとなくオブジェクト指向のメリットがわかったところで、具体的なコードを見ていきます。
public class JankenRule {
//相手が何の手を出したかを格納する変数(文字列)
String opponentHand;
//自分が何の手を出したかを格納する変数(文字列)
String myHand;
//試合の結果を格納する変数(1を自分の勝ち、2を相手の勝ち、3をあいことする)
int matchResult;
//コンストラクタ
//実際の試合結果を出力するにあたっての変数の定義
JankenRule(String opponentHand, String myHand){
this.opponentHand = opponentHand;
this.myHand = myHand;
match();
}
//どの手がどの手に勝つかの情報を格納したメソッド
void match() {
//自分が勝つパターン
if(opponentHand.equals("グー") && myHand.equals("パー")) {
matchResult = 1;
} else if (opponentHand.equals("チョキ") && myHand.equals("グー")) {
matchResult = 1;
} else if (opponentHand.equals("パー") && myHand.equals("チョキ")) {
matchResult = 1;
}
//相手が勝つパターン
if(opponentHand.equals("パー") && myHand.equals("グー")) {
matchResult = 2;
} else if(opponentHand.equals("グー") && myHand.equals("チョキ")) {
matchResult = 2;
} else if (opponentHand.equals("チョキ") && myHand.equals("パー")){
matchResult = 2;
}
//あいこのパターン
if(opponentHand.equals(myHand)) {
matchResult = 3;
}
}
//出された手をもとに勝敗を出力するメソッド
void resultOutput() {
if (matchResult == 1) {
System.out.println("私が勝ちました。");
} else if (matchResult == 2) {
System.out.println("相手が勝ちました。");
} else if (matchResult == 3) {
System.out.println("あいこです。");
}
}
}
public class JankenMatch {
public static void main(String[] args) {
JankenRule examplematch1 = new JankenRule("チョキ", "パー");
examplematch1.resultOutput();
}
}
出力結果:
相手が勝ちました。
長いですね。本当だったらもう少しスタイリッシュに書けると思うのですが、今回はわかりやすさを重視してあえてくどい書き方をしています。
ここから先は、「文字型」「変数」「if文」「条件判定」あたりの項目は理解できている前提で説明します。
また今回は不正な値は与えられない前提とします。
また変数名やメソッド名があまり適切でないことに後から気づきましたがご容赦くださると幸いです。
20204/08/14追記:
intでの直接的な勝敗の管理は避けた方が良い旨コメントでご指摘いただきました。ありがとうございます。
クラスを分けるメリット
今回はJankenRule
とJankenMatch
にクラスを分けました。
(ファイルも分かれているのは、クラスにのっとってファイルも分けるのが慣例というだけですのであまり気にしなくて大丈夫です)
JankenRule
は、 じゃんけんのルールが書いてあるクラス です。
JankenMatch
は、 じゃんけんの実際の試合結果について管理するクラス です。
じゃんけんには当たり前にルールがあります。1個のルールがあるだけで何万回でも何億回でもそのルールでじゃんけんができます。
ただ、じゃんけんをするとき毎回ルール説明が入ったら面倒ですよね。だからこそ、 ルール説明 と 実際にじゃんけんをするときの管理 を分けられるのが、クラスを扱うメリットなわけです。
ただしJankenMatch
は、あくまで実際の試合結果を 管理する クラスです。実際の 試合結果そのもの ではありませんのでご注意を。
JankenRuleを見る
ではこれにのっとって、じゃんけんのルールを定義していきます。
煩雑になるので、今回は1対1のパターンだけを想定します。
メンバ変数(フィールド変数)
public class JankenRule {
//相手が何の手を出したかを格納する変数(文字列)
String opponentHand;
//自分が何の手を出したかを格納する変数(文字列)
String myHand;
//試合の結果を格納する変数(1を自分の勝ち、2を相手の勝ち、3をあいことする)
int matchResult;
まずはこの部分。コメントアウトした通りの定義ですが、「そもそもなんでこれを最初に書くの?」となるかと思います。
クラスを扱う際には、 「このクラスではこういう変数(概念)を扱いますのでよろしくお願いします」 という宣言を最初に書く必要があります。この変数を 「メンバ変数」または「フィールド変数」 といいます。
今回このクラスではじゃんけんのルールを書くわけですから、自分と相手の手と、勝敗について管理できればいいかな。と思ったのでこの3種類となりました。
コンストラクタ・this
//コンストラクタ
//実際の試合結果を出力するにあたっての変数の定義
JankenRule(String opponentHand, String myHand){
this.opponentHand = opponentHand;
this.myHand = myHand;
match();
おそらく最もつまづくポイントの一つ、コンストラクタです。
よく「実体」とか言われるもの(=インスタンス)を作る部分ですね。thisという見慣れない単語もいるので余計混乱しますね。
まず、人間はふわっとした理解でも判断できますが、コンピュータはそうはいきません。
「じゃんけんのルール上で定義されたプレイヤーの手」と、「実際のじゃんけんの試合でプレイヤーが出した手」は、プログラム上では別物として取り扱われます。
当たり前ですが、「じゃんけんでグーはチョキに勝つんだよ」と説明している時の「グー」が、「X月X日に田中さんが私に出したグー」と全く同一なことはないですよね。後者はただの具体例です。
それを踏まえて再度コードを見ていきます。
JankenRule(String opponentHand, String myHand){
まずこの部分です。 ここで引数に指定しているopponentHandとmyHandは、先ほどのメンバ変数とは異なります。
先ほどの メンバ変数はルール上の概念にすぎませんが、この引数に指定している変数は実際に出されたものになります (例えば「田中さんが出したグー」など)。
つまりこの行では、 「今から具体的に田中さんと私が出した手の話をしますよ」という話のその材料(田中さんと私が出した手)を持ってきたこと になります。
this.opponentHand = opponentHand;
this.myHand = myHand;
match();
次にここ。
このthis
は、「今具体的な話をしているそれ」を指定してくれるものです。先ほどの例でいうなら「田中さんや私が出した手」です。
つまり、 「今回具体的に挙げた手(田中さんの出したグーなど)は、このルールにおける「手」という概念として取り扱っていいですよ」 という宣言なわけです。
もしかしたらグー・チョキ・パーなどが具体例で登場したとしても、コンピュータ側は「もしかしたら手遊びをしているだけの可能性もあるからな」と思ってしまうわけです。そうではなくて、 「今回出された手は、じゃんけんの手として出したものなので、これから先で定義するじゃんけんのルールにのっとって管理するのでよろしくお願いします」 という宣言をしているわけです。
なお、match();
は「(この後に定義されている)matchメソッドをこの後実行するよ」というだけなので、あまり深く考えなくてよいです。
余談
「thisを使ってるから同じ変数が二つ並んでるように見えてややこしいんじゃない? 具体的な手に関しては別の関数名をつければややこしくなくなるのでは?」 と思った方いらっしゃいませんか。
引数から持ってくる時点でもう関数名をexampleOpponentHand
にして、exampleOpponentHand = opponentHand;
にしてしまうとか、できそうですよね。
できるかできないかで言えば、できます。ただし今回は特にやるメリットがないです。
単純に変数名が増えると管理するのが大変ですよね。そもそも今回のじゃんけんの例では、ルール上のグーと実際に出されたグーは、そっくりそのままルール上のものか実際のものかの違いしかないわけです。
となると、明示的に変数名を分けたところであまり意味がない。むしろ管理が面倒になるし、自分以外の人がコードを見た時に、「わざわざ変数名を変えているってことはこの二つの変数は役割が全く異なるのか?」とか思わせてしまう原因にもなるわけです。
勝敗の処理
ここまでで準備が整ったので、具体的に勝敗がどのような条件で決まるのか記述していきます。
void match() {
//自分が勝つパターン
if(opponentHand.equals("グー") && myHand.equals("パー")) {
matchResult = 1;
} else if (opponentHand.equals("チョキ") && myHand.equals("グー")) {
matchResult = 1;
} else if (opponentHand.equals("パー") && myHand.equals("チョキ")) {
matchResult = 1;
}
//相手が勝つパターン
if(opponentHand.equals("パー") && myHand.equals("グー")) {
matchResult = 2;
} else if(opponentHand.equals("グー") && myHand.equals("チョキ")) {
matchResult = 2;
} else if (opponentHand.equals("チョキ") && myHand.equals("パー")){
matchResult = 2;
}
//あいこのパターン
if(opponentHand.equals(myHand)) {
matchResult = 3;
}
}
特別なことは特になにもやっていないですね。単純にパターン分けしているだけです。(1,2,3は冒頭のメンバ変数の部分でどの数値がどの結果なのかコメントアウトしてあります)
先ほどコンストラクタを設定したから、特に変な処理を挟まなくてもルールにおける概念上の「相手の手」や「自分の手」に、今回具体的に持ってくる手を当てはめて条件判定ができるわけです。
勝敗を出力する
先ほどまでの段階で勝敗の判定はできました。それを出力していきます。
void resultOutput() {
if (matchResult == 1) {
System.out.println("私が勝ちました。");
} else if (matchResult == 2) {
System.out.println("相手が勝ちました。");
} else if (matchResult == 3) {
System.out.println("あいこです。");
}
}
さっきのメソッドと分けなくても同じことはできます。ただ分けた方が何をしているのかわかりやすいですよね。というだけの話です。
やっとここまででJankenRule
クラスがわかりました。言い換えると「ルールが定義できた」ということになります。
JankenMatchを見る
ルールが設定できたので、実際のじゃんけんの試合をやってみましょう。
mainメソッドを読み込む
public class JankenMatch {
public static void main(String[] args) {
まずこの部分。 Javaではこのmain
メソッドが、どこにあろうが最初に実行されます。
この仕様があることによって、コード上では先にルールを定義していたとしても、「あれ?具体的なケースはどこに行ったの?」とならないわけです(先にmainで具体的なケースを読み込むことは前提なので)。
インスタンスを作る(newする)
JankenRule examplematch1 = new JankenRule("チョキ", "パー");
おなじみ、「newする」です。
「新しくJanekenRule
クラスにのっとって具体的なケース(=examplematch1
)を作るのでよろしくお願いします!具体的には相手がチョキを出して、こちらがパーを出すときです!」 という宣言です(言い換えるならここでインスタンスを作っているということです)。
JanekenRule
におけるコンストラクタの記載の時に、
JankenRule(String opponentHand, String myHand){
こう書いていたので、前者のopponentHand
の位置に記載したチョキが相手の手になり、後者のパーは自分の手になるわけです。
実行したいメソッドを実行する
examplematch1.resultOutput();
「さっき作ったケース(=examplematch1
)あるじゃないですか。JankenRule
の中に、resultOutput
っていうメソッド(=勝敗を出力するメソッド)ありましたよね?あれをexamplematch1
に適用したいので実行してもらえますか?」 という意味です。
つまり、「相手がチョキを出し、自分がパーを出している時、じゃんけんのルール(=JankenRule
)にのっとるとどうなりますか?」と聞いているのと同じなわけですから、
出力結果:
相手が勝ちました。
こうなるわけです。
わかることと書けることは違う
これでやっと説明が終わったわけですが、理解はできても書けるかというのはまた別の話ですよね。
私もこれを書き上げるのにいろいろと調べましたし、合っているかAIに聞きながら進めました。こういう壁打ちが気兼ねなくできるのはやっぱりAIの強いところですね。それでも言語化して実際に書いてみるとやはり理解が進む気がします。
地道にやれることを増やしていきたいです。
なお繰り返しになりますが私も学習中の身ですので、間違いなどございましたらコメントでご指摘くださいますと幸いです。