Javaで特定のキーが一致するデータの重複を回避する方法

# # 初めに
以前複数のCSVファイルの特定のキーが一致するデータ同士を計算してDBに登録する機能の実装を行いました。その際にデータの重複や正しく正しく計算が行われないなど大変な思いをしました。今回はその解決方法を２つ紹介します。

|No|見出し|
|---|---|
|1|[1. 概要](#1-概要)|
|2|[2. 実装方法１(For文を2回回す)](#2-実装方法１for文を2回回す)|
|3|[3. 実装方法２(mapを使用する)](#3-実装方法２mapを使用する)|
|4|[4. 最後に](#4-最後に)|

# 1. 概要
本記事では、以下に示すような「__あるクラスの人たちの1学期から3学期までのテストの結果が入力されたCSVデータ__」があります。
このデータのある特定のキー（__ID(出席番号), 名前, 性別__）が一致する場合は点数を合算した結果を表示することを想定します。
```: あるクラスの人たちの1学期から3学期までのテストの結果が入力されたCSVデータ
ID(出席番号),名前,性別,国語,数学,英語
"001", "佐藤太郎", "男", "60", "60", "60"
"001", "佐藤太郎", "男", "65", "65", "65"
"001", "佐藤太郎", "男", "70", "70", "70"
"002", "加藤一葉", "女", "70", "70", "70"
"002", "加藤一葉", "女", "75", "75", "75"
"002", "加藤一葉", "女", "80", "80", "80"
"003", "山田次郎", "男", "80", "80", "80"
"003", "山田次郎", "男", "85", "85", "85"
"003", "山田次郎", "男", "90", "90", "90"
"004", "伊藤二葉", "女", "90", "90", "90"
"004", "伊藤二葉", "女", "95", "95", "95"
"004", "伊藤二葉", "女", "100", "100", "100"
```
以下は表示される想定結果です。
```:想定結果
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
```
以下はCSVデータを表すデータクラスです。単純なデータクラスではありませんが一旦はフィールドとgetter,setterだけ気にしておいてください。
```: TestResultBean.java
package com.app.beans;

import java.util.Arrays;
import java.util.Objects;

public class TestResultBean implements Cloneable {
	/** ID(出席番号) */
	private String id;
	/** 名前 */
	private String name;
	/** 性別 */
	private String gender;
	/** テストの点数 */
	private String[] testPoints;

	/**
	 * ID(出席番号)を取得
	 * @return ID(出席番号)
	 */
	public String getId() {
		return id;
	}

	/**
	 * ID(出席番号)をセット
	 * @param id ID(出席番号)
	 */
	public void setId(String id) {
		this.id = id;
	}

	/**
	 * 名前を取得
	 * @return 名前
	 */
	public String getName() {
		return name;
	}

	/**
	 * 名前をセット
	 * @param 名前
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * 性別を取得
	 * @return 性別
	 */
	public String getGender() {
		return gender;
	}

	/**
	 * 性別をセット
	 * @param 性別
	 */
	public void setGender(String gender) {
		this.gender = gender;
	}

	/**
	 * テストの点数を取得
	 * @return テストの点数
	 */
	public String[] getTestPoints() {
		return testPoints;
	}

	/**
	 * テストの点数をセット
	 * @param テストの点数
	 */
	public void setTestPoints(String[] testPoints) {
		this.testPoints = testPoints;
	}

	/**
	 * クローンメソッド(オブジェクトのコピー)
	 */
	@Override
	public TestResultBean clone() {
		TestResultBean clone = new TestResultBean();
		try {
			clone = (TestResultBean) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return clone;
	}

	/**
	 * toStringメソッド(オブジェクトの内容を表示)
	 */
	@Override
	public String toString() {
		return "id: " + this.id + ", name: " + this.name + ", gender:" + this.gender + ", testPoints:"
				+ Arrays.toString(this.testPoints);
	}

	/**
	 * イコールズメソッド
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (this == obj) {
			return true;
		}
		TestResultBean comparisonData = (TestResultBean) obj;
		// どちらか片方がnullかどうか判定
		if (Objects.isNull(this.id) && Objects.nonNull(comparisonData.id)
				|| Objects.nonNull(this.id) && Objects.isNull(comparisonData.id)) {
			return false;
		}
		if (Objects.isNull(this.name) && Objects.nonNull(comparisonData.name)
				|| Objects.nonNull(this.name) && Objects.isNull(comparisonData.name)) {
			return false;
		}
		if (Objects.isNull(this.gender) && Objects.nonNull(comparisonData.gender)
				|| Objects.nonNull(this.gender) && Objects.isNull(comparisonData.gender)) {
			return false;
		}
		return Objects.equals(this.id, comparisonData.id) && Objects.equals(this.name, comparisonData.name)
				&& Objects.equals(this.gender, comparisonData.gender) 
				&& Objects.deepEquals(this.testPoints, comparisonData.getTestPoints());
	}

	@Override
	public int hashCode() {
		// ・hashCode メソッドの発番基本構文：　int 変数名 = 31 * 0以外の素数 + インスタンスフィールドの値;
		int result = 7;
		result *= 31;
		result += Objects.isNull(this.id) ? 0 : this.id.hashCode(); 
		result *= 31;
		result += Objects.isNull(this.name) ? 0 : this.name.hashCode(); 
		result *= 31;
		result += Objects.isNull(this.gender) ? 0 : this.gender.hashCode(); 
		return result;
	}
}
```
実際にCSVデータを取得する処理を書くのは大変なので今回は以下のMockデータをCSVデータとして扱います。
```:CSVデータ取得メソッドの処理
    /**
	 * CSVデータを取得
	 * @return
	 */
	private static List<TestResultBean> getCsvData() {
		List<TestResultBean> csvDataList = new LinkedList<TestResultBean>();

		for (String[] csvLine : getMockCsvData()) {
			TestResultBean csvData = new TestResultBean();
			// ID(出席番号)
			csvData.setId(csvLine[0]);
			// 名前 
			csvData.setName(csvLine[1]);
			// 性別
			csvData.setGender(csvLine[2]);
			// テストの点数(国(3)、数(4)、英(5))
			String[] testPoints = { csvLine[3], csvLine[4], csvLine[5] };
			csvData.setTestPoints(testPoints);
			csvDataList.add(csvData);
		}
		return csvDataList;
	}

	/**
	 * CSVのモックデータを取得
	 * @return CSVのモックデータ
	 */
	private static List<String[]> getMockCsvData() {

		String[] csvLine1 = { "001", "佐藤太郎", "男", "60", "60", "60" };
		String[] csvLine2 = { "001", "佐藤太郎", "男", "65", "65", "65" };
		String[] csvLine3 = { "001", "佐藤太郎", "男", "70", "70", "70" };
		String[] csvLine4 = { "002", "加藤一葉", "女", "70", "70", "70" };
		String[] csvLine5 = { "002", "加藤一葉", "女", "75", "75", "75" };
		String[] csvLine6 = { "002", "加藤一葉", "女", "80", "80", "80" };
		String[] csvLine7 = { "003", "山田次郎", "男", "80", "80", "80" };
		String[] csvLine8 = { "003", "山田次郎", "男", "85", "85", "85" };
		String[] csvLine9 = { "003", "山田次郎", "男", "90", "90", "90" };
		String[] csvLine10 = { "004", "伊藤二葉", "女", "90", "90", "90" };
		String[] csvLine11 = { "004", "伊藤二葉", "女", "95", "95", "95" };
		String[] csvLine12 = { "004", "伊藤二葉", "女", "100", "100", "100" };

		List<String[]> csvList = new LinkedList<String[]>() {
			{
				add(csvLine1);
				add(csvLine2);
				add(csvLine3);
				add(csvLine4);
				add(csvLine5);
				add(csvLine6);
				add(csvLine7);
				add(csvLine8);
				add(csvLine9);
				add(csvLine10);
				add(csvLine11);
				add(csvLine12);
			}
		};
		return csvList;
	}
```
以下はテストデータを合算するメソッドです。（あまり重要ではありません。）
```:
	/**
	 * テストの点数を合算
	 * @param loop1CsvData
	 * @param loop2CsvData
	 * @return
	 */
	private static String[] calcTotalTesPoint(String[] data1, String[] data2) {
		String[] result = new String[3];

		BigDecimal[] resultCalc = new BigDecimal[3];
		for (int i = 0; i < result.length; i++) {
			// テストの点数を加算
			resultCalc[i] = new BigDecimal(data1[i])
					.add(new BigDecimal(data2[i]));
			result[i] = resultCalc[i].toString();
		}

		return result;
	}
```
# 2. 実装方法１(For文を2回回す)
最初に思いつくのは以下のように同じデータをFor文で2回回す方法ではないでしょうか。
```:実装方法1
	/**
	 * やり方１
	 * For文を2回回す
	 */
	private static void pattern1() {
		List<TestResultBean> totalResultList = new LinkedList<TestResultBean>();
		for (TestResultBean loop1CsvData : getCsvData()) {
			TestResultBean totalResult = loop1CsvData.clone();
			for (TestResultBean loop2CsvData : getCsvData()) {
				if (Objects.deepEquals(loop1CsvData, loop2CsvData)) {
					// 同じデータは無視
					continue;
				}
				if (checkKeyData(loop1CsvData, loop2CsvData)) {
					totalResult.setTestPoints(
							calcTotalTesPoint(totalResult.getTestPoints(), loop2CsvData.getTestPoints()));
				}
			}
			totalResultList.add(totalResult);
		}

		// 重複削除
		for (TestResultBean result : totalResultList.stream().distinct().collect(Collectors.toList())) {
			System.out.println(result.toString());
		}
	}

	/**
	 * ID(出席番号)、名前、性別が一致するか判定
	 * @param loop1CsvData 1回目のループさせているCSVの1行分のデータ
	 * @param loop2CsvData 2回目のループさせているCSVの1行分のデータ
	 * @return 判定結果
	 */
	private static boolean checkKeyData(TestResultBean loop1CsvData, TestResultBean loop2CsvData) {
		return Objects.equals(loop1CsvData.getId(), loop2CsvData.getId())
				&& Objects.equals(loop1CsvData.getName(), loop2CsvData.getName())
				&& Objects.equals(loop1CsvData.getGender(), loop2CsvData.getGender());
	}
```
以下は、for文で2回回しているCSVデータの「__ID(出席番号)、名前、性別__」が一致するかを判定するメソッドです。
```: キーが一致するデータかを比較するメソッド
	/**
	 * ID(出席番号)、名前、性別が一致するか判定
	 * @param loop1CsvData 1回目のループさせているCSVの1行分のデータ
	 * @param loop2CsvData 2回目のループさせているCSVの1行分のデータ
	 * @return 判定結果
	 */
	private static boolean checkKeyData(TestResultBean loop1CsvData, TestResultBean loop2CsvData) {
		return Objects.equals(loop1CsvData.getId(), loop2CsvData.getId())
				&& Objects.equals(loop1CsvData.getName(), loop2CsvData.getName())
				&& Objects.equals(loop1CsvData.getGender(), loop2CsvData.getGender());
	}
```
上記の条件が __true__ の場合のみ合算処理を行います。
```:pattern1一部抜粋
if (checkKeyData(loop1CsvData, loop2CsvData)) {
    totalResult.setTestPoints(
            calcTotalTesPoint(totalResult.getTestPoints(), loop2CsvData.getTestPoints()));
}
```
以下の部分は同じCSVデータをFor文で回しているのでデータが完全に同じ場合は合算等はせずに次のFor文処理にスキップしています。
```: pattern1一部抜粋
if (Objects.deepEquals(loop1CsvData, loop2CsvData)) {
    // 同じデータは無視
    continue;
}
```
そして2つ目のFor文(中のFor文)が終わるごとに外部で定義した合算結果を格納するリスト(__totalResultList__)に結果を追加しています。
```:pattern1一部抜粋
		List<TestResultBean> totalResultList = new LinkedList<TestResultBean>();
		for (TestResultBean loop1CsvData : getCsvData()) {
			TestResultBean totalResult = loop1CsvData.clone();
			for (TestResultBean loop2CsvData : getCsvData()) {
				if (Objects.deepEquals(loop1CsvData, loop2CsvData)) {
					// 同じデータは無視
					continue;
				}
				if (checkKeyData(loop1CsvData, loop2CsvData)) {
					totalResult.setTestPoints(
							calcTotalTesPoint(totalResult.getTestPoints(), loop2CsvData.getTestPoints()));
				}
			}
			totalResultList.add(totalResult);
		}
```
しかし、上記のままだと特定のキーが一致するデータの数だけ合算結果を追加してしまいます。
```:上記で追加したリスト(totalResultList)の内容を出力
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
```
重複するデータを削除しないといけないですね。
重複データは以下の方法で削除できます。
```:重複削除方法
totalResultList.stream().distinct().collect(Collectors.toList())
```
ただし、上記で削除を行うにはデータクラスに以下の2つのメソッドを記述しないといけません。
「__equalsメソッド、hashCodeメソッド___」
equalsメソッドはそのままオブジェクトが同じオブジェクトが比較するかを判定するメソッドを記述しています。
hashCodeメソッドは今回の場合はキーが一致するものを省きたいので「__id,name,gender__」で新しく採番しています。
```: TestResultBean.java のequalsメソッドとhashCodeメソッド
	/**
	 * イコールズメソッド
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (this == obj) {
			return true;
		}
		TestResultBean comparisonData = (TestResultBean) obj;
		// どちらか片方がnullかどうか判定
		if (Objects.isNull(this.id) && Objects.nonNull(comparisonData.id)
				|| Objects.nonNull(this.id) && Objects.isNull(comparisonData.id)) {
			return false;
		}
		if (Objects.isNull(this.name) && Objects.nonNull(comparisonData.name)
				|| Objects.nonNull(this.name) && Objects.isNull(comparisonData.name)) {
			return false;
		}
		if (Objects.isNull(this.gender) && Objects.nonNull(comparisonData.gender)
				|| Objects.nonNull(this.gender) && Objects.isNull(comparisonData.gender)) {
			return false;
		}
		return Objects.equals(this.id, comparisonData.id) && Objects.equals(this.name, comparisonData.name)
				&& Objects.equals(this.gender, comparisonData.gender) 
				&& Objects.deepEquals(this.testPoints, comparisonData.getTestPoints());
	}

	@Override
	public int hashCode() {
		// ・hashCode メソッドの発番基本構文：　int 変数名 = 31 * 0以外の素数 + インスタンスフィールドの値;
		int result = 7;
		result *= 31;
		result += Objects.isNull(this.id) ? 0 : this.id.hashCode(); 
		result *= 31;
		result += Objects.isNull(this.name) ? 0 : this.name.hashCode(); 
		result *= 31;
		result += Objects.isNull(this.gender) ? 0 : this.gender.hashCode(); 
		return result;
	}
```
equalsやhashCodeについては以下の記事たちを参考にしてください。

https://qiita.com/yoshi389111/items/9e34fe297bd908a36065

https://workteria.forward-soft.co.jp/blog/detail/10219

重複削除を実装したら以下のようにして結果を表示しています。
```:pattern1一部抜粋
// 重複削除
for (TestResultBean result : totalResultList.stream().distinct().collect(Collectors.toList())) {
    System.out.println(result.toString());
}
```
```:実行結果
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
```
以上が実装方法の1つ目です。

# 3. 実装方法２(mapを使用する)
2つ目の実装方法はMapを使用する方法です。
```:実装方法2
	/**
	 * やり方2
	 * Mapを使用する
	 */
	private static void pattern2() {
		Map<String, TestResultBean> map = new LinkedHashMap<String, TestResultBean>();
		for (TestResultBean csvData : getCsvData()) {
			String key = generateMapKey(csvData);
			if (!map.containsKey(key)) {
				map.put(key, csvData);
			} else {

				// 計算済みのテストの点数
				String[] mapTestPoints = map.get(key).getTestPoints();
				// まだ未計算のテストの点数
				String[] csvDataTestPoints = csvData.getTestPoints();
				// 加算した結果をセット
				csvData.setTestPoints(calcTotalTesPoint(mapTestPoints, csvDataTestPoints));
				// mapを更新
				map.replace(key, csvData);
			}
		}
		for (Map.Entry<String, TestResultBean> entry : map.entrySet()) {
			System.out.println(entry.getValue());
		}
	}

	/**
	 * Mapに設定するキーの生成
	 * @param csvData csvデータ
	 * @return Mapに設定するキー 
	 */
	private static String generateMapKey(TestResultBean csvData) {
		return csvData.getId() + csvData.getName() + csvData.getGender();
	}
```
違いとしては実装方法1では特定のキーが一致するデータを比較するメソッドを作成しましたが、実装方法2では特定のキーを連結した文字列をMapに設定するキーとして使用し、キーが含まれていれば合算をしてなければ新たにキーを生成するという処理を行います。
以下がMapに設定するキーを作成するメソッドです。特定のキーである「__id,name,gender__」をただ連結しているだけです。
```:Mapに設定するキーを作成するメソッド
	/**
	 * Mapに設定するキーの生成
	 * @param csvData csvデータ
	 * @return Mapに設定するキー 
	 */
	private static String generateMapKey(TestResultBean csvData) {
		return csvData.getId() + csvData.getName() + csvData.getGender();
	}
```
以下のように __map__ にキーが存在しない場合は新規にキーと値を設定(put)して、同じキーが存在する場合はテストの点数を合算してmapを更新(replace)しています。
```:  pattern2一部抜粋
        Map<String, TestResultBean> map = new LinkedHashMap<String, TestResultBean>();
		for (TestResultBean csvData : getCsvData()) {
			String key = generateMapKey(csvData);
			if (!map.containsKey(key)) {
				map.put(key, csvData);
			} else {

				// 計算済みのテストの点数
				String[] mapTestPoints = map.get(key).getTestPoints();
				// まだ未計算のテストの点数
				String[] csvDataTestPoints = csvData.getTestPoints();
				// 加算した結果をセット
				csvData.setTestPoints(calcTotalTesPoint(mapTestPoints, csvDataTestPoints));
				// mapを更新
				map.replace(key, csvData);
			}
		}
```

mapの内容を以下のようにして結果を表示しています。また、mapの場合は重複の削除処理は必要ありません。
pattern2一部抜粋
```:pattern2一部抜粋
for (Map.Entry<String, TestResultBean> entry : map.entrySet()) {
     System.out.println(entry.getValue());
}
```
```:実行結果
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
```
以上が実装方法の2つ目です。

# 4. 最後に
最後にどちらのほうが良いかについてですがこれは圧倒的にパターン2だと思います。
まず、コードの記述量が少なくデータクラスに余計な処理を書きません。。
次に実行速度についてもパターン2のほうが早いです。
以下は実行速度を計測した今回作成した機能の全量です。
```: Main.java
package com.app;

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import com.app.beans.TestResultBean;

public class Main {

	public static void main(String[] args) {

		System.out.println("********** テストの合計1 **********");
		// 処理前の時刻を取得
		long startTime1 = System.currentTimeMillis();
		pattern1();
		// 処理後の時刻を取得
		long endTime1 = System.currentTimeMillis();
		System.out.println("開始時刻1：" + startTime1 + " ms");
		System.out.println("終了時刻1：" + endTime1 + " ms");
		System.out.println("処理時間1：" + (endTime1 - startTime1) + " ms");
		System.out.println("********** テストの合計2 **********");
		long startTime2 = System.currentTimeMillis();
		pattern2();
		long endTime2 = System.currentTimeMillis();
		System.out.println("開始時刻2：" + startTime2 + " ms");
		System.out.println("終了時刻2：" + endTime2 + " ms");
		System.out.println("処理時間2：" + (endTime2 - startTime2) + " ms");
	}

	/**
	 * やり方１
	 * For文を2回回す
	 */
	private static void pattern1() {
		List<TestResultBean> totalResultList = new LinkedList<TestResultBean>();
		for (TestResultBean loop1CsvData : getCsvData()) {
			TestResultBean totalResult = loop1CsvData.clone();
			for (TestResultBean loop2CsvData : getCsvData()) {
				if (Objects.deepEquals(loop1CsvData, loop2CsvData)) {
					// 同じデータは無視
					continue;
				}
				if (checkKeyData(loop1CsvData, loop2CsvData)) {
					totalResult.setTestPoints(
							calcTotalTesPoint(totalResult.getTestPoints(), loop2CsvData.getTestPoints()));
				}
			}
			totalResultList.add(totalResult);
		}

		// 重複削除
		for (TestResultBean result : totalResultList.stream().distinct().collect(Collectors.toList())) {
			System.out.println(result.toString());
		}
	}

	/**
	 * ID(出席番号)、名前、性別が一致するか判定
	 * @param loop1CsvData 1回目のループさせているCSVの1行分のデータ
	 * @param loop2CsvData 2回目のループさせているCSVの1行分のデータ
	 * @return 判定結果
	 */
	private static boolean checkKeyData(TestResultBean loop1CsvData, TestResultBean loop2CsvData) {
		return Objects.equals(loop1CsvData.getId(), loop2CsvData.getId())
				&& Objects.equals(loop1CsvData.getName(), loop2CsvData.getName())
				&& Objects.equals(loop1CsvData.getGender(), loop2CsvData.getGender());
	}

	/**
	 * やり方2
	 * Mapを使用する
	 */
	private static void pattern2() {
		Map<String, TestResultBean> map = new LinkedHashMap<String, TestResultBean>();
		for (TestResultBean csvData : getCsvData()) {
			String key = generateMapKey(csvData);
			if (!map.containsKey(key)) {
				map.put(key, csvData);
			} else {

				// 計算済みのテストの点数
				String[] mapTestPoints = map.get(key).getTestPoints();
				// まだ未計算のテストの点数
				String[] csvDataTestPoints = csvData.getTestPoints();
				// 加算した結果をセット
				csvData.setTestPoints(calcTotalTesPoint(mapTestPoints, csvDataTestPoints));
				// mapを更新
				map.replace(key, csvData);
			}
		}
		for (Map.Entry<String, TestResultBean> entry : map.entrySet()) {
			System.out.println(entry.getValue());
		}
	}

	/**
	 * Mapに設定するキーの生成
	 * @param csvData csvデータ
	 * @return Mapに設定するキー 
	 */
	private static String generateMapKey(TestResultBean csvData) {
		return csvData.getId() + csvData.getName() + csvData.getGender();
	}

	/**
	 * テストの点数を合算
	 * @param loop1CsvData
	 * @param loop2CsvData
	 * @return
	 */
	private static String[] calcTotalTesPoint(String[] data1, String[] data2) {
		String[] result = new String[3];

		BigDecimal[] resultCalc = new BigDecimal[3];
		for (int i = 0; i < result.length; i++) {
			// テストの点数を加算
			resultCalc[i] = new BigDecimal(data1[i])
					.add(new BigDecimal(data2[i]));
			result[i] = resultCalc[i].toString();
		}

		return result;
	}

	/**
	 * CSVデータを取得
	 * @return
	 */
	private static List<TestResultBean> getCsvData() {
		List<TestResultBean> csvDataList = new LinkedList<TestResultBean>();

		for (String[] csvLine : getMockCsvData()) {
			TestResultBean csvData = new TestResultBean();
			// ID(出席番号)
			csvData.setId(csvLine[0]);
			// 名前 
			csvData.setName(csvLine[1]);
			// 性別
			csvData.setGender(csvLine[2]);
			// テストの点数(国(3)、数(4)、英(5))
			String[] testPoints = { csvLine[3], csvLine[4], csvLine[5] };
			csvData.setTestPoints(testPoints);
			csvDataList.add(csvData);
		}
		return csvDataList;
	}

	/**
	 * CSVのモックデータを取得
	 * @return CSVのモックデータ
	 */
	private static List<String[]> getMockCsvData() {

		String[] csvLine1 = { "001", "佐藤太郎", "男", "60", "60", "60" };
		String[] csvLine2 = { "001", "佐藤太郎", "男", "65", "65", "65" };
		String[] csvLine3 = { "001", "佐藤太郎", "男", "70", "70", "70" };
		String[] csvLine4 = { "002", "加藤一葉", "女", "70", "70", "70" };
		String[] csvLine5 = { "002", "加藤一葉", "女", "75", "75", "75" };
		String[] csvLine6 = { "002", "加藤一葉", "女", "80", "80", "80" };
		String[] csvLine7 = { "003", "山田次郎", "男", "80", "80", "80" };
		String[] csvLine8 = { "003", "山田次郎", "男", "85", "85", "85" };
		String[] csvLine9 = { "003", "山田次郎", "男", "90", "90", "90" };
		String[] csvLine10 = { "004", "伊藤二葉", "女", "90", "90", "90" };
		String[] csvLine11 = { "004", "伊藤二葉", "女", "95", "95", "95" };
		String[] csvLine12 = { "004", "伊藤二葉", "女", "100", "100", "100" };

		List<String[]> csvList = new LinkedList<String[]>() {
			{
				add(csvLine1);
				add(csvLine2);
				add(csvLine3);
				add(csvLine4);
				add(csvLine5);
				add(csvLine6);
				add(csvLine7);
				add(csvLine8);
				add(csvLine9);
				add(csvLine10);
				add(csvLine11);
				add(csvLine12);
			}
		};
		return csvList;
	}
}
```
データ量が少ないので実行してもわかりづらいかと思いますが明らかに速度に違いがあります。
```:実行結果
********** テストの合計1 **********
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
開始時刻1：1672587031415 ms
終了時刻1：1672587031515 ms
処理時間1：100 ms
********** テストの合計2 **********
id: 001, name: 佐藤太郎, gender:男, testPoints:[195, 195, 195]
id: 002, name: 加藤一葉, gender:女, testPoints:[225, 225, 225]
id: 003, name: 山田次郎, gender:男, testPoints:[255, 255, 255]
id: 004, name: 伊藤二葉, gender:女, testPoints:[285, 285, 285]
開始時刻2：1672587031515 ms
終了時刻2：1672587031516 ms
処理時間2：1 ms

```
以上のことからパターン2をおすすめします。

# 参考

https://shinshin86.hateblo.jp/entry/2016/09/11/175441

https://qiita.com/KuwaK/items/255de3454ea3327211d3#_reference-3eea29e1395c60c1f323

https://qiita.com/yoshi389111/items/9e34fe297bd908a36065

https://workteria.forward-soft.co.jp/blog/detail/10219