【Java】抽象类和接口

1. 抽象类

1.1 抽象类的定义

在Java中,所有的事物都可以用类来描述,但并不是所有的类都是用来描绘的。如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类

1.2 抽象类的语法

访问权限修饰符 abstract class 类名 {
    // 成员变量
    // 成员方法
    // 构造方法
    
    // 抽象方法(用abstract修饰的方法) 
    abstract 权限修饰符 返回类型 方法名(参数);
}

注意:用 abstract 关键字修饰的类即为抽象类,抽象类也是类,内部可以包含普通方法和属性,甚至构造方法。

1.3 抽象类的特性

  1. 抽象类可以和普通类一样,有普通的成员变量或成员方法,构造方法等,构造方法用于子类为该抽象类的属性初始化。(如下代码)
abstract class Shape {
	// 抽象类也是类,也可以增加普通方法和属性
	protected double area; // 面积
	
	public double getArea(){
		return area;
	}
	
	// 抽象方法:被abstract修饰的方法,没有方法体
	abstract public void draw();
	abstract void calcArea();
}
  1. 抽象类不能实例化,只能被普通类或抽象类继承。

// 编译报错,抽象类无法实例化
Shape shape = new Shape();

  1. 抽象类不一定要有抽象方法,但一个类有抽象方法,该类一定为抽象类。(如下图)
    在这里插入图片描述

  2. 若一个抽象类有抽象方法,则该抽象方法不能被实现,只有方法体也不行,只能在声明的方法后面以 ; 结尾。(如下图)
    在这里插入图片描述

  3. 抽象类可以继承另一个抽象类。若抽象类含有抽象方法,则继承该抽象类的普通类必须重写该抽象类及其父类的所有抽象方法。(将子类声明为抽象类,则可不实现抽象方法)
    在这里插入图片描述

  4. 抽象类不能用 final 关键字修饰,抽象方法不能用 private 或 static 修饰。(原因: 抽象类必须被继承,而加上 final 关键字的类不能被继承;抽象方法必须被子类重写,而 private 修饰的方法只能在类内使用;static 修饰的方法为类方法,子类不能进行重写)
    在这里插入图片描述

1.4 抽象类的作用

抽象类虽然可以使用普通类替代,但实际上使用抽象类可以充分利用编译器的校验功能。在某些场景下,一些工作应该由子类去完成(重写父类方法),若不小心误用父类,使用实例化的普通父类编译器并不会报错,从而造成逻辑错误。

2. 接口

2.1 接口的概念

在日常生活中,接口几乎随处可见:(如图)
在这里插入图片描述
在这里插入图片描述
电源插座插孔上,可以插:电脑、电视机、电饭煲…所有符合规范的设备。
电脑的USB口上,可以插:U盘、鼠标、键盘…所有符合USB协议的设备。

从以上例子可知:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口描述了一个类或对象的外部行为,而不关心其内部实现。其他类可以实现这个接口,并提供相应的方法实现

2.2接口的语法形式

interface 接口名称 {
	// 可以有成员变量,以下写法等价
	int x;
	public final static double y;	

	// 抽象方法,以下写法等价
	public abstract void method1();		// public abstract 为固定搭配,可以省略
	public void method2();
	abstract int method3();
	double method4();	// 推荐接口方法使用该写法,表达更为简洁
}

注意:接口可以有成员变量,默认由public final static修饰;接口的方法一定是抽象方法,默认由 public abstract 修饰。

2.3 接口的使用

接口可以被类使用 implements 关键字实现,普通类实现接口必须重写接口的所有方法;也可以被另一个接口使用 extends 关键字继承,用于拓展接口的功能。

例如有以下父类和接口:

// 父类
abstract class Animal {
    protected String name;		// 属性
    protected String species;
    
	// 构造方法
    public Animal(String name, String species) {
        this.name = name;
        this.species = species;
    }
 	// 成员方法
    public void eat() {}
}

// 接口,实现该接口的类都具有“跑”的功能
interface Running {
    void run();
}

接口的使用:
在这里插入图片描述

2.4 接口的特性

  • 接口类型是一种引用类型,但是不能使用 new 直接实例化成对象。
  • 接口中可以有成员变量,但是接口中的变量会被隐式的指定为 public static final 变量。
  • 接口中的每一个方法都是由 public 修饰抽象方法,即接口中的方法会被隐式的指定为 public abstract 方法。实现接口的类重写方法时,方法的访问权限修饰符不能为 default 等比 public 权限低的修饰符。
    在这里插入图片描述
  • 接口中不能有静态代码块和构造方法。
    在这里插入图片描述
  • 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class文件。
  • 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类。
  • jdk8中:接口可以包含由 default 修饰的普通方法。(类实现接口可不重写该方法)
    在这里插入图片描述
  • 在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口
  • 一个接口可以使用 extends 关键字继承另一个接口,以此拓展接口的功能。

2.5 接口的使用实例

2.5.1 Comparable 和 Comparator

实现Comparable接口需要在类内部重写compareTo()方法,该方法返回一个整型值,表示当前对象与另一个同类型对象之间的大小关系。

Comparable接口的作用:

  • 让实现该接口的类具有自然排序的能力。即当需要对该类的对象进行排序时,可以直接使用Collections.sort() 或 Arrays.sort()方法对该类的对象进行排序。
  • 当使用ArrayList等数据结构时,若当前类实现了该接口,则类对象会自动按照compareTo()方法实现的排序规则对对象元素进行排列。
  • 可以实现 Comparable接口,使类能被放入有序集合中,如TreeSet 、TreeMap、PriorityQueue等。

使用Comparable接口进行非递减排序,应让调用该方法的对象的属性值 - 作为方法参数的对象的属性值,即返回值为 this.属性 - o.属性。反之,进行非递增排序,返回值为 o.属性 - this.属性。

使用实例:
1.创建一个Student 对象并实现Comparable接口

class Student implements Comparable<Student>{
    private String name;
    private int age;    
    private int score;
    
    public Student(String name, int age, int score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public int compareTo(Student o) {
        if (this == o) return 0;
        // 非递减排序
        // 先按照分数排序
        int ret = this.score - o.score;
        // 分数如果相同按年龄大小排序
        ret = ret==0?this.age-o.age:ret;
        
        return ret;
    }
}

2.创建Studnet对象数组并进行排序

	public static void main(String[] args) {

        Student[] students = new Student[]{
                new Student("zhangsan",15, 78),
                new Student("lisi",18, 95),
                new Student("wangwu", 12,88)};
        System.out.println("===========");
        Arrays.sort(students);

    }

在这里插入图片描述


实现Conparator接口需要重写compare()方法,该方法同样返回一个整数值,表示两个对象之间的大小关系。

Comparator接口的作用:

  • 实现该接口可以让用户在类外部根据自身需求自定义排序规则。
  • 与Comparable一样,可以实现 Comparator接口,使类能被放入有序集合中,如TreeSet 、TreeMap、PriorityQueue等。

使用实例:(实现Comparator接口创建大根堆)
1.先创建一个Student类

class Student {
    private String name;
    private int age;
    private int score;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public int getScore() {
        return score;
    }
}

2.创建优先队列,使用内部类实现Comparator接口并重写compare()方法;加入Student对象。

    public static void main(String[] args) {
        PriorityQueue<Student> queue = new PriorityQueue<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getScore() - o1.getScore();
            }
        });
        queue.add(new Student("zhangsan",15, 78));
        queue.add(new Student("lisi",18, 95));
        queue.add(new Student("wangwu", 12,88));
        queue.add(new Student("tianqi", 12,99));
        System.out.println("===========");
    }

在这里插入图片描述

2.5.2 Cloneable接口

Cloneable的中文意思是“可克隆的”,从它的含义我们大概猜出实现这个接口的类就具有“克隆”的能力,事实也确实是这样的,但要想真正了解Cloneable接口的正确用法,我们需要先了解Object类中的clone()方法。

使用clone()方法实现类的浅拷贝

假设有以下定义的Person类和一个对应的实例化对象

// Person类
class Person {
    public int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }
}

//实例化一个Person对象
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person(18);
    }
}

在Java中所有的类都默认继承Object类,而Object类中带有一个clone()方法,如果我们想要得到两个一模一样的类对象,就可以通过一个对象调用该方法“克隆”出另一个相同对象。

但如果我们真正去调用该方法时,可能会出现Person类的方法调用中找不到clone()方法的情况,原因是什么呢?
其实我们通过查看Object()类的方法实现可发现clone()方法是被 protected 关键字修饰的,因此该方法只能在Object类内、与Object()类同个包的类 或 其子类内使用(当前即Person类)。
当我们想要在另一个类的调用clone()方法,应在Person类中重写clone()方法才能实现在另一个类调用该方法的效果。(重写方法的前后效果对比如下)

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

在这里插入图片描述


当我们重写clone()方法后,会发现使用clone()方法依然会出现如下报错。
在这里插入图片描述
这里的原因其实有两个:

  1. 重写clone()方法时,会抛出一个异常,因此我们在调用该方法时,也应该在main()方法的声明中 throws 对应的异常类型。
  2. clone()方法返回的是一个Object类的对象,此处会发生向下转型,因此我们需要将返回值强转为Person类型。

代码修改后的效果如下
在这里插入图片描述


当我们完成以上步骤,调用toString()方法打印将这两个对象的信息,依然发现程序运行时报了以下异常。
在这里插入图片描述
这个异常的意思是该类不支持被克隆。
到此,Cloneable接口的作用在这里体现出来了,只有实现Cloneable接口的类才支持被克隆

通过前面对接口的介绍,我们都知道实现一个接口必须重写接口中的所有方法,但实际上Cloneable接口是一个空接口,即这个接口里面没有任何方法
在这里插入图片描述
因此,Cloneable接口相当于一个标记接口,实现该接口则证明了该类是可以被克隆的。

接口实现及代码运行结果如下:

class Person implements Cloneable{
    public int age;

    public Person(int age) {
        this.age = age;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }

}

public class Test {

    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(18);
        Person person2 = (Person) person1.clone();
        System.out.println(person1.toString());
        System.out.println(person2.toString());
    }
}

在这里插入图片描述


使用clone()接口实现深拷贝

什么是浅拷贝?
浅拷贝可以简单理解为调用clone()方法克隆出的对象并非所有资源独立,而存在部分共享资源。

对于上面定义的Person类,我们通过 “==” 可以知道person1 和 person2 这两个引用并非指向同一个对象,因此它们此时是资源独立的。
在这里插入图片描述


当我们对Person类作出以下修改:(在Person类增加一个类对象成员)

// 新增类
class Purse {
    int money;
}

class Person implements Cloneable{
    public int age;
    Purse purse;	// 新增类成员

    public Person(int age, int money) {
        this.age = age;
        purse = new Purse();
        purse.money = money;
    }

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

     @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", money=" + purse.money +
                '}';
    }

}

同样实例化一个Person对象,使用clone()方法克隆出一个新的对象并对两个person对象的信息进行打印:
在这里插入图片描述

可以看出表面上确实克隆出了一个一模一样的全新的对象,但当我们通过其中一个person对象修改money的值时会发现两个perosn对象的money值都发生了改变。
在这里插入图片描述
原因是什么呢?
通过简单的思考我们也许可以猜测出,虽然person1 和 person2 所指的是两个不同的对象,但是对象里面purse引用指向的是同一个pruse对象,所以修改其中一个person对象里面purse对象里面money属性,会同时影响person1 和 perosn2 中的money属性。(person1 和 person2 的内存分布及代码验证如下)
在这里插入图片描述
在这里插入图片描述

因此要实现深拷贝,我们需要将Person类中的Pruse也实现Cloneable接口并重写Object类的clone()方法。当调用person1的clone()方法时,不要立即返回结果,先将返回值用临时Person对象接收,并调用purse成员的clone()方法,使purse引用指向一个新的Purse对象。(代码实现及程序运行结果如下)

class Purse implements Cloneable{
    int money;

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

class Person implements Cloneable{
    public int age;
    Purse purse;

    public Person(int age, int money) {
        this.age = age;
        purse = new Purse();
        purse.money = money;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp = (Person) super.clone();
        tmp.purse = (Purse) tmp.purse.clone();
        return tmp;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", money=" + purse.money +
                '}';
    }
}

在这里插入图片描述

3. 抽象类和接口的区别

抽象类和接口最核心的区别为:
抽象类可以包含普通成员变量和普通方法,而接口只能包含由 public final static 修饰的常量和抽象方法。

其他方面的区别如下:
在这里插入图片描述


以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章的不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值