1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Geminiの助けを借りて置き去りにされたJavaに追いつきたい:Java 8

Posted at

お疲れさまです、みやもとです。

Java学び直しの続き、今回はJava 8の変更点です。

Java 8の変更点

今回もGeminiがピックアップしてくれた内容について見ていきます。
個人的に一番理解が追い付いていないところから。

ラムダ式

関数型インターフェースの実装を簡潔に記述できる匿名関数です。
もうこの説明の時点でよくわかってない。

さっそくGeminiが作ってくれたサンプルコードを見てみましょう。

// Java 6 (匿名クラスを使用)
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.Comparator;

public class Java6Comparator {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Alexander");

        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        });

        for (String name : names) {
            System.out.println(name);
        }
    }
}
// Java 8 以降 (ラムダ式を使用)
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
//Comparatorもimportしてもしなくてもコンパイルできる
import java.util.Comparator;

public class Java8LambdaComparator {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Alexander");

        Collections.sort(names, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
        //Comparator.comparingIntを使うとより短くかける。
        //Collections.sort(names, Comparator.comparingInt(String::length));

        names.forEach(System.out::println); // メソッド参照を使用
    }
}

…短くなったのはわかりますが。
業務でほとんど使ったことがないので、匿名クラスをどういう時に使うのかぴんと来ない。
こういう例でCollections.sortを使っているのは今までにも何度か見かけたのですが、データ処理をSQL側で済ませてるとわざわざJavaでsort使うこともそうないし…。

しかしここで「よーわからん、終わり!」にしてたら学び直しになりません。
一度戻って、匿名クラスからやり直しましょう。

匿名クラスとは?

匿名クラスとは「名前のないクラス」、つまりはクラス定義を行わないクラスのことです。
こちらのリンク先を参照しました。

定義とインスタンス化を同時に行うため、1度きりしか使わない処理を匿名クラスにすることでわざわざクラスファイルを作る必要もなく、処理の内容をその場で確認しやすくなる…とのこと。

それを読んでからJava 6のサンプルコードを再度確認すると、確かにインタフェースであるComparatorをnewすると同時にOverrideでメソッドを実装しているのがわかりますね。

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

いよいよラムダ式

匿名クラスについてちょっと理解したところで、いよいよラムダ式です。
まずは匿名クラスを書き換えたサンプルコードを改めて見てみます。

Collections.sort(names, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

「Integer.compare(s1.length(), s2.length())」の部分は匿名クラスのreturn部分なので、「->」の左「(s1, s2)」は引数、右の「Integer.compare(s1.length(), s2.length())」は処理内容というか、呼び出し元に結果として返すところですね。
namesからふたつずつ引数として渡して比較している…ということか。
ところで「->」は何?

アロー演算子

「Java '->'」で検索してみたのですが、キーワードの指定があかんのでしょうか。
それらしい記事が見当たらず、改めてGeminiに聞いてみたところ以下のような答えが。

Javaのラムダ式における -> は、アロー演算子と呼ばれ、ラムダ式の構文を構成する重要な要素です。これは、ラムダ式のパラメータリストと本体を分離する役割を果たします。

とりあえず、先述の「右が処理内容、左が引数」という認識自体は合っているようですね。
つまりラムダ式の基本の書き方はこう。

(引数) -> { 処理内容 }

これでおしまい、と言いたいところですが、まだサンプルコードによくわからないところが残っています。

Collections.sort(names, Comparator.comparingInt(String::length));

引数もアロー演算子もない、これは一体…。

メソッド参照

これもJava 8から導入された機能で、メソッド参照というらしい。
既に存在しているメソッドを関数型インターフェースオブジェクトとして扱ってしまおうというもの…だそうです。
そういえばCollections.sort以外にももうひとつ使われていました。

names.forEach(System.out::println);

私としてはこっちはまだわかりやすいです。
forEachでnamesの要素を1つずつ取り出して出力しているんですよね?
System.out.printlnは引数として出力したい文字列等を必要としますが、メソッド参照で記述すると引数が省略されるのでこうなる、と。

問題は先のCollections.sortの部分。
これもGeminiに聞いてみたところ、String::length部分に関してこんな感じで返ってきました。

これは、String クラスの length() メソッドを参照するメソッド参照です。
インスタンスメソッドの参照 (特定オブジェクトのインスタンスメソッドではなく、クラスのインスタンスメソッド)でString オブジェクトを受け取り、その length() メソッドを呼び出して、文字列の長さを int 値として返します。

結局、namesに格納されている要素を引き渡し、引き渡したStringインスタンスの持つlengthメソッドを実行している…ということで合ってるんでしょうか。いまいちピンとこない。
names.forEachでの記載ははまだ腑に落ちるあたり、ラムダ式やメソッド参照の疑問というよりCollections.sortとComparatorの内部動作がイメージできなくてもやもやしている感じ。
めちゃくちゃ横道に逸れそうなので、ちょっとここは保留します。

余談:拡張for文の思い出

さらっとforEachはわかる、と書きましたが、そういえばforEachもJava 8からの機能でした。
なんとなくわかったのは「Each」という単語自体の意味もあって「1つずつ取り出すんだなー」という印象になるからでしょうね。
あと拡張for文初めて見たときに「えっこれで要素1つずつ取り出してくれんの?」とびっくりしたので、「ああこれもそうなのね」とイメージできたのかもしれません。

なんで拡張for文でびっくりしてるの?習わなかったの?と思われた方。
私がJavaを最初に勉強したのは2004年4月~6月あたりにかけてでした。
そしてこちらがJavaの歴史。

私が勉強した頃にはなかった機能で、その後長らくCOBOLで仕事してたので触れる機会が無かったのです。
時代を感じますね!

とりあえずラムダ式絡みはここまで。

ストリームAPI

Java 8から導入されたAPIで、コレクションや配列の要素を宣言的に処理するためのAPIです。
まずはGeminiが作ってくれたサンプルコードを見てみましょう。

// Java 6 (forループを使用)
import java.util.Arrays;
import java.util.List;

public class Java6ListProcessing {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        for (int number : numbers) {
            System.out.println(number * 2);
        }
    }
}
// Java 8 以降 (Stream APIを使用)
import java.util.Arrays;
import java.util.List;

public class Java8StreamProcessing {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        numbers.stream()          // Streamの作成
               .map(n -> n * 2)   // 各要素を2倍にする中間操作
               .forEach(System.out::println); // 各要素を出力する終端操作
    }
}

…ぱっと見特に短くなっている感じもしなければ、わかりやすいという感じでもない気がします。
これ何が良いんでしょう?

ということでさらにGeminiに聞いてみると、こんなサンプルをくれました。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.stream()
    .filter(n -> n % 2 == 0)  // 偶数のみをフィルタリング
    .map(n -> n * 2)         // 2倍にする
    .findFirst();             // 最初の要素を取得 (ショートサーキット)

さらにサンプルに対する説明がこんな感じ。

この例では:
stream(): Stream を作成しますが、まだデータは読み込まれません。
filter(n -> n % 2 == 0): 偶数のみをフィルタリングする中間操作を定義します(まだ実行されません)。
map(n -> n * 2): 各要素を2倍にする中間操作を定義します(まだ実行されません)。
findFirst(): 最初の要素を取得する終端操作が呼び出されます。
ここで初めて、Stream は numbers から要素を1つずつ取得し始めます。
最初の要素 1 は filter で除外されます。
次の要素 2 は filter を通過し、map で 4 に変換され、findFirst によって取得され、処理が終了します(ショートサーキット)。
残りの要素 3 以降は処理されません。

すごくざっくり書くと「最後のメソッドまで処理を保留して、一連の処理の流れで必要なところだけを実行できる」ということか。
そう思うと確かに便利そうに感じます。

もうちょっと詳しく見てみようと思って検索したら、以下のサイトが見つかりました。

以下の説明がすごく腑に落ちたので引用します。

ストリームAPIで書けることは、for文で頑張ってロジックを書けば同様の処理ができます。
しかし、ソースコードの簡略化や処理パフォーマンスを考慮すると、ストリームAPIで書いた方がメリットがあることが多々あります。
複雑な要件を全てストリームAPIで無理やり書くと逆に可読性が下がりますが、Collection操作をする場合はまずはfor文よりもストリームAPIの使用を検討すると良いのではないでしょうか。

日時API

日付や時刻を扱うためのAPIです。
前から無かった?と思いましたが、DateやCalendarとは別みたい。

import java.util.Calendar;
import java.util.Date;

public class DateExampleJava6 {

    public static void main(String[] args) {
        // 現在の日付を取得
        Date now = new Date();
        System.out.println("Current Date: " + now);

        // 1年後の日付を計算 (Calendarを使用)
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(now);
        calendar.add(Calendar.YEAR, 1); // 1年加算
        Date nextYear = calendar.getTime();
        System.out.println("Next Year: " + nextYear);

        // 日付のフォーマット (SimpleDateFormatを使用)
        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy/MM/dd");
        System.out.println("Formatted Date: " + sdf.format(now));
    }
}
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class DateExampleJava8 {

    public static void main(String[] args) {
        // 現在の日付を取得
        LocalDate now = LocalDate.now();
        System.out.println("Current Date: " + now);

        // 1年後の日付を計算
        LocalDate nextYear = now.plusYears(1);
        System.out.println("Next Year: " + nextYear);

        // 日付のフォーマット
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
        System.out.println("Formatted Date: " + now.format(formatter));
    }
}

これは明確に便利!
年月日どれを計算するか指定が必要なCalendarクラスのaddと比較して、plusYearsというのはメソッド名だけで何をするのかわかります。
また、日付の取得も計算も同じ変数で実行できるのは、日付計算が必要になるたび検索かけて「変数ひとつで片付けられんかー」となっていた身には喜ばしい変更です。
毎回調べてないで覚えろ、というのはごもっとも。

Optionalクラス

値が存在しない可能性を表すクラス。
NullPointerExceptionを回避するのに役立つと聞いてがぜん色めき立ちました。

まずはJava 6のサンプルコードから。

public class OptionalExampleJava6 {

    public static String getUserName(String userId) {
        // userIdに対応するユーザーを取得する (nullの可能性がある)
        User user = findUserById(userId);

        if (user != null) {
            return user.getName();
        } else {
            return "Unknown User";
        }
    }
    
    // ユーザーデータ取得のダミーメソッド
    private static User findUserById(String userId) {
        if ("validId".equals(userId)) {
            return new User("John Doe");
        } else {
            return null;  // nullを返す可能性がある
        }
    }

    static class User {
        private String name;

        public User(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


    public static void main(String[] args) {
        String userName1 = getUserName("validId");
        System.out.println("User Name 1: " + userName1); // User Name 1: John Doe

        String userName2 = getUserName("invalidId");
        System.out.println("User Name 2: " + userName2); // User Name 2: Unknown User
    }
}

!= null を判定して、その上でインスタンスの持つメソッドを実行する手間を踏んでいますね。
業務でもよく見かけるやつです。
うっかり忘れてテスト環境でNullPointerExceptionが吐かれたログを何度見かけたか…。

続いてJava 8以降、Optionalを使った場合のサンプルがこちら。

import java.util.Optional;

public class OptionalExampleJava8 {

    public static String getUserName(String userId) {
        // userIdに対応するユーザーをOptionalで取得
        Optional<User> userOptional = findUserById(userId);

        // Optionalを使った安全な値の取り出しとデフォルト値の設定
        return userOptional.map(User::getName).orElse("Unknown User");
    }

    // ユーザーデータ取得のダミーメソッド (Optionalを返す)
    private static Optional<User> findUserById(String userId) {
        if ("validId".equals(userId)) {
            return Optional.of(new User("John Doe"));
        } else {
            return Optional.empty(); // 空のOptionalを返す
        }
    }

     static class User {
        private String name;

        public User(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        String userName1 = getUserName("validId");
        System.out.println("User Name 1: " + userName1);  // User Name 1: John Doe

        String userName2 = getUserName("invalidId");
        System.out.println("User Name 2: " + userName2);  // User Name 2: Unknown User
    }
}

また出たなメソッド参照。

ともあれ、newする際にOptional.ofで包んでメソッド実行時にmap()を使用することでインスタンスがnullの場合はnull、nullでなければ指定の処理を実行するという切り分けをしてくれるということですね。
うっかりOptional.ofとかmap()とか使い忘れそうで怖いですが、インスタンス生成時はともかくメソッド実行時はちゃんとOptional指定しておけばIDEの方で補完してくれるでしょう。

ちなみにOptional.ofについての説明に以下の文が。

引数がnullの場合、NullPointerExceptionが発生します。

やっぱりnullチェック要るんちゃうんか?と一瞬思ったのですが、Optional.ofNullableを使うとnullかもしれない引数を使っても大丈夫とのこと。
こっち使った方が安心できそうですね。

デフォルトメソッドと静的メソッドの実装

主にラムダ式とメソッド参照がわからなさすぎてだいぶかかりましたが、ようやくJava 8に関する重要変更ピックアップの最後です。

Java 8からインターフェースにデフォルトメソッドと静的メソッドを実装できるようになりました。
デフォルトメソッドはインタフェースに処理を記述して、継承したクラス内でオーバーライドしなくても使えるメソッド。
静的メソッドはインターフェースに処理を記述して、クラスメソッドと同様にインスタンス化せずに使えるメソッド。

ここだけサンプルコードにJava 6なしでいきます。
まずはこんな感じでインターフェースを作成します。

interface MyInterface {
    // 通常の抽象メソッド
    void abstractMethod();

    // デフォルトメソッド
    default void defaultMethod() {
        // デフォルトの実装
        System.out.println("Default method implementation");
    }
    // 静的メソッド
    static void staticMethod() {
        System.out.println("Static method in interface");
    }
}

そしてこんな感じで継承させたクラスを使います。

// MyInterface を実装するクラス
class MyClass implements MyInterface {
    @Override
    public void abstractMethod() {
        System.out.println("Abstract method implementation");
    }

    // defaultMethod() はオーバーライドしなくてもよい (必要ならオーバーライドも可能)
}

public class DefaultMethodExample {
    public static void main(String[] args) {
        MyClass obj = new MyClass();
        obj.abstractMethod();  // Abstract method implementation
        obj.defaultMethod();   // Default method implementation (MyClass でオーバーライドされていないため)

        // ラムダ式でインターフェースを実装 (defaultMethod は自動的に利用可能)
        MyInterface lambdaImpl = () -> System.out.println("Lambda implementation");
        lambdaImpl.abstractMethod(); // Lambda implementation
        lambdaImpl.defaultMethod();  // Default method implementation
        // インターフェース名.静的メソッド名() で呼び出す
        MyInterface.staticMethod(); // Static method in interface
    }
}

abstractMethodがオーバーライドされているのに対して、defaultMethodはされていません。
静的メソッドはインスタンス化することもなく実行されています。

…と、ここまで書いてJava 5より前のバージョンで勉強していた身はひっかかりました。
インターフェースって実処理書けるようになったん?
あとなんかインスタンス化もしてない?

メソッド実装可能になった背景

さっそくGeminiに追加質問しました。
相手がAIと思うと遠慮がなくなりますね。

答えがこちら。

なぜ変更されたのか (背景)

後方互換性を保ちながらインターフェースを進化させるため:
Javaの初期の設計では、インターフェースに新しいメソッドを追加すると、そのインターフェースを実装するすべてのクラスを修正する必要がありました。これは、大規模なライブラリやフレームワークでは非常に大きな問題でした。デフォルトメソッドの導入により、既存のクラスを壊すことなく、インターフェースに新しい機能を追加できるようになりました。

コードの再利用性を高めるため:
デフォルトメソッドを使用すると、インターフェースの複数の実装クラスで共通のロジックを定義できます。これにより、コードの重複を減らし、保守性を向上させることができます。

ラムダ式との連携を強化するため:
Java 8ではラムダ式が導入されました。ラムダ式は、関数型インターフェース(抽象メソッドを1つだけ持つインターフェース)の実装を簡潔に記述するための構文です。デフォルトメソッドは、関数型インターフェースに機能を追加する際に特に便利です。

なるほど。
私が過去に参加してきたプロジェクトでは基本インターフェースどころかベースとなるスーパークラスも基盤チームのみ作成可能となっていることが多かったのであまり意識しなかったのですが、クラスの変更ですら関連する処理への影響であっちこっち直すことがあるのですからインターフェースならなおのことでしょう。
また、ラムダ式が抽象メソッドを1つだけ持つインターフェースの実装のために使われるということなら、逆に言えばラムダ式で実装しているインターフェースはメソッドの追加ができないということ。
後からこういう処理するメソッドも欲しい!となった時に対応が面倒くさくなることは想像できます。

なお、「インスタンス化もしてない?」という疑問については「インスタンス化はできないけど匿名クラスやラムダ式を使ってそれっぽいことはできるよ」という回答が返ってきました。
ラムダ式はともかく、匿名クラスはnewしているのでインスタンス化に見えるんですが違うのか…。

一旦まとめ

前回「あと2回くらい」と書きましたが、収まるか不安になってきました。
こうして改めてまとめると、Java 8の変更ってかなり大きかったんですね。
その割に、実行環境としては対応しているはずの過去のいろんな現場で見かけた記憶がないのはどうしたことか。
今後さらにバージョンが上がった時にえらいことになりそうで怖いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?