面试整整一年!今天把我的秋招+春招面试总结都写给大家,希望能帮助你们顺利的面试!

360 篇文章 2 订阅
180 篇文章 2 订阅
本文是一位经历了一年面试的程序员,分享他在秋招和春招中的面试经历和总结。他详细梳理了Java相关知识,包括ArrayList、LinkedList、HashMap的原理与操作,以及面试中遇到的热点问题。他还讨论了面试技巧和经验,旨在帮助准备面试的求职者。
摘要由CSDN通过智能技术生成

写在前头

有一肚子的话想说出来,到现在又不知该如何表达了,如果屏幕能传递感情,就好了

我也经历了秋招和春招,把积累的一些心得和知识分享出来,趁着春招还没结束,应该还能给大家一些帮助,在牛课上潜水索取了这么久,也是时候回馈了,这篇帖子开始写于河工大图书馆,也用来纪念为期不多的大学时光

下面的一些经验不一定全对也不一定全部有用,也仅仅是把我知道的一些技巧性的东西分享出来罢了,如果能对大家产生一点点帮助,都是我无比荣幸的事情

春招历程(截至2021年3月31日)

  • 字节跳动 大力教育后端开发 北京:2月17日牛客内推投递
  • 2月23日一面 被面试官发现非科班,基本上问的全是计算机网络和操作系统相关 挂
  • 京东物流 Java开发工程师 北京:2月17日官网内推码投递
  • 3月3日 一面 一个多小时,口干舌燥也畅快琳琳 过
  • 3月4日 二面 四十多分钟,问的比较发散,不局限于八股文,更关注具体场景业务分析 过
  • 3月10日 HR面 大概只有六七分钟
  • 3月12日 查询状态变为HR面完成,Offer灯亮起
  • 3月15日 收到正式Offer
  • 美团 支付部门后端开发 北京:3月4日牛客内推投递
  • 3月13日 笔试,2AC,剩下3道题每题只骗了18%
  • 3月19日 一面,体验非常好的一次面试,面试官很和气,过
  • 3月22日 二面,时间蛮长的,一个多小时,过
  • 3月31日 三面(HR面),十七分钟,薛定谔的美团面试结果
  • 笔试过没回应的:携程,VIVO,跟谁学
  • 投了没有回应的:华为,OPPO,陌陌,作业帮,小米,搜狗
  • 给了笔试没笔的:顺丰科技,猿辅导,好未来,滴滴,便利蜂

整理出来的面经

Java相关

ArrayList

使用场景:ArrayList的底层是一个数组,适合快速匹配,不适合频繁的增删

允许add null 值,会自动扩容,其中size(),isEmpty(),get(),add()方法的复杂度为O(1)

使用Collentions.synchronizedList(),实现线程安全或者Vector也可(Vector在方法上加的synchronized锁)

调用无参构造函数的时候,在JDK1.8默认为空数组(DEFAULT_EMPTY_ELEMENTDATA = {}),数字大小为10是我们第一次调用add方法是进行扩容的数组大小
若我们在执行构造函数传入的数组大小为0时,它使用的不是DEFAULT_EMPTY_ELEMENTDATA,而是另一个空数组EMPTY_ELEMENTDATA = {}(这个知识点面试没说过)

add方法的过程

先确定数组大小是否足够,如果我们创建ArrayList的时候指定了大小,那么则以给定的大小创建一个数组,否则默认大小为10;容量够大的情况,直接赋值;如果容量不够大,则进行扩容方法grow(),扩容的大小为原来大小的1.5倍(newCapicity = oldCapicity + oldCapicity >> 1,其中>>1,右移一位除以2),如果扩容后的大小还不够的话,则会将数组大小直接设置为我们需要的大小,扩容的最大值为Integer.MAX_VALUE,之后会调用Arrays.copyOf()方法将原数组中的数组复制过来
其中Arrays.copyOf()底层调用的是System.arrayCopy(),大家可以去简单了解下

remove方法

该方将被删除位置后的元素向前复制,底层调用的也是System.arrayCopy()方法,复制完成后,将数组元素的最后一个设置为null(因为向前复制一个位置,所以最后位置的元素是重复的),这样就解决了复制重复元素的问题

迭代器和增强for是一样的(这是一个Java语法糖,我后边还会再写语法糖相关的),过程中会判断modCount的值是否符合循环过程中的期望,如果不符合的话则会抛出并发修改异常,比较常见的情况就是在增强for中进行删除操作

LinkedList

使用场景:适合增删,不适合快速匹配

底层数据结构是双向链表,每一个节点为Node,有pre和next属性

提供从头添加和从尾添加的方法,节点删除也提供了从头删除和从尾删除的方法

HashMap

底层数据结构:数组 + 链表 + 红黑树

允许put null 值,HashMap在调用hash算法时,如果key为null,那么hash值为0,这一点区别于HashTable和ConcurrentHashmap
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

loadFactor:负载因子默认为0.75,是均衡了时间和空间损耗计算出来的,较高的值会减少空间的开销,扩容减小,数组大小增加速度变慢,但是增加了查找的成本,hash冲突增加,链表变长

如果有很多需要储存到HashMap中的数据,要在一开始把它的容量设置为足够大,防止出现不断扩容

通过Collections.synchronizedMap()来实现线程安全或者使用ConcurrentHashmap

put过程

首先会判断数组有没有进行初始化,没有的话,先执行初始化操作,resize()方法
(n - 1) & hash用来定位到数组中具体的位置,如果数组中的该位置为空,直接在该位置添加值
如果数组当前位置有值的话,如果是链表,采用的是尾插发,并且当链表长度大于等于8时,会进行树化操作;如果是红黑树的话,则会调用红黑树的插入值的方法;添加完成后,会判断size是否大于threshold,是否需要扩容,若扩容的话,数组大小为之前的2倍大小,扩容完成后,将原数组上的节点移动到新数组上。
一篇我觉得写得不错的博客儿:HashMap扩容时的rehash方法中(e.hash & oldCap) == 0算法推导

为什么树化操作的阈值是8?

链表的查询时间复杂度为O(n),红黑树的查询时间复杂度为O(logn),在数据量不多的时候,使用链表比较快,只有当数据量比较大的时候,才会转化为红黑树,但是红黑树占用的空间大小是链表的2倍,考虑到时间和空间上的损耗,所以要设置边界值(其实链表长度为8的概率很低,在HashMap注释中写了,出现的概率不择千万分之一,红黑树只是为了在极端情况下来保证性能)

为什么还要有一个阈值是6?(去年面试快手的时候问过)

避免频繁的进行树退化为链表的操作,因为退化也是有开销的,当我们移除一个红黑树上的值的时候,如果只有阈值8的话,那么它会直接退化,我们若再添加一个值,它有可能又需要变为红黑树了,添加阈值6相当于添加了一个缓冲

hash算法
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),右移16位的操作使得hash值更加分散

为什么数组大小始终为2的n次幂?

因为在确定某个值在数组位置的下标时,采用的是(数组大小 - 1)位与上hash值,而数组大小减一之后,用2进制表示最后几位都是1,这样每位在位与运算之后,不是0就是1,如果我们hash值是均匀分布的话,那么我们得到的数组下表也是均匀分布的,而如果我们的数组容量不是2的n次幂,那么就没有这个特性了

数组大小为什么默认是16?

16是一个经验值,2,4,8有些小,会频繁的扩容,32有些大,这样就多占用了空间

为什么JDK1.8采用了尾插法?

JDK1.7时采用的是头插法,它在扩容后rehash,会使得链表的顺序颠倒,引用关系发生了改变,那么在多线程的情况下,会出现链表成环而死循环的问题,而尾插法就不会有这样的问题,rehash后链表顺序不变,引用关系也不会发生改变,也就不会发生链表成环的问题

红黑树的5个特点

根节点是黑色;
所有叶子节点是黑色;
其他节点是红色或黑色;
从每个叶子节点到根节点所有路径上不能有两个连续的红色节点;
从任一节点到每个叶子节点的所有简单路径上包含相同数量的黑色节点

HashMap和Hashtable的区别

实现方式不同:Hashtable:继承了Dictionary类,而HashMap继承的是AbstractMap类
初始容量不同:HashMap的初始容量为16,Hashtable为11,负载因子都是0.75
扩容机制不同:HashMap是翻2倍,Hashtable是翻两倍+1

HashSet、TreeMap、TreeSet、LinkedHashMap、LinkedHashSet

HashSet底层基于HashMap实现,若想实现线程安全,需要使用Collections.synchronizedSet();
它在底层组合的HashMap,并没有继承关系,其中Value值使用的都是被声明为Object的PRESENT对象
private static final Object PRESENT = new Object();
TreeMap的底层数据结构是红黑树,会对key进行排序,维护key的大小关系
我们可以传入比较器Comparator或者让作为key对象的类实现Comparable接口重写compareTo方法
禁止添加null值
LinkedHashMap 本身继承了HashMap,拥有HashMap的所有特性,在此基础上添加了两个新的特性:
能按照插入的顺序进行访问(不过它仅仅提供了单向访问,即按照插入的顺序从头到尾访问);
能实现访问最少最先删除的功能(LRU算法)
LinkedHashSet 底层基于LinkedHashMap实现
3.1.5 ConcurrentHashMap(JDK1.8)
底层基于CAS + synchronized实现,所有操作都是线程安全的,允许多个线程同时进行put、remove等操作
底层数据结构:数组、链表和红黑树的基础上还添加了一个转移节点,在扩容时应用
table数组被volatile修饰
其中有一个比较重要的字段,sizeCtl
= -1 时代表table正在初始化
table未初始化时,代表需要初始化的大小
table初始化完成,表示table的容量,默认为0.75table大小

put过程

key和value都是不能为空的,否则会产生空指针异常,之后会进入自旋(for循环自旋),如果当前数组为空,那么进行初始化操作,初始化完成后,计算出数组的位置,如果该位置没有值,采用CAS操作进行添加;如果当前位置是转移节点,那么会调用helptransfer方法协助扩容;如果当前位置有值,那么用synchronized加锁,锁住该位置,如果是链表的话,采用的是尾插发,如果是红黑树,则采用红黑树新增的方法,新增完成后需要判断是否需要扩容,大于sizeCtl的话,那么执行扩容操作

初始化过程

在进行初始化操作的时候,会将sizeCtl利用CAS操作设置为-1,CAS成功之后,还会判断数组是否完成初始化,有一个双重检测的过程
过程:进入自旋,如果sizeCtl < 0, 线程礼让(Thread.yield())等待初始化;否则CAS操作将sizeCtl设置为-1,再次检测是否完成了初始化,若没有则执行初始化操作
在JDK1.7采用的是Segment分段锁,默认并发度为16

CopyOnWriteArrayList

线程安全的,通过锁 + 数组拷贝 + volatile 保证线程安全(底层数组被volatile修饰)
每次进行数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作之后再赋值回去
对数组的操作,一般分为四步
1 加锁
2 从原数组中拷贝出新数组
3 在新数组上进行操作,并把新数组赋值给原引用

解锁

已经加锁了,为什么还需要拷贝新数组?
因为在原数组上进行修改,没有办法触发volatile的可见性,需要修改内存地址,即将新拷贝的数组赋值给原引用
在进行写操作的时候,是能读的,但是读的数据是老数组的,能保证数组最终的一致性,不能保证实时一致性;
存在内存占用问题,写时复制比较影响性能

基本类型包装类

自动装箱与拆箱是Java语法糖,发生在编译期(深入理解JVM中的前端编译优化)
Character的缓存为0-127;Byte、Short、Integer、Long的缓存为 -128-127,若使用的值是这个范围的值,则直接在缓存中取
float和double在计算中发生精度损失的问题
十进制数能转化为二进制数;而小数有时候不能用二进制数进行表示,会造成精度丢失
解决办法:使用BigDecimal,传入构造函数的参数是String

hashCode()和equals()方法

hashCode是Object类中一个被native修饰的方法,通常是将对象的内存地址转换为整数后返回
为什么重写hashCode必须重写equals?
两个对象相等,hashCode一定相等;而hashCode相等,两个对象不一定相等,需要用equals进一步比较

封装、继承和多态

你是如何理解面向对象的三个特征的?(京东一面问过)
面向对象的特性是封装、继承和多态,封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化,这样做使得代码的复用性更高;继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类–有父类的行为和属性,也有自己特有的行为和属性,这样做扩展了已存在的代码块,进一步提高了代码的复用性;多态是为了实现接口重用,多态的一大作用就是为了解耦,允许父类引用(或接口)指向子类(或实现类)对象。多态的表现形式有重写和重载
说说重写和重载
重写发生在父类与子类之间,方法名相同,参数列表相同,返回值可以“变小”,抛出的异常可以“变小”,访问修饰符权限不能变小,发生在运行期
重载实在一个类中,方法名相同,参数列表不同(参数顺序不同也行),返回值和访问修饰符可以不同,发生在编译期

反射

对于任何一个类,都能获取它的方法和属性,动态获取信息和动态调用方法的功能是反射

Java语法糖(《深入理解JVM 第三版》第10章 前端编译优化)

泛型 Java选择的是“类型擦除似泛型”,在.java源代码经过编译成.class文件后,泛型相关的信息就消失了,泛型是在编译器层面来保证的
泛型上界 <? extends T>, 编译器指导里边存的是T的子类,但是不知道是什么具体的类型,只能取,不能往里放
泛型下界 <? super T>, 能往里放,也能往外拿,但是拿出来的全是Object类型,这就使得元素类型失效了

自动装箱和拆箱

增强for循环,编译后会变为使用迭代器的形式
条件编译,在if条件中,若条件为布尔常量,编译器会把分支中不成立的代码消除掉
字符串拼接,编译时会自动创建StringBuilder对象执行append方法拼接
枚举,编译器会自动创建一个被final修饰的枚举类继承了Enum,所以自定义枚举类型是无法被继承的
还有其他的语法糖,lambda表达式等等,大家感兴趣可以再去了解…

最后

在文章的最后作者为大家整理了很多资料!包括java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书等等!

全部免费分享给大家,有需要的朋友戳这里直接下载就好了,验证码:csdn
在这里插入图片描述
在这里插入图片描述

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值