《互联网大厂 Java 面试:核心知识、多线程与框架技术大考验》

52 阅读10分钟

互联网大厂 Java 面试:核心知识、多线程与框架技术大考验

在互联网大厂的一间安静面试室内,严肃的面试官正襟危坐,对面坐着略显紧张的求职者王铁牛。一场关于 Java 核心知识的面试即将拉开帷幕。

第一轮面试开始 面试官:首先问几个基础的 Java 核心知识问题。Java 中基本数据类型有哪些? 王铁牛:有 byte、short、int、long、float、double、char、boolean。 面试官:回答得不错。那说说 Java 中访问修饰符有哪些,分别有什么作用? 王铁牛:有 public、protected、default(默认,不写修饰符)、private。public 修饰的成员可以在任何地方被访问;protected 修饰的成员可以在同一个包内以及不同包的子类中访问;default 只能在同一个包内访问;private 只能在本类中访问。 面试官:非常好。那 Java 中的多态是如何实现的? 王铁牛:多态可以通过继承、接口实现,还有方法的重写和重载来实现。子类继承父类重写父类方法,就可以在运行时根据实际对象类型调用不同的方法;接口也是类似,实现类实现接口方法,在使用接口类型引用实际实现类对象时体现多态;方法重载则是在一个类中定义多个同名方法但参数列表不同。 面试官:你的回答很清晰,基础很扎实。

第二轮面试开始 面试官:接下来聊聊 JUC 和多线程相关的。JUC 包是什么,有什么作用? 王铁牛:JUC 是 java.util.concurrent 包,它提供了一些用于并发编程的工具类,能帮助我们更方便地进行多线程编程。 面试官:那线程池的好处有哪些,如何创建一个线程池? 王铁牛:线程池的好处就是可以复用线程,减少线程创建和销毁的开销,还能控制线程的数量,避免资源过度使用。创建线程池可以使用 Executors 类的工厂方法,像 newFixedThreadPool、newCachedThreadPool 等。 面试官:你提到了使用 Executors 类,不过阿里巴巴开发手册不建议用它创建线程池,能说说为什么吗? 王铁牛:这个……嗯……好像是因为它可能会导致一些资源问题,但具体我有点说不清楚了。 面试官:这里要注意,Executors 创建的线程池可能会导致 OOM(OutOfMemoryError),比如 newFixedThreadPool 和 newSingleThreadExecutor 的工作队列是无界的 LinkedBlockingQueue,可能会堆积大量任务导致内存溢出;newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE,可能会创建大量线程导致内存溢出。不过你前面的基础回答还是不错的。

第三轮面试开始 面试官:我们再深入一些,聊聊框架相关的。Spring 框架的核心特性有哪些? 王铁牛:Spring 的核心特性有 IOC(控制反转)和 AOP(面向切面编程)。IOC 就是把对象的创建和依赖关系的管理交给 Spring 容器,AOP 是在不修改原有代码的基础上增加一些额外的功能,比如日志记录、事务管理等。 面试官:那 Spring Boot 相比 Spring 有什么优势? 王铁牛:Spring Boot 简化了 Spring 的配置,它有自动配置的功能,能快速搭建项目,减少了很多繁琐的 XML 配置。 面试官:MyBatis 中 #{} 和 {} 的区别是什么? **王铁牛**:这个……我记得好像和 SQL 注入有关,但具体区别我讲不太明白。 **面试官**:#{} 是预编译处理,它会把参数替换为占位符?,能防止 SQL 注入;{} 是直接替换,会把参数直接拼接到 SQL 语句中,存在 SQL 注入风险。另外,我们还涉及到分布式和中间件相关的内容,像 Dubbo、RabbitMq、xxl - job、Redis,你在这些方面有深入了解吗? 王铁牛:有一些了解,但是具体细节我不太能说清楚。

面试结束 面试官扶了扶眼镜,说道:“王铁牛,今天的面试就到这里。你在一些基础的 Java 核心知识、JUC 和 Spring 框架的基础部分回答得还不错,说明你有一定的知识储备。不过在一些深入的问题上,比如线程池创建的风险、MyBatis 中符号的区别,以及分布式和中间件相关内容,你的回答还不够清晰和准确。我们后续会综合评估这次面试情况,你先回家等通知吧。”

问题答案详细解析

  1. Java 中基本数据类型有哪些? Java 中的基本数据类型分为四大类:
    • 整数类型
      • byte:占 1 个字节,取值范围 -128 到 127。
      • short:占 2 个字节,取值范围 -32768 到 32767。
      • int:占 4 个字节,取值范围 -2147483648 到 2147483647。
      • long:占 8 个字节,取值范围 -2^63 到 2^63 - 1,定义 long 类型变量时需要在数字后面加 L 或 l。
    • 浮点类型
      • float:占 4 个字节,定义 float 类型变量时需要在数字后面加 F 或 f。
      • double:占 8 个字节,是浮点数的默认类型。
    • 字符类型
      • char:占 2 个字节,用于表示单个字符,用单引号括起来,例如 'A'。
    • 布尔类型
      • boolean:只有两个值,true 和 false,用于逻辑判断。
  2. Java 中访问修饰符有哪些,分别有什么作用?
    • public:公共的,使用 public 修饰的类、方法、属性可以在任何地方被访问,没有访问权限限制。
    • protected:受保护的,被 protected 修饰的成员可以在同一个包内的类中访问,也可以在不同包的子类中访问。
    • default(默认,不写修饰符):只能在同一个包内的类中访问,不同包的类无法访问。
    • private:私有的,被 private 修饰的成员只能在本类中访问,其他类无法直接访问。
  3. 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(); // 输出 "Dog barks"
    }
}
- **接口和实现**:一个类实现一个或多个接口,不同的实现类可以有不同的实现方式。例如:
interface Shape {
    double area();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double area() {
        return length * width;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(3, 4);
        System.out.println(circle.area());
        System.out.println(rectangle.area());
    }
}
- **方法重载**:在一个类中定义多个同名方法,但方法的参数列表不同(参数的类型、个数、顺序不同)。编译器会根据调用方法时传入的参数来决定调用哪个方法。例如:
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        System.out.println(calculator.add(1, 2));
        System.out.println(calculator.add(1.5, 2.5));
    }
}
  1. JUC 包是什么,有什么作用? JUC 是 java.util.concurrent 包,它是 Java 5 引入的用于并发编程的工具包。其作用主要是为了简化多线程编程,提供了一系列高级的并发编程工具和类,包括:
    • 线程池:如 ExecutorService 及其实现类,用于管理线程的生命周期,提高线程的复用性和性能。
    • 锁机制:如 ReentrantLock、ReadWriteLock 等,提供更灵活的锁控制,比 synchronized 关键字更强大。
    • 并发集合:如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些集合类在多线程环境下是线程安全的。
    • 同步器:如 CountDownLatch、CyclicBarrier、Semaphore 等,用于线程之间的同步和协作。
  2. 线程池的好处有哪些,如何创建一个线程池?
    • 线程池的好处
      • 减少线程创建和销毁的开销:线程的创建和销毁需要消耗系统资源,线程池可以复用线程,避免了频繁创建和销毁线程带来的性能损耗。
      • 提高响应速度:当有任务提交时,线程池中如果有空闲线程可以立即执行任务,而不需要等待线程的创建。
      • 控制线程数量:可以避免创建过多的线程导致系统资源耗尽,通过设置线程池的参数,可以合理地控制线程的数量。
    • 创建线程池的方式
      • 使用 Executors 类的工厂方法
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        // 创建一个可缓存的线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        // 创建一个单线程的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    }
}
    - **使用 ThreadPoolExecutor 类手动创建**
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                60, // 线程空闲时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(10) // 工作队列
        );
    }
}
  1. 为什么阿里巴巴开发手册不建议用 Executors 类创建线程池?
    • newFixedThreadPool 和 newSingleThreadExecutor:这两个方法创建的线程池使用的工作队列是无界的 LinkedBlockingQueue。当任务提交速度超过线程处理速度时,任务会不断堆积在队列中,可能会导致内存溢出(OOM)。
    • newCachedThreadPool:该方法创建的线程池的最大线程数是 Integer.MAX_VALUE,当有大量任务提交时,会创建大量的线程,可能会耗尽系统资源,导致 OOM。
  2. Spring 框架的核心特性有哪些?
    • IOC(控制反转):也称为依赖注入(DI),它是一种设计模式,将对象的创建和依赖关系的管理从代码中转移到 Spring 容器中。通过 Spring 容器来创建和管理对象,对象之间的依赖关系由容器来注入,降低了代码的耦合度。例如:
// 定义一个接口
interface UserService {
    void sayHello();
}

// 实现接口
class UserServiceImpl implements UserService {
    @Override
    public void sayHello() {
        System.out.println("Hello!");
    }
}

// 配置 Spring 容器
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
}

// 使用 Spring 容器获取对象
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        userService.sayHello();
    }
}
- **AOP(面向切面编程)**:是一种编程范式,它允许我们在不修改原有代码的基础上,对程序的某些部分进行增强。例如,在方法执行前后添加日志记录、事务管理等功能。AOP 通过定义切面(Aspect)、切点(Pointcut)和通知(Advice)来实现。例如:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("Before method: " + joinPoint.getSignature().getName());
    }

    @After("serviceMethods()")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("After method: " + joinPoint.getSignature().getName());
    }
}
  1. Spring Boot 相比 Spring 有什么优势?
    • 简化配置:Spring Boot 提供了自动配置的功能,它会根据项目中引入的依赖自动配置 Spring 应用程序,减少了大量的 XML 配置文件和 Java 配置类。例如,引入 Spring Boot Starter Web 依赖后,Spring Boot 会自动配置嵌入式的 Tomcat 服务器和 Spring MVC。
    • 快速搭建项目:使用 Spring Initializr 可以快速生成一个 Spring Boot 项目的骨架,包含了项目所需的基本依赖和目录结构,开发者可以立即开始编写业务代码。
    • 生产级特性:Spring Boot 提供了一些生产级的特性,如健康检查、指标监控、外部配置等,方便对应用程序进行管理和监控。
    • 微服务支持:Spring Boot 非常适合用于构建微服务架构,它可以轻松地创建独立运行的微服务,并且可以与 Spring Cloud 等框架集成,实现服务注册、发现、配置管理等功能。
  2. MyBatis 中 #{} 和 ${} 的区别是什么?
    • #{}:是预编译处理,MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为占位符?,然后使用 PreparedStatement 进行预编译,将参数作为预编译语句的参数传入。这样可以防止 SQL 注入攻击,因为参数会被自动进行转义处理。例如:
<select id="getUserById" parameterType="int" resultType="com.example.User">
    SELECT * FROM users WHERE id = #{id}
</select>
- **${}**:是直接替换,MyBatis 在处理 ${} 时,会将 ${} 直接替换为参数的值,然后再执行 SQL 语句。这种方式存在 SQL 注入风险,因为参数的值会直接拼接到 SQL 语句中,没有进行任何转义处理。例如:
<select id="getUserByColumnName" parameterType="String" resultType="com.example.User">
    SELECT * FROM users WHERE ${columnName} = 'value'
</select>

所以,在实际开发中,尽量使用 #{} 来避免 SQL 注入问题,只有在一些特殊情况下,如动态表名、动态列名时才使用 ${}。