外观
07 面向对象编程(中级)
5766 字约 19 分钟
2024-09-01
7.2 包
包的作用:1. 区分相同名字的类 2. 当类很多时,便于管理 3. 控制访问范围
语法:
package com.name
其中com
name
分别是 一级 和 二级目录,用.
分隔包的本质:就是创建不同 文件夹/目录 来保存 类 文件
命名规则:
- 只能包含 数字
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.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;JAVA
—— 上述方法、字段见 [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
与this
都要求放在第一行,所以此两个方法不能同时存在于同一构造器。 - Java 所有的类都是
Object
的子类。换言之,Object
是所有类的父类。 - 父类构造器的调用不限于直接父类,将持续向上直至追溯到顶级父类
Object
- 子类 最多只能直接继承 一个 父类。即,Java 中是 单继承机制。
- 不能滥用继承。子类 和 父类 之间必须满足 is - a 的逻辑关系。
is-A继承关系:“表示类与类之间的继承关系、接口与接口之间的继承的关系以及类对接口实现的关系”。 has-A合成关系:“是关联关系的一种,是整体和部分(通常为一个私有的变量)之间的关系,并且代表的整体对象负责构建和销毁代表部分对象,代表部分的对象不能共享”
is_a是继承关系. has_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):如若子类有一个方法,和父类的某方法的 名称、返回类型、参数 一样,那么我们就说该子类方法 覆盖 了那个父类方法。
使用细节:
- 子类方法的参数,方法名称,要和父类方法完全一致。
- 子类方法的返回类型需和父类方法 一致,或者是父类返回类型的子类。,向下兼容。
- 子类方法 不能缩小 父类方法的访问范围(访问修饰符)
阻止继承:final类、方法和域
经过final修饰的类不可拓展,除了域,它的方法也为final的。
7.6 多态
多态:方法 或 对象 有多种形态。多态 是面向对象的第三大特征,是建立在 封装 和 继承 的基础之上的
7.6.1 多态的体现
方法的多态:重写 和 重载 体现了 方法的多态。
对象的多态:
一个对象的 编译类型 和 运行类型 可以不一致。
Animal animal = new Dog();
上例,编译类型是
Animal
,运行类型是子类Dog
。要理解这句话,请回想 [[6 面向对象编程(基础)]]的继承部分:animal
是对象的引用。编译类型在定义对象时就确定了,不能改变。
运行类型是可以变化的。
上例中,再让
animal = new Cat();
,这样,运行类型变为了Cat
编译类型看定义时
=
的左边,运行类型看=
的右边。
7.6.2 使用细节
多态的前提:两个对象 / 类存在继承关系。
多态的向上转型:
- 本质:父类的引用指向了子类的对象。(如 [ 7.6.1.2 ])
- 语法:
父类类型 引用名 = new 子类类型(参数列表);
- 编译类型看左边,运行类型看右边。
- 可以调用父类中的所有成员,但不能调用子类特有的成员,而且需要遵守访问权限。因为在编译阶段,能调用哪些成员是由编译类型决定的。
- 最终的运行结果要看子类的具体实现。即从子类起向上查找方法调用(与 [ 7.5.2 ] 规则相同)。
向上转型的好处:
提高代码的灵活性:通过向上转型,可以将不同的子类对象转换为其父类对象,从而可以使用统一的接口来操作这些对象,减少了代码的复杂性和冗余性。
提高代码的扩展性:当需要增加新的子类对象时,只需要在父类对象的基础上进行扩展即可,而不需要对原有的代码进行重构,从而降低了代码的维护成本。
提高代码的可维护性:在向上转型的情况下**,只需要关注父类对象的行为,而不需要关注子类对象的具体实现细节**,从而提高了代码的可维护性。
例如:
class A{ public void play(){...} } class B extend A{ @Override public void play(){...} } class C extend A{ @Override public void play(){...} } //如果我们想要调用某个类的paly方法,无需像这样实现: public void show(B b){ b.paly(); } public void show(C c){ c.paly(); } //而是这样: public void show(A a){ a.paly(); }
如果是访问成员变量,编译的话就是看父类,运行同样是看父类。 如果访问的方法,编译就看父类,运行则看子类。 如果是静态方法,编译和运行都是看父类。
总结:只有方法才有多态的特性
- 多态的向下转型:
语法:
子类类型 引用名 = (子类类型)父类引用;
[7.6.2.2] 的例子里,向下转型。这个语法其实和 [2.8.2 强制类型转换] 很像。
Dog dog = (Dog)animal;
只能强转父类的引用,不能强转父类的对象。只有引用子类对象的父类引用才能被向下转型为子类对象。
也就是说,向下转型之前,必须先向上转型。
要求父类的引用必须指向的是当前目标类型的对象。即上例中的
animal
运行类型需是Dog
向下转型后,可以调用子类类型中的所有成员。
属性没有重写一说。和 方法 不同,属性的值 看编译类型。
instanceof
比较操作符。用于判断对象类型是否是某类型或其子类型。此时判断的是 运行类型。//向下转型的时候比较常见,可以防止意外的类型转换异常。 A a = new B(); if(B instanceof A){ B b = (B)a; }
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("--------------------------------------");
}
}