《互联网大厂面试:Java核心知识、框架与中间件大考验》

41 阅读12分钟

互联网大厂面试:Java核心知识、框架与中间件大考验

在互联网大厂的一间严肃的面试室内,一位神色冷峻的面试官正对面坐着略显紧张的王铁牛。面试开始,一场对Java相关知识的考验就此拉开帷幕。

第一轮提问 面试官:首先问你几个基础的Java核心知识问题。Java里的基本数据类型有哪些? 王铁牛:这个我知道,有byte、short、int、long、float、double、char、boolean。 面试官:回答得不错。那你说说String类为什么是不可变的? 王铁牛:因为String类是用final修饰的,一旦创建,它的值就不能被改变了。 面试官:很好。再问你,Object类里有哪些常用的方法? 王铁牛:有equals()、hashCode()、toString()、clone()、wait()、notify()、notifyAll() 。 面试官:非常棒,基础很扎实。接下来问你JUC相关的,JUC里的CountDownLatch是做什么用的? 王铁牛:CountDownLatch可以让一个或多个线程等待其他线程完成操作后再继续执行,通过一个计数器来实现。

第二轮提问 面试官:进入第二轮。说说JVM的内存结构是怎样的? 王铁牛:嗯……有堆、栈,还有……好像还有方法区啥的。 面试官:具体说说堆和栈的区别呢? 王铁牛:这个……堆好像是放对象的,栈嘛……就是放一些数据啥的,具体我也说不太清。 面试官:那JVM的垃圾回收机制,你能讲讲常见的垃圾回收算法吗? 王铁牛:好像有个啥标记清除,其他的我就不太记得了。 面试官:多线程方面,线程的生命周期有哪些状态? 王铁牛:我记得有新建、运行,后面就不太清楚了。 面试官:线程池的核心参数有哪些,能说一下吗? 王铁牛:好像有最大线程数,其他的我有点迷糊了。

第三轮提问 面试官:最后一轮。说说HashMap的底层实现原理。 王铁牛:这个……好像是数组加链表,具体怎么回事我不太能说清楚。 面试官:ArrayList和LinkedList的区别是什么? 王铁牛:好像ArrayList是数组实现的,LinkedList是链表实现的,其他就不太懂了。 面试官:Spring框架里的依赖注入是什么意思? 王铁牛:就是把一个对象给另一个对象用吧,具体细节我不太明白。 面试官:Spring Boot有什么优点? 王铁牛:好像能快速开发,其他就不清楚了。 面试官:MyBatis的一级缓存和二级缓存是怎么回事? 王铁牛:我就知道有缓存,具体不太懂。

面试官:今天的面试就到这里,你回家等通知吧。我们会综合评估后给你答复。

答案详解

  1. Java基本数据类型
    • byte:8位,有符号,范围是 -128 到 127。
    • short:16位,有符号,范围是 -32768 到 32767。
    • int:32位,有符号,范围是 -2147483648 到 2147483647。
    • long:64位,有符号,范围是 -9223372036854775808 到 9223372036854775807,定义时需要在数字后面加 “L”。
    • float:32位,单精度浮点数,定义时需要在数字后面加 “F”。
    • double:64位,双精度浮点数。
    • char:16位,无符号,用于表示单个字符,用单引号括起来。
    • boolean:只有两个值,true 和 false。
  2. String类不可变的原因
    • String类被final修饰,其内部的字符数组 value 也是被final修饰的。这意味着一旦String对象被创建,其内部的字符数组引用不能被改变,而且字符数组的内容也不能被修改。这样设计的好处有很多,比如可以提高字符串常量池的效率,保证线程安全,以及作为HashMap等容器的键时更可靠。
  3. Object类常用方法
    • equals():用于比较两个对象是否相等,默认比较的是对象的引用地址,子类可以重写该方法来实现内容的比较。
    • hashCode():返回对象的哈希码值,用于哈希表等数据结构,重写equals()方法时通常需要重写hashCode()方法,以保证相等的对象具有相同的哈希码。
    • toString():返回对象的字符串表示,默认返回类名和对象的哈希码的十六进制表示,通常子类会重写该方法以提供更有意义的信息。
    • clone():用于创建并返回对象的一个副本,需要实现Cloneable接口,否则会抛出CloneNotSupportedException异常。
    • wait():使当前线程进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法。
    • notify():唤醒在此对象监视器上等待的单个线程。
    • notifyAll():唤醒在此对象监视器上等待的所有线程。
  4. JUC里的CountDownLatch
    • CountDownLatch是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它通过一个计数器来实现,在创建CountDownLatch对象时需要指定一个初始计数值,当一个线程完成任务后,调用countDown()方法将计数值减1,当计数值变为0时,等待的线程将被唤醒继续执行。例如,在多线程下载任务中,可以使用CountDownLatch让主线程等待所有子线程下载完成后再进行后续操作。
  5. JVM的内存结构
    • 堆(Heap):是JVM中最大的一块内存区域,所有对象实例和数组都在这里分配内存。它是垃圾回收的主要区域,又可以分为新生代和老年代。
    • 栈(Stack):分为虚拟机栈和本地方法栈。虚拟机栈为Java方法服务,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈为本地方法服务。
    • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK 1.8 及以后,方法区被元空间(Metaspace)取代,元空间使用的是本地内存。
    • 程序计数器(Program Counter Register):可以看作是当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器。
  6. 堆和栈的区别
    • 存储内容:堆主要存储对象实例和数组,栈主要存储局部变量和方法调用的信息。
    • 线程共享性:堆是所有线程共享的,栈是每个线程独立拥有的。
    • 内存分配和回收方式:堆的内存分配和回收由垃圾回收器负责,栈的内存分配和回收是自动的,方法执行结束后栈帧会自动弹出。
  7. JVM常见的垃圾回收算法
    • 标记 - 清除算法(Mark - Sweep):首先标记出所有需要回收的对象,然后统一回收这些对象。缺点是会产生大量的内存碎片。
    • 标记 - 整理算法(Mark - Compact):先标记出需要回收的对象,然后将存活的对象向一端移动,最后清理掉边界以外的内存。解决了内存碎片的问题,但效率相对较低。
    • 复制算法(Copying):将可用内存分为大小相等的两块,每次只使用其中一块,当这块内存用完后,将存活的对象复制到另一块内存上,然后清除使用过的内存块。优点是不会产生内存碎片,但会浪费一半的内存空间。
    • 分代收集算法(Generational Collection):根据对象的存活周期将内存划分为不同的区域,如新生代和老年代,针对不同区域采用不同的垃圾回收算法。新生代对象存活时间短,采用复制算法;老年代对象存活时间长,采用标记 - 清除或标记 - 整理算法。
  8. 线程的生命周期状态
    • 新建(New):线程对象被创建,但还没有调用start()方法。
    • 就绪(Runnable):线程调用了start()方法,等待CPU分配时间片。
    • 运行(Running):线程获得CPU时间片,正在执行。
    • 阻塞(Blocked):线程因为某些原因放弃CPU使用权,暂时停止执行。阻塞状态又可以分为等待阻塞(调用wait()方法)、同步阻塞(获取锁失败)和其他阻塞(如调用sleep()、join()方法)。
    • 死亡(Terminated):线程执行完毕或者因为异常退出。
  9. 线程池的核心参数
    • corePoolSize:核心线程数,线程池在创建后默认不会立即创建线程,当有任务提交时,才会创建线程执行任务,直到线程数达到核心线程数。
    • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
    • keepAliveTime:线程空闲时间,当线程池中的线程数超过核心线程数时,多余的线程在空闲时间达到keepAliveTime后会被销毁。
    • unit:keepAliveTime的时间单位,如秒、毫秒等。
    • workQueue:任务队列,用于存储等待执行的任务。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
    • threadFactory:线程工厂,用于创建线程。
    • rejectedExecutionHandler:拒绝策略,当任务队列已满且线程数达到最大线程数时,新提交的任务会被拒绝,此时会调用拒绝策略来处理。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(由调用线程处理任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
  10. HashMap的底层实现原理
    • 在JDK 1.7及以前,HashMap是基于数组 + 链表实现的。数组是HashMap的主体,链表是为了解决哈希冲突而存在的。当一个键值对要插入到HashMap中时,首先通过哈希函数计算键的哈希值,然后根据哈希值找到对应的数组下标。如果该下标位置已经有元素,则通过链表将新元素插入到链表的头部(头插法)。
    • 在JDK 1.8及以后,HashMap采用数组 + 链表 + 红黑树实现。当链表长度达到8且数组长度达到64时,链表会转换为红黑树,以提高查找效率。当红黑树的节点数小于6时,红黑树会转换回链表。
  11. ArrayList和LinkedList的区别
    • 数据结构:ArrayList是基于动态数组实现的,LinkedList是基于双向链表实现的。
    • 随机访问性能:ArrayList支持随机访问,通过下标可以直接访问元素,时间复杂度为O(1);LinkedList不支持随机访问,需要从头或尾开始遍历链表,时间复杂度为O(n)。
    • 插入和删除性能:在列表中间插入或删除元素时,ArrayList需要移动大量元素,时间复杂度为O(n);LinkedList只需要修改指针,时间复杂度为O(1)。但如果是在列表末尾插入元素,ArrayList的性能也很高。
    • 内存占用:ArrayList需要连续的内存空间,可能会有一定的内存浪费;LinkedList每个节点需要额外的指针来指向前一个和后一个节点,会占用更多的内存空间。
  12. Spring框架里的依赖注入
    • 依赖注入(Dependency Injection,简称DI)是Spring框架的核心特性之一,它是一种设计模式,用于实现对象之间的解耦。在传统的编程中,对象之间的依赖关系是在对象内部创建和管理的,这样会导致对象之间的耦合度很高。而在依赖注入中,对象的依赖关系由外部容器来负责创建和注入。例如,一个Service类依赖于一个Dao类,通过依赖注入,Service类不需要自己创建Dao类的实例,而是由Spring容器将Dao类的实例注入到Service类中。依赖注入的方式主要有构造器注入、Setter方法注入和接口注入。
  13. Spring Boot的优点
    • 快速开发:Spring Boot提供了大量的starter依赖,通过简单的配置就可以快速集成各种功能,减少了开发的时间和工作量。
    • 自动配置:Spring Boot根据项目中引入的依赖自动进行配置,大大减少了配置文件的编写。例如,引入Spring Boot的Web starter依赖后,Spring Boot会自动配置嵌入式Tomcat服务器。
    • 独立运行:Spring Boot应用可以打包成一个可执行的JAR或WAR文件,直接通过java -jar命令运行,不需要额外的服务器环境。
    • 生产就绪:Spring Boot提供了丰富的监控和管理功能,如Actuator,可以方便地对应用进行监控和管理。
  14. MyBatis的一级缓存和二级缓存
    • 一级缓存:也称为本地缓存,是基于SqlSession的。在同一个SqlSession中,执行相同的查询语句时,MyBatis会先从一级缓存中查找,如果缓存中存在,则直接返回结果,不会再去数据库中查询。当SqlSession关闭或执行了增删改操作时,一级缓存会被清空。
    • 二级缓存:是基于Mapper文件的命名空间的,多个SqlSession可以共享二级缓存。二级缓存默认是关闭的,需要在MyBatis的配置文件中开启。当开启二级缓存后,不同的SqlSession执行相同命名空间下的查询语句时,可以从二级缓存中获取结果。当执行了增删改操作时,会清空该命名空间下的二级缓存。