java-beginner.com ブログ

プログラミングを学習するブログ(Javaをメインに)

Stream APIのfilter()メソッドをあえて2回呼び出すやり方をしたら処理時間が遅くなった

投稿日:

最終更新日:2024年05月05日

アイキャッチ

こんにちは。今回はStream APIのfilterメソッドに関する話題です。

Stream APIのfilterメソッドではデータの抽出ができます。filterメソッドを呼び出した後、再びfilterメソッドを呼び出すことができます。しかし、抽出する条件次第ではfilterメソッドの呼び出しは1度で済むこともあり得ます。

1度で済むfilterメソッドを2回に分けたら、処理時間にどのくらい影響があるのだろうと疑問に思い、今回は、処理時間の比較のサンプルを作りました。

データと抽出条件

例えば、身体測定などのデータとして、ID、身長、体重からなるデータを考えます。

データの一覧から以下の条件を満たすデータを抽出することを目的とします。

項目 条件
身長 160より大きい
体重 70より小さい

上記のデータを、以下のMyPersonクラスで扱うとします。

MyPerson

/**
 * 個人を扱うクラス
 */
public class MyPerson {

	/** ID */
	private int id;

	/** 身長 */
	private double height;

	/** 体重 */
	private double weight;

	public MyPerson(int id, double height, double weight) {
		this.id = id;
		this.height = height;
		this.weight = weight;
	}

	public int getId() {
		return id;
	}


	public double getHeight() {
		return height;
	}


	public double getWeight() {
		return weight;
	}

	@Override
	public String toString() {
		return "MyPerson [id=" + id
					+ ", height=" + height
					+ ", weight=" + weight
					+ "]";
	}

}

フィールドは上から順に、ID、身長、体重を格納するための変数です。便宜上IDは単なる整数値とします。

コンストラクタでは、フィールドの値を受け取ります。

その次の3つのメソッドは各フィールドに対応するゲッターメソッドです。

最後はtoString()メソッドをオーバーライドしています。

このメソッドのもとは、クラスObjectに定義されているメソッドです。API仕様書の定義としては、「オブジェクトの文字列表現」というメソッドです。大抵は、そのまま使うと、フィールドの内容を表示してくれるわけではありません。そのため、オーバーライドして、フィールドの内容がわかる文字列を返却するようにしています。

データを生成するクラス

データ抽出のメソッドを作る前に、MyPersonのリストをある程度ランダムなデータで作るクラスを考えました。

以下のようにデータ生成のためのクラスを作りました。

MyPersonListGenerator

/**
 * 個人のリストを生成するクラス
 */
public class MyPersonListGenerator {

	/**
	 * 個人のデータを生成して、返却する。
	 * @param num 生成するデータ数
	 * @return MyPersonのリスト
	 */
	public static ArrayList<MyPerson> get(int num) {
		ArrayList<MyPerson> myPersons = new ArrayList<>();

		for(int i = 1; i <= num; i++) {
			myPersons.add(
					new MyPerson(
							i,                      // (1)ID
							159.5 + Math.random(),  // (2)身長
							69.5 + Math.random()    // (3)体重
						));
		}

		return myPersons;
	}
}

ArrayListのインスタンスを生成して、add()メソッドでデータを追加しています。

MyPersonのコンストラクタを呼び出して、値を設定しています。

IDはfor文の繰り返し変数の値そのものです。値は1から順に設定されます。

身長と体重の設定では、Math.random()メソッドを使いました。このメソッドが返却する値は、正の符号の付いたdouble値で0.0以上で1.0より小さいの値です。身長には159.5以上で160.5より小さい値、体重には69.5以上で70.5より小さい値が設定される想定です。

以下のクラスで、このメソッドの動きをテストしました。

MyPersonListGeneratorTest

import java.util.List;

public class MyPersonListGeneratorTest {

	public static void main(String[] args) {
		List<MyPerson> myPersons = MyPersonListGenerator.get(10);
		myPersons.stream().forEach(p -> System.out.println(p));
	}

}

forEach()メソッドの引数にラムダ式を記述しています。System.out.println()メソッドにはMyPersonオブジェクトを渡しています。このメソッドにオブジェクトを渡すと、そのメソッドのtoString()メソッドの結果が出力されます。

以下は、実行結果です。

結果

MyPerson [id=1, height=159.55764373744796, weight=70.44595750643666]
MyPerson [id=2, height=159.62688222094118, weight=69.82442445431327]
MyPerson [id=3, height=160.08121481276578, weight=70.10280818421938]
MyPerson [id=4, height=160.22640834837458, weight=70.07176832018312]
MyPerson [id=5, height=160.02815021556916, weight=69.51690447956078]
MyPerson [id=6, height=160.14051790139598, weight=70.40913767446075]
MyPerson [id=7, height=159.7001081024073, weight=70.1703665963673]
MyPerson [id=8, height=160.07136414434729, weight=69.97712353472161]
MyPerson [id=9, height=159.70502476728856, weight=70.12620769486139]
MyPerson [id=10, height=159.50977993915268, weight=70.19065333873188]

Math.random()メソッドを使っているため、結果は実行するたびに変わりします。想定通り、heightには160周辺の値、weightには70周辺の値が設定されています。

データを抽出するクラス

今回は、最初にデータを抽出する役割のインターフェースを定義することにしました。その実装クラスを作って、処理時間を比較するということを考えました。

まず、インターフェースは以下のように定義しました。

MyPersonListFilter

import java.util.List;

public interface MyPersonListFilter {

	/**
	 * @param list データ一覧
	 * @return 抽出したデータの一覧
	 */
	public abstract List<MyPerson> exe(List<MyPerson> myPersons);
}

上記インターフェースを実装したクラスを2つ作って、処理時間を比較するという想定です。

最初に作ったクラスは以下です。

MyPersonListFilter1

import java.util.List;
import java.util.stream.Collectors;

public class MyPersonListFilter1 implements MyPersonListFilter {

	@Override
	public List<MyPerson> exe(List<MyPerson> myPersons) {
		return myPersons
				.stream()
				.filter(p -> { return p.getHeight() > 160;}) // 身長の条件で抽出
				.filter(p -> { return p.getWeight() < 70; }) // 体重の条件で抽出
				.collect(Collectors.toList());
	}

}

実装の良しあしはともかく、filter()メソッドを2回呼び出す例として、上記のように作りました。最初のfilter()メソッドでは、身長の条件でデータを抽出しています。その後のfilter()メソッドでは、体重の条件でデータを抽出しています。

次に作ったクラスは、以下です。

MyPersonListFilter2

import java.util.List;
import java.util.stream.Collectors;

public class MyPersonListFilter2 implements MyPersonListFilter {

	@Override
	public List<MyPerson> exe(List<MyPerson> myPersons) {
		return myPersons
				.stream()
				.filter(p -> {
						return p.getHeight() > 160 // 身長の条件を満たす
							&& p.getWeight() < 70; // かつ、体重の条件を満たす
					})
				.collect(Collectors.toList());
	}

}

上記はfilterメソッド()の呼び出しを1回で済ませた例です。A「身長の条件」、B「体重の条件」は判定する対象が別々なので、AかつBという条件式にまとめられます。なので、上記のようにfilter()メソッドのリターン文に条件式「AかつB」を記述する事で、filter()メソッドの呼び出しは1度で済みます。

データの抽出が出来るかを次のようにクラスを作って簡単な確認をしました。

ソース

import java.util.List;

public class MyPersonListFilterTest {

	public static void main(String[] args) {
		// (1) データの生成
		List<MyPerson> myPersons = MyPersonListGenerator.get(10);

		// (2) 生成されたデータの出力
		System.out.println("生成されたデータ");
		myPersons.stream().forEach(p -> System.out.println(p.toString()));

		System.out.println();

		// (3) MyPersonListFilter1の実行と結果出力
		List<MyPerson> persons_after1 = test(new MyPersonListFilter1(), myPersons);

		System.out.println();

		// (4) MyPersonListFilter2の実行と結果出力
		List<MyPerson> persons_after2 = test(new MyPersonListFilter2(), myPersons);

		System.out.println();

		// (5) 念のため、比較
		System.out.println("要素の比較結果: " + persons_after1.equals(persons_after2));

	}

	private static List<MyPerson> test(MyPersonListFilter filter, List<MyPerson> myPersons) {
		List<MyPerson> persons_after = filter.exe(myPersons);

		System.out.println(filter.getClass().getSimpleName() + "の抽出結果");

		persons_after.stream().forEach(p -> System.out.println(p.toString()));

		return persons_after;
	}

}

上記クラスのtest()メソッドでは、MyPersonListFilterのメソッドを実行して、結果を出力しています。また、メソッドの実行結果を返却しています。

おおまかには、以下のような処理をしています。

(1)でデータを生成し、(2)でその内容を出力しています。

(3)、(4)それぞれでMyPersonListFilter1、MyPersonListFilter2のインスタンスをtest()メソッドに渡して、それぞれのメソッドの実行結果を出力しています。

(5)では念のため、equals()メソッドでリストの内容が等しいかどうかを出力しています。

変数persons_after1と変数persons_after2には、クラスArrayListのインスタンスが格納されています。そのため、このクラスに実装されているequals()メソッドが呼び出されます。APIドキュメントによると、以下のすべてを満たす場合にtrueが返却されます。

  1. 指定されたオブジェクトもリストである。
  2. サイズが同じである。
  3. 2つのリストの対応する要素すべてが等しい。

以下が実行結果です。

結果

生成されたデータ
MyPerson [id=1, height=160.39866941761204, weight=69.64552294265506]
MyPerson [id=2, height=160.0622466331505, weight=70.08420676241339]
MyPerson [id=3, height=160.10251346953504, weight=69.52149159190813]
MyPerson [id=4, height=160.25881934745684, weight=70.43714663532761]
MyPerson [id=5, height=159.7222651551749, weight=69.53950105718253]
MyPerson [id=6, height=160.37962224896356, weight=70.19040121639385]
MyPerson [id=7, height=160.31456916180545, weight=70.17584513898314]
MyPerson [id=8, height=159.88260702279857, weight=70.42359192280655]
MyPerson [id=9, height=160.31780240441788, weight=69.63534993922922]
MyPerson [id=10, height=160.0358099647124, weight=69.59769085198216]

MyPersonListFilter1の抽出結果
MyPerson [id=1, height=160.39866941761204, weight=69.64552294265506]
MyPerson [id=3, height=160.10251346953504, weight=69.52149159190813]
MyPerson [id=9, height=160.31780240441788, weight=69.63534993922922]
MyPerson [id=10, height=160.0358099647124, weight=69.59769085198216]

MyPersonListFilter2の抽出結果
MyPerson [id=1, height=160.39866941761204, weight=69.64552294265506]
MyPerson [id=3, height=160.10251346953504, weight=69.52149159190813]
MyPerson [id=9, height=160.31780240441788, weight=69.63534993922922]
MyPerson [id=10, height=160.0358099647124, weight=69.59769085198216]

要素の比較結果: true

想定通りのデータが抽出されています。簡易的なテストですが、動きは大丈夫そうです。

処理時間を比較

処理時間の比較は、以下のようにクラスを作って実施しました。

ソース

import java.util.Arrays;
import java.util.List;

public class Test {

	public static void main(String[] args) {
		for (int i = 0; i < 5; i++) {
			System.out.println("比較" + (i + 1) + "回目");

			// (1-1) データを生成
			List<MyPerson> list = MyPersonListGenerator.get(100);

			// (1-2) それぞれの抽出クラスの処理時間を計測
			test(new MyPersonListFilter1(), list);
			test(new MyPersonListFilter2(), list);

			System.out.println();
		}
	}

	private static void test(MyPersonListFilter filter, List<MyPerson> myPersons) {
		int length = 10;
		long[] times = new long[length];

		// (2-1) 処理時間の計測を繰り返す
		for(int i = 0; i < length; i++) {
			times[i] = getTime(filter, myPersons);
		}

		// (2-2) 結果を出力
		System.out.println(filter.getClass().getSimpleName()
				+ "; ヒット件数:" + filter.exe(myPersons).toArray().length
				+ "; 処理回数:" + length
				+ "; 処理時間の平均:" + Arrays.stream(times).average().getAsDouble());
	}

	private static long getTime(MyPersonListFilter filter, List<MyPerson> myPersons) {
		int max = 10000;
		long start, end;

		// (3-1) 処理時間の計測
		start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
        	filter.exe(myPersons);
        }
        end = System.currentTimeMillis();

        return end - start;
	}

}

main()メソッドでは大まかな流れがわかるようにしました。データを生成し、それに対する処理時間の比較をfor文で繰り返すということをしています。

(1-1)では、MyPersonListGeneratorクラスを使い、データを生成しています。試しにデータ件数は100件にしてみました。

(1-2)では、後に定義しているtest()メソッドを呼び出しています。このメソッドの第1引数ではMyPersonListFilter型を受け取るようにしています。
test()メソッドでは、第1引数のメソッドを処理時間の平均を出力するようにしています。

(2-1)では、処理時間をいったん、配列timesに格納しています。

ここで、後に手定義しているgetTime()メソッドを呼び出しています。このメソッドは、MyPersonListFilterの実装クラスのexe()メソッドを一定回数で呼び出して経過時間を返却します。

(2-2)では、以下を出力しています。

  1. 変数に格納されたインスタンスのクラス名:これは、どのクラスのメソッドが呼び出されたのか区別のために出力しています。
  2. ヒット件数:念のため、ヒット件数が同じかどうかを確かめるため出力しています。
  3. 処理回数:念のため出力。深い意味はないです。
  4. 処理時間の平均:全部出力すると長くなるため、平均を出力するようにしました。

getTime()メソッドでは、実際に処理時間を計測しています。

(3-1)では、MyPersonListFilter型変数filterに対して、決められた回数、exe()メソッドを呼び出しています。変数filterにはMyPersonListFilter1かMyPersonListFilter2のインスタンスが格納されている想定です。

繰り返し処理の前後で、クラスSystemのcurrentTimeMillis()メソッドを呼び出しています。その差が経過ミリ秒であり、getTime()メソッドの戻り値にしています。

以下は実行結果です。

結果

比較1回目
MyPersonListFilter1; ヒット件数:24; 処理回数:10; 処理時間の平均:14.5
MyPersonListFilter2; ヒット件数:24; 処理回数:10; 処理時間の平均:11.1

比較2回目
MyPersonListFilter1; ヒット件数:25; 処理回数:10; 処理時間の平均:12.7
MyPersonListFilter2; ヒット件数:25; 処理回数:10; 処理時間の平均:8.9

比較3回目
MyPersonListFilter1; ヒット件数:18; 処理回数:10; 処理時間の平均:8.7
MyPersonListFilter2; ヒット件数:18; 処理回数:10; 処理時間の平均:7.7

比較4回目
MyPersonListFilter1; ヒット件数:25; 処理回数:10; 処理時間の平均:9.2
MyPersonListFilter2; ヒット件数:25; 処理回数:10; 処理時間の平均:8.2

比較5回目
MyPersonListFilter1; ヒット件数:30; 処理回数:10; 処理時間の平均:9.7
MyPersonListFilter2; ヒット件数:30; 処理回数:10; 処理時間の平均:8.0

このように、上記結果では、filter()メソッドを1回で済ませた方が処理時間は少ないです。ただし、データの偏り次第ではどうなるかわかりません。

今回の結果により、filter()メソッドの呼び出しが1回で済ませられるなら、そのようにした方が無難だと思いました。

以上、参考になれば幸いです。