面试官:请简要介绍一下 Java 核心知识中的面向对象三大特性。
王铁牛:这个简单,封装、继承、多态嘛。
面试官:不错,回答正确。那在多线程场景中,如何确保线程安全?
王铁牛:可以用 synchronized 关键字,还有 Lock 接口。
面试官:嗯,回答得还可以。接下来问你几个关于 JUC 的问题,什么是 CAS?
王铁牛:呃……这个嘛,就是比较并交换,好像是这样。
第一轮结束。
面试官:谈谈 JVM 的内存结构。
王铁牛:就是有堆、栈、方法区这些。
面试官:那类加载机制有哪些?
王铁牛:嗯……有什么双亲委派模型吧。
面试官:线程池的参数都有什么作用?
王铁牛:这个,不太清楚。
第二轮结束。
面试官:讲讲 HashMap 的底层实现。
王铁牛:好像是数组加链表啥的。
面试官:ArrayList 是如何实现动态扩容的?
王铁牛:这个……不太记得了。
面试官:Spring 框架中依赖注入有几种方式?
王铁牛:瞎答一个,好像有构造器注入啥的。
第三轮结束。
面试结束,面试官表示会让王铁牛回家等通知。
答案:
- 面向对象三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口,隐藏内部实现细节。这样可以提高代码的安全性和可维护性,比如一个类中的属性可以通过 private 修饰,然后提供 public 的 get 和 set 方法来访问和修改。
- 继承:子类继承父类的属性和方法,实现代码复用。例如一个 Animal 类有 eat 方法,Dog 类继承自 Animal 类,就可以直接使用 eat 方法。
- 多态:同一行为具有多个不同表现形式。比如一个父类类型的引用可以指向其子类的对象,当调用该引用的某个方法时,会根据实际指向的子类对象来执行相应的方法。
- 多线程场景确保线程安全:
- synchronized 关键字:可以修饰方法或代码块。修饰方法时,同一时刻只能有一个线程访问该方法;修饰代码块时,只有获得该对象锁的线程才能执行代码块中的内容。例如在一个共享资源的方法上使用 synchronized,就能保证在同一时间只有一个线程能访问该资源,避免数据竞争。
- Lock 接口:提供了比 synchronized 更灵活的锁控制。比如可以实现可中断锁、定时锁等。通过 Lock 接口的 lock 方法获取锁,unlock 方法释放锁。例如 ReentrantLock 类实现了 Lock 接口,它可以实现公平锁和非公平锁,并且可以在获取锁时设置等待时间等。
- JUC 中的 CAS:
- 比较并交换:是一种乐观锁机制。它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置 V 的值与预期原值 A 相同时,将内存位置 V 的值更新为新值 B,否则不做任何操作。在 Java 中,原子类如 AtomicInteger 就是通过 CAS 实现的。例如 AtomicInteger 的 incrementAndGet 方法,它通过 CAS 不断尝试将当前值加 1,直到成功。
- JVM 的内存结构:
- 堆:是 JVM 中最大的一块内存区域,用于存放对象实例。可以分为新生代、老年代等区域。对象在新生代中创建,经过多次垃圾回收后,如果还存活,就会被晋升到老年代。
- 栈:每个线程都有自己独立的栈空间,用于存放局部变量、方法调用等信息。栈中的数据是线程私有的,并且随着方法的调用和结束而自动入栈和出栈。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等数据。在 Java 8 及以后,方法区被元空间取代,元空间使用本地内存,而不是像方法区那样使用 JVM 堆内存。
- 类加载机制:
- 双亲委派模型:当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这样可以保证 Java 核心类库的安全性和一致性。
- 线程池的参数作用:
- corePoolSize:线程池的核心线程数。当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
- maximumPoolSize:线程池允许的最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,会创建新的线程直到线程数达到 maximumPoolSize。
- keepAliveTime:线程池中非核心线程的存活时间。当线程数大于 corePoolSize 时,多余的线程在空闲时会存活 keepAliveTime 这么长时间,之后会被销毁。
- unit:keepAliveTime 的时间单位。
- BlockingQueue:任务队列,用于存放提交的任务。当提交的任务数大于 corePoolSize 时,会将任务放入任务队列中。常见的有 ArrayBlockingQueue、LinkedBlockingQueue 等。
- HashMap 的底层实现:
- HashMap 底层是数组 + 链表 + 红黑树的结构。初始时会创建一个长度为 16 的数组。当往 HashMap 中插入键值对时,首先通过 key 的 hash 值计算出在数组中的索引位置,如果该位置为空,则直接插入新的节点;如果不为空,则会遍历链表或红黑树,找到相同 key 的节点则更新其 value,否则在链表尾部插入新节点。当链表长度达到 8 且数组长度大于等于 64 时,链表会转换为红黑树,以提高查询效率。
- ArrayList 实现动态扩容:
- ArrayList 内部维护了一个数组。当添加元素时,如果当前数组容量不足,就会进行扩容。扩容时会创建一个新的更大的数组,一般是原数组容量的 1.5 倍(如果原数组容量小于 64),然后将原数组中的元素复制到新数组中。例如当 ArrayList 中添加了 16 个元素后,数组容量变为 24(16 * 1.5),如果再继续添加元素,当超过 24 个时又会进行扩容。
- Spring 框架中依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖。例如一个类有一个依赖对象,在其构造函数中传入该依赖对象。优点是注入的依赖在对象创建时就已经可用,并且必须有依赖才能创建对象,能保证对象的完整性。缺点是如果依赖较多,构造函数参数列表会很长。
- setter 注入:通过提供 setter 方法来注入依赖。先创建对象,然后再通过 setter 方法设置依赖。优点是灵活性高,可以在对象创建后再设置依赖。缺点是对象创建时依赖可能为空,需要额外的逻辑来处理空指针问题。
- 接口注入:实现一个接口,通过接口方法注入依赖。但这种方式使用较少。