写在前面
关于Java接口网络上有很多很好的资料,理论上完全没有必要写一篇文章来介绍Java接口,但是我想在这里谈一下我对Java接口的理解,如有错误或者与各位大佬意见不合的地方欢迎指出。
接口的简单介绍(详细介绍后面再说)
接口可以简单概括为,一系列抽象方法的集合。或者按照《Java核心技术》一书中所言“接口不是类,而是对希望符合这个接口的类的一组需求”。
接口中的方法没有实现,只有声明(默认方法我们稍后再说)。
以下可以声明一个接口
public interface InterfaceExample {
void funExample();
}
接口不能创建实例,要使用它,我们需要在设计类的时候实现接口,同时实现接口中的抽象方法。
以下使用implements关键字定义一个类实现接口。
public class ClassExample implements InterfaceExample{
@Override
public void funExample() {
System.out.println("实现了接口");
}
}
为什么要有接口(接口有什么用)
1.向编译器保证类实现了某个方法
例如用Arrays.sort()方法对对象数组进行排序时就要求数组当中的元素一定要实现compareTo方法。而Java作为一个强类型语言,为确保安全,编译器在编译的时候就要确定数组当中的元素是否实现了compareTo方法,而使用接口就可以完美实现这一点。我们只需要定义一个接口,这个接口中只有compareTo这一个抽象方法,然后再要求调用Arrays.sort()的数组元素必须属于某个实现了这个接口的类就行了。编译器只需检查数组元素所属的类是不是实现了某个接口,而实现了某个接口的类当中一定有我们期望它拥有的方法。
Java的接口Comparable<>就是调用Arrays.sort()方法的数组元素所属的类所需要实现的,Comparable<>中只有一个compareTo抽象方法。
public class Student implements Comparable<Student>{
private String name;
private int id;
public String getName() {
return name;
}
public int getId() {
return id;
}
public Student(){}
public Student(String name,int id){
this.name = name;
this.id = id;
}
@Override
public int compareTo(Student o) {
return this.id-o.id;
}
@Override
public String toString(){
return name+" "+id;
}
}
public class Main {
public static void main(String[] args) {
Random rand = new Random();
Student[] students = new Student[10];
for(int i=0;i<10;i++){
students[i] = new Student("张"+(i+1),rand.nextInt(10000,20000));
}
Arrays.sort(students);
for (Student student : students) {
System.out.println(student);
}
}
}
2.满足多继承的需要
学习过c++的对多继承应该不会感到陌生
多继承指的是,一个子类可以继承自多个超类,Java是不允许多继承的。
原因也很简单,当出现如下一种情况时
也就是类B和类C都继承自类A,且类D同时继承自类B和类C时,会出现一个类D的对象当中有两个类A的的部分。对于允许多继承的编程语言来说(如c++),往往使用了复杂的方法来解决这个问题(c++使用了虚基类来避免这种情况,但这很复杂,所以c++程序员也很少用这个),Java为了避免变得跟c++一样复杂(毕竟Java在刚开始的时候以简单著称,相对与c++而言),所以取消了多继承。
但是多继承终归还是有需要的(不然c++为什么要提出这个概念呢),所以Java使用接口来实现多继承。类虽然只能继承只一个超类,但是可以实现多个接口。在实际的设计中,超类为主,用多个接口来提供辅助功能。
3.作为函数式接口传递代码块
接口的另一个重要作用就是作为函数式接口与lambda表达式一起使用,函数式接口就是只有一个抽象函数的接口。
因为Java没有加入函数类型,当我们需要传递一个代码块给一个函数时就需要通过接口。我们还是用Array.sort函数举例,但是这一回我不想用数组元素自带的compareTo方法进行排序或者元素本身就没有实现Comparable<>接口。
public class Student {
private String name;
private int id;
public String getName() {
return name;
}
public int getId() {
return id;
}
public Student(){}
public Student(String name,int id){
this.name = name;
this.id = id;
}
@Override
public String toString(){
return name+" "+id;
}
}
public class Main {
public static void main(String[] args) {
Random rand = new Random();
Student[] students = new Student[10];
for(int i=0;i<10;i++){
students[i] = new Student("张"+(i+1),rand.nextInt(10000,20000));
}
Arrays.sort(students,new comparator());
for (Student student : students) {
System.out.println(student);
}
}
}
class comparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.getId()-o2.getId();
}
}
这一回的Student类没有实现Comparable<>接口,而且我们向Arrays.sort函数当中传递了一个comparator类的对象,comparator类实现了另一个接口Comparator<>。
这是因为Arrays.sort方法有一个重载如下
public static <T> void sort(T[] a, Comparator<? super T> c)
可以看到sort的第二个参数是接口类型的,后面我们会说接口类型的变量可以引用实现了这个接口的类的对象。在这里我们实际上是通过接口传递了一个代码块进去,当Student类没有compareTo方法的时候,sort函数需要知道如何对Student进行排序,所以我们要传递一个代码块进去告诉sort如何排序。其他的支持函数式编程的语言如c++可以通过函数指针来传递代码块,但是Java并没有加入函数类型,只能通过对象的方式传递代码块(当然在实际开发当中使用lambda表达式更好,不过lambda表达式也是传递给函数式接口),接口在这里充当了重要作用,它告诉使用者该传入什么样的对象进去。
4.进行抽象定义,将定义和实现分开
Java集合类库当中的Collection,List,Map,Set等接口就是一个例子,如List接口的部分代码如下
public interface List<E> extends SequencedCollection<E> {
boolean isEmpty();
Iterator<E> iterator();
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E elemet);
void add(int index, E element);
E remove(int index);
ListIterator<E> listIterator();
}
List接口只定义了List列表的行为,如可以加入元素,删除元素,判断是否为空,返回一个迭代器等等,并没有提供具体实现。那么,为什么要用接口将定义与实现分离呢,还是用List接口来举例说明,实现List列表有很多种方法,比如可以用链表来实现,也可以用动态的数组来实现。将接口与实现分离,List接口只管定义列表需要有什么功能,而具体使用什么方法实现就交给具体的类去做比如ArrayList类采取动态数组的方式,LinkedList采取链表的方式实现List接口
接口的字段和方法
接口中的字段
接口当中不可以包含实例字段,但是可以包含静态常量。
public interface InterfaceExample {
public static final double PI = 3.1415926535;
}
不过上面声明常量的修饰符public static final是冗余的,接口中所有字段自动为public static final。
接口中的方法
默认方法
虽然接口是抽象方法的集合,但是你也可以为方法提供一个默认的实现,这样的方法叫做默认方法,用default修饰。
public interface InterfaceExample {
default void fun(){
System.out.println("fun");
}
}
在使用默认方法的时候会出现一些意外,比如Student类的超类Base中定义了方法a,而Student类又实现了一个接口B,B也包含一个名字叫a的默认方法。这就会造成默认方法冲突。
Java解决默认方法冲突遵循两个原则:1.超类优先,2.向程序员报告接口冲突。我们一个个来解释
1.超类优先:如果接口与超类造成默认方法冲突,则只会考虑超类方法,接口的默认方法被忽略(不论有多少个接口造成冲突)
2.报告接口冲突:如果造成冲突的是两个接口的默认方法,那么编译器会把这个问题报告给程序员,程序员必须决定改使用哪个接口,具体实现方式是在Student类里面提供方法a的实现,在实现方法a的时候可以选择调用哪个接口的默认方法,也可以自己重新实现。
public class Main {
public static void main(String[] args) {
Student a = new Student();
a.getInfo();
}
}
class Student implements Person,Named{
@Override
public void getInfo() {
Person.super.getInfo();//调用Person的方法
Named.super.getInfo();//调用Named的方法
System.out.println("Student");//自己实现方法
}
}
interface Person{
default void getInfo(){
System.out.println("person");
}
}
interface Named{
default void getInfo(){
System.out.println("name");
}
}
需要注意的是,如果Peson和Named两个接口当中只有Person(或者Named)的getInfo是默认的,而另一个不是,那么编译器任然会报告冲突并要求程序员解决。
静态方法
Java 8以后Java接口中的方法可以是静态的。这样做虽然有违接口的初衷,但是有时候确实需要静态方法。早期把静态方法放在与接口对应的伴随类里面。静态方法必须有实现(但它不是默认方法,不能使用default修饰)
public interface InterfaceExample {
static void fun(){
System.out.println("fun");
}
}
方法的控制域
Java的接口中的方法起先都默认是public,而无需用public关键字修饰,Java 9开始接口中的方法可以是private的,但默认控制域仍然是public。且private方法必须提供实现(但它不是默认方法,不能使用default修饰)
private只能由接口内的其他方法调用,只作为辅助方法使用。
public interface InterfaceExample {
void fun1();//默认是public
public void fun2();//是public
private void fun3() {//private,只能由其他方法调用
System.out.println("fun3");
}
}
总结
字段自动是public static final
实例方法通常是抽象的,但可以有默认实现,用default修饰。
可以包含静态方法且必须提供实现。
实例方法和静态方法自动是public,但是可以用private修饰,且私有方法必须要有实现,通常用作辅助方法
默认方法冲突遵循1.超类优先,2.向程序员报告接口冲突
接口变量
接口不是类,不能使用new对接口进行实例化
但可以声明接口变量,接口变量可以引用实现了接口的类的对象
public interface InterfaceExample {
void funExample();
}
public class ClassExample implements InterfaceExample{
@Override
public void funExample() {
System.out.println("实现了接口");
}
}
public class Main {
public static void main(String[] args) {
InterfaceExample a = new ClassExample();
a.funExample();
}
}
最后
接口可以继承,接口A可以继承自接口B,当实现接口A的时候也要实现接口B的抽象方法。