Java核心技术卷一 7. java集合

Java 集合框架

最初常用的数据结构类:Vector 、Stack 、Hashtable 、BitSet 与 Enumeration 接口。

将集合的接口与实现分离 Queue接口

Java 集合类库将接口与实现分离。

看队列(queue)是如何分离的。队列接口指出可以在尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就因该使用队列。

队列接口最简形式:

public interface Queue<E> {
    void add(E element);
    E remove();
    int size();
}

实现队列的两种方式:

  • 使用循环数组
  • 使用链表

实现类的实现方式:

public class CircularArrayQueue<E> implements Queue<E> {
    private int head;
    private int tail;
    private E[] elements;
    
    CircularArrayQueue(int capacity) {...};
    public void add(E element) {...};
    public E remove() {...};
    public int size() {...};
}

public class LinkedListQueue<E> implements Queue<E> {
    private int head;
    private int tail;
    
    LinkedListQueue() {...};
    public void add(E element) {...};
    public E remove() {...};
    public int size() {...};
}

java 类库中需要一个循环数组队列,使用ArrayQueue;java 类库中需要一个链表队列,使用LinkedList

可以使用接口类型存放集合的引用:

Queue<Customer> expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));

Queue<Customer> expressLane = new LinkedListQueue<>();
expressLane.add(new Customer("Harry"));

每种实现都有它的好处与坏处。循环数组要比链表更高效,多数人优先选择循环数组。

使用循环数组的代价:

循环数组是一个有界集合,即容量有限。如果程序中收集的对象数量没有上限,最好使用链表来实现。

Collection 接口

集合类的基本接口是 Collection 接口。它有两个基本方法:

public interface Collection<E>{
    bollean add(E element);
    Iterator<E> iterator();
    ...
}

add 方法用于向集合中添加元素。如果添加元素改变了集合返回 true,如果集合没有改变就返回 false。集合中不允许有重复的对象。

iterator 方法用于返回一个实现了 Iterator 接口的对象。可以使用这个迭代器对象一次访问集合中的元素。

迭代器 Iterator接口 Iterable 接口

Iterator 接口包含 4 个方法:

public interface Iterator<E>{
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}

next 方法

反复调用 next 方法,可以逐个访问集合中的每个元素。到达集合末尾,next 方法抛出一个异常。

调用 next 方法之前调用 hasNext 方法,迭代器对象还有多个供访问的元素就返回 true ,如果没有供访问的元素就返回 false 。

如果想要查看集合中的所有元素,可以请求一个迭代器,并在 hasNext 返回 true 时调用 next 方法:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
    String element = iter.next(); 
    System.out.println(element);
}

for each 循环

用 for each 循环可以更加简单地表示同样的循环操作:

Collection<String> c = ...;
for (String element : c) {
    System.out.println(element);
}

for each 循环可以与任何实现了 Iterable 接口的对象一起工作,这个接口被 Collection 借口扩展,所以所有实现 Collection 接口的实现类都可以使用 for each 循环:

public interface Iterable<E>{
    Iterator<E> iterator();
    ...
}

Java SE 8 可以调用 forEachRemaining 方法提供一个 lambda 表达式:

iterator.forEachRemaining(element -> System.out.println(element));

元素被访问的顺序

取决于集合类型:

  • ArrayList 进行迭代时,迭代器将从索引 0 开始,每迭代一次,索引值加 1 。
  • HashSet 进行迭代时,每个元素会按照某种随机的次序出现。能够遍历所有元素,但无法预知元素被访问的次序。

Java 迭代器概念

Java 中的迭代器在查找操作与位置变更是紧密相连的。查找一个元素的为方法是调用 next,而在执行查找操作的同事,迭代器的位置随之向前移动。

Java 迭代器因该认为位于两个元素之间。当调用 next 时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。

remove 方法

删除上次调用 next 方法返回的元素。所以删除指定位置上的元素,要越过这个元素。

删除第一个越过的方法:

Iterator<String> it = c.iterator();
it.next();
it.remove();

如果 remove 之前没有调用 next 将会抛出 IllegalStateException 异常。

如果向删除两个相邻的元素,也要越过这两个元素:

it.next();
it.remove();
it.next();
it.remove();

泛型实用方法

检测任意集合是否包含指定元素的泛型方法:

public static <E> boolean contains(Collection<E> c, Object obj){
    for(E element : c){
        if(element.equals(obj)){
            return true;
        }
    }
    return false;
}

Collection 接口声明了很多有用的方法,所有的实现类都必须提供这些方法。

Java 类库提供了一个类 AbstractCollection,它将基础方法 size 和 iterator 抽象化了,但是在此提供了例行方法。

public abstract class AbstractCollection<E> implements Collection<E>{
    ...
    public abstract Iterator<E> iterator();
    
    public boolean contains(Object obj){
        for(E element : this){
            if(element.equals(obj)){
                return true;
            }
        }
        return false;
    }
    ...
}

一个具体的集合类扩展 AbstractCollection 类,它就能替我们实现一些类。也可以自己实现重写覆盖即可。

API

//java.util.Collection<E>
Iterator<E> iterator() //返回一个用于访问集合中每个元素的迭代器
int size() //返回集合中的元素个数
boolean isEmpty() //集合中没有元素返回 true
boolean contains(Object obj) //集合中包含一个与 obj 相等的对象,返回 true
boolean containsAll(Collection<?> other) //集合包含了所有 other 中的所有元素,返回 true
boolean add(Object element) //将一个元素添加到集合中,集合改变返回 true
boolean addAll(Collection<? extends E> other) //将 other 集合中的所有元素添加到集合中,改变了集合返回 true
boolean remove(Object obj) //删除集合中等于 obj 的对象。匹配对象被删除,返回 true
boolean removeAll(Collection<?> other) //集合删除 other 集合中所有存在的元素。集合改变返回 true
default boolean removeIf(Predicate<? super E> filter) 8 //集合删除 filter 返回 true 的所有元素。集合改变返回 true
void clear() //删除集合中的所有元素
boolean retainAll(Collection<?> other) //删除所有与 other 集合中不同的元素,集合改变返回 true
Object[] toArray() //返回这个集合的对象数组
<T> T[] toArray(T[] arrayToFill) //返回这个集合的对象数组填入 arrayToFill 对象数组中。够大剩余填补 null,不够大生成一个大小规格好的 arrayToFill 对象数组。

//java.util.Iterator
boolean hasNext() //存在可访问的元素返回 true
E next() //返回要访问的下一个对象,超过尾部抛出异常。
void remove() //删除上次访问的对象。紧跟 next 之后,如果不是或者集合已经改变抛出一个异常。

集合框架中的接口

java 为不同的集合定义了大量接口:

//Iterable
Iterable<E> <-- Collection<E>

//Iterator
Iterator<E> <-- ListIterator<E>

//Collection
Collection<E> <--- List<E>
Collection<E> <--- Set<E>
Collection<E> <--- Queue<E>

Set<E> <--- SortedSet<E>
Queue<E> <--- Deque<E>

SortedSet<E> <--- NavigableSet<E>

//Map
Map<K,V> <--- SortedMap<K,V>

SortedMap<K,V> <--- NavigableMap<K,V>

//RandomAccess
RandomAccess

Map 接口由于映射包包含键值对,需要一些方法来添加和获取:

V put(K key, V value);

V get(Object key);

List 接口是一个有序集合。元素添加到特定位置。可采用两种方式访问元素,迭代器访问和随机访问。

void add(int index, E element)
void remove(int index)
E get(int index)
E set(int index, E element)

ListIterator 接口是 Iterator 的一个子接口定义了一个方法用于在迭代器位置前面增加一个元素:

void add(E e);
void set(E e);

不妥处:集合框架有两种有序集合,性能开销有很大差异。有数组支持的有序集合可以快速的随机访问,使用 List 方法并提供一个整数索引来访问。链表尽管也有序但是随机访问很慢,最好使用迭代器来遍历。如果原先提供两个接口就会容易一些了。

为了避免对链表完成随机访问操作,接口 RandomAccess 不提供任何方法,但是可以用它来测试一个特定的集合是否支持高效的随机访问:

if (c instanceof RandomAccess){
    do something;
} else {
    do something;
}

Set 接口等同于 Collection 接口,但方法的行为有更严谨的定义。

  • 集(Set)的 add 方法不允许增加重复的元素。
  • 集的 equals 方法,只要两个集包包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。
  • 集的 hashCode 方法的定义要包装包含相同元素的两个集会得到相同的散列码。

SortedSet 和 SortedMap 接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。

NavigableSet 和 NavigableMap 接口包含一些用于搜索和遍历有序集合和映射的方法。TreeSet 和 TreeMap 类实现了这些接口。

具体的集合

除了以 Map 结尾的类之外,其他类都实现了 Collection 接口,而已 Map 结尾的类实现了 Map 接口。

具体集合:

  • ArrayList 可以动态增长和缩减的索引序列
  • LinkedList 可以在任何位置进行高效地插入和删除操作的有序序列
  • ArrayDeque 用循环数组实现的双端队列
  • HashSet 没有重复元素的无序集合
  • TreeSet 有序集
  • EnumSet 包含枚举型值的集
  • LinkedHashSet 可以记住元素插入次序的集
  • PriorityQueue 允许高效删除最小元素的集合
  • HashMap 存储键值关联的数据结构
  • TreeMap 键值有序排列的映射表
  • EnumMap 键值属于枚举类型的映射表
  • LinkedHashMap 可以记住键值项添加次序的映射表
  • WeakHashMap 其值无用武之地后可以被垃圾回收器回收的映射表
  • IdentityHashMap 用==而不是用equals比较键值的映射表

LinkedHashMap 测试:

LinkedHashMap<Integer, Double> linkedHashMap;
linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put(4,123.4);
linkedHashMap.put(6,13.4);
linkedHashMap.put(2,12.4);
linkedHashMap.put(8,53.4);
linkedHashMap.put(3,33.4);
linkedHashMap.put(1,63.4);
linkedHashMap.put(9,83.4);
linkedHashMap.put(14,12.4);
linkedHashMap.put(5,14.4);
linkedHashMap.put(2,17.4);
System.out.println(linkedHashMap.toString());

HashMap<Integer, Double> hashMap = new HashMap<>();
hashMap.put(4,123.4);
hashMap.put(6,13.4);
hashMap.put(2,12.4);
hashMap.put(8,53.4);
hashMap.put(3,33.4);
hashMap.put(1,63.4);
hashMap.put(9,83.4);
hashMap.put(14,12.4);
hashMap.put(5,14.4);
hashMap.put(2,17.4);
System.out.println(hashMap.toString());
        
//{4=123.4, 6=13.4, 2=17.4, 8=53.4, 3=33.4, 1=63.4, 9=83.4, 14=12.4, 5=14.4}
//{1=63.4, 2=17.4, 3=33.4, 4=123.4, 5=14.4, 6=13.4, 8=53.4, 9=83.4, 14=12.4}

可以看出,它和HashMap相比他是有顺序的。

链表 LinkedList

数组和数组列表有个重大的缺陷,从数组中删除一个元素要付出很大的代价。原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动,在数组中间的位置插入一个元素也是如此。

链表(linked list)在插入和删除方面效率相比数组很高。链表将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。Java 程序设计语言中,所有链表都是双向链接的,每个结点还存放着指向前驱结点的引用。

具体结构如下:

LinkedList     Link1         Link2             Link3             Link3
               data          data              data              data
first -> Link1 next -> Link2 next -> Link3     next -> Link4     next
               previous      previous -> Link1 previous -> Link2 previous -> Link3

从链表中间删除一个元素,只需要更新前后的结点链接即可。

链表是一个有序集合,每个对象的位置都十分重要。LinkedList.add 方法将对象添加到链表的尾部。

常常需要将元素添加到链表的中间,由于迭代器是描述集合中的位置,所以这种依赖于位置的 add 方法将由迭代器负责 ListIterator 接口(扩展 Iterator 接口)。像集(set)类型,其中类型完全无需,在 Iterator 接口中没有 add 方法。

//class LinkedList
public void add(E e) {}
public void add(int index, E element) {}

public interface ListIterator<E> extends Iterator<E> {
    void add(E e);
}

ListIterator 接口有两个方法,用来反向遍历链表。

E previous() //返回越过的对象
boolean hasPrevious() //判断是否之前有越过的对象,有返回 true
    
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(20);
linkedList.add(10);
linkedList.add(0, 30);
ListIterator<Integer> listIterator = linkedList.listIterator();
listIterator.next();
System.out.println(listIterator.hasPrevious()); //true
System.out.println(listIterator.previous());//30

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(20);
linkedList.add(10);
linkedList.add(0, 30);
ListIterator<Integer> listIterator = linkedList.listIterator();
System.out.println(listIterator.hasPrevious()); //false
System.out.println(listIterator.previous());//java.util.NoSuchElementException

LinkedList 类的 listIterator 方法返回一个实现了 ListIterator 接口的迭代器对象。

ListIterator<String> iter = staff.listIterator();

迭代器对象使用 add 方法是在迭代器位置之前添加一个新对象:

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
ListIterator<Integer> listIterator = linkedList.listIterator();
listIterator.next();
listIterator.add(40);
System.out.println(linkedList.toString());//[10, 40, 20, 30]

迭代器对象可以给链表头和链表尾添加元素,所有如果有 n 个元素,那么迭代器对象可以有 n+1 个位置插入新元素。

调用 next 之后,在调用 remove 方法会删除迭代器左侧的元素,如果在调用 previous 会删除右边的元素,并且不能连续两次调用 remove 方法。

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
ListIterator<Integer> listIterator = linkedList.listIterator();
listIterator.next();
listIterator.next();

// 1
// [10, 20,| 30]
listIterator.previous();
// [10,| 20, 30]
listIterator.remove();
System.out.println(linkedList.toString());//[10, 30]

// 2
// [10, 20,| 30]
//listIterator.previous();
// [10,| 20, 30]
//listIterator.previous();
// [|10, 20, 30]
//listIterator.remove();
//System.out.println(linkedList.toString());//[20, 30]

set 方法用一个新元素取代调用 next 或 previous 方法返回的上一个元素。同样 next 后改变前头,previous 改变后头。

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
ListIterator<Integer> listIterator = linkedList.listIterator();
listIterator.next();
listIterator.next();
listIterator.next();
listIterator.set(40);
System.out.println(linkedList.toString());//[10, 20, 40]
listIterator.previous();
listIterator.set(50);
System.out.println(linkedList.toString());//[10, 20, 50]

如果有两个迭代器对象,一个迭代器对元素 next 并 remove 后,另一个迭代 next 就会抛出一个异常。

避免修改的规则:

  • 根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。
  • 单独附加一个既能读又能写的迭代器

链表访问元素的方式:链表不支持快速地随机访问。如果要查看链表中第 n 个元素,就必须从头开始,越过 n-1 个元素。没有捷径可走。程序需要采用整数索引访问元素时,通常不选用链表。

按索引获取元素使用 get 方法,从 0 开始匹配元素.

这个方法效率不高,如果你使用这个方法因该解决的问题是使用了错误的数据结构。

LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
System.out.println(linkedList.get(2));//30

nextIndex 方法返回下一次调用 next 方法时返回元素的整数索引;

previousIndex 方法返回下一次调用 previous 方法时返回元素的整数索引。他比 nextIndex 返回的索引值小 1。

这两个方法效率很高,因为迭代器保持着当前的位置。

使用链表的场合,减少在列表中间插入或删除元素所付出的代价。如何元素较少应该使用 ArrayList 。

数组列表 ArrayList

数据列表使用 get 和 set 方法随机地访问每个元素非常有用。

ArrayList 类,实现了 List 接口,封装了一个动态再分配的对象数组。

Vector 类也是动态数组,但是 Vector 类的所有方法都是同步的。两个线程可以安全的访问一个 Vector 对象,但是在同步操作上需要耗费大量的时间。建议在不需要同步时使用 ArrayList 。

散列集 HashSet

如果不在意元素的顺序,可以有几种能够快速查找元素的数据结构。缺点是无法控制元素出现的次序。

散列表(hash table)可以快速的查找所需要的对象。散列表为每个对象计算一个整数,称为散列码(hash code)。

散列码(hash code)是对象实例域产生的一个整数,不同数据域的对象产生不同的散列码。

散列表(hash table)用链表数组实现,每个链表称为桶(bucket)。

  • 想查找散列表中对象的位置,就要写计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引(确定那个链表),在找到散列码就可以找到这个对象。散列函数 除法散列法
  • 如果插入元素时,如果桶被占满,称为散列冲突,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。
  • 如果散列表太满,就需要在散列。如果要对散列表再散列,就需要创建一个桶数更多的表,将所有元素插入到这个新表中,然后丢弃原来的表。
  • 装填因子决定何时对散列表进行散列。如,装填因子为 0.75 ,则表中用了超过 75%的位置,就会用双倍桶数进行再散列。

HashSet 类,实现了基于散列表的集,可以使用 add 方法添加元素。contains 方法被重定义,用来快速的查看是否有某个元素在集里。它只在某个桶中查找元素,而不必查看集合中的所有元素。

散列迭代器依次访问所有的桶。散列将元素分散在表的各个位置上、所以访问他们的顺序几乎是随机的。不关心集合中元素的顺序时才使用 HashSet 。

//java.util.HashSet<E>
HashSet() //构造空的散列集。
HashSet(Collection<? extends E> elements) //构造一个散列集,并将集合中的所有元素添加到这个散列集中。
HashSet(int initialCapacity) //构造空的散列集,并指定了容量(桶数)
HashSet(int initialCapacity, float loadFactor) //构造了空的散列集,并指定了容量和装填因子(0.0 ~ 1.0 直接)。

//java.lang.Object
int hashCode() 返回对象散列码。

树集 TreeSet

TreeSet 类与散列集十分类似,不过,它比散列集有所改进。

树集是一个有序集合。可以按任意顺序将元素插入到集合中。

树集遍历时,每个值将自动地按照排序后的顺序呈现。

排序是由树结构完成的(红黑树),每次将一个元素添加到树中时,都被放置在正确的排序位置上。所以使用树集必须能够比较元素,这些元素必须实现 Comparable 接口或者构造集合时提供一个 Comparator。

所以添加元素时,树集比散列集慢。

//java.util.TreeSet
TreeSet() //构造一个空树集
TreeSet(Comparator<? super E> comparator) //构造一个空树集
TreeSet(Collection<? extends E> elements) //构造一个树集,增加一个集合中的元素
TreeSet(SortedSet<E> s) //构造一个树集,增加一个集合或有序集合的所有元素

//java.util.SortedSet<E>
Comparator<? super E> comparator() //对元素进行排序的比较器
E first() //返回有序集合中的最小元素
E last() //返回有序集合中的最大元素

//java.util.NavigableSet<E>
E higher(E value) //返回大于 value 的最小元素
E lower(E value) //返回小于 value 的最大元素
E ceiliing(E value) //返回大于等于 value 的最小元素
E floor(E value) //返回小于等于 value 的最大元素
E pollFirst() //删除并返回这个集合中的最大元素
E pollLast() //删除并返回这个集合中的最小元素
Iterator<E> descendingIterator() //返回一个按照递减顺序遍历集合元素的迭代器

队列与双端队列 ArrayDeque

队列可以有效的在尾部添加一个元素,在头部删除一个元素。又两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。

队列不支持在中间添加元素。Java SE 6 引入 Deque 接口,由 ArrayDeque 和 LinkedList 类实现。两个类都提供了双端队列,在必要时可以增加队列的长度。

//java.util.Queue<E> 接口
boolean add(E element) //队列没满,将元素添加到双端队列尾部并返回 true。队列满抛出异常。
boolean offer(E element) //队列没满,将元素添加到双端队列尾部并返回 true。队列满返回 false。
E remove() //队列不为空时,删除并返回队列头部的元素。空的则抛出异常。
E poll() //队列不为空时,删除并返回队列头部的元素。空的则返回 null。
E element() //如果队列不空,返回这个队列头部的元素。为空抛出异常。
E peek() //如果队列不空,返回这个队列头部的元素。为空返回 null。

//java.util.Deque<E> 接口
void addFirst(E element) //将对象添加到双端队列的头部。如果满了抛出异常。
void addLast(E element) //将对象添加到双端队列的尾部。如果满了抛出异常。
boolean offerFirst(E element) //将对象添加到双端队列的头部。如果满了返回false。
boolean offerLast(E element) //将对象添加到双端队列的尾部。如果满了返回false。
E removeFirst() //删除并返回队列头部的元素。队列为空抛出异常。
E removeLast() //删除并返回队列尾部的元素。队列为空抛出异常。
E pollFirst() //删除并返回队列头部的元素。队列为空返回 null。
E pollLast() //删除并返回队列尾部的元素。队列为空返回 null。
E getFirst() //返回队列头部的元素,队列为空抛出异常
E getLast() //返回队列尾部的元素,队列为空抛出异常
E peekFirst() //返回队列头部的元素。队列为空返回 null。
E peekLast() //返回队列尾部的元素。队列为空返回 null。

//java.util.ArrayDeque<E> 类
ArrayDeque() //构造一个无限双端队列,默认初始容量16
ArrayDeque(int initialCapacity) //构造一个无限双端队列,指定初始容量

优先级队列 PriorityQueue

优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索

调用 remove 方法,总是获得当前优先级队列中最小的元素。

如果使用迭代的方法处理这些元素,并不需要对它们进行排序。优先级队列使用了高效的数据结构,称为堆。

堆是一个可以自我调整的二叉树,对树执行添加和删除操作,可以让最小的元素移动到根,而不必花费时间进行排序。

PriorityQueue<Integer> arrayQueue = new PriorityQueue<>();
arrayQueue.add(10);
arrayQueue.add(20);
arrayQueue.add(30);
arrayQueue.add(40);
System.out.println(arrayQueue);//[10, 20, 30, 40, 50, 60, 70]
arrayQueue.remove();
System.out.println(arrayQueue);//[20, 40, 30, 70, 50, 60]
arrayQueue.remove();
System.out.println(arrayQueue);//[30, 40, 60, 70, 50]
arrayQueue.remove();
System.out.println(arrayQueue);//[40, 50, 60, 70]
arrayQueue.remove();
System.out.println(arrayQueue);//[50, 70, 60]
arrayQueue.remove();
System.out.println(arrayQueue);//[60, 70]

与 TreeSet 类一样,一个优先队列既可以保存实现了 Comparable 接口的类对象,也可以保存在构造器中提供的 Comparator 对象。

优先队列的典型示例就是任务调度,每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。

//java.util.PriorityQueue
PriorityQueue() //构造空的优先队列
PriorityQueue(int initialCapacity) //构造指定了初始容量的优先队列
PriorityQueue(int initialCapacity, Comparator<? super E> c) //构造一个优先队列,并用指定的比较器对元素进行排序。

映射

映射(map)数据结构用来存放键值对。如果提供了键,就能够查找到值。

基本映射操作 HashMap TreeMap

Java 类库提供了两个通用的映射类:HashMap 和 TreeMap 。它们都实现了 Map 接口。

HashMap 对键进行散列;TreeMap 用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于。与键关联的值不能进行散列或比较。

HashMap 和 TreeMap 相比,散列稍微快一些,如果不需要按照排序列顺序访问键,就最好选择 HashMap 。

HashMap<Integer, String> hashMap = new HashMap<>();
System.out.println(hashMap.put(1, "xhl")); //null
System.out.println(hashMap.get(1)); //xhl
System.out.println(hashMap.get(2)); //null,get 方法没有对应的键时
System.out.println(hashMap.getOrDefault(2, "没有对应的键")); //没有对应的键
System.out.println(hashMap.put(1, "xhl2")); //xhl
System.out.println(hashMap.get(1)); //xhl2
hashMap.put(2, "xhl3");
System.out.println(hashMap); //{1=xhl2, 2=xhl3}
hashMap.remove(2);
System.out.println(hashMap); //{1=xhl2}
System.out.println(hashMap.size()); //1
hashMap.put(2, "xhl3");
hashMap.put(3, "xhl4");
hashMap.forEach((k, v) ->
        System.out.println("key=" + k + ", " + "value=" + v));
//key=1, value=xhl2
//key=2, value=xhl3
//key=3, value=xhl4

常用API

//java.util.Map<K, V>
V get(K)
default V getOrDefault(K, V)
V put(K, V)
void putAll(Map<? extends K, ? extends V> entries)
boolean containsKey(K) //查看是否存在键
boolean containsValue(V) //查看是否存在值
default void forEach(BiConsumer<? super K, ? super V> action) 8

//java.util.HashMap<K, V>
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor) //容量和装填因子
    
//TreeMap<K, V>
TreeMap() //为实现 Comparator 接口的键的空树映射
TreeMap(Comparator<? super K> c) //指定的比较器对键排序
TreeMap(Map<? extends K, ? extends V> entries) //将映射集的值加入到构造的树映射
TreeMap(SorteMap<? extends K, ? extends V> entries) //将有序映射中的所有条目添加到构造的树映射中
    
//java.util.SortedMap<K, V>
Comparator<? super K> comparator() //对键进行排序的比较器
K firstKey() //返回映射中最小的元素
K lastKey() //返回映射中最大的元素 

更新映射项 get put

正常情况,得到与一个键关联的原值,完成更新,在放回更新后的值。

特殊情况,键第一次出现,put 方法返回的是 null 。

需求:映射统计一个单词在文件中出现的频度,没看到一个单词就更新加 1 。

counts.put(word, counts.get(word) + 1);
//当 word 第一次加一时,get方法返回 null,这里出现异常

counts.put(word, counts.getOrDefault(word, 0) + 1);
//getOrDefault 方法当键不存在时,用默认值 0 返回

counts.putIfAbsent(word, 0);
//当键原先存在时,才会放入一个值。随后在更新。
counts.put(word, counts.get(word) + 1);
//更新时,就能保证键存在。

counts.merge(word, 1, Integer::sum);
//将 word 与 1 关联,否则使用 Integer::sum 函数组合原值和1。

映射视图 keySet values entrySet

Map 接口有三种获得视图的方法,分别获得 映射的键集、值集合以及键值对集:

Set<Integer> integers = hashMap.keySet();
//返回映射所有键的一个键集。可删除元素,所删除的值及相应的键将从映射中删除。
Collection<String> values = hashMap.values();
//返回映射所有值的一个集合视图。可删除元素,所删除的值及相应的键将从映射中删除。
Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
//条目集返回的元素是实现 Map.Entry 接口的类的对象。可以从这个集中删除元素,将从映射中删除,但是不能增加任何元素。还有可有修改键值的值。
System.out.println(integers); //[1, 2, 3]
System.out.println(values); //[xhl2, xhl3, xhl4]
System.out.println(entries); //[1=xhl2, 2=xhl3, 3=xhl4]
System.out.println(hashMap); //{1=xhl2, 2=xhl3, 3=xhl4}

可以使用 foreach 遍历集合。

for (Integer integer:
     integers) {
    System.out.print(integer + " ");
} //1 2 3

for (String string :
        values) {
    System.out.print(string + " ");
} //xhl2 xhl3 xhl4 

for (Map.Entry<Integer, String> map :
        entries) {
    System.out.println(map.getKey() + "=" + map.getValue());
}
//1=xhl2
//2=xhl3
//3=xhl4

API

//java.util.Map<K, V>
K getKey() //返回这个条目的键
V getValue() //返回这个条目的值
V setValue(V newValue) //将相关映射中的值改为新值,返回原来的值

弱散列映射 WeakHashMap

WeakHashMap 散列映射里的元素如果没有用了,可以使用垃圾回收器删除它。当外键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键值对。

机制的内部运行情况:

  • WeakHashMap 使用弱引用保存键。
  • WeakReference 对象将引用保存到另一个对象中,散列键。
  • 垃圾回收器用一种特有的方式进行处理这个对象。当发现某个特定的对象已经没有他人引用了,就将其回收。
  • 如果某个对象只能由 WeakReference 引用,垃圾回收期任然回收它。但要将引用这个对象的弱引用放入队列中。
  • 弱引用进入队列就意味着这个键不再被他人使用,并且已经被收集起来。
  • 于是,WeakHashMap 将删除对应的条目

链接散列集与映射 LinkedHashMap LinkedHashSet

LinkedHashMap 和 LinkedHashSet 类用来记住插入元素项的顺序。避免在散列表中的项随机排列。当条目插入到表中时,就会并入到双向链表中。

插入什么顺序,输出就是什么顺序。

链接散列映射将用访问顺序,而不是插入顺序,对映射条目进行迭代。

枚举集与映射 EnumSet

EnumSet 是一个枚举类型元素集的高效实现。由于枚举类型只有有限的实例,所以 EnumSet 内部用位序列实现。如果对应的值在集中,则相应的位被置为 1。

标识散列映射 IdentityHashMap

IdentityHashMap 类中,键的散列值不是用 hashCode 函数计算的,而是用 System.identiryHashCode 方法计算。这是 Object.hashCode方法根据对象的内存地址来计算散列码时所使用的方法。

所以 IdentityHashMap 的两个对象进行比较时,使用==而不是equals

不同的键对象,即使内容相同,也被视为是不同的对象。实现对象遍历算法时,这个类非常有用,可以用来跟踪每个对象的遍历状况。

视图与包装器

轻量级集合包装器 Arrays.asList Collections

Arrays.asList 方法返回一个包装了普通 Java 数组的 List 包装器。它可以将数组传递给一个期望得到列表或集合参数的方法。

Card[] cardDeck = new Card[52];
List<Card> cardDeck = Arrays.asList(cardDeck);

它是一个视图,带有访问底层数组的 get 和 set 方法,任何该表数组大小的打法都将出现异常。

asList 方法可以接受可变数目的参数:

List<String> names = Arrays.asList("Amy", "Bob", "Carl");

Collections.nCopies 方法返回一个视图对象,包含 10 个参数,每个参数都是"xhl"

List<String> xhl = Collections.nCopies(10, "xhl");
System.out.println(xhl); //[xhl, xhl, xhl, xhl, xhl, xhl, xhl, xhl, xhl, xhl]

Collections 的 singleton 、singletonList 和 singletonMap 分别返回一个不可修改的单元素 Set 、List 和 Map 不需要付出建立数据结构的开销。

Set<String> xhl1 = Collections.singleton("xhl");
List<String> xhl2 = Collections.singletonList("xhl");
Map<Integer, String> xhl3 = Collections.singletonMap(10, "xhl");

还可以使用 Collections.empty* 方法生成各种空的集、列表、映射等。

子范围

可为集合建立子范围视图。

如 List 的 subList 方法。

SortedSet 接口声明的三个方法。subSet 、headSet 和 tailSet

SortedMap 接口声明的三个方法。subMap、headMap 和 tailMap

不可修改的视图

Collections 的几个方法可以产生集合的不可修改视图。增加了一个运行时的检查,发现修改则抛出异常,同时保持未修改的状态。

unmodifiableCollection(Collection<T> c)
unmodifiableList(List<T> list)
unmodifiableMap(Map<K, V> m)
unmodifiableNavigableMap(NavigableMap<E, V> m)
unmodifiableNavigableSet(NavigableSet<T> s)
unmodifiableSet(Set<T> s)
unmodifiableSortedMap(SortedMap<E, V> m)
unmodifiableSortedSet(SortedSet<T> s)

同步视图

使用视图机制确保常规集合的线程安全,而不是实现线程安全的集合类。

Collections 类的静态 synchronizedMap 方法可以将任何一个映射表转换成具有同步访问方法的 Map:

Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());

//众多方法
synchronizedCollection(Collection<T> c)
synchronizedList(List<T> list)
synchronizedMap(Map<K, V> m)
synchronizedNavigableMap(NavigableMap<E, V> m)
synchronizedNavigableSet(NavigableSet<T> s)
synchronizedSet(Set<T> s)
synchronizedSortedMap(SortedMap<E, V> m)
synchronizedSortedSet(SortedSet<T> s)

此时,get 和 put 方法将是同步操作的。

受查视图 防止插入错误类型

受查视图用来对泛型类型发生问题时提供调试支持。

将错误类型的元素混入泛型集合中的问题极有可能发生,受查视图可以探测到这类问题。

定义一个安全列表:

ArrayList<String> strings = new ArrayList<>();
ArrayList rawList = strings;
rawList.add(new Date());
System.out.println(strings.get(0));//java.util.Date cannot be cast to java.lang.String

ArrayList<String> strings = new ArrayList<>();
List<String> safeStrings = Collections.checkedList(strings, String.class);
ArrayList rawList = (ArrayList) safeStrings;
rawList.add(new Date());//java.util.Collections$CheckedRandomAccessList cannot be cast to java.util.ArrayList

视图的 add 方法将检测插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个 ClassCastException 异常。

算法

我们不希望每次都测试和调用以下代码:

Static <T extends Comparable> T max(T[] a)
Static <T extends Comparable> T max(ArrayList<T> v)
Static <T extends Comparable> T max(LinkedList<T> T)

直接使用迭代器遍历元素计算最大元素,可以将 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.comparaTo(next) < 0){
            largext = next;
        }
    }
    return largest;
}

链表、数组列表或数组都可直接使用求最大元素。

排序与混排

Collections 类中的 sort 方法可以对实现了 List 接口的集合进行排序。

List<String> staff new LinkedList<>();
Collections.sort(staff);
staff.sort(Comparator.comparingDouble(Employee::getSalary));//假定实现了 Comparable 接口
staff.sort(Comparator.reverseOrder());//降序
staff.sort(Compapator.comparingDouble(Employee::getSalart).reversed());//降序

集合类库中使用的排序算法比快速排序要慢一些,快速排序是通用排序算法的传统选择。

java 中归并排序直接将所有元素转入一个数组,对数组进行排序,然后,再将排序后的序列复制回列表。

归并排序的优点:

  • 稳定,不需要交换相同的元素

可以传递的列表必须是可修改的,不必是可以改变打小的。

Collections 类的 shuffle 方法,可以随机地混排列表中元素的顺序:

ArrayList<Card> cards = ...;
Collections.shuffle(cards);

如果列表没有实现 RandomAccess 接口,shuffle 方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。

二分查找

Collections 类的 binarySearch 方法实现了二分查找。注意,集合必须排好序,否则将返回错误的答案。

要给方法提供集合和要查找的元素,如果集合没有采用 Comparable 接口的 compareTo 方法进行排序,就要还要提供一个比较器。

i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);

返回的数值>=0,则表示匹配对象的索引(c.get(i))。

返回负值,则表示没有匹配的元素。可以利用这个值,将查找的值插入到对应的排序好的位置。

采用随机访问,二分查找才有意义。如果为 binarySerach 算法提供一个链表(扩展了 AbstractSequentialList类),它将自动地变为线性查找。

简单算法

它们可以让程序员阅读算法变得很轻松。

//java.util.Collections
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp)
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
//返回集合中最小最大元素
public static <T> void copy(List<? super T> dest, List<? extends T> src)
//将列表中的所有元素赋值到目标列表的相应位置上
public static <T> void fill(List<? super T> list, T obj)
//将列表中所有位置设置为相同的值
public boolean addAll(Collection<? extends E> coll)
//将所有的值添加到集合中。集合改变返回 true
public static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
//用 newVal 取代所有值为 oldVal 的元素
public static int indexOfSubList(List<?> source, List<?> target)
public static int lastIndexOfSubList(List<?> source, List<?> target)
//返回 source 中第一个或最后一个等于 target 子列表的索引。不存在返回 -1 。存在返回出现的索引位置。
public static void swap(List<?> list, int i, int j)
//交换给定偏移量的两个元素
public static void reverse(List<?> list)
//逆序
public static void rotate(List<?> list, int distance)
//旋转列表中的元素,将 i 移动到 (i + d) % list.size()
public static int frequency(Collection<?> c, Object o)
//返回 c 中与对象 o 相同的元素个数
public static boolean disjoint(Collection<?> c1, Collection<?> c2)
//如果两个集合没有共同的元素,返回 true

批操作

coll1.removeAll(coll2);

删除出现的

coll1.retainAll(coll2);

删除未出现的

Set<String> result = new HashSet<>(a);
result.retainAll(b);

利用中转集合 result ,找出 a 和 b 两个集的交集。

Map<String, Employee> staffMap = ...;
Set<String> terminatedIDs = ...;
staffMap.keySet().removeAll(terminatedIDs);

利用视图,提供了要删除的员工的键,即可删除集合中的要删除的员工。

relocated.addAll(staff.subList(0, 10));

将一个列表的前 10 个元素增加到另一个容器。

staff.subList(0, 10).clear();

子范围可完成更多操作,删除集合的子范围的所有元素。

集合与数组的转换

数组转换为集合:

HashSet<String> staff = new HashSet<>(Arrays.asList(new String[]{...}))

集合转换为数组,获得一个对象数组,就算知道类型也不能强转:

Object[] values = staff.toArray()

集合转换为数组,返回的数组会创建相同的数组类型:

String[] values = staff.toArray(new String[0]);

集合转换为数组,返回的数组会创建相同的数组类型,构造指定大小的数组,此时不会创建新数组:

String[] values = staff.toArray(new String[staff.size()]);

编写自己的算法

写自己的算法,要使用接口,而不要具体的实现。

遗留的集合

Hashtable 类

Hashtable 类与 HashMap 类的作用一样,拥有相同的接口。Hashtable 是同步的。

现在如果对同步性和遗留代码的兼容性没有要求,就是用 HashMap,如果需要并发就使用 ConcurrentHashMap 。

枚举

静态方法 Collections.enumeration将生产一个枚举对象。

属性映射

  • 键值都是字符串
  • 表可以保存到一个文件中,也可以从文件中加载,loadstore
  • 使用一个默认的辅助表

Properties 类就是实现属性映射的类。

Stack 类,有 push 、 pop 和 peek 方法。Stack 类扩展为 Vector 类。

位集

BitSet 类存放一个位序列。

猜你喜欢

转载自www.cnblogs.com/lovezyu/p/9146471.html