当前位置: 首页 > news >正文

Java核心——面向对象编程(上)包-继承-多态

作者:~小明学编程 

文章专栏:JavaSE基础

格言:目之所及皆为回忆,心之所想皆为过往
在这里插入图片描述

目录

什么是包?

导入包中的类

静态导入

将类放到包中

 包的访问权限控制

常见的系统包

继承

需求的引入

语法规则

 protected 关键字

复杂的继承关系

final关键字

组合

多态

向上转型

动态绑定

方法重写

关于重写的注意事项

@Override关键字

重载与重写的区别

体会动态绑定和方法重写

关于多态

 使用多态的好处

向下转型

super 关键字

在构造方法中调用重写的方法

小结

抽象类

语法规则

抽象类的作用


什么是包?

包(package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性,例如在一个大型的项目中两个人所写的类的名字相同,在我们对代码进行编译的时候就不知道到底调用哪一个类了,而我们使用包就就可以把每个人写的类放在自己的包中想使用类的时候可以通过先引用包的方式再调用我们包中的类。

导入包中的类

Java中提供了许多现成的类供我们使用

public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

像这样使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类写起来比较的繁琐和麻烦我们可以通过import 语句导入包.

import java.util.Date;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

下面的这种写法明显比上面的看起来简洁一些。

如果需要使用 java.util 中的其他类, 可以使用 import java.util.*

import java.util.*;
public class Test {
    public static void main(String[] args) {
        Date date = new Date();
// 得到一个毫秒级别的时间戳
        System.out.println(date.getTime());
    }
}

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

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
        Date date = new Date();
        System.out.println(date.getTime());
    }
}

此时我们就需要输入完整的类名

import java.util.*;
import java.sql.*;
public class Test {
    public static void main(String[] args) {
        java.util.Date date = new java.util.Date();
        System.out.println(date.getTime());
    }
}

静态导入

使用 import static 可以导入包中的静态的方法和字段

import static java.lang.Math.*;
public class Test {
    public static void main(String[] args) {
        double x = 30;
        double y = 40;
// 静态导入的方式写起来更方便一些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
        double result = sqrt(pow(x, 2) + pow(y, 2));
        System.out.println(result);
    }

这种静态导入写起来会比较方便不用再写入包名,但是这种写法的可读性比较低会使得代码不易理解和解读,所以个人不建议这种写法。

将类放到包中

基本规则
1.在文件的最上方加上一个 package 语句指定该代码在哪个包中.
2.包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.baidu.demo1 ).
3.包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/baidu/demo1 来存储代码.
4.如果一个类没有 package 语句, 则该类被放到一个默认包中

 包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用。
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。
例如:

我们创建了一个dome1的包然后又分别创建了两个类dome1和testDome

public class testDome {
    int val =10;
}

我们在testDome中定义了一个成员变量val

public class dome{
    public static void main(String[] args) {
        testDome dome = new testDome();
        System.out.println(dome.val);
    }
}

我们在我们的dome类里面可以直接引用testDome类里面的成员变量val,这是因为这两个类都在同一个包dome1中而且我们的成员val没有任何的访问权限的关键字修饰所以就默认其最大权限为同一包的不同类。 

public class TestDome {
    public int max = 10;
    protected int pro = 555;
    int val = 255;
   
}

上面我们在我们的dome类里面想引用我们不同包里面的TestDome类里面的val我们可以看到代码已经报红,并不支持我们这样的引用。

常见的系统包

1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。


2. java.lang.reflect:java 反射编程包;


3. java.net:进行网络编程开发包。


4. java.sql:进行数据库开发的支持包。


5. java.util:是java提供的工具程序包,(集合类等) 非常重要。


6. java.io:I/O编程开发包。

继承

需求的引入

学继承之前我们首先要知道我们为什么需要继承

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
    }
}
class Dog {
    public String name;
    public Dog(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
    }
}
class Bird {
    public String name;
    public Bird(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
    }
    public void fly() {
        System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
    }
}

上述代码我们设计了三个类分别是Animal,Dog,Bird三个类分别实现一些功能,但是我们发现在我们的三个类中都有eat这个方法,如果我们的动物类比较多的话岂不是会产生大量的代码冗余,这时候我们就会想我们的Dog或者Bird能否用Animal中的eat呢?这样我们就相当于继承了Animal中的方法了,然后我们就能够使用Animal中的方法了。

此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。

语法规则

class 子类 extends 父类 {

}

使用 extends 指定父类.


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


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


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


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

所以对于上述的代码我们让我们的Bird类和Dog类去继承我们的Animal类这样我们的Bird和Dog类就可以直接使用我们父类Animal中的一些方法和成员了。

class Animal {
    public String name;
    public int age;
    private int count;
    protected int pro = 100;
    public void eat() {

        System.out.println(name+"eat()");
    }
    public Animal(String name,int age) {
        System.out.println(name+age);
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{
    int age;
    public Dog(String name,int age) {
        super(name,age);
    }
}
class Bird extends Animal{
    public String name;
    public Bird(String name,int ag) {
        super(name,ag);
    }
    public void fly() {
        System.out.println(super.name+"fly");
    }
}

接着我们new一个对象出来来检查一下我们继承的类能不能正常的使用

public class TestDome {
    public int max = 10;
    protected int pro = 555;
    int val = 255;
     public static void main(String[] args) {
        Dog dog = new Dog("小白",10);
        Bird bird = new Bird("小鸟",20);
        bird.fly();
        dog.eat();
    }
}

这是我们代码的运行结果,我们可以看到我们一共输出4行的内容,前面两行都是构造方法中的输出,当我们的父类中有构造方法的时候我们的子类也必须有构造方法才能使得语法正常,然后我们通过super将我们子类构造方法中的参数传到父类中的构造方法之中。

下面的Bird.fly()是调用我们子类中的方法,而下面中的dog.eat()则是调用我们父类中的方法,此时我们已经完成了继承所以可以直接调用我们父类中的方法了。

当我们将Animal中修饰name的public改为private这时候就会显示报错,因为子类中是被private修饰的所以不能被访问。

 protected 关键字

刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 "封装" 的初衷,两全其美的办法就是 protected 关键字。


1.对于类的调用者来说, protected 修饰的字段和方法是不能访问的
2.对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
 

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


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


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


protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.


public : 类内部和类的调用者都能访问

什么时候下用哪一种呢?
我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

复杂的继承关系

我们刚刚的代码只有Bird和dog继承Animal但是我们如果继承的关系比较的复杂的话该怎么办呢?

例如

// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}

这样的继承方式我们称之为多层继承,即我们的子类可以再派生出新的子类,但是即使如此我们也不希望我们的继承关系超过三层,一旦超过三层的话我们的类与类之间的关系将会变得十分的复杂,代码也会变得及其的难以维护。所以我们就想到我们能不能给我们的类加上限制让其不能再被继承。

final关键字

之前我们学过final修饰我们的变量,当我们的变量被final修饰之后则我们的变量就不能被修改了,今天我们将要了解的是final修饰我们的类。

当我们的final修饰我们的类之后我们的类将不能被继承了。

我们可以看到当我们在我们的Aniaml类的前面加上我们final修饰之后此时我们再想要继承我们的Animal类的时候这个时候编译器会给我们报错,告诉我们此类已经不能再被继承了。

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

我们final修饰的方法也不能被重写,关于重写我们后面将会讲到。
 

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.
例如表示一个学校:

public class Student {
...
}
public class Teacher {
...
}
public class School {
    public Student[] students;
    public Teacher[] teachers;
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.
这是我们设计类的一种常用方式之一.
组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 "包含" 若干学生和教师.
继承表示 is - a 语义
在上面的 "动物和猫" 的例子中, 我们可以理解成一只猫也 "是" 一种动物.
大家要注意体会两种语义的区别
 

多态

向上转型

在我们先前的代码中我们创建一个对象是这样的

Dog dog = new Dog("小白",10);

这个代码同时也可以这样写

Dog dog = new Dog("小白",10);
Animal dog2 = dog;
// 或者写成下面的方式
Animal dog2 = new Dog("小白",10);

此时 dog2 是一个父类 (Animal) 的引用, 指向一个子类 (Dog) 的实例. 这种写法称为 向上转型.


为啥叫 "向上转型"?
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 "向上转型" , 表示往父类的方向转。

向上转型发生的时机:
1.直接赋值
2.方法传参
3.方法返回
直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.
 

方法传参

class Animal {
    public String name;
    public int age;
    private int count;
    protected int pro = 100;
    public void eat() {
        System.out.println(name+"eat()");
    }
    public Animal(String name,int age) {
        System.out.println(name+age);
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{

    int age;
    public Dog(String name,int age) {
        super(name,age);
    }
}
public class TestDome {
    public static void main(String[] args) {
        Dog dog = new Dog("小绿",12);
        eat(dog);
    }
    public static void eat(Animal animal) {
        animal.eat();
    }
   
}

这里我们构造了一个静态的方法eat,静态方法中的参数是Animal类型的对象,但是我们在主函数里面传入的是是一个我们Dog类型的对象但是我们看到程序仍然是调用了我们Animal类里面的方法中的eat()方法,这是因为此时发生了向上转型,此时形参 animal 的类型是 Animal (基类), 实际上对应到 Dog(父类) 的实例。

方法返回
 

public class Test {
    public static void main(String[] args) {
        Animal animal = findMyAnimal();
}
    public static Animal findMyAnimal() {
        Bird bird = new Bird("小紫");
        return bird;
    }
}

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例
 

动态绑定

上面我们已经讨论完向上转型的情况了,当我们的父类引用子类对象的时候将会发生向上转型,这时我们的变量就相当于是一个父类的对象了,同时可以引用父类的一些变量和方法了。但是当我们当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

class Animal {
    public String name;
    public int age;
    private int count;
    protected int pro = 100;
    public void eat() {

        System.out.println(name+"Animal eat()");
    }
    public Animal(String name,int age) {
        this.name = name;
        this.age = age;
    }
}
class Dog extends Animal{
    int age;
    public Dog(String name,int age) {
        super(name,age);
    }
}
class Bird extends Animal{
    public Bird(String name,int ag) {
        super(name,ag);
        this.name = name;
    }

    public void eat() {
        System.out.println(name+"Bird eat()");
    }
}
public class TestDome {

    public static void main(String[] args) {
        Animal animal1 = new Animal("小白",12);
        Animal animal2 = new Bird("小黑",15);
        animal1.eat();
        animal2.eat();
    }
}

 此时, 我们发现:animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向Bird 类型的实例.
针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而
animal2.eat() 实际调用了子类的方法.

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

方法重写

针对刚才的 eat 方法来说:子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)。

关于重写的注意事项

1.普通方法可以重写, static 修饰的静态方法不能重写.

这里我们将eat()方法修改为静态方法,然后编译器提示报错了。 

2.重写中子类的方法的访问权限不能低于父类的方法访问权限.

这里我们将子类中的方法权限改为了private编译器就给我们报错了,因为我们父类中的eat方法的访问权限是public,子类的权限已经低于父类的权限了。 

3.重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

因为重写方法有着诸多的要求所以我们有一个特殊的关键字来提醒着我们我们重写方法是否正确

@Override关键字

我们推荐在代码中进行重写方法时显式加上 @Override 注解,例如当我们的重写方法名写错的时候。

 此时就会给我们提醒,我们的重写的方法名有错误。

重载与重写的区别

方法的重载与重写可以说基本没有关系,它们是两种概念,但是由于名称相似所以会有一些混淆,从最根本来看,重载是在一个类之中发生的,而重写(覆写)是发生在继承关系之中的。

然后看它们的概念,方法的重载时方法名相同,而参数的类型以及个数是不同的,而重写则是方法名称,返回值的类型,参数的类型,以及个数都是完全相同的。

最后就是权限,方法的重载是没有任何权限的的要求的,但是方法的重写严格的权限要求的即被重写的方法不能拥有比父类更加严格的访问权限。

体会动态绑定和方法重写

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

关于多态

在我们学完了向上转型,动态绑定,方法的重写之后我们就可以用多态的思想来编程了。

我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况,下面就是一个具体的例子来介绍多态思想的编程。

代码示例: 打印多种形状
 

class Shape {
    public void draw() {
// 啥都不用干
    }
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
class Rect extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("♣");
    }
}
/分割线//
// testDome.java
public class testDome {
    public static void main(String[] args) {
        Shape shape1 = new Flower();
        Shape shape2 = new Cycle();
        Shape shape3 = new Rect();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
    }
    // 打印单个图形
    public static void drawMap(Shape shape) {
        shape.draw();
    }
}

这里我们的父类是一个Shape然后我们的三个子类继承了我们的父类,这里面首先涉及到了一个方法的重写,然后就是我们下面的一个testDome,之后我们创建了一个静态的方法,里面的参数是我们的父类。

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态。

 使用多态的好处

1.类调用者对类的使用成本进一步降低.
1)封装是让类的调用者不需要知道类的实现细节.
2)多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。

2.能够降低代码的 "圈复杂度", 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下

public static void drawShapes() {
        Rect rect = new Rect();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
        for (String shape : shapes) {
            if (shape.equals("cycle")) {
                cycle.draw();
            } else if (shape.equals("rect")) {
                rect.draw();
            } else if (shape.equals("flower")) {
                flower.draw();
            }
        }    
}

代码看起来比较的冗余,当我们利用多态的思想去编程时。

 public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
        Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
                new Rect(), new Flower()};
        for (Shape shape : shapes) {
            shape.draw();
        }
 }    

这样看起来代码就简洁了许多。

什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构.

3.可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。
我们只需要新增一个类就行了,而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高。。

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

向下转型

public static void main(String[] args) {
        Animal animal = new Bird("小黑",18);
        animal.eat();
    }

还是熟悉的配方,我们现在是一个向上转型的代码,又因为其中涉及到了方法的重写所以我们最终调用的是子类中的eat()方法。

 但是我们现在想要调用子类中的fly()方法该怎么办呢?

我们首先想到的是直接animal.fly();但是下面会给我们报错,这时就需要我们的向下转型了。

 public static void main(String[] args) {
        Animal animal = new Bird("小黑",18);
        animal.eat();
        ((Bird) animal).fly();
    }

这里相当于对我们的animal进行了一个强制转换,也就是我们的向下转型,最后就是调用fly()了。

但是这种向下转型不是很可靠:

 public static void main(String[] args) {
        Animal animal1 = new Dog("小白",15);
        ((Bird)animal1).fly();
    }

我们看到代码给我们报错了,这是因为animal 本质上引用的是一个 Dog 对象, 是不能转成 Bird 对象的,运行时就会抛出异常。

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。

 public static void main(String[] args) {
        Animal animal1 = new Dog("小白",15);
        if (animal1 instanceof Bird) {
            Bird bird = (Bird)animal1;
            bird.fly();
        }

    }

我们可以写成上述的形式,instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了。

super 关键字

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

super 表示获取到父类实例的引用. 涉及到两种常见用法
1.使用了 super 来调用父类的构造器

public Bird(String name,int ag) {
        super(name,ag);
        this.name = name;
    }

2.使用 super 来调用父类的普通方法

class Bird extends Animal{
    public Bird(String name,int ag) {
        super(name,ag);
        this.name = name;
    }

    @Override
    public void eat() {
        System.out.println(name+"Bird eat()");
    }

    public void fly() {
        super.eat();
        System.out.println(super.name+"fly");
    }
}

如上我们直接通过super.eat()来调用父类中的eat()方法。

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat 了将会打印"Bird eat()". 而加上 super 关键字, 才是调用父类的方法。

注意 super 和 this 功能有些相似, 但是还是要注意其中的区别
this是访问本类中的属性和方法而super则是访问父类中的属性和方法。

this先查找本类,如果本类中没有才调用父类,super则是直接调用的父类。

this表示的是当前的对象,而则啥也不表示。

在构造方法中调用重写的方法

下面是一段比较坑的代码,如果对于动态绑定不是很熟悉的话非常容易犯错误。

class A{
    public A(){
        func();
    }
    public void func(){
        System.out.println("A hello");
    }
}
class B extends A{
    private int num = 10;
    public void func(){
        System.out.println(num+" a hello");
    }
}
public class dome {
    public static void main(String[] args) {
        B b = new B();
    }
}

我们创建了两个类,A和B,其中我们的B类继承A类,父类中有构造方法,所以在我们创建一个B的对象的时候我们会调用A中的构造方法,我们A中的构造方法又调用了func方法,我们想要打印的是A hello但是事实如此吗?

 事实并非如此我们看到我们调用的是B类中的方法,这一步是因为我们对方法进行了重写所以最后调用的是子类中的方法,但是num的值明明是10为什么会打印一个0出来呢?

这是因为我们在构造B对象的时候会调用A中的构造方法然后父类中的构造方法将会调用func方法因为这里我们进行了一个重写,所以调用子类中的方法但是此时我们的子类还没有构造完成num还在初识状态,所以此时的值是0。

小结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系。

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


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


抽象类

语法规则

上述打印图形的代码中我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class)。

abstract class Shape {
    abstract public void draw();
}

像这种包含抽象方法的类,我们把它叫做抽象类。

在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类。

注意事项:

1.抽象类不能被实例化。

        Shape shape = new Shape();

上述的写法是不合法的,因为我们的抽象类是不能被实例化的。

2.抽象方法不能是 private 的

    abstract private void draw();

上述写法中把我们的抽象方法用private修饰了,这是错误的写法,我们的抽象方法是不能用private修饰的。

 3.抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。

抽象类的作用

抽象类存在的最大意义就是为了被继承,抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法。

那么问题来了,我们普通的类也可以被继承,普通的方法一样可以被重写,为啥非要用抽象类呢?

确实如此. 但是使用抽象类相当于多了一重编译器的校验,使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。

很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.充分利用编译器的校验, 在实际开发中是非常有意义的.

相关文章:

  • Ambari自动部署Hadoop集群实战
  • 33.0、C语言——C语言预处理(1) - 翻译环境详解
  • java-php-python-springboot网上订餐系统计算机毕业设计
  • 【VUE项目实战】66、上线-通过node创建Web服务器
  • About 9.25 This Week
  • 三、基本命令
  • MySQL中select ... for update会锁表还是锁行?
  • 计算机毕业设计选题 SSM大学生企业推荐系统(含源码+论文)
  • 【Java设计模式 思想原则重构】设计思想、设计原则、重构总结
  • js逆向-逆向基础
  • 【前端】【探究】HTML - input类型为file时如何实现自定义文本以更好的美化
  • 二叉树的dp问题和Morris遍历
  • 重新认识IO以及五种IO模型(理论认识)
  • leetcode: 647. 回文子串
  • SQL语言概述与SQL语言的数据定义
  • 《Java编程思想》读书笔记-对象导论
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • create-react-app项目添加less配置
  • CSS盒模型深入
  • JS数组方法汇总
  • MobX
  • MySQL QA
  • Redis的resp协议
  • unity如何实现一个固定宽度的orthagraphic相机
  • zookeeper系列(七)实战分布式命名服务
  • Zsh 开发指南(第十四篇 文件读写)
  • 编写高质量JavaScript代码之并发
  • 缓存与缓冲
  • 记一次和乔布斯合作最难忘的经历
  • 消息队列系列二(IOT中消息队列的应用)
  • 异步
  • HanLP分词命名实体提取详解
  • Semaphore
  • 第二十章:异步和文件I/O.(二十三)
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • #{} 和 ${}区别
  • #if 1...#endif
  • #if和#ifdef区别
  • #WEB前端(HTML属性)
  • #单片机(TB6600驱动42步进电机)
  • (007)XHTML文档之标题——h1~h6
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (9)目标检测_SSD的原理
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (二)fiber的基本认识
  • (六)vue-router+UI组件库
  • (论文阅读23/100)Hierarchical Convolutional Features for Visual Tracking
  • (论文阅读30/100)Convolutional Pose Machines
  • (十七)devops持续集成开发——使用jenkins流水线pipeline方式发布一个微服务项目
  • (顺序)容器的好伴侣 --- 容器适配器
  • (转)自己动手搭建Nginx+memcache+xdebug+php运行环境绿色版 For windows版
  • (轉貼) 蒼井そら挑戰筋肉擂台 (Misc)
  • .NET8.0 AOT 经验分享 FreeSql/FreeRedis/FreeScheduler 均已通过测试