Java8から17へ
2022-09-16 第98回社内勉強会
onozaty
はじめに
• Java8(LTS)を使い続けていて、そろそろJava17(LTS)にきりかえよう
かと思っている人向けに、実際に使いそうな機能を紹介
• 発表者自身は、まだJava8でコード書いていることが多い😅
レコード
レコード (Java16)
• イミュータブル(不変な)データを保持するクラスを簡単に定義でき
るようになった
• フィールドのリストを記載するだけで、下記が自動的に定義される
• コンストラクタ
• 各フィールドのアクセサ
• equals
• hashCode
• toString
• レコード・クラス - Java言語更新 - Oracle Help Center
https://github.com/onozaty/redmine-view-customize
参考
レコード (Java16)
• classの代わりにrecordで定義し、名前の後にフィールドのリストを
記載
• 下記の標準クラスと同等になる
public final class Rectangle {
private final int length;
private final int width;
public Rectangle(int length, int width) {
this.length = length;
this.width = width;
}
public int length() { return this.length; }
public int width() { return this.width; }
public boolean equals(Object obj) {...} // 省略(各フィールドを使った適切な処理)
public int hashCode() {...} // 省略(各フィールドを使った適切な処理)
public String toString() {...} // 省略(各フィールドを使った適切な処理)
}
public record Rectangle(int length, int width) { }
レコード (Java16)
// フィールドを指定して生成
Rectangle rectangle = new Rectangle(1, 2);
// getプリフィックスは付かない
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();
assertThat(rectangle.area()).isEqualTo(2);
レコード (Java16)
• 独自メソッドも宣言できる
• コンストラクタにチェック処理を追加したり、独自コンストラクタ
を追加できる
public record Rectangle(int length, int width) {
public Rectangle {
if (length < 0 || width < 0) {
throw new IllegalArgumentException();
}
}
public Rectangle(int length) {
this(length, 0);
}
}
public record Rectangle(int length, int width) {
public int area() {
return length * width;
}
}
レコード (Java16)
• レコードがあればLombokを消せるというわけでもない
• レコードはイミュータブルなものしか定義できない
• フィールド数が多くなるとBuilderが無いとしんどい
• 無理に置き換えずに、フィールドが少ないもので使っていくのがよ
さそう
• タプル的なものとか
テキストブロック
テキストブロック (Java15)
• 複数行にまたがる文字列を宣言できるようになった
• メジャーなプログラミング言語で書けなかったのはJavaくらいでは
• Programmer's Guide to Text Blocks - Oracle Help Center
https://docs.oracle.com/javase/jp/15/text-blocks/index.html
参考
テキストブロック (Java15)
• “”” で囲んで書く
• “”” の後に改行で次の行からが文字列の開始
• テキストブロック内の改行は¥nになる
• インデントは一番浅い位置が基準になる
String text = """
1行目
2行目
""";
assertThat(text)
.isEqualTo("1行目¥n2行目¥n");
String text = """
1行目
2行目(インデント)
""";
assertThat(text)
.isEqualTo("1行目¥n 2行目(インデント)¥n");
テキストブロック (Java15)
• 末尾のスペースは自動的に除去される
• スペースを付与したい場合には ¥s を利用
末尾に付与すれば、それより前のスペースも含めて維持される
• 改行を付与したくない場合には、末尾に ¥ を付与
String text = """
1行目 ¥s
2行目¥s
""";
assertThat(text)
.isEqualTo("1行目 ¥n2行目 ¥n");
String text = """
1行目¥
2行目¥
""";
assertThat(text)
.isEqualTo("1行目2行目");
テキストブロック (Java15)
• SQLをコードに埋め込むような場合や、テストコードでの比較など
で重宝しそう
• MyBatisでSQLをアノテーションで書いているが、複数行文字列が書けない
がために今まではGroovyで書いていた
@Select("""
SELECT
*
FROM
customers
WHERE
id = #{id}
""")
public Customer findOne(@Param("id") int id);
Stringクラス関連
String#formatted (Java15) ※テキストブロック関連で追加
• 今までString.formatで呼び出していたものを、Stringのインスタン
スメソッドとして呼び出せるようにしたものとしてformattedメ
ソッドが追加
String formatText = "%,d円です。";
String text = formatText.formatted(1234);
assertThat(text).isEqualTo("1,234円です。");
// String.formatで同等のコード
String text = String.format(formatText, 1234);
String#formatted (Java15) ※テキストブロック関連で追加
• テキストブロックに変数埋め込みは入らなかったが、formattedメ
ソッドを使うことで、(多少)わかりやすく書くことができる
• 変数埋め込み的なものは、別の機能改善として検討
• JEP draft: String Templates (Preview) https://openjdk.org/jeps/8273943
String text = """
%sさん。
金額は%,d円です。
""".formatted("太郎", 1234);
assertThat(text).isEqualTo("太郎さん。¥n金額は1,234円です。¥n");
String#stripIndent (Java15) ※テキストブロック関連で追加
• stripIndentは、複数行の文字列から一番浅いインデント分の空白を
取り除く
String text = " 1¥n 2¥n 3";
assertThat(text.stripIndent()).isEqualTo("1¥n 2¥n 3"); // 先頭行にあわせてインデントが1つ取り除かれた
String#translateEscapes (Java15)
• translateEscapesは、エスケープシーケンスを変換
• たとえば、¥nという2文字を改行コード(U+000A)に変換してくる
• 入力値としてスケープシーケンスを有効にしたい場合に使えそうだ
が、Unicodeエスケープ(¥u0020とか)には対応していないので注意
• https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/String.ht
ml#translateEscapes()
String text = "a¥¥n¥¥tb";
System.out.println(text); // a¥n¥tb
assertThat(text.translateEscapes()).isEqualTo("a¥n¥tb");
System.out.println(text.translateEscapes()); // a
// b
String#repeat (Java11)
• String#repeat で対象文字列を指定回数繰り返した文字列を作成でき
るようになった
String text = "a".repeat(10);
assertThat(text).isEqualTo("aaaaaaaaaa");
String#strip、stripLeading、stripTrailing (Java11)
• String#trim、trimLeft、trimRight と同じようなものだが、全角ス
ペースなども対象になるところが異なる
• trimだとASCIIで0x20以下が対象
(スペース、タブ、改行、その他制御コード)
• stripだと上記にプラスしてUnicodeのカテゴリで空白文字となるようなもの
なども対象
(全角スペースや段落区切り文字(PARAGRAPH SEPARATOR)とか)
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");
// trim系だと全角スペースは消えない
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を取得できるようになった
• 以前から Files.lines でファイルから読み込み時は同じことができていた
String text = "a¥nb¥r¥nc";
Stream<String> lines = text.lines();
assertThat(lines.count()).isEqualTo(3);
String#indent (Java12)
• String の indent メソッドで、指定した文字数分のインデントを付け
られるようになった
String base = "1¥n2¥n¥n";
String indented = base.indent(2);
assertThat(indented).isEqualTo(" 1¥n 2¥n ¥n");
String#transform (Java12)
• String を引数に取るメソッドを実行して結果を返すメソッドとして、
transform メソッドが追加
• メソッド呼び出し1回だと、あまりメリットを感じないが、メソッ
ド呼び出しが続くような場合には、処理が左から右に流れるように
書けるので、読みやすくなる
int num = "1".transform(Integer::parseInt);
// これと同じ
int num = Integer.parseInt("1");
int num = "12#34".transform(this::clean).transform(Integer::valueOf);
switch式
switch式 (Java14)
• switchを式としても書けるようになった
• 式なのでswitchが値を返すように
• 式 - Java言語更新 - Oracle Help Center
https://docs.oracle.com/javase/jp/13/language/switch-expressions.html
参考
switch式 (Java14)
• switch文で条件に応じて値を設定するようなものが、switch式に置
き換えられる
// switch文
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
default:
throw new IllegalStateException();
}
// switch式
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException();
};
switch式 (Java14)
• 新たに追加されたアローcaseラベル(アロー構文)か、今まで通りの
コロンcaseラベル+yieldで値を返す
// アローcaseラベル
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException();
};
// コロンcaseラベル+yield
int numLetters = switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
yield 6;
case TUESDAY:
yield 7;
case THURSDAY:
case SATURDAY:
yield 8;
case WEDNESDAY:
yield 9;
default:
throw new IllegalStateException();
};
switch式 (Java14)
• アローcaseラベルでも、複数の式が必要な場合は、ブロック+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();
};
switch式 (Java14)
• コロンcaseラベルでも、ラベルをカンマ区切りで複数書けるように
なった
• 既存の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();
}
switch式 (Java14)
• アローcaseラベルとコロンcaseラベルの2つの書き方があるが、ア
ローcaseラベルだとbreak忘れて意図せずフォールスルーすること
が無いので、アローcaseラベルの方がよさそう
• アローcaseラベルでブロックで書いた場合でも、yieldが無いとエラーにな
る
パターンマッチングinstanceof
パターンマッチングinstanceof (Java16)
• instanceof で型チェックするのと同時に、その型の変数を定義でき
るようになった
Object value = 10;
if (value instanceof Integer num) {
assertThat(num).isEqualTo(10);
}
// 以前だと再度キャストする必要があった
if (value instanceof Integer) {
Integer num = (Integer) value;
assertThat(num).isEqualTo(10);
}
ローカル変数の型推論
ローカル変数の型推論(var) (Java10)
• varという構文が追加され、ローカル変数の場合にはvarで型が省略
できるようになった
var num = 1;
var list = List.of("a");
// 上記は下記と同じ
int num = 1;
List<String> list = List.of("a");
ローカル変数の型推論(var) (Java10)
• 右辺で型が自明なものは良いが、メソッドの戻りの代入などは型が
わかりずらくなるので注意
• IDEですぐに型がわかるとはいえ、コードみただけで型がわかった方が良い
ので、わかりづらくなるようなパターンを避けるためにあまり使わなそう
// 型が自明(右辺でわかる)
var calculator = new Calculator();
var list = List.of("a");
// 型がわかりずらくなる
var result = compute();
インタフェースにおけるprivateメソッドのサポート
インタフェースにおけるprivateメソッドのサポート (Java9)
• インターフェースでprivateメソッドが定義できるようになった
• privateなので実装クラスからは参照できない
• インターフェースのデフォルトメソッドやstaticメソッドからの参照
public interface Hoge {
private void fuga() {
}
}
コレクション関連
コレクションのファクトリメソッド (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"));
コレクションの変更不可なコピー作成 (Java10)
• List、Set、Map に copyOf という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);
// 以前は配列自体を渡していた
String[] array = list.toArray(new String[list.size()]);
I/O関連
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メソッドで文字の書き込みが行える
ようになった
• 今までも Files.write でバイト配列は書き込めたが、それをStringのまま指定
できるようになった感じ
• 同様に readString で1メソッドで読み込みが行えるようになった
• こちらも今までは Files.readAllBytes でバイト配列は読み込めた
Files.writeString(filePath, "あいうえお", StandardCharsets.UTF_8);
String text = Files.readString(filePath, StandardCharsets.UTF_8);
assertThat(text).isEqualTo("あいうえお");
InputStream#readAllBytes、transferTo (Java9)
• InputStream に全ての内容を読み込む readAllBytes が追加された
• ファイルの場合には、Files.readAllBytes があるが、それと同じことが
InputStream に対しても出来るようになった
• 内容を OutputStream に書き込む transferTo も追加された
byte[] results = inputStream.readAllBytes();
inputStream.transferTo(outputStream);
Stream関連
Stream#toList (Java16)
• StreamにtoListメソッドが追加
• collect(Collectos.toList())で書いていた箇所が、すっきりとした形で
書けるように
List<String> list = Stream.of("a", "b").toList();
// 今まで
List<String> list = Stream.of("a", "b").collect(Collectors.toList());
Stream#toList (Java16)
• StreamのtoListは不変なリストを返す
• 追加/削除/変更を行うと、UnsuptorpedOperationExceptionが発生
• Collectors.toListで返すリストは、API仕様としては可変性は保証さ
れていないが、現在の実装としてはArrayListになっているため、追
加/削除/変更は出来てしまう状態
• collect(Collectors.toList())をtoList()に置き換えると、追加/削除/変更ができ
なくなり、問題が出る可能性もあるので注意が必要
• Java10でCollectors.toUnmodifiableListも追加されている
List<String> list = Stream.of("a", "b").toList();
list.set(0, "x"); // UnsupportedOperationException が発生
Stream.of("a", "b").toList(); // 不変
Stream.of("a", "b").collect(Collectors.toList()); // API仕様としては可変性は保証されないとなっているが、現在の実装としては可変
Stream.of("a", "b").collect(Collectors.toUnmodifiableList()); // 不変
Stream#mapMulti (Java16)
• flatMapと似たようなメソッドとしてmapMultiが追加
• flatMapがStreamを返すのに対して、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");
// flatMapで書いた場合
List<String> upperChars =
Stream.of("abc", "xyz")
.flatMap(str -> str.chars()
.mapToObj(c -> String.valueOf((char) c).toUpperCase()))
.toList();
Stream#takeWhile、dropWhile (Java9)
• takeWhileで指定した条件を満たす間のデータを対象に
• dropWhileで指定した条件を満たす間のデータを対象外に
// 2の乗数で100以下のものを求める
int[] results = IntStream.iterate(2, x -> x * 2)
.takeWhile(x -> x <= 100)
.toArray();
assertThat(results)
.containsExactly(2, 4, 8, 16, 32, 64);
// 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#takeWhile、dropWhile (Java9)
• filterとの違いとして、takeWhileは条件に一致しなくなったら
Stream処理自体を止める
• 要素が無制限なStreamや、大きなStreamの先頭の一部しか使わないような
場合に有用
• filterだと全ての要素に対して処理を行うことになる
// takeWhileだと、無制限なStreamでも条件に一致しなくなった時点で止めてくれる
IntStream.iterate(2, x -> x * 2)
.takeWhile(x -> x <= 100)
.toArray();
// filterだと、条件に一致しなくてもStream処理は継続される=終わらない
IntStream.iterate(2, x -> x * 2)
.filter(x -> x <= 100)
.toArray();
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.not で Predicate を反転させられるようになった
• 反転のためにメソッド参照を諦めていたような場所でメソッド参照
が使えるようになる
long notEmptyCount = Stream.of("", "x", "", "x", "x")
.filter(Predicate.not(String::isEmpty))
.count();
assertThat(notEmptyCount).isEqualTo(3);
// 以前は反転させたいときはラムダで
long notEmptyCount = Stream.of("", "x", "", "x", "x")
.filter(x -> !x.isEmpty())
.count();
Optional関連
Optional#ifPresentOrElse (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);
Optional#or (Java9)
• orElse と似たようなものとして、or が追加された
• or は Optional を返す 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)
• Optional に isEmpty が追加
• isPresent の逆
Optional<String> value = Optional.ofNullable(null);
assertThat(value.isEmpty()).isTrue();
assertThat(value.isPresent()).isFalse();
おわりに
おわりに
• リリースサイクルが変わり、新しい機能が半年に1回入るように
なったことで、Javaもどんどん書きやすくなっている
• 1.4から8くらいまでは、リリース間隔も数年と長くて、停滞している感じ
がした
• 機能とバージョンを紐づけてリリースタイミングを考えるのではなく、リ
リース周期を決めて、そのタイミングで出せる機能を出すといった形に
なったことで、早めにPreviewとしてリリースし、フィードバックを得れる
ようになったのは、Javaの進化のスピードを速めたのでは
• 今後もよさそうな機能が予定されているので、また次のLTSがリ
リースされたタイミングで試してみて、発表したい

Java8から17へ