1、集合框架,list,map,set都有哪些具体的实现类,区别都是什么?
Java集合里使用接口来定义功能,是一套完善的继承体系。Iterator是所有集合的总接口,其他所有接口都继承于它,该接口定义了集合的遍历操作,Collection接口继承于Iterator,是集合的次级接口(Map独立存在),定义了集合的一些通用操作。
Java集合的类结构图如下所示:
List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;
Set:无序,不可重复;
Map:键值对,键唯一,值多个;
1.List,Set都是继承自Collection接口,Map则不是;
2.List特点:元素有放入顺序,元素可重复;
Set特点:元素无放入顺序,元素不可重复,重复元素会盖掉,(注意:元素虽然无放入顺序,但是元素在set中位置是由该元素的HashCode决定的,其位置其实是固定,加入Set 的Object必须定义equals()方法;
另外list支持for循环,也就是通过下标来遍历,也可以使用迭代器,但是set只能用迭代,因为他无序,无法用下标取得想要的值)。
3.Set和List对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
4.Map适合储存键值对的数据。
5.线程安全集合类与非线程安全集合类
LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;
HashMap是非线程安全的,HashTable是线程安全的;
StringBuilder是非线程安全的,StringBuffer是线程安的。
ArrayList与LinkedList的区别和适用场景
Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续,ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,其在开辟内存空间的时候不需要等一个连续的地址,对新增和删除操作add和remove,LinedList比较占优势。LikedList 适用于要头尾操作或插入指定位置的场景。
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对此访问的情况下选用ArrayList,当要对数据进行多次增加删除修改时采用LinkedList。
ArrayList和LinkedList怎么动态扩容的吗?
ArrayList:
ArrayList 初始化大小是 10 (如果你知道你的arrayList 会达到多少容量,可以在初始化的时候就指定,能节省扩容的性能开支) 扩容点规则是,新增的时候发现容量不够用了,就去扩容 扩容大小规则是,扩容后的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5+1 = 16)
LinkedList:
linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
ArrayList与Vector的区别和适用场景
ArrayList有三个构造方法:
public ArrayList(intinitialCapacity)// 构造一个具有指定初始容量的空列表。
public ArrayList()// 构造一个初始容量为10的空列表。
public ArrayList(Collection<? extends E> c)// 构造一个包含指定 collection 的元素的列表
复制代码
Vector有四个构造方法:
public Vector() // 使用指定的初始容量和等于零的容量增量构造一个空向量。
public Vector(int initialCapacity) // 构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。
public Vector(Collection<? extends E> c)// 构造一个包含指定 collection 中的元素的向量
public Vector(int initialCapacity, int capacityIncrement)// 使用指定的初始容量和容量增量构造一个空的向量
复制代码
ArrayList和Vector都是用数组实现的,主要有这么四个区别:
1)Vector是多线程安全的,线程安全就是说多线程访问代码,不会产生不确定的结果。而ArrayList不是,这可以从源码中看出,Vector类中的方法很多有synchronied进行修饰,这样就导致了Vector在效率上无法与ArrayLst相比;
2)两个都是采用的线性连续空间存储元素,但是当空间充足的时候,两个类的增加方式是不同。
3)Vector可以设置增长因子,而ArrayList不可以。
4)Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。
适用场景:
1.Vector是线程同步的,所以它也是线程安全的,而ArraList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
2.如果集合中的元素的数目大于目前集合数组的长度时,Vector增长率为目前数组长度的100%,而ArrayList增长率为目前数组长度的50%,在集合中使用数据量比较大的数据,用Vector有一定的优势。
HashSet与TreeSet的区别和适用场景
1.TreeSet 是二叉树(红黑树的树据结构)实现的,Treest中的数据是自动排好序的,不允许放入null值。
2.HashSet 是哈希表实现的,HashSet中的数据是无序的可以放入null,但只能放入一个null,两者中的值都不重复,就如数据库中唯一约束。
3.HashSet要求放入的对象必须实现HashCode()方法,放的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复但是同一个类的对象可以放入不同的实例。
适用场景分析:
HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
HashMap与TreeMap、HashTable的区别及适用场景
HashMap 非线程安全
HashMap:基于哈希表(散列表)实现。使用HashMap要求的键类明确定义了hashCode()和equals()[可以重写hasCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主分两种,一种是开放定址法,另一种是链表法。HashMap实现中采用的是链表法。
TreeMap:非线程安全基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
适用场景分析:
HashMap和HashTable:HashMap去掉了HashTable的contain方法,但是加上了containsValue()和containsKey()方法。HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。
HashMap:适用于Map中插入、删除和定位元素。
Treemap:适用于按自然顺序或自定义顺序遍历键(key)。
2、set集合从原理上如何保证不重复?
1)在往set中添加元素时,如果指定元素不存在,则添加成功。
2)具体来讲:当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后用这个(元素的hashcode)%(HashMap集合的大小)+1计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。
3、HashMap和HashTable的主要区别是什么?,两者底层实现的数据结构是什么?
HashMap和HashTable的区别:
二者都实现了Map 接口,是将唯一的键映射到特定的值上,主要区别在于:
1)HashMap 没有排序,允许一个null 键和多个null 值,而Hashtable 不允许;
2)HashMap 把Hashtable 的contains 方法去掉了,改成containsvalue 和containsKey, 因为contains 方法容易让人引起误解;
3)Hashtable 继承自Dictionary 类,HashMap 是Java1.2 引进的Map 接口的实现;
4)Hashtable 的方法是Synchronized 的,而HashMap 不是,在多个线程访问Hashtable 时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供额外的同步。Hashtable 和HashMap 采用的hash/rehash 算法大致一样,所以性能不会有很大的差异。
HashMap和HashTable的底层实现数据结构:
HashMap和Hashtable的底层实现都是数组 + 链表结构实现的(jdk8以前)
4、HashMap、ConcurrentHashMap、hash()相关原理解析?
hash()原理:
把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。
常见的Hash函数有以下几个:
直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
上面介绍过碰撞。衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞,常见的解决碰撞的方法有以下几种:
- 开放定址法:
-
- 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
- 链地址法
-
- 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
- 再哈希法
-
- 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
- 建立公共溢出区
-
- 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
HashMap 的数据结构
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。上面我们提到过,常用的哈希函数的冲突解决办法中有一种方法叫做链地址法,其实就是将数组和链表组合在一起,发挥了两者的优势,我们可以将其理解为链表的数组。
我们可以从上图看到,左边很明显是个数组,数组的每个成员是一个链表。该数据结构所容纳的所有元素均包含一个指针,用于元素间的链接。我们根据元素的自身特征把元素分配到不同的链表中去,反过来我们也正是通过这些特征找到正确的链表,再从链表中找出正确的元素。其中,根据元素特征计算元素数组下标的方法就是哈希算法,即本文的主角hash()函数(当然,还包括indexOf()函数)。
hash方法 我们拿JDK 1.7的HashMap为例,其中定义了一个final int hash(Object k) 方法,其主要被以下方法引用。
上面的方法主要都是增加和删除方法,这不难理解,当我们要对一个链表数组中的某个元素进行增删的时候,首先要知道他应该保存在这个链表数组中的哪个位置,即他在这个数组中的下标。而hash()方法的功能就是根据Key来定位其在HashMap中的位置。HashTable、ConcurrentHashMap同理。
hash方法的功能是根据Key来定位这个K-V在链表数组中的位置的。也就是hash方法的输入应该是个Object类型的Key,输出应该是个int类型的数组下标。
调用Object对象的hashCode()方法,该方法会返回一个整数,然后用这个数对HashMap或者HashTable的容量进行取模就行了。没错,其实基本原理就是这个,只不过,在具体实现上,由两个方法int hash(Object k)和int indexFor(int h, int length)来实现。但是考虑到效率等问题,HashMap的实现会稍微复杂一点。
hash :该方法主要是将Object转换成一个整型。
indexFor :该方法主要是将hash生成的整型转换成链表数组中的下标。
HashMap In Java 7
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
复制代码
indexFor方法其实主要是将hash生成的整型转换成链表数组中的下标。那么return h & (length-1);是什么意思呢?其实,他就是取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
X % 2^n = X & (2^n – 1)
2^n表示2的n次方,也就是说,一个数对2^n取模 == 一个数和(2^n – 1)做按位与运算 。
假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 = 7 ,即0111。
此时X & (2^3 – 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
复制代码
所以,return h & (length-1);只要保证length的长度是2^n的话,就可以实现取模运算了。而HashMap中的length也确实是2的倍数,初始值是16,之后每次扩充为原来的2倍。
HashMap的数据是存储在链表数组里面的。在对HashMap进行插入/删除等操作时,都需要根据K-V对的键值定位到他应该保存在数组的哪个下标中。而这个通过键值求取下标的操作就叫做哈希。HashMap的数组是有长度的,Java中规定这个长度只能是2的倍数,初始值为16。简单的做法是先求取出键值的hashcode,然后在将hashcode得到的int值对数组长度进行取模。为了考虑性能,Java总采用按位与操作实现取模操作。
两个不同的键值,在对数组长度进行按位与运算后得到的结果相同,这不就发生了冲突吗。来看下Java是如何做的。
其中的主要代码部分如下:
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
复制代码
这段代码是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
其实,使用位运算代替取模运算,除了性能之外,还有一个好处就是可以很好的解决负数的问题。因为我们知道,hashcode的结果是int类型,而int的取值范围是-2^31 ~ 2^31 – 1,即[ -2147483648, 2147483647];这里面是包含负数的,我们知道,对于一个负数取模还是有些麻烦的。如果使用二进制的位运算的话就可以很好的避免这个问题。首先,不管hashcode的值是正数还是负数。length-1这个值一定是个正数。那么,他的二进制的第一位一定是0(有符号数用最高位作为符号位,“0”代表“+”,“1”代表“-”),这样里两个数做按位与运算之后,第一位一定是个0,也就是,得到的结果一定是个正数。
HashTable In Java 7
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}
复制代码
相当于只是对k做了个简单的hash,取了一下其hashCode。而HashTable中也没有indexOf方法,取而代之的是这段代码:int index = (hash & 0x7FFFFFFF) % tab.length;。也就是说,HashMap和HashTable对于计算数组下标这件事,采用了两种方法。HashMap采用的是位运算,而HashTable采用的是直接取模。
为啥要把hash值和0x7FFFFFFF做一次按位与操作呢,主要是为了保证得到的index的第一位为0,也就是为了得到一个正数。因为有符号数第一位0代表正数,1代表负数。
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。
- HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。
- HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
- 当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。因为hash结果越分散效果越好。
- 在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。
- 但是,HashMap为了提高效率使用位运算代替哈希,这又引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。
ConcurrentHashMap In Java 7
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
int j = (hash >>> segmentShift) & segmentMask;
复制代码
上面这段关于ConcurrentHashMap的hash实现其实和HashMap如出一辙。都是通过位运算代替取模,然后再对hashcode进行扰动。区别在于,ConcurrentHashMap 使用了一种变种的Wang/Jenkins 哈希算法,其主要母的也是为了把高位和低位组合在一起,避免发生冲突。
HashMap In Java 8
在Java 8 之前,HashMap和其他基于map的类都是通过链地址法解决冲突,它们使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。为了解决在频繁冲突时hashmap性能降低的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。
关于Java 8中的hash函数,原理和Java 7中基本类似。Java 8中这一步做了优化,只做一次16位右位移异或混合,而不是四次,但原理是不变的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的。以上方法得到的int的hash值,然后再通过h & (table.length -1)来得到该对象在数据中保存的位置。
HashTable In Java 8
在Java 8的HashTable中,已经不在有hash方法了。但是哈希的操作还是在的,比如在put方法中就有如下实现:
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
复制代码
ConcurrentHashMap In Java 8
Java 8 里面的求hash的方法从hash改为了spread。实现方式如下:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
复制代码
Java 8的ConcurrentHashMap同样是通过Key的哈希值与数组长度取模确定该Key在数组中的索引。同样为了避免不太好的Key的hashCode设计,它通过如下方法计算得到Key的最终哈希值。不同的是,Java 8的ConcurrentHashMap作者认为引入红黑树后,即使哈希冲突比较严重,寻址效率也足够高,所以作者并未在哈希值的计算上做过多设计,只是将Key的hashCode值与其高16位作异或并保证最高位为0(从而保证最终结果为正整数)。
HashMap何时扩容:
当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即大于当前数组的长度乘以加载因子的值的时候,就要自动扩容。
扩容的算法是什么:
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
5、ArrayMap跟SparseArray在HashMap上面的改进?
HashMap要存储完这些数据将要不断的扩容,而且在此过程中也需要不断的做hash运算,这将对我们的内存空间造成很大消耗和浪费。
SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间,我们从源码中可以看到key和value分别是用数组表示:
private int[] mKeys;
private Object[] mValues;
复制代码
同时,SparseArray在存储和读取数据时候,使用的是二分查找法。也就是在put添加数据的时候,会使用二分查找法和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好,所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。 而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap快的多。
ArrayMap利用两个数组,mHashes用来保存每一个key的hash值,mArrray大小为mHashes的2倍,依次保存key和value。
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
复制代码
当插入时,根据key的hashcode()方法得到hash值,计算出在mArrays的index位置,然后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在index的相邻位置插入。
假设数据量都在千级以内的情况下:
1、如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用
2、如果key类型为其它的类型,则使用ArrayMap。
6.LinkedHashMap与HashMap的区别
LinkedHashMap类与HashMap非常相似。 但是,链接的哈希映射基于哈希表和链表,以增强哈希映射的功能。
除了默认大小为16的基础数组外,它还维护一个运行所有条目的双向链表。
为了维护元素的顺序,链接HashMap通过添加指向下一个和前一个条目的指针来修改HashMap的Map.Entry类:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
复制代码
请注意,Entry类只添加了两个指针; before和after使其能够将自己挂钩到链表。 除此之外,它使用HashMap的Entry类实现。
最后,请记住,此链接列表定义了迭代的顺序,默认情况下是元素的插入顺序(插入顺序)。
@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
map.put(1, null);
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
Set<Integer> keys = map.keySet();
Integer[] arr = keys.toArray(new Integer[0]);
for (int i = 0; i < arr.length; i++) {
assertEquals(new Integer(i + 1), arr[i]);
}
}
复制代码
我们可以保证此测试将始终通过,因为始终会保持插入顺序。 我们无法为HashMap提供相同的保证。
如果客户端需要在调用API之前以相同的方式对返回的映射进行排序,那么链接的散列映射是可行的方法。
如果将键重新插入映射,插入顺序不会受到影响。
LinkedHashMap提供了一个特殊的构造函数,使我们能够在自定义加载因子(LF)和初始容量之间指定一个称为访问顺序的不同排序机制/策略:
LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);
复制代码
第一个参数是初始容量,后跟负载因子,最后一个参数是排序模式。 因此,通过传入true,我们生成了访问顺序,而默认值是insert-order。
此机制确保元素的迭代顺序是上次访问元素的顺序,从最近访问到最近访问。
因此,使用某种映射构建最近最少使用(LRU)缓存非常简单实用。 成功的put或get操作会导致访问该条目:
@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
LinkedHashMap<Integer, String> map
= new LinkedHashMap<>(16, .75f, true);
map.put(1, null);
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
Set<Integer> keys = map.keySet();
assertEquals("[1, 2, 3, 4, 5]", keys.toString());
map.get(4);
assertEquals("[1, 2, 3, 5, 4]", keys.toString());
map.get(1);
assertEquals("[2, 3, 5, 4, 1]", keys.toString());
map.get(3);
assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}
复制代码
请注意,当我们在映射上执行访问操作时,如何转换键集中元素的顺序。
简单地说,映射上的任何访问操作都会产生一个顺序,使得如果要立即执行迭代,则访问的元素将显示在最后。
在上面的例子之后,很明显putAll操作为指定映射中的每个映射生成一个条目访问。
当然,对映射视图的迭代不会影响支持映射的迭代顺序; 只有映射上的显式访问操作才会影响排序。
LinkedHashMap还提供了一种机制,用于维护固定数量的映射,并在需要添加新映射时不断删除最旧的条目。
可以重写removeEldestEntry方法以强制执行此策略以自动删除过时映射。
public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 5;
public MyLinkedHashMap(
int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor, accessOrder);
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
}
复制代码
我们上面的覆盖将允许映射增长到最大大小为5个条目。 当大小超过该大小时,将插入每个新条目,代价是丢失映射中的最旧条目,即最后访问时间在所有其他条目之前的条目:
@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
LinkedHashMap<Integer, String> map
= new MyLinkedHashMap<>(16, .75f, true);
map.put(1, null);
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
Set<Integer> keys = map.keySet();
assertEquals("[1, 2, 3, 4, 5]", keys.toString());
map.put(6, null);
assertEquals("[2, 3, 4, 5, 6]", keys.toString());
map.put(7, null);
assertEquals("[3, 4, 5, 6, 7]", keys.toString());
map.put(8, null);
assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}
复制代码
就像HashMap一样,只要哈希函数的尺寸合适,LinkedHashMap就会在常量时间内执行添加,删除和包含的基本Map操作。 它还接受null键和null值。
但是,由于维护双向链表的额外开销,LinkedHashMap的这种常量性能可能比HashMap的常量时间稍差。
迭代LinkedHashMap的集合视图也需要线性时间O(n)类似于HashMap的线性时间。 另一方面,LinkedHashMap在迭代期间的线性时间性能优于HashMap的线性时间。
这是因为,对于LinkedHashMap,O(n)中的n只是映射中的条目数,而与容量无关。 然而,对于HashMap,n是容量和大小总和,O(大小+容量)。
负载因子和初始容量与HashMap精确定义。 但请注意,对于LinkedHashMap而言,为初始容量选择过高的值的惩罚不如HashMap严重,因为此类的迭代次数不受容量的影响。
就像HashMap一样,LinkedHashMap实现不同步。 因此,如果要从多个线程访问它,并且这些线程中至少有一个可能在结构上进行更改,那么它必须在外部进行同步。
Map m = Collections.synchronizedMap(new LinkedHashMap());
复制代码
与HashMap的不同之处在于需要进行结构修改。 在访问顺序链接的哈希映射中,仅调用get API会导致结构修改。 除此之外,还有像put和remove这样的操作。
7.Iterator
JDK提供的迭代接口进行Java集合的迭代。
Iterator iterator = list.iterator();
while(iterator.hasNext()){
iterator.next();
//do something
}
复制代码
迭代其实我们可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类,它是一个很典型的设计模式。Iterator模式是用于遍历集合类的标准访问方法。
它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。 在没有迭代器时我们都是这么进行处理的。如下:
int[] arrays = new int[10];
for(int i = 0 ; i < arrays.length ; i++){
int a = arrays[i];
//do something
}
复制代码
List<String> list = new ArrayList<String>();
for(int i = 0 ; i < list.size() ; i++){
String string = list.get(i);
//do something
}
复制代码
对于这两种方式,我们总是都事先知道集合的内部结构,访问代码和集合本身是紧密耦合的,无法将访问逻辑从集合类和客户端代码中分离出来。同时每一种集合对应一种遍历方法,客户端代码无法复用。
在实际应用中如何需要将上面将两个集合进行整合是相当麻烦的。所以为了解决以上问题,Iterator模式腾空出世,它总是用同一种逻辑来遍历集合。
使得客户端自身不需要来维护集合的内部结构,所有的内部状态都由Iterator来维护。客户端从不直接和集合类打交道,它总是控制Iterator,向它发送”向前”,”向后”,”取当前元素”的命令,就可以间接遍历整个集合。
java.util.Iterator
在Java中Iterator为一个接口,它只提供了迭代了基本规则,在JDK中他是这样定义的:对 collection 进行迭代的迭代器。迭代器取代了 Java Collections Framework 中的 Enumeration。迭代器与枚举有两点不同:
1、迭代器允许调用者利用定义良好的语义在迭代期间从迭代器所指向的 collection 移除元素。
2、方法名称得到了改进。
复制代码
public interface Iterator {
  boolean hasNext();
  Object next();
  void remove();
}
复制代码
Object next():返回迭代器刚越过的元素的引用,返回值是Object,需要强制转换成自己需要的类型
boolean hasNext():判断容器内是否还有可供访问的元素
void remove():删除迭代器刚越过的元素
复制代码
对于我们而言,我们只一般只需使用next()、hasNext()两个方法即可完成迭代。如下:
for(Iterator it = c.iterator(); it.hasNext(); ) {
  Object o = it.next();
   //do something
}
复制代码
前面阐述了Iterator有一个很大的优点,就是我们不必知道集合的内部结果,集合的内部结构、状态由Iterator来维持,通过统一的方法hasNext()、next()来判断、获取下一个元素,至于具体的内部实现我们就不用关心了。
各个集合的Iterator的实现
ArrayList的Iterator实现
在ArrayList内部首先是定义一个内部类Itr,该内部类实现Iterator接口,如下:
private class Itr implements Iterator<E> {
//do something
}
而ArrayList的iterator()方法实现:
public Iterator<E> iterator() {
return new Itr();
}
复制代码
在Itr内部定义了三个int型的变量:cursor、lastRet、expectedModCount。其中cursor表示下一个元素的索引位置,lastRet表示上一个元素的索引位置
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
复制代码
public boolean hasNext() {
return cursor != size;
}
复制代码
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 + 1
return (E) elementData[lastRet = i]; //lastRet + 1 且返回cursor处元素
}
复制代码
checkForComodification()主要用来判断集合的修改次数是否合法,即用来判断遍历过程中集合是否被修改过。
。modCount用于记录ArrayList集合的修改次数,初始化为0,,每当集合被修改一次(结构上面的修改,内部update不算),如add、remove等方法,modCount + 1,所以如果modCount不变,则表示集合内容没有被修改。
该机制主要是用于实现ArrayList集合的快速失败机制,在Java的集合中,较大一部分集合是存在快速失败机制的。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
对于remove()方法的是实现,它是调用ArrayList本身的remove()方法删除lastRet位置元素,然后修改modCount即可。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
复制代码
FastFail机制
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException异常,从而产生fail-fast机制。
public class FailFastTest {
private static List<Integer> list = new ArrayList<>();
/**
* @desc:线程one迭代list
* @Project:test
* @file:FailFastTest.java
* @Authro:chenssy
* @data:2014年7月26日
*/
private static class threadOne extends Thread{
public void run() {
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
int i = iterator.next();
System.out.println("ThreadOne 遍历:" + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @desc:当i == 3时,修改list
* @Project:test
* @file:FailFastTest.java
* @Authro:chenssy
* @data:2014年7月26日
*/
private static class threadTwo extends Thread{
public void run(){
int i = 0 ;
while(i < 6){
System.out.println("ThreadTwo run:" + i);
if(i == 3){
list.remove(i);
}
i++;
}
}
}
public static void main(String[] args) {
for(int i = 0 ; i < 10;i++){
list.add(i);
}
new threadOne().start();
new threadTwo().start();
}
}
复制代码
ThreadOne 遍历:0
ThreadTwo run:0
ThreadTwo run:1
ThreadTwo run:2
ThreadTwo run:3
ThreadTwo run:4
ThreadTwo run:5
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)
复制代码
通过上面的示例和讲解,我初步知道fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。
迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:
protected transient int modCount = 0; 那么他什么时候因为什么原因而发生改变呢?
ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。
解决方案:
方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。
CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。
1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?
复制代码
第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。
第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:
private static class COWIterator implements ListIterator {
/* 省略此处代码 /
public E next() {
if (!(hasNext()))
throw new NoSuchElementException();
return this.snapshot[(this.cursor++)];
}
/** 省略此处代码 */
}
复制代码
CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:
public boolean add(E paramE) {
ReentrantLock localReentrantLock = this.lock;
localReentrantLock.lock();
try {
Object[] arrayOfObject1 = getArray();
int i = arrayOfObject1.length;
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);
int j = 1;
return j;
} finally {
localReentrantLock.unlock();
}
}
final void setArray(Object[] paramArrayOfObject) {
this.array = paramArrayOfObject;
}
复制代码
CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:
Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);
arrayOfObject2[i] = paramE;
setArray(arrayOfObject2);
复制代码
就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。
所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。
Collections类
java中的Collections类是一个有用的实用程序类,用于处理java中的集合。 java.util.Collections类直接扩展Object类,并且只包含对Collections进行操作或返回它们的静态方法。
Collections类包含对集合和“包装器”进行操作的多态算法 - 它返回由指定集合支持的新集合。
Collections类包含3个字段:EMPTY_LIST,EMPTY_SET,EMPTY_MAP,它们可分别用于获取不可变的空List,Map和Set。
boolean addAll(Collection c, T... elements)
此方法一次将所有提供的元素添加到指定的集合。 元素可以以逗号分隔的列表的形式提供。
List fruits = new ArrayList();
Collections.addAll(fruits, "Apples", "Oranges", "Banana");
fruits.forEach(System.out::println);
复制代码
Apples
Oranges
Banana
复制代码
void sort(List list, Comparator c)
此方法根据自然顺序对提供的列表进行排序。 如果我们想要一些自定义排序,我们也可以传入Comparator。
Collections.sort(fruits);
System.out.println("Sorted according to natural ordering:");
fruits.forEach(System.out::println);
Collections.sort(fruits, Comparator.reverseOrder());
System.out.println("Sorted according to reverse of natural ordering:");
fruits.forEach(System.out::println);
复制代码
Sorted according to natural ordering:
Apples
Banana
Oranges
Sorted according to reverse of natural ordering:
Oranges
Banana
Apples
复制代码
Queue asLifoQueue(Deque deque)
此方法返回Deque视图作为后进先出(Lifo)队列。添加和删除方法分别映射到push,pop等。 当我们想要使用需要Queue的方法但我们需要Lifo排序时,这可能很有用。
Deque deque = new LinkedList();
deque.addFirst("Apples");
deque.add("Oranges");
deque.addLast("Bananas");
Queue queue = Collections.asLifoQueue(deque);
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
复制代码
Apples
Oranges
Banana
复制代码
int binarySearch(List<? extends Comparable> list, T key)
此方法使用指定列表中的二进制搜索来搜索关键字。 在调用此方法之前,列表应按自然顺序排序,否则,结果将是未定义的。 如果找到元素,它返回排序列表中元素的索引,在其他情况下,它返回( - (插入点)-1)。 其中,插入点定义为关键字将插入列表的点,即第一个元素的索引大于关键字,或者list.size(),如果列表中的所有元素都小于指定的 关键字。 请注意,如果找到关键字,则可以保证返回值> = 0。
Collections.sort(fruits);
System.out.println(Collections.binarySearch(fruits, "Banana"));
System.out.println(Collections.binarySearch(fruits, "Grapes"));
复制代码
1
-3
复制代码
我们还可以传入一个Comparator,它表示列表按指定比较器引发的顺序排序。
Collection checkedCollection(Collection c, Class type)
此方法提供所提供集合的动态类型安全视图。 注意集合是很有用的,任何错误输入的元素都不会插入其中。
List list = new ArrayList();
Collections.addAll(list, "one", "two", "three", "four");
Collection checkedList = Collections.checkedCollection(list, String.class);
System.out.println("Checked list content: " + checkedList);
//we can add any type of element to list
list.add(10);
//we cannot add any type of elements to chkList, doing so
//throws ClassCastException
checkedList.add(10);
复制代码
同样,我们有特定集合的检查方法,如List,Map,Set等。
void copy(List dest, List src)
此方法将所有元素从源列表复制到目标列表。 执行此操作后,目标列表中每个复制元素的索引将与源列表中的索引相同。 在前面的方法示例中,我们创建了一个包含5个元素的列表。 让我们将“fruits”列表复制到此列表中,看看会发生什么:
Collections.copy(list, fruits);
list.forEach(System.out::println);
复制代码
Oranges
Banana
Apples
four
10
复制代码
目标列表必须至少与源列表一样长。 在我们的示例中,它比源列表长,因此在这种情况下,目标列表中的其余元素不受影响(此处为“four”和“10”)。
boolean disjoint(Collection c1, Collection c2)
如果两个指定的集合没有共同的元素,则此方法返回true。 在前面的例子中,我们将水果复制到列表中,所以现在它们不是不相交的。 所以,当我们执行时:
System.out.println(Collections.disjoint(list, fruits));
复制代码
false
复制代码
让我们为“vegetables”创建另一个列表,并检查它是否与“fruits”不相交。
List vegetables = new ArrayList();
Collections.addAll(vegetables, "Potato", "Cabbage");
System.out.println(Collections.disjoint(vegetables, fruits));
复制代码
true
复制代码
如果我们在两个参数中传递相同的集合,我们会得到false,除非它们是空的:
System.out.println(Collections.disjoint(vegetables, vegetables));
System.out.println(Collections.disjoint(new ArrayList(), new ArrayList()));
复制代码
false
true
复制代码
void fill(List list, T obj)
此方法用指定的元素替换指定列表的所有元素。 如果我们填写“list”列表,它的所有五个元素都将被替换:
Collections.fill(list, "filled with dummy data");
list.forEach(System.out::println);
复制代码
filled with dummy data
filled with dummy data
filled with dummy data
filled with dummy data
filled with dummy data
复制代码
int frequency(Collection c, Object o)
此方法返回指定集合中等于指定对象的元素数。
System.out.println(Collections.frequency(list, "filled with dummy data"));
复制代码
5
复制代码
int indexOfSubList(List source, List target)
此方法返回指定源列表中指定目标列表第一次出现的起始位置,如果不存在,则返回-1。
List fruitsSubList1 = new ArrayList();
Collections.addAll(fruitsSubList1, "Oranges", "Banana");
System.out.println(Collections.indexOfSubList(fruits, fruitsSubList1));
复制代码
1
复制代码
因为该子列表从fruits的索引1开始。 现在,如果我们尝试对另一个不存在的子列表执行相同操作:
List fruitsSubList2 = new ArrayList();
Collections.addAll(fruitsSubList2, "Kiwi", "Pinapple");
System.out.println(Collections.indexOfSubList(fruits, fruitsSubList2));
复制代码
-1
复制代码
还要注意,如果子列表的大小>列表的大小,我们得到-1。 我们有另一个方法int lastIndexOfSubList(List source,List target),它只返回指定子列表的最后一个索引,否则产生与此一个相同的输出。
static ArrayList list(Enumeration e)
和Enumeration enumeration(Collection c)
这些方法分别从枚举返回一个列表和从列表返回枚举,以便在返回枚举的旧API和需要集合的新API之间提供互操作性。
T max(Collection coll, Comparator comp)
此方法根据元素的自然顺序返回集合中的最大元素。
System.out.println(Collections.max(fruits));
复制代码
Oranges
复制代码
如果我们想要自定义排序,我们也可以在此方法中传递Comparator。 类似地,也可以使用min方法,它也可以与Comparator一起使用或不与Comparator一起使用。
Collection<T> synchronizedCollection(Collection<T> c)
此方法返回由提供的集合支持的同步(线程安全)集合。 在需要时从任何集合对象获取同步集合很方便。 API还为我们提供了synchronizedList方法,该方法返回参数中由提供的列表支持的线程安全列表。
Collection<String> synchronizedCollection =
Collections.synchronizedCollection(fruits);
List<String> synchronizedList = Collections.synchronizedList(fruits);
复制代码
此外,还有synchronizedMap,synchronizedSet,synchronizedSortedSet以及可用于执行类似工作的synchronizedSortedMap方法。
这些是常用的方法,除此之外我们还有其他方法,如newSetFromMap,replaceAll,swap,reverse等。
请注意,如果提供给它们的集合或类对象为null,则此类的所有方法都会抛出NullPointerException。