文章有点长,归纳较为详细,如有写的不全面和错误的希望大家热心指正。
在学习类的继承之前,先明确Java语言有三大特性:
- 封装性:详见本章
- 继承性:Java中类和类可以继承,即可共用一套代码。例如所有动物类由动物共同特点来抽象描述好,针对某一类动物如猫既有动物的共同特点又有自身的特点,那么猫类可以继承动物类,然后在自身的类中第一新的属性和方法。
- 多态性:当一个类中定义的属性和方法其它类继承后,可以在其它类表现出不同的类型和行为,即同名的属性和方法在不同的类中有不同的意义。
一、什么是继承
1.什么是继承
- 继承是面向对象的三大特征之一
- 继承是一种由已有的类作为基础派生出新类的机制
- 继承是实现类的复用的重要标志
- 实现继承的类叫子类,也叫派生类,被继承的叫做父类,也叫基类或超类
2.怎么使用继承
- 关键字:extends
- 一般格式
class 子类名 extends 父类名{
子类类体
}
- 使用思路
使用继承机制设计程序时:
- 先提取公有属性,创建父类
- 提取特殊属性,使用extends关键字创建子类
- 在子类中根据需要增加特有的状态(属性)和行为(方法)
- 举个例子
需要开发一个学生管理系统,就需要定义一个学生类,学生具有人的一般属性,定义的学生类只要继承人类中已经定义的属性和方法,然后在学生类中添加学生特有的属性和方法
class People {
String name;
int age;
public void selfInfo() {
System.out.println("我的名字是" + name);
System.out.println("我的年龄是" + age);
}
}
//继承父类People
class Student extends People{
private String school;
private int sno;
@Override
public void selfInfo() {//重写父类方法
super.selfInfo();//调用父类方法
System.out.print(",所在学校"+school);
System.out.println(",学号是"+sno);
}
}
}
3.继承的优点
根据上面的例子,可以总结出一些继承的优点:
- 提高代码的复用性
- 轻松定义字类,提高编程效率
- 父类的方法可用于子类
4.继承有哪些特点
- 只允许单继承:子类只有一个直接父类,下面的代码就会编译错误
class SubClass extends Base1,Base2{...}
- 子类比父类更加具体,如"学生是人"、“轿车是车”,子类比父类"人"和"车"有更多属性和方法,即一个类继承父类时常常要对父类进行扩展
- 子类有父类方法和属性
- 子类可重写父类的方法,隐藏属性
- 子类可以自定义自己的方法和属性
二、隐藏
上面说到继承的一些特点,属性可以被隐藏,方法呢?隐藏是指什么?隐藏了之后怎么调用呢?
1.属性的隐藏
- 定义:子类中定义了与父类同名的属性,则在子类访问该属性时,默认情况下父类的属性会被隐藏,调用的是子类定义的成员属性。
- 只要同名,属性就会被隐藏,与再次定义的属性的数据类型无关,可与父类相同也可不同
- 一般属性的隐藏实际意义不大,应避免使用
2.方法的重写
- 定义:子类重新定义与父类同名的方法,又称方法的覆盖
- 方法的重写具有实际意义
3.总结
关键词都在于同名,定义了同名的属性和方法,一个叫重写,一个叫隐藏,对于同名方法,如何在子类中使用和扩展这些与父类相同的方法呢?
三、与继承相关的一些方式:重写与重载
1.重写是什么
(1)属性被覆盖
- 举例:
class F {
int x = 1;
}
}
class S extends F {
int x = 2;
(2)普通方法被重写
-
举例:
-
被覆盖的方法在子类中被调用时,将访问在子类中定义的方法
class Bird{
public void fly(){
System.out.println("鸟儿在天空中自由飞行");
}
}
public class Ostrich extends Bird{
@Override
public void fly() {
System.out.println("鸵鸟不会飞,只会跑");
}
public static void main(String[] args){
Ostrich oc =new Ostrich();
oc.fly();
}
}
运行结果:
鸵鸟不会飞,只会跑
-
方法的重写需要子类中的方法头和父类的方法头完全相同,即有完全相同的方法名、返回类型和参数列表
-
如果不希望子类对父类继承来的方法进行重写,则需要在方法名前加关键字final
-
子类中重写的方法不能比它重写的父类中的方法有更严格的访问权限
class Bird{
public void fly(){
System.out.println("鸟儿在天空中自由飞行");
}
}
public class Ostrich extends Bird{
@Override
protected void fly() {//错误,子类重写方法比父类的方法权限要小
System.out.println("鸵鸟不会飞,只会跑");
}
public static void main(String[] args){
Ostrich oc =new Ostrich();
oc.fly();
}
}
2.重载是什么
- 定义:一个类中方法可以重载,不同类中,子类也可以重载父类中的方法。重载的方法与父类的方法被重载方法有相同的方法名和不同的参数形式。
- 举例
class People {
String name;
int age;
public People(){
}
public People(String name){}
public People(String name, int age) {//同类重载
this.name = name;
this.age = age;
}
public void show(){
System.out.println("People中的show方法");
}
}
public class Example5_2 {
public static void show(String sno) { //不同类的方法的重载
System.out.println(sno);
}
public static void main(String[] args) {
Example5_2.show("2");
}
}
3.重写和重载有什么区别
- 举例
class People {
public void show() {
System.out.println("People中的show方法");
}
protected void show(int i) {//同类中可重载
System.out.println("People中的show方法");
}
}
public class Example5_2 extends People {
@Override
public void show() {
System.out.println("子类中的show方法"); //重写
}
protected static void show(String sno) { //重载,对修饰方法没有要求
System.out.println(sno);
}
public static void main(String[] args) {
new Example5_2().show();
new Example5_2().show("2");
}
}
- 从上面的两个代码中可以看到比较明显的区别,接着归纳一下两者明显的区别
重写 | 重载 | |
---|---|---|
是否同类 | 不能同类 | 可同类也可不同类 |
方法名的参数形式是否相同 | 必须相同 | 必须不同 |
返回类型 | 必须相同 | 可同可不同 |
访问修饰符 | 子类不能比父类权限小 | 无要求 |
方法体异常 | 不能抛出新的异常或异常不能范围变大 | 无要求 |
构造方法 | 不能被重写 | 可以被重载 |
多态实现方式 | 实现运行时多态 | 实现编译时多态 |
关于最后一点可以详细见多态一节
- 有个比较有意思的例子
public class Example5_1 {
public boolean equals(Example5_1 other) {
System.out.println("use Example equals.");
return true;
}
public static void main(String[] args) {
Object o1 = new Example5_1();
Object o2 = new Example5_1();
Example5_1 o3 = new Example5_1();
Example5_1 o4 = new Example5_1();
/**
* 调用的是object的equal方法,比较的是堆地址,new新对象时堆地址不同,返回的是false
* 所以方法体里面的输出执行不到,所以此方法执行结束,什么都不输出
*/
if (o1.equals(o2)) {
System.out.println("o1 is equal with 02");
}
/**
* 调用的是Example5_1类中的equal方法,所有程序先输出equal方法里面的语句;
* 接着返回true;
* 然后执行到方法体里面输出"o3 is equal with 04"
*/
if (o3.equals(o4)) {
System.out.println("o3 is equal with 04");
}
}
}
程序的运行结果是:
use Example equals.
o3 is equal with 04
四、与继承相关的一些关键字
在子类对父类进行一些扩展之后,对于原来父类中的一些方法和属性,子类中要怎么去使用呢?这就要用到一些关键字了。
1.final
在java语言中,final关键字有3种用法:修饰变量、修饰方法和修饰类
(1)修饰变量
一个变量如果被final关键字修饰,则该变量就是常量,只能被赋值一次,不能被重新赋值
(2)修饰方法
一个方法如果不希望被子类重写,就要用final修饰,这种情况下,子类只能从父类继承该方法,无法对父类的此方法进行重写,好处就是防止子类对父类的关键方法进行重写时产生错误。
声明final可增加代码的安全性和正确性,也可提高类的运行效率。
class F{
final void show(){
System.out.println("a final method");
}
}
class S extends F{
void show(){//错误,不能重写父类用final修饰的方法
System.out.println("override a final method");
}
}
(3 )修饰类
如果一个类不希望被继承,则可在定义该类时加上关键字final
final class F{
...
}
class Y extends F{
...
}
该程序将编译错误,被final修饰的类无法被继承
(4)总结
- 被final修饰的类不能被继承
- 被final修饰的方法不能被子类重写
- 被final修饰的变量不能被第二次赋值
2.super
前面提到了子类隐藏父类的属性或重写了父类的方法,子类无法访问父类的被重写的方法和被隐藏的属性,那么Java中提供了关键字super调用被隐藏的属性和被重写的方法
(1)调用父类属性或方法
- 格式
super.父类中的的属性;
super.父类中的方法;
- 举例调用父类属性
class F {
int x = 1;
public void add(){
x++;
}
}
class S extends F {
int x = super.x + 1;
@Override
public void add() {
super.add();
}
}
(2)调用父类的构造方法
- 格式
super([参数列表])
子类可以继承父类中的普通方法和属性,那么父类的构造方法可以被继承吗?
- 调用无参的构造方法
class People {
String name;
int age;
public People(){
System.out.println("父类的构造方法");
}
}
class Student extends People{
private String school;
private int sno;
public Student(){
System.out.println("子类的构造方法");
}
}
public class Example5_2 {
public static void main(String[] args){
new Student();
}
}
程序的运行结果是:
父类的构造方法
子类的构造方法
该类实例化的是子类Student的对象,但是程序先调用父类People中的无参构造方法,之后才调用了子类本身的构造方法
实际上,在子类构造方法的第一行默认了一个super语句:
public Student(){
super;
System.out.println("子类的构造方法");
}
如果程序中指定了构造方法,默认构造方法不会再生成,需要手动在People类中增加一个无参的构造方法来保证使用无参构造方法实例化子类对象的正确性:
class People {
String name;
int age;
public People() {}//手动添加的无参构造方法
public People(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends People {
private String school;
private int sno;
public Student() {
}
}
public class Example5_2 {
public static void main(String[] args) {
new Student();
}
}
- 调用有参数的构造方法
如果要调用有参数的构造方法来完成Student对象的初始化,People类代码不变,要使程序不出现编译错误,可进行如下更改:
class People {
String name;
int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends People {
private String school;
private int sno;
public Student(String name,int age,String school) {
super(name,age);
this.school = school;
}
}
public class Example5_2 {
public static void main(String[] args) {
new Student("李白",20,"北京大学");
}
}
注意,用super调用父类的构造方法时只能放在程序中方法体的第一行,使用super调用父类的非构造方法时,是否放在首行根据需要确定。
- 结论
构造方法不能被继承,子类对象实例化的时候会默认调用父类的无参构造方法,之后再调用本类的相应的构造方法;
若父类有有参的构造方法时而无默认的构造方法时,子类必须在构造方法中指定调用父类中有参的构造方法或者在父类中手动添加默认(即无参)的构造方法,如果子类中不去调用,则程序会报错。
3.super和this的区别
(1)super和this调用构造方法的操作不能同时出现:因为调用构造方法时两者均要放在方法体的首行;
(2)super和this都是相对于某个对象而言,而static相对于类而言,所以不能在类方法中出现this或super
class F{
protected void show(){
System.out.println("F类中的show方法");
}
}
public class S extends F{
@Override
protected void show() {
super.show();
System.out.println("S类中的show方法");
}
public static void main(String[] args){
new S().show();
super.show();//错误,静态方法中不能使用super
}
}
五、继承中类的层次结构
因为有了继承,使用类和类之间有了一种层次关系,可以使用快捷键ctrl+h(其它快捷键可点此链接)查看继承关系
可以发现最底层是Object类,在Java中,Object类是所有类的父类,如果有个类没有直接指出父类,那么默认该类的父类为Object类,所以Object类中的所有Public方法可以被任何类使用,如上述例子中的equals方法。Object类中的常用方法可以查看jdk帮助文档,个人认为查看源码受益很多
六、总结上文思路
- 先总结继承的一些特点,再根据特点来学习
- 父类中的一些属性和方法会被隐藏
- 子类也可以自定义自己的方法和属性:重写和重载以及区别
- 对于一些被隐藏的属性和方法,子类如何使用:关键字super和this派上了用场
- 如果不想被继承:关键字final
七、继承与组合
继承实现了类的高度复用,减少了代码量,但也增加了子类与父类的耦合性,父类的细节完全暴露给了子类,子类可对父类的一些方法和数据恶意篡改,为了保证父类的良好封装性,应该做到:
- 父类的成员变量尽量初始化,有些属性要被继续则使用protected修饰
- 父类方法尽量用final或protected修饰
- 尽量不要在父类构造器中调用被子类重写的方法
最后写个demo,总结上述的一些知识点
/**
* @Author: qp
* @Time: 2021/8/7 20:03
* @Description IT公司有行政、财务、技术和销售4个部门;现在需要招聘技术和销售人员
* 分解:需要两个类:一个公司类,一个部门类,公司类包含部门信息,部门类包含部门名称、岗位、工资
* 公司类中包含部门信息,即把部门类作为公司类的一个组合成分,达到既可以使两个类都具有良好的封装性,又可以在公司类中复用部门类
*/
class Section {
private String scName; //部门名称
private String title; //招聘岗位
private float salary; //工资
public String getScName() {
return scName;
}
public void setScName(String scName) {
this.scName = scName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
class ITCompany {
private String name; //公司名称
private String city; //所在城市
private Section section; //部门
public ITCompany(String name, String city, Section sc) {
this.name = name;
this.city = city;
this.section = sc;
}
//显示公司招聘信息,组合式输出招聘信息,\t空Tab键
public void showResult() {
System.out.print(name+"\t"+city+"\t");
System.out.println(section.getScName() + "\t" + section.getTitle() + "\t" + section.getSalary());
}
}
public class TestCompose {
public static void main(String[] args) {
Section sc1 = new Section();
sc1.setScName("技术部");
sc1.setTitle("经理");
sc1.setSalary(9000);
Section sc2 = new Section();
sc2.setScName("技术部");
sc2.setTitle("开发人员");
sc2.setSalary(6500);
Section sc3 = new Section();
sc3.setScName("销售部");
sc3.setTitle("销售人员");
sc3.setSalary(5000);
System.out.println("公司名称\t城市\t部门\t岗位\t工资\t");
ITCompany itc1 = new ITCompany("xx公司", "北京", sc1);
itc1.showResult();
ITCompany itc2 = new ITCompany("xx公司", "北京", sc2);
itc2.showResult();
ITCompany itc3 = new ITCompany("xx公司", "北京", sc3);
itc3.showResult();
}
}
输出结果:
公司名称 城市 部门 岗位 工资
xx公司 北京 技术部 经理 9000.0
xx公司 北京 技术部 开发人员 6500.0
xx公司 北京 销售部 销售人员 5000.0
继承和组合都可以实现代码的复用,但是在具体使用的时候根据情况来选择:
- 如果表达的是一种"是(is-a)"的关系,即两个类之间是一般和特殊的关系,那么使用继承来实现
- 如果表达的是一种"有(has-a)"的关系,即一个类有另外一个类的成分,那么使用组合技术
人生就是一个不断继承与组合的事情,学无止境,人生无限!