第七章 面向对象编程(进阶)

2023-12-01 08:47:00 浏览数 (1)

1. this 关键字

1.1 this 的含义

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

1.2 什么时候用 this

  1. 实例方法或构造器中使用当前对象的成员 使用 this 来区分成员变量和局部变量 ​

​​

  1. 同一个类中构造器互相调用 this 可以作为一个类中构造器相互调用的特殊格式。
    • this():调用本类的无参构造器
    • this(实参列表):调用本类的有参构造器

注意:

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

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

2.1 继承性的理解

生活上:财产的继承、颜值的继承

代码层面:

  • 自上而下:定义了一个类 A,在定义另一个类 B 时,发现类 B 的功能与类 A 相似,考虑类 B 继承于类 A
  • 自下而上:定义了类 B,C,D 等,发现 B、C、D 有类似的属性和方法,则可以考虑将相同的属性和方法进行抽取,封装到类 A 中,让类 B、C、D 继承于类 A,同时,B、C、D 中的相似的功能就可以删除了。

有了继承性以后:

  • 子类就获取到了父类中声明的所有的属性和方法。
  • 但是,由于封装性的影响,可能子类不能直接调用父类中声明的属性或方法。
  • 子类在继承父类以后,还可以扩展自己特有的功能(体现:增加特有的属性、方法)
  • extends:延展、扩展、延伸
  • 子类和父类的理解,要区别于集合和子集
  • 不要为了继承而继承。在继承之前,判断一下是否有 is a 的关系。

默认的父类:

Java 中声明的类,如果没有显式的声明其父类时,则默认继承于 java.lang.Object

补充说明:

  • Java 是支持多层继承。
  • 概念:直接父类、间接父类
  • Java 中的子父类的概念是相对的。
  • Java 中一个父类可以声明多个子类。反之,一个子类只能有一个父类(Java 的单继承性)

2.2 继承性的好处

  • 继承的出现减少了代码冗余,提高了代码的复用性。
  • 继承的出现,更有利于功能的扩展。
  • 继承的出现让类与类之间产生了is-a​ 的关系,为多态的使用提供了前提。
    • 继承描述事物之间的所属关系,这种关系是:is-a​ 的关系。可见,父类更通用、更一般,子类更具体。

2.3 继承的格式

代码语言:javascript复制
class A{
    //属性、方法
}

class B extends A{

}

继承中的基本概念:

  • 类 A: 父类、superClass、超类、基类
  • 类 B: 子类、subClass、派生类

3. 方法的重写

3.1 方法重写的要求

  1. 子类重写的方法必须​ 和父类被重写的方法具有相同的方法名称​、参数列表​。
  2. 子类重写的方法的返回值类型不能大于​ 父类被重写的方法的返回值类型。(例如:Student < Person)。

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

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

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

  1. 子类方法抛出的异常不能大于父类被重写方法的异常

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

3.2 为什么需要方法的重写?

子类在继承父类以后,就获取了父类中声明的所有的方法。但是,父类中的方法可能不太适用于子类,换句话说,子类 需要对父类中继承过来的方法进行覆盖、覆写的操作。

举例(银行账户):

代码语言:javascript复制
class Account{//账户
    double balance;//余额

    //取钱
    public void withdraw(double amt){
        //判断balance余额是否够amt取钱的额度
    }
}

class CheckAccount extends Account{ //信用卡
    double protectedBy;//透支额度


    public void withdraw(double amt){
        //判断balance余额是否够amt取钱的额度
        //如果不够,还可以考虑从protectedBy额度里取
    }
}

class AccountTest{
    public static void main(String[] args){
        CheckAccount acct = new CheckAccount();
        acct.withdraw(); //执行的是子类重写父类的方法
    }
}

3.3 区分方法的重载(overload)与重写(override / overwrite)

重载:“两同一不同”

重写:继承以后,子类覆盖父类中同名同参数的方法

4. 关键字 super

super 的理解:父类的

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

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

注意:

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

4.1 为什么需要 super?

  • 举例 1:子类继承父类以后,对父类的方法进行了重写,那么在子类中,是否还可以对父类中被重写的方法进行调用? 可以!
  • 举例 2:子类继承父类以后,发现子类和父类中定义了同名的属性,是否可以在子类中区分两个同名的属性? 可以!

4.2 如何调用?

使用 super 关键字即可。

4.3 super 调用构造器

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

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

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

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

⑤ 由 ③ 和 ④ 得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。 只能是这两种情况之一。

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

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

4.4 super 调用属性、方法

子类继承父类以后,我们就可以在子类的方法或构造器中,调用父类中声明的属性或方法。(满足封装性的前提下)

如何调用呢?需要使用"super."的结构,表示调用父类的属性或方法。

一般情况下,我们可以考虑省略"super."的结构。但是,如果出现子类重写了父类的方法或子父类中出现了同名的属性时, 则必须使用"super."的声明,显式的调用父类被重写的方法或父类中声明的同名的属性。

4.5 this 和 super

this:当前对象

  • 在构造器和非静态代码块中,表示正在 new 的对象
  • 在实例方法中,表示调用当前方法的对象

super:引用父类声明的成员

2、this 和 super 的使用格式

  • this
    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略 this.
    • this()或 this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super
    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或 super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

5. 面向对象特征之三 : 多态

如何理解多态 :

  • 理解:理解为一个事物的多种形态。

生活举例:

女朋友:我想养一个宠物。 孩子:我想要一个玩具。 老板:张秘书,安排一个技术科的同事,跟我一起下周出差。

5.1 多态的形式和体现

5.1.1 对象的多态性:

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

格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)

代码语言:javascript复制
父类类型 变量名 = 子类对象;

举例:

代码语言:javascript复制
Person p = new Student();

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

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

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

5.1.2 多态的理解

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

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) ** “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)**

多态的使用前提:① 类的继承关系 ② 方法的重写

5.2 多态的好处与弊端

弊端:

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

好处:

  • 极大的减少了代码的冗余,不需要定义多个重载的方法。

举例:

代码语言:javascript复制
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();

    }
}

5.3 向上转型和向下转型

首先,一个对象在 new 的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

为什么要类型转换:

因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间​,就会出现类型转换的现象。

但是,使用父类变量接收了子类对象之后,我们就不能调用​ 子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过​。

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的

  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生 ClassCastException,为了安全,可以通过 isInstanceof 关键字进行判断

5.4 如何向上或向下转型

向上转型:自动完成

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

代码语言:javascript复制
package com.atguigu.polymorphism.grammar;

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之间是没有继承关系的
    }
}

5.5 instanceof 关键字

instanceof 的使用:

  1. 建议在向下转型之前,使用 instanceof 进行判断,避免出现类型转换异常
  2. 格式: a instanceOf A : 判断对象 a 是否是类 A 的实例。
  3. 如果 a instanceOf A 返回 true,则: a instanceOf superA 返回也是 true。其中,A 是 superA 的子类。

为了避免 ClassCastException 的发生,Java 提供了instanceof​ 关键字,给引用变量做类型的校验。如下代码格式:

代码语言:javascript复制
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
  • 说明:
    • 只要用 instanceof 判断返回 true 的,那么强转为该类型就一定是安全的,不会报 ClassCastException 异常。
    • 如果对象 a 属于类 A 的子类 B,a instanceof A 值也为 true。
    • 要求对象 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误。

代码:

代码语言:javascript复制
package com.atguigu.polymorphism.grammar;

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();
            }
        }
    }
}

6. Object 类的使用

java.lang.Object​ 是类层次结构的根类,即所有其它类的父类。每个类都使用 Object​ 作为超类。

7. 面试题

==和 equals 的区别:

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址
  • equals 的话,它是属于 java.lang.Object 类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到 String 等类的 equals 方法是被重写过的,而且 String 类在日常开发中用的比较多,久而久之,形成了 equals 是比较值的错误观点。
  • 具体要看自定义类里有没有重写 Object 的 equals 方法来判断。
  • 通常情况下,重写 equals 方法,会比较类中的相应属性是否都相等。

0 人点赞