tobiuo1990の日記

プログラマになるブログ

【Java】AtCoderのジャッジシステムアップデート(言語アップデート)の影響を確認する

AtCoderのジャッジシステムアップデートが近づいています。

アップデートされたジャッジシステムをテストするためのコンテストも本日21時から開催予定です。

atcoder.jp

ジャッジシステムのアップデートによって使用できる言語のバージョンもアップデートされます。

ここでは、その言語アップデートのなかでもJavaについて影響ありそうなことをまとめておきます。

 

使用可能なJavaのバージョン

  • 今まで              :OpenJDK1.7.0、OpenJDK1.8.0
  • アップデート後:OpenJDK11.0.6、OpenJDK1.8.0

7が使用できなくなり、11が新たに使用可能になります。

以降では、8→11間のアップデート内容の中から、競プロをする上で関係ありそうなものをピックアップします。

 

 競プロに影響ありそうな変更

影響ありそうな変更をまとめた感想を先に述べておきますが、

まぁあんまり影響ないと思います。(まとめ記事を書く意味ないやんという感じですが、影響ないことを確認できた、ということで。)

一応APIの変更をメインに取り上げていますが、実装の変更で恩恵とかがありそうなものもいくつか取り上げています。

※各変更点についてサンプルコードとかも記載しようと思っていますが、間に合っていないので後から追記する予定です。

※標準ライブラリのソースコードなどはOpenJDKに内蔵されているソースコードから引用しています 

 

Collection.toArray(IntFunction)

https://bugs.openjdk.java.net/browse/JDK-8060192

リリースバージョン:11 

Collectionインターフェースに以下の新たなtoArrayメソッドが追加されました。

default <T> T toArray(IntFunction<T> generator)

 既存のtoArrayメソッドはそのままです。

Object toArray();
<T> T
 toArray(T a);

■メリット

Collectionを配列に変換する際にほんのちょっとだけいい感じになります。

        List<Integer> list = List.of(1, 2, 3);
        Integer oldToArray = list.toArray(new Integer[list.size()]);
        Integer newToArray = list.toArray(Integer::new);

保持したい要素数が不明な時にひとまずListなどのコレクションで保持しておいて、あとから配列に変換して配列で処理する、みたいにしたいとき使えるかもしれません。

(ただ自作ライブラリとかはたいていint[]とかで作ってあって結局最初から配列でどうにかすることが多いのは私だけでしょうか...)

 

不変List/Set/Mapのstaticファクトリメソッド

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

リリースバージョン:9

 immutableなコレクションインスタンスをスッキリと作成できるようになりました。

以下はJEPに記載されているサンプルです。

【これまでの方法】

        Set<String> set1 = new HashSet<>();
        set1.add("a");
        set1.add("b");
        set1.add("c");
        set1 = Collections.unmodifiableSet(set);

        Set<String> set2 = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

        Set<String> set3 = Collections.unmodifiableSet(new HashSet<String>() {{
                add("a");
                add("b");
                add("c");
        }});
        
        Set<String> set4 = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));

【新しい方法】

        Set<String> set = Set.of("a", "b", "c");

競プロだとコードの寿命が短いかつ一人でのコーディングであることがほとんどなため、インスタンスがimmutableであることの恩恵はあまり感じられない気がします。

ただ、問題によっては数えるほどの要素しか持たないリストやセットを作りたいときもあると思うのでそんな時に手軽に作れるかと思います。(そんなとき、あるかな。。)

 

streamAPI(takeWhile、dropWhile)

https://bugs.openjdk.java.net/browse/JDK-8071597

 リリースバージョン:9

Streamインターフェースに以下の二つのデフォルトメソッドが追加されました。

default Stream<T> takeWhile​(Predicate<? super T> predicate)
default Stream<T> dropWhile​(Predicate<? super T> predicate)

takeWhileの方は、引数に渡したpredicateが最初にfalseになる要素の一つ前までを含むstreamを返し、一方dropWhileは最初にfalseになった要素以降のstreamを返します。

基本的にはソートされた状態でつかうことになると思いますが、それ以外でも使用できる場面はありそうです。

List<Integer> sortedList = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> unsortedList = List.of(3, 1, 4, 1, 5, 9, 2);

// before
System.out.println("before - take - sorted");
for (int i = 0; i < sortedList.size(); i++) {
    if (sortedList.get(i) < 5) {
        System.out.println(sortedList.get(i));
    } else {
        break;
    }
}
System.out.println("before - take - unsorted");
for (int i = 0; i < unsortedList.size(); i++) {
    if (unsortedList.get(i) < 5) {
        System.out.println(unsortedList.get(i));
    } else {
        break;
    }
}
boolean drop = true;
System.out.println("before - drop - sorted");
for (int i = 0; i < sortedList.size(); i++) {
    if (!(sortedList.get(i) < 5)) {
        drop = false;
    }
    if (!drop) {
        System.out.println(sortedList.get(i));
    }
}
drop = true;
System.out.println("before - drop - unsorted");
for (int i = 0; i < unsortedList.size(); i++) {
    if (!(unsortedList.get(i) < 5)) {
        drop = false;
    }
    if (!drop) {
        System.out.println(unsortedList.get(i));
    }
}

// after
System.out.println("after - take - sorted");
sortedList.stream().takeWhile(x -> x < 5).forEach(System.out::println);

System.out.println("after - take - unsorted");
unsortedList.stream().takeWhile(x -> x < 5).forEach(System.out::println);

System.out.println("after - drop - sorted");
sortedList.stream().dropWhile(x -> x < 5).forEach(System.out::println);

System.out.println("after - drop - unsorted");
unsortedList.stream().dropWhile(x -> x < 5).forEach(System.out::println);

 

Stringの結合処理

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

 リリースバージョン:9

これは特にコードの書き方には影響しないのですが、今までStringを+演算子で結合する場合はコンパイル時にStringBuilderでの結合に置き換えられていたのが、今後は実行時に結合処理を呼び出すようになります。

また、デフォルトで呼び出される結合のアルゴリズムとしては、最初に文字列の長さを厳密に調べたうえでその長さの配列を用意し、そこに文字列を詰めていく、という方法になるようです。

これによって+演算子での文字列結合処理は速くなるようですが、繰り返し結合する場合はもちろんStringBuilderで結合することになるので、ほとんど恩恵はなさそうです。

ここら辺の話はちょっと難しかったので以下のページを参考にさせていただきました。

https://vdocuments.mx/jep280-java-9-.html

https://www.techscore.com/blog/2017/12/09/benchmark/

 

Stringのコンパクト化

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

 リリースバージョン:9

これもまた、Stringの内部処理の話です。

これまで文字列をUTF-16の文字配列として保持していたのを、Latin-1で表現できるものはLatin-1で持つようになったということです。これはStringだけでなくStringBuilderなどでも同様です。

競プロでは扱う文字列は英数字であることが多いと思うので、メモリ使用量の削減などが期待できると思います。

一方でLatin-1に収まらない日本語などを使う場合は、一度Latin-1で作成していた配列を破棄してUTF-16で作り直すようで、パフォーマンス低下が起こる可能性もありそうです。

 

ローカル変数の型推論

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

  リリースバージョン:10

普段のコーディングではあまり需要を感じない型推論ですが*1、競プロのようにコード自体も数十行で済み、かつ寿命もACするまでの数分しかないなかで、1秒でもコーディング時間を短くしたい、という状況では使う余地はあると思います。ただ、使える場面は限られるので、宣言をvarで行うかどうか悩むくらいならこれまで通り型を宣言するほうが楽な気もします。IDEとかの補完機能で型を宣言するのもそんなにコスト高くないと思いますし。

型推論を使える場面】

  • イニシャライザを持つローカル変数
  • 拡張forループのインデックス
  • 従来のforループで宣言されたローカル変数 

var map = new HashMap<>();

for (var i = 0; i < 5; i++) {
  map.put(i, i * i);
}

for (var e : map.entrySet()) {
  System.out.println(String.format("key: %d, value: %d", e.getKey(), e.getValue()));
}

ちなみに、上記のサンプルで最初の変数宣言時の右辺でダイヤモンド演算子を省略すると、拡張for文内でgetKey()やgetValue()が使えません。理由はよくわかってないです。。ここら辺のジェネリクスの話とかをよく分かってないから調べなきゃ。

 

Character.toString()

https://bugs.openjdk.java.net/browse/JDK-8198837

   リリースバージョン:11

(文字のコードポイントを指定してStringを生成するメソッドが追加されたのですが、競プロで使う場面なさそうだったので取り消しました。)

 

String.repeat()

https://bugs.openjdk.java.net/browse/JDK-8198296

   リリースバージョン:11

以下のメソッドがStringクラスに追加されました。

public String repeat​(int count)

引数に指定した回数その文字列を連結した文字列を返すメソッドです。特定の文字列を一定回数繰り返す処理は競プロで時々求められると思いますが、その時にfor文などをいちいち用いる必要なくシンプルに書けそうです。

List<String> list = List.of("a", "b", "c");

// before
StringBuffer before = new StringBuffer();
for (int i = 0; i < list.size(); i++) {
  for (int j = 0; j <= i; j++) {
    before.append(list.get(i));
  }
}
System.out.println(before.toString());

//after
StringBuffer after = new StringBuffer();
for (int i = 0; i < list.size(); i++) {
  after.append(list.get(i).repeat(i+1));
}
System.out.println(after.toString());

 

拡張for文のバイトコード生成改善

 https://bugs.openjdk.java.net/browse/JDK-8175883

https://www.oracle.com/technetwork/java/javase/10-relnote-issues-4108729.html#JDK-8175883

 記載したものの、どういうことなのかまだきちんと把握できていません。自分が想定している変更内容をJava11で試すと相変わらずOOMEになってしまいます。

このアップデートによって拡張for文がGCをブロックしていた問題が解決された、というのがJBSなどを読んだ私の認識なのですが、実際に動かしてみるとどうもうまくいきません。JBSにも記載されているコードを手元で実行するとOOMEになってしまいます。

private static final int HALF_OF_MEMORY = (int) (Runtime.getRuntime().maxMemory() * 0.5);

public static void main(String args) {
  byte
data = new byte[HALF_OF_MEMORY];

  for (byte b : data); // <-- if you comment this line - the application finished successfully
  data = null; // this expects to discard reference -> allow to release the memory

  byte data2 = new byte[HALF_OF_MEMORY]; // the memory can't be allocated second time, if the "for" loop statement above is used

  System.out.println("Success");
}

実験として、①for文 ②拡張for文 ③while文で同じような処理をしてみると、①for文のみOOMEが発生しませんでした。

//①for文

private static final int HALF_OF_MEMORY = (int) (Runtime.getRuntime().maxMemory() * 0.5);

public static void main(String args) {
  byte data = new byte[HALF_OF_MEMORY];

  for (int i = 0; i < 1; i++) {
    byte a = data;
    System.out.println(a);
    break;
  }
  data = null;
  byte data2 = new byte[HALF_OF_MEMORY];

  System.out.println("Success");
}

//=>Success

 

//②拡張for文

private static final int HALF_OF_MEMORY = (int) (Runtime.getRuntime().maxMemory() * 0.5);

public static void main(String args) {
  byte data = new byte[HALF_OF_MEMORY];

  for (byte b : data) {
    byte a = data;
    System.out.println(a);
    break;
  }
  data = null;
  byte data2 = new byte[HALF_OF_MEMORY];

  System.out.println("Success");
}

//=> Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

 

//③while文

private static final int HALF_OF_MEMORY = (int) (Runtime.getRuntime().maxMemory() * 0.5);

public static void main(String args) {
  byte data = new byte[HALF_OF_MEMORY];

  while (true) {
    byte a = data;
    System.out.println(a);
    break;
  }
  data = null;
  byte[] data2 = new byte[HALF_OF_MEMORY];

  System.out.println("Success");
}

//=> Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

ここにピックアップしたものの、AtCoderしかやったことないので他のコンテストの事情は分かりませんが、空間計算量が厳しいことはないので、この問題が解決されていようがいまいがさほど影響はないとは思います。強いて言えば、マラソンとかでいろんな状態に対して処理を繰り返すときとかに参照しなくなったはずのインスタンスが残り続けると問題になることも出てくるのかなぁ、とかはありえそうな気もします。

とりあえず、このアップデート内容について知っている方(もしくは上記の検証方法に誤りがあればそれに気づいた方)いらっしゃったら教えてください!

 

参考

▼各バージョンでの主な変更点まとめ

http://openjdk.java.net/projects/jdk9/

https://openjdk.java.net/projects/jdk/10/

https://openjdk.java.net/projects/jdk/11/

▼その他

https://qiita.com/nowokay/items/1ce24079f4daafc73b4a

https://nowokay.hatenablog.com/entry/20180704/1530712754

https://gist.github.com/ykubota/b37a62de579dc92d02c9483974160c67

https://gist.github.com/ykubota/3afcfdac5b252bd31ae8c14b54b5d32f

 

最後に

アップデート後に初めてコンテスト参加する際は、きちんと言語を選択してからsubmitするよう気を付けてください!

また、アップデート前のコンテストの問題を開いた後に、アップデート後のコンテストの問題を開くと言語選択がリセットされているので気を付けてください。

*1:もちろん個人的な意見です。コード書く時に型の宣言をサボれるメリットに比べて、それ以降そのコードを読む際の不便のデメリットの方が大きいと感じています。