之前讲了Java的三大特征有封装、继承、多态,这样说其实我们少讲了一个,那就是抽象性,在平时的教程中认为只有三种特征,因为它们把抽象放到继承里了,认为抽象类是继承的一种,这也使抽象性是否是Java的一大特征具有争议。而Java语言中对抽象概念定义的机制就是抽象类和接口,正由于它们才赋予Java强大的面向对象的功能。他们两者之间对抽象概念的支持有很大的相似,但是也有区别。所以接下来我们就来学习它们两的使用和区别。(如果在平时面试时遇到问Java有几大特征,我们就说封装、继承、多态这三特征即可)
1、抽象类
在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如我们在创建一个Animal对象时,我们只知道动物有吃喝拉撒睡等特征,但是却不知道它到底是哪一种动物,此时这个Animal类是比较抽象的,所以我们需要一个具体的类来描述该动物,如用狗、猫来对它进行特定的描述,我们才知道它是什么动物。
在Java中用abstract关键字来修饰的类就是抽象类,当然这个关键字也可以用来修饰方法,表明该方法是抽象方法。
我们在使用抽象类的时候需要注意以下几点:
①、抽象类不能实例化,必须要由继承它的子类来创建实例。
②、抽象方法只有方法的声明,没有方法体。
③、抽象类中的抽象方法必须要在子类中重写。
④、抽象类中既可以有抽象方法,也可以有普通方法,普通方法可不用重写。
⑤、只要包含一个抽象方法的类,该类必须要定义成抽象类。
⑥、abstract不能用来修饰属性、构造器等结构。
⑦、abstract不能与final并列修饰同一个类。
⑧、abstract不能与private、static、final或native并列修饰同一个方法。
然后给出一个简单的示例:
1 public abstract class Animal { 2 //抽象方法 3 public abstract void eat(); 4 //普通方法 5 public void sleep(){ 6 System.out.println("动物需要睡觉..."); 7 } 8 } 9 10 class Cat extends Animal{ 11 12 @Override 13 public void eat() { 14 System.out.println("猫喜欢吃鱼..."); 15 } 16 17 @Override 18 public void sleep() { 19 System.out.println("猫需要睡觉..."); 20 } 21 } 22 23 class Dog extends Animal { 24 25 @Override 26 public void eat() { 27 System.out.println("狗喜欢吃骨头..."); 28 } 29 30 @Override 31 public void sleep() { 32 System.out.println("狗需要睡觉..."); 33 } 34 35 public static void main(String[] args) { 36 Animal a1=new Dog(); 37 a1.eat(); 38 a1.sleep(); 39 40 Animal a2= new Cat(); 41 a2.eat(); 42 a2.sleep(); 43 } 44 }
程序运行结果:
到这里我们可以看出,抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么这个抽象类就没有任何的意义了。
2、接口
接口的英文名称是 interface。接口是属于和类类型同等级别的结构,但是它不是类。却和类类型有着共同点,在接口中可以含有变量、方法。但是需要注意是,接口中的变量会被隐式地指定为public static final变量,并且只能是public static final变量,用private修饰会报编译错误。而接口中的方法会被隐式地指定为public abstract方法,且只能是public abstract方法,如果用其他关键字,如private、protected、static、 final等修饰都会导致报编译错误。也就是说接口中的变量全都是常量,方法都是抽象方法(JDK1.8以前)。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”。
准确来说接口定义的是一种规范。 例如鸟和蝴蝶都是可以飞的动物,它们都符合 飞行 的规范,所以它们可以实现 飞 这个接口,对于任何其他可以飞行的动物都需要实现该接口。因为它们符合这种规范。
我们在使用接口的时候同样需要注意以下几点:
①、接口使用interface关键字来定义,并且其子类要implement关键字来实现该接口。
②、接口和抽象类一样不能实例化,必须要由实现它的子类来创建实例。
③、接口中定义的成员变量都是全局常量,系统会在变量前面自动加上public static final,可以通过类命名直接访问:ImplementClass.name。
④、接口中的方法都是抽象方法,但是在JDK1.8以后,可以包含普通方法。
⑤、接口可以多实现,而抽象类只有单继承。
1 public interface Fly { 2 public void fly(); 3 } 4 5 class Bird implements Fly{ 6 7 @Override 8 public void fly() { 9 System.out.println("鸟可以飞行..."); 10 } 11 } 12 13 class Butterfly implements Fly{ 14 15 @Override 16 public void fly() { 17 System.out.println("蝴蝶可以飞行..."); 18 } 19 } 20 21 class Test{ 22 public static void main(String[] args) { 23 Fly bird = new Bird(); 24 bird.fly(); 25 26 Fly butterfly = new Butterfly(); 27 butterfly.fly(); 28 } 29 } 30 //运行结果: 31 //鸟可以飞行... 32 //蝴蝶可以飞行...
接口在JDK1.8之前说里面的方法全都是抽象方法,但是在JDK1.8中,接口新添加了一些特性,我们可以在接口中添加静态方法和默认方法。当然从技术的角度来看,这完全合法的,只是看起来违反了接口作为一个抽象定义的理念。
静态方法:使用static关键字来修饰。可以使用接口直接调用静态方法并且执行该方法体的内容。我们经常在相互一起使用的类中 使用静态方法。你可以在标准库中找到像Collection / Collections或者Path/Paths这样成对的接口和类。
默认方法:默认方法必须要用default关键字来修饰。可以通过子类的实现类来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如Collection、Comparator等接口中提供了丰富的默认方法。
1 public interface InterfaceTest { 2 //静态方法 3 public static void method1(){ 4 System.out.println("接口中静态方法..."); 5 } 6 7 //默认方法 8 public default void method2(){ 9 System.out.println("接口中默认方法..."); 10 } 11 12 //默认方法 13 default void method3(){ 14 System.out.println("接口中无修饰默认方法..."); 15 } 16 } 17 //创建一个父类 18 class SuperClass{ 19 public void method2() { 20 System.out.println("父类中默认方法method2..."); 21 } 22 23 public void method3() { 24 System.out.println("父类中默认方法method3..."); 25 } 26 } 27 //创建子类,继承SuperClass,实现InterfaceTest接口 28 class SubClass extends SuperClass implements InterfaceTest{ 29 //重写一个方法method2 30 @Override 31 public void method2() { 32 System.out.println("子类中默认方法method2..."); 33 } 34 } 35 36 class Test{ 37 public static void main(String[] args) { 38 //调用接口中静态方法 39 InterfaceTest.method1(); 40 41 InterfaceTest test=new SubClass(); 42 test.method2(); 43 test.method3(); 44 } 45 }
程序运行结果:
通过运行我们可以得出以下几点:
①、接口中静态方法只能用接口. 方法来调用,而不能用实例调用。
②、接口中默认方法必须要用子类的实例来调用。
③、如果子类重写了接口中的默认方法,则调用的是子类中重写方法的内容。
④、如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的方法,那么子类在没有重写该方法的情况下,默认调用父类中的同名同参数的方法。-->类优先原则。
⑤、如果实现类实现了多个接口(没有继承),而多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,会报错。-->接口冲突。这就需要我们在实现类中重写此方法。
⑥、如果需要调用父类中方法或接口中默认方法,父类使用 super . 方法名,接口使用 接口名 . super . 方法名。
这里讲的非常的细,其实我们只需要了解即可。
3、抽象类和接口的区别
以下参考链接:https://www.cnblogs.com/dolphin0520/p/3811437.html
尽管抽象类和接口之间存在较大的相同点,甚至有时候还可以互换,但这样并不能弥补他们之间的差异之处。下面将从语法层次和设计层次两个方面对抽象类和接口进行阐述。
1.语法层面上的区别
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2.设计层面上的区别
1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念:
1 abstract class Door { 2 public abstract void open(); 3 public abstract void close(); 4 }
或者:
1 interface Door { 2 public abstract void open(); 3 public abstract void close(); 4 }
但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路:
1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能;
2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。
从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。
1 interface Alram { 2 void alarm(); 3 } 4 5 abstract class Door { 6 void open(); 7 void close(); 8 } 9 10 class AlarmDoor extends Door implements Alarm { 11 void oepn() { 12 //.... 13 } 14 void close() { 15 //.... 16 } 17 void alarm() { 18 //.... 19 } 20 }