《第九章 集合》
一.Java集合框架
1.将集合的接口和实现分离
与现代的数据结构类库的常见情况一样,Java的集合类库将接口(interface)与实现(implement)分离
那么什么是接口与实现分离呢?
Java类库中关于Queue接口的定义如下:
public interface Queue<E> extends Collection<E> {
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
}
可以看到Queue接口并没有说明队列是如何实现的,这其实就实现了接口与实现的分离
因为接口本身就没有涉及到具体的实现,具体实现由接口的实现类来实现
队列通常有两种实现方式:一种是使用循环数组;另一种是使用链表
在Java类库中,如果需要一个循环数组队列,可以使用ArrayDeque类
ArrayDeque类的定义如下:
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
{
//Deque接口extends Queue接口
...
}
如果需要一个链表队列,可以使用LinkedLList类
LinkedList类的定义如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//Deque接口extends Queue接口
...
}
因为,接口与实现的分离,所以可以使用接口类型存放集合的引用
//链表实现的队列
Queue<Integer> queue = new LinkedList<>();
queue.add(100);
//数组实现的队列
Queue<Integer> queue = new ArrayDeque<>();
queue.add(100);
在Java类库中,还存在另外一组以Abstract开头的类
例如上面代码中的AbstractCollection,AbstractSequentialList
这些类是为类库实现者而设计的,例如想要实现自己的Collection类
会发现扩展AbstractCollection类要比实现Collection接口中的所有方法轻松得多
2.Collection接口
在Java类库中,集合类的基本接口之一就是Collection接口
Collection接口有两个基本方法
public interface Collection<E> extends Iterable<E>
{
boolean add(Element);
Iterator<E> iterator();
...
}
add添加元素,如果添加后改变了集合就返回true;
iterator方法用于依次访问集合中的元素
而且Collection接口还扩展了Iterable接口
所以对于所有的Collection接口的实现类都可以使用"for each"循环
3.迭代器
迭代器Iterator接口包含4个方法
public interface Iterator<E>
{
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");}
default void forEachRemaining(Consumer<? super E> action) {
Object.requireNonNull(action);
while (hasNext())
action.accept(next());
}
反复调用next方法,可以逐个访问集合中的每个元素
但是到了集合的末尾,next方法会抛出一个NoSuchElementException
因此需要在调用next之前先调用hasNext方法
Iterator接口的remove方法将会删除上次调用next方法返回的元素
如果调用remove之前没有调用next将是不合法的
forEachRemaining方法的参数可以是一个lambda表达式
执行该方法时将对迭代器的每一个元素调用lambda表达式iterator.forEachRemaining(elememnt->do something with element)
4.集合框架中的接口:
Java集合框架为不同类型的集合定义了大量接口
可以看到,集合中有两个基本接口:Collection和Map
Map接口插入元素时使用put方法,从键值对中读取值就需要get方法
SortedSet和SortedMap接口会提供用于排序的比较器对象
二.具体的集合
下图主要展示了Java类库中的集合,并简要介绍了每个集合类的用途(不包括线程安全集合)
除了以Map结尾的类之外,其他类都实现了Collection接口
而以Map结尾的类实现了Map接口
集合框架中的类
1.链表
众所周知,数组擅长随机访问,但不擅长删除或者插入元素
而链表则是擅长移动或者插入元素,但访问数据是只能按顺序访问,效率比较慢
在Java中,所有链表实际上都是双向链表——即每个结点还存放着指向前驱节点的引用
在数据结构这门课中,我们知道要想在链表中插入或者删除元素,就需要将节点的指针“绕来绕去”
但在Java中美则不同,其对链表LinkedList的插入或者删除非常简单,举例如下:
先添加3个元素,再将第二个元素删除
List\<String\> staff = new LinkedList<>();
/*构造链表*/
staff.add("Amy");//在链表的末尾添加元素
staff.add("Bob");
staff.add("Carl");
/*删除元素*/
Iterator iter = staff.iterator();
String first = (String)iter.next();
String second = (String)iter.next();
iter.remove();//remove方法将会删除上次调用next方法返回的元素
/*插入元素*/
ListIterator<String> listIterator = staff.listIterator();
listIterator.next();
listIterator.add("Juliet");//在Bob之前插入Juliet
可以看出上面的代码并没有让指针“绕来绕去”
它利用了Iterator接口的remove方法来实现链表的删除
利用ListIterator接口的add来实现列表的插入
另外,ListIterator接口有两个方法,可以用来反向遍历链表
E previous()
boolean hasPrevious()
2.数组列表
ArrayList封装了一个动态再分配的对象数组,但Vector类也能实现该功能
但Vector类的所有方法都是同步的,是线程安全的,而ArrayList方法不是同步的,即非线程安全
若是单线程访问则推荐使用ArrayList类,因为相比之下,ArrayList更快
3.散列集
有一种众所周知的数据结构,可以快速查找所需的对象,这既是散列表(hash table)
散列表为每个对象计算一个整数,称为散列码(hash code)
具有不同数据域的对象将产生不用的散列码
在Java中,散列表使用链表数组实现
Java集合类库中,提供了一个HashSet类,它实现了基于散列表的值,用add方法添加元素
contains方法快速地查看是否某个元素已经出现在集合中
散列集迭代器将依次访问所有的桶
由于散列将元素分散在表的各个位置,所以访问它们的顺序几乎是随机的
当不关心集合中的元素顺序时才应该使用HashSet
4.树集
TreeSet与散列集十分相似,不过,它比散列集有所改进
树集是一个有序集合
可以以任意顺序插入到集合中。在对集合进行遍历时,每个值将自动按照排序后的顺序呈现
Set<String> set = new TreeSet<>();
set.add("Bob");
set.add("Amy");
set.add("Carl");
for (String s: set) {
System.out.println(s);
}
每个值将按照顺序打印出来:Amy Bob Carl。
TreeSet的排序是用树结构完成的,当前实现用的是红黑树
红黑树本身就是一个二叉排序树,即每次放入一个元素时,该元素都会被放置在正确的位置上
要使用树集,树集中的元素必须要能够相互比较
5.队列
队列:可以在尾部添加元素,在头部删除元素
有两个端头的队列,即双端队列,可以在头部和尾部用时进行插入或者删除元素
优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索
也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素
优先级队列使用了堆(最小堆)这一数据结构来实现
三.映射
1.基本映射操作
映射用来存放键值对。如果提供了键,就能找到值
Java中提供了两个映射的通用实现:HashMap和TreeMap。这两者都实现了Map接口
HashMap对键进行散列
TreeMap是用键的整体顺序对元素进行排序,并将其组织成搜索树
Map<String,String> staff = new HashMap<>();
/*使用put方法来添加元素*/
staff.put("987-98-996","harry");
staff.put("123-456-789","Alice");
String id="987-98-996";
/*使用get方法来检索对象*/
String value = staff.get(id);
System.out.println(value);
/*当key不能存在时,一般会返回null*/
/*使用getOrDefault方法,可以指定key不存在时返回的值*/
String errorId="123";
String defaultValue = staff.getOrDefault(errorId,"0");
System.out.println(defaultValue);
/*迭代处理映射的键和值*/
staff.forEach((k,v)-> System.out.println("k="+k+"v="+v));
2.映射视图
集合框架不认为映射本身是一个集合
不过,可以得到映射的视图——这是实现了Collection接口或者某个子接口的对象
对应的映射视图有:
Set<K> keyset()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
上述的三个方法会分别返回键集、值集合以及键/值对集
于是,可以得到我们Map的对应的遍历方法
Map<Integer,String> map = new HashMap<>();
map.put(1,"hello");
map.put(2,"world");
//对应的遍历方法
// 1.keySet
for(Integer key:map.keySet()){
System.out.println(key);
System.out.println(map.get(key));
}
//2.values
for(String value:map.values()){
System.out.println(value);
}
//3.enterSet
for(Map.Entry<Integer, String> entry:map.entrySet()){
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
四.遗留的集合
HashTable类
HashTable类与HashMap类的作用一样
实际上它们拥有相同的接口
与Vector类一样,HashTable也是同步的
如果对同步性或者遗留代码的兼容性没有任何要求,就应该使用HashMap
如果需要并发访问,则要使用ConcurrentHashMap
枚举
遗留集合使用Enumeration接口对元素顺序进行遍历。
该接口有两个方法:hasMoreElements和nextElement。
属性映射
属性映射(property map)是一个类型非常特殊的映射结构。
- 键与值都是字符串
- 表可以保存得到一个文件中,也可以从文件中加载
- 使用一个默认的辅助表
- 实现属性映射的Java平台类称为Properties
- 属性映射通常用于程序的特殊配置选项
栈
从1.0版本开始,标准类库中就包含了Stack类,有push和pop方法
还有peek方法,返回栈顶元素却不弹出
位集
Java的BitSet类用于存放一个位序列。
如果需要高效地存储位序列(例如,标志)就可以使用位集。