阿里云【名师课堂】Java面向对象开发65 ~ 67:抽象类的定义和使用
注意:在以后的开发过程中,绝大多数情况下,千万不要直接去继承一个已经定义好的类,而只能继承抽象类和接口。
经过 《阿里云【名师课堂】Java面向对象开发64:多态性》的学习我们知道,对象多态性的本质在于方法的覆写。但是如果现在子类没有去进行指定方法的覆写,这样的操作(继承、多态)就有些不合要求。所以如果要对子类方法进行一些强制的要求就必须采用抽象类来解决。
65:抽象类基本概念
抽象类就是在不同类的方法上扩充了一些抽象方法。
- 抽象方法指的是只允许声明,不允许实现(没有方法体),所有的抽象方法都要求采用
abstract
关键字来修饰,并且抽象方法所在的类也一定要使用abstract
关键字来定义。 - 范例:定义一个抽象类
abstract class A {
private String msg = "colder thy kiss" ; // 属性
public void print() { // 普通方法
System.out.println(msg) ;
}
public abstract void method() ; // 声明一个抽象方法,注意观察:没有{}
// {}为方法体,所有的抽象方法都不包含方法体,只是声明一个方法
}
public class TestDemo {
public static void main(String args[]) {
A a = new A() ;
}
}
为什么?
- 抽象类中包含有抽象方法,而抽象方法与普通方法的最大区别:没有方法体,即没有具体实现。
- 而现在产生了实例化对象,则意味着可以调用类中的所有操作。这显然违背抽象方法的性质。
所以,抽象类的使用原则:
- 所有抽象类都必须要有子类;
- 抽象类的子类(子类不是抽象类)必须覆写抽象类中的全部抽象方法;
- 方法覆写一定要考虑到权限问题,抽象方法可以使用任意权限,但是要求权限尽量都用
public
;- 理论上任意权限,但是实际上权限不能是private,因为private是私有的方法或属性,不能被继承,而abstract要求必须被继承。
- 方法覆写一定要考虑到权限问题,抽象方法可以使用任意权限,但是要求权限尽量都用
- 抽象类的对象可以通过对象多态性,利用子类为其实例化。
范例:使用抽象类
- 首先只定义一个抽象类的子类,不进行抽象方法的覆写
abstract class A {
······
}
// 一个子类只能够利用extends来继承抽象类,所以依然有单继承局限
class B extends A { // 定义抽象类的子类
}
public class TestDemo {
public static void main(String args[]) {
}
}
- 完整的使用抽象类
abstract class A {
······
}
// 一个子类只能够利用extends来继承抽象类,所以依然有单继承局限
class B extends A { // 定义抽象类的子类
public void method() { // 覆写全部抽象方法
System.out.println("Hello World") ;
}
}
public class TestDemo {
public static void main(String args[]) {
A a = new B() ; // 实例化子类对象
a.method() ; // 调用被子类覆写过的抽象方法
}
}
下面讲解一种特殊使用方式:抽象类中的内部类
abstract class A {
private String msg = "colder thy kiss" ; // 属性
public void print() { // 普通方法
System.out.println(msg) ;
}
public static A getInstance() { // 定义一个方法:取得A类对象
class B extends A { // 定义抽象类的子类(内部类)
public void method() {
System.out.println("Hello World") ;
}
}
return new B() ;
}
public abstract void method() ; // 声明一个抽象方法,注意观察:没有{}
// {}为方法体,所有的抽象方法都不包含方法体,只是声明一个方法
}
public class TestDemo {
public static void main(String args[]) {
A a = A.getInstance() ; // 通过抽象类中的内部类取得实例化对象
a.method() ; // 调用被子类覆写过的抽象方法
}
}
这种用法属于非正常模式,但是对于程序的封装性有好处。现在暂时还不会用到,推荐正常格式:抽象类➡子类➡覆写所有抽象方法。
66:抽象类使用限制(相关规定)
1、抽象类中的构造
抽象类只是比普通类多了一些抽象方法的定义,所以在抽象类中依然允许提供构造方法,并且子类也会遵守子类对象的实例化流程:实例化子类对象前一定会先调用父类的构造方法。
1.1、父类中有无参构造时:
abstract class Person {
private String name ;
private int age ;
public Person() {
System.out.println("【父类】***************") ;
}
public abstract String getInfo() ; // 声明一个抽象方法,注意观察:没有{}
}
class Student extends Person {
private String school ;
public Student() {
System.out.println("【子类】###############") ;
}
public String getInfo() {
return null ;
}
}
public class TestDemo {
public static void main(String args[]) {
new Student() ; // 子类实例化时先调用父类构造在调用本类构造
}
}
1.2、父类中没有无参构造时:
abstract class Person {
private String name ;
private int age ;
public Person(String name, int age) {
this.name = name ;
this.age = age ;
System.out.println("【父类】***************") ;
}
public String getName() {
return this.name ;
}
public int getAge() {
return this.age ;
}
public abstract String getInfo() ; // 声明一个抽象方法,注意观察:没有{}
}
class Student extends Person {
private String school ;
public Student(String name, int age, String school) {
super(name, age) ; // super()明确调用父类的属性
this.school = school ;
System.out.println("【子类】###############") ;
}
public String getInfo() {
return "【Student】name = " + super.getName() + ",age = " + super.getAge() + ",school = " + this.school ;
}
}
public class TestDemo {
public static void main(String args[]) {
Person per = new Student("Dexter",20,"hfut") ;
System.out.println(per.getInfo()) ;
}
}
1.3、理解
- 抽象类中毕竟也是有属性的,所有的属性都要在对象实例化的时候进行空间的开辟,才可以进行后续的赋值之类操作,则对象的实例化必须使用构造方法。
拓展:对象实例化操作的核心步骤
请结合《阿里云【名师课堂】Java面向对象开发3 ~ 6:类与对象》
- 进行类的加载;
- 进行类对象的空间开辟;
- 对类对象中的属性初始化(调用构造方法);
范例:观察对象初始化操作
abstract class A {
public A() { // 3、调用父类构造
this.print() ; // 4、调用被子类覆写过的方法
}
public abstract void print() ;
}
class B extends A {
private int num = 100 ; // 实际上super之后子类构造没有执行,就不会给num初始化,这条语句只是开辟空间
public B(int num) { // 2、调用子类构造实例化对象
super() ; // 3、这时一条隐含语句,实际目的:调用父类构造
this.num = num ; // 7、这条传入数据赋值来给num赋值在第6步结束之后才执行
}
public void print() { // 5、子类覆写的方法,而此时对象属性(num)还没有被初始化
System.out.println(this.num) ; // 6、输出,这时num还是其数据类型的默认值
}
}
public class TestDemo {
public static void main(String args[]) {
B b = new B(200) ; // 1、实例化子类对象
b.print() ; // 8、这时num值初始化为传入的200
}
}
结论:如果构造方法没有执行,那么对象中的属性一定都是其对应数据类型的默认值
2、抽象类中可不可以没有构造方法?
抽象类中允许不定义任何的抽象方法,但是此时抽象类对象依然无法进行直接实例化处理。
abstract class A {
public void print() {}
}
public class TestDemo {
public static void main(String args[]) {
A a = new A() ;
}
}
说白了,允许是允许,但是这么做肯定有问题,所以不要这么做。
3、抽象类不能使用final进行声明
- 使用final定义的类不能有子类,而抽象类必须有非抽象子类来实现功能。
- 抽象方法权限不能是private,因为private是私有的方法或属性,不能被继承,而abstract要求必须被继承。
4、抽象类能不能被static定义?
抽象类也分内部抽象类和外部抽象类,内部抽象类中可以使用static进行定义,描述为外部抽象类。
范例:观察内部抽象类
abstract class A {
public abstract void printA() ;
abstract class B { // 这种内部抽象类出现概率较小
public abstract void printB() ;
}
}
class X extends A {
public void printA() {}
class Y { // 相当于class Y extends B
public void printB() {}
}
}
public class TestDemo {
public static void main(String args[]) {
}
}
那么现在,如果外部抽象类上使用了static
,是语法错误;而如果内部抽象类上使用static
,描述为外部抽象类。
abstract class A {
public abstract void printA() ;
static abstract class B { // 这种内部抽象类出现概率较小
public abstract void printB() ;
}
}
class X extends A.B {
public void printB() {} // 这时只需要覆写B
}
67:模版设计模式(抽象类的实际应用)
抽象类的最大特点:强制规定了子类的实现结构。然而除了这一特点,抽象类还可以起到模板的作用,下面做一个分析:
- 人 = 吃饭 + 睡觉 + 工作 ;
- 🐖 = 吃饭 + 睡觉 ;
- 🤖 = 吃饭 + 工作 ;
现在假设有一个控制按钮,一旦向其下达了某些指令之后就可以进行相应的处理。比如:传入了工作指令,人和🤖就开始工作。
首先,定义抽象的行为
abstract class Action { // 描述的是一个对象的行为
// 传入的指令,对应行为
public static final int EAT = 1 ; // 注意:常量不要出现二者之和是另一个指令的情况
public static final int SLEEP = 5 ;
public static final int WORK = 10 ;
public void command(int cmd) { // 通过指令控制行为,就是题目中定义的按钮
switch (cmd) {
case EAT :
this.eat() ;
break ;
case SLEEP :
this.sleep() ;
break ;
case WORK :
this.work() ;
break ;
case EAT + WORK:
this.eat() ;
this.work() ;
break ;
}
}
// 不确定具体的实现(人、🐖、🤖有别),但是行为先定义好
public abstract void eat() ;
public abstract void sleep() ;
public abstract void work() ;
}
其次,定义各个行为的子类
class Human extends Action {
public void eat() {
System.out.println("蒸羊羔、蒸熊掌、蒸鹿尾儿、烧花鸭、烧雏鸡、烧子鹅、卤猪、卤鸭、酱鸡、腊肉、松花小肚儿、晾肉、香肠") ;
}
public void sleep() {
System.out.println("晚安") ;
}
public void work() {
System.out.println("996是福报") ;
}
}
class Pig extends Action {
public void eat() {
System.out.println("吃饲料") ;
}
public void sleep() {
System.out.println("吃完就睡多多长肉") ;
}
public void work() {} // 虽然🐖的功能中没有工作,但是抽象类的抽象方法必须被覆写
}
class Android extends Action {
public void eat() {
System.out.println("充电") ;
}
public void sleep() {} // 虽然🤖的功能中没有睡觉,但是抽象类的抽象方法必须被覆写
public void work() {
System.out.println("724全年无休") ;
}
}
最后,调用各自的行为操作
public class TestDemo {
public static void main(String args[]) {
method(new Human()) ;
method(new Pig()) ;
method(new Android()) ;
}
public static void method(Action action) {
action.command(Action.EAT + Action.SLEEP + Action.WORK) ; // 按要求传入参数,比如需要吃饭就传Action.EAT
}
}
总结
通过这个结构是为了体现:
- 抽象类在实际的使用过程中会定义一些固化的使用模式,它只能接收几种特定的指令(如本程序中的
command
); - 但是每种指令的具体实现由子类去完成,父类只做了方法的约定;
注意:
- 抽象类依然有单继承局限;
- 抽象类的使用必须要通过子类进行对象实例化。
在后面学习Servlet时会对抽象类有更深的理解。