第十四讲 深入了解继承

第十四讲 深入了解继承

1 构造方法的调用

public class Animal 
{
	private String name;
	private String type;
	
	public Animal() {
		System.out.println("父类的无参构造方法!");
	}
}


class Dog extends Animal
{
	public Dog() {
         Animal(); // 这个方法是隐含的,但肯定在这里出现,一定是出现在Dog()这个方法的第一句之前。也就是说肯定是最先被调用的父类的无参构造。
		System.out.println("Dog的无参构造!");
	}
}

class Test {
	public static void main(String[] args) {
		new Dog(); // 打印结果是什么?
		// 父类的无参构造方法!
		// Dog的无参构造!
	}
}
  • 上述代码中,我们在main方法中没有直接调用父类的无参构造,为什么会执行父类的无参构造?
    • 一个很简单的事实:要有孩子,首先要有父亲。
    • 那么父类的构造是怎么被调用的?很简单,隐式的调用。我们根本看不见这个方法在哪里,他就被执行了。那么,它一定在子类的构造方法中被调用了,这个毫无疑问。
    • 我们可以推断,在Dog的无参构造中,有一个隐含的父类Animal的无参构造。它一定是Dog无参构造中的第一条语句。
public class Animal 
{
	private String name;
	private String type;
	
	public Animal() {
		System.out.println("父类的无参构造方法!");
	}
	public Animal(String name, String type) {
		this.name = name;
		this.type = type;
		System.out.println("父类的有参构造执行!!");
	}
}


class Dog extends Animal
{
	public Dog() {
		System.out.println("Dog的无参构造!");
	}
	
	public Dog(String name, String type) {
		System.out.println("子类的有参构造执行!!");
	}
}

class Test {
	public static void main(String[] args) {
		//new Dog(); // 打印结果是什么?
		// 父类的无参构造方法!
		// Dog的无参构造!

		new Dog("faf", "fewaw");
		// 父类的无参构造方法!
		// 子类的有参构造执行!!
	}
}
  • 上述代码中,我们给父类和子类都定义了有参构造。我们在main方法中调用子类的有参构造,得到的结果是:
    • 先执行了父类的无参构造。
    • 然后再执行了子类的有参构造。
    • 我们一样得出一个结论,在调用子类有参构造的时候,父类的无参构造在子类的有参构造中,且是最先被执行的。
  • 结论:我们可以得出子类构造方法的一个原型。
// subclass 子类 派生类derive 扩展类
// superclass 父类 基类(base)
class SuperClass {}
class SubClass extends SuperClass{
    public SubClass() {
        SuperClass();
        ....
    }
    public SubClass(args_list) {
        SuperClass();
        ......
    }
}
  • 我们在父类中定义了一个有参的构造,那么系统不会给我们默认的无参构造。我们不在父类中定义无参构造,所的结果:

public class Animal 
{
	private String name;
	private String type;
/*	
	public Animal() {
		System.out.println("父类的无参构造方法!");
	}
*/
	public Animal(String name, String type) {
		this.name = name;
		this.type = type;
		System.out.println("父类的有参构造执行!!");
	}
}


class Dog extends Animal
{
	public Dog() {
		System.out.println("Dog的无参构造!");
	}

	public Dog(String name, String type) {
		System.out.println("子类的有参构造执行!!");
	}
}

class Test {
	public static void main(String[] args) {
		new Dog();
	}
}

上述代码情况是这样的:
    父类没有定义无参构造,只定义了有参构造,那么父类中是不存在无参构造的。
    子类中定义了有参无参两个构造方法。
    我们在测试程序中调用子类的无参构造。
    编译器报以下错误:无法将类 Animal中的构造器 Animal应用到给定类型;
	这个错误是什么意思:意思是,子类中的无参构造方法需要父类有对应的构造方法。
        也就是说,父类没有无参,子类有无参,那么编译不通过。
        这说明,子类构造中一定有一个隐含的父类的构造。
        经过试验证明,这个父类的构造方法是无参的,隐式的存在。
        
/*
Animal.java:21: 错误: 无法将类 Animal中的构造器 Animal()到给定类型;
        public Dog() {
                     ^
Animal();它在该构造方法中 在父类中永远找不到的  找到:    没有参数
Animal(String, String);它不在构造方法中 在父类中可以找到的,  需要: String,String
  需要: String,String
  找到:    没有参数
  原因: 实际参数列表和形式参数列表长度不同
*/

如果父类和子类中都只有有参,那么编译器依然会报错。
报错如下:

E:\java基础\day07-1>javac Animal.java
Animal.java:26: 错误: 无法将类 Animal中的构造器 Animal应用到给定类型;
        public Dog(String name, String type) {  
         			 Animal(String, String); 在父类中可以找到的
                      Animal();               在父类中永远找不到的
  需要: String,String (在父类的构造其中,有一个是有两个参数的构造)
  找到:    没有参数 (是指在子类的构造方法中,有一个父类的构造器,是无参数的,但父类中不存在。)
  原因: 实际参数列表和形式参数列表长度不同
  • 上述的编译错误告诉我们一个事实,子类中的构造器中一定存在父类无参构造(只不过被隐藏了而已)

  • 重要的结论

    • 在继承中,父类的无参构造一定要存在(默认的存在,或者是显式的存在,最好是显式的定义),否则子类编译都通不过。

2 找到子类构造器中的那个隐藏的父类无参构造

public class Test01 
{
	public static void main(String[] args) 
	{
		new Student();
		System.out.println("Hello World!");
	}
}


class Person
{
	private String name;
	public Person() {
		System.out.println("11111111");
	}
	public Person(String name) {}
}

class Student extends Person
{
	public Student() {
		super();
        // 使用super() 和 不使用super()所得效果一样
        // 这也证明了,确确实实在子类的构造中存在父类的无参构造。
        // 只不过这个构造器的名字不是父类名(),而是super().
		System.out.println("22222222222");
	}
}
  • 通过实验,我们看到了,super()确实存在于子类的构造方法中,且是第一行。
  • 如果super()不是在第一行,那么无法解释打印的结果先输出父类无参构造中的内容。
  • 如果super()不是在第一行,编译器报错:错误: 对super的调用必须是构造器中的第一个语句
  • super()可以省略,也可以不省略。
  • 以上我们所有的纠结的来源都是因为省略了super()

3 认识super

  • super代表了什么?

    super()代表着父类的无参构造器,这么说不妥。如果它代表了父类的无参构造器,说明了什么问题?说明父类的无参构造器就在子类中了,这也说明父类的无参构造器被子类继承了。

    我们说过,继承是is-a的关系。一旦继承过来之后,就归子类所有了。但是我们上节课已经验证了,子类并没有继承父类的构造方法,如果继承了就乱套了。

  • 要想了解super到底代表什么,我们就一定要先知道继承在java中到底是什么含义?

    子类继承父类,继承了父类中的属性和方法(private除外,构造方法除外),私有的属性也继承过来了,只不过不能直接访问。可以通过其他的方式访问。

    那么实际上,我们可以理解为,子类继承父类,能够继承的东西,子类都拥有一份。要不然子类怎么去调用父类中的方法呢?那么也就可以理解为子类复制了一份父类中的代码,归子类所有了。这就是is-a的含义。比如说我继承了我父亲的鼻子、眼睛、嘴巴,这些是不是就是我的,就长在我身上的。

    • super()不是父类的构造,只代表通过super关键字,构造了父类的特征。
    • super()的用法:
      • 在构造方法中只能出现一次,且是第一条语句
      • super()只能用在构造方法中,其他地方不行。
      • 错误: 对super的调用必须是==“构造器”==中的第一个语句
    • super:可以在成员方法中使用,可以通过super直接访问子类继承的父类的方法。静态的方法也是可以使用super访问的,但super不代表类名。
    • super代表的是父类的特征。这些特征归子类所有。
    • super是可以省略的,但是在一种情况下,不能省略:
      • 当父类和子类中拥有同样的方法时,要区分子类和父类中的方法,如果是子类的,this.方法(),这个this是可以省略的。如果是父类的方法,super.方法(),super不能省略。
      • 对于静态方法来讲,它是类级别的,与this、super无关。不能用this去掉静态的方法,因为静态方法中没有this这个参数。super是可以调用静态方法的,但是它只是代表了父类的名字,名字也是父类的一个特征。
      • 所谓特征,就是父类的属性、方法、类名,都是父类的特征。
      • 私有的被private修饰的方法,在类外是无法直接访问的。但是可以间接访问。
public class Test01 
{
	public static void main(String[] args) 
	{
		Student s = new Student("lsi");
		s.test();
	}
}

class Person
{
	private String name;
	public Person() {
		System.out.println("11111111");
	}
	public Person(String name) {
		this.name = name;
		System.out.println("父类有参" + this.name);
	}
	public void work() {
		System.out.println("父类,work!!");
		sleep();
	}
	public static void eat() {
		System.out.println("eat");
	}
	private void sleep() {
		System.out.println("sleep!!!");
	}
}

class Student extends Person
{
	public Student() {
		super();
		System.out.println("22222222222");
		//super();//错误: 对super的调用必须是构造器中的第一个语句
	}

	public Student(String name) {
		super(name);//错误: 对super的调用必须是构造器中的第一个语句
		System.out.println("子类有参");
	}
	public void test() {
		// work();
		// 这个work方法是谁的?子类的,为什么是子类的?this
		// 我现在不想调用子类的work()。我只想调用父类的work()。
		super.work(); // 父类,work!!
	}
	public void work() {
		System.out.println("子类 work!");
	}
}
  • 补充
    • this() 也有这种语法格式,它代表的是当前的构造方法,只能在构造方法中用。它的作用是一个构造方法调用另一个构造的时候使用。
public class User 
{

	private int id;
	private String name;

	public User() {
        super();
		// this(0, "用户");// 这样调用会很常见,尤其是在源码中。
		this.name = "小乖乖";
	}

	public User(int id, String name) {
		// this(); //这样调用意义不大
		this.id = id;
		this.name = name;
	}
	public static void main(String[] args) 
	{
		User user = new User(100,"zhangsan");
		System.out.println(user);
	}

	public String toString() {
		return "id= " + id + ", " + "name= " + name;
	}
}

4 Object类

  • Object类是老祖宗类,它是所有类的基类,也就是说它是所有类的父类。java中只支持单继承,这就造就了Object类是最高层的父类。
  • 如果一个没有继承任何类,它默认继承Object。如果它继承了其他的类,它的父类、父类的父类,父类的父类的父类,总有一个是源头,但这个源头的父类是Object。
  • 继承Object类的语法是extends Object,这个省略了。
public class User
{

	private int id;
	private String name;

	public User() {
		super();
	}

	public User(int id, String name) {
		
		this.id = id;
		this.name = name;
	}
	public static void main(String[] args) 
	{
		User user = new User(100,"zhangsan");
		System.out.println(user);
	}

	public String toString() {
		return "id= " + id + ", " + "name= " + name;
	}
}
这段代码编译不会报错,而且能够正确执行。
    那么就说明User类有一个父类,这个父类也是隐含的,他就是Object。
    为什么说它的父类是Object?
    
public class User
{

	private int id;
	private String name;

	public User() {
		super();
	}

	public User(int id, String name) {
		
		this.id = id;
		this.name = name;
	}
	public static void main(String[] args) 
	{
		User user = new User(100,"zhangsan");
		System.out.println(user.toString());//User@2ff4acd0
	}
}

user直接调用了toString()方法,这个方法从哪里来?一定是从它的父类中来的。否则,我们没有定义过这个方法。但是user这个引用直接调用了,说明这个方法在别的地方定义了,而且user能访问。只有一种可能,那么就是user从它父亲拿来的。
    
 Object.java源码中的toString()方法:
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
 * Class {@code Object} is the root of the class hierarchy.
 * Every class has {@code Object} as a superclass. 
 这句话的意思是,Object是类继承的根,所有的类都有一个超类叫做Object
 也就是说,如果一个类没有继承任何类,它的父类是Object
 如果一个类继承了其他的某个类,那么它的父类没有显式继承任何类,它的父类就继承了Object类。因为继承是可以传递的。那么,Object类也是这个子类的超类。只不过Object是该子类的祖父而已。也就是说,这个子类拥有Object类的所有特征。是因为这个子类的父类继承了Object,所以父类拥有了Object类所有的特征,子类拥有父类所有的特征,也就拥有了Object类所有的特征。
 
public class User
{

	private int id;
	private String name;
	static Man man;

	public User() {
		super();
	}

	public User(int id, String name) {
		
		this.id = id;
		this.name = name;
	}
	public static void main(String[] args) 
	{
		//User user = new User(100,"zhangsan");
		Man m = new Man();
		User.man = m;
		User.man.buy();
		//System.out.println();
		// System.out.println(user);//User@2ff4acd0
		// 打印引用,没有给toString()方法,自己直接调用了toString()
		// 怎么做到的呢?System.out.println(user)
		// System.out.println()这种用法表示什么意思?你从这里面读到了什么内容?
		// System.out:这种语法是不是就是"类名."的方式,
		// 说明,out是System类中静态的属性的引用。out这个引用一定有一个类型
		// 这个类型的类中一定有一个println()方法。这个方式是重载过的。
	}
}
源码中调用的过程:

在这里插入图片描述

5 方法的重写

  • 方法的重写,override

  • 方法的重写发生的条件:

    • 父类中的方法无法满足子类中的业务需求,这时候我们需要对父类的方法升级,我们可以重新书写父类的方法中的业务逻辑
    • 重写:
      • 方法名一定要相同(方法名不同就是一个新方法了!)
      • 方法的参数列表一定要相同(参数列表不同,就是重载了,也是新方法)
      • 方法的参数位置一定要相同(也是重载)
      • 方法的返回值类型一定要相同(有继承关系的引用进行自动类型转换不会报错。但用得不多。)
      • 方法的权限,重写只能更高,不能更低,也就是说public修饰的方法,重写的时候不能低于public权限,否则报错。
      • private修饰的方法不能被重写
      • 重写后的方法抛出的异常不能更多,只能更少
      • 最最重要的一条,一定要有继承关系,才有重写的概念。没有继承关系,重写方法没有意义。也就是说A类和B类没有半毛钱关系,它们当中的方法也没有半毛钱的关系。
  • 总结:

    关于重写,或者叫做覆盖,一定要是有继承关系的两个类。
    重写的时候,最好是原封不动的将父类中要被重写的方法copy过来。建议不要自己写。
    		Dog dog = new Dog();
    		dog.eat();//我们不在Dog中重写eat()方法
    		// 这里调用的是父类的eat方法,只会打印动物吃东西。
    		// 满足不了我们的业务需求,我们就希望小狗吃骨头
    		// 所以,我们在Dog类中重新写了Animal类中的eat()方法
    		Cat cat = new Cat();
    		cat.eat();
    
    • 重写方法的好处
    有了重写,才有了面向对象中最最最牛X的特性:多态!!!!!!!!!!!
    
    • 多态有什么好处
    OCP: open close principle
    23种设计模式中的七大原则中著名的开闭原则
    开:对扩展开放
    闭:对修改关闭
    意思就是:在扩展功能的,增加这功能的时候,支持程序扩展新的功能,但是不要去更改程序中原有的代码
    因为:原有的代码已经能够正常的运转,如果你增加功能的时候,修改了原有的代码逻辑,在原有的代码基础上增加了很多代码,就有可能造成更多的bug。这样的话破坏了软件的稳定性。扩展功能,最好的方式是,要求不更改原有的代码,但又能够扩展功能。这是开闭原则。
    
    • 继承有什么缺点
      • 如果父类中的代码逻辑发生了改变,程序员修改了父类中的代码。子类中的代码逻辑是不是也要相应的修改。
      • 比如我们在Animal类中修改了eat()方法,那么在Animal所有的子类中,重写了该方法的都需要修改。
      • 这样做修改的地方比较多,工作量大,代码逻辑发生改变。
      • 这就是高耦合。
      • 怎么避免呢?未来会说,有一个东西叫做接口。确实要少用继承。

6 多态

  • 父类型引用指向子类型对象
Animal a = new Dog();这也叫做自动类型转换。
  • 这么做为什么可以?
  • 子类型的引用指向父类型的对象,为什么不可以。

在这里插入图片描述

public class Animal 
{
	private String name;
	private String type;

	public Animal(){}

	public Animal(String name, String type) {
		this.name = name;
		this.type = type;
	}

	public void eat() {
		System.out.println("动物要吃东西!");
	}
}



public class Cat extends Animal {
	public void eat() { //Cat中的eat()无法覆盖Animal中的eat()
		System.out.println("小猫吃鱼");
		
	}
}



public class Dog extends Animal {
	private int id;
	public Dog() {
		super();
		this.id = 0;
		
	}
	public void eat() {
		System.out.println("小狗吃骨头");
	}

	public void gateGard() {
		System.out.println("小狗看门!!!");
	}
}

public class ZooKeeper {
/*
	public void feed(Dog dog) {
		System.out.println("管理员正在给 " + dog + " 喂食");
		dog.eat();
	}
	public void feed(Cat cat) {
		System.out.println("管理员正在给 " + cat + " 喂食");
		cat.eat();
	}
*/
	public void feed(Animal animal) {
		System.out.println("管理员正在给 " + animal + " 喂食");
		animal.eat();
	}
}

public class Client
{
	public static void main(String[] args) {
		
		Animal a = new Animal();
		lr.feed(a);
		Animal animal = new Dog(); 
		lr.feed(animal);
		Animal cat1 = new Cat();
		lr.feed(cat1);
	}
/*
		管理员正在给 Dog@816f27d 喂食
		小狗吃骨头
		管理员正在给 Cat@3e3abc88 喂食
		小猫吃鱼
*/
}
  • 增加Tiger类
public class Tiger extends Animal
{
	public void eat() {
		System.out.println("老虎吃大象!");
	}
}


// 只要在客户端增加一个功能
public class Client
{
	public static void main(String[] args) {
		Animal animal = new Dog(); 
		lr.feed(animal);
		Animal cat1 = new Cat();
		Animal tiger1 = new Tiger();
		lr.feed(cat1);
		lr.feed(tiger1);	
	}
}
// 这就是开闭原则
public class Client
{
	public static void main(String[] args) {
		Animal a = new Animal();
		lr.feed(a); // 这里调用的是Animal中的eat方法
		Animal animal = new Dog(); //这就是多态 向上转型
		lr.feed(animal);
		Animal cat1 = new Cat();
		Animal tiger1 = new Tiger();
		// 这个叫做父类型的引用指向子类型的对象,这种就是多态。
		// Cat cat2 = new Animal();//?
		lr.feed(cat1);
		lr.feed(tiger1);
		Object obj = new Dog();
		//obj.eat();// 可以吗?obj中就没有eat()这个特性,这个特性是它的子类的。
		// 所以它没办法去调这个方法。子类比父类要强大。父类的引用只能调自己的方法
		// 多态就是围绕父类的方法展开,但是父类的方法能力有限,所以有了重写。
		// 子类有权力在自己的类中重写父类的老旧的方法。以满足自己的需求。
		// 儿子继承了父亲的公司,父亲公司的管理方法老旧,不能满足儿子的需求
		// 儿子可以在父亲的管理方法基础之上进行改革。父亲只会自己的那一套,
		// 因为父亲跟不上时代了,老了,跟不上儿子的步伐,只能用自己的一套。

		// Animal类中没有gateGard() 方法,所以Animal animal = new Dog();
		// 使用animal永远没有办法调到gateGard()方法。怎么办呢?
		Dog haBaGou = (Dog) animal;// 强制类型转换 这也叫作向下转型。
		haBaGou.gateGard();
		// 向上、向下转,一定发生在继承关系中
	}
}
  • 类型转换异常:这是一个著名的以后开发中常见的异常
Exception in thread "main" java.lang.ClassCastException:
public class ZooKeeper {
	public void feed(Animal animal) {
		System.out.println("管理员正在给 " + animal + " 喂食");
		Dog dog = (Dog) animal;
		// 编译时是通过的,运行时抛出异常ClassCastException
		dog.eat();
	}
}
  • 怎么避免类型转换异常:instanceof
语法:
    (引用 instanceof 类型) 结果为boolean
为什么要强制类型转换,我们已经说过了,就是要用多态机制,同时子类对象又要访问自己特有的属性和方法的时候,就要使用强制类型转换。
    强制类型转换的前提是,要有继承关系。
    
public class ZooKeeper {

	public void feed(Animal animal) {
		System.out.println("管理员正在给 " + animal + " 喂食");
// 这里是解决ClassCastException的办法
		if(animal instanceof Dog) {
			Dog dog = (Dog) animal;
			dog.eat();
			dog.gateGard();
		} else if(animal instanceof Cat) {
			Cat cat = (Cat) animal;
			cat.eat();
			cat.catchMouse();
		} else {
			animal.eat();
		}
		
//		Dog dog = (Dog) animal;
		// 编译时是通过的,运行时抛出异常ClassCastException
		
//		dog.eat();
	}
}


  • 到底什么是多态:
涉及两个概念,静态绑定与动态绑定
静态绑定:在编译的时候就会进行一些关联。Animal是父类,Dog是子类,Animal a = new Dog();
它在编译的时候,是这样的,编译器会判断Animal和Dog是否有继承关系,且左边是父类的引用,右边是子类的对象。语法上是通过的,编译器也认为没有问题。
这时候,编译器会将Animal中的方法与Animal a绑定起来。
如果父类的引用有调用子类特有的方法,编译器就会报错。

在这里插入图片描述

  • 动态绑定

  • 运行的时候java允许动态的关联,也就是说,a.eat(),如果子类重写了这个方法,那么运行时,父类型的引用所关联的这个eat()方法,实际上子类重写的那一个。这叫做动态绑定。

  • 多态就是由静态绑定和动态绑定组成的。编译时静态绑定,运行时动态绑定。这使得一个方法有多种形态,编译时是一种形态,运行时又是另外一种形态。这就叫做多态!!!

  • 运行时,父类型的引用会先去子类中找重写的方法,如果没有,就会去到父类中找。父类中也没有,就会更上一层的父类中找。不可能找不到。

      	cat.eat();
      	cat.catchMouse();
      } else {
      	animal.eat();
      }
    

// Dog dog = (Dog) animal;
// 编译时是通过的,运行时抛出异常ClassCastException

// dog.eat();
}
}


+ 到底什么是多态:

涉及两个概念,静态绑定与动态绑定
静态绑定:在编译的时候就会进行一些关联。Animal是父类,Dog是子类,Animal a = new Dog();
它在编译的时候,是这样的,编译器会判断Animal和Dog是否有继承关系,且左边是父类的引用,右边是子类的对象。语法上是通过的,编译器也认为没有问题。
这时候,编译器会将Animal中的方法与Animal a绑定起来。
如果父类的引用有调用子类特有的方法,编译器就会报错。


[外链图片转存中...(img-nCVJivpp-1611644404302)]



+ 动态绑定
+ 运行的时候java允许动态的关联,也就是说,a.eat(),如果子类重写了这个方法,那么运行时,父类型的引用所关联的这个eat()方法,实际上子类重写的那一个。这叫做动态绑定。
+ 多态就是由静态绑定和动态绑定组成的。编译时静态绑定,运行时动态绑定。这使得一个方法有多种形态,编译时是一种形态,运行时又是另外一种形态。这就叫做多态!!!

+ 运行时,父类型的引用会先去子类中找重写的方法,如果没有,就会去到父类中找。父类中也没有,就会更上一层的父类中找。不可能找不到。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210126150304628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NvbG9famll,size_16,color_FFFFFF,t_70#pic_center)

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值