zoukankan      html  css  js  c++  java
  • Java编程的逻辑 (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()); 

    输出类似这样:

    Point@76f9aa66

    这是什么意思呢?@之前是类名,@之后的内容是什么呢?我们来看下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强制访问父类的变量和方法。
    • 子类对象可以赋值给父类引用变量,这叫多态,实际执行调用的是子类实现,这叫动态绑定。

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

  • 相关阅读:
    ubuntu 安装 redis desktop manager
    ubuntu 升级内核
    Ubuntu 内核升级,导致无法正常启动
    spring mvc 上传文件,但是接收到文件后发现文件变大,且文件打不开(multipartfile)
    angular5 open modal
    POJ 1426 Find the Multiple(二维DP)
    POJ 3093 Margritas
    POJ 3260 The Fewest Coins
    POJ 1837 Balance(二维DP)
    POJ 1337 A Lazy Worker
  • 原文地址:https://www.cnblogs.com/ivy-xu/p/12383689.html
Copyright © 2011-2022 走看看