Java集合入门看一篇就行

集合

在这里插入图片描述
关注公众号摸鱼汪的杂货铺回复集合学习资源获取JDK源码学习环境和API文档

1.Java集合框架

1.1集合接口与实现分离

Java集合类库也将接口(interface)与实现(implementation)分离

我们利用数据结构——队列(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;
    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;
    LinkListQueue(){...}
    public void add(E element){...}
    public E remove(){...}
    public int size(){...}
}

注意: 实际上Java类库中没有名为CircularArrayQueueLinkedListQueue的类。这里只是作为一个示例来解释集合接口与实现在概念上的区分。循环数组队列可以使用ArrayDeque类,链表队列可以使用LinkList类,这个类实现了Queue接口。

PS:

接口类型存放集合引用:

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

利用这一种方法,一旦改变了想法,就可以很轻松的实现另外一种不同的实现,只需要对程序的一个地方进行修改,即调用构造器的地方。

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

1.2 Collection接口

在Java类库中,集合类的基本接口是Collection接口。

public interface Collection<E>{
    boolean add(E element);
    Iterator<E> iterator();
    ...
}
/**
 *add方法用于向集合中添加元素。如果元素确实改变了集合就放回true;如果集合没有发生变化返回false.
 *iterator方法用于返回实现了Itertor接口的对象。可以使用这个迭代器对象依次访问集合中的元素.
 **/

1.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) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

通过反复调用next方法,可以逐个访问集合中的每个元素。如果达到集合的末尾,next方法将抛出一个NoSuchFlementException。因此,需要在调用next之前调用hasNext方法。如果迭代器还有对象访问这个方法就会返回true。

PS:“for each”循环可以处理了任何实现了Iterable接口的对象,这个接口只包含一个抽象方法

public interface Iterable<T> {
    Iterator<T> iterator();
    
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Collection接口扩展了Iterable接口 Collection extends Iterable
也可以不写循环,而是调用forEachRemaining方法并提供一个lambda表达式(它会处理每一个元素)。将对迭代器的每一个元素调用这个lambda表达式,直到再没有元素为止。

iterator.forEachRemaining(element -> do something with element);

访问元素的顺序取决于集合类型。如果迭代处理一个ArrayList,迭代器将从索引0开始,每迭代一次,索引值加1(有序)。如果访问HashSet中的元素,会按照一种基本上随机的顺序获得元素(无序)。虽然可以确保在迭代过程中能够遍历到集合中的所有元素,但是无法预知访问各个元素的顺序。

Java集合类库中迭代器与其他类库中的迭代器在概念上有着重要的区别。在传统的集合类库中,例如,C++的标准模板库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,可以查找存储在指定位置上的元素,就像如果知道数组索引i,就可以查找数组元素a[i]。不需要查找元素,也可以将迭代器向前移动一个位置。这与不需要执行查找操作而通过调用i++将数组索引向前移动一样。但是,在Java迭代器并不是这样处理的。查找操作与位置变更紧密耦合。查找一个元素的唯一方法是调用next,而在执行查找操作同时,迭代器的位置就会随之向前移动。可以认为Java迭代器位于两个元素之间,当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
在这里插入图片描述

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。
next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调用next,将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。

1.3泛型实用方法

由于Colloection与Iterator都是泛型接口,这意味着你可以编写处理任何集合类型的实用方法。

2.集合框架中的接口

在这里插入图片描述

集合有两个基本接口:Collection和Map ,可以用以下方法在集合中插入元素:

boolean add(E element);

由于映射键/值对,所以用put方法来插入:

V put(K key,V value);

要从集合读取元素,可以使用迭代器访问元素。不过,从映射中读取值则要使用get方法:

V get(K key)

List 是一个有序集合(ordered collection)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引访问。后来这种方法被称为随机访问(random access),因为这样可以按任意顺序访问元素。使用迭代器访问必须顺序地访问元素。

List接口定义了多个用于随机访问的方法:

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

PS:集合框架有两种有序集合,其性能开销有很大的差异。由数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问。与之不同,链表尽管也是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。

Set集合等同于Collection接口,不过其方法的行为更严谨的定义。集(set)的add方法不允许存在重复的元素。要适当的定义集的equals方法:只要两个集包含同样的元素就认为他们是相等的,而不是去要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。

PS:为什么要建立一个单独的接口呢?

​ 从概念上讲,并不是所有集合都是集。建立一个Set接口可以允许程序员编写只接受集的方法。

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

Java 6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法。

3.具体集合

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

在这里插入图片描述

3.1链表

数组和数组列表都有一个重大的缺陷。这就是从数组中间删除一个元素开销很大,其原因是数组中位于被删除的元素之后的所有元素都要向数组的前端移动(见图3-1)。在数组中间插入一个元素也是如此。

在这里插入图片描述

数据结构——链表

俩表就解决了数组删除元素时开销大的问题。数组是在连续存储的位置上存放对象引用,而链表则是将每个对象放在单独的链接(link)中。每个链接还存放序列中下一个链接的引用。在Java中程序设计语言中,所有链实际上都是双向链接的(doubly linked)——即每个链接还存放着其前驱的引用。(见图3-2)

在这里插入图片描述

从列表中间删除一个元素是一个很轻松的操作,只需要跟新所删除元素周围的链接即可。(见图3-3)

在这里插入图片描述

在下面代码示例中,先添加3个元素,然后再将第2个元素删除:

List<String> staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator<String> iter = staff.iterator();
String first = iter.next();//visit first element
String second = iter.next();//visit second element
iter.remove();//remove last visited element

链表与泛型集合之间有一个重要的区别。链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的底部。但是,常常需要将元素添加到链表中间。由于迭代器描述了集合中位置,所以这种依赖与位置的add方法将有迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。集(set)数据类型中,元素是完全无序的。

Iterator接口中没有add方法。实际上,集合类库提供了一个子接口ListIterator,其中包含add方法。
与Collection.add不同,这个方法不返回boolean类型的值,他假定add操作总会改变链表。
ListIterator接口两个方法,可以用来反向遍历链表。

E previous()
boolean hashPrevious()

与next方法一样,previous方法返回越过对象.

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

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

add方法在迭代器位置之前添加一个新对象。例如,下面的代码将越过链表中的第一个元素之前添加“Juliet”

List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iterator = staff.listIterator();
String next = iterator.next();//skip past first element
System.out.println(next);
iterator.add("Juliet");
System.out.println(Arrays.toString(new List[]{staff}));

如果多次调用add方法,将按照提供的次序把元素添加到链表中。他们被依次添加到迭代器当前位置之前。

在这里插入图片描述

但是如果调用List的add方法则会在集合的尾部添加,LinkedList有序序列。

List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
ListIterator<String> iterator = staff.listIterator();
String next = iterator.next();//skip past first element
System.out.println(next);
staff.add("Juliet");
System.out.println(Arrays.toString(new List[]{staff}));

如果链表有n个元素,会有n+1个位置可以添加新元素。这些位置与迭代器的n+1个可能的位置相对应。例如,如果链表包含3个元素,A、B、C,就有4个位置(标为1)可以插入元素:

在这里插入图片描述

PS:在用”光标“类比时要格外小心。remove操作与Backspace键的工作方式不太一样。在调用next之后,remove方法确实与Backspace键一样会删除迭代器左侧的元素。但是,如果调用了previous,就会将右侧的元素删除。不能连续调用两次remove。

add方法只依赖于迭代器的位置,而remove方法不同,他依赖于迭代器的状态。
set方法用于一个新元素替换调用next或previous方法返回的上一个元素。

ListIterator<String> iter = list.listIterator();
String next = iter.next();
iter.set(newValue);

​ 如果在某个迭代器修改集合时,另一个迭代器却在遍历这个集合,那么一定会出现混乱。例如,假设一个迭代器指向一个元素前面的位置,而另一个迭代器刚刚删除了这个元素,现在前一个迭代器就是无效的,并且不能再使用。链表迭代器设计为可以检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个ConcurrentModificationException异常。

List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next();//throws ConcurrentModificationException

由于iter2检测出这个链表从外部修改了,所以对iter2.next的调用抛出一个ConcurrentModificationException异常。
为了避免发生并发修改异常,请遵循这样一个简单的规则:可以根据需要为一个集合关联多个迭代器,前提是这些迭代器只能读取集合。或者,可以关联一个能同时读写的迭代器。
有一种简单的方法可以检测到并发修改。集合可以跟踪到更改操作(诸如添加或删除元素)的次数。每个迭代器都会为它负责的更改操作维护一个单独的更改操作数。在每一个迭代器方法的开始处,迭代器会检测他自己的更改操作数是否与集合的更改操作数相等。如果不一致,就抛出一个ConcurrentModificationException异常。

PS:链表只跟踪对列表的结构性修改,例如,添加和删除链接。set方法被视为结构性修改。可以为一个链表关联多个迭代器,所有迭代器,都调用set方法修改现有链接的内容。

​ 链表并不支持快速随机访问。如果要查看链表中的第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个原因,需要按整数索引访问元素时,程序员通常不会选用链表。
​ 不过,LinkedList类还是提供了一个用来访问某个特定元素的get方法:

LinkedList<String> list = ...;
String obj = list.get(n);

当然,这个方法的效率并不高。绝对不应该使用这个”虚假“的随机访问方法来遍历列表。下面这段代码的效率极低:

for (int i = 0; i < list.size(); i++) {
	/*do something with*/ list.get(i);            
}

每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本没有缓存位置信息。

PS:get方法做了一个微小的优化;如果索引大于等于size()/2,就从列表尾端开始搜索元素。

​ 列表迭代器接口还有一个方法,可以告诉你当前位置的索引。由于Java迭代器指向两个元素之间的位置,所以可以有两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex方法返回下一次调用previous方法时所返回的整数索引。这两个方法的效率非常高,因为有一个迭代器保持着当前位置的计数值。
​ 如果链表中只有很少几个元素,就完全没有必要为get方法和set开销而烦恼。但是,最初为什么使用链表呢?使用链表唯一理由时尽可能地减少在链表中间插入或删除元素的开销。如果链表只有很少几个元素,就完全可以使用ArrayList。
​ 建议避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,就使用数组或ArrayList,而不是使用链表。

程序清单:

package cn.aojiaoge.test.learnDome;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

/**
 * @author aojiaoge
 * @date 2021/1/26
 */
public class LinkedListTest {
    public static void main(String[] args) {
        List<String> a = new LinkedList<>();
        a.add("Amy");
        a.add("Carl");
        a.add("Erica");

        List<String> b= new LinkedList<>();
        b.add("Bob");
        b.add("Doug");
        b.add("Frances");
        b.add("Gloria");

        //merge the words from b into a

        ListIterator<String> aIter = a.listIterator();
        Iterator<String> bIter = b.iterator();

        while (bIter.hasNext()){
            if (aIter.hasNext()) aIter.next();
            aIter.add(bIter.next());
        }
        System.out.println(a);

        //remove every second word from b
        bIter = b.iterator();
        while (bIter.hasNext()){
            bIter.next();//skip one element
            if (bIter.hasNext()){
                bIter.next();//skip next element
                bIter.remove();//remove that element
            }
        }
        System.out.println(b);

        //bulk operation: remove all words in b from a
        a.removeAll(b);
        System.out.println(a);
    }
}

在程序清单中的程序具体使用了链表。他创建了两个列表,将它们合并在一起,然后从第二个列表中每间隔一个元素删除一个元素,最后测试removeAll方法。建议追踪一下程序流程,要特别注意迭代器。
注意调用:

System.out.println(a);

这回调用AbstracColletion类中的toString方法打印链表a中的所有元素。

3.2数组列表

List接口用于描述一个有序集合,并且集合中的每个元素的位置都很重要。有两种访问元素的协议:一种是通过描述迭代器,另一种是通过get和set方法随机地访问每个元素。后者不适用于链表,但get和set方法对数组很有用。ArrayList封装了一个动态分配地对象数组。

PS:在需要动态数组时,可能会使用Vector类。为什么要使用ArrayList而不是Vector呢?原因很简单:Vector类地所有方法都是同步的。可以安全地从两个线程访问一个Vector对象。但是,如果只从一个线程访问Vector(这种情况更为常见),代码就会在同步操作上白白浪费大量的时间。而与之不同,ArrayList方法不是同步的,因此,建议在需要同步时使用ArrayList,而不要使用Vector

3.3散列集

​ 链表和数组允许你根据意愿指定元素的次序。如果不在意元素的顺序,有几种能够快速查找元素的数据结构。前期缺点就是无法控制元素出现的次序。

​ 有一种众所周知的数据结构,可以用于快速地查找对象,这就是散列表(hash table)。散列表为每个对象计算一个整数,成为散列码(hash code)。散列码是由对象的示例字段得出的一个整数。更准确地说,有不同数据地对象将产生不同地散列码。下表中列出了几个散列码的示例,它们是有String类的hashCode方法产生的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCe5iXQN-1612690630260)(E:\javaSource\demo\hashCode方法得到的散列码.png)]

​ 如果定义你自己的类,你就要负责实现自己的hashCode方法。注意,你的实现应该与equals方法兼容,即如果a.equals(b)为true,a与b必须有相同的散列码。
​ 最重要的问题是要能够快速地计算出散列码,并且这个计算只与计算散列的那个对象状态有关,与散列表中的其他对象无关。
​ 在Java中,散列表用链表数组实现。每个列表被称为桶(bucket,见图3-5)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数区域,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的散列码为76268,并且有128个桶,那么这个对象应该保存在第108号桶中(因为76268%128的余数是108).或许很幸运在这个桶中没有其他元素,此时将元素直接插入到桶中就可以了。当然,有时会遇到桶已经被填充的情况。这种现象被称为散列冲突(hash collision)。这时,需要将新对象与桶中的所有对象进行比较,查看这个对象与桶中的所以对象进行比较,查看这个对象是否已经存在。如果散列码合理地随机分布,桶地数目也足够大,需要比较的次数就会很少。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29WGL7wt-1612690630262)(E:\javaSource\demo\3-5散列表.png)]

PS:在Java 8中,桶满时会从链表变为平衡二叉树。如果选择地散列函数不好,会产生很多冲突,或者如果有恶意代码试图在散列表中填充多个有相同散列码地值,这样改为二叉树能提高性能。

​ 如果想要控制散列表的性能,可以指定一个初始的桶数。桶数是指用于收集有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突数量,降低检索性能。
​ 通常将桶数设置为预计元素个数的75%~150%。有一些人认为:最好将桶数设置为一个素数( 素数就是质数,一个大于1的自然数,除了1和它自身外,不能整除其他自然数的数叫做质数,即素数;否则称为合数。 ),以防止键的聚集。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将自动的转换为2的下一个幂值)。
​ 如果散列表太慢,就需要再散列(rehashed)。如果要对散列再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子(load factor)可以确定何时对散列表进项再散列。例如,如果装填因子为0.75(默认值),说明已经填满了75%以上,就会自动再散列,新表的桶数是原来的两倍。对于大多数应用程序来说,装填因子为0.75是合理的。
​ 散列表可以用于实现很多重要的数据结构。其中最简单的是集类型。集是没有重复元素的元素集合。集的add方法首先在这个集中查找添加的对象,如果不存在,就添加这个对象。
​ Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以使用add方法添加元素。contains方法已经被重新定义,用来快速查找某个元素是否已经在集中。它只查看一个桶中的元素,而不必查看集合中的所有元素。
​ 散列集迭代器将依次访问所有的桶。由于散列将元素分散在表中,所以会以一中看起来随机的顺序访问元素。只是不关心集合中元素的顺序时在应该使用HashSet。

程序清单:

package cn.aojiaoge.test.learnDome;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

/**
 * @author aojiaoge
 * @date 2021/1/26
 */
public class HashSetTest {
    public static void main(String[] args) {
        Set<String> words = new HashSet<String>();
        long totalTime=0;

        try (Scanner in = new Scanner(System.in)){
            while(in.hasNext()){
                String word = in.next();
                long l = System.currentTimeMillis();
                words.add(word);
                long l1 = System.currentTimeMillis() - l;
                totalTime+=l1;
            }
        }

        Iterator<String> iter = words.iterator();
        for (int i = 0; i <= 20 && iter.hasNext(); i++) {
            System.out.println(iter.next());
            System.out.println("......");
            System.out.println(words.size()+"distinct words."+totalTime+"milliseconds.");
        }
    }
}

3.4树集

​ 树集是一个有序集合(sorted collection)。可以任意顺序将元素插入到集合中。在对集合进行遍历时,值将自动地按照排序后地顺序呈现。
TreeSet类与散列集十分类似,不过它比散列集有所改进。例如,假设插入3个字符串,然后访问添加的所有元素。

TreeSet<String> sorter = new TreeSet<>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s : sorter) {
    System.out.println(s);
}

这时,值将按照有序顺序打印:Amy Bob Carl。正如TreeSet类名所示,排序是用一个树数据结构完成的(当前使用的是红黑树(red-black tree)。
每次将一个元素添加到树中时,都会将其放置在正确的排序位置上。因此,迭代器总是以有序的顺序访问每一个元素。

​ 将一个元素添加到树中要比添加到散列表中慢,见表“将元素添加到散列集和树集”中的比较,但是,与检查数组或链表中的重复元素相比,使用树会快很多。
在这里插入图片描述

PS:要使用树集,必须能够比较元素。这些元素必须实现接口Comparable接口,或者构造集时,必须提供一个Comparator。

​ 树的排序顺序必须是全序。也就是说任意两个元素时必须可比的,并且只有两个元素相等时结果才为0。

PS:从Java6起,TreeSet类实现了NavigableSet接口。这个接口增加了几个查找元素以及反向遍历的便利方法。

程序清单:
创建了Item对象的两个树集。第一个按照部件编号排序,这是Item对象的默认排序顺序。第二个通过使用一个定制的比较器按照描述信息排序。

import java.util.Objects;

/**
 * @author aojiaoge
 * @date 2021/2/7
 */
public class Item implements Comparable<Item>{
    
    private String description;
    private int partNumber;

    public Item(String description, int partNumber) {
        this.description = description;
        this.partNumber = partNumber;
    }

    public String getDescription() {
        return description;
    }

    @Override
    public String toString() {
        return "Item{" +
                "description='" + description + '\'' +
                ", partNumber=" + partNumber +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Item item = (Item) o;
        return partNumber == item.partNumber &&
                Objects.equals(description, item.description);
    }

    @Override
    public int hashCode() {
        return Objects.hash(description, partNumber);
    }

    @Override
    public int compareTo(Item o) {
        int diff = Integer.compare(partNumber, o.partNumber);
        return diff != 0 ? diff : description.compareTo(o.description);
    }
}
import java.util.Comparator;
import java.util.TreeSet;

/**
 * @author aojiaoge
 * @date 2021/2/7
 */
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet<Item> parts = new TreeSet<>();
        parts.add(new Item("Toaster",1234));
        parts.add(new Item("Widget",4562));
        parts.add(new Item("Modem",9912));
        System.out.println(parts);

        TreeSet<Item> sortByDescription = new TreeSet<>(Comparator.comparing(Item::getDescription));
        sortByDescription.addAll(parts);
        System.out.println(sortByDescription);
    }
}

3.5队列与双端队列

​ 队列允许你高效的在尾部添加元素,并在头部删除元素。双端队列(deuqe)允许在头部和尾部都高效地添加或删除元素。不支持在队列中间添加元素。Java6中引入了Deque接口,ArrayDeque和LinkedList类实现了这个接口。这两个类都可以提供双端队列,其大小可根据需要扩展。

3.6优先队列

​ 优先队列(priority queue)中的元素可以按照任意顺序插入,但会按照有序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先队列中最小的元素。不过,优先队列并没有对所有元素进行排序。如果迭代处理这些元素,并不需要对他们进行排序。优先队列使用了一个精巧且高效的数据结构,称为堆(heap)。堆是一个自组织的二叉树,其添加(add)和删除(remove)操作可以让最小元素移动到根,而不必花费时间对元素进行排序。
​ 与TreeSet一样,优先队列既可以保存实现了Comparable接口的类对象,也可以保存构造器中提供的Comparator对象。
​ 优先队列的典型用法是任务调度。每一项任务有一个优先级,任务随机顺序添加到队列中。每当启动一个新的任务是,都将优先级最高的任务从列表中删除。

程序清单:

import java.time.LocalDate;
import java.util.PriorityQueue;

/**
 * @author aojiaoge
 * @date 2021/2/7
 */
public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue<LocalDate> localDates = new PriorityQueue<>();
        localDates.add(LocalDate.of(1906,12,9));
        localDates.add(LocalDate.of(1815,12,10));
        localDates.add(LocalDate.of(1903,12,3));
        localDates.add(LocalDate.of(1910,6,22));

        System.out.println("-------------------------");
        for (LocalDate localDate : localDates) {
            System.out.println(localDate);
        }
        System.out.println("-------------------------");
        while(!localDates.isEmpty()) System.out.println(localDates.remove());
    }
}

4.映射

集是一个集合,允许你快速的查找现有的元素。我们知道某些关键信息,希望查找与之关联的元素。映射(map)数据结构就是为此设计的。映射用来存放键/值对。如果提供了键,就能够查找到值。

4.1基本映射操作

​ Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类实现了Map接口。
​ 散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。与键关联的值不进行散列或比较。
​ 应该选择散列映射还是树映射?与集一样,散列稍微快一些,如果不需要按照有序的顺序访问键,最好进行散列映射。
​ 一下代码建立一个散列映射来存储员工信息:

HashMap<String, Employee> staff = new HashMap<>();
Employee harry_hacker = new Employee("Harry Hacker");
staff.put("987-98-9996",harry_hacker);

每当忘映射中添加一个对象时,必须同时提供一个键。在这里,键就是一个字符串,对应的值是Employee对象。
要想检索一个对象,必须使用(因而,必须记住)键。

String id = "987-98-9996";
Employee e = staff.get(id);//gets harry_hacker

如果映射中没有存储与给定键对应的信息,get将返回null。
null返回值可能并不方便。有时对应没有出现在映射中的键,可以使用一个好的默认值。然后使用getDefault方法。

HashMap<String, Integer> scores = new HashMap<>();
Integer score = scores.getOrDefault(id, 0);//gets 0 if id is not present

​ 键必须是唯一的。不能对同一个键存放两个值。如果对同一个键调用两次put方法,第二个值就会取代第一个值。实际上,put将返回与这个键参数关联的上一个值。
​ remove方法从映射中删除给定键对应的元素。size方法返回映射中的元素数。
​ 要迭代处理映射的键和值,最容易的方法是调用forEach方法。可以提供一个接收键和值的lambda表达式。映射中的每一项会依次调用这个表达式。

scores.forEach(k,v) -> System.out.println("key="+k+",value="+v);

程序清单:
从映射中删除一个键,同时与之关联的也会删除。修改与某一个键关联的值,并调用get方法查找一个值。

import java.time.LocalDate;

/**
 * @author aojiaoge
 * @date 2021/2/8
 */
public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name){
        this.name=name;
    }

    public Employee(String name, double salary, LocalDate hireDay) {
        this.name = name;
        this.salary = salary;
        this.hireDay = hireDay;
    }

    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary+=raise;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void setHireDay(LocalDate hireDay) {
        this.hireDay = hireDay;
    }
    
    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                '}';
    }
}
import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        HashMap<String, Employee> staff = new HashMap<>();
        staff.put("144-25-5464",new Employee("Amy Lee"));
        staff.put("567-24-2546",new Employee("Harry Hacker"));
        staff.put("157-62-7935",new Employee("Gary Cooper"));
        staff.put("456-62-5527",new Employee("Francesca Cruz"));

        //print all entries
        System.out.println(staff);
        ///remove an entry
        staff.remove("567-24-2546");
        //replace an entry
        staff.put("456-62-5527",new Employee("Francesca Miller"));
        //look up a value
        System.out.println(staff.get("157-62-7935"));
        //iterate through all entries
        staff.forEach(k, v) -> System.out.println("key="+k+", value="+v);
    }
}

4.2更新映射条目

​ 正情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个特殊情况,即键第一次出现。下面来看一个例子,考虑使用映射统计一个单词再文件中出现的频度。看到一个单词(word)时,我们统计计数器增1,如下所示:

counts.put(word,counts.get(word)+1);

​ 这是可以的,不过有一种情况除外:就是第一次看到word时。在这种情况下,get会返回null,因此会出现NullPointerException异常。
​ 一种简单的补救是使用getDefault方法:

counts.put(word,counts.getDefault(word,0)+1);

​ 另一种方法是首先调用putIfAbsent方法。只有当键原先存在(或者映射到null)时才会放入一个值。

counts.putIfAbsent(word,0);
counts.put(word,counts.get(word)+1);//now we know that get will succeed

​ 不过还可以做更好。merge方法可以简化这个常见操作。如果键原先不存在,下面的调用:

counts.merge(word,1,Integer::sum);

将把word与1关联,否则使用Integer::sum函数组合原值和1(也就是将原值与1就和)。

4.3映射视图

​ 集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键/值对集合,或者是按键索引的值集合。)不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。
​ 有三种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。下面方法:

Set<k> keySet()
Collection<v> values()
Set<Mao.Entry<k,v>> entrySet()

会分别返回这3个视图。(映射条目集的元素是实现了Map.Entry接口的类的对象。)
需要说明的是,KetSet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象。Set接口扩展了Collection接口。因此,可以像使用任何集合一样使用ketSet。
例如,可以枚举一些映射的所有键:

Set<String> keys = map.keySet();
for(String key:keys) {/*do somthing with key*/}

如果相同时查看键和值,可以通过枚举映射条目来避免查找值。使用以下代码:

for(Map.Entry<String,Employee> entry : staff.entrySet()){
    String k = entry.getKey();
    Employee v = entry.getValue();
    //do something with k,v
}

​ 如果在键集视图上调用迭代器的remove方法,实际上会从映射中删除这个键和与它关联的值。不能向键集视图中添加元素。如果添加一个键而没有添加值也没有意义的。如果试图调用add方法,它会抛出一个UnsupportedOperationException。映射条目集视图有同样的限制,尽管理论上增加一个新的键/值对好像有意义。

4.4弱散列映射

​ 设计WeakHashMap类是为了解决一个有趣的问题。如果有一个值,它对应的键已经不再再程序中的任何地方使用,将会出现什么情况呢?假定对某个键的最后一个引用消失,那么不再有任何途径可以引用这个值的对像了。但是,由于程序中任何部分不会再有这个键,所以,无法从映射中删除这个键/值对。为什么垃圾回收器不能删除它呢?删除无用的对象不就是垃圾回收的工作吗?
​ 遗憾的是,事情没有这么简单。垃圾回收器会跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,他们不能被回收。因此,需要由程序负责从 长期存活的映射表中删除那些无用的值。你可以使用WeakHashMap。当对键的唯一引用来自散列表映射条目时,这个数据结构将与垃圾回收器协同工作一起删除键/值对。
​ WeakHashMap使用弱引用(weak references)保存键。WeakReference对象包含另一个对象的引用,在这里,就是一个散列表键。对于这种类型的对象,垃圾回收器采用一种特有的方式进行处理。正常情况下,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器也会将其回收,但会将引用这个对象的弱引用放入一个队列。WeakHashMap将周期性地检查队列,以便找出新添加地弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已回收。于是,WeakHashMap将删除相关联地映射条目。

4.5链接散列集与映射

​ LinkedHashSet和LinkedHashMap类会记住插入元素项地顺序。这样就可以避免散列表中地项看起来顺序时随机地。再表中插入元素项时,就会并入到双向链表中。
在这里插入图片描述

​ 链接散列映射可以使用访问顺序而不是插入顺序来迭代处理映射条目。每次调用get或put时,收到影响的项将从当前地位置删除,并放入到项链表地尾部(只影响在链表中的位置,而散列表的桶不受影响。映射条目总是在键散列码对应的桶中)。要构造这样一个散列映射,需要调用

LinkedHashMap<K,V>(initialCapacity,loadFactor,true)

​ 访问顺序对于实现缓存的"最近最少使用"原则十分重要。例如,你可能希望将访问频率高的元素存放在内存中,而访问频率低的元素从数据库中读取。当在表中找不到元素项而且表已经相当满时,可以得到表的一个迭代器,并删除它枚举的前几个元素。这些项是近期最少使用的几个元素。
​ 甚至可以让这一过程自动化。构造linkedHashMap的一个子类,然后覆盖下面这个方法:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest)

​ 每当你的方法返回true时,添加一个新映射条目就会导致删除eldest项。例如,下面的缓存最多可以存放100个元素:

new LinkedHashMap<K,V>(128,0.75f,true){
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size()>100;
    }
}

​ 或者,还可以考虑eldest元素,来决定是否将它删除。例如,可以检查与这一项一起存储的时间戳。

4.6枚举集与映射

​ EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。
​ EnumSet类没有公共的构造器。要使用静态工厂方法构造这个集:

/**
 * @author aojiaoge
 * @date 2021/2/9
 */
public enum Weekday {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}
import java.util.EnumSet;

/**
 * @author aojiaoge
 * @date 2021/2/9
 */
public class TEST {
    public static void main(String[] args) {
        EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
        EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
        EnumSet<Weekday> workday = EnumSet.range(Weekday.MONDAY, Weekday.FRIDAY);
        EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY);
    }
}

​ 可以使用Set接口的常用方法来修改EnumSet。
​ EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地实现为一个值数组。需要在构造器中指定键类型:

EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);

PS:在EnumSet的API文档中,将会看到形如E extends Enum的奇怪类型参数。简单地说,它地意思是"E是一个枚举类型。"所有地枚举类型都扩展了泛型Enum类。例如,Weekday扩展了Enum。

4.7标识散列映射

​ 类IdentityHashMap有特殊地用途。在这个类中,键的散列值不是用hashCode函数计算,而是用System.identityHashCode方法计算的。这是Object.hashCode根据对象的内存地址计算散列码时所使用的方法。而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不是使用equals。
​ 也就是说,不同的键对象即使内容相同,也被视为不同的对象。在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以来跟踪那些对象已经遍历过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值