一、抽象类
在面向对象的概念中,所有对象都是通过类来描绘的,但并不是所有类都是用来描绘对象的。如果一个类中没有足够的属性和行为来描绘一个完整具体的对象,Java 提供了一个语法——抽象类。
例如我们需要一个描述形状的类,成员方法可以输出当前的形状,但是形状有很多种,一个类中如果包含那么多方法就会显得很乱。可以使用抽象类,抽象类无法描述一个形状,但是其子类可以,继承抽象类然后重写抽象方法来输出不同的形状。这样就可以实现代码的重用和扩展性。
抽象类的概念
Java 抽象类是一种特殊的类,抽象类不能被实例化,只能被继承。抽象类的主要作用是为了实现代码的重用和扩展性。通过定义抽象类,可以将一些通用的属性和方法提取出来,供多个子类共享使用和重写。子类可以根据自身的需求对抽象方法进行实现,从而实现不同的功能。
抽象类的特点
1.被关键字 abstract
修饰的类称为抽象类,抽象类不能实例化对象;
2.抽象类中包含抽象方法和成员方法。抽象方法没有具体实现,只有方法的声明,当子类继承抽象类时,子类必须重写这些抽象方法;
3.抽象类可以拥有成员变量和成员方法,可以被子类继承和使用;
4.如果一个类继承了抽象类,那么子类必须重写所有的抽象方法。如果是抽象类则不用重写,但是一旦子类抽象类被普通类继承,需要重写两个父类的所有抽象方法;
5.抽象类可以拥有构造方法,但是不能用于实例化对象。
如何定义抽象类
在文章前面了解到,可以使用 abstract
关键字修饰一个类,让这个类成为抽象类(abstract class),抽象类不能实例化对象,其主要作用就是为了被继承。
也可以使用 abstract
关键字修饰方法,让这个方法成为抽象方法(abstract method),抽象方法没有具体的实现,其主要作用就是为了被子类重写。
1.代码示例: 定义一个图形抽象类
// 抽象形状类
abstract class Shape {
// 成员变量:形状的名字
private String name;
// 抽象方法:输出当前形状,没有具体实现
abstract public void display();
// 成员方法:获取当前形状的名字
public String getName() {
return this.name;
}
}
2.继承上方的抽象类并重写抽象方法
class Triangle extends Shape {
// 构造方法:
// 先构造父类;
// 后构造子类;
public Triangle(String name) {
super(name);
}
//重写抽象方法
@Override
public void display() {
System.out.println("△");
}
}
注意: 如果没有重写抽象类的抽象方法,编译会报错!
3.使用子类重写父类的抽象方法,继承父类的属性、方法
public static void main(String[] args) {
// 实例化对象
Triangle t = new Triangle("三角形");
// 使用子类重写父类的抽象方法:△
t.display();
// 使用子类继承抽象类的成员方法:三角形
System.out.println("形状的名称是 " + t.getName());
}
抽象类的特性
1.抽象类不能被实例化;
public static void main(String[] args) {
Shape shape = new Shape("三角形");
// java: Main.Shape是抽象的; 无法实例化
}
2.抽象方法的访问修饰符不能是 private
;
abstract class Shape {
abstract private void display();
// java: 非法的修饰符组合: abstract和private
}
3.抽象方法的本质就是被子类重写,不能被 static
final
关键字修饰;
abstract class Shape {
abstract public final void draw();
// java: 非法的修饰符组合: abstract和final
abstract public static void display();
// java: 非法的修饰符组合: abstract和private
}
4.抽象类被继承,子类必须重写父类所有抽象方法,否则会报错;
// 抽象类
abstract class Shape {
abstract public void display();
}
// 普通类子类
class Triangle extends Shape{
// java: Triangle不是抽象的, 并且未覆盖Shape中的抽象方法display()
}
当抽象类继承抽象类时,不需要重写父类抽象方法,如果子类再被普通类继承,那么需要重写所有抽象方法:
abstract class Shape {
abstract public void display();
}
abstract class Triangle extends Shape{
// 因为Triangle也是抽抽象类,所以不用重写父类抽象方法
abstract public void draw();
}
class Circle extends Triangle{
@Override
public void draw() {
System.out.println("○");
}
// 就算重写了父类Triangle所有的抽象方法;
// 还是要重写Triangle父类Shape所有的抽象方法;
@Override
public void display() {
System.out.println("△");
}
}
总结:抽象类的作用
1.提供了一种抽象的模版和基类,用于定义一组相关类的共同特征和行为。子类继承抽象类后,必须重写父类抽象方法,从而使得子类具有相同的特征和行为;
2.限制类的实例化。这样可以确保抽象类的实例化对象具有一定的特征和行为,而不是一个没有明确定义的对象;
3.实现代码的复用和扩展。抽象类可以作为其他类的基类,子类可以继承抽象类的特征和行为。这样可以避免重复编写相同的代码,提高代码的复用性;
4.提供多态性。通过抽象类可以统一处理一组相同类的对象,实现多态性。
二、接口
接口这个名称一听大家就能联想到:插座、USB、音响、手机充电接口等等……
产品提供了公共的接口,用户在使用时必须符合规范标准,如果不符合规范标准就不能使用,还会造成不必要的损失。而在 Java 中,接口可以看成是:多个类的公共规范,是一种抽象引用数据类型。
此图就是使用接口不符合规范标准的示例,注意,此图为反例!切勿用于实际。
接口的概念
在 Java 中,接口是一种特殊的引用类型,它定义了一组抽象方法和常量。接口可以被类实现(implement)或者被其他接口类继承(extends)。
接口的主要作用是定义一组方法的规范,强制实现类提供特定的方法。它可以提供代码的灵活性和可扩展性,降低代码的耦合度,使得程序更易于维护和扩展。接口常用于回调方法、定义常量集合、实现多态等场景。
如何定义接口
语法格式
public interface 接口名称 {
// public static final 静态常变量……
// public abstract 抽象方法……
}
语法规定
1.创建接口时使用 interface
关键字;
2.实现接口使用 implements
关键字,支持实现多个接口;
3.创建接口时命名一般以大写的 I
开头,命名一般为形容词性;
4.定义成员变量时,变量默认属性为 public static final
;
5定义成员方法时,方法默认属性为 public abstract
;
6.定义变量和方法时,如果更改了默认属性,则会发生报错。
代码示例: 创建一个 USB 接口
public interface IUSB {
String NAME = "USB-3.0"; // 接口名称
void useUSB(); // 使用USB接口
void closeUSB(); // 关闭接口
}
实现 IUSB 接口
public class USB implements IUSB{
@Override
public void useUSB() {
System.out.println("正在使用" + NAME + "接口");
}
@Override
public void closeUSB() {
System.out.println("关闭" + NAME + "接口");
}
}
使用实现接口的类
public static void main(String[] args) {
USB usb = new USB();
usb.useUSB(); // 输出结果:正在使用USB-3.0接口
usb.closeUSB(); // 输出结果:关闭USB-3.0接口
}
接口特性
1.接口类型是一种引用类型,但是没有构造方法,不能实例化对象;
public static void main(String[] args) {
IUSB usb = new ISUB(); // 无法解析符号“ISUB”
}
2.接口的变量和接口都有默认的属性,定义时书写默认关键字时,这些关键字就会变灰色,表示可以省略。如果定义的时候更改默认属性,就会发生报错;
3.接口中的抽象方法和抽象类的抽象方法一样不能实现,只能由实现接口的子类来实现;
4.重写接口方法时,访问修饰符等级不等低于 public
5.接口中没有构造方法和静态代码块,定义会报错;
6.接口虽然不是类,但是接口编译完成后生成的字节码文件后缀名也是 .class
7.和抽象类一样,实现了接口,如果不重写其抽象方法,那么这个类必须为抽象类。后者如果有普通类,必须重写所有抽象方法。
实现多个类
文章上面也谈到过,Java 是单继承多实现,指的是 Java 类只能继承一个类,但是一个类可以实现多个接口。例如将动物所有的动作都抽象为接口,然而动物代表普通类,普通类需要那些动作,就实现那些接口即可。
将不同的动作抽离出来定义成接口
interface IFly {
String NAME = "飞翔";
void fly(); // 飞翔
}
interface IRun {
String NAME = "奔跑";
void run(); // 奔跑
}
interface ISwim {
String NAME = "游泳";
void swim(); // 游泳
}
interface IJump {
String NAME = "弹跳";
void jump(); // 弹跳
}
interface IRoll {
String NAME = "滚动";
void Roll(); // 滚动
}
然后由不同特性的动物类来实现这些行为即可
// 小狗会跑、游泳和跳
class Dog implements IRun,ISwim,IJump{
public String name; // 姓名
// 奔跑
@Override
public void run() {
System.out.println(this.name + "正在狂跑");
}
// 狗刨
@Override
public void swim() {
System.out.println(this.name + "正在游泳");
}
// 跳跃
@Override
public void jump() {
System.out.println(this.name + "正在跳过一个一个小坎");
}
}
// 小鸟会飞翔、跳跃
class bird implements IFly,IJump {
public String name; // 姓名
@Override
public void fly() {
System.out.println(this.name + "正在飞");
}
@Override
public void jump() {
System.out.println(this.name + "正在树干上来回跳跃");
}
}
注意: 一个类可以实现多个接口,但是必须重写这些接口的所有抽象方法,否则必须设置为抽象类。
接口之间继承
虽然类与类之间只能单继承,但是 Java 中接口与接口之间可以实现多继承。使用关键字 extends
。
例如高级动物——人,有很多种行为:
interface IRun {
String NAME = "跑步";
void run(); // 跑步
}
interface IEat {
String NAME = "吃饭";
void eat(); // 吃饭
}
interface IRead {
String NAME = "阅读";
void read(); // 阅读
}
interface IWork {
String NAME = "工作";
void work(); // 工作
}
就拿学生举例,学生在上方定义的接口中,会跑步、吃饭和阅读。但是学生也可以有自己独立的属性:班级、学校地址等等…… 独立的行为:学习、睡觉、休息等等……
interface IStudent extends IRun,IEat,IRead {
String GRADE = "六年级"; // 年级
String ADDRESS = "巨人小学"; // 地址
void learn(); // 学习
void sleep(); // 睡觉
void rest(); // 休息
}
当我们使用普通类实现 IStudent
接口时,需要重写所有的抽象方法。接口之间的继承就相当于将多个接口合并在一起。
Comparable 接口
这里拿数组排序作为例子,数组排序在面向过程语言实现时,需要自己手动编写相关代码逻辑。而在面向对象语言中,早就封装好了一系列的方法,用户只需要调用即可。
面向过程数组排序——C语言
#include <stdio.h>
// 打印数
void display(int array[], int len)
{
if (array == NULL)
{
printf("数组为NULL, 打印失败!");
return;
}
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", array[i]);
}
printf("\n"); // 换行
}
// 冒泡排序
int* bubble_sort(int array[], int len)
{
if (array == NULL)
{
printf("数组为NULL,排序失败!");
return NULL;
}
int i = 0;
for (i = 0; i < len - 1; i++)
{
int j = 0;
int orderly = 1; // 判断是否有序
for (j = 0; j < len - i - 1; j++) {
if (array[j] < array[j + 1])
{
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
orderly = 0;
}
}
if (orderly)
{
break;
}
}
return array;
}
// 主函数
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int len = sizeof(arr) / sizeof(arr[0]); // 求数组长度
printf("排序前打印:");
display(arr, len);
printf("排序后打印:");
display(bubble_sort(arr, len), len);
return 0;
}
输出结果
面向对象数组排序——Java
public static void main(String[] args) {
Integer[] array = {1,2,3,4,5,6,7,8,9,10};
System.out.println("排序前打印:" + Arrays.toString(array));
Arrays.sort(array, Collections.reverseOrder());
System.out.println("排序后打印:" + Arrays.toString(array));
}
输出结果:
排序前打印:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
排序后打印:[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
可以看出,调用 Arrays
类中现成的 sort()
方法, 的确要比C语言要方便。Arrays
类中的 sort()
方法除了能排序基本数值类型的数组,还可以将对象数组进行排序,例如排序一组学生数据的数组。
学生类
class Student {
String name; // 姓名
int age; // 年龄
double score; // 成绩
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
}
定义五个学生存放至数组调用 sort()
Student stu1 = new Student("小红",18,98);
Student stu2 = new Student("小明",15,50);
Student stu3 = new Student("小蓝",18,60);
Student stu4 = new Student("小菊",18,70);
Student stu5 = new Student("小小",16,50);
Student[] students = {stu1,stu2,stu3,stu4,stu5};
Arrays.sort(students);
发生报错
报错的原因是因为学生对象的属性有很多种,sort()
方法不知道根据那个属性进行排序,也不知道排序的规则是什么。这就需要用户来告知编译器该根据哪个属性那种规则进行排序。
sort()
的底层是根据 Comparable
接口的 compareTo()
方法的返回值来进行排序的,而我们需要做的就是重写 compareTo()
方法来重新定义比较规则,这就需要学生类来实现 Comparable
接口。
重定义比较规则,重写 compareTo()
方法,根据 age
排序
- A > B 返回 正数
- A < B 返回 负数
- AB相同 返回 0
class Student implements Comparable {
// 成员变量
String name; // 姓名
int age; // 年龄
double score; // 成绩
// 构造方法
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
// 重新定义比较规则
@Override
public int compareTo(Object o) {
Student student = (Student)o;
return this.age - student.age;
}
// 重写toString()方法
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
" age=" + age +
" score=" + score +
'}' + '\n';
}
}
当我们再一次调用 sort
方法时,则不会报错,因为编译器已经知道了比较对象和规则。
public static void main(String[] args) {
Student stu1 = new Student("小红",18,98);
Student stu2 = new Student("小明",15,50);
Student stu3 = new Student("小蓝",18,60);
Student stu4 = new Student("小菊",14,70);
Student stu5 = new Student("小小",16,50);
Student[] students= {stu1,stu2,stu3,stu4,stu5};
System.out.println("排序前打印:");
System.out.println(Arrays.toString(students));
Arrays.sort(students);
System.out.println("排序后打印:");
System.out.println(Arrays.toString(students));
System.out.println("降序打印:");
Arrays.sort(students,Comparator.reverseOrder());
System.out.println(Arrays.toString(students));
}
输出结果:
排序前打印:
[Student{name='小红' age=18 score=98.0}
, Student{name='小明' age=15 score=50.0}
, Student{name='小蓝' age=18 score=60.0}
, Student{name='小菊' age=14 score=70.0}
, Student{name='小小' age=16 score=50.0}
]
排序后打印:
[Student{name='小菊' age=14 score=70.0}
, Student{name='小明' age=15 score=50.0}
, Student{name='小小' age=16 score=50.0}
, Student{name='小红' age=18 score=98.0}
, Student{name='小蓝' age=18 score=60.0}
]
降序打印:
[Student{name='小红' age=18 score=98.0}
, Student{name='小蓝' age=18 score=60.0}
, Student{name='小小' age=16 score=50.0}
, Student{name='小明' age=15 score=50.0}
, Student{name='小菊' age=14 score=70.0}
]