场景:在一间明亮的面试室内,严肃的面试官正对面坐着紧张的求职者王铁牛,一场互联网大厂Java求职者面试正在进行。
第一轮: 面试官:首先问你几个Java核心知识的问题。Java中,接口和抽象类有什么区别? 王铁牛:接口里的方法都是抽象的,不能有方法体,而且只能定义全局常量;抽象类可以有抽象方法,也可以有非抽象方法,能定义普通成员变量。 面试官:回答得不错。那多态在实际开发中有什么作用? 王铁牛:多态能提高代码的可扩展性和可维护性,比如可以用父类类型的变量来引用子类对象,调用方法时会根据实际对象类型来执行相应的方法。 面试官:很好。最后一个问题,简述一下Java的异常处理机制。 王铁牛:就是try - catch - finally结构,try里放可能出现异常的代码,catch捕获异常进行处理,finally不管有没有异常都会执行。
第二轮: 面试官:接下来问几个关于JUC的问题。什么是CAS? 王铁牛:CAS就是比较并交换,通过比较内存值和预期值是否一样,如果一样就更新为新值,不一样就重试。 面试官:那AQS又是什么? 王铁牛:AQS是抽象队列同步器,是构建锁和同步器的框架,像ReentrantLock就是基于AQS实现的。 面试官:不太准确。再问,线程池的核心参数有哪些? 王铁牛:有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。
第三轮: 面试官:最后一轮,问几个更深入的问题。说说JVM的垃圾回收算法有哪些? 王铁牛:有标记清除、标记整理、复制算法等。 面试官:简述一下HashMap的底层实现原理。 王铁牛:就是数组加链表,还有红黑树,根据key的哈希值找到对应的桶位置,链表长度超过阈值就转成红黑树。 面试官:不太清晰。Spring中,依赖注入有几种方式? 王铁牛:有构造器注入、setter方法注入等。
面试结束,面试官表示会综合评估后让王铁牛回家等通知。王铁牛带着紧张又期待的心情离开了面试室。他在回答问题时,简单的问题回答得不错,得到了面试官的夸赞,但复杂问题回答得有些混乱不清晰,这可能会影响最终的面试结果。在后续等待通知的过程中,他也意识到自己在一些知识细节上还需要进一步加强学习,以便下次能更好地应对面试。
答案:
- Java核心知识:
- 接口和抽象类的区别:接口里的方法都是抽象的,不能有方法体,且只能定义全局常量;抽象类可以有抽象方法,也可以有非抽象方法,能定义普通成员变量。接口主要用于实现多重继承,强调行为规范;抽象类用于定义具有部分实现的类层次结构,作为子类的公共抽象部分。
- 多态在实际开发中的作用:多态能提高代码的可扩展性和可维护性。可以用父类类型的变量来引用子类对象,调用方法时会根据实际对象类型来执行相应的方法。这样当需要添加新的子类时,只需要在合适的地方使用父类引用,而不需要修改大量调用该对象方法的代码。例如,在一个图形绘制系统中,定义一个父类Shape,子类Circle和Rectangle继承自Shape,通过Shape类型的变量来存储不同的图形对象,调用draw方法时就能根据实际图形类型绘制相应的图形,而不需要在调用draw方法的地方逐个判断图形类型。
- Java的异常处理机制:Java的异常处理机制主要通过try - catch - finally结构来实现。try块中放置可能出现异常的代码,如果try块中的代码抛出异常,程序会立即跳转到对应的catch块中进行异常处理。catch块根据异常类型进行针对性的处理。finally块不管try块中的代码是否抛出异常都会执行,通常用于资源清理等操作,比如关闭文件、数据库连接等。例如,在读取文件内容时,将读取文件的代码放在try块中,如果读取过程中出现文件不存在等异常,会被catch块捕获并处理,最后通过finally块关闭文件资源,确保资源得到正确释放。
- JUC:
- CAS(比较并交换):CAS是一种乐观锁机制,通过比较内存值和预期值是否一样,如果一样就更新为新值,不一样就重试。它是实现原子操作的基础,比如在AtomicInteger类中,很多方法就是通过CAS实现的。例如,AtomicInteger的incrementAndGet方法,它通过CAS不断尝试将当前值加1,直到成功更新内存中的值。
- AQS(抽象队列同步器):AQS是构建锁和同步器的框架。它内部维护了一个FIFO队列来管理等待获取锁的线程。像ReentrantLock就是基于AQS实现的。当一个线程尝试获取锁时,如果锁被占用,该线程会被加入到AQS队列中等待。例如,当一个线程调用ReentrantLock的lock方法时,首先会尝试通过CAS获取锁,如果获取失败,就会被放入AQS队列中,等待锁的释放并被唤醒。
- 线程池的核心参数:
- corePoolSize:线程池的核心线程数,当提交的任务数小于corePoolSize时,线程池会创建新线程来执行任务。
- maximumPoolSize:线程池允许的最大线程数,当提交的任务数大于corePoolSize且任务队列已满时,会创建新线程执行任务,但线程数不能超过maximumPoolSize。
- keepAliveTime:线程池中的线程在空闲时的存活时间,当线程空闲时间超过keepAliveTime时,会被销毁,以减少资源消耗。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,用于存放提交的任务,当提交的任务数大于corePoolSize时,任务会被放入workQueue中等待执行。
- threadFactory:线程工厂,用于创建线程,可自定义线程的名称、优先级等属性。
- handler:拒绝策略,当线程池的线程数达到maximumPoolSize且任务队列已满时,会调用handler来处理新提交的任务。常见的拒绝策略有AbortPolicy(直接抛出异常)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)、CallerRunsPolicy(由调用线程执行任务)。
- JVM:
- 垃圾回收算法:
- 标记清除算法:分为两个阶段,首先标记出所有需要回收的对象,然后统一回收被标记的对象。这种算法效率不高,会产生大量内存碎片。
- 标记整理算法:也是先标记出需要回收的对象,然后将存活对象向一端移动,最后清理边界以外的内存。它解决了标记清除算法产生内存碎片的问题,但移动存活对象的开销较大。
- 复制算法:将内存空间分为两块,每次只使用其中一块。当这一块内存使用完后,将存活对象复制到另一块内存中,然后清理原来的内存块。这种算法效率较高,但浪费了一半的内存空间。
- 垃圾回收算法:
- HashMap的底层实现原理:HashMap底层是数组加链表,还有红黑树的结构。当往HashMap中插入元素时,首先根据key的哈希值通过哈希函数计算出对应的桶位置。如果该桶为空,就直接插入新节点;如果该桶不为空,就会遍历链表或红黑树,找到相同key的节点则更新其值,否则在链表或红黑树的末尾插入新节点。当链表长度超过阈值(默认为8)时,链表会转换为红黑树,以提高查找效率。例如,当插入一个键值对时,计算出的桶位置为3,如果桶3为空,就直接在桶3中插入新节点;如果桶3不为空,就遍历链表查找是否有相同key的节点。
- Spring:
- 依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖对象。这种方式可以确保依赖对象在对象创建时就已经被注入,并且依赖关系不可变。例如,在一个UserService类中,通过构造函数注入UserDao对象,使得UserService在创建时就拥有了访问数据库的能力。
- setter方法注入:通过setter方法来注入依赖对象。这种方式可以在对象创建后动态地设置依赖对象,并且依赖关系可以在运行时修改。例如,在一个UserService类中,通过setter方法注入UserDao对象,在运行时可以更换不同的UserDao实现类。
- 依赖注入的方式: