集合

基本内容

  • java集合框架
  • 具体的集合
  • 映射
  • 视图与包装器
  • 算法
  • 遗留的集合

java集合框架

将集合的接口与实现分离

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

首先,看下熟悉的数据结构–队列(queue)是怎样分离的.

队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找元素中的个数.
(当需要收集对象,并按照"先进先出"的规则检索对象时就应该使用队列)

队列接口的最简形式类似下面这样:

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;
          
      }
    
  • 一种是使用链表

      通过一个实现Queue的接口表示
       public class LinkedListQueue<E> implements Queue<E>
      {
          private int head;
          private int tail;
          
          LinkedListQueue(int capacity){
              ...
          }
          public void add(E element){
              ...
          }
          public E remove(){
              ...
          }
          public int size(){
              ...
          }
          
      }
    

以上的两个类只是事例,如果需要一个链表数组,就直接使用LinkedList类
如果需要一个循环数组,就可以使用ArrayDeque类

循环数组往往比链表更高效,大多数人会选择循环数组,但是循环数组是一个有限集合,即容量有限,如果程序要收集的对象数量没有上线,就最好用链表来实现

如果想要实现自己的队列类(也许不太可能),会发现扩展AbstractQueue会比实现Queue接口中的方法轻松地多
Collection接口

在java类库中,集合的基本接口是Collection接口
(这个接口有两个基本方法)

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

当然除了这两个基本方法,还有几个方法

add方法:用于往集合中添加元素,如果添加元素确实改变了集合就返回true,否则就返回false

例如:如果向集中添加重复的对象,这个添加请求就没有实效,因为集中不允许有重复的对象

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

迭代器

Iterator包含四个方法:

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

如果想要查看集合中的所有元素,就需要先对集合元素进行判断(在hasNext返回之前反复的调用next方法)

Collection c = …;
Iterator iter = c.iterator();

while(iter.hasNext){
String element = iter.next();

}

用for each循环可以更简练的表示同样的循环操作

for(String element :c){

}

编译器简单的将for each 循环翻译为带有迭代器的循环

Collection接口扩展了Iterable接口,因此,对于标准类库的任何集合都可以使用"foreach"循环

在javaSE8中,甚至不用写循环,可以调用forEachRemaining方法并提供一个leabda表达式(他会处理一个元素)

iterator.forEachRemaining(element ->do some with element)
  • 元素被访问的顺序取决于集合类型,如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加一
  • 如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现,虽然可以确定迭代过程中能够遍历到集合中的所有元素,但是无法预知元素被访问的顺序

java集合类库的迭代器与其他类库的迭代器在概念上有着重要的区别

例如在c++中,迭代器是根据数组索引建模的,就像知道数组索引i就可以查看
数组元素a[i]一样,不需要查找元素,就可以将迭代器向前移动一个位置

java的迭代器并不是这样操作的,查找操作与位置变更是紧密相连的,查找数组的唯一方法是调用next,
而在执行查找操作的同时,迭代器的位置随之向前移动

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

Iterator接口的remove方法

next方法与remove方法具有互相依赖性,如果调用remove之前没有调用next方法将是不合法的,如果这样做,将会抛出IllegalStateException异常

如果想删除两个相同的元素,不能直接的这样调用

it.remove();
it.remove();//Error!!

相反的,必须先调用next越过将要删除的元素

it.remove();
it.next();
it.remove();//这样是正确的!!
泛型实用方法

由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法

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

java类库的设计者认为:这些使用方法很有用,应该将他们提供给用户使用,这样类库的使用者就不必重新构建这些方法了

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

但是,如果实现Collection接口的每一个类都要提供如此多的例行方法将是一件很烦人的事情,为了让实现者更轻易的实现这个接口,Java类库提供了一个AbstrctCollection,他将基础方法size和iterator抽象化了

现在要由具体的集合类提供iterator方法,而contains方法已经有超类提供了

对于javaSE8,这种方法有些过时了,如果这些方法是Collection接口会更好,但实际上不是这样,不过确实已经增加了很多默认方法,其中很多方法都与流的处理有关

集合框架中的接口

java集合框架为不同类型的集合定义了大量接口

  • 集合有两个基本接口:Collection和Map

    我们已经看到,可以用以下方法在集合中插入元素:

    boolean add(E element)

    不过,由于包含键/值对,所以要用put方法来插入:

    V get(K key,V value)

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

    V get(K key)

  • List是一个有序集合(ordered collection),元素会增加到容器中的特定位置

    可以采用两种方式访问元素:1.使用迭代器访问 2.使用一个整数索引来访问

    后一种方式称为随机访问(random access),因为这样可以按任意顺序访问元素,与之不同,使用迭代器必须顺序的访问元素

    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 element)

    集合框架在这一部分设计的很不好

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

  • Set接口等同于Collection接口,不过其方法有更严谨的定义.

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

从概念上来讲,集合并不是集,建立一个set接口可以让程序员编写只接受集的方法

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

javaSE6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法TreeSet和TreeMap类实现了这些接口

具体的集合

java类库的集合(省略了线程安全集合)

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

链表

有很多示例使用了数组以及动态的ArrayList类,然而,数组和数组列表都有一个重大的缺陷(从数组的中间位置删除一个元素要付出很大的代价,数组位于被删除元素之后的所有元素都要向数组的前端移动)

数据结构-链表(linked list) 解决了这个问题,尽管数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用.

在java设计语言中,所有的链表实际上都是双向链接的(doubly linked)—每个节点还存放着指向前驱节点的引用,从立案表中间删除一个数据十分简单,即只需要更新被删除元素附近的链接

但是,链表与泛型集合之间有一个重要的区别,链表是一个有序集合(ordered collection),每个对象的位置十分重要,LinkedList方法将对象添加到链表的尾部,但是很多时候我们需要将元素添加到中部,由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责,但是只有对自然有序的集合使用迭代器添加元素才有意义,(例如:集(Set)类型,其中的元素完全无序),因此,在Iterator接口中就没有add方法,相反的,集合类库提供了子接口ListIterator,其中包含add方法

与Collection.add的不同

ListIterator.add方法不返回boolean类型的值,他假定添加操作总会改变链表

另外:ListIteartor接口有两个方法,可以用来反向遍历链表

E previous()
boolean hasPrevious()

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

LIinkedList类的listItreartor方法返回一个实现了ListIterator接口的迭代器的对象

当用一个刚刚有Iterator方法返回,并且指向链表表头的迭代器调用add操作时,
新添加的元素将会变成列表的新表头,当迭代器越过链表的最后一个元素时
(即hasNext返回false),添加的元素将会变成列表的新表尾
需要注意的是,add方法只依赖迭代器的位置,remove方法依赖迭代器的状态
链表迭代器的设计使他能够检测到集合的修改,并抛出并发修改异常(Concurrent ModificationException)
为了避免并发修改的异常,请遵循以下简单规则
  • 可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表
  • 另外再附加一个既能读又能写的迭代器

有一种简单的方法可以检测出来并发修改的问题,集合可以跟踪改写操作(诸如添加或删除元素)的次数,每个迭代器都维护一个独立的计数值,在每个迭代器方法的开始处检查自己改写的操作的计数值是否与集合改写的计数值一致,如果不一致,抛出一个ConcurrentModificationException异常,但是!!!只能检测结构性的修改,set方法不被视为结构性修改,所以,可以将多个迭代器附加给一个链表,Collections类的许多算法都需要使用这个功能

在java类库中,还存在许多在理论上存在一定争议的方法.链表不支持快速的随机访问,如果要查看第n个元素,就必须从头开始越过n-1个元素,鉴于这个原因在程序需要采用整数索引访问元素时,程序员通常不会使用链表

尽管如此,LinkedList还是提供了一个用来访问某个特定元素的get方法

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

!!! 这个方法的效率不太高,当发现自己在使用这种方法的时候,肯定是对于要解决的问题采用了错误的数据结构

!!! 绝对不要用下面这种令人误解的随机访问方式来遍历列表,下面这段代码效率极低

for(int i= 0;i<size();i++)

list.get(i)

每次查找一个元素就要从头部开始搜索,LinkedList对象根本不做任何缓存信息的操作

列表迭代器接口还有一个方法,可以告知当前位置的索引

  • 实际上从概念上来讲,java迭代器指向两个元素之间的位置,所以可以产生两个索引,nextIndex方法返回下一次调用next方法时返回元素的整数索引;previousIndex方法返回下一次调用previous方法时返回元素的整数索引(这两个方法的效率非常高,这是因为迭代器保持着当前位置的计数值)
如果链表中只有几个元素,就完全可以使用ArrayList
        package linkedlist;
    
    import java.util.Iterator;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.ListIterator;
    
    
    /**
     * 这是用来跟随链表的工作流程
     * @author Conan
     */
    public class LinkedListTest {
    
        public static void main(String[] args) {
    
            List<String> a = new LinkedList<>();
    
            a.add("any");
            a.add("body");
            a.add("cri");
    
            List<String> b = new LinkedList<>();
    
            b.add("echo");
            b.add("didi");
            b.add("force");
    /**
     *  LinkedList的listIterator方法返回实现了ListIterator接口的迭代器对象
     */
            ListIterator<String> alter = a.listIterator();
            Iterator<String> bIter = b.iterator();
    
     /**
      *  逻辑:如果b集合还有下一个元素,并且a集合的迭代器越过一位,在后一位添加上b元素
      */
            while (bIter.hasNext())
            {
                if (alter.hasNext()) {
                    alter.next();
                }
                alter.add(bIter.next());
            }
    
            System.out.println(a);
    
    /**
    * 接下来从b集合中删除第二个元素
    */
    
            bIter = b.iterator();
    
            while (bIter.hasNext()){
    
    /**
     * 越过第一个元素
     */
                bIter.next();
    
                if(bIter.hasNext()){
    
                    bIter.next();
                    bIter.remove();
    
                }
            }
            System.out.println(b);
    
    /**
     * 把a中关于b集合的元素全部删除
     */
    
            a.removeAll(b);
    
            System.out.println(a);
        }
    
    
    
    }
数组列表

List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要

有两种访问元素的协议

  • 一种是用迭代器
  • 一种是用get和set方法随机的访问每个元素

后者不适用于链表,但是对数组很有用

集合类库提供了ArrayList类,这个类也实现了List接口,ArrayList封装了一个动态再分配的对象数组

散列集

链表和数组可以按照人们的意愿排列元素的次序,但是,如果想查看某个元素,但又忘记了所在元素的位置,就需要访问所有元素,直到找到为止

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

有一种众所周知的数据结构,可以快速的查找所需的对象,这就是散列表(hash table)

散列表为每一个对象计算一个整数,称为散列码(hash code)

散列码是由对象的实例域产生的一个整数,具有不同数据域的对象将产生不同的散列码

如果自定义类,就要负责实现这个类的hashcode方法

在java中,散列表用链表数组实现,每个列表被称为桶(bucket),要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引

例如:某个对象的散列码为76268,并且有128个桶,对象应该保存在108号桶中
或许会很幸运,在这个桶中没有其他元素,此时将元素直接插入到桶中,

有时候遇到桶被占满的情况,这也是不可避免的,这种现象称为散列冲突(hash collision)

这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在,如果散列码是合理且随机分布的,桶的数目足够大,需要比较的次数就会很少

javaSE8中,桶满时会从链表变成平衡二叉树

如果想更多的控制散列表的运行性能,就要指定一个初始的桶数,桶数是指用于收集具有相同散列值的桶的数目,如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能(最好将桶数设置成素数,以防止键的集聚)

如果散列表太满,就需要再散列(rehashed)

如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入这个新表中,然后丢弃原来的表

装填因子(load factor)决定何时对散列进行再散列

对于大多数应用程序来说,装填因子为0.75是比较合理的

散列表可以用于实现几个重要的数据结构,其中最简单的是set类型,set是没有重复元素的元素集合

Java集合类库提供了一个HashSet类,他实现了基于散列表的集,可以用add方法添加元素

散列集迭代器将以此访问所有的桶,由于散列将元素分散在标的各个位置上,所以访问它们的顺序几乎是随机的(只有不关心元素中的顺序才应该使用HashSet)

在更改集中的元素时要格外小心,如果元素的散列码发生了变化,元素在结构中的位置也会发生变化

    package set;
    
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * @author Conna
     */
    public class SetTest {
        public static void main(String[] args) {
    
            Set<String> words = new HashSet<>();
    
            words.add("app");
            words.add("pdd");
            words.add("lbw");
            words.add("cxq");
    
    
    /**
     * 输出结果不按照添加顺序
      */
            System.out.println(words);
    
        }
    
    }

树集

TreeSet类与散列集十分类似,但它比散列集有所改进,树集是一个有序集合(sorted collection),可以以任意顺序将元素插入到集合中,在对集合进行遍历时,每个值将自动的按照排序后的顺序呈现,

正如TreeSet类名所示,排序是用树结构完成的(当前实现是使用的红黑树),每次将一个元素添加到树中时,都被放置在正确的排序的位置上,因此,迭代器总是以排好序的顺序访问每个元素

将一个元素添加到树中要比添加到散列当中慢,但是,与检查数组或者链表中的重复元素相比还是快很多,如果一棵树包含1000个元素,添加一个新元素大概需要比较10次

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

是否使用树集代替散列集? 应该取决于所要具体收集的数据,如果不需要对数据进行排序,就没有必要付出排序的开销,而且对某些数据来说,对其排序要比散列函数更为困难

要想具体的了解他们之间的关系,还需要研究一个收集矩形类的任务

如果使用TreeSet,就需要提供Compaarator

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

Rectangle类已经定义了散列函数,他直接对坐标进行散列

从javaSE6开始,TreeSet类实现了NavigableSet接口,这个接口增加了几个便于定位元素以及反向遍历的方法

    package treeset;

import java.util.Objects;

/**
 * @author Conan
 */
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 +
            '}';
}

/**
 *
 * 重写equals方法hashcode方法为了比较时进行坐标和内容比较
 *
 */
@Override
public boolean equals(Object o) {
    if (this == o) {
        return true;
    }
    if (!(o instanceof Item)) {
        return false;
    }
    Item item = (Item) o;
    return partNumber == item.partNumber &&
            getDescription().equals(item.getDescription());
}

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


/**
 *要使用树集,必须能够比较元素
 * 如果调用Comparator接口的compareTo方法进行比较返回null
 */
@Override
public int compareTo(Item other) {
    int diff = Integer.compare(partNumber,other.partNumber);
    return diff!=0 ? diff:description.compareTo(other.description);
}
}


--------------------------------------------------------------------

    package treeset;


import java.util.Comparator;
import java.util.NavigableSet;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * @author Conna
 */
public class TreeSetTest {


public static void main(String[] args) {


    SortedSet<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类实现了NavigableSet接口
 */
        NavigableSet<Item> sortByDecsription = new TreeSet<>(
                Comparator.comparing(Item::getDescription));
        sortByDecsription.addAll(parts);

        System.out.println(sortByDecsription);


        }







}

队列与双端队列

队列可以让人有效的在尾部添加一个元素,在头部删除一个元素,有两个端头的队列,即双端队列,可以让人在头部和尾部同时添加或者删除元素,不支持在队列中间添加元素

在javaSE6中引入了Deque接口,并由ArrayDeque和LinkedList类实现,
这两个类都提供了双端队列,并在有必要时可以增加队列的长度,之后
在并发时可以看到有限队列和有限双端队列



ArrayDeque(int initialCapactity)
用初始16或给定的初始容量创建一个无限双端队列

优先级队列(priority queue)

优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索.然而优先级队列并没有对所有的元素进行排序.

优先级队列使用了一个优雅而高效的数据结构,称为堆(heap)

堆是一个可以自我调整的二叉树,对树进行add(添加)或者remove(删除)操作,可以让最小的元素移动到根

使用优先级队列的典型是任务调度

  • 每个任务都有一个优先级,任务以随机顺序添加到队列中

  • 每当启动一个新任务时,都将优先级最高的任务从队列中删除

  • (由于习惯上将1设为优先级最高,所以默认会将最小的元素删除)

      package priorityqueue;
      
      import java.time.LocalDate;
      import java.util.PriorityQueue;
      
      /**
       * @author Conna
       */
      public class PriorityQuqueTest {
      
          public static void main(String[] args) {
    
      PriorityQueue<LocalDate> pq = new PriorityQueue<>();
    
      pq.add(LocalDate.of(1906,12,9));
      pq.add(LocalDate.of(1815,12,10));
      pq.add(LocalDate.of(1903,12,3));
      pq.add(LocalDate.of(1910,6,22));
    
      System.out.println("打印----------");
    
      for (LocalDate date : pq) {
    
          System.out.println(date);
    
      }
      System.out.println("删除----------");
    
      while(!pq.isEmpty()){
          pq.remove();
      }
    

    }
    }

映射(Map)

集是一个集合,它可以快速的查找现有的元素,但是,要查看一个元素,需要有要查找元素的精确副本.这不是一种非常通用的查找方式.

通常我们知道某些键的信息,并想要查找与之对应的元素,映射(map)数据结构就是为此设计的

映射用来存放键/值对,如果提供了键就能查找到值

基本映射操作

Java类库为映射提供了两个通用的实现:HashMap和TreeMap,这两个类都实现了Map接口

  • 散列映射对键进行散列
  • 树映射用键的整体顺序对元素进行排序,并将其组织成搜索树
  • 散列或比较函数只能作用于键,与键关联的值不能进行散列或比较
与集一样,散列稍微快一点,如果不需要按照排列顺序访问键,就最好使用散列

每次往映射中添加对象是,必须提供一个键

要想检索一个对象,必须提供一个键(因而,必须记住)

如果在映射中没有与给定键的对应信息,get将返回null
有时可以有一个好的默认值,用作为映射中默认的键

T.get(id,0)

键必须是唯一的,不能同一个键存放两个值,如果对同一个键两次调用put方法,第二个值就会取代第一个值

remove方法用于从映射中删除给定键对应的元素,size方法返回映射中的元素数

要迭代处理映射的键和值,最容易的是使用forEach,可以提供一个接收键和值的lambda表达式,映射中的每一项会一次调用这个表达式

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

    package map;

import java.util.HashMap;

/**
 * @author Conan
 */
public class MapTest {


public static void main(String[] args) {

    HashMap<String, Employee> staff = new HashMap<>(5);

    staff.put("123-25-5464",new Employee("tom"));
    staff.put("456-25-5464",new Employee("jerry"));
    staff.put("789-25-5464",new Employee("kitty"));
    staff.put("101-25-5464",new Employee("echo"));

    System.out.println(staff);

    staff.remove("123-25-5464");

    staff.put("456-25-5464",new Employee("jerry"));

    System.out.println(staff.get("101-25-5464"));

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



}


}
更新映射项

处理映射时的一个难点就是更新映射项

正常情况下,可以得到一个与键关联的原值,完成更新,再放回更新后的值

但是必须考虑一个特殊情况,即键第一次出现

例如:使用映射统计一个单词在文件中出现的频度,看到一个单词(word)时,计数器加一

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

下面是可能发生的异常情况

- 第一次看到word时,get()返回null,这时出现空指针异常
  
  作为补救,可以使用getOrDefault方法
  
    counts.put(word,counts.get(word,0)+1);
    
- 不过还可以做得更好,merge方法可以简化这个常见的操作

     counts.put(word,counts.merge(word,1,Integer::sum));
映射视图

集合框架不认为映射本身是一个集合(其他数据结构框架认为映射是一个键/值对集合,或者是由键索引的值的集合),不过,可以得到映射的视图(view)—这是实现了Collection接口或某个子类的对象

有三个视图:键集,值集合(不是一个集),以及键/值对集.

  • 键和键/值对可以构成一个集,因为映射中一个键只能有一个副本

    下面的方法:

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

    会分别返回这三个视图(条目集的元素是实现Map.Entry类的对象)

  • 需要说明的是,keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象,Set接口扩展了colllection接口,因此可以像使用集合一样使用keySet

    例如,可以枚举一个映射的所有键:
    Set keys = map.keySet();
    for(String key:key)
    {
    do something with key
    }

如果在键集视图上调用迭代器的remove方法,实际上会从映射中删除这个键和与他关联的值,不过,不能向键集视图添加元素

  • 另外,如果增加一个键而没有增加值的话,也是没有意义的
  • 如果视图调用add方法,他会抛出一个UnsupportedOperationException方法
  • 条目集视图也同样有限制,尽管理论上增加一个新的键/值是比较有意义的
弱散列映射(WeakHashMap)

在集合视图中有几个专用的映射类

设计WeakHashMap类是为了解决一个有趣的问题

如果有一个值,对应的键不再使用了,那么由于这个键不再程序的任何地方所使用,
那么这个键/值对将无法从映射中删除,(垃圾回收器也无法将它删除,因为垃圾回收器跟踪
活动的对象,只要映射对象是活动的,其中的所有桶是活动的,他们不能被回收)
因此需要由程序负责从长期存活的映射表中删除那些无用的值,或者使用WeakHashMap完成
这件事情

当对键的唯一引用来自三类条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对

这种机制的内部运行

WeakHashMap使用弱引用(weak references)保存键

  • WeakReference对象将引用保存在另外一些对象中,在这里就是散列键
  • 通常,垃圾回收器发现某个特定的对象已经没有人用了,就将其回收
  • 然而,如果这个对象只能由WeakReference引用,但是垃圾回收器仍然要回收他,就要将这个对象的弱引用放入队列中
  • WeakHashMap将周期性的检查队列,以便查找出新添加的弱引用
  • 一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来
  • 于是WeakHashMap将删除对应的条目
链接散列集与映射

LinkedHashSet和LinkedHashMap类用来记住插入元素项的顺序,这样就可以在散列表中的项是随机排列的,当条目插入到表中,就会并入到双向链表中

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

每次调用get或put方法,受到影响的条目将会从当前位置删除,并放到条目链表的尾部(
只有条目在链表中的位置会受到影响,而散列表中的桶不会受到影响,一个条目总位于与键散列码对应的桶中)

要想构造这样一个散列映射表,需要调用

LinkedHashMap<k,v>(initalCapacity,loadFactor,ture)

访问顺序对于实现高速缓存原则 "最近最少使用"原则十分重要

例如,可能希望将访问频率高的元素放在内存中,而访问频率低的元素从数据库中获取

当在表中找不到元素项且表已经满时,可以将迭代器加入表中,并将枚举的前几个元素删除掉(这是近期使用最少的元素)

上述过程甚至可以自动化,即构造一个LinkedHashMap的子类,然后覆盖下面的方法:

protected boolean removeEldestEntry(Map.Entry<k,v> eldest)

每当方法返回ture的时候,就添加一个新条目,从而导致删除eldest条目

例如,下面的高速缓存可以存放100个元素

Map<String,Employee> cache = new
            LinkedHashMap<>(128,0.75,true)
    {
        protected boolean removeEldestEntry(Map.Entry<String,Employee> eldest){


            return size() >100;
        }
    }();

另外,还可以对eldest条目进行评估,以此决定是否应该将他删除,例如,可以检查与这个条目一起存在的时间戳

枚举类与映射

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

EumSet类没有公用的构造器,可以使用静态方法类构造这个集

enum Weekday{MONDAY,TUESDAY,WEDNESDAY,THURDAY,FRIDAY,SATURDAY,SUNDAY};
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(Weekend.MONDAY,Weekday.WEDNESDAY,Weekend.FRIDAY);

可以使用Set接口的常用方法来修改EnumSet

EnumMap是一个键类型为枚举类型的映射,它可以直接且高效的用一个数组实现,在使用时,需要在构造器中指定键类型

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

标识散列映射

类IdentityHashMap有特殊的作用,在这个类中,键的散列值不是用hashcode函数计算的,而是用System.identityHashCode方式计算的,这是Object.hashCode方法根据内存地址来计算散列码时所使用的方式,而且在两个对象进行比较时,identityHashMap类使用==,而不使用equals.

也就是说,不同的键对象,即使内容相同,也被视为是不同的对象,在实现对象遍历算法(如对象串行化时),这个类非常有用,可以用来跟踪每个对象的遍历情况


视图与包装器

通过使用视图(views)可以获得其他实现了Collection接口和Map接口的对象,映射类的KeySet方法就是这样一个示例

KeySet方法返回一个实现Set接口的类对象,这个类的方法对原映射进行操作,这种集合称为视图

视图技术在集框架中有许多非常有用的应用

轻量级集合包装器

Arrays 类的静态方法asList将返回一个包装了普通java数组的List包装器,这个方法可以将数组传递给一个期望得到列表或集合参数的方法

Card[] cardDeck = new Card[52];
...

List<card> cardList = Array.asList(cardDeck);

返回的对象不是ArrayList,他是一个视图对象,带有访问底层数组的get,set方法,改变数组大小的所有方法
(例如,与迭代器相关的add和remove方法)都会抛出一个Unsupported OperationException异常

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

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

这个方法调用

Collection.nCopies(n,anObject)

将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个anObject的错觉

//下面的调用将创建一个包含100个字符串的List,每个串都被设置成 "DEFAUIT"

List<String> setting = Collection.nCopies(100,"Default");

存储代价很小,这是视图技术的一个巧妙应用

如果调用下列方法

Collections.singleton(anObject)

则将返回一个视图对象,这个对象实现了Set接口(与产生的List的ncaopies方法不同),返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销.

特别的,集的类型可以推导得出:

Set<String> deepThoughts = Collections.emptySet();
子范围

可以为很对集合建立子范围(subrange)视图,假设,假如有一个列表staff,想从中取出第10~19个元素,可以使用subList方法来获得一个列表的子范围视图

List ground2 = staff.subist(10,20);

第一个索引包含在内,第二个索引则不包含在内.

可以将任何操作应用于子范围,并能够自动反应整个列表的情况.

//例如,可以删除整个子范围

ground2.clear();

现在,元素自动的从staff列表中清除了,并且groud2为空

对于有序集和映射,可以使用排序顺序而不是元素位置建立子范围,SortedSet接口声明了三个方法:

    SortedSet<E> subSet(E from,E to)
    SortedSet<E> headSet(E to)
    SortedSet<E> tailSet(E from)

这些方法将返回大于等于from且小于to的所有元素子集,有序映射也有类似的方法:

返回映射视图,该映射包含键落在指定范围内的所有元素

JavaSE 6引入的Navigable接口赋予了子范围操作更多控制的能力,可以指定是否包含边界:

NavigableSet<E> subSet(E form,boolean from Inclusive,E to,boolean toInclusive)
NavigableSet<E> headSet(E to,boolean toInclusive)
NavigableSet<E> tailSet(E form,boolean fromInclusive)
不可修改的视图

Collections还有几个方法,用来产生集合的不可修改视图(unmodifiable views),这些视图对现有的集合增加了一个运行时的检查,如果发现试图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态

可以使用下面8种方法获得不可修改视图:

Collection.unmodifiableCollection
Collection.unmodifiableList
Collection.unmodifiableSet
Collection.unmodifiableSortedSet
Collection.unmodifiableNacigableSet
Collection.unmodifiableMap
Collection.unmodifiableSortedMap
Collection.unmofifiableNavigableMap

每个方法都定义于一个接口,例如Collection.unmodifiableList与ArrayList,LinkedList或者任何实现了List接口的其他类一起协同工作

//假如,想要查看某部分代码,但又不触及某个集合的内容,就可以进行下列操作:

List<String> staff = new LinkedList<>();
..
lookAt(Collections.unmodifiableList(staff));

Collection.unodifiableList对象将会返回一个实现List接口的类对象,其访问器方法将从staff集合获取值

不可修改视图并不是集合本身不可修改,仍然可以通过集合的原始引用(在这里是staff)对集合进行修改,并且仍然可以让集合的元素调用更改器方法

由于视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的方法

LinkedList类中有一些非常方便的方法,addFirst和addLast,
他们都不是List接口的方法,不能通过不可修改视图进行访问
同步视图

如果多个线程访问集合,就必须确保集不会被意外的破坏

例如:一个线程试图将元素添加到散列表中,同时另一个元素正在对散列表进行再散列,其结果是灾难性的

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

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

Map<String Employee> map = 

Collection.synchronizedMap(new HashMap<String ,Employee>)

现在,就可以由多线程访问Map对象了,像get和put这类方法都是同步操作的
即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成

受查视图

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

实际上,将错误类型的元素混入泛型集合中的问题极有可能发生,例如:

ArrayList<String> string = new ArrayList<>;
ArrayList rawList  = string;
rawList.add(new Date());

这个错误的命令在运行时检测不到,相反,只有在稍后的另一部分代码中调用get方法,并将结果转化为String时,这个类才会抛出异常

受查视图可以探测到这类问题:下面定义了一个安全列表:

List safeString = Coolections.checkedList(String,String.class)

视图的add方法将检测插入的对象是否属于给定的类,否则则会抛出ClassCastException

这样做的好处是错误可以在正确的位置得到报告:

ArrayList rawList = safeString;
rawList.add(new Date());

警告受查对象受限于虚拟机可以运行的运行时检查

关于可选操作的说明

通常,视图具有一些局限性,即可能只能读,无法改变大小,只支持删除而不能插入,这个与映射的键的视图相同,如果视图进行不恰当的操作,受限制的视图就会抛出UnsupportedOperationException异常

在集合与迭代器接口的ApI文档中,许多方法描述为可选操作(这与接口的概念有所抵触)

但是应该有这么一种不必依靠极端衡量的 "可选的"接口操作来解决这类问题的方案

算法除外,集合部分告落

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值