前言
推荐一个网站给想要了解或者学习人工智能知识的读者,这个网站里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏网站,请点击下方链接查看。 https://www.captainbed.cn/f1
Java的接口是一种引用类型,是方法的集合,它不能被实例化,但可以被类实现。接口定义了一组规范,实现该接口的类必须遵循这些规范。接口主要用于实现多态性,提高代码的灵活性和可维护性。
提示, IDEA 中使用 ctrl i
快速实现接口
一、接口是什么
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量.
在Java中,接口是一种抽象类型,它定义了一组方法的契约(contract
),但没有具体的实现。
接口可以看作是一种规范,用于描述类应该具有哪些方法,并且允许多个类实现同一个接口。
接口使用关键字"interface
"来定义,其中可以声明方法的签名但不包含实现细节。其他类可以实现接口并提供具体的方法实现。一个类可以实现多个接口,从而获得多态性的优势。
接口可以用于定义公共行为,提供一种约束机制,使得不同的类能够按照相同的方式进行交互。接口还可以用于实现回调机制,允许类将自身的功能注册给其他类,以实现特定的逻辑。
在Java中,接口是一种强大的工具,提供了一种灵活的方式来定义类之间的关系和交互。它是面向对象编程中重要的概念之一。
二、接口的语法规则
引例
我们可以继续按照我的上篇文章的示例进行拓展
代码语言:javascript复制class Shape {
public void draw() {
// 啥都不用干
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
/我是分割线//
// Test.java
public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawShape(shape1);
drawShape(shape2);
drawShape(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
我们的父类 Shape
并没有包含别的非抽象方法, 也可以设计成一个接口
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
- 使用
interface
定义一个接口 - 接口中的方法一定是抽象方法, 因此可以省略
abstract
- 接口中的方法一定是
public
, 因此可以省略public
Cycle
使用implements
继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
- 接口不能单独被实例化.
接口中只能包含抽象方法. 对于字段来说, 接口中只能包含静态常量(final static).
代码语言:javascript复制interface IShape {
void draw();
public static final int num = 10;
}
其中的 public
, static
, final
的关键字都可以省略. 省略后的 num
仍然表示 public
的静态常量.
- 我们创建接口的时候, 接口的命名一般以大写字母
I
开头. - 接口的命名一般使用 “形容词” 词性的单词.
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.
扩展(extends) vs 实现(implements)
扩展指的是当前已经有一定的功能了, 进一步扩充功能.
实现指的是当前啥都没有, 需要从头构造出来.
一个错误的代码
代码语言:javascript复制interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println("□") ; //权限更加严格了,所以无法覆写。
}
}
代码语言:javascript复制interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
public void draw() {
System.out.println("□") ; //可以覆写。
}
}
实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends
一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
示例
现在我们通过类来表示一组动物.
代码语言:javascript复制class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.
代码语言:javascript复制interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物
猫, 是会跑的.
代码语言:javascript复制class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name "正在用四条腿跑");
}
}
鱼, 是会游的.
代码语言:javascript复制class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name "正在用尾巴游泳");
}
}
青蛙, 既能跑, 又能游(两栖动物)
代码语言:javascript复制class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name "正在蹬腿游泳");
}
}
还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”
代码语言:javascript复制class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
继承表达的含义是 is - a
语义, 而接口表达的含义是 具有 xxx 特性 .
- 猫是一种动物, 具有会跑的特性.
- 青蛙也是一种动物, 既能跑, 也能游泳
- 鸭子也是一种动物, 既能跑, 也能游, 还能飞
作用
这样设计有什么好处呢?
时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.
例如, 现在实现一个方法, 叫 “散步”
代码语言:javascript复制public static void walk(IRunning running) {
System.out.println("我带着伙伴去散步");
running.run();
}
在这个 walk
方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行
Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
// 执行结果
我带着伙伴去散步
小猫正在用四条腿跑
我带着伙伴去散步
小青蛙正在往前跳
甚至参数可以不是 “动物”, 只要会跑!
代码语言:javascript复制class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
// 执行结果
机器人正在用轮子跑
三、接口使用实例
刚才的例子比较抽象, 我们再来一个更能实际的例子.
给对象数组排序
给定一个学生类
代码语言:javascript复制class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" this.name ":" this.score "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
代码语言:javascript复制Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
按照我们之前的理解, 数组我们有一个现成的 sort
方法, 能否直接使用这个方法呢?
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
java.lang.Comparable
仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定.
让我们的 Student
类实现 Comparable
接口, 并实现其中的 compareTo
方法
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" this.name ":" this.score "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort
方法中会自动调用 compareTo
方法.
compareTo
的参数是 Object
, 其实传入的就是 Student
类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0;
再次执行程序, 结果就符合预期了.
代码语言:javascript复制// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项:对于 sort
方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo
这样的能力. 通过重写 compareTo
方法的方式, 就可以定义比较规则.
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort
方法来完成刚才的排序过程(使用冒泡排序)
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound ) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
再次执行代码
代码语言:javascript复制sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
sort
Java中的sort
方法可以用来对数组或集合进行排序。它是Arrays
类和Collections
类中的静态方法。
对于数组,sort
方法可以直接对其进行排序,例如:
int[] arr = {5, 2, 1, 6, 3};
Arrays.sort(arr);
对于集合,可以使用sort
方法对其进行排序,例如:
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(2);
list.add(1);
list.add(6);
list.add(3);
Collections.sort(list);
sort
方法默认按升序排序,对于基本数据类型和字符串类型,可以直接使用默认的排序方法。对于自定义对象的排序,需要实现Comparable
接口或使用Comparator
接口来指定排序方法。
例如,对于自定义的Person
类,可以实现Comparable
接口来指定排序规则:
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造方法和getter/setter方法省略
@Override
public int compareTo(Person other) {
// 根据年龄进行排序
return this.age - other.age;
}
}
List<Person> list = new ArrayList<>();
list.add(new Person("Alice", 25));
list.add(new Person("Bob", 30));
list.add(new Person("Charlie", 20));
Collections.sort(list);
compareTo
compareTo
方法是Java中用于比较两个对象的方法,它定义在Comparable
接口中。在sort
方法中,如果要对自定义对象进行排序,需要实现Comparable
接口并重写compareTo
方法。
compareTo
方法有以下几种返回值:
- 返回负数:当前对象小于参数对象。
- 返回0:当前对象等于参数对象。
- 返回正数:当前对象大于参数对象。
在compareTo
方法中,需要根据自定义的比较逻辑来实现比较操作。例如,对于一个Person
类,可以根据姓名进行比较:
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造方法和getter/setter方法省略
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
在这个例子中,compareTo
方法调用了String
类的compareTo
方法,实现了按姓名的字典序进行比较。如果希望按照年龄进行比较,可以修改compareTo
方法的实现:
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造方法和getter/setter方法省略
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
在这个例子中,compareTo
方法返回的是两个Person
对象的年龄差值,实现了按年龄进行比较。
需要注意的是,compareTo
方法必须满足以下要求:
- 自反性:对于任意的
x
,x.compareTo(x)
应该返回0。 - 对称性:对于任意的
x
和y
,如果x.compareTo(y)
返回0,则y.compareTo(x)
也应返回0。 - 传递性:对于任意的
x
、y
和z
,如果x.compareTo(y)
返回0,并且y.compareTo(z)
返回0,则x.compareTo(z)
也应返回0。 - 一致性:对于任意的
x
和y
,如果x.compareTo(y)
返回非0值,则x.compareTo(y)
在多次调用中应保持一致的结果。 compareTo
方法与equals
方法保持一致性:如果x.equals(y)
返回true
,则x.compareTo(y)
应该返回0。
四、接口间的继承
接口可以继承一个接口, 达到复用的效果. 使用 extends
关键字.
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
通过接口继承创建一个新的接口 IAmphibious
表示 “两栖的”. 此时实现接口创建的 Frog
类, 就继续要实现 run
方法,也需要实现 swim
方法.
接口间的继承相当于把多个接口合并在一起.
在Java中,一个接口是可以同时继承多个其他接口的。Java接口允许使用关键字“extends
”来继承一个或多个其他接口。这种方式称为接口的多继承。一个接口可以继承一个或多个其他接口的方法和常量,并且可以通过实现该接口来实现多个接口的功能。这种设计允许在接口层级中实现代码的复用和灵活性,同时遵循Java中单继承的类限制。总体而言,Java的接口多继承提供了一种优雅的方式来定义多个相关接口之间的关系。
五、Clonable 接口和深拷贝
Java 中内置了一些很有用的接口, Clonable
就是其中之一.
Object
类中存在一个 clone
方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone
方法, 必须要先实现 Clonable
接口, 否则就会抛出 CloneNotSupportedException
异常.
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
// 输出结果
// false
浅拷贝 VS 深拷贝
Cloneable
拷贝出的对象是一份 “浅拷贝”
观察以下代码
代码语言:javascript复制public class Test {
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
return (B)super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
B b = new B();
B b2 = b.clone();
b.a.num = 10;
System.out.println(b2.a.num);
}
}
// 执行结果
10
通过 clone
拷贝出的 b
对象只是拷贝了 b
自身, 而没有拷贝内部包含的 a
对象. 此时 b
和 b2
中包含的 a
引用仍然是指向同一个对象. 此时修改一边, 另一边也会发生改变.
.
六、拓展
接口与抽象类的比较
- 接口与实现它的类不构成继承关系,即接口不是类继承体系的一部分,不相关的类可以实现相同的接口;而抽象类属于一个类的继承体系。
- 接口中的所有方法都是抽象的,而抽象类中既可以声明抽象方法,也可以声明非抽象方法。
- 一个类可以实现多个接口,但只能继承一个抽象类。
接口优势
使用接口的优势是
类通过实现多个接口实现多重继承,能够抽象出不相关类之间的相似性,而不必强行形成继承关系。利用接口让某些功能方法与某些类完全解耦,可以使方法更加灵活、通用,编写的代码可重用性更好。只有在必须使用方法或成员变量时,才应该考虑使用抽象类,否则用接口代替抽象类。