Java9から17で入った新機能ピックアップ

2022/03/18に公開

Java9から17で入った新機能で、実際コード書く上で使いそうなものをピックアップします。
Java8を使い続けていた人向けです。

レコード (Java16)

不変なデータを保持するクラスを簡単に定義できるようになりました。

classの代わりにrecordで定義します。
名前の後に、フィールドのリスト(コンポーネントと呼ばれます)を記載します。

public record Rectangle(int length, int width) {
}

classと同じくnewでインスタンスを生成します。

Rectangle rectangle = new Rectangle(1, 2);

recordには、下記メソッドが自動的に定義されます。

  • フィールド参照用のアクセサ
  • equals
  • hashCode
  • toString
assertThat(rectangle.length())
        .isEqualTo(1);
assertThat(rectangle.width())
        .isEqualTo(2);
assertThat(rectangle.toString())
        .isEqualTo("Rectangle[length=1, width=2]");

Rectangle other = new Rectangle(1, 2);
assertThat(rectangle.hashCode())
        .isEqualTo(other.hashCode());
assertThat(rectangle.equals(other))
        .isTrue();

独自メソッドも宣言できます。

public record Rectangle(int length, int width) {

    public int area() {
        return length * width;
    }
}

参考

テキストブロック (Java15)

複数行にまたがる文字列を宣言できます。"""で囲んで書きます。

String text = """
        1行目
        2行目
        """;

assertThat(text)
        .isEqualTo("1行目\n2行目\n");

インデントは一番浅い位置が基準になります。

String text = """
        1行目
          2行目(インデント)
        """;

assertThat(text)
        .isEqualTo("1行目\n  2行目(インデント)\n");

末尾のスペースは自動的に除去されます。
スペースを付与したい場合には\sを利用します。末尾に付与すれば、それより前のスペースも含めて維持されます。

String text = """
        1行目 \s
        2行目\s
        """;

assertThat(text)
        .isEqualTo("1行目  \n2行目 \n");

改行を付与したくない場合には、末尾に\を付与します。

String text = """
        1行目\
        2行目\
        """;

assertThat(text)
        .isEqualTo("1行目2行目");

参考

String#formatted、stripIndent、translateEscapes (Java15)

Stringクラスのインスタンスメソッドとして、formattedstripIndenttranslateEscapes が追加されました。
テキストブロックに関連しての追加のようです。

formattedメソッドは、今までString.formatで呼び出していたものを、Stringのインスタンスメソッドとして呼び出せるようにしたものです。

String formatText = "%,d円です。";
String text = formatText.formatted(1234);

assertThat(text).isEqualTo("1,234円です。");

テキストブロックでは変数埋め込みが出来ませんが、formattedメソッドを使うことで、下記のように書くことが出来ます。

String text = """
        %sさん。
        金額は%,d円です。
        """.formatted("太郎", 1234);

assertThat(text).isEqualTo("太郎さん。\n金額は1,234円です。\n");

stripIndentは複数行の文字列から、一番浅いインデント分の空白を取り除きます。

String text = " 1\n  2\n   3";

assertThat(text.stripIndent()).isEqualTo("1\n 2\n  3");

translateEscapesはエスケープシーケンスを変換します。
たとえば、\nという2文字をU+000Aに変換してくれます。

String text = "a\\n\\tb";

assertThat(text.translateEscapes()).isEqualTo("a\n\tb");

switch式 (Java14)

switchを式としても書けるようになりました。

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
    default -> throw new IllegalStateException("Invalid day: " + day);
};

上記はアロー構文を使った記述ですが、switch文同様のコロンラベル+yieldで書く方法もあります。

int numLetters = switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        yield 6;
    case TUESDAY:
        System.out.println(7);
        yield 7;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        yield 8;
    case WEDNESDAY:
        System.out.println(9);
        yield 9;
    default:
        throw new IllegalStateException("Invalid day: " + day);
};

アロー構文でも複数の式が必要な場合は、ブロック+yieldを使います。

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> {
        System.out.println(6);
        yield 6;
    }
    case TUESDAY -> {
        System.out.println(7);
        yield 7;
    }
    case THURSDAY, SATURDAY -> {
        System.out.println(8);
        yield 8;
    }
    case WEDNESDAY -> {
        System.out.println(9);
        yield 9;
    }
    default -> throw new IllegalStateException("Invalid day: " + day);
};

アロー構文はbreakを忘れて意図せずフォールスルーすることが無いので、アロー構文を使った方がよさそうです。

参考

switchの複数ラベル (Java14)

switchのラベルをカンマ区切りで複数書けるようになりました。
switch式だけでなく、既存のswitch文でも利用できます。

switch (day) {
    case MONDAY, FRIDAY, SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY, SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
    default:
        throw new IllegalStateException("Invalid day: " + day);
}

パターンマッチングinstanceof (Java16)

instanceof で型をチェックするのと同時に、その型の変数を定義できるようになりました。

Object value = 10;

if (value instanceof Integer num) {
    assertThat(num).isEqualTo(10);
}

ローカル変数の型推論(var) (Java10)

varという構文が追加され、ローカル変数の場合にはvarで型が省略できるようになりました。

var num = 1;
var list = List.of("a");

右辺で型が自明なもの(newしていたりなど)では良いと思いますが、メソッドの戻りの代入などでvarを使ってしまうと、型がわかりずらくなるので注意です。(そこまで積極的に使わなくて良いかな..)

// 型が自明(右辺でわかる)
var calculator = new Calculator();
var list = List.of("a");

// 型がわかりずらくなる
var result = compute();

privateインタフェース・メソッドのサポート (Java9)

インターフェースでprivateメソッドが定義できるようになりました。

public interface Hoge {

    private void fuga() {
    }
}

privateなので実装クラスからは参照できません。インターフェースのデフォルトメソッドやstaticメソッドからの参照になります。

Stream#toList (Java16)

StreamtoListメソッドが追加されました。
collect(Collectos.toList())で書いていた箇所が、もう少しすっきりとした形で書けるようになります。

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

StreamtoListは不変なリストを返します。そのため、そのリストに対して追加/削除/変更を行うと、UnsuptorpedOperationExceptionが発生します。
Collectors.toListで返すリストは、実装としてはArrayListになっているため、追加/削除/変更は出来ていました。そのため、Collectors.toListを置き換えると問題が出る箇所もありえるので注意が必要です。

なお、Java10でCollectors.toUnmodifiableListが追加されており、こちらも不変なリストを返すものになっています。

Stream#mapMulti (Java16)

flatMapと似たようなメソッドとしてmapMultiが追加されました。
flatMapStreamを返すのに対して、mapMultiは渡されたConsumerに対して値を設定していく形となります。

List<String> upperChars =
        Stream.of("abc", "xyz")
                .mapMulti((String str, Consumer<String> consumer) -> {
                    for (char c : str.toCharArray()) {
                        consumer.accept(String.valueOf(c).toUpperCase());
                    }
                })
                .toList();

assertThat(upperChars)
        .containsExactly("A", "B", "C", "X", "Y", "Z");

Stream#takeWhile、Stream#dropWhile (Java9)

takeWhile で指定した条件を満たす間のデータを対象とします。

// 2の乗数で100以下のものを求める
int[] results = IntStream.iterate(2, x -> x * 2)
        .takeWhile(x -> x <= 100)
        .toArray();

assertThat(results)
        .containsExactly(2, 4, 8, 16, 32, 64);

dropWhile で指定した条件を満たす間のデータを対象外とします。

// 2の乗数で100以上、1000以下のものを求める
int[] results = IntStream.iterate(2, x -> x * 2)
        .dropWhile(x -> x < 100)
        .takeWhile(x -> x <= 1000)
        .toArray();

assertThat(results)
        .containsExactly(128, 256, 512);

Stream#ofNullable (Java9)

ofNullable は、nullの場合は空のStreamを、null以外の場合は指定された値を持つ要素数1のStreamを返却します。

long count = Stream.ofNullable("a").count();
assertThat(count).isEqualTo(1);
long count = Stream.ofNullable(null).count();
assertThat(count).isEqualTo(0);

Predicate.not (Java11)

Predicate.notPredicateを反転させられるようになりました。反転のためにメソッド参照を諦めていたような場所でメソッド参照が使えるようになります。

long notEmptyCount = Stream.of("", "x", "", "x", "x")
        .filter(Predicate.not(String::isEmpty))
        .count();

assertThat(notEmptyCount).isEqualTo(3);

コレクションのファクトリメソッド(List.of、Set.of、Map.of) (Java9)

コレクションクラス(List``Set``Map)のファクトリメソッドが追加されました。
全てイミュータブル(追加/削除/変更不可)なコレクションになります。

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

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

// Map
Map<Integer, String> map1 = Map.of(1, "a", 2, "b", 3, "c");
// Map#ofEntries を使った方が、キーと値のセットがわかりやすい
Map<Integer, String> map2 = Map.ofEntries(Map.entry(1, "a"), Map.entry(2, "b"), Map.entry(3, "c"));

コレクションの変更不可なコピー作成(List.copyOf、Set.copyOf、Map.copyOf) (Java10)

ListSetMapcopyOfというstaticメソッドが追加され、変更不可な複製が生成できるようになりました。

コピー元が変更不可な場合、複製を作る必要がないので、コピー元のインスタンス自体が返却されます。

var base = List.of("a", "b", "c");

// baseは変更不可なので、base自体が返される
var copied = List.copyOf(base);
assertThat(copied == base).isTrue();
var base = Arrays.asList("a", "b", "c");

// baseは変更可能なので、変更不可とした複製が返される
var copied = List.copyOf(base);
assertThat(copied == base).isFalse();

Collection#toArray(IntFunction<T[]> generator) (Java11)

Collection#toArrayで引数に配列をnewする関数を指定できるようになりました。
以前は配列を生成して渡していた(new String[0]とか)のが、コンストラクタをメソッド参照で指定できるようになった感じです。

List<String> list = List.of("a", "b", "c");
String[] array = list.toArray(String[]::new);

Path.of (Java11)

Paths.getだったのが、Path.of で書けるようになりました。他の作法と合わせたような感じです。

Path path1 = Paths.get("dir");
Path path2 = Path.of("dir");

assertThat(path2).isEqualTo(path1);

Files.writeString、readString (Java11)

Files.writeStringが追加され、1メソッドで文字の書き込みが行えるようになりました。
同様にreadStringで1メソッドで読み込みが行えるようになりました。

Files.writeString(filePath, "あいうえお", StandardCharsets.UTF_8);

String text = Files.readString(filePath, StandardCharsets.UTF_8);

assertThat(text).isEqualTo("あいうえお");

InputStream#readAllBytes、transferTo (Java9)

InputStreamに全てを読み込むメソッドのreadAllBytesが追加されました。

byte[] results = inputStream.readAllBytes();

ファイルの場合には、Files.readAllBytesがありましたが、それがInputStreamに対しても同じようなことが出来るようになりました。

また、内容をOutputStreamに書き込むtransferToも追加されました。

inputStream.transferTo(outputStream);

String#repeat (Java11)

String#repeatで文字列を指定回数繰り返した文字列を作成できるようになりました。

String text = "a".repeat(10);
assertThat(text).isEqualTo("aaaaaaaaaa");

String#strip、stripLeading、stripTrailing (Java11)

String#trimtrimLefttrimRight と同じようなものですが、全角スペースなども対象になるところが異なります。

String text = "\n\u3000 a \u3000\n";

assertThat(text.strip()).isEqualTo("a");
assertThat(text.stripLeading()).isEqualTo("a \u3000\n");
assertThat(text.stripTrailing()).isEqualTo("\n\u3000 a");

assertThat(text.trim()).isEqualTo("\u3000 a \u3000"); // 全角スペースは消えない

String#isBlank (Java11)

String#isBlank で空白かどうか判定できます。

assertThat(" ".isBlank()).isTrue();
assertThat("\n\t\u3000".isBlank()).isTrue();

String#lines (Java11)

String#lines で行ごとの文字列のStreamを取得できるようになりました。

String text = "a\nb\r\nc";
long lineCount = text.lines().count();

assertThat(lineCount).isEqualTo(3);

String#indent (Java12)

Stringindentメソッドで、指定した文字数分のインデントを付けられるようになりました。

String base = "1\n2\n\n";
String indented = base.indent(2);

assertThat(indented).isEqualTo("  1\n  2\n  \n");

String#transform (Java12)

Stringtransformメソッドが追加されました。
Stringを引数に取るメソッドを渡すとそれを実行してその結果を返します。

int num = "1".transform(Integer::parseInt);

メソッド呼び出し1回だと、あまりメリットを感じませんが、メソッド呼び出しが続くような場合には、読みやすくなります。

int num = "12#34".transform(this::clean).transform(Integer::valueOf);

Optional#ifPresentOrElse、or (Java9)

ifPresent は値があったときのみ実行されましたが、値が無かった時のアクションも同時に実行できるifPresentOrElseが追加されました。

AtomicInteger hasValueCounter = new AtomicInteger();
AtomicInteger emptyValueCounter = new AtomicInteger();

Optional<String> value = Optional.ofNullable(null);

value.ifPresentOrElse(
        v -> hasValueCounter.incrementAndGet(),
        () -> emptyValueCounter.incrementAndGet());

assertThat(hasValueCounter.get()).isEqualTo(0);
assertThat(emptyValueCounter.get()).isEqualTo(1);

orElseと似たようなものとして、orが追加されました。
orOptionalを返すSupplierを指定します。
Optionalのままデフォルト値で補完するようなときに使えるのかもしれません。

Optional<String> value = Optional.empty();

Optional<String> result = value.or(() -> Optional.of(""));
assertThat(result.get()).isEqualTo("");

// 取り出す際に埋めるような場合には、orElseでも十分
assertThat(value.orElse("")).isEqualTo("");

Optional#stream (Java9)

streamメソッドでStreamに変換できるようになりました。
値がある場合、値が一つ格納されたStream、値が無い場合、空のStreamになります。

Optional<String> value = Optional.of("a");

long count = value.stream().count();
assertThat(count).isEqualTo(1);
Optional<String> value = Optional.empty();

long count = value.stream().count();
assertThat(count).isEqualTo(0);

Optional#isEmpty (Java11)

OptionalisEmptyが追加されました。isPresentの逆になります。

Optional<String> value = Optional.ofNullable(null);

assertThat(value.isEmpty()).isTrue();
assertThat(value.isPresent()).isFalse();

Discussion