阶段性探寻Java接口的奥秘

本文详细介绍了Java中的接口,包括接口的地位、定义、特性,以及如何使用接口实现多接口以弥补单继承的不足。特别关注了Comparator、Comparable和Cloneable接口的实际应用,以及Collection和Map接口的基础知识。
摘要由CSDN通过智能技术生成

前言:

本文将通过两个问题对Java中的接口做出一些解释

看本文需要对Java最基础语法有一定了解,如类与对象、继承、多态等概念


Java中时常能听到一句话:单继承多接口,多接口可以弥补Java中单继承的缺点,这里的接口就是今天要介绍的特性了

第一个问题:接口是什么

地位

先明确一下接口的地位,它和类是一个级别的,怎么定义类就怎么定义接口

定义类时我们使用关键字class,而定义接口需要关键字interface

引入

在学习类时我们应该了解过由abstract关键字修饰的抽象类,有等式抽象类 = 抽象方法 + 其他方法 + 属性

而接口可以看作抽象类的PRO-MAX版,比抽象类更加抽象的我们管它就叫做接口

  • 这里就不给出接口的官方定义了,直接给出接口的等式:接口 = 抽象方法 + 常量

    接口等式的产生是编译器要求的,其内所有的变量会默认设置为public static final修饰(常量),而方法默认为public abstract(抽象方法),当我们尝试定义其他类型的变量和方法时,编译器是不会通过运行的,如果不使用访问修饰符编译器会默认为等式中的状态

依据上面的等式可以发现,接口里面是没有具体的方法和变量的,它相当于一个约定

如果说abstract是单个函数声明,那么接口就类似于专门放函数声明的.h文件一样的东西了

特性

  1. 我们知道抽象类因为其抽象性是不能被new(实例化)的,而接口作为抽象上的更进一步,自然更不能new

  2. 接口由抽象方法常量组成,不能new对应抽象方法,而常量则表明连构造方法也不能有了,毕竟没有变量,构造也无从下手

  3. 之前学习到的继承是父类与子类间的继承,既然接口和类是一个级别的,那么接口自然不能去继承类,即一个抽象的东西不能去继承一个实际的东西,但是接口可以继承接口,且一次可以继承很多个,使用extend关键字后后面接上需要继承的接口即可,用逗号分开

    !!!注意:

    学习继承时明确过Java中只有单继承,这里接口可以继承很多个只是一个意外

    如果在面试中问到单继承问题,不要拿接口的多继承杠

使用

使用继承时有关键字extends,而使用接口时的关键字则是implements(实现),使用方法都是类似的

如果一个类实现接口,那么必须要实现接口里面的所有抽象方法,类似于继承时的重写,只不过继承时不要求必须重写,而接口的抽象方法必须全部实现

就像一个公司中项目经理给出任务的大概轮廓和步骤,而员工则要完成这些具体步骤的实现

与继承对比一下:继承时满足子类是一个父类,而实现接口则满足类有一个接口的功能

现在我们有了一个实现接口的类,那么我们在new这个类时,除了正常类名 = 实现类以外

依据多态的知识,还可以使用接口 = 实现类(eg. D类实现了A接口,而我们现在要new一个D的引用对象,可以写A x = new D())

特殊接口

  • 标记接口:接口里面什么都没有的接口
  • 函数式接口:接口中只有一个新定义的抽象方法

JDK8后的新特性

  • default关键字修饰后的方法可以在接口中写出默认的具体方法实现
  • 接口内的方法可以用private修饰但必须要有具体的实现(实用性不广)

了解完接口的定义后,回到文章开头提到的多接口可以弥补单继承缺点的问题

对于这个问题,我个人的理解是有很多官方定义的接口并且有相应的抽象方法,如果要使用相应的方法多继承下可以继承多个父类实现,而单继承下则依赖于使用多接口后重写其抽象类,用其内部的逻辑去完成多继承中可以达到的效果

下面以一些实用的接口例子来理解一下这句话

第二个问题:有哪些实用接口

高级排序接口

我们知道排序一个最普通的数组时可以用数组工具类Arrays里的sort方法完成

那如果我们要对一个字符串数组按照其长度,首字母字典数等一系列要求进行不同的排序呢,这就有了高级排序接口的出现

高级排序接口分为内外部比较器两个,下来我们来依次看一看

Comparator接口(外部比较器)

现在我们有一个字符串数组students,里面包含了三个人名,想要按照名字的长短对它们进行排序

首先我们要有一个实现Comparator接口的具体实现类的定义,这个类里面写什么呢?

实现Comparator里面规定的抽象方法comparecompare的两个参数类型即为我们要比较的数组内的元素字符串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是一个接口,ListSet可以简单理解为继承了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

本文到这里就结束了,博主也是初学,如果文章中有错误请帮忙指出!感谢阅读!

  • 9
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值