Java:抽象类和接口

本文详细介绍了Java中的抽象类和接口,包括它们的概念、特性及用途。抽象类作为不能实例化的类,主要用作被继承的基类,强制子类重写抽象方法。接口则规定了类的行为规范,一个类可以实现多个接口,实现多继承的效果。此外,文章还探讨了匿名内部类、接口继承、重要接口(Comparable、Comparator、Clonable)的用法,以及浅拷贝和深拷贝的区别。
摘要由CSDN通过智能技术生成

抽象类

什么是抽象类?

在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());
    }
}

在这里插入图片描述

匿名内部类(拓展)

在前面“类和对象”的文章中内部类部分详细地讲解了实例内部类和静态内部类,而匿名内部类会涉及到接口部分相关的知识,所以放在本文此处进行讲解。

示例代码:

interface I{
    default void func() {
        System.out.println("666");
    }

    void func2();
}

public class Main {
    public static void main(String[] args) {
        I i=new I() {
            //对于func(),是default修饰的,可以重写也可以不重写
            @Override
            public void func() {
                System.out.println("111");
            }

            //对于func2(),因为是纯抽象方法,必须进行重写
            @Override
            public void func2() {
                System.out.println("333");
            }
        };
        i.func();   //111
        i.func2();   //333
        
        //匿名内部类只能够使用一次
        I i2=new I() {
            @Override
            public void func2() {

            }
        };
        i2.func();   //666
        i2.func2();   //无打印值
    }
}

从上面的代码中可以得出结论:
● 匿名内部类是在实例化接口的时候出现的,可以对接口中的方法进行重写(其中,接口中的纯抽象方法必须进行重写)
● 匿名内部类只能够使用一次,也就是在第二次调用这个接口的时候,还会是原来的效果

实现多个接口

在Java中,类和类之间是进行单继承的,一个类只能有一个父类,也就是说,在Java中是不支持多继承的。那么如何跟C++一样可以实现多继承呢?

因为在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();
    }
}

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

接口间的继承

在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间是可以多继承的。

类和接口之间的关系是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接口

在上面的Comparable接口,我们会发现一个与实际开发不符的地方:把实现排序的方法写在Student类中,这导致这个排序方法直接就写死了。就比如,我已经实现了按年龄排序,但是我有想再按姓名进行排序,那么这时候可能就要先将原本的按年龄排序方法先注释掉,再重写一个按姓名排序的方法,长此以往,效率实在是太低了。那么,又该如何解决这样的问题呢?

在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方法进行拷贝。但是如果我们是想对一个对象进行拷贝的话,又该如何操作呢?

很显然,直接调用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是抛出异常(必须写上,否则会报错)只需了解即可,在后面文章中介绍到异常会详细讲到。

浅拷贝和深拷贝

浅拷贝

在上面例子中,我们实现的拷贝是对类中的基本数据类型进行拷贝。那么这时候就会有一个疑问,对于类中的非基本数据类型(比如String等引用类型变量),我们又应该如何进行拷贝呢?

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

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类

在Java中,默认提供了一个类:Object类。Object类默认是所有类的父类,也就是说所有类的对象都可以使用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()方法是一个能够帮我们算具体对象位置存在的方法。这个方法是一个native方法,底层是由C/C++代码写的。

如果不对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()的作用是获取对象的散列码,进而确定该对象在散列表中的位置

接收引用数据类型

在前面我们知道了Object可以接收任意对象,因为Object是所有类的父类,但是Object并不局限于此,它可以接收所有数据类型,包括:类、数组、接口。

以接收接口为例:

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来实现的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔡欣致

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值