« Back to Home

Blog

Re: Javaでのパターンマッチを考える

2012-12-22 05:59:01 +0900

@kis さんのエントリ「Javaでのパターンマッチを考える」がとても興味深かったので、私も書いてみました。

ユーティリティクラスを使って解決することにしたらどうでしょう?

Scalaの記法をよく見て、なるべく似たような形になるようにクラス構造を考えました。
caseなどのキーワードに当たる部分はstaticメソッドで乗り切り、マッチした際に実行されるコードブロックはインナークラスを使って表現します。

package patternmatch;

import java.util.Arrays;

public class PatternMatcher {
    public static PatternMatcher subject(Object... objects) {
        return new PatternMatcher(objects);
    }

    public static Case case_(Object... objects) {
        return new Case(objects);
    }
    
    public static final Case default_ = new Case() {
        @Override
        boolean matches(Object[] objects) {
            return true;
        }
    };
    
    public static final WildCard _ = new WildCard();

    private final Object[] objects;
    
    PatternMatcher(Object... objects) {
        this.objects = objects;
    }
    
    public void match(CaseBlock... caseBlocks) {
        for (CaseBlock caseBlock : caseBlocks) {
            Case c = caseBlock.getCase();
            if (c.matches(objects)) {
                caseBlock.getBlock().apply();
                return;
            }
        }
    }
    
    // コンパイル用(本来不要)
    public void match(CaseBlock cb1, CaseBlock cb2,
            CaseBlock cb3, CaseBlock cb4) {
        match(new CaseBlock[]{cb1, cb2, cb3, cb4});
    }
    
    public static class Case {
        private final Object[] objects;
        
        Case(Object... objects) {
            this.objects = objects;
        }
        
        public CaseBlock exec(Block b) {
            return new CaseBlock(this, b);
        }
        
        boolean matches(Object[] objects) {
            // ちょっと省略
            return Arrays.equals(this.objects, objects);
        }
    }
    
    public static interface Block {
        public void apply();
    }
    
    static class CaseBlock {
        private final Case c;
        private final Block b;
        
        public CaseBlock(Case c, Block b){
            this.c = c;
            this.b = b;
        }

        public Case getCase() {
            return c;
        }

        public Block getBlock() {
            return b;
        }
    }
    
    static class WildCard {
        @Override
        public boolean equals(Object obj) {
            return true;
        }
    }
}

このPatternMatcherクラスを使ってFizzBuzzを書くとこうなります。

package patternmatch.use;

import static java.lang.System.out;
import static patternmatch.PatternMatcher.*;
import patternmatch.PatternMatcher.Block;

public class Test {
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            final int j = i;
            subject(j % 3, j % 5).match(
                case_(0, 0).exec(new Block(){
                    @Override
                    public void apply() {
                        out.println("fizzbuzz");
                    }}),
                case_(0, _).exec(new Block(){
                    @Override
                    public void apply() {
                        out.println("fizz");
                    }}),
                case_(_, 0).exec(new Block(){
                    @Override
                    public void apply() {
                        out.println("buzz");
                    }}),
                default_.exec(new Block(){
                    @Override
                    public void apply() {
                        out.println(j);
                    }})
            );
        }
    }
}

これではあまり嬉しくないのですが、JavaSE8のLambdaを使えば以下のように書けます。

package patternmatch.use;

import static java.lang.System.out;
import static patternmatch.PatternMatcher.*;

public class Test2 {
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            final int j = i;
            subject(j % 3, j % 5).match(
                case_(0, 0).exec(()-> out.println("fizzbuzz") ),
                case_(0, _).exec(()-> out.println("fizz") ),
                case_(_, 0).exec(()-> out.println("buzz") ),
                default_.exec(()-> out.println(j) )
            );
        }
    }
}

おお、多少のムリヤリ感は拭えないまでも、Lambdaで書くとかなり記述量が減って良い感じですね!

ちなみに、執筆時点で最新のOpenJDK with Lambda support (lambda-8-b69-macosx-x86_64-17_dec_2012) を使用してコンパイルしてみたところ、まだコンパイラにバグがあるようで、可変長引数メソッドを参照するとコンパイルが通りませんでした。

アノテーションのインスタンスを取得する

2012-12-19 04:30:46 +0900

このエントリはJava Advent Calendar 2012の第19日目のエントリです。
昨日は @yoshioterada さんの「Concurrency Update (jsr166e)のご紹介」でした。
明日は @kis さんの「Javaでのパターンマッチを考える」です。

お二人に挟まれて、私は自分の少し恥ずかしい話をします。今日のテーマはアノテーションのインスタンスについてです。

事の起こり

私はある日、こういうメソッドに出くわしました。

select(Annotation...)

なるほどAnnotationクラスを引数として渡すのね、と初めは軽く考えてしまったのですが、実際にこのメソッドを呼ぼうとしたとき困りました。

最初に書いたコードはこうです。(笑

select(@Customized);

コンパイルエラーになりました。というかEclipseなので赤線が出ました。ここでアレっ?と思ったわけです。そこでコードを次のように直してみました。

select(Customized.class);

それでもやっぱり赤線が出ます。おや? メソッドが呼べない! もう大混乱です。

Annotationクラスって何でしょう

メソッドの定義がおかしいのでしょうか。いやいや、呼べないようなメソッドが定義されているはずはないので(なにしろJavaEEのAPIです)、私のコードが間違っている可能性の方が極めて高いです。頭を冷やしてよく考えてみましょう。

アノテーションでよく使うのはこういうコードですよね。

@Override
public String toString() {
     // ...
}

この@Overrideはアノテーションによる注釈付けをするときの書き方なわけです。

ではCustomized.classは・・・? 変数に代入してみるとよくわかります。

Class<Customized> type = Customized.class;

なるほど、Customized.classはCustomizedクラスを表すクラスインスタンスです。

とするとAnnotationクラスはどういった場面で登場するのでしょうか。こういうときは初心に帰って、Java docを開いてみます。java.lang.annotation.Annotationインターフェースの「使用」を見てみると、Class#getAnnotations()というメソッドの戻り値がAnnotation[]になっています。リフレクションですね。

@Customized
public class ObtainAnnotation {
     public static void main(String[] args) {
          Annotation[] annotations
               = ObtainAnnotation.class.getAnnotations();

          for (Annotation a : annotations) {
               System.out.println(a);
          }
     }
}
@Customized()

ようやく分かってきました。アノテーションによって注釈付けされた要素に付いている、実際のアノテーションを表すのがAnnotationクラスのインスタンスです。

アノテーションをインスタンス化するには・・・?

次に問題になるのは、アノテーションのインスタンスをいかに得るかということです。確かに注釈付けされた要素からリフレクションで得られることはわかりましたが、まさかいちいち注釈付け→リフレクションして取得するというのも奇妙な話です。

実際のところ、JavaEE6にはアノテーションのインスタンスを取得するためのクラス、AnnotationLiteral<T>が用意されています。具体的な使い方はProgrammatic CDIの方に書きましたので、よろしければ参照してみてください。

当然ながらそのクラスは、JavaEE環境でなければ(標準では)使用することができません。そこでJavaSE環境でアノテーションのインスタンスを取得するための方法を考えてみましょう。

newしてみる

まずは普通にnewしてみたらどうでしょうか。たぶんうまく行かない気がしますがやってみます。

Customized c = new Customized();

予想通り、これはコンパイルエラーになりました。

リフレクションで生成

次に思いつくのはクラスからリフレクションで生成するという方法です。

public class ObtainAnnotation2 {
     public static void main(String[] args) {
          try {
               Customized c = Customized.class.newInstance();
               System.out.println(c);
          } catch (InstantiationException e) {
               e.printStackTrace();
          } catch (IllegalAccessException e) {
               e.printStackTrace();
          }
     }
}
java.lang.InstantiationException: Customized
     at java.lang.Class.newInstance0(Class.java:357)
     at java.lang.Class.newInstance(Class.java:325)
     at ObtainAnnotation2.main(ObtainAnnotation2.java:5)

例外が発生してしまいました。これでもやっぱりダメなようですね。メッセージには出ていませんが、該当行の辺りを見てみるとコンストラクタを取得するところで失敗しています。どういうことでしょう?

アノテーションはインターフェース

手がかりはアノテーションの定義にあります。アノテーションを定義するコードは、例えば以下のようなものですよね。

@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Customized {

}

よく見てみると@interfaceと書いてあるではないですか。そうなんでした、アノテーションは特別なインターフェースなのです。直接インスタンス化できないのは当然です。

アノテーションを実装する

インターフェースと分かれば、道は見えました。実装を自分で書けばインスタンス化できるはずです。

public class CustomizedModifier implements Customized {
     private static final Class type = Customized.class;

     @Override
     public Class<? extends Annotation> annotationType() {
          return type;
     }

     @Override
     public boolean equals(Object o) {
          if (o == this) {
               return true;
          }
          if (!type.isInstance(o)) {
               return false;
          }
          return true;
     }

     @Override
     public int hashCode() {
          return 0;
     }
}
// ...
Customized c = new CustomizedModifier();

これでコンパイルエラーは出なくなり、その代わりに見慣れない警告:"The annotation type Customized should not be used as a superinterface for CustomizedModifier"が出るようになりました。コンパイラが不審がるのももっともです。アノテーションは注釈をつけるための仕組みであるから、振る舞い(=実装)を持つべきではない、というわけです。

ちなみに@SuppressWarningsでこの警告のみを抑制する値を探したのですが、見つかりませんでした。コンパイラ次第ですが、"all"を使うしかないかもしれません。

リフレクションの実装を見る

ちょっと目先を変えて、お手本になりそうなものを見てみましょう。OpenJDKではアノテーションのインスタンスをどうやって作っているのでしょうか。OpenJDKのリフレクション部分のソースを追ってみると、sun.reflect.annotation.AnnotationParser内に実装がありました。以下該当メソッドの引用です。

public static Annotation annotationForMap(
        Class<? extends Annotation> type, Map<String, Object> memberValues)
{
    return (Annotation) Proxy.newProxyInstance(
        type.getClassLoader(), new Class[] { type },
        new AnnotationInvocationHandler(type, memberValues));
}

なるほど、Proxyを使っているんですね。リフレクションでは未知のアノテーションも扱わなければなりませんので、このようになっているのでしょう。うーむ、あまり参考になりませんでしたね。

まとめ

残念ながら、すっきりした解決策は今のところありません。既知のアノテーションのインスタンスを取得するのであれば、@SuppressWarnings("all")のリスクがあるとしても、型安全を考えてそのアノテーションインターフェースを実装するのが良いと思います。

ただしアノテーションの正しい実装を書くことは、特にメンバーを持つアノテーションの場合、実はかなり骨の折れる作業です。java.lang.annotation.AnnotationのJava docにequals()やhashCode()の実装方法が規定されていますので、それを注意深く実装する必要があります。JavaEE6のCDIに用意されたAnnotationLiteral<T>は、その部分を実装してくれているのです。

そのCDIの参照実装であるJBoss Weldのコミュニティでも、AnnotationLiteral<T>の使用について疑問が呈されているのを目にしました。結論としては、Javaにアノテーションのリテラル記法が用意されていない以上、これ(AnnotationLiteral<T>の使用=アノテーションインターフェースの実装)が最善であるとされたようです。

そういえばリフレクションを使っていると、もどかしく感じることが多々ありますよね。

  • パッケージリテラル
  • フィールドリテラル
  • メソッドリテラル
  • アノテーションリテラル

仮にこの4つが言語仕様に追加されたとすれば、リフレクションもかなり簡潔に書けるようになるのではないでしょうか。このうちパッケージリテラルは、どこかで導入が検討されている/いたと話に聞いたことがあるような・・・?

Programmatic CDI

2012-12-16 10:33:53 +0900

このエントリはJavaEE Advent Calendar 2012 第16日目のエントリです。
昨日は @kokuzawa さんの「Point-to-Point on JMS」でした。
明日は @yumix_h さんの「JAX-RSでファイルアップロード!」です。

今日のテーマはCDI 1.0 (JSR 299: Contexts and Dependency Injection for the Java EE platform)です。特にAPIからCDIを使う方法について書き留めたいと思います。なお、動作検証はMac OS X 10.8.2 + JDK 1.7.0_05 + GlassFish 3.1.2 で行っています。

CDIの基本

まずはCDIの基本的な使い方について、簡単にまとめておきましょう。キーになるのはスコープとインジェクションポイントです。

スコープ

スコープとは、インスタンスの生存期間のことです。それぞれのアノテーションでクラス、あるいはプロデューサフィールド/メソッドをマークすることによって表します。

// Userクラスをセッションスコープとしてマーク
@SessionScoped
public class User implements Serializable {

}

予め用意されているスコープは

  • @ApplicationScoped
  • @SessionScoped
  • @ConversationScoped (with JSF)
  • @RequestScoped

の4種類です。意味は名前からだいたい想像がつくと思いますが、ConversationScopeは少し特殊かもしれませんね。これはJSFと共に使用するもので、ブラウザタブ毎に複数リクエストに渡って維持することのできるスコープです。

インジェクションポイント

インジェクションポイントとは、依存性を解決されたインスタンスが注入される場所のことで、@Injectを使って表します。フィールド、コンストラクタ、あるいはメソッドをマークします。

@RequestScoped
public class Ephemera {
  // Userインスタンスをインジェクション
  @Inject
  private User user;
}

beans.xml

CDIを有効化するためには、beans.xmlファイルをWEB-INF/(Webモジュールの場合)、あるいはMETA-INF/(EJBモジュールの場合)に配置します。また、依存jarファイルにインジェクション対象となるクラスを含む場合、そのjarファイル内のMETA-INF/にもbeans.xmlファイルを配置する必要があります。いずれのbeans.xmlも空のファイルで構いません。

APIを利用する

ほかにもQualifierとかいろいろ面白いのですが、その辺りは今日の本題ではないので別の機会に譲ります。ということで、このようにアノテーションによってコンテキストと依存性を解決することができるのがCDIです。ただしアノテーションはあくまで静的な記述ですから、

  • インジェクション対象となるクラスは、依存性を一意に解決できる
  • 1つのインジェクションポイントには、常に1つのクラスのインスタンスがインジェクションされる

といった前提条件があります。しかし場合によってはもう少し柔軟な処理が必要になることもあるでしょう。例えばフレームワークやライブラリを書いているときなどは、あるインターフェースを実装するクラスが複数存在したり、1つのインジェクションポイントに条件次第で異なる実装クラスをインジェクションしたり・・・などということもよくよくあるものです。

CDIにはアノテーション以外のAPIも定義されていますので、それを使ってみましょう。

Instance<T>

javax.enterprise.inject.Instance<T>はT型のCDI管理インスタンスを選択・取得するためのクラスです。T型の実装が存在しない、あるいは複数存在する可能性がある場合に使用できます。

例えばこのようなクラス構成を考えます。(それぞれのクラスについているアノテーションはQualifierとします)

Programmatic CDI classes

そして次のサーブレットを実行してみましょう。

public class MyServlet extends HttpServlet {
     private static final long serialVersionUID = 1L;

     // インターフェースPersonの実装をインジェクション
     @Inject @Any Instance<Person> instance;

     protected void doGet(HttpServletRequest request,
               HttpServletResponse response) throws ServletException, IOException {
          PrintWriter out = response.getWriter();

          if (instance.isUnsatisfied()) {
               out.println("Person is not implemented");
          } else if (instance.isAmbiguous()) {
               out.printf("Person is implemented:%n");
               for (Person person : instance) {
                    out.printf(" - %s%n", person);
               }
          } else {
               out.printf("Person is implemented: %s%n", instance.get());
          }
     }
}
$ curl "http://localhost:8080/cdi/MyServlet"
Person is implemented:
 - Admin@6e028a6a
 - User@7a0c4fd9
 - SpecialUser@3e4e6e28

なお@Injectと共に付いている@AnyはQualifierを無視するためのアノテーションです。@Anyを付けない場合、QualifierのないUserクラスのインスタンスのみが選択されてインジェクションされます。

さてこれで複数の実装クラスを手にすることができました。次はいずれかの実装を選択しましょう。そのためにはInstance#select(Annotation...)メソッドにQualifierを渡します。

・・・ちょっと待ってください、引数としてアノテーションのインスタンスを渡す必要があるのですね。アノテーションのインスタンスってどうやって作るのでしょう? この話題はJava Advent Calendarの方で書こうと思っているのですが、CDIにはちゃんとインスタンス化の方法が用意されています。(でもやっぱりちょっと面倒です)

具体的にはjavax.enterprise.util.AnnotationLiteral<T>を拡張して以下のようなクラスを作成します。

@SuppressWarnings("all")
public class SupervisorQualifier
     extends AnnotationLiteral<Supervisor> implements Supervisor {
     private static final long serialVersionUID = 1L;
}

こうして作ったクラスを使って、Instance#select(Annotation...)メソッドをコールします。ついでに言うとこのメソッドの戻り値もInstance<T>なので、複数のQualifierを順に絞っていくような選択の仕方も可能です。

out.printf("@Supervisor %s%n",
          instance.select(new SupervisorQualifier()).get());
out.printf("@Default %s%n",
          instance.select(new DefaultQualifier()).get());
out.printf("@Customized %s%n",
          instance.select(new CustomizedQualifier()).get());
$ curl "http://localhost:8080/cdi/MyServlet"
...
@Supervisor Admin@77f606ea
@Default User@81bbd46
@Customized SpecialUser@49414ab

これでそれぞれの実装を選択することができました。

BeanManager, Bean<T>

前項のInstance<T>は@Injectと共に使用するクラスでした。つまりInstance<T>が使用できるのは、@Injectが有効な場所に限られます。

それに対してjavax.enterprise.inject.spi.BeanManagerはJNDI名を持っているため、ほぼどこでも使用することができます。(もちろんBeanManagerそのものを@Injectでインジェクションすることも可能です)

CDIと無関係なクラス、MySeviceからBeanManagerを使ってCDIを呼び出してみましょう。

public class MyService {
     public void exec(PrintWriter out){
          try {
               BeanManager bm  = InitialContext.doLookup("java:comp/BeanManager");

               Set<Bean<?>> beans = bm.getBeans(Person.class);
               Bean<?> bean = bm.resolve(beans);   // 一意に解決できる場合
               CreationalContext<?> cc = bm.createCreationalContext(bean);
               Person person = (Person) bm.getReference(bean, Person.class, cc);
               out.printf("Person: %s%n", person);

          } catch (NamingException e) {
               e.printStackTrace();
          }
     }
}
$ curl "http://localhost:8080/cdi/MyServlet2"
Person: User@cbbe607

ちなみにMyServlet2は、普通にnew MyService()してからexec(PrintWriter)を呼び出すだけのサーブレットです。

CDI管理下のクラスそれぞれに対応する、javax.enterprise.inject.spi.Bean<T>クラスのインスタンスをBeanManagerから取得することができます。スコープやインジェクションポイント、QualifierなどCDIとしての情報はこのBean<T>クラスが保持しています。

これでどこでもCDIが使えるようになりました。ただこのBeanManagerからPersonを取得するまでのコードは少し冗長ですよね。もともとPortable Extensionで使用するためにあるクラスとは言え、簡易メソッドとしてBeanManager#getReference(Type beanType, Annotation... qualifiers) ぐらいはあってもいい気がするんですが・・・。

CDI 1.1

ちょっと追記です。現在Public ReviewになっているCDIの次期バージョン、CDI 1.1 (JSR 346: Contexts and Dependency Injection for Java EE 1.1) を見てみたところ、新しくjavax.enterprise.inject.spi.CDI<T>というクラスが定義される予定になっているようです。このクラスはInstance<T>を実装していて、かつCDI<Object>を返すstaticメソッドcurrent()を持っています。つまり、

Person person = CDI.current()
     .select(Person.class, new DefaultQualifier()).get();

などとすれば、Personインターフェースの実装インスタンスを取得することができるようになるということですね。

r53tools-1.0.0を公開

2012-12-06 00:13:21 +0900

AWSのDNSサービスにAmazon Route 53なるものがあります。最近多数のドメインをそのAmazon Route 53へ設定しなければならないことがあったため、CLIで処理をしたくなりました。

AWSに関するCLIとしては以前s3toolsを書いていたので、割合簡単に書けることも分かっていました。そこで再び自分でCLIを書いてみました。

r53tools: Amazon Route 53 CLI

これを修正BSDライセンスで公開します。動作環境はJavaSE6.0以降です。

機能としては、ゾーンの作成/一覧・レコードの作成/一覧/削除、を備えています。ゾーンの削除はAWSのWebコンソールから実行した方が良いように思ったので、今のところ用意しませんでした。

例によってLinux/Mac用のshスクリプトしか書いていません(Windows用を書いてもテストが・・・)。s3toolsの方も機能追加しようと思いつつ出来てないですが、そのうちやります。

ところで、たいてい私は繰り返しが20程度の数になったら楽をできないか考え始めるのですが、この20って少ないんでしょうかね?

JavaOne 2012 San Francisco レポート(5日目)

2012-10-04 22:23:24 +0900

ついにベールを脱いだProject Avatar。新しいWebアプリケーションの本命となれるか

2012/10/04(木):5日目でJavaOneは最終日となりました。昨日までとは打って変わって、曇りがちで少し肌寒いほどの涼しさでしたが、朝行われたCommunity KeynoteにはJavaの父、ゴスリンさんが登場! スタンディング・オベーションも出る盛り上がりでした。

Building HTML5 Web Applications with Avatar

昨年から名前だけは出ていたProject Avatarの内容がついに明らかになりました。
その中心となるのはHTML5とJavaScriptです。

  • クライアントサイド・サーバサイドともJavaScriptを開発言語の主軸とする
  • クライアントサイドがビューを担い、サーバサイドはサービスのみを担う
  • クライアント-サーバ間の通信はREST, WebSocket, Server-Sent Eventsによって行う

つまり、クライアントはビュー・サーバはサービスと完全な分業を行い、標準化されたHTML5関連の技術によって、リッチなUIと相互運用性の高いデータ通信を実現しようというわけです。

少し気になるのはクライアントサイド・サーバサイド両方の開発の中心に、JavaScriptを据えていることですね。今後HTML5を使っていく以上、クライアントサイドでJavaScriptが必須になるのはわかりますし、ならばサーバサイドでも同じ言語を使用する方が都合の良いことも多いとは思います。しかしいくらNashornの搭載でJavaのAPIを使えると言っても、JavaScriptでの開発に抵抗のある開発者も少なくないでしょう。
Project Easelでの開発ツールの充実、テスト方式の確立などでそういった抵抗感をいかに払拭できるかが、これからProject Avatarの課題になると私は考えています。

[caption id="attachment_715" align="aligncenter" width="983"]Project Avatar Project Avatarの
新しいサーバアーキテクチャ TSA[/caption]

JavaOne 2012 San Francisco レポート