目录
1.继承
1.1为什么要继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是 现实世界错综复杂,事物之间可能会存在一些关联。
比如:猫和狗,它们都是动物。
下面用代码来描述
package com.baidu.demo3;
/**
* Created with IntelliJ IDEA
* Description:
* User:34398
* Date:2024-03-17
* Time:9:54
*/
class Dog{
//共同属性
private int age;
private String name;
private String color;
//独特属性
public void bark(){
System.out.println("汪汪汪");
}
}
class Cat{
//共同属性
private int age;
private String name;
private String color;
//独特属性
public void mew(){
System.out.println("喵喵喵");
}
}
我们发现代码出现了重复的情况,也就是共有属性这里。
当我们需要描述多更多类型的猫科动物和犬科动物时,代码的重复率就会不断提高,为了解决这类问题,提高代码的复用率,JAVA引进了继承的概念。
1.2继承的概念
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特 性 的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了 由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用。
上述图示中,Dog和Cat都继承了Animal类.
其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的 子类/派生类.
继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可.
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态。
1.3继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:
对1.2场景使用继承的方式,重新设计
注意:
1. 子类会将父类中的成员变量或者成员方法继承到子类中了
2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1.4父类成员访问
1.4.1
1.子类和父类不存在同名成员变量
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 访问子类自己的c
}
}
2. 子类和父类成员变量同名
public class Base {
int a;
int b;
int c;
}
/
public class Derived extends Base{
int a; // 与父类中成员a同名,且类型相同
char b; // 与父类中成员b同名,但类型不同
public void method(){
a = 100; // 访问父类继承的a,还是子类自己新增的a?
b = 101; // 访问父类继承的b,还是子类自己新增的b?
c = 102; // 子类没有c,访问的肯定是从父类继承下来的c
// d = 103; // 编译失败,因为父类和子类都没有定义成员变量b
}
}
在子类方法中 或者 通过子类对象访问成员时:
如果访问的成员变量子类中有,优先访问自己的成员变量。
如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
1.4.2 子类中访问父类的成员方法
1.父类和子类成员方法不同名
与1.4.1结论相同
2.父类和子类成员方法同名
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
public void methodA(int a) {
System.out.println("Derived中的method(int)方法");
}
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodA(); // 没有传参,访问父类中的methodA()
methodA(20); // 传递int参数,访问子类中的methodA(int)
methodB(); // 直接访问,则永远访问到的都是子类中的methodB(),基类的无法访问到
}
}
说明:
1.通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错。(就近原则)
2.通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用 方法适传递的参数选择合适的方法访问,如果没有则报错;
问题:如果子类中存在与父类中相同的成员时,那如何在子类中访问父类相同名称的成员呢?
1.5 super关键字
该关键字主要作用:在子类方法中访问父类的成员。
public class Base {
int a;
int b;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
int a; // 与父类中成员变量同名且类型相同
char b; // 与父类中成员变量同名但类型不同
// 与父类中methodA()构成重载
public void methodA(int a) {
System.out.println("Derived中的method()方法");
}
// 与基类中methodB()构成重写(即原型一致,重写后序详细介绍)
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
// 对于同名的成员变量,直接访问时,访问的都是子类的
a = 100; // 等价于: this.a = 100;
b = 101; // 等价于: this.b = 101;
// 注意:this是当前对象的引用
// 访问父类的成员变量时,需要借助super关键字
// super是获取到子类对象中从基类继承下来的部分
super.a = 200;
super.b = 201;
// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
methodA(); // 没有传参,访问父类中的methodA()
methodA(20); // 传递int参数,访问子类中的methodA(int)
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
super.methodB(); // 访问基类的methodB()
}
}
注意事项:
1.super关键字,只能在非静态方法中使用
原因:静态方法不依赖于对象,在类加载是创建。
成员变量只有在实例化对象时才被建立。静态方法存在时还内有实例化对象,无法使用super关键字去访问对象。编译器报错。
2.在子类方法中,访问父类的成员变量和方法。
3.super()和this()不能同时存在于构造方法中
原因:首先,java语言规定,在执行该类构造前必须先执行父类的构造,直到Object类的构造。
因此,如果本类不是Object类,那么在调用本类构造方法的时候,第一条语句一定是先调用,其父类的构造方法。
如果本类调用的构造方法中,没有用super(..)调用父类构造方法。那么编译器会为该构造方法默认添加一个super(),去调用父类构造方法。
如果本类调用的构造方法中,有用super(),那么就用指定的构造方法
所以当存在this()和super()时,编译器先调用本类所指的,构造方法,而那个构造方法一定会率先调用父类构造方法,初始化成员变量。当执行到super()语句时,又会调用父类构造方法,导致出现两次super()的调用。
4.静态方法中不可以出现this\super关键字
1.6 super与this关键字的异同
相同点:
1.都是Java中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1. this是当前对象的引用,当前对象即调用实例方法的对象。
2.super相当于是子类对象中从父类继承下来部分成员的引用。
3. 在非静态成员方法中,this用来访问本类的方法和属性。
4.super用来访问父类继承下来的方法和属性。
5. 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现 。
6. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有。
1.7再谈初始化
还记得类和对象中所讲的几块重要的代码块吗?
我们简单的回顾一下:实例代码块和静态代码块,在没有继承的情况下的执行顺序。
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static {
System.out.println("静态代码块执行");
}
}
public class TestDemo {
public static void main(String[] args) {
Person person1 = new Person("bit",10);
System.out.println("============================");
Person person2 = new Person("gaobo",20);
}
}
执行结构:
静态代码块执行
实例代码块执行
构造方法执行
============================
实例代码块执行
构造方法执行
总结:
1.静态代码块先执行(在类加载阶段),且只执行一次。
2.当对象创建时,才会执行实例代码块,实例代码块执行完,才执行构造方法。
接下来我们再来看看:
【继承关系上的执行顺序】
package demo1;
/**
* Created with IntelliJ IDEA
* Description:
* User:34398
* Date:2024-03-18
* Time:19:11
*/
class Person{
public static String Nation;
//静态代码块
static {
Nation="China";
System.out.println("父类静态代码块执行");
}
private boolean sex;
private int age;
//实例代码块
{
sex=false;
age=10;
System.out.println("父类实例代码块执行");
}
//构造方法
public Person(boolean sex,int age){
super();
this.age=age;
this.sex=sex;
System.out.println("父类构造方法执行");
}
}
class Student extends Person{
public static String Calss="yiban";
//静态代码块
static{
System.out.println("子类静态代码块执行");
}
private String name;
//实例代码块
{
System.out.println("子类实例代码块执行");
}
//构造方法
public Student(String name,boolean sex,int age){
super(sex,age);//记住,如果父类写了带参数的,构造方法,记得这里也要带上他所需要的参数
this.name=name;
System.out.println("子类构造方法执行");
}
}
public class Test {
public static void main(String[] args) {
Student student1=new Student("zouhai1",true,18);
System.out.println("=====================");
Student student=new Student("sdf",false,18);
}
}
执行结果:
父类静态代码块执行
子类静态代码块执行
父类实例代码块执行
父类构造方法执行
子类实例代码块执行
子类构造方法执行
=====================
父类实例代码块执行
父类构造方法执行
子类实例代码块执行
子类构造方法执行
由执行结果可以分析得出一下结论:
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.8访问权限关键字的选择
什么时候下用哪一种呢? 我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者. 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限.
例如如果一个方法能用 private, 就尽量不要 用 public.
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public.
不过这种方式属于是 对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内 部自己用, 还是类的调用者使用, 还是子类使用).
注意:父类中的private修饰的成员变量虽然不能在子类中直接访问,但也继承到了子类中.
1.9继承的方式
在现实生活中,事物之间的关系是非常复杂,灵活多样,比如:
而在JAVA中只支持一下几种方式:
1.单继承
2.多层继承:
3.不同子类继承同一个父类
注意:
1.类的多继承(一个子类,可以继承多个父类),JAVA是不支持的。
2.一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了。
3.如果想要限制继承次数,就可以使用final关键字。
1.10 final关键字
使用方式:
1.修改变量或字段,表示常量(即不能修改)
f inal int a = 10;
a = 20; // 编译出错
2.修饰类:表示此类不能被继承
final public class Animal{
…………
}
此时的Animal类无法被其他类继承。
3.修饰方法:表示该方法不能被重写(下文介绍)
1.11继承和组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
组合并没有涉及到特殊的语法 (诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
它类似于C语言中结构体嵌套的行为。
继承表示对象之间是is-a的关系,比如:
狗(子类)是一个动物(父类)
组合表示对象之间是has-a的关系,比如:
车(父类)有一个发动机(子类)
注释:
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择。
一般建议:能用组合尽量用组合。
以下是官方解释:
2.多态
2.1多态的概念:
通俗来讲,就是多种形态,具体一点就是,去完成某一个行为,不同的对象去完成,会产生不同的状态。
举个不恰当的例子(没有别的任何意思,仅仅用于理解)
左边两位,都用同样的方式去邀请,右边的女生吃饭。但女生对两位同志的态度并不相同。(抽象的理解)
2.2多态的实现条件
JAVA中想要实现多态,一下条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写(要先向上转型)
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
我们先来看看,多态在JAVA中长什么样子(在这个例子中,逐一解释,实现多态的条件):
package demo1;
/**
* Created with IntelliJ IDEA
* Description:
* User:34398
* Date:2024-03-18
* Time:19:11
*/
class Animal {
String name;
int age;
public Animal(String name, int age) {
this.age = age;
this.name = name;
}
@Override //用于检测重写是否成功
public void eat() {
System.out.println(this.name + "吃饭");
}
}
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override //用于检测重写是否成功
public void eat() {
System.out.println(this.name + "吃猫粮");
}
}
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
public void eat() {
System.out.println(this.name + "吃狗粮");
}
}
public class Test {
public static void test(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("小猫",10);
Dog dog = new Dog("小狗", 5);
test(dog);
test(cat);
}
}
输出结果:
小狗吃狗粮
小猫吃猫粮
发现没有,同样是调用eat()方法,但是dog对象,cat对象调用所打印的结果却是不同的,这就是多态的体现了。
2.2.1必须在继承的体系下
这个好理解,在上面的例子中,猫和狗都是动物继承下来的
2.2.2子类必须要对父类中方法进行重写
在Animal类中有这样一段代码
在狗类中也有类似的代码
发现没有,他们的返回类型,方法名,参数数量都是一样的。
我们就说 猫类 中的eat()方法 重写(也叫覆写)了 Animal类 中的eat()方法
那么重写方法具体要符合那些规则呢?
- 父类的成员方法只能被它的子类重写。
- 子类重写父类方法时:返回类型 参数列表 方法名要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 例子:
-
class Animal { public Animal getAnimal() { return new Animal(); } } class Dog extends Animal { @Override public Dog getAnimal() { return new Dog(); } } //如果子类Dog中的getAnimal方法的返回类型不是与Animal相对应的Dog,而是String,那么编译器就会报错
- 被final ,static,private修饰 的方法不能被重写。
- 被重写的方法的权限,不能比父类对应的方法的权限低。
- 构造方法不能被重写。
2.2.3 向上转型
概念:将子类的引用赋给父类对象(只有这样该引用才既可以同时调用父类、子类的方法)
所以,上文代码中,哪里出现了,子类引用赋值给了父类对象?
test()方法,传递的参数类型是Animal的子类,Dog,Cat
而test()方法的定义中,参数类型又是Animal,这就满足了向上转型的条件了
当然,向上转型的方式不止这一种,一般都是这三种方式:
1.直接赋值
2.方法传参(所给例子)
3.方法返回
2.2.4向下转型(顺便介绍)
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。
由图可知,向下转型并不安全
如下列代码
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = dog;
animal.eat();
// 向上转型
// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
cat = (Cat)animal;
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗是安全的
dog = (Dog)animal;
dog.bark();
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入 了instanceof,如果该表达式为true,则可以安全转换。
如下列代码:
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
2.3多态实现小结
当2.2中所有条件满足,test()方法中的父类引用(animal)再次调用父类和子类都有的eat()方法时,就会自动调用所赋值的子类引用的eat()方法.
这时,多态就实现了。
2.4JAVA中多态性的表现(重写和重载)
重载和重写其实都是JAVA多态性的表现
多态实际分为两种:
(1) 编译时多态(设计时多态):其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数。
(2) 运行时多态:而运行时多态是动态的,它是通过动态绑定来实现的,JAVA运行时系统根据调用该方法的实例的类型来决定选择调用哪个方法则被称为运行时多态。(我们平时说得多的是运行时多态,所以多态主要也是指运行时多态)
重写是父类与子类之间多态性的表现,在运行时起作用(动态多态性,譬如实现动态绑定)
而重载是一个类中多态性的表现,在编译时起作用(静态多态性,譬如实现静态绑定)。
2.5多态的优缺点
2.5.1好处
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
什么叫 "圈复杂度" ? 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如 果有很多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一个方法的圈复杂度太高, 就需要考虑重构. 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");
}
}
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect()};
for (Shape shape : shapes) {
shape.draw();
}
}
在上面的代码中如果我们要还要实现下列方法
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
那么就只需要调用drawShapes()方法,然后new一个Flower这个类的实例即可改动成本低。
2.5.3多态的缺点(代码的运行效率降低。)
1. 属性没有多态性 当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性 见如下文
2.66 不要在构造方法中调用重写的方法
一段有坑的代码. 我们创建两个类,
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.func() 0
解释
1.构造 D 对象的同时, 会调用 B 的构造方法.
2.B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
3.此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
4.如果具备多态性,num的值应该是1.
5.所以在构造方法内,尽量避免使用实例方法,除了final和private方法(记一下)
结论:
"用尽量简单的方式使对象进入可工作状态"。
尽量不要在构造器(构造方法)中调用方法(如果这个方法被子类重写, 就会触 发动态绑定, 但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题。