Java 面向对象编程(中级)
Java 面向对象编程(中级)
sinarcsinxIDE:集成开发环境
IDEA
Eeclipse:一个开源的、基于 Java 的可扩展开发平台。是由 IBM 公司开发,在 2001 年 11 月贡献给开源社区的,目前最优秀的 Java 开发 IDE 之一。
IDEA 的使用
IDEA:全程 IntelliJ IDEA。在业界被公认为最好的 Java 开发工具。是捷克 JetBrains 公司的产品。除了 Java 开发,还支持 HTML,CSS,PHP,MySQL,Python 等。
常用快捷键
- 删除当前行:
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
模板快捷键
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
:查看模板快捷键
包
包的作用:1. 区分相同名字的类 2. 当类很多时,便于管理 3. 控制访问范围
语法:
package com.name
其中com
name
分别是 一级 和 二级目录,用.
分隔包的本质:就是创建不同 文件夹/目录 来保存 类 文件
如何使用包中的对象:
先引入包,之后创建对象
1
2
3import com.name.T;
...
T tools = new T();不引入包,而在创建对象时写全路径
1
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. 开头的目录下了。若如此做,这些包会被禁止加载。
访问修饰符
访问权限特点
Java 提供 4 种 访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围)
公开级别:
public
,对外公开。受保护级别:
protected
,对 子类 和 同一个包中的类 公开。默认级别:没有修饰符号,向 同一个包的类 公开。
私有级别:
private
,只有 同类 可以访问,不对外公开。
(⌐■_■)
默认(无修饰符) | private | protected | public | |
---|---|---|---|---|
本类 | 可 | 可 | 可 | 可 |
同包中的子类 | 可 | 不可以 | 可 | 可 |
同包的非子类 | 可 | 不可以 | 可 | 可 |
其他包的子类 | 不可以 | 不可以 | 可 | 可 |
其他包的非子类 | 不可以 | 不可以 | 不可以 | 可 |
使用说明
- 修饰符可以修饰类中的 属性、成员方法 及 类
- 只有 默认 和
public
才能修饰 类,并遵循上述访问权限特点 - 成员方法 的访问规则和 属性 相同
- private 修饰的变量可以被 任意本对象同类的对象访问
封装
封装(encapsulation)就是把抽象出的 数据[属性] 和对数据的 操作[方法] 封装在一起。数据 被保护在内部,程序的其他部分只有通过被授权的 操作[方法],才能对数据进行操作。
封装的好处:
- 隐藏实现细节
- 可以对数据进行验证,保证安全合理
实现步骤:
- 将属性私有化
private
- 提供一个公共的
set
方法,用于对属性判断并赋值 - 提供一个公共的
get
方法,用于获取属性的值
编译多个源文件:
1 | javac MyClass.java |
该文件中使用了其他类时,Java 编译器会查找对应名称的 .class 文件。没有找到的场合,转而寻找 .java 文件,并对其编译。倘若 .java 文件相较原有 .class 文件更新,编译器也会自动重新编译该文件。
静态导入
有一种 import 语句允许导入静态方法和字段,而不只是类
比如:
1 | import static java.lang.Math.*; |
这个场合,使用 Math 包内的静态方法、字段时,不需要再添加类名前缀。
1 | double n = pow(10, 5); // <———— 本来是 double n = Math.pow(10, 5); |
—— 上述方法、字段见 [[12 常用类]]
JAR 文件
为了避免向用户提供包含大量类文件的复杂目录结构,可以将 Java 程序打包成 JAR (Java 归档)文件。
一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。
JAR 文件是压缩的。其使用了 ZIP压缩格式。
创建 JAR:
使用 jar 工具以制作 JAR 文件。该工具在 jdk/bin 目录下
1 | jar cvf 包名 文件名1 文件名2 ... |
关于 jar 工具的各种指令,还是自己去百度一下吧
继承
继承:能解决代码复用,让我们的编程更接近人类思维。当多个类存在相同的 属性(变量)和 方法 时,可以从这些类中抽象出 父类(基类/超类)。在 父类 中定义这些属性·方法,所有的子类不需要重新定义这些属性和方法,只需要通过
extends
来声明继承父类即可。通过继承的方法,代码的复用性提高了,代码的维护性和拓展性也提高了。
1 | public class Son extends Father {}; // Son 类继承了 Father 类 |
定义类时可以指明其父类,也能不指明。不指明的场合,默认继承 Object 类。
所有类有且只有一个父类。Object 是所有类的直接或间接父类。只有 Object 本身没有父类。
使用细节
子类 继承了所有属性和方法,但私有(
private
)的 属性·方法 不能在 子类 直接访问。要调用父类提供的 公共(public
)等方法 访问。子类 必须调用 父类 的 构造器,完成 父类 的 初始化。
当创建 子类对象 时,不管使用 子类的哪个构造器,默认情况下总会调用 父类的无参构造器。如果 父类 没有提供 无参构造器,则必须在 子类的构造器 中用
super
去指定使用 父类的哪个构造器 完成 对父类的初始化。否则编译不能通过。如果希望指定调用 父类的某构造器,则显式地调用一下:
super(形参列表);
super
在使用时,必须放在构造器第一行。super
只能在构造器中使用。由于
super
与this
都要求放在第一行,所以此两个方法不能同时存在于同一构造器。Java 所有的类都是
Object
的子类。换言之,Object
是所有类的父类。父类构造器的调用不限于直接父类,将持续向上直至追溯到顶级父类
Object
子类 最多只能直接继承 一个 父类。即,Java 中是 单继承机制。
不能滥用继承。子类 和 父类 之间必须满足 is - a 的逻辑关系。
继承的本质
- 内存布局:
- 在 方法区,自顶级父类起,依次加载 类信息。
- 在 堆 中开辟一个空间,自顶级父类起,依次创建并初始化各个类包含的所有属性信息。
- 在 栈 中存放该空间的 地址。
- 如何查找信息?
- 查看该子类是否有该属性。如果该子类有这个属性且可以访问,则返回信息。
- 子类没有该属性的场合,查看父类是否有该属性。如有且可访问,则返回信息。如不可访问,则报错。
- 父类也没有该属性的场合,继续查找上级父类,直到顶级父类(Object)。
- 如需调用某个特定类包含的特定信息,可以调用该类提供的方法。
super 关键字
super
代表父类的引用。用于访问父类的 属性、方法、构造器。
super 的使用:
super.属性名
:访问父类的属性。不能访问父类的私有(private)属性。super.方法名(形参列表)
:访问父类的方法。不能访问父类的私有(private)方法。super(参数列表);
:访问父类的构造器。此时,super 语句必须放在第一句。
使用细节:
- 调用父类构造器,好处是分工明确。父类属性由父类初始化,子类由子类初始化。
- 子类中由和父类中成员(属性和方法)重名时,要调用父类成员必须用
super
。没有重名的场合,super
、this
及直接调用的效果相同。 super
的访问不限于直接父类。如果爷爷类和本类中都有同名成员也能使用。如果多个基类中都有同名成员,则遵循就近原则。
方法重写 / 覆盖
方法重写/覆盖(Override):如若子类有一个方法,和父类的某方法的 名称、返回类型、参数 一样,那么我们就说该子类方法 覆盖 了那个父类方法。
使用细节:
- 子类方法的参数,方法名称,要和父类方法完全一致。
- 子类方法的返回类型需和父类方法 一致,或者是父类返回类型的子类。
- 子类方法 不能缩小 父类方法的访问范围(访问修饰符)。
多态
多态:方法 或 对象 有多种形态。多态 是面向对象的第三大特征,是建立在 封装 和 继承 的基础之上的
多态的体现
方法的多态:重写 和 重载 体现了 方法的多态。
对象的多态:
一个对象的 编译类型 和 运行类型 可以不一致。
Animal animal = new Dog();
上例,编译类型是
Animal
,运行类型是子类Dog
。要理解这句话,请回想 [[6 面向对象编程(基础)]]:animal
是对象的引用。编译类型在定义对象时就确定了,不能改变。
运行类型是可以变化的。
上例中,再让
animal = new Cat();
,这样,运行类型变为了Cat
编译类型看定义时
=
的左边,运行类型看=
的右边。
使用细节
多态的前提:两个对象 / 类存在继承关系。
多态的向上转型:
- 本质:父类的引用指向了子类的对象。
- 语法:
父类类型 引用名 = new 子类类型(参数列表);
- 编译类型看左边,运行类型看右边。
- 可以调用父类中的所有成员,但不能调用子类特有的成员,而且需要遵守访问权限。因为在编译阶段,能调用哪些成员是由编译类型决定的。
- 最终的运行结果要看子类的具体实现。即从子类起向上查找方法调用。
多态的向下转型:
语法:
子类类型 引用名 = (子类类型)父类引用;
[7.6.2.2] 的例子里,向下转型。这个语法其实和 [2.8.2 强制类型转换] 很像。
Dog dog = (Dog)animal;
只能强转父类的引用,不能强转父类的对象。
要求父类的引用必须指向的是当前目标类型的对象。即上例中的
animal
运行类型需是Dog
向下转型后,可以调用子类类型中的所有成员。
属性没有重写一说。和 方法 不同,属性的值 看编译类型。
instanceof
比较操作符。用于判断对象类型是否是某类型或其子类型。此时判断的是 运行类型。
理解方法调用
在对象上调用方法的过程如下:
编译器查看对象的声明类型和方法名。该类和其父类中,所有同名方法(包括参数不同的方法)都被列举。
至此,编译器已经知道所有可能被调用的方法。
编译器确认方法调用中提供的参数类型。
那些列举方法中存在参数类型完全匹配的方法时,即调用该方法。
没有发现匹配方法,抑或是发现经过类型转换产生了多个匹配方法时,就会报错
至此,编译器已经知道要调用方法的名字和参数类型
如若是 private 方法、static 方法、final 方法、构造器,那么编译器将能准确知道要调用哪个方法。这称为 静态绑定
与之相对的,如果调用方法依赖于隐式参数类型,那么必须在运行时 动态绑定
程序运行并采取动态绑定方法时,JVM 将调用那个 实际类型 对应的方法。
倘若每次调用方法都进行以上搜索,会造成庞大的时间开销。为此,JVM 预先为每个类计算了 方法表。
方法表中列举了所有方法的签名与实际调用的方法。如此,每次调用方法时,只需查找该表即可。
特别地,使用 super 关键字时,JVM 会查找其父类的方法表。
动态绑定机制:
- 当调用对象方法的时候,该方法和该对象(隐式参数)的内存地址/运行类型绑定。
- 当调用对象属性时,没有动态绑定机制。于是哪里声明,哪里调用。
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 的默认实现如下
1 | public String toString() { |
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();
主动触发垃圾回收机制。这个方法一经调用就会继续执行余下代码,而不会等待回收完毕。 - 实际开发中,几乎不会运用该方法。
断点调试(Debug)
断点调试:在程序某一行设置一个断点,调试时,代码运行至此就会停住,然后可以一步一步往下调试。调试过程中可以看各个变量当前的值。如若出错,则测试到该出错代码行即显示错误并停下。进行分析从而找到这个 Bug。
调试过程中是运行状态,所以,是以对象的 运行类型 执行。
断点调试是程序员必须掌握的技能,能帮助我们查看 Java 底层源代码的执行过程,提高程序员 Java 水平。
快捷键如下
- 跳入:
F7
- 跳过:
F8
- 跳出:
shift + F8
- resume,执行到下一个断点:
F9
附录
零钱通程序
Wallet.java
1
2
3
4
5
6
7
8
9package com.the_wallet;
public class Wallet {
public static void main(String[] args) {
Data p1 = new Data("Sinarcsinx");
p1.menu();
System.out.println("再见~");
}
}Data.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139package 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("--------------------------------------");
}
}