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

39 阅读15分钟

在一间明亮却略显严肃的面试房间里,一位神色专注的面试官正准备对前来应聘Java岗位的求职者展开考察。

面试官:“第一轮面试开始,先从基础的Java核心知识问起。第一个问题,Java中重载和重写的区别是什么?”

王铁牛:“重载是在一个类中,方法名相同但参数列表不同;重写是子类重写父类的方法,方法名、参数列表和返回值类型都要一样,不过返回值类型可以是父类返回值类型的子类,而且重写的方法访问修饰符不能比父类更严格。”

面试官:“回答得不错。第二个问题,ArrayList和HashMap在存储原理上有什么不同?”

王铁牛:“ArrayList是基于数组实现的,按顺序存储元素;HashMap是基于哈希表,通过键的哈希值来确定存储位置,以键值对形式存储。”

面试官:“很好。最后一个问题,在多线程场景下,ArrayList直接使用会有什么问题?”

王铁牛:“ArrayList不是线程安全的,多线程同时操作可能会出现数据不一致,比如添加元素时可能会覆盖或丢失数据。”

面试官:“第一轮表现不错。接下来第二轮,我们聊聊JUC和多线程。第一个问题,线程池的核心参数有哪些,分别有什么作用?”

王铁牛:“有核心线程数、最大线程数、存活时间、时间单位,还有任务队列。核心线程数就是线程池一开始创建的线程数量,最大线程数是能创建的最大线程数,存活时间是线程空闲多久会被销毁,时间单位就是存活时间的单位,任务队列就是存放提交但还没执行任务的地方。”

面试官:“还行。第二个问题,说说AQS(AbstractQueuedSynchronizer)的原理。”

王铁牛:“嗯……AQS好像是用来实现同步器的,它有个队列,线程获取不到锁就会进入队列等待,具体怎么实现的我不太清楚了。”

面试官:“好吧。第三个问题,在高并发场景下,如何保证线程安全的同时提高性能?”

王铁牛:“可以用线程池,还有用并发包下的一些类,像ConcurrentHashMap,不过具体怎么提高性能我也说不太明白。”

面试官:“第二轮整体一般。进入第三轮,谈谈框架相关。第一个问题,Spring的IOC和AOP是什么,有什么作用?”

王铁牛:“IOC是控制反转,把对象的创建和管理交给Spring容器,这样代码耦合度降低;AOP是面向切面编程,能把一些通用功能,比如日志、事务切到业务逻辑中,不影响业务代码的独立性。”

面试官:“不错。第二个问题,Spring Boot相比Spring,优势在哪里?”

王铁牛:“Spring Boot简化了Spring的配置,能快速搭建项目,内置了很多插件,开发起来更方便。”

面试官:“挺好。第三个问题,MyBatis的一级缓存和二级缓存有什么区别,在实际项目中怎么应用?”

王铁牛:“一级缓存是SqlSession级别的,在一个SqlSession内查询相同数据会从缓存取;二级缓存是mapper级别的,多个SqlSession都能共享。实际应用中,一级缓存默认开启,二级缓存要手动配置,不过要注意数据一致性问题,更新数据时要清理缓存。”

面试官:“第四个问题,Dubbo的服务调用流程是怎样的?”

王铁牛:“嗯……好像是服务提供者注册服务到注册中心,服务消费者从注册中心获取服务,然后进行调用,具体细节不太清楚。”

面试官:“最后一个问题,RabbitMQ在项目中如何保证消息不丢失?”

王铁牛:“可以开启持久化,还有确认机制,不过具体怎么操作不太记得了。”

面试官:“今天的面试就到这里。你在基础的Java知识和部分框架基础概念上回答得还可以,但对于一些深入的原理和实际应用场景的细节掌握得不够扎实。我们后续会综合评估所有候选人,你回家等通知吧,无论结果如何,我们都会在一周内给你回复。”

问题答案

  1. Java中重载和重写的区别
    • 重载(Overloading):发生在同一个类中,方法名必须相同,参数列表不同(参数个数、类型、顺序至少有一个不同),与方法的返回值类型、访问修饰符无关。例如:
public class OverloadingExample {
    public void method(int a) {
        // 方法实现
    }
    public void method(String s) {
        // 方法实现
    }
}
- **重写(Overriding)**:发生在子类与父类之间,子类重写父类的方法。方法名、参数列表、返回值类型(返回值类型可以是父类返回值类型的子类,即协变返回类型)必须相同,访问修饰符不能比父类更严格(例如父类方法是protected,子类重写方法不能是private)。同时,子类重写方法不能抛出比父类更多的异常(可以抛出更少或不抛出异常)。例如:
class Animal {
    public void makeSound() {
        System.out.println("Animal makes sound");
    }
}
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. ArrayList和HashMap在存储原理上的不同
    • ArrayList:基于动态数组实现。它在内存中是连续存储的,元素按照添加顺序依次存放。当添加元素超过当前数组容量时,会进行扩容,一般是扩容为原来容量的1.5倍。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
- **HashMap**:基于哈希表实现。它以键值对(key - value)的形式存储数据。通过对键进行哈希运算得到哈希值,再根据哈希值确定元素在哈希表中的存储位置。如果发生哈希冲突(不同的键计算出相同的哈希值),会使用链地址法(在Java 8之前)或红黑树(Java 8及之后,当链表长度超过8时会转换为红黑树)来解决冲突。例如:
HashMap<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
  1. 在多线程场景下,ArrayList直接使用会有什么问题: ArrayList不是线程安全的。在多线程环境下,多个线程同时对ArrayList进行添加、删除等操作时,可能会出现数据不一致的情况。比如,当一个线程正在添加元素时,另一个线程也在添加元素,可能会导致元素覆盖、丢失或者数组越界等问题。例如:
ArrayList<Integer> list = new ArrayList<>();
Thread thread1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        list.add(i);
    }
});
Thread thread2 = new Thread(() -> {
    for (int i = 1000; i < 2000; i++) {
        list.add(i);
    }
});
thread1.start();
thread2.start();
// 最终list的大小可能不是2000,会出现数据丢失等情况
  1. 线程池的核心参数有哪些,分别有什么作用
    • 核心线程数(corePoolSize):线程池在初始化后,会创建corePoolSize个核心线程,这些线程会一直存活,即使处于空闲状态也不会被销毁(除非设置了allowCoreThreadTimeOut为true)。例如,一个Web服务器的线程池,一开始就创建一定数量的核心线程来处理请求。
    • 最大线程数(maximumPoolSize):线程池允许创建的最大线程数量。当任务队列已满,且核心线程都在忙碌时,线程池会创建新的线程,直到线程数量达到maximumPoolSize。但如果此时任务队列已满且线程数已达到最大线程数,新的任务会根据拒绝策略进行处理。
    • 存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在存活时间内没有接到新任务,就会被销毁。例如,设置存活时间为10秒,那么空闲超过10秒的多余线程会被销毁。
    • 时间单位(unit):存活时间的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
    • 任务队列(workQueue):用于存放提交但尚未执行的任务。常见的任务队列有ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列,默认容量为Integer.MAX_VALUE)、SynchronousQueue(不存储任务,直接提交给线程处理)等。不同的任务队列特性会影响线程池的性能和行为。
  2. AQS(AbstractQueuedSynchronizer)的原理: AQS是Java并发包中很多同步器实现的基础框架。它内部维护了一个FIFO的双向队列(CLH队列),用于存放等待获取锁的线程。当一个线程尝试获取锁失败时,会被包装成一个节点加入到队列尾部。AQS通过一个int类型的变量state来表示同步状态,不同的同步器对state的含义和操作方式不同。例如,ReentrantLock中,state表示锁的持有次数,当state为0时表示锁未被持有,线程获取锁成功后会将state加1,再次获取锁(重入)时state继续加1,释放锁时state减1,当state为0时表示锁已完全释放。AQS提供了一些模板方法,如acquire、release等,具体的同步器通过重写这些方法来实现自己的同步逻辑。
  3. 在高并发场景下,如何保证线程安全的同时提高性能
    • 使用线程池:合理配置线程池参数,复用线程,减少线程创建和销毁的开销。例如,对于I/O密集型任务,可以设置较大的核心线程数,因为I/O操作等待时间长,线程空闲时可以处理其他任务;对于CPU密集型任务,核心线程数应接近CPU核心数,避免过多线程竞争CPU资源。
    • 使用并发包下的类:如ConcurrentHashMap,它采用分段锁机制(Java 8之前)或CAS操作(Java 8及之后),允许多个线程同时对不同的段进行操作,提高并发性能。而传统的HashMap在多线程环境下会出现数据不一致问题。又如CopyOnWriteArrayList,在进行写操作时会复制一份新的数组,读操作则直接读取原数组,读写分离,保证线程安全的同时提高读性能,但写操作开销较大,适用于读多写少的场景。
    • 减少锁的粒度:避免对整个对象或方法加锁,而是对关键的代码块加锁。例如,在一个包含多个操作的方法中,如果只有部分操作需要同步,可以只对这部分操作使用synchronized代码块,而不是对整个方法加锁。
    • 使用无锁数据结构:如原子类(AtomicInteger、AtomicLong等),它们通过硬件级别的CAS操作实现无锁的原子性操作,性能比使用锁更高。
  4. Spring的IOC和AOP是什么,有什么作用
    • IOC(Inversion of Control,控制反转)
      • 概念:将对象的创建和管理控制权从应用程序代码转移到Spring容器中。在传统编程中,对象的创建和依赖关系管理由开发者在代码中手动完成,而在Spring中,通过配置(XML或注解)告诉Spring容器需要创建哪些对象以及它们之间的依赖关系,Spring容器负责创建和注入这些对象。
      • 作用:降低代码的耦合度。例如,一个Service类依赖另一个Dao类,如果在Service类中手动创建Dao对象,那么Service类与Dao类的具体实现紧密耦合。使用IOC后,Spring容器可以根据配置提供不同的Dao实现,Service类只需要声明依赖,而不需要关心具体的创建过程,使得代码更易于维护和扩展。
    • AOP(Aspect - Oriented Programming,面向切面编程)
      • 概念:将一些通用的功能(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,形成独立的切面(Aspect),然后在运行时将这些切面动态地织入到业务逻辑中。
      • 作用:提高代码的可维护性和复用性。以日志记录为例,如果在每个业务方法中都手动添加日志记录代码,会导致代码冗余且难以维护。使用AOP,可以将日志记录逻辑封装在切面中,通过配置切点(Pointcut)指定在哪些方法上应用该切面,这样业务代码只关注核心业务逻辑,通用功能可以统一管理和维护。
  5. Spring Boot相比Spring,优势在哪里
    • 快速搭建项目:Spring Boot提供了大量的starter依赖,通过引入相关的starter,就能快速集成各种框架和功能,如Spring Boot Starter Web可以快速搭建一个Web应用,无需像Spring那样进行繁琐的配置。
    • 简化配置:Spring Boot采用约定大于配置的原则,很多配置都有默认值,开发者只需要在少量情况下进行自定义配置。例如,对于数据库连接,Spring Boot可以根据引入的数据库驱动自动配置数据源等相关信息,而在Spring中需要手动配置大量的XML或Java配置类。
    • 内置服务器:Spring Boot内置了Tomcat、Jetty等服务器,可直接将应用打包成可执行的jar或war文件,通过命令行即可启动应用,方便部署和测试。
    • 监控和管理:Spring Boot Actuator提供了对应用的监控和管理功能,如查看应用的健康状态、性能指标等,方便运维和调试。
  6. MyBatis的一级缓存和二级缓存有什么区别,在实际项目中怎么应用
    • 一级缓存
      • 作用域:一级缓存是SqlSession级别的缓存,在同一个SqlSession内,执行相同的SQL查询时,MyBatis会先从一级缓存中查找数据,如果存在则直接返回,不再执行数据库查询。
      • 生命周期:与SqlSession的生命周期一致,当SqlSession关闭或提交事务时,一级缓存会被清空。
      • 应用场景:适用于在一个SqlSession内多次执行相同查询的场景,能减少数据库查询次数,提高性能。例如,在一个Service方法中多次调用同一个Mapper方法查询相同数据,一级缓存可以发挥作用。
    • 二级缓存
      • 作用域:二级缓存是mapper级别的缓存,多个SqlSession可以共享二级缓存。当一个SqlSession查询数据并将结果放入二级缓存后,其他SqlSession执行相同查询时可以从二级缓存获取数据。
      • 生命周期:二级缓存的生命周期比一级缓存长,它在应用程序运行期间一直存在,除非手动清除或应用程序关闭。
      • 应用场景:适用于不同SqlSession之间需要共享数据的场景,如多个Service方法可能会调用同一个Mapper方法查询相同数据。但要注意数据一致性问题,当数据发生变化(增删改操作)时,需要及时清理二级缓存,否则可能会读到脏数据。在MyBatis中,需要在Mapper配置文件中开启二级缓存,并配置相关的缓存策略(如LRU、FIFO等)。
  7. Dubbo的服务调用流程
    • 服务注册:服务提供者启动时,将自己提供的服务注册到注册中心(如Zookeeper、Nacos等)。注册中心记录了服务提供者的地址、端口、服务接口等信息。
    • 服务订阅:服务消费者启动时,向注册中心订阅自己需要的服务。注册中心会将服务提供者的信息推送给服务消费者。
    • 服务调用:服务消费者根据从注册中心获取的服务提供者信息,通过网络通信(如Netty)调用服务提供者的接口。在调用过程中,Dubbo支持负载均衡策略(如随机、轮询、权重等),选择一个合适的服务提供者实例进行调用。如果服务提供者有多个实例,Dubbo会根据负载均衡算法选择其中一个进行调用。
    • 监控与治理:Dubbo提供了监控中心,用于统计服务的调用次数、调用时间等指标。同时,通过控制台可以对服务进行治理,如动态调整服务的权重、流量控制等。
  8. RabbitMQ在项目中如何保证消息不丢失
    • 生产者端
      • 开启确认机制(publisher confirm):生产者发送消息后,RabbitMQ会给生产者发送确认消息,告知消息是否成功到达Broker。生产者可以通过实现ConfirmCallback接口来处理确认结果。如果未收到确认消息,生产者可以进行消息重发。例如:
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
    if (!ack) {
        // 处理消息未确认情况,进行重发等操作
    }
});
    - **开启事务机制(transaction)**:生产者发送消息前开启事务,发送消息后提交事务。如果消息发送失败,事务回滚,生产者可以重新发送消息。但事务机制会严重影响性能,一般不建议在高并发场景下使用。例如:
rabbitTemplate.execute(new RabbitTemplate.ConfirmCallback() {
    @Override
    public Object doInRabbit(RabbitTemplate rabbitTemplate) throws AmqpException {
        rabbitTemplate.setChannelTransacted(true);
        try {
            rabbitTemplate.convertAndSend("exchange", "routingKey", "message");
            rabbitTemplate.execute(channel -> {
                channel.txCommit();
                return null;
            });
        } catch (Exception e) {
            rabbitTemplate.execute(channel -> {