Java講座: オブジェクト指向構文(お気持ち編)
Javaを学ぶ上で避けては通れない概念がオブジェクト指向です。
インターネットで「オブジェクト指向とは」とか検索すると、だいたいこんな感じの結果がヒットします。
「ある役割を持ったモノ(オブジェクト)」ごとにクラスを分割し、モノ同士の関係性を定義していくことでシステムを構築していこうとする考え方
何言ってるのかわかりませんね。
筆者の主観と独断では、オブジェクト指向は「モノごとにファイルやクラスを分割して役割分担すると開発がしやすくなるよね~」という考え方をもとに作り出された手法だと思っています。つまり、オブジェクト指向が無くてもプログラム自体は作れてしまうというわけですね。
これまで皆さんが書いてきた・見てきたプログラムはせいぜい数十行程度のものが多かったと思います。しかし、「全部で数千行~数万行近く書かなきゃいけないシステムを開発しなきゃいけない」となったらどうしましょう?すべてを1つのクラスに書いていたら⋯あまり考えたくはないですね。
そこで、モノごとにクラスを分割して開発ができたら読みやすいし、エラーが発生したときに原因の特定が容易になります。これがオブジェクト指向の強みです。あくまで開発をしやすくするための考え方だと思っておくといい感じじゃないでしょうか。
オブジェクト指向は初学者にはなかなか難しい概念です。筆者もちゃんとオブジェクト指向を理解できているかと聞かれたらうーん⋯という感じです。
クラスで管理するべき「オブジェクト」とは?
オブジェクト指向の考え方では、いろんなものが「オブジェクト」として捉えられます。「車」だってオブジェクトだし、「人間」だってオブジェクトだし、「ゲームのボタン」だってオブジェクトだし、なんなら「ゲームのユーザ」という概念もオブジェクトです。とにかくオブジェクト指向では、「プログラムを作るために必要な『モノ』をすべてクラスで分割して管理する」ことを徹底します。
といっても、初心者のうちは何がクラスで管理するべきオブジェクトなのかわからなくなりがちです。なんなら「わざわざクラスで管理しなくても、全部mainメソッド内に書いちゃえばよくね?」と思っている人もいるかも知れません。まあ必ずしもその考えは間違いではありませんが、初心者こそオブジェクト指向で書くべきだというのが筆者の思想です。
では、具体的にクラスで管理するべきオブジェクトはどうやって判別すれば良いのでしょうか?結論から言ってしまうと、以下の条件のどちらかを満たすものはオブジェクトと言えます。つまり独立したクラスで管理すべきです。
- 複数のパラメータを持つこと
- 複数の機能(振る舞い)を持つこと
これらをすべてひとまとめにしてクラスで管理しちゃおうという開発手法がオブジェクト指向です。
例えば、以前作ったシンプルなポケモンゲームにも色々オブジェクトがあります。
まず一番わかりやすいのは「ポケモン」ですね。わかりやすくパラメータ(名前とかHPとか)と機能(「攻撃する」とか)をもっています。が、他にも色々あります。
実は、「ポケモンのわざ」や「ポケモンのタイプ」なども別々のオブジェクトとして管理するべきです。
タイプに関しては特に機能がないので少し特殊ですが、いろんなクラスから共通して参照されるデータは、それ専用のファイルを作成したほうがバグらせにくくなります。タイプって「ポケモン」からも参照されますし、「わざ」からも参照されますよね?そういうことです。
以前実装しようとしてくれた人もいるかも知れませんが、モンスターボールなどの「ボール」や、きずぐすりなどの「アイテム」や、ポケモンの「もちもの」だって全部オブジェクトです。
なんならこれはゲームなわけですから、「ゲームの進行役」という概念的なものですらオブジェクトと捉えられます。
どうやってプログラムとして書けば良い?
ここまでで「何をオブジェクト(クラス)として分けるべきか」はなんとなくイメージできたかと思います。では、いざそのオブジェクトをクラスとしてファイルに書くとき、具体的に何を記述すればいいのでしょうか?
結論から言うと、クラスにはフィールドとメソッドの2つを書きます。フィールドと聞くと聞き馴染みがないかもしれませんが、変数のことです。まとめると以下のようになります。
- フィールド: そのオブジェクトがもつパラメータのこと。変数を使って定義する。
- メソッド: そのオブジェクトがもつ機能のこと。メソッドを使って定義する。
以上を踏まえると、クラスはパラメータと機能をひとまとめにした「設計図」 ということができます。こうやってまとめるとクラスは案外簡単な概念だとわかります。要は「変数とメソッドを書く場所」に名前を無理やりつけてるようなものですからね。
具体例として、「犬」をオブジェクト指向的に解釈したDogクラスを書いてみます。
犬にはこんなパラメータがあるとします。
- 名前
- 年齢
さらに、犬にはこんな振る舞い・機能があるとします
- 吠える
- 噛みつく
- ステータスを表示する(現実世界ではありえないことですが、プログラムではステータス表示などの機能はよく実装されます)
これをDogクラスとしてJavaで書くと以下のようになります。(ファイル名はDog.javaにしてください)
public class Dog {
String name;
int age;
public void bark() {
System.out.println(name + "「ワン!」");
}
public void bite() {
System.out.println("噛みつきました。");
}
public void showStatus() {
System.out.println("名前: " + name);
System.out.println("年齢: " + age);
}
}インスタンス化とコンストラクタ
インスタンス化の前に、Javaの実行コマンドの仕組みについて説明します。
みなさんがJavaプログラムを実行するときには、java 〇〇みたいなコマンドを使いますよね?あれは実は「〇〇というJavaファイルにあるmainメソッドを実行する」というコマンドです。つまり、mainメソッドがないファイルは実行することができないわけです。
それを踏まえてDogクラスを見ると、どこにもmainメソッドなんて書かれていません。つまりこのままではこのDogクラスに書かれた内容はどこからも実行することができないわけです。実はこれはわざとこのようにしています。なぜならDogクラスはあくまで「犬の設計図」であり、設計図にそれ以上の機能を持たせるのは好ましい手法ではないからです。
インスタンス化
このクラスは設計図であるため、実際に動かすのは設計図自身ではなく設計図から作り出されたモノであるべきです。このように設計図からモノを作り出す作業をインスタンス化と呼び、作り出されたモノのことをインスタンスと呼びます。
Javaでは、インスタンス化を行うためにnewという演算子を使用します。ScannerやArrayListなどを初期化するときにも使いましたね。あれもやっていることはインスタンス化だったわけです。
インスタンスがもつフィールドやメソッドにアクセスしたい場合はインスタンス名.フィールド名やインスタンス名.メソッド名()の構文でアクセスできます。
文章で書いてもわかりにくいため、実際にプログラムを実行してみましょう。Dog.javaと同じディレクトリにMain.javaを作成し、以下の内容を書いてください。
public class Main {
public static void main(String[] args) {
Dog pochi = new Dog();
pochi.name = "ポチ";
pochi.age = 3;
Dog hachi = new Dog();
hachi.name = "ハチ";
hachi.age = 5;
pochi.bark();
hachi.bark();
}
}プログラムの実行方法ですが、コンパイルはDog.javaとMain.javaの両方行ってください。ただし、実行するのはmainメソッドがあるMain.javaだけです。ディレクトリ内の全てのJavaファイルを一斉にコンパイルする方法として、以下のようなコマンドがあります。
javac *.javaコンストラクタ
先程のMain.javaでは、インスタンスの持つフィールドを毎回pochi.nameとか書いて設定していました。これは正直面倒です。パラメータが2つくらいならまだしも、もっとパラメータが増えたらどうでしょう。もし「名前を設定し忘れる」みたいなミスをしてしまったら名無しの犬が爆誕してしまい、後々システム全体に重大なエラーを引き起こす可能性だってあります。
これを解決する方法がコンストラクタです。コンストラクタとは、「インスタンス化と同時にフィールドの初期化を行う手法」のことです。コンストラクタは設計図になるクラスに実装します。今回の場合はDog.javaですね。実際にDog.javaにコンストラクタを追加しましょう。
public class Dog {
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println(name + "「ワン!」");
}
public void bite() {
System.out.println("噛みつきました。");
}
public void showStatus() {
System.out.println("名前: " + name);
System.out.println("年齢: " + age);
}
}コンストラクタの書き方はメソッドに似ていますが、以下のような特徴があります。
- 名前はクラス名と同じにする
- 返り値はもたない
- 返り値の型を書かない
コンストラクタを作成したことで、インスタンス化する際に必ず名前と年齢を入力しなければいけなくなりました。したがって、Main.javaも書き換える必要があります。
public class Main {
public static void main(String[] args) {
Dog pochi = new Dog("ポチ", 3);
Dog hachi = new Dog("ハチ", 5);
pochi.bark();
hachi.bark();
pochi.showStatus();
hachi.showStatus();
}
}pochi.name = ... と何行も書いていたコードが、たった1行にスッキリ収まりました。コードが読みやすくなるだけでなく、「設定し忘れ」というヒューマンエラーをプログラムを動かす前に防ぐことができるのが、コンストラクタ最大のメリットです。これから設計図となるクラスを作成する際にパラメータを初期化したいと思った場合には、コンストラクタを積極的に使用していきましょう。
カプセル化とprivate修飾子
ここまでで、クラスという設計図を作り、コンストラクタによってインスタンス化ができるようになりました。が、実は今のDogクラスにはまだ重大な弱点が残っています。それは、外部のクラスからフィールドを好きにいじれてしまうというものです。
例えば、Main.javaをこのように書いたらどうなるでしょう?
public class Main {
public static void main(String[] args) {
Dog pochi = new Dog("ポチ", 3);
Dog hachi = new Dog("ハチ", 5);
pochi.age = -500;
pochi.name = "ゴリラ";
pochi.bark();
hachi.bark();
pochi.showStatus();
hachi.showStatus();
}
}せっかくコンストラクタで正しく初期化したのに、後から勝手にフィールドをいじってしまったせいでポチが-500歳のゴリラになってしまいました。このように、「誰でも、どこからでも、自由にデータを書き換えられる状態」にしておくと、いつの間にかデータがおかしくなっていて、後々原因不明のエラーに悩まされることになります。
これを防ぐのがカプセル化です。カプセル化とは、「見せたくない・勝手にいじられたくないフィールドを他のクラスから隠してしまう」という概念です。オブジェクト指向の3大要素の1つでもあります。
Javaにおいてカプセル化を実現するためには、private修飾子を用います。使い方は至ってシンプルです。フィールドの宣言時にprivateとつけるだけです。実際にDog.javaを書き換えてみましょう。
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
public void bark() {
System.out.println(name + "「ワン!」");
}
public void showStatus() {
System.out.println("名前: " + name);
System.out.println("年齢: " + age);
}
}private修飾子は「自分自身のクラス内でしかその要素にアクセスできない」という意味を持ちます。今nameとageにprivateを付与したため、これらのフィールドにはDogクラス自身からしかアクセスできなくなりました。
オブジェクト指向の考え方では、一部を除き基本的にクラス内のすべてのフィールド(変数)はprivateとするべきです。
ゲッター(getter)
ここで困ったことが起きます。privateによってカプセル化を行ったことで、「データの書き換え」だけでなく「現在のデータを読み取ること(確認すること)」すらできなくなってしまいました。例えば、「ポチの現在の年齢を使って、別の計算をしたい」と思っても、pochi.ageにアクセスできないので値を取り出せません。これはかなり困ります。
そこで、「書き換えは禁止するけど、見るだけならOK」というメソッドを用意してあげます。この「値を取得するためだけのメソッド」のことを、ゲッター(Getter)と呼びます。
ゲッターを書くための暗黙のルールがあります。
- メソッド名は
get+フィールド名(先頭は大文字)にする。(nameならgetName) - 引数は無し。フィールドの値をそのまま
returnする。
Dog.javaにゲッターを追加してみましょう。
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
// nameを取得するためのゲッター
public String getName() {
return name;
}
// ageを取得するためのゲッター
public int getAge() {
return age;
}
}実際にゲッターによってフィールドの値の取得ができているか確認しましょう。Main.javaを書き換えてみてください。
public class Main {
public static void main(String[] args) {
Dog pochi = new Dog("ポチ", 3);
System.out.println("この犬の名前は " + pochi.getName() + " です。");
System.out.println("年齢は " + pochi.getAge() + " 歳です。");
}
}「わざわざprivateにしてゲッターを作るなんて面倒くさい!」と思うかもしれません。筆者も最初はそう思いました。が、いつかこのカプセル化による安心感を実感できる時が来るはずです。初心者こそ、「フィールドはとりあえず全部 privateにして、必要に応じてゲッターを作る」というクセをつけておくことを強くおすすめします。