聊聊java面向对象核心知识点
# 写在文章开头
java面向对象涉及很多设计思想和抽象理念,所以笔者针对过去的几篇稿件进行整理和梳理出这样一篇文章,希望对你有所帮助。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 详解java面向对象核心知识点
# 能不能给我说说面向对象和面向过程的区别
面向过程更多强调的是功能上的实现,将问题拆解成子步骤,然后针对要实现的步骤点进行逐个实现,并依次调用,不考虑复用等问题。
而面向对象则将一个问题拆解步骤后划分到成多个对象中实现,基于对象对步骤进行封装、组合来解决问题。

# 面向对象有哪些特性
- 封装
- 多态
- 继承
# 重载和重写的区别是什么
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
方法重载的规则:
- 方法名一致,参数列表中参数的顺序,类型,个数不同。
- 重载与方法的返回值无关,存在于父类和子类,同类中。
- 可以抛出不同的异常,可以有不同修饰符。
# 访问修饰符有那些?
- default
(即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。 - private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
- public : 对所有类可见。可以修饰类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类
(外部类)。
# 为什么说Java不支持多继承?原因是什么?
多继承会带来棱形继承问题,会带来各种不友好方法管理,例如B、C同时继承A并重写其方法foo,假设D继承了B、C,此时D若要调用foo方法就会出现矛盾,所以Java语言设计者在设计之初就抛弃这一概念,只能允许继承一个类和多个接口:

# 抽象类和接口的区别
- 接⼝的⽅法默认是
public,所有⽅法在接⼝中不能有实现(Java 8 开始接⼝⽅法可以有默认实现),⽽抽象类可以有⾮抽象的⽅法。 - 接⼝中除了
static、final变量,不能有其他变量,⽽抽象类中则不⼀定。 - ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接⼝⾃⼰本身可以通过
extends关键字扩展多个接⼝。 - 接⼝⽅法默认修饰符是
public,抽象⽅法可以有public、protected和default这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。 - 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。
# 两个类继承抽象类,都使用super.类的方法存在线程安全问题?
先说答案,不会,我们可以通过代码的方式印证,首先给出父类代码:
public abstract class Parent {
private HashMap<String, String> map =null;
//构造方法会初始化map
public Parent() {
map = new HashMap<>();
}
//往map里添加东西
public void add(String key, String value) {
map.put(key, value);
}
//获取map
public HashMap<String, String> getMap() {
return map;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后两个子类代码
public class A extends Parent{
public A(){
super.add("1","A");
}
}
2
3
4
5
public class B extends Parent{
public B(){
super.add("2","B");
}
}
2
3
4
5
6
使用多线程模式进行调试时可以发现每一个自类都会走到父类的构造方法初始化一个父类,这就意味着彼此的map都是独立的,所以不存在线程安全问题。
可以看到A类的map地址为704。

B类的map地址为711。

# 成员变量和局部变量有哪些区别?
- 从语法上分析:
成员变量:可被访问修饰符以及static修饰。局部变量:不可被访问修饰符以及static等修饰。 - 从存储位置:成员变量在堆内存,局部变量是在栈内存。
- 从生存时间来看:成员变量若为
static则随着类存在而存在,若无static则随着对象存在而存在。局部变量则随着方法调用结束。 - 从是否有默认值的角度来看:成员变量有默认值,局部变量没有默认值一说。
# 静态变量有什么作用
- 可被多个类共享,无论创建多少个类,使用
static修饰的变量永远只有这一个。 - 若
static再加一个final,这个变量就相当于一个常量,所以我们使用static关键字就必须考虑线程安全问题。
# this关键字的作用是什么?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。this 的用法在 Java 中大体可以分为 3 种:
- 普通的直接引用,this 相当于是指向当前对象本身
- 形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
2
3
4
- 引用本类的构造函数
# Java中的多态指的是什么?
在Java中多态强调的应该是一种运行期的特性,通过多态可以对单个接口的继承或者类方法的重写给出不同的实现,并通过组合关系解耦类型之间的关系,便于代码的拓展和维护,这其中我们日常使用Spring框架就是多态的典型运用:
@Service
public class OrderService {
//通过多态常见不同的bean,然后根据bean名称完成灵活注入,保证解耦
@Autowired
StateMachine<OrderStatusEnum, OrderEventEnum, Order> stateMachine;
//......
}
2
3
4
5
6
7
8
# 使用RPC接口进行返回基本类型时,你建议使用基本类型还是包装类?
一般建议是使用包装类,因为日常接口传值的某些数值类型传值是空的,为了避免歧义和业务上的判空,我们建议使用包装类,因为包装类默认没有初始化时得到的值是null,若是int默认则是0、float则是0.0:

# 什么是SPI,和API有啥区别
API规范应用开发人员,SPI服务于框架拓展人员。API更多强调的软件组件之间的规则和接口的约定,用户可以通过继承实现不同的业务逻辑,调用方只需调用即可。SPI则是一种拓展,用户必须通过导入SPI得到内置功能亦或者自行实现。
更多关于SPI的讲解可以参考笔者这篇文章:
# 什么是序列化与反序列化
序列化是Java内置的一种将对象进行持久化的技术手段,即将对象按照规定规范转成字节数组保存到物理文件中,需要时将字节数组通过反序列化还原成对象。
# Java序列化的原理是什么
通过继承Serializable接口,然后通过ObjectOutputStream的writeObject方法完成序列化并写入到文件中,对应源码如下,可以看到其内部writeObject0会判断并根据各个类类型按照相应的序列化规则将类数据写入:
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
//......
// 检查各个对象类型并通过指定方法完成数据写入
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
//......
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# serialVersionUID 有何用途? 如果没定义会有什么问题
反序列化时JVM不仅会比对类路径和功能代码是否一致,还会比对serialVersionUID 判断类型是否一致,只有字节流中的serialVersionUID 与本地相应实体类一致,JVM才会认为是类是安全且没有被篡改的,才进行反序列化,反之就会抛出InvalidClassException。
# 你知道fastjson的反序列化漏洞吗
fastjson通过Autotype让原本只能拿到抽象类型的数据可以拿到原始类型,这就可能存在被攻击者利用的风险,举个例子,假设我们有一个序列化数据,该数据某个字段存记录数据库url的信息,如果我们反序列化可以得到原始类型,黑客就很可能利用这一点修改这个数据库地址信息,进而导致反序列的setter操作这个字段时执行某些高危指令,严重影响服务器安全。
# 什么是深拷贝和浅拷贝
浅拷贝如果对象内部有对象仅仅复制对象的的内存地址,这意味着浅拷贝得到的对象被修改后可能会影响到原有对象的值。

而深拷贝进行对象克隆时会复制所有对象的值而不是地址:

# SimpleDateFormat是线程安全的吗?使用时应该注意什么
SimpleDateFormat不保证线程安全并发场景可能导致大量日期格式化结果错误,使用时注意上锁,或者使用spring自带的日期格式化工具DateTimeContextHolder。
# 为什么建议多用组合少用继承
- 通过组合可以避免类的特性对外暴露,而继承会对外暴露过多细节可能存在隐患
- 组合关系保证类与类之间松耦合,彼此独立。
- 组合关系可以通过面向接口组合的方式保证代码可拓展性。
# try中return A,catch中return B,finally中return C,最终返回值是什么
以finnally为主,所以返回的都是c
# final、finally、finalize有什么区别
final:修饰变量、方法或者类,使变量不可变、方法不可重写、类不可继承。finally:异常处理代码首尾的代码段,常用于资源清理和释放。finalize:Object的一个方法,通知垃圾回收器执行清理操作,具体何时清理不一定,所以不推荐使用。
# 为什么建议自定义一个无参构造函数
当声明有参构造时会覆盖无参,所以平时建议显示声明一下。
# Java中的static都能用来修饰什么
- 方法
- 成员变量
- 代码块
- 内部类
# 有了equals为啥需要hashCode方法
hashCode除了用于比对对象是否一致的参考条件以外,还是作为hashmap定位数组地址的算法依据。
# 反射与封装的思想理念是否矛盾?如何解决该问题
不矛盾,而反射适用于一些大型框架中一些非侵入性的增强保证用户更加方便的实现某些特性,这其中最经典的就是Spring的依赖注入的注解其本质就是框架底层通过反射扫描注解完成bean容器的加载。
而封装是异常内部实现细节,暴露必要接口实现功能上的复用和数据完整性及安全性的做法。
# 小结
自此,我们针对java面向对象模块的核心知识点进行相对体系的梳理和总结,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家,mini-redis的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。
同时也非常欢迎你star我的开源项目mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。
