Java基础


关于Java基础的杂记

第一章 序

万丈高楼平地起。

第二章 概述

java 转义字符

\\t :一个制表位,实现对齐功能
\\n :换行符
\\\ :一个真实的斜杠
\\” : 一个真实的双引号
\\’ : 一个真实的单引号
\\r :一个回车,没有换行,将光标置于最前,逐个输出\r 后的字符

注释

  • 单行注释

    1
    //需要注释的语句
  • 多行注释

    1
    2
    3
    /*
    这里是多行注释
    */
  • Javadoc 注释

    1
    2
    3
    4
    /**
    * @author lael
    * @version 1.0
    */
  • 细节

    • 注释语句不会执行。

代码规范

  1. 类、方法的注释,要以 javadoc 的方式来写。
  2. 非 javadoc 注释着重告诉维护者如何修改、为什么这样写,以及注意事项。
  3. 运算符和 = 两边加一个空格。
  4. 实际工作使用 UTF-8 编码格式。
  5. 行宽不超过 80 字符。
  6. 代码编写使用行尾风格或次行风格。
  7. 一段代码一个模块,尽量只写一个功能,避免混乱。

JDK、JRE、JVM 之间关系

  1. JDK = JRE + JAVA 开发工具
  2. JRE = JVM + 核心类库

第三章 变量

变量注意事项

  1. int 4 个字节、double 8 个字节,每个类型占用空间不同。
  2. 变量必须先声明,后使用。
  3. 变量在同一个作用域中不可重名。
  4. 变量 = 变量名 + 值 + 数据类型 (三要素)。

+ 号

  • 当 + 左右两边有一方为字符串,则做拼接运算。
  • 两边均为数值类型,则做加法运算。
  • 运算顺序为从左到右。

数据类型

  • 数值型
    • 整数:byte[1]、short[2]、int[4]、long[8]
    • 浮点数(小数):float[4]、double[8]
  • 字符型
    • char[2]:存放单个字符,如 ‘a’
  • 布尔型
    • boolean[1]:存放 true/false

整型使用细节

  1. java 整型常量默认为 int 类型,声明 long 型常量须后加 ‘l’ 或 ‘L’

    • bit:计算机中最小存储单位

    • byte:计算机中基本存储单位

    • 1 byte = 8 bit

浮点型使用细节

  1. 默认为 double 类型,声明 float 型单精度常量须后加 ‘f’ 或 ‘F’
  2. 表示形式:
    • 十进制
    • 科学计数法,如:5.12e10 = 5.12 x 1010
  3. 通常情况下应该使用 double,因为它比 float 更精确,而 float 会损失一些小数位

字符型使用细节

  1. 字符常量用单引号括起单个字符
  2. 允许使用转义字符
  3. char 的本质是一个整数,所以可以直接给 char 赋值整数,输出时是对应 unicode 对应字符
  4. char 类型可以进行运算

基本数据类型的转换

  • 精度小的类型自动转换为精度大的数据类型

    char int long float double

    byte short int long float double

  • 有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的数据类型,再进行计算

  • 精度大的赋值给精度小的数据类型就会报错

  • (byte, short) 和 char 之间不会相互自动转换

  • byte, short, char 三者之间可以计算,在计算时首先转换为 int 类型

  • boolean 不参与类型自动转换

强制转换细节

  1. 需要数据从大到小时,就需要使用强制转换

字符串转换基本类型

1
2
3
4
5
6
7
8
int num1 = Integer.parseInt(s5);
double num2 = Double.parseDouble(s5);
float num3 = Float.parseFloat(s5);
long num4 = Long.parseLong(s5);
byte num5 = Byte.parseByte(s5);
boolean b = Boolean.parseBoolean("true");
short num6 = Short.parseShort(s5);
char num7 = s5.charAt(0); //获取字符串的第一个字符
  1. 转换错误类型,会抛出异常,程序终止

第四章 运算符

% 取余,取模

1
2
3
4
5
6
// % 取模,取余
// 本质公式:a % b = a - a / b * b
System.out.println(10 % 3); //1
System.out.println(-10 % 3); //-1
System.out.println(10 % -3); //1
System.out.println(-10 % -3); //-1

++ 自增 / — 自减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ++ 自增
int i = 10;
i++; //自增 等价于 i = i + 1 => i = 11
++i; //自增 等价于 i = i + 1 => i = 12
System.out.println("i=" + i);

/*
作为表达式使用
前++ :++i先自增后赋值
后++ :i++先赋值后自增
*/
int j = 8;
// int k = ++j; //等价于 j = j + 1; k = j;
int k = j++; //等价于 k = j; j = j + 1;
System.out.println("k=" + k + "\nj=" + j);

逻辑运算符

逻辑与 &

对于逻辑与,第一个条件为 false,后面条件仍会判断执行

短路与 &&

对于短路与,第一个条件为 false,后面条件不再判断执行

逻辑或 |

对于逻辑或,第一个条件为 true,后面条件仍会判断执行

逻辑或 ||

对于短路或,第一个条件为 true,后面条件不再判断执行

异或操作 ^

a ^ b,如果 a 与 b 结果不同,则为 true,否则为 false

取反操作 ~

对参数二进制格式进行 01 取反

三元运算符

条件表达式 ? 表达式1 : 表达式2

  1. 如果条件表达式为 true,运算后的结果是表达式 1
  2. 如果条件表达式为 false,运算后的结果是表达式 2
1
2
3
4
int a = 10;
int b = 99;
int result = a > b ? a++ : b--;
System.out.println("result: " + result + "\nb: " + b);

原码、反码、补码

重要:负数的计算,需要先从原码转换为反码,反码再转换为补码再进行位运算,运算完毕后,再依次转回反码、原码。

  1. 二进制的最高位是符号位:0 表示正数,1 表示负数。
  2. 正数源码、反码、补码都一样。
  3. 负数的 反码 = 它的原码符号位不变,其他位取反。
  4. 负数的 补码 = 反码 + 1,反码 = 补码 -1。
  5. 0 的反码,补码都是 0。
  6. java 没有无符号数。
  7. 计算机运算都以补码方式运算。
  8. 看运算结果的时候,要看它的原码。

位运算符

按位与 &

两者全为 1,结果为 1,否则为 0。

1
2
3
4
5
6
7
8
//1. 先得到 2 的补码 => 2的原码 00000000 00000000 0000000 0000010
//2. 3的补码 3的原码 00000000 00000000 0000000 0000011
//3. 按位与 &
// 00000000 00000000 0000000 0000010
// 00000000 00000000 0000000 0000011
//4. 得到是运算后的补码,因为是正数,三码合一,也是原码
// 00000000 00000000 0000000 0000010 => 2
System.out.println(2&3);//2

按位或 |

两位有一个为 1,结果为 1,否则为 0

1
2
3
4
//1. 先得到 2 的补码 = 2的原码 00000000 00000000 0000000 0000010
//2. 3的补码 = 3的原码 00000000 00000000 0000000 0000011
//3. 按位或 | 得到 00000000 00000000 0000000 0000011 => 3
System.out.println(2|3);//3

按位异或 ^

两位有一个为 0,一个 1,结果为 1,否则为 0

1
2
3
4
//1. 先得到 2 的补码 = 2的原码 00000000 00000000 0000000 0000010
//2. 3的补码 = 3的原码 00000000 00000000 0000000 0000011
//3. 按位异或 ^ 得到 00000000 00000000 0000000 0000001 => 1
System.out.println(2^3);//1

按位取反 ~

0 -> 1,1 -> 0

1
2
3
4
5
6
7
8
9
10
11
12
//1. 先得到 -2 的原码 10000000 00000000 00000000 00000010
//2. -2 的反码 11111111 11111111 11111111 11111101
//3. -2 的补码 11111111 11111111 11111111 11111110
//4. ~-2 操作 00000000 00000000 00000000 00000001 运算后的补码
//5. 运算后的原码就是 00000000 00000000 00000000 00000001 => 1
System.out.println(~-2);//1

//1. 得到2的补码 0000000 00000000 00000000 00000010 正数原码、补码、反码三合一
//2. ~2 操作 11111111 11111111 11111111 11111101 运算后的补码(负数)
//3. 运算后的反码 11111111 11111111 11111111 11111100 反码 = 补码 - 1(负数)
//4. 运算后的原码 10000000 00000000 00000000 00000011 => -3
System.out.println(~2); //-3

算数左移运算符 <<

符号位不变,按二进制位数左移指定位数,低位补 0。

算数右移运算符 >>

低位溢出,符号位不变,并用符号位补溢出的高位。

1
2
3
4
5
6
7
//1. 先得到 -1 的原码 10000000 00000000 0000000 00000001
//2. 得到反码 11111111 11111111 11111111 11111110
//3. 转换为补码 11111111 11111111 11111111 11111111
//4. >>2操作 11111111 11111111 11111111 11111111
//5. 转换为反码 11111111 11111111 11111111 11111110
//6. 转换为原码 操作 10000000 00000000 00000000 00000001 => -1
int b = -1 >> 2; //-1

逻辑右移操作符 >>>

低位溢出,高位补 0。

标识符的命名规则和规范

^cd9c26

标识符概念

  1. Java 中对各种变量、方法和类等命名时使用的字符序列称为标识符
  2. 凡是自己起名字的地方都称为标识符

命名规则

  1. 由 26 个英文字母大小写,0-9,或$组成
  2. 不可以使用数字开头
  3. 不可以使用关键字和保留字,但可以包含
  4. 严格区分大小写,长度无限制
  5. 标识符不能包含空格

包名

多单词组成时所有字母小写

例:com.hahah.cpm

类名 、接口名

多单词组成时,所有单词首字母大写 [大驼峰]

例:TankShotGame

变量名、方法名

多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写 [小驼峰]

例:tankShotGame

常量名

所有字母都大写,多个单词时用下划线连接

例:定义一个所得税税率 TAX_RATE

进制

二进制

0,1,满 2 进 1,以 0b0B 开头

转八进制

从低位(右)开始,将二进制数每三位一组,转成对应八进制数。

例:0b11010101 转为八进制

0b11(3)010(2)101(5) = 325

十进制

0-9,满 10 进 1

转二进制

将十进制数不断除以 2,直到商为 0,再将每步得到的余数倒置,就是对应的二进制

例:十进制 34 转为二进制

0b100010

转八进制

将十进制数不断除以 8,直到商为 0,再将每步得到的余数倒置,就是对应的八进制

例:十进制 131 转为八进制

0203

转十六进制

将十进制数不断除以 16,直到商为 0,再将每步得到的余数倒置,就是对应的十六进制

例:十进制 237 转为十六进制

0xED

八进制

0-7,满 8 进 1,以数字0开头

转二进制

将八进制数每 1 位转换为对应 3 位二进制

例:八进制 0237 转为二进制

0b010011111

十六进制

0-9以及A(10)-F(15)

满 16 进 1,以0x0X开头

A-F不区分大小写

转二进制

将十六进制数每 1 位转换为对应 4 位二进制

例:十六进制 0x23B 转为二进制

0b001000111011

第五章 控制结构

顺序控制

从上到下逐行执行,中间没有任何判断和跳转,遵循向前引用原则。

分支控制

if else

如果表达式为 true,则执行语句块 1,如果表达式为 false,则执行语句块 2。

双分支:

1
2
3
4
5
if (表达式) {
语句块1;
} else {
语句块2;
}

多分支:

1
2
3
4
5
6
7
8
9
10
if(表达式1) {
语句块1;
} else if(表达式2) {
语句块2;
...
} else if(表达式n) {
语句块n;
} else {
语句块n+1;
}

如果表达式 1 为 true,则执行语句块 1,如果表达式 1 为 false,则继续判断表达式 2,为 true 则执行语句块 2,以此类推。

特别说明:

  1. 多分支可以没有 else。
  2. 如果所有表达式都不成立,则默认执行 else 代码块。

嵌套分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(表达式1) {
if(表达式2) {
语句块1;
} else {
语句块2;
}
} else {
if(表达式3) {
语句块3;
} else if(表达式4) {
语句块4;
} else {
if(表达式n) {
语句块n;
} else {
语句块n+1;
}
}
}

实际开发中,嵌套不要超过三层,可读性不好。

switch

表达式对应一个值,当表达式的值等于值 1,就执行语句块 1,并break跳出 swtich,不等于值 1 便逐次与值 2、值 3……进行匹配。
如果表达式和值一个都没有匹配上,就默认执行 default 语句块。

如果不加break,则发生穿透,执行完该值的语句块并继续进行下一个值的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch(表达式) {
case1:
语句块1;
break;
case2:
语句块2;
break;

case 值n:
语句块n;
break;
default:
语句块n+1;
break;
}

switch 细节讨论:

  1. 表达式数据类型应与case后的常量类型一致,或者是能够自动转换或比较的类型。如charint类型可以相互转换。
  2. switch 中的表达式的返回值必须是:byte, short, int, char, enum[枚举], String 中的一种。
  3. case子句中的值必须是常量(1, ‘a’)或者是常量表达式,不能是变量。
  4. default的子句是可选的,当没有匹配的case时,执行default
  5. break用于退出 switch,如果没有,程序会进行穿透,执行到 switch 结尾。
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
//3. 季节判断,使用穿透。
System.out.println("输入月份:");
int season = myScanner.nextInt();
switch (season) {
case 3:
case 4:
case 5:
System.out.println("春季。");
break;
case 6:
case 7:
case 8:
System.out.println("夏季。");
break;
case 9:
case 10:
case 11:
System.out.println("秋季。");
break;
case 1:
case 2:
case 12:
System.out.println("冬季。");
break;
default:
System.out.println("月份错误!");
}

switch 与 if 的比较

  1. 如果判断具体数值不多,并且符合 byte, short, int, char, enum[枚举], String 这六种类型,建议使用 switch 语句。
  2. 对于区间判断、结果为 boolean 类型的判断,使用 if 的范围更广。

循环控制

for

1
2
3
for(循环变量初始化; 循环条件; 循环变量迭代) {
语句块; //只有一句语句可以省略 {}。
}

for 循环细节:

  1. 循环条件应返回一个布尔值的表达式。

  2. 初始化变量和变量迭代可以写到其他地方,分号不能省略。

1
2
3
4
5
int i = 1; //循环变量初始化
for (; i <= 10;) {
System.out.println("你好" + i);
i++; //循环变量迭代
}
  1. 循环变量初始值可以有多条初始化语句,但是要求类型一致,循环遍历迭代同理。

while

先判断,再循环。

1
2
3
while(条件表达式) {
语句块;
}

do while

先执行,再判断,一定会执行一次。

1
2
3
do {
语句块;
}while(条件表达式);

循环嵌套

将一个循环放在另一个循环体内,内层循环作为外层循环的执行代码。

九九乘法表:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
System.out.println("乘法口诀表:");
// 外层循环
for (int i = 1; i <= 9; i++) {
// 内层循环
for (int j = 1; j <= i; j++) {
System.out.print(j + "*" + i + "=" + j * i + "\t");
}
System.out.println();
}
}

空心金字塔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//分析思路
//化繁为简:
//1. 先打印一个矩形
//2. 打印半个金字塔
//3. 打印整个金字塔 (一个循环中嵌套两个循环。先算星星,再算空格)
//4. 打印空心金字塔 (第一个位置是*,最后一个位置也是*,最后一层全部是*)
//5. 先死后活: 层数作为变量

int totalLevel = 54; //层数

for (int i = 1; i <= totalLevel; i++) { //i为层数
//再打印星星之前需要打印空格,空格 = 总层数 -当前层
for (int k = 1; k <= totalLevel -i; k++) {
System.out.print(" ");
}
for (int j = 1; j <= 2 * i - 1; j++) {
//当前行第一个位置是*,最后一个位置也是*,最后一层全部是*
if (j ==1 || j ==2 * i -1 || i == totalLevel) {
System.out.print("*");
} else { //其他情况输出空格
System.out.print(" ");
}
}
System.out.println();

空心菱形:

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
/*
化繁为简
1. 打印一个金字塔 (空格个数为总层数-当前层)
2. 打印菱形
3. 打印空心菱形

先死后活
设置层数变量
*/
int n = 4; //层数
int totalLevel = n/2;

for (int i = 1; i <= totalLevel; i++) {
for (int k = 1; k <= totalLevel - i; k++) {
System.out.print(" ");
}
for (int j = 1; j <= i * 2 -1; j++) {
if (j == 1 || j == i * 2 - 1) {
System.out.print("*");
} else {
System.out.print(" ");
}
}
System.out.println();
}

for (int i = totalLevel - 1 ; i >= 1; i--) {
for (int k = 1; k <= totalLevel - i; k++) {
System.out.print(" ");
}
for (int j = 1; j <= i * 2 -1; j++) {
if (j == 1 || j == i * 2 - 1) {
System.out.print("*");
} else {
System.out.print(" ");
}
}
System.out.println();

跳转控制

break

break 语句可以强制终止所在所在循环,在嵌套循环中,break 仅影响当前所在循环。
一个循环中可以不只有一个 break 语句。

break 语句出现在多层嵌套的语句块中,可以通过标签指明要终止的是哪一层语句块。

1
2
3
4
5
6
7
8
9
10
lable1:
for (int j = 0; j < 4; j++) {
lable2:
for (int i = 0; i < 10; i++) {
if (i == 2) {
break; //等价 break label2
}
System.out.println("i = " + i);
}
}

continue

countinue 与 break 的区别在于,countinue 并不是终止整个循环,而是中止最近的一次循环的当次迭代。

注意:

  1. continue 语句只能用在 while 语句、for 语句或者 foreach 语句的循环体之中,在这之外的任何地方使用它都会引起语法错误。
  2. 终止指的是结束整个循环,中止指的是结束循环的当次迭代(跳过)。

coutinue 语句出现在多层嵌套的语句块中,可以通过标签指明要中止的是哪一层语句块。

1
2
3
4
5
6
7
8
9
10
11
        label1:
for (int j = 0; j < 4; j++) {
label2:
for (int i = 0; i < 10; i++) {
if (i == 2) {
// continue; //等价于 coutinue label2
continue label1;
}
System.out.println("i = " + i);
}
}

return

在方法中,return 表示跳出所在的方法。

注意:如果 return 在 main 中使用,将表示为退出程序。

1
2
3
4
5
6
7
8
for (int i = 1; i <= 5; i++) {
if (i == 3) {
System.out.println("你好" + i);
return; //当使用在方法时。表示跳出方法,如果在main中,表示退出程序
}
System.out.println("Hello World!");
}
System.out.println("go on..");

第六章 数组、排序和查找

数组

数组可以存放多个同一类型的数据。数组也是一种数据类型,是引用类型(数组就是一组数据)。

1
2
3
4
5
6
7
8
9
10
//定义一个数组
double[] hens = {3, 5, 1, 3.4, 2, 50};
double sum = 0;

//遍历数组得到所有元素的和
for (int i = 0; i < 6; i++) {
System.out.println("第 " + (i + 1) + " 个元素的值:" + hens[i]);
sum += hens[i];
}
System.out.println("所有元素和:" + sum);
  1. double[] 表示该数组为 double 类型的数组,数组名 hens。
  2. {3, 5, 1, 3.4, 2, 50}表示该数组的值/元素,依次表示数组的第几个元素。
  3. 通过 数组名[下标] 来访问数组的元素,下标从 0 开始编号,第一个元素就是 hens[0],第二个元素就是 hens[1],以此类推。

动态初始化:

  1. 直接定义数组大小类型。
    数据类型[] 数组名 = new 数据类型[大小]
1
2
int[] a = new int[3];
//int 类型的名为 a 的数组存放了 3 个int类型的数据
  1. 先声明数组,再 new 分配空间。
1
2
3
4
5
6
//1. 声明数组
int[] a;
//或 int a[],此方法不推荐使用

//2. 分配空间
a = new int[3]; //分配内存空间,可以存放数据
  1. 静态初始化
    数据类型[] 数组名 = {元素值1, 元素值2...}
1
2
double[] hens = {3, 5, 1, 3.4, 2, 50};
//注意:仅适用于有指定元素。

数组使用细节:

  1. 数组元素可以是任何数据类型,但是不能混用。
  2. 数组创建后如果没有赋值,会有默认值,char 为\u0000,String 为 null,boolean 为 false。
  3. 数组属于引用类型,数组型数据是对象()。

数组赋值机制(重要):

  1. 基本数据类型赋值,这个值就是具体的数据(栈),而且相互不影响。
1
int n1 = 2; int n2 = n1;
  1. 数组在默认情况下是引用转递,赋的值是地址(堆),实际数据存放在堆中相应地址空间。
1
2
int [] arr1 = {1, 2, 3};
int [] arr2 = arr1;
  1. 数组赋值手动内容拷贝方式,数据传递,非引用传递。
1
2
3
4
5
6
7
8
9
10
//数组内容拷贝,非地址
int[] arr1 = {10, 20, 30};

//创建一个新的数组arr2,开辟新的数据空间
int[] arr2 = new int[arr1.length];

//遍历 arr1,把每个元素拷贝到 arr2 对应位置
for (int i = 0; i < arr1.length; i++) {
arr2[i] = arr1[i];
}

数组操作:

  1. 翻转

方式一:元素逐个交换

1
2
3
4
5
6
7
8
9
10
11
//定义数组
int[] arr = {11, 22, 33, 44, 55, 66};
int temp = 0;

//共交换三次
for (int i = 0; i < arr.length / 2; i++) {
temp = arr[i]; //临时变量保存需要替换的元素
arr[i] = arr[arr.length - i - 1];
arr[arr.length - i - 1] = temp;
}
System.out.println(Arrays.toString(arr));

方式二:逆序遍历,赋值新数组

1
2
3
4
5
6
7
8
9
10
11
//定义数组
int[] arr = {11, 22, 33, 44, 55, 66};
int[] arr2 = new int[arr.length];
int temp = 0;

//逆序遍历原数组,再存放入新数组
for (int i = arr.length; i <= 0; i--) {
arr2[i] = arr[i];
}
arr = arr2; //arr原空间数据没有被变量引用,jvm自动销毁
System.out.println(Arrays.toString(arr));
  1. 扩容
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
int[] arr = {1, 2, 3};
Scanner sc = new Scanner(System.in);

while (true) {
System.out.println("请输入添加元素值:");
int num = sc.nextInt();
//建立新数组用于存放新添加的元素
int[] arr2 = new int[arr.length + 1];

for (int i = 0; i < arr.length; i++) {
arr2[i] = arr[i];
}
arr2[arr2.length - 1] = num;
arr = arr2;

System.out.println("添加成功!");
System.out.println(Arrays.toString(arr));

//判断是否继续添加
System.out.println("是否继续添加(y/n):");
char yes = sc.next().charAt(0);
if (yes == 'n') {
break;
} else if (yes == 'y') {
System.out.println("继续添加!");
} else {
System.out.println("输入有误,退出程序!");
break;
}
}
  1. 缩减
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
int[] arr = {1, 2, 3, 4, 5};
Scanner sc = new Scanner(System.in);

while (true) {
//建立新数组用于存放缩减后的元素
int[] arr2 = new int[arr.length - 1];

for (int i = 0; i < arr.length - 1; i++) {
arr2[i] = arr[i];
}

arr = arr2;

System.out.println("缩减成功!");
System.out.println(Arrays.toString(arr));

//判断是否能够继续缩减
if (arr.length == 1) {
System.out.println("数组仅剩一位,无法继续缩减!");
break;
}

//判断是否继续缩减
System.out.println("是否继续缩减(y/n):");
char yes = sc.next().charAt(0);
if (yes == 'n') {
break;
} else if (yes == 'y') {
System.out.println("继续缩减!");
} else {
System.out.println("输入有误,退出程序!");
break;
}
}

排序

将多个数据,依照指定的顺序进行排列的过程。

冒泡排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int[] arr = {24, 69, 80, 57, 13};
int temp = 0;
boolean sorted = true;

for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
//如果前面的数大于后面的数,就交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
sorted = false;
}
}
if (sorted) break; //如果某一轮没有交换了,代表已经有序,提前退出程序
}
System.out.println(Arrays.toString(arr));

查找

顺序查找:

从头到尾一一比对,成功则停止,不成功则继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义一个字符串数组
String[] names = {"aa", "bb", "cc", "dd"};
Scanner sc = new Scanner(System.in);

System.out.println("请输入名字:");
String findName = sc.next();

//遍历数组,逐一比较
for (int i = 0; i < names.length; i++) {
if (findName.equals(names[i])) {
System.out.println("找到了!在第 " + (i + 1) + " 位。");
return; //直接退出
}
}
System.out.println("没找到!");

二维数组

数组的数组,即二维数组也是一个特殊的一维数组,其每个元素又是一个一维数组。

1
数据类型[][] 数组名 = {{元素值1, 元素值2...}, {元素值1, 元素值2...}...}
1
2
3
4
5
6
7
8
9
10
11
12
//定义二维数组
int[][] arr = {{0, 0, 0, 0, 0, 0}, {0, 0, 1, 0, 0, 0},
{0, 2, 0, 3, 0, 0}, {0, 0, 0, 0, 0, 0}};

//输出图形
for (int i = 0; i < arr.length; i++) {
//遍历二维数组的每个元素(数组)
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println(); //换行
}

二维数组中访问元素数组的元素:数组名[元素数组下标][元素数组中的元素的下标]

动态初始化:

  1. 直接定义数组。
1
2
3
4
5
6
7
8
9
int arr[][] = new int[2][3];
arr[1][1] = 7;
//遍历
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println(); //换行
}
  1. 先声明,再 new 分配空间。
1
2
3
4
//1. 声明数组
int[][] a; //或者 int[] a[];
//2. 分配空间
a = new int[3][2]; //分配内存空间,可以存放数据
  1. 列数不确定

    在多维数组中,列数可以不同。
    并且通过先声明,再分配空间的方式,有效节约不需要使用的一维数组(列)所占空间大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//有三个一维数组,但每个一维数组还没有开辟空间
int[][] arr = new int[3][];
for (int i = 0; i < arr.length; i++) {
//给每个一维数组开辟空间
//如果没有给一维数组new,那么arr[i]就是null
arr[i] = new int[i + 1];

//遍历一维数组,并给元素赋值
for (int j = 0; j < arr[i].length; j++) {
arr[i][j] = i + 1;
}
}
//遍历输出
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println(); //换行
}
  1. 静态初始化
1
数据类型[][] 数组名 = {{元素值1, 元素值2...}, {元素值1, 元素值2...}...}
1
2
3
int[][] arr = {{0, 0, 0, 0, 0, 0}, {0, 0, 1, 0, 0, 0},
{0, 2, 0, 3, 0, 0}, {0, 0, 0, 0, 0, 0}};
//注意:仅适用于有指定元素。>)

杨辉三角:

使用二维数组打印一个 10 行的杨辉三角。

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
/*
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1

1. 第一行有1个元素,第n行有n个元素
2. 每一行的第一个元素和最后一个元素都是1
3. 从第三行开始,对于非第一个元素和最后一个元素的元素的值

arr[i][j] = arr[i-1][j] + arr[i-1][j-1]
= 上一行的同一列的数 + 上一行的前一列的数
*/

int[][] yangHui = new int[10][];
for (int i = 0; i < yangHui.length; i++) {
//给每个一维数组(行)开辟空间
yangHui[i] = new int[i + 1];
//给每个一维数组(行)赋值
for (int j = 0; j < yangHui[i].length; j++) {
//每一行的第一个元素和最后一个元素都是1
if (j == 0 || j == yangHui[i].length - 1) {
yangHui[i][j] = 1;
} else { //中间的元素
yangHui[i][j] = yangHui[i-1][j] + yangHui[i-1][j-1];
}
}
}
//输出杨辉三角
for (int i = 0; i < yangHui.length; i++) {
for (int j = 0; j < yangHui[i].length; j++) {
System.out.print(yangHui[i][j] + "\t");
}
System.out.println();
}

第七章 面向对象(初级)

使用传统方式储存物体信息,会有一定问题:

  1. 单独定义变量:不利于数据的管理(拆解了信息)。
1
2
3
4
5
6
String cat1 = "小白";
String cat2 = "小花";
int age1 = 3;
int age2 = 100;
String color1 = "白色";
String color2 = "花色";
  1. 使用数组:

    (1) 数据类型无法定义。
    (2) 只能通过下标获取信息,造成变量名字和内容的对应关系不明确。
    (3) 不能体现猫的行为。

1
2
String[] cats1 = {"小白","3","白色"};
String[] cats2 = {"小花","100","花色"};

总的来说,就是效率低,不利于管理。因此引出类与对象(OOP)

类与对象及属性

类: 自定义的一种抽象数据类型(像 int、double 就是系统定义的数据类型)。在类中,我们可以定义属性(name、age、color……)和行为(run、cry、eat……)。

对象: 具体的一个物体(实例),属于某类,而类的这些属性和行为会相应的传递给这个对象。

类是对象的模板,对象是类的一个个体(实例)。

类到对象的说法(通用):

  • 创建一个对象。
  • 实例化一个对象。
  • 把类实例化。

创建一个猫类,并给它一些属性,再创建两个对象,对其猫类实例化:

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
public class Object01 {
public static void main(String[] args) {
//判断猫

//使用OOP面向对象解决
//实例化一只猫[创建一只猫对象]
//第一只猫,catx1就是一个对象
Cat catx1 = new Cat();
catx1.name = "小白";
catx1.age = 3;
catx1.color = "白色";
catx1.weight = 20;

//第二只猫,catx2也是一个对象
Cat catx2 = new Cat();
catx2.name = "小花";
catx2.age = 100;
catx2.color = "花色";
catx2.weight = 20;

//访问对象的属性
System.out.println("第一只猫的信息:" + catx1.name + catx1.color + catx1.age + catx1.weight);
System.out.println("第二只猫的信息:" + catx2.name + catx2.color + catx2.age + catx2.weight);
}
}

//面向对象解决问题
//定义一个猫类 Cat => 自定义的数据类型
class Cat {
//属性
String name; //名字
int age; //年龄
String color; //颜色
double weight; //体重

//行为
}

Java 内存的结构:

  1. 栈:一般存放基本数据类型(局部变量)。
  2. 堆:存放对象(Cat cat,数组等)。
  3. 方法区:存放常量池(常量,如字符串)、类加载信息。

对象在内存中的存在形式:

创建对象后,在栈中的对象名称指向一个地址,这个地址在堆中存放了这个对象,其中包含各个地址,这些地址分别指向方法区中常量池里的各个具体的属性值

需要注意的是,如果对象的属性是基本数据类型(非 String),就直接存放在堆的对象中,而不会指向方法区。

在创建对象的过程中(new),在方法区中会加载该类信息,包含属性信息和方法信息。

属性 / 成员变量细节:

  1. 成员变量 = 属性 = field(字段)
  2. 属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)。
  3. 属性的定义语法和变量相同:访问修饰符 属性类型 属性名。(访问修饰符用于控制属性的访问范围:public,protected,默认,private,会在认识后进行系统学习)
  4. 属性如果不赋值,会有默认值,规则与数组一致(char 为\u0000,String 为 null,boolean 为 false)。
  5. 对象赋值给对象,也是与数组一致的通过改变地址指向,而非改变内容,因此赋值后两个对象指向同一个地址。

创建对象方式:

  1. 先声明,再创建
1
2
Cat cat; //声明对象cat
cat = new Cat(); //正式创建对象,在内存堆中开辟空间,此时cat指向这个空间
  1. 直接创建
1
Cat cat = new Cat();

创建对象流程分析:

  1. 加载对象的类信息(属性和方法的信息,且只会加载一次)。
  2. 在堆中分配空间,进行默认初始化(属性赋予默认值)。
  3. 堆中的对象的地址赋给栈中的对象名称,此时对象名称指向了相应的对象。
  4. 进行指定初始化,如 对象.属性 = XXX

成员方法

某些情况下,我们需要指定一个对象的行为,即成员方法(简称方法),如人类,除了有属性(年龄、性别等)之外,还会有一些行为。

成员方法使用:

  1. 在类中创建方法。
  2. 创建对象,调用方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Method01 {
public static void main(String[] args) {
//新建对象
Persons p1 = new Persons();
//调用方法
p1.speak();
}
}
class Persons {
String name;
int age;

//添加speak成员方法并输出
/*
1. public:表示方法是公开的
2. void:表示方法没有返回值
3. speak():speak是方法名,()为形参列表
4. {}方法体,可以写要执行的代码
*/
public void speak() {
System.out.println("我是一个好人。");
}
}

方法可以接收外部信息,经过处理后,可以再传出。

getSum 成员方法,可以计算两个数的和:

1
2
3
4
5
6
7
8
9
// 1. public 表示方法是公开的
// 2. int 表示方法执行后,返回一个int值
// 3. getSum 方法名
// 4. (int num1, int num2) 形参列表,2个形参,可以接收用户传入的数据
// 5. return res; 表示把res的值返回
public int getSum(int num1, int num2) {
int res = num1 + num2;
return res;
}

调用 getSum 成员方法:

1
2
3
4
5
//新建对象
Persons p1 = new Persons();
//调用方法
int returnRes = p1.getSum(10,20); //填入需要传入的形参
System.out.println(returnRes);

成员方法调用机制:

  1. 当程序执行到方法时,就会开辟一个独立的栈空间(还在 main 主方法栈空间内部)。
  2. 当方法执行完毕,或者执行到 return 语句时,就会返回到调用方法的地方(main 栈)。
  3. 返回后,继续执行犯法后面的代码。
  4. 当 main 主方法(栈)执行完毕,程序退出。

使用成员方法的优点:

  1. 提高代码复用性。
  2. 可以将实现的细节封装,然后供其他用户来调用。
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
public class Method02 {
public static void main(String[] args) {
//遍历一个数组,输出数组的各个元素值
int [][] map = {{0,0,1},{1,1,1},{1,1,3}};

//遍历map数组
//传统方式:直接遍历
//缺点:要多次遍历的话,代码冗余度过高,一段代码复制多遍
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}

//方法
//把输出的功能,写到一个类的方法中,直接调用方法
myTools tool = new myTools();
tool.printArr(map);
tool.printArr(map);
}
}

class myTools {
//方法,接收一个二维数组
public void printArr(int[][] map) {
//对传入的map数组进行遍历
System.out.println("==========");
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}
}
}

成员方法的定义:

1
2
3
4
public 返回数据类型 方法名(形参列表...) {//方法体
语句;
return 返回值;
}
  1. 返回数据类型:表示成员方法食醋胡,void 表示没有返回值。
  2. 参数列表:表示成员方法输入(如 cal(int n),n 就是输入的数据)。
  3. 方法体:表示为了实现某一功能的代码块。
  4. return 语句不是必须的。

成员方法使用细节:

  1. 访问修饰符如果不写则是默认访问,有四种:public, protected, 默认, private。
  2. 一个方法最多有一个返回值,可使用数组来解决返回多个值问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MethodDetail {
public static void main(String[] args) {
//1. 一个方法最多有一个返回值,如何返回多个结果:数组
AA a = new AA();
int[] res = a.getSumAndSub(1, 4);
System.out.println("和 = " + res[0] + " 差 = " + res[1]);
}
}

class AA { //使用数组作为数据类型
public int[] getSumAndSub(int n1, int n2) {
int[] resArr = new int[2];
resArr[0] = n1 + n2;
resArr[1] = n1 - n2;
return resArr;
}
}
  1. 如果方法要求有返回数据类型,则方法体中的最后的执行语句必须为return 值;,并且要求返回值得类型必须和 return 的值类型一致或兼容。
  2. 如果方法是的返回数据类型为 void,则方法体中可以没有 return 语句,或者只写return;
  3. 方法名遵循小驼峰命名法,见名知意,如得到两个数的和则以此命名:getSum。

形参列表使用细节:

  1. 一个方法可以有 0 个或多个参数。
  2. 参数类型可以为任意类型。
  3. 调用带参数的方法时,要对应传入相同或兼容的参数。
  4. 方法定义时的参数称为为形式参数(形参),方法调用的时候参数称为实际参数(实参),实参和形参的类型要一致或兼容个数、顺序必须一致

方法调用细节:

  1. 同一个类中的方法:可以直接在自己的方法体相互去调用。
  2. 不同类中的方法:需要通过对象名来调用。
  3. 跨类的方法调用和方法的访问修饰符相关,后面细说。
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
class A {
//同一个类中的方法互相调用:直接调用

public void print(int n) {
System.out.println("print()方法被调用 n=" + n);

}

public void sayOk() { //sayOk调用 print print(10); //直接调用
System.out.println("继续执行sayOk()。");
}

//不同类中的方法:需要通过对象名来调用
//A类里调用B类的方法
public void m1() {
//创建B类的B对象,再调用B对象的方法。
System.out.println("m1()方法被调用。");
B b = new B();
b.hi();
System.out.println("m1()方法继续执行。");
}
}

class B {
public void hi() {
System.out.println("B类中的hi()被执行。");
}
}

方法传参机制:

  1. 基本数据类型:传递的是值(值拷贝,非地址),形参的任何改变都不会影响到实参。
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
public class MethodParameter01 {
public static void main(String[] args) {
int a =10;
int b =20;
//创建AA2对象
AAp obj = new AAp();
obj.swap(a,b);

//swap方法将会在栈中新开辟一块独立空间执行方法体语句,和主方法隔离,
//因此此时的a和b还是主方法的a和b
System.out.println("主方法 a=" + a + "\tb=" + b); // a=10 , b=20
}
}

class AAp {
public void swap (int a, int b) {
System.out.println("\na和b交换前的值 a=" + a + "\tb=" + b);
// a=10, b=20

int tmp = a;
a = b;
b = tmp;
System.out.println("\na和b交换后的值 a=" + a + "\tb=" + b);
// a=20, b=10

}
}
  1. 引用数据类型(数组、对象):传递的是地址(指向堆中实际的数组空间),形参与实参共享一个地址,可以通过形参影响实参。
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
public class MethodParameter02 {
public static void main(String[] args) {
B1 b = new B1();
int [] arr = {1, 2, 3};
b.test100(arr); //调用方法

System.out.println("主方法的arr数组:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println(); //换行

//测试
Personx px = new Personx();
px.age = 10;
px.name ="jack";

b.test200(px);
System.out.println(px.name + px.age);
//在方法中修改属性成功
//结论:对象和数组一样是引用类型,栈地址指向堆。
}
}
class Personx {
String name;
int age;
}

class B1 {
public void test100(int[] arr) {
arr[0] = 200;
//遍历数组
System.out.println("test100的arr数组:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println(); //换行
}
//在方法中修改对象属性
public void test200(Personx px) {
px.age = 10000; //修改对象属性
}
}

方法递归

韩顺平递归原理讲解

1
2
3
4
5
6
7
8
//计算 n 的阶乘
public int factorial(int n) {
if (n == 1) {
return 1; //n为1时停止循环调用
} else {
return factorial(n - 1) * n; //n > 1 时自身-1再循环调用factorial方法
}
}

递归原则:

  1. 执行一个方法时,就会创建一个新的受保护的独立空间(栈空间)。
  2. 方法的局部变量是独立的,不会相互影响,如 n 变量。
  3. 方法中的引用类型变量(数组,对象)之间会共享该引用类型的数据
  4. 递归必须向退出递归的条件逼近,否则会死循环,无限递归。
  5. 当一个方法执行完毕,或者遇到了 return 语句,就会返回,遵循 “谁调用,返回谁” 的原则。

汉诺塔问题:

吐槽一下,韩顺平这里讲的非常简略,压根没讲细节,就光演示了,非常不利于新手理解。

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
public class HanoiTower {
public static void main(String[] args) {
Tower tower = new Tower();
tower.move(3, 'A', 'B', 'C');
}
}

class Tower {
//方法
//num 表示要移动的个数,num - 1 表示除去底层一个盘,其他盘的个数
public void move(int num, char a, char b, char c) {
if (num == 1) {
//只有一个盘的情况,也是最终
System.out.println(a + " => " + c);
} else {
//如果有多个盘,可以都分为两个盘,最下面的1盘和上面的num-1盘

// 1. 先借助 c 将上面的 num-1 盘移动到 b。
move(num - 1, a, c, b);
// 2. 把最下面的这个1盘,移动到 c。
System.out.println(a + " => " + c);
// 3. 再把暂时放在 b 的 num-1 盘 全部放到 c。
move(num - 1, b, a, c);
}
}
}

如果只要移动一个盘子到 C 上,那么很简单:

A => C

要将两个盘子从 A 放到 C 上,也不难:

move(2, A, B, C)
A => B,先把上面的盘子从 A 放到 B。
A => C,再把底层的那个盘子从 A 放到 C。
B => C,最后将临时放在 B 上的盘子放到 C 上。

如此一来两个盘子都在 C 上了。

实现 3 个盘子从 A=>B 的程序执行过程:

move(3, A, B, C)
move(2, a, c, b),先把上面的 2 个盘子(即 num - 1 个盘子)从 A 放到 B。
A => C,再把底层的那个盘子从 A 放到 C。
move(2, b, a, c),最后将临时放在 B 上的盘子放到 C 上。

通过对移动三个盘子的过程进行整合,可将多个盘的移动的过程抽象为 1 个盘和 num-1 个盘的移动。

八皇后问题:

递归中的回溯问题。

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
public class QueenTest{
public static void main(String[] args){
Tx t1 = new Tx();
t1.put(0);
}
}
class Tx{
int arr[] = new int[8];

//输出算法
public void print(){
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i] + " ");
}
System.out.println();
}

//判断算法
public boolean judge(int index){
for(int i = 0; i < index; i++){//判断和之前的index个皇后是否冲突
if(arr[i] == arr[index] || Math.abs(arr[index]-arr[i]) == Math.abs(index -i)){
return false;
}
}
return true; //必须放在for循环外部,
}

//放置算法,即在棋盘中放置第几个皇后
public void put(int index){
//首先判断index是否等于8
if(index == 8){//当index等于8时,即第9个棋子,已经完成了8个棋子的放置
print();//调用输出算法
}else{
for(int i = 0; i < 8; i++){
arr[index] = i;//
if(judge(index)){//如果返回true则继续判断
put(index + 1);
}

}
}
}
}

在最重要的放置算法中,首先判断 index 是否为 8,index 为 8 就代表已经是第 9 颗棋子了,所需要的 8 个皇后已经放置完毕,因此结束当前递归中的此次方法,但不会结束程序,因为方法是递归的,循环嵌套。

例如第一个皇后放置在(0, 0),下一个皇后会放在(1, X),再下一个皇后会放在(2, X),直到第八个皇后放置结束,if(index == 8)判断为 true,结束本次方法。

但注意这个方法是层层递归到第八个皇后的放置的,因此在第八个皇后放置完后,又回到第七个皇后的放置方法中,接着往下看有没有别的地方可以放(即放完第七个皇后别的位置后,再去放第八个皇后,以此往复,检查有无可以放的位置)。

以此类推,第七个皇后能放的位置都放完了,再去检查第六个、第五个……直到第一个皇后也把能放的位置放完了,则完成最后的递归循环,结束程序。

在运行过程中,遇到死棋,也就是中间有皇后已经无法放置了,也会回到上一个递归的方法,即无法放置的皇后的前一个方法,逐个放置不同的位置,再递归到下一个皇后测试,如果一个能放置的位置都没有,那么就会再往前回一个皇后,进行同样操作,直到那个死棋可以放置,以此往复,此为回溯。

其实就是细数每一个位置正确的可能,然后逐个暴力推算破解,不行就润到上一层,行就继续找,找到底再润回上一层找,直到每层都找完了,就结束。

方法重载

同一个类中,可以有多个同名方法,但是这些同名方法的形参列表必须不一致

1
2
3
4
5
6
7
8
9
10
public class OverLoad01 {
public static void main(String[] args) {
//out 对象的 println 方法
//通过同方法的重载,加载不同的形参
System.out.println(100); //int类型
System.out.println("hello, world"); //字符串类型
System.out.println(1.1); //double类型
System.out.println(true); //boolean类型
}
}

如果没有方法的重载,那么光一个 println 方法,需要打印不同的类型的话,岂不是需要 printInt、printChar……这么多个不同名称的方法了。

因此重载的好处有:

  1. 减轻了起名的麻烦。
  2. 减少了记名的麻烦。
  3. 利于接口编程。

通过重载创建四个不同的 calculate 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyCalculator {
//四个方法使用了重载技巧

//两个整数的和
public int calculate(int n1, int n2) {
return n1 + n2;
}

//一个整数,一个double的和
public double calculate(int n1, double n2) {
return n1 + n2;
}

//一个double,一个Int的和
public double calculate(double n1, int n2) {
return n1 + n2;
}

//三个Int的和
public int calculate(int n1, int n2, int n3) {
return n1 + n2 + n3;
}
}

调用方法时,只需要填入相应的形参列表,就可以选择对应的方法了。例如调用两个整数的和的方法,只需要calculate(1, 1),两个参数都为 Int 类型即可。

方法重载使用细节:

  1. 方法名称必须相同。
  2. 方法形参列表必须不同(形参类型、个数和顺序至少有一样不同,参数名无所谓)。
  3. 方法的返回值可以不同。
  4. 重载中方法形参类型优先级高于自动类型转换。
1
2
3
4
5
6
7
public void m(int n) {
System.out.println(n * n);
}

public void m(double n) {
System.out.println(n * n);
}

如果在调用 m 方法时,输入的形参为 Int 类型,并不会强制转换成 double 类型,而是优先选择能够使用 Int 类型的方法进入。

若只有一个使用 double 类型的方法的形参,那么输入的 Int 类型实参会强制转换为 double 类型带入方法。

可变参数

若需要方法名称相同,参数类型相同,但参数个数不同的方法实现同一种功能,如计算 2 个数的和,3 个数的和,4 个数的和…… 的方法,虽可以通过方法的重载来实现,但相对还是比较麻烦。

因此可以使用可变参数来优化,使用一个方法完成不同个数的参数的引用

可变参数的使用:

public 返回类型 方法名(参数类型... 参数名) {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VarParameter01 {
public static void main(String[] args) {
HspMethod m = new HspMethod();
m.sum(1, 5, 100);
}
}

class HspMethod {
//一个可以计算2个数的和,3个数的和,4个数的和…… 的方法
//可以使用方法重载,但很麻烦
//方法名称相同,功能相同,参数个数不同,使用可变参数优化

// 1. int...表示接收的是可变参数,类型是int,即可以接收多个int
// 2. 使用可变参数是时,可以当作数组来使用,即 nums 可以作为数组
public void sum(int... nums) {
System.out.println("接收的参数个数:" + nums.length);
int res = 0;
for (int i = 0; i < nums.length; i++) {
res += nums[i];
}
System.out.println(res);
}
}

在可变参数中,形参列表作为数组引入方法过程,唯一的参数名就是数组名称,数组的内容就是各个参数值。

可变参数使用细节:

  1. 可变参数的实参可以为 0 或任意多个。
  2. 可变参数的实参可以为数组。
  3. 可变参数的本质就是数组
  4. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数放在最后。
1
public void f2(String str, double... nums) {}
  1. 一个形参列表中只能出现一个可变参数。

作用域

  1. 在 Java 中,主要的变量就是属性(成员变量)和局部变量。
  2. 局部变量一般是指在成员方法中定义的变量。
  3. 作用域分为全局变量(属性)与局部变量,全局变量顾名思义,作用域整个类中的所有方法,局部变量就是除了属性之外的其他变量,作用域为定义它的代码块。
  4. 全局变量(属性)可以不赋值,直接使用,因为会有默认值,但是局部变量必须赋值后才能使用,因为没有默认值。
1
2
3
4
5
6
7
8
class T3 {
int x2; //全局变量可以不进行初始化

public void test3() {
int x = 0; //局部变量必须要初始化
System.out.println(x2); //正常调用
}
}

作用域使用细节:

  1. 属性和局部变量可以充满,访问时遵循就近原则。
  2. 在同一个作用域中(如同一个成员方法中),两个局部变量不能重名。
  3. 属性生命周期较长,随着对象的创建而创建,随着对象销毁而销毁。局部变量生命周期焦点,伴随着自身代码块执行而创建,伴随代码块的结束而死亡。
  4. 作用域的范围不同,属性可以被本类或其他类通过对象调用。局部变量只能在本类中的对应的方法使用。
  5. 全局变量/属性可以加修饰符,局部变量不可以加修饰符。

构造方法/构造器

构造器是类的一种特殊的方法,它的主要作用是完成对象的初始化

1
2
3
[修饰符] 方法名(形参列表) {
方法体;
}
  1. 构造器的修饰符可以默认,也可以是 public/protected/private。
  2. 构造器没有返回值
  3. 构造器的方法名和类名必须一致
  4. 参数列表与成员方法规则一致。
  5. 构造器的调用由系统完成。
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
public class Constructor01 {
public static void main(String[] args) {
//当 new 一个新对象时,直接通过构造器指定整个对象的年龄和姓名
Person4 p1 = new Person4("张三", 80);

System.out.println("p1对象信息:");
System.out.println("name = " + p1.name);
System.out.println("age = " + p1.age);
}
}

//构造方法:在创建人类的对象时,就直接指定这个对象的年龄和姓名
class Person4 {
String name;
int age;

// 1. 构造器没有返回值,也不能写void
// 2. 构造器的名称和类Person4一样
// 3. 构造器形参列表规则与成员方法一样

public Person4(String pName, int pAge) {
System.out.println("构造器被调用,完成对象的初始化。");
name = pName;
age = pAge;
}
}

构造器使用细节:

  1. 一个类可以定义多个构造器,即构造器的重载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConstructorDetail {
public static void main(String[] args) {
Person5 p1 = new Person5("king", 40);
Person5 p2 = new Person5("king");
}
}

class Person5 {
String name;
int age;

//第一个构造器
public Person5(String pName, int pAge) {
name = pName;
age = pAge;
}

//第二个构造器
public Person5(String pName) {
name = pName;
}
}
  1. 构造器是完成对象的初始化,并不是创建对象。
  2. 在创建对象时,系统自动调用该类的构造方法。
  3. 如果程序员未定义构造器,系统会自动给类生成一个默认无参构造器,如Person(){}
  4. 如果自己定义了构造器,默认的无参构造器就被覆盖了,除非显示的定义:Dog(){}

再次分析对象的创建流程(以 Person 类为例):

1
2
3
4
5
6
7
8
class Person { 类
int age = 90;
String name;
Person(String n, int a) { //构造器
name = n; //属性赋值
age = a;
}
}
  1. 加载 Person 类信息(Person.class),只会加载一次。
  2. 在堆中分配空间(地址)。
  3. 完成对象初始化:
    3.1 默认初始化:age=0 name=null
    3.2 显式初始化:age=90, name=null
    3.3 构造器的初始化:age=20, name = “xxx”
  4. 把对象在堆中的地址,返回给 p(对象名,实际上是对象的引用)。

this 关键字

this 相当于在对象中的一个属性,值为地址,这个地址指向对象本身,因此 this 相当于引用的对象自身。哪个对象调用,this 就代表哪个对象

HashCode 将对象的实际地址转换成整数并输出,我们可以通过 HashCode 来判断是否为同一个对象地址:

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
public class This01 {
public static void main(String[] args) {
Dog dog1 = new Dog("大壮", 3);
System.out.println("dog1的hashCode值:" + dog1.hashCode());
Dog dog2 = new Dog("大黄", 2);
System.out.println("dog2的hashCode值:" + dog2.hashCode());
dog1.info();
dog2.info();
}
}

class Dog {
String name;
int age;

//根据作用域原则,构造器的name就是局部变量,不是属性
public Dog(String name, int age) { //构造器
//this.name 就是当前对象的属性 name this.name = name;
//this.age 就是当前对象的属性 age this.age = age;

System.out.println("this的hashCode值:" + this.hashCode());

}

public void info() {
System.out.println(name + "\t" + age + "\t"); //成员方法,输出属性信息
}
}

可见两个对象的地址和其 this 的地址输出一致。

this 使用细节:

  1. this 可以用来访问本类的属性、方法、构造器
  2. this 用于区分当前类的属性和局部变量。
  3. 访问成员方法的语法:this.方法名(参数列表)
  4. 访问构造器的语法:this(参数列表)(只能在构造器中访问另一个构造器,且必须放在结构体的第一句)。
  5. this 不能在类的外部使用,只能在类定义的方法中使用。
  6. 传统方式调用类的成员变量,会遵循就近原则,如果在方法内定义过了和成员变量相同名称的变量,那么只会调用到方法内的变量,而不会是方法外的成员变量。但使用 this 关键字,可以直接访问到方法外部的成员变量。

猜拳游戏:

虽然是简单的 if 语句判断,但是融合了类与对象的知识,将功能模块化分装成方法,是非常好的面对对象实践。

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
import java.util.Random;
import java.util.Scanner;

public class Homework14 {
//猜拳
public static void main(String[] args) {
caiquan t = new caiquan();

//创建二维数组,记录出拳情况
int[][] arr1 = new int[3][3];
int j = 0;

//创建一维数组,记录输赢情况
String[] arr2 = new String[3];

for (int i = 0; i < 3; i++) {

//获取玩家出拳
arr1[i][j + 1] = t.getUserG();

//判断输入是否正确
if (arr1[i][j + 1] == -1) {
i--;
continue;
}

//获取电脑出拳
arr1[i][j + 2] = t.getComG();

//出拳情况比较
String win = t.vs();
arr2[i] = win;

System.out.println("局数\t玩家出拳\t电脑出拳\t输赢情况");
System.out.println(i + 1 + "\t" + arr1[i][j + 1] + "\t" + arr1[i][j + 2] + "\t" + arr2[i]);
System.out.println("------------------------------");

}
//列出结果
System.out.println("局数\t玩家出拳\t电脑出拳\t输赢情况");
for (int i = 0; i < arr1.length; i++) {
System.out.println(i + 1 + "\t" + arr1[i][j + 1] + "\t" + arr1[i][j + 2] + "\t" + arr2[i]);
}
System.out.println("总共玩了:" + t.count + "次。");
System.out.println("总共赢了:" + t.userWin + "次。");
}
}

class caiquan {
//用户出拳,电脑出拳,用户赢次数,总共玩次数
int userG, comG, userWin, count;

public int getUserG() {
try {
Scanner sc = new Scanner(System.in);
System.out.println("请出拳(0-拳头,1-剪刀,2-布):");
userG = sc.nextInt();
//判断是否输入超值
if (userG >= 0 && userG <= 2) {
return userG;
} else {
System.out.println("输入错误,请重试!");
return -1;
}
} catch (Exception e) {
//判断是否输入其他字符
System.out.println("数字输入错误,请重试!");
return -1;
}
}

//获取电脑出拳
public int getComG() {
Random r = new Random();
comG = r.nextInt(3);
return comG;
}

//判断输赢
public String vs() {
count++;
if (comG == 1 && userG == 0) {
userWin++;
return "玩家赢。";
} else if (userG == 1 && comG == 2) {
userWin++;
return "玩家赢。";
} else if (userG == 2 && comG == 0) {
userWin++;
return "玩家赢。";
} else if (userG == comG) {
return "平局";
} else {
return "电脑赢。";
}
}
}

第八章 面向对象(中级)

IDEA 使用

快捷键功能
Ctrl + Alt + L一键格式化代码
Ctrl + D复制当前行到下一行
Shift + Enter新建一行,并且光标移到新行
Ctrl + Y删除当前行
Alt + Enter自动导入当前行所需要的类
Alt + R运行当前程序
Alt + Insert生成构造方法
Ctrl + H查看所选类的层级关系
Ctrl + B定位到所选方法所在位置
方法.var自动分配变量名
F8Step Over 跳过当前 Debug 语句
F7Step Into 跳入当前 Debug 自定义的方法
Alt+ Shift +F7Force Step Into 强制跳入,可以进入任何方法
Shift + F8Step Out 跳出当前 Debug 方法
F9Resume Program 继续执行到下一个断点

三大作用:

  1. 区分相同名字的类。
  2. 当类很多时,可以方便的对类进行管理。
  3. 控制访问范围。

基本语法:package com.laelwz

package 关键字表示打包,com.laelwz表示包名。

原理: 实际上就是创建不同的文件夹来保存类文件,画出示意图。

包的命名:

  1. 命名规则:只能包含数字、字母下划线、小圆点.,不能用数字开头,不能是保留字或关键字。
  2. 命名规范:小写字母+小圆点,一般为com.公司名.项目名.业务模块名
    例如:com.laelwz.oa.modelcom.laelwz.oa.controller

常用包:

  • java.lang.* 基本包,默认引入,无需手动引入。
  • java.util.* 系统提供的默认工具包、工具类(Scanner 之类)。
  • java.net.* 网络包,网络开发使用。
  • java.awt.* 界面开发,GUI 使用。

包的引用方法: import 包;

1
2
3
4
//引用某个包下的某个类
import java.util.Arrays; //只引入java.util包下的Arrays类
//引用整个包
import java.util.*; //将java.util包所有类都引入

包使用细节:

  1. package的作用是声明当前类所在的包,需要放在类的最上面,一个类最多只有一句。
  2. import放在package之后,在类定义之前,可以有多句,没有顺序要求。

访问修饰符

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

  1. 公开:public 修饰,对外公开。
  2. 受保护:protectd 修饰,对子类同一个包中的类公开。
  3. 默认:没有修饰,只向同一个包的类公开。
  4. 私有:private 修饰,只有类本身可以访问,不对外公开。
访问级别修饰符同类同包子类不同包
公开public
受保护protectedX
默认没有修饰符XX
私有privateXXX

访问修饰符使用细节:

  1. 修饰符可以用来修饰类中的属性、成员方法以及类。
  2. 类只有默认与 public 修饰符。
  3. 成员方法的访问规则和属性完全一致。

OOP 三大特征

封装

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

封装的作用:

  1. 隐藏实现细节:调用方法,只要求传入参数,返回结果。
  2. 可以对数据进行验证,保证安全。

封装的使用:

  1. 将属性私有化(private),意味着不能直接在外部直接修改属性。
  2. 提供一个公共的 set 方法,用于属性的判断与赋值。

    1
    2
    3
    4
    public void setXxx(类型 参数名) {
    //数据验证
    属性 = 参数名;
    }
  3. 提供一个公共的 get 方法,用于获取属性的值。

    1
    2
    3
    4
    public 属性类型 getXxx() {
    //权限判断
    return 属性值;
    }

例:对工人类属性、方法进行封装处理

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
package com.laelwz.encap;

public class Encapsulation01 {
public static void main(String[] args) {
Person person = new Person();
person.setName("asd"); //设置名字并验证
person.setAge(30); //设置年龄并验证
person.setSalary(30000); //设置薪水
System.out.println(person.info()); //输出工人信息方法
System.out.println(person.getSalary()); //输出获取薪水信息方法
}
}

class Person {
public String name = "无名人"; //名字公开
private int age; //年龄私有
private int salary; //工资私有

//手写get/set太慢,可以使用快捷键
//根据要求完善代码

public String getName() {
return name;
}

public void setName(String name) {
//加入对数据的校验,相当于加入了业务逻辑
if (name.length() >= 2 && name.length() <= 6) {
this.name =name;
} else {
System.out.println("名字长度错误,需要2-6字符,设置默认名字:无名人");
this.name = "无名人";
}
}

public int getAge() {
return age;
}

public void setAge(int age) {
//判断
if (age >= 1 && age <= 120) {
this.age = age;
} else {
System.out.println("年龄错误,需要 1-120,设置默认年龄为18");
this.age = 18; //默认年龄
}
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

//写一个方法,返回属性
public String info() {
return "信息为 name=" + name + " age=" + age + " salary=" + salary;
}
}

将构造器与 setXxx 结合:

若需要在构造器中对输入的属性进行验证,可直接在构造器中执行 setXxx 方法进行属性设置。

1
2
3
4
5
6
7
8
9
10
11
12
//三个属性的构造器
public Person(String name, int age, int salary) {
//传统方法
// this.name = name;
// this.age = age;
// this.salary = salary;

//可以将setXxx方法写在构造器中,这样仍然可以进行数据的验证
setName(name);
setAge(age);
setSalary(salary);
}

继承

当多个类存在相同的属性和方法,会导致代码的冗余,这时可以使用继承的方法来提高代码效率。

通过继承,从这些相似的类中抽象出父类(基类/超类),在父类中定义这些相同的属性和方法,子类就可以直接继承到父类的这些属性和方法,而不需要每个子类都再重新定义一遍。

基本语法:

1
2
3
class 子类 extends 父类 {

}

继承的优点:

  1. 代码复用性提高。
  2. 代码的扩展性和可维护性提高。

继承使用细节:

  1. 子类能够继承父类所有属性和方法,但是父类的私有属性不能在子类直接访问,需要通过父类提供的公共方法间接访问。例如用一个公共 public 权限的方法去返回一个私有 private 权限的属性。
  2. 子类必须调用父类的构造器,完成父类的初始化。( 默认执行super()方法,会自动调用父类的无参构造器)
  3. 当创建子类对象时,无论使用子类的任何构造器,都会默认去调用父类的无参构造器(无参构造器默认存在),如果父类没有提供无参构造器,则必须在子类的构造器中使用super()方法去指定使用父类的任意构造器,以此完成父类的初始化工作,否则无法通过编译。
  4. 指定调用父类的某个构造器,只需要显式的使用super(参数列表)调用即可,这里的 super 就是指向父类的构造器。
  5. super()方法必须放在构造器的第一行,且只能在构造器中使用。
  6. super 与 this 在构造器中只能二选一,不可同时出现。
  7. Java 中所有类都是 Object 类的子类,Object 是所有类的基类。
  8. 父类构造器的调用不限于上一级父类,会一层一层追溯到最顶层的 Object 类,每个父类的构造器都会被调用。
  9. 子类最多只能继承一个父类(直接继承),即单继承机制
  10. 不可滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
1
2
Xiaoming is a Person => Xiaoming extends Person
Cat is a Animal => Cat extends Animal

继承在内存中的分析:

  1. 当一个对象创建,首先从该对象的类的最顶层父类 Obeject 开始逐一加载父类与子类,这些类信息都加载在内存方法区中。
  2. 在堆中开辟一个属于该对象的内存空间。
  3. 在该空间内,根据父类子类的顺序从父到子依次写入每个类的属性、方法(基本数据类型直接写入数据,String 类型与方法则写入地址,实际数据储存到方法区的常量池中),每个类都拥有一个区块(避免重名问题)。
  4. 最后将该对象的内存空间地址返回到(指向)主方法中的对象名称。

对象属性在各级类之间的查找关系:

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
package com.laelwz.extend_;

public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son();
//属性查找关系
/*
1. 首先看子类是否有该属性
2. 如果有并且可以访问,则返回信息
3. 如果子类没有这个属性,则向父类查找该属性,如果有则返回
4. 如果父类没有,则继续像上级类逐级查找,直到Object基类
*/
System.out.println(son.name);
System.out.println(son.age);
System.out.println(son.hobby);
}
}

class GranPa { //爷类
String name = "大头爷爷";
String hobby = "旅游";
}

class Father extends GranPa { //父类
String name = "大头爸爸";
// String hobby = "钓鱼";
int age = 23;
}

class Son extends Father { //子类
String name = "大头儿子";
// String hobby = "学习";
}

继承设计的基本思想: 父类的构造器完成父类属性初始化,子类的构造器完成子类属性初始化。

1
2
3
4
5
6
7
8
9
10
package com.laelwz.extend_.exercise;

public class PC extends Computer {
private String brand;

public PC(String cpu, int memory, int disk, String brand) {
super(cpu, memory, disk); //先初始化父类的构造器
this.brand = brand; //再初始化子类(自身)
}
}

多态

若有同一种行为,但是目标和对象很多而且不同,那么按照传统写法,势必要写多个方法,如

1
2
3
4
5
6
7
8
9
10
11
12
//主人给狗喂食骨头
public void feed(Dog dog, Bone bone) {
System.out.println("主人" + name + "给" + dog.getName() + "吃" + bone.getName());
}

//方法的重载
//主人给猫喂黄花鱼
public void feed(Cat cat,Fish fish) {
System.out.println("主人" + name + "给" + cat.getName() + "吃" + fish.getName());
}

//如果动物和食物很多 => 方法很多,复用性不高,不利于管理和维护

^2394bc

很明显的,这样带来了问题:方法很多,复用性不高,不利于管理和维护。因此引出多态的方法来进一步解决代码复用性不高的问题。

多态是指方法或对象具有多种形态,其建立在封装和继承的基础之上。

方法重载体现多态:传入不同参数,就调用不同方法。

方法重写体现多态:同名不同类,调用不同方法。

对象体现多态:

  1. 一个对象的编译类型和运行类型可以不一致,即可以让父类的引用名称,指向子类的对象。
  2. 编译类型在定义对象时就确定了,不可改变
  3. 运行类型可以变化的。
1
2
3
4
5
6
//Animal就是编译类型,Dog()、Cat()就是运行类型
Animal dog = new Dog();
Animal cat = new Cat();

//运行类型是可以改变的。
dog = new Cat(); //dog的运行类型由Dog变成了Cat,但编译类型仍为Animal

因此使用多态机制,可以较为方便的解决开篇的复用问题

1
2
3
4
//使用多态机制,可以统一的管理主人喂食的问题
public void feed(Animal animal, Food food) {
System.out.println("主人" + name + "给" + animal.getName() + "吃" + food.getName());
}

多态的转型:

前提:两个对象(类)存在继承关系。

(1) 向上转型:父类类型 引用名 = new 子类类型();

  1. 本质:父类引用指向了子类的对象。
  2. 在使用时可以调用父类中的所有方法(遵循访问权限),但不能调用子类的特有方法(即重写方法以外的那些方法)
  3. 向上转型时,按照从子类(运行类型)开始查找方法,没有找到则到上一级父类查找方法,先子后父,与方法调用规则一致。

(2) 向下转型:子类类型 引用名 = (子类类型) 父类引用;

  1. 本质:子类引用指向了父类的对象。
  2. 只能强转父类的引用(相当于一个名称,绑定了内存中对象的地址),不能强转父类的对象(对象已经是在内存中)。
  3. 可以要求父类引用必须指向的是当前目标类型的对象。
1
2
3
Animal animal = new Cat(); //向上转型 √
Cat cat = (Cat) animal; //向下转型 √
Dog dog = (Dog) animal; //错误的向下转型 √
  1. 当向下转型后,可以调用子类类型中的所有成员。

多态的使用细节:

  1. 属性没有重写之说,属性的值看编译类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PolyDetail02 {
public static void main(String[] args) {
Base base = new Sub(); //编译类型为Base
System.out.println(base.count); //输出的值为Base类中的10
}
}

class Base {
//父类
int count = 10;
}

class Sub extends Base {
//子类
int count = 20;
}
  1. instanceOf 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的子类型。
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
public class PolyDetail03 {
public static void main(String[] args) {
BB bb = new BB();
System.out.println(bb instanceof BB); //true
System.out.println(bb instanceof AA); //true

//aa 编译类型为 AA,运行类型为 BB AA aa = new BB();
System.out.println(aa instanceof AA); //true
System.out.println(aa instanceof BB); //true

Object obj = new Object();
System.out.println(obj instanceof AA); //false

String str = "Hello";
// System.out.println(str instanceof AA);
System.out.println(str instanceof Object); //true

}
}

class AA {
}

class BB extends AA {
}
  1. 动态绑定机制:当调用对象方法的时候,该方法会和对象的内存地址/运行类型绑定,优先调用其绑定的类型中的方法。(属性没有动态绑定机制,哪里声明,哪里使用

多态的具体应用:

  1. 多态数组

将数组的定义类型为父类类型,而数组内保存的实际元素类型为子类类型。

1
2
3
4
5
6
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18,100);
persons[2] = new Student("smith", 19, 30);
persons[3] = new Teacher("scott", 30, 20000);
persons[4] = new Teacher("king", 50, 25000);

persons 数组为 Person[] 类型,而其元素为 Person 类型的子类对象。

若想再多态数组的子类对象中调用其特有方法,则需要将该子类元素对象向下转型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//循环遍历多态数组,调用say
for (int i = 0; i < persons.length; i++) {
//person[i]编译类型是Person,巡行类型是根据实际情况JVM判断

//动态绑定,
System.out.println(persons[i].say());

//想要运行子类的特定方法,必须向下转型
if (persons[i] instanceof Student) { //判断person[i]的运行类型是否为 学生
Student student = (Student) persons[i]; //向下转型
student.study();
} else if (persons[i] instanceof Teacher) {
Teacher teacher = (Teacher) persons[i]; //向下转型
teacher.teach();
} else if (persons[i] instanceof Person) {
//不做处理
} else {
System.out.println("类型有误,请检查。");
}
}
  1. 多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型。

1
2
3
4
5
6
7
8
//调用子类方法
public void testWork(Employee e) { //形参为父类类型
if (e instanceof Worker) { //判断实参是否为工人类
((Worker) e).work(); //向下转型
} else if (e instanceof Manager) { //判断实参是否为管理类
((Manager) e).mange(); //向下转型
}
}

super 关键字

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

super 的优点:

  1. 调用父类构造器,分工明确,父类属性由父类初始化,子类属性由子类初始化
  2. 当子类中由父类的成员(属性和方法)重名时,为了访问父类的成员,必须通过 super。若没有重名,使用 super/this/直接访问 是一样的效果。

super 与 this、直接访问的区别解析:

  1. 寻找普通方法/属性时,先找本类,再找父类,没有再找父类的父类,直到 Object 类
  2. 使用 this 时,普通方法规则一致。
  3. 使用 super 时,直接跳过本类,其他与普通方法规则一致
  4. 如果在查找方法时找到了但不能访问(权限不够)则报错,没找到则提示方法不存在。
  5. super 的访问不限于直接父类,如果和多个上级类都有同名成员,则遵循就近原则。
区别thissuper
属性访问本类属性,如果没有则从父类中查找。直接访问父类中的属性。
方法访问本类方法,如果没有则从父类中查找。直接访问父类中的方法。
构造器调用本类构造器,且只能放在构造器的首行。调用父类构造器,且必须放在子类构造器的首行。
意义表示当前对象。表示父类对象。

override 方法重写

当子类有一个方法与父类(或更高级)的某个方法名称、返回类型、形参列表相同,那么就称子类的这个方法覆盖了父类的方法。即外壳不变,核心重写

方法重写使用细节:

  1. 子类的方法的形参列表、方法名称要和父类方法的形参列表、方法名称完全相同
  2. 子类的返回类型要和父类返回类型相同,或者是父类返回类型的子类,如父类返回类型为 Object,子类返回类型为 String。
  3. 子类方法不能缩小父类方法的访问权限,但是可以扩大。

方法重载和重写的比较:

名称发生范围方法名参数列表返回类型修饰符
重载 overload本类必须一样类型、个数和顺序至少有一个不同无要求无要求
重写 override父子类必须一样相同子类重写的方法,返回的类型和父类返回的类型须一致,或者是其返回类型的子类子类方法不能缩小父类方法的访问范围,但是可以扩大

Object 类

equals()

== 与 equals 的对比,

== 是一个比较运算符:

  1. 既可以判断基本类型,也可以判断引用类型。
  2. 如果判断基本类型,判断的是值是否相等
  3. 如果判断引用类型,判断的是地址是否相等(即判定是否是同一个对象)。
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
package com.laelwz.Object_;

public class Equals01 {
public static void main(String[] args) {
A a = new A();
A b = a;
A c = b;

System.out.println(a == c); //true
System.out.println(b == c); //true

B bObjc = a;
//指向同一个对象空间,equals就认为相同
System.out.println(bObjc == c); //true

int num1 = 10;
double num2 = 10.0;
System.out.println(num1 == num2); //true 基本数据类型,判断值是否相同

}
}

class B {
}

class A extends B {
}

equals 是 Object 类中的方法:

  1. 只能判断引用类型(如 String)。
  2. 默认判断地址是否相等,但子类中往往重写该方法,用于判断内容是否相等(如 Integer 类型和 String 类型中)。
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
System.out.println("Object的equals:");
Object Obj1 = new Object();
Object Obj2 = new Object();
System.out.println(Obj1 == Obj2); //false 对象地址不同
System.out.println(Obj1.equals(Obj2)); //false Object的equals默认也只判断地址

/*
Object的equals方法源码,只判断是否地址相同

public boolean equals(Object obj) {
return (this == obj);
}
*/
System.out.println("String的equals:");
String str1 = new String("哈哈");
String str2 = new String("哈哈");
System.out.println(str1 == str2); //false 对象地址不同
System.out.println(str1.equals(str2)); //true 内容相同

/*
String的equals方法源码,String继承于Object,
重写了Object的equals方法,
用于判断字符串的值是否相等

public boolean equals(Object anObject) {
if (this == anObject) { //如果是同一个对象
return true; //返回true
}
if (anObject instanceof String) { //判断类型是不是String
String aString = (String)anObject; //向下转型
if (coder() == aString.coder()) { return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value); }}
return false;
}
*/
Integer integer1 = new Integer(6);
Integer integer2 = new Integer(6);

System.out.println("Integer的equals:");
System.out.println(integer1 == integer2); //false 对象地址不同
System.out.println(integer1.equals(integer2)); //true 值相同

/*
Integer也重写了Object的equals方法
变为判断两个值是否相同

public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();}
return false;
}
*/

重写 equals 方法实例:

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
package com.laelwz.Object_;

public class EqualsExercise01 {
public static void main(String[] args) {
Person person1 = new Person("jack", 10, '男');
Person person2 = new Person("jack", 10, '男');

//默认为假,继承Object的equals方法,值判断地址是否相等
System.out.println(person1.equals(person2));
}
}

class Person{
private String name;
private int age;
private char gender;

//重写Object的equals方法
public boolean equals(Object obj) {
//判断如果比较的两个对象是同一个对象,则直接返回true
if (this == obj) {
return true;
}
//类型判断
if (obj instanceof Person) { //是Person,我们才比较
//进行向下转型,因为需要得到obj的各个特有属性
Person p = (Person) obj;
//返回各个属性的比较结果,各个属性的内容一致则返回true
return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
}
//如果不是Person,则直接返回false
return false;
}

public Person(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public char getGender() {
return gender;
}

public void setGender(char gender) {
this.gender = gender;
}
}

hashCode()

  1. hashCode() 方法能够提高具有哈希结构的容器的效率。
  2. 如果多个引用指向同一个对象,则其哈希值一定一致
  3. 如果多个引用指向不同对象,则哈希值不同
  4. 哈希值是基于地址来编写的,但不等价于地址
  5. 如果需要,则可以重写 hashCode() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.laelwz.Object_;

public class HashCode_ {
public static void main(String[] args) {
AA aa1 = new AA();
AA aa2 = new AA();
AA aa3 = aa1;

System.out.println(aa1.hashCode());
System.out.println(aa2.hashCode());
System.out.println(aa3.hashCode());
}
}
class AA{}

toString()

  1. toString()方法默认返回 全类名+@+哈希值的十六进制
1
2
3
4
5
6
7
//Object的toString()源码

//1. getClass().getName() 类的全类名(包名+类名)
//2. Integer.toHexString(hashCode()) 将对象的hashCode值转换为16进制字符串

public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());}
  1. 子类往往重写 toString()方法,用于返回对象的属性信息。
1
2
3
4
5
6
7
8
@Override
public String toString() { //重写后,一般是把对象的属性值输出。
return "Monster{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
", sal=" + sal +
'}';
}
  1. 当直接输出一个对象时,toString 方法默认调用。
1
2
3
4
Monster monster = new Monster("小妖怪", "巡山", 1000);
//两行输出等价
System.out.println(monster.toString());
System.out.println(monster);

finalize()

  1. 当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作。
  2. 回收的前提:对象没有任何引用,jvm 认为这是一个垃圾对象,就会调用垃圾回收机制来销毁该对象,在销毁前,会先调用 finalize 方法
  3. 垃圾回收机制的调用是由系统决定的,也可以通过 System.gc() 主动出发垃圾回收机制。
  4. Java18 中已彻底弃用,该方法仅作了解

Debug 断点调试

断点调试是指在程序的某一行设置一个断点,调试时运行到这一行就会停住,然后可以一步一步往下调试,过程中可以看到各个变量当前值,出错的话就会显示错误并停在错误行。

在实际开发中,可以使用断点调试功能来一步一步的查看源码执行过程,从而发现问题所在。

也可以通过断点调试来查看 java 底层源代码的执行过程,提升水平。

注意: 断点调试过程中是运行状态,是以对象的运行类型来执行的。

Debug 快捷键参见 IDEA 使用


评论