互联网大厂Java求职者面试大揭秘:核心知识全考验
面试官:好了,面试开始。第一轮,先问你几个Java核心知识的问题。首先,Java中的多态是如何实现的?
王铁牛:多态就是一个对象可以有多种形态嘛。通过继承和重写方法就能实现,子类继承父类,然后重写父类的方法,调用的时候就会根据实际对象的类型来调用相应的方法。
面试官:回答得不错。那再问你,什么是Java的字节码文件?它有什么作用?
王铁牛:字节码文件就是Java源文件经过编译器编译后生成的文件呀。它是一种中间表示形式,能在不同的操作系统上运行,因为Java虚拟机可以执行字节码文件。
面试官:嗯,理解得挺到位。最后一个问题,简述一下Java中的异常处理机制。
王铁牛:异常处理机制就是用try、catch、finally块来处理程序运行时可能出现的异常。try块里放可能会出现异常的代码,catch块捕获异常并处理,finally块无论如何都会执行。
面试官:第一轮表现还行。接下来第二轮,考考你JUC和JVM相关的知识。首先,讲讲什么是JUC?
王铁牛:JUC就是Java.util.concurrent包呀,里面提供了很多并发编程的工具类,比如线程池、闭锁、信号量这些。
面试官:那JVM的内存结构分为哪几个部分?各部分的作用是什么?
王铁牛:JVM内存结构分为堆、栈、方法区、程序计数器、本地方法栈。堆用来存储对象实例;栈存放局部变量和方法调用信息;方法区存储类信息、常量等;程序计数器记录当前线程执行的字节码指令地址;本地方法栈用于执行本地方法。
面试官:回答得有点乱。再问你,JVM的垃圾回收算法有哪些?
王铁牛:有标记清除算法、标记整理算法、复制算法、分代收集算法。
面试官:第二轮就到这了。现在进入第三轮,关于多线程、线程池、HashMap等方面的问题。首先,创建线程有哪几种方式?
王铁牛:可以继承Thread类,重写run方法;也可以实现Runnable接口,实现run方法;还可以实现Callable接口,通过FutureTask包装来创建线程。
面试官:那线程池的核心参数有哪些?它们的作用是什么?
王铁牛:有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。corePoolSize是核心线程数,maximumPoolSize是最大线程数,keepAliveTime是线程存活时间,unit是时间单位,workQueue是任务队列,threadFactory是线程工厂,handler是拒绝策略。
面试官:最后一个问题,简述一下HashMap的底层实现原理。
王铁牛:HashMap底层是数组+链表+红黑树。通过哈希值计算出在数组中的位置,当有冲突时就会形成链表,链表长度超过一定阈值就会转换为红黑树。
面试官:好了,面试结束。回去等通知吧。
答案:
Java中的多态实现:
多态是面向对象编程的一个重要特性。它主要通过继承和重写来实现。当子类继承父类时,子类可以重写父类的方法。在调用这些方法时,会根据对象的实际类型来决定调用哪个类的方法,这就是多态的体现。例如,父类有一个方法void print(),子类重写了这个方法,当使用父类引用指向子类对象时,调用print()方法,实际执行的是子类重写后的方法。
Java的字节码文件: Java源文件经过编译器编译后生成字节码文件(.class文件)。字节码文件是一种中间表示形式,它不依赖于具体的操作系统。Java虚拟机(JVM)可以执行字节码文件。JVM通过加载、验证、解析字节码文件,然后在自己的运行时环境中执行其中的指令,从而实现Java程序的跨平台运行。
Java中的异常处理机制:
Java的异常处理机制主要通过try、catch、finally块来实现。try块中放置可能会抛出异常的代码。当try块中的代码抛出异常时,程序会跳转到对应的catch块中进行异常处理。catch块可以捕获特定类型的异常,并对其进行处理。如果有多个catch块,会按照顺序匹配异常类型,找到第一个匹配的catch块执行。finally块无论是否有异常都会执行,通常用于资源清理等操作。
JUC:
JUC即Java.util.concurrent包,它提供了一系列用于并发编程的工具类。这些工具类可以帮助开发人员更方便地实现多线程程序,提高程序的并发性能和可维护性。例如,ThreadPoolExecutor用于创建和管理线程池,CountDownLatch用于实现线程间的同步,Semaphore用于控制对共享资源的访问等。
JVM的内存结构:
- 堆(Heap):是JVM中最大的一块内存区域,用于存储对象实例。所有对象都在堆中分配内存。堆被分为新生代、老年代和永久代(Java 8后为元空间)。新生代又分为Eden区和两个Survivor区。
- 栈(Stack):每个线程都有自己独立的栈空间。栈中主要存放局部变量、方法调用信息等。当一个方法被调用时,会在栈中为该方法创建一个栈帧,用于存储该方法的局部变量和执行上下文。
- 方法区(Method Area):存储类信息、常量、静态变量等。它是各个线程共享的内存区域。在Java 8后,永久代被元空间取代,元空间使用本地内存,不再受限于JVM的内存大小。
- 程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址。它是线程私有的,每个线程都有自己独立的程序计数器。
- 本地方法栈(Native Method Stack):用于执行本地方法(用C或C++实现的方法)。与栈类似,每个线程都有自己的本地方法栈。
JVM的垃圾回收算法:
- 标记清除算法(Mark-Sweep):分为两个阶段,首先标记出所有需要回收的对象,然后清除这些对象所占用的内存空间。这种算法会产生内存碎片。
- 标记整理算法(Mark-Compact):先标记出所有需要回收的对象,然后将存活的对象向一端移动,最后清除边界以外的内存。解决了内存碎片问题,但移动对象的开销较大。
- 复制算法(Copying):将内存空间分为两块,每次只使用其中一块。当这一块内存使用完后,将存活的对象复制到另一块内存中,然后清除原来的那块内存。这种算法适用于对象存活率较低的情况。
- 分代收集算法(Generational Collection):根据对象的存活周期将内存分为新生代、老年代等不同区域,针对不同区域采用不同的垃圾回收算法。新生代对象存活率低,适合采用复制算法;老年代对象存活率高,适合采用标记清除或标记整理算法。
创建线程的方式:
- 继承Thread类:定义一个类继承自
Thread类,并重写run方法。然后通过创建该类的实例来启动线程,调用实例的start方法即可启动线程。例如:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行");
}
}
MyThread thread = new MyThread();
thread.start();
- 实现Runnable接口:定义一个类实现
Runnable接口,实现run方法。然后通过Thread类的构造函数将该实例作为参数传入,创建Thread对象并启动线程。例如:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
- 实现Callable接口:定义一个类实现
Callable接口,实现call方法。call方法可以有返回值。通过FutureTask类包装Callable对象,然后将FutureTask作为参数传入Thread类的构造函数来创建线程。例如:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程执行返回值";
}
}
FutureTask<String> task = new FutureTask<>(new MyCallable());
Thread thread = new Thread(task);
thread.start();
try {
String result = task.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
线程池的核心参数:
- corePoolSize(核心线程数):线程池中的核心线程数量。当提交的任务数小于
corePoolSize时,线程池会创建新的线程来执行任务。 - maximumPoolSize(最大线程数):线程池允许的最大线程数量。当提交的任务数大于
corePoolSize且任务队列已满时,会创建新的线程来执行任务,直到线程数量达到maximumPoolSize。 - keepAliveTime(线程存活时间):当线程数量大于
corePoolSize时,多余的线程在空闲一段时间后会被销毁。keepAliveTime就是指定线程的存活时间。 - unit(时间单位):
keepAliveTime的时间单位。 - workQueue(任务队列):用于存放提交的任务。当任务数小于
corePoolSize时,任务会直接交给核心线程执行;当任务数大于corePoolSize时,任务会被放入任务队列中。 - threadFactory(线程工厂):用于创建线程的工厂。可以通过自定义线程工厂来设置线程的名称、优先级等属性。
- handler(拒绝策略):当线程池中的线程数量达到
maximumPoolSize且任务队列已满时,会调用拒绝策略来处理新提交的任务。常见的拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(由调用线程执行任务)、DiscardPolicy(丢弃新提交的任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
HashMap的底层实现原理: HashMap底层是基于数组+链表+红黑树实现的。它通过哈希函数将键映射到数组的某个位置。当有多个键映射到同一个位置时,就会形成链表。链表的插入和查询时间复杂度为O(n),当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,红黑树的插入和查询时间复杂度为O(log n),从而提高性能。
HashMap在存储键值对时,首先计算键的哈希值,然后通过哈希值与数组长度取模得到在数组中的索引位置。如果该位置为空,则直接插入新的键值对;如果不为空,则检查键是否相等,如果相等则更新值,如果不相等则遍历链表或红黑树找到合适的位置插入新的键值对。在扩容时,会将原数组中的元素重新计算哈希值并插入到新的数组中。