《互联网大厂面试:Java核心知识、JUC、JVM等全方位考察》
在互联网大厂的一间安静的面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的王铁牛。面试开始,一场关于Java技术知识的考验拉开帷幕。
第一轮提问
- 面试官:首先问几个基础的Java核心知识问题。Java中基本数据类型有哪些?
- 王铁牛:嗯,有byte、short、int、long、float、double、char、boolean。
- 面试官:回答正确。那Java中多态的实现方式有哪些?
- 王铁牛:主要有继承和接口实现,通过方法重写和方法重载来体现多态。
- 面试官:非常好。那说说String、StringBuilder和StringBuffer的区别。
- 王铁牛:String是不可变的,每次对String的操作都会生成新的对象;StringBuilder是非线程安全的,性能较高;StringBuffer是线程安全的,性能相对较低。
- 面试官:不错,基础很扎实。
第二轮提问
- 面试官:接下来考察一下JUC和多线程方面的知识。什么是线程池?
- 王铁牛:线程池就是预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。
- 面试官:很好。那线程池有哪些常用的创建方式?
- 王铁牛:可以通过Executors工厂类创建,比如newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor等。
- 面试官:那说说线程池的工作流程。
- 王铁牛:呃……就是有任务来了就执行呗,执行完就释放线程。(回答不清晰)
- 面试官:这个回答不太准确,需要更深入理解。再问一个,JUC中常用的同步工具类有哪些?
- 王铁牛:嗯……好像有CountDownLatch、CyclicBarrier,其他的不太记得了。
第三轮提问
- 面试官:现在考察框架相关知识。Spring中Bean的生命周期是怎样的?
- 王铁牛:呃……就是创建、初始化,然后销毁吧。(回答不清晰)
- 面试官:这样回答太笼统了。那Spring Boot的自动配置原理是什么?
- 王铁牛:好像是根据依赖自动配置一些东西,具体不太清楚。(回答不清晰)
- 面试官:那MyBatis中#{}和${}的区别是什么?
- 王铁牛:嗯……好像一个安全,一个不安全,但具体不太明白。(回答不清晰)
- 面试官:看来你对框架的理解还不够深入。
面试接近尾声,面试官表情严肃地说:“今天的面试就到这里,你先回家等通知吧。我们后续会综合评估你的表现,有结果会及时通知你。”
问题答案详细解析
- Java中基本数据类型有哪些?
- Java中有8种基本数据类型,分为4类:
- 整数类型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)。
- 浮点类型:float(4字节)、double(8字节)。
- 字符类型:char(2字节)。
- 布尔类型:boolean(理论上没有明确大小,实际实现中通常是1字节)。
- Java中有8种基本数据类型,分为4类:
- Java中多态的实现方式有哪些?
- 多态是指同一个行为具有多个不同表现形式或形态的能力。Java中多态的实现方式主要有:
- 继承:子类继承父类,重写父类的方法,通过父类引用指向子类对象,调用重写的方法实现多态。例如:
- 多态是指同一个行为具有多个不同表现形式或形态的能力。Java中多态的实现方式主要有:
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Dog();
animal.sound();
}
}
- 接口实现:类实现接口,重写接口中的方法,通过接口引用指向实现类对象,调用重写的方法实现多态。
- 方法重载:在同一个类中,方法名相同,但参数列表不同(参数个数、类型、顺序不同),根据调用时传入的参数不同来调用不同的方法。
3. String、StringBuilder和StringBuffer的区别 - String:是不可变的,一旦创建,其值不能被修改。每次对String进行操作(如拼接、替换等)都会创建一个新的String对象,因此频繁操作String会产生大量的临时对象,影响性能。 - StringBuilder:是可变的,非线程安全的。它的内部使用可变的字符数组来存储字符串,在进行字符串拼接等操作时,不会创建新的对象,而是直接在原数组上进行修改,因此性能较高。适用于单线程环境下的字符串操作。 - StringBuffer:也是可变的,但它是线程安全的。它的方法大多使用了synchronized关键字进行同步,保证了在多线程环境下操作的安全性,但由于同步会带来一定的性能开销,所以性能相对StringBuilder较低。适用于多线程环境下的字符串操作。 4. 什么是线程池? - 线程池是一种线程使用模式。线程池预先创建好一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。任务执行完后,线程不会销毁,而是返回线程池,等待下一个任务。使用线程池可以减少线程创建和销毁的开销,提高系统的性能和资源利用率,同时可以更好地管理线程的数量,避免创建过多线程导致系统资源耗尽。 5. 线程池有哪些常用的创建方式? - Java中可以通过Executors工厂类创建线程池,常用的创建方式有: - newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量固定不变。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入队列中等待。例如:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
- newCachedThreadPool:创建一个可缓存的线程池,线程池中的线程数量可以根据需要动态调整。如果有新任务提交,而线程池中没有空闲线程,则会创建新的线程来执行任务;如果线程在一段时间内没有被使用,则会被销毁。例如:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- newSingleThreadExecutor:创建一个单线程的线程池,线程池中只有一个线程。所有任务会按照提交的顺序依次执行。例如:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 线程池的工作流程
- 当有新任务提交到线程池时,线程池的工作流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下一步。
- 线程池判断工作队列是否已经满了。如果工作队列没有满,则将新任务存储在这个工作队列里。如果工作队列满了,则进入下一步。
- 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
- 常见的饱和策略有:AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理该任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试执行新任务)。
- 当有新任务提交到线程池时,线程池的工作流程如下:
- JUC中常用的同步工具类有哪些?
- JUC(java.util.concurrent)是Java提供的并发包,其中常用的同步工具类有:
- CountDownLatch:允许一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,计数器的初始值为线程的数量。当一个线程完成任务后,计数器的值减1,当计数器的值为0时,等待的线程可以继续执行。例如:
- JUC(java.util.concurrent)是Java提供的并发包,其中常用的同步工具类有:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads have finished, main thread can continue");
}
}
- CyclicBarrier:允许一组线程相互等待,直到所有线程都到达一个屏障点,然后所有线程可以继续执行。它可以重复使用,当所有线程都到达屏障点后,屏障会被重置。例如:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads have reached the barrier");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working");
try {
Thread.sleep(1000);
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has passed the barrier");
}).start();
}
}
}
- Semaphore:用于控制同时访问某个资源的线程数量。它通过一个许可证来实现,线程在访问资源前需要获取许可证,访问完后释放许可证。例如:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " has acquired the semaphore");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " has released the semaphore");
}
}).start();
}
}
}
- Spring中Bean的生命周期是怎样的?
- Spring中Bean的生命周期包括以下几个主要阶段:
- 实例化:通过反射创建Bean的实例。
- 属性赋值:为Bean的属性注入值。
- 初始化:
- 实现InitializingBean接口的afterPropertiesSet方法。
- 自定义的init-method方法。
- 使用:Bean可以被使用。
- 销毁:
- 实现DisposableBean接口的destroy方法。
- 自定义的destroy-method方法。
- 例如,定义一个实现InitializingBean和DisposableBean接口的Bean:
- Spring中Bean的生命周期包括以下几个主要阶段:
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class MyBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("MyBean is initializing");
}
@Override
public void destroy() throws Exception {
System.out.println("MyBean is destroying");
}
}
- Spring Boot的自动配置原理是什么?
- Spring Boot的自动配置是基于条件注解和类路径扫描实现的。其核心原理如下:
- Spring Boot启动时,会通过@SpringBootApplication注解开启自动配置功能,该注解包含了@EnableAutoConfiguration注解。
- @EnableAutoConfiguration注解会导入AutoConfigurationImportSelector类,该类会读取META-INF/spring.factories文件,该文件中定义了所有的自动配置类。
- Spring Boot会根据类路径中存在的类和配置文件中的属性,使用条件注解(如@ConditionalOnClass、@ConditionalOnMissingBean等)来判断是否需要加载某个自动配置类。如果满足条件,则会加载该自动配置类,为应用程序自动配置相应的Bean。
- 例如,当类路径中存在DataSource类时,Spring Boot会自动配置数据源相关的Bean。
- Spring Boot的自动配置是基于条件注解和类路径扫描实现的。其核心原理如下:
- MyBatis中#{}和${}的区别是什么?
- #{}:是预编译处理,MyBatis会将#{}替换为占位符?,然后使用PreparedStatement进行参数设置。这样可以防止SQL注入攻击,因为参数会被自动进行类型转换和转义处理。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis会直接将${}替换为传入的参数值。这种方式存在SQL注入风险,因为参数不会进行任何处理,直接拼接到SQL语句中。例如:
<select id="getUserByColumnName" parameterType="String" resultType="User">
SELECT * FROM users WHERE ${columnName} = 'value'
</select>
- 一般情况下,尽量使用#{}来防止SQL注入,只有在需要动态传入表名、列名等情况时才使用${},但要确保传入的参数是安全的。