第十四讲 深入了解继承
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()方法,实际上子类重写的那一个。这叫做动态绑定。
+ 多态就是由静态绑定和动态绑定组成的。编译时静态绑定,运行时动态绑定。这使得一个方法有多种形态,编译时是一种形态,运行时又是另外一种形态。这就叫做多态!!!
+ 运行时,父类型的引用会先去子类中找重写的方法,如果没有,就会去到父类中找。父类中也没有,就会更上一层的父类中找。不可能找不到。
