一、抽象类
语法规则
看代码,我们发现父类 Shape 中的 draw 方法好像并没有什么实际工作,主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的.
class Shape{
public void draw(){
//?
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("⚪");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("♣");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
像这种没有实际工作的方法,我们可以把它设计成抽象方法(abstract method),包含抽象方法的类我们称为抽象类(abstract class).
abstract class Shape{
abstract public void draw();
}
在 draw 方法前面加上 abstract 关键字,表示这是一个抽象方法.
同时抽象方法没有方法体(没有括号{ },不能执行具体代码).
对于包含抽象方法的类,必须加上 abstract 关键字表示这是一个抽象类.
注意事项
1)抽象类不能直接实例化.
Shape shape = new Shape();
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
2)抽象方法不能是 private 的.
abstract class Shape {
abstract private void draw();
}
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private
3)抽象类中可以包含其它的非抽象方法,也可以包含字段.
这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用.
abstract class Shape {
abstract public void draw();
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
// 执行结果
func
抽象类的作用
抽象类存在的最大意义,就是为了被继承.
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类.
然后让子类重写抽象类中的抽象方法.
使用抽象类,相当于多一重编译器的校验.
就如上面的代码,实际工作不应该由父类完成,而是由子类完成.
那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题.
很多语法存在的意义都是为了 “预防出错”,例如我们曾经使用的 final。
创建变量的用户不去修改,不相当于常量吗?但是加上 final 能够在不小心误修改的时候,让编译器及时提醒我们.
充分利用编译器的校验,在实际开发中是非常有意义的.
二、接口
接口是抽象类的更进一步.
抽象类中还可以包含非抽象方法,字段.
而接口中包含的方法都是抽象方法,字段只能包含静态常量.
语法规则
在刚才的打印图形的示例中,我们父类 Shape 并没有包含别的非抽象方法,也可以设计出一个接口.
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape ishape = new Cycle();
ishape.draw();
}
}
1. 使用 interface 定义一个接口.
2. 接口中的方法一定是抽象方法,因此可以省略 abstract.
3. 接口中的方法一定是 public,因此可以省略 public.
4. Cycle 类使用 implements 继承接口. 此时表达的含义不再是 “扩展”,而是 “实现”.
5. 在调用的时候,同时可以创建一个接口的引用,对应到一个子类的实例.
6. 接口不能单独被实例化.
扩展(extends)VS 实现(implements)
扩展:指的是当前已经有一定功能了,进一步扩充功能.
实现:指的是当前啥都没有,需要从头构造出来.
接口中只能包含抽象方法.
对于字段来说,接口中只能包含静态常量(final static).
其中的 public,static,final 的关键字都可以省略。省略后的 num 仍然表示 public 的静态常量。
interface IShape{
void draw();
public static final int num = 10;
}
接口命名
1. 我们创建接口的时候,接口的命名一般以大写字母 I 开头.
2. 接口的命名一般使用 “形容词” 词性的单词.
3. 阿里编码规范中约定,接口中的方法和属性不要加任何修改符号,保持代码的简洁性.
一个错误的代码
interface IShape {
abstract void draw() ; // 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println("□") ; //权限更加严格了,所以无法覆写。
}
}
接口完整格式和简化格式对比
//完整格式
interface IMessage{
public static final String MSG = "hello";
public abstract void print();
}
//简化格式
interface IMessage{
String MSG = "hello";
void print();
}
实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
现在我们通过类来表示一组动物。
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 Cat extends Animal implements IRunning{
public Cat(String name){
super(name);
}
@Override
public void run() {
System.out.println(this.name + "在跑");
}
}
鱼会游泳
class Fish extends Animal implements ISwimming{
public Fish(String name){
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "在游泳");
}
}
青蛙会跑会游泳
class Frog extends Animal implements IRunning,ISwimming{
public Frog(String name){
super(name);
}
@Override
public void run() {
System.out.println(this.name + "在跑");
}
@Override
public void swim() {
System.out.println(this.name + "在游泳");
}
}
还有鸭子,水陆空三栖
class Dubk extends Animal implements ISwimming, IRunning, IFlying{
public Dubk(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "在游泳");
}
@Override
public void run() {
System.out.println(this.name + "在跑步");
}
@Override
public void fly() {
System.out.println(this.name + "在飞");
}
}
上面代码展示了 Java 面向对象编程中的最常见的用法:一个类继承一个父类,同时实现多个接口。
猫是一种动物,具有跑的特性
青蛙也是一种动物,既能跑,也能游泳
鸭子也是一种动物,既能跑,也能游,还能飞
这样设计有什么好处?时刻牢记多态的好处,让程序员忘记类型。
有了接口之后,类的使用者不再关注具体类型,而只关注某个类是否具备某种能力。
例如,我们实现一个方法,叫“散步”,参数是 IRunning.
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);
// 执行结果
机器人正在用轮子跑
接口的使用实例
如果刚才的例子比较抽象,我们再来一个更实际的例子。
给对象数组排序
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组,对这个对象数组中的元素进行排序(按分数降序).
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
按照我们之前的理解,数组我们有一个现成的 sort 方法,能否直接使用这个方法呢?
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
仔细思考,不难发现。
和普通的整数不一样,两个整数是可以直接比较的,大小关系明确.
而两个学生对象的大小关系怎么确定? 需要我们额外指定.
让我们的 Student 类实现 Comparable 接口, 并实现其中的compareTo 方法
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort 方法中会自动调用 compareTo 方法.
compareTo 的参数是 Object,其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
- 如果当前对象应排在参数对象之前,返回小于 0 的数字;
- 如果当前对象应排在参数对象之后,返回大于 0 的数字;
- 如果当前对象和参数对象不分先后,返回 0;
再次执行程序,结果就符合预期了.
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项
对于 sort 方法来说,需要传入的数组的每个对象都是 “可比较” 的,需要具备 compareTo 这样的能力。通过重写 compareTo 方法的方式,就可以定义比较规则。
为了进一步加深对接口的理解,我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
再次执行代码.
sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
接口间的继承
接口可以继承一个接口,达到复用的效果. 使用 extends 关键字.
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类,就继续要实现 run 方法,也需要实现 swim 方法.
接口间的继承相当于把多个接口合并在一起.
Clonable 接口和深拷贝
Java 中内置了一些很有用的接口, Clonable 就是其中之一.
Object 类中存在一个 clone 方法,调用这个方法可以创建一个对象的 “拷贝”.
但是要想合法调用 clone 方法,必须要先实现 Clonable 接口,否则就会抛出CloneNotSupportedException 异常.
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
// 输出结果
// false
浅拷贝 VS 深拷贝
Cloneable 拷贝出的对象是一份 "浅拷贝".
观察以下代码
public class Test {
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
return (B)super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
B b = new B();
B b2 = b.clone();
b.a.num = 10;
System.out.println(b2.a.num);
}
}
//执行结果
10
通过 clone 拷贝出的 b 对象只是拷贝了 b 自身,而没有拷贝内部包含的 a 对象.
此时 b 和 b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边,另一边也会发生改变.
三、总结
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握.
同时又要认清两者的区别(重要!!! 常见面试题).
核心区别
抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写);
而接口中不能包含普通方法,子类必须重写所有的抽象方法。
像之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
再次提醒:
抽象类存在的意义是为了让编译器更好的校验,
像 Animal 这样的类我们并不会直接使用, 而是使用它的子类.
万一不小心创建了 Animal 的实例, 编译器会及时提醒我们.
抽象类和接口的区别
抽象类(abstract) | 接口(interface) |
---|---|
普通类+抽象方法 | 抽象方法+全局常量 |
各种权限 | public |
使用 extends 关键字继承抽象类 | 使用 implements 关键字实现接口 |
一个抽象类可以实现若干接口 | 接口不能继承抽象类,但是接口可以使用 extends 关键字继承多个父接口 |
一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |