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、选择抽象类和接口的时候记得一句话:抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。

 

posted @ 2017-05-23 15:24  风也不知道往哪吹  阅读(276)  评论(0编辑  收藏  举报