《揭秘互联网大厂Java面试:从基础到进阶的核心知识大考察》

46 阅读12分钟

在一间明亮却略显严肃的面试室内,一位身着正装的面试官正准备对面前的求职者进行一场关于Java技术栈的深度考察。

面试官:好,我们开始面试。第一轮,先从Java基础问起。第一个问题,ArrayList和HashMap在存储结构上有什么区别? 王铁牛:ArrayList是基于数组的,能顺序存储数据。HashMap是基于哈希表,通过键值对存储。 面试官:回答得不错。第二个问题,HashMap在什么情况下会发生哈希冲突,怎么解决哈希冲突? 王铁牛:呃,数据多的时候可能会冲突吧,好像是用链表解决。 面试官:嗯,方向对。最后一个问题,Java中多线程创建有几种方式? 王铁牛:有继承Thread类,还有实现Runnable接口。 面试官:第一轮表现还行。接下来第二轮,我们聊聊JUC和多线程。第一个问题,线程池有哪些核心参数,分别有什么作用? 王铁牛:有核心线程数,最大线程数,还有个队列啥的。核心线程数好像是一开始的线程数量,最大就是最多能有多少线程。 面试官:回答得不太完整。第二个问题,在高并发场景下,如何保证线程安全,除了synchronized关键字还有哪些方式? 王铁牛:嗯……可以用Lock接口吧,具体咋用不太清楚。 面试官:好,第三个问题,CountDownLatch和CyclicBarrier有什么区别,在业务场景中怎么用? 王铁牛:这……不太知道,好像都是和线程同步有关。 面试官:第二轮表现不太理想。进入第三轮,说说框架相关。第一个问题,Spring的IOC和AOP是什么,在项目中有什么实际应用? 王铁牛:IOC是控制反转,对象创建由Spring容器管理。AOP是面向切面编程,比如可以用来做日志记录。 面试官:回答得还行。第二个问题,Spring Boot和Spring有什么区别,Spring Boot的优势在哪? 王铁牛:Spring Boot更简化,不用配置那么多东西,开发快。 面试官:好,第三个问题,MyBatis中#{}和{}有什么区别,在SQL注入防范上有什么不同? **王铁牛**:#{}好像能防注入,{}不能,具体为啥不太明白。 面试官:第四个问题,Dubbo在分布式系统中有什么作用,它的架构原理是什么? 王铁牛:是做服务治理的,架构原理不太清楚。 面试官:最后一个问题,RabbitMQ在消息队列场景下,如何保证消息的可靠投递? 王铁牛:呃,不太确定,好像有确认机制啥的。

面试官思索片刻后说道:“今天的面试就到这里,你的表现有亮点也有不足。后续我们会综合评估所有候选人,你回家等通知吧。如果通过,我们会在一周内联系你。”

问题答案

  1. ArrayList和HashMap在存储结构上有什么区别
    • ArrayList:基于动态数组实现,它按照顺序存储元素,元素在内存中是连续存储的。这种结构适合频繁的按顺序访问操作,例如通过索引获取元素,时间复杂度为O(1)。但在插入和删除元素时,如果不是在末尾操作,需要移动大量元素,时间复杂度为O(n)。
    • HashMap:基于哈希表实现,以键值对的形式存储数据。它通过对键进行哈希运算来确定存储位置,理想情况下,插入、删除和查找操作的时间复杂度都接近O(1)。不过,当不同的键经过哈希运算后得到相同的存储位置时,就会发生哈希冲突。
  2. HashMap在什么情况下会发生哈希冲突,怎么解决哈希冲突
    • 发生哈希冲突的情况:当不同的键通过哈希函数计算得到相同的哈希值,从而映射到哈希表的同一个桶(bucket)时,就会发生哈希冲突。这在哈希表中是不可避免的,尤其是在数据量较大且哈希函数不够理想的情况下更容易发生。
    • 解决哈希冲突的方式
      • 链地址法:在Java的HashMap中,当发生哈希冲突时,会在对应的桶位置使用链表来存储冲突的键值对。JDK 1.8之后,如果链表长度超过8且数组长度大于64,链表会转换为红黑树,以提高查找效率。
      • 开放定址法:当发生冲突时,通过某种探测算法在哈希表中寻找下一个空闲位置来存储数据。常见的探测算法有线性探测、二次探测等。
  3. Java中多线程创建有几种方式
    • 继承Thread类:创建一个类继承Thread类,重写run()方法,在run()方法中编写线程执行的逻辑。然后创建该类的实例,并调用start()方法启动线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
MyThread thread = new MyThread();
thread.start();
  • 实现Runnable接口:创建一个类实现Runnable接口,实现run()方法。然后将该类的实例作为参数传递给Thread类的构造函数,再调用start()方法启动线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行中");
    }
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
  • 实现Callable接口:与Runnable类似,但Callable接口的call()方法可以有返回值,并且可以抛出异常。通过FutureTask类来获取Callable任务的执行结果。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    Integer result = futureTask.get();
    System.out.println("任务执行结果: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
  1. 线程池有哪些核心参数,分别有什么作用
    • 核心线程数(corePoolSize):线程池中保持存活的最小线程数。即使这些线程处于空闲状态,也不会被销毁,除非设置了allowCoreThreadTimeOut为true。当有新任务提交时,如果当前线程数小于核心线程数,会创建新的线程来处理任务。
    • 最大线程数(maximumPoolSize):线程池中允许存在的最大线程数。当任务队列已满,且当前线程数小于最大线程数时,会创建新的线程来处理任务。但如果线程数达到最大线程数,且任务队列也已满,新的任务会根据拒绝策略进行处理。
    • 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)、SynchronousQueue(同步队列)等。不同的队列特性会影响线程池的性能和行为。
    • 线程存活时间(keepAliveTime):当线程数大于核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。超过这个时间,空闲线程会被销毁。
    • 时间单位(unit):用于指定线程存活时间的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。
    • 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大线程数时,新任务会被拒绝。常见的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用者线程处理任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,然后尝试提交新任务)。
  2. 在高并发场景下,如何保证线程安全,除了synchronized关键字还有哪些方式
    • 使用Lock接口:Lock接口提供了比synchronized更灵活的锁控制。例如ReentrantLock,它是可重入锁,支持公平锁和非公平锁。使用时,通过lock()方法获取锁,unlock()方法释放锁,并且可以在获取锁时进行中断操作。例如:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class SafeCounter {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}
  • 使用原子类:Java的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger、AtomicLong等。这些类通过硬件级别的原子操作来保证数据的原子性,从而实现线程安全。例如:
import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}
  • 使用线程安全的集合类:如ConcurrentHashMap、CopyOnWriteArrayList等。ConcurrentHashMap采用分段锁机制,允许多个线程同时对不同段进行操作,提高并发性能。CopyOnWriteArrayList在写操作时会复制一份新的数组,读操作则在原数组上进行,保证了读写分离,实现线程安全。
  1. CountDownLatch和CyclicBarrier有什么区别,在业务场景中怎么用
    • 区别
      • CountDownLatch:它允许一个或多个线程等待,直到其他一组线程完成操作。它的计数器只能使用一次,一旦计数为0,就不能再重置。例如,主线程等待多个子线程完成任务后再继续执行。
      • CyclicBarrier:它允许一组线程相互等待,直到所有线程都到达某个屏障点,然后再一起继续执行。它的计数器可以重置,即可以重复使用。例如,多个运动员在起跑线等待,等所有人都准备好后一起起跑。
    • 业务场景应用
      • CountDownLatch:在一个电商系统中,可能需要在生成订单前,等待库存检查、用户信息验证等多个异步任务完成,这时可以使用CountDownLatch。
import java.util.concurrent.CountDownLatch;

public class OrderProcess {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 3;
        CountDownLatch latch = new CountDownLatch(taskCount);

        new Thread(() -> {
            // 库存检查
            System.out.println("库存检查完成");
            latch.countDown();
        }).start();

        new Thread(() -> {
            // 用户信息验证
            System.out.println("用户信息验证完成");
            latch.countDown();
        }).start();

        new Thread(() -> {
            // 支付渠道检查
            System.out.println("支付渠道检查完成");
            latch.countDown();
        }).start();

        latch.await();
        System.out.println("所有任务完成,生成订单");
    }
}
 - **CyclicBarrier**:在一个分布式计算任务中,多个节点需要同时开始计算,并且在每一轮计算结束后等待所有节点都完成,然后再进行下一轮计算。这时可以使用CyclicBarrier。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class DistributedComputing {
    public static void main(String[] args) {
        int nodeCount = 3;
        CyclicBarrier barrier = new CyclicBarrier(nodeCount, () -> {
            System.out.println("所有节点完成一轮计算,开始下一轮");
        });

        for (int i = 0; i < nodeCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 开始计算");
                    // 模拟计算
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 计算完成,等待其他节点");
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  1. Spring的IOC和AOP是什么,在项目中有什么实际应用
    • IOC(控制反转)
      • 概念:IOC是一种设计思想,将对象的创建和管理控制权从应用程序代码转移到Spring容器。Spring容器负责创建对象、管理对象的生命周期以及对象之间的依赖关系。
      • 实际应用:在一个企业级Java项目中,可能有多个服务类,如UserService、OrderService等,这些服务类可能依赖于其他的DAO类(如UserDAO、OrderDAO)。通过IOC,Spring容器可以自动创建这些对象,并注入它们所需的依赖。例如,UserService需要UserDAO,Spring容器可以在创建UserService实例时,将UserDAO的实例注入进去,这样代码中就不需要手动创建和管理这些对象的依赖关系,提高了代码的可维护性和可测试性。
    • AOP(面向切面编程)
      • 概念:AOP是一种编程范式,它将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以切面的形式进行统一管理。这些横切关注点可以在多个业务逻辑中复用。
      • 实际应用:以日志记录为例,在一个电商系统中,可能在多个业务方法(如订单创建、商品查询等)中都需要记录日志。通过AOP,可以创建一个日志切面,在这个切面中定义日志记录的逻辑,然后将这个切面应用到需要记录日志的方法上。这样,不需要在每个业务方法中重复编写日志记录代码,提高了代码的复用性和可维护性。
  2. Spring Boot和Spring有什么区别,Spring Boot的优势在哪
    • 区别
      • 配置方式:Spring需要大量的XML配置文件或者Java配置类来配置各种Bean、数据源、事务等。而Spring Boot采用自动配置机制,默认情况下能根据项目的依赖自动配置大部分常用的组件,大大减少了配置的工作量。
      • 项目搭建:Spring项目搭建相对复杂,需要手动配置Web服务器(如Tomcat)、依赖管理等。Spring Boot内置了Web服务器(如Tomcat、Jetty等),可以直接以jar包的形式运行,项目搭建更加简单快捷。
    • Spring Boot的优势
      • 快速开发:通过自动配置和起步依赖(starter),开发者可以快速搭建项目,减少了繁琐的配置工作,提高了开发效率。例如,添加Spring Boot的Web起步依赖,就可以快速搭建一个Web应用,无需手动配置Tomcat等服务器。
      • 易于集成:Spring Boot可以很方便地与各种第三方框架集成,如数据库连接池、消息队列、缓存等。只需要添加相应的起步依赖,Spring Boot就能自动配置好相关组件。
      • 生产就绪:Spring Boot提供了一些生产环境中常用的特性,如健康检查、指标监控等。可以通过Actuator模块方便地监控和管理应用程序的运行状态。
  3. MyBatis中#{}和${}有什么区别,在SQL注入防范上有什么不同
    • 区别
      • #{}:是预编译处理,MyBatis在处理#{}时,会将SQL中的#{}替换为?占位符,然后使用PreparedStatement进行设置参数值。例如:select * from user where username = #{username},实际执行的SQL是select * from user where username =?,然后通过PreparedStatement的set方法设置参数值。
      • **:是字符串替换,MyBatis在处理{}**:是字符串替换,MyBatis在处理{}时,会直接将中的内容替换到SQL中。例如:selectfromuserwhereusername={}中的内容替换到SQL中。例如:`select * from user where username = {username},如果username的值为“admin”,实际执行的SQL就是select * from user where username = admin`。
    • SQL注入防范
      • #{}:由于采用预编译处理,数据库会将参数值作为一个整体进行处理,不会将参数值解析为SQL语句的一部分,所以能有效防止SQL注入。例如,即使用户输入“admin' or '1'='1”,也不会导致SQL注入。
      • ${}:因为是字符串替换,所以如果参数值来自用户输入且未经过严格过滤,就很容易导致SQL注入。例如,用户输入“admin' or '1'='1”,实际执行的SQL就会变为select * from user where username = admin' or '1'='1,这会导致非法查询。
  4. Dubbo在分布式系统中有什么作用,它的架构原理是什么
    • 作用
      • 服务治理:Dubbo提供了服务注册与发现功能,使得服务提供者可以将自己的服务注册到注册中心,服务消费者可以从注册中心获取服务地址,实现了服务的自动发现和动态配置。同时,Dubbo还支持服务的负载均衡、容错处理等功能,提高了分布式系统的可靠性和性能。
      • 高性能通信:Dubbo采用了多种高性能