JavaBasicsNo: Java Data Structure

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部分源码:

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 部分源码:

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部分源码:

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对象。

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部分源码:

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;

    }

}

HashMap和Hashtable的区别

HashMap和Hashtable都实现了Map接口,主要的区别有:线程安全性,同步(synchronization),以及速度。

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

Hashtable、HashMap、TreeMap心得

三者均实现了Map接口,存储的内容是基于key-value的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值。

(1)元素特性
HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然。
(2)顺序特性
HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式。
(3)初始化与增长方式
初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。
扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。
(4)线程安全性

HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。(就是说,如果你不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。)


HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。
 
(5)HashMap

HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回 值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。(注意这是JDK1.8后的内容)

HashMap 解析:

注意 : 在Java 8 里  HashMap本身发生了非常大的变化
  1. hashmap 内部实现分析  :
  2. 容量 (capacity )和负载因子( load factor)
  3. 树化
    1.  
??????????

猜你喜欢

转载自blog.csdn.net/ddhmbbklyk2018/article/details/81660260