抽象类 接口 匿名类的区别

1 抽象类                                                                 

  当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些情况下,某个父类并不需要实现,因为它只需要当做一个模板,而具体的实现,可以由它的子类来实现。比如说一个长方体的表面积和一个立方体的表面积计算方式是有区别的,长方体表面积需要有三个参数,而立方体需要一个参数。
  抽象方法可以只有方法签名,而没有方法实现。

1.1 抽象方法和抽象类的定义  

  抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下:
  1.抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。连大括号都不能用
  2.抽象类不能被实例化(实例化是为了调用属性和方法,抽象类本身没有方法实现的),无法使用new关键字来调用抽象类的构造器来初始化抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
  3.抽象类可以包含属性、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类、枚举类六种成分。抽象类的构造器不能用于创建实例,主要用于被其子类调用
  4.含义抽象方法的类(包括直接定义了一个抽象方法:继承了一个抽象父类,但没有完全实现父类包含的抽象方法;以及实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
根据定义规则:普通类不能包含抽象方法,而抽象类不仅可以包含普通方法,也可以包含抽象方法。

复制代码
abstract class 类名称{

  属性;

    权限类型  返回值类型 方法名称(参数类别 参数列表){

}

    权限类型 abstract  返回值类型 抽象方法名称(参数类型 参数列表)

}

  注意些抽象方法时候:只能是声明,后面连大括号{}都不能跟。大括号里面哪怕什么都没有,也表示是空语句,不是抽象方法。

  一个抽象类不能用final来声明,因为抽象类是需要继承来覆写抽象类的方法,但是final关键字意味着抽象类不能有子类,显然矛盾。

  抽象类的抽象方法也不能用private声明,因为抽象类的抽象方法需要子类覆写,使用private修饰的话,子类无法覆写抽象方法。

  抽象方法和空方法体的方法不是同一个概念。例如public abstract void test();是一个抽象方法,它根本没有方法体,即方法定义后面没有一对花括号;但public void test(){}方法是一个普通方法,它已经定义了一个方法体,只是方法体为空,方法体上面也不做,这个方法不能用abstract来修饰。
abstract不能用于修饰属性,不能用于修饰局部变量,即没有抽象属性、抽象变量的说法,abstract也不能用于修饰构造器,抽象类里定义的构造器只能是普通构造器。
  除此之外,当使用static来修饰一个方法时,表面这个方法属于当前类,即该方法可以通过类来调用,如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误),因此static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
  abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时使用。

package chapter6;

import java.util.Scanner;

public class Circle extends Shape{
    private double radius;
    public Circle(String color,double radius){
        super(color);
        if(radius > 0){
            this.radius = radius;
        }
    }
    public void setRadius(double radius){
        if(radius > 0){
            this.radius = radius;
        }
    }
    //重写计算园周长的方法
    public double calPerimeter(){
        return 2 * Math.PI * radius;
    }
    public String getType(){
        return getColor() + "圆";
    }
    
    public static void main(String[] args){
        Triangle s1 = new Triangle("红色",3,4,5);
        Circle s2 = new Circle("紫色",5);
        System.out.println(s1.getType() + " 周长是" + s1.calPerimeter());
        System.out.println(s2.getType() + " 周长是" + s2.calPerimeter());
        
    }
}

1.2 抽象类的作用                  

  从前面的实例可以看出,抽象类不能创建实例,它只能当成父类被继承。从语义角度来看,抽象类是从多个具体类抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
  抽象类提醒的就是一种模板模式的设计,抽象类作为子类的模板,子类在抽象类的继承上进行扩展和改造。
  如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给子类实现,这就是一种模板模式,模板模式也是最常见、最简单的设计模式之一。如前面的Shape、Circle和Triangle就是使用这种模式。

2 接口:更彻底的抽象                                             

  抽象类是从多个类抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的"抽象类"——接口(interface),接口里不能包含普通方法,接口里所有方法都是抽象方法。

2.1 接口的概念                   

  经常听说接口,比如PCI接口、AGP接口,因此很多人认为接口等同于主机板上的插槽,这是一种错误的认识。当我们说PCI接口时,指的是主机板上那条插槽遵守的了PCI规范,而具体的PCI插槽只是PCI接口的实例。
  对于不同型号的主机板而言,它们各自的PCI插槽都需要遵守一个规范,遵守这个规范就可以保证插入该插槽里的板卡能与主机板正常通信。对于同一个型号的主机板而言,它们的PCI插槽都需要相同的数据交换方式、相同的实现细节,它们都是同一个类的不同实例。
  下图展示了三种层次的关系:接口层次、类层次和对象层次


  从上图可以看出,同一个类的内部状态数据,各种方法的实现细节完全相同,类是一种具体实现体。而接口定义了一种规范,接口定义某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不需要关系这些类里方法的实现细节。它只规定这批类里必须提供某些方法,提供这些方法的类就可满足实际需要。
可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计这些。

  让规范和实现分离是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。例如主机板上提供了PCI插槽,只要一块显卡遵守PCI接口规范,就可以插入PCI插槽内,与该主板正常通信。至于这块显卡是哪个厂家制造的,内部如果实现,主机板无需关心。
  类似的,软件系统的各模块之间也应该采用面向接口的耦合,尽量降低各模块之间的耦合,为系统提供更好的可扩展性和可维护性。
  因此接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组共用方法。

2.2 接口的定义                        

  和类定义不同,定义接口不再使用class关键字,而是使用interface关键字,接口定义的基本语法是:

修饰符 interface 接口名 extends 父接口1,父接口2...{
        零到多个常量定义...
        领个到多个抽象方法定义...
    }

语法的详细说明如下:
  1.修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
  2.接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要合法即可,但实际是需要是有意义。
  3.一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
  接口是一种规范,因此接口里不能包含构造器和初始化块定义,接口里只能包含常量属性、抽象实例方法(不能有static)、内部类(包括内部接口)和枚举类定义。
  由于接口是一种公共规范,因此接口里面的所有成员包括常量、抽象方法、内部类和枚举类都是public访问权限。定义接口成员时候,可以省略控制修饰符,但如果指定,则只能是public。对于接口里定义的常量属性而言,它们是接口相关的,它们只能是常量,因此系统会自动为这些属性增加static和final两个修饰符。也就是说,在接口定义属性时,不管是否使用publicstatic final修饰符,接口里的属性总将使用这三个修饰符来修饰。而且由于接口里没有构造器和初始化块,因此接口里定义的属性只能定义时指定默认值。
  接口里定义属性采用如下两行代码的结果完全一样:

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

  接口定义的抽象方法修饰符是public abstract,不能有static修饰,内部类和枚举类都默认采用public static两个修饰符,不管定义时是否只能这两个修饰符,系统自动使用public static 对它们进行修饰。

package chapter6;

public abstract interface Output {
    //接口定义的属性只能是常量
    int MAX_CACHE_LINE = 50;
    //接口里定义只能是抽象方法
    void out();
    void getData(String name);
}
package chapter6;

public class TestOutputProperty {
    public static void main(String[] args){
        //访问另一个包中的属性        System.out.println(chapter6.Output.MAX_CACHE_LINE);
        //不能重新赋值,因为接口的属性都是final的
        //Output.MAX_CACHE_LINE = 3;    }
}

2.3接口的继承                      

  接口的继承和类继承不同,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将获得父接口的所有抽象方法、常量属性、内部类和枚举类定义。 
  一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号隔开。

package chapter6;

interface interfaceA{
    int PROP_A = 5;
    void test_A();
}

interface interfaceB{
    int PROP_B = 6;
    void test_B();
}

interface interfaceC extends interfaceA,interfaceB{
    int PROP_C = 6;
}
public class TestInterfaceExtends {
    public static void main(String[] args){
        System.out.println(interfaceA.PROP_A);
        System.out.println(interfaceB.PROP_B);
        System.out.println(interfaceC.PROP_C);
    }
}

2.4 使用接口                      

  接口不能用于创建实例,但接口可以用于声明引用类型的变量。当使用接口来声明引用类型的变量时,这个引用类型的变量必须引用到其实现类的对象。除此之外,接口主要用途是被实现类实现。
  一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式:

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

  实现接口与继承父类相似,一样可以获得所实现接口里定义常量属性、抽象方法、内部类和枚举类定义。
  让类实现接口需要类定义后增加implements部分,当需要实现多个接口时,多个接口之间以英文逗号(,)隔开。一个类可以继承一个父类,并同时实现多个接口,implements部分必须放在extends部分之后。
  一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义抽象类。下面看一个实现接口的类。

import lee.Output;

//定义一个Product接口interface Product
{
    int getProduceTime();
}
//让Printer类实现Output和Product接口public class Printer implements Output , Product
{
    private String[] printData 
        = new String[MAX_CACHE_LINE];
    //用以记录当前需打印的作业数
    private int dataNum = 0;
    public void out()
    {
        //只要还有作业,继续打印
        while(dataNum > 0)
        {
            System.out.println("打印机打印:" + printData[0]);
            //把作业队列整体前移一位,并将剩下的作业数减1
            System.arraycopy(printData , 1
                , printData, 0, --dataNum);
        }
    }
    public void getData(String msg)
    {
        if (dataNum >= MAX_CACHE_LINE)
        {
            System.out.println("输出队列已满,添加失败");
        }
        else
        {
            //把打印数据添加到队列里,已保存数据的数量加1。
            printData[dataNum++] = msg;
        }
    }
    public int getProduceTime()
    {
        return 45;
    }
    public static void main(String[] args)
    {
        //创建一个Printer对象,当成Output使用
        Output o = new Printer();
        o.getData("轻量级Java EE企业应用实战");
        o.getData("疯狂Java讲义");
        o.out();
        o.getData("疯狂Android讲义");
        o.getData("疯狂Ajax讲义");
        o.out();
        //创建一个Printer对象,当成Product使用
        Product p = new Printer();
        System.out.println(p.getProduceTime());
        //所有接口类型的引用变量都可直接赋给Object类型的变量
        Object obj = p;
    }
}

  实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或相等,所以实现类实现接口里的方法时只能使用public访问权限。
  接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。所以上面的程序可以把Product类型变量直接赋给Object类型的变量,这是利用向上转型实现,因为编译器知道任何Java对象都必须是Object或其子类的实例。

2.5 接口和抽象类对比            

  接口和抽象都很像,它们都具有如下特征:
  1. 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  2. 接口和抽象类可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

  但接口和抽象类之间的差别很大,这种差别主要体现在二者设计目的上,下面具体分析二者的差别。
  接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
但接口和抽象类之间的差别非常大,这种差别主要体现在二者的设计目的上,下面具体分析两者的差别。
  接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
  从某种程度上来看,接口类似于整个系统的总纲,制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
  抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象父类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有跟进一步的完善,这种完善可能有几种不同方式。
除此之外,接口和抽象类在用法上也存在如下区别。
  1. 接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则完全可以包含普通方法。
  2. 接口里不能定义静态方法;抽象类里可以定义静态方法。
  3. 接口里只能定义静态常量属性,不能定义普通属性;抽象类里则既可以定义普通属性,也可以定义静态常量属性。
  4. 接口不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而让其子类调用这些构造器来完成属于抽象类的初始化操作。
  5. 接口里不能包含初始化块,但抽象类则完全可以包含初始化块。
  6. 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

2.6 面向接口编程

  接口体现的是一种规范和设计分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。
基于这种原则,很多软件架构设计理论都倡导面向接口编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。下面将介绍两种常用场景来示范面向接口编程的优势。

2.6.1 工场模式                       

   有这样的场景,假设程序中有个Computer类需要组合一个输出设备,现在又两个选择:直接让Computer该类组合衣柜Printer属性,或者让Computer组合一个Output属性,采用哪种方式更好呢?

  假设让Computer组合一个Printer属性,如果有一天系统需要重构,需要使用BetterPrinter来代替Printer,于是需要打开Computer类源代码进行修改。如果系统中只有一个Computer类组合了Printer属性,那么我们只需要修改这一个Computer类就可以了,但是如果有1000个、10000个类甚至更多的类,那么就需要一个个打开这么多文件进行修改(并不是这么多类都放在一个word文档里面进行批量替换这么简单的)。这种工作量非常之大。为了避免这个问题,我们让Computer组合一个Output属性,将Computer类与Printer类完全分离。Computer对象实际组合的是Printer对象,还是BetterPrinter对象,对Computer而言是屏蔽的,由Output来进行耦合Computer和Output属性
理清一下思路:
  假设现在一个应用程序要打印一份文件,那么这个应用程序可以调用windows平台开放的打印程序,而一台计算机上可以连接多个不同类型的打印机,这样打印程序就可以接口的形式来调用具体实现打印功能的类
示例:
下面这个程序可以当做windows平台的打印程序,用于开放给应用程序

package chapter6;

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接口来耦合,也就是使用Output来生产(创建)Printer类对象来供Computer类来使用。不再用Computer来生产。

package chapter6;

public class OutputFactory {
    //制造Printer对象
    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讲义");
        c.keyIn("简明德语语法");
        c.print();
    }
}


public class Printer implements Output{
    private String[] printData = new String[MAX_CACHE_LINE];
    //记录当前需要打印的页数
    private int dataNum = 0;
    public void out(){
        //只要页数大于0继续打印
        while(dataNum > 0){
            System.out.println("打印机打印:" + printData[0]);
            //作业队列整体前移一位,并将剩下的作业减1
            System.arraycopy(printData, 1, printData, 0, --dataNum);
        }
    }
    public void getData(String msg){
        if(dataNum >= MAX_CACHE_LINE){
            System.out.println("输出队列已满,添加失败");
        }else{
            printData[dataNum++] = msg;
        }
    }
}

  我们可以把上面的Printer代码修改成BetterPrinter,并且在OutputFactory程序中修改制造对象。

2.6.2 命令模式                       

  有这样的场景:某个方法需要完成某个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组时候还需要做什么操作,需要在调用该方法时指定具体的处理行为。这个看起来很奇怪,难道调用函数时候,能够把行为作为一个参数?在某些编程语言,如Ruby,这种特性是支持的,在Java里面不支持,但是可以间接做到。
  Java是不允许代码块单独存在的,因此可以使用一个Command接口来定义一个方法来封装处理行为。

package chapter6;

public interface Command {
    //定义封装抽象的行为
    void process(int[] target);
    
}
package chapter6;

public class ProcessArray {
    public void process(int[] target,Command cmd){
        cmd.process(target);
    }
}
  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值