目录
一、前言
二、抽象类
2.1 抽象类的概念
2.2 抽象类语法
2.3 抽象类特性
2.4 抽象类的作用
三、接口
3.1 什么是接口
3.2 语法规则
3.3 接口使用
3.4 接口特性
3.5 实现多接口
3.6 接口间的继承
四、Object类
4.1 获取对象信息( toString() )
4.2 对象间的比较 ( equals() )
4.3 hashcode 方法
五、接口使用实例
5.1 给对象数组排序 (Comparable接口—compareTo方法)
5.2 Cloneable 接口
六、抽象类和接口的区别
一、前言
在当今迅速发展的软件开发领域,面向对象编程(OOP)已经成为了构建复杂系统的重要方法论。在这一过程中,抽象类和接口作为核心概念,发挥着不可或缺的作用。它们不仅帮助我们设计灵活、可扩展的系统结构,还促进了代码的重用和维护。
在本篇博客中,我们将探讨抽象类和接口的定义、特点及其实际应用。
二、抽象类
2.1 抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
比如:我们之前所学习的 Animal 类,其成员变量或方法所描绘的是所有动物的共性,并不能描述具体对象,因此可以设计为抽象类。
【说明】:
- Animal 是动物类,每个动物都有叫的方法,但是由于 Animal 不是一个具体的动物,因此内部 bark() 方法无法具体实现。
- Dog 类是狗类,与 Animal 是继承关系,狗是一种具体的动物,狗叫:汪汪汪,其 bark() 可以实现。
- Cat 类是猫类,与 Animal 是继承关系,猫是一种具体的动物,猫叫:喵喵喵,其 bark() 可以实现。
- 因此:Animal 可以设计为抽象类。
在上述例子中,可以发现,父类 Animal 的 bark() 方法貌似并没有什么实际的工作,主要描述动物的叫声都是由 Animal 的子类中 bark() 方法来完成的。像这种没有实际工作的方法,我们把它设计成一个抽象方法(abstract method),包含抽象方法的类我们称为抽象类(abstract class)。
2.2 抽象类语法
在 Java 中,一个类如果被 abstract 修饰称为抽象类,在抽象类中被 abstract 修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。抽象类也是类,内部可以包含普通方法和属性,甚至构造方法。
// 抽象类:被 abstract 修饰的类
public abstract class Shape {protected double area; // 面积// 抽象方法:被 abstract 修饰的方法,没有方法体abstract public void draw();abstract void calcArea();// 抽象类也是类,也可以增加普通方法和属性public double getArea() {return area;}
}
2.3 抽象类特性
- 抽象类不能直接实例化对象
Shape shape = new Shape();// 编译出错 java: Shape是抽象的; 无法实例化
- 抽象方法不能是 private 修饰的
- 抽象方法不能被 final 和 static 修饰,因为抽象方法要被子类重写
- 抽象类必须被继承,并且继承之后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
// 矩形类 public class Rect extends Shape{private double length;private double width;public Rect(double length, double width) {this.length = length;this.width = width;}@Overridepublic void draw() {System.out.println("矩形:length = " + length + " width = " + width);}@Overridevoid calcArea() {area = length * width;} }// 圆类 public class Circle extends Shape {private double r;final private static double PI = 3.14;public Circle(double r) {this.r = r;}@Overridepublic void draw() {System.out.println("圆:r = " + r);}@Overridevoid calcArea() {area = PI * r * r;} }
// 三角形类 public abstract class Triangle extends Shape{private double a;private double b;private double c;@Overridepublic void draw() {System.out.println("三角形:a = " + a + " b = " + b + " c = " + c);}//三角形:直角三角形、等腰三角形等,还可以继续细化//@Override//void calcArea(); // 编译失败:要么实现该抽象方法,要么将三角形设计为抽象类 }
- 抽象类中不一定包含抽象方法,但有抽象方法的类一定是抽象类
- 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
2.4 抽象类的作用
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。
那么我们就会有一个疑问:普通的类也可以被继承,普通的方法也可以被重写,为啥非得用抽象类和抽象方法呢?
确实如此,但是抽象类相当于多了一重编译器的校验
使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而是应该由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的,但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。
很多语法存在的意义都是为了"预防出错",例如我们曾经用过的 final 也是类似,创建的变量用户不去修改,不就相当于常量吗?但是加上 final 能够在我们不小心误修改的时候,让编译器提醒我们。
充分利用编译器的校验,在实际开发中是非常有意义的。
三、接口
3.1 什么是接口
接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。在 Java 中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
3.2 语法规则
接口的定义格式与定义类的格式基本相同,将 class 关键字换成 interface 关键字,就定义了一个接口。
public interface IUSB {public abstract void method(); // public abstract 可以不写public void method2();abstract void method3();void method4();
}// 上述写法都是抽象方法,更推荐方式4,代码更简洁
接口的命名一般以大写字母 I 开头,其后一般跟"形容词"词性的单词。
3.3 接口使用
接口不能直接使用,需要有 "实现类" 来 "实现" 该接口,实现接口中的所有抽象方法。子类和父类之间是 extends 关系,类与接口之间是 implements 实现关系。
// USB接口
public interface IUSB {void openDevice();void closeDevice();
}
// 鼠标类,实现USB接口
public class Mouse implements IUSB{@Overridepublic void openDevice() {//... 实现接口中的抽象方法openDevice}@Overridepublic void closeDevice() {//... 实现接口中的抽象方法closeDevice}
}
Mouse 类就是接口 USB 的一个实现类,Mouse 类中可以实现自己的方法,但必须将接口中所有的抽象方法重写,否则就会报错。
3.4 接口特性
- 接口类型是一种引用类型,但是不能直接 new 接口的对象。
- 接口中每一个方法都是 public 修饰的抽象方法,即接口中的方法会被隐式的指定为 public abstract (只能是 public abstract ,其他修饰符会报错)。
- 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现。
- 实现类重写接口方法时,要使用 public 修饰。
- 接口当中的成员变量,默认是 public static final 修饰的(可以不写)。
- 接口中不能有构造方法和代码块。
- 接口当中不可以有普通的方法。
- 一个接口也会产生独立的字节码文件( .class )。
- 如果类没有实现接口中所有的抽象方法,那么这个类必须设置为抽象类。
- 接口也是可以发生向上转型和动态绑定的。
3.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();
}
接下来创建一些类对上面的类和接口进行实现:
猫:会跑,所以用 IRunning 接口:
class Cat extends Animal implements IRunning{public Cat(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "正在用四条腿跑");}
}
鱼:会游泳,所以用 ISwimming 接口:
class Fish extends Animal implements ISwimming {public Fish(String name) {super(name);}@Overridepublic void swim() {System.out.println(this.name + "正在用尾巴游泳");}
}
青蛙:既能在陆地上跑,又能在水里游,所以用 IRunning 接口和 ISwimming 接口:
class Frog extends Animal implements IRunning,ISwimming {public Frog(String name) {super(name);}@Overridepublic void run() {System.out.println(this.name + "正在往前跳");}@Overridepublic void swim() {System.out.println(this.name + "正在蹬腿游泳");}
}
鸭子:能跑,能游泳,还能飞,所以用 IRunning 接口、ISwimming 接口和 IFlying 接口:
class Duck extends Animal implements ISwimming, IRunning, IFlying {public Duck(String name) {super(name);}@Overridepublic void fly() {System.out.println(this.name + "正在用翅膀飞");}@Overridepublic void run() {System.out.println(this.name + "正在岸上跑");}@Overridepublic void swim() {System.out.println(this.name + "正在用脚蹼游泳");}
}
注意:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类。
接下来我们可以测试一下我们的代码:
public class Test {public static void testRun(IRunning running) {running.run();}public static void testSwim(ISwimming iSwimming) {iSwimming.swim();}public static void testFly(IFlying iFlying) {iFlying.fly();}public static void main(String[] args) {Cat cat = new Cat("咪咪");Fish fish = new Fish("小鲤鱼");Frog frog = new Frog("呱呱");Duck duck = new Duck("嘎嘎");testRun(cat);testRun(frog);testRun(duck);System.out.println("==========");testSwim(fish);testSwim(frog);testSwim(duck);System.out.println("==========");testFly(duck);}
}
上述的代码就是 Java 面向对象编程中最常见的用法:一个类继承一个父类,同时实现多种接口。当然也可以不继承父类,只实现接口。
继承的表达含义是 is-a 语义,而接口表达的含义是 具有xxx特性 。
猫是一种动物,具有会跑的特性青蛙也是一种动物,既能跑,也能游泳鸭子也是一种动物,能飞、能跑也能游泳
3.6 接口间的继承
在 Java 语言中:类和类之间只能是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。
接口可以继承一个或多个接口,达到复用的效果,使用 extends 关键字。
// 两栖的动物,既能跑也能游泳
public interface IAmphibious extends IRunning,ISwimming {}
接口间的继承相当于把多个接口合并在一起。
四、Object类
Object 类是 Java 默认提供的一个类,Java 中所有的类都默认继承 Object 类。换句话说就是:Object 类是所有类的父类,所有类的对象都可以使用 Object 的引用进行接收。
使用 Object 接收所有类的对象:
class Person {
}class Student {
}public class Test {public static void function(Object obj) {System.out.println(obj);}public static void main(String[] args) {function(new Person());function(new Student());}
}//执行结果:
Person@3b07d329
Student@404b9385
关于 Object 类中的方法,我们可以在帮助文档中查看:
其中常用的方法有:toString()方法、equals()方法、hashcode()方法...
4.1 获取对象信息( toString() )
Object 类中的 toString 方法实现:返回值是 类名+@+哈希码的十六进制表示。
如果不重写 Object 的 toString() 方法,打印出来的结果是 Java 中对象的默认字符串表示形式:
public class Person {String name;String gender;int age;public Person(String name, String gender, int age) {this.name = name;this.gender = gender;this.age = age;}public static void main(String[] args) {Person person = new Person("Jim", "男", 18);System.out.println(person);}
}//打印结果:Person@4eec7777
//Person 是对象属于的类名称。
//@4eec7777 是对象的哈希码的十六进制表示,表示对象在内存中的唯一性。
println() 方法为什么跟 Object 类中的 toString() 方法联系上了呢?
我们可以使用 Ctrl + 鼠标左键点击 println 跳转到方法内部去查看其逻辑:层层关系之下,我们可以看到最后其 return 的值是 null 或者 Object 类的 toString 方法。
故此想要打印具体的对象信息,可以重写 toString() 方法:
public class Person {String name;String gender;int age;public Person(String name, String gender, int age) {this.name = name;this.gender = gender;this.age = age;}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", gender='" + gender + '\'' +", age=" + age +'}';}public static void main(String[] args) {Person person = new Person("Jim", "男", 18);System.out.println(person);}
}
4.2 对象间的比较 ( equals() )
Object 类中的 equals 方法实现:equals方法默认是按照地址比较的。
我们之前学习过 == 也可以进行比较:
- 当 == 两边是基本数据类型变量时,比较的是变量中值是否相同
- 当 == 两边是引用数据类型变量时,比较的是引用变量地址是否相同
如果要比较两个引用数据类型变量的内容是否相同就要用到 equals() 方法并且需要重写。
public class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}public static void main(String[] args) {Person p1 = new Person("Jim", 18);Person p2 = new Person("Jim", 18);int a = 10;int b = 10;System.out.println(a == b); //trueSystem.out.println(p1 == p2); //falseSystem.out.println(p1.equals(p2)); //false}
}
Person 类重写 equals 方法后,然后比较:
public class Person {String name;int age;@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return age == person.age && Objects.equals(name, person.name);}public Person(String name, int age) {this.name = name;this.age = age;}public static void main(String[] args) {Person p1 = new Person("Jim", 18);Person p2 = new Person("Jim", 18);System.out.println(p1.equals(p2)); //true}
}
故此,比较对象中内容是否相等的时候,一定要重写 equals 方法。
4.3 hashcode 方法
在刚刚我们调用 toString 方法时,已经见过 hashcode 方法了,他帮我们算了一个具体的对象位置,然后调用Integer.toHexString()方法,将这个地址以16进制输出。
我们同样可以在 Object 类中找到 hashcode 方法源码:
public native int hashCode();
该方法是一个 native 方法,底层是由 C/C++ 代码写的,我们看不到。
public class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}public static void main(String[] args) {Person p1 = new Person("Jim", 18);Person p2 = new Person("Jim", 18);System.out.println(p1.hashCode()); //1324119927System.out.println(p2.hashCode()); //990368553}
}
此时,两个对象的 hash 值不一样。
类似于重写 equals 方法一样,我们也可以重写 hashcode 方法。此时我们再来看代码:
public class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}@Overridepublic int hashCode() {return Objects.hash(name, age);}public static void main(String[] args) {Person p1 = new Person("Jim", 18);Person p2 = new Person("Jim", 18);System.out.println(p1.hashCode()); //2309797System.out.println(p2.hashCode()); //2309797}
}
此时,两个对象的 hash 值一样。
【结论】:
- hashcode 方法用来确定对象在内存中的存储位置是否相同。
- 事实上 hashcode 在散列表中才有用,在其他情况下没用。在散列表中 hashcode 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
五、接口使用实例
了解完 Object 类后,我们可以更好的理解接口的使用情景:
5.1 给对象数组排序 (Comparable接口—compareTo方法)
class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}@Overridepublic String toString() {return "[" + this.name + this.score + ']';}public static void main(String[] args) {Student[] students = new Student[] {new Student("zs",95),new Student("ls",80),new Student("ww",61),new Student("zl",93),};Arrays.sort(students);System.out.println(Arrays.toString(students));}
}
按照我们之前所学习的内容,数组本身提供一个排序的 sort 方法,那么能否像上述代码那样使用 sort 方法排序后打印呢?运行一下,我们看到程序报错了:
仔细思考,不难发现,和普通的整数不一样,两个整数是可以直接比较的,大小关系明确。而两个学生对象的大小关系怎么确定?需要我们额外指定。
让我们的 Student 类实现 Comparable 接口,并实现其中的 compareTo 方法:
class Student implements Comparable<Student>{private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}@Overridepublic String toString() {return "[" + this.name + this.score + ']';}@Overridepublic int compareTo(Student o) {return this.score - o.score; //大于输出整数,小于输出负数,等于输出0}public static void main(String[] args) {Student[] students = new Student[] {new Student("zs",95),new Student("ls",80),new Student("ww",61),new Student("zl",93),};Arrays.sort(students);System.out.println(Arrays.toString(students));}
}
这样就将无序的对象数组排序了。对于 sort 方法来说,需要传入的数组的每个对象都是"可比较"的,需要具备 compareTo 这样的能力。通过重写该方法就可以定义比较规则。
为了加深理解,我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序):
class Student implements Comparable<Student>{private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}@Overridepublic String toString() {return "[" + this.name + this.score + ']';}@Overridepublic int compareTo(Student o) {return this.score - o.score; //大于输出整数,小于输出负数,等于输出0}public static void sort(Comparable[] array){for (int i = 0; i < array.length; i++) {for (int j = 0; j < array.length - 1 - i; j++) {if (array[j].compareTo(array[j+1]) > 0) {Comparable tmp = array[j];array[j] = array[j + 1];array[j + 1] = tmp;}}}}public static void main(String[] args) {Student[] students = new Student[] {new Student("zs",95),new Student("ls",80),new Student("ww",61),new Student("zl",93),};sort(students); //这里使用的是自己定义的 sort 而不是 Arrays.sort()System.out.println(Arrays.toString(students));}
}
5.2 Cloneable 接口
Java 中内置了一些很有用的接口,Cloneable 就是其中之一:
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝"。但是要想合法调用clone 方法, 必须要先实现 Cloneable 接口, 否则就会抛出 CloneNotSupportedException 异常。
class Animal implements Cloneable{private String name;@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}@Overridepublic String toString() {return "Animal{" +"name='" + name + '\'' +'}';}public Animal(String name) {this.name = name;}public static void main(String[] args) throws CloneNotSupportedException {Animal animal1 = new Animal("小鸟");Animal animal2 = (Animal) animal1.clone();System.out.println(animal1 == animal2);System.out.println(animal1);System.out.println(animal2);}
}
六、抽象类和接口的区别
核心区别:抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法,子类必须重写所有的抽象方法。
抽象类存在的意义是为了让编译器更好的校验,像 Animal 这样的类我们并不会直接使用,而是使用它的子类。万一不小心创建了 Animal 的实例,编译器会及时提醒我们。
靡不有初,鲜克有终