Java核心技术-集合
在实现方法时,选择不同的数据结构会导致其实现风格以及性能存在着很大的差异,例如:
需要快速地搜索成千上万个有序的数据项吗?需要快速地在有序的序列中插入和删除元素吗?需要建立键与值之间的关联吗?
1 Java集合框架
1.1 将集合的接口与实现分离
与现代的数据结构类库的常见情况一样,Java集合类库也将接口与实现分离。
首先看一下我们熟悉的数据结构——队列是如何分离的:
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列。
队列接口的最简形式如下:
public interface Queue<E> { void add(E element); E remove(); int size(); }
队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表
每一个实现都可以通过一个实现了Queue接口的类表示:
public class CircularArrayQueue<E> implements Queue<E> { private int head; private int tail; CircularArrayQueue(int capacity){...} public void add(E element){...} public E remove(){...} public int size(){...} private E[] elements; }
public class LinkedListQueue<E> implements Queue<E> { private Link head; private Link tail; LinkedListQueue(){...} public void add(E element){...} public E remove(){...} public int size(){...} }
使用接口类型存放集合的引用,利用这种方式,可以轻松的使用另一种不同的实现。
Queue<Customer> expressLane=new CircularArrayQueue<>(100); expressLane.add(new Customer("Harry"));
为什么选择这种实现而不选择那种实现? 接口本身并不能说明哪种实现的效率究竟如何。
循环数组要比链表更高效,因此大多数人优先选择循环数组。但是,循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。
在API文档中,有一组名字以Abstract开头的类,如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口轻松得多。
1.2 Collection接口
在Java类库中,集合类的基本接口是Collection接口:
public interface Collection<E> { boolean add(E element); Iterator<E> iterator(); ... }
add方法用于向集合中添加元素,如果这个元素在集中已经存在,这个添加请求就没有实效,因为集中不允许有重复的元素。
iterator方法返回了一个可以依次访问集合中元素的迭代器对象
1.3 迭代器
Iterator接口包含4个方法:
public interface Iterator<E> { E next(); boolean hasNext(); void remove(); default void forEachRemaining(Consumer<? super E> action) }
编译器简单的将"foreach“循环翻译为带有迭代器的循环
"foreach“循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个抽象方法:
public interface Iterable<E> { Iterator<E> iterator; ... }
Collection接口扩展了Iterable接口。因此,对于标准库中的任何集合都可以使用”foreach"循环。
Java SE 8中可以调用forEachRemaining方法并提供一个lambda表达式代替循环:
iterator.forEachRemaining(element->do something with element);
应该将Java迭代器认为是位于两个元素之间,当调用next时,迭代器就越过下一个元素,并返回刚刚越过的元素的引用。
next方法和remove方法的调用具有相互依赖性,如果调用remove之前没有调用next将是不合法的。
1.4 泛型实用方法
由于Collection和Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。
当然,实现Collection接口的每一个类都要提供接口中如此之多的方法是一件很烦人的事情,为了能够让实现着更容易地实现这个接口,Java类库提供了一个类AbstractCollection,它将基础的size和iterator抽象化了,但是提供了例行方法contains
对于Java SE 8,这种方法有些过时了。如果这些方法是Collection接口的默认实现会更好,其中还有一个很有用的方法:
default boolean removeIf(Predicate<? super E> filter)
这个方法用于删除满足某个条件的元素。
1.5 集合框架中的接口
集合有两个基本接口:Collection和Map
List是一个有序集合,可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后一种称为随机访问,因为可以按任意顺序访问元素。与之不同,使用迭代器访问时只能顺序的访问元素。
ListIterator接口是Iterator的一个子接口。它定义了一个方法用于在迭代器位置前面添加一个元素。
RandomAccess接口用来测试一个特定的集合是否支持高效的随机访问。
由数组支持的有序集合可以快速地随机访问(使用整数索引),链表尽管也是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。
Set接口等同于Collection接口,不过方法更加严谨:
add方法不允许添加重复的元素
equals方法只要两个集中包含同样元素就相等,不要求顺序相同
hashCode方法要保证包含相同元素的两个集会得到相同的散列码
既然方法签名是一样的,为什么还要建立一个单独的接口呢?
从概念上讲,并不是所有集合都是集。
2 具体的集合
除了以Map结尾的类之外,其他类都实现了Collection接口,而以Map结尾的类实现了Map接口。
2.1 链表
数组和数组列表都有一个重大缺陷——从数组中间位置插入、删除一个元素要付出很大的代价。
通过链表可以解决这个问题,在Java程序设计语言中,所有链表实际上都是双向链接的
链表是一个有序集合,LinkedList.add方法将对象添加到尾部,如果需要依赖于位置的add方法可以使用ListIterator(Iterator不含add方法)
interface ListIterator<E> extends Iterator<E> { void add(E element); ... }
与Collection.add不同,这个方法不返回boolean,它假定添加操作总会改变链表
另外,ListIterator接口有两个方法,可以用来反向遍历链表
E previous()
boolean hasPrevious()
下面代码将越过第一个元素,在第二个元素之前添加一个6
List<Integer> staff=new LinkedList<>(); staff.add(1); staff.add(2); staff.add(3); staff.add(1); ListIterator<Integer> iter=staff.listIterator(); Integer s=iter.next(); iter.add(6);
add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态
不能连续两次调用remove
如果一个集合有两个迭代器,并且一个迭代器发现它的集合被另一个迭代器修改了,或者是被集合自身的方法修改了,就会抛出ConcurrentModificationException异常。
为避免发生并发修改的异常,可以根据需要给容器附加多个读迭代器,再单独附加一个既能读又能写的迭代器。
可以为每个迭代器维护一个独立的跟踪改写操作的计数值来检测并发修改的问题。
列表中元素较少时,优先使用ArrayList
2.2 数组列表
ArrayList封装了一个动态再分配的对象数组。
在不需要同步时使用ArrayList,而不要使用Vector(Vector类的所有方法都是同步的,要在同步操作上耗费大量的时间)
2.3 散列集
链表和数组可以按照人们的意愿排列元素的次序,但如果想要查看某个元素却忘记了它的位置,就要访问所有的元素,直到找到为止
有几种能够快速找到元素的数据结构,其缺点是无法控制元素出现的次序,将按照有利于其操作目的的原则组织数据
散列表可以快速的查找所需要的对象。
散列表根据对象的实例域产生一个整数,称为散列码,不同数据域的对象产生不同的散列码
在Java中,散列表用链表数组实现,每个列表被称为桶。
要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。
有时候会遇到桶被占的情况,这种现象被称为散列冲突,这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。
Java SE 8中,桶满时会从链表变成平衡二叉树——如果散列函数不当或者有恶意代码试图在散列表中填充多个相同散列码的值,这样就能提高性能
如果想更多地控制散列表的性能,就要指定一个初始的桶数,通常,将桶数设置为预计元素个数的75%-150%
有可能最初的估计过低导致散列表太满,就要再散列。装填因子决定何时对散列表进行再散列(默认0.75)
散列表可以用于实现几个重要的数据结构,其中最简单的是set类型,set是没有重复元素的元素集合,set的add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去
HashSet类是基于散列表的集,迭代器将依次访问所有的桶。只有不关心集合中元素的顺序时才应该使用HashSet
2.4 树集
TreeSet与散列集十分类似,不过有所改进。树集是一个有序集合,可以以任意顺序(默认是字典排序)将元素插入到集合中,在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。
正如TreeSet类名所示,排序是通过红黑树实现的
将一个元素添加到树中要比添加到散列表中慢
要使用树集,必须能够比较元素。这些元素必须实现Comparable接口,或者构造集时提供一个Comparator
使用树集还是散列集要根据具体情况来定:
如果不需要对数据进行排序,就没有必要付出排序的开销,更重要的是,对于某些数据来说,对其排序要比散列更加困难。
2.5 队列与双端队列
有两个端头的队列称为双端队列,可以让人们有效地在头部和尾部同时添加或删除元素,不支持在队列中间添加元素。
Java SE 6中引入了Deque接口,并由ArrayDeque和LinkedList类实现,这两个类都提供了双端队列,而且必要时可以增加队列的长度。
2.6 优先级队列
优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索,即无论何时调用remove方法,总是获得当前优先级队列中最小的元素。
特性:迭代并不是按照元素的顺序访问,而删除却总是删掉剩余元素中优先级最小的元素。
优先级队列使用堆作为数据结构,堆是一个可以自我调整的二叉树,可以让最小的元素移动到根,而不必花费时间对元素进行排序
一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供Comparator对象
使用优先级队列的典型示例是任务调度
3 映射
集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有要查找元素的精确副本
映射数据结构就是为此设计的,映射用来存放键/值对
3.1 基本映射操作
Java类库为映射提供了两个通用的实现:HashMap和TreeMap
散列映射对健进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列和比较函数只能用于键
选择散列映射还是树映射?
与集一样,散列稍微快一些,如果不需要按照排序顺序访问键,就最好选择散列
散列映射使用put、get方法添加、获得键值对
如果在映射中没有与给定键对应的信息,get将返回null,有时可以有一个更好的默认值,使用getOrDefault方法:
Map<String,Integer> scores=...;
int score=score.get(id,0); //get 0 if the id is not present
键必须是唯一的,且不能对同一个键存放两个值(第二个值覆盖第一个)
remove方法用于从映射中删除给定键对应的元素,size方法用于返回映射中的元素数
要迭代的处理映射的键和值,最容易的方法是使用forEach方法。可以提供一个lambda表达式进行处理:
scores.forEach((k,v)->System.out.println("key="+k+",value="+v));
3.2 更新映射项
处理映射时的一个难点就是更新映射,尤其是键的第一次出现,例如:
counts.put(word,counts.get(word)+1);
在第一次调用时,get会返回null,作为补救,可以使用getOrDefault:
counts.put(word,counts.getOrDefault(word,0)+1);
另一种方法是首先调用putIfAbsent:
counts.putIfAbsent(word,0);
counts.put(word,counts.get(word)+1);
还有一种更好的方法:
counts.merge(word,1,Integer::sum);
3.3 映射视图
集合框架不认为映射本身是一个集合,不过,可以得到映射的视图——实现了Collection接口或某个子接口的对象
有三种视图:键集、值集合(不是一个集)以及键/值对集。
因为映射中一个键只能有一个副本,所以键和键/值对是一个集
三种视图分别对应下面的方法:
Set<K> KeySet()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()——Entry(条目集)是实现了Map.Entry接口的类的对象
这里的keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象,Set接口扩展了Collection接口。因此可以像使用集合一样使用keySet。
如果想同时查看键和值,可以通过枚举条目来避免查找值:
for(Map.Entry<String,Employee> entry:staff.entrySet()) { String k=entry.getKey(); Employee v=entry.getValue(); }
原先这是访问所有映射条目最高效的方法,现在,只需要使用forEach方法:
counts.forEach((k,v)->{ do something with k,v};
不能向键集视图、条目集视图增加元素,可以删除元素。
3.4 弱散列映射
在集合类库中有几个专用的映射类
WeakHashMap类是为了解决一个有趣的问题:如果有一个值,对应的键已经不再使用了,但是垃圾回收机制不会删除它,原因是垃圾回收器跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收
因此,需要由程序负责从长期存活的映射表中删除那些无用的值,或者使用WeakHashMap完成这件事。
当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
3.5 链接散列集与映射
LinkedHashSet和LinkedHashMap类用来记住插入元素项的顺序,这样可以避免在散列表中的项从表面上看是随机排列的。
链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。
每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)
访问顺序对于实现高速缓存的“最近最少使用”原则十分重要
构造一个LinkedHashMap的子类,可以让这一过程自动化,然后覆盖下面的方法:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
每当方法返回true时,就添加一个新条目,从而导致删除eldest条目,例如下面的高速缓存可以存放100个元素:
Map<K,V> cache=new LinkedHashMap<>(128,0.75F,true) { protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return size()>100; } }();
另外,可以对eldest条目进行评估,以此决定是否应该将它删除。例如,可以检查与这个条目一起存在的时间戳。
3.6 枚举集与映射
EnumSet是一个枚举类型元素集的高效实现,由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1
EnumSet类没有公共构造器,可以使用静态工厂方法构造这个集:
可以使用Set接口常用的方法修改EnumSet。
EnumMap是一个键类型为枚举类型的映射,需要在构造器中指定键类型:
EnumMap<Weekday,Employee> personInCharge=new EnumMap<>(Weekday.class)
E extends Enum<E>表示E是一个枚举类型,所有的枚举类型都扩展于泛型Enum类,例如,Weekday扩展于Enum<WeekDay>
3.7 标识散列映射
类IdentityHashMap有特殊用途,在这个类中,键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的,这个方法将根据对象的内存地址来计算散列码,两个对象进行比较时,使用==而不是equals
在实现对象遍历算法(如对象的串行化)时,这个类非常有用,可以跟踪每个对象的遍历情况
4 视图与包装器
通过使用视图可以获得其他的实现了Collection接口和Map接口的对象。映射类的keySet方法就是一个这样的示例:
初看起来,好像这个方法创建了一个新集,并将映射中的所有键都填进去,然后返回这个集。但是,情况并非如此。keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射进行操作。这种集合称为视图
视图技术在集框架中有许多非常有用的应用,下面将讨论这些应用:
4.1 轻量级集合包装器
Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器:
Card[] cardDeck=new Card[52]
List<Card> cardList=ArrayList.asList(cardDeck);
这个方法返回的对象不是ArrayList,它是一个视图对象,带有访问底层数组的get和set方法。改变数组大小的所有方法都会抛出一个Unsupported OperationException异常。
这个方法调用:
Collections.nCopies(n,anObject)
将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都是一个anObject的错觉。不需要付出建立数据结构的开销因而存储代价很小,这是视图技术的一种巧妙应用。
Collections·类包含了很多实用方法,这些方法的参数和返回值都是集合,不要将它与Collection接口混淆起来。
4.2 子范围
可以为很多集合建立子范围视图,例如:
List group2=staff.subList(10,20);
第一个索引包含在内,第二个索引则不包含在内
可以将任何操作应用于子范围,并且能够自动地反映整个列表情况,例如,可以删除整个子范围:
group2.clear();
对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围,例如SortedSet、SortedMap接口声明的:
SortedSet<E> subSet(E from,E to);
SortedMap<K,V> subMap(K from,K to)
Java SE 6引入的NavigableSet接口赋予子范围操作更多的控制能力,可以指定是否包含边界:
NavigableSet<E> subSet(E from,boolean fromInclusive,E to,boolean toInclusive)
4.3 不可修改的视图
Collections还有几个方法,用于产生集合的不可修改视图,这些视图对现有集合增加了一个运行时的检查,如果发现视图对集合进行修改,就抛出一个异常:
Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableNavigableSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
Collections.unmodifiableNavigableMap
例如,想要查看某部分代码,但又不触及某个集合的内容,就可以进行以下操作:
List<String> staff=new LInkedList<>(); ... lookAt(Collections.unmodifiableList(staff));
4.4 同步视图
如果由多个线程访问集合,就必须确保集不会被意外地破坏。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类
Collections类的静态方法synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的Map:
Map<String,Employee> map=Collections.synchronizedMap(new HashMap<String,Employee>());
4.5 受查视图
“受查”视图用来对泛型类型发生问题时提供调试支持
例如:
ArrayList<String> strings=new ArrayList<>(); ArrayList rawList=strings; rawList.add(new Date());
这个错误的add命令在运行时检测不到,受查视图可以探测到这类问题:
ArrayList<String> strings=new ArrayList<>(); List<String>safeStrings=Collections.checkedList(strings,String.class); ArrayList rawList=strings; rawList.add(new Date());
这样,视图add方法将检测插入的对象是否属于给定的类。
受查视图受限于虚拟机可以运行的运行时检查,例如,无法检查ArrayList<Pair<String>>
4.6 关于可选操作的说明
通常,视图有一些局限性,即可能只可以读、无法改变大小、只支持删除而不支持插入,在集合和迭代器接口的API文档中,许多方法描述为“可选操作”
5 算法
泛型集合接口有一个很大的优点,即算法只需要实现一次。
使用集合接口,可以将max实现为能够接收任何实现了Collection接口的对象:
public static <T extends Comparable> T max(Collection<T> c) { if(c.isEmpty()) throw new NoSuchElementException(); Iterator<T> iter=c.iterator(); T largest=iter.next(); while(iter.hasNext()) { T next=iter.next(); if(largest.compareTo(next)<0) largest=next; } return largest; }
5.1 排序与混排
Collections类中的sort方法可以对实现了List接口的集合进行排序
这个方法假定列表元素实现了Comparable接口,如果想采用其它方式对列表进行排序,可以使用List接口的sort方法并传入一个Comparator对象:
staff.sort(Comparator.comparingDouble(Employ::getSalary));
如果想按照降序排列:
staff.sort(Comparator.reverseOrder())
这个方法将根据元素类型的compareTo方法排序
staff.sort(Comparator.comparingDouble(Employ::getSalary).reversed());
这个方法将指定根据元素的工资降序排列
集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。但是,归并排序有一个主要的优点:稳定,即不需要交换相同的元素
因为集合不需要实现所有的“可选"方法,因此,所有接收集合参数的方法必须描述什么时候可以安全地将集合传递给算法。
例如,显然不能将unmodifiableList列表传递给排序算法,可以传递什么类型的列表呢?文档说明,列表必须是可修改的,但不必是可以改变大小的。
术语定义:
*如果列表支持set方法,则是可修改的
*如果列表支持add和remove方法,则是可以改变大小的
5.2 二分查找
Collections类的binarySearch方法实现了这个算法。注意,集合必须是排好序的,如果集合没有采用Comparable接口的compareTo方法排序,就还要提供一个比较器对象:
i=Collections.binarySearch(c,element);
i=Collections.binarySearch(c,element,comparator);
返回的数值大于等于0,则表示匹配对象的索引,即c.get(i)等于匹配的element;如果返回负值,则表示没有匹配元素。但是可以利用返回值计算应该将element插入到集合的哪个位置:
if(i<0) c.add(-i-1,element);
只有随机访问,二分查找才有意义
5.3 简单算法
在Collections类中包含了几个简单且很有用的算法:
*查找集合中的最大元素
*将一个列表中的元素复制到另外一个列表中
*用一个常量值填充容器
*逆置一个列表的元素顺序
Java SE 8中增加了默认方法Collection.removeIf和List.replaceAll,这两个方法要提供一个lambda表达式来测试或转换元素
例如,下面的代码将删除所有短词,并将其余单词改为小写:
words.removeIf(w->w.length()<=3);
words.replaceAll(String::toLowerCase);
5.4 批操作
很多操作会”成批“复制或删除元素,例如:
coll1.removeAll(coll2);
将从coll1中删除soll2中出现的所有元素
coll1.retainAll(coll2);
将从coll1中删除所有未在coll2中出现的元素
事实上:每一个集合都有这样一个构造器,其参数是包含初始值的另一个集合:
Set<String> result=new HashSet<>(a);
5.5 集合与数组的转换
如果需要把一个数组转换称为集合,ArrayList.asList包装器可以达到这个目的:
String[] values=...;
HashSet<String> staff=new HashSet<>(ArrayList.asList(values));
从集合得到数组会更困难一些,可以使用toArray方法:
Object[] values=staff.toArray();
不过这样做的结果是一个对象数组且不能进行强制类型转换。
实际上,必须使用toArray的一个变体形式,提供一个所需类型且长度为0的数组。这样一来,返回的数组就会创建为相同类型的数组:
String[] values=staff.toArray(new String[0]);
String[] values=staff.toArray(new String[staff.size()]);——这种情况下不会创建新数组
5.6 编写自己的算法
如果编写自己的算法(实际上,是以集合为参数的任何方法),应该尽可能的使用接口(Collection),而不要使用具体实现(ArrayList、LinkedList等)。
如果编写一个返回集合的方法,还是要返回接口(List),而不是返回类的方法(AbstractList、ArrayList、LinkedList等)。
情景:不复制所有的菜单项,而仅仅提供这些菜单项的视图(使用AbstractList类),在这种情况下,必须提醒调用者返回的对象是一个不可修改的列表
6 遗留的集合
在Java第一版问世以来,在集合框架出现之前已经存在大量的“遗留类”容器类:
下面各节将简要介绍这些遗留类
6.1 HashTable类
HashTable类与HashMap类作用一样,不过HashTable的方法是同步的。如果对同步性或与遗留代码的兼容性没有任何要求,就应该使用HashMap,如果需要并发访问,则使用ConcurrentHashMap。
6.2 枚举
遗留集合使用Enumeration接口对元素序列进行遍历。有两个方法,即hasMoreElements和nextElement:
例如,HashTable类的elements方法将产生一个用于描述表中各个枚举值的对象:
Enumeration<Employee> e = staff.elements(); while(e.hasMoreElements()) { Employee e = e.nextElement(); }
有时还会遇到遗留的方法,其参数是枚举类型的。静态方法Collections.enumeration将产生一个枚举对象:
List<InputStream> streams = ...; SequenceInputStream in = new SequenceInputStream(Collections.enumeration(streams));
6.3 属性映射属性
属性映射是一个类型非常特殊的映射结构:
*键和值都是字符串
*表可以保存到一个文件中,也可以从文件中加载
*使用一个默认的辅助表
实现属性映射的Java平台类是Properties
属性映射通常用于程序的特殊配置选项
6.4 栈
从1.0版本开始,标准类库中就包含了Stack类,有push和pop方法。但是Stack类扩展为Vector类并不令人满意(含insert和remove方法)。
6.5 位集
Java平台的BitSet类用于存放一个位序列,如果需要高效地存储位序列(例如:标志)就可以使用位集,使用位集比使用Boolean对象的ArrayList更加高效
BitSet类提供了一个便于读取、设置或清除各个位的接口,使用这些接口可以避免屏蔽和其他麻烦的位操作
例如,对于一个名为bucketOfBits的BitSet:
bucketOfBits.get(i)——如果第i位处于"开"状态,就返回true;否则返回false
bucketOfBits.set(i)——将第i位置为“开”状态
bucketOfBits.clear(i)——将第i位置为“关”状态