Android面试指南-Java容器知识巩固

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~

一、集合框架概述,list,set,map都有哪些具体的实现类,各自的区别、使用场景都是什么?

Java集合里使用接口来定义功能,是一套完善的继承体系。Iterator是所有集合的总接口,其他所有接口都继承于它,该接口运用了迭代器模式定义了集合的遍历操作,Collection接口继承于Iterator,是集合的次级接口,它定义了集合的一些通用操作,而Map类族是独立存在的。

  • List:有序、可重复。索引查询速度快,插入、删除伴随数据移动,速度慢。

  • Map:键值对,键唯一,值多个;

  • Set:无序只能用迭代,不可重复。检索元素效率低下,插入和删除效率高,并且不会引起元素位置改变。

1、set集合从原理上如何保证不重复?

在往set中添加元素时,如果指定元素不存在,则添加成功。具体来讲:当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后用这个(元素的hashcode)%(HashSet集合的大小)+1计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去,如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找一个空位添加。

2、线程安全集合类与非线程安全集合类

ArrayList、LinkedList、HashSet、TreeSet是非线程安全的

ArrayList与LinkedList的区别和适用场景

Arraylist

  • 优点:是基于动态数组实现的数据结构,因地址连续,一旦数据存储好了,查询操作效率会比较高,因为在内存里是连着放的。

  • 缺点:因为地址连续,ArrayList要移动数据,所以插入和删除操作效率比较低。

LinkedList

  • 优点:是基于链表的数据结构,地址是任意的,其在开辟内存空间的时候不需要等一个连续的地址,对新增和删除操作,LinkedList比较占优势。适用于要头尾操作或插入指定位置的场景。

  • 缺点:要移动指针,所以查询操作性能比较低。

适用场景分析

当需要对数据进行快速访问的情况下选用ArrayList,当要对数据进行多次增加删除修改时采用LinkedList。

ArrayList和LinkedList的动态扩容

ArrayList

ArrayList 初始化大小是 10,如果你知道你的ArrayList 会达到多少容量,可以在初始化的时候就指定,能节省扩容的性能开支。

扩容的规则是,新增的时候发现容量不够用了,就去扩容。扩容大小的规则是,扩容后的大小= 原始大小+原始大小/2 + 1。(例如:原始大小是 10 ,扩容后的大小就是 10 + 5 + 1 = 16)

LinkedList

linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

HashSet与TreeSet的区别和适用场景

HashSet 是基于哈希表实现的,其中的数据是无序的可以放入null,但只能放入一个null,两者中的值都不重复,就如数据库中的唯一约束,它要求放入的对象必须实现hashCode(),因为放入的对象是以hashCode码作为标识的。

TreeSet 是基于红黑树的树据结构实现的,其中的数据是自动排序好的,不允许放入null。

适用场景

HashSet是基于Hash算法实现的,其性能通常都优于TreeSet,它是为快速查找而设计的Set,这种场景我们通常都应该使用HashSet,只有在我们还需要排序的功能时,我们才使用TreeSet。

HashMap、TreeMap是非线程安全的,HashTable、ConcurrentHashMap是线程安全的。

HashMap与TreeMap、HashTable的区别及适用场景如下:

  • HashMap:非线程安全,基于哈希表(散列表)实现。使用HashMap要求的键类需要明确定义了hashCode()和equals(),可以重写hasCode()和equals(),为了优化HashMap空间的使用,可以调优初始容量和负载因子,其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法,而HashMap实现中采用的是链表法。

  • TreeMap:非线程安全,基于红黑树实现,TreeMap没有调优选项,因为该树总处于平衡状态。

  • HashMap和HashTable:HashMap去掉了HashTable的contain方法,但是加上了containsValue()和containsKey()方法。HashMap允许空键值,而HashTable不允许。

  • HashMap适用于Map中插入、删除和定位元素,Treemap适用于按自然顺序或自定义顺序遍历键。

二、HashMap

1、Map类族图

2、HashMap是什么?哈希冲突如何解决?

HashMap 主要用来存放键值对,它是基于哈希表的Map接口实现的,相比 AVL、红黑树而言,牺牲了顺序性,但换来了更好的性能。

  • 哈希表:根据键的 Hash 值来决定对应值的存储位置,以加快查找的速度,这个映射函数叫哈希函数,存放记录的数组就叫做哈希表。

HashMap 并不是全能的,对于一些特殊场景下的需求官方拓展了一些其他的类来满足,如线程安全的ConcurrentHashMap、记录插入/访问顺序的LinkedHashMap、给key排序的TreeMap等等。

JDK1.8 之前 HashMap 是由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的,也就是使用拉链法(链地址法)解决了冲突,当哈希冲突落在同一个桶中时,直接放在链表头部,在JDK 1.8以后是放到链表尾部。此外,解决Hash冲突的方式还有很多,比如开放定址法和再哈希法:

开放定址法

  • 线性探测法:很简单,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置后面的空位置上。

  • 平方探测法: +1、+4、+9、+16。

  • 这种方法存在着很多缺点,例如,查找、扩容等都很不方便,所以我不建议作为解决哈希冲突的首选。

再哈希法(二次哈希法)

顾名思义就是在产生地址冲突时再使用另外一个 hash 算法来计算出下一个位置要去哪儿,直到冲突不再发生,这种方法不易产生聚集,但却增加了计算时间。如果我们不考虑添加元素的时间成本,且对查询元素的要求极高,就可以考虑使用这种算法设计。

为什么JDK1.8之前,链表元素增加采用的是头插法,1.8之后改成尾插法了。1.8之前采用头插法是基于什么设计思路呢?

JDK1.7是考虑新增数据大多是热点数据,所以考虑放在链表头位置,也就是数组中,这样可以提高查询效率,但这种方式会出现插入数据是逆序的。在JDK1.8中,HashMap链表在节点长度超过8之后会变成红黑树,这样一来在数组后节点长度不断增加时,遍历一次的次数就会少很多,此时相比头插法而言,尾插法操作额外的遍历消耗已经小很多了。

JDK1.8 以后在解决哈希冲突时有了较大的变化,如果链表长度大于阈值8且数组长度大于等于64时,会将链表转化为红黑树将搜索时间由 O(n) 变为 O(logn)。如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。

链表转红黑树的阈值为什么是8?红黑树转链表为什么是6?

红黑树中的TreeNode是链表中Node所占空间的2倍,虽然红黑树的查找效率为O(lgn),要优于链表的O(n),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。所以要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。

之所以是8,是因为Java的源码贡献者在进行大量实验后发现,hash碰撞发生8次的概率已经降低到了千万分之6,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此次操作的hash碰撞可能性非常大了,后序可能还会继续发生hash碰撞。所以这个时候就应该将链表转换为红黑树了,这也就是为什么链表转红黑树的阈值是8的原因。

最后,红黑树转链表的阈值为6,主要是因为如果也将该阈值设置为8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源。

3、HashMap 1.7 原理

其实真正存放数据的是 Entry<K,V>[] table,Entry 是 HashMap 中的一个静态内部类,它有key、value、next、hash(key的hashcode)成员变量。

put 方法

1、首次调用put方法时会创建HashMap的数组。

2、HashMap 通过 key 的 hashCode 经过扰动函数hash()处理过后得到 hash 值,然后通过 (数组长度 - 1) & hash 判断当前元素存放的位置,如果定位到的数组位置没有元素,就直接插入。

扰动函数hash()

防止一些实现比较差的 hashCode() ,减少碰撞。

先将 hashCode 值无符号右移 16 位,也就是取 int 类型的一半,刚好可以将该二进制数对半切开,然后位异或hashCode值,目的就是尽量打乱 hashCode 真正参与运算的低 16 位。

为什么数组长度总是2的n次方?

这样就能通过位运算实现取余,从而让下标能落在数组长度范围内。

而且,在数组扩容的时候也利用到了2的幂次方这个特性。

如果初始化大小设置的不是2的幂次方,hashmap也会调整到比初始化值大且最近的一个2的幂作为capacity。

为什么使用 (数组长度 - 1) & hash 来判断当前元素存放位置?

目的就是为了减少哈希冲突,均匀分布元素。2的幂次方减1后除了最高位每一位都是1,让数组每一个位置都能添加到元素。

例如十进制8,对应二进制1000,减1是0111,这样在&hash值使数组每个位置都是可以添加到元素,如果有一个位置为0,那么无论hash值是多少那一位总是0,例如0101,&hash后第二位总是0,也就是说数组中下标为2的位置总是空的。

3、如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较,如果key相同就直接覆盖,不相同就通过拉链法(头插法)解决冲突。

4、当放入元素超过当前阈值时会进行扩容:直接创建原数组两倍的长度,然后将原有对象二次哈希(rehash)后找到新的下标后重新放入,为什么要进行rehash?因为长度扩大以后,Hash的规则也随之改变。

阈值 = 数组长度 * 负载因子,负载因子表示添加到多少填充比时进行扩容,太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散,而默认值0.75f是官方给出的一个比较好的临界/折中值。

get 方法

1、首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。

2、如果该位置不是链表就根据 key、key 的 hashcode 是否相等来返回值,如果为链表则需要遍历链表直到 key 及 hashcode 相等时才返回值,如果什么都没取到就直接返回 null 。

4、HashMap 1.8 原理

put 方法

1、首次调用put方法时会创建HashMap的数组。

2、对Key求Hash值,然后再通过 (n-1) & hash 计算下标。

3、如果没有碰撞,直接放入桶中,如果碰撞了,以链表的方式链接到后面。

4、如果链表长度超过阈值8,就把链表转成红黑树,如果链表长度低于6,就把红黑树转回链表。

5、如果节点已经存在就替换旧值,如果数组满了,就需要扩容2倍后重排。

HashMap 1.8扩容优化

在 JDK1.7 中,HashMap 整个扩容过程就是分别取出数组元素,一般该元素是最后一个放入链表中的元素,然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标,然后进行交换。这样的扩容方式会将原来哈希冲突的单向链表尾部变成扩容后单向链表的头部,从而形成环形链表。

而在 JDK 1.8 中,HashMap 对扩容操作做了优化。由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化,在扩容中只用判断原来的 hash 值和原数组长度做与运算,0 的话索引不变,1 的话索引变成原索引加上原数组长度。

之所以能通过这种与运算来重新分配索引,是因为 hash 值本来就是随机的,而 hash 按位与上 newTable 得到的 0(扩容前的索引位置)和 1(扩容前索引位置加上扩容前数组长度的数值索引处)就是随机的,所以扩容的过程就能把之前哈希冲突的元素再随机分布到不同的索引中去。

get 方法

1、首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。

2、如果桶为空则直接返回 null ,否则判断桶的第一个位置的 key 是否为查询的 key,是就直接返回 value。

3、如果第一个不匹配,则判断它的下一个是红黑树还是链表,红黑树就按照树的查找方式返回值,不然就按照链表的方式遍历匹配返回值。

5、HashMap的时间复杂度

hashmap的最优时间复杂度是O(1),而最坏时间复杂度是O(n)。

在没有产生hash冲突的情况下,查询和插入的时间复杂度是O(1)。

而产生hash冲突的情况下,如果是最终插入到链表,链表的插入时间复杂度为O(1),而查询时间复杂度为O(n),如果最终插入的是红黑树,插入和查询的平均时间复杂度是O(lgn)。

6、HashMap优化

1、在预知存储数据量的情况下,提前设置初始容量(初始容量 = 预知数据量 / 加载因子)。这样做的好处是可以减少扩容操作,提高 HashMap 的效率。

2、重写 key 值的 hashCode() 方法,降低哈希冲突,从而减少链表的产生,高效利用哈希表,达到提高性能的效果。

3、结合自己的场景来设置加载因子参数:当查询操作较为频繁时,我们可以适当地减少加载因子,如果对内存利用率要求比较高,可以适当的增加加载因子。

7、问题 => ConcurrentHashMap

1.7 的HashMap在并发场景下使用时容易出现死循环:具体是在扩容的时候会调用 resize(),就是这里的并发操作容易在一个桶上形成环形链表,这样当获取一个不存在的 key 时,计算出的下标正好是环形链表的下标就会出现死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系,将原来哈希冲突的单向链表尾部变成扩容后单向链表的头部,从而形成环形链表。环形列表例子:A->B->C,resize之后,有可能B->A,但是A已经指向了B,所以构成环形链表。

1.8 优化,扩容时候链表尾节点插入,避免环链形成。

8、为什么插入HashMap的数据需要实现hashcode和equals方法?对这两个方法有什么要求?

通过hashcode来确定插入的下标,通过equals比较来寻找数据,两个相等的key的hashcode必须相等,但拥有相同hashcode的对象不一定相等。

如果两个相同对象的hashcode不同,那么会造成HashMap中存在相同的key,所以equals返回相同的key他们的hashcode一定要相同。

三、ArrayMap、SparseArray

HashMap在存储数据时将要不断的扩容,且不断地做hash运算,这将对我们的内存空间造成很大消耗和浪费。建议在特点场景下使用ArrayMap、SparseArray。

1、ArrayMap

基于两个数组实现,一个存放 hash,一个存放键值对,例如第一个位置放key,第二个位置放value,依次类推。

存放 hash 的数组是有序的,查找时使用二分法查找。当插入时,根据key的hashcode()得到hash值,计算出在键值对数组中的下标位置,然后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在index的相邻位置插入,也就是使用了开放地址法中的线性探测法。

不适合存大量数据(1000以下),因为数据量大的时候二分查找相比红黑树会慢很多。

扩容时不像 HashMap 直接 double,内存利用率高;也不需要重建哈希表,只需要调用 system.arraycopy 进行数组拷贝,性能较高。

ArrayMap中的小数组复用池:通常来说,在 Android 里面用到的 map 都很小,所以它就把 4 和 8 这样大小的数组缓存起来,以备使用,这样就能够减小 GC。

2、SparseArray

SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式,从而节约内存空间。

同时,SparseArray在存储和读取数据时候,使用的是二分查找。也就是在put添加数据的时候,会使用二分查找和之前的key比较当前我们添加的元素的key的大小,然后按照从小到大的顺序排列好,所以,SparseArray存储的元素都是按元素的key值从小到大排列好的。 而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap快的多。

3、使用场景

如果数据量在千级以内且增删不频繁的情况下:

  • 1、如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用。

  • 2、如果key类型为其它的类型,则使用ArrayMap。

如果数据量在千级以上或增删频繁的情况下,直接采用HashMap。

如果有新的开发同学用错了HashMap,可以自定义一个HashMap继承系统的HashMap,重写put方法,在调用父类的put方法前获取当前HashMap中的元素个数,如果小于1000则弹出警报,最后再结合Gradle编译插件将项目中的HashMap替换为我们自定义的HashMap即可。

Contact Me

现如今,Android 行业人才已逐渐饱和化,但高级人才依旧很稀缺,我们经常遇到的情况是,100份简历里只有2、3个比较合适的候选人,大部分的人都是疲于业务,没有花时间来好好学习,或是完全不知道学什么来提高自己的技术。对于 Android 开发者来说,尽早建立起一个完整的 Android 知识框架,了解目前大厂高频出现的常考知识点,掌握面试技巧,是一件非常需要重视的事情。

去年,为了进入一线大厂去做更有挑战的事情,拿到更高的薪资,我提前准备了半年的时间,沉淀了一份 「两年磨一剑」 的体系化精品面试题,而后的半年,我都在不断地进行面试,总共面试了二三十家公司,每一场面试完之后,我都将对应的面试题和详细的答案进行了系统化的总结,并更新到了我的面试项目里,现在,在每一个模块之下,我都已经精心整理出了 超高频和高频的常考 知识点。

在我近一年的大厂实战面试复盘中逐渐对原本的内容进行了大幅度的优化,并且新增了很多新的内容。它可以说是一线互联网大厂的面试精华总结,同时后续还会包含如何写简历和面试技巧的内容,能够帮你省时省力地准备面试,大大降低找到一个好工作的难度。

这份面试项目不同于我 Github 上的 Awesome-Android-Interview 面试项目:https://github.com/JsonChao/Awesome-Android-Interview,Awesome-Android-Interview 已经在 2 年前(2020年 10 月停止更新),内容稍显陈旧,里面也有不少点表述不严谨,总体含金量较低。而我今天要分享的这份面试题库,是我在这两年持续总结、细化、沉淀出来的体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高。

在分享之前,有一点要注意的是,一定不要将资料泄露出去!细想一下就明白了:

1、如果暴露出去,拿到手的人比你更快掌握,更早进入大厂,拿到高薪,你进大厂的机会就会变小,毕竟现在好公司就那么多,一个萝卜一个坑。

2、两年前我公开分享的简陋版 Awesome-Android-Interview 面试题库现在还在被各个培训机构当做引流资料,加大了现在 Android 内卷。。

所以,这一点一定要切记。

现在,我已经在我的成长社群里修订好了 《体系化高频核心 Android 面试题库》 中的 ”计算机基础高频核心面试题“ 和 ”Java 和 kotlin 高频核心面试题“ 部分,后续还会为你带来我核心题库中的:

  • “Android基础 高频核心面试题”

  • “基础架构 高频核心面试题”

  • “跨平台 高频核心面试题”

  • “性能优化 高频核心面试题”

  • ”Framework 高频核心面试题“

  • ”NDK 高频核心面试题“

获取方法:扫描下方的二维码。

出身普通的人,如何真正改变命运?

这是我过去五、六年一直研究的命题。首先,是为自己研究,因为我是从小城镇出来的,通过持续不断地逆袭立足深圳。越是出身普通的人,就越需要有耐心,去进行系统性地全面提升,这方面,我有非常丰富的实践经验和方法论。因此,我开启了 “JsonChao” 的成长社群,希望和你一起完成系统性地蜕变。

星球目前有哪些服务?

  • 每周会提供一份让 个人增值,避免踩坑 的硬干货

  • 每日以文字或语音的形式分享我个人学习和实践中的 思考精华或复盘记录

  • 提供 每月 三 次成长、技术或面试指导的咨询服务。

  • 更多服务正在研发中...

超哥的知识星球适合谁?

  • 如果你希望持续提升自己,获得更高的薪资或是想加入大厂,那么超哥的知识星球会对你有很大的帮助。

  • 如果你既努力,又焦虑,特别适合加入超哥的知识星球,因为我经历过同样的阶段,而且最后找到了走出焦虑,靠近梦想的地方。

  • 如果你希望改变自己的生活状态,欢迎加入超哥的知识星球,和我一起每日迭代,持续精进。

星球如何定价?

365元每年

每天一元,给自己的成长持续加油💪

为了回馈 JsonChao 的 CSDN 忠实用户,我申请了少量优惠券,先到者先得,错过再无

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值