随笔③ java集合类 --- ArrayList,LinkedList,Vector,Hashtable,HashMap,ConcurrentHashMap

java集合类 --- 继承关系

 

java集合类 --- List接口

java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

一:ArrayList

① ArrayList有三个构造函数

(1)ArrayList()构造一个初始容量为 10 的空列表。

(2)ArrayList(Collection<? extends E> c)构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的

(3)ArrayList(int initialCapacity)构造一个具有指定初始容量的空列表

② ArrayList是实现了基于动态数组的数据结构

这里的所谓动态数组并不是那个“ 有多少元素就申请多少空间 ”的意思,通过查看源码,可以发现,这个动态数组是这样实现的,如果没指定数组大小,则申请默认大小为10的数组,当元素个数增加,数组无法存储时,系统会另个申请一个长度为当前长度1.5倍的数组,然后,把之前的数据拷贝到新建的数组中

③ ArrayList的Iterator实现

在ArrayList内部首先是定义一个内部类Itr,该内部类实现Iterator接口,如下:

1 private   class  Itr  implements  Iterator<E> {  
2     //do something   
3 }  

而ArrayList的iterator()方法实现:

1 public  Iterator<E> iterator() {  
2      return   new  Itr();  
3 }  

所以通过使用ArrayList.iterator()方法返回的是Itr()内部类,所以现在我们需要关心的就是Itr()内部类的实现:

在Itr内部定义了三个int型的变量:cursor、lastRet、expectedModCount。其中cursor表示下一个元素的索引位置,lastRet表示上一个元素的索引位置。

1 int  cursor;               
2 int  lastRet = - 1 ;       
3 int  expectedModCount = modCount; 

从cursor、lastRet定义可以看出,lastRet一直比cursor少一所以hasNext()实现方法异常简单,只需要判断cursor和lastRet是否相等即可。

1 public   boolean  hasNext() {  
2     return  cursor != size;  
3 }  

对于next()实现其实也是比较简单的,只要返回cursor索引位置处的元素即可,然后修改cursor、lastRet即可,

 1 public  E next() {   
 2    checkForComodification();  
 3   int  i = cursor;     //记录索引位置    
 4    if  (i >= size)     //如果获取元素大于集合元素个数,则抛出异常   
 5    throw   new  NoSuchElementException();  
 6    Object[] elementData = ArrayList.this .elementData;  
 7    if  (i >= elementData.length)  
 8        throw   new  ConcurrentModificationException();  
 9    cursor = i + 1 ;       //cursor + 1   
10    return  (E) elementData[lastRet = i];   //减少数据依赖cursor,有利于指令重排序lastRet + 1 且返回cursor处元素   
11 }  

二:LinkedList

LinkedList是基于链表的数据结构。ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间。

三:ConcurrentHashMap

jdk1.7中的ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

 

JDK6,7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁使用volatile来保证可见性,当要统计全局时(比如size),首先会尝试多次计算modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。

jdk7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能,所以jdk8 中完全重写了concurrentHashmap,代码量从原来的1000多行变成了 6000多 行,实现上也和原来的分段式存储有很大的区别

主要设计上的变化有以下几点:

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V> table保存数据采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

 四:Vector & ArrayList 的主要区别 

1) 同步性:Vector是线程安全的,也就是说是同步的 ,而ArrayList 是线程序不安全的,不是同步的。 
2)数据增长:当需要增长时,Vector默认增长为原来一倍 ,而ArrayList却是原来的50%  ,这样,ArrayList就有利于节约内存空间。 
      如果涉及到堆栈,队列等操作,应该考虑用Vector,如果需要快速随机访问元素,应该使用ArrayList 。
【扩展知识】:

1. Hashtable & HashMap 
Hashtable和HashMap在它们的性能方面的比较类似Vector和ArrayList,比如Hashtable的方法是同步的,而HashMap的不是。

2. ArrayList & LinkedList

ArrayList的内部实现是基于内部数组Object[],所以从概念上讲,它更象数组,但LinkedList的内部实现是基于一组连接的记录,所以,它更象一个链表结构,所以,它们在性能上有很大的差别:   
       从上面的分析可知,在ArrayList的前面或中间插入数据时,你必须将其后的所有数据相应的后移,这样必然要花费较多时间,所以,当你的操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能; 而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。

五:HashMap与Hashtable

1.  关于HashMap的一些说法:

 

a)  HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。HashMap的底层结构是一个数组,数组中的每一项是一条链表。

 

b)  HashMap的实例有两个参数影响其性能: “初始容量” 和 装填因子

 

c)  HashMap实现不同步,线程不安全。 HashTable线程安全

 

d) HashMap中的key-value都是存储在Entry中的

 

e)  HashMap可以存null键和null值,不保证元素的顺序恒久不变,它的底层使用的是数组和链表,通过hashCode()方法和equals方法保证键的唯一性

 

f) 解决冲突主要有三种方法:定址法,拉链法,再散列法。HashMap是采用拉链法解决哈希冲突的

 

【注】: 链表法是将相同hash值的对象组成一个链表放在hash值对应的槽位;

 

用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。 沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。

 

拉链法解决冲突的做法是: 将所有关键字为同义词的结点链接在同一个单链表中 。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。拉链法适合未规定元素的大小。

 


2.  Hashtable和HashMap的区别:

 

a) 继承不同。  public class Hashtable extends Dictionary implements Map

 

public class HashMap extends AbstractMap implements Map

 

b)  Hashtable中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。

 

c)  Hashtable 中, key 和 value 都不允许出现 null 值。

 

在 HashMap 中, null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。当 get() 方法返回 null 值时,即可以表示 HashMap 中没有该键,也可以表示该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。

 

d)  两个遍历方式的内部实现上不同。Hashtable、HashMap都使用了Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

 

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

 

f)  Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。

 

HashTable中hash数组默认大小是11,增加的方式是old*2+1。

 

HashMap中hash数组的默认大小是16,而且一定是2的指数。

 

【注】: HashSet子类依靠hashCode()和equal()方法来区分重复元素

 

HashSet内部使用Map保存数据,即将HashSet的数据作为Map的key值保存,这也是HashSet中元素不能重复的原因。而Map中保存key值的,会去判断当前Map中是否含有该Key对象,内部是先通过key的hashCode,确定有相同的hashCode之后,再通过equals方法判断是否相同。

 

posted @ 2017-11-10 19:25  叶枫啦啦  阅读(278)  评论(0编辑  收藏  举报