すでにJava8にアップグレードする必要がある理由
公開: 2022-03-11Javaプラットフォームの最新バージョンであるJava8は、1年以上前にリリースされました。 あるプラットフォームバージョンから別のプラットフォームバージョンへの移行には多くの問題があるため、多くの企業や開発者は以前のバージョンで作業を続けています。これは理解できることです。 それでも、多くの開発者はまだ古いバージョンのJavaで新しいアプリケーションを開始しています。 Java 8によって言語にいくつかの重要な改善がもたらされたため、これを行う正当な理由はほとんどありません。
Java 8には多くの新機能があります。最も便利で興味深い機能をいくつか紹介します:
- ラムダ式
- コレクションを操作するためのストリームAPI
-
CompletableFuture
を使用した非同期タスクチェーン - 真新しいTimeAPI
ラムダ式
ラムダは、1回以上実行するために参照して、別のコードに渡すことができるコードブロックです。 たとえば、他の言語の無名関数はラムダです。 関数と同様に、ラムダは実行時に引数を渡して、結果を変更できます。 Java 8ではラムダ式が導入されました。ラムダ式は、ラムダを作成して使用するための簡単な構文を提供します。
これによりコードがどのように改善されるかの例を見てみましょう。 ここに、2つのInteger
値を2を法として比較する単純なコンパレータがあります。
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
このクラスのインスタンスは、将来、このコンパレータが必要なコードで次のように呼び出される可能性があります。
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
新しいラムダ構文により、これをより簡単に行うことができます。 これは、 BinaryComparator
のcompare
メソッドと同じことを行う単純なラムダ式です。
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
構造には、関数と多くの類似点があります。 括弧内に、引数のリストを設定します。 構文->
は、これがラムダであることを示しています。 そして、この式の右側に、ラムダの動作を設定します。
これで、前の例を改善できます。
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
このオブジェクトで変数を定義できます。 それがどのように見えるか見てみましょう:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
これで、次のようにこの機能を再利用できます。
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
これらの例では、前の例でBinaryComparator
のインスタンスが渡されたのと同じ方法で、ラムダがsort()
メソッドに渡されていることに注意してください。 JVMはラムダを正しく解釈することをどのように知っていますか?
関数がラムダを引数として取ることができるようにするために、Java8では新しい概念である関数型インターフェースが導入されています。 機能インターフェイスは、抽象メソッドが1つしかないインターフェイスです。 実際、Java 8は、ラムダ式を関数型インターフェースの特別な実装として扱います。 これは、ラムダをメソッド引数として受け取るために、その引数の宣言された型が機能インターフェイスである必要があることを意味します。
機能インターフェイスを宣言するときに、 @FunctionalInterface
表記を追加して、開発者にそれが何であるかを示すことができます。
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
これで、メソッドsendDTO
を呼び出して、次のようにさまざまなラムダを渡してさまざまな動作を実現できます。
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
メソッドリファレンス
Lambda引数を使用すると、関数またはメソッドの動作を変更できます。 最後の例でわかるように、ラムダが別のメソッド( sendToAndroid
またはsendToIos
)を呼び出すためだけに機能する場合があります。 この特別な場合のために、Java8は便利な省略形を導入します:メソッド参照。 この省略された構文は、メソッドを呼び出すラムダを表し、 objectName::methodName
の形式になります。 これにより、前の例をさらに簡潔で読みやすくすることができます。
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
この場合、メソッドsendToAndroid
およびsendToIos
はthis
クラスに実装されています。 別のオブジェクトまたはクラスのメソッドを参照することもあります。
ストリームAPI
Java 8は、まったく新しいStream APIの形で、 Collections
と連携する新しい機能をもたらします。 この新機能はjava.util.stream
パッケージによって提供され、コレクションを使用したプログラミングへのより機能的なアプローチを可能にすることを目的としています。 これから説明するように、これは主に、先ほど説明した新しいラムダ構文のおかげで可能になります。
Stream APIは、コレクションの簡単なフィルタリング、カウント、マッピングに加えて、コレクションから情報のスライスとサブセットを取得するさまざまな方法を提供します。 機能スタイルの構文のおかげで、Stream APIを使用すると、コレクションを操作するためのより短く、より洗練されたコードが可能になります。
簡単な例から始めましょう。 すべての例でこのデータモデルを使用します。
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
2005年以降に本を書いたbooks
のコレクションのすべての著者を印刷する必要があると想像してみましょう。Java7でどのように印刷しますか?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
そして、Java 8でそれをどのように行うのでしょうか?
books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element
たった1つの表現です! コレクションでstream()
メソッドを呼び出すと、そのCollection
のすべての要素をカプセル化したStream
オブジェクトが返されます。 これは、 filter()
やmap()
)など、StreamAPIのさまざまな修飾子を使用して操作できます。 各修飾子は、変更の結果を含む新しいStream
オブジェクトを返します。これは、さらに操作できます。 .forEach()
メソッドを使用すると、結果のストリームのインスタンスごとに何らかのアクションを実行できます。
この例は、関数型プログラミングとラムダ式の密接な関係も示しています。 ストリーム内の各メソッドに渡される引数は、カスタムラムダまたはメソッド参照のいずれかであることに注意してください。 技術的には、前のセクションで説明したように、各修飾子は任意の機能インターフェイスを受け取ることができます。
Stream APIは、開発者がJavaコレクションを新しい角度から見るのに役立ちます。 ここで、各国で利用可能な言語のMap
を取得する必要があると想像してください。 これはJava7でどのように実装されますか?
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }
Java 8では、物事は少しすっきりしています。
import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
collect()
メソッドを使用すると、さまざまな方法でストリームの結果を収集できます。 ここでは、最初に国ごとにグループ化し、次に各グループを言語ごとにマップしていることがわかります。 ( groupingBy()
とtoSet()
は、どちらもCollectors
クラスの静的メソッドです。)

StreamAPIには他にも多くの機能があります。 完全なドキュメントはここにあります。 このパッケージが提供するすべての強力なツールをより深く理解するために、さらに読むことをお勧めします。
CompletableFuture
を使用した非同期タスクチェーン
Java 7のjava.util.concurrent
パッケージには、 Future<T>
というインターフェースがあります。これにより、将来の非同期タスクのステータスまたは結果を取得できます。 この機能を使用するには、次のことを行う必要があります。
- 非同期タスクの実行を管理し、進行状況を追跡する
Future
オブジェクトを生成できるExecutorService
を作成します。 -
Runnable
で実行可能なタスクを作成します。 -
ExecutorService
でタスクを実行します。これにより、Future
がステータスまたは結果にアクセスできるようになります。
非同期タスクの結果を利用するには、 Future
インターフェースのメソッドを使用して外部から進行状況を監視し、準備ができたら明示的に結果を取得して、さらにアクションを実行する必要があります。 これは、特に同時タスクが多数あるアプリケーションでは、エラーなしで実装するのはかなり複雑になる可能性があります。
ただし、Java 8では、非同期タスクのチェーンの作成と実行を可能にするCompletableFuture<T>
インターフェースを使用して、 Future
の概念がさらに進められています。 これは、Java 8で非同期アプリケーションを作成するための強力なメカニズムです。これにより、完了時に各タスクの結果を自動的に処理できるようになります。
例を見てみましょう:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
CompletableFuture.supplyAsync
メソッドは、デフォルトのExecutor
(通常はForkJoinPool
)に新しい非同期タスクを作成します。 タスクが終了すると、その結果は関数this::getLinks
への引数として自動的に提供されます。この関数は、新しい非同期タスクでも実行されます。 最後に、この第2段階の結果は、 System.out
に自動的に出力されます。 thenApply()
とthenAccept()
は、 Executors
を手動で使用せずに並行タスクを構築するのに役立ついくつかの便利なメソッドのほんの2つです。
CompletableFuture
を使用すると、複雑な非同期操作のシーケンスを簡単に管理できます。 3つのタスクでマルチステップの数学演算を作成する必要があるとします。 タスク1とタスク2は、異なるアルゴリズムを使用して最初のステップの結果を見つけます。一方だけが機能し、もう一方は失敗することがわかっています。 ただし、どちらが機能するかは、事前にわからない入力データによって異なります。 これらのタスクの結果は、タスク3の結果と合計する必要があります。 したがって、タスク1またはタスク2のいずれかの結果、およびタスク3の結果を見つける必要があります。 これを実現するために、次のように書くことができます。
import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution
Java 8がこれをどのように処理するかを調べると、3つのタスクすべてが同時に非同期で実行されることがわかります。 タスク2は例外で失敗しますが、最終結果は正常に計算および印刷されます。
CompletableFuture
を使用すると、複数のステージを持つ非同期タスクの構築がはるかに簡単になり、各ステージの完了時に実行する必要のあるアクションを正確に定義するための簡単なインターフェイスが提供されます。
Java Date and Time API
Java自身の承認で述べられているように:
Java SE 8リリース以前は、Javaの日付と時刻のメカニズムは、
java.util.Date
、java.util.Calendar
、java.util.TimeZone
クラス、およびそれらのサブクラス(java.util.GregorianCalendar
。 これらのクラスには、次のようないくつかの欠点がありました。
- Calendarクラスはタイプセーフではありませんでした。
- クラスは変更可能であるため、マルチスレッドアプリケーションでは使用できませんでした。
- 異常な月数と型安全性の欠如により、アプリケーションコードのバグは一般的でした。」
Java 8は、日付と時刻を操作するためのクラスを含む新しいjava.time
パッケージを使用して、これらの長年の問題を最終的に解決します。 それらはすべて不変であり、人気のあるフレームワークJoda-Timeに似たAPIを備えています。これは、ほとんどすべてのJava開発者が、ネイティブのDate
、 Calendar
、 TimeZone
の代わりにアプリケーションで使用します。
このパッケージの便利なクラスのいくつかを次に示します。
-
Clock
-現在の時刻、日付、タイムゾーン付きの時刻など、現在の時刻を知らせる時計。 -
Duration
、およびPeriod
-時間。Duration
は「76.8秒」などの時間ベースの値を使用し、「Period
」は「4年、6か月、12日」などの日付ベースの値を使用します。 -
Instant
-いくつかの形式の瞬間的な時点。 -
LocalDate
、LocalDateTime
、LocalTime
、Year
、YearMonth
-ISO-8601カレンダーシステムのタイムゾーンなしの日付、時刻、年、月、またはそれらの組み合わせ。 -
OffsetDateTime
、OffsetTime
「2015-08-29T14:15:30 + 01:00」など、ISO-8601カレンダーシステムのUTC/グリニッジからのオフセットがある日時。 -
ZonedDateTime
「1986-08-29T10:15:30 + 01:00ヨーロッパ/パリ」など、ISO-8601カレンダーシステムに関連付けられたタイムゾーンを持つ日時。
場合によっては、「その月の最初の火曜日」などの相対的な日付を見つける必要があります。 このような場合、 java.time
は特別なクラスTemporalAdjuster
を提供します。 TemporalAdjuster
クラスには、静的メソッドとして使用できるアジャスターの標準セットが含まれています。 これらにより、次のことが可能になります。
- 月の最初または最後の日を検索します。
- 翌月または前月の最初または最後の日を検索します。
- 年の最初または最後の日を検索します。
- 翌年または前年の最初または最後の日を検索します。
- 「6月の第1水曜日」など、1か月以内の最初または最後の曜日を検索します。
- 「次の木曜日」など、次または前の曜日を検索します。
月の最初の火曜日を取得する方法の簡単な例を次に示します。
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
まとめJava8
ご覧のとおり、Java8はJavaプラットフォームの画期的なリリースです。 特にラムダの導入により、多くの言語の変更があります。ラムダは、より機能的なプログラミング機能をJavaに導入する動きを表しています。 Stream APIは、ラムダが、すでに慣れている標準のJavaツールでの作業方法をどのように変えることができるかを示す良い例です。
また、Java 8は、非同期プログラミングを操作するためのいくつかの新機能と、その日時ツールの待望のオーバーホールをもたらします。
同時に、これらの変更はJava言語の大きな前進を表しており、Java開発をより面白く効率的にします。