細かすぎて伝わらないJava7の変更点

本日のエントリーはJava Advent Calendarの25日目です。昨日は @mike_neck さんのmike、mikeなるままに…: hamcrestを拡張してmoreThanとか作ってみたでした。本日はクリスマスですが、Advent Calendarはまだまだ続きます。明日はt.ogisawaさんのhttp://webinter.sakura.ne.jp/pbd/210です。

さて、今年はJava7がリリースされましたね。try-with-resources、diamond operator、invoke dynamic、Folk/Join framework…魅力的な新機能の数々が、多くの人によって紹介されています。が、Java7の変更はそれだけじゃないはず。小粒だが、キラリと光る変更がきっとあるはず。ということで、Java6とJava7のsrc.zipを比較してみました。小ネタなのでサラっと読んで頂ければと思います。

まず、比較に用いたのは以下の通り。


で、まず普通にdiffを掛けてみたのだが、意外とdocコメント上で頑張っていることが判明。docコメント内のcodeタグやttタグ、preタグの多くを{@code}や{@link}で書き直してある部分が目立つ。まぁ、未だにcodeタグのままの部分も多いのだが、頑張ったよね。個人的にはHTMLタグよりも読みやすいと思っているので、嬉しいです。

あと、Java6までのソースは、インデントがTABだったりSPACEだったりまちまちだった。そしてどうやらTAB幅8としているようで、TAB幅4の環境で見るとそれはそれは残念な感じになってしまう。もうこの辺りのコーディングスタイルに関しては、標準APIであるにも関わらずもう壊滅的でしたよね。自分のコーディング規約やJavadocの書き方ルール策定の参考にしようと思って愕然とした記憶がある。しかし、Java7では、インデントがSPACEに直っているのが目立つ。未確認だが、全部統一されているのかもしれない。喜ばしい。

といった所の差異までdiffで見えてしまうと、もはやノイズでしかないので、ここは思い切って両者のソースをいじってしまいます。

find . -type f | xargs sed -e 's/\<code\>\([^<]*\)\<\/code\>/{@code \1}/g' -i ""
find . -type f | xargs sed -e 's/\<tt\>\([^<]*\)\<\/tt\>/{@code \1}/g' -i ""

両者のソースに対して、上記の置換を掛けた後、俺俺コードフォーマッタを掛けてからdiffに挑みました。で、com.sunパッケージなんかの差を見始めてもアレなので、ひとまず java.* パッケージに絞って、そして都元が個人的に気になったポイントを中心に、以下にご紹介しまーす。

スペルミスや細かいバグフィックス

まずは軽く。Java6と7の比較、という視点ではありませんが、結構色々直していますね。

 * @throw new NullPointerException

なんていうdocコメントがあったり。勢いでnewって書いちゃったんだろうなーw

public Foo {
  // …
  private void Foo() {}
}

なんてやっちゃってるクラスもありました。

古いコーディングスタイルの刷新

Java6のソースにはstatic publicという語順(?)や、char foo[]; のような配列宣言、rawtype型など、古い書き方が内部に随分残ってます。こういった所がちょいちょい直してありました。あと、ダイアモンドオペレータもきっちり使われてましたよ。

各Exception実装クラスにserialVersionUIDが追加

ExceptionはSerializableのサブタイプです。従って、例外の実装クラスには全てserialVersionUIDを記述するのが望ましいんですね。まぁ、実装の詳細の話ではありますが、各ExceptionにserialVersionUIDが追加されてます。

Byte/Integer/Long/BigIntegerの文字列paese

int i1 = Integer.parseInt("3");
int i2 = Integer.parseInt("-6");
int i3 = Integer.parseInt("+2");

皆さん、このコード実行するとどうなると思います? 実は、Java6だと "+2" は NumberFormatException になってしまうのです。文字列の整数parseにおいて、プラス記号は今まで使えませんでした。これが、Java7からは普通に通るようになります。

ちなみに、DoubleやFloatのparseでは、Java6でもプラス記号が使えます。

primitive wrapper class

Integer.compare(10, 15);

というような、比較ロジックがstaticメソッドとして提供されるようになりました。プリミティブラッパー型にそれぞれ定義されています。比較ロジックを引き算で実装してバグを出してしまう位なら、このユーティリティメソッドに委譲してしまうのがよいですね。

比較ロジックの引き算実装については下記参考書籍の「パズル65」を参照。

Java Puzzlers 罠、落とし穴、コーナーケース

Java Puzzlers 罠、落とし穴、コーナーケース

ComparableとComparator

従来、Comparable#compareToやComparator#compareの引数にnullを渡した時の仕様は「未定義」でした。また、私は基底型のjavadocに明示してある例外しか投げないようにしているため、「nullとは比較できないComparable」を実装したい時、とても気持ち悪い思いをしていました。

そんな中、Java7ではこれらの比較メソッドのjavadocに、NPEの記述が追加されました。

@throws NullPointerException if the specified object is null

引数がnullな時はNPEを投げてよくなったのですね。

Collections

以前から、Collectionsクラスには emptyList() 等のメソッドがありましたが、似たような感じで以下のメソッドが追加になりました。大したことではありませんが、使う機会があれば使った方が良いですね。

  • emptyEnumeration()
  • emptyIterator()
  • emptyListIterator()

Objets

新しいユーティリティクラスです。requireNonNullやnull-safeなequals/hashCode/toStringなど、小粒ながら使い勝手は良さそうです。

ReflectiveOperationException

従来、リフレクションAPIを利用しようとすると数多くのチェック例外を処理する必要がありました。NoSuchMethodException, InvocationTargetException, ClassNotFoundException, IllegalAccessException… 実際は発生しないと踏んでいる例外をこんなに大量にキャッチさせられるのはストレスでしたね。しかし、Java7からはReflectiveOperationExceptionという基底クラスが定義されました。これによって、リフレクション操作時の例外をまとめてキャッチできます。

AutoCloseable

try-catch-resources用に出て来た新しいインターフェイスです。Closeableはもちろん、Connection/ResultSet/Statementもこいつのサブタイプに。

従来はJDBC API用の closeQuietly 的なユーティリティを、I/O とは別に書かなければなりませんでしたが、今度からまとめられますね。

ThreadLocalRandom

っていうクラスが追加になっています。まぁ、内容は名前から想像できる通りです。

Scanner

ScannerがCloseableのサブタイプになりました。今までCloseableじゃなかったのは、単に忘れてただけなのでしょうかw

Integer, Short, LongのvalueOf

Sun(Oracle)のJavaにおいて、Integer型のインスタンスは-128〜127の値がキャッシュされる、というのは有名な(?)話でした。しかし、このキャッシュは「Sun(Oracle)の実装がたまたまそうなっている」だけであり、Javaの仕様ではないものでした。つまり、別のJava実装(例えばIBM Javaとか?)ではキャッシュをしていないかもしれません。キャッシュの範囲が違うかもしれません。

という状況だったのですが。Java7のjavadocには以下のような記述が追加されています。

【Integer#valueOf及びShort#valueOfより】
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
【Long#valueOfより】
     * Note that unlike the {@linkplain Integer#valueOf(int)
     * corresponding method} in the {@code Integer} class, this method
     * is <em>not</em> required to cache values within a particular
     * range.

これって…。Javadocに明示されたということは、キャッシュの挙動も含めて仕様化したということで良いんでしょうかね。

System

System#lineSeparator() っていうメソッドが追加。プラットフォーム依存の改行文字(列)をさくっと手に入れられて便利ですね。

また、Systemクラス内の in, out staticフィールドの初期化方法が変わったようです。Java6までは、staticフィールドが参照元クラスにインライン展開されないように、ちょっとしたハックがされていましたが、Java7では直接null初期化しています。これはインライン展開されなくなったんですかね。そこまでは追いきれませんでした。

暦システム関連

week yearのサポートが手厚くなってます。GregorianCalendarに以下の3メソッドが追加になりました。

  • getWeeksInWeekYear()
  • getWeekYear()
  • isWeekDateSupported()


って何の事だかわからんですね。えーと。「1週間は月曜〜日曜である」として、「2011年の第1週」って何日から何日だと思いますか? まぁこのネタはこちらが詳しいので参照してください。 → 今が年の何週目か - 気付いたとき、気が向いたとき。by ykhr

で、結論としては、ISO8601的には1/3〜1/9が「2011年の第1週」ということになります。ということは、2010/12/27〜2011/01/02は「2010年の第52週」なんですね。デフォルトのGregorianCalendarの挙動は「1週間は日曜始まりで、1/1を含むのが第1週」ということにになっていますが、それをISO的に「1週間は月曜始まりで、1/4を含むのが第1週」という設定をするのが先ほどの id:ykhr-kokko 氏のエントリです。

で、ここで注目したいのは「2011/01/02は、2011年の日付であるにも関わらず、週レベルで見ると2010年に属する」ということです。めんどくさいですねw 今日は何年の第何週なのかが知りたいとします。第何週かは cal.get(Calendar.WEEK_OF_YEAR) で良いでしょう。では「ある日付を与えた時、それは週レベルでは何年に属するのか?」をどうやって取るのか。これが getWeekYear() です。

とは言え、大抵 Calendar.YEAR と一致してますから、年末年始だけ気をつけておけば簡単に計算できるんじゃね? …と思いきや、正確にやり出すと結構大変みたいです。1582年以前は、今のグレゴリオ暦ではなくユリウス暦が…詳しくは getWeekYear() の実装をご覧下さい。

んでまぁ、上記に関連してSimpleDateFormatに新しいパターンが追加になりました。Y, u, X の3つですが、このYってのがweek yearです。uとXは大した事ないので気になったら各自ググってくださいw

ちなみに「1週間は月曜始まりで、1/4を含むのが第1週」というルールを知っているのはCalendarですから、下記の通りSimpleDateFormatにcalを与えてやらないと、ISOのルール通りには動きませんので注意しましょう。

Calendar cal = new GregorianCalendar();
cal.setMinimalDaysInFirstWeek(4);
cal.setFirstDayOfWeek(Calendar.MONDAY);
SimpleDateFormat f = new SimpleDateFormat("yyyy/MM/ddはYYYY年の第ww週です。");
f.setCalendar(cal);
System.out.println(f.format(new Date()));


…と、まぁdiffをしてみて気づいたあれやこれやを並べてみました。明日以降も引き続き、Java Advent Calendarでお楽しみください :)