NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    Javaジェネリクスで作る!柔軟で再利用可能な共通処理の実装

    はじめに

    こんにちは、NRIネットコムの島崎です。 今年5月に入社後すぐにJava(Spring Boot等)の社内研修を受けました。

    その中で改めて学び、理解を深めた&今後役立ちそうだなと思ったジェネリクスについて、 このブログでアウトプットさせていただきます。

    ジェネリクスとは?

    ジェネリクスとは、コードの再利用性や安全性を向上させるためのJavaの機能です。 型をパラメータ化することで、異なる型に対して同じコードを適用することができるためアプリケーションの共通処理の作成に役立ちます。

    なぜ共通処理が重要か?

    今回はジェネリクスを利用した共通処理の例について解説します。 共通処理を利用することで、コードの重複を減らし、保守性を向上させることができます。 共通処理は一箇所で管理されるため、変更が必要な場合もメンテナンスが容易になります。

    基本的なジェネリクスの使い方

    ではジェネリクスの基本的な使い方について説明していきます。

    ジェネリクスの基本構文

    ジェネリクスの基本構文は以下の通りです。

    public class GenericsSample<T> {
      private T t;
      public void set(T t) {
        this.t = t;
      }
      public T get() {
        return t;
      }
    }
    

    Tは型パラメータで、任意の型を受け取ることができます。

    以下のように呼び出して異なる型での設定・取り出しができるかを確認してみます。

           // String
            GenericsSample<String> genericsTestString = new GenericsSample<String>();
            genericsTestString.set("ジェネリクステスト");
            System.out.println(genericsTestString.get());
            
            // Integer
            GenericsSample<Integer> genericsTestInteger = new GenericsSample<Integer>();
            genericsTestInteger.set(Integer.valueOf(123));
            System.out.println(genericsTestInteger.get());
    

    出力結果

    ジェネリクステスト
    123

    異なる型でもGenericsSampleのインスタンスを生成して値の出し入れが行えることが確認できました。

    このようにジェネリクスを利用することで、異なる型の共通処理を実現することができます。

    共通処理とジェネリクス

    ジェネリクスを使用することで、異なる型に対して同じ共通処理を提供することができました。 さらに可変となる型の中にあるフィールドを操作する共通処理をジェネリクスで実装してみます。

    ジェネリクス・リフレクションを利用した共通処理の例

    ではジェネリクスとJavaのリフレクションを利用して、可変となる型の各フィールドを別インスタンスにコピーする共通処理を作ってみます。

    実際のコード例

    ではジェネリクスを利用した共通処理のサンプルを見ていきます。

    ジェネリクスを利用して様々な型を扱う例

    以下の例では、ジェネリクスを利用して様々な型を扱うことができるサンプルです。 以下のようなジェネリクスクラスを作成して様々な型のペアを扱ってみます。

    Pair.java

    package com.example.demo.service;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class Pair<T1,T2> {
        private T1 value1;
        private T2 value2;
    }
    

    PairService.java

    package com.example.demo.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class PairService {
        
        public Pair<String, Integer> createPair() {
            return new Pair<String, Integer>("Pair1", 1);
        }
        
        public Pair<String, Boolean> createPair1() {
            return new Pair<String, Boolean>("Pair2", Boolean.TRUE);
        }
        
        public Pair<Integer, String> createPair2() {
            return new Pair<Integer, String>(3, "Pair");
        }
    }
    

    Main.java

    package com.example.demo;
    
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import com.example.demo.service.PairService;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            PairService service = new PairService();
            System.out.println(service.createPair());
            System.out.println(service.createPair1());
            System.out.println(service.createPair2());
        }
    
    }
    

    コンソール出力結果

    Pair(value1=Pair1, value2=1)
    Pair(value1=Pair2, value2=true)
    Pair(value1=3, value2=Pair)

    解説

    まず、対となる値を保持するPairクラスを作成しています。 Pairクラスはジェネリクスで宣言されており、いろいろな型でインスタンスを生成することができるようになっています。 PairServiceクラスでいろいろな型を指定してインスタンスを生成することで、同じPairクラスでも異なる型のペアを返却することができるようになっています。

    csvから各フィールドを読み込んでを使用した共通処理

    さらに別の例として、下記のようなcsvファイルを読み込み、ヘッダの名前と一致するフィールドに値を格納する例を実装してみます。

    まず下記のようなcsvファイルがあるとします。

    user.csv

    id,name,department,age
    1,taro,Tokyo,30

    次に、上記データを格納するDTOクラスを用意します。 ※下記のケースではLombokを利用

    UserData.java

    package com.example.demo.dto;
    
    import lombok.Data;
    
    @Data
    public class UserData {
        private String id;
        private String name;
        private String department;
        private Integer age;
    }
    

    ファイル読み込みを行い、DTOに値を詰めるServiceクラスを作成します。 読み込むcsvの1行目はヘッダとして読み飛ばし、2行目以降をデータとして取り込んでいきます。 0行目のヘッダ文字をフィールド名として、名前が一致するコピー先のフィールドへコピーしていきます。 次にcsvには型情報がないため、型を判定してデータをセットしていきます。 このサンプルでは型情報を取得するためにJavaのリフレクションを使用しています。 このサンプルではIntegerとString型だけを想定して作成しており、それ以外の型はコンソール出力をして読み飛ばす形になっています(想定する型がほかにもある場合は、あらかじめそれをすべて想定しておく必要があります)。

    FileReadService .java

    package com.example.demo.service;
    
    import java.lang.reflect.Field;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.Stream;
    
    public class FileReadService {
        public <T> List<T> copy(String filename, Class<T> to) throws Exception {
            List<T> result = new ArrayList<>();
            List<String> lines = Files.readAllLines(Paths.get(filename));
            // 先頭行ヘッダを取得
            var head = lines.getFirst();
            var headFields = head.split(",");
            // ヘッダ行を除去
            lines.remove(0);
            // コピー先のフィールドを取得
            var fields = to.getDeclaredFields();
            
            for (var line: lines) {
                var record = line.split(",");
                T recObj;
                recObj = (T) to.getConstructors()[0].newInstance();
    
                for (var i = 0; i < record.length; i++) {
                    // 読み込んだファイルのヘッダと一致するコピー先のフィールドを取得する
                    final var headField = headFields[i];
                    final var value = record[i];
                    Optional<Field> field = Stream.of(fields)
                            .filter(ff -> ff.getName().equals(headField)).findFirst();
                    // 値をセットする
                    field.ifPresent(r -> {
                        // 型を意識してデータをセットする
                        r.setAccessible(true);
                        try {
                            if(r.getType().equals(String.class)) {
                                r.set(recObj, value);
                            } else if(r.getType().equals(Integer.class)) {
                                r.set(recObj, Integer.valueOf(value));
                            } else {
                                System.out.println("想定外の型の為、読み込みをスキップします。(" + r.getType() + ")");
                            }
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            System.out.println("値の読み込みに失敗しました");
                            e.printStackTrace();
                        }
                    });
                }
                result.add(recObj);
            }
            return result;
        }
    }
    

    上記処理を呼び出して結果をコンソールに出力してみます。

    Main.java

    package com.example.demo;
    
    import java.util.List;
    
    import com.example.demo.dto.UserData;
    import com.example.demo.service.FileReadService;
    
    public class Main{
    
        public static void main(String[] args) throws Exception {
            FileReadService fileReadService = new FileReadService();
            
            List<UserData> userDataList = fileReadService.copy("/demo/src/main/resources/user.csv", UserData.class);
            
            userDataList.forEach(l -> {
                System.out.println(l);
                });
        }
    
    }
    

    コンソール出力結果

    UserData(id=1, name=taro, department=Tokyo, age=30)

    補足(ジェネリクスでできないこと)

    ジェネリクスで抽象化した型に対しては、以下のようにnewでインスタンス生成をすることはできません。

    T recObj = new T();
    

    今回の例では、代わりに以下のような形でコンストラクタを呼び出してインスタンスを生成しています。

    T recObj = (T) to.getConstructors()[0].newInstance();
    

    まとめ

    いかがでしたでしょうか?

    ジェネリクスを活用することで、柔軟で再利用可能な共通処理を構築することができます。

    今回はリフレクションと組み合わせることで、動的なフィールド操作が可能になり、さらに便利な実装ができました。

    執筆者:島崎将和
    2024年5月ネットコム中途入社のシステムエンジニア
    直近数年前職ではWebアプリケーションのバックエンド開発・運用を担当していました。