一、继承
1、为什么需要继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体
但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑
在进行对象的创建的时候,我们肯定会经历对不同的对象,有重复创建的过程
class Dog {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
}
class Cat {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
public void miaomiao() {
System.out.println(this.name + "正在喵喵叫!");
}
}
比如这段代码,对一下都进行了重复创建

为了简化代码,这里我们引入了继承这个定义
2、继承概念
继承是一种思想。 对共性进行抽取,从而达到了代码的复用效果
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性的基础上进行扩展,增加新功能,这样产生新的类,称派生类
继承主要解决的问题是:共性的抽取,实现代码复用

3、继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字
修饰符 class 子类 extends 父类 {
// ...
}
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Dog extends Animal{
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
}
//扩展 拓展
class Cat extends Animal{
public void miaomiao() {
System.out.println(this.name + "正在喵喵叫!");
}
}
注意:
- 子类会将父类中的成员变量或者成员方法继承到子类中了
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
4、父类成员访问
1. 子类中访问父类的成员变量
(1)子类和父类不存在同名成员变量
class Base {
public int a;
public int b;
}
class Derived extends Base {
public int c;
public void method() {
a = 10;
b = 20;
c = 30;
}
}
public class Test2 {
public static void main(String[] args) {
Derived derived = new Derived();
}
}

(2)子类和父类成员变量同名
/**
*同名的成员变量
*/
class Base {
public int a = 9;
public int b = 99;
}
class Derived extends Base {
public int a = 88;
public void method() {
System.out.println("a: " + a);
System.out.println("b: " + b);
}
}
public class Test2 {
public static void main(String[] args) {
Derived derived = new Derived();
derived.method();
}
}

在子类方法中 或者 通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
- 成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找
2. 子类中访问父类的成员方法
(1) 成员方法名字不同
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
}
public class Derived extends Base{
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodB(); // 访问子类自己的methodB()
methodA(); // 访问父类继承的methodA()
// methodD(); // 编译失败,在整个继承体系中没有发现方法methodD()
}
}
成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时
再到父类中找,如果父类中也没有则报错
(2)成员方法名字相同
class Base {
public void method() {
System.out.println("B::method()");
}
}
class Derived extends Base {
public void method2() {
System.out.println("Derived::method()");
}
public void test() {
method();
method2();
}
public void method() {
System.out.println("Derived::method()");
}
}
public class Test2 {
public static void main(String[] args) {
Derived derived = new Derived();
derived.test();
}
}
说明:
- 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
- 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错
5、super关键字
如果子类中存在与父类中相同的成员时,那如何在子类中访问父类相同名称的成员呢?
直接访问是无法做到的,Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。
class Base {
int a = 99;
public void method() {
System.out.println("B::method()");
}
}
class Derived extends Base {
int a = 199;
public void method2() {
System.out.println("Derived::method()");
}
public void method() {
System.out.println("Derived::method()");
}
public void test() {
super.method();
method2();
System.out.println(a);
System.out.println(this.a);//是子类自己
System.out.println(super.a);
}
}
public class Test2 {
public static void main(String[] args) {
Derived derived = new Derived();
derived.test();
}
}


6、子类构造方法
父子父子,先有父再有子
即:子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法
class Animal {
//private String name;//访问修饰限定符只能决定访问权限,不能决定能不能被继承
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
/* public Animal() {
}*/
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Dog extends Animal{
public Dog() {
super("yuanyuan",10);
}
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
}
如果父类存在一个带参数的构造方法,但是子类没有对父类进行初始化,就会报错
或者说因为父类没有一个不带参数的构造方法所以会出现错误(救急不救穷)
子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。
父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
注意:
- 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
- 在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中第一条语句。
- super(…)只能在子类构造方法中出现一次,并且不能和this同时出现
7、super和this
super和this都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
- 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造
方法中出现 - 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有
8、再谈初始化
class Animal {
//private String name;//访问修饰限定符只能决定访问权限,不能决定能不能被继承
public String name;
public int age;
static {
System.out.println("Animal::static{静态代码块}");
}
{
System.out.println("Animal::{实例代码块}");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Animal(String name, int age)");
}
/* public Animal() {
}*/
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
}
class Dog extends Animal{
/*public Dog() {
super();
}*/
static {
System.out.println("Dog::static{静态代码块}");
}
{
System.out.println("Dog::{实例代码块}");
}
public Dog(String name,int age) {
super(name,age);//super 只能写一个 和this一样只能放在第一行
//调用父类的初始方法 帮助初始化 子类从父类继承过来的成员 并不会生成父类对象
System.out.println("Dog(String name,int age)");
}
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
}
//扩展 拓展
class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
System.out.println("Cat(String name,int age)");
}
public void miaomiao() {
System.out.println(this.name + "正在喵喵叫!");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("diandian",2);
}
public static void main1(String[] args) {
Dog dog = new Dog("huahua",2);
dog.bark();
dog.eat();
System.out.println("=============");
Cat cat = new Cat("mimi",3);
cat.miaomiao();
cat.eat();
}
}

通过分析执行结果,得出以下结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
9、protected 关键字
在类和对象章节中,为了实现封装特性,Java中引入了访问限定符
主要限定:类或者类中成员能否在类外或者其他包中被访问

package demo1;
public class Test3 {
protected int a = 1999;
}
package demo2;
import demo1.Test3;
public class Test extends Test3 {
public void func() {
System.out.println(super.a);
}
public static void main(String[] args) {
Test test = new Test();
test.func();
//System.out.println(test3.a);
}
}

如果想在 不同包的子类中调用,需要用到 protected 关键字,并且需要用 super 去调用
并且所继承的的父类,一定是public 的父类
10、继承方式
我们先看一张简单的图

但是,我们在后面会有需求,当继承到某个层次之后,我们不建议再进行继承了

java 当中是不支持多继承的
如果真的想要继承多个,就要运用接口
11、final 关键字
final关键可以用来修饰变量、成员方法以及类
1. 修饰变量或字段,表示常量(即不能修改)

此时 a 就叫做常量,a 只能被初始化一次
2. 修饰类:表示此类不能被继承

3. 修饰方法:表示该方法不能被重写
12、继承与组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车

class Student {
}
class Teacher {
}
class School {
public Student[] students;//默认是null
public Teacher[] teachers;
public int a;
public School() {
this.students = new Student[];
this.teachers = new Teacher[];
this.a = 10;
}
}
这种也可以的达到复用,这种方法叫做组合
二、多态
1、多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果
2、多态实现条件
java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写
- 通过父类的引用调用重写的方法
当完成这三个部分的时候就发生了动态绑定

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
public Dog(String name,int age) {
//帮助初始化了子类从父类继承下来的成员
super(name,age);
}
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Dog("yuanyuan",10);
animal.eat();
}
public static void func1(Animal animal) {//2.方法的参数,传参的时候进行向上转型
}
public static Animal func2() {//3.返回值 向上转型
Dog dog = new Dog("yuanyuan",19);
return dog;
}
public static void main3(String[] args) {
Dog dog = new Dog("yuanyuan",19);//1.直接赋值
func1(dog);
Animal animal = func2();
}
/**
*什么是向上转型
* @param args
*/
public static void main2(String[] args) {
/*Dog dog = new Dog("yuanyuan",19);
//animal这个引用指向了dog这个引用 所指向的对象
Animal animal = dog; */
Animal animal = new Dog("yuanyuan",10);
animal.eat();//父类自己的一个eat
//animal.bark();
}
public static void main1(String[] args) {
Dog dog = new Dog("yuanyuan",19);
dog.eat();
dog.bark();
System.out.println("============");
/*
Animal animal = new Animal("huahua",10);
animal.eat();
animal.bark();
结论:通过父类引用 只能调用父类自己的成员 方法 或者 成员变量
*/
}
}
3、重写
重写(override):也称为覆盖。
重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。
即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
也就是说子类能够根据需要实现父类的方法
实现重写的基本原则:
- 最基本的是,返回值、参数列表、方法名必须是一样的
- 被重写的方法的访问修饰限定符,在子类中要大于等于父类的
- 被 private 修饰的方法 是不可以被重写的
- 被 static 修饰的方法 是不可以被重写的
- 被 final 修饰的方法 是不可以被重写的
- 构造方法 也是不可以被重写的
Object类 是所有类的父类
重写和重载的区别

方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
4、向上转移和向下转型
1. 向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用
语法格式:父类类型 对象名 = new 子类类型()
Animal animal = new Cat("mimi",2);
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
三个常见的向上转型的时机:

这里我们根据重写和向上转型的定义,重新看一下多态的定义
会发现清晰很多

class Animal {
public String name;
public int age;
public void eat() {
System.out.println(this.name + "正在吃饭!");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal {
public Dog(String name,int age) {
//帮助初始化了子类从父类继承下来的成员
super(name,age);
}
public void bark() {
System.out.println(this.name + "正在汪汪叫!");
}
@Override
public void eat() {
System.out.println(this.name + "正在吃狗粮");
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void miaomiao() {
System.out.println(name + "咪咪叫");
}
@Override
public void eat() {
System.out.println(this.name + "正在吃猫粮!"); }
}
public class Test {
public static void eatFunc(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Dog dog = new Dog("yuanyuan",19);
eatFunc(dog);
Cat cat = new Cat("mimi",2);
eatFunc(cat);
}
public static void main4(String[] args) {
Animal animal = new Dog("yuanyuan",19);
animal.eat();
Animal animal1 = new Cat("mimi",2);
animal.eat();
/*Animal animal = new Dog("yuanyuan",10);
animal.eat();*/
}
}

向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法
2. 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法
但有时候可能需要调用子类特有的方法
此时:将父类引用再还原为子类对象即可,即向下转换
但是向下转型非常不安全
public static void main(String[] args) {
Animal animal = new Dog("yuanyuan",3);
/*Dog dog = (Dog)animal;
dog.bark();*/
Cat cat = (cat)animal;
cat.miaomiao();
}

因为在运行的时候向上转型为了狗,这里再次转换成猫,就会发生类型转换异常
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。
Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换
public static void main(String[] args) {
Animal animal = new Dog("yuanyuan",3);
/*Dog dog = (Dog)animal;
dog.bark();*/
//如果animal 引用的对象 是Cat对象 的实例
if(animal instanceof Cat) {
Cat cat = (Cat)animal;
cat.miaomiao();
}else {
System.out.println("haha");
}
}
5、多态的优缺点
如果说到多态的优点,很多人都会说,高级!!!
那么这是为什么呢?
接下来我们先看一个没有用多态的代码例子
class Shape {
public void draw() {
System.out.println("画一个图形");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("矩形!");
}
}
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("三角形!");
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("圆圈!");
}
}
public class Test {
public static void main(String[] args) {
Cycle cycle = new Cycle();
Rect rect = new Rect();
Triangle triangle = new Triangle();
String[] strings = {"cycle","rect","cycle","rect","triangle"};
for (String x :strings) {
if (x.equals("cycle")){
cycle.draw();
}else if (x.equals("rect")) {
rect.draw();
}else if (x.equals("triangle")) {
triangle.draw();
}
}
}
}
在这块代码中,我们运用了大量的 if else 语句,这样增加了代码的“圈复杂度”。
什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.
如果一个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
我们可以看出上边的代码多且复杂,但是如果我们运用多态就会简化
public static void main(String[] args) {
/* Shape shape = new Cycle();
Shape shape1 = new Triangle();
Shape shape2 = new Rect();*/
Shape[] shapes = {new Cycle(),new Rect(),new Cycle(),new Rect(),new Triangle()};
for (Shape shape : shapes) {
shape.draw();
}
}
我们对 main 方法内部的代码进行简化
运用多态创建一个数组
并且对数组进行遍历,由此就可以得到打印的图形
由此可知
多态的优点
- 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
- 可扩展能力更强
多态缺陷:代码的运行效率降低。
- 属性没有多态性
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性 - 构造方法没有多态性
6、避免在构造方法中调用重写的方法
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}

构造 D 对象的同时, 会调用 B 的构造方法.
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0. 如果具备多态性,num的值应该是1.
所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
因此:
“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题
本文围绕Java的继承和多态展开。继承可实现代码复用,介绍了其语法、成员访问、super关键字等内容,还提及final关键字和继承与组合。多态指不同对象完成同一行为有不同状态,实现需满足继承、重写、父类引用调用重写方法等条件,同时分析了优缺点及相关注意事项。

被折叠的 条评论
为什么被折叠?



