【Java基础】为什么不支持多重继承?方法重载和方法重写之间区别、Exception 和 Error 区别?
Hi~!这里是奋斗的明志,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~
🌱🌱个人主页:奋斗的明志
🌱🌱所属专栏:Java基础面经
📚本系列文章为个人学习笔记,在这里撰写成文一为巩固知识,二为展示我的学习过程及理解。文笔、排版拙劣,望见谅。
Java 基础
- 一、为什么 Java 不支持多重继承?
- 1、解释
- 2、多继承不行,为什么接口多实现可以呢?
- 2.1 Java JDK8 之前
- 2.1 Java JDK8+
- 3、总结
- 二、Java 中 方法重载和方法重写之间的区别是什么?
- 1、区别
- 2、重载注意点
- 2、重写注意点
- 三、Java 中 Exception 和 Error 有什么区别?
- 1、Exception
- 2、Error
- 3、图示各层关系
- 4、异常处理注意点
- 4.1 尽量不要捕获类似 Exception 这样通用的异常,而应该捕获特定的异常
- 4.2 不要“吞”了异常
- 4.3 不要延迟处理异常
- 4.4 只在需要try-catch的地方try-catch,try-catch的范围能小则小
- 4.5 不要通过异常来控制程序流程
- 4.6 不要在finally代码块中处理返回值或者直接return
一、为什么 Java 不支持多重继承?
1、解释
主要是因为多继承会产生
菱形继承
(也叫钻石继承)问题,Jva之父就是吸取C++他们的教训,因此不支持多继承。
所谓的菱形继承很好理解,我们来看下这个图:
是不是看起来很像一个菱形,BC继承了A,然后D继承了BC,假设此时要调用D内定义在A的方法,因为B和C都有不同的实现
此时就会出现歧义,不知道应该调用哪个了。
// 假设允许多继承(伪代码)
class A { void foo() { System.out.println("A"); } }
class B { void foo() { System.out.println("B"); } }
class C extends A, B {} // 错误:C 中的 foo() 是继承 A 还是 B?
2、多继承不行,为什么接口多实现可以呢?
接口和抽象类不同,接口里的方法都是抽象的(在Java 8之前),没有具体的实现。
所以当一个类实现多个接口时,即使这些接口有相同的方法签名,类只需要提供一个具体的实现,这样就避免了方法冲突的问题。因为接口本身没有实现,不会有多个实现带来的歧义。即使两个接口有相同的方法,实现类只需要实现一次这个方法,这样就不会有问题。
Java 8之后接口可以有默认方法,这时候如果有多个接口有相同的默认方法,实现类必须重写这个方法,否则编译器会报错。
这时候就需要在实现类中明确指定使用哪个接口的默认方法,或者自己重新实现。这可能也是允许接口多实现的一个调整,但核心还是因为接口的默认方法需要显式处理冲突
,而类继承的结构性问题无法解决。
2.1 Java JDK8 之前
interface X { void foo(); }
interface Y { void foo(); }
class Z implements X, Y {
@Override
public void foo() {
System.out.println("Z 统一实现 X 和 Y 的 foo()");
}
}
2.1 Java JDK8+
- 这种强制重写机制将冲突的解决权交给开发者,确保逻辑明确。
interface X { default void foo() { System.out.println("X"); } }
interface Y { default void foo() { System.out.println("Y"); } }
class Z implements X, Y {
@Override
public void foo() {
X.super.foo(); // 显式选择调用 X 的默认实现
// 或 Y.super.foo(); 或自定义逻辑
}
}
3、总结
Java 通过禁止类的多继承避免了菱形问题和状态冲突,而接口的多实现通过以下机制保证安全:
-
接口方法由实现类统一实现(消除方法歧义)。
-
默认方法冲突需显式解决(开发者控制逻辑)。
-
接口不涉及状态管理(避免字段冲突)。
这种设计在保证语言简洁性的同时,提供了高度的灵活性,符合“单一继承,多重接口”
的面向对象设计原则。
二、Java 中 方法重载和方法重写之间的区别是什么?
1、区别
-
方法重载(Overloading)
:在同一个类中,允许有多个同名方法,只要它们的参数列表不同(参数个数、类型或顺序)。主要关注方法的签名变化,适用于在同一类中定义不同场景下的行为
。 -
方法重写(Overriding)
:子类在继承父类时,可以重写父类的某个方法(参数列表、方法名必须相同),从而为该方法提供新的实现。主要关注继承关系,用于子类改变父类的方法实现,实现运行时多态性
。 -
区别主要如下:
区别 | 重载 | 重写 |
---|---|---|
发生的场所 | 在同一个类中 | 在继承关系的子类和父类之间 |
参数列表 | 必须不同(参数的数量、类型或顺序不同) | 必须相同,不能改变参数列表 |
返回类型 | 可以不同 | 必须与父类方法的返回类型相同,或者是父类返回类型的子类(协变返回类型) |
访问修饰符 | 不受访问修饰符影响 | 子类方法的访问修饰符不能比父类更严格,通常是相同或更宽泛 |
静态和非静态方法 | 可以是静态方法或非静态方法 | 只能重写非静态方法,静态方法不能被重写(静态方法可以被隐藏) |
异常处理 | 方法的异常处理可以不同 | 子类的异常不能抛出比父类更多的异常(可以抛出更少的或相同类型的异常) |
2、重载注意点
重载中提到的方法同名 但参数列表不同(参数个数、类型或顺序),这里要注意和
返回值
没有关系,方法的签名仅是名字和参数列表
不包括返回值
。
重载通常用于提供同一操作的不同实现,例如构造函数的重载、不同类型输入的处理等。
- 代码实现:
public class A{
// 重载方法:参数数量不同
public void print(int a) {
System.out.println("Printing int: " + a);
}
// 重载方法:参数类型不同
public void print(String a) {
System.out.println("Printing String: " + a);
}
// 重载方法:参数类型和数量不同
public void print(int a, int b) {
System.out.println("Printing two ints: " + a + ", " + b);
}
}
2、重写注意点
在重写时,子类方法不能使用比父类更严格的
访问级别
。
例如,父类的方法是 protected
,子类不能将其修改为 private
,但可以改为public
。
且子类方法抛出的异常
必须与父类一致
,或者是其父类异常的子类
。
重写通常用于在子类中提供父类方法的具体实现,以实现运行时多态性。例如,子类对父类方法进行扩展或修改以适应特定需求。
- 代码实现:
class A{
public void display() {
System.out.println("Parent display");
}
}
class B extends A{
@Override
public void display() {
System.out.println("Child display");
}
}
public class Test{
public static void main(String[] args) {
A a = new B();
a.display(); // 输出 "Child display"
}
}
- 还有一个
@Override
注解,在重写方法时使用@Override
注解,编译器可以帮助检查是否正确实现了重写,以防误操作。
三、Java 中 Exception 和 Error 有什么区别?
Exception
和Error
都是Throwable
类的子类(在Java代码中,只有继承了Throwable
类的实例才可以被throw
或者被catch
)
它们表示在程序运行时发生的异常或者错误情况
总结:
Exception
表示可以被处理的程序异常,Error
表示系统级的不可恢复错误
1、Exception
Exception
: 是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
常见子类有:IOException
、SQLException
、NullPointerException
、IndexOutofBoundsException
等。
Exception
又分为 Checked Exception(编译期异常)
和 Unchecked Exception(运行时异常)
。
Checked Exception
: 在编译时必须显式处理(如使用try-catch块或通过throws声明抛出)。如IOException
Unchecked Exception
: 运行时异常,不需要显式捕获。常见的如 NullPointerException
、IllegalArgumentException
等,继承自 RuntimeException
2、Error
Error
表示严重的错误,通常是 JVM
层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutofMemoryError)
、栈溢出(Stackoverf1 OwError)
。Error 不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
3、图示各层关系
比如用户编写一个程序,如果文件没找到,会抛出
FileNotFoundException
,属于编译异常,必须处理。而如果程序递归太深导致栈溢出,会抛出StackOverflowError
,属于Error
,程序一般无法处理。
4、异常处理注意点
4.1 尽量不要捕获类似 Exception 这样通用的异常,而应该捕获特定的异常
软件工程是一门协作的艺术,在日常的开发中我们有义务使自己的代码能更直观、清晰地表达出我们想要表达的信息。
但是如果你什么异常都用了 Exception
,那别的开发同事就不能一眼得知这段代码实际想要捕获的异常,并且这样的代码也会捕获到可能你希望它抛出而不希望捕获的异常。
4.2 不要“吞”了异常
如果我们捕获了异常,不把异常抛出,或者没有写到日志里,那会出现什么情况?线上除了bug莫名其妙的没有任何的信息,你都不知道哪里出错以及出错的原因。
这可能会让一个简单的bug变得难以诊断,而且有些同学比较喜欢用 catch
之后 e.printStackTrace()
,在我们产品中通常不推荐用这种方法,一般情况下这样是没有问题的但是这个方法输出的是个标准错误流。
比如是在分布式系统中,发生异常但是找不到 stacktrace
。
所以最好是输入到日志里,我们产品可以自定义一定的格式,将详细的信息输入到日志系统中,适合清晰高效的排查错误。
4.3 不要延迟处理异常
比如有个方法,参数是个 name
,函数内部调了别的好几个方法,其实你的 name
传的是 null
值,但是你没有在进入这个方法或者这个方法一开始就处理这个情况,而是在你调了别的好几个方法然后爆出这个空指针。这样的话明明你的出错堆栈信息只需要抛出一点点信息就能定位到这个错误所在的地方,经过了好多方法之后可能就是一坨堆栈信息。
4.4 只在需要try-catch的地方try-catch,try-catch的范围能小则小
只要必要的代码段使用try-catch,不要不分青红皂白try住一坨代码,因为try-catch中的代码会影响JVM对代码的优化,例如重排序。
4.5 不要通过异常来控制程序流程
一些可以用 if/lse 的条件语句来判断 例如 null
值等,就不要用异常,异常肯定是比一些条件语句低效的,有CPU分支预测的优化等。
而且每实例化一个 Exception
都会对栈进行快照,相对而言这是一个比较重的操作,如果数量过多开销就不能被忽略了。
4.6 不要在finally代码块中处理返回值或者直接return
在 finally
中 return
或者处理返回值会让发生很诡异的事情,比如覆盖了try
中的return
,或者屏蔽的异常。