前言:
本文将通过两个问题对Java中的接口做出一些解释
看本文需要对Java最基础语法有一定了解,如类与对象、继承、多态等概念
Java中时常能听到一句话:单继承多接口,多接口可以弥补Java中单继承的缺点,这里的接口就是今天要介绍的特性了
第一个问题:接口是什么
地位
先明确一下接口的地位,它和类是一个级别的,怎么定义类就怎么定义接口
定义类时我们使用关键字class
,而定义接口需要关键字interface
引入
在学习类时我们应该了解过由abstract
关键字修饰的抽象类,有等式抽象类 = 抽象方法 + 其他方法 + 属性
而接口可以看作抽象类的PRO-MAX版,比抽象类更加抽象的我们管它就叫做接口
-
这里就不给出接口的官方定义了,直接给出接口的等式:
接口 = 抽象方法 + 常量
接口等式的产生是编译器要求的,其内所有的变量会默认设置为
public static final
修饰(常量),而方法默认为public abstract
(抽象方法),当我们尝试定义其他类型的变量和方法时,编译器是不会通过运行的,如果不使用访问修饰符编译器会默认为等式中的状态
依据上面的等式可以发现,接口里面是没有具体的方法和变量的,它相当于一个约定
如果说abstract
是单个函数声明,那么接口就类似于专门放函数声明的.h
文件一样的东西了
特性
-
我们知道抽象类因为其抽象性是不能被
new
(实例化)的,而接口作为抽象上的更进一步,自然更不能new
了 -
接口由抽象方法和常量组成,不能
new
对应抽象方法,而常量则表明连构造方法也不能有了,毕竟没有变量,构造也无从下手 -
之前学习到的继承是父类与子类间的继承,既然接口和类是一个级别的,那么接口自然不能去继承类,即一个抽象的东西不能去继承一个实际的东西,但是接口可以继承接口,且一次可以继承很多个,使用
extend
关键字后后面接上需要继承的接口即可,用逗号分开!!!注意:
学习继承时明确过Java中只有单继承,这里接口可以继承很多个只是一个意外
如果在面试中问到单继承问题,不要拿接口的多继承杠
使用
使用继承时有关键字extends
,而使用接口时的关键字则是implements
(实现),使用方法都是类似的
如果一个类实现接口,那么必须要实现接口里面的所有抽象方法,类似于继承时的重写,只不过继承时不要求必须重写,而接口的抽象方法必须全部实现
就像一个公司中项目经理给出任务的大概轮廓和步骤,而员工则要完成这些具体步骤的实现
与继承对比一下:继承时满足子类是一个父类
,而实现接口则满足类有一个接口的功能
现在我们有了一个实现接口的类,那么我们在new
这个类时,除了正常类名 = 实现类
以外
依据多态的知识,还可以使用接口 = 实现类
(eg. D类实现了A接口,而我们现在要new
一个D的引用对象,可以写A x = new D()
)
特殊接口
- 标记接口:接口里面什么都没有的接口
- 函数式接口:接口中只有一个新定义的抽象方法
JDK8后的新特性
- 用
default
关键字修饰后的方法可以在接口中写出默认的具体方法实现 - 接口内的方法可以用
private
修饰但必须要有具体的实现(实用性不广)
了解完接口的定义后,回到文章开头提到的多接口可以弥补单继承缺点的问题
对于这个问题,我个人的理解是有很多官方定义的接口并且有相应的抽象方法,如果要使用相应的方法多继承下可以继承多个父类实现,而单继承下则依赖于使用多接口后重写其抽象类,用其内部的逻辑去完成多继承中可以达到的效果
下面以一些实用的接口例子来理解一下这句话
第二个问题:有哪些实用接口
高级排序接口
我们知道排序一个最普通的数组时可以用数组工具类Arrays
里的sort
方法完成
那如果我们要对一个字符串数组按照其长度,首字母字典数等一系列要求进行不同的排序呢,这就有了高级排序接口的出现
高级排序接口分为内外部比较器两个,下来我们来依次看一看
Comparator
接口(外部比较器)
现在我们有一个字符串数组students
,里面包含了三个人名,想要按照名字的长短对它们进行排序
首先我们要有一个实现Comparator
接口的具体实现类的定义,这个类里面写什么呢?
实现Comparator
里面规定的抽象方法compare
,compare
的两个参数类型即为我们要比较的数组内的元素字符串String
而实现方法内的逻辑类似于我们在学习c语言的qsort
排序需要的cmp
辅助函数一样,返回一个数值表示两个参数的大小关系,表面是否需要交换
这里就不着重分析内部的逻辑了,可参考c语言中qsort排序的使用
public class LengthComparator implements Comparator<String> {//实现这个接口后面有一个<String>,这个是泛型,可以先简单理解为要比较元素的类型
public int compare(String i, String j) {
return i.length() - j.length();//按字段长短将字符串数组进行排序
}
}
现在我们就得到了一个具体的实现类,下来在main
方法中对这个实现类做一个引用实例化,再把这个引用作为数组工具类的一个参数传进去即可完成排序
public static void main(String[] args) {
String[] students = {"Petercc", "Mary", "Paudl"};
var comp = new LengthComparator();//这个var是自动判断类型用的
Arrays.sort(students, comp);
for (String i : students) {//输出数组里的所有元素
System.out.print(i + " ");
}
}
运行结果:
当然排序接口不光可以比较数组里的字符串,也可以比较数组里的对象(类似于c语言中比较结构体数组一样)
我们用内部比较器给出一个例子
Comparable
接口(内部比较器)
这两比较器的区别其实就是用不用写一个新的类去实现相应接口内的方法,方法内部的逻辑是一样的
内部即指直接在目标类实现Comparable
接口后的内部重写接口中的方法compareTo
(两个接口调用的方法名称不一样)
假设我们现在有一个学生类Student
public class Student implements Comparable{
int age;
String name;
Student(){};
Student(int age, String name) {
this.age = age;
this.name = name;
}
}
现在它要实现Comparable
接口,那么就要实现里面的compareTo
方法
可以发现该方法参数只有一个,因为是在内部,所以另一个参数实际上就是当前类的this
指代的所需要比较的变量
同时如果我们不像外部比较器一样改变参数的类型,直接用万能类型先接住传入的参数,那就需要在内部进行强制转化
而比较对象时也可以实现多个属性的比较,先用一个变量记录第一个属性的比较结果,如果第一个属性相同,则可以调用String
类本身自带的compareTo
方法去比较
public class Student implements Comparable{
int age;
String name;
Student(){};
Student(int age, String name) {
this.age = age;
this.name = name;
}
public int compareTo(Object o) {
Student inputPerson = (Person)o;//强制转化
int flag1 = this.age - inputPerson.age;//降序
if (flag1 == 0) {
this.name.compareTo(inputPerson.name);//String类内部实现的方法可以直接调
}
return flag1;//重写返回逻辑,先根据年龄排序,如果年龄相同,则根据名字排序
}
public String toString() {
return this.name + this.age;
}//重写toString方法,方便输出
}
我们在主函数里面运行一下试试:
public static void main(String[] args) {
Student[] t = new Student[]{
new Student(70, "小王"),
new Student(20, "小马"),
new Student(21, "张三"),
new Student(21, "李四"),
};//一次性实例化一个有多个属性的类
var ii = new StudentSortByAge();
Arrays.sort(t, ii);
System.out.println(Arrays.toString(t));//输出数组的简便方法
}
运行结果:
Cloneable
克隆接口
我们在前期学习所有类的父类Object
时应该有了解到其中的一个方法clone
如果我们直接去调这个方法,编译器会给我们报错
而要正确使用这个方法就要先实现Cloneable
接口,将其protected
访问权限改为public
现在我们用一个clonetest
类去实现这个接口
public class clonetest implements Cloneable{
int age;
public clonetest clone() throws CloneNotSupportedException{
return (clonetest) super.clone();
}
}
可以发现这个方法实现的很怪异,首先我们可以看到clone
方法没有参数,返回值的变成了类名,后面还有一个throws
跟上一大串,方法体内部的实现似乎还能理解一点,super.clone()
即调用了Object
里面的clone
方法,获得了一个新对象后再将其强制转化为我们需要的对象类型
现在就剩下了这个throws
,我们可以先简单知道它是一个抛出异常终止程序用的,没有这句话是无法正常实现Cloneable
方法的
我们现在写出main
方法试一下效果
public static void main(String[] args) throws CloneNotSupportedException{
//创建一个新的对象
clonetest hh = new clonetest();
hh.age = 18;
//创建两个引用,一个使用clone方法,另一个直接赋值
clonetest pp = hh.clone();
clonetest mm = hh;
//查看引用是否相同
System.out.println(hh == pp);
System.out.println(hh == mm);
//改变数值
hh.age = 30;
//展现效果
System.out.println(hh.age + " " + mm.age + " " + pp.age);
}
运行结果:
可以发现,使用clone
方法创建的对象已经是一个新的对象了,改变原对象的属性不会对它造成影响,而直接赋值的对象实际上只是创建了一个新的引用,两个引用指向的地方还是一样的,即两个引用相同
我们再来做一个测试,现在为原类中添加一个新的内部成员类,并将该类的一个引用作为原类的一个属性
public class clonetest implements Cloneable{
int age;
public A ll = new A();
public clonetest clone() throws CloneNotSupportedException{
return (clonetest) super.clone();
}
public class A{
int num;
}
}
修改一下main
仍然做一次测试,对成员对象里面的值进行修改
public static void main(String[] args) throws CloneNotSupportedException{
//创建一个新的对象
clonetest hh = new clonetest();
hh.age = 18;
hh.ll.num = 100;
//创建两个引用,一个使用clone方法,另一个直接赋值
clonetest pp = hh.clone();
clonetest mm = hh;
//查看引用是否相同
System.out.println(hh == pp);
System.out.println(hh == mm);
//改变数值
hh.age = 30;
hh.ll.num = 0;
//展现效果
System.out.println(hh.age + " " + mm.age + " " + pp.age);
System.out.println(hh.ll.num + " " + mm.ll.num + " " + pp.ll.num);
}
运行结果:
大事不妙了,拷贝的全新对象pp
里的成员对象的值也跟着改变了,这就要引出一个新的概念深浅拷贝了
先前我们对于clone
的实现实际上是浅拷贝
这样拷贝出来的新引用只拷贝了除了对象以外的值,当原引用的成员对象改变时,新引用的相关值仍然会发生改变,如果我们想要让这个新引用做到真真正正的新,就要使用深拷贝进行拷贝了
使用深拷贝的第一步要先实现不同的clone
接口,同样直接先给出实现
public clonetest clone() throws CloneNotSupportedException{
clonetest newhh = (clonetest) super.clone();//进行浅拷贝
ll = ll.clone();//该行决定着本次拷贝是深拷贝还是浅拷贝
return newhh;
}
不难看出深拷贝先进行了浅拷贝,然后将外层类里的成员对象拿出来单独进行了拷贝,其他细节保持不变
光进行这一步改变是无法实现深拷贝的,我们还需要对内部类做出一个改变
public class A implements Cloneable{
int num;
public A clone() throws CloneNotSupportedException{
return (A) super.clone();
}
}
第一步我们使用了内部类的clone
方法,正常情况下clone
方法是protected
保护的,如果我们直接拷贝绝对报错,所以内部类也得实现Cloneable
接口,确保上面对成员对象拷贝时的正确性
其实深拷贝就是两个浅拷贝合在一块,如果有成员对象,那就得对其单独处理,修改了这些,我们依然做上面的测试再来看看效果:
这次没问题了,新对象是真真切切的新了
总结一下:
只在类内部实现clone
接口调用时为浅拷贝
如果该类内部包含其他类对象的引用,浅拷贝无法将其他类对象内的值拷贝过去,这个时候需要用到深拷贝,在其他类中也实现clone
方法,在原类的clone
方法里加上对其他类引用的拷贝
Collection
集合接口和Map
接口
这里就先简单提一下这两个接口了(毕竟单拿出来讲都可以写成一篇博客了),不做具体的深入,这两个接口一般在做题的过程中较为多见
Collection
(单值集合)和Map
(键值对集合)不存在继承的关系
Collection
集合接口(单值)
Collection
是一个接口,List
和Set
可以简单理解为继承了Collection
接口的子接口,而蓝底白字的部分是其具体的实现类
特点:1. 元素可重复,输入顺序和输出顺序不一致,即无序; 2. 长度自适应,随操作而改变
Collection
里规定的抽象方法
Map
接口(双值)
特点:1. 根据键(唯一)找到值(不唯一),键值一一对应; 2.输入输出的顺序为无序
常用方法
Hash map = new HashMap();
map.put("00000001", "张三");//插入数据:前为键,后为值
map.get("00000001");//获取数据:根据键寻找值
int i = map.size();//获取数据量
System.out.println(map.containsKey("00000001"));
System.out.println(map.containsValue("张三"));//根据键或值查询元素是否存在
List lsi = map.values();
Set set = map.keySet();//双值转化为单值存储,可用于遍历,第二行不能用List,List元素不是唯一的
map.remove("00000001");//根据键删除值,删除的返回值为键对应的值对象,Collection里的remove返回值是boolean反映是否删除成功
map.getOrDefault("00000001", 0);//当map集合中有这个key时,就使用这个key对应的value值,如果没有这个key就使用默认值defaultValue
本文到这里就结束了,博主也是初学,如果文章中有错误请帮忙指出!感谢阅读!