Java学习笔记7——集合框架

一、集合框架

拿一张网上搜索的集合框架的图。

集合框架的示意图

首先来整体讲一下集合框架。
整个集合框架的接口都是源于Collection接口。Collection接口继承了Iterable接口,使得所有继承Collection接口的接口,在被实现之后,都可以使用迭代器Iterator访问和遍历。

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

先说一下Collection接口下声明的方法。
(1)获得集合的大小:int size()
(2)判断集合是否为空:boolean isEmpty()
(3)是否包含指定的Object对象:boolean contains(Object o)
(4)获取迭代器:Iterator< E > iterator()
(5)将集合转为Object数组(无参):Object[] toArray()
(6)使用泛型,对集合进行数组转换:< T > T[] toArray(T[] a);
(7)增加元素:boolean add(E e)
(8)删除元素:boolean remove(Object o)
(9)与目标集合进行比较,判断是否相同:boolean containsAll(Collection< ? > c)
(10)往集合中增加一个集合:boolean addAll(Collection< ? extends E > c)
(11)删除在目标集合中的指定集合内的元素:boolean removeAll(Collection< ? > c)
(12)取得两个集合的交集:boolean retainAll(Collection< ? > c)
(13)清空集合:void clear()
(14)与集合进行比较:boolean equals(Object o)
(15)获取哈希值:int hashCode()

List接口和Set接口继承了Collection接口。而Map接口也是集合框架中的一个接口,但它的实现类是以键值对的形式作存储,与其他两个接口不一样。

下面分别说一说:

1、List

List是一个有序的集合接口,可以类比顺序表等线性表。
List接口是支持对元素进行增删改查动作的(add,set,get,remove)。List是有索引存在的。所以实现List的类都可以通过get(int index)获取对应的元素。
遍历List有2种,迭代器迭代遍历循环遍历+get方法

实现List接口的常见类有ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList等。

List特有的迭代器ListIterator
ListIterator< E > listIterator()

Iterator接口的方法:
(1)删除集合中Iterator指向位置后面的元素:void remove()
(2)返回集合中Iterator指向位置后面的元素:E next()
(3)迭代器指向位置后面是否还有元素:boolean hasNext()

而ListIterator接口中新声明的方法有:
(ListIterator继承了Iterator)
(1)void add(E e)
将指定的元素插入列表,插入位置为迭代器当前位置之前
(2)void set(E e)
从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e
(3)E previous()
返回列表中ListIterator指向位置前面的元素
(4)boolean hasPrevious()
逆向遍历列表,判断列表迭代器前是否还有元素
(5)int previousIndex()
返回列表中ListIterator所需位置前面元素的索引
(6)int nextIndex()
返回列表中ListIterator所需位置后面元素的索引

ListIterator可以从头或者从尾进行遍历。ListIterator可以遍历修改,而Iterator只能遍历,不能修改。

//使用迭代器
Iterator<Object> it=list.iterator();
while(it.hasNext()){
    Object obj = it.next();
    System.out.println(obj);
}

//使用ListIterator
ListIterator<Object> listIt = list.listIterator();
while(listIt .hasNext()){
    Object obj = listIt .next();
    System.out.println(obj);
}

//使用foreach
for(Object obj : list){
    System.out.println(obj);
}

下面来看看实现List接口的类。

(1)ArrayList
ArrayList是数据结构中的数组类型。
默认构造方法是新建一个长度为10的数组。

public ArrayList() {
    this(10);
}

其遍历效率高,但增加删除麻烦。
因为ArrayList新建的时候,是通过创建新数组再复制老数组至新数组完成的,所以增加或删除的时候效率低。

这里列一下add的源码

public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

通过计算add后的长度与现容量的大小作比较,必要时增加数组容量(新建一个更大的数组,然后将老数组复制至新数组),确保新加进来的数据是可以正常保存的。

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
        newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

ArrayList是线程不安全的(因为ArrayList内部没有使用同步)。在需要考虑多线程的情况下,可以通过Collections.synchronizedList(List i)函数返回一个线程安全的ArrayList类,或者使用Concurrent并发包下对应的集合类。

(2)LinkedList

LinkedList是基于双向循环链表实现的,是链表结构,不同步。
因为LinkedList是双向循环链表,所以顺序访问效率高,但是随机访问效率低。
由于实现了Queue接口,因此也可以用于实现堆栈、队列。

LinkedList接口

因为LinkedList实现了List接口,所以其类中也有get和remove等方法根据索引值获取值。它是通过让索引与1/2的长度做比较,决定从哪边开始遍历,从而较快找到对应索引的元素。其他利用索引进行的操作也是如此。(通过调用entry(index)的方法)

public E get(int index) {
    return entry(index).element;
}
private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
    Entry<E> e = header;
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

而Entry是LinkedList内部的一个私有静态类。Entry类是一个链表的节点结构。包括了存储的数据,节点前和节点后对应的节点。

在LinkedList中,也包含了队列、堆栈相关操作的方法。
首先看一下LinkedList作为链表的方法。
获取LinkedList的第一个元素:E getFirst()
获取LinkedList的最后一个元素:E getLast()
删除LinkedList的第一个元素:E removeFirst()
删除LinkedList的最后一个元素:E removeLast()
在起始位置增加元素:addFirst(E e)
在结束位置增加元素:addLast(E e)

下面是关于模拟队列的操作:
返回第一个节点:E peek()
当大小为0时,返回null

public E peek() {
    if (size==0)
        return null;
    return getFirst();
}

返回第一个节点:E element()
当大小为0时,抛出异常(NoSuchElementException)

public E element() {
    return getFirst();
}

移除第一个元素:E poll()
移除第一个元素:E remove()
区别与上面的相同。

将元素添加到末端:boolean offer(E e)
将元素添加到第一个位置:boolean offerFirst(E e)
将元素添加到末端:boolean offerLast(E e)
返回第一个节点:E peekFirst()
返回最后一个节点:E peekLast()
删除并返回第一个节点:E pollFirst()
删除并返回最后一个节点:E pollLast()

模拟堆栈:
添加到第一个节点(进堆):void push(E e)
删除并返回第一个节点:E pop()


从LinkedList的实现方式可以发现,LinkedList不会出现容量不足的问题(双向链表特性)
因为LinkedList也实现了Deque接口,所以LinkedList也可以被当做双向队列进行使用。

队列方法        等效方法
add(e)          addLast(e)
offer(e)        offerLast(e)
remove()        removeFirst()
poll()          pollFirst()
element()       getFirst()
peek()          peekFirst()

也可以当做堆栈进行使用:

栈方法             等效方法
push(e)             addFirst(e)
pop()               removeFirst()
peek()              peekFirst()

(3)Vector(已过时)
因为现在也不建议使用这个类了,所以就粗略讲讲就好了。
Vector实现了一个动态数组。和ArrayList和相似。

Vector是同步访问的。
Vector包含了许多传统的方法,这些方法不属于集合框架。

为什么不建议使用Vector?
因为Vector是线程安全的,所以它的性能是比ArrayList差很多的。而且现在也有更好的办法可以解决ArrayList线程不安全的问题。所以就过时了。

(4)Stack(已过时)
堆栈类。(过时的我也不想看了= =。懒)
为什么不建议使用Stack?
因为Stack继承了Vector。原因不讲了。

(5)CopyOnWriteArrayList
CopyOnWriteArrayList是一个ArrayList的线程安全的变体。其实这里可以抽成CopyOnWrite容器进行讨论,下面引用一下别人的文章段落来说说。

其实是一个CopyOnWrite的容器。当我们往一个容器添加元素的时候,不是直接往当前容器添加,而是先将当前容器进行复制,复制出一个新的容器,然后往新容器里添加元素,添加完之后,再将原容器的引用指向新容器。这样可以对CopyOnWrite容器进行并发的读,也不需要加锁。CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

看看add方法。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

在复制容器的过程中,是加了锁了,这样才能保证不会复制多个容器。
而在读的时候,是不需要加锁的,就算有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

CopyOnWrite并发容器用于读多写少的并发场景。

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
  数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。


什么场景下更适宜使用LinkedList,而不用ArrayList
(1) 你的应用不会随机访问数据。因为如果你需要LinkedList中的第n个元素的时候,你需要从第一个元素顺序数到第n个数据,然后读取数据。
(2)你的应用更多的插入和删除元素,更少的读取数据。因为插入和删除元素不涉及重排数据,所以它要比ArrayList要快。


2、Set接口

Set接口中声明的方法与List基本一致。但是实现Set接口的类中的元素则是无序的、唯一的(不可重复)。
实现Set接口的类有:HashSet,LinkedHashSet,TreeSet,CopyOnWriteArraySet等等。

遍历Set的方式:

// 迭代遍历:
Set<String> set = new HashSet<String>();
Iterator<String> it = set.iterator();
while (it.hasNext()) {
  String str = it.next();
  System.out.println(str);
}

// for循环遍历:
for (String str : set) {
      System.out.println(str);
}

(1)HashSet
HashSet是不保证顺序的,允许使用null元素。

HashSet底层结构是使用HashMap作存储。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable{

    private transient HashMap<E,Object> map;

    public HashSet() {
    map = new HashMap<E,Object>();
    }

    ……
}

而且HashSet实现不是同步的,涉及多个线程同时访问的时候,必须保持外部同步。使用 Collections.synchronizedSet 方法来“包装” Set。

Set s = Collections.synchronizedSet(new HashSet(...));

HashSet拥有List接口中的方法,用法一致,但实现方式不同。
下面举例add方法。

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

PRESENT是类中定义的一个对象,个人理解其实就是用来充数占位而已。而使用要增加的对象做键,存入HashMap中。这就是为什么HashSet是无序且唯一的原因。
其他的方法都是对内置的HashMap对象进行数据操作。具体操作等到后面说到HashMap的时候再详细说。

如果需要将自定义对象放入HashSet中,需要重写该对象对应类的equals方法和hashCode()方法。

(2)LinkedHashSet
LinkedHashSet继承了HashSet,是使用哈希表和链表实现的。
LinkedHashSet底层使用LinkedHashMap来保存所有元素。这个与HashSet类似。

它定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。使得集合保证唯一性,看起来就像以插入顺序保存一样。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

(3)TreeSet
emmmm….猜的没错。
还是老样子。TreeSet的底层确实又是用TreeMap。TreeSet是基于TreeMap的NavigableSet接口(NavigableSet接口继承了SortedSet接口)实现的。
所以,TreeSet是实现SortedSet的其中一个实现类。(另一个是ConcurrentSkipListSet)
因为TreeSet实现了SortedSet接口,所以它能够确保集合元素处于排序状态。
TreeSet支持2种排序方式,自然排序和定制排序。这个取决于使用的构造方法。

构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。

public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。

构造一个新的空 TreeSet,它根据指定比较器进行排序。

public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<E,Object>(comparator));
}

如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。

(4)CopyOnWriteArraySet
这个类跟上面讲过的CopyOnWriteArrayList类似。它是线程安全的、无序的集合。可以看成是一个线程安全的HashSet。而需要注意的一点是,CopyOnWriteArraySet是通过CopyOnWriteArrayList(动态数组队列)实现的。

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

因为写操作需要复制一个新的数组,所以对于写操作相关的开销很大。

这里引用一个例子,这个例子很好说明了CopyOnWriteArraySet的使用方式:

public class CollectionDemo {

    // TODO: set是HashSet对象时,程序会出错。
    //private static Set<String> set = new HashSet<String>();
    private static Set<String> set = new CopyOnWriteArraySet<String>();
    public static void main(String[] args) {

        // 同时启动两个线程对set进行操作!
        new MyThread("ta").start();
        new MyThread("tb").start();
    }

    private static void printAll() {
        String value = null;
        Iterator iter = set.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
        System.out.println();
    }

    private static class MyThread extends Thread {
        MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
                int i = 0;
            while (i++ < 10) {
                // “线程名” + "-" + "序号"
                String val = Thread.currentThread().getName() + "-" + (i%6);
                set.add(val);
                // 通过“Iterator”遍历set。
                printAll();
            }
        }
    }
}

运行结果:

ta-1, tb-1, 
ta-1, tb-1, ta-2, 
ta-1, tb-1, ta-2, ta-3, 
ta-1, tb-1, ta-2, ta-3, ta-4, 
ta-1, ta-1, tb-1, tb-1, 
ta-2, ta-1, ta-3, tb-1, ta-4, ta-2, ta-5, 
ta-3, ta-4, ta-5, ta-1, tb-2, 
tb-1, ta-1, ta-2, tb-1, ta-3, ta-2, ta-4, ta-3, ta-5, ta-4, tb-2, ta-5, ta-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, 
ta-1, tb-2, tb-1, ta-0, ta-2, ta-3, ta-4, ta-5, tb-3, 
tb-2, ta-0, tb-3, 
ta-1, ta-1, tb-1, tb-1, ta-2, ta-2, ta-3, ta-4, ta-5, ta-3, tb-2, ta-4, ta-0, ta-5, tb-3, 
tb-2, ta-0, ta-1, tb-3, tb-1, tb-4, 
ta-2, ta-3, ta-1, ta-4, tb-1, ta-5, ta-2, tb-2, ta-3, ta-0, ta-4, tb-3, ta-5, tb-4, 
tb-2, ta-0, tb-3, tb-4, tb-5, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 
ta-1, tb-1, ta-2, ta-3, ta-4, ta-5, tb-2, ta-0, tb-3, tb-4, tb-5, tb-0, 

3、Map接口

Map接口的实现类的特点是以键值对(key-value)的形式进行存储。

public interface Map< K, V > {...}

常见的实现类有:HashMap,Hashtable,LinkedHashMap,TreeMap,Attributes,Properties等等。

下面先介绍一下Map接口中一些声明的主要方法。
int size():返回map集合的大小。
boolean isEmpty():判断集合是否为空。
boolean containsKey(Object key):判断是否包含某个key。
boolean containsValue(Object value):判断是否包含某个value。
V get(Object key):通过key获取值。
V put(K key, V value):向map集合中新增元素。
V remove(Object key):根据key移除对应元素。
void clear():清空map集合。
Set< K > keySet():获取key的集合。
还有equals和hashCode两个常见的方法,就不列举了。

遍历Map的方法:

Map<Object, Object> map = new HashMap<>();
// 遍历键
for(Object obj : map.keySet()){
    System.out.println("key = " + obj);
}

// Map中的值遍历
for(Object obj : map.values()){
    System.out.println("value = " + obj);
}

// 在for-each循环中使用entries来遍历
for (Map.Entry<Object , Object > entry : map.entrySet()) {
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  

// 使用Iterator遍历
Iterator<Map.Entry<Object , Object >> entries = map.entrySet().iterator();
while (entries.hasNext()) {  
    Map.Entry<Integer, Integer> entry = entries.next();  
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());  
}  

// 键值遍历
for (Object key : map.keySet()) { 
    Object value = map.get(key);  
    System.out.println("Key = " + key + ", Value = " + value);  

}

(1)HashMap

HashMap它实现了Map接口,所以存储的元素也是键值对映射的结构,并允许使用null值和null键,其内元素是无序的,如果要保证有序,可以使用LinkedHashMap。HashMap是线程不安全的。

HashMap的构造方法。

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
}

HashMap中key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。

下面是put方法:

public V put(K key, V value) {
    // 处理key为null,HashMap允许key和value为null  
    if (key == null)
        return putForNullKey(value);
    // 得到key的哈希码
    int hash = hash(key.hashCode());
    // 通过哈希码计算出bucketIndex  
    int i = indexFor(hash, table.length);
    // 取出bucketIndex位置上的元素,并循环单链表,判断key是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 哈希码相同并且对象相同时  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            // 新值替换旧值,并返回旧值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // key不存在时,加入新元素
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

还有关于HashMap的深入理解的文章《HashMap之深入理解》

看完上面的文章之后,自己初步总结的结论是:
HashMap在得到哈希值的时候,先是使用了hashCode计算出key的hash值,然后再调用本身的hash方法做更细致的计算,以降低hash冲突发生的概率(自认为一个好的哈希函数可以降低哈希冲突发生的次数)。在发生哈希冲突的时候,HashMap采用链表解决冲突。将新的节点(addEntry)加到对应位置的表头(第一个位置)。
HashMap另一个是及时扩容,通过哈希表容量*负载因子计算出一个临界值,到达这个临界值的时候,将做扩容操作,然后将老数组复制后给新数组。所以合理利用哈希表长度和负载因子可以有效提供HashMap的性能。扩容之后,元素对应的位置一般会发生改变。

另一个问题是,HashMap的方法是不同步的(线程不安全的),解决这个问题的方法可以有:
一是使用Hashtable;二是使用Collections类的synchronizedMap方法得到一个封装对象。

(2)Hashtable
Hashtable与HashMap类似,包括底层实现,哈希冲突的处理,扩容等等,但是Hashtable是线程安全的,HashMap是线程不安全的。


当然此处经常会有一个问题会遇到:
Hashtable和HashMap的区别
①继承的父类不同。Hashtable基于Dictionary类,而HashMap是基于AbstractMap。
②Hashtable的键和值都不允许为null值;而HashMap允许键和值都是null。
③Hashtable的方法是同步的,线程安全;HashMap的方法是不同步的,线程不安全。
④hash值不同。哈希值的使用不同,Hashtable直接使用对象的hashCode。而HashMap重新计算hash值。
⑤内部实现使用的数组初始化和扩容方式不同。
Hashtable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍+1,而HashMap扩容时,将容量变为原来的2倍。
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

(3)LinkedHashMap
LinkedHashMap继承了HashMap类,与HashMap有着同样的存储结构,但它又增加了一个双向链表的头结点,将所有添加到LinkedHashmap的节点一一串成了一个双向循环链表,所以LinkedHashMap也保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。

public class LinkedHashMap<K,V>
    extends HashMap<K,V> implements Map<K,V>{

    private transient Entry<K,V> header;
    ...
}

其构造函数

// 默认构造方法
public LinkedHashMap() {
    super();
    accessOrder = false;
}

// 指定集合的初始容量大小的构造方法
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

// 指定集合初始容量大小和负载因子的构造方法
public LinkedHashMap(int initialCapacity, float loadFactor){
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

// 指定集合容量大小、负载因子、排序方式的构造方法
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

// 传入Map的构造方法
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}

其中accessOrder返回false代表是基于插入顺序,返回true代表是基于访问顺序。

LinkedHashMap内部类Entry定义了其前后节点的引用,构成了一个双向链表的结构基础。

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    Entry<K,V> before, after;
    ...
}

在LinkedHashMap中,put方法未重写,而是重写了父类HashMap中put方法中的调用方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。

LinkedHashMap 能够做到按照插入顺序或者访问顺序进行迭代。

(4)TreeMap
TreeMap是基于红黑树实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap是非同步的。

// 默认构造函数。使用该构造函数,TreeMap中的元素按照自然排序进行排列。
public TreeMap() {
    comparator = null;
}

// 创建的TreeMap包含Map
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

// 指定Tree的比较器
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

// 创建的TreeSet包含SortedMap
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}
// 这是TreeMap很重要的成员变量
private final Comparator<? super K> comparator;
private transient Entry<K,V> root = null;
private transient int size = 0;

这是TreeMap内部类Entry的结构,内部结构是树。

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

    ...
}

此处关于红黑树的内容不做描述(因为我还没去看= =以后补上),因为TreeMap是基于红黑树算法实现的,所以此处就暂时到这里了。先附上一篇文章。
《Java提高篇(二七)—–TreeMap》


最后附上一个问题:

重写equals要满足几个条件
自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
对于任何非空引用值 x,x.equals(null) 都应返回 false。


其实集合框架的内容很多也很细,本来想做一些详细的学习和介绍,但这里只是做一个简单的了解和描述。也是自己学习Java中集合框架的一个起点吧,再接再厉。因为第一次写,没什么经验,结构和内容应该是比较混乱的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值