【java学习】面向对象编程(一)【详解篇13】

【前言】本节学习的是面向对象编程,因内容较多所以分两部分来写,这一部分主要是对包、继承、组合和多态的学习。如发现错误,烦请指正!!

包(package)是组织类的一种方式。

使用包的主要目的是保证类的唯一性。

在公司中,一个项目一般都是协同开发,也就是一个项目要和好几个同事共同完成,假设,你在代码中写了一个 TestDemo 类. 然后你的同事也可能写一个 TestDemo 类. 在IDEA中的src这个目录下不能同时有两个相同的类名,因为在一个文件夹下就不能同时创建两个文件名相同的文件,所以如果出现两个同名的类, 就会冲突,因此会导致代码不能编译通过.如图:

image-20210905183436359

为了解决这一冲突,就引入了包这个概念!!

什么是包?

如何创建包?

过程如图:

image-20210905181322231

创建包的注意事项

注意事项1:创建包的时候,最好采用域名,包名一定是小写的。

  • 什么是域名?比如百度的域名:www.baidu.com,这就是百度的域名;而我们在创建包的时候,也要尽量使用域名,只不过是要反着来的,比如com.baidu.www。

比如我们创建一个试试:image-20210905181226991

image-20210905182135509

  • 在IEDA上我们叫包,但是对windows来说它就是一个文件夹,在某个路径下生成的src文件夹.如图:

image-20210905182827879

  • 此时,如果你在代码中写了一个 TestDemo 类,之后还想再写一个一样的就可以写在生成的包的这个文件夹下面了,而且不会发生冲突,即使用包给有可能重名的类换个文件夹。如图:image-20210905183630660

注意事项2:创建的包一定要在前面加上package及包的路径,在IDEA上会自动加好,但是如果要在命令框或者文本文档上写的话则需要手动加上。如图:

image-20210905184358401

注意事项3:以前说过打印数组的时候通过Array.toString(array)来打印一个数组,使用Arrrays时也需要导包import,不过import导的是系统包,而package导的是自己的包,如图:image-20210905205351851

  • 使用帮助手册可以查看Arrays在哪个包的下面,如图:在java.util的下面

image-20210905205632224

  • 一般情况下,这个包会自动导入,如果没有自动导入,则需要手动导入。

  • 当然还有的包是不需要导入的,如String字符串类型,它是java.long ,不过java.long底下的包不需要我们手动导入。

image-20210905212002440

  • 导入包中的类有两种写法:如图image-20210905213831179

  • 如果需要使用 java.util 中的其他类, 可以使用 import java.util.* ,因为java包下很多类,但是不会一次性加载进来,而是使用到谁就用谁。如图:

image-20210905214708514

  • 但是我们更建议显式的指定要导入的类名. 否则还是容易出现冲突的情况

image-20210905215552568

注意事项4: import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要.

import 只是为了写代码的时候更方便. import 更类似于 C++ 的namespace和using。

附图:如:java.util包下的类image-20210905220907689

总结:

将类放到包中基本规则

  • 在文件的最上方加上一个 package 语句指定该代码在哪个包中.

  • 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).

  • 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码.

  • 如果一个类没有 package 语句, 则该类被放到一个默认包中.

常见的系统包

  1. △java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入,不需要手动导入。
  2. java.lang.reflflect:java 反射编程包;
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包。(集合类等) 非常重要
  6. java.io:I/O编程开发包。

包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用.

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用.

继承

前面我们学习封装的时候已经说过OOP语言的三大特征是:继承、封装、多态,封装已经学过了,那下面进入继承和多态的学习吧!

简单回忆一下封装吧

封装是当前属性被prient所修饰,在类外不能够访问,只能在类内来进行访问;

封装的意义就是:它能够降低代码的管理复杂度,类的调用者只能调用共有的setter、getter方法就可以了。

继承的背景

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.

继承的优点就是可以达到代码的复用的效果

例如, 设计一个类表示动物

package com.xiaoba.demo1;
//设计一个动物类
class Animal{
    public String name;//属性/成员变量
    public void eat(){//成员方法
        System.out.println("Animal::eat()");
    }
    public void sleep(){//成员方法
        System.out.println("Animal::sleep()");
    }
}
    //设计一个猫类
    class Cat extends Animal{//extends:继承;  语法:猫继承了动物;
       public String name;
       public void eat(){
           System.out.println("Cat::eat()");
       }
    }
    //设计一个鸟类
    class Bird{
        public String name;
        public void eat(){
            System.out.println("Bird::eat()");
        }
        public void fly(){
            System.out.println("Bird::fly()");
        }
    }
public class TestDemo {
    public static void main(String[] args) {

    }
}

这里一共写了三个类,分别是Animal、Cat、Bird;

  • 在这个代码中我们发现其中存在了大量的冗余代码: 因为动物和猫和鸟这三个类分别都有一个相同的属性叫name,而且意义是完全一样的;还分别都有一个相同的方法叫eat(), 而且行为是完全一样的,所以仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系。

  • 从逻辑上讲, Cat 和 Bird 都是一种 Animal,他们的关系是is a的关系 (is a 语义).如:Cat is a Animal, Bird is a Animal;

  • 此时就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码复用的效果

    基本语法规则

    class 子类 extends 父类 { 
    
    } 

    使用 extends 指定父类.

    Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).

    子类会继承父类的所有 public 的字段和方法.

    对于父类的 private 的字段和方法, 子类中是无法访问的.

    子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。(关于super关键字的讲解,在下面的多态中会讲,这里会因为需要先用一下。

    Cat和Bird未继承 Animal 类的代码示例:

    //例如, 设计一个类表示动物
    class Animal {
        public String name;//属性/成员变量
        public void eat() {//成员方法
            System.out.println("Animal::eat()");
        }
        public void sleep() {
            System.out.println("Animal::sleep()");
        }
    }
     //设计一个猫类
        class Cat {
           public String name;
           public void eat(){//成员方法
            System.out.println(this.name+" "+"Cat::eat()");
           }
        }
    //设计一个鸟类
        class Bird{
            public String name;
            public void eat(){
                System.out.println(this.name+" "+"Bird::eat()");
            }
            public void fly(){
                System.out.println("Bird::fly()");
            }
        }
    public class TestDemo {
            public static void main(String[] args) {
                Cat cat = new Cat();//实例化对象,调用不带有参数的默认构造方法
                cat.name = "咪咪";
                cat.eat();
                Bird bird = new Bird();
                bird.name = "麻雀";
                bird.eat();
                bird.fly();
    
            }
        }
    //打印结果:
    咪咪 Cat::eat()
    麻雀 Bird::eat()
    Bird::fly()

    Cat和Bird继承 Animal 类的代码示例:

class Animal {
    public String name;
    public void eat() {
        System.out.println(this.name+" "+"Animal::eat()");
    }
    public void sleep() {
        System.out.println("Animal::sleep()");
    }
}
    class Cat extends Animal{//语法:猫继承了动物;extends:继承
     
    }
    class Bird extends Animal{
        public void fly(){
            System.out.println("Bird::fly()");
        }
    }
public class TestDemo {
        public static void main(String[] args) {
            Cat cat = new Cat();//实例化对象,调用不带有参数的默认构造方法
            cat.name = "咪咪";
            cat.eat();
            cat.sleep();
            Bird bird = new Bird();
            bird.name = "麻雀";
            bird.eat();
            bird.fly();

        }
    }
//打印结果:
咪咪 Animal::eat()
Animal::sleep()
麻雀 Animal::eat()
Bird::fly()
  • Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类;和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果
  • 因此可以知道,继承是两个类之间的关系,他们的关系是is a的关系。如果满足这种关系的话,就可以通过继承来实现。
  • 通过画图查看继承之前和继承之后的区别:
  • ![image-20210907162459550](https://img-blog.csdnimg.cn/img_convert/f16bce0f8781034efeb3be4a1de1cc72.png

面试问题:子类继承了父类的什么?

子类继承了父类除构造方法外的其他所有的。

子类继承父类,相当于拥有了父类的属性。

注意:

1、java是单继承,即在java当中使用extends只能继承一个类。如图:image-20210907165202254

2、子类会继承父类的所有 public 的字段和方法,但是如果将 public 改为private(私有的),也可以继承,但不能被访问,private只能在类中被访问,也就是只能在class中访问,不能在类外访问。**即对于父类的 private 的字段和方法, 子类中是无法访问的 。**如图:image-20210907170101045

3、子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。

根据代码来理解这句话的意思,如图:image-20210907171629395

疑问1:为什么没有提供构造方法之前就不报错,提供了构造方法之后就报错了呢?

答:因为没有提供构造方法之前,默认是有一个不带参数构造方法的,当提供一个对象之后,原来默认不带有参数的默认构造方法就不存在了。

疑问2:为什会报错呢?

答:子类在构造的时候,要先构造父类,即要先帮助父类进行构造。

疑问3:那父类怎构造呢?

答:我们知道构造一个对象只能通过构造方法来进行构造,所以可以这样解决报错问题。如图:

image-20210907180603924

面试问题:请说出this关键字和super关键字的区别?

答:

this代表当前对象的引用,可不是当前对象哦!

对于this来说,它有三种用法:

1、this();//代表调用本类其他的构造方法;

2、this.data;//代表访问当前类中对的属性;

3、this.func();//代表调用本类的其他的成员方法。

super代表父类对象的引用

对于super来说,它也有三种用法“

1、super();//代表调用父类的构造方法,它必须放到第一行;

2、super.data();//代表访问父类的属性;

3、super.func();//代表访问父类的成员方法。

这里我们使用的是super的第一种用法:如图image-20210907181834476

image-20210907180820895

image-20210907180843942

所以当子类继承了父类之后,一定要先帮助父类来进行构造。

此时还需要注意一个问题:super(name)必须放在第一行,否则会报错。如图:image-20210907181258143

完整的继承代码:帮助父类进行构造,并显示的调用表构造方法

class Animal {
    public String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        System.out.println("Animal(String)");//打印
    }
    public void eat() {
        System.out.println(this.name+" "+"Animal::eat()");
    }
   private void sleep() {
        System.out.println("Animal::sleep()");
    }
}
    class Cat extends Animal{//语法:猫继承了动物;extends:继承
        public Cat(String name){//子类构造的时候只能构造子类Cat,在构造猫这个类的时候,首先去调用父类的构造方法,同时给name赋值,而这个name就是super(name)传过去的值
            super(name);//显示调用父类的构造方法
            System.out.println("Cat(String)");    
    }
    class Bird extends Animal{
        public Bird(String name) {
            super(name);
        }
        public void fly(){
            System.out.println(this.name+"Bird::fly()");//子类继承父类,子类也会有父类中的属性,可以访问
            //但如果父类中的name变成私有的,子类也会继承父类,但就不能访问父类中的私有属性了。
        }
    }
public class TestDemo {
        public static void main(String[] args) {
            Cat cat = new Cat("咪咪");//实例化对象,调用带有参数的构造方法
            cat.eat();
            Bird bird = new Bird("麻雀");
            bird.eat();
            bird.fly();//访问自己
        }
    }
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()
Animal(String)
麻雀 Animal::eat()
麻雀Bird::fly()

protected 关键字

我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷.所以既要体现封装性又要在继承上使用,两全其美的办法就是protected 关键字。

对于类的调用者来说, protected 修饰的字段和方法是不能访问的

对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的。

验证同一个包中的同一类可以进行访问

image-20210907225619681

验证同一个包中的不同类进行访问的测试

image-20210907225713318

不同包当中的子类进行访问的测试

image-20210907225758797

小总结: Java 中对于字段和方法共有四种访问权限

private: 类内部能访问, 类外部不能访问

default:默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问。

protected: protected主要体现在继承上,类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问。

public : 类内部和类的调用者都能访问(即哪里都可以访问)。

画成表格是下面这样:

No范围privatedefaultprotectedpublic
1同一包中的同一类
2同一包中的不同类
3不同包中的子类
4不同包中的非子类

什么时候下用哪一种呢?

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.

因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public。

另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 所以还是需要在写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)。

因此,不要无脑的只是用public,也可以适当的使用一下private、default、和protected。

继承总结

以上我们结合代码主要讲了

  • extends,继承的基础,继承的意义:就是为了代码的复用,如果不继承的话,那么重复的代码会变得非常多,所以要通过extends关键字实现继承;

  • A extends B:A是子类或派生类,B是父类或基类或超类。

  • 注意:1、子类继承了父类除构造方法外的其他所有的

    ​ 2、子类在构造的时候,要先帮助父类来进行构造。

    ​ 3、访问修饰限定符。private、default、protected、public,权限范围从小到大排列。(着重记忆protected

  • this关键字和super关键字的区别。

更复杂的继承关系

刚才我们的例子中, 只涉及到 Animal, Cat 和 Bird 三种类. 但是如果情况更复杂一些呢?

针对 Cat 这种情况, 我们可能还需要表示更多种类的猫~

image-20210907234856589

这个时候使用继承方式来表示, 就会涉及到更复杂的体系。

代码示例:层层继承,层层调用。

class Animal {
    public String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        System.out.println("Animal(String)");//打印
    }
    public void eat() {
        System.out.println(this.name+" "+"Animal::eat()");
    }
   private void sleep() {
        System.out.println("Animal::sleep()");
    }
}
    class Cat extends Animal{//Cat的父类是Animal
        public Cat(String name){//子类构造的时候只能构造子类Cat,在构造猫这个类的时候,首先去调用父类的构造方法,同时给name赋值,而这个name就是super(name)传过去的值
            super(name);//显示调用父类的构造方法
            System.out.println("Cat(String)");
        }
    }
    class  ChineseGardenCat extends Cat{//中华田园猫继承猫,ChineseGardenCat的父类是Cat
    public ChineseGardenCat(String name){
        super(name);//调用父类Cat带有一个参数的,显示调用父类的构造方法
    }
}
class  OrangeCat extends ChineseGardenCat{//橘猫继承中华田园猫,OrangeCat的父类是ChineseGardenCat
     public OrangeCat(String name){
         super(name);
     }
  }
public class TestDemo {
        public static void main(String[] args) {
            Cat cat = new Cat("咪咪");//实例化对象,调用带有参数的构造方法
            cat.eat();
            ChineseGardenCat chineseGardenCat=new ChineseGardenCat("中华田园猫");
            chineseGardenCat.eat();
            OrangeCat orangeCat=new OrangeCat("橘猫");
            orangeCat.eat();
        }
    }
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()
Animal(String)
Cat(String)
中华田园猫 Animal::eat()
Animal(String)
Cat(String)
橘猫 Animal::eat()

如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类.

时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.

但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.

如果想从语法上进行限制继承, 就可以使用 final 关键字。

final 关键字

曾经我们学习过 fifinal 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改).

final关键字可以修饰常量

final int a=10;//final修饰常量,被final修饰的常量,只能被初始化一次,之后就不能被修改了。

final int a = 10; 
a = 20; // 编译出错
final关键字可以修饰类

final修饰类也被称为密封类,它的特性是被修饰的类就不能被继承了。

所以一旦一个类被final所修饰,这个类必然就不能被继承了

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
} 
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承

final 关键字的功能是 限制 类被继承

“限制” 这件事情意味着 “不灵活”. 在编程中, 灵活往往不见得是一件好事. 灵活可能意味着更容易出错.使用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的。

我们平时是用的 String字符串类,就是用final修饰的,不能被继承。

final关键字也可以修饰方法

final修饰方法也被称为密封方法

组合

和继承类似, 组合也是一种表达类之间关系的方式是一种has 的关系(谁有…), 也是能够达到代码重用的效果.

例如表示一个学校:

public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
}
  • 组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.这是我们设计类的一种常用方式之一。

组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师。

继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 "是“一种动物。

要注意体会两种语义的区别

多态

向上转型

向上转型代码示例:

package com.xiaoba.demo4;
import java.util.concurrent.Callable;
class Animal {
    protected String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        System.out.println("Animal(String)");//打印
    }
    public void eat() {//成员方法
        System.out.println(this.name+" "+"Animal::eat()");
    }
}
class Cat extends Animal {//Cat继承Animal,可以访问name属性和eat方法
    public int count=99;
    public Cat(String name){//子类构造的时候只能构造子类Cat,在构造猫这个类的时候,首先去调用父类的构造方法,同时给name赋值,而这个name就是super(name)传过去的值
        super(name);//显示调用父类的构造方法
        System.out.println("Cat(String)");
    }
}
class Bird extends Animal {//Bird继承Animal
    public Bird(String name) {//构造方法
        super(name);显示调用父类的构造方法
    }
    public void fly(){
        System.out.println(this.name+"Bird::fly()");
    }
}
public class TestMain {
    //向上转型:就是将子类赋值给父类
    public static void main(String[] args) {
    //向上转型:父类引用,引用子类对象
        //一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型
        Animal animal=new Cat("咪咪");//将子类对象赋值给父类引用就叫向上转型
        animal.eat();//向上转型之后可以访问(调用)eat方法
       // animal.count;//error,原因:向上转型之后,通过父类的引用只能访问父类自己对象里面的方法或者属性,即父类引用只能访问自己特有的。
    }
}
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()

画图理解分析:image-20210908010102670

为甚麽叫向上转型呢?

如上图:因为把子类对象赋值给父类引用了,所以叫向上转型。

发生向上转型的时机(形式)

1、直接赋值

直接赋值发生向上转型的代码示例:

public class TestMain {
    //向上转型:就是将子类赋值给父类
    public static void main(String[] args) {
        Animal animal=new Cat("咪咪");//将子类对象赋值给父类引用就叫向上转型
        animal.eat();//向上转型之后可以访问(调用)eat方法
    }
}
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()

2、方法传参

传参发生向上转型的代码示例:形参 animal 的类型是 Animal实际上对应到 Cat 的实例

public class TestMain {
    //在这个过程中发生了向上转型
    public static void func(Animal animal){//传参,使用Animal来接收( 形参 animal 的类型是 Animal (基类))
        animal.eat();//调用eat方法
    }
    public static void main(String[] args) {
        Cat cat=new Cat("咪咪");
        func(cat);//调用func
    }
}
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()    

3、方法返回

返回值发生向上转型的代码示例:func 返回的是一个 Animal 类型的引用, 但是实际上对应到 Cat 的实例.

public class TestMain {
    //在这个过程中发生了向上转型
    public static Animal func(){//返回值设为Animal
        Cat cat=new Cat("咪咪");
        return cat;
    } 
    public static void main(String[] args) {
        Animal animal=func();
        animal.eat();
    }
}
//打印结果:
Animal(String)
Cat(String)
咪咪 Animal::eat()
  • 这三种情况都可以发生向上转型,所以,向上转型不一定只有直接赋值的,只要在传的过程当中可以实现向上转型的情况都可以。

动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的说明.

 class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
    }
}
 class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
}
public class Testdtbd {
    public static void main(String[] args) {
        Animal animal1 = new Animal("圆圆");
        animal1.eat("谷子");
        Animal animal2 = new Bird("扁扁");
        animal2.eat("谷子");
    }
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子

此时, 我们发现:

  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向Bird 类型的实例.

  • 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法.

  • 因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写

代码引入:这个代码中,父类当中有一个eat方法,子类当中有一个eat方法,且方法名相同,返回值相同,参数列表也相同,因此这两个eat方法我们把它叫做重写

class Animal {
    protected String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        System.out.println("Animal(String)");//打印
    }
    public void eat() {//成员方法
        System.out.println(this.name+" "+"Animal::eat()");
    }
}
class Cat extends Animal {//Cat继承Animal,可以访问name属性和eat方法
    public int count=99;
    public Cat(String name){
        super(name);//显示调用父类的构造方法
        System.out.println("Cat(String)");
    }
    public void eat() {//猫自己实现一个eat方法
        System.out.println(this.name+" "+"Cat::eat()");
    }
}
 public static void main(String[] args) {
        Animal animal=new Cat("咪咪");//将子类对象赋值给父类引用就叫向上转型
        animal.eat();//向上转型之后可以访问(调用)eat方法
    }
  • 回忆重载(overload):需要满足的条件

1、方法名相同;

2、参数列表不同(参数列表指的是参数的个数和类型)

3、返回值不做要求

4、同一个类当中

  • 重写(override):需要满足的条件

1、方法名称相同;

2、返回值相同;

3、参数列表相同。

4、它在不同的类当中,这个不同的类是在继承关系上。

当猫类没有定义eat方法时,它调用的时Animal中的eat,如图:image-20210908023925345

当猫类定义了eat方法时,它调用的时Cat中的eat,如图:

image-20210908024003342

通常情况下,父类只能调用自己的属性和方法,也就是只能调用父类自己的eat方法,怎么会调用猫的呢?

上面这个代码的现象叫运行时绑定(也称为动态绑定,关于动态绑定,上面也说了)。

运行时绑定的前提是父类引用引用子类对象,同时通过父类引用调用同名的覆盖方法,此时才会发生运行时绑定

为甚麽叫做运行时绑定?

反汇编java代码可能就理解了。(没懂02:20:00)。

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 即在编译的时候确实调用的是Animal的eat方法,但在运行的时候,却调用的是Cat()的eat方法。因此称为 动态绑定.

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).

关于重写的注意事项

1.重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
2. 普通方法可以重写, static 修饰的静态方法不能重写.
3.重写中子类的方法的访问权限不能低于父类的方法访问权限.
4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

  1. 需要重写的方法,不能是被final修饰的。被修饰之后,他是密封方法,不可以修改。
  2. 被重写的方法,访问修饰限定符一定不能私有的private.
  3. =3、被重写的方法,子类的当中的访问修饰限定要大于等于父类的访问修饰限定(private、default、protected、public,权限范围从小到大排列)。
  4. 被static修饰的方法是不可以重写的。

思考:

1、重载和重写的区别是甚麽?

NO区别重载(overload)重写(override)
1概念方法名称相同,参数的类型及个数不同方法名称、返回值类型、参数的类型及个数完全相同
2范围一个类继承关系
3限制没有权限要求被覆盖的方法不能拥有比父类更严格的访问控制权限

2、运行时绑定是啥?

答:父类引用引用子类对象,同时通过父类引用调用同名的覆盖方法,此时才会发生运行时绑定也叫做动态绑定。

3、体会动态绑定和方法重写

事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述的是相同的事情, 只是侧重点不同.

理解多态

有了上面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低

封装是让类的调用者不需要知道类的实现细节.

多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.

这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

对于封装来说,如果一旦采用封装(把某一个属性封装了),只需提供公有的方法即可,对于类的调用者来说,不需要知道具体的实现细节,只管调用这些公有的方法就好了。它的意义就在于降低了代码的管理复杂度。

对于继承来说,继承的最大意义就是可以让代码进行复用。

对于多态来说,多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.所以就出现了多态

2) 能够降低代码的 圈复杂度”, 避免使用大量的 if - else

例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

  public static void drawShapes() {
        React react = new React();
        Cycle cycle = new Cycle();
        Quert quert = new Quert();
        String[] shapes = {"cycle", "react", "cycle", "react", "quert"};//如果没有多态,需要画一次圆,画一次菱形,画一次圆,画一次菱形,画一次正方形
        //把字符串都放到数组当中,然后通过for循环遍历数组,看是哪个字符串,是哪个字符串就画哪个形状
        for (String shape : shapes) {
            if (shape.equals("cycle")) {//字符串的比较相等用equals
                cycle.draw();
            } else if (shape.equals("react")) {
                rect.draw();
            } else if (shape.equals("quert")) {
                flower.draw();
            }
            //在实现这个代码的过程中可以看出使用了大量的if和else来判断画的是谁
        }
    }

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单

public static void drawShapes() {
        // 我们创建了一个 Shape 对象的数组.
        //如果有了多态,可以创建一个Shape类型的数组,数组是相同类型的集合,但在这里 Shape类型的数组里面存放的并不是shape对象
        //因为这里面发生了向上转型,Shape是父类,这些数据继承了Shape,所以Shape类型的数组也可以存放这些数据类型。
       /* Shape shape1=new Cycle();//shape1是Shape类型,它引用的是Cycle这个对象,所以这一步可以省略,直接将它放在Shape类型的数组里
        Shape[] shapes = {shape1, new React(), new Cycle(),
                new React(), new Quert()};*/
        Shape[] shapes = {new Cycle(), new React(), new Cycle(),
                new React(), new Quert()};//把对象放在数组中,这些对象都是Shape的子类,可以发生向上转型
        for (Shape shape : shapes) {//遍历这个数组,冒号左边是数组里面数据的类型,冒号右边是数组,由于每一个元素都继承于Shape,所以直接用shape
            //接收即可,在这里发生了多态的现象。
            shape.draw();//调用draw方法,打印不同形状
        }
    }

什么叫 “圈复杂度” ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.(简单理解就是if elae的数量)

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构。

不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .(即if elae的数量不能超过10个,一个不写最好,所以就出现了多态。)

3) 可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。

class Triangle extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("△"); 
 } 
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,

但是也有一定的用途.

向下转型代码示例:

class Animal {
    protected String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        System.out.println("Animal(String)");//打印
    }
    public void eat() {//成员方法
        System.out.println(this.name+" "+"Animal::eat()");
    }
}
class Cat extends Animal {//Cat继承Animal,可以访问name属性和eat方法
    public int count=99;
    public Cat(String name){
        super(name);//显示调用父类的构造方法
        System.out.println("Cat(String)");
    }
}
class Bird extends Animal {//Bird继承Animal
    public Bird(String name) {//构造方法
        super(name);显示调用父类的构造方法
    }
    public void fly(){
        System.out.println(this.name+"Bird::fly()");
    }
}
    /**
     * 多态
     * 演示向下转型
     * @param args
     */
public static void main(String[] args) {
        Animal animal=new Bird("八哥");
        animal.eat();
       // animal.fly();//error:因为父类没有fly这个方法,通过父类属性只能调用他自己本身的,所以无法调用fly,会报错。
        //解决办法:使用向下转型,向下转型就是将父类的引用赋值给子类
        Bird bird=(Bird)animal;
        bird.fly();
    }

画图理解如下:image-20210910003811446

注意:向下转型很不安全,所以很少使用。为什么不安全呢?

不安全的向下转型的代码示例:

image-20210910005503783

如何解决这样的问题呢?

想要正确使用向下转型,需要引入另外一个关键字也叫运算符instanceof,来判断animal是否是某个方法的实例,if为真,向下执行,否则就执行else。

 public static void main(String[] args) {
        Animal animal=new Cat ("八哥");
        animal.eat();
        //A instanceof B :判断A是不是B的一个实例
        if(animal instanceof Bird){//判断animal 引用的是否为Bird对象,不是,它引用的是Cat对象,所以if语句里的代码不能被执行。
            Bird bird=(Bird)animal;
            bird.fly();
        }else{
            System.out.println("hehehehehe");
        }
    }

建议不要使用向下转型(写项目或工作时),可根据情况而定。

super关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字.

super 表示获取到父类实例的引用. 涉及到两种常见用法.

  1. 使用了 super 来调用父类的构造器(这个代码在动态绑定处已经写过了)。
public Bird(String name) {
        super(name);
    }
  1. 使用 super 来调用父类的普通方法
class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void eat(String food) {
        // 修改代码, 让子调用父类的接口.
        super.eat(food);
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
}
  • 在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递

归了). 而加上 super 关键字, 才是调用父类的方法.

  • 注意 super 和 this 功能有些相似, 但是还是要注意其中的区别.

上面其实已经说过了super 和 this 功能及区别(面试问题),这里我们用表格的形式再归纳一下。

No区别thissuper
1概念访问本类中的属性、方法由子类访问父类中的属性和、方法
2查找范围先查找本类,如果本类没有就调用父类不查找本类,而是直接调用父类定义
3特殊表示当前对象

在构造器(方法)中调用重写的方法(一个坑)

一个坑指的是:在构造器(方法)中调用重写的方法也是会发生动态绑定的。

一段有坑的代码. 我们创建两个类, Animal 是父类,Cat 是子类. Cat 中重写 eat 方法. 并且在 Animal的构造方法中调用 eat。

class  Animal {
    protected String name;
    public Animal(String name){//对Animal提供一个构造方法,并且它的构造方法有一个参数
        this.name=name;//赋值
        eat();//在animal中调用eat方法
    }
    public void eat() {//成员方法
        System.out.println(this.name+" "+"Animal::eat()");
    }
}
class Cat extends Animal {//Cat继承Animal,可以访问name属性和eat方法
    public int count=99;
    public Cat(String name){//带有一个参数的构造方法
        super(name);//要想调用带有一个参数的构造方法,需要显示调用父类的构造方法
    }
    public void eat() {//猫自己实现一个eat方法
        System.out.println(this.name+" "+"Cat::eat()");
    }
}
/*父类当中有一个eat方法,子类当中有一个eat方法,且方法名相同,返回值相同,参数列表也相同,因此这两个eat方法我们把它叫做重写*/
public class TestMain {
    /**
     * 在构造方法中调用重写的方法
     *
     * @param args
     */
    public static void main(String[] args) {
        //在动物里面有eat方法,在猫里面也有eat方法
        Cat cat=new Cat("小八哥");//调用带有一个参数的构造方法
        cat.eat();//通过cat调用eat方法
}
//打印结果:
小八哥 Cat::eat()
小八哥 Cat::eat()
  • 构造Cat 对象的同时, 会调用 Animal 的构造方法.
  • Animal 的构造方法中调用了 eat 方法, 此时会触发动态绑定, 会调用(触发)到 Cat 中的 eat方法
  • 此时 Cat 对象自身还没有构造, 此时 this.name处在已初始化的状态, 值为 小八哥.

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发

动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。

画图分析原由:image-20210910085946163

面试问题;构造方法当中是否会发生动态绑定?

答:在构造方法当中可以发生动态绑定。(如上述代码中:Animal 的构造方法中调用了 eat 方法, 此时会触发动态绑定, 会调用(触发)到
Cat 中的 eat方法)。

多态总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带

来的编码上的好处.

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.

  • C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
  • Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值