面向对象(二)
一、继承
1.继承概述
将多个类的共性抽取出来,形成一个父类。让所有具备这些共性属性的类,继承这个父类,成为其子类。这样做的好处在于:
1)这样可以避免重复代码,提高代码复用性;
2)继承是面向对象另一特点多态的基础之一,有了继承,才有多态;
3)同时可以使子类的功能更加强大灵活,便于扩展
注意:
1)不要仅为了获取其他类的功能,而使用继承;必须在类与类之间有所属关系的情况下,才使用继承;2)Java中只支持单继承。因为多继承容易带来安全隐患。如果支持多继承,多个父类当中有相同名字的不同功能的方法,子类在调用同名方法的时候,虚拟机不能确定要运行哪个;
3)Java中保留了多继承的机制,用多实现的方式来体现(implements);
4)Java支持多层继承,C继承B,B继承A,以此类推,形成一个继承体系。继承体系相对复杂,想要良好运用该体系,先查阅体系中父类的描述,通过了解父类中的共性功能,就能了解该继承体系的基本功能。在具体调用时,应创建最小子类的对象。由于:
1> 父类不能创建对象,如抽象类;
2> 子类对象有更多的功能,包括父类和自由的功能
2.继承的表现形式
类与类之间不止有A属于B这一种关系,还有聚集的关系。聚集关系分为:
1)聚合关系:A由B组成,B是A中的一个,但是缺少一个B,不影响A的整体功能;例如:球队由球员组成,一个球员是球队的一员;
2)组合关系:A由B组成,B是A的一部分,缺少B,A的功能将受到极大影响;例如:心脏是人的一部分
3.继承的应用
1)super关键字
1> super
关键字和this关键字的用法基本一致;
2> this
代表本类对象的引用,super
代表父类对象的引用
2)这一小节探讨继承关系出现后,类中成员的变化
1> 变量的变化
示例代码:
class Parent {
int num = 4;
}
class Sub extends Parent {
int num = 5;
void show() {
System.out.println(super.num);
}
}
class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.show();
}
}
继承状态下,成员变量内存加载图示:
继承状态下,成员变量的变化如下:
如果子类和父类中出现非私有同名变量,子类要访问本类中的同名变量,用this
关键字;子类要访问父类中的同名变量,用super
关键字。
小扩展:
继承的时候,子类继承父类的成员变量,同时继承该成员变量的值。示例代码:
class Parent {
private int num = 4;
}
class Sub extends Parent {
void show() {
System.out.println(num);
System.out.println(this.num);
}
}
class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.show();
}
}
2> 方法的变化
示例代码:
class Parent {
void show() {
System.out.println("Parent show");
}
}
class Sub extends Parent {
void show() {
System.out.println("Sub show");
}
}
class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.show();
}
}
继承状态下,成员方法的变化如下:
当子类出现和父类一模一样的方法时,子类对象调用同名方法,会运行子类方法的内容。由于子类继承了父类的方法,但是子类在具体实现的时候需要有不同的功能,这个时候,只需要用到方法的另一个特性:复写。在子类中定义一个和父类完全相同的方法,在方法体中实现子类的特有功能即可。如果要在子类的复写方法中,调用父类的方法,只需要加上super.方法名
即可。
小扩展:
super关键字的使用示例代码:
class Parent {
void show() {
System.out.println("Parent show");
}
}
class Sub extends Parent {
void show() {
super.show();
System.out.println("Sub show");
}
}
子类对象调用show()
方法时,上述代码既输出"Parent show"
,也输出"Sub show"
。
复写注意事项:
a. 复写成功必须保证子类方法权限大于等于父类方法权限;
b. 静态方法只能复写静态方法;
c. 复写和重载的区别在与,复写要求子类父类方法一模一样,重载要求同名函数的参数列表不一样
3> 构造方法的变化
示例代码:
class Parent {
Parent() {
System.out.println("Parent construct...");
}
}
class Sub extends Parent {
Sub() {
//super();
System.out.println("Sub construct...");
}
Sub(int x) {
//super();
System.out.println("4");
}
}
class Test {
public static void main(String[] args) {
Sub s = new Sub();
}
}
子类对象进行初始化时,父类的构造方法也会执行。因为子类的构造方法第一行默认写有隐式语句super()
;super()
会访问父类中空参数的构造方法。super();
存在于所有子类的所有构造方法的第一行。如果访问父类中指定的构造方法,只需通过手动定义super
语句。
子类一定要访问父类构造方法的原因:
由于子类继承了父类的成员,子类对象在被创建时,应该先知道父类是如何对其成员进行初始化的,以避免代码功能的重复。
注意:
构造函数中,this和super只能存在一个,因为两者都必须写在构造方法第一行。
二、final关键字
1.final的定义及特点
final作为一个修饰符:
1)可以修饰类,变量以及方法;
2)被final
修饰的类不能被继承;
3)被final
修饰的方法不能被复写;
4)被final
修饰的变量是常量,值不能被改变;既可以修饰成员变量,也可以修饰局部变量;
5)内部类定义在类中的局部位置上时,只能访问该局部被final修饰的局部变量
示例代码:
//TestFinal被final关键字修饰,不能被继承
final class TestFinal {
//PI被final修饰,为常量,值不能修改
private final int PI = 3.14;
}
class TestFinal {
//method1被final关键字修饰,不能被复写
final void method1() {
}
void method2() {
}
}
三、抽象类
1.抽象的定义
抽象就是将多个事物的共性的、本质的内容进行抽取和概括。例如:老虎,猫,狮子都是猫科动物,猫科就是一个抽象的概念。
2.抽象类
抽象类是包含抽象方法的类。抽象方法只有方法的定义,没有方法体。抽象方法的具体实现,有子类复写完成。抽象方法只负责抽象出不同对象的共性,但是不涉及具体实现的细节。例如:猫和狗都会叫,但是叫的内容不同。抽象方法定义一个叫的功能,具体叫的内容则有子类对象来实现。
3.抽象类的特点
1)抽象方法一定定义在抽象类中;
2)抽象方法和抽象类都被abstract
关键字修饰;
3)抽象类不可以实例化;
4)抽象类中的抽象方法要被使用,必须由子类复写其所有抽象方法,由子类对象进行调用;如果子类中只覆盖了部分抽象方法,那么该子类还是一个抽象类;
5)抽象类中可以不定义抽象方法,仅为了让该类不被实例化
代码示例:
abstract class Employee {
abstract void work();
}
class Programmer extends Employee {
void work() {
System.out.println("Program...");
}
}
class Janitor extends Employee {
void work() {
System.out.println("Clean...");
}
}
4.抽象类练习
1)练习1:
开发一个系统需要对员工进行建模,员工包含3个属性:姓名,工号以及工资;经理也是员工,除了含有员工的属性外,另外还有一个奖金属性。请使用继承的思想设计出员工类和经理类。
示例代码:
package test;
public abstract class Employee {
/**
* 声明员工的三个属性
*/
private String name;
private String id;
private double pay;
/**
* 声明员工的构造方法,对三个属性进行初始化
* @param name 员工姓名
* @param id 员工工号
* @param pay 员工工资
*/
Employee(String name, String id, double pay) {
this.name = name;
this.id = id;
this.pay = pay;
}
/**
* 声明员工的抽象方法;由于每个员工都需要工作,而具体的工作内容不明确,所以声明为抽象方法
*/
public abstract void work();
}
class Programmer extends Employee {
/**
* 声明程序员的构造方法,调用父类的构造方法对程序员属性进行初始化
* @param name 程序员姓名
* @param id 程序员工号
* @param pay 程序员工资
*/
Programmer(String name, String id, double pay) {
super(name, id, pay);
}
/**
* 复写父类的work方法,具体实现程序员的工作
*/
public void work() {
System.out.println("Programming...");
}
}
class Manager extends Employee {
/**
* 声明经理独有的属性
*/
private int bonus;
/**
* 声明经理的构造方法,调用父类的构造方法对经理属性进行初始化
* @param name 经理姓名
* @param id 经理工号
* @param pay 经理工资
* @param bonus 经理奖金
*/
Manager(String name, String id, double pay, int bonus) {
super(name, id, pay);
this.bonus = bonus;
}
/**
* 复写父类的work方法,具体实现经理的工作
*/
public void work() {
System.out.println("Managing...");
}
}
2)练习2:模版方法模式
使用System
类中的currentTimeMillis()
方法获取一段程序运行的时间。
示例代码:
package test;
class GetTime {
public static void getTime() {
/**
* 获取程序开始时间
*/
long start = System.currentTimeMillis();
/**
* 程序执行
*/
for(int x = 0; x < 1000; x ++) {
System.out.print(x);
}
/**
* 获取程序结束时间
*/
long end = System.currentTimeMillis();
/**
* 打印程序运行时间
*/
System.out.println("Time elapse: " + (end = start));
}
}
class UpTime {
public static void main(String[] args) {
GetTime.getTime();
}
}
如果GetTime
类要被其他程序使用,来获取程序运行时间,那么该类中所要运行的程序代码是不确定的。因此,首先将要运行的代码提取出来,封装在一个方法中。
示例代码:
package test;
class GetTime {
public static void getTime() {
/**
* 获取程序开始时间
*/
long start = System.currentTimeMillis();
/**
* 程序执行
*/
runCode();
/**
* 获取程序结束时间
*/
long end = System.currentTimeMillis();
/**
* 打印程序运行时间
*/
System.out.println("Time elapse: " + (end = start));
}
/**
* 封装要运行的代码块
*/
public void runCode() {
for(int x = 0; x < 1000; x ++) {
System.out.print(x);
}
}
}
class UpTime {
public static void main(String[] args) {
GetTime.getTime();
}
}
现在想运行另外一段代码,直接修改源代码的方式操作性极差。那么可以运用一个类,继承GetTime
类,然后以复写runCode()
方法的方式更改要运行的代码,并在主函数中创建子类的对象,调用getTime()
方法即可。
示例代码:
package test;
class GetTime {
public void getTime() {
/**
* 获取程序开始时间
*/
long start = System.currentTimeMillis();
/**
* 程序执行
*/
runCode();
/**
* 获取程序结束时间
*/
long end = System.currentTimeMillis();
/**
* 打印程序运行时间
*/
System.out.println("Time elapse: " + (end = start));
}
/**
* 封装要运行的代码块
*/
public void runCode() {
for(int x = 0; x < 1000; x ++) {
System.out.print(x);
}
}
}
/**
* 声明Sub类,继承GetTime类,复写runCode()方法
*/
class Sub extends GetTime {
public void runCode() {
for(int x = 0; x < 2000; x ++) {
System.out.print(x);
}
}
}
class UpTime {
public static void main(String[] args) {
Sub s = new Sub();
s.getTime();
}
}
进一步,在GetTime
类中定义一段代码,并没有意义。GetTime
类的作用是计算时间,至于要运行的代码,需要使用GetTime
类的程序来确定。同时,GetTime
类是用来计算时间的,计算时间的方法不需要其他程序来改变,否则该类将失去意义。因此,最好的方式是,将GetTime
类声明为抽象类,将runCode()
方法声明为抽象方法,将getTime()
方法声明为final
方法。子类继承GetTime
类,复写runCode()
抽象方法,写入要运行的代码,再用子类对象调用getTime()
方法计算运行时间。
示例代码:
package test;
abstract class GetTime {
public final void getTime() {
/**
* 获取程序开始时间
*/
long start = System.currentTimeMillis();
/**
* 程序执行
*/
runCode();
/**
* 获取程序结束时间
*/
long end = System.currentTimeMillis();
/**
* 打印程序运行时间
*/
System.out.println("Time elapse: " + (end = start));
}
/**
* 声明抽象方法
*/
public abstract void runCode();
}
/**
* 声明Sub类,继承GetTime类,复写runCode()方法
*/
class Sub extends GetTime {
public void runCode() {
for(int x = 0; x < 2000; x ++) {
System.out.print(x);
}
}
}
class UpTime {
public static void main(String[] args) {
Sub s = new Sub();
s.getTime();
}
}
这是模版方法设计模式。在定义功能时,功能的一部分是确定的,有一部分不确定;而确定的部分在使用不确定的部分。这时,就将不确定的部分暴露出去,由该类的子类来实现不确定的部分。
四、接口
1.定义
在Java中,用interface
关键字定义接口。格式为:
interface {
}
其他类可以通过implements
关键字实现接口,并复写其功能。接口可以实现"多继承"的机制,即一个类,可以实现多个接口。
2.接口的特点
1)接口中通常定义常量和抽象方法
2)接口中的成员有固定的修饰符
常量:public static final
方法:public abstract
示例代码:
interface Inter {
public static final int x = 1;
public abstract void run();
}
3)接口中的成员都是public权限
4)接口不能实例化
5)子类必须复写接口中的所有抽象方法才能实例化
6)一个类可以实现多个接口
示例代码:
interface A {
}
interface B {
}
class C implements A, B {
}
7)一个类可以在继承之后继续实现一个或多个接口
示例代码:
interface A {
}
interface B {
}
class C {
}
class D extends C implements A, B {
}
8)接口和接口之间可以继承
示例代码:
interface A {
}
interface B extends A {
}
9)接口之间可以多继承 原因是抽象方法没有方法体,具体方法,具体实现即可
示例代码:
interface A {
}
interface B {
}
interface C extends A, B {
}
3.接口的应用
在描述问题的时候,如果是类的基本功能,就可以提取到抽象类类当中,将功能定义为方法或抽象方法。如果是类可以具备,但是不是必要的扩展功能,可以定义在接口中,作为类的功能扩展。
示例代码:
package test;
public class TestInterface {
}
class ITWorker {
public static void program() {
System.out.println("Programming...");
}
public static void sleep() {
System.out.println("Sleeping...");
}
}
interface FixComputer {
abstract void fix();
}
class Programmer extends ITWorker implements FixComputer {
@Override
public void fix() {
System.out.println("Reinstall OS...");
}
}
示例代码中,编程和睡觉,可以作为IT男的基本功能被提取到ITWorker
类当中;而修电脑,则作为扩展功能,定义成接口,让有修电脑能力的程序员,实现这个接口即可,而没有修电脑功能的程序员,只用继承IT男的基本功能。