基础知识差缺补漏-面试版持续更新

Java 基础篇

  1. 静态代理和动态代理的区别

静态代理:本质就是每多一个实体类需要编写一个静态代理对象去做局部增强,简单代码例子:

// 静态代理类(经纪人)

class SingerProxy implements Singer {

private RealSinger target; // 代理知道要给谁当经纪人

public SingerProxy(RealSinger target) {

this.target = target;

}

@Override

public void sing(String song) {

System.out.println("💰 经纪人:收订金");

target.sing(song); // 让真正的歌手唱

System.out.println("💰 经纪人:收尾款");

}

}

public class Main {

public static void main(String\[\] args) {

RealSinger jay = new RealSinger();

SingerProxy proxy = new SingerProxy(jay); // 手动创建经纪人

proxy.sing("夜曲"); // 现在有增强效果了

}

}

当多创建一个 Dancer,那么要一个 DancerProxy,以此类推。

动态代理:为一个接口实现了 InvocationHandler,使用了 Object proxy、Method method(标识)、Object\[\] args(放置具体方法)来实现的,可以只用一段代码为多个类都生成代理类,简单代码实现:

// 1. 这是"增强逻辑"的工厂,专门负责在调用方法前后"加戏"

class DynamicProxyHandler implements InvocationHandler {

private Object target; // 这个 target 是谁?就是被代理的歌手 jay

public DynamicProxyHandler(Object target) {

this.target = target;

}

@Override

public Object invoke(Object proxy, Method method, Object\[\] args) throws Throwable {

// ⭐ 这里就是"增强"的地方:在真正唱歌之前执行

System.out.println("💰 动态经纪人:收订金");

// ⭐ 这行是核心:通过反射,真实地调用 jay.sing("夜曲")

Object result = method.invoke(target, args);

// ⭐ 在真正唱歌之后执行

System.out.println("💰 动态经纪人:收尾款");

return result;

}

}

追问:泛型和动态代理都是减少了重复代码的使用,所以动态代理的本质是泛型吗?

回答:1. 泛型它运用的周期是编译前,当开始编译的时候就会被擦除。实际例子:

原型:

复制代码
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

编译后:

复制代码
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

对于编译时,代码为 List<String> 还是 List<Integer> 没有区别,无法识别,是根据最后的强转实现的。

  1. 动态代理它是运行时生成。

例子可以延续上面的简单案例,在运行前,它就是这一段代码,依旧只存在 Object 类型,当你传递后才会自动根据代码创建对应类型的代理,最终的类型就类似于你自己手动写出了新类型的静态代理,可以实例化。

追问:JDK 动态代理和 CGLIB 动态代理的区别

回答:1. JDK 动态代理只能实现(implement)有接口的类,例子解释原因是因为 invoke(Object proxy, Method method, Object\[\] args) 中 Method method 是从接口中获取方法的,所以必须传入接口类,如果没有接口,这个会报错。

  1. CGLIB 可以实现接口与类的双重实现,因为两者实现的接口不一样,JDK 实现的是 InvocationHandler,而 CGLIB 继承(extend)的是 MethodInterceptor。

它是通过子类继承父类来实现的动态代理,所以它所创建的动态代理本质是父类的子类,所以可以通过父类的方法集知道有什么方法,所以 CGLIB 无法代理父类的 final 方法和 private 方法。

  1. IOC 和 AOP 是通过什么机制实现的

回答:1. AOP 本质是面向切面编程,作用是不修改内部方法增强函数方法,所使用的机制则是动态代理,JDK 动态代理中的 invoke 函数以及 CGLIB 中的 intercept 函数均是增强函数。

  1. IOC 容器控制主要是依赖反射来实现依赖翻转,从原来需要用户手动创建如 new User()(自己控制生成、使用、销毁等一系列过程)到直接使用 @Resource(生成、使用、销毁交给系统决定),源码伪代码大致如下:
复制代码
Class<?> clazz = Class.forName("com.example.UserService");
Object instance = clazz.newInstance();  // 通过反射创建对象
// 第三步:依赖注入(仍然用反射)
Field field = clazz.getDeclaredField("userDao");
field.setAccessible(true);  // 处理 private 字段
field.set(instance, userDaoInstance);
  1. Hashtable 如何实现的线程安全

回答:Hashtable 中所有的 public 方法都使用了 synchronized 进行修饰,所有方法的访问都需要排队。

  1. ConcurrentHashMap 使用的是悲观锁还是乐观锁

回答:若桶为空桶则使用 CAS 乐观锁,CAS 的原名为 compare and swap(比较并交换),这是一条 CPU 中的原子级别的命令,不受线程切换的影响。若不是空桶则使用 synchronized 悲观锁。

追问:已经用了 synchronized 为什么还要用 CAS 呢?

答:CAS 是属于无锁加入,性能极快,无需加锁。synchronized 属于兜底机制,需要加锁消耗性能。

追问:在 1.8 前 ConcurrentHashMap 使用的是什么?

答:在 1.8 前 ConcurrentHashMap 使用的是 Segment,Segment 中的最小颗粒度是桶,Segment0: 桶012,Segment1: 桶345 类似,当数条数据的 hash 值被分到 012 因为是坐落在同一个 Segment 中,所以这数条命令写操作都需要排队。

  1. volatile 作用解释

  2. 可见性,强制线程从主内存中读取被 volatile 修饰的变量信息,如果不使用 volatile,可能修改变量但是别的线程无法感知,因为每个线程都有自己的工作内存。

  3. 阻止系统重排序。

  4. HashMap 在多线程会出现的问题

  5. 扩容容易导致环形链表出现:

这是 JDK 1.7 最著名的 Bug。让我用具体例子演示:

初始状态(单线程正常情况)

text

复制代码
HashMap 扩容前(容量=2):
桶0:A → B
桶1:空
扩容后(容量=4):
需要把 A 和 B 重新 hash 到新数组
假设 A 和 B 在新数组中都在 桶0
JDK 1.7 头插法扩容过程:
遍历旧链表:A → B
取出 A,头插到新数组:A
取出 B,头插到新数组:B → A
最终顺序反转:B → A
多线程并发扩容(死循环形成)

text

复制代码
初始:桶0 链表:A → B
线程1 和 线程2 同时触发扩容
线程1 执行到 e = A, next = B(还没完成扩容)
线程2 先完成扩容,结果变成:B → A
线程1 继续执行:
线程1 的 e = A,准备把 A 移到新数组
线程1 发现 A.next 已经被线程2 改成了 B(但 B.next = A)
线程1 继续处理 B,发现 B.next = A
线程1 再处理 A,发现 A.next = B
⭐ 形成环形链表:A ↔ B(相互引用)
环形链表 → get() 操作会无限循环 → CPU 100% 💥
  1. 多线程同时使用 put 会导致 key 被覆盖:
复制代码
初始状态:桶3 是空的
时间点 1:
线程1 计算出桶3 为空,准备插入 key1
线程2 计算出桶3 为空,准备插入 key2
时间点 2:
线程1 将 key1 插入桶3(成功)
线程2 也将 key2 插入桶3(成功)
时间点 3:
线程1 的 key1 被线程2 的 key2 覆盖!
或者线程2 的 key2 被线程1 的 key1 覆盖!
结果:只有一个 key 存在,另一个丢失 ❌

为什么不可以同时两个 key 都关联上呢?因为源码底层当判断为空的时候,会将桶直接 = node1 而不是关联,所以会被覆盖。

  1. 重写 HashMap 的 equals 和 hashCode 方法需要注意什么

准则如下:当 equals 相同时,hashCode 必须相同。当 hashCode 相同时,equals 不一定相同(哈希冲突)。

  1. HTTP 长连接和 WebSocket 有什么区别

答:全双工和半双工:TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。应用场景区别:在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(Comet)的效果。对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。

  1. JWT 令牌泄露了怎么办

  2. 黑名单(会导致服务器要存储信息,降级成 Session)。

  3. 短有效期 + 自动续签:一个短期大概只有十五分钟,在 Cookie 中存储;长期有七天时间,在 HttpOnly Cookie 中存储。当过期使用长期的申请一个短期的,同时也会再次生成一个长期的,将旧长期废除。旧的废除本质也是使用了黑名单,但是这样尽可能避免直接使用黑名单导致降级。

相关推荐
深入云栈1 小时前
Nacos 2.x 源码深度解析 (二):通信协议迭代 —— HTTP长轮询到gRPC演进
java
兔兔兔兔11 小时前
记录C++ 3
开发语言·c++
andongni2031 小时前
SpringBoot 入门实验报告
java·spring boot·后端
黄鸭部落格ShiYuYuanFang1 小时前
【分享】软考每日一练与解析-计组 8/12
java·开发语言
m0_617493941 小时前
Python 使用 cryptography 实现数据加密与解密
开发语言·python
大白同学4211 小时前
初始Qt——了解Qt背景
开发语言·qt
XWalnut2 小时前
LeetCode刷题 day37
java·数据结构·算法·leetcode
万山寒2 小时前
python依赖包导出离线安装到没有外网的服务器
服务器·开发语言·python
huaweichenai2 小时前
使用maven搭建sprint-boot
java·maven·sprint