Java 三大基础特性の继承

383 阅读4分钟

这是我参与8月更文挑战的第 18 天,活动详情查看:8月更文挑战

前言

当我们整理好需求后需要编写一个新的 class B 时,发现手里已经有一个写好了的 class A,而且,A 中已经基本涵盖了我们的需求,我们要做的就是使用 ctrl + c 复制一个 A,然后 ctrl + v 得到了 A(1).java,对新文件重命名得到 B.java,最后,在 B.java 中补上我们还缺失的功能。我们大部分的开发生涯,应该都是这么过来的吧

图片.png

好消息是, Java 提供了一种 class 复用的方案,就是 继承,向上面一个例子,如果 class B 的声明是 class B extends A,那 B 默认就能访问 A 的全部字段、全部方法,就好像自己也写了一份似的 (当然,A中的那些方法必须声明为 public 或 protected,亦或者是包访问权限,且 A,B 位于同一个包目录下)

class A{
  public void f(){
    // do something
  };
}
class B extends A{
  public void g();
}
new B().f();	// use f() from A
new B().g();	// use g() by self

限制

在 java 中,每个类都只能继承一个类;如果没有显示的指明自己继承的类,则会默认继承 java.lang.Object

使用继承,一个语义化的描述就是 xx is a yy,例如 Trangle is a Shape, Dog is a Animal, Fish is Food如果不存在这种 is a 的关系,建议使用组合来替代继承

另一个考量的地方,我们需要编写一些实体类,而且这些实体类有着共同的行为,或者是共同的成员,通过继承,我们可以省略这部分公共的代码,却能实际拥有这些功能。这样还有另一个好处,如果是编写一些公共的处理方法,我们甚至能引入泛型,例如 public <T extends POJO> void func(T t){},这种带限定的泛型,允许我们使用 POJO 这个父类中的方法、参数等

重写

当我们使用继承后,发现父类中的某些功能,满足不了新的需求,我们要对这个方法进行升级,或者是更加彻底的重新编写

class A{
	public void f(){
    // do something
    System.out.print("A.f()")
  };
}
class B extends A{
  @Overrid
  public void f(){
    // do otherthings
    System.out.print("B.f()")
    super.f(); // 如果需要调用父类的 f(),可以用类似 super.xx() 的形式
  }
  public void g();
}

在这个示例代码块中,我们使用了注解 @Override 表示当前的方法是重写了父类中对应的方法,若父类中不存在该方法,就会有报错提示 避免我们重写父类中并不存在的方法

如果我们的重写方法的目的是对父类中对应的方法做功能增强,那么我们可以用类似方法调用的方式,通过 super.xxx() 来执行父类中的 xxx()

和重载的区别

重写

重点是子类对父类中对应方法的覆盖编写

重载

在同一个类中,使用相同的方法名,但是,他们的参数列表不同,JVM 借此可以定位到你实际调用的哪一个方法

或者说,在同一个 class 中,方法名+参数列表 是独一无二的

// 方法重载的示意
class C{
  void f(){};
  void f(int a, String b){}
  void f(String b, int a){}
}

重载”陷阱“

重载的概念,不仅用于方法,也能适用与构造器,仔细阅读下一段代码

class D{
  D(int a, String b){}
  D(Strgin b, int a){}
  static createInstanceForA(int a, String b){}
  static createInstanceForB(Strgin b, int a){}
}

在初始化 class D 的时候,我们只需要传入两个参数,但是,在初始化的过程中,我们需要做不同的操作,这时候,能做的就是调整参数的顺序,通过重载的方式获得一个全新用途的构造器,但这种操作存在一个非常致命的问题, 语义非常不明显,他人不知道要实际需要调用哪一个构造器,因为他们的入参类型完全一致,仅仅是顺序不同,当然,我们可以贴心的提供注释说明(程序员最讨厌的事情之一:为他人写注释文档)

为了解决这个问题,推荐使用静态工厂的思路来创建实例,为这个方法提供详细的描述名,使用者见名知意,具体的操作方式就是后面的两个静态方法,更加友好了,对吧

子类初始化细节

class A{
  A(int a){}
  A(){};
}
class B extends A{
  B(int a){
    // A(); // 如果不显示指定,就默认调用父类中的无参构造器
    // do
  }
  B(){
  	A(1);	// 我们也可以手动调用需要的 A 构造器
  	// do
  }
}

对于需要手动调用父类构造器的情况:

  1. 父类中不存在无参构造器
  2. 我们不希望默认使用父类的无参构造器

这个调用操作必须写在子类构造器的第一行