一、接口是什么
我的电脑上有 USB 接口。当我想要使用无线鼠标的时候,只需要将无线鼠标的 USB 连接头插入其中,无线鼠标就能使用了;当我想要给我的手机充电时,只需要将充电线的 USB 头插入其中,手机就开始充电了;当我想要获取 U 盘中的资料信息时,将其插入 USB 接口中,U 盘就连接上我的电脑了。
这就是我们生活中的接口,试想如果电脑上没有这样的一个 USB 接口,是不是就意味着,每当想要实现以上的某种功能时,就需要大费周章的在电脑上造一个新的口来实现这一项功能,想要实现的功能太多太多,电脑迟早变成蜂窝,上面全是为了实现某一项功能而特地创造的口。
如果是在代码中举例,就应该是这样的。。。
某机器人公司发明了两种机器人,机器人A和机器人B,这俩机器人都有共同点,那就是都会说话,都拥有自己的名字,不同功能是A会走路,B会唱歌,如果用代码就应该是这样的。。。
📑代码示例:
class Robot {
public String name;
public Robot(String name) {
this.name = name;
}
public void talking() {
System.out.println(this.name + "会说话!");
}
}
class RobotA extends Robot {
public RobotA(String name) {
super(name);
}
public void walking() {
System.out.println(this.name + "会走路!");
}
}
class RobotB extends Robot {
public RobotB(String name) {
super(name);
}
public void singing() {
System.out.println(this.name + "会唱歌!");
}
}
public class TestDemo {
public static void main(String[] args) {
RobotA robotA = new RobotA("机器人A");
robotA.talking();
robotA.walking();
RobotB robotB = new RobotB("机器人B");
robotB.talking();
robotB.singing();
}
}
🏸 代码结果:
问题:
-
如果有一天,机器人A也会唱歌了,那么 RobotA 这个类是不是要进行修改,加个唱歌功能?
-
如果有一天,不想要机器人B有说话这一功能了,那么 Robot 这个类是不是要进行修改,删了说话功能,可是机器人A是会说话的,那么连同 RobotA 这个类岂不是也要进行修改?
- 既然功能这么多,干脆将这些功能都写成类吧,统统继承!统统继承!!!问题是一个机器人有好几项功能,但是
Java 并不支持多继承
啊。。。
这种关键时候,就将隆重介绍出本节的主题对象:接口
!
接口就是一种解决 Java 不支持多继承这一问题的技术,它是用来描述一个类应该应该会做些什么,实现什么功能,具体这个功能是怎么实现的是不指定的,而是由实现接口的类来实现接口中的方法
。
一个类是可以实现一个或多个接口的,有些情况可能要求符合这些接口,只要有这种要求,就可以使用实现了这个接口的类的对象。
二、初识接口
接口的声明:
[可见度] interface 接口名称 [extends 其他的接口名] {
// 声明变量
// 抽象方法
}
💬代码解释:
- 接口是由
interface
关键字来修饰的- 接口中的方法一般来说
不能有普通方法
,方法只能是抽象方法
,且接口中的方法默认是public abstract
修饰的- 接口中的成员变量,只能是
静态常量
,默认是public static final
修饰的- 在 JDK 1.8 开始,接口中的方法也可以是普通的方法,且这些方法被
default
关键字修饰,但是这种用法可以存在,但很少用- 创建接口的时,接口名称一般以大写字母
I
开头,一般用形容词词性
进行命名- 为了保持代码的简洁明了,默认修饰的符号就不要添加在代码当中
机器人代码的改进:
class Robot {
public String name;
public Robot(String name) {
this.name = name;
}
}
interface IWalking {
void walking();
}
interface ISinging {
void singing();
}
interface ITalking {
void talking();
}
class RobotA extends Robot implements IWalking,ITalking {
public RobotA(String name) {
super(name);
}
@Override
public void walking() {
System.out.println(this.name + "有走路功能");
}
@Override
public void talking() {
System.out.println(this.name + "有说话功能");
}
}
class RobotB extends Robot implements ISinging {
public RobotB(String name) {
super(name);
}
@Override
public void singing() {
System.out.println(this.name + "有唱歌功能");
}
}
public class TestDemo {
public static void main(String[] args) {
IWalking iWalking = new RobotA("机器人A");
iWalking.walking();
ISinging iSinging = new RobotB("机器人B");
iSinging.singing();
}
}
🏸 代码结果:
💬代码解释:
接口是
不能进行实例化
的,且它们不是被类继承了,而是要被类实现让类实现一个接口通常有两个步骤:
将类声明为实现给定的接口
对接口中的所有抽象方法提供定义(提供具体实现)
要将类声明为实现某个接口,需要用到
implements
关键字
- 一个类可以使用 implements 关键字实现多个接口,接口之间用
逗号
进行隔开,由此来实现类似多继承
的效果- 接口和接口之间,使用
extends
关键字来维护,说明该接口扩展了其他接口的功能,在类中都需要将这些接口进行具体实现- 接口也可以发生
向上转型
和多态
总结:
- 接口表达的含义就是 XXX 具有
XXX特性(功能)
- 接口带来的好处是,让程序员不需要知道什么具体类型,只需要知道这个类是否具有某种特性(功能)。比如,不止机器人,人也有说话,走路,唱歌这样的特性,因此拿人这个类实现这些接口就行
三、接口使用实例
3.1 Comparable 和 Comparator 接口
我们知道想要将一个基本类型的数组进行排序可以使用 Arrays
这个类底下的 sort()
方法,使用该方法进行排序后数组里的数据将会从小到大进行排序。
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
int[] array = {3,20,15,4,1};
System.out.println("排序前:" + Arrays.toString(array));
Arrays.sort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
}
🏸 代码结果:
那么如果想要用该方法将一个自定义类型进行排序,结果又当如何,实际上就算没有进行排序我们也应该可以预料的到,结果是不可行的,因为自定义的类型中会有很多可以排序的依据。。。
📑代码示例:
class Book {
public double price;
public String name;
public Book(double price, String name) {
this.price = price;
this.name = name;
}
@Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
}
public class TestDemo {
public static void main(String[] args) {
Book[] books = new Book[3];
books[0] = new Book(32.3,"C语言");
books[1] = new Book(53.4,"Java详解");
books[2] = new Book(23.6,"神奇的C++");
System.out.println("排序前:" + Arrays.toString(books));
Arrays.sort(books);
System.out.println("排序后:" + Arrays.toString(books));
}
}
🏸 代码结果:
结果显示在类型转换上出现了异常,那么为了正确的排序,应该怎么做呢?
这里就涉及到 Comparable
和 Comparator
接口,这两种接口分别对应着两种排序的方法。
首先,介绍 Comparable
接口
📑代码示例:
class Book implements Comparable<Book>{
private double price;
private String name;
public Book(double price, String name) {
this.price = price;
this.name = name;
}
@Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
@Override
/*public int compareTo(Book o) {
//从小到大排序
return (int) (this.price - o.price);
//从大到小排序
//return (int) (o.price - this.price);
}*/
public int compareTo(Book o) {
return this.name.compareTo(o.name);
}
}
public class TestDemo {
public static void main(String[] args) {
Book[] books = new Book[3];
books[0] = new Book(32.3,"C语言");
books[1] = new Book(53.4,"Java详解");
books[2] = new Book(23.6,"神奇的C++");
System.out.println("排序前:" + Arrays.toString(books));
Arrays.sort(books);
System.out.println("排序后:" + Arrays.toString(books));
}
}
🏸 代码结果:
💬代码解释:
- Book 为自定义类型,books 为 Book 类型的数组
Comparable
是一个接口,拥有排序的功能,使Book
类型实现该接口,该类型就拥有了排序
的功能<>
中放的是需要拥有排序功能的类型名称
,此处是 Book 类型- 实现了接口之后需要在 Book 类中对该接口中的
compareTo
方法进行具体的实现。用当前对象与 o 进行比较,如果这个对象小于 o 则返回一个负整数;如果相等则返回0;否则的话返回一个正整数。- 如果以 price 进行排序,需要将比较的结果
强制类型
转换成 int 类型,如果以 name 进行排序,则需要用到compareTo
方法
显而易见,通过Comparable 接口,我们成功地将自定义类型的数组按照我们想要的结果进行排序,并且和基本类型的数组排序相比较,它可以更加自由的决定从小到大排序还是从大到小排序;但是缺点也非常的明显,该接口对类的侵入性比较强
,每次想要换一种比较的方式,都需要对类内部的 compareTo 方法进行修改,制定新的比较规则,局限性很大,因此该接口相当于“内部比较器”
。
为了解决这一问题,现在隆重向大家推出另一种使类具有排序功能的接口——Comparator
接口。
📑代码示例:
class Book {
public double price;
public String name;
public Book(double price, String name) {
this.price = price;
this.name = name;
}
@Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}
}
//按照价格进行排序的比较器
class PriceComparator implements Comparator<Book> {
@Override
public int compare(Book o1, Book o2) {
return (int)(o1.price - o2.price);
}
}
//按照书名进行排序的比较器
class NameComparator implements Comparator<Book> {
@Override
public int compare(Book o1, Book o2) {
return o1.name.compareTo(o2.name);
}
}
public class TestDemo {
public static void main(String[] args) {
Book[] books = new Book[3];
books[0] = new Book(32.3,"C语言");
books[1] = new Book(53.4,"Java详解");
books[2] = new Book(23.6,"神奇的C++");
System.out.println("排序前:" + Arrays.toString(books));
PriceComparator priceComparator = new PriceComparator();
//NameComparator nameComparator = new NameComparator();
Arrays.sort(books,priceComparator);
System.out.println("排序后:" + Arrays.toString(books));
}
}
🏸 代码结果:
💬代码解释:
- 为了避免制造出太多代码量(提供 Getter 和 Setter方法),这里将两个成员变量用 Public 来修饰,一般来说为了加强封装,应该用 Private 来修饰
- 当某个类并未实现
Comparable
接口时,我们建立比较器
来进行排序,该比较器只需要实现 Comparator 接口即可- 实现该接口需要重写其中的
compare
方法,来比较其中的两个参数 o1 和 o2 的大小, o1 比 o2 小,返回负数;o1 和 o2 相等,返回0;否则的话返回一个正整数。- 在调用 Arrays.sort 时,传参不仅要传自定义类型数组,也要传比较器
可以发现使用 Comparator 接口不仅拥有 Comparable 接口的优点,也避免了其缺点即对类的侵入较为严重,想要哪种比较方法,直接可以在类的外面写一个新的比较器,因此该接口相当于“外部比较器”
。
3.2 Cloneable 接口
Cloneable
接口是 Java 中用于拷贝对象的标记接口
,该接口是一个空接口
,某个类实现了该接口说明该类是可以被拷贝的 。
📑代码示例:
class Book implements Cloneable{
public double price = 9.9;
public String name = "神奇的JAVA";
/* @Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
'}';
}*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Book book = new Book();
System.out.println(book);
Book book1 = (Book)book.clone();
System.out.println(book1);
}
}
🏸 代码结果:
重写 toString() 方法后
💬代码解释:
- Book 类实现了 Cloneable 接口,拥有了被拷贝的能力,然后需要在类中重写
Object
类中的clone
方法,然后调用父类(Object类)的 clone 方法,对象才会克隆成功- 如果没有实现接口就会抛出异常:CloneNotSupportedException
- 通过结果可以看出 book1 的地址和 book 的地址是不一样的,但是内容是一样的,说明克隆成功
- 此时的拷贝是完全拷贝的,通过 book1 修改其中的值是不会影响 book 中的值
关于拷贝,就不得不提到深拷贝和浅拷贝了,在 Java 详解数组这一节中有讲到数组的拷贝,其中就涉及这个。
【Java】剖析数组的使用
那么对于对象拷贝也会有这样的问题,如果拷贝的对象中还存在着另一个对象,那么用之前的 clone 方法是没有办法将对象中的对象的内容进行拷贝的,此时的拷贝只是浅拷贝。
📑代码示例:
class Number{
public int number = 23;
}
class Book implements Cloneable{
public double price = 9.9;
public String name = "神奇的JAVA";
public Number num = new Number();
@Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
", num.number=" + num.number +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Book book = new Book();
Book book1 = (Book) book.clone();
System.out.println("修改前");
System.out.println(book);
System.out.println(book1);
book1.num.number = 44;
System.out.println("修改后");
System.out.println(book);
System.out.println(book1);
}
}
🏸 代码结果:
💬代码解释:
从结果中可以看出,由于未进行完全的拷贝,导致可以通过新拷贝的 book1 将 book 中的 num 指向的对象的内容改变
想要进行深拷贝,将对象进行完全的拷贝,就需要将 clone 方法再次进行重写,这回要将对象中的对象也拷贝一份
📑代码示例:
class Number implements Cloneable{
public int number = 23;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Book implements Cloneable{
public double price = 9.9;
public String name = "神奇的JAVA";
public Number num = new Number();
@Override
public String toString() {
return "Book{" +
"price=" + price +
", name='" + name + '\'' +
", num.number=" + num.number +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Book bookClone = (Book)super.clone();
bookClone.num = (Number)this.num.clone();
return bookClone;
}
}
public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Book book = new Book();
Book book1 = (Book) book.clone();
System.out.println("修改前");
System.out.println(book);
System.out.println(book1);
book1.num.number = 44;
System.out.println("修改后");
System.out.println(book);
System.out.println(book1);
}
}
🏸 代码结果:
💬代码解释:
- 从结果中可以看出,由于进行了完全的拷贝,通过新拷贝的 book1 是没有办法将 book 中的 num 指向的对象的内容改变
- 所做的修改:
- 使 Number 类型也具有被拷贝的能力,实现 Cloneable 接口,然后需要在类中重写 Object 类中的 clone 方法,然后调用父类(Object类)的 clone 方法
- 重写 Book 类型中 clone 方法,使对象中成员指向的对象也被拷贝
完!