来日绮窗前
寒梅著花未
目录
继承的概念
继承是什么
问:为什么 JAVA 会有继承呢?
我这里举个例子
假设我们这里有两个类 -- 学生类、教师类
Student
public class Student{ private String name; private int age; private int id; public void behavior(){ System.out.println("上学"); } }
Teacher
public class Teacher{ private String name; private int age; private int id; public void behavior(){ System.out.println("教书"); } }
Student、Teacher 两个类有共同的属性 -- 名字、年龄、编号,如果我们在创建一个职工类也要重新定义这些属性就显得代码十分冗余。既然属性是绝大部分类共有的,那么我们只需把这些共有的属性组织起来形成一个父类(基类),再将需要这些属性的子类(派生类)去继承,实现代码复用。这样代码就会精简许多 ~
继承小概念
继承 (inheritance) 机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用
这里我们还是以上面的 Student、Teacher 为例子:
Student 和 Teacher 都继承了Person 类,其中:
<1>Person 类称为 父类/基类
<2>Student 和 Person 可以称为 Person 的 子类/派生类
<3>继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可
继承关键字
在 Java 中如果要表示类之间的继承关系,需要借助 extends 关键字,具体如下:
【修饰符】 class 【子类】 extends 【父类】
{
// ...
}
继承的使用
问:在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中如何访问父类继承下来的成员呢?
我们这里先做几个测试在总结:
子类中访问父类的成员变量
子类和父类不存在同名成员变量
class Base {
protected int a = 10;
protected int b = 20;
}
public class Derived extends Base{
public int c;
public Derived(){
c = 30;
}
public void PrintDerived(){
System.out.println("a = " + a); // 访问从父类中继承下来的a
System.out.println("b = " + b); // 访问从父类中继承下来的b
System.out.println("c = " + c); // 访问子类自己的c
}
}
class TestDerived{
public static void main(String[] args) {
Derived obj = new Derived();
obj.PrintDerived();
}
}
我们这里就不会有什么冲突,我们再看一个例子 ~
子类和父类成员变量同名
class Base {
protected int a = 10;
protected int b = 20;
}
public class Derived extends Base{
public int c;
public int a = 100;
public int b = 200;
public Derived(){
c = 30;
}
public void PrintDerived(){
System.out.println("a = " + a); // 访问子类自己的a
System.out.println("b = " + b); // 访问子类自己的b
System.out.println("c = " + c); // 访问子类自己的c
}
}
class TestDerived{
public static void main(String[] args) {
Derived obj = new Derived();
obj.PrintDerived();
}
}
我们发现如果我们子类中自己有就不会去访问父类中的同名变量了,如果子类中没有才会去父类中找
总结:
如果访问的成员变量子类中有,优先访问自己的成员变量 |
如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错 |
如果访问的成员变量与父类中成员变量同名,则优先访问自己的 |
子类中访问父类的成员方法
子类和父类不存在同名成员方法
class Base {
protected int a = 10;
protected int b = 20;
public void methodA(){
System.out.println("methodA()");
}
}
public class Derived extends Base{
public int c;
public Derived(){
c = 30;
}
public void PrintDerived(){
System.out.println("a = " + a); // 访问从父类中继承下来的a
System.out.println("b = " + b); // 访问从父类中继承下来的b
System.out.println("c = " + c); // 访问子类自己的c
}
public void methodB(){
System.out.println("methodB()");
}
}
class TestDerived{
public static void main(String[] args) {
Derived obj = new Derived();
obj.methodA();
obj.methodB();
}
}
和成员变量一样,如果子类中没有就继承下来直接就可以用
子类和父类成员方法同名
class Base {
protected int a = 10;
protected int b = 20;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(int n){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
public int c;
public Derived(){
c = 30;
}
public void PrintDerived(){
System.out.println("a = " + a); // 访问从父类中继承下来的a
System.out.println("b = " + b); // 访问从父类中继承下来的b
System.out.println("c = " + c); // 访问子类自己的c
}
public void methodA(){
System.out.println("Derived中的methodA()");
}
public void methodB(){
System.out.println("Derived中的methodB()");
}
}
class TestDerived{
public static void main(String[] args) {
Derived obj = new Derived();
obj.methodA();
obj.methodB(10);
}
}
如果子类中有同名方法就优先访问子类的方法,如果子类的同名方法与父类的方法构成重载就根据具体的参数来判断
总结:
通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错 |
通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错 |
super 关键字
问:由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?
答:Java 提供了 super 关键字,该关键字主要作用 -- 在子类方法中访问父类的成员
运用场景一:构造父类成员方法
public class Person {
private String name = "张三";
private int age = 18;
private int id = 2024125;
public void PrintPerson(){
System.out.println("name: "+name+" age: "+age+" id: "+id);
}
}
//
public class Student extends Person {
public void behavior(){
System.out.println("上学");
}
}
//
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.PrintPerson();
}
}
以上代码是最基础的子类继承父类,并调用了父类的成员方法
我们将这段代码稍稍改一下:
在 Person 中加入一下这段代码(有参的构造方法):
public Person(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
我们会发现 Student 会报错 ~
这里就很直观的说明我们在创建子类对象的时候,系统会自动调用父类的构造方法帮我们初始化父类的属性,因为我们这里的父类是有参构造,所以我们必须手动调用。那么一开始的代码为什么不会报错呢?原因是我们没有写构造系统会默认生成一个无参的构造方法
所以解决的方法是
(1)在子类的构造方法中初始化我们父类的成员属性,这里就需要用到 super 关键字
public Student() {
super("李四",19,2024137);
}
该关键字的作用就是:在子类方法中访问父类的成员
(2)在父类中创建一个无参构造方法
public Person() {
this.name = "王五";
this.age = 20;
this.id = 2024567;
}
这样就可以解决这个报错信息了!!!
在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整 ,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 |
注意点《1》
super 在构造方法中不能调用显示两次
注意点《2》
super 在构造方法中必须是第一条语句,因为是先初始化父类
注意点《3》
super 与 this 所调用的构造方法不能同时存在,原因还是只能显示调用一个构造方法
总结
若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的 super() 调用,即调用基类构造方法 |
如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败 |
在子类构造方法中,super 调用父类构造时,必须是子类构造函数中第一条语句,且只能在子类构造方法中出现一次 |
运用场景二:同名成员访问父类
class Base {
protected int a;
protected int b;
public void methodB(int n){
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
private int a;
private int b;
public Derived(){
this.a = 100;
this.b = 200;
super.a = 10;
super.b= 20;
}
public void methodB(){
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("Derived中的methodB()");
}
}
class TestDerived{
public static void main(String[] args) {
Derived obj = new Derived();
obj.methodB();
System.out.println("===================");
obj.methodB(10);
}
}
这里稍微解释一下,我们创建对象先初始化父类,然后走 Derived 的构造方法,分别初始化了子类 Derived 、Base ,因为父类和子类的方法同名构成重载,所以我们要根据参数来判断是调子类方法还是父类方法 -- 无参的是子类 Derived ,有参的是父类 Base |
supper 与 this 很相似: supper 只能在非静态方法中使用,且必须在子类中才能调用父类的成员变量或者方法。这里可以拿 this 的性质解释,因为 supper 像是父类的对象的引用,非静态方法不依赖对象所以用不了,而只有在子类中才继承有父类的引用 ~
supper 与 this 的同异
问:super 和 this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点
都是 Java 中的关键字 |
只能在类的非静态方法中使用,用来访问非静态成员方法和字段 |
在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在 |
不同点
this 是当前对象的引用,当前对象即调用实例方法的对象,super 相当于是子类对象中从父类继承下来部分成员的引用 |
在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属 |
在构造方法中:this(.) 用于调用本类构造方法,super(.) 用于调用父类构造方法,两种调用不能同时在构造方法中出现 |
继承子类的构造方法中一定会存在 super(.) 的调用,用户没有写编译器也会增加,但是 this(.) 用户不写则没有(原因是子类的构造方法必须要有 super ,且与 this 不能同时存在。这样就已经有构造方法了,系统就不会生成默认无参构造。所以 this 不写则没有) |
类中方法和代码块优先级
我们可以根据以下代码来推断:
public class Person {
protected String name = "张三";
protected int age = 18;
protected int id = 2024125;
static{
System.out.println("父类的静态代码块");
}
{
System.out.println("父类的 构造代码块/实例化代码块");
}
public Person(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
System.out.println("父类有参的构造方法");
}
public Person() {
this.name = "王五";
this.age = 20;
this.id = 2024567;
System.out.println("父类无参的构造方法");
}
public void PrintPerson(){
System.out.println("name: "+name+" age: "+age+" id: "+id);
}
}
//
public class Student extends Person {
static{
System.out.println("子类的静态代码块");
}
{
System.out.println("子类的 构造代码块/实例化代码块");
}
public Student() {
System.out.println("子类的无参构造方法");
}
public void behavior(){
System.out.println("上学");
}
}
//
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
}
}
我们可以发现无论是什么代码块和方法都是父类在子类前面
所以可以得出,我们在创建子类对象的时候必须先初始化父类成员,然后再初始化子类成员
运行顺序为:
父类静态代码块优先于子类静态代码块执行,且是最早执行 |
父类实例代码块和父类构造方法紧接着执行 |
子类的实例代码块和子类构造方法紧接着再执行 |
第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行 |
protected 关键字
我们还是以这个代码为例 ~
public class Person {
private String name;
private int age;
private int id;
public void PrintPerson(){
System.out.println("name: "+name+" age: "+age+" id: "+id);
}
public Person(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
public Person(){
this.name = "张三";
this.age = 18;
this.id = 20241036;
}
}
//
public class Student extends Person {
public int a;
public Student() {
super("李四",19,2024567);
}
public void behavior(){
System.out.println("上学");
}
}
//
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.PrintPerson();
}
}
那么父类的成员变量在保证封装的前提下,怎么在子类中访问呢?
这个时候就需要运用到我们的修饰符 -- protected(保护)
protected String name = "张三";
这个时候就不会有报错了,那么非继承的类能使用 Person 的属性吗?--- 答案是不行的
问:用 protected 修饰的父类成员在子类中能访问说明继承了这些属性,那么用 private 修饰的成员是否也继承了这些属性呢?
根据调试窗口我们可以很明确的知道,在 private 修饰下,在子类中虽然不能直接访问但是确实继承下来了
不同访问修饰符对继承的影响
问:那父类中不同访问权限的成员,在子类中的可见性又是什么样子的呢?
在同个包下
public class B {
private int a;
protected int b;
public int c;
int d;
}
//
public class C extends B {
public void method(){
// super.a = 10;
// 编译报错,父类中private成员在不同包子类中不可见
--------------------------------------------------------
super.b = 20;
// 父类中protected修饰的成员在不同包子类中可以直接访问
--------------------------------------------------------
super.c = 30;
// 父类中public修饰的成员在不同包子类中可以直接访问,但是会影响类的封装
--------------------------------------------------------
// 父类中没有修饰的成员在不同包子类中可以直接访问
super.d = 40;
}
}
在不同包下
public class D extends B {
public void method(){
// super.a = 10;
// 编译报错,父类 private 成员在相同包子类中不可见
super.b = 20;
super.c = 30;
//super.d = 40;
// 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
}
}
public class TestC {
public static void main(String[] args) {
C c = new C();
c.method();
// System.out.println(c.a);
// 编译报错,父类中private成员在不同包其他类中不可见
--------------------------------------------------------------------
// System.out.println(c.b);
// 父类中protected成员在不同包其他类中不能直接访问
System.out.println(c.c);
}
}
总结:
在现实开发中,我们希望类要尽量做到 "封装", 即隐藏内部实现细节,只暴露出必要的信息给类的调用者。因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用 private,就尽量不要用 public。在继承中父类的 private 则可以替换成 protected 以此来扩充子类的可操作性
final 关键字
如果一个程序员写的一个类不想给其他人继承,我们则可以使用 final 关键字,它的作用就是:用 final 修饰的类就不能被继承。当然 final 的作用不仅限于此:
final 修饰的变量或者字段,表示常量(不能被修改,跟 C 的 count 类似)
final 修饰的方法,不能被重写(关于重写本篇并不涉及,这是多态的内容)
final 修饰变量
final int a = 10;
a = 20;
final 修饰变量或字段:表示常量(即不能修改)
final 修饰类
public final class B {}
---------------------------------
public class C extends B {}
final 修饰类:表示此类不能被继承
Java 的继承方式
Java 中只支持以下几种继承方式:
注意:Java中不支持多继承时刻牢记, 我们写的类是现实事物的抽象。而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到 一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多。类之间的关系也会更加复杂 ~但是即使如此, 我们并不希望类之间的继承层次太复杂,一般我们不希望出现超过三层的继承关系,如果继承层次太多, 就需要考虑对代码进行重构了如果想从语法上进行限制继承, 就可以使用 final 关键字
继承与组合
继承
继承(Inheritance)是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系;继承是一种 is-a 关系:
组合
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法 (诸如 extends 这样的关键字),仅仅是将一个类的实例作为另外一个类的字段组合(Composition)体现的是整体与部分、拥有的关系,即 has-a 的关系:
public class Person {
private int a;
public Person(int n) {
this.a = n;
}
}
----------------------------------------------
public class Student {
private int b;
private Person p;
public Student() {
this.b = 10;
this.p= new Person(20);
}
}
----------------------------------------------
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
}
}
我们可以看到组合中依旧可以访问其他类的成员
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择
一般建议:能用组合尽量用组合