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 extends E> 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
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。在链表中的每个元素才是真正的
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之后,它的结构大致如下图:
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对应的链表)
// 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:
LinkedList:
Stack:
Queue:
HashSet:
Map: