面试官:请简要介绍一下 Java 核心知识中面向对象的三大特性。
王铁牛:面向对象的三大特性是封装、继承和多态。封装就是把对象的属性和方法结合成一个独立的整体,对外提供统一的访问接口;继承是指一个类可以继承另一个类的属性和方法;多态则是指同一个行为具有不同表现形式或形态的能力。
面试官:回答得不错。那说说 JUC 包下常用的几个类及其作用。
王铁牛:比如 CountDownLatch,它可以让一个线程等待其他线程完成一系列操作后再继续执行;还有 CyclicBarrier,能让一组线程互相等待,直到所有线程都到达某个屏障点,然后再一起继续执行。
面试官:嗯,了解得还挺清楚。再问个关于 JVM 的问题,简述一下 Java 内存区域都有哪些。
王铁牛:Java 内存区域主要有程序计数器、虚拟机栈、本地方法栈、堆和方法区。程序计数器记录着当前线程执行的字节码指令地址;虚拟机栈存放着局部变量表、操作数栈等;本地方法栈用于执行本地方法;堆是对象实例的存放地;方法区存储类信息、常量、静态变量等。
第一轮结束。
面试官:谈谈多线程中如何实现线程同步。
王铁牛:可以使用 synchronized 关键字来修饰方法或代码块,保证同一时刻只有一个线程能访问被修饰的部分。也可以用 ReentrantLock 类,它提供了更灵活的锁控制。
面试官:那线程池有哪些参数,分别有什么作用。
王铁牛:线程池的参数有 corePoolSize(核心线程数),当提交的任务数小于它时,会创建新线程执行任务;maximumPoolSize(最大线程数),当任务数超过核心线程数且队列满时,会创建线程直到达到这个数;keepAliveTime(线程存活时间),当线程数大于核心线程数时,多余的线程在空闲时会在这个时间后被销毁;unit(时间单位),指定 keepAliveTime 的时间单位;workQueue(任务队列),用来存放提交的任务;threadFactory(线程工厂),用于创建线程;handler(拒绝策略),当线程数达到最大且队列满时,如何处理新提交的任务。
面试官:说说 HashMap 的底层实现原理。
王铁牛:HashMap 底层是数组 + 链表 + 红黑树的结构。当插入元素时,先计算 key 的哈希值,然后通过哈希值找到对应的数组下标,如果该下标为空,则直接插入新节点;如果不为空,则判断 key 是否相同,相同则覆盖值,不同则将新节点插入链表或红黑树中。
第二轮结束。
面试官:简述一下 Spring 的核心特性。
王铁牛:Spring 的核心特性有依赖注入,能方便地将对象之间的依赖关系进行管理;面向切面编程,可以在不修改原有代码的基础上,动态地添加功能;IoC 容器,负责创建、配置和管理对象。
面试官:那 Spring Boot 自动配置的原理是什么。
王铁牛:Spring Boot 自动配置是通过条件注解来实现的。它会根据项目中引入的依赖和配置信息,自动配置相应的 bean。比如引入了数据库相关的依赖,就会自动配置数据源等相关的 bean。
面试官:MyBatis 中 #{} 和 ${} 的区别是什么。
王铁牛:#{} 是预编译处理,能防止 SQL 注入,它会将参数用? 代替,然后通过 PreparedStatement 设置参数值。${} 是字符串替换,不会进行预编译,可能会存在 SQL 注入风险,它会直接将参数的值拼接到 SQL 中。
面试官:请回家等通知。
面试结束,王铁牛在回答一些简单问题时表现尚可,但对于复杂问题回答得比较混乱,没有清晰地阐述技术要点。面试官会综合评估其表现后决定是否给予录用通知。
答案:
- Java 面向对象三大特性:
- 封装:把对象的属性和方法结合成一个独立的整体,对外提供统一的访问接口。这样可以隐藏对象内部的实现细节,提高代码的安全性和可维护性。例如,一个类中的私有属性,只能通过该类提供的公共方法来访问和修改。
- 继承:一个类可以继承另一个类的属性和方法。通过继承,可以实现代码的复用,子类可以继承父类的非私有属性和方法,并且可以根据自身需求进行扩展。比如,定义一个父类 Animal,子类 Dog 可以继承 Animal 的属性(如 name、age 等)和方法(如 eat() 方法),同时 Dog 类还可以添加自己特有的方法,如 bark() 方法。
- 多态:同一个行为具有不同表现形式或形态的能力。多态可以分为编译时多态(方法重载)和运行时多态(方法重写)。方法重载是指在同一个类中定义多个同名但参数列表不同的方法,编译器会根据调用方法时传入的参数类型和个数来决定调用哪个方法。方法重写是指子类继承父类后,重新定义父类中的某个方法,当通过子类对象调用这个方法时,实际执行的是子类重写后的方法。例如,定义一个父类 Shape,子类 Circle 和 Rectangle 继承 Shape 类并重写 draw() 方法,当通过 Shape 类型的变量调用 draw() 方法时,会根据实际指向的对象是 Circle 还是 Rectangle 来执行相应的 draw 操作。
- JUC 包下常用类及其作用:
- CountDownLatch:用于让一个线程等待其他线程完成一系列操作后再继续执行。它有一个计数器,通过调用 countDown() 方法可以使计数器减 1,当计数器的值变为 0 时,等待的线程会被唤醒继续执行。例如,多个线程需要完成一些初始化操作后,主线程才能继续执行,可以使用 CountDownLatch 来实现。假设有三个线程负责初始化资源,主线程在 CountDownLatch 上调用 await() 方法等待,三个初始化线程完成任务后分别调用 countDown() 方法,当计数器变为 0 时,主线程被唤醒继续执行后续操作。
- CyclicBarrier:能让一组线程互相等待,直到所有线程都到达某个屏障点,然后再一起继续执行。它内部也有一个计数器,当每个线程调用 await() 方法时,计数器会减 1,当计数器的值变为 0 时,所有等待的线程会被唤醒,继续执行后续代码。例如,有多个线程需要共同完成一个复杂任务,每个线程负责一部分,当所有线程都完成自己的部分后,一起进行汇总处理。可以使用 CyclicBarrier 来实现,在所有线程的任务代码后调用 await() 方法,当所有线程都调用了 await() 方法且计数器变为 0 时,就可以执行汇总处理的代码。
- Java 内存区域:
- 程序计数器:记录着当前线程执行的字节码指令地址。它是线程私有的,每个线程都有自己独立的程序计数器。程序计数器的作用是保证线程可以准确地执行下一条指令,在多线程环境下,不同线程可以切换执行,程序计数器会记录每个线程当前执行到的位置,以便在切换回来时能继续正确执行。
- 虚拟机栈:存放着局部变量表、操作数栈等。它也是线程私有的,每个方法在执行时会创建一个对应的栈帧,栈帧中包含局部变量表和操作数栈等。局部变量表用于存放方法内定义的局部变量,操作数栈用于存储方法执行过程中的操作数和中间结果。当方法执行结束,对应的栈帧会出栈销毁。
- 本地方法栈:用于执行本地方法。本地方法是用其他语言(如 C、C++)实现的方法,在 Java 中通过 JNI(Java Native Interface)来调用本地方法。本地方法栈与虚拟机栈类似,也是线程私有的,负责管理本地方法调用过程中的栈帧。
- 堆:是对象实例的存放地。堆是 Java 内存中最大的一块区域,被所有线程共享。对象在堆中分配内存,当对象不再被引用时,会被垃圾回收机制回收。堆可以分为新生代、老年代和永久代(在 JDK 8 及以后,永久代被元空间取代)。新生代主要用于存放新创建的对象,老年代用于存放经过多次垃圾回收后仍然存活的对象,永久代(元空间)用于存放类信息、常量、静态变量等。
- 方法区:存储类信息、常量、静态变量等。它也是被所有线程共享的区域。方法区中的类信息包含类的结构、字段、方法等信息,常量池用于存放常量,静态变量在方法区中分配内存。在 JDK 8 及以后,元空间取代了永久代,元空间使用本地内存,而不是像永久代那样在堆中分配空间。
- 多线程中实现线程同步的方法:
- 使用 synchronized 关键字:
- 可以修饰方法,被修饰的方法在同一时刻只能被一个线程访问。例如:
public class SynchronizedExample { public synchronized void method() { // 方法体 } } - 也可以修饰代码块,指定锁对象,只有获取到该锁对象的线程才能执行代码块中的内容。例如:
public class SynchronizedExample { private final Object lock = new Object(); public void method() { synchronized (lock) { // 代码块 } } }
- 可以修饰方法,被修饰的方法在同一时刻只能被一个线程访问。例如:
- 使用 ReentrantLock 类:
- ReentrantLock 提供了更灵活的锁控制。它可以实现公平锁和非公平锁,默认是非公平锁。例如:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); try { // 代码块 } finally { lock.unlock(); } } } - 它还提供了一些其他方法,如 tryLock() 方法可以尝试获取锁,若获取成功返回 true,否则返回 false,不会阻塞线程;lockInterruptibly() 方法可以在获取锁的过程中响应中断。
- ReentrantLock 提供了更灵活的锁控制。它可以实现公平锁和非公平锁,默认是非公平锁。例如:
- 使用 synchronized 关键字:
- 线程池的参数及其作用:
- corePoolSize(核心线程数):当提交的任务数小于它时,会创建新线程执行任务。核心线程会一直存活在线程池中,除非设置了 allowCoreThreadTimeOut 为 true。例如,线程池的 corePoolSize 为 5,当提交的任务数小于 5 时,会创建新线程来执行任务。
- maximumPoolSize(最大线程数):当任务数超过核心线程数且队列满时,会创建线程直到达到这个数。如果任务继续增加,超过最大线程数后,会根据拒绝策略进行处理。比如,线程池的 maximumPoolSize 为 10,corePoolSize 为 5,当提交的任务数大于 5 且任务队列已满时,会创建新线程直到线程数达到 10。
- keepAliveTime(线程存活时间):当线程数大于核心线程数时,多余的线程在空闲时会在这个时间后被销毁。例如,keepAliveTime 为 60 秒,当线程数超过 corePoolSize 后,空闲线程超过 60 秒没有任务执行,就会被销毁。
- unit(时间单位):指定 keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒,TimeUnit.MINUTES 表示分钟等。
- workQueue(任务队列):用来存放提交的任务。当提交的任务数超过 corePoolSize 时,会将任务放入任务队列中。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。例如,使用 ArrayBlockingQueue 作为任务队列,它有一个固定的容量,当任务数超过这个容量且线程数未达到 maximumPoolSize 时,会根据拒绝策略处理新任务。
- threadFactory(线程工厂):用于创建线程。可以通过自定义线程工厂来设置线程的名称、优先级等属性。例如:
import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class MyThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; public MyThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s!= null)? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) { t.setDaemon(false); } if (t.getPriority()!= Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } - handler(拒绝策略):当线程数达到最大且队列满时,如何处理新提交的任务。常见的拒绝策略有 AbortPolicy(抛出 RejectedExecutionException 异常)、CallerRunsPolicy(调用者线程执行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试重新提交新任务)。例如,使用 AbortPolicy 拒绝策略,当线程池无法处理新任务时,会抛出异常。
- HashMap 的底层实现原理:
- HashMap 底层是数组 + 链表 + 红黑树的结构。
- 当插入元素时,先计算 key 的哈希值,然后通过哈希值找到对应的数组下标:
- 计算哈希值:通过 key 的 hashCode() 方法计算出哈希值,然后对哈希值进行扰动处理,使哈希值更均匀地分布。例如:
static final int hash(Object key) { int h; return (key == null)? 0 : (h = key.hashCode()) ^ (h >>> 16); } - 找到数组下标:用计算得到的哈希值与数组长度减 1 进行按位与操作,得到数组下标。例如:
(n - 1) & hash
- 计算哈希值:通过 key 的 hashCode() 方法计算出哈希值,然后对哈希值进行扰动处理,使哈希值更均匀地分布。例如:
- 如果该下标为空,则直接插入新节点;如果不为空,则判断 key 是否相同:
- 相同则覆盖值。通过比较 key 的 hash 值和 equals() 方法来判断 key 是否相同。例如:
if (p.hash == hash && ((k = p.key) == key || (key!= null && key.equals(k)))) { e = p; } - 不同则将新节点插入链表或红黑树中:
- 如果链表长度小于 8 且当前节点数小于 64(JDK 8 中的优化),则将新节点插入链表尾部。例如:
if (binCount < TREEIFY_THRESHOLD) { for (TreeNode<K,V> p = tail; p!= null; p = p.prev) { if ((e.hash ^ p.hash) < 0) { e.prev = p.prev; if (p.prev == null) head = e; else p.prev.next = e; p.next = e; break; } } } - 如果链表长度大于等于 8 且当前节点数大于等于 64,则将链表转换为红黑树,将新节点插入红黑树中。例如:
if (binCount >= TREEIFY_THRESHOLD) { treeifyBin(tab, hash); }
- 如果链表长度小于 8 且当前节点数小于 64(JDK 8 中的优化),则将新节点插入链表尾部。例如:
- 相同则覆盖值。通过比较 key 的 hash 值和 equals() 方法来判断 key 是否相同。例如:
- Spring 的核心特性:
- 依赖注入:能方便地将对象之间的依赖关系进行管理。通过依赖注入,对象不需要自己创建依赖对象,而是由容器将依赖对象注入到需要的对象中。例如,一个 Service 类依赖于一个 Dao 类,在 Spring 中,可以通过配置将 Dao 对象注入到 Service 类中。可以使用构造器注入、setter 方法注入等方式。例如构造器注入:
public class UserService { private UserDao userDao; public UserService(UserDao userDao) { this.userDao = userDao; } } - 面向切面编程(AOP):可以在不修改原有代码的基础上,动态地添加功能。通过 AOP,可以将一些通用的功能(如日志记录、事务管理等)从业务逻辑中分离出来,以切面的形式织入到业务逻辑中。例如,使用 @Aspect 注解定义切面类,通过切入点表达式指定要织入切面的位置,使用通知方法定义具体的增强逻辑。如:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class LogAspect { @Before("execution(* com.example.demo.service.*.*(..))") public void logBefore() { System.out.println("方法执行前记录日志"); } } - **IoC
- 依赖注入:能方便地将对象之间的依赖关系进行管理。通过依赖注入,对象不需要自己创建依赖对象,而是由容器将依赖对象注入到需要的对象中。例如,一个 Service 类依赖于一个 Dao 类,在 Spring 中,可以通过配置将 Dao 对象注入到 Service 类中。可以使用构造器注入、setter 方法注入等方式。例如构造器注入: