关键字this

实例方法或构造器中使用当前对象的成员

在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的可读性。不过,通常我们都习惯省略this。

当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量局部变量

this可以调用的结构:成员变量、方法和构造器、(准确的说是非static修饰的方法)

另外,使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。

1
2
//this.成员变量 局部变量
this.name = name

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

this可以作为一个类中构造器相互调用的特殊格式。

  • this():调用本类的无参构造器

  • this(实参列表):调用本类的有参构造器

  • 不能出现递归调用。比如,调用自身构造器。

    • 推论:如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了"this(形参列表)"
  • this()和this(实参列表)只能声明在构造器首行

    • 推论:在类的一个构造器中,最多只能声明一个"this(参数列表)"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Student {
    private String name;
    private int age;

    // 无参构造
    public Student() {

    }

    // 有参构造
    public Student(String name) {
        this();//调用本类无参构造器
        this.name = name;
    }
    // 有参构造
    public Student(String name,int age){
        this(name);//调用本类中有一个String参数的构造器
        this.age = age;
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
	public void marry(Boy boy){
        System.out.println("我要嫁给"+boy.getName());
        // this:当前对象,调用marry方法的当前对象
        boy.marry(this);
    }

   /**
     * 比较两个Girl对象的大小。
     * @param girl
     * @return 正数:当前对象大 ; 负数:当前对象小(或形参girl大) ; 0:相等
     */
    public int compare(Girl girl){
        // this:当前对象
        if(this.age > girl.age){
            return 1;
        }else if(this.age < girl.age){
            return -1;
        }else{
            return 0;
        }

    }

1、按照UML类图,创建Account类,提供必要的结构。

  • 在提款方法withdraw()中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。
  • deposit()方法表示存款。

2、按照UML类图,创建Customer类,提供必要的结构。

3、按照UML类图,创建Bank类,提供必要的结构。

  • addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer对象,然后把它放到 customer 数组中。 还必须把 numberOfCustomer 属性的值加 1。
  • getNumOfCustomers 方法返回 numberofCustomers 属性值。
  • getCustomer方法返回与给出的index参数相关的客户。

4、创建BankTest类,进行测试。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class Bank {
    private Customer[] customers;//用于保存多个客户
    private int numberOfCustomer;//用于记录存储的客户的个数

    public Bank(){
        customers = new Customer[10];
    }

    /**
     * 将指定姓名的客户保存在银行的客户列表中
     * @param f
     * @param l
     */
    public void addCustomer(String f,String l){
        Customer cust = new Customer(f,l);
//        customers[numberOfCustomer] = cust;
//        numberOfCustomer++;
        //或
        customers[numberOfCustomer++] = cust;
    }

    /**
     * 获取客户列表中存储的客户的个数
     * @return
     */
    public int getNumOfCustomers(){
        return numberOfCustomer;
    }

    /**
     * 获取指定索引位置上的客户
     * @param index
     * @return
     */
    public Customer getCustomer(int index){
        if(index < 0 || index >= numberOfCustomer){
            return null;
        }

        return customers[index];

    }
}

public class BankTest {
    public static void main(String[] args) {
        Bank bank = new Bank();

        bank.addCustomer("操","曹");
        bank.addCustomer("备","刘");

        bank.getCustomer(0).setAccount(new Account(1000));
        bank.getCustomer(0).getAccount().withdraw(50);

        System.out.println("账户余额为:" + bank.getCustomer(0).getAccount().getBalance());
    }
}

public class Customer {
    private String firstName;//名
    private String lastName;//姓

    private Account account; //账户

    public Customer(String f, String l) {
        this.firstName = f;
        this.lastName = l;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }
}


public class Account {
    private double balance;//余额

    public Account(double init_balance) {
        this.balance = init_balance;
    }

    public double getBalance() {
        return balance;
    }

    //存钱
    public void deposit(double amt){
        if(amt > 0){
            balance += amt;
            System.out.println("成功存入:" + amt);
        }
    }

    //取钱
    public void withdraw(double amt){
        if(balance >= amt && amt > 0){
            balance -= amt;
            System.out.println("成功取出:" + amt);
        }else{
            System.out.println("取款数额有误或余额不足");
        }
    }
}

项目2:客户管理系统

软件包含三个模块:

CustomerView为主模块,负责菜单的显示和处理用户操作

CustomerList为Customer对象的管理模块,内部用数组管理一组Customer对象,并提供相应的添加、修改、删除和遍历方法,供CustomerView调用

Customer为实体对象,用来封装客户信息

键盘访问:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//从键盘读取一个字符
	public static char readChar() {
   		String str = readKeyBoard(1, false);
    	return str.charAt(0);
	}
/**
	从键盘读取一个字符,并将其作为方法的返回值。
	如果用户不输入字符而直接回车,方法将以defaultValue 作为返回值。
*/
    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);
        return (str.length() == 0) ? defaultValue : str.charAt(0);
    }

    private static String readKeyBoard(int limit, boolean blankReturn) {
        //用于确定当输入为空字符串时是否直接返回该空字符串
        String line = "";

        while (scanner.hasNextLine()) {
            line = scanner.nextLine();
            if (line.length() == 0) {
                if (blankReturn) return line;
                else continue;
            }

            if (line.length() < 1 || line.length() > limit) {
                System.out.print("输入长度(不大于" + limit + ")错误,请重新输入:");
                continue;
            }
            break;
        }

        return line;
    }

快捷get、set方法,构造器:alt+insert

构造器的作用: new对象,并在new对象的时候为实例变量赋值。

文档注释/**

删除数据,其他数据前移,最后一位置为null(地址)

查询所有数据,新建一个total长度的数组,防止打印原数组中空白元素

继承

面向对象特征二:继承(Inheritance)

角度一:从上而下

为描述和处理个人信息,定义类Person,为描述和处理学生信息,定义类Student,通过继承,简化Student类的定义。

说明:Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。

角度二:从下而上

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系

继承的好处

  • 继承的出现减少了代码冗余,提高了代码的复用性。

  • 继承的出现,更有利于功能的扩展。

  • 继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。

    • 继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。

注意:不要仅为了获取其他类中某个功能而去继承!

  • is-a(英语:subsumption,包含架构):代表类之间或类与接口之间的继承关系。具体来说,当类D是另一个类B的子类(类B是类D的父类)时,我们就说D与B之间存在is-a的关系。这种关系表达的是“这个东西是那个东西的一种”的概念。例如,猫和狗都是动物,都继承了动物的特性,在面向对象语言中,将猫狗定义为两种不同的类,都继承了动物类。
  • has-a:代表对象和它的成员的从属关系,即整体和部分之间的关系。如果A has a B,那么B就是A的组成部分。同一个类的对象,可以通过它们属性的不同值来区别。例如,张三和李四都是人,他们都是“人”这一个类,但具有不同的姓名属性,姓名和人是属于has-a关系。

继承的语法

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

1
2
3
4
5
6
7
8
[修饰符] class 类A {
	...
}

[修饰符] class 类B extends 类A {
	...
}

类B,称为子类、派生类(derived class)、SubClass

类A,称为父类、超类、基类(base class)、SuperClass

属性:都有默认初始化值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Animal {
    // 定义name属性
    String name;
    // 定义age属性
    int age;

    // 定义动物的吃东西方法
    public void eat() {
        System.out.println(age + "岁的"
                + name + "在吃东西");
    }
}

class Cat extends Animal{
    int count;//记录每只猫抓的老鼠数量

    // 定义一个猫抓老鼠的方法catchMouse
    public void catchMouse() {
        count++;
        System.out.println("抓老鼠,已经抓了"
                + count + "只老鼠");
    }
}
class TestCat {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.name = "Tom";
        // 为该猫类对象的age属性进行赋值
        cat.age = 2;
        // 调用该猫继承来的eat()方法
        cat.eat();
        // 调用该猫的catchMouse()方法
        cat.catchMouse();
        cat.catchMouse();
        cat.catchMouse();
    }
}

继承的细节

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

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

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

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

2、子类不能直接访问父类中私有的(private)的成员变量和方法

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

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

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

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

  • 子类和父类是一种相对的概念

  • 顶层父类是Object类。所有的类默认继承Object,作为父类。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 构造器
public class Kids extends ManKind{
    private int yearsOld;

    public Kids(){

    }

    public Kids(int yearsOld){
        this.yearsOld = yearsOld;
    }

    public Kids(int sex, int salary,int yearsOld){
        this.yearsOld = yearsOld;
        setSex(sex);
        setSalary(salary);
    }
}

方法的重写

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

在程序执行时,子类的方法将覆盖父类的方法。

@Override使用说明:

写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

方法重写的要求

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表

  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。

    注意:如果返回值类型是基本数据类型和void,那么必须是相同

    父类被重写的方法的返回值类型是引用数据类型(比如类),则子类重写的方法的返回值类型可以与被重写的方法的返回值类型相同 或 是被重写的方法的返回值类型的子类

  3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)

    注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写

  4. 子类方法抛出的异常不能大于父类被重写方法的异常。子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被重写的方法抛出的异常类型的子类。

  5. 此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

重载与重写

重载:“两同一不同” 参数列表不同,意味着参数个数或参数类型的不同 重写:继承以后,子类覆盖父类中同名同参数的方法

[类比]相同类型的面试题:

throws / throw final / finally / finalize Collection / Collections String / StringBuffer / StringBuilder ArrayList / LinkedList HashMap / LinkedHashMap / Hashtable …

sleep() / wait() == / equals() 同步 / 异步

关键字super

super的理解

在Java类中使用super来调用父类中的指定操作:

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

注意:

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

super的使用场景

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

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;

  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

  • 总结:

    • 方法前面没有super.和this.

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

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

      • 从当前子类的直接父类找,如果没有,继续往上追溯

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

属性不会覆盖,重名的也单独为一个。方法可以覆盖

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别.(实例变量==成员变量)
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Father{
	int a = 10;
	int b = 11;
}
class Son extends Father{
	int a = 20;
    
    public void test(){
		//子类与父类的属性同名,子类对象中就有两个a
		System.out.println("子类的a:" + a);//20  先找局部变量找,没有再从本类成员变量找
        System.out.println("子类的a:" + this.a);//20   先从本类成员变量找
        System.out.println("父类的a:" + super.a);//10    直接从父类成员变量找
		
		//子类与父类的属性不同名,是同一个b
		System.out.println("b = " + b);//11  先找局部变量找,没有再从本类成员变量找,没有再从父类找
		System.out.println("b = " + this.b);//11   先从本类成员变量找,没有再从父类找
		System.out.println("b = " + super.b);//11  直接从父类局部变量找
	}
	
	public void method(int a, int b){
		//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a		
		System.out.println("局部变量的a:" + a);//30  先找局部变量
        System.out.println("子类的a:" + this.a);//20  先从本类成员变量找
        System.out.println("父类的a:" + super.a);//10  直接从父类成员变量找

        System.out.println("b = " + b);//13  先找局部变量
		System.out.println("b = " + this.b);//11  先从本类成员变量找
		System.out.println("b = " + super.b);//11  直接从父类局部变量找
    }
}
class Test{
    public static void main(String[] args){
        Son son = new Son();
		son.test();
		son.method(30,13);  
    }
}

总结:起点不同(就近原则)

  • 变量前面没有super.和this.

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

    • 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量==(权限修饰符允许在子类中访问的)
  • 变量前面super.

    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)

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

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

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。

② 规定:“super(形参列表)”,必须声明在构造器的首行

③ 我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器, 结合②,结论:在构造器的首行,“this(形参列表)” 和 “super(形参列表)“只能二选一。

④ 如果在子类构造器的首行既没有显示调用"this(形参列表)",也没有显式调用"super(形参列表)", ​ 则子类此构造器默认调用"super()",即调用父类中空参的构造器

⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。 只能是这两种情况之一。(构造器首行,一定会存在this或super形参列表/显示或隐示)

⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)",则剩下的那个一定使用"super(形参列表)"。//n个this的话会形成环,报错

–> 我们在通过子类的构造器创建对象时,一定在调用子类构造器的过程中,直接或间接的调用到父类的构造器。也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法加载到内存中,供子类对象使用。

目的:为了初始化信息,赋值等作用;隐式的构造器-加载父类的属性或方法

子类在构造过程中调用父类构造器的目的主要是:

  1. 初始化继承的字段:子类继承了父类的所有字段(除了私有的静态字段),但在子类实例化时,这些字段可能还没有被初始化。因此,子类需要确保在构造自己的实例之前,父类的字段被正确地初始化。
  2. 执行父类的初始化逻辑:父类构造器中可能包含一些必要的初始化逻辑,如检查参数的有效性、设置默认值、执行一些必要的计算等。子类需要确保这些逻辑在创建子类实例之前被执行。
  3. 当创建子类的实例时,首先会执行父类的构造器,然后执行子类的构造器。

开发中常见错误:

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

因为:

创建类以后,在没有显示提供任何构造器的情况下,系统会默认提供一个空参的构造器,且构造器的权限 与类声明的权限相同。 一旦类中显示声明了构造器,则系统不再提供默认的空参的构造器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Person {
    //属性
    String name;//姓名
    int age;//年龄
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class Student extends Person{
    public Student(String name, int age) {
        //调用父类的构造器来初始化
        super(name, age);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class A{
	A(){
		System.out.println("A类无参构造器");
	}
}
class B extends A{
	B(){
		System.out.println("B类无参构造器");
	}
}
class Test{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个无参构造,
		//B类显示声明一个无参构造,        
		//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
        //可以看到会输出“A类无参构造器"和"B类无参构造器")
    }
}

class A{
	A(int a){
		System.out.println("A类有参构造器");
	}
}
class B extends A{
	B(){
		System.out.println("B类无参构造器");
	}
}
class Test05{
    public static void main(String[] args){
        B b = new B();
        //A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
		//B类显示声明一个无参构造,        
		//B类的无参构造没有写super(...),表示默认调用A类的无参构造
        //编译报错,因为A类没有无参构造
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Interview02{
    public static void main(String[] args) {
        Father f = new Father();
        Son s = new Son();
        System.out.println(f.getInfo());//atguigu
        System.out.println(s.getInfo()); //atguigu
        s.test();//atguigu atguigu
        System.out.println("-----------------");
        s.setInfo("大硅谷");
        System.out.println(f.getInfo());//atguigu
        System.out.println(s.getInfo());//大硅谷
        s.test(); //大硅谷 大硅谷
    }
}
class Father{
    private String info = "atguigu";
    public void setInfo(String info){
        this.info = info;
    }
    public String getInfo(){
        return info;
    }
}
class Son extends Father{
    private String info = "尚硅谷";
    public void test(){
        System.out.println(this.getInfo());
        System.out.println(super.getInfo());
    }

//    public String getInfo(){
//        return info;
//    }
}

子类对象实例化全过程

  1. 从结果的角度来看:体现为类的继承性

当我们创建子类对象后,子类对象就获取了其父类中声明的所有的属性和方法,在权限允许的情况下,可以直接调用。

  1. 从过程的角度来看:

当我们通过子类的构造器创建对象时,子类的构造器一定会直接或间接的调用到其父类的构造器,而其父类的构造器同样会直接或间接的调用到其父类的父类的构造器,….,直到调用了Object类中的构造器为止。

正因为我们调用过子类所有的父类的构造器,所以我们就会将父类中声明的属性、方法加载到内存中,供子类的对象使用。

  1. 问题:创建子类的对象时,内存中到底有几个对象? 就只有一个对象!即为当前new后面构造器对应的类的对象。

构造器调用过程,首行一定是this或者super–对象的加载过程是先加载父类,最后再加载自己

面向对象特征三:多态性

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象

对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象

开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。

1
2
3
4
5
6
7
父类类型 变量名 = 子类对象
    
Person p = new Student();

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

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

Java引用变量有两个类型:编译时类型运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。(虚方法调用)

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)

  • 编译看左,运行看右,直接点p.eat会进入到person类,运行的时候是student类

  • 多态的使用前提:① 类的继承关系 ② 方法的重写 (@Override注解表示方法重写)

  • 多态的适用性:适用于方法,不适用于属性。

    1
    2
    
    Person p = new Student();
    System.out.println(p.id); 调用属性为父类中的值
    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Person{
    private Pet pet;
    public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
        this.pet = pet;
    }
    public void feed(){
        pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
    }
}
public class TestPerson {
    public static void main(String[] args) {
        Person person = new Person();

        Dog dog = new Dog();
        dog.setNickname("小白");
        person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
        person.feed();
    }
}
// Pet pet = new Dog()

多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Account{
    public void withdraw(){} //取钱
}

class CheckAccount extends Account{ //信用卡
    //存在方法的重写
    public void withdraw(){} //取钱
}
class SavingAccount extends Account{ //储蓄卡
    //存在方法的重写
}

class Customer{
    Account account;

    public void setAccount(Account account){
        this.account = account;
    }

    public Account getAccount(){
        return accout;
    }

}

class CustomerTest{
    main(){
        Customer cust = new Customer();
        cust.setAccount(new CheckAccount());

        cust.getAccount().withdraw();

    }
}

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。

在多态的场景下,我们创建了子类的对象,也加载了子类特有的属性和方法(占用了内存)。但是由于声明为父类的引用,导致我们没有办法直接调用子类特有的属性和方法。

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。

  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package com.atguigu.polymorphism.grammar;

public class TestVariable {
    public static void main(String[] args) {
        Base b = new Sub();
        System.out.println(b.a);
        System.out.println(((Sub)b).a);

        Sub s = new Sub();
        System.out.println(s.a);
        System.out.println(((Base)s).a);
    }
}
class Base{
    int a = 1;
}
class Sub extends Base{
    int a = 2;
}
1
2
3
4
5
6
Student m = new Student();
m.school = "pku"; 	//合法,Student类有school成员变量
Person e = new Student(); 
e.school = "pku";	//非法,Person类没有school成员变量

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

开发中:

使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

【开闭原则OCP】

  • 对扩展开放,对修改关闭
  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

虚方法调用(Virtual Method Invocation)

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

拓展:

静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。

动态链接(或晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

学习到视频 p100

https://www.bilibili.com/video/BV1PY411e7J6/?p=100&spm_id_from=pageDriver&vd_source=ad42090d7d6fcdfc144126ae0e2884ac