集合容器总结
目录
1.2.6 关于hashcode和equals的一些问题,在面试中会问道:
SynchronizedMap和ConcurrentHashMap 区别
4.5.4 Arraylist 与 LinkedList 区别?
补充内容 ArrayDeque :Java容器类详解(十四)ArrayDeque详解
补充内容 LinkedBlockingDeque :Java并发编程之LinkedBlockingDeque阻塞队列详解
补充内容 Comparable 和 Comparator 的区别:
1.数据结构
1.1 :线性表
[1.1.1]:顺序存储结构(也叫顺序表)
一个线性表是n个具有相同特性的数据元素的有限序列。数据元素是一个抽象的符号,其具体含义在不同的情况下一般不同。
[1.1.2]:链表
链表里面节点的地址不是连续的,是通过指针连起来的。
1.2:哈希表
解释一:
哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
解释二:
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:
1.2.1 Hash 表优缺点
优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。
哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。
如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。
缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
1.2.2 哈希表的原理:
1,对对象元素中的关键字(对象中的特有数据),进行哈希算法的运算,并得出一个具体的算法值,这个值 称为哈希值。
2,哈希值就是这个元素的位置。
3,如果哈希值出现冲突,再次判断这个关键字对应的对象是否相同。如果对象相同,就不存储,因为元素重复。如果对象不同,就存储,在原来对象的哈希值基础 +1顺延。
4,存储哈希值的结构,我们称为哈希表。
5,既然哈希表是根据哈希值存储的,为了提高效率,最好保证对象的关键字是唯一的。
这样可以尽量少的判断关键字对应的对象是否相同,提高了哈希表的操作效率。
1.2.3 哈希表存储过程:
1.调用对象的哈希值(通过一个函数f()得到哈希值):存储位置 = f(关键字)
2.集合在容器内搜索有没有重复的哈希值,如果没有,存入新元素,记录哈希值
3.再次存储,重复上边的过程
4.如果有重复的哈希值,调用后来者的equals方法,参数为前来者,结果得到true,集合判断为重复元素,不存入
1.2.4 哈希冲突
然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?
也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。
1.2.5 哈希冲突的解决方案有多种:
- 开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)
- 再散列函数法
- 链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式
1.2.6 关于hashcode和equals的一些问题,在面试中会问道:
1.两个对象哈希值相同,那么equals方法一定返回true吗?
不一定:取决于如何重写equals,如果重写固定了它返回false,结果就一定是false
2.equals方法返回true,那么哈希值一定相同吗?
一定:如果类中定义一个静态变量(static int a = 1),然后重写hashcode返回a+1,那么每一个对象的哈希值都不一样,不过java中规定:对象相等,必须具有相同的哈希码值,所以这里是一定的,这样才有意义。
1.3 数组
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
1.4 数组,链表,哈希表区别
数组
优点:(1)随机访问效率高(根据下标查询),(2)搜索效率较高(可使用折半方法)。
缺点:(1)内存连续且固定,存储效率低。(2)插入和删除效率低(可能会进行数组拷贝或扩容)。
链表
优点:(1)不要求连续内存,内存利用率高,(2)插入和删除效率高(只需要改变指针指向)。
缺点:(1)不支持随机访问,(2)搜索效率低(需要遍历)。
Hash表
优点:(1)搜索效率高,(2)插入和删除效率较高,
缺点:(1)内存利用率低(基于数组),(2)存在散列冲突。
2.集合类种重要概念词解释
2.1 泛型
java中很重要的概念, 集合里面应用很多。集合的元素,可以是任意类型对象的引用,如果把某个对象放入集合,则会忽略它的类型,就会把它当做Object类型处理。
泛型则是规定了某个集合只可以存放特定类型的对象的引用,会在编译期间进行类型检查,可以直接指定类型来获取集合元素
在泛型集合中有能够存入泛型类型的对象实例还可以存入泛型的子类型的对象实例。
注意:
- 1 泛型集合中的限定类型,不能使用基本数据类型
- 2 可以通过使用包装类限定允许存放基本数据类型
泛型的好处
- 1 提高了安全性(将运行期的错误转换到编译期)
- 2 省去强转的麻烦
2.2.哈希值
- 就是一个十进制的整数,有操作系统随机给出
- 可以使用Object类中的方法hashCode获取哈希值
- Object中源码: int hashCode()返回该对象的哈希码值;
源码:
native:指调用了本地操作系统的方法实现
public native int hashCode();
2.3 平衡二叉树(AVL树)
解释:其特点是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。
注意:关键点是左子树和右子树的深度的绝对值不超过1
那什么是左子树深度和右子树深度呢?
如上图中: 如果插入6元素, 则8的左子树深度就为2, 右子树深度就为0,绝对值就为2, 就不是一个平很二叉树
[2.3.1].二叉排序树(BST)
二叉搜索树(二叉排序树,二叉查找树,二叉检索树)是一个概念,其特点是:
- 1若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 2若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 3左、右子树也分别为二叉排序树
平衡二叉树也是一种BST,只不过平衡性更高,左右子树的高度差绝对值不超过1
[2.3.2].旋转
假设一颗 AVL 树的某个节点为 r,有四种操作会使 r 的左右子树高度差大于 1,从而破坏了原有 AVL 树的平衡性。使用旋转达到平衡性。
2.4.红黑树
参考:
红黑树的特点:
- 符合bst的特点,若左子树不为空则小于根节点,若右子树不为空则大于根节点,且左右子树均为二叉查找树。
- 根节点为黑色,节点规定为红色或者黑色
- 红色的节点一定有子节点,且子节点为黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 每个叶子节点都是黑色的空节点(NIL节点)
- 从任意一个节点出发到其每个叶子节点的所有路径均包含相同的黑色节点数
叶子节点:一棵树当中没有子结点(即度为0)的结点称为叶子节点,简称“叶子”。 叶子是指出度为0的结点,又称为终端结点。
下图中这棵树,就是一颗典型的红黑树:
- 红黑树(Red Black Tree) 是一种自平衡二叉查找树。
- 可以在O(logn)时间内做查找,插入和删除
- 红黑树从根节点到叶子节点的最长路径不会超过最短路径的两倍,高度不会超过2logN(红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。)。
- 红黑树的JAVA应用:TreeMap,TreeSet,java8中的HashMap中
- 红黑树在插入和删除节点时会破坏其自平衡的特性,为了维持红黑树的特性,于是红黑树通过变色和旋转来实现调整。
变色:为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色。
旋转:分为左旋转和右旋转
左旋转:
逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。说起来很怪异,大家看下图:
图中,身为右孩子的Y取代了X的位置,而X变成了自己的左孩子。此为左旋转。
自己的理解:原来的父节点变成左子节点。
右旋转:
顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。大家看下图:
图中,身为左孩子的Y取代了X的位置,而X变成了自己的右孩子。此为右旋转。
自己的理解:原来的父节点变成右子节点。
2.5 迭代器
2.5.1 迭代器模式
把访问逻辑从不同类型的集合类中抽取出来,从而避免向外部暴露集合的内部结构。在java中它是一个对象,其目的是遍历并选中其中的每个元素,而使用者(客户端)无需知道里面的具体细节。
任何集合都有迭代器。任何集合类,都必须能以某种方式存取元素,否则这个集合容器就没有任何意义。
迭代器,也是一种模式(也叫迭代器模式)。迭代器要足够的“轻量”——创建迭代器的代价小。
迭代器与枚举有两点不同:
1. 迭代器在迭代期间可以从集合中移除元素。
2. 方法名得到了改进,Enumeration的方法名称都比较长。
迭代器的好处:屏蔽了集合之间的不同,可以使用相同的方式取出
2.5.2 Iterator
Collection集合元素的通用获取方式:在取出元素之前先判断集合中有没有元素。如果有,就把这个元素取出来,继续再判断,如果还有就再取出来,一直把集合中的所有元素全部取出来,这种取出元素的方式专业术语称为迭代。
java.util.Iterator:在Java中Iterator为一个接口,它只提供了迭代的基本规则。在JDK中它是这样定义的:对Collection进行迭代的迭代器。迭代器取代了Java Collection Framework中的Enumeration。
Collection中有一个抽象方法iterator方法,所有的Collection子类都实现了这个方法;返回一个Iterator对象
定义:
package java.util;
public interface Iterator<E> {
boolean hasNext();//判断是否存在下一个对象元素
E next();//获取下一个元素
void remove();//移除元素
}
异常:
在使用Iterator的时候禁止对所遍历的容器进行改变其大小结构的操作。例如: 在使用Iterator进行迭代时,如果对集合进行了add、remove操作就会出现ConcurrentModificationException异常。
在进行集合元素取出的时候,如果集合中没有元素了,还继续使用next()方法的话,将发生NoSuchElementException没有集合元素的错误
修改并发异常:在迭代集合中元素的过程中,集合的长度发生改变(进行了元素增加或者元素删除的操作), 增强for的底层原理也是迭代器,所以也需要避免这种操作;
解决以上异常的方法:使用ListIterator
2.5.3 Iterable(1.5)
Java中还提供了一个Iterable接口,Iterable接口实现后的功能是‘返回’一个迭代器,我们常用的实现了该接口的子接口有:Collection<E>、List<E>、Set<E>等。该接口的iterator()方法返回一个标准的Iterator实现。实现Iterable接口允许对象成为Foreach语句的目标。就可以通过foreach语句来遍历你的底层序列。
Iterable接口包含一个能产生Iterator对象的方法,并且Iterable被foreach用来在序列中移动。因此如果创建了实现Iterable接口的类,都可以将它用于foreach中。
好处:代码减少,方便遍历
弊端:没有索引,不能操作容器里的元素
定义:
Package java.lang;
import java.util.Iterator;
public interface Iterable<T> {
Iterator<T> iterator();
}
Iterable是Java 1.5的新特性, 主要是为了支持forEach语法, 使用容器的时候, 如果不关心容器的类型, 那么就需要使用迭代器来编写代码. 使代码能够重用.
使用方法很简单:
List<String> strs = Arrays.asList("a", "b", "c");
for (String str: strs) {
out.println(str);
}
增强for循环底层也是使用了迭代器获取的,只不过获取迭代器由jvm完成,不需要我们获取迭代器而已,所以在使用增强for循环变量元素的过程中不准使用集合对象对集合的元素个数进行修改;
2.5.4 forEach()(1.8)
使用接收lambda表达式的forEach方法进行快速遍历.
List<String> strs = Arrays.asList("a", "b", "c");
strs.forEach(out::println);// 使用Java 1.8的lambda表达式
2.5.5 Spliterator迭代器
参考:https://www.cnblogs.com/qingshanli/p/11756940.html
Spliterator是1.8新增的迭代器,属于并行迭代器,可以将迭代任务分割交由多个线程来进行。Spliterator可以理解为Iterator的Split版本(但用途要丰富很多)。使用Iterator的时候,我们可以顺序地遍历容器中的元素,使用Spliterator的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。使用 Spliterator 每次可以处理某个元素集合中的一个元素 — 不是从 Spliterator 中获取元素,而是使用 tryAdvance() 或 forEachRemaining() 方法对元素应用操作。但Spliterator 还可以用于估计其中保存的元素数量,而且还可以像细胞分裂一样变为一分为二。这些新增加的能力让流并行处理代码可以很方便地将工作分布到多个可用线程上完成
2.5.6 ListIterator
ListIterator是一个更强大的Iterator子类型,能用于各种List类访问,前面说过Iterator支持单向取数据,ListIterator可以双向移动,所以能指出迭代器当前位置的前一个和后一个索引,可以用set方法替换它访问过的最后一个元素。我们可以通过调用listIterator方法产生一个指向List开始处的ListIterator,并且还可以用过重载方法listIterator(n)来创建一个指定列表索引为n的元素的ListIterator。
ListIterator可以往前遍历,添加元素,设置元素(修改)
Iterator和ListIterator的区别:
两者都有next()和hasNext(),可以实现向后遍历,但是ListIterator有previous()和hasPrevious()方法,即可以实现向前遍历
ListIterator可以定位当前位置,nextIndex()和previous()可以实现
ListIterator有add()方法,可以向list集合中添加数据
都可以实现删除操作,但是ListIterator可以实现对对象的修改,set()可以实现,Iterator仅能遍历,不能修改
2.5.7 Fail-Fast
fail-fast:fail-fast快速失败策略,优先考虑出现异常的情况,当异常产生时,直接抛出异常,程序终止运行
什么时候回出现fail-fast呢?
之前在集合中提到过,利用迭代器进行遍历集合对象输出的时候尽量不要修改元素(增删改),如果修改了元素,那么就会出现ConcurrentModificationException的异常
查看ArrayList源码,在执行next()方法的时候,会执行checkForComodification()方法
@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;
return (E) elementData[lastRet = i];
}
//...............省略.............
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
上述代码中,当 modCount != exceptionModCount 时,就会抛出ConcurrentModCountException的异常,modCount存在于AbstractList记录Lis自己和被修改(add,remove方法)的次数,exceptionModCount存在于内部迭代器实现,存储当前集合的修改次数
原理:
1.迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount的变量
2. 集合在被遍历期间如果内容发生变化,就会改变modCount的值
3.每当迭代器使用hasNext/next()方法遍历下一个元素之前,都会检测modCount变量和exceptionmodCount的值是否相等
4.如果相等就返回遍历,否则抛出异常,终止遍历
举例:
//会抛出ConcurrentModificationException异常
for(Person person : Persons){
if(person.getId()==2)
student.remove(person);
}
注意:
这里异常的抛出条件时检测到 modCount = exceptionModCount这个条件,如果集合发生变化时修改modCount的值,刚好又设置了exceptionmodCount的值,则异常不会抛出,(比如删除看数据,再添加一条数据)。
举例:
//不会抛出ConcurrentModificationException异常
for(Person person : Persons){
if(person.getId()==2){
Persons.remove(person);
Persons.add(new Person());
}
}
所以不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议检测并发修改的bug
使用场景:
在java.util包下下的集合类都是快速失败机制的,不能在多线程下发生并发修改(迭代过程中被修改)
2.5.8 Fail-Safe
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先copy原有的集合内容,在拷贝的集合上进行遍历
原理:
由于迭代时是对元集合拷贝的值进行遍历,所以在遍历过程中对原集合所做的修改并不能被迭代器所检测到,所以不会发生ConcurrentModificationException
缺点:
由于拷贝内容的优点是为了避免ConcurrentModificationException,但同样的,迭代器并不能访问到修改后的内容(简单来说,就是迭代器遍历的是从开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生改变迭代器是不知道的)
使用场景:
java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改
java.util.concurrent包下线程安全的集合类(CopyOnWriteArrayList,ConcurrentHashMap)
3.集合类概念
3.1 集合类的作用
- 集合类也叫做容器类,和数组一样,用于存储数据,但数组类型单一,并且长度固定,限制性很大,而集合类可以动态增加长度。
- 集合存储的元素都是对象(引用地址),所以集合可以存储不同的数据类型,但如果是需要比较元素来排序的集合,则需要类型一致。
- 集合中提供了统一的增删改查方法,使用方便。
- 支持泛型,避免数据不一致和转换异常,还对常用的数据结构进行了封装。
- 所有的集合类的都在java.util包下。
3.2 集合框架体系的组成
集合框架体系是由Collection、Map(映射关系)和Iterator(迭代器)组成,各部分的作用如下所示。
Collection体系中有三种集合:Set、List、Queue
3.2.1 Collection
List (列表)
元素是有序的且可重复。
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
Set (集)
元素是无序的且不可重复。(treeSet和LinkedHashSet 是有序的)
- HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)
Queue(队列)
封装了数据结构中的队列。
- AbstractDueue: 此类提供某些 Queue 方法的骨干实现,为其它类集合的实现提供方便。
- PriorityQueue:一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。
- ArrayDeque:双端队列的一个数组实现, 数组双端队列没有容量限制;它们可根据需要增加以支持使用。
- LinkedList: LinkedList 类实现了接口 Queue 和 Deque, 具备了操作队列的基本方法。
- BlockingQueue: java.util.concurrent 包里的 BlockingQueue是一个接口,,定义了queue的并发接口
3.2.2 Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
- Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
3.2.3 如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。
3.2.4 Collection的由来
由于数组中存放对象,对对象操作起来不方便。java中有一类容器,专门用来存储对象 集合可以存储多个元素,但我们对多个元素也有不同的需求,针对不同的需求:java就提供了很多集合类,多个集合类的数据结构不同。但是,结构不重要,重要的是能够存储东西,能够判断,获取.把集合共性的内容不断往上提取,最终形成集合的继承体系---->Collection.并且所有的Collection实现类都重写了toString()方法.
3.2.5 Collection的作用:
如果一个类的内部有很多相同类型的属性,并且他们的作用与意义是一样的,比如说学生能选课学生类就有很多课程类型的属性,或者工厂类有很多机器类型的属性,我们用一个类似于容器的集合去盛装他们,这样在类的内部就变的井然有序,这就是:
- 在类的内部,对数据进行组织的作用。
- 有的集合接口在其内部提供了映射关系的结构,可以通过关键字(key)去快速查找对应的唯一对象,而这个关键可以是任意类型的。比如:HashSet
- 有的集合接口,提供了一系列排列有序的元素,并且可以在序列中间快速的插入或者删除有关元素。treeSet
- 简单而快速的搜索查找其中的某一条元素
3.3 集合和数组
集合与数组的区别:
- 数组的长度固定的,而集合长度时可变的
- 数组只能储存同一类型的元素,而且能存基本数据类型和引用数据类型。集合可以存储不同类型的元素,只能存储引用数据类型
- 一个数组实例具有固定的大小,不能伸缩。集合则可根据需要动态改变大小
- 数组声明了它容纳的元素的类型,而集合不声明
- 数组是一种可读/可写数据结构没有办法创建一个只读数组。然而可以使用集合提供的ReadOnly只读方式来使用集合。该方法将返回一个集合的只读版本。
3.4 泛型与集合的区别
泛型听起来很高深的一个词,但实际上它的作用很简单,就是提高java程序的性能。
而使用泛型则很好的解决这个问题,本质就是在编译阶段就告诉编译器,数据结构中元素的种类,既然编译器知道了元素的种类,自然就避免了拆箱、封箱的操作,从而显著提高java程序的性能。
比如List<string>就直接使用string对象作为List的元素,而避免使用object对象带来的封箱、拆箱操作,从而提高程序性能。
4.集合接口与类
4.1 Array-数组
数组:是以一段连续内存保存数据的。
特点:
- 随机访问是最快的,但不支持插入,删除,迭代等操作。
- Array可以包含基本类型和对象类型
- Array大小是固定的
- 所创建的对象都放在堆中。
- 够对自身进行枚举(因为都实现了IEnumerable接口)。
- 具有索引(index),即可以通过index来直接获取和修改任意项。
缺陷:
- 数组长度固定不变,不能很好地适应元素数量动态变化的情况。
- 可通过数组名.length获取数组的长度,却无法直接获取数组中真实存储的个数。
- 在进行频繁插入、删除操作时同样效率低下。
与ArrayList的区别:
- Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。
- Array只能存储同构的对象,而ArrayList可以存储异构的对象。
- 在CLR托管对中的存放方式,Array是始终是连续存放的,而ArrayList的存放不一定连续。
- Array不能够随意添加和删除其中的项,而ArrayList可以在任意位置插入和删除项。
4.2 Arrays
数组的工具类,里面都是操作数组的工具。
常用方法:
- 数组的排序:Arrays.sort(a);//实现了对数组从小到大的排序//注:此类中只有升序排序,而无降序排序。
- 数组元素的定位查找:Arrays.binarySearch(a,8);//二分查找法
- 数组的打印:Arrays.toString(a);//String 前的a和括号中的a均表示数组名称
- 查看数组中是否有特定的值:Arrays.asList(a).contains(1);
- Arrays.asList()方法返回的List集合是一个固定长度的List集合,不是ArrayList实例,也不是Vector的实例,而使其内部类。
4.3 Collection
见 3.2.1 Collection
4.4 Collections
[1]排序操作
- Collections提供以下方法对List进行排序操作
- void reverse(List list):反转
- void shuffle(List list),随机排序
- void sort(List list),按自然排序的升序排序
- void sort(List list, Comparator c);定制排序,由Comparator控制排序逻辑
- void swap(List list, int i , int j),交换两个索引位置的元素
- void rotate(List list, int distance),旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
[2]查找,替换操作
- int binarySearch(List list, Object key), 对List进行二分查找,返回索引,注意List必须是有序的
- int max(Collection coll),根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
- int max(Collection coll, Comparator c),根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
- void fill(List list, Object obj),用元素obj填充list中所有元素
- int frequency(Collection c, Object o),统计元素出现次数
- int indexOfSubList(List list, List target), 统计targe在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target).
- boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素。
[3]同步控制
Collections中几乎对每个集合都定义了同步控制方法, 这些方法,来将集合包装成线程安全的集合
- SynchronizedList(List);
- SynchronizedSet(Set);
- SynchronizedMap(Map);
SynchronizedMap和ConcurrentHashMap 区别
- Collections.synchronizedMap()和Hashtable一样,实现上在调用map所有方法时,都对整个map进行同步,ConcurrentHashMap的实现却更加精细,它对map中的所有桶加了锁。所以,只要要有一个线程访问map,其他线程就无法进入map,而如果一个线程在访问ConcurrentHashMap某个桶时,其他线程,仍然可以对map执行某些操作。这样,ConcurrentHashMap在性能以及安全性方面,明显比Collections.synchronizedMap()更加有优势。同时,同步操作精确控制到桶,所以,即使在遍历map时,其他线程试图对map进行数据修改,也不会抛出ConcurrentModificationException。
- ConcurrentHashMap从类的命名就能看出,它必然是个HashMap。而Collections.synchronizedMap()可以接收任意Map实例,实现Map的同步
- 线程安全,并且锁分离。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
4.5 List
List:有序(元素存入集合的顺序和取出的顺序一致),元素都有索引。元素可以重复。
- List本身是Collection接口的子接口,具备了Collection的所有方法。
- List的特有方法都有索引,这是该集合最大的特点。
- List集合支持对元素的增、删、改、查。
- List中存储的元素实现类排序,而且可以重复的存储相关元素。
- 次序是List最重要的特点:它保证维护元素特定的顺序。List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
- 除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
和set的最大区别区别:List允许有相同的元素,而set不允许
优点:操作读取操作效率高,基于数组实现的,可以为null值,可以允许重复元素,有序,异步。
缺点:由于它是由动态数组实现的,不适合频繁的对元素的插入和删除操作,因为每次插入和删除都需要移动数组中的元素
方法声明 | 方法功能 |
void add(int index,Object element) | 在列表的index位置添加元素 |
Object remove(int index) | 删除列表中index位置的元素 |
Object get(int index) | 返回列表index位置的元素 |
Object set(int index,Object element) | 用指定元素替换列表中指定位置的元素 |
int indexOf(Object o) | 返回列表中首次出现指定元素的索引,如果列表中不包含此元素,则返回-1 |
int lastIndexOf(Object o) | 返回列表中最后出现指定元素的索引,如果列表中不包含此元素,则返回-1 |
4.5.1 ArrayList
要点:
- ArrayList 是基于动态数组实现,内存中分配连续的空间,需要维护容量大小。实现了RandomAccess接口,支持随机访问。
- 新建ArrayList的时候,JVM为其分配一个默认(10)或指定大小的连续内存区域(封装为数组)。
- 频繁遍历查看元素,使用 ArrayList 集合,ArrayList 查询快,增删慢
- 索引ArrayList时,速度比原生数组慢是因为你要用get方法,这是一个函数调用,而数组直接用[ ]访问,相当于直接操作内存地址,速度当然比函数调用快。
- 存入的元素有序,可以为null,允许重复,非线程安全。
- ArrayList里面的removeIf方法就接受一个Predicate参数,采用如下Lambda表达式就能把,所有null元素删除:
ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间list.removeIf(e -> e == null);
- ArrayList也采用了快速失败(Fail-Fast机制)的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
扩容机制:
加载因子的系数小于等于1,意指 即当 元素个数 超过 容量长度*加载因子的系数 时,进行扩容。
另外,扩容也是有默认的倍数的,不同的容器扩容情况不同。
- ArrayList基于数组方式实现,默认初始容量为 10,容量限制不大于Integer.MAX_VALUE的小大,原容量的 0.5倍+1 如 ArrayList的容量为10,一次扩容后是容量为16.
- ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间
- 如果提前知道数组元素较多,可以在添加元素前通过调用ensureCapacity()方法提前增加容量以减小后期容量自动增长的开销。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
是否允许NULL值?
允许多个null值
是否线程安全?如何保证线程安全
ArrayList不是一个线程安全的集合,如果集合的增删操作需要保证线程的安全性,可以考虑使用CopyOWriteArrayList或者使用collections.synchronizedList(Lise l)函数返回一个线程安全的ArrayList类。
为什么ArrayList的elementData是用transient修饰的?
transient修饰的属性意味着不会被序列化,也就是说在序列化ArrayList的时候,不序列化elementData。
elementData不总是满的,每次都序列化,会浪费时间和空间
重写了writeObject 保证序列化的时候虽然不序列化全部 但是有的元素都序列化
所以说不是不序列化 而是不全部序列化。
elementData属性采用了transient来修饰,不使用Java默认的序列化机制来实例化,自己实现了序列化writeObject()和反序列化readObject()的方法。
4.5.2 linkedList
- LinkedList 是基于循环双向链表数据结构,不需要维护容量大小。顺序访问。 LinedList适合用迭代遍历;
- 频繁插入删除元素 使用 LinkedList 集合
- 线程不安全
- 链表增删快,查找慢
- LinkedList是一个继承于AbstractSequentialList的双向链表,它可以被当做堆栈(stack),队列(queue)或双向队列(deque)。LinkedList是List和Deque接口的双向链表的实现。实现了所有可选列表操作,并允许包括null值。
- 使用foreach适合循环LinkedList,使用双链表结构实现的应当使用foreach循环。
- LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
是否允许NULL值?
允许多个null值
4.5.3 vector
- Vector:底层的数据结构就是数组,线程同步的,Vector无论查询和增删都巨慢。
- 就是一个线程安全的ArrayList ,不推荐使用,线程安全可以用 CopyOnWriteArrayList
扩容机制:
- 加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容
- 扩容增量:原容量的 1倍, 可指定
- 如 Vector的容量为10,一次扩容后是容量为20
Stack
Stack继承自Vector,实现一个后进先出的堆栈.
属于stack自己的方法包括
- push( num) //入栈
- pop() //栈顶元素出栈
- empty() //判定栈是否为空
- peek() //获取栈顶元素
- search(num) //判端元素num是否在栈中,如果在返回1,不在返回-1。
注意:pop()和peek()的区别。pop()会弹出栈顶元素并返回栈顶的值,peek()只是获取栈顶的值,但是并不会把元素从栈顶弹出来。
4.5.4 Arraylist 与 LinkedList 区别?
1. 是否保证线程安全: ArrayList
和 LinkedList
都是不同步的,也就是不保证线程安全;
2. 底层数据结构: Arraylist
底层使用的是 Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
3. 插入和删除是否受元素位置的影响: ① ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候, ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList
采用链表存储,所以对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n))
因为需要先移动到指定位置再插入。
4. 是否支持快速随机访问: LinkedList
不支持高效的随机元素访问,而 ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。
5. 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
补充内容:RandomAccess接口
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess
接口中什么都没有定义。所以,在我看来 RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在 binarySearch(
)方法中,它要判断传入的list 是否 RamdomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList
实现了 RandomAccess
接口, 而 LinkedList
没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
- 实现了
RandomAccess
接口的list,优先选择普通 for 循环 ,其次 foreach, - 未实现
RandomAccess
接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环
补充内容:双向链表和双向循环链表
双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。
双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。
4.6 Set
- 无序(存入和取出顺序有可能不一致),不可以存储重复元素。必须保证元素唯一性。
注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的
- Set具有与Collection完全一样的接口,因此没有任何额外的功能,只是行为不同。这是继承与多态思想的典型应用:表现不同的行为。
- Set本身有去重功能是因为String内部重写了hashCode()和equals()方法,在add里实现了去重, Set集合是不允许重复元素的,但是集合是不知道我们对象的重复的判断依据的,默认情况下判断依据是判断两者是否为同一元素(euqals方法,依据是元素==元素),如果要依据我们自己的判断来判断元素是否重复,需要重写元素的equals方法(元素比较相等时调用)hashCode()的返回值是元素的哈希码,如果两个元素的哈希码相同,那么需要进行equals判断。【所以可以自定义返回值作为哈希码】 equals()返回true代表两元素相同,返回false代表不同。
- set集合没有索引,只能用迭代器或增强for循环遍历
- set的底层是map集合
- Set最多有一个null元素
- Set是基于对象的值来判断归属的,由于查询速度非常快速,HashSet使用了散列,HashSet维护的顺序与TreeSet或LinkedHashSet都不同,因为它们的数据结构都不同,元素存储方式自然也不同。
- TreeSet的数据结构是“红-黑树”,HashSet是散列函数,LinkedHashSet也用了散列函数;如果想要对结果进行排序,那么选择TreeSet代替HashSet是个不错的选择}
4.6.1 Hashset
- HashSet类直接实现了Set接口, 其底层其实是包装了一个HashMap去实现的。HashSet采用HashCode算法来存取集合中的元素,因此具有比较好的读取和查找性能。
- 对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都说调用HashMap的相关方法来实现的,对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。
- 线程 是不同步的 。 无序,高效
- 此类允许使用Null元素,放在数组头部,但只能放入一个null
- 不要像hashSet中放入可变对象,有可能改变equals 比较结果。
补充内容 hashCode方法
把对象内的每个意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的hashCode值。用第1步计算出来的多个hashCode值组合计算出一个hashCode值返回
return f1.hashCode()+(int)f2;
为了避免直接相加产生的偶然相等(两个对象的f1、f2实例变量并不相等,但他们的hashCode的和恰好相等),可以通过为各个实例变量的hashCode值乘以一个质数后再相加
return f1.hashCode()*19+f2.hashCode()*37;
4.6.2 TreeSet
- TreeSet : 保存次序的Set, 底层为红黑树结构。使用它可以从Set中提取有序的序列。
- TreeSet 继承AbstractSet类,实现NavigableSet、Cloneable、Serializable接口。与HashSet是基于HashMap实现一样,TreeSet 同样是基于TreeMap 实现的。由于得到Tree 的支持,TreeSet 最大特点在于排序,它的作用是提供有序的Set集合。
- 存入TreeSet的元素必须具有可比较性,否则在运行时会抛出ClassCastException 异常。
TreeSet集合排序有两种方式,Comparable和Comparator区别:
- 让元素自身具备比较性,需要元素对象实现Comparable接口,覆盖compareTo方法。obj1.compareTo(obj2),如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。
- 让集合自身具备比较性,需要定义一个实现了Comparator接口的比较器,并覆盖compare方法,并将该类对象作为实际参数传递给TreeSet集合的构造函数。
- 不能写入空数据,不能存null
- TreeSet方法保证元素唯一性的方式:就是参考比较方法的结果是否为0,如果return 0,视为两个对象重复,不存。
- 向TreeSet中添加一个元素,只有第一个不需要使用compareTo()方法,后面的都要调用该方法。因为只有相同类的两个实例才会比较大小,所以向TreeSet中添加的应该是同一个类的对象,在默认的compareTo方法中,需要将的两个的类型的对象的转换同一个类型,因此需要将的保证的加入到TreeSet中的数据类型是同一个类型,但是如果自己覆盖compareTo方法时,没有要求两个对象强制转换成同一个对象,是可以成功的添加treeSet中
TreeSet类是SortedSet接口的实现类。因为需要排序,所以性能肯定差于HashSet。与HashSet相比,额外增加的方法有:
- first():返回第一个元素
- last():返回最后一个元素
- lower(Object o):返回指定元素之前的元素
- higher(Obect o):返回指定元素之后的元素
- subSet(fromElement, toElement):返回子集合
可以定义比较器(Comparator)来实现自定义的排序。默认自然升序排序。
- BigDecimal、BigInteger以及所有的数值类型对应的包装类型,按对应的数值大小进行比较
- Character:按字符的Unicode值进行比较
- Boolean:true对应的包装类实例大于false包装类对应的实例
- String:按字符对应的Unicode值进行比较
- Date、Time:后面的时间、日期比前面的时间、日期大
4.6.3 LinkedHashSet
- LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序
- 线程 是不同步的 。 有序,高效
- 这个相对于HashSet来说有一个很大的不一样是LinkedHashSet是有序的。但是插入时性能稍微逊色于HashSet。
- LinkedHashSet不允许元素的重复
4.7 Queue
- Queue用于模拟队列这种数据结构,实现“FIFO”等数据结构。即第一个放进去就是第一个拿出来的元素(从一端进去,从另一端出来)。队列常作被当作一个可靠的将对象从程序的某个区域传输到另一个区域的途径。通常,队列不允许随机访问队列中的元素。
- Queue 接口并未定义阻塞队列的方法,而这在并发编程中是很常见的。BlockingQueue 接口定义了那些等待元素出现或等待队列中有可用空间的方法,这些方法扩展了此接口。
- Queue 实现通常不允许插入 null 元素,尽管某些实现(如 LinkedList)并不禁止插入 null。即使在允许 null 的实现中,也不应该将 null 插入到 Queue 中,因为 null 也用作 poll 方法的一个特殊返回值,表明队列不包含元素。
Queue的实现:
LinkedList提供了方法以支持队列的行为,并且实现了Queue接口。
- offer:在允许的情况下,将一个元素插入到队尾,或者返回false
- peek,element:在不移除的情况下返回队头,peek在队列为空返回null,element抛异常NoSuchElementException
- poll,remove:移除并返回队头,poll当队列为空是返回null,remove抛出NoSuchElementException异常
注意:queue.offer在自动包装机制会自动的把random.nextInt转化程Integer,把char转化成Character
Queue的并发实现:BlockQueue
具体参考:https://www.jianshu.com/p/f4eefb069e27
https://www.cnblogs.com/haimishasha/p/11198465.html
JDK 8 中提供了七个阻塞队列可供使用(上图的DelayedWorkQueue是ScheduledThreadPoolExecutor的内部类):
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的无界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
4.7.1 Deque(双向队列)
- Deque是Queue的子接口,我们知道Queue是一种队列形式,而Deque则是双向队列,它支持从两个端点方向检索和插入元素,因此Deque既可以支持LIFO形式也可以支持LIFO形式.
- Deque接口是一种比Stack和Vector更为丰富的抽象数据形式,因为它同时实现了以上两者.
添加功能
- void push(E) 向队列头部插入一个元素,失败时抛出异常
- void addFirst(E) 向队列头部插入一个元素,失败时抛出异常
- void addLast(E) 向队列尾部插入一个元素,失败时抛出异常
- boolean offerFirst(E) 向队列头部加入一个元素,失败时返回false
- boolean offerLast(E) 向队列尾部加入一个元素,失败时返回false
获取功能
- E getFirst() 获取队列头部元素,队列为空时抛出异常
- E getLast() 获取队列尾部元素,队列为空时抛出异常
- E peekFirst() 获取队列头部元素,队列为空时返回null
- E peekLast() 获取队列尾部元素,队列为空时返回null
删除功能
- boolean removeFirstOccurrence(Object) 删除第一次出现的指定元素,不存在时返回false
- boolean removeLastOccurrence(Object) 删除最后一次出现的指定元素,不存在时返回false
弹出功能
- E pop() 弹出队列头部元素,队列为空时抛出异常
- E removeFirst() 弹出队列头部元素,队列为空时抛出异常
- E removeLast() 弹出队列尾部元素,队列为空时抛出异常
- E pollFirst() 弹出队列头部元素,队列为空时返回null
- E pollLast() 弹出队列尾部元素,队列为空时返回null
迭代器
- Iterator<E> descendingIterator() 返回队列反向迭代器
Deque的实现也可以划分成通用实现和并发实现.
- 通用实现:主要有两个实现类ArrayDeque和LinkedList.
- 并发实现:LinkedBlockingDeque,在队列为空的时候,它的takeFirst,takeLast会阻塞等待队列处于可用状态
补充内容 ArrayDeque :Java容器类详解(十四)ArrayDeque详解
补充内容 LinkedBlockingDeque :Java并发编程之LinkedBlockingDeque阻塞队列详解
补充内容 ArrayDeque和LinkedList.比较
- ArrayDeque是个可变数组,它是在Java 6之后新添加的,而LinkedList是一种链表结构的list,LinkedList要比ArrayDeque更加灵活,因为它也实现了List接口的所有操作,并且可以插入null元素,这在ArrayDeque中是不允许的.
- 从效率来看,ArrayDeque要比LinkedList在两端增删元素上更为高效,因为没有在节点创建删除上的开销.最适合使用LinkedList的情况是迭代队列时删除当前迭代的元素.此外LinkedList可能是在遍历元素时最差的数据结构,并且也LinkedList占用更多的内存,因为LinkedList是通过链表连接其整个队列,它的元素在内存中是随机分布的,需要通过每个节点包含的前后节点的内存地址去访问前后元素
- 总体ArrayDeque要比LinkedList更优越,在大队列的测试上有3倍与LinkedList的性能,最好的是给ArrayDeque一个较大的初始化大小,以避免底层数组扩容时数据拷贝的开销.
4.8 Map
- Map主要用于存储健值对,根据键得到值,因此不允许键重复,但允许值重复。
- Map接口概述:Java.util.Map<k,v>接口:是一个双列集合
- Map集合的特点: 是一个双列集合,有两个泛型key和value,使用的时候key和value的数据类型可以相同。也可以不同.
- Key不允许重复的,value可以重复的;
- 一个key只能对应一个value
- 底层是一个哈希表(数组+单向链表):查询快,增删快, 是一个无序集合
Map接口中的常用方法:
- 1.get(key) 根据key值返回对应的value值,key值不存在则返回null
- 2.put(key , value); 往集合中添加元素(key和value) 注意:添加的时候,如果key不存在,返回值null。 如果Key已经存在的话,就会新值替换旧值,返回旧值
- 3. remove(key); 删除key值对应的键值对;如果key不存在 ,删除失败。返回值为 null,如果key存在则删 除成功,返回值为删除的value
Map遍历方式
第一种方式:通过key找value的方式:
Map中有一个方法:
Set <k> keySet(); 返回此映射包含的键的Set 集合
操作步骤:
1.调用Map集合的中方法keySet,把Map集合中所有的健取出来,存储到Set集合中
2.遍历Set集合,获取Map集合中的每一个健
3.通过Map集合中的方法get(key),获取value值
可以使用迭代器跟增强for循环遍历
第二种方式:Map集合遍历键值方式
Map集合中的一个方法:
Set<Map.Entry<k,v>> entrySet(); 返回此映射中包含的映射关系的Set视图
使用步骤
* 1.使用Map集合中的方法entrySet,把键值对(键与值的映射关系),取出来存储到Set 集合中
* 2.遍历Set集合,获取每一个Entry对象
* 3.使用Entry对象中的方法getKey和getValue获取健和值
可以使用迭代器跟增强for循环遍历
4.8.1 HashMap
HashMap的底层实现
JDK1.8之前
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
static int hash(int h) {
// 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);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
- HashMap非线程安全,高效,支持null;
- 遍历时,取得数据的顺序是完全随机的。
- HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null。(不允许键重复,但允许值重复)
- HashMap不支持线程的同步(任一时刻可以有多个线程同时写HashMap,即线程非安全),可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap() 方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
初始容量大小和每次扩充容量大小的不同
- HashMap 默认的初始化大小为16,之后每次扩充,容量变为原来的2倍。
- 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,HashMap 会将其扩充为2的幂次方大小(HashMap 中的
tableSizeFor()
方法保证,下面给出了源代码)。
补充内容 HashMap 的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“
(n - 1) & hash
”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
面试可以这么回答:因为数组长度和hashcode值进行取模运算,得到余数作为数组下标,而长度是2的幂次方倍可以转化成位操作,简化计算机的运算从而减少哈希冲突,数据尽可能的平均分配。
补充内容 HashMap 多线程操作导致死循环问题
主要原因在于 并发下的Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
4.8.2 LinkedHashMap
- LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
4.8.3 TreeMap
- TreeMap实现SortMap接口,能够把它保存的记录根据键排序。
- 默认是按键的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
补充内容 Comparable 和 Comparator 的区别:
- comparable接口实际上是出自java.lang包 它有一个
compareTo(Object obj)
方法用来排序 - comparator接口实际上是出自 java.util 包它有一个
compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()
方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
.
Comparator定制排序
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组:");
System.out.println(arrayList);
// void reverse(List list):反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
// void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList):");
System.out.println(arrayList);
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("定制排序后:");
System.out.println(arrayList);
Output:
原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]
重写compareTo方法实现按年龄来排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* TODO重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
} else if (this.age < o.getAge()) {
return -1;
}
return age;
}
}
public static void main(String[] args) {
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
pdata.put(new Person("张三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("小红", 5), "xiaohong");
// 得到key的值的同时得到key所对应的值
Set<Person> keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName());
}
}
Output:
5-小红
10-王五
20-李四
30-张三
4.8.4 HashTable
- Hashtable与 HashMap类似。不同的是:它不允许记录的键或者值为空;它支持线程的同步(任一时刻只有一个线程能写Hashtable,即线程安全),因此也导致了 Hashtable 在写入时会比较慢。
扩容机制
- Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1
- 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小
补充内容 ConcurrentHashMap
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
- 实现线程安全的方式(重要):① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
参考:集合类--最详细的面试宝典--看这篇就够用了(java 1.8)