面经
- Java基础
-
- 集合都有哪些
- 面向对象的三大特点
- ArrayList和LinkedList的区别?
- ArrayList底层扩容是怎么实现的?
- 讲一讲HashMap、以及put方法的过程
- 讲一讲HashMap的扩容过程
- Hashmap为什么要用红黑树而不用其他的树?
- Java8新特性有哪些
- LoadFactor负载因子参数怎么调?什么时候调?
- Object的hashCode和equals为什么要重写,假如没有重写hashCode会有什么问题?
- 阻塞队列怎么实现,用哪些变量实现,入队出队函数是什么样的
- ConcurrentHashMap 加锁流程
- ConcurrentHashMap 为什么 key 和 value 不能为 null?
- JVM
- JUC
- Mysql
- Redis
- Spring
- Mybatis
- Linux
- 计算机网络
- 操作系统
- 其他
- 设计模式
- 项目
- 算法
Java基础
集合都有哪些
答:
Java集合主要分为两类,一类是实现了Collection
接口的,另一类是实现Map
接口的。
Collection
集合可以分为:
Set
:存储的元素无序,不能重复- HashSet:底层实现是基于HashMap,只使用了HashMap的Key。
- LinkedHashSet:底层实现是基于HashMap,并且将各个节点通过双向链表连接。
- TreeSet:底层实现是基于红黑树,并且可以自定义排序规则,不允许存储NULL元素。
List
:存储的元素有序,可以重复- ArrayList:底层实现是基于数组,内存空间连续,可以实现随机访问。
- LinkedList :底层实现是基于双向链表。
Queue
:存储的元素先进先出,可以重复- PriorityQueue:底层实现是基于堆,默认是小顶堆,不允许存储NULL元素
Deque
:双端队列- ArrayDeque:底层实现是基于数组,使用两个指针指向首尾,不允许存储NULL元素
- LinkedList:同上
BlockingQueue
:阻塞队列- ArrayBlockingQueue:底层实现是基于数组的有界阻塞队列
- LinkedBlockingQueue:底层实现是基于单向链表的可选有界阻塞队列
- PriorityBlockingQueue:底层实现是基于堆的有界阻塞队列,支持按元素优先级排序
- SynchronousQueue:同步队列,每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。不存储元素。
- DelayQueue:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。
Map
集合可以分为:
- HashMap:不是线程安全,key 和 value 可以存储 null 值
JDK 8之前
,底层实现是基于数组 + 链表JDK 8之后
,当链表长度大于阈值(默认8),则将链表转化为红黑树。
- Hashtable:线程安全,key 和 value 不可以存储 null 值
- 底层实现是基于数组 + 链表
- TreeMap:底层实现是基于红黑树,并且可以自定义排序规则。
面向对象的三大特点
答:
- 封装: 把属性和方法封装到对象的内部,对外只暴露获取和修改的方法。
- 继承: 类之间的一种扩展关系,子类拥有父类对象的属性和方法(包括私有属性和私有方法,但无法访问)。可以重写父类的方法。
- 多态: 具体表现为父类的引用指向子类的实例。(
编译看左边,运行看右边
)
ArrayList和LinkedList的区别?
答:
ArrayList:
- 基于动态数组实现的,默认初始容量为
10
,当元素数量超过当前容量时会进行自动扩容。因为是基于数组,所以可以实现随机访问。
LinkedList:
- 基于双向链表实现的,每个节点不仅要存储数据,还要存储指向前一个节点和后一个节点的指针。不支持随机访问,插入和删除效率高。
都不是线程安全的。
ArrayList底层扩容是怎么实现的?
答:
- ArrayList在添加一个元素之前,首先会检查数组容量是否足够,如果元素数量超过了数组容量的大小,则会进行扩容。
- 扩容:调用
grow(int)
方法进行扩容。扩容的大小为原数组容量的1.5
倍, - 复制:扩容后,会调用
Arrays.copyOf(elementData, newCapacity)
方法,创建一个容量为扩容后大小的数组,并把原数组的元素复制进去。 - 添加:再将一开始要添加的元素加入到数组中。
讲一讲HashMap、以及put方法的过程
答:
- HashMap是一个用于存储键值对类型数据的集合,
JDK1.8之前
,使用的是数组 + 链表实现的。 - HashMap存储元素的过程(put方法过程):
- 首先会对key进行
hash
,得到要存储在数组中的哪个位置(因为hash后得到的值非常大,超过了数组的范围,所以hash值要对数组长度求余
),知道要存储的位置后,首先判断该位置是否有元素存在,如果没有元素存在,则在该位置创建一个node节点
,并将key,value保存到node节点内部
。 - 如果该位置已经存在元素了,意味着产生了
哈希冲突
。然后遍历由node节点组成的链表,判断其中有没有节点中key的hash和值是否与插入的key相等的
,如果有,则将其value进行覆盖。如果遍历到链表末尾都没有,则在尾部插入一个新的节点。 - 插入新节点后,同时会判断
链表的长度是否大于了阈值(8)
,如果大于了则将链表转化为红黑树
,提高查询效率。
讲一讲HashMap的扩容过程
答:
- 当数组中元素个数大于
数组容量
乘以负载因子
(0.75
)的值时,会对数组容量进行扩容。 - 新创建一个数组,大小为原数组的
2倍
。将原数组中所有的元素重新哈希,并放到新数组的对应位置。
Hashmap为什么要用红黑树而不用其他的树?
答:
红黑树: 红黑树是一种平衡二叉树,其插入、删除、查找的时间复杂度都为O(logn)
相比于其他树:
- 普通二叉树: 避免了二叉树在最坏情况下O(n)的时间复杂度。
- 其他平衡二叉树: 其他平衡二叉树是比红黑树更严格的平衡树,为了保持平衡,需要旋转的次数更多。
- b树/b+树: 用B/B+树的话,在数据量不是很多的情况下,数据都会挤在一个结点里面,这时候就退化成了链表。B和B+树主要用于数据存储在磁盘上的场景
Java8新特性有哪些
答:
- 支持
Lambda
表达式- 就是对匿名实现类的表现形式进行简写
- 支持方法引用
- 新增函数式接口
- 在一个接口中,只声明了
一个抽象方法
,则此接口就称为函数式接口
- 在一个接口中,只声明了
- 新增了
Stream API
LoadFactor负载因子参数怎么调?什么时候调?
答:
- 负载因子:用于衡量HashMap内部存储空间的充满程度。比如说0.4,那么表示当容器
填满40%
的时候,HashMap就会进行扩容,扩充为原来的2倍大小。 - 负载因子越小:冲突的几率就越低,但是会消耗更多的空间。
- 负载因子越大:冲突的几率就越大,但是会更节省空间。
- 如果内存资源充足,希望提高查询效率: 负载因子就可以调低一点。
- 如果内存资源紧张,查询效率不那么重要: 负载因子就可以调高一点。
- 默认负载因子:
0.75
Object的hashCode和equals为什么要重写,假如没有重写hashCode会有什么问题?
答:
重写equals: 是为了判断当两个对象的属性值相同时,才认为是相同的对象。
重写hashCode: 为了根据对象的属性值来生成哈希码,与equals保持一致。
没有重写hashCode: 当向HashSet集合添加元素时,两个对象即使属性值一样,但也会添加进去。
阻塞队列怎么实现,用哪些变量实现,入队出队函数是什么样的
- 阻塞队列(Blocking Queue)是一种线程安全的数据结构,它在队列为空时阻塞出队操作,在队列满时阻塞入队操作。
使用变量
- 队列(queue):存储数据的容器。
- 最大容量(maxSize):队列的最大容量。
- 锁对象(lock):用于确保线程安全的对象锁。
入队函数
- 获取锁对象。
- 使用while循环检查队列是否已满,如果已满,调用lock.wait()使线程等待。
- 将元素添加到队列末尾,更新队列状态。
- 调用lock.notifyAll()通知所有等待的线程队列状态已改变。
- 释放锁对象。
出队函数
- 获取锁对象。
- 使用while循环检查队列是否为空,如果为空,调用lock.wait()使线程等待。
- 从队列头部移除一个元素,更新队列状态。
- 调用lock.notifyAll()通知所有等待的线程队列状态已改变。
- 释放锁对象。
import java.util.LinkedList;
import java.util.Queue;
public class BlockingQueue<T> {
private Queue<T> queue = new LinkedList<>();
private int maxSize;
private final Object lock = new Object();
public BlockingQueue(int maxSize) {
this.maxSize = maxSize;
}
public void enqueue(T item) throws InterruptedException {
synchronized (lock) {
while (queue.size() == maxSize) {
lock.wait();
}
queue.add(item);
lock.notifyAll();
}
}
public T dequeue() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
T item = queue.poll();
lock.notifyAll();
return item;
}
}
}
ConcurrentHashMap 加锁流程
- put() 操作时,先对 key 和 value 是否为
null
进行判断,如果是的话则抛出 NullPointerException。 - 通过对 key 哈希并对数组长度取余,获得数组对应的位置。
- 如果要添加的位置还没有其他 Node 节点,使用 Unsafe 的
CAS
操作进行添加。 - 如果要添加的位置已经有其他 Node 节点,则对 Node 节点添加
synchronized
锁。 - 遍历由 Node 节点组成的链表或是红黑树,如果存在 Node 节点key的hash与待插入的key的hash一致,并且equals方法也一致,则会对该位置的 Node 节点进行修改。如果遍历到数组尾部还没有,则新创建一个 Node 节点。
ConcurrentHashMap 为什么 key 和 value 不能为 null?
- 为了
避免歧义
:在一个高并发的环境中,假设一个线程读取键值对,而另一个线程恰好删除了这个键值对,这时候第一个线程获取的值有可能是null,如果是允许存储null的话,就不知道原本这个值就是null,还是其他线程修改的null了。
JVM
在java怎么确保一个类不被重复加载
答:
- 依靠双亲委派机制
- 当一个类加载器要加载字节码文件时,首先向上查找父类加载器是否加载过,
- 如果加载过,则直接返回。
- 如果一直到顶级类加载器(
Bootstrap
)也没有加载过,则再从上至下尝试加载。 - 好处:避免类的重复加载、保证JDK的核心类库不会被替换。
有哪些类加载器
答:
- 启动类加载器(Bootstrap):默认加载
Java安装目录/jre/lib
下的类文件,比如rt.jar,tools.jar,resources.jar等。 - 扩展类加载器:默认加载
Java安装目录/jre/lib/ext
下的类文件 - 应用程序类加载器:默认加载为
应用程序classpath
下的类文件。 - 自定义类加载器:继承
ClassLoader
抽象类,重写findClass
方法。在findClass方法中,定义从哪里读取字节码文件,然后调用defineClass
方法,在方法区和堆区创建对象。
Class对象能够被GC吗
答:
满足以下3个条件,就可以被回收
- 此类所有实例对象以及子类对象都已经被回收
- 加载该类的类加载器已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用。
方法区在JDK8是怎么实现的
答:
JDK7
及之前的版本将方法区存放在堆区域中的永久代空间。JDK8
及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中。会发生内存溢出。
线上项目发生内存溢出原因,如何定位,怎么解决
答:
内存溢出原因:
- 一次性申请的对象太多。例如:一次性将数据库的千万级数据查询出来放到内存中。解决方法:分页查询
- 内存资源耗尽未释放。例如:高并发场景下,不断的使用资源信息例如
jdbc connection
没有释放。解决方法:池化技术 - 本身资源不够。例如:分配的堆内存太小,不足以支撑业务。解决方法:使用
jmap -heap
查看堆信息。
如何定位:
1、程序已经OOM挂了
- 提前设置了JVM参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=保存路径
(如果没有设置,提桶跑路吧)
2、系统运行中还未OOM
- 使用命令
jmap -dump:format=b,file=demo.hprof java进程号
在线导出dump文件。(缺点:会进行一次Full GC
)
如何解决:
- 使用
jvisualvm
工具(JVM诊断工具)导入保存的dump文件,查看跟业务有关的对象,找到根对象,查看根对象的线程栈。定位到内存泄漏点。进行代码修复或者JVM参数调优。
什么是堆存储文件
答:
- 堆转储