java笔记(六):集合,容器

这一节是java最复杂的体系之一,包含各种各样的容器和方法。

传统遗留

枚举(Enumeration),位集合(BitSet),向量(Vector),栈(Stack),字典(Dictionary),哈希表(Hashtable),属性(Properties)均是java遗留的数据结构,现在已经很少使用。
参看源码,目前的替代方式如下:
Enumeration –>Iterator
BitSet
Vector –>List
Stack –>Deque
Dictionary,Hashtable –>Map
Properties

集合框架

为了使得集合框架间的操作更高效,互操作性更强,集合框架围绕一组标准接口设计。
所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型,接口允许集合独立操纵其代表的细节。
  • 实现(类):是集合接口的具体实现。
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

接口

集合框架中常有的接口有:
Iterable

Iterable

该接口最主要的是iterator()方法,实现了该接口的类就能够使用for-each循环。

Iterator

Iterator取代了java中原本Enumeration的位置,主要区别在于:

  • 在迭代过程中,允许删除当前元素

部分源码如下:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    //迭代中,允许删除当前元素
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
}

Collection

集合框架中最重要的两个基础接口之一(另一个是Map)。
Collection代表了一类数据的集合(非数学意义上的集合),这些数据可能是有序的,无序的,可重复的,不可重复的。然而jdk并没有为这些不同类的数据提供直接的实现,而是提供了基于Collection的子接口,例如Set和List。
另外注意下该接口的判断参数的一个准则:

(o==null ? e==null : o.equals(e))//o是类本身,e是参数

然后直接看下该接口的源码吧:

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    //Returns an array containing all of the elements 
    //in this collection,this method must allocate a new array 
    //even if this collection is backed by an array
    Object[] toArray();
    /**
    *和上面的方法不同的是,当参数的类型和运行时(即初始化指定的类型一致时),
    *该Collection将直接被返回,而不会重新生成新的数组。
    * 比如 String[] y = x.toArray(new String[0]);
    * 建议使用此方法
    */
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    //Removes all of this collection's elements that are also contained 
    //in the specified collection (optional operation)
    boolean removeAll(Collection<?> c);
    //Retains only the elements in this collection
    //that are contained in the specified collection
    boolean retainAll(Collection<?> c);
    void clear();

Set

不包含重复元素的集合。
本质上来说,不同时包含满足e1.equals(e2)的元素。
部分源码如下:

public interface Set<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean retainAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    void clear();
    //o是一个集合,且两个集合的元素相同
    boolean equals(Object o);
    //两个相同的集合,其hashcode也一样
    int hashCode();

Map

另一个重要的接口。将键(key)和值(value)关联到一起。功能类似于字典(Dictionary)。
简单看下源码吧:

public interface Map<K,V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    //覆盖之前的value,并返回之。如果key先前存在的话
    V put(K key, V value);
    V remove(Object key);
    //注意的是,对于重复的key,新的value会覆盖旧的valu
    //相当于循环调用put
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    /**
    * 返回的set任然和该Map存在联系
    * 即,对于返回的Set的修改将影响该Map本身
    * 另外,返回的Set支持remove,retainAll和clear等操作
    * 但不支持add方法
    */
    Set<K> keySet();
    //与keySet相对的,返回值的集合。原理和keySet()相同
    Collection<V> values();
        //若两个map的键值对相同,返回true
    boolean equals(Object o);
    int hashCode();
    //返回map的键值对
    Set<Map.Entry<K, V>> entrySet();

在继续往下前,先来看看Map的内部接口——Entry。

Entry

Map.Entry对象只有在循环迭代时才有意义。
Entry即一条键值对。该键值对的改变会直接影响到原Map。

public interface Entry<K,V> {
    K getKey();
    V getValue();
    V setValue(V value);
    //比较两个键值对的键和值是否相等
    boolean equals(Object o);

Entry有四个比较方法需要单列出来。就一种一个进行解释说明:

public static <K extends Comparable<? super K>, V>
    Comparator<Map.Entry<K,V>> comparingByKey() {
    return (Comparator<Map.Entry<K, V>> & Serializable)
        (c1, c2) -> c1.getKey().compareTo(c2.getKey());
}

首先,定义一个键值对<K,V>,其中,K必须满足以下条件:

  • K实现了接口泛型类为K父类的Comparable接口

(c1, c2) -> c1.getKey().compareTo(c2.getKey()) 的写法有点类似scala的函数式编程的写法,即先确定两个变量c1,c2,返回c1.getKey()和c2.getKey()的比较结果。
即明确了一种比较的方法,最后把这种比较方法强制转换为(Comparator<Map.Entry<K, V>> & Serializable) 返回。
即该方法是返回了一个比较器,用于比较键(key)的自然顺序。
当然啦,方法体里的Comparable也是一个接口:

Comparable

强制加上自然排序。实现了该接口的类使用sort()方法后有自动排序功能

public interface Comparable<T> {
    public int compareTo(T o);
}

Map源码看到一半,Entry接口算是完毕了。
紧接着剩下额Map源码,剩下的都是default方法体。

default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

类似于scala的getOrElse方法。不解释了,很简单。

//根据action执行每一个键值对
default void forEach(BiConsumer<? super K, ? super V> action)

default V getOrDefault(Object key, V defaultValue) {
    V v;
    return (((v = get(key)) != null) || containsKey(key))
        ? v
        : defaultValue;
}

后面还有些静态的方法,但都大同小异。

List

接口继承接口。List继承了Collection接口

public interface List<E> extends Collection<E> {
    //除了重新声明Collection的方法,还有部分新增的方法如下:
    E get(int index);
    //替换
    E set(int index, E element);
    //插入
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);

方法里面有一个ListIterator接口,先看看吧。

ListIterator

该接口的实现能双向遍历,在迭代中进行修改。贴下源代码:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();
    E next();
    int nextIndex();
    //下面三组方法和上面三组方法操作相反
    boolean hasPrevious();
    E previous();
    int previousIndex();

Queue

队列,可也以理解为链表。继承了Collection接口。
该类实现了先进先出(FIFO)策略和后进先出(LIFO)策略,也包括优先级队列。
无论是哪一种实现,都必须指明所使用的排序属性。
和add()功能相同,Queue声明了offer()方法用于添加元素,其特点是添加失败时,不报错,而会返回false。
同理,作为和remove()同功能的方法,poll()当元素为空时,也不会报错,而是返回null。
以下是Queue源代码中新增的内容:

public interface Queue<E> extends Collection<E> {
    boolean offer(E e);
    E poll();
    //返回第一个元素,当为空时,不报错
    E peek();

Deque

继承自Queue。
一个线性存储的集合,支持在两端进行插入和删除操作即双向队列(double ended queue)。
和Queue类似,该类对于删除,插入等方法也有两种情况:操作失败时报错,和返回null或者false。
Deque可以被当做堆和栈使用。
部分源码如下:

public interface Deque<E> extends Queue<E> {
    void addFirst(E e);
    void addLast(E e);
    boolean offerFirst(E e);
    boolean offerLast(E e);
    E removeFirst();
    E removeLast();
    E pollFirst();
    E pollLast();
    E getFirst();
    E getLast();
    E peekFirst();
    E peekLast();
    boolean removeFirstOccurrence(Object o);
    boolean removeLastOccurrence(Object o);
    //该方法等同于addFirst
    void push(E e);
    //该方法等同于removeFirst
    E pop();
}

抽象类

AbstractCollection

实现自Collection的抽象类,为Collection提供了一个大体的骨架。
实现一个不可修改的Collection实体类,只需要在继承此抽象类的基础上实现iterator()方法和size()方法。
实现一个不可修改的Collection实体类,需要额外的覆盖add()方法和remove()方法。
直接看看源码,下面列出AbstractCollection在Collection基础上实现了的方法(即非抽象方法):

public abstract class AbstractCollection<E> implements Collection<E> {
    //该方法需要实现size()
    public boolean isEmpty() {
        return size() == 0;
    }
    //该方法需要实现iterator()
    public boolean contains(Object o) {
        Iterator<E> it = iterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return true;
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return true;
        }
        return false;
    }
    //删除找到的第一个o
    public boolean remove(Object o) {...}

其他方法就是一些具体实现了。建议看源码,很容易理解。

AbstractList

与其他同功能的抽象类不同的是,该抽象类的继承不需要实现iterator()方法,因为在该抽象类中已经实现了。
首先看看AbstractList中Iterator的实现吧:

Itr

在AbstractList内部定义的类

private class Itr implements Iterator<E> {
    int cursor = 0;
    int lastRet = -1;
    public boolean hasNext() {
        return cursor != size();
    }
    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
        }
    }
}

部分实现可以看出,其实Itr也是通过类似数组的索引方式来实现迭代操作的。
同样的还有ListItr类(实现ListIterator接口),两者都是通过AbstractList内部的add方法,remove方法,get方法来改变List的结构,并返回索index。
但AbstractList内部并没有实现add,remove,get方法,这依赖于继承类了。

为了了解AbstractList的下一个方法subList,需要先认识另外几个接口或抽象类

RandomAccess

接口,提供快速获取元素。

RandomAccessSubList

继承自SubList,该方法暂时没有实现任何对于快速获取的操作,实际上等同于SubList(见下)。

SubList

继承自AbstractList的抽象类。源码进行了不同的安全措施,包括检查边界等,实质是对AbstractList的封装。

subList方法

    public List<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<>(this, fromIndex, toIndex) :
                new SubList<>(this, fromIndex, toIndex));
    }

对于抽象类来说,返回的对象其实没有区别,如果子类继承了AbstractList并实现了与RandomAccess有关的方法,那么该方法就返回一个有RandomAccess功能的AbstractList。
AbstractList剩下的没列出的方法包括比较和边界安全。这些代码较简单。

AbstractSequentialList

用于实现顺序存取的抽象类,当需要随机存取时,应该使用实现了AbstractList抽象类的ArrayList。
部分源代码如下:
该类的大部分方法都进行了重写,主要逻辑是先通过listIterator(int index)返回迭代器,返回在进行增删改查的操作。

AbstractSet

用于实现集合的抽象类,继承自AbstractCollection。该类并没有覆盖方法,主要是简化了equals方法。

AbstractMap

为Map实体类提供大体框架,实现了Map接口。
继承该类的子类需要实现entrySet()方可进行操作。
在AbstractMap内部实现了Entry接口——SimpleEntry类和SimpleImmutableEntry类。两个类中都有属性key和value,表示一条记录的键值对。
AbstractMap的部分源码如下:

public abstract class AbstractMap<K,V> implements Map<K,V> {
    transient Set<K> keySet;
    transient Collection<V> values;
    //如下为其中的代表性方法,该类对于元素的操作都是通过entrySet()实现的
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }   

实体类

ArrayList

来看看第一个实体类吧。
关于该类有几点说明:

  • 可以改变存储大小
  • 该类可以存储null
  • 在O(1)时间内运行size,isEmpty,get,set,iterator,listIterator方法,add也可以认为是O(1)时间
  • 其他的方法都是线性时间。

特别提到了,当需要线程安全的时候,需要进行一层包装。如下:

List list = Collections.synchronizedList(new ArrayList(...));

看看源码吧:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //存储数据
    transient Object[] elementData;

ArrayList内部的indexOf,lastIndexOf方法都对AbstractList内部的同名方法进行了重写,因为AbstractList的这两个方法是通过获取Iterator对象进行查找,而由于ArrayList内部基于数组实现数据存储,因此直接使用遍历的方法查找,提高效率。
看一个简单的例子:

ArrayList<String> strList = new ArrayList<String>();
strList.add("live");
strList.add(0,"love");
strList.add("sunshine");
strList.set(2,"!");
strList.remove(strList.size()-1);
System.out.println(strList.toString()); //输出[love, live]

LinkedList

继承自AbstractSequentialList。
部分源码如下:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}   

首先是一个用于代表单个几点的内部定义的类——Node。
可以看出该定义和经典的链表实现思想是一致的。
然后是LinkedList实体类定义如下:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;

    public void addFirst(E e) {
        linkFirst(e);
    }
    public void addLast(E e) {
        linkLast(e);
    }
    /*...*/

所有的实现操作都是基于经典链表来实现的。
甚至包括了指定位置的插入删除等操作。
另外,由于LinkedList实现自Queue,因此,实现了Queue的所有方法,包括这种栈的操作和队列的操作。
因此栈的实现使用LinkedList来完成
毫无疑问,LinkedList在限定端的新增删除元素操作比ArrayList要快,但随即存取的速度要慢,因为每次都需要遍历。

LinkedList<String> link = new LinkedList<String>();
link.add("love");
link.add("live");
link.add("!");
System.out.println(link.toString());  //输出[love, live, !]

Vector

该类原本是java遗留项目的一个版本,用法和ArrayList类似,但不同的是,该类通过实现AbstractList进行了重写,在实现上也和ArrayList类似,通过数组实现。
官方解释,该类适合于可增长的数组,即容量可以动态增长。
最大的区别是,Vector是线程安全的,因为Vector的所有查询和修改等操作方法都使用了synchronized关键字修饰。但线程同步会使得性能极大的削减,因此在不要求线程同步的时候,建议使用ArrayList。

HashMap

哈希图,java中最常用键值对存储方式。
需要注意的是,HashMap内的存储不保证顺序。HashMap的存取操作都是线性的时间。
HashMap的实现有点繁琐,笔者只研究其中最基础的部分。
首先需要明确的是,HashMap使用数组存储数据,该数组的类型是Node。Node定义如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  //hash值,用于在数组中定位该元素
    final K key;  //键
    V value;  //值
    Node<K,V> next;  //指向下一个hash值相同,key不同的元素

HashMap的数据存储定义如下:

transient Node<K,V>[] table;

那么,HashMap如何保证key的唯一性呢?
这里不贴代码了,直接说原理。
1. 根据key计算该key的hash值。
2. 到table查到对应位置有没有元素,没有则将该元素放在此处
3. 若有元素,则将该键值对放在该位置元素最后一个next指向的地方
同时,获取元素时也是相同的步骤,先比较hash值,在比较key的值。
HashMap的实例如下:

Map<String, String> map = new HashMap<String, String>();
map.put("bibi","nico maki eli");
map.put("lily white","umi rin nozomi");
map.put("printemps","honoka kotori hanayo");
for(Map.Entry e:map.entrySet()){
    System.out.println(e.getKey()+":"+e.getValue());
}
//输出
//bibi:nico maki eli
//lily white:umi rin nozomi
//printemps:honoka kotori hanayo

上述的循环也是推荐的循环操作。

Hashtable

稍微提一下Hashtable。
这里只说结论:
Hashtable功能上基本等同于HashMap,但Hashtable是线程安全的,翻看源码可以发现,Hashtable大部分方法体的实现上都添加了synchronized关键字,因此Hashtable的方法肯定都会慢于HashMap。但若需要多线程操作map也建议不使用Hashtable,而是使用线程安全的HashMap,见java笔记(十一):Collections

HashSet

不允许重复元素的集合,对于元素的顺序不保证。
对于存取等基本操作的耗时是常量时间。
HashSet的实现实际上是内部有一个HashMap属性,然后使用HashMap的key来存储不可重复的元素。
部分源码如下:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    //用于存储key的map
    private transient HashMap<E,Object> map;
    //该静态成员充当每一个key的value
    private static final Object PRESENT = new Object();

实际例子如下:

Set<String> set = new HashSet<String>();
System.out.println(set.add("love"));
System.out.println(set.add("love"));
System.out.println(set.add("live"));
System.out.println(set.add("!"));
Iterator i = set.iterator();  
while(i.hasNext()){
    System.out.print(i.next());  
}
//结果如下:
//true
//false
//true
//true
//love!live

TreeMap

作为Collection里最复杂的部分,将统一介绍TreeMap的相关源码。
暂时先从整体介绍下TreeMap系列。

SortedMap

SortedMap本身是一个接口,提供了许多跟排序有关的方法,包括获取子图,获取第一个key,最后一个key等操作。

也是一个接口,主要提供了获取当前key前后的key的方法,比如不小于,不大于当前key的接口之类的方法。

然后TreeMap,实现了上述两个接口。
具体是通过红黑树来实现的:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

上面的代码时TreeMap的主入口,定义一个指向父节点的二叉树。
然后再次基础上实现了各种排序,遍历操作。
最需要意的有一点:
TreeMap保证在log(n)时间内实现containsKey,get,put,remove操作
因此该Map的性能是不如HashMap的,若不是需要有序,一般不建议使用TreeMap。
其操作的具体实现细节就不剖析了,因为所有实现都基于红黑树,可以参考《算法导论》。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值