Java 面向对象:类的继承

禁止码迷,布布扣,豌豆代理,码农教程,爱码网等第三方爬虫网站爬取!

继承

继承(inheritance)是面向对象编程的重要手法,思想是基于已有的类创建新的类。继承已存在的类时可以复用原类的方法,同时增加一些新的方法和字段,使得新的类可以对新的情况进行处理。
例如虚竹误打误撞破了玲珑棋局,继承了无崖子七十余年的功力,而后面他在童姥和李秋水互斗时获取了她们的九成功力。如果把无崖子当做是原类,无崖子的内力当做类的状态,招式当做类的方法,虚竹就当做是新类。我们发现虚竹不仅继承了无涯子内力和招式,还获得了其他的内力和新招式,也就是说类可以新类不仅能继承原类的字段和方法,也能自己拥有些独特的字段和方法。

超类、子类

一般将已存在的类称为超类、基类或父类,新的类被称之为子类或派生类。虽然原类被称之为超类,但是它并不见得比子类拥有更多的功能,实际上是“青出于蓝而胜于蓝”,子类将具有更多的特色。超类和子类的概念来源于数学的集合,例如员工是一个集合,经理虽然属于员工,但是并不等同于员工,因此可以称经理是员工的子集,或员工是经理的超集。
当使用继承对超类进行拓展时,只需要指出子类和超类的不同之处。这个操作将使用到 extends 关键字,表示正在构造的新类派生于一个已存在的类。例如我有一个 Employee 类,现在我要派生出 Manager 类:

public class Manager extends Employee{
   private double bonus;

   public void setBonus(double b){
      bonus = b;
   }
}

覆盖

继承时我们没必要把原有的类全套照搬,也要“取其精华去其糟粕”。当遇到不使用的方法时,可以写一个新的方法来覆盖之。例如 Employee 类有个方法为 getSalary():

private String name;
private double salary;
private LocalDate hireDay;

public double getSalary(){
      return salary;
}

当需要重写 getSalary() 方法时,就可以在新的类中重新定义一次,这样就能够覆盖掉原方法了。

private double bonus;

public double getSalary(){
      return salary + bonus;
}

不过很可惜这种写法是错的,因为在原类中,salary 字段是私有字段,也就是说该方法只能被 Employee 类的方法使用。

新的 Manager 类不能访问 Employee 类的字段,那就得使用 Employee 类的字段访问器 getSalary() 来访问。

public double getSalary(){
      return salary;
}

但是这个方法已经被我们重写了,而且调用这个方法将会出现自己调用自己的递归调用情况。因此我们需要用关键字 super,这个关键字可以指示调用的是超类中的方法。

private double bonus;

public double getSalary(){
      double baseSalary = super.getSalary();
      return baseSalary + bonus;
}

子类构造器

由于子类的构造器不能访问超类的私有字段,因此如果要初始化超类中的字段,就需要调用超类的构造器。此时还是可以使用 super 关键字来表示对超类构造器的调用:

private double bonus;

public Manager(String name, double salary, int year, int month, int day){
      super(name, salary, year, month, day);
      bonus = 0;
}

如果子类构造器没有显式地调用超类的构造器,那么会自动调用超类的无参构造器

多态

继承并不仅限于一个层次,而从某个特定的类到其祖先的路径被称为继承链。通常一个祖先类可以有多个子孙链,而且可以不断地继承下去。

根据替换原则,程序中出现超类对象的任何地方都可以用子类对象来替换,也就是说子类的每个对象也会是超类对象。Java 类的对象是多态的,也就是说一个类变量不仅可以引用一个它本身的类对象,也可以引用它的子类的对象。例如:

Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
Employee staff = new Employee[3];
staff[0] = boss;

虽然 staff[0] 和 boss 引用的是一个相同的对象,但是 staff[0] 认为 boss 是个 Employee 对象。也就是说,staff[0] 只能使用 Employee 的方法,而 Manager 的方法只能由 boss 对象来用。同时还有一点,不能讲超类的引用赋给子类,也就是多态不能反过来。
子类引用的数组可以转换成超类引用的数组,这个过程不需要转换。例如:

Manager[] managers = new Manager[10];
Employee[] staff = managers;

这种写法是可以的,但是在实际中很可能就会混用,从而出现问题。所以对于数组我们要注意在创建时是什么类,并且仅将相同类对象的引用存储到数组中。

禁止继承

有的类被定义之后,可能不希望被新的类继承,这种不可扩展的类称之为 final 类。在类定义时使用 final 关键字就可以表示为 final 类,String 类就是一个知名的 final 类。例如:

public final class Manager extends Employee

其类中的方法同样也不能被覆盖,当然也可以给超类中的某几个方法加上 final。

public final String getName(){
      return name;
}

将方法或类声明为 final 可以确保其不会在子类中改变语义,例如 String 类就要注意避免这种事发生。在设计类的层次时,需要好好考虑哪些类和方法要被定义为 final。

强制类型转换

需要暂时忽略对象的实例类型,使用对象的全部功能时,就需要对对象进行强制类型转换。对象类型转换的语法和数值表达式的语法蕾西,用圆括号把目标类名括起来,卸载需要转换的对象引用前。例如:

Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
Employee staff = new Employee[3];
staff[0] = boss;
Manager boss = (Manager) staff[0];

当一个超类引用赋予一个子类变量时,必须要进行强制类型转换。不过强制类型转换并不是个很好的做法,实际中应当少用。

抽象类

对于一个继承层次,越上层的类于具有一般性,同时也会显得更为抽象。例如对于员工和铠甲勇士,他们都是人类,那么他们都具有一些相同的特征,例如名字。

可以将这种类抽象出来,使用 abstract 关键字抽象出来,同时也可以用该关键字将方法定义为抽象方法。抽象类中可以包括字段、抽象方法和具体方法,通常是用来给子类提供模板,子类可以保留抽象类中的方法,也可以进行酬谢覆盖。

public abstract class Person{
   public abstract String getDescription();
   private String name;

   public Person(String name){
      this.name = name;
   }

   public String getName(){
      return name;
   }
}

如果一个类被声明为抽象类,就不能创建这个类的对象。当定义一个抽象类的对象变量时,该变量只能引用非抽象子类的对象。

受保护访问

当希望某个超类的方法只允许子类访问,或者希望子类的方法可以访问超类的字段时,可以将类方法或者字段声明为受保护 protected。受保护字段可以限制该字段只能来同一个包中的类访问,但是这个字段需要慎用,因为其他程序员可能会从这个类派生出新的类,从而破坏封装性。

访问控制修饰符 作用
private 仅对本类可见
public 对外部可见
protected 对本包和所有子类可见
不加修饰符 对本包可见

包装器和装箱

基本类型的数据都不是对象,但是我们有时候需要将一个基本类型变量转换为对象。Java 为每个基本类型都对应了一个类,这些类就被称之为包装器,例如 Int 类型对应的类为 Integer。包装器类是不可变的,一旦构造了包装器就不能修改其中的值,同时包装器类是 final,也就是说包装器不能被继承。
例如我需要一个 int 类型的数组列表,但是 <> 中的类型不能是基本类型,这是就可以使用 Integer 包装器类来声明。

var list = new ArrayList<Integer>();

当使用 ArrayList 的 add() 方法添加元素时,由于 int 类型变量不是对象,因此类似 “list.add(1)” 这种写法会自动转换为下面的代码,这种变换称为自动装箱

list.add(Integer.valueOf(1));

当一个 Integer 对象赋值给 int 变量时,例如 “int n = list.get(i)” 也会自动转换,这种变换称为自动拆箱

int n = list.get(i).intValue();

Integer 对象提供了很多有用的方法,例如 parseInt(String) 方法就可以把一个字符串转换为 int 类型变量。Integer 也支持进制转换,分别使用 toBinaryString、toOctalString 和 toHexString 方法就可以得到。

int num;
String str = "123";
		
num = Integer.parseInt(str);
System.out.println(Integer.toBinaryString(num));
System.out.println(Integer.toOctalString(num));
System.out.println(Integer.toHexString(num));

包装器对象可以引用 null,同时如果混用了 Integer 和 Double 类,Integer 也会自动拆箱升级为 double,在自动装箱为 Double。

继承的设计技巧

  1. 将公共操作和字段放在超类中。
  2. 不要使用 protected 字段,虽然它适合知识不提供一般用途,而是应该在子类覆盖的方法。这是因为子类集合是无限制的,任何人都能由你的类派生出子类。而且 Java 中同一个包的所有类,都可以随意访问 protected 字段。
  3. 使用继承实现 “is-a” 关系,也就是所谓的类的父子继承关系。
  4. 除非所有的继承方法都很有意义,否则不要使用继承。
  5. 覆盖方法时,不要改变方法预期的行为。
  6. 使用多态,不要使用类型信息。使用多态方法或接口实现的代码,比使用多个类型检查的代码更易于维护和扩展。

参考资料

《Java 核心技术 卷Ⅰ》,[美]Cay S.Horstmann 著,林琪 苏钰涵 等译,机械工业出版社

posted @ 2020-08-07 00:27  乌漆WhiteMoon  阅读(416)  评论(0编辑  收藏  举报