java容器类的知识整理
目录结构:
1. collection和Map的大致说明
2. List Map Set
3. List的实现类
4. Set的实现类
5. Map的实现类
6. 队列
7.并发环境下的容器类简述
1. collection和Map的大致说明
用途是:保存对象
collection : 1> 一个独立元素的序列
2> public interface Collection<E> extends Iterable<E> 那么实现该接口的所有的类都是可迭代的.
Map : 1> 一组成对的"键值对"对象
2>public interface Map<K,V>
该接口提供了两个方法:Set<K> keySet() / Set<Map.Entry<K, V>> entrySet(); set是可迭代的,所以可以认为Map的key也是可迭代的.
collections所有的实现类都重写了toString()方法,因为他们都extends继承自AbstractList。在抽象类AbstractList中重写了toString()方法。
Map所有的实现类都重写了toString()方法,因为他们都extends继承自AbstractMap。在抽象类AbstractMap中重写了toString()方法。
遍历容器的两种方式是迭代和forEach()方法,实际上只有迭代。foreach语法最终被编译器转为了对Iterator.next()的调用。迭代器的定义如下:
<span style="font-size:14px;">public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}</span>
Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构
例如,如果没有使用Iterator,遍历一个数组的方法是使用索引:
for(int i=0; i<array.size(); i++) { ... get(i) ... }
客户端都必须事先知道集合的内部结构,访问代码和集合本身是紧耦合,无法将访问逻辑从集合类和客户端代码中分离出来,每一种集合对应一种遍历方法,客户端代码无法复用。
更恐怖的是,如果以后需要把ArrayList更换为LinkedList,则原来的客户端代码必须全部重写。为解决以上问题,Iterator模式总是用同一种逻辑来遍历集合:
for(Iterator it = c.iterater(); it.hasNext(); ) { ... }
奥秘在于客户端自身不维护遍历集合的"指针",所有的内部状态(如当前元素位置,是否有下一个元素)都由Iterator来维护,而这个Iterator由集合类通过工厂方法生成,因此,它知道如何遍历整个集合。客户端从不直接和集合类打交道,它总是控制Iterator,向它发送"向前","向后","取当前元素"的命令,就可以间接遍历整个集合。
当使用Iterator对集合元素进行迭代的时候,collection并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量。所以修改迭代变量的值对集合元素本身的值没有任何改变。迭代器Iterator不保存对象,它依附于Collection对象,仅用于遍历集合。迭代器Iterator采用的是快速-失败(fail-fast)机制,一旦在迭代的过程中检测到该集合已经被修改,程序立即引发java.util.ConcurrentModificationException,而不是显示修改后的结果。这样可以避免共享资源而引发的潜在问题。
forEach语句可以用于数组或其他任何Iterable,当这并不意味着数组肯定也是一个Iterable.而任何的自动包装也不会自动发生. 可以将数组包装成一个集合进行forEach遍历,你会发现它会报错.
<span style="font-size:14px;">public class TestCollections {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10; i++) {
list.add("hello_" + i);
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
if (str.equals("hello_5")) {
// iterator.remove();
list.remove(str);
}
str = "hehe";
}
System.out.println(list);
}
}</span>
执行 { str = "hello" } 语句,对外部的元素没有任何影响,执行 iterator.remove()会删除当前的迭代对象. 执行list.remove(str) ,会报java.util.ConcurrentModificationException ,因为他检测到集合的元素被修改了.
Iterator只有向后迭代的能力, 集合中还有一种迭代listIterator, 它在Iterator的基础上增加了向前迭代的方法,但是该迭代器只能用于遍历list的集合,因为它在遍历的过程中可以添加元素,添加重复元素,违反set的规则。
<span style="font-size:14px;">public interface ListIterator<E> extends Iterator<E></span>
示例如下:
<span style="font-size:14px;">import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorTest {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 3; i++) {
list.add("test_" + i);
}
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
//向listIterator.next()得到的元素之后添加元素
listIterator.add("×××××××××××××××分割线×××××××××××××××");
}
System.out.println("*****************************************************************************");
//此时迭代的指针指向list集合的最后一个元素,所以hasNext为false
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
System.out.println("*****************************************************************************");
System.out.println("逆序迭代开始:===================");
//向前迭代
while (listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
// 迭代内部调用set方法,设置的是当前的listIterator.previous()指向的元素的值,类似于remove的用法
listIterator.set("设置元素");
}
// 迭代外部调用set方法,设置的是迭代完成之后的最后一个元素的值,对于向后迭代,修改的是最后一个元素的值,对于向前迭代,自然修改的是第一个元素的值。
listIterator.set("listIterator");
System.out.println("查看修改后的元素:===================");
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
listIterator.set("你好");
System.out.println("查看第二次修改后的元素:===================");
while (listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
}
}
}</span>
结果:
<span style="font-size:14px;">test_0
test_1
test_2
*****************************************************************************
*****************************************************************************
逆序迭代开始:===================
×××××××××××××××分割线×××××××××××××××
test_2
×××××××××××××××分割线×××××××××××××××
test_1
×××××××××××××××分割线×××××××××××××××
test_0
查看修改后的元素:===================
listIterator
设置元素
设置元素
设置元素
设置元素
设置元素
查看第二次修改后的元素:===================
你好
设置元素
设置元素
设置元素
设置元素
listIterator</span>
Iterator和ListIterator主要区别有:
一、ListIterator有add()方法,可以向List中添加对象,而Iterator不能。
二、ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
三、ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator 没有此功能。
四、都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。
集合支持泛型, 泛型用来限制集合中的元素类型,使得代码更加健壮。使用泛型,可以在编译器防止将错误类型的对象放置在容器中. 并且使用泛型在将对象从集合中取出来的时候不需要做类型转换,
2. List Map Set
其中List和Set继承自Collection接口。
Set不允许元素重复。HashSet和TreeSet是两个主要的实现类。
List有序且允许元素重复。ArrayList、LinkedList和Vector是三个主要的实现类。
Map也属于集合系统,但和Collection接口不同。Map是key对value的映射集合,其中key列就是一个集合。key不能重复,但是value可以重复。HashMap、TreeMap和Hashtable是三个主要的实现类。
SortedSet和SortedMap接口对元素按指定规则排序,SortedMap是对key列进行排序。
有序否 | 允许元素重复否 | ||
Collection | 否 | 是 | |
List | 是 | 是 | |
Set | AbstractSet | 否 | 否 |
HashSet | |||
TreeSet | 是(用二叉树排序) | ||
Map | AbstractMap | 否 | 使用key-value来映射和存储数据,Key必须惟一,value可以重复 |
HashMap | |||
TreeMap | 是(用二叉树排序) |
3. List的实现类
list判断两个对象相等的标准是只要通过equals方法比较返回true即可。
ArrayList
ArrayList 采用的是数组形式来保存对象的,这种方式将对象放在连续的位置中,所以最大的缺点就是插入删除时非常麻烦,因为的插入是,先对数组扩容,然后赋值. ArrayList的底层实现是通过数组来实现的.数组不能保存长度变化的数组,数组也不能保存具有映射关系的数据。数组可以存放基本数据类型,集合只能保存对象类型
<span style="font-size:14px;">import java.util.ArrayList;
import java.util.List;
public class ListTest {
public static void main(String[] args) {
List<String> stringList = new ArrayList<String>();
stringList.add(new String("heheh"));
stringList.add(new String());
stringList.add(new String());
stringList.add(null);
stringList.add(null);
System.out.println(stringList);
stringList.remove(new A());
System.out.println(stringList);
stringList.remove(new A());
System.out.println(stringList);
}
}
class A{
@Override
public boolean equals(Object obj) {
return true;
}
}</span>
结果是:
<span style="font-size:14px;">[heheh, , , null, null]
[, , null, null]
[, null, null]
</span>
可以看出: list允许存放重复元素,并且允许null. remove(Object o) 删除的list中第一个匹配的元素。list集合能够自动扩容,初始容量是10。可以设置list的容量大小.
Vector
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的。Vector本身为了多线程访问,就已经做了同步的操作,例如锁操作。当线程再为了Vector做同步时,显得多此一举。如果多个线程对这个Vector的同步没做好,有可能出现死锁等问题。所以当多线程访问Vector时,无需再考虑同步的问题。
Vector之所以是线程安全的,他的大部分实现的方法,都有synchronized关键字,也就是方法本身已经加锁了。既然被同步了,多个线程就不可能同时访问vector中的数据,只能一个一个地访问,所以不会出现数据混乱的情况,所以是线程安全的。
ArrayList和Vector类都是基于数组实现的List类, 都封装了一个动态再分配的Object[] 数组,都有他一个capacity属性,用来表示数组的长度,并且提供两个操作该属性的方法.
1.将集合的capacity调整为minCapacity
<span style="font-size:14px;"> /**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) </span>
<span style="font-size:14px;"> /**
* Trims the capacity of this <tt>ArrayList</tt> instance to be the
* list's current size. An application can use this operation to minimize
* the storage of an <tt>ArrayList</tt> instance.
*/
public void trimToSize() </span>
Stack
public class Stack<E> extends Vector<E> {}
Stack 是Vector的子类, 他用于模拟栈这种数据结构."栈"通常是指"后进先出"(LIFO)的容器,进栈出栈的元素都是Object类型的,因此取出来的元素都要做类型转换.除非只是用Object的操作. LinkedList具有能够直接实现栈的所有功能的方法.因此可以直接将LinkedList作为栈使用.
Stack 类的push/pop ,实际上还是调用的Vector类的addElement()和removeElement()方法。
<span style="font-size:14px;">import java.util.Stack;
import java.util.Vector;
public class StackTest {
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
stack.add("hellop-1");
stack.add("hellop-2");
stack.add("hellop-3");
stack.add("hellop-4");
stack.add("hellop-5");
stack.add("hellop-6");
System.out.println(stack);
// peek()查看顶部元素,但是不删除
System.out.println(stack.peek());
System.out.println(stack);
// 输出顶部元素,同时删除
System.out.println(stack.pop());
// 调用Vector的remove方法
System.out.println(stack.remove(3));
System.out.println(stack);
}
}</span>
输出:
<span style="font-size:14px;">[hellop-1, hellop-2, hellop-3, hellop-4, hellop-5, hellop-6]
hellop-6
[hellop-1, hellop-2, hellop-3, hellop-4, hellop-5, hellop-6]
hellop-6
hellop-4
[hellop-1, hellop-2, hellop-3, hellop-5]</span>
固定长度的list
<span style="font-size:14px;">import java.util.Arrays;
import java.util.List;
public class FixedSizeList {
public static void main(String[] args) {
List<String> fixedList = Arrays.asList(new String[]{"test1","test2","test3"});
System.out.println(fixedList);
fixedList.add("test4");
System.out.println(fixedList);
}
}</span>
结果:<span style="font-size:14px;">[test1, test2, test3]
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:131)
at java.util.AbstractList.add(AbstractList.java:91)
at com.evan.test.collection.FixedSizeList.main(FixedSizeList.java:11)</span>
Arrays.asList(Object... a)
<span style="font-size:14px;"> public static <T> List<T> asList(T... a) {
return new ArrayList<T>(a);
}</span>
根据特定的数组返回固定长度的数组,返回的list实现了序列化和RandomAcces随机访问接口。程序只能遍历访问集合里的元素,不能增加或删除集合里的元素。允许根据索引修改集合的元素。
LinkedList
<span style="font-size:14px;">public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { }</span>
LinkedList不是线程安全的。LinkedList 采用的将对象存放在独立的空间中,而且在每个空间中还保存下一个链接的索引 但是缺点就是查找非常麻烦 要丛第一个索引开始. LinkedList既实现了List接口,还实现了Deque接口(双向队列)。所以linkedList的实现是一个双向列表的实现。LinkedList内部以链表的形式保存集合中的元素,随机访问集合性能较差,但是插入和删除操作性能出色. LinkedList不仅提供了List的功能, 还额外提供了双向队列, 栈的功能.
LinkedList添加元素结构图:
ArrayList和LinkedList的大致区别:
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
测试时间复杂度比较:
二分查找(binary search) 遍历一个有序的list列表
<span style="font-size:14px;">import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
public class TestList {
public static final int N = 50000;
public static List<Integer> values = null;
//初始化values
static {
Integer vals[] = new Integer[N];
Random r = new Random();
for (int i = 0, currval = 0; i < N; i++) {
vals[i] = new Integer(currval);
currval += r.nextInt(100) + 1;
}
values = Arrays.asList(vals);
}
static long timeList(List<Integer> lst) {
long start = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
// 进行二分查找
int index = Collections.binarySearch(lst, values.get(i));
if (index != i)
System.out.println("***错误***");
}
return System.currentTimeMillis() - start;
}
public static void main(String args[]) {
System.out.println("ArrayList消耗时间:" + timeList(new ArrayList(values)));
System.out.println("LinkedList消耗时间:" + timeList(new LinkedList(values)));
}
}</span>
输出
<span style="font-size:14px;">ArrayList消耗时间:14
LinkedList消耗时间:4759</span>
二分查找法使用的随机访问(random access)策略,而LinkedList是不支持快速的随机访问的。对一个LinkedList做随机访问所消耗的时间与这个list的大小是成比例的。而相应的,在ArrayList中进行随机访问所消耗的时间是固定的。 在某些情况下LinkedList的表现要优于ArrayList,有些算法在LinkedList中实现时效率更高。比方说,利用Collections.reverse方法对列表进行反转时,其性能就要好些.
空间复杂度比较
在LinkedList中有一个私有的内部类,定义如下:
- private static class Entry {
- Object element;
- Entry next;
- Entry previous;
- }
每个Entry对象reference列表中的一个元素,同时还有在LinkedList中它的上一个元素和下一个元素。一个有1000个元素的LinkedList对象将有1000个链接在一起的Entry对象,每个对象都对应于列表中的一个元素。这样的话,在一个LinkedList结构中将有一个很大的空间开销,因为它要存储这1000个Entity对象的相关信息。
ArrayList使用一个内置的数组来存储元素,这个数组的起始容量是10.当数组需要增长时,新的容量按如下公式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长50%。这就意味着,如果你有一个包含大量元素的ArrayList对象,那么最终将有很大的空间会被浪费掉,这个浪费是由ArrayList的工作方式本身造成的。如果没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会导致性能急剧下降。如果我们知道一个ArrayList将会有多少个元素,我们可以通过构造方法来指定容量。我们还可以通过trimToSize方法在ArrayList分配完毕之后去掉浪费掉的空间。
比较
ArrayList和LinkedList在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
3.LinkedList不支持高效的随机元素访问。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
如果要进行大量的随机访问,就使用ArrayList, 如果要经常从表中间插入或者是删除元素,则应该使用LinkedList.
关于List的建议如下:
1. 如果需要遍历list元素,对于arrayList,vector,则应该使用随机访问方法来遍历元素, 这样性能更好, 对于linkedlist集合,则应该采用迭代器来遍历集合元素.
2.如果需要经常执行插入, 删除操作来改变list集合的大小, 则应该使用linkedList集合,而不是ArrayList集合.使用ArrayList, Vector集合需要重新分配内部数组的大小,时间开销很大.
4. Set的实现类
set判断元素是否重复的标准是equals方法是否返回true。
hashSet特点:
很好的存取和查找性能
不是同步的
不保证元素的排列顺序
集合元素值可以是null.
hashSet 判断元素相等的标准是两个对象通过equals方法比较相等,并且两个方法的hashCode方法返回值也相等。
linkedHashSet是hashSet的子类,根据元素的hashcode的值决定元素的存储位置,同时使用链表维护元素的顺序。当遍历linkedHashSet集合里的元素时,hashSet会按照元素的添加顺序来访问集合里的元素。linkedHashSet需要维护元素的插入顺序,性能略低于hashSet。但在迭代访问Set的全部元素的时候有很好的性能,因为它以链表维护内部顺序。
TreeSet是SortedSet的唯一实现,可以确保集合元素处于排序状态,TreeSet是根据元素值进行排序的,它采用红黑树的数据机构对元素进行排序。hashSet是根据hash算法来决定元素的存储位置。TreeSet支持两种排序规则:自然排序和定制排序。默认是后者。
如果要把对象添加到TreeSet中时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。TreeSet中添加的应该是同一类的对象。对于TreeSet比较两个对象不相等的依据是:两个对象通过equals方法返回false,或通过compareTo返回0,即使两个对象是同一个对象,TreeSet也会把他当成两个对象处理。注意: 如果两个对象通过equals方法比较返回true,这两个对象通过compareTo方法比较应返回0.
hashSet的性能总是比TreeSet的性能好,特别是查询和添加元素等操作。因为TreeSet总是需要通过红黑树来维持元素的插入顺序。
linkedHashSet比hashSet略微慢点,因为维护链表所带来的额外开销造成的,但是它的遍历会很快。EnumSet是所有set里面性能最好的。
EnumSet中的所有值都必须是枚举类型的枚举值,以枚举值在枚举类内定义的顺序来决定集合的顺序,EnumSet的内部以位向量的形式存储。这种存储方式紧凑,高效,EnumSet对象占用内存很小,而且运行效率很好。适合批量操作。
不允许加入null元素,
<span style="font-size:14px;">import java.util.EnumSet;
public class EnumSetTest {
public static void main(String[] args) {
EnumSet<Session> enumSet = EnumSet.allOf(Session.class);
//创建空集合,集合元素类型是Session
System.out.println("enumSet: " + enumSet);
EnumSet<Session> enumSet1 = EnumSet.noneOf(Session.class);
enumSet1.add(Session.SPRING);
enumSet1.add(Session.SUMMER);
System.out.println("enumSet: " + enumSet1);
//以指定的session类型的元素创建集合
EnumSet<Session> enumSet2 = EnumSet.of(Session.SPRING, Session.WINTER);
System.out.println("enumSet: " + enumSet2);
//以获取区间的session类型的元素创建集合
EnumSet<Session> enumSet3 = EnumSet.range(Session.SPRING, Session.WINTER);
System.out.println("enumSet: "+ enumSet3);
//拷贝已有的EnumSet中的元素创建新的集合
EnumSet<Session> enumSet4 = EnumSet.copyOf(enumSet3);
System.out.println("enumSet: "+ enumSet4);
}
}
enum Session {
SPRING, SUMMER, FALL, WINTER;
}</span>
输出结果:
<span style="font-size:14px;">enumSet: [SPRING, SUMMER, FALL, WINTER]
enumSet: [SPRING, SUMMER]
enumSet: [SPRING, WINTER]
enumSet: [SPRING, SUMMER, FALL, WINTER]
enumSet: [SPRING, SUMMER, FALL, WINTER]</span>
重写equals方法,也应该重写equals方法,规则是:@如果两个对象通过equals方法返回true,这两个对象的hashcode也应该相同。@对象中用作equals比较标准的属性,都应该用来计算hashcode值。
hashSet,TreeSet, EnumSet都是线程不安全的。
<span style="font-size:14px;"> Collections.synchronizedSet(new TreeSet<String>());</span>
<span style="font-size:14px;">import java.util.HashSet;
public class TestHash {
public static void main(String[] args) {
HashSet<Person> hashSet = new HashSet<Person>();
hashSet.add(new Person("AA"));
hashSet.add(new Person("AB"));
hashSet.add(new Person("AC"));
hashSet.add(new Person("AC"));
System.out.println(hashSet);
}
}
class Person{
private String name;
public Person(String name) {
this.name =name;
}
public String getName() {
return name;
}
@Override
public int hashCode() {
return 20;
}
}</span>
输出:
<span style="font-size:14px;">[com.augmentum.test.Person@14, com.augmentum.test.Person@14, com.augmentum.test.Person@14, com.augmentum.test.Person@14]</span>
很明显它的散列值相同,在取元素的时候其实也是线性输出. 跟遍历list性能一样,为什么不采用开放地址法?
散列表
整个散列过程其实就是两步。
1. 在存储的时候,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
2. 当査找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。散列函数设计:除留余数法
除留余数法此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为: f( key ) = key mod p ( p ≤ m ) . mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。使用除留余数法的一个经验是,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
拉链法的优势与缺点
与开放定址法相比,拉链法有如下几个优点:
- 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
- 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
- 开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
- 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
拉链法的缺点:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
当构造一个新的HashSet空集合,其底层默认初始容量是 16,加载因子是 0.75,当超过加载因子的限制时,它会自动将数组的大小增大,由于原HashSet中的元素的下标是根据其容量16来生成的,为了保持其HashCode 的特性,所以不能简单的将原来数组中的元素复制增大的数组中,他必须把原数组中分别元素取出来,然后根据增大后的容量来生成对应的下标。这样导致了他在存储时的开销是很大的;
Set(interface) : 存入set的每个元素都必须是唯一的,因为Set不保存重复元素.加入Set的元素必须定义equals方法以确保对象的唯一性.Set与collection有完全一样的接口. Set接口不保证维护元素的次序.
HashSet: 为快速查找而设计的Set, 它对速度做了优化. 存入HashSet的元素必须定义HashCode.
TreeSet: 保持次序的Set, 底层为树结构. 使用它可以从Set中提取有序的序列. 元素必须实现Comparable接口.
LinkedHashSet: 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入次序), 元素也必须定义hashcode()方法.
5. Map的实现类
HashMap和hashTable的区别:
hashTable是线程安全的Map实现, hashMap是线程不安全的, hashMap比hashTable的性能高一点.hashTable保证同步的原因,也是在基本所有的方法都加上了sychronized的关键字.
HashTable不允许使用null作为key或者value, 但hashMap允许使用null作为key或者value.
hashMap和hashTable判断两个key相等的标准是: 两个key通过equals方法比较返回true, 两个可以的hashCode值也相等. 判断两个value相等的标准是: 只要两个对象通过equals方法比较返回true即可.
尽量不要使用可变对象作为hashMap和hashTable作为key, 如果确实需要,尽量在程序中不要修改作为key的可变对象.
HashMap默认的“加载因子”是0.75, 默认的容量大小是16。Hashtable默认的“加载因子”是0.75, 默认的容量大小是11。
当HashMap的 “实际容量” >= “阈值”时,(阈值 = 总的容量 * 加载因子),就将HashMap的容量翻倍。 当Hashtable的 “实际容量” >= “阈值”时,(阈值 = 总的容量 x 加载因子),就将变为“原始容量x2 + 1”。
HashMap添加元素时,是使用自定义的哈希算法。
Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
Properties
public class Properties extends Hashtable<Object,Object>
Properties类是hashTable的子类,Properties可以把Map对象和属性文件结合起来,可以把map中的key-value对写入属性文件,也可以把属性文件中的属性名=属性值加载到Map对象中. 它的key和value都是字符串类型. 他是hashTable的子类,所以不保证key-value的加载次序.
Properties可以保存成一个流,也可以加载从一个流去加载
public synchronized void load(Reader reader) throws IOException {
load0(new LineReader(reader));
}
Properties的键和值都是string类型.
<span style="font-size:14px;">import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Properties;
public class TestProperties {
public static void main(String[] args) throws FileNotFoundException, IOException {
Properties properties = new Properties();
properties.setProperty("hello", "world");
properties.setProperty("hello-1", "world-1");
properties.setProperty("hello-2", "world-2");
//将properties文件内容保存到test.properties文件中
properties.store(new FileOutputStream(new File("test.properties")), "comment line");
System.out.println(properties);
Properties propertiesNew = new Properties();
propertiesNew.setProperty("hello-3", "world-3");
propertiesNew.setProperty("hello-4", "world-4");
propertiesNew.setProperty("hello-5", "world-5");
//将test.properties文件中的属性名=属性值追加到propertiesNew中
propertiesNew.load(new FileInputStream("test.properties"));
System.out.println(propertiesNew);
// 将properties文件内容保存到test1.properties文件中. 默认文件的位置实在/home/user/workspace/Java_collection/下面,也就是在项目的那一级目录中
propertiesNew.store(new FileOutputStream(new File("test1.properties")), "comment line");
//遍历properties里面的键和值
Enumeration<?> propertyNames = propertiesNew.propertyNames();
while (propertyNames.hasMoreElements()) {
String propertyName = (String) propertyNames.nextElement();
String propertyValue = propertiesNew.getProperty(propertyName);
System.out.println(propertyValue);
}
}
}</span>
输出:
<span style="font-size:14px;">{hello-2=world-2, hello=world, hello-1=world-1}
{hello-5=world-5, hello-4=world-4, hello-3=world-3, hello=world, hello-2=world-2, hello-1=world-1}
world-5
world-4
world-3
world-2
world
world-1</span>
输出无序,再看看文件中的内容: test.properties
<span style="font-size:14px;">#comment line
#Wed Jun 24 10:47:42 CST 2015
hello-2=world-2
hello=world
hello-1=world-1</span>
test1.properties
<span style="font-size:14px;">#comment line
#Wed Jun 24 10:47:42 CST 2015
hello-5=world-5
hello-4=world-4
hello-3=world-3
hello=world
hello-2=world-2
hello-1=world-1</span>
Properties还可以把key-value以XML文件的形式保存,也可以从XML文件中加载属性名-属性值.当然XML的解析也可以使用DOM和SAXA去解析.
org.springframework.beans.factory.config.PropertyPlaceholderConfigurer在执行替换properties文件中的值时就使用的Properties类去根据key获取值,同时替换属性文件中的值.
TreeMap
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable {}
TreeMap也是基于红黑树对所有的key进行排序, 从而保证所有的key-value对处于有序状态, TreeMap判断两个key相等的依据是两个key通过equals方法比较返回true, 而通过compareTo比较返回0. TreeMap即认为这两个key是相等的.
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
红黑树
R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
注意:
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
红黑树示意图如下:
具体可以参考资料:
红黑树(一)之 原理和算法详细介绍 http://www.cnblogs.com/skywang12345/p/3245399.html
教你透彻了解红黑树 : http://blog.chinaunix.net/uid-26575352-id-3061918.html
WeakHashMap
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {}
HashMap的key保留对实际对象的强引用,这意味这只要该hashMap对象不被销毁, 该hashMap对象所有key所引用的对象不会被垃圾回收.HashMap也不会自动删除这些key所对应的key-value对象.但WeakHashMap的key只保留对实际对象的弱引用.这意味着如果WeakHashMap对象所有key所引用的对象没有被其他强引用变量所引用, 则这些key所引用的对象可能被垃圾回收, hashMap也可能自动删除这些key所对应的key-value对象.
<span style="font-size:14px;">import java.util.WeakHashMap;
public class WeakHashMapTest {
public static void main(String[] args) {
WeakHashMap<String, String> weakHashMap = new WeakHashMap<String, String>();
weakHashMap.put(new String("hello-1"), new String("world-1"));
weakHashMap.put(new String("hello-2"), new String("world-2"));
weakHashMap.put(new String("hello-3"), new String("world-3"));
weakHashMap.put("hello-4", "world-4");
weakHashMap.put("hello-5", new String("world-5"));
String str = new String("hello-6");
weakHashMap.put(str, new String("world-6"));
//测试weakHashMap的key是否允许是null
weakHashMap.put(null, new String("world-7"));
//测试weakHashMap对重复的key的处理方式
weakHashMap.put(null, new String("world-8"));
System.out.println(weakHashMap);
System.gc();
// Runtime.getRuntime().gc();
System.out.println(weakHashMap);
}
}</span>
输出结果:
<span style="font-size:14px;">{hello-1=world-1, hello-2=world-2, hello-3=world-3, null=world-8, hello-4=world-4, hello-5=world-5, hello-6=world-6}
{null=world-8, hello-4=world-4, hello-5=world-5, hello-6=world-6}
</span>
一个问题:
1.new String("hello") 其实创建了两个对象,那么为什么"hello-4"对象能保留下来,"hello-1"对象不能保存下来
添加前三个 key-value对时,这三个key都是匿名字符串对象,只有WeakHashMap保留了对他们的弱引用. 第四个key-value对的key是一个字符串直接量, 系统会缓存这个字符串直接量(即系统保留了对该字符串对象的强引用).所以垃圾回收的时候不会回收它.
通过源码知道, weakHashMap会判断put进来的key是否为null,如果是,那么key会被替换为NULL_KEY,NULL_KEY的定义是Object NULL_KEY = new Object(); 理论上WeakHashMap判断如果放进来的key相同的话 ,旧值会被替换成新值.在源码中可以看到NULL_KEY的定义是final类型的,也就是说final Object NULL_KEY = new Object(),这也就解释了为什么连续放进去两个key是null的元素,会变成一个.
放进去的元素按散列码hashCode进行排序,所以不一定是按照插入顺序排列的.
WeakHashMap和hashMap的比较:
1. HashMap实现了Cloneable和Serializable接口,而WeakHashMap没有。
HashMap实现Cloneable,意味着它能通过clone()克隆自己。
HashMap实现Serializable,意味着它支持序列化,能通过序列化去传输。
2. HashMap的“键”是“强引用(StrongReference)”,而WeakHashMap的键是“弱引用(WeakReference)”。
WeakReference的“弱键”能实现WeakReference对“键值对”的动态回收。当“弱键”不再被使用到时,GC会回收它,WeakReference也会将“弱键”对应的键值对删除。
这个“弱键”实现的动态回收“键值对”的原理是通过WeakReference(弱引用)和ReferenceQueue(引用队列)实现的。 首先,我们需要了解WeakHashMap中:
关于弱引用,引申如下:
Reference 是引用对象的基类, 它定义了对所有引用对象的通用操作.因为引用对象是通过与垃圾回收器的密切合作来实现的,所以不能直接为此类创建子类
<span style="font-size:14px;">public abstract class Reference<T></span>
有三个继承自抽象类Reference的类: SoftReference, WeakReference, 和PhantomReference(由强到弱排列). SoftReference用以实现内存敏感的高速缓存. WeakReference是为实现"规范映射"而设计的,它不妨碍垃圾回收器回收映射的键和值. PhantomReference用以调度回收前的清理工作. 如果一个对象是"可获得的", 垃圾回收器就不能释放它.因为它仍然为你的程序所用.
“弱键”的原理,大致上就是,通过WeakReference和ReferenceQueue实现的。实现步骤是:
(01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
(02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
(03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对。
这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。
这是一种节约存储空间的技术,向WeakHashMap添加键和值的操作,没有什么特殊要求,映射自动使用WeakReference包装它们.如果WeakHashMap的key在系统内持有强引用,那么WeakHashMap就退化为HashMap,所有的表项无法被垃圾收集器自动清理。
使用场景:
WeakHashMap可以作为简单缓存表的解决方案,当系统内存不够的时候,垃圾收集器会自动的清除没有在其他任何地方被引用的键值对。用它做缓存实际上也不靠谱.
对于String类型的字面量,它就是jvm在内存中的softReference. 所以不会被垃圾回收. final, static 修饰的变量属于哪种类型的引用?
IdentityHashMap
public class IdentityHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, java.io.Serializable, Cloneable {}
简单说IdentityHashMap与常用的HashMap的区别是:前者比较key时是“引用相等”而后者是“对象相等”,即对于k1和k2,当k1==k2时,IdentityHashMap认为两个key相等,而HashMap只有在k1.equals(k2) == true 时才会认为两个key相等。IdentityHashMap 允许使用null作为key和value. 不保证任何Key-value对的之间的顺序, 更不能保证他们的顺序随时间的推移不会发生变化.
IdentityHashMap有其特殊用途,比如序列化或者深度复制。或者记录对象代理。
举个例子,jvm中的所有对象都是独一无二的,哪怕两个对象是同一个class的对象,而且两个对象的数据完全相同,对于jvm来说,他们也是完全不同的,如果要用一个map来记录这样jvm中的对象,你就需要用IdentityHashMap,而不能使用其他Map实现。
import java.util.IdentityHashMap;
public class IdentityHashMapTest {
public static void main(String[] args) {
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<String, String>();
// "hello" 是字符串直接量,所以下面的两个字节序列完全相同,java会缓存字符串直接量.所以通过==返回true
identityHashMap.put("hello", "78");
identityHashMap.put("hello", "80");
// new String("java") 是新创建的字符串,以下两个都是新创建的,故identityHashMap认为他们不是同一个对象.内存地址不一样
identityHashMap.put(new String("java"), "81");
identityHashMap.put(new String("java"), "82");
System.out.println(identityHashMap);
}
}
输出结果不一样:
{java=82, hello=80, java=81}
LinkedHashMap
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {}
LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
}
LinkedHashMap的Entry对象在父类HashMap的Entry对象的基础上,增加了两个entry对象, 用这两个entry对象可以在迭代的时候按序输出. 这也说明LinkedHashMap比HashMap多维护了一个链表。
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K, V> {
private static final float hashTableLoadFactor = 0.75f; // 负载因子
private LinkedHashMap<K, V> map;
private int cacheSize;
public LRUCache(int cacheSize) {
this.cacheSize = cacheSize;
int hashTableCapacity = (int) Math.ceil(cacheSize / hashTableLoadFactor) + 1;
map = new LinkedHashMap<K, V>(hashTableCapacity, hashTableLoadFactor, true) {
// (an anonymous inner class)
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > LRUCache.this.cacheSize;
}
};
}
public synchronized V get(K key) {
return map.get(key);
}
public synchronized void put(K key, V value) {
map.put(key, value);
}
// 清除缓冲
public synchronized void clear() {
map.clear();
}
// 返回缓存中最多使用的Entry对象
public synchronized int usedEntries() {
return map.size();
}
// 返回一个包含所有缓存的Entry对象的拷贝
public synchronized Collection<Map.Entry<K, V>> getAll() {
return new ArrayList<Map.Entry<K, V>>(map.entrySet());
}
public static void main(String[] args) throws Exception {
LRUCache<String, String> c = new LRUCache<String, String>(3);
c.put("1", "one"); // 1
c.put("2", "two"); // 2 1
c.put("3", "three"); // 3 2 1
c.put("4", "four"); // 4 3 2
if (c.get("2") == null) //调用get方法, "2"这个key就被使用了一次,所以他肯定就移到前排
throw new Exception(); // 2 4 3
c.put("5", "five"); // 5 2 4
c.put("4", "second four"); // 4 5 2
// Verify cache content.
if (c.usedEntries() != 3)
throw new Exception();
if (!c.get("4").equals("second four"))
throw new Exception();
if (!c.get("5").equals("five"))
throw new Exception();
if (!c.get("2").equals("two"))
throw new Exception();
// List cache content.
for (Map.Entry<String, String> e : c.getAll())
System.out.println(e.getKey() + " : " + e.getValue());
}
}
输出:
4 : second four
5 : five
2 : two
每次访问,都会将被访问的元素放在前面,那么最少访问的元素自然就被放到了最后.
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {}
<span style="font-size:14px;">import java.util.EnumMap;
public class TestEnumMap {
public static void main(String[] args) {
EnumMap<Session, String> enumMap = new EnumMap<Session, String>(Session.class);
enumMap.put(Session.SPRING, "春暖花开");
enumMap.put(Session.SUMMER, "炎炎夏日");
enumMap.put(Session.WINTER, "漫天飞雪");
enumMap.put(Session.WINTER, "大雪纷飞");
enumMap.put(Session.FALL, "秋高气爽");
System.out.println(enumMap);
}
}</span>
输出结果:
<span style="font-size:14px;"><span style="font-size:14px;">{SPRING=春暖花开, SUMMER=炎炎夏日, FALL=秋高气爽, WINTER=大雪纷飞}</span></span>
故: WebHook实体配置中是否考虑使用EnumMap
Map 是“键值对”映射的抽象接口。
AbstractMap 实现了Map中的绝大部分函数接口。它减少了“Map的实现类”的重复编码。
SortedMap 有序的“键值对”映射接口。
NavigableMap 是继承于SortedMap的,支持导航函数的接口。
HashMap, Hashtable, TreeMap, WeakHashMap这4个类是“键值对”映射的实现类。它们各有区别:
HashMap Map基于散列表的实现.插入和查询"键值对"的开销是固定的.可以通过构造器设置容量和负载因子.以调整容器的性能.一般用于单线程程序中。
Hashtable 也是基于“拉链法”实现的散列表。它一般用于多线程程序中。
LinkedHashMap 迭代遍历它时,取得"键值对"的顺序是其插入次序.或者是最近最少使用(LRU Least Recently Used )的次序, 比hashMap慢一点,而在迭代访问时,反应更快.因为它使用链表维护内部次序.
TreeMap 基于红黑树的实现, 是有序的散列表,查看"键"或者"键值对"的时候,他们会被排序(次序由Comparable或Comparator决定). 特点在于: 所得到的结果都是经过排序的.TreeMap是唯一带有subMap()方法的Map,他可以返回一个子树. 它一般用于单线程中存储有序的映射。
WeakHashMap 也是基于“拉链法”实现的散列表,它一般也用于单线程程序中。相比HashMap,WeakHashMap中的键是“弱键”,当“弱键”被GC回收时,它对应的键值对也会被从WeakHashMap中删除;而HashMap中的键是强键。弱键映射, 允许释放映射所指向的对象;这是为解决某类特殊问题而设计的.如果映射之外没有引用指向某个"键", 则此键可以被垃圾收集器回收.
ConcurrentHasMap 一种线程安全的Map, 不涉及同步加锁.
IdentityHashMap 使用==代替equals()对"键"进行比较的散列映射.专为解决特殊问题而设计的.
EnumMap的性能最好,但他只能使用枚举类的枚举值作为key
除了IdentityHashMap,所有的Map实现的插操作都会随着Map尺寸的变大而明显的变慢. 因为比较元素的不同,IdentityHashMap具有完全不同的性能.
如果要使用自己的类作为HashMap的键,必须同时重载HashCode()和equals()方法. 正确的equals方法必须同时满足5个条件:自反性,对称性,传递性,一致性, 对任何不是null的x,x.equals(null) 一定返回false; 默认的Object.equals()只是比较对象的地址. Object的hashcode()方法生成散列码,默认是使用对象的地址计算散列码.
6. 队列
Queue接口
public interface Queue<E> extends Collection<E>
模拟队列这种数据结构。队列通常是指“FIFO”的容器,新元素插入到队列的尾部,访问poll操作会返回队列的头部元素,通常队列不支持随机访问。Queue的两个实现类: LinkedList和PriorityQueue 他们的差异在于排序行为,而不是性能问题.
队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径.队列在并发编程中很重要,因为它可以安全的将对象从一个任务传输给另一个任务,
PriorityQueue
优先级队列声明下一个弹出的元素是最重要的元素, 比如构建一个消息系统,某些消息比其他消息更重要,应该被优先处理,优先级队列的设计是为了提供这类行为的自动实现.
PriorityQueue保存队列元素的顺序不是按照加入队列的顺序,而是按队列元素的大小进行排序, PriorityQueue不允许插入null元素, 它还需要对队列元素进行排序,,排序的方法有2中:自然排序(集合元素必须实现comparable接口)和定制排序(传入一个comparator对象).
PriorityQueue可以确保当调用peek(), poll(), remove()方法时,获取的元素将是队列中优先级最高的元素.
<span style="font-size:14px;">import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
public class PriorityQueueTest {
static void printQueue(Queue queue){
while (queue.peek() != null) {
System.out.print(queue.remove() + ", ");
}
System.out.println();
}
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();
Random random = new Random(47);
for (int i = 0; i < 10; i++) {
priorityQueue.offer(random.nextInt(i + 10));
}
printQueue(priorityQueue);
System.out.println("**************************************");
List<Integer> inits = Arrays.asList(12,25,0,13,46,78,21,24,23,6,5,24,779);
priorityQueue = new PriorityQueue<Integer>(inits);
printQueue(priorityQueue);
System.out.println("**************************************");
priorityQueue = new PriorityQueue<Integer>(inits.size(), Collections.reverseOrder());
priorityQueue.addAll(inits);
printQueue(priorityQueue);
System.out.println("**************************************");
String str = "uiopty asdf rttrey klqwer acxv";
List<Character> characters = new ArrayList<Character>();
for (char character : str.toCharArray()) {
characters.add(character);
}
System.out.println(characters);
PriorityQueue<Character> priorityQueue2 = new PriorityQueue<Character>(characters.size(), Collections.reverseOrder());
priorityQueue2.addAll(characters);
printQueue(priorityQueue2);
System.out.println("**************************************");
priorityQueue2 = new PriorityQueue<Character>(characters);
printQueue(priorityQueue2);
}
}
</span>
输出:
<span style="font-size:14px;">0, 1, 1, 1, 1, 1, 3, 5, 8, 14,
**************************************
0, 5, 6, 12, 13, 21, 23, 24, 24, 25, 46, 78, 779,
**************************************
779, 78, 46, 25, 24, 24, 23, 21, 13, 12, 6, 5, 0,
**************************************
[u, i, o, p, t, y, , a, s, d, f, , r, t, t, r, e, y, , k, l, q, w, e, r, , , a, c, x, v]
y, y, x, w, v, u, t, t, t, s, r, r, r, q, p, o, l, k, i, f, e, e, d, c, a, a, , , , , ,
**************************************
, , , , , a, a, c, d, e, e, f, i, k, l, o, p, q, r, r, r, s, t, t, t, u, v, w, x, y, y, </span>
可以发现 PriorityQueue是允许重复的,最小的值拥有最高的优先级(如果是String,空格也可以算作值,并且比字母的优先级高)
7.并发环境下的容器类简述
CopyOnWriteArrayList
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
}
CopyOnWriteArrayList 是线程安全的,所有对变量的可变操作(add, set等),都是通过实现对底层数组的一个全新copy。虽然拷贝一份数组代价是昂贵的,但是遍历操作是很高校的,而且在多线程环境下不需要添加sychronized关键字就能实现同步。迭代数组时,在迭代器创建的时候,有一个快照记录数组当前的状态,这个数组的状态不会被改变在迭代的生命周期内。
CopyOnWriteArrayList在写的时候复制整个数组,这样操作其实比较耗时,所以CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况,此时有很好的性能。 读操作(get(),indexOf(),isEmpty(),contains())不加任何锁,而写操作(set(),add(),remove())通过Arrays.copyOf()操作拷贝当前底层数据结构(array),在其上面做完增删改等操作,再将新的数组置为底层数据结构,同时为了避免并发增删改, CopyOnWriteList在这些写操作上通过一个ReetranLock进行并发控制。synchronized在多线程情况下吞吐量下降很明显,而Lock的话会好很多,所以并发包中采用了Lock的机制.
CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
使用CopyOnWriteMap需要注意两件事情:
1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。@因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存). @CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法.adIfAbsent方法同样采用锁保护,并创建一个新的大小+1的Object数组。遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。
ConcurrentHashMap
hashMap结构图
ConcurrentHashMap的结构:
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment。再哈希,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。
get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读,我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作 ,Volatile是轻量级的synchronized,但他也只是确保读到的是当前这个变量的最新值. 所以使用这个关键字不能保证同步.
ConcurrentLinkedQueue
线程安全的队列,如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS(compare-and-swap 一个原子操作)的方式来实现.
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现.
参考资料: http://ifeve.com/concurrentlinkedqueue/ (ConcurrentLinkedQueue的实现原理分析)
解决共享资源竞争
1.使用sychronized关键字
如果正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么必须使用同步,并且,读写线程都必须用相同的监视器锁同步. 每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作.
2.使用显式的Lock对象
显式的互斥机制.Lock对象必须被显式的创建,锁定和释放.
如果使用sychronized关键字时,某些事物失败了,那么就会抛出一个异常.但是我们没有任何机会去做任何清理工作,以维护系统使其处于良好状态.有了显式的Lock对象, 就可以使用finally子句将系统维护到正常的状态.
大体上当使用sychronized关键字时,需要写的代码量少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题的时候,才使用显式的Lock对象.例如: 用sychronized关键字不能尝试的获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,必须使用concurrent类库.
Atomic 当对一个对象的临界更新被限制为只涉及单个变量时,只有使用Atomic这种方式才能够工作.
使用Lock通常会比使用sychronized高效很多,而且sychronized的开销看起来变化范围太大,而Lock相对比较一致.
免锁容器
Collections类提供的各种static的同步装饰的方法来同步不同类型的容器,这种开销仍然是基于sychronized的加锁机制.
免锁容器背后的通用策略是: 对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可.修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的.并且这个副本在修改过程中是不可视的.只有当修改完成时,被修改的数据结构才会自动地与主数据结构进行交换.之后读取者就可以看到这个修改了.
CopyOnWriteArrayList,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组再被修改的时候,读取操作可以安全的执行.当修改完成时,一个原子性的操作把新的数组换入,使得新的读取操作可以看到这个新的修改.
好处之一就是: 当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModifiedException.
CopyOnWriteArraySet 将使用CopyOnWriteArrayList来实现其免锁行为
ConcurrentHashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制 和修改.然而任何修改完成之前,读取者仍然不能看到他们. ConcurrentHashMap不会抛出ConcurrentModifiedException.
public interface Iterator<E> {
boolean hasNext(); //如果有下一个元素,返回true
E next(); //返回迭代器中的下一个元素
void remove();
}
Itr类是ArrayList的一个私有的内部类,它的实现如下:
private class Itr implements Iterator<E> {
int cursor; // 返回下一个元素的索引
int lastRet = -1; // 返回当前遍历的最后一个索引; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; //指针cursor向后移动一位
return (E) elementData[lastRet = i]; //返回当前的元素,同时lastRet指向当前元素的下标
}
public void remove() {
if (lastRet < 0) //lastRet为-1,然而并没有这个下标的元素,肯定被报错,这也就说明,首次遍历不能直接执行remove,肯定会报错,必须先执行next,给lastRet赋值.
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;//移出当前迭代的元素之后,cursor变成了lastRet的值,也就是指针前移一位.
lastRet = -1; //这个修改,直接的后果就是不允许重复删除同一个元素.
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
接合上面的代码看下面这个例子:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class TestListIterator {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 3; i++) {
list.add("test_" + i);
}
System.out.println(list);
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
listIterator.remove();
}
System.out.println(list);
}
}
结果:
[test_0, test_1, test_2]
Exception in thread "main" java.lang.IllegalStateException
at java.util.ArrayList$Itr.remove(ArrayList.java:844)
at com.augmentum.test.TestListIterator.main(TestListIterator.java:16)
默认lastRet的值为-1. 所以调用外部ArrayList的remove(-1)肯定会报错,同时多次执行remove()也会报错.
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0; //cursor默认从0开始
}
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i); //这也就意味着不能在指针cursor为默认0的时候直接调用该方法.elementData[-1]会报异常的
lastRet = cursor = i; //始终保证cursor 比lastRet 大.
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor; //首先拿到指针cursor的位置,也就是下一个元素的索引
AbstractList.this.add(i, e); //给下一个元素的位置添加一个元素,同时肯定涉及cursor之后元素都向后移动一位,因为是调用Arraylist的add方法
lastRet = -1; //此时这么操作,不允许执行删除操作
cursor = i + 1; //指针后移一位
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
针对上面所说,看下面的例子:
public class TestListIterator {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 3; i++) {
list.add("test_" + i);
}
System.out.println(list);
//listIterator.previous(); //直接执行previous()方法会报异常,因为默认cursor的值是0
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
listIterator.add("hello");
}
System.out.println(list);
}
}
[test_0, test_1, test_2]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:457)
at java.util.ArrayList$ListItr.add(ArrayList.java:914)
at com.augmentum.test.TestListIterator.main(TestListIterator.java:17)
从结果中明白: 每次执行都将cursor的指针后移一位,然而他都会在后移的位置上添加一个元素,调用外部的添加元素的方法,list会自动扩容.因为hasNext()肯定是有值的,所以会一直执行下去,直到报出异常.所以也就明白了下面这种add()方法指针的移动情况了:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class TestListIterator {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < 3; i++) {
list.add("test_" + i);
}
System.out.println(list);
//listIterator.previous();
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
listIterator.add("hello");
}
System.out.println(list);
}
}
输出:
[test_0, test_1, test_2]
test_0
test_1
test_2
[test_0, hello, test_1, hello, test_2, hello]
2. Set集合如果放进去重复元素,是替换掉原来的元素还是保持旧的元素 不变? (分析代码,同时进行进行测试确认).
测试示例:
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
User user = new User("zhangsan", "abc123_");
user.setAddress("上海浦东");
user.setEmail("www.quncrm.com");
Set<User> set = new HashSet<User>();
set.add(user);
for (Iterator<User> iterator = set.iterator(); iterator.hasNext();) {
User userNew = (User) iterator.next();
System.out.println(userNew);
}
user.setName("lisi");
user.setAddress("上海松江");
user.setEmail("www.sina.com");
set.add(user);
for (Iterator<User> iterator = set.iterator(); iterator.hasNext();) {
User userNew = (User) iterator.next();
System.out.println(userNew);
}
}
}
class User{
private String name;
private String password;
private String address;
private String email;
public User(String name, String password) {
super();
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public int hashCode() { //计算hashCode的值不需要考虑address和email
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((password == null) ? 0 : password.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (address == null) {
if (other.address != null)
return false;
} else if (!address.equals(other.address))
return false;
if (email == null) {
if (other.email != null)
return false;
} else if (!email.equals(other.email))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
return true;
}
@Override
public String toString() {
return "User [name=" + name + ", password=" + password + ", address=" + address + ", email=" + email + "]";
}
}
测试结果:
User [name=zhangsan, password=abc123_, address=上海浦东, email=www.quncrm.com]
User [name=lisi, password=abc123_, address=上海松江, email=www.sina.com]
User [name=lisi, password=abc123_, address=上海松江, email=www.sina.com]
源码中关于HashSet元素的add方法的解释如下:
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set //如果set已经包含了这个元素,这个方法的调用导致这个set不改变,并且返回false
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
set判断元素的标准是通过equals方法返回true,并且hashCode是相同的,上面的实例之所以输出这样的结果, 首先我只要求name和password相等,那么equals和hashcode值就相等, 对于set集合来说就是重复元素,他是不会替换的,但是我们对user元素的值的设置,修改的是user引用的对象的值.也就是修改的是堆内存中user引用对象的值, 输出hashSet发现结果变了,并不是一种替换. 可通过下面的例子输出结果知晓:
package com.augmentum.test;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;
public class HashSetTest {
public static void main(String[] args) {
HashMap<User, String> hashMap = new LinkedHashMap<User, String>();
Set<User> set = new LinkedHashSet<User>(16, 0.75f);
for (int i = 0; i < 3; i++) {
User user = new User("zhangsan" + i, "abc123_");
user.setAddress("上海浦东" + i);
user.setEmail("www.quncrm.com");
set.add(user);
hashMap.put(user, "test-" + i);
}
for (Iterator<User> iterator = set.iterator(); iterator.hasNext();) {
User userNew = (User) iterator.next();
System.out.println(userNew);
}
User userTest = null;
for (Iterator<User> iterator = set.iterator(); iterator.hasNext();) {
User userNew1 = (User) iterator.next();
userTest = userNew1;
break;
}
System.out.println("*******************");
System.out.println(userTest);
System.out.println("*******************");
userTest.setName("这是个Test");
userTest.setAddress("上海松江");
userTest.setEmail("www.163.com");
for (Iterator<User> iterator = set.iterator(); iterator.hasNext();) {
User userNew2 = (User) iterator.next();
System.out.println(userNew2);
}
}
}
class User{
private String name;
private String password;
private String address;
private String email;
public User(String name, String password) {
super();
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public int hashCode() { //计算hashCode的值不需要考虑address和email
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((password == null) ? 0 : password.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (address == null) {
if (other.address != null)
return false;
} else if (!address.equals(other.address))
return false;
if (email == null) {
if (other.email != null)
return false;
} else if (!email.equals(other.email))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
return true;
}
@Override
public String toString() {
return "User [name=" + name + ", password=" + password + ", address=" + address + ", email=" + email + "]";
}
}
输出:
User [name=zhangsan0, password=abc123_, address=上海浦东0, email=www.quncrm.com]
User [name=zhangsan1, password=abc123_, address=上海浦东1, email=www.quncrm.com]
User [name=zhangsan2, password=abc123_, address=上海浦东2, email=www.quncrm.com]
*******************
User [name=zhangsan0, password=abc123_, address=上海浦东0, email=www.quncrm.com]
*******************
User [name=这是个Test, password=abc123_, address=上海松江, email=www.163.com]
User [name=zhangsan1, password=abc123_, address=上海浦东1, email=www.quncrm.com]
User [name=zhangsan2, password=abc123_, address=上海浦东2, email=www.quncrm.com]
那么对上面的例子,第二次为什么会输出两个看似一模一样的User, 我猜想,是不是因为第一次放进去的User已经有一个hashcode值,然后我们修改User对象的引用.第二次放进去的User,它的hashCode值变了,也就是说Set会重新计算它在散列表的位置,故会输出两个User.这两个User在Hash表的不同位置.这个时候我猜想,如果现在把这个Set重新Hash一次,这个Set肯定就会只有一个元素了,我测试我的猜想, HashSet默认的表的长度是16,当达到它的负载因子0.75,即表的位置占据12个以上的时候,他需要ReHash一下.我做了测试,结果发现依然输出上面的结果.然后我去看了一下代码 的实现:
public boolean add(E e) {
return map.put(e, PRESENT)==null; //private static final Object PRESENT = new Object(); 这是PRESENT的定义
}
调用HashMap的put方法:
If the map previously contained a mapping for the key, the old value is replaced. 如果这个Map已经包含了这个映射的key,那么旧值就会被替换掉
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key); //计算key的hash值
int i = indexFor(hash, table.length); //根据Key的hash值取得散列表的下标
for (Entry<K,V> e = table[i]; e != null; e = e.next) { //迭代遍历Entry对象
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); //这个方法是针对LinkedHashMap,因为他要维护链表的顺序
return oldValue; //返回的是旧指,但实际上已经替换掉了旧值
}
}
modCount++;
addEntry(hash, key, value, i); //这个地方会添加这个Entry对象
return null;
}
添加Entry对象的实现
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
可以看出 ,它是将散列表对应下标出的bucketIndex处的Entry对象的Table表,扩容成2倍.然后重新计算添加进来的key的hash值,然后创建这个Entry对象,也就是说之前的元素不会重新在hash了,所以我们看到Set中有两个看似相同的User.
然后我修改了name的结果,HashSet发现这个不是重复元素,然后就把它添加进来,之所以会输出两个看似内容相同的User,实际上是因为第一次添加User进去的时候,它计算得到hashCode的值,修改User对象,不会导致它重新再hash一次. 故第二次添加进来的hashCode的值不一样,自然就存在两个元素,那么下面这个例子,我们让他执行一次再hash,看看结果.
默认hashSet的容量是16 这个在rehash的时候到底做了什么,需要有待确认
- 底层实际将将该元素作为key放入HashMap。
- * 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key
- * 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true),
- * 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变,
- * 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中,
- * 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。
3. HashSet默认的hashCode采用的散列函数的计算方式是什么?散列表的 下标是数组的索引下标还是散列函数计算的结果.
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
根据hashCode获取到它在散列表中的位置. 也就是散列表中的下标索引.
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
transient int hashSeed = 0;
计算hash的时候, 空键null总是被映射到hash 0,也就是说,下标为0的位置放的是Null键,故计算hash的时候需要判断 (0!= h).
reshash的时候不会将所有的元素在hash一次吗?
4. 怎么重写HashCode,是一个合法的方式?
覆盖hashcode方法需要遵守的约定:
无论何时,对同一个对象调用hashCode()都应该生成同样的值.涉及的HashCode方法尽量不要依赖于hashCode中易变的数据,因为此数据发生变化时,HashCode就会生成一个不同的散列码,相当于产生了一个不同的键.
不应该使Hashcode依赖于具有唯一性的对象消息.尤其是使用this的值.这只能产生很糟糕的Hashcode().应该使用对象内有意义的识别信息.
好的HashCode应该产生分布均匀的散列码.
1> 给int变量赋予某个非0值常量.
2> 为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:
域类型 计算
boolean c = (f ? 0 : 1)
byte, char, short或Int c = (int) f
long c = (int)(f ^ (f >>> 32))
float c = Float.floatToIntBits( f )
double long l = Double.doubleToLongBits( f );
c = (int))(l ^ (l >>> 32))
Object,其equals()调用这个域的equals() c = f.hashCode()
数组 对每个元素应用上述规则
3> 合并计算得到的散列码
result = 37*result + c;
4> 检查hashCode最后生成的结果, 确保相同 的对象有相同的散列码
参考 Effect java 中关于重写equals(), hashCode(), toString()等方法的说明
5. noSQL的锁和关系型数据库的锁的区别?数据库的并发读写的实现,以 mysql为例.
http://mapserver000-gmail-com.iteye.com/blog/1535390
6. 红黑树内部实现和二叉树的区别.
《STL源码剖析》中有关红黑树的介绍,书中介绍了红黑树的组织规则
http://www.cnblogs.com/renyuan/archive/2013/12/11/3469752.html
http://blog.csdn.net/yiweibin/article/details/5400202
红黑树和之前所讲的AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。自从红黑树出来后,AVL树就被放到了博物馆里,据说是红黑树有更好的效率,更高的统计性能。
红黑树和AVL树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。
8. 数据结构: 数组,栈,队列,平衡二叉树,哈希表等的java实现
《Java数据结构和算法(第二版)》 参考这本书,里面详细介绍了各种数据结构的实现