带你整理面试过程中关于 Java 中的集合 List,Queue,Set的相关知识点
Posted 南淮北安
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于 Java 中的集合 List,Queue,Set的相关知识点相关的知识,希望对你有一定的参考价值。
文章目录
数组:可以存储对象,也可以存储基本数据类型,但是一次只能存储一种类型,且长度一定,不可改变
集合:只能存储对象,长度可变,可以存储不同类型的对象。
Java的集合类被定义在Java.util
包中,主要有4种集合,分别为List、Queue、Set和Map,每种集合的具体分类如图2-1所示。
Collection接口是集合类 (List,Set,Queue) 的根接口,java中没有提供这个接口的直接实现类。有三个子接口 List,Set,Queue,注意,Map 不是 Collection 的子接口
一、 List:可重复
List是非常常用的数据类型,是有序的Collection,一共有三个实现类,分别是ArrayList
、Vector
和LinkedList
。
关于 List 的具体操作:一篇文章带你搞定 Java 中的 List 接口
1. ArrayList:基于数组实现,增删慢,查询快,线程不安全
public class ArrayList<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
arraylist实现List接口,继承AbstractList。底层是数组实现,可以自增扩容。是非线程安全的,一般用于单线程环境中(与Vector
最大的区别就是,V是线程安全的,所以A比V的性能相对要好些),在多线程中,可以选择Vector
或者CopyOnWriteArrayList
。
CopyOnWriteArrayList
:是一个线程安全的list,底层是通过复制数组的方式实现的,比如在它的 add() 方法里,首先会加 lock 锁,锁住,然后会复制出一个新的数组,往新的数组里面加 add 真正的元素,最后把 array 的指向改变为新的数组
缺点:
每次 set/get 都会复制一个数组出来,耗费内存;
只能保证数据的最终一致性,不能保证实时一致性(比如两个线程,A线程读取 CopyOnWriteArrayList 的数据,没读完,B线程把这个 list 清了,线程A还是可以把剩余的数据读取出来。)
CopyOnWrite
:使用了 cow 机制,子进程被 fork 之后,和父进程使用的是相同的内存空间,当父子进程中有更改的行为发生时,再为子进程分配相应的物理空间,
这样做的好处就是等真正发生修改时再去分配资源,可以减少分配或者复制大量资源时带来的瞬间延时,简单说就是常见的懒加载,真正用的时候再分配
ArrayList是使用最广泛的List实现类,其内部数据结构基于数组实现所以也可以通过下标直接访问,提供了对List的增加(add)、删除(remove)和访问(get)功能。
ArrayList的缺点是对元素必须连续存储,当需要在ArrayList的中间位置插入或者删除元素时,需要将待插入或者删除的节点后的所有元素进行移动,其修改代价较高,因此,ArrayList不适合随机插入和删除的操作,更适合随机查找和遍历的操作
开发中用的最多的就是 ArrayList ,因为日常开发中遍历的需求比增删多,即便是增删,往往在 list 尾部添加就行了
ArrayList不需要在定义时指定数组的长度,在数组长度不能满足存储要求时,ArrayList会创建一个新的更大的数组并将数组中已有的数据复制到新的数组中,然后再进行下一步操作。
总结:
- ArrayList 是List接口的一个可变大小的数组的实现
- ArrayList 的内部是使用一个 Object 对象数组来存储元素的
- 初始化 ArrayList 的时候,可以指定初始化容量的大小,如果不指定,就会使用默认大小,为10
- 当添加一个新元素的时候,首先会检查容量是否足够添加这个元素,如果够就直接添加,如果不够就进行扩容,扩容为原数组容量的1.5倍
源码里有个
grow
方法,每次扩容原来的1.5 倍,数组扩完容之后,会调用Arrays.copyOf()
对数组进行拷贝,也就是创建了一个新的扩容后的数组,然后把原有的元素复制过来,再添加新的元素
如果提前知道需要加入大量元素,可以在 add 之前调用 ensureCapacity 方法,这样如果我们所需的最小容量如果大于第一次扩容的1.5倍,则会直接扩容到我们所需的容量,减少增量重新分配的次数。
- 当删除一个元素的时候,会将数组右边的元素全部左移,添加一个元素时,右移。
内容补充:
(1)Java 本身有数组了,为什么还要引入 ArrayList?
原生的数组,使用时必须要为其指定大小,指定大了可能内存浪费,指定小了,可能装不下,而 ArrayList 则不用,它是动态扩容的。
(2)Arrays.asList 构建的集合,不能赋值给 ArrayList
Arrays.asList
构建出来的List与new ArrayList
得到的List,压根就不是一个List
由Arrays.asList()
返回的是 Arrays 的内部类 ArrayList, 而不是java.util.ArrayList
。Arrays的内部类ArrayList 和 java.util.ArrayList
都是继承AbstractList,remove、add等方法 AbstractList 中是默认throw UnsupportedOperationException而且不作任何操作。
java.util.ArrayList
重写了这些方法而Arrays的内部类ArrayList没有重写,所以会抛出异常。
2. Vector:基于数组实现,增删慢,查询快,线程安全
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
Vector的数据结构和ArrayList一样,都是基于数组实现的,不同的是Vector支持线程同步,即同一时刻只允许一个线程对Vector进行写操作(新增、删除、修改),以保证多线程环境下数据的一致性,但需要频繁地对Vector实例进行加锁和释放锁操作,因此,Vector的读写效率在整体上比ArrayList低。
Vector中的操作是线程安全的;安全的原因是其中的方法都加了 synchronized
关于 Vector 的详细知识可参考:点击参考
3. LinkedList:基于双向链表实现,增删快,查询慢,线程不安全
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable
LinkedList
采用双向链表结构存储元素,在对LinkedList
进行插入和删除操作时,只需在对应的节点上插入或删除元素,并将上一个节点元素的下一个节点的指针指向该节点即可,数据改动较小,因此随机插入和删除效率很高。但在对LinkedList
进行随机访问时,需要从链表头部一直遍历到该节点为止,因此随机访问速度很慢。
除此之外,LinkedList
还提供了在List接口中未定义的方法,用于操作链表头部和尾部的元素,因此有时可以被当作堆栈、队列或双向队列使用。
4. 三者的区别联系
(1)ArrayList 与 Vector:
- 相同点:两者都是基于存储元素的数组来实现的,它们会在内存中开辟块连续的空间来存储,由于数据存储是连续的,它们支持用序号(下标)来访问元素,但是插入和删除是要移动容器中的元素,所以执行较慢。两者都有一个初始化的容量的大小,为10;当里面存储的元素超过这个大小时,就会动态的进行扩容。Vector默认扩充为原来的2倍,ArrayList默认扩充为原来的1.5倍。
- 区别:二者最大的区别在与synchronization(同步)的使用。在ArrayList中没有一个方法是同步的,而在Vector中,绝大部分方法都是同步的。所以Vector是线程安全的,而ArrayList不是线程安全的。由于Vector提供同步,所以性能上较低于ArrayList。
(2)ArrayList 和 LinkedList
- ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于双向链表的数据结构
- 对于随机访问,ArrayList 要优于 LinkedList,因为LinkedList要移动指针
- 对于插入和删除,LinkedList较占优势,ArrayList要移动数据。
- ArrayList和LinkedList都是非线程安全的容器
(3)List是线程安全的吗?如果要线程安全要怎么做?
List中的 Vector 才是线程安全的,其他要实现线程安全使用工具类 Collections.synchronizedList(new ArrayList())
方法或者使用 CopyOnWriteArrayList
List list = Collections.synchronizedList(new LinkedList(...));
(4)怎么给List排序?
使用 List 自身的 sort 方法,或者使用Collections.sort(list)方法;
(5)=Arrays.asList方法后的 List 可以扩容吗?
Arrays.asList`使用的是final数组,是一个抽象的List,里面没有对常用的方法比如 add进行覆写,所以不支持扩容。
(6)List和Array之间如何互相转换?
List 到 Array使用toArray方法,Array 到 List使用Arrays.asList(array)
方法,由于它是固定的,不固定的可以使用 new ArrayList(Arrays.asList(array))
。
(7)你知道的List都有哪些?:ArrayList、LinkedList、Vector等。
(8)List是有序的吗?: List是有序的。
(9)List和Vector有什么区别?:Vector是List接口下线程安全的集合,扩容时不一样
(10)ArrayList和LinkedList的底层数据结构是什么?
ArrayList使用的是数组结构,LinkedList使用的是链表结构。
(11)ArrayList 默认大小是多少,是如何扩容的?
Jdk1.7之前ArrayList默认大小是10,每次约按1.5倍扩容。
(12)ArrayList和LinkedList的区别?分别用在什么场景?
ArrayList和LinkedList数据结构不一样,前者用在查询较多的场合,后者适用于插入较多的场合。
(13)ArrayList 与 LinkedList 都有自己的使用场景,如果你不能很好的确定,那么就使用ArrayList 。但如果你能确定你会在集合的首位有大量的插入、删除以及获取操作,那么可以使用 LinkedList ,因为它都有相应的方法 addFirst 、 addLast 、removeFirst 、 removeLast 、 getFirst 、 getLast ,这些操作的时间复杂度都是 O(1) 1),非常高效。
LinkedList 的链表结构不一定会比 ArrayList 节省空间,首先它所占用的内存不是连续的,其次他还需要大量的实例化对象创造节点。虽然不一定节省空间,但链表结构也是非常优秀的数据结构,它能在你的程序设计中起着非常优秀的作用,例如可视化的链路追踪图,就是需要链表结构,并需要每个节点自旋一次,用于串联业务
二、Queue
Queue是队列结构,Java中的常用队列如下:
- ArrayBlockingQueue:基于数组数据结构实现的有界阻塞队列。
- LinkedBlockingQueue:基于链表数据结构实现的有界阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:支持延迟操作的无界阻塞队列。
- SynchronousQueue:用于线程同步的阻塞队列。
- LinkedTransferQueue:基于链表数据结构实现的无界阻塞队列。
- LinkedBlockingDeque:基于链表数据结构实现的双向阻塞队列。
三、Set:不可重复
关于 Set 相关的使用可参考:一篇文章带你搞懂 Java 中的 Set 接口
Set
核心是独一无二的性质,适用于存储无序且值不相等的元素。
对象的相等性在本质上是对象的HashCode
值相同,Java依据对象的内存地址计算出对象的HashCode
值。如果想要比较两个对象是否相等,则必须同时覆盖对象的hashCode
方法和equals
方法,并且hashCode
方法和equals
方法的返回值必须相同。
Set 最多有一个 null 元素
1. HashSet:HashTable实现,无序
HashSet 存放的是散列值,它是按照元素的散列值来存取元素的。
元素的散列值是通过元素的hashCode方法计算得到的,HashSet首先判断两个元素的散列值是否相等,如果散列值相等,则接着通过equals方法比较,如果equls方法返回的结果也为true, HashSet就将其视为同一个元素;
如果equals方法返回的结果为false, HashSet就不将其视为同一个元素。
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()
和equals()
方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet
HashSet是如何保证元素的唯一性?
当调用add()
方法向集合中存入对象的时候,首先会使用 hash()
函数生成一个 int
类型的 hashCode
散列值,然后与已经存储的元素的 hashCode
值作比较,
如果 hashCode
值不相等,则所存储的两个对象一定不相等,此时把这个新元素存储在它的 hashCode
位置上;
如果 hashCode
相等,存储元素的对象还是不一定相等,此时会调用 equals()
方法判断两个对象的内容是否相等,如果 equals()
返回true
,则是同一个对象,无需存储;
如果 equals()
返回 false
,那么就是不同的对象,就需要存储,不过由于 hashCode
相同,也就是发生了哈希碰撞,HashSet
会以链式结构将两个对象保存在同一位置,这将导致性能下降,因此在编码时应避免出现这种情况。
HashSet 底层是哈希表实现,所以负载因子是0.75
2. TreeSet:二叉树实现
TreeSet基于二叉树的原理对新添加的对象按照指定的顺序排序(升序、降序),每添加一个对象都会进行排序,并将对象插入二叉树指定的位置。
Integer 和 String 等基础对象类型可以直接根据 TreeSet 的默认排序进行存储,而自定义的数据类型必须实现 Comparable 接口,并且覆写其中的 compareTo 函数才可以按照预定义的顺序存储。若覆写 compare 函数,则在升序时在 this.对象小于指定对象的条件下返回 -1,在降序时在 this.对象大于指定对象的条件下返回1。
TreeSet底层数据结构采用红黑树来实现,元素唯一且已经排好序;
唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。
根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;
比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;
3. LinkHashSet:HashTable实现数据存储,双向链表记录顺序
还有一种Set类型,LinkedHashSet,它是HashSet的子类,底层数据结构采用链表+哈希表,链表保证元素的添加顺序,哈希表保证元素的唯一性
所有的方法和操作都与 HashSet 相同,因此 LinkedHashSet 的实现比较简单,只提供了4个构造方法,并通过传递一个标识参数调用父类的构造器,在底层构造一个LinkedHashMap来记录数据访问,其他相关操作与父类HashSet相同,直接调用父类HashSet的方法即可。
四、分析比较
五、面试题
- 说一下ArrayList和LinkedList区别和场景?
- list、map、set 分析,使用场景?(阿里)
- 理解 equals 和 hashCode?(阿里)
结合 HashSet 的存储的元素不可重复来讲
- ArrayList 中的 contains 方法
该方法主要是用来判断,元素是否在动态数组中
时间复杂度是O(N),因为ArrayList 底层是数组,需要动态遍历才能定位到元素
与它对应的还有HashSet也有contains,它的时间负责度就是O(1),因为它的底层是基于HashMap存储的,由这个元素可以直接计算得到它的hashCode,然后直接定位判断
以上是关于带你整理面试过程中关于 Java 中的集合 List,Queue,Set的相关知识点的主要内容,如果未能解决你的问题,请参考以下文章
带你整理面试过程中关于Redis 中的字典及 rehash的相关知识点
带你整理面试过程中关于Java 中多线程的创建方式的最全整理