你很清楚的知道什么时候用抽象类,什么时候用接口么?
p.s. 多文字预警!
1 抽象类和接口简介
1.1 抽象类
1.1.1 一个小案例
我们先来看这样一个案例:世界上有许许多多不同种类的动物,每一种动物都要吃东西,移动(走路?飞?)等等。现在让你用java语言描述一下这个案例。
啊,你会觉得,so easy。我可是学过继承的人,一个小继承就能解决问题:
// 父类
public class Animal {
public void move(){
System.out.println("i an move");
}
}
// 鸟 类
public class Bird extends Animal {
@Override
public void move() {
System.out.println("i can fly");
}
}
// 狗 类
public class Dog extends Animal {
@Override
public void move() {
System.out.println("i can run");
}
}
//......more
看起来,你大概完成的不错。
但是,我想问你一个问题: new Animal().move()
这段代码描述了一个什么现实情景?
”创建了一个动物,然后让这个动物移动“,你可能会这么回答我。但是,你难道没有发现问题么?现实世界里,有叫做【动物】的生物么?你见过这个叫做【动物】的生物移动么?
动物,是对生物的一种统称,狗是动物,鸟也是动物。但是【动物】本身是一个抽象的概念,你在现实世界中,并没有见过一种叫做【动物】生物吧?
你应该明白了,我们可以new一个Bird,new一个Dog,因为它们是实实在在的对象,但是我们不应该new出一个Animal来,因为动物是一个抽象的概念,实际上它并不存在。
事实上,Animal中的move()方法,也是有问题的不是么?既然Animal不存在,那它怎么会有真实存在的move()方法呢?
问题来了。。。
1.1.2 抽象类和抽象方法
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
就像我们上面中的例子一样。Dog和Bird可以用一个普通类来描绘,但是Animal不可以,Animal就应该是一个抽象类。
在java中,被abstract修饰的类,叫做抽象类。抽象类中可以定义抽象方法,也可以定义普通方法。抽象类不可以被实例化,只有被实体类继承后,抽象类才会有作用。
抽象方法:
- 被abstract修饰的方法叫做抽象方法,抽象方法没有方法体,也就是说抽象方法没有具体的实现。
- 抽象方法必须定义在抽象类中。
- 举个例子:
abstrac void move();
。这就是一个抽象方法。
回到刚才的问题,我们现在利用抽象类来重构一下我们的代码:
// 父类
public abstract class Animal {
public abstract void move();//抽象方法
}
// 鸟 类
public class Bird extends Animal {
@Override
public void move() {
System.out.println("i can fly");
}
}
// 狗 类
public class Dog extends Animal {
@Override
public void move() {
System.out.println("i can run");
}
}
//......more
因为抽象类不可以实例化,所以现在就不用担心new Animal()
这样的情况出现了。并且我们将Animal类中的move方法也定义为抽象方法,所以上面的所有问题,都迎刃而解了。
抽象类就是用来被继承的,脱离的继承,抽象类就失去了价值。继承了抽象类的子类,需要重写抽象类中所有的抽象方法。
在使用抽象类时需要注意几点:
- 抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。
为什么抽象类不能实例化对象:
- 抽象类的设计目的就是为了处理类似于Animal这种无法准确描述为一个对象的情况。所以不可以实例化。
- 抽象类中可以定义抽象方法。抽象方法是没有方法体的,必须被子类重写后,该方法才能被正确调用。如果抽象类能实例化,那么抽象方法也就可以被调用,这显然是不行的。
-
子类必须重写所有抽象方法。
当然,不都重写也可以,但是这样的话,子类也必须是抽象类。
-
一个类里只要有一个抽象方法,那么这个类必须定义为抽象类。
-
抽象类中可以包含具体的方法,当然也可以不包含抽象方法。
-
abstract不能与final并列修饰同一个类。
abstract类就是为了让子类继承,而final类不能被继承。
-
abstract 不能与private、static、final或native并列修饰同一个方法。
抽象方法必须被子类重写才能使用。
1.2 接口
java中的接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为。
接口是一种比抽象类更加抽象的【类】。这里给【类】加引号是我找不到更好的词来表示,但是我们要明确一点就是,接口本身就不是类。为什么说它更抽象呢?因为抽象类中还可以定义普通方法,但是接口中只能写抽象方法。
接口是用来建立类与类之间的协议,它所提供的只是一种形式,而没有具体的实现。接口中的所有方法默认都是public abstract的。
接口是抽象类的延伸,java了保证数据安全是不能多重继承的,也就是说继承只能存在一个父类,但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补了抽象类不能多重继承的缺陷,但是推荐继承和接口共同使用,因为这样既可以保证数据安全性又可以实现多重继承。
在使用接口过程中需要注意如下几个问题:
-
接口中的所有方法访问权限自动被声明为public。确切的说只能为public,当然你可以显示的声明为protected、private,但是编译会出错。
-
接口中可以定义变量,但是它会被强制变为不可变的常量,因为接口中的“成员变量”会自动变为为public static final。可以通过类命名直接访问:ImplementClass.name。
-
实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。
-
在实现多接口的时候一定要避免方法名的重复。
因为一个类可能会实现多个接口,如果这两个接口有名字相同的方法,会产生意想不到的问题。
-
不能使用new操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用(refer to)一个实现该接口的类的对象。可以使用 instanceof 检查一个对象是否实现了某个特定的接口。例如:if(anObject instanceof Comparable){}。
-
接口中不存在具体的方法。
值得一提的是,在java8中,接口里也可以定义默认方法:
public interface java8{
//在接口里定义默认方法
default void test(){
System.out.println("java 新特性");
}
}
2 抽象类和接口的区别
基础知识看完了,我们来看抽象类和接口的区别。
2.1 从概念上来看
前面讲过了,这里不再赘述。
2.2 语法定义层面看
在语法层面,Java语言对于abstract class和interface给出了不同的定义方式。
//抽象类
public abstract class AbstractTest {
abstract void method1();
void method2(){
//实现
}
}
//接口
interface InterfaceTest {
void method1();
void method2();
}
2.3 设计理念层面看
前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在【is-a】关系,即父类和派生类在概念本质上应该是相同的。
对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的协议而已。
我们来看一个例子:假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一 个表示该抽象概念的类型,定义方式分别如下所示:
//抽象类
abstract class Door{
abstract void open();
abstract void close();
}
//接口
interface Door{
void open();
void close();
}
其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使 用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。
如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)下面将罗列出可能的解决方案,并从设计理念层面对 这些不同的方案进行分析。
解决方案一:
简单的在Door的定义中增加一个alarm方法,如下:
abstract class Door{
abstract void open();
abstract void close();
abstract void alarm();
}
或者
interface Door{
void open();
void close();
void alarm();
}
这种方法违反了面向对象设计中的一个核心原则 ISP,在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变而改变,反之依然。
比如说,有一个普普通通的门,实现了Door接口,或者继承了Door抽象类,它只需要开门和关门的行为,但是当你像上面一样修改了接口或者抽象类以后,那么这个【普通门】也不得不具备了【报警】的功能,这显然是不合理的。
ISP(Interface Segregation Principle):面向对象的一个核心原则。它表明使用多个专门的接口比使用单一的总接口要好。
一个类对另外一个类的依赖性应当是建立在最小的接口上的。
一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
解决方案二
既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表两个不同概念的抽象类里面,定义的方式有三种:
- 两个都使用抽象类来定义。
- 两个都使用接口来定义。
- 一个使用抽象类定义,一个是用接口定义。
由于java不支持多继承所以第一种是不可行的。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。
如果选择第二种都是接口来定义,那么就反映了两个问题:
- 我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还报警器。
- 如果我们对问题域的理解没有问题,比如我们在分析时确定了AlarmDoor在本质上概念是一致的,那么我们在设计时就没有正确的反映出我们的设计意图。因为你使用了两个接口来进行定义,他们概念的定义并不能够反映上述含义。
第三种,如果我们对问题域的理解是这样的:
- AlarmDoor本质上Door,但同时它也拥有报警的行为功能,这个时候我们使用第三种方案恰好可以阐述我们的设计意图。
- AlarmDoor本质是门,所以对于这个概念我们使用抽象类来定义,同时AlarmDoor具备报警功能,说明它能够完成报警概念中定义的行为功能,所以alarm可以使用接口来进行定义。如下:
abstract class Door{
abstract void open();
abstract void close();
}
interface Alarm{
void alarm();
}
class AlarmDoor extends Door implements Alarm{
void open(){}
void close(){}
void alarm(){}
}
这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。
其实abstract class表示的是【is-a】关系,interface表示的是【like- a】关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有 Door的功能,那么上述的定义方式就要反过来了。
3 抽象类和接口的使用
看了那么多乱糟糟的分析,我们究竟如何选择呢?到底是使用抽象类,还是使用接口?
首先,我们要明确一点:抽象类是为了把相同的东西提取出来, 是为了重用; 而接口的作用是提供程序里面固化的契约, 是为了降低偶合。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。
比如说,现在,我要用java描述一下学生和老师。学生和老师都有姓名,年龄,性别等,都会走路,吃饭;但是老师要授课,而学生要听课,不同的老师授课的科目不同,不同专业的学生听的课也不同。
我们可以把老师和学生共有的属性和方法提取出来,用抽象类表示:
public abstract class Person {
String name;
int age;
String sex;
abstract void eat();
abstract void run();
}
老师会授课,不同的老师授课不同,我们可以定义一个接口:
public interface Teach {
void teach(String className);
}
学生要上课,不同专业的学生上的科目不同,我们也可以定义为接口:
public interface TakeClass {
void takeClass(String className);
}
定义老师:
public class Teacher extends Person implements Teach {
@Override
public void teach(String className) {
System.out.println("teach " + className);
}
@Override
void eat() {
System.out.println("teacher eat");
}
@Override
void run() {
System.out.println("teacher run");
}
}
定义学生:
public class Student extends Person implements TakeClass {
@Override
public void takeClass(String className) {
System.out.println("take class: " + className);
}
@Override
void eat() {
System.out.println("student eat");
}
@Override
void run() {
System.out.println("student run");
}
}
这样使用抽象类和接口,我觉得是一种很合理的方式。
现在有很多讨论和建议提倡用interface代替abstract类,两者从理论上可以做一般性的混用,但是在实际应用中,他们还是有一定区别的。抽象类一般作为公共的父类为子类的扩展提供基础,这里的扩展包括了属性上和行为上的。而接口一般来说不考虑属性,只考虑方法,使得子类可以自由的填补或者扩展接口所定义的方法。
就像这个老师和学生的例子,抽象类提取了他们共有的属性,他们各自有什么属性可以交给子类去完成。有人可能会说,为什么不把eat 和 run 方法定义为接口呢?这当然也是可以的。但是我觉得,吃和走,是人自身的一种行为,它不像授课和上课这种是因为某种身份而特有的行为,吃和走与人自身的属性(姓名,年龄)都是【人】本身就有的,所以我觉得一起放到抽象类里更合适一些。当-然,你单独定义一个【人行为】的接口从语法角度讲也没问题。
4 再谈多态
前面讲过,继承(实现)是多态的前提之一。现在学完了抽象类和接口,多态的使用场景就更多了。
比如我们常用的List接口:
List<String> l = new ArrayList<>();
List<Integer> l1 = new LinkedList<>();
这就是多态的体现。
由于篇幅已经过长,我就不细说了~
5 总结
总结一下抽象类和接口:
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现(不讨论java8的情况下)
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现。不能写成abstract void abc(){}。
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
11、从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。
12、选择抽象类和接口的时候记得一句话:抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。
13、使用抽象类,要保证和实现类之间是【is-a】关系。
其实抽象类和接口的使用时有很多争议的,没有一个人敢说他的想法就是绝对正确的,而别人的想法就是错误的。在设计的时候,如何选择,不仅仅是根据一些理解,还需要一些经验。有的时候,抽象类是配合接口一起使用的,接口为几个【普通类】定义了一系列方法,然后抽象类实现该接口并实现了这几个【普通类】共同的方法,然后几个【普通类】再继承抽象类,分别实现各自不同的方法。
知识是死的,人是活的,怎么使用不是听别人怎么说你就怎么用,更多的是自己的理解。因为,他说的也不一定对啊?
本篇文章到这里就结束了。满满的文字,大概你认真看完也累的半死了。但是学习就是这样,不辛苦一点怎么能学的全,学得会呢?
如果文章内容有什么问题或者错误,请及时与我联系。
转载请注明出处:
本文地址:http://blog.csdn.net/qq_31655965/article/details/54972723
原创自csdn:http://blog.csdn.net/qq_31655965
同步更新在:
我的博客:wpblog.improvecfan.cn
简书:http://www.jianshu.com/u/8dc5811b228f
博主:cleverfan
看完了,如果对你有帮助,随手点个赞呗~~~
参考资料:
http://blog.csdn.net/ttgjz/article/details/2960451
http://blog.csdn.net/xw13106209/article/details/6923556
http://blog.csdn.net/wenwen091100304/article/details/48381023
网友评论
博主我觉得这句话 我是这样理解的 : 实现接口的非抽象类必须重写该接口的所有方法 ,而并不是一定要去实现。 抽象类可以不用重写 。
这样理解。。