Java面向对象:继承特性的学习

本文介绍了面向对象的继承特性: 什么是继承 继承的概念 Java中继承的语法 在继承下父类成员的访问 super和this关键字 父类和子类构造方法 在继承下类中出现初始化代码的执行顺序 父类成员的访问权限对子类的可见性 Java的继承关系 final关键字 认识继承和组合关系

一.面向对象特性:继承

继承是面向对象特性之一,其最大特点就是对共性的抽取,实现代码的复用以此衍生出重写,向上转型,动态绑定,多态等用法…

1.为什么需要继承

在Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计面向对象程序是就需要考虑类与类之间的关系

比如:狗类 猫类 其是两个不同的对象,具有自身各自的特点,但是从类角度来看,猫类和狗类存在关系链其都属于动物!
它们都属于动物,其也具备动物的属性和行为,也就是狗类和猫类会存在共同的属性和行为…
当然不局限于猫类和狗类,自然界也有很多动物,它们之间各有各自的行为,但也有作为动物共同拥有的属性和行为

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
用Java语法简单描述下猫类和狗类:

class Dog {
    
     //狗类
 public String name;
    public int age;

    public void sleep(){
    
      //动物都具备的行为
        System.out.println(age+"岁的"+name+"正在睡觉");
    }
     public void eat(){
    
       //单独的狗行为
        System.out.println(age+"岁的"+name+"正在吃狗粮");
}
class Cat{
    
      //猫类

public String name;
    public int age;

public void sleep(){
    
    
        System.out.println(super.age+"岁的"+name+"正在睡觉");
    }
    //动物共有的属性和行为

    public void eat(){
    
     //单独的猫类行为
        System.out.println(age+"岁的"+name+"正在吃猫粮");
    }
}

以上是猫类和狗类的示例代码,猫和狗同时具备姓名年龄属性,也具备睡觉的行为,
但它们也有各自的行为如:吃猫粮吃狗粮
在设计这些类时,会出现很多重复的代码书写,如给性别年龄属性…睡觉行为,当再描述狮子,老虎类时,也要写上这些属性和行为,在设计类时出现了大量重复的代码,
这些重复代码都是每个类共有的特性,针对这种情况,可以设计对多个类共性的抽取得到一份代码,
而这份代码能被这些抽取的类使用,这便是发生了继承
而根据继承这一特性,即可以对这种情况进行优化,大幅度减少重复代码的书写

2.继承的概念

继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承主要解决的问题是:共性的抽取,实现代码复用。

例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用。

将猫和狗抽取出来的共性形成一个类,而这个类里的属性和行为都能被猫狗类使用,这个类里的属性行为也被看成是动物特有的,也就是动物类…
可以理解为:

动物类具有的这些属性和方法拓展出各自特有的方法和属性就形成了不同的动物如:猫类和狗类
猫类和狗类都是继承于动物类,其都具有动物类特有的属性和方法,自身具备自身类特有的属性和方法…

继承关系可以发生在很多类之间,不能笼统的用动物类 猫狗类表示
对多个类共性的抽取,形成的新的类,这个类称之为 父类 .超类或者基类
而被抽取共性的类 称之为子类 或者 派生类

由此形成了子类和父类这种继承关系…

在这里插入图片描述
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,
继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,实现多态

父类派生出子类 子类还可以作为父类自己派生出子类…

3.Java继承的语法

在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:

修饰符 class 子类 extends 父类 {
    
    
// ...
}

使用继承关系 关键字extends对 猫类狗类进行优化

class Animal{
    
    
String name;
int age;
public void sleep(){
    
      //动物都具备的行为
        System.out.println(age+"岁的"+name+"正在睡觉");
    }
}


class Dog extends Animal{
    
     //狗类继承了动物类
     public void eat(){
    
       //单独的狗行为
        System.out.println(age+"岁的"+name+"正在吃狗粮");
}


class Cat extends{
    
      //猫类 继承 动物类

    //动物共有的属性和行为

    public void eat(){
    
     //单独的猫类行为
        System.out.println(age+"岁的"+name+"正在吃猫粮");
    }
}

对猫类狗类 (子类)共性抽取 形成动物类 (父类) , 通过extends 形成父子类关系
此时猫类狗类没有重复的代码,但是其共用的属性和行为都没有丢失

此时实例化子类对象后,父类里的成员变量和方法都会继承一份到子类里(由编译器完成)
子类可以使用继承过来的属性和行为
在这里插入图片描述
注意:

1 . 只有子类实例化成子类对象后才会将父类中的成员变量或者成员方法继承到子类中
2 .子类对象继承父类之后,必须要新添加自己特有的成员,体现出与父类的不同,否则就没有必要继承了
3 .只能继承成员变量和方法也就是非静态的,不能继承静态的

4.父类成员的访问

当子类对象继承父类后,在子类对象里可以使用父类的成员变量和成员方法
但是,子类和父类也是两个独立的类,其自身可以出现同名的成员变量和成员方法
所以在设计上可能出现子类父类同名的情况

下面分析一下当父类有成员变量和成员方法,子类可能存在同名成员变量和同名成员方法情况

①. 子类和父类不存在同名成员变量时

当父类里有成员变量,而子类里不存在时,这是正常继承的情况,可以直接访问继承过来的父类成员变量
示例:

class Animal{
    
    
 String name;
 int age;
}
//狗类
class Dog extends Animal{
    
    

}
//....
public static void main(String[] args) {
    
    
        Dog dog=new Dog();
        dog.name="小白";  //访问继承的父类name
        dog.age=3;       //访问继承的父类age
    }

此时狗类对象有一份从父类继承过来的成员变量

②.子类和父类存在同名成员变量时

同名成员变量条件 :类型名相同 变量名相同
当子类和父类存在同名变量,子类对象调用同名成员变量时,访问的是子类自身的成员变量
示例:

class Animal{
    
    
 String name;
 int age;
}
//狗类
class Dog extends Animal{
    
    
String name;
int age;
}
//....
public static void main(String[] args) {
    
    
        Dog dog=new Dog();
        dog.name="小白";  //访问子类自己的name
        dog.age=3;       //访问子类自己的age
    }

此时狗类自身有一份成员变量,还有一份从父类继承过来的成员变量…
就近原则访问的是子类自己的,当然也可以通过super关键字指定访问父类的同名成员变量…

注意:

1.父类对象可以用父类自己的成员,不能用子类的成员,子类可以用自己的成员还能用继承的父类成员…
2.当出现同名情况时子类对象 优先访问子类自身的成员变量
3. 如果访问的成员变量子类中没有父类也没有定义,则编译报错

③.子类和父类不存在同名成员方法时

当子类和父类中不存在同名成员方法时,实例化子类对象后,访问的是继承的父类成员方法

class Animal{
    
    
 public void sleep(){
    
    ...};
}
//狗类
class Dog extends Animal{
    
    

}
//....
public static void main(String[] args) {
    
    
        Dog dog=new Dog();
          //访问继承的父类name
        dog.sleep()       //访问继承的父类sleep成员方法
    }

此时只有父类有成员方法,子类没有同名的,访问的是父类的…
此时访问的父类方法,其方法内只能访问父类或者父类以上的成员变量或者方法

④.子类和父类存在同名成员方法时

成员方法同名条件: 返回类型 方法名 参数列表 都要相同 子类访问权限要大于等于父类

示例:

class Animal{
    
    
 public void sleep(){
    
    ...};
}
//狗类
class Dog extends Animal{
    
    
 public void sleep(){
    
    ...};
}
//....
public static void main(String[] args) {
    
    
        Dog dog=new Dog();
          //访问继承的父类name
        dog.sleep()       //访问继承的子类sleep成员方法
    }

当子类和父类出现同名成员方法时,子类对象访问的是子类自身的成员方法(这里也发生了方法重写),也可以通过super关键字访问父类方法
在子类方法里可以访问子类自身的方法或变量,也可以访问父类即以上的方法和变量
在父类方法里可以访问父类自身和以上的类方法和变量但是不能访问子类的

简单认识方法重写(Override)

方法重写也成方法覆盖,发生在子类父类继承关系上,当子类和父类有同名方法时,此时发生了子类方法重写父类方法…此时使用的子类方法访问的子类和父类同名成员方法时调用的是子类同名方法
这就好比父类自身有自己的行为,而其子类也有相同的行为但是具体表现形式可以根据子类自身而改变,也就是重写了方法内部实现

当子类和父类有相同方法名的方法 但是参数列表里 参数类型 参数个数 参数顺序 不同时,它们不构成同名方法不构成重写,而是重载!!!

注意:

1.通过子类对象访问父类与子类中同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,都没找到编译报错。
2.通过子类类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用 方法适传递的参数选择合适的方法访问,如果没有则报错;

⑤.使用super访问父类成员

上面说到当子类和父类出现同名成员变量和成员方法时,使用子类对象优先访问的是子类的成员变量和成员方法,但是并不是父类成员变量成员方法就不存在,其也被继承了一份,
此时想要访问需要用到super关键字

Java提供了super关键字,该关键字主要作用:在子类方法中访问父
类的成员

在对应成员变量和成员方法前加上super即表示访问父类内的
示例:

class Animal{
    
    
 String name;
 int age;
 public void sleep(){
    
      
        System.out.println(age+"岁的"+name+"正在睡觉");
    }
}
//狗类
class Dog extends Animal{
    
    
String name;
 int age;
 public void sleep(){
    
      // 子类成员方法
        super.name = "大白";  // 访问父类的name成员变量
        super.age = 6; //访问父类的age成员变量
        super.sleep();  //访问父类的name成员方法
        System.out.println(age+"岁的"+name+"正在睡觉");  //子类成员变量
    }
}
//....
public static void main(String[] args) {
    
    
        Dog dog=new Dog();
        //dog.super.name="大白";  //  错误写法 不能在非子类以及静态内访问父类的成员
        //dog.super.age=6;       // 需要在子类非静态的方法内访问父类的成员 
        dog.name="小白";   //子类自身的name
        dog.age=3;    //子类的age
        //dog.super.sleep();  不能在非子类非成员方法内访问父类成员
        dog.sleep();  //子类对象的成员方法
    }

在这里插入图片描述
通过在子类方法里使用 super关键字 调用子类方法,访问到了父类里的同名成员方法和成员变量
通过对象引用访问到子类的成员方法和成员变量

注意:

1.super是子类对象内能够使用的关键字,需要通过访问子类对象,在子类对象内才能调用super访问父类成员,在非子类或者子类的子类里以及任何静态代码内都不能使用super关键字
2.在子类对象内使用super关键字,访问的是父类的成员和方法,如果访问的是父类方法,其方法不能访问到子类的成员

6. super和this的区别

super能够在子类里访问父类的成员,this在子类里代表着子类对象的引用能访问子类的成员,二者又有什么区别呢?

①.super和this的相同点

它们都是Java关键字
它们都是在有实例对象情况下使用的
super和this都是子类对象内非静态方法下使用的关键字
super和this关键字 都可以用来访问成员变量 成员方法 成员构造方法但不能访问静态的成员
在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在

②.super和this的不同点

this出现在有对象情况下使用,super出现有对象情况下且发生继承关系
super能够访问父类的成员方法(super.成员方法) 父类的成员变量 (super.成员变量)父类的构造方法 ( super(构造方法) )
this能够访问子类的成员方法 成员变量 成员构造方法, 当子类没有的成员 父类里有,此时this是可以在子类对象里访问到父类的成员的,因为父类成员被继承就相当于是子类对象的一部分.(所以在特殊情况下this既能访问父类成员也能访问子类 但是不能访问父类构造方法)
而super不能访问子类的任何成员…
super只是关键字,不能代表父类对象的引用,而this是子类对象的引用

7.父类构造方法

①.如何使用父类构造方法?

父类和子类都是不同的类,都有自己的构造方法,当它们发生继承关系后,其对应关系是怎么样的呢?

父子父子,先有父再有子,即:子类对象构造时,需要先调用父类构造方法,先给父类成员进行构造,然后再执行子类的构造方法。

在类和对象章节认识到了构造方法,其是用来给对象成员进行初始化的,我们可以通过它在实例化对象时就给定参数时对对象内部成员进行初始化,
在发生继承后,一般都是使用的从父类继承过来的成员,而父类它也有自身的构造方法,用于我们在实例化子类对象时先给父类的成员进行初始化

示例:

class Animal{
    
    
 String name;
 int age;
 public void sleep(){
    
      
        System.out.println(age+"岁的"+name+"正在睡觉");
    }
    
    public Animal(String name, int age) {
    
    
        this.name = name;  //父类里 可以用this表示当前父类的自身成员变量或者成员方法 可以用super表示它自己的父类 但是不能访问子类的成员变量和方法
        this.age = age;
        System.out.println("调用两个参数的父类构造方法");
    }
}

class Dog extends Animal{
    
    
public String name;  //子类成员
public int age;
public void sleep(){
    
     //子类方法
        super.sleep();
        System.out.println(age+"岁的"+name+"正在睡觉");
    }
    
Dog(String name,int age){
    
    
 public Dog(String name, int age) {
    
    
        //自己设置的带两个参数的子类对象构造方法,每个子类构造方法里首行没有写super会默认加上一个不带参的super

        //this();  // super() 和this() 不能同时出现,当没有写super时首行可以写this()但只能是无参的写其他的会报错并且当前子类构造方法得是有参的
        // (在调用当前子类构造方法给父类无参构造方法构造完后还可以调用子类无参构造方法(不能是其它有参的),但是此时不能再出现super()),运行时编译器还是会在this()前面加super()
        super(name,age);
        
      }
   }
}

在这里插入图片描述
通过实例化子类对象传两个参数给子类构造方法后再将这两个参数通过super关键字调用父类构造方法 给父类成员变量进行初始化
此时成功输出6岁的大白正在睡觉 ,而出现0岁的null正在睡觉是因为子类成员并未初始化

当没有父类和子类没有同名变量时,父类有成员,此时还可以在子类构造方法里使用this给继承过来的父类成员赋值

注意:

1.子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
3. 在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中第一条语句。
4. super(…)只能在子类构造方法中出现一次,并且不能和this同时出现

②.父类和子类构造方法之间的爱恨情仇

编译器是如何实现给子类构造前必须给父类成员进行构造的呢?

当有继承关系,子类和父类没有构造方法时,子类默认编译器提供一个无参构造方法,内部没有调用父类构造方法语句会提供一个super();语句 而父类会默认提供一个无参构造方法
当子类有构造方法时,首行没有出现super或者this语句编译器也会默认提供一个super()调用父类无参构造方法.

当父类存在有参构造方法时,子类也必须写构造方法,因为父类有构造方法后编译器不会再给父类提供无参构造方法, 其内调用的无参父类构造方法已经无效了,必须自己写super语句调用对应父类有参构造方法或者父类自己加一个无参构造方法

  1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构 造方法
  2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的 父类构造方法调用,否则编译失败

8.初始化代码执行顺序的笔试题练习

在上一篇static和代码块 博客中 最后有道代码执行顺序练习,这里再来复习一道

class Person {
    
    
public String name;
public int age;
public Person(String name, int age) {
    
    
this.name = name;
this.age = age;
System.out.println("构造方法执行");
}
 {
    
    
System.out.println("实例代码块执行");
} 
static {
    
    
System.out.println("静态代码块执行");
}

}
public class TestDemo {
    
    
public static void main(String[] args) {
    
    
Person person1 = new Person("小白",10);
System.out.println("============================");
Person person2 = new Person("大白",20);
}
}
//代码输出结果是什么?

实例化对象先加载类 执行静态代码块 再实例化对象 执行构造代码块 再构造方法
再实例化对象后不会加载类 此时只执行构造代码块 然后执行构造方法
执行结果:

静态代码块执行
实例代码块执行
构造方法执行

============================
实例代码块执行
构造方法执行

  1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
  2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行

当出现继承关系后.子类会先给父类构造,同时加载子类也会先加载父类,根据这些特点再看看下面代码输出结果↓

class Person {
    
    
public String name;
public int age;
public Person(String name, int age) {
    
    
this.name = name;
this.age = age;
System.out.println("Person:构造方法执行");
}

 {
    
    
System.out.println("Person:实例代码块执行");
} 
static {
    
    
System.out.println("Person:静态代码块执行");
}

}
class Student extends Person{
    
    
public Student(String name,int age) {
    
    
super(name,age);
System.out.println("Student:构造方法执行");
} 

{
    
    
System.out.println("Student:实例代码块执行");
} 
static {
    
    
System.out.println("Student:静态代码块执行");
}

}

public class TestDemo4 {
    
    
public static void main(String[] args) {
    
    
Person person1 = new Person("小白",10);
System.out.println("============================");
Person person2 = new Person("大白",20);
}
}
//代码输出结果是什么

实例化子类对象 先加载类 加载子类前先加载父类 执行父类静态代码块 再加载子类 执行子类静态代码块 实例化子类对象 先构造父类 执行父类构造代码块 执行父类构造方法
给子类构造 执行子类构造代码块 执行子类构造方法

再次实例化子类对象: 不用再加载类 此时先给父类构造 执行父类构造代码块 执行父类构造方法 给子类构造 执行子类构造代码块 执行子类构造方法
运行结果↓

Person:静态代码块执行
Student:静态代码块执行
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行
===========================
Person:实例代码块执行
Person:构造方法执行
Student:实例代码块执行
Student:构造方法执行

9.不同访问权限的父类成员在子类里的可见性

在类和对象中,为了实现封装特性,Java中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。

介绍到了 private default protected public 四种访问权限修饰符

那父类中不同访问权限的成员,在子类中的可见性又是什么样子的呢?

在父类中被public修饰的成员 子类里都是可见的是可以访问
在父类默认权限修饰的成员 子类是在同一个包中的情况下是可以访问的,但是在不同包内情况下,虽然继承了但是没有访问权限,不能直接访问 可以通过公开的方法到对应的包内访问

被private修饰的成员(只能在当前类里访问) 在子类中虽然继承了该成员,但是子类中不能直接访问,可以通过调用父类方法在父类中访问到

被protected修饰的成员,子类和父类在同一个包中时,子类可以直接访问成员,
在这里插入图片描述

但是当在不同包的情况下必须在子类里的且是通过子类对象调用才能访问,在其他类中无法访问,在子类里用父类对象也无法调用…
在这里插入图片描述
注意:父类中有些成员变量虽然在子类中不能直接访问,但是也继承到子类中了,需要提供交互接口或一些特殊方法访问

10.Java中的继承关系

在现实生活中,事物之间的关系是非常复杂,灵活多样,可能出现单继承 A继承B A继承C 一个儿子继承父和母也可以多层继承 A继承B B继承C 儿子继承父亲 父亲继承爷爷

但在Java中只支持以下几种继承方式:
单继承: A->B
多层继承 A->B->C
不同类继承同一个类 A->C B->C

注意:Java中不支持多继承。

时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到 一系列复杂的概念,
都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会 更加复杂. 但是即使如此,
我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了.
如果想从语法上进行限制继承, 就可以使用 final 关键字

11.认识final关键字

final在Java中起到密封的意义,可以对类 方法 成员变量 局部变量进行密封

当一个类不想被其他类继承时可以用final修饰

final修饰的类为密封类表示该类不能被继承

public final Test{
    
     //密封类
}

在方法内想得到一个不同类型的常量标识符时可以使用final修饰局部变量

final修饰的局部变量表示为常量,可以同时对其初始化或者在后续使用使初始化,但是一旦赋值就不能更改

在对象内表示一个常量标识符可以用final修饰非静态成员变量

final修饰的非静态成员变量为非静态成员常量,在声明时必须同时对其初始化,或者可以在所有构造方法 构造代码块内对其初始化, 必须保证对象实例化出来后此常量内有具体的值

public final NUM=1;// 规定大写

想表示一个类常量时可以使用final修饰静态成员变量

final修饰的静态成员变量为类常量,在声明时需要对其就地初始化,或者在静态代码块对其初始化,在加载类后其内一定要有确定的值

public static final NUM=1; //规定大写

发生继承关系时,不想子类重写父类方法可以用final修饰父类方法

final修饰的方法为密封方法,此方法会被继承到子类但是子类不能写同名的方法,即子类不能重写父类方法

public final void func(){
    
    }  // 修饰静态方法没意义

当实例子类对象发生继承情况,父类中的被static修饰的静态成员不会被继承,父类成员被final修饰的会被继承,但是子类不能再有同名的成员(重写) 父类成员被一些访问权限修饰符限定 且在范围外时 如private protected 其会被子类继承,但是子类无法直接访问,需要通过交互接口

12.继承和组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法
(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。

继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车

继承是两个类之间使用extends
而组合是一个类里存放另一个类的引用

在这里插入图片描述

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lch1552493370/article/details/129159123