1. 概要

コレクションリストでフィルタリングすることは、一般的なビジネスロジックシナリオです。 これを達成する方法はたくさんあります。 ただし、適切に実行しないと、ソリューションのパフォーマンスが低下する可能性があります。

このチュートリアルでは、いくつかのフィルタリングの実装を比較し、それらの長所と短所について説明します

2. For-Eachループの使用

最も古典的な構文であるfor-eachループから始めます。

この記事およびこの記事の他のすべての例では、次のクラスを使用します。

public class Employee {

    private Integer employeeNumber;
    private String name;
    private Integer departmentId;
    //Standard constructor, getters and setters.
}

また、簡単にするために、すべての例で次のメソッドを使用します。

private List<Employee> buildEmployeeList() {
    return Arrays.asList(
      new Employee(1, "Mike", 1),
      new Employee(2, "John", 1),
      new Employee(3, "Mary", 1),
      new Employee(4, "Joe", 2),
      new Employee(5, "Nicole", 2),
      new Employee(6, "Alice", 2),
      new Employee(7, "Bob", 3),
      new Employee(8, "Scarlett", 3));
}

private List<String> employeeNameFilter() {
    return Arrays.asList("Alice", "Mike", "Bob");
}

この例では、 Employeesの最初のリストをEmployee名の2番目のリストに基づいてフィルタリングし、それらの特定の名前のEmployeesのみを検索します。

次に、従来のアプローチを見てみましょう– 両方のリストをループして、一致するものを探します:

@Test
public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingForEachLoop() {
    List<Employee> filteredList = new ArrayList<>();
    List<Employee> originalList = buildEmployeeList();
    List<String> nameFilter = employeeNameFilter();

    for (Employee employee : originalList) {
        for (String name : nameFilter) {
            if (employee.getName().equals(name)) {
                filteredList.add(employee);
                // break;
            }
        }
    }

    assertThat(filteredList.size(), is(nameFilter.size()));
}

これは単純な構文ですが、非常に冗長で、実際には非常に非効率的です。 簡単に言えば、it は、2つのセットのデカルト積を繰り返し処理して答えを取得します。

break を追加して早期に終了しても、平均的なケースではデカルト積と同じ順序で反復されます。

従業員リストのサイズをnと呼ぶと、の場合、 nameFilter も同じ大きさのオーダーになり、にO(n2)分類が与えられます。

3. ストリームとList#containsの使用

ラムダを使用して構文を簡素化し、読みやすさを向上させることで、以前のメソッドをリファクタリングしますラムダフィルターとしてList#containsメソッドも使用しましょう。

@Test
public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambda() {
    List<Employee> filteredList;
    List<Employee> originalList = buildEmployeeList();
    List<String> nameFilter = employeeNameFilter();

    filteredList = originalList.stream()
      .filter(employee -> nameFilter.contains(employee.getName()))
      .collect(Collectors.toList());

    assertThat(filteredList.size(), is(nameFilter.size()));
}

Stream API を使用することで、読みやすさが大幅に向上しましたが、デカルト積を内部で反復処理しているため、コードは以前の方法と同じくらい非効率的です。 したがって、同じ O(n2)分類があります。

4. HashSetでのストリームの使用

パフォーマンスを向上させるには、 HashSet#containsメソッドを使用する必要があります。 このメソッドは、ハッシュコードルックアップを実行し、一定時間の操作回数を提供するため、List#containsとは異なります。

@Test
public void givenEmployeeList_andNameFilterList_thenObtainFilteredEmployeeList_usingLambdaAndHashSet() {
    List<Employee> filteredList;
    List<Employee> originalList = buildEmployeeList();
    Set<String> nameFilterSet = employeeNameFilter().stream().collect(Collectors.toSet());

    filteredList = originalList.stream()
      .filter(employee -> nameFilterSet.contains(employee.getName()))
      .collect(Collectors.toList());

    assertThat(filteredList.size(), is(nameFilterSet.size()));
}

HashSet、を使用することにより、読みやすさに影響を与えることなく、コードの効率が大幅に向上しました。 HashSet#containsは一定時間で実行されるため、分類をO(n)に改善しました。

5. 結論

このクイックチュートリアルでは、コレクションを値のリストでフィルタリングする方法と、最も簡単な方法のように見えるものを使用することの欠点を学びました。

コードが巨大なデータセットで実行される可能性があり、パフォーマンスの問題がそのような環境で壊滅的な結果をもたらす可能性があるため、常に効率を考慮する必要があります。

この記事で紹介するすべてのコードは、GitHubから入手できます。