1、基础语法与面向对象
1.1 重载与重写的区别
- 重载是对象的方法之间,它们方法名相同,但方法的参数列表不同
- 重写是父子类(包括接口与实现类)中两个同名方法,它们方法名相同,且方法的参数列表相同
- 重载在编译阶段,由编译器根据传递给方法的参数来区分方法,例如
- 而重写是在运行阶段,由虚拟机解释器去获取引用对象的实际类型,根据类型才能确定该调用哪个方法,例如
- 有没有发生重写,可以使用 @Override 来检查
MyObject obj = ... obj.test(123); // 应该是调用 test(int x) 这个方法 obj.test("abc"); // 应该是调用 test(String x) 这个方法
Super obj = ... obj.test(); // 到底是调用父类,还是子类的 test 方法,必须检查引用对象的实际类型才能确定
P.S.
- 括号内的说明是为了严谨,自己知道就行,回答时不必说出,这样比较简洁
- 个人觉得,在回答方法重载时,不必去细说什么参数的类型、个数、顺序,就说参数列表不同就完了
- 个人觉得,重点在于点出:重载是编译时由编译器来区分方法,而重写是运行时由解释器来区分方法
- 语法细节,问了再说,不问不必说
- 重写时,子类方法的访问修饰符要 >= 父类方法的访问修饰符
- 重写时,子类方法抛出的检查异常类型要 <= 父类方法抛出的检查异常类型,或子类不抛异常
- 重写时,父子类的方法的返回值类型要一样,或子类方法返回值是父类方法返回值的子类
1.2 == 与 equals 的区别
- 对于基本类型,== 是比较两边的值是否相同
- 对于引用类型,== 是比较两边的引用地址是否相同,用来判断是否引用着同一对象
- equals 要看实现
- Object.equals(Object other) 的内部实现就是 ==,即判断当前对象和 other 是否引用着同一对象
- 比如 String,它的内部实现就是去比较两个字符串中每个字符是否相同,比较的是内容
- 比如 ArrayList,它的内部实现就是去比较两个集合中每个元素是否 equals,比较的也是内容
1.3 String,StringBuilder 和 StringBuffer 的区别
- 它们都可以用来表示字符串对象
- String 表示的字符串是不可变的,而后两者表示的字符串是内容可变的(可以增、删、改字符串里的内容)
- StringBuilder 不是线程安全的,StringBuffer 是线程安全的,而 String 也算是线程安全的
适用场景
- 大部分场景下使用 String 就足够了
- 如果有大量字符串拼接的需求,建议用后两者,此时
- 此字符串对象需要被多线程同时访问,用 StringBuffer 保证安全
- 此字符串对象只在线程内被使用,用 StringBuilder 足够了
另外针对 String 类是 final 修饰会提一些问题,把握下面几点
- 本质是因为 String 要设计成不可变的,final 只是条件之一
- 不可变的好处有很多:线程安全、可以缓存等
1.4 说说 Java 中的异常
异常的重要继承关系如图所示,其中
- Throwable 是其它异常类型的顶层父类
- Error 表示无法恢复的错误,例如 OutOfMemoryError 内存溢出、StackOverflowError 栈溢出等
- 这类异常即使捕捉住,通常也无法让程序恢复正常运行
- Exception 表示可恢复的错误,处理方式有两种
- 一是自己处理,用 catch 语句捕捉后,可以进行一些补救(如记录日志、恢复初始状态等)
- 二是用 throw 语句将异常继续抛给上一层调用者,由调用者去处理
- Exception 有特殊的子类异常 RuntimeException,它与 Exception 的不同之处在于
- Exception 被称之为检查异常,意思是必须在语法层面对异常进行处理,要么 try-catch,要么 throws
- RuntimeException 和它的子类被称为非检查异常(也可以翻译为字面意思:运行时异常),在语法层面对这类异常并不要求强制处理,不加 try-catch 和 throws 编译时也不会提示错误
- 常见的非检查异常有
- 空指针异常
- 算术异常(例如整数除零)
- 数组索引越界异常
- 类型转换异常
- ...
2、集合类
2.1 你知道的数据结构有哪些
线性结构
- 动态数组:相对于普通数组可以扩容
- java 中 ArrayList 就属于动态数组
- 数组的特点是其中元素是连续存储的
- 链表:由多个节点链在一起
- java 中的 LinkedList 就属于链表
- 链表的特点是其中元素是不连续存储的,每次需要根据当前节点,才能找到相邻节点
- 栈:符合 First In Last Out(先进后出)规则
- java 中的 LinkedList 可以充当栈
- 它的 push 方法向栈顶添加元素
- 它的 pop 方法从栈顶移除元素
- 它的 peek 方法从栈顶获取元素(不移除)
- 队列:符合 First In First Out(先进先出)规则
- java 中 LinkedList 也可以充当队列
- 它的 offer 方法用来向队列尾部添加元素(入队)
- 它的 poll 方法用来从队列头部移除元素(出队)
非线性结构
- 优先级队列:在队列基础上增加了优先级,队列会根据优先级调整元素顺序,保证优先级高的元素先出队
- java 中 PriorityQueue 可以作为优先级队列
- 它底层用大顶堆或小顶堆来实现
- 它适用于实现排行榜、任务调度等编码
- 它特别适合于流式数据的处理,利用它能够大大节省内存
- Hash 表(哈希表,也叫散列表):由多对 key - value 组成,会根据 key 的 hash 码把它们分散存储在数组当中,其中 key 的 hash 码与数组索引相对应
- java 中的 HashMap,Hashtable 都属于哈希表
- 它特别适用于实现数据的快速查找
- 红黑树:可以自平衡的二叉查找树,相对于线性结构来说,拥有更好的性能
- java 中的 TreeMap 属于红黑树
- 跳表:多级链表结构,也能达到与红黑树同级的性能,且实现更为简单
- java 中的 ConcurrentSkipListMap 用跳表结构实现
- redis 中的 SortedSet 也是用跳表实现
- B+ 树:可以自平衡的 N 叉查找树
- 关系型数据库的索引常用 B+ 树实现
P.S.
- 以上数据结构不必全部掌握,根据自己实际情况,捡熟悉的回答即可
2.2 说说 java 中常见的集合类
重要的集合接口以及实现类参考下图
重要的集合接口以及实现类参考下图
classDiagram class Collection {<<interface>>} class List {<<interface>>} class Set {<<interface>>} class Map { <<interface>> entrySet()* keySet()* values()* } Collection <|-- List Collection <|-- Set List <|.. ArrayList List <|.. LinkedList List <|.. Vector Set <|.. HashSet Map <|.. HashMap Map <|.. TreeMap Map <|.. Hashtable Map <|.. ConcurrentHashMap HashMap <|.. LinkedHashMap Set <-- Map Collection <-- Map |
|
接口
- 接口四个:Collection、List、Set、Map,它们的关系:
- Collection 是父接口,List 和 Set 是它的子接口
- Map 接口与其它接口的关系
- Map 调用 entrySet(),keySet() 方法时,会创建 Set 的实现
- Map 调用 values() 方法时,会用到 Collection 的实现
List 实现(常见三个)
- ArrayList 基于数组实现
- 随机访问(即根据索引访问)性能高
- 增、删由于要移动数组元素,性能会受影响
- 【进阶】但如果增、删操作的是数组尾部不牵涉移动元素
- LinkedList 基于链表实现
- 随机访问性能低,因为需要顺着链表一个个才能访问到某索引位置
- 增、删性能高
- 【进阶】说它随机访问性能低是相对的,如果是头尾节点,无论增删改查都快
- 【进阶】说它增删性能高也是有前提的,并没有包含定位到该节点的时间,把这个算上,增删性能并不高
- Vector 基于数组实现
- 相对于前两种 List 实现是线程安全的
- 【进阶】一些说法说 Vector 已经被舍弃,这是不正确的
Set 实现
- HashSet 内部组合了 HashMap,利用 Map key 唯一的特点来实现 Set
- 集合中元素唯一,注意需要为元素实现 hashCode 和 equals 方法
- 【进阶】Set 的特性只有元素唯一,有些人说 Set 无序,这得看实现,例如 HashSet 无序,但TreeSet 有序
Map 实现(常见五个)
- HashMap 底层是 Hash 表,即数组 + 链表,链表过长时会优化为红黑树
- 集合中 Key 要唯一,并且它需要实现 hashCode 和 equals 方法
- LinkedHashMap 基于 HashMap,只是在它基础上增加了一个链表来记录元素的插入顺序
- 【进阶】这个链表,默认会记录元素插入顺序,这样可以以插入顺序遍历元素
- 【进阶】这个链表,还可以按元素最近访问来调整顺序,这样可以用来做 LRU Cache 的数据结构
- TreeMap 底层是红黑树
- Hashtable 底层是 Hash 表,相对前面三个实现来说,线程安全
- 【进阶】它的线程安全实现方式是在 put,get 等方法上都加了 synchronized,锁住整个对象
- ConcurrentHashMap 底层也是 Hash 表,也是线程安全的
- 【进阶】它的 put 方法执行时仅锁住一个链表,并发度比 Hashtable 高
- 【进阶】它的 get 方法执行不加锁,是通过 volatile 保证数据的可见性
P.S.
- 未标注的是必须记住的部分
- 标注【进阶】的条目是该集合比较有特色的地方,回答出来就是加分项,不过也根据自己情况来记忆
2.3 HashMap 原理(数据结构)
底层数据结构:数组+链表+红黑树
接下来的回答中要点出数组的作用,为啥会有冲突,如何解决冲突
- 数组:存取元素时,利用 key 的 hashCode 来计算它在数组中的索引,这样在没有冲突的情况下,能让存取时间复杂度达到 O(1)
- 冲突:数组大小毕竟有限,就算元素的 hashCode 唯一,数组大小是 n 的情况下要放入 n+1 个元素,根据鸽巢原理,肯定会发生冲突
- 解决冲突:一种办法就是利用链表,将这些冲突的元素链起来,当然在在此链表中存取元素,时间复杂度会提高为 O(n)
接下来要能说出为什么在链表的基础上还要有红黑树
- 树化目的是避免链表过长引起的整个 HashMap 性能下降,红黑树的时间复杂度是 O(log{n})
有一些细节问题可以继续回答,比如树化的时机【进阶】
- 时机:在数组容量达到 >= 64 且 链表长度 >= 8 时,链表会转换成红黑树
- 如果树中节点做了删除,节点少到已经没必要维护树,那么红黑树也会退化为链表
2.4 HashMap 原理(扩容)
扩容因子:0.75 也就是 3/4
- 初始容量 16,当放入第 13 个元素时(超过 3/4)时会进行扩容
- 每次扩容,容量翻倍
- 扩容后,会重新计算 key 对应的桶下标(即数组索引)这样,一部分 key 会移动到其它桶中
2.5 HashMap 原理(方法执行流程)
以 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,如果超过阈值,要走扩容逻辑
- 如果该桶是一棵红黑树,走红黑树新增和更新逻辑,同样新增的最后要看是否需要扩容
P.S.
- 以上讲解基于 jdk 1.8 及以上版本的 HashMap 实现
3、网络编程
3.1 说说 BIO、NIO、AIO
问这个问题,通常是考察你对 Web 应用高并发的理解
预备知识
- 开发 Web 应用,肯定分成客户端和服务器。
- 客户端与服务器交互,肯定得做这么几件事:
- 服务器线程等待有客户端连接上来
- 客户端真的连上来了,建立连接
- 客户端没有向服务器发送请求,此时服务器线程需要等待数据准备好
- 客户端向服务器发送请求,需要将请求数据从网卡复制到系统内存
- 上面 a. c. 这两个阶段,没有客户端连接,没有数据请求,这时是否需要一个线程时刻盯着?
- 如果需要占用一个线程,那么就称线程被阻塞
- 如果不需要线程盯着,线程可以腾出手来去干别的活,那么就称线程非阻塞
- d. 阶段的数据复制,不会用到 CPU,也就是不会用到线程,同样也存在线程阻塞还是线程非阻塞两种情况
BIO(阻塞 I/O)
- 是指 b. c. d.这几个阶段,线程都得阻塞,腾不出手干别的,即使此时它无所事事
- 高并发下,阻塞线程多了,处理连接、处理请求的能力就会大受影响
- 增加线程不可行,毕竟线程是有限资源,这是成本问题
- 不增加线程也不行,没有新线程,没人去处理新连接,处理新请求
NIO(非阻塞 I/O)
- 是指 b. c. 这两个阶段,线程可以不阻塞,腾出手干别的(怎么干别的,要靠多路复用)
- 非阻塞 I/O 通常结合多路复用技术一起使用,能够在高并发下用少量线程处理大量请求
- 多路复用是以面向事件的方式处理连接、处理请求,有事件发生才去处理,没有事件则不会占用线程
- 使用了多路复用技术后,新客户端来了要连接,客户端发来了新请求,都会产生事件,把这些事件交给一个线程去统一处理就行了
- 线程不会在高并发下存在无事可做的现象,它被充分压榨,利用率高
AIO(异步 I/O)
- NIO 在 d. 这个阶段,线程仍需阻塞,不能被解放出来干其它活
- AIO 则更进一步,只需要提前准备好回调函数,在数据复制时线程被解放,该干嘛干嘛,等数据复制完毕,由系统使用另外线程来调用回调函数做后续处理
- AIO 在 Linux 下本质还是用多路复用技术来实现
小结
- BIO 并发性低,但代码更容易编写
- NIO 并发性高,不过代码编写困难
- AIO 并发性在 Linux 下没有本质提高,用的人少
- 【进阶】Java 21 起,正式支持虚拟线程
- 配合虚拟线程时,仍然是以 BIO 方式来编写代码,代码编写容易
- 虚拟线程非常廉价,线程不是不够吗,可劲加就行(不用担心线程闲置问题)
- Java 21 重新实现了网络 API,虚拟线程底层也会配合多路复用机制,在代码易编写的情况下,兼具高性能
P.S.
- B 是 Blocking 阻塞
- N 是 Non-Blocking 非阻塞
- A 是 Asynchronous 异步
4、IO流
分类
- 字节流,读写时以字节为单位,抽象父类是 InputStream 和 OutputStream
- 字符流,读写时以字符为单位,抽象父类是 Reader 和 Writer
- 转换流,用来把字节流转换为字符流,相关类:InputStreamReader 和 OutputStreamWriter
- 缓冲流,增加缓冲来提高读写效率,相关类:
- BufferedInputStream
- BufferedOutputStream
- BufferedReader
- BufferedWriter
- 对象流,配合序列化技术将 java 对象转换成字节流或逆操作,相关类:ObjectInputStream,ObjectOutputStream
5、线程与并发
5.1 ThreadLocal 的原理
ThreadLocal 的主要目的是用来实现多线程环境下的变量隔离
- 【解释】即每个线程自己用自己的资源,这样就不会出现共享,没有共享,就不会有多线程竞争的问题
原理
- 每个线程对象内部有一个 ThreadLocalMap,它用来存储这些需要线程隔离的资源
- 资源的种类有很多,比如说数据库连接对象、比如说用来判断身份的用户对象 ...
- 怎么区分它们呢,就是通过 ThreadLocal,它作为 ThreadLocalMap 的 key,而真正要线程隔离的资源作为 ThreadLocalMap 的 value
- ThreadLocal.set 就是把 ThreadLocal 自己作为 key,隔离资源作为值,存入当前线程的 ThreadLocalMap
- ThreadLocal.get 就是把 ThreadLocal 自己作为 key,到当前线程的 ThreadLocalMap 中去查找隔离资源
- ThreadLocal 一定要记得用完之后调用 remove() 清空资源,避免内存泄漏
5.2 解释悲观锁与乐观锁
悲观锁
- 像 synchronized,Lock 这些都属于悲观锁
- 如果发生了竞争,失败的线程会进入阻塞
- 【理解】悲观的名字由来:害怕其他线程来同时修改共享资源,因此用互斥锁让同一时刻只能有一个线程来占用共享资源
乐观锁
- 像 AtomicInteger,AtomicReference 等原子类,这些都属于乐观锁
- 如果发生了竞争,失败的线程不会阻塞,仍然会重试
- 【理解】乐观的名字由来:不怕其他线程来同时修改共享资源,事实上它根本不加锁,所有线程都可以去修改共享资源,只不过并发时只有一个线程能成功,其它线程发现自己失败了,就去重试,直至成功
适用场景
- 如果竞争少,能很快占有共享资源,适合使用乐观锁
- 如果竞争多,线程对共享资源的独占时间长,适合使用悲观锁
P.S.
- 这里讨论 Java 中的悲观锁和乐观锁,其它领域如数据库也有这俩概念,当然思想是类似的
5.3 synchronized 原理
以重量级锁为例,比如 T0、T1 两个线程同时执行加锁代码,已经出现了竞争(代码如下)
synchronized(obj) { // 加锁 ... } // 解锁
- 当执行到行1 的代码时,会根据 obj 的对象头找到或创建此对象对应的 Monitor 对象(C++对象)
- 检查 Monitor 对象的 owner 属性,用 Cas 操作去设置 owner 为当前线程,Cas 是原子操作,只能有一个线程能成功
- 假设 T0 Cas 成功,那么 T0 就加锁成功,可以继续执行 synchronized 代码块内的部分
- T1 这边 Cas 失败,会自旋若干次,重新尝试加锁,如果
- 重试过程中 T0 释放了锁,则 T1 不必阻塞,加锁成功
- 重试时 T0 仍持有锁,则 T1 会进入 Monitor 的等待队列阻塞,将来 T0 解锁后会唤醒它恢复运行(去重新抢锁)
5.4【追问】 synchronized 锁升级
synchronized 锁有三个级别:偏向锁、轻量级锁、重量级锁,性能从左到右逐渐降低
- 如果就一个线程对同一对象加锁,此时就用偏向锁
- 又来一个线程,与前一个线程交替为对象加锁,但只是交替,没有竞争,此时要升级为轻量级锁
- 如果多个线程加锁时发生了竞争,必须升级为重量级锁
【说明】
- 自 java 6 开始对 synchronized 提供了锁升级功能,之前只有重量级锁
- 但从 java 15 开始,偏向锁被标记为已废弃,将来会移除(因为实际带来的性能提升不明显,某些情况下反而影响性能)
5.5 对比 synchronized 和 volatile
并发编程需要从三个方面考虑线程安全,分别是:原子性、可见性、有序性
- volatile 修饰共享变量,可以保证它的可见性和有序性,但不能保证原子性(JMM模型)
- synchronized 代码块,不仅能保证共享变量的可见性、有序性,同时也能保证原子性
P.S.
- 实际上用 volatile 去保证可见性和有序性,并不像上面那一句话描述的那么简单,可以参考黑马课程
5.6 对比 synchronized 和 Lock
- synchronized 是关键字,Lock 是 Java 接口
- 前者底层是 C++ 代码实现锁,后者是 Java 自己的代码来实现锁
- Lock 功能更多,比如可以选择是公平锁还是非公平锁、可以设置加锁超时时间、可打断等
- Lock 的提供多种扩展实现(例如读写锁),可以根据场景选择更合适的实现
- Lock 释放锁需要调用 unlock 方法,而 synchronzied 在代码块结束无需显式调用就可以释放锁
5.7 线程池的核心参数
记忆七个参数
- 核心线程数
- 核心线程会常驻线程池
- 最大线程数
- 如果同时执行的任务数超过了核心线程数,且队列已满,会创建新的线程来救急
- 总线程数(新线程+原有的核心线程)不超这个最大线程数
- 存活时间
- 超过核心线程数的线程一旦闲下来,会存活一段时间,然后被销毁
- 存活时间单位
- 工作队列
- 如果同时执行的任务数超过了核心线程数,会把暂时无法处理的任务放入此队列
- 线程工厂
- 可以控制池中线程的命名规则,是否是守护线程等(不太重要的参数)
- 拒绝策略,队列放满任务,且所有线程都被占用,再来新任务,就会有问题,此时有四种拒绝策略:
- AbortPolicy 报错策略,直接抛异常
- CallerRunsPolicy 推脱策略,线程池不执行任务,推脱给任务提交线程
- DiscardOldestPolicy 抛弃最老任务策略,把队列中最早的任务抛弃,新任务加入队列等待
- DiscardPolicy 抛弃策略,直接把新任务抛弃不执行
6、JVM 虚拟机
6.1 JVM 堆内存结构
堆内存的布局与垃圾回收器有关。
传统的垃圾回收器会把堆内存划分为:老年代和年轻代,年轻代又分为
- 伊甸园 Eden
- 幸存区 S0,S1
如果是 G1 垃圾回收器,会把内存划分为一个个的 Region,每个 Region 都可以充当
- 伊甸园
- 幸存区
- 老年代
- 巨型对象区
6.2 垃圾回收算法
记忆三种:
- 标记-清除算法。优点是回收速度快,但会产生内存碎片
- 标记-整理算法。相对清除算法,不会有内存碎片,当然速度会慢一些
- 标记-复制算法。将内存划分为大小相等的两个区域 S0 和 S1
- S0 的职责用来存储对象,S1 始终保持空闲
- 垃圾回收时,只需要扫描 S0 的存活对象,把它们复制到 S1 区域,然后把 S0 整个清空,最后二者互换职责即可
- 不会有内存碎片,特别适合存活对象很少时(因为此时复制工作少)
6.3【追问】伊甸园、幸存区、老年代细节
- 对象最初都诞生在伊甸园,这些对象通常寿命都很短,在伊甸园空间不足,会触发年轻代回收,还活着的对象进入幸存区 S0,年轻代回收适合采用标记-复制算法
- 接下来再触发年轻代回收时,会将伊甸园和 S0 仍活着的对象复制到 S1,清空 S0,交换 S0 和 S1 职责
- 经过多次回收仍不死的对象,会晋升至老年代,老年代适合放那些长时间存活的对象
- 老年代回收如果满了,会触发老年代垃圾回收,会采用标记-整理或标记-清除算法。老年代回收时的暂停时间通常比年轻代回收更长
还会常问
晋升条件
- 注意不同垃圾回收器,晋升条件不一样
- 在 parallel 里,经历 15 次(默认值)新生代回收不死的对象,会晋升
- 可以通过 -XX:MaxTenuringThreshold 来调整
- 例外:如果幸存区中的某个年龄对象空间占比已经超过 50%,那么大于等于这个年龄的对象会提前晋升
大对象的处理
- 首先大对象不适合存储在年轻代,因为年轻代是复制算法,对象移动成本高
- 注意不同垃圾回收器,大对象处理方式也不一样
- 在 serial 和 cms 里,如果对象大小超过阈值,会直接把大对象晋升到老年代
- 这个阈值通过 -XX:PretenureSizeThreshold 来设置
- 在 g1 里,如果对象被认定为巨型对象(对象大小超过了 region 的一半),会存储在巨型对象区
- Region 大小是堆内存总大小 / 2048(必须取整为2的幂),或者通过 -XX:G1HeapRegionSize 来设置
P.S.
著名教材《深入理解Java虚拟机》一书关于这些论述,很多观点陈旧过时,需要带批判眼光来学习。例如在它的《内存分配与回收策略》这一章节,提到了这些:
- 对象优先在Eden分配(OK)
- 大对象直接进入老年代(没有提到 g1 情况)
- 长期存活的对象将进入老年代(即我上面讲的晋升条件,但没强调要区分垃圾回收器)
- 动态对象年龄判定(即提前晋升)
- 空间分配担保(已过时)文中提到的 -XX:+HandlePromotionFailure 参数在 jdk8 之后已经没了
7、Lambda表达式
什么是 Lambda 表达式
- 文献中把 Lambda 表达式一般称作匿名函数,语法为 (参数部分) -> 表达式部分
- 它本质上是一个函数对象
- 它可以用在那些需要将行为参数化的场景,例如 Stream API,MyBatisPlus 的 QueryWrapper 等地方
Lambda 与匿名内部类有何异同
- 它们都可以用于需要行为参数化的场景
- Lambda 表达式必须配合函数式接口使用,而匿名内部类不必拘泥于函数式接口,其它接口和抽象类也可以
- Lambda 表达式比匿名内部类语法上更加简洁
- 匿名内部类是在编译阶段由程序员编写提供,而 Lambda 表达式是在运行阶段动态生成它所需的类
- 【进阶】Lambda 中 this 含义与匿名内部类中的 this 不同
8、反射及泛型
8.1 反射
什么是反射
- 反射是 java 提供的一套 API,通过这套 API 能够在运行期间
- 根据类名加载类
- 获取类的各种信息,如类有哪些属性、哪些方法、实现了哪些接口 ...
- 类型参数化,根据类型创建对象
- 方法、属性参数化,以统一的方式来使用方法和属性
- 反射广泛应用于各种框架实现,例如
- Spring 中的 bean 对象创建、依赖注入
- JUnit 单元测试方法的执行
- MyBatis 映射查询结果到 java 对象
- ...
- 反射在带来巨大灵活性的同时也不是没有缺点,那就是反射调用效率会受一定影响
8.2 泛型
什么是 Java 泛型
- 泛型的主要目的是实现类型参数化,java 在定义类、定义接口、定义方法时都支持泛型
- 泛型的好处有
- 提供编译时类型检查,避免运行时类型转换错误,提高代码健壮性
- 设计更通用的类型,提高代码通用性
【例如】想设计 List 集合,里面只放一种类型的元素,如果不用泛型,怎么办呢?你必须写很多实现类
- Impl1 实现类中,只放 String
- Impl2 实现类中,只放 Integer
- ...
- 要支持新的元素类型,实现类型也得不断增加,解决方法需要把元素类型作为参数,允许它可变化:List<T>,其中 T 就是泛型参数,它将来即可以是 String,也可以是 Integer ...
P.S.
- 【例如】是为了帮助你理解,不是必须答出来。
- 关键是答出类型参数化,懂的面试官不必多说,不懂的也没必要跟他继续啰嗦
9、Tomcat优化
P.S.
Tomcat 优化要从多方面综合考虑,如
- Tomcat JVM 参数调优
- Tomcat 线程池配置
- 网络配置优化
- 静态资源优化
- 日志记录优化
- ...
篇幅原因,本题作答时只侧重其中线程池配置和网络配置这两方面
以 springboot(3.2.3) 中的 tomcat 配置为例
server.tomcat.max-connections=8192 server.tomcat.accept-count=100 server.tomcat.threads.max=200 server.tomcat.threads.min-spare=10
- Tomcat I/O 模式默认采用 NIO,由于一般采用 Linux 系统,因此改成 NIO2 没有必要
- 这些配置项值都是 springboot 的默认值,这些值其实够用,根据情况调整,其中
- max-connections 控制最大连接数
- accept-count 控制连接队列中的连接数
- threads.max 控制线程池中最大线程数
- threads.min-spare 控制线程池中最少备用线程数
【进阶】虚拟线程优化
- springboot(3.2.x)配合 jdk 21 可以使用虚拟线程来优化
- 更早 springboot 想使用 jdk 21 虚拟线程,可以用替换 Tomcat 线程池的办法