業務系のJavaプログラマーが知っておくべき10個のBad Partsとその対策
Java: The Good Partsの本のタイトルに触発されて、逆にJava言語の使いにくい部分をいくつかピックアップしてみました。Java EEなどの業務系のアプリケーションプログラマーの視点で書いていますので、別の立場ではここで指摘している事項が必ずしもBad Partではないという指摘もあるかもしれませんし、他にもいろいろなポイントがあると思いますが、とりあえず、私の独断で思いついたものを10個説明したいと思います。
1.標準APIのチェック例外が扱いにくい
Java言語のチェック例外は本当にGood Partなのか? - 達人プログラマーを目指してでも取り上げましたが、Bad Partの第一番目として標準APIのチェック例外が扱いにくいという点を指摘させていただきたいと思います。チェック例外については、理屈上コンパイラーによって例外の処理をプログラマーに強制させることができるすばらしい考え方のように思われますし、実際に以前はそのように信じられていたところがありますが、少なくとも業務アプリケーション開発の領域において、Java APIのチェック例外が邪魔になったことはあれ、便利だと感じたことはほとんどありません。特に、
- リフレクションAPIを使う場合
- RMI APIを直接使う場合
- JDBC APIを直接使う場合
- JAXPなどのXML関連のAPIを直接使う場合*1
- J2EE系のAPI(JNDI、EJB2.x、JMS、Java Mailなど)を直接使う場合
などの場合、例外はアプリケーション側で対処可能な原因で発生しないことがほとんどで、面倒なことはあっても役に立っているという実感は感じられませんね。たとえば、リフレクションを使って特定のフィールドの値を読み込む以下のコードを考えてみてください。
Field field; try { field = getClass().getField("testField"); Object value = field.get(this); } catch (SecurityException e) { // 例外処理 } catch (NoSuchFieldException e) { // 例外処理 } catch (IllegalAccessException e) { // 例外処理 }
単に動的にフィールドの値を取得したいだけなのに、相当面倒なコードの記述が必要なので、本来便利なリフレクションAPIの敷居が必要以上に高くなってしまっています。*2これがJDBCなどと組み合わさるとさらに複雑なことになるのは容易に想像できるでしょう。実際に上記の例外に対してアプリ側で対処できることはせいぜい
- ログを記録する
- 実行時例外に変換して再送出する
- 例外を戻り値に変換する
- VMをシャットダウンする
といった決まりきったことしかできないのが普通であり、こういった例外処理はAOPで自動化してしまえるものです。標準APIのチェック例外で、私が時々便利と感じられるのは
- ParseExceptionなど入力チェックの例外
- IOExceptionなどIO系の例外
- InterruptedExceptionなどスレッドの割り込み時の例外
くらいでしょうか。
私はチェック例外の存在意義について正しく理解するには、もともとJavaがバージョン1で登場してきた時代のことを考えるべきだと思います。今では信じられないかもしれませんが、この時代のJavaにはサーブレットもJDBCもリフレクションAPIもありませんでした。今では常識のJavaBeansという概念すらなかったのです。この頃はアプレットという言葉に代表されるように数個のクラスからなる小規模なアプリケーションやネットワーク通信のユーティリティなどを作成する言語であると考えられていました。つまり、小規模でマルチスレッドなプログラムを作成するのに特化していたというところがあると思います。このようなコンテキストでIOExceptionやInterruptedExceptionなどがチェック例外として定義されました。残念ながらこの時の成功によって、それから後に登場するAPIでは盲目的にチェック例外を使うべきという基準ができてしまったのだと推測されます。そして、典型的なGolden Hammerアンチパターン*3に陥ってしまい、以降も本来不適切な部分にまでチェック例外が利用されてしまったのではないかと思われるのです。
チェック例外に対しては、システム系の例外は実行時例外+AOPでハンドリングするのがベスト - 達人プログラマーを目指してで説明したようにSpringなどのフレームワークを使ったり、AOPを使ったりすることで、適切な非チェック例外に変換して処理するのが現実的だと思います。また、ScalaやGroovyなどの軽量言語を利用するのもよいと思います。現代的なアーキテクトであれば、実際に業務ロジックの開発を行うプログラマーが苦しまないように、適切なプログラミングモデルを標準化しておくことが大切だと思います。*4
2.リソースの解放処理の適切な記述が面倒
上記のチェック例外の問題とも関連しているのですが、Javaで例外発生時に正しいリソースの解放処理を記述することは面倒だし、実際に初心者の人が間違えやすいポイントとなりがちです。
たとえば、ファイル入出力処理をJavaの標準APIのみを使って記述すると以下のように記述する必要があります。
// tryブロックの外で宣言 Reader in = null; try { in = new BufferedReader(new FileReader("ファイル名")); // 入力処理 } catch (IOException e) { // 例外処理 } finally { // finallyブロックで後処理 if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } }
以上のような書き方については、イディオムとして記述方法を丸暗記しておくことが肝要ですが、このような複雑な記述は初心者の人にとっては難しいと思いますね。*5
現在では、IO関連のAPIはアプリケーションロジック中で直接利用するのではなく、最低でもCommons IOなどのライブラリーを利用することを考えるべきです。また、決まりきった処理であればTemplate Methodパターンを使ったフレームワークを独自に工夫して、業務ロジックの記述者が本質的なロジックのみに集中できるように工夫するべきでしょう。Spring FrameworkのJdbcTemplateなどの設計が参考になります。
それから、現状、Javaプログラマーがとりえる対処としてもっと良いのはGroovyを併用することですね。たとえば、ファイルの読み込み処理であれば、親しみやすいJavaライクな構文を使いながら、以下のようにRubyなみに簡単に記述できるようになります。
new File('ファイル名').eachLine {line -> // line に各行の文字列が入っている。 } // リソースの解放は自動的に行われるため考えなくて良い。
(追記)コメント欄にてご指摘があったのですが、今年の夏リリースが予定されているJava SE7ではリソースの自動解放機能のための構文拡張が行われるようです。もともとのJavaの例だと以下のように書けるみたいです。
try (Reader in = new BufferedReader(new FileReader("ファイル名"))) { // 入力処理 } catch (IOException e) { // 例外処理 } // リソースは自動的に解放される。
地味な拡張ですが、現状と比べるとかなり便利かもしれません。
Redirecting...
3.日付関連APIの使い勝手が悪い
業務アプリケーションでは日付型のデータを扱うことはかなり頻繁にありますが、Javaの日付関連のAPIはあまり使い勝手の良いものではありません。特定の日時の日付を生成したり、フォーマットしたりするのにかなり面倒なコーディングが必要になります。また、JUnitで日付関連のテストを作成するのはユーティリティを工夫しないと、かなり面倒になってしまいます。
たとえば、この文章を書いている今日の日付を生成するには以下のようなコードが必要です。
// 今日の日付を生成 Calendar cal = Calendar.getInstance(); cal.set(2011, Calendar.FEBRUARY, 26); Date today = cal.getTime(); // yyyy/mm/ddでフォーマット DateFormat df = new SimpleDateFormat("yyyy/MM/dd"); df.format(today);
さらに、日付の計算もDateのインスタンスに対する普通の演算子やメソッド呼び出しでなく、Calendarを使う必要があり結構面倒です。たいていのまともなプロジェクトであれば、こうした処理を簡易化するユーティリティクラスを作成して対処していると思いますが、Joda Timeのようなライブラリーを利用するのもよいと思います。
Joda-Time
また、Groovyだと以下のように簡単に文字列と相互変換できたり、計算できたりします。
Date day = Date.parse('yyyy/MM/dd', '2011/2/26') Date nextDay = day + 1 println nextDay.format('yyyy/MM/dd')
4.特定の精度を保った金額計算が面倒
日付と同様に、特定の精度で桁落ちやオーバーフローしない演算もお金を扱うような業務システムではよく必要になります。Javaの場合はjava.mathパッケージのBigDecimalというクラスを使ってデータの保持と計算を行い、java.text.DecimalFormatを用いて文字列にフォーマットします。これも、日付と並んで面倒なところです。もともとJavaは業務系というよりはCのようなシステム系のプログラミングを想定していたのかもしれませんが、言語としてこういった金額計算のサポートがなく、ライブラリーとして後付けで対応しているため、不便になっているのだと思います。
また、JavaがC#のように自動的に値がコピーされる値型のオブジェクトを持たないことから仕方が無いのですが、BigDecimalは不変クラスとして設計されていて、計算結果は別のインスタンスになるため、忘れずに計算結果を再代入する必要もあります。普通の四則演算の演算子が使えるなら問題ないのですが、メソッド呼び出しの形式になっていると、くせでうっかりターゲットのオブジェクトの状態が変更されていると勘違いしてしまうバグもあります。(このバグはFindBugsで検出できる。)
BigDecimal num1 = new BigDecimal("123456.789"); BigDecimal num2 = new BigDecimal("111.111"); // num1の状態は不変なので結果を別の変数で受け取る BigDecimal result = num1.multiply(num2); // 普通の演算子が使えないので複雑な計算が面倒。
Groovyだと数値リテラルの最後にGという接尾辞をつけるだけで自動的にBigIntegerやBigDecimalを生成でき、後は普通に四則演算の記号を使って計算できます。
def result = 123456.789G * 111.111G // 自動的にBigDecimalを使って計算される
5.標準の文字列演算APIが限られている
Javaの標準APIの範囲内ではStringのオブジェクトに対して操作可能なメソッドは非常に限られています。ちょっと複雑な処理をしようとするとStringTokenizerなどを使って独自にロジックを作成する必要が出てきます。こうした場合、独自にロジックを作成するのではなく、少なくともCommons Langに含まれているStringUtilsを併用することを考えるべきでしょう。このライブラリーを使えば、
文字列が空文字かnullかを判別するのに
StringUtils.isEmpty(str);
といった記述ができますし、カンマ区切りの文字列を生成するのに
String[] strArray = {"data1", "data2", "data3"}; StringUtils.join(strArray);
のような記述が可能になります。StringUtilsのようなユーティリティークラスの外部メソッドを使った難点は、記述が不自然なことですね。GroovyではStringクラスに以下のようなメソッドを追加することで、より自然で便利に操作できるようになっています。
http://groovy.codehaus.org/groovy-jdk/java/lang/String.html
6.正規表現APIを使うのが面倒
これは以前にJavaで正規表現を扱うのは難しい - 達人プログラマーを目指してでも紹介したことがあるのですが、JDK1.4から追加された正規表現のAPIは非常に使いこなすことが難しいと思います。こうした理由もあるのだと思いますが、本来は正規表現APIを使えば簡単、かつ、効率的に処理できるようなケースであっても独自のループや判定ロジックを書いてしまう人がいます。正規表現の使いやすさもGroovyで大幅に改善されるところですね。以下にチュートリアルがあります。
http://groovy.codehaus.org/Japanese+Tutorial+4+-+Regular+expressions+basics
7.長い文字列の記述方法が面倒
SQLやJPQLの記述など、業務アプリケーションでは長い文字列リテラルをコード中に記述する必要のあるケースが結構あります。通常、Javaの場合は短い文字列に区切ってそれを「+」演算子で結合するという方法をとります。
Sting jpql = "SELECT c FROM Customer " + "WHERE c.name = :name " + "ORDER BY c.id";
もちろん、SQLなどはxmlファイルで管理するなどの方法もありますが、xmlにSQLを書く場合、冗長なタグやCDATAセクションの必要性など、それはそれで結構面倒な場合が多いです。
Groovyには「'''」「"""」という3つのクオーテーションを使って長い文字列を定義する仕組みがあります。この記法を使えば、複数行にわたる長い文字列を簡単に定義できます。
def jpql = ''' SELECT c FROM Customer WHERE c.name = :name ORDER BY c.id '''
8.基本型とラッパークラスの存在
Java言語にはintとInteger、doubleとDoubleのように、基本型と対応する参照型のラッパークラスが存在しています。*6私は今ではこの点は常識というか普通に考えられるようになりましたが、やはり、初めてJavaを学習する初心者の人にとっては理解して正しく使い分けることが難しいところかと思います。基本的な理解としては
- NOT NULL制約のないDBのカラムとマップする時のように、null値を表現する必要がある場合はラッパー型を使う
- MapやListに格納する時にはラッパー型を使う*7
- Integer.parseInt()などのように基本型に付随するメソッドを呼び出したいときはラッパー型のクラスメソッドを使う
- それ以外は基本型を使う
というので良いと思います。この点も歴史上の制約というか、Javaが互換性を捨てない限りやむを得ない問題かもしれません。Java5になって自動的なボックス化、非ボックス化による基本型との相互変換が提供されたので、以前と比べてかなりましにはなっていますが、意図しないNullPointerExceptionの問題*8などもあり注意も必要です。
Groovyだと、基本的に数値リテラルは自動的にラッパークラスに変換されるためより自然に扱えるようになっていますが、基本型には性能面でオブジェクトより遥かに有利なところもあり、画像の処理など大量の数値データを処理しなくてはならないような場合にはなくてはならないものです。性能が求められるケースでは現状のGroovyは不利ですね。
9.配列型とコレクションとの不統一
Java言語はC++の構文をまねて作成されたと言われていますが、演算子オーバーローディングやテンプレート機能*9が無かったこともあり、配列とコレクションライブラリーについては扱いがまったく不統一になっています。まず、誰でも気づくこととして両者で初期化や要素アクセスなど見かけ上の構文上の扱いがまったく異なります。また、配列はlengthフィールドで長さを取り出すのに、Listはsize()メソッドで長さを取得するなど、まるで統一感がありません。
また、そうした表面上の違い以外にも、Genericsを考えると配列とそれ以外のコレクションに対しては大きな不統一があります。配列の型は実際にコンパイル結果のバイトコード中に型情報が埋め込まれます*10。つまり、String[ ]で宣言された変数とInteger[ ]で宣言されたオブジェクトはまったく別の型として扱われます。一方で、List
List<T> list = new ArrayList<T>(); // OK T[] array = new T[3]; // コンパイルエラー「総称型の配列を作成できません」
以上のようになる理由が理解できるでしょうか。配列型は型情報がバイトコード中に埋め込まれるため、newで生成するためには実行時に型情報を知る必要があります。ところが、右辺のnewが実行される段階では型Tの情報は残っていませんので、実行時に適切な型のオブジェクトを生成することができません。つまり、Javaでnew T()という書き方ができないのと同じ理由でnew T[]は許されません。*12しかし、List
Genericsについては、もう一つ大きな違いとして型パラメーターに対して共変性(covariance)のある配列と違いコレクションの型は非変性(invariance)があるという点ですね。*13つまり、
String[] strArray = ...; Object[] objArray = ...; List<String> strList = ...; List<Object> objList = ...; objArray = strArray; // OK objList = strList; // コンパイルエラー「List<String> から List<Object> には変換できません」
ということです。一般に要素の追加が可能なコレクションに対して型安全性を考えると妥当性のある仕様なのですが、時々コンパイルエラーで苦労する箇所ですね。*14
この点、厳密な型安全性を犠牲にする必要がありますが、Groovyは非常によい選択肢になります。基本的にListや配列の初期化や要素アクセスは以下のように統一して記述できます。
// 普通は配列とListの違いを意識する必要はない。 String[] strArray = ['test', 'test2'] List<String> strList = ['test', 'test2'] // インデックスを使ったアクセス ptintln strArray[0] ptintln strList[0] // 内部イテレーターを使ったアクセス strArray.forEach { println it } strList.forEach { println it }
10.互換性の問題に制約されていること
Java言語の場合は、エンタープライズ系で早く採用されたということもありますし、Write Once Run Anywhereの思想があるのかもしれませんが、過去のバージョンとの互換性というのを非常に大切にしています。それは、ある意味でJavaのGood Partだと思いますが、互換性の問題に引きづられて後発の言語と比べて不便な部分が残ってしまっているというところもありますね。
したがって、他に代替手段が提供されているにもかかわらずいまだにVectorやHashtableといったレガシーなクラスがずっと残っているし、前節で説明した型消去によるGenericsの機能制限など不便なところも出てきています。
また、JavaEEではほとんどだれも使わないようなAPIが残存していることにより実装が大変になったり仕様が膨れ上がってきています。JavaEE6では仕様の間引き(Pruning)という仕組みができたので、将来のバージョンでは思い切って仕様がスリム化される可能性もありますね。
同時に、古くて使われなくなったAPIについては、今後段階的に削除することも検討されています。これはプルーニング(Pruning)と呼ばれており、現時点ではEJB 2.xのエンティティBeanやJAX-RPCが候補に上がっています。
Java EE 6 時代のアプリケーション開発: 第1回 Java EE 6概要と開発・実行環境の導入
(追記)
本エントリではGroovyを中心にした改善案を書いていますが、id:xuweiさんがScalaを使った方法についてまとめてくださっていますので、参考になると思います。Javaの10個のBad Partsのほとんどはscalaだと解決されちゃうんだぜ - xuwei-k's blog
*1:Xstreamなど最近ではXMLを扱う便利なライブラリーが利用できるため、DOMやSAXを直接使う必要性はほとんどなくなりました。
*2:初心者の人は安易にExceptionやThrowableの全体をキャッチするコードを書いてしまうこともあるのですが、これでは適切な例外処理がされなくなるということで逆効果になっていまいます。
*3:気に入った方法が、あらゆるところで利用できると思い込むこと。
*4:コーディング作業から遠ざかっているhands-offアーキテクトは、古い教科書の価値観に縛られて、こういったプログラマーの苦しみが分からないこともある。以下も参照。アーキテクトもプログラミングするべきか? - 達人プログラマーを目指して
*5:C#など.NETの言語ではusing文というのがあり、遥かに簡単に記述できます。
*6:兄弟言語であるC#ではラッパー型はないが代わりにint?などのnullable型がある。Scalaだとnullを使わずOption型で表現できる。
*7:Java5では型宣言の部分以外では見かけ上基本型を扱っているようにコーディングできる。ただしList< int >のような宣言はできず、実際にIntegerに変換されて格納される。この点ではC#のGenericsと大きく異なっている。
*8:たとえば、Integer型にnullが入っており、これが自動的にintに変換される際にNullPointerExceptionになる
*9:後付けされたGenericsに記述方法は近いが、概念的にはまったく異なるものです。
*10:バイトコード中に埋め込まれる型はreified typeと呼ばれます。
*11:この点は誤解されやすいのですが、総称型を宣言して利用する側のクラス定義中にはGenericsの型がメタ情報として存在しています。たとえば、フィールドの型がList
*12:この制約に対する次善の策としてClassクラスをタイプトークンとして利用して、リクレクションAPIでインスタンスを生成することができます。以下を参照。http://blog.isocchi.com/2009/02/new.html
*13:共変性とは配列のように型パラメータの継承関係と配列自身の型の継承関係が同一であることを言います。つまり、ChildがParentのサブクラスであればChild[ ]はParent[ ]のサブクラスという関係です。しかし、コレクションには共変性がないため、List
*14:Javaでは配列の共変性により、コンパイル時に完全には型安全性を保障できません。たとえば、String[ ]のインスタンスを参照する配列がObject[ ]型の変数に代入された場合、実際にIntegerを設定できてしまいますが、実行時にArrayStoreExceptionになる可能性を防げません。普通の(業務)Javaアプリケーションでは配列をなるべく使用しない方がよい - 達人プログラマーを目指して