计算机程序的思维逻辑 (15) - 初识继承和多态

继承

上节我们谈到,将现实中的概念映射为程序中的概念,我们谈了类以及类之间的组合,现实中的概念间还有一种非常重要的关系,就是分类,分类有个根,然后向下不断细化,形成一个层次分类体系。这种例子是非常多的:

在自然世界中,生物有动物和植物,动物有不同的科目,食肉动物、食草动物、杂食动物等,食肉动物有狼、狗、虎等,这些又分为不同的品种 ...

打开电商网站,在显著位置一般都有分类列表,比如家用电器、服装,服装有女装、男装,男装有衬衫、牛仔裤等 ...

计算机程序经常使用类之间的继承关系来表示对象之间的分类关系。在继承关系中,有父类和子类,比如动物类Animal和狗类Dog,Animal是父类,Dog是子类。父类也叫基类,子类也叫派生类,父类子类是相对的,一个类B可能是类A的子类,是类C的父类。

之所以叫继承是因为,子类继承了父类的属性和行为,父类有的属性和行为,子类都有。但子类可以增加子类特有的属性和行为,某些父类有的行为,子类的实现方式可能与父类也不完全一样。

使用继承一方面可以复用代码,公共的属性和行为可以放到父类中,而子类只需要关注子类特有的就可以了,另一方面,不同子类的对象可以更为方便的被统一处理。

本节主要通过图形处理中的一些简单例子来介绍Java中的继承,会介绍继承的基本概念,关于继承更深入的讨论和实现原理,我们在后续章节介绍。

Object

在Java中,所有类都有一个父类,即使没有声明父类,也有一个隐含的父类,这个父类叫Object。Object没有定义属性,但定义了一些方法,如下图所示:


本节我们会介绍toString()方法,其他方法我们会在后续章节中逐步介绍。toString()方法的目的是返回一个对象的文本描述,这个方法可以直接被所有类使用。

比如说,对于我们之前介绍的Point类,可以这样使用toString方法:

Point p = new Point(2,3);
System.out.println(p.toString()); 

输出类似这样:

[email protected]

这是什么意思呢?@之前是类名,@之后的内容是什么呢?我们来看下toString的代码:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

getClass().getName() 返回当前对象的类名,hashCode()返回一个对象的哈希值,哈希我们会在后续章节中介绍,这里可以理解为是一个整数,这个整数默认情况下,通常是对 象的内存地址值,Integer.toHexString(hashCode())返回这个哈希值的16进制表示。

为什么要这么写呢?写类名是可以理解的,表示对象的类型,而写哈希值则是不得已的,因为Object类并不知道具体对象的属性,不知道怎么用文本描述,但又需要区分不同对象,只能是写一个哈希值。

但子类是知道自己的属性的,子类可以重写父类的方法,以反映自己的不同实现。所谓重写,就是定义和父类一样的方法,并重新实现。

Point类 - 重写toString()

我们再来看下Point类,这次我们重写了toString()方法。

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public double distance(Point point){
        return Math.sqrt(Math.pow(this.x-point.getX(),2)
                +Math.pow(this.y-point.getY(), 2));
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public String toString() {
        return "("+x+","+y+")";
    }
}

toString方法前面有一个 @Override,这表示toString这个方法是重写的父类的方法,重写后的方法返回Point的x和y坐标的值。重写后,将调用子类的实现。比如,如下代码的输出就变成了:(2,3)

Point p = new Point(2,3);
System.out.println(p.toString());

图形处理类

接下来,我们以一些图形处理中的例子来进一步解释,先来看幅图:

这都是一些基本的图形,图形有线、正方形、三角形、圆形等,图形有不同的颜色。接下来,我们定义以下类来说明关于继承的一些概念:

  • 父类Shape,表示图形。
  • 类Circle,表示圆。
  • 类Line,表示直线。
  • 类ArrowLine,表示带箭头的直线。

图形 (Shape)

所有图形都有一个表示颜色的属性,有一个表示绘制的方法,下面是代码:

public class Shape {
    private static final String DEFAULT_COLOR = "black";

    private String color;

    public Shape() {
        this(DEFAULT_COLOR);
    }

    public Shape(String color) {
        this.color = color;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public void draw(){
        System.out.println("draw shape");
    }
}

以上代码基本没什么可解释的,实例变量color表示颜色,draw方法表示绘制,我们不会写实际的绘制代码,主要是演示继承关系。

圆 (Circle)

圆继承自Shape,但包括了额外的属性,中心点和半径,以及额外的方法area,用于计算面积,另外,重写了draw方法,代码如下:

public class Circle extends Shape {
    //中心点
    private Point center;

    //半径
    private double r; 

    public Circle(Point center, double r) {
        this.center = center;
        this.r = r;
    }

    @Override
    public void draw() {
        System.out.println("draw circle at "
                +center.toString()+" with r "+r
                +", using color : "+getColor());
    }

    public double area(){
        return Math.PI*r*r;
    }
}

说明几点:

  • Java使用extends关键字标明继承关系,一个类最多只能有一个父类。
  • 子类不能直接访问父类的私有属性和方法,比如,在Circle中,不能直接访问shape的私有实例变量color。
  • 除了私有的外,子类继承了父类的其他属性和方法,比如,在Circle的draw方法中,可以直接调用getColor()方法。

看下使用它的代码:

public static void main(String[] args) {
    Point center = new Point(2,3);
    //创建圆,赋值给circle
    Circle circle = new Circle(center,2);
    //调用draw方法,会执行Circle的draw方法
    circle.draw();
    //输出圆面积
    System.out.println(circle.area());
}

程序的输出为:

draw circle at (2,3) with r 2.0, using color : black
12.566370614359172

这里比较奇怪的是,color是什么时候赋值的?在new的过程中,父类的构造方法也会执行,且会优先于子类先执行。在这个例子中,父类Shape的默认构造方法会在子类Circle的构造方法之前执行。关于new过程的细节,我们会在后续章节进一步介绍。

直线 (Line)

线继承自Shape,但有两个点,有一个获取长度的方法,另外,重写了draw方法,代码如下:

public class Line extends Shape {
    private Point start;
    private Point end;

    public Line(Point start, Point end, String color) {
        super(color);
        this.start = start;
        this.end = end;
    }

    public double length(){
        return start.distance(end);
    }

    public Point getStart() {
        return start;
    }

    public Point getEnd() {
        return end;
    }

    @Override
    public void draw() {
        System.out.println("draw line from "
                + start.toString()+" to "+end.toString()
                + ",using color "+super.getColor());
    }
}

这里我们要说明的是super这个关键字,super用于指代父类,可用于调用父类构造方法,访问父类方法和变量:

  • 在line构造方法中,super(color)表示调用父类的带color参数的构造方法,调用父类构造方法时,super(...)必须放在第一行。
  • 在draw方法中,super.getColor()表示调用父类的getColor方法,当然不写super.也是可以的,因为这个方法子类没有同名的,没有歧义,当有歧义的时候,通过super.可以明确表示调用父类的。
  • super同样可以引用父类非私有的变量。

可以看出,super的使用与this有点像,但super和this是不同的,this引用一个对象,是实实在在存在的,可以作为函数参数,可以作为返回值,但super只是一个关键字,不能作为参数和返回值,它只是用于告诉编译器访问父类的相关变量和方法。

带箭头直线 (ArrowLine)

带箭头直线继承自Line,但多了两个属性,分别表示两端是否有箭头,也重写了draw方法,代码如下:

public class ArrowLine extends Line {

    private boolean startArrow;
    private boolean endArrow;

    public ArrowLine(Point start, Point end, String color,
            boolean startArrow, boolean endArrow) {
        super(start, end, color);
        this.startArrow = startArrow;
        this.endArrow = endArrow;
    }

    @Override
    public void draw() {
        super.draw();
        if(startArrow){
            System.out.println("draw start arrow");
        }
        if(endArrow){
            System.out.println("draw end arrow");
        }
    }
}

ArrowLine继承自Line,而Line继承自Shape,ArrowLine的对象也有Shape的属性和方法。

注意draw方法的第一行,super.draw()表示调用父类的draw()方法,这时候不带super.是不行的,因为当前的方法也叫draw()。

需要说明的是,这里ArrowLine继承了Line,也可以直接在类Line里加上属性,而不需要单独设计一个类ArrowLine,这里主要是演示继承的层次性。

图形管理器

使用继承的一个好处是可以统一处理不同子类型的对象,比如说,我们来看一个图形管理者类,它负责管理画板上的所有图形对象并负责绘制,在绘制代码中,只需要将每个对象当做Shape并调用draw方法就可以了,系统会自动执行子类的draw方法。代码如下:

public class ShapeManager {
    private static final int MAX_NUM = 100;
    private Shape[] shapes = new Shape[MAX_NUM];
    private int shapeNum = 0;

    public void addShape(Shape shape){
        if(shapeNum<MAX_NUM){
            shapes[shapeNum++] = shape;
        }
    }

    public void draw(){
        for(int i=0;i<shapeNum;i++){
            shapes[i].draw();
        }
    }
}

ShapeManager使用一个数组保存所有的shape,在draw方法中调用每个shape的draw方法。ShapeManager并不知道每个shape具体的类型,也不关心,但可以调用到子类的draw方法。

我们来看下使用ShapeManager的一个例子:

public static void main(String[] args) {
    ShapeManager manager = new ShapeManager();

    manager.addShape(new Circle(new Point(4,4),3));
    manager.addShape(new Line(new Point(2,3),
            new Point(3,4),"green"));
    manager.addShape(new ArrowLine(new Point(1,2),
            new Point(5,5),"black",false,true));

    manager.draw();
}

新建了三个shape,分别是一个圆、直线和带箭头的线,然后加到了shape manager中,然后调用manager的draw方法。

需要说明的是,在addShape方法中,参数Shape shape,声明的类型是Shape,而实际的类型则分别是Circle,Line和ArrowLine。子类对象赋值给父类引用变量,这叫向上转型,转型就是转换类型,向上转型就是转换为父类类型。

变量shape可以引用任何Shape子类类型的对象,这叫多态,即一种类型的变量,可引用多种实际类型对象。这样,对于变量shape,它就有两个类型,类型Shape,我们称之为shape的静态类型,类型Circle/Line/ArrowLine,我们称之为shape的动态类型。在ShapeManager的draw方法中,shapes[i].draw()调用的是其对应动态类型的draw方法,这称之为方法的动态绑定。

为什么要有多态和动态绑定呢?创建对象的代码 (ShapeManager以外的代码)和操作对象的代码(ShapeManager本身的代码),经常不在一起,操作对象的代码往往只知道对象是某种父类型,也往往只需要知道它是某种父类型就可以了。

可以说,多态和动态绑定是计算机程序的一种重要思维方式,使得操作对象的程序不需要关注对象的实际类型,从而可以统一处理不同对象,但又能实现每个对象的特有行为。后续章节我们会进一步介绍动态绑定的实现原理。

小结

本节介绍了继承和多态的基本概念:

  • 每个类有且只有一个父类,没有声明父类的其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,可以重写父类的方法实现。
  • new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的话,调用父类的默认构造方法。
  • 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
  • 子类对象可以赋值给父类引用变量,这叫多态,实际执行调用的是子类实现,这叫动态绑定。

但关于继承,还有很多细节,比如实例变量重名的情况。另外,继承虽然可以复用代码,便于统一处理不同子类的对象,但继承其实是把双刃剑,使用不当,也有很多问题。让我们下节来讨论这些问题,而关于继承和多态的实现原理,让我们再下节来讨论。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。原创文章,保留所有版权。

时间: 2024-10-11 17:03:48

计算机程序的思维逻辑 (15) - 初识继承和多态的相关文章

计算机程序的思维逻辑 (15) - 初识继承和多态【转】

继承 上节我们谈到,将现实中的概念映射为程序中的概念,我们谈了类以及类之间的组合,现实中的概念间还有一种非常重要的关系,就是分类,分类有个根,然后向下不断细化,形成一个层次分类体系.这种例子是非常多的: 在自然世界中,生物有动物和植物,动物有不同的科目,食肉动物.食草动物.杂食动物等,食肉动物有狼.狗.虎等,这些又分为不同的品种 ... 打开电商网站,在显著位置一般都有分类列表,比如家用电器.服装,服装有女装.男装,男装有衬衫.牛仔裤等 ... 计算机程序经常使用类之间的继承关系来表示对象之间的

计算机程序的思维逻辑 (11) - 初识函数

函数 前面几节我们介绍了数据的基本类型.基本操作和流程控制,使用这些已经可以写不少程序了. 但是如果需要经常做某一个操作,则类似的代码需要重复写很多遍,比如在一个数组中查找某个数,第一次查找一个数,第二次可能查找另一个数,每查一个数,类似的代码都需要重写一遍,很罗嗦.另外,有一些复杂的操作,可能分为很多个步骤,如果都放在一起,则代码难以理解和维护. 计算机程序使用函数这个概念来解决这个问题,即使用函数来减少重复代码和分解复杂操作,本节我们就来谈谈Java中的函数,包括函数的基础和一些细节. 定义

计算机程序的思维逻辑 (22) - 代码的组织机制

使用任何语言进行编程都有一个类似的问题,那就是如何组织代码,具体来说,如何避免命名冲突?如何合理组织各种源文件?如何使用第三方库?各种代码和依赖库如何编译连接为一个完整的程序? 本节就来讨论Java中的解决机制,具体包括包.jar包.程序的编译与连接,从包开始. 包的概念 使用任何语言进行编程都有一个相同的问题,就是命名冲突,程序一般不全是一个人写的,会调用系统提供的代码.第三方库中的代码.项目中其他人写的代码等,不同的人就不同的目的可能定义同样的类名/接口名,Java中解决这个问题的方法就是包

计算机程序的思维逻辑 (23) - 枚举的本质

前面系列,我们介绍了Java中表示和操作数据的基本数据类型.类和接口,本节探讨Java中的枚举类型. 所谓枚举,是一种特殊的数据,它的取值是有限的,可以枚举出来的,比如说一年就是有四季.一周有七天,虽然使用类也可以处理这种数据,但枚举类型更为简洁.安全和方便. 下面我们就来介绍枚举的使用,同时介绍其实现原理. 基础 基本用法 定义和使用基本的枚举是比较简单的,我们来看个例子,为表示衣服的尺寸,我们定义一个枚举类型Size,包括三个尺寸,小/中/大,代码如下: public enum Size {

计算机程序的思维逻辑 (21) - 内部类的本质

内部类 之前我们所说的类都对应于一个独立的Java源文件,但一个类还可以放在另一个类的内部,称之为内部类,相对而言,包含它的类称之为外部类. 为什么要放到别的类内部呢?一般而言,内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上也往往更为简洁. 不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类这回事的, 每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件. 也就是说,每个内部

计算机程序的思维逻辑 (28) - 剖析包装类 (下)

本节探讨Character类,它的基本用法我们在包装类第一节已经介绍了,本节不再赘述.Character类除了封装了一个char外,还有什么可介绍的呢?它有很多静态方法,封装了Unicode字符级别的各种操作,是Java文本处理的基础,注意不是char级别,Unicode字符并不等同于char,本节详细介绍这些方法以及相关的Unicode知识. 在介绍这些方法之前,我们需要回顾一下字符在Java中的表示方法,我们在第六节.第七节.第八节介绍过编码.Unicode.char等知识,我们先简要回顾一

计算机程序的思维逻辑 (25) - 异常 (下)

上节我们介绍了异常的基本概念和异常类,本节我们进一步介绍对异常的处理,我们先来看Java语言对异常处理的支持,然后探讨在实际中到底应该如何处理异常. 异常处理 catch匹配 上节简单介绍了使用try/catch捕获异常,其中catch只有一条,其实,catch还可以有多条,每条对应一个异常类型,比如说: try{ //可能触发异常的代码 }catch(NumberFormatException e){ System.out.println("not valid number"); }

计算机程序的思维逻辑 (29) - 剖析String

上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比较简单直接的,我们来看下. 基本用法 可以通过常量定义String变量 String name = "老马说编程"; 也可以通过new创建String String name = new String("老马说编程"); String可以直接使用+和+=运算符,如: S

计算机程序的思维逻辑 (17) - 继承实现的基本原理

第15节我们介绍了继承和多态的基本概念,而上节我们进一步介绍了继承的一些细节,本节我们通过一个例子,来介绍继承实现的基本原理.需要说明的是,本节主要从概念上来介绍原理,实际实现细节可能与此不同. 例子 这是基类代码: public class Base { public static int s; private int a; static { System.out.println("基类静态代码块, s: "+s); s = 1; } { System.out.println(&qu