5.1 子类与父类
举一个不恰当的例子,你在给别人做自我介绍时,有些东西不用全部说。
我叫张三,我有俩眼睛,一个鼻子一个嘴巴。
这时候你就要想了,这不明摆着废话吗? 雀氏这样,那么我们深入思考下,为什么这些基本情况不用从头说起?
这时候就有人要说了,你听听你说的是人
话吗?
关键点还真就在这个人
上面。
前面张三的自我介绍,这就是一个人的基本特征
,这些都是人类所共同拥有的
,这些东西就不用说。
那么当我们在编写一个类时,发现某个类
有我们所需要的行为和特征时。我们再次编写这样会造成代码冗余,为了解决这个问题,我们要考虑代码复用性
问题
我能不能把之前已经写好的类拿过来直接用呢,或者说将以前写过的类当做一个模板,直接套用呢?
能!!!! 这个黑魔法在Java的世界中称之为继承
。
继承是用已有类创建新的类的一种机制。
在使用继承前,我们要先把共同特征
抽象出来,比如说不论是男人还是女人,在正常情况下有两条腿,
我们又知道不管是男人还是女人同属于人
,那么综上所述,我们可以将父类 人
抽象出。
人:有两条腿
男人女人同继承自人
,所以男人女人都有两条腿。
那么我们来梳理下上面的关系
人(被继承者):叫做父类/超类/基类
男/女人(继承者):称之为子类/派生类
5.1.1 子类
上面我们已经知道了,子类是继承者。在Java中使用关键字
extends
来表示继承。
语法如下
// 父类:人
class People{
}
// 子类:男人 继承自 人
class Man extends People{
}
// 子类: 女人 继承自 人
class Woman extends People{
}
如果你看到一个类,上面带有
**extends**
关键字,那么这个类一定是一个子类
5.1.2 类的树形结构
Java中类的集是以树形
组织的。
如果你看一个类没有使用 extends
关键字 那么他同样是 Object
的子类
因为
Object类是Java中所有类的父类
那么上面的代码就可以描述为
- Object
- People
- Man
- Woman
5.2 子类的继承性
类可以有两种重要的成员:成员变量
和方法
。
子类成员有一部分是子类自己声明,定义的,另一部分是从父类继承过来的。
什么叫继承呢?
父类中的成员变量或者方法,可以直接在子类中去使用,这就是继承。
5.2.1 子类和父类在同一包中的继承性
如果子类和父类在同一个包中,那么子类继承了父类中不是
**private**
的成员变量和成员方法,作为自己的成员变量,成员方法。
package base.ch5;
class People {
private int legs = 2;
public void walk() {
System.out.println(this + ":我用" + legs + "条腿走路");
}
void drink(){
System.out.println("喝水水");
}
private void test(){
System.out.println("Hello,World!");
}
}
class Man extends People {
}
class Woman extends People {
}
public class ExtendsTest {
public static void main(String[] args) {
new People().walk();
new Man().walk();
// 友好方法可以被继承
new Man().drink();
new Woman().walk();
// new Woman().test()
}
}
base.ch5.People@60e53b93:我用2条腿走路
base.ch5.Man@5e2de80c:我用2条腿走路
喝水水
base.ch5.Woman@1d44bcfa:我用2条腿走路
看这里的子类继承了父类的walk,还有drink方法。 但是父类的 private
方法子类却无法继承。
5.2.2 子类和父类不在同一包中的继承性
子类和父类不在同一个包中时,子类只能继承父类
**protected**
和**public**
访问权限成员变量/方法,作为子类的成员变量/方法。
这里要注意的是 子类和父类不在同一个包中 protected
方法虽然可以被子类调用,但是不可以被子类实例化对象直接调用,需要在子类通过super
关键字间接调用。
5.2.3 继承关系的UML图
如果一个类是另一个类的子类,那么UML中通过
**实线**
连接两个类的UML图,终点用**空心三角结束**
5.2.4 protected 的进一步说明
- 当子类和父类在同一个包中:子类可以直接访问父类的protected方法
- 当子类和父类不在同一个包中: 子类可以通过super关键字来访问父类的protected方法
5.3 子类与对象
instanceof运算符
instanceof运算符是Java独有的
双目
运算符,左边操作元是对象,右边操作元是类当左边操作元的对象是右边操作元的
**类或者其子类**
创建时表达式值为**true**
class Main {
public static void main(String[] args) {
System.out.println(new Man() instanceof People);
}
}
5.4 成员变量的隐藏和方法重写
5.4.1 成员变量的隐藏
当子类的成员变量和继承自父类的成员变量
名字相同
时(类型可以不同),子类会隐藏掉继承自父类的成员变量
- 子类
自己定义的方法操作与父类同名的成员变量
是子类重新生成的成员变量 - 子类调用
从父类继承的成员方法操作与父类同名的成员变量
一定是被子类隐藏的成员变量(父类的成员变量)
示例代码
public class Animal {
public int legs =2;
void info(){
System.out.println("[super] legs:"+legs);
}
}
class Rabbit extends Animal{
// 和父类成员变量同名
int legs = 4;
// 子类自己的方法
void intro(){
System.out.println("[child] legs:"+legs);
}
}
class AnimalTest{
public static void main(String[] args) {
Rabbit rabbit = new Rabbit();
// 子类调用继承自父类的成员方法 访问的是隐藏的成员变量
rabbit.info();
// 子类调用自己的成员方法 访问的是 子类的成员变量
rabbit.intro();
}
}
[super] legs:2
[child] legs:4
5.4.2 方法重写
同样的子类可以通过重写来隐藏继承自父类的方法,这个过程称之为
重写 (Override)
方法重写的规则
如果子类有权继承自父类的某个方法,那么子类就有权利重写这个方法。方法重写就是子类中定义一个和父类方法**名字**
,**参数列表**
,**返回值**
一模一样的方法来”替换”掉父类方法
方法重写的目的
那就举个栗子叭,我们都知道兔子和老虎都属于动物,那么我们就可以说 兔子和老虎都继承自动物,接下来抽取共同特性,动物都会吃,所以父类中定义一个eat
方法。
那么根据Java中继承的规则,兔子和老虎都继承了这个eat的方法。
我们要思考这里面存在的问题 :兔子是食草动物
,老虎是食肉动物
。
所以我们要在兔子这个类中用一个新的方法去”替换”掉父类中写的eat
,让兔子去食草。
同理我们也要在老虎中用一个新的方法去”替换”掉eat
方法,让老虎食肉。
需要注意的一点是这里的”替换”,是要满足上面方法重写规则呦,我们把这个替换的过程称之为方法的重写
public class Animal {
void eat(){
System.out.println("动物会吃");
}
}
class Rabbit extends Animal{
@Override
void eat() {
System.out.println("兔子食草");
}
}
class Tiger extends Animal{
@Override
void eat() {
System.out.println("老虎食肉");
}
}
class OverrideTest{
public static void main(String[] args) {
Animal animal = new Animal();
animal.eat();
Rabbit rabbit = new Rabbit();
rabbit.eat();
Tiger tiger = new Tiger();
tiger.eat();
}
}
这时候你就要问了 这里的 @Override
是什么呀?
其实这里的@Override
是Java的一个注解,暂时不用做过多的研究,你只需要知道 在你重写的方法上面加上这个,他就会对你的方法进行重写规则检查,在这里起一个检查
的作用。
重写时注意的事项
在重写父类方法时,不允许
降低
方法的访问权限,但是可以提高访问权限。
来复习下 什么是访问权限修饰符?
访问权限从高到低依次是什么?
分别代表什么作用?
5.5 super关键字
5.5.1 用super关键字操作被隐藏的成员变量和方法
子类隐藏了父类的成员变量,那么子类创建的悐就不再拥有该成员变量,如果我就是要访问咋办呢? 可以使用
super
关键字
当子类想要调用父类中被隐藏的成员变量/方法时,可以使用**super**
关键字
class AnimalI{
int legs = 2;
void info(){
System.out.println("[super] legs:"+legs);
}
}
class Frog extends AnimalI{
int legs = 4;
@Override
void info() {
System.out.println("[child] legs:"+legs);
super.info();
System.out.println("[super-legs]:"+super.legs);
}
}
public class SuperTest {
public static void main(String[] args) {
new AnimalI().info();
System.out.println("---------");
new Frog().info();
}
}
[super] legs:2
---------
[child] legs:4
[super] legs:2
[super-legs]:2
5.5.2 用super调用父类的构造方法
当子类构造方法创建一个子类对象时,会首先调用父类的构造方法。
class Parent{
public Parent() {
System.out.println("Parent:无参构造被触发");
}
}
class Child extends Parent{
public Child() {
System.out.println("Child:无参构造被触发");
}
}
public class ConstructorTest {
public static void main(String[] args) {
new Child();
}
}
Parent:无参构造被触发
Child:无参构造被触发
由于子类不能继承父类构造方法,因此子类在构造方法内要通过super关键字来调用父类构造方法。
super()
,并且super必须是子类构造方法中的第一条语句
即便你没有写Java也会默认生成一个 super()
即默认无参构造方法
public class Student {
protected String name;
protected int age;
protected String no;
public Student(String name, int age, String no) {
this.name = name;
this.age = age;
this.no = no;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", no='" + no + '\'' +
'}';
}
}
class StudentA extends Student{
protected String level;
public StudentA(String name, int age, String no, String level) {
// 调用父类构造方法
super(name, age, no);
this.level = level;
}
@Override
public String toString() {
return "StudentA{" +
"name='" + name + '\'' +
", age=" + age +
", no='" + no + '\'' +
", level='" + level + '\'' +
'}';
}
}
class StudentTest{
public static void main(String[] args) {
Student lisi = new Student("李四", 21, "00002");
System.out.println(lisi);
StudentA studentA = new StudentA("张三", 22, "0001", "大三");
System.out.println(studentA);
}
}
Student{name='李四', age=21, no='00002'}
StudentA{name='张三', age=22, no='0001', level='大三'}
这里有亿点点细节
- 如果你没写构造方法那么Java会给你生成一个默认的无参构造方法
- 如果你写了构造方法,那么Java不会为你生成默认无参构造方法
如果父类中没有默认无参构造方法,那么子类中一定要通过 super 来调用父类的有参构造方法
5.6 final关键字
final
是一个关键字,可以用来修饰类
,成员变量/方法
,局部变量
5.6.1 final类
被 final关键字修饰的类,不能够被继承
5.6.2 final 方法
如果父类中一个方法被 final关键字修饰那么子类中无法重写该方法
5.6.3 常量
如果一个类的成员变量或者局部变量被final关键字修饰,那么它将成为一个常量
何为常量?
常量顾名思义就是 常数量
是不可以被修改的。
5.7 对象的上转型对象
我们经常说,“老虎是动物”, 这里的老虎是子类
,动物是父类
,那么二者之间的关系就是 “is-a” 关系。
当我们说老虎是动物时,老虎将丧失掉老虎独有的属性和功能。
我们总不能说老虎会爬树,所有的动物都会爬树吧,显然太过于绝对。
当我们使用这种上溯思维时,子类看做父类时,子类将丧失自己的行为和特征
public class Animal {
protected void eat(){
System.out.println("动物会吃");
}
}
class Tiger extends Animal{
protected void climb(){
System.out.println("老虎会爬树");
}
}
class Test{
public static void main(String[] args) {
Tiger tiger = new Tiger();
tiger.eat(); // 老虎是动物所以会吃
tiger.climb(); // 老虎独有特性:会爬树
System.out.println("--------------");
Animal animal = tiger; // 将老虎看做动物,老虎将丧失爬树技能
animal.eat();
// animal.climb(); // 错误:方法不存在
}
}
将一个子类的引用传递给父类子类将丢失自己的特性,这个过程称之为
对象的上转型
那么上转型对象都有哪些特性呢?
- 上转型对象不能操作子类新增的成员变量/方法 (丢失掉的特性)
- 上转型对象可以访问子类继承或隐藏的成员变量,也可以调用子类继承的方法,或者子类重写的方法
- 不要将父类创建的对象和子类对象的上转型混淆
- 可以将对象的上转型对象再强制转换到一个子类对象,此时子类对象又具备了子类的所有属性和功能。
- 如果子类重写了父类的静态方法,那么子类对象的上转型对象不能调用子类重写的静态方法,只能调用父类的静态方法(因为JVM静态方法区原因)
public class Apes {
protected int foot =2;
public void walk(){
System.out.println("我是类人猿我用"+foot+"条腿走路");
}
protected static void sayHello(){
System.out.println("wo~wo~wo~");
}
}
class People extends Apes{
@Override
public void walk() {
System.out.println("我是人我用"+foot+"条腿走路");
}
protected static void sayHello(){
System.out.println("你好啊,我是佩奇!");
}
}
class ApesTes{
public static void main(String[] args) {
Apes apes = new Apes();
People people = new People();
apes.walk();
people.walk();
// ----------上转型对象 -------
apes = people;
// 上转型对象调用的是父类的静态方法,而不是子类重写的静态方法
apes.sayHello();
// 上转型对象可以强制转换为子类对象,此时丢失的特性恢复
People p =(People) apes;
p.walk();
p.sayHello();
}
}
我是类人猿我用2条腿走路
我是人我用2条腿走路
wo~wo~wo~
我是人我用2条腿走路
你好啊,我是佩奇!
上面代码可能理解起来有些困难,建议多读几遍
Apes p = new People(); // 这也是上转型对象
5.8 继承与多态
其实在前面的上转型对象中我们就已经接触到了多态。
老虎食肉,兔子食草,他们都是继承自动物,并且可通过重写eat
方法实现,那么我们将这两个子类的引用放到父类对象中,也就是我们说的上转型对象,就实现了多态
。
同样是eat
方法,根据对象上转型的规则,当我们将老虎赋值给动物时,调用eat
方法时调用的是老虎自己的eat
方法,那么兔子也是如此。
这个过程中产生了多种状态,所以称之为多态
那么多态的具体概念是什么呢?
父类么某个方法被子类重写时,可以产生自己的功能行为。
当老虎重写父类的eat
方法时,老虎食肉
当兔子重写父类的eat
方法时,兔子食草
public class Animal {
protected void eat(){
System.out.println("动物会吃");
}
}
class Tiger extends Animal{
@Override
protected void eat() {
System.out.println("老虎食肉");
}
}
class Rabbit extends Animal{
@Override
protected void eat() {
System.out.println("兔子食草");
}
}
class Test{
public static void main(String[] args) {
Animal tiger = new Tiger();
Animal rabbit = new Rabbit();
tiger.eat();
rabbit.eat();
System.out.println("------------");
// 这时候你就要问了 这里即便不使用上转型对象也可以实现啊 为啥还要上转型对象呢?
// 我们将代码稍微变动下,这样是不是提高了代码复用率呢,没错多态就是这么的神奇
invoke(tiger);
invoke(rabbit);
}
public static void invoke(Animal animal){
animal.eat();
}
}
老虎食肉
兔子食草
------------
老虎食肉
兔子食草
多态是一个重点+难点一定要掌握牢
5.9 abstract类与abstract方法
用关键字
abstract
修饰的类称之为abstract类抽象类
abstract class A{
}
用关键字
abstract
修饰的方法称之为abstract方法抽象方法
abstract void eat();
- 对于abstract方法,只允许声明,不允许实现,也就是说abstract方法,没有方法体,
{}
- 不允许使用
static
修饰abstract方法,abstract方法必须是实例方法 - 不允许使用 final 和 abstract方法同时修饰一个方法或者类(思考下为什么不允许?)
abstract类中可以有非abstract方法
和普通类相比,abstract类中,可以有abstract方法,也可以有非abstract方法。
public abstract class Calculate {
// abstract 方法 (没有方法体)
abstract double add(double a,double b);
// 非 abstract 方法 (有方法体)
double sub(double a,double b){
return a-b;
}
}
abstract类中也可以没有abstract 方法。
abstract类不能使用**new**
运算符创建对象
对于abstract类,不能使用new运算符创建该类的对象,如果一个非抽象类是某个抽象类的子类,那么他必须重写父类的抽象方法,这就是为什么不允许使用final和abstract同时修饰同一个方法或类的原因。
abstract类的子类
- 如果一个非抽象类是一个抽象类的子类,他必须重父类的抽象方法。
- 如果一个抽闲类是一个抽象类的子类,他可以重写父类的抽象方法,也可以继承父类的抽象方法。
public abstract class Animal {
// 抽象方法不需要方法体
abstract void eat();
// 抽象类中可以有非抽象方法
void walk(){
System.out.println("动物会跑");
}
}
abstract class Cats extends Animal{
// 重写父类抽象方法
@Override
void eat() {
System.out.println("猫科动物都食肉");
}
// 继承了 walk 方法
}
class Cat extends Cats{
@Override
void walk() {
System.out.println("猫会跑");
}
}
class Test{
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
cat.walk();
}
}
猫科动物都食肉
猫会跑
abstract类的对象作为上转型对象
可以使用abstract类声明对象,尽管不能使用 new
运算符创建该对象,但该对象可以成为其子类对象的上转型对象。
理解abstract类
- 抽象类可以抽象出 重要的行为标准,该行为用抽象方法表示。 也可以说 抽象类定义了子类
**必须**
要有的行为准则 - 抽象类声明的对象可以成为其子类的对象的上转型对象,调用子类重写的方法,体现了子类根据抽象类行为准则”约束自己”
加深理解抽象类
男孩找女朋友,对女孩提取一些”约束要求”,例如
会做饭
,勤俭持家
,等一些行为标准。
根据这些行为标准,我们写出抽象类
public abstract class GirlFriend {
abstract boolean cooking(); // 是否会做饭
abstract boolean husband(); // 是否勤俭持家
}
class GirlA extends GirlFriend {
@Override
boolean cooking() {
System.out.println("A:我会做饭");
return true;
}
@Override
boolean husband() {
System.out.println("A:胡吃海喝,我妈说彩礼30W");
return false;
}
@Override
public String toString() {
return "GirlA";
}
}
class GirlB extends GirlFriend {
@Override
boolean cooking() {
System.out.println("B:会做饭");
return true;
}
@Override
boolean husband() {
System.out.println("B:勤俭持家,相夫教子");
return true;
}
@Override
public String toString() {
return "GirlB";
}
}
class GirlC extends GirlFriend {
@Override
boolean cooking() {
System.out.println("C:不会做饭");
return false;
}
@Override
boolean husband() {
System.out.println("B:天天酒吧,KTV");
return false;
}
@Override
public String toString() {
return "GirlC";
}
}
class Boy {
// 结婚
public boolean tryMarried(GirlFriend gf) {
if ((!gf.cooking() || !gf.husband())) {
System.out.println("Boy:不合适哦," + gf);
return false;
}
System.out.println("Boy:" + gf + ",真是一个贤妻良母");
return true;
}
public static void main(String[] args) {
Boy boy = new Boy();
boy.tryMarried(new GirlA());
boy.tryMarried(new GirlB());
boy.tryMarried(new GirlC());
}
}
5.10 面向抽象编程
在设计程序时,经常会用到abstract类,原因是 抽象类只需要关系操作,无需关系实现细节,它实现了对行为的约束,从而让开发者把精力放到程序设计上,而不是纠结内部实现。(将这些细节留给子类的设计者)。
使用多态进行程序设计核心技术之一:使用上转型对象,即将abstract类声明的对象作为其子类的上转型对象。
所谓面相抽象编程指的是当设计某一个重要的类时,不让该类指向对应的类,而是应当指向抽象类,那么前面的动物-老虎-兔子,我们就可以改写成抽象类。
public abstract class Animal {
abstract void eat(); // 动物会吃
abstract void run(); // 动物也会跑
}
class Cat extends Animal{
@Override
void eat() {
System.out.println("猫食肉");
}
@Override
void run() {
System.out.println("猫会爬树");
}
}
class Rabbit extends Animal{
@Override
void eat() {
System.out.println("兔子食草");
}
@Override
void run() {
System.out.println("兔子会跳跃");
}
}
class Test{
public static void invoke(Animal an){
an.eat();
an.run();
}
public static void main(String[] args) {
invoke(new Cat());
invoke(new Rabbit());
}
}
看吧这里我们用一个方法就完成了多态的实现
同一个方法,作用在不同的对象上,导致了不同的结果
通过面向抽象编程目的是为了应对用户需求的编号,比如说上面的例子中我们加一个FIsh他也同样满足动物的特征,以不变应万变。
class Fish extends Animal{
@Override
void eat() {
System.out.println("我是食草/肉动物");
}
@Override
void run() {
System.out.println("我会游");
}
}
class Test{
public static void invoke(Animal an){
an.eat();
an.run();
}
public static void main(String[] args) {
invoke(new Fish());
}
}
5.11 开闭原则
所谓开闭原则,就是说对设计的系统 扩展开放,修改关闭。
当我们给系统新增模块时不应当修改现有模块,从而提高系统稳定性
先看一个不使用开闭原则的例子
public abstract class Pen {
abstract void write(); // 笔的特性: 写
}
class BluePen extends Pen{
@Override
void write() {
System.out.println("我能书写蓝色笔迹");
}
}
class RedPen extends Pen{
@Override
void write() {
System.out.println("我能书写红色笔迹");
}
}
class Test{
public static void main(String[] args) {
Pen bluePen = new BluePen();
Pen redPen = new RedPen();
bluePen.write();
redPen.write();
}
}
我能书写蓝色笔迹
我能书写红色笔迹
明显这个系统不易维护,如果我们要写一个 紫色的笔,我们就要写一个新的类,。。。。。 然后陷入了无限的循环,那这样我们的笔就成了一个一次性的笔
为什么说上面的代码不满足开闭原则,因为我们每次新添加一个类,都要修改 write 方法,显然这样是不合乎常理的。
那么如何改善这个问题呢?
我们深入思考下,笔里面是有笔芯的,笔芯可替换,那么我们要根据开闭原则,来更换笔芯,而不是增加笔。
public abstract class Refill {
protected String color;
abstract void write(); // 笔芯的特性书写
// 笔芯构造方法
public Refill(String color) {
this.color = color;
}
}
// 默认笔芯
class DefaultRefill extends Refill {
@Override
void write() {
System.out.println("我能书写:" + color);
}
public DefaultRefill(String color) {
super(color);
}
}
class Pen {
private Refill refill;
// 更换笔芯
public void setRefill(Refill refill) {
this.refill = refill;
}
public void write() {
refill.write();
}
}
class Test {
public static void main(String[] args) {
Pen pen = new Pen();
pen.setRefill(new DefaultRefill("红色"));
pen.write();
pen.setRefill(new DefaultRefill("蓝色"));
pen.write();
}
}
我能书写:红色
我能书写:蓝色
这样在笔写的时候,调用的是笔芯的write
方法,即便我们换了笔芯,也不用修改pen 里面的东西。这就是开闭原则。
这样的抽象能力不是一下子就能学会的,需要多练习,多思考,从事物本质去思考。
5.12 小结
- 继承是一种由现有类创建新的类的机制。
- 子类继承父类后,父类的一些 可被允许 的成员方法/变量,会被子类继承。(访问权限修饰符允许的范围内)
- 子类继承父类的方法,只能操作子类继承和隐藏的成员变量
- 子类重写或新增的方法,能操作子类继承和声明的成员变量,但能直接操作被隐藏的成员变量(需要使用super关键字)
- 多态是面向对象编程的一个重要特性,子类可以实现多态。子类根据需要重写父类方法。
- 在设计多态程序时,要熟练使用面相抽象编程的思想,以便体现”开闭原则”