Java 接口

本文最后更新于:2024年3月18日 凌晨

Java 接口

Java中不支持多重继承,而是通过接口实现比多重继承更强的功能,Java通过接口使处于不同层次,甚至互不关联的类可以具有相同的行为。

接口定义

  • 接口由常量和抽象方法组成,由关键字interface引导接口定义,具体语法如下:
1
2
3
4
[public]interface 接口名 [extends 父接口名列表]{
[public][static][final] 域类型域名 = 常量值;
[public][abstract] 返回值方法名(参数列表)[throw 异常列表];
}
  • 有关接口定义要注意以下几点:
    • 声明接口可给出访问控制符,用public修饰的是公共接口。
    • 接口具有继承性,一个接口还可以继承多个父接口,父接口间用逗号分隔。
    • 接口中所有属性的修饰默认是public static final,也就是均为静态常量。
    • 接口中所有方法的修饰默认是public abstract
  • 例如,所有的shape都有一个draw()area()成员方法,可以创建一个接口:
1
2
3
4
interface Shape{
void draw();// 用于绘制形状。
double area();// 用于求面积。
}
  • 接口是抽象类的一种,不能用于创建对象,接口的作用在于规定一些功能框架,集体功能的实现则由遵守该接口约束的类去完成。

接口的实现

  • 接口定义了一套行为规范,一个类实现这个接口就要遵循接口中定义的规范,也就是要实现接口中定义的所有方法,换句话说,在类中要用具体方法覆盖掉接口中定义的抽象方法。
  • 有关接口的实现,要注意以下问题:
    • 一个类可以实现多个接口,在类的声明部分用implements关键字声明该类将要实现哪些接口,接口间用逗号分隔。
    • 接口的抽象方法的访问限制符默认为public,在实现时要在方法头中显式地加上public修饰,这点很容易忽视。
    • 如果实现某接口的类没有将接口的所有抽象方法具体实现,则编译时将提示该类只能为抽象类,而抽象类是不能创建对象的。
  • 接口的多重实现机制在很大程度上弥补了Java类单冲继承的局限性,不仅一个类可以实现多个接口,而且多个无关的类可以实现同一接口。

[例8-2]:接口应用举例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Copyable{// 定义Copyable接口。
Object copy();
}

class Book implements Copyable{//Book类实现Copyable接口。
String book_name; // 书名。
String book_id; // 书号。

public Book(String name,String id){
book_name = name;
book_id = id;
}

public String toString(){
return "书名:"+book_name+",书号="+book_id;
}

public Object copy(){ // 覆盖接口中定义的抽象方法。
return new Book(book_name,book_id);
}

public static void main(String[] args) {
Book x = new Book("Java程序设计","ISBN8359012");
System.out.println(x);
System.out.println(x.copy());
Book y = (Book)x.copy(); // 赋值要用强制转换。
System.out.println(y);
}
}

书名:Java程序设计,书号=ISBN8359012
书名:Java程序设计,书号=ISBN8359012
书名:Java程序设计,书号=ISBN8359012
  • 本例定义了一个Copyable接口,其中包含copy()方法,在Book类中实现该方法,它将生成一个书名和书号相同的Book对象作为返回对象,程序中Book类的copy()方法类型为Object,所以第26行将返回结果赋给Book引用变量要进行强制转换,实际上,Book类的copy()方法也可将返回类型定义为Book类型,同样不违背接口定义,因为Book是Object的子类,那样的话,将copy()方法结果赋给Book引用变量就不需要强制转换。
  • 由于一个类可以继承某个父类同时实现多个接口,因此,也会带来多重继承上的二义性问题,例如,以下代码中Test类继承了Parent类同时实现了Frob接口,不难注意到,在接口和父类中均有变量v,这时通过Test类的一个对象直接访问v就存在二义性问题,编译将提示错误,因此,程序中通过super.v和Frob.v来具体指定是哪个v,事实上,这两个v不仅数值不同,而且性质不同,接口中的是常量,而类中定义的是属性常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interdace Frob{// 接口Frob定义。
float v = 2.0f;
}

class Parent{//Parent类定义。
int v = 3;
}

class Test extends Parent implements Frob{// 继承Parent类并实现Frob接口。
public static void main(String[] args){
new Test().printV();
}

void printV{
System.out.println((super.v+Frob.v)/2);
}
}

接口的修饰符

  • 通常使用java 的接口时都是不带修饰符的。
1
2
3
4
public interface Test {
int a = 0;
void test();
}
  • 其实修饰符都是默认省略,省略的都是默认必须要带的修饰,正确的类型应该是:
1
2
public static final int a = 0;
public abstract void test();
  • 接口中除了抽象方法和静态常量外还有以下组成。

静态方法

  • 静态方法必须要有方法体,并且静态方法是不能被实现类实现的,只能通过接口调用这个方法,例如:Test.test2();
1
2
3
4
5
6
7
8
9
10
public interface Test {
public static final int a = 0;
public void test();

public static void test2(){ // 正确的。

}

public static void test3();// 错误的。
}

default修饰方法

  • default加入就是为了解决接口中不能有默认方法的问题,在实现类中可以重写这个default方法也可以不重写。
  • default修饰的方法跟接口中的静态方法区别是default方法可以被实现类重写,这样可以得到扩展并且不修改原来接口中功能,而静态方法就有点太苛刻了,还不如把静态方法写在实现类中,这样每个实现类都可以自己写自己的功能实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Test {
public static final int a = 0;
public void test();

public static void test2(){
System.out.println("test2");
}

default void test3(){

}

default void test4();// 错误,default修饰方法必须要有实现体。

}

错误情况

在接口中方法都是不能用private和protected修饰的,这两种修饰就违背了面向对象的原则。

  • private好理解,接口是需要被实现才有意义的,不能被实现也就没有意义了。
  • protected需要理解面向对象的概念,protected是不在一个包内不能被访问,所以在类和接口不在同一个包内时就会有问题。
    • 假设public接口I有一个protected方法M,那么位于其他包的public类C就可以实现这个接口(方法M依然是protected),那么C的同包类D调用方法M只能这样访问:
1
2
3
4
5
C c = new C();
c.M();
// 无法这样访问:
I c = new C();
c.M();
  • 这样就失去了使用接口的重要意义:提供统一的接口,面向接口编程思想也无法体现。

为什么接口中的属性必须是public static final 修饰的

  • public:对外提供服务,让接口的实现类可以使用接口。
  • static:我们假设有两个接口A和B,而类C实现了接口A和B,假设,此时,A和B中都有一个变量N,如果N不是static类型的,那么在C中该如何区分N到底是A的还是B的呢?而,如果是static类型的,我们可以通过A.N和B.N来区别调用A或者B中的成员变量N
  • final:接口是一种通用的协议存在的,多个类实现同一个接口可以方便针对这个接口的调用类对接,所以final就是为了禁止在接口中不同的实现类修改属性造成混乱,如果变量不是final,那么每个实现接口的类就可以更改这个变量的值,也就违反了OCP(开闭原则)原则。

接口的继承

  • 一个接口使用关键字extends来继承自其他接口,关键字extends之后是以逗号分隔的继承接口名称列表。
  • 被继承的接口称为超级接口,继承接口的接口称为子接口。
  • 接口继承其超级接口的以下成员:
    1. 抽象和默认方法。
    2. 常量字段。
    3. 嵌套类型。
  • 接口不从其超级接口继承静态方法。
  • 接口可以重写它从其超级接口继承的继承的抽象和默认方法。
  • 如果超级接口和子接口具有相同名称的字段和嵌套类型,则子接口获胜。
1
2
3
4
5
6
7
8
9
10
11
interface A {
String s = "A";
}
interface B extends A {
String s = "B";
}
public class Main {
public static void main(String[] argv){
System.out.println(B.s);
}
}
  • 以下代码显示如何重写默认方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface A {
default String getValue(){
return "A";
}
}
interface B extends A {
default String getValue(){
return "B";
}
}

class MyClass implements B{
}

public class Main {
public static void main(String[] argv){
System.out.println(new MyClass().getValue());// B
}
}

默认方法冲突

  1. 超类优先:如果超类与接口的方法冲突,而超类提供了一个具体方法,同名而且有相同参数类型的接口默认方法将被忽略。
  2. 类必须重写冲突的方法:如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突,代码如下:
1
2
3
4
5
6
7
8
9
interface Named
{
default String getName(){ return getClass().getName() + "_" + hashCode():}
} 

interface Person
{
default String getName(){ return getClass().getName() + "_" + hashCode():}
} 
  • 如果有一个类同时实现了这两个接口会怎么样呢?
1
2
3
4
class Student implements Person,Named
{
...
}  
  • 类会继承Person和Named接口提供的两个不一致的getName方法,并不是从中选择一个,Java编译器会报告一个错误,让程序员来解决这个二义性,只需要在Student类中提供一个getName方法,在这个方法中,可以选择两个冲突方法中的一个,如下所示:
1
2
3
4
5
class Student implements Person,Named
{
public String getName(){ return Person.getName();}
...
}
  • 现在假设Named接口没有为getName提供默认实现:
1
2
3
4
interface Named
{
String getName();
}
  • Students类会从Person接口继承默认方法吗?这好像挺有道理,不过,Java设计者更强调一致性,两个接口如何冲突并不重要,如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。