Bootstrap

Java接口详解

学习步骤:先了解接口是什么,它怎么用,再去了解他的一些常用特性

1 接口是什么 / 怎么用

接口(interface)是用来描述应该做些什么的一组需求,又或者说是规范。而该怎么做,就看它是怎么实现这个接口的了。而这里实现接口的类我们也称之为实现类

要将类声明为某个接口的实现类,就需要使用关键字implements 实现接口。

//Person类实现接口Comparable
public class Person implements Comparable{......}

为了让类实现一个接口,通常需要完成以下三个步骤:

  1. 先声明一个接口
  2. 在声明一个实现类,实现接口
  3. 最后实现类中对接口中的所有方法提供定义(重写)。

1.1 一个例子

例如,我编写了一个处理数据的软件,而这个软件对外暴露了API,用以接收其它人写的程序传输数据进我的软件,好帮他处理这些传输进来的数据。而该怎样连接我这个软件,我只定义了一个接口,必须将这个接口的实现类传入API中才能进行连接:

//伪代码,用来做示例的,不要太在意细节
public interface Login {  
    //创建连接  
    void createConnection();  
    //准备数据  
     void prepareData();  
    //进行连接  
    boolen toConnect();  
}

而每个人都有各自的连接方式,有些人喜欢在准备数据阶段验证数据的完整性,有些人喜欢在连接时添加日志功能等等…而不管那些人要怎么连接,但他一定要创建一个类实现这个接口,并完成我这个接口里规定的方法,这就是规定类应该做些什么。

假如这个是Tom实现Login接口的方式:

public class TomLogin implements Login{
    @Override
    public void createConnection() {
        System.out.println("这是创建连接的方法实现");
    }

    @Override
    public void prepareData() {
        System.out.println("验证数据是否完整,如果完整,则继续,否则退出程序");
        System.out.println("这是准备数据的方法实现");
    }

    @Override
    public boolean toConnect() {
        System.out.println("这是开始连接的方法实现");
		return true;
    }
}

这个是Black实现Login接口的方式:

public class BlackLogin implements Login{  
  
    @Override  
    public void createConnection() {  
        System.out.println("日志:创建连接");  
        System.out.println("这是创建连接的方法实现");  
    }  
  
    @Override  
    public void prepareData() {  
        System.out.println("日志:准备数据");  
        System.out.println("这是准备数据的方法实现");  
    }  
  
    @Override  
    public boolean toConnect() {  
        System.out.println("日志:开始连接");  
        System.out.println("这是开始连接的方法实现");  
        return true;    }  
}

这两个实现类都可以完成对我的软件的连接,只是连接的方式不同而已,当Tom和Black将他们的实现类开源后,其它人就可以拷贝他们的实现类,直接用在自己的程序中用以连接我的软件。

1.2 更实用的例子

在Java的Arrays类中,sort方法可以对 对象数组 进行排序,但该对象必须实现Comparable接口。

//<T>是泛型,用来限制参数类型的,这里不懂可以无视它,看下面一个接口,作用一样
public interface Comparable<T>{
	int CompareTo(T other);
}

public interface Comparable{
	int CompareTo(Object other);
}

现在有一个自定义类Person,希望通过Arrays类的sort方法根据Person的age属性排序,故Person类就必须实现Comparable接口,并实现 CompareTo方法。

  • 使用泛型
public class Person implements Comparable<Person>{
    public String name;
    public int age;

    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }

    @Override
    public int compareTo(Person person) {
        return this.age-person.age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • 不使用泛型
public class Person implements Comparable{
    public String name;
    public int age;

    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }

    @Override
    public int compareTo(Object obj) {
        Person p = (Person) obj;
        return this.age - p.age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

}
public static void main(String[] args) {
	/*定义一个Person类的数组*/
    Person[] personArray = new Person[3];
    personArray[0] = new Person("小明",10);
    personArray[1] = new Person("小红",15);
    personArray[2] = new Person("小蓝",8);

	/*使用sort排序前*/
    for (Person p : personArray){
        System.out.println(p);
    }
    //Person{name='小明', age=10}
    //Person{name='小红', age=15}
    //Person{name='小蓝', age=8}

    Arrays.sort(personArray);
	/*使用sort排序后*/
    for (Person p : personArray){
        System.out.println(p);
    }
    //Person{name='小蓝', age=8}
    //Person{name='小明', age=10}
    //Person{name='小红', age=15}
}

现在可以看到想让一个类使用Arrays提供的排序服务就必须实现 Comparable接口,并实现compareTo方法。因为Arrays.sort()需要被排序的对象提供比较两者大小的方法,也就是Comparable.compareTo(),它的返回值决定了两个Person对象谁大谁小,知道大小才能排序。

而为什么不能直接在Person类里直接定义一个compareTo方法呢?那是因为Java是一种强类型语言,在调用方法时,编译器要能检查到这个方法确实存在。实现 Comparable接口后,就会强制令实现类要实现compareTo方法,否则就无法运行,从而保证了compareTo方法一定会出现在实现类中

这就相当于,String name,必须要接收字符串对象, 接口Comparable的实现类必须要有compareTo方法一样。

小结

看完本小节你应该知道

  • 接口是什么?
  • 实现类是什么?
  • 接口的作用是什么?
  • 接口怎么用?

2 接口的特点

  • 一个类可以实现一个或多个接口。可以使用逗号将想要实现的多个接口分隔开。
public class Person implements Comparable,Iterable, Comparator{...}
  • 接口中的所有方法都默认是public方法。(因此在接口中声明public方法时不必提供关键字public)

  • 接口中的方法若是public修饰的,则只能是没有方法体的“抽象方法”。若方法是用private修饰的,则可以有方法体。

private void test1(){  
    System.out.println("方法体");  
}  

public void test2(); //等价于:void test2();  因为接口中方法默认为public
  • 接口中的字段总是public static final 修饰的,也就是说只要是在接口中定义的属性,它就是一个公开的静态常量。
public interface InterfaceProper {  
    int ID = 0;  
}
public class Test{
    public static void main(String[] args) {
        System.out.println(InterfaceProper.ID); //0
    }
}

3 接口类型的属性

接口不是类,不能使用new实例化一个接口。虽然不能直接创建对象,但确能声明接口类型的变量。

还记得第一小节中的Comparable接口吗?在“更实用的例子”中,使用Person类实现了此接口,因此如果要声明Comparable类型的变量,其的引用值就必须为这个Comparable接口的实现类对象,也就是Person类的对象。

//错误  
Comparable comparable = new Comparable();  

//正确  
Comparable comparable1 = new Person("小明",11);

instanceof运算符不仅可以检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口,如:

Person person = new Person("小红",12);  
System.out.println(person instanceof Comparable);//true 

//当然,反过来用也是可以的
Person person = new Person("小红",12);  
System.out.println(comparable instanceof Person);//true

4 静态和私有方法

自Java8起就允许在接口中增加静态方法,静态方法里有方法体。

接口中的方法也可以是private修饰的。private方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们通常作为接口中其它方法的辅助方法。

5 接口与抽象类

看完以上内容,是否发现接口和抽象类很像?

  • 不能实例化:接口和抽象类都不能直接实例化。它们都需要通过具体的类来实现或继承,然后才能创建对象。
  • 定义抽象行为:接口和抽象类都允许我们定义抽象方法,即只有声明而没有具体实现的方法。这些抽象方法需要在子类或实现接口的类中具体实现。
  • 作为类型引用:接口和抽象类都可以作为类型引用,比如定义变量、作为方法的参数或返回值类型等。

那为毛要有接口呢?用抽象类不一样可以规范子类实现特定行为吗?这里就讲抽象类和接口最大的区别:单继承和多实现。

每个类只能扩展(继承)一个类,而每个类可以实现多个接口。例如上个实现Person排序的例子,如果Comparable是一个抽象类,在Person继承了它并重写了CompareTo方法后,Person就不能再扩展(继承)第二个类了。

例如Java中还有一个很好用的接口Iterable,实现这个接口的类必须提供一个迭代器(Iterator),便可以遍历对象中的元素。Java中的集合类(如ListSet)都实现了Iterable接口。如果你想让自己的自定义类也能使用foreach迭代,就需要实现这个接口,但如果IterableComparable都是抽象类的话,那你的自定义类就只能选择一个功能了,而接口可以全都要!

像这样就可以让Person类既可以使用Arrays.sort()排序,又可以使用foreach迭代了:

public class Person implements Comparable,Iterable{......}

注意,上面Person数组可以被迭代,是因为数组本身在Java中有特殊的处理。当你使用for-each循环来迭代一个数组时,编译器会自动将for-each循环转换为传统的for循环,使用数组的索引来迭代每个元素。若Person实现了Iterable接口,则可以直接迭代Person对象里的内容。迭代Person[]和迭代Person是不一样的。

6 默认方法和默认方法冲突

从Java 8开始,接口可以包含默认方法,接口中的默认方法可以在不破坏现有实现的情况下向接口添加新的方法。有时,接口中的某些方法可能具有通用的实现逻辑,这些逻辑可以在所有实现类之间共享。通过将这些方法定义为默认方法,可以避免在每个实现类中重复相同的代码。而抽象类不支持这样的功能。

6.1 如何定义默认方法

还是拿之前假如我编写了一个处理数据的软件为例,这里因为业务原因要在此接口中添加打印当前系统时间的方法,并且打印时间的格式为yyyy-MM-dd HH:mm:ss,而这种业务完全可以使用默认方法实现,在不用重写其它实现类的情况下添加新的方法:

//伪代码,用来做示例的,不要太在意细节
public interface Login {  
    //创建连接  
    void createConnection();  
    //准备数据  
     void prepareData();  
    //进行连接  
    boolen toConnect();  

	//新添打印日期方法
    default void getDate(){
        // 获取当前日期和时间
        LocalDateTime now = LocalDateTime.now();

        // 创建一个DateTimeFormatter对象,并设置日期时间的格式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        // 使用DateTimeFormatter对象的format方法将LocalDateTime对象格式化为字符串
        String formattedDateTime = now.format(formatter);

        // 打印格式化后的日期时间字符串
        System.out.println(formattedDateTime);
    }
}

在接口中添加了打印日期的方法后,其它实现类也不用重写,原来是怎样的就怎样,只需要在使用实现类实例的使用调用getDate()方法就能使用在接口中定义的默认方法了。这与继承父类的方法十分相似。

//这是之前按Tom实现接口的类,不需要对实现类做修改,就可以使用新添的默认方法带来的功能
public class TomLogin implements Login{
    @Override
    public void createConnection() {
        System.out.println("这是创建连接的方法实现");
    }

    @Override
    public void prepareData() {
        System.out.println("验证数据是否完整,如果完整,则继续,否则退出程序");
        System.out.println("这是准备数据的方法实现");
    }

    @Override
    public boolean toConnect() {
        System.out.println("这是开始连接的方法实现");
        List list = new ArrayList();
        return true;
    }
}
public static void main(String[] args) {
    TomLogin tomLogin = new TomLogin();
    tomLogin.getDate();
    tomLogin.createConnection();
    tomLogin.prepareData();
    tomLogin.toConnect();
}

打印输出:

2024-04-21 21:45:19
这是创建连接的方法实现
验证数据是否完整,如果完整,则继续,否则退出程序
这是准备数据的方法实现
这是开始连接的方法实现

6.2 默认方法冲突

如果现在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?

例如:在接口 Login 中定义了getDate()方法打印当前系统时间,TomLogin是Login的实现类,但TomLogin又同时继承了TomFatherLogin类,并又实现了Register、Exit两个接口,最重要的是Login、TomFatherLogin、Register、Exit 例都定义了getDate()方法,当TomLogin实例对象调用getDate()方法时,被调用的getDate()到底是谁里面定义的getDate()?

关于这种问题,在Java中则遵循如下规则:

  1. 超类优先。如果超类(父类)提供了一个具体方法,接口中同名同参的默认方法就会被忽略。
  2. 强制重写方法。如果多个接口都提供了相同的方法,则Java编译器就会报错,强制让程序员重写该方法,解决这个多选一的问题。如下所示:
public interface Register {
    default void getDate(){
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);
        System.out.println("Register:\n"+formattedDateTime);
    }
}

在TomLogin实现了Login和Register接口后,IDE就会报错,强制你重写getDate()方法:

public class TomLogin implements Login,Register{
    @Override
    public void createConnection() {
        System.out.println("这是创建连接的方法实现");
    }

    @Override
    public void prepareData() {
        System.out.println("验证数据是否完整,如果完整,则继续,否则退出程序");
        System.out.println("这是准备数据的方法实现");
    }

    @Override
    public boolean toConnect() {
        System.out.println("这是开始连接的方法实现");
        List list = new ArrayList();
        return true;
    }

	//重写getDate(),指定一个getDate(),或者哪个接口的getDate()都不选,自己重写方法体
    @Override
    public void getDate() {
		//指定使用Login中的默认方法getDate()
        Login.super.getDate();
    }

}
;