一篇博客带你了解什么是封装、继承和多态
文章目录
- 面向对象编程的三大板块
-
- 封装
- 继承
-
- 继承的规则
-
- 多重继承
- 多层继承
- 隐式继承
- 关于pretected权限
- super关键字
-
- 修饰属性
- 修饰构造方法
- 修饰普通方法
- final关键字
-
- 修饰属性
- 修饰类
- 组合
- 多态
-
- 向上转型
- 方法重写
- 向上转型三种的方法
- 构造方法与多态的结合
- 方法的调用
- 向下转型
- 抽象类
-
- 抽象类和抽象方法的定义
- 接口
-
- 接口表示规范
- 接口表示能力
面向对象编程的三大板块
封装
使用private
将属性或方法进行封装(表示这个属性或方法只在当前类的内部可见,对外部隐藏,想要访问这个属性或方法,只能通过间接方法访问)
封装是为了保护性和易用性(通过程序对外提供的方法来操作属性)
继承
先来看一段代码:
package animal;public class Animal { public String name; public void eat(String food){ System.out.println(this.name + "正在吃" + food); }}
package animal;public class Dog { public String name; public void eat(String food){ System.out.println(this.name + "正在吃" + food); }}
package animal;public class Test { public static void main(String[] args) { Animal animal = new Animal(); animal.name = "animal"; animal.eat("food"); Dog dog = new Dog(); dog.name = "qiqi"; dog.eat("狗粮"); }}//执行结果animal正在吃foodqiqi正在吃狗粮
我们分别定义了这三个类
但是我们可以发现Animal
和Dog
这两个类中定义的属性和方法是完全一致的
按照道理来说,所有的Animal的类都应该具备name
属性和eat
这个方法
狗是动物dog is an Animal
、猫是动物cat is an Animal
当类和类之间满足了上面这样的一个类 is a 另一个类
,那么一定存在着继承关系,就像猫狗都是动物一样,是一种天然的继承关系,而不是我们强行赋予的关系
因此这里的猫、狗都是动物,包括人类也是一个动物
而当一个类继承了另一个类,另一个类中的所有的属性和方法在子类中就天然具备了
Java中使用extends
表示类的继承
package animal;public class Animal { public String name; public void eat(String food){ System.out.println(this.name + "正在吃" + food); }}
package animal;public class Dog extends Animal{}
package animal;public class Test { public static void main(String[] args) { Animal animal = new Animal(); animal.name = "animal"; animal.eat("food"); Dog dog = new Dog(); dog.name = "qiqi"; dog.eat("狗粮"); }}//执行结果animal正在吃foodqiqi正在吃狗粮
和开始展示的代码的执行结果完全一致
这么写减少了我们不少麻烦,但也不能为了省略代码就随意使用继承,要想使用继承必须满足is a 关系
比如Person extends Dog
人类和狗的某些属性虽然相同,但是人类不是狗,相反狗也不是人类,因此这两者之间不存在继承关系
所以说不满足 is a 关系的类,千万不能使用继承
继承的规则
- 要能使用继承,前提必须满足类之间的 is a 关系
- 一个子类只能使用extends继承一个父类(单继承),Java中不允许多重继承,extends后面只能跟一个父类,不过允许多层继承,就好比说没法当儿子,还能当孙子
- 子类会继承父类的所有属性和方法,显示继承(public属性和方法可以直接使用),隐式继承(private属性和方法虽然被子类继承,但无法直接使用)
多重继承
多层继承
这里的多层继承又称为继承“树”,满足继承关系的类之间一定是逻辑上垂直的关系,比如泰迪既是狗的子类,也是Animal的子类
隐式继承
关于隐式继承,先看一段代码:
package animal;public class Animal { public String name; private int age;//私有属性 public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
package animal;public class Dog extends Animal{}
package animal;public class Test { public static void main(String[] args) { Dog dog = new Dog(); dog.setAge(10);//通过父类给定的方法,对继承的私有属性进行操作 System.out.println(dog.getAge()); }}//执行结果 10
对于Animal
类中的private int age
,子类在继承的时候,这个私有属性也被继承了
假设没有被继承,Dog
类中就没有age
这个属性,那么对象dog
也就无法进行getAge
和setAge
操作,因为age都不存在,程序编译的时候就会出错
但是从上面的代码执行的结果来看,不仅程序编译通过,甚至还设置好了age
的值,由此可见,子类确实是继承了父类的私有属性
只不过父类的私有属性没法直接使用,所以被称为隐式继承,子类没法直接使用父类的私有属性,必须通过父类提供的方法来操作
关于静态属性和方法,他们都是属于类的,当子类继承父类的时候,毫无疑问静态属性和方法也都会继承,但是依然要区分其权限,如果是private的私有静态属性,子类还是只能通过方法来间接调用
package animal;public class Animal { public String name; private int age; static String test = "hello";}
package animal;public class Dog extends Animal{}
package animal;public class Test { public static void main(String[] args) { System.out.println(Dog.test); }}//执行结果 hello
如果代码中的test是静态私有属性,编译就会报错:test 在 Animal 中是 private访问控制
关于pretected权限
pretected作用域:只在当前类和同包中的不同类中以及不同包下的子类内部中是可以使用的
package animal;public class Animal { protected String name = "测试name";//当前类和其子类是可见的,没有继承关系且不在同个包的的类之间不可见 private int age;}
package person;import animal.Animal;public class Test { public static void main(String[] args) { Animal animal = new Animal(); System.out.println(animal.name); }}//编译不通过:name 在 animal.Animal 中是 protected 访问控制
上面的代码中的Test
不是Animal
的子类且不在一个包中,因此name
对于Test
来说是不可见的
package person;import animal.Animal;public class Person extends Animal { public void fun(){ System.out.println(name); }}public class Test { public static void main(String[] args) { Person person = new Person(); person.fun(); }}//执行结果:测试nama
这里的Person
继承了Animal
,那么父类Animal
中的name
对Person
来说是可见的
(考点)如果Test中这么写:
package person;public class Test { public static void main(String[] args) { Person per = new Person(); System.out.println(per.name); }}//编译不通过:name 在 animal.Animal 中是 protected 访问控制
理由:我们前面说过protected
可以让当前类和不同包下的子类内部可见,那么这里的Test
即不和Animal
同包,也没有继承,可以说是没有任何关系,因此对Animal
来说,name
这个属性在Test
下是不可见的
换言之,想要在不同包下调用protected
修饰的属性,那么就一定要在其子类下调用,否则没有权限访问
这也是为什么我们在Person
类下设置一个fun
方法,就是为了方便同包下的Test
调用fun
方法来间接访问在Test
中不可见的name
属性
protected
权限在同包中没有继承关系的类之间是如何?
pretected
的权限在同包中没有继承关系的类之间是可见的,既然protected
权限 > 包权限,那么包权限可见的范围,protected
一定可见
preotcted
相比于包访问权限,只不过多了一个能在不同包下的子类内部访问的权限,如果不同包,不是子类内部依然不能访问
总的来说:同包下的没有继承关系的类之间以及不同包的有继承关系的类之间可见的
一点点细节:
还有一点,在子类通过父类实例对象访问父类中protected权限的属性是有限制的
package interface_test.a;public class Base { protected String name = "base"; protected void show(){ System.out.println(name); }}package interface_test.b;import interface_test.a.Base;public class SubType extends Base { public void fun(){ System.out.println(name); System.out.println(this.name); System.out.println(super.name);//前三者均可通过编译,并输出base Base base = new Base(); System.out.println(base.name);//error,在子类中可以直接使用是没问题的,但是通过父类去直接访问是不行的 base.show();//error, 方法也不能通过父类实例访问 Type type = new Type(); System.out.println(type.name);//error,也不能通过其他子类访问 }}class Type extends Base {}
实际上,不仅仅是不能通过父类实例访问,还不能通过其他子类对象访问父类中的protected,总而言之,protected在不同包中的子类中只能通过子类实例访问
super关键字
回忆一下this
关键字:表示当前对象的引用
this
关键字可以修饰属性、修饰普通方法和构造方法、表示当前对象的引用
this
是到当前类中去寻找,而super
就是到父类去寻找
在将super
关键字之前,先要了解一个知识点:
要产生一个子类对象,首先要产生一个父类对象
代码示例:
package animal;public class Animal { protected String name = "测试name"; private int age; public Animal(){ System.out.println("Animal的无参构造"); }}
package person;import animal.Animal;public class Person extends Animal { public Person(){ System.out.println("Person的无参构造"); }}
package person;public class Test { public static void main(String[] args) { Person person = new Person(); }}//执行结果Animal的无参构造Person的无参构造
可见在创建Person的对象的时候,先是执行了父类的构造方法,然后再执行子类的构造方法
这是符合逻辑的,因为子类是继承父类的,也就是说属性和方法都来自于父类的,在子类中并没有真正定义过,因此想要输出子类的成员属性,首先要有父类的成员属性
所以,当调用子类的无参构造产生子类对象之前,先默认调用父类的构造方法产生父类对象
来看一道阿里的笔试题:下面代码的输出结果是什么?
package animal;public class B { public B(){ System.out.println("1.B的构造方法-----------"); } { System.out.println("2.B的构造块-------------"); } static{ System.out.println("3.B的静态方法块----------"); }}
package animal;public class D extends B{ public D(){ System.out.println("4.D的构造方法-----------"); } { System.out.println("5.D的构造块-------------"); } static{ System.out.println("6.D的静态代码块----------"); } public static void main(String[] args) { System.out.println("7.main开始-------------"); new D(); new D(); System.out.println("8.main结束-------------"); }}
执行结果:
3.B的静态方法块----------
6.D的静态代码块----------
7.main开始-------------
2.B的构造块-------------
1.B的构造方法-----------
5.D的构造块-------------
4.D的构造方法-----------
2.B的构造块-------------
1.B的构造方法-----------
5.D的构造块-------------
4.D的构造方法-----------
8.main结束-------------
解析:
由于主方法main
存在于子类D中,若要调用主方法,首先要加载主类
加载主类的时候静态代码块也会被加载,但是这里的主类D是继承了父类B的,因此在加载D之前会先加载父类B,因此3(父类的静态代码块)会优先于6(子类的静态代码块)加载
类加载结束后,进入主方法 7
此时将要产生子类的对象,不过在产生子类对象之前,还是先产生父类对象,而在类中定义的构造块 2 会优先于构造方法的主体 1 先执行
父类对象创建完成之后才真正到子类对象的创建,先执行构造块 5 ,然后是构造方法主体 4
接下来又是创建一个子类对象,重复上述两步操作 2 1 5 4
最后main方法结束 8
所以最终的结果就是:36 7 2154 2154 8
我们详细解释了创建子类对象前会先创建父类对象,接下来来看看super
有什么作用
修饰属性
表示直接从父类中去寻找同名属性
package supertest;public class Person { protected String name = "person";}
package supertest;public class China extends Person{ public String name = "china"; public void fun(){ //在访问成员变量的时候,建议写上this,尤其是有继承的时候,不容易混淆 System.out.println(name);//这里的name } public static void main(String[] args) { China china = new China(); china.fun(); }}//执行结果 china
上面代码中的fun()
功能里,打印的name
依然遵循就近匹配原则,会找最近的name
,而不是去找父类的name
,相当于this.name
如果子类中没有name
这个属性时:
package supertest;public class China extends Person{ public void fun(){ System.out.println(this.name); } public static void main(String[] args) { China china = new China(); china.fun(); }}//执行结果 person
当存在继承关系时,this
关键字默认先在当前类中寻找同名属性,若没找到,继续向上寻找
那么如果我们想要直接访问父类中的成员属性,就可以使用super
关键字 :
package supertest;public class China extends Person{ public String name = "china"; public void fun(){ System.out.println(super.name); } public static void main(String[] args) { China china = new China(); china.fun(); }}//执行结果 person
当然如果父类中的成员属性是private
的话,就没法用super
调用了
那如果父类也有一个父类的时候会怎么样?
package supertest;public class Animal { protected String name = "animal";}
package supertest;public class Person extends Animal{ protected String name = "person";}
package supertest;public class China extends Person{ public String name = "china"; public void fun(){ System.out.println(super.name); } public static void main(String[] args) { China china = new China(); china.fun(); }}//执行结果 person
由此可见super
是会寻找直接父类的
如果直接父类中没有要寻找的成员属性又会如何呢?
public class Animal { protected String name = "animal";}
public class Person extends Animal{ protected String name = "person";}
public class China extends Person{ public String name = "china"; public void fun(){ System.out.println(super.name); } public static void main(String[] args) { China china = new China(); china.fun(); }}//执行结果 animal
相信聪明的你已经想到了:super
会先从直接父类中寻找同名属性,若不存在就继续向上寻找
这里又有一个细节:如果在直接父类中的成员属性是私有的,super会找到直接父类中的成员属性,只不过没有权限访问,并且因为super
找到了父类属性,于是不会再往上找了
public class Person extends Animal{ private String name = "person";}public class China extends Person{ String name = "Chinese"; public void fun(){ System.out.println(super.name); } public static void main(String[] args){ new Person().fun(); }}
此时如果在子类China
中调用super.name
的时候,程序直接报错:name 在 supertest.Person 中是 private 访问控制
修饰构造方法
如果三层继承关系的类中都定义了构造方法,会怎么执行?
public class Animal { public Animal(){ System.out.println("Animal的无参构造"); } protected String name = "animal";}
public class Person extends Animal{ public Person(){ System.out.println("Person的无参构造"); } protected String name = "person";}
public class China extends Person{ public String name = "china"; public China(){ System.out.println("China的无参构造"); } public static void main(String[] args) { China china = new China(); }}//执行结果Animal的无参构造Person的无参构造China的无参构造
当产生子类对象时,默认先产生父类对象,若父类对象还有父类,继续向上,先产生祖父类的对象
如果子类的直接父类没有无参构造,那么在调用子类的无参构造时就会出错:
public class Person{ protected String name = "person"; public Person(String name){ this.name = name; System.out.println("Person的有参构造"); }}
public class China extends Person{ public String name = "china"; public China(){ System.out.println("China的无参构造"); } public static void main(String[] args) { China china = new China(); }}//执行结果java: 无法将类 supertest.Person中的构造器 Person应用到给定类型; 需要: java.lang.String 找到: 没有参数 原因: 实际参数列表和形式参数列表长度不同
也就是说,Person中定义了一个有参构造,于是无参构造就没有再产生,现在要创建一个子类China的对象,需要调用子类的无参方法,在调用子类的无参方法之前,又要去调用父类的无参构造时,因为没有无参构造,程序就出错了
实际上,在子类的构造方法的第一行中隐藏了一条语句:super()
但是因为父类中没有无参构造,因此我们不能省略super()
,并且还要传入参数
public class Person{ protected String name = "person"; public Person(String name){ this.name = name; System.out.println(this.name + "Person的有参构造"); }}
public class China extends Person{ public String name = "china"; public China(){ super("父类"); System.out.println("China的无参构造"); } public static void main(String[] args) { China china = new China(); }}//执行结果父类Person的有参构造China的无参构造
super修饰构造方法的语法:
super(父类构造方法的参数)
super();//直接父类的无参构造,可写可不写
若父类中不存在无参构造,则子类构造的首行必须使用super(有参构造)
,因为自动生成的super()
已经不适用了
但是在一个构造方法中无法同时显式使用this()
和super()
public China(String name){ super("父类"); this(); this.name = name; System.out.println("China的有参构造");}//编译不通过
因为this()
调用构造方法要放在首行,而super()
调用构造方法也要放在首行,互不兼容,必然会出错
但如果父类中存在无参构造,那么我们只写一个this()
调用当前类的无参构造是没有问题的,因为super()
调用无参构造是可以不写的
public China(String name){ //super();这里有一个隐藏的super调用的无参构造 this(); this.name = name; System.out.println("China的有参构造");}//编译不通过
修饰普通方法
和修饰属性一样,直接到父类中去寻找方法
public void test(){ super.fun();//直接从父类中调用fun方法}
当然,如果子类中没有定义fun
方法,而且在test
方法中直接调用fun()
,那么不加super
也能调用父类中的fun
方法
最后一点,和this关键字不同,super不能指代当前父类的对象
public void test(){ System.out.println(this); System.out.println(super);//error}
super
关键字类似于一种指示器,告知你到父类中去寻找,而不是引用父类对象
调用父类中的toString方法
public void test(){ System.out.println(this); super.toString();}//这两句话的执行结果是一致的
因为这个this
表示对当前对象的引用,如果有toString
方法就会调用toString
,如果没有就继续向上寻找,如果在父类中找到了就打印父类的toString
方法
final关键字
final一共可以修饰三个东西,这里暂且讲两个
修饰属性
表示属性值不能变,常量
修饰类
表示这个类无法被继承final class Person
-> 表示Person
无法被继承
组合
类和类之间除了继承关系,还有组合关系 has a
class School{ Student student; Teacher teacher; }
School has a student
School has a teacher
多态
一个引用可以表现出多种行为/特性 -> 多态性
讲多态之前,有必要先聊聊向上转型
向上转型
过去,我们创建对象是这样的:
Animal animal = new Animal();
Dog dog = new Dog();
实际上dog也是动物,因此也可以这么写:
Animal animal = new Dog();
这种子类 is a 父类的,是一种天然语义,就像狗肯定是一个动物,这是毫无疑问的
这种创建方式称为向上转型:父类名称 父类引用 = new 子类实例()
向上转型只发生在有继承关系的类之间,而且不一定是直接子类,也可以是孙子类
Animal animal = new Teddy();
Teddy is a dog, and the dog is a animal
向上转型最大的意义在于参数统一化,降低使用者的使用难度
package polymorphism;public class Test { public static void main(String[] args) { //2.类的使用者/程序的使用者 //如果没有向上转型,我要使用fun方法,就需要了解Animal及其子类的所有对象 //这样才能知道要调用那个fun方法 } //1.作为类的实现者: //现在我们需要一个方法来接收Animal及其子类的对象作为参数 //假设没有向上转型,那么我们应该重载多个方法,用于接收不同类型的方法 //这样的话Animal有多少个子类,我们就要重载多少次fun方法 //大自然中动物的种类有上百万中,我们就要重载上百万次代码 public static void fun(Animal animal){} public static void fun(Dog dog){} public static void fun(Teddy teddy){}}
这么写代码太麻烦了
那么既然子类有父类,为什么不能用父类去指代所有的子类呢?
因此就有了向上转型,而代码就可以写成这样:
public class Test { public static void main(String[] args) { fun(new Animal()); fun(new Dog()); fun(new Teddy()); } //只要是Animal及其子类,都是天然的Animal对象,都满足is a关系 //通过Animal最顶层的父类引用,指代所有的子类对象 //换言之,只要是子类参数,也可以用父类引用来接收 public static void fun(Animal animal){}}
有了向上转型之后,最顶层的父类引用就可以指代所有子类对象
当父类有了一个新的子类时,扩展也变的非常容易
比如现在多了一个类Cat,调用fun方法仅需要加一句fun(new Cat());
而已,无需再去重载一个方法来接收Cat类对象的引用
其次,多态的关键在于方法重写,因此先了解一下什么是方法重写
方法重写
先来看一组代码的执行结果:
//polymorphismpublic class Animal { public void eat(){ System.out.println("Animal 的eat方法"); }}public class Dog extends Animal{ public void eat(){ System.out.println("Dog 的eat方法"); }}public class Teddy extends Dog{ public void eat(){ System.out.println("Teddy 的eat方法"); }}public class Cat extends Animal{ public void eat(){ System.out.println("Cat 的eat方法"); }}public class Test { public static void main(String[] args) { fun(new Animal()); fun(new Dog()); fun(new Teddy()); fun(new Cat()); } public static void fun(Animal animal){ animal.eat(); }}//执行结果Animal 的eat方法Dog 的eat方法Teddy 的eat方法Cat 的eat方法
非常奇妙,虽然我们在各自的类中定义了eat
方法,但是fun
方法接收的参数是Animal
类的引用,按常理来说不应该调用的是Animal
类中的eat
方法吗?
实际上fun中animal
这个局部变量的引用调用eat
方法时,当传入不同类型的对象时,表现出来了不同的eat
方法,这就是我们前面说的多态性
也就是说,虽然看起来是同一个引用(变量名称),同一个方法名称,但却根据实际对象的不同,表现了出来了不同的行为
产生这种情况的本质就是方法重写:
回顾一下方法重载(overload):发生在同一个类中,定义了若干个方法名称相同,参数列表不同的一组方法
方法重写(override):发生在有继承关系的类之间,子类定义了和父类除了权限不同,其他全都相同的方法,这样的一组方法称之为方法重写/覆写
实际上方法重写的返回值是可以不同的,但是返回值至少是向上转型类的返回值,毫无关系的两种类型不能作为方法重写的返回值
eg : 父类中的返回值是int类型,而子类中是boolean类型,这是不行的
除了向上转型外,其他类型返回值严格要求相同
如果父类的返回值为Student,子类的返回值是Person,那就不行了,因为Person不一定是个Student,并且从父类中的Student返回值到子类的Person返回值属于是向下转型了
方法被调用时,到底是使用谁的方法呢?
我们不用去看前半部分,看当前是通过哪个类new的对象,若该类重写了相关方法,则调用的一定是重写后的方法
千万不要被类名称搞晕,就看new的是谁,只要new的这个对象的类中覆写了同名方法,则调用的一定是复写后的方法
若子类没有重写方法,则向上搜索,碰到第一个父类重写的方法就调用,也就是最“接近”的eat方法
方法权限:当发生重写时,子类权限必须 >= 父类权限才可以重写
在上面写过的代码中,重写代码的权限都是等于父类的方法权限,即public
此时如果我们修改一下代码:
public class Animal { protected void eat(){ System.out.println("Animal 的eat方法"); }}public class Teddy extends Dog{ void eat(){ System.out.println("Teddy 的eat方法"); }}//编译不通过polymorphism.Teddy中的eat()无法覆盖polymorphism.Dog中的eat() 正在尝试分配更低的访问权限; 以前为public
如果现在父类是private
权限,然后子类都是public
,这样是否可以?
答案是不可以,如果尝试编译,会报这样的错误:
eat() 在 polymorphism.Animal 中是 private 访问控制
如果我们把主方法定义在父类中,编译之后又会是什么结果呢?
可见调用的是父类的fun
方法
这样的结果其实是合理的,因为eat
方法在Animal
中属于私有方法,私有方法隐含着一个信息就是不能被重写,虽然子类上看上去是重写了,其实和父类中的方法无关,只不过方法名相同罢了,在多态调用的时候,只会直接找父类方法
然而,把主方法定义在父类中时,因为父类中的方法是private
权限而无法被重写,因此即便是向上转型了,也不会去子类中寻找重写的方法,因为编译器根本就不认为存在方法重写,所以会直接向上寻找在父类中eat
方法,然后执行父类中的eat
方法
那为什么要求子类的覆写方法权限要大于等于父类呢?
覆写的意思其实就是要求覆盖,既然子类是继承父类的,那么父类中有的,子类肯定要有,也就是说子类中继承来的方法在权限上至少应该等于父类的权限,更何况方法的覆写是建立在继承关系上的,如果子类的权限要低于父类,这相当于连继承的条件都没有达到,这显然是矛盾的
在Java中有一个注解@Override
使用这个注解写在重写方法之前,帮助校验你的方法重写是否符合规则
public class Dog extends Animal{ @Override public void eat(){ System.out.println("Dog 的eat方法"); }}
方法重写能重写static方法吗?
答案还是不行,多态的本质就是调用了不同的子类的对象,且这些子类对象所属的类覆写了相应的方法,而调用static方法都不通过对象来调用,这两者之间是矛盾的
向上转型三种的方法
向上转型一共可以发生三个位置
//赋值时Animal animal1 = new Dog();Animal animal2 = new Teddy();//方法传参时fun(animal2);public static void fun(Animal animal){ animal.eat();}//方法返回值public static Animal test(){ Dog dog = new Dog(); return dog;}
构造方法与多态的结合
代码的执行结果是什么?
public class B { public B(){ fun(); } public void fun(){ System.out.println("B的fun方法"); }}public class D extends B{ private int num = 10; public void fun(){ System.out.println("D的fun方法, num = " + num); } public static void main(String[] args) { D d = new D(); }}
分析:
首先在创建D的对象的时候调用D的无参构造,因为存在继承,优先调用B的构造方法,以确保先产生父类对象
于是进入B的构造方法执行fun
方法,又因为new的是子类对象,且存在方法覆写,因此执行子类中覆写的fun
方法,此时D中的num
还没有显式初始化,即此时的num = 0
,所以输出了D的fun方法, num = 0
执行完fun
方法之后,在回到D的构造方法,为num
进行显式初始化
方法的调用
假设此时Dog类中有一个扩展方法play()
这个方法Animal类中不具备,此时还能通过animal.play();
进行调用吗?
public class Dog extends Animal{ public void play(){ System.out.println("Dog is playing"); } public static void main(String[] args) { Animal animal = new Dog(); animal.play(); }}//编译不通过
原因:
类名称 引用名称 = new 类实例();
引用名称.方法名称();
这里能否访问该方法,是由类名称说了算的
能访问的这些方法必须都在类中定义过,编译器会先在类中查找是否包含指定方法
至于这个方法到底表现出来是哪个类的样子,由实例所在的方法说了算
Animal animal = new Dog();
animal.fun();
这个fun
能不能调用,看Animal,因为这个引用还是父亲的引用
如果animal.fun()
能调用了,结果到底是什么样子的fun()
,要看new的实例是通过哪个子类new的,以及该子类是否重写了fun()
方法
到底能.
哪些方法前面说了算,到底.
之后这个方法长啥样,后面new的说了算
向下转型
Animal animal = new Dog();
animal.play();
我们可以这么理解:animal这个引用是披着狗皮的动物,本质上是个dog,披了个Animal的外衣,此时只能调用Animal中定义的方法
如果我们想要脱掉这层外衣,还原为子类引用 -> 就是向下转型
语法:子类名称 子类引用 = (子类名称) 父类引用
public static void main(String[] args) { Animal animal = new Dog(); Dog dog = (Dog) animal; dog.play();}//执行结果Dog is playing
这样写的好处是:animal
和dog
这两个引用都指向同一个对象,而没有创建新的对象,然后通过向下转型之后的引用执行父类中没有子类中有的独有方法
向上转型就好像大家都只把人类当做动物看待,而不是当做人来看待,那么此时在大家眼里一个人所做的事情只不过是所有动物都会做的事情,比如吃饭睡觉
但我们写代码是人独有的方法,别的动物都不会,那么人在写代码时就一定是作为人的,而不是动物,因为别的动物不会写代码~~
当然两个类之间不是随意就能发生转型:
要发生向下转型,要先向上转型,如果没有向上转型,两者之间就没有任何关系
上面代码中的animal本身就是Animal类的对象,和Dog类毫无关系,就好像你非要把一只动物说是一只狗,这不合理,因此这种没有任何关系的两个类之间没法强转
可见,当发生向下转型时会有风险,可能会发生类型转换异常
使用instanceof
关键字辅助检测向下转型是否合理
引用名称 instanceof 类名
-> 返回布尔值,表示该引用指向的对象是不是该类的对象
public static void main(String[] args) { Animal animal1 = new Animal(); Animal animal2 = new Dog(); System.out.println(animal1 instanceof Dog);//false System.out.println(animal2 instanceof Dog);//true//instanceof使用方法 if(animal1 instanceof Dog){ Dog dog = (Dog) animal1; System.out.println("animal1转型成功"); }else{ System.out.println("animal1不是指向Dog类型的引用"); } if(animal2 instanceof Dog){ Dog dog = (Dog) animal2; System.out.println("animal2转型成功"); }else{ System.out.println("animal2不是指向Dog类型的引用"); }}//执行结果animal1不是指向Dog类型的引用animal2转型成功
我们使用instanceof
关键字搭配分支语句来进行转换
什么时候发生向上转型 : 方法接收一个类和当前类的子类,参数指定为相应的父类引用,发生的就是向上转型
只有某个特殊的情况,即需要使用子类扩展的方法,才需要将原本向上转型的引用向下转型还原为子类引用
抽象类
根据上面的内容,我们就知道多态非常依赖子类覆写方法,看一下这段代码:
//都在同一个包下public class Sharp { public void print() {}}public class Cycle extends Sharp { @Override public void print(){ System.out.println("圆"); }}public class Square extends Sharp { @Override public void print() { System.out.println("方块"); }}public class Triangle extends Sharp { @Override public void print() { System.out.println("三角形"); }}public class Test { public static void main(String[] args) { fun(new Cycle()); fun(new Square()); fun(new Triangle()); } public static void fun(Sharp sharp){ sharp.print(); }}//执行结果圆方块三角形
但是在需要用到多态的时候,普通父类没法强制要求子类覆写方法
换言之,我可以在子类中什么也不写,编译照样通过,但这不是我们想要的结果,我们希望定义一个子类,里面必须要覆写父类的方法
这个时候,若需要强制要求子类覆写方法,我们就会用到抽象类
现实生活中有很多的抽象类,这些类都是概念化的,没法具体到某个实例,但是能描述这一类对象共同的属性和行为
人类 -> 抽象,没法对应的具体某个或者某一类人,可以是中国人、日本人、印第安人等,总之不能具化
而上面的Sharp类,包括Sharp类中的print()方法,都是抽象概念
既然我们没有具化Sharp类中的属性和方法,我们就可以定义为抽象类
抽象类是普通类的“超集”,只是比普通类多了一些抽象方法而已,可以是[0,N]个
也就是说普通类有的,抽象类全有
抽象方法所在的类必须是抽象类,子类若继承了抽象类,必须覆写所有抽象方法(前提是子类是普通类)
抽象类和抽象方法的定义
Java中定义抽象类或抽象方法使用abstract
关键字
- 抽象方法所在的类必须使用
abstract
声明为抽象类
抽象方法指的是使用abstract
关键字声明,只有函数声明,没有函数实现的方法(或者说没有方法体,即{...}
),称为抽象方法
一个类若存在抽象方法,必须使用abstract
关键字声明为抽象类
抽象方法的意思就是抽象类中没有具体实现,延迟到子类实现
笔试题:
Java中,没有方法体的方法就是抽象方法 - 这句话是False
因为本地方法 - native方法 也没有方法体,但是他不是抽象方法
本地方法的具体实现由JVM实现,JVM是C++写的,本地方法就是指调用了C++中的同名方法
- 若一个类使用
abstract
声明为抽象类,则无法直接通过该类实例化对象,哪怕该类中一个抽象方法都没有
当一个类是抽象类,不管他有没有抽象方法,这个类本身就是一个抽象的概念,没法具体到某个特定的实例,换言之,抽象类不能创建对象
只能通过子类向上转型为抽象父类的引用
Person person = new Person();//false
Person person = new China();//true
- 子类继承了抽象类,就必须强制子类(普通类)覆写抽象类中的所有抽象方法,并且满足单继承局限,一个子类只能extends一个抽象类
abstract class A{ abstract void printA();}//B是抽象类,可以选择性的覆写父类的抽象方法,因为B会继承A的抽象方法abstract class B extends A { abstract void printB();}//C是普通类,必须覆写B中的所有抽象方法(包括继承来的抽象方法)public class C extends B { @Override void printA() {} @Override void printB() {}}
- 抽象类是普通类的超集(普通类有的内容,抽象类全都有),只是比普通类多了些抽象方法而已,抽象类虽然没法直接实例化对象,但是也可以存在构造方法,子类在实例化时,仍然遵从继承的规则,先调用父类的构造方法,然后再调用子类构造方法
abstract class BaseTest{ public BaseTest(){ this.print(); } abstract void print();}public class Fun extends BaseTest { private int num = 10; @Override void print() { System.out.println("num = " + num); } public static void main(String[] args) { new Fun(); }}//执行结果 num = 0
接口
若一个需求既可以使用抽象类,又可以使用接口,优先使用接口,因为抽象类仍然是单继承局限
抽象类虽然没法直接实例化对象,子类仍然满足 is a 原则,子类和抽象父类之间仍然满足“继承树”的关系,而接口在这一方面就不同于抽象类
一般来说,接口的使用表示两种场景
- 接口表示具备某种能力/行为,子类实现接口时不需要满足is a 关系,而是具备这种行为或者能力即可
游泳这种能力或行为,那么Person可以满足游泳接口,Dog能满足游泳接口,Duck也能满足游泳接口
- 接口表示一种规范或者标准
比如说USB接口,5G标准
接口中只有全局常量和抽象方法,这是一个更加纯粹的抽象概念,其他东西比如普通方法、构造方法统统没有,并且全局常量几乎没有,99%都是抽象方法
接口使用关键字interface
声明,子类使用implements
来实现接口,并覆写所有抽象方法
接口表示规范
//定义接口public interface USB { //插入 public abstract void plugIn(); //工作 public abstract void work();}
生活中,USB接口的子类有鼠标、键盘等
//鼠标public class Mouse implements USB { @Override public void plugIn(){ System.out.println("安装鼠标驱动中..."); } @Override public void work(){ System.out.println("鼠标驱动安装完成,现在可正常使用!"); }}//键盘public class KeyBoard implements USB { @Override public void plugIn(){ System.out.println("键盘驱动安装中..."); } @Override public void work(){ System.out.println("键盘驱动安装完成,现在可正常使用!"); }}//新增一个相机子类public class Camera implements USB { @Override public void plugIn(){ System.out.println("读取相机文件中..."); } @Override public void work() { System.out.println("相机文件读取成功,已打开文件夹!"); }}
电脑算不算USB接口的子类?
当然不算,电脑算是USB规范的使用者/载体,也就是说,所有带有USB线插入到电脑中的设备,都应该满足USB规范
//电脑public class Computer { //fun方法就模拟电脑的USB的插口 public void fun(USB usb){ usb.plugIn(); usb.work(); } //主方法 public static void main(String[] args) { Computer computer = new Computer(); Mouse mouse = new Mouse(); //插入鼠标 computer.fun(mouse); KeyBoard keyBoard = new KeyBoard(); //插入键盘 computer.fun(keyBoard); //新增一个相机子类后插入相机 Camera camera = new Camera(); computer.fun(camera); }}
fun方法,为什么使用的参数是USB接口引用?
对于电脑的使用者和生产者来说,不需要关心到底是哪一个具体设备插入到我的电脑上
只需要设备满足USB接口的规范,就都能被电脑识别
用USB来接收参数,就可以实现,一个接口可以接收任何设备
只要接收的设备满足规范,那么都可以插入到电脑并被电脑识别
反过来说,如果接收的Mouse参数,那么这个接口就只能插入鼠标,而键盘就无法被识别
同时,这也是区分接口和抽象类的一个例子,抽象类只能满足垂直逻辑关系,鼠标和键盘从功能来说并不满足垂直逻辑关系,而是横向的,因此USB应该定义为接口比较合适
总的来说就是为了兼容所有USB子类对象
并且对于新的子类来说使用也非常的方便,比如我们只需要创建好相机子类,覆写USB中的抽象方法即可,之后再插入相机,就能被电脑识别了
这种程序设计方法被称为开闭原则,所有设计模式的核心思想:
程序应当对扩展开放,对修改关闭,换言之就是方便扩展,并不能影响已经写好的程序
接口表示能力
接口允许多实现,一个类可能具备多个能力,同时实现多个接口,若实现多个父接口,子类是普通类的话,需要覆写所有的抽象方法
//三个接口public interface IRun { public abstract void run();}public interface ISwim { public abstract void swim();}public interface IFly { public abstract void fly();}//定义子类public class Person implements IRun { @Override public void run() { System.out.println("A person is running ..."); }}public class Dog implements IRun,ISwim{ @Override public void run() { System.out.println("A dog is running ..."); } @Override public void swim() { System.out.println("A dog is swimming ..."); }}public class Duck implements IRun,ISwim,IFly{ @Override public void run() { System.out.println("A duck is running ..."); } @Override public void swim() { System.out.println("A duck is swimming ..."); } @Override public void fly() { System.out.println("A duck is flying ..."); }}//测试public class Test { public static void main(String[] args) { IRun run1 = new Person(); IRun run2 = new Dog(); ISwim swim = new Dog(); IFly fly = new Duck(); run1.run(); run2.run(); swim.swim(); fly.fly(); }}//执行结果A person is running ...A dog is running ...A dog is swimming ...A duck is flying ...
可见接口是允许多继承的
由于接口中只有全局常量和抽象方法,因此接口中的public abstract
和static final
都可以省略不写
代码示例:
//省略并修改后的代码public interface IRun { void run();}public interface ISwim { void swim();}public interface IFly { String show = "wow!"; void fly();}//子类public class Duck implements IRun,ISwim,IFly{ @Override public void run() { System.out.println("A duck is running ..."); } @Override public void swim() { System.out.println("A duck is swimming ..."); } @Override public void fly() { System.out.println(IFly.show); System.out.println("A duck is flying ..."); }}//测试public class Test { public static void main(String[] args) { IFly fly = new Duck(); fly.fly(); }}//执行结果wow!A duck is flying ...
在接口声明中public abstract
和static final
关键字都不用写,只保留最核心的方法返回值,方法参数列表,以及名称即可
当然这种“福利”只有在接口中才有,抽象类是不能这么写的