Java集合类详解

Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。

Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。

List里存放的对象是有序的(添加元素的顺序,而不是字典序),同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。

Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map的输出是有序的,按照字典序(即hashcode的大小顺序)iterator输出。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。

Iterator,所有的集合类,都实现了Iterator接口,这是一个用于遍历集合中元素的接口主要包含以下三种方法:

    1.hasNext()是否还有下一个元素。

    2.next()返回下一个元素。

    3.remove()删除当前元素。

ArrayList和LinkedList在用法上没有区别,但是在功能上还是有区别的。LinkedList经常用在增删操作较多而查询操作很少的情况下,ArrayList则相反。

一、ArrayList

以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按数组下标访问元素—get(i)/set(i,e)的性能很高,这是数组的基本优势。直接在数组末尾加入元素—add(e)的性能也高,但如果按下标插入、删除元素—add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。

ArrayList是一个相对来说比较简单的数据结构,最重要的一点就是它的自动扩容,可以认为就是我们常说的“动态数组”。

当我们在ArrayList中增加元素的时候,会使用add函数。他会将元素放到末尾。具体实现如下:

public boolean add(E e) {
    ensureCapacityInternal(size + 1); //自动扩容
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal()函数判断传入的参数size+1是否大于当前容量(默认容量为10),若大于则自动扩容,将数组变为原长度的1.5倍,之后的操作就是把老的数组拷到新的数组里面。然后再把当前元素加入该数组。

Array的set和get函数就比较简单了,先做index检查,然后执行赋值或访问操作

public E set(int index, E element) {
    rangeCheck(index);
    E oldValue =elementData(index);
    elementData[index] =element;
    return oldValue;
}
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

快速失败(fail-fast):

它是Java集合的一种错误检测机制。某个线程在对collection进行迭代时,不允许其他线程对该collection进行结构上的修改。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast。

Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;

由于HashMap(ArrayList)并不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map(这里的修改是指结构上的修改,并非指单纯修改集合内容的元素),那么将要抛出ConcurrentModificationException 即为fail-fast策略  

二、LinkedList

LinkedList 是一个双向循环链表,且头结点不存放数据。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList实现 List 接口,能对它进行队列操作。header是双向链表的头节点,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。 size是双向链表中节点实例的个数。

LinkedList中之定义了两个属性:

private transient Entry header = new Entry(null, null, null);
private transient int size = 0;

header是双向链表的头节点,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。

size是双向链表中节点实例的个数。


Entry代码:

private static class Entry {
    E element;
    Entry next;
    Entry previous;

    Entry(E element,Entry next, Entry previous) {
        this.element =element;
        this.next = next;
        this.previous =previous;
   }
}
public LinkedList() {
    header.next = header.previous = header;
}
public LinkedList(Collection c) {
    this();
    addAll(c);
}

LinkedList提供了两个构造方法。第一个构造方法不接受参数,将header实例的previous和next全部指向header实例(注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null),这样整个链表其实就只有header一个节点,用于表示一个空的链表。第二个构造方法接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。

Add:

// 将元素(E)添加到LinkedList中
     public boolean add(E e) {
         // 将节点(节点数据是e)添加到表头(header)之前。
         // 即,将节点添加到双向链表的末端。
         addBefore(e, header);
         return true;
     }

     public void add(int index, E element) {
         addBefore(element, (index==size ? header : entry(index)));
     }
    
    private Entry addBefore(E e, Entry entry) {
         Entry newEntry = new Entry(e, entry, entry.previous);
         newEntry.previous.next = newEntry;
         newEntry.next.previous = newEntry;
         size++;
         modCount++;
         return newEntry;
    }

addBefore(E e,Entry entry)先通过Entry的构造方法创建e的节点newEntry(包含了将其下一个节点设置为entry,上一个节点设置为entry.previous的操作,相当于修改newEntry的“指针”),之后修改插入位置后newEntry的前一节点的next引用和后一节点的previous引用,使链表节点间的引用关系保持正确。之后修改和size大小和记录modCount,然后返回新插入的节点。

public boolean addAll(Collection c)

    添加指定collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。

Remove:

private E remove(Entry e) {
    if (e == header)
        throw new NoSuchElementException();
    // 保留将被移除的节点e的内容
    E result = e.element;
   // 将前一节点的next引用赋值为e的下一节点
    e.previous.next = e.next;
   // 将e的下一节点的previous赋值为e的上一节点
    e.next.previous = e.previous;
   // 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用
   e.next = e.previous = null;
  // 将被移除的节点的内容设为null
  e.element = null;
  // 修改size大小
  size--;
  modCount++;
  // 返回移除节点e的内容
  return result;
}

由于删除了某一节点因此调整相应节点的前后指针信息,如下:

e.previous.next = e.next;//预删除节点的前一节点的后指针指向预删除节点的后一个节点。

e.next.previous = e.previous;//预删除节点的后一节点的前指针指向预删除节点的前一个节点。

清空预删除节点:

e.next = e.previous = null;

e.element = null;

交给gc完成资源回收,删除操作结束。

与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。


Get(int)方法

首先判断位置信息是否合法(大于等于0,小于当前LinkedList实例的Size),然后遍历到具体位置,获得节点的业务数据(element)并返回。为了提高效率,需要根据获取的位置判断是从头还是从尾开始遍历。

三、遍历(Iterator迭代器):

Set s=new HashSet();
List list =new LinkedList();
s.add("abc");
s.add("aaa");
Iterator it=s.iterator();   //list.iterator();
        
while(it.hasNext())
{
      System.out.println(it.next());
}

四、HashMap

http://www.importnew.com/19938.html

影响HashMap性能的两个重要参数:初始容量(默认为16),加载因子(默认为0.75)。其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。

HashMap底层实现还是数组,只是数组的每一项都是一条链。Entry数组。

在HashMap的实现中,采用的链地址法来解决冲突,它有一个桶的概念:对于Entry数组而言,数组的每个元素处存储的是链表,而不是直接的Value。在链表中的每个元素才是真正的。而一个链表,就是一个桶!因此HashMap最多可以有Entry.length 个桶。

static classEntry implements Map.Entry {
        final K key;
        V value;
        Entry next;//指向下一个节点,连成链
        int hash;
}

Hash的原理:在散列码的基础上,定义一个哈希函数,再对哈希函数计算出的结果求模,最终得到该对象在哈希表的位置。哈希函数hash(Object k) 中用到了hashCode()。然后再经过进一步的特殊处理,得到一个最终的哈希值。根据该哈希值Mod N(求余)计算出其在哈希表的位置。indexFor(int h, int length)实际上完成的就是求余操作。只不过求余操作涉及到除法,而这里可以通过移位操作来代替除法。

resize 操作(即扩容)。

扩容是是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。 很明显,扩容是一个相当耗时的操作,因为它需要重新计算这些元素在新的数组中的位置并进行复制处理。

但是在Jdk1.8中,改进了该方法,使得数组容量扩大一倍,这样做的好处就是,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

比如原来的hash是对n=3(即桶的个数)取余,这时往hashmap中放两个数,7和23,取余后分别是1和2,因此分别放入第一个桶和第二个桶中。扩容以后,n=6(即桶的个数变成了6个),此时要把原来table的Entry数组元素移到新的table中,那么使7和23分别对6取模,结果是1和5(和原来分别相差0和3,即要么相差0,要么相差一个原来n的大小),可以看到,经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置,这样大大减少了计算量。

再举一个例子,桶的个数为4(n=4)这时往hashmap中放两个数,11和21,取模后可知分别放入3号桶和1号桶中,此时扩容,n=8,再次取模后,11和21分别放在3号桶和5号桶(和原来相差0和4(原n=4))

五、HashTable

和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。默认容量为11

Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。它是线程安全的。  

count是Hashtable的大小,它是Hashtable保存的键值对的数量。

Hashtable通过synchronized关键字保证线程安全。

//put,contains,remove等方法都有synchronized关键字。

public synchronized V get(Object key) {} 

遍历方法:

获取键:

Integer integ = null;
Iterator iter =table.entrySet().iterator();

while(iter.hasNext()) {
   Map.Entry entry = (Map.Entry)iter.next();
   // 获取key
   key = (String)entry.getKey();
   // 根据key,获取value
   integ= (Integer)table.get(key);
}

获取值:

Integer value = null;
Collection c = table.values();
Iterator iter= c.iterator();

while (iter.hasNext()) {
   value = (Integer)iter.next();
}

六、HashMap和Hashtable比较

HashMap:

1.  HashMap和Hashtable都是是基于哈希表实现的,每一个元素是一个key-value对

2.  HashMap是非线程安全的,只是用于单线程环境下,在hashmap做put操作的时候。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失

3.  HashMap存数据的过程是:HashMap内部维护了一个存储数据的Entry数组,HashMap采用链表解决冲突,每一个Entry本质上是一个单向链表。当准备添加一个key-value对时,首先通过hash(key)方法计算hash值,然后比较该value是否已经存在。

4.  初始容量和加载因子。这两个参数是影响HashMap性能的重要参数

HashTable:

1.  Hashtable是线程安全的,能用于多线程环境中。Hashtable中的方法是Synchronize的

2.  Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

关于NULL:

1.  Hashtable中,key和value都不允许出现null值。

2.  HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null,对应table[0]。

关于Hash值:

1.  哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。

关于是否提供contains方法:

HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。

Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。


重写equals()方法就必须重写hashCode()方法的原因:

  • hashCode()和equals()保持一致,如果equals方法返回true,那么两个对象的hasCode()返回值必须一样。如果equals方法返回false,hashcode可以不一样,即多个不同的对象可以返回同一个hash值,但是这样不利于哈希表的性能,一般我们也不要这样做。重写equals()方法就必须重写hashCode()方法的原因也就显而易见了。
  • 假设两个对象,重写了其equals方法,其相等条件是属性相等,就返回true。如果不重写hashcode方法,其返回的依然是两个对象的内存地址值,必然不相等。这就出现了equals方法相等,但是hashcode不相等的情况。这不符合hashcode的规则。
  • 重写equals()方法就必须重写hashCode()方法主要是针对HashSet和Map集合类型。集合框架只能存入对象。

    在向HashSet集合中存入一个元素时,HashSet会调用该对象(存入对象)的hashCode()方法来得到该对象的hashCode()值,然后根据该hashCode值决定该对象在HashSet中存储的位置。简单的说:HashSet集合判断两个元素相等的标准是:两个对象通过equals()方法比较相等,并且两个对象的HashCode()方法返回值也相等。如果两个元素通过equals()方法比较返回true,但是它们的hashCode()方法返回值不同,HashSet会把它们存储在不同的位置,依然可以添加成功。这样就产生了错误,因此在修改equals方法时需同时修改hashcode。

    同样:在Map集合中,存储的数据是对,key,value都是对象,被封装在Map.Entry,即:每个集合元素都是Map.Entry对象。在Map集合中,判断key相等标准也是:两个key通过equals()方法比较返回true,两个key的hashCode的值也必须相等。判断valude是否相等equal()相等即可。


重写hashCode()的原则:

(1)同一个对象多次调用hashCode()方法应该返回相同的值;

(2)当两个对象通过equals()方法比较返回true时,这两个对象的hashCode()应该返回相等的(int)值;


七、ConcurrentHashMap:

http://www.importnew.com/21781.html

ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable。对于ConcurrentHashMap是如何提高其效率的,可能大多人只是知道它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。

首先我们要知道,线程的执行会有重排序,Java编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致。

ConcurrentHashMap针对并发稍微做了一点调整。它把区间按照并发级别分成了若干个segment。默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。并发级别和每个段(segment)的初始容量都是可以通过构造函数设定的。

创建好默认的ConcurrentHashMap之后,它的结构大致如下图:

Java集合类详解_第1张图片

segment的定义:

static final class Segment extends ReentrantLock implements Serializable

Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。这种做法称之为“分离锁(lock striping)”

分拆锁(lockspliting)就是若原先的程序中多处逻辑都采用同一个锁,但各个逻辑之间又相互独立,就可以拆为使用多个锁,每个锁守护不同的逻辑。

分拆锁有时候可以被扩展成可大可小加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁(lockstriping)

拿get方法具体解释一下这个segment如何使用:

public V get(Object key) {
    int hash = hash(key); // throws NullPointerException if key null
    return segmentFor(hash).get(key, hash);
}

它没有使用同步控制,交给segment去找,再看Segment中的get方法:

V get(Object key, int hash) {
        if (count != 0) { // read-volatile // ①
            HashEntry e = getFirst(hash); 
            while (e != null) {
                if (e.hash == hash && key.equals(e.key)) {
                    V v = e.value;
                    if (v != null)  // ② 注意这里
                        return v;
                    return readValueUnderLock(e); // recheck
                }
                e = e.next;
            }
        }
        return null;
}

相当于单例模式中的双重校验锁-线程安全


八、Set集合类详解:

用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复

如果对两个引用(变量)调用hashCode方法,会得到相同的结果,如果对象所属的类没有覆盖Object的hashCode方法的话,hashCode会返回每个对象特有的序号(java是依据对象的内存地址计算出的此序号),所以两个不同的类对象的hashCode值是不可能相等的。

HashSet:

哈希表存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序(和List显然不同) 是按照哈希值来存的,所以取数据也是按照哈希值取得。可以写入空数据。

如果想要让两个不同的Person类对象视为相等的,就必须覆盖Object继下来的hashCode方法和equals方法,因为Object  hashCode方法返回的是该对象的内存地址,所以必须重写hashCode方法,才能保证两个不同的对象具有相同的hashCode,同时也需要两个不同对象比较equals方法时返回true。

TreeSet:

TreeSet的特点是:

1.不能写入空数据   2.写入的数据是有序的   3.不写入重复数据

TreeSet可以自然排序,TreeSet是有排序规则的(比较性)。

一、元素自身具备比较性:

元素自身具备比较性,需要元素实现Comparable接口,重写compareTo方法,也就是让元素自身具备比较性,这种方式叫做元素的自然排序也叫做默认排序。例如String类对象,字符串实现了一个接口,叫做Comparable 接口.字符串重写了该接口的compareTo 方法,所以String对象具备了比较性.

二、让容器自身具备比较性,自定义比较器。

定义一个类实现Comparator 接口,覆盖compare方法。并将该接口的子类对象作为参数传递给TreeSet集合的构造函数。当Comparable比较方式,及Comparator比较方式同时存在,以Comparator比较方式为主。


Java解决Hash冲突的方法:

1.开放定址法(线性探测再散列,二次探测再散列)

    线性探测再散列:向后每次加1寻找空节点并放入

    二次探测再散列:使得原数(要放入hash表的数)加1的平方;减1的平方,加2的平方,减2的平方。。。加k的平方,减k的平方。直到找到空节点放入(如58/11=3),但是3号桶中有元素,此时需要(58+1^2=59/11得4)而4号桶中仍然有元素,再将(58-1^2=57/11得2),若还有元素,则需要(58+2^2=62/11得7),若7号桶无元素,放入7号桶内。

2.再哈希法(使用其他的hash函数进行再哈希)

3.链地址法(有冲突时放入key对应的链表)

Collections.synchronizedMap()

// synchronizedMap方法
public static  Map synchronizedMap(Map m) {
       return new SynchronizedMap<>(m);
   }
// SynchronizedMap类
private static class SynchronizedMap
       implements Map, Serializable {
       private static final long serialVersionUID = 1978198479659022715L;

       private final Map m;     // Backing Map
       final Object      mutex;        // Object on which to synchronize

       SynchronizedMap(Map m) {
           this.m = Objects.requireNonNull(m);
           mutex = this;
       }

       SynchronizedMap(Map m, Object mutex) {
           this.m = m;
           this.mutex = mutex;
       }

       public int size() {
           synchronized (mutex) {return m.size();}
       }
       public boolean isEmpty() {
           synchronized (mutex) {return m.isEmpty();}
       }
       public boolean containsKey(Object key) {
           synchronized (mutex) {return m.containsKey(key);}
       }
       public boolean containsValue(Object value) {
           synchronized (mutex) {return m.containsValue(value);}
       }
       public V get(Object key) {
           synchronized (mutex) {return m.get(key);}
       }

       public V put(K key, V value) {
           synchronized (mutex) {return m.put(key, value);}
       }
       public V remove(Object key) {
           synchronized (mutex) {return m.remove(key);}
       }
       // 省略其他方法
}

SynchronizedMap类和Hashtable的不同是指,hashtable是使用synchronized关键字修饰了整个实例方法,比如:

public synchronized V get(Object key){
 do something...
} 

而SychonizedMap则是在方法内,使用synchronized关键字修饰部分代码块,该方法内的其他部分代码是不加锁的,这样使得需要同步的代码块加锁就可以了,可以很好的提高效率。


常见集合类方法总结:

ArrayList:

  • add()
  • size()
  • get()
  • set()


LinkedList:

  • add()
  • get()
  • set()
  • size()
  • peek()
  • poll()


Stack:

  • push()
  • pop()
  • peek()
  • size()
  • get()


Queue:

  • add()
  • size()
  • peek()
  • poll()


HashSet:

  • add()
  • size()
  • iterator()


Map:

  • get()
  • put()
  • size()
  • keySet()
  • valueSet()


你可能感兴趣的:(Java)