一、变量与运算符
1. 变量
变量是计算机程序中用于存储和表示数据的一种概念。它是内存中的一块空间,用于保存各种类型(整数、浮点数、布尔值、字符等)的数据。使用变量来临时存储数据,并在需要的时候读取和修改这些数据。通过给变量赋值和访问变量的值,我们可以在程序中进行数据操作和计算。
1.1 变量的使用
数据类型 变量名 = 变量值
int age = 18
-
数据类型
数据类型分为两大类:
- 基本数据类型:byte、short、int、long、float、double、char、boolean
- 引用数据类型:class、interface、数组、enum、@interface、record
-
变量名:变量的标识符
-
关键字:被赋予了特殊含义,用做专门用途的字符串。全部关键字都是小写字母。
-
标识符:对 变量、方法、类 命名时使用的字符串。
标识符规则
只能由 26个英文字母(大小写敏感)、数字、下划线(_)、美元符号($)组成。
数字不可以开头。
不可以直接使用关键字和保留字。
标识符规范
包名:多单词组成时所有字母都小写:java.lang
类名、接口名:多单词组成时,所有单词的首字母大写:String
变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:toUpperCase
常量名:所有字母都大写,多单词时每个单词用下划线连接:public static final double PI = 3.14159265358979323846
-
-
变量值:依据数据类型所存储的具体数据
类型 在内存中占用的空间 范围 备注 byte 1个字节 -128 ~ 127 1个字节=8位
二进制系统中 每个0或1代表一位short 2个字节 -215 ~ 215-1 int 4个字节 -231 ~ 231-1 整型常量默认为 int 型 long 8个字节 -263 ~ 263-1 赋值时需要以 L 作为后缀 float 4个字节 赋值时需要以 F 作为后缀 double 8个字节 浮点型常量默认为 double 型 char 2个字节 boolean 只有两个值:true、false Java 虚拟机中没有提供 boolean 值专用的字节码指令
在编译之后都使用 int 数据类型来代替:
true用1表示 false用0表示
1.2 进制的转换
计算机数据的存储使用二进制补码形式存储,并且最高位是符号位:0代表正数 1代表负数
-
正数的补码与反码、原码一样,称为三码合一
-
负数的补码与反码、原码不一样,计算方式为:
-
计算负数原码:把十进制转为二进制,然后最高位设置为1
-
计算负数反码:在原码的基础上,最高位不变,其余位取反
-
计算负数补码:反码+1
-
1.2.1 二进制转十进制
以 byte 为例:
- 最大的正数:01111111 => 20 + 21 + 22 + 23 + 24 + 25 + 26 = 1 + 2 + 4 + 8 + 16 + 32 + 64 = 127
- 最小的负数:10000000 => -28 = -128
- 小数的计算:对于二进制小数 0.00011 => 2-4 + 2-5 = 1/16 + 1/32 = 0.0625 + 0.03125 = 0.09375
1.2.2 十进制转二进制
以 14.625 为例
-
整数部分:除2取余的逆 直到商为0为止。
- 14/2 = 商 7 余 0
- 7/2 = 商 3 余 1
- 3/2 = 商 1 余 1
- 1/2 = 商 0 余 1
1110 => 21 + 22 + 23 = 2 + 4 + 8 = 14
-
小数部分:乘2取整 直到结果等于1或者小数部分为0为止。
- 0.625 * 2 = 1.25 取 1
- 0.25 * 2 = 0.5 取 0
- 0.5 * 2 = 1 取 1
0.101 => 2-1 + 2-3 = 1/2 + 1/8 = 0.5 + 0.125 = 0.625
并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。
浮点类型 float、double 的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用 BigDecimal 类。
1.2.3 二进制与八进制、十六进制间的转换
- 二进制转八进制:三位二进制 等于 一位八进制
- 二进制转十六进制:四位二进制 等于 一位十六进制
1.3 基本数据类型变量间运算规则
不同的基本数据类型(只有7种,不包含boolean类型)变量的值经常需要进行相互转换。
-
自动类型提升:将取值范围小(或容量小)的类型自动提升为取值范围大(或容量大)的类型。规则如下:
-
当把存储范围小的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围大的变量时
int i = 'A'; // char 自动升级为 int double d = 10; // int 自动升级为 double long num = 1234567; // int 自动升级为 long long bigNum = 12345678912L; // 右边的整数常量值如果超过int范围,必须加L,显式表示long类型。否则编译不通过。
-
当存储范围小的数据类型与存储范围大的数据类型变量一起混合运算时,会按照其中最大的类型运算
int i = 1; byte b = 1; double d = 1.0; double sum = i + b + d;// 混合运算,升级为double
-
当byte、short、char 数据类型的变量进行算术运算时,按照 int 类型处理
byte b1 = 1; byte b2 = 2; byte b3 = b1 + b2;// 编译报错,b1 + b2 自动升级为 int char c1 = '0'; char c2 = 'A'; int i = c1 + c2;// 至少需要使用int类型来接收
-
-
强制类型转换:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小)的类型。可能会损失精度或溢出
- 数据类型 变量名 = (数据类型) 被强转数据值
// 损失精度 int i = (int)3.14; double d = 1.2; int num = (int)d; // 溢出 int i = 200; // 200 转换为二进制 原码=>11001000 反码=>10110111 补码=>10111000 byte b = (byte)i; // -(2^3 +2^4 + 2^5) = -(8 + 16 + 32) = -56
自动类型提升是Java自动执行的,而强制类型转换是自动类型提升的逆运算,需要我们自己手动执行。
2. 运算符
运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。
算数运算符
符号 | 运算规则 | 示例 |
---|---|---|
+ | 操作符左右两边的操作数均为数值代表加法 操作符左右两边的操作数至少有一个字符串代表拼接 | int num = 1 + ‘a’; // 98 char b = 1 + ‘a’; // b String str = 1 + “str”; // 1str |
- | 减法 | |
* | 乘法 | |
/ | 对于整数 “/” 运算符将返回除法的商 对于浮点数 “/” 运算符将返回精确的除法结果 | int result = 7 / 2; // 3 double result = 7.0 / 2.0; // 3.5 |
% | 取余 | |
++ | 前++:变量先自增1 然后再运算 后++:变量先运算 然后再自增1 | |
– | 前–:变量先自减1 然后再运算 后–:变量先运算 然后再自减1 |
// 易错练习
int i = 1;
int j = i++ + ++i * i++; // 1 + 3 * 3
System.out.println("j = " + j); // 10
System.out.println("i = " + i); // 4
int i = 2;
int j = i++;
System.out.println(j); // 2
int m = 2;
m = m ++; // (1)先取m的值“2”放操作数栈 (2)m再自增,m=3 (3)再把操作数栈中的"2"赋值给m,m=2
System.out.println(m); // 2
赋值运算符
符号 | 运算规则 | 示例 |
---|---|---|
= | 操作符右边的值赋值给左边的变量 | int a = 10; |
+= | int a += 10; => int a = a + 10; | |
-= | int a -= 10; => int a = a - 10; | |
*= | int a *= 10; => int a = a * 10; | |
/= | int a /= 10; => int a = a / 10; | |
%= | int a %= 10; => int a = a % 10; |
对于赋值操作符 和 自增自减操作符 不会改变变量本身的数据类型
byte b = 10; b+=10; // 是编译通过的不会要求 b的接收类型为 int
比较运算符
符号 | 运算规则 |
---|---|
== | 相等 |
!= | 不等 |
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
逻辑运算符
符号 | 运算规则 | 示例 |
---|---|---|
&&、& | 且:当符号左右两边布尔值都是true时 结果为true(一假即假) | true&&false=>false |
||、| | 或:当符号左右两边布尔值有一边为true时 结果为true(一真即真) | true||false=>true |
! | 非:当变量布尔值为true时,结果为false。当变量布尔值为false时,结果为true。 | !true=>false |
^ | 异或:当符号左右两边布尔值不同时,结果为true。当两边布尔值相同时,结果为false。 | true^true=>false |
&&和& 以及 ||和| 的区别:
&&和&:对于&&如果符号左边是false 则不再继续执行符号右边的操作 而&需要执行
||和|:对于||如果符号左边是true 则不再继续执行符号右边的操作 而|需要执行
位运算符
符号 | 运算规则 | 示例 |
---|---|---|
<< | 左移(乘2的幂) | 3<<2 = 3 * 22 = 12 |
>> | 右移(除2的幂) | 4>>2 = 4 / 22 = 1 |
>>> | 无符号右移 | 往右移动后 左边空出来的位直接补 0 |
& | 与(都为真才为真) | 只有 1&1 = 1 其余结果为0 |
| | 或(都为假才为假) | 只有 0|0 = 0 其余结果为1 |
^ | 异或(同为假 异为真) | 1^1 = 0;0^0 = 0; 1^0 = 1 |
~ | 取反 |
高效的方式计算 2 * 8 的值
int m = 2 << 3; int m = 8 << 1;
条件运算符(条件表达式)
语法形式如下:
condition ? expression1 : expression2
- 表达式:表达式是由运算符、操作数和表达式本身组成的代码片段,它可以计算出一个值。表达式通常包括变量、常量、函数调用、运算符和表达式的组合。
condition
是一个逻辑条件,可以是一个布尔表达式或具有布尔值的表达式。如果condition
的结果为真(true),则整个条件表达式的值为expression1
的值;如果condition
的结果为假(false),则整个条件表达式的值为expression2
的值。
二、流程控制语句
1. 顺序结构
程序从上到下逐行地执行 中间没有任何判断和跳转。上一行对某个变量的修改对下一行会产生影响。
2. 分支结构
- if-else 条件判断结构
if(条件表达式){
语句块;
}
if(条件表达式) {
语句块1;
}else {
语句块2;
}
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
}else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}
- switch-case 选择结构
switch(表达式){
case 常量值1:
语句块1;
[break;]
case 常量值2:
语句块2;
[break;]
...
[default:
语句块n+1;
break;
]
}
在 switch 语句中,如果 case 的后面不写 break,将出现穿透现象,也就是一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到 break 或者整个 switch 语句结束,执行终止。
- if-else 与 switch-case 比较
凡是使用 switch-case 的结构都可以转换为 if-else 结构。反之不成立。if-else 的条件是一个布尔类型值,所以可以用于范围的判断,也可以用于等值的判断。switch 的条件是一个常量值(byte、short、int、char、enum、String)只能判断某个变量或表达式的结果是否等于某个常量值,效率稍高,且可以利用 switch 的 穿透性,同时执行多个分支。
3. 循环结构
在某些条件满足的情况下,反复执行特定的代码
- for 循环
for (初始化部分; 循环条件部分; 迭代部分){
循环体部分;
}
- while 循环
初始化部分
while(循环条件部分){
循环体部分;
迭代部分;
}
- do-while 循环
初始化部分;
do{
循环体部分
迭代部分
}while(循环条件部分);
-
break 和 continue
-
break:一旦执行就退出当前循环,此关键字后面不能声明语句。
-
continue:一旦执行就退出当次循环,此关键字后面不能声明语句。
-
带标签(label)的 break 和 continue 的使用:
- label-break:退出标签处的循环
- label-continue :退出当次循环到标签处。
label:for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){ if(j % 4 == 0){ // break label; // 123 continue label; // 123123123123 } System.out.print(j); } System.out.println(); }
-
三、数组
存储指定长度的相同类型的元素的连续内存块。
1. 一维数组
- 数组的声明
数据类型[] 数组名
int[] scores;
String[] names;
-
数组的初始化
- 静态初始化
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,…};
int[] scores = new int[]{60,80,90,100};
数据类型[] 数组名;
数组名 = new 数据类型[]{元素1,元素2,元素3,…};int[] scores; scores = new int[]{60,80,90,100};
数据类型[] 数组名 = {元素1,元素2,元素3…};
int[] scores = {60,80,90,100};
- 动态初始化
数据类型[] 数组名 = new 数据类型[长度];
int[] scores = new int[4];
数据类型[] 数组名;
数组名 = new 数据类型[长度];int[] scores; scores = new int[4];
-
数组的使用
- 获取数组的长度
每个数组都有一个 length 属性。属性是指对象所具有的特征或状态。在面向对象编程中,对象通常由属性和方法组成。
int length = scores.length;
- 获取数组中的某个元素
每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为数组索引(index)或下标,可以通过数组的索引/下标访问到数组中的元素。
为什么数组下标从0开始?
数组在内存中是一块连续的空间,数组的第一个元素的地址就是数组本身的起始地址。由于计算机内存的特性,我们可以通过偏移量来访问数组中的特定元素。偏移量表示元素相对于数组起始位置的距离。将第一个元素的索引设置为0(即偏移量为0)使得计算偏移量更加简单和高效。
int score = scores[0];
- 遍历数组
for(int i = 0; i < scores.length; i++){ System.out.print(scores[i]); }
- 数组元素的默认值
数据类型 默认值 byte 0 short 0 int 0 long 0L float 0.0F double 0.0 char \u0000 boolean false 引用数据类型 null
2. 多维数组
- 二维数组的声明
int[][] scores;
-
二维数组的初始化
- 静态初始化
int[][] scores = new int[][]{ {60,80,90,90,100}, {90,70,100}, {90,70,80,80} };
可以将这个二维数组 分为三个一维数组
int[0] = {60,80,90,90,100}
int[1] = {90,70,100}
int[2] = {90,70,80,80}
-
动态初始化
- 规则二维表:每一行的列数是相同的
数据类型[][] 数组名 = new 数据类型[m][n]; // m:表示这个二维数组有多少个一维数组。或者说一共二维表有几行。 // n:表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格。
- 不规则:每一行的列数不一样
// 先确定总行数 数据类型[][] 数组名 = new 数据类型[总行数][]; // 此时只是确定了总行数,每一行里面现在是 null // 再确定每一行的列数,创建每一行的一维数组 数组名[行下标] = new 元素的数据类型[该行的总列数]; // 此时已经 new 完的行的元素就有默认值了,没有 new 的行还是 null // 再为元素赋值 二维数组名[行下标][列下标] = 值;
3. 数组的使用
3.1 数值型数组特征值统计
- 数组元素的总值和平均值
int sum = 0;
int[] arr = new int[]{10, 40, 20, 60, 80, 100, 90, 30};
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
System.out.println(sum); // 总值
System.out.println((double) sum / arr.length); // 平均值
- 数组元素的总乘积
int sum = 1;
int[] arr = new int[]{1, 4, 2, 6, 8, 10, 9, 3};
for (int i = 0; i < arr.length; i++) {
sum *= arr[i];
}
System.out.println(sum);
- 数组元素中偶数的个数
int sum = 0;
int[] arr = new int[]{1, 4, 2, 6, 8, 10, 9, 3};
for (int i = 0; i < arr.length; i++) {
if (arr[i] % 2 == 0) {
sum++;
}
}
System.out.println(sum);
- 数组元素的最大值
int[] arr = new int[]{1, 4, 2, 6, 8, 10, 9, 3};
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
System.out.println(max);
- 最值及其第一次出现的下标
int[] arr = new int[]{1, 4, 2, 6, 8, 10, 9, 3};
int max = arr[0];
int index = 0;
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
index = i;
}
}
System.out.println(max);
System.out.println(index);
- 最值及其所有最值的下标
int[] arr = new int[]{1, 4, 10, 6, 8, 10, 9, 3};
int max = arr[0];
String index = "0";
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
index = i + "";
} else if (arr[i] == max) {
index += "," + i;
}
}
System.out.println(max);
System.out.println(index);
3.2 数组元素的赋值与数组复制
-
打印10行的杨辉三角
yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];
// 确认行数 int[][] yangHui = new int[10][]; // 为每一行的一维数组进行赋值 for (int i = 0; i < yangHui.length; i++) { // 每一行的列数 及每一个一维数组的长度 yangHui[i] = new int[i + 1]; // 对每一个一维数组的首尾元素进行赋值 都为1 yangHui[i][0] = yangHui[i][i] = 1; // 对中间元素进行赋值 for (int j = 1; j < yangHui[i].length - 1; j++) { yangHui[i][j] = yangHui[i - 1][j - 1] + yangHui[i - 1][j]; } // 打印 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(); } }
-
声明两个数组 int[] array1 和 int[] array2,初始化 array1,将 array1 赋值给 array2,修改 array2 的偶索引元素,使其等于索引值。打印出 array1 。
拓展:实现 array2 对 array1 数组的复制
// 此时对 array2 修改会影响到 array1 // 因为 array1 和 array2 都指向同一个地址 int[] array1 = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int[] array2 = array1; for (int i = 0; i < array2.length; i++) { if (i % 2 == 0) { array2[i] = i; } } System.out.println(Arrays.toString(array1)); // 实现复制 int[] array1 = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int[] array2 = new int[array1.length]; for (int i = 0; i < array2.length; i++) { array2[i] = array1[i]; } System.out.println(Arrays.toString(array1)); System.out.println(Arrays.toString(array2));
-
数组的每个元素去除第一个元素,得到的商作为被除数所在位置的新值。
int[] array = new int[]{3, 1, 3, 4, 5, 6, 7, 8, 9, 10};
// 需要找一个临时变量将第一个元素存起来
// 不能直接 array[i] = array[i] / array[0]; 除以它本身的时候会修改掉 array[0] 的值
int num = array[0];
for (int i = 0; i < array.length; i++) {
array[i] = array[i] / num;
}
System.out.println(Arrays.toString(array));
- 创建一个长度为6的int型数组,要求数组元素的值都在1-30之间,且是随机赋值。同时,要求元素的值各不相同。
int[] array = new int[6];
for (int i = 0; i < array.length; i++) {
int random = (int) (Math.random() * 30) + 1;
array[i] = random;
// 去遍历已经生成的这些数
for (int j = 0; j < i; j++) {
// 如果相同了 这次重新随
if (array[i] == array[j]) {
i--;
break;
}
}
}
- 生成一副扑克牌
String[] hua = {"黑桃", "红桃", "梅花", "方片"};
String[] dian = {"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"};
String[] pai = new String[hua.length * dian.length];
int k = 0;
for (int i = 0; i < hua.length; i++) {
for (int j = 0; j < dian.length; j++) {
pai[k] = hua[i] + dian[j];
k++;
}
}
- 回形数
int n = 7; // 回形矩形的边长
int[][] arr = new int[n][n];
int count = 0; //要显示的数据
int minX = 0; // x轴的最小下标
int maxX = n - 1; // x轴的最大下标
int minY = 0; // Y轴的最小下标
int maxY = n - 1; // Y轴的最大下标
while (minX <= maxX) {
for (int x = minX; x <= maxX; x++) {
arr[minY][x] = ++count;
}
minY++;
for (int y = minY; y <= maxY; y++) {
arr[y][maxX] = ++count;
}
maxX--;
for (int x = maxX; x >= minX; x--) {
arr[maxY][x] = ++count;
}
maxY--;
for (int y = maxY; y >= minY; y--) {
arr[y][minX] = ++count;
}
minX++;
}
3.3 数组元素的反转
// 双指针
int[] arr = {1, 2, 3, 4, 5};
System.out.println("反转之前:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
for (int left = 0, right = arr.length - 1; left < right; left++, right--) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
System.out.println("反转之后:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
3.4 数组的扩容与缩容
- 现有数组 int[] arr = new int[]{1,2,3,4,5}; 将数组长度扩容1倍,并将10,20,30三个数据添加到arr数组中。
int[] arr = new int[]{1, 2, 3, 4, 5};
int[] newArr = new int[arr.length << 1];
for (int i = 0; i < arr.length; i++) {
newArr[i] = arr[i];
}
newArr[arr.length] = 10;
newArr[arr.length + 1] = 20;
newArr[arr.length + 2] = 30;
// 将原来的指针 指向新的数组
arr = newArr;
System.out.println(Arrays.toString(arr));
- 现有数组 int[] arr={1,2,3,4,5,6,7}。现需删除数组中索引为4的元素。
int[] arr = {1, 2, 3, 4, 5, 6, 7};
for (int i = 4; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr[arr.length - 1] = 0;
System.out.println(Arrays.toString(arr));
3.5 数组的元素查找
- 二分查找:要求此数组必须是有序的。
int value = 256;
int[] arr3 = new int[]{-99, -54, -2, 0, 2, 33, 43, 256, 999};
int head = 0;//首索引位置
int end = arr3.length - 1;//尾索引位置
while (head <= end) {
int middle = (head + end) / 2;
if (arr3[middle] == value) {
System.out.println("找到了指定元素,索引为" + middle);
return;
} else if (arr3[middle] > value) {
end = middle - 1;
} else if (arr3[middle] < value) {
head = middle + 1;
}
}
3.6 数组元素排序
- 冒泡排序(稳定)
int[] arr = new int[]{10, 40, 20, 60, 80, 100, 90, 30};
// 外层循环 比较多少个元素
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]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
4. Arrays 工具类
- 数组元素打印
Arrays.toString(Object[] a);
- 数组元素排序
Arrays.sort(Object[] a); // 用的快排
- 数组的二分查找
Arrays.binarySearch(Object[] a,Object key);
- 数组的复制
Arrays.copyOf(T[] original,int newLength);
- 比较数组是否相等
Arrays.equals(Object[] a,Object[] a2);
四、面向对象
面向对象,是软件开发中的一类编程风格、开发范式。
1. 类和对象
类:类是一种抽象的数据类型,用于描述对象的属性(成员变量)和行为(方法)。它是创建对象的模板或蓝图。类定义了对象的结构和行为,并封装了数据和操作。可以将类看作是一种自定义的数据类型,具有自己的特征和行为。
对象:对象是类的实例化(实例),是类的具体化表现。它具有类中定义的属性和行为,并且可以通过调用对象的方法来执行相应的操作。对象是类的实体,可以独立存在,并且可以与其他对象进行交互。
1.1 创建对象
- 创建类
[修饰符] class 类名{
属性声明;
方法声明;
}
- 创建对象(new 关键字)
类名 对象名 = new 类名();
new 类名(); // 匿名对象
- 调用对象的属性和方法
对象名.属性
对象名.方法
- 凡是 new 出来的结构(对象、数组)都存放在堆空间
- 对象的属性存放在堆空间
- 当声明一个新的变量使用现有的对象进行赋值时(比如 person1 = person2)此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用。
- 创建一个类的多个对象(比如 person1 、person2),则每个对象都拥有当前类的一套"副本"(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。
1.2 类的成员
1.2.1 成员变量
[修饰符1] class 类名{
[修饰符2] 数据类型 成员变量名 [= 初始化值];
}
成员变量 vs 局部变量
成员变量:在方法体外,类体内声明的变量。分为实例变量(不用static修饰)和类变量(用static修饰)。
局部变量:在方法体内部等位置声明的变量。分为形参、方法局部变量、代码块局部变量。
不同点:
- 声明的位置不同。
- 在内存中存储的位置不同:实例变量在堆空间。局部变量在栈空间。
- 作用域不同:实例变量在本类中,或其他类中"对象.实例变量"。局部变量在方法作用域内。
- 可使用的修饰符不同:实例变量:public、protected、private、final、volatile、transient等。局部变量:final。
- 默认值不同:实例变量有默认值。局部变量没有默认值,必须手动初始化,形参处需要实参初始化。
-
类中成员变量赋值的过程(顺序)
- 默认初始化
- 显示初始化 多个初始化块依次被执行(同级别按先后顺序)
- 构造器中初始化
- 通过"对象.属性"或"对象.方法"进行赋值
其中 1,2,3 在创建对象的过程中只执行一次 4可以根据需求执行多次
1.2.2 方法
[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
方法体的功能代码
}
- 方法是类或对象行为特征的抽象 用来完成某种功能的操作
- 将功能封装为方法的目的:实现代码重用 减少冗余 简化代码
- Java 里的方法不能独立存在 所有方法必须定义在类里
- 实例方法调用
对象.方法名([实参列表])
必须先声明后使用 方法必须定义在类的内部
方法调用一次就执行一次 不调用就不执行
方法中可以调用类中的方法和属性 不能在方法中定义方法
- return 关键字
return 在方法中的作用:
- 结束一个方法
- 在结束一个方法的同时 返回数据给方法的调用者
return 关键字的后面不能声明执行语句
- 方法调用内存分析
- 方法在没有被调用的时候 都放在方法区的字节码文件中存储
- 方法在被调用的时候 需要放到栈中运行。方法每调用一次就会在栈中有一个入栈操作 即给当前的方法开辟一块独立的内存空间 用于存储当前方法的局部变量的值
- 当方法结束调用时 会释放内存空间 称为出栈 如果方法有返回值 会把结果返回方法的调用处 如果没有返回值就直接结束 回到调用处的下一条指令
1.2.2.1方法的重载(overload)
- 在同一个类中允许存在一个以上的同名方法 只需要参数列表(参数个数或参数类型)不同即可
- 方法的重载与方法的修饰符、返回值、异常列表无关
- JVM 在调用重载方法时 会优先匹配个数和类型最相同的 其次会匹配个数和类型兼容的 如果多个方法都可以兼容会编译报错
- 可变参
方法名(参数的类型名 ...参数名)
- 可变参 指在方法的形参列表处 可传入的实参个数可以是 0个、1个或多个
- 可变参的方法 与 同名方法 彼此之间构成重载
// 这三个方法彼此构成重载
public void method() {}
public void method(int... params){}
public void method(int params) {}
- 可变参的方法 和 形参列表是数组的方法 二者是一致的 不能同时声明
// 二者一致 不能同时声明
public void method(int... params){}
public void method(int[] params){}
- 可变参必须声明在所有形参最后 且 最多声明一个可变参
-
方法的值传递机制
Java 里方法的参数传递方式只有一种:值传递。即将实际参数的副本传入方法内,而参数本身不受影响。
- 形参是基本数据类型:将实参的基本数据类型变量的数据值传递给形参
- 形参是引用数据类型:将实参的引用数据类型变量的地址值传递给形参
-
递归方法(recursion)
方法内部自己调用自己。递归方法包含了一种隐式循环 会重复执行某段代码。需要向已知方向递归。
1.2.3 构造器
在 new 一个对象的时候 所有的成员变量都是默认值 需要一个一个的赋值 构造器可以在 new 的时候为 成员变量直接赋值。
[修饰符] class 类名{
[修饰符] 构造器名(){
// 实例初始化代码
}
[修饰符] 构造器名(参数列表){
// 实例初始化代码
}
}
- 构造器名必须与类名相同
- 构造器没有返回值 也不需要void
- 构造器的修饰符只能是权限修饰符
- 使用说明
- 当我们没有显示的声明一个构造器的时候 编译器会默认生成一个无参的构造器 并且修饰符和类的修饰符一致
- 当我们显示的声明一个构造器的时候 编译器不再提供默认的构造器
- 在类中至少存在一个构造器
- 构造器是可以重载的
1.2.4 代码块
当成员变量想要初始化的值不是一个硬编码的常量值时可以使用代码块。代码块只能被 static 修饰。
- 静态代码块
【修饰符】 class 类{
static{
静态代码块
}
}
- 静态代码块的执行要先于非静态代码块
- 静态代码块随着类的加载而加载 且只执行一次
- 若有多个静态的代码块 那么按照从上到下的顺序依次执行
- 不可以对非静态的属性初始化
- 可以对类的属性、类的声明进行初始化操作
- 可以有输出语句
- 非静态代码块
【修饰符】 class 类{
{
非静态代码块
}
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}
- 每次创建对象的时候,都会执行一次。且先于构造器执行
- 可以对类的属性、类的声明进行初始化操作
- 除了调用非静态的结构外,还可以调用静态的变量或方法
- 若有多个非静态的代码块,那么按照从上到下的顺序依次执行
2. 面向对象的特征
2.1 封装性
把客观事物封装成抽象概念的类。并且类可以把自己的数据和方法只向可信的类或者对象开放,向其它类或者对象隐藏信息。
- Java 通过权限修饰符来体现封装性
修饰符 | 本类内部 | 本包内 | 其它包的子类 | 其它包非子类 |
---|---|---|---|---|
private | 可访问 | × | × | × |
缺省 | 可访问 | 可访问 | × | × |
protected | 可访问 | 可访问 | 可访问 | × |
public | 可访问 | 可访问 | 可访问 | 可访问 |
外部类可用的权限修饰符:public、缺省
成员变量、成员方法、成员内部类、构造器可用的权限修饰符:public、protected、缺省、private
- JavaBean
JavaBean 是一种 Java 语言写成的可重用组件。所谓 JavaBean 是符合如下标准的 Java 类型:
- 类是公共的
- 有一个无参的公共的构造器
- 有属性,且有对应的get、set方法
2.2 继承性
2.2.1 this 关键字
-
什么时候使用 this:
-
实例方法或构造器中使用当前对象的成员
在实例方法或构造器中 如果使用当前类的成员变量或成员方法可以在其面前添加this 增强程序的可读性 不过通常习惯省略this
但是当形参和成员变量同名时 必须添加 this 来明确表明该变量是类的成员变量 即可以使用 this 来区分 成员变量 和 局部变量
Public class Person{ private String name; public void setName(String name){ this.name = name; } }
-
同一个类中构造器互相调用
- this():调用本类无参的构造器
- this(实参列表):调用本类的有参构造器
-
this() 或 this(实参列表) 只能声明在 构造器首行 所以在类的构造器中 最多只能声明一个 this
-
不能出现自身构造器调用 this(实参列表) 的递归调用
-
2.2.1 继承
通过extends关键字 可以声明一个类B继承另外一个类A
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
-
子类会继承父类所有的实例变量和实例方法
- 当子类对象被创建时 会在堆空间为对象分配空间 此时需要看子类的父类都声明了哪些实例变量 这些父类的实例变量都需要分配内存
- 当子类调用对象方法时 编译器会先在子类模板中查看是否有该方法 如果没有 会自下而上去寻找其父类甚至是父类的父类是否有该方法 找到了就停止 如果到根父类(Object)都没有找到 就会编译报错
-
子类不能访问父类中的私有成员变量和私有成员方法
子类虽然能继承父类中的私有成员变量 但子类不能访问
-
在 Java 中 继承的关键字是 extends 即子类不是父类的子集 而是对父类的扩展
-
Java 支持多层继承 不支持多重继承
// 多层继承 class Person{} class Man extends Person{} class Student extends Man{} // 不支持多重继承 class Student extends Man,Person // 编译报错
2.2.2 方法的重写(override)
父类中所有的实例方法子类都会继承 但是当某个方法被继承到子类后 子类觉得父类的实现不适合自己 子类可以对父类中继承来的方法进行改造 即称为方法的重写
@override 说明:
写在方法上面 用来检测是不是满足方法重写的要求 可以不写 只要满足重写要求 也是正确的方法重写。
但建议保留 编译器可以帮我们检查格式 也可以让程序员明确知道这是一个重写的方法。
- 子类中重写的方法 必须和父类中的方法 具有相同的方法名称、参数列表
- 子类中重写的方法 的返回值类型 不能大于父类中的方法 的返回值类型
- 子类中重写的方法 的访问权限 不能小于父类中的方法 的访问权限
- 子类中重写的方法 抛出的异常 不能大于父类中的方法 抛出异常
此外 方法重写只适用于实例方法(非 static 修饰的方法)子类和父类中同时声明 static 方法不属于方法重写 因为static 属于类
2.2.3 super 关键字
- 什么时候用 super
- 访问父类中定义的成员方法
- 若子类没有重写父类的方法 只要访问修饰符允许 子类可以直接调用父类的方法
- 若子类重写了父类的方法 子类需要通过
super.
来明确表明调用的是父类中的方法
- 访问父类中定义的成员变量
- 如果子类的实例变量和父类的实例变量没有重名 子类可以直接访问父类中的变量 也可以通过
super.
或this.
来访问 - 如果子类的实例变量和父类的实例变量重名 且父类的实例变量子类可见 在子类中访问父类的实例变量 需要通过
super.
来明确表明调用的是父类中的实例变量
- 如果子类的实例变量和父类的实例变量没有重名 子类可以直接访问父类中的变量 也可以通过
- 在子类构造器中调用父类的构造器
- 子类继承父类时 不会继承父类的构造器 只能通过
super(形参列表)
的方式调用指定的父类构造器 super(形参列表)
必须声明在构造器首行 所以this(形参列表)
和super(形参列表)
只能二选一- 如果子类构造器首行既没有显示调用
this(形参列表)
也没有调用super(形参列表)
则此构造器默认调用父类的空参构造 所以子类任何一个构造器中 要么调用本类中重载的构造 要么调用父类的构造 即无论如何最终都会调用父类的构造
- 子类继承父类时 不会继承父类的构造器 只能通过
- 访问父类中定义的成员方法
this 和 super
this 指向的是当前对象的某个成员 调用this(形参列表)是调用的本类的构造器
super 指向的是父类对象的某个成员 调用super(形参列表)是调用的父类的构造器
this(形参列表) 和 super(形参列表) 都必须声明在构造器的首行
2.2.4 子类对象实例化的全过程
class Creature {
public Creature() {
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
}
public Animal(String name, int age) {
this(name);
System.out.println("Animal带两个参数的构造器,其age为" + age);
}
}
public class Dog extends Animal {
public Dog() {
super("汪汪队阿奇", 3);
System.out.println("Dog无参数的构造器");
}
public static void main(String[] args) {
new Dog();
}
}
// 结果
Creature无参数的构造器
Animal带一个参数的构造器,该动物的name为汪汪队阿奇
Animal带两个参数的构造器,其age为3
Dog无参数的构造器
2.3 多态性
父类类型 变量名 = 子类对象
Object obj = new Person();
-
父类的引用指向子类的对象。在 Java 中 子类的对象可以代替父类进行使用 所以一个引用类型变量可能指向多种不同类型的对象。
-
Java 中的引用类型变量有两个类型 编译时类型 和 运行时类型 编译时类型由声明的类型决定 运行时类型由实际赋值的对象决定 即 编译看左边 运行看右边。若编译和运行时类型不一致 就出现的对象的多态。
-
多态使用的前提:
- 类的继承
- 方法的重写
多态的好处:实现了动态绑定。代码更灵活。
多态的弊端:不能访问子类中添加的属性和方法。
- 虚方法调用:指在编译的阶段不能确定调用的入口 在运行阶段才能确定方法 即可能被重写的方法
静态链接:当一个字节码文件被装载进 JVM 内部时 如果被调用的方法在编译期可知 且运行时保持不变 这种情况下 符号引用 转换为 直接引用的过程 称之为 静态链接。那么调用这样的方法 就称为 非虚方法调用。
动态链接:当一个字节码文件被装载进 JVM 内部时 如果被调用的方法在编译器期无法确定下来 只能够在程序运行期将调用方法的 符号引用 转换为 直接引用 由于这种转换过程具有动态性 所以称之为 动态链接。调用这样的方法 称为 虚方法调用。
-
成员变量没有多态性
即使 子类 定义了和父类同名的实例变量 那么子类的实例变量也不可能覆盖父类中定义的实例变量
3.1 向上转型与向下转型
-
为什么需要类型转换
因为多态 会把子类对象赋值给父类变量 这个时候在编译期间就会出现 类型转换 当父类变量接收到子类对象后 就不能调用子类独有的方法 所以要想使用子类独有的方法就必须做类型转换 使得编译通过
-
向上转型
当左边的变量类型(父类)> 右边的变量类型(子类)时 我们称为向上转型 此时编译时按照左边的变量类型进行处理 就只能调用父类中的属性和方法 不能调用子类特有的属性和方法 但是运行时仍是右边的变量类型 所以执行的是子类的方法 此时一定是安全的 且是自动完成的
-
向下转型
当左边的变量类型(子类)< 右边的变量类型(父类)时 我们称为向下转型 此时编译时按照左边的变量类型进行处理 就可以调用子类特有的属性和方法 但是运行时仍是右边的变量类型 所以不是所有的向下转型都是正确的 可能会发生 ClassCastException 为了安全 可以通过 instanceof 关键字进行判断
-
instanceof 关键字
对象 instanceof 数据类型
3.2 Object 类的使用
类 java.lang.Object 是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。
-
equals()
public boolean equals(Object obj) { return (this == obj); }
- ==:对于基本数据类型比较的是数值 对于引用数据类型比较的是地址值
- 如果不重写 Object 类中的 equals 方法 那么还是比较的地址值
- String 类的 equals 方法被重写过
-
toString()
- 默认情况下 返回 “对象的运行时类型@对象的hashCode值的十六进制形式”
- 在 String 与其它类型数据连接的时候自动调用 toString 方法
- 可以根据需要在用户自定义类型中重写
-
clone():implements Cloneable
protected native Object clone() throws CloneNotSupportedException
native 关键字:说明这个方法是原生函数,也就是这个方法是用 C/C++ 等非 Java 语言实现的。
-
finalize():当对象被回收时,系统自动调用该对象的 finalize() 方法。在销毁该对象的时候 会先调用 finalize() 方法。JDK 9 以后标记为过时的
-
getClass():获取对象的运行时类型
-
hashCode():返回某个对象的 hash 值
3. static 关键字
static 可以修饰属性、方法、代码块、内部类等。被修饰后的成员:
1. 随着类的加载而加载
2. 优先于对象存在
3. 被所有实例共享
4. 访问权限允许时 可以不创建对象 直接通过类调用
- 静态变量:使用static修饰的成员变量就是静态变量
[修饰符] class 类{
[其他修饰符] static 数据类型 变量名;
}
- 静态变量的默认值和实例变量一样
- 静态变量的值是所有实例共享的
- 静态变量可以在任意代码块 方法 构造器中使用
- 可以使用
类名.静态变量
的方式使用- 静态变量的 get/set 方法也是静态的 当局部变量和静态变量重名时 可以通过
类名.静态变量
进行区分- 静态变量的值存在方法区
- 静态方法:用static修饰的成员方法就是静态方法
[修饰符] class 类{
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}
静态方法可以在任意代码块 方法 构造器中使用
可以使用
类名.静态方法名
的方式使用在 static 方法内部只能访问类的 static 修饰的属性或方法,不能访问类的 非static 的结构
静态方法可以被子类继承 但不能被子类重写
静态方法的调用都只看编译时类型
因为不需要实例就可以访问 static 方法,因此 static 方法内部不能有 this,也不能有 super。如果有重名问题,使用
类名.
进行区别
4. final 关键字
- final 修饰类:表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。
- final 修饰方法:表示这个方法不能被子类重写。
- final修饰变量:可以修饰成员变量和局部变量。一旦赋值 它的值就不能被修改 即常量 常量名建议使用大写字母。
public int addOne(final int x) {
// return ++x; // 编译报错
return x + 1; // 返回3
}
5. abstract 关键字
- abstract 修饰类
[权限修饰符] abstract class 类名{}
- abstract 修饰方法
[其他修饰符] abstract 返回值类型 方法名([形参列表]);
抽象类不能创建对象 只能创建其抽象类子类的对象
抽象类也有构造器 是供子类创建对象时 初始化父类成员变量使用的
抽象类中不一定含有抽象方法 但是有抽象方法的类必须是抽象类
抽象类的子类必须重写抽象类中所有的抽象方法 除非这个子类也是一个抽象类
6. 接口
接口是从多个相似类中抽象出来的规范 不需要提供具体实现
接口中没有构造器 没有初始化块 因为接口中没有成员变量需要动态初始化
[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法
// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}
公共的静态的常量:其中
public static final
可以省略公共的抽象的方法:其中
public abstract
可以省略公共的默认的方法:其中 public 可以省略,建议保留,但是 default 不能省略
公共的静态的方法:其中public 可以省略,建议保留,但是 static 不能省略
私有方法:private 修饰
- 类实现接口
【修饰符】 class 实现类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
- 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法
- 默认方法可以选择保留,也可以重写。重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了。
- 接口中的静态方法不能被继承也不能被重写
- 接口的多实现:一个类是可以实现多个接口的
【修饰符】 class 实现类 implements 接口1,接口2,接口3... {
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3...{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次
-
接口与实现类对象构成多态引用
实现类实现接口 类似于子类继承父类。因此 接口类型的变量与实现类的对象之间 也可以构成多态引用。通过接口类型的变量调用方法 最终执行的是实现类对象实现的方法
-
默认方法冲突问题
- 类优先原则:当一个类 既继承一个父类 又实现若干个接口时。父类中的成员方法与接口中的默认方法重名 子类就近选择执行父类的成员方法。
public interface Friend { default void date(){ System.out.println("吃喝玩乐"); } } public class Father { public void date(){ System.out.println("爸爸约吃饭"); } } public class Son extends Father implements Friend {} public class TestSon { public static void main(String[] args) { Son s = new Son(); s.date(); // 爸爸约吃饭 } }
-
接口冲突:
-
当一个类同时实现了多个父接口 而多个父接口中包含方法签名相同的默认方法时
-
当一个子接口同时继承了多个接口 而多个父接口中包含方法签名相同的默认方法时
无论多难抉择都需要在 实现类或者子接口中重写默认方法
-
interface Friend { default void date() { System.out.println("吃喝玩乐"); } } interface BoyFriend { default void date() { System.out.println("神秘约会"); } } class Son implements BoyFriend, Friend { @Override public void date() { BoyFriend.super.date(); } } public class TestSon { public static void main(String[] args) { Son s = new Son(); s.date(); } }
-
常量冲突:
-
当一个类同时实现了多个父接口 而多个父接口中存在相同常量时
-
当一个类继承了父类 又实现了接口 父类的成员变量和接口的常量 存在同名时
无论多难抉择都需要在 使用变量时指明使用的是哪个类或者接口的常量
interface Friend { int x = 1; } class Father { int x = 2; } class Son extends Father implements Friend { public void method() { // 不能直接 System.out.println(x); System.out.println(super.x); System.out.println(Friend.x); } }
-
接口和抽象类:
接口中包含:常量、抽象方法、默认方法、静态方法、私有方法。子类通过 implements 实现。
抽象类中包含:常量、抽象方法、构造方法、普通方法、变量。子类通过 extends 继承。
都通过对象的多态性产生实例化对象。
7. 内部类
7.1 成员内部类
[修饰符] class 外部类{
[其他修饰符] [static] class 内部类{
}
}
成员内部类作为类的成员存在:可以使用四种权限修饰符,可以调用外部类的结构,可以声明为static的。
成员内部类可以直接使用外部类的所有成员,包括私有的数据。
- 实例化内部类
class Outer{
class NoStaticInner{}
}
Outer outer = new Outer();
Outer.NoStaticInner inner = outer.new NoStaticInner();
- 实例化静态内部类
class Outer{
class static StaticInner{}
}
Outer.StaticInner inner = new Outer.StaticInner();
7.2 局部内部类
定义在方法内部的类
[修饰符] class 外部类{
[修饰符] 返回值类型 方法名(形参列表){
[final/abstract] class 内部类{
}
}
}
和成员内部类不同的是,它前面不能有权限修饰符。
- 非匿名局部内部类
interface Person {
void say();
}
public class Test {
public static void main(String[] args) {
class Student implements Person {
@Override
public void say() {
System.out.println("好好学习");
}
}
Student student = new Student();
student.say();
}
}
- 匿名内部类
public static void main(String[] args) {
new Person() {
@Override
public void say() {
System.out.println("好好学习");
}
}.say();
}
8. 枚举类
- JDK5之前如何声明枚举类
- 私有化类的构造器 保证不能在类的外部创建其对象
- 在类的内部创建枚举类的实例。声明为:public static final 对外暴露这些常量对象
- 对象如果有实例变量,应该声明为 private final(建议,不是必须)并在构造器中初始化
public class Season {
// 季节的名称
private final String SEASONNAME;
// 季节的描述
private final String SEASONDESC;
private Season(String seasonName, String seasonDesc) {
this.SEASONNAME = seasonName;
this.SEASONDESC = seasonDesc;
}
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "白雪皑皑");
}
class SeasonTest {
public static void main(String[] args) {
System.out.println(Season.AUTUMN);
}
}
- JDK 5 以后使用枚举类
public enum SeasonEnum {
SPRING("春天","春风又绿江南岸"),
SUMMER("夏天","映日荷花别样红"),
AUTUMN("秋天","秋水共长天一色"),
WINTER("冬天","窗含西岭千秋雪");
private final String seasonName;
private final String seasonDesc;
SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
class SeasonTest {
public static void main(String[] args) {
System.out.println(SeasonEnum.AUTUMN.getSeasonName());
}
}
- 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。自动添加 public static final 修饰。
- 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略。
- 编译器给枚举类默认提供的是 private 的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数。
- 如果枚举类需要的是有参构造,需要手动定义,有参构造的 private 可以省略。
- 枚举类默认继承的是 java.lang.Enum 类,因此不能再继承其他的类型。
- enum中常用方法
String toString(): 默认返回的是常量名(对象名),可以重写该方法
static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法
static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException
String name():得到当前枚举常量的名称。建议优先使用toString()
int ordinal():返回当前枚举常量的次序号,默认从0开始
-
实现接口的枚举类
- 和普通 Java 类一样,枚举类可以实现一个或多个接口
- 若每个枚举值在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可
enum A implements 接口1,接口2{ //抽象方法的实现 }
- 若需要每个枚举值在调用实现的接口方法呈现出不同的行为方式,则可以让每个枚举值分别来实现该方法
enum A implements 接口1,接口2{ 常量名1(参数){ //抽象方法的实现或重写 }, 常量名2(参数){ //抽象方法的实现或重写 }; } public interface Info { void show(); } public enum SeasonEnum implements Info { SPRING("春天", "春风又绿江南岸") { public void show() { System.out.println(""); } }, SUMMER("夏天", "映日荷花别样红") { public void show() { System.out.println(""); } }, AUTUMN("秋天", "秋水共长天一色") { public void show() { System.out.println(""); } }, WINTER("冬天", "窗含西岭千秋雪") { public void show() { System.out.println(""); } }; //... }
9. 注解
-
元注解
-
@Target:用于描述注解的使用范围:java.lang.annotation.ElementType
-
@Retention:用于描述注解的生命周期:java.lang.annotation.RetentionPolicy
RUNTIME 才能被反射读取到
-
@Documented:表明这个注解应该被 javadoc 工具记录
-
@Inherited:允许子类继承父类中的注解
-
-
自定义注解
【元注解】
【修饰符】 @interface 注解名{
【成员列表】
}
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String columnName();
String columnType();
}
@Table("t_stu")
public class Student {
@Column(columnName = "sid",columnType = "int")
private int id;
@Column(columnName = "sname",columnType = "varchar(20)")
private String name;
}
注解的成员列表以 无参数有返回值的抽象方法的形式来声明 我们又称为配置参数
返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
可以使用 default 关键字为抽象方法指定默认返回值。
如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是"方法名 = 返回值",如果只有一个抽象方法需要赋值,且方法名为value,可以省略"value=",所以如果注解只有一个抽象方法成员,建议使用方法名value。
10. 包装类
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
-
包装类与基本数据类型间的转换
-
装箱:把基本数据类型转为包装类对象 为的是使用专门为对象设计的API和特性
-
拆箱:把包装类对象拆为基本数据类型 为的是基本数据类型能够进行计算
-
自动装箱与拆箱:从 JDK 5.0 开始,基本类型与包装类的装箱、拆箱动作可以自动完成
-
基本数据类型、包装类与字符串间的转换
- 基本数据类型转为字符串
int a = 10; String str = String.valueOf(a);
- 字符串转为基本数据类型
Integer.parseInt(String s); Long.parseLong(String s); Double.parseDouble(String s);
- 字符串转为包装类
Integer.valueOf(String s); Long.valueOf(String s); Double.valueOf(String s);
-
-
包装类对象的特点
- 包装类缓存对象
包装类 缓存对象 Byte -128~127 Short -128~127 Integer -128~127 Long -128~127 Float 没有 Double 没有 Character 0~127 Boolean true和false Integer i = new Integer(1); Integer j = new Integer(1); System.out.println(i == j); // true Integer m = 1; Integer n = 1; System.out.println(m == n); // false Integer x = 128; Integer y = 128; System.out.println(x == y); // false
缓存的对象在方法区 如果明确是new的还是会保存在堆中
- 类型转换问题
自动拆装箱 存在于基本数据类和包装类之间 如果两个都是包装类不会进行自动类型转换
// 会编译报错 Integer i = 1; Double d = 1.0 System.out.println(i==d); // 表达式内会自动类型提升 Object o1 = true ? new Integer(1) : new Double(2.0); System.out.println(o1); // 1.0 Object o2; if (true) o2 = new Integer(1); else o2 = new Double(2.0); System.out.println(o2); // 1
- 包装类对象不可变
方法的参数传递机制:
- 若传递的是基本数据类型 则形参的修改完全不影响实参
- 若传递的是引用数据类型 若形参修改对象的属性 会影响实参的属性
- 若传递的是包装类 会 new 一个新对象 也与形参无关 即不会影响实参的数值
五、异常处理
1. Java 异常体系
-
java.lang.Throwable :Java 程序执行过程中发生的异常事件对应的类的根父类
-
java.lang.Error:extends Throwable。Java 虚拟机 无法解决的严重问题 如堆栈内存溢出。
-
java.lang.Exception:extends Throwable。因编程错误或偶然的外在因素导致的一般性问题 需要针对代码进行处理 使程序继续运行。否则一旦发生异常 程序也会挂掉。
-
编译时异常和运行时异常:
- 编译时异常(受检异常)(checked Exception):在代码编译期间 编译器能明确警示当前代码可能发生但不一定发生的异常 并明确监督程序员需要对这些异常编写处理代码。如果没有编写异常处理代码 则会编译失败。通常这类异常不是由程序员编写代码引起,或者不是加简单判断就能避免的。
- 运行时异常(非受检异常)(unchecked Exception):在代码编译阶段 编译器不做任何检查 无论异常是否发生 在代码运行期间 才能发生异常。通常这类异常是由程序员编写代码不当 或稍加判断就能避免的。java.lang.RuntimeException类及它的子类都是运行时异常。
2. 异常的处理
-
异常捕获(try-catch-finally)
try{ ...... //可能产生异常的代码 } catch( 异常类型1 e ){ ...... //当产生异常类型1型异常时的处置措施 } catch( 异常类型2 e ){ ...... //当产生异常类型2型异常时的处置措施 } finally{ ...... //无论是否发生异常,都无条件执行的语句 }
- 整体执行过程:
- 如果在程序运行时,try块中的代码没有发生异常,那么catch所有的分支都不执行
- 如果在程序运行时,try块中的代码发生了异常,根据异常对象的类型,将从上到下选择第一个匹配的catch分支执行。此时try中发生异常的语句下面的代码将不执行,而整个try…catch之后的代码可以继续运行。
- 如果在程序运行时,try块中的代码发生了异常,但是所有catch分支都无法匹配(捕获)这个异常,那么JVM将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了。
- finally 的使用:
- 因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都需要执行
- 不论在try代码块中是否发生了异常事件 catch语句是否执行 catch语句是否有异常 catch语句中是否有return finally块中的语句都会被执行。
public static void main(String[] args) { int result = test("a"); System.out.println(result); } public static int test(String str) { try { Integer.parseInt(str); // 会出异常 return 1; } catch (NumberFormatException e) { return -1; } finally { System.out.println("test结束"); return 0; } } // test结束 // 0
// finally 块中的代码会执行 但不会改变最终的返回值 public static void main(String[] args) { int result = test(); System.out.println(result); } public static int test() { int i = 100; try { return i; } finally { i++; } }
- 整体执行过程:
-
throws + 异常类型
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{}
3. 手动抛出异常
Java 中异常对象的生成有两种方式:
- 由虚拟机自动生成:在程序运行过程中 虚拟机检测到当前程序发生了问题 会自动创建一个对应异常类的实例并抛出
- 由开发人员手动创建:如果程序创建好的异常不抛出对程序没有任何影响 一旦 throw 就会对程序产生影响
throw new 异常类名(参数);
throw语句抛出的异常对象,和JVM自动创建和抛出的异常对象一样。可以抛出的异常必须是Throwable或其子类的实例。
4. 自定义异常
- 要继承一个异常类型:
- 自定义一个编译时异常类型:自定义类继承 java.lang.Exception
- 自定义一个运行时异常类型:自定义类继承 java.lang.RuntimeException
- 建议大家提供至少两个构造器,一个是无参构造,一个是(String message)构造器
- 自定义异常需要提供 serialVersionUID
public class MyException extends Exception {
private static final long serialVersionUID = 123123123123L;
public MyException(String message) {super(message);}
public MyException() {}
}
public class Test {
public static void main(String[] args) {
try {
throw new MyException("我是自定义异常");
} catch (MyException e) {
e.printStackTrace();
}
}
}
六、多线程
1. 创建和启动线程
- 继承Thread类
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("开启一个线程");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
// myThread.start(); 一个线程只能 start 一次
}
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常 IllegalThreadStateException
-
实现Runnable接口
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
- 创建Runnable实现类的实例,并以此实例作为Thread 的 target参数 来创建Thread对象,该Thread对象才是真正
的线程对象 - 调用线程对象的start()方法,启动线程。调用Runnable接口实现类的run方法
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("启动一个线程"); } } public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); // 要把 Runnable 的实现类作为 Thread 的 target参数传入 Thread thread = new Thread(myRunnable); thread.start(); }
-
实现Callable接口
public class MyCallable implements Callable {
// 1.相比 run 方法可以有返回值
// 2.方法可以抛出异常
// 3.支持泛型的返回值(需要借助FutureTask类,获取返回结果)
@Override
public Object call() throws Exception {
int i = 0;
for (int j = 0; j < 10; j++) {
i += j;
}
return i;
}
}
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
// 1.FutureTask是Futrue接口的唯一的实现类
// 2.FutureTask 同时实现了Runnable, Future接口。
// 它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
FutureTask futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
Object o = null;
try {
o = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println(o);
}
-
使用线程池
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行 Runnable<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般用来执行 Callablevoid shutdown()
:关闭连接池
- Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(int nThreads)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(int corePoolSize)
:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
// 创建一个线程池 ExecutorService service = Executors.newFixedThreadPool(10); // 执行 Runnable MyRunnable myRunnable = new MyRunnable(); service.execute(myRunnable); // 执行 Callable MyCallable myCallable = new MyCallable(); Future submit = service.submit(myCallable); try { Object o = submit.get(); System.out.println(o); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } // 关闭线程池 service.shutdown();
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。
2. Thread类
-
构造器
public Thread() :分配一个新的线程对象。 public Thread(String name) :分配一个指定名字的新的线程对象。 public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法 public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
-
常用方法
public void run() :此线程要执行的任务在此处定义代码。 public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。 public String getName() :获取当前线程名称。 public void setName(String name):设置该线程名称。 public static Thread currentThread() :返回对当前正在执行的线程对象的引用。 public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。 public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。 public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。 void join() :等待该线程终止。 void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。 public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。 void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时,不建议使用。 public final int getPriority() :返回线程优先级 public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
3. 线程安全问题及解决
- 为什么会出现线程安全问题:
-
局部变量不能共享
class Window extends Thread { public void run() { int ticket = 100; while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } // 此时每个窗口都有100张票
-
不同对象的实例变量不能共享
class Window extends Thread { private int ticket = 100; public void run() { while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } // 此时每个窗口都有100张票
-
静态变量是共享的
class Window extends Thread { private static int ticket = 100; public void run() { while (ticket > 0) { try { Thread.sleep(10);//加入这个,使得问题暴露的更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } // 此时可能出现不同窗口卖同一张票的情况
-
同一个对象的实例变量是共享的
class Window implements Runnable { private int ticket = 100; public void run() { while (ticket > 0) { try { Thread.sleep(10);//加入这个,使得问题暴露的更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } } } // 此时可能出现不同窗口卖同一张票的情况
4. 同步机制解决线程安全问题
- 同步代码块:表示这个区域的资源访问是互斥的
synchronized(同步锁){
需要同步操作的代码
}
-
同步方法:表示同一时刻只有一个线程能进入该方法
public synchronized void method(){ 可能会产生线程安全问题的代码 }
-
同步锁是什么:
- 同步锁对象可以是任意类型,但是必须保证竞争"同一个共享资源"的多个线程必须使用同一个"同步锁对象"
- 对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或 类名.class) 但是对于同步方法来说 同步锁对象只能是默认的:
- 静态方法:当前类的Class对象(类名.class)
- 非静态方法:this
-
Lock(锁)
class LockTest{
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private final ReentrantLock lock = new ReenTrantLock();
public void method(){
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
5. 线程的通信
-
等待唤醒机制
- wait()/wait(time):线程不再活动 不再参与调度 进入wait set 中 因此也不会浪费CPU资源 也不会去参与锁的竞争。需要等待其他线程执行 notify 或者等待时间到 这个线程才从 wait set 中释放出来 重新进入调度队列
- notify:选取 wait set 中的一个线程释放
- notifyAll:释放 wait set 中的全部线程
被唤醒的线程也不会马上执行 需要等到再次获取到锁以后才会继续从wait方法之后的地方恢复执行
- wait方法与notify方法必须要由 同一个锁对象调用
- wait方法与notify方法是属于Object类的方法的。
- wait方法与notify方法必须要在 同步代码块 或者是 同步函数 中使用
-
sleep() 和 wait()
- sleep 和 wait 执行后 线程都进入阻塞状态
- sleep 定义在 Thread类 wait 定义在 Object 类
- sleep 可以在任意位置调用 wait 必须在同步代码块或方法中调用
- sleep 不会释放同步锁 wait 会释放同步锁
- sleep 必须指定时间 时间一到就结束阻塞 wait 可以指定时间也可以不指定时间 可以通过 notify/notifyAll 方法结束阻塞
6. 线程的声明周期
在 java.lang.Thread.State 的枚举类中这样定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:新建。仅仅由 JVM 分配了内存 并初始化了实例变量。
- RUNNABLE:可运行。调用start() 方法后 此时对象被标记为可运行进入就绪状态。至于什么时候被真正执行需要等待被调度。或调用 notify/notifyAll/join结束/wait结束 后。
- TERMINATED:被终止。表示线程已经结束生命周期 运行停止。
- BLOCKED:阻塞状态。等待同步监视器(同步锁)的线程处于这一状态。
- WAITING:无限等待。一个正在无限期等待另一个线程唤醒的线程处于这一状态。调用 join(time)/wait(time)等方法时。
- TIMED_WAITING:计时等待。一个正在有限期等待另一个线程唤醒的线程处于这一状态。调用 join/wait 等方法时。
七、常用类和基础API
1. String
-
String的特性
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { private final char value[]; // ... }
-
String 类 是final 的,意味着我们不能继承String
-
String对象的字符内容是存储在一个字符数组value[]中
- private意味着外面无法直接获取字符数组,而且String没有提供value的get和set方法
- final意味着字符数组的引用不可改变,而且String也没有提供方法来修改value数组某个元素值
- 因此字符串的字符数组内容也不可变的,即String代表着不可变的字符序列。即,一旦对字符串进行修改,就会产生新对象。
-
-
String的内存结构
- 因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对象。字符串常量池在堆空间。
String str = new String(“abc”) 创建了几个对象?
创建了两个对象。
一个对象是字符串常量"abc" 在编译器被存储到字符串常量池中 所以它是不可变的。
另一个对象是通过 new 关键字在堆内存中被创建的 String 对象 并将字符串常量池中的字符串拷贝到堆内存中 这个操作会创建一个可变的字符串对象 且与字符串常量池中的对象不是同一个对象。
-
字符串拼接
String s1 = "hello"; String s2 = "world"; String s3 = "hello" + "world"; // 编译器会自动和优化为 helloworld String s4 = s1 + "world"; String s5 = s1 + s2; String s6 = (s1 + s2).intern(); System.out.println(s3 == s4); // false System.out.println(s3 == s5); // false System.out.println(s4 == s5); // false System.out.println(s3 == s6); // true
常量+常量:结果是常量池。且常量池中不会存在相同内容的常量
常量与变量 或 变量与变量:结果在堆中
拼接后调用intern方法:返回值在常量池中
concat 方法拼接,哪怕是两个常量对象拼接,结果也是在堆。因为在代码中 new 了一个新 String。
-
StringBuffer 和 StringBuilder
StringBuffer 代表可变的字符序列。在 JDK 1.0 中声明 可以对字符串内容进行增删 不会产生新对象。是线程安全的。
StringBuilder 在 JDK 5.0 中引入。是线程不安全的。
String str = null; StringBuffer sb = new StringBuffer(); sb.append(str); System.out.println(sb.length());//4 System.out.println(sb);//null StringBuffer sb1 = new StringBuffer(str); System.out.println(sb1);// 报错
原因如下:
// append 方法对应 null 进行了特殊处理 public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; } // 此时调用了 .length 方法 会有空指针异常 public StringBuffer(CharSequence seq) { this(seq.length() + 16); append(seq); }
2. 日期时间API
-
JDK 8 以前
- java.util.Date
// 获取当前毫秒级时间戳 long time = System.currentTimeMillis(); System.out.println(time); // 1691738523697 // 获取当前时间的 Date 对象 Date date = new Date(); System.out.println(date); // Fri Aug 11 15:22:03 CST 2023 // 在 Date 对象中获取毫秒级时间戳 long time1 = date.getTime(); System.out.println(time1); // 1691738523698 // 根据毫秒级时间戳创建 Date 对象 Date date1 = new Date(1691738474609L); System.out.println(date1); // Fri Aug 11 15:21:14 CST 2023 // 格式化日期 date 转 字符串 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒 E Z"); String str = simpleDateFormat.format(date); System.out.println(str); // 2023年08月11日 15时24分00秒 404毫秒 星期五 +0800 // 解析日期 字符串 转 date String str1 = "2023年08月19日 16时03分14秒 545毫秒 星期四 +0800"; SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒 E Z"); Date date2 = null; try { date2 = sf.parse(str1); } catch (ParseException e) { e.printStackTrace(); } System.out.println(date2);
-
java.util.Calendar
Date类的API大部分被废弃了 换为Calendar。Calendar 是一个抽象类。可以修改或获取 YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND等 日历 对应的时间值。
// 实例化 Calendar 类 Calendar calendar = Calendar.getInstance(); // 返回给定日历字段的值 int year = calendar.get(Calendar.YEAR); System.out.println(year); // 2023 // 将给定的日历字段设置为指定的值 calendar.set(Calendar.YEAR, 2022); // 将Calendar转成Date对象 Date date = calendar.getTime(); System.out.println(date); // Thu Aug 11 15:33:53 CST 2022 // 根据日历的规则,为给定的日历字段添加或者减去指定的时间量 calendar.add(Calendar.YEAR,5); Date date2 = calendar.getTime(); System.out.println(date2); // Wed Aug 11 15:33:53 CST 2027 // 使用指定的Date对象重置Calendar的时间 calendar.setTime(date); Date date3 = calendar.getTime(); System.out.println(date3); // Thu Aug 11 15:33:53 CST 2022
-
JKD8 以后
- 本地日期时间:LocalDate、LocalTime、LocalDateTime
// 日期 LocalDate localDate = LocalDate.now(); System.out.println(localDate); // 2023-08-11 // 时间 LocalTime localTime = LocalTime.now(); System.out.println(localTime); // 15:54:54.552 // 日期时间 LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); // 2023-08-11T15:54:54.552 // 从当前对象减去几月、几周、几天、几年、几小时 LocalDateTime minusLocalDateTime = localDateTime.minus(1, ChronoUnit.YEARS); System.out.println(minusLocalDateTime); // 2022-08-11T15:54:54.552 // 向当前对象添加几天、几周、几个月、几年、几小时 LocalDateTime plusLocalDateTime = localDateTime.plus(10, ChronoUnit.YEARS); System.out.println(plusLocalDateTime); // 2033-08-11T15:54:54.552 // get类方法 int year = localDateTime.getYear(); int month = localDateTime.getMonth().getValue(); int dayOfMonth = localDateTime.getDayOfMonth(); int hour = localDateTime.getHour(); int minute = localDateTime.getMinute(); int second = localDateTime.getSecond(); System.out.println(year + "-" + month + "-" + dayOfMonth + " " + hour + ":" + minute + ":" + second); // 2023-8-11 15:54:54
- 瞬时:Instant
// 静态方法,返回默认UTC时区的Instant类的对象 大陆与UTC的时差为+8 Instant now = Instant.now(); System.out.println(now); // 2023-08-11T07:54:54.552Z // 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象 Instant instant = Instant.ofEpochMilli(10); System.out.println(instant); // 1970-01-01T00:00:00.010Z // 结合即时的偏移来创建一个 OffsetDateTime OffsetDateTime offsetDateTime = now.atOffset(ZoneOffset.ofHours(8)); System.out.println(offsetDateTime); // 2023-08-11T15:54:54.552+08:00 // 返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳 long l = now.toEpochMilli(); System.out.println(l); // 1691740494552
- 日期时间格式化:DateTimeFormatter
// 格式化 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"); String strDateTime = dateTimeFormatter.format(LocalDateTime.now()); System.out.println(strDateTime); // 2023/08/11 15:57:21 // 解析 TemporalAccessor accessor = dateTimeFormatter.parse("2023/08/11 21:05:42"); LocalDateTime localDateTime2 = LocalDateTime.from(accessor); System.out.println(localDateTime2); // 2023-08-11T21:05:42
- 持续日期/时间:Period和Duration
- Duration:用于计算两个"时间"间隔
- Period:用于计算两个"日期"间隔
- TemporalAdjuster : 时间校正器
// 获取当前日期的下一个周日是哪天 TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY); LocalDateTime localDateTime3 = LocalDateTime.now().with(temporalAdjuster); System.out.println(localDateTime3);
3. 比较器
- 自然排序:java.lang.Comparable
- Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
- 实现 Comparable 的类必须实现
compareTo(Object obj)
方法 两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj 则返回正整数。如果当前对象this小于形参对象obj 则返回负整数。如果当前对象this等于形参对象obj,则返回零。
public interface Comparable<T> {
public int compareTo(T o);
}
- 定制排序:java.util.Comparator
- 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码。
- 如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用。
public interface Comparator<T> {
int compare(T o1, T o2)
}
4. 数学相关API
-
java.lang.Math
绝对值:public static double abs(double a) 返回大于等于参数的最小的整数:public static double ceil(double a) Math.ceil(-3.3)// -3 返回小于等于参数最大的整数:public static double floor(double a) Math.floor(-3.3) // -4 四舍五入:public static long round(double a) Math.round(-3.3) // -3 Math.round(-3.8)// -4
-
BigInteger
BigInteger(String val) // 根据字符串构建BigInteger对象
- Integer类作为int的包装类,能存储的最大整型值为231-1,Long类也是有限的,最大为263-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。
- java.math包的BigInteger可以表示 不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。
-
BigDecimal
- 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到java.math.BigDecimal类。BigDecimal类支持不可变的、任意精度的有符号十进制定点数。
public BigDecimal(String val)
加:add 减:subtract 乘:multiply 除:divide 取余:remainder 去余和商:divideAndRemainder
public BigDecimal
divide
(BigDecimal divisor, int scale, int roundingMode):divisor是除数,scale指明保留几位小数,roundingMode指明舍入模式(ROUND_UP :向上加1、ROUND_DOWN :直接舍去、ROUND_HALF_UP:四舍五入)
八、集合
1. 数组的特点和弊端
- 数组初始化以后,长度就不可变了,不便于扩展
- 数组中提供的属性和方法少,不便于进行添加、删除、插入、获取元素个数等操作,且效率不高
- 数组存储数据的特点单一,只能存储有序的、可以重复的数据
2. Java 集合体系
-
Collection 接口:用于存储一个一个的数据,也称 单列数据集合
-
List 接口:用来存储有序的、可以重复的数据
-
Set 接口:用来存储无序的、不可重复的数据
-
-
Map 接口:用于存储具有映射关系"key-value对"的集合,即一对一对的数据,也称 双列数据集合
3. Collection 接口
- add(E obj):添加元素对象到当前集合中
- addAll(Collection other):添加other集合中的所有元素对象到当前集合中
- int size():获取当前集合中实际存储的元素个数
- boolean isEmpty():判断当前集合是否为空集合
- boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素
- boolean containsAll(Collection coll):判断coll集合中的元素是否在当前集合中都存在。即coll集合是否是当前集合的"子集"
- boolean equals(Object obj):判断当前集合与obj是否相等
- void clear():清空集合元素
- boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素
- boolean removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素
- boolean retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与coll集合中的元素相同的元素,即当前集合中仅保留两个集合的交集
- Object[] toArray():返回包含当前集合中所有元素的数组
- hashCode():获取集合对象的哈希值
- terator():返回迭代器对象,用于集合遍历
4. Iterator 接口
- Iterator 迭代器对象在遍历集合时 内部采用指针的方式来跟踪集合中的元素
public E next():指针下移 将下移以后集合位置上的元素返回
public boolean hasNext():判断是否还有下一个元素
Iterator iter = coll.iterator();
while(iter.hasNext()){
Object obj = iter.next();
if(obj.equals("Tom")){
iter.remove();
}
}
// Iterator 中的 remove 可以按条件删除
- foreach循环:专门用来 遍历数组和集合
for(元素的数据类型 局部变量 : Collection集合或数组){
//操作局部变量的输出操作
}
它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。
当你使用
foreach
循环遍历集合时,迭代器会在背后被创建并用于访问集合元素。这个迭代器保留了对集合的引用和迭代的位置。当你在循环中进行增删操作时,会直接修改了集合,而不是通过迭代器进行操作。这会导致迭代器维护的状态和实际集合的状态不一致。
5. List接口
存储有序的、可重复的数据
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
Object get(int index):获取指定index位置的元素
List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
int indexOf(Object obj:返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
Object remove(int index):移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele
5.1 主要实现类:ArrayList
// 属性
transient Object[] elementData; // 一个 Object[] 数组
private int size; // 记录数组的长度
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //初始化为空数组
}
// 扩容代码
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 当前数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新数组容量是旧数组容量的1.5倍 右移一位*1/2
// 看旧数组的1.5倍是否够
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 看旧数组的1.5倍是否超过最大数组限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 复制一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
5.2 主要实现类:Vector
ArrayList的古老实现 是线程安全的
ArrayList 与 Vector 的区别:
- ArrayList 线程不安全 Vector 线程安全
- ArrayList 默认扩容为原来的1.5倍,Vector 默认扩容增加为原来的2倍
- 如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector的内部数组的初始容量默认为10。而ArrayList在 JDK 6.0 及之前的版本也是10,JDK8.0 之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。
5.3 主要实现类:LinkedList
每个元素之间会多一个双向链表
// 属性
transient Node<E> first; //记录第一个结点的位置
transient Node<E> last; //记录当前链表的尾元素
transient int size = 0; // 记录最后一个结点的位置
// 其中,Node类定义如下
private static class Node<E> {
E item; //元素数据
Node<E> next; //下一个结点
Node<E> prev; //前一个结点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
6. Set接口
存储无序的、不可重复的数据
6.1 主要实现类:HashSet
HashSet 的内部实现确实就是 HashMap。Set 中的元素存储在 HashMap 的 key 中,value 是一个 Object。
不能保证元素的排列顺序
HashSet 线程不安全
集合元素可以是 null
HashSet 集合 判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的哈希值相等,并且两个对象的 equals() 方法返回值为true。
对于存放在 Set 容器中的对象,对应的类一定要重写hashCode()和equals(Object obj)方法,以实现对象相等规则。即:“equals 相同的对象 hashcode 一定相等”。
HashSet集合中元素的无序性,不等同于随机性。这里的无序性与元素的添加位置有关。具体来说:我们在添加每一个元素到数组中时,具体的存储位置是由元素的hashCode()调用后返回的hash值决定的。导致在数组中每个元素不是依次紧密存放的,表现出一定的无序性。
HashSet中添加元素的过程:
- 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法得到该对象的 hashCode值,然后根据 hashCode值,通过某个散列函数决定该对象在 HashSet 底层数组中的存储位置
- 如果要在数组中存储的位置上没有元素,则直接添加成功
- 如果要在数组中存储的位置上有元素,则继续比较:
- 如果两个元素的hashCode值不相等,则添加成功
- 如果两个元素的hashCode()值相等,则会继续调用equals()方法
- 如果equals()方法结果为false,则添加成功
- 如果equals()方法结果为true,则添加失败
6.3 主要实现类:LinkedHashSet
LinkedHashSet 是 HashSet 的子类。LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用 双向链表 维护元素的次序,这使得元素看起来是以添加顺序 保存的。
6.4 主要实现类:TreeSet
- TreeSet 是 SortedSet 接口的实现类,TreeSet 可以按照添加的元素的指定的属性的大小顺序进行遍历。
- 添加到 TreeSet 中的元素必须实现 Comparable 接口 或者 在创建 TreeSet 对象的时候实现 Comparator。
- 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是 同一个类的对象。
- 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过
compareTo(Object obj) 或compare(Object o1,Object o2)
方法比较返回值。返回值为0,则认为两个对象相等。
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return 0;
}
});
7. Map 接口
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
7.1 主要实现类:HashMap
HashMap是线程不安全的。允许添加 null 键和 null 值。
存储数据采用的哈希表结构,底层使用
一维数组
+单向链表
+红黑树
进行key-value数据的存储。与HashSet一样,元素的存取顺序不能保证一致。HashMap
判断两个key相等的标准
是:两个 key 的hashCode值相等,通过 equals() 方法返回 true。HashMap
判断两个value相等的标准
是:两个 value 通过 equals() 方法返回 true。
7.1.1 数据添加过程分析
1. 在 JDK 7 以前创建一个 长度为16的Entry[] table的数组。在 JDK 8 以后并没有初始化 在添加第一个元素的时候初始化。
2. 将 (key1,value1) 添加到当前 hashmap 的对象中。首先会调用 key1 所在类的hashCode()方法,计算key1的**哈希值1**
3. 此**哈希值1**再经过某种运算**hash()**,得到**哈希值2**。此**哈希值2**再经过某种运算**indexFor()**,确定在底层table数组中的索引位置 index
4. 如果数组索引为 index 上的数据为空,则 (key1,value1 ) 直接添加成功
5. 如果数组索引为 index 上的数据不为空,有 (key2,value2),则需要进一步判断
1. 判断 key1 的**哈希值2**与 key2 的 **哈希值2**是否相同
2. 如果哈希值不同,则(key1,value1)直接添加成功
1. 如果哈希值相同,则需要继续调用key1所在类的equals()方法,将key2放入equals()形参进行判断
2. equals 方法返回false : 则(key1,value1)直接添加成功
3. equals 方法返回true : 默认情况下,value1会覆盖 value2
- Node
public class HashMap<K,V>{
transient Node<K,V>[] table;
// Node类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// ...
}
// TreeNode类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red; // 是红结点还是黑结点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
//....
}
- 属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子
static final int TREEIFY_THRESHOLD = 8; // 默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;// 默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表
//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量64
transient Node<K,V>[] table; // 数组
transient int size; // 记录有效映射关系的对数,也是Entry对象的个数
int threshold; // 阈值,当size达到阈值时,考虑扩容
final float loadFactor; // 加载因子,影响扩容的频率
- 构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// ...其他字段都是默认值
}
public HashMap(int initialCapacity, float loadFactor) {
//校验initialCapacity合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//校验initialCapacity合法性
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//校验loadFactor合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
//计算得到table数组的长度(保证capacity是2的整次幂)
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//加载因子,初始化为0.75
this.loadFactor = loadFactor;
// threshold 初始为默认容量
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化table数组
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
- put() 方法
public V put(K key, V value) {
// 如果key是null,单独处理,存储到table[0]中,如果有另一个key为null,value覆盖
if (key == null)
return putForNullKey(value);
//对key的hashCode进行干扰,算出一个hash值
/*
hashCode值 xxxxxxxxxx
table.length-1 000001111
hashCode值 xxxxxxxxxx 无符号右移几位和原来的hashCode值做^运算,使得hashCode高位二进制值参与计算,
也发挥作用,降低index冲突的概率。
*/
int hash = hash(key);
//计算新的映射关系应该存到table[i]位置,
//i = hash & table.length-1,可以保证i在[0,table.length-1]范围内
int i = indexFor(hash, table.length);
//检查table[i]下面有没有key与我新的映射关系的key重复,如果重复替换value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//添加新的映射关系
addEntry(hash, key, value, i);
return null;
}
// 如果key是null,直接存入[0]的位置
private V putForNullKey(V value) {
//判断是否有重复的key,如果有重复的,就替换value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 把新的映射关系存入[0]的位置,而且key的hash值用0表示
addEntry(0, null, value, 0);
return null;
}
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否需要库容
//扩容:(1)size达到阈值(2)table[i]正好非空
if ((size >= threshold) && (null != table[bucketIndex])) {
//table扩容为原来的2倍,并且扩容后,会重新调整所有key-value的存储位置
resize(2 * table.length);
//新的key-value的hash和index也会重新计算
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//存入table中
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//原来table[i]下面的映射关系作为新的映射关系next
table[bucketIndex] = new Entry<>(hash, key, value, e);
//个数增加
size++;
}
7.1.2 HashMap 相关问题:
- 说说你理解的哈希算法
答:哈希算法 是一种将任意长度的输入数据映射成固定长度输出的算法。它的主要目的是将输入数据转换为哈希值,该哈希值通常是一个较短且固定长度的字符串或数字。
唯一性:哈希算法通过执行特定的计算将输入转换为唯一的哈希值。由于哈希值长度是固定的 可能存在输入不同数据生成相同哈希值的情况。好的哈希算法应尽量避免哈希碰撞的概率。
不可逆性:无法从哈希值推导出原始输入数据。
一致性:同一输入数据在多次执行哈希算法是应生成相同的哈希值。
固定长度:哈希算法生成的哈希值通常是固定长度的,无论数据大小。
- Entry中的hash属性为什么不直接使用key的hashCode()返回值呢
答:为了提高hash属性 和 数组长度的 按位与 操作后 尽可能的分部均匀。
- HashMap是如何决定某个key-value存在哪个桶的呢
答:hash 值 & (table.length-1),任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围
- 为什么要保持table数组一直是2的n次幂呢
答:因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到
- 解决[index]冲突问题
答:数组+链表+红黑树
- 为什么JDK1.8会出现红黑树和链表共存呢
答:当链表过长时查询效率会大大降低 如果只使用红黑树占用内存较多。
- 载因子的值大小有什么关系
答:加载因子决定了 table 数组何时扩容。太大导致冲突严重 链表结构节点个数增多 查询变慢。太小导致扩容频繁 造成空间浪费。
- 什么时候树化?什么时候反树化
答:当一个节点下的链表长度大于等于8 且table数组的长度大于等于64时 会树化。当节点下链表长度小于等于6时会反树化。
- key-value中的key是否可以修改
答:如果 key 修改了会导致 hash值的更改 从而导致查询的时候查询不到原有的数据。
- JDK1.7中HashMap的循环链表是怎么回事?如何解决?
答:在多线程的环境下 JDK7的头插法会导致循环链表。JDK8中用尾插法解决。
7.2 主要实现类:LinkedHashMap
LinkedHashMap 是 HashMap 的子类。在HashMap存储结构的基础上,使用了一对 双向链表 来 记录添加元素的先后顺序 ,可以保证遍历元素时,与添加的顺序一致。通过哈希表结构可以保证键的唯一、不重复,需要键所在类重写hashCode()方法、equals()方法。
static class Entry<K,V> extends HashMap.Node<K,V> {
// 多了一对双向链表
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
7.3 主要实现类:TreeMap
TreeMap存储 key-value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 key-value 对处于 有序状态。和 TreeSet 一样 添加到 TreeMap 中的元素必须实现 Comparable 接口 或者 在创建 TreeMap 对象的时候实现 Comparator。
7.4 主要实现类:Hashtable
Hashtable 是 Map接口 的古老实现类 是线程安全的
与 HashMap 不同,Hashtable 不允许使用 null 作为 key 或 value
7.5 主要实现类:Properties
Properties 类是 Hashtable 的子类,该对象用于处理属性文件
由于属性文件里的 key、value 都是字符串类型,所以 Properties 中要求 key 和 value 都是字符串类型
存取数据时,建议使用 setProperty(String key,String value)方法和 getProperty(String key)方法
Collections工具类
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
int binarySearch(List list,T key,Comparator c)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定
int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。// 只读
boolean addAll(Collection c,T… elements)将所有指定元素添加到指定 collection 中。
Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
九、泛型
1. 泛型的基础说明
-
<类型>这种语法形式就叫泛型。
- <类型>的形式我们称为类型参数,这里的"类型"习惯上使用T表示,是Type的缩写。即:
<T>
。这里的T,可以替换成K,V等任意字母
- <类型>的形式我们称为类型参数,这里的"类型"习惯上使用T表示,是Type的缩写。即:
-
在哪里可以声明类型变量<T>。
- 声明类或接口时
【修饰符】 class 类名<类型变量列表> 【extends 父类】 【implements 接口们】{} 【修饰符】 interface 接口名<类型变量列表> 【implements 接口们】{}
- 声明方法时
[修饰符] <类型变量列表> 返回值类型 方法名([形参列表])[throws 异常列表]{}
2. 自定义泛型类或泛型接口
-
我们在声明完自定义泛型类以后,可以在类的内部(比如:属性、方法、构造器中)使用类的泛型。
-
我们在创建自定义泛型类的对象时,可以指明泛型参数类型。一旦指明,内部凡是使用类的泛型参数的位置,都具体化为指定的类的泛型类型。
-
如果在创建自定义泛型类的对象时,没有指明泛型参数类型,那么泛型将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。
-
泛型的指定中必须使用引用数据类型。不能使用基本数据类型,此时只能使用包装类替换。
-
除创建泛型类对象外,子类继承泛型类时、实现类实现泛型接口时,也可以确定泛型结构中的泛型参数。
-
泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。
-
JDK7.0 开始,泛型的简化操作(钻石操作符):
ArrayList<Fruit> flist = new ArrayList<>()
。 -
如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
-
不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
-
在类/接口上声明的泛型,在本类或本接口中即代表某种类型,但不可以在静态方法中使用类的泛型。
-
异常类不能是带泛型的。
3. 自定义泛型方法
- 方法,也可以被泛型化,与其所在的类是否是泛型类没有关系。
- 泛型方法中的泛型参数在方法被调用时确定。
- 泛型方法可以根据需要,声明为static的。
public class Father<T> {
// 此处方法是静态方法 可以用自己的类型参数E 但不能用类的类型参数T
public static <E> E test() {
return null;
}
}
4. 通配符的使用
泛型(Generics)是 Java 的一项特性,它允许我们在定义类、接口或方法时使用类型参数,以实现对不同类型的通用操作。
通配符(Wildcard)则是用于在泛型类型中表示未知类型的占位符。通配符使用 “?” 符号来表示,可以用在泛型类、泛型方法和泛型接口中作为类型参数的辅助。
即我们在使用泛型的时候仍然不知道这是个什么类型。
<?>
允许所有泛型的引用调用<? extends 类/接口>
使用时指定的类型必须是继承某个类,或者实现某个接口,即<=<? super 类/接口>
使用时指定的类型必须是操作的类或接口,或者是操作的类的父类或接口的父接口,即>=
十、File类和IO流
1. File 类
- 构造器
public File(String pathname):以pathname为路径创建File对象
public File(String parent, String child):以parent为父路径,child为子路径创建File对象
public File(File parent, String child):根据一个父File对象和子文件路径创建File对象
- 常用方法
public String getName():获取名称
public String getPath():获取路径
public String getAbsolutePath():获取绝对路径
public File getAbsoluteFile():获取绝对路径表示的文件
public String getParent():获取上层文件目录路径。若无,返回null
public long length():获取文件长度(即:字节数)。不能获取目录的长度
public long lastModified():获取最后一次的修改时间,毫秒值
public String[] list():返回一个String数组,表示该File目录中的所有子文件或目录
public File[] listFiles():返回一个File数组,表示该File目录中的所有的子文件或目录
public boolean renameTo(File dest):把文件重命名为指定的文件路径
public boolean exists():此File表示的文件或目录是否实际存在
public boolean isDirectory():此File表示的是否为目录
public boolean isFile():此File表示的是否为文件
public boolean canRead():判断是否可读
public boolean canWrite():判断是否可写
public boolean isHidden():判断是否隐藏
public boolean createNewFile():创建文件。若文件存在,则不创建,返回false。
public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建。
public boolean delete():删除文件或者文件夹。删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。
2. IO 流
Input:输入流。读取外部数据到内存中。
Output:输出流。从内存读取数据到外部存储设备中。
2.1 流的分类
- 按流的流向不同分为:输入流和输出流。
- 按操作数据单位的不同分为:字节流(以InputStream/OutputStream结尾)和字符流(以Reader/Writer结尾)。
- 按角色的不同分为:节点流(直接从数据源到目的地)和处理流(在已有的链接之上 通过对数据的处理提供更强大的读写功能)。
2.2 节点流
- FileReader\FileWriter:Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不能操作图片,视频等非文本文件
// 复制一份 hello.txt
File src = new File("hello.txt");
File dest = new File("hello_copy.txt");
FileReader fileReader = new FileReader(src);
FileWriter fileWriter = new FileWriter(dest);
// 缓存数组
char[] buffer = new char[5];
int len;
// 每次从缓存数组中读取 不为-1表示还有数据
while ((len = fileReader.read(buffer)) != -1) {
fileWriter.write(buffer, 0, len);
}
- FileInputStream\FileOutputStream:如果我们读取或写出的数据是非文本文件,则Reader、Writer就无能为力了,必须使用字节流
File src = new File("hello.txt");
File dest = new File("hello_copy2.txt");
FileInputStream fileInputStream = new FileInputStream(src);
FileOutputStream fileOutputStream = new FileOutputStream(dest);
byte[] buffer = new byte[1024]; // 1kb
int len;
while ((len = fileInputStream.read(buffer)) != -1) {
// 原封不动的复制一份 是没问题的 如果中途要看数组里内容可能会出现乱码 因为缓存数组的长度导致
// String s = new String(buffer, 0, len);
// System.out.println(s);
fileOutputStream.write(buffer, 0, len);
}
2.3 处理流
2.3.1 缓冲流
- 字节缓冲流
File src = new File("hhh.png");
File dest = new File("hhh_copy2.png");
// 节点流
FileInputStream fileInputStream = new FileInputStream(src);
FileOutputStream fileOutputStream = new FileOutputStream(dest);
// 对节点流包一层
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
// 缓存数组
byte[] buffer = new byte[1024];
int len;
while ((len = bufferedInputStream.read(buffer)) != -1) {
bufferedOutputStream.write(buffer, 0, len);
}
// 资源关闭 有四个流
// 先关外层 由于外层流的关闭 也会自动对内层流进行关闭操作 所以可以省略内层流的关闭
bufferedOutputStream.close();
bufferedInputStream.close();
- 字符缓冲流
File src = new File("hello.txt");
File dest = new File("hello_1.txt");
FileReader fileReader = new FileReader(src);
FileWriter fileWriter = new FileWriter(dest);
// 对节点流包一层
BufferedReader bufferedReader = new BufferedReader(fileReader);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
String data;
// 一行一行的读
while ((data = bufferedReader.readLine()) != null) {
bufferedWriter.write(data);
bufferedWriter.newLine();
// 刷新方法:每当调用此方法时 就会将内存中的数据写出到磁盘文件
bufferedWriter.flush();
}
// 在资源关闭的时候就会刷新出去 希望在执行过程中及时的刷新 可以调 flush 方法
bufferedWriter.close();
bufferedReader.close();
File src = new File("hello.txt");
FileReader fileReader = new FileReader(src);
BufferedReader bufferedReader = new BufferedReader(fileReader);
char[] buffer = new char[1024];
int len;
// 读缓存数组
while ((len = bufferedReader.read(buffer)) != -1) {
String s = new String(buffer, 0, len);
System.out.print(s);
}
bufferedReader.close();
2.3.2 转换流
字节和字符之间的桥梁
字节流——InputStreamReader——字符流(读入为字符)
// 从磁盘读入文件
File file = new File("utf_8.txt");
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); // 使用的默认的IDEA UTF-8
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
char[] cBuffer = new char[1024];
int len;
while ((len = inputStreamReader.read(cBuffer)) != -1) {
String s = new String(cBuffer, 0, len);
System.out.print(s);
}
inputStreamReader.close();
字符流——OutputStreamWriter——字节流(输出为字节)
-
对象流:序列化 ObjectOutputStream(内存至文件)/ 反序列化 ObjectInputStream(文件至内存)
-
对象序列化机制:允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
-
序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该
对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存
了一个对象的信息。 public final void writeObject (Object obj) -
反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行
反序列化
。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。public final Object readObject ()
-
-
如何实现序列化机制:实现 java.io.Serializable 接口。
- 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现
Serializable
接口 - 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰 静态(static)变量
的值不会序列化。因为静态变量的值不属于某个对象
- 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现
-
Serializable
接口给需要序列化的类,提供了一个序列版本号:serialVersionUID
。凡是实现 Serializable接口的类都应该有一个表示序列化版本标识符的静态变量:static final long serialVersionUID = 234242343243L; //它的值由程序员随意指定即可。
-
serialVersionUID用来表明类的不同版本间的兼容性。简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
-
如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节
自动生成
的。若类的实例变量做了修改,serialVersionUID可能发生变化
。因此,建议显式声明。 -
如果声明了serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。
-
-
2.3.3 打印流
PrintStream(打印字节)/PrintWriter(打印字符)
// 指向一个日志文件
PrintStream out = new PrintStream(new FileOutputStream("log.txt", true));
// 改变输出方向
System.setOut(out);
// 日期当前时间
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);
System.out.println(strTime + ": " + msg);
十一、网络编程
如何准确地定位网络上一台或多台主机:使用 IP 地址
如何定位主机上的特定的应用:使用 端口号 给每一个进程分配不同的端口号(用两个字节表示的整数,它的取值范围是0~65535)
找到主机后,如何可靠、高效地进行数据传输:网络通信协议(TCP/IP 将网络协议分为四层:应用层、传输层、网络层、物理+数据链路层)
InetAddress 一个类的实例 就代表一个具体的 ip 地址
InetAddress byName = InetAddress.getByName(); // 指定ip
InetAddress localHost = InetAddress.getLocalHost(); // 本机ip
String hostName = inetAddress.getHostName(); // 获取域名
String hostAddress = inetAddress.getHostAddress(); // 获取ip地址
1. TCP 和 UDP
- TCP:
- TCP协议进行通信的两个应用进程:客户端、服务端。
- 使用TCP协议前,须先
建立TCP连接
,形成基于字节流的传输数据通道 - 传输前,采用“三次握手”方式,点对点通信,是
可靠的
- TCP协议使用
重发机制
,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。
- TCP协议使用
- 在连接中可进行
大数据量的传输
- 传输完毕,需
释放已建立的连接,效率低
TCP 的三次握手 和 四次挥手:
三次握手:
1. 客户端向服务器端发起TCP连接的请求 2. 服务器端发送针对客户端TCP连接请求的确认 3. 客户端发送确认的确认
四次挥手:
- 客户端向服务器端提出结束连接,
让服务器做最后的准备工作
。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。- 服务器接收到客户端释放连接的请求后,
会将最后的数据发给客户端
。并告知上层的应用进程不再接收数据。- 服务器发送完数据后,会给客户端
发送一个释放连接的报文
。那么客户端接收后就知道可以正式释放连接了。- 客户端接收到服务器最后的释放连接报文后,要
回复一个彻底断开的报文
。这样服务器收到后才会彻底释放连接。
- UDP:
- UDP协议进行通信的两个应用进程:发送端、接收端。
- 将数据、源、目的封装成数据包(传输的基本单位),
不需要建立连接
- 发送不管对方是否准备好,接收方收到也不确认,不能保证数据的完整性,故是
不可靠的
- 每个数据报的大小限制在
64K
内 - 发送数据结束时
无需释放资源,开销小,通信效率高
2. TCP 网络编程
- 客户端发送内容给服务端,服务端将内容打印到控制台上
// 服务端
int port = 8989;
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("服务器端已开启...");
// 阻塞式的方法
Socket accept = serverSocket.accept();
System.out.println("收到了来自于" + accept.getInetAddress().getHostAddress() + "的连接");
// 接收数据
InputStream inputStream = accept.getInputStream();
// 如果太小了就会乱码
byte[] buffer = new byte[3];
// 专门用 ByteArrayOutputStream 接收字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int len;
while ((len = inputStream.read(buffer)) != -1) {
// 把数据写到内存里面
byteArrayOutputStream.write(buffer, 0, len);
}
System.out.println(byteArrayOutputStream.toString());
System.out.println("数据接收完毕");
// 客户端
// 创建一个 socket
InetAddress inetAddress = InetAddress.getByName("192.168.83.208"); // 对方的ip地址
int port = 8989; // 对方的端口号
Socket socket = new Socket(inetAddress, port);
// 发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好,我是客户端".getBytes());
- 从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接
// 服务端
int port = 9090;
ServerSocket serverSocket = new ServerSocket(port);
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
File file = new File("server_1.png");
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
System.out.println("数据接收完毕");
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你的图片发送完毕".getBytes());
outputStream.close();
inputStream.close();
fileOutputStream.close();
socket.close();
serverSocket.close();
// 客户端
InetAddress inetAddress = InetAddress.getByName("192.168.83.208"); // 对方的ip地址
int port = 9090; // 对方的端口号
Socket socket = new Socket(inetAddress, port);
File file = new File("hhh.png");
FileInputStream fileInputStream = new FileInputStream(file);
OutputStream outputStream = socket.getOutputStream();
// 读写数据
byte[] buffer = new byte[1024];
int len;
while ((len = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
System.out.println("发送完毕");
// 要告诉服务端 客户端的图已经发生完成了 不然服务端会一直等着
// Demo02 不等是因为 socket 关闭了
socket.shutdownOutput();
InputStream inputStream = socket.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer1 = new byte[1024];
int len1;
while ((len1 = inputStream.read(buffer1)) != -1) {
byteArrayOutputStream.write(buffer1, 0, len1);
}
System.out.println(byteArrayOutputStream.toString());
byteArrayOutputStream.close();
inputStream.close();
outputStream.close();
fileInputStream.close();
socket.close();
3. UDP 网络编程
// 接收端
int port = 9090;
DatagramSocket datagramSocket = new DatagramSocket(port);
byte[] bytes = new byte[1024 * 64];
DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length);
datagramSocket.receive(datagramPacket);
String str = new String(datagramPacket.getData(), 0, datagramPacket.getLength());
System.out.println(str);
datagramSocket.close();
// 发送端
DatagramSocket datagramSocket = new DatagramSocket();
// 数据都在数据报里面
// 指明发给谁 ip 端口 数据
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
int port = 9090;
byte[] bytes = "我是发送端".getBytes();
DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, inetAddress, port);
datagramSocket.send(datagramPacket);
datagramSocket.close();
4. URL 编程
URL Uniform Resource Locator :统一资源定位符 一个URL就对应着互联网上某一资源的地址
// 将 URL 代表的资源下载到本地
String str = "";
URL url = new URL(str);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 从网络拿到流
InputStream inputStream = connection.getInputStream();
File file = new File("XXX.png");
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
fileOutputStream.close();
inputStream.close();
connection.disconnect();
十二、反射
1. Class类
Class 类是反射的源头。存储在堆空间。
- Class本身也是一个类
- Class 对象只能由系统建立对象
- 一个加载的类在 JVM 中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的一个.class文件
- 每个类的实例都会记得自己是由哪个 Class 实例所生成
- 通过Class可以完整地得到一个类中的所有被加载的结构
- Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象
- 获取Class类的实例
Class clazz = String.class
Class clazz = "www.atguigu.com".getClass();
Class clazz = Class.forName("java.lang.String");
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");
2. 类的加载与ClassLoader
2.1 类的加载
类在内存中的完整声明周期:加载、链接、初始化、使用、卸载。
类的加载过程分为:装载(Loading)、链接(Linking)、初始化(Initialization)三个阶段。
装载:将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
链接:
1. 验证:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
2. 准备:正式为类变量(static)分配内存并`设置类变量默认初始值`的阶段,这些内存都将在方法区中进行分配。
3. 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:
* 执行`类构造器<clinit>()方法`的过程。`类构造器<clinit>()方法`是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
* 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
* 虚拟机会保证一个`类的<clinit>()方法`在多线程环境中被正确加锁和同步。
2.2 ClassLoader
- 类加载器的作用:
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
- 类加载器的分类:
- 启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用
C/C++语言
实现的,嵌套在JVM内部。获取它的对象时往往返回null - 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
- 这个类加载使用
- 扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
- 应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
- 用户自定义类加载器
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@7f31245a
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
Class<Person> personClass = Person.class;
ClassLoader classLoader = personClass.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 对于 java 的核心 api 使用 Bootstrap ClassLoader
Class<?> aClass = Class.forName("java.lang.String");
ClassLoader classLoader1 = aClass.getClassLoader();
System.out.println(classLoader1); // null
- 使用 ClassLoader 获取流
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info.properties");
3. 反射的基本应用
- 创建运行时类的对象
Class<?> clazz = Class.forName("com.demo.Person");
- 通过获取构造器对象来进行实例化
Object obj = clazz.newInstance();
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
Object obj = constructor.newInstance("Tom",12);
-
获取运行时类的完整结构
- 获取所有的属性及相关细节
// 获取当前运行时类中声明的所有属性 Field[] declaredFields = clazz.getDeclaredFields(); for(Field f : declaredFields){ System.out.println(f); } // 权限修饰符 是个数字 /** PUBLIC = 0x00000001; 1 1 PRIVATE = 0x00000002; 2 10 PROTECTED = 0x00000004; 4 100 STATIC = 0x00000008; 8 1000 FINAL = 0x00000010; 16 10000 **/ int modifier = f.getModifiers(); // 变量类型 Class type = f.getType(); // 变量名 String fName = f.getName();
- 获取所有的方法及相关细节
// 获取当前运行时类中声明的所有方法 Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method m : declaredMethods) { System.out.println(m); }
// 权限修饰符 System.out.print(Modifier.toString(m.getModifiers()) + "\t"); // 返回值类型 System.out.print(m.getReturnType().getName() + "\t"); // 方法名 System.out.print(m.getName()); // 参数列表 Class[] parameterTypes = m.getParameterTypes(); // 异常列表 Class[] exceptionTypes = m.getExceptionTypes();
- 获取其他结构(构造器、父类、接口、包、注解等)
// 获取当前类中的所有的构造器 Constructor[] cons = clazz.getDeclaredConstructors(); // 获取运行时类的父类 Class superclass = clazz.getSuperclass(); // 获取运行时类的所在的包 Package pack = clazz.getPackage(); // 获取运行时类的注解 Annotation[] annos = clazz.getAnnotations(); // 获取运行时类所实现的接口 Class[] interfaces = clazz.getInterfaces(); // 获取运行时类的带泛型的父类 Type genericSuperclass = clazz.getGenericSuperclass();
- 获取泛型父类信息
// 只能得到父类名,无法得到父类的泛型实参列表 Type type = clazz.getGenericSuperclass(); ParameterizedType pt = (ParameterizedType) type; // 获取泛型父类的泛型实参列表 Type[] typeArray = pt.getActualTypeArguments();
-
调用运行时类的指定结构
- 调用指定的属性
Class<Person> personClass = Person.class; Person person = personClass.getDeclaredConstructor().newInstance(); // 获取运行时类中指定名的属性 Field name = personClass.getDeclaredField("name"); // java.lang.NoSuchFieldException: name // java.lang.IllegalAccessException: class _reflection.Demo06 cannot access a member of class _reflection.Person with modifiers "private" name.setAccessible(true); name.set(person, "Tom"); Object o = name.get(person); System.out.println(o);
- 调用指定的方法
Class<Person> personClass = Person.class; Person person = personClass.getDeclaredConstructor().newInstance(); // java.lang.NoSuchMethodException: _reflection.Person.showNation(java.lang.String, java.lang.Integer) // 值可以自动装箱 类型不能 Method showNationMethod = personClass.getDeclaredMethod("showNation", String.class, int.class); showNationMethod.setAccessible(true); // 如果 返回值是 void 则 invoke 方法的返回值 为 null Object returnValue = showNationMethod.invoke(person, "Tom", 10); System.out.println(returnValue);
-
调用注解信息
// 获取类声明上的主键 Class<Person> personClass = Person.class; Table tableAnnotation = personClass.getDeclaredAnnotation(Table.class); String value = tableAnnotation.value(); System.out.println(value);
// 获取属性声明的注解信息 Class<Person> personClass = Person.class; Field nameField = personClass.getDeclaredField("name"); Column columnAnnotation = nameField.getDeclaredAnnotation(Column.class); String columnName = columnAnnotation.columnName(); String columnType = columnAnnotation.columnType(); System.out.println(columnName); System.out.println(columnType);
十三、Lambda 表达式 和 StreamAPI
1. Lambda 表达式
->
:被称为 Lambda 操作符 或 箭头操作符。
箭头操作符的左侧 定义了 Lambda 表达式需要的参数列表
箭头操作符的右侧 定义了 Lambda 表达式执行的功能
- 语法一:无参 无返回值
Runnable r2 = () -> {
System.out.println("hello");
};
- 语法二:一个参 无返回值
Consumer<String> con1 = (String s) -> {
System.out.println(s);
};
// 类型推断后
Consumer<String> con1 = (s) -> {
System.out.println(s);
};
// 一个参数时 小括号可省略
Consumer<String> con1 = s -> {
System.out.println(s);
};
- 语法三:多个参数 有返回值
Comparator<Integer> com2 = (o1,o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
// 当 Lambda 体只有一条语句时,return 与大括号若有,都可以省略
Comparator<Integer> com2 = (o1,o2) -> o1.compareTo(o2);
Lambda 表达式的本质:
- Lambda 作为接口实现类的对象
- Lambda 是一个匿名函数
2. 函数式接口
只包含一个抽象方法的接口称为函数式接口。可以通过 Lambda 表达式来创建该接口的对象。我们可以在一个接口上使用 @FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
- 四大函数式接口
函数式接口 | 用途 | |
---|---|---|
Consumer<T> | 消费型接口 | 对类型为T的对象应用操作,包含方法: void accept(T t) |
Supplier<T> | 供给型接口 | 返回类型为T的对象,包含方法:T get() |
Function<T, R> | 函数型接口 | 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t) |
Predicate<T> | 判断型接口 | 确定类型为T的对象是否满足某约束,并返回 boolean 值。包含方法:boolean test(T t) |
只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
3. 方法引用 与 构造器引用
方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
-
方法引用格式
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名
-
方法引用使用前提
- Lambda体只有一句语句,并且是通过调用一个对象的/类现有的方法来完成的
- 针对
对象::实例方法名
:函数式接口中的抽象方法a在被重写时使用了某一个对象的方法b。如果方法a的形参列表、返回值类型与方法b的形参列表、返回值类型都相同。
// Supplier中的T get() Supplier<String> sup1 = () -> emp.getName(); // Employee中的String getName() Supplier<String> sup2 = emp::getName;
- 针对
类::实例方法名
:函数式接口中的抽象方法a在被重写时使用了某一个类的静态方法b。如果方法a的形参列表、返回值类型与方法b的形参列表、返回值类型都相同。
// Comparator中的int compare(T t1,T t2) Comparator<Integer> com1 = (t1,t2) -> Integer.compare(t1,t2); // Integer中的int compare(T t1,T t2) Comparator<Integer> com2 = Integer::compare;
- 针对
类::实例方法名
:函数式接口中的抽象方法a在被重写时使用了某一个对象的方法b。如果方法a的返回值类型与方法b的返回值类型相同,同时方法a的形参列表中有n个参数,方法b的形参列表有n-1个参数。且方法a的第1个参数作为方法b的调用者,且方法a的后n-1参数与方法b的n-1参数匹配。
// Comparator中的int comapre(T t1,T t2) Comparator<String> com1 = (s1,s2) -> s1.compareTo(s2); // String中的int t1.compareTo(t2) Comparator<String> com2 = String :: compareTo;
-
构造器引用格式
当 Lambda 表达式是创建一个对象。并且满足Lambda表达式形参,正好是给创建这个对象的构造器的实参列表,就可以使用构造器引用。
- 类名::new
// Integer 是入参 Employee 是返回值 Function<Integer,Employee> func1 = id -> new Employee(id); Function<Integer,Employee> func2 = Employee :: new; // 调用的构造器是有 int 这个参数的
- 数组类型名::new
当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度,就可以数组构造引用。
// Integer 是入参 String[] 是返回值 Function<Integer,String[]> func1 = length -> new String[length]; Function<Integer,String[]> func2 = String[] :: new;
4. StreamAPI
Stream API 是 Java 8 中引入的一个用于处理集合数据的功能强大的工具。它提供了一种函数式的方式来对集合进行操作和处理,使得代码更加简洁、易读和高效。
-
Stream 的特点:
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果
- Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了
-
Stream的操作三个步骤
-
创建 Stream:一个数据源(如:集合、数组),获取一个流
- 通过集合
default Stream stream() : 返回一个顺序流
default Stream parallelStream() : 返回一个并行流
- 通过数组
static Stream stream(T[] array): 返回一个流
- 通过Stream的of()
public static Stream of(T… values) : 返回一个流
-
中间操作:每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。
方法 描述 filter(Predicate p) 接收 Lambda 从流中排除某些元素 distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素 limit() 截断流,使其元素不超过给定数量 skip() 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流 map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 sorted(Comparator com) 产生一个新流,其中按比较器顺序排序 -
终止操作(终端操作):终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。
方法 描述 allMatch() 检查是否匹配所有元素 anyMatch() 检查是否至少匹配一个元素 noMatch() 检查是否没有匹配所有元素 findFirst() 返回第一个元素 findAny() 返回当前流中的任意元素 count() 返回流中元素总数 max() 返回流中最大值 min() 返回流中最小值 forEach() 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了) reduce() 可以将流中元素反复结合起来,得到一个值。 collect() 将流转换为其他形式。接收一个 Collector 接口的实现。Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。如:toList(把流中元素收集到List)、toSet(把流中元素收集到Set)、toCollection(把流中元素收集到创建的集合)、counting(计算流中元素的个数)、summingInt(对流中元素的整数属性求和)、averagingInt(计算流中元素Integer属性的平均值)、summarizingInt(收集流中Integer属性的统计值)、joining(连接流中每个字符串)、maxBy(根据比较器选择最大值)、minBy(根据比较器选择最小值)、reducing(从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值)、collectingAndThen(包裹另一个收集器,对其结果转换函数)、groupingBy(根据某属性值对流分组,属性为K,结果为V)、partitioningBy(根据true或false进行分区)
-
十四、面试题
-
一个 “.java” 源文件中是否可以包括多个类 有什么限制
可以包括多个类。但是只能有一个公共类(public class)这个公共类作为其他文件引用和使用的入口点 必须与文件名相同。成员内部类虽然可以被 public 修饰,单其不是公共类,它是作为类的成员存在的。
public class Demo01 { public class Demo02 { } } // 此时 Demo01 是入口点 Demo01 demo01 = new Demo01(); Demo01.Demo02 demo02 = demo01.new Demo02();
-
Java 的优势
跨平台、安全、简单、高性能、面向对象、健壮
-
windows 下常用的几个命令行操作都有哪些
dir:列出当前目录下的所有文件和子目录
cd:更改当前文件目录
mkdir:创建新目录
del:删除文件
copy:复制文件
move:移动文件(剪切)
ren:重命名文件
-
Java 中是否存在内存溢出 内存泄漏 如何解决 举例说明
存在。
内存溢出:在程序执行的过程中 申请的内存超出了 JVM 可分配的最大内存限制 从而引发异常。
解决办法:增加 JVM 内存限制。优化代码:减少内存占用。
内存泄露:在程序中存在某些对象的引用 但这些对象已经不再被需要使用 却没有得到正确的释放 随着时间的推移导致未被释放的内存占用越来越多的内存 从而导致内存耗尽。
- 长生命周期对象持有短生命周期对象的引用
private List<Object> objects = new ArrayList<>(); public void addObject(Object obj) { objects.add(obj); } // objects 列表持有了外部传入对象的引用。如果该列表不被清空或者手动移除对这些对象的引用,那这些对象就会一直存在于内存中,无法被垃圾回收。
- 静态集合类持有对象的引用
private static Map<String, Object> cacheMap = new HashMap<>(); public static void addToCache(String key, Object value) { cacheMap.put(key, value); } // 静态 cacheMap 集合会一直持有添加到缓存中的对象的引用。如果没有提供相应的方法来从缓存中移除不再需要的对象,那么这些对象将一直保留在内存中。
解决办法:确保及时释放不再使用的对象。避免静态集合类或静态变量持有对象的引用。
-
如何看待Java是一门半编译半解释型的语言
Java 代码在执行之前需要经过两个阶段的处理:编译和解释。
编译:Java 源代码会被编译器编译成 class文件。
解释:JVM 会把 class 文件逐行解释成机器码并执行。
但是在执行过程中并不是完全解释执行,也不是纯粹的编译执行。
编译优化:在编译阶段,编译器对字节码进行了优化处理,例如去除无用代码、内联方法调用等,以提高程序的执行效率。
即时编译:将热点代码(经常执行的代码块)编译成本地机器码,以提高这部分代码的执行速度。
-
高效的方式计算2 * 8的值
2<<3 或 8 <<1
-
&和&&的区别
&&:短路与 当左边的表达式为false 不再执行右边的表达式
&:当左右两边表达式返回类型为 boolean 时作为逻辑 且左右两边的表达式都要执行。
当左右两边表达式返回类型为 数值 时 作为位运算符 1&1=1 (一假即假)
-
Java中的基本类型有哪些 String 是最基本的数据类型吗
byte、short、int、long、float、double、char、boolean。
String 不是基本数据类型
-
Java 开发中计算金额时使用什么数据类型
BigDecimal 类
-
char型变量中能不能存储一个中文汉字 为什么
能。char 使用的是 unicode 字符集。包含了世界范围内的所有字符。
-
short s1=1; s1=s1+1; 有什么错
s1 的类型必须是int。在强制类型提升时 int 作为了数值的默认类型。如果想要保留 s1 的类型为 short 可以使用自增或自减。
-
int i=0; i=i++执行这两句化后变量 i 的值为
i 的值为0。
-
如何将两个变量的值互换
使用中间变量。
String s1 = "abc"; String s2 = "123"; String temp = s1; s1 = s2; s2 = temp;
-
boolean 占几个字节
boolean 类型 JVM 分配内存空间的时候分配一个槽位即4个字节。
-
为什么Java中 0.1 + 0.2 结果不是 0.3
对于小数。并不是所有的小数都能用准确的二进制进行表达。
-
break 和 continue 的作用
break:跳出当前循环
continue:跳出当次循环
break 和 continue 后都不能再编写语句
-
if 分支语句 和 switch 分支语句的异同之处
if 分支语句 和 switch 分支语句 都用于条件判断。
if 分支语句 可以判断范围区间和常量。
switch 分支语句 只能判断常量。可选类型有 byte、short、int 、char 、String、Enum。对于浮点型和布尔型不行。
-
switch 语句中忘写 break 会发生什么
case 穿透。会不再判断是否符合条件 一直执行 直到遇到下一个 break 或 return
-
Java 支持哪些类型循环
for循环、while、do-while、增强for循环(foreach)
-
while 和 do-while 循环的区别
do-while 循环会至少执行一次
-
数组有没有length()这个方法 String有没有length()这个方法
数组没有 length() 这个方法 它有 length属性
String 有 length() 这个方法
-
有数组int[] arr,用Java代码将数组元素顺序颠倒
int[] arr = new int[]{1, 2, 3, 5, 6}; for (int i = 0; i < arr.length / 2; i++) { int temp = arr[i]; arr[i] = arr[arr.length - i - 1]; arr[arr.length - i - 1] = temp; } System.out.println(Arrays.toString(arr));
-
数组有什么排序的方式 手写一下
// 冒泡排序 int[] arr = new int[]{1, 4, 2, 5, 6}; 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]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println(Arrays.toString(arr));
-
二分算法实现数组的查找
int start = 0; int end = arr.length - 1; while (start < end) { int mid = start + (end - start) / 2; if (value == arr[mid]) { return; } else if (value > arr[mid]) { end = mid - 1; } else { start = mid + 1; } }
-
Arrays 类的排序方法是什么 如何实现排序的
Arrays.sort(); 快排
-
面向对象 面向过程的理解
面向对象 和 面向过程是两种不同的编程范式。
面向过程:以函数为核心。每个函数都执行特定的任务。这些函数按照顺序依次调用。关注的是解决问题的步骤和流程。
面向对象:以对象为核心。程序被组织成一组相互关联的对象。每个对象都有自己的属性和方法。通过对象之间的相互交互来完成任务。
-
Java 的引用类型有哪几种
类、接口、枚举、注解、记录、数组
-
类和对象的区别
类是对某些有共同特点的事务的抽象。是创建对象的蓝图模板。对象是类的实例。
-
面向对象,你解释一下,项目中哪些地方用到面向对象
项目中任何地方都能使用到对象。如自定义的类、Java中核心的类库、甚至异常、文件都是对象。
-
Java虚拟机中内存划分为哪些区域,详细介绍一下
栈:栈用于存储线程的执行上下文,包括局部变量、方法参数、返回值、操作数栈等。每个线程都有自己的栈空间,栈中的数据随着方法的调用和返回而入栈和出栈。
堆:堆是 Java 虚拟机管理的最大一块内存区域。它是被所有线程共享的内存区域,用于存储对象实例和数组。
方法区:用于存储类的结构信息(Class)、常量、静态变量、即时编译器编译后的代码等数据
本地方法栈:用于存放 C 或 C++ 编写的方法
程序计数器:用于存储当前线程正在执行的字节码指令地址
-
private 、缺省、protected、public的表格化作用区域
权限修饰符 访问本类 访问本包中的类型 访问其它包的子类 访问其他包 private 可以 缺省 可以 可以 protected 可以 可以 可以 public 可以 可以 可以 可以 -
main方法的public能不能换成private 为什么
可以换成 private 修饰的。但一旦换成 private 修饰的 此方法不再是程序的入口方法。
-
构造方法和普通方法的区别
构造方法 没有返回值 且方法名必须和类名一致。
但是在编写的时候其实没有共同点 声明的格式和作用都不一致。
只是在 字节码文件中 构造方法会以
<init>
方法 呈现 用以初始化对象。 -
构造器Constructor是否可被overload
是
-
无参构造器和有参构造器的的作用和应用
都用于实例化对象。有参构造器可以在实例化对象的同时 为对象内的属性赋值。无参构造器使用的是默认值。
-
成员变量与局部变量的区别
成员变量定义在类的内部 可以使用四种权限修饰符 有默认值 作用域在类的内部 存储在堆空间
局部变量定义在方法内部 只能使用默认的权限修饰符 没有默认值 作用域在方法内部 存储在栈空间
-
变量赋值和构造方法加载的优先级问题
先赋值的默认值 接着是变量的显示赋值 然后是代码块赋值 最后是构造方法赋值。
-
父类哪些成员可以被继承,属性可以被继承吗?可以或者不可以,请举下例子。
父类的属性和方法可以被继承。构造器不能被继承 可以被调用。
-
什么是Override,与Overload的区别
Override:方法重写。出现在继承中。子类重写父类的方法。
Overload:方法重载。出现在同一个类中。方法名相同但参数列表不同的方法。
-
Overload的方法是否可以改变返回值的类型
可以 在Overload 返回值并不是方法重载的条件。 在 Override中 子类重写父类的方法 返回值类型必须是父类返回值的本身或者子类。
-
构造器Constructor是否可被override
不能。构造器只能被重载不能被重写。
-
为什么要有重载,我随便命名一个别的函数名不行吗 谈谈你是怎么理解的
- 方便使用。同一个方法名传递不同的参数得到的结果不同。
- 提高可读性。即使相同的方法名可以通过参数的不同推测方法的含义。
-
super和this的区别
super 用于继承中。访问父类的属性或者方法。或者调用父类的构造器。
this 用于本类中。访问当前调用对象的属性或者方法。或者本类的构造器。
-
谈谈你对多态的理解
多态是面向对象编程的一个核心概念 它允许父类的引用指向子类的对象。
多态的前提是有继承。而继承中存在方法重写。
所以在运行时可以动态的选择调用哪个对象的方法 使得代码更加的灵活和易扩展。
-
多态new出来的对象跟不多态new出来的对象区别在哪
多态new出来的对象 在编译期是父类类型而在运行时是子类类型。方法的调用在运行时才确认。
不多态new出来的对象 在编译期和运行时都是固定的类型。编译期和运行时不会改变。
-
说说你认为多态在代码中的体现
父类的引用指向了子类的对象。通常使用在接口作为方法参数需要传入实例化对象的地方。
-
== 与 equals 的区别
==:在比较基本数据类型的时候 比较的是数值是否相等 在比较引用数据类型的时候 比较的是地址值是否相等
equals:作为 object 的一个方法 内部的实现也是比较地址值是否相同 如果需要比较引用数据类型是否相同 需要重写 equals 方法
-
重写 equals 方法要注意什么
1、遵循 equals 方法的规约:
自反性: x.equals(x) 一定为 true。
对称性: x.equals(y) 为 true 则 y.equals(x) 一定为 true。
传递性: x.equals(y) 为 true y.equals(z) 为 true 则 x.equals(z) 一定为 true。
一致性:多次调用x.equals(y)的结果应当保持一致,即在对象没有发生变化的情况下,多次调用equals方法应返回相同的结果。
2、覆盖 hashcode 方法。即如果 equals 相同则 hashcode 一定相同。
3、equals 方法中要比较所有相关的字段。
-
Java 中所有类的父类是什么 它都有什么方法
Object类。包含的方法有:getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait()、finalize()
-
静态变量和实例变量的区别
1、静态变量存储在方法区。实例变量存储在堆空间。
2、静态变量随着类的加载而加载 声明周期与类一致。实例变量随着对象的加载而加载 声明周期与对象一致。
3、静态变量可以通过类之间访问。实例变量需要通过对象进行访问。
-
静态属性和静态方法是否可以被继承 是否可以被重写 以及原因
静态属性和静态方法是随着类的加载而加载的 所以不能被继承也不能被重写。继承是与对象相关联的。
-
是否可以从一个static方法内部发出对非static方法的调用
可以。
-
被static修饰的成员(类、方法、成员变量)能否再使用private进行修饰
可以。
-
类的组成和属性赋值执行顺序
类由:1、成员变量 2、方法 3、构造器 4、代码块 组成
属性赋值顺序:
- 默认值
- 显示赋值
- 代码块赋值
- 构造器赋值
-
静态代码块,普通代码块,构造方法,从类加载开始的执行顺序
- 静态代码块
- 普通代码块
- 构造方法
-
描述一下对final理解
final 可以修饰类、属性、方法。
final 在修饰类的时候表明这个类不能被继承。修饰属性的时候 表明这个属性是一个常量。在修饰方法的时候 表明这个方法不能被重写。
-
使用final修饰一个变量时,是引用不能改变,引用指向的对象可以改变
是的。放用final修饰一个变量时 其引用不能改变。但引用指向的对象可以改变。
final MyClass obj = new MyClass(); obj.someMethod(); // 可以调用对象的方法
-
final不能用于修饰构造方法
是的。
-
final 或 static final 修饰成员变量,能不能进行++操作
不能。
-
什么是抽象类 如何识别一个抽象类
包含抽象方法的类就是抽象类。用 abstract 关键字修饰的类。
-
为什么不能用 abstract 修饰属性、私有方法、构造器、静态方法、final的方法
因为 abstract 修饰的内容都必须被重写。而上述的内容都不能被重写。
-
接口与抽象类的区别
接口 关键字是 interface。类通过 implements 实现接口里的抽象方法。定义的属性必须是 public static final 修饰的全局常量。没有构造方法。
抽象类 关键字 是 abstract。类 通过 extends 实现类里的抽象方法。可以有属性 构造方法 普通方法。
-
接口是否可继承接口 抽象类是否可实现(implements)接口 抽象类是否可继承实现类(concrete class)
接口可以继承接口。
抽象类可以实现接口。
抽象类可以继承实现类。
-
接口可以有自己属性吗
可以。只是属性必须由 public static final 修饰。
-
访问接口的默认方法如何使用
实现这个接口的对象中都可以使用。
-
内部类有哪几种
成员内部类。作为类的成员存在。
局部内部类。定义在方法、代码块 或构造器内。
匿名内部类。
静态内部类。作为类的成员存在。由static修饰。
-
内部类的特点说一下
内部类可以直接调用外部类的所有成员。将一个类的实现细节隐藏在另一个类的内部 实现了封装和隐藏。
-
匿名类说一下
没有名字的类。通常用于创建一个没有具体实现的类的对象。并且在创建对象的时候能直接调用实现类的方法。通常用于简化代码 避免创建过多的类文件。
-
枚举可以继承吗
不可以。枚举本身就继承了 Enum 类。
-
Java基本类型与包装类的区别
基本类型存储的是数值 包装类存储的是对象 存储的对象是对基本数据类型的数值的封装。
基本数据类型的默认值为0。包装类的默认值为 null。
基本数据类型只能用于数值计算。包装类提供了许多对象使用的方法。
泛型只能使用包装类 不能使用基本数据类型。
-
Java的异常体系简单介绍下
Throwable 接口
Exception接口 和 Error 接口
-
Java异常处理机制
当 Java 程序遇到异常后 会不断的向上传播 直到找到合适的 try…catch 结构来处理异常,或者程序最终终止。
-
异常的两种类型,Error和Exception的区别
Error 是 JVM 遇到的严重错误。JVM 自身处理不了的异常。比如堆栈异常。
Exception 通常是由程序自身出现的问题。需要进行手动处理 否则也会导致 JVM退出。
-
运行时异常与一般异常有何异同
异常分为运行时异常和非运行时异常。
非运行时异常即受检异常。在编译期 程序会自动检测的异常。由外部环境决定 这个异常是否会发生。
运行时异常即非受检异常。通常在运行时才会出现 由代码自身的健壮性有问题或逻辑有问题的异常。
-
说几个你常见到的异常
空指针异常、数组越界异常、类型转换异常、栈溢出、堆溢出
-
说说final、finally、finalize的区别
final 用于修饰 属性、类和方法。
finally 通常用于 try … catch 以后。用来保证无论如何都需要执行的代码 如资源的关闭。
finalize 是 object 中的方法。是对象被垃圾回收前的最后一步操作。在 JDK9以后被标记为过时的。
-
如果不使用try-catch,程序出现异常会如何
如果使用了 throws 程序出现异常会把异常向上抛给方法的调用者。如果没有throws 处理 也没有try-catch处理 则程序会打印出异常信息后会退出 JVM。
-
try … catch捕捉的是什么异常
捕获的是 Exception 和 RuntimeException。不能捕获 Error。
-
如果执行finally代码块之前方法返回了结果或者jvm退出了,这时finally块中的代码还会执行吗
方法返回了结果是会执行的 但是 jvm退出了 整个程序都挂掉了 finally块 就不会执行了。
-
在try语句中有return语句,最后写finally语句,finally语句中的code会不会执行?何时执行?如果执行是在return前还是后
finally 语句无论如何都会执行。在 return 之前执行。
-
捕获异常在catch块里一定会进入finally吗?catch里能return吗?catch里return还会进finally吗?在try里return是什么情况
catch 块里一定会进入 finally。catch 里能return。catch 里 return 还会进入 finally。在 try 里return 如果在 return 前遇到了异常则return代码不会执行
如果没有遇到异常 则会把方法的返回值返回给方法的调用者。
-
throw和throws的区别
throw 用于在方法内部手动抛出一个异常 终止当前方法的执行
throws 用于在方法声明处 表示当前方法不处理该异常 由上层调用者进行处理
-
子类重写父类抛出异常的方法,能否抛出比父类更高级别的异常类
不能。
-
如何自定义一个异常
继承 Exception 接口。通常情况下会提供一个参数为 message 的构造器 也会指定一个 serialVersionUID。
-
什么是线程
线程是在单个程序中执行的一条独立的指令序列 是CPU调度和执行的基本单位。
-
线程和进程有什么区别
进程是操作系统分配的最小执行单元 线程是进程中的内部执行者 一个进程由多个线程组成。
-
多线程使用场景
和并发相关的场景中。
-
如何在Java中出实现多线程
-
继承 Thread类 重写 run 方法 调用 start执行
-
实现 runnable 接口 重写 run 方法 创建 Thread 类的实例 传入 runnable 的 run 方法的实现 调用 start 执行
-
实现 callable 接口 重写 call 方法
-
利用线程池
-
-
Thread类中的start()和run()有什么区别
run 方法用于定义一个线程具体执行的内容
start 方法用于启动这个线程 但具体何时执行 还要看CPU何时调度
-
启动一个线程是用run()还是start()
start()
-
Java中Runnable和Callable有什么不同
runnable 和 callable 都可以创建一个线程
callable 的功能更加强大 可以有返回值 和 异常处理
-
什么是线程池,为什么要使用它
线程池是预先准备的线程序列。因为线程频繁的创建和销毁需要消耗资源 所以预先定义固定数量的线程 当使用时直接从线程池中取 使用完毕再次放回线程池。
-
sleep() 和 yield()区别
seelp() 使当前线程进入阻塞状态 当阻塞时间过后 回到运行状态继续运行
yield() 使当前线程让出CPU资源 以便其它拥有相同优先级的线程先执行 至于CPU是否采纳是不确定的
-
线程创建的中的方法、属性情况
start() run() 方法
name 线程的名称
priority 线程的优先级
-
线程的生命周期
新建 就绪 运行 阻塞 等待 计时等待 终止
-
线程的基本状态以及状态之间的关系
当一个线程创建后属于 新建状态
调用 start 方法后 处于就绪状态
等待CPU调度到该线程是 就处于运行状态
可以使用 wait sleep 等方法将处于运行状态的线程变成 阻塞状态
线程运行结束 就变成了终止状态
-
stop()和suspend()方法为何不推荐使用
stop() 方法会使当前线程停止 会导致资源无法正常释放
suspend() 会使当前线程暂停 必须配合 resume() 使用 会造成死锁情况
-
Java 线程优先级是怎么定义的
线程的优先级定义在 Thread 类中 分为 1,5,10 三个优先级
-
你如何理解线程安全的?线程安全问题是如何造成的
线程安全指的是 不同线程操作共享的资源 不会产生错误的结果。
线程安全问题通常由多线程操作共享的资源没有正确的采取同步机制造成。
-
多线程共用一个数据变量需要注意什么
注意线程安全问题
-
多线程保证线程安全一般有几种方式
synchronized 关键字 的 同步方法 同步代码块
Lock 显示锁
-
用什么关键字修饰同步方法
synchronized
-
synchronized加在静态方法和普通方法区别
> 同步监视器会不同。在静态方法上 同步监视器就是当前类的 .class 普通方法就是this。
-
Java中synchronized和ReentrantLock有什么不同
synchronized 锁的粒度比 ReentrantLock 粗
synchronized 是关键字 ReentrantLock 是类 使用 lock 方法 和 unlock 方法 实现线程同步
-
当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法
synchronized此时的同步监视器是当前对象。所以其他线程可以访问当前对象的非 synchronized 修饰的方法。不能访问 synchronized 修饰的方法。
-
线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
同步不一定阻塞。只有当同步的区域遇到线程间的竞争时才会阻塞
阻塞也不一定同步。阻塞可能由于线程执行了特定的方法 不一定与共享资源相关
-
什么是死锁,产生死锁的原因及必要条件
互斥条件:只能有一个线程占用
请求与保持条件:这个线程一直保持对资源的占用不释放
不剥夺条件:资源只能由这个线程主动是否 其他线程无法强制剥夺
循环等待条件:每个线程都等待上一个线程释放资源
-
如何避免死锁
破坏上诉的四个必要条件
-
Java中notify()和notifyAll()有什么区别
notify 唤醒 一个在 wait set 中的线程
notifyAll 唤醒 所有 在 wait set 中的线程
-
为什么wait()和notify()方法要在同步块中调用
wait 和 notify 方法要有同一个同步监视器
-
wait()和sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?
wait 是 Object 中的方法 调用后 线程进入阻塞状态 可能是限时阻塞 也可能是无限期阻塞 需要 notify 进行唤醒
sleep 是 Thread 中的方法 调用后 线程进入限时阻塞 达到指定时间后 重新进入就绪状态
-
手写一个单例模式(Singleton),还要安全的
Public class Singleton{ private Singleton(){} private static volatile singleton; public static getInstance(){ if(singleton == null){ synchronized(Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
-
以下两种方式创建的String对象有什么不同 String str = new String(“test”); String str = “test”;
String str = new String(“test”); 会在字符串常量池创建一个 test 然后在堆空间开辟一个对象 指向字符串常量池中的 test
String str = “test”; 会直接在字符串常量池中创建一个 test
-
String s = new String(“xyz”);创建了几个String Object?
两个。
-
String a=“abc” String b=“a”+“bc” 问a==b?
对的。
-
String 中 “+” 怎样实现?
String 中的 “+”:当 连接符两边都是字符串常量时 编译器会自动将字符串常量合成一个字符串
当连接符两边有一个是变量时 编译器会在底层创建一个 StringBuilder 重新返回一个 new 的对象
-
Java中String是不是final的
> 是
-
String为啥不可变,在内存中的具体形态
用 final 修饰。在内存中是一个 byte[]/char[] 数组。存储在字符串常量池
-
String 可以在 switch中使用吗
可以。利用 String 的 equals 方法。
-
String中有哪些方法?列举几个
trim()、contains()、toString()、subString()、toUpperCase()、toLowerCase()、valueOf()、equals()
-
subString()到底做了什么
截取了[start,end)的字符串 重新new了一个返回
-
Java中操作字符串有哪些类?他们之间有什么区别
> String:final 修饰。不可变 每次修改都是 new 一个新的字符串。
>
> StringBuilder:线程不安全的。
>
> StringBuffer:线程安全的。
-
String的线程安全问题
String的线程不安全
-
StringBuilder和StringBuffer的线程安全问题
StringBuffer 线程安全 每个方法 都被 synchronized 修饰
StringBuilder 线程不安全
-
简单说说 Comparable 和 Comparator 的区别和场景
Comparable 自然排序 compareTo(o) 方法 用于自己定义的类 可以实现 Comparable 接口
Comparator 定制排序 compare(o1,o2) 方法 用于不是自己定义的类 或者自然排序的规则不满足需求时
-
List,Set,Map是否继承自collection接口
List 和 Set 继承于 Collection Map 不继承
-
说说List,Set,Map三者的区别
List 存储 一个一个的元素 有序 可重复
Set 存储 一个一共的元素 无序 不可重复
Map 存储 由 key-value 构成的一对一对元素 key 不可重复
-
集合的父类是谁?哪些安全的?
Collection 和 Map
Vector、Hashtable 线程安全
-
集合说一下哪些是线程不安全的
ArrayList、HashMap 线程不安全
-
遍历集合的方式有哪些
普通for循环、增强for循环、forEach、Iteartor迭代器
-
List下面有哪些实现
ArrayList
LinkedList
Vector
-
ArrayList与LinkedList区别
LinkedList 比 ArrayList 多了一对双向链表 方便数据的插入
-
ArrayList与Vector区别呢?为什么要用ArrayList取代Vector呢
Vector 是 ArrayList 的古老实现 是线程安全的
ArrayList 在第一次add元素的时候初始化长度为10的数组 每次扩容为原来的1.5倍
Vector 默认初始化长度为10的数组 每次扩容为原来的2倍
ArrayList 的效率高于 Vector
-
Java.util.ArrayList常用的方法有哪些
add()、clear()、contains()、get()、indexOf()、iterator()、remove()
-
Arraylist 是有序还是无序?为什么
有序的 底层是一个 Object[] 数组
-
Set集合有哪些实现类,分别有什么特点
HashSet:无序 不可重复
LinkedHashSet:无序 不可重复 比较HashSet 多了一对双向链表
TreeSet:添加的元素会排序
-
List集合和Set集合的区别
List 存储有序可重复的数据
Set 存储无序不可重复的数据
-
Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别
Set 里的元素首先比较 hashcode 如果 hashcode相同了再使用 equals 方法比较
-
TreeSet两种排序方式在使用的时候怎么起作用
> TreeSet 添加的元素必须实现 Comparable 接口 或者在创建 TreeSet 实例的时候 使用 Comparator 实现。
-
TreeSet的数据结构
红黑树
-
说一下Java的集合Map有哪些Map
HashMap
LinkedHashMap
TreeMap
Hashtable
Properties
-
final怎么用,修饰Map可以继续添加数据吗
final 修饰的Map 其自己的引用不可变了。但是其中的元素不受影响。可以继续添加数据。
-
Set和Map的比较
Set 的底层就是由 Map 实现的。Map 的 value 统一为 Object 对象。
-
HashMap说一下,线程安全吗
线程不安全。
-
HashMap和Hashtable的区别
HashMap 线程不安全 Hashtable 线程安全
HashMap 由 数组 链表 红黑树构成 在第一次添加元素的时候初始化容量为16的数组 每次扩容为原来的2倍
Hashtable 由 数组和链表构成 实例化的时候就创建了一个容量为16的数组
-
Hashtable是怎么实现的,为什么线程安全
Hashtable 由 数组和链表构成
因为 每一个方法都加了 synchronized 关键字
-
HashMap和LinkedHashMap的区别
LinkedHashMap 在 HashMap 的基础上 每一个 Node 节点上添加了 前后指针
-
HashMap 和 TreeMap 的区别
底层实现完全不一样
HashMap 使用的是 数组 链表 红黑树构成
TreeMap 底层是红黑树
-
HashMap里面实际装的是什么
装的是 Node 对象
-
HashMap的key存储在哪里?和value存储在一起吗?那么value存储在哪里?说具体点
HashMap 的 key 和 value 都存储在 Node 对象中。
-
自定义类型可以作为Key么
可以。最好实现 hashcode 和 equals 方法
-
集合类的工具类是谁?用过工具类哪些方法
Collections。排序、乱序、判断是否为空、反转、求最大值、最小值。
-
Collection 和 Collections的区别
Collection 是集合接口
Collections 是集合的工具类
-
ArrayList 如何实现排序
> Collections.sort()
-
HashMap是否线程安全,怎样解决HashMap的线程不安全
线程不安全。利用 Collections 中的 synchronized 方法。或者自己使用锁。或者使用 ConcurrentHashMap 代替。
-
链表和数组有什么区别
数组在内存中是一块连续的空间。链表是多个Node节点通过指针指向下一个节点。
-
栈是如何运行的
先进后出。
-
ArrayList的默认大小是多少,以及扩容机制
默认大小是10。扩容为原来的1.5倍。
-
ArrayList的底层是怎么实现的
Object[] 数组
-
在ArrayList中remove后面几个元素该怎么做
ArrayList 中 remove 传入的参数是数组的下标。
removeRange 方法可以移出指定区域的元素。
-
ArrayList1.7和1.8的区别
ArrayList 在 JDK7中实例化后就会创建一个长度为10的数组
在 JDK8 中 在添加第一个元素时才会初始化
-
数组和 ArrayList 的区别
ArrayList 底层有数组实现。称为动态数组。
数组一旦创建长度不会改变。ArrayList 中会对数组进行动态扩容。
-
什么是线程安全的List
Vector
-
说说HahMap底层实现
数组+链表+红黑树
-
HashMap初始值16,临界值12是怎么算的
初始值16是 HashMap 中定义好的。其中有属性 0.75作为加载因子。16*0.75=12 即是临界值。
-
HashMap长度为什么是2的幂次方
方便 位运算的时候找到元素存储在数组的哪个索引。
-
HashMap怎么计算哈希值和索引?扩容机制?怎么解决hash冲突
通过元素的hashcode拿到hash值后再通过 hash() 方法拿到最终的 哈希值
利用这个hash值 和数组的长度进行位运算拿到 index
当数组的长度达到了临界值就会进行扩容 扩容为原来的2倍
遇到hash冲突后 通过链表和红黑树进行解决
-
HashMap底层是数组+链表,有数组很快了,为什么加链表
为了解决hash冲突。
-
HashMap为什么长度达到一定的长度要转化为红黑树
链表过长 查询效率会变慢
-
HashMap什么时候扩充为红黑树,什么时候又返回到链表
当数组的长度大于64 且当前索引位置的链表长度大于8时扩充为红黑树
当索引位置的链表长度小于6时返回链表
-
在 JDK1.8中,HashMap的数据结构与1.7相比有什么变化,这些变化的好处在哪里
JDK8中 HashMap 采用尾插法 JD7中采用头插法 防止了在多线程环境下的循环链表
JDK8中 加入红黑树 优化了链表过长时的查询效率
JDK8中 在初次put元素的时候才初始化数组 优化了内存空间
-
HashMap的get()方法的原理
先拿到元素的hash值 和数组上的元素进行比较 如果相同判断该索引处是否有链表或者树结构 然后通过equals 方法依次比较
-
equals相等hash值一定相等吗?hash值相等equals一定相等吗
equals 为true hashcode 一定相同的
hashcode 相同 equals 不一定为true
-
HashSet存放数据的方式
和 HashMap 一样 只是在 value 上存储的是 Object 类型的对象。
-
Set是如何实现元素的唯一性
先比较元素的 hashcode 再比较元素的 equals 方法
-
用哪两种方式来实现集合的排序
自然排序(Comparable 接口) 和 定制排序(Comparator 接口)
-
Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别?
泛型:用于在方法或类中定义没法确定的类型。方便了程序的可扩展性。JDK5之前没有泛型 在 JDK7中进入了钻石操作符进行类型推断。
-
谈谈Java IO里面的常用类,字节流,字符流
File、InputStream/OutputStream、Reader/Writer
字节流:FileInputStream/OutputStream
字符流:FileReader/FileWriter
-
Java 中有几种类型的流?JDK为每种类型的流提供一些抽象类以供继承,请说出他们分别是哪些类
按流向分为:输入流和输出流。
按处理数据的单位分为:字节流和字符流
按流的角色分为:节点流和处理流。
InputStream/OutputStream、Reader/Writer
-
流一般需不需要关闭?如果关闭的话用什么方法?处理流是怎么关闭的
需要关闭。关闭的话用 close()方法。处理流也是用close方法关闭 其内部的节点流会跟着关闭。
-
OutputStream里面的write()是什么意思
将内存中的数据写出到磁盘或其它介质。
-
BufferedReader属于哪种流?他主要是用来做什么的
字符输入缓冲流。属于处理流。用于给 FileReader 这种节点流提供一个缓冲期。减少与磁盘的交互。
-
什么是缓冲区?有什么作用
缓冲区内部有一个数组 当读取的数据达到一定额度才会与磁盘交互。减少了与磁盘交互的次数 提高了读写效率。
-
字节流和字符流是什么?怎么转换
字节流用于操作 图片、音视频这种文件
字符流用于操作 文本文件
通过转换流转换 InputStreamReader/OutputStreamWriter
-
什么是Java序列化,如何实现
Java序列化 是将Java对象转换成与平台无关的二进制流。从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。实现 Serializable 接口的对象都可进行序列化操作。
-
Java有些类中为什么需要实现Serializable接口
便于此类对象实现序列化操作。
持久化存储。网络传输。远程方法调用。缓存和消息队列。
-
TCP协议和UDP协议的区别
TCP协议 是可靠的通信协议。必须建立连接 可以进行大数据量的传输。
UDP协议 是不可靠的通信协议。发送端只负责发送 不考虑接收端是否接受到了数据。每次的传输数据报为64kb
-
简单说说TCP协议的三次握手与四次挥手机制
三次握手:
- 客户端向服务端发起请求
- 服务端向客户端发送确认消息
- 客户端向服务端发送确认消息的确认
四次挥手:
- 客户端向服务端发起结束的请求
- 服务端此时不再接受上次的数据 将最后的数据发给客户端
- 服务端向客户端发起可以释放的请求
- 客户端向服务端发起释放链接的请求
-
对反射了解吗?反射有什么好处?为什么需要反射
反射是指在运行时动态的获取、操作和修改类的属性、方法、构造函数等信息的能力。我们可以在编译时无需知道类名和方法名 动态的创建对象 调用方法 访问属性。
-
反射的使用场合和作用、及其优缺点
反射通常用于框架和库的开发、注解的获取和处理。优点是动态性和扩展性高,缺点是需要在进行是进行类型的检查会带来性能上的损耗。
-
实现Java反射的类有什么
Class、Field、Method、Constructor、Modifier
-
反射是怎么实现的
先找到类的 Class 对象。然后根据这个 Class 对象的 newInstance 方法获取实例。通过invoke来调用方法。通过set来设置属性。
-
Class类的作用?生成Class对象的方法有哪些
Class类 提供了获取类的元数据的方式。反射的源头。
Person.class
Person person = new Person(); person.getClass();
Class.forName(“com.demo.Person”);
-
Class.forName(“全路径”) 会调用哪些方法 ? 会调用构造方法吗?加载的类会放在哪
会执行类的构造器
cinit<>
方法。不会调用构造方法。加载的类放在方法区。 -
类加载流程
装载:通过类加载器读取字节码文件到内存,创建该类的 Class 对象。
验证:字节码文件是否符合 JVM 规范。
准备:为静态字段分配空间和默认值。
解析:将符号引用转为直接引用。
初始化:为静态字段赋值。
使用:使用类创建实例对象或调用静态方法。
-
说一下创建对象的几种方法
- 直接 new
- 通过反射 newInstance
- 通过 clone() 方法
- 通过 序列化和反序列化
-
如何找到对象实际类的
对象.getClass()
-
Java反射创建对象效率高还是通过new创建对象的效率高
new 创建对象效率高。
-
如何利用反射机制来访问一个类的方法
通过 Class 对象的 getMethod(methodName, parameterTypes) 方法获取方法对象
先调用 setAccessible(true) 方法设置为可访问
使用方法对象的 invoke(object, arguments) 方法来调用方法。
-
说一下Java反射获取私有属性,如何改变值
通过 Class 对象的 getField(fieldName) 方法获取字段对象。
先调用 setAccessible(true) 方法设置为可访问
使用字段对象的 get(object) 方法获取字段的值。
使用字段对象的 set(object, value) 方法设置字段的值