
继承(inheritance)是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承最大的作用就是:实现代码复用,还有就是来实现多态!
extends在 Java 中如果要表示类之间的继承关系,需要借助 extends 关键字,具体如下:
修饰符 class 子类 extends 父类 {
// ...
}☘️注意事项:
super由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作❓❓❓
直接访问是无法做到的, 所以 Java 提供了 super 关键字,该关键字主要作用:在子类方法中访问父类的成员。也就是说,如果在子类方法中想要明确访问父类中成员时,借助 super 关键字即可!
☘️注意事项:
this 一样,不能在「静态方法」中使用。super 只是一个关键字,是为了提高代码的可读性的,并不会生成父类的引用,所以实际上创建子类是不会创建出父类引用的!super 只能指代当前类的父类,而不能指代当前类的爷爷类等!class Base{
public int a = 10;
}
class Derived extends Base {
int a = 20;
public void func(){
System.out.println("a is " + a); // 优先访问子类自己的a
System.out.println("a is " + super.a); // 通过super直接访问父类的a
}
}
public class test1 {
public static void main(String[] args) {
Derived d = new Derived();
d.func();
}
}
// 执行结果
a is 20
a is 10牢记一个原则:先有父再有子!即子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。
☘️注意事项:
super() 对父类进行构造的话,super() 语句必须放在子类构造方法的第一行!super() 和 this() 不能放在一个方法中使用,因为两者都必须放在构造方法的第一行,这就导致了冲突,所以不能一起使用!如下所示:
super() 只能在子类的构造方法中调用,并且只能出现一次!下面是代码示例,如下所示:
class Base{
public Base(){
System.out.println("This is Base!");
}
}
class Derived extends Base{
public Derived(){
super(); // 无参super()函数就算我们不写也会自动调用!!!
System.out.println("This is Derived!");
}
}
public class test1 {
public static void main(String[] args) {
Derived d = new Derived();
}
}
// 执行结果
This is Base!
This is Derived!如果父类中的构造函数需要我们传入参数的话,则我们必须要显式调用一下 super() 函数进行传参,不然会报错,如下图所示:

因为此时 Base 类有了有参构造方法,那么编译器就不会默认为 Base 类创建一个无参的构造方法,此时子类就必须要显式传入需要的参数!
super 和 this 的关系super 和 this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢❓❓❓
☘️相同点:
非静态方法」中使用,用来访问非静态成员方法和字段。super() 和 this() 都必须是构造方法中的第一条语句,所以它们不能同时存在。☘️不同点:
this 是当前对象的引用;而 super 是子类对象中从父类继承下来部分成员的引用!this 用来访问本类的方法和属性,super 用来访问从父类继承下来的方法和属性。super(...) 的调用,就算用户没有写,编译器也会增加;但是 this(...) 用户不写则没有。结合前面我们所学的类的初始化以及代码块的知识,再串上现在的继承,尝试着理解下面这道题:
class Base{
static {
System.out.println("This is Base 静态代码块");
}
{
System.out.println("This is Base 实例代码块");
}
public Base(){
System.out.println("This is Base()!");
}
}
class Derived extends Base{
static {
System.out.println("This is Derived 静态代码块");
}
{
System.out.println("This is Derived 实例代码块");
}
public Derived(){
System.out.println("This is Derived()!");
}
}
public class test1 {
public static void main(String[] args) {
Derived d1 = new Derived();
System.out.println("----------------");
Derived d2 = new Derived();
}
}
// 执行结果:
This is Base 静态代码块
This is Derived 静态代码块
This is Base 实例代码块
This is Base()!
This is Derived 实例代码块
This is Derived()!
----------------
This is Base 实例代码块
This is Base()!
This is Derived 实例代码块
This is Derived()!通过分析执行结果,得出以下结论:
先执行父类和子类的静态,再执行父类的实例和构造方法,最后执行子类的实例和构造方法。
protected 关键字在类和对象中,为了实现封装特性,Java 中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。

学了继承之后我们就知道了,protected 关键字是为了给不同包中的子类提供访问父类的成员的权限!
因为如果没有 protected 关键字,只有 default 和 public 的话,不同包中的子类要想访问父类的成员的话,此时就没办法控制好访问粒度,所以才引入了 protected 关键字!
注意:父类中 private 成员变量虽然在子类中不能直接访问,但也继承到子类中了!
在 Java 中只支持以下几种继承方式:

注意:Java 中不支持多继承。
final 关键字C 中的 const 关键字)此时就有一个易错点,如下代码所示:
public static void main(String[] args) {
final int[] arr = new int[9];
arr = new int[10]; // ❌
arr[0] = 20; // ✅
}因为 final 修饰的是 arr 变量,也就是 arr 中保存的值,也就是 arr 的引用地址不能改变,但如果修改的是 arr[0],即改的是堆区中的元素,不会被 final 影响!
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的实例作为另外一个类的字段。
is-a 的关系。比如:狗是动物,猫是动物has-a 的关系。比如:汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:尽量用组合,因为组合更安全,更简单,更灵活,更高效!
// 轮胎类
class Tire{
...
}
// 发动机类
class Engine{
...
}
// 车载系统类
class VehicleSystem{
...
}
// 使用组合方式
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
...
}
// 使用继承方式:奔驰是汽车
class Benz extend Car{
...
}它们的优缺点如下所示:
组合关系 | 继承关系 |
|---|---|
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立 | 缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性 |
优点:具有较好的可扩展性 | 缺点:支持扩展,但是往往以增加系统结构的复杂度为代价 |
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象 | 缺点:不支持动态继承。在运行时,子类无法选择不同的父类 |
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 | 缺点:子类不能改变父类的接口 |
缺点:整体类不能自动获得和局部类同样的接口 | 优点:子类能自动继承父类的接口 |
缺点:创建整体类的对象时,需要创建所有局部类的对象 | 优点:创建子类的对象时,无须创建父类的对象 |
通俗来说,就是多种形态,具体点就是去在完成某个动作时,不同的对象完成会产生出不同的状态!
在 java 中要实现多态,必须要满足如下几个条件,缺一不可:
class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃饭");
}
}
class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
}
// 下面的注解表示当前方法进行了重写,编译器会帮忙检查是否重写了
@Override
public void eat(){
System.out.println(name+"吃鱼~~~");
}
}
class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
// 下面的注解表示当前方法进行了重写,编译器会帮忙检查是否重写了
@Override
public void eat(){
System.out.println(name+"吃骨头~~~");
}
}
///////////////////////////////分割线//////////////////////////////////////////////
public class test1 {
// 编译器在编译代码时,并不知道要调用Dog还是Cat中eat()的方法
// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用哪个方法
// 注意:此处的形参类型必须时父类类型才可以
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("元宝", 2);
Dog dog = new Dog("小七", 1);
eat(cat);
eat(dog);
}
}
// 执行结果:
元宝吃鱼~~~
小七吃骨头~~~在上述代码中,假设分割线上方的代码是类的实现者编写的,分割线下方的代码是类的调用者编写的。
当类的调用者在编写 eat() 这个方法的时候,参数类型为 Animal(父类),此时在该方法内部并不知道、也不关注当前的 a 引用指向的是哪个类型(哪个子类)的实例,此时 a 这个引用调用 eat() 方法可能会有多种不同的表现(和 a 引用的实例相关),这种行为就称为多态。
overridepublic 修饰,则子类中重写该方法就不能声明为 protected。final、static、private 修饰的方法都不能被重写。@Override 注解来显式指定。有了这个注解能帮我们进行一些合法性校验,例如不小心将方法名字拼写错了(比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写。重写和重载的区别:
区别点 | 重写(override) | 重载(overload) |
|---|---|---|
参数列表 | 不能修改 | 必须修改 |
返回类型 | 不能修改(除非构成父子关系) | 可以修改 |
访问限定符 | 权限不能比父类该方法小 | 可以修改 |
静态绑定:也称为前期绑定,即在编译时根据用户所传递实参类型就确定了具体调用那个方法。典型代表就是方法重载。
动态绑定:也称为后期绑定,即在编译时不能确定方法的行为,需要等到程序运行时才能够确定具体调用那个类的方法。典型代表就是方法重写。
简单地说就是拿父类的引用指向子类的空间!比如下面的代码:
Base tmp = new Derived();☘ 使用场景:
// 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a) {
a.eat();
}
public static void main(String[] args) {
Dog dog = new Dog("小七", 1);
eatFood(dog);
}3. 方法返回:
// 作为返回值:返回任意子类对象对应的父类引用
public static Animal buyAnimal(String var){
if("狗" == var) {
return new Dog("狗狗",1);
}else if("猫" == var) {
return new Cat("猫猫", 1);
}else {
return null;
}
}instanceof有时候可能需要调用子类特有的方法,此时将父类引用再还原为子类对象即可,称为向下转型。

但是向下转型用的比较少,并且向下转型是不安全的,因为向下转型到不是原来引用的子类的时候,运行时就会抛异常,也是因为不同子类可能有不同数量的拓展成员。
Java 中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为 true,则可以安全转换,如下所示:
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝", 2);
Dog dog = new Dog("小七", 1);
// 向上转型,是安全的,可以直接转换
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
System.out.println("--------------");
// 向下转型,是不安全的,要使用instanceof关键字判断
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
// 执行结果:
元宝吃鱼~~~
小七吃骨头~~~
--------------
狗bark~优点:
if-else 语句。缺点:
一段有坑的代码,我们创建两个类,B 是父类,D 是子类,D 中重写 func 方法,并且在 B 的构造方法中调用 func,如下所示:
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果:
D.func() 0D 对象的同时,会先调用 B 的构造方法。B 的构造方法中调用了 func 方法,此时会触发动态绑定,会调用到 D 中的 func。D 对象自身还没有构造,此时 num 处在未初始化的状态,所以值为 0。结论:"用尽量简单的方式使对象进入可工作状态",尽量不要在构造器中调用方法,因为如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。