欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
美团优选后端一面:Java 八股文相关内容
美团优选后端一面中关于 Java 中的八股文主要考察了深拷贝、JVM、抽象类、MySQL 相关的内容,都是比较常见的八股文
回答抽象类和接口区别时可以结合设计模式的模板方法模式来进行介绍,讲解一下抽象类在这个设计模式中扮演什么职责,以及什么时候会使用接口、什么时候会使用抽象类!
深拷贝和浅拷贝?Java 里如何实现?
深拷贝和浅拷贝的区别为:
- 深拷贝: 完全复制一个新的对象出来,对象中的属性如果是引用类型,就再创建一个该类型的变量赋值给拷贝对象
- 浅拷贝: 在堆中创建一个新的对象,对象中的属性如果是引用类型,就直接将该属性的引用赋值给拷贝对象,那么此时原对象和拷贝对象共享这个引用类型的属性了
浅拷贝实现:
Java 中实现浅拷贝通过实现 Cloneable 接口来实现,从输出可以发现原 Student 对象和新 Student 对象中的引用属性 Address 的地址相同,说明是浅拷贝:
public class Student implements Cloneable {
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.setAddress((Address) student.getAddress().clone());
return student;
}
@Override
public String toString() {
return "Student{" +
"address=" + address +
'}';
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student();
student.setAddress(new Address());
Student clone = (Student) student.clone();
System.out.println(student);
System.out.println(clone);
System.out.println(student == clone);
/**
* 输出:
* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}
* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}
*/
}
}
public class Address {}
深拷贝实现:
实现深拷贝的话,只需要修改 Student 的 clone() 方法,在该方法中对 Address 对象也进行克隆就好了,Address 对象也需要实现 Cloneable 接口
public class Student implements Cloneable {
public Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.setAddress((Address) student.getAddress().clone());
return student;
}
@Override
public String toString() {
return "Student{" +
"address=" + address +
'}';
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student();
student.setAddress(new Address());
Student clone = (Student) student.clone();
System.out.println(student);
System.out.println(clone);
System.out.println(student == clone);
/**
* 输出:
* Student{address=com.example.springbootpro.DeepCopy.Address@6f539caf}
* Student{address=com.example.springbootpro.DeepCopy.Address@79fc0f2f}
* false
*/
}
}
public class Address implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
JVM 的类加载过程?
采用双亲委派机制进行加载,JVM 在加载类的 class 文件时,Java虚拟机采用的是 双亲委派机制 ,即把请求交给父类加载器去加载
工作原理:
- 如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器也存在其父类加载器,则继续向上委托
- 如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成类加载任务,则会由自家在其尝试自己去加载
优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改(例如,如果我们自定义一个java.lang.String类,然后我们去new String(),我们会发现创建的是jdk自带的String类,而不是我们自己创建的String类)
扩展:哪里打破了双亲委派机制?为什么要打破?
- 在实际应用中,可能存在 JDK 的基础类需要调用用户代码,例如:SPI 就打破双亲委派模式(打破双亲委派意味着上级委托下级加载器去加载类)
- 比如,数据库的驱动,Driver 接口定义在 JDK 中,但是其实现由各个数据库的服务上提供,由系统类加载器进行加载,此时就需要
启动类加载器
委托子类加载器去加载 Driver 接口的实现
- 比如,数据库的驱动,Driver 接口定义在 JDK 中,但是其实现由各个数据库的服务上提供,由系统类加载器进行加载,此时就需要
JVM运行时数据区?
运行时数据区主要包含:方法区、堆、栈,如下图:
这里只要讲清楚各个区域存储哪些对象就好了,是 JVM 中比较基础的内容,这里就不写太详细了
抽象类和接口的区别?
抽象类和接口之间最明显的区别:抽象类可以存在普通成员函数(即可以存在实现了的方法),而接口中的方法是不能实现的
接口设计的目的: 是对类的行为进行约束,约束了行为的有无,但不对如何实现进行限制
抽象类的设计目的: 是代码复用,将不同类的相同行为抽象出来,抽象类不可以实例化
接下来举一个例子更好理解,在常用的设计模式 模板方法模式 中,就会用到抽象类,在抽象类中可以对外暴露出一个方法的调用入口,这个方法中规定了需要执行哪些步骤,之后再将这些步骤的实现延迟到子类,那么不同的子类就可以有不同的实现了
如下,假设有一个制作饮料的过程,那么对外暴露一个方法 prepareRecipe()
供外界调用,该方法规定了制作饮料需要哪些步骤,将这些步骤定义为抽象方法,交由子类来实现,那么不同子类(制作柠檬水、制作咖啡)就可以对这些步骤进行定制化操作了
public abstract class Beverage {
// 模板方法,定义算法的框架
public void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew(); // 由子类提供具体实现
abstract void addCondiments(); // 由子类提供具体实现
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
}
class Tea extends Beverage {
void brew() {
System.out.println("Steeping the tea");
}
void addCondiments() {
System.out.println("Adding lemon");
}
}
class Coffee extends Beverage {
void brew() {
System.out.println("Dripping Coffee through filter");
}
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
个人认为抽象类的功能是包含了接口的功能的,也就是抽象类功能更多,那么在使用时如何来选择呢,到底使用接口还是使用抽象类?
可以从这个方面进行考虑,Java 是 单继承、多实现 的,因此继承抽象类的话,只能继承 1 个,而实现接口可以实现多个,因此在工程设计中,如果只是需要规定一个实现类中需要实现哪些方法,这个功能对于接口和抽象类都具有,那么毫无疑问使用接口,这样就可以将继承的机会给留出来,毕竟很宝贵
而如果需要使用模板方法模式,也就是要定义一些方法的执行流程,并且将这些流程延迟到子类实现,那只能使用抽象类,因为接口并不具备该功能
我们现在一般使用自增作为主键,为什么要这样做?
这个要从 MySQL 如何存储来讲了,MySQL 的数据在底层是通过 B+ 树索引进行存储的,根据 主键值 来建立聚集索引
在向表里插入数据时,如果 主键自增 ,那么插入的每一条新数据都在上一条数据的后边,当达到页的最大填充因子,下一条记录就会被写入新的页中
如果不自增的话, 选择 UUID 作为主键 ,那么每一条数据插入的位置是随机的,因此新插入的数据很大概率会落在已有数据的中间位置,存在以下问题:
1、写入的目标页可能已经刷到磁盘上并从内存中删除,或者还没有被加载到内存中,那么 InnoDB 在插入之前,需要先将目标页读取到内存中。这会导致大量随机 IO
2、写入数据是乱序的,所以 InnoDB 会频繁执行页分裂操作
3、由于频繁的页分裂,页会变得稀疏并且被不规则地填充,最终数据会有碎片
关于乐观锁和悲观锁,MySQL 是怎么实现的?
MySQL 中的乐观锁通过 版本号 来实现,如下面 SQL
UPDATE table_name SET column1 = new_value, version = version + 1 WHERE id = some_id AND version = old_version;
如果 version 未被修改,则允许更新;如果 version 已被修改,则拒绝更新操作
乐观锁适用于并发冲突较少的场景,因为它避免了在读取数据时加锁,从而减少了锁的开销
但是在高并发环境中,如果冲突频繁,乐观锁可能导致大量的重试操作,从而影响性能。在这种情况下,可能需要考虑使用悲观锁或其他并发控制策略
悲观锁的话,MySQL 中有行锁、表锁来保证共享数据资源的并发访问安全,行级锁又分为了记录锁、间隙锁、临键锁、插入意向锁,各个锁之间的职责不同
扩展:可以了解一下 MySQL 中的行级锁