互联网大厂面试:Java 核心知识与热门技术大考验
在互联网大厂的一间安静面试室内,严肃的面试官坐在桌前,对面是略显紧张的求职者王铁牛。面试正式开始。
第一轮提问 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:嗯,有 byte、short、int、long、float、double、char、boolean。 面试官:回答正确,不错。那 Java 里的访问修饰符有几种,分别有什么作用? 王铁牛:有四种,public 可以被任意访问;protected 可以被同一个包内的类和不同包的子类访问;默认(不写修饰符)只能被同一个包内的类访问;private 只能在本类中访问。 面试官:非常好。那说说 Java 中多态的实现方式有哪些? 王铁牛:主要有两种,方法重载和方法重写。方法重载是在同一个类中,方法名相同但参数列表不同;方法重写是子类重写父类的方法。 面试官:回答得很清晰。看来你对 Java 核心知识掌握得不错。
第二轮提问 面试官:接下来聊聊 JUC 和多线程相关的。什么是线程安全? 王铁牛:线程安全就是在多线程环境下,对共享资源的访问不会产生数据不一致等问题。 面试官:回答正确。那 Java 中实现线程的方式有哪些? 王铁牛:有三种,继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。 面试官:很好。那说说线程池的好处有哪些? 王铁牛:线程池可以降低资源消耗,提高响应速度,还能方便管理线程。 面试官:不错。那你知道线程池的核心参数有哪些吗? 王铁牛:这个……好像有最大线程数、核心线程数,其他的我有点记不清了。 面试官:整体回答还行,不过线程池核心参数这块还需要再巩固。
第三轮提问 面试官:我们再深入一点,谈谈 Spring 和 Spring Boot。Spring 的核心特性有哪些? 王铁牛:有依赖注入和面向切面编程。依赖注入可以降低代码的耦合度,面向切面编程可以实现日志记录、事务管理等功能。 面试官:回答得可以。那 Spring Boot 的自动配置原理是什么? 王铁牛:这个……好像是根据类路径下的依赖和配置文件自动配置一些 Bean,具体我也说不太清楚。 面试官:看来你有一定的了解,但不够深入。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:#{} 是预编译处理,能防止 SQL 注入,{} 是字符串替换,可能会有 SQL 注入风险。 面试官:这个回答对了。最后问你,Dubbo、RabbitMq、xxl - job、Redis 这几个技术里,你对哪个比较熟悉,简单说下它的应用场景。 王铁牛:我对 Redis 有点了解,它可以用来做缓存,提高系统的响应速度,其他的我不太清楚。
面试接近尾声,面试官看着王铁牛说道:“今天的面试就到这里了。你对一些基础的知识掌握得还可以,像 Java 核心知识和部分多线程、MyBatis 的内容回答得不错。不过对于一些复杂的知识点,比如线程池核心参数、Spring Boot 自动配置原理等,回答得不够完整和深入。我们需要综合评估所有面试者的情况,你先回家等通知吧,后续有结果会及时联系你。”
答案详解
- Java 基本数据类型:
- Java 中有 8 种基本数据类型,其中 4 种整数类型(byte 占 1 个字节,范围 -128 到 127;short 占 2 个字节;int 占 4 个字节,是最常用的整数类型;long 占 8 个字节,通常在需要表示大整数时使用),2 种浮点类型(float 占 4 个字节,单精度浮点型;double 占 8 个字节,双精度浮点型,精度更高),1 种字符类型(char 占 2 个字节,用于表示单个字符),1 种布尔类型(boolean 只有两个值 true 和 false)。
- Java 访问修饰符:
- public:访问权限最大,类、接口、变量、方法等都可以用 public 修饰,任何地方都可以访问。
- protected:同一个包内的类可以访问,不同包的子类也可以访问。通常用于父类希望子类能访问某些成员,但又不希望其他无关类访问的情况。
- 默认(不写修饰符):只能被同一个包内的类访问,提供了一定的封装性。
- private:访问权限最小,只能在本类中访问,用于封装类的内部状态,防止外部直接访问和修改。
- Java 多态的实现方式:
- 方法重载(Overloading):在同一个类中,方法名相同但参数列表不同(参数的类型、个数、顺序不同)。调用时根据传入的参数来决定调用哪个方法,它是一种编译时多态。例如:
public class OverloadExample {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
- 方法重写(Overriding):子类重写父类的方法,方法名、参数列表和返回值类型都要相同(返回值类型可以是父类返回值类型的子类,称为协变返回类型)。重写的方法不能比父类被重写的方法有更严格的访问权限。它是一种运行时多态,根据实际创建的对象类型来决定调用哪个类的方法。例如:
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");
}
}
- 线程安全:在多线程环境下,多个线程可能会同时访问和修改共享资源,如果没有合适的同步机制,就可能会导致数据不一致、脏读、幻读等问题。线程安全的代码在多线程环境下能保证数据的一致性和正确性。例如,多个线程同时对一个计数器进行自增操作,如果没有同步,可能会出现计数错误。
- Java 实现线程的方式:
- 继承 Thread 类:创建一个类继承 Thread 类,重写 run() 方法,在 run() 方法中定义线程要执行的任务。然后创建该类的对象并调用 start() 方法启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
// 使用
MyThread thread = new MyThread();
thread.start();
- 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run() 方法。然后创建该类的对象,并将其作为参数传递给 Thread 类的构造函数,最后调用 Thread 对象的 start() 方法启动线程。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
// 使用
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
- 实现 Callable 接口:创建一个类实现 Callable 接口,实现 call() 方法,该方法有返回值。然后使用 FutureTask 包装 Callable 对象,再将 FutureTask 对象传递给 Thread 类的构造函数启动线程。可以通过 FutureTask 的 get() 方法获取线程执行的结果。例如:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
// 使用
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 线程池的好处:
- 降低资源消耗:创建和销毁线程需要消耗系统资源,线程池可以复用已经创建的线程,减少了线程创建和销毁的开销。
- 提高响应速度:当有任务提交时,线程池中有空闲线程可以立即执行任务,不需要等待线程创建。
- 方便管理线程:可以对线程进行统一的管理,如设置线程的最大数量、核心数量、任务队列等,避免创建过多线程导致系统资源耗尽。
- 线程池的核心参数:
- corePoolSize:核心线程数,线程池初始化时创建的线程数量,当有任务提交时,首先会使用核心线程来执行任务。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数量。当任务队列满了,且线程数量达到核心线程数时,会创建新的线程直到达到最大线程数。
- keepAliveTime:线程空闲时间,当线程空闲时间超过这个值时,非核心线程会被销毁。
- unit:keepAliveTime 的时间单位,如 TimeUnit.SECONDS 表示秒。
- workQueue:任务队列,用于存储提交的任务。常见的任务队列有 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。
- threadFactory:线程工厂,用于创建线程,可以自定义线程的名称、优先级等属性。
- handler:拒绝策略,当任务队列满了且线程数量达到最大线程数时,新提交的任务会触发拒绝策略。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(由提交任务的线程执行任务)等。
- Spring 的核心特性:
- 依赖注入(Dependency Injection,DI):通过将对象的依赖关系从对象内部转移到外部,降低了代码的耦合度。例如,一个类需要使用另一个类的对象,不需要在类内部创建该对象,而是通过构造函数、Setter 方法等方式将对象注入进来。
- 面向切面编程(Aspect - Oriented Programming,AOP):可以在不修改原有代码的情况下,对程序进行增强,如日志记录、事务管理等。AOP 通过定义切面(Aspect)、切点(Pointcut)和通知(Advice)来实现。切面是包含多个通知和切点的模块,切点定义了哪些方法会被增强,通知定义了在方法执行的不同阶段(如前置、后置、环绕等)执行的代码。
- Spring Boot 的自动配置原理: Spring Boot 基于约定大于配置的理念,通过类路径下的依赖和配置文件来自动配置 Spring 应用。其核心是自动配置类(通常以 AutoConfiguration 结尾),这些类使用 @Configuration 注解标记。在启动时,Spring Boot 的自动配置机制会根据类路径下的依赖和配置文件,通过 @Conditional 系列注解来判断是否满足自动配置的条件,如果满足则会自动配置相应的 Bean。例如,如果类路径下存在 MyBatis 的依赖,Spring Boot 会自动配置 MyBatis 的相关 Bean。
- MyBatis 中 #{} 和 ${} 的区别:
- #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符?,然后使用 PreparedStatement 进行参数设置,可以有效防止 SQL 注入。例如:
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
- ${}:是字符串替换,MyBatis 会直接将 ${} 替换为传入的参数值,可能会导致 SQL 注入。通常用于动态表名、列名等情况。例如:
<select id="getUsersByTable" parameterType="String" resultType="User">
SELECT * FROM ${tableName}
</select>
- Redis 的应用场景:
- 缓存:将经常访问的数据存储在 Redis 中,当有请求时先从 Redis 中获取数据,如果没有再从数据库中获取并更新到 Redis 中,可以大大提高系统的响应速度。例如,将热门商品信息、用户登录信息等存储在 Redis 中。
- 分布式锁:在分布式系统中,多个服务可能会同时访问共享资源,使用 Redis 可以实现分布式锁,保证同一时间只有一个服务可以访问共享资源。
- 消息队列:Redis 提供了 List 数据结构,可以实现简单的消息队列。生产者将消息放入 List 中,消费者从 List 中取出消息进行处理。
- 计数器:Redis 的原子操作可以方便地实现计数器功能,如统计网站的访问量、文章的阅读量等。