《互联网大厂Java求职者面试大揭秘:核心知识与实战问答》

52 阅读12分钟

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

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

面试官:嗯,回答得不错。那说说JUC中常用的并发工具类有哪些。

王铁牛:常用的有CountDownLatch、CyclicBarrier、Semaphore、Exchanger等。

面试官:很好。再问一个,讲讲JVM的内存模型。

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

面试官:第一轮面试结束,整体表现还可以。接下来第二轮,说说多线程中如何实现线程安全。

王铁牛:可以用synchronized关键字,还有Lock接口及其实现类。

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

王铁牛:核心参数有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。corePoolSize是核心线程数;maximumPoolSize是最大线程数;keepAliveTime是线程池线程空闲时的存活时间;unit是时间单位;workQueue是任务队列;threadFactory用于创建线程;handler是任务拒绝策略。

面试官:讲讲HashMap的底层实现原理。

王铁牛:HashMap底层是数组+链表+红黑树。当链表长度超过8且数组长度大于64时,链表会转换为红黑树。

面试官:第二轮面试也结束了,表现有起有伏。最后一轮,说说Spring中依赖注入的方式有哪些。

王铁牛:有构造器注入、setter方法注入、基于注解的注入。

面试官:那Spring Boot自动配置原理了解吗。

王铁牛:Spring Boot通过@EnableAutoConfiguration注解开启自动配置,它会根据类路径下的依赖来自动配置相关的Bean。

面试官:MyBatis的缓存机制是怎样的。

王铁牛:MyBatis有一级缓存和二级缓存。一级缓存是SqlSession级别的,同一个SqlSession内查询会优先从一级缓存中获取数据。二级缓存是namespace级别的,多个SqlSession可以共享二级缓存。

面试官:面试结束了,你回家等通知吧。

答案:

  1. 面向对象的三大特性
    • 封装
      • 封装是把数据和操作数据的方法绑定在一起,对外提供统一的接口。这样可以隐藏内部实现细节,提高代码的安全性和可维护性。例如,一个类中的私有成员变量,外部类不能直接访问和修改,只能通过该类提供的公共方法来操作,这样就保证了数据的完整性和安全性。
    • 继承
      • 继承允许一个类继承另一个类的属性和方法。被继承的类称为父类或超类,继承的类称为子类。子类可以复用父类的代码,实现代码的复用和扩展。比如,定义一个父类Animal,有eat方法,子类Dog继承Animal后,就可以直接使用eat方法,同时还可以添加自己特有的方法,如bark。
    • 多态
      • 多态是指同一个行为具有多个不同表现形式或形态。在Java中,多态主要体现在方法的重写和重载上。当子类重写父类的方法时,调用该方法会根据对象的实际类型来决定执行哪个子类的方法。例如,父类Animal有一个move方法,子类Dog和Bird都重写了move方法,当Animal a = new Dog();时,调用a.move()会执行Dog类的move方法。
  2. JUC中常用的并发工具类
    • CountDownLatch
      • 它是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。例如,有一个主线程需要等待多个子线程完成任务后再继续执行,就可以使用CountDownLatch。通过调用countDown方法来减少计数器的值,当计数器的值为0时,等待的线程会被唤醒。
    • CyclicBarrier
      • 它允许一组线程互相等待,直到到达某个公共屏障点。例如,多个线程需要同时完成一些初始化操作后再一起执行后续任务,就可以使用CyclicBarrier。当所有线程都调用await方法时,它们会被阻塞,直到所有线程都到达屏障点,然后所有线程会同时被唤醒继续执行。
    • Semaphore
      • 它是一个计数信号量,用于控制对共享资源的访问。例如,有一个资源池,最多允许n个线程同时访问,就可以使用Semaphore来控制。通过acquire方法获取许可,release方法释放许可。
    • Exchanger
      • 它允许在两个线程之间交换数据。例如,两个线程分别处理不同的数据,处理完成后需要交换数据,就可以使用Exchanger。通过exchange方法进行数据交换。
  3. JVM的内存模型
      • 堆是JVM中最大的一块内存区域,用于存储对象实例。它是线程共享的,所有对象的实例都在堆中分配。堆可以分为新生代、老年代和永久代(Java 8后为元空间)。新生代又分为Eden区和两个Survivor区,新创建的对象首先在Eden区分配,当Eden区满时,会触发Minor GC,将存活的对象复制到Survivor区,经过多次Minor GC后,仍然存活的对象会被晋升到老年代。
      • 栈是线程私有的内存区域,每个线程都有自己的栈。栈中主要存放局部变量、操作数栈、动态链接、方法出口等信息。局部变量是在方法内部定义的变量,操作数栈用于存放方法执行过程中的操作数,动态链接用于将方法调用与方法的实际实现关联起来,方法出口记录了方法执行完毕后的返回地址。
    • 方法区
      • 方法区用于存储类信息、常量、静态变量等数据。它也是线程共享的。在Java 8之前,方法区被称为永久代,Java 8后,永久代被元空间取代,元空间使用本地内存,而不是像永久代那样使用JVM的堆内存。
    • 程序计数器
      • 程序计数器是一块较小的内存区域,它记录了当前线程执行的字节码指令地址。每个线程都有自己独立的程序计数器,它是线程私有的。程序计数器的作用是保证线程切换后可以恢复到正确的执行位置。
    • 本地方法栈
      • 本地方法栈用于执行本地方法(即调用用C或C++实现的方法)。它也是线程私有的,和栈的结构类似,用于存放本地方法的局部变量、操作数栈等信息。
  4. 多线程中实现线程安全的方法
    • synchronized关键字
      • 它可以修饰方法或代码块。当修饰方法时,该方法在同一时刻只能被一个线程访问;当修饰代码块时,可以指定锁对象,只有获取到该锁对象的线程才能执行代码块中的内容。例如,在一个银行账户类中,存钱和取钱方法可以用synchronized修饰,保证在同一时刻只有一个线程能对账户进行操作,从而实现线程安全。
    • Lock接口及其实现类
      • Lock接口提供了比synchronized更灵活的锁控制。例如,ReentrantLock类实现了Lock接口。它可以实现公平锁,通过构造函数传入true来创建公平锁,公平锁会按照线程请求锁的顺序来分配锁,避免线程饥饿现象。同时,Lock接口还提供了tryLock方法,可以尝试获取锁,而不是像synchronized那样一直等待。
  5. 线程池的核心参数及作用
    • corePoolSize
      • 核心线程数。当提交的任务数小于corePoolSize时,线程池会创建新的线程来执行任务。如果提交的任务数大于corePoolSize,任务会被放入workQueue中。
    • maximumPoolSize
      • 最大线程数。当workQueue已满,且提交的任务数大于corePoolSize时,线程池会创建新的线程来执行任务,直到线程数达到maximumPoolSize。如果此时还有新任务提交,就会根据handler指定的策略来处理。
    • keepAliveTime
      • 线程池线程空闲时的存活时间。当线程池中的线程数大于corePoolSize时,这些多余的线程在空闲时间超过keepAliveTime后会被销毁,以减少资源消耗。
    • unit
      • keepAliveTime的时间单位。可以是TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。
    • workQueue
      • 任务队列。用于存放提交的任务,当提交的任务数大于corePoolSize时,任务会被放入该队列中。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
    • threadFactory
      • 用于创建线程的工厂。可以通过自定义threadFactory来设置线程的名称、优先级等属性。
    • handler
      • 任务拒绝策略。当线程池中的线程数达到maximumPoolSize且workQueue已满时,新提交的任务会由handler来处理。常见的任务拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用线程运行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
  6. HashMap的底层实现原理
    • HashMap底层是数组+链表+红黑树。
      • 它使用一个数组来存储键值对,数组的每个元素是一个链表的头节点。当插入一个键值对时,首先根据键的哈希值计算出在数组中的位置。
      • 如果该位置为空,就直接插入新的节点。
      • 如果该位置不为空,就会遍历链表,找到相同键的节点,然后更新其值;如果没有找到相同键的节点,就将新节点插入到链表尾部。
      • 当链表长度超过8且数组长度大于64时,链表会转换为红黑树,以提高查询效率。因为红黑树的查找时间复杂度为O(log n),而链表的查找时间复杂度为O(n)。
  7. Spring中依赖注入的方式
    • 构造器注入
      • 通过构造函数来注入依赖。例如,在一个类中定义一个构造函数,将需要的依赖对象作为参数传入构造函数。这样在创建该类的实例时,就会自动注入依赖对象。优点是注入的依赖对象在对象创建时就已经确定,不会出现空指针异常;缺点是如果依赖对象较多,构造函数参数列表会很长,代码不够简洁。
    • setter方法注入
      • 提供一个setter方法来设置依赖对象。例如,定义一个setter方法,将依赖对象传入该方法。优点是代码更加灵活,可以在对象创建后再设置依赖对象;缺点是需要注意在使用之前确保依赖对象已经被注入,否则可能会出现空指针异常。
    • 基于注解的注入
      • 使用注解来实现依赖注入,如@Autowired、@Resource等。@Autowired可以自动根据类型匹配并注入依赖对象,如果有多个匹配的类型,还可以结合@Qualifier注解来指定具体的bean。@Resource可以根据名称或类型来注入依赖对象。优点是代码简洁,配置方便;缺点是需要在类或方法上添加注解,侵入性较强。
  8. Spring Boot自动配置原理
    • Spring Boot通过@EnableAutoConfiguration注解开启自动配置。
      • 该注解会导入一个AutoConfigurationImportSelector类,它会扫描classpath下所有的META - INF/spring.factories文件。
      • 在spring.factories文件中定义了一系列的自动配置类,例如DataSourceAutoConfiguration、WebMvcAutoConfiguration等。
      • 这些自动配置类会根据类路径下的依赖来自动配置相关的Bean。比如,如果类路径下有数据库驱动依赖,DataSourceAutoConfiguration就会自动配置数据源相关的Bean,包括创建DataSource实例、配置事务管理器等。这样Spring Boot就可以根据项目的依赖自动完成很多常见的配置,大大简化了开发过程。
  9. MyBatis的缓存机制
    • 一级缓存
      • 一级缓存是SqlSession级别的缓存。同一个SqlSession内执行相同的查询语句时,会优先从一级缓存中获取数据。例如,在一个Service类中,使用同一个SqlSession执行多次相同的查询方法,第一次查询会从数据库中获取数据并放入一级缓存,后续的查询会直接从一级缓存中获取,而不会再次查询数据库,从而提高查询效率。当SqlSession关闭时,一级缓存会被清空。
    • 二级缓存
      • 二级缓存是namespace级别的缓存。多个SqlSession可以共享二级缓存。例如,不同的Service类中使用相同的Mapper接口进行查询,只要这些查询的namespace相同,就可以共享二级缓存。开启二级缓存需要在Mapper.xml文件中配置标签,并且在需要使用二级缓存的Mapper接口方法上添加@CacheNamespace注解。二级缓存会在数据发生变化时进行更新,从而保证缓存数据的一致性。