Java 抽象类和接口

偷心编程 2024-06-13 14:35:12 阅读 57

Java 抽象类和接口

1. 抽象类1.1 什么是抽象类1.2 抽象类的语法1.3 抽象类的特性1.4 抽象类的意义 2. 接口2.1 接口的概念2.2 接口的语法2.2.1 接口的关键字2.2.2 接口中的成员变量以及成员方法 2.3 接口的使用2.4 接口的特性2.5 实现多个接口2.6 接口间的继承 3. 接口的使用实例3.1 实现对象的可比较3.1.1 Comparable 接口3.1.2 Comparator 接口 3.2 对象数组的排序3.2.1 根据Comparable 接口排序3.2.2 根据比较器进行排序 3.3对象的拷贝3.3.1 普通拷贝3.3.2 浅拷贝3.3.3 深拷贝 4. 判断类相等

在这里插入图片描述


🔥 博客主页: 偷心编程

🎥 系列专栏: 《Java学习》 《C语言学习》

❤️ 感谢大家点赞👍收藏⭐评论✍️

在这里插入图片描述

1. 抽象类

1.1 什么是抽象类

从程序上来说就是这个类中的某些方法没有具体的实现代码,只是定义了一个这样的方法的名字,表示存在一个这样的方法。因此抽象类一定是用来继承的,必须要子类来实现这些具体的方法。方便多态的实现。

从实际意义上理解是这样的:在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。如下:

在这里插入图片描述

1.2 抽象类的语法

关键字:abstract


// 抽象类:被abstract修饰的类public abstract class Shape { // 抽象方法:被abstract修饰的方法,没有方法体 abstract public void draw(); abstract void calcArea(); // 抽象类也是类,也可以增加普通方法和属性 public double getArea(){ return area; } protected double area; // 面积}

注:

从语法上来说,抽象类也允许存在普通方法和属性,甚至是构造方法

1.3 抽象类的特性

抽象类不能够实例化对象

Shape shape = new Shape(); // 编译出错Error:(30, 23) java: Shape是抽象的; 无法实例化 抽象类的存在就是为了继承,因此一个普通的类继承了这个抽象类必须要重写抽象类的所有的抽象方法抽象类的抽象方法不能够被final、static、private修饰,因为有上述情况就不能够被重写了若是子类在继承了抽象类后没有重写所有的抽象方法,那么该子类也必须是抽象类(加上abstract)

abstract class Shape{ abstract public void draw();}abstract class A extends Shape{ // A没有重写draw方法,因此也是抽象类 public int a; abstract public void a();}class B extends A{ // B不仅需要重写A的抽象方法,还有重写Shape的抽象方法 @Override public void draw() { } @Override public void a() { }} 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

1.4 抽象类的意义

我们知道父类的意义是为了实现代码的复用,抽取对象的共性。

父类中某个成员变量的意义是:比如String name;表示所有的子类都有名字。父类中的某个方法的意义是:说明继承这个类的所有子类都具备这个特点(功能);

因此子类一般会进行重写父类的方法,用来实现 多态(不同的对象对于统一行为会做出不同的表现),所以呢我们父类并不需要具体实现某个方法,而是交给子类去实现,所以我们就用abstract修饰,这样父类 只需要告诉存在这个方法就行。

其次,由于我们要用的是子类重写的方法,所以如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.充分利用编译器的校验, 在实际开发中是非常有意义的.

2. 接口

2.1 接口的概念

接口其实就是一种功能,类可以实现接口,也就意味着具有了接口的功能。无论什么类,只要实现了这个接口,就可以使用接口的功能。

2.2 接口的语法

2.2.1 接口的关键字

关键字:interface

接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。

public interface 接口名称{ }

提示:

创建接口时, 接口的命名一般以大写字母 I 开头.接口的命名一般使用 “形容词” 词性的单词.阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.

2.2.2 接口中的成员变量以及成员方法

接口中的成员变量默认都是被 public static final 修饰,因此成员变量必须就地初始化。public static final int a=1; 为了代码的简便,只要写int a;

接口中的成员方法默认都是 public abstract 修饰的,但是这些都可以不写,只要void method4();就可以了

接口中的方法虽说一般都是抽象方法,但是也有特例,就是被static和default修饰的方法,是可以有具体的实现代码的。

2.3 接口的使用

关键字:inplements

接口不能够直接使用,必须要有一个“实现类”来“实现”该接口,实现接口中 的所有抽象方法。

public class 类名称 implements 接口名称{ // ...}

注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。


下面我们实现笔记本电脑使用USB鼠标、USB键盘的例子

USB接口:包含打开设备、关闭设备功能笔记本类:包含开机功能、关机功能、使用USB设备功能鼠标类:实现USB接口,并具备点击功能键盘类:实现USB接口,并具备输入功能

// USB接口public interface USB { void openDevice(); void closeDevice();} // 鼠标类,实现USB接口public class Mouse implements USB { @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); } public void click(){ System.out.println("鼠标点击"); }} // 键盘类,实现USB接口public class KeyBoard implements USB { @Override public void openDevice() { System.out.println("打开键盘"); } @Override public void closeDevice() { System.out.println("关闭键盘"); } public void inPut(){ System.out.println("键盘输入"); }}// 笔记本类:使用USB设备public class Computer { public void powerOn(){ System.out.println("打开笔记本电脑"); } public void powerOff(){ System.out.println("关闭笔记本电脑"); } public void useDevice(USB usb){ usb.openDevice(); if(usb instanceof Mouse){ Mouse mouse = (Mouse)usb; mouse.click(); }else if(usb instanceof KeyBoard){ KeyBoard keyBoard = (KeyBoard)usb; keyBoard.inPut(); } usb.closeDevice(); }} // 测试类:public class TestUSB { public static void main(String[] args) { Computer computer = new Computer(); computer.powerOn(); // 使用鼠标设备 computer.useDevice(new Mouse()); // 使用键盘设备 computer.useDevice(new KeyBoard()); computer.powerOff(); }} 这一个例子的一些反思: 1.接口可以理解为某一个形容词,具有……功能,然后具体功能的实现,还是要靠实现这个接口的类 2.其实接口的实现与父类的继承还是蛮像的,最终体现的也是一个多态。对于接口来说,不同的类实现同一接口,也会表现出不同的“行为”;对于父类来说,子类继承了父类后,根据子类不同,重写父类中的方法也不相同,所以体现的也是多态。 3.其实接口实现和父类继承的区别在于我们,因为 父类继承说的是 狗是动物这种关系,接口实现说的是 狗能跑这种能力,相对来说继承更加全面一点,接口范围更小一点 4.最终要实现多态,还是得再写一个方法,在方法参数列表是进行 向上转型,从而实现多态。

2.4 接口的特性

接口是一种引用的类型,但是不能够直接new一个对象

public class TestUSB { public static void main(String[] args) { USB usb = new USB(); }} // Error:(10, 19) java: day20210915.USB是抽象的; 无法实例化 重写接口中的方法时,不能使用默认的访问权限(因为要满足重写的条件)接口中不能有静态代码块和构造方法接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class

2.5 实现多个接口

在java中,子类只能够继承一个父类,即java中不支持多继承,但是一个类可以实现多个接口

实现多个接口就用英文 “,” 连接就可以了


见代码如下:

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 Duck extends Animal implements IRunning, ISwimming, IFlying { public Duck(String name) { super(name); } @Override public void fly() { System.out.println(this.name + "正在用翅膀飞"); }@Overridepublic 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) { System.out.println("我带着伙伴去散步"); 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);

// 执行结果

机器人正在用轮子跑

2.6 接口间的继承

在java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的能力。

接口可以继承一个接口,达到复用的效果。同样使用extends关键字

类与类之间叫:继承

类与接口之间叫:实现

接口与接口之间叫:拓展

但是在接口拓展了另一个接口后,某个类要想实现这个接口,必须重写该接口以及继承的另外一个接口的所有的方法;


class Animal { protected String name; public Animal(String name) { this.name = name; }}interface IFlying { void fly();}interface IRunning extends IFlying{ // 拓展了IFlying这个接口 void run();}class Bird extends Animal implements IRunning{ public Bird(String name) { super(name); } @Override public void fly() { // 这个接口的方法也必须要重写 } @Override public void run() { }}

3. 接口的使用实例

3.1 实现对象的可比较

我们写一个数据通常是可以比较的,不如整型数字可以比较数字的大小,字符类型可以比较ASCII码值等等,因此我们自定义了一个类之后,最好也是可以比较的,因此我们可以通过接口实现这个功能。

3.1.1 Comparable 接口

class Student implements Comparable<Student>{ // Comparable接口 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 int compareTo(Student o) { // 以姓名为标准 return this.name.compareTo(o.name); } */}public class test { public static void main(String[] args) { Student student1=new Student("zhangsan",10); Student student2=new Student("lisi",4); System.out.println(student1.compareTo(student2)); // 使用的方法 }}

对于上述代码有以下注意的点:

1. Comparable<Student> 这个中括号里面是泛型,后面会说

2. 加了 Comparable<Student> 后就表示这个类是可比较的

3. 其实上面的代码就是在类里面写了一个方法,若是不用接口当然也是可以的,但是使用了接口的话那我们的 compareTo 就属于是方法重写了,会改变一些计算机的底层,比如后面我们对象数组进行排序使用Array.sort()方法的时候由于重写了 compareTo 所以Array.sort()方法使用的就是我们重写的 compareTo 而不是计算机自己的

4. String自己也重写了 compareTo 方法所以我们在以姓名为标准进行比较的时候,调用 compareTo 使用的是String自己重写的

缺点:

在使用了这个接口后,那么我们的比较标准也就只能是age或者name了,而不能是同时有age和name两个比较的标准

3.1.2 Comparator 接口

针对Comparable 接口的缺点,所以还有另外一个接口 Comparator 接口,这个接口不写在我们要比较的类的里面,而是另外定义一个比较器类实现这个接口

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 test { public static void main(String[] args) { Student student1=new Student("zhangsan",10); Student student2=new Student("lisi",4); AgeComparator ageComparator = new AgeComparator(); NameComparator nameComparator = new NameComparator(); System.out.println(ageComparator.compare(student1,student2)); System.out.println(nameComparator.compare(student1,student2)); }}

上述代码的注意事项:

1. Comparator<Student> 这个后面同样是泛型,需要比较什么类,我们就写这个类的名称

2. 需要重写 compare 方法

3. 将我们的比较另外写一个类,这样我们就可以根据不同的比较标准写出不同的比较器,并且在同一个代码里面我们可以同时存在多个标准进行比较,就非常的方便

3.2 对象数组的排序

我们知道对普通整型数组可以通过 Arrays.sort() 方法进行排序,但是对于一个普通的类是不可以的,因为 Arrays.sort() 的底层中需要将我们要比较的数据强转为Comparable类型,但是一个普通的类(没有实现Comparable接口)不能随意转换为其他的类型。

对一个对象数组进行排序需要依据我们上面对象的比较,因为要进行排序,肯定需要进行比较,下面就根据我们的两种比较方法给出两种不同的排序方法。

3.2.1 根据Comparable 接口排序

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.name.compareTo(o.name); }}public class test { public static void main(String[] args) { Student[] students = new Student[3]; students[0]=new Student("zhangsan",10); students[1]=new Student("lisi",4); students[2]=new Student("wangwu",25); System.out.println("排序前:"+ Arrays.toString(students)); System.out.println("================"); Arrays.sort(students); System.out.println("排序后:"+ Arrays.toString(students)); }}//输出结果排序前:[Student{ name='zhangsan', age=10}, Student{ name='lisi', age=4}, Student{ name='wangwu', age=25}]================排序后:[Student{ name='lisi', age=4}, Student{ name='wangwu', age=25}, Student{ name='zhangsan', age=10}]

说明:

1. 之所以Student这个类在实现了Comparable接口后就可以比较了是因为在 Arrays.sort() 底层中可以将Student类型强转为Comparable类型了,并且这时在调用 compareTo 后,会调用我们自己重写的方法

2. 我们这个举例是根据姓名进行升序排序,还可以有其他的排序方法(降序等等),只要我们将Student类中重写的 compareTo 方法改一下就可以了

3.2.2 根据比较器进行排序

在java中我们的Arrays.sort其实还有其他的重载的方法,下面我们给出根据比较器进行比较的例子。

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 test { public static void main(String[] args) { Student[] students = new Student[3]; students[0]=new Student("zhangsan",10); students[1]=new Student("lisi",4); students[2]=new Student("wangwu",25); AgeComparator ageComparator = new AgeComparator(); System.out.println("排序前:"+ Arrays.toString(students)); System.out.println("================"); Arrays.sort(students,ageComparator); System.out.println("排序后:"+ Arrays.toString(students)); }}// 输出结果排序前:[Student{ name='zhangsan', age=10}, Student{ name='lisi', age=4}, Student{ name='wangwu', age=25}]================排序后:[Student{ name='lisi', age=4}, Student{ name='zhangsan', age=10}, Student{ name='wangwu', age=25}]

说明:

1. 这里我们在写了比较器后,只要在Arrays.sort中传入两个参数就能够根据我们的比较器标准进行比较了

3.3对象的拷贝

3.3.1 普通拷贝

我们要想使得两个对象的内容一模一样,可以让两个对象的引用赋值相等

public class test { public static void main(String[] args) { Student student1= new Student("lisi",10); Student student2= student1; // student2这个引用与student1的引用指向同一片空间 System.out.println(student2); }}

但是这个时候是student1 和 student2 两个引用中的内容是一样的,也就是指向了同一片空间,这个时候无论我们修改哪一个,另外一个也会跟着改变。

具体的原理我们见下面的图:

在这里插入图片描述

3.3.2 浅拷贝

浅拷贝就是指如果我们要拷贝的对象里面有另外一个对象的话,那这个内对象还是共用了同一片空间,而不是另外开辟一个空间,这个时候修改一个对象的值,另外一个也会跟着改变。

class Money{ public int m=10; @Override public String toString() { return "Money{" + "m=" + m + '}'; }}class Student implements Cloneable{ public String name; public int age; public Money money = new Money(); public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", money=" + money + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class test { public static void main(String[] args) throws CloneNotSupportedException { Student student1= new Student("lisi",10); Student student2 = (Student) student1.clone(); System.out.println("修改前:student1.money.m "+student1.money.m); System.out.println("修改前:student2.money.m "+student2.money.m); System.out.println("=============="); student2.money.m=99; System.out.println("修改后:student1.money.m "+student1.money.m); System.out.println("修改后:student2.money.m "+student2.money.m); }}//输出结果修改前:student1.money.m 10修改前:student2.money.m 10==============修改后:student1.money.m 99修改后:student2.money.m 99

说明:

1. 首先我们对象的拷贝,需要用到clone方法

2. 其次由于clone方法返回的类型是Object,因此我们需要进行强制类型转换

3. 然后我们还需要申明异常(具体知识我们后面再学习,今天知道要拷贝必须要申明异常)

4. 然后clone方法是Object里面的方法,按道理Object类是所有类的父类,但是由于clone方法的访问权限是protected,所以我们要想使用clone方法只能是间接使用,也就是在Student类中重写clone方法,返回super.clone();

5. 这个类要想拷贝,必须实现Cloneable接口,此时Cloneable接口叫做空接口(标记接口),也就是说明这个类是可比较的

缺点:

1. 从我们运行结果来看,缺点就是这个对象中的对象还是共用了同一片空间,并不是开辟另一片新的空间

在这里插入图片描述

3.3.3 深拷贝

深拷贝也就是给我们的对象里面的对象也开辟一块空间,而不是共用一块空间。

class Money implements Cloneable{ public int m=10; @Override public String toString() { return "Money{" + "m=" + m + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}class Student implements Cloneable{ public String name; public int age; public Money money = new Money(); public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", money=" + money + '}'; } @Override protected Object clone() throws CloneNotSupportedException { Student tmp = (Student)super.clone(); tmp.money=(Money)this.money.clone(); return tmp; }}public class test { public static void main(String[] args) throws CloneNotSupportedException { Student student1= new Student("lisi",10); Student student2 = (Student) student1.clone(); System.out.println("修改前:student1.money.m "+student1.money.m); System.out.println("修改前:student2.money.m "+student2.money.m); System.out.println("=============="); student2.money.m=99; System.out.println("修改后:student1.money.m "+student1.money.m); System.out.println("修改后:student2.money.m "+student2.money.m); }}//结果修改前:student1.money.m 10修改前:student2.money.m 10==============修改后:student1.money.m 10修改后:student2.money.m 99

说明:

1. Money类也要实现Cloneable接口,并且在里面也要重写clone方法

2. 这个时候,Student类中重写的clone方法需要变一下,具体怎么变看上面的代码

在这里插入图片描述

4. 判断类相等

使用的是equals()方法,这个方法同样是Object类里面包含的,并且访问权限是 public 因此我们可以直接使用,但是equals()方法判断的标准是1.左右两侧是基本类型变量,比较的是变量中值是否相同; 2.左右两侧是引用类型变量,比较的是引用变量地址是否相同

如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的,我们可以自己写,也可以直接generator使用编译器生成。

class 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 boolean equals(Object o) { Student student = (Student) o; return this.age == student.age && this.name.equals(student.name); } }public class test { public static void main(String[] args) throws CloneNotSupportedException { Student student1= new Student("lisi",10); Student student2 = new Student("lisi",10); System.out.println(student1.equals(student2)); }}//运行结果:true



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。