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

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

程序员文章站 2022-06-23 23:19:17
一.抽象类 在某些情况下,父类知道其子类应该包含哪些方法,但是无法确定这些子类如何实现这些方法。这种有方法签名但是没有具体实现细节的方法就是抽象方法。有抽象方法的类只能被定义成抽象类,抽象方法和抽象类必须使用abstract修饰。抽象类里可以没有抽象方法。 1.1 抽象类和抽象方法 抽象类和抽象方法 ......

一.抽象类

在某些情况下,父类知道其子类应该包含哪些方法,但是无法确定这些子类如何实现这些方法。这种有方法签名但是没有具体实现细节的方法就是抽象方法。有抽象方法的类只能被定义成抽象类,抽象方法和抽象类必须使用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);
    }
}