<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 模板快捷键
main
:public static void main(String[] args) {}
sout
:System.out.println();
fori
:for (int i = 0; i < ; i++) {}
xxx.for
:for(int i = 0; i < xxx; i++) {}
更多的请在 File - Settings - Editor - Live template 中查看或添加
或者,通过下列快捷键查看
ctrl + J
:查看模板快捷键
7.2 包
包的作用:1. 区分相同名字的类 2. 当类很多时,便于管理 3. 控制访问范围
语法:
package com.name
其中com
name
分别是 一级 和 二级目录,用.
分隔包的本质:就是创建不同 文件夹/目录 来保存 类 文件
如何使用包中的对象:
-
先引入包,之后创建对象
import com.name.T; ... T tools = new T();
-
不引入包,而在创建对象时写全路径
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.*
使用细节:
-
package
的作用是声明当前类所在的包,要放在 类 的 最上面。一个 类 中最多有一句package
-
import
放在package
下面,类定义 前面。可以有多条语句,且没有顺序要求 -
编译器编译时 不会 检查目录结构。
即使一个包处于错误的目录下(只要其不依赖其他包)也可能通过编译。
但是,虚拟机会找不到该包,最终程序无法运行。
-
从 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 使用说明
- 修饰符可以修饰类中的 属性、成员方法 及 类
- 只有 默认 和
public
才能修饰 类,并遵循上述访问权限特点 - 成员方法 的访问规则和 属性 相同
- private 修饰的变量可以被 任意本对象同类的对象访问
7.4 封装
封装(encapsulation)就是把抽象出的 数据[属性] 和对数据的 操作[方法] 封装在一起。数据 被保护在内部,程序的其他部分只有通过被授权的 操作[方法],才能对数据进行操作。
封装的好处:
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
实现步骤:
- 将属性私有化
private
- 提供一个公共的
set
方法,用于对属性判断并赋值 - 提供一个公共的
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 使用细节
-
子类 继承了所有属性和方法,但私有(
private
)的 属性·方法 不能在 子类 直接访问。要调用父类提供的 公共(public
)等方法 访问。 -
子类 必须调用 父类 的 构造器,完成 父类 的 初始化。
-
当创建 子类对象 时,不管使用 子类的哪个构造器,默认情况下总会调用 父类的无参构造器。如果 父类 没有提供 无参构造器,则必须在 子类的构造器 中用
super
去指定使用 父类的哪个构造器 完成 对父类的初始化。否则编译不能通过。 -
如果希望指定调用 父类的某构造器,则显式地调用一下:
super(形参列表);
-
super
在使用时,必须放在构造器第一行。super
只能在构造器中使用。 -
由于
super
与this
都要求放在第一行,所以此两个方法不能同时存在于同一构造器。 -
Java 所有的类都是
Object
的子类。换言之,Object
是所有类的父类。 -
父类构造器的调用不限于直接父类,将持续向上直至追溯到顶级父类
Object
-
子类 最多只能直接继承 一个 父类。即,Java 中是 单继承机制。
-
不能滥用继承。子类 和 父类 之间必须满足 is - a 的逻辑关系。
7.5.2 继承的本质
-
内存布局:
- 在 方法区,自顶级父类起,依次加载 类信息。
- 在 堆 中开辟一个空间,自顶级父类起,依次创建并初始化各个类包含的所有属性信息。
- 在 栈 中存放该空间的 地址。
-
如何查找信息?
- 查看该子类是否有该属性。如果该子类有这个属性且可以访问,则返回信息。
- 子类没有该属性的场合,查看父类是否有该属性。如有且可访问,则返回信息。如不可访问,则报错。
- 父类也没有该属性的场合,继续查找上级父类,直到顶级父类(Object)。
- 如需调用某个特定类包含的特定信息,可以调用该类提供的方法。
7.5.3 super
关键字
super
代表父类的引用。用于访问父类的 属性、方法、构造器。
super 的使用:
super.属性名
:访问父类的属性。不能访问父类的私有(private)属性。super.方法名(形参列表)
:访问父类的方法。不能访问父类的私有(private)方法。super(参数列表);
:访问父类的构造器。此时,super 语句必须放在第一句。
使用细节:
- 调用父类构造器,好处是分工明确。父类属性由父类初始化,子类由子类初始化。
- 子类中由和父类中成员(属性和方法)重名时,要调用父类成员必须用
super
。没有重名的场合,super
、this
及直接调用的效果相同。 super
的访问不限于直接父类。如果爷爷类和本类中都有同名成员也能使用。如果多个基类中都有同名成员,则遵循就近原则。
7.5.4 方法重写 / 覆盖
方法重写/覆盖(Override):如若子类有一个方法,和父类的某方法的 名称、返回类型、参数 一样,那么我们就说该子类方法 覆盖 了那个父类方法。
使用细节:
- 子类方法的参数,方法名称,要和父类方法完全一致。
- 子类方法的返回类型需和父类方法 一致,或者是父类返回类型的子类。
- 子类方法 不能缩小 父类方法的访问范围(访问修饰符)。
7.6 多态
多态:方法 或 对象 有多种形态。多态 是面向对象的第三大特征,是建立在 封装 和 继承 的基础之上的
7.6.1 多态的体现
-
方法的多态:重写 和 重载 体现了 方法的多态。
-
对象的多态:
-
一个对象的 编译类型 和 运行类型 可以不一致。
Animal animal = new Dog();
上例,编译类型是
Animal
,运行类型是子类Dog
。要理解这句话,请回想 [6.1.4 类与对象的内存访问机制]:animal
是对象的引用。 -
编译类型在定义对象时就确定了,不能改变。
-
运行类型是可以变化的。
上例中,再让
animal = new Cat();
,这样,运行类型变为了Cat
-
编译类型看定义时
=
的左边,运行类型看=
的右边。
-
7.6.2 使用细节
-
多态的前提:两个对象 / 类存在继承关系。
-
多态的向上转型:
- 本质:父类的引用指向了子类的对象。(如 [ 7.6.1.2 ])
- 语法:
父类类型 引用名 = new 子类类型(参数列表);
- 编译类型看左边,运行类型看右边。
- 可以调用父类中的所有成员,但不能调用子类特有的成员,而且需要遵守访问权限。因为在编译阶段,能调用哪些成员是由编译类型决定的。
- 最终的运行结果要看子类的具体实现。即从子类起向上查找方法调用(与 [ 7.5.2 ] 规则相同)。
-
多态的向下转型:
-
语法:
子类类型 引用名 = (子类类型)父类引用;
[7.6.2.2] 的例子里,向下转型。这个语法其实和 [2.8.2 强制类型转换] 很像。
Dog dog = (Dog)animal;
-
只能强转父类的引用,不能强转父类的对象。
-
要求父类的引用必须指向的是当前目标类型的对象。即上例中的
animal
运行类型需是Dog
-
向下转型后,可以调用子类类型中的所有成员。
-
-
属性没有重写一说。和 方法 不同,属性的值 看编译类型。
-
instanceof
比较操作符。用于判断对象类型是否是某类型或其子类型。此时判断的是 运行类型。
7.6.3 理解方法调用
在对象上调用方法的过程如下:
-
编译器查看对象的声明类型和方法名。该类和其父类中,所有同名方法(包括参数不同的方法)都被列举。
至此,编译器已经知道所有可能被调用的方法。
-
编译器确认方法调用中提供的参数类型。
那些列举方法中存在参数类型完全匹配的方法时,即调用该方法。
没有发现匹配方法,抑或是发现经过类型转换产生了多个匹配方法时,就会报错
至此,编译器已经知道要调用方法的名字和参数类型
-
如若是 private 方法、static 方法、final 方法、构造器,那么编译器将能准确知道要调用哪个方法。这称为 静态绑定
与之相对的,如果调用方法依赖于隐式参数类型,那么必须在运行时 动态绑定
-
程序运行并采取动态绑定方法时,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 是不同对象,两者的散列码基本上不会相同。
字符串的散列码是由其内容导出的,而其他引用对象的散列码是根据存储地址得出的。
散列码的作用:
- 提高哈希结构的容器的效率。
- 两个引用,若是指向同一对象,则哈希值一般不同。
- 哈希值是根据地址生成的,因而,哈希值不能等同于地址
相关方法:
-
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 方法
- 当对象被回收时,系统会自动调用该对象的
finalize
方法。子类可以重写该方法,做一些释放资源的操作。 - 何时被回收:当某对象没有任何引用时,JVM 就认为该对象是一个垃圾对象,就会(在算法决定的某个时刻)使用垃圾回收机制来销毁该对象。在销毁该对象前,会调用
finalize
方法。 - 垃圾回收机制的调用,是由系统决定。也可以通过
System.gc();
主动触发垃圾回收机制。这个方法一经调用就会继续执行余下代码,而不会等待回收完毕。 - 实际开发中,几乎不会运用该方法。
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("--------------------------------------"); } }