Java面向对象之抽象类与接口

简介: 本篇文章为面向对象部分的第三篇文章,前两篇文章见链接包和继承、组合与多态。抽象类和接口都是继承关系中父类的更进一步,结合前两篇文章来阅读更易理解掌握。

抽象类


语法规则


在上一篇文章【组合与多态】打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个抽象方法(abstract method), 包含抽象方法的类我们称为抽象类(abstract class)。


注意事项:


1.什么是抽象方法:一个没有具体实现的方法,被abstract修饰

abstract class Shape { abstract public void draw(); }

2.抽象类不能被实例化(不能被new)

3.因为不能被实例化,所以这个抽象类只能被继承

4.抽象类当中,也可以包含和普通类一样的成员和方法(和普通的类一样就是多了一层验证)

5.一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象类的所有抽象方法

6.抽象类的最大作用就是为了被继承

7.一个抽象类A如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法

8.结合第七点,当A类再次被一个普通类继承后,那么A和B这两个抽象类当中的所有抽象方法,必须被重写

9.抽象类和抽象方法不能被final,private修饰(互相矛盾,抽象类就是为了被继承和重写的而被这两个关键字修饰后不能再改变)


抽象类的作用


抽象类存在的最大意义就是为了被继承.

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.


有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?


确实如此. 但是使用抽象类相当于多了一重编译器的校验.

使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,

使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.


很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛?

但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们. 充分利用编译器的校验, 在实际开发中是非常有意义的


接口


接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法和字段,而接口中包含的方法都是抽象方法, 字段只能包含静态常量.接口其实就是性能更专一且兼具多继承功能的抽象类


注意事项


1.使用interface来修饰


interface IA {}


2.接口当中的普通方法,不能有具体的实现(非要实现只能通过关键字default来修饰这个方法,其作用和普通类相同)

3.接口当中可以有static方法

4.接口中所有方法默认都是public的

5.抽象方法默认是public abstract的

6.接口也不能够被实例化(接口时抽象类的更进一步,抽象类就不能被实例化,接口相同)

7.类和接口之间的关系是通过关键字implements实现的

8.当一个类实现了一个接口,就必须重写接口中的抽象方法

9.接口当中的成员变量,默认是public static final修饰的


interface IShape { 
 void draw(); //抽象方法,默认被public abstract修饰
 int num = 10; //public static final int num = 10;
}


10.当一个类实现一个接口后,重写这个方法的时候,这个方法前面必须加上public(因为接口中的抽象方法默认被public修饰,子类的重写方法的访问权限应该大于等于父类重写方法的访问权限)

一个错误的代码


interface IShape { 
 abstract void draw() ; // 即便不写public,也是public 
} 
class Rect implements IShape { 
 void draw() { //应该加上public
 System.out.println("□") ; //权限更加严格了,所以无法覆写。
 } 
}


11.一个类可以通过关键字extends继承一个抽象类或者普通类,但是只能继承一个类;但是可以通过关键字implements实现多个接口,接口之间使用逗号隔开

12.类与类之间的关系通过extends操作,类与接口之间的关系通过implements操作,那么接口与接口之间会存在什么样的关系呢?


接口与接口之间可以使用extends来操作他们的关系,此时其意为:拓展(相当于就是类与类之间的继承,所以用extends关键字)


一个接口B通过extends来拓展另一个接口A的功能,此时当一个类C通过implements实现这个接口B的时候,此时重写的方法不仅仅是B的抽象方法,还有它从C接口拓展来的抽象方法


理解接口


有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.

然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.

现在我们通过类来表示一组动物.


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


另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, "会游泳的 ”


interface IFlying { 
 void fly(); 
} 
interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
}


接下来我们创建几个具体的动物

猫, 是会跑的.


class Cat extends Animal implements IRunning { 
 public Cat(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用四条腿跑"); 
 } 
}


鱼, 是会游的


class Fish extends Animal implements ISwimming { 
 public Fish(String name) { 
 super(name); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在用尾巴游泳"); 
 } 
}


青蛙, 既能跑, 又能游(两栖动物)


class Frog extends Animal implements IRunning, ISwimming { 
 public Frog(String name) { 
 super(name); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在往前跳"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在蹬腿游泳"); 
 } 
}


还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”


class Duck extends Animal implements IRunning, ISwimming, IFlying { 
 public Duck(String name) { 
 super(name); 
 } 
 @Override 
 public void fly() { 
 System.out.println(this.name + "正在用翅膀飞"); 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用两条腿跑"); 
 } 
 @Override 
 public void swim() { 
 System.out.println(this.name + "正在漂在水上"); 
 } 
}


上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.

继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .


猫是一种动物, 具有会跑的特性.

青蛙也是一种动物, 既能跑, 也能游泳

鸭子也是一种动物, 既能跑, 也能游, 还能飞


这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.

例如, 现在实现一个方法, 叫 “散步”


public static void walk(IRunning running) { 
 running.run(); 
}


在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的就行


Cat cat = new Cat("小猫"); 
walk(cat); 
Frog frog = new Frog("小青蛙"); 
walk(frog); 
// 执行结果
小猫正在用四条腿跑
小青蛙正在往前跳


甚至参数可以不是 “动物”, 只要会跑!


class Robot implements IRunning { 
 private String name; 
 public Robot(String name) { 
 this.name = name; 
 } 
 @Override 
 public void run() { 
 System.out.println(this.name + "正在用轮子跑"); 
 } 
} 
Robot robot = new Robot("机器人"); 
walk(robot); 
// 执行结果
机器人正在用轮子跑


常用接口


Comparable


我们在对数组进行排序时使用以下代码:


import java.util.Arrays;
public class Test1 {
    public static void main(String[] args) {
        int[]arr={1,5,3,4,9};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}


但如果我们给对象数组排序,就不能仅仅通过这么简单的方法来实现排序了,接下来我们一起来看看如何对对象数组进行排序。


import java.util.Arrays;
class Student {
    public int age;
    public double score;
    public String name;
    public Student(int age, double score, String name) {
        this.age = age;
        this.score = score;
        this.name = name;
    }
    @Override//这个重写方法时为了打印对象数组的具体成员
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
}
public class Test1 {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student(18,85.0,"A");
        students[1]=new Student(17,80.5,"B");
        students[2]=new Student(19,90.3,"C");
        System.out.println(Arrays.toString(students));
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}


微信图片_20230110162120.png

在对对象数组进行排序时发生以上异常,那么这是为什么呢?


因为再对象中有多个成员属性,在对对象数组进行排序时,不能确定对哪一个成员属性进行排列


所以我们引出了比较器来专门解决对象数组排序的问题,需要让类连接接口Comparable(注意接口后的<>中写比较的类型)并重写compareTo方法,实现对象数组的指定元素排序


class Student implements Comparable<Student> {
    public int age;
    public double score;
    public String name;
    public Student(int age, double score, String name) {
        this.age = age;
        this.score = score;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
======================================================================================
    @Override
    public int compareTo(Student o) {
        if(this.age>o.age)
            return 1;
        else if(this.age==o.age)
            return 0;
        else
            return -1;
    }
}


//运行结果:


微信图片_20230110162115.png

=================================================================================
上边的重写方法也可以写成如下形式
@Override //升序
    public int compareTo(Student o) {
        return this.age - o.age;
    }
@Override //降序
    public int compareTo(Student o) {
        return this.age - o.age;
    }


对于其中的compareTo方法,谁调用它谁就是this,传过去的参数就是o,如果返回的值大于0,表示this大于o;等于0,两者相等;小于0表示this小于o。如果需要降序排列,将this与o的位置换一换即可。


如果要进行字符串或者其他类的比较,需要调用类中所提供的compareTo方法进行比较,比如字符串:


=================================================================================
@Override
    public int compareTo(Student o) {
        return this.name.compareTo(o.name);
    }


按两个学生的年龄进行排序


public class Test1 {
    public static void main(String[] args) {
        Student student1=new Student(18,85.0,"A");
        Student student2=new Student(17,80.5,"B");
        System.out.println(student1.compareTo(student2));
    }
}
//运行结果:
1
说明第一个学生的年龄比第二个学生的年龄大(o1.data>o2.data返回正数,o1.data==o2.data返回0,o1.data<o2.data返回负数)


所以,如果自定义类型的数据要进行大小的比较,一定要实现可以比较的接口,但是这个接口有个很大的缺点:对类的侵入性非常强,一旦写好了,不敢轻易改动,因此实际当中常常使用比较器Comparator,下面就来聊一聊Comparator。


Comparator(比较器)

public interface Comparator<T> {
    int compare(T o1, T o2);
  ...
}


通过调用上边这个接口就可以实现对象数组中指定元素的比较,以下为代码实现,这样就将需要比较的类的属性从原类中抽取出来,对类的侵入性大大减小


import java.util.Comparator;
class AgeComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}
class ScoreComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score-o2.score);
    }
}
class NameComparator implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
    }
}
class Student {
    public int age;
    public double score;
    public String name;
    public Student(int age, double score, String name) {
        this.age = age;
        this.score = score;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", score=" + score +
                ", name='" + name + '\'' +
                '}';
    }


我们先使用比较器来比较两个学生的年龄:


public class Test1 {
    public static void main(String[] args) {
        Student student1=new Student(18,85.0,"A");
        Student student2=new Student(17,80.5,"B");
        AgeComparator ageComparator = new AgeComparator();
        System.out.println(ageComparator.compare(student1,student2));
    }
}
//运行结果:
1
说明第一个学生的年龄比第二个学生的年龄大(o1.data>o2.data返回正数,o1.data==o2.data返回0,o1.data<o2.data返回负数)


对学生数组排序:


public class Test1 {
    public static void main(String[] args) {
        Student[] students=new Student[3];
        students[0]=new Student(18,85.0,"A");
        students[1]=new Student(17,80.5,"B");
        students[2]=new Student(19,90.3,"C");
        AgeComparator ageComparator = new AgeComparator();
        Arrays.sort(students,ageComparator);
        System.out.println("按年龄排序:");
        System.out.println(Arrays.toString(students));
        ScoreComparator scoreComparator=new ScoreComparator();
        Arrays.sort(students,scoreComparator);
        System.out.println("按分数排序:");
        System.out.println(Arrays.toString(students));
        NameComparator nameComparator=new NameComparator();
        Arrays.sort(students,nameComparator);
        System.out.println("按姓名排序:");
        System.out.println(Arrays.toString(students));
    }
}
//默认排序为升序,如果需要降序排列,同样的道理,将o1与o2交换一下位置就好了。


运行结果:


微信图片_20230110162107.png

与Comparable接口相比较,Comparator(比较器)更加灵活且对类的侵入性非常弱,但究竟使用哪一种接口,还需要根据具体的业务需求来决定。


Cloneable接口和深浅拷贝


Cloneable接口


目前我们所学到的创建对象的方式只有通过new关键字来实例化对象,接下来的一种新的创建对象的方式就是通过Cloneable接口来实现。

现在我们有像人这样一个类,Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”.


class Person {
    public int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }
}
public class demo1 {
    public static void main(String[] args) {
        Person person1=new Person(18);
        Person person2=person1.clone();
    }


以上代码此时无法完成任务


微信图片_20230110162059.png

要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常.


微信图片_20230110162056.png

Cloneable接口其实为空接口,它的作用就是为了标致当前类是否可以被克隆。

要想实现clone方法还有以下语法规定:


  • 必须在实现接口的类中重写Object clone()方法

微信图片_20230110162053.png

  • 实现克隆时还需将clone()方法的返回值进行强制转化为object的子类型


微信图片_20230110162050.png

  • 主函数中也需要抛出异常


微信图片_20230110162046.png

class Person implements Cloneable{
    public int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class demo1 {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person1=new Person(18);
        Person person2=(Person) person1.clone();
        System.out.println(person2);
    }
}


//运行结果:


微信图片_20230110162041.png

深拷贝和浅拷贝


在刚才的代码中,其内存分布情况如下:


微信图片_20230110162038.png

我们通过person2.age来改变数值不会影响到person1.age,但如果我们再有一个类Money,变成如下代码(只需注意新增代码即可)


class Money {//新增类
    public double m=12.5;
}
class Person implements Cloneable{
    public int age;
    public Money money=new Money();//新增代码
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class demo1 {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person person1=new Person(18);
        Person person2=(Person) person1.clone();
        System.out.println(person1.money.m);//新增代码
        System.out.println(person2.money.m);//新增代码
        System.out.println("======================");//新增代码
        person1.money.m=99.9;//新增代码
        System.out.println(person1.money.m);//新增代码
        System.out.println(person2.money.m);//新增代码
    }
}


//运行结果


微信图片_20230110162034.png

我们发现通过person1.money.m的值的改变,person2.money.m也发生了改变,其内存分布情况如下:


微信图片_20230110162030.png

其实这就是浅拷贝,只是拷贝了对m的引用,person1和person2指向两个对象,而两者其中的money都指向一个对象,所以在对m的值进行改变时,两个对象的m值都会发生变动。

那我们如何深拷贝变成这样呢


微信图片_20230110162027.png

这样就不会因为一个对象中m的改变而影响另一个对象中的m了,我们如何通过代码来实现呢?

我们还需要进一步拷贝money,所以Money类也需要实现Cloneable接口


class Money implements Cloneable{
    public double m = 12.5;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}


然后还需要在Person类的clone重写方法中进行改动,将money也进行拷贝(实例化新拷贝一份,就会再开辟新的空间)


 

@Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp=(Person) super.clone();
        tmp.money=(Money) this.money.clone();
        return tmp;
    }


//运行结果


微信图片_20230110162022.png

这就是深拷贝


总结:


深浅拷贝取决于两点


  • 代码的写法
  • 拷贝的数据类型


如果想要更深入了解深浅拷贝,参考这位博主的【文章】


总结


抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别(重要!!! 常见面试题)


核心区别:


抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.

如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.


class Animal { 
 protected String name; 
 public Animal(String name) { 
 this.name = name; 
 } 
}
相关文章
|
17天前
|
Java
Java中的抽象类:深入了解抽象类的概念和用法
Java中的抽象类是一种不能实例化的特殊类,常作为其他类的父类模板,定义子类行为和属性。抽象类包含抽象方法(无实现)和非抽象方法。定义抽象类用`abstract`关键字,子类继承并实现抽象方法。抽象类适用于定义通用模板、复用代码和强制子类实现特定方法。优点是提供抽象模板和代码复用,缺点是限制继承灵活性和增加类复杂性。与接口相比,抽象类可包含成员变量和单继承。使用时注意设计合理的抽象类结构,谨慎使用抽象方法,并遵循命名规范。抽象类是提高代码质量的重要工具。
32 1
|
1天前
|
存储 安全 Java
[Java基础面试题] Map 接口相关
[Java基础面试题] Map 接口相关
|
1天前
|
安全 Java
Java基础&面向对象&继承&抽象类
Java基础&面向对象&继承&抽象类
|
1天前
|
Java
【Java基础】详解面向对象特性(诸如继承、重载、重写等等)
【Java基础】详解面向对象特性(诸如继承、重载、重写等等)
5 0
|
7天前
|
安全 Java 机器人
《Java 简易速速上手小册》第2章:面向对象的 Java(2024 最新版)
《Java 简易速速上手小册》第2章:面向对象的 Java(2024 最新版)
19 0
|
7天前
|
Java 开发者
探索 Java 的函数式接口和 Lambda 表达式
【4月更文挑战第19天】Java 中的函数式接口和 Lambda 表达式提供了简洁、灵活的编程方式。函数式接口有且仅有一个抽象方法,用于与 Lambda(一种匿名函数语法)配合,简化代码并增强可读性。Lambda 表达式的优点在于其简洁性和灵活性,常用于事件处理、过滤和排序等场景。使用时注意兼容性和变量作用域,它们能提高代码效率和可维护性。
|
8天前
|
Java
Java接口中可以定义哪些方法?
【4月更文挑战第13天】
14 0
Java接口中可以定义哪些方法?
|
9天前
|
设计模式 Java
Java接口与抽象类
Java接口与抽象类
17 0
|
10天前
|
存储 Java
Java基础教程(7)-Java中的面向对象和类
【4月更文挑战第7天】Java是面向对象编程(OOP)语言,强调将事务抽象成对象。面向对象与面向过程的区别在于,前者通过对象间的交互解决问题,后者按步骤顺序执行。类是对象的模板,对象是类的实例。创建类使用`class`关键字,对象通过`new`运算符动态分配内存。方法包括构造函数和一般方法,构造函数用于对象初始化,一般方法处理逻辑。方法可以有0个或多个参数,可变参数用`类型...`定义。`this`关键字用于访问当前对象的属性。
|
14天前
|
安全 Java 编译器
接口之美,内部之妙:深入解析Java的接口与内部类
接口之美,内部之妙:深入解析Java的接口与内部类
35 0
接口之美,内部之妙:深入解析Java的接口与内部类