目录
[2== 与 equals 的区别?](#2== 与 equals 的区别?)
[3String,StringBuilder 和 StringBuffer 的区别?](#3String,StringBuilder 和 StringBuffer 的区别?)
[4 说说 Java 中的异常?](#4 说说 Java 中的异常?)
[6说说 java 中常见的集合类?](#6说说 java 中常见的集合类?)
[7HashMap 原理(数据结构)?](#7HashMap 原理(数据结构)?)
[8HashMap 原理(扩容)?](#8HashMap 原理(扩容)?)
[9HashMap 原理(方法执行流程)?](#9HashMap 原理(方法执行流程)?)
[10说说 BIO、NIO、AIO?](#10说说 BIO、NIO、AIO?)
[11 IO流?](#11 IO流?)
[12ThreadLocal 的原理?](#12ThreadLocal 的原理?)
[14synchronized 原理?](#14synchronized 原理?)
[15【追问】 synchronized 锁升级?](#15【追问】 synchronized 锁升级?)
[16对比 synchronized 和 volatile?](#16对比 synchronized 和 volatile?)
[17对比 synchronized 和 Lock?](#17对比 synchronized 和 Lock?)
[19JVM 堆内存结构?](#19JVM 堆内存结构?)
1重载与重写的区别?
重载是对象的方法之间,它们方法名相同,但方法的参数列表不同
重写是父子类(包括接口与实现类)中两个同名方法,它们方法名相同,且方法的参数列表相同
重载是在编译阶段,由编译器根据传递给方法的参数列表来区分方法
重写是在运行阶段,由虚拟机的解释器来获取引用对象的实际类型,根据类型判断是哪个方法
语法细节,问了再说,不问不必说
重写时,子类方法的访问修饰符要 >= 父类方法的访问修饰符
重写时,子类方法抛出的检查异常类型要 <= 父类方法抛出的检查异常类型,或子类不抛异常
重写时,父子类的方法的返回值类型要一样,或子类方法返回值是父类方法返回值的子类
2== 与 equals 的区别?
对于基本数据类型来说,==判断两边值是否相同
对于引用数据类型来说,==判断两边的地址是否相同,即是否引用同一个对象
equals要看实现
对与Objects.equals();来说,判断是否是引用同一个对象
比如 String,它的内部实现就是去比较两个字符串中每个字符是否相同,比较的是内容
比如 ArrayList,它的内部实现就是去比较两个集合中每个元素是否 equals,比较的也是内容
3String,StringBuilder 和 StringBuffer 的区别?
他们都表示字符串对象
String表示的字符串对象是不可变的,后两者表示字符串的内容是可变的
String和StringBuffer线程是安全的,StringBuilder 不是线程安全的
适用场景:
大部分场景下,都是使用String
多线程下,建议使用String和StringBuffer
单线程的话可以使用StringBuilder
在需要大量字符串拼接的场景下,StringBuilder 和 StringBuffer比较合适
4 说说 Java 中的异常?
Throwable是所有异常的顶层父类
它有Error和Exception两个子类,其中Error表示不可恢复的异常,即使捕获也无法使程序正常运行
Exception表示可恢复异常,处理方式有两种:
1是自己处理,使用catch进行操作
2是使用throw将异常抛给上一层调用者
Exception还有一个子类RuntimeException,二者不同之处在于
Exception是检查异常,必须对异常进行处理
RuntimeException是非检查异常,在语法层面对这类异常并不要求强制处理
5你知道的数据结构有哪些?
数据结构分为线性结构和非线性结构
线性结构中有
动态数组:相较于普通数组可以自动扩容
java中的ArrayList属于动态数组
它的特点是元素是连续存储的
链表由多个节点连接在一起
java中的LinkedList属于链表
它的元素是不连续存储的,需要根据当前节点找到下一个节点
栈:符合先进后出的原则
java中的LinkedList可以充当栈
其中push()方法往栈顶添加元素
pop()方法从栈顶移除元素
peek()方法从栈顶获取元素
**队列:**符合先进先出的原则
java中的LinkedList可以充当队列
其中offer()方法可以在队头添加元素
poll()方法可以从队尾移除元素
非线性结构中有:
**优先级队列:**在队列的基础上加了优先级,可以根据优先级调整元素的顺序,保证 优先级高的元素先出队
java中的PriorityQueue可以作为优先级队列
它的底层是用大根堆/小根堆实现的
适合用于排行榜,任务调度等编码
**Hash表:**由多个key-value组成,根据key的hash码将value分散存储数组中,其中key的hash码与数组的索引相对应
java中的hashMap,hashtable都属于Hash表
可以用于快速查找数据
红黑树:可以自平衡的二叉查找树
java中的TreeMap属于红黑树
**跳表:**多级链表结构,也能达到与红黑树同级的性能,且实现更为简单
java 中的 ConcurrentSkipListMap 用跳表结构实现
redis 中的 SortedSet 也是用跳表实现
**B+ 树:**可以自平衡的 N 叉查找树
关系型数据库的索引常用 B+ 树实现
6说说 java 中常见的集合类?
接口有四个:Collection,set,list,map
collection是父接口,list和set是其子接口
map调用entryset(),keyset()时会创建set的实现
map调用values()时,会用到collection的实现
list实现(常见3个)
arraylist:基于数组实现
随机访问性能高,但是增删性由于要移动元素性能不高
- 【进阶】但如果增、删操作的是数组尾部不牵涉移动元素
linkedlist:基于链表实现:随机访问性能低,但增删性能高
【进阶】说它随机访问性能低是相对的,如果是头尾节点,无论增删改查都快
【进阶】说它增删性能高也是有前提的,并没有包含定位到该节点的时间,把这个算上,增删性能并不高
vector:基于数组实现:相较与前两个线程安全
- 【进阶】一些说法说 Vector 已经被舍弃,这是不正确的
set实现
hashset内部组合了hashmap,利用map key唯一的特点来实现set
集合中元素唯一,注意需要为元素实现 hashCode 和 equals 方法
【进阶】Set 的特性只有元素唯一,有些人说 Set 无序,这得看实现,例如 HashSet 无序,但TreeSet 有序
map实现(5个)
HashMap 底层是 Hash 表,即数组 + 链表,链表过长时会优化为红黑树
- 集合中 Key 要唯一,并且它需要实现 hashCode 和 equals 方法
LinkedHashMap 基于 HashMap,只是在它基础上增加了一个链表来记录元素的插入顺序
【进阶】这个链表,默认会记录元素插入顺序,这样可以以插入顺序遍历元素
【进阶】这个链表,还可以按元素最近访问来调整顺序,这样可以用来做 LRU Cache 的数据结构
TreeMap 底层是红黑树
Hashtable 底层是 Hash 表,相对前面三个实现来说,线程安全
- 【进阶】它的线程安全实现方式是在 put,get 等方法上都加了 synchronized,锁住整个对象
ConcurrentHashMap 底层也是 Hash 表,也是线程安全的
【进阶】它的 put 方法执行时仅锁住一个链表,并发度比 Hashtable 高
【进阶】它的 get 方法执行不加锁,是通过 volatile 保证数据的可见性
7HashMap 原理(数据结构)?
底层数据结构是:数组+链表+红黑树
数组的作用是存取元素时利用key的hashcode来计算它在数组中的索引,这样能在没有冲突的情况下,让时间复杂度达到o(1)
数组的大小毕竟优先,就算元素的hashcode唯一,但是当大小为n的数组放入n+1个元素时也有引起冲突;
解决冲突的办法就是使用链表,将这些冲突的元素链接起来,此时在链表中存取元素,时间复杂度会达到o(1);
树化的目的是防止链表过长引起整个hashmap的性能下降,红黑树的时间复杂度是o(logn)
有一些细节问题可以继续回答,比如树化的时机【进阶】
时机:在数组容量达到 >= 64 且链表长度 >= 8 时,链表会转换成红黑树
如果树中节点做了删除,节点少到已经没必要维护树,那么红黑树也会退化为链表
8HashMap 原理(扩容)?
扩容因子:0.75
比如说初始容量为16,当放入第13个元素后,超过了0.75,就会扩容,扩容容量翻倍;
扩容后,会重新计算key对应的桶下标,这样一部分key会移动到 其他桶中
9HashMap 原理(方法执行流程)?
以 put 方法为例进行说明
产生 hash 码。
先调用 key.hashCode() 方法
为了让哈希分布更均匀,还要对它返回结果进行二次哈希,这个结果称为 hash
二次哈希就是把 hashCode 的高 16 位与低 16 位做了个异或运算
搞定数组。
如果数组还不存在,会创建默认容量为 16 的数组,容量称为 n
否则使用已有数组
计算桶下标。
利用 (n - 1) & hash 得到 key 对应的桶下标(即数组索引)
也可以用 hash % n 来计算,但效率比前面的方法低,且有负数问题
用 (n - 1) & hash 有前提,就是容量 n 必须是 2 的幂(如 16,32,64 ...)
计算好桶下标后,分三种情况
如果该桶位置还空着,直接根据键值创建新的 Node 对象放入该位置即可
如果该桶是一条链表,沿着链表找,看看是否有值相同的 key,有走更新,没有走新增
走新增逻辑的话,是把节点链到尾部(尾插法)
新增后还要检查链表是否需要树化,如果是,转成红黑树
新增的最后要检查元素个数 size,如果超过阈值,要走扩容逻辑
如果该桶是一棵红黑树,走红黑树新增和更新逻辑,同样新增的最后要看是否需要扩容
10说说 BIO、NIO、AIO?
BIO是同步阻塞IO:采用请求-连接-线程的模式
客户端连接后,服务器会一直阻塞到数据读写完成
优点是简单易用
缺点是并发能力差,线程开销大
NIO是同步非阻塞IO:基于Reactor模式,核心组件有Channel,bufffer,selector;
一个selector可以管理多个channel,线程不断轮询就绪事件,不用阻塞等待
服务端一个线程可以处理多个请求
优点:并发能力强,资源占用少
缺点:编程复杂
AIO是异步非阻塞IO:基于Preactor模式
读写操作完全异步,操作系统完成后才通知应用程序
应用不需要轮询,也不需要处理数据读写,直接回调
优点:性能更高,编程更简洁
11 IO流?
字节流:读写时以 字节为单位,抽象父类是Inputstream和outputstream
字符流:读写时以 字符为单位,抽象父类是writer和reader
转换流:用来把字节流转换为字符流,抽象父类是Inputstreamreader和outputstreamwriter
缓冲流:增加缓冲来提高读写效率,抽象父类是buffered 还是那四个(如bufferedreader)
对象流:配合序列化技术将java对象转换成字节流或逆操作,相关类:objectInputstream和objectoutputstream
12ThreadLocal 的原理?
ThreadLocal主要目的是用来多线程环境下的变量隔离;
每个线程对象内部都有一个ThreadLocalMap,它用来存储这些需要线程隔离的资源
资源的种类有很多,通过ThreadLocal来区分,它作为ThreadLocalMap的key,需要隔离的资源作为value
有ThreadLocal.set存储隔离资源
ThreadLocal.get获取隔离资源
使用完ThreadLocal 后一定要remove()避免内存泄漏
13解释悲观锁与乐观锁?
悲观锁:像synchronized,lock都属于悲观锁
如果发生竞争,失败的线程会阻塞
乐观锁:像atomicinteger, Atomicreferance等原子类,都属于乐观锁
如果发生竞争,失败的线程不会阻塞,仍然会重试
适用场景
如果竞争少,能很快占有共享资源,适合使用乐观锁
如果竞争多,线程对共享资源的独占时间长,适合使用悲观锁
14synchronized 原理?
以重量级锁为例,比如t0,t1两个线程同时执行加锁代码,发生了竞争
当执行到加锁代码时,会根据对象的对象头找到或创建此对象的monitor对象
检查monitor对象的owner属性,用cas操作去设置owner为当前现成,cas是原子操作,只能有一个线程能成功
假设t0cas成功,那么to就能加锁,就能访问执行synchronized 代码块内的部分
t1这边cas失败,会自旋若干次,重新尝试加锁,如果
自选期间t0释放锁,那么t1不必阻塞,加锁成功
自选期间t0没有释放锁,那么t1会加入到monitor的等待队列阻塞,当t0释放锁后会重新唤起它恢复并运行
15【追问】 synchronized 锁升级?
synchronized锁有三个级别:偏向锁,轻量级锁,重量级锁,性能依次降低
当就一个线程对一个对象进行加锁,用偏向锁
当两个线程交替为对象加锁,但没有发生竞争,就用轻量级锁
当多个线程加锁发生竞争,就使用重量级锁
16对比 synchronized 和 volatile?
并发编程需要从原子性,可见性,有序性考虑线程安全;
volatile修饰共享变量,可以保证它的可见性和有序行,但是不能保证其原子性
synchronized代码块,不仅能保证共享变量的可见性、有序性,同时也能保证原子性
补充:
17对比 synchronized 和 Lock?
synchronized是关键字,Lock是java的接口
synchronized底层是由C++实现锁,Lock是靠java代码实现锁
Lock功能更多,可以设置公平锁还是非公平锁,还可以设置加锁时间,可打断等
Lock提供多种扩展方式,可以根据场景选择更合适的实现
Lock 释放锁需要调用 unlock 方法,而 synchronzied 在代码块结束无需显式调用就可以释放锁
18线程池的核心参数?
记忆七个参数
核心线程数:常驻线程池的线程
最大线程数:
如果同时执行的任务数超过了核心线程数,且队列已满,会创建新的线程来救急
总线程数(新线程+原有的核心线程)不超这个最大线程数
存活时间:
- 超过核心线程数的线程一旦闲下来,会存活一段时间,然后被销毁
存活时间单位:
工作队列:
- 如果同时执行的任务数超过了核心线程数,会把暂时无法处理的任务放入此队列
线程工厂:
- 可以控制池中线程的命名规则,是否是守护线程等(不太重要的参数)
拒绝策略:
AbortPolicy 报错策略,直接抛异常
CallerRunsPolicy 推脱策略,线程池不执行任务,推脱给任务提交线程
DiscardOldestPolicy 抛弃最老任务策略,把队列中最早的任务抛弃,新任务加入队列等待
DiscardPolicy 抛弃策略,直接把新任务抛弃不执行
19JVM 堆内存结构?
JVM 堆内存结构与垃圾收集器有关
在传统的垃圾收集器中JVM 堆内存结构分为新生代和老年代
年轻代又分为
伊甸园 Eden
幸存区 S0,S1
如果是 G1 垃圾回收器,会把内存划分为一个个的 Region,每个 Region 都可以充当
伊甸园
幸存区
老年代
巨型对象区
20垃圾回收算法?
标记-清除算法。优点是回收速度快,但会产生内存碎片
标记-整理算法。相对清除算法,不会有内存碎片,当然速度会慢一些
标记-复制算法。将内存划分为大小相等的两个区域 S0 和 S1
S0 的职责用来存储对象,S1 始终保持空闲
垃圾回收时,只需要扫描 S0 的存活对象,把它们复制到 S1 区域,然后把 S0 整个清空,最后二者互换职责即可
不会有内存碎片,特别适合存活对象很少时(因为此时复制工作少)
21【追问】伊甸园、幸存区、老年代细节?
对象最初都诞生在伊甸园,这些对象通常寿命都很短,在伊甸园空间不足,会触发年轻代回收,还活着的对象进入幸存区 S0,年轻代回收适合采用标记-复制算法
接下来再触发年轻代回收时,会将伊甸园和 S0 仍活着的对象复制到 S1,清空 S0,交换 S0 和 S1 职责
经过多次回收仍不死的对象,会晋升至老年代,老年代适合放那些长时间存活的对象
老年代回收如果满了,会触发老年代垃圾回收,会采用标记-整理或标记-清除算法。老年代回收时的暂停时间通常比年轻代回收更长
晋升条件
注意不同垃圾回收器,晋升条件不一样
在 parallel 里,经历 15 次(默认值)新生代回收不死的对象,会晋升
可以通过 -XX:MaxTenuringThreshold 来调整
例外:如果幸存区中的某个年龄对象空间占比已经超过 50%,那么大于等于这个年龄的对象会提前晋升
大对象的处理
首先大对象不适合存储在年轻代,因为年轻代是复制算法,对象移动成本高
注意不同垃圾回收器,大对象处理方式也不一样
在 serial 和 cms 里,如果对象大小超过阈值,会直接把大对象晋升到老年代
这个阈值通过 -XX:PretenureSizeThreshold 来设置
在 g1 里,如果对象被认定为巨型对象(对象大小超过了 region 的一半),会存储在巨型对象区
Region 大小是堆内存总大小 / 2048(必须取整为2的幂),或者通过 -XX:G1HeapRegionSize 来设置
22Lambda表达式?
什么是 Lambda 表达式
文献中把 Lambda 表达式一般称作匿名函数 ,语法为
(参数部分) -> 表达式部分它本质上是一个函数对象
它可以用在那些需要将行为参数化的场景,例如 Stream API,MyBatisPlus 的 QueryWrapper 等地方
Lambda 与匿名内部类有何异同
它们都可以用于需要行为参数化的场景
Lambda 表达式必须配合函数式接口使用,而匿名内部类不必拘泥于函数式接口,其它接口和抽象类也可以
Lambda 表达式比匿名内部类语法上更加简洁
匿名内部类是在编译阶段由程序员编写提供,而 Lambda 表达式是在运行阶段动态生成它所需的类
【进阶】Lambda 中 this 含义与匿名内部类中的 this 不同
23什么是反射?
反射是 java 提供的一套 API,通过这套 API 能够在运行期间
根据类名加载类
获取类的各种信息,如类有哪些属性、哪些方法、实现了哪些接口 ...
类型参数化,根据类型创建对象
方法、属性参数化,以统一的方式来使用方法和属性
反射广泛应用于各种框架实现,例如
Spring 中的 bean 对象创建、依赖注入
JUnit 单元测试方法的执行
MyBatis 映射查询结果到 java 对象
...
反射在带来巨大灵活性的同时也不是没有缺点,那就是反射调用效率会受一定影响
24什么是泛型?
泛型的主要目的是实现类型参数化,java 在定义类、定义接口、定义方法时都支持泛型
泛型的好处有
提供编译时类型检查,避免运行时类型转换错误,提高代码健壮性
设计更通用的类型,提高代码通用性
25Tomcat优化?
Tomcat 优化主要从内存、线程、连接器、配置禁用、部署方式几个方面入手,目的是提高并发、减少卡顿、避免 OOM。
1JVM 内存优化:
- 堆初始值与最大值一致,避免扩容开销
- 合理设置元空间,防止类过多 OOM
2线程池优化:使用 NIO/NIO2 模式,比 BIO 并发高很多,合理配置线程池的参数
3禁用 DNS 反向解析
4开启 GZIP 压缩,减少传输体积;
5动静分离,静态资源交给 Nginx,Tomcat 只处理业务;
6会话分布式存储,提升集群能力。

