kagamihogeの日記

kagamihogeの日記です。

JEP 359: Records (Preview)をテキトーに訳した

http://openjdk.java.net/jeps/359

JEP 359: Records (Preview)

Author   Brian Goetz
Owner   Vicente Arturo Romero Zaldivar
Type    Feature
Scope   SE
Status  Candidate
Component   specification / language
Discussion  amber dash dev at openjdk dot java dot net
Effort  M
Duration    M
Reviewed by Alex Buckley
Created 2019/04/19 19:30
Updated 2019/08/27 23:17
Issue   8222777

Summary

Javaプログラミング言語recordsで機能拡張します。shallowlyなイミュータブルデータの透過的なホルダーとなるクラスを宣言するためのコンパクトなシンタックスがrecordです。

Motivation and Goals

Javaに対するよくある批判に"冗長すぎる(Java is too verbose)"とか"大仰(ceremony)"があります。その中で最悪なものの一つに、単なる集約をするだけの"データの入れ物(data carriers)"でしかないクラス、があります。入れ物クラスを適切に作るには、大量の低レベルの値・似たようなコードの繰り返し、間違えやすいコンストラクタ・アクセサ・equals()hashCode()toString()など、が必要です。開発者はこれら重要なメソッドなどを省略したり(結果奇妙な振る舞いやデバッグしにくいコードになる)、完全ではない代替クラスをサービスに入れたり(とりあえず動くので後でちゃんとしたクラスを作りたくなくなる)、などの誘惑に駆られます。

IDEの支援機能で入れ物クラスのコードを書くことは出来ますが、ボイラープレートから"このクラスはx,y,xの入れ物クラスです(I'm a data carrier for x, y, and z)"という設計意図を読むことは出来ません。正しく読み・書き・確認するための、Javaでシンプルな集約モデルを書けるようにします。

recordは表面的にはボイラープレート削減策として魅力的に映りますが、それよりも、セマンティックな強化が目的です。データとしてデータをモデリングする(modeling data as data)。(セマンティクスが正しければ、ポイラープレート自身が処理を行う) 。この機能は、shallowly-immutableで一般的なデータ集約を宣言するのが容易で明瞭簡潔にします。

Non-Goals

"ボイラープレート戦争(war on boilerplate)"を唱えることが目的ではありません。特に、JavaBean命名規約によるmutableクラスに関する問題は扱いません。プロパティ・メタプログラミングアノテーション駆動コード生成、などの機能追加は行いません。たとえ、これらが問題に対する"解決策(solutions)"として頻繁に提案されていたとしても、です。

Description

RecordsとはJava言語の新しい種類の型宣言です。enumのような、クラスに制限を加えた形式がrecordです。recordはそれが持つ表現(representation)を宣言し、その表現にマッチするAPIをコミットします*1。recordはクラスが通常持つ自由度を制限し、表現とAPIを分離します。その結果、recordにより簡潔さが得られます。

recordは名前と状態記述(state description)を持ちます。状態記述はrecordのコンポーネント(components)を宣言します。また、recordはボディを持つことも出来ます。以下は例です。

record Point(int x, int y) { }

recordは、データの透過的なホルダーという、シンプルなセマンティック上の意図を記述するので、recordは自動的に多数のメンバを作ります。

  • 状態記述コンポーネントそれぞれに対してprivate final fieldを付与。
  • 状態記述コンポーネントそれぞれに対してpublicなreadアクセサメソッドを作成、コンポーネントと同名で同一型。
  • 状態記述と同一シグネチャを持つpublicコンストラクタ、これは対応する引数で各フィールドを初期化する。
  • equalshashCodeの実装。2つのrecordが同一型かつ同一状態を持つ場合に等しい。
  • toStringの実装。コンポーネント名と文字列表現をすべて返す。

recordの表現は状態記述から機械的に生成し、construction, deconstruction(最初にアクセサ、次に、パターンマッチングが有ればdeconstruction patterns), 同値性(equality), 表示(display)、も同様です。

Restrictions on records

recordは、他のクラスをextends出来ず、状態記述コンポーネントのprivate finalフィールド以外のインスタンスフィールドを宣言出来ません。他のフィールドはstaticが必須です。この制約により状態記述だけが表現を定義していることを保証します。

recordは暗黙的にfinalでabstractに出来ません。recordのAPIは状態記述によってのみ定義され、あとから別クラスやrecordによる拡張が出来ないこと、をこの制約により強調しています。

recordのコンポーネントは暗黙的にfinalです。この制約は、集約データ用に広く受け入れられている、デフォルトでイミュータブル(immutable by default)を表します。

上記制限はありますが、recordは通常のクラスのようにも振る舞います。トップレベルやネスト・ジェネリック・interfaceの実装・newインスタンス化、が可能です。recordのボディに、staticメソッド・staticフィールド・static初期化子・コンストラクタ・インスタンスメソッド・インスタンス初期化子・ネストクラス、を宣言出来ます。recordと状態記述のコンポーネントにはアノテーションを付与出来ます。ネストしたrecordは暗黙的にstaticになり、これはエンクロージングインスタンスが暗黙的にrecordへ状態追加するのを避けるためです。

Explicitly declaring members of a record

状態記述から自動生成されるメンバは明示的な宣言も可能です。ただし、不用意にアクセサやequals/hashCodeを実装するとrecordの不変性を破るリスクがあります。

canonicalコンストラクタ(recordの状態記述と一致するシグネチャを持つコンストラクタ)の明示的な宣言には特別の注意が必要です。コンストラクタをformal parameter list(状態記述を指すと仮定)無しで宣言する場合、コンストラクタ正常終了時に未初期化(definitely unassigned)recordフィールドは、対応するformal parameterが有れば、暗黙的に初期化します(this.x = x)。これにより、明示的なcanonical constructorでパラメータに対してvalidationとnormalizationだけを書き、フィールド初期化を省略できます。

record Range(int lo, int hi) {
  public Range {
    if (lo > hi)  /* ここで暗黙的なコンストラクタパラメータを参照 */
      throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
  }
}

Grammar

RecordDeclaration:
  {ClassModifier} record TypeIdentifier [TypeParameters] 
    (RecordComponents) [SuperInterfaces] [RecordBody]

RecordComponents:
  {RecordComponent {, RecordComponent}}

RecordComponent:
  {Annotation} UnannType Identifier

RecordBody:
  { {RecordBodyDeclaration} }

RecordBodyDeclaration:
  ClassBodyDeclaration
  RecordConstructorDeclaration

RecordConstructorDeclaration:
  {Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
    [Throws] ConstructorBody

Annotations on record components

レコードのコンポーネントアノテーションは、そのアノテーションがレコードコンポーネント・パラメータ・フィールド・メソッド、で使用可能であれば付与出来ます。これらに対するアノテーションは暗黙的に宣言されるメンバにも自動的に適用されます。

レコードコンポーネントの型をmodifyするtype annotationは暗黙的に宣言されるメンバ(コンストラクタ・パラメータ・フィールド宣言・メソッド宣言)の型にも自動的に適用されます。メンバの明示的な宣言をする場合、対応関係にあるレコードコンポーネントの型と一致する必要があります。なおtype annotationsは含みません。

Reflection API

以下のpublicメソッドをjava.lang.Classに追加します。

  • RecordComponent[] getRecordComponents()
  • boolean isRecord()

メソッドgetRecordComponents()は新規クラスjava.lang.reflect.RecordComponentの配列を返します。配列の要素はレコードコンポーネントに対応し、その順序はレコード宣言順です。配列のRecordComponentから取得可能な情報としては、名前・型・generic type・アノテーション・アクセサメソッド、です。

メソッドisRecord()は、そのクラスがレコードとして宣言されていた場合、trueを返します。(isEnum()と似た関係)

Alternatives

Recordsは名目上のtuplesと見なせます。recordではなくstructural tuplesを実装するという案もあります。

tuplesは軽量な集約(lighterweight means of expressing some aggregates)となり、劣化集約になりがちです。

  • Javaの中核思想では名前が重要(names matter)です。クラスとそのメンバは意味のある名前を持ちますが、タプルとタプルコンポーネントはそうではありません。つまり、匿名タプルのString, Stringよりも、PersonクラスがプロパティがfirstNameと``'lastName```を持つ方が明確で明白です。
  • クラスはコンストラクタで状態のvalidationが可能ですが、タプルはそうではありません。不変データを持つ何らかの集約(数値範囲など)を、コンストラクタで強制出来れば、以降はその事に依存できます。タプルにはそれが出来ません。
  • クラスはその状態に基づく振る舞いを持ちます。状態と振る舞いを一緒にすることで可読性が上がります。タプルはrawデータの関係上そうした事は出来ません。

Dependencies

recordsはsealed types (JEP 360)と組み合わせて使うと便利です。ある種の構造体を共に形成するrecordsとsealed typesは代数的データ型(algebraic data types)と呼ぶことがあります。また、recordはパターンマッチングを適用しやすいです。recordsがAPIと状態記述を対で持つので、recordでdeconstruction patternが使用可能となり、type patternやdeconstruction patternsを使うswitch式での網羅性チェックにsealed typeが使用可能となります。

*1:It declares its representation, and commits to an API that matches that representation. たぶん何か抽象度の高い言い回しなんだけどこの場合のcommit toて何と訳せば良いのやら

RedisのKEYSを同時に何個か実行する

RedisのkeysがO(N)を実際に見る で見たようにKEYSは登録キー件数によってはかなり遅くなる。次に、特に重いKEYS *を同時に複数実行すると、タイムアウトするらしいので、そこを実際にやってみる。

ソースコード

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

後述するがタイムアウトを防止するため、以下のプロパティをapplication.ymlに設定する。

spring:
  redis:
    host: localhost
    timeout: 10000000

下記のような感じで、5スレッドでKEYS *を実行する。また、開始前にあらかじめkey0~10,000,000で1千万件エントリ作っておく。

        ExecutorService es = Executors.newFixedThreadPool(5);
        
        for (int i=0; i < 5; i++) {
            es.submit(() -> {
                System.out.println("start");
                long start = System.currentTimeMillis();
                Set<String> keys = redis.keys("*");
                
                System.out.println("##" + (System.currentTimeMillis() - start) + " " + keys.size());
            });
        }

        es.shutdown();

実行結果

30554
42794
57868
73334
88105

後のスレッドになればなるほど実行時間がかかっているのが分かる

そこでjvisualvmでスレッドを見る。すると、キレイに前のスレッドが終わってから次のスレッド、になっている。スレッド開始はほぼ同時なので、後のスレッドになるほど実行時間がかかるのと合致する。

f:id:kagamihoge:20190827213304j:plain

各種文献にあるとおり、Redisはシングルスレッドでリクエストを処理する。このため、同時にKEYS *を走らせても一つずつしか処理しない。つまり、redisへのリクエストはマルチで出せるが、2スレッド目以降は先行スレッドが終わらない限り待たされる。

また、spring-data-redisのデフォルトタイムアウトは60秒*1。なのでspring.redis.host.timeout: 10000000が無いと、上記コードは3スレッド目以降はタイムアウトする。よって、エントリ数次第ではKEYS *を下手に乱発するとタイムアウト頻発になる可能性が高い。

というわけで、マニュアルにある通りKEYS *は慎重に使うもの、という事が分かる。

KEYS(pattern)

言い換えると、このコマンドはデバッグやデータベースのスキーマの変更を行うなどの特別な操作を除いて使うべきではありません。通常のコードでは使わないでください。

http://redis.shibu.jp/commandreference/alldata.html#command-KEYS より抜粋

参考文献

*1:ドキュメント探せなかったが実際にやってみると60秒でタイムアウトした

RedisのkeysがO(N)を実際に見る

RedisのKEYSO(n)な点に注意が必要、と各種文献に書かれている。なので実際にデータ作ってみて試してみる。

ソースコード

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

Redisのエントリは以下のようにしてkey 0 - nをsetする。

        var list = redis.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                StringRedisConnection stringRedisConn = (StringRedisConnection) connection;

                for (int i = 0; i < MAX; i++) {
                    stringRedisConn.set("" + i, "hogeValue");
                }

                return null;
            }
        });

あらかじめRedisにエントリを作った上で、以下のkeysメソッドの引数を色々変えて、その実行時間を計測する。

@Autowired
StringRedisTemplate redis;

long start = System.currentTimeMillis();
Set<String> keys = redis.keys("...");
System.out.println(System.currentTimeMillis() - start);

Redisエントリ2, 4, 6, 8 ,10百万件に対し、keys nのnを0, 100, 10000, *でその件数返るコマンドを実行する。ただし、keys 100*とかでピッタリ1000件とか返るわけではないが、今回そこは重要ではないので無視する。

結果

計測結果

以下はRedis件数とkeys nの取得件数に対する速度結果表。それぞれ3回実行してその平均値を記載。

10,000,000 8,000,000 6,000,000 4,000,000 2,000,000
0 1915 1585 1400 1271 903
100 1963 1557 1426 1076 868
10000 1962 1600 1395 1094 898
* 14502 11901 8775 5642 3033

f:id:kagamihoge:20190826204117p:plain

https://keisan.casio.jp/exec/system/1403589783 で作成。

確かにおおむねO(n)になる。また、最終的な結果件数で実行時間はほぼ変化しない。

ただし、keys *で全件返すとクライアント側のオーバーヘッドが激増するため、実行時間が増す。おそらく、オーバーヘッドが全体の実行時間に影響を与え始めるn値があると思われるが調べていない。

1-2秒は確かに高速ではあるが、Redisはシングルスレッドなのでその間他の処理が出来なくなる。環境にも依るが、Redisで数秒止まるのは許容出来ない場合が多いと思われる。

KEYS(pattern)

この操作は計算時間O(n)となっているが、定数時間は非常に小さいものとなっています。(略)もちろんこのコマンドは注意深く使わないとデータベース性能を落としてしまうということを意識するにこしたことはありません。

言い換えると、このコマンドはデバッグやデータベースのスキーマの変更を行うなどの特別な操作を除いて使うべきではありません。通常のコードでは使わないでください。

http://redis.shibu.jp/commandreference/alldata.html#command-KEYS より抜粋