Java 所有的集合类都位于 java.util 包下
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
Java 集合类型(也称容器)分为: Collection 和 Map
List的4种实现类
1、ArrayList
ArrayList是我们在java开发过程中最常见的一种List实现类,属于线程不安全,读取速度快的一种List实现类。也是java入门时最常用的实现类。
其中最重要的三个参数,初始数组增量和一个数组
因为ArrayList采用的是数组的方式实现,所以其取值速度快,插入碰到扩容问题时速度会减慢
2、Vector
和ArrayList基本相似,利用数组及扩容实现List,但Vector是一种线程安全的List结构,它的读写效率不如ArrayList,其原因是在该实现类内在方法上加上了同步关键字,其不同之处还在于Vector的增长速度不同
Vector在默认情况下是以两倍速度递增
所以capacityIncrement可以用来设置递增速度,因此Vector的初始化多了一种方式,即设置数组增量
3、LinkedList
4、Stack
ArrayList 的扩容机制?
ArraylList底层实现是Object数组。数组大小一旦规定则无法修改。
扩容机制:当调用ArrayList的add方法时就会设计到扩容机制。
总结:ArrayList在第一次插入元素add()时分配10(默认)个对象空间。假如有20个数据需要添加,那么会在第11个数据的时候(原始数组容量存满时),按照1.5倍增长;之后扩容会按照1.5倍增长每次扩容都是通过Arrays.copyOf(elementData, newCapacity) —>Array.copyOf方法将elementData数组指向新的长度为扩容后长度的内存空间这样的方式实现的。
ArrayList和LinkedList的异同?
都是线程不安全的,相对于线程安全的Vector,执行效率高。
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)。 3、时间复杂度为O(n)。 就代表数据量增大几倍,耗时也增大几倍。
ArrayList集合实现RandomAccess接口有何作用?为何LinkedList集合却没实现这接口?
ArrayList实现RandomAccess接口,但是RandomAccess接口里面是空的!LinkedList并没有实现RandomAccess接口。
原因:实现RandomAccess接口的List可以通过for循环来遍历数据比使用iterator遍历数据更高效,未实现RandomAccess接口的List可以通过iterator遍历数据比使用for循环来遍历数据更高效。
RandomAccess接口是一个标志接口(Marker)。只要List集合实现这个接口,就能支持快速随机访问
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快
RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!
怎么判断出接收的List子类是ArrayList还是LinkedList呢?
这时就需要用InstanceOf来判断List集合子类是否实现RandomAccess接口!
数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?
ArrayList和Vector的区别?
Vector是同步类(synchronize),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差 ,因此已经是Java中的遗留容器
Set的实现类
1、HashSet
2、LinkedHashSet
3、TreeSet
HashSet 怎么保证元素不重复的?
元素的值是map的key,map的value是present变量,这个变量只作为放入map的一个占位符而存在,并没有什么实际作用。
HashMap 的 key 是不能重复的,而这里HashSet的元素又是作为了map的key,当然不能重复
Map接口常用实现类
HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类
HashMap存储结构
2、LinkedHashMap
3、TreeMap
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
TreeSet底层使用红黑树结构存储数据
TreeMap 的 Key 的排序:
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
4、Hashtable
5、Properties
Set和Map容 器 都有基于哈希存储和排序树的两种实现版本,基于哈希存储 的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果 。
HashMap的底层原理
在JDK1.7之前,HashMap采用数据+联表的结构来实现。在JDK1.8中,HashMap采用数据+联表+红黑树实现。当链表长度超过阈值8时,将链表转换为红黑树。
扩容机制:HashMap的默认初始容量:16,这个容量会以2的指数增长。当元素到达一定比例就会扩容,这个比例是负载因子,默认为0.75。阈值=初始容量16*负载因子0.75
HashMap的put方法流程
判断数组,若发现数组为空,则进行首次扩容
判断头节点,若发现头节点为空,则新建链表节点,存入数组
判断头结点,若发现头结点不为空,则将元素插入槽内
若元素的key与头节点一致,则直接覆盖头节点
若元素为树形节点,则元素追加到树中
若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表的长度以决定是否转为红黑树。(1)若链表长度达到8,数组容量没达到64,则扩容。(2)若链表长度达到8、数组容量达到4,则转为红黑树。
扩容机制:向HashMap中添加数据时,三个条件触发扩容行为
(1)如果数组为空,则进行首次扩容
(2)将元素接入链表后,如果长度达到8,并且数组长度 < 64,则扩容
(3)添加后,如果数组中元素超过阈值,即超出负载因子的限制,则扩容。
并且每次扩容时,都将容量翻倍,即创建一个2倍大的新数组,然后将旧数组中的数据迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。
插入元素后,判断元素的个数,若发现超过阈值则再次扩容
HashMap 的 get方法流程
HashMap 的 resize 方法的执行过程
HashMap 的 size 为什么必须是 2 的整数次方?
Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的
HashMap 多线程死循环问题?
死循环形成是在扩容转移元素的时候发生的。具体在tansfer函数中,默认情况下rehash方法为false
当往HashMap中添加元素时,会引起HashMap容器的扩容
假设有两个线程:T1 T2,假设HashMap的当前树组容量是2:
1、现在,有线程T1和T2同时对该HashMap进行扩容,并且它们扩容后,都把结点元素全部移动到新树组的索引3处。-----在索引1处的链表引用关系是 a -> b -> c -> d -> null。
2、假设线程T1运行到Entry
这行代码,时间片就用完,即当前T1已计算得出e=a,e.next=b
。
3、线程T2开始执行并且完成了整个扩容操作,并把链表移到了索引3处----索引3处的链表引用关系是 d -> c -> b -> a -> null。
4、线程T1拿到时间片了,继续执行Entry
后面的代码,注意此时T1中e=a,e.next=b
,所以需要将结点a头插到索引3的位置----索引1处的链表引用关系是 a -> null
5、由于T2中扩容后得到的链表关系是 d -> c -> b -> a -> null,因此T1线程中此时链表结点引用关系实际上应是这样的:b -> a -> null -> d -> c
6、然后,执e = next;
和Entry
代码,对e变量以及e.next变量重新赋值,得到:e=a
,e.next=null。
继续将a头插到索引3的位置:a -> b 而 b -> a; d -> c -> b。链表结点a和b互相引用了,即形成了一个环。当我们使用get方法,取到索引为3中的某元素时候,将会出现死循环
由于d结点和c结点并没有其他结点指向它们,所以,d和c结点的数据也将会丢失。
原因
解决
HashMap 的 get 方法能否判断某个元素是否在 map 中?
不能。因为get返回null有可能是不包含该key,也有可能该key对应的value为null。因为HashMap中的key和value都允许为null
LinkedHashMap 的实现原理?
LinkedHashMap 基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在table里,是额外独立出来的。
LinkedHashMap通过继承HashMap 中的Entry ,并添加两个属性Entry before,after。和header结合起来组成一个双向链表,来实现安插入顺序或访问顺序排序
LinkedHashMap 定义了排序模式AccessOrder,该属性为boolean型变量,对于访问顺序为true,对于插入顺序则为false。一般情况下,不比指定排序模式,其迭代顺序即为默认插入顺序。
HashTable和HashMap的区别
相同点:
HashMap和HashSet的区别
ConcurrentHashMap 的实现原理是什么?
JDK1.7中,ConcurrentHashMap 采用数组+Segment+分段锁的实现方式
JDK1.8中,ConcurrentHashMap 采用数组+链表+红黑树的实现方式,内部大量采用CAS操作
ConcurrentHashMap 主干是Segment数组。Segment继承了ReentrantLock,所以它是一种可重入锁
在ConcurrentHashMap ,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。就按默认的ConcurrentLevel为16来讲,理论上就允许16个线程并发执行。
所以,对于同一个Segment的操作才需考虑线程同步不同的Segment 则无需考虑。Segment类似与HashMap,一个Segment维护一个HashEntry数组
HashEntry 是目前我们提到的最小逻辑处理单元。一个ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作。第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。
HashMap和HashTable与ConCurrentHashMap的区别
ConCurrentHashMap仅仅锁定map的某个部分,HashMap锁住整个map
当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
HashTable(同一把锁):使用synchronize来保证线程安全,但效率非常低下。ConCurrentHashMap(分段锁):(锁分段技术)每一把锁只锁容器其中一部分数据,多线程访问容器不同数据段的数,就不会存在竞争,提高并发访问率。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。concurrenthashmap是由Segment数组结构和HahEntry数组结构组成。Segment是一种可重入锁ReentrantLock,扮演锁的角色。HashEntry用于存储键值对数据。一个concurrenthashmap里包含一个Segment数组。Segment的结构和Hashmap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment。
Iterator 怎么使用?有什么特点?
迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价小。Java 中的 Iterator 功能比较简单,并且只能单向移动:
使用方法 iterator() 要求容器返回一个 Iterator。第一次调用 Iterator 的 next() 方法时,它返回序列的第一个元素。注意:iterator() 方法是 java.lang.Iterable 接口,被 Collection 继承。
使用 next() 获得序列中的下一个元素。
使用 hasNext() 检查序列中是否还有元素。
使用 remove() 将迭代器新返回的元素删除。
Iterator 和 ListIterator 有什么区别?
Iterator 和 Enumeration 接口的区别?
与 Enumeration 相比,Iterator 更加安全,因为当一个集合正在被遍历的时候,它会阻止其它线程去修改集合。否则会抛出 ConcurrentModificationException 异常。这其实就是 fail-fast 机制。具体区别有三点:
fail-fast (快速失败)与 fail-safe(安全失败)有什么区别?
Iterator的fail-fast属性与当前的集合共同起作用(安全失败是基于对底层集合做拷贝),因此他不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而Java.util.concurrent中的集合类都为fail-safe。当检测到正在遍历的集合的结构被改变时,fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException
Collection 和 Collections 有什么区别?
Hash算法
指任意长度输入经过hash算法转化为固定长度输出。多本书,总结出一本摘要,理解为hash值。hash算法可以理解为摘要算法或者散列算法
特点:
hash冲突解决方案:key经过hash算法定位到hash桶的某一个位置,该位置有值时会产生冲突,这个通过1、链表解决称为链地址法;2、也可以向下线性或者随机探测不冲突的地址,称为开放地址法。3、也可以通过多个hash算法重新定位称为再hash法。