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

25 阅读13分钟

面试官:请简要介绍一下 Java 核心知识中面向对象的三大特性。

王铁牛:嗯,面向对象的三大特性是封装、继承和多态。封装就是把数据和操作数据的方法封装在一起,对外提供统一的接口;继承是指一个类可以继承另一个类的属性和方法;多态就是同一个行为具有多个不同表现形式。

面试官:回答得不错。那在多线程编程中,如何确保线程安全?

王铁牛:可以使用 synchronized 关键字来同步代码块或方法,也可以使用 Lock 接口及其实现类,比如 ReentrantLock。

面试官:很好。再问一个,说说 JVM 的内存结构。

王铁牛:JVM 内存结构主要包括堆、栈、方法区、程序计数器、本地方法栈。堆是存放对象实例的地方;栈是存储局部变量、操作数栈等;方法区存储类信息、常量等;程序计数器记录当前线程执行的字节码指令地址;本地方法栈用于执行本地方法。

面试官:第一轮面试结束,表现不错。接下来第二轮,说说线程池的核心参数及其作用。

王铁牛:线程池的核心参数有 corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory 和 handler。corePoolSize 是核心线程数,当提交的任务数小于它时,会创建新线程执行任务;maximumPoolSize 是最大线程数,当任务数超过 corePoolSize 且 workQueue 满时,会创建新线程直到达到这个数;keepAliveTime 是线程池中的线程在空闲时的存活时间;unit 是 keepAliveTime 的时间单位;workQueue 是任务队列,用于存放提交的任务;threadFactory 用于创建线程;handler 是拒绝策略,当线程池满且无法接受新任务时的处理策略。

面试官:那 HashMap 在多线程环境下会有什么问题?

王铁牛:在多线程环境下,HashMap 可能会出现链表形成环形结构,导致死循环,还有可能会出现数据覆盖等问题。

面试官:说说 ArrayList 的线程安全问题。

王铁牛:ArrayList 不是线程安全的,在多线程环境下对其进行读写操作可能会导致数据不一致等问题。

面试官:第二轮结束。现在第三轮,讲讲 Spring 的核心特性。

王铁牛:Spring 的核心特性有依赖注入、面向切面编程、IoC 容器等。依赖注入可以将对象之间的依赖关系通过配置进行注入;面向切面编程能实现业务逻辑和横切关注点的分离;IoC 容器负责创建、管理和装配对象。

面试官:那 Spring Boot 与传统 Spring 相比有哪些优势?

王铁牛:Spring Boot 简化了 Spring 应用的开发,它提供了自动配置,能快速搭建项目,减少了很多繁琐的配置工作。

面试官:说说 MyBatis 的工作原理。

王铁牛:嗯……它是通过 SqlSessionFactory 创建 SqlSession,然后通过 SqlSession 执行 SQL 语句。它会根据映射文件中的配置信息,将 SQL 语句和 Java 对象进行映射。

面试官:面试结束,回去等通知吧。

答案:

  1. 面向对象的三大特性
    • 封装
      • 封装是把数据和操作数据的方法封装在一起,对外提供统一的接口。这使得对象内部的数据可以得到保护,避免外部的非法访问和修改。例如,在一个类中,将一些私有属性通过公共的 get 和 set 方法来访问和修改,外部类只能通过这些公共方法来操作内部数据,而不能直接访问私有属性,这样就保证了数据的安全性和一致性。
    • 继承
      • 继承是指一个类可以继承另一个类的属性和方法。通过继承,可以实现代码的复用。比如,定义一个父类“Animal”,包含一些通用的属性和方法,然后让“Dog”类和“Cat”类继承“Animal”类,它们就可以直接拥有父类的一些属性(如名字、年龄)和方法(如吃东西),同时还可以根据自身特点添加新的属性和方法,这样就避免了重复编写相同的代码。
    • 多态
      • 多态就是同一个行为具有多个不同表现形式。在 Java 中,多态主要体现在方法的重写和重载上。当一个子类继承了父类并重写了父类的方法时,在调用这个方法时,根据对象的实际类型来决定调用哪个类的重写方法。例如,父类有一个方法“move”,子类“Bird”和“Fish”都重写了这个方法,当有一个“Animal”类型的变量指向“Bird”或“Fish”对象时,调用“move”方法会执行不同子类的实现,这就是多态的体现。
  2. 确保线程安全的方法
    • synchronized 关键字
      • 可以用于同步代码块或方法。当一个线程访问被 synchronized 修饰的代码块或方法时,它会首先获取对象的锁。如果锁已经被其他线程持有,那么该线程会进入等待状态,直到锁被释放。例如,在一个多线程访问共享资源的场景中,将访问共享资源的代码块用 synchronized 修饰,就可以保证同一时间只有一个线程能够访问该资源,从而确保线程安全。
    • Lock 接口及其实现类(如 ReentrantLock)
      • Lock 接口提供了比 synchronized 更灵活的锁控制。它可以实现公平锁,即多个线程按照请求锁的顺序依次获得锁,避免了某些线程一直等待锁的情况。例如,使用 ReentrantLock 时,可以通过调用 lock()方法获取锁,unlock()方法释放锁。还可以通过 tryLock()方法尝试获取锁,如果获取不到可以立即返回,不会像 synchronized 那样一直等待,这在一些需要限时获取锁的场景中非常有用。
  3. JVM 的内存结构
      • 堆是存放对象实例的地方。它是 JVM 中最大的一块内存区域,被所有线程共享。对象在堆中分配内存,当对象不再被引用时,会被垃圾回收器回收。例如,我们创建一个类的实例,如“new User()”,这个 User 对象就会被分配到堆内存中。
      • 栈是存储局部变量、操作数栈等。每个线程都有自己独立的栈空间。局部变量在栈中分配内存,当方法执行结束,局部变量就会被销毁。比如,在一个方法中定义一个局部变量“int num = 10”,这个 num 变量就存放在栈中,当方法执行完毕,num 变量就不再存在。
    • 方法区
      • 方法区存储类信息、常量等。它也是被所有线程共享的。类的字节码文件加载后,类的元数据信息就存放在方法区。例如,类的字段、方法定义等都在方法区。常量池也在方法区,存放着各种常量,如字符串常量、数字常量等。
    • 程序计数器
      • 程序计数器记录当前线程执行的字节码指令地址。它是线程私有的。每个线程都有自己的程序计数器,用于指示下一条要执行的指令。比如,当一个线程执行一段代码时,程序计数器会记录当前执行到哪一条字节码指令,以便线程能够准确地继续执行下去。
    • 本地方法栈
      • 用于执行本地方法。本地方法是用其他语言(如 C、C++)编写的方法,通过 JNI(Java Native Interface)与 Java 代码交互。当 Java 代码调用本地方法时,会在本地方法栈中执行相应的操作。
  4. 线程池的核心参数及其作用
    • corePoolSize
      • 核心线程数。当提交的任务数小于它时,线程池会创建新线程来执行任务。例如,一个线程池的 corePoolSize 为 5,当提交的任务数小于 5 时,会有新线程不断创建来执行这些任务。
    • maximumPoolSize
      • 最大线程数。当任务数超过 corePoolSize 且任务队列 workQueue 满时,线程池会创建新线程直到达到这个数。如果超过这个数,任务会根据拒绝策略进行处理。比如,corePoolSize 为 5,workQueue 容量为 10,当提交的任务数达到 15 时,就会开始创建新线程,直到线程数达到 maximumPoolSize。
    • keepAliveTime
      • 线程池中的线程在空闲时的存活时间。当线程空闲时间超过这个值时,非核心线程会被销毁。例如,keepAliveTime 为 60 秒,一个线程执行完任务后,如果 60 秒内没有新任务,它就会被销毁。
    • unit
      • keepAliveTime 的时间单位。可以是 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。
    • workQueue
      • 任务队列。用于存放提交的任务。当提交的任务数小于 corePoolSize 时,任务会被放入这个队列中。常见的任务队列有 ArrayBlockingQueue、LinkedBlockingQueue 等。比如 ArrayBlockingQueue 是一个有界队列,它的容量是固定的,当队列满了,新任务就会根据其他策略处理。
    • threadFactory
      • 用于创建线程。可以自定义线程的名称、优先级等属性。例如,通过自定义 threadFactory 可以创建具有特定名称格式的线程,方便调试和管理。
    • handler
      • 拒绝策略。当线程池满且无法接受新任务时,会调用这个策略来处理新任务。常见的拒绝策略有 AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃新任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)。
  5. HashMap 在多线程环境下的问题
    • 链表形成环形结构导致死循环
      • 在多线程环境下,当扩容时可能会出现链表形成环形结构。比如,在扩容过程中,新链表的头节点和旧链表的头节点可能会形成循环引用。假设两个线程同时进行扩容操作,一个线程在插入新节点时,另一个线程可能会在调整链表结构时导致环形链表的形成。当后续有线程尝试遍历链表时,就会陷入死循环。
    • 数据覆盖
      • 多线程同时对 HashMap 进行 put 操作时,如果两个线程计算出相同的 hash 值,并且插入到同一个位置,就可能导致数据覆盖。例如,线程 A 和线程 B 同时计算出某个 key 的 hash 值相同,都要插入到同一个桶位置,先执行的线程插入的数据可能会被后执行的线程覆盖。
  6. ArrayList 的线程安全问题
    • ArrayList 不是线程安全的。在多线程环境下,当多个线程同时对其进行读写操作时,可能会导致数据不一致等问题。比如,一个线程在遍历 ArrayList 的同时,另一个线程对其进行添加或删除操作,就可能会抛出 ConcurrentModificationException 异常。因为 ArrayList 在遍历过程中会维护一个 modCount 变量记录结构修改的次数,当其他线程修改了结构(如添加或删除元素),而遍历线程没有感知到这个变化时,就会出现问题。
  7. Spring 的核心特性
    • 依赖注入
      • 依赖注入可以将对象之间的依赖关系通过配置进行注入。通过依赖注入,对象不需要自己创建依赖对象,而是由外部容器(如 Spring 的 IoC 容器)提供。例如,一个类需要依赖另一个类来完成某个功能,通过依赖注入,只需要在配置文件或注解中声明依赖关系,Spring 容器会自动创建并注入依赖对象。这样可以降低对象之间的耦合度,提高代码的可维护性和可测试性。
    • 面向切面编程(AOP)
      • 能实现业务逻辑和横切关注点的分离。横切关注点是指一些与业务逻辑无关但又会影响多个业务模块的功能,如日志记录、事务管理等。通过 AOP,可以将这些横切关注点独立出来,以切面的形式织入到业务逻辑中。例如,使用 @Aspect 注解定义一个切面,在切面中编写日志记录的逻辑,然后通过切入点表达式指定在哪些方法执行前或执行后进行日志记录,这样就可以在不修改业务逻辑代码的情况下实现日志功能。
    • IoC 容器
      • IoC 容器负责创建、管理和装配对象。它根据配置信息(如 XML 配置文件或注解)来创建对象,并将它们装配在一起。例如,在 Spring 配置文件中定义了各个 bean 的信息,Spring 容器会根据这些信息创建相应的对象实例,并通过依赖注入等方式将它们之间的依赖关系装配好,使得整个应用的对象之间能够协同工作。
  8. Spring Boot 与传统 Spring 相比的优势
    • 自动配置
      • Spring Boot 提供了自动配置功能。它能够根据项目中引入的依赖自动配置相关的组件和功能。比如,当项目中引入了 Spring Data JPA 依赖,Spring Boot 会自动配置好 JPA 的相关数据源、事务管理器等,开发者只需要简单地编写少量配置甚至不需要编写配置,就可以快速搭建起基于 JPA 的数据访问层,大大减少了繁琐的配置工作。
    • 快速搭建项目
      • 相比传统 Spring,Spring Boot 可以更快速地搭建项目。传统 Spring 需要开发者手动配置大量的 XML 文件来定义 bean、数据源、事务等各种配置,而 Spring Boot 通过约定大于配置的原则,默认配置好了很多常用的功能,开发者可以更专注于业务逻辑的实现,能够在短时间内创建出一个可用的项目。
  9. MyBatis 的工作原理
    • SqlSessionFactory
      • MyBatis 通过 SqlSessionFactory 创建 SqlSession。SqlSessionFactory 是 MyBatis 的关键对象,它是线程安全的,可以被多个线程共享。它根据配置文件(如 mybatis-config.xml)和映射文件(如 UserMapper.xml)来创建 SqlSession。例如,在应用启动时,会创建一个 SqlSessionFactory 对象,后续的数据库操作都通过从这个工厂获取 SqlSession 来进行。
    • SqlSession
      • SqlSession 是 MyBatis 执行 SQL 语句的主要接口。它提供了一系列方法来执行 SQL 语句,如 selectOne、selectList、insert、update、delete 等。通过 SqlSession 执行 SQL 语句时,会根据映射文件中的配置信息,将 SQL 语句和 Java 对象进行映射。比如,在 UserMapper.xml 中定义了一个查询用户的 SQL 语句,通过 SqlSession 的 selectOne 方法执行该 SQL 语句,并将结果映射为 User 对象返回。
    • 映射文件
      • 映射文件中定义了 SQL 语句和 Java 对象之间的映射关系。它通过 、、、 等标签来定义 SQL 语句,并通过 resultMap 标签来定义结果集的映射规则。例如,在映射文件中可以定义如何将查询结果中的字段映射到 Java 对象的属性上,使得 MyBatis 能够准确地将数据库数据转换为 Java 对象。