Skip to content

Latest commit

 

History

History
688 lines (468 loc) · 28 KB

File metadata and controls

688 lines (468 loc) · 28 KB

[TOC]


抽象类接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的面向对象的能力。 他们两者之间对抽象概念的支持有很大的相似,甚至可以互换,但是也有区别。


抽象类和抽象方法

抽象类

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的。 如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类。 比如new Animal(),我们都知道这个是产生一个动物Animal对象,但是这个Animal具体长成什么样子我们并不知道,它没有一个具体动物的概念,所以他就是一个抽象类,需要一个具体的动物,如狗、猫来对它进行特定的描述,我们才知道它长成啥样。

抽象类的作用在于将许多有关的类组织在一起,提供一个公共的类,即抽象类,而那些被它组织在一起的具体的类做为它的子类由它派生出来。

抽象类体现数据抽象的思想,是实现程序多态性的一种手段。 抽象类定义了一组抽象的方法,至于这组抽象方法的具体表现形式有派生类来实现。同时抽象类提供了继承的概念,它的出发点就是为了继承,否则它没有存在的任何意义。 所以说定义的抽象类一定是用来继承的,同时在一个以抽象类为节点的继承关系等级链中,叶子节点一定是具体的实现类。

Java语言中,用abstract关键字来修饰一个类时,这个类叫做抽象类。一个abstract类只关心它的子类是否具有某种功能,并不关心该功能的具体实现,功能的具体行为由子类负责实现的。

定义抽象类的格式如下:

abstract class abstractClass{ /* 类定义体 */ }

抽象方法是指在返回值类型前加上abstract关键字,且没有方法体。格式如下:

abstract<返回值类型><抽象方法名>([<形式参数列表>]);

例如:

   public abstract class Drawing {
   public abstract void drawDot(int x, int y);
   public void drawLine(int x1, int y1,int x2, int y2) {
     ………… 
     }
   }

final类相反,abstract类必须被继承,abstract方法必须被重写。

当一个类的定义完全表示抽象的概念时,它不应该被实例化为一个对象。例如Java中的Number类就是一个抽象类,它只表示数字这一抽象概念,只有当它作为整数类Integer或实数类Float等的父类时才有意义。

定义一个抽象类的格式如下:

abstract class abstractClass{ /* 类定义体 */ }

抽象方法是指在返回值类型前加上abstract关键字,且没有方法体。 格式如下:

abstract<返回值类型><抽象方法名>([<形式参数列表>]);

抽象类中可以包含抽象方法,为所有子类定义一个统一的接口,对抽象方法只需声明,而不需实现,因此它没有方法体。其格式如下:

abstrac returnType abstractMethod([paramlist));

例子

定义一个抽象动物类Animal,提供抽象方法叫cry(),猫、狗都是动物类的子类,由于cry()为抽象方法,所以Cat、Dog必须要实现cry()方法。如下:

public abstract class Animal {
    public abstract void cry();
}

public class Cat extends Animal{

    @Override
    public void cry() {
        System.out.println("猫叫:喵喵...");
    }
}

public class Dog extends Animal{

    @Override
    public void cry() {
        System.out.println("狗叫:汪汪...");
    }

}

public class Test {

    public static void main(String[] args) {
        Animal a1 = new Cat();
        Animal a2 = new Dog();
        
        a1.cry();
        a2.cry();
    }
}

Output:

猫叫:喵喵... 狗叫:汪汪...

抽象类总结规定

  1. 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  2. 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  3. 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  4. 构造方法,类方法(用static修饰的方法)不能声明为抽象方法。
  5. 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

抽象方法

如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。 abstract关键字同样用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。 抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。 即,用abstract来修饰一个方法时,该方法叫做抽象方法。

public abstract class Employee
{
   private String name;
   private String address;
   private int number;
   
   public abstract double computePay();
   
   //其余代码
}

声明抽象方法会造成以下两个结果:

  • 如果一个类包含抽象方法,那么该类必须是抽象类。
  • 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。

继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。 最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。 如果Salary类继承了Employee类,那么它必须实现computePay()方法:

public class Salary extends Employee
{
   private double salary; // Annual salary
  
   public double computePay()
   {
      System.out.println("Computing salary pay for " + getName());
      return salary/52;
   }
 
   //其余代码
}

对于抽象类与抽象方法的限制

  • 凡是用abstract修饰符修饰的类被称为抽象类,凡是用abstract修饰符修饰的成员方法被称为抽象方法;
  • 抽象类中可以有零个或多个抽象方法,也可以包含非抽象的方法;
  • 抽象类中可以没有抽象方法,但是,有抽象方法的类必须是抽象类;
  • 对于抽象方法来说,在抽象类中只指定其方法名及其类型,而不书写其实现代码;
  • 抽象类可以派生子类,在抽象类派生的非抽象子类中必须实现抽象类中定义的所有抽象方法;
  • 抽象类不能使用new运算符创建对象,但仍可在抽象类中定义构造方法,可由派生类的构造方法调用;
  • abstractfinal不能同时修饰一个类;
  • abstract不能与privatestaticfinalnative同时修饰一个方法;
  • abstract类中不能有private的数据成员或成员方法;
  • 父类是非抽象类(即具体类),则其子类仍可是抽象类;
  • 不能使用new创建抽象类的对象,但抽象类可以当作数据类型使用,如可以声明抽象类的引用,抽象类也可以作为方法的形参类型、返回值类型、数组元素类型等。

创建抽象类和抽象方法非常有用,因为他们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样使用他们。抽象类还是有用的重构器,因为它们使我们可以很容易地将公共方法沿着继承层次结构向上移动。(From:Think in java )

练习

请写出以下这段程序的运行结果:

abstract class AA{
    abstract void callme( );
    
    void metoo( ){
    	System.out.println("InsideA's metoo() method");
   	}
}

class BB extends AA{
   void callme( ){
	   System.out.println("Inside B's callme() method");
  	}
}

public class AAbstract{
   public static void main(String args[]){
   		AA cc=new BB(); //cc为上转型对象
   		cc.callme();
   		cc.metoo();
 	}
}

接口

与C++不同,Java不支持多重继承,而是用接口实现比多重继承更强的功能。 接口是Java中实现多重继承的唯一途径。

接口则把方法的定义和类的层次区分开来,通过它可以在运行时动态地定位所调用的方法。同时,接口可以实现“多重继承”,且一个类可实现多个接口,正是这些机制使得接口提供了比多重继承更简单,更灵活,而且更强健的功能。

==接口就是抽象方法声明和常量值的集合。==从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的声明,而没有方法的实现。

通过接口使得处于不同层次,甚至互不相关的类可以具有相同的行为。接口其实就是方法定义和常量值的集合。

它的优点主要体现在下面几个方面:

  • (1)通过接口可以实现不相关类的相同行为,而不需要考虑这些类之间的层次关系。
  • (2)通过接口可以指明多个类需要实现的方法。
  • (3)通过接口可以了解对象的交互界面,而不需了解对象所对应的类。

接口把方法的定义和类的层次区分开来,通过它可以在运行时动态地定位所调用的方法。同时接口中可以实现“多重继承”,且一个类可以实现多个接口。正是这些机制使得接口提供了比多重继承(如C++等语言)更简单、更灵活、而且更强劲的功能。

接口声明

Java 不支持多继承性,即一个类只能有一个父类。单继承性使得Java类层次简单,易于程序的管理。为了克服单继承的缺点,Java使用了接口,一个类可以实现多个接口。使用关键字interface来定义一个接口。接口的定义和类的定义很相似,分为接口声明和接口体两部分。

完整的接口定义格式如下:

[修饰符] interface 接口名extends 父接口名列表]{
	//常量数据成员声明
	//抽象方法声明
}
  • 其中public修饰符指明任意类均可以使用这个接口,缺省情况下,只有与该接口定义在同一个包中的类才可以访问这个接口。
  • extends子句与类声明中的extends子句基本相同,不同的是一个接口可以有多个父接口,用逗号隔开,而一个类只能有一个父类。
  • 子接口继承父接口中所有的常量和方法。
  • 通常接口名称以ableible结尾,表明接口能完成一定的行为,例如Runnable、Serializable。

注意

  • (1) interface是声明接口的关键字,可以把它看成一个特殊类;
  • (2) 接口名要求符合Java标识符命名规则;
  • (3) 修饰符有两种:public 和默认。public修饰的接口是公共接口,可以被所有的类和接口使用;默认修饰符的接口只能被同一个包中的其它类和接口使用;
  • (4) 父接口列表:接口也具有继承性。定义一个接口时可以通过extends关键字声明该接口是某个已经存在的父接口的派生接口,它将继承父接口的所有属性和方法。与类的继承不同的是一个接口可以有一个以上的父接口,它们之间用逗号分隔;
  • (5) 常量数据成员声明:常量数据成员前可以有也可省略修饰符。修饰符是public static 和 fina1;接口中的数据成员都是用 final修饰的常量,写法如下:
修饰符 数据成员类型 数据成员名=常量值

数据成员类型 数据成员名 = 常量值

例如

public final static double PI=3.14159// double PI=3.14159;
int SUM=100// public final static int SUM=100; 
  • (6) 接口中没有自身的构造方法,所有成员方法都是抽象方法;在接口中只能给出这些抽象方法的方法名、返回值和参数列表,而不能定义方法体,即仅仅规定了一组信息交换、传输和处理的“接口”。格式如下:
返回值类型 方法名参数列表); 

==Each interface is compiled into a separate bytecode file, just like a regular class.==

接口体

接口体中包含常量定义和方法定义两部分。 其中常量定义部分定义的常量均具有publicstaticfinal属性。 其格式如下:

returnType methodName([paramlist]);

接口中只能进行方法的声明,而不提供方法的实现,所以,方法定义没有方法体,且用分号(;)结尾,在接口中声明的方法具有publicabstract属性。另外,如果在子接口中定义了和父接口同名的常量,则父接口中的常量被隐藏。

例如

interface Summaryable {
	final int MAX=50; // MAX具有public、static、final属性
	void  printone(float x);
	float sum(float x ,float y);
}

上面这段程序可以以Summaryable.java来保存,也可以写入其它Java程序中。

接口的实现

接口定义仅仅是实现某一特定功能的一组功能的对外接口和规范,而不能真正地实现这个功能,这个功能的真正实现是在“继承”这个接口的各个类中完成的,要由这些类来具体定义接口中各抽象方法的方法体。

一个类只能继承一个父类,但是可实现多个接口。类定义的完整格式如下:

[<修饰符>] class <类名>[extends<父类名>] [implements<接口名1>,<接口名2>,……]  {
// 类定义体
}
  • 当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。

实例

/* 文件名 : Animal.java */
interface Animal {
   public void eat();
   public void travel();
}
/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
 
   public void eat(){
      System.out.println("Mammal eats");
   }
 
   public void travel(){
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
      return 0;
   }
 
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

以上实例编译运行结果如下:

Mammal eats Mammal travels

重写接口中声明的方法时,需要注意以下规则:

  • 类在实现接口的方法时,不能抛出强制性异常,只能在接口中,或者继承接口的抽象类中抛出该强制性异常。
  • 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型。
  • 如果实现接口的类是抽象类,那么就没必要实现该接口的方法。

在实现接口的时候,也要注意一些规则:

  • 一个类可以同时实现多个接口。
  • 一个类只能继承一个类,但是能实现多个接口。
  • 一个接口能继承另一个接口,这和类之间的继承比较相似。
  • (1)在类中,用implements关键字就可以调用接口。一个类可以调用多个接口,这时,在implements后用逗号隔开多个接口的名字;
  • (2)如果实现某接口的类不是抽象类,则在类的定义体部分必须实现指定接口的所有抽象方法,即为所有抽象方法定义方法体,而且方法头部分应该与接口中的定义完全一致,即有完全相同的返回值和参数列表;
  • (3)如果实现某接口的类是抽象类,则它可以不实现该接口所有的方法。
  • (4)接口的抽象方法的访问限制符都己指定为public,所以类在实现方法时,必须显式地使用public修饰符,否则编译系统警告为缩小了接口中定义的方法的访问控制范围。
class Calculate extends Computer implements Summary,Substractable {
……
}

类Calculate使用了Summary 和Substractable接口,继承了Computer类。

如果一个类使用了某个接口,那么这个类必须实现该接口的所有方法,即为这些方法提供方法体。需要注意的如下:

  • 1)在类中实现接口的方法时,方法的名字,返回类型,参数个数及类型必须与接口中的完全一致。
  • 2)接口中的方法被默认是public, 所以类在实现接口方法时,一定要用public来修饰。
  • 3)另外,如果接口的方法的返回类型如果不是void的,那么在类中实现该接口方法时,方法体至少要有一个return语句。如果是void型,类体除了两个大括号外,也可以没有任何语句.

使用多重接口的例子

// MultInterfaces.java
interface I1 {
   abstract void test(int i); 
}

interface I2 {
   abstract void test(String s); 
}

public class MultInterfaces implements I1, I2 {
  public void test(int i) {
    System.out.println("In MultInterfaces.I1.test");
  }
 
  public void test(String s) {
    System.out.println("In MultInterfaces.I2.test");
  }
 
  public static void main(String[] a) {
    MultInterfaces t = new MultInterfaces();
    t.test(42);
    t.test("Hello");
  }
}

接口的继承

  • 一个接口能继承另一个接口,和类之间的继承方式比较相似。
  • 接口的继承使用extends关键字,子接口继承父接口的方法。

下面的Sports接口被Hockey和Football接口继承:

// 文件名: Sports.java
public interface Sports {
   public void setHomeTeam(String name);
   public void setVisitingTeam(String name);
}
 
// 文件名: Football.java
public interface Football extends Sports {
   public void homeTeamScored(int points);
   public void visitingTeamScored(int points);
   public void endOfQuarter(int quarter);
}
 
// 文件名: Hockey.java
public interface Hockey extends Sports {
   public void homeGoalScored();
   public void visitingGoalScored();
   public void endOfPeriod(int period);
   public void overtimePeriod(int ot);
}

Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。 相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。

  • 接口的多继承

在Java中,类的多继承是不合法,但接口允许多继承。 在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。

如:

public interface Hockey extends Sports, Event

以上的程序片段是合法定义的子接口,与类不同的是,接口允许多继承,而 Sports及 Event 可能定义或是继承相同的方法。

接口和抽象类的区别

抽象类和接口是java语言中两种不同的抽象概念,他们的存在对多态提供了非常好的支持,虽然他们之间存在很大的相似性。但是对于他们的选择往往反应了您对问题域的理解。只有对问题域的本质有良好的理解,才能做出正确、合理的设计。

接口与类相似点:

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

Java中所有的类有共同的父类java.lang.Object,但接口没有共同的根。 接口可以当作抽象类使用。

抽象类和接口所反映的设计理念是不同的,抽象类所代表的是“is-a”的关系,而接口所代表的是“like-a”的关系。

  • 抽象类在java语言中所表示的是一种继承关系,一个子类只能存在一个父类,但是可以存在多个接口。

  • 在抽象类中可以拥有自己的成员变量和非抽象类方法,但是接口中只能存在静态的不可变的成员数据,而且它的所有方法都是抽象的。

  • 接口不能用于实例化对象。(接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。)

  • 接口没有构造方法;抽象类中可以有构造方法。

  • 接口中所有的方法必须是抽象方法,没有实现部分;抽象类中可以有具体的方法。(由于定义在接口中的所有方法都是抽象方法,接口中可以省略abstract修饰符;但是抽象类中的抽象方法必须用abstract修饰。)

  • 接口不能包含成员变量,数据成员必须是必须是常量,默认使用staticfinal 变量;抽象类中的数据成员可以是变量。

  • 接口不是被类继承了,而是要被类实现。

  • 接口支持多继承(一个类只能继承一个抽象类,而一个类却可以实现多个接口)。

语法层次

在语法层次,java语言对于抽象类和接口分别给出了不同的定义。下面已Demo类来说明他们之间的不同之处。

  • 使用抽象类来实现:
public abstract class Demo {
    abstract void method1();
    
    
    void method2(){
        //实现
    }
}
  • 使用接口来实现
interface Demo {
    void method1();
    void method2();
}

抽象类方式中,抽象类可以拥有任意范围的成员数据,同时也可以拥有自己的非抽象方法,但是接口方式中,它仅能够有静态、不能修改的成员数据(但是我们一般是不会在接口中使用成员数据),同时它所有的方法都必须是抽象的。在某种程度上来说,接口是抽象类的特殊化。

对子类而言,它只能继承一个抽象类(这是java为了数据安全而考虑的),但是却可以实现多个接口。

设计层次

只有从设计理念的角度才能看出它们的本质所在。一般来说他们存在如下三个不同点:

  • 抽象层次不同 抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

  • 跨域不同 抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。

  • 设计层次不同 对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。

实例

我们有一个Door的抽象概念,它具备两个行为open()和close(),此时我们可以定义通过抽象类和接口来定义这个抽象概念:

- 抽象类

abstract class Door{
    abstract void open();
    abstract void close();
}

- 接口

interface Door{
    void open();
    void close();
}

至于其他的具体类可以通过使用extends使用抽象类方式定义Door或者Implements使用接口方式定义Door,这里发现两者并没有什么很大的差异。

但是现在如果我们需要门具有报警的功能,那么该如何实现呢?

解决方案一

给Door增加一个报警方法:clarm();

abstract class Door{
    abstract void open();
    abstract void close();
    abstract void alarm();
}

或者

interface Door{
    void open();
    void close();
    void alarm();
}

这种方法违反了面向对象设计中的一个核心原则 ISP (Interface Segregation Principle)1,在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。 这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变而改变,反之依然。

解决方案二

既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表两个不同概念的抽象类里面,定义的方式有三种:

  • 1、两个都使用抽象类来定义。
  • 2、两个都使用接口来定义。
  • 3、一个使用抽象类定义,一个是用接口定义。

由于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(){}
}

这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。


本文档 Github : https://github.com/bushehui/Java_tutorial

<script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"> </script> <script type="text/x-mathjax-config"> MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}}); </script>

Footnotes

  1. ISP(Interface Segregation Principle):面向对象的一个核心原则。它表明使用多个专门的接口比使用单一的总接口要好。
    一个类对另外一个类的依赖性应当是建立在最小的接口上的。
    一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。