《互联网大厂Java面试:核心知识大考验》

38 阅读14分钟

面试官:请简要介绍一下Java核心知识中的面向对象编程概念,以及它在实际业务场景中的应用。

王铁牛:面向对象编程主要有封装、继承和多态。封装就是把数据和操作数据的方法封装在一起。继承可以实现代码复用。多态就是同一个行为具有多个不同表现形式。在实际业务中,比如开发一个电商系统,商品类可以封装商品的属性和操作方法,不同类型的商品可以继承商品类,并且根据自身特点实现不同的行为,这就是多态的体现。

面试官:不错,回答得很清晰。那再问你,JUC中常用的并发工具类有哪些,以及它们在高并发场景下如何保证数据的一致性?

王铁牛:JUC里有CountDownLatch、CyclicBarrier、Semaphore这些。CountDownLatch可以让一个或多个线程等待其他线程完成操作。CyclicBarrier能让一组线程互相等待,直到所有线程都到达某个屏障点。Semaphore用于控制同时访问某个资源的线程数量。在高并发场景下,通过合理使用这些工具类,比如用CountDownLatch来确保所有初始化操作完成后主线程再继续执行,就能保证数据一致性。

面试官:嗯,理解得还算可以。接下来问你关于JVM的问题,Java内存区域分为哪些,各自的作用是什么?

王铁牛:Java内存区域有堆、栈、方法区等。堆用来存放对象实例。栈主要存放局部变量。方法区存储类信息、常量、静态变量等。

面试官:第一轮面试结束,整体表现还不错,稍等进入第二轮面试。

面试官:多线程中,如何创建一个线程,有几种方式,它们有什么区别?

王铁牛:可以通过继承Thread类,重写run方法来创建线程。还可以实现Runnable接口,实现run方法。另外还有Callable接口,通过FutureTask包装来创建线程。继承Thread类的话,多个线程之间不好共享资源,实现Runnable接口更适合资源共享,Callable接口可以有返回值。

面试官:线程池的核心参数有哪些,分别有什么作用?

王铁牛:核心参数有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。corePoolSize是线程池的核心线程数,maximumPoolSize是最大线程数,keepAliveTime是线程池线程数超过corePoolSize后,多余线程的存活时间,unit是时间单位,workQueue是任务队列,threadFactory是线程工厂,handler是拒绝策略。

面试官:说说HashMap的底层数据结构,以及在多线程环境下可能出现的问题。

王铁牛:HashMap底层是数组加链表加红黑树。在多线程环境下,可能会出现死循环、数据丢失等问题。比如在扩容时,如果多个线程同时操作,就可能导致链表形成环形结构,造成死循环。

面试官:第二轮面试结束,表现有好有坏,准备进入第三轮面试。

面试官:Spring中,依赖注入有几种方式,它们的实现原理是什么?

王铁牛:有构造器注入、setter方法注入、接口注入。构造器注入是通过构造函数传入依赖对象,setter方法注入是通过setter方法设置依赖对象,接口注入是通过实现特定接口来注入依赖。实现原理就是通过反射机制来获取和设置对象的属性。

面试官:Spring Boot的自动配置原理是什么,它是如何简化开发的?

王铁牛:Spring Boot的自动配置原理是通过@EnableAutoConfiguration注解,它会根据类路径下的依赖自动配置相关的Bean。它通过约定大于配置的原则,减少了很多样板代码,让开发人员可以更专注于业务逻辑,比如自动配置数据源、事务管理等,大大提高了开发效率。

面试官:MyBatis的核心配置文件中主要配置哪些内容,它是如何实现数据库操作的?

王铁牛:核心配置文件里配置数据源、事务管理器、映射器等。它通过SQL映射文件,将SQL语句和Java方法进行映射,通过SqlSessionFactory创建SqlSession,再通过SqlSession执行SQL语句来实现数据库操作。

面试官:三轮面试结束了,整体来看,你对一些基础知识有一定的了解,但在复杂问题上回答得不是很理想。我们会综合评估,然后通知你是否通过面试,回家等通知吧。

答案:

  1. Java核心知识中的面向对象编程概念及应用
    • 封装:把数据和操作数据的方法封装在一起,对外提供统一的访问接口。比如商品类,将商品的属性(如价格、名称等)和操作方法(如计算总价等)封装起来,外部只能通过规定的接口来访问和操作这些数据,提高了数据的安全性和可维护性。
    • 继承:实现代码复用的机制。例如,不同类型的商品(如电子产品、服装等)可以继承商品类,继承商品类的通用属性和方法,同时可以根据自身特点添加特殊的属性和方法,减少了重复代码的编写。
    • 多态:同一个行为具有多个不同表现形式。在电商系统中,商品类可以有不同的子类,如手机类、衣服类等,它们都继承自商品类。当调用某个商品的展示方法时,不同子类的商品会根据自身特点展示不同的内容,这就是多态的体现。多态提高了代码的灵活性和扩展性。
  2. JUC中常用的并发工具类及保证数据一致性的方式
    • CountDownLatch:用于让一个或多个线程等待其他线程完成操作。例如,在一个多线程任务中,有几个初始化操作需要并行执行,完成后主线程才能继续。可以创建一个CountDownLatch,设置计数器的值为初始化操作的数量。每个初始化线程完成任务后调用countDown()方法,主线程调用await()方法等待,直到计数器变为0,主线程才会继续执行,从而保证了数据在所有初始化操作完成后才进行后续处理,保证了数据的一致性。
    • CyclicBarrier:能让一组线程互相等待,直到所有线程都到达某个屏障点。比如在一个多线程计算任务中,每个线程负责一部分数据的计算,当所有线程都计算完成后,需要进行汇总操作。可以使用CyclicBarrier,所有线程在计算完成后调用await()方法等待,当所有线程都到达这个屏障点时,才会继续执行后续的汇总操作,确保了数据的完整性和一致性。
    • Semaphore:用于控制同时访问某个资源的线程数量。例如,在一个系统中,有一个共享资源(如数据库连接池),为了避免过多线程同时访问导致资源耗尽或性能问题,可以使用Semaphore来限制同时访问的线程数量。通过acquire()方法获取许可,release()方法释放许可,保证了资源的合理使用,从而保证了数据在资源访问上的一致性。
  3. Java内存区域及其作用
    • :用来存放对象实例。所有的对象实例都在堆中分配内存。比如创建一个User对象,User对象的实例就会存放在堆中。堆是Java内存管理中最重要的区域之一,因为对象的创建、销毁和垃圾回收都与堆密切相关。
    • :主要存放局部变量。当一个方法被调用时,方法内的局部变量会在栈中分配内存。例如,在一个方法中定义一个局部变量int num = 10,这个num变量就会存放在栈中。栈的内存分配和回收速度很快,因为它遵循先进后出的原则。
    • 方法区:存储类信息、常量、静态变量等。类的字节码文件加载后会存放在方法区,类的元数据(如类名、父类、接口等)也在方法区。常量池中的常量(如字符串常量)和静态变量也都在方法区。方法区在Java 8及以后被称为元空间,它的内存分配和回收与堆不同,相对独立。
  4. 多线程中创建线程的方式及区别
    • 继承Thread类:通过继承Thread类,并重写run方法来创建线程。例如:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("This is a thread created by extending Thread class");
    }
}
MyThread thread = new MyThread();
thread.start();

这种方式的缺点是多个线程之间不好共享资源,因为每个线程都有自己独立的Thread对象。

  • 实现Runnable接口:实现Runnable接口,实现run方法来创建线程。例如:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("This is a thread created by implementing Runnable interface");
    }
}
Thread thread = new Thread(new MyRunnable());
thread.start();

这种方式更适合资源共享,因为多个线程可以共享同一个Runnable对象。

  • 实现Callable接口:通过实现Callable接口,重写call方法来创建线程,并且可以有返回值。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
try {
    Integer result = futureTask.get();
    System.out.println("The result is: " + result);
} catch (Exception e) {
    e.printStackTrace();
}

这种方式适用于需要获取线程执行结果的场景。 5. 线程池的核心参数及作用

  • corePoolSize:线程池的核心线程数。当提交的任务数小于corePoolSize时,线程池会创建新的线程来执行任务。例如,如果corePoolSize设置为5,当提交5个以下任务时,会创建相应数量的线程来处理。
  • maximumPoolSize:线程池的最大线程数。当提交的任务数大于corePoolSize,且任务队列已满时,线程数会增加到maximumPoolSize。如果此时线程数达到maximumPoolSize,再有新任务提交,就会根据拒绝策略进行处理。
  • keepAliveTime:线程池线程数超过corePoolSize后,多余线程的存活时间。例如,如果keepAliveTime设置为60秒,那么超过corePoolSize的线程在空闲60秒后会被销毁。
  • unit:keepAliveTime的时间单位。可以是秒、分钟、小时等。
  • workQueue:任务队列。用于存放提交的任务,当任务数小于corePoolSize时,任务会放入队列中等待线程执行。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
  • threadFactory:线程工厂。用于创建线程,通过它可以自定义线程的名称、优先级等属性。
  • handler:拒绝策略。当线程数达到maximumPoolSize且任务队列已满时,会调用拒绝策略来处理新提交的任务。常见的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(调用者运行)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃最旧的任务)等。
  1. HashMap的底层数据结构及多线程问题
    • 底层数据结构:HashMap底层是数组加链表加红黑树。在JDK 1.8之前,当链表长度大于等于8且数组容量大于等于64时,链表会转换为红黑树,以提高查询效率。当链表长度小于等于6且数组容量大于等于64时,红黑树会转换回链表。例如,当向HashMap中插入数据时,如果计算出的哈希值对应的数组位置为空,就直接插入新节点。如果该位置不为空,就会遍历链表或红黑树,找到相同哈希值的节点进行处理(如果是插入则添加到链表或红黑树末尾,如果是更新则覆盖值)。
    • 多线程问题
      • 死循环:在多线程环境下,当进行扩容操作时,如果多个线程同时操作,可能会导致链表形成环形结构,造成死循环。例如,在扩容时,新的数组大小是原来的两倍,需要重新计算每个节点在新数组中的位置。如果多个线程同时进行这个操作,可能会出现链表节点的引用混乱,最终导致死循环。
      • 数据丢失:当多个线程同时对HashMap进行读写操作时,如果一个线程在读取数据时,另一个线程正在对HashMap进行扩容操作,可能会导致数据丢失。因为扩容时会重新计算节点位置,可能会覆盖掉原来的节点数据。
  2. Spring中依赖注入的方式及实现原理
    • 构造器注入:通过构造函数传入依赖对象。例如:
class UserService {
    private final Dao dao;
    public UserService(Dao dao) {
        this.dao = dao;
    }
}

实现原理是通过反射机制,在创建UserService对象时,根据构造函数的参数类型找到对应的依赖对象,并通过反射调用构造函数来完成对象的创建和依赖注入。

  • setter方法注入:通过setter方法设置依赖对象。例如:
class UserService {
    private Dao dao;
    public void setDao(Dao dao) {
        this.dao = dao;
    }
}

实现原理同样是利用反射机制,在创建UserService对象后,通过反射调用setter方法来设置依赖对象。

  • 接口注入:通过实现特定接口来注入依赖。例如:
interface DependencyInjector {
    void inject(Dao dao);
}
class UserService implements DependencyInjector {
    private Dao dao;
    @Override
    public void inject(Dao dao) {
        this.dao = dao;
    }
}

实现原理是通过实现特定接口,在需要注入依赖的地方调用接口的方法来注入依赖对象,也是基于反射机制来找到对应的实现类并调用其方法进行注入。 8. Spring Boot的自动配置原理及简化开发的方式

  • 自动配置原理:Spring Boot的自动配置原理是通过@EnableAutoConfiguration注解,它会根据类路径下的依赖自动配置相关的Bean。例如,如果项目中引入了Spring Data JPA的依赖,Spring Boot会自动配置数据源、事务管理器等相关的Bean,根据约定的配置规则来创建和配置这些组件。它会扫描类路径下的META - INF/spring.factories文件,里面定义了各种自动配置类,Spring Boot会根据这些配置类来自动创建和管理Bean。
  • 简化开发的方式:通过约定大于配置的原则,减少了很多样板代码。开发人员不需要手动配置大量的XML文件或编写复杂的配置类来创建和管理各种组件。比如对于数据源的配置,Spring Boot会根据默认的配置规则,自动从应用程序的配置文件中读取相关信息来配置数据源,大大提高了开发效率。同时,Spring Boot还提供了各种starter依赖,开发人员只需要引入相应的starter依赖,就可以快速集成各种功能,如Spring Data JPA的starter依赖可以快速搭建基于JPA的数据库访问功能。
  1. MyBatis的核心配置文件内容及数据库操作实现方式
    • 核心配置文件内容:主要配置数据源、事务管理器、映射器等。例如:
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/yourdb"/>
                <property name="username" value="root"/>
                <property name="password" value="password"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>
</configuration>

这里配置了数据源(包括驱动、URL、用户名、密码等)、事务管理器(采用JDBC方式),并引入了映射器配置文件。

  • 数据库操作实现方式:通过SQL映射文件,将SQL语句和Java方法进行映射。例如在UserMapper.xml文件中:
<mapper namespace="com.example.mapper.UserMapper">
    <select id="getUserById" parameterType="int" resultType="com.example.model.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

定义了一个查询用户的SQL语句。在Java代码中,通过SqlSessionFactory创建SqlSession,再通过SqlSession执行SQL语句。例如:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("com.example.mapper.UserMapper.getUserById", 1);
sqlSession.close();

通过这种方式实现了数据库操作,将SQL语句和Java方法进行了有效的关联,方便了数据的持久化操作。