1.继承
继承是面向对象的三大特征之一。
类与类之间的三种关系:
is a :继承关系,例如:公共汽车 is a 汽车
use a:使用关系,例如:人 use a 钳子
has a:包含关系,例如:人has a 胳膊
1.1.为什么要继承
- 继承的出现提高了代码的复用性,提高软件开发效率。
- 继承的出现让类与类之间产生了关系,提供了多态的前提。
1.2.继承的定义格式
在程序中,如果想声明一个类继承另一个类,需要使用extends关键字。
[访问修饰符] class 子类 extends 父类 {
子类成员变量
子类成员方法
}
●注意:子类无法继承父类的构造方法
1.3 方法重写(方法覆盖|override)
1.3.1什么是方法重写
答:在子类中将父类的方法再重新定义一遍。即方法名,参数列表,返回值类型与父类相同
1.3.2为什么要方法重写
class Pet {//宠物
public void sound(){
System.out.println("宠物叫");
}
}
class Dog extends Pet {//狗
}
class Cat extends Pet{//猫
}
public class PetShop{//宠物店
public static void main(String[] args) {
Dog dog = new Dog();
dog.sound();
Cat cat = new Cat();
cat.sound();
}
}
如果子类从父类继承的方法不能满足子类的需要,或者不适合子类的需要。
此时子类可以将从父类继承的方法重写定义成满足自己需要的方法。
重新定义称为重写。
改进本例,让猫和狗重写父类的sound()方法,实现各自的叫声。
class Pet {//宠物
public void sound(){
System.out.println("宠物叫");
}
}
class Dog extends Pet {//狗
//方法重写
@Override
public void sound() {
System.out.println("汪汪汪");
}
}
class Cat extends Pet{//猫
//方法重写
@Override
public void sound(){
System.out.println("喵喵喵");
}
}
public class PetShop{//宠物店
public static void main(String[] args) {
Dog dog = new Dog();
dog.sound();//调用狗的sound()
Cat cat = new Cat();
cat.sound();//调用猫的sound()
}
}
1.4.使用继承
案例:公司有2个部门,人事部和研发部,各自属性如下
●定义人事部类:
/*
* 人事部
*/
public class PersonalDepartment {
private int ID;// 部门编号
private String name = "待定";// 部门名称
private int amount = 0;// 部门人数
private String responsibility = "待定";// 部门职责
private String manager = "无名氏";// 部门经理
private int count;//招聘人数
public PersonalDepartment() {
}
public PersonalDepartment(int ID, String name, int amount, String responsibility, String manager, int count) {
this.ID = ID;
this.name = name;
this.amount = amount;
this.responsibility = responsibility;
this.manager = manager;
this.count = count;
}
//为了阅读方便,以下省略set//get方法
......
●定义研发部类
/**
* 研发部
*/
public class ResourceDepartment {
private int ID;// 部门编号
private String name = "待定";// 部门名称
private int amount = 0;// 部门人数
private String responsibility = "待定";// 部门职责
private String manager = "无名氏";// 部门经理
private String speciality =null;//研发方向
public ResourceDepartment() {
}
public ResourceDepartment(int ID, String name, int amount, String responsibility, String manager, String speciality) {
this.ID = ID;
this.name = name;
this.amount = amount;
this.responsibility = responsibility;
this.manager = manager;
this.speciality = speciality;
}
//为了阅读方便,以下省略set//get方法
......
发现问题:两个类中的属性有相同的部分,代码冗余。解决办法是将两个部门共性的属性抽取出来,放在父类中,然后让两个部门继承父类。
●定义父类:两个部门共性的属性抽取到父类中
/**
* 部门父类:存放所有子类共性的内容
*/
public class Deparment {
private int ID;// 部门编号
private String name = "待定";// 部门名称
private int amount = 0;// 部门人数
private String responsibility = "待定";// 部门职责
private String manager = "无名氏";// 部门经理
//为了阅读方便,以下省略set//get方法
......
●子类修改如下:
- 共性的属性删除
- 共性属性的get和set方法删除
- 构造方法做调整
●让人事部和研发部继承部门的父类
- 使用extends Deparment继承部门类
/**
* 人事部
*/
public class PersonalDepartment extends Deparment{
private int count;//招聘人数
public PersonalDepartment() {
}
public PersonalDepartment(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
- 使用extends Deparment继承部门类
/**
* 研发部
*/
public class ResourceDepartment extends Deparment{
private String speciality =null;//研发方向
public ResourceDepartment() {
}
public ResourceDepartment(String speciality) {
this.speciality = speciality;
}
public String getSpeciality() {
return speciality;
}
public void setSpeciality(String speciality) {
this.speciality = speciality;
}
}
●验证子类是否真的从父类继承了数据
验证方法:
- 创建人事部和研发部的对象,如果能够调用出父类的方法,证明继承了
创建一个公司类,测试一下:
/**
* 公司类
*/
public class Company {
public static void main(String[] args) {
//创建人事部对象
PersonalDepartment personalDepartment = new PersonalDepartment();
//创建研发部对象
ResourceDepartment resourceDepartment = new ResourceDepartment();
//调用父类的方法
System.out.println(personalDepartment.getName()); //getName()是父类的
System.out.println(resourceDepartment.getName()); //getName()是父类的
}
}
继承的好处:
- 好处1:代码复用
- 好处2:父类可以控制子类能继承什么,通过封装控制
来一个新的需求:每个部门都自我介绍一下
具体办法:在部门 的父类中添加一个方法,toString(),实现自我介绍
@Override
public String toString() {
return "Deparment{" +
"ID=" + ID +
", name='" + name + '\'' +
", amount=" + amount +
", responsibility='" + responsibility + '\'' +
", manager='" + manager + '\'' +
'}';
}
由于toString()方法定义在父类中,所以人事部和研发部都继承了这个toString()方法。
在公司类中让每个部门自我介绍一下,只要调用toString()方法就可以自我介绍了。
public class Company {
public static void main(String[] args) {
//创建人事部对象
PersonalDepartment personalDepartment = new PersonalDepartment();
//创建研发部对象
ResourceDepartment resourceDepartment = new ResourceDepartment();
//调用父类的方法
System.out.println(personalDepartment.toString());//自我介绍
System.out.println(resourceDepartment);//自我介绍,如果一个对象放在了sout语句中,那么这个对象默认会调用toString()方法。
}
}
运行结果:
Deparment{ID=0, name='待定', amount=0, responsibility='待定', manager='无名氏'}
Deparment{ID=0, name='待定', amount=0, responsibility='待定', manager='无名氏'}
来一个新的需求:为部门设置属性值
●为人事部设置属性值
public class Company extends Object{
public static void main(String[] args) {
//创建人事部对象
PersonalDepartment personalDepartment = new PersonalDepartment();
personalDepartment.setID(110); //通过父类的set方法为属性赋值
personalDepartment.setCount(5); //通过父类的set方法为属性赋值
personalDepartment.setManager("卡卡"); //通过父类的set方法为属性赋值
personalDepartment.setResponsibility("招聘"); //通过父类的set方法为属性赋值
personalDepartment.setName("人事部"); //通过父类的set方法为属性赋值
//创建研发部对象
ResourceDepartment resourceDepartment = new ResourceDepartment();
//调用父类的方法
System.out.println(personalDepartment.toString());
System.out.println(resourceDepartment);
}
}
运行结果:
Deparment{ID=110, name='人事部', amount=0, responsibility='招聘', manager='卡卡'}
Deparment{ID=0, name='待定', amount=0, responsibility='待定', manager='无名氏'}
结论:对象的属性可以通过set方法赋值
来一个新的需求:当创建部门对象时,直接初始化部门的所有信息。不要new完以后再调用set方法赋值。
解释:创建子类对象时的运行规则
- 第一个方面:构造函数的调用顺序问题
- new子类时,首先调用子类构造函数,但是不执行子类构造函数
- 子类构造函数被调用后立即调用父类的构造函数。
- 第二个方面:属性初始化顺序的问题
- 从Object类开始初始化
-
然后依次从父到子的顺序初始化(哪个类定义的属性,就由哪个类负责初始化)
子类通过关键字super()调用父类构造函数
super()必须放在子类构造函数的第一行代码
代码编写步骤:
●第一步:重构父类构造
- 首先为父类添加构造,父类的构造为父类中定义的属性初始化
- 子类定义的特有属性不是父类负责初始化的。
public Deparment(int ID, String name, int amount, String responsibility, String manager) {
super();
this.ID = ID;
this.name = name;
this.amount = amount;
this.responsibility = responsibility;
this.manager = manager;
}
●第二步:重构子类构造
- 子类构造应该获得所有属性的初始值,本例中共6个属性,其中5个是父类的,1个是自己的
- 子类构造的第一行必须是调用父类构造,
- 如果不写super(),那么默认再子类第一行添加调用父类无参的构造super()
- 子类再调用父类构造代码super()的下面负责给自己的属性赋值
public PersonalDepartment(int ID, String name, int amount, String responsibility, String manager,int count) {
super(ID,name,amount,responsibility,manager);
this.count = count;
}
●第三步:重构实例化子类对象
- 再测试类中new子类时,要为子类传入属性的参数
PersonalDepartment personalDepartment = new PersonalDepartment(110,"人事部",5,"负责招聘","卡卡",10);
●第四步:测试
研发部按照人事部同样处理。
※本例的结论:
- new子类时父类new了吗?答new了
- new子类时构造函数的调用顺序是什么?从子到父
- new子类时构造函数的执行顺序是什么?从父到子
1.5.继承的注意事项
类只支持单继承,不允许多继承
class A{}
class B{}
class C extends A,B{} // C类不可以同时继承A类和B类
多个类可以继承一个父类
class A{}
class B extends A{}
class C extends A{} // 类B和类C都可以继承类A
允许多层继承
class A{}
class B extends A{} // 类B继承类A,类B是类A的子类
class C extends B{} // 类C继承类B,类C是类B的子类,同时也是类A的子类
子类和父类是一种相对概念
也就是说一个类是某个类父类的同时,也可以是另一个类的子类。例如上面的这种情况中,B类是A类的子类,同时又是C类的父类。
1.6.继承的体系 之 Object类
Object是所有类的父类
如果一个类没有显示定义父类,那么默认继承Object类
Object类中没有定义属性,但是定义了12个方法,并且这些方法都是实例方法。因此每个对象都拥有这14个方法。
序号 | 方法签名 | 说明 |
---|---|---|
1 | String toString() | 返回当前对象本身的有关信息,返回字符串对象 |
2 | boolean equals(Object) | 比较两个对象是否是同一个对象,若是,返回true |
3 | Object clone() | 生成当前对象的一个副本,并返回 |
4 | int hashCode(); | 返回该对象的哈希码值 |
5 | void finalize() | 在垃圾收集器将对象从内存中清除出之前做必要的清理工作。 |
6 | void wait() | 线程等待 |
7 | void wait(int) | 线程等待 |
8 | void wait(long,int) | 线程等待 |
9 | void notify() | 线程唤醒 |
10 | void notifyall() | 线程唤醒 |
11 | Class getClass() | 获取类结构信息,返回Class对象 |
1.7. ※继承过程分析(父子类的实例化顺序)(重要)
当new子类时,父类也new了
那先new谁呢?先new Object,然后依次从上向下new 子类。
执行:从父到子
调用:从子到父
哪个类定义的属性,就由哪个类赋值初始化,对于父类来说,初始化的数据是由子类通过super(数据)传入给父类的
1.7 继承后子类的成员的变化
了解了继承给我们带来的好处,提高了代码的复用性。继承让类与类或者说对象与对象之间产生了关系。那么,当继承出现后,类的成员之间产生了那些变化呢?
子类中的成员包括:
- 子类自己定义的属性和方法
- 从父类继承的属性和方法
- 子类不能使用从父类继承的私有成员 原因:因为被封装了。
1.8. this和super
继承中的this(调用子类成员)
- this可以调用子类成员
- this可以调用子类重载的构造函数,必须是子类构造函数的第一行
继承中的super(调用父类成员)
- super调用父类成员
- super调用父类构造函数,必须是子类构造函数的第一行
this的冤家和搭档
一对搭档 this和super
this代表自己,this可以调用
- 自己的属性和方法
- 在本类的构造函数内调用其他构造函数
super代表父类,super可以调用
- 父类的属性和方法
- 在子类构造中调用父类构造
一对冤家 this和static
this表示实例
static表示静态
static不能调实例,static看this就像冤家
对象名可以调用static,但是this确不能调用static,this看static也像冤家
2. 多态
多态是面向对象的三大特征之一
2.1.什么是多态
多态就是多种形态,多种形式
多态的实现方法有两种
- 方法重写(公认的多态)
- 方法重载(非公认的多态)
2.2.多态的语法格式
多态的定义格式:就是父类的引用变量指向子类对象
父类类型 变量名 = new 子类类型();
变量名.方法名();
Pet pet = new Cat();
pet.toString();
2.3. 多态的实现(方法重写)
2.3.1普通类实现多态
- 普通类多态定义的格式
父类 变量名 = new 子类();
例如:
class Fu {}
class Zi extends Fu {}
Fu f = new Zi();//类的多态使用
2.3.2.抽象类实现多态
- 抽象类多态定义的格式
抽象类 变量名 = new 抽象类子类();
例如:
abstract class Fu {
public abstract void method();
}
class Zi extends Fu {
public void method(){
System.out.println(“重写父类抽象方法”);
}
}
Fu fu= new Zi();//类的多态使用
2.3.3 接口实现多态
- 接口多态定义的格式
接口 变量名 = new 接口实现类();
``
例如:
```java
interface Fu {
public abstract void method();
}
class Zi implements Fu {
public void method(){
System.out.println(“重写接口抽象方法”);
}
}
Fu fu = new Zi();//接口的多态使用
- 注意事项
同一个父类的方法会被不同的子类重写。父类引用在调用方法时,调用的为各个子类重写后的方法。
Person p1 = new Student();
Person p2 = new Teacher();
p1.work(); //p1会调用Student类中重写的work方法
p2.work(); //p2会调用Teacher类中重写的work方法
当变量名指向不同的子类对象时,由于每个子类重写父类方法的内容不同,所以会调用不同的方法。
2.4.多态的实现(方法重载)
class Student {
public void doWork(){
System.out.println(" is working ...");
}
public void doWork(Date from ,Date end){
SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(" from "+ sdf.format(from) +"... end "+ sdf.format(end) +"... working....");
}
}
public class TestInstanceof {
public static void main(String[] args) {
Student student = new Student();
student.doWork();//第1种重载(第1种多态)
student.doWork(new Date(1658972077239L),new Date(1658973187239L));//第2种重载(第2种多态)
}
}
2.5. 多态的转型
多态的转型分为向上转型与向下转型两种:
- 向上转型就是父类引用指向子类对象
- 向下转型就是子类引用指向父类对象
2.5.1.向上转型
向上转型:当有子类对象赋值给一个父类引用时,便是向上转型,多态本身就是向上转型的过程。
使用格式:
父类类型 变量名 = new 子类类型();
例如:
Person p = new Student();
2.5.2向下转型
向下转型:一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用转为子类引用,这个过程是向下转型。
如果是直接创建父类对象,是无法向下转型的!
使用格式:
子类类型 变量名 = (子类类型) 父类类型的变量;
例如:
Student stu = (Student) p; //变量p 实际上指向Student对象
2.5.3 多态的好处和弊端
当父类的引用指向子类对象时,就发生了向上转型,即把子类类型对象转成了父类类型。向上转型的好处是隐藏了子类类型,提高了代码的扩展性。
但向上转型也有弊端,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。看如下代码
//描述动物类,并抽取共性eat方法
abstract class Animal {
abstract void eat();
}
// 描述狗类,继承动物类,重写eat方法,增加lookHome方法
class Dog extends Animal {
void eat() {
System.out.println("啃骨头");
}
void lookHome() {
System.out.println("看家");
}
}
// 描述猫类,继承动物类,重写eat方法,增加catchMouse方法
class Cat extends Animal {
void eat() {
System.out.println("吃鱼");
}
void catchMouse() {
System.out.println("抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //多态形式,创建一个狗对象
a.eat(); // 调用对象中的方法,会执行狗类中的eat方法
// a.lookHome();//使用Dog类特有的方法,需要向下转型,不能直接使用
// 为了使用狗类的lookHome方法,需要向下转型
// 向下转型过程中,可能会发生类型转换的错误,即ClassCastException异常
// 那么,在转之前需要做健壮性判断
if( !(a instanceof Dog)){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;
}
Dog d = (Dog) a; //向下转型
d.lookHome(); //调用狗类的lookHome方法
}
}
来总结一下
-
什么时候使用向上转型
当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型。
例如:
Animal a = new Dog(); a.eat();
-
什么时候使用向下转型
当要使用子类特有功能时,就需要使用向下转型。
例如:
Dog d = (Dog) a; //向下转型 d.lookHome(); //调用狗类的lookHome方法
向下转型的好处:可以使用子类特有功能。
-
向下转型的弊端是:需要面对具体的子类对象;在向下转型时容易发生ClassCastException类型转换异常。在转换之前必须做类型判断。例如:
if(a instanceof Dog){…}
instanceOf运算符解决子类扩展方法的调用
//描述动物类,并抽取共性eat方法
abstract class Animal {
abstract void eat();
}
// 描述狗类,继承动物类,重写eat方法,增加lookHome方法
class Dog extends Animal {
void eat() {
System.out.println("啃骨头");
}
void lookHome() {
System.out.println("看家");
}
}
// 描述猫类,继承动物类,重写eat方法,增加catchMouse方法
class Cat extends Animal {
void eat() {
System.out.println("吃鱼");
}
void catchMouse() {
System.out.println("抓老鼠");
}
}
public class TestInstanceof {
public static void main(String[] args) {
Animal a = new Dog(); //多态形式,创建一个狗对象
a.eat(); // 调用对象中的方法,会执行狗类中的eat方法
//a.lookHome();//使用Dog类特有的方法,需要向下转型,不能直接使用
// 为了使用狗类的lookHome方法,需要向下转型
// 向下转型过程中,可能会发生类型转换的错误,即ClassCastException异常
// 那么,在转之前需要做健壮性判断
if( !(a instanceof Dog)){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;
}
Dog d = (Dog) a; //向下转型
d.lookHome(); //调用狗类的lookHome方法
}
}
Java子方法重载(overload)和方法重写(override)的区别
- 方法重载:
- 在同一个类中(包括从父类继承的) 方法 同名 不同参 与返回值无关
- 方法重写:
- 方法重写存在于继承关系中
- 父子类之间的方法 同名,同参,同返回