倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

「日曜日から土曜日まですっぽりとハマってる月は今月が実に823年ぶり 」は本当かどうか Java 日時 API に問うてみた

2月1日にこんなツイートがありました:


で、即刻「デマだ!」というリプとかもあって「やられた」と思ったんですが、やはりこういうのは自分で確かめてみるべきだなと思い、Java8 で導入された Stream APIJava 日時 API (Java DateTime API) でこういう年を探すコードを書いてみました。

2月がカレンダーにすっぽりとハマるようになるための条件は

  • その年は閏年 (leap year) ではない
  • 2月1日が日曜日

なので、結構簡単に判定できますね。 閏年の判定はめんどくさそうにみえるけど、API に丸投げで OK。 Year#isLeap() で出来ます*1。 また、時刻は関係ないので Year や LocalDate などで充分事足りますな。 特に、最初ハードルが高そうに感じるタイムゾーンやオフセットなどは不必要。

以下のコードでは、今年(2015年)から始めて100年(range() の引数)遡って上記の条件を満たす年を表示します。

import java.util.stream.IntStream;

import java.time.Year;
import java.time.MonthDay;
import java.time.LocalDate;

import static java.time.Month.FEBRUARY;
import static java.time.DayOfWeek.SUNDAY;

public class Main {

    public static void main(String... args){
        IntStream.range(0, 100)

                .mapToObj(i -> Year.of(2015 - i))
                .filter(year -> !year.isLeap())    // 閏年かどうか

                .map(year -> year.atMonthDay(MonthDay.of(FEBRUARY, 1)))
                .filter(day -> day.getDayOfWeek() == SUNDAY)    // 2月1日が日曜かどうか

                .map(LocalDate::getYear)
                .forEach(System.out::println);
    }
}

Java 日時 API は当初不評だった気がしますが、結構使いやすいですな。 IDE の補完機能を使えばほとんどドキュメントいらず。 ちょっと JavaDoc 見たけど。

で、実行結果は

2015
2009
1998
1987
1981
1970
1959
1953
1942
1931
1925

えっ! 結構あるな(笑) 過去100年で11年。 ちなにみ過去1000年で試したら110年くらいあったヨ!

修正
コード中で int のストリームの生成方法を range() メソッドを使うように変更しました。

デートTIME

デートTIME

Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング

Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング

*1:YearManth#isLeapYear() など、他のクラスにも判定メソッドはあるようです。

どんとこいタイプ・アノテーション! IntelliJ IDE 独自のタイプ・アノテーション

最近、Java8 で導入されたタイプ・アノテーションを使う方法をあれこれ試してるんですが、コンパイラに機能を追加するだけあって、設定がちょっと面倒なことが多いですね。 タイプ・アノテーションを具体的に使う際によく挙げられている例に、変数に代入される値の null チェックをするというものがあるんですが、Java Tutorials ではこの機能を提供するライブラリとして Checker Framework について言及されています。

で、この Checker Framework を IntelliJ IDEA 上で使えるように設定を頑張ってたんですが、ちょっとその前に IntelliJ IDEA が独自に提供している null チェックのライブラリを試してみることにしました。 と言いつつ、提供されているアノテーションの使い方ではなく設定方法だけ書いてます。

参考

設定手順

設定手順は以下の通り。 結果を先に言うと、Project Structure でライブラリに IntelliJ IDEA が提供する annotations.jar を追加しましょう、ってだけです。 IntelliJ IDEA に慣れてない人のために(久し振りに)画像をキャプチャしてアップしてみました。 IDEA の Java プロジェクトは既に作成されているとします。 設定のスタートはメニューから:

[メニュー] File ▶ Project Structure...

後は以下の手順で:


f:id:waman:20140526081710p:plain
なってなければ「Project language level」を「8.0」に。
(上の「Project SDK」が「1.8」になってなければそちらも設定。)
f:id:waman:20110105151547p:plain
f:id:waman:20140526081718p:plain
Libraries に IDEA の組み込みアノテーションのための Jar ファイル (annotations.jar) を以下の手順で追加。
f:id:waman:20110105151547p:plain
f:id:waman:20140526081759p:plain
annotations.jar は《IntelliJ IDEAのインストール・ディレクトリ》/lib 下にあります。
f:id:waman:20110105151547p:plain
f:id:waman:20140526081803p:plain
f:id:waman:20110105151547p:plain
f:id:waman:20140526081808p:plain
ライブラリに annotations が追加されていることを確認。
(赤線引き忘れたけど、真ん中の列で確認すればいいかな。)
f:id:waman:20110105151547p:plain
f:id:waman:20140526081813p:plain
Project language level を変更していた場合はこのダイアログが表示されると思うので Yes を選択。

これで設定できたので、実際にタイプ・アノテーションを試してみましょう。

タイプ・アノテーションを試してみる

設定が出来たら、以下のような Java コードを書いてみましょう:

import org.jetbrains.annotations.NotNull;

public class Main {

    public static void main(String... args){
        @NotNull Object obj = null;
    }
}

Checker Framework の同様のアノテーション @NonNull と違って @NotNull なのに注意。 このとき、「null」の部分に警告のハイライト(?)がされ、マウスを乗せると以下のように警告メッセージが表示されるはずです:


f:id:waman:20140526081817p:plain

画像が小さくて見えないか・・・

'null' is assigned to a variable that is annotated with @NotNull less... (Ctrl+F1)

@NotNull が付けられている型の変数に null 値を代入してる旨のメッセージですね。

IntelliJ IDEA の設定ファイル

特に知っておく必要はないんですが、タイプ・アノテーションを有効にするために必要な設定が IntelliJ IDEA の設定ファイルにどのように記述されているのかをメモっておきます。 ちょっと、Checker Framework の設定の参考にするんで。 タイプ・アノテーションの設定は以下のファイルに反映されているようです(ディレクトリ・ベース):

$PROJECT_DIR$

  • .idea
    • annotations.xml
    • compiler.xml
  • checker-test.iml

.idea/libraries/annotations.xml
annotations.jar へのパス設定:

<component name="libraryTable">
  <library name="annotations">
    <CLASSES>
      <root url="jar://$APPLICATION_HOME_DIR$/lib/annotations.jar!/" />
    </CLASSES>
    <JAVADOC />
    <SOURCES />
  </library>
</component>

.idea/compiler.xml
コンパイラに対するアノテーション処理設定:

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CompilerConfiguration">
    <option name="DEFAULT_COMPILER" value="Javac" />
    <resourceExtensions />
    <wildcardResourcePatterns>
      <entry name="!?*.java" />
      <entry name="!?*.form" />
      <entry name="!?*.class" />
      <entry name="!?*.groovy" />
      <entry name="!?*.scala" />
      <entry name="!?*.flex" />
      <entry name="!?*.kt" />
      <entry name="!?*.clj" />
    </wildcardResourcePatterns>

    <!-- 以下がアノテーション処理設定 -->
    <annotationProcessing>
      <profile default="true" name="Default" enabled="false">
        <processorPath useClasspath="true" />
      </profile>
    </annotationProcessing>
  </component>
</project>

checker-test.iml

<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" inherit-compiler-output="true">
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />

    <!-- モジュールのライブラリ設定 -->
    <orderEntry type="library" name="annotations" level="project" />
  </component>
</module>

まぁ、以外と設定ファイルも内容見てどんな設定が書かれてるのか分かるもんだね。 とか言いつつ設定見逃してたりするかもしれないけど。

プロダクティブ・プログラマ -プログラマのための生産性向上術 (THEORY/IN/PRACTICE)

プロダクティブ・プログラマ -プログラマのための生産性向上術 (THEORY/IN/PRACTICE)

どんとこいタイプ・アノテーション! Checker Framework 導入あの手この手 ~without IDE 編~

前回の記事『ラムダ式やストリーム API や新しい日時 API だけじゃない! Java8 のタイプ・アノテーションあの手この手』で、Java8 で導入されたタイプ・アノテーションがどういったものか紹介しました。 その記事では同一要素に重複して同じアノテーションを付けられる @Repeatable なども紹介しましたが、タイプ・アノテーションは主に「タイプ(型)が使われているところはどこにでもアノテーションが付けられる」という機能を指しているのだと思います。

で、その機能を紹介したのはいいんですが、実際の使い方には触れていませんでした。 これでは片手落ち感が否めないので、タイプ・アノテーションを使用できるライブラリである Checker Framework というのを見ていきたいと思います(が、次に述べる理由により、ライブラリ自体の使い方はこの記事で扱ってません)。 このライブラリは @NonNull のようなアノテーションを型に付加して null 値代入をコンパイル時にエラーを出すといったように、コンパイル時のエラーチェックを行うものです(@Override アノテーションみたいな感じの使い方)。 Java の公式のチュートリアルでもこのライブラリについて言及されていて、これから標準的に使われるようになるかもしれませ。 ただし、1つの難点は Java7 時代の JavaFX のような設定の面倒さがあるところ*1*2

ってことで、この記事では Checker Framework の機能ではなく、インストール方法というかコンパイラにチェック機能を追加する設定方法を見ていきます。 Checker Framework の機能自体もそのうち見ていきたいとは思ってるんですが、いつになることやら。 また、これらの設定は通常 IDE でもやらないといけないものですが、ちょっと手が回らないので今回はビルドツールをいくつか扱うだけにします。

この記事で使用する Checker Framework のバージョンは 1.8.1 とします。 Java は 8 です。 試してませんが Java7 でも同じような設定(ただし jdk8.jar の代わりにjdk7.jar が必要)でできるんじゃないかと思います。

参考

コマンドラインから使用する

まずは、あまり使わないかと思いますが、コマンドラインから Checker Framework を使う方法を見ていきます(Windows)。 別に設定が難しいわけではないのですが、使用するコンパイラを変えるのが如何に面倒かを味わってもらおうかと(笑) ってのは半分冗談ですが、あとで Gradle についての設定方法を見ていくのですが、Gradle には Checker Framework のプラグインが(現時点で)ないので、ビルドスクリプト (build.gradle) を手書きしてやります。 このときにコンパイラにどのような設定をするかは、コマンドラインから Checker Framework を使用する方法をほとんどそのまま移植してやることでできます。 なので、Gradle 上で Chekcer Framework を使いたい人は、コマンドラインからの使用方法(4つ!紹介しますが、特に4つ目)を理解しておいて下さい。 誰かこれを参考にして作ってくれないかなぁ。

コマンドラインから Checker Framework を使う方法は、公式のマニュアルに3つ、別途に通常の javac コマンドにオプションを付けて使用する方法を1つ紹介します。 どれか1つでOK。 最後の方法は公式のドキュメントには載ってないので使用は自己責任で。

最初3つの方法の、Unix 系(というか bash)での設定方法は The Checker Framework Manual 「1.3 Installation」に載ってます。 Windows に関しては The Checker Framework Manual 「24.1 Javac Compiler」にいくつかの方法が載ってます。

コマンドラインから使用する場合は、当然のことながらライブラリが自動ダウンロードされないので、まずは以下の設定をしておいて下さい:

  1. http://types.cs.washington.edu/checker-framework/current/checker-framework.zip から Zip ファイルをダウンロードして、適当なディレクトリに展開する(例えば "C:\java\")
  2. 環境変数*3CHECKERFRAMEWORK」に上記の Zip ファイルを展開したルート・ディレクトリを設定する(上記の例では "C:\java\checker-framework-1.8.1")

また、実際にコンパイル時にアノテーションによるチェックが行われているかを試すための Java ソースコードとして、以下の「GetStarted.java」ファイルを使います( The Checker Framework Manual 「1.3 Installation」から拝借):

// GetStarted.java
import org.checkerframework.checker.nullness.qual.*;

public class GetStarted{
    void sample(){
        @NonNull Object ref = new Object();
        //@NonNull Object ref = null;
    }
}

コメントアウトしている部分を外すと(代わりにその上の行をコメントアウトする)、コンパイル時にエラーが出ます。 では設定方法を見ていきましょう。

Checker Framework の javac コマンドを javac コマンドとして使う
まずは Checker Framework を展開したときに bin ディレクトリに含まれている javac コマンドを使ってコンパイルする方法(Windows)。

set PATH=%CHECKERFRAMEWORK%\checker\bin;%PATH%
javac -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java
  • 1行目は1度だけでOK。 set は BASH の export と同じで環境変数を設定するコマンドですね。 PATH の値をセットする際に、%CHECKERFRAMEWORK%\checker\bin を最初に書いてあるところに注意。 これは Checker Framework の javac コマンドを優先して使うようにするために必要です。
  • javac コマンド実行時に -processor オプションによってアノテーション・プロセッサを設定しています。 このアノテーション・プロセッサの設定は他の方法でもどこかで指定する必要があります。 面倒ですが諦めて下さい。 複数のプロセッサを指定する場合はコンマ (,) で区切ります。

この方法では PATH が汚染されるのと、もとの javac コマンドが使えないのが難点。

Checker Framework の javac コマンドを javacheck コマンドとして使う
次の方法は Checker Framework の javac コマンドを javacheck コマンド(別に他の名前でもいいですが)として使う方法:

doskey javacheck=%CHECKERFRAMEWORK%\checker\bin\javac $*
javacheck -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java
  • doskey は BASH の alias ですね。 初めて使ったw doskey では引数をパイプ(?)するため、最後に「$*」を付けておかないといけないようです。 alias はいらないらしいそうで。 この doskey コマンドの実行も1度で OK。
  • 2行目は1つ目の方法で javac の代わりに javacheck を使ってるだけです。

この方法だと、もともとの javac コマンドはそのまま使うことができます。 まぁ、普通にコマンドラインから使うにはこれで充分です。 次はコマンドラインから使う方法ですが、Java コード中から使うのに応用できる方法。

Checker Framework の Jar ファイルを使ってコンパイルする
この方法は、Checker Framework のアーカイブに含まれている checker.jar を実行可能 Jar ファイルとして実行する方法です:

java -jar %CHECKERFRAMEWORK%\checker\dist\checker.jar -processor ^
org.checkerframework.checker.nullness.NullnessChecker GetStarted.java

もしくは

doskey javacheck=java -jar %CHECKERFRAMEWORK%\checker\dist\checker.jar $*
javacheck -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java

Jar ファイルの実行なので javac コマンドではなく java コマンドを使っていることに注意。 checker.jar に含まれているメインクラスは

です(リンクはソースコード)。 ソースコードは framework サブプロジェクトにあります。 まぁ、ソースコード読もうという人はあんまりいないかもしれませんが、メモのために書いておくと、このクラスは引数をオプションとして解析したり Jar ファイルへのクラスパスを設定したりする処理を行い、実際のコンパイルは com.sun.tools.javac.Main に投げてます。

通常の javac コマンドでコンパイルする
最後は公式のドキュメントには載ってませんが、javac コマンドに(ちょっと長めの)オプションをあれこれ指定して、通常の javac コマンドでコンパイルする方法。

javac -cp .;checker.jar;javac.jar ^
-Xbootclasspath/p:jdk8.jar ^
-processor org.checkerframework.checker.nullness.NullNessChecker GetStarted.java
  • クラスパスに checker.jar, javac.jar を含めます。 上記のコマンドではカレント・ディレクトリにこれらの Jar ファイルがあるとしてますが、そうでない場合はそれらへの(相対 or 絶対)パスをきちんと書く必要があります。
  • 非標準のオプション -Xbootclasspath/p: で jdk8.jar ファイルを指定します。 これもカレント・ディレクトリにない場合はきちんとパスを書く必要があります。 また、Java7 で使いたい場合は jdk7.jar にします(たぶん。 試してないけど)。

まぁ、長々とオプション書いて何が楽しいねんと言われそうですが、ちょっと Gradle で手書きするのに必要なので載せました。

Maven2/3 の設定

次は Maven2/3 で Checker Framework を使う方法。 Maven2/3 に対しては Checker Framework 側でプラグインを作ってくれているので、pom.xml の XML 地獄以外は特に問題なく使えます。 ドキュメントは The Checker Framework Manual 「24.3 Maven plugin」にあります:

<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
         
  <modelVersion>4.0.0</modelVersion>
  <groupId>my.test</groupId>
  <artifactId>annotation-test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>Type annotation Test</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.checkerframework</groupId>
      <artifactId>checker-qual</artifactId>
      <version>1.8.1</version>
    </dependency>    
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.checkerframework</groupId>
        <artifactId>checkerframework-maven-plugin</artifactId>
        <version>1.8.1</version>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <goals>
              <goal>check</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <processors>
            <processor>org.checkerframework.checker.nullness.NullnessChecker</processor>
          </processors>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
  • マニュアルにあるリポジトリの設定は必要ありません(バージョン 1.8.0 以降)。
  • 複数のアノテーション・プロセッサが必要な場合、<checkerframework-maven-plugin> 要素下にある <processors> 要素に <processor> 要素を付け加えていけばいいんでしょう。
  • 結構あれこれとプラグインに設定ができるそうです。 詳しくはマニュアル参照。

ちょっと疑問なのが、process-classes フェーズに check ゴールを付加してるところ。 コンパイル2回してたりしない? まさかね。 もともとのコンパイルはスキップしたりしてるのかな?

Gradle の設定

最後は Gradle。 Gradle に関してもマニュアルに設定方法が書いてるのですが、その方法だと、「コマンドラインから使用する」の箇所で書いたようなアーカイブのダウンロード & 展開と環境変数の設定が必要になります。 うーむ、Maven2/3 では pom.xml を書く以外は全自動だったのに、Gradle では手動のインストールが必要みたいに扱われてるのは Gradle にとって不当な扱いだ! ってことで、それらのインストールがいらない build.gradle を書いてみました。 手書きなのでちょっと汚いです。 誰か Gradle プラグイン作って。

apply plugin : 'java'

group = "my.test"
version = "1.0-SNAPSHOT"
sourceCompatibility = 1.8
targetCompatibility = 1.8

project.ext{
    enc = 'UTF-8'
    checkerVersion = '1.8.1'
    processors = [
        'nullness.NullnessChecker',
        'interning.InterningChecker'
    ].collect{ 'org.checkerframework.checker.'+it }
}

repositories.mavenCentral()

dependencies{
    ['checker', 'checker-qual', 'compiler', 'jdk8'].each{
        compile "org.checkerframework:$it:$checkerVersion"
    }
}

tasks.withType(Compile){
    options.encoding = enc

    if(it in JavaCompile){
        options.with{
            fork = true

            def jdk8JarPath = configurations.compile.files.find{
                it.name == "jdk8-${checkerVersion}.jar"
            }.absolutePath

            compilerArgs = [
                "-Xbootclasspath/p:$jdk8JarPath",
                '-processor', processors.join(',')
            ]
        }
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = "1.12"
}
  • 基本的には、「コマンドラインから使用する」の箇所に書いた4つ目の方法を Groovy & Gradle 風に書き直しただけです。 JavaCompiler タスクに最低限のオプションだけを設定しています。
  • このままだとテストコードについてもチェックが行われるので(別にいいんですけど)、テストコードのコンパイルではチェックを行わないような設定もしたいところ。
  • アノテーション・プロセッサを追加したい場合は getAnnotationProcessors() メソッドの適当な箇所 project.ext ノード下の processors プロパティの要素に追加して下さい。
  • checker-qual への依存関係はなくても Gredle でビルドする分にはいりませんが、IDE とか使う場合はいるんじゃないかと。
  • マルチ・プロジェクト(サブプロジェクトがあるプロジェクト)では、マニュアルにあるように allprojects{ ... } の ... の部分にコンパイラ等のスクリプトを書けば OK。
  • 【追記】Gradle のバージョンによってはこの build.gradle ではエンコーディングの設定がうまくいきません。 「tasks.withType(AbstractCompile){ ... }」とすると大丈夫かと。

以上、コマンドラインからの実行、Maven, Gradle の設定方法を見てきました。 マニュアルには Apache Ant についての設定も書いてあります。 必要な方はそちらをどうぞ。

Checker Framework を実際の開発に使うには、さらに IDE の方で

  • Java8 タイプ・アノテーションに対するサポートの設定
  • Checker Framework によるコンパイルの設定

を行う必要があるので、今回の記事の設定だけでは Checker Framework を導入するには不十分ですが、この記事はとりあえずこのへんで。 @Override アノテーションも今や使っている人の方が多いんじゃないかと思うので、Checker Framework も使われ出したら広まるの速いんじゃないかなぁ。 Lombok のような別ライブラリもあるけど。 なんにしろまず使ってみないことには話が始まらないね。

修正

  • 前半の日本語が変だったところをいくつか修正しました。
  • 「Gradle の設定」の build.gradle をちょっとリファクタリングしました(修正前と動作は同じだと思います)。

修正2
Gradle の箇所に【追記】しました。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

  • 作者: 佐藤聖規,和田貴久,河村雅人,米沢弘樹,山岸啓,川口耕介
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/11/11
  • メディア: 単行本(ソフトカバー)
  • 購入: 26人 クリック: 496回
  • この商品を含むブログ (65件) を見る

*1:ライセンスがどうこうという話ではありませんが。

*2:あまり正確なところは分からないのですが、Java7 でも使えるようになってるせいか、Java8 で使う場合にもコンパイラを実行するときに設定がいるのが原因じゃないかと。 まぁ、逆に言えば Java7 でも使えるようなので、ご興味のある方は試してみて下さい。 Java6 以前は非サポートなようです。 対応してた痕跡はコードに残ってますが。

*3:Windows のバージョンによるかと思いますが、「コントロールパネル ▶ システムとセキュリティ ▶ システム ▶ システムの詳細設定 ▶ 環境変数」あたりで設定できます。

ラムダ式やストリーム API や新しい日時 API だけじゃない! Java8 のタイプ・アノテーションあの手この手

Java8 ではラムダ式やストリーム API に注目が集まってますが、何気にアノテーションに関しても機能拡張がされているようなので簡単な変更点や使い方を今夜試してみました。

目次

参考

ElementType の追加

java.lang.annotation.ElementType はアノテーションを付加できる箇所を指定する定数(列挙型)です。 Java7 までは

  • PACKAGE ・・・ パッケージ宣言
  • TYPE ・・・ インターフェース、クラス宣言
  • ANOTATION_TYPE ・・・ アノテーション型宣言
  • FIELD ・・・ フィールド宣言
  • CONSTRUCTOR ・・・ コンストラクタ宣言
  • METHOD ・・・ メソッド宣言
  • PARAMETER ・・・ パラメータ(メソッド引数)宣言
  • LOCAL_VARIABLE ・・・ ローカル変数宣言

の8種類がありました。 これらはいずれも何かしらの「宣言 (declaration)」部分だけでしたが、Java8 からは

  • TYPE_PARAMETER ・・・ 型パラメータ宣言
  • TYPE_USE ・・・ 型使用箇所

が追加され、TYPE_USE は今までの宣言箇所とは違って型が使用されているあらゆるところで使用することが出来るようになりました。

TYPE_PARAMETER
ElementType.TYPE_PARAMETER はアノテーションが付加できる箇所として、ジェネリクスの型パラメータ宣言の部分を指定します。 たとえば、以下のようなアノテーションを作った場合

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_PARAMETER)
public @interface OnTypeParam {}

以下のようにアノテーションを付加することが出来ます:

public class MyClass<@OnTypeParam E>{

    public <@OnTypeParam F> F yourMethod(){
        //...
    }
}

まぁ、難しくないですね。 実行時にアノテーションを取得する方法も見ておきましょう。 まず @OnTypeParam アノテーションのリテンション・ポリシー(アノテーション情報をどの段階まで保持しておくか)を RUNTIME にしておきます:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnTypeParam {}

MyClass クラスへのアノテーションの付け方は上記と同じとして、@OnTypeParam アノテーションオブジェクトを取得するには以下のようにします:

import java.lang.reflect.TypeVariable;
import java.lang.reflect.Method;

// クラスの型パラメータ
TypeVariable<Class<MyClas>> tv = MyClass.class.getTypeParameters()[0];  // 型パラメータ1つ目
OnTypeParam tp = tv.getAnnotation(OnTypeParam.class);
System.out.println(tp);    // 「@OnTypeParam()」と表示

// メソッドの型パラメータ
TypeVariable<Method> tv = MyClass.class.getDeclaredMethod("yourMethod")
                                       .getTypeParameters()[0];
OnTypeParam tp = tv.getAnnotation(OnTypeParam.class);
System.out.println(tp);    // 「@OnTypeParam()」と表示

TypeVariable とかなんとかかんとかが面倒ですが、途中にヘンにローカル変数を入れなければあんまり気にする必要はありません。

TYPE_USE
TYPE_USE は型が使われている箇所ならどこにでもアノテーションを追加できるようにする ElementType です。 文章で説明するより具体例を見た方がいいでしょう。 以下のように @OnTypeUse アノテーションを定義しましょう:

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
public @interface OnTypeUse {}

このとき、以下のようにあちこちこのアノテーションを付加することが出来ます:

import java.util.List;
import java.util.ArrayList;

import javafx.application.Application;
import javafx.stage.Stage;

@OnTypeUse    // 型宣言
public class MyApp<@OnTypeUse E>    // 型パラメータ宣言
        extends @OnTypeUse Application{    // 拡張(or 実装)する型

    @Override
    public void start(@OnTypeUse Stage stage)    // パラメータの型
            throws @OnTypeUse Exception {    // スローする例外の型

        @OnTypeUse Number value = 0;    // ローカル変数の型

        List<@OnTypeUse Integer> list = new @OnTypeUse ArrayList<>();  // コンストラクタ呼び出し
        Integer i = (@OnTypeUse Integer)value;    // 型によるキャスト
    }

    public @OnTypeUse Integer getIndex(){    // 返り値の型
        return 1;
    }

    public <@OnTypeUse F> F getName(){    // ジェネリック・メソッドの型パラメータ宣言
        return null;
    }
}
  • ローカル変数や返り値の型などの場合、プリミティブ型でも同じようにアノテーションを付加することができます。
  • IntelliJ IDEA で試したところ、ジェネリックなメソッドの型パラメータと返り値に同時にアノテーションをつける(上記の例で、getIndex() と getName() のような付け方を同時にする)とコンパイルエラーが表示されますが、実行は可能なので IDEA の方の問題かと。
  • ローカル変数やパラメータ(メソッド引数)宣言に付ける場合、ElementType.LOCAL_VARIABLE, ElementType.PARAMETER との使い分けがちょっとややこしくなりそうな気もします。
  • import 文には付けられません。 

なんかあちこちにアノテーション付けられますね・・・ これで網羅してるかどうかも分かりませんし。 こんなにあちこちに付けられて何に使うかというと、チュートリアルや上記参考記事などを参照して下さい。 どうも外部ツールを使って @NonNull のような追加の型チェックや getter/setter の自動生成などのソースコード・ジェネレーションを行うことができるようです。 これらを試してみないと Java8 の拡張が片手落ちって気もしますが、今回はやりません。

定義済みアノテーション

次は Java8 の標準 API に追加された定義済みアノテーションを見ていきます。 主要パッケージを見たところ、

  • @Repeatable アノテーション(java.lang.annotation パッケージ)
  • @FunctionalInterface アノテーション(java.lang パッケージ)
  • @Native アノテーション

というのが追加された模様。

@Repeatable アノテーション
@Repeatable アノテーションは、1つの要素に同じアノテーションを複数付加したい場合に使用します。 たとえば、以下のように Main クラスのクラス宣言に @Grab アノテーションを2つ追加したいとしましょう:

@Grab("org.apache.commons:commons-math3:3.2")
@Grab("junit:junit:4.11")
public class Main{

    public static void main(String... args){
        // ...
    }
}

今までは Main というクラスに @Grab というアノテーションを付加する場合、1つしかできませんでしたが、Java8 からは出来るようになりました。 ただし、実際にはそれらをまとめた別のアノテーション(以下、コンテナ・アノテーション)に付け替えているような感じです。 つまり、コンテナ・アノテーションを @Grapes アノテーションとして

@Grapes({
    @Grab("org.apache.commons:commons-math3:3.2"),
    @Grab("junit:junit:4.11")
})
public class Main{

    public static void main(String... args){
        // ...
    }
}

と宣言しているのと同じになります。 ではアノテーションの定義方法を見ていきましょう。

まず、複数付加したい @Grab アノテーション の定義:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Grapes.class)
public @interface Grab {
    String value();
}

このアノテーションに @Repeatable アノテーションを付加します。 この @Repeatable アノテーションには、コンテナ・アノテーションとなる @Grapes アノテーションのクラスを指定します。 リテンション・ポリシーを指定しているのは後のためですが、通常は必須ではありません。

次は、コンテナ・アノテーションとなる @Grapes アノテーションの定義:

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Grapes {
    Grab[] value();    // @Grab アノテーションの配列を返す value() メソッド
}

コンテナ・アノテーションには value() メソッドを定義し、複数追加したいアノテーション @Grab の配列を返すようにします。 少々注意が必要なのは、アノテーション定義に付加している @Target, @Retention アノテーションの指定は @Grab に指定したものと同じでなければいけないところ。 まぁ、違ってたらコンパイラに怒られるだけですが。 以上で1つの要素に複数の @Grab アノテーションを付加できるようになります。 同じようなアノテーション定義を2つ書く必要があるのがちょっと面倒。 こういうのをアノテーション使ったソースコード/ジェネレーションでなんとかしてほしい(笑)

ちなみに、実行時にこれらのアノテーションを取得したい場合は以下のように、まず @Grapes アノテーションを取得してから、value() メソッドで @Grab アノテーションの配列を取得します:

@Grab("org.apache.commons:commons-lang3:3.3.2")
@Grab("junit:junit:4.11")
public class Main{

    public static void main(String... args){
        // まずは @Grapes アノテーションを取得
        Grapes grapes = Main.class.getDeclaredAnnotation(Grapes.class);

        // Grapes#value() によって @Grab アノテーションの配列を取得
        for(Grab grab : grapes.value()){
            System.out.println(grab.value());
        }
    }
}

まぁ、別に難しくはないですね。

@FunctionalInterface アノテーション
付加した型が SAM (Single Abstract Method) 型であること、つまり抽象メソッドが1つしかないインターフェースもしくは抽象クラスであることをコンパイル時にチェックします。 Java8 では SAM 型はラムダ式を代入できるという意味で特別扱いされてます。

@Native アノテーション
ネイティブコード(C/C++ でしょうね)から参照される定数フィールドに付加して、ヘッダファイルの生成のヒントにするらしいです。 らしいです。


Java8 で追加されたアノテーション関連の機能を見てきましたが、あんまり具体的に役立ちそうな使い方は扱ってなかったので、「だからどうした?」とつっこまれそう・・・ これらのアノテーションの機能は @Override 見たいにバグを減らすのにものすごく貢献しそうなものだそうなので、積極的に使っていきたいところ。

デートTIME

デートTIME

GDK に Java8 で導入された日時 API の拡張が追加されてたので試してみた

Java8 の目玉機能はラムダ式ですが、その次によく取り上げられるのが新しい日時 API である Date and Time API(JSR-310) でしょう。 ただ、この API はナカナカに取っつきにくいという非難の声が。 まぁ、日時の扱い自体が面倒なので仕方がないという擁護の声もありますが。

で、Java で扱いにくい API を似たような文法を保ちつつ扱い易くしよう!ってのが Groovy による JDK の拡張である GDK です。 この GDK に早くも新しい日時 API に対する拡張が追加されてたのでちょっくらイジってみました。 ただし、そもそもこの日時 API 自体にいまいち馴染んでいないので、GDK には関係なく単に JavaAPI を Groovy 上で動かしてるだけの箇所もあります。

この記事では日付 (Date) と時間 (Time) をともに扱う XxxxDateTime のみを扱い、昔ながらの java.util.Date に変換する箇所以外は、タイムゾーンを考慮しない LocalDateTime を見ていきます。 Date に変換する箇所では ZonedDateTime を扱います。

日時(~日付+時間) 日付 時間
タイムゾーンを考慮しない LocalDateTime LocalDate LocalTime
オフセット云々 OffsetDateTime - OffsetTime
タイムゾーンを考慮する ZonedDateTime - -

ちなみに使用するバージョンは、Java はもちろん8、Groovy は 4.1.2014 とします。

概要

  1. 日時の作成
  2. 文字列からの読み取り/への書き出し(パース/フォーマット)
  3. Date への/からの変換
  4. Groovy による演算子オーバーロード
  5. その他

参考

日時の作成

まずは日時の作成方法、つまり LocalDateTime オブジェクトの取得方法から見ていきましょう。 次節で見る文字列からの読み取りもよく使いますが、とりあえずそれ以外の方法のものを。 GDK は全く関係ありません。 次節以降のための準備です。 現在時刻は LocalDateTime#now() メソッドを使って取得します(コードは Groovy):

import java.time.*

def ldt1 = LocalDateTime.now()
println "It is $ldt1 now."
// 現在の日付と時刻 「It is 2014-04-01T01:23:45.678 now.」などと表示

日時の数値を指定してインスタンス生成するには LocalDateTime#of() メソッドを使います:

def ldt2 = LocalDateTime.of(2014, 4, 1, 1, 23, 45, 678000000)
assert( ldt2.toString() == '2014-04-01T01:23:45.678' )

def ldt3 = LocalDateTime.of(2014, Month.APRIL, 1, 1, 23, 45)
assert( ldt3.toString() == '2014-04-01T01:23:45' )

2つ目では月の指定に Month 定数 (Enum) を使い、ナノ秒部分を省略しました。 Enum 使えるの今どきw

次に、後で使う ZonedDateTime オブジェクトの取得方法を見てみましょう。 ZonedDateTime クラスは LocalDateTime にタイムゾーン(対応するクラスは java.time.ZoneId)の情報を付加したようなクラスです。 タイムゾーンとは 'Asia/Tokyo'*1 とか 'Greenwich' とか 'US/Hawaii' とかいうものです。  ZonedDateTime クラスの生成方法はいろいろあります。 LocalDateTime#atZone() メソッドを使うのあたりが、知らないと気づかなさそう:

//ZonedDateTime (≒ LocalDateTime + ZoneId) のインスタンス生成
// その1
def zdt1 = ZonedDateTime.now(ZoneId.systemDefault())

// その2
def zdt2 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())
assert( zdt1.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

// LocalDateTime オブジェクトを用意
def localDateTime = LocalDateTime.of(2014, 4, 1, 0, 0, 0)

// その3
def zdt3 = ZonedDateTime.of(localDateTime, ZoneId.systemDefault())

// その4
def zdt4 = localDateTime.atZone(ZoneId.systemDefault())

その他の方法は ZonedDateTime の JavaDoc でもご覧下さい。 どの方法でも ZoneId オブジェクトを何とかして取得しないといけないんですが、デフォルトのものを使うなら ZoneId#systemDefault() メソッドで取得できます。 その他のものは ZoneId#of() メソッドで取得できます。 このメソッドの引数にする利用可能な文字列一覧は ZoneId#getAvailableZoneIds() で取得できます:

// 利用可能な ZoneID (を表す文字列)の一覧を表示
ZoneId.availableZoneIds.sort{ it }.each{ println it }

文字列からの読み取り/への書き出し

次は文字列を読み取って LocalDateTime オブジェクトを構築したり、逆にフォーマットを指定して文字列に書き出す方法。 文字列から読み取るには LocalDateTime#parse() メソッドを使います:

def ldt4 = LocalDateTime.parse('2014-04-01T01:23:45')
assert( ldt4 == ldt3 )    // ldt3 は前節で作成したオブジェクト

読み取る文字列の形式は決まっていて、「yyyy-MM-ddTHH:mm:ss.nnnnnnnnn」のようにしないといけません(DateTimeFormatter#ofPattern() メソッドで他のパターンを使用可)。

  • d と H の間の T は文字
  • n はナノ秒で最大9桁
  • n 以外は桁を省略できない、つまり1時は「01」と2桁で書かないといけない
  • ナノ秒を省略したり(「yyyy-MM-ddTHH:mm:ss」)、ナノ秒と秒を省略したり(「yyyy-MM-ddTHH:mm」)できる。 分以上は省略不可っぽい

などの決まりがあります。 LocalDateTime オブジェクトの toString() メソッドはこの形式の文字列を返します。 別の形式の文字列に整形したい場合は LocalDateTime#format() メソッドと java.time.fomat.DateTimeFormatter クラスを使います:

import java.time.format.DateTimeFormatter as Formatter

assert( ldt4.format(Formatter.BASIC_ISO_DATE) == '20140401' )

独自フォーマットの DateTimeFormatter を作るのは結構大変っぽいので(「続・今日から始めるJava8 - JSR-310 Date and Time API」参照)、DateTimeFormatter の static 定数として定義されてるオブジェクトを使い回すのが無難。 ちなみに、頑張って独自の DateTimeFormatter オブジェクトを作っても、読み取りには使えなさそう DateTimeFormatter#ofPattern() メソッドを使うとできるようです。

GDK!
GDK は Date クラスに対して

def date = Date.parse('yyyy/MM/dd', '2014/04/01')
assert( date.format('yyyy/MM/dd') == '2014/04/01' )

のように、フォーマットとそれにマッチする文字列を指定して Date オブジェクトを読み取る parse() メソッドと、フォーマットを文字列で指定して書き出す format() メソッドが定義されていました。 GDK は LocalDateTime クラスにも同様のメソッド parse()/format() メソッドを追加しています:

// GDK!
// parse()
def ldt5 = LocalDateTime.parse('yyyy/MM/dd', '2014/04/01')
assert( ldt5.toString() == '2014-04-01T00:00')

// format()
assert( ldt5.format('yyyy/MM/dd') == '2014/04/01' )

Date への/からの変換

新しい日時 API で結構不評っぽいのが、昔ながらの Date オブジェクトへの変換が結構面倒だ、という点です。 まずは通常の Java API を使って変換する方法を見ていきましょう。 今まではタイムゾーンを気にしてませんでしたが、Date オブジェクトに変換するにはこれを扱う必要があるので、java.time.ZonedDateTime を考えます。 さて、まずはデフォルトの ZoneId を使うことにして ZonedDateTime と Date との変換を行ってみましょう。 この変換に際しては、

  • java.time.Instant オブジェクトを介して行う
  • java.util.Date クラスの from()/toInstant() メソッドを使う

という点に注意。 ZonedDateTime の APIjava.util.Date などの旧式のクラスに汚染されていません(作成者の意向らしいですが)。 ZonedDateTime から Date への変換は Date#from() メソッドを使います。 ZonedDateTime オブジェクトをいったん Instant オブジェクトに変換する必要があります:

def zdt1 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())

// ZonedDateTime -> Date
def date1 = Date.from(zdt1.toInstant())
assert( date1.toString() == 'Tue Apr 01 00:00:00 JST 2014' )

案外簡単。 次は逆に Date から ZonedDateTime へ変換する方法。 こちらは Date#toInstant() メソッドによって Instant オブジェクトを取得し、Instant#atZone() メソッドによって ZoneDateTime オブジェクトを取得します:

// Date -> ZonedDateTime
def zdt2 = date1.toInstant().atZone(ZoneId.systemDefault())
// もしくは
// def zdt2 = ZonedDateTime.ofInstant(date1.toInstant(), ZoneId.systemDefault())
assert( zdt2.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

さて、ちょっと別のタイムゾーンに変換してみましょうか。

def date = Date.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 00:00:00')

// Date -> ZonedDateTime で別の ZoneId に変換
def zdt3 = date.toInstant().atZone(ZoneId.of('Greenwich'))
assert( zdt3.toString() == '2014-03-31T15:00Z[Greenwich]' )

時差のせいで日付と時間がズレてますね。

GDK!
GDK はこれらの変換を簡単にするメソッドを追加しています:

def zdt1 = ZonedDateTime.of(2014, 4, 1, 0, 0, 0, ZoneId.systemDefault())

// ZonedDateTime -> Date
def date = zdt1.toDate()
assert( date.toString() == 'Tue Apr 01 00:00:00 JST 2014' )

// Date -> ZonedDateTime その1
def zdt2 = date.toZonedDateTime()
assert( zdt2.toString() == '2014-04-01T00:00+09:00[Asia/Tokyo]')

// Date -> ZonedDateTime その2
def zdt3 = date.toZonedDateTime(ZoneId.of('Greenwich'))
assert( zdt2.toString() == '2014-03-31T15:00Z[Greenwich]')

ZonedDateTime#toDate() は引数なしのものだけ。 Date#toZonedDateTime() は ZoneId を指定して変換可能。 引数を省略した場合は、システムデフォルトのタイムゾーンが使われるもよう。 タイムゾーンを指定して Date オブジェクトに変換する方法はサポートされてないようですね。 そう言えば、旧式の日付 APIjava.util.TimeZone クラスというのがありますが、ZoneId と TimeZone との変換が面倒なためか GDK が追加するメソッドでは Date オブジェクトはデフォルトのタイムゾーンになるものしか使えなさそう。 GDK がどうこう以前に Java API のレベルでタイムゾーンを変換する方法があんまりよくわからない(できるかどうか不明)なので、これは GDK のせいではないような気がします。

Groovy による演算子オーバーロード

GDK は Date クラスに対して

  • previous()/next() ・・・ ++/-- 演算子
  • plus()/minus() ・・・ +/- 演算子
  • upto()/downto() ・・・ for 文のような列挙

のようなメソッドを追加し、演算や制御構造を簡単に書けるようになってまいした:

def date1 = Date.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 00:00:00')

// previous()/next()
date1++
assert( date1.toString() == 'Wed Apr 02 00:00:00 JST 2014' )

// plus()/minus()
def date2 = date1 + 7
assert( date3.toString() == 'Wed Apr 09 00:00:00 JST 2014' )

// upto(), downto()
date1.upto(date3){ d ->
    println d
}

LocalDateTime クラスについてもほぼ同じメソッドが追加されています:

def ldt1 = LocalDateTime.parse('yyyy/MM/dd hh:mm:ss', '2014/04/01 01:23:45')

// previous()/next()
ldt1++
assert( ldt1.toString() == '2014-04-01T01:23:45' )

// plus()/minus()
def ldt2 = ldt1 + 7
assert( ldt3.toString() == '2014-04-08T01:23:45' )

// upto(), downto()
ldt1.upto(ldt3){ d ->
    println d
}

Date の場合と同じく、日時に整数を足したり引いたりする場合は日 (day) の値として計算されます。 インクリメント/ディクリメント(++/--)の場合も同じ。

ところで、LocalDateTime (や ZonedDateTime)には GDK に関係なく plus()/minus() メソッドが定義されています。 たとえば plus() メソッドの場合

  • plus(long, TemporalUnit)
  • plus(TemporalAmount)

という2つのオーバーロードがありますが、このうち、2番目の引数が1つの方は GDK によらずに「+」演算子で書くことが出来ます。 ただし、TemporalAmount オブジェクトはいまいちどこか生成・取得すればいいのかよく分かりません。 また、日 (day) 以外にも年や分を加算するメソッド plusYears() や plusMinutes() などのメソッドもあるんですが、これらは残念ながら「+」演算子で使うことはできません。 さぁ、そんな時こそ GDK!

GDK!
そんな時こそ GDK! とか言いつつ、ちょっと GDK の機能というわけでもないのですが、Groovy のチカラを使ってこういった演算を式っぽく書くことがでいます。 これには TemporalCategory というカテゴリクラスを使って、以下のように DSL っぽく書きます:

def ldt1 = LocalDateTime.parse('yyyy-MM-dd hh:mm:ss', '2014-04-01 01:23:45')

use(TemporalCateogry){
    def ldt2 = lat1 + 1.year + 7.day - 1.second
    assert( ldt2.toString() == '2015-04-08T01:23:44' )
}

TemporalCategory カテゴリが1や7のような int 値に getYear() などのメソッド(year プロパティのように書ける)を追加して、TemporalAmout オブジェクトを生成するようにしています。 いちいちカテゴリを使わないといけないのは、こういった int 値に対するメソッドをグローバルに付加するわけにはいかないからでしょう。 ちなみに、このカテゴリは upto()/downto() の拡張版で、ステップ間隔を指定できる step() メソッドにも使えます:

use(TemporalCategory){
    ldt1.step(ldt2, 12.hour){ d ->
        println d
    }
}

// c.f.
ldt1.step(ldt2, 2){ d -> // 間隔が整数なら日(day)指定
    println d
}

残念ながら previous()/next() には日(day)以外を指定できないので、秒のインクリメントなどはできません。

あと、ちょっと細かい部分ですが、Date 同士の引き算は日数を表す int が返されますが、LocalDateTime 同士の引き算は時間(間隔)を与える java.time.Duration オブジェクトになるようです。

その他

後に残ってるのは copyWith() メソッド。 これは LocalDateTime クラスに定義されている withXxxx() メソッドの拡張版です。 LocalDateTime クラスは Date と違ってイミュータブルなので、月や分だけを変更するといったことは出来ませんが、それらだけを変更した別のオブジェクトを生成する withXxxx() メソッドというものが定義されています。 Groovy には @Immutable アノテーションによってイミュータブルにしたクラスには同様のことを行う copyWith() メソッドが生成されるのですが、GDK はこれを模して LocalDataTime に copyWith() メソッドを追加しています。 引数が Map になっていて、使い方は withXxxx() メソッドより楽だと思います(Groovy 上では)

def ldt1 = LocalDateTime.parse('yyyy-MM-dd hh:mm:ss', '2014-04-01 01:23:45')

def ldt2 = ldt1.copyWith(year:2015, second:56)
assert( ldt2.toString() == '2015-04-01T01:23:56' )

手軽ですね。

まとめ

大雑把に GDK が LocalDateTime, ZonedDateTime に追加しているメソッドを見てきましたが、大体のものは Date に追加されたものの焼き直しです。 また、拙者があまり新しい日時 API に精通してるわけでもないため(昔ながらの日付 API にも詳しくないですが)、日時の取り扱いで気をつけるべき部分などをおろそかにしてそうですが(特にタイムゾーンが絡んでるあたり)、Java API の下地になっている概念をきちんと理解した上で、それらを簡単・簡潔に扱うために GDK を活用していくのがいいんじゃないでしょうか。 

Happy April 1st !

日経ソフトウエア 2014年 03月号

日経ソフトウエア 2014年 03月号

プログラミングGROOVY

プログラミングGROOVY

*1:'Asia/Tokyo' 以外に 'Japan' というのがあるのが謎なんですけど・・・

Java8 のリリースに便乗して集約プロパティってのを考えてみた

いやー、やっと Java8 が正式リリースになりましたね。 せっかくなので拙者もそれに関連しそうな記事を書こうかと。 ただし、あんまり直接関係するわけではないですが。

Java には実質的に仕様と言ってもいいような「プロパティ」というものがありますね。 これは、もともとはクラスにフィールドを定義する際に、フィールド自体はプライベートにして直接アクセス出来ないようにし、代わりに getter/setter (もしくは mutator/accessor)と呼ばれるメソッドを定義してそれらを介してフィールドにアクセスするようにしよう設計です。 プロパティはこれを少し進めて、たとえフィールドがクラスに定義されてなくても、getter/setter があればあたかもフィールドを持っているクラス設計かのように見做そう、みたいな話です。 このプロパティは JavaBeans の定義の一部にも入ってるし*1、標準 API 内の java.beans パッケージ内のクラスなどを使って操作できるし、大抵の IDE では getter/setter を自動生成できるし、Groovy などではクラスにフィールドを定義すると暗黙のうちに getter/setter メソッドを定義してプロパティにしてしまったりもします。

さて、このプロパティってのは1つのオブジェクトをフィールドとして持つかのようなクラス設計に対してのものですが、同様にして(同型の)複数のオブジェクトを保持したい場合に集約プロパティというものを考え、getter/setter メソッドのような標準的なアクセスメソッドとしてどんなものがあると最低限の処理ができそうかな、というの考えたいと思います。 えぇ、Java8 とか直接関係ありません。 Java8 から導入された java.util.Stream や java.util.function パッケージの型を少々使いますよ、って程度です。

関数型言語ではモナド理論がどうとか、filter, map, flatMap がどうとかという話を持ち出してきて、これらのメソッドが定義されてれば必要充分だってな話ができるんでしょうけど、Java8 ではラムダ式が導入されて関数型プログラミングが行いやすくなったとは言え、状態や副作用のあるなしをコードから簡単に区別できないので、状態ありきのクラス設計でいきます。 accessor だけじゃなく mutator も考えますよ、ということ。

集約プロパティにアクセスするメソッド ドラフト

集約プロパティにアクセスするメソッドを考えていきたいんですが、あれやこれやと機能を突き詰めていくと(集約の仕方、用途によって)List や Set みたいになるだろうということで(そしてむやみにメソッド数を増やすと実装が面倒なので)、簡単に実装できそうなメソッドを数個ずつ3段階(必須・オプショナル・お試し)に分けて観ていきましょう。 選定は特に何らかの根拠・裏打ちがあるって訳ではありませんが、データベースの CRUD (Create/Read/Update/Delete) を元にしてみました:

CRUD 必須 オプショナル お試し
Create
(add)
addElement(E) addElements(Collection<E>) addElements(E[])
addElements(Supplier<E>, int)
addElements(IntFunction<E>, int)
Read
(get/forEach)
forEachElement(Consumer<E>) getElements() elementStream()
Update
(set)
- setElements(Collection<E>) mapElement(UnaryOperator<E>)
Delete
(remove)
removeElements(Predicate<E>) removeElement(E)
clearElements()
-
  • E は集約プロパティの要素の型です。 通常、メソッド引数に E のコレクションを指定する場合、Collection<? extends E> や Collection<? super E> のような型を指定すべきですが(PECS もしくは Get&Put 原則*2)、宣言が長くなるので、今回はとりあえずおいておきます*3
  • メソッド名に付けている Element は、1つの(コンテナ)クラスに集約プロパティが複数種ある場合に区別するためのものです。 1種類の集約プロパティしかない場合は付けなくてもいいんじゃないでしょうかね*4。 付けない場合は addElements() などは addAll() などとしておくのが標準 API に続く慣習です*5
  • 各メソッドの返り値は以下のメソッド別の項目の箇所で見ていきます。

これらのメソッドの他には、要素数を返す

  • size() / getElementCount()

というメソッドも必要そうだけど、これは実装とかが大して難しくなさそうなので以下では考えません。

必須

まずは集約プロパティにアクセスするために必須のメソッド群。

CRUD メソッド 返り値
Create addElement(E) void (or boolean)
Read forEachElement(Consumer<E>) void
Update - -
Delete removeElements(Predicate<E>) Collection<E>

CRUD を元にするとか言って、早速 Update に対応するものを挙げてないんですがそのあたりはご愛敬w

各メソッドを見ていく前に、まずこれらの使い方を見てみましょう。 ラムダ式に慣れてれば大して難しくありません、慣れてれば。 例えば Number オブジェクトを、順序を保ったまま List のように格納する NumberContainer クラスを作成したとして、以下のように使えるようにします:

NumberContainer<Integer> ints = new NumberContainer<>();

IntStream.range(0, 10).forEach( i ->    // i が 0 から 9 まで。 別に for 文でまわしても OK
    // Create (add) : 要素の追加
    ints.addNumber(i)
);

// Read (get/forEach) : 要素の列挙
ints.forEachNumber( i -> System.out.print(i) )    // 「0123456789」と表示
System.out.println();

// Delete (remove) : 要素の削除
ints.removeNumbers( i -> i % 2 != 0 );    // 奇数を削除

ints.forEachNumber( System.out::print )    // 「02468」と表示
System.out.println();

NumberContainer 型の ints が Integer オブジェクトを集約プロパティとして持ちます。 この例では要素(Integer オブジェクト)を追加・列挙・削除しています。 では、各メソッドを見ていきましょう。

addElement(E) メソッド
これは引数のオブジェクトを集約プロパティに追加するメソッドです。 まぁ基本的な動作は問題ないかと。 ただし返り値の型はいくつか選択肢があります。

  • boolean : Java 標準 API では Collection#add() の返り値は boolean になっているので、それに合わせておく。 この返り値はコレクションの要素が変化した場合に true を、変化しなかった場合には false を返すという仕様です。 要素が変化しない場合というのは、例えば Set オブジェクトに既に保持している要素を加えたときなどです。
  • void : 集約プロパティの要素を List のように保持する場合には同じオブジェクトでも常に要素が追加されるので、特に要素が変化したかどうかを条件分岐する必要がありません。 このように、必要がないなら返り値を void にしておいた方が実装は簡単かと思います。
  • Optional<E> : Java8 から java.util パッケージに追加された Optional オブジェクトを使うこともできます。 Optional はオブジェクトのラップみたいなもので、null の代わりに空の Optional (Optional#empty() で取得可)があって、NullPointerException を投げずにあれこれ処理を行えるメソッドが定義されています(詳しくは JavaDoc などを参照)。 ただし、addElement() メソッドによって追加されなかったオブジェクトを取得したい場合は、標準 API の Collection#add の返り値の boolean と意味がっぽくなるのでご注意を。

さて、このメソッドの簡単な実装を見てみましょう。 作るのは上記で見た、Number のサブタイプを集約プロパティとして保持する NumberContainer クラスです。 集約プロパティの要素は List として保持することにし、PECS は無視してます:

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********
    public boolean addNumber(E e){
        return this.numbers.add(e);
    }
}

返り値は boolean にしてます。 まぁ、しょーもない実装。 後からどんどんメソッドを追加していきます。

forEachElement(Consumer<E>) メソッド
Java8 で導入されたラムダ式によって最もよく使い、また強力になるのはこの forEachElement() メソッドでしょう。 もし Java7 以前で集約プロパティの値を取得・列挙したい場合は要素を何らかのコレクション(大抵 List か Set でしょうけど)に入れて返すことになります。 この場合、セキュリティのために防御的コピーを行う必要があります*6。 しかし、ラムダ式と forEach() メソッドを使えばその必要がなくなります。 コレクションの次の段階、next generation と言っていいかと思います*7。 引数には、要素を受け取り返り値を返さない関数型である Consumer 型を指定します。

実装の仕方はいろいろあると思いますが、せっかく Java8 もリリースされたことだし、なるべく Stream を使ってやってみましょう:

import java.util.function.Consumer;

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********
    public boolean addNumber(E e){ ... }

    public void forEachNumber(Consumer<E> consumer){
        this.numbers.stream().forEach(consumer);
    }
}

コレクションから Stream オブジェクトを生成するのにどの程度コストがかかるのか分かりませんが、今後どんどん最適化されていくでしょうから、素直に Stream 使うのが無難かと。 for 文を使うなら

    public void forEachNumber(Consumer<E> consumer){
        for(E e : this.numbers){
            consumer.accept(e);
        }
    }

みたいな感じですかね。

removeElements(Predicate<E>) メソッド
必須メソッドの最後は要素を削除する removeElements() メソッド。 個人的にはこの削除メソッドが Java8 を使って一番すっきり書けるようになったんではないかと思います。 Java7 までのコレクション API では、要素を削除するメソッド remove() に削除したいオブジェクトを指定する必要がありました。 この場合、String のようにリテラルが定義されている型の削除は特に問題ないんですが、そうでない型の場合には「削除したいオブジェクトを取得する」という、なんとなく虚しさ(と思ってるのは拙者だけ?)作業が必要でした。 Java8 で導入された Predicate 型を使えばこの虚しさをなくすことができます。 Predicate 型は要素を受け取り boolean 値を返す関数型で、フィルターの役目をします*8

オブジェクトを直接指定して削除しない場合、削除したオブジェクトを参照できるようにそれらのオブジェクトを返り値として返しておく方がいいでしょう。 使用するコレクションの型は List や Set など、適切な型にしておいていいと思います。

Stream をなるべく使った実装はこんな感じ:

import java.util.function.Predicate;
import java.util.stream.Collectors;

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********
    public boolean addNumber(E e) { ... }
    public void forEachNumber(Consumer<E> consumer){ ... }

    public List<E> removeNumbers(Predicate<E> predicate) {
        List<E> removes = this.numbers.stream()
                                 .filter(predicate)
                                 .collect(Collectors.toList());
        this.numbers.removeAll(removes);
        return removes;
    }
}
  • Stream#filter() メソッドとメソッド引数の Predicate オブジェクトによって、削除する要素を取得
  • Stream#collect() メソッドと Collectors#toList() で返されるコレクタによって、Stream オブジェクトを List に変換

という処理を最初にしています。 このメソッドの実装は Stream を使わずに書こうとするともっと面倒になりそうです(言うほど複雑にはなりませんが)。

実装まとめ(必須版)
結局、必須メソッドをまとめた実装はこんな感じになります:

import java.util.List;
import java.util.LinkedList;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********
    public boolean addNumber(E e) {
        return this.numbers.add(e);
    }

    public void forEachNumber(Consumer<E> consumer){
        this.numbers.stream().forEach(consumer);
    }

    public List<E> removeNumbers(Predicate<E> predicate) {
        List<E> removes = this.numbers.stream()
                                 .filter(predicate)
                                 .collect(Collectors.toList());
        this.numbers.removeAll(removes);
        return removes;
    }
}

これくらいのコード量なら、集約プロパティを持たせたいクラスそれぞれに実装するのも不可能ではないかと。 面倒さはあるでしょうけど。

オプショナル

さて、次はオプショナルなメソッド。 よく使いそうなものは定義しておくと便利かもね、的なメソッドです。

CRUD メソッド 返り値
Create addElements(Collection<E>) void (or boolean)
Read getElements() Collection<E>
Update setElements(Collection<E>) void
Delete removeElement(E)
clearElements()
Optional<E>
void

これらを使ったコードはこんな感じになります:

NumberContainer<Integer> ints = new NumberContainer<>();

// Create (add) : 要素の追加
ints.addNumbers(Arrays.asList(0, 1, 2, 3));

// Read (get/forEach) : 要素の列挙
for(Integer i : ints.getElements()){
    System.out.print(i);    // 「0123」と表示
}
System.out.println();

// Update (set) : 要素の更新
ints.setNumbers(Arrays.asList(0, 1, 2, 3, 4, 5));
ints.forEachNumber( System.out::print )    // 「012345」と表示
System.out.println();

// Delete (remove) : 要素の削除
ints.removeNumber(5);    // 5を削除
ints.forEachNumber( System.out::print )    // 「01234」と表示
System.out.println();

ints.clearNumbers();    // 要素をすべて削除

これらの処理は必須メソッドを使って行うことも出来ますが、よく使う処理は別途定義しておくと楽。 ではそれぞれのメソッドを見ていきましょう。

addElements(Collection<E>) メソッド
これは複数の要素をまとめて追加したいときに使うメソッド。 Collection#addAll() に対応するものですね。 引数の Collection 型は場合によって List や Set に限定しておいていいと思います。 返り値は addElement() の場合と同じくいくつかの候補があります:

  • boolean : 集約プロパティの要素が変更された場合に true、そうでない場合に false を返す
  • void : 要素の変更を知る必要がないなら void で充分
  • Collection<E> : 追加した要素、もしくは追加されなかった要素を知りたい場合はそれらを返す。 addElement() のときの Optional<E> にあたるもの。

要素をコレクションで保持するなら実装は簡単:

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********

    //********** optional **********
    public boolean addNumbers(List<E> es){
        return this.numbers.addAll(es);
    }
}

必須メソッドの addElement() メソッドだけを使って実装することもできますが、boolean を返す場合には見た目がちょっと複雑に:

    public boolean addNumbers(List<E> es){
        boolean modified = false;
        for(E e : es){
            modified |= addNumber(e);
        }
        return modified;
    }

ちょっとやってみただけ。

getElements() メソッド
場合によっては要素をコレクションに格納して取得したい場合もあるかもしれません。 よく使うなら防御的コピーを行った get メソッドを定義しておくのもアリかと。 返り値のコレクションは List や Set にしておいても良いでしょう。

各要素を列挙して処理したい場合に、1つ forEachElement() メソッドが使いにくいことがあります。 それは各要素に対する処理が例外を投げる場合です*9。 この例外処理が定型なら forEach に組み込んでおけばいいんですが、メソッド使用者に例外処理を書かせる場合には getElement() でコレクションを返してしまう方が楽な場合もあります。 場合by場合ですが。

実装はまぁ、普通の防御的コピーで:

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********

    //********** optional **********
    public boolean addNumbers(List<E> es){ ... }

    public List<E> getNumbers(){
        return new ArrayList<>(this.numbers);
    }
}

setElements(Collection<E>) メソッド
要素を完全に入れ替えることがよくあるなら add&remove より一括更新:

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********

    //********** optional **********
    public boolean addNumbers(List<E> es){ ... }
    public List<E> getNumbers(){ ... }

    public void setNumbers(List<E> es){
        this.numbers.clear();
        this.numbers.addAll(es);
    }
}

addElements() と removeElements() (もしくは後で見る clearElements())で書くこともできます。

removeElement(E) メソッド
removeElement() メソッドは要素を直接指定して削除する昔ながらの削除メソッド。 必須メソッドの removeElements() を使っても1行で書けます:

NumberContainer<Integer> ints = ...;
Integer i = ...;

ints.removeNumbers( j -> j.equals(i));
// ints.removeNumber(i) と同じ

要素の型によっては equals() メソッドじゃなく == 演算子で十分なときもありますが(列挙型のときとか)。

では実装。 まぁ削除の処理自体は簡単ですが、返り値として削除が実行されたかどうかの boolean もしくは Optional オブジェクトを返すようにする場合はちょっとコーディンが必要、といってもそんなに量はないですが:

import java.util.Optional;

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********

    //********** optional **********
    public boolean addNumbers(List<E> es){ ... }
    public List<E> getNumbers(){ ... }
    public void setNumbers(List<E> es){ ... }

    public Optional<E> removeNumber(E e){
        boolean result = this.numbers.remove(e);
        return result ? Optional.of(e) : Optional.empty();
    }
}

ちなみに Stream (と必須メソッドの removeElements())を使って書くとこんな風になります:

    public Optional<E> removeNumber(E e){
        return removeNumbers(f -> f.equals(e)).stream().findAny();
    }

Stream#findAny() メソッドが丁度いい Optional を返してくれるので使ってます。 ただ、パッと見はちょっとわかりにくい実装ですかね。

clearElements() メソッド
clearElements() メソッドも必須の removeElements() を使って書けます:

NumberContainer<Integer> ints = ...;

ints.removeNumbers( i -> true );
// ints.clearNumbers() と同じ

実装はまぁ、removeElement() メソッド使ってもよし、コレクションの Collection#clear() メソッドが使えるならそれもよし、って感じです:

public class NumberContainer<E extends Number>{

    private final List<E> numbers = new LinkedList<>();

    //********** require **********

    //********** optional **********
    public boolean addNumbers(List<E> es){ ... }
    public List<E> getNumbers(){ ... }
    public void setNumbers(List<E> es){ ... }
    public Optional<E> removeNumber(E e){ ... }

    public void clearNumbers(){
        this.numbers.clear();
        // removeNumbers( i -> true );
    }
}

お試し

なんか思いの外、記事が長くなってきたので、お試しメソッドはサラッと行っちゃいましょう。 お試しメソッドは「まぁいらんだろうけど、もしかしたら・・・」程度のメソッド。

CRUD メソッド 返り値
Create addElements(E[])
addElements(IntFunction<E>, int)
addElements(Supplier<E>, int)
void (or boolean)
Collection<E>
Collection<E>
Read elementStream()
forEachElement(Predicate<E>, Consumer<E>)
getElements(Predicate<E>)
Stream<E>
void
Collection<E>
Update mapElement(UnaryOperator<E>) void
Delete - -

forEachElement() と getElements() のオーバーロードメソッドは最初の表に載せてませんでしたが、フィルタリングをする Predicate オブジェクトを指定できるようにしたものです。 これらを使ったサンプルは各メソッドの項で見ていきましょう。

addElements(E[]) メソッド
オプショナルメソッドの箇所で見た addElements() メソッドはコレクションを引数にとって一括追加を行うメソッドでした。 Java では List オブジェクトを生成するリテラル表記がないので、List を生成するのは結構面倒です(上記の例では Arrays#asList() メソッドを使ってた)。 そこで可変長引数をとる addElements() メソッドを定義しておくと便利な場合があります。 使い方は

NumberContainer<Integer> ints = new NumberContainer<>();
ints.addNumbers(2014, 3, 26);

のようになります。 実装はオプショナルな addElements() メソッドを使って

    @SuppressWarnings("unchecked")
    // @SafeVarargs
    public boolean addNumbers(E... es){
        return addNumbers(Arrays.asList(es));
    }

とできます。 悲しいのは、型パラメータの配列(E...)を使うとコンパイル時に警告が出るところ。 理由は以下を参照:

警告を抑えるには @SuppressWorning("unchecked") もしくは @SafeVarargs をメソッドに付加しておく必要があります。 要素の型を具体的な型にしておいて、型パラメータを使わないという手もありますが。

addElements(IntFunction<E>, int) メソッド
Java8 で導入された java.util.function パッケージの IntFunction 型を使って要素を追加するメソッド。 IntFunction<E> 型は、int を引数にとり E 型を返すメソッドを1つ持つ関数型です。 使用例はこんな感じ

NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers( i -> i*i, 4)

この例では0から3までの整数(0から4つの整数)に対して、その自乗 0, 1, 4, 9 を要素として追加しています。 実装は以下のような感じ:

    public List<E> addNumbers(IntFunction<E> f, int n){
        return IntStream.range(0, n)
                .mapToObj(i -> f.apply(i))
                .peek(e -> addNumber(e))
                .collect(Collectors.toList());
    }

最後の Stream#collect() は返り値のためだけのものです。 返り値を返す必要がなければ、Stream#peek() ではなく Stream#forEach() で充分です。

addElements(Supplier<E>, int) メソッド
次も上記の addElements() メソッドに似ていて、Java8 で導入された java.util.function パッケージの Supplier 型を使って要素を追加するメソッド。 Supplier<E> 型は、引数をとらずに E 型を返すメソッドを1つ持つ関数型です。 使用例としては、例えばランダムな整数を10個追加したい場合

NumberContainer<Interger> ints = new NumberContainer<>();

Random random = new Random();
ints.addNumbers( () -> random.nextInt(), 10)

のようにします。 実装は上記の addElements() を使うと簡単:

    public List<E> addNumbers(Supplier<E> supplier, int n){
        return addNumbers(i -> supplier.get(), n);
    }

elementStream() メソッド
elementStream() メソッドは、要素を Stream オブジェクトとして返すメソッド。 これがあれば要素を取得するメソッドは何でもできます。 それ以上のこと(フィルタリングやオブジェクト変換)が簡単にできます。 ただ、そういう処理がよく必要になるなら、集約プロパティとしてモデリングしたのが間違ってないかどうかを検討した方がいい気がします。

    public Stream numberStream(){
        return this.numbers.stream();
    }

forEachElement(Predicate<E>, Consumer<E>) / getElements(Predicate<E>) メソッド
要素を取得する際にフィルタリングをかけるメソッド。 フィルターの役目をするのは Predicate 型。 使い方は(forEachElement() の場合のみ):

NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers(o, 1, 2, 3, 4, 5)

ints.forEachNumber( i%2 == 0, System.out::print);    // 「024」と偶数のみ表示

実装は1行ずつ:

    public void forEachNumber(Predicate<E> filter, Consumer<E> consumer){
        this.numbers.stream().filter(filter).forEach(consumer);
    }

    public List<E> getNumbers(Predicate<E> filter){
        return stream().filter(filter).collect(Collectors.toList());
    }

必須、オプショナルの forEachElement(), getElements() メソッドの実装に filter() メソッドを挟み込んだだけです。

mapElement(UnaryOperator<E>) メソッド
最後は、集約プロパティの要素を一括変換するするメソッド。 java.util.function パッケージの UnaryOperator 型を使っています。 UnaryOperator<E> は E 型を E 型に変換する(つまり E 型を1つとって E 型を返す)メソッドを持つ関数型です。 使い方は

NumberContainer<Interger> ints = new NumberContainer<>();
ints.addNumbers(o, 1, 2, 3)

ints.mapNumbers( i -> i*i*i );    // 各要素をその3乗へ変換

ints.forEachNumber(System.out::print)    // 「01827」と表示

まぁ、あんまり使わないでしょうね。

最後に

集約プロパティを扱う上で、それは必要不可欠だ、ってものからお試しですら必要なさそうだ、ってものまでいろいろなメソッドを見てきましたが、大抵の場合には必須メソッドの3つでいいんじゃないかなぁと。 無理矢理持ち出してきたようなメソッドは、Java8 から導入された java.util.function パッケージのいろいろな型を使ってみようと思った結果出てきたものもあります。

  • (Int)Function ・・・ addElements()
  • (Unary)Operator ・・・ mapElements()
  • Predicate ・・・ removeElements()
  • Consumer ・・・ forEachElement()
  • Supplier ・・・ addElements()

java.util.function パッケージについては「java.util.function パッケージ」も参照のこと。 それはともかく、ゆくゆくは集約プロパティにアクセスするメソッドが IDE で自動生成できるようになればいいなと願います。

Scalaスケーラブルプログラミング第2版
プログラミングGROOVY

*1:JavaBeans の他の定義は「引数のないコンストラクタがある」、「Serializable を実装している」などがあったかと思います。

*2:EFFECTIVE JAVA 第2版 (The Java Series)』を参照

*3:そのうちできればいいな。 というか、今回の記事はこれをまじめにやるための下準備のつもりですが。

*4:複数の集約プロパティがある場合は単に設計が悪いだけ、とか言われそうだけど。

*5:単に add() としてメソッドの引数の型の違いでオーバーロードしようとすると、要素の型がコレクション型の場合にコンパイラに怒られます。 逆に、ジェネリクスを使わずに具体的な型で集約プロパティを定義したい場合は特に問題ないかと。

*6:たまに取得したコレクションに加えた変更が元に反映される backed collection なんてのもありますが。

*7:JavaXML を扱ってたころ(今も使えますがw)、どんな言語でも同じように使えるように DOM というのがありましたが、Java で使うなら Java のコレクションを使いたい!ってことで JDOM や dom4j ってのが開発されました。 もし今 XML を扱うライブラリを作るなら、各要素や各属性を列挙する forEachElement(), forEachAttribute() メソッドを定義することでしょう。 こういう意味で「次の段階」と言えるでしょう。

*8:predicate は「述語」という意味。 「~である」という命題を作る気持ち。

*9:この場合、forEachElement() メソッドの引数に Consumer 型自体が使えませんが。

Stream インターフェースの親である BaseStream がちゃんとある

以前の記事『Stream インターフェースの親として BaseStream というのがあるようで・・・』で java.util.stream.Stream インターフェースの親タイプとして BaseStream というのがあるようだが公開されてないか削除された、ということを書きましたが、java8-ea-b120 で久し振りに確かめたら普通にそういうインターフェースが public なインターフェースとして存在しているようで。 しかもメソッドが増えてるし。 ということで BaseStream がどんな定義なのかもう一度見ていきましょう。

インターフェース定義

まずは BaseStream インターフェースの定義:

package java.util.stream;

import java.util.Iterator;
import java.util.Spliterator;

public interface BaseStream<T, S extends BaseStream<T, S>>
        extends AutoCloseable{

    @Override void close();
    S onClose(Runnable closeHandler);

    Iterator<T> iterator();
    Spliterator<T> spliterator();

    boolean isParallel();

    S sequential();
    S parallel();
    S unordered();
}

前回から変わっているのは

  • AutoCloseable を拡張している
  • close(), onClose() メソッドが追加されている

です。 java.lang.AutoCloseable インターフェースは以下のような定義です:

package java.lang;

public interface AutoCloseable{

    void close() throws Exception;
}

これは try-with-resouces 文とともに使って、処理中に例外が発生した場合でも close() メソッドが確実に呼ばれることを保証する型です*1。 BaseStream インターフェースでは close() メソッドがオーバーライドされて例外が投げられなくなってます*2。 onClose() メソッドはストリームを閉じるときのフック・メソッドで、行いたい追加処理を Runnable オブジェクトとして渡します。 まぁ、ラムダ式を使って書くことが多くなるんじゃないかなぁ。

ちなみに、これら以外のメソッドは前回の記事を書いた時点で存在してましたが、細かい仕様の変更とかまで確認してません^ ^;)

Stream インターフェースとの関係

Stream インターフェースは、上記の BaseStream を使って

package java.util.stream;

public interface Stream<T> extends BaseStream<T,Stream<T>>{
    ...
}

と定義されています。 BaseStream に定義されている sequential(), parallel(), unordered(), onClose() メソッドは、型パラメータの2つ目の型 (S) が返されるので、Stream ではこれらの返り値のメソッドは Stream<T> になります。

まぁ、ジェネリクスが導入されてから、サブタイプでメソッドの(自分と同型の)返り値の型範囲を狭めたいときによくやる設計、って感じですね。 前に見たときに JavaDoc がおかしくなってたのは何だったんだろう・・・?

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

*1:そう言えば、id:nowokay 氏の『Java8で複数の処理を確実に実行するイディオム』で try-with-resouces 文を使ってたのはこの機能のためなのね。 そこでは java.io.Closeable (java.lang.AutoCloseable のサブタイプ)を使ってますが。 あっと、別に Stream や BaseStream は関係ないです。

*2:AutoCloseable インターフェースの JavaDoc に、サブタイプでは例外の型を狭めるか例外を投げないようにオーバライドすべし、と書いてますね。

蒐集してやんよ java.util.stream.Collectors クラス (6) - 独自コレクタを作る必要はない!? -

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 前回、簡単な独自コレクタを作ってみましたが、作ったコレクタをあちこちで使い回したりしないなら、ラムダ式と Stream インターフェースに定義されているメソッドで事足りるようです。

コレクタの種類は大別して「値を返す」ものと「コンテナ・オブジェクトを返す」ものがあることを前回見ましたが、それぞれ

  • 値を返す・・・Stream#reduce() メソッド
  • コンテナ・オブジェクト・・・Stream#collect() メソッド

を使えば同様の機能を簡単に使うことができます。 もちろん、コレクタの accumulator や combiner が簡単な(1行の)ラムダ式で書けないとか、いろいろな場所で同様のコレクタを使うとかなら、クラスとして抽出するのも OK ですが。

値を返すコレクタ ・・・reduce() メソッド

値を返すコレクタとして、文字列のストリームから全文字数を計算するコレクタを作ってみましょう。

public static class StringLengthCollector implements Collector<String, Integer>{
    	
    public Set<Collector.Characteristics> characteristics(){
        return Collections.emptySet();
    }
    
    public Supplier<Integer> resultSupplier(){
        return () -> 0;
    }
    	
    public BiFunction<Integer, String, Integer> accumulator(){
        return (i, s) ->  i + s.length();
        // return (i, s) -> { System.out.println(s); return i + s.length(); };
    }
        
    public BinaryOperator<Integer> combiner(){
        return (i, j) ->  i + j;
        //return (i, j) ->  { System.out.println("combine!"); return i + j; };
    }
}

これを使うには

Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream0.collect(new StringLengthCollector()) == 39;

Stream<String> stream1 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
assert stream1.collect(new StringLengthCollector()) == 39;

これを reduce() メソッドを使って書いてみましょう。 使う reduce() のシグニチャ

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

です。 Collector#resultSupplier() の代わりに reduce() メソッドの第1引数に初期値を渡す以外は同じです:

Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream2.reduce(0, (i, s) -> i + s.length(), (i, j) -> i + j) == 39;

Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
assert stream3.reduce(0, (i, s) -> i + s.length(), (i, j) -> i + j) == 39;

コンテナ・オブジェクトを返すコレクタ・・・collect() メソッド

コンテナ・オブジェクトを返すコレクタは前回作った、StringJoinerCollector を使い回しましょう。

public class StringJoinerCollector implements Collector<String, StringJoiner>{
    	
    public Set<Collector.Characteristics> characteristics(){
    	return EnumSet.of(STRICTLY_MUTATIVE);
    }
    
    public Supplier<StringJoiner> resultSupplier(){
        return () -> new StringJoiner(", ");
    }
    	
    public BiFunction<StringJoiner, String, StringJoiner> accumulator(){
    	return (sj, s) -> sj.add(s);
    }
        
    public BinaryOperator<StringJoiner> combiner(){
    	return (sj0, sj1) -> sj0.add(sj1);
    }
}

これを使うには

Stream<String> stream4 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(
    stream4.collect(new StringJoinerCollector()));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

Stream<String> stream5 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
System.out.println(
    stream5.collect(new StringJoinerCollector()));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

これを collect() メソッドを使って書いてみましょう。 使う collect() のシグニチャ

<R> R collect(Supplier<R> resultFactory,
              BiConsumer<R,? super T> accumulator,
              BiConsumer<R,R> combiner)

です。 コンテナ・オブジェクトを返す場合、accumulator や combiner が返り値を返す必要がない(第1引数が collect() メソッドの返り値のコンテナ・オブジェクトになる)ので、collect() メソッドの引数が BiFunction, BinaryOperator から BiConsumer に変更されています。 まぁ、ラムダ式で書く場合はあんまり違いを意識する必要はありませんが。 上記のサンプルを collect() メソッドで書くと以下のようになります:

Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(
    stream2.collect(() -> new StringJoiner(", "), (sj, s) -> sj.add(s), (sj0, sj1) -> sj0.add(sj1.toString())));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
System.out.println(
    stream2.collect(() -> new StringJoiner(", "), (sj, s) -> sj.add(s), (sj0, sj1) -> sj0.add(sj1.toString())));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

reduce(), collect() メソッド、どちらで書き換える場合もラムダ式を複数書かないといけないので、複雑になりそうならコレクタをクラスとして抽出する方が無難かもしれませんね。 あと、今回はコレクタの特性はあまり気にしていませんでしたが、並列実行やパフォーマンスなどの点から特性を指定する必要があるならコレクタ・クラスを実装すべきでしょう。

プログラミングGROOVY

プログラミングGROOVY

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

蒐集してやんよ java.util.stream.Collectors クラス (5) - 独自コレクタを作ってみよう! -

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 前回 parallel ストリームと concurrent コレクタに関してあれこれ試したワリにはにいまいちしっくり来てない状態なのですが、今回は独自コレクタを作ってみます。

あまりキチンと言及してませんでしたが、Java 8 のコレクタには大まかに2つに分類することができます。 それは「値を返す」か「コンテナ・オブジェクトを返す」かです。 コンテナ・オブジェクトとはコレクションや配列のような、(同一の型の)オブジェクトを複数保持したオブジェクトのことを指しており、状態を持つという性質があります。 この意味で StringBuilder や StringJoiner のようなコレクションでないオブジェクトもコンテナ・オブジェクトと言えるでしょう。

全要素を走査して値を計算し返すメソッドは、言語によっては inject()*1, foldLeft(), reduceLeft*2, reduce()*3 などのように、別の名前が付けられていることが多いので注意が必要です。*4

さて、java.util.stream.Collector インターフェースを実装するには、まず Collector インターフェースがどんなものかを知らないといけませんね。 Stream<T> 型のストリームに適用でき、R 型のオブジェクトを返す Collector<T, R> インターフェースはこんなの:

package java.util.stream;

public interface Collector<T,R>{

    Set<Collector.Characteristics> characteristics();

    Supplier<R> resultSupplier();
    BiFunction<R,T,R> accumulator();
    BinaryOperator<R> combiner();

    public static enum Characteristics{
        CONCURRENT,
        STRICTLY_MUTATIVE,
        UNORDERED
    }
}
  • サブインターフェースの Collector.Characteristics 定数(列挙型)は以前の記事で見たコレクタの特性です。 characteristics() メソッドでは、これらの特性のうち実装しているコレクタが持っている特性を Set として返します。 実装する際は java.util.EnumSet などを使うといいでしょう。 コンテナ・オブジェクトを返すコレクタは特性として STRICTLY_MUTATIVE を返すようにしておいた方がいい(しておかないといけない)でしょう。
  • resultSupplier() メソッドは、値を返すコレクタでは初期値を、コンテナ・オブジェクトを返すコレクタでは空コンテナを生成します
  • accumulator() メソッドは「R apply(R, T)」というシグニチャのメソッドを持つ Function<R, T, R> オブジェクトを返します:
    • 値を返すコレクタでは、それまでに計算した値(R 型)と次の要素(T 型)を受け取り、新たな値(R 型)を計算する
    • コンテナ・オブジェクトを返すコレクタでは、コンテナ・オブジェクト(R 型)と次の要素(T 型)を受け取り、コンテナ・オブジェクト(R 型)を返す
  • combiner() メソッドは「R apply(R, R)」というシグニチャのメソッドを持つ BinaryOperator<T> オブジェクトを返します。
    • 値を返すコレクタでは、別のグループ内で計算された結果値(R 型)を受け取り、それらをまとめた計算値(R 型)を返す
    • コンテナ・オブジェクトを返すコレクタでは、別々のグループ内で構築されたコンテナ・オブジェクト(R 型)を受け取り、それらを連結したコンテナ・オブジェクトを返す

CONCURRENT なコレクタがコンテナ・オブジェクトを返す場合、accumulator(), combiner() メソッドに対して第1引数と返り値が同じオブジェクトでなければならないようです。

ちなみに JavaDoc によると、コレクタの処理は

BiFunction<R,T,R> accumulator = collector.accumulator();
R result = collector.resultSupplier().get();
for (T t : data)
    result = accumulator.apply(result, t);
return result;

と等価だそうです。 ここには combiner() メソッドは現れてませんが。

ではサンプルコード。 と言っても、いまいちイイ例が思い浮かばなかったので、StringJoiner による文字列の連結を行うコレクタ(Collectors.toStringJoiner() で返されるコレクタの劣化版)を作ってみようと思います。 まぁ、文字列を連結する処理はほとんど java.util.StringJoiner まかせなんですが。

public class StringJoinerCollector implements Collector<String, StringJoiner>{
    	
    public Set<Collector.Characteristics> characteristics(){
    	return EnumSet.of(STRICTLY_MUTATIVE);
    }
    
    public Supplier<StringJoiner> resultSupplier(){
        return () -> new StringJoiner(", ");
    }
    	
    public BiFunction<StringJoiner, String, StringJoiner> accumulator(){
    	return (sj, s) -> sj.add(s);
    }
        
    public BinaryOperator<StringJoiner> combiner(){
    	return (sj0, sj1) -> sj0.add(sj1);
    }
}

このコレクタを使用するには

// sequential ストリーム
Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(stream0.collect(new StringJoinerCollector()));
    // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示
System.out.println();

// parallel ストリーム
Stream<String> stream1 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(stream1.parallel().collect(new StringJoinerCollector()));
    // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

のようにします。 sequential ストリーム、parallel ストリーム、どちらに対しても上手く動くハズです。 どちらの場合も元のストリームの順序が保存されています。 ちなみに、もし上記の StringJoinerCollector の実装で combiner() メソッドが null を返すように実装した場合、sequential ストリームに対しては普通に動きますが、parallel ストリームに対して実行すると NullPoinerException が投げられます。 これは前回見た結果と一致しますね。

そういえば、StringJoinerCollector の実装で accumulator(), combiner() の各メソッドでラムダ式を直接返す実装にしてますが、メソッドを返すたびに新たにオブジェクトが生成されたりはしないようなのでご心配なく。

プログラミングGROOVY

プログラミングGROOVY

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

*1:Groovy

*2:Scala

*3:Clojure

*4:これらの言語では collect() メソッドがコンテナ・オブジェクト(もっと狭くコレクション・オブジェクト)を返す場合に限られてます。

蒐集してやんよ java.util.stream.Collectors クラス (4) - parallel ストリーム と concurrent コレクタ -

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 Stream や Collector に関する記事をそこそこ書いてきて今さらいうのもアレですが、parallel ストリームと concurrent コレクタに関してちょっと(というか根本的に?)勘違いしてた気がするので、ちょっとここいらに関する記事を書いておくことに。

java.util.stream.Collectors クラスの static メソッドから取得できるコレクタのなかで特性 (characteristics) が CONCURRENT なのは toConcurrentMap() と groupingByConcurrent() の2つでした()。 これらのメソッドで返されるコレクタは CONCURRENT 特性を持ちますが、これらのコレクタを単に使うだけで並行処理をしてくれるワケではないようです(よく考えてみれば当たり前かな・・・)。

CONCURRENT 特性を持つコレクタは並行実行しても、したがって parallel ストリームに対して用いても返り値となるコンテナオブジェクトには同期化して処理が行われるってことだと思いますが、そうなると例えばストリームの要素をグループ化する際に sequential / parallel ストリームと groupingBy() / groupingByConcurrent() の組合せ、計4通りの実行方法があることになります。 以下でこれらを実際に試してみます。 ただし、以下のサンプルで次の DebugList クラスを用いています:

class DebugList<E> extends LinkedList<E>{
		
    @Override
    public boolean add(E e){
        // 「JRuby --> [Java, Jython]」のように --> で追加情報を表示
        System.out.printf("%20s --> %s\n", e, this);
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c){
        // 「[JRuby] ->> [Java, Jython]」のように ->> で追加情報を表示
        System.out.printf("%20s ->> %s\n", c, this);
        return super.addAll(c);
    }
}

add() と addAll() が呼ばれた時に、それぞれ「-->」, 「->>」で表示するようにしています。

Sequential ストリーム × groupingBy()
まずは完全に非並行な場合。 groupingBy() メソッドで、文字列の頭文字によるグループ化をしています。 ストリームの要素は頭文字が重なるように適当に選んでるだけです。

Stream<String> stream =
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby",
              "Fantom", "Smalltalk", "C", "C++", "C#", "JavaScript",
              "Go", "F#", "Scheme", "COBOL", "FORTRAN");

Map<Character, List<String>> dic0 = stream.collect(
    groupingBy(s -> s.charAt(0), LinkedHashMap::new, toCollection(DebugList::new)));

System.out.println();

dic0.forEach((c, list) -> println(c + " : "+list));

Map の実装として LinkedHashMap を使っているのは要素を追加した順序を保つためです。 これを実行すると以下のように表示されるはずです:

                Java --> []
              Groovy --> []
               Scala --> []
             Clojure --> []
              Jython --> [Java]
               JRuby --> [Java, Jython]
              Fantom --> []
           Smalltalk --> [Scala]
                   C --> [Clojure]
                 C++ --> [Clojure, C]
                  C# --> [Clojure, C, C++]
          JavaScript --> [Java, Jython, JRuby]
                  Go --> [Groovy]
                  F# --> [Fantom]
              Scheme --> [Scala, Smalltalk]
               COBOL --> [Clojure, C, C++, C#]
             FORTRAN --> [Fantom, F#]

J : [Java, Jython, JRuby, JavaScript]
G : [Groovy, Go]
S : [Scala, Smalltalk, Scheme]
C : [Clojure, C, C++, C#, COBOL]
F : [Fantom, F#, FORTRAN]

ストリームの各要素に対する処理はストリームの順序に従って行われています("Java" で始まり "FORTRAN" で終わる)。 また、要素はすべて add() によって(コレクタで言うと accumulator によって)追加されています。 これはまぁ、そのままの処理って感じですね。

parallel ストリーム × groupingBy()
次は parallel ストリームに対して groupingBy() を使う場合。 これって何となくマズそうな組合せの気がするけど・・・

Stream<String> stream =
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby",
              "Fantom", "Smalltalk", "C", "C++", "C#", "JavaScript",
              "Go", "F#", "Scheme", "COBOL", "FORTRAN");

Map<Character, List<String>> dic1 = stream.parallel().collect(
    groupingBy(s -> s.charAt(0), LinkedHashMap::new, toCollection(DebugList::new)));

System.out.println();

dic1.forEach((c, list) -> println(c + " : "+list));

stream.paralell() によって parallel ストリームを生成している部分以外は上記のサンプルと同じです。 これを実行すると

               COBOL --> []
              Fantom --> []
             FORTRAN --> []
               Scala --> []
              Scheme --> []
           Smalltalk --> []
             Clojure --> []
              Jython --> []
               JRuby --> [Jython]
                  C# --> []
                Java --> []
          JavaScript --> []
                   C --> []
              Groovy --> []
                 C++ --> [C]
     [Jython, JRuby] ->> [Java]
                [C#] ->> [C, C++]
         [Smalltalk] ->> [Scala]
                  Go --> []
                  F# --> []
           [FORTRAN] ->> [F#]
             [COBOL] ->> [C, C++, C#]
 [C, C++, C#, COBOL] ->> [Clojure]
        [JavaScript] ->> [Java, Jython, JRuby]
                [Go] ->> [Groovy]
       [F#, FORTRAN] ->> [Fantom]
            [Scheme] ->> [Scala, Smalltalk]

J : [Java, Jython, JRuby, JavaScript]
G : [Groovy, Go]
S : [Scala, Smalltalk, Scheme]
C : [Clojure, C, C++, C#, COBOL]
F : [Fantom, F#, FORTRAN]

少なくとも拙者の環境ではうまく動いてますね。 先ほどのサンプルと違って addAll() による(combiner による)要素の追加も行われています。 結果はキチンと先ほどのものと同じになっているので、これはこれで大丈夫っぽいですね。 マルチコアプロセッサでやったらダメだった、みたいな人いればご報告を。

sequential ストリーム × groupingByConcurrent()
次は sequential ストリームに CONCURRENT コレクタである groupingByConcurrent() を使う場合。 なんか無駄な同期があるだけで結果は上手くいくんじゃないかな?と予想。

Stream<String> stream =
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby",
              "Fantom", "Smalltalk", "C", "C++", "C#", "JavaScript",
              "Go", "F#", "Scheme", "COBOL", "FORTRAN");

Map<Character, List<String>> dic2 = stream.collect(
    groupingByConcurrent(s -> s.charAt(0), toCollection(DebugList::new)));

System.out.println();

dic2.forEach((c, list) -> println(c + " : "+list));

ほとんど、最初のサンプルで groupingBy() の代わりに groupingByConcurrent() を使っているだけです。 返される ConcurrentMap の実装は指定していません。 実行結果は

                Java --> []
              Groovy --> []
               Scala --> []
             Clojure --> []
              Jython --> [Java]
               JRuby --> [Java, Jython]
              Fantom --> []
           Smalltalk --> [Scala]
                   C --> [Clojure]
                 C++ --> [Clojure, C]
                  C# --> [Clojure, C, C++]
          JavaScript --> [Java, Jython, JRuby]
                  Go --> [Groovy]
                  F# --> [Fantom]
              Scheme --> [Scala, Smalltalk]
               COBOL --> [Clojure, C, C++, C#]
             FORTRAN --> [Fantom, F#]

S : [Scala, Smalltalk, Scheme]
J : [Java, Jython, JRuby, JavaScript]
G : [Groovy, Go]
F : [Fantom, F#, FORTRAN]
C : [Clojure, C, C++, C#, COBOL]

まぁ sequential ストリームに対して処理を行っているので、処理される要素の順序は "Java" で始まり "FORTRAN" に終わる通常の順序。 ConcurrentMap の実装クラスで(LinkedHashMap のように)追加順序を保存するものはなさそうなので結果の Map はエントリの順序が整列されていませんが、各エントリの値であるリストの内部では順序が保たれてますね。

parallel ストリーム × groupingByConcurrent()
最後は parallel ストリームと groupingByConcurrent() の合わせ技。 サンプル・コード自体は問題ないかと:

Stream<String> stream =
    Stream.of("Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby",
              "Fantom", "Smalltalk", "C", "C++", "C#", "JavaScript",
              "Go", "F#", "Scheme", "COBOL", "FORTRAN");

Map<Character, List<String>> dic3 = stream.parallel().collect(
    groupingByConcurrent(s -> s.charAt(0), toCollection(DebugList::new)));

System.out.println();

dic3.forEach((c, list) -> println(c + " : "+list));

これを実行すると

               COBOL --> []
              Fantom --> []
             FORTRAN --> [Fantom]
                  C# --> [COBOL]
           Smalltalk --> []
              Scheme --> [Smalltalk]
          JavaScript --> []
              Jython --> [JavaScript]
                   C --> [COBOL, C#]
               JRuby --> [JavaScript, Jython]
                 C++ --> [COBOL, C#, C]
               Scala --> [Smalltalk, Scheme]
             Clojure --> [COBOL, C#, C, C++]
                Java --> [JavaScript, Jython, JRuby]
                  Go --> []
              Groovy --> [Go]
                  F# --> [Fantom, FORTRAN]

C : [COBOL, C#, C, C++, Clojure]
G : [Go, Groovy]
S : [Smalltalk, Scheme, Scala]
J : [JavaScript, Jython, JRuby, Java]
F : [Fantom, FORTRAN, F#]

要素の処理順序は完全に変わってしまってますね。 ただしすべて add() による(accumulator による)追加だけしか行われていません。

さて、これら4つのサンプルをまとめると

  • 順序を保存したいなら sequential ストリーム
  • combiner を使いたくないなら groupingByConcurrent()

あと、(マルチコア環境での)パフォーマンスも重要なファクターになってくると思いますが、今日が終わってしまうんでこの辺で。 誰か試して~。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―