1、Java基础面试题问题
- 问题 21. 介绍一下 Set 集合,以及它有怎样的特性?
- 问题 22. 介绍一下 Set 集合,有哪些常见的方法?
- 问题 23. 介绍一下 HashSet 的底层结构和相关原理
- 问题 24. 介绍一下 LinkedHashSet 的底层结构和相关原理
- 问题 25. 介绍一下 TreeSet 的底层结构和相关原理
- 问题 26. 请解释一下 Java 中的 EnumSet?
- 问题 27. 请解释一下 Java 中的 SortedSet?
- 问题 28. 请解释一下 Java 中的 NavigableSet
- 问题 29. 请解释一下 Java 中的 Comparable 接口?
- 问题 30. 请解释一下 Java 中的 Comparator 接口?
- 问题 31. 请解释一下 Java 中的 CopyOnWrite
- 问题 32. 请解释一下 Java 中的 CopyOnWriteArrayList
- 问题 33. 简述什么是 Fail Fast?
- 问题 34. 简述什么是 Fail Safe?
- 问题 35. 请解释一下 Java 中的 ConcurrentModificationException?
- 问题 36. Java 中迭代器 Iterator 是什么?
- 问题 37. Java 中 Iterator 和 ListIterator 有什么区别?
- 问题 38. 为什么使用 Iterator 删除元素更加安全?
- 问题 39. 如何在 Java 中使用 Java 8 的 Stream API 处理集合?
- 问题 40. 如何在 Java 中使用 Java 8 的 forEach 方法遍历集合?
2、Java基础面试题解答
2.1、JavaSet集合相关-特性&方法
- 问题 21. 介绍一下 Set 集合,以及它有怎样的特性?
解答:Set
是 Java 集合框架中的一个接口,它继承自 Collection
接口。Set
集合中的元素是无序的,并且不包含重复的元素。
Set
集合的主要特性包括:
- 无序:
Set
集合中的元素没有特定的顺序。也就是说,我们不能通过索引来访问Set
集合中的元素。 - 不可重复:
Set
集合不允许插入重复的元素。如果试图插入已经存在的元素,Set
集合不会报错,但是插入操作不会有任何效果。 - 元素可为 null:
Set
集合中可以添加 null 元素。
Java 中的 HashSet
、LinkedHashSet
和 TreeSet
都是 Set
接口的实现类,它们具有上述的 Set
特性,但是在内部实现和性能上有所不同。例如,HashSet
是基于哈希表实现的,插入和查询的性能较高;LinkedHashSet
是在 HashSet
的基础上,增加了链表来保证元素的插入顺序;TreeSet
是基于红黑树实现的,元素会按照自然顺序或者自定义的顺序进行排序。
- 问题 22. 介绍一下 Set 集合,有哪些常见的方法?
解答:Set
接口在 Collection
接口的基础上,没有新增任何方法,主要的方法都继承自 Collection
接口。以下是 Set
接口中一些常见的方法:
-
boolean add(E e)
:向集合中添加元素,如果集合已经包含该元素,则返回false
。 -
void clear()
:清空集合,移除所有元素。 -
boolean contains(Object o)
:判断集合是否包含指定的元素。 -
boolean isEmpty()
:判断集合是否为空。 -
Iterator<E> iterator()
:返回一个用于遍历集合的迭代器。 -
boolean remove(Object o)
:从集合中移除指定的元素。 -
int size()
:返回集合中元素的数量。 -
Object[] toArray()
:将集合转换为数组。
以上就是 Set
接口中一些常见的方法,它们提供了丰富的功能,使得我们可以方便地对集合进行操作。
2.2、JavaSet集合相关-具体实现
- 问题 23. 介绍一下 HashSet 的底层结构和相关原理
解答:HashSet
是基于 HashMap
实现的,底层采用 HashMap
来保存所有元素。因此,HashSet
的数据结构就是 HashMap
的数据结构。
HashMap
是一个散列表,它存储的内容是键值对 (key-value)。HashMap
通过键的哈希值进行快速查找,具有较高的查找和插入速度。
HashSet
中的元素实际上作为 HashMap
的键存在,而 HashMap
的值则存储了一个固定的对象 PRESENT
。因此,HashSet
中的元素不能重复,这是因为 HashMap
的键不能重复。
HashSet
的操作都是基于 HashMap
的操作来实现的,例如添加元素、删除元素、查找元素等。
- 问题 24. 介绍一下 LinkedHashSet 的底层结构和相关原理
解答:LinkedHashSet
是 HashSet
的一个子类,它的底层是基于 LinkedHashMap
来实现的。
LinkedHashMap
是 HashMap
的一个子类,它在 HashMap
的基础上,增加了一个双向链表。这个双向链表连接了所有的键值对,定义了键值对的迭代顺序。迭代的顺序可以是插入顺序,也可以是访问顺序。
LinkedHashSet
中的元素实际上作为 LinkedHashMap
的键存在,而 LinkedHashMap
的值则存储了一个固定的对象 PRESENT
。因此,LinkedHashSet
中的元素不能重复,这是因为 LinkedHashMap
的键不能重复。
LinkedHashSet
的操作都是基于 LinkedHashMap
的操作来实现的,例如添加元素、删除元素、查找元素等。由于 LinkedHashSet
维护了一个运行于所有条目的双向链表,因此,可以在用迭代器遍历 LinkedHashSet
时,得到一个确定的顺序(插入的顺序)。
- 问题 25. 介绍一下 TreeSet 的底层结构和相关原理
解答:TreeSet
是基于 TreeMap
实现的,底层采用 TreeMap
来保存所有元素。因此,TreeSet
的数据结构就是 TreeMap
的数据结构。
TreeMap
是一个红黑树(自平衡的排序二叉树)。它存储的内容是键值对 (key-value)。TreeMap
通过键的自然顺序或者自定义的比较器进行排序,具有较高的查找和插入速度。
TreeSet
中的元素实际上作为 TreeMap
的键存在,而 TreeMap
的值则存储了一个固定的对象 PRESENT
。因此,TreeSet
中的元素不能重复,这是因为 TreeMap
的键不能重复。
TreeSet
的操作都是基于 TreeMap
的操作来实现的,例如添加元素、删除元素、查找元素等。由于 TreeSet
是基于 TreeMap
实现的,所以 TreeSet
的元素是有序的,元素的排序方式取决于构造 TreeSet
时提供的 Comparator
,或者依赖元素的自然顺序(Comparable
)。
TreeSet
是 SortedSet
接口的一个实现类,它提供了一个基于树结构的 Set
,元素可以按照自然顺序或者自定义的比较器进行排序。
- 问题 26. 请解释一下 Java 中的 EnumSet?
解答:EnumSet
是 Java 中的一个专门为枚举类型设计的集合类。它继承自 AbstractSet
,并实现了 Set
接口。
以下是 EnumSet
的一些特性:
-
EnumSet
中的所有元素都必须来自同一个枚举类型,它在创建时显式或隐式地指定。 -
EnumSet
是有序的,其元素的顺序就是它们在源代码中的顺序。 -
EnumSet
集合类的实现是非常高效和快速的,其大部分操作都是通过位运算实现的。 -
EnumSet
不允许使用null
元素,如果尝试添加null
元素,它会抛出NullPointerException
。 -
EnumSet
是线程不安全的,如果多个线程同时修改EnumSet
,需要进行同步处理。
以下是创建 EnumSet
的一些方法:
EnumSet.allOf(Class<E> elementType)
:创建一个包含指定枚举类型的所有元素的EnumSet
。EnumSet.noneOf(Class<E> elementType)
:创建一个指定枚举类型的空EnumSet
。EnumSet.of(E first, E... rest)
:创建一个最初包含指定元素的EnumSet
。EnumSet.range(E from, E to)
:创建一个包含从from
元素到to
元素范围内的所有元素的EnumSet
。EnumSet.copyOf(Collection<E> c)
或EnumSet.copyOf(EnumSet<E> s)
:创建一个与指定EnumSet
具有相同元素类型的EnumSet
,最初包含相同的元素(如果有的话)。
- 问题 27. 请解释一下 Java 中的 SortedSet?
解答:SortedSet
是 Java 集合框架中的一个接口,它继承自 Set
接口。SortedSet
接口为集合中的元素提供了一个总的排序。
以下是 SortedSet
的一些特性:
-
SortedSet
中的元素按照自然顺序或者自定义的比较器(Comparator)进行排序。 -
SortedSet
不允许插入null
元素。如果尝试插入null
元素,它会抛出NullPointerException
。 -
SortedSet
是线程不安全的,如果多个线程同时修改SortedSet
,需要进行同步处理。
以下是 SortedSet
的一些主要方法:
-
Comparator<? super E> comparator()
:返回用于对此 set 中的元素进行排序的比较器;如果此 set 使用其元素的自然顺序,则返回null
。 -
E first()
:返回此 set 中当前第一个(最低)元素。 -
SortedSet<E> headSet(E toElement)
:返回此 set 的部分视图,其元素严格小于toElement
。 -
E last()
:返回此 set 中当前最后一个(最高)元素。 -
SortedSet<E> subSet(E fromElement, E toElement)
:返回此 set 的部分视图,其元素的范围从fromElement
(包括)到toElement
(不包括)。 -
SortedSet<E> tailSet(E fromElement)
:返回此 set 的部分视图,其元素大于等于fromElement
。
- 问题 28. 请解释一下 Java 中的 NavigableSet
解答:NavigableSet
是 Java 集合框架中的一个接口,它继承自 SortedSet
接口。NavigableSet
描述了一种可以通过搜索方法导航的数据结构。
以下是 NavigableSet
的一些特性:
-
NavigableSet
中的元素按照自然顺序或者自定义的比较器(Comparator)进行排序。 -
NavigableSet
提供了多种导航方法,例如获取小于/大于某个元素的最大/最小元素等。 -
NavigableSet
不允许插入null
元素。如果尝试插入null
元素,它会抛出NullPointerException
。 -
NavigableSet
是线程不安全的,如果多个线程同时修改NavigableSet
,需要进行同步处理。
以下是 NavigableSet
的一些主要方法:
-
E lower(E e)
:返回此 set 中严格小于给定元素的最大元素;如果不存在这样的元素,则返回null
。 -
E floor(E e)
:返回此 set 中小于等于给定元素的最大元素;如果不存在这样的元素,则返回null
。 -
E ceiling(E e)
:返回此 set 中大于等于给定元素的最小元素;如果不存在这样的元素,则返回null
。 -
E higher(E e)
:返回此 set 中严格大于给定元素的最小元素;如果不存在这样的元素,则返回null
。 -
E pollFirst()
:获取并移除此 set 中的第一个(最低)元素;如果此 set 为空,则返回null
。 -
E pollLast()
:获取并移除此 set 中的最后一个(最高)元素;如果此 set 为空,则返回null
。
TreeSet
是 NavigableSet
接口的一个实现类,它提供了一个基于树结构的 Set
,元素可以按照自然顺序或者自定义的比较器进行排序。
2.3、Java排序接口相关
- 问题 29. 请解释一下 Java 中的 Comparable 接口?
解答:Comparable
是 Java 中的一个接口,用于定义对象之间的自然排序规则。如果一个类实现了 Comparable
接口,那么它的对象就可以进行比较和排序。
Comparable
接口定义了一个 compareTo
方法,需要实现类进行重写。compareTo
方法接收一个同类型的对象作为参数,返回一个整数,有三种可能:
- 返回 0,表示 this 等于参数对象;
- 返回正数,表示 this 大于参数对象;
- 返回负数,表示 this 小于参数对象。
例如,下面的代码定义了一个 Person
类,实现了 Comparable
接口,按照年龄进行排序:
public class Person implements Comparable<Person> {
private String name;
private int age;
// ... 省略构造方法和 getter、setter 方法 ...
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
这样,我们就可以对 Person
对象进行排序:
List<Person> people = Arrays.asList(
new Person("Alice", 20),
new Person("Bob", 18),
new Person("Charlie", 22)
);
Collections.sort(people);
以上就是 Comparable
接口的基本概念和用法。通过实现 Comparable
接口,我们可以定义对象的自然排序规则,使得对象可以进行比较和排序。
- 问题 30. 请解释一下 Java 中的 Comparator 接口?
解答:Comparator
是 Java 中的一个接口,用于定义对象之间的定制排序规则。如果一个类没有实现 Comparable
接口,或者实现了但是开发者希望有其他的排序方式,那么可以使用 Comparator
。
Comparator
接口定义了一个 compare
方法,需要开发者进行重写。compare
方法接收两个同类型的对象作为参数,返回一个整数,有三种可能:
- 返回 0,表示第一个参数等于第二个参数;
- 返回正数,表示第一个参数大于第二个参数;
- 返回负数,表示第一个参数小于第二个参数。
例如,下面的代码定义了一个 Person
类,以及一个按照年龄排序的 Comparator
:
public class Person {
private String name;
private int age;
// ... 省略构造方法和 getter、setter 方法 ...
}
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
这样,我们就可以使用 AgeComparator
对 Person
对象进行排序:
List<Person> people = Arrays.asList(
new Person("Alice", 20),
new Person("Bob", 18),
new Person("Charlie", 22)
);
Collections.sort(people, new AgeComparator());
以上就是 Comparator
接口的基本概念和用法。通过实现 Comparator
接口,我们可以定义对象的定制排序规则,使得对象可以按照我们想要的方式进行排序。
2.4、Java集合并发相关
- 问题 31. 请解释一下 Java 中的 CopyOnWrite
解答:CopyOnWrite
是 Java 中的一种并发策略,主要应用于多线程环境下的读多写少的场景。Java 提供了 CopyOnWriteArrayList
和 CopyOnWriteArraySet
两个线程安全的集合类,它们的实现都采用了 “写时复制”(Copy-On-Write)的策略。
“写时复制” 的基本思想是:当我们需要修改集合(如添加、删除元素)时,不直接在当前集合上进行修改,而是先将当前集合进行复制,然后在新的副本上进行修改,最后再将引用指向新的副本。这样,读操作都是在原集合上进行,不需要加锁;写操作是在副本上进行,也不会影响读操作,实现了读写分离。
“写时复制” 的优点是可以实现高并发的读操作,适合读多写少的并发场景。但是,它也有一些缺点:
- 内存占用:每次写操作都会复制一份新的集合,如果数据量大,会占用较多的内存。
- 数据一致性:读操作可能无法读取到最新的数据,因为写操作是在副本上进行的。
- 写操作性能:由于需要复制新的集合,所以写操作的性能会比较低。
以上就是 “写时复制” 的基本原理和特点。在使用 CopyOnWriteArrayList
和 CopyOnWriteArraySet
时,需要根据实际的并发场景来权衡其优缺点。
- 问题 32. 请解释一下 Java 中的 CopyOnWriteArrayList
解答:CopyOnWriteArrayList
是 Java 中的一个线程安全的 List
实现,它是通过"写时复制"(Copy-On-Write)策略来保证并发安全的。
- 写时复制策略:当对
CopyOnWriteArrayList
进行修改操作(如add
、set
、remove
等)时,它并不直接在当前数组上进行修改,而是先将当前数组进行复制,然后在新的数组上进行修改,最后再将引用指向新的数组。这样可以保证在修改过程中不会影响到读操作,实现了读写分离。 - 读操作无锁:由于所有的写操作都是在新的数组上进行的,所以读操作是无锁的,可以直接读取,这对于读多写少的场景性能提升很大。
- 写操作加锁:写操作(修改、添加、删除等)需要加锁,防止多线程同时写入时导致数据不一致。
- 内存占用:由于每次写操作都需要复制一个新的数组,所以
CopyOnWriteArrayList
在内存占用上会比普通的ArrayList
大。
总的来说,CopyOnWriteArrayList
是一种适用于读多写少且需要线程安全的场景的 List
实现。但是由于写时复制策略,它在内存占用和写操作性能上有一定的开销。
- 问题 33. 简述什么是 Fail Fast?
解答:“Fail Fast” 是 Java 集合框架中的一个错误检测机制。当多个线程对一个集合进行并发操作时,如果一个线程通过迭代器(Iterator)在遍历集合的过程中,其他线程修改了集合的结构(如添加、删除元素),那么正在遍历的线程会立即抛出 ConcurrentModificationException
异常。
“Fail Fast” 的主要目的是为了快速发现并发修改的问题,而不是等到程序运行一段时间后才发现问题。这种机制可以帮助我们尽早发现并发编程中的错误,避免出现难以预料的结果。
需要注意的是,“Fail Fast” 机制并不能保证在所有情况下都能检测到并发修改的问题,它只能尽最大可能地发现问题。另外,“Fail Fast” 机制并不是用来解决并发问题的,如果需要在多线程环境下安全地操作集合,应该使用线程安全的集合类,或者通过同步机制来保护非线程安全的集合。
- 问题 34. 简述什么是 Fail Safe?
“Fail Safe” 是 Java 集合框架中的一种错误处理机制。在 “Fail Safe” 机制下,当一个线程正在遍历集合的过程中,其他线程对集合进行修改,不会抛出 ConcurrentModificationException
异常。
“Fail Safe” 机制的实现通常是通过创建集合的副本来实现的。当进行遍历操作时,遍历的是原集合的副本,而不是原集合。因此,对原集合的修改不会影响到遍历操作,也就不会抛出 ConcurrentModificationException
异常。
Java 中的 CopyOnWriteArrayList
和 CopyOnWriteArraySet
就是使用了 “Fail Safe” 机制。这两个类在进行修改操作时,会创建原集合的副本,然后在副本上进行修改,最后再将引用指向新的副本。
需要注意的是,“Fail Safe” 机制虽然可以避免抛出 ConcurrentModificationException
异常,但是由于需要创建集合的副本,所以在内存占用和性能上会有一些开销。另外,由于遍历操作是在原集合的副本上进行的,所以可能无法看到其他线程对原集合的修改结果。
- 问题 35. 请解释一下 Java 中的 ConcurrentModificationException?
解答:ConcurrentModificationException
是 Java 中的一个运行时异常,通常在多线程环境下,一个线程正在遍历集合的过程中,另一个线程修改了集合的结构(如添加、删除元素),那么正在遍历的线程可能会抛出这个异常。
这个异常通常是由 “Fail Fast” 机制引发的。“Fail Fast” 是 Java 集合框架中的一个错误检测机制,它的目的是为了尽早发现并发修改的问题,避免出现难以预料的结果。
需要注意的是,“Fail Fast” 机制并不能保证在所有情况下都能检测到并发修改的问题,它只能尽最大可能地发现问题。另外,“Fail Fast” 机制并不是用来解决并发问题的,如果需要在多线程环境下安全地操作集合,应该使用线程安全的集合类,或者通过同步机制来保护非线程安全的集合。
如果遇到 ConcurrentModificationException
异常,应该检查代码,确保在遍历集合的过程中,没有其他线程对集合进行修改。如果需要在遍历过程中修改集合,可以使用 Iterator
的 remove()
方法,或者使用 ListIterator
的 add()
和 set()
方法,这些方法可以安全地在遍历过程中修改集合。
2.5、Java迭代器相关
2.2、Java迭代器相关
- 问题 36. Java 中迭代器 Iterator 是什么?
Iterator
是 Java 中的一个接口,它提供了一种统一的方式来遍历集合中的元素。Iterator
接口定义了三个方法:
-
hasNext()
:检查是否还有下一个元素,如果有则返回true
,否则返回false
。 -
next()
:返回当前元素,并将迭代器向前移动到下一个元素。 -
remove()
:删除迭代器最后一次返回的元素。这个方法是可选的,不是所有的迭代器都支持。
在 Java 的集合框架中,所有的 Collection
子类都提供了一个 iterator()
方法,用于返回一个 Iterator
对象,通过这个对象可以遍历集合中的元素。
例如,下面的代码展示了如何使用 Iterator
遍历一个 ArrayList
:
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
这就是 Iterator
的基本概念和用法。通过 Iterator
,我们可以方便地遍历集合中的元素,而不需要关心集合的具体实现。
- 问题 37. Java 中 Iterator 和 ListIterator 有什么区别?
Iterator
和 ListIterator
都是 Java 中的迭代器接口,它们都提供了遍历集合元素的方法,但是 ListIterator
提供了更多的功能。
- 双向遍历:
Iterator
只能进行单向遍历,从前往后。而ListIterator
支持双向遍历,既可以从前往后,也可以从后往前。ListIterator
提供了hasPrevious
和previous
方法来实现从后往前的遍历。 - 添加元素:
ListIterator
提供了add
方法,可以在遍历过程中添加元素,而Iterator
不支持这个操作。 - 修改元素:
ListIterator
提供了set
方法,可以修改最后一次返回的元素,而Iterator
不支持这个操作。 - 获取元素索引:
ListIterator
提供了nextIndex
和previousIndex
方法,可以获取下一个或上一个元素的索引,而Iterator
不支持这个操作。 - 使用范围:
Iterator
可以应用于所有的Collection
子类,而ListIterator
只能应用于List
子类。
以上就是 Iterator
和 ListIterator
的主要区别。在需要进行更复杂的遍历操作时,可以选择使用 ListIterator
。
- 问题 38. 为什么使用 Iterator 删除元素更加安全?
使用 Iterator
删除集合中的元素更加安全,主要有以下两个原因:
- 避免并发修改异常:在使用
for-each
循环或者普通的for
循环遍历集合的过程中,如果直接调用集合的remove
方法删除元素,可能会抛出ConcurrentModificationException
异常。这是因为在遍历过程中,集合的结构发生了改变,但是这个改变并没有同步到正在进行的迭代过程中,所以会抛出异常。 - 避免索引问题:在使用普通的
for
循环遍历List
的过程中,如果直接调用List
的remove
方法删除元素,可能会出现索引问题。因为删除元素后,后面的元素的索引会发生改变,可能会导致跳过某些元素或者重复处理某些元素。而使用Iterator
的remove
方法删除元素,迭代器会正确地移动到下一个元素,不会出现这个问题。
因此,推荐在遍历集合的过程中,使用 Iterator
的 remove
方法删除元素。
2.6、Java-8中的流处理
- 问题 39. 如何在 Java 中使用 Java 8 的 Stream API 处理集合?
Java 8 引入了一个新的 Stream
API,它提供了一种新的方式来处理集合。Stream
API 可以让我们以声明式的方式处理数据,使代码更简洁,易读。
以下是一些使用 Stream
API 处理集合的例子:
过滤:使用 filter()
方法可以过滤出满足条件的元素。
List<String> names = Arrays.asList("John", "Jane", "Adam", "Tom");
List<String> result = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
上述代码会过滤出所有以 “J” 开头的名字。
映射:使用 map()
方法可以将元素转换成其他形式。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
上述代码会将每个数字映射成它的平方。
排序:使用 sorted()
方法可以对元素进行排序。
List<String> names = Arrays.asList("John", "Jane", "Adam", "Tom");
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
上述代码会将名字按照字母顺序进行排序。
统计:使用 count()
、max()
、min()
、average()
等方法可以进行统计。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long count = numbers.stream().count();
Optional<Integer> max = numbers.stream().max(Integer::compare);
上述代码会统计数字的数量和最大值。
以上就是一些使用 Stream
API 处理集合的例子,Stream
API 还提供了很多其他的方法,如 reduce()
、collect()
、flatMap()
等,可以满足各种复杂的数据处理需求。
- 问题 40. 如何在 Java 中使用 Java 8 的 forEach 方法遍历集合?
解答:Java 8 在 Iterable
接口中添加了一个新的 forEach
方法,可以更简洁地遍历集合。forEach
方法接受一个 Consumer
函数式接口的实例作为参数,用于处理集合中的每个元素。
以下是使用 forEach
方法遍历集合的例子:
List<String> names = Arrays.asList("John", "Jane", "Adam", "Tom");
// 使用 lambda 表达式
names.forEach(name -> System.out.println(name));
// 使用方法引用
names.forEach(System.out::println);
在上述代码中,我们使用了 lambda 表达式和方法引用两种方式来处理集合中的每个元素。这两种方式都可以使代码更简洁,易读。
需要注意的是,forEach
方法的遍历顺序并不是固定的,它取决于具体的集合实现。如果需要固定的遍历顺序,应该使用 List
或者 LinkedHashSet
等有序的集合。