1、Java集合概述
Java集合,也叫作容器。由两大接口派生而来:Collection接口,用于存放单一元素;Map接口,主要用于存放键值对。对于Collection接口,下面又有三个主要的子接口:List、Set、Queue
2、说说List,Set,Queue,Map的区别:
- List:存储的元素是有序的,可重复的。
- Set:存储的元素是无序的,不可重复的。
- Queue:按特定的排队规则来确定先后顺序
3、如何选用集合?
根据集合的特点来选择合适的集合:
- 我们需要根据键值获取到元素值时就选用Map接口下的集合 ,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap
- 我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List的接口比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用
4、为什么要使用集合?
当我们需要存储一组类型相同的数据时,数组是最常用且最基本的容器之一,但是,使用数组存储对象存在一些不足,因为在实际开发中,存储的数据类型多种多样且数量不确定时,这时,Java集合就派上用场了。与数组相比,Java集合提供了更灵活、更有效的方法来存储多个数据对象。Java集合框架中的各种集合类和接口可以存储不同类型和数量的对象,同时还具有多样化的操作方式。相较于数组,Java集合的优势在于它们的大小可变、支持泛型、具有内建算法等。总的来说,Java集合提高了数据的存储和处理灵活性,可以更好地适应现代软件开发中多样化的数据需求,并支持高质量的代码编写。
5、List
ArrayList和Array(数组)的区别?
- ArrayList内部基于动态数组实现,比Array(静态数组)使用起来更加灵活:
- ArrayList会根据实际存储的元素动态地扩容和缩容,而Array被创建之后就不能改变它的长度
- ArrayList允许使用泛型来确保类型安全,Array不可以
- ArrayList中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如Integer等)。Array可以直接存储基本类型数据,也可以存储对象。
- ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的API操作方法;Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
- ArrayList创建时不需要指定大小,而Array创建时必须指定大小
ArrayList可以添加null值吗
- ArrayList中可以存储任何类型的对象,包括null值。不过,不建议向ArrayList中添加null值,null值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。
ArrayList与LinkedList区别?
-
是否保证线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全
-
底层数据结构:ArrayList底层使用的是Object数组;LinkedList底层使用的是双向链表数据结构。
-
插入和删除是否受元素位置的影响
- ArrayList采用数组存储,插入和删除元素的时间复杂度受元素位置影响。
- LinkedList采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响。
-
是否支持快速随机访问:LinkedList不支持高效的随机元素访问,而ArrayList(因为实现了RandomAccess接口)支持。快速随机访问就是通过元素的序号快速获取元素对象。
-
内存空间占用:ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)
我们在项目中一般是不会使用到LinkedList的,需要用到LinkedList的场景几乎都可以使用ArrayList来代替,并且,性能通常会更好!
6、Set
无序性和不可重复性的含义是什么
- 无序性不等于随机性。无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定
- 不可重复性是指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashCode()方法
比较HashSet、LinkedHashSet和TreeSet三者的异同
- 三者都是Set接口的实现类,都能保证元素唯一,并且都不是线程安全的
- 底层数据结构不同。HashSet的底层数据结构是哈希表(基于HashMap实现)。LinkedHashSet的底层数据结构是链表和哈希表,元素的插入和取出顺序满足FIFO。TreeSet底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。‘
- 底层不同导致这三者的应用场景不同。HashSet用于不需要保证元素插入和取出顺序的场景,LinkedHashSet用于保证元素的插入和取出顺序满足FIFO的场景,TreeSet用于支持对元素自定义排序规则的场景。
7、Queue
Queue与Deque的区别
- Queue是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循FIFO原则
- Deque是双端队列,在队列的两端均可以插入或删除元素。
- Deque还提供有push()和pop()等其他方法,可用于模拟栈
ArrayDeque与LinkedList的区别
二者都实现了Deque接口,两者都具有队列的功能,区别如下:
- ArrayDeque是基于可变长的数组和双指针来实现,而LinkedList则通过链表来实现
- ArrayDeque不支持存储NULL数据,而LinkedList支持
- ArrayDeque是在jdk1.6才被引入,而LinkedList早在jdk1.2时就已经存在
- ArrayDeque插入时可能存在扩容过程,不过均摊后的插入操作依然为0(1)。虽然LinkedList不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用ArrayDeque来实现队列要比LinkedList更好。此外,ArrayDeque也可以用于实现栈
说一说PriorityQueue
PriorityQueue是在JDK1.5中被引入,其与Queue的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队
什么是BlockingQueue
BlockingQueue(阻塞队列)是一个接口,继承自Queue。BlockingQueue阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持如果队列已满,一直等到队列可以放入新元素时再放入。
8、Map
HashMap和Hashtable的区别
- 线程是否安全:HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable内部的方法基本都经过synchronized修饰。
- 效率:因为线程安全的问题,HashMap要比Hashtable效率高一点。另外,Hashtable基本被淘汰,不要在代码中使用它
- HashMap可以存储null的key和value,但null作为键只能有一个,null作为值可以有多个;Hashtable不允许有null键和null值,否则会抛出空指针异常
- 底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable没有这样的机制
HashMap和TreeMap区别
TreeMap和HashMap都继承自AbstractMap,但是,TreeMap还实现了NavigableMap接口和SortedMap接口。实现NavigableMap接口让TreeMap有了对集合内元素的搜索的能力;实现SortedMap接口TreeMap有了对集合中的元素根据键排序的能力。默认是按Key的升序排序,不过我们也可以指定排序的比较器。
HashMap的底层实现
- Jdk1.8之前,HashMap底层是数组和链表结合在一起使用也就是链表散列。HashMap通过key的hashcode经过扰动函数处理过后得到hash值,然后通过 (n-1)&hash 判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
- jdk1.8之后,相比于之前的版本,JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。
HashMap多线程操作导致死循环问题
JDK1.7及之前的HashMap在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用HashMap,因为多线程下使用HashMap还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap
HashMap为什么线程不安全?
JDK1.7及之前版本,在多线程环境下,HashMap扩容时会造成死循环和数据丢失的问题。JDK1.8之后,在HashMap中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对HashMap的Put操作会导致线程不安全,具体来说会有数据覆盖的风险。
HashMap常见的遍历方式
存在阻塞时parallelStream性能最高,非阻塞时parallelStream性能最低
HashMap 遍历从大的方向来说,可分为以下 4 类:
- 迭代器(Iterator)方式遍历;
- For Each 方式遍历;
- Lambda 表达式遍历(JDK 1.8+);
- Streams API 遍历(JDK 1.8+)。
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
- 使用迭代器(Iterator)EntrySet 的方式进行遍历;
- 使用迭代器(Iterator)KeySet 的方式进行遍历;
- 使用 For Each EntrySet 的方式进行遍历;
- 使用 For Each KeySet 的方式进行遍历;
- 使用 Lambda 表达式的方式进行遍历;
- 使用 Streams API 单线程的方式进行遍历;
- 使用 Streams API 多线程的方式进行遍历。
ConcurrentHashMap线程安全的具体实现方式/底层具体实现
Java8中,ConcurrentHashMap取消了Segment分段锁,采用Node+CAS+synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
Java8中,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,就不会影响其他Node的读写,效率大幅提升。
9、Java集合使用注意事项总结
集合判空
判断所有集合内部的元素是否为空,使用isEmpty()方法,而不是size()==0的方式
因为isEmpty()方法的可读性更好,并且时间复杂度为O(1)
集合转Map
在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。
集合遍历
不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
集合去重
可以利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains() 进行遍历去重或者判断包含操作。
集合转数组
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
数组转集合
使用工具类Array.aslist()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常