6 面向对象编程(基础)

面向对象是一种开发软件的方法,使分析、设计和实现一个系统的方法尽可能接近人们认识一个系统的方法。包括三个方面:面向对象分析、面向对象设计、面向对象程序设计。

Java 语言是纯面向对象的语言。其所有数据类型都有相应的类,程序可以完全基于对象编写。

6.1 类与对象(OOP)

类 就是数据类型。可以是 int 也可以是 人类

对象 就是其中具体的实例。可以是 100 也可以是 韩顺平

从 类 到 对象,可以称为 创建一个对象,也可以说 实例化一个对象,或者 把对象实例化

  1. 类 是抽象的、概念的,代表一类事物
  2. 对象 是具体的、实际的,代表一个个具体事物
  3. 类 是 对象 的模板,对象 是 类 的一个个体,对应一个实例

下面,我们定义了一个类 Cat 并创建了一些 对象 cat1 cat2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Code6_1{
public static void main(String[] args){

Cat cat1 = new Cat();
cat1.name = "福福";
cat1.age = 2;
Cat cat2 = new Cat();
cat2.name = "妞子";
cat2.age = 1;
System.out.println(cat1.name);
}
}

class Cat{
String name;
int age;
}JAVA

6.1.1 属性/成员变量

从概念或叫法上看:成员变量 = 属性 = field(字段)

1
2
3
4
class Cat{
String name;
int age;
}JAVA

其中,String name; 就是一个成员变量(属性)。

属性可以是基本数据类型,也可以是引用数据类型。

  1. 属性的定义语法同变量。

    1
    访问修饰符 属性类型 属性名
    • 访问修饰符:控制属性的访问范围。有四种:publie protected 默认(空) private
  2. 属性的定义类型可以为任意类型,包含 基本类型 或 引用类型

  3. 属性如果不赋值,有默认值。规则同 [[5.1.1 数组 - 使用细节 - 3]](https://i-melody.github.io/2021/11/27/Java/入门阶段/5 数组、排序和查找/#5-1-1-使用细节)

6.1.2 创建对象

  • 先声明再创建:

    1
    2
    Cat cat1;  				    //声明对象cat1
    cat1 = new Cat(); //创建对象JAVA
  • 直接创建:

    1
    2
    Cat cat2 = new Cat();
    JAVA

注意事项:

  1. 声明对象的场合,只是在内存中建立了一个引用。此时,该地址引用不指向任何内存空间。

    对象的引用,也被称为对象的句柄。

  2. 使用 new 运算符创建对象实例时,会为对象分配空间,就会调用类的构造方法。那之后,会将该段内存的首地址赋给刚才建立的引用。

6.1.3 访问对象

基本语法:对象名.属性名

1
2
System.out.println(cat1.name);
JAVA

6.1.4 类与对象的内存访问机制

栈:一般存放基本数据类型(局部变量)

堆:存放对象(如Cat cat1 = new Cat(),是在这里开辟的空间)

方法区:常量池(常量,比如字符串),类加载信息

  1. 创建对象时,先加载 类 信息,然后在 堆 中分配空间,栈 中的对象名被赋予指向那个空间的地址。
  2. 之后进行指定初始化。该对象的 属性 中,是 基本数据类型 的直接记录在 堆 中;是 字符串 的记录一个地址,该地址指向 方法区,那里的常量池有该字符串。

6.2 成员方法

在某些情况下,我们需要定义成员方法。比如 Cat 除了有属性(name age)外,还可以有一些行为比如玩耍。

1
2
3
4
修饰符 返回数据类型 方法名(形参列表){
方法体语句;
returen 返回值; //返回数据类型是 void 的场合,return语句不是必须的
}JAVA
  1. 方法名必须是一个合法的标识符

  2. 返回类型即返回值的类型。如果方法没有返回值,应声明为 void

  3. 修饰符段可以有几个不同的修饰符。

    比如

    1
    2
    3
    public static strictfp final void method() {
    System.out.println("哎咿呀~ 哎咿呀~");
    }JAVA

    其中 public(访问修饰符)、static(static 关键字)、final(final 关键字)

    —— 访问修饰符见 [[7.3 访问修饰符]](https://i-melody.github.io/2021/12/05/Java/入门阶段/7 面向对象编程(中级)/#7-3-访问修饰符)

    —— static 关键字见 [[9.1.1 类变量]](https://i-melody.github.io/2021/12/14/Java/入门阶段/9 面向对象编程(高级)/#9-1-1-类变量)

    —— final 关键字见 [[9.5 final 关键字]](https://i-melody.github.io/2021/12/14/Java/入门阶段/9 面向对象编程(高级)/#9-5-final-关键字)

    —— strictfp 关键字见 [[12.1.4 strictfp 关键字]](https://i-melody.github.io/2021/12/19/Java/入门阶段/12 %常用类/#12-1-4-strictfp-关键字)

  4. 参数列表是传递给方法的参数表。各个元素间以 , 分隔。每个元素由一个类型和一个标识符表示的参数组成。

    特别地,参数类型... 标识符 这样的参数被称为可变参数

    —— 可变参数见 [[6.5 可变参数]](https://i-melody.github.io/2021/11/29/Java/入门阶段/6 面向对象编程(基础)/#6-5-可变参数)

  5. 方法体是实际要执行的代码块。方法体一般用 return 作为方法的结束。

使用 成员方法,能提高代码的复用性。而且能把实现的细节封装起来,供其他用户调用。

1
2
3
4
5
6
7
8
class Cat{
String name; //属性 name
int age; //属性 age

public void speak(){ //方法 speak()
System.out.println("喵~");
}
}JAVA
  1. 方法写好后,不去调用就不会输出
  2. 先创建对象,然后调用方法即可

下面,展示一个含有成员方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Code6_2{
public static void main(String[] args){
Cat cat1 = new Cat();
cat1.speak(10, 15); //调用 speak 方法,并且给 n1 = 10, n2 = 15
int r = cat1.speak2(15, 135); //调用 speak2 方法,返回值赋给 r
}
}

class Cat{
public void speak(int n1, int n2){ //(int n1, int n2)形参列表,当前有两个形参 n1,n2
int res = n1 + n2;
System.out.println("喵~" + n1 + " + " + n2 +" 的值是:" + res);
}

public int speak2(int n1, int n2){ //int 表示方法执行后,返回一个 int 值
int res = n1 + n2;
return res; //返回 res 的值
}
}JAVA

6.2.1 方法的调用机制

以前文代码为例:

1
2
3
4
5
6
7
8
...
int r = cat1.speak2(15, 135);
...
public int speak2(int n1, int n2){
int res = n1 + n2;
return res;
}
...JAVA
  1. 当程序执行到方法时,在 栈 中开辟一个新的 栈空间。该空间里储存 n1 = 15 n2 = 135,之后计算并储存结果 res = 150
  2. 当方法执行完毕,或执行到 return 语句时,就会返回
  3. 把 新栈空间 中的 res = 150 返回 main栈 中调用方法的地方
  4. 返回后,继续执行该方法的后续代码

6.2.2 使用细节

  1. 访问修饰符:作用是控制方法的使用范围。

    • 不写(默认访问控制范围)
    • public:公共
    • protected:受保护
    • private:私有

    —— 访问修饰符见 [[7.3访问修饰符]](https://i-melody.github.io/2021/12/05/Java/入门阶段/7 面向对象编程(中级)/#7-3-访问修饰符)

  2. 返回数据类型:

    • 一个方法最多有一个返回值。要返回多个结果可以使用 数组。
    • 返回类型为任意类型。包括 基本数据类型 和 引用数据类型。
    • 如果方法要求有返回数据类型,则方法体中最后的执行语句必为 return 值,且返回类型必须和 return 的值一致。
    • 如果 返回数据类型 为 void,则可以不写 return 语句
  3. 方法名:

    • 遵循驼峰命名法,最好见名知意,表达出该功能的意思。
  4. 参数列表(形参列表):

    • 一个方法可以有 0 个参数,也可以有多个参数。参数间用 , 间隔。
    • 参数类型可以为任意类型,包含 基本类型 和 引用类型。
    • 调用带参数的方法时,一定对应着 参数列表 传入 相同类型 或 兼容类型 的参数。
    • 方法定义时的参数称为 形式参数 ,简称 形参;方法调用时的参数(传入的参数)称为 实际参数,简称 实参。实参 与 形参 的类型、个数、顺序必须一致。
  5. 方法体:

    • 写完成功能的具体语句。方法中不能再定义方法。即:方法不能嵌套定义。
  6. 调用细节:

    • 同一个类中的方法调用,可以直接调用。

    • 跨类的方法调用,需要创建新对象,然后再调用方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class C1{
      public void m1(){
      }
      public void m2(){
      m1(); //同一个类中的方法调用,可以直接调用。
      }
      }

      class C2{
      public void m3(){
      C1 c = new C1();
      c.m2(); //跨类的方法调用,需要创建新对象,然后再调用方法。
      }
      }JAVA

6.2.3 成员方法传参机制

Java 语言对对象采用的是 值传递,方法得到的总是那个传入对象的副本。

  • 方法不能修改基本数据类型的参数。基本数据类型传递的是一个值,形参不影响实参。

  • 方法可以改变对象参数的状态。

    引用类型传递的是一个地址,形参和实参指向一处,两者总会相关。

    但改变那个形参地址指向的场合,实参的指向不会改变。

6.3 方法递归调用

递归:即方法自己调用自己,每次调用时传入不同变量。递归有助于编程者解决复杂问题,同时可以让代码变得简洁。

下面,示范一个斐波那契数列方法

1
2
3
4
5
6
7
8
9
>class T{
public int fib(int n){
if(n == 1 || n == 2){
return 1;
}else{
return (fib(n - 1)) + (feb(n - 2));
}
}
>}JAVA

6.3.1 使用细节

  1. 执行一个方法时,就创建一个新的受保护的独立 栈空间。
  2. 方法的局部变量是独立的,不会相互影响。
  3. 如果方法中使用的是引用变量,就会共享数据。(因为 [[6.2.3 成员方法传参机制]](https://i-melody.github.io/2021/11/29/Java/入门阶段/6 面向对象编程(基础)/#6-2-3-成员方法传参机制))
  4. 递归必须向退出递归的条件逼近,否则就是无限递归,会提示 StackOverflowError “死龟”
  5. 当一个方法执行完毕,或遇到 return 就会返回。遵守谁调用就返回给谁。同时当方法执行完毕或返回时,该方法也执行完毕。

6.4 方法重载

方法重载(Overload):Java 中允许同一类中,多个同名方法的存在,但要求 形参列表 不一致。

这样,减轻了起名和记名的麻烦。

使用细节:

  1. 方法名:必须相同
  2. 形参列表:必须不同(参数的类型、个数、顺序,这其中至少一个不同)
  3. 返回值:无要求

签名:

由于重载的存在,要完整的描述一个方法,要指定方法名及参数类型。这叫做方法的签名。

如:

1
2
3
4
public void act() {}
public int act(int n) {
return n;
}JAVA

两个方法的签名分别是:act()act(int n)

6.5 可变参数

Java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。

语法:访问修饰符 返回类型 方法名(数据类型... 形参名){代码块;}

1
2
3
4
5
public void m(int... n){
//此时,n 相当于一个 数组。
int length = n.length;
int num1 = n[0];
}JAVA

6.5.1 使用细节

  1. 可变参数 的实参可以是 0 个,也可以是 任意多 个。

  2. 可变参数 的实参可以是数组

  3. 可变参数 本质就是数组

    因此,出现:

    1
    2
    public void met(int... n){				//这个方法与下面的方法不能构成重载
    }JAVA

    的场合,不能有方法:

    1
    2
    public void met(int[] n){				//这个方法与上面的方法不能构成重载
    }JAVA
  4. 可变参数 和 普通参数 可以一起放在形参列表,但必须保证 可变参数 在最后

    1
    2
    public void m(double dou, int... n) {}
    JAVA
  5. 一个形参列表最多出现 一个 可变参数。

6.6 作用域

  1. 在 Java 编程中,主要的变量就是 属性(成员变量)和 局部变量。
  2. 我们说的 局部变量 一般是指在成员方法中定义的变量。
  3. 作用域的分类
    • 全局变量:也就是 属性,作用域为整个类体
    • 局部变量:除了属性外的其他变量。作用域为定义它的代码块中
  4. 全局变量(属性)可以不赋值直接使用,那个场合有默认值。局部变量必须赋值使用

6.6.1 使用细节

  1. 属性 和 局部变量 可以重名,访问时遵循就近原则

  2. 在同一作用域中,两个局部变量不能重名

  3. 属性 的生命周期较长。其伴随对象的创建而创建,伴随对象的销毁而销毁。

    局部变量 生命周期较短。其伴随代码块的执行而创建,伴随代码块的结束而销毁。

  4. 全局变量/属性 可以被本类使用,也可以被其他类(通过对象)使用。

    局部变量 只能被本类的对应方法中调用

  5. 全局变量/属性 可以加 修饰符

    局部变量 不能加 修饰符

6.7 构造方法、构造器

构造方法又叫构造器(constructor),是类的一种特殊的方法。它的主要作用是完成对新对象的初始化。

语法:[修饰符] 方法名(形参列表){方法体}

  1. 构造器的修饰符可以是默认。也可以是别的
  2. 参数列表 规则同 成员方法

以下示范一个构造器:

1
2
3
4
5
6
7
8
9
class T{
String name;
int mun;
//下面这块就是构造器
public T(String str, int i){
name = str;
num = i;
}
}JAVA

6.7.1 使用细节

  1. 构造器本质也是方法。所以,可以 构造器重载。
  2. 构造器名 和 类名 相同
  3. 构造器无返回值
  4. 构造器是完成对象的初始化,而不是创建
  5. 创建对象时,系统自动调用构造器
  6. 如果程序员没有定义构造器,系统会自动给类生成一个无参构造器(默认构造器)
  7. 一旦定义了自己的构造器,就不能用无参构造器了。除非显式的定义一个无参构造器

6.7.2 流程分析

1
2
3
4
5
6
7
8
9
10
11
12
Person p1 = new Person("Amy", 10);

...

class Person{
String name;
int age = 20;
public Person(String pName, int pAge){
name = pName;
age = pAge;
}
}JAVA
  1. 加载 类信息(方法区)

  2. 在 堆 中开辟空间(地址)

  3. 完成对象初始化

    • 首先默认初始化。age = 0; name = null

    • 之后显式初始化。age = 20; name = null

      其中,显式初始化和代码块初始化按编写的先后顺序依次进行。

    • 之后构造器的初始化。age = 10; name = "Amy"

  4. 把对象在 堆 中的 地址,返回给 p1

6.8 this 关键字

JVM 会给每个对象分配 this 代表当前对象。

相当于在 堆 中,this 指向自己(对象)

在类定义的方法中,Java 会自动用 this 关键字把所有变量和方法引用结合在一起。

遇到有同名的局部变量的场合,需要程序员加入 this 关键字进行区分。不加入 this 关键字的场合,Java 遵循就近原则。

1
2
3
4
class Example{
int n = 0;
public void act(int n) {}
}JAVA

上面这个类的 act() 方法实际有 2 个参数。对其调用:

1
2
Example e = new Exmaple();
e.act(100);JAVA

可见,出现在方法名前的参数 e,以及出现在方法名后的括号中的参数 100

出现在方法名前的参数被称为 隐式参数(也称为 方法调用的 目标 或 接收者)

出现在方法名后的参数被称为 显式参数,就是所谓的实参

在每一个方法中,用 this 指代隐式参数。

1
2
3
public void act(int n) {
this.n = n;
}JAVA

此时,再以相同方式调用方法:

1
2
e.act(100);					// <———— 相当于 e.n = 100;
JAVA

6.8.1 使用方法

  1. this 关键字可以用来访问本类的属性、方法、构造器

  2. this 用于区分当前类的 属性 和 局部变量

  3. 访问本类中成员方法的语法:this.方法名

  4. 访问构造器的语法:this(参数列表);

    注意:只能在构造器中访问另一个构造器。而且,如果有这个语法,必须放置在第一条语句。

  5. this 不能在类定义的 外部 使用,只能在类定义的 方法中 使用

附录

迷宫游戏代码

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
/**
*@author Melody
*@version v1.2.6
**/

//迷宫
import java.util.Scanner;
public class MazeOut{

public static void main(String[] args){
//tools 方便后面调用方法。 inP 可以接收用户输入
T tools = new T();
Scanner inP = new Scanner(System.in);

//提示并接收用户输入信息
System.out.println("\n输入迷宫宽度(至少为6):");
int x = inP.nextInt();
System.out.println("\n输入迷宫长度(至少为6):");
int y = inP.nextInt();
//若用户输入的长或宽超出范围,则将其重置为正常值
if(x < 6){
x = 6;
} else if(x > 110){
x = 110;
}
if(y < 6){
y = 6;
} else if(y > 60){
y = 60;
}
System.out.println("\n输入迷宫的困难度(请输入1 - 6的数字,数字越高越不容易获胜):");
int hard = inP.nextInt();
if(hard == 7){
System.out.println("\n\n您选择了找点麻烦");
} else if (hard == 8 || hard == 9){
System.out.println("\n\n您选择了给自己添堵");
}
System.out.println("\n\t迷宫生产完毕\n");

//设置一个 count 值,记录步数。设为数组,以便数据通用。第一位记录当前值,第二位为最大值。
int[] count = {0, 0};
//调用方法,生成迷宫
char[][] maze =new char[y][x];
tools.newMaze(maze.length, maze[0].length, maze, hard);
//调用方法,展示迷宫
tools.showMaze(maze);

//提示用户开始游戏
System.out.println("\n召唤一个探索者,来探索迷宫吧(随便输点什么吧)");
//输入 r 或 c 则采用递归方法,其余采用爬墙方法
char inC = inP.next().charAt(0);
if(inC == 'c'){
System.out.println("\n您触发了迷宫之神的眷顾。");
if(hard > 5){
System.out.println("\n迷宫之神眉头一皱,发现事情并不简单。");
}
if(x > 12 || y > 12){
System.out.println("看到地图这么大,迷宫之神悻悻而归。他只喜欢12格以下的地图。");
return;
}
} else if(inC == 'r'){
System.out.println("\n您引来了一群无畏小黄鸡。他们视死如归,一心想着寻找出口");
} else {
System.out.println("\n我们找来了一只小蜘蛛。试试看吧。");
}
System.out.println("\n");
//调用方法,解密
if(inC == 'r' || inC == 'c'){
tools.outMazeRec(maze, inC, count);
} else {
tools.outMaze(maze, count);
}

}
}





class T{
//=======================================================================================

//方法 newMaze:让 n3 生成随机的 长 * 宽 = n1 * n2 的迷宫,其困难度为 n4
public void newMaze(int n1, int n2, char[][] n3, int n4){
//构建迷宫墙壁,以'#'表示。并随机向其中填充石块,以'O'表示
////墙壁是迷宫的 开头和结尾行 以及 每行的开头和结尾
for(int i = 0; i < n1; i++){
for(int j = 0; j < n2; j++){
if(i == 0 || i == n1 - 1 ||j == 0 || j == n2 - 1){
n3[i][j] = '#';
}else{
//ran 是一个随机值,此处是概率生成挡路的石块'O'。其概率与 n4 值的大小正相关
//此外,若 n4(即用户输入的难度值 hard)超过范围,则按照 难度6 计算
int ran;
if(n4 <= 9 && n4 >= 0){
ran = (int)(Math.random() * (9 - n4) + 1);
}else{
ran = (int)(Math.random() * 3 + 1);
}
n3[i][j] = (ran == 1) ? 'O' : ' ';
}
}
}
//生成起点、终点,优化地形
n3[1][1] = 'B';
n3[2][1] = ' ';
n3[1][2] = ' ';
n3[n1 - 2][n2 - 2] = 'F';
n3[n1 - 3][n2 - 2] = ' ';
n3[n1 - 2][n2 - 3] = ' ';
}





//方法 showMaze:展示一个迷宫
public void showMaze(char[][] n1){
for(int i = 0; i < n1.length; i++){
for(int j = 0; j < n1[i].length; j++){
System.out.print(" " + n1[i][j]);
}
System.out.println();
}
}
//=======================================================================================






//=======================================================================================
//方法 outMazeRec:递归方法迷宫游戏入口。可以接入普通递归方法,或最短路径方法。
public void outMazeRec(char[][] n1, char n2, int[] count){
//out:是否走出迷宫
boolean out = false;
//将迷宫的起止位置记为通路
n1[1][1] = ' ';
n1[n1.length - 2][n1[0].length -2] = ' ';
//如果输入的是'c',则采用最短路径法。反之采用普通递归方法
if(n2 == 'c'){
out = outCountMaze(1, 1, n1, count);
}else{
out = outMazeRecursion(1, 1, n1, count);
}
//把迷宫起始位置重新标注出来
n1[1][1] = 'B';
//判断是否解谜成功。如果成功,迷宫终点显示'V',并展示步数,否则显示'F'
if(out){
n1[n1.length - 2][n1[0].length -2] = 'V';
showMaze(n1);
System.out.println("\t YOU WIN!!!");
System.out.println("通过路径为 " + count[1] + " 格");
} else {
n1[n1.length - 2][n1[0].length -2] = 'F';
showMaze(n1);
System.out.println("\t YOU LOSE");
}

}
//=======================================================================================




//=======================================================================================
//方法 outMazeRecursion:迷宫游戏,普通递归方法
public boolean outMazeRecursion(int y, int x, char[][] n3, int[] count){
count[1]++;
if(n3[n3.length - 2][n3[0].length - 2] == '.'){
return true;
} else if(n3[y][x] == ' '){
n3[y][x] = '.';
if(outMazeRecursion(y, x + 1, n3, count)){
return true;
} else if(outMazeRecursion(y + 1, x, n3, count)){
return true;
} else if(outMazeRecursion(y, x - 1, n3, count)){
return true;
} else if(outMazeRecursion(y - 1, x, n3, count)){
return true;
} else{
count[1]--;
n3[y][x] = '+';
}
} else {
count[1]--;
return false;
}
count[1]--;
return false;
}
//=======================================================================================






//=======================================================================================
//方法 outCountMaze:迷宫游戏,最短路径法的入口。这个入口由普通递归法接入。
public boolean outCountMaze(int y, int x, char[][] n, int[] count){

//首先,创建一个里数组。该数组用于 part1,原数组用于 part2。
//似乎没必要作此设计。但我还是予以保留。
char[][] inMaze = new char[n.length][n[0].length];
for(int i = 0; i < n.length; i++){
for(int j = 0; j < n[0].length; j++){
inMaze[i][j] = n[i][j];
}
}

//首先进行 part1,然后必定进行 part2。因为 part1 总会返回一个 false
if(countMazeRec(y, x, inMaze, count) || true){
count[0] = 0;
return outMazeRecC(y, x, n, count);
}
return false;
}


//方法 countMazeRec:迷宫游戏,最短路径法,part1
//该方法是先统计最短路径。最终总会返回 false
public boolean countMazeRec(int y, int x, char[][] n3, int[] count){
count[0]++;
if(y == n3.length - 2 && x == n3[0].length - 2){
if(count[0] < count[1] || count[1] == 0){
count[1] = count[0];
}
} else if(n3[y][x] == ' '){
n3[y][x] = '.';
if(countMazeRec(y, x + 1, n3, count)){
return true;
} else if(countMazeRec(y + 1, x, n3, count)){
return true;
} else if(countMazeRec(y, x - 1, n3, count)){
return true;
} else if(countMazeRec(y - 1, x, n3, count)){
return true;
} else{
n3[y][x] = ' ';
count[0]--;
return false;
}
} else {
count[0]--;
return false;
}
count[0]--;
return false;
}






//方法 outMazeRecC:迷宫游戏,最短路径法,part2
//该方法是在 part1 统计完最短路径后,按最短路径走出迷宫,并绘制路径
public boolean outMazeRecC(int y, int x, char[][] n3, int[] count){
count[0]++;
if(y == n3.length - 2 && x == n3[0].length - 2){
if(count[0] <= count[1]){
return true;
} else {
n3[n3.length - 2][n3[0].length - 2] = ' ';
count[0]--;
return false;
}
} else if(n3[y][x] == ' '){
n3[y][x] = '.';
if(outMazeRecC(y, x + 1, n3, count)){
return true;
} else if(outMazeRecC(y + 1, x, n3, count)){
return true;
} else if(outMazeRecC(y, x - 1, n3, count)){
return true;
} else if(outMazeRecC(y - 1, x, n3, count)){
return true;
} else{
n3[y][x] = ' ';
count[0]--;
return false;
}
} else {
count[0]--;
return false;
}

}
//=======================================================================================






//=======================================================================================
//方法 outMaze:爬墙方法迷宫游戏入口
public void outMaze(char[][] n1, int[] count){
//boolean out:记录是否走出迷宫
boolean out = false;
//角色光标 m
n1[1][1] = 'm';

//创建一系列变量,后面解释用法
//创建 角色坐标
int x = 1;
int y = 1;
//创建 辅助坐标 及 方向字符。初始方向为右。
int xi = 1;
int yi = 0;
char dir = 'r';
//创建 里迷宫,标记起止点。
char[][] inMaze = new char[n1.length][n1[0].length];
inMaze[1][1] = 'B';
inMaze[n1.length - 2][n1[0].length - 2] = 'F';

//开始走迷宫。
//如果一个迷宫有出路,则沿着一侧的墙壁走就一定能走到出路。以下方法就是基于这个原理。
//角色坐标 y,x 是角色所在的位置坐标。辅助坐标 yi,xi 是角色靠近的墙壁坐标。
//dir 代表角色此时的朝向。只要角色按照墙壁延申的方向向前,就一定不会迷路。
//里迷宫的大小和真迷宫相同,坐标也一一对应。目的是为了记录数据,这些数据不会被用户看到。
//里迷宫记载了 起始点 和 终点 的位置。如角色回到起点,则必定失败。到达终点则成功。
for(;;){

//判断 是否走出迷宫。如若是,则展示迷宫,记录脱出为真,并退出寻路
if(inMaze[y][x] == 'F'){
n1[y][x] = 'V';
n1[1][1] = 'B';
showMaze(n1);
out = true;
break;
}

//通过爬墙方式试图走出迷宫
//这是方向朝右时的情况
if(dir == 'r'){
//如果角色面对墙壁,意味着走到了墙角,则角色坐标不变,调整墙壁坐标,并转向
if(n1[y][x + 1] == '#' || n1[y][x + 1] == 'O'){
dir = yi > y ? 'u' : 'd';
yi = y;
xi = x + 1;
//如果面前有路,且墙壁延伸,则前进
} else if (n1[yi][xi + 1] == '#' || n1[yi][xi + 1] == 'O'){
n1[y][x] = '.';
x++;
xi++;
n1[y][x] = 'm';
count[1]++;
//如果面前有路,但墙壁不延伸,则是遇到了转角。角色移动,转向,但墙壁坐标不变
} else {
dir = yi > y ? 'd' : 'u';
n1[y][x] = '.';
n1[y][x + 1] = '.';
y = yi;
x = xi + 1;
n1[y][x] = 'm';
count[1] += 2;
}
//这是方向朝左的情况
} else if(dir == 'l'){
if(n1[y][x - 1] == '#' || n1[y][x - 1] == 'O'){
dir = yi > y ? 'u' : 'd';
yi = y;
xi = x - 1;
} else if(n1[yi][xi - 1] == '#' || n1[yi][xi - 1] == 'O'){
n1[y][x] = '.';
x--;
xi--;
n1[y][x] = 'm';
count[1]++;
} else {
dir = yi > y ? 'd' : 'u';
n1[y][x] = '.';
n1[y][x - 1] = '.';
y = yi;
x = xi - 1;
n1[y][x] = 'm';
count[1] += 2;
}
//这是方向朝下的情况
} else if(dir == 'd'){
if(n1[y + 1][x] == '#' || n1[y + 1][x] == 'O'){
dir = xi < x ? 'r' : 'l';
yi = y + 1;
xi = x;
} else if(n1[yi + 1][xi] == '#' || n1[yi + 1][xi] == 'O'){
n1[y][x] = '.';
y++;
yi++;
n1[y][x] = 'm';
count[1]++;
} else {
dir = xi < x ? 'l' : 'r';
n1[y][x] = '.';
n1[y + 1][x] = '.';
y = yi + 1;
x = xi;
n1[y][x] = 'm';
count[1] += 2;
}
//这是方向朝上的情况
} else if(dir == 'u'){
if(n1[y - 1][x] == '#' || n1[y - 1][x] == 'O'){
dir = xi < x ? 'r' : 'l';
yi = y - 1;
xi = x;
} else if(n1[yi - 1][xi] == '#' || n1[yi - 1][xi] == 'O'){
n1[y][x] = '.';
y--;
yi--;
n1[y][x] = 'm';
count[1]++;
} else {
dir = xi < x ? 'l' : 'r';
n1[y][x] = '.';
n1[y - 1][x] = '.';
y = yi - 1;
x = xi;
n1[y][x] = 'm';
count[1] += 2;
}
}

//判断 是否回到起点。如若是,则一定是迷宫无解。展示迷宫并退出寻路
if(inMaze[y][x] == 'B'){
showMaze(n1);
break;
}
}
//输出结果
if(out){
System.out.println("\t YOU WIN!!!\n\t您的步数为:" + count[1]);
} else {
System.out.println("\t YOU LOSE");
}
}
}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
import java.util.Scanner;
public class EightQueen{

public static void main(String[] args){
T tools = new T();
char[][] chess = new char[8][8];
//调用方法,建立棋盘
tools.buildChess(chess);
//调用方法,开始游戏
tools.eightQueen(chess);

}
}



class T{
//buildChess:建立一个新棋盘。该棋盘白色格子用' '表示,黑色格子用'#'表示
public void buildChess(char[][] chess){
for(int i = 0; i < chess.length; i++){
for(int j = 0; j < chess[0].length; j++){
chess[i][j] = ((i + j) % 2 == 0) ? ' ' : '#';
}
}
}




//eightQueen:八皇后游戏的接入口
public void eightQueen(char[][] chess){
//建立 里棋盘 inward 及 计数数组 count。里棋盘用于计算问题,原棋盘输出给用户看。
//计数 count 使用数组,这样其数据在所有方法都能通用
char[][] inward = new char[chess.length][chess[0].length];
int[] count = {0} ;
//进行游戏。因为穷举所有方法,最后返回的一定是 false。反正我们不在意。
boolean isFinished = gameEQS(0, 0, chess, inward, count);
}



//gameEQS:八皇后游戏的基本方法
//八皇后游戏方法。y 代表当前位置的纵坐标,x 是横坐标。chess 是棋盘,inward 是里棋盘,count 是计数数组
public boolean gameEQS(int y, int x, char[][] chess, char[][] inward, int[] count){
//当 y 超出棋盘 时,显然已经完成八皇后。
//由于要进行穷举,此时我们计数并输出棋盘,然后返回 false 使其继续计算
if(y == inward.length){
count[0]++;
System.out.println();
gameEQS2(chess, inward, count);
return false;
//当 x 超出棋盘 时,显然棋盘该列已经无合法放置位置。我们返回 false
} else if(x == inward[0].length){
return false;
//gameEQS1,这个方法是查看该格子是否是合法放置位置。如若是,返回 true,而且在该位置放置棋子'Q'
//当这个位置合法,我们进入下一行,从头开始判断。
//如果后面的判断为 false,我们就拿掉这枚棋子。如果后面判断为 true 说明我们找到了一个方法。
//特别地,由于代码目前是穷举模式,我想我们永远不会在此输出 true
} else if(gameEQS1(y, x, inward)){
if(gameEQS(y + 1, 0, chess, inward, count)){
return true;
} else {
inward[y][x] = ' ';
}
}
//如果代码进行到这个位置,证明我们所在的格子不适合放置棋子。我们只好去看看下一格如何。
return gameEQS(y, x + 1, chess, inward, count);
}



//gameEQS1:该方法是输入一个坐标,并输入里棋盘地址,在里棋盘上查看该位置是否合法
//什么是合法的位置:就是该坐标的 同列、同行、同斜线 没有别的棋子
//如果是合法位置,我们放置一个棋子,并返回 true
public boolean gameEQS1(int y, int x, char[][] inward){
for(int i = 0; i < inward.length; i++){
for(int j = 0; j < inward[0].length; j++){
if(j == x || i == y || i - j == y - x || i + j == y + x){
if(inward[i][j] == 'Q'){
return false;
}
}
}
}
inward[y][x] = 'Q';
return true;
}



//gameEQS2:这个方法是把当前 里棋盘 的棋子放置到棋盘上,输出棋盘 并 输出计数。
//在输出完成后,会清空棋盘。
public void gameEQS2(char[][] chess, char[][] inward,int[] count){
for(int i = 0; i < chess.length; i++){
for(int j = 0; j < chess[0].length; j++){
if(inward[i][j] == 'Q'){
chess[i][j] = 'Q';
}
System.out.print(" " + chess[i][j]);
}
System.out.println();
}
System.out.print("\n" + count[0] + "\n");
buildChess(chess);
}



//gameEQSDebug
//输出里棋盘。测试用。
public void gameEQSDebug(char[][] inward){
for(int i = 0; i < inward.length; i++){
for(int j = 0; j < inward[0].length; j++){
System.out.print(" " + inward[i][j]);
}
System.out.println();
}
System.out.println();
}
}

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. 先引入包,之后创建对象

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

    1
    2
    com.name.T tools = new com.name.T();
    JAVA

命名规则:

  • 只能包含 数字 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 语句允许导入静态方法和字段,而不只是类

比如:

1
2
import static java.lang.Math.*;
JAVA

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

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

—— 上述方法、字段见 [[12.5 Math 类]](https://i-melody.github.io/2021/12/19/Java/入门阶段/12 常用类/#12-5-Math-类)

7.3 访问修饰符

7.3.1 访问权限特点

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

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

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

    ——什么是 子类?详见 [[ 7.5 继承 ]](https://i-melody.github.io/2021/12/05/Java/入门阶段/7 面向对象编程(中级)/#7-5-继承(重点))

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

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

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

7.3.2 使用说明

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

7.4 封装

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

封装的好处:

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

实现步骤:

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

编译多个源文件:

1
2
javac MyClass.java
CMD

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

7.4.1 静态导入

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

比如:

1
2
import static java.lang.Math.*;
JAVA

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

1
2
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 目录下

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

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

7.5 继承

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

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

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

定义类时可以指明其父类,也能不指明。不指明的场合,默认继承 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 类与对象的内存访问机制]](https://i-melody.github.io/2021/11/29/Java/入门阶段/6 面向对象编程(基础)/#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 方法

1
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 方法

1
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 方法

1
String toString()

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

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}JAVA
  • 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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.the_wallet;

    public class Wallet {
    public static void main(String[] args) {
    Data p1 = new Data("Melody");
    p1.menu();
    System.out.println("再见~");
    }
    }JAVA
  • 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
    139
    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("--------------------------------------");
    }
    }

8 项目:房屋出租系统

crud: c[create] r[read] u[update] d[delete]

一阶段毕业作业

  • RentSystem.java(入口)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.house_rent;

    public class RentSystem {
    public static void main(String[] args) {
    RentSysMenu rentSys = new RentSysMenu();
    rentSys.menu();
    System.out.println("再见~");
    }
    }JAVA
  • RentSysMenu.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
    package com.house_rent;

    import java.util.Scanner;

    public class RentSysMenu {
    Method method = new Method();
    private Scanner inp = new Scanner(System.in);
    //menu:这是出租系统的系统菜单界面。用户输入 1 - 6,并据此调用方法。该方法自身会无限循环。
    public void menu() {
    for (; ; ) {
    //这是系统菜单的显示页面
    System.out.print("\n---------------房屋出租系统---------------"
    + "\n\t\t\t 1 新 增 房 屋"
    + "\n\t\t\t 2 查 找 房 屋"
    + "\n\t\t\t 3 删 除 房 屋"
    + "\n\t\t\t 4 修 改 房 屋 信 息"
    + "\n\t\t\t 5 房 屋 列 表"
    + "\n\t\t\t 6 退 出"
    + "\n请选择(1-6):");
    char inpChar = inp.next().charAt(0);
    System.out.println("----------------------------------------\n");
    switch (inpChar) {
    case '1':
    //输入 1 时,调用 创建房屋 方法
    method.newHouse();
    break;
    case '2':
    //输入 2 时,调用 搜索房屋 方法
    method.searchAHouseFromMenu();
    break;
    case '3':
    //输入 3 时,调用 删除房屋 方法
    method.deleteSpecificHouse();
    break;
    case '4':
    //输入 4 时,调用 修改信息 方法
    method.changeHouseInfo();
    break;
    case '5':
    //输入 5 时,调用 展示全部 方法
    method.showAllHouseFromMenu();
    break;
    case '6':
    //输入 6 时,进行二次确认。通过则退出系统。
    System.out.println("确认退出吗?:(Y/N):");
    //yesOrNo 方法,是可以判断用户输入的到底是 Y 还是 N 的方法。
    if (method.yesOrNo(inp.next().charAt(0))) {
    System.out.println("程序已退出。");
    return;
    }
    break;
    default:
    //当用户输入了 1 - 6 以外的东西,提示一个错误信息
    System.out.println("错误,请输入纯数字(1-6)");
    }
    }
    }
    }JAVA
  • Method.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
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    package com.house_rent;

    import java.util.Scanner;

    class Method {

    //创建一个 数据数组。该数组将存放所有未来的房屋数据,每个房屋占用 1 个长度。目前,其长度为0
    Data[] houses = new Data[0];
    private Scanner inp = new Scanner(System.in);

    //newHouse:1 创建房屋 方法。由 menu 接入。
    public void newHouse() {
    System.out.print("=================添加房屋================="
    + "\n请输入姓名:");
    String inName = inp.next();
    //如果名字太短,补长一些,方便排版
    if (inName.length() <= 2) {
    inName = inName + " ";
    }
    System.out.print("请输入电话:");
    String inCall = inp.next();
    System.out.print("请输入地址:");
    String inLocation = inp.next();
    //如果地址太短,补长一些,方便排版
    if (inLocation.length() <= 2) {
    inLocation = inLocation + " ";
    }
    System.out.print("请输入月租金:");
    double inPrice = inp.nextDouble();
    System.out.print("是否租出?(Y/N):");
    //yesOrNo 方法,是可以判断用户输入的到底是 Y 还是 N 的方法。
    boolean inIsRented = yesOrNo(inp.next().charAt(0));
    //调用 写入创建房屋 方法,将以上参数写入 数据数组
    creatData(inName, inCall, inLocation, inPrice, inIsRented);
    System.out.println("=================添加完成=================");
    //调用 真-搜索房屋 方法。为用户展示刚刚创建的房屋信息。
    searchAHouse(houses[houses.length - 1].getId());
    System.out.println("\n请牢记房屋 ID\n如果想要修改信息,请于主菜单选择[4 修改房屋信息]");
    }

    //searchAHouseFromMenu:2 搜索房屋 方法。由 menu 接入。
    public void searchAHouseFromMenu() {
    System.out.println("=================查找房屋=================");
    //调用 真-搜索房屋 方法
    searchAHouse();
    System.out.println("=================查找完毕=================");
    }

    //deleteSpecificHouse:3 删除房屋 方法。由 menu 接入。
    public void deleteSpecificHouse() {
    System.out.println("=================删除房屋=================");
    //首先,搜索并展示用户想要的房屋。
    //调用 真-搜索房屋 方法。如果找到,会返回该房屋的 数组编号;否则,返回 -100
    int num = searchAHouse();
    //如果找到房屋(返回值不为 -100),则进行二次确认。
    if (num != -100) {
    System.out.println("确定要删除该房屋信息吗?(Y/N):");
    //yesOrNo 方法,是可以判断用户输入的到底是 Y 还是 N 的方法。
    if (yesOrNo(inp.next().charAt(0))) {
    System.out.println("一经删除无法恢复!请再次确认,是否删除!(Y/N):");
    if (yesOrNo(inp.next().charAt(0))) {
    //调用 注销房屋 方法。
    deleteHouse(num);
    System.out.println("===============房屋已删除===============");
    }
    }
    }
    }

    //changeHouseInfo:4 修改信息 方法。由 menu 接入
    public void changeHouseInfo() {
    System.out.println("=================修改房屋=================");
    //首先,搜索并展示用户想要的房屋
    //调用 真-搜索房屋 方法。如果找到,会返回该房屋的 数组编号;否则,返回 -100
    int num = searchAHouse();
    //找到房屋的场合,逐项提示并进行修改
    if (num != -100) {
    System.out.println("姓名(" + houses[num].getName() + "):");
    String inName = inp.next();
    if (inName.length() <= 2) {
    inName = inName + " ";
    }
    houses[num].setName(inName);
    System.out.println("电话号码(" + houses[num].getCall() + "):");
    houses[num].setCall(inp.next());
    System.out.println("地址(" + houses[num].getLocation() + "):");
    String inLocation = inp.next();
    if (inLocation.length() <= 2) {
    inLocation = inLocation + " ";
    }
    houses[num].setLocation(inLocation);
    System.out.println("月租金(" + houses[num].getPrice() + "):");
    houses[num].setPrice(inp.nextDouble());
    System.out.println("是否租出(" + (houses[num].isRented() ? "已出租!" : "未出租。") + ")(Y/N):");
    houses[num].setRented(yesOrNo(inp.next().charAt(0)));
    //到这里就改完了
    //调用 真-搜索房屋 方法,展示房屋
    System.out.println("=================修改完成=================");
    searchAHouse(houses[num].getId());
    }
    }

    //showAllHouseFromMenu:5 展示全部 方法。由 menu 接入
    public void showAllHouseFromMenu() {
    System.out.println("===============所有房屋信息===============");
    //调用 展示全部房屋 方法
    showAllHouses();
    System.out.println("============以上是所有房屋信息=============");
    }

    //yesOrNo:这个方法用于判断用户输入的字符究竟表示 Yes 还是 No。
    //判断不了的情况,提示错误语句,并默认为 No
    public boolean yesOrNo(char cha) {
    switch (cha) {
    case 'y':
    case 'Y':
    case '是':
    case '对':
    case '已':
    return true;
    case 'n':
    case 'N':
    case '否':
    case '没':
    case '不':
    case '未':
    return false;
    default:
    System.out.println("输入无效。");
    return false;
    }
    }

    //searchAHouse:真-搜索房屋 方法(无参)。
    //如果调用的是这无参方法,会先提示用户输入一个 ID,然后调用有参方法
    private int searchAHouse() {
    System.out.println("请输入4位数房屋ID:");
    String id = inp.next();
    return searchAHouse(id);
    }

    //searchAHouse:真-搜索房屋 方法(有参)。输入房屋 ID,搜索该房屋。
    //找到房屋的场合,展示房屋 并返回其 数组坐标。如果没找到对应房屋,提示错误信息,并返回 -100。
    private int searchAHouse(String id) {
    for (int i = 0; i < houses.length; i++) {
    if (houses[i].getId().equals(id) && houses[i].isActive()) {
    System.out.println("房屋ID\t姓名\t\t电话\t\t\t地址\t\t月租金\t出租状态");
    houses[i].showThisData();
    return i;
    }
    }
    System.out.println("没有找到房屋!");
    return -100;
    }

    //creatData:写入创建房屋 方法。需要的信息齐集的场合,将其写入 数据数组,并生成一个 ID
    private void creatData(String name, String call, String location, double price, boolean isRented) {
    Data[] tempData = new Data[houses.length + 1];
    for (int i = 0; i < houses.length; i++) {
    tempData[i] = houses[i];
    }
    tempData[tempData.length - 1] = new Data(tempData.length, name, call, location, price);
    tempData[tempData.length - 1].setRented(isRented);
    houses = tempData;
    }

    //showAllHouses:展示全部房屋 方法。由 展示全部(showAllHouseFromMenu)方法接入
    private void showAllHouses() {
    System.out.println("房屋ID\t姓名\t\t电话\t\t\t地址\t\t月租金\t出租状态");
    for (int i = 0; i < houses.length; i++) {
    if (houses[i].isActive()) {
    houses[i].showThisData();
    }
    }
    }

    //deleteHouse:注销房屋 方法。将该房屋注销。数据其实保留在 数据数组,但注销使其无法被查到。
    private void deleteHouse(int num) {
    houses[num].setActive(false);
    }


    }JAVA
  • 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
    package com.house_rent;

    class Data {
    //房屋信息。包含:ID,名字,电话,地址,房租,是否租出。
    private String id;
    private String name;
    private String call;
    private String location;
    private double price;
    private boolean isRented = false;
    private boolean active = true;

    public Data(int id, String name, String call, String location, double price) {
    //将输入的数字 ID 转化成固定 4 位的 字符串 ID
    if (id < 10) {
    this.id = "000" + id;
    } else if (id < 100) {
    this.id = "00" + id;
    } else if (id < 1000) {
    this.id = "0" + id;
    } else {
    this.id = "" + id;
    }
    this.name = name;
    this.call = call;
    this.location = location;
    this.price = price;
    }

    public boolean isActive() {
    return active;
    }

    public void setActive(boolean active) {
    this.active = active;
    }

    public void setId(String id) {
    this.id = id;
    }

    public String getId() {
    return id;
    }

    public String getName() {
    return name;
    }

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

    public String getCall() {
    return call;
    }

    public void setCall(String call) {
    this.call = call;
    }

    public String getLocation() {
    return location;
    }

    public void setLocation(String location) {
    this.location = location;
    }

    public double getPrice() {
    return price;
    }

    public void setPrice(double price) {
    this.price = price;
    }

    public boolean isRented() {
    return isRented;
    }

    public void setRented(boolean rented) {
    isRented = rented;
    }

    //展示信息
    public void showThisData() {
    System.out.println(id + "\t" + name + "\t" + call + "\t" + location + "\t" + price + "\t"
    + (isRented ? "已出租!" : "未出租。"));
    }

    }

9 面向对象编程(高级)

9.1 类变量和类方法

9.1.1 类变量

类变量:也叫 静态变量/静态属性。是该类所有对象共享的变量。任何一个该类对象访问时都是相同的值,任何一个该类对象修改时也是同一个变量。

语法(推荐):访问修饰符 static 数据类型 变量名;

或者也可以:static 访问修饰符 数据类型 变量名;

根据 JDK 版本的不同,类变量存放在 堆 中或 方法区 中。

  1. 什么时候需要用类变量:

    当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)

  2. 类变量 与 实例变量(普通属性)的区别:

    类变量 是该类所有对象共享的,而 实例变量 是每个对象独享的

  3. 加上 static 称为 类变量 或 静态变量。否则称为 实例变量/普通变量/非静态变量

  4. 静态变量 可以通过 类名.类变量名;对象名.类变量名; 来访问。但 Java 设计者推荐我们用 类名.类变量名; 来访问。(需满足访问权限和范围)

  5. 类变量 是在加载类时就初始化了。所以,没有创建对象实例也能访问。

  6. 类变量 的生命周期是随着 类的加载 开始,随着 类的消亡 而销毁。

  7. 特别地:一个 null 对象也可以访问静态变量 / 静态方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test{
    static int n = 0;
    static void met() {
    System.out.println(++n);
    }

    public static void main(String[] args){
    Test t = null;
    System.out.println(t.n); //这样不会报错
    t.met(); //这样也不会报错
    }
    }JAVA

9.1.2 类方法

当方法使用 static 修饰后,就是 静态方法。静态方法就能访问静态属性。如果我们不希望创建实例,也能调用方法,这个场合把方法做成静态方法是合适的。开发工具类时就可以如此做。

  1. 类方法和普通方法都是随着类的加载而加载,将结构信息存储在 方法区。
  2. 类方法中不允许使用和对象有关的关键字。所以,类方法没有 thissuper
  3. 类方法可以通过类名调用,也能通过对象名调用。普通方法不能通过类名调用。
  4. 类方法 中只能访问 类变量 或 类方法
  5. 普通方法既可以访问普通方法也可以访问类方法

9.2 理解 main 方法语法

1
public static void main(String[] args){...}
  1. main 方法 是 JVM 调用的方法。所以该方法的 访问权限 必须为 public

  2. JVM 在执行 main 方法时不必创建对象,所以 main方法 必须为 static

  3. 该方法接收 String 类型的数组参数。该数组中保存执行 Java 命令 时传递给所运行的类的参数。

    工作台中:javac 执行的程序.java

    java 执行的程序 参数1(arg[0]) 参数2(arg[1]) 参数3(arg[2]) ..

    IDEA中怎么做?

  4. main 方法 中,我们可以直接调用 main 方法 所在类的静态方法或静态属性。

    但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例后才能通过该实例访问非静态成员。

9.3 代码块

代码块:又称为 初始化块。属于类中的成员。类似于方法,将逻辑语句封装在方法体中,通过 { } 包围起来。

和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类 显式调用,而是加载类时,或创建对象时 隐式调用。

语法:[修饰符]{代码};

  1. 修饰符 是可选项,可不写。要写的话,只能写 static
  2. 代码块分为两类:
    • 静态代码块:有 static
    • 普通代码块:无 static
  3. 逻辑语句可以为任意的逻辑语句。
  4. ; 可以写,也可以省略。建议写上。
  5. 代码块相当于另一种形式的构造器(构造器的补充机制),可以做初始化操作
  6. 如果多个构造器中都有重复语句,就可以抽取到初始化块中,提高代码复用率。这样,不管用哪个构造器,都会执行代码块。

9.3.1 使用细节

  1. static 代码块:作用是对类进行初始化。随着 类的加载 会且只会执行一次。相对的:普通代码块每创建一个对象就执行一次。
  • **类什么时候被加载? **

    • 创建对象实例时(new)
    • 创建子类对象实例,父类也会加载
    • 使用类的静态成员时(父类也会加载)

    以上情况下类会被加载。加载后不需要再次加载,所以,静态代码块也只会执行一次。

  1. 创建一个对象时,在 一个类里 调用顺序是:

    • 调用静态代码块 和 静态属性初始化。这两者优先级相同,多个存在时按照定义的顺序依次执行。
    • 调用普通代码块 和 普通属性初始化。这两者优先级也相同。
    • 调用构造器。
  2. 构造器 的最前面其实隐含了 super();调用普通代码块。而静态相关的代码块,属性初始化,在类加载时就执行完毕了。

    这样,创建一个对象时,在 有继承关系的多个类里 调用顺序是:

    • 父类 静态代码块 和 静态初始化
    • 子类 静态代码块 和 静态初始化
    • 父类 普通代码块 和 普通初始化
    • 父类 构造器
    • 子类 普通代码块 和 普通初始化
    • 子类 构造器
  3. 静态代码块 只能调用 静态成员。普通代码块 能调用 任意成员。

9.4 单例设计模式

什么是设计模式:设计模式是在大量的实践中总结和理论化后优选的代码结构、编程风格、解决问题的思考方式。设计模式就像是经典的棋谱,免去我们自己再思考和摸索。

单例设计模式:采取一定的方法,保证再整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法

9.4.1 应用实例

后面会学更多,这里先展示两种:饿汉式、懒汉式

9.4.1.1 饿汉式

步骤如下:

  1. 构造器私有化(防止用户直接 new)

  2. 类的内部创建对象

  3. 向外暴露一个静态的公共方法

  4. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class GF{
    private String name;
    private static GF gf = new GF("萝茵");
    private GF(String name){
    this.name = name;
    }
    public static GF getGF(){
    return gf;
    }
    }JAVA

    对象,通常都是重量级的对象

    有时,我们用不到这个创建的对象,那个场合,会造成资源浪费。

9.4.1.2 懒汉式

步骤如下:

  1. 构造器私有化

  2. 定义一个静态属性对象

  3. 提供一个静态的公共方法,可以返回对象。如果静态对象为空,则创建对象

  4. 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      >class GF{
    private String name;
    private static GF gf;
    private GF(String name){
    this.name = name;
    }
    public static GF getGF(){
    if(gf == null){
    gf = new GF("萝茵");
    }
    return gf;
    }
    >}JAVA

9.4.1.3 两种方法对比

  1. 二者创建对象的时机不同。饿汉式在加载类信息时创建,懒汉式在使用时才创建
  2. 饿汉式可能造成资源浪费,懒汉式可能存在线程安全问题(学习[线程]后会进行完善)。
  3. Java SE 标准类中 java.lang.Runtime 就是一个单例模式。

9.5 final 关键字

final 可以修饰 类、属性、方法、局部变量

以下情况下,可能用到 final

  1. final 修饰类:该类不能被继承
  2. final 修饰方法:该方法不能被重写
  3. final 修饰值:该值不能被修改

9.5.1 使用细节

  1. final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名(全大写字母+下划线)

  2. final 修饰的属性在定义时,必须赋初始值,且之后不能再修改。赋值可以在下列位置之一:

    • 定义时
    • 构造器中
    • 代码块中

    注意:如果 final 修饰的属性是静态的,则只能在以下位置赋值。

    • 定义时
    • 静态代码块中
  3. final 类不能继承,但能实例化对象。对的,是可以的。

  4. 如果不是 final 类,但含有 final 方法,虽然该方法不能重写,但能被继承。

  5. final 类可以有 final 方法。可以,但没必要。

  6. final 不能修饰构造方法。

  7. finalstatic 搭配使用,效率更高(那个场合,虽然顺序不限,还是推荐 static 在前)。底层编译器做了优化处理。这样做,调用 属性(定义时赋值) 时居然 不会造成类的加载!

  8. 包装类(Integer、Double、Float、Boolean、String等)都是 final 类,都不能被继承。

9.6 抽象类

当父类的某些方法需要声明,却不知道如何实现时,可以将其声明为抽象方法。那个场合,要将该类声明为 abstract 类。

抽象类的价值更多是用于设计。设计者设计好后,让子类继承并实现。也是考官爱问的考点。

定义抽象类:访问修饰符 abstract 类名{...}

定义抽象方法(注意:无方法体):访问修饰符 abstract 返回值 方法名(形参列表);

9.6.1 使用细节

  1. 抽象类不能被实例化
  2. 抽象类不一定包含抽象方法。也就是说,抽象类可以没有 abstract方法
  3. 一旦包含 abstract 方法,则该类一定要声明为 abstract
  4. abstract 只能修饰 类 和 方法,不能修饰其他。
  5. 抽象类可以有任意成员(非抽象方法、构造器、静态属性等)。即,抽象类本质还是类。
  6. 抽象方法不能有主体。即,抽象方法不能实现。
  7. 如果一个类继承了 abstract 类,则其必须实现所有 abstract 方法,除非其自己也是 abstract 类。
  8. 抽象方法不能用 private final static 来修饰。因为,这些关键词都和 重写 相违背。

9.6.2 模板设计模式

9.7 接口

接口就是给出一些没有实现的方法,封装到一起,到某个类要用的时候,再根据具体情况把这些方法写出来。

语法:interface 接口名{...}

1
class 类名 implements 接口名{...必须实现接口的抽象方法...}

注意:JDK 7.0 以前,接口中只能是抽象方法。而 JDK 8.0 后,接口可以有静态(static)方法、默认(default)方法。

在接口中,抽象方法可以省略 abstract

接口中可以存在:

  • 属性(只有静态 static 属性,可以不加 static 关键字)
  • 方法(抽象 abstract 方法、默认 default 实现方法、静态 static 方法)

9.7.1 使用细节

  1. 接口 不能被实例化。
  2. 接口中所有方法都是 public 方法。接口中的 抽象方法 可以不用 abstract 修饰。
  3. 一个普通类实现接口,就必须把该接口所有方法都实现。(用快捷键吧 alt + enter
  4. 抽象类实现接口,可以不用实现接口的方法。
  5. 一个类可以同时实现多个接口。class Name implements In1,In2{...}
  6. 接口中的属性只能是 final 的,并且是 public static final 修饰符。修饰符就算不写,还是这样。
  7. 接口中属性的访问形式:接口名.属性名
  8. 接口不能 继承 其他的类,但可以 继承 多个别的接口。(不是也不能 实现 别的接口)
  9. 接口的修饰符只能是 public 和 默认。这点和类的修饰符相同。

9.7.2 实现接口 vs 继承类

  1. 当子类继承父类,就自动拥有父类的所有功能。如果需要扩展功能,可以通过接口方式扩展。
  2. 可以认为,接口 是对于 Java 单继承机制的补充。
  3. 继承的价值主要在于:解决代码的复用性和可维护性。
  4. 接口的价值主要在于:设计。设计好各种规范,让其他类去实现这些方法。
  5. 接口比继承更加灵活。继承需要满足 is - a 的关系,而接口只需要满足 like - a 关系。
  6. 接口在一定程度上实现代码解耦。(即:接口规范性 + 动态绑定机制)

9.7.3 接口的多态特性

  1. 多态参数(接口的引用可以指向实现了接口的类的对象)

    viod work(Inerface01 i1){...} 参数可以传入任意实现该接口的类

  2. 多态数组

  3. 接口存在多态传递现象

9.8 内部类

一个类的内部又完整的嵌套了另一个类结构。被嵌套的类被称为 内部类。

1
2
3
4
5
6
class Outer{		//外部类
class Inner{ //内部类
}
}
class Other{ //外部其他类
}JAVA

内部类的最大特点是可以直接访问私有属性,并且可以体现类与类之间的包含关系。

9.8.1 四种内部类

分别是:

  • 定义在外部类的局部位置上
    • 局部内部类:有 类名
    • 匿名内部类:无 类名
  • 定义在外部类的成员位置上
    • 成员内部类:无 static 修饰
    • 静态内部类: static 修饰的类

9.8.2 局部内部类

局部内部类:定义在外部类的局部位置上,并且有类名。(局部位置?比如:方法/代码块里)

1
2
3
4
5
6
class Outer {				//外部类
public void tools01() {
class Inner { //局部内部类
}
}
}JAVA

9.8.2.1 使用细节

  1. 定义在外部类的局部位置上,并且有类名。

  2. 可以访问外部类的所有成员,包含私有成员

  3. 局部内部类可以 直接访问 外部类的成员。

  4. 不能添加 访问修饰符,因为其地位相当于局部变量。但,可以使用 final,因为局部变量也能用 final

  5. 作用域 仅仅在定义它的方法或代码块中

  6. 外部类 在方法中,可以创建 局部内部类 的对象实例,然后调用方法。

  7. 外部其他类 不能访问 局部内部类

  8. 如果外部类和局部内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用 外部类名.this.变量名

    外部类名.this 本质就是 外部类的对象。即,调用了该方法(上例的 tools01 )的对象

    还不懂的话,看一下 这个视频 悟一悟

9.8.3 匿名内部类

匿名内部类:定义在外部类的局部位置,且没有类名

1
2
3
>new 类/接口 (参数列表) {
类体
>}JAVA

匿名内部类本质是没有名字的类,而且是内部类。同时,还是一个对象。

可以用匿名内部类简化开发

一个例子

1
2
3
4
5
6
7
8
>class Outer {							//外部类
public void tools01() {
Inter whatEver = new Inter(){ //匿名内部类
};
}
>}
>interface Inter{
>}JAVA

其实,这个匿名内部类 new Inter(){} 的运行类型就是 class XXXX implements Inter。系统自动分配的名字是 Outer$1whatEver.getClass = "Outer$1"

JDK 在创建匿名内部类 Outer$1 时,立即创建了一个对象实例,并将地址返回给了 whatEver

匿名内部类使用一次后就不能再次使用(Outer$1 就这一个了)

9.8.3.1 使用细节

  1. 匿名内部类语法比较独特。其既是一个类的定义,也是一个对象。因此,从语法上看,其既有 定义类的特征,也有 创建对象的特征。
  2. 可以访问外部类的所有成员,包括私有的。
  3. 局部内部类可以 直接访问 外部类的成员。
  4. 不能添加 访问修饰符,因为其地位相当于局部变量。但,可以使用 final,因为局部变量也能用 final
  5. 作用域:仅仅在定义它的方法或方法快中
  6. 外部其他类 不能访问 匿名内部类
  7. 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用 外部类名.this.变量名

9.8.3.2 使用场景

  1. 当作实参直接传递,简洁高效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Homework1 {
    public static void main(String[] args) {
    new Cellphone().clock(new Bell() { //看这里看这里
    @Override
    public void belling() {
    System.out.println("小懒猪起床了!");
    }
    });
    }
    }

    interface Bell {
    void ringing();
    }

    class Cellphone{
    public void clock(Bell bell){
    bell.ringing();
    }
    }JAVA

9.8.4 成员内部类

成员内部类:定义在外部类的成员位置,并且没有 static 修饰。

1
2
3
4
class Outer{
class Inner{
}
}JAVA

9.8.4.1 使用细节

  1. 可以直接访问外部类的所有成员,包括私有的
  2. 可以添加任意访问修饰符。因为,成员内部类的地位就是一个成员。
  3. 作用域 和外部类其他成员相同,为整个类体。
  4. 局部内部类可以 直接访问 外部类的成员。
  5. 外部类可以通过创建对象的方式访问成员内部类
  6. 外部其他类访问成员内部类
    • Outer.Inner name = Outer.new Inner(); 下个方法的缩写
    • Outer.Inner name = new Outer().new Inner();
    • 在外部类中编写一个方法,返回一个 Inner 的对象实例(就是对象的 getter)
  7. 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用 外部类名.this.变量名

9.8.5 静态内部类

静态内部类:定义在外部类的成员位置,经由 static 修饰。

1
2
3
4
class Outer{
static class Inner{
}
}JAVA

9.8.5.1 使用细节

  1. 可以直接访问外部类的所有 静态 成员,包括私有的。但不能访问非静态成员
  2. 可以添加访问修饰符。因为,静态内部类的地位就是一个成员。
  3. 作用域 和其他成员相同,为整个类体。
  4. 静态内部类可以 直接访问 外部类的成员。
  5. 外部类可以通过创建对象的方式访问静态内部类
  6. 外部其他类访问静态内部类
    • Outer.Inner name = new Outer.Inner(); 即通过类名直接访问
    • 在外部类中编写一个方法,返回一个 Inner 的对象实例
    • 如果外部类和匿名内部类的成员重名时,默认遵循就近原则。那个场合,访问外部类成员使用 外部类名.变量名(怎么不一样了呢?因为静态内部类访问的都是静态成员)

10 枚举和注解

10.1 枚举

把具体的对象一一列举出来的类,就称为 枚举类(enumeration)

实现方法:

  1. 自定义枚举
  2. enum 关键字枚举

10.1.1 自定义枚举

  1. 构造器私有化
  2. 去掉 set 方法(可保留 get),防止属性被修改。因为枚举对象值通常为只读
  3. 在类内部直接创建固定对象。使用 final + static 共同修饰,对象名通常全部大写。
  4. 枚举对象按照需要可以有多个属性

10.1.2 enum 关键字

1
2
3
4
5
6
7
8
9
10
11
enum Example {
RED("小红", 10), BLUE("小蓝", 11), YELLOW; //这句话要放在前面,第三个是无参
private String name;
private int age;

private Example(String name, int age) {
this.name = name;
this.age = age;
}

}JAVA
  1. 用关键字 enum 替代 class
  2. 常量名.(形参列表); 代替创建对象(放在前面)。多个对象的场合,用 , 间隔。
  3. 如果使用 enum 枚举,要求将常量对象写在前面

10.1.2.1 使用细节

  1. 当我们用 enum 关键字开发一个枚举类时,默认会继承 java.lang.Enum 类,而且是一个 final 类。

    这样,我们就能使用 Enum 中的相关方法。

    也正因为如此,enum 类不能继承其他类,亦不能被其它类继承

    不过,enum 类和其他类一样,可以实现接口。

  2. 传统的 public ststic final RED("小红", 10); 简化为 RED("小红", 10);

  3. 如果使用无参构造器创建枚举对象,则 实参列表 和 ( ) 都能省略

10.1.2.2 Enum 类中的常用方法

  • valueOf:将字符串转换成枚举对象。要求字符串必须是已有的常量名,否则报异常。
  • values:返回一个数组 Example[],其中包含定义的所有枚举对象
  • getDeclaringClass:得到枚举常量所属的 class
  • name:得到当前枚举常量的名称。建议优先用 toString
  • ordinal:输出该枚举对象的次序/编号(从 0 开始编号。如上例 BLUE.ordinal = 1
  • compareTo:比较两个枚举常量的编号(调用常量编号 减去 传入常量编号)
  • clone:枚举类不能 clone,所以,这是一个只会抛出异常的方法

10.2 注解

注解(Annotation)也被称为元数据(Metadata)。用于修饰 包、类、方法、属性、构造器、局部变量 等数据信息

和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息

在 JavaSE 中,注解的使用目的比较简单,例如 标记过时的功能,忽略警告 等。在 JavaEE 中注解占据了更重要的角色,例如用于配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等。

  1. 使用 @Annotation 时要在前面增加 @ 符号,并把该注解当成一个修饰符使用。用于修饰它支持的程序元素。

  2. 三个基本的

    1
    @Annotation

    • @Override:限定某个方法,是 重写 父类方法。该注解只能用于方法。如果你写了该注解,编译器会替你校验,看看是不是真的 重写 了父类方法。
    • @Deprecated:用于表示某个程序元素(类、方法等)已经过时
    • @SuppressWarnings():抑制编辑器警告
  3. 如果发现 public @interface XXX{} 这种东西,说明是定义了一个注解类 XXX。这里的 @interface 不代表接口(interface)。

10.2.1 @Override

  1. @Override 表示指定重写父类的方法(从编译器层面验证),如果父类没有该方法,就报错
  2. 不写该注解,重写依然构成重写
  3. @Override 只能修饰方法,不能修饰其他 类、包、属性等
  4. 查看 @Override 注解源码为 @Target(ElementType.METHOD) 说明只能修饰方法
  5. @Target 是修饰注解的注解,称为 元注解

10.2.2 @Deprecated

  1. @Deprecated 表示指定的某个程序元素(类、方法等)已过时

  2. 不推荐使用,但仍能使用

  3. 可以修饰方法、类、字段、包、参数

    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})

  4. @Deprecated 可以做到新旧版本的兼容和过度

10.2.3 @SuppressWarnings()

  1. 当我们不想看到警告信息时,用 @SuppressWarnings()

    语法:@SuppressWarnings({"..."})

    在后面 ({"unused", "unchecked"}) 这样写入你希望抑制的信息,以下是所有可以输入的关键字

    关键字 用途
    all 抑制所有警告
    boxing 抑制与装箱/解装箱操作相关的警告
    cast 抑制与强制转换操作相关的警告
    dep-ann 抑制与已弃用注释相关的警告
    deprecation 抑制与弃用有关的警告
    fallthrough 抑制与 switch 语句中丢失断点相关的警告
    finally 抑制不返回的 finally 块的相关警告
    hiding 抑制与隐藏变量的局部变量相关的警告
    incomplete-switch 抑制与 switch 语句中缺少条目相关的警告(enum)
    nls 抑制与 非nls 字符串字面值相关的警告
    null 抑制相对于null分析的警告
    rawtypes 在类参数上使用泛型时,抑制与非特定类型相关的警告
    restriction 抑制与不推荐或禁止引用有关的警告
    serial 抑制与可序列化类缺少serialVersionUID字段相关的警告
    static-access 抑制与不正确的静态访问有关的警告
    synthetic-access 抑制与未优化的内部类访问相关的警告
    unchecked 抑制与未检查的操作相关的警告
    unqualified-field-access 抑制与字段访问不合格相关的警告
    unused 抑制与未使用代码相关的警告
  2. @SuppressWarnings 的范围与你放置的位置相关。

10.2.4 JDK 的元注解(了解即可)

JDK 的 元注解 是用于修饰其他注解的注解

  1. @Rentention:指定注解的作用范围,有三种范围 SOURCE CLASS RUNTIME

    @Rentention 的三种值:

    • RententionPolicy.SOURCE:编译器使用后,直接丢弃这种策略的注释
    • RententionPolicy.CLASS:编译器把注解记录在 class 文件中。当运行 Java 程序时, JVM 不会保留注释。这是默认值
    • RententionPolicy.RUNTIME:编译器把注解记录在 class 文件中。当运行 Java 程序时,JVM 会保留注解。程序可以通过反射获取该注解
  2. @Target:指定注解的使用范围

    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})

  3. @Documented:指定该注解会不会在 Javadoc 体现

  4. @Inherited:子类会继承父类注解

11 异常(Exception)

在 Java 语言中,将程序执行中发生的不正常情况称为 “异常”(开发过程中的语法错误和逻辑错误不是异常)

这些 异常事件 可分为两类:

  • Error(错误):Java 虚拟机无法解决的严重问题。

    如:JVM 系统内部错误,资源耗尽等严重情况。Error 是严重错误,程序会崩溃。

  • Exception:其他因编程错误或偶然的外部因素导致的一般性问题,可以使用针对性的代码进行处理。

    如:空指针访问,试图读取不存在的文件,网络中断等等。

    Exception 又分为两大类:

    • 运行时异常(程序运行时,发生的异常)
    • 编译时异常(编程时,编译器检查出的异常)

11.1 异常体系图

编译异常(受检异常) 和 运行异常(非受检异常)

Java 源程序 ——(javac.exe)——> 字节码文件 ——(java.exe)——> 在内存中加载,运行类

编译异常↑ 运行异常↑

img

(异常体系图_11.1)

异常分为两大类:运行时异常 和 编译时异常

  • 运行时异常,编译器不要求强制处置的异常。一般是指编程的逻辑错误,是程序员应该避免其出现的异常。

    java.lang.RuntimeException 类及它的子类都是运行时异常

    对于运行时异常,可以不做处理。因为这类异常很普遍,若全处理会对程序的可读性和运行效率产生影响

  • 编译时异常,是编译器要求必须处置的异常

11.1.1 常见的运行时异常

常见的运行时异常(RuntimeException)包括

  • NullPointerException:空指针异常
  • ArithmeticException:数学运算异常
  • ArrayIndexOutOfBoundsException:数组下标越界异常
  • ClassCastException:类型转换异常
  • NumberFormatException:数学格式异常

空指针异常

  • 当应用程序试图在需要对象的地方使用 null 时,抛出该异常。
1
2
String str = null;
int n = str.length; //这里,出现了 空指针异常JAVA

数学运算异常

  • 当出现异常的运算条件时,抛出该异常。
1
2
double n = 100 / 0;					//这里,出现了 数学运算异常
JAVA

数组下标越界异常

  • 用非法索引(为负或超出范围)访问数组时,抛出该异常。
1
2
int[] nums = {0, 0, 0, 0};
nums[-50] = 100; //这里,出现了 数组下标越界异常JAVA

类型转换异常

  • 当试图把对象强制转换为不是实例的子类时,抛出该异常。
1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args){
A a1 = new A1();
A2 a1 = (A2)a1; //这里,出现了 类型转换异常
}
}
class A {}
class A1 extends A {}
class A2 extends A {}JAVA

数字格式不正确异常

  • 当应用程序试图将字符串转成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
1
2
String str = "ABC";
int num = Integer.parseInt(str); //这里,出现了 数字格式不正确异常JAVA

11.1.2 常见的编译异常

常见的编译异常:

  • SQLException:操作数据库时,查询表可能发生异常
  • IOException:操作文件时,发生的异常
  • FileNotFoundException:操作一个不存在的文件时,发生的异常
  • ClassNotFoundException:加载类,而该类不存在时,发生的异常
  • EOFException:操作文件,到文档末尾,发生的异常
  • IllegalArguementException:参数异常

因为还没有学习 SQL、文件编程 等,这里不举例子

11.2 异常处理

异常发生时,对异常的处理方式。如果没有显式异常处理,默认处理方式是 throws

  • try - chatch - finally:程序员在代码中捕获发生的异常,自行处理
  • throws:将发生的异常抛出,交给调用者(方法)来处理。最顶级的处理者就是 JVM

11.2.1 try - catch 异常处理

Java 提供 try 和 catch 块 来处理异常。try 块用于包含可能出错的代码,catch 块用于处理 try 块中的异常。可以根据需要在程序中有多个 try - catch 块。

基本语法

1
2
3
4
5
6
7
8
9
try {
//可疑代码
//将异常生成对应的异常对象,传递给 catch 块
} catch(Exception e) {
//如果发生异常,执行这些代码
} finally {
//无论是否异常,都执行这些代码
//finally 块可以不写
}JAVA

快捷键:选中代码后按 ctrl + alt + T

11.2.1.1 使用细节

  1. 如果异常发生了,则异常发生后面的代码块都不执行,直接进入 catch 块

  2. 如果异常未发生,则顺序执行 try 代码块,catch 块不执行

  3. 如果希望不管是否异常,都执行一些代码,则使用 finally

  4. 可以有多个 catch 捕获不同的异常。要求 子类异常在前,父类异常在后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    try {
    ...
    } catch (NullPointerException e) {
    ...
    } catch (ArithmeticException e) {
    ...
    } catch (ArrayIndexOutOfBoundsException e) {
    ...
    } catch (Exception e){
    ...
    }JAVA
  5. 可以进行 try - finally 配合使用(不写 catch)。这种用法相当于没有捕获异常,此时程序如果出错会直接退出。

    应用场景,就是写一段代码,不管是否发生异常,都必须执行某个业务逻辑。

  6. 如果没有出现异常,执行 try 中所有语句,不执行 catch 语句,最后执行 finally 语句

  7. 如果出现异常,则 try 块异常发生后,剩余语句不执行。之后执行 catch 语句,最后,执行 finally 语句。

11.2.2 throws 异常处理

如果一个方法可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显式地声明抛出异常,表明该方法将不对这些异常进行处理,而由调用者负责处理

在方法中声明 throws 语句可以声明抛出异常的列表。throws 后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

语法

1
2
3
public void metord() throws FileNontFoundException ,NullPointerException {
...
}JAVA

11.2.2.1 使用细节

  1. 对于 编译异常,程序中必须处理。
  2. 对于 运行异常,程序中诺没有处理,默认处理是 throws
  3. 子类 重写 父类方法时,子类方法抛出的异常类型必须和父类一致,或者是父类抛出异常类型的子类型。
  4. 如果有 try - catch 就不必 throws

11.3 自定义异常

当程序中出现了某些 “错误”,但该信息并未在 Throwable 子类中描述处理,这时候可以自己设计异常类,用于描述该错误信息

  1. 定义类:自定义异常类名,继承 RuntimeException 或 Exception
  2. 如果继承 Exception,属于 编译异常。
  3. 如果继承 RuntimeException,属于 运行异常。(一般来说,选这个。这样利用了默认处理机制,更方便)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>class Metords {
public void method() {
int n = 10;
if(n > 100){
throw new CustomException("不能大于100");
}
}
>}

>class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
>}JAVA

11.3.1 throwthrows

意义 位置 后面跟的东西
throws 异常处理的一种方式 方法声明时 异常类型
throw 手动生成异常对象关键字 方法体中 异常对象

11.3.2 编程小技巧

老韩曰:编写异常时,先确定正确的情况,然后取反。这样写,你的思路就不乱。

12 常用类

12.1 包装类

包装类(Wrapper):针对 八种基本数据类型 相应的 引用类型

有了类的特点,就可以调用类中的方法

基本数据类型 包装类 父类
boolean Boolean Object
char Character Object
int Integer Number
float Float Number
double Double Number
long Long Number
short Short Number
byte Byte Number
void Void Object

12.1.1 装箱和拆箱

  • 手动装箱和拆箱(JDK 5 以前)

    1
    2
    3
    4
    int n1 = 100;
    Integer integer = new Integer(n1); // 手动装箱
    Integer integer2 = Integer.valueOf(n1); // 手动装箱
    int i = integer.intValue(); // 手动拆箱JAVA
  • 自动装箱和拆箱(JDK 5 以后)

    1
    2
    3
    n2 = 200;
    Integer integer3 = n2; // 自动装箱
    int j = integer3; // 自动拆箱JAVA

    虽然可以自动装箱、拆箱,但使用 == 直接比较两个包装类时,仍然是比较其地址。以下比较通常会失败:

    1
    2
    3
    Integer ia = 1000;
    Integer ib = 1000;
    System.out.print(ia == ib); // falseJAVA

    但,Java 实现仍有可能使其成立。Byte、Boolean 以及 Short、Integer 中 [-128, 127] 间的值已被包装到固定的对象中。对他们的比较可以成功。

    1
    2
    3
    Integer ia = 127;
    Integer ib = 127;
    System.out.print(ia == ib); // trueJAVA

    由此可见,使用 == 直接比较两个包装类会带来不确定性。尽量使用 equals 方法对包装类进行比较。

装箱与拆箱是 编译器 的工作。在生成可执行的字节码文件时,编译器已经插入了必要的方法调用。

12.1.2 包装类和 String 的相互转换

  • 包装类转 String

    1
    2
    3
    4
    >Integer integer = 100;
    >String str1 = integer + ""; //方法1(自动拆箱)
    >String str2 = integer.toString(); //方法2(toString方法)
    >String str3 = String.valueOf(integer); //方法3(自动拆箱)JAVA
  • String 转包装类:

    1
    2
    3
    String str4 = "100";
    Integer integer2 = Integer.parseInt(str4); //方法1(自动装箱)
    Integer integer3 = new Integer(str4); //方法2(构造器)JAVA

12.1.3 包装类的常用方法

  • Integer.MIN_VALUE:返回最大值

  • Double.MAX_VALUE:返回最小值

  • byteValue()doubleValue()floatValue()intValue()longValue()

    按各种基本数据类型返回该对象的值

  • Character.isDigit(int):判断是不是数字

    Character.isLetter(int):判断是不是字母

    Character.isUpperCase(int):判断是不是大写字母

    Character.isLowerCase(int):判断是不是小写字母

    Characher.isWhitespace(int):判断是不是空格

  • Character.toUpperCase(int):转成大写字母

    Character.toLowerCase(int):转成小写字母

  • Integer.parseInt(string):将 String 内容转为 int

    Double.parseDouble(string)

  • Integer.toBinaryString(int):将数字转为 2 进制表示的字符串

    Integer.toHexString(int):将数字转为 16 进制表示的字符串

    Integer.toOctalString(int):将数字转为 8 进制表示的字符串

    特别地,浮点数类型的包装类只有转成 16 进制的方法。而 Short、Byte 及其他包装类无此方法

  • int Integer.bitCount(i int):统计指定数字的二进制格式中 1 的数量

12.1.4 strictfp 关键字

由于不同处理器对于浮点数寄存采取不同策略(有些处理器使用 64 位寄存 double,有些则是 80 位),对于浮点数的运算在不同平台上可能出现不同结果。

使用 strictfp 关键字标记的方法或类中,所有指令都会使用严格统一的浮点数运算。

比如,把 main 方法标记为 strictfp

1
2
3
4
public static strictfp void main(String[] args) {
double ᓚᘏᗢ = 1 / 13.97;
System.out.println(ᓚᘏᗢ);
}JAVA

12.2 String

  1. String 对象用于保存字符串,也就是一组字符序列

  2. 字符串常量对象是用双引号扩起的字符序列。例如 "你好"

  3. 字符串的字符使用 Unicode 字符编码。一个字符(不论字母汉字)占 2 字节

  4. 常用构造器:

    • String str1 = new String();

    • String str2 = new String(String original);

    • String str3 = new String(char[] a);

    • String str4 = new String(char[] a, int startIndex, int count);

      这句意思是:char[]startIndex 起的 count 个字符

  5. String 实现了接口 SerializableComparable ,可以 串行化和 比较大小

    串行化:即,可以被网络传输,也能保存到文件

  6. Stringfinal 类,不能被继承

  7. String 有属性 private final char[] value; 用于存放字符串内容。

    valuefinal 属性。其在栈中的地址不能修改,但堆中的内容可以修改。

12.2.1 String 构造方法

  • 直接指定

    1
    2
    String str1 = "哈哈哈";
    JAVA

    该方法:先从常量池看是否有 "哈哈哈" 数据空间。有的场合,指向那个空间;否则重新创建然后指向。

    这个方法,str1 指向 常量池中的地址。

  • 构造器

    1
    2
    String str2 = new String("嘿嘿嘿");
    JAVA

    该方法:先在堆中创建空间,里面维护一个 value 属性,指向 或 创建后指向 常量池的 "嘿嘿嘿" 空间。

    这个方法,str2 指向 堆中的地址

12.2.2 字符串的特性

  • 常量相加,看的是池

    1
    2
    String str1 = "aa" + "bb";				//常量相加,看的是池
    JAVA

    上例由于构造器自身优化,相当于 String str1 = "aabb";

  • 变量相加,是在堆中

    1
    2
    3
    String a = "aa";
    String b = "bb";
    String str2 = a + b; //变量相加,是在堆中JAVA

    上例的底层是如下代码

    1
    2
    3
    4
    StringBuilder sb = new StringBuilder();
    sb.append(a);
    sb.append(b);
    str2 = sb.toString(); //sb.toString():return new String(value, 0, count);JAVA

12.2.3 String 的常用方法

以下方法不需死记硬背,手熟自然牢记

  • boolean equals(String s):区分大小写,判断内容是否相等

    boolean equalsIgnoreCase(String s):判断内容是否相等(忽略大小写)

  • boolean empty():返回是否为空

  • int charAt(int index):获取某索引处的字符(代码单元)。

    必须用 char c = str.charAt(15);,不能用 char c = str[15];

    int codePointAt(int index)

    int length():获取字符(代码单元)的个数

    —— 代码单元,见 [[2.6.2.4 字符本质与编码表]](https://i-melody.github.io/2021/11/22/Java/入门阶段/2 变量/#2-6-2-字符本质与编码表)

    IntStream codePoints():返回字符串中全部码点构成的流

    long codePoints().count():返回真正长度(码点数量)

    —— 流,见 [[27.4 Stream API]](https://i-melody.github.io/2022/03/09/Java/入门阶段/27 Java 8 新特性/#27-4-Stream-API)

  • int indexOf(String str):获取字符(串)在字符串中第一次出现的索引。如果找不到,返回 -1

    int indexOf(int char) 参数也可以传入一个 int。由于自动类型转换的存在,也能填入 char

    int indexOf(String str, int index):从 index 处(包含)开始查找指定字符(串)

    int lastIndexOf(String str):获取字符在字符串中最后一次出现的索引。如果找不到,返回 -1

  • String substring(int start, int end):返回截取指定范围 [start, end) 的 字符串

    String substring(int index):截取 index(包含)之后的部分

  • String trim():返回去前后空格的新字符串

  • String toUperCase():返回字母全部转为大写的新字符串

    String toLowerCase():返回字母全部转为小写的新字符串

  • String concat(String another):返回拼接字符串

  • String replace(char oldChar, char newChar):替换字符串中的元素

    1
    2
    String str1 = "Foolish cultists";
    String str2 = str1.replace("cultists", "believers"); //str1不变,str2为改变的值JAVA
  • String[] split(String regex):分割字符串。

    对于某些分割字符,我们需要转义

    1
    2
    3
    4
    String str1 = "aaa,bbb,ccc";
    String[] strs1 = str1.split(","); //这个场合,strs = {"aaa", "bbb", "ccc"};4
    String str2 = "aaa\bbb\ccc";
    String[] strs2 = str2.split("\\"); //"\" 是特殊字符,需要转义为 "\\"JAVA
  • int compareTo(String another):按照字典顺序比较两个字符串(的大小)。

    返回出现第一处不同的字符的编号差。前面字符相同,长度不同的场合,返回那个长度差。

    1
    2
    3
    4
    5
    6
    String str1 = "ccc";
    String str2 = "ca";
    String str3 = "ccc111abc";
    int n1 = str1.compareTo(str2); //此时 n1 = 'c' - 'a' = 2
    int n2 = str1.compareTo(str3); //此时 n2 = str1,length - str3.length = -6
    int n3 = str1.compareTo(str1); //此时 n3 = 0JAVA
  • char[] toCharArray():转换成字符数组

    byte[] getBytes():字符串转为字节数组

  • String String.format(String format, Object... args):(静态方法)格式字符串

    1
    2
    3
    4
    5
    6
    7
    String name = "Roin";
    String age = "1M";
    String state = "computer";
    String formatStr = "I am %s, I am %s old, I am a %s";
    String str = String.format(formatStr, name, age, state);
    //其中 %s 是占位符。此时,str = "I am Roin, I am 1M old, I am a computer";
    //%s 表示字符串替换;%d 表示整数替换;#.2f 表示小数(四舍五入保留2位)替换;%c 表示字符替换JAVA
  • String join(deli, ele...):拼接字符串(ele...),以 deli 间隔。

  • boolean startsWith(str):测试 str 是否为当前字符串的前缀

  • String repeat(int n):返回该字符串重复 n 次的结果

12.3 StringBuffer

java.lang.StringBuffer 代表可变的字符序列。可以对字符串内容进行增删。

很多方法和 String 相同,但 StringBuffer 是可变长度。同时,StringBuffer 是一个容器

  1. StringBuffer 的直接父类是 AbstractStringBuffer
  2. StringBuffer 实现了 Serialiazable,可以串行化
  3. 在父类中,AbstractStringBuffer 有属性 char[] value 不是 final
  4. StringBuffer 是一个 final 类,不能被继承

String 对比 StringBuffer

  • String 保存字符串常量,其中的值不能更改。每次更新实际上是更改地址,效率较低
  • StringBuffer 保存字符串变量,里面的值可以更改。每次更新是更新内容,不用每次更新地址。

12.3.1 StringBuffer 构造方法

  1. 无参构造

    1
    2
    StringBuffer strb1 = new StringBuffer();
    JAVA

    创造一个 16 位容量的空 StringBuffer

  2. 传入字符串构造

    1
    2
    String str1 = "abcabc";
    StringBuffer strb2 = new StringBuffer(str1);JAVA

    (上例)创造一个 str1.length + 16 容量的 StringBuffer

  3. 指定容量构造

    1
    2
    StringBuffer strb3 = new StringBuffer(3);
    JAVA

    (上例)创造一个 3 容量的空 StringBuffer

12.3.2 StringStringBuffer的转换

  1. StringBuffer

    1
    2
    3
    4
    String str1 = "abcabc";
    StringBuffer strb1 = new StringBuffer(str1); //方法1(构造器)
    StringBuffer strb1 = new StringBuffer();
    strb1 = strb1.append(str1); //方法2(先空再append)JAVA
  2. String

    1
    2
    String str2 = strb1.toString();					//方法1(toString)
    String str3 = new String(strb1); //方法2(构造器)JAVA

12.3.3 StringBuffer 的常用方法

  • append(char c):增加

    append(String s) 参数也能是字符串

    特别的,append(null); 的场合,等同于 append("null");

  • delete(start, end):删减 [start, end) 的内容

  • replace(start, end, string):将 start 与 end 间的内容替换为 string

  • indexOf:查找指定字符串第一次出现时的索引。没找到的场合返回 -1

  • insert:在指定索引位置之前插入指定字符串

  • length():返回字符长度

    capacity():返回当前的容量

    String 类对象分配内存时,按照对象中所含字符个数等量分配。

    StringBuffer 类对象分配内存时,除去字符所占空间外,会另加 16 字符大小的缓冲区。

    对于 length() 方法,返回的是字符串长度。对于 capacity() 方法,返回的是 字符串 + 缓冲区 的大小。

12.4 StringBuilder

一个可变的字符序列。此类提供一个与 StringBuffer 兼容的 API,但不保证同步(有线程安全问题)。该类被设计成 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先使用该类。因为在大多数实现中,它比起 StringBuffer 要快。

StringBuilder 是的主要操作是 appendinsert 方法。可以重载这些方法,以接受任意类型的数据。

  1. StringBuilder 也继承了 AbstractStringBuffer
  2. StringBuilder 也实现了 Serialiazable,可以串行化
  3. 仍然是在父类中有属性 char[] value ,而且不是 final
  4. StringBuilder 也是一个 final 类,不能被继承
  5. StringBuilder 的方法,没有做互斥的处理(没有 synchronize),故而存在线程安全问题

12.4.1 StringStringBufferStringBuilder 的对比

  1. StringBuilderStringBuffer 类似,均代表可变字符序列,而且方法也一样

  2. String:不可变字符序列,效率低,但复用率高

  3. StringBuffer:可变字符序列,效率较高,线程安全

  4. StringBuilder:可变字符序列,效率最高,存在线程安全问题

  5. String 为何效率低:

    1
    2
    3
    4
    5
    String str1 = "aa";					//创建了一个字符串
    for(int n = 0; n < 100; n++){
    str1 += "bb"; //这里,原先的字符串被丢弃,创建新字符串
    } //多次执行后,大量副本字符串留在内存中
    //导致效率降低,也会影响程序性能JAVA

    如上,对 String 大量修改的场合,不要使用 String

12.5 Math

  • Math.multiplyExact(int n1, int n2):进行乘法运算,返回运算结果

    通常的乘法 n1 * n2 在结果大于那个数据类型存储上限时,可能返回错误的值。

    使用此方法,结果大于那个数据类型存储上限时,会抛出异常

    Math.addExact(int n1, int n2):加法

    Math.subtractExact(int n1, int n2):减法

    Math.incrementExact(int n1):自增

    Math.decrementExact(int n1):自减

    Math.negateExact(int n1, int n2):改变符号

  • Math.abs(n):求绝对值,返回 |n1|

  • Math.pow(n, i):求幂,返回 n3 ^ i

  • Math.ceil(n):向上取整,返回 >= n3 的最小整数(转成double)

  • Math.floor(n):向下取整,返回 <=n4 的最小整数(转成double)

  • Math.floorMod(int n1, int n2):返回 n1 除以 n2 的余数

    n1 % n2 的场合,返回的可能是负数,而不是数学意义上的余数

  • Math.round(n):四舍五入,相当于 Math.floor(n5 + 0.5)

  • Math.sqrt(n):求开方。负数的场合,返回 NaN

  • Math.random():返回一个 [0, 1) 区间的随机小数

  • Math.sin(n):正弦函数

    Math.cos(n):余弦函数

    Math.tan(n)Math.atan(n)Math.atan2(n)

    要注意,上述方法传入的参数是 弧度值

    要得到一个角度的弧度值,应使用:Math.toRadians(n)

  • Math.exp(n):e 的 n 次幂

    Math.log10(n):10 为底的对数

    Math.log():自然对数

  • Math.PI:圆周率的近似值

    Math.E:e 的近似值

12.6 Arrays

  • Arrays.toString():返回数组的字符串形式

    1
    2
    int[] nums = {0, 1, 33};
    String str = Array.toString(nums); //此时,str = "[0, 1, 33]"JAVA

    特别的,输入为 null 时返回 “null”

  • Arrays.sort(arr):排序

    因为数组是引用类型,使用 sort 排序后,会直接影响到实参。

    默认(自然排序)从小到大排序。

    Arrays.sort(arr, Comparator c):按照传入的比较器决定排序方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Integer[] nums;
    ...
    Comparator<Integer, Integer> c = new Comparator<Integer, Integer>(){
    @Override
    public int compare(Integer o1, Integer o2){
    return n2 - n1; // 这个场合,变成从大到小排序
    }
    }
    Arrays.sort(nums, c);JAVA
  • Arrays.binarySearch(array, num):通过二分搜索法查找。前提是必须先排序。

    找不到的场合,返回 - (low + 1)。即,其应该在的位置的负值

    1
    2
    3
    Integer[] nums2 = {-10, -5, -2, 0, 4, 5, 9};
    int index = Arrays.binarySearch(nums2, 7); // 此时 index = -7
    // 如果 7 存在,应该在第 7 个位置JAVA
  • Arrays.copyOf(arr, n):从 arr 中,复制 n 个元素(成为新的数组)。

    n > arr.length 的场合,在多余的位置添加 null。n < 0 的场合,抛出异常。

    该方法的底层使用的是 System.arraycopy

  • Arrays.fill(arr, o):用 o 填充 num 的所有元素。

  • Arrays.equals(arr1, arr2):比较两个数组元素是否完全一致(true/false

  • Arrays.asList(a, b, c, d):将输入数据转成一个 List 集合

12.7 System

  • System.exit(0):退出当前程序。0 表示一个状态,正常状态是 0

  • System.arraycopy(arr, 0, newArr, 0 ,3):复制数组元素。

    上例是:arr 自下标 0 起开始,向 newArr 自下标 0 开始,依次拷贝 3 个值

    这个方法比较适合底层调用。我们一般使用 Arrays.copyOf 来做

  • System.currentTimeMillis:返回当前时间距离 1970 - 1 - 1 的毫秒数

  • System.gc:运行垃圾回收机制

12.8 BigIntegerBigDecimal

BigInteger:适合保存更大的整数

BigDecimal:适合保存精度更大的浮点数

1
2
//用引号把大数变成字符串
BigInteger bigNum = new BigInteger("100000000000000000000000");JAVA

构造方法:

  • new BigInteger(String intStr):通过一个字符串构建大数
  • BigInteger BigInteger.valueOf(1):通过静态方法,让整数类型转成大数

另外,在对 BigIntegerBigDecimal 进行加减乘除的时候,需要使用对应方法

不能直接用 + - * /

常用方法:

  • BigInteger add(BigInteger):加法运算。返回新的大数

  • BigInteger subtract(BigInteger):减法

  • BigInteger multiply(BigInteger):乘法

  • BigInteger divide(BigInteger):除法运算

    该方法可能抛出异常。因为可能产生是无限长度小数。

    解决方法(保留分子精度):bigDecimal.divide(bD3, BigDecimal.ROUND_CELLING)

  • 一些常量:

    BigInteger.ONEBigInteger.ZEROBigInteger.TEN 分别是 1、0、10

    one 就是英文的 1,zero 就是英文的 0……这个大家都懂的吧?

12.9 日期类

12.9.1 第一代日期类

Date:精确到毫秒,代表特定瞬间。这里的是 java.util.Date

SimpleDateFormat:格式和解析日期的类

  1. Date d1 = new Date();:调用默认无参构造器,获取当前系统时间。

    默认输出日期格式是国外的格式,因此通常需要进行格式转换

    1
    2
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH.mm.ss");
    String dateFormated = sdf.(d1); //日期转成指定格式。JAVA
  2. 通过指定毫秒数得到时间:

    1
    2
    Date d2 = new Date(10000000000);
    JAVA
  3. 把一个格式化的字符串转成对应的 Date:

    1
    2
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 HH点mm分 E");
    Date d3 = sdf2.parse("2021年12月22日 00点03分 星期三");JAVA

    这个场合,给定的字符串格式应和 sdf2 格式相同,否则会抛出异常

12.9.2 第二代日期类

Calendar:构造器是私有的,要通过 getInstance 方法获取实例

  1. Calendar 是一个抽象类,其构造器私有

    1
    2
    Calendar c1 = Calendar.genInstance();				//获取实例的方法
    JAVA
  2. 提供大量方法和字段提供给程序员使用

    • c1.get(Calendar.YEAR):获取年份数

    • c1.get(Calendar.MONTH):获取月份数

      特别的,实际月份是 返回值 +1。因为 Calendar 的月份是从 0 开始编号的

    • c1.get(Calendar.DAY_OF_MONTH):获取日数

    • c1.get(Calendar.HOUR):获取小时数(12小时制)

      c1.get(Calendar.HOUR_OF_DATE):获取小时数(24小时制)

    • c1.get(Calendar.MINUTE):获取分钟数

    • c1.get(Calendar.SECOND):获取秒数

    Calendar 没有专门的格式化方法,需要程序员自己组合来显示

12.9.3 第三代日期类

JDK 1.0 加入的 Date 在 JDK 1.1 加入 Calendar 后已被弃用

然而,Calendar 也存在不足:

  1. 可变性:像日期和实际这样的类应该是不可改变的
  2. 偏移性:年份都是从 1900 年开始,月份都是从 0 开始
  3. 格式化:只对 Date 有用,对 Calendar 没用
  4. 其他问题:如不能保证线程安全,不能处理闰秒(每隔 2 天多 1 秒)等

于是,在 JDK 8 加入了以下新日期类:

  • LocalDate:只包含 日期(年月日),可以获取 日期字段
  • LocalTime:只包含 时间(时分秒),可以获取 时间字段
  • LocalDateTime:包含 日期 + 时间,可以获取 日期 + 时间字段
  • DateTimeFormatter:格式化日期
  • Instant:时间戳
  1. 使用 now() 方法返回当前时间的对象

    1
    2
    LocalDateTime ldt = LocalDateTime.now();				//获取当前时间
    JAVA
  2. 获取各字段方法:

    • ldt.getYear();:获取年份数

    • ldt.getMonth();:获取月份数(英文)

      ldt.getMonthValue();:获取月份数(数字)

    • ldt.getDayOfMonth();:获取日数

    • LocalDateTime ldt2 = ldt.plusDays(100);:获取 ldt 时间 100 天后的时间实例

    • LocalDateTime ldt3 = ldt.minusHours(100);:获取 ldt 时间 100 小时前的时间实例

  3. 格式化日期:

    1
    2
    DateTimeFormatter dtf = new DateTimeFormatter("yyyy.MM.dd HH.mm.ss");
    String date = dtf.format(ldt); //获取格式化字符串JAVA
  4. InstantDate 类似

    • 获取当前时间戳:Instant instant = Instant.now();
    • 转换为 DateDate date = Date.form(instant);
    • Date 转换:Instant instant = date.toInstant;

12.10 泛型

泛型(generic):又称 参数化类型。是JDK 5 出现的新特性。解决数据类型的安全性问题。

在类声明或实例化时只要制定好需要的具体类型即可。

举例说明:

1
2
Properties<Person> prop = new Properties<Person>();
JAVA

上例表示存放到 prop 中的必须是 Person 类型。

如果编译器发现添加类型不符合要求,即报错。

遍历时,直接取出 Person 而非 Object

  1. 编译时,检查添加元素的类型。可以保证如果编译时没发出警告,运行就不会产生 ClassCastException 异常。提高了安全性,使代码更加简洁、健壮。

  2. 也减少了转换的次数,提高了效率。

  3. 泛型的作用是:可以在类声明是通过一个标识表示类中某个属性的类型,或某个方法返回值的类型,或参数类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class P<E> {
    E e; //E 表示 e 的数据类型,在定义 P类 时指定。在编译期间即确认类型
    public P(E e){ //可作为参数类型
    this.e = e;
    }
    public E doSth(){ //可作为返回类型
    return this.e;
    }
    }JAVA

    实例化时指定 E 的类型,编译时上例所有 E 会被编译器替换为那个指定类型

使用方法:

  • 声明泛型:

    1
    2
    interface InterfaceName<T> {...}
    class ClassName<A, B, C, D> {...}JAVA

    上例 T、A、B、C、D 不是值,而是类型。可以用任意字母代替

  • 实例化泛型:

    1
    2
    List<String> strList = new ArrayList<String>();
    Iterator<Integer> iterator = vector.interator<Integer>();JAVA

    类名后面指定类型参数的值

注意细节:

  1. 泛型只能是引用类型

  2. 指定泛型具体类型后,可以传入该类型或其子类类型

  3. 在实际开发中往往简写泛型

    1
    2
    List<String> strList = new ArrayList<>();
    JAVA

    编译器会进行类型推断,右边 < > 内容可以省略

  4. 实例化不写泛型的场合,相当于默认泛型为 Object

自定义泛型类 · 接口:

1
2
class Name<A, B...> {...}				//泛型标识符 可有多个,一般是单个大写字母表示
JAVA

这就是自定义泛型啊

  1. 普通成员可以使用泛型(属性、方法)

  2. 泛型类的类型,是在创建对象时确定的。

    因此:静态方法中不能使用类的泛型;使用泛型的数组,也不能初始化。

  3. 创建对象时不指定的场合,默认 Object。建议还是写上 <Object>,大气,上档次

  4. 自定义泛型接口

    1
    2
    interface Name<T, R...> {...}
    JAVA

    泛型接口,其泛型在 继承接口 或 实现接口 时确定。

自定义泛型方法:

1
2
修饰符 <T, R...> 返回类型 方法名(形参) {...}
JAVA
  1. 可以定义在普通类中,也可以定义在泛型类中

  2. 当泛型方法被调用时,类型会确定

  3. 以下场合

    1
    2
    3
    4
    Class C<T> {
    public void cMethord(T t){
    }
    }JAVA

    没有 < >,不是泛型方法,而是使用了泛型的普通方法

泛型继承:

  1. 泛型不具有继承性
  2. <?>:支持任意泛型类型
  3. <? extends A>:支持 A 及 A的子类,规定了泛型的上限
  4. <? super B>:支持 B 及 B 的父类,规定了泛型的下限