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

45 阅读11分钟

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

面试官:好了,面试开始。首先第一轮,我问你几个关于Java核心知识的问题。在Java中,什么是多态?

王铁牛:多态就是一个对象具有多种形态。比如一个父类的引用可以指向它的子类对象,调用同一个方法会根据对象的实际类型执行不同的操作。

面试官:回答得不错。那接口和抽象类有什么区别?

王铁牛:接口里的方法都是抽象的,不能有方法体,而且一个类可以实现多个接口。抽象类里可以有抽象方法,也可以有非抽象方法,一个类只能继承一个抽象类。

面试官:嗯,理解得挺到位。最后一个问题,说说Java中的异常处理机制。

王铁牛:就是用try、catch、finally块来捕获和处理异常嘛。try块里放可能会出现异常的代码,catch块捕获特定类型的异常并处理,finally块无论如何都会执行。

面试官:第一轮表现还可以,接下来第二轮,我问几个关于JUC和多线程的问题。什么是线程池?有什么作用?

王铁牛:线程池就是预先创建一定数量的线程,当有任务来的时候,从线程池里取线程来执行任务,执行完再放回线程池。它能减少线程创建和销毁的开销,提高性能。

面试官:那如何创建一个线程池?

王铁牛:可以用ThreadPoolExecutor类,设置核心线程数、最大线程数、线程存活时间、任务队列等参数来创建。

面试官:在多线程环境下,如何保证数据的一致性?

王铁牛:可以用synchronized关键字加锁,或者用Lock接口及其实现类来加锁,还可以用并发容器比如ConcurrentHashMap来保证数据一致性。

面试官:第二轮还算过得去,现在进入第三轮,我问几个关于JVM和HashMap的问题。JVM的内存结构分为哪几个部分?

王铁牛:有堆、栈、方法区、程序计数器、本地方法栈。

面试官:那HashMap的底层实现原理是什么?

王铁牛:它是基于数组和链表实现的,当链表长度超过一定阈值时会转为红黑树。

面试官:最后一个问题,说说JVM的垃圾回收算法有哪些?

王铁牛:有标记清除算法、标记整理算法、复制算法、分代收集算法。

面试官:今天的面试就到这里,回去等通知吧。

问题答案

  1. 多态

    • 多态是指同一个行为具有多个不同表现形式或形态的能力。在Java中,多态主要体现在两个方面:方法重写(Override)和方法重载(Overload)。
    • 方法重写发生在子类中,子类重新实现了父类中已经定义的方法。当通过父类引用调用被重写的方法时,实际执行的是子类中重写后的方法。这体现了运行时多态,即根据对象的实际类型来决定调用哪个方法。
    • 方法重载是指在同一个类中定义多个同名的方法,但参数列表不同。编译器会根据调用方法时传递的参数类型和数量来决定调用哪个重载方法,这是编译时多态。
  2. 接口和抽象类的区别

    • 方法定义
      • 接口中所有的方法都是抽象的,默认是public abstract修饰的,不能有方法体。
      • 抽象类中可以有抽象方法(需要用abstract修饰,无方法体),也可以有非抽象方法(有具体实现)。
    • 实现方式
      • 一个类可以实现多个接口,通过implements关键字。
      • 一个类只能继承一个抽象类,通过extends关键字。
    • 作用
      • 接口主要用于实现功能的规范和扩展,比如定义一组相关的行为规范,让不同的类去实现这些规范以达到功能的统一和可替换性。
      • 抽象类更侧重于定义一些公共的属性和行为,为子类提供一个公共的基础,子类可以基于这个基础进行扩展和细化。
  3. Java中的异常处理机制

    • try块
      • 用于包含可能会抛出异常的代码。当程序执行到try块中的代码时,如果发生异常,异常会被抛出,程序流程会立即跳转到对应的catch块(如果有匹配的catch块)。
    • catch块
      • 用于捕获特定类型的异常。当try块中抛出的异常类型与catch块声明的异常类型匹配时,catch块中的代码会被执行,用于处理该异常。可以有多个catch块,用于捕获不同类型的异常。
    • finally块
      • 无论try块中的代码是否抛出异常,也无论catch块是否捕获到异常,finally块中的代码都会执行。通常用于执行一些清理资源的操作,比如关闭文件、释放数据库连接等。即使在try块或catch块中有return语句,finally块也会在return之前执行。
  4. 线程池

    • 线程池是一种预先创建一定数量线程的技术,这些线程组成一个池子,当有任务提交时,从线程池中获取线程来执行任务,任务执行完毕后线程不会被销毁,而是放回线程池供下次使用。
    • 线程池的作用主要有以下几点:
      • 减少线程创建和销毁的开销:创建和销毁线程是比较耗费资源的操作,线程池可以复用线程,避免频繁创建和销毁线程带来的性能损耗。
      • 提高响应速度:由于线程已经预先创建好,当有任务到来时可以立即执行,无需等待线程创建,从而提高系统的响应速度。
      • 便于管理:可以对线程池中的线程数量、任务队列等进行统一管理和控制,比如设置线程池的核心线程数、最大线程数、线程存活时间等参数,以适应不同的业务场景。
  5. 创建线程池

    • 在Java中,可以通过ThreadPoolExecutor类来创建线程池。ThreadPoolExecutor的构造函数如下:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        //...
    }
    
    • 参数说明
      • corePoolSize:核心线程数。当提交的任务数小于corePoolSize时,线程池会创建新的线程来执行任务。
      • maximumPoolSize:最大线程数。当提交的任务数大于corePoolSize且任务队列已满时,线程池会创建新的线程,直到线程数达到maximumPoolSize。
      • keepAliveTime:线程存活时间。当线程数大于corePoolSize时,多余的线程在空闲时会存活keepAliveTime这么长时间,之后会被销毁。
      • unit:keepAliveTime的时间单位。
      • workQueue:任务队列。用于存放提交的任务,当线程池的线程都在忙碌时,新提交的任务会放入这个队列中。
      • threadFactory:线程工厂。用于创建线程,可自定义线程的名称、优先级等属性。
      • handler:拒绝策略。当任务队列已满且线程数达到maximumPoolSize时,会调用该拒绝策略来处理新提交的任务。常见的拒绝策略有AbortPolicy(直接抛出异常)、CallerRunsPolicy(调用者线程执行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃任务队列中最旧的任务)。
  6. 多线程环境下保证数据一致性

    • synchronized关键字
      • 它是Java中的一个关键字,用于实现同步机制。可以修饰方法或代码块。
      • 当修饰方法时,该方法在同一时刻只能被一个线程访问,其他线程需要等待该方法执行完毕才能访问。
      • 当修饰代码块时,只有获取到该代码块对应的锁的线程才能执行代码块中的内容。
    • Lock接口及其实现类
      • Lock接口提供了比synchronized更灵活的锁控制。它定义了获取锁、释放锁、尝试获取锁等方法。
      • 常用的实现类有ReentrantLock,它支持可重入锁、公平锁和非公平锁等特性。通过调用lock()方法获取锁,unlock()方法释放锁,还可以通过tryLock()方法尝试获取锁,避免死锁。
    • 并发容器
      • 比如ConcurrentHashMap,它是线程安全的哈希表。在多线程环境下可以高效地进行读写操作。它采用分段锁机制,将整个哈希表分成多个段,每个段有自己的锁,这样在不同的段上可以同时进行读写操作,提高了并发性能。
  7. JVM的内存结构

    • 堆(Heap)
      • 是JVM中最大的一块内存区域,用于存储对象实例。所有的对象实例都在堆中分配内存。堆是垃圾回收的主要区域,垃圾回收器会定期对堆中的对象进行回收,释放不再使用的内存空间。
    • 栈(Stack)
      • 每个线程都有自己独立的栈空间。栈中主要存储局部变量、方法调用等信息。当一个方法被调用时,会在栈中为该方法创建一个栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等信息。方法执行完毕后,对应的栈帧会被销毁。
    • 方法区(Method Area)
      • 用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区在JDK 8及以后被称为元空间(MetaSpace),它不再像之前一样在堆中分配内存,而是使用本地内存。
    • 程序计数器(Program Counter Register)
      • 是一块较小的内存区域,它记录了当前线程正在执行的字节码指令的地址。每个线程都有自己独立的程序计数器,它是线程私有的。
    • 本地方法栈(Native Method Stack)
      • 与栈类似,用于执行本地方法(用C、C++等编写的方法)。当Java程序调用本地方法时,会在本地方法栈中为该方法分配栈帧,执行完毕后销毁栈帧。
  8. HashMap的底层实现原理

    • 数组
      • HashMap底层是基于数组实现的。它有一个Node类型的数组table,用于存储键值对。数组的初始容量是16(可以通过构造函数指定初始容量)。
    • 链表
      • 当向HashMap中插入键值对时,如果计算得到的哈希值对应的数组位置为空,则直接插入新的Node节点。如果该位置不为空,则会将新节点插入到链表的头部(JDK 1.8之前)或尾部(JDK 1.8之后)。链表的作用是解决哈希冲突,即不同的键值对计算得到相同的哈希值时,会通过链表来存储这些键值对。
    • 红黑树
      • 当链表长度超过一定阈值(默认是8)时,链表会转为红黑树。红黑树是一种自平衡二叉查找树,它可以提高在链表中查找、插入和删除操作的效率。当红黑树节点数小于等于6时,又会退化为链表。
  9. JVM的垃圾回收算法

    • 标记清除算法(Mark-Sweep Algorithm)
      • 该算法分为两个阶段。首先是标记阶段,从根节点开始遍历,标记出所有存活的对象。然后是清除阶段,遍历整个堆空间,清除所有未被标记的对象。这种算法的缺点是会产生大量不连续的内存碎片。
    • 标记整理算法(Mark-Compact Algorithm)
      • 也是先进行标记阶段,标记出存活的对象。然后在清除阶段,将所有存活的对象向一端移动,然后直接清理掉边界以外的内存。这样可以避免内存碎片问题,但移动对象的操作开销较大。
    • 复制算法(Copying Algorithm)
      • 将内存空间分为两块相等的区域,每次只使用其中一块区域。当这一块区域中的对象存活时,将它们复制到另一块区域,然后清除原来的区域。这种算法适用于对象存活率较低的场景,比如新生代。它的优点是不会产生内存碎片,缺点是需要两倍的内存空间。
    • 分代收集算法(Generational Collection Algorithm)
      • 基于对象的年龄不同,将堆内存分为新生代、老年代和永久代(JDK 8及以后为元空间)。新生代又分为Eden区和两个Survivor区。不同代采用不同的垃圾回收算法。新生代对象存活率低,适合采用复制算法;老年代对象存活率高,适合采用标记清除或标记整理算法。这种算法可以提高垃圾回收的效率。