继承是面向对象编程(OOP)的三个基本特征之一。继承是指可以先定义一个相对通用的类,然后从这个通用的类出发定义更多功能的类。新定义的类不仅继承了前面通用类的域和方法,而且还给这个新定义的类增加了一些新的实例域和方法。继承的概念就像儿子继承了父亲的特征和技能,而且拥有了比父亲更多的技能。
1.父类和子类
第7章中定义了MasterCard类。在使用过程中,学校一卡通之间的相同点虽然比较多,但是学生卡和老师卡的使用还是有区别的。例如,学生卡和老师卡之间都必须包含姓名域、发卡时间、性别、卡内余额等,但二者的不同之处在于学校可以给教师的一卡通消费打折。
因此可以重新设计MasterCard类,使其包含所有共同的数据域和方法。定义了新的TeacherMasterCard类 和 StudentMasterCard类。便于为每个类增加一些新的功能,也可以再次使用MasterCard类中已经编写好的部分代码,并将MasterCard类中所有的数据域保存下来。
TeacherMasterCard类与MasterCard类之间存在一种“IS”的关系。即教师一卡通是一卡通。继承关系的一般格式如下
class 子类名 extends 父类名{
方法
静态变量
数据域
}
java中定义继承类使用关键字extends,而C++使用冒号。java中所有的继承都是公共的,而C++是私有的、保护继承的。
关键词extends表示正在构造一个新的类,派生于一个已经存在的类,已经存在的类被称为超类(Super Class)或者父类(Parent Class),而新类被称为子类(Sub Class)或派生类(Derived Class)或孩子类(Child Class)
假设教师在食堂吃饭刷卡是有政策优惠的,例如,给教师打9折。那就需要一个常量域,继承这个折扣,并在教师卡内添加相应的消费方法。
package example;
class TeacherMasterCard extends MasterCard{
//覆盖consume方法
public void consume(double money) {
this.money = this.money - money * discount; //重新给mioney赋值
}
private static final double discount = 0.9; //声明静态域,表示折扣价
}
子类TeacherMasterCard中方法域money必须被定义为包护类型,如果是私有类型,则子类将不能直接访问
因为consume()方法不是在MasterCard类中定义的,所以MasterCard类对象不能使用consume()方法。而TeacherMasterCard类自动继承了父类MasterCard中所有方法和数据域,因此TeacherMasterCard类的对象可以随意使用他们。
通常定义子类的时候只需要定义出父类与子类的不同方法和域,所以在设计类的时候,应该将通用的方法放在父类中,将特殊用途的方法放在子类中。
2.super()构造方法调用
子类可以调用父类的方法,也可以调用自己的方法。如果是覆盖的方法,那么子类中如何调用父类的方法呢?
在定义子类的构造方法时,可以调用父类的构造方法,子类的构造方法使用一种特殊的方法调用度类的构造方法。**父类的构造方法初始化所有从父类继承下来的数据域。**这样子类构造方法只需要调用父类的构造方法即可。没有必要重新对父类的域初始化。
调用父类构造方法的一般格式如下:
public 子类名(子类构造方法参数){
super(父类构造方法参数);
其他域的赋值语句
}
子类的构造方法通过super()方法调用父类的构造方法,该方法必须放在子类构造方法的第一句。并且,super()方法可以通过参数来区别调用是父类构造方法中的哪一个。
下面是一个TeacherMasterCard类的构造函数
public TeacherMasterCard(){
super(); //调用父类的无参构造方法
storey = 0; //初始化storey
}
public TeacherMasterCard(String name,char sex,double money,int year,int month,int day,int storey){
{
super("T"+flag,name,sex,money,year,month,day); //调用父类的构造方法
if(storey<0){ //当storey小于0时
System.out.println("办公室门牌号不能为负数");//输出字符串信息
System.exit(0); //退出程序
}
}
public TeacherMasterCard(String name,char sex,double money,int storey){
super("T"+flag,name,sex,money); //调用父类的构造方法
if(storey<0){ //当storey小于0时
System.out.println("办公室门牌号不能为负数");//输出字符串信息
System.exit(0); //退出程序
}
}
public TeacherMasterCard(String name,char sex,double money,double initial,int storey){
super("T"+flag,name,sex,money); //调用父类的构造方法
if(storey<0){ //当storey小于0时
System.out.println("办公室门牌号不能为负数");//输出字符串信息
System.exit(0); //退出程序
}
charge(initial); //设置折扣价
}
子类TeacherMasterCard的每一个构造方法都调用了父类的MasterCard的构造方法,使用super()语句实现了这样的功能
在调用父类的构造的构造方法时,不能使用子类的构造方法名,必须使用super()方法调用。例如,子类的TeacherMasterCard调用父类的MasterCard的构造方法必须使用super()方法实现
在子类的构造方法中没有调用父类的构造方法,那么系统会自动调用无参构造父类的构造方法。
public TeacherMasterCard(String name,char sex,double money,int year,int month,int day,int storey){
{
// super(); //调用父类的构造方法
if(storey<0){ //当storey小于0时
System.out.println("办公室门牌号不能为负数");//输出字符串信息
System.exit(0); //退出程序
}
}
在一个子类中拥有所有的数据域,如果要对这些数据域进行初始化,那么在父类的构造方法总初始化是最方便的,所以,在定义一个子类的构造方法时,总是先调用父类的构造方法,如果没有自己调用,则系统会自动调用父类中没有参数的构造方法。
This构造方法调用与super构造方法调用区别在于,this调用的是同类的构造方法,而super调用的是父类的构造方法。一个构造方法中不能同时包括this调用和super调用
3.封装和继承
父类声明为private的数据域也被继承到子类的实例域,但是具有private属性的实例域表示这个实例域是专有实例域,也就是说,只能在父类中定义方法时访问,不能再其他类的方法中访问,即使子类也是不可以的。
例如,如果程序中父类MasterCard的各个域都被定义为private,则在该类中可以直接访问。
public void printinfo(){
System.out.println("name="+name+"sex="+sex+"money="+money+"releasedDay="+releasedDay.toString()); //输出字符串信息
}
private String name; //声明字符串类型数据域,表示持卡人的姓名
private char sex; //声明字符类型数据域,表示持卡人性别
private double money;//声明double类型数据域,表示卡内余额
private Date releasedDay;//声明Date类型数据域,表示发卡时间
在子类TeacherMasterCard中不能直接访问父类的私有数据域,只有通过共有访问方法或变更方法,如getName() 、setName()方法才可以访问。在子类TeacherMasterCard的printinfo()方法中使用父类public方法访问父类的私有数据域。
public void printInfor(){
System.out.println("name="+getName()+"sex="+getSex()+"money="+getMoney()+"releasedDay="+releasedDay.toString()+"storey="+storey); //输出字符串信息
}
定义为private类型的父类数据域不能通过变量名或方法被子类直接访问,但是子类通过名称可以访问父类中两种类型的实例域和方法,即保护访问和包访问。保护访问可以提供子类访问,而包访问只有子类和父类在同一个包里才能被访问。
如果一个方法或实例被protected修饰,那么他可以被本类、继承类、所在包内的任何类通过使用变量名或方法形名的方式访问。
例如,在MasterCard类中的变量是这样定义的:
protected String name; //声明字符串数据域,表示持卡人的类型
private char sex; //声明字符类型数据域,表示持卡人的性别
protected double money; //声明double类型数据域,表示卡内余额
priate Date releasedDay; //声明Date类型数据域,表示发卡时间
那么子类TeacherMasterCard中printInfo()方法就可以通过如下方式访问父类的私有数据域:
//直接访问对象的成员变量
public void printInfo(){
System.out.println("name="+name+"sex="+sex+"money="+money+"releasedDay="+releasedDay.toString());
}
也可以通过public类型的getter()方法访问父类的私有数据域
public void printInfo(){
System.out.println("name="+getName()+"sex="+getSex()+"money="+getMoney()+"releasedDay="+releasedDay.toString()+"storey="+storey);
}
如果类中的实例域,方法没有被public、protected、private中任何一个形式,那么这个变量或方法被称为默认访问。又叫做包访问、友好访问。
在内部定义为包访问权限的域、方法可以被任何类通过使用名称来访问。
4.使用继承
java中除了基本数据类型,所有数据都是以对类对象的形式表示的。并且所有的类都是Object类的子类。换句话说,Object类是所有Java类的祖先。
1.Object类
Object类是JDK类库中最为特殊的一个类,因为它是所有类直接或间接的父类,包括类库中的类和自定义的类。当定义一个类时,如果不指定是Object类的派生类,那么Java会自动将其编程Object类的子类。
Object的子类可以编写参数为Object类型的方法,这个参数可以被其他任何类型的对象代替。Object类在java.lang包中,该包会被系统自动导入,无需程序员导入。
2.equals()方法
如果要创建如下两个字符串,那么表达式A==B返回真还是假呢?
String str1 = new String("hello"); //创建并初始化字符串str1
String str2 = new String("hello"); //创建并初始化字符串str2
System.out.println(str1==str2); //输出str1的引用和str2的引用是否相同//false
String字符串是java中比较特殊的一个类。String被定义为final类型,并且同样的字符串在常量池中不能重复。java虚拟机有一个存放字符串的字符串池。在创建一个字符串时,系统的常量池会自动查找字符串是否存在。如果找到该字符串,那么会返回字符串池中的字符串地址。如果没有找到,则创建一个新的字符串,并返回地址。
使用关键字new创建新的字符串,java会在堆中分配新的空间。所以即使字符串的内容相同,通过new创建的字符串的引用也不同。所以上诉代码的返回值为false。
如果要比较两个字符串的值是否相等同(而不是他们的引用是否相同)。可以使用equal()方法。
System.out.println(str1.equals(str2));//输出字符串的值是否相同。
Object类中equals()方法的原型为:
public boolean equals(Object obj){
return (this == obj);//返回对象的引用是否相同
}
Object中定义的equals(Object)方法用于比较两个对象的引用是否相同,TeacherMasterCard类中给出了一个比较好的equals()覆盖方法,如下:
//覆盖Master类的Object方法
public boolean equals(Object otherObject){
if(otherObject==null)
return false;
else if(getClass()!=otherObject.getClass())
return false;
else{
MasterCard otherMasterCard = (MasterCard)otherObject;
return(id.equals(otherMasterCard.id)&&name.equals(otherMasterCard.name)
&&sex==otherMasterCard.sex&&money==otherMasterCard.money&&releasedDay.equals(otherMasterCard).releasedDay))
}
}
instanceof操作符域getClass()方法都可以检测一个对象类型。两者的区别在于,instanceof操作符测试对象是否属于某种类型,而getClass()方法则使用“==”或“!=”测试这两个对象是否是同一个类创建的
instanceOf操作符的语法如下:
object instanceof ClassName
如果对象object是类 ClassName的对象,则返回true。如果A派生于B类,B类派生于C类,且C类派生于D类,那么下面的运算都返回true。
A a = new A();
a instanceof A;
a instanceof B;
a instanceof C;
a instanceof D;
5.关于设计好类的几个建议
1.java类继承只能实现单个类的继承,而不支持多类继承。即一个类只能有一个父类,不允许继承多个子类。但是反过来,一个类可以被多个类继承。
如下图所示的类ClassB继承了一个类ClassA
class ClassA{}
class ClassB extends ClassA{}
2.java可以实现多层继承。即一个类可以继承一个可以存在的类,而新形成的这个类又可以作为其他类的父类。如下面所示,类ClassB继承了ClassA,类ClassC继承了类ClassB,所以类ClassC间接继承了ClassA。
class ClassA{}
class ClassB extends ClassA{}
class classC extends ClassB{}
3.java中子类可以继承父类所拥有的成员变量和成员方法。但不能继承父类的构造方法。在子类的构造方法中,可以调用super(参数列表)完成对父类构造方法的调用,如子类ClassB调用父类ClassA的构造方法:
class Class A{
public ClassA(){
}
}
class ClassB extends ClassA{
public ClassB(){
super();
}
}
4.若子类的构造方法没有显式调用父类的某个构造方法,也没有显式通过关键字this调用其他的构造方法。那么在产生类对象时,系统会调用无参数的构造方法。
class Class A{
public ClassA(){
}
}
class ClassB extends ClassA{
public ClassB(){
// 自动调用super();
}
}
如果父类没有显式定义任何构造方法,则系统会自动提供默认的无参构造方法。这等同于父类中具有无参数的构造方法。
6.实例:一卡通的继承
1.一卡通的继承类
public class TeacherMasterCard {
//声明枚举类型Title
enum Title{
TeachingAssistant,Lecturer,AssistantProfessor,Professor,Default
}
private int storey;//声明整形数据域,表示办公室楼牌号。
private static int flag = 0;//声明静态整型数据域,用于自动生成卡ID。
private static double discount = 0.8;//声明浮点型数据域,表示折扣
private Title title ; //声明Title枚举类型域,表示教师职称。
}
2.构造方法
TeacherMasterCard类设计为MasterCard3的子类。所以要添加extends关键字来继承父类。
由于teacherMasterCard继承了MasterCard的域和方法,因此必须对域进行初始化。对于父类的域不要需要再重新进行初始化,而是调用父类的构造方法完成对父类域的初始化。
//构造方法
//TeacherMasterCard类的无参构造方法
public TeacherMasterCard() {
super(); //调用父类的构造方法
setStorey(0); //初始化成员变量Storey
title = Title.Default;//初始化成员变量title
}
//TeacherMasterCard类的构造方法
public TeacherMasterCard(String name,char sex,double money,int year,int month,int day,int storey,Title title) {
super(name,sex,money,year,month,day);//调用父类的构造方法
setStorey(storey); //设置办公室的门牌号
setTitle(title); //设置职称
}
//TeacherMasterCard类的构造方法
public TeacherMasterCard(String name,char sex,double money,int storey,Title title) {
super(name,sex,money);//调用父类的构造方法
setStorey(storey); //设置办公室的门牌号
setTitle(title); //设置职称
}
//TeacherMasterCard类的构造方法
public TeacherMasterCard(String name,char sex,double money,double initial,int storey,Title title) {
super(name,sex,money);//调用父类的构造方法
setStorey(storey); //设置办公室的门牌号
setTitle(title); //设置职称
charge(initial);6
}
4个构造方法的第一条语句都是调用super()方法初始化父类的域,然后初始化子类的新域。
3.setter()方法
TeacherMasterCard类提供了域的setter()方法泳衣设置办公室的门牌号、职称、折扣价,由于门牌号是一个正整数,所以当设置门牌号为负数时,程序将报错并退出。
//setter()方法
//设置办公室门牌号storey
public void setStorey(int storey) {
if(storey<0) {
System.out.println();
System.exit(0);//退出程序
}
this.storey = storey;//赋值给成员变量storey
}
//设置职称title
public void setTitle(Title title) {
this.title = title; //给成员变量title赋值
}
//设置折扣价
public static void setDiscount(double discount) {
TeacherMasterCard.discount = discount;//设置discount的值
}
//设置卡号
public static void setFlag(int Flag) {
TeacherMasterCard.flag = flag;
}
4.getter()方法
同setter方法对应,TeacherMasterCard提供了域的getter()方法获得域的值
//getter()方法
//返回办公室门牌号storey
public int getStorey() {
return storey; //返回成员变量storey;
}
//返回之职称的字符串表示
public String getTitle() {
return title.toString(); //返回title的字符串
}
//返回折扣价
public static double getDiscount() {
return discount; //返回discount的值
}
//返回卡号
public static int getFlag() {
return flag;
}
5.方法覆盖
父类中定义了consume()、printInfor()方法,所以TeacherMasterCard类重写了这两个方法。
//方法覆盖
//持卡人用卡消费
public void consume(double money) {
super.consume(money*discount); //调用父类的consume()方法
}
//输出卡的信息
public void printInfo() {
System.out.println("name="+getName()+"|sex="+getSex()+"|money="+getMoney()+"|releasedDay="+getStringofReleasedDay()+"|storey="+storey+"|title="+title.toString());
}
6.equals()方法
equals()是Object类的一个方法。重写该方法后,可以按照用户的定义的规则进行比较。
//定义equals方法
public boolean equals(Object otherobject) {
if(otherobject == null ) //判断otherObject对象是否为空
return false;
//判断该对象类型是否与otherObject类型相同
else if(getClass()!=otherobject.getClass())
return false;//返回false;
else {
//对otherobject对象进行强制类型转换,并创建otherMasterCard对象
TeacherMasterCard otherMasterCard = (TeacherMasterCard)otherMasterCard;
//分别判断对象的值是否相等,如果全部相等则返回true,否则返回false
return
getName().equals(otherMasterCard.getName())
&&getSex()==otherMasterCard.getSex()
&&getReleasedDay().equals(otherMasterCard.getStringofReleasedDay())
&&storey==otherMasterCard.storey
&&title == otherMasterCard.title;
}
}
7.其他需要强调的地方
该类还使用了初始化块。每当创建一个对象时初始化块就执行一次,因此当创建一个对象时,flag的值会自动加1.由于教师的职称只有4种类型,所以选择枚举类型组委保存教师职称的数据类型。
下面是初始化块和枚举类型的声明代码。
//初始化块,flag每次自动加1
{
flag++;
}
8.创建测试类UseTeacherMasterCard
package example;
import example.TeacherMasterCard.Title;
public class UseTeacherMasterCard {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建TeacherMasterCard类型的数组
TeacherMasterCard[] mc = new TeacherMasterCard[2];
//构建对象,并指向mc[0]、mc[1]
//通过构造函数初始化mc[0]
mc[0] = new TeacherMasterCard("李梅","M",100,2016,3,15,101,Title.AssistantProfessor);
//初始化mc[1]
mc[1] = new TeacherMasterCard("韩雷雷","F",25,2016,2,18,200,Title.Lecturer);
//输出第一个人的部分信息
mc[0].printInfo();
//输出第一个人的金额
System.out.println("充值前:"+mc[0].getMoney());//查看卡的数据类型
//为第一个人充值
mc[0].charge(20);//充值20
System.out.println("充值后"+mc[0].getMoney());//查看卡的数据类型
//为第二个人充值并显示信息
mc[1].printInfo();
System.out.println("充值前:"+mc[1].getMoney());//查看卡的数据类型
mc[1].charge(50);//充值20
System.out.println("充值后"+mc[1].getMoney());//查看卡的数据类型
}
}
7.拓展——领导和员工的差异
对于在同一家公司工作的领导和员工而言,两者之间是有很多共同点的,例如,每个月都会发工资。但是领导除了基本工资,还有分红奖金。此时,如果我们使用员工类来编写领导类就会省略很多重复代码,利用继承技术,可以让领导类使用员工中的属性和方法。
可以通过继承类演示领导和员工的差异。
1.Employee.java
package example;
import java.util.Date;
public class Employee {
public String getName() { //获取员工姓名
return name;
}
public void setName(String name) {//设置员工姓名
this.name = name;
}
public double getSalary() { //获取员工工资
return salary;
}
public void setSalary(double salary) { //设置员工工资
this.salary = salary;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
private String name; //员工的姓名
private double salary; //员工的工资
private Date date; //员工的入场时间
}
2.Manager.java
package example;
public class Manager extends Employee {
private double bounds;//经理的奖金
public double getBounds() { //获得经理的奖金
return bounds;
}
public void setBounds(double bounds) {//设置经理的奖金
this.bounds = bounds;
}
}
3.Test.java
package example;
import java.util.GregorianCalendar;
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Employee employee = new Employee();
employee.setName("tom");
employee.setSalary(5000);
employee.setDate(new GregorianCalendar(2016,3,1).getTime());
Manager manager = new Manager();
manager.setName("mary");
manager.setSalary(5500);
manager.setDate(new GregorianCalendar(2012,7,1).getTime());
manager.setBounds(2000);
manager.setBounds(2000);
//输出员工和经理的属性值
System.out.println("员工的名字"+ employee.getName());
System.out.println("员工的工资"+ employee.getSalary());
System.out.println("员工的入职时间"+ employee.getDate());
System.out.println("经理的名字"+ manager.getName());
System.out.println("经理的工资"+ manager.getSalary());
System.out.println("经理的入职时间"+ manager.getDate());
System.out.println("经理的奖金"+ manager.getBounds());
}
}
在面向对象的程序设计中,继承是其基本特征之一。
在父类里声明的域,如果没有被声明为private,则可以在子类里直接使用,不需要再次声明。
成员方法同样也可以继承。父类中声明的非private方法,在子类中也可以直接访问而无需再次声明,同样可以做到代码重用的目的。子类除了可以继承父类中的域和方法,还可以增加自己的成员方法。
8.技术解惑
8.1 super语句必须是子类构造方法的第一句吗
答案是肯定的。子类的构造方法的调用必须先完成父类构造方法的调用,这是系统初始化的特性。所以super语句必须放在第一位,否则编译器会报错:“构造函数调用必须是构造函数中的第一条语句”、
2.equals方法的用法和“==”的用法一致吗
两者的用法不同,首先,==是java的一个关系运算符,表示相等。所以,在所有表示相等关系的表达式中都可以使用。表达式是一个布尔表达式。equals()方法时Java提供的一个Object类方法。用于判断两个对象的内容是否相同,所以equals是不能比较基本类型的变量的。
==比较对象时,比较的是两个对象的引用地址。
String a = new String("foo");
String b = new String("foo");
这两个new语句创建了两个对象,然后用a、b两个变量分别指向他们。a、b是两个不同的String对象,他们的首地址是不同的。(即a 、b中存储的值,也就是两个字符串的首地址是不同的)。但是这两个对象中的内容是相同的。所以 a==b 为 false,a.equals(b)为true