ASE265

《第九章 集合》

一.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类用于存放一个位序列。
如果需要高效地存储位序列(例如,标志)就可以使用位集。

分类:

技术点:

相关文章: