IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 面向对象三剑客——封装、继承与多态 -> 正文阅读

[Java知识库]面向对象三剑客——封装、继承与多态

文章目录

1. 面向对象基础

1.1 类和对象的使用

Step 1:创建类,设计类的成员
Step 2:创建类的对象
Step 3:通过“对象.属性”或“对象.方法”的形式调用对象的结构

1.2 匿名对象

在创建对象时,未显式的赋给一个变量名,即为匿名对象。匿名对象只能使用一次。再 new 就不是同一个对象了。

method(new Person());

1.3 “万事万物皆对象”

(1)在 Java 语言范畴中,将功能结构等封装到类中,通过类的实例化(对象),来调用具体的功能结构。
(2)涉及到 Java 语言与前端 HTML、后端数据库交互时,前、后端结构在 Java 层面交互时,都体现为类、对象。

1.4 对象的内存解析

堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
栈(Stack):通常所说的栈是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。
方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

练习:画出下列代码的内存解析图

public class Person {
    String name;
    int age = 1;
    boolean isMale;
}
public class PersonTest {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "Tom";
        p1.isMale = true;
        Person p2 = new Person();
        System.out.println(p2.name);   // null
        Person p3 = p1;
        p3.age = 10;
    }
}

内存解析图如下:

1.5 变量的分类(按声明位置)

变量按声明的位置不同可以分为成员变量和局部变量。其中,成员变量等同于类的属性。

1.5.1 属性(成员变量)与局部变量的相同点与不同点

1. 相同点

(1)定义变量的格式:数据类型 变量名 = 变量值;
(2)先声明,后使用
(3)变量都有其对应的作用域

2. 不同点

(1)在类中声明的位置不同:属性定义在类内方法外;局部变量定义在方法内、方法形参、代码块内、构造器形参、构造器内
(2)权限修饰符不同:属性可以使用权限修饰符指明其权限;局部变量不能使用权限修饰符

1.5.2 属性与局部变量的默认初始化

1. 属性

类的属性,根据其类型,都有默认初始化值

  • 整型(byte、short、int、long):0
  • 浮点型(float、double):0.0
  • 字符型(char):0(或\u0000
  • 布尔型(boolean):false
  • 引用数据类型(类、接口、数组):null

2. 局部变量

没有默认初始化值,在调用局部变量之前,一定要显式赋值。特别地,形参在调用时赋值即可。

1.5.3 属性与局部变量在内存中加载的位置

属性(非静态):加载到堆空间
局部变量:加载到栈空间

1.6 方法

1.6.1 return 关键字的再理解

(1)使用范围:在方法体内
(2)作用:① 结束方法;② 针对于有返回值类型的方法,使用“return 数据”的方式返回所需的数据
(3)注意:return 关键字后面不可以声明执行语句

1.6.2 重载

(1)定义

一个类中,允许存在一个以上的同名方法,只要它们的参数个数或参数类型不同即可。

附:“两同一不同”:

  • 同一个类、相同方法名;

  • 参数列表不同(参数个数不同、参数类型不同);

举个栗子

public void getSum(String s,int i){

}

public void getSum(int i,String s){

}

上面两个方法是重载,因为参数列表不同(参数顺序不同)。

// 方法1
public void getSum(int i,int j){
	System.out.println(i + j);
}

// 方法2
public int getSum(int i,int j){
	return 0;
}

// 方法3
public void getSum(int m,int n){

}

// 方法4
private void getSum(int i,int j){

}
  • 方法 1 和方法 2 不是重载,因为参数列表相同(与返回类型无关)。

  • 方法 1 和方法 3 也不是重载,因为参数列表相同(与参数名无关)。

  • 方法 1 和方法 4 也不是重载,因为参数列表相同(与方法的权限无关)。

(2)判断是否重载

跟方法的权限修饰符、返回值类型、形参变量名、方法体都没有关系。

public class OverLoadTest {
    public static void main(String[] args) {
        OverLoadTest overLoadTest = new OverLoadTest();
        overLoadTest.getSum(1,2);   // 这里进行了自动类型提升
    }

//    public void getSum(int num1,int num2){
//        System.out.println("你调用了第一个方法");
//    }

    public void getSum(double num1,double num2){
        System.out.println("你调用了第二个方法");
    }
}

注意:上面代码中,两个getSum 方法是重载。注释掉第一个方法后,在调用方法时,虽然传入的参数是两个 int 型的数,但它们会进行自动类型提升,从而调用第二个方法。

1.6.3 可变个数形参

JDK 5.0 新增的特性,其作用相当于数组

具体使用

(1)格式:数据类型 ... 变量名

(2)当调用具有可变个数形参的方法时,传入的参数个数可以是:0 个,1 个,2 个……

(3)具有可变个数形参的方法与本类中方法名相同、形参不同的方法之间构成重载

(4)具有可变个数形参的方法与本类中方法名相同、形参为相同类型数组的方法不构成重载

(5)可变个数形参作为参数时,必须声明在参数列表的末尾

(6)方法中最多只能声明一个可变形参

(7)具有可变个数形参的方法与形参为数组的方法的遍历方法相同,赋值方法不同

说明:

(3)下面的两个方法构成重载

public void show(String str){
    System.out.println(str);
}

public void show(String ... strs){
    for (int i = 0; i < strs.length; i++) {
        System.out.print(strs[i] + " ");
    }
}

(4)下面代码相当于定义了一个方法,报错

(5)可变个数形参必须声明在参数列表的末尾

(6)方法中最多只能声明一个可变个数形参

(7)

public class ChangedArgsTest4 {
 public static void main(String[] args) {
     ChangedArgsTest4 c = new ChangedArgsTest4();

       // 具有可变个数形参的方法可以直接在形参列表中赋值
       // 而形参为数组的方法的赋值必须遵循数组的赋值方法,如下
       c.show(new String[]{"hello","my","name","is","hstar"});

 }

 // 可变形个数参具有和数组一样的属性,如 length
 //    public void show(String ... strs){
 //        for (int i = 0; i < strs.length; i++) {
 //            System.out.print(strs[i] + " ");
 //        }
 //    }

  // 和上面具有可变个数形参的方法等效
  public void show(String[] str){
        for (int i = 0; i < str.length; i++) {
            System.out.print(str[i] + " ");
       }
  }
}

1.6.4 参数传递机制

1. 关于变量的赋值

  • 如果变量是基本数据类型,此时赋值的是变量所保存的数据值

  • 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值

附:

引用数据类型变量存储的,不仅包含地址,还包括数据类型。

如下,arr1 和 arr2 存储的不仅包含地址还包含数据类型,两者数据类型不同,自然不能赋值。

2. 关于形参与实参

  • 形参:方法定义时,声明在小括号内的参数
  • 实参:方法调用时,实际传递给形参的数据

3. 种类

纵观所有的高级编程语言,方法参数的传递机制可以分为三种:按名调用、按值调用和按引用调用。

什么是值传递,什么又是引用传递?
  • 值传递:是指在调用方法时将实参复制一份传递到方法中,即实参与形参没有关系。如果在方法中对参数进行修改,将不会影响到实参。
  • 引用传递:是指在调用方法时将实参的地址传递到方法中,如果在方法中对参数进行修改,将影响到实参。

附:按值调用(call by value)和值传递(pass by value)说的是一个事,其他两种以此类推。这里习惯用值传递这种称呼。

4. 证明 Java 中是值传递

既然在 C++ 中有值传递和引用传递,那么就有人猜了在 Java 也是这样的,基本数据类型是值传递,引用数据类型是引用传递。乍一听感觉很有道理,但这个猜测是不正确的,因为 Java 中只有值传递。

Java 中的变量按数据类型分为基本数据类型和引用数据类型,现在就分别举例说明这两种变量的参数传递方式都是值传递。

(1)基本数据类型
public class ParamTest2 {
    public static void main(String[] args) {
        int num1 = 3;
        int num2 = 5;
        System.out.println("交换前:num1 = " + num1 + ", num2 = " + num2);
        swap(num1,num2);
        System.out.println("交换后:num1 = " + num1 + ", num2 = " + num2);
    }

    public static void swap(int x, int y){
        int temp = x;
        x = y;
        y = temp;
    }
}

上述程序的初衷是交换两个数的值,但结果并不如人意,没有实现交换,可以画一张内存中变量加载的图来解释。

由上图可知,在调用 swap 方法时,是将实参复制了一份传入到方法中,这时形参与实参是相互独立的。因此方法执行时交换的是形参的值,而不是实参的值。

(2)引用数据类型
public class ParamTest3 {
    public static void main(String[] args) {
        Student student1 = new Student("小明");
        Student student2 = new Student("小美");
        System.out.println("交换前:student1 = " + student1.getName());
        System.out.println("交换前:student2 = " + student2.getName());
        System.out.println("********我是一条可爱的分割线********");
        swap(student1,student2);
        System.out.println("********我是一条可爱的分割线********");
        System.out.println("交换后:student1 = " + student1.getName());
        System.out.println("交换后:student2 = " + student2.getName());
    }

    public static void swap(Student s1, Student s2){
        Student temp = s1;
        s1 = s2;
        s2 = temp;
        System.out.println("swap方法中:s1 = " + s1.getName());
        System.out.println("swap方法中:s2 = " + s2.getName());
    }
}

class Student{
    private String name;

    public String getName() {
        return name;
    }

    public Student(String name) {
        this.name = name;
    }
}

上述代码在内存中加载的示意图如下所示:

如果是引用传递,则调用 swap 方法后,student1 与 student2 指向将发生改变,但事实上并未改变。由上图可知,在调用方法时,是将实参的地址值复制了一份传入到方法中,这时形参与实参是相互独立的,因此修改形参并不会影响实参。

练习

public class TransferTest3 {
    public static void main(String args[]) {
        TransferTest3 test = new TransferTest3();
        test.first();
    }
    
    public void first() {
        int i = 5;
        Value v = new Value();
        v.i = 25;
        second(v, i);
        System.out.println(v.i);
    }
    
    public void second(Value v, int i) {
        i = 0;
        v.i = 20;
        Value val = new Value();
        v = val;
        System.out.println(v.i + " " + i);
    } 
}

class Value {
    int i = 15;
}

内存解析图:

2. 三剑客之一——封装性

在实际问题中,我们往往需要给属性赋值加入额外的限制条件,这个条件不能在属性声明时体现,只能通过方法进行限制条件的添加。同时,我们需要避免用户再使用“对象.属性”的方式对属性进行赋值,则需要将属性声明为私有的。

2.1 体现

  • 将类的属性私有化,同时提供公共的方法来获取和设置属性的值
  • 不对外暴露的私有方法,仅供类内的其他方法调用
  • 单例模式(私有化构造器)

2.2 实现:使用权限修饰符

四种权限修饰符:

修饰符类内部同一个包不同包的子类同一个工程
private
(缺省)
protect
public

附:

关于权限修饰符

  • 权限大小(由小到大):private、缺省、protected、public

  • 4 种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类

    修饰类的话,只能使用:public、缺省

关于类的命名:

  • 在 Java 中,同一包下不能有同名的类,但在不同的包下可以重名

3. 构造器(构造方法)

3.1 特征

  • 与类名相同
  • 没有返回值(不是 void)
  • 不能被 static、final、synchronized、abstract、native 修饰,不能有 return 返回语句

3.2 构造器的作用

  • 创建对象
  • 给对象进行初始化

如:

Student student = new Student();

Teacher teacher = new Teacher("Tom",35);

3.3 说明

(1)定义构造器的格式:权限修饰符 类名(形参列表){}

(2)如果没有显式的定义类的构造器,则系统会提供一个默认的无参构造器

(3)如果在一个类中显式的定义了构造器,则系统不再提供默认的无参构造器

(4)一个类中至少会有一个构造器

(5)同一个类中定义的多个构造器,彼此构成重载

(6)抽象类中有构造器,接口中无构造器

附:需要指出的是,构造器和方法还是有区别的。构造器用于创建对象,而方法(非静态)是由对象调用以实现某些功能。

3.4 属性赋值的先后顺序

顺序如下

(1)默认初始化

(2)显式初始化

(3)构造器中赋值

(4)通过"对象.方法"或"对象.属性"的方式进行赋值

附:

  • 前三个只执行一次,(4)可以根据需要执行多次
  • 赋值结果与赋值顺序相反,从后往前看

3.5 JavaBean

JavaBean 是一种用 Java 语言写的可重用组件,符合如下要求:

  • 类是公共的
  • 有一个无参的公共构造器
  • 有属性(一般为 private 的),且有对应的 get、set 方法

附:每个 JavaBean 都对应数据库中的一张表,而 JavaBean 中的属性与数据库表中的字段对应。

4. this 关键字

(1)this 可以用来修饰:属性、方法、构造器

(2)this 修饰属性和方法可以理解为当前对象或当前正在创建的对象:

  • 在类的方法中,可以使用"this.属性"或"this.方法"的方式,调用当前对象的属性或方法。但在通常情况下,都省略"this."。特殊情况下,如果方法的形参和类的属性同名,就必须显式的使用"this.变量"的方式,表明此变量是属性而非形参。
  • 在类的构造器中,可以使用"this.属性"或"this.方法"的方式,调用当前正在创建的对象的属性或方法。但在通常情况下,都省略"this."。特殊情况下,如果构造器的形参和类的属性同名,就必须显式的使用"this.变量"的方式,表明此变量是属性而非形参。

(3)this 调用构造器

  • 在类的构造器中,可以显式的使用"this(参数列表)"的方式,调用本类中指定的其他构造器
  • 在构造器中不能使用"this(参数列表)的方式调用自己"
  • 如果一个类中有 n 个构造器,则最多有 n - 1 个构造器中使用了"this(参数列表)"
  • 规定:"this(参数列表)“必须声明在当前构造器的首行”
  • 构造器内部最多只能声明一个"this(参数列表)",用来调用其他的构造器

附:

  • 一个类中有 n 个构造器,其中的 n - 1 个构造器都调用了本类中的其他构造器,剩下的一个构造器不能在调用本类中的构造器,否则这种调用关系会“成环”,引发悲剧后果
  • 因为"this(参数列表)"必须写在首行,所以只能使用一次该声明(联系可变个数的形参只能定义一个)

5. package 关键字

(1)使用 package 关键字声明类或接口所属的包,声明在源文件的首行

(2)包属于标识符,遵循标识符的命名规则、规范、“见名知意”

(3)每"."一次,就代表一层文件目录

(4)同一包下,不能命名同名的类、接口;不同的包下,可以命名同名的类、接口

6. import 关键字

(1)声明在包的声明和类的声明之间

(2)在源文件中显式的使用 import 结构导入指定包下的类、接口

(3)如果需要导入多个结构,则可以并列写出(多行)

(4)可以使用"xxx.*"的方式,导入 xxx 包下的所有结构

(5)使用"xxx.*"的方式后,可以调用 xxx 包下的所有结构。但如果想使用 xxx 子包下的结构,需要显式导入子包下的结构

(6)如果使用的类、接口是 java.lang 包下定义的,则可以省略 import 结构

(7)如果使用的类、接口是本包中定义的,则可以省略 import 结构

(8)如果在源文件中使用了不同包下的同名的类,则必须至少有一个类需要以全类名的方式显示

(9)import static:导入指定类或接口中的静态结构(属性或方法)

package com.hstar.oopexer8;

import static java.lang.System.out;   // 导入静态结构

public class BoyGirlTest {
    public static void main(String[] args) {
        out.println("hello");
    }
}

7. 三剑客之二——继承性

7.1 使用继承的优点

(1)减少了代码的冗余,提高了代码的复用性

(2)便于功能的扩展

(3)多态使用的前提

7.2 继承的格式

class A extends B{}

其中,A 称为子类、派生类、subclass,B 称为超类、基类、superclass

7.3 继承的体现

  • 子类继承父类后,子类就会获取父类中声明的所有属性和方法(原因见“10.子类对象的实例化过程”。特别的,父类中声明的 private 的属性或方法,子类也可以在继承父类后获取,只是受封装性的影响,使得子类不能直接调用父类的结构。
  • 子类继承父类以后,还可以声明自己特有的属性或方法,以实现功能的扩展。

证明子类继承父类后,可以获取父类 private 的属性和方法:

package com.hstar.oop.extend.prove;

public class Person {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void study(){
        System.out.println("刻苦学习……");
        gaming();
    }

    private void gaming(){
        System.out.println("放松娱乐来一把……");
    }
}
package com.hstar.oop.extend.prove;

public class Student extends Person{

}
package com.hstar.oop.extend.prove;

public class Test {
    public static void main(String[] args) {
        Student student = new Student();
        // 证明继承了私有属性
        student.setName("南宫辉星");
        System.out.println(student.getName());
        // 证明继承了私有方法
        student.study();
    }
}

7.4 Java 中继承的规定

(1)一个类可以被多个子类继承

(2)Java 中类是单继承的,即一个类只能有一个(直接)父类

(3)子父类是一种相对概念。子类直接继承的父类称为直接父类,间接继承的父类称为间接父类

(4)子类继承父类后,就获取了直接父类以及所有间接父类中声明的属性和方法

(5)如果没有显式的声明一个类的父类,则此类继承于 java.lang.Object 类。Java 中所有的类(除 java.lang.Object 类外)都直接或间接的继承于 java.lang.Object 类。意味着,所有的 Java 类都具 有 java.lang.Object 类声明的功能。

8. 重写方法

8.1 什么是重写?

子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作。

8.2 重写的应用

重写以后,当创建子类对象以后,通过子类对象调用父类中同名同参数的方法时,实际执行的是子类重写父类的方法。(这里不是多态的应用)

8.3 重写的规定

方法的声明:权限修饰符 返回值类型 方法名(形参列表) throws 异常类型{ }

约定:子类中的叫做重写的方法,父类中的叫做被重写的方法。

(1)方法名和形参列表:

  • 子类重写的方法的方法名和形参列表与父类中被重写的方法的方法名和形参列表相同

(2)权限修饰符:

  • 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符

    特别地,子类不能重写父类中声明为 private 的方法

(3)返回值类型:

  • 父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void
  • 父类被重写的方法的返回值类型是 A 类型,则子类重写的方法的返回值类型可以是 A 类或 A 类的子类
  • 父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型

(4)异常类型:

  • 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型

附:子类和父类中同名同参数的方法都声明为非 static 的,是重写;声明为 static 的不是重写。

重写思维导图

9. super 关键字

super 关键字可以调用:属性、方法、构造器

(1)调用属性和方法

  • 可以在子类的构造器中,通过"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。通常情况下,习惯省略"super."

    ① 特别地,当子类和父类中定义了同名的属性时,如果想在子类中调用父类中声明的同名属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。

    ② 特别地,当子类重写了父类中的方法后,如果想在子类中调用父类中被重写的方法,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。

(2)调用构造器

  • "super(形参列表)"必须声明在构造器的首行

  • 可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定构造器

  • 在类的构造器中,"this(形参列表)"和"super(形参列表)"只能二选一,不能同时出现

  • 在构造器的首行,如果没有显式的声明"this(形参列表)“或"super(形参列表)”,则默认使用"super()"调用父类的空参构造器

  • 在类的多个构造器中,至少有一个类的构造器中使用了"super(形参列表)",调用父类中的构造器

附:

  1. 一种极端情况:类中有 n 个 构造器,且其中 n-1 个构造器使用了 this 关键字调用其他的构造器,这时剩下的一个构造器不能再使用 this 调用其他构造器,所以它要么默认调用父类的空参构造器,要么调用父类指定的构造器。
  2. 如果父类中声明了有参的构造器,而没有声明无参构造器,则父类中没有无参构造器。此时,如果子类中没有显式声明构造器,则子类中有一个默认无参构造器,而该默认无参构造器会调用父类的无参构造器,所以此时程序报错。

10. 子类对象的实例化过程

(1)从结果上看:子类继承父类后,就获取了父类中声明的属性或方法。创建子类的对象,就会在堆空间中加载所有父类中声明的属性。

(2)从过程上看:当通过子类的构造器创建对象时,一定会直接或间接的调用父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类中的空参构造器为止。正因为加载过所有父类的结构,所以内存中会有父类的结构,子类对象才可以调用。

附:

  • 虽然在创建子类对象时调用了父类的构造器,但自始至终就创建过一个对象,即为 new 的子类对象。
  • 为什么子类继承父类后,可以拥有父类的属性?
    • 因为子类在通过构造器创建对象时,会直接或间接的调用父类的构造器,会加载父类的结构。

子类对象实例化图示:

11. 三剑客之三——多态性

11.1 何为多态?

对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)

11.2 多态的使用

(1)多态性的使用前提:① 子类继承父类;② 方法的重写;③ 父类引用指向子类对象

(2)有了对象的多态性后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子 类重写父类的方法。其中,调用的方法称为虚拟方法。上述可以总结为:编译看左边,运行看右 边。

(3)多态性只适用于方法,不适用于属性(属性的编译、运行都看左边)。

  • 若子类重写了父类方法,就意味着子类中定义的方法彻底覆盖了父类中的同名方法,系统不可能把父类中的方法转移到子类中:编译看左边,运行看右边

  • 对于实例变量,即使子类中定义了与父类中完全相同的实例变量,子类中的实例变量也不会覆盖父类中定义的实例变量:编译、运行都看左边

举个栗子:

public class Base {
    int num = 10;

    public void display(){
        System.out.println(this.num);
    }
}
public class Sub extends Base{
    int num = 20;
    public void display(){
        System.out.println(this.num);
    }
}
public class Test {
    public static void main(String[] args) {
        Sub s = new Sub();
        System.out.println(s.num);   // 20
        s.display();   // 20
        System.out.println("********我是一条可爱的分割线********");
        Base b = s;
        System.out.println(b == s);   // true
        System.out.println(b.num);   // 10(多态不适用于属性)
        b.display();   // 20
    }
}

附:

  • 如果子类没有重写父类的方法,使用多态就没有意义了。

  • 虚拟方法调用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法。在调用虚拟方法时,编译看左,运行看右。

    问题:怎样才能执行父类的方法?答:父类设计出来的目的,就是为了实现继承,最终实现多态,可能实际中并不需要执行父类的方法。如果确实需要执行,就创建父类的对象。

虚拟方法的调用

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

静态绑定与动态绑定

静态绑定:在方法调用之前,或者说在编译期,编译器就已经确定了所要调用的方法。

动态绑定:只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法。

重载是静态绑定,重写(使用多态)是动态绑定。

  • Bruce Eckel 说:“不要犯傻,如果它不是晚绑定,它就不是多态。”
  • 重载可以是子类重载父类的方法,但在方法调用时是静态绑定,因此重载不是多态。(为什么呢?因为子类在继承父类时,继承了方法,则可以重载该方法。严谨来说,重载发生在一个类中,即使是子类中

11.3 为什么使用多态(以println方法为例)

看源码得知:println 方法调用了 Object 类的 toString 方法,由于这里使用了多态,所以最终调用的是 Student 类重写的方法。

println 方法:

(1)如果传入的参数调用了 toString 方法,则直接调用该参数的 toString 方法。如果没有重写 toString 方法,就调用子类从 Object 类中继承过来的;如果重写了,调用子类重写。

(2)如果传入的参数没有调用 toString 方法,则会调用 println 中的 toString 方法,流程是:先将传入的对象向上转型为 Object 类型的,然后实现了父类引用在调用虚拟方法时,调用的是子类重写后的方法。

可以想象:如果不使用多态,println 方法就需要为每一个对象写一次。而有了多态,不论传入什么类型的对象,都向上转型为 Object 类型,然后通过动态绑定实现调用子类重写的方法。

11.4 向上转型与向下转型

为什么使用向下转型?

? 有了对象的多态性后,内存中实际上加载了子类特有的属性和方法,但由于变量声明为父类类型,导致编译时只能调用父类中声明的属性和方法,子类中特有的属性和方法不能调用

? 为了调用子类中特有的属性和方法,需要使用向下转型,即使用强制类型转换符,将父类引用转换为对应的子类对象。

在进行向下转型时,可能会出现 ClassCastException 的异常。为了避免出现该异常,可以在向下转型之前使用 instanceof 关键字。

instanceof 关键字

a instanceof A:判断对象 a 是否为类 A 的实例。如果是,返回 true;如果不是,返回 false。

类 B 是类 A 的父类:

如果a instanceof A 返回 true,则 a instanceof B 也返回 true。


多态的主体内容已完结,关于其他 ↓ ↓ ↓


11.5 可变个数形参在多态中的体现

package com.hstar.oop.polymorphism.exercise4;

public class Test {
    public static void main(String[] args) {
        Base base = new Sub();
        base.add(1,2,3);   // sub_1

        Sub sub = (Sub) base;
        sub.add(1,2,3); // sub_2,这里是重载,确定参数形参的优先级高于可变个数形参
    }
}

class Base{
    public void add(int a, int ... arr){
        System.out.println("base");
    }
}

class Sub extends Base{
    public void add(int a, int[] arr){   // 可变个数形参和数组等价,这里是方法的重写
        System.out.println("sub_1");
    }

    public void add(int a, int b, int c){
        System.out.println("sub_2");
    }
}

总结:可变个数形参和数组等价。

11.6 思考:如果未重写父类的方法

? 如下,Student 类只是继承了 Person 类,而并未重写 eat 方法。所以父类引用 p 调用的是子类继承过来的 eat 方法。

public class Person {
    public void eat(){
        System.out.println("吃饭");
    }
}

class Student extends Person{

}
public class PersonTest {
    public static void main(String[] args) {
        Person p = new Student();
        System.out.println(p.getClass());   // class com.hstar.oopexer7.Student
    }
}

多态思维导图

练习

练习1

面试题:

多态是编译时行为还是运行时行为?如何证明?

package com.atguigu.test;

import java.util.Random;

class Animal {
    protected void eat() {
        System.out.println("animal eat food");
    }
}

class Cat extends Animal {
    protected void eat() {
        System.out.println("cat eat fish");
    }
}

class Dog extends Animal {
    public void eat() {
        System.out.println("Dog eat bone");
    }
}

class Sheep extends Animal  {
    public void eat() {
        System.out.println("Sheep eat grass");
    }
}

public class InterviewTest {
    public static Animal getInstance(int key) {
        switch (key) {
            case 0:
                return new Cat ();
            case 1:
                return new Dog ();
            default:
                return new Sheep ();
        }

    }

    public static void main(String[] args) {
        int key = new Random().nextInt(3);
        System.out.println(key);
        Animal animal = getInstance(key);
        animal.eat(); 
    }
}

12. Object 类

12.1 Object 类说明

(1)Object 类是所有 Java 类的根父类

(2)如果在类的声明中未使用 extends 关键字指明其父类,则默认父类为 java.lang.Object 类

(3)Object 类中的功能(属性、方法)具有通用性

(4)Object 类中只声明了一个空参构造器

12.2 面试题:== 与 equals 的区别

(1)运算符:==

可以使用在基本数据类型变量和引用数据类型变量中

① 如果比较的是基本数据类型变量,则比较两个变量保存的数据是否相等(两个变量不一定类型相同)

byte b = 1;
short s = 2;
float f = 1.0f;
double d = 1.0;
System.out.println(b == f);   // true(自动类型提升)
System.out.println(s == d);   // false

② 如果比较的是引用数据类型变量,则比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体(必须保证符号两边的变量类型一致

Person per = new Person();
Student stu = new Student();
System.out.println(per == stu);   // 编译出错

(2)equals 方法

  • 是一个方法,而非运算符

  • 只适用于引用数据类型

  • Object 类中 equals 方法的定义:

    public boolean equals(Object obj) {
        return (this == obj);
    }
    

    Object 类中定义的 equals 方法和运算符 == 的作用等效,比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体

  • 如 String、Date、File、包装类等都重写了 Object 类中的 equals 方法。重写后,比较的不再是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同

  • 通常情况下,我们自定义的类中如果使用 equals 方法,愿景也是比较两个对象的“实体内容”是否相同。那么,就需要重写 equals 方法。

    重写的原则:比较两个对象的“实体内容”是否相同

12.3 重写 equals 方法

重写的思路:

? 传参时,进行了向上转型,转换为 Object 类型;

  1. 首先,用双等判断两个引用指向的是否为同一个地址。是,两个对象相同;否,进入 2;
  2. 判断传入的参数是否为本类类型(使用 instanceof)。否,两个对象不同;是,把传入的参数向下转型为本类类型,并进入 3;
  3. 用双等判断基本数据类型的属性;用 equals 判断引用数据类型的属性,如果是 String 等类型,由于 JDK 已经重写,不需要手动重写;否则,需要重写。
public boolean equals(Object obj){
    if(this == obj){
        return true;
    }
    if(obj instanceof Student){
        Student stu = (Student) obj;
        // return this.id == stu.id && this.name == stu.name;
        return this.id == stu.id && this.name.equals(stu.name);
    }
    return false;
}

附:

总结:在重写 equals 方法时,如果需要比较的属性是引用类型的变量,则在判断两个属性的内容是否相同时,需要考虑此处调用的 equals 方法是否需要重写。但如果是 String 类型的变量,可以直接使用 equals 方法,因为该方法已经被重写;而且,String 也可以直接使用 == 。

12.4 toString 方法

(1)输出一个对象的引用时,实际上调用了当前对象的 toString 方法

(2)Object 类中 toString 方法的定义:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

(3)如String、Date、File、包装类等都重写了 Object 类中的 toString 方法,使得在调用对象的 toString 方法时,返回的是“实体内容”

12.5 关于引用数据类型的根父类

引用数据类型包括类、接口和数组。类和数组的根父类是 Object 类;而接口相对独立

package com.hstar.commonclass.objectclass.subclass;

import org.junit.Test;

public class SubClass {
    @Test
    public void test(){
        int[] arr = new int[]{1,2,3};
        System.out.println(arr instanceof Object);   // true
        System.out.println(arr.getClass());   // class [I
        System.out.println(arr.getClass().getSuperclass()); // class java.lang.Object
    }
}

总结:数组也作为 Object 类的子类,可以调用 Object 类中声明的方法。

13. 包装类

13.1 为什么使用包装类

为了使基本数据类型的变量具有类的特征。

基本数据类型与包装类的对应关系:

13.2 基本数据类型 <—> 包装类

1. 基本数据类型 —> 包装类

(1)构造器1,参数为相应的基本数据类型;

(2)构造器2,字符串参数

  • 对于构造器 1,所有转换都类似

  • 对于构造器 2,不同的包装类,有一些差别,具体如下:

    数值型:如 int、float 等,在使用构造器 2 时,字符串里面的内容应该严格是该类型的数值,否则报错,如Integer integer = new Integer("123abc"); → NumberFormatException。

    布尔型:在忽略大小写的情况下,如果字符串中不是“true”,结果就是 false。

Boolean b1 = new Boolean(true);
Boolean b2 = new Boolean("TrUe");   
Boolean b3 = new Boolean("true123");
System.out.println(b1);   // true
System.out.println(b2);   // true(忽略大小写)
System.out.println(b3);   // false(不是true就是false)

2. 包装类 —> 基本数据类型

调用包装类 Xxx 的 "XxxValue()"方法

Integer integer = new Integer(10);
int i = integer.intValue();
System.out.println(i);

3. 自动装箱与自动拆箱

  • JDK 5.0 新特性

  • 自动装箱:基本数据类型 → 包装类

    自动拆箱:包装类 → 基本数据类型

Integer integer = 10;   // 自动装箱
int num = integer;   // 自动拆箱

13.3 基本数据类型、包装类与 String 类型相互转换

1. 前者 → 后者

  • 方式一:连接运算

    int num1 = 10;
    String str1 = num1 + "";
    System.out.println(str1);
    
  • 方式二:调用 String 重载的 valueOf 方法

    String str2 = String.valueOf(10);   // 基本数据类型
    Integer integer = 11;
    String str3 = String.valueOf(integer);   // 包装类
    System.out.println(str2);
    System.out.println(str3);
    

2. 后者 → 前者

调用包装类的 parseXxx 方法

String str4 = "111";
int i = Integer.parseInt(str4);
System.out.println(i);

String str5 = "TruE";   // 不区分大小写的 true,其结果都是“true”
boolean b = Boolean.parseBoolean(str5);
System.out.println(b);

总结:

练习

public class Interview {
    @Test
    public void test1(){
        Object o1 = true ? new Integer(1) : new Double(2.0);// 三元运算符在编译时会进行自动类型提升
        System.out.println(o1);   // 1.0
    }

    @Test
    public void test2(){
        Object o2;
        if(true)
            o2 = new Integer(1);
        else
            o2 = new Double(2.0);
        System.out.println(o2);   // 不存在自动类型提升
    }

    @Test   // 练习3
    public void test3(){
        Integer i = new Integer(1);
        Integer j = new Integer(1);
        System.out.println(i == j);   // false

        Integer m = 1;
        Integer n = 1;
        System.out.println(m == n);   // true

        Integer x = 128;
        Integer y = 128;
        System.out.println(x == y); // false
    }
}

对于练习3:

Integer 内部定义了 IntegerCache 结构,IntegerCache 中定义了 Integer[],该数组中保存了 -128 ~ 127 范围内的整数。如果使用自动装箱的方式,给 Integer 赋值的范围在 -128 ~ 127 范围内时,可以直接使用数组中的元素,不用再去 new 了。目的:提高效率。

14. static 关键字

14.1 static 修饰内容

static 可以用来修饰:属性、方法、代码块、内部类

(1)static 修饰属性

static 修饰的属性称为静态变量类变量;没用 static 修饰的属性称为实例变量。

  • 实例变量:如果创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样属性值的修改。
  • 静态变量:如果创建了类的多个对象,这些对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象在调用此静态变量时,是修改过了的。

附:变量按声明的位置可以分为成员变量和局部变量:

  • 成员变量:在方法体外,类体内声明的变量

  • 局部变量:在方法体内部声明的变量

注意:两者在初始化值方面的异同:

同:都有声明周期

异:局部变量除形参外,需显式初始化

(2)static 修饰方法

static 修饰的方法称为静态方法。关于静态方法:

  • 随着类的加载而加载,可以通过"类.静态方法"的方式进行调用

  • 关于静态方法与非静态方法的调用:

    静态方法非静态方法
    可以调用不可以调用
    对象可以调用可以调用
  • 静态方法中只能调用静态的方法或属性;非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性。

    附:静态方法中也可以调用非静态方法,只不过需要通过“对象.方法”的形式调用。

    public class StaticTest {
        public static void main(String[] args) {
            StaticTest staticTest = new StaticTest();
            staticTest.show();    // 利用对象调用非静态方法
        }
    
        public void show(){
            System.out.println("我是一个非静态方法");
        }
    }
    

14.2 static 其他说明

(1)static 的“禁忌”

  • static 不能修饰构造器

    static 修饰的是与对象无关的内容,而构造器却要生成对象。另外。在时间维度上,static 修饰的内容随着类的加载而加载,而对象的创建要晚于类的加载。

  • 在静态方法中,不能使用 this、super 关键字;在静态方法中,不能访问非静态的属性和方法

    由于静态方法是随着类的加载而加载的,这时还没有对象。在静态方法的内部无法确定非 static 的内容属于哪个对象,同样地使用 this 也是模糊不清的。

  • static 修饰的方法不能被重写,但可以被继承

    ① 证明 static 修饰的方法不能被重写

    package com.hstar.oop.static_keyword;
    
    public class StaticTest {
        public static void main(String[] args) {
            Base base = new Son();
            base.method1();   // Base_method1。如果能被重写。此处应该体现多态
            base.method2();   // Son_method2
        }
    }
    
    class Base{
        public static void method1(){
            System.out.println("Base_method1");
        }
    
        public void method2(){
            System.out.println("Base_method2");
        }
    }
    
    class Son extends Base{
        public static void method1(){
            System.out.println("Son_method1");
        }
    
        public void method2(){
            System.out.println("Son_method2");
        }
    }
    

    ② 证明 static 修饰的方法可以被继承

    package com.hstar.oop.static_keyword;
    
    public class StaticTest1 {
        public static void main(String[] args) {
            Child.method();
        }
    }
    
    class Parent{
        public static void method(){
            System.out.println("Parent");
        }
    }
    
    class Child extends Parent{
    //    public static void method(){
    //        System.out.println("Child");
    //    }
    }
    

    附:静态的方法可以被继承,但是不能重写。

    如果父类中有一个静态的方法,子类也有一个与其方法名、参数类型、参数个数都一样的 static 方法,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。也就是说父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法要看是哪个对象的引用,因而这种父子类方法不存在多态的性质。

(2)如何运用 static?

开发中,如何确定一个属性是否要声明为 static 的?

  • 属性可以被多个对象共享,不会随着对象的不同而不同
  • 类中的常量通常声明为 static 的

开发中,如何确定一个方法是否要声明为 static 的?

  • 操作静态属性的方法,通常声明为 static 的
  • 工具类中的方法,通常声明为 static 的。如 Math、Arrays、Collections等

(3)静态变量与实例变量的内存解析

静态变量随着类的加载而加载,加载到方法区的静态域中。

举个栗子:

package com.hstar.oop.static_keyword;

public class Student {
    int id;
    String name;
    static String school;
}
package com.hstar.oop.static_keyword;

public class StudentTest {
    public static void main(String[] args) {
        Student.school = "清华大学";
        Student student1 = new Student();
        student1.id = 1001;
        student1.name = "南宫辉星";
        Student student2 = new Student();
        student2.id = 1005;
        student2.name = "刘银河";
        student1.school = "北邮";
        student2.school = "南大";
    }
}

内存解析图如下:

14.3 static 关键字的应用 —— 单例模式

单例设计模式,就是采取一定的方法保证在整个软件体系中,对某个类只能存在一个对象实例。单例模式有两种:饿汉式单例模式和懒汉式单例模式。

(1)饿汉式单例模式

  • 私有化构造器
  • 在类的内部创建对象,并且声明为 static 的
  • 提供 public、static 的方法,返回类的对象,用于其他类使用本类对象
package com.hstar.oop.designpatterns.singleton;

public class SingletonTest1 {
    public static void main(String[] args) {
        Bank bank1 = Bank.getInstance();
        Bank bank2 = Bank.getInstance();
        System.out.println(bank1 == bank2);   // true
    }
}

class Bank{
    // 1. 私有化类的构造器
    private Bank(){

    }
    // 2. 在类的内部创建对象
    // 4. 要求此对象也必须声明为静态的
    private static Bank instance = new Bank();

    // 3. 提供公共的静态方法,返回类的对象,用于其他类使用本类对象
    public static Bank getInstance(){
        return instance;
    }
}

(2)懒汉式单例模式

  • 私有化构造器
  • 声明类的对象,不初始化,且此对象必须声明为 static 的
  • 提供 public、static 的方法,返回类的对象,用于其他类使用本类对象
package com.hstar.oop.designpatterns.singleton;

public class SingletonTest2 {
    public static void main(String[] args) {
        Order order1 = Order.getInstance();
        Order order2 = Order.getInstance();
        System.out.println(order1 == order2);   // true
    }
}

class Order{
    // 1. 私有化类的构造器
    private Order(){

    }

    // 2. 声明类的对象,不初始化
    // 4. 此对象也必须声明为static的
    private static Order instance = null;

    // 3. 声明public、static方法,返回当前类的对象
    public static Order getInstance(){
        if(instance == null){
            instance = new Order();
        }
        return instance;
    }
}

(3)饿汉式 VS 懒汉式

饿汉式:

  • 优点:线程安全
  • 确定:对象加载时间过长

懒汉式:

  • 优点:延迟对象的创建
  • 缺点:目前线程不安全

(4)单例模式的优点与应用场景

单例模式的优点:

由于单例模式只生成一个实例,减少了系统的性能开销。当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过应用启动时直接产生一个单例对象,然后永久驻留内存的方式解决。如 java.lang.Runtime。

单例模式的应用场景:

  • 网站的计数器,一般也是单例模式实现,否则难以同步。

  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。

  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,都生成一个对象去读取。

  • Application 也是单例的典型应用

  • Windows 的 Task Manager (任务管理器)就是很典型的单例模式

  • Windows 的 Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

附:

单例模式的设计思想就是只产生一个对象,下面提供一种非典型的单例模式:

package com.hstar.oop.designpatterns.singleton;

// 也是一种单例模式,只是程序的可扩展性差
public class SingletonTest3 {
    public static void main(String[] args) {
        Student stu1 = Student.instance;
        Student stu2 = Student.instance;
        System.out.println(stu1 == stu2);   // true
        //        Student.instance = null;
    }
}

class Student{
    private Student(){

    }
	// 定义为静态常量,只能调用且不可修改
    public static final Student instance = new Student();   
}

static 的主体内容已完结,关于其他 ↓ ↓ ↓


14.4 main 的再理解

public static void main(String[] args){ //方法体}

main 方法的使用说明:

  • main 方法作为程序的入口
  • main 方法也是一个静态方法
  • main 方法可以作为与控制台交互的方式(也可以使用 Scanner)

static 思维导图

15. 代码块

  • 类的成员之一。

  • 代码块的作用:用来初始化类、对象

  • 代码块如果有修饰的话,只能使用 static

  • 代码块分为静态代码块和非静态代码块

(1)静态代码快

  • 内部可以定义 Java 语句

  • 随着类的加载而加载,而且只执行一次

  • 作用:初始化类的信息

  • 如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行

    静态代码块的执行要优先于非静态代码块的执行

  • 静态代码块内只能调用静态的属性、方法,不能调用非静态的结构

(2)非静态代码块

  • 内部可以定义 Java 语句
  • 随着对象的创建而执行,每创建一个对象,就执行一次非静态代码块
  • 作用:可以在创建对象时,对对象的属性等进行初始化
  • 如果一个类中定义了多个非静态的代码块,则按照声明的先后顺序执行
  • 非静态代码块可以调用静态的属性、方法,或非静态的属性、方法

代码块主线:

① 作用:初始化类、对象

② 内容:输出语句、初始化语句。静态中只能调用静态属性、方法;非静态中可以包括静态、 非静态结构

③ 执行时间:静态 → 随类加载而执行;非静态 → 随对象创建而执行

? 执行次数:静态:只执行一次;非静态:创建多少个对象,执行多少次

④ 存在多个静态代码块时,按声明顺序执行;非静态同静态。静态与非静态同时存在时,先执 行静态


代码块的主体内容已完结,关于其他 ↓ ↓ ↓


(3)总结:属性赋值的位置和顺序

属性赋值的位置:

① 默认初始化

② 显式初始化

③ 构造器中初始化

④ 有了对象后,可以通过"对象.属性"或"对象.方法"的方式进行赋值

⑤ 在代码块中赋值

属性赋值的顺序

① 默认初始化

② 显式初始化 / ⑤ 在代码块中赋值

③ 构造器中初始化

④ 有了对象后,可以通过"对象.属性"或"对象.方法"的方式进行赋值

由上可知:

  • 显式初始化和代码块中赋值的先后顺序看定义的相对顺序
  • 代码块的执行要先于构造器

代码块思维导图

16. final 关键字

final 可以用来修饰的结构:变量、方法、类

(1)final 修饰类

final 修饰的类不能被其他类继承。如 String、System、StringBuffer

因为这些类已经提供了足够的功能,不需要再继承扩充功能了。

(2)final 修饰方法

final 修饰的方法不可以被重写。如 Object 类中的 getClass 方法

因为这个方法提供的功能够使了,不需要再重写了。

(3)final 修饰变量

final 修饰变量后,此“变量”就变为一个常量。另外,可以用 static final 来定义一个全局常量。

  • final 修饰属性:可以考虑赋值的位置,显式初始化、代码块中初始化、构造器中初始化

    附:如果每个对象的属性值都相同,考虑显式初始化;如果每个对象的属性值不同,考虑构造器初始化;代码块用于调用方法赋值的情况。

  • final 修饰局部变量:尤其是使用 final 修饰形参时,表明此形参是一个常量。当调用此方法时,给常量形参赋一个实参,一旦赋值后,就只能在方法体中使用此形参,而不能再重新赋值。

    public void show(final int number){   // 在调用方法时传入实参
        // number = 5;   // 编译出错
        System.out.println(number);
    }
    

附:

如果有多个构造器,只要一个构造器对 final“变量”进行了赋值,其他构造器也必须赋值。(因为 final “变量”一定要初始化,但个别构造器中未给 final “变量”赋值,这就导致调用这些构造器后,该“变量”可能会未被赋值)

final int NUM3;
// 如果有多个构造器,只要一个构造器对“常量”进行了赋值,其他构造器也必须赋值
public FinalTest(){
    NUM3 = 2;
}

public FinalTest(int num){   
    this.NUM3 = num;
}

总结:对于属性,如果要用“final”修饰,就一定要考虑初始化。

final 思维导图

练习

练习 1:

public class Something{
    public int addOne(final int x){
        // return ++x;   // 编译出错,因为改变了 x 的值
        return x + 1;   // 编译通过,因为没有改变 x 的值
    }
}

练习 2:

public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().addOne(o);
        System.out.println(o.i);   // 1
    }

    public void addOne(final Other o){
//        o = new Other();   // 编译出错
        o.i++;   // 编译通过。址值不变,但是地址值指向的堆空间中的属性可以变。
    }
}

class Other{
    public int i;
}

17. abstract 关键字

abstract 可以用来修饰的结构:方法、类

(1)abstract 修饰类

  • 表示“抽象类”,抽象类不能被实例化,即不能创建对象
  • 抽象类中一定有构造器,便于子类实例化时调用
  • 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关操作

(2)abstract 修饰方法

  • 表示“抽象方法”,抽象方法只有方法的声明,没有方法体

  • 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法

    因为抽象方法没有方法体,不能被调用,所以为了做到不被调用,就将类声明为抽象的,这样类就不能产生对象,也就不会调用抽象方法了。所以抽象方法所在的类一定是抽象类。

    另外,抽象类中不定义抽象方法没意义。

  • 若子类中重写了父类中的所有抽象方法后,则此子类可以实例化;若子类没有重写父类中的所有抽象方法,则此子类仍然是一个抽象类

(3)abstract 的“禁忌”

  • abstract 不能修饰:属性、构造器
  • abstract 不能修饰私有方法、静态方法、final 的方法、final 的类

附:

总结方法不能重写的情况

  • 子类可以继承父类的 private 方法,但“看不见”,也就没办法重写
  • static 方法不能重写
  • final 修饰的方法不能重写

总结不能修饰构造器的关键字

  • static
  • abstract

(4)abstract 应用 —— 模板方法设计模式

在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

模板方法模式的应用

  • 数据库访问的封装

  • Junit 单元测试

  • JavaWeb 的 Servlet 中关于 doGet/doPost 方法调用

  • Hibernate 中模板程序

  • Spring 中 JDBCTemlate、HibernateTemplate 等

abstract 思维导图

18. 接口

18.1 接口的定义及成员

(1)Java 中接口使用 interface 关键字定义。而且类与接口是两个并列的结构

(2)接口中的成员:

  • JDK 7 及以前:只能定义全局常量抽象方法

    • 全局常量:public static final 的,在书写时可以省略
    • 抽象方法:public abstract 的,同样可以是省略
  • JDK 8 及以后:除了定义全局常量抽象方法外,还可以定义静态方法默认方法

    ① 接口中定义的静态方法,只能通过接口来调用

    ② 通过实现类的对象,可以调用接口中的默认方法。如果实现类重写了接口中的默认方法,则在调用时,执行的是实现类重写的方法。(多态

(3)接口不同于抽象类,接口中不能定义构造器,意味着接口不能实例化

(4)Java 开发中,接口通过让类去实现(implements)的方式来使用。如果实现类覆盖了接口中 的所有抽象方法,则此实现类可以实例化;如果实现类没有覆盖接口中的所有抽象方法,则此实现 类仍为一个抽象类。

  • 类优先原则(只适用于方法):如果一个类同时继承一个类和实现一个接口(继承写在前面),且父类和接口中声明了同名同参数的方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的方法。

  • 接口冲突:如果实现类实现了多个接口,而这些接口中的多个都定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下会报错,解决:在实现类中重写该默认方法。

  • 如何在子类(或实现类)的方法中调用父类、接口中(被重写)的方法

    class SubClass extends SuperClass implements CompareA{
        public void method2(){
            System.out.println("在接口的实现类中重写默认方法");
        }
    
        // 在子类(或实现类)中调用父类或接口中的方法
        public void MyMethod(){
            super.method3();   // 调用父类中的方法
            CompareA.super.method3();   // 调用接口中的方法
        }
    }
    

(4)Java 类可以实现多个接口,这弥补了 Java 单继承的局限性

class A extends B implements C,D,E{ }

接口与接口之间可以继承,而且可以多继承。

总结:

JDK 8及以后,接口中可以定义 abstract 方法、static 方法和 default 方法。其中,只有 default 方法可以被重写

② 类与类之间是的继承是单继承:一个类只能有一个父类,一个类可以被多个类继承;

? 接口与接口之间可以是多继承:一个接口可以继承多个接口;

? 类与接口之间是实现:一个类可以实现多个接口,同时还可以继承其他类。

18.2 接口的应用 —— 代理模式

代理模式是 Java 开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

应用场景:

  • 安全代理:屏蔽对真实角色的直接访问。

  • 远程代理:通过代理类处理远程方法调用(RMI)

  • 延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象。比如你要开发一个大文档查看软件,大文档中有大的图片,有可能一个图片有100 MB,在打开文件时,不可能将所有的图片都显示出来,这样就可以使用代理模式,当需要查看图片时,用 proxy 来进行大图片的打开。

分类:

  • 静态代理(静态定义代理类)

  • 动态代理(动态生成代理类) JDK 自带的动态代理,需要反射等知识


接口的主体内容已完结,关于其他 ↓ ↓ ↓


18.3 接口与抽象类的对比

接口思维导图

练习:

练习 1:

package com.hstar.oop.interfacetest.java8;

interface A{
    int x = 0;
}

class B{
    int x = 1;
}

public class C extends B implements A{
    public void px(){
//   System.out.println(x); // Reference to 'x' is ambiguous, both 'B.x' and 'A.x' match
        System.out.println("父类中的:x = " + super.x);
        System.out.println("接口中的:x = " + A.x);
    }

    public static void main(String[] args) {
        new C().px();
    }
}

附:属性不适用于类优先原则。

练习 2:

编译不通过:ball 是静态常量。

19. 内部类

19.1 定义与分类

定义

Java 中允许将一个类 A 声明在另一个类 B 中,则类 A 就是内部类,类 B 称为外部类

分类

成员内部类:静态成员内部类、非静态成员内部类

局部内部类:方法内、代码块内、构造器内

成员内部类和局部内部类在编译后,都会生成字节码文件,格式:

成员内部类:外部类$内部类名.class

局部内部类:外部类$数字内部类名.class

19.2 成员内部类

(1)作为外部类的成员

  • 调用外部类的结构
  • 分为静态成员内部类和非静态成员内部类
  • 可以被 4 种权限修饰符修饰

(2)作为一个类

  • 类内可以定义属性、方法、构造器
  • 可以被 final 修饰,表示此类不能被继承
  • 可以被 abstract 修饰

19.3 内部类需要关注的四个问题

(1)分类、内部结构

(2)如何实例化成员内部类的对象?

package com.hstar.oop.innerclass.inner1;

public class Person {

    // 静态成员内部类
    static class Eye{
        
        public Eye(){

        }
    }

    // 非静态成员内部类
    class Nose{
       
        public Nose(){

        }
    }
}
package com.hstar.oop.innerclass.inner1;

public class InnerClassTest {
    public static void main(String[] args) {
        // 创建Eye的实例(静态成员内部类)
        Person.Eye eye = new Person.Eye();

        // 创建Nose的实例(非静态成员内部类)
        Person person = new Person();
        Person.Nose nose = person.new Nose();
    }
}

(3)如何在成员内部类中区分调用外部类的结构

package com.hstar.oop.innerclass.inner1;

public class Person {
    String name = "辉星";

    public void eat(){
        System.out.println("吃饭");
    }

    // 非静态成员内部类2
    class Nose{
        String name = "鼻子";

        public Nose(){

        }

        public void method(){
            System.out.println("鼻子闻到了远处飘来的花香");
            Person.this.eat();   // 调用外部类的非静态方法
        }

        // 如何在成员内部类中区分调用外部类的结构
        public void method1(String name){
            System.out.println(name);   // 打印形参变量
            System.out.println(this.name);   // 打印内部类的成员变量
            System.out.println(Person.this.name);   // 打印外部类的成员变量
        }
    }
}

(4)局部内部类在开发中的应用

开发中局部内部类的使用:返回接口实现类的对象

package com.hstar.oop.innerclass.inner1;

public class InnerClassTest1 {

    // 返回一个实现了Comparable接口的类的对象
    public Comparable getComparable(){
        // 创建一个实现了Comparable接口的类:局部内部类
        class MyComparable implements Comparable{
            @Override
            public int compareTo(Object o) {
                return 0;
            }
        }
        return new MyComparable();

        // 匿名实现类的匿名对象
//        return new Comparable() {   
//            @Override
//            public int compareTo(Object o) {
//                return 0;
//            }
//        };
    }
}

附:

1.

? 在局部内部类的方法中,如果想要调用局部内部类所在的方法中的局部变量,需要将该局部变量定义为 final 的。

  • JDK 7 及以前:要求此局部变量显式地声明为 final 的
  • JDK 8 及以后:可以省略 final
public class InnerClassTest2 {
    public void method(){
        final int num = 10;
        class AA{
            public void show(){
//                num = 20;
                System.out.println(num);
            }
        }
    }
}

2.

JDK中的内部类:

  • Integer --> IntegerCache

  • Thread --> State

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-20 14:57:12  更:2021-08-20 14:59:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 1:34:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码