关于Java基础的杂记
第一章 序
万丈高楼平地起。
第二章 概述
java 转义字符
\\t :一个制表位,实现对齐功能
\\n :换行符
\\\ :一个真实的斜杠
\\” : 一个真实的双引号
\\’ : 一个真实的单引号
\\r :一个回车,没有换行,将光标置于最前,逐个输出\r 后的字符
注释
代码规范
- 类、方法的注释,要以 javadoc 的方式来写。
- 非 javadoc 注释着重告诉维护者如何修改、为什么这样写,以及注意事项。
- 运算符和 = 两边加一个空格。
- 实际工作使用 UTF-8 编码格式。
- 行宽不超过 80 字符。
- 代码编写使用行尾风格或次行风格。
- 一段代码一个模块,尽量只写一个功能,避免混乱。
JDK、JRE、JVM 之间关系
- JDK = JRE + JAVA 开发工具
- JRE = JVM + 核心类库
第三章 变量
变量注意事项
- int 4 个字节、double 8 个字节,每个类型占用空间不同。
- 变量必须先声明,后使用。
- 变量在同一个作用域中不可重名。
- 变量 = 变量名 + 值 + 数据类型 (三要素)。
+ 号
- 当 + 左右两边有一方为字符串,则做拼接运算。
- 两边均为数值类型,则做加法运算。
- 运算顺序为从左到右。
数据类型
- 数值型
- 整数:byte[1]、short[2]、int[4]、long[8]
- 浮点数(小数):float[4]、double[8]
- 字符型
- 布尔型
整型使用细节
java 整型常量默认为 int 类型,声明 long 型常量须后加 ‘l’ 或 ‘L’
bit:计算机中最小存储单位
byte:计算机中基本存储单位
- 1 byte = 8 bit
浮点型使用细节
- 默认为 double 类型,声明 float 型单精度常量须后加 ‘f’ 或 ‘F’
- 表示形式:
- 十进制
- 科学计数法,如:5.12e10 = 5.12 x 1010
- 通常情况下应该使用 double,因为它比 float 更精确,而 float 会损失一些小数位
字符型使用细节
- 字符常量用单引号括起单个字符
- 允许使用转义字符
- char 的本质是一个整数,所以可以直接给 char 赋值整数,输出时是对应 unicode 对应字符
- char 类型可以进行运算
基本数据类型的转换
精度小的类型自动转换为精度大的数据类型
char int long float double
byte short int long float double
有多种类型的数据混合运算时,系统首先自动将所有数据转换成容量最大的数据类型,再进行计算
精度大的赋值给精度小的数据类型就会报错
(byte, short) 和 char 之间不会相互自动转换
byte, short, char 三者之间可以计算,在计算时首先转换为 int 类型
boolean 不参与类型自动转换
强制转换细节
- 需要数据从大到小时,就需要使用强制转换
字符串转换基本类型
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 2 3 4 5 6
|
System.out.println(10 % 3); System.out.println(-10 % 3); System.out.println(10 % -3); System.out.println(-10 % -3);
|
++ 自增 / — 自减
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int i = 10; i++; ++i; System.out.println("i=" + i);
int j = 8;
int k = j++; System.out.println("k=" + k + "\nj=" + j);
|
逻辑运算符
逻辑与 &
对于逻辑与,第一个条件为 false,后面条件仍会判断执行
短路与 &&
对于短路与,第一个条件为 false,后面条件不再判断执行
逻辑或 |
对于逻辑或,第一个条件为 true,后面条件仍会判断执行
逻辑或 ||
对于短路或,第一个条件为 true,后面条件不再判断执行
异或操作 ^
a ^ b,如果 a 与 b 结果不同,则为 true,否则为 false
取反操作 ~
对参数二进制格式进行 01 取反
三元运算符
条件表达式 ? 表达式1 : 表达式2
- 如果条件表达式为 true,运算后的结果是表达式 1
- 如果条件表达式为 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);
|
原码、反码、补码
重要:负数的计算,需要先从原码转换为反码,反码再转换为补码再进行位运算,运算完毕后,再依次转回反码、原码。
- 二进制的最高位是符号位:0 表示正数,1 表示负数。
- 正数源码、反码、补码都一样。
- 负数的 反码 = 它的原码符号位不变,其他位取反。
- 负数的 补码 = 反码 + 1,反码 = 补码 -1。
- 0 的反码,补码都是 0。
- java 没有无符号数。
- 计算机运算都以补码方式运算。
- 看运算结果的时候,要看它的原码。
位运算符
按位与 &
两者全为 1,结果为 1,否则为 0。
1 2 3 4 5 6 7 8
|
System.out.println(2&3);
|
按位或 |
两位有一个为 1,结果为 1,否则为 0
1 2 3 4
|
System.out.println(2|3);
|
按位异或 ^
两位有一个为 0,一个 1,结果为 1,否则为 0
1 2 3 4
|
System.out.println(2^3);
|
按位取反 ~
0 -> 1,1 -> 0
1 2 3 4 5 6 7 8 9 10 11 12
|
System.out.println(~-2);
System.out.println(~2);
|
算数左移运算符 <<
符号位不变,按二进制位数左移指定位数,低位补 0。
算数右移运算符 >>
低位溢出,符号位不变,并用符号位补溢出的高位。
逻辑右移操作符 >>>
低位溢出,高位补 0。
标识符的命名规则和规范
^cd9c26
标识符概念
- Java 中对各种变量、方法和类等命名时使用的字符序列称为标识符
- 凡是自己起名字的地方都称为标识符
命名规则
- 由 26 个英文字母大小写,0-9,或$组成
- 不可以使用数字开头
- 不可以使用关键字和保留字,但可以包含
- 严格区分大小写,长度无限制
- 标识符不能包含空格
包名
多单词组成时所有字母小写
例:com.hahah.cpm
类名 、接口名
多单词组成时,所有单词首字母大写 [大驼峰]
例:TankShotGame
变量名、方法名
多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写 [小驼峰]
例:tankShotGame
常量名
所有字母都大写,多个单词时用下划线连接
例:定义一个所得税税率 TAX_RATE
进制
二进制
0,1
,满 2 进 1,以 0b
或 0B
开头
转八进制
从低位(右)开始,将二进制数每三位一组,转成对应八进制数。
例: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,以0x
或0X
开头
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,以此类推。
特别说明:
- 多分支可以没有 else。
- 如果所有表达式都不成立,则默认执行 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(表达式) { case 值1: 语句块1; break; case 值2: 语句块2; break; … case 值n: 语句块n; break; default: 语句块n+1; break; }
|
switch 细节讨论:
- 表达式数据类型应与
case
后的常量类型一致,或者是能够自动转换或比较的类型。如char
与int
类型可以相互转换。 - switch 中的表达式的返回值必须是:byte, short, int, char, enum[枚举], String 中的一种。
case
子句中的值必须是常量(1, ‘a’)或者是常量表达式,不能是变量。default
的子句是可选的,当没有匹配的case
时,执行default
。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
| 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 的比较
- 如果判断具体数值不多,并且符合 byte, short, int, char, enum[枚举], String 这六种类型,建议使用 switch 语句。
- 对于区间判断、结果为 boolean 类型的判断,使用 if 的范围更广。
循环控制
for
1 2 3
| for(循环变量初始化; 循环条件; 循环变量迭代) { 语句块; }
|
for 循环细节:
循环条件应返回一个布尔值的表达式。
初始化变量和变量迭代可以写到其他地方,分号不能省略。
1 2 3 4 5
| int i = 1; for (; i <= 10;) { System.out.println("你好" + i); i++; }
|
- 循环变量初始值可以有多条初始化语句,但是要求类型一致,循环遍历迭代同理。
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
|
int totalLevel = 54;
for (int i = 1; i <= totalLevel; 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
|
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; } System.out.println("i = " + i); } }
|
continue
countinue 与 break 的区别在于,countinue 并不是终止整个循环,而是中止最近的一次循环的当次迭代。
注意:
- continue 语句只能用在 while 语句、for 语句或者 foreach 语句的循环体之中,在这之外的任何地方使用它都会引起语法错误。
- 终止指的是结束整个循环,中止指的是结束循环的当次迭代(跳过)。
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 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; } 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);
|
- double[] 表示该数组为 double 类型的数组,数组名 hens。
- {3, 5, 1, 3.4, 2, 50}表示该数组的值/元素,依次表示数组的第几个元素。
- 通过 数组名[下标] 来访问数组的元素,下标从 0 开始编号,第一个元素就是 hens[0],第二个元素就是 hens[1],以此类推。
动态初始化:
- 直接定义数组大小类型。
数据类型[] 数组名 = new 数据类型[大小]
- 先声明数组,再 new 分配空间。
1 2 3 4 5 6
| int[] a;
a = new int[3];
|
- 静态初始化
数据类型[] 数组名 = {元素值1, 元素值2...}
1 2
| double[] hens = {3, 5, 1, 3.4, 2, 50};
|
数组使用细节:
- 数组元素可以是任何数据类型,但是不能混用。
- 数组创建后如果没有赋值,会有默认值,char 为
\u0000
,String 为 null,boolean 为 false。 - 数组属于引用类型,数组型数据是对象()。
数组赋值机制(重要):
- 基本数据类型赋值,这个值就是具体的数据(栈),而且相互不影响。
1
| int n1 = 2; int n2 = n1;
|
- 数组在默认情况下是引用转递,赋的值是地址(堆),实际数据存放在堆中相应地址空间。
1 2
| int [] arr1 = {1, 2, 3}; int [] arr2 = arr1;
|
- 数组赋值手动内容拷贝方式,数据传递,非引用传递。
1 2 3 4 5 6 7 8 9 10
| int[] arr1 = {10, 20, 30};
int[] arr2 = new int[arr1.length];
for (int i = 0; i < arr1.length; i++) { arr2[i] = arr1[i]; }
|
数组操作:
- 翻转
方式一:元素逐个交换
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; System.out.println(Arrays.toString(arr));
|
- 扩容
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 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 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(); }
|
- 先声明,再 new 分配空间。
1 2 3 4
| int[][] a;
a = new int[3][2];
|
列数不确定
在多维数组中,列数可以不同。
并且通过先声明,再分配空间的方式,有效节约不需要使用的一维数组(列)所占空间大小。
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++) { 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, 元素值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
|
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++) { 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 2 3 4 5 6
| String cat1 = "小白"; String cat2 = "小花"; int age1 = 3; int age2 = 100; String color1 = "白色"; String color2 = "花色";
|
使用数组:
(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) {
Cat catx1 = new Cat(); catx1.name = "小白"; catx1.age = 3; catx1.color = "白色"; catx1.weight = 20;
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); } }
class Cat { String name; int age; String color; double weight;
}
|
Java 内存的结构:
- 栈:一般存放基本数据类型(局部变量)。
- 堆:存放对象(Cat cat,数组等)。
- 方法区:存放常量池(常量,如字符串)、类加载信息。
对象在内存中的存在形式:
创建对象后,在栈中的对象名称指向一个地址,这个地址在堆中存放了这个对象,其中包含各个地址,这些地址分别指向方法区中常量池里的各个具体的属性值。
需要注意的是,如果对象的属性是基本数据类型(非 String),就直接存放在堆的对象中,而不会指向方法区。
在创建对象的过程中(new),在方法区中会加载该类信息,包含属性信息和方法信息。
属性 / 成员变量细节:
- 成员变量 = 属性 = field(字段)
- 属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)。
- 属性的定义语法和变量相同:
访问修饰符 属性类型 属性名
。(访问修饰符用于控制属性的访问范围:public,protected,默认,private,会在认识包后进行系统学习) - 属性如果不赋值,会有默认值,规则与数组一致(char 为
\u0000
,String 为 null,boolean 为 false)。 - 对象赋值给对象,也是与数组一致的通过改变地址指向,而非改变内容,因此赋值后两个对象指向同一个地址。
创建对象方式:
- 先声明,再创建
1 2
| Cat cat; cat = new Cat();
|
- 直接创建
创建对象流程分析:
- 加载对象的类信息(属性和方法的信息,且只会加载一次)。
- 在堆中分配空间,进行默认初始化(属性赋予默认值)。
- 把堆中的对象的地址赋给栈中的对象名称,此时对象名称指向了相应的对象。
- 进行指定初始化,如
对象.属性 = XXX
。
成员方法
某些情况下,我们需要指定一个对象的行为,即成员方法(简称方法),如人类,除了有属性(年龄、性别等)之外,还会有一些行为。
成员方法使用:
- 在类中创建方法。
- 创建对象,调用方法。
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;
public void speak() { System.out.println("我是一个好人。"); } }
|
方法可以接收外部信息,经过处理后,可以再传出。
getSum 成员方法,可以计算两个数的和:
1 2 3 4 5 6 7 8 9
|
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);
|
成员方法调用机制:
- 当程序执行到方法时,就会开辟一个独立的栈空间(还在 main 主方法栈空间内部)。
- 当方法执行完毕,或者执行到 return 语句时,就会返回到调用方法的地方(main 栈)。
- 返回后,继续执行犯法后面的代码。
- 当 main 主方法(栈)执行完毕,程序退出。
使用成员方法的优点:
- 提高代码复用性。
- 可以将实现的细节封装,然后供其他用户来调用。
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}};
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) { 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 返回值; }
|
- 返回数据类型:表示成员方法食醋胡,void 表示没有返回值。
- 参数列表:表示成员方法输入(如 cal(int n),n 就是输入的数据)。
- 方法体:表示为了实现某一功能的代码块。
- return 语句不是必须的。
成员方法使用细节:
- 访问修饰符如果不写则是默认访问,有四种:public, protected, 默认, private。
- 一个方法最多有一个返回值,可使用数组来解决返回多个值问题。
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) { 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; } }
|
- 如果方法要求有返回数据类型,则方法体中的最后的执行语句必须为
return 值;
,并且要求返回值得类型必须和 return 的值类型一致或兼容。 - 如果方法是的返回数据类型为 void,则方法体中可以没有 return 语句,或者只写
return;
。 - 方法名遵循小驼峰命名法,见名知意,如得到两个数的和则以此命名:getSum。
形参列表使用细节:
- 一个方法可以有 0 个或多个参数。
- 参数类型可以为任意类型。
- 调用带参数的方法时,要对应传入相同或兼容的参数。
- 方法定义时的参数称为为形式参数(形参),方法调用的时候参数称为实际参数(实参),实参和形参的类型要一致或兼容,个数、顺序必须一致。
方法调用细节:
- 同一个类中的方法:可以直接在自己的方法体相互去调用。
- 不同类中的方法:需要通过对象名来调用。
- 跨类的方法调用和方法的访问修饰符相关,后面细说。
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() { System.out.println("继续执行sayOk()。"); }
public void m1() { 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 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; AAp obj = new AAp(); obj.swap(a,b);
System.out.println("主方法 a=" + a + "\tb=" + b); } }
class AAp { public void swap (int a, int b) { System.out.println("\na和b交换前的值 a=" + a + "\tb=" + b);
int tmp = a; a = b; b = tmp; System.out.println("\na和b交换后的值 a=" + a + "\tb=" + b);
} }
|
- 引用数据类型(数组、对象):传递的是地址(指向堆中实际的数组空间),形参与实参共享一个地址,可以通过形参影响实参。
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
| public int factorial(int n) { if (n == 1) { return 1; } else { return factorial(n - 1) * n; } }
|
递归原则:
- 执行一个方法时,就会创建一个新的受保护的独立空间(栈空间)。
- 方法的局部变量是独立的,不会相互影响,如 n 变量。
- 方法中的引用类型变量(数组,对象)之间会共享该引用类型的数据。
- 递归必须向退出递归的条件逼近,否则会死循环,无限递归。
- 当一个方法执行完毕,或者遇到了 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 { public void move(int num, char a, char b, char c) { if (num == 1) { System.out.println(a + " => " + c); } else {
move(num - 1, a, c, b); System.out.println(a + " => " + 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++){ if(arr[i] == arr[index] || Math.abs(arr[index]-arr[i]) == Math.abs(index -i)){ return false; } } return true; }
public void put(int index){ if(index == 8){ print(); }else{ for(int i = 0; i < 8; i++){ arr[index] = i; if(judge(index)){ 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) { System.out.println(100); System.out.println("hello, world"); System.out.println(1.1); System.out.println(true); } }
|
如果没有方法的重载,那么光一个 println 方法,需要打印不同的类型的话,岂不是需要 printInt、printChar……这么多个不同名称的方法了。
因此重载的好处有:
- 减轻了起名的麻烦。
- 减少了记名的麻烦。
- 利于接口编程。
通过重载创建四个不同的 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; }
public double calculate(int n1, double n2) { return n1 + n2; }
public double calculate(double n1, int n2) { return n1 + n2; }
public int calculate(int n1, int n2, int n3) { return n1 + n2 + n3; } }
|
调用方法时,只需要填入相应的形参列表,就可以选择对应的方法了。例如调用两个整数的和的方法,只需要calculate(1, 1)
,两个参数都为 Int 类型即可。
方法重载使用细节:
- 方法名称必须相同。
- 方法形参列表必须不同(形参类型、个数和顺序至少有一样不同,参数名无所谓)。
- 方法的返回值可以不同。
- 重载中方法形参类型优先级高于自动类型转换。
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 {
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); } }
|
在可变参数中,形参列表作为数组引入方法过程,唯一的参数名就是数组名称,数组的内容就是各个参数值。
可变参数使用细节:
- 可变参数的实参可以为 0 或任意多个。
- 可变参数的实参可以为数组。
- 可变参数的本质就是数组
- 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数放在最后。
1
| public void f2(String str, double... nums) {}
|
- 一个形参列表中只能出现一个可变参数。
作用域
- 在 Java 中,主要的变量就是属性(成员变量)和局部变量。
- 局部变量一般是指在成员方法中定义的变量。
- 作用域分为全局变量(属性)与局部变量,全局变量顾名思义,作用域整个类中的所有方法,局部变量就是除了属性之外的其他变量,作用域为定义它的代码块。
- 全局变量(属性)可以不赋值,直接使用,因为会有默认值,但是局部变量必须赋值后才能使用,因为没有默认值。
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
| [修饰符] 方法名(形参列表) { 方法体; }
|
- 构造器的修饰符可以默认,也可以是 public/protected/private。
- 构造器没有返回值。
- 构造器的方法名和类名必须一致。
- 参数列表与成员方法规则一致。
- 构造器的调用由系统完成。
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) { 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;
public Person4(String pName, int pAge) { System.out.println("构造器被调用,完成对象的初始化。"); name = pName; age = pAge; } }
|
构造器使用细节:
- 一个类可以定义多个构造器,即构造器的重载。
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; } }
|
- 构造器是完成对象的初始化,并不是创建对象。
- 在创建对象时,系统自动调用该类的构造方法。
- 如果程序员未定义构造器,系统会自动给类生成一个默认无参构造器,如
Person(){}
。 - 如果自己定义了构造器,默认的无参构造器就被覆盖了,除非显示的定义:
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; } }
|
- 加载 Person 类信息(Person.class),只会加载一次。
- 在堆中分配空间(地址)。
- 完成对象初始化:
3.1 默认初始化:age=0 name=null
3.2 显式初始化:age=90, name=null
3.3 构造器的初始化:age=20, name = “xxx” - 把对象在堆中的地址,返回给 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;
public Dog(String name, int age) {
System.out.println("this的hashCode值:" + this.hashCode());
}
public void info() { System.out.println(name + "\t" + age + "\t"); } }
|
可见两个对象的地址和其 this 的地址输出一致。
this 使用细节:
- this 可以用来访问本类的属性、方法、构造器。
- this 用于区分当前类的属性和局部变量。
- 访问成员方法的语法:
this.方法名(参数列表)
。 - 访问构造器的语法:
this(参数列表)
(只能在构造器中访问另一个构造器,且必须放在结构体的第一句)。 - this 不能在类的外部使用,只能在类定义的方法中使用。
- 传统方式调用类的成员变量,会遵循就近原则,如果在方法内定义过了和成员变量相同名称的变量,那么只会调用到方法内的变量,而不会是方法外的成员变量。但使用 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 | 自动分配变量名 |
F8 | Step Over 跳过当前 Debug 语句 |
F7 | Step Into 跳入当前 Debug 自定义的方法 |
Alt+ Shift +F7 | Force Step Into 强制跳入,可以进入任何方法 |
Shift + F8 | Step Out 跳出当前 Debug 方法 |
F9 | Resume Program 继续执行到下一个断点 |
包
三大作用:
- 区分相同名字的类。
- 当类很多时,可以方便的对类进行管理。
- 控制访问范围。
基本语法:package com.laelwz
package 关键字
表示打包,com.laelwz
表示包名。
原理: 实际上就是创建不同的文件夹来保存类文件,画出示意图。
包的命名:
- 命名规则:只能包含数字、字母下划线、小圆点
.
,不能用数字开头,不能是保留字或关键字。 - 命名规范:小写字母+小圆点,一般为
com.公司名.项目名.业务模块名
。
例如:com.laelwz.oa.model
、com.laelwz.oa.controller
常用包:
java.lang.*
基本包,默认引入,无需手动引入。java.util.*
系统提供的默认工具包、工具类(Scanner 之类)。java.net.*
网络包,网络开发使用。java.awt.*
界面开发,GUI 使用。
包的引用方法: import 包;
1 2 3 4
| import java.util.Arrays;
import java.util.*;
|
包使用细节:
package
的作用是声明当前类所在的包,需要放在类的最上面,一个类最多只有一句。import
放在package
之后,在类定义之前,可以有多句,没有顺序要求。
访问修饰符
Java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):
- 公开:public 修饰,对外公开。
- 受保护:protectd 修饰,对子类和同一个包中的类公开。
- 默认:没有修饰,只向同一个包的类公开。
- 私有:private 修饰,只有类本身可以访问,不对外公开。
访问级别 | 修饰符 | 同类 | 同包 | 子类 | 不同包 |
---|
公开 | public | √ | √ | √ | √ |
受保护 | protected | √ | √ | √ | X |
默认 | 没有修饰符 | √ | √ | X | X |
私有 | private | √ | X | X | X |
访问修饰符使用细节:
- 修饰符可以用来修饰类中的属性、成员方法以及类。
- 类只有默认与 public 修饰符。
- 成员方法的访问规则和属性完全一致。
OOP 三大特征
封装
把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法),才能对数据进行操作。
封装的作用:
- 隐藏实现细节:调用方法,只要求传入参数,返回结果。
- 可以对数据进行验证,保证安全。
封装的使用:
- 将属性私有化(private),意味着不能直接在外部直接修改属性。
提供一个公共的 set 方法,用于属性的判断与赋值。
1 2 3 4
| public void setXxx(类型 参数名) { 属性 = 参数名; }
|
提供一个公共的 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;
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) {
setName(name); setAge(age); setSalary(salary); }
|
继承
当多个类存在相同的属性和方法,会导致代码的冗余,这时可以使用继承的方法来提高代码效率。
通过继承,从这些相似的类中抽象出父类(基类/超类),在父类中定义这些相同的属性和方法,子类就可以直接继承到父类的这些属性和方法,而不需要每个子类都再重新定义一遍。
基本语法:
1 2 3
| class 子类 extends 父类 {
}
|
继承的优点:
- 代码复用性提高。
- 代码的扩展性和可维护性提高。
继承使用细节:
- 子类能够继承父类所有属性和方法,但是父类的私有属性不能在子类直接访问,需要通过父类提供的公共方法间接访问。例如用一个公共 public 权限的方法去返回一个私有 private 权限的属性。
- 子类必须调用父类的构造器,完成父类的初始化。( 默认执行
super()
方法,会自动调用父类的无参构造器) - 当创建子类对象时,无论使用子类的任何构造器,都会默认去调用父类的无参构造器(无参构造器默认存在),如果父类没有提供无参构造器,则必须在子类的构造器中使用
super()
方法去指定使用父类的任意构造器,以此完成父类的初始化工作,否则无法通过编译。 - 指定调用父类的某个构造器,只需要显式的使用
super(参数列表)
调用即可,这里的 super 就是指向父类的构造器。 super()
方法必须放在构造器的第一行,且只能在构造器中使用。- super 与 this 在构造器中只能二选一,不可同时出现。
- Java 中所有类都是 Object 类的子类,Object 是所有类的基类。
- 父类构造器的调用不限于上一级父类,会一层一层追溯到最顶层的 Object 类,每个父类的构造器都会被调用。
- 子类最多只能继承一个父类(直接继承),即单继承机制。
- 不可滥用继承,子类和父类之间必须满足 is-a 的逻辑关系
1 2
| Xiaoming is a Person => Xiaoming extends Person Cat is a Animal => Cat extends Animal
|
继承在内存中的分析:
- 当一个对象创建,首先从该对象的类的最顶层父类 Obeject 开始逐一加载父类与子类,这些类信息都加载在内存方法区中。
- 在堆中开辟一个属于该对象的内存空间。
- 在该空间内,根据父类子类的顺序从父到子依次写入每个类的属性、方法(基本数据类型直接写入数据,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
| package com.laelwz.extend_;
public class ExtendsTheory { public static void main(String[] args) { Son son = new Son();
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 = "大头爸爸";
int age = 23; }
class Son extends Father { String name = "大头儿子";
}
|
继承设计的基本思想: 父类的构造器完成父类属性初始化,子类的构造器完成子类属性初始化。
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 4 5 6
| Animal dog = new Dog(); Animal cat = new Cat();
dog = new Cat();
|
因此使用多态机制,可以较为方便的解决开篇的复用问题:
1 2 3 4
| public void feed(Animal animal, Food food) { System.out.println("主人" + name + "给" + animal.getName() + "吃" + food.getName()); }
|
多态的转型:
前提:两个对象(类)存在继承关系。
(1) 向上转型:父类类型 引用名 = new 子类类型();
- 本质:父类引用指向了子类的对象。
- 在使用时可以调用父类中的所有方法(遵循访问权限),但不能调用子类的特有方法(即重写方法以外的那些方法)。
- 向上转型时,按照从子类(运行类型)开始查找方法,没有找到则到上一级父类查找方法,先子后父,与方法调用规则一致。
(2) 向下转型:子类类型 引用名 = (子类类型) 父类引用;
- 本质:子类引用指向了父类的对象。
- 只能强转父类的引用(相当于一个名称,绑定了内存中对象的地址),不能强转父类的对象(对象已经是在内存中)。
- 可以要求父类引用必须指向的是当前目标类型的对象。
1 2 3
| Animal animal = new Cat(); Cat cat = (Cat) animal; Dog dog = (Dog) animal;
|
- 当向下转型后,可以调用子类类型中的所有成员。
多态的使用细节:
- 属性没有重写之说,属性的值看编译类型。
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(); System.out.println(base.count); } }
class Base { int count = 10; }
class Sub extends Base { int count = 20; }
|
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); System.out.println(bb instanceof AA);
System.out.println(aa instanceof AA); System.out.println(aa instanceof BB);
Object obj = new Object(); System.out.println(obj instanceof AA);
String str = "Hello";
System.out.println(str instanceof Object);
} }
class AA { }
class BB extends AA { }
|
- 动态绑定机制:当调用对象方法的时候,该方法会和对象的内存地址/运行类型绑定,优先调用其绑定的类型中的方法。(属性没有动态绑定机制,哪里声明,哪里使用)
多态的具体应用:
- 多态数组
将数组的定义类型为父类类型,而数组内保存的实际元素类型为子类类型。
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
| for (int i = 0; i < persons.length; i++) {
System.out.println(persons[i].say());
if (persons[i] instanceof Student) { 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 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 的优点:
- 调用父类构造器,分工明确,父类属性由父类初始化,子类属性由子类初始化
- 当子类中由父类的成员(属性和方法)重名时,为了访问父类的成员,必须通过 super。若没有重名,使用 super/this/直接访问 是一样的效果。
super 与 this、直接访问的区别解析:
- 寻找普通方法/属性时,先找本类,再找父类,没有再找父类的父类,直到 Object 类
- 使用 this 时,普通方法规则一致。
- 使用 super 时,直接跳过本类,其他与普通方法规则一致
- 如果在查找方法时找到了但不能访问(权限不够)则报错,没找到则提示方法不存在。
- super 的访问不限于直接父类,如果和多个上级类都有同名成员,则遵循就近原则。
区别 | this | super |
---|
属性 | 访问本类属性,如果没有则从父类中查找。 | 直接访问父类中的属性。 |
方法 | 访问本类方法,如果没有则从父类中查找。 | 直接访问父类中的方法。 |
构造器 | 调用本类构造器,且只能放在构造器的首行。 | 调用父类构造器,且必须放在子类构造器的首行。 |
意义 | 表示当前对象。 | 表示父类对象。 |
override 方法重写
当子类有一个方法与父类(或更高级)的某个方法名称、返回类型、形参列表相同,那么就称子类的这个方法覆盖了父类的方法。即外壳不变,核心重写。
方法重写使用细节:
- 子类的方法的形参列表、方法名称要和父类方法的形参列表、方法名称完全相同。
- 子类的返回类型要和父类返回类型相同,或者是父类返回类型的子类,如父类返回类型为 Object,子类返回类型为 String。
- 子类方法不能缩小父类方法的访问权限,但是可以扩大。
方法重载和重写的比较:
名称 | 发生范围 | 方法名 | 参数列表 | 返回类型 | 修饰符 |
---|
重载 overload | 本类 | 必须一样 | 类型、个数和顺序至少有一个不同 | 无要求 | 无要求 |
重写 override | 父子类 | 必须一样 | 相同 | 子类重写的方法,返回的类型和父类返回的类型须一致,或者是其返回类型的子类 | 子类方法不能缩小父类方法的访问范围,但是可以扩大 |
Object 类
equals()
== 与 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
| 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); System.out.println(b == c);
B bObjc = a; System.out.println(bObjc == c);
int num1 = 10; double num2 = 10.0; System.out.println(num1 == num2);
} }
class B { }
class A extends B { }
|
equals 是 Object 类中的方法:
- 只能判断引用类型(如 String)。
- 默认判断地址是否相等,但子类中往往重写该方法,用于判断内容是否相等(如 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); System.out.println(Obj1.equals(Obj2));
System.out.println("String的equals:"); String str1 = new String("哈哈"); String str2 = new String("哈哈"); System.out.println(str1 == str2); System.out.println(str1.equals(str2));
Integer integer1 = new Integer(6); Integer integer2 = new Integer(6);
System.out.println("Integer的equals:"); System.out.println(integer1 == integer2); System.out.println(integer1.equals(integer2));
|
重写 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, '男');
System.out.println(person1.equals(person2)); } }
class Person{ private String name; private int age; private char gender;
public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person) obj; return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender; } 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()
- hashCode() 方法能够提高具有哈希结构的容器的效率。
- 如果多个引用指向同一个对象,则其哈希值一定一致。
- 如果多个引用指向不同对象,则哈希值不同。
- 哈希值是基于地址来编写的,但不等价于地址。
- 如果需要,则可以重写 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()
- toString()方法默认返回 全类名+@+哈希值的十六进制。
1 2 3 4 5 6 7
|
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode());}
|
- 子类往往重写 toString()方法,用于返回对象的属性信息。
1 2 3 4 5 6 7 8
| @Override public String toString() { return "Monster{" + "name='" + name + '\'' + ", job='" + job + '\'' + ", sal=" + sal + '}'; }
|
- 当直接输出一个对象时,toString 方法默认调用。
1 2 3 4
| Monster monster = new Monster("小妖怪", "巡山", 1000);
System.out.println(monster.toString()); System.out.println(monster);
|
finalize()
- 当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作。
- 回收的前提:对象没有任何引用,jvm 认为这是一个垃圾对象,就会调用垃圾回收机制来销毁该对象,在销毁前,会先调用 finalize 方法。
- 垃圾回收机制的调用是由系统决定的,也可以通过
System.gc()
主动出发垃圾回收机制。 - Java18 中已彻底弃用,该方法仅作了解。
Debug 断点调试
断点调试是指在程序的某一行设置一个断点,调试时运行到这一行就会停住,然后可以一步一步往下调试,过程中可以看到各个变量当前值,出错的话就会显示错误并停在错误行。
在实际开发中,可以使用断点调试功能来一步一步的查看源码执行过程,从而发现问题所在。
也可以通过断点调试来查看 java 底层源代码的执行过程,提升水平。
注意: 断点调试过程中是运行状态,是以对象的运行类型来执行的。
Debug 快捷键参见 IDEA 使用。