JavaSe八股分析

JavaSE

1.流式输出和非流式输出

对比点 流式输出 非流式输出
数据传输 边生产边传输 生成完后一次传输
响应延迟 首字节快,用户能尽快看到结果 必须等所有数据生成后才能看到
内存占用 占用更少内存(分段处理) 可能占用大量内存(一次性加载)
实现复杂度 较高(需要支持分段协议/推送机制) 较低(一次性返回)
应用场景 视频流、日志实时消费、AI Chat逐字打印 小文件下载、查询一次性返回结果

非流式输出是等数据全部生成后一次性返回,而流式输出则是边生成边返回,能降低延迟和内存占用,更适合大数据量和实时场景。

2.HashMap remove 方法的实现细节

  1. 首先,remove(key)方法会计算keyhash值。
  2. 根据hash值定位到它在底层table数组中的索引位置(即bucket)。
  3. 如果该bucket为空,直接返回null
  4. 如果bucket不为空,则遍历该位置的链表或红黑树,逐个节点使用hash值和equals()方法进行比较,直到找到要删除的目标节点。如果遍历完没找到,也返回null

如果当前是链表结构,是头节点的话,即让头节点的下一个节点成为新的头节点。

p是中间节点或尾节点。那么就跳过这个节点,GC将自动回收这个不再被引用的节点

  • 红黑树的删除操作要复杂得多,因为它必须在删除节点后,通过一系列的旋转(Rotation)和重新着色(Recoloring)\操作,来*维持红黑树的5条性质*(例如,根是黑的、不能有连续的红节点、任何节点到其每个叶子节点的所有路径都包含相同数目的黑色节点等),从而保证树的平衡性。
  • HashMap会调用内部的removeTreeNode方法来执行这个复杂的过程。
  • 在红黑树中删除了一个节点后,HashMap还会检查该bucket的节点数量。如果数量减少到了一个阈值(UNTREEIFY_THRESHOLD,默认为6),为了节省内存和在节点数少时提升性能,这棵红黑树会退化(untreeify)变回普通的链表结构。
  • 删除成功后,HashMapsize会减1。
  • 方法会返回被删除节点的value值。

3.说说 ArrayList、LinkedList、CopyOnWriteArrayList 这三者的适用场景与关键差异

1. ArrayList:

  • 底层结构: 基于动态数组实现,内存是连续的。它实现了 RandomAccess 标记接口。
  • 关键差异:
    • 读性能: 支持高效的随机访问,get(index) 操作的时间复杂度是 O(1)。
    • 写性能: 尾部添加(add(e))均摊复杂度是 O(1),但在中间插入或删除元素,需要移动后续所有元素,时间复杂度是 O(n),开销很大。
  • 迭代一致性: 它的迭代器是快速失败(Fail-fast)的。如果在迭代过程中,集合结构被其他线程修改,会立刻抛出 ConcurrentModificationException

2. LinkedList:

  • 底层结构: 基于双向链表实现。
  • 关键差异:
    • 读性能: 不支持高效的随机访问,访问一个元素需要从头或尾遍历,时间复杂度是 O(n)。
    • 写性能: 在头部或尾部进行增删操作,时间复杂度是 O(1),效率极高。但在中间位置操作,需要先遍历定位,所以复杂度也是 O(n)。
  • 迭代一致性: 和 ArrayList 一样,是快速失败(Fail-fast)的。

3. CopyOnWriteArrayList (COWArrayList):

  • 底层结构: 同样基于数组
  • 关键差异:
    • 并发安全: 它是线程安全的,核心思想是“写时复制”。
    • 读性能: 读操作完全不加锁,直接访问底层数组,性能和 ArrayList 相当,非常高效。
    • 写性能: 写操作(增删改)开销巨大。它需要先加锁,然后完整地拷贝一份新数组,在新数组上修改,最后再将引用指向新数组。
  • 迭代一致性: 它的迭代器是快照(Snapshot)模式。迭代器创建时会引用当时的底层数组快照,后续的修改对该迭代器不可见,不会抛出异常,保证了迭代的绝对安全,但牺牲了数据的实时性。

4.反射的原理&&应用

反射机制允许程序在运行时动态地获取任意一个类的信息(如属性、方法、构造器)并进行操作。它的优点是极大地增加了程序的灵活性,是很多框架(如 Spring IoC)的实现基石。

JDK 动态代理中,反射主要用在最关键的一步——创建代理对象实例

整个流程是:我们调用 Proxy.newProxyInstance() 方法来创建代理对象。在这个方法内部,它会:

  1. 在运行时动态地创建一个新的代理类(.class 文件)。
  2. 然后,它会使用反射,通过 proxyClass.getConstructor(InvocationHandler.class) 获取到这个新代理类的构造器。
  3. 最后,再通过反射调用 constructor.newInstance(invocationHandler),传入我们自己实现的 InvocationHandler,来实例化这个代理对象。
  4. 比如代理模式的实现就是在对象进行初始化的时候,在bootpostproffer的后置处理的时候,将原先的bean换成我们代理的bean

所以,反射是用在了获取代理类的构造器并创建其实例这最核心的一步。

5.Netty 如何封装 NIO

Netty 是对Java原生NIO的一个高度封装和增强的框架,它解决了原生NIO在使用上非常复杂、功能有限、且容易出错的痛点。

  • 封装Selector与事件循环:
    • 原生NIO需要我们手动编写一个死循环,不断地调用selector.select(),然后遍历selectedKeys,再根据key的类型(OP_ACCEPT, OP_READ等)进行if-else判断,代码繁琐且容易出错。
    • Netty将其封装成了EventLoop。每个EventLoop内部都包含一个Selector和一个线程。这个EventLoop线程会自动地、高效地执行事件轮询和分发,我们开发者完全不需要关心底层的Selector操作。
  • 封装Channel与Buffer:
    • 原生NIO的Buffer使用起来非常反直觉,需要我们手动flip()clear()rewind(),很容易出错。
    • Netty提供了自己的ByteBuf,它通过读写指针分离的设计,彻底告别了flip()操作,使用起来非常方便。它还提供了零拷贝(Zero-Copy)池化(Pooling)\和*堆外内存*等高级功能,性能远超原生Buffer
    • Netty的Channel接口也比原生的更统一、更易用。
  • 封装责任链与业务逻辑解耦:
    • 原生NIO的所有I/O处理逻辑都混杂在一起。
    • Netty引入了ChannelPipelineChannelHandler的设计,这是一个经典的责任链模式。我们可以将网络处理逻辑(如解码、编码、业务处理)拆分成一个个独立的Handler,然后像“搭积木”一样将它们组织在Pipeline中。这使得代码结构清晰、高度解耦、易于扩展和复用。

Reactor 模型

Netty的线程模型正是经典多Reactor模型的实现,通常是主从Reactor模式(Master-Slave Reactor

  • 主Reactor(Boss Group):

    • 通常只配置一个线程EventLoop)。
    • 它的唯一职责就是监听服务端的连接请求(OP_ACCEPT事件)
    • 当接收到一个新的客户端连接后,主Reactor会将这个新建立的SocketChannel注册到从Reactor上,然后继续回去监听新的连接。它不处理任何I/O读写
  • 从Reactor(Worker Group):

    • 通常配置多个线程EventLoop),数量一般是CPU核心数的1倍或2倍。
    • 它的职责是处理所有已连接Channel的I/O读写事件(OP_READ, OP_WRITE
    • 一个Channel的整个生命周期内的所有I/O操作,都会被绑定在同一个从Reactor线程上执行,这避免了多线程并发处理同一个连接时需要加锁的问题。