集合(1)

概述

  • Java集合大致可分为Set、List、Queue和Map四种体系,其中Set代表无序、不可重复的集合;List代表有序、重复的集合;而Map代表具有映射关系的集合;Java5又增加了Queue集合体系,代表一种队列集合实现。
  • Java集合就像一个容器,可以把多个对象(实际上是对象的引用,但习惯上都称对象)“丢进”该容器中。
  • 集合体系如下:

Iterator

  • Iterator接口也是java集合框架中的成员,它并不是作为容器,而是主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。
  • Iterator接口隐藏了各种Collection实现类的底层细节,向外界提供了遍历Collection集合的统一编程接口。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class IteratorTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i);
}

Iterator iterator = list.iterator(); //获取到集合的Iterator
while (iterator.hasNext()) { //如果集合元素还没有遍历完,则返回true
Integer i = (Integer) iterator.next(); //next方法返回的是Object,需进行强制类型转换
System.out.print(i + "\t");
if (i == 1) {
iterator.remove(); //在集合中删除上一次next方法返回的元素
}
}

System.out.println(list);
}
}

输出结果:

1
0	1	2	3	4	[0, 2, 3, 4]

注意

  1. 当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响。
  2. 当使用Iterator变量元素时,Collection集合的元素不能被修改,除了通过Iterator的remove方法删除上一次next方法返回的元素,否则会引发java.util.ConcurrentModificationException异常。例如下面操作:
    1
    2
    3
    if (i == 1) {
    list.remove(i); //该操作会引发异常
    }

这是因为Iterator采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即抛出异常,这样可以避免共享资源引发的潜在问题。

Predicate

  • Java8为Collection集合新增了一个removeIf(Predicate<? super E> filter)方法,该方法将会批量删除符合条件(Predicate接口中的test方法返回结果为true)的所有元素

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PredicateTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(i);
}

list.removeIf(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer == 2; //删除值为2的元素
}
});

System.out.println(list);
}
}

输出结果:

1
[0, 1, 3, 4]

由于Predicate也是函数式接口,因此可以使用Lambda表达式作为参数,化简后如下

1
list.removeIf(integer -> integer == 2);     //删除值为2的元素

Stream

  • Java8还新增了Stream、IntStream、LongStream、DoubleStream等流式接口,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream分别代表int、long、double元素的流。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StreamTest {
public static void main(String[] args) {
IntStream.Builder builder = IntStream.builder(); //先创建该Stream对应的Builder
for (int i = 0; i < 5; i++) {
builder.add(i); //添加元素
}
IntStream intStream = builder.build(); //获取对应的流

//注意:只能调用一次聚集方法,即max和min只能调用一个,否则会抛出异常
System.out.println("所以元素的最大值为:" + intStream.max().getAsInt());
//System.out.println("所以元素的最小值为:" + intStream.min().getAsInt());

//将原来的Stream映射成一个新的Stream,新Stream中每个元素都是原来的两倍
//IntStream newIs = intStream.map(operand -> operand * 2);
//遍历Stream里面的元素
//newIs.forEach(value -> System.out.print(value + "\t"));
}
}

输出结果:

1
所以元素的最大值为:4

如果注释max,取消注释map的话

1
0	2	4	6	8

Stream的方法

  • Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。中间方法允许流保持打开状态,并允许直接调用后续方法,例如map方法。而末端方法是对流的最终操作,执行完该方法后流将会被“消耗”而不可再用,例如max和min方法。

常用的中间方法

常用的末端方法


Set

HashSet

  • HashSet是Set接口的典型实现,HashSet按哈希算法来存储集合中的元素,因此具有很好的存取和查找功能。
  • HashSet具有以下特点:
    1. 不能保证元素的排列顺序
    2. HashSet不能同步的,如果有多个线程同时访问一个HashSet对象,必须通过代码来保证其同步
    3. 集合元素值可以是null(但只能有一个)
  • 当向HashSet集合中存入一个元素时,HashSet会调用对象的hashCode()方法来得到该对象的哈希值,然后根据该哈希值决定该对象在HashSet中的存储位置。即使两个元素通过equals()方法比较返回true,只要它们的哈希值不一样,HashSet还是会把它们放在不同位置,依然可以添加成功。
  • HashSet是底层是基于HashMap来实现的,所以HashMap适用的HashSet也基本适用,反之亦然。

注意

  • 当重写一个类的equals方法或hashCode方法时,应该尽量保证两个对象通过equals方法比较返回true时,它们的hashCode方法返回值也相等。原因如下:

    HashSet(或者HashMap)判断两个元素相等的标志是两个对象通过equals方法比较返回true,并且它们的hashCode方法返回值也相等。如果两个对象通过equals方法比较返回true,但这两个对象返回的哈希值不同,那么这两个对象都可以添加成功,这违背了Set集合的规则。如果通过equals方法比较返回false,但两个对象的哈希值相同,那么这两个对象都能添加成功,这部违背Set集合的规则,但是由于两个对象的哈希值相同,HashSet试图将他们保存在同一位置,但这样又不行,所以实际上会在这个位置用链式结构来保持多个对象,这将会导致性能下降。

  • 当程序把可变对象添加到HashSet之后,尽量不要去修改该对象中参与计算hashCode、equals的实例变量,否则将会导致HashSet无法正确操作这些集合元素。

LinkedHashSet

  • LinkedHashSet是HashSet的子类,LinkedHashSet也是根据元素的哈希值来决定元素的存储位置,但它同时使用链表来维护元素的次序,也就是说当遍历LinkedHashSet的元素时,将会按照元素的添加顺序来遍历。
  • LinkedHashSet需要维护元素的顺序,因此性能略低于HashSet,但在迭代访问Set集合的全部元素时有很好的性能。

TreeSet

  • TreeSet是SortedSet接口的实现类,可以确保集合元素处于排序状态。
  • 与HashSet集合相比,TreeSet还提供了几个额外的方法,例如访问第一个、最后一个、前一个、后一个或者截取子TreeSet的方法。
  • TreeSet采用红黑树的数据结构来存储集合元素。
  • TreeSet支持两种排序:自然排序和定制排序。默认情况下,TreeSet采用自然排序。

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>();
set.add(5);
set.add(2);
set.add(10);
set.add(-7);
System.out.println(set); //输出集合元素,看到集合已经处于排序状态(升序)
System.out.println(set.first()); //输出第一个元素
System.out.println(set.last()); //输出最后一个元素
System.out.println(set.headSet(3)); //返回一个SortSet(下同),由小于3的元素组成
System.out.println(set.tailSet(5)); //由大于或等于5的元素组成
System.out.println(set.subSet(-7, 5)); //由[-7, 5)的元素组成
}
}

输出结果:

1
2
3
4
5
6
[-7, 2, 5, 10]
-7
10
[-7, 2]
[5, 10]
[-7, 2]

自然排序

  • TreeSet会调用集合元素的compareTo方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。

    java提供了一个Comparable接口,该接口里定义了一个compareTo(T o)方法,该方法返回一个整数值。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2);如果该方法返回0,则表示这两个对象相等;如果返回一个正整数,则表示obj1大于obj2;如果返回一个负整数,则表示obj1小于obj2。

  • 如果试图把一个对象添加到TreeSet,那么该对象的类必须实现Comparable接口,否则程序将抛出ClassCastException异常
  • 当把一个对象加入TreeSet集合时,TreeSet调用该对象的compareTo方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。如果两个对象通过compareTo方法比较返回0,则表示这两个对象相等,新对象无法添加到集合中。

定制排序

  • 如果需要实现定制排序,例如降序排列,则可以通过Comparator接口(注意和上面的Comparable接口区别开)的compare(T o1, T o2)来实现,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TreeSetTest2 {
public static void main(String[] args) {
TreeSet<Integer> set = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 > o2 ? -1 : o1 < o2 ? 1 : 0; //重写该方法,定制降序排列
}
});
set.add(5);
set.add(2);
set.add(10);
set.add(-7);

System.out.println(set);
}
}

输出结果:

1
[10, 5, 2, -7]

注意

  1. 当需要把一个对象加入TreeSet中,如果重写该对象对应类的equals方法,必须保证该方法与compareTo方法返回的结果一致。其规则是:如果这两个对象通过equals方法比较返回true时,通过compareTo方法比较应该返回0。
  2. 不要修改集合元素的关键实例变量(涉及到比较元素是否相等的实例变量),否则将会导致对应元素无法删除,并且可能破坏了集合元素的有序性。

EnumSet

  • EnumSet是专门为枚举类设计的集合类,EnumSet中的所有元素必须是同一个枚举类中的枚举元素。
  • EnumSet的集合元素也是有序的,其以枚举值在其枚举类中定义的顺序来排序。
  • EnumSet在内部以位相量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如containsAll()、retainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。
  • EnumSet不允许加入null元素,否则将会抛出java.lang.NullPointerException异常。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EnumSetTest {
enum Season { //枚举类
SPRING, SUMMER, FAIL, WINTER
}

public static void main(String[] args) {
//创建一个集合元素为Season枚举类的全部枚举值的EnumSet
EnumSet set1 = EnumSet.allOf(Season.class);
System.out.println(set1);
//创建一个EnumSet空集合,指定其元素是Season枚举类的枚举值
EnumSet set2 = EnumSet.noneOf(Season.class);
System.out.println(set2);
set2.add(Season.SPRING); //添加元素
System.out.println(set2);
//以指定枚举值创建EnumSet
EnumSet set3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(set3);
//创建一个包含从from枚举值(第一个参数)到to枚举值(第二个参数)范围内所有枚举值的EnumSet
EnumSet set4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(set4);
//创建的集合是另外一个集合的补集
//即set5的枚举元素 = Season所有枚举元素 - set4的枚举元素
EnumSet set5 = EnumSet.complementOf(set4);
System.out.println(set5);
//通过复制另一个EnumSet集合来创建EnumSet
EnumSet set6 = EnumSet.copyOf(set5);
System.out.println(set6);
}
}

输出结果:

1
2
3
4
5
6
7
[SPRING, SUMMER, FAIL, WINTER]
[]
[SPRING]
[SUMMER, WINTER]
[SUMMER, FAIL, WINTER]
[SPRING]
[SPRING]

各Set实现类的性能分析

  • HashSet和TreeSet是Set的两个典型实现,HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护元素的次序。只有当需要一个保存顺序的Set时,才考虑使用TreeSet,否则都应该使用HashSet。
  • 对于普通的插入、删除操作,LinkedHashSet比HashSet要慢一些,这是由于维护链表所带来的额外开销,但由于有了链表,遍历LinkedHashSet会更快。
  • EnumSet是所有Set实现类中性能最好的,但它只能保持同一个枚举类的枚举值。
  • 这几个实现类都是线程不安全的,如果有多个线程同时访问并修改一个Set集合,则必须手动保证该Set集合的同步性。
-------------    本文到此结束  感谢您的阅读    -------------
0%