Javaのabstractとは?抽象クラスの基礎とabstractの使い方
ここでは、Javaにおけるabstractについて解説していきます。継承は理解できたけど、抽象クラスについてはイマイチ理解できない・・・という人に向けた内容となっています。
英単語としてのabstractは日本語では「抽象的、あいまい」といった意味ですが、Javaにおいてもabstractがついたクラスやメソッドをそれぞれ「抽象クラス」、「抽象メソッド」といいます。
ちなみにみなさんがこれまで(abstractを知る以前)にJavaを学ぶ中で目にしてきた、あるいは実際にコーディングしてきたクラスやメソッドは「具象クラス」、「具象メソッド」といいます。
「具象(具体的)」に対する「抽象」というわけです。これだけではなんのことやらさっぱり意味がわかりませんね。それでは、abstractについて具体的に見ていきましょう。
なお、ソースコードは見やすさ優先で余計な記述は省いていますので、特に注釈がないかぎりは同一パッケージに属しているものとして読み進めてください。
目次
1.Javaにおける抽象クラス、抽象メソッド(abstract class、abstract method)とは
ずばり一言で表現するなら、「処理内容が確定していないクラス(メソッド)」のことです。これが何を意味するかは後述するとして、まずは抽象クラス、抽象メソッドの簡単なサンプルを紹介します。
▪️抽象クラス、抽象メソッドの宣言
abstract class Sample { //抽象クラスの宣言 abstract void hoge(); //抽象メソッドの宣言 }
このように、abstract自体は修飾子のひとつです。修飾子とはpublicやprivate、protectedといったアクセス修飾子やstaticやfinalのことです。abstractもこれらと同じく、クラスやメソッドの宣言時に修飾子の一つとして記載します。
クラスやメソッドを定義する時にabstractを加えることで「このクラス(あるいはメソッド)は抽象クラス(メソッド)です」と指定するのです。
書き方については抽象クラスの方はabstractが付いている以外、従来のクラス定義と変わりありません。しかし抽象メソッドの方は見慣れないですね。メソッド名()の後に{}がありません。
抽象メソッドは処理内容が確定していない。つまり、処理内容を書く必要がないということです。必要がなければ、処理を書くためのブロックも必要ありません。そのため、このような記述になります。抽象メソッドを定義する時は{}の代わりに行末に;を書くことを覚えておきましょう。
☆Point!☆
- 抽象クラス(メソッド)とは、どういう処理を行うかが決まっていないクラス(メソッド)
- クラスやメソッドの宣言時にabstractを追加することで、抽象クラス(メソッド)であることを指定する
- 抽象メソッドは処理内容を書かない。だから{}ではなく;で終わらせる
2.抽象クラス、抽象メソッドの特徴
抽象クラスや抽象メソッドは次の特徴を持ちます。
- 抽象クラスはnewによるインスタンス化ができない
- サブクラスが具象クラスの場合、抽象メソッドのオーバーライドを強制する
2−1. 抽象クラスはnewによるインスタンス化ができない
抽象クラスをインスタンス化しようとしても、コンパイルできずエラーとなります。
例えば、以下のようにmainメソッドで抽象クラスをインスタンス化しようとします。
public static void main(String[] args) { Sample sample = new Sample(); }
しかし、結果は以下の通りです。(Eclipseで実行しています)
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 型 Sample のインスタンスを生成できません
ルールに則って作ったクラスなのにエラーが出るなんていじわるじゃないか!と思われるかもしれません。しかし、ここは一つ落ち着いて考えてみてください。抽象クラスは「処理内容が確定していないクラス」でしたね。つまり未完成な設計図です。
処理内容が決まっていないクラスがインスタンス化できてしまうということは、未完成な設計図からモノが作られてしまうということです。まともに動かないものが作られてしまうのはとてもマズイことです。抽象クラスがインスタンス化できないのはいじわるではなく、むしろJavaの優しさなのです。
では未完成な設計図をどう扱えばいいのか。継承してサブクラスで具体的な処理を実装して完成させればいいのです。そしてこの完成した設計図こそが、具象クラスです。処理内容が未確定というあいまいな状態(抽象的)から、処理内容が確定した状態(具体的)になったのです。
ちなみに、サブクラスもabstractで抽象クラスに指定すれば、抽象メソッドをオーバーライドしていなくてもエラーにはなりません。サブクラスにオーバーライドが強制されるのは、具象クラスのときだけです。
☆Point!☆
- 抽象クラスはインスタンス化できないことで、未完成のインスタンスが生まれることを防いでくれる
- すなわち、抽象クラスは継承元(スーパークラス)となることが前提のクラス
- 抽象クラスの継承先(サブクラス)をふたたび抽象クラスにすることもできる
2−2. サブクラスが具象クラスの場合、抽象メソッドのオーバーライドを強制する
1章の冒頭で抽象クラスと抽象メソッドの宣言は以下のように行うと紹介しました。
abstract class Sample { //抽象クラスの宣言 abstract void hoge(); //抽象メソッドの宣言 }
あなたが抽象クラスを継承したサブクラスを作ろうとした時に、うっかり抽象メソッドをオーバーライドし忘れ、以下のようなコードを書いたとします。
public class ChildSample extends Sample { }
すると、以下のような警告が表示されます。(表示はEclipseのものです)
当然、エラーが出たままではコンパイルすることはできません。抽象メソッドのオーバーライド漏れがあった場合はエラーが出る=オーバーライドを強制する、ということがお分かりいただけたでしょうか。
上の例ではうっかり忘れたとしましたが、仮にメソッド名をhogeeなど、間違った場合も全く別のメソッドと判断されるため、オーバーライドをしたことにはならず同様にエラーとなります。
ただ「オーバーライドを強制する」、と聞くとめんどくさいルールを押し付けられているように感じるかもしれません。しかし、強制することはプログラマが意図しない記述(うっかり忘れや記述ミス)をしたときにそのことを教えてくれる便利な機能なのです。これもまたJavaの優しさですね。また、この機能により抽象メソッドは確実にオーバーライドされるため、サブクラスで処理内容が確定することが保証されます。
☆Point!☆
- 抽象メソッドのオーバーライドを強制することで、プログラマのミスを未然に防いでくれる
- 抽象メソッドは継承先(サブクラス)でオーバーライドされることで処理内容が確定する
3.abstractの使い方とメリット
抽象クラスと抽象メソッドのポイントの中でも次の2点に注目します。
- 抽象クラスは継承元(スーパークラス)となることが前提のクラス
- 抽象メソッドは継承先(サブクラス)でオーバーライドされることで処理内容が確定する
抽象クラスのことはわかったけれども、なぜこんな回りくどいことをする必要があるのか。と思われるかもしれません。その理由を知るためにも、abstractの使い方を見ていきましょう。
例えば、あるお店をクラスとして表現するとします。ここではそのお店での「会計の方法」を考えます。世の中にある会計の方法を考えると、いくつか方法が思い浮かびます。
- レジまで商品を持っていき、店員さんに商品を渡してレジでバーコードを読み取ってもらいお金を払う
- セルフレジまで商品を持っていき、自分でバーコードを読み取りお金を払う
- 商品を手にする前に券売機で券をあらかじめ買う
これらは方法は違えど、「会計をする」という行為自体に違いはありません。では実際にどのようにして抽象クラスを使うのか、例を紹介します。
まず、抽象クラスStoreを定義します。
// 抽象クラスStore abstract class Store { abstract void payment(); //「会計をする」メソッド }
次に、このStoreクラスを継承した3つのサブクラスを定義します。
- ConvenienceStoreクラス
class ConvenienceStore extends Store { void payment() { System.out.println("レジまで商品を持っていって店員さんに渡す"); } }
- SuperMarketクラス
class SuperMarket extends Store { void payment() { System.out.println("セルフレジまで商品を持っていって自分でバーコードを読み取る"); } }
- FastFoodStoreクラス
class FastFoodStore extends Store { void payment() { System.out.println("券売機で食券を買って店員さんに渡す"); } }
そして、これらサブクラスをインスタンス化してpaymentメソッドを実行してみます。
public class Main { public static void main(String[] args) { ConvenienceStore store1= new ConvenienceStore(); SuperMarket store2= new SuperMarket(); FastFoodStore store3= new FastFoodStore(); store1.payment(); store2.payment(); store3.payment(); } }
結果はこちらです。
レジまで商品を持っていって店員さんに渡す セルフレジまで商品を持っていって自分でバーコードを読み取る 券売機で食券を買って店員さんに渡す
オーバーライドが正しく行われているため、それぞれのお店の方法で会計が行われています。
すでに解説したとおり、各サブクラスでpaymentメソッドをオーバーライドし忘れていたり、あるいはメソッド名を1文字でも間違えていた場合、コンパイルはできずエラーとなります。つまり、抽象クラスを定義することで「お店を作る時は必ず会計方法を決めるんだよ。会計の方法はそれぞれのお店に任せるよ」というルールが作られたのです。
このルールはStoreクラスを継承するすべてのサブクラスに適用されるため、今後お店の種類が増えていっても、会計方法が定義されていないお店が生まれてしまうリスクが無くなります。
このように、「抽象クラスを継承させ、抽象メソッドをオーバーライドを強制させる」ことは、「同じような処理を行うがそれぞれ処理内容が異なるクラスを複数作らせたい」ときにとても有用なのです。
これこそが、abstractを用いるメリットです。
☆Point!☆
- 抽象クラスを定義する=サブクラスを作るためのルールを作る
4.抽象クラスを理解する=チームで開発するということを理解する
メリットって言うけど、あらかじめキチンとどのようなクラスを作るか把握しておけば抽象クラスをわざわざ作る必要なんてないんじゃない?と疑問に思われるかもしれません。その疑問はある面では正解です。
あなたが一人で開発をしている場合や、今後の機能の拡張を考えない場合、abstractを利用することはあまりないかもしれません。それどころか、余計なコードが増えるだけになる恐れさえあります。お店の例で言えば、コンビニ以外の店舗を作る予定もなく、会計方法も決まったものにする場合、コンビニクラス1つを作れば事足ります。
ここでは、抽象クラスを通じてぜひチームで開発することを知ってもらいたいです。
抽象クラスはサブクラスを作るためのルールを作ることだと言いましたが、それはルールを作った方が誰にとっても幸せなことだからです。
突然ですが、Javaの開発をチームで行うことをイメージしてみてください。あなたはお店クラスの設計を任されました。
お店クラスを設計する際に、あなたはプログラマ(ここではコーディングだけを行う人)たちに最低限実装してほしい機能を伝えたいと考えます。そこで設計書やサンプルクラスを作ってそれを記載したとします。ですが、見落とす可能性や実装漏れが発生するリスクは否定できません。テストで漏れなく確認できればいいですが、手戻りのコストを考えるとうまい方法とは言えません。漏れなく確認できたという担保をどうやってとるのかも考えなければなりません。こうしてあなたは各お店クラスが果たして正しく作られるか不安を抱えることになります。
あなたが設計者として責任を持ってチェックするという強い意志を持って確認すると決めたとしても、チェックが常に正しいとは限りません。人間はミスをしちゃうものです。
それにいつまでも同じ現場にあなたがいるとは限りません。そして、あなたがいなくなった後も保守や拡張のため開発が続くことは十分に考えられます。プログラマにとっても、必要な仕様を網羅していると思っていても自分の作業に漏れやミスがないかどこか不安を抱えたまま作業を行うことになります。
お互い不安を抱えたまま開発が進められていくのは誰にとっても不幸なことです。
こうした不安も、抽象クラスを正しく利用することで解消されます。仮に設計した人がいなくなっても、継承元にすべきクラスがわかっていれば、必要なオーバーライドが正しく行われているかはコンパイラがチェックしてくれるため、プログラマは不安なく処理内容の実装に集中できます。
このように、抽象クラスは設計者の立場から見ると、同じチームのメンバーやまだ見ぬ未来のメンバーがどうすれば開発や保守、拡張を効率よく進められるかを考えて用意するものです。
あなたが継承を学んだとき、差分だけコーディングすればいいのでプログラミングの効率が上がるといったことを目にしたかと思います。これは、プログラマ自身の作業を効率化させることを主としたものです。
同じ効率を上げる方法にしても、設計者とプログラマの立場の違いでアプローチの仕方が変わっているのがお分かりでしょうか。
しかし、アプローチは違えど使っている知識や技術は共通しているものがほとんどです。理解するためにはそれぞれの立場の違いを理解し、考え方を変えればいいのです。
Javaを学び始めたばかりの人には有用性や使いどころがイメージしにくいかと思い、このような話をしました。
5.さいごに
抽象クラスの使い方を通じて、チームで開発をすることまでイメージしてもらいました。
このイメージするというのはオブジェクト指向を理解する上でとても大切なことです。Javaの文法を理解するのと同じ考え方では、分かりにくいことがあるかもしれません。そんな時は、使う場面や誰が使うか、どんなときに必要なのかをイメージして考え方を切り替えてみてください。不思議とすんなり理解が進むことがあります。
といっても、ここで紹介した内容は抽象クラスのほんの一部でしかありません。この先、抽象クラスに関連した内容としてはinterfaceやポリモルフィズムといった概念が出てきます。それらを使いこなす未来の自分をイメージしながら楽しんで学んでくだされば幸いです。
コメント