找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:JavaSE
接上文:初始Java篇(JavaSE基础语法)(7)抽象类和接口(上)-CSDN博客
目录
实现多个接口
接口间的继承
使用Java内置的部分接口
Comparable 接口
Cloneable 接口
深拷贝
抽象类和接口的区别
Object类
实现多个接口
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。下面通过类来表示一组动物。
class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
上面是一些动物都有的特征,但是还有一部分是某些动物特有的。例如:飞,跑,游泳……
没有学习接口之前,我们可能会用继承的思想来实现。
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void run() {
System.out.println(name+" 正在遛!");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void run() {
System.out.println(name+" 正在遛!");
}
}
应用接口之后:
// 狗 是一个 动物 具备 跑的能力
class Dog extends Animal implements IRun{
public Dog(String name, int age) {
super(name, age);
}
//重写接口的方法
@Override
public void run() {
System.out.println(name+" 正在遛!");
}
}
// 猫 是一个 动物 具备 跑的能力
class Cat extends Animal implements IRun{
public Cat(String name, int age) {
super(name, age);
}
//重写接口的方法
@Override
public void run() {
System.out.println(name+" 正在遛!");
}
}
interface IRun {
void run();
}
而实现了接口之后,在以后的编程中,我们就只需要知道哪些是具备跑的能力,就可以用接口来实现其跑的方法,而不在需要关注是谁调用的它。
了解基本的用法之后,就可以开始实现,用一个类继承一个类,实现多个接口了。
狗是一种动物,具有会跑的特性。
青蛙也是一种动物,既能跑,也能游泳。
鸭子也是一种动物,既能跑,也能游,还能飞。
class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal implements IRun{
public Dog(String name, int age) {
super(name, age);
}
@Override
public void run() {
System.out.println(name+" 正在遛!");
}
}
class Cat extends Animal implements IRun{
public Cat(String name, int age) {
super(name, age);
}
@Override
public void run() {
System.out.println(name+" 正在遛!");
}
}
class Frog extends Animal implements IRun, ISwim {
public Frog(String name, int age) {
super(name, age);
}
@Override
public void run() {
System.out.println(name+" 正在跳来跳去!");
}
@Override
public void swim() {
System.out.println(name+" 正在游泳!");
}
}
class Duck extends Animal implements IRun, ISwim, IFly {
public Duck(String name, int age) {
super(name, age);
}
@Override
public void run() {
System.out.println(name+" 正在飞快地跑!");
}
@Override
public void swim() {
System.out.println(name+" 正在游泳");
}
@Override
public void fly() {
System.out.println(name+" 正在飞得更高!");
}
}
interface IRun {
void run();
}
interface ISwim {
void swim();
}
interface IFly {
void fly();
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("大黄",4);
dog.run();
Duck duck = new Duck("唐老鸭", 3);
duck.fly();
duck.run();
duck.swim();
Frog frog = new Frog("青蛙王子", 3);
frog.run();
frog.swim();
}
}
这就是接口的妙处:只要这个类有这个功能,那么就可以实现该接口,继而间接实现多继承。并且也不用管实现这个接口是什么类,只要有这个功能就行。
例如:机器人也具有跑的功能。
接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。 接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字。
interface A {
void a();
}
interface B extends A{
void b();
}
这里的 extends 可以理解为拓展的意思,即B接口拓展了A接口的功能。那也就意味着当有类去实现B接口时,也要把A接口的抽象方法也给重写。
interface A {
void a();
}
interface B extends A{
void b();
}
class C implements B {
@Override
public void a() {
}
@Override
public void b() {
}
}
接口间的继承相当于把多个接口合并在一起。
使用Java内置的部分接口
Comparable 接口
首先,创建一个数组,里面放入多个学生对象。
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Test {
public static void main(String[] args) {
Student[] students = {new Student("张三",20),
new Student("李四", 18),
new Student("王五",13)};
}
}
现在要对这个数组进行排序(根据年龄),我们之前在学习数组时,学过一个方法sort可以对数组进行排序。
之所以会出现这样的情况就是因为我们给得是两个对象,而之前我们在比较的时候是用的两个整数。而两个对象要进行比较就得用到一个接口 Comparable ,用这个接口里的 compareTo 方法。
// 要比较的类型
class Student implements Comparable<Student>{
String name;
int age;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
if (this.age > o.age) {
return -1;
}else if (this.age < o.age) {
return 1;
}else {
return 0;
}
}
}
public class Test {
public static void main(String[] args) {
Student[] students = {new Student("张三", 20),
new Student("李四", 13),
new Student("王五", 18)};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
在 sort 方法中会自动调用 compareTo 方法。
注意:对于 sort 方法来说,需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力。通过重写 compareTo 方法的方式, 就可以定义比较规则。
但是上面的方法也有局限性。当我们写的是年龄比较时,那么就只能比较年龄,而不能比较其他的。这时,就可以把这个比较方法单独写成一个类,我们需要用的时候,就可以通过对象的引用来调用这个方法。
这样就更加便捷了。
Cloneable 接口
Object 类(待会解释)中存在一个 clone 方法,调用这个方法可以创建一个对象的 "拷贝",但是要想合法调用 clone 方法, 必须要先实现 Cloneable 接口, 否则就会抛出 CloneNotSupportedException 异常。
直接拷贝,肯定是不行的。得先实现 Cloneable 这个接口,再重写这个clone方法。至于为什么要重写这个方法?是因为这个方法是由 protected 修饰的,在外部类中只有拥有继承关系的才能够直接调用这个方法,因此我们得重写这个方法。
class Animal implements Cloneable{
String name;
int gae;
public Animal(String name, int age) {
this.name = name;
this.gae = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Animal animal = new Animal("狗类", 20);
//现在要实现对象的拷贝
Animal animal1 = (Animal)animal.clone();
}
}
从编译器生成的方法来看,的确只是因为访问权限的问题,毕竟只是调用父类的clone方法。
这里的 throws CloneNotSupportedException 声明是一种防御性编程的体现,确保了API的使用者明白克隆操作可能存在的限制或前提条件。但实际上,如果你的类已经实现了 Cloneable 接口,并且确定所有引用的对象也支持克隆,那么在正常情况下,这个异常通常不会被抛出。
简单理解就是不在使用clone方法的后面加上 throws CloneNotSupportedException ,会导致程序报错,抛出不支持克隆异常。如下图:
我们也要注意一下,这个clone方法的返回值。返回的是一个Object - 父类(待会解释), 父类对象给到子类引用,有很大危险!除非是同型的。例如:父类的也是Animal,那么就没问题了。
IDEA上生成重写方法等的快捷的方法:
从上图我们可以看到有许多可以生成的快捷方法。例如:重载,获取private修饰的成员变量的方法等。 下图是克隆对象的流程图:
深拷贝
在了解深拷贝之前,我们先改一改前面的代码。
class Food {
int food = 100;
}
class Animal implements Cloneable{
String name;
int gae;
public Animal(String name, int age) {
this.name = name;
this.gae = age;
}
//对象里面又有一个对象
Food animalFood = new Food();
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Animal animal = new Animal("狗类", 20);
//现在要实现对象的拷贝
Animal animal1 = (Animal)animal.clone();
System.out.println("改变前:");
System.out.println("没有被拷贝的animal:"+animal.animalFood.food);
System.out.println("被拷贝的animal:"+animal1.animalFood.food);
animal.animalFood.food = 50;
System.out.println("改变后:");
System.out.println("没有被拷贝的animal:"+animal.animalFood.food);
System.out.println("被拷贝的animal:"+animal1.animalFood.food);
}
}
这就是浅拷贝,只是拷贝了一级对象本身,并没有拷贝一级对象里的二级对象等。
而深拷贝就是只要是对象,那么就都会被拷贝到我们的那个引用里。
从这个结果来看,肯定是我们的拷贝方法出了问题。那么我们就得重新写这个方法。
浅拷贝示意图:
深拷贝示意图:
思路:先把animal的对象拷贝一份,放到 tmp 中,再把animal对象里的 animalfood 的对象拷贝一份放到animal1food里,最后把 animal1 返回。(这里要注意对象之间的类型转换)
class Food implements Cloneable {
int food = 100;
//克隆Food
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Animal implements Cloneable{
String name;
int gae;
public Animal(String name, int age) {
this.name = name;
this.gae = age;
}
//对象里面又有一个对象
Food animalFood = new Food();
@Override
protected Object clone() throws CloneNotSupportedException {
//克隆了animal对象
Animal tmp = (Animal)super.clone();
//现在要克隆animalfood对象,先得在animalfood这个类中实现克隆接口,并且重写克隆方法
tmp.animalFood = (Food)this.animalFood.clone();
return tmp;
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Animal animal = new Animal("狗类", 20);
//现在要实现对象的拷贝
Animal animal1 = (Animal)animal.clone();
System.out.println("改变前:");
System.out.println("没有被拷贝的animal:"+animal.animalFood.food);
System.out.println("被拷贝的animal:"+animal1.animalFood.food);
animal.animalFood.food = 50;
System.out.println("改变后:");
System.out.println("没有被拷贝的animal:"+animal.animalFood.food);
System.out.println("被拷贝的animal:"+animal1.animalFood.food);
}
}
这里我们来解析一下上面的代码。主要目的是拷贝这个 animalfood 对象。而克隆这个对象之前,先得实现Cloneable 这个接口,再重写克隆方法,这样就可以克隆该对象了。做完准备工作之后,就可以开始克隆animal这个对象,再把这个对象里的animalfood对象再克隆一个放到刚刚拷贝的对象里的animalfood 。即 tmp.animalfood = (Food)this.animalfood.clone(); 最后再返回一级对象。
抽象类和接口的区别
抽象类和接口都是 Java 中多态的常见使用方式。都需要重点掌握,同时又要认清两者的区别。
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法, 子类必须重写所有的抽象方法。
例如: Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的。因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口。(因为接口中的字段必须是一个常量)
区别 | 抽象类 | 接口 |
结构组成 | 普通类的基础上+抽象方法(可无) | 抽象方法和常量 |
权限 | private default protected public | public |
子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
关系 | 一个抽象类可以实现多个接口 | 接口不能继承抽象类,但是接口可以使用extends关键字拓展多个接口 |
子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
Object类
Object是Java默认提供的一个父类,也就是说不管是什么类,都是Object的子类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
例如:
对象比较equals方法
在Java中,==进行比较时:
如果==左右两侧是基本类型变量,比较的是变量中值是否相同。
如果==左右两侧是引用类型变量,比较的是引用变量存放的地址是否相同。
如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的。
// Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj); //比较的是两个引用的值,也就是地址
}
如果没有重写 equals 方法,那么就会直接比较对象引用的值,也就是地址会失败。
可能有小伙伴有疑惑:为什么都是狗和20岁,地址会不相同。
因为在new一个对象时,jvm(Java虚拟机)都会给这个对象在堆区里分配空间,而第一份空间和第二份空间的地址是不相同的(内存中不可能存在两个相同的地址)。
因此,我们想要比较对象的内容,就得重写 equals 方法,不再比较对象。
结论:比较对象中内容是否相同的时候,一定要重写equals方法。
好啦!本期 初始Java篇(JavaSE基础语法)(7)抽象类和接口(下)的学习之旅就到此结束了!我们下一期再一起学习吧!