Java学习-继承、super关键字

 

1、引例

2、继承

2.1 概述

2.2优缺点

2.3Java继承的类型

3、关键字

3.1super关键字

3.2super与this关键字的区别

4、继承需要注意的几个问题

4.1成员变量和方法

4.2构造器

4.3继承的执行顺序问题

4.4继承破坏父类封装性问题

4.5何时适合用继承


1、引例

假如现在有四个类它们的成员属性分别为:

  • 学生类:姓名,性别,年龄,学校
  • 教师类:姓名,性别,年龄,学科
  • 工人类:姓名,性别,年龄,工种
  • 农民类:姓名,性别,年龄,收入

      对于上面的四个类我们在实际写代码的时候会发现,发我在重复性地写一些代码比如(姓名、性别、年龄)是这四个类共有的成员属性。这时候我们就可以建一个person类把(姓名,性别,年龄)作为它的成员属性,而上面四个类通过继承这个person类实现对共有的成员属性的调用以提高代码的复用性

Java学习-继承、super关键字_第1张图片

2、继承

2.1 概述

     继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

  • 继承是面向对象思想的三大特性之一,使类与类之间产生特殊的关系,即is-a关系

  • 继承是从已有类中派生出新的类,新的类能吸收已有类的属性和方法,并且能拓展新的属性和行为

  • 在Java中使用extends关键字表示继承,语法表示为: class 子类 extends 父类{}

  • 子类被称为派生类,父类又被称为超类

  • 子类继承父类,表示子类是一种特殊的父类,子类拥有父类的非private属性和方法,并且子类可以拓展具有父类所没有的一些属性和方法。

  • 子类即使不扩展父类,也能维持拥有父类的操作。

2.2优缺点

继承的好处是:

  • 提高了代码的复用性
  • 提高了代码的维护性
  • 让类与类之间产生了关系,是多态的前提

继承的缺点是:

  • 增加了耦合性(OOP思想开发原则:高内聚,低耦合    耦合:类与类之间的关系  内聚:自身完成事情的能力)

2.3Java继承的类型

  • Java只支持单继承,不支持多重继承

Java学习-继承、super关键字_第2张图片

Java学习-继承、super关键字_第3张图片

多重继承会存在安全隐患,因为当继承的多个类都存在相同的属性或方法体不同的方法,子类进行调用时,就会产生不知道该调用哪一个类中的方法的情况

  • Java支持继承体系

Java学习-继承、super关键字_第4张图片

3、关键字

3.1super关键字

我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

两种方法:

 1、用在子类的构造方法里(初始化用),主要是调用父类的默认构造方法,如果父类有不止一个构造方法,可以通过super指定具体的构造函数,比如 super(paras);

注意:super表示当前类的父类,super()调用的是父类默认的构造方法,即这样可以对父类进行初始化。如何没有对父类进行初始化,当子类调用父类的方法时,便会从逻辑上出现错误,因为没对父类初始化,父类的方法和属性便没有内存空间。

 2、用在子类里调用隐藏或重写的属性或行为,比如 super.onDestroy()等等

3.2super与this关键字的区别

1. super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)
2. this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)
3. super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
4. this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名这时需用this来指明成员变量名)
5. 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
6. super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它方法。
7. super()和this()均需放在构造方法内第一行。
8. 尽管可以用this调用一个构造器,但却不能调用两个。
9. this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
10. this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
11. 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字

4、继承需要注意的几个问题

4.1成员变量和方法

  1. 子类只能继承父类的所有非私有的成员变量和方法。可以继承public protected 修饰的成员,不可以继承private修饰的。
  2. 但是子类可以通过父类中提供的public 的setter和getter方法进行间接的访问和操作private 的属性
  3. 对于子类可以继承父类中的成员变量和成员方法,如果子类中出现了和父类同名的成员变量和成员方法时,父类的成员变量会被隐藏,父类的成员方法会被覆盖。需要使用父类的成员变量和方法时,就需要使用super关键字来进行引用。           (隐藏是针对成员变量和静态方法,覆盖是针对普通方法。

  4. 当创建一个子类对象时,不仅会为该类的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量,也依然会为父类中定义的、被隐藏的变量分配内存。

  5. 如果子类中的实例变量被私有了 ,其父类中的同名实例变量没有被私有,那么子类对象就无法直接调用该变量,但可以通过先将对象变量强制向上转型为父类型,在通过该对象引用变量来访问那个实例变量,就会得到的是父类中的那个实例变量。                                                                                                                                                                                          (补充:什么是向上转型和向下转型?)

4.2构造器

  1. 子类不能继承获得父类的构造方法,但是可以通过super关键字来访问父类构造方法。

  2. 在一个构造器中调用另一个重载构造器使用this调用完成,在子类构造器中调用父类构造器使用super调用来完成。

  3. super 和 this 的调用都必须是在第一句,否则会产生编译错误,this和super只能存在一个。不能进行递归构造器调用,即多个构造器之间互相循环调用。

  4. 如果父类有无参构造时,所有构造方法(包含任意有参构造)自动默认都会访问父类中的空参构造方法。(自带super();)

  5. 因为继承的目的是子类获取和使用父类的属性和行为,所以子类初始化之前,一定要先完成父类数据的初始化。

  6. 在Java中,每个类都会默认继承Object超类,所以每一个构造方法的第一条默认语句都是super()

  7. 如果父类没有无参构造,反而有其他的有参构造方法时,子类继承父类后,子类必须显式的创建构造器,不论子类的构造器是否和父类构造器中参数类型是否一致,都必须在子类的构造器中显式的通过super关键字调用和父类构造器相应参数的构造方法,否则编译都通不过。

代码示例:

class Person {
 public Person(int age){
     System.out.println(age);
 }

}
class Student extends Person{
 public Student(int age) {
     super(age);
 }
  public Student(){
     super(10); //必须调用父类的有参构造
     System.out.println("子类可以创建其他类型构造器,但是必须显式的用super调用父类构造器")
 }
}

也可以使用this先调用子类中的构造方法,再间接调用父类中的有参构造方法:

public class ExtendTest1  {
 public static void main(String[] args) {
     new Student();
 }

}
class Person {
 public Person(int age){
     System.out.println("父类有参构造");
 }

}
class Student extends Person{
 public Student(int age) {
     super(age);
     System.out.println("子类有参构造");
 }
 public Student(){
     this(10); //可以使用this先调用子类中的有参构造,从而间接调用父类中的有参构造
     System.out.println("子类无参构造");
 }
}

使用this,执行顺序结果为:先调用了子类中无参构造,此无参构造会接着调用子类中的有参构造,又接着调用父类中的有参构造,此时首先执行完毕了父类有参构造,接着子类有参构造执行完毕,最后子类无参构造才执行完毕。

父类有参构造
子类有参构造
子类无参构造

以下这种是错误的:(因为当父类中没有无参构造器时,父类中没有这种类型的构造方法):

class Student extends Person{
 public Student(String name){
     super();
 } //错误的,因为当父类中没有无参构造器时,父类中没有这种类型的构造方法
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}

以下这种正确:(因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有)

class Student extends Person{
 //因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}
class Student extends Person{
//因为当父类中没有无参构造器时,子类中的构造方法的类型在父类中有
 public Student(String name ,int age){
     super(name,age);
 }
 public Student(int age) {
     super(age);
 }
}

class Person {
 public Person(String name ,int age){
     System.out.println(name+age);
 }
 public Person(int age){
     System.out.println(age);
 }
}

结论:当父类中没有无参构造器时,子类继承父类,子类中的构造器方法类型可以和父类中的构造器不同,但是必须每个构造器都显式的使用super关键字调用父类中的某个有参构造器,也可以使用this调用子类中的某个有参构造器,但这个有参构造器必须通过super访问父类中的有参构造器。

4.3继承的执行顺序问题

1、继承体系中的构造器执行顺序

  • 当调用子类构造器实例化子类对象时,父类构造器总是在子类构造器之前执行。
  • 创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果父类通过this调用了同类中的重载构造器,就会依次执行此父类的多个构造器。

2、继承体系中的静态域执行顺序:

  • 当调用子类构造器实例化子类对象时,父类优先于子类进行加载到内存,所以会先执行父类中的静态域

  • 从该类所在继承树最顶层类开始加载,并执行其静态域,依次向下执行,最后执行本类。

  • 静态域优先于main方法,优先于构造器执行

3、父类和子类中都有静态代码块和构造代码块

class Test2_Extends {
  static {
    System.out.println("主类静态块");
}
public static void main(String[] args) {
    Zi z = new Zi();
}
}
class Fu {
static {
    System.out.println("静态代码块Fu");
}

{
    System.out.println("构造代码块Fu");
}

public Fu() {
    System.out.println("构造方法Fu");
}
}

class Zi extends Fu {
static {
    System.out.println("静态代码块Zi");
}

{
    System.out.println("构造代码块Zi");
}

public Zi() {
    System.out.println("构造方法Zi");
}
}

执行结果:

主类静态块
静态代码块Fu
静态代码块Zi
构造代码块Fu
构造方法Fu
构造代码块Zi
构造方法Zi

执行顺序分析:

  1. 主类Test2_Extends先加载到内存,静态域优先于main方法执行,先输出了主类静态块,其中的main方法入栈执行,main方法中创建了子类对象
  2. 子类对象创建过程中,父类和子类都加载到内存中,并且Fu.class优先于Zi.class加载,父类中的静态域先执行后,再执行子类中的静态域,此时会第一个输出:静态代码块Fu,第二个输出:静态代码块Zi
  3. 创建对象时进入子类的构造器,因为Java是分层初始化的,所以会先初始化父类再初始化子类,子类构造器会自动默认先执行父类的构造器,因为构造代码块优先于构造方法执行,所以此时就会先执行父类的构造代码块后,再执行父类的构造方法。所以第三个输出:构造代码块Fu,第四个输出:构造方法Fu
  4. Fu类初始化结束后,子类初始化,第五个输出的是:构造代码块Zi,第六个输出:构造方法Zi

4.4继承破坏父类封装性问题

1、继承严重破坏了父类的封装性,每个类都应该它内部信息和实现细节,而只暴露必要的方法给其它类使用。但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法, 从而造成子类和父类的严重耦合。

2、父类的实现细节对其子类不再透明,从而导致子类可以恶意篡改父类的方法

改进方法:

  • 尽量隐藏父类的内部数据。
  • 尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量
  • 不要让子类随意访问、修改父类的方法
  • 父类中那些仅为辅助其他的工具方法,应该使用private修饰,让子类无法访问方法;
  • 如果父类中的方法需要被外部类调用,则必须以public修饰,但又不想让子类重写,就可以使用final修饰符。
  • 如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰方法。
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

查看下面例子说明在父类构造器中调用被子类重写的方法引发的错误:

package extend;

class Base
{
    public Base()
    {
        System.out.println("父类构造器");
        test();
    }
    public void test()           // ①号test()方法
    {
        System.out.println("将被子类重写的方法");
    }
}
public class Sub extends Base
{
    public Sub(){
        System.out.println("子类构造器");
    }
    private String name="aa";
    public void test()         // ②号test()方法
    {
        System.out.println("子类test");
        System.out.println("子类重写父类的方法,"
            + "其name字符串长度" + name.length());
    }
    public static void main(String[] args)
    {
        // 下面代码会引发空指针异常
        Sub s = new Sub();
    }
}

执行结果:

父类构造器
子类test
Exception in thread "main" java.lang.NullPointerException

分析:

当创建Sub对象时,先执行其父类构造器,如果父类构造器调用了被子类重写覆盖的方法,就调用被子类重写后的②号test()方法,子类的test方法调用了子类的实例变量name,父类直接调用的子类的test方法,此时子类还未初始化,还未调用子类构造器,实例变量name还未被指定初始值,仍然为默认值null,所以引发了空指针异常。

4.5何时适合用继承

  • 子类需要额外增加属性,而不仅仅是属性值的改变。
  • 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。

 

你可能感兴趣的:(java学习)