本文最后更新于: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 { Object copy () ; }class Book implements 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{ float v = 2.0f ; }class Parent { int v = 3 ; }class Test extends Parent implements Frob { public static void main (String[] args) { new Test().printV(); } void printV{ System.out.println((super .v+Frob.v)/2 ); } }
接口的修饰符
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 () ; }
错误情况
在接口中方法都是不能用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 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()); } }
默认方法冲突
超类优先 :如果超类与接口的方法冲突,而超类提供了一个具体方法,同名而且有相同参数类型的接口默认方法将被忽略。
类必须重写冲突的方法 :如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突,代码如下:
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设计者更强调一致性,两个接口如何冲突并不重要,如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。