都元ダイスケ IT-PRESS このページをアンテナに追加 RSSフィード

最近は会社ブログしか書いてません。

2014-04-01

[]クラスメソッド株式会社に転職しました!

都元ダイスケ(当時34)は、クラスメソッド株式会社に転職しました。

こんだけJavaJavaしてた都元が、なんとAWSエンジニアになっております。世の中どうなるかわからんですね〜。まぁとは言え、ちょいちょいJava触ってますが。

そんなわけで、今後共よろしくお願いします!












って1年以上前の話やけどな。

2011-12-25

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

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

さて、今年は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」を参照。


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初期化しています。これはインライン展開されなくなったんですかね。そこまでは追いきれませんでした。


暦システム関連

f:id:daisuke-m:20111225165712p:image:right

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でお楽しみください :)

2011-11-26

[][]Spring 3.1 の Cache Abstraction(キャッシュの抽象化)

しばらくコード付きのエントリ書いてないなぁ、と思ったので。Springの新機能についてひとつ。

Spring3.1は、まだリリース版は出ていないのだけど、RC1が出ている。(参考 Spring 3.1 RC1リリース

その新機能にCache Abstraction(キャッシュの抽象化)ってのがあって、色々調べてみた。例えばWebAPIなんかを叩いて情報を取ってくるようなメソッドは、情報があまり変化しないものであればキャッシュしちゃえばいいよね。例えば Amazon API で、ASINから商品名やら何やかんやを取ってくるメソッドとか。

下準備

package jp.xet.sample;

public interface EntityRepository {
    
  String get(int id);
    
  void put(int id, String value);
    
}

例えばこんな(↑)インターフェイスがあって、このgetのコストが高いとしましょう。で、今回使うサンプルの実装がコレ(↓)。コストの高さをThread.sleepで表現してみました。実際は単なるMapストレージなんだけども。

package jp.xet.sample;

import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
public class EntityRepositoryImpl implements EntityRepository {
    
  private Map<Integer, String> storage = new HashMap<Integer, String>();
    
    
  @Override
  public String get(int id) {
      
    // simulate slow operation
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      throw new AssertionError(e);
    }
      
    return storage.get(id);
  }
    
  @Override
  public void put(int id, String value) {
    storage.put(id, value);
  }
}

で、こいつ(↑)には@Repositoryアノテーションがついている。まぁ@Componentと全く一緒*1らしい。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd">

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

で、jp.xet.sampleパッケージ以下のアノテーション付きのコンポーネントをcontext.xmlで読んでもらう、と。まぁ、EntityRepositoryImplしかないんですが。そんなわけで、Mainクラスいきましょう。

package jp.xet.sample;

import org.apache.commons.lang.time.StopWatch;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    
    
  public static void main(String[] args) {
    ApplicationContext ctx =
      new ClassPathXmlApplicationContext("/context.xml");
    EntityRepository repos = ctx.getBean(EntityRepository.class);

こうして出来たコンテナからは、EntityRepositoryが取り出せる。あとは、適当にデータを置いて、時間をはかってみる。

    repos.put(1, "one");
    repos.put(2, "two");
    repos.put(3, "three");
    
    StopWatch sw = new StopWatch();
    sw.start();
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
    
    repos.put(1, "壱");
    repos.put(3, "参");
    
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(1), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(2), sw.toString());
    System.out.format("time=%2$s, value=%1$s%n",
        repos.get(3), sw.toString());
  }
}
time=0:00:01.002, value=one
time=0:00:02.012, value=two
time=0:00:03.014, value=one
time=0:00:04.014, value=two
time=0:00:05.015, value=three
time=0:00:06.016, value=壱
time=0:00:07.018, value=two
time=0:00:08.018, value=壱
time=0:00:09.019, value=two
time=0:00:10.020, value=参

だいたい、1getにつき1秒掛かってますね。

このget処理をキャッシングして、高速化しよう

まずcontext.xmlに手をいれましょう。キャッシュの設定。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:cache="http://www.springframework.org/schema/cache"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">

 <cache:annotation-driven />
 <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
   <set>
    <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
     <property name="name" value="default"/>
    </bean>
   </set>
  </property>
 </bean>

 <context:component-scan base-package="jp.xet.sample"/>

</beans>

この設定は、裏ではシンプルに普通のConcurrentHashMapに値をキャッシュする設定です。この他に、springではehcacheにも対応したりしてるらしい。で、ここでは「default」という名前のキャッシュを1つ作りました、ということになります。次にEntityRepositoryImplに @Cacheable アノテーションをつける。

  @Cacheable(value = "default")
  public String get(int id) {
    // ...
  }

こんな感じ。このメソッドの結果はキャッシュして、キャッシュヒットした場合は中身は実際には呼ばずに済ますよ、という意味です。この状態でもっかいMainを実行してみよう。

time=0:00:01.004, value=one
time=0:00:02.016, value=two
time=0:00:02.017, value=one
time=0:00:02.017, value=two
time=0:00:03.018, value=three
time=0:00:03.018, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=one
time=0:00:03.019, value=two
time=0:00:03.019, value=three

最初のone,twoには1秒ずつ掛かっているけど、その次のone,twoは一瞬で終わっている。threeは初アクセスなので再び1秒掛かってる。で、残りはもう全部キャッシュに載っているのでずばーーーっと。計約3秒。すばらしい。

漢数字はどうした

    repos.put(1, "壱");
    repos.put(3, "参");

途中でoneとthreeの値を書き換えたのだが、無視してキャッシュを返しちゃってますね。これを何とかしたい。ならば@CacheEvictアノテーションだ。

  @CacheEvict(value = "default", key = "#id")
  public void put(int id, String value) {
    // …
  }

このアノテーションがついたメソッドが呼ばれた時は、特定のキャッシュエントリを無効にします、っていうアノテーションだ。で、どのキャッシュエントリを無効にするの? ってのが key パラメータ。ここでは「idって名前の引数を利用します」ってことです。この表記法はSpEL(Spring Expression Langage)参照。

ちなみに、@CacheEvict(value = "default", allEntries = true) とすると、全キャッシュエントリをクリアしてくれる。

という対策を施し、再びMainを回す。

time=0:00:01.001, value=one
time=0:00:02.011, value=two
time=0:00:02.012, value=one
time=0:00:02.012, value=two
time=0:00:03.013, value=three
time=0:00:04.014, value=壱
time=0:00:04.014, value=two
time=0:00:04.015, value=壱
time=0:00:04.015, value=two
time=0:00:05.015, value=参

キャッシュのevictが上手く動いていることが分かります。

*1:ざっくりとしたコンポーネントには@Componentを付ければいいんだけど「こいつはコンポーネントだけど、特にリポジトリなんだよ」っていう気分で@Repositoryってのが用意されている。あとは@Serviceなんてのもある。気分で使い分ければよろし。

2011-11-20

[]nullを受け付けないメソッドにnullを渡した時の挙動

っていう話を書こうと思ってたら、以前書いてた(cf. 前提条件を破った場合、どのような挙動をするのか? - 都元ダイスケ IT-PRESS

上のエントリにも書いたのだが、自分はこういう時IllegalArgumentExceptionを使うようにしている。そして、commons-lang v2.6 をよく使っていたので、その中の Validate#notNull を利用してnullチェックをするのが自分のスタイルだ。

しかし、いい加減commons-lang v3系に移行しようかな、と考えて色々触っていたのだが、v3から Validate#notNull の仕様が変わり、NullPointerExceptionを投げるようになったのだ。

といった辺りの俺周辺タイムライン → Javaでnullを受け付けないメソッドにnullを渡したらどうなるべきか - Togetter

で、いくつかのライブラリの引数チェックユーティリティの仕様を調べてみたのだが、以下の通り、統一感がない。。。

Apache commons-lang v2.6 Validate#notNull IllegalArgumentException
Apache commons-lang v3.1 Validate#notNull NullPointerException
Spring spring-core v3.0.5 Assert#notNull IllegalArgumentException
Google guava v10.0.1 Preconditions#checkNotNull NullPointerException

最後に、Facebook Questionsを利用してアンケートを取ってみたところ…。IAEの圧勝。

http://www.facebook.com/questions/309546165723428


色々考えて出した結論

自分が引数チェックを執拗に行うのは、アプリケーションコードというよりも、汎用ライブラリコード側だ。そういったライブラリについては、事実上Validateを使うためだけにcommonsに依存している状態だ。別に自分はcommonsにこだわりがある訳じゃない。

むしろ、汎用ライブラリはなるべく依存ライブラリが無い方が良いだろう。

というわけで、…ライブラリ毎にオレオレ引数チェックユーティリティクラスをIAE版として作って、コピーして使うのがいいのかな、と思った。

2011-06-10

[]オブジェクト指向のソースを読むのが難しい理由

ダラダラ書かない予定だよ。ざっくり行くよ。あと、分かってる人には当たり前な事だと思うよ。


あるクラスについて知りたかったら、まずその基底クラスを知れ

例えば、Integerクラスについて知りたいと思ったら、Integer.java だけを読んでいてはダメだ。確かに「Integerに特化した責務・構造・操作」は読み取れるかもしれないが、数値としての基本的な責務・構造・操作はNumberに書かれている。それを読まずして、Integerが保つ数値という一面を知ることはできない。Integer.javaには「Integer - Number」*1の情報しか書いてないのだよ。差分プログラミング。

さらに、忘れちゃいけない。Object.javaを読め。全ての道は暗黙的にObjectにつながっている。Objectを知らずしてJavaのクラスを知る事は絶対にできない。Objectなんて、みんな「知った気」になってるんじゃなかろうか*2。あと、クラスのextends関係だけではなく、当然インターフェイスのimplementsも必須。

ソースを読むのが難しいのは、出来るだけソースを読みたくないから「そのファイルだけに読む範囲を絞ろう」という甘えがあるからだ。知りたいクラスが使用*3しているクラスをも知るに越した事はないが、時間的制約もあるだろうから、せめて基底は押さえておく、という方針が良いのではないかとおもっちょる。


あるクラスについて知りたかったら、まずそのドキュメントを読め

トークンの並びを理解する前に、そのトークン列が何を達成しようとしてるのかを知らないと。というわけで、最初に読まなきゃいけないのは、実は「コード」じゃなくて「ドキュメンテーションコメント(Javadoc)」だ。

当然、メソッドやフィールドについているコメントも重要だが、もうほんとに最初に読むのは「型(classとかinterface等)に対するドキュメント」ね。結構分かった風になっちゃってるから飛ばすんだよね、みんな。

例えばStringBuilder。みんな、これを「文字列(string)組み立て(build)屋(er)」だと思ってるでしょ。まぁ、間違っちゃいないし、初心者連載では俺もそう説明しているんだけど。しかし、このクラスの第一義は「可変(mutable)な、文字(character)の、列(sequence)」だ。「屋(er)」を表すんじゃなくて、「列(sequence)」を表すクラスだったりする。Javadoc見りゃ1行目に書いてあることさ。

(あとはStringをネタにして深淵に潜り込むのもよし。これは本当に「文字列」か? 文字(character)の列(sequence)ならば、CharSequenceではないのか? そういう型もあるぞ? とか言って。あとはもうどうにでもしてクダサイ。俺はめんどくさいので燃料だけ投下して離脱しますw)

で、まぁこの例だと「だから何だ」になりかねないが。みんな名前だけで仕様と責務を想像して、その上で実装読んでたりしない? じゃなくて、ドキュメントで仕様と責務を把握した上で実装読むんだよ。

ソースを読むのが難しいのは、「英語を読みたくない」という甘えがあるからだ。英語ドキュメント読むよりトークン列を読むほうが楽だと、みんな思っているんだよね。僕にはわけがわからないよ。

*1:引き算ね。

*2:toString, equals, hashCode辺りが有名すぎてナメられがち。本当に知ってる?

*3:compositionとかuseとか。

2011-03-20

[]今度はMahoutでクラスタリング(ソース編)

Mahoutシリーズを最初から読む場合はこちらApache Mahoutで機械学習してみるべ - 都元ダイスケ IT-PRESS。前回はこちら今度はMahoutでクラスタリング - 都元ダイスケ IT-PRESS

準備

まずmvnの依存設定を。以前と同じようにmahout-coreは要ります。それに加えて*1slf4jとlogback*2、そしてcommons-io*3を入れておきます。

pom.xml
    <dependency>
      <groupId>org.apache.mahout</groupId>
      <artifactId>mahout-core</artifactId>
      <version>0.4</version>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${lib.slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${lib.slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${lib.logback.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${lib.logback.version}</version>
    </dependency>
  
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.0</version>
    </dependency>
  
...

  <properties>
    <lib.slf4j.version>1.6.0</lib.slf4j.version>
    <lib.logback.version>0.9.21</lib.logback.version>
  </properties>
logback.xml

で、ログ設定ファイルこんなんをsrc/main/resouces直下に置いておきましょう。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <Target>System.out</Target>
    <layout class="ch.qos.logback.classic.PatternLayout">
      <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
    </layout>
  </appender>

  <root>
    <level value="INFO" />
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Javaソース

やっと本質的なトコはいりますよー。とりあえず、サンプルコードでは、最後の3次元ベクトルをクラスタリングしてみましょう。

まずはクラスタリング対象のベクトル群を用意します。ここでは前回の3Dベクトル9つを使います。

static final double[][] points = {
    {8, 8, 8}, {8, 7.5, 9}, {7.7, 7.5, 9.8},
    {0, 7.5, 9}, {0.1, 8, 8}, {-1, 9, 7.5},
    {9, -1, -0.8}, {7.7, -1.2, -0.1}, {8.2, 0.2, 0.2},
};

で、今回のクラスタリングには k-means clastering という手法を使います。この手法では、あらかじめ「最終的にいくつのクラスタを作るのか」、という k の値を決めなければなりません。ここでは k = 3 として、3つのクラスタを作る前提でいきます。

Mahoutのクラスタリングでは、いきなりHadoopが出て来ます。とは言え、Hadoopクラスタを組む必要はなく、standaloneで走らせることはできます。その際「クラスタリングの対象となる9つのベクトル」と「3つのクラスタ」をあらかじめHDFS上にファイルとして配置する必要があります。これを writePointsToFile と writeClustersToFile メソッドで行っています。

そしてクラスタリングの処理を実行。クラスタリングの計算は、HDFSからデータを読み込み、そして結果もHDFSに書き込みます。従って、計算後にはHDFSを読み出す処理として readClusteredPointsFromFile を実行しています。

public static void main(String args[]) throws Exception {
    int k = 3;
    List<Vector> vectors = getPoints(points);
    Configuration conf = new Configuration();
    FileSystem fs = FileSystem.get(conf);
   
   // HDFSにベクトルとクラスタを書き込む
    writePointsToFile(vectors, "target/input/points/file1", fs, conf);
    writeClustersToFile(k, vectors, "target/input/clusters/part-00000", fs, conf);
   
   // クラスタリングを実行
    Path pointsPath = new Path("target/input/points");
    Path clustersPath = new Path("target/input/clusters");
    Path outputPath = new Path("target/output");
    KMeansDriver.run(conf, pointsPath, clustersPath, outputPath,
            new EuclideanDistanceMeasure(), 0.001, 10, true, false);
    
    // クラスタリングの結果をHDFSから読み出し、コンソールに表示する
    readClusteredPointsFromFile(fs, conf);
}

static List<Vector> getPoints(double[][] raw) {
    List<Vector> points = new ArrayList<Vector>();
    for (double[] fr : raw) {
        Vector vec = new RandomAccessSparseVector(fr.length);
        vec.assign(fr);
        points.add(vec);
    }
    return points;
}

static void writePointsToFile(List<Vector> points, String fileName, FileSystem fs, Configuration conf)
        throws IOException {
    Path path = new Path(fileName);
    SequenceFile.Writer writer = null;
    try {
        writer = new SequenceFile.Writer(fs, conf, path, LongWritable.class, VectorWritable.class);
        long recNum = 0;
        VectorWritable vec = new VectorWritable();
        for (Vector point : points) {
            vec.set(point);
            writer.append(new LongWritable(recNum++), vec);
        }
    } finally {
        IOUtils.closeQuietly(writer);
    }
}

static void writeClustersToFile(int k, List<Vector> vectors, String fileName, FileSystem fs, Configuration conf)
        throws IOException {
    Path path = new Path(fileName);
    SequenceFile.Writer writer = null;
    try {
        writer = new SequenceFile.Writer(fs, conf, path, Text.class, Cluster.class);
        for (int i = 0; i < k; i++) {
            Vector vec = vectors.get(i);
            Cluster cluster = new Cluster(vec, i, new EuclideanDistanceMeasure());
            writer.append(new Text(cluster.getIdentifier()), cluster);
        }
    } finally {
        IOUtils.closeQuietly(writer);
    }
}

static void readClusteredPointsFromFile(FileSystem fs, Configuration conf) throws IOException {
    Path path = new Path("target/output/" + Cluster.CLUSTERED_POINTS_DIR + "/part-m-00000");
    SequenceFile.Reader reader = null;
    try {
        reader = new SequenceFile.Reader(fs, path, conf);
        IntWritable key = new IntWritable();
        WeightedVectorWritable value = new WeightedVectorWritable();
        while (reader.next(key, value)) {
            System.out.println(value.toString() + " belongs to cluster " + key.toString());
        }
    } finally {
        IOUtils.closeQuietly(reader);
    }
}

参考までに、importはこちら。同じ単純名のクラスが意外とある。

import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.apache.mahout.clustering.WeightedVectorWritable;
import org.apache.mahout.clustering.kmeans.Cluster;
import org.apache.mahout.clustering.kmeans.KMeansDriver;
import org.apache.mahout.common.distance.EuclideanDistanceMeasure;
import org.apache.mahout.math.RandomAccessSparseVector;
import org.apache.mahout.math.Vector;
import org.apache.mahout.math.VectorWritable;

結果

クラスタリングの結果は以下の通り。それぞれのベクトルが cluster 0 〜 cluster 2 に分類されていることが分かると思います。

1.0: [8.000, 8.000, 8.000] belongs to cluster 1
1.0: [8.000, 7.500, 9.000] belongs to cluster 1
1.0: [7.700, 7.500, 9.800] belongs to cluster 1
1.0: [1:7.500, 2:9.000] belongs to cluster 2
1.0: [0.100, 8.000, 8.000] belongs to cluster 2
1.0: [-1.000, 9.000, 7.500] belongs to cluster 2
1.0: [9.000, -1.000, -0.800] belongs to cluster 0
1.0: [7.700, -1.200, -0.100] belongs to cluster 0
1.0: [8.200, 0.200, 0.200] belongs to cluster 0

参考までに、結果を出す前にだーーっと流れるログはこんな感じ。Hadoopのジョブとして動いているのが分かると思います。

22:16:11.733 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - Input: target/input/points Clusters In: target/input/clusters Out: target/output Distance: org.apache.mahout.common.distance.EuclideanDistanceMeasure

22:16:11.738 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - convergence: 0.0010 max Iterations: 10 num Reduce Tasks: org.apache.mahout.math.VectorWritable Input Vectors: {}
22:16:11.739 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - K-Means Iteration 1
22:16:11.768 [main] INFO  o.a.hadoop.metrics.jvm.JvmMetrics - Initializing JVM Metrics with processName=JobTracker, sessionId=
22:16:11.878 [main] INFO  org.apache.mahout.common.HadoopUtil - Deleting target/output/clusters-1
22:16:11.885 [main] WARN  org.apache.hadoop.mapred.JobClient - Use GenericOptionsParser for parsing the arguments. Applications should implement Tool for the same.
22:16:12.497 [main] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:12.781 [main] INFO  org.apache.hadoop.mapred.JobClient - Running job: job_local_0001
22:16:12.787 [Thread-14] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:12.880 [Thread-14] INFO  org.apache.hadoop.mapred.MapTask - io.sort.mb = 100
22:16:17.532 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 0% reduce 0%
22:16:17.534 [Thread-14] INFO  org.apache.hadoop.mapred.MapTask - data buffer = 79691776/99614720
22:16:17.535 [Thread-14] INFO  org.apache.hadoop.mapred.MapTask - record buffer = 262144/327680
22:16:17.646 [Thread-14] INFO  org.apache.hadoop.mapred.MapTask - Starting flush of map output
22:16:18.047 [Thread-14] INFO  org.apache.hadoop.mapred.MapTask - Finished spill 0
22:16:18.051 [Thread-14] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0001_m_000000_0 is done. And is in the process of commiting
22:16:18.055 [Thread-14] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:18.055 [Thread-14] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0001_m_000000_0' done.
22:16:18.064 [Thread-14] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:18.072 [Thread-14] INFO  org.apache.hadoop.mapred.Merger - Merging 1 sorted segments
22:16:18.087 [Thread-14] INFO  org.apache.hadoop.mapred.Merger - Down to the last merge-pass, with 1 segments left of total size: 239 bytes
22:16:18.087 [Thread-14] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:18.184 [Thread-14] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0001_r_000000_0 is done. And is in the process of commiting
22:16:18.185 [Thread-14] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:18.186 [Thread-14] INFO  org.apache.hadoop.mapred.TaskRunner - Task attempt_local_0001_r_000000_0 is allowed to commit now
22:16:18.190 [Thread-14] INFO  o.a.h.m.l.output.FileOutputCommitter - Saved output of task 'attempt_local_0001_r_000000_0' to target/output/clusters-1
22:16:18.191 [Thread-14] INFO  o.a.hadoop.mapred.LocalJobRunner - reduce > reduce
22:16:18.192 [Thread-14] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0001_r_000000_0' done.
22:16:18.535 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 100% reduce 100%
22:16:18.535 [main] INFO  org.apache.hadoop.mapred.JobClient - Job complete: job_local_0001
22:16:18.537 [main] INFO  org.apache.hadoop.mapred.JobClient - Counters: 13
22:16:18.537 [main] INFO  org.apache.hadoop.mapred.JobClient -   Clustering
22:16:18.538 [main] INFO  org.apache.hadoop.mapred.JobClient -     Converged Clusters=1
22:16:18.538 [main] INFO  org.apache.hadoop.mapred.JobClient -   FileSystemCounters
22:16:18.538 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_READ=2741232
22:16:18.539 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_WRITTEN=2792502
22:16:18.539 [main] INFO  org.apache.hadoop.mapred.JobClient -   Map-Reduce Framework
22:16:18.539 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input groups=3
22:16:18.540 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine output records=3
22:16:18.540 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map input records=9
22:16:18.541 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce shuffle bytes=0
22:16:18.541 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce output records=3
22:16:18.541 [main] INFO  org.apache.hadoop.mapred.JobClient -     Spilled Records=6
22:16:18.542 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output bytes=675
22:16:18.542 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine input records=9
22:16:18.543 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output records=9
22:16:18.543 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input records=3
22:16:18.547 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - K-Means Iteration 2
22:16:18.548 [main] INFO  o.a.hadoop.metrics.jvm.JvmMetrics - Cannot initialize JVM Metrics with processName=JobTracker, sessionId= - already initialized
22:16:18.576 [main] INFO  org.apache.mahout.common.HadoopUtil - Deleting target/output/clusters-2
22:16:18.578 [main] WARN  org.apache.hadoop.mapred.JobClient - Use GenericOptionsParser for parsing the arguments. Applications should implement Tool for the same.
22:16:19.072 [main] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:19.630 [main] INFO  org.apache.hadoop.mapred.JobClient - Running job: job_local_0002
22:16:19.632 [Thread-28] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:20.622 [Thread-28] INFO  org.apache.hadoop.mapred.MapTask - io.sort.mb = 100
22:16:20.719 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 0% reduce 0%
22:16:22.272 [Thread-28] INFO  org.apache.hadoop.mapred.MapTask - data buffer = 79691776/99614720
22:16:22.273 [Thread-28] INFO  org.apache.hadoop.mapred.MapTask - record buffer = 262144/327680
22:16:22.321 [Thread-28] INFO  org.apache.hadoop.mapred.MapTask - Starting flush of map output
22:16:22.323 [Thread-28] INFO  org.apache.hadoop.mapred.MapTask - Finished spill 0
22:16:22.326 [Thread-28] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0002_m_000000_0 is done. And is in the process of commiting
22:16:22.327 [Thread-28] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:22.327 [Thread-28] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0002_m_000000_0' done.
22:16:22.358 [Thread-28] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:22.360 [Thread-28] INFO  org.apache.hadoop.mapred.Merger - Merging 1 sorted segments
22:16:22.360 [Thread-28] INFO  org.apache.hadoop.mapred.Merger - Down to the last merge-pass, with 1 segments left of total size: 239 bytes
22:16:22.361 [Thread-28] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:22.428 [Thread-28] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0002_r_000000_0 is done. And is in the process of commiting
22:16:22.429 [Thread-28] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:22.430 [Thread-28] INFO  org.apache.hadoop.mapred.TaskRunner - Task attempt_local_0002_r_000000_0 is allowed to commit now
22:16:22.434 [Thread-28] INFO  o.a.h.m.l.output.FileOutputCommitter - Saved output of task 'attempt_local_0002_r_000000_0' to target/output/clusters-2
22:16:22.435 [Thread-28] INFO  o.a.hadoop.mapred.LocalJobRunner - reduce > reduce
22:16:22.436 [Thread-28] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0002_r_000000_0' done.
22:16:23.265 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 100% reduce 100%
22:16:23.266 [main] INFO  org.apache.hadoop.mapred.JobClient - Job complete: job_local_0002
22:16:23.266 [main] INFO  org.apache.hadoop.mapred.JobClient - Counters: 12
22:16:23.267 [main] INFO  org.apache.hadoop.mapred.JobClient -   FileSystemCounters
22:16:23.267 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_READ=5484503
22:16:23.267 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_WRITTEN=5583630
22:16:23.267 [main] INFO  org.apache.hadoop.mapred.JobClient -   Map-Reduce Framework
22:16:23.268 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input groups=3
22:16:23.268 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine output records=3
22:16:23.268 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map input records=9
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce shuffle bytes=0
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce output records=3
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Spilled Records=6
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output bytes=675
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine input records=9
22:16:23.269 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output records=9
22:16:23.270 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input records=3
22:16:23.273 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - K-Means Iteration 3
22:16:23.274 [main] INFO  o.a.hadoop.metrics.jvm.JvmMetrics - Cannot initialize JVM Metrics with processName=JobTracker, sessionId= - already initialized
22:16:23.289 [main] INFO  org.apache.mahout.common.HadoopUtil - Deleting target/output/clusters-3
22:16:23.291 [main] WARN  org.apache.hadoop.mapred.JobClient - Use GenericOptionsParser for parsing the arguments. Applications should implement Tool for the same.
22:16:23.496 [main] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:24.679 [Thread-41] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:24.690 [main] INFO  org.apache.hadoop.mapred.JobClient - Running job: job_local_0003
22:16:24.729 [Thread-41] INFO  org.apache.hadoop.mapred.MapTask - io.sort.mb = 100
22:16:25.043 [Thread-41] INFO  org.apache.hadoop.mapred.MapTask - data buffer = 79691776/99614720
22:16:25.044 [Thread-41] INFO  org.apache.hadoop.mapred.MapTask - record buffer = 262144/327680
22:16:25.101 [Thread-41] INFO  org.apache.hadoop.mapred.MapTask - Starting flush of map output
22:16:25.103 [Thread-41] INFO  org.apache.hadoop.mapred.MapTask - Finished spill 0
22:16:25.106 [Thread-41] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0003_m_000000_0 is done. And is in the process of commiting
22:16:25.107 [Thread-41] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:25.107 [Thread-41] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0003_m_000000_0' done.
22:16:25.113 [Thread-41] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:25.114 [Thread-41] INFO  org.apache.hadoop.mapred.Merger - Merging 1 sorted segments
22:16:25.115 [Thread-41] INFO  org.apache.hadoop.mapred.Merger - Down to the last merge-pass, with 1 segments left of total size: 239 bytes
22:16:25.115 [Thread-41] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:25.190 [Thread-41] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0003_r_000000_0 is done. And is in the process of commiting
22:16:25.191 [Thread-41] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:25.191 [Thread-41] INFO  org.apache.hadoop.mapred.TaskRunner - Task attempt_local_0003_r_000000_0 is allowed to commit now
22:16:25.195 [Thread-41] INFO  o.a.h.m.l.output.FileOutputCommitter - Saved output of task 'attempt_local_0003_r_000000_0' to target/output/clusters-3
22:16:25.196 [Thread-41] INFO  o.a.hadoop.mapred.LocalJobRunner - reduce > reduce
22:16:25.196 [Thread-41] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0003_r_000000_0' done.
22:16:25.702 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 100% reduce 100%
22:16:25.703 [main] INFO  org.apache.hadoop.mapred.JobClient - Job complete: job_local_0003
22:16:25.704 [main] INFO  org.apache.hadoop.mapred.JobClient - Counters: 13
22:16:25.704 [main] INFO  org.apache.hadoop.mapred.JobClient -   Clustering
22:16:25.705 [main] INFO  org.apache.hadoop.mapred.JobClient -     Converged Clusters=3
22:16:25.705 [main] INFO  org.apache.hadoop.mapred.JobClient -   FileSystemCounters
22:16:25.705 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_READ=8227859
22:16:25.706 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_WRITTEN=8374758
22:16:25.706 [main] INFO  org.apache.hadoop.mapred.JobClient -   Map-Reduce Framework
22:16:25.706 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input groups=3
22:16:25.706 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine output records=3
22:16:25.707 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map input records=9
22:16:25.707 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce shuffle bytes=0
22:16:25.707 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce output records=3
22:16:25.708 [main] INFO  org.apache.hadoop.mapred.JobClient -     Spilled Records=6
22:16:25.709 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output bytes=675
22:16:25.709 [main] INFO  org.apache.hadoop.mapred.JobClient -     Combine input records=9
22:16:25.710 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output records=9
22:16:25.710 [main] INFO  org.apache.hadoop.mapred.JobClient -     Reduce input records=3
22:16:25.713 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - Clustering data
22:16:25.714 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - Running Clustering
22:16:25.714 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - Input: target/input/points Clusters In: target/output/clusters-3 Out: target/output/clusteredPoints Distance: org.apache.mahout.common.distance.EuclideanDistanceMeasure@343a9d95
22:16:25.714 [main] INFO  o.a.m.clustering.kmeans.KMeansDriver - convergence: 0.0010 Input Vectors: org.apache.mahout.math.VectorWritable
22:16:25.714 [main] INFO  o.a.hadoop.metrics.jvm.JvmMetrics - Cannot initialize JVM Metrics with processName=JobTracker, sessionId= - already initialized
22:16:25.730 [main] INFO  org.apache.mahout.common.HadoopUtil - Deleting target/output/clusteredPoints
22:16:25.732 [main] WARN  org.apache.hadoop.mapred.JobClient - Use GenericOptionsParser for parsing the arguments. Applications should implement Tool for the same.
22:16:25.932 [main] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:26.259 [main] INFO  org.apache.hadoop.mapred.JobClient - Running job: job_local_0004
22:16:26.270 [Thread-54] INFO  o.a.h.m.lib.input.FileInputFormat - Total input paths to process : 1
22:16:26.404 [Thread-54] INFO  org.apache.hadoop.mapred.TaskRunner - Task:attempt_local_0004_m_000000_0 is done. And is in the process of commiting
22:16:26.405 [Thread-54] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:26.405 [Thread-54] INFO  org.apache.hadoop.mapred.TaskRunner - Task attempt_local_0004_m_000000_0 is allowed to commit now
22:16:26.410 [Thread-54] INFO  o.a.h.m.l.output.FileOutputCommitter - Saved output of task 'attempt_local_0004_m_000000_0' to target/output/clusteredPoints
22:16:26.411 [Thread-54] INFO  o.a.hadoop.mapred.LocalJobRunner - 
22:16:26.411 [Thread-54] INFO  org.apache.hadoop.mapred.TaskRunner - Task 'attempt_local_0004_m_000000_0' done.
22:16:27.261 [main] INFO  org.apache.hadoop.mapred.JobClient -  map 100% reduce 0%
22:16:27.261 [main] INFO  org.apache.hadoop.mapred.JobClient - Job complete: job_local_0004
22:16:27.262 [main] INFO  org.apache.hadoop.mapred.JobClient - Counters: 5
22:16:27.262 [main] INFO  org.apache.hadoop.mapred.JobClient -   FileSystemCounters
22:16:27.262 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_READ=5484682
22:16:27.262 [main] INFO  org.apache.hadoop.mapred.JobClient -     FILE_BYTES_WRITTEN=5581897
22:16:27.262 [main] INFO  org.apache.hadoop.mapred.JobClient -   Map-Reduce Framework
22:16:27.263 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map input records=9
22:16:27.263 [main] INFO  org.apache.hadoop.mapred.JobClient -     Spilled Records=0
22:16:27.263 [main] INFO  org.apache.hadoop.mapred.JobClient -     Map output records=9

*1:以下は俺の趣味なので、必須のライブラリではありませんが。

*2:ログ出力の俺好み設定ファイルをこっちで作っているからです。無くてもよいです。その場合、以下のlogback.xmlは不要です。ただし、さらにその下に示すログ出力は別の表記に変わります。

*3:IOUtil.closeQuietlyのためだけに入ってます。

2011-03-02

[]Javaのcloneは悪者か?

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

Java: The Good Partsが(賛否両論の)話題を呼んでいるが、それ以前にEffective Javaは皆さん、読んだだろうか? この本の項目11に、「cloneを注意してオーバーライドする」というセクションがある。そのほかにも、Javaのcloneメソッドは各所で嫌われているようだ。

そのような論調において、clone代替案としては、コピーコンストラクタと、staticなファクトリメソッドがしばしば挙げられる*1

うん、確かにJavaのcloneメソッドはイケてない。俺もそう思う。まぁ、イケてない理由はみんなと同じだから上記の各文献を当たってください。まぁ要するに「きちんと実装するのが難しく、それをコンパイラに強制させられない」のです。

では、cloneメソッドは要らないのか? と言われれば、俺はNOだ。要る。要件として「コピー元と同じ実装クラスのインスタンスを生成しなければならない」というケースでcloneを使わざるを得ない場合がある。

  • コピーコンストラクタは、コピー元の実装クラスを知らなければ使えない。
  • staticなファクトリメソッドも、オーバーライドが出来ないため、やはりコピー元の実装クラスを知る必要がある。

まぁ、言いたいことはだいたい伝わったとは思うのだが、具体例で説明してみたい。…で、これを説明するにあたって、良く知られたクラスを例に出したかった。が、そんな例が見つからなかった…。不当に嫌われすぎていて、そんな実装例が見つからないのだ*2

というわけで、現実の話を少し曲げて説明に使ってみる。

JavaにはListというインターフェイスがあって、その実装としてArrayListやLinkedListがある。まず前提として確認したいのは、Listは Cloneable インターフェイスを実装しておらず、ArrayListとLinkedListはこれを実装しているということ。これは何故だろう、と考えると、ArrayListやLinkedListは、順序付き集合をオンメモリで扱う実装だからだ。メモリ上の集合ならばcloneしても構わない。しかし、Listはというと、実装がオンメモリであるとは限らない。add/removeなどを実行する度に、律儀にファイルやDB、もしくはリモートのサーバに内容を書き出すような実装クラスを作っても、Listインターフェイス契約的には何ら問題にならない。List型にcloneを認めてしまうと、このような実装を妨げてしまうからであろう。

ここで、現実の話を少し曲げる。Listとその実装型の間に、仮に OnMemoryList というインターフェイスを挟んであるものだと仮定する。さらにもう一つ、ArrayList#clone()も戻り値型はObject型ではなくArrayList型、そしてLinkedList#clone()の戻り値型は同様にLinkedList型である、とします。共変戻り値ですね。

f:id:daisuke-m:20110302140802p:image:w600

ではここで問題。

/**
 * {@code in}に与えたリストが持つ要素のうち、{@code p}を満たす要素のみで
 * 構成される新しいリストを返す。
 * 
 * <p>戻り値のリストの実装型は{@code in}の実装型と同じである。
 * また、{@code in}は破壊してはならない。
 * 戻り値のリストの要素順は、{@code in}の要素間の相互の位置関係を維持する。
 * </p>
 * 
 * @param<E> 要素の型
 * @param in 入力のリスト
 * @param p 条件を表す述語
 * @return 新しい {@link OnMemoryList}
 * @throws IllegalArgumentException 引数に{@code null}を与えた場合
 */
public <E>OnMemoryList<E> filter(OnMemoryList<E> in, Predicate<? super E> p) {
  // ...
}
ArrayList<String> idList = new ArrayList<String>();
// LinkedList<String> idList = new LinkedList<String>();
// どちらでもテストは成功すること。

idList.add("dai.0304");
idList.add("daisuke_m");
idList.add("dai19780304");
idList.add("daisuke-m");
idList.add("dai0304");
idList.add("daisuke");

OnMemoryList<String> filtered = filter(idList, new Predicate<String>() {
  public boolean apply(String input) {
    return input.matches(".*[0-9].*"); // contains digits
  }
});

// idListの非破壊を確認
assertThat(idList.toString(), is("[dai.0304, daisuke_m, dai19780304, daisuke-m, dai0304, daisuke]"));

// 正常にフィルタリングされていることを確認
assertThat(filtered.toString(), is("[dai.0304, dai19780304, dai0304]"));

// 実装クラスが同じであることを確認
assertThat(filtered.getClass().equals(idList.getClass()), is(true));

このメソッド、どうやって実装しますか? よくある関数型っぽいことをするためのメソッドですね。ミソは「戻り値のリストの実装型は{@code in}の実装型と同じである」ってところで、これが恐らく、cloneを使わないと実装できないところだと思います。

Validate.notNull(in);
Validate.notNull(p);
OnMemoryList<E> result = in.clone();
Iterator<E> itr = result.iterator();
while (itr.hasNext()) {
  if (p.apply(itr.next()) == false) {
    itr.remove();
  }
}
return result;

このように、cloneにも重要な役割があるのであって、cloneはイケてないから一律使わない、と思考停止するのはあんまりよくないんじゃないかなー、と思っています。Effective Javaは、本当にcloneが必要なケースかを問いかけ、必要ならばうまくやれ、と言っているんだ。決して「cloneはイケてないから使うな」とは言っておらず「イケてないから、注意深く実装しようね」と指摘しているに過ぎないのだ。

cloneをうまく使っているコードってあまり見ないなぁ、不当に迫害され過ぎてんじゃないかなぁ、と思ったので、こんなん書いてみました。

まぁ、4ヶ月ほど前まで、自分が思考停止してたんですけどネ。

*1:もしくは、シリアライズ+デシリアライズの組み合わせ、なんてのを提案する場合もある。

*2:分かってる、コレは俺の願望だw