一、迭代器
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
相对其它几种方式能达到更好的性能。