详解抽象类和接口

前言:

在JAVA中经常会接触到接口和抽象类这两个概念,对于一些刚接触的人来说简直有时候懵逼得分不清两者的区别。最近在复习总结一些基础知识时也进过这两个东西的一些坑,翻阅一下课本和网上的一些零散资料来总结和详解一下接口和抽象类。先分别来认识一下各自的特点再来进行两者的一些比较,看到网上大多都只是写着两者的区别而没有解析其中的一些原因,所以很多时候都只能单凭靠记忆了,这样的效果不是很理想。

一、抽象类

我们可能常说“好抽象啊,能不能说详细点啊”,这里的抽象和JAVA里的抽象也差不多了。抽象只是一个泛指并没有具体的实体和实现,描述的内容越少就越抽象、外延越大。放在JAVA的面向对象的概念中理解,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。eg:我说“交通工具“,但你不知道我说的是哪一种交通工具吧,可能是汽车、火车、飞机、轮船这些工具,那么这“交通工具”就是一个抽象类,因为它没有足够信息让我们知道它是一种什么样的“交通工具”。

抽象类一种专门用来当做父类的类,其功能类似于我们所说的“模板”(模板设计模式亦由此延伸)。那么好了,既然是模板那么我们使用的时候就是Ctrl+C->修修改改->Ctrl+V了,其中Ctrl+C对应的是继承,“修修改改”对应重写覆盖,Ctrl+V对应实体实现。那么第一个问题来了,抽象类能直接实例化吗?答案是:

  • 抽象类不能被直接实例化,只能通过抽象类派生出的非抽象类来创建对象和实例化。

这就好比如直接去实现一个”模板“,这样的操作是毫无意义的,这样就和普通类没有质的区别了。接下来用代码来介绍一下抽象类的一些特点吧。

首先是抽象类的定义格式:

abstract class 抽象类名称{
    属性;
    [访问权限] [返回值类型] 方法名称(参数){       //普通方法
        [return 返回值];
    }
    [访问权限] [abstracy] [返回值类型] 方法名称(参数);     //抽象方法
    // 在抽象方法中是没有方法体的
}

从以上格式中可以发现,抽象类的定义比普通类多了一些抽象方法,其他地方与普类的组成基本上都是一样的。那么在这里就又可以解决一些疑问了,抽象类可以有属性吗?可以包含普通方法(非抽象方法)吗?答案当然是可以有啦,就如交通工具也得有发明时间吧,都可以运动吧。

  • 抽象方法可以定义有属性,其访问权限是任意的,也可以包含普通方法(非抽象方法)。

从而又可以推断出抽象类也可以有mian方法,因为mian方法也只是一种静态方法,只是在mian方法中不能实例化一个抽象类。接下来写一个交通工具的抽象类:

import java.util.Date;

public abstract class Transportation {    //定义交通工具抽象类
    public Date InventiveTime;   //定义交通工具的发明时间
    public void sport(){       //定义交通工具可以运动的普通方法
        System.out.println("我是抽象类的普通方法");
    }
    public abstract void sportSpeed();    //定义抽象方法,交通工具的运动速度
}

从以上代码可以发现得出,抽象类的定义比普通类多了一些用abstract修饰的抽象方法,其他地方与普通类的组成上基本是一样的。那除了这个abstract方法外还有什么是和普通类是不一样的吗?那问题又来了,一个抽象类可以用final关键字来声明吗?抽象类的方法可以用private、final和static来声明吗?抽象类的方法默认访问权限是什么?我们先来看答案再来解析一下:

  • 抽象类不能使用final关键字来声明,而且抽象方法不能使用private、final和static修饰,当然也不能使用static修饰;关于抽象方法默认访问权限:JDK 1.8以前,抽象类的方法默认访问权限为protected,而JDK 1.8时,抽象类的方法默认访问权限变为default。

 对于第一个问题很容易得出原因,从上面可以得知抽象类就是为了继承而生,如果被final关键字来声明那么此类就不能能子类继承,而抽象类又必须被子类覆写,这不就是前后矛盾了吗?故抽象类不能被final声明。由此又可以得到抽象方法要被子类覆写,那就说明抽象方法不能使用private声明,否则子类还是无法覆写的,同样也不能使用static和final来修饰,静态方法和final方法是不可以继承的。对于抽象方法的默认访问权限就要分JDK版本来说了,结论就是上面的答案那样。

那搞清楚与普通类的一些区别后我们来继承一个抽象类,用代码来实现一个交通工具的汽车类——Car类:

public class Car extends Transportation{     //Car类是通过继承Transportation抽象类得到的
    private String CarInventiveTime;
    private static int wheel;    //定义车轮
    public static void main(String[] args){
        wheel=4;
        System.out.println("我是汽车实体类,"+"我有"+wheel+"个轮子");
        Car car=new Car();
        car.sportSpeed();
    }
    public void sportSpeed(){    //重写父类Transportation的抽象方法
        super.sport();     //可调用super引用父类方法
        System.out.println("我是Car重写的抽象方法");
    }
}

代码执行结果如下:

我是汽车实体类,我有4个轮子
我是抽象类的普通方法
我是Car重写的抽象方法

上面这个例子是继承抽象类得到的普通子类,那么抽象类可以派生出另一个抽象类吗?答案是当然是可以啦:

  • 抽象类派生的子类(非抽象类)必须覆写抽象类中的全部抽象方法,如果没有覆写全部抽象方法或者又加上新的抽象方法,那么这个子类又必须定义成抽象类用abstract来声明。
public abstract class LandTranspotation extends Transportation {   //继承交通工具抽象类得到陆地交通类
    public abstract String  sportTrack();    //定义新的抽象方法,分别是否是轨道交通工具
}

 上面例子代码就是抽象类派生的另一个抽象类,其中包含新的抽象方法,但不包含父类的抽象方法。然后还有一个问题就是,抽象类里可以定义构造方法吗?答案是可以的:

  • 抽象类中是允许存在构造方法的

这个问题可能会有点难解析,实际上在一个抽象类中是允许存在构造方法的,因为抽象类依然使用的是类的继承关系,而且抽象类中也存在各个属性,所以子类在实例化前必须先要对父类进行实例化。下面改一下上面Transportation类的代码来验证一下:

import java.util.Date;

public abstract class Transportation {    //定义交通工具抽象类
    public Date InventiveTime;   //定义交通工具的发明时间
    public Transportation(){      //定义Transportation的构造方法
        System.out.println("我是Transportation抽象类的构造方法");
    }
    public void sport(){       //定义交通工具可以运动的普通方法
        System.out.println("我是抽象类的普通方法");
    }
    public abstract void sportSpeed();    //定义抽象方法,交通工具的运动速度
}

 代码中只是增加了一个无参构造方法,运行Car类中的main方法结果如下:

我是汽车实体类,我有4个轮子
我是Transportation抽象类的构造方法
我是抽象类的普通方法
我是Car重写的抽象方法

可以看出抽象类的构造方法是会被其派生子类实例化时调用,其实子类中就是省略了super()这个调用父类无参构造的方法,即使我们在定义是没写上构造方法但子类还是会默认调用父类的无参构造方法的。同理得到在抽象类中定义有参构造方法也是可以的。

既然抽象类有构造方法,那么抽象类是否能继承实体类?

  • 抽象类是可以继承实体类,但前提是实体类必须有明确的构造函数。

对于明确的构造函数,我理解为可以提供给子类访问的构造器。因为所有的class都必须有一个构造方法,如果你没有在代码里声明构造方法,系统会自动给你生成一个公有无参的构造方法。而且所有的子类构造器都要求在第一行代码中调用父类构造器,如果不写,系统默认去调用父类的无参构造器。

二、接口

或许在开发过程中产品那边会提到“写一个接口来实现这个功能”,虽然表达上是要实现某个功能,但是这里说的接口API和JAVA里的接口还是有点差别的。在JAVA中接口也是一种特殊的类,接口是抽象类的变体但比抽象类更加抽象,接口中所有的方法都是抽象的。其中接口更关注的是对行为的抽象,就如汽车能干什么,它能载人、能加速还能漂移,那么我就能把这些写成一个接口,然后让这些类去实现这些功能。那么好了,第一个问题来了,既然汽车能有那么多用处那我能不能都用接口来实现呀?接口能直接实例化吗?这就是接口的多继承了,即一个类能实现多个接口,但接口也不能直接实例化。

  • 一个类只能继承一个类,但是可以实现多个接口
  • 接口不能直接实例化,要其派生类(非接口类)来实现接口从而实例化类。

这就好像汽车它只能是陆地交通工具而不能同时又是航空交通工具(飞行汽车是耍流氓,我不管),但汽车能载人、能加速还能漂移。同样的你的这个接口只定义了载人的功能,但没说到是什么载人啊,和抽象类一样存在抽象方法,没有具体指明是哪个对象,当然不能直接实例化了。下面来看一下接口的定义格式:

[访问权限] interface 接口名称 [extends父接口名]{
        [访问权限] [static] [final] 数据类型变量名;    //静态常量
        [访问权限] [abstract] [native] [返回值类型] 方法名(参数列表);   //抽象方法
}

在大多数的教程或文章中接口的定义基本格式就是这样,在JDK1.8之前接口是由全局变量和抽象方法组成的,这样的定义格式是完全没问题的。因为在Java7和更早版本中接口中只能只能定义如下两种:

  • 常量
  • 抽象方法

但在Java8中就增加了默认方法静态方法。即定义可以是:

  • 常量
  • 抽象方法
  • 默认方法
  • 静态方法

到了Java9 就更过分了,它又增加了私有方法。即定义可以是:

  • 常量
  • 抽象方法
  • 默认方法
  • 静态方法
  • 私有方法
  • 私有静态方法

没有什么是一成不变的,那些教材中重点标出的规矩在今天看来有些是不合适的了,当然你还没用上Java8及以上时是没有任何感觉的,但人嘛总得往高处爬,不断更新自身知识很重要。废话少说上一段代码来感受一下,我的环境是Java8的,所以Java9的私有方法就不列出了。

public interface ICar {
    String CarName = "五菱宏光";   //定义静态变量
    public static final int LimitLoad = 20;

    default void accelerate(int speed){    //默认方法
        System.out.println("我是加速方法,我要加速"+speed+"码");
    }

    static void drift(){     //静态方法
        System.out.println("我还会漂移方法");
    }

    // 其他抽象方法
    public abstract void method1();
    void method2(String arg);
}

 相信你也在我的代码中看出一些猫腻了,我在定义静态成员变量时一个加了public static final,另一个没加,但是它会默认加上public static final。同样地在接口中定义的抽象方法也是默认加上public abstract的,只时平时我们一般会省略掉而已,抽象方法和抽象类中的抽象方法一样是没有方法实现体的。

  • 接口仅能够有静态、不能修改的成员数据,但在日常使用中很少在接口中定义这些成员数据。
  • Java8中接口能接口中的方法可以是public的,也可以是default的;在Java9中接口中的方法还可以是private的。

接下来就来实现以下这个接口吧。

public class Car implements ICar{
    @Override
    public void method1() {
        System.out.println("我是"+ICar.CarName);
        System.out.println("我限载人数为"+ICar.LimitLoad);
        ICar.drift();
    }

    @Override
    public void method2(String arg) {

    }

    public static void main(String[] args){
        Car car=new Car();
        car.method1();
        car.accelerate(200);
    }
}

程序运行输出:

我是五菱宏光
我限载人数为20
我还会漂移方法
我是加速方法,我要加速200码

由以上代码可以看出要实现接口则要实现其全部抽象方法,这和抽象类又有几分相似。那么接口中能像抽象类那样定义构造方法吗?

  • 接口中不能定义构造器和初始化块。

构造方法是用来在对象初始化前对对象进行一些预处理的,提供了实例化一个具体东西的入口。接口只是声明而已,不一定要进行什么初始化,就算要进行初始化,也可以到实现接口的那一些类里面去初始化。接口只是用来表述动作表述规范来的,可以实例化一台汽车,但我们无法实例化载客量、加速度、漂移。因此,接口要构造方法何用?接口是一种规范,被调用时,主要关注的是里边的方法,而方法是不需要初始化的,类可以实现多个接口,若多个接口都有自己的构造器,则不好决定构造器的调用次序,构造器是属于类自己的,不能继承,因为是纯虚的,接口不需要构造方法。

三、联系与总结

  • 抽象类可以实现接口,但接口不能继承抽象类,接口允许继承多个接口。

 有的时候将接口和抽象类配合起来使用可以为开发带来相当的便利。使用抽象类来实现接口这么做并非是没有意义的,当你自己写的类想用接口中个别方法的时候(注意不是所有的方法),那么你就可以用一个抽象类先实现这个接口(方法体中为空),然后再用你的类继承这个抽象类,这样就可以达到你的目的了,如果你直接用类实现接口,那是所有方法都必须实现的。

至于具体什么时候用抽象类和接口就得看你的场景了。在既需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用抽象类。而接口更多是需要实现特定的多项功能,而这些功能之间可能完全没有任何联系,即是只关注其实现的部分功能。

最近看了挺多资料才整理出这文章,其中或许有笔误,如有错误之处请指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章