はじめに。
どうも初めまして、にかわみかんです。
私が半年ほどJavaを学んでいて結構引っかかりやすい部分だなぁと感じたAbstract(抽象化)についてなるべくわかりやすく解説していきたいと思います。
私は職業訓練校にてJavaを勉強させていただいたのですが、抽象クラスを継承して子クラスを作成する。という事自体が正直言って最初はまったく意味がわかりませんでした。
クラスを抽象化したところで直接インスタンス生成できなくなるだけで何のメリットがあるのかが全く理解できなかったのです。
ですが、私が今まで書いてきたコードにはAbstractは既に欠かせないものになっています。
そんなわけで、抽象化のメリットと書き方を解説していきたいと思います。
Abstractのメリット
まずクラス継承のメリットとして共通の変数を定義したり、共通の処理を記述したりすることで、
同じ処理を記述する必要がなくなりコーディング速度やメンテナンス性の向上などのメリットがあります。
さらに抽象化によって以下のようなメリットがあります。
繰り返し処理の共通化
まず以下のようなコードがあります。
public class Test {
public static void main(String[] args) {
Area a = new Area(5);
Volume v = new Volume(5);
a.calcResult();
v.calcResult();
System.out.println(a.getResult());
System.out.println(v.getResult());
}
}
// 面積計算クラス
class Area {
int side;
int result;
public Area(int side) {
this.side = side;
}
public int getResult() {
return result;
}
public void calcResult() {
result = side * side;
}
}
// 体積計算クラス
class Volume {
int side;
int result;
public Volume(int side) {
this.side = side;
}
public int getResult() {
return result;
}
public void calcResult() {
result = side * side * side;
}
}
これは1辺の長さから正方形の面積と正立方体の体積をそれぞれ計算し出力するクラスです。
共通項が多いので何かしらの一つの親クラスがあればもっと完結にかけそうな気がしますね。
では以下の様に共通の変数とメソッドを一つのクラスにまとめてみます
public class Test {
public static void main(String[] args) {
Area a = new Area(5);
Volume v = new Volume(5);
a.calcResult();
v.calcResult();
System.out.println(a.getResult());
System.out.println(v.getResult());
}
}
// 共通処理、共通変数を記述した抽象クラス
abstract class Abst {
int side;
int result;
public Abst(int side) {
this.side = side;
}
public int getResult() {
return result;
}
}
class Area extends Abst {
public Area(int side) {
super(side);
}
public void calcResult() {
result = side * side;
}
}
class Volume extends Abst {
public Volume(int side) {
super(side);
}
public void calcResult() {
result = side * side * side;
}
}
少しすっきりしましたね!
親クラスを継承することで親クラスに記述されている変数side
とresult
を子クラスに記述する必要がなくなり、getResult()
メソッドも親クラスに記述されているものだけで済むようになりました!
ですが、まだ同じ名前の変数が多いしmainメソッド
では同じような構文で同じような事をしています。
そこで以下のように抽象メソッドが役立ってくれます!
public class Test {
public static void main(String[] args) {
Abst[] aArray = { new Area(5), new Volume(5) };
for (Abst a : aArray) {
a.calcResult();
}
for (Abst a : aArray) {
System.out.println(a.getResult());
}
}
}
abstract class Abst {
int side;
int result;
public Abst(int side) {
this.side = side;
}
public int getResult() {
return result;
}
abstract void calcResult(); // 抽象メソッドを定義
}
class Area extends Abst {
public Area(int side) {
super(side);
}
@override
public void calcResult() {
result = side * side;
}
}
class Volume extends Abst {
public Volume(int side) {
super(side);
}
@override
public void calcResult() {
result = side * side * side;
}
}
やっと繰り返し処理で書けるようになりました!
いままでなぜ繰り返し処理で書けなかったのかというと、別々のクラス型で処理していたからなんです。
なので、今回は親クラスであるAbst
型の配列で別々のインスタンスを配列に入れることにしました!
ですが、同じ名前のメソッドとはいえ別々に定義されているcalcResult()
メソッドは扱う事ができません。
ですので、抽象クラスであるAbst
に抽象メソッドcalcResult()
を定義しています。
どういうことかというと、親のクラス型に実装する場合。
子クラスに独自で実装されたメソッドというのは見つけることができないようになっています。
これは親クラスの型で呼び出している事であくまでも親のクラスの中のメソッドしか読み込まないのが原因です。
なので、先に名前と戻り値だけ抽象的に定義しておくことで、どこを読み込んだらいいか?というアドレス
を教えるような意味になるのです。
このように抽象化するとちょっとした処理の違いというギャップを吸収しながら繰り返し処理で処理する可能になります!
匿名インナークラスで応用してみよう
まず、匿名インナークラスについて解説してみたいと思います。
public class Test2 {
public static void main(String[] args) {
// 型に入れないで直接インスタンスのprintResult()を実行する
new Calc(5, 6).printResult(); // 11
new Calc(5, 6)
// 中括弧をつけてCalcクラスの中身を加筆する。
{
// printResult()を書き換えて引き算にする。
@Override
void printResult() {
System.out.println(a - b);
}
}
// 変更したメソッドを実行する
.printResult(); // -1
}
}
class Calc {
int a;
int b;
Calc(int a, int b) {
this.a = a;
this.b = b;
}
void printResult() {
System.out.println(a + b);
}
}
なんだかわかりにくいですよね。
私自身この書き方になれるまで色々なコードを参考にしたり、書いたりして覚えたものなのですぐに覚えられる方は天才だと思います。
これはCalc
クラスをmain
メソッド内でインスタンス化するんですが、その時にextends
して内容を加筆しているようなものになります。
ここではprintResult()
メソッドをOverride
して、直接メソッドを使ってコンソールへ表示しています。
これが匿名インナークラスというもので1度しか使用されないであろうクラスの処理をこのように記述していきます。
でも、このままだとちょっとメンドクサイ気がしませんか?
というのも、どのメソッドをOverride
すれば計算を変更方法が変更できるかがわかりませんし、printResult()
でSystem.out.print()
を書き忘れたらそもそも表示すらされず複雑化したプログラム内では何が問題かも探しにくくなるわけです。
ということで以下のように書き直しましょう!
public class Test2 {
public static void main(String[] args) {
// system.out.println()に直接Calcインスタンスを入れてtoString()メソッドを実行させる
System.out.println(new Calc(5, 6) {
// 計算するためにgetResult()をOverride
@Override
protected int getResult() {
return a + b;
}
});
System.out.println(new Calc(5, 6) {
@Override
protected int getResult() {
return a - b;
}
});
}
}
abstract class Calc {
int a;
int b;
Calc(int a, int b) {
this.a = a;
this.b = b;
}
// 計算式自体はあとから決めるのでabstractとしておく
abstract protected int getResult();
// toString()メソッドOverrideして計算結果を出力するようにする
@Override
public String toString() {
return getResult() + "";
}
}
今回はコンソールへ表示することが目的のため、toString()
メソッドをOverride
してSystem.out.println()
へ直接放り込むことにしました!
実はabstract
で定義されたメソッドは必ず実装する時にoverride
しなくてはならない、という制約があります。
そこでIDEの仕様でabstract
メソッドが含まれるクラスを匿名インナークラスでインスタンス化しようとした時に自動でOverride
するべきメソッドを見つけてひな形を作成する機能を利用してこのコードを書いています!
ここで実際に私が記述したのはSysmtem.out.println(new Calc(5,6){});
とreturn
の後のa + b
だけなんです!
このように、もともとの書式を覚えていなくても自動でコードを補完する事を前提にコーディングすることで打ち間違いを減らす事ができるのでコーディングが楽になります!
最後に
いかがでしたでしょうか?
今回はなるべくわかりやすく解説するためにカンタンな計算を行うプログラムしか書きませんでしたが、より複雑なプログラムにおいてはなるべく同じようなコードを書かない様に抽象化した処理が必要になる場面が多く存在すると思います。
ですので、「何度も同じ事書いてるな」ってかんじるコードがあったらぜひ参考にしてください!