• Java面向对象进阶篇(抽象类和接口)


    一.抽象类

    在某些情况下,父类知道其子类应该包含哪些方法,但是无法确定这些子类如何实现这些方法。这种有方法签名但是没有具体实现细节的方法就是抽象方法。有抽象方法的类只能被定义成抽象类,抽象方法和抽象类必须使用abstract修饰。抽象类里可以没有抽象方法。

    1.1 抽象类和抽象方法

          抽象类和抽象方法的规则如下:

          1.抽象类和抽象方法都必须使用abstract修饰符修饰,抽象方法不能有方法体

          2.抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使这个抽象类里不包含抽象方法。

          3.抽象类可以包含成员变量,方法(普通方法、抽象方法),构造器,初始化块,内部类(接口,枚举类)5中成分。抽象类的构造器不能用于创建实例,主要是用于被子类调用。

          4.含有抽象方法的类(直接定义了一个抽象方法;继承了一个父类,没有完全实现父类包含的抽象方法;或实现一个接口,但没有完全实现接口包含的抽象方法)只能被定义成抽象类。

        定义抽象方法只需在普通方法上加上abstract修饰符,并把普通方法的方法体全部去掉,并在方法后增加分号即可

        定义抽象类只需在普通类上增加abstract修饰符即可。

       抽象类不能用于创建实例,只能当作父类被其他子类继承。

    package com.company2;
    
    abstract class Shape{
        private String color;
        //定义一个计算周长的方法
        public abstract  double calPerimeter();
        //定义一个返回形状的方法
        public abstract  String getType();
    
        public Shape(String color) {
            this.color = color;
        }
    
        public Shape() {
        }
    
        public String getColor() {
            return color;
        }
    
        public void setColor(String color) {
            this.color = color;
        }
    }
    
    public class Triangle extends Shape{
        
        private double a;
        private double b;
        private double c;
    
        public Triangle(String color, double a, double b, double c) {
            super(color);
            setSides(a,b,c);
        }
        
        public void setSides(double a, double b, double c)
        {
            if(a+b > c || a+c > b || b+c > a){
                this.a = a;
                this.b = b;
                this.c = c;
            }
            System.out.println("三角形两边之和必须大于第三边");
        }
        @Override
        public double calPerimeter() {
            return a+b+c;
        }
    
        @Override
        public String getType() {
            return "三角形";
        }
    }

            当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须有子类提供实现(重写)。而final修饰的方法不能被重写,final修饰的类不能被继承。因此final

    和abstract永远不能同时使用。static和abstract不能同时修饰某个方法,没有类抽象方法的说法,但可以同时修饰内部类。abstract也不能修饰变量和构造器。

           利用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活。

    1.2 抽象类的作用

        从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象,它体现的是一种模板模式的设计。抽象类作为多个子类的通用模板,子类在抽象类的基础上进行

    拓展,改造,避免了子类设计的随意性。

    抽象类的普通方法可依赖与抽象方法,抽象方法则推迟到子类中提供实现

    package com.company2;
    
    abstract class Speedmeter
    {
        private double turnRate;//转速
    
        public Speedmeter() {
        }
        //把返回车轮半径的方法定义成抽象方法
        public abstract double getRadius();
        public void setTurnRate(double turnRate){
            this.turnRate = turnRate;
        }
        public double getSpeed()
        {
            return Math.PI*2*getRadius()*turnRate;
        }
    }
    public class CarSpeedmeter extends Speedmeter{
    
        @Override
        public double getRadius() {
            return 0.28;
        }
    
        public static void main(String[] args)
        {
            CarSpeedmeter csm = new CarSpeedmeter();
            csm.setTurnRate(15);
            System.out.println(csm.getSpeed());
        }
    }

    模板模式在面向对象的软件中很常用,其原理简单,实现也简单。下面是使用模板模式的一些简单规则:

    1.抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类实现。

    2.父类中可能包含需要调用其他系列方法的方法。这些被调用方法既可以由父类实现,也可以由子类实现。父类提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,必须

       依赖于其子类的帮助。

    二. Java 8改进的接口(interface)

    2.1 接口的概念

          接口是多个相似类中抽象出来的一组公共行为规范,接口不提供任何实现,它体现的是规范和实现分离的哲学。

         规范和实现分离正是接口的好处,它让软件系统各组件之间面向接口耦合,可以为软件系统提供很好的松耦合设计,从而降低个模块间的耦合,为系统提供更好的可拓展性和可维护性。

         接口可以有多个直接父接口,但接口只能继承接口,不能继承类

    2.2 接口的定义规则

          1.由于接口定义的是一种规范,用interface跟class区分,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量),方法(只能是抽象实例方法,类方法或

             默认方法), 内部类(内部接口,枚举)定义

           2.因为接口定义的是多个类共同的公共行为规范。因此接口的所有成员(包括常量,方法,内部类)都是public访问权限。只能指定public修饰符,也可以省略。

           3.接口定义的成员变量只能在定义时指定默认值,默认使用public static final修饰符修饰。可以省略不写。

           4.接口定义的方法只能是抽象方法,类方法和默认方法。类方法和默认方法都必须有方法实现(方法体)。

    下面定义一个接口

    package com.company2;
    
    public interface Output {
        //定义的成员变量只能是常量,默认public static final修饰
        int MAX_CACHE_LINE = 50;
        
        //接口里定义的普通方法只能是public的抽象方法,没有方法体
        void out();
        void getData(String msg);
        //接口里定义的默认方法,需要使用default修饰,默认public修饰,有方法体
        default void print(String... msgs)
        {
            for(String msg:msgs)
            {
                System.out.println(msg);
            }
        }
        
        default void test()
        {
            System.out.println("默认的test()方法");
        }
        //接口里定义的类方法,需要使用static修饰,默认public修饰,有方法体
        static String staticTest()
        {
            return "接口里的类方法";
        }
    }

    不同包下的另一个类访问接口里的成员变量。如下

    package com.company;
    
    import com.company2.Output;
    
    public class OutputFIeldTest {
        public static void main(String[] args)
        {
            System.out.println(Output.MAX_CACHE_LINE);
            System.out.println(Output.staticTest());
            
        }
    }

    2.3 接口的继承

    接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口拓展某个父接口,将会获得父接口里定义的所有抽象方法、常量

    一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以逗号隔开。

    2.4 使用接口

     接口的主要用途

     1.定义变量,也可用于强制类型转换。接口声明引用类型变量

     2.调用接口中定义的常量

     3.被其他类实现

           类可以使用implements关键字实现一个或多个接口,这也是Java为单继承灵活性不足所做的补充。实现接口与继承父类相似,一样可以获得所实现接口里定义的常量,方法。

           一个类可以继承一个父类,并同时实现多个接口,implements部分必须在extends部分之后。

           一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则该类将保留从父接口那里继承得到的抽象方法,该类也必须

    定义成抽象类。

           一个类实现某个接口时,该类将会获得接口中定义的常量,方法等。因此可以把实现接口理解为一种特殊的继承,相当于实现类完全继承了一个彻底抽象的类。

          接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。

    三. 接口和抽象类

    3.1 接口与抽象类的相似特征

         1.接口和抽象类都不能被实例化,用于被其他类实现和继承

         2.接口和抽象类都包含抽象方法,实现接口和继承抽象类的普通子类都必须实现这些抽象方法

    3.2 接口与抽象类的设计目的差别

         接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式);对于接口的调用者而言,接口规定了调用者

    可以调用哪些服务,以及如何调用这些服务(就是如何调用方法)。当一个程序中使用接口时,接口是多个模块间的耦合标准;在多个应用程序之间使用接口时,接口是多个程序之间的通讯标准。

         从某种程度上来看,接口类似于整个系统的总纲,它制定了各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一但接口发生改变,其影响是辐射性的,导致系统中的大部分都需

     要重写。

         抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那

    些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有进一步的改善。

    3.3 接口与抽象类的用法差别

        1.接口里只能包含抽象方法,静态方法,默认方法,不能为普通方法提供方法实现。抽象类则可以完全包含普通方法。

         2.接口里只能定义静态常量,不能定义普通成员变量。抽象类里即可以定义静态常量,也可以定义普通成员变量。

         3.接口里不包含构造器。抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

         4.接口里不能包含初始化块。抽象类则可以完全包含初始化块

         5.一个类最多只能有一个直接父类,包括抽象类。但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

    四. 面向接口编程

    很多软件架构设计理论都倡导面向接口编程来降低程序的耦合。下面介绍两种常用的场景来示范接口编程的优势

    4.1 简单工厂模式

    假设程序中有个Computer类需要组合一个输出设备。有两种选择,让Computer类组合一个Printer对象,或者让Computer类组合一个Output。

    假设让Computer类组合一个Printer对象,假如有一天系统需要重构,需要BetterPrinter来代替Printer,这就需要打开Computer类源代码进行修改,假如系统中有100个类组合了Printer,甚至1000个、

    10000个...,这是多么大的工作量啊

    为了避免这个问题,工厂模式建议让Computer类组合一个Output类型的对象,将Computer类和Printer类完全分离。Computer对象实际组合的是Printer对象还是BetterPrinter对象,对Computer而言

    完全透明。当Printer对象切换到BetterPrinter对象时,系统完全不受影响。

    package com.company2;
    
    public interface Output {
        //定义的成员变量只能是常量,默认public static final修饰
        int MAX_CACHE_LINE = 50;
    
        //接口里定义的普通方法只能是public的抽象方法,没有方法体
        void out();
        void getData(String msg);
        //接口里定义的默认方法,需要使用default修饰,默认public修饰,有方法体
        default void print(String... msgs)
        {
            for(String msg:msgs)
            {
                System.out.println(msg);
            }
        }
    
        default void test()
        {
            System.out.println("默认的test()方法");
        }
        //接口里定义的类方法,需要使用static修饰,默认public修饰,有方法体
        static String staticTest()
        {
            return "接口里的类方法";
        }
    }

    Output接口定义了一系列打印行为

    package com.company2;
    
    interface Product
    {
        int getProduceTime();
    }
    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);
            }
        }
    
        @Override
        public void getData(String msg) {
            if(dataNum >= MAX_CACHE_LINE)
            {
                System.out.println("输出队列已满,添加失败");
            }
            else{
                //把打印数据添加到队列里,已保存数据的数量加1
                printData[dataNum++] = msg;
            }
        }
    
        @Override
        public int getProduceTime() {
            return 45;
        }
    }

    Printer实现了Output接口

    package com.company2;
    
    public class Computer {
        private Output out;
    
        public Computer(Output out) {
            this.out = out;
        }
        public void keyIn(String msg)
        {
            out.getData(msg);
        }
    
        public void print()
        {
            out.out();
        }
    }

    Computer类完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Outputer对象,系统提供一个Ouputer工厂来负责生产Output对象。

    package com.company2;
    
    public class OutputFactory {
        public Output getOutput()
        {
            return new Printer();
        }
    
       public static void main(String[] args)
       {
           OutputFactory of = new OutputFactory();
           Computer c = new Computer(of.getOutput());
           c.keyIn("轻量级Java EE企业应用实战");
           c.keyIn("疯狂Java讲义");
           c.print();
       }
    
    
    }

    在该OutputFactory类中包含了一个getOutput()方法,返回Output实现类的实例,该方法负责创建Output实例

    package com.company2;
    
    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);
            }
        }
    
        @Override
        public void getData(String msg) {
            if(dataNum >= MAX_CACHE_LINE * 2)
            {
                System.out.println("输出队列已满,添加失败");
            }
            else{
                printData[dataNum++] = msg;
            }
        }
    }

    BetterPrinter类也实现了Output接口,因此也可当成Output对象使用,只要把OutputFactory工厂类的getOutput()方法中的下划线部分改为如下代码

    return new BetterPrinter();

    4.2 命令模式

    假设一个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。

    对于这样的一个需求,必须把处理行为作为参数传入该方法,这个“处理行为”用编程来实现就是一段代码。

    可以考虑使用一个Command接口定义一个方法,用这个方法来封装“处理行为”,但这个方法没有方法体,因为现在还无法确定这个处理行为

    public interface Command
    {
    void process(int[] target);
    }

    需要创建一个数组的处理类,在这个处理类包含一个process方法,这个方法还无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责

    对数组的处理行为

    public class ProcessArray
    {
    public void process(int[] target,Command cmd)
    {
      cmd.process(target);
    }
    }

    通过Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没提供真正的处理,只有等到需要

    调用ProcessArray对象的process()方法时,才真正传入一个Command对象,才确定对数组的处理行为。

    下面代码示范了对数组的两种处理方式

    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());
    //第二次处理数组,具体处理行为取决于AddCommand
    pa.process(target,new AddCommand());
    }
    } 

    两次不同的处理行为通过PrintCommand类和AddCommand类提供

    package com.company2;
    
    public class PrintCommand implements Command{
        public void process(int[] target)
        {
            for(int tmp:target)
            {
                System.out.println("迭代输出数组的元素"+tmp);
            }
        }
    }
    package com.company2;
    
    public class AddCommand implements Command{
        public void process(int[] target)
        {
            int sum = 0;
            for(int tmp:target)
            {
                sum += tmp;
            }
            System.out.println("数组的元素总和为"+sum);
        }
    }
  • 相关阅读:
    使用函数式语言实践DDD
    理解函数式编程中的函数组合--Monoids(二)
    理解函数式编程语言中的组合--前言(一)
    信息熵 交叉熵
    激光雷达感知方案
    卡尔曼滤波算法
    我平时用的 golang 项目结构
    记一个 aliyun tablestore go client 的大坑
    关于 signal.Notify 的一个小问题
    【Linux】【Jenkins】Linux环境搭建和遇到的问题和解决方案等
  • 原文地址:https://www.cnblogs.com/yumiaoxia/p/9012050.html
Copyright © 2020-2023  润新知