进制人生程序员

菜鸟成长系列-多态、接口和抽象类

2018-02-09  本文已影响83人  glmapper_2018

面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。

多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

动态绑定

动态绑定是实现多态的技术,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的作用

消除类型之间的耦合关系。即:把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。

多态存在的三个必要条件

一、要有继承;
二、要有重写;
三、父类引用指向子类对象。

多态的优点

1.可替换性(substitutability)。多态对已存在代码具有可替换性。
2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。
3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

多态的实现方式

Java中多态的实现方式:

例子

无论工作还是学习中,笔都是我们经常用到的工具。但是笔的种类又非常的繁多,铅笔、签字笔、水笔、毛笔、钢笔...。现在我们要对“笔”进行抽象,抽象成一个抽象父类“Pen”

package com.glmapper.demo.base;

/**
 * 抽象父类:笔
 * @author glmapper
 */
public abstract class Pen {
    //笔的长度
    private int length;
    //颜色
    private String color;
    //类型
    private String type;
    //价格
    private double price;
    
    //写字
    public abstract void write(String cnt);
    
    public int getLength() {
        return length;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    
}

现在有两个子类,分别是:铅笔和钢笔。

铅笔类,继承父类Pen,并重写write方法

package com.glmapper.demo.base;
/**
 * 铅笔类 继承父类 笔(满足必要条件一:有继承【其实如果是接口的话,implement实现也是可以的】)
 * @author glmapper
 *
 */
public class Pencil extends Pen{
    /**
     * 父类的抽象方法委托子类具体实现:覆盖
     */
     //满足必要条件二:要有重写【当然,如果是对于write有重载也是可以的,不同的概念而已】
    @Override
    public void write(String cnt) {
        System.out.println("这是一只铅笔写的内容,内容是:"+cnt);
    }

}
package com.glmapper.demo.base;
/**
 * 钢笔类 继承父类 笔
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{

    @Override
    public void write(String cnt) {
        System.out.println("这是一支钢笔写的内容,内容是:"+cnt);
    }

}

测试:

package com.glmapper.demo.base;

public class MainTest {
    
    public static void main(String[] args) {
        
    /*  Pen pen= new Pencil();*/
        //必要条件三:父类引用指向子类对象。
        Pen pen= new Fountainpen();
        pen.write("我是一支笔");
        
    }
}

输出结果:这是一支钢笔写的内容,内容是:我是一支笔

说明

可替换性:多态对笔Pen类工作,对其他任何子类,如铅笔、钢笔,也同样工作。
可扩充性:在实现了铅笔、钢笔的多态基础上,很容易增添“笔”类的多态性。

接口

一个Java接口,就是一些方法特征的集合。【本文角度并非是java基础角度来说,主要是以设计模式中的应用为背景,因此对于相关定义及用法请自行学习。http://www.runoob.com/java/java-interfaces.html
我们在平时的工作中,提到接口,一般会含有两种不同的含义,

前者叫做“java接口”,后者叫着“接口”。例如:java.lang.Runnable就是一个java接口。

为什么使用接口

我们考虑一下,假如没有接口会怎么样呢?一个类总归是可以通过继承来进行扩展的,这难道不足以我们的实际应用吗?
一个对象需要知道其他的一些对象,并且与其他的对象发生相互的作用,这是因为这些对象需要借住于其他对象的行为以便于完成一项工作。这些关于其他对象的知识,以及对其他对象行为的调用,都是使用硬代码写在类里面的,可插入性几乎为0。如:钢笔中需要钢笔水,钢笔水有不同的颜色:
钢笔水类:

package com.glmapper.demo.base;
/**
 * 钢笔墨水
 * @author glmapper
 */
public class PenInk {
    //墨水颜色
    private String inkColor;

    public String getInkColor() {
        return inkColor;
    }

    public void setInkColor(String inkColor) {
        this.inkColor = inkColor;
    }

    public PenInk(String inkColor) {
        super();
        this.inkColor = inkColor;
    }
    
}

钢笔中持有一个墨水类的对象引用:

package com.glmapper.demo.base;
/**
 * 钢笔类 继承父类 笔
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{
    //引用持有
    PenInk ink =new PenInk("black");
    @Override
    public void write(String cnt) {
        System.out.println("钢笔墨水颜色是:"+ink.getInkColor());
        System.out.println("这是一支钢笔写的内容,内容是:"+cnt);
    }
}

但是这种时候,我们需要换一种颜色怎么办呢?就必须要对Fountainpen中的代码进行修改,将创建PenInk对象时的inkColor属性进行更改;现在假如我们有一个具体的类,提供某种使用硬代码写在类中的行为;
现在,要提供一些类似的行为,并且可以实现动态的可插入,也就是说,要能够动态的决定使用哪一种实现。一种方案就是为这个类提供一个抽象父类,且声明出子类要提供的行为,然后让这个具体类继承自这个抽象父类。同时,为这个抽象父类提供另外一个具体的子类,这个子类以不同的方法实现了父类所声明的行为。客户端可以动态的决定使用哪一个具体的子类,这是否可以提供可插入性呢?
改进之后的代码:
子类1:黑色墨水

package com.glmapper.demo.base;
/**
 * 黑色墨水
 * @author glmapper
 */
public class BlackInk extends PenInk{

    public BlackInk() {
        super("black");
    }
}

子类2:蓝色墨水

package com.glmapper.demo.base;
/**
 * 蓝色墨水
 * @author glmapper
 */
public class BlueInk extends PenInk{

    public BlueInk() {
        super("blue");
    }
}

钢笔类引用:

package com.glmapper.demo.base;
/**
 * 钢笔类 继承父类 笔
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{
    PenInk ink ;
    //通过构造函数初始化PenInk ,PenInk由具体子类来实现
    public Fountainpen(PenInk ink) {
        this.ink = ink;
    }
    @Override
    public void write(String cnt) {
        System.out.println("钢笔墨水颜色是:"+ink.getInkColor());
        System.out.println("这是一支钢笔写的内容,内容是:"+cnt);
    }
}

客户端调用:

public static void main(String[] args) {
        /**
         * 使用黑色墨水子类
         */
        Pen pen= new Fountainpen(new BlackInk());
        pen.write("我是一支笔");
        
    }

从上面代码可以看出,确实可以在简单的情况下提供了动态可插入性。

但是由于java语言是一个单继承的语言,换言之,一个类只能有一个超类,因此,在很多情况下,这个具体类可能已经有了一个超类,这个时候,要给他加上一个新的超类是不可能的。如果硬要做的话,就只好把这个新的超类加到已有的超类上面,形成超超类的情况,如果这个超超类的位置也已经被占用了,就只好继续向上移动,直到移动到类等级结构的最顶端。这样一来,对一个具体类的可插入性设计,就变成了对整个等级结构中所有类的修改。这种还是假设这些超类是我们可以控制的,如果某些超类是由一些软件商提供的,我们无法修改,怎么办呢?因此,假设没有接口,可插入性就没有了保证。

类型

java接口(以及java抽象类)用来声明一个新的类型。
java设计师应当主要使用java接口和抽象类而不是具体类进行变量的类型声明、参数的类型声明、方法的返还类型声明,以及数据类型的转换等。当然,一个更好的做法是仅仅使用java接口,而不要使用抽象java类来做到上面这些。在理想的情况下,一个具体java类应当只实现java接口和抽象类中声明过的方法,而不应该给出多余的方法。

TreeMap类有多个类型,它的主要类型是AbstractMap,这是一种java的聚集;而Cloneable接口则给出了一个次要类型,这个类型说明当前类的对象是可以被克隆;同时Serializable也是一个次要类型,它表明当前类的对象是可以被序列化的。而NavigableMap继承了SortedMap,因为之前说到过,子类型是可以传递的,因此对于TreeMap来说,SortedMap(或者说NavigableMap)表明这个聚集类是可以排序的。

接口的一些用法

public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
public interface Serializable {
}
package com.glmapper.demo.base;

public interface MyConstants {
    public static final String USER_NAME="admin";
};

这样一来,凡是实现这个接口的类都会自动继承这些常量,并且都可以像使用自己的常量一样,不需要再用MyConstants.USER_NAME来使用。

抽象类

在java语言里面,类有两种,一种是具体类,一种是抽象类。在上面给出的代码中,使用absract修饰的类为抽象类。没有被abstract修饰的类是具体类。抽象类通常代表一个抽象概念,它提供一个继承的出发点。而具体类则不同,具体类可以被实例化,应当给出一个有逻辑实现的对象模板。由于抽象类不可以被实例化,因此一个程序员设计一个新的抽象类,一定是用来被继承的。(不建议使用具体类来进行相关的继承)。

关于代码重构

假设有两个具体类,类A和类B,类B是类A的子类,那么一个比较简单的方案应该是建立一个抽象类(或者java接口),暂定为C,然后让类A和类B成为抽象类C的子类【没有使用UML的方式来绘制,请见谅哈】。

image.png

上面其实就是里氏替换原则,后面会具体介绍到的。这种重构之后,我们需要做的就是如何处理类A和类B的共同代码和共同数据。下面给出相关准则。

image.png

在一个继承等级结构中,共同的代码应当尽量向结构的顶层移动,将重复的代码从子类中抽离,放在抽象父类中,提高代码的复用率。这样做的另外一个好处是,在代码发生改变时,我们只需要修改一个地方【因为共同代码均在父类中】。

Has - A 与Is -A

当一个类是另外一个类的角色时【我 有一个 玩具】,这种关系就不应当使用继承来描述了,这个将会在后面说到的“合成/聚合复用原则”来描述。
Has - A: 我有一只笔(聚合)
Is - A:钢笔是一种笔(继承)

关于子类扩展父类的责任

子类应当扩展父类的职责,而不是置换掉或者覆盖掉超类的职责。如果一个子类需要将继承自父类的责任取消或者置换后才能使用的话,就很有可能说明这个子类根本不属于当前父类的子类,存在设计上的缺陷。

最后,说明下,我们在平时的工作中会经常使用的工具类,再次特地申明一下,我们也尽可能少的去从工具类进行继承扩展。

参考:

上一篇下一篇

猜你喜欢

热点阅读