Javaの進化

Apr 09 2023
過去 20 年間に Java に追加された最も重要な言語拡張 1996 年 1 月 23 日に Java が最初にリリースされ、その後何年にもわたって大幅な変更が行われてきました。私は 2000 年代初頭に J2SE 1 を使用して Java を使い始めました。

過去 20 年間に Java に追加された最も重要な言語拡張機能

Unsplashのジェシカ・ルイスによる写真

1996 年 1 月 23 日に Java が最初にリリースされ、その後何年にもわたって大幅な変更が行われてきました。私は 2000 年代初頭に Java を使い始めました。J2SE 1.3 を使用していましたが、J2SE 1.3 には現在一般的な機能が欠けていました。Java 5 では、ジェネリック、「for each」ループ、注釈、オートボクシング、アンボクシングが導入され、より最新の Java コードが生まれました。

当時の Java コードは次のようになっていました。

List users = new ArrayList();
users.add(new User("John"));
users.add(new User("Mary"));

for (Iterator iterator = users.iterator(); iterator.hasNext(); ) {
    User user = (User) iterator.next();
    System.out.println(user.getName());
}

List<User> users = new ArrayList<>();
users.add(new User("John"));
users.add(new User("Mary"));

for (User user : users) {
    System.out.println(user.getName());
}

オプション

オプションは、より堅牢で安全な方法で null 値を処理する方法として Java 8 で導入されました。Java 8 より前は、Java コードで null 値を処理するとエラーが発生しやすく、NullPointerExceptions が発生する可能性がありました。

Java の Optional は、値を含む場合と含まない場合があるコンテナー オブジェクトです。null 参照を使用する代わりに、値が存在しない可能性がある場合を表すために使用されます。Optional クラスは、含まれる値を操作するため、または値が存在しない場合を処理するための一連のメソッドを提供します。

Optional を使用することで、Java 開発者は、読みやすく保守しやすい、より簡潔で表現力豊かなコードを作成できます。オプションは、Java コードのバグの一般的な原因となる可能性がある のリスクを軽減するのにも役立ちますNullPointerExceptions

簡単な例:

Optional<String> optionalString = Optional.ofNullable(getString());
if (optionalString.isPresent()) {
    String string = optionalString.get();
    System.out.println(string);
} else {
    System.out.println("The string is not present");
}

String street = null;

if (employee != null && employee.getAddress() != null) {
    street = employee.getAddress().getStreet();
} else {
    street = "No street";
}

String street = Optional.ofNullable(employee)
        .map(Employee::getAddress)
        .map(Address::getStreet)
        .orElse("No street");

ストリームは Java Collections API に新しく追加されたもので、データのコレクションを処理および操作するための強力な方法を提供します。ストリームの導入により、Java プログラマーは、コレクションを操作するときに、より簡潔で、読みやすく、表現力豊かなコードを簡単に作成できます。Stream API は、流暢で機能的なプログラミング スタイルを提供し、開発者がデータのコレクションに対して複雑な操作を簡単に実行できるようにします。

ストリームは最先端のものである必要があります。ストリームは Java 8 (2014) で既に導入されています。それにもかかわらず、実際にはストリームに夢中になったことがないという理由で、ストリームを好まない Java プログラマーがいまだにいることに気付きました。

forEach とフィルター

ユーザーのリストがあり、まだロックされていないすべてのユーザーに対して setLocked(true) メソッドを呼び出したいとします。古い方法では、次のようにします。

for (User user : users) {
    if (!user.isLocked()) {
        user.setLocked(true);
    }
}

users.stream()
      .filter(u -> !u.isLocked())
      .forEach(u -> u.setLocked(true));

マッピングして削減する

定期的に行うもう 1 つのことは、コレクションから単一の値を計算することです。たとえば、従業員のリストのすべての給与の合計を計算します。

古い方法:

Double totalSalary = 0.0;

for (Employee employee : employees) {
    totalSalary += employee.getSalary();
}

Double totalSalary = employees.stream()
        .map(Employee::getSalary)
        .reduce(0.0, (a, b) -> a + b);

Double totalSalary = employees.stream()
        .map(Employee::getSalary)
        .reduce(0.0, Double::sum);

ラムダ式

ラムダ式は、動作をパラメーターとしてメソッドに渡す方法を提供します。それらはJava 8でも導入されました。すでにそれらを常に使用していると思います。しかし、ラムダを受け入れる独自のメソッドも実装していますか?

2 つの値を返すメソッドが必要だとします。複数の値を返すことができる Ruby のような言語は他にもあります。Java はこれを許可しません。

1 つのオプションは、値を配列に入れて配列を返すことですが、これは最も洗練された解決策ではないかもしれません。もう 1 つのオプションは、値を保持するために追加のクラスを定義することですが、これは時間とリソースの面でコストがかかる可能性があります。

ただし、より洗練された解決策があります: ラムダ式 :

User user = new User("John Doe", false);

extractFirstAndLastName(user, (firstName, lastName) -> {
    System.out.println("First name: " + firstName);
    System.out.println("Last name: " + lastName);
});

このようなメソッドを作成するには、期待されるコールバック ラムダを記述するインターフェイスを定義する必要があります。

@FunctionalInterface
public interface ExtractNames {
    void extract(String firstName, String lastName);
}

public void extractFirstAndLastName(User user, ExtractNames callback) {
    String firstName = user.getName().split(" ")[0];
    String lastName = user.getName().split(" ")[1];

    callback.extract(firstName, lastName);
}

LocalDate、LocalTime、LocalDateTime

開発者として常に問題を抱えていることが 2 つあります。エンコーディングと時間。私はこれらの両方のサポートグループを数回始めたいと思っていました.

Java 8 では、LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Duration、および Period が提供されました。日付と時刻のためのまったく新しい API で、作業がずっと楽になります。

時間の「ない」日付を取得するために、次のことをどのくらいの頻度で行ったか覚えていますか?:

Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.MILLISECOND, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.HOUR, 0);

Date date = calendar.getTime();

LocalDate date = LocalDate.now();

古い API:

SimpleDateFormat format = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss z");
format.setTimeZone(TimeZone.getTimeZone("UTC"));
System.out.println(format.format(new Date()));

ZonedDateTime dateTime = ZonedDateTime.now(ZoneId.of("UTC"));
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss z");
System.out.println(formatter.format(dateTime));

まだ古い日付をいじっている場合は、最終的に新しい API の使用を開始する必要があります。

変数

Java 10 では、ローカル変数を宣言する構文を簡素化し、ボイラープレート コードを削減するために、「var」キーワードが導入されました。Java 10 より前は、変数を宣言するときに、変数を初期化する式から型が簡単に推測できる場合でも、開発者はその型を明示的に指定する必要がありました。

「var」キーワードを使用すると、開発者は型を明示的に指定せずにローカル変数を宣言できます。代わりに、コンパイラは変数が使用されているコンテキストから変数の型を推測します。これにより、コードがより簡潔で読みやすくなり、変数の型の入力ミスによるエラーのリスクも軽減されます。

全体として、Java 10 での「var」キーワードの導入は、言語構文に対する小さいながらも重要な改善であり、Java 言語の重要な機能である型の安全性を維持しながら、コードをより簡潔で読みやすくすることができます。

String name = "John Doe";
Integer age = 30;
Double salary = 1000.0;

var name = "John Doe";
var age = 30;
var salary = 1000.0;

List<String> strings = Arrays.asList("apple", "banana", "cherry");
strings.sort((String s1, String s2) -> s1.length() - s2.length());

List<String> strings = Arrays.asList("apple", "banana", "cherry");
strings.sort((var s1, var s2) -> s1.length() - s2.length());

Java 11 より前は、サードパーティのライブラリを使用して HTTP リクエストを作成する必要がありました。HttpURLConnectionJava にはとで表される独自の API がありましたHttpClientが、これは非常に複雑で、いくつかの制限がありました。

Java 11 には、古い API を大幅に改善した新しい組み込み HTTP が含まれており、Java アプリケーションで HTTP 要求を送信し、応答を受信するための最新の使いやすい API を提供します。また、HTTP/2、認証、再試行ポリシーのサポートなど、いくつかの高度な機能も提供され、開発者が Web サービスとやり取りする高性能で信頼性の高いアプリケーションを構築するのに役立ちます。

簡単な例:

package http;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.concurrent.CompletableFuture;

public class HttpClientExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/v1/data"))
                .header("Authorization", "Bearer your-bearer-token")
                .GET()
                .build();

        // send the HTTP request asynchronously and handle the response
        CompletableFuture<HttpResponse<String>> response 
                  = client.sendAsync(request, BodyHandlers.ofString());

        // wait for the response to complete and handle any errors
        HttpResponse<String> httpResponse = response.join();
        if (httpResponse.statusCode() == 200) {
            // print the response body
            System.out.println(httpResponse.body());
        } else {
            // handle the error response
            System.err.println("HTTP error " + 
              httpResponse.statusCode() + 
              ": " + httpResponse.body());
        }
    }
}

テキスト ブロックは Java 13 で導入された機能で、より読みやすい構文で複数行の文字列リテラルを作成できます。

Java 13 より前は、複数行の文字列を作成するには、エスケープ文字を使用するか、複数の文字列を連結する必要があり、コードが読みにくく、維持しにくいものになる可能性がありました。テキスト ブロックを使用すると、エスケープ文字と文字列の連結が不要になり、コードがより簡潔になり、読みやすくなります。

テキスト ブロックの構文では、3 つの二重引用符を使用してブロックの開始と終了をマークし、開始マーカーと終了マーカーの間に文字列の内容を表示します。テキスト ブロックでは、先頭と末尾に空白を含めることもできます。これは書式設定に役立ちます。

String html = """
            <html>
                <head>
                    <title>Example</title>
                </head>
                <body>
                    <h1>Hello, world!</h1>
                </body>
            </html>
         """;

最初の Java バージョンでは、switch ステートメントに使用できる型は short、char、int、および byte のみでした。Java 5 では列挙型を使用した switch ステートメントのサポートが追加され、Java 7 では switch ステートメントで文字列を使用するためのサポートが追加され、Java 12 では switch 式が導入されました。

従来の switch ステートメントは次のようになります。

int day = 1;

switch (day) {
    case 1:
        System.out.println("Monday");
        break;
    case 2:
        System.out.println("Tuesday");
        break;
    default:
        throw new IllegalStateException("Unexpected value: " + day);
}

switch (day) {
    case 1, 2, 3, 4, 5 -> System.out.println("Weekday");
    case 6, 7 -> System.out.println("Weekend");
    default -> throw new IllegalStateException("Unexpected value: " + day);
}

public static String test(Day day) {
    return switch (day) {
        case MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY -> "Weekday";
        case SATURDAY, SUNDAY -> "Weekend";
    };
}

Object obj = "Hello";
int length = switch (obj) {
    case String s && s.length() > 0 -> s.length();
    case Integer i -> i;
    default -> 0;
};
System.out.println("Length is " + length);

Java レコードは、Java 16 (JEP 395) で導入された新機能で、主にデータの格納に使用される単純なクラスを宣言する簡潔な方法を提供します。クラスに似ていますが、その主な目的は、複雑な動作や機能ではなく、データ レコードまたはデータ転送オブジェクト (DTO) を表すことです。

レコードは、recordキーワードを使用して定義され、その後にクラス名と、レコードが表すデータを定義するコンポーネント フィールドのリストが続きます。

次のようなものを書く代わりに:

public class User {
    private String firstName;
    private String lastName;
    private int age;

    public UserOld(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String firstName() {
        return firstName;
    }

    public String lastName() {
        return lastName;
    }

    public int age() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserOld userOld = (UserOld) o;
        return age == userOld.age && Objects.equals(firstName, userOld.firstName) && Objects.equals(lastName, userOld.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }

    @Override
    public String toString() {
        return "UserOld{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                '}';
    }
}

public record User( String firstName, String lastName, int age ) { }

封印されたクラス

Sealed クラスは、Java 15 (JEP 360) で導入された新機能で、開発者がクラスまたはインターフェイスのサブクラスを事前定義された一連のクラスに制限できるようにします。封印されたクラスは、クラス階層をより詳細に制御し、コードをより保守しやすく安全にするのに役立ちます。

シールされたクラスまたはインターフェイスを宣言するには、sealedキーワードの後に​​、クラスまたはインターフェイス名、および許可されたサブクラスのリスト (他のクラスまたはインターフェイスのいずれか) を続けて使用します。例えば:

public sealed class Shape permits Circle, Rectangle, Square {
...
}

public final class Square extends Shape {
...
}

public final class Rectangle extends Shape {
...
}

public final class Square extends Shape {
...
}

instanceof のパターン マッチング

パターン マッチングはinstanceofJava 16 で導入された機能で、操作を実行する前にオブジェクトの型をチェックする一般的なユース ケースを簡素化します。

instanceofJava 16 より前では、型に基づいてオブジェクトに対して操作を実行する場合は、通常、次のように と キャストを組み合わせて使用​​します。

if (obj instanceof String) {
    String s = (String) obj;
    // do something with s
} else if (obj instanceof Integer) {
    Integer i = (Integer) obj;
    // do something with i
} else {
    // do something else
}

if (obj instanceof String s) {
    // do something with s
} else if (obj instanceof Integer i) {
    // do something with i
} else {
    // do something else
}

先を見据えて、Java の将来のバージョンに向けて、いくつかのエキサイティングな機能強化がすでに行われています。たとえば、2023 年 9 月にリリースされる予定の Java 18 では、次のような新機能を導入する計画があります。

  • デフォルトでUTF-8
  • 単純な Web サーバー
  • Java API ドキュメントのコード スニペット
  • ベクター API
  • インターネットアドレス解決 SPI