<Java>7 面向对象编程(中级)

本文最后更新于:2022年7月6日 早上

7 面向对象编程(中级)

IDE:集成开发环境

  • IDEA

  • Eeclipse:一个开源的、基于 Java 的可扩展开发平台。是由 IBM 公司开发,在 2001 年 11 月贡献给开源社区的,目前最优秀的 Java 开发 IDE 之一。

7.1 IDEA 的使用

IDEA:全程 IntelliJ IDEA。在业界被公认为最好的 Java 开发工具。是捷克 JetBrains 公司的产品。除了 Java 开发,还支持 HTML,CSS,PHP,MySQL,Python 等。下载地址

7.1.1 常用快捷键

  • 删除当前行:ctrl + Y
  • 复制当前行:ctrl + D
  • 补全代码:alt + /
  • 添加 / 取消注释:ctrl + /
  • 导入该行需要的类:alt + enter
  • 快速格式化代码:ctrl + alt + L
  • 快速运行程序:shift + F10(我改成了alt + R
  • 生成构造器:alt + insert
  • 查看一个类的层级关系:ctrl + H
  • 定位一个方法:把光标放在一个方法上,按 ctrl + B
  • 自动分配变量名:在后面加上 .var
  • 查看模板快捷键:ctrl + J
  • 快速环绕代码:ctrl + alt + T

7.1.2 模板快捷键

  • mainpublic static void main(String[] args) {}
  • soutSystem.out.println();
  • forifor (int i = 0; i < ; i++) {}
  • xxx.forfor(int i = 0; i < xxx; i++) {}

更多的请在 File - Settings - Editor - Live template 中查看或添加

或者,通过下列快捷键查看

  • ctrl + J:查看模板快捷键

7.2 包

包的作用:1. 区分相同名字的类 2. 当类很多时,便于管理 3. 控制访问范围

语法:package com.name 其中 com name 分别是 一级 和 二级目录,用 . 分隔

包的本质:就是创建不同 文件夹/目录 来保存 类 文件

如何使用包中的对象:

  1. 先引入包,之后创建对象

    import com.name.T;
    ...
    T tools = new T();
  2. 不引入包,而在创建对象时写全路径

    com.name.T tools = new com.name.T();

命名规则:

  • 只能包含 数字 1 2 3、字母 a b A b、下划线 _、小圆点 .
  • 不能用 数字 开头。每级目录都不能。

命名规范:

  • 全小写字母 + 小圆点
  • com.公司名.项目名.业务模块名

常用的包:

java.lang:基本包,默认引入,不需要再引入

java.util:系统提供的工具包。工具类。

java.net:网络包,网络开发。

java.awt:Java 的界面开发,GUI。

引入包:

  • 只引入该包下的一个类:import java.util.Scanner
  • 引入该包的所有内容(不建议):import java.util.*

使用细节:

  1. package 的作用是声明当前类所在的包,要放在 类 的 最上面。一个 类 中最多有一句 package

  2. import 放在 package 下面,类定义 前面。可以有多条语句,且没有顺序要求

  3. 编译器编译时 不会 检查目录结构。

    即使一个包处于错误的目录下(只要其不依赖其他包)也可能通过编译。

    但是,虚拟机会找不到该包,最终程序无法运行。

  4. 从 1.2 版本开始,用户不能再把包放在 java. 开头的目录下了。若如此做,这些包会被禁止加载。

7.4.1 静态导入

有一种 import 语句允许导入静态方法和字段,而不只是类

比如:

import static java.lang.Math.*;

这个场合,使用 Math 包内的静态方法、字段时,不需要再添加类名前缀。

double n = pow(10, 5);					// <———— 本来是 double n = Math.pow(10, 5);
double pi = PI;							// <———— 本来是 double pi = Math.PI;

—— 上述方法、字段见 [12.5 Math 类]

7.3 访问修饰符

7.3.1 访问权限特点

Java 提供 4 种 访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围)

  • 公开级别:public,对外公开。

  • 受保护级别:protected,对 子类 和 同一个包中的类 公开。

    ——什么是 子类?详见 [ 7.5 继承 ]

  • 默认级别:没有修饰符号,向 同一个包的类 公开。

  • 私有级别:private,只有 同类 可以访问,不对外公开。

(⌐■_■) 默认(无修饰符) private protected public
本类
同包中的子类 不可以
同包的非子类 不可以
其他包的子类 不可以 不可以
其他包的非子类 不可以 不可以 不可以

7.3.2 使用说明

  1. 修饰符可以修饰类中的 属性、成员方法 及 类
  2. 只有 默认 和 public 才能修饰 类,并遵循上述访问权限特点
  3. 成员方法 的访问规则和 属性 相同
  4. private 修饰的变量可以被 任意本对象同类的对象访问

7.4 封装

封装(encapsulation)就是把抽象出的 数据[属性] 和对数据的 操作[方法] 封装在一起。数据 被保护在内部,程序的其他部分只有通过被授权的 操作[方法],才能对数据进行操作。

封装的好处:

  • 隐藏实现细节
  • 可以对数据进行验证,保证安全合理

实现步骤:

  1. 将属性私有化 private
  2. 提供一个公共的 set 方法,用于对属性判断并赋值
  3. 提供一个公共的 get 方法,用于获取属性的值

编译多个源文件:

javac MyClass.java

该文件中使用了其他类时,Java 编译器会查找对应名称的 .class 文件。没有找到的场合,转而寻找 .java 文件,并对其编译。倘若 .java 文件相较原有 .class 文件更新,编译器也会自动重新编译该文件。

7.4.1 静态导入

有一种 import 语句允许导入静态方法和字段,而不只是类

比如:

import static java.lang.Math.*;

这个场合,使用 Math 包内的静态方法、字段时,不需要再添加类名前缀。

double n = pow(10, 5);					// <———— 本来是 double n = Math.pow(10, 5);
double pi = PI;							// <———— 本来是 double pi = Math.PI;

—— 上述方法、字段见 [12.5 Math 类]

7.4.2 JAR 文件

为了避免向用户提供包含大量类文件的复杂目录结构,可以将 Java 程序打包成 JAR (Java 归档)文件。

一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。

JAR 文件是压缩的。其使用了 ZIP压缩格式。

创建 JAR:

使用 jar 工具以制作 JAR 文件。该工具在 jdk/bin 目录下

jar cvf 包名 文件名1 文件名2 ...

关于 jar 工具的各种指令,还是自己去百度一下吧

7.5 继承

继承:能解决代码复用,让我们的编程更接近人类思维。当多个类存在相同的 属性(变量)和 方法 时,可以从这些类中抽象出 父类(基类/超类)。在 父类 中定义这些属性·方法,所有的子类不需要重新定义这些属性和方法,只需要通过 extends 来声明继承父类即可。

通过继承的方法,代码的复用性提高了,代码的维护性和拓展性也提高了。

public class Son extends Father {};				// Son 类继承了 Father 类

定义类时可以指明其父类,也能不指明。不指明的场合,默认继承 Object 类。

所有类有且只有一个父类。Object 是所有类的直接或间接父类。只有 Object 本身没有父类。

7.5.1 使用细节

  1. 子类 继承了所有属性和方法,但私有(private)的 属性·方法 不能在 子类 直接访问。要调用父类提供的 公共(public)等方法 访问。

  2. 子类 必须调用 父类 的 构造器,完成 父类 的 初始化。

  3. 当创建 子类对象 时,不管使用 子类的哪个构造器,默认情况下总会调用 父类的无参构造器。如果 父类 没有提供 无参构造器,则必须在 子类的构造器 中用 super 去指定使用 父类的哪个构造器 完成 对父类的初始化。否则编译不能通过。

  4. 如果希望指定调用 父类的某构造器,则显式地调用一下:super(形参列表);

  5. super 在使用时,必须放在构造器第一行。super 只能在构造器中使用。

  6. 由于 superthis 都要求放在第一行,所以此两个方法不能同时存在于同一构造器。

  7. Java 所有的类都是 Object 的子类。换言之,Object 是所有类的父类。

  8. 父类构造器的调用不限于直接父类,将持续向上直至追溯到顶级父类 Object

  9. 子类 最多只能直接继承 一个 父类。即,Java 中是 单继承机制。

  10. 不能滥用继承。子类 和 父类 之间必须满足 is - a 的逻辑关系。

7.5.2 继承的本质

  • 内存布局:

    1. 在 方法区,自顶级父类起,依次加载 类信息。
    2. 在 堆 中开辟一个空间,自顶级父类起,依次创建并初始化各个类包含的所有属性信息。
    3. 在 栈 中存放该空间的 地址。
  • 如何查找信息?

    1. 查看该子类是否有该属性。如果该子类有这个属性且可以访问,则返回信息。
    2. 子类没有该属性的场合,查看父类是否有该属性。如有且可访问,则返回信息。如不可访问,则报错。
    3. 父类也没有该属性的场合,继续查找上级父类,直到顶级父类(Object)。
    4. 如需调用某个特定类包含的特定信息,可以调用该类提供的方法。

7.5.3 super 关键字

super 代表父类的引用。用于访问父类的 属性、方法、构造器。

super 的使用:

  • super.属性名:访问父类的属性。不能访问父类的私有(private)属性。
  • super.方法名(形参列表):访问父类的方法。不能访问父类的私有(private)方法。
  • super(参数列表);:访问父类的构造器。此时,super 语句必须放在第一句。

使用细节:

  1. 调用父类构造器,好处是分工明确。父类属性由父类初始化,子类由子类初始化。
  2. 子类中由和父类中成员(属性和方法)重名时,要调用父类成员必须用 super。没有重名的场合,superthis 及直接调用的效果相同。
  3. super 的访问不限于直接父类。如果爷爷类和本类中都有同名成员也能使用。如果多个基类中都有同名成员,则遵循就近原则。

7.5.4 方法重写 / 覆盖

方法重写/覆盖(Override):如若子类有一个方法,和父类的某方法的 名称、返回类型、参数 一样,那么我们就说该子类方法 覆盖 了那个父类方法。

使用细节:

  1. 子类方法的参数,方法名称,要和父类方法完全一致。
  2. 子类方法的返回类型需和父类方法 一致,或者是父类返回类型的子类。
  3. 子类方法 不能缩小 父类方法的访问范围(访问修饰符)。

7.6 多态

多态:方法 或 对象 有多种形态。多态 是面向对象的第三大特征,是建立在 封装 和 继承 的基础之上的

7.6.1 多态的体现

  1. 方法的多态:重写 和 重载 体现了 方法的多态。

  2. 对象的多态:

    • 一个对象的 编译类型 和 运行类型 可以不一致。

      Animal animal = new Dog();

      上例,编译类型是 Animal,运行类型是子类 Dog。要理解这句话,请回想 [6.1.4 类与对象的内存访问机制]animal 是对象的引用

    • 编译类型在定义对象时就确定了,不能改变。

    • 运行类型是可以变化的。

      上例中,再让 animal = new Cat();,这样,运行类型变为了 Cat

    • 编译类型看定义时 = 的左边,运行类型看 = 的右边。

7.6.2 使用细节

  1. 多态的前提:两个对象 / 类存在继承关系。

  2. 多态的向上转型:

    • 本质:父类的引用指向了子类的对象。(如 [ 7.6.1.2 ])
    • 语法:父类类型 引用名 = new 子类类型(参数列表);
    • 编译类型看左边,运行类型看右边。
    • 可以调用父类中的所有成员,但不能调用子类特有的成员,而且需要遵守访问权限。因为在编译阶段,能调用哪些成员是由编译类型决定的。
    • 最终的运行结果要看子类的具体实现。即从子类起向上查找方法调用(与 [ 7.5.2 ] 规则相同)。
  3. 多态的向下转型:

    • 语法:子类类型 引用名 = (子类类型)父类引用;

      [7.6.2.2] 的例子里,向下转型。这个语法其实和 [2.8.2 强制类型转换] 很像。

      Dog dog = (Dog)animal;

    • 只能强转父类的引用,不能强转父类的对象。

    • 要求父类的引用必须指向的是当前目标类型的对象。即上例中的 animal 运行类型需是 Dog

    • 向下转型后,可以调用子类类型中的所有成员。

  4. 属性没有重写一说。和 方法 不同,属性的值 看编译类型。

  5. instanceof 比较操作符。用于判断对象类型是否是某类型或其子类型。此时判断的是 运行类型

7.6.3 理解方法调用

在对象上调用方法的过程如下:

  1. 编译器查看对象的声明类型和方法名。该类和其父类中,所有同名方法(包括参数不同的方法)都被列举。

    至此,编译器已经知道所有可能被调用的方法。

  2. 编译器确认方法调用中提供的参数类型。

    那些列举方法中存在参数类型完全匹配的方法时,即调用该方法。

    没有发现匹配方法,抑或是发现经过类型转换产生了多个匹配方法时,就会报错

    至此,编译器已经知道要调用方法的名字和参数类型

  3. 如若是 private 方法、static 方法、final 方法、构造器,那么编译器将能准确知道要调用哪个方法。这称为 静态绑定

    与之相对的,如果调用方法依赖于隐式参数类型,那么必须在运行时 动态绑定

  4. 程序运行并采取动态绑定方法时,JVM 将调用那个 实际类型 对应的方法。

倘若每次调用方法都进行以上搜索,会造成庞大的时间开销。为此,JVM 预先为每个类计算了 方法表

方法表中列举了所有方法的签名与实际调用的方法。如此,每次调用方法时,只需查找该表即可。

特别地,使用 super 关键字时,JVM 会查找其父类的方法表。

动态绑定机制:

  • 当调用对象方法的时候,该方法和该对象(隐式参数)的内存地址/运行类型绑定。
  • 当调用对象属性时,没有动态绑定机制。于是哪里声明,哪里调用。

7.7 Object 类

Object 类是所有类的超类。Java 中所有类默认继承该类。

equals 方法

boolean equals(Object obj)

用于检测一个对象是否等于另一对象。

在 Object 中,该方法的实现是比较 形参 与 隐式参数 的对象引用是否一致。

== 的区别:

  • ==:既可以判断基本类型,也可以判断引用类型。如果判断基本类型,判断的是值是否相等。如果判断引用类型,判断的是地址是否相等。

  • equals 方法:是 Object 中的方法,只能判断引用类型。默认判断地址是否相等,但子类中往往重写该代码,以判断内容是否相等。

    在子类中定义 equals 方法时,首先调用超类的 equals 方法。那个一致时,再比较子类中的字段。

Java 语言规范要求 equals 方法具有如下特性:

  • 自反性:对于任何非空引用 x,x.equals(x) 应返回 true

  • 对称性:对于任何引用 x 和 y,当且仅当 x.equals(y) 为 true 时,y.equals(x) 为 true

    如果所有的子类具有相同的相等性语义,可以使用 instanceof 检测其类型。否则,最好使用 getClass 方法比较类型。

  • 传递性:对于任何引用 x、y、z,如果 x.equals(y) 为 true ,y.equals(z) 为 true,那么 x.equals(z) 也应该为 true

  • 一致性:如果 x 和 y 的引用没有发生变化,反复调用 x.equals(y) 应该返回相同的结果

  • 对于任何非空引用 x,x.equals(null) 应该返回 false

hashCode 方法

int hashCode()

返回对象的 散列码值。

散列码值是由对象导出的一个整型值。散列码是无规律的。如果 x 与 y 是不同对象,两者的散列码基本上不会相同。

字符串的散列码是由其内容导出的,而其他引用对象的散列码是根据存储地址得出的。

散列码的作用:

  1. 提高哈希结构的容器的效率。
  2. 两个引用,若是指向同一对象,则哈希值一般不同。
  3. 哈希值是根据地址生成的,因而,哈希值不能等同于地址

相关方法:

  • Objects.hashCode(Object obj)

    这是一个 null 安全的返回散列值的方法。传入 null 时会返回 0

  • Objects.hash(Object... values)

    组合所有传入参数的散列值

  • Integer.hashCode(int value)

    返回给定基本数据类型的散列值。所有包装类都有该静态方法

  • Arrays.hashCode(xxx[] a)

    计算数组的散列码。数组类型可以是 Object 或基本数据类型

空对象调用 hashCode 方法会抛出异常。

hashCode 与 equals 的定义必须相符。如果 x.equals(y) 返回 true,那么 x.hashCode()y.hashCode() 应该返回相同的值。

toString 方法

String toString()

返回表示对象的一个字符串。Object 的默认实现如下

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

    返回包含对象信息的 Class 对象。

  • String getName()

    由 Class 类实例调用。返回这个类的全类名

    全类名:即包名 + 类名。比如 com.prictice.codes.Person

  • Class getSuperClass()

    由 Class 类实例调用。以 Class 形式返回其父类

    Object 使用时返回 null

  • Integer.toHexString(int val)

    返回一个数字的十六进制表示的字符串

toString 方法非常实用。Java 标准类库中的很多类重写了该方法,以便用户能获得一些有关对象状态的信息。

打印对象 或 使用 + 操作符拼接对象 时,都会自动调用该对象的 toString 方法。

当直接调用对象时,也会默认调用该方法。

finalize 方法

  1. 当对象被回收时,系统会自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作。
  2. 何时被回收:当某对象没有任何引用时,JVM 就认为该对象是一个垃圾对象,就会(在算法决定的某个时刻)使用垃圾回收机制来销毁该对象。在销毁该对象前,会调用 finalize 方法。
  3. 垃圾回收机制的调用,是由系统决定。也可以通过 System.gc(); 主动触发垃圾回收机制。这个方法一经调用就会继续执行余下代码,而不会等待回收完毕。
  4. 实际开发中,几乎不会运用该方法。

7.8 断点调试(Debug)

断点调试:在程序某一行设置一个断点,调试时,代码运行至此就会停住,然后可以一步一步往下调试。调试过程中可以看各个变量当前的值。如若出错,则测试到该出错代码行即显示错误并停下。进行分析从而找到这个 Bug。

调试过程中是运行状态,所以,是以对象的 运行类型 执行。

断点调试是程序员必须掌握的技能,能帮助我们查看 Java 底层源代码的执行过程,提高程序员 Java 水平。

快捷键如下

  • 跳入:F7
  • 跳过:F8
  • 跳出:shift + F8
  • resume,执行到下一个断点:F9

附录

零钱通程序

  • Wallet.java

    package com.the_wallet;
    
    public class Wallet {
        public static void main(String[] args) {
            Data p1 = new Data("Melody");
            p1.menu();
            System.out.println("再见~");
        }
    }
  • Data.java

    package com.the_wallet;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Scanner;
    
    public class Data {
        private String name = "user";
        private double balance = 0;
        private String[][] detail = new String[1][5];
    
        private Data() {
            detail[0][0] = "项目\t";
            detail[0][1] = "\t\t";
            detail[0][2] = "时间";
            detail[0][3] = " ";
            detail[0][4] = " ";
        }
    
        public Data(String name) {
            this();
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void menu() {
            char inp = 'a';
            double inpD;
            Scanner scanner = new Scanner(System.in);
            while (inp != 'y' && inp != 'Y') {
                System.out.print("\n===============零钱通菜单==============="
                        + "\n\t\t\t1.零钱通明细"
                        + "\n\t\t\t2.收益入帐"
                        + "\n\t\t\t3.消费入账"
                        + "\n\t\t\t4.退   出"
                        + "\n请选择(1-4):");
                inp = scanner.next().charAt(0);
                System.out.println("======================================");
                switch (inp) {
                    case '4':
                        System.out.println("确定要退出吗?(y/n):");
                        inp = scanner.next().charAt(0);
                        while (inp != 'y' && inp != 'n' && inp != 'Y' && inp != 'N') {
                            System.out.println("请输入“y”或者“n”!听话!");
                            inp = scanner.next().charAt(0);
                        }
                        break;
                    case '1':
                        showDetail();
                        break;
                    case '2':
                        System.out.println("请输入收益数额:");
                        inpD = scanner.nextDouble();
                        if (inpD <= 0) {
                            System.out.print("收益需要为正,记录消费请选择“消费入账”");
                            break;
                        }
                        earning(inpD);
                        break;
                    case '3':
                        System.out.println("请输入支出数额:");
                        inpD = scanner.nextDouble();
                        if (inpD < 0) {
                            inpD = -inpD;
                        }
                        if (balance < inpD) {
                            System.out.println("您的余额不足!");
                            break;
                        }
                        System.out.println("请输入支出项目:");
                        spending(inpD, scanner.next());
                        break;
                    case 'g':
                        break;
                    default:
                        System.out.print("错误。请输入数字(1-4)");
                }
            }
        }
    
        private void earning(double earn) {
            String[][] temp = new String[this.detail.length + 1][5];
            record(detail, temp);
            this.balance += earn;
            tidy("收益入账", earn, true, temp);
            showDetail();
            System.out.println("\n收益记录完成");
        }
    
    
        private void spending(double spend, String title) {
            String[][] temp = new String[this.detail.length + 1][5];
            record(detail, temp);
            this.balance -= spend;
            tidy(title, spend, false, temp);
            showDetail();
            System.out.println("\n消费记录完成");
    
        }
    
        private void record(String[][] detail, String[][] temp) {
            for (int i = 0; i < detail.length; i++) {
                for (int j = 0; j < 5; j++) {
                    temp[i][j] = detail[i][j];
                }
            }
        }
    
        private void tidy(String title, double num, boolean isPos, String[][] temp) {
            Date date = new Date();
            SimpleDateFormat sDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            if (title.length() <= 2) {
                temp[temp.length - 1][0] = title + "\t\t";
            } else {
                temp[temp.length - 1][0] = title + "\t";
            }
            String sign = isPos ? "+" : "-";
            temp[temp.length - 1][1] = sign + num + "";
            temp[temp.length - 1][2] = sDate.format(date);
            temp[temp.length - 1][3] = "余额:";
            temp[temp.length - 1][4] = balance + "";
            detail = temp;
        }
    
        private void showDetail() {
            System.out.println("--------------------------------------");
            for (int i = 0; i < detail.length; i++) {
                System.out.println(detail[i][0] + detail[i][1] + "\t" + detail[i][2] + "\t\t" + detail[i][3] + detail[i][4]);
            }
            System.out.println("--------------------------------------");
        }
    }

<Java>7 面向对象编程(中级)
https://i-melody.github.io/2021/12/05/Java/入门阶段/7 面向对象编程(中级)/
作者
Melody
发布于
2021年12月5日
许可协议