JAVA常用数据结构及原理分析(面试总结)






JAVA常用数据结构及原理分析(面试总结)

最近准备面试,因此整理一份Java中常用的数据结构资料,方便面试;

java.util包中三个重要的接口及特点:List(列表)、Set(保证集合中元素唯一)、Map(维护多个key-value键值对,保证key唯一)。其不同子类的实现各有差异,如是否同步(线程安全)、是否有序。

常用类继承树:

这里写图片描述

以下结合源码讲解常用类实现原理及相互之间的差异。

Collection (所有集合类的接口)
List、Set都继承自Collection接口,查看JDK API,操作集合常用的方法大部分在该接口中定义了。

这里写图片描述

Collections (操作集合的工具类)
对于集合类的操作不得不提到工具类Collections,它提供了许多方便的方法,如求两个集合的差集、并集、拷贝、排序等等。
由于大部分的集合接口实现类都是不同步的,可以使用Collections.synchronized*方法创建同步的集合类对象。
如创建一个同步的List:
List synList = Collections.synchronizedList(new ArrayList());
其实现原理就是重新封装new出来的对象,操作对象时用关键字synchronized同步。看源码很容易理解。
Collections部分源码:
  1. static class SynchronizedCollection<e> implements Collection<e>, Serializable {  
  2.    
  3.         final Collection<e> c;  // Backing Collection  
  4.         final Object mutex;     // Object on which to synchronize  
  5.    
  6.         SynchronizedCollection(Collection<e> c) {  
  7.             if (c==null)  
  8.                 throw new NullPointerException();  
  9.             this.c = c;  
  10.             mutex = this;  
  11.         }  
  12.         //…  
  13.         public boolean add(E e) {  
  14.             //操作集合时简单调用原本的ArrayList对象,只是做了同步  
  15.             synchronized (mutex) {return c.add(e);}  
  16.         }  
  17.         //…  
  18. }  
static class SynchronizedCollection<e> implements Collection<e>, Serializable {

        final Collection<e> c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize

        SynchronizedCollection(Collection<e> c) {
            if (c==null)
                throw new NullPointerException();
            this.c = c;
            mutex = this;
        }
        //...
        public boolean add(E e) {
            //操作集合时简单调用原本的ArrayList对象,只是做了同步
            synchronized (mutex) {return c.add(e);}
        }
        //...
}

List (列表)

ArrayList、Vector是线性表,使用Object数组作为容器去存储数据的,添加了很多方法维护这个数组,使其容量可以动态增长,极大地提升了开发效率。它们明显的区别是ArrayList是非同步的,Vector是同步的。不用考虑多线程时应使用ArrayList来提升效率。
ArrayList、Vector 部分源码:

  1. public boolean add(E e) {  
  2.     ensureCapacityInternal(size + 1);  // Increments modCount!!  
  3.     //可以看出添加的对象放到elementData数组中去了  
  4.     elementData[size++] = e;  
  5.     return true;  
  6. }  
  7. //ArrayList.remove  
  8. public E remove(int index) {  
  9.     rangeCheck(index);  
  10.    
  11.     modCount++;  
  12.     E oldValue = elementData(index);  
  13.    
  14.     int numMoved = size - index - 1;  
  15.     if (numMoved > 0)  
  16.         //移除元素时数组产生的空位由System.arraycopy方法将其后的所有元素往前移一位,System.arraycopy调用虚拟机提供的本地方法来提升效率  
  17.         System.arraycopy(elementData, index+1, elementData, index,  
  18.                          numMoved);  
  19.     elementData[–size] = null// Let gc do its work  
  20.    
  21.     return oldValue;  
  22. }  
  23.    
  24. //Vector add方法上多了synchronized关键字  
  25. public synchronized boolean add(E e) {  
  26.     modCount++;  
  27.     ensureCapacityHelper(elementCount + 1);  
  28.     elementData[elementCount++] = e;  
  29.     return true;  
  30. }  
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //可以看出添加的对象放到elementData数组中去了
    elementData[size++] = e;
    return true;
}
//ArrayList.remove
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        //移除元素时数组产生的空位由System.arraycopy方法将其后的所有元素往前移一位,System.arraycopy调用虚拟机提供的本地方法来提升效率
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // Let gc do its work

    return oldValue;
}

//Vector add方法上多了synchronized关键字
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

LinkedList是链表,略懂数据结构就知道其实现原理了。链表随机位置插入、删除数据时比线性表快,遍历比线性表慢。
双向链表原理图:
这里写图片描述
LinkedList部分源码:
  1. public class LinkedList<e>  
  2. extends AbstractSequentialList<e>  
  3. implements List<e>, Deque<e>, Cloneable, java.io.Serializable  
  4. {  
  5.     //头尾节点  
  6.     transient Node<e> first;  
  7.     transient Node<e> last;  
  8. }  
  9. //节点类  
  10.  private static class Node<e> {  
  11.     //节点存储的数据  
  12.     E item;  
  13.     Node<e> next;  
  14.     Node<e> prev;  
  15.     Node(Node<e> prev, E element, Node<e> next) {  
  16.         this.item = element;  
  17.         this.next = next;  
  18.         this.prev = prev;  
  19.     }  
  20. }  
public class LinkedList<e>
extends AbstractSequentialList<e>
implements List<e>, Deque<e>, Cloneable, java.io.Serializable
{
    //头尾节点
    transient Node<e> first;
    transient Node<e> last;
}
//节点类
 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;
    }
}

由此可根据实际情况来选择使用ArrayList(非同步、非频繁删除时选择)、Vector(需同步时选择)、LinkedList(频繁在任意位置插入、删除时选择)。

Map(存储键值对,key唯一)

HashMap结构的实现原理是将put进来的key-value封装成一个Entry对象存储到一个Entry数组中,位置(数组下标)由key的哈希值与数组长度计算而来。如果数组当前下标已有值,则将数组当前下标的值指向新添加的Entry对象。
有点晕,看图吧:


这里写图片描述
看完图再看源码,非常清晰,都不需要注释。
  1. public class HashMap<k,v>  
  2. extends AbstractMap<k,v>  
  3. implements Map<k,v>, Cloneable, Serializable  
  4. {  
  5.     transient Entry<k,v>[] table;  
  6.     public V put(K key, V value) {  
  7.         if (key == null)  
  8.             return putForNullKey(value);  
  9.         int hash = hash(key);  
  10.         int i = indexFor(hash, table.length);  
  11.         //遍历当前下标的Entry对象链,如果key已存在则替换  
  12.         for (Entry<k,v> e = table[i]; e != null; e = e.next) {  
  13.         Object k;  
  14.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  15.             V oldValue = e.value;  
  16.             e.value = value;  
  17.             e.recordAccess(this);  
  18.             return oldValue;  
  19.         }  
  20.     }  
  21.        addEntry(hash, key, value, i);  
  22.         return null;  
  23.     }  
  24. }  
  25. static class Entry<k,v> implements Map.Entry<k,v> {  
  26.     final K key;  
  27.     V value;  
  28.     Entry<k,v> next;  
  29.     int hash;  
  30. }  
public class HashMap<k,v>
extends AbstractMap<k,v>
implements Map<k,v>, Cloneable, Serializable
{
    transient Entry<k,v>[] table;
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        //遍历当前下标的Entry对象链,如果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;
        }
    }
       addEntry(hash, key, value, i);
        return null;
    }
}
static class Entry<k,v> implements Map.Entry<k,v> {
    final K key;
    V value;
    Entry<k,v> next;
    int hash;
}

TreeMap是由Entry对象为节点组成的一颗红黑树,put到TreeMap的数据默认按key的自然顺序排序,new TreeMap时传入Comparator自定义排序。红黑树网上很多资料,我讲不清,这里就不介绍了。

Set(保证容器内元素唯一性)
之所以先讲Map是因为Set结构其实就是维护一个Map来存储数据的,利用Map结构key值唯一性
HashSet部分源码:

  1. public class HashSet<e>  
  2. extends AbstractSet<e>  
  3. implements Set<e>, Cloneable, java.io.Serializable  
  4. {      
  5.    
  6.     //无意义对象来作为Map的value   
  7.     private static final Object PRESENT = new Object();  
  8.     public boolean add(E e) {  
  9.         return map.put(e, PRESENT)==null;  
  10.     }  
  11. }  
public class HashSet<e>
extends AbstractSet<e>
implements Set<e>, Cloneable, java.io.Serializable
{    

    //无意义对象来作为Map的value 
    private static final Object PRESENT = new Object();
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
}
  1. <span style=“font-size:18px;”><strong> HashMap 支持key=null 但是 Hashtable 不支持 key =null</strong></span><span style=“font-size:14px;”>  
  2. </span>  
 HashMap 支持key=null 但是 Hashtable 不支持 key =null


HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

  1. HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
  2. HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
  3. 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
  4. 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
  5. HashMap不能保证随着时间的推移Map中的元素次序是不变的。

要注意的一些重要术语:

1) sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。

2) Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。

3) 结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。

我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);

结论

Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。



HashSet、TreeSet分别默认维护一个HashMap、TreeMap。

猜你喜欢

转载自blog.csdn.net/lilinsqq/article/details/80046882