java温故知新——抽象类与接口
一、 抽象类和接口简介
1.1 抽象类
1.1.1 一个小案例
我们先来看这样一个事例:世界上有许多的动物,每一种动物都要吃,移动(走?飞?)等等。现在让你用java语言描述一下这个案例。
你会说:很简单啊,我可是学过继承的人,一个小继承就能解决问题。
// 父类 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.1 语法定义层面
在语法层面,Java语言对于abstract class和interface给出了不同的定义方式。
//抽象类 public abstract class AbstractTest { abstract void method1(); void method2(){ //实现 } } //接口 interface InterfaceTest { void method1(); void method2(); }
2.2 设计理念层面
前面已经提到过,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不支持多继承所以第一种是不可行的。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。
如果选择第二种都是接口来定义,那么就反映了两个问题:
1、我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还报警器。
2、如果我们对问题域的理解没有问题,比如我们在分析时确定了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的功能,那么上述的定义方式就要反过来了。
三、 抽象类和接口的使用
那我们究竟如何选择呢?到底是使用抽象类,还是使用接口?
首先,我们要明确一点:抽象类是为了把相同的东西提取出来, 是为了重用; 而接口的作用是提供程序里面固化的契约, 是为了降低偶合。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。
比如说,现在,我要用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 方法定义为接口呢?这当然也是可以的。但是吃和走,是人自身的一种行为,它不像授课和上课这种是因为某种身份而特有的行为,吃和走与人自身的属性(姓名,年龄)都是【人】本身就有的,所以我觉得一起放到抽象类里更合适一些。当然,你单独定义一个【人行为】的接口从语法角度讲也没问题。
四、 总结
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现(不讨论java8的情况下)
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现。不能写成abstract void abc(){}。
7、抽象类里可以没有抽象方法
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、从实践的角度来看,如果依赖于抽象类来定义行为,往往导致过于复杂的继承关系,而通过接口定义行为能够更有效地分离行为与实现,为代码的维护和修改带来方便。
10、选择抽象类和接口的时候记得一句话:抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。