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

41 阅读11分钟

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

在互联网大厂的一间严肃的面试室内,一位神情严肃的面试官正对面坐着略显紧张的求职者王铁牛。面试正式开始。

第一轮提问 面试官:首先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些? 王铁牛:Java 的基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那 String 类为什么是不可变的呢? 王铁牛:因为 String 类是用 final 修饰的,它的底层是一个用 final 修饰的字符数组,一旦创建就不能改变。 面试官:很好。那讲讲 Java 中多态的实现方式有哪些? 王铁牛:Java 多态的实现方式主要有方法重载和方法重写。方法重载是在同一个类中,方法名相同但参数列表不同;方法重写是在子类中重写父类的方法。 面试官:回答得很清晰,看来基础很扎实。

第二轮提问 面试官:接下来问一些 JUC、JVM 和多线程相关的问题。JUC 包中常用的类有哪些? 王铁牛:JUC 包中常用的类有 CountDownLatch、CyclicBarrier、Semaphore 等。 面试官:不错。那 JVM 的内存模型是怎样的? 王铁牛:JVM 内存模型主要包括堆、栈、方法区等。堆是存储对象的地方,栈是存储局部变量和方法调用信息的,方法区存储类的信息、常量等。 面试官:很好。那在多线程编程中,如何保证线程安全? 王铁牛:可以使用 synchronized 关键字或者 Lock 接口来保证线程安全。synchronized 可以修饰方法或者代码块,Lock 接口可以实现更灵活的锁机制。 面试官:回答得很好,对这部分知识掌握得挺不错。

第三轮提问 面试官:现在问一些关于框架和中间件的问题。Spring 框架的核心特性有哪些? 王铁牛:Spring 框架的核心特性有 IOC(控制反转)和 AOP(面向切面编程)。IOC 是将对象的创建和依赖关系的管理交给 Spring 容器,AOP 是在不修改原有代码的基础上增强功能。 面试官:那 Spring Boot 相对于 Spring 有什么优势? 王铁牛:嗯……就是它好像更简单点,能快速搭建项目。 面试官:再具体说说呢,比如从配置方面。 王铁牛:这个……我有点不太清楚了。 面试官:那 MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:好像……#{} 是预编译的,能防止 SQL 注入,{} 就是直接替换。但是具体原理我也不太说得清。 面试官:看来你对框架的理解还不够深入。Dubbo 是做什么的,它的工作原理了解吗? 王铁牛:Dubbo 好像是个分布式服务框架,工作原理……我不太能说清楚。

面试总结 面试官扶了扶眼镜,表情有些严肃地说:“王铁牛,通过这三轮面试,能看出你对 Java 核心知识、JUC、JVM 和多线程这些基础部分掌握得还可以,对于一些简单的概念回答得比较准确,这说明你有一定的基础。但是在框架和中间件方面,你明显存在不足,像 Spring Boot 的优势、MyBatis 的一些细节以及 Dubbo 的工作原理等问题,你回答得不够清晰或者不太完整,这反映出你对这些技术的理解还不够深入,实际应用和掌握程度有待提高。我们后续会综合考虑所有候选人的情况,你先回家等通知吧。”

问题答案

  1. Java 中基本数据类型有哪些? Java 的基本数据类型分为四大类,共八种:
    • 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
    • 浮点类型:float(4 字节)、double(8 字节)。
    • 字符类型:char(2 字节)。
    • 布尔类型:boolean(理论上 1 位,但 JVM 处理时按 1 字节)。
  2. String 类为什么是不可变的呢? String 类是不可变的,主要基于以下几点:
    • String 类被 final 修饰,这意味着它不能被继承,防止子类修改其行为。
    • 其底层使用一个用 final 修饰的字符数组 private final char value[] 来存储字符串内容。final 修饰数组表示数组的引用不能改变,虽然数组元素可以改变,但 String 类没有提供修改数组元素的公共方法,所以一旦创建,字符串内容就不能改变。
    • 不可变的特性使得 String 对象可以被安全地共享,在多线程环境下不需要额外的同步处理,同时也能提高缓存和哈希计算的效率。
  3. Java 中多态的实现方式有哪些?
    • 方法重载(Overloading):在同一个类中,方法名相同但参数列表不同(参数的类型、个数或顺序不同)。方法重载与返回值类型无关。例如:
public class OverloadingExample {
    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");
    }
}
  1. JUC 包中常用的类有哪些?
    • CountDownLatch:一个同步辅助类,它允许一个或多个线程等待直到在其他线程中执行的一组操作完成。例如,主线程等待多个子线程完成任务后再继续执行:
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟任务执行
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread finished");
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("All threads have finished");
    }
}
- **CyclicBarrier**:一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。与 CountDownLatch 不同的是,CyclicBarrier 可以重复使用。例如:
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int parties = 3;
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> System.out.println("All threads reached the barrier"));
        for (int i = 0; i < parties; i++) {
            new Thread(() -> {
                try {
                    // 模拟任务执行
                    Thread.sleep(1000);
                    System.out.println("Thread reached the barrier");
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
- **Semaphore**:一个计数信号量,用于控制同时访问某个资源的线程数量。例如,限制同时访问数据库连接池的线程数量:
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int MAX_CONNECTIONS = 2;
    private static Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println("Thread acquired a connection");
                    // 模拟使用连接
                    Thread.sleep(1000);
                    semaphore.release();
                    System.out.println("Thread released a connection");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. JVM 的内存模型是怎样的? JVM 内存模型主要包括以下几个部分:
    • 堆(Heap):是 JVM 中最大的一块内存区域,用于存储对象实例和数组。堆是所有线程共享的,垃圾回收主要就是针对堆进行的。堆可以分为新生代和老年代,新生代又可以进一步分为 Eden 区、Survivor 区(通常有两个 Survivor 区,即 From 区和 To 区)。
    • 栈(Stack):每个线程都有自己独立的栈,用于存储局部变量、方法调用信息和操作数栈等。栈中的数据是线程私有的,当方法被调用时,会在栈中创建一个栈帧,方法执行完毕后,栈帧会被销毁。栈分为虚拟机栈和本地方法栈,虚拟机栈用于执行 Java 方法,本地方法栈用于执行本地方法(使用 C 或 C++ 编写的方法)。
    • 方法区(Method Area):是所有线程共享的内存区域,用于存储类的信息(如类的结构、常量池、静态变量等)。在 Java 8 之前,方法区也被称为永久代,Java 8 及以后,方法区被元空间(Metaspace)取代,元空间使用本地内存,不再受 JVM 堆大小的限制。
    • 程序计数器(Program Counter Register):每个线程都有一个独立的程序计数器,它是一个较小的内存区域,用于记录当前线程执行的字节码指令的地址。如果线程正在执行的是 Java 方法,程序计数器记录的是正在执行的字节码指令的地址;如果线程正在执行的是本地方法,程序计数器的值为 undefined。
  2. 在多线程编程中,如何保证线程安全?
    • 使用 synchronized 关键字
      • 修饰实例方法:当一个线程访问一个被 synchronized 修饰的实例方法时,会自动获取该对象的锁,其他线程必须等待该线程释放锁才能访问该方法。例如:
public class SynchronizedExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}
    - **修饰静态方法**:当一个线程访问一个被 synchronized 修饰的静态方法时,会自动获取该类的类对象的锁,其他线程必须等待该线程释放锁才能访问该方法。例如:
public class SynchronizedStaticExample {
    private static int count = 0;
    public static synchronized void increment() {
        count++;
    }
}
    - **修饰代码块**:可以指定要获取的锁对象,例如:
public class SynchronizedBlockExample {
    private int count = 0;
    private Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}
- **使用 Lock 接口**:Lock 接口提供了比 synchronized 更灵活的锁机制,例如 ReentrantLock 类。使用 Lock 接口需要手动加锁和解锁,例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
  1. Spring 框架的核心特性有哪些?
    • IOC(控制反转):也称为依赖注入(DI),是 Spring 框架的核心特性之一。在传统的编程中,对象的创建和依赖关系的管理是由程序员手动完成的,而在 Spring 中,对象的创建和依赖关系的管理交给了 Spring 容器。Spring 容器通过配置文件或注解来创建和管理对象,并将对象之间的依赖关系注入到相应的对象中。例如,通过注解的方式实现依赖注入:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
class ServiceA {
    private ServiceB serviceB;
    @Autowired
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

@Component
class ServiceB {
    // 类的具体实现
}
- **AOP(面向切面编程)**:是一种编程范式,它允许在不修改原有代码的基础上增强功能。在 Spring 中,AOP 主要用于处理一些横切关注点,如日志记录、事务管理、安全验证等。Spring AOP 基于代理模式实现,有两种代理方式:JDK 动态代理和 CGLIB 代理。例如,使用 Spring AOP 实现日志记录:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @After("execution(* com.example.service.*.*(..))")
    public void logAfterMethod(JoinPoint joinPoint) {
        System.out.println("Method " + joinPoint.getSignature().getName() + " executed");
    }
}
  1. Spring Boot 相对于 Spring 有什么优势?
    • 简化配置:Spring 需要大量的 XML 配置文件或 Java 配置类来配置各种组件,而 Spring Boot 采用了约定大于配置的原则,通过自动配置机制,减少了大量的手动配置。例如,在 Spring 中配置一个数据源需要编写复杂的 XML 配置或 Java 配置类,而在 Spring Boot 中,只需要在 application.propertiesapplication.yml 中配置几个简单的属性即可:
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456
- **快速搭建项目**:Spring Boot 提供了 Spring Initializr 工具,可以快速生成项目骨架,包含了基本的依赖和配置,开发者可以直接开始编写业务代码。同时,Spring Boot 内置了嵌入式服务器(如 Tomcat、Jetty 等),不需要手动部署到服务器上,直接运行项目的主类即可启动应用。
- **依赖管理**:Spring Boot 提供了 Starter 依赖,每个 Starter 依赖包含了一组相关的依赖,开发者只需要引入相应的 Starter 依赖,Spring Boot 会自动管理这些依赖的版本和兼容性,避免了依赖冲突的问题。例如,引入 `spring-boot-starter-web` 依赖就可以快速搭建一个 Web 应用:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符 ?,然后使用 PreparedStatement 来执行 SQL 语句。这样可以防止 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>
  1. Dubbo 是做什么的,它的工作原理了解吗?
    • Dubbo 的作用:Dubbo 是一个高性能的分布式服务框架,主要用于解决分布式系统中服务之间的远程调用和服务治理问题。它可以将不同的服务提供者和服务消费者连接起来,实现服务的注册、发现、调用和监控等功能,提高系统的可扩展性和可维护性。
    • 工作原理
      • 服务注册:服务提供者在启动时,将自己提供的服务信息(如服务接口、服务地址等)注册到注册中心(如 ZooKeeper、Nacos 等)。
      • 服务发现:服务消费者在启动时,从注册中心获取服务提供者的信息,并缓存到本地。
      • 远程调用:服务消费者根据本地缓存的服务提供者信息,通过网络调用服务提供者的服务。Dubbo