面试官:请简要介绍一下 Java 核心知识中,类和对象的区别。
王铁牛:类是对象的模板,对象是类的实例。比如说,定义一个人类,这就是类,然后具体的某个人就是对象。
面试官:不错,回答得很清晰。那再说说多线程中,如何创建一个线程?
王铁牛:可以通过继承 Thread 类或者实现 Runnable 接口来创建线程。
面试官:很好。最后一个问题,简述一下 HashMap 的工作原理。
王铁牛:HashMap 基于数组和链表实现,通过 key 的哈希值来确定在数组中的位置,如果有冲突就会形成链表。
面试官:第一轮提问结束,整体表现不错。接下来第二轮提问。说说 JUC 包中的 CountDownLatch 有什么作用?
王铁牛:它可以让一个线程等待其他多个线程完成操作后再继续执行。
面试官:那如何使用它来实现多个线程并发执行任务后,主线程再汇总结果呢?
王铁牛:呃……我想想,就是先创建 CountDownLatch 对象,然后每个任务完成后调用 countDown 方法,主线程调用 await 方法等待。
面试官:回答得不太清晰。再问一个,JVM 中的垃圾回收算法有哪些?
王铁牛:好像有标记清除、标记整理、复制算法这些。
面试官:第二轮提问结束。现在进入第三轮。讲讲线程池的核心参数有哪些?
王铁牛:有 corePoolSize、maximumPoolSize、keepAliveTime 这些。
面试官:那如何合理设置这些参数呢?
王铁牛:嗯……这个要根据具体业务来,我不是很清楚。
面试官:最后一个问题,Spring 框架中,依赖注入有几种方式?
王铁牛:大概有构造器注入、setter 注入这些吧。
面试官:三轮提问结束。你的整体表现有好有坏,简单问题回答得不错,但复杂问题回答得不太理想。回家等通知吧。
答案:
- 类和对象的区别:类是具有相同属性和行为的一组对象的抽象描述,它定义了对象的属性和方法。对象是类的具体实例,是实实在在存在的个体。比如定义一个“汽车类”,包含颜色、速度等属性和行驶方法,而具体的某一辆汽车就是“汽车类”的对象。
- 创建线程的方式:
- 继承 Thread 类:定义一个类继承 Thread 类,重写 run 方法,在 run 方法中编写线程执行的代码。然后通过创建该类的实例并调用 start 方法来启动线程。
- 实现 Runnable 接口:定义一个类实现 Runnable 接口,实现其中的 run 方法。然后创建 Thread 类的实例,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 类的构造函数,最后调用 Thread 类的 start 方法启动线程。
- HashMap 的工作原理:HashMap 内部维护一个数组,通过 key 的哈希值计算出在数组中的索引位置。如果该位置为空,则直接插入新的键值对。如果该位置不为空,就会检查 key 是否相同,如果相同则覆盖 value;如果不同,则通过链表或红黑树(当链表长度达到一定阈值时会转换为红黑树)来解决冲突,将新的键值对添加到链表或红黑树中。
- CountDownLatch 的作用及使用场景:CountDownLatch 可以让一个线程等待其他多个线程完成操作后再继续执行。例如有多个线程并发执行任务,当所有任务都完成后,主线程需要汇总这些任务的结果,就可以使用 CountDownLatch。主线程创建 CountDownLatch 对象,指定计数器的值为任务的数量。每个任务完成后调用 countDown 方法,计数器减 1。主线程调用 await 方法等待,直到计数器的值变为 0,此时主线程继续执行汇总结果的操作。
- JVM 中的垃圾回收算法:
- 标记清除算法:首先标记出所有需要回收的对象,然后统一回收掉所有被标记的对象。这种算法会产生大量不连续的内存碎片。
- 标记整理算法:先标记出需要回收的对象,然后将存活的对象向一端移动,最后清理掉端边界以外的内存。
- 复制算法:将内存空间分为两块,每次只使用其中一块。当这一块内存使用完后,将存活的对象复制到另一块内存上,然后清理原来的那块内存。这种算法适用于对象存活率低的情况。
- 线程池的核心参数:
- corePoolSize:线程池的核心线程数,当提交的任务数小于 corePoolSize 时,线程池会创建新的线程来执行任务。
- maximumPoolSize:线程池允许的最大线程数,当提交的任务数大于 corePoolSize 且任务队列已满时,会创建新的线程直到线程数达到 maximumPoolSize。
- keepAliveTime:线程池中的线程在空闲时的存活时间,当线程空闲时间超过 keepAliveTime 时,非核心线程会被销毁。
- Spring 框架中依赖注入的方式:
- 构造器注入:通过构造函数来注入依赖对象。优点是注入的依赖关系在对象创建时就已经确定,保证了对象的完整性和一致性;缺点是当依赖对象较多时,构造函数参数列表会变得很长。
- setter 注入:通过 setter 方法来注入依赖对象。优点是灵活性高,可以在对象创建后再设置依赖关系;缺点是如果忘记调用 setter 方法,可能会导致依赖对象未被注入。