《互联网大厂 Java 面试:核心知识、框架与中间件大考验》

55 阅读9分钟

互联网大厂 Java 面试:核心知识、框架与中间件大考验

严肃的面试官坐在办公桌后,面前放着求职者王铁牛的简历。王铁牛紧张地走进面试房间,坐下后,面试开始了。

第一轮提问 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:Java 的基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那在 Java 里,什么是自动装箱和自动拆箱? 王铁牛:自动装箱就是把基本数据类型自动转换为对应的包装类,比如把 int 转换为 Integer;自动拆箱就是把包装类自动转换为基本数据类型,像把 Integer 转换为 int。 面试官:回答得很好。那说说 Java 中 static 关键字的作用有哪些? 王铁牛:static 关键字可以修饰变量、方法、代码块和内部类。修饰变量时,这个变量属于类,而不是类的实例;修饰方法时,该方法可以直接通过类名调用;修饰代码块时,会在类加载时执行且只执行一次;修饰内部类时,这个内部类可以不依赖外部类的实例而被实例化。 面试官:非常棒,看来你对 Java 核心知识掌握得很扎实。

第二轮提问 面试官:接下来问一些 JUC、JVM 和多线程相关的问题。JUC 里的 CountDownLatch 是做什么用的? 王铁牛:CountDownLatch 主要用于让一个或多个线程等待其他线程完成操作。它有一个计数器,初始化时设置一个值,当一个线程完成任务后,计数器减 1,当计数器为 0 时,等待的线程就可以继续执行。 面试官:回答正确。那 JVM 的内存区域是如何划分的? 王铁牛:JVM 内存区域主要分为堆、栈、方法区、本地方法栈和程序计数器。堆是存放对象实例的地方;栈主要存储局部变量和方法调用信息;方法区存储类的信息、常量、静态变量等;本地方法栈为本地方法服务;程序计数器记录当前线程执行的字节码的行号。 面试官:还不错。那在多线程环境下,如何避免死锁? 王铁牛:可以通过一些方法避免死锁,比如按顺序加锁,避免一个线程同时获取多个锁;设置锁的超时时间,如果在一定时间内没有获取到锁就放弃;使用定时锁等。 面试官:回答得挺全面的,你对这块知识也有一定的理解。

第三轮提问 面试官:现在问一些关于框架和中间件的问题。Spring 中的 IOC 和 AOP 是什么? 王铁牛:IOC 是控制反转,就是把对象的创建和依赖关系的管理交给 Spring 容器,而不是由对象自己去创建和管理依赖。AOP 是面向切面编程,它可以在不修改原有代码的情况下,对程序进行增强,比如实现日志记录、事务管理等功能。 面试官:有一定的理解。那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:这个……嗯……好像 #{} 是预编译的,能防止 SQL 注入,{} 是直接替换,可能会有 SQL 注入风险。 面试官:回答得不太完整。那 Dubbo 的负载均衡策略有哪些? 王铁牛:嗯……好像有随机、轮询什么的,具体我有点记不太清了。 面试官:看来你对框架和中间件的掌握还不够深入。

面试结束后,面试官整理了一下资料,对王铁牛说:“今天的面试就到这里,你在 Java 核心知识、JUC、JVM 和多线程方面表现得还不错,对一些基础概念有较好的理解和掌握。但在框架和中间件部分,回答得不够完整和深入,有些问题没有准确全面地回答出来。我们需要综合评估后再做决定,你先回家等通知吧。”

问题答案详细解释

  1. Java 中基本数据类型有哪些? Java 的基本数据类型分为 4 类 8 种。
    • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
    • 浮点类型:float(4 字节)、double(8 字节)。
    • 字符类型:char(2 字节)。
    • 布尔类型:boolean(理论上 1 位,但在 Java 里没有明确规定大小)。
  2. 在 Java 里,什么是自动装箱和自动拆箱? 自动装箱是 Java 编译器在基本数据类型和对应的包装类之间进行的自动转换。例如:
Integer i = 10; // 自动装箱,相当于 Integer i = Integer.valueOf(10);

自动拆箱则是把包装类对象转换为基本数据类型。例如:

int j = i; // 自动拆箱,相当于 int j = i.intValue();
  1. 说说 Java 中 static 关键字的作用有哪些?
    • 静态变量:被 static 修饰的变量属于类,而不是类的实例。所有实例共享同一个静态变量,可通过类名直接访问。例如:
class Test {
    static int num = 10;
}
// 访问静态变量
int value = Test.num;
- **静态方法**:静态方法不依赖于类的实例,可以直接通过类名调用。静态方法中不能使用 thissuper 关键字,也不能直接访问非静态成员。例如:
class Test {
    static void print() {
        System.out.println("This is a static method.");
    }
}
// 调用静态方法
Test.print();
- **静态代码块**:静态代码块在类加载时执行,且只执行一次,常用于初始化静态变量。例如:
class Test {
    static {
        System.out.println("Static block is executed.");
    }
}
- **静态内部类**:静态内部类不依赖于外部类的实例,可以直接创建对象。例如:
class Outer {
    static class Inner {
        void print() {
            System.out.println("This is a static inner class.");
        }
    }
}
// 创建静态内部类对象
Outer.Inner inner = new Outer.Inner();
inner.print();
  1. JUC 里的 CountDownLatch 是做什么用的? CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。使用步骤如下:
    • 创建 CountDownLatch 对象,并指定计数器的初始值。
    • 在等待的线程中调用 CountDownLatch 的 await() 方法,线程会阻塞直到计数器为 0。
    • 在其他线程完成任务后,调用 CountDownLatch 的 countDown() 方法,计数器减 1。 例如:
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(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task completed by thread " + Thread.currentThread().getName());
                latch.countDown();
            }).start();
        }

        // 主线程等待其他线程完成任务
        latch.await();
        System.out.println("All tasks are completed.");
    }
}
  1. JVM 的内存区域是如何划分的?
    • 堆(Heap):是 JVM 中最大的一块内存区域,用于存放对象实例和数组。所有通过 new 创建的对象都存放在堆中,堆是线程共享的。
    • 栈(Stack):每个线程都有自己的栈,栈中存储局部变量、方法调用信息和操作数栈等。栈是线程私有的,每个方法执行时会创建一个栈帧,方法执行结束后栈帧出栈。
    • 方法区(Method Area):用于存储类的信息、常量、静态变量、编译后的代码等。方法区是线程共享的,在 JDK 1.8 之前叫永久代,JDK 1.8 及以后叫元空间。
    • 本地方法栈(Native Method Stack):与栈类似,不过它是为本地方法服务的,本地方法是用其他语言(如 C、C++)编写的方法。
    • 程序计数器(Program Counter Register):记录当前线程执行的字节码的行号,是线程私有的。
  2. 在多线程环境下,如何避免死锁? 死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象。可以通过以下方法避免死锁:
    • 按顺序加锁:所有线程按照相同的顺序获取锁,避免循环等待。例如,线程 A 和线程 B 都需要获取锁 1 和锁 2,那么都按照先获取锁 1 再获取锁 2 的顺序。
    • 设置锁的超时时间:使用带有超时时间的锁获取方法,如果在一定时间内没有获取到锁就放弃,避免无限等待。例如,使用 ReentrantLock 的 tryLock(long timeout, TimeUnit unit) 方法。
    • 使用定时锁:在获取锁时设置一个时间限制,如果超过这个时间还没有获取到锁,就进行相应的处理。
    • 减少锁的持有时间:尽量缩短线程持有锁的时间,减少死锁的可能性。
  3. Spring 中的 IOC 和 AOP 是什么?
    • IOC(Inversion of Control,控制反转):是一种设计思想,将对象的创建和依赖关系的管理从对象本身转移到 Spring 容器中。Spring 容器负责创建对象、管理对象的生命周期和依赖关系。例如:
<bean id="userService" class="com.example.service.UserService">
    <property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.example.dao.UserDao"/>

在上述配置中,Spring 容器会创建 UserService 和 UserDao 对象,并将 UserDao 对象注入到 UserService 中。 - AOP(Aspect-Oriented Programming,面向切面编程):是对面向对象编程的一种补充,它允许在不修改原有代码的情况下,对程序进行增强。AOP 的主要概念包括切面(Aspect)、通知(Advice)、连接点(Join Point)和切入点(Pointcut)。例如,使用 AOP 实现日志记录:

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("Before method execution");
    }
}

上述代码中,LoggingAspect 是一个切面,beforeAdvice 是一个前置通知,在 com.example.service 包下的所有方法执行前会执行该通知。 8. MyBatis 中 #{} 和 ${} 的区别是什么? - #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符?,然后使用 PreparedStatement 进行预编译,能有效防止 SQL 注入。例如:

<select id="getUserById" parameterType="int" resultType="com.example.entity.User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 在处理 ${} 时,会将 ${} 直接替换为传入的值,可能会导致 SQL 注入。例如:
<select id="getUserByTableName" parameterType="String" resultType="com.example.entity.User">
    SELECT * FROM ${tableName}
</select>

所以,在使用时尽量使用 #{},只有在需要动态传入表名、列名等无法使用占位符的情况下才使用 ${}。 9. Dubbo 的负载均衡策略有哪些? Dubbo 提供了多种负载均衡策略,主要有以下几种: - RandomLoadBalance(随机):随机选择一个服务提供者,默认的负载均衡策略。它根据权重随机选择,权重越大被选中的概率越高。 - RoundRobinLoadBalance(轮询):按顺序依次选择服务提供者,同样会考虑权重。如果某个服务提供者的权重较高,会在一个周期内被多次选择。 - LeastActiveLoadBalance(最少活跃调用数):优先选择活跃调用数最少的服务提供者。如果有多个服务提供者的活跃调用数相同,则根据权重随机选择。 - ConsistentHashLoadBalance(一致性哈希):根据请求的某些关键信息(如参数)计算哈希值,将请求映射到一个哈希环上,然后选择距离该哈希值最近的服务提供者。这样可以保证相同的请求总是路由到同一个服务提供者。