互联网大厂Java面试:核心知识大考验
面试官:请简要介绍一下Java核心知识中面向对象的三大特性。
王铁牛:嗯……面向对象的三大特性是封装、继承和多态。封装就是把数据和操作数据的方法封装在一起;继承是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式。
面试官:回答得不错。那说说在多线程环境下,如何保证数据的一致性?
王铁牛:可以用锁呀,比如synchronized关键字,能保证同一时间只有一个线程访问共享资源。
面试官:很好。再问一个,JVM的内存结构主要分为哪几个部分?
王铁牛:嗯……有堆、栈、方法区……还有本地方法栈和程序计数器。
第一轮结束。
面试官:接下来问关于JUC的问题。请解释一下CountDownLatch的作用。
王铁牛:这个……好像是用来让一个或多个线程等待其他线程完成一系列操作后再执行。
面试官:那CyclicBarrier和CountDownLatch有什么区别?
王铁牛:呃……这个不太清楚,感觉差不多吧。
面试官:讲讲线程池的几种创建方式。
王铁牛:有ThreadPoolExecutor和Executors工具类创建的几种方式,像newFixedThreadPool之类的。
第二轮结束。
面试官:说说HashMap的底层实现原理。
王铁牛:它是基于数组和链表实现的,当链表长度超过一定阈值会转为红黑树。
面试官:那ArrayList的扩容机制是怎样的?
王铁牛:扩容就是翻倍增加容量。
面试官:Spring框架中IoC和AOP的概念是什么?
王铁牛:IoC是控制反转,AOP是面向切面编程。
第三轮结束。
面试官:今天的面试就到这里,回去等通知吧。
面试结束。面试官通过三轮提问,全面考察了求职者关于Java核心知识、JUC、JVM、多线程、线程池、HashMap、ArrayList、Spring等多方面的技术掌握情况。王铁牛在简单问题上回答尚可,但对于复杂问题的回答不够清晰准确。整体来看,面试结果还需综合评估,等待通知阶段求职者也可继续巩固知识,提升自身技术水平,以便更好地应对后续可能的考察或工作中的实际需求。
答案:
- 面向对象三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一的访问接口,提高数据的安全性和可维护性。例如一个类中有私有成员变量和公共的getter/setter方法来访问这些变量。
- 继承:子类继承父类的属性和方法,实现代码复用。子类可以通过继承获得父类的非私有成员,并且可以根据自身需求进行扩展。比如一个Animal类,Dog类继承自Animal类,Dog类就可以拥有Animal类的一些通用属性和行为,同时也可以有自己特有的属性和行为。
- 多态:同一个行为具有多个不同表现形式。包括编译时多态(方法重载)和运行时多态(方法重写)。例如一个父类引用可以指向其子类对象,调用同一个方法时会根据实际对象的类型执行不同的操作。
- 多线程环境下保证数据一致性:
- synchronized关键字:可以修饰方法或代码块。当一个线程访问被synchronized修饰的方法或代码块时,会首先获取对象的锁,其他线程必须等待该锁被释放后才能访问。例如在一个共享资源的操作方法上使用synchronized修饰,就能保证同一时间只有一个线程能操作这个共享资源,从而保证数据一致性。
- JVM内存结构:
- 堆:是Java对象存储的地方,几乎所有的对象实例都在这里分配内存。它是JVM内存中最大的一块区域,被所有线程共享。
- 栈:每个线程都有自己独立的栈空间,用于存储局部变量、方法调用等。栈中的数据存储和读取速度非常快。
- 方法区:存储类信息、常量、静态变量等数据。方法区在JDK 8及以后被称为元空间,它不再在堆中分配,而是使用本地内存。
- 本地方法栈:和栈类似,用于执行本地方法(用C或C++实现的方法)。
- 程序计数器:记录当前线程正在执行的字节码指令地址,是线程私有的。
- CountDownLatch作用:用于让一个或多个线程等待其他线程完成一系列操作后再执行。例如有多个线程需要完成一些初始化工作,然后主线程需要等待所有初始化工作完成后再继续执行,就可以使用CountDownLatch。初始化线程完成工作后调用countDown方法减少计数器的值,主线程通过await方法等待计数器变为0。
- CyclicBarrier和CountDownLatch区别:
- CountDownLatch:计数器只能使用一次,一旦计数为0,就不能再重置。它主要用于一个线程等待其他多个线程完成操作。
- CyclicBarrier:计数器可以循环使用,当所有线程都到达屏障点后,会重置计数器并继续循环。它更像是多个线程之间的一种同步点,所有线程都到达这个同步点后再一起继续执行。
- 线程池创建方式:
- ThreadPoolExecutor:可以通过自定义参数来创建线程池,如核心线程数、最大线程数、队列容量、线程存活时间、拒绝策略等。例如ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy());
- Executors工具类:
- newFixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量始终保持不变。适用于处理CPU密集型任务,能有效控制线程数量,避免资源耗尽。
- newCachedThreadPool:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。适用于执行很多短期异步任务的场景。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。适用于需要顺序执行任务的场景。
- HashMap底层实现原理:
- 数组:HashMap底层维护了一个Entry数组,用于存储键值对。数组的每个元素是一个链表的头节点。
- 链表:当不同的键计算出相同的哈希值时,会将这些键值对存储在同一个链表中。链表的存在是为了解决哈希冲突。
- 红黑树:当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查询效率。红黑树是一种自平衡二叉查找树,它的插入、删除和查找操作的时间复杂度都是O(log n),相比链表的O(n)有很大提升。
- ArrayList扩容机制:当ArrayList中的元素数量超过当前数组容量时,会进行扩容。扩容时会创建一个新的更大的数组,新数组的容量是原数组容量的1.5倍(如果原数组容量小于10),如果原数组容量大于等于10,则新数组容量为原数组容量加上原数组容量的一半。然后将原数组中的元素复制到新数组中,最后将新数组赋值给原数组引用。这样就完成了扩容操作。
- Spring框架中IoC和AOP概念:
- IoC(控制反转):传统的程序设计中,对象的创建和依赖关系管理由程序自身负责。而在IoC容器中,对象的创建和依赖注入由容器来完成。例如一个类需要依赖另一个类的对象,在IoC模式下,不需要在这个类中主动创建依赖对象,而是由IoC容器将依赖对象注入到该类中。这样可以降低对象之间的耦合度,提高程序的可维护性和可测试性。
- AOP(面向切面编程):将业务逻辑的各个部分进行分离,把一些通用的功能抽取出来,比如日志记录、事务管理等,这些功能被称为切面。通过AOP可以在不修改原有业务逻辑代码的情况下,动态地将切面功能织入到目标对象的方法执行过程中。例如在一个方法执行前后添加日志记录,或者在方法执行出现异常时进行事务回滚等操作。