一、继承
1.1 为什么需要继承
代码示例
public class Cat {
public String name;
public int age;
public String color;
public void eat(){
System.out.println(this.name+"正在吃饭");
}
public void act(){
System.out.println(this.name+"正在跑");
}
public void barks(){
System.out.println(this.name+"正在叫");
}
}
class Dog{
public String name;
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭");
}
public void act(){
System.out.println(this.name+"正在跑");
}
public void miaoMiao(){
System.out.println(this.name+"正在喵喵叫");
}
}
观察上述代码,会发现类中存在大量的重复,如下。那么能否将这些共性抽取出来呢,类比静态成员变量相对每个实例化的对象来说都是所共有的,这里也可以将共性抽取出来,存到一个新的类中,视为这些类所共有的
//共有的内容
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭");
}
public void act(){
System.out.println(this.name+"正在跑");
}
}
1.2 继承的概念
继承机制: 是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类(子类),原来被继承的类称父类/基类/超类。继承呈现了面向对象程序设计的程序结构,体现了由简单到复杂的认知过程。
继承主要解决的问题是:共性的抽取,实现代码复用
在1.1的引例中,Animal类为Dog和Cat类的共性的抽取,那么这种思想就是继承的思想,可理解为Dog和Cat类继承了Animal类,Dog和Cat类为子类,Animal类为父类,继承之后,子类可以复用父类的成员,子类在实现时只需关心自己新增加的成员即可
继承最大的作用就是:实现代码复用,还有就是来实现多态
1.3 继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:
修饰符 class 子类 extends 父类{
//...
}
示例:对1.1中场景进行继承设计
class Animal{
public String name;
public int age;
public void eat(){
System.out.println(this.name+"正在吃饭");
}
public void act(){
System.out.println(this.name+"正在跑");
}
}
public class Cat extends Animal{
public String color;
public void barks(){
System.out.println(this.name+"正在喵喵叫");
}
}
class Dog extends Animal{
public void wang(){
System.out.println(this.name+"正在旺旺叫");
}
}
- 子类会将父类的成员继承到子类当中
- 子类继承父类之后,必须要新添自己特有的成员,体现与父类的不同,否则就没有必要继承了
- static修饰的成员无法被继承,因为该成员属于其类
1.4 父类成员访问
1.4.1 子类中访问父类的成员变量
- 子类和父类不存在同名成员变量
class Base{
int a;
int b;
}
class Derived extends Base{
int c;
public void fun(){
a=1;//访问从父类继承下来的a
b=2;//访问从父类继承下来的b
c=3;//访问子类自己的c
}
}
- 子类和父类成员变量相同
class Base{
int a=10;
int b=9;
int c=8;
}
class Derived extends Base {
int a;
int c=3;
public void fun(){
System.out.println(a);
System.out.println(c);
System.out.println(b);
}
}
从结果可知,当自己与父类存在相同成员变量时,优先使用自己的成员变量
如果想指定访问父类中的成员可以使用super关键字
System.out.println(super.a);
System.out.println(super.c);
System.out.println(super.b);
总结:在子类方法中或者通过子类对象访问成员时
- 如果访问的成员变量子类中有,优先访问自己的成员变量
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错
- 如果访问的成员变量与父类成员变量重名,则优先访问自己的
成员变量访问遵循就近原则,自己有优先自己的,自己没有就去父类去找
1.4.2 子类中访问父类的成员方法
class Derived{
public void fun1(){
System.out.println("父类的fun");
}
public void method(){
System.out.println("父类的方法");
}
public void method1(){
System.out.println("父类的方法");
}
}
class Base extends Derived{
public void fun2(){
System.out.println("子类的fun");
}
public void method(){
System.out.println("子类的方法");
}
public void method1(int x){
System.out.println("子类的方法");
}
public void fun3(){
fun1(); //访问父类的
fun2(); //访问子类的
method();//优先访问子类的
super.method();//访问父类的
method1();//访问父类的
method1(5);//访问子类的
}
}
访问成员方法的规则与访问成员变量基本一致
1.5 super关键字
由于设计原因或者是场景需要,子类和父类中可能会出现相同名称的成员,如果要在子类方法中访问父类同名成员时,可以使用Java中提供的super关键字
super表示的是从父类继承过来的数据的引用
super的出现也增强了代码的可读性,因为看到super就知道是访问的父类同命名员
不能出现在static修饰的方法中,只能在非静态的方法中使用(注意区分:super可以访问static修饰的成员方法)
class Derived{
public void method2(){
System.out.println("调用成功");
}
}
class Base extends Derived {
public static void fun3(){
//报错:super出现在了static修饰的成员方法中
super.method2();
}
}
- super的用法:
1)super.父类成员
2)super.父类成员方法
3)super()父类的构造方法
1.6 子类构造方法
构造方法无法被继承
构造方法名必须与类名完全一致,而子类的类名与父类不同;构造方法的作用是初始化对象,而子类的实例需要同时初始化父类和子类的成员。如果子类继承了父类的构造方法,可能导致父类构造方法直接操作子类特有的成员,引发逻辑混乱;因此子类无法直接“继承”父类的构造方法
class Animal{
public String name;
public int age;
//构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
//报错
public class Cat extends Animal{
public String color;
public void barks(){
System.out.println(this.name+"正在喵喵叫");
}
}
- 如何解决构造方法无法被继承的问题
子类继承了父类之后,一定要先帮助父类的成员进行初始化(使用构造方法来进行初始化)
产生子类的时候,父类的成员就要初始化好,而子类产生之前,需要使用子类自身的构造方法,所有可以在子类的构造方法中,进行对父类成员的初始化
在继承的思想中是现有父后有子,子类对象构造时,需要先调用父类构造方法,再调用子类构造方法,因为子类对象中成员由两部分组成,基类继承下来的部分以及子类独有的部分。由于子类是继承的父类,所以在构造子类对象时要先调用基类的构造方法,将从基类继承下来的成员构造完整,再调用子类自己的构造方法,将子类自己新加的成员初始化完整
class Animal{
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Cat extends Animal {
public String color;
public Cat(String name, int age, String color) {
//调用父类的带有两个参数的构造方法,来初始化父类当中的成员
super(name, age);
this.color = color;
}
}
默认情况下,编译器提供了不带参数的构造方法
class Derived{
public Derived() {
}
}
class Base extends Derived {
public Base() {
super();
}
}
- 注意:
1)若父类显式定义为无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
2)若父类构造方法是带有参数的,则需要为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法的调用,否则编译失败
3)在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中的第一条语句
4)super(…)只能在子类的构造方法中出现一次,并且不能和this()同时出现,因为this()也需要在第一行
1.7 super和this
1.7.1 为什么super和this不能出现在静态方法中?
因为super和this都代表对象的引用,而static修饰的方法加载优先级要先于对象的实例化,所有静态方法加载后,对象不一定存在;另一方面,static修饰的方法属于类的范畴,不属于单个对象,所有super和this不能出现在静态方法中
1.7.2 super和this的区别
- 相同点:
1)都是java中的关键字
2)只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3)在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在 - 不同点:
1)this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
2)在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
3)在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
4)构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有
1.8 再谈代码块
实例代码块和静态代码块和构造方法在没有继承关系时的执行顺序为:静态代码块->实例代码块->构造方法
当有了继承关系之后,其父类和子类的代码块和构造方法执行顺序应该是怎么样的呢?
class Derived{
static {
System.out.println("Derive::static{}");
}
public Derived() {
System.out.println("Derive::public Derived(){}");
}
{
System.out.println("Derive::{}");
}
}
class Base extends Derived {
public Base() {
super();
System.out.println("Base::public Base(){}");
}
static {
System.out.println("Base::static{}");
}
{
System.out.println("Base::{}");
}
}
从以上运行结果可以得知,执行顺序应该是:父类静态代码块->子类静态代码块->父类实例代码块->父类构造方法->子类实例代码块->子类构造方法
不管什么情况下,静态的一定是先执行的
- 总结:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不
1.9 protected 关键字
被protected修饰的类或者类的成员的使用范围为同一个包中可以,不同包中的子类可以,不同包中的非子类不可以使用
- 同一个包中的同一个类
public class P1 {
protected int a;
public static void main(String[] args) {
P1 p1=new P1();
System.out.println(p1.a);
}
}
- 同一个包中的不同类
public class P1 {
protected int a=5;
}
public class P2 {
public static void main(String[] args) {
P1 p1=new P1();
System.out.println(p1.a);
}
}
- 不同包中的子类
//包pack6下代码
package pack6;
public class P1 {
protected int a=5;
}
*******************************
//包pack7下代码
package pack7;
//导入包pack6中的P1类
import pack6.P1;
//PP1继承P1,为其不同包中的子类
public class PP1 extends P1{
public void fun() {
//需要使用super引用来访问,而super不能在静态方法下使用,所以只能在非静态fun方法下使用
System.out.println(super.a);
}
public static void main(String[] args) {
PP1 pp1=new PP1();
pp1.fun();
}
}
- 不同包中的非子类
被protected修饰的成员与不同包中的非子类相当于毫无关系了,所以定不能访问
类的修饰符只能是public和默认不修饰(default)
1.10 继承方式
在现实生活中,事物之间的关系是复杂多样的,比如:
但在Java当中,只支持以下几种继承方式:
-
单继承
-
多层继承
-
不同类继承同一个类
-
Java不支持一个类同时继承多个类,为了解决这一问题,引入了接口
注意:一般来说不建议出现超过三层的继承关系,如果继承层数太多,就要考虑对代码进行重构了,为了限制继承层数太多的问题,可以使用final关键字
1.11 final关键字
final可以修饰变量、成员方法以及类
- final修饰变量或者字段的时候,表示常量,即不能对其进行修改
final int a=10;
a=20;//报错
- 修饰类:表示此类不能被继承
final public class Demo3 {
}
//报错,被final修饰的类不能被继承
class Demo4 extends Demo3{
}
被final修饰的类叫做密封类
平时使用的字符串类型String,就是用final修饰的,不能被继承
- final修饰方法
代表当前方法不能被重写
1.12 继承与组合
与继承类似,组合也是一种表达关系的方式,同样能够达到代码重用的效果;组合没有涉及到特殊的语法,仅仅是将一个类的实例作为另一个类的字段
public class Student {
}
class Teacher{
}
class School {
Teacher[] teachers=new Teacher[10]; //10个老师
Student[] students;
}
继承表示对象之间是is-a的关系:如狗是动物,猫是动物
组合表示对象之间是has-a的关系:如汽车与其方向盘、轮胎、发动机等关系就是组合,因为汽车是由这些部件组成的
组合和继承都可以实现代码的复用,需要根据场景来选择使用,一般建议能用组合就尽量用组合
二、多态
2.1 多态的概念
通俗来说就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
如:打印机:彩色打印打印的是彩色的图片,黑白打印机打印的是黑白的图片;动物:猫这个动物吃猫粮,狗这个动物吃狗粮
总的来说:同一件事情,发生在不同的对象身上,就会产生不同的结果
2.2 多态实现条件
- 在Java中实现多态要铺垫很多条件,必须满足如下几个条件,缺一不可:
1)必须在继承体系下
2)子类必须要对父类中的方法进行重写(动态绑定)
3)通过父类的引用调用重写的方法(向上转型)
下面来看一个引例:该代码包括Animal类、Dog类(继承了Animal类)以及Text类
class Animal{
public int age;
public String name;
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
public void eat(){
System.out.println(this.name+"正在吃");
}
}
class Dog extends Animal{
public String color;
public Dog(int age,String name,String color){
super(age,name);
this.color=color;
}
public void bark(){
System.out.println(this.name+"正在叫");
}
public void eat(){
System.out.println(this.name+"正在吃狗粮");
}
}
public class Text {
public static void main(String[] args) {
Dog d=new Dog(5,"旺财","黄色");
d.eat();
d.bark();
Animal a=new Animal(10,"大黄");
a.eat();
//a.bark();//报错,通过Animal这个引用,只能调用Animal自己的属性
}
}
通过以上可以发现Animal仅能调用自己的属性,而不能调用继承它的类Dog的属性,
要了解多态先要理解向上转型(把子类对象给父类对象的引用)
向上转型是发生多态的必要条件之一
//向上转型
//dog为子类Dog实例化的对象
Animal animal=dog;//animal指向dog所指向的对象
以下两行代码可以合并为一行
//Dog d=new Dog(10,"大黄","黄色");
//Animal a=d;
//父类引用引用了子类对象->向上转型
Animal a=new Dog(10,"大黄","黄色");
到这里通过父类引用仍然不能调用子类属性
将main方法中的代码改为以下代码后,新情况出现了:Animal中的eat()方法按道理来说应该是旺财正在吃,而这里的eat方法显然是Dog类中的方法
public static void main(String[] args) {
Animal animal=new Dog(5,"旺财","黄色");
animal.eat();
}
为什么会出现以上情况呢?
观察两类中的eat方法后会发现,其方法名、参数列表(个数、顺序、数据类型)、返回值都一模一样
此时父类和子类拥有了同样的方法(重写),发生了重写为什么会调用子类?
- 此时发生了动态绑定:编译期间无法确定或者说确定的其他的
打开反汇编,很容易发现,第9行为其构造方法;调用eat时,对应代码为第14行,注释中编译器编译时仍然认为eat为Animal中的方法,但是运行时变成了子类中的eat(运行的时候绑定到了其他方法上执行去了)——理解多态的基础
父类引用 引用子类对象、通过父类引用 去调用 父类和子类 同名的方法->此时调用的是 子类的方法,这个过程就叫做动态绑定
静态绑定:重载就是一种静态绑定->编译时期绑定的(根据给的参数不一样,我就确定你调用哪个方法)
- 向上转型的三种方式(本质都是父类引用指向子类引用指向的对象)
1)直接赋值
Animal animal=new Dog();
2)通过传参
public static void fun(Animal animal) {
}
fun(new Dog());
3)通过返回值
public static Animal fun(){
return new Dog();
}
2.3 多态的实现
下列代码包括Text类、Animal类及其子类Dog和Bird类,其中Animal及其子类都有同返回值、同方法名、同参数列表的方法eat(),即满足向上转型时的动态绑定操作
Text类中的fun方法提供向上转型操作,而三个类存在eat方法,当其向上转型后使用时会进行动态绑定;而动态绑定的结果就构成了不同事物的不同行为,正是这种结果,形成了多态——同一个引用 调用了 同一个方法 但是因为引用的对象不一样,所表现的行为一样,这种思想就是多态
class Animal{
public int age;
public String name;
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
public void eat(){
System.out.println(this.name+"正在吃");
}
}
class Dog extends Animal{
public String color;
public Dog(int age,String name,String color){
super(age,name);
this.color=color;
}
public void bark(){
System.out.println(this.name+"正在叫");
}
public void eat(){
System.out.println(this.name+"正在吃狗粮");
}
}
class Bird extends Animal{
public Bird(int age,String name){
super(age,name);
}
public void eat(){
System.out.println(this.name+"正在吃鸟粮");
}
}
public class Text {
public void fun(Animal animal) {
//发生了多态
animal.eat();
}
public static void main(String[] args) {
Text t=new Text();
Dog dog=new Dog(5,"旺财","黄色");
t.fun(dog);
Bird bird=new Bird(10,"鹦鹉");
t.fun(bird);
}
}
2.4 重写
重写也称覆盖,是子类对父类非静态、非private修饰、非final修饰、非构造方法等的实现过程进行重新编写,返回值和形参都不能改变,核心重写!重写的好处在于子类可以根据需要定义特定于自己的行为,也就是说子类能够根据需要实现父类的方法
- 注意点:
1)被final修饰的方法不可以被重写,这个方法叫做密封方法
2)被static修饰的方法不能被重写
3)子类重写父类方法的时候,子类的方法访问修饰限定符大于等于父类
4)被private修饰的方法是不能被重写的(私有方法)
5)被重写的方法返回值可以不同且唯一不同情况为返回值构成父子关系
public Animal fun(){
}
public Dog fun(){
}
重写的设计原则:对于已经投入使用的类,尽量不要进行修改。最好的方式是重新定义一个新的类,来重复利用其中的功性内容,并且添加或改动新的内容
2.5 向上转型和向下转型
- 向上转型:实际上就是创建一个子类对象,将其当成父类对象来使用
语法格式:父类类型 对象名 = new 子类类型() ;
Animal animal = new Dog();
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
向上转型的缺陷是不能调到用子类特有的方法
- 向下转型
将一个子类对象经过向上转型后可以当父类方法使用,再无法调用子类方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型
Animal animal=new Bird(1,"布谷鸟");
//向下转型
Bird bird=(Bird)animal;
但向下转型一旦使用不当便会出错,如下
Animal animal=new Dog(1,"大黄","黄色");
//出错
Bird bird=(Bird)animal;
因为原来animal引用的是一个bird,所以之后animal强转为bird没问题;但后面原来的引用为dog,所以不能强转为bird,只是骗过了编译器
- 为了增强向下转型的安全性,Java提供了instanceof关键字
A instanceof B :判断引用A是不是引用了引用B的对象,若是值为true,否则为false
编译通过,但不发生强转
Animal animal=new Dog(1,"大黄","黄色");
if(animal instanceof Bird){
Bird bird=(Bird)animal;
}
2.6 多态的优缺点
- 多态的优点
1)能够降低代码的“圈复杂度”,避免使用大量的if-else
什么叫“圈复杂度”?
可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数
2)可拓展能力强
如果要新增一个行为或者事物,只需使用多态的方式(仅需增加一个类),代码改动成本低
- 多态缺点
属性和构造方法没有多态性,见下代码
2.7 避免在构造方法中调用重写的方法
示例代码
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private static int num = 1;
public void func() {
System.out.println("D.func() " + num);
}
}
public class Main {
public static void main(String[] args) {
D d=new D();
}
}
当父类的构造方法中调用父类和子类同名的方法时,也会发生动态绑定。此时也意味着构造方法内也会发生动态绑定,所以在构造方法中不要调用重写的方法。避免。