Java言語セミナーを受講してシリーズが続いていましたが、今回で一応、完結です。
抽象クラス
Java言語では処理内容を記述しないメソッドや、それを持つクラスを定義することができ、この処理内容を記述しないメソッドのことを抽象メソッド(abstractメソッド)、抽象メソッドを持つクラスのことを抽象クラス(abstractクラス)と呼びます。
それに対して、これまで定義してきたクラスは、処理内容を記述したメソッド = 具象メソッドを持ち、インスタンス化もできるクラスでした。このようなクラスを具象クラスと呼びます。
クラス・メンバの宣言に特定のキーワードを指定すると修飾子になります。
abstractキーワードを指定する = abstract修飾子をつける
抽象クラス
abstract class クラス名{ }
抽象クラスの特徴
- クラス宣言でabstractキーワードを指定します。
- 具象メソッド、抽象メソッドを混在させれます。
- インスタンス化はできず、利用する場合はサブクラスを定義し、サブクラス側で抽象メソッドをオーバーライドしなければいけません。
- 抽象クラスをもとにサブクラスを定義するにはextendsキーワードを使います。(継承)
抽象メソッド
abstract 戻り値の型 メソッド名();
抽象メソッドの特徴
- メソッド宣言でabstractキーワードを指定します。
- 修飾子、戻り値の型、メソッド名、引数リストは、具象メソッドと同様に記述します。
- 処理を持たないため、メソッド名( )の後に{ }を記述せず『;』(セミコロン)で終えます。
abstractキーワードの指定する際の記述の仕方の例
abstract class Bicycle{.....} // OK class abstract Bicycle{.....} // NG コンパイルエラー
abstract void funcA(); // OK void abstract funcA(); // NG コンパイルエラー abstract void funcA(){}; // NG コンパイルエラー
抽象クラス・メソッドは何のためにあるの?
抽象クラスは、宣言された抽象メソッドを全てオーバーライドしたサブクラス(具象クラス)を定義しなければいけません。
サブクラスでオーバーライドしたメソッドで名前や引数リスト、戻り値の型にオーバーライド前のメソッドと一致しない点があると、コンパイルエラーにより知ることができます。
この仕組みにより、メソッドの名前や呼び出し方が統一された複数のクラスを、安全に定義できます。
抽象メソッドをオーバーライドして処理内容を記述することを実装と呼びます。
実装エラーの例
抽象クラスを継承したクラスの定義
//抽象クラス abstrct class Super{ protected abstrct void methodA(); // 抽象メソッド public void methodB(){} // 具象メソッド } //具象クラス class Sub extends Super{ protected void method(){} // 必須 // 抽象メソッドは必ず実装 //以下でもOK。アクセス修飾子は同じか公開範囲の広いものを指定 //public void methodA(){} public void methodB(){} // 任意 // 必要に応じて実装可能 }
抽象クラスを継承した抽象クラスの定義
// 抽象クラス abstract class X{ protected abstract void methodA(); } // 抽象クラス abstract class Y extends X{} // 具象クラス class z extends Y{ public void methodA(){} }
抽象クラスであるXクラスを継承したYクラスは、methodA()を実装していませんが、Yクラスも抽象クラスであるため問題ありません。
しかし、Yクラスを継承したZクラスは具象クラスなので、methodA()を実装する(オーバーライドする)必要があります。
インターフェイス
インターフェイスとは、公開すべき操作をまとめたクラス仕様(取り決め)です。
実際に行う処理の内容は、インターフェイスには記述しないようだった(Java SE7までは実装を持つメソッドをインターフェースに定義できない)のですが、Java SE8からは「defaultキーワード」「staticキーワード」 をメソッドに指定することで、メソッドに実装を持たせることができるようになったようです。
⇩ Java SE8で変わったインターフェイスについては下記サイトへ
・Java8のインタフェース実装から多重継承とMix-inを考える | ギークを目指して
・抽象クラス・インターフェース | 型とクラス | プログラミング言語の比較 | hydroculのメモ
・Java8で、Objectが持つメソッドについてはインターフェースでdefault実装をできないようにしている理由 - なみひらブログ
インターフェイスの特徴
- インターフェイス宣言には、interface キーワードを指定します。
- 変数は、定数しか定義できません。
- メソッドは抽象メソッド、Java SE8からはdefaultキーワードを指定することでデフォルト実装を持ったメソッド(Objectクラスが実装を持っているメソッド、例えば equals などはデフォルト実装を定義できない)を、staticキーワードを指定でstaticメソッド(defaultキーワードは不要)を定義できます。
- インスタンス化はできず、使用する際は実装クラスを定義します。実装クラス側では抽象メソッドは全てオーバーライドします。
- 実装クラスを定義するには、implements キーワードを指定します。
- extends キーワードを指定することでインターフェイスをもとにサブインタフェースを宣言できます。
インターフェイスでの変数とメソッド
インターフェイスで宣言あるいは定義できるのは、
Java SE7までは、抽象メソッドと定数
Java SE8からは、デフォルト実装のメソッド、staticメソッド
が追加されたようです。
インターフェイス内での変数
インターフェイス内で変数を宣言すると、暗黙的に public static final 修飾子が付与されるようです。つまり、static な定数となります。(final修飾子で変更ができなくなるため定数扱いになるということだと思います。)
したがって、宣言と同時に初期化しないとコンパイルエラーを起こします。
インターフェイス内の変数の例
インターフェイス内のメソッドの例
インターフェイスの実装クラス
抽象メソッドを全てオーバーライドしたクラスを定義したインターフェイスの実装クラスを宣言するには、implements キーワードを指定します。
Java SE8(JDK1.8)以降ではこれ以外にも方法があるかもしれませんが、自分は初学者なので分かりません。
⇩ デフォルト実装のメソッドについては下記サイトへ
Java8新機能 デフォルトメソッドと多重継承の違い - Yuji Blog
[修飾子] class クラス名 implements インターフェイス名{}
public interface MyInterface{} // インターフェイス public class Myclass implements MyInterface{} // 実装クラス
implemetsの後には、「,」 (カンマ)で区切って複数のインターフェイスを指定することができます。
実装クラスでは、implementsで指定した全てのインターフェイスの抽象メソッドをオーバーライドする必要があり、オーバーライドする際はpublic修飾子を指定する必要があります。
インターフェイスの実装クラスのイメージ図
インターフェイスの実装クラスのイメージ図2
インターフェイスの継承
インターフェイスを継承したサブインターフェイスを作成することができます。
継承関係を持つため、サブインターフェイスを宣言するにはクラスと同様にextendsキーワードを指定します。
サブインターフェイスを実装したクラスが具象クラスの場合、スーパーインターフェイス、サブインターフェイスの全てのメソッドをオーバーライドする必要があります。
interface AIF{ void methodA(); } interface BIF{ void methodB(); } interface SubIF extends AIF, BIF{ void methodC(); } class Myclass implements SubIF{ public void methodA(){System.out.println("methodA()");} public void methodB(){System.out.println("methodB()");} public void methodC(){System.out.println("methodC()");} } class sample_test31{ public static void main(String[] args){ Myclass c = new MyClass(); c.methodA(); c.methodB(); c.methodC(); } }
AIF、BIFはともにインターフェイスです。
これらを継承したサブインターフェイスがSubIFです。
そして、SubIFを実装したクラスがMyclassです。
インターフェイスは複数のインターフェイスを継承(extends)することが可能です。
また、複数のインターフェイスを実装(implemets)することも可能です。
それとは異なり、具象クラス、抽象クラスは1つのクラスしか継承(extends)できません(単一継承)。
型変換
Java言語では、2種類の型変換の仕組みがあります。
- 暗黙の型変換
- キャストによる型変換
暗黙の型変換は、Java側のプログラムが暗黙的に自動でデータ型を決定してくれるもので、それに対してキャストによる型変換は、キャスト演算子を使い強制的にデータ型を変えることです。
基本データ型の型変換
イメージ図(基本データ型の型変換)
暗黙の型変換(基本データ型)
イメージ図(基本データ型の型変換)にあるように、Javaは自動で⇨の方向に型変換してるので、普通の状態ではint型のデータをbyte型に入れようとしたりするとエラーになります。
①short型で宣言した変数の値をint型の変数に代入。i変数には10が格納されます。
②int型で宣言した変数の値をdouble型の変数に代入。d変数には100.0が格納されます。
③メソッドの引数でint型の値を渡し、double型で宣言した変数で受け取る。d変数には100.0が格納されます。
④メソッドで処理した結果をint型で返します。int型の戻り値をdouble型で宣言した変数で受け取ります。d変数には100.0が格納されます。
キャストを使用した型変換(基本データ型)
byte型の値をint型の変数に代入する場合は暗黙的に型変換されますが、その逆はコンパイルエラーになります。
このとき、キャスト演算子を使うと強制的に型変換が行われます。
ただし、数値によっては情報の一部が失われるので、使用時には注意が必要です。
(変換後の型)値
double d = 10.5; int i = (int)d; // iにはdouble型の変数dの値がint型に変換されて格納されるので、10が代入されます。
①int型で宣言した変数の値をshort型に変換してからshort型で宣言した変数に代入するので、s変数には100が格納されます。
②double型で宣言した変数の値をint型に変換してからint型で宣言した変数に代入。i変数には10が格納されます。
③メソッドの引数でdouble型の値をint型に変換してから渡し、int型で宣言した変数で受け取ります。i変数には10が格納されます。
④メソッドで処理した結果をint型に変換してから返す。戻り値をint型で宣言した変数で受け取ります。i変数には10が格納されます。
class sample_test33{ public static void main(String[] args){ double d = 10.5; //int i = d; // NG コンパイルエラー int i = (int)d; // OK キャスト演算子で処理してるため(ただしint型に変換されるので、小数点は切り捨てられます。) System.out.println("iの値:" + i); //foo(i); // NG 定義側でfoo(short a)と引数をshort型にしてるため、int型のiを引数にはできません。 foo((short)i); // OK キャスト演算子でint型の値を格納した変数iをshort型に型変換してるため } static void foo(short a){ System.out.println("aの値:" + a); } }
参照型の型変換
参照データ(オブジェクト)の型変換は代入先の変数の型変換は、代入先の変数の型(クラス、インターフェイス)と代入されるオブジェクトの間に継承関係、もしくは実装関係が必要です。
サブクラスのオブジェクトを、スーパークラスを型として宣言した変数で扱えます。
実装クラスのオブジェクトを、インターフェイスを型として宣言した変数で扱えます。
イメージ図(参照型の型変換)
暗黙の型変換(参照型)
①A型(スーパークラス)で宣言した変数に、X型(サブクラス)のオブジェクトを代入。
②B型(インターフェイス)で宣言した変数に、Y型(実装クラス)のオブジェクトを代入。
③メソッドの引数でX型(サブクラス)のオブジェクトを渡し、A型(スーパークラス)で宣言した変数で受け取ります。
④メソッドで処理した結果をY型(実装クラス)のオブジェクトで返します。戻り値をB型(インターフェイス)で宣言した変数で受け取ります。
キャストを使用した型変換(参照型)
スーパークラスを型として宣言した変数で参照しているサブクラスのオブジェクトを、サブクラスを型として宣言した変数で扱うにはキャストを用います。
インターフェイスを型として宣言した変数で参照している実装クラスのオブジェクトを、実装クラスを型として宣言した変数で扱うにはキャストを用います。
あるクラスをインスタンス化
⇩
⇩
キャストによりもとのクラス(型)に戻す
もとのクラス(型)以外のクラスにキャストしようとすると、実行時にClassCastExceptionというエラーが発生します。
(目的の型)オブジェクト
class Super(){} class Sub extends Super(){} class Test{ Super super = new Sub(); Sub sub = (Sub)Super; }
①A型(スーパークラス)で宣言した変数で扱っているX型(サブクラス)のオブジェクトを、X型に変換してからX型で宣言した変数に代入。
②B型(インターフェイス)で宣言した変数で扱っているY型(実装クラス)のオブジェクトを、Y型に変換してからY型で宣言した変数に代入。
③メソッドの引数でX型(サブクラス)のオブジェクトを渡し、A型(スーパークラス)で宣言した変数で受け取った後、X型に変換してからX型で宣言した変数に代入。
④メソッドで処理した結果をY型(実装クラス)のオブジェクトで返します。戻り値の型をB型(インターフェイス)で宣言した変数で受け取った後、Y型に変換してからY型で宣言した変数に代入。
コンパイルエラーになる場合と対処の仕方
参照型の暗黙の型変換で、サブクラス(もしくは実装クラス)のオブジェクトは、スーパークラス(もしくはインターフェイス)を型として宣言した変数で扱える説明がありました。
ですが、スーパークラス(もしくはインターフェイス)を型とする変数に対して、サブクラス(もしくは実装クラス)で独自に定義したメソッドを呼び出すコードを書くと、コンパイルエラーになります。
コンパイル時には、呼びだそうとしているメソッドが、変数の型となっているかどうかチェックされてるようです。
実行時には、インスタンス化されているオブジェクトのメソッドが呼び出されます。
例1:コンパイルエラー
3行目でSubクラスのインスタンスを、スーパークラスであるSuper型の変数に代入してるため、4行目でコンパイラはSuperクラスにmethodA()メソッドがあるかチェックしますが、methodA()メソッドはSuperクラスで定義されてるのでOKです。
次に5行目でコンパイラはSuperクラスにmethodB()メソッドがあるかチェックしますが、Superクラスでは、methodB()メソッドは定義されていないため、5行目でエラーとなり結果はコンパイルエラーになります。
例2:コンパイル成功
例1と同じく、3行目でSubクラスをインスタンス化しスーパークラスであるSuper型の変数に代入しています。
まず、4行目でSuperクラスにmethodA()メソッドがあるかチェックしますが、methodA()メソッドはSuperクラスで定義されてるのでOKです。
5行目では、Sub型にキャスト後、Sub型で宣言した変数に代入しています。
このため6行目でSubクラスにmethodB()メソッドがあるかチェックされますが、Subクラスでは、methodB()メソッドが定義されてるのでOKです。
実行時は、4行目のmethodA()呼び出しに対し、呼び出されるメソッドは、Subクラスで定義(オーバーライド)したメソッド(②のメソッド)です。(インスタンス化しているのがSubクラスのオブジェクトのため。)
パッケージ宣言とインポート
パッケージ
ソースファイルにクラスを定義しコンパイルすると、.class拡張子のファイルが作成され、このクラスファイル名にはclass宣言したクラス名が使われています。
もし、同じ名前のクラスがあるとコンパイルした際にクラスファイルは上書きされてしまいます。
つまり、1つのアプリケーションの中ですでに使用されたクラス名は使用できません。
規模の大きいアプリケーション開発では、クラス名が重複する可能性(クラス名の衝突 = コンフリクト)が出てきます。
いつ何時如何なる時どんな状況になってもクラス名が衝突しないように、Java言語ではパッケージという仕組みを提供しています。
パッケージ名は「Robo社」だけでも、「Robo社」「営業部」という階層を設けてもかまいません。
実際には、名前の衝突が起きにくいように複数の階層を設けるやり方が推奨されるようです。
パッケージ化
クラスをパッケージに含めることをパッケージ化というようです。
package パッケージ名 class X{・・・・・}
クラスをパッケージ化するには、ソースファイルの先頭に package キーワードでパッケージ名を記述し、「 ; 」(セミコロン)で閉じます。
パッケージ宣言は必ずソースファイルの先頭に記述します。(空白とコメントはその前に記述することができます。)
パッケージ名を階層化する場合は、各階層を「 . 」(ドット)で区切ります。
パッケージの宣言規則まとめ
- ソースファイルの先頭にpackageキーワードを使用し宣言します。
- パッケージ名を階層化する場合は、「.」(ドット)で区切ります。
com.robo.sales.sales1000
- 1つのソースファイルに宣言できるパッケージは1つのみです。
package.com.robo.sales.sales1000; class X{} // com.robo.sales.sales1000.Xとして扱われます。 class Y{} // com.robo.sales.sales1000.Yとして扱われます。
パッケージされたクラスのコンパイルと実行
プログラムでパッケージを試してみます。
package com.robo; class Foo{ void print(){ System.out.println("package sample");; } } class sample_test34{ public static void main(String[] args){ Foo f = new Foo(); f.print(); } }
sample_test34.javaをコンパイルし、javaコマンドを実行すると、実行時エラーとなります。
パッケージ化されたクラスは、クラス名単体で扱うことができないためです。
「パッケージ名 + クラス名」 で扱う必要があります。
そのために、パッケージ名に対応したフォルダを用意し、そのフォルダの中にクラスファイルが保存されるようにしなければいけません。
クラスをパッケージするまでの手順
①パッケージ名と同じフォルダを任意の場所に作成します。ここでは『java01 』フォルダの中にパッケージ名と同じ「com」フォルダ、「robo」フォルダを作成してます。
②「robo」フォルダの中にソースファイルを保存します。
③コンパイルはパッケージ階層の一番上のフォルダが見える場所(今回は『java01 』フォルダのこと)で行います。フォルダが階層化してる場合は、フォルダ名の間は『¥』(半角円記号)でつなぎます。
コンパイルが完了すると、クラスファイルがソースファイルと同じ場所に作成されます。
④実行もコンパイル時と同様、パッケージ階層の一番上のフォルダが見える場所(今回は『java01 』フォルダのこと)で実行します。
ただし、フォルダ名の間は「 . 」(ドット)でつなぎます。
「 . 」(ドット)を使い階層構造まで指定する理由は、クラス名がパッケージ化されたことにより、クラス名だけでは呼び出せなくなったためです。
「~パッケージに属する~クラス」と完全な名前で呼び出す必要があります。
import文
プログラムの規模が大きくなると、あるクラスが自分が属するパッケージ以外のクラスを利用するケースが出てきます。
import文は、異なるパッケージに属するクラスを、いちいちパッケージ名を指定しなくてもクラス名だけで利用可能にする方法です。Java言語ではこの方法をインポートと呼ぶようです。
クラスをインポートするには、ソースファイルの先頭でimport文を記述します。
複数のクラスをインポートするときは、インポートするクラス1つ1つに対してimport文を記述します。
ただし、クラス名の代わりに「*」(アスタリスク)を記述すると指定されたパッケージに属する全てのクラスを利用可能にできます。
import com.robo.sales.Foo; // ①OK import com.robo.sales*; // ②OK import com.*; // ③NG コンパイルエラー import com.robo.*; // ④NG コンパイルエラー
①はクラス名まで指定しており、②はパッケージ名の後に「*」を指定していますが、どちらも正しい記述でOKです。
③や④のようにパッケージ名の途中で「*」を使用することはできないため、コンパイルエラーになります。
import文とpackage文の両方を記述する場合には、package文を先に記述します。
package com.robo; import com.robo.sales.Foo; class sample_test35{ }
実際にpackage文とimport文を使ってみます。
com.robo.salesという階層化したパッケージにFooクラスを、com.roboという階層化したパッケージにsample_test35クラスが保存されるようにソースファイルを用意します。
package com.robo.sales; public class Foo{ public void print(){ System.out.println("Package sample"); } }
package com.robo; import com.robo.sales.Foo; class sample_test35{ public static void main(String[] args){ Foo f = new Foo(); f.print(); } }
sample_test35.javaをコンパイルし実行した結果
利用されるクラス側の注意点
sample_test35.javaではFooクラスを利用していましたが、Fooクラスでは
- クラス宣言にpublic修飾子がついている
- print()メソッドにpublic修飾子がついている
となっていました。
これは、sample_test35クラスとFooクラスでは属するパッケージが異なるためです。
自分が属するパッケージ以外のクラスに利用利用を許可するためには、public修飾子が必要です。
Javadocとアノテーション
Javadocとは
サン・マイクロシステムズが開発したコンピュータソフトで、JavaのソースコードからHTML形式のAPI仕様書を生成するものである。
JavadocはJavaクラスの仕様書の標準の書式であり、多くのIDEは自動的にJavadoc HTMLを生成する機能を備えている。
アノテーション(annotation)
あるデータに対して関連する情報(メタデータ)を注釈として付与すること。
XML等の記述形式を用いてメタデータをタグ付けする場合が多い。
付与したメタデータやタグを指してアノテーションという場合もある。
大雑把に言うと、Javadocは、プログラマー(人間)に対してのコメントで、アノテーションは、コンパイラや実行環境(機械)に対するコメントのことらしいです。
Java SE5以降のバージョンでは、正しくオーバーライドしていることをコンパイラーにチェックさせるために、@Overrideというアノテーションを明示的に付けることが推奨されているそうです。
@Overrideを付ける理由
例えば、PrinterクラスのtoString()メソッド(PrinterクラスのスーパークラスであるObjectクラスのtoString()メソッドをオーバーライドしたもの)を使おうとしたのに、タイピングミスでtoStrimg()としてしまったとします。
(今回は、C¥sample¥java01¥Printer.javaとして、ファイルの文字コードをShift-JISで保存しています。)
public class Printer{ // ⇩ タイピングミスでtoString()としたかったところをtoStrimg()としてしまった public String toStrimg(){ return "toStringメソッドを実行しました。"; } public static void main(String[] args){ Printer obj = new Printer(); System.out.println(obj.toString()); } }
この場合、toStrimg()というメソッドが実行されてしまい、toString()をオーバライドして使うという意図とは異なる結果になっていしまいます。
そんな時に、@Overrideというアノテーションを使用すると、コンパイラーが、コンパイルエラーを発生させてプログラマーに教えてくれます。
public class Printer{ @Override public String toStrimg(){ return "toStringメソッドを実行しました。"; } public static void main(String[] args){ Printer obj = new Printer(); System.out.println(obj.toString()); } }
@Overrideを付けたことによって、コンパイラーが『オーバライドされてません』的なエラーを表示してくれます。
public class Printer{ @Override public String toString(){ return "toStringメソッドを実行しました。"; } public static void main(String[] args){ Printer obj = new Printer(); System.out.println(obj.toString()); } }
toStrimg()をtoString()に修正し、意図した動作になりました。
ポリモーフィズム(polymorphism)
同名のメソッドや型などをオブジェクトの種類に応じて使い分けることができる性質のことで、日本語では「多相性」「多態性」「多様性」などと呼ばれるようです。
Javaでポリモフィズムを実現する方法としては、インタフェースを使うパターンと、抽象メソッドを定義し抽象クラスで実現するパターンがあるようです。
ちなみに、
の3つをオブジェクト指向の三大要素で、中でもポリモーフィズムが最も強力かつ難解且つ重要な技術といわれてるようです。
今回は、
オブジェクト指向の悟りを開く!初心者でも分かるOOPの最重要技術ポリモーフィズムの使い方! | やまろうのITエンジニア仕事術
のやまろうさんのブログを参考にさせていただき、ソースコードにポリモーフィズムを適用させることを試していきたいと思います。
まずは、
『AppLogAnalyzer.java(C¥sample¥java01¥AppLogAnalyzer.java)』『WebLogAnalyzer.java(C¥sample¥java01¥WebLogAnalyzer.java)』
の2つのjavaファイルと、
なにかしら入力されたファイル読み込み用のテキストファイル
『applog.txt(C¥sample¥java01¥applog.txt)』
『weblog.txt(C¥sample¥java01¥weblog.txt)』
何も書かれてないアウトプット用のテキストファイル
『applogAnalyze.txt(C¥sample¥java01¥applogAnalyze.txt)』
『weblogAnalyze.txt(C¥sample¥java01¥weblogAnalyze.txt)』
適当なhtmlファイル(名前から推測するに会社概要みたいなページでしょうか)
『company_info.html(C¥sample¥java01¥company.html)』
を用意します。
※javaファイルと他のファイルの位置関係は推測なので間違ってる可能性がありますので実行できない場合につきましては何卒ご容赦下さい。
実行できなくても何となくやろうとしてることのニュアンスを感じ取っていただけたらと思います。
// java.ioパッケージはJavaで用意されてるものです。 // java.ioパッケージに入ってるものをimport文で読み込む。(下記URL先の情報はJava SE7の説明です。) // java.ioパッケージの階層構造 ⇒ https://docs.oracle.com/javase/jp/7/api/java/io/package-tree.html // java.ioパッケージに入ってるものの説明 ⇒ https://docs.oracle.com/javase/jp/7/api/java/io/package-summary.html import java.io.BufferedReader; // BufferedReaderクラス(クラスのサマリー) import java.io.FileInputStream; // FileInputStreamクラス(クラスのサマリー) import java.io.FileNotFoundException; // FileNotFoundExceptionクラス(例外のサマリー) import java.io.FileOutputStream; // FilterOutputStreamクラス(クラスのサマリー) import java.io.IOException; // IOExceptionクラス (例外のサマリー) import java.io.InputStream; // InputStreamクラス(クラスのサマリー) import java.io.InputStreamReader; // InputStreamReaderクラス(クラスのサマリー) import java.io.PrintStream; // PrintStreamクラス(クラスのサマリー) import java.io.Reader; // Readerクラス(クラスのサマリー) // AppLogAnalyzerクラス public class AppLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む InputStream inputStream = null; // InputStreamクラス型の変数inputStreamにnullを代入。 Reader inputStreamReader = null; // Readerクラス型の変数inputStreamReaderにnullを代入。 BufferedReader in = null; // BufferedReaderクラス型の変数inにnullを代入。 int warningCount = 0; // int型の変数warningCountに0を代入。 int errorCount = 0; // int型の変数errorCountに0を代入。 /* try、catch、finallyはJavaでの例外処理における基本事項です。 try { 例外をスローする可能性のある処理 } catch (例外クラス型 引数名) { 例外処理(例外ハンドラ) } finally { 最後に必ず実行される処理 } */ // 例外処理 try { inputStream = new FileInputStream("applog.txt"); // FileInputStreamクラスのオブジェクトを作成(引数に"applog.txt")し、参照変数inputStreamに代入。 inputStreamReader = new InputStreamReader(inputStream); // InputStreamReaderクラスのオブジェクトを作成(引数はFileInputStreamオブジェクト)し、参照変数InputStreamReaderに代入。 in = new BufferedReader(inputStreamReader); // BufferedReaderオブジェクトを作成(引数はInputSteamReaderオブジェクト)し、BufferedReader型の変数inに代入。 String line = null; // String型の変数lineにnullを代入。 // readLine()はBufferedReaderクラスのメソッドで、「テキスト行を読み込みます。」とあるので、読み込むテキストがある間は繰り返す。 while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする(IndexOf()はStringクラスのメソッド(全てのクラスはJava.langパッケージのObjectクラスを継承)) if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する FileOutputStream fileOutputStream = null; // FileOutputStreamクラス型の変数fileOutputStreamにnullを代入。 PrintStream out = null; // PrintStreamクラス型の変数outにnullを代入。 // 例外処理 try { fileOutputStream = new FileOutputStream("applogAnalyze.txt"); // FileOutoutStreamオブジェクトを作成(引数は"applogAnalyze.txt")し、参照変数fileOutputStreamに代入。 out = new PrintStream(fileOutputStream); // PrintStreamオブジェクトを作成(引数はFileOutputStreamオブジェクト)し、PrintStreamクラス型の変数outに out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } catch (FileNotFoundException e) { e.printStackTrace(); return; } finally { if (out != null) { out.close(); } } } }
//同じくimport文で読み込み import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; // WebLogAnalyzerクラス public class WebLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む InputStream inputStream = null; Reader inputStreamReader = null; BufferedReader in = null; int count = 0; // 例外処理 try { inputStream = new FileInputStream("weblog.txt"); inputStreamReader = new InputStreamReader(inputStream); in = new BufferedReader(inputStreamReader); String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する FileOutputStream fileOutputStream = null; PrintStream out = null; // 例外処理 try { fileOutputStream = new FileOutputStream("weblogAnalyze.txt"); out = new PrintStream(fileOutputStream); out.println(count); } catch (FileNotFoundException e) { e.printStackTrace(); return; } finally { if (out != null) { out.close(); } } } }
共通化できる部分があるので、まずは、手続き型言語的なアプローチで共通化していくということで、重複したコードをメソッド/関数にまとめていきます。
まとめたものを『FileUtil.java』ファイルとします。
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; public class FileUtil { public static BufferedReader openInputFile(String fileName) { InputStream inputStream = null; Reader inputStreamReader = null; BufferedReader in = null; try { inputStream = new FileInputStream(fileName); inputStreamReader = new InputStreamReader(inputStream); in = new BufferedReader(inputStreamReader); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } return in; } public static PrintStream openOutputFile(String fileName) { FileOutputStream fileOutputStream = null; PrintStream out = null; try { fileOutputStream = new FileOutputStream(fileName); out = new PrintStream(fileOutputStream); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } return out; } }
FileUtilという共通クラスにメソッドをまとめたので、『AppLogAnalyzer.java』『WebLogAnalyzer.java』ファイルを修正していきます。
import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; //少しコードは短くなったがまだまだ長い public class AppLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む BufferedReader in = null; int warningCount = 0; int errorCount = 0; try { // openInputFile()はstaticなメソッドなので、『クラス.メソッド名』で呼び出せます。 in = FileUtil.openInputFile("applog.txt"); if (in == null) { return; } String line = null; while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = null; try { out = FileUtil.openOutputFile("applogAnalyze.txt"); if (out == null) { return; } out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } finally { if (out != null) { out.close(); } } } }
import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; //少しコードは短くなったがまだまだ長い public class WebLogAnalyzer { public static void main(String[] args) { // 解析対象のファイルを読み込む BufferedReader in = null; int count = 0; try { in = FileUtil.openInputFile("weblog.txt"); if (in == null) { return; } String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = null; try { out = FileUtil.openOutputFile("weblogAnalyze.txt"); if (in == null) { return; } out.println(count); } finally { if (out != null) { out.close(); } } } }
try-catchのコードが重複していますが、中身のreadFile()メソッドやwriteFile()メソッドの処理だけをファイルごとに変えていきたいわけです。
ここでポリモーフィズムの出番となるようです。
try-catchの処理の中のreadFile()メソッドやwriteFile()メソッドだけを変えたいので、これを抽象メソッドとして持つクラス(抽象クラス)を作成し、『AppLogAnalyzerクラス』『WebLogAnalyzerクラス』に継承させて、それぞれ抽象メソッドをオーバーライドさせることで、処理内容だけ変えることができます。
抽象メソッドは、処理の内容がサブクラス側の実装の定義で変わってくる(具体的な処理を決める)のですが、このような設計をTemplate Methodパターンと言うようです。
抽象クラス用ファイルを作成。
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; public abstract class FileAnalyzer { /** * このメソッドは処理の全体フローを記述するTemplate Methodです。 * ファイル読み込み & 集計処理 * 1. 読み込み用ファイルをオープンする * 2. ファイルを読んでなんらかの集計処理をする(抽象メソッド:readFileを呼び出す) * 3. ファイルを閉じる * 4. 例外発生時にはスタックトレースを出力する * * 集計結果ファイル出力処理 * 1. 出力用ファイルをオープンする * 2. ファイルになんらかの集計結果を出力する(抽象メソッド:writeFileを呼び出す) * 3. ファイルを閉じる * */ public void analyzeFile(String inputFile, String outputFile) { // 解析対象のファイルを読み込む BufferedReader in = FileUtil.openInputFile(inputFile); if (in == null) { return; } try { readFile(in); } catch (FileNotFoundException e) { e.printStackTrace(); return; } catch (IOException e) { e.printStackTrace(); return; } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } // 集計結果をファイルに保存する PrintStream out = FileUtil.openOutputFile(outputFile); if (out == null) { return; } try { writeFile(out); } finally { if (out != null) { out.close(); } } } /** サブクラスでファイルを読んでなんらかの集計処理を実装するための抽象メソッド */ protected abstract void readFile(BufferedReader in) throws FileNotFoundException, IOException; /** サブクラスでファイルになんらかの集計結果を出力する処理を実装するための抽象メソッド */ protected abstract void writeFile(PrintStream out); }
『AppLogAnalyzer.java』『WeblogAnalyzer.java』を抽象クラス『FileAnalyzerクラス』をextends(継承)した形に書き換えます。
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; //最初のバージョンに比べてかなりコードが短くなっている public class AppLogAnalyzer extends FileAnalyzer{ private int warningCount = 0; private int errorCount = 0; @Override protected void readFile(BufferedReader in) throws FileNotFoundException, IOException { String line = null; while ((line = in.readLine()) != null) { // エラーと警告の件数をカウントする if (line.indexOf("[ERROR]") >= 0) { errorCount++; } else if (line.indexOf("[WARN]") >= 0) { warningCount++; } } } @Override protected void writeFile(PrintStream out) { // 集計結果をファイルに保存する out.println("error=" + errorCount + "件"); out.println("warning=" + warningCount + "件"); } public static void main(String[] args) { AppLogAnalyzer obj = new AppLogAnalyzer(); obj.analyzeFile("applog.txt", "applogAnalyze.txt"); } }
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; //最初のバージョンに比べてかなりコードが短くなっている public class WebLogAnalyzer extends FileAnalyzer { int count = 0; @Override protected void readFile(BufferedReader in) throws FileNotFoundException, IOException { String line = null; while ((line = in.readLine()) != null) { // 会社情報ページのアクセス数をカウントする String[] tokens = line.split("\t"); if (tokens[5].equals("company_info.html")) { count++; } } } @Override protected void writeFile(PrintStream out) { // 集計結果をファイルに保存する out.println(count); } public static void main(String[] args) { AppLogAnalyzer obj = new AppLogAnalyzer(); obj.analyzeFile("weblog.txt", "weblogAnalyze.txt"); } }
ポリモーフィズムを使うことでコードを短縮することができるようです。
ちなみに、画像などのバイナリファイルを読み書きするには、FileInputStream/FileOutputStreamクラスを利用しますが、
『ファイルなど共有のリソースを利用する際には、tryブロック直後のカッコで宣言します。これによって、リソースはtryブロックを抜けるタイミングで自動的に解放されますので、いわゆる「ファイルの閉じ忘れ」を防げます。この構文は、JavaSE 7で新たに追加されたtry-with-resources構文です。』
という使い方ができるそうです。
⇩ 詳しくは下記サイトへ
・FileInputStream/FileOutputStreamクラス | Javaコード入門
try (FileInputStream in = new FileInputStream("C:/sample/java01/sample.jpg"); FileOutputStream out = new FileOutputStream("C:/sample/java01/sample2.jpg")) { int data; while ((data = in.read()) != -1) { out.write((byte) data); } } catch (IOException e) { e.printStackTrace(); }
FileInputStream/FileOutputStreamコンストラクターで操作対象のパスを指定することで、それぞれファイルを読み取り/書き込み専用で開けます。
ファイルからバイトデータを読み込むにはreadメソッドを利用します。
readメソッドは、読み込みに成功した場合はその値を、読み込むべきデータがなかった場合には-1を返します。
上では、その性質を利用して、readメソッドが-1を返すまでwhileループを繰り返すことで、ファイルの内容をすべて読み込めます。
バイトデータは、writeメソッドを利用することで、対象のファイルに書き込みます。
⇩ ポリモーフィズムについては下記サイトへ
・オブジェクト指向わかった気になっている?[ポリモーフィズム] java - Qiita
ここまでの内容は下記書籍を参考にさせていただきました。
今回はこのへんで。