按类型过滤 Java 集合 - javarevisited


有时您在 Java 中有一个混合集合。一个简单的例子是具有List<Number>,其中所述列表可以包含Integer,Float,Long和Double实例。如何轻松过滤掉List<Integer>或List<Double>?我将演示如何使用经典 Java 执行此操作,在 Java 16 中使用模式匹配 instanceof,并在Eclipse Collections 中使用两种不同的方法。我还使用 JDK 17 EA 和 Switch 中的模式匹配添加了一个预览解决方案。
 
解决方案:经典 Java
使用标准 Java foreach 循环解决问题的一种方法。

@Test
public void classicJava()
{
    List<Number> numbers = List.of(1, 2L, 3.0, 4.0f);
    List<Integer> integers = new ArrayList<>();
    List<Long> longs = new ArrayList<>();
    List<Double> doubles = new ArrayList<>();
    List<Float> floats = new ArrayList<>();
    for (Number each : numbers)
    {
        if (each instanceof Integer)
        {
            integers.add((Integer) each);
        }
        else if (each instanceof Long)
        {
            longs.add((Long) each);
        }
        else if (each instanceof Double)
        {
            doubles.add((Double) each);
        }
        else if (each instanceof Float)
        {
            floats.add((Float) each);
        }
    }

    Assertions.assertEquals(List.of(1), integers);
    Assertions.assertEquals(List.of(2L), longs);
    Assertions.assertEquals(List.of(3.0), doubles);
    Assertions.assertEquals(List.of(4.0f), floats);

这种方法的好处是我们只对数字列表迭代一次。缺点是它非常冗长,并且需要强制转换。
 
解决方案:Java 16 — instanceof 的模式匹配
@Test
public void patternMatchingForInstanceOfJava16()
{
    List<Number> numbers = List.of(1, 2L, 3.0, 4.0f);
    List<Integer> integers = new ArrayList<>();
    List<Long> longs = new ArrayList<>();
    List<Double> doubles = new ArrayList<>();
    List<Float> floats = new ArrayList<>();
    numbers.forEach(number ->
    {
        if (number instanceof Integer each)
        {
            integers.add(each);
        }
        else if (number instanceof Long each)
        {
            longs.add(each);
        }
        else if (number instanceof Float each)
        {
            floats.add(each);
        }
        else if (number instanceof Double each)
        {
            doubles.add(each);
        }
    });
    Assertions.assertEquals(List.of(1), integers);
    Assertions.assertEquals(List.of(2L), longs);
    Assertions.assertEquals(List.of(3.0), doubles);
    Assertions.assertEquals(List.of(4.0f), floats);
}

这样做的好处是我们只迭代一次数字列表,并且没有不安全的转换。缺点是它仍然很冗长。这将在 Java 17 中通过模式匹配进行切换。
 
解决方案:Eclipse Collections selectInstanceOf

@Test
public void selectInstancesOf()
{
    MutableList<Number> numbers = 
        Lists.mutable.with(1, 2L, 3.0, 4.0f);
    MutableList<Integer> integers =
        numbers.selectInstancesOf(Integer.class);
    MutableList<Long> longs = 
        numbers.selectInstancesOf(Long.class);
    MutableList<Double> doubles =
        numbers.selectInstancesOf(Double.class);
    MutableList<Float> floats =
        numbers.selectInstancesOf(Float.class);

    Assertions.assertEquals(Lists.mutable.with(1), integers);
    Assertions.assertEquals(Lists.mutable.with(2L), longs);
    Assertions.assertEquals(Lists.mutable.with(3.0), doubles);
    Assertions.assertEquals(Lists.mutable.with(4.0f), floats);
}

这种方法的优点是非常简洁。缺点是它需要对数字列表进行多次迭代。
 
解决方案:Eclipse Collections CaseProcedure
@Test
public void caseProcedure()
{
    MutableList<Number> numbers =
            Lists.mutable.with(1, 2L, 3.0, 4.0f);
    MutableList<Integer> integers = Lists.mutable.empty();
    MutableList<Long> longs = Lists.mutable.empty();
    MutableList<Double> doubles = Lists.mutable.empty();
    MutableList<Float> floats = Lists.mutable.empty();

    numbers.forEach(new CaseProcedure<>()
            .addCase(Integer.class::isInstance,
                    each -> integers.add((Integer) each))
            .addCase(Float.class::isInstance,
                    each -> floats.add((Float) each))
            .addCase(Long.class::isInstance,
                    each -> longs.add((Long) each))
            .addCase(Double.class::isInstance,
                    each -> doubles.add((Double) each)));

    Assertions.assertEquals(Lists.mutable.with(1), integers);
    Assertions.assertEquals(Lists.mutable.with(2L), longs);
    Assertions.assertEquals(Lists.mutable.with(3.0), doubles);
    Assertions.assertEquals(Lists.mutable.with(4.0f), floats);
}


这种方法的优点是它相对简洁,只需要遍历数字列表一次。缺点是它需要显式和不安全的强制转换。
 
解决方案:Java 17 — Switch 中的模式匹配
当 Java 17 发布时,我们可以解决在 switch 中使用模式匹配的问题。

@Test
public void patternMatchingForSwitchJava17()
{
    List<Number> numbers = List.of(1, 2L, 3.0, 4.0f);
    List<Integer> integers = new ArrayList<>();
    List<Long> longs = new ArrayList<>();
    List<Double> doubles = new ArrayList<>();
    List<Float> floats = new ArrayList<>();
    numbers.forEach(number -> {
            switch(number)
            {
                case Integer each -> integers.add(each);
                case Float each -> floats.add(each);
                case Long each -> longs.add(each);
                case Double each -> doubles.add(each);
                default -> { break; }
            };
    });
    Assertions.assertEquals(List.of(1), integers);
    Assertions.assertEquals(List.of(2L), longs);
    Assertions.assertEquals(List.of(3.0), doubles);
    Assertions.assertEquals(List.of(4.0f), floats);
}

这种方法的优点是简洁、清晰和单遍迭代。我想不出任何缺点。
 
实验:Java 17 — Switch、Var 和 Eclipse 集合中的记录、模式匹配注入
这纯粹是我在安装并运行 Java 17 EA 后添加的思想实验。我想看看在 Switch 中使用 Java 记录和 Eclipse Collections injectInto 和 Pattern Matching 可以做什么。

@Test
public void recordsPatternMatchingInSwitchAndInjectInto()
{
    record Numbers(List<Integer> integers, List<Long> longs,
                   List<Double> doubles, List<Float> floats)
    {
        public Numbers()
        {
            this(mList(), mList(), mList(), mList());
        }

        Numbers filter(Number number)
        {
            switch(number)
            {
                case Integer each -> integers.add(each);
                case Float each -> floats.add(each);
                case Long each -> longs.add(each);
                case Double each -> doubles.add(each);
                default -> { break; }
            }
            return this;
        }
    }

    var numbers = Lists.mutable.with(1, 2L, 3.0, 4.0f);
    var filtered = 
        numbers.injectInto(new Numbers(), Numbers::filter);

    Assertions.assertEquals(List.of(1), filtered.integers);
    Assertions.assertEquals(List.of(2L), filtered.longs);
    Assertions.assertEquals(List.of(3.0), filtered.doubles);
    Assertions.assertEquals(List.of(4.0f), filtered.floats);
}

 
一些想法
根据类型过滤混合 Java 集合可能会很痛苦,但今天作为 Java 开发人员,我们有一些选择。Eclipse Collections 提供了当今最简洁的选项。该selectInstancesOf方法结合了过滤和更具体的返回类型,缺点是需要对每个子类型进行单独的迭代。CaseProcedure提供了当今最简洁、最高效的选项,但缺点是仍然需要cast。当 Java 17 与 Switch 中的模式匹配一​​起发布时,可用选项将真正得到改进。Java 17 将在发布时提供最简洁和高性能的选项。