Java笔记(九)——继承 /访问控制public private /重写 /多态

面向对象的特性有很多:
类和对象、抽象、封装、继承、组合、多态、反射等等。
类和对象《Java笔记(六)——类和对象(上) 类的定义 / 初始化 / toString》
《Java笔记(七)——类和对象(下) static关键字 /访问限制符 /内部类 /栈 堆》

1 继承

1.1 基本概念 / 背景

目的:代码重用,类的重用
概念:父类(基类,超类)
   子类(派生类)
关键字:extends(扩展)
继承主要解决的问题是:共性的抽取

例子:鸟和猫同属于动物。这里有三个主体,鸟、猫、动物,这里面就有共性的东西可以抽取出来。
在这里插入图片描述
  其中涉及三个类,动物类、鸟类 和猫类。动物类中有属性名字、方法吃,而鸟类和猫类都有名字属性和吃的方法,另外,它们又分别由不同的东西,鸟有飞的方法,猫有跳的方法。来 ~ 我们上代码康康 ~

public class Animal {
    public String name;

    public void eat(String food){
        System.out.println(name + "正在吃" + food);
    }
}
public class Cat {
    public String name;

    public void eat(String food){
        System.out.println(name + "正在吃" + food);
    }
    public void jump(){
        System.out.println(name + "正在眺");
    }
}
public class Bird {
    public String name;

    public void eat(String food){
        System.out.println(name + "正在吃" + food);
    }
    public void fly(){
        System.out.println(name + "正在飞");
    }
}

  这时,我们会发现,这三段代码有重复的地方。
  完全一样就不必写三个类;部分一样,部分不一样时,就必须要有三各类。但因为有部分一样,我们就可以使用继承,把 Cat 和 Bird 相同的部分提取出来,放到 Animal 中,然后再让 Cat 和 Bird 分别继承自 Aimal。看如下代码,用extends 关键字继承Animal ,就不用重复的写名字的属性和吃的方法了。

public class Cat extends Animal{
    public void jump(){
        System.out.println(name + "正在眺");
    }
}
public class Bird extends Animal{
    public void fly(){
        System.out.println(name + "正在飞");
    }
}

  Cat 类继承自 Animal ,于是 Animal 中包含的属性和方法, Cat 就自动具有了。
  继承就是为了代码重用,为了把多各类之间的共同的代码提取出来,放到 “ 父类 ” 中,然后再由各个子类分别继承这个父类,从而就可以把这些子类的重复代码消灭了,只剩一份了。改动时,只用改父类,在这里是 Animal ,就可以使子类都改变了。

1.2 语法规则

1、使用关键字 extends 指定父类;
2、Java 中的一个子类只能继承一个父类,父类可以作为子类再继承别的类,而C++/Python等语言支持多继承;
3、子类会继承父类的所有属性和方法 ,无论是public 还是private,只不过 private 修饰的成员在子类中无法访问(private 修饰只能在类的内部能访问,就算是它的子类也不能访问);
4、子类的实例中也包含着父类的实例,当我们去 new 一个 Cat 实例的时候,就会先创建一个 Animal 实例在 Cat 的内部;
在这里插入图片描述
5、使用 super 关键字可以获取到父类的实例的引用;使用 this 可以获取到整个子类的实例的引用;

6、构造方法
每个类都有构造方法,如果我们不显式的构造方法,那么编译器就会给这个类自动生成一个构造方法。
(1) 当父类里面没有写构造方法的时候,就自动生成了没参数版本的构造方法,此时,如果直接 new 子类实例,就会调用到刚才父类这个没参数版本的构造方法;

public Animal(){    // 无参版本,自动创建
}

(2) 当父类里面有构造方法的时候,并且这个构造方法带有参数的时候,编译器就不再自动生成无参数版本的构造方法了,此时,创建子类实例,就需要显示的调用构造方法,并进行传参,否则创建不出来父类的实例,就会编译出错。(父类有多个构造方法时,子类根据调用父类构造方法是的传参来选择一个)

public class Animal {
    public String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
   public void eat(String food){
        System.out.println(name + "正在吃" + food);
    }
}
public class Cat extends Animal{     // 编译出错
    public void jump(){
        System.out.println(name + "正在眺");
    }
}

在这里插入图片描述
修改:在子类的构造方法中,显示的调用父类的构造方法。
通过super 关键字,调用父类的构造方法(在报错的红波浪线上 Alt + Enter 可直接创建出)

public class Cat extends Animal{
    public Cat(String name){
        super(name);
    }

    public void jump(){
        System.out.println(name + "正在眺");
    }
}

注意:
(1) 每次 new 一个新的 Cat,即一个子类实例里面包含一个父类实例,就有一个新的name 属性,new 几个新的子类实例就有几个父类实例;
(2) 当子类继承了父类后,就会把父类中的name 继承过来,所以在子类当中,可以通过 this 关键字调用name;
(3) name 这个属性是从父类这里继承过来的,既可以使用 this.name 的方式获取,也可以通过 super.name 的方式获取,获取到的是同一个name ;
(4) 强制要求:如果显示调用父类的构造方法,要把 super 调用写在最前面第一行,为了保证先执行父类的构造,再执行子类的构造。
先构造父类对象(执行父类构造方法的逻辑),再构造子类对象(再执行子类构造方法的逻辑)

public Cat(String name){
    System.out.println();      // 错误!!!
    super(name);
}
public Cat(String name){
    super(name);             // 正确
    System.out.println();

}

1.3 访问权限控制

public :可以在类外部访问
protected :可以被同包的其他类访问,也可以被其他包的子类访问;
default(什么都不写) :可以被同包的其他类访问;
private :只能在类内部访问。
访问范围大小:
public > protected > default > private

范围privatedefautprotectedpublic
1 同一包中的类
2 同一包中的不同类
3 不同包中的子类
4 不同包中的非子类
属性和方法,前按照以上规则添加控制,类前只能加 public或不加。

1.4 final关键字

final 关键字修饰类,此时就表示被修饰的类不能被继承。

2 组合

也是为了代码重用,也是面向对象的一个重要特性。一个类的成员也可以是其他类,就是组合。

public class School {
    public SchoolMaster schoolMaster = new SchoolMaster();

    public Student student1 = new Student();
    public Student student2 = new Student();
    public Student student3 = new Student();

    public ClassRoom classRoom1 = new ClassRoom();
    public ClassRoom classRoom2 = new ClassRoom();
    public ClassRoom classRoom3 = new ClassRoom();

}
public class SchoolMaster {
}
public class Student {
}
public class ClassRoom {
}

  School 类中包含了SchoolMaster 类、Student类、 ClassRoom类,可以访问各个类中的属性。
  组合表示的语义是 拥有什么,包含什么;继承表示的语义是 是什么。

3 多态

能够帮助我们解决大量的分支语句的情况。

3.1 向上转型

向上转型是多态的重要语法基础,有三种方式完成向上转型,其实都相当于赋值。
(1) 基本的向上转型:直接赋值
使用一个父类的引用指向一个子类的实例。
沿用上面的例子,Animal 父类和 Cat 子类。

public class Main {
    public static void main(String[] args) {
        
        Animal animal = null;
        Cat cat = new Cat("团子");
        animal = (Animal)cat;

	// 上面三行可简化为这样:
	Animal animal = new Cat("团子");
    }
}

在这里插入图片描述
不同类型的引用一般情况下是不能相互赋值的,但是父类和子类是可以的。
(2) 在方法传参的过程中,向上转型

func(new Cat());

public static void func(Animal animal){

}

(3) 在方法返回时,向上转型

Animal animal = func2();

public static Animal func2(){
    return new Cat();
}

3.2 动态绑定

如果父类中包含的方法在子类中有对应的同名参数的方法,就会进行动态绑定,决定运行时要调用哪个方法。
静态 => 编译期 (注意和static 没有关系)
动态 => 运行时
下面是方法存在的几种可能:
(1) 如果eat 方法中只在父类中存在,此时调用 eat 就是父类的这个 eat 方法;(不涉及动态绑定,静态)

Animal animal = new Cat();
animal.eat("鱼");

public class Animal {
        public static void eat(String food){
            System.out.println("Animal 正在吃" + food);
        }
}  

public class Cat extends Animal {
}

	// 运行结果:Animal 正在吃鱼

(2) 如果 eat 方法只在子类中存在,此时调用的 eat 就会编译报错;(不涉及动态绑定,静态)

Animal animal = new Cat();
animal.eat("鱼");

public class Animal {
}

public class Cat extends Animal{
    public  void eat(String food){
        System.out.println("Cat 正在吃" + food);
    }
}

在这里插入图片描述
(3) 如果 eat 方法在父类和子类中都存在,并且参数也相同,此时调用 eat 看 animal 究竟指向一个父类的实例还是子类的实例,指向父类的实例就调用父类版本的 eat 方法;指向子类的实例就调用子类版本的 eat 方法,在编译期无法确定 animal 到底指向谁,在运行时才能确定;(动态绑定)

Animal animal = new Cat();    // 指向子类 Cat 的实例
//Animal animal = new Animal();// 指向父类 Animal 的实例
animal.eat("鱼");

public class Animal {
        public void eat(String food){
            System.out.println("Animal 正在吃" + food);
        }
}

public class Cat extends Animal{
	 // 加注解 @override,编译器能更好的帮我们检查和校验工作
	 // 明确告诉编译器,我们的目的就是重写
	 // 写在子类的重写的方法之前
	 @Override
    public void eat(String food){
        System.out.println("Cat 正在吃" + food);
    }
}     
	// 运行结果:Cat 正在吃鱼

(4) 如果 eat 方法在父类和子类中都存在,并且参数不相同,此时调用 eat ,像方法重载但是不是方法重载。(不涉及动态绑定)

Animal animal = new Cat();
animal.eat("鱼");

public class Animal {
        public void eat(String food,String a ){
            System.out.println("Animal 正在吃" + food);
        }
}

public class Cat extends Animal{
  public void eat(String food){
        System.out.println("Cat 正在吃" + food);
    }
}

在这里插入图片描述
  根据调用方法传入的参数的类型和个数,判断父类中是否存在匹配的方法,如果不存在,就会编译报错。上面的代码,子类父类有一样的方法,但父类传入了两个参数,我们传入的是一个参数,不能与之匹配,所以,会编译报错。
  上述的动态绑定的规则,我们是站在编译器和JVM实现者的角度来看待;在Java 的语法层次上也有一个术语叫做 " 方法重写 override "。
注意:
如果父类的方法 eat 是public;子类的方法 eat 是private ,外部看不到子类的这个方法,此时不涉及动态绑定,编译会报错。

3.3 方法的重写

1、重写和重载的区别:完全不同的两个概念,重写子类和父类两个类中,包含有一样的方法,方法同名、参数一样,此时父类引用调用该方法,就会触发重写,具体执行哪个版本的方法由动态绑定的规则决定;重载是在同一个作用域中,两个方法名字相同但是参数的类型或是数量不同。
2、普通方法可以重写,static 修饰的静态方法不可以重写,重写和父类实例、子类实例密切相关,静态方法是类相关,不是实例相关;
3、重写中子类的方法访问权限不能低于父类的方法访问权限;
4、重写方法的返回值类型不一定和父类方法的返回值类型相同:假如父类和子类方法的返回值完全互不相干,这种情况下是会编译出错的;如果父类和子类方法的返回值类型具有父子关系,这种情况下,是正确的;

3.4 父类访问子类的方法

借助方法重写,由父类的引用触发子类的方法,再由子类的方法间接来操作子类的属性

    Animal animal = new Cat();
    animal.eat("鱼");
    System.out.println(animal.getGender());

public class Animal {

    public String getGender(){
        return "";
    }
}

public class Cat extends Animal{
    String gender = "公猫";
    @Override
    public String getGender(){
        return gender;
    }
}
	// 运行结果:公猫

3.5 向下转型

3.5.1 前提条件:

Animal 父类,Bird、Cat 子类,和调用者 Test 类。

public class Animal {
}
public class Bird extends Animal{
}
public class Cat extends Animal{
}
public class Test {
    public static void main(String[] args) {
        // 向上转型
        Animal animal = new Cat();
        // 向下转型,这里要确保 animal 指向的是 Cat 类型的实例,才可以进行转换,否则会转换失效
        Cat cat = (Cat)animal;

        Animal animal2 = new Bird();
        // 向下转型,animal 指向的不是 Cat 类型的实例,会抛 出异常如下图
        Cat cat2 = (Cat)animal;
    }
}

类型转换异常:
在这里插入图片描述
向下转型:
相当于向上转型的逆过程,向上转型中得到的父类的引用,可以借助向下转型还原回原来的类型。在 JDBC 编程中会常用到。所以在向下转型时,我们要注意考虑父类引用有没有指向我们需要的哪个子类的类型,Java中有个关键字可以用到 instanceof ,用它来规避这个错误。

3.5.2 instanceof

对象比较的三种形式
1、比较值 String通过equals比较
2、比较身份 ==
3、比较类型 instanceof

Animal animal2 = new Bird();
if (animal2 instanceof Cat) {
    Cat cat2 = (Cat)animal2;
}

用 instanceof 比较类型,如果父类引用引用的实例是当前类型,才会进行向下转型。

3.6 多态总结

1、多态直观理解,就是:一个引用,对应多种形态,这多种形态指不同类型的实例。
2、多态应用练习:
(1) 创建一个父类 Shape,类中构造一个方法 draw

public class Shape {
    public void draw(){
    }
}

(2) 创建三个子类,分别是 Circle、Triangle、Flower,并且在各自类中重写 draw 方法

public class Circle extends Shape{
    @Override
    public void draw(){
        System.out.println("⚪");
    }
}
public class Triangle extends Shape{
    @Override
    public void draw(){
        System.out.println("🔺");
    }
}
public class Flower extends Shape{
    @Override
    public void draw(){
        System.out.println("❀");
    }
}

(3) 创建一个 Test 类,在其中实现多态的应用

public class Test {
    public static void main(String[] args) {
    // 创建实例,应用了向上转型
//        Shape shape = new Circle();
//        Shape shape1 = new Triangle();
//        Shape shape2 = new Flower();
	// 用方法 draw 执行相关内容
//        draw2(shape);
//        draw2(shape1);
//        draw2(shape2);

        Shape[] shapes = {
                new Circle(),
                new Flower(),
                new Triangle(),
        };
        for (Shape shape:shapes) {
            draw2(shape);
        }
    }
    
    // 创建 draw2 方法画出图形
	public static void draw2(Shape shape){
		shape.draw();          
	// 注意,此draw 为Shape中的方法
	// 也是Circle、Flower、Triangle中重写的方法
	}
}

3、多态优点:
(1) 多态的设计思想,本质上是封装的更进一步,封装的目的是为了让类的使用者不需要知道类的实现细节,就能使用,但是使用者仍然需要知道这个类是什么类型;使用多态后,此类的使用者,不仅不需要知道类的实现细节,也不需要知道这个类具体是什么类,只需要知道有这个draw 方法就可。
(2) 如果需要新增一个形状,创建出一个新的子类即可,并且让子类也去重写这个 draw 方法,类的调用者这里的代码改动就比较简单甚至不用改动。
(3) 可以减少一些分支语句:if…else… / switch…case…如果不使用多态,同样的逻辑就会比较复杂。

3.7 关于在构造方法中调用函数的问题

首先,我们有两个类,一个父类 A 一个子类 B,一个测试类 Test ;
然后,A 类中有一个构造方法;
再然后,A 类和 B 类都有 一个 func 方法,方法中有一个打印语句,来帮助我们判断执行了哪个方法;
最后,我们在 Test 类中创建一个类 B 的对象。
这是一个注意应该避免写出的代码,避免在构造方法中调用方法 !
代码就这样出来啦:

class A{
    public A(){
        func();
    }

    public void func(){
        System.out.println("A.func");
    }
}
class B extends A{
    private int num = 1;

	@Override
    public void func(){
        System.out.println("B.func " + num);
    }
}

public class Test2 {
    public static void main(String[] args) {
        B b = new B();
    }

}

分析过程:
1、创建 B 的实例时,因为它继承自 A 所以会先先创建 A 的实例;
2、创建 A 的实例时,就会调用构造方法;
3、调用构造方法时,就会调用func 方法,相当于 this. func() 方法,this 是指子类实例,触发动态绑定,此时就会执行子类 B 中的 func 方法;
5、因为直接调用了这个 func 方法,还没有执行num 的初始化 “ private int num = 1 ”,所以是默认值 0 。
运行结果:
B.func 0

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值