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

34 阅读3分钟

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

王铁牛怀揣着紧张与期待,坐在互联网大厂的面试室里,对面是表情严肃的面试官,一场关于 Java 技术的严峻考验即将拉开帷幕。

第一轮提问 面试官:我们先从 Java 核心知识开始。Java 中基本数据类型有哪些,它们的包装类又是什么? 王铁牛:基本数据类型有 byte、short、int、long、float、double、char、boolean。对应的包装类分别是 Byte、Short、Integer、Long、Float、Double、Character、Boolean。 面试官:回答得不错。那说说 Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有继承和接口。通过父类引用指向子类对象,或者接口引用指向实现类对象,调用相同的方法会有不同的表现。 面试官:很好。再问一下,Java 中异常处理机制是怎样的? 王铁牛:Java 中异常分为受检查异常和非受检查异常。可以使用 try-catch-finally 语句来捕获和处理异常,也可以使用 throws 关键字在方法声明处抛出异常。 面试官:非常棒,基础很扎实。

第二轮提问 面试官:接下来聊聊 JUC、JVM 和多线程。JUC 包中常用的工具类有哪些,各自有什么作用? 王铁牛:嗯……有 CountDownLatch 吧,好像是用来控制线程同步的。还有……还有什么来着,我有点记不清了。 面试官:没关系,那说说 JVM 的内存模型,各个区域分别存储什么数据? 王铁牛:有堆,堆是存储对象的地方。还有栈,栈里存局部变量啥的。其他的我就不太确定了。 面试官:那多线程中线程的生命周期有哪些状态,状态之间是如何转换的? 王铁牛:状态有新建、运行、阻塞,然后……然后怎么转换我也说不太清楚。 面试官:这部分知识掌握得不够扎实,还需要加强学习。

第三轮提问 面试官:现在谈谈一些常用的集合和框架。HashMap 的底层实现原理是什么,在多线程环境下使用会有什么问题? 王铁牛:HashMap 好像是数组加链表的结构,多线程问题嘛……可能会数据不一致,但具体怎么回事我不太懂。 面试官:Spring 框架的核心特性有哪些,Spring 是如何实现依赖注入的? 王铁牛:核心特性有 IOC 和 AOP。依赖注入嘛,就是把对象的创建和依赖关系的管理交给 Spring 容器,具体怎么实现我不太清楚。 面试官:MyBatis 中 #{} 和 ${} 的区别是什么,分别在什么场景下使用? 王铁牛:这个……我只知道它们有点不一样,但具体区别和使用场景不太明白。 面试官:整体来看,你对一些基础知识有一定了解,但对于复杂的技术点掌握得不够深入。你先回家等通知吧,后续如果有消息我们会及时联系你。

问题答案

  1. Java 中基本数据类型有哪些,它们的包装类又是什么?
    • Java 中的基本数据类型分为四类八种:
      • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
      • 浮点类型:float(4 字节)、double(8 字节)。
      • 字符类型:char(2 字节)。
      • 布尔类型:boolean(没有明确规定大小)。
    • 对应的包装类分别是 Byte、Short、Integer、Long、Float、Double、Character、Boolean。包装类的作用主要是将基本数据类型封装成对象,以便在一些需要对象的场景中使用,比如集合中只能存储对象,不能存储基本数据类型。
  2. Java 中多态的实现方式有哪些?
    • 继承:通过父类引用指向子类对象,当调用父类和子类都有的方法时,会根据实际指向的子类对象来调用子类重写的方法。例如:
class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}
class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }
}
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat(); // 输出 Dog is eating
    }
}
- **接口**:接口引用指向实现类对象,调用接口中的抽象方法时,会执行实现类中实现的具体方法。例如:
interface Shape {
    void draw();
}
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}
public class Main {
    public static void main(String[] args) {
        Shape shape = new Circle();
        shape.draw(); // 输出 Drawing a circle
    }
}
  1. Java 中异常处理机制是怎样的?
    • Java 中的异常分为受检查异常(Checked Exception)和非受检查异常(Unchecked Exception)。受检查异常是指在编译时必须进行处理的异常,否则编译不通过,例如 IOException、SQLException 等;非受检查异常是指在编译时不需要进行处理的异常,通常是由程序逻辑错误引起的,例如 RuntimeException 及其子类(如 NullPointerException、ArrayIndexOutOfBoundsException 等)。
    • 异常处理方式主要有两种:
      • try-catch-finally 语句:用于捕获和处理异常。try 块中放置可能会抛出异常的代码,catch 块用于捕获并处理相应类型的异常,finally 块中的代码无论是否发生异常都会执行。例如:
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("除数不能为零:" + e.getMessage());
} finally {
    System.out.println("finally 块执行");
}
    - **throws 关键字**:用于在方法声明处抛出异常,表示该方法可能会抛出某种类型的异常,将异常的处理交给调用该方法的上层方法。例如:
public void readFile() throws IOException {
    // 读取文件的代码
}
  1. JUC 包中常用的工具类有哪些,各自有什么作用?
    • CountDownLatch:用于控制线程同步,它允许一个或多个线程等待其他线程完成操作。通过一个计数器来实现,初始化时设置计数器的值,每当一个线程完成任务后,计数器减 1,当计数器的值为 0 时,等待的线程可以继续执行。例如:
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() + " 完成任务");
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("所有任务完成,继续执行");
    }
}
- **CyclicBarrier**:也是用于线程同步,它可以让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行。与 CountDownLatch 不同的是,CyclicBarrier 可以重复使用。例如:
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程到达屏障,继续执行"));
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 到达屏障");
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).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() + " 获取到许可证,开始访问资源");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 释放许可证");
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. JVM 的内存模型,各个区域分别存储什么数据?
    • 程序计数器(Program Counter Register):是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,用于记录线程执行的位置,以保证线程切换后能恢复到正确的执行位置。
    • Java 虚拟机栈(Java Virtual Machine Stacks):每个线程在创建时都会创建一个虚拟机栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表存储了方法执行过程中的局部变量,包括基本数据类型和对象引用。
    • 本地方法栈(Native Method Stacks):与虚拟机栈类似,只不过它是为本地方法服务的。本地方法是使用其他语言(如 C、C++)实现的方法。
    • Java 堆(Java Heap):是 Java 虚拟机所管理的内存中最大的一块,所有线程共享。它是对象实例和数组分配内存的地方。堆可以分为新生代和老年代,新生代又可以分为 Eden 区和两个 Survivor 区。
    • 方法区(Method Area):也是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 8 及以后,方法区被元空间(Metaspace)所取代,元空间使用本地内存。
  2. 多线程中线程的生命周期有哪些状态,状态之间是如何转换的?
    • 线程的生命周期有以下几种状态:
      • 新建(New):当创建一个 Thread 对象时,线程处于新建状态。例如:Thread thread = new Thread();
      • 就绪(Runnable):调用线程的 start() 方法后,线程进入就绪状态,等待 CPU 分配时间片。例如:thread.start();
      • 运行(Running):当 CPU 分配时间片给处于就绪状态的线程时,线程进入运行状态,开始执行 run() 方法中的代码。
      • 阻塞(Blocked):线程在某些情况下会进入阻塞状态,例如等待获取锁、调用 sleep() 方法、调用 wait() 方法等。阻塞状态分为三种:
        • 等待阻塞:调用 wait() 方法后,线程进入等待队列,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它。
        • 同步阻塞:当线程试图获取一个已经被其他线程占用的锁时,会进入同步阻塞状态,直到锁被释放。
        • 其他阻塞:调用 sleep() 方法或 join() 方法时,线程会进入其他阻塞状态。
      • 终止(Terminated):线程执行完 run() 方法中的代码或者因异常退出时,线程进入终止状态。
    • 状态转换示例:
public class ThreadStateExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程进入运行状态");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程执行完毕,进入终止状态");
        });
        System.out.println("线程处于新建状态");
        thread.start();
        System.out.println("线程进入就绪状态");
        Thread.sleep(200);
        System.out.println("线程可能处于运行或阻塞状态");
        thread.join();
        System.out.println("线程已终止");
    }
}
  1. HashMap 的底层实现原理是什么,在多线程环境下使用会有什么问题?
    • 底层实现原理:在 JDK 1.7 及以前,HashMap 是由数组和链表组成的。数组是 HashMap 的主体,链表是为了解决哈希冲突而存在的。当插入一个键值对时,首先通过哈希函数计算键的哈希值,然后根据哈希值找到对应的数组索引位置。如果该位置已经有元素,则将新元素插入到链表的头部(头插法)。
    • 在 JDK 1.8 及以后,HashMap 采用数组 + 链表 + 红黑树的结构。当链表长度达到 8 且数组长度达到 64 时,链表会转换为红黑树,以提高查找效率;当红黑树的节点数减少到 6 时,红黑树会转换回链表。
    • 多线程问题:在多线程环境下使用 HashMap 会出现数据不一致和死循环的问题。在 JDK 1.7 中,当多个线程同时进行扩容操作时,可能会导致链表形成环形结构,从而造成死循环;在 JDK 1.8 中,虽然解决了死循环的问题,但仍然存在数据覆盖的问题。因此,在多线程环境下建议使用 ConcurrentHashMap。
  2. Spring 框架的核心特性有哪些,Spring 是如何实现依赖注入的?
    • 核心特性
      • IOC(Inversion of Control,控制反转):将对象的创建和依赖关系的管理交给 Spring 容器,而不是由对象自己来创建和管理依赖。通过 IOC 可以降低代码的耦合度,提高代码的可维护性和可测试性。
      • AOP(Aspect-Oriented Programming,面向切面编程):允许在不修改原有代码的情况下,对程序的功能进行增强。例如,可以在方法执行前后添加日志记录、事务管理等功能。
    • 依赖注入实现方式
      • 构造函数注入:通过构造函数将依赖对象传递给目标对象。例如:
public class UserService {
    private UserDao userDao;
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
    - **Setter 方法注入**:通过 Setter 方法将依赖对象传递给目标对象。例如:
public class UserService {
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
    - **注解注入**:使用 @Autowired@Resource 等注解来自动注入依赖对象。例如:
public class UserService {
    @Autowired
    private UserDao userDao;
}
  1. MyBatis 中 #{} 和 ${} 的区别是什么,分别在什么场景下使用?
    • 区别
      • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为 ? 占位符,然后使用 PreparedStatement 进行参数设置,这样可以防止 SQL 注入攻击。
      • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis 在处理 {} 时,会将 ${} 直接替换为传入的参数值。由于是直接替换,所以可能会存在 SQL 注入风险。
    • 使用场景
      • #{}:通常用于传递 SQL 语句中的参数,例如查询条件、插入值等。例如:
<select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>
    - **${}**:主要用于动态传入表名、列名等。例如:
<select id="getUserByColumn" parameterType="map" resultType="User">
    SELECT * FROM users WHERE ${column} = #{value}
</select>