赞
踩
JAVA集合和数据结构也是面试常考的点,内容也是比较多。所以我分为上下两篇向大家介绍!
在看之前希望各位如果方便可以点赞收藏,给我点个关注,创作不易!
Java
集合类主要由两个根接口Collection
和Map
派生出来的,Collection
派生出了三个子接口:List
、Set
、Queue
(Java5
新增的队列),因此Java
集合大致也可分成List
、Set
、Queue
、Map
四种接口体系。
注意:Collection是一个接口,Collections是一个工具类,Map不是Collection的子接口。
Java集合框架图如下:
图中,List
代表了有序可重复集合,可直接根据元素的索引来访问;Set
代表无序不可重复集合,只能根据元素本身来访问;Queue
是队列集合。
Map
代表的是存储key-value
对的集合,可根据元素的key
来访问value
。
上图中淡绿色背景覆盖的是集合体系中常用的实现类,分别是ArrayList、LinkedList、ArrayQueue、HashSet、TreeSet、HashMap、TreeMap
等实现类。
线程安全的:
线性不安全的:
ArrayList 和 LinkedList
都是不同步的,也就是不保证线程安全;Arraylist
底层使用的是Object
数组;LinkedList 底层使用的是双向循环链表数据结构;ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)
方法的时候, ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))
时间复杂度就为 O(n-i)
。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 LinkedList
采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)
而数组为近似 O(n)
。LinkedList
不支持高效的随机元素访问,而ArrayList
实现了RandomAccess
接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)
。ArrayList
的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。Vector是线程安全的,ArrayList不是线程安全的
。其中,Vector
在关键性的方法前面都加了synchronized
关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector
,因为不需要我们自己再去考虑和编写线程安全的代码。ArrayList
在底层数组不够用时在原来的基础上扩展0.5倍
,Vector
是扩展1倍,这样ArrayList
就有利于节约内存空间。ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍。
以JDK1.8为例说明:
public boolean add(E e) { //判断是否可以容纳e,若能,则直接添加在末尾;若不能,则进行扩容,然后再把e添加在末尾 ensureCapacityInternal(size + 1); // Increments modCount!! //将e添加到数组末尾 elementData[size++] = e; return true; } // 每次在add()一个元素时,arraylist都需要对这个list的容量进行一个判断。通过ensureCapacityInternal()方法确保当前ArrayList维护的数组具有存储新元素的能力,经过处理之后将元素存储在数组elementData的尾部 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { //如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 若ArrayList已有的存储能力满足最低存储要求,则返回add直接添加元素;如果最低要求的存储能力>ArrayList已有的存储能力,这就表示ArrayList的存储能力不足,因此需要调用 grow();方法进行扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // 获取elementData数组的内存空间长度 int oldCapacity = elementData.length; // 扩容至原来的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); //校验容量是否够 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //若预设值大于默认的最大值,检查是否溢出 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 调用Arrays.copyOf方法将elementData数组指向新的内存空间 //并将elementData的数据复制到新的内存空间 elementData = Arrays.copyOf(elementData, newCapacity); }
Array
可以包含基本类型和对象类型,ArrayList
只能包含对象类型。
Array
大小是固定的,ArrayList
的大小是动态变化的。
ArrayList
提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()
等等。
在JDK1.7 和JDK1.8 中有所差别:
在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
当链表超过 8 且数据总量超过 64 才会转红黑树。
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是 链地址法 。
p=H(key)
出现冲突时,则以p为基础,再次hash,p1=H(p)
,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点
。hash
函数,当R1=H1(key1
)发生冲突时,再计算R2=H2(key1)
,直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
回答这个问题前,我们来先看下HashMap的默认构造函数:
int threshold; // 容纳键值对的最大值
final float loadFactor; // 负载因子
int modCount;
int size;
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳键值对的最大值。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
默认的loadFactor是0.75,0.75是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :
如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。
相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
我们来追溯下作者在源码中的注释(JDK1.7):
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
翻译过来大概的意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。
总结:数据结构和集合这快的面试考点还是很多的,希望大家仔细看看,这只是一部分。我们分三部分来更新。
多谢大家支持!!!
如果没看过前几篇的文章的,可以点击链接去看看,如果方便可以点赞收藏,给我点个关注,创作不易!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。