欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java抽象类

程序员文章站 2024-02-17 16:35:40
...

参考资料

[1]. 疯狂Java讲义(第三版) 李刚

抽象类

抽象方法和抽象类

抽象类可以继承非抽象类
基本规则

// 抽象类必须使用abstract修饰符来定义,即成为抽象类
// 这个类里面可以没有抽象方法
public abstract class Shape {
    // 抽象方法必须使用abstract修饰符来修饰,且不能有方法体
    public abstract void draw(Canvas c);
}

// 只要使用了abstract修饰符,这个类就无法被实例化
// 基本成员与普通类一样,但主要用于被其子类实现

下面定义一个Shape抽象类,用于被其他类继承:

/**
 * AbstractShape class
 *
 * @author shuaige
 * @date 2018/01/22
 */
public abstract class AbstractShape {

    /**
     * 初始化块
     */
    {
        System.out.println("执行Shape的初始块");
    }
    /**
     * color 颜色
     */
    private String color;
    /**
     * 定义一个计算周长的抽象方法
     *
     * @return 周长
     */
    public abstract double calPerimeter();

    /**
     * 定义一个返回形状的抽象方法
     * @return 返回形状
     */
    public abstract String getType();

    /**
     * 定义Shape的构造器,该构造器并不是用于创建Shape对象,而是被子类调用
     */
    public AbstractShape(){}

    /**
     * 构造器
     * @param color 颜色
     */
    public AbstractShape(String color)
    {
        System.out.println("执行Shape的构造器...");
        this.color = color;
    }

    /**
     * 返回颜色
     * @return 颜色
     */
    public String getColor()
    {
        return color;
    }

    /**
     * 设置颜色
     * @param color 颜色
     */
    public void setColor(String color)
    {
        this.color = color;
    }
}

声明一个Triangle子类,用于实现上面的父类:

/**
 * Triangle class
 *
 * @author shuaige
 * @date 2018/01/22
 */
public class Triangle extends AbstractShape {

    /**
     * 三角形的a边
     */
    private double a;
    /**
     * 三角形的b边
     */
    private double b;
    /**
     * 三角形的c边
     */
    private double c;

    /**
     * 构造器
     * @param color 颜色
     * @param a 三角形的a边
     * @param b 三角形的b边
     * @param c 三角形的c边
     */
    public Triangle(String color, double a, double b, double c)
    {
        super(color);
        this.setSides(a, b, c);
    }

    /**
     * 设置边
     * @param a 三角形的a边
     * @param b 三角形的b边
     * @param c 三角形的c边
     */
    public void setSides(double a, double b, double c)
    {
        if (a >= b+c || b >= a+c || c >= a+b)
        {
            System.out.println("三角形的两边之和必须大于第三边");
            return;
        }
        // 如果符合三角形的规则,则设置下面的值
        this.a = a;
        this.b = b;
        this.c = c;
    }

    /**
     * 重写Shape类的计算周长的抽象方法
     * @return 返回周长
     */
    @Override
    public double calPerimeter() {
        return a + b + c;
    }

    /**
     * 重写Shape类的返回形状的抽象方法
     * @return 返回形状
     */
    @Override
    public String getType() {
        return "三角形";
    }
}

声明一个Circle类,用于实现上面的父类:

/**
 * Circle class
 *
 * @author shuaige
 * @date 2018/01/22
 */
public class Circle extends AbstractShape {
    /**
     * 半径
     */
    private double radius;

    /**
     * 构造器
     * @param color 颜色
     * @param radius 半径
     */
    public Circle(String color, double radius)
    {
        super(color);
        this.radius = radius;
    }

    /**
     * 设置半径
     * @param radius 半径
     */
    public void setRadius(double radius)
    {
        this.radius = radius;
    }

    /**
     * 计算周长
     * @return 返回周长
     */
    @Override
    public double calPerimeter() {
        return 2 * Math.PI * radius;
    }

    /**
     * 返回形状
     * @return 形状
     */
    @Override
    public String getType() {
        return getColor() + "圆形";
    }

    /**
     * 实例化过程
     * @param args
     */
    public static void main(String[] args)
    {
        // 三角形
        AbstractShape shape1 = new Triangle("黑色", 3, 4, 5);
        System.out.println(shape1.getType());
        System.out.println(shape1.calPerimeter());


        // 圆形
        AbstractShape shape2 = new Circle("黄色", 3);
        System.out.println(shape2.getType());
        System.out.println(shape2.calPerimeter());
    }
}

上面的main()方法中定义了两个Shape类型的引用变量,它们分别指向Triangle对象和Circle对象。由于在Shape类中定义了calPerimeter()方法和getType()方法,无须强制类型转换为其子类型。
利用抽象类和抽象方法的优势,可以更好的发挥多态的优势,使程序更加灵活。
abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、抽象成员变量的说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通的构造器。

抽象类的作用

抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
父类:

/**
 * CarSpeedMeter class
 *
 * @author shuaige
 * @date 2018/01/22
 */
public abstract class AbstractSpeedMeter {
    /**
     * trunRate 转速
     */
    private double trunRate;
    public AbstractSpeedMeter()
    {

    }

    /**
     * 把返回车轮半径的方法定义成抽象方法
     *
     * @return null
     */
    public abstract double getRadius();

    public void setTrunRate(double trunRate)
    {
        this.trunRate = trunRate;
    }


    /**
     * 定义计算速度的通用算法
     *
     * @return 速度等于 车轮半径  * 2 * PI * 转速
     */
    public double getSpeed()
    {
        return Math.PI * 2 * getRadius() * trunRate;
    }
}

实现了父类的子类:

package com.qunar;


/**
 * CarSpeedMeter class
 *
 * @author shuaige
 * @date 2018/01/22
 */
public class CarSpeedMeter extends AbstractSpeedMeter{


    @Override
    public double getRadius() {
        return 0.28;
    }

    public static void main(String[] args)
    {
        CarSpeedMeter csm = new CarSpeedMeter();
        csm.setTrunRate(15);
        System.out.println(csm.getSpeed());
    }
}

抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
父类中可能包含需要调用其他系列方法的方法,这些被调用方法既可以由父类实现,也可以由其子类实现,父类里提供的方法只是定义了一个通用的算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。

Java 8 改进的接口

接口(interface),接口里不能包含普通方法,接口里的所有方法都是抽象方法,。Java 8对接口进行了改进,运行在接口中定义默认方法,默认方法可以提供方法实现。

Java 8中接口的定义

基本语法如下:

[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
    零到多个常量定义...
    零到多个抽象方法定义...
    零到多个内部类、接口、枚举定义...
    零到多个默认方法或类方法定义...
}

语法说明如下:
1. 修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
2. 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。
下面两个变量效果一样。

int MAX_SIZE = 50;
public static final int MAX_SIZE = 50;

系统自动为普通方法增加public abstract修饰符,因此下面两个方法效果一样。

int size();
public abstract int size();

默认方法必须使用default修饰,并且无论是否指定,系统总会默认使用public修饰。
类方法必须使用static方法修饰,并且无论是否指定,系统总会默认使用public修饰。
下面定义一个接口示例:

/**
 * 示范接口 Output
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface Output {
    /**
     * 接口里定义的成员变量只能是常量
     * 这个变量是一个类变量,因为默认修饰为public static final
     */
    int MAX_CACHE_LINE = 50;
    /**
     * 接口里定义的普通方法只能是public的抽象方法
     */
    void out();

    /**
     * 接口里定义的普通方法只能是public的抽象方法
     * @param msg 字符串参数
     */
    void getData(String msg);

    /**
     * 在接口中定义默认方法,需要使用default修饰
     * @param msgs 字符串参数
     */

    default void print(String... msgs)
    {
        for (String msg : msgs)
        {
            System.out.println(msg);
        }
    }

    /**
     * 在接口中定义默认方法,需要使用default修饰
     */
    default void test()
    {
        System.out.println("默认的test()方法");
    }

    /**
     * 接口中定义的类方法,需要使用static修饰
     * @return
     */
    static String staticTest()
    {
        return "接口里的类方法";
    }
}

调用

// 访问接口里的常量
System.out.println(Output.MAX_CACHE_LINE);
// 下面语句将引发错误
// Cannot assign a value to final variable'MAX_CACHE_LINE'
// Output.MAX_CACHE_LINE = 100;
// 调用默认类
System.out.println(Output.staticTest());

接口的继承

接口支持多继承,子接口扩展某个父接口后,将会获得父接口里定义的所有抽象方法和变量。
interfaceA 接口

/**
 * interfaceA interface
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface InterfaceA {
    /**
     * PROP_A常量
     */
    int PROP_A = 5;

    /**
     * testA方法
     */
    void testA();
}

interfaceB接口

/**
 * interfaceB interface
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface InterfaceB {
    /**
     * PROP_B常量
     */
    int PROP_B = 6;
    /**
     * testB方法
     */
    void testB();
}

interfaceC接口

/**
 * interfaceC interface
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface InterfaceC extends InterfaceA, InterfaceB{
    /**
     * PROP_C常量
     */
    int PROP_C = 6;

    /**
     * testC方法
     */
    void testC();

}

现在来使用interfaceC来调用A和B的接口内的常量

System.out.println(InterfaceC.PROP_A);
System.out.println(InterfaceC.PROP_B);
System.out.println(InterfaceC.PROP_C);

使用接口

接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。接口的用途归纳如下:
1. 定义引用型变量,也可用于进行强制类型转换。
2. 调用接口中定义的常量。
3. 被其他类实现。

类实现接口的语法格式如下:

[修饰符] class 类名 extends 父类 implements 接口1, 接口2...
{
    类体部分
}

下面进行一个实例
接口Product

/**
 * Product interface
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface Product {
    /**
     * 返回时间
     * @return 返回整数
     */
    int getProduceTime();
}

当子类实现了接口以后,还可以引用变量给父类,这样就可以使多个实现的子类之间兼容。

/**
 * Printer interface
 * 实现了Output和Product接口
 * @author shuaige
 * @date 2018/01/23
 */
public class Printer implements Output, Product{

    private String[] printData = new String[MAX_CACHE_LINE];

    /**
     * 用以记录当前需打印的作业数
     */
    private int dataNum = 0;

    /**
     * 输出
     */
    @Override
    public void out() {
        // 只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("打印机打印:" + printData[0]);
            // 把作业队列整体前移一位,并将剩下的作业数减1
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }

    /**
     *
     * @param msg 字符串参数
     */
    @Override
    public void getData(String msg) {
        if (dataNum >= MAX_CACHE_LINE)
        {
            System.out.println("输出队列已满,添加失败");
        }else{
            // 把打印数据添加到队列里,已保存数据的数量加1
            printData[dataNum++] = msg;
        }
    }

    /**
     * 返回整数
     * @return
     */
    @Override
    public int getProduceTime() {
        return 45;
    }

    public static void main(String[] args)
    {
        // 创建一个Printer对象,当成Output使用
        Output o = new Printer();
        o.getData("a");
        o.getData("b");
        o.out();
        o.getData("c");
        o.getData("d");
        o.out();
        // 调用Output接口中定义的默认方法
        o.print("1", "2", "3");
        o.test();
        // 创建一个Printer对象,当成Product使用
        Product p = new Printer();
        System.out.println(p.getProduceTime());
        // 所有接口类型的引用变量都可直接赋给Object类型的变量
        Object obj = p;
    }
}

从上面程序中可以看出,Printer类实现了Output接口和Product接口,因此Printer对象既可直接赋给Output变量,也可直接赋给Product变量。仿佛Printer类既是Object类的子类,也是Product类的子类,这就是Java提供的模拟多继承。

面向接口编程

接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好的降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。

简单工厂模式

实现类

/**
 * Computer class
 * 接受注入的Output类型class,然后调用它的方法
 * @author shuaige
 * @date 2018/01/23
 */
public class Computer {
    /**
     * Output接口
     */
    private Output out;


    /**
     *
     * @param out Output接口
     */
    public Computer(Output out)
    {
        this.out = out;
    }

    /**
     * 定义一个模拟获取字符串输入的方法
     * @param msg
     */
    public void keyIn(String msg)
    {
        out.getData(msg);
    }

    /**
     * 定义一个模拟打印的方法
     */
    public void print()
    {
        out.out();
    }
}

工厂类

/**
 * OutputFactory class
 * 工厂类,获取Output类型的类
 * @author shuaige
 * @date 2018/01/23
 */
public class OutputFactory {

    /**
     * 返回一个Output接口的实现类
     * @return Output接口的实现类
     */
    public Output getOutput()
    {
        return new Printer();
    }

    public static void main(String[] args)
    {
        // 工厂类,获取Output类型的类
        OutputFactory of = new OutputFactory();
        // 注入到实现类,供实现类调用
        Computer c = new Computer(of.getOutput());
        // 调用keyIn方法
        c.keyIn("a");
        // 调用print方法
        c.print();
    }
}

类之间的关联关系图如下:
Java抽象类
工厂类组合输出类,计算机类又依赖工厂类。
假设现在Printer类废弃了,现在新增了一个BetterPrinter实现类,只需要把OutputFactory里面的getOutput()方法即可。

/**
 * OutputFactory class
 * 工厂类,获取Output类型的类
 * @author shuaige
 * @date 2018/01/23
 */
public class OutputFactory {
    /**
     * 返回一个Output接口的实现类
     * @return Output接口的实现类
     */
    public Output getOutput()
    {
        return new BetterPrinter();
    }

    ...
}

BetterPrinter实现类也需要实现Output接口,代码如下:

package com.qunar;
/**
 * BetterPrinter class
 * 实现了Output和Product接口
 * @author shuaige
 * @date 2018/01/23
 */
public class BetterPrinter implements Output{
    /**
     * 打印数量
     */
    private String[] printData = new String[MAX_CACHE_LINE * 2];
    /**
     * 用以记录当前需打印的作业数
     */
    private int dataNum = 0;

    /**
     * 输出
     */
    @Override
    public void out() {
        // 只要还有作业,就继续打印
        while(dataNum > 0)
        {
            System.out.println("高速打印机正在打印:" + printData[0]);
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }

    /**
     * 获取数据
     * @param msg 字符串参数
     */
    @Override
    public void getData(String msg) {
        if (dataNum >= MAX_CACHE_LINE * 2)
        {
            System.out.println("输出队列已满,添加失败");
        }else {
            // 把打印数据添加到队列中,已保存数据的数量加1
            printData[dataNum++] = msg;
        }
    }
}

现在类图结构如下:
Java抽象类

命令模式

假设某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定,示例代码如下:
接口Commond :

/**
 * Commond interface
 *
 * @author shuaige
 * @date 2018/01/23
 */
public interface Commond {
    /**
     * 接口里定义的process方法用于封装“处理行为”
     * @param target
     */
    void process(int[] target);
}

实现类PrintCommand:

/**
 * PrintCommand class
 *
 * @author shuaige
 * @date 2018/01/23
 */
public class PrintCommand implements Commond{
    /**
     * 处理数组
     * @param target
     */
    @Override
    public void process(int[] target)
    {
        for (int tmp : target)
        {
            System.out.println("迭代输出目标数组的元素:" + tmp);
        }
    }
}

实现类AddCommand:

/**
 * AddCommand class
 *
 * @author shuaige
 * @date 2018/01/23
 */
public class AddCommand implements Commond{
    /**
     * 处理数组
     * @param target
     */
    @Override
    public void process(int[] target)
    {
        // 设置初始值
        int sum = 0;
        for (int tmp : target)
        {
            // 遍历相加所有元素
            sum += tmp;
        }
        System.out.println("数组元素的总和是:" + sum);
    }
}

组合类:

/**
 * ProcessArray class
 *
 * @author shuaige
 * @date 2018/01/23
 */
public class ProcessArray {
    /**
     * 组合Commond实现和传入的int[]参数
     * @param target
     * @param cmd
     */
    public void process(int[] target, Commond cmd)
    {
        cmd.process(target);
    }
}

调用示例:

/**
 * CommandTest class
 *
 * @author shuaige
 * @date 2018/01/23
 */
public class CommandTest {
    public static void main(String[] args)
    {
        ProcessArray pa = new ProcessArray();
        int[] target = {3, -4, 6, 4};
        // 第一次处理数组,具体行为取决于PrintCommand
        pa.process(target, new PrintCommand());
        System.out.println("---------");
        // 第一次处理数组,具体行为取决于AddCommand
        pa.process(target, new AddCommand());

    }
}

Commond是接口,然后PrintCommand类和AddCommand类都实现了它的process方法,最后由ProcessArray类组合PrintCommand类或者AddCommand类和数组就可以实现命令模式,即传入的是哪个处理类,就执行哪个处理类的逻辑。
类图如下:
Java抽象类