面试官:第一轮提问,先说说 Java 核心知识里的面向对象三大特性是什么?
王铁牛:这个我知道,是封装、继承、多态。
面试官:不错,回答正确。那再问你,接口和抽象类有什么区别?
王铁牛:接口里的方法都是抽象的,不能有具体实现,抽象类可以有抽象方法也可以有具体方法。
面试官:嗯,理解得挺到位。最后一个问题,说说 final 关键字的作用。
王铁牛:final 可以修饰类、方法和变量,修饰类不能被继承,修饰方法不能被重写,修饰变量一旦赋值就不能再变。
面试官:很好,第一轮表现不错。接下来第二轮提问,讲讲 JUC 里的 CountDownLatch 是怎么用的?
王铁牛:就是用来控制线程等待,等计数到 0 了,所有等待的线程就可以继续执行了。
面试官:那 CyclicBarrier 呢?
王铁牛:这个也是让多个线程等待,等所有线程都到了指定点,再一起继续执行。
面试官:不太准确,它是等所有线程都到达屏障点后,才一起执行后续操作,而且可以循环使用。再问你,Semaphore 有什么作用?
王铁牛:这个不太清楚,乱说一个,是控制资源访问数量的吧。
面试官:第二轮结束,你对一些简单问题回答得还行,但复杂点的就不太清晰了。下面进入第三轮提问,说说 JVM 内存模型的主要区域有哪些?
王铁牛:有堆、栈、方法区这些。
面试官:那对象创建在 JVM 里是怎么个过程?
王铁牛:先在堆里分配内存,然后初始化成员变量,最后执行构造方法。
面试官:回答得很笼统。最后问你,垃圾回收算法有哪些?
王铁牛:有标记清除、标记整理、复制算法这些。
面试官:整体来看,你对一些基础的知识点有一定了解,但遇到稍微深入点的问题就回答得不够准确和清晰。回去等通知吧。
答案:
- 面向对象三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的接口,隐藏内部实现细节。这样可以提高代码的安全性和可维护性,例如在一个类中,将一些属性设置为私有,通过公有的 get 和 set 方法来访问和修改这些属性。
- 继承:子类继承父类的属性和方法,实现代码的复用。比如一个父类有一些通用的方法,子类可以继承这些方法并根据自身需求进行扩展。
- 多态:同一个行为具有不同表现形式或形态的能力。可以通过方法重写和接口实现来体现,例如一个父类类型的引用可以指向子类的对象,调用同一个方法时会根据对象的实际类型执行不同的操作。
- 接口和抽象类的区别:
- 方法定义:接口中的方法都是抽象方法,不能有具体实现;抽象类中可以有抽象方法,也可以有具体方法。
- 实现方式:类实现接口使用 implements 关键字,一个类可以实现多个接口;类继承抽象类使用 extends 关键字,一个类只能继承一个抽象类。
- 作用:接口主要用于实现多实现关系,定义一组规范;抽象类主要用于实现继承体系,抽取公共部分。
- final 关键字的作用:
- 修饰类:被 final 修饰的类不能被继承,例如 String 类就是 final 类。
- 修饰方法:被 final 修饰的方法不能被重写,子类无法覆盖父类的该方法。
- 修饰变量:被 final 修饰的变量一旦赋值就不能再重新赋值,常量一般用 final 修饰,如 public static final int PI = 3.14;
- CountDownLatch 的使用:
- CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。例如有一个主线程需要等待多个子线程完成任务后再继续执行。可以创建一个 CountDownLatch 对象,传入需要等待的线程数量。子线程执行完任务后调用 countDown()方法,主线程调用 await()方法等待,直到计数器变为 0,主线程才会继续执行。
- CyclicBarrier 的使用:
- CyclicBarrier 也用于线程同步,它让一组线程到达一个屏障点(barrier point)时被阻塞,直到最后一个线程到达屏障点,然后所有线程才会继续执行后续操作。并且它可以循环使用,不像 CountDownLatch 只能使用一次。比如多个运动员进行接力比赛,每个运动员完成自己的一段赛程后到达一个等待点,等所有运动员都到达这个等待点后,再一起开始下一段赛程。
- Semaphore 的作用:
- Semaphore 是一个计数信号量,用于控制对共享资源的访问。它维护了一个许可计数,线程可以通过 acquire()方法获取许可,如果许可计数为 0,线程会被阻塞,直到有其他线程释放许可;线程通过 release()方法释放许可。例如在一个停车场,有固定数量的停车位(相当于许可数量),车辆(线程)需要获取停车位许可才能进入停车场,车辆离开时释放许可。
- JVM 内存模型的主要区域:
- 堆:是 JVM 中最大的内存区域,用于存储对象实例。所有对象的实例和数组都在堆中分配内存。堆可以分为新生代、老年代等不同区域,不同区域有不同的垃圾回收策略。
- 栈:每个线程都有自己独立的栈,用于存储局部变量、方法调用等。栈内存随着线程的创建而创建,随着线程的结束而销毁。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也被称为永久代(在 JDK 8 及以后,永久代被元空间取代)。
- 程序计数器:是一块较小的内存空间,它记录当前线程执行的字节码指令地址。每个线程都有自己独立的程序计数器。
- 对象创建在 JVM 里的过程:
- 分配内存:在堆中为对象分配内存空间。JVM 会通过不同的内存分配策略来为对象分配合适的内存区域,比如在新生代采用不同的分配算法(如复制算法)。
- 初始化成员变量:为对象的成员变量赋默认值,如整型默认值为 0,引用类型默认值为 null 等。
- 执行构造方法:调用对象的构造方法来初始化对象的属性和执行其他初始化操作。构造方法可以重写父类的构造方法,对对象进行更具体的初始化。
- 垃圾回收算法:
- 标记清除算法:分为两个阶段,首先标记出所有需要回收的对象,然后统一回收被标记的对象。这种算法的缺点是会产生大量不连续的内存碎片。
- 标记整理算法:也是先标记出需要回收的对象,然后将存活的对象向一端移动,最后清理边界以外的内存。解决了标记清除算法产生内存碎片的问题。
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块内存上,然后把已使用的内存空间一次性清理掉。这种算法适用于新生代,因为新生代对象存活率低。