Java集合工具介绍


一、迭代器

1. 基本介绍

迭代器与 for 循环类似,可实现集合元素的遍历,常用方法如下:

方法 作用
next() 访问后一位元素。
hasNext() 是否存在下一位,返回 Boolean。
remove() 删除上一次调用 next 返回的元素。
public static void iterateDemo() {
    List<String> list = new ArrayList<>();
    list.add("Hello");
    list.add("World");
    list.add("!");
    System.out.println(list);

    // 利用迭代器遍历集合
    // 效果等价于 for 循环
    Iterator iter = list.iterator();
    while (iter.hasNext()){
        System.out.println(iter.next());
    }
}

2. 列表迭代

ListIterator 继承于 Iterator,拓展提供了更丰富的操作。

方法 作用
next() 访问后一位元素。
previous() 访问前一位元素。
hasNext() 是否存在下一位,返回 Boolean。
remove() 删除上一次调用 next 返回的元素。
public static void listDemo() {
    List<String> list = new ArrayList<>();
    list.add("Hello");
    list.add("World");
    list.add("!");
    System.out.println(list);

    ListIterator iter = list.listIterator();
    iter.next();
    System.out.println(iter.previous());
}

二、集合工具

1. 包含查询

不论是 Set 还是 List 其都提供了 contains() 用于判断是否包含元素。

虽然 JDK 中常见的集合都提供了 contains() 方法,但各自的实现过程却相差甚远。对于 List 而言其通过单次循环复杂度为 O(n),而 Set 则存储结构为 Map 在实现效率上远超 List

public void containsDemo(){
    List<String> list = new ArrayList<>();
    list.add("AA");
    list.add("BB");

    // 是否包含元素
    Boolean isContains = list.contains("BB");
}

2. 模糊查询

通过 contains() 可以快速定位集合是否包含某一元素,其相当于精确查询,但如果需要实现模糊查询呢?

Java 中的 java.util 包下提供了正则表达式相关的工具包 regex,通过其即可实现模糊查询功能。

public void vagueDemo() {
    List<User> list = new ArrayList<>();
    list.add(new User("张三", 20));
    list.add(new User("李四", 30));

    String key = "张";
    Pattern pattern = Pattern.compile(key, Pattern.CASE_INSENSITIVE);
    List<User> result = new ArrayList<>();
    for (User user : list) {
        Matcher matcher = pattern.matcher(user.getName());
        if (matcher.find()) {
            result.add(user);
        }
    }
    System.out.println(result);
}

3. 子串截取

通过 subList(start, end) 可用于获取集合的子串,需要注意数组越界问题。

需要注意截取的字串其类型为 SubList 继承于 AbstractList,而 SubList 中并不包含空构造器,在序列化传输时可能会异常。

public void subListDemo() {
    List<Integer> list1 = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        list.add(i);
    }
    System.out.println(list);

    // subList 取值区间 (start, end-1)
    List<Integer> list2 = list1.subList(0, 3);
    System.out.println(list2);

    // end > size, 下标越界
    List<Integer> list3 = list1.subList(0, 6);
    System.out.println(list3);
}

三、集合排序

1. 基本排序

JDK 中提供了 Collections.sort() 方法实现集合内容进行排序。

import java.util.Collections; 

public void sortDemo() {
    List<String> list = new ArrayList();
    list.add("Alex");
    list.add("Mark");
    list.add("Beth");

    Collections.sort(list);
    System.out.println(list);
}

2. 动态排序

若列表存储的为实体对象,可根据实体对象某一字段值进行排序。

public class User {
    private String name;
    private int age;

    public int getAge() {
        return age;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Test {
    /**
     * 根据年龄字段排序
     */
    public static void main(String[] args) {
        List<User> list = new ArrayList();
        list.add(new User("Alex", 20));
        list.add(new User("Beth", 15));
        list.add(new User("Mark", 25));
        list.sort(Comparator.comparing(User::getAge));
    }
}

3. 排序接口

针对列表等集合类型实现排序可通过 Comparator 搭配 lambda 表达式快速实现,而对于数组数据同样可通过 Arrays.sort()Comparator<T> 接口实现。

同样与上述 User 排序类型,下述为利用 Comparator<T> 接口实现,其中 compare() 返回值取值范围为:(-1,0,1),依次代表 (大于,等于,小于)

public class SortTest {

    @Test
    public void demo() {
        User[] users = {
                new User("Alex", 20),
                new User("Beth", 15),
                new User("Mark", 25)
        };
        Arrays.sort(users, new UserComparator());
        System.out.println(Arrays.toString(users));
    }

    static class UserComparator implements Comparator<User> {
        @Override
        public int compare(User user1, User user2) {
            return Integer.compare(user1.getAge(), user2.getAge());
        }
    }
}

四、Steam操作

Java 8 中引入新概念 stream ,其包含对集合的部分基本操作。

1. 串行流

Stream 提供两种遍历方式:串行流与并发流。通过的 stream 流的方式可以利用 lambda 表达式实现更便捷的操作,相应的方法与其描述参考下表。

方法 作用
map() 实现集合动态数据提取。
sorted() 对集合数据实现排序,可传入排序器实现动态排序。
filter() 实现对集合数据进行过滤处理。
distinct() 实现对集合数据实现去重处理。
findFirst() 返回集合中的随机一个元素。
findAny() 返回集合中的随机一个元素,但在非 parallel 的情况下通常返回的都是第一个元素。
orElse() 通过 orElse() 用于处理当 findAny() 或 findFirst() 不匹配时返回结果。
anyMatch() 作用效果类似 contains() 方法。

上述表格中的方法接口具体使用示例如下:

public void demo() {
    List<User> list = new ArrayList<>();
    for (int i = 1; i <= 5; i++) {
        list.add(new User(String.valueOf(i), i));
    }

    String name = list.stream()
            .filter(user -> user.getAge() == 1)
            .map(User::getUserName)
            .distinct()
            .findAny()
            .orElse(null);
    System.out.println(name);
}

2. 并发流

除了串行流 Stream 同时提供了并发流 parallelStream() 从而实现更高效的计算,其使用 Fork-Join 框架,默认并发数为 CPU 核心的两倍。

若需要指定 parallelStream() 并发数可通过外层包裹 ForkJoinPool() 线程池控制,同时在使用时一定要注意其 lambda 表达式中涉及的操作都是线程安全。

public void streamDemo() {
    List<Integer> list = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }

    // 使用默认的 forkjoin 核心线程数
    list.parallelStream().forEach(System.out::println);

    // 手动设置核心线程数
    int parallelSize = 5;
    new ForkJoinPool(parallelSize).submit(() -> {
            list.parallelStream().forEach(it -> {
                // doSomething
            });
        }).join();
}

3. 类型转化

上述介绍了 Stream 提供了一系列 API 用于集合计算,其同时提供了一系列方法用于指定保存计算完成后的集合类型,具体信息参考下表:

方法 作用
collect(Collectors.toList()) 将目标集合转化 List 类型。
collect(Collectors.toSet()) 将目标集合转化 Set 类型。
collect(Collectors.toMap()) 将目标集合转化 Map 类型。
public static void main(String[] args) {
    List<Integer> origin = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        origin.add(i);
    }
    // 结果保存为 List 类型
    List<Integer> list = origin.stream()
            .collect(Collectors.toList());
    // 结果保存为 Set 类型
    Set<Integer> set = origin.stream()
            .collect(Collectors.toSet());
    // 结果保存为 Map 类型
    Map<Integer, Integer> map = origin.stream()
            .collect(Collectors.toMap(e -> e, e -> e));
}
方法 作用
collect(Collectors.groupingBy()) 将目标集合根据指定条件进行分组,返回 Map 对象。
collect(Collectors.collectingAndThen()) 对操作完成的集合结果实现二次操作。
collect(Collectors.Collectors.counting()) 返回 stream 操作完成后符合结果的元素个数。
public static void main(String[] args) {
    List<User> origin = new ArrayList<>();
    origin.add(new User("111", "Alex1"));
    origin.add(new User("111", "Alex2"));
    origin.add(new User("222", "Beth"));

    // 对列表中的元素根据 ID 字段去重
    List<User> unique2 = origin.stream().collect(collectingAndThen(toCollection(() ->
            new TreeSet<>(Comparator.comparing(User::getId))), ArrayList::new));
    System.out.println(unique2);
}

4. 分组操作

Stream 流中提供了 groupingBy() 接口可快速实现实现集合数据分组,如下述示例根据 Foo 类的 username 字段进行数据分组与数量统计。

public class GroupingTest {

    private static final List<Foo> fooList = new ArrayList<>();

    @Before
    public void init() {
        fooList.add(new Foo("1-1", "Foo-1"));
        fooList.add(new Foo("1-1", "Foo-1"));
        fooList.add(new Foo("1-2", "Foo-1"));
        fooList.add(new Foo("2-1", "Foo-2"));
        fooList.add(new Foo("3-1", "Foo-3"));
        fooList.add(new Foo("3-2", "Foo-3"));
        System.out.println("\nOrigin: " + fooList);
    }

    @Test
    public void groupingDemo() {
        Map<String, List<Foo>> result1 = fooList.stream()
                .collect(Collectors.groupingBy(Foo::getUsername));
        System.out.println("\nResult 1: " + result1);

        Map<String, Long> result2 = fooList.stream()
                .collect(Collectors.groupingBy(Foo::getUsername, Collectors.counting()));
        System.out.println("\nResult 2: " + result2);
    }
}

五、性能对照

不知道你有没有这么一个疑惑,随着 JDK 的升级, Java 针对集合提供越来越多的遍历方式,但它们之间又孰优孰劣?今天就让我们用实际代码来测试一下不同遍历方式之间的性能差异。

1. 基本介绍

先盘点一下 Java 为让我们提供了哪些遍历方式,我相信大家用的最多的应该就是 for 循环了,但在 JDK 8 之后,Java 引入了 Lambda 表达式与 Stream 流的概念,其中流又分为串行流 stream 与 并行流 parallelStream 两类。

下面针对不同的方式举个简单例子说明:

public void visitedDemo() {
    List<String> list = new ArrayList<>();
    for (int i = 1; i <= 5; i++) {
        list.add("element " + i);
    }

    // for 循环遍历
    for (String str : list) {
        System.out.print(str + ", ");
    }

    // lambda 表达式遍历
    list.forEach(str -> {
        System.out.print(str + ", ");
    });

    // 串行流遍历
    list.stream().forEach(str -> {
        System.out.print(str + ", ");
    });

    // 并发流遍历
    list.parallelStream().forEach(str -> {
        System.out.print(str + ", ");
    });
}

2. 性能测试

既然 Java 为我们提供多种遍历方案那么我们应该如何根据应用场景进行选择?哪种遍历方式又能达到最优性能?下面通过 Demo 测试在不同数据量情况下上述几种方式性能差异。

测试的方法很简单,提供定义好一个列表,使用不同方式对其进行遍历,分别记录在遍历开始时间与结束时间,二者差值即为遍历耗时,对比不同方式耗时即可得出不同遍历方式的性能差异。

这里我列表大小设置为十万,测试代码如下:

public void performTest() {
    long begin, end;
    List<String> list = new ArrayList<>();
    for (int j = 0; j < 100000; j++) {
        list.add(String.valueOf(Math.random()));
    }

    for (int i = 1; i <= 10; i++) {
        System.out.println("------------------ 第" + i + "次测试 ------------------ ");
        begin = System.nanoTime();
        for (String s : list) {
            s.toString();
        }
        end = System.nanoTime();
        System.out.println("普通 for 循环耗时: " + (end - begin) / 1000 + " μs");

        begin = System.nanoTime();
        list.forEach(e -> {
            e.toString();
        });
        end = System.nanoTime();
        System.out.println("lambda 表达式 foreach 耗时: " + (end - begin) / 1000 + " μs");

        begin = System.nanoTime();
        list.stream().forEach(e -> {
            e.toString();
        });
        end = System.nanoTime();
        System.out.println("串行流 stream 耗时: " + (end - begin) / 1000 + " μs");

        begin = System.nanoTime();
        list.parallelStream().forEach(e -> {
            e.toString();
        });
        end = System.nanoTime();
        System.out.println("并行流 stream 耗时: " + (end - begin) / 1000 + " μs");
    }
}

3. 结果分析

根据测试结果可以很明显的看到在第一次测试中 lambda 表达式耗时最长, for 循环耗时遥遥领先,但随着测试次数增加情况却发生翻转,这是因为 lambda 遍历方式首次编译启动需要进行相应的定义声明,导致首次耗时大大增加,而之后的测试中则无需重复进行定义因此耗时与其它方式基本无差,这也是网上许多人说 lambda 性能低于 for 循环的主要原因。

------------------1次测试 ------------------ 
普通 for 循环耗时: 6230 μs
lambda 表达式 foreach 耗时: 130087 μs
串行流 stream 耗时: 9358 μs
并行流 stream 耗时: 11641 μs

------------------2次测试 ------------------ 
普通 for 循环耗时: 2436 μs
lambda 表达式 foreach 耗时: 992 μs
串行流 stream 耗时: 2045 μs
并行流 stream 耗时: 3391 μs

------------------3次测试 ------------------ 
普通 for 循环耗时: 17906 μs
lambda 表达式 foreach 耗时: 937 μs
串行流 stream 耗时: 870 μs
并行流 stream 耗时: 1607 μs

------------------4次测试 ------------------ 
普通 for 循环耗时: 34833 μs
lambda 表达式 foreach 耗时: 866 μs
串行流 stream 耗时: 957 μs
并行流 stream 耗时: 1076 μs

------------------5次测试 ------------------ 
普通 for 循环耗时: 24371 μs
lambda 表达式 foreach 耗时: 807 μs
串行流 stream 耗时: 785 μs
并行流 stream 耗时: 1270 μs

------------------6次测试 ------------------ 
普通 for 循环耗时: 17677 μs
lambda 表达式 foreach 耗时: 925 μs
串行流 stream 耗时: 893 μs
并行流 stream 耗时: 1127 μs

------------------7次测试 ------------------ 
普通 for 循环耗时: 19604 μs
lambda 表达式 foreach 耗时: 1417 μs
串行流 stream 耗时: 887 μs
并行流 stream 耗时: 1163 μs

------------------8次测试 ------------------ 
普通 for 循环耗时: 18726 μs
lambda 表达式 foreach 耗时: 855 μs
串行流 stream 耗时: 1099 μs
并行流 stream 耗时: 1166 μs

------------------9次测试 ------------------ 
普通 for 循环耗时: 21533 μs
lambda 表达式 foreach 耗时: 850 μs
串行流 stream 耗时: 829 μs
并行流 stream 耗时: 910 μs

------------------10次测试 ------------------ 
普通 for 循环耗时: 17918 μs
lambda 表达式 foreach 耗时: 824 μs
串行流 stream 耗时: 837 μs
并行流 stream 耗时: 1810 μs

当我们将列表大小设置为百万时,得到如下结果。通过对比可以看到 for 循环在第一次测试中能达到相对较少的耗时,而之后的计中方式耗时基本趋于一致,但并行流 parallelStream 的方式显然效率更高。

因为 parallelStream 相对于其它几种串行的方式相当于多线程,因此在面对大量数据时自然能够表现的更为出色。

------------------1次测试 ------------------ 
普通 for 循环耗时: 22637 μs
lambda 表达式 foreach 耗时: 110947 μs
串行流 stream 耗时: 21404 μs
并行流 stream 耗时: 26229 μs

------------------2次测试 ------------------ 
普通 for 循环耗时: 47007 μs
lambda 表达式 foreach 耗时: 15911 μs
串行流 stream 耗时: 8568 μs
并行流 stream 耗时: 7379 μs

------------------3次测试 ------------------ 
普通 for 循环耗时: 31820 μs
lambda 表达式 foreach 耗时: 8790 μs
串行流 stream 耗时: 9338 μs
并行流 stream 耗时: 7069 μs

------------------4次测试 ------------------ 
普通 for 循环耗时: 36320 μs
lambda 表达式 foreach 耗时: 7964 μs
串行流 stream 耗时: 7947 μs
并行流 stream 耗时: 7296 μs

------------------5次测试 ------------------ 
普通 for 循环耗时: 11554 μs
lambda 表达式 foreach 耗时: 7542 μs
串行流 stream 耗时: 7842 μs
并行流 stream 耗时: 7555 μs

------------------6次测试 ------------------ 
普通 for 循环耗时: 8256 μs
lambda 表达式 foreach 耗时: 8815 μs
串行流 stream 耗时: 10931 μs
并行流 stream 耗时: 10562 μs

------------------7次测试 ------------------ 
普通 for 循环耗时: 10917 μs
lambda 表达式 foreach 耗时: 11637 μs
串行流 stream 耗时: 11600 μs
并行流 stream 耗时: 7650 μs

------------------8次测试 ------------------ 
普通 for 循环耗时: 10064 μs
lambda 表达式 foreach 耗时: 8703 μs
串行流 stream 耗时: 9465 μs
并行流 stream 耗时: 6836 μs

------------------9次测试 ------------------ 
普通 for 循环耗时: 10255 μs
lambda 表达式 foreach 耗时: 7792 μs
串行流 stream 耗时: 8809 μs
并行流 stream 耗时: 8194 μs

------------------10次测试 ------------------ 
普通 for 循环耗时: 9003 μs
lambda 表达式 foreach 耗时: 11233 μs
串行流 stream 耗时: 8331 μs
并行流 stream 耗时: 8037 μs

因此,当业务场中需要频繁或嵌套使用遍历, lambda 表达式与流的方式遍历都能达到不错的性能,反之则选择增强的 for 循环,而当数据量达到一定数量时,并行流 parallelStream 相对其它几种方式能达到更好的性能。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录