JavaSE之抽象类和接口
文章目录
- 抽象类
- 抽象类概念
- 抽象类语法
- 抽象类要点总结
- 接口
- 接口的概念
- 接口实例
- 接口间的多继承
- 接口要点总结
- Comparable接口
- Comparator接口
- Clonable接口
- 深拷贝的代码实现
- 抽象类和接口的区别

给个关注叭
个人主页
JavaSE专栏
前言:本篇文章主要整理了抽象类的概念及语法、抽象类要点总结.接口的概念,通过实例进一步理解接口、接口间的多继承、接口要点总结、Comparable接口、Comparator接口、Cloneable接口、浅拷贝和深拷贝代码实现及其堆栈图、抽象类和接口的区别。
抽象类
抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类就是抽象类。
比如:
- Animal类是动物类,其内部有一个bark方法,每个动物都有不同叫的方法,但是由于Animal类不是一个具体的动物,因此其内部bark()方法无法具体实现
- Dog是狗类,与Animal类是继承关系,其次狗是一种具体的动物,其bark方法有具体的叫法,“汪汪汪”
- Cat是猫类,与Animal类是继承关系,其次猫是一种具体的动物,其bark方法有具体的叫法,“喵喵喵”
- 所以这种Animal类中的bark()方法不需要有具体的实现,那么可以把Animal类设置为抽象类,只需让其具体的子类根据需要去重写这个抽象方法
在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法, 包含抽象方法的类我们称为 抽象类
抽象类语法
抽象类需要用abstract
的关键字来修饰类,同时abstract
关键字也可以修饰方法,被abstract
修饰的方法称为抽象方法,抽象方法可以没有具体的实现。代码示例如下:
// 抽象类:被abstract修饰的类
public abstract class Shape {
// 抽象方法:被abstract修饰的方法,没有方法体abstract public void draw();abstract void calcArea();
// 抽象类也是类,也可以增加普通方法和属性public double getArea(){return area;}protected double area; // 面积
}
抽象类首先是一个类,和普通类一样,可以有普通方法和变量,甚至构造方法,可以被继承,唯一不同的是抽象类不能实例化对象
抽象类要点总结
- 被
abstract
修饰的类称为抽象类,被abstract
修饰的方法称为抽象方法,抽象方法可以没有具体的实现 - 抽象类和普通类一样,可以定义普通类中的成员变量和成员方法,构造方法
- 抽象类不能实例化对象
- 如果一个类中包含抽象方法,那么这个类一定是抽象类(被
abstract
修饰) - 抽象类就是用来被继承的,继承后,子类一定要重写抽象类中的所有抽象方法,不然会有红线
在子类B中要重写抽象类A中的所有抽象方法,代码示例如下:@Overridepublic void func1() {}
- 如果不想重写抽象类的抽象方法,那么可以同样用
abstract
修饰子类;但是“出来混迟早是要还的”,如果再有一个类继承了这个类(已经继承了抽象类的类),那么必须重写这两个类中所有的抽象方法。
这里虽然B类使用abstract
关键字修饰来避免了重写父类A中的抽象方法,但是C类继承了B类,C类还是要把A类和B类中的所有抽象方法全部重写
代码示例如下:class C extends B {@Overridepublic void func1() {}@Overridepublic void func2() {} }
- 抽象方法既然要被重写,就要遵循重写的规则,抽象方法不能被final、private、static修饰
- 被private关键字修饰后,说明这个变量或方法只能在当前类中被访问。有关
private
关键字的更多介绍,请移步 封装.访问限定符 - 被static关键字修饰后,说明这个变量或方法是当前类的变量或方法,不属于某一个具体的对象,是所有对象共享的;随类的加载而创建,随类的销毁而卸载;而且只随类的创建加载一次,再次加载类或者new一个对象时,不会再次加载。有关
static
关键字的更多介绍,请移步 封装.static
接口
接口的概念
接口类似一种功能,只要某个事物具备这样的功能,都可以实现这个接口。(公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。)
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
接口实例
实例一、
定义IShpe接口,Rect、Triangle、Cycle三个类实现IShape接口
public interface IShape {void draw();//默认被public abstract修饰,建议不加,代码更简洁
}
public class Rect implements IShape{@Overridepublic void draw() {System.out.println("画一个矩形...");}
}
public class Triangle implements IShape{@Overridepublic void draw() {System.out.println("画一个三角形...");}
}
public class Cycle implements IShape{@Overridepublic void draw() {System.out.println("画一个圆...");}
}
public class Test {public static void drawShape(IShape shape) {shape.draw();}public static void main(String[] args) {IShape rect = new Rect();IShape triangle = new Triangle();IShape cycle = new Cycle();drawShape(rect);drawShape(triangle);drawShape(cycle);}
}
运行结果:
画一个矩形…
画一个三角形…
画一个圆…
结果表明,向上转型也可以体现在类实现接口的关系上(同时也体现出了动态绑定和多态)
实例二、
实现笔记本电脑使用USB鼠标、USB键盘
- USB接口:包含打开设备、关闭设备功能
public interface IUsb {void openDevice();void closeDevice();
}
- 鼠标类:实现USB接口,并具备点击功能
public class Mouse implements IUsb{@Overridepublic void openDevice() {System.out.println("连接鼠标...");}@Overridepublic void closeDevice() {System.out.println("关闭鼠标...");}public void click() {System.out.println("点击鼠标...");}
}
- 键盘类:实现USB接口,并具备输入功能
public class KeyBoard implements IUsb{@Overridepublic void openDevice() {System.out.println("连接键盘...");}@Overridepublic void closeDevice() {System.out.println("关闭键盘...");}public void input() {System.out.println("输入数据...");}
}
- 笔记本类:包含开机功能、关机功能、使用USB设备功能
public class Computer {public void openPower() {System.out.println("打开电脑...");}public void closePower() {System.out.println("关闭电脑...");}public void IUsbService(IUsb 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 Test {public static void main(String[] args) {Computer computer = new Computer();computer.openPower();//使用鼠标computer.IUsbService(new Mouse());//使用键盘computer.IUsbService(new KeyBoard());computer.closePower();}
}
运行结果:
打开电脑…
连接鼠标…
点击鼠标…
关闭鼠标…
连接键盘…
输入数据…
关闭键盘…
关闭电脑…
实例三、
一个类实现多个接口,定义Animal、Dog、Bird、Duck类,定义IRunable、ISwimable、IFlyable接口。让这些动物类实现合适的一个或多个接口
接口定义:
public interface IRunning {void run();
}public interface ISwimming {void swim();
}public interface IFlying {void fly();
}
各种类的定义
public class Animal {String name;int age;public Animal(String name, int age) {this.name = name;this.age = age;}public void eat() {System.out.println("正在吃...");}
}
下面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
public class Dog extends Animal implements IRunning,ISwimming{public Dog(String name,int age) {super(name,age);}@Overridepublic void run() {System.out.println(this.name + " 正在跑...");}@Overridepublic void swim() {System.out.println(this.name + " 正在游...");}
}
public class Bird extends Animal implements IFlying{public Bird(String name,int age) {super(name,age);}@Overridepublic void fly() {System.out.println(this.name + " 正在飞...");}
}
public class Duck extends Animal implements IRunning,ISwimming,IFlying{public Duck(String name,int age) {super(name,age);}@Overridepublic void run() {System.out.println(this.name + " 正在跑...");}@Overridepublic void swim() {System.out.println(this.name + " 正在游...");}@Overridepublic void fly() {System.out.println(this.name + " 正在飞...");}
}
测试类
public class Test {public static void main(String[] args) {Dog dog = new Dog("旺财",3);Bird bird = new Bird("小飞飞",1);Duck duck = new Duck("唐老鸭",5);walk(dog);walk(duck);fly(bird);fly(duck);}public static void walk(IRunning iRunning) {iRunning.run();}public static void fly(IFlying iFlying) {iFlying.fly();}
}
运行结果:
旺财 正在跑…
唐老鸭 正在跑…
小飞飞 正在飞…
唐老鸭 正在飞…
run方法和fly方法,使用接口这种引用类型作为参数,可以不用关注类的调用者的具体类型,只要这个类具备这个功能特性(实现了这个接口),就可以使用
接口间的多继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的.比如接口C 继承接口A和接口B,那C就具A和B的功能特性,当一个类实现这个接口C时,就要重写A、B、C接口中所有的方法
interface A {void testA();
}
interface B {void testB();
}
interface C extends A,B{void testC();
}
public class Test implements C{@Overridepublic void testA() {}@Overridepublic void testB() {}@Overridepublic void testC() {}
}
接口要点总结
- 接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口
- 创建接口时, 接口的命名一般以大写字母 I 开头.
- 接口的命名一般使用 “形容词” 词性的单词.
- 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性
- 接口中不能定义构造方法,也不能定义有具体实现的方法(否则会有红线)
- 接口中如果要定义有具体实现的方法,要用static 或 default 关键字修饰
- 接口中可以定义变量,默认被public static final 修饰,为静态常量(如果用默认值以外的修饰符修饰,会有红线)
- 接口中可以定义没有具体实现的方法,默认被public abstract修饰,为抽象方法(如果用默认值以外的修饰符修饰,会有红线)
- 接口中变量 或 方法可以不加任何限定修饰符,它们都有自己的默认修饰符
- 接口和抽象类一样,不能直接实例化对象,接口是用来被类实现的
- 一个类实现接口,使用 implements关键字,代表类实现了接口
- 实现接口的类,要在类中重写接口中所有的(抽象)方法
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
- 一个类可以实现多个接口,中间用逗号隔开
- 接口与接口之间也可以存在继承关系,接口与接口之间可以多继承
Comparable接口
在Comparable接口源码中,有一个compareTo方法
现有一个学生类,需要对两个学生对象 按照年龄 进行比较。不能使用直接使用 >、<或== 来比较两个学生引用。
应该让Student类实现Comparable接口,然后在Student中 按照比较需求 重写这个接口中的compareTo方法。代码示例如下:
public class Student implements Comparable<Student>{String name;int age;public Student(String name,int age) {this.name = name;this.age = age;}@Overridepublic int compareTo(Student o) {if(this.age > o.age) {return 1;}else if(this.age == o.age) {return 0;}else {return -1;}}//可以把上面一堆代码是换成 return this.age-o.age
}
测试类:
public class Test {public static void main(String[] args) {Student student1 = new Student("zhangSan",18);Student student2 = new Student("liSi",17);int ret = student1.compareTo(student2);if(ret > 0) {System.out.println("student1 > student2");}else if(ret == 0) {System.out.println("student1 == student2");}else {System.out.println("student1 < student2");}}
}
运行结果:
student1 > student2
使用Arrays.sort() 对学生类数组students进行排序,下图是Arrays.sort() 的源码,可以看出Arrays.sort() 的底层也是使用了comparable接口,所以Student类必须要实现comparable接口,并根据比较需求重写compareTo方法。
【students数组中的每个元素都是学生对象,比较时也是通过冒泡排序的方式,源码中要对学生对象强转为comparable接口,并且使用compareTo方法,所以Student类必须实现comparable接口】
Student 类实现comparable接口,重写compareTo方法,使用Arrays.sort(students)对学生数组按照年龄排序,代码示例如下:
public class Student implements Comparable<Student>{String name;int age;public Student(String name,int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic int compareTo(Student o) {return this.age - o.age;}
}
public class Test {public static void main(String[] args) {Student student1 = new Student("zhangSan",18);Student student2 = new Student("liSi",17);Student student3 = new Student("aBao",13);Student[] students = new Student[3];students[0] = student1;students[1] = student2;students[2] = student3;Arrays.sort(students);System.out.println(Arrays.toString(students));}
}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
Student 类实现comparable接口,重写compareTo方法,使用Arrays.sort(students)对学生数组按照姓名排序,代码示例如下:
public class Student implements Comparable<Student>{String name;int age;public Student(String name,int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic int compareTo(Student o) {return this.name.compareTo(o.name);}
}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
只需改动compareTo方法的比较方式,测试类Test不变
【点进String类的源码,其实String类中也实现了Comparable接口,所以String类也重写了compareTo方法,因此对于String这种类的引用是可以调用compareTo方法,来比较两个引用类型的大小的】
使用冒号排序BubbleSort()实现 Arrays.sort(),按照姓名进行比较排序,代码示例如下:
public class Student implements Comparable<Student>{String name;int age;public Student(String name,int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}@Overridepublic int compareTo(Student o) {return this.name.compareTo(o.name);}
}
public class Test {public static void main(String[] args) {Student student1 = new Student("zhangSan",18);Student student2 = new Student("liSi",17);Student student3 = new Student("aBao",13);Student[] students = new Student[3];students[0] = student1;students[1] = student2;students[2] = student3;bubbleSort(students);System.out.println(Arrays.toString(students));public static void bubbleSort(Student[] students) {for (int i = 0; i < students.length-1; i++) {for (int j = 0; j < students.length-1-i; j++) {if(students[j].compareTo(students[j+1]) > 0) {Student tmp = students[j];students[j] = students[j+1];students[j+1] = tmp;}}}}
}
运行结果:
[Student{name=‘aBao’, age=13}, Student{name=‘liSi’, age=17}, Student{name=‘zhangSan’, age=18}]
students[j].compareTo(students[j+1]
能够使用compareTo()比较的原因是,数组的每个元素都是Student类对象,而学生类实现了comparable接口,同时重写了compareTo方法
使用冒号排序BubbleSort()实现 Arrays.sort(),按照年龄从小到大进行比较排序,只需更改compareTo方法里面的比较方式。代码示例如下:
@Overridepublic int compareTo(Student o) {return this.age - o.age;}
如果想要按照年龄从大到小排序,只需将
return this.age - o.age
改为return o.age - this.age
Comparator接口
Comparable接口的缺点:如果一个类已经实现了Comparable接口,并且根据需要已经重写了compareTo抽象方法,那么一般这个已经改写的抽象方法就不会再变了,比如说之前的业务都是使用年龄age来比较,但是如果确实需要通过姓名比较,也不能把原来已经重写好的方法再次改写按照姓名比较,这样一来以前使用年龄age比较的所有业务都会出错。那么我们可以使用一个新的接口Comparator接口,可以设置两种不同比较方式的类,让两个类都实现Comparator接口,分别按照比较需求重写Comparator中的抽象方法。
以下是comparator接口的源码,可以看到有两个抽象方法
这个接口里面有两个抽象方法,但是一个类实现了这个接口后只重写compare()方法就可以了,也不会报错。这是为啥子呢?解释:我们知道Object类是所有类的父类,其实在Object类中就有equals()方法的具体实现,而作为子类的AgeComparator也实现了Comparator接口,可以认为子类已经从父类继承了equals方法,近似认为已经重写了equals()方法,所以不需要再次重写equals()方法。
使用AgeComparator类中重写的compare()方法按照年龄比较,代码示例如下:
public class AgeComparator implements Comparator<Student> {@Overridepublic int compare(Student o1, Student o2) {return o1.age - o2.age;}
}
public class Test {public static void main(String[] args) {Student student1 = new Student("zhangSan",18);Student student2 = new Student("liSi",17);AgeComparator ageComparator = new AgeComparator();int ret = ageComparator.compare(student1,student2);if(ret > 0) {System.out.println("student1 > student2");}else if(ret == 0) {System.out.println("student1 == student2");}else {System.out.println("student1 < student2");}}
}
运行结果:
student1 > student2
使用NameComparator类中重写的compare()方法按照姓名比较,代码示例如下:
public class NameComparator implements Comparator<Student> {@Overridepublic 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",18);Student student2 = new Student("liSi",17);NameComparator nameComparator = new NameComparator();int ret = nameComparator.compare(student1,student2);if(ret > 0) {System.out.println("student1 > student2");}else if(ret == 0) {System.out.println("student1 == student2");}else {System.out.println("student1 < student2");}}
运行结果:
student1 > student2
Clonable接口
现通过一个实例总结使用clone()的注意事项;有一个Goods类,代码示例如下:
class Money {public double money = 9.9;
}
public class Goods {public String name;public Money money;public Goods(String name) {this.name = name;this.money = new Money();}
}
现需要创建一个Goods对象,并克隆这个对象。在Test类中,创建好了一个goods对象,当用这个对象的引用通过 点 访问clone()方法时,但实际 点 不出来clone()方法,此时可以在子类Goods中重写clone()方法,这时候就能 点出来了。这是因为Object类中已经实现了clone()方法,所以作为子类的Goods类,重写这个方法后一定可以被访问。
但是我们发现还是会有红线,此时阅读Cloneable的源代码可以发现:
1 . 返回值是Object,所以要把Object类型的赋值给左边Goods类型的,必须要对右边进行强转为(Goods)。
public static void main(String[] args) {Goods goods = new Goods("苹果");Goods goods1 = (Goods)goods.clone();}
- 强转之后发现还会有红线,这是因为
throw CloneNotSuppurtedException
是一个不支持克隆的异常,需要在编译时解决异常,解决异常的方法是:main()方法后 加throw CloneNotSuppurtedException
。
public static void main(String[] args) throws CloneNotSupportedException {Goods goods = new Goods("苹果");Goods goods1 = (Goods)goods.clone();}
- 解决当前类是否能被克隆,编译通过但运行时报错,是因为没有实现Cloneable接口,实现Cloneable接口后才能真正实现拷贝,
阅读Cloneable接口的源码发现,其内部没有任何方法,
那么实现这个接口有什么用呢?它只是用来标记当前类是可以被拷贝的
完整代码示例如下:
class Money {public double money = 9.9;
}
public class Goods implements Cloneable{public String name;public Money money;public Goods(String name) {this.name = name;this.money = new Money();}@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
public class Test {public static void main(String[] args) throws CloneNotSupportedException {Goods goods = new Goods("苹果");Goods goods1 = (Goods)goods.clone();}
}
public class Test {public static void main(String[] args) throws CloneNotSupportedException {Goods goods = new Goods("苹果");Goods goods1 = (Goods)goods.clone();System.out.println("改变前goods money = " +goods.money.money );System.out.println("改变前goods1 money = " +goods1.money.money );goods1.money.money = 66.6;System.out.println("改变后goods money = " +goods.money.money );System.out.println("改变后goods1 money = " +goods1.money.money );}
}
运行结果:
改变前goods money = 9.9
改变前goods1 money = 9.9
改变后goods money = 66.6
改变后goods1 money = 66.6
通过以上测试类的结果说明,Money这个类的对象并没有被真正拷贝,新拷贝的goods1中的money这个引用还是指向了原来的地址,这是浅拷贝
使用堆栈图更能清晰的表示:
此时的goods1 和 goods 中的money引用所指向的对象是同一个对象(引用所存放的地址相同),所以在通过goods.money.money = 66.6
这条语句改变money的值时,goods中的money也会改变,money引用指向的地址都是同一个地址
深拷贝的代码实现
以下是实现深拷贝的具体代码实现:
class Money implements Cloneable{public double money = 9.9;@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
public class Goods implements Cloneable{public String name;public Money money;public Goods(String name) {this.name = name;this.money = new Money();}@Overrideprotected Object clone() throws CloneNotSupportedException {Goods tmp = (Goods)super.clone();tmp.money = (Money)this.money.clone();return tmp;}}
public class Test {public static void main(String[] args)throws CloneNotSupportedException {Goods goods = new Goods("苹果");Goods goods1 = (Goods)goods.clone();System.out.println("改变前goods money = " +goods.money.money );System.out.println("改变前goods1 money = " +goods1.money.money );goods1.money.money = 66.6;System.out.println("改变后goods money = " +goods.money.money );System.out.println("改变后goods1 money = " +goods1.money.money );}
}
运行结果:
改变前goods money = 9.9
改变前goods1 money = 9.9
改变后goods money = 9.9
改变后goods1 money = 66.6
操作的堆栈图:
先拷贝一份goods 给 tmp,然后再对money这个引用的对象进行拷贝,把拷贝好的money对象的地址 给 tmp.money。这里money引用也进行拷贝,所以Money也要实现Cloneable接口,重写clone()方法。
注意:从代码层次上,浅拷贝或深拷贝 是由具体的代码实现来决定的,不能说某个clone()方法是浅拷贝或深拷贝
抽象类和接口的区别
- 抽象类中可以有普通成员变量和普通成员方法,构造方法,也可以有抽象方法;接口中可以定义变量,不能定义构造方法 和 有具体实现的方法(如果想有具体实现,使用static或default修饰),接口中的方法默认是被public abstract 修饰(抽象方法),变量默认是被 public static final 修饰(静态常量)
- 抽象类使用 abstract 关键字;接口使用 interface 关键字
- 抽象类 用来被继承,,关键字 extends;接口用来被实现,关键字 implements
- 类与类之间不支持多继承;接口可以实现多继承,一个类可以实现多个接口,接口与接口之间也可以继承,甚至继承多个接口