Java基础(八):封装、继承、多态性

Java基础系列文章

Java基础(一):语言概述

Java基础(二):原码、反码、补码及进制之间的运算

Java基础(三):数据类型与进制

Java基础(四):逻辑运算符和位运算符

Java基础(六):数组

Java基础(七):面向对象编程

Java基础(八):封装、继承、多态性


目录

  • 一、封装性
  • 二、关键字-this
  • 三、继承(Inheritance)
    • 1、基本概念
    • 2、方法的重写(override/overwrite)
  • 四、关键字-super
    • 1、子类中调用父类被重写的方法
    • 2、子类中调用父类中同名的成员变量
    • 3、子类构造器中调用父类构造器
  • 五、子类对象实例化全过程
  • 六、多态性
    • 1、基本概念
    • 2、多态的好处和弊端
    • 3、成员变量没有多态性
    • 4、向上转型与向下转型


一、封装性

  • 所谓封装,就是把客观事物封装成抽象概念的类
  • 并且类可以把自己的数据和方法只向可信的类或者对象开放
  • 向没必要开放的类或者对象隐藏信息
  • 通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想

Java如何实现数据封装

  • 实现封装就是控制类或成员的可见性范围
  • 这就需要依赖访问控制修饰符,也称为权限修饰符来控制
  • 权限修饰符:publicprotected缺省private
  • 具体访问范围如下:
修饰符 本类内部 本包内 其他包的子类 其他包非子类
private
缺省
protected
public
  • 类:只能使用public、缺省修饰
    • 定义为 private 很显然是没有意义的,一个类定义出来其他的类都无法和他沟通,独立存在是没有意义的
    • 定义为protected
      • protected 是在defaul的基础之上增加了子类也可访问的权限
      • 也就是说,你就算超出同一个包,即在另外一个包想要访问这个类,你得是他的子类
      • 假如:在另外一个包里建一个类继承他,根本无法找到父类,怎么继承(先有鸡还是先有蛋)
  • 类的内部成员:可以使用4种权限修饰进行修饰

成员变量/属性私有化

概述:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能

  • 使用 private 修饰成员变量
public class Person {
    private String name;
  	private int age;
    private boolean marry;
}
  • 提供 getXxx方法 / setXxx 方法,可以访问成员变量
public class Person {
    private String name;
  	private int age;
    private boolean marry;

	public void setName(String n) {
		name = n;
    }

    public String getName() {
        return name;
	}

    public void setAge(int a) {
        age = a;
    }

    public int getAge() {
        return age;
    }
    
    public void setMarry(boolean m){
        marry = m;
    }
    
    public boolean isMarry(){
        return marry;
    }
}
  • 测试:
public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person();

        //实例变量私有化,跨类是无法直接使用的
		/* p.name = "张三";
        p.age = 23;
        p.marry = true;*/

        p.setName("张三");
        System.out.println("p.name = " + p.getName());

        p.setAge(23);
        System.out.println("p.age = " + p.getAge());

        p.setMarry(true);
        System.out.println("p.marry = " + p.isMarry());
    }
}

成员变量封装的好处:

  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问
  • 还可以进行数据检查,从而有利于保证对象信息的完整性
  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改
  • 例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改

二、关键字-this

this是什么?

  • 在Java中,this关键字不算难理解,它的作用和其词义很接近
    • 它在方法(准确的说是实例方法或非static的方法)内部使用,表示调用该方法的对象
    • 它在构造器内部使用,表示该构造器正在初始化的对象
  • this可以调用的结构:成员变量、方法和构造器

什么时候使用this

  • 实例方法或构造器中使用当前对象的成员
  • 在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的可读性。不过,通常我们都习惯省略this
  • 但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量
  • 即:我们可以用this来区分成员变量局部变量
  • 比如:

Java基础(八):封装、继承、多态性_第1张图片

同一个类中构造器互相调用

  • this可以作为一个类中构造器相互调用的特殊格式
    • this():调用本类的无参构造器
    • this(实参列表):调用本类的有参构造器
public class Student {
    private String name;
    private int age;

    // 无参构造
    public Student() {
//        this("",18);//调用本类有参构造器
    }

    // 有参构造
    public Student(String name) {
        this();//调用本类无参构造器
        this.name = name;
    }
    // 有参构造
    public Student(String name,int age){
        this(name);//调用本类中有一个String参数的构造器
        this.age = age;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public String getInfo(){
        return "姓名:" + name +",年龄:" + age;
    }
}

注意:

  • 不能出现递归调用。比如,调用自身构造器
    • 推论:如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(形参列表)"
  • this()和this(实参列表)只能声明在构造器首行
    • 推论:在类的一个构造器中,最多只能声明一个"this(参数列表)"

三、继承(Inheritance)

1、基本概念

语法格式

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:

[修饰符] classA {
	...
}

[修饰符] classB extendsA {
	...
}
  • 类B,称为子类、派生类(derived class)、SubClass
  • 类A,称为父类、超类、基类(base class)、SuperClass

继承性的细节说明

1、子类会继承父类所有的实例变量和实例方法

从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征

  • 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存
  • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误

所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板

Java基础(八):封装、继承、多态性_第2张图片
2、子类不能直接访问父类中私有的(private)的成员变量和方法

  • 子类虽会继承父类私有(private)的成员变量
  • 但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问
  • 如图所示:

Java基础(八):封装、继承、多态性_第3张图片

3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”

  • 子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展

4、Java支持多层继承(继承体系)

Java基础(八):封装、继承、多态性_第4张图片

class A{}
class B extends A{}
class C extends B{}
  • 子类和父类是一种相对的概念
  • 顶层父类是Object类。所有的类默认继承Object,作为父类

5、一个父类可以同时拥有多个子类

class A{}
class B extends A{}
class D extends A{}
class E extends A{}

6、Java只支持单继承,不支持多重继承

Java基础(八):封装、继承、多态性_第5张图片

public class A{}
class B extends A{}

//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} 	//ok
class C extends A,B...	//error

2、方法的重写(override/overwrite)

  • 父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?
  • 子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置覆盖
  • 在程序执行时,子类的方法将覆盖父类的方法

方法重写的要求

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表
  2. 子类重写的方法的返回值类型小于等于父类被重写的方法的返回值类型。(例如:Student < Person)
    • 注意:如果返回值类型是基本数据类型和void,那么必须是相同
  3. 子类重写的方法使用的访问权限大于等于父类被重写的方法的访问权限。(例如:public > protected > 缺省 > private)
    • 注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写
  4. 子类方法抛出的异常小于等于父类被重写方法的异(例如:RuntimeException < Exception)

父类对象:

public class Person {

    protected Person eat() throws Exception{
        System.out.println("人吃饭");
        return null;
    }
}

子类对象:

public class Student extends Person {

    @Override
    public Student eat() throws RuntimeException{
        System.out.println("学生干饭");
        return null;
    }
}

多态创建对象:

public class Test {
    public static void main(String[] args) {
        Person p = new Student();
        try {
            p.eat();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 如果子类返回值类型比父类大,那么Person p = new Student()则不对等
  • 如果子类抛出异常比父类大,那么p.eat()则捕捉不到异常
  • 如果子类修饰符比父类小,那么子类的方法可能访问不到

四、关键字-super

super的理解

  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可用于在子类构造器中调用父类的构造器

注意:

  • 当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类
  • super和this的用法相像,this代表本类对象的引用,super代表引用父类声明的成员

super的使用场景

1、子类中调用父类被重写的方法

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法
  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

举例:

public class Phone {
    public void sendMessage(){
        System.out.println("发短信");
    }
    public void call(){
        System.out.println("打电话");
    }
    public void showNum(){
        System.out.println("来电显示号码");
    }
}

//smartphone:智能手机
public class SmartPhone extends Phone{
    //重写父类的来电显示功能的方法
    public void showNum(){
        //来电显示姓名和图片功能
        System.out.println("显示来电姓名");
        System.out.println("显示头像");

        //保留父类来电显示号码的功能
        super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
    }
}

总结

  • 方法前面没有super.和this.
    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有this.
    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有super.
    • 从当前子类的直接父类找,如果没有,继续往上追溯

2、子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

总结

  • 变量前面没有super.和this.
    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量
    • 如果不是局部变量,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
  • 变量前面有this.
    • 通过this找成员变量时,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
  • 变量前面super.
    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)

特别说明:应该避免子类声明和父类重名的成员变量

阿里开发规范

Java基础(八):封装、继承、多态性_第6张图片

3、子类构造器中调用父类构造器

  • ① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器
  • 规定:“super(形参列表)”,必须声明在构造器的首行
  • ③ 我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器,
     结合②,结论:在构造器的首行,“this(形参列表)” 和 "super(形参列表)"只能二选一
  • ④ 如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)"
    ​  则子类此构造器默认调用"super()",即调用父类中空参的构造器
  • ⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
    ​  只能是这两种情况之一。
  • ⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)“,则剩下的那个一定使用"super(形参列表)”

开发中常见错误:
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错

五、子类对象实例化全过程

举例:

// 生物类
class Creature {
    public Creature() {
        System.out.println("Creature无参数的构造器");
	}
}
// 动物类
class Animal extends Creature {
    public Animal(String name) {
        System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
    }
    public Animal(String name, int age) {
        this(name);
        System.out.println("Animal带两个参数的构造器,其age为" + age);
	}
}
// 猫类
public class Dog extends Animal {
    public Dog() {
        super("汪汪队阿奇", 3);
        System.out.println("Dog无参数的构造器");
    }
    public static void main(String[] args) {
        new Dog();
	}
}

内存图:

Java基础(八):封装、继承、多态性_第7张图片

  • 当我们创建子类对象后,子类对象就获取了其父类中声明的所有的属性和方法,在权限允许的情况下,可以直接调用
  • 当我们通过子类的构造器创建对象时,子类的构造器一定会直接或间接的调用到其父类的构造器
    • 而其父类的构造器同样会直接或间接的调用到其父类的父类的构造器
    • 直到调用了Object类中的构造器为止
  • 正因为我们调用过子类所有的父类的构造器,所以我们就会将父类中声明的属性、方法加载到内存中,供子类的对象使用

问题:在创建子类对象的过程中,一定会调用父类中的构造器吗?

答案:是的

问题:创建子类的对象时,内存中到底有几个对象?

答案:只有一个对象!即为当前new后面构造器对应的类的对象

六、多态性

1、基本概念

对象的多态性

  • 多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象
  • 格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;

举例:

Person p = new Student();

Object o = new Person();//Object类型的变量o,指向Person类型的对象

o = new Student(); //Object类型的变量o,指向Student类型的对象
  • 对象的多态:在Java中,子类的对象可以替代父类的对象使用
  • 所以,一个引用类型变量可能指向(引用)多种不同类型的对象

多态的理解

  • Java引用变量有两个类型:编译时类型运行时类型
  • 编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定
  • 简称:编译时,看左边;运行时,看右边
    • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
    • 多态情况下
      • “看左边”:看的是父类的引用(父类中不具备子类特有的方法)
      • “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
  • 多态的使用前提:① 类的继承关系 ② 方法的重写

举例

宠物类

public class Pet {
    private String nickname; //昵称

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void eat(){
        System.out.println(nickname + "吃东西");
    }
}

猫类

public class Cat extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("猫咪" + getNickname() + "吃鱼仔");
    }

    //子类扩展的方法
    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}

狗类

public class Dog extends Pet {
    //子类重写父类的方法
    @Override
    public void eat() {
        System.out.println("狗子" + getNickname() + "啃骨头");
    }

    //子类扩展的方法
    public void watchHouse() {
        System.out.println("看家");
    }
}

1、方法内局部变量的赋值体现多态

public class TestPet {
    public static void main(String[] args) {
        //多态引用
        Pet pet = new Dog();
        pet.setNickname("小白");

        //多态的表现形式
        /*
        编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
        运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
         */
        pet.eat();//运行时执行子类Dog重写的方法
//      pet.watchHouse();//不能调用Dog子类扩展的方法

        pet = new Cat();
        pet.setNickname("雪球");
        pet.eat();//运行时执行子类Cat重写的方法
    }
}

2、方法的形参声明体现多态

public class Person{
    private Pet pet;
    public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed(){
        pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
    }
}

3、方法返回值类型体现多态

public class PetShop {
    //返回值类型是父类类型,实际返回的是子类对象
    public Pet sale(String type){
        switch (type){
            case "Dog":
                return new Dog();
            case "Cat":
                return new Cat();
        }
        return null;
    }
}

2、多态的好处和弊端

  • 好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了
  • 弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法
Student m = new Student();
m.school = "pku"; 	//合法,Student类有school成员变量
Person e = new Student(); 
e.school = "pku";	//非法,Person类没有school成员变量

// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

3、成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中
  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不能覆盖父类中定义的实例变量
public class TestVariable {
    public static void main(String[] args) {
        Base b = new Sub();
        System.out.println(b.a);  // 1
        System.out.println(((Sub)b).a);  // 2

        Sub s = new Sub();
        System.out.println(s.a); // 2
        System.out.println(((Base)s).a);  // 1
    }
}
class Base{
    int a = 1;
}
class Sub extends Base{
    int a = 2;
}

4、向上转型与向下转型

为什么要类型转换

  • 因为多态,就一定会有把子类对象赋值给父类变量的时候
  • 这个时候,在编译期间,就会出现类型转换的现象
  • 但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了
  • 所以,想要调用子类特有的方法,必须做类型转换,使得编译通过

Java基础(八):封装、继承、多态性_第8张图片

如何向上或向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

public class ClassCastTest {
    public static void main(String[] args) {
        //没有类型转换
        Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog

        //向上转型
        Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
        pet.setNickname("小白");
        pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
//        pet.watchHouse();//不能调用父类没有的方法watchHouse

        Dog d = (Dog) pet;
        System.out.println("d.nickname = " + d.getNickname());
        d.eat();//可以调用eat方法
        d.watchHouse();//可以调用子类扩展的方法watchHouse

        Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
        //这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
    }
}

instanceof关键字

  • 为了避免ClassCastException的发生,Java提供了 instanceof 关键字
  • 给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A 
  • 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常
  • 如果对象a属于类A的子类B,a instanceof A值也为true
  • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误
public class TestInstanceof {
    public static void main(String[] args) {
        Pet[] pets = new Pet[2];
        pets[0] = new Dog();//多态引用
        pets[0].setNickname("小白");
        pets[1] = new Cat();//多态引用
        pets[1].setNickname("雪球");

        for (int i = 0; i < pets.length; i++) {
            pets[i].eat();

            if(pets[i] instanceof Dog){
                Dog dog = (Dog) pets[i];
                dog.watchHouse();
            }else if(pets[i] instanceof Cat){
                Cat cat = (Cat) pets[i];
                cat.catchMouse();
            }
        }
    }
}

你可能感兴趣的:(java基础,java,jvm,servlet)