8.4 List集合

List集合代表一个元素有序可重复的集合,集合中每个元素都有其对应的顺序索引。

  • List集合允许使用重复元素,
  • 可以通过索引来访问指定位置的集合元素。
  • List集合默认按元素的添加顺序设置元素的索引,例如第一次添加的元素索引为0,第二次添加的元素索引为1,以此类推。

8.3.5 各Set实现类的性能分析

优先使用HashSet

HashSetTreeSetset的两个典型实现,到底如何选择HashSetTreeSet呢?
HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。

需要排序时使用TreeSet

只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet

LinkedHashSet有序

HashSet还有一个子类: LinkedHashSet,对于普通的插入、删除操作, LinkedHashSetHashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。

EnumSet用来保存枚举

EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。

HashSet TreeSet EnumSet都是线程不安全的

必须指出的是,Set的三个实现类HashSetTreeSetEnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。

如何线程安全:使用Collections工具类包装Set集合

通常可以通过Collections工具类的synchronizedSortedSet方法来”包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。例如:
SortedSet s= Collections.synchronizedSortedSet(new TreeSet(...));
关于Collections工具类的更进一步用法,可以参考8.8节的内容。

8.3.4 EnumSet类

EnumSet是一个专为枚举类设计的集合类, EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。

EnumSet有序

EnumSet的集合元素也是有序的, EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序

EnumSet以向量存储 内存占用小

EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好,尤其是调用containsAll(),retainAll()等方法来进行批量操作时。如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。

EnumSet中不能加入null

EnumSet集合不允许加入null元素,如果试图插入null元素, EnumSet将抛出NullPointerException异常。如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为EnumSet中无法存入null,所以根本没有任何null元素被删除。

EnumSet不提供构造器,通过类方法来创建EnumSet对象

EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。

常用EnumSet类方法

EnumSet类它提供了如下常用的类方法来创建EnumSet对象。

创建所有枚举值构成的EnumSet

方法 描述
static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) 创建一个包含指定枚举类里所有枚举值的EnumSet集合。

创建EnumSet的补集构成的EnumSet

方法 描述
static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) 创建一个其元素类型与指定EnumSet元素类型相同的EnumSet集合,EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值)。就是类似求差集

复制Collection集合或者其他EnumSet中的所有元素来创建EnumSet

方法 描述
static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) 使用一个普通集合来创建EnumSet集合。
static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) Creates an enum set with the same element type as the specified enum set, initially containing the same elements (if any).

创建 指定枚举类型的 空的EnumSet

方法 描述
static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) 创建一个元素类型为指定枚举类型的空EnumSet

直接给定的枚举值来创建EnumSet

of方法可以创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。

方法 描述
static <E extends Enum<E>> EnumSet<E> of(E e) Creates an enum set initially containing the specified element.
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) Creates an enum set initially containing the specified elements.
static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) Creates an enum set initially containing the specified elements.
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) Creates an enum set initially containing the specified elements.
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) Creates an enum set initially containing the specified elements.
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) Creates an enum set initially containing the specified elements.

创建指定区间之间的枚举值构成的EnumeSet

方法 描述
static <E extends Enum<E>> EnumSet<E> range(E from, E to) 创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。

程序 创建EnumSet的各种姿势

下面程序示范了如何使用EnumSet来保存枚举类的多个枚举值。

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
30
31
32
33
34
35
36
import java.util.*;

enum Season
{
SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest
{
public static void main(String[] args)
{
// 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1); // 输出[SPRING,SUMMER,FALL,WINTER]

// 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值。
EnumSet es2 = EnumSet.noneOf(Season.class);
System.out.println(es2); // 输出[]

// 手动添加两个元素
es2.add(Season.WINTER);
es2.add(Season.SPRING);
System.out.println(es2); // 输出[SPRING,WINTER]

// 以指定枚举值创建EnumSet集合
EnumSet es3 = EnumSet.of(Season.SUMMER , Season.WINTER);
System.out.println(es3); // 输出[SUMMER,WINTER]

EnumSet es4 = EnumSet.range(Season.SUMMER , Season.WINTER);
System.out.println(es4); // 输出[SUMMER,FALL,WINTER]

// 新创建的EnumSet集合的元素和es4集合的元素有相同类型,
// es5的集合元素 + es4集合元素 = Season枚举类的全部枚举值
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5); // 输出[SPRING]
}
}

运行效果:

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

通过复制集合中的元素来创建EnumSet

除此之外,还可以复制另一个EnumSet集合中的所有元素来创建新的EnumSet集合,或者复制另一个Collection集合中的所有元素来创建新的EnumSet集合。当复制Collection集合中的所有元素来创建新的EnumSet集合时,要求Collection集合中的所有元素必须是同一个枚举类的枚举值。

复制Collection来创建EnumSet的前提

当试图复制一个Collection集合里的元素来创建EnumSet集合时,必须保证Collection集合里的所有元素都是同一个枚举类的枚举值

程序 复制集合中的元素来创建EnumSet

下面程序示范了这个用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

public class EnumSetTest2
{
public static void main(String[] args)
{
Collection c = new HashSet();
c.clear();
c.add(Season.FALL);
c.add(Season.SPRING);

// 复制Collection集合中所有元素来创建EnumSet集合
EnumSet enumSet = EnumSet.copyOf(c); // ①
System.out.println(enumSet); // 输出[SPRING,FALL]

c.add("疯狂Java讲义");
c.add("轻量级Java EE企业应用实战");
// 下面代码出现异常:因为c集合里的元素不是全部都为枚举值
enumSet = EnumSet.copyOf(c); // ②
}
}

上面程序中表示的①,和②处的代码没有任何区别,只是因为执行②行代码时,c集合中的元素不全是枚举值,而是包含了两个字符串对象,所以在②行代码处抛出ClassCastException异常

8.3.3 TreeSet类 2.定制排序

自然排序默认是升序排列的

TreeSet的自然排序是根据集合元素的大小, TreeSet将它们以升序排列

定制排序

如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法:

Comparator接口的compare方法 描述
int compare(T o1, T o2) Compares its two arguments for order.

该方法用于比较o1o2的大小:

  • 如果该方法返回正整数,则表明o1大于o2;
  • 如果该方法返回0,则表明o1等于o2;
  • 如果该方法返回负整数,则表明o1小于o2;

如何实现定制排序

如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑
由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。

程序 TreeSet定制排序 使用Lambda表达式

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
30
31
32
33
import java.util.*;

class M
{
int age;
public M(int age)
{
this.age = age;
}
public String toString()
{
return "M[age:" + age + "]";
}
}
public class TreeSetTest4
{
public static void main(String[] args)
{
// 此处Lambda表达式的目标类型是Comparator
TreeSet ts = new TreeSet((o1 , o2) ->
{
M m1 = (M)o1;
M m2 = (M)o2;
// 根据M对象的age属性来决定大小,age越大,M对象反而越小
return m1.age > m2.age ? -1
: m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}
}

上面程序中使用了目标类型为ComparatorLambda表达式,它负责ts集合的排序。所以当把M对象添加到ts集合中时,M类无须实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较大小,而是由与TreeSet关联的Lambda表达式来负责集合元素的排序
运行程序,看到如下运行结果:

1
[M[age:9], M[age:5], M[age:-3]]

放入TreeSet中对象的类型要相同

当通过Comparator对象(或Lambda表达式)来实现TreeSet的定制排序时,依然不可以向TreeSet中添加类型不同的对象,否则会引发ClassCastException异常。

相等的元素不会再次添加到TreeSet中

使用定制排序时, TreeSet对集合元素排序不管集合元素本身的大小,而是由Comparator对象(或Lambda表达式)负责集合元素的排序规则。 此时,TreeSet判断两个集合元素相等的标准是:通过Comparator(或Lambda表达式)比较两个元素返回了0,这样TreeSet不会把第二个元素添加到集合中。

8.3.3 TreeSet类

TreeSet是自动排序的集合

TreeSetSortedSet接口的实现类,正如SortedSet名字所暗示的, TreeSet可以确保集合元素处于排序状态

TreeSet比HashSet多出的方法

HashSet集合相比, TreeSet还提供了如下几个额外的方法。

SortedSet接口方法

SortedSet接口方法 描述
Comparator<? super E> comparator() 如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null.
E first() 返回集合中的第一个元素。
SortedSet<E> headSet(E toElement) 返回此Set的子集,由小于toElement的元素组成。
E last() 返回集合中的最后一个元素。
default Spliterator<E> spliterator() Creates a Spliterator over the elements in this sorted set.
SortedSet<E> subSet(E fromElement, E toElement) 返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)(范围前闭后开,类似String类的substring方法)。
SortedSet<E> tailSet(E fromElement) 返回此Set的子集,由大于或等于fromElement的元素组成。
方法 描述
E lower(E e) 返回集合中位于指定元素之前的元素(即集合中小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。
E higher(E e) 返回集合中位于指定元素之后的元素(即集合中大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。

小结

表面上看起来这些方法很多,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。

实例

下面程序测试了TreeSet的通用用法。

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
import java.util.*;

public class TreeSetTest
{
public static void main(String[] args)
{
TreeSet nums = new TreeSet();
// 向TreeSet中添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 输出集合元素,看到集合元素已经处于排序状态
System.out.println(nums);
// 输出集合里的第一个元素
System.out.println(nums.first()); // 输出-9
// 输出集合里的最后一个元素
System.out.println(nums.last()); // 输出10
// 返回小于4的子集,不包含4
System.out.println(nums.headSet(4)); // 输出[-9, 2]
// 返回大于5的子集,如果Set中包含5,子集中还包含5
System.out.println(nums.tailSet(5)); // 输出 [5, 10]
// 返回大于等于-3,小于4的子集。
System.out.println(nums.subSet(-3 , 4)); // 输出[2]
}
}

运行效果:

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

TreeSet排序

根据上面程序的运行结果即可看出, TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序
HashSet集合采用hash算法来决定元素的存储位置不同,

红黑树

TreeSet采用红黑树的数据结构来存储集合元素。

那么TreeSet进行排序的规则是怎样的呢? TreeSet支持两种排序方法:自然排序定制排序在默认情况下, TreeSet采用自然排序

1.自然排序

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

Comparable接口

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object object)方法:

Comparable接口方法 描述
int compareTo(T o) Compares this object with the specified object for order.

该方法返回个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如object1.compareTo(object2)

  • 如果该方法返回0,则表明这两个对象相等;
  • 如果该方法返回一个正整数,则表明object1大于object2;
  • 如果该方法返回一个负整数,则表明object1小于object2

实现了Comparable接口的常见类

Java的一些常用类已经实现了Comparable接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类。

  • BigDecimalBigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。
  • Character:按字符的unicode值进行比较。
  • Boolean:true对应的包装类实例大于false对应的包装类实例
  • String:按字符串中字符的unicode值进行比较。
  • DateTime:后面的时间、日期比前面的时间、日期大。

TreeSet中的对象必须实现Comparable接口

如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。如下程序示范了这个错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;

class Err{}
public class TreeSetErrorTest
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
// 向TreeSet集合中添加两个Err对象
ts.add(new Err());
ts.add(new Err()); //①
}
}

运行效果:

1
2
3
4
5
Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1290)
at java.util.TreeMap.put(TreeMap.java:538)
at java.util.TreeSet.add(TreeSet.java:255)
at TreeSetErrorTest.main(TreeSetErrorTest.java:10)
  • 上面程序试图向TreeSet集合中添加两个Err对象,添加第一个对象时, TreeSet里没有任何元素,所以不会出现任何问题;
  • 当添加第二个Err对象时, TreeSet就会调用该对象的compareTo()方法与集合中的其他元素进行比较。
    • 如果其对应的类没有实现Comparable接口,则会引发ClassCastException异常。因此,上面程序将会在①号代码处引发该异常。

TreeSet中的第一个元素可以不实现Comparable接口

TreeSet集合中添加元素时,只有第一个元素无须实现Comparable接口,后面添加的所有元素都必须实现Comparable接口
当然这也不是一种好做法,当试图从TreeSet中取出元素时,依然会引发ClassCastException异常。
还有一点必须指出:大部分类在实现compareTo(Object object)方法时,都需要将被比较对象object强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。

TreeSet中应该存放同一个类的对象

当试图把一个对象添加到TreeSet集合时, TreeSet会调用该对象的compareTo(Object object)方法与集合中的其他元素进行比较——这就要求集合中的其他元素与该元素是同一个类的实例。也就是说,TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException异常。如下程序示范了这个错误。

程序 TreeSet中添加不同类型的对象会出现异常

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.*;

public class TreeSetErrorTest2
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
// 向TreeSet集合中添加两个对象
ts.add(new String("疯狂Java讲义"));
ts.add(new Date()); // ①
}
}

运行效果:

1
2
3
4
5
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Date
at java.util.Date.compareTo(Date.java:131)
at java.util.TreeMap.put(TreeMap.java:568)
at java.util.TreeSet.add(TreeSet.java:255)
at TreeSetErrorTest2.main(TreeSetErrorTest2.java:10)
  • 上面程序先向TreeSet集合中添加了一个字符串对象,这个操作完全正常。
  • 当添加第二个Date对象时, TreeSet就会调用该对象的compareTo(Object object)方法与集合中的其他元素进行比较
    • Date对象的compareTo(Object object)方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常

如果向TreeSet中添加的对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,且实现compareTo(Object object)方法没有进行强制类型转换。但当试图取出TreeSet里的集合元素时,不同类型的元素依然会发生ClassCastException异常。

总结 TreeSet只能添加同一种类的对象

总结起来一句话:如果希望TreeSet能正常运作, TreeSet只能添加同一种类型的对象。
当把一个对象加入TreeSet集合中时, TreeSet调用该对象的compareTo(Object object)方法与容器中的其他对象比较大小,然后根据红黑树结构找到它的存储位置。

相等的元素不会添加到TreeSet中

如果两个对象通过compareTo(Object object)方法比较相等,新对象将无法添加到TreeSet集合中。

compareTo返回0则TreeSet就认为这两个对象相等

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object object)方法比较是否返回0:

  • 如果通过compareTo( Object object)方法比较返回0, TreeSet则会认为它们相等
  • 否则就认为它们不相等。

程序

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
30
31
32
33
34
35
36
37
import java.util.*;

class Z implements Comparable
{
int age;
public Z(int age)
{
this.age = age;
}
// 重写equals()方法,总是返回true
public boolean equals(Object object)
{
return true;
}
// 重写了compareTo(Object object)方法,总是返回1
public int compareTo(Object object)
{
return 1;
}
}
public class TreeSetTest2
{
public static void main(String[] args)
{
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
// 第二次添加同一个对象,输出true,表明添加成功
System.out.println(set.add(z1)); //①
// 下面输出set集合,将看到有两个元素
System.out.println(set);
// 修改set集合的第一个元素的age变量
((Z)(set.first())).age = 9;
// 输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age);
}
}

程序中①代码行把同一个对象再次添加到TreeSet集合中,因为z1对象的compareTo(Object object)方法总是返回1,虽然它的equals方法总是返回true,但TreeSet会认为z1对象和它自己也不相等,因此TreeSet可以添加两个z1对象。图8.5显示了TreeSet及Z对象在内存中的存储示意图
这里有一张图片
从图8.5可以看到TreeSet对象保存的两个元素的引用,实际上是同一个元素的引用(集合里放的总是引用)。所以当修改TreeSet集合里第一个元素的age变量后,该TreeSet集合里最后一个元素的age变量也随之改变了。

TreeSet中元素的规则

要确保equals方法返回true时compareTo返回0

**如果两个对象通过equals方法比较返回true时,这两个对象通过compareTo(Object object)方法比较应该返回0**。

equals方法返回false时compareTo返回0引起的问题

如果两个对象通过compareTo(Object object)方法比较返回0时,但它们通过equals方法比较返回false,这将很麻烦,因为两个对象通过compareTo(Object object)方法比较相等, TreeSet不会让第二个元素添加进去,这就会与Set集合的规则产生冲突。

TreeSet只会在添加元素时排序

如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但**TreeSet不会再次调整它们的顺序**,甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object object)方法比较返回0。

程序 TreeSet存放可变对象的情况

下面程序演示了这种情况。

展开/折叠
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.*;

class R implements Comparable
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
// 重写equals方法,根据count来判断是否相等
public boolean equals(Object object)
{
if (this == object)
{
return true;
}
if(object != null && object.getClass() == R.class)
{
R r = (R)object;
return r.count == this.count;
}
return false;
}
// 重写compareTo方法,根据count来比较大小
public int compareTo(Object object)
{
R r = (R)object;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeSetTest3
{
public static void main(String[] args)
{
TreeSet ts = new TreeSet();
ts.add(new R(5));
ts.add(new R(-3));
ts.add(new R(9));
ts.add(new R(-2));
// 打印TreeSet集合,集合元素是有序排列的
System.out.println(ts); // ①
// 取出第一个元素
R first = (R)ts.first();
// 对第一个元素的count赋值
first.count = 20;
// 取出最后一个元素
R last = (R)ts.last();
// 对最后一个元素的count赋值,与第二个元素的count相同
last.count = -2;
// 再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
System.out.println(ts); // ②
// 删除实例变量被改变的元素,删除失败
System.out.println(ts.remove(new R(-2))); // ③
System.out.println(ts);
// 删除实例变量没有被改变的元素,删除成功
System.out.println(ts.remove(new R(5))); // ④
System.out.println(ts);
}
}

上面程序中的R对象对应的类正常重写了equals()方法和compareTo()方法,这两个方法都以R对象的count实例变量作为判断的依据。
当程序执行①行代码时,看到程序输出的Set集合元素处于有序状态;
因为R类是一个可变类,因此可以改变R对象的count实例变量的值,程序后续代码行改变了该集合里第一个元素和最后一个元素的count实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。运行上面程序,看到如下所示的结果。

1
2
3
4
5
6
[R[count:-3], R[count:-2], R[count:5], R[count:9]]
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
false
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
true
[R[count:20], R[count:-2], R[count:-2]]

一旦改变了TreeSet集合里可变元素的实例变量,当再试图删除该对象时, TreeSet也会删除失败(甚至集合中原有的实例变量没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count-2的R对象时,没有任何元素被删除;
程序执行④代码时,可以看到删除了count为5的R对象,这表明:TreeSet可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象
注意
当执行了④代码后, TreeSet会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet中的所有元素了,包括那些被修改过实例变量的元素。HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的实例变量被修改时, TreeSet在处理这些对象时将非常复杂,而且容易出错

不要修改TreeSet中对象的关键实例变量值

为了让程序更加健壮,推荐不要修改放入HashSetTreeSet集合中元素的关键实例变量

8.3.2 LinkedHashSet类

LinkedHashSet有序

HashSet还有一个子类LinkedHashSet, LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
也就是说,当遍历LinkedHashSet集合里的元素时, LinkedHashSet将会按元素的添加顺序来访问集合里的元素
LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

程序 LinkedHashSet类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;

public class LinkedHashSetTest
{
public static void main(String[] args)
{
LinkedHashSet books = new LinkedHashSet();
books.add("疯狂Java讲义");
books.add("轻量级Java EE企业应用实战");
System.out.println(books);
// 删除 疯狂Java讲义
books.remove("疯狂Java讲义");
// 重新添加 疯狂Java讲义
books.add("疯狂Java讲义");
System.out.println(books);
}
}

编译、运行上面程序,看到如下输出:

1
2
[疯狂Java讲义, 轻量级Java EE企业应用实战]
[轻量级Java EE企业应用实战, 疯狂Java讲义]

LinkedHashSet是有序的集合

输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致

Set集合不可重复

虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复

8.3 Set集合

Set集合无序

前面已经介绍过Set集合,它类似于一个罐子,程序可以依次把多个对象”丢进”Set集合,而Set集合通常不能记住元素的添加顺序

Set集合不可重复

Set集合与Collection基本相同,没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同(Set不允许包含重复元素)。

Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,并且新元素不会覆盖旧元素。

上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSetTreeSetEnumSet三个实现类,只是三个实现类还各有特色。

8.3.1 HashSet类

HashSetSet接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。 HashSetHash算法来存储集合中的元素,具有很好的存取和査找性能。

HashSet特点

HashSet具有以下特点。

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
  • 集合元素值可以是null

HashSet判断两个元素相等的标准

当向HashSet集合中存入一个元素时, HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。

如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等, HashSet将会把它们存储在不同的位置,依然可以添加成功。

也就是说, HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等

程序 HashSet判断元素相同的标准

下面程序分别提供了三个类A、B和C,它们分别重写了equalshashCode()两个方法的一个或全部,通过此程序可以让读者看到HashSet判断集合元素相同的标准。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.util.*;

// 类A的equals方法总是返回true,但没有重写其hashCode()方法
class A
{
public boolean equals(Object obj)
{
return true;
}
}
// 类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
public int hashCode()
{
return 1;
}
}
// 类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
class C
{
public int hashCode()
{
return 2;
}
public boolean equals(Object obj)
{
return true;
}
}
public class HashSetTest
{
public static void main(String[] args)
{
HashSet books = new HashSet();
// 分别向books集合中添加两个A对象,两个B对象,两个C对象
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}

上面程序中向books集合中分别添加了两个A对象、两个B对象和两个C对象,其中C类重写了equals方法总是返回true, hashCode()方法总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行上面程序,看到如下运行结果:

1
[A@7852e922, B@1, B@1, C@2, A@4e25154f]

从上面程序可以看出:

  • 即使两个A对象通过equals()方法比较返回true,但HashSet依然把它们当成两个对象;
  • 即使两个B对象的hashCode()返回相同值(都是1),但HashSet依然把它们当成两个对象

HashSet中的元素要求 equals方法和hashCode方法返回值相同

这里有一个注意点:当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法
规则是:如果两个对象通过equals方法比较返回true,这两个对象的hashCode()方法的返回值也应该相同

hashCode返回值相同 equals返回值不同时会怎样

如果两个对象的hashCode()方法返回的hashCode值相同,但它们通过equals()方法比较返回false时将更麻烦:因为两个对象的hashCode值相同, HashSet将试图把它们保存在同一个位置,但是放在同一个位置是不行的,因为新的对象会覆盖旧的对象导致数据丢失,所以实际上会在这个位置用链式结构来保存多个对象;

HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上的元素具有相同的hashCode值,这需要链表上继续查找,这将会导致性能下降。

hashCode()方法对于HashSet是不是十分重要

hash算法可以快速查找被检索的对象,hash算法的价值在于速度。当需要查询集合中某个元素时,hash算法可以直接根据该元素的hashCode计算出该元素的存储位置,从而快速定位到该元素

通过索引可以计算数组元素在内存中的位置

为了理解这个概念,可以先看数组(数组是所有能存储一组元素里最快的数据结构)数组可以包含多个元素,每个元素都有索引,如果需要访问某个数组元素,只需提供该元素的索引,接下来即可根据索引计算该元素在内存里的存储位置

通过hashCode也可以计算元素的存储位置

表面上看起来, HashSet集合里的元素都没有索引,实际上,当程序向HashSet集合中添加元素时, HashSet会根据该元素的hashCode值来计算它的存储位置,这样也可快速定位到该元素

为什么不直接使用数组、还需要使用HashSet呢?

为什么不直接使用数组、还需要使用HashSet呢?因为数组元素的索引是连续的,而且数组的长度是固定的,无法自由增加数组的长度

HashSet就不一样了, HashSet采用每个元素的hashCode值来计算其存储位置,从而可以自由增加HashSet的长度,并可以根据元素的hashCode值来访问元素。

因此,当从HashSet中访问元素时, HashSet先调用该对象的hashCode方法来计算该元素的hashCode值,然后直接到该hashCode值对应的内存地址去取出该元素,这就是HashSet速度很快的原因。

HashSet的桶

HashSet中每个能存储元素的”槽位”(slot)通常称为”桶”(bucket),如果有多个元素的hashCode值相同,但它们通过equals方法比较返回false,就需要在一个”桶”里放多个元素,这样会导致性能下降

重写hashCode()方法的基本规则

前面介绍了hashCode()方法对于HashSet的重要性(实际上,对象的hashCode值对于后面的HashMap同样重要),下面给出重写hashCode()方法的基本规则。

  1. 在程序运行过程中,同一个对象多次调用hashCode()方法应该返回相同的值
  2. 两个对象通过equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值
  3. 对象中用作equals方法比较标准的实例变量,都应该用于计算hashCode

重写hashCode()方法的一般步骤

下面给出重写hashCode()方法的一般步骤。

  1. 把对象内每个参与equals方法比较标准的实例变量都计算出一个int类型的hashCode。计算方式如下表所示:
实例变量类型 计算方式
boolean变量f hashCode=(f?0:1)
整数类型(byte,short,char,int)变量f hashCode=(int) f;
long类型变量f hashCode=(int)(f^(f>>>32));
float类型变量f hashCode=Float.frontToIntBits(f);
double类型变量f long l=Double.doubleToLongBits(f); hashCode=(int)(l^(l>>>32));
引用类型变量f hashCode=f.hashCode();
  1. 用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回。例如如下代码:
    1
    return f1.hashCode()+(int)f2;
    为了避免直接相加产生偶然相等(两个对象的f1f2实例变量并不相等,但它们的hash Code的和恰好相等),可以通过为各实例变量的hashCode值各自乘以任意一个质数后再相加。例如如下代码:
    1
    return f1.hashCode() * 19 +(int) f2 *31;

HashSet中添加可变对象出现的问题

如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。

下面程序演示了这种情况。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.*;

class R
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
R r = (R)obj;
return this.count == r.count;
}
return false;
}
public int hashCode()
{
return this.count;
}
}
public class HashSetTest2
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
// 打印HashSet集合,集合元素没有重复
System.out.println(hs);
// 取出第一个元素
Iterator it = hs.iterator();
R first = (R)it.next();
// 为第一个元素的count实例变量赋值
first.count = -3; // ①
// 再次输出HashSet集合,集合元素有重复元素
System.out.println(hs);
// 删除count为-3的R对象
hs.remove(new R(-3)); // ②
// 可以看到被删除了一个R元素
System.out.println(hs);
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3))); // 输出false
System.out.println("hs是否包含count为-2的R对象?"
+ hs.contains(new R(-2))); // 输出false
}
}

上面程序中提供了R类,R类重写了equals(Object object)方法和hashCode()方法,这两个方法都是根据R对象的count实例变量来判断的。

上面程序的①号代码处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。

程序运行结果如下所示:

1
2
3
4
5
[R[count:-2], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:5], R[count:9]]
hs是否包含count为-3的R对象?false
hs是否包含count为-2的R对象?false

正如上面运行结果中所见到的,HashSet集合中的第1个元素和第2个元素完全相同,这表明两个元素已经重复

此时HashSet会比较混乱:当试图删除count-3的R对象时, HashSet会计算出该对象的hashCode值,从而找出该对象在集合中的保存位置,然后把此处的对象与count-3的R对象通过equals方法进行比较,如果相等则删除该对象。

HashSet只有第2个元素才满足该条件(第1个元素实际上保存在count-2的R对象对应的位置),所以第2个元素被删除

至于第一个count-3的R对象,它保存在count-2的R对象对应的位置,但使用equals方法拿它和count-2的R对象比较时又返回false
这将导致HashSet不可能准确访问该元素。

元素放到HashSet后,不要修改参与计算hashCode和equals的实例变量的值

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

注意:
当向HashSet中添加可变对象时,必须十分小心。如果修改HashSet集合中的对象,有可能导致该对象与集合中的其他对象相等,从而导致HashSet无法准确访问该对象。

8.2.6 使用Java 8新增的Stream操作集合

Java 8还新增了StreamIntStreamLongStreamDoubleStream等流式API,这些API代表多个支持串行并行聚集操作的元素。上面4个接口中, Stream是一个通用的流接口,而IntStreamLongStreamDoubleStream则代表元素类型为intlongdouble的流。

如何创建类型为int long double的Stream

Java 8为上面每个流式API提供了对应的Builder,例如Stream.BuilderIntStream.BuilderLongStream.BuilderDoubleStream.Builder,开发者可以通过这些Builder来创建对应的流

独立使用Stream的步骤

独立使用Stream的步骤如下:

  1. 使用StreamXxxStreambuilder()类方法创建该Stream对应的Builder
  2. 重复调用Builderadd()方法向该流中添加多个元素。
  3. 调用Builderbuild()方法获取对应的Stream
  4. 调用Stream的聚集方法。

在上面4个步骤中,第4步可以根据具体需求来调用不同的方法, Stream提供了大量的聚集方法供用户调用,具体可参考StreamXxxStreamAPI文档。

每个Stream只能执行一次

对于大部分聚集方法而言,每个Stream只能执行一次

程序 IntStream使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

import java.util.stream.*;

public class IntStreamTest {
public static void main(String[] args) {
IntStream intStream = IntStream.builder().add(20).add(13).add(-2).add(18).build();
// 下面调用聚集方法的代码每次只能执行一个
// System.out.println("流中的最大值:" + intStream.max().getAsInt());
// System.out.println("流中的最小值:" + intStream.min().getAsInt());
// System.out.println("流中所有元素的总和:" + intStream.sum());
// System.out.println("流中所有元素的总数:" + intStream.count());
// System.out.println("流中所有元素的平均值:" + intStream.average());
// System.out.println("流中所有元素的平方是否都大于20:" + intStream.allMatch(ele -> ele * ele > 20));
// System.out.println("流中是否包含任何元素的平方大于20:" + intStream.anyMatch(ele -> ele * ele > 20));

// 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
IntStream newIs = intStream.map(ele -> ele * 2 + 1);
// 使用方法引用的方式来遍历集合元素
newIs.forEach(System.out::println); // 输出41 27 -3 37
}
}

上面程序先创建了一个IntStream,接下来分别多次调用IntStream的聚集方法执行操作,这样即可获取该流的相关信息。注意:IntStream的方法每次只能执行一个,因此需要把其他代码注释掉。

中间方法和末端方法

Stream提供了大量的方法进行聚集操作,这些方法既可以是”中间的”(intermediate),也可以是”末端的”(terminal)。

  • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流
  • 末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被”消耗”且不再可用。上面程序中的sum()count()average()等方法都是末端方法。

流的方法有什么特征

除此之外,关于流的方法还有如下两个特征。

  • 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量,保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
  • 短路方法:短路方法可以尽早结束对流的操作,不必检査所有的元素。

Stream常用的中间方法

下面简单介绍一下Stream常用的中间方法。

Stream中间方法 描述
Stream<T> filter(Predicate<? super T> predicate) 过滤Stream中所有不符合predicate的元素。
IntStream mapToInt(ToIntFunction<? super T> mapper) 使用ToIntFunction对流中的元素执行一对一的转换,该法返回的新流中包含了ToIntFunction转换生成的所有元素。
LongStream mapToLong(ToLongFunction<? super T> mapper) Returns a LongStream consisting of the results of applying the given function to the elements of this stream.
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper) Returns a DoubleStream consisting of the results of applying the given function to the elements of this stream.
Stream<T> peek(Consumer<? super T> action) 依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的素。该方法主要用于调试.
Stream<T> distinct() 该方法用于排序流中所有重复的元素(判断元素重复的标准是使 equals()比较返回true )这是一个有状态的方法
Stream<T> sorted() 该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
Stream<T> sorted(Comparator<? super T> comparator) Returns a stream consisting of the elements of this stream, sorted according to the provided
Stream<T> limit(long maxSize) 该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。

Stream常用的末端方法

下面简单介绍一下Stream常用的末端方法。

方法 描述
void forEach(Consumer<? super T> action) 遍历流中所有元素,对每个元素执行action

将流转成数组

方法 描述
Object[] toArray() 将流中所有元素转换为一个数组。
<A> A[] toArray(IntFunction<A[]> generator) Returns an array containing the elements of this stream, using the provided generator function to allocate the returned array, as well as any additional arrays that might be required for a partitioned execution or for resizing.

合并流中的元素:reduce方法

该方法有三个重载的版本,都用于通过某种操作来合并流中的元素

方法 描述
Optional<T> reduce(BinaryOperator<T> accumulator) Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any.
T reduce(T identity, BinaryOperator<T> accumulator) Performs a reduction on the elements of this stream, using the provided identity value and an associative accumulation function, and returns the reduced value.
<U> U reduce(U identity, BiFunction<U,​? super T,​U> accumulator, BinaryOperator<U> combiner) Performs a reduction on the elements of this stream, using the provided identity, accumulation and combining functions.

求最大值 最小值 统计流中元素数量

方法 描述
Optional<T> max(Comparator<? super T> comparator) 返回流中所有元素的最大值
Optional<T> min(Comparator<? super T> comparator) 返回流中所有元素的最小值
long count() 返回流中所有元素的数量。

判断流中的元素是否符合Predicate条件

方法 描述
boolean allMatch(Predicate<? super T> predicate) 判断流中是否每个元素都符合Predicate条件。
boolean anyMatch(Predicate<? super T> predicate) 判断流中是否至少包含一个元素符合Predicate条件。
boolean noneMatch(Predicate<? super T> predicate) 判断流中是否所有元素都不符合Predicate条件

返回流中的元素

方法 描述
Optional<T> findFirst() 返回流中的第一个元素。
Optional<T> findAny() 返回流中的任意一个元素。

获取集合对应的流

Java 8允许使用流式API来操作集合, Collection接口提供了一个stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。

Collection接口的stream方法 描述
default Stream<E> stream() Returns a sequential Stream with this collection as its source.

程序 使用流操作集合

例如,对于8.2.5节介绍的示例程序,该程序需要额外定义一个calAll()方法来遍历集合元素,然后依次对每个集合元素进行判断——这太麻烦了。如果使用Stream,那么就直接对集合中所有元素进行批量操作
下面使用Stream来改写这个程序。

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
30
import java.util.Collection;
import java.util.HashSet;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class CollectionStream {
public static void main(String[] args) {
// 创建books集合、为books集合添加元素的代码与8.2.5小节的程序相同。
Collection<String> collection = new HashSet<>();
collection.add("C");
collection.add("C++");
collection.add("Java");
collection.add("JavaScirpt");
collection.add("Python");
// 先获取包含字母C的流,
Stream<String> strContainsC = collection.stream().filter(str -> str.contains("C"));
// 输出流中元素的长度
System.out.println("包含字符C的元素个数:" + strContainsC.count());
// 简写形式:
System.out.println("以字符J开头的元素个数:" + collection.stream().filter(str -> str.startsWith("J")).count());
System.out.println("---------------------------");
collection.forEach(str -> System.out.print(str + "\t"));
System.out.println();
// 先调用Collection对象的stream()方法将集合转换为Stream,
// 再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
IntStream intStream = collection.stream().mapToInt(str -> str.length());
// 调用forEach()方法遍历IntStream中每个元素,注意没有顺序
intStream.forEach(intv -> System.out.print(intv + "\t"));
}
}

运行效果:

1
2
3
4
5
包含字符C的元素个数:2
以字符J开头的元素个数:2
---------------------------
Java C++ C JavaScirpt Python
4 3 1 10 6

从上面程序中的代码可以看出,

程序只要调用Collectionstream()方法即可返回该集合对应的Stream,接下来就可通过Stream提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码
上面程序中最后一段代码:

1
2
3
IntStream intStream = collection.stream().mapToInt(str -> str.length());
// 调用forEach()方法遍历IntStream中每个元素,注意没有顺序
intStream.forEach(intv -> System.out.print(intv + "\t"));

先调用Collection对象的stream()方法将集合转换为Stream对象,然后调用Stream对象的mapToInt()方法将其转换为IntStream——这个mapToInt()方法就是一个中间方法,因此程序可继续调用IntStreamforEach()方法来遍历流中的元素。

8.2.5 使用Java8新增的Predicate操作集合

谓词:在计算机语言的环境下,谓词是指条件表达式的求值返回真或假的过程
谓词是返回真/假(即布尔)值的函数
在Java 8中, 谓词是一个功能接口,它接受参数并返回布尔值
谓词的一般含义是对正确或错误的陈述。 在编程中,谓词表示返回布尔值的单个参数函数

使用removeIf方法 批量删除 集合中的元素

Java 8Collection集合新增了一个removeIf(Predicate filter)方法:

Collection接口的removeIf方法 描述
default boolean removeIf(Predicate<? super E> filter) Removes all of the elements of this collection that satisfy the given predicate.

该方法将会批量删除符合filter条件的所有元素。

Predicate是函数式接口

该方法需要一个Predicate(谓词)对象作为参数, Predicate也是函数式接口,因此可使用Lambda表达式作为参数。

实例 使用removeIf批量删除集合元素

如下程序示范了使用Predicate来过滤集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Collection;
import java.util.HashSet;

public class PredicateTest {
public static void main(String[] args) {
// 创建一个集合
Collection<String> collection = new HashSet<>();
collection.add("1");
collection.add("2");
collection.add("3");
collection.add("4");
collection.add("5");
System.out.println(collection);
System.out.println("------------------------");
// removeIf方法使用Lambda表达式删除满足Lambda表达式的集合元素
collection.removeIf(str -> Integer.valueOf(str) > 3);
System.out.println(collection);
}
}
1
2
3
[1, 2, 3, 4, 5]
------------------------
[1, 2, 3]

上面程序中调用了Collection集合的removeIf()方法批量删除集合中符合条件的元素,程序传入一个Lambda表达式作为过滤条件

函数式接口Predicate的抽象方法 test

Predicate的抽象方法 描述
boolean test(T t) Evaluates this predicate on the given argument.

Predicatetest方法可以判断一个对象是否满足谓词中给定的条件.

使用Predicate类的test方法统计集合元素

可以创建一个countCollectionByPredicate方法,第一个参数是Collection,第二个参数是Predicate,
然后就可以在方法中遍历集合元素时,计算Predicate对象.test(集合元素)方法返回true的次数,从而实现对集合元素的统计.

实例Predicate统计集合元素

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
30
31
32
33
34
35
36
37
38
import java.util.*;
import java.util.function.*;

public class PredicateTest2 {
public static void main(String[] args) {
// 创建books集合、为books集合添加元素的代码与前一个程序相同。
Collection<String> collection = new HashSet<>();
collection.add("HelloWorld");
collection.add("C");
collection.add("C++");
collection.add("Java");
collection.add("JavaScirpt");
System.out.println(collection);
System.out.println("----------------------------");
System.out.println("包含字符C的语言个数:" + countCollectionByPredicate(collection, str -> str.contains("C")));
System.out.println("以字符J开头的语言个数:" + countCollectionByPredicate(collection, str -> str.startsWith("J")));
System.out.println("大写字母开头的语言个数:"
+ countCollectionByPredicate(collection, str -> str.charAt(0) <= 'Z' && str.charAt(0) >= 'A'));
}

/**
* 统计集合中符合给定谓词的元素的个数.
*
* @param collection 要统计的集合.
* @param predicate 谓词.
* @return 集合中满足给定谓词的元素的个数.
*/
public static int countCollectionByPredicate(Collection<String> collection, Predicate<String> predicate) {
int total = 0;
for (String str : collection) {
// 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
if (predicate.test(str)) {
total++;
}
}
return total;
}
}

运行效果如下:

1
2
3
4
5
[Java, C++, C, JavaScirpt, HelloWorld]
----------------------------
包含字符C的语言个数:2
以字符J开头的语言个数:2
大写字母开头的语言个数:5

8.2.4 使用foreach循环遍历集合元素

除了可以使用Iterator接口迭代访问Collection集合里的元素之外,使用Java 5提供的foreach循环迭代访问集合元素更加便捷。

JDK 1.5foreach循环需要注意的地方

不要修改foreach循环中的迭代变量的值

与使用Iterator接口迭代访问集合元素类似的是, foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此foreach循环中修改迭代变量的值也没有任何实际意义.

迭代集合时集合不能改变

同样,当使用foreach循环迭代访问集合元素时,该集合也不能被改变,否则将引发ConcurrentModificationException异常。

程序 使用foreach循环 遍历Collection集合

如下程序示范了使用foreach循环来迭代访问集合元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class ForeachTest {
public static void main(String[] args) {
// 创建集合、添加元素的代码与前一个程序相同
Collection<String> collection = new HashSet<String>();
collection.add("1");
collection.add("2");
collection.add("3");
collection.add("4");
for (String str : collection) {
System.out.println(str);
if (str.equals("2")) {
// str只是一个副本,对forEach循环的迭代变量赋值没有意义
str = "NewStr";
}
if (str.equals("3")) {
// 遍历集合时不可修改集合元素
// collection.remove(str);
}
}
System.out.println(collection);
}
}
1
2
3
4
5
1
2
3
4
[1, 2, 3, 4]

取消collection.remove(str);这行代码前面的注释.再次运行:

1
2
3
4
5
6
7
1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1493)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1516)
at ForeachTest.main(ForeachTest.java:11)

上面代码使用foreach循环来迭代访问Collection集合里的元素更加简洁,这正是JDK 1.5foreach循环带来的优势。