《互联网大厂 Java 面试:核心知识、JUC、JVM 等深度考察》

51 阅读12分钟

互联网大厂 Java 面试:核心知识、JUC、JVM 等深度考察

在互联网大厂的一间安静的面试室内,严肃的面试官坐在桌前,对面是略显紧张的求职者王铁牛。一场关于 Java 技术的严峻考验即将展开。

第一轮提问 面试官:首先,我们来聊聊 Java 核心知识。Java 中多态的实现方式有哪些? 王铁牛:多态的实现方式主要有两种,一种是方法重载,在同一个类中,方法名相同但参数列表不同;另一种是方法重写,子类重写父类的方法。 面试官:回答得不错。那 Java 的访问修饰符有哪些,分别有什么作用? 王铁牛:Java 有四种访问修饰符,分别是 public、protected、default(默认,不写修饰符时)和 private。public 修饰的成员可以被任何类访问;protected 修饰的成员可以被同一个包内的类以及不同包的子类访问;default 修饰的成员只能被同一个包内的类访问;private 修饰的成员只能在本类中访问。 面试官:很好,看来你基础掌握得挺扎实。那说说 Java 中的异常处理机制,try-catch-finally 是怎么工作的? 王铁牛:try 块中放置可能会抛出异常的代码,如果 try 块中的代码抛出了异常,会根据异常类型匹配相应的 catch 块进行处理。finally 块中的代码无论是否发生异常都会执行,通常用于释放资源。

第二轮提问 面试官:接下来我们深入一点,谈谈 JUC(Java 并发工具包)。CountDownLatch 和 CyclicBarrier 有什么区别? 王铁牛:嗯……它们好像都和线程同步有关,具体区别我有点说不太清了,感觉都是等一些线程完成任务吧。 面试官:看来你对这个掌握得不太好。那再问你,Semaphore 是什么,在什么场景下会使用它? 王铁牛:Semaphore 好像是用来控制并发线程数量的,但具体在啥场景用,我就不太确定了,可能是限制资源访问吧。 面试官:回答得不够清晰。那说说 JVM 的内存模型,堆和栈的主要区别是什么? 王铁牛:堆和栈嘛,堆是放对象的,栈是放局部变量的,其他的我就不太清楚了。

第三轮提问 面试官:我们再聊聊实际应用场景。在多线程环境下,使用 ArrayList 会有什么问题,如何解决? 王铁牛:好像会有线程安全问题,但具体怎么解决,我有点懵,是不是加个锁就行? 面试官:不太准确。那说说 Spring 的核心特性有哪些,IOC 和 AOP 分别是什么? 王铁牛:Spring 核心特性啊,IOC 和 AOP 我知道。IOC 好像是控制反转,就是把对象的创建和管理交给 Spring 容器;AOP 是面向切面编程,能在不修改原有代码的情况下增强功能,但具体原理我说不太好。 面试官:理解得有一些,但不够深入。最后问你,Redis 有哪些数据结构,在实际业务中如何应用? 王铁牛:Redis 数据结构有字符串、哈希、列表、集合和有序集合。但在业务中咋用,我就只知道可以做缓存,其他不太清楚了。

面试官:今天的面试就到这里吧,你先回家等通知。我们会综合评估后给你反馈。

问题答案

  1. 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 makeSound() {
        System.out.println("Animal makes a sound");
    }
}
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. Java 的访问修饰符有哪些,分别有什么作用?
    • public:被 public 修饰的类、方法、变量可以被任何类访问,没有访问限制。
    • protected:被 protected 修饰的成员可以被同一个包内的类以及不同包的子类访问。例如,在不同包的子类中可以通过继承关系访问父类的 protected 成员。
    • default(默认,不写修饰符时):只能被同一个包内的类访问。如果一个类没有指定访问修饰符,那么它的成员和类本身都具有默认访问权限。
    • private:只能在本类中访问。使用 private 修饰的成员,只有在定义该成员的类内部才能使用,其他类无法直接访问。
  2. Java 中的异常处理机制,try-catch-finally 是怎么工作的?
    • try 块:包含可能会抛出异常的代码。当 try 块中的代码执行时,如果发生异常,程序会立即停止执行 try 块中剩余的代码,转而寻找匹配的 catch 块。
    • catch 块:用于捕获和处理 try 块中抛出的异常。可以有多个 catch 块,每个 catch 块捕获不同类型的异常。异常类型会按照 catch 块的顺序依次匹配,一旦匹配成功,就会执行该 catch 块中的代码。例如:
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("除数不能为零:" + e.getMessage());
}
- **finally 块**:无论 try 块中是否发生异常,finally 块中的代码都会执行。通常用于释放资源,如关闭文件、数据库连接等。例如:
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 读取文件操作
} catch (FileNotFoundException e) {
    System.out.println("文件未找到:" + e.getMessage());
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  1. CountDownLatch 和 CyclicBarrier 有什么区别?
    • CountDownLatch:是一个计数器,它允许一个或多个线程等待其他线程完成操作。构造函数接受一个整数作为计数器的初始值,当计数器的值为 0 时,等待的线程会被唤醒继续执行。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();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        System.out.println("所有线程任务完成,主线程继续执行");
    }
}
- **CyclicBarrier**:允许一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后所有线程可以继续执行。CyclicBarrier 可以重复使用,当所有线程都到达屏障点后,计数器会重置,可以继续进行下一轮的等待。例如,多个线程完成一部分任务后,等待其他线程也完成这部分任务后再一起继续执行后续任务:
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int threadCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程到达屏障点,继续执行后续任务");
        });
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟线程执行任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 到达屏障点");
                    barrier.await();
                    // 继续执行后续任务
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. Semaphore 是什么,在什么场景下会使用它?
    • Semaphore 是一个计数信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可证数量,线程在访问资源前需要先获取许可证,访问完成后释放许可证。如果许可证数量为 0,线程会被阻塞,直到有其他线程释放许可证。
    • 应用场景
      • 资源限流:当系统中某个资源的并发访问量有限时,可以使用 Semaphore 来控制并发线程的数量,避免资源被过度使用。例如,数据库连接池,限制同时使用的数据库连接数量。
      • 流量控制:在分布式系统中,控制对某个服务的并发请求数量,防止服务被大量请求压垮。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int THREAD_COUNT = 5;
    private static final Semaphore SEMAPHORE = new Semaphore(2);

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    SEMAPHORE.acquire();
                    System.out.println(Thread.currentThread().getName() + " 获取到许可证,开始访问资源");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 访问资源完成,释放许可证");
                    SEMAPHORE.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. JVM 的内存模型,堆和栈的主要区别是什么?
    • 存储内容
      • :主要用于存储对象实例和数组。Java 中的所有对象都在堆上分配内存,堆是线程共享的,多个线程可以同时访问堆中的对象。
      • :主要用于存储局部变量和方法调用的上下文信息。每个线程都有自己独立的栈,栈中的数据是线程私有的。
    • 内存分配和回收
      • :内存分配和回收由垃圾回收器(GC)负责。当对象不再被引用时,GC 会自动回收对象所占用的内存。堆的内存分配和回收相对较慢,因为需要考虑垃圾回收的性能和效率。
      • :内存分配和回收由 JVM 自动完成。当方法被调用时,会在栈上为该方法分配一个栈帧,用于存储局部变量和方法调用的上下文信息。当方法执行完毕后,栈帧会被自动销毁,释放所占用的内存。栈的内存分配和回收速度较快,因为是按照后进先出(LIFO)的原则进行操作。
    • 内存大小
      • :堆的内存大小通常比较大,可以通过 JVM 参数进行调整。堆的大小会影响垃圾回收的性能和应用程序的运行效率。
      • :栈的内存大小相对较小,每个线程的栈空间是有限的。如果方法调用的深度过深,可能会导致栈溢出异常(StackOverflowError)。
  2. 在多线程环境下,使用 ArrayList 会有什么问题,如何解决?
    • 问题:ArrayList 不是线程安全的类。在多线程环境下,如果多个线程同时对 ArrayList 进行读写操作,可能会导致数据不一致、数组越界等问题。例如,一个线程在删除元素时,另一个线程同时进行添加元素的操作,可能会导致数组下标越界异常。
    • 解决方法
      • 使用 Vector:Vector 是线程安全的,它的方法都是同步的。但由于同步操作会带来一定的性能开销,所以在单线程环境下不建议使用。
import java.util.Vector;

public class VectorExample {
    public static void main(String[] args) {
        Vector<String> vector = new Vector<>();
        vector.add("element");
    }
}
    - **使用 Collections.synchronizedList()**:可以将 ArrayList 转换为线程安全的列表。它通过在每个方法上添加同步锁来保证线程安全。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        List<String> synchronizedList = Collections.synchronizedList(list);
        synchronizedList.add("element");
    }
}
    - **使用 CopyOnWriteArrayList**:它是一种线程安全的列表,采用写时复制的策略。在进行写操作时,会创建一个新的数组副本,将修改后的元素写入新数组,然后将原数组引用指向新数组。读操作不需要加锁,因此读操作的性能较高。但写操作会有一定的开销,因为需要复制数组。
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("element");
    }
}
  1. Spring 的核心特性有哪些,IOC 和 AOP 分别是什么?
    • Spring 的核心特性
      • IoC(Inversion of Control,控制反转):将对象的创建和管理交给 Spring 容器,而不是由对象本身来控制。通过依赖注入(Dependency Injection,DI)的方式,将对象之间的依赖关系注入到对象中,降低了对象之间的耦合度。
      • AOP(Aspect-Oriented Programming,面向切面编程):允许在不修改原有代码的情况下,对程序的功能进行增强。通过将横切关注点(如日志记录、事务管理等)封装成切面,在特定的连接点(如方法调用、异常抛出等)插入切面代码,实现对程序的增强。
      • 事务管理:Spring 提供了统一的事务管理接口,支持编程式事务和声明式事务。声明式事务通过 AOP 实现,将事务管理代码与业务逻辑代码分离,提高了代码的可维护性。
      • MVC 框架:Spring MVC 是一个基于 Servlet 的 Web 框架,用于构建 Web 应用程序。它采用了 MVC(Model-View-Controller)架构模式,将业务逻辑、数据和视图分离,提高了代码的可维护性和可测试性。
    • IOC(控制反转):传统的程序中,对象的创建和依赖关系的管理由对象本身负责,而在 Spring 中,将这些控制权反转给了 Spring 容器。通过 XML 配置文件、注解等方式,Spring 容器负责创建对象,并将对象之间的依赖关系注入到对象中。例如:
// 定义一个接口
interface Service {
    void doSomething();
}
// 实现接口
class ServiceImpl implements Service {
    @Override
    public void doSomething() {
        System.out.println("Doing something");
    }
}
// 定义一个依赖 Service 的类
class Client {
    private Service service;
    public Client(Service service) {
        this.service = service;
    }
    public void callService() {
        service.doSomething();
    }
}
// 在 Spring 配置文件中配置对象和依赖关系
// <bean id="service" class="ServiceImpl"/>
// <bean id="client" class="Client">
//     <constructor-arg ref="service"/>
// </bean>
- **AOP(面向切面编程)**:AOP 的主要概念包括切面(Aspect)、连接点(Join Point)、切点(Pointcut)、通知(Advice)和引入(Introduction)。切面是一个横切关注点的模块化,包含了通知和切点。连接点是程序执行过程中的某个点,如方法调用、异常抛出等。切点是一组连接点的集合,用于定义哪些连接点会被增强。通知是在切点处执行的代码,包括前置通知、后置通知、环绕通知等。引入是在不修改现有类的情况下,为类添加新的方法或属性。例如,使用 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 logAfter(JoinPoint joinPoint) {
        System.out.println("Method " + joinPoint.getSignature().getName() + " executed");