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

17 阅读7分钟

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

王铁牛:嗯……面向对象的三大特性是封装、继承和多态。封装就是把数据和操作数据的方法封装在一起;继承就是子类继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型而表现出不同的行为。

面试官:不错,回答得很准确。那JUC里的CountDownLatch你了解吗?它在什么场景下会被使用?

王铁牛:CountDownLatch啊,我知道!它可以用来让一个或多个线程等待其他线程完成一组操作后再继续执行。比如在一个多线程任务中,主线程需要等待所有子线程都完成任务后再进行汇总操作,就可以用CountDownLatch。

面试官:很好。接下来问几个关于JVM的问题。类加载器有哪些类型,它们的加载顺序是怎样的?

王铁牛:类加载器有启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器。加载顺序是先启动类加载器,然后扩展类加载器,接着应用程序类加载器,最后是自定义类加载器。

面试官:第一轮面试结束,整体表现不错。下面进入第二轮。首先,多线程中的线程安全问题你是怎么理解的?在ArrayList中如何保证线程安全?

王铁牛:线程安全就是多个线程同时访问一个资源时不会出现数据不一致的情况。在ArrayList中,可以用Collections.synchronizedList()方法把它包装成一个线程安全的List。

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

王铁牛:核心参数有corePoolSize、maximumPoolSize、keepAliveTime、unit和workQueue。corePoolSize是核心线程数,maximumPoolSize是最大线程数,keepAliveTime是线程池中的线程在空闲时的存活时间,unit是存活时间的单位,workQueue是任务队列。

面试官:第二轮表现也还行。最后一轮,说说HashMap的底层数据结构和扩容机制。

王铁牛:HashMap底层是数组和链表(JDK 1.8后还有红黑树)。扩容机制就是当HashMap中的元素个数超过阈值(容量*负载因子)时,就会进行扩容,扩容后的容量是原来的2倍。

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

答案

  • 面向对象的三大特性
    • 封装:将数据和操作数据的方法绑定在一起,对外提供统一的访问接口。这样可以隐藏内部实现细节,提高数据的安全性和程序的可维护性。例如,在一个类中,将一些属性设置为私有,通过公有的getter和setter方法来访问和修改这些属性。
    • 继承:子类继承父类的属性和方法,实现代码的复用。子类可以扩展父类的功能,也可以重写父类的方法。比如,创建一个父类Animal,子类Dog和Cat继承自Animal,它们可以继承Animal的一些通用属性和方法,同时也可以有自己特有的属性和行为。
    • 多态:同一个方法可以根据对象的不同类型而表现出不同的行为。这使得程序具有更好的扩展性和灵活性。例如,定义一个父类Shape,子类Circle和Rectangle继承自Shape,它们都有draw()方法,但具体的绘制行为不同。通过多态,可以使用一个Shape类型的变量来引用Circle或Rectangle对象,调用draw()方法时会执行相应子类的绘制逻辑。
  • JUC里的CountDownLatch
    • 它是一个同步辅助类,允许一个或多个线程等待其他一组线程完成操作。
    • 使用场景:比如在一个多线程任务中,主线程需要等待所有子线程都完成某个特定任务后,再继续执行后续的汇总或收尾工作。可以创建一个CountDownLatch对象,构造函数传入子线程的数量。每个子线程完成任务后调用CountDownLatch的countDown()方法,主线程调用await()方法等待,直到计数器的值变为0,主线程才会继续执行。
  • 类加载器的类型及加载顺序
    • 启动类加载器:负责加载Java核心库,如rt.jar等,是Java虚拟机自带的加载器,由C++实现。
    • 扩展类加载器:负责加载Java扩展库,如jre/lib/ext目录下的jar包。
    • 应用程序类加载器:负责加载应用程序的类路径下的类文件,也被称为系统类加载器。
    • 自定义类加载器:用户可以根据自己的需求自定义类加载器,用于加载特定路径或格式的类文件。
    • 加载顺序:先启动类加载器,然后扩展类加载器,接着应用程序类加载器,最后是自定义类加载器。当一个类需要被加载时,会从最顶层的启动类加载器开始尝试加载,如果加载不到,再依次向下层的类加载器查找。
  • 多线程中的线程安全问题及ArrayList保证线程安全的方法
    • 线程安全问题:多个线程同时访问和修改共享资源时,可能会导致数据不一致、竞争条件等问题。例如,多个线程同时对一个变量进行读写操作,可能会出现读到脏数据或者数据丢失的情况。
    • ArrayList保证线程安全的方法:可以使用Collections.synchronizedList()方法将ArrayList包装成一个线程安全的List。例如:List synchronizedList = Collections.synchronizedList(new ArrayList<>()); 这样在多线程环境下,对这个synchronizedList的操作会被同步,避免线程安全问题。
  • 线程池的核心参数及作用
    • corePoolSize:核心线程数。当提交的任务数小于corePoolSize时,线程池会创建新的线程来执行任务。
    • maximumPoolSize:最大线程数。当提交的任务数大于corePoolSize且任务队列已满时,会创建新的线程来执行任务,但线程数不会超过maximumPoolSize。
    • keepAliveTime:线程池中的线程在空闲时的存活时间。当线程空闲时间超过keepAliveTime时,非核心线程会被销毁。
    • unit:存活时间的单位,如TimeUnit.SECONDS表示秒。
    • workQueue:任务队列。用于存放提交到线程池但尚未被执行的任务。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
  • HashMap的底层数据结构和扩容机制
    • 底层数据结构:在JDK 1.8之前,HashMap底层是数组 + 链表。数组中的每个元素是一个链表节点,用于存储键值对。当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查找效率。在JDK 1.8及之后,HashMap底层是数组 + 链表 + 红黑树。
    • 扩容机制:当HashMap中的元素个数超过阈值(容量 * 负载因子,默认负载因子为0.75)时,就会进行扩容。扩容后的容量是原来的2倍。扩容时,会重新计算每个键值对在新数组中的位置,因为新数组大小变化了,哈希值对应的索引位置也会改变。具体来说,会遍历原数组,对每个元素重新计算哈希值,并根据新的容量计算在新数组中的插入位置,然后将元素插入到新数组中。如果原数组中的某个链表节点在重新计算位置后,其哈希值与新位置上的元素哈希值相同,就会在链表或红黑树中继续添加节点。