《互联网大厂Java求职者面试大揭秘:核心知识深度考察》

49 阅读8分钟

互联网大厂Java求职者面试大揭秘:核心知识深度考察

面试官:好,接下来面试开始。第一轮,先问几个Java核心知识的问题。首先,Java中的多态是怎么实现的?

王铁牛:嗯,多态就是一个对象可以表现出多种形态。通过方法重写和父类引用指向子类对象就能实现。

面试官:回答得不错。那类的加载机制是什么样的?

王铁牛:类加载机制大概就是通过加载、验证、准备、解析、初始化这几个步骤来把类加载到内存中。

面试官:很好。最后一个问题,Java中的异常处理机制了解吧?简单讲讲。

王铁牛:就是try、catch、finally嘛,try块里放可能会出现异常的代码,catch捕获异常,finally不管有没有异常都会执行。

面试官:第一轮表现不错,接下来第二轮,关于JUC和多线程的。什么是CAS?

王铁牛:CAS就是比较并交换,通过比较内存值和预期值来决定是否更新。

面试官:那说说线程池的几种类型及其特点。

王铁牛:有FixedThreadPool固定大小线程池,CachedThreadPool可缓存线程池,SingleThreadExecutor单线程线程池,ScheduledThreadPool定时线程池。固定大小就是线程数固定,可缓存会缓存线程,单线程就一个线程执行,定时能定时执行任务。

面试官:线程安全的集合类有哪些?

王铁牛:像ConcurrentHashMap、CopyOnWriteArrayList这些。

面试官:第二轮回答得还可以。进入第三轮,关于JVM、HashMap、ArrayList的。JVM的内存结构分为哪几个部分?

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

面试官:HashMap的底层实现原理说一下。

王铁牛:就是数组加链表,后来还有红黑树,通过key的hash值找到对应的桶位置。

面试官:ArrayList是线程安全的吗?为什么?

王铁牛:不是,它不是线程安全的,因为它的方法没有加锁。

面试官:好了,面试就到这里,回去等通知吧。

答案解析

Java核心知识问题答案

  1. Java中的多态是怎么实现的?
    • 多态是通过方法重写和父类引用指向子类对象来实现的。当一个子类继承自父类并覆盖了父类的某个方法时,通过父类引用调用这个方法,实际执行的是子类重写后的方法,这就体现了多态。例如:
    class Animal {
        public void sound() {
            System.out.println("动物发出声音");
        }
    }
    class Dog extends Animal {
        @Override
        public void sound() {
            System.out.println("汪汪汪");
        }
    }
    public class Main {
        public static void main(String[] args) {
            Animal animal = new Dog();
            animal.sound();
        }
    }
    
    这里animalAnimal类型的引用,但指向的是Dog对象,调用sound方法时执行的是Dog类中的实现,这就是多态的体现。
  2. 类的加载机制是什么样的?
    • 类加载机制包括加载、验证、准备、解析、初始化五个步骤。
      • 加载:将类的字节码文件加载到内存中,形成一个类的二进制数据流。
      • 验证:检查加载的字节码文件是否符合Java虚拟机规范,确保其安全性。
      • 准备:为类的静态变量分配内存,并设置默认初始值。
      • 解析:将常量池中的符号引用替换为直接引用,使得程序可以直接访问这些对象。
      • 初始化:执行类的静态代码块和为静态变量赋值的操作,完成类的初始化。
  3. Java中的异常处理机制了解吧?简单讲讲。
    • Java的异常处理机制通过try、catch、finally语句块来实现。
      • try块:用于包含可能会抛出异常的代码。
      • catch块:用于捕获try块中抛出的异常,并进行相应的处理。可以有多个catch块来捕获不同类型的异常。
      • finally块:无论try块中的代码是否抛出异常,finally块中的代码都会执行。通常用于释放资源等操作。例如:
    try {
        int result = 10 / 0; // 这里会抛出异常
    } catch (ArithmeticException e) {
        System.out.println("捕获到算术异常: " + e.getMessage());
    } finally {
        System.out.println("finally块执行");
    }
    

JUC和多线程问题答案

  1. 什么是CAS?
    • CAS即比较并交换(Compare And Swap)。它是一种无锁的原子操作,通过比较内存值和预期值来决定是否更新。在Java中,java.util.concurrent.atomic包下的一些类(如AtomicInteger)就使用了CAS操作。CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置V的值等于预期原值A时,将内存位置V的值更新为新值B,否则不做任何操作。例如:
    import java.util.concurrent.atomic.AtomicInteger;
    public class CASExample {
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger(5);
            boolean success = atomicInteger.compareAndSet(5, 10);
            if (success) {
                System.out.println("更新成功,新值为: " + atomicInteger.get());
            } else {
                System.out.println("更新失败");
            }
        }
    }
    
    这里通过compareAndSet方法尝试将atomicInteger的值从5更新为10,如果当前值是5(预期原值),则更新成功并返回true,否则返回false。
  2. 说说线程池的几种类型及其特点。
    • FixedThreadPool:固定大小的线程池,创建时指定线程数量,线程数量固定不变。当提交的任务数小于线程数时,线程池中的线程会执行任务;当任务数大于线程数时,任务会被放入阻塞队列中排队等待。适用于需要控制线程数量,避免资源过度消耗的场景。
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 5; i++) {
        fixedThreadPool.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " 执行任务");
        });
    }
    fixedThreadPool.shutdown();
    
    • CachedThreadPool:可缓存线程池,线程数量不固定。如果有任务提交时,线程池中有空闲线程则复用空闲线程执行任务;如果没有空闲线程,则创建新线程执行任务。当线程空闲一段时间后(默认60秒),会被回收。适用于执行大量短期异步任务的场景。
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        cachedThreadPool.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " 执行任务");
        });
    }
    cachedThreadPool.shutdown();
    
    • SingleThreadExecutor:单线程线程池,只有一个线程。所有任务按照提交顺序依次执行,保证任务顺序执行。适用于需要顺序执行任务的场景。
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 5; i++) {
        singleThreadExecutor.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " 执行任务");
        });
    }
    singleThreadExecutor.shutdown();
    
    • ScheduledThreadPool:定时线程池,可以定时执行任务或周期性执行任务。通过ScheduledExecutorService接口实现,有schedule方法用于定时执行一次任务,scheduleAtFixedRate方法用于按固定速率周期性执行任务,scheduleWithFixedDelay方法用于按固定延迟周期性执行任务。例如:
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
    scheduledThreadPool.schedule(() -> {
        System.out.println("定时执行任务");
    }, 2, TimeUnit.SECONDS);
    scheduledThreadPool.scheduleAtFixedRate(() -> {
        System.out.println("按固定速率周期性执行任务");
    }, 0, 2, TimeUnit.SECONDS);
    scheduledThreadPool.shutdown();
    
  3. 线程安全的集合类有哪些?
    • ConcurrentHashMap:线程安全的哈希表。它采用分段锁机制,将数据分成多个段,每个段有自己的锁,在多线程环境下可以提高并发性能。例如:
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    map.put("key1", 1);
    Integer value = map.get("key1");
    
    • CopyOnWriteArrayList:线程安全的可变数组。当对其进行写操作时,会复制一份原数组,在新数组上进行修改,修改完成后再将原数组引用指向新数组。读操作直接在原数组上进行,所以读操作是线程安全的,并且性能较高。例如:
    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    list.add(1);
    Integer num = list.get(0);
    

JVM、HashMap、ArrayList问题答案

  1. JVM的内存结构分为哪几个部分?
    • JVM的内存结构分为堆、栈、方法区、程序计数器、本地方法栈。
      • :是Java对象存储的地方,所有对象实例都在堆中分配内存。堆是垃圾回收的主要区域。
      • :每个线程都有自己的栈,用于存储局部变量、方法调用等。栈中的数据存储是后进先出的。
      • 方法区:存储类信息、常量、静态变量等。在Java 8及以后,方法区被元空间(MetaSpace)取代,元空间使用本地内存,而不是像之前方法区那样使用堆内存。
      • 程序计数器:记录当前线程正在执行的字节码指令地址,是线程私有的。
      • 本地方法栈:用于执行本地方法(用C或C++实现的方法)。
  2. HashMap的底层实现原理说一下。
    • HashMap底层是基于数组加链表,后来引入了红黑树。
    • 首先,通过对key的hashCode进行扰动函数处理,得到一个哈希值。然后用这个哈希值与数组长度取模,得到在数组中的索引位置。如果该位置为空,则直接插入新节点;如果不为空,则遍历链表或红黑树(当链表长度大于等于8且数组长度大于等于64时,链表会转换为红黑树),找到相同key的节点进行更新操作,没有相同key则在链表或红黑树末尾插入新节点。例如:
    HashMap<String, Integer> map = new HashMap<>();
    map.put("key1", 1);
    
    当执行put操作时,先计算key1的哈希值,然后通过取模找到在数组中的位置,若该位置为空,则创建一个新的节点存储key1和1;若不为空,则遍历链表或红黑树找到key1进行值的更新(如果存在)或插入新节点。
  3. ArrayList是线程安全的吗?为什么?
    • ArrayList不是线程安全的。
    • 因为它的方法没有加锁,在多线程环境下,当多个线程同时对ArrayList进行读写操作时,可能会出现数据不一致的问题。例如,一个线程在读取ArrayList的同时,另一个线程可能正在修改它的结构(如添加或删除元素),这会导致读取到的数据不准确或抛出异常。如果需要在多线程环境下使用ArrayList,可以使用线程安全的CopyOnWriteArrayList