在一个明亮却略显严肃的面试房间里,一位求职者正襟危坐,对面的面试官表情冷峻,一场决定命运的互联网大厂Java面试即将拉开帷幕。
面试官:“第一轮,先从基础的Java核心知识开始。第一个问题,Java中重载和重写的区别是什么?” 王铁牛:“重载是方法名相同,参数列表不同,在同一个类里;重写是子类重写父类的方法,方法名、参数列表、返回值类型都得一样,而且访问修饰符不能比父类更严格。” 面试官:“回答得不错。第二个问题,说说ArrayList和HashMap的底层数据结构。” 王铁牛:“ArrayList底层是数组,能动态扩容;HashMap底层是数组加链表,JDK1.8后引入红黑树,当链表长度达到8且数组长度达到64时,链表转红黑树。” 面试官:“很好。第三个问题,多线程中线程的生命周期有哪些状态?” 王铁牛:“新建、就绪、运行、阻塞、死亡这几种状态。新建就是刚创建还没调用start方法;就绪是调用了start方法,等待CPU调度;运行就是获取到CPU资源在执行;阻塞是因为某些原因暂停执行;死亡就是线程执行完毕或者异常终止。” 面试官:“第一轮表现不错,进入第二轮。先问JVM相关,JVM的内存区域有哪些?” 王铁牛:“嗯……有堆、栈,还有……对了,方法区!” 面试官:“好,那堆内存又分为哪几个部分?” 王铁牛:“好像有新生代和老年代,新生代里还有……还有个什么来着,哎呀,想不起来了。” 面试官:“行,再问Spring相关,Spring的IOC是什么?” 王铁牛:“IOC就是控制反转,把对象的创建和管理交给Spring容器,这样代码耦合度就低了。” 面试官:“那Spring AOP呢?” 王铁牛:“AOP就是面向切面编程,能在不修改原有代码的基础上,增加一些功能,像日志记录啥的。不过具体实现原理我不太清楚。” 面试官:“进入第三轮。Dubbo的工作原理是什么?” 王铁牛:“Dubbo就是个分布式服务框架,能实现服务的注册和发现,好像是通过Zookeeper做注册中心,具体怎么通信的我不太确定。” 面试官:“RabbitMQ的消息确认机制了解吗?” 王铁牛:“嗯……就是消息发出去后,要确认有没有被正确接收,好像有个什么confirm机制,具体细节不太记得了。” 面试官:“xxl - job的核心功能是什么?” 王铁牛:“它是个分布式任务调度平台,能管理任务调度,不过具体怎么调度和监控的,我不太说得清楚。” 面试官:“最后问Redis,Redis的持久化方式有哪些?” 王铁牛:“有RDB和AOF,RDB是快照方式,AOF是记录写操作日志,不过它们具体区别和应用场景我不太明白。”
面试官:“今天的面试就到这里。你整体对一些基础概念掌握得还可以,但对于一些深入的原理和细节了解得不够透彻。回去等通知吧,我们会综合评估所有候选人后,再做决定。”
答案:
- Java中重载和重写的区别:
- 重载(Overloading):发生在同一个类中,方法名相同,但参数列表不同(参数个数、类型、顺序不同)。与返回值类型、访问修饰符无关。例如:
public class OverloadExample {
public void method(int a) {
// 方法体
}
public void method(String b) {
// 方法体
}
}
- **重写(Overriding)**:发生在父子类之间,子类重写父类的方法。方法名、参数列表、返回值类型(JDK1.5后,返回值类型可以是父类方法返回值类型的子类)必须相同,访问修饰符不能比父类更严格(可以相同或更宽松)。例如:
class Parent {
public void method() {
// 方法体
}
}
class Child extends Parent {
@Override
public void method() {
// 重写的方法体
}
}
- ArrayList和HashMap的底层数据结构:
- ArrayList:底层是数组结构。它允许动态扩容,当添加元素超过当前容量时,会重新分配一个更大的数组,并将原数组内容复制到新数组。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
- **HashMap**:JDK1.7及以前,底层是数组加链表。数组的每个元素是一个链表头节点。当发生哈希冲突(不同的键计算出相同的哈希值)时,会将新的键值对以链表的形式挂在对应数组位置的链表上。JDK1.8开始,当链表长度达到8且数组长度达到64时,链表会转换为红黑树,以提高查找效率。例如:
HashMap<String, Integer> map = new HashMap<>();
map.put("key", 1);
- 多线程中线程的生命周期状态:
- 新建(New):线程对象被创建,但还未调用start()方法,此时线程还没有开始运行。例如:
Thread thread = new Thread(() -> System.out.println("线程执行")); - 就绪(Runnable):调用了start()方法后,线程进入就绪状态,等待CPU调度。此时线程具备了运行的条件,但还没有分配到CPU资源。
- 运行(Running):线程获取到CPU资源,正在执行run()方法中的代码。
- 阻塞(Blocked):由于某些原因,线程暂时停止执行。比如线程调用了sleep()方法、等待I/O操作完成、等待获取锁等。例如:
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } - 死亡(Dead):线程执行完毕run()方法,或者因异常终止,此时线程生命周期结束。
- 新建(New):线程对象被创建,但还未调用start()方法,此时线程还没有开始运行。例如:
- JVM的内存区域:
- 堆(Heap):是JVM中最大的一块内存区域,用于存放对象实例。堆又分为新生代和老年代,新生代主要存放新创建的对象,老年代存放经过多次垃圾回收后依然存活的对象。新生代又细分为Eden区和两个Survivor区(一般称为S0和S1)。大部分对象在Eden区创建,当Eden区满时,会触发Minor GC,将存活对象移动到Survivor区,在Survivor区经过一定次数的GC后,依然存活的对象会被移动到老年代。
- 栈(Stack):每个线程都有自己的栈,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。栈的生命周期与线程相同,线程结束,栈也随之销毁。
- 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在JDK8及以后,方法区被元空间(Meta Space)取代,元空间使用本地内存。
- Spring的IOC(控制反转): IOC是一种设计思想,将对象的创建和管理从应用程序代码中转移到Spring容器中。通过依赖注入(Dependency Injection,DI)的方式,Spring容器在运行时将对象所依赖的其他对象自动注入到该对象中,降低了代码的耦合度。例如,有一个Service类依赖一个Dao类:
public class UserService {
private UserDao userDao;
// 通过构造函数注入
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// 业务方法
public void saveUser() {
userDao.save();
}
}
在Spring配置文件中:
<bean id="userDao" class="com.example.dao.UserDao"/>
<bean id="userService" class="com.example.service.UserService">
<constructor - arg ref="userDao"/>
</bean>
这样,Spring容器会创建UserDao和UserService对象,并将UserDao注入到UserService中。 6. Spring AOP(面向切面编程): AOP是一种编程范式,用于将横切关注点(如日志记录、事务管理、权限控制等)从业务逻辑中分离出来,以提高代码的可维护性和复用性。Spring AOP基于动态代理实现,有两种代理方式:JDK动态代理和CGLIB代理。JDK动态代理基于接口实现,CGLIB代理基于继承实现,用于代理没有实现接口的类。例如,为一个业务方法添加日志记录:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method " + joinPoint.getSignature().getName() + " is called.");
}
}
这里通过@Aspect注解定义切面,@Before注解定义在方法执行前执行的逻辑。 7. Dubbo的工作原理: - 服务注册与发现:Dubbo使用注册中心(如Zookeeper)来实现服务的注册和发现。服务提供者启动时,将自己提供的服务注册到注册中心;服务消费者启动时,从注册中心订阅自己需要的服务。 - 通信:Dubbo支持多种通信协议,如Dubbo协议、HTTP协议等。服务提供者和消费者之间通过这些协议进行远程方法调用(RPC)。例如,服务提供者定义一个接口和实现类:
public interface UserService {
String sayHello(String name);
}
public class UserServiceImpl implements UserService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
}
在Dubbo配置文件中配置服务提供者:
<dubbo:service interface="com.example.service.UserService" ref="userService"/>
<bean id="userService" class="com.example.service.UserServiceImpl"/>
服务消费者配置:
<dubbo:reference id="userService" interface="com.example.service.UserService"/>
- RabbitMQ的消息确认机制:
- 生产者确认(Confirm机制):生产者将消息发送到RabbitMQ后,RabbitMQ会给生产者发送一个确认消息,告知消息是否成功接收。生产者可以通过设置channel为confirm模式来开启该机制。例如:
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息已确认,deliveryTag: " + deliveryTag);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("消息未确认,deliveryTag: " + deliveryTag);
}
});
- **消费者确认(Ack机制)**:消费者接收并处理完消息后,需要向RabbitMQ发送一个确认消息,告知RabbitMQ可以将该消息从队列中删除。如果消费者没有发送确认消息,RabbitMQ会认为消息没有被成功处理,会重新将消息发送给其他消费者(如果有多个消费者)。消费者可以通过设置autoAck为false来手动确认消息:
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "myConsumerTag",
(consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF - 8");
System.out.println("Received message: " + message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
},
consumerTag -> {});
- xxl - job的核心功能:
- 任务调度:支持多种调度方式,如CRON表达式调度。用户可以在xxl - job的管理界面中创建任务,并设置调度规则。
- 任务执行:任务执行器(Executor)负责实际执行任务。任务执行器可以是一个独立的应用,通过注册到xxl - job的调度中心,接收调度中心分配的任务并执行。
- 任务监控:xxl - job提供任务执行日志查看、任务执行状态监控等功能,方便用户了解任务的执行情况,及时发现和解决问题。
- Redis的持久化方式:
- RDB(Redis Database):RDB是一种快照持久化方式,Redis会在指定的时间间隔内,将内存中的数据以快照的形式写入磁盘。优点是恢复速度快,适合大规模数据的恢复;缺点是可能会丢失最近一次快照后的修改数据,因为快照是定期生成的。例如,可以通过配置文件设置快照规则:
save 900 1
save 300 10
save 60 10000
表示900秒内如果有1个键被修改,或者300秒内有10个键被修改,或者60秒内有10000个键被修改,就进行一次快照。 - AOF(Append - Only - File):AOF是一种追加式的持久化方式,Redis会将每个写操作以日志的形式追加到文件末尾。优点是数据完整性高,因为只要日志没有损坏,就能恢复到最新的状态;缺点是日志文件可能会变得很大,需要定期进行重写(rewrite)操作。可以通过配置文件开启AOF:
appendonly yes
在重写时,Redis会读取当前内存中的数据,将其以最小的写命令集重新写入新的AOF文件,然后替换旧的AOF文件。