8.3.5 各Set实现类的性能分析
8.3.5 各Set实现类的性能分析
优先使用HashSet
HashSet和TreeSet是set的两个典型实现,到底如何选择HashSet和TreeSet呢?HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。
需要排序时使用TreeSet
只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
LinkedHashSet有序
HashSet还有一个子类: LinkedHashSet,对于普通的插入、删除操作, LinkedHashSet比HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。
EnumSet用来保存枚举
EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
HashSet TreeSet EnumSet都是线程不安全的
必须指出的是,Set的三个实现类HashSet、 TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。
如何线程安全:使用Collections工具类包装Set集合
通常可以通过Collections工具类的synchronizedSortedSet方法来”包装”该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。例如:SortedSet s= Collections.synchronizedSortedSet(new TreeSet(...));
关于Collections工具类的更进一步用法,可以参考8.8节的内容。
8.3.4 EnumSet类
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 | import java.util.*; |
运行效果:
1 | [SPRING, SUMMER, FALL, WINTER] |
通过复制集合中的元素来创建EnumSet
除此之外,还可以复制另一个EnumSet集合中的所有元素来创建新的EnumSet集合,或者复制另一个Collection集合中的所有元素来创建新的EnumSet集合。当复制Collection集合中的所有元素来创建新的EnumSet集合时,要求Collection集合中的所有元素必须是同一个枚举类的枚举值。
复制Collection来创建EnumSet的前提
当试图复制一个Collection集合里的元素来创建EnumSet集合时,必须保证Collection集合里的所有元素都是同一个枚举类的枚举值
程序 复制集合中的元素来创建EnumSet
下面程序示范了这个用法。
1 | import java.util.*; |
上面程序中表示的①,和②处的代码没有任何区别,只是因为执行②行代码时,c集合中的元素不全是枚举值,而是包含了两个字符串对象,所以在②行代码处抛出ClassCastException异常
8.3.3 TreeSet类 2.定制排序
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. |
该方法用于比较o1和o2的大小:
- 如果该方法返回正整数,则表明
o1大于o2; - 如果该方法返回0,则表明
o1等于o2; - 如果该方法返回负整数,则表明
o1小于o2;
如何实现定制排序
如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。
由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。
程序 TreeSet定制排序 使用Lambda表达式
1 | import java.util.*; |
上面程序中使用了目标类型为Comparator的Lambda表达式,它负责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类 1.自然排序
8.3.3 TreeSet类
TreeSet是自动排序的集合
TreeSet是SortedSet接口的实现类,正如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的元素组成。 |
NavigableSet接口方法
| 方法 | 描述 |
|---|---|
E lower(E e) |
返回集合中位于指定元素之前的元素(即集合中小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。 |
E higher(E e) |
返回集合中位于指定元素之后的元素(即集合中大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。 |
小结
表面上看起来这些方法很多,其实它们很简单:因为TreeSet中的元素是有序的,所以增加了访问第一个、前一个、后一个、最后一个元素的方法,并提供了三个从TreeSet中截取子TreeSet的方法。
实例
下面程序测试了TreeSet的通用用法。
1 | import java.util.*; |
运行效果:
1 | [-9, 2, 5, 10] |
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接口的常用类。
BigDecimal、BigInteger以及所有的数值型对应的包装类:按它们对应的数值大小进行比较。Character:按字符的unicode值进行比较。Boolean:true对应的包装类实例大于false对应的包装类实例String:按字符串中字符的unicode值进行比较。Date、Time:后面的时间、日期比前面的时间、日期大。
TreeSet中的对象必须实现Comparable接口
如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。如下程序示范了这个错误。
1 | import java.util.*; |
运行效果:
1 | Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable |
- 上面程序试图向
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 | import java.util.*; |
运行效果:
1 | Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Date |
- 上面程序先向
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 | import java.util.*; |
程序中①代码行把同一个对象再次添加到 这里有一张图片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 | import java.util.*; |
上面程序中的R对象对应的类正常重写了equals()方法和compareTo()方法,这两个方法都以R对象的count实例变量作为判断的依据。
当程序执行①行代码时,看到程序输出的Set集合元素处于有序状态;
因为R类是一个可变类,因此可以改变R对象的count实例变量的值,程序后续代码行改变了该集合里第一个元素和最后一个元素的count实例变量的值。当程序执行②行代码输出时,将看到该集合处于无序状态,而且集合中包含了重复元素。运行上面程序,看到如下所示的结果。
1 | [R[count:-3], R[count:-2], R[count:5], R[count:9]] |
一旦改变了TreeSet集合里可变元素的实例变量,当再试图删除该对象时, TreeSet也会删除失败(甚至集合中原有的实例变量没被修改但与修改后元素相等的元素也无法删除),所以在上面程序的③代码处,删除count为-2的R对象时,没有任何元素被删除;
程序执行④代码时,可以看到删除了count为5的R对象,这表明:TreeSet可以删除没有被修改实例变量、且不与其他被修改实例变量的对象重复的对象。
注意
当执行了④代码后, TreeSet会对集合中的元素重新索引(不是重新排序),接下来就可以删除TreeSet中的所有元素了,包括那些被修改过实例变量的元素。与HashSet类似的是,如果TreeSet中包含了可变对象,当可变对象的实例变量被修改时, TreeSet在处理这些对象时将非常复杂,而且容易出错。
不要修改TreeSet中对象的关键实例变量值
为了让程序更加健壮,推荐不要修改放入HashSet和TreeSet集合中元素的关键实例变量
8.3.2 LinkedHashSet类
8.3.2 LinkedHashSet类
LinkedHashSet有序
HashSet还有一个子类LinkedHashSet, LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
也就是说,当遍历LinkedHashSet集合里的元素时, LinkedHashSet将会按元素的添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。
程序 LinkedHashSet类
1 | import java.util.*; |
编译、运行上面程序,看到如下输出:
1 | [疯狂Java讲义, 轻量级Java EE企业应用实战] |
LinkedHashSet是有序的集合
输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致。
Set集合不可重复
虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复
8.3 Set集合 8.3.1 HashSet类
8.3 Set集合
Set集合无序
前面已经介绍过Set集合,它类似于一个罐子,程序可以依次把多个对象”丢进”Set集合,而Set集合通常不能记住元素的添加顺序。
Set集合不可重复
Set集合与Collection基本相同,没有提供任何额外的方法。实际上Set就是Collection,只是行为略有不同(Set不允许包含重复元素)。
Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,并且新元素不会覆盖旧元素。
上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、 TreeSet和EnumSet三个实现类,只是三个实现类还各有特色。
8.3.1 HashSet类
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。 HashSet按Hash算法来存储集合中的元素,具有很好的存取和査找性能。
HashSet特点
HashSet具有以下特点。
- 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。- 集合元素值可以是
null
HashSet判断两个元素相等的标准
当向HashSet集合中存入一个元素时, HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。
如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等, HashSet将会把它们存储在不同的位置,依然可以添加成功。
也就是说, HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。
程序 HashSet判断元素相同的标准
下面程序分别提供了三个类A、B和C,它们分别重写了equals、 hashCode()两个方法的一个或全部,通过此程序可以让读者看到HashSet判断集合元素相同的标准。
1 | import java.util.*; |
上面程序中向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()方法的基本规则。
- 在程序运行过程中,同一个对象多次调用
hashCode()方法应该返回相同的值。 - 当两个对象通过
equals()方法比较返回true时,这两个对象的hashCode()方法应返回相等的值。 - 对象中用作
equals方法比较标准的实例变量,都应该用于计算hashCode值。
重写hashCode()方法的一般步骤
下面给出重写hashCode()方法的一般步骤。
- 把对象内每个参与
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步计算出来的多个
hashCode值组合计算出一个hashCode值返回。例如如下代码:为了避免直接相加产生偶然相等(两个对象的1
return f1.hashCode()+(int)f2;f1、f2实例变量并不相等,但它们的hash Code的和恰好相等),可以通过为各实例变量的hashCode值各自乘以任意一个质数后再相加。例如如下代码:1
return f1.hashCode() * 19 +(int) f2 *31;
HashSet中添加可变对象出现的问题
如果向HashSet中添加一个可变对象后,后面程序修改了该可变对象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象通过equals方法比较返回true,两个对象的hashCode值也相等),这就有可能导致HashSet中包含两个相同的对象。
下面程序演示了这种情况。
1 | import java.util.*; |
上面程序中提供了R类,R类重写了equals(Object object)方法和hashCode()方法,这两个方法都是根据R对象的count实例变量来判断的。
上面程序的①号代码处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。
程序运行结果如下所示:
1 | [R[count:-2], R[count:-3], R[count:5], R[count:9]] |
正如上面运行结果中所见到的,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 使用Java8新增的Stream操作集合
8.2.6 使用Java 8新增的Stream操作集合
Java 8还新增了Stream、 IntStream、 LongStream、 DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中, Stream是一个通用的流接口,而IntStream、 LongStream、DoubleStream则代表元素类型为int、long、 double的流。
如何创建类型为int long double的Stream
Java 8为上面每个流式API提供了对应的Builder,例如Stream.Builder、 IntStream.Builder、LongStream.Builder、 DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤
独立使用Stream的步骤如下:
- 使用
Stream或XxxStream的builder()类方法创建该Stream对应的Builder。 - 重复调用
Builder的add()方法向该流中添加多个元素。 - 调用
Builder的build()方法获取对应的Stream - 调用
Stream的聚集方法。
在上面4个步骤中,第4步可以根据具体需求来调用不同的方法, Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。
每个Stream只能执行一次
对于大部分聚集方法而言,每个Stream只能执行一次。
程序 IntStream使用示例
1 | |
上面程序先创建了一个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 | import java.util.Collection; |
运行效果:
1 | 包含字符C的元素个数:2 |
从上面程序中的代码可以看出,
程序只要调用Collection的stream()方法即可返回该集合对应的Stream,接下来就可通过Stream提供的方法对所有集合元素进行处理,这样大大地简化了集合编程的代码。
上面程序中最后一段代码:
1 | IntStream intStream = collection.stream().mapToInt(str -> str.length()); |
先调用Collection对象的stream()方法将集合转换为Stream对象,然后调用Stream对象的mapToInt()方法将其转换为IntStream——这个mapToInt()方法就是一个中间方法,因此程序可继续调用IntStream的forEach()方法来遍历流中的元素。
8.2.5 使用Java8新增的Predicate操作集合
8.2.5 使用Java8新增的Predicate操作集合
谓词:在计算机语言的环境下,谓词是指条件表达式的求值返回真或假的过程。
谓词是返回真/假(即布尔)值的函数
在Java 8中, 谓词是一个功能接口,它接受参数并返回布尔值。
谓词的一般含义是对正确或错误的陈述。 在编程中,谓词表示返回布尔值的单个参数函数。
使用removeIf方法 批量删除 集合中的元素
Java 8为Collection集合新增了一个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 | import java.util.Collection; |
1 | [1, 2, 3, 4, 5] |
上面程序中调用了Collection集合的removeIf()方法批量删除集合中符合条件的元素,程序传入一个Lambda表达式作为过滤条件
函数式接口Predicate的抽象方法 test
| Predicate的抽象方法 | 描述 |
|---|---|
boolean test(T t) |
Evaluates this predicate on the given argument. |
Predicate的test方法可以判断一个对象是否满足谓词中给定的条件.
使用Predicate类的test方法统计集合元素
可以创建一个countCollectionByPredicate方法,第一个参数是Collection,第二个参数是Predicate,
然后就可以在方法中遍历集合元素时,计算Predicate对象.test(集合元素)方法返回true的次数,从而实现对集合元素的统计.
实例Predicate统计集合元素
1 | import java.util.*; |
运行效果如下:
1 | [Java, C++, C, JavaScirpt, HelloWorld] |
8.2.4 使用foreach循环遍历集合元素
8.2.4 使用foreach循环遍历集合元素
除了可以使用Iterator接口迭代访问Collection集合里的元素之外,使用Java 5提供的foreach循环迭代访问集合元素更加便捷。
JDK 1.5的foreach循环需要注意的地方
不要修改foreach循环中的迭代变量的值
与使用Iterator接口迭代访问集合元素类似的是, foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素的值赋给迭代变量,因此在foreach循环中修改迭代变量的值也没有任何实际意义.
迭代集合时集合不能改变
同样,当使用foreach循环迭代访问集合元素时,该集合也不能被改变,否则将引发ConcurrentModificationException异常。
程序 使用foreach循环 遍历Collection集合
如下程序示范了使用foreach循环来迭代访问集合元素。
1 | import java.util.*; |
1 | 1 |
取消collection.remove(str);这行代码前面的注释.再次运行:
1 | 1 |
上面代码使用foreach循环来迭代访问Collection集合里的元素更加简洁,这正是JDK 1.5的foreach循环带来的优势。