0
点赞
收藏
分享

微信扫一扫

Java:抽象类和接口

RJ_Hwang 2022-04-22 阅读 48
java后端

文章目录

抽象类

什么是抽象类?

常见的,如果我们在类中写了一个方法,但是这个方法并没有具体的实现细节(不给出具体的实现体),那么我们就可以将这个方法设计成一个抽象方法,那么包含抽象方法的类就可以称为抽象类

abstract class A{
    //被abstract修饰的方法没有方法体
    abstract public void eat();

    //抽象类也是类,可以添加普通变量和方法
    private String name;

    public void sleep(){
        System.out.println("666");
    }

    //抽象类也是可以构造方法
    public A(String name) {
        this.name = name;
    }
}

注意:

  • 使用abstract修饰方法的就是抽象方法,使用abstract修饰的类就是抽象类。
  • 抽象类也是类,内部既可以包含抽象方法,也可以包含普通方法和属性,甚至是构造方法等。

抽象类的特性

  • 抽象类不能直接实例化对象,需要子类继承这个抽象类,再实例化这个子类
  • 在之前我们讲过,如果不加访问限定符的话,会默认是包访问权限;但是如果是抽象方法在没有加访问限定符,默认则是public
  • 抽象类一般来说,是必须被继承的,并且继承后子类要重写父类中的所有抽象方法,否则子类也必须是抽象类(就是必须被abstract修饰的);若两样都不具备,编译器则会直接报错
abstract class A{
    //被abstract修饰的方法没有方法体
    abstract public void eat();

    //抽象类也是类,可以添加普通变量和方法
    private String name;

    public void sleep(){
        System.out.println("666");
    }

    //抽象类也是可以构造方法
    public A(String name) {
        this.name = name;
    }
}

abstract class C extends A{
    public int age;

    public C(String name) {
        super(name);
    }
}

//或者

class E extends A{
    public int age;

    public E(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println("888");
    }
}
  • 抽象方法不能被final和private修饰,因为抽象方法要被子类进行重写(但其实就算子类是抽象类没有对父类的抽象方法进行重写,父类中的抽象方法也是不能被final和private修饰的)
  • 抽象方法不能被static修饰,原因也是抽象方法要被子类进行重写,虽然被static修饰后事不依赖对象的,但是既然是抽象类,那么其中的抽象方法一定会被子类所重写,所以抽象方法是不能被static修饰的
  • 抽象类中不一定包含有抽象方法,但是有抽象方法的类一定是抽象类
  • 抽象类中可以有构造方法,供子类创建对象的时候,初始化父类的成员变量
  • 抽象类存在的最大意义就是为了被继承
  • 抽象类也可以发生向上转型,进一步发生多态

以上抽象类的这几条特性,只要读透、把握好,最后其实会发现抽象类也并不是很难理解。可以仔细理解下面这段示例代码:

abstract class Shape{
    public abstract 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 Triangle extends Shape{
    @Override
    public void draw() {
        System.out.println("△");
    }
}

public class Main {
    public static void drawMap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawMap(new Cycle());
        drawMap(new Rect());
        drawMap(new Triangle());
    }
}

运行结果:
在这里插入图片描述

为什么会有抽象类这种东西?


对于这样的疑问,我的回答是:其实抽象类相当于多了一重编译器的校验。例如有些程序本来是不可以调用父类中的内容的,但是如果不小心调用了父类的内容,对于普通类时不会报错的,但是对于抽象类会直接报错,这样有助于快速定位到错误的地方。

接口

什么是接口?

在Java中,接口其实就是一种行为的规范和标准,可以看成:多个类的公共规范,是一种引用数据类型。

interface I{
    public abstract void func1();   //默认就是public abstract,可以省略不写
    void func2();
}

接口的定义格式与类的定义格式基本是相同的,只是将class关键字换成interface关键字而已。
注意:

  • 在接口中的抽象方法中public abstract是固定搭配,在定义抽象方法的时候,可以省略不写
  • 创建接口的时候,接口命名一般都是以大写字母 I 开头

接口的特性

  • 子类和父类之间是extend继承关系,类与接口之间是implement实现关系
  • 接口中每一个成员方法都是抽象方法,如果不写修饰符,则接口中的方法会被隐式指定为public abstract;如果写也只能是写public abstract,写成其他的修饰符都会直接报错
  • 接口中的每一个成员变量都会被隐式制定为public static final变量
  • 接口类型是一种引用类型,但是不能直接new接口对象,是不能够进行实例化的
  • 接口中的方法是不能在接口中实现的,因为接口中的方法默认都是抽象方法,只能由实现接口的类来进行实现
  • 接口中的方法,如果要实现,需要使用default来进行修饰
interface I{
    //可以不在实现接口类中进行重写
    default void func() {
        System.out.println("666");
    }
}
  • 接口中的静态方法可以有具体的实现
interface I{
    //和前面一样,是不依赖对象的
    public static void func2(){
        System.out.println("这是一个静态方法");
    }
}
  • 一个普通的类可以通过implements来实现这个接口
  • 接口也可以发生向上转型,进一步发生多态
  • 接口虽然不是类,但是接口编译完成后字节码文件后缀格式也是.class的文件夹

以上接口的这几条特性,跟抽象类一样,只要读透、把握好,最后其实会发现接口和前面的那些只是点并无两别。可以仔细理解下面这段示例代码:

interface IShape{
    public abstract void draw();
}

class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}

class Rect implements IShape {
    @Override
    public void draw() {
        System.out.println("◇");
    }
}

class Triangle implements IShape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}

public class Main {
    public static void drawMap(IShape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawMap(new Cycle());
        drawMap(new Rect());
        drawMap(new Triangle());
    }
}

在这里插入图片描述

实现多个接口

因为在Java中是不支持多继承,所以引出了接口这个概念。虽然不能实现多继承,但是可以一个类实现多个接口。
示例代码:

class Animal{
    public String name;
    public int age;

    public Animal(String name) {
        this.name = name;
    }
}

interface IRun{
    void run();
}

interface ISwim{
    void swim();
}

interface IFly{
    void fly();
}

class Duck extends Animal implements IRun,ISwim,IFly{

    public Duck(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(this.name+"正在跑");
    }

    @Override
    public void swim() {
        System.out.println(this.name+"正在游");
    }

    @Override
    public void fly() {
        System.out.println(this.name+"正在飞");
    }
}

public class Main {
    public static void main(String[] args) {
        Duck duck=new Duck("小灰");
        duck.run();
        duck.swim();
        duck.fly();
    }
}

运行结果:
在这里插入图片描述

接口间的继承

类和接口之间的关系是implement,接口和接口之间的关系是extends。
示例代码:

interface IRun{
    void run();
}

interface ISwim{
    void swim();
}

interface IFly{
    void fly();
}

interface IDuck extends IRun,ISwim,IFly{

}

class Duck implements IDuck{

    @Override
    public void run() {

    }

    @Override
    public void swim() {

    }

    @Override
    public void fly() {

    }
}

总结:其实接口间的继承就相当于把多个接口合并在一起,拓展了之前的功能。但是最后在实现这个接口的时候,还是要对之前接口中的抽象方法进行重写。

三个重要的接口

Comparable接口

就比如说,我有如下的对象数组,想要对这些对象按照年龄进行排序:

Student[] students=new Student[3];
students[0]=new Student("张三",18);
students[1]=new Student("李四",23);
students[2]=new Student("王五",9);

这时候,如果只是简单地对对象进行>或者<进行判断,又或者使用equals来进行判断都会是不行的,因为在代码中其实就并未指定到底是按何种方式进行排序的(姓名还是年龄)。
所以就引出了Comparable接口,使用Comparable接口来调用Student类,然后来对这个接口中排序的抽象方法进行重写(也就是重写Comparable接口中的compareTo方法),就可以达到按姓名或者年龄排序的效果了,示例代码:

//按年龄进行排序
class Student implements Comparable<Student>{
    public String name;
    public int age;

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

   @Override
    public int compareTo(Student o) {
        return this.age-o.age;
    }
}

public class Main {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student("张三",18);
        students[1]=new Student("李四",23);
        students[2]=new Student("王五",9);
        System.out.println("排序前:"+ Arrays.toString(students));
        Arrays.sort(students);
        System.out.println("排序后:"+ Arrays.toString(students));
    }
}

Comparator接口

在Java中,这时候就引出了另外一个接口——Comparator接口,重写Comparator接口接口中的compareTo方法,使用这个接口来跟Comparable接口打配合,即可完成多种排序共同存在,想要什么排序就直接在sort方法中传入哪个类即可。
示例代码:

class Student implements Comparable<Student>{
    public String name;
    public int age;

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return 0;
    }
}

//根据年龄比较
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}

//根据姓名比较
class NameComparator implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}

public class TestDemo1 {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student("张三",18);
        students[1]=new Student("李四",23);
        students[2]=new Student("王五",9);
        AgeComparator ageComparator=new AgeComparator();
        System.out.println("排序前:"+ Arrays.toString(students));
        Arrays.sort(students,ageComparator);
        System.out.println("排序后:"+ Arrays.toString(students));
    }
}

**注意:**sort方法之前我们都是只传了一个参数,其实它是可以传入第二个参数的,也就是可以传入一个比较器。

Clonable接口

很显然,直接调用clone方法肯定是不行的,这时候我们就要使用一个接口:Clonable接口,这样之后就可以合法调用Object类中的clone方法。
示例代码:

class A implements Cloneable{
    public int a=10;
    public int b=20;

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(b);
    }
}

注意:需要重写Object类中的clone方法;此处使用的throws是抛出异常(必须写上,否则会报错)只需了解即可,在后面文章中介绍到异常会详细讲到。

浅拷贝和深拷贝

浅拷贝

如果我们还是像上面代码一样,就会出现一些问题,先上代码:

class B {
    public int c=100;
}

class A implements Cloneable{
    public int a=10;
    public B b=new B();

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(a.b.c);
        System.out.println(b.b.c);
        System.out.println("==============");
        a.b.c=50;
        System.out.println(a.b.c);
        System.out.println(b.b.c);
    }
}

运行结果:
在这里插入图片描述

从上面代码以及运行结果可以看出,当对对象a中调用的其他对象中的成员变量进行修改之后,对象b也会是相同的结果,也就是说这个代码其实没有实现真正的拷贝,因为b对象会随着a对象的改变而改变,其实b对象只是对a对象的一个拷贝,而b对象中b引用指向的还是原来a对象中b引用的对象,这句话可能会比较难理解,这里通过画图来进行展示:
在这里插入图片描述

对于这种只拷贝了一半,有一部分并没有进行拷贝,就叫做浅拷贝
深拷贝
对于深拷贝来说,相较于浅拷贝来说,是对整个对象全部都进行拷贝的。就是说,会先将一个对象1进行拷贝得到对象2,再将对象1引用的对象3进行拷贝得到对象4,接着让对象2引用对象4,这样的话,如果再对对象1引用的对象3进行修改,也不会影响到对象2和对象4了。下面通过画图展示:
在这里插入图片描述

示例代码:

class B implements Cloneable{
    public int c=100;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class A implements Cloneable{
    public int a=10;
    public B b=new B();

    @Override
    public String toString() {
        return "A{" +
                "a=" + a +
                ", b=" + b +
                '}';
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        A tmp=(A)super.clone();
        tmp.b=(B)this.b.clone();
        return tmp;
    }
}

public class TestDemo1 {
    public static void main(String[] args) throws CloneNotSupportedException {
        A a=new A();
        A b= (A) a.clone();
        System.out.println(a.b.c);
        System.out.println(b.b.c);
        System.out.println("==============");
        a.b.c=50;
        System.out.println(a.b.c);
        System.out.println(b.b.c);
    }
}

运行结果:
在这里插入图片描述

抽象类和接口的区别

在前面的学习中,我们已经知道了抽象类和接口的使用方法以及注意事项等,接下来就对两者的一个区分。
在Java中,抽象类和接口都是实现多态的常用方式,其核心区别是:抽象类可以包含普通方法和普通字段,这些普通方法和普通字段在子类中是可以直接进行使用的,不需要对其进行重写操作;而接口中不能包含普通方法,子类必须重写接口中的所有抽象方法。

区别抽象类接口
结构组成普通类+抽象方法抽象方法+全局常量
权限各种权限public
子类使用使用extends关键字继承抽象类使用implements关键字实现接口
关系一个抽象类可以实现若干接口接口不能继承抽象类,但是接口可以使用extends关键字继承多个父接口
子类限制一个子类只能继承一个抽象类一个子类可以实现多个接口

Object类

本文只是对Object类中的一部分方法进行简单的介绍。完整内容后续掌握。

对象打印toString方法

前面文章介绍过,详细见之前文章。

对象比较equals方法

在Java中,对==进行比较的时候:

  • 如果==两边都是基本数据类型,比较的是变量之间的值是否相同
  • 如果==两边是引用类型变量,比较的是引用变量之间的地址是否相同
  • 如果要比较对象中的内容,必须重写Object类中的equals方法,因为equals方法默认是按照地址进行比较的
class A{
    public int a=10;
    public String b="abc";
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        int a=1;
        int b=1;
        System.out.println(a==b);   //true
        System.out.println(a1==a2);   //false 按地址进行比较
        System.out.println(a1.equals(a2));   //false 按地址进行比较
    }
}

重写equals方法之后:

class A{
    public int a=10;
    public String b="abc";

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;   //两个引用 引用同一个对象
        if (o == null || getClass() != o.getClass()) return false;
        A a1 = (A) o;
        return a == a1.a && Objects.equals(b, a1.b);
    }
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        int a=1;
        int b=1;
        System.out.println(a==b);   //true
        System.out.println(a1==a2);   //false
        System.out.println(a1.equals(a2));   //true
    }
}

总结:在比较对象是否相同的时候,一定要重写equals方法。

hashcode方法

如果不对hashcode进行重写的话,因为a1和a2都是new出来的对象,那么即使这两个对象里面存储的内容一样,也会判定这两个对象是在不同位置的(地址是不相等的)。

class A{
    public int a=10;
    public String b="abc";
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        System.out.println(a1.hashCode());
        System.out.println(a2.hashCode());
    }
}

运行结果:
在这里插入图片描述

对hashcode进行重写后,因为new出来的这两个对象存储的内容是一样的,是由重写之后会判定这两个对象的地址是一样的。

class A{
    public int a=10;
    public String b="abc";

    @Override
    public int hashCode() {
        return Objects.hash(a, b);
    }
}

public class Main {
    public static void main(String[] args) {
        A a1=new A();
        A a2=new A();
        System.out.println(a1.hashCode());
        System.out.println(a2.hashCode());
    }
}

运行结果:
在这里插入图片描述

总结:

  • hashcode方法用来确定对象在内存中存储的位置是否相同
  • 事实上hashcode()在散列表中才有用,在其他情况下没用,在散列表中hashcode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置

接收引用数据类型

以接收接口为例:

interface I{
    void func();
}

class A implements I{
    @Override
    public void func() {
        System.out.println("666");
    }
}

public class Main {
    public static void main(String[] args) {
        Object obj=new A();   //向上转型
        A a=(A)obj;   //向下转型
    }
}

Object真正达到了参数统一,如果一个类希望接收所有的数据类型,就是Object完成,在Java中,泛型就是底层就是通过Object来实现的。

举报

相关推荐

0 条评论