XiSun的博客

Learning is endless

0%

1613031436

bit 和 byte

计算机本质是一系列的电路开关。每个开关存在两种状态:开 (on) 和关 (off)。如果电路是开的,它的值是 1,如果电路是关的,它的值是 0。

  • 一个 0 或者一个 1 存储为一个比特 (bit),是计算机中最小的存储单位。

  • 计算机中最基本的存储单元是字节 (byte) 。每个字节由 8 个比特构成。

计算机的存储能力是以字节来衡量的。如下:

千字节 (kilobyte,KB) = 1024 B

兆字节 (megabyte,MB) = 1024 KB

千兆字节 (gigabyte,GB) = 1024 MB

万亿字节 (terabyte,TB) = 1024 GB

JDK、JRE 和 JVM

image-20210211160001517

  • JDK = JRE + 开发工具集(例如 Javac 编译工具等)

  • JRE = JVM + Java SE 标准类库

阶段

image-20210211160243390

编写:编写的 java 代码保存在以 .java 为结尾的源文件中。

在一个 java 源文件中,可以声明多个 class 类,但是,只能最多有一个类声明为 public。而且,声明为 public 的类名,必须与源文件名相同。

编译:使用 javac.exe 命令编译 java 源文件。格式:javac 源文件名.java

编译之后,会生成一个或多个以 .class 结尾的字节码文件,字节码文件的文件名与 java 源文件中的类名相同,二者是一一对应的。

运行:使用 java.exe 命令解释运行字节码文件。格式:java 类名

运行的字节码文件,需要有入口函数 main() 方法,且书写格式是固定的。

编译完源文件后,生成一个或多个字节码文件。然后运行时,使用 JVM 中的类的加载器和解释器,对生成的字节码文件进行解释运行。即:此时,需要将字节码文件对应的类加载到内存中,这个过程涉及到内存解析。

注释

单行注释:// 注释文字

多行注释: /* 注释文字 */

文档注释:/** 注释文字 */

对于单行注释和多行注释,被注释的文字,不会被 JVM 解释执行;

多行注释里面不允许有多行注释嵌套;

文档注释内容可以被 JDK 提供的工具 javadoc 所解析,生成一套以网页文件形式体现该程序的说明文档。

关键字和保留字

关键字 (key word)

定义:被 java 语言赋予了特殊含义,用做专门用途的字符串 (单词)。

特点:关键字中所有字母都为小写。

官方地址:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

1613032077 1613032239(1)

保留字 (reserved word)

现有 java 版本尚未使用,但以后版本可能会作为关键字使用。自己命名标识符时要避免使用这些保留字:goto 、const。

标识符

java 对各种变量、方法和类等要素命名时使用的字符序列称为标识符。

技巧:凡是自己可以起名字的地方都叫标识符。

定义合法标识符规则:

  • 由 26 个英文字母大小写,0 - 9,_ 或 $ 组成;

  • 数字不可以开头;

  • 不可以使用关键字和保留字,但能包含关键字和保留字;

  • java 中严格区分大小写,长度无限制;

  • 标识符不能包含空格。

如果不遵守以上规则,编译不通过。

名称命名规范:

  • 包名:多单词组成时所有字母都小写:xxxyyyzzz;
  • 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz;
  • 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz;
  • 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ。

在命名时,为了提高阅读性,要尽量有意义,做到见名知意。

java 采用 unicode 字符集,因此标识符也可以使用汉字声明,但是不建议使用。

变量

定义

变量是内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化。

变量是程序中最基本的存储单元。包含变量类型、变量名和存储的值。

java 中每个变量必须先声明,后使用,使用变量名来访问这块区域的数据。

变量的作用域:其定义所在的一对 { } 内,变量只有在其作用域内才有效,在同一个作用域内,不能定义重名的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VariableTest{
public static void main(String[] args){
// 变量的定义
int myAge = 12;
// 变量的使用
System.out.println(myAge);

// 编译错误:使用myNumber之前未定义myNumber
// System.out.println(myNumber);

// 变量的定义
int myNumber;

// 编译错误:使用myNumber之前未赋值myNumber
// System.out.println(myNumber);

// 变量的赋值
myNumber = 1001;

// 变量的使用
System.out.println(myNumber);
}
}

按数据类型分类

java 是强类型语言,对于每一种数据都定义了明确的具体数据类型,并在内存中分配了不同大小的内存空间。

image-20210212124012438

基本数据类型

整数类型
image-20210212144204390

java 各整数类型有固定的表数范围和字段长度,不受具体 OS 的影响,以保证 java 程序的可移植性。

java 的整型常量默认为 int 型,声明 long 型常量须后加 ‘l’ 或 ‘L’。java 程序中变量通常声明为 int 型,除非不足以表示较大的数,才使用 long。

浮点类型
image-20210212151610319

与整数类型类似,java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的影响。

float:单精度,尾数可以精确到 7 位有效数字。很多情况下,精度很难满足需求。

double:双精度,精度是float的两倍。通常采用此类型。

java 的浮点型常量默认为 double 型,声明 float 型常量,须后加 ‘f’ 或 ‘F’。

字符类型

char 型数据用来表示通常意义上的 “字符”,占用 2 个字节。char 类型是可以进行运算的。因为它都对应有 Unicode 码。

java 中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。

字符型变量的三种表现形式:

  • 字符常量是用单引号括起来的单个字符。例如:char c1 = 'a'; char c2= '中'; char c3 = '9';

  • java 中还允许使用转义字符 \ 来将其后的字符转变为特殊字符型常量。例如:char c3 = '\n'; // '\n'表示换行符。常用的转义字符如下:

image-20210212152921878

  • 直接使用 Unicode 值来表示字符型常量:’\uXXXX’。其中,XXXX 代表一个十六进制整数。如:\u000a 表示 \n。
布尔类型

boolean 类型用来判断逻辑条件,一般用于程序流程控制。

boolean 类型数据只允许取值 true 和 false,无 null。

java 虚拟机中没有任何供 boolean 值专用的字节码指令,java 语言表达所操作的 boolean 值,在编译之后都使用 java 虚拟机中的 int 数据类型来代替:true 用 1 表示,false 用 0 表示。———《java 虚拟机规范 8 版》

基本数据类型之间的转换

自动类型转换:不同数据类型的变量做运算时,容量小的数据类型自动转换为容量大的数据类型。数据类型按容量大小排序为:

image-20210212173122461

此处的容量大小,指的是该数据类型表示数的范围的大和小。

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

  • byte,short 和 char 之间不会相互转换,他们三者在计算时首先转换为 int 类型。

  • boolean 类型不能与其它数据类型运算。

  • 当把任何基本数据类型的值和字符串 (String) 进行连接运算时 (+),基本数据类型的值将自动转化为字符串 (String) 类型。

强制类型转换:自动类型转换的逆过程,将容量大的数据类型转换为容量小的数据类型。使用时要加上强制转换符:(),但可能造成精度降低或溢出。

  • 通常,字符串不能直接转换为基本类型,但通过基本类型对应的包装类,可以实现把字符串转换成基本类型。如:String a = “43”; int i = Integer.parseInt(a);

  • boolean 类型不可以转换为其它的数据类型。

引用数据类型

String

String 不是基本数据类型,属于引用数据类型 (class)。使用方式与基本数据类型一致,例如:String str = “abcd”;

一个字符串可以串接另一个字符串,也可以直接串接其他类型的数据。例如:str = str + “xyz” ; int n = 100; str = str + n;

String 与 8 种基本数据类型做运算时,但只能是连接运算。

按声明位置分类

成员变量:在方法体外,类体内声明的变量。

局部变量:在方法体内部声明的变量。

image-20210212141422206

成员变量和局部变量在初始化值方面的异同:
同:都有生命周期;异:局部变量除形参外,需显式初始化。

进制

所有数字在计算机底层都以二进制形式存在。

对于整数,有四种表示方式:

  • 二进制 (binary) :0 - 1,满 2 进 1,以 0b 或 0B 开头表示。

  • 十进制 (decimal) :0 - 9,满 10 进 1。

  • 八进制 (octal) :0 - 7,满 8 进 1,以数字 0 开头表示。

  • 十六进制 (hex) :0 - 9 及 A - F,满 16 进 1,以 0x 或 0X 开头表示。此处的 A - F 不区分大小写。如:0x21AF +1= 0X21B0。

image-20210212212735447

二进制

java 整数常量默认是 int 类型,当用二进制定义整数时,其第 32 位是符号位;当是 long 类型时,二进制默认占 64 位,第 64 位是符号位。

二进制的整数有如下三种形式:

  • 原码:直接将一个数值换成二进制数,最高位是符号位。
  • 负数的反码:是对原码按位取反,但最高位 (符号位) 不变,确定为1。
  • 负数的补码:其反码加 1。
image-20210212213913672

正数的原码、反码、补码都相同。

计算机以二进制补码的形式保存所有的整数。

原码到补码的转换:

image-20210213153635591

不同进制间转换

十进制转二进制:除 2 取余的逆。

image-20210213153954522

二进制和八进制、十六进制转换:

image-20210213161118538 image-20210213161151205

运算符

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

算术运算符

1613290563

如果对负数取模,可以把模数负号忽略不记,如:5 % -2 = 1。 如果被模数是负数,则不可忽略,如: -5 % 2 = -1。此外,取模运算的结果不一定总是整数。

对于除号 “/“,它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。 例如:int x = 3510; x = x / 1000 * 1000;,x 的结果是 3000。

“+” 除字符串相加功能外,还能把非字符串转换成字符串。例如:System.out.println("5 + 5 = " + 5 + 5); ,打印结果是:5 + 5 = 55 。

赋值运算符

符号:=。当 “=” 两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。支持连续赋值。

扩展赋值运算符: +=,-=,*=,/=,%=。这几个赋值运算符不会改变变量本身的数据类型。

1
2
3
4
5
int i = 1;
i *= 0.1;
System.out.println(i);// 0
i++;
System.out.println(i);// 1
1
2
3
4
5
int m = 2;
int n = 3;
n *= m++;// n = n * m++;
System.out.println("m = " + m);// 3
System.out.println("n = " + n);// 6
1
2
3
int n = 10;
n += (n++) + (++n);// n = n + (n++) + (++n); → n = 10 + 10 + 12;
System.out.println(n);// 32

比较运算符 (关系运算符)

1613309263

比较运算符的结果都是 boolean 型。

逻辑运算符

&:逻辑与,|:逻辑或,!:逻辑非。
&&:短路与,||:短路或,^:逻辑异或。

image-20210215090130168
  • 逻辑运算符用于连接布尔型表达式,在 java 中不可以写成 3 < x < 6,应该写成 x > 3 & x < 6。

  • “&” 和 “&&” 的区别:& 表示,左边无论真假,右边都进行运算;&& 表示,如果左边为真,右边参与运算,如果左边为假,右边不参与运算。

  • “|” 和 “||” 的区别同理:| 表示,左边无论真假,右边都进行运算;||表示,如果左边为假,右边参与运算,如果左边为真,右边不参与运算。

  • 异或 (^) 与或 (|) 的不同之处是:当左右都为 true 时,结果为 false。即:异或,追求的是异!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int x = 1;
int y = 1;
if (x++ == 2 & ++y == 2) {
x = 7;
}
System.out.println("x = " + x + ", y = " + y);// x = 2, y = 2

x = 1;
y = 1;
if (x++ == 2 && ++y == 2) {
x = 7;
}
System.out.println("x = " + x + ", y = " + y);// x = 2, y = 1

x = 1;
y = 1;
if (x++ == 1 | ++y == 1) {
x = 7;
}
System.out.println("x = " + x + ", y = " + y);// x = 7, y = 2

x = 1;
y = 1;
if (x++ == 1 || ++y == 1) {
x = 7;
}
System.out.println("x = " + x + ", y = " + y);// x = 7, y = 1
1
2
3
4
5
6
7
8
9
10
boolean x = true;
boolean y = false;
short z = 42;
if ((z++ == 42) && (y = true)) {
z++;
}
if ((x = false) || (++z == 45)) {
z++;
}
System.out.println("z = " + z);// z = 46

位运算符

image-20210215095803025 1613359206

无 <<<。

位运算是直接对整数的二进制进行的运算。

image-20210215102050019 1613359872

image-20210215113211505

  • << :在一定范围内,每向左移一位,相当于乘以 2。

  • >>:在一定范围内,每向右移一位,相当于除以2。

面试题:最高效的计算 2 * 8。利用:2 << 3,或者 8 << 1。

交换两个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int num1 = 10;
int num2 = 20;
System.out.println(num1 + ", " + num2);

// 方式一
int temp;
temp = num1;
num1 = num2;
num2 = temp;
System.out.println(num1 + ", " + num2);

// 方式二
num1 = 10;
num2 = 20;
num1 = num1 + num2;
num2 = num1 - num2;
num1 = num1 - num2;
System.out.println(num1 + ", " + num2);

// 方式三
num1 = 10;
num2 = 20;
num1 = num1 ^ num2;
num2 = num1 ^ num2;
num1 = num1 ^ num2;
System.out.println(num1 + ", " + num2);

// 方式四
num1 = 10;
num2 = 20;
num1 = num1 << 1;
num2 = num2 >> 1;
System.out.println(num1 + ", " + num2);

三元运算符

格式:

image-20210215115911103
  • 表达式 1 和表达式 2 要求类型是一致的,因为要与接受的参数类型相同。

凡是可以使用三元运算符的地方,都可以改写为 if - else 结构,反之,不成立。如果既可以使用三元运算符,又可以使用 if - else 结构,优先使用三元运算符,因为更简洁、效率更高。

三元运算符与 if - else 的联系与区别:

  • 三元运算符可简化 if - else 语句。
  • 三元运算符要求必须返回一个结果。
  • if 后的代码块可有多个语句。

运算符的优先级

image-20210215125103840
  • 运算符有不同的优先级,所谓优先级就是表达式运算中的运算顺序。如上表,上一行运算符总优先于下一行。
  • 只有单目运算符、三元运算符、赋值运算符是从右向左运算的。

程序流程控制

流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块。

流程控制方式采用结构化程序设计中规定的三种基本流程结构,即:

  • 顺序结构:程序从上到下逐行地执行,中间没有任何判断和跳转。
  • 分支结构:根据条件,选择性地执行某段代码。有 if - else 和 switch - case 两种分支语句。
  • 循环结构:根据循环条件,重复性的执行某段代码。有while、do - while、for三种循环语句。
1613398126

注:JDK 1.5 提供了 foreach 循环,方便遍历集合、数组元素。

if - else 结构

1613392531 1613392642

switch - case 结构

1613392720
  • switch (表达式) 中表达式的值,必须是下述几种类型之一:byte ,short,char,int,枚举类 (jdk 5.0),String 类 (jdk 7.0)。
  • case 子句中的值必须是常量,不能是变量名或不确定的表达式值。
  • 同一个 switch 语句,所有 case 子句中的常量值互不相同。
  • break 语句用来在执行完一个 case 分支后使程序跳出 switch 语句块;如果没有 break,程序会顺序执行到 switch 结尾。
  • default 子句是可任选的。同时,位置也是灵活的。当没有匹配的 case 时,执行 default。
  • 如果多个 case 的执行语句相同,则可以将其合并。
  • 同等情况下,switch - case 结构比 if - else 结构的效率稍高。
1
2
3
4
5
6
7
8
9
10
11
12
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
switch (num) {
case 0:
System.out.println(0);
case 1:
System.out.println(1);
case 2:
System.out.println(2);
default:
System.out.println("other");
}

添加 break 和不添加 break 的结果是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
switch (num) {
case 0:
System.out.println(0);
break;
case 1:
System.out.println(1);
break;
case 2:
System.out.println(2);
break;
default:
System.out.println("other");
break;// default位于最后,此break可以不添加。
}

键盘输入一个月份和天数,判断其是一年中的第几天:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入month:");
int month = scanner.nextInt();
System.out.println("请输入day:");
int day = scanner.nextInt();
int sumDays = 0;

switch (month) {
case 12:
sumDays += 30;
case 11:
sumDays += 31;
case 10:
sumDays += 30;
case 9:
sumDays += 31;
case 8:
sumDays += 31;
case 7:
sumDays += 30;
case 6:
sumDays += 31;
case 5:
sumDays += 30;
case 4:
sumDays += 31;
case 3:
sumDays += 28;
case 2:
sumDays += 31;
case 1:
sumDays += day;
}

System.out.println(month + "月" + day + "日,是当年的第" + sumDays + "天。");
}

键盘输入一个年份、月份和天数,判断其是该年中的第几天:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入year:");
int year = scanner.nextInt();
System.out.println("请输入month:");
int month = scanner.nextInt();
System.out.println("请输入day:");
int day = scanner.nextInt();
int sumDays = 0;

switch (month) {
case 12:
sumDays += 30;
case 11:
sumDays += 31;
case 10:
sumDays += 30;
case 9:
sumDays += 31;
case 8:
sumDays += 31;
case 7:
sumDays += 30;
case 6:
sumDays += 31;
case 5:
sumDays += 30;
case 4:
sumDays += 31;
case 3:
if ((year % 4 == 0 && year % 100 != 0) ||
year % 400 == 0) {
sumDays += 29;// 闰年2月29天
} else {
sumDays += 28;// 平年2月28天
}
case 2:
sumDays += 31;
case 1:
sumDays += day;
}

System.out.println(year + "年" + month + "月" + day + "日,是当年的第" + sumDays + "天。");
}

判断一年是否是闰年的标准:

1)可以被 4 整除,但不可被 100 整除。

2)可以被 400 整除。

for 循环

语法格式:

image-20210216100450322

执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ② - ③ - ④ - ….. - ②

说明:

  • ② 循环条件部分为 boolean 类型表达式,当值为 false 时,退出循环。
  • ① 初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔。
  • ④ 迭代部分可以有多个变量更新,用逗号分隔。

键盘输入两个正整数,求他们的最大公约数和最小公倍数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入第一个正整数:");
int firstNum = scanner.nextInt();
System.out.println("请输入第二个正整数:");
int secondNum = scanner.nextInt();
int min = Math.min(firstNum, secondNum);
int max = Math.max(firstNum, secondNum);

// 最大公约数
for (int i = min; i >= 1; i--) {
if (firstNum % i == 0 && secondNum % i == 0) {
System.out.println(firstNum + "和" + secondNum + "的最大公约数为:" + i);
break;
}
}

// 最小公倍数
for (int i = max; i <= firstNum * secondNum; i++) {
if (i % firstNum == 0 && i % secondNum == 0) {
System.out.println(firstNum + "和" + secondNum + "的最小公倍数为:" + i);
break;
}
}
}

请输入第一个正整数:
12
请输入第二个正整数:
20
1220的最大公约数为:4
1220的最小公倍数为:60

while 循环

语法格式:

image-20210216105032083

执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ② - ③ - ④ - ….. - ②

说明:

  • 注意不要忘记声明 ④ 迭代部分。否则,循环将不能结束,变成死循环。
  • for 循环和 while 循环可以相互转换。

do - while 循环

语法格式:

image-20210216105553968

执行过程:① - ③ - ④ - ② - ③ - ④ - ② - ③ - ④ - ② - ③ - ④ - ….. - ②

说明:

  • do - while 循环至少执行一次循环体 。

嵌套循环

  • 将一个循环放在另一个循环体内,就形成了嵌套循环。其中,for,while,do - while 均可以作为外层循环或内层循环。
  • 实质上,嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为 false 时,才会完全跳出内层循环,才可结束外层的当次循环,开
    始下一次的循环。
  • 设外层循环次数为 m 次,内层为 n 次,则内层循环体实际上需要执行 m * n 次。

九九乘法表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
System.out.print(j + " * " + i + " = " + (j * i) + "\t");
}
System.out.println();
}
}

1 * 1 = 1
1 * 2 = 2 2 * 2 = 4
1 * 3 = 3 2 * 3 = 6 3 * 3 = 9
1 * 4 = 4 2 * 4 = 8 3 * 4 = 12 4 * 4 = 16
1 * 5 = 5 2 * 5 = 10 3 * 5 = 15 4 * 5 = 20 5 * 5 = 25
1 * 6 = 6 2 * 6 = 12 3 * 6 = 18 4 * 6 = 24 5 * 6 = 30 6 * 6 = 36
1 * 7 = 7 2 * 7 = 14 3 * 7 = 21 4 * 7 = 28 5 * 7 = 35 6 * 7 = 42 7 * 7 = 49
1 * 8 = 8 2 * 8 = 16 3 * 8 = 24 4 * 8 = 32 5 * 8 = 40 6 * 8 = 48 7 * 8 = 56 8 * 8 = 64
1 * 9 = 9 2 * 9 = 18 3 * 9 = 27 4 * 9 = 36 5 * 9 = 45 6 * 9 = 54 7 * 9 = 63 8 * 9 = 72 9 * 9 = 81

10000 以内所有的质数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 方式一
public static void main(String[] args) {
// 质数:素数,只能被1和它本身整除的自然数,2是最小的质数。
int count = 0;
boolean ifFlag = true;
for (int i = 2; i <= 100000; i++) {
// 优化一:使用Math.sqrt(i)代替i,减少循环的次数
// i除以一个从2开始的小数,会得到一个从i-1开始的大数,因此,除以2开始的小数与除以从i-1开始的大数,
// 可以省略一个,以减少次数,这样计算的中点是i开方的值。
for (int j = 2; j <= Math.sqrt(i); j++) {
if (i % j == 0) {
ifFlag = false;
// 优化二:使用break,跳出不必要的循环
break;
}
}
if (ifFlag) {
// 优化三:不打印,i越大,打印的耗时越长
// System.out.println("质数:" + i);
count++;
}
// 重置
ifFlag = true;
}
System.out.println("质数的个数有:" + count);// 质数的个数有:9592
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方式二
public static void main(String[] args) {
// 质数:素数,只能被1和它本身整除的自然数,2是最小的质数。
int count = 0;
label:
for (int i = 2; i <= 100000; i++) {
// 优化一:使用Math.sqrt(i)代替i,减少循环的次数
// i除以一个从2开始的小数,会得到一个从i-1开始的大数,因此,除以2开始的小数与除以从i-1开始的大数,
// 可以省略一个,以减少次数,这样计算的中点是i开方的值。
for (int j = 2; j <= Math.sqrt(i); j++) {
if (i % j == 0) {
continue label;
}
}
count++;
}
System.out.println("质数的个数有:" + count);// 质数的个数有:9592
}

break 和 continue

  • break 使用在 switch - case 结构或者循环结构中。

  • continue 只能使用在循环结构中。

  • break 语句用于终止某个语句块的执行,跳出当前循环,continue 语句用于跳过其所在循环语句块的当次执行,继续下一次循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    for (int i = 1; i <= 10; i++) {
    if (i % 4 == 0) {
    break;// 输出结果:1 2 3
    continue;// 输出结果:1 2 3 5 6 7 9 10
    }
    System.out.print(i + "\t");
    }
    }
  • break 语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块 (默认跳出包裹 break 最近的一层循环):

image-20210216161327840

  • continue 语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环 (默认跳出包裹 continue 最近的一层循环)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void main(String[] args) {
    label:
    for (int i = 1; i <= 4; i++) {
    for (int j = 1; j <= 10; j++) {
    if (j % 4 == 0) {
    break label;// 结束指定标识label层的当前循环
    continue label;// 结束指定标识label层的当次循环
    }
    System.out.print(j);
    }
    System.out.println();
    }
    }
    break label输出结果:
    1 2 3
    continue label输出结果:
    1 2 3 2 3 1 2 3 1 2 3
  • break 和 continue 关键字后面不能直接声明执行语句。

随机数

获取 [a, b] 之间的随机数:

1
int v = (int) (Math.random() * (b - a + 1) + a);

如获取 [10, 99] 之间的随机数:

1
int v = (int) (Math.random() * 90 + 10);

数组

数组 (Array),是多个相同类型数据按一定顺序排列的集合,使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

数组的相关概念:

  • 数组名
  • 元素
  • 下标 (或索引)
  • 数组的长度

数组的特点:

  • 数组是有序排列的。
  • 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
  • 数组本身是引用数据类型的变量,而数组中的元素可以是任何数据类型,既可以是基本数据类型,也可以是引用数据类型。
  • 可以直接通过下标 (或索引) 的方式调用指定位置的元素,速度很快。
  • 数组的长度一旦确定,就不能修改。

数组的分类:

  • 按照维度:一维数组、二维数组、三维数组、…
  • 按照元素的数据类型分:基本数据类型元素的数组、引用数据类型元素的数组 (即对象数组)。

一维数组

声明方式:type var[] 或 type[] var;。例如:int a[]; int[] a1; double b[]; String[] c;// 引用类型变量数组

不同写法:int[] x;int x[];

java 语言中声明数组时,不能指定其长度 (数组中元素的数), 例如:int a[5];// 非法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void main(String[] args) {
// 1-1.静态初始化,方式一
int[] ids = new int[]{1001, 1002, 1003, 1004, 1005};

// 1-2.静态初始化,方式二,类型推断
int[] ids2 = {1001, 1002, 1003, 1004, 1005};

// 2.动态初始化
String[] names = new String[5];
names[0] = "Student A";
names[1] = "Student B";
names[2] = "Student C";
names[3] = "Student D";
names[4] = "Student E";

// 3.数组的长度
System.out.println("ids 的长度:" + ids.length);// 5
System.out.println("names 的长度:" + names.length);// 5

// 4.遍历数组
for (int i = 0; i < ids.length; i++) {
System.out.println(ids[i]);
}

for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}

// 5.简写方式遍历数组
for (int id : ids) {
System.out.println(id);
}

for (String name : names) {
System.out.println(name);
}

// 6.数组元素的默认初始化值
int[] arrs = new int[5];
for (int arr : arrs) {
System.out.println(arr);// 0
}

String[] arrs2 = new String[5];
for (String arr2 : arrs2) {
System.out.println(arr2);// null
}
}
  • 静态初始化:数组的初始化,和数组元素的赋值操作同时进行。

  • 动态初始化 :数组的初始化,和数组元素的赋值操作分开进行。

  • 定义数组并用运算符 new 为之分配空间后,才可以引用数组中的每个元素。

  • 数组元素的引用方式:数组名[数组元素下标]。

  • 数组元素下标从 0 开始,长度为 n 的数组的合法下标取值范围:0 — n-1。如:int a[] = new int[3];,则可引用的数组元素为 a[0]、a[1] 和 a[2]。

  • 数组元素下标可以是整型常量或整型表达式。如 a[3],b[i],c[6*i]。

  • 数组一旦初始化完成,其长度也随即确定,且长度不可变。每个数组都有一个属性 length 指明它的长度,例如:a.length 指明数组 a 的长度 (元素个数)。

  • 数组是引用类型,它的元素相当于类的成员变量,因此数组一经分配空间,其中的每个元素也被按照成员变量同样的方式被隐式初始化。然后,再根据实际代码设置,将数组相应位置的元素进行赋值,即显示赋值。

    image-20210217180157738

    对于基本数据类型而言,默认的初始化值各有不同;对于引用数据类型而言,默认的初始化值为 null。

    char 类型的默认值是 0,不是 ‘0’,表现的是类似空格的一种效果。

  • 一维数组内存解析:

    image-20210219091722284

  • 一个计算联系方式的数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    int[] arr = new int[]{8, 2, 1, 0, 3};
    int[] index = new int[]{2, 0, 3, 2, 4, 0, 1, 3, 2, 3, 3};
    String tel = "";
    for (int i = 0; i < index.length; i++) {
    tel += arr[index[i]];
    }
    System.out.println("联系方式:" + tel);// 联系方式:18013820100
    }

二维数组

java 语言里提供了支持多维数组的语法。如果把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格。

对于二维数组的理解,可以看成是一维数组 array1,作为另一个一维数组 array2 的元素而存在。其实,从数组底层的运行机制来看,没有多维数组。

不同写法:int[][] x;int[] x[];int x[][];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public static void main(String[] args) {
// 1-1.静态初始化,方式一
int[][] arr = new int[][]{{3, 8, 2}, {2, 7}, {9, 0, 1, 6}};

// 1-2.静态初始化,方式二,类型推断
int[][] arr2 = {{3, 8, 2}, {2, 7}, {9, 0, 1, 6}};
System.out.println(arr2[2][3]);

// 2-1.动态初始化,方式一
/*
定义了名称为arr3的二维数组,二维数组中有3个一维数组,内层每一个一维数组中有2个元素
内层一维数组的名称分别为arr3[0],arr3[1],arr3[2],返回的是地址值
给内层第一个一维数组1脚标位赋值为78写法是:arr3[0][1] = 78;
*/
int[][] arr3 = new int[3][2];
arr3[0][1] = 78;
System.out.println(arr3[0]);// [I@78308db1
System.out.println(arr3[0][1]);// 78

// 2-2.动态初始化,方式二
/*
二维数组arr4中有3个一维数组,内层每个一维数组都是默认初始化值null(注意:区别于格式2-1)
可以对内层三个一维数组分别进行初始化
*/
int[][] arr4 = new int[3][];
// 初始化第一个
arr4[0] = new int[3];
// 初始化第二个
arr4[1] = new int[1];
// 初始化第三个
arr4[2] = new int[2];

// 3.特殊写法
int[] x, y[];// x是一维数组,y是二维数组
x = new int[3];
y = new int[3][2];

// 4.获取数组长度
System.out.println("arr的长度:" + arr.length);// 3
System.out.println("arr第一个元素的长度:" + arr[0].length);// 3

// 5.遍历二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + "\t");// 3 8 2 2 7 9 0 1 6
}
}
System.out.println();

// 6.简写遍历二维数组
for (int[] valueArr : arr) {
for (int value : valueArr) {
System.out.print(value + "\t");// 3 8 2 2 7 9 0 1 6
}
}
System.out.println();

// 7.二维数组元素的默认初始化值
int[][] arr5 = new int[3][2];
System.out.println(arr5);// [[I@27c170f0
System.out.println(arr5[1]);// [I@5451c3a8
System.out.println(arr5[1][1]);// 0

String[][] arr6 = new String[3][2];
System.out.println(arr6);// [[Ljava.lang.String;@2626b418
System.out.println(arr6[1]);// [Ljava.lang.String;@5a07e868
System.out.println(arr6[1][1]);// null

String[][] arr7 = new String[3][];
System.out.println(arr7[1]);// null,因为内层数组未初始化
System.out.println(arr7[1][1]);// NullPointerException
}
  • 动态初始化方式一,初始化时直接规定了内层一维数组的长度,动态初始化方式二,可以在使用过程中根据需要另行初始化内层一维数组的长度。利用动态初始化方式二时,必须要先初始化内层一维数组才能对其使用,否则报空指针异常。

  • int[][] arr = new int[][3]; 的方式是非法的。

  • 注意特殊写法情况:int[] x,y[];// x是一维数组,y是二维数组

  • java 中多维数组不必都是规则矩阵形式。

  • 数组元素的默认初始化值:针对形如 int[][] arr = new int[4][3]; 的初始化方式,外层元素的初始化值为地址值,内层元素的初始化值与一维数组初始化情况相同;针对形如 int[][] arr = new int[4][]; 的初始化方式,外层元素的初始化值为 null,内层元素没有初始化,不能调用。

  • 二维数组内存解析:

    image-20210219091000779

  • 杨辉三角:

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

    image-20210219101905712

    提示:1. 第一行有 1 个元素,第 n 行有 n 个元素;2. 每一行的第一个元素和最后一个元素都是 1;3. 从第三行开始,对于非第一个元素和最后一个元
    素的元素,有:yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public static void main(String[] args) {
    // 1.声明二维数组并初始化
    int[][] arrs = new int[10][];
    for (int i = 0; i < arrs.length; i++) {
    System.out.print("[" + i + "]\t");
    // 2.初始化内层数组,并给内层数组的首末元素赋值
    arrs[i] = new int[i + 1];
    arrs[i][0] = 1;
    arrs[i][arrs[i].length - 1] = 1;
    for (int j = 0; j < arrs[i].length; j++) {
    // 3.给从第三行开始内层数组的非首末元素赋值
    if (i >= 2 && j > 0 && j < arrs[i].length - 1) {
    arrs[i][j] = arrs[i - 1][j - 1] + arrs[i - 1][j];
    }
    System.out.print(arrs[i][j] + "\t");
    }
    System.out.println();
    }
    System.out.print("\t");
    for (int i = 0; i < arrs.length; i++) {
    System.out.print("[" + i + "]\t");
    }
    }

数组中涉及到的常见算法

数组元素的赋值 (杨辉三角、回形数等)

  • 创建一个长度为 6 的 int 型数组,要求数组元素的值都在 1 - 30 之间,且是随机赋值。同时,要求元素的值各不相同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static void main(String[] args) {
    int[] arr = new int[6];
    for (int i = 0; i < arr.length; i++) {
    // 获取一个1-30之间的随机数
    arr[i] = (int) (Math.random() * 30 + 1);
    // 判断是否有相同的值
    for (int item : arr) {
    if (item == arr[i]) {
    i--;
    break;
    }
    }
    }

    // 遍历数组
    for (int value : arr) {
    System.out.println(value);
    }
    }
  • 回形数

    从键盘输入一个 1 - 20 的整数,然后以该数字为矩阵的大小,把 1,2,3 … n*n 的数字按照顺时针螺旋的形式填入其中。例如:

    输入数字 2,则程序输出:

    image-20210219104822490

    输入数字 3,则程序输出:

    image-20210219104929770

    输入数字 4, 则程序输出:

    image-20210219105019369

    方式一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("输入一个数字");
    int len = scanner.nextInt();
    int[][] arr = new int[len][len];
    int s = len * len;

    /*
    * k = 1:向右,k = 2:向下,k = 3:向左,k = 4:向上
    */
    int k = 1;
    int i = 0, j = 0;
    for (int m = 1; m <= s; m++) {
    if (k == 1) {
    if (j < len && arr[i][j] == 0) {
    arr[i][j++] = m;
    } else {
    k = 2;
    i++;
    j--;
    m--;
    }
    } else if (k == 2) {
    if (i < len && arr[i][j] == 0) {
    arr[i++][j] = m;
    } else {
    k = 3;
    i--;
    j--;
    m--;
    }
    } else if (k == 3) {
    if (j >= 0 && arr[i][j] == 0) {
    arr[i][j--] = m;
    } else {
    k = 4;
    i--;
    j++;
    m--;
    }
    } else if (k == 4) {
    if (i >= 0 && arr[i][j] == 0) {
    arr[i--][j] = m;
    } else {
    k = 1;
    i++;
    j++;
    m--;
    }
    }
    }

    // 遍历数组
    for (int m = 0; m < arr.length; m++) {
    for (int n = 0; n < arr[m].length; n++) {
    System.out.print(arr[m][n] + "\t");
    }
    System.out.println();
    }
    }

    方式二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("输入一个数字");
    int n = scanner.nextInt();
    int[][] arr = new int[n][n];

    int count = 0;// 要显示的数据
    int maxX = n - 1;// x轴的最大下标
    int maxY = n - 1;// Y轴的最大下标
    int minX = 0;// x轴的最小下标
    int minY = 0;// 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++;
    }

    // 遍历数组
    for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr.length; j++) {
    String space = (arr[i][j] + "").length() == 1 ? "0" : "";
    System.out.print(space + arr[i][j] + " ");
    }
    System.out.println();
    }
    }

求数值型数组中元素的最大值、最小值、平均数、总和等

  • 定义一个 int 型的一维数组,包含 10 个元素,分别赋一些随机整数,然后求出所有元素的最大值,最小值,和值,平均值,并输出出来 。要求:所有随机数都是两位数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public static void main(String[] args) {
    // 初始化及赋值
    int[] arr = new int[10];
    int length = arr.length;
    for (int i = 0; i < length; i++) {
    int value = (int) (Math.random() * 90 + 10);
    arr[i] = value;
    }

    // 遍历
    for (int value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 计算
    int max = arr[0];
    int min = arr[0];
    int sum = 0;
    double average = 0;
    for (int value : arr) {
    max = max < value ? value : max;
    min = min > value ? value : min;
    sum += value;
    }
    average = sum / (length * 1.0);
    System.out.println("最大值:" + max);
    System.out.println("最小值:" + min);
    System.out.println("和值:" + sum);
    System.out.println("平均值:" + average);
    }

数组的复制、反转、查找 (线性查找、二分法查找)

  • 复制

    虚假的复制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public static void main(String[] args) {
    // 声明arr1和arr2
    int[] arr1, arr2;
    arr1 = new int[]{2, 3, 5, 7, 11, 13, 17, 19};

    // 遍历arr1
    for (int value : arr1) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 赋值arr2变量等于arr1
    // 不能称作数组的复制,实际上是把arr1指向的地址(以及其他一些信息)赋给了arr2,堆空间中只有一个数组对象
    arr2 = arr1;

    // 遍历arr2
    for (int value : arr2) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 更改arr2
    for (int i = 0; i < arr1.length; i++) {
    if (i % 2 == 0) {
    arr2[i] = i;
    continue;
    }
    arr2[i] = arr1[i];
    }

    // 遍历arr2
    for (int value : arr2) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 遍历arr1
    for (int value : arr1) {
    System.out.print(value + "\t");
    }
    System.out.println();
    }
    输出结果:
    2 3 5 7 11 13 17 19
    2 3 5 7 11 13 17 19
    0 3 2 7 4 13 6 19
    0 3 2 7 4 13 6 19

    arr1 和 arr2 地址值相同,都指向了堆空间中唯一的一个数组实体:

    image-20210219140343665

    真实的复制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public static void main(String[] args) {
    // 声明arr1和arr2
    int[] arr1, arr2;
    arr1 = new int[]{2, 3, 5, 7, 11, 13, 17, 19};

    // 遍历arr1
    for (int value : arr1) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 数组的复制
    arr2 = new int[arr1.length];
    for (int i = 0; i < arr1.length; i++) {
    arr2[i] = arr1[i];
    }

    // 遍历arr2
    for (int value : arr2) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 更改arr2
    for (int i = 0; i < arr1.length; i++) {
    if (i % 2 == 0) {
    arr2[i] = i;
    continue;
    }
    arr2[i] = arr1[i];
    }

    // 遍历arr2
    for (int value : arr2) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 遍历arr1
    for (int value : arr1) {
    System.out.print(value + "\t");
    }
    System.out.println();
    }
    输出结果:
    2 3 5 7 11 13 17 19
    2 3 5 7 11 13 17 19
    0 3 2 7 4 13 6 19
    2 3 5 7 11 13 17 19

    arr1 和 arr2 地址值不同,指向了堆空间中两个不同的数组实体:

    image-20210219140437149
  • 反转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public static void main(String[] args) {
    String[] arr = {"A", "B", "C", "D", "E", "F", "G"};

    // 遍历arr
    for (String value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 反转,方式一
    for (int i = 0; i < arr.length / 2; i++) {
    String temp = arr[i];
    arr[i] = arr[arr.length - 1 - i];
    arr[arr.length - 1 - i] = temp;
    }
    for (String value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();

    // 反转,方式二
    for (int i = 0, j = arr.length - 1; i < j; i++, j--) {
    String temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }
    for (String value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();
    }
  • 线性查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static void main(String[] args) {
    String[] arr = {"A", "B", "C", "D", "E", "F", "G"};

    // 遍历arr
    for (String value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();

    String dest = "D";
    boolean isFlag = true;
    for (int i = 0; i < arr.length; i++) {
    if (dest.equals(arr[i])) {
    System.out.println("找到了指定的元素:" + dest + ",位置为:" + i);
    isFlag = false;
    break;
    }
    }
    if (isFlag) {
    System.out.println("没找到指定的元素:" + dest);
    }
    }
    输出结果:
    A B C D E F G
    找到了指定的元素:D,位置为:3
  • 二分法查找,前提:所要查找的数组必须有序。

    image-20210219153157857

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public static void main(String[] args) {
    int[] arr = {2, 5, 7, 8, 10, 15, 18, 20, 22, 25, 28};

    // 遍历arr
    for (int value : arr) {
    System.out.print(value + "\t");
    }
    System.out.println();

    int dest = 10;
    // 初始的首索引
    int head = 0;
    // 初始的末索引
    int end = arr.length - 1;
    boolean isFlag = true;
    while (head <= end) {
    int middle = (head + end) / 2;
    if (dest == arr[middle]) {
    System.out.println("找到了指定的元素:" + dest + ",位置为:" + middle);
    isFlag = false;
    break;
    } else if (dest < arr[middle]) {
    end = middle - 1;
    } else {// dest2 > arr2[middle]
    head = middle + 1;
    }
    }
    if (isFlag) {
    System.out.println("没找到指定的元素:" + dest);
    }
    }
    输出结果:
    2 5 7 8 10 15 18 20 22 25 28
    找到了指定的元素:10,位置为:4

数组元素的排序算法

排序:假设含有 n 个记录的序列为 {R1, R2, …, Rn},其相应的关键字序列为 {K1, K2, …, Kn}。将这些记录重新排序为 {Ri1, Ri2, …, Rin},使得相应的关键字值满足条 Ki1<= Ki2 <= … <= Kin,这样的一种操作称为排序。通常来说,排序的目的是快速查找。

衡量排序算法的优劣:

  1. 时间复杂度:分析关键字的比较次数和记录的移动次数。

  2. 空间复杂度:分析排序算法中需要多少辅助内存。

  3. 稳定性:若两个记录 A 和 B 的关键字值相等,但排序后 A、B 的先后次序保持不变,则称这种排序算法是稳定的。

排序算法分类:内部排序和外部排序。

  • 内部排序:整个排序过程不需要借助于外部存储器 (如磁盘等),所有排序操作都在内存中完成。

  • 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器 (如磁盘等)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。

十大内部排序算法:

image-20210219164854677

排序算法性能对比:

image-20210219203707336

  • 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序。
  • 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于 Shell 排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。
  • 从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell 排序和堆排序是不稳定排序。
  • 从待排序的记录数 n 的大小看,n 较小时,宜采用简单排序;而 n 较大时宜采用改进排序。

排序算法的选择:

  • 若 n 较小 (如 n ≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
  • 若文件初始状态基本有序 (指正序),则应选用直接插入、 冒泡或随机的快速排序为宜。
  • 若 n 较大,则应采用时间复杂度为 O(nlgn) 的排序方法: 快速排序、 堆排序或归并排序。
冒泡排序

冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

排序思想:

  1. 比较相邻的元素。如果第一个比第二个大 (升序),就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) {
int[] arr = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};

// 遍历arr
for (int value : arr) {
System.out.print(value + "\t");
}
System.out.println();

// 冒泡排序
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}

// 遍历arr
for (int value : arr) {
System.out.print(value + "\t");
}
System.out.println();
}
输出结果:
43 32 76 -98 0 64 33 -21 32 99
-98 -21 0 32 32 33 43 64 76 99
快速排序

快速排序通常明显比同为 O(nlogn) 的其他算法更快,因此常被采用,而且快速排序采用了分治法的思想,所以在很多笔试面试中能经常看到快速排序的影子,可见掌握快速排序的重要性。

快速排序 (Quick Sort) 由图灵奖获得者 Tony Hoare 发明,被列为 20 世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种。

快速排序属于冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为 O(nlog(n))。

排序思想:

  1. 从数列中挑出一个元素,称为”基准” (pivot)。
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面 (相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区 (partition) 操作。
  3. 递归地 (recursive) 把小于基准值元素的子数列和大于基准值元素的子数列排序。
  4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代 (iteration) 中,它至少会把一个元素摆到它最后的位置去。

image-20210219173315424

image-20210219173351751

Arrays 工具类的使用

java.util.Arrays 类为操作数组的工具类,包含了用来操作数组 (比如排序和搜索) 的各种方法。常用的方法有:

image-20210219205214845

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) {
// 1.boolean equals(int[] a,int[] b):判断两个数组是否相等
int[] arr1 = new int[]{1, 2, 3, 4};
int[] arr2 = new int[]{1, 3, 2, 4};
boolean isEquals = Arrays.equals(arr1, arr2);
System.out.println("arr1和arr2是否相等:" + isEquals);

// 2.String toString(int[] a):遍历数组信息
System.out.println("arr1:" + Arrays.toString(arr1));

// 3.void fill(int[] a,int val):将指定值填充到数 组之中
Arrays.fill(arr1, 10);
System.out.println("arr1填充后:" + Arrays.toString(arr1));

// 4.void sort(int[] a):对数组进行排序,底层使用的是快速排序
System.out.println("arr2排序前:" + Arrays.toString(arr2));
Arrays.sort(arr2);
System.out.println("arr2排序后:" + Arrays.toString(arr2));

// 5.int binarySearch(int[] a,int key):对排序后的数组进行二分法检索指定的值
int[] arr3 = new int[]{-98, -34, 2, 34, 54, 66, 79, 105, 210, 333};
int dest = 211;
int index = Arrays.binarySearch(arr3, dest);
if (index >= 0) {
System.out.println(dest + "在数组中的位置为:" + index);
} else {
System.out.println(dest + "在数组中未找到:" + index);
}
}
输出结果:
arr1和arr2是否相等:false
arr1:[1, 2, 3, 4]
arr1填充后:[10, 10, 10, 10]
arr2排序前:[1, 3, 2, 4]
arr2排序后:[1, 2, 3, 4]
211在数组中未找到:-10

数组中的常见异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
// ArrayIndexOutOfBoundsException
int[] arr = new int[]{7, 10};
System.out.println(arr[2]);// 数组脚标越界
System.out.println(arr[-1]);// 访问了数组中不存在的脚标

// NullPointerException:空指针异常,arr引用没有指向实体,却被操作实体中的元素
// 情形一
int[] arr2 = null;
System.out.println(arr2[0]);
// 情形二
int[][] arr3 = new int[4][];
System.out.println(arr3[0]);// null
System.out.println(arr3[0][0]);// NullPointerException
// 情形三
String[] arr4 = new String[]{"AA", "BB", "CC"};
arr4[0] = null;
System.out.println(arr4[0].toString());// 对null调用了方法
}

ArrayIndexOutOfBoundsException 和 NullPointerException,在编译时,不报错!!

面向对象

三条主线:

  1. java 类及类的成员:属性、方法、构造器、代码块、内部类。
  2. 面向对象的三大特征:封装性、继承性、多态性、(抽象性)。
  3. 其他关键字:this、super、static、final、abstract、interface、package、import等。

面向过程 (POP) 与面向对象 (OOP):

  • 二者都是一种思想,面向对象是相对于面向过程而言的。
  • 面向过程,强调的是功能行为,以函数为最小单位,考虑怎么做。面向对象,将功能封装进对象,强调具备了功能的对象,以类/对象为最小单位,考虑谁来做。
  • 面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。

例如,人把大象装进冰箱:

image-20210220095421669

面向对象的三大特征:

  • 封装 (Encapsulation)
  • 继承 (Inheritance)
  • 多态 (Polymorphism)

面向对象的思想概述:

  1. 程序员从面向过程的执行者转化成了面向对象的指挥者。
  2. 面向对象分析方法分析问题的思路和步骤:
    • 根据问题需要,选择问题所针对的现实世界中的实体。
    • 从实体中寻找解决问题相关的属性和功能,这些属性和功能就形成了概念世界中的类。
    • 把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义。即借助某种程序语言,把类构造成计算机能够识别和处理的数据结构。
    • 将类实例化成计算机世界中的对象。对象是计算机世界中解决问题的最终工具。

java基本元素:类和对象

类 (Class) 和对象 (Object) 是面向对象的核心概念。

  • 类是对一类事物的描述,是抽象的、概念上的定义。

  • 对象是实际存在的该类事物的每个个体,因而也称为实例 (instance)。

常见的类的成员有:

  • 属性:对应类中的成员变量。

  • 方法:对应类中的成员方法。

image-20210220101707280

类的成员构成 version 1.0:

image-20210220101855377

类的成员构成 version 2.0:

image-20210220101937398

类的语法格式:

image-20210220102219342

创建 java 自定义类步骤:

  1. 定义类:考虑修饰符、类名。
  2. 编写类的属性:考虑修饰符、属性类型、属性名、初始化值。
  3. 编写类的方法:考虑修饰符、返回值类型、方法名、形参等。

类的访问机制:

  • 在一个类中的访问机制:类中的方法可以直接访问类中的成员变量。例外:static 方法访问非 static 属性,编译不通过。

  • 在不同类中的访问机制: 先创建要访问类的对象, 再用对象访问类中定义的成员。

对象的创建和使用:

image-20210220102712330
  • 创建对象语法:

    image-20210224101451563
  • 使用 对象名.对象成员 的方式访问对象成员,包括属性和方法。

image-20210220110300854
  • 如果创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性 (非 static 的),即:修改一个对象的属性 a,不影响另外一个对象属性 a 的值。

对象的产生:

image-20210220113937600

对象的使用:

image-20210220114111628

对象的生命周期:

image-20210220114314460

对象的内存解析:

image-20210220114421250

例如,下面一段代码的内存图如下:

image-20210220112243394

匿名对象:

  • 不定义对象的句柄,而直接调用这个对象的方法,这样的对象叫做匿名对象。如:new Person().shout();

  • 使用情况:如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象。我们经常将匿名对象作为实参传递给一个方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 二、创建类的对象 = 类的实例化
*/
public class PersonTest {
public static void main(String[] args) {
// 1.创建Person类的对象
Person person = new Person();

// 2.调用对象的结构:属性和方法
// 2-1.调用属性:"对象.属性"
person.name = "Tom";
person.isMale = true;
System.out.println("年龄:" + person.age);// 1
// 2-2.调用方法:"对象.方法"
person.eat();// 人可以吃饭
person.sleep();// 人可以睡觉
person.talk("Chinese");// 人可以说话,语言是:Chinese

// 3.创建一个新的Person类的对象
Person person2 = new Person();
System.out.println(person2.name);// null,非Tom

// 4.将person变量保存的地址值赋值给person3,此时,二者指向堆空间中的同一个对象实体
// 修改person和person3,效果相同
Person person3 = person;
System.out.println(person3.name);// Tom
person3.age = 10;
System.out.println(person.age);// 10
}
}

/**
* 一、类的设计,其实就是类的成员的设计:
* 属性 = 成员变量 = Field = 域、字段
* 方法 = 成员方法 = 函数 = Method
*/
class Person {
// 属性
String name;
int age = 1;
boolean isMale;

// 方法
public void eat() {
System.out.println("人可以吃饭");
}

public void sleep() {
System.out.println("人可以睡觉");
}

public void talk(String language) {
System.out.println("人可以说话,语言是:" + language);
}
}

类的成员之一:属性 (field)

语法格式:

image-20210224101159350
  • 常用的权限修饰符有:private、缺省、protected、public。其他修饰符:static、final。

  • 数据类型:任何基本数据类型 (如 int、boolean 等) 或任何引用数据类型。

  • 属性名:属于标识符,符合命名规则和规范即可。

属性 (成员变量) 与局部变量的区别:

image-20210220145207862

image-20210220145307284

  • 成员变量的默认初始化值:当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外的变量类型都是引用类型。

    image-20210220145909689
  • 局部变量的默认初始化值:局部变量声明后,没有默认初始化值,必须显式赋值,方可使用。特别的,形参在调用时,赋值即可。

  • 成员变量 vs 局部变量的内存位置:

    image-20210220154103931

属性赋值的方式和先后顺序:

  • 赋值的方式:

    • ① 默认初始化
    • ② 显示初始化
    • ③ 构造器中初始化
    • ④ 通过 “对象.属性” 或 “对象.方法” 的方式赋值
  • ⑤ 在代码块中初始化

  • 赋值的先后顺序:① - ② / ⑤ - ③ - ④

  • ② 和 ⑤,谁定义在前,谁先赋值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Test {
    public static void main(String[] args) {
    Order order = new Order();
    System.out.println(order.orderId);// 4
    }
    }

    class Order {
    int orderId = 3;

    {
    orderId = 4;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Test {
    public static void main(String[] args) {
    Order order = new Order();
    System.out.println(order.orderId);// 3
    }
    }

    class Order {
    {
    orderId = 4;
    }

    int orderId = 3;
    }
  • 总结:程序中成员变量赋值的执行顺序

    image-20210301170915863

类的成员之二:方法 (method)

什么是方法 (method 、函数):

  • 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。
  • 将功能封装为方法的目的是,可以实现代码重用,简化代码。
  • java 里的方法不能独立存在,所有的方法必须定义在类里。

声明格式:

image-20210220155316669
  • 权限修饰符:public,缺省,private,protected 等。

  • 返回值类型:

    • 没有返回值:使用void。
    • 有返回值:在方法声明时,必须指定返回值的类型,同时,方法体中需要使用 return 关键字返回指定类型的变量或常量。
  • 方法名 :属于标识符,命名时遵循标识符命名规则和规范,能够见名知意。

  • 形参列表:可以包含零个,一个或多个参数。多个参数时,中间用 “,” 隔开。

  • 方法体程序代码:方法功能的具体实现。

  • 返回值:方法在执行完毕后返还给调用它的程序的数据。

方法的分类:按照是否有形参及返回值。

image-20210220161830762

方法的调用:

  • 方法通过方法名被调用,且只有被调用才会执行。

  • 方法调用的过程:

    image-20210220162017612

  • 方法被调用一次,就会执行一次。

  • 没有具体返回值的情况,返回值类型用关键字 void 表示,此时方法体中可以不必使用 return 语句。如果使用,表示用来结束方法。

  • 定义方法时,方法的结果应该返回给调用者,交由调用者处理。

  • 方法中可以调用当前类的属性或方法,不可以在方法内部定义方法。

方法的重载 (overload)

  • 概念:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。

  • 特点:与方法的权限修饰符、返回值类型、形参变量名、方法体都无关,只看参数列表,且参数列表 (参数个数或参数类型) 必须不同。调用时,根据方法参数列表的不同来区别。

  • 如果方法一不存在,main 方法依然正常执行,此时涉及到的是自动类型转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 方法一
    public static int getSum(int m, int n) {
    return m + n;
    }

    // 方法二
    public static double getSum(double m, double n) {
    return m + n;
    }

    public static void main(String[] args) {
    System.out.println(getSum(1, 2));
    }

可变个数的形参:

  • JavaSE 5.0 中提供了 Varargs (variable number of arguments) 机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。

    image-20210222201117424
  • 声明格式:方法名(参数的类型名 … 参数名)

  • 可变参数:方法参数部分指定类型的参数个数是可变多个 — 0个,1个或多个。

  • 可变个数形参的方法与同名的方法之间,彼此构成重载。

  • 可变参数方法的使用与方法参数部分使用数组是一致的,二者不共存。如下所示,方法二与方法三是相同的,不共存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 方法二
    public static void show(int... m) {
    System.out.println(Arrays.toString(m));// m参数等同于数组,与数组的使用方法相同。
    }

    // 方法三
    public static void show(int[] m) {
    System.out.println(m);
    }
  • 方法的参数部分有可变形参,需要放在形参声明的最后。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 合法
    public static void show(String str, int... m) {
    System.out.println(Arrays.toString(m));
    }

    // 不合法
    public static void show(int... m, String str) {
    System.out.println(Arrays.toString(m));
    }
  • 在一个方法的形参位置,最多只能声明一个可变个数形参。

方法参数的值传递机制

  • 方法,必须由其所在类或对象调用才有意义。若方法含有参数:

    • 形参:方法声明时的参数。
    • 实参:方法调用时实际传给形参的数据。
  • java 的实参值如何传入方法呢?

    • java 里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本 (复制品) 传入方法内,而参数本身不受影响。
      • 形参是基本数据类型:将实参基本数据类型变量的 “数据值” 传递给形参。
      • 形参是引用数据类型:将实参引用数据类型变量的 “地址值” 传递给形参。
  • 形参时基本数据类型与引用数据类型之间的区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class ValueTransferTest {
    public static void main(String[] args) {
    System.out.println("***************基本数据类型***************");
    int m = 10;
    int n = m;
    System.out.println("m = " + m + ", n = " + n);
    n = 20;
    System.out.println("m = " + m + ", n = " + n);

    System.out.println("***************引用数据类型***************");
    Order o1 = new Order();
    o1.orderId = 1001;
    Order o2 = o1;// 赋值后, o1和o2的地址值相同, 都指向了堆空间中的同一个实体
    System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);
    o2.orderId = 1002;
    System.out.println("o1.orderId = " + o1.orderId + ", o2.orderId = " + o2.orderId);
    }
    }

    class Order {
    int orderId;
    }
    输出结果:
    ***************基本数据类型***************
    m = 10, n = 10
    m = 10, n = 20
    ***************引用数据类型***************
    o1.orderId = 1001, o2.orderId = 1001
    o1.orderId = 1002, o2.orderId = 1002
  • 对于基本数据类型,两个不同方法内的局部变量,互不影响,不因变量名相同而改变,因为是将实参基本数据类型变量的 “数据值” 传递给形参:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class ValueTransferTest {
    public void swap(int m, int n) {
    System.out.println("swap方法中, 交换之前: m = " + m + ", n = " + n);
    int temp = m;
    m = n;
    n = temp;
    System.out.println("swap方法中, 交换之后: m = " + m + ", n = " + n);
    }

    public static void main(String[] args) {
    int m = 10;
    int n = 20;
    System.out.println("main方法中, 交换之前: m = " + m + ", n = " + n);

    // 能够交换m和n的值
    int temp = m;
    m = n;
    n = temp;
    System.out.println("main方法中, 交换之后: m = " + m + ", n = " + n);

    // 不能够交换m和n的值
    ValueTransferTest valueTransferTest = new ValueTransferTest();
    System.out.println("main方法中, 调用swap方法之前: m = " + m + ", n = " + n);
    valueTransferTest.swap(m, n);// // swap方法调用完成后, 该方法内的局部变量temp, 形参m和n从栈内存中弹出回收
    System.out.println("main方法中, 调用swap方法之后: m = " + m + ", n = " + n);
    }
    }
    输出结果:
    main方法中, 交换之前: m = 10, n = 20
    main方法中, 交换之后: m = 20, n = 10
    main方法中, 调用swap方法之前: m = 20, n = 10
    swap方法中, 交换之前: m = 20, n = 10
    swap方法中, 交换之后: m = 10, n = 20
    main方法中, 调用swap方法之后: m = 20, n = 10

    内存解析图参考:

    image-20210223111755001
  • 对于引用数据类型,两个不同方法的局部变量,会互相影响,因为是将实参引用数据类型变量的 “地址值” 传递给形参,二者指向的是堆内存中的同一个对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public class ValueTransferTest {
    public void swap(Data data) {
    System.out.println("swap方法中, 交换之前: data.m = " + data.m + ", data.n = " + data.n);
    int temp = data.m;
    data.m = data.n;
    data.n = temp;
    System.out.println("swap方法中, 交换之后: data.m = " + data.m + ", data.n = " + data.n);
    }

    public static void main(String[] args) {
    Data data = new Data();
    data.m = 10;
    data.n = 20;
    System.out.println("main方法中, 交换之前: data.m = " + data.m + ", data.n = " + data.n);

    // 能够交换m和n的值
    int temp = data.m;
    data.m = data.n;
    data.n = temp;
    System.out.println("main方法中, 交换之后: data.m = " + data.m + ", data.n = " + data.n);

    // 能够交换m和n的值
    ValueTransferTest valueTransferTest = new ValueTransferTest();
    System.out.println("main方法中, 调用swap方法之前: data.m = " + data.m + ", data. = " + data.n);
    valueTransferTest.swap(data);// swap方法调用完成后, 该方法内的局部变量temp和形参data从栈内存中弹出回收
    System.out.println("main方法中, 调用swap方法之后: data.m = " + data.m + ", data.n = " + data.n);
    }
    }

    class Data {
    int m;
    int n;java
    }
    输出结果:
    main方法中, 交换之前: data.m = 10, data.n = 20
    main方法中, 交换之后: data.m = 20, data.n = 10
    main方法中, 调用swap方法之前: data.m = 20, data. = 10
    swap方法中, 交换之前: data.m = 20, data.n = 10
    swap方法中, 交换之后: data.m = 10, data.n = 20
    main方法中, 调用swap方法之后: data.m = 10, data.n = 20

    内存解析图参考:

    image-20210223111838133
  • 实例一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class ValueTransferTest {
    public void first() {
    int i = 5;
    Value v = new Value();
    v.i = 25;
    second(v, i);
    System.out.println(v.i);// 20
    }

    public void second(Value v, int i) {
    i = 0;
    v.i = 20;
    Value val = new Value();
    v = val;
    System.out.println(v.i + " " + i);// 15 0
    }

    public static void main(String[] args) {
    ValueTransferTest test = new ValueTransferTest();
    test.first();
    }
    }

    class Value {
    int i = 15;
    }
    image-20210223113602448
  • 实例二:

    image-20210223115107087

    方法一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {
    public static void method(int a, int b) {
    System.out.println("a = " + a * 10);
    System.out.println("b = " + b * 20);
    System.exit(0);
    }

    public static void main(String[] args) {
    int a = 10;
    int b = 10;
    method(a, b);
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    }
    }

    方法二:重写 PrintStream 的 println 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Test {
    public static void method(int a, int b) {
    PrintStream printStream = new PrintStream(System.out) {
    @Override
    public void println(String x) {
    if ("a = 10".equals(x)) {
    x = "a = 100";
    } else if ("b = 10".equals(x)) {
    x = "b = 200";
    }
    super.println(x);
    }
    };
    System.setOut(printStream);
    }

    public static void main(String[] args) {
    int a = 10;
    int b = 10;
    method(a, b);
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    }
    }
  • 实例三:

    定义一个 int 型的数组:int[] arr = new int[]{12,3,3,34,56,77,432};,让数组的每个位置上的值去除以首位置的元素,得到的结果,作为该位置上的新值,然后遍历新的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Test {
    public static void main(String[] args) {
    int[] arr = new int[]{12, 3, 3, 34, 56, 77, 432};
    System.out.println("计算前: " + Arrays.toString(arr));

    // 正确写法一
    int temp = arr[0];
    for (int i = 0; i < arr.length; i++) {
    arr[i] = arr[i] / temp;
    }

    // 正确写法二
    for (int i = arr.length - 1; i >= 0; i--) {
    arr[i] = arr[i] / arr[0];
    }

    // 错误写法
    /*for (int i = 0; i < arr.length; i++) {
    arr[i] = arr[i] / arr[0];
    }*/

    System.out.println("计算后: " + Arrays.toString(arr));
    }
    }
  • 实例四:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Test {
    public static void main(String[] args) {
    int[] arr = new int[]{1, 2, 3};
    System.out.println(arr);// 地址值

    char[] arr1 = new char[]{'a', 'b', 'c'};
    System.out.println(arr1);// 传入char数组时,println方法体内是遍历这个数组
    }
    }
    输出结果:
    [I@78308db1
    abc

递归方法 (recursion):

  • 一个方法体内调用它自身。方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。

  • 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似于死循环

  • 实例一:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class PassObject {
    // 1-n之间所有自然数的和
    public static int getSum(int n) {
    if (n == 1) {
    return 1;
    } else {
    return n + getSum(n - 1);
    }
    }

    // 1-n之间所有自然数的乘积
    public static long getProduct(int n) {
    if (n == 1) {
    return 1;
    } else {
    return n * getProduct(n - 1);
    }
    }

    public static void main(String[] args) {
    // 方式一:循环
    int sum = 0;
    for (int i = 1; i <= 100; i++) {
    sum += i;
    }
    System.out.println("1-100之间自然数的和: " + sum);

    // 方式二:递归
    System.out.println("1-100之间自然数的和: " + getSum(100));
    System.out.println("1-100之间自然数的积: " + getProduct(5));
    }
    }
  • 实例二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 已知有一个数列:f(0) = 1, f(1) = 4, f(n+2) = 2 * f(n+1) + f(n), 其中n是大于0的整数, 求f(10)的值。
    */
    public class PassObject {
    public static int f(int n) {
    if (n == 0) {
    return 1;
    } else if (n == 1) {
    return 4;
    } else {
    return 2 * f(n - 1) + f(n - 2);
    }
    }

    public static void main(String[] args) {
    int f = f(10);
    System.out.println(f);
    }
    }
    输出结果:
    10497
  • 实例三:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 已知一个数列: f(20) = 1, f(21) = 4, f(n+2) = 2 * f(n+1) + f(n), 其中n是大于0的整数, 求f(10)的值。
    */
    public class PassObject {
    public static int f(int n) {
    if (n == 20) {
    return 1;
    } else if (n == 21) {
    return 4;
    } else {
    return f(n + 2) - 2 * f(n + 1);
    }
    }

    public static void main(String[] args) {
    int f = f(10);
    System.out.println(f);
    }
    }
    输出结果:
    -3771
  • 实例四:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    /**
    * 斐波那契数列: 1 1 2 3 5 8 13 21 34 55 ...
    * 规律: 一个数等于前两个数之和
    * 要求:计算斐波那契数列(Fibonacci)的第n个值,并将整个数列打印出来
    */
    public class PassObject {
    public static int f(int n) {
    if (n <= 0 || n >= 30) {
    return 0;
    }
    if (n == 1) {
    return 1;
    } else if (n == 2) {
    return 1;
    } else {
    return f(n - 1) + f(n - 2);
    }
    }

    public static void main(String[] args) {
    int[] arr = new int[5];
    int sum = 0;
    for (int i = 0; i < arr.length; i++) {
    arr[i] = f(i + 1);
    sum += arr[i];
    }

    System.out.println(Arrays.toString(arr));
    System.out.println("和: " + sum);
    }
    }
    输出结果:
    [1, 1, 2, 3, 5]
    和: 12
  • 实例五:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class PassObject {
    private static int count = 0;

    public static int recursion(int k) {
    count++;
    System.out.println("count1: " + count + ", k: " + k);
    if (k <= 0) {
    return 0;
    }
    return recursion(k - 1) + recursion(k - 2);
    }

    public static void main(String[] args) {
    recursion(4);
    }
    }
    输出结果:
    count1: 1, k: 4
    count1: 2, k: 3
    count1: 3, k: 2
    count1: 4, k: 1
    count1: 5, k: 0
    count1: 6, k: -1
    count1: 7, k: 0
    count1: 8, k: 1
    count1: 9, k: 0
    count1: 10, k: -1
    count1: 11, k: 2
    count1: 12, k: 1
    count1: 13, k: 0
    count1: 14, k: -1
    count1: 15, k: 0

    递归过程:

    image-20210223160555518

    遍历过程相当于二叉树的前序遍历。

OOP 特征一:封装和隐藏

封装性的设计思想:隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。

程序设计追求 “高内聚,低耦合”:

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉。

  • 低耦合:仅对外暴露少量的方法用于使用。

信息的封装和隐藏:

  • java 中通过将对象的属性声明为私有的 (private),再提供公共的 (public) 方法 — getXxx()setXxx(),来实现对属性的操作,并以此达到下述目的:

    • 隐藏一个类中不需要对外提供的实现细节。

    • 使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作。

    • 便于修改,增强代码的可维护性。

    image-20210223164658179

封装性的体现:属性私有、方法私有、构造器私有 (单例模式) 等。

封装性的体现,需要权限修饰符的配合。

四种权限修饰符

  • 从小到大排列:private、缺省 (什么都不写)、protected、public。

  • 权限修饰符置于类的成员定义前,用来限定对象对该类成员的访问权限:

    image-20210223173051273
  • 权限修饰符可以用来修饰类及类的内部结构:属性、方法、构造器、内部类。

  • 对于 class 的权限修饰只可以用 public 和 default (缺省)。

    • public 类可以在任意地方被访问。
  • default 类只可以被同一个包内部的类访问。

  • 对于 class 的内部结构,四种权限修饰符都可以使用。

封装性总结:java 提供了 4 种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用时的可见性的大小。

image-20210223203339682

本类中任意调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package cn.xisun.database;

public class Order {
private int orderPrivate;
int orderDefault;
protected int orderProtected;
public int orderPublic;

private void methodPrivate() {
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}

void methodDefault() {
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}

protected void methodProtected() {
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}

public void methodPublic() {
orderPrivate = 1;
orderDefault = 2;
orderProtected = 3;
orderPublic = 4;
}
}

同包中的其他类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.xisun.database;

public class OrderTest {
public static void main(String[] args) {
Order order = new Order();

order.orderDefault = 1;
order.orderProtected = 2;
order.orderPublic = 3;

order.methodDefault();
order.methodProtected();
order.methodPublic();

// 同一个包中的其他类,不可以调用Order类中private的属性和方法
/*order.orderPrivate = 4;

order.methodPrivate();*/
}
}

不同包中的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.xisun.database.postgresql;

import cn.xisun.database.Order;

public class SubOrder extends Order {
public void method() {
orderProtected = 1;
orderPublic = 2;

methodProtected();
methodPublic();

// 不同包的子类中,不可以调用Order类中private和缺省的属性和方法
/*orderPrivate = 3;
orderDefault = 4;

methodPrivate();
methodDefault();*/
}
}

不同包中的其他类 (非子类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.xisun.database.postgresql;

import cn.xisun.database.Order;

public class OtherOrderTest {
public static void main(String[] args) {
Order order = new Order();

order.orderPublic = 1;

order.methodPublic();

// 不同包下的普通类(非子类),不可以调用Order类中private、缺省和protected的属性和方法
/*order.orderPrivate = 2;
order.orderDefault = 3;
order.orderProtected = 4;

order.methodPrivate();
order.methodDefault();
order.methodProtected();*/
}
}

类的成员之三:构造器 (构造方法,constructor)

构造器的作用:

  • 创建对象;给对象进行初始化。如:Order o = new Order(); Person p = new Person(“Peter”, 15);

语法格式:

image-20210224091734897

根据参数不同,构造器可以分为如下两类:

  • 隐式无参构造器 (系统默认提供)。

  • 显定义一个或多个构造器 (无参、有参)。

构造器的特征:

  • 构造器具有与类相同的名称,不声明返回值类型,与声明为 void 不同。
  • java 语言中,每个类都至少有一个构造器。
  • 如果没有显示的定义类的构造器,则系统默认提供一个无参构造器。一旦显式定义了构造器, 则系统不再提供默认构造器。
  • 一般情况下,为了防止一些框架出异常,无论要不要自定义其他构造器,都应该把类的无参构造器显示的定义出来。
  • 构造器的修饰符默认与所属类的修饰符一致,即:public 或 default (缺省)。
  • 构造器不能被 static、final、synchronized、abstract、native 修饰,不能有 return 语句返回值。
  • 一个类中定义的多个构造器,彼此构成重载。
  • 父类的构造器不可被子类继承。

JavaBean

  • JavaBean 是一种 java 语言写成的可重用组件。

  • 所谓 JavaBean,是指符合如下标准的 java 类:

    • 类是公共的。
    • 有一个无参的公共的构造器。
    • 有属性,且有对应的 get、set 方法。
  • 用户可以使用 JavaBean 将功能、处理、值、数据库访问和其他任何可以用 java 代码创造的对象进行打包,并且其他的开发者可以通过内部的 JSP 页面、Servlet、其他 JavaBean、applet 程序或者应用来使用这些对象。用户可以认为 JavaBean 提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

UML 类图

image-20210224112245811

关键字:this

this 关键字的使用:

  • this 可以用来修饰或调用:属性、方法、构造器。

  • this 修饰属性和方法:

    • this理解为:当前对象或当前正在创建的对象。
    • 在类的方法中,可以使用 “this.属性” 或 “this.方法” 的方式,调用当前属性或方法。
      • 通常情况下,可以省略 “this.”。
      • 特殊情况下,如果方法的形参和类的属性同名,则必须显示的使用 “this.变量” 的方式,表明此变量是属性,而非形参。
    • 在类的构造器中,可以使用 “this.属性” 或 “this.方法” 的方式,调用当前正在创建的对象的属性或方法。
      • 通常情况下,可以省略 “this.”。
      • 特殊情况下,如果构造器的形参和类的属性同名,则必须显示的使用 “this.变量” 的方式,表明此变量是属性,而非形参。
    • 使用 this 访问属性和方法时,如果在本类中未找到,会从父类中查找。
  • this 调用构造器:

    • 在类的构造器中,可以显示的使用 “this(形参列表)” 的方式,调用本类中的其他构造器。
      • 存在构造器的多重调用时,创建的对象仍然是只有一个,而不是调用一个构造器就创造了一个新的对象,只有最开始被调用的构造器才创造了对象。
    • 构造器中,不能使用 “this(形参列表)” 的方式调用自己。
    • 如果一个类中有 n 个构造器,则最多有 n - 1 个构造器中使用了 “this(形参列表)”。
      • 构造器在彼此调用时,不能形参一个封闭环,如:构造器 A 中调用了构造器 B,则在构造器 B 中不能再调用构造器 A,多构造器调用类推。
    • 规定:”this(形参列表)” 必须声明在当前构造器的首行。
    • 一个构造器内部,最多只能声明一个 “this(形参列表)”,即只能调用一个其他的构造器。
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class Person {
    private String name;

    private int age;

    // 无参构造器
    public Person() {
    this.eat();
    }

    // 带name的构造器
    public Person(String name) {
    this();// 调用无参构造器
    this.name = name;
    }

    // 带name和age的构造器
    public Person(String name, int age) {
    this(name);// 调用带name的构造器
    this.age = age;
    }

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

    public String getName() {
    return this.name;// 此处this可以省略
    }

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

    public int getAge() {
    return this.age;// 此处this可以省略
    }

    public void eat() {
    System.out.println("人吃饭");
    this.study();// this调用方法,此处this可以省略
    }

    public void study() {
    this.eat();// this调用方法,此处this可以省略
    System.out.println("人学习");
    }

    public static void main(String[] args) {

    }
    }
  • 实例二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Boy {
    private String name;

    private int age;

    public Boy() {

    }

    public Boy(String name, int age) {
    this.name = name;
    this.age = age;
    }

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

    public String getName() {
    return name;
    }

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

    public int getAge() {
    return age;
    }

    public void marray(Girl girl) {
    System.out.println("我想娶" + girl.getName());
    }

    public void shout() {
    System.out.println("我想找对象");
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class Girl {
    private String name;

    private int age;

    public Girl() {

    }

    public Girl(String name, int age) {
    this.name = name;
    this.age = age;
    }

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

    public String getName() {
    return name;
    }

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

    public int getAge() {
    return age;
    }

    public void marry(Boy boy) {
    System.out.println("我想嫁给" + boy.getName());
    boy.marray(this);// 传入当前Girl对象
    }

    public int compare(Girl girl) {
    return this.age - girl.age;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Person {
    public static void main(String[] args) {
    Boy boy = new Boy("罗密欧", 20);
    boy.shout();
    Girl girl = new Girl("朱丽叶", 18);
    girl.marry(boy);

    Girl girl2 = new Girl("祝英台", 19);
    System.out.println("年龄差:" + girl.compare(girl2));
    }
    }

关键字:package

package 语句作为 java 源文件的第一条语句,指明该文件中定义的类所在的包。若缺省该语句,则指定为无名包。

语法格式:

image-20210224161550855
  • 包对应于文件系统的目录,package 语句中,用 . 来指明包 (目录) 的层次。

  • 包属于标识符,遵循标识符的命名规范,通常用小写单词标识。通常使用所在公司域名的倒置,如:com.atguigu.xxx

  • 同一个包下,不能命名同名的接口、类。不同的包下,可以命名同名的接口、类。

  • JDK 中主要的包介绍:

    • java.lang —- 包含一些 java 语言的核心类,如 String、Math、Integer、 System 和 Thread,提供常用功能。
    • java.net —- 包含执行与网络相关的操作的类和接口。
    • java.io —- 包含能提供多种输入/输出功能的类。
    • java.util —- 包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。
    • java.text —- 包含了一些 java 格式化相关的类。
    • java.sql —- 包含了 java 进行 JDBC 数据库编程的相关类/接口。
    • java.awt —- 包含了构成抽象窗口工具集 (abstract window toolkits) 的多个类,这些类被用来构建和管理应用程序的图形用户界面 (GUI)。(B/S 和 C/S)

关键字:import

为使用定义在不同包中的 java 类,需用 import 语句来引入指定包层次下所需要的类或全部类 (.*)。import 语句告诉编译器到哪里去寻找类。

语法格式:

image-20210224163212506
  • 在源文件中使用 import 语句,可以显式的导入指定包下的类或接口。

  • 声明在包的声明和类的声明之间。

  • 如果需要导入多个类或接口,那么就并列显式声明多个 import 语句即可。

  • 举例:可以使用 import java.util.*; 的方式,一次性导入 java.util 包下所有的类或接口。

  • 如果导入的类或接口是 java.lang 包下的,或者是当前包下的,则可以省略此 import 语句。

  • 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。

  • 如果已经导入 java.a 包下的类,那么如果需要使用 a 包的子包下的类的话,仍然需要导入。

  • import static 组合的使用:导入指定类或接口下的静态的属性或方法。

    1
    2
    3
    4
    5
    6
    7
    import static java.lang.System.*;

    public class Person {
    public static void main(String[] args) {
    out.println("打印方法");// 可以省略System
    }
    }

OOP 特征二:继承性

如果多个类中存在相同的属性和行为时,将这些内容抽取到单独一个类中,那么这多个类无需再定义这些属性和行为,只要继承那个抽出来的类即可。

此处的多个类称为子类 (派生类、subclass)**,单独的这个类称为父类 (基类、超类、superclass)**。可以理解为:”子类 is a 父类”。

类继承语法规则:

image-20210225094119803

继承性的作用:

  • 继承的出现减少了代码冗余,提高了代码的复用性。
  • 继承的出现,更有利于功能的扩展。
  • 继承的出现,让类与类之间产生了关系,提供了多态的前提。

继承性的特点:

  • 子类继承了父类,就继承了父类中声明的所有属性和方法。特别的,父类中声明为 private 的属性和方法,子类继承父类以后,仍然认为子类获取了父类中私有的结构,只是因为封装性的影响,使得子类的实例不能直接调用父类的这些私有的结构而已 (事实上,父类的实例,也不能直接调用这些私有的结构)。
  • 在子类中,可以使用父类中定义的方法和属性,也可以声明创建子类特有的属性和方法,以实现功能的扩展。
  • 在 java 中,继承的关键字用的是 extends,即子类不是父类的子集,而是对父类的扩展。

继承性的规则:

  • 子类不能直接访问父类中私有的 (private) 的成员变量和方法。

    image-20210225102501129

  • java 只支持单继承和多层继承,不允许多重继承。

    • 一个子类只能有一个父类。

    • 一个父类可以派生出多个子类。

      image-20210225102723411 image-20210225102803030 image-20210225104141839
    • 此处强调的是 java 类的单继承性,java 中,接口是可以多继承的。

  • 子类和父类是一个相对概念。子类直接继承的父类,称为直接父类,间接继承的父类,称为间接父类。

  • 子类继承父类后,就获取了直接父类及所有间接父类中声明的属性和方法。

  • 所有的 java 类 (除 java.lang.Object 类之外),都直接或间接继承 java.lang.Object。

方法的重写 (override/overwrite)

在子类中可以根据需要,对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。

重写的要求:

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表。

  • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限 (权限修饰符)。

    • 子类不能重写父类中声明为 private 权限的方法。
    • 子类中可以声明与父类 private 方法相同名称和参数列表的方法,但不属于重写。
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。

    • 父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void。
    • 父类被重写的方法的返回值类型是 A 类型,则子类重写的方法的返回值类型可以是 A 类或 A 类的子类。
    • 父类被重写的方法的返回值类型是基本数据类型 (比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型 (即,只能是 double)。
  • 子类重写的方法抛出的异常类型不能大于父类被重写的方法抛出的异常类型。

  • 子类与父类中同名同参数的方法必须同时声明为非 static 的(此时属于重写),或者同时声明为 static 的 (此时不属于重写)。因为 static 方法是属于类的,子类无法覆盖父类的方法。

实例一:

image-20210225142115203

实例二:

image-20210225142247945

方法重载与重写的区别:

  1. 二者的定义细节:略。
  2. 从编译和运行的角度看:重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。java 的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为 “早绑定”“静态绑定”**;而对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为 **”晚绑定” 或 **”动态绑定”**。引用一句 Bruce Eckel 的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
  3. 重载不表现为多态性,重写表现为多态性。

关键字:super

  • super 理解为:父类的。

  • super 可以用来调用父类的:属性、方法、构造器。

  • 在子类的方法或构造器中,可以通过使用 “super.属性” 或 “super.方法” 的形式,显示的调用父类中声明的属性或方法。

    • 通常情况下,可以省略 “super.”。
    • 特殊情况:当子类和父类中定义了同名的属性时,要想在子类中调用父类中声明的该属性,则必须显示的使用 “super.属性” 的方式,表明调用的是父类中声明的属性。
    • 特殊情况:当子类重写了父类中的方法以后,要想在子类中调用父类中被重写的方法时,则必须显示的使用 “super.方法” 的方式,表明调用的是父类中被重写的方法。
  • 在子类的构造器中,可以通过使用 “super(形参列表)” 的形式,显示的调用父类中声明的指定的构造器。

    • “super(形参列表)” 的使用,必须声明在子类构造器的首行。
    • 在类的构造器中,针对于 “this(形参列表)” 或 “super(形参列表)”,只能二选一,不能同时出现。
    • 在构造器的首行,如果没有现实的声明 “this(形参列表)” 或 “super(形参列表)”,则默认调用的是父类中空参的构造器,即:super();
      • 子类中所有的构造器默认都会访问父类中空参的构造器。
      • 当父类中没有空参的构造器时,子类的构造器必须通过 “this(形参列表)” 或 “super(形参列表)” 语句,指定调用本类或者父类中相应的构造器。同时,只能二选一,且必须放在构造器的首行。
      • 如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错。
    • 在类的多个构造器中,至少有一个类的构造器中使用了 “super(形参列表)”,调用父类中的构造器。
  • 实例:

    父类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Person {
    String name;
    int age;
    int id = 1000;

    public Person() {
    System.out.println("父类的空参构造器");
    }

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

    public void eat() {
    System.out.println("吃饭");
    }

    public void sleep() {
    System.out.println("睡觉");
    }
    }

    子类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class Student extends Person {
    // String name;// 父类中已有的属性,可以省略
    // int age;// 父类中已有的属性,可以省略
    String major;
    int id = 1001;

    public Student() {

    }

    public Student(String name, int age, String major) {
    this.name = name;
    this.age = age;
    this.major = major;
    }

    // 父类中已有的方法,可以省略,如有需要,可以重写,
    // public void eat() {
    // System.out.println("吃饭");
    // }

    // 重写父类的方法
    @Override
    public void sleep() {
    System.out.println("学生睡觉");
    }

    public void study() {
    System.out.println("学习");
    }

    public void show() {
    System.out.println("子类中的id: " + this.id);// this可以省略,就近原则
    System.out.println("父类中的id: " + super.id);// 子类与父类有同名的属性id,此时super不可以省略
    }

    public static void main(String[] args) {
    Student student = new Student();
    student.show();
    }
    }

this 和 super 的区别:

image-20210225163054270

思考:

  • 为什么 “super(形参列表)” 和 “this(形参列表)” 调用语句不能同时在一个构造器中出现?
    • 因为 “super(形参列表)” 和 “this(形参列表)” 调用语句都必须出现在构造器中的首行。
  • 为什么 “super(形参列表)” 和 “this(形参列表)” 只能作为构造器中的第一句出现?
    • 因为无论通过哪个构造器创建子类对象,都需要保证先初始化父类。这样做的目的是:当子类继承父类后,可以获得父类中所有的属性和方法,这样子类就有必要在一开始就知道父类是如何为对象进行初始化。

子类对象实例化过程

image-20210226153054261

  • 从结果上看:

    • 子类继承父类之后,就获取了父类中声明的属性和方法。(继承性)

    • 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

      image-20210225171130690
  • 从过程上看:

    • 当通过子类的构造器创建子类对象时,一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了 java.lang.Object 类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才能够进行调用。

    • 明确:虽然创建子类对象时,调用了父类的构造器,但是自始至终只创建了一个对象,即为 new 出来的子类对象。

      image-20210225171520474
  • 实例:

    从输出结果可以看出,在创建 Man 的实例时,先进入了父类的空参构造器,然后执行子类的空参构造器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class Person {
    String name;
    int age;

    public Person() {
    System.out.println("父类空参构造器");
    }

    public void eat() {
    System.out.println("人吃饭");
    }

    public void walk() {
    System.out.println("人走路");
    }

    public static void main(String[] args) {
    Person person = new Man();
    person.eat();
    person.walk();
    }
    }

    class Man extends Person {
    boolean isSmoking;

    public void earnMoney() {
    System.out.println("男人负责挣钱养家");
    }


    @Override
    public void eat() {
    System.out.println("男人多吃肉,长肌肉");
    }

    @Override
    public void walk() {
    System.out.println("男人霸气的走路");
    }
    }
    输出结果:
    父类空参构造器
    子类空参构造器
    男人多吃肉,长肌肉
    男人霸气的走路

OOP 特征三:多态性

多态性,也叫对象的多态性:父类的引用指向子类的对象 (或子类的对象赋给父类的引用)。

  • 一个变量只能有一种确定的数据类型。

  • 一个引用类型变量可能指向 (引用) 多种不同类型的对象。

  • 子类可看做是特殊的父类,所以父类类型的引用可以指向子类的对象:向上转型 (upcasting)。

  • 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法:

    image-20210226115046078

多态的使用:

  • 虚拟方法调用。

  • 有了对象的多态性以后,在编译期,只能调用父类中声明的方法,但在运行期,实际执行的是子类中重写的父类的方法。

  • 编译,看左边;运行,看右边。

    • java 引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
    • 若编译时类型和运行时类型不一致,就出现了对象的多态性。
    • 多态情况下,看左边:看的是父类的引用 (父类中不具备子类特有的方法),看右边:看的是子类的对象 (实际运行的是子类重写父类的方法)。
  • 对象的多态性,只适用于方法,不适用于属性。对于属性,编译期和运行期,看的都是左边,即都是父类中声明的那个属性。

    • 成员方法:编译时,要查看引用变量所声明的类中是否有所调用的方法。运行时,调用实际 new 的对象所属的类中的重写方法。
    • 成员变量:不具备多态性,只看引用变量所声明的类。
  • 子类继承父类:

    • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
    • 编译,看左边;运行,看右边。
    • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
      • 编译,运行,都看左边。
  • 实例:

    父类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Person {
    String name;
    int age;

    public void eat() {
    System.out.println("人吃饭");
    }

    public void walk() {
    System.out.println("人走路");
    }

    public static void main(String[] args) {
    // 对象的多态性:父类的引用指向子类的对象
    Person person = new Man();
    // 多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写的父类的方法---虚拟方法调用
    // 编译期,只能调用父类Person类中的方法;运行期,执行的是子类Man类中的方法。
    person.eat();
    person.walk();
    // 不能调用子类特有的属性或方法,因为编译时,person是Person类型,而Person类中没有子类的这个特有属性或方法。
    // 有了对象的多态性以后,内存中实际上是加载了子类特有的属性或方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中
    // 声明的属性和方法,子类中特有的属性和方法不能调用。
    // person.isSmoking = true;
    // person.earnMoney();

    System.out.println("*********************************")
    // 如何才能使用子类特有的属性和方法?
    // 向下转型:使用强制类型转换符
    Man man = (Man) person;
    man.isSmoking = true;
    man.earnMoney();
    // 使用强转时,可能出现java.lang.ClassCastException异常
    Woman woman = (Woman) person;
    woman.goShopping();
    }
    }
    输出结果:
    父类空参构造器
    子类空参构造器
    男人多吃肉,长肌肉
    男人霸气的走路
    *********************************
    男人负责挣钱养家

    子类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Man extends Person {
    boolean isSmoking;

    public void earnMoney() {
    System.out.println("男人负责挣钱养家");
    }


    @Override
    public void eat() {
    System.out.println("男人多吃肉,长肌肉");
    }

    @Override
    public void walk() {
    System.out.println("男人霸气的走路");
    }
    }
  • 实例二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class FieldMethodTest {
    public static void main(String[] args) {
    Sub s = new Sub();
    System.out.println(s.count);// 20
    s.display();// 20
    Base b = s;
    // 对于引用数据,==比较的是两个引用数据类型变量的地址值
    System.out.println(b == s);// true
    System.out.println(b.count);// 10
    b.display();// 20
    }
    }

    class Base {
    int count = 10;

    public void display() {
    System.out.println(this.count);
    }
    }

    class Sub extends Base {
    int count = 20;

    @Override
    public void display() {
    System.out.println(this.count);
    }
    }
  • 实例三:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class InterviewTest1 {
    public static void main(String[] args) {
    Base base = new Sub();
    base.add(1, 2, 3);// sub_1

    Sub s = (Sub) base;
    s.add(1, 2, 3);// sub_2
    }
    }

    class Base {
    public void add(int a, int... arr) {
    System.out.println("base");
    }
    }

    class Sub extends Base {
    @Override
    public void add(int a, int[] arr) {
    System.out.println("sub_1");
    }

    // 这个方法没有重写,在Base类中不存在这样声明的方法,
    // 也就没有多态,所以base.add(1, 2, 3)方法输出sub_1
    public void add(int a, int b, int c) {
    System.out.println("sub_2");
    }
    }

多态性的使用前提:

  • 有类的继承关系。
  • 有方法的重写。
  • 如果没有以上两个前提,就不存在多态。

多态性的优点:

  • 提高了代码的通用性,常称作接口重用。

  • 方法声明的形参类型为父类类型,可以使用子类的对象作为实参调用该方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    public class AnimalTest {
    // 多态的使用:传入的是Animal对象,但实际传入的可以是Animal的子类
    public void func(Animal animal) {
    animal.eat();
    animal.shout();
    }

    public static void main(String[] args) {
    AnimalTest animalTest = new AnimalTest();

    animalTest.func(new Dog());
    animalTest.func(new Cat());
    }
    }

    class Animal {
    public void eat() {
    System.out.println("动物:进食");
    }

    public void shout() {
    System.out.println("动物:叫");
    }
    }

    class Dog extends Animal {
    @Override
    public void eat() {
    System.out.println("狗吃骨头");
    }

    @Override
    public void shout() {
    System.out.println("汪!汪!汪!");
    }
    }

    class Cat extends Animal {
    @Override
    public void eat() {
    System.out.println("猫吃鱼");
    }

    @Override
    public void shout() {
    System.out.println("喵!喵!喵!");
    }
    }

    // 举例二
    class Order {
    // 此方法可以传入任意对象,而不需要每个特定对象都创建一次method()方法
    public void method(Object object) {
    // 方法体
    }
    }
  • 抽象类、接口的使用。(抽象类和接口不能实例化,它们的使用也体现了多态)

虚拟方法调用:

  • 正常的方法调用:

    1
    2
    3
    4
    Person e = new Person();
    e.getInfo();
    Student e = new Student();
    e.getInfo();
  • 虚拟方法调用 (多态情况下)

    子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

    1
    2
    Person e = new Student();
    e.getInfo();// 调用Student类的getInfo()方法
  • 编译时类型和运行时类型

    上面代码中,编译时 e 为 Person 类型,而方法的调用是在运行时确定的,所以调用的是 Student 类的 getInfo() 方法 —— 动态绑定

  • 重写是多态,重载不是。

  • 实例:

    image-20210226131836066

多态是编译时行为还是运行时行为?

  • 多态是运行时行为,证明方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    public class InterviewTest {
    public static Animal getInstance(int key) {
    switch (key) {
    case 0:
    return new Cat();
    case 1:
    return new Dog();
    default:
    return new Sheep();
    }
    }

    public static void main(String[] args) {
    // 因为key需要在运行时才能得到值,编译期时无法判断getInstance()方法输出什么
    int key = new Random().nextInt(3);
    System.out.println(key);
    Animal animal = getInstance(key);
    animal.eat();
    }
    }

    class Animal {
    protected void eat() {
    System.out.println("animal eat food");
    }
    }

    class Cat extends Animal {
    @Override
    protected void eat() {
    System.out.println("cat eat fish");
    }
    }

    class Dog extends Animal {
    @Override
    public void eat() {
    System.out.println("Dog eat bone");
    }
    }

    class Sheep extends Animal {
    @Override
    public void eat() {
    System.out.println("Sheep eat grass");
    }
    }

关键字:instanceof

a instanceof A:检验对象 a 是否为类 A 的对象实例,如果是,返回 true,如果不是,返回 false。

  • 使用情景:为了避免向下转型时出现 java.lang.ClassCastException,在向下转型之前,先进行 instanceof 判断,在返回 true 时,才进行向下转型。

  • 要求 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误。

  • 如果 a 属于类 A 的子类 B,a instanceof A 的返回值也为 true。

    image-20210226172858254

对象类型转换 (casting)

image-20210226212518453
  • 基本数据类型的 Casting

    • 自动类型转换:小的数据类型可以自动转换成大的数据类型。如 long g = 20; double d = 12.0f;
      • 强制类型转换:可以把大的数据类型强制转换 (casting) 成小的数据类型。如 float f = (float)12.0; int a = (int)1200L;
  • 对 java 对象的强制类型转换称为造型

    • 从子类到父类的类型转换可以自动进行。
    • 从父类到子类的类型转换必须通过造型 (强制类型转换) 实现。
    • 无继承关系的引用类型间的转换是非法的。
    • 在造型前可以使用 instanceof。
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ConversionTest {
    public static void main(String[] args) {
    double d = 13.4;
    long l = (long) d;
    System.out.println(l);
    int in = 5;
    // boolean b = (boolean)in;
    Object obj = "Hello";
    String objStr = (String) obj;
    System.out.println(objStr);
    Object objPri = new Integer(5);
    // 下面代码运行时引发ClassCastException异常
    String str = (String) objPri;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {
    public void method(Person e) {// 设Person类中没有getschool()方法
    // System.out.pritnln(e.getschool());// 非法,编译时错误
    if (e instanceof Student) {
    Student me = (Student) e;// 将e强制转换为Student类型
    System.out.pritnln(me.getschool());
    }
    }

    public static void main(String[] args) {
    Test t = new Test();
    Student m = new Student();
    t.method(m);
    }
    }

Object 类的使用

  • Object 类是所有 java 类的根父类。

  • 如果在类的声明中未使用 extends 关键字指明其父类,则默认父类为 java.lang.Object 类。

    image-20210227162812948
  • 验证方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ObjectTest {
    public static void main(String[] args) {
    Base base = new Base();
    System.out.println("父类:" + base.getClass().getSuperclass());// 父类:class java.lang.Object
    }
    }

    class Base {
    }
  • Object 类中的主要结构

    image-20210227163126263

== 操作符与 equals() 方法

== 运算符:

  • 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等,不一定类型要相同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) {
    int i = 10;

    int j = 10;
    System.out.println(i == j);// true

    double k = 10.0;
    System.out.println(i == k);// true

    char c = 10;
    System.out.println(i == c);// true

    char c1 = 'A';
    char c2 = 65;
    System.out.println(c1 == c2);// true
    }
  • 如果比较的是引用数据类型变量:比较两个变量的地址值是否相同,即两个引用是否指向同一个对象实体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // String类比较特殊,要注意。
    public static void main(String[] args) {
    String s1 = "javacdfa";// 这样写的javacdfa,位于常量池中
    String s2 = "javacdfa";
    System.out.println(s1 == s2);// true

    String s3 = new String("iam");// 这样new的,在堆内存中
    String s4 = new String("iam");
    System.out.println(s3 == s4);// false
    }
  • 用 == 进行比较时,符号两边的数据类型必须兼容 (可自动转换的基本数据类型除外),否则编译出错。

equals() 方法:

  • 是一个方法,而非运算符,只能适用于引用数据类型。

  • 使用格式:obj1.equals(obj2)

  • 所有类都继承了 Object,也就获得了 equals() 方法,也可以对其重写 。

  • Object 类中 equals() 方法的定义:

    1
    2
    3
    public boolean equals(Object obj) {
    return (this == obj);
    }

    说明:其作用与 == 相同, 比较是否指向同一个对象 。

  • 像 File、String、Date 及包装类等,都重写了 Object 类中的 equals() 方法,重写以后,比较的不是两个引用对象的地址是否相同,而是比较两个引用对象的 “实体内容” 是否相同。

  • 通常情况下,自定义的类使用 equals() 方法时,也是比较两个引用对象的 “实体内容” 是否相同。那么,就应该重写 equals() 方法。比如 String 类的 equals() 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public boolean equals(Object anObject) {
    // 先判断地址
    if (this == anObject) {
    return true;
    }
    // 再判断内容
    if (anObject instanceof String) {
    String anotherString = (String)anObject;
    int n = value.length;
    if (n == anotherString.value.length) {
    char v1[] = value;
    char v2[] = anotherString.value;
    int i = 0;
    while (n-- != 0) {
    if (v1[i] != v2[i])
    return false;
    i++;
    }
    return true;
    }
    }
    return false;
    }
  • 重写 equals() 方法的原则:

    • 对称性:如果 x.equals(y) 返回是 true,那么 y.equals(x) 也应该返回是 true。
    • 自反性:x.equals(x) 必须返回是 true。
    • 传递性:如果 x.equals(y) 返回是 true,而且 y.equals(z) 返回是 true,那么 z.equals(x) 也应该返回是 true。
    • 一致性:如果 x.equals(y) 返回是 true,只要 x 和 y 内容一直不变,不管重复 x.equals(y) 多少次,返回都是 true。
    • 任何情况下,x.equals(null) 永远返回是 false;x.equals(和x不同类型的对象) 永远返回是 false。

面试题:== 和 equals() 的区别?

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型是比较值,对于引用类型是比较内存地址。
  • equals() 方法属于 java.lang.Object 类里面的方法,如果该方法没有被重写过,默认也是 ==。
  • 具体到特定自定义的类,要看该类里有没有重写 Object 的 equals() 方法以及重写的逻辑。
  • 通常情况下,重写 equals() 方法,是比较类中的相应属性是否都相等。

toString() 方法

  • 当输出一个对象的引用时,实际上就是调用当前对象的 toString() 方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class ObjectTest {
    public static void main(String[] args) {
    Order order = new Order();
    System.out.println(order);// cn.xisun.database.Order@78308db1
    System.out.println(order.toString());// cn.xisun.database.Order@78308db1
    }
    }

    class Order {

    }
  • Object 类中 toString() 方法的定义:

    1
    2
    3
    public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
  • 像 File、String、Date 及包装类等,都重写了 Object 类中的 toString() 方法,使得在调用对象的 toString() 方法时,返回相应的 “实体内容”。

  • 自定义类也可以重写 toString() 方法,当调用此方法时,返回 相应的 “实体内容”。比如 String 类的 equals() 方法:

    1
    2
    3
    public String toString() {
    return this;
    }
  • 基本类型数据转换为 String 类型时,调用了对应包装类的 toString() 方法。

  • 面试题:

    1
    2
    3
    4
    5
    6
    7
    8
    public static void main(String[] args) {
    char[] arr = new char[] { 'a', 'b', 'c' };
    System.out.println(arr);// abc
    int[] arr1 = new int[] { 1, 2, 3 };
    System.out.println(arr1);// [I@78308db1
    double[] arr2 = new double[] { 1.1, 2.2, 3.3 };
    System.out.println(arr2);// [D@27c170f0
    }

包装类 (Wrapper) 的使用

包装类:也叫封装类,是针对八种基本数据类型定义的相应的引用数据类型,以使得基本数据类型的变量具有类的特征。

image-20210228120809840
  • JDK 1.5 之后,支持自动装箱,自动拆箱,但类型必须匹配。

基本类型、包装类与 String 类之间的转换:

image-20210228125636680
  • 基本数据类型转换成包装类
    • 装箱:基本数据类型包装成包装类的实例,通过包装类的构造器实现。例如:int i = 500; Integer t = new Integer(i);
    • 自动装箱,例如:int i =500; Integer t = i;
  • 包装类转换成基本数据类型
    • 拆箱:获得包装类对象中包装的基本类型变量,通过调用包装类的 .xxxValue() 方法。例如:boolean b = bObj.booleanValue();
    • 自动拆箱,例如:Integer t = 500; int i = t;
  • 基本数据类型/包装类转换成字符串
    • 调用字符串重载的 valueOf() 方法,例如:String fstr = String.valueOf(2.34f);。
    • 更直接的方式,连接运算,例如:String intStr = 5 + "";
  • 字符串转换成基本数据类型/包装类
    • 通过包装类的构造器实现,例如:int i = new Integer("12");
    • 通过包装类的 parseXxx(String s) 静态方法,例如:Float f = Float.parseFloat(“12.1”);

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 三目运算符比较基本数据类型,在编译阶段自动拆箱为int和double类型,由于三目运算符要求表达式2和表达式3类型一致,
// 所以在编译阶段自动类型提升(即int自动类型转换为double类型),再自动装箱为Object,输出时使用多态调用重写
// 的toString(),即Double包装类的toString()方法
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
}
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);// new了两个对象,false
Integer m = 1;
Integer n = 1;
System.out.println(m == n);// 自动装箱,且在-128~127范围内,true
Integer x = 128;// 相当于new Integer(128);
Integer y = 128;// 相当于new Integer(128);
System.out.println(x == y);// false
}

Integer 类内部定义了 IntegerCache 结构,IntegerCache 中定义了一个 Integer[] 数组,保存了从 -128127 范围的整数。如果使用了自动装箱的方式,给 Integer 赋值在 -128127 范围内时,可以直接使用数组中的元素,不用 new。目的:提高效率。如果赋值超过了此范围,会 new 一个新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 利用Vector代替数组处理:从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。
* 提示:数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类java.util.Vector可以根据需要动态伸缩。
*  创建Vector对象:Vector v=new Vector();
*  给向量添加元素:v.addElement(Object obj);// obj必须是对象
*  取出向量中的元素:Object obj=v.elementAt(0);
*  注意第一个元素的下标是0,返回值是Object类型的。
*  计算向量的长度:v.size();
*  若与最高分相差10分内:A等;20分内:B等;30分内:C等;其它:D等。
*/
public class ScoreTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
Vector<Object> objects = new Vector<>();
int maxScore = -1;
while (true) {
int nextScore = scanner.nextInt();
if (nextScore < 0) {
break;
}
if (nextScore > 100) {
continue;
}
objects.add(nextScore);// 自动装箱
if (maxScore < nextScore) {
maxScore = nextScore;
}
}

char level;
for (int i = 0; i < objects.size(); i++) {
Object object = objects.elementAt(i);
int score = (Integer) object;// 自动拆箱
if (maxScore - score < 10) {
level = 'A';
} else if (maxScore - score < 20) {
level = 'B';
} else if (maxScore - score < 30) {
level = 'C';
} else {
level = 'D';
}
System.out.println("Student-" + i + " score is " + score + ", level is " + level);
}
}
}

关键字:static

当编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过 new 关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。有时候,希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。例如:所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

实例变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
Circle c1 = new Circle(2.0); // c1.radius=2.0
Circle c2 = new Circle(3.0); // c2.radius=3.0
}
}

class Circle {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

public double findArea() {
return Math.PI * radius * radius;
}
}
  • 上述代码中,c1 的 radius 独立于 c2 的 radius,存储在不同的空间。c1 中的 radius 变化不会影响 c2 的 radius,反之亦然。
  • 像 Circle 类中的变量 radius 这样的,叫**实例变量 (instance variable)**,它属于类的每一个对象,不能被同一个类的不同对象所共享。
  • 如果想让一个类的所有实例共享数据,就用类变量。类变量的定义,就需要用到 static 关键字。

static 关键字的使用:

  • static:静态的。

  • static 可以用来修饰:属性、方法、代码块、内部类。

  • static 修饰后的成员具备以下特点:

    • 随着类的加载而加载。
    • 优先于对象存在。
    • 修饰的成员,被所有对象所共享。
    • 访问权限允许时,可不创建对象,直接被类调用。
  • 使用 static 修饰属性:静态变量 (类变量/class variable)

    • 属性,按是否使用 static 修饰,分为:静态属性和非静态属性 (实例变量)。

      • 实例变量:当创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象的非静态属性时,不会导致其他对象中同样的属性值被修改。

      • 静态变量:当创建了类的多个对象,每个对象都共用同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改之后的值。 注意:实际操作时,虽然编译能过通过,但不应该通过类的实例对象来访问静态成员。

      • 静态变量随着类的加载而加载。可以通过 “类.静态变量”的方式进行调用。

      • 静态变量的加载要早于对象的创建。(实例变量在创建对象的过程中,或创建对象之后,才创建。)

      • 由于类只会加载一次,则静态变量在内存中也只会存在一份:保存在方法区的静态域中。

      • 类可以访问静态变量,但不能访问实例变量 (实例变量在对象产生时才生成),对象可以访问实例变量,也能访问静态变量 (不推荐)。

    • 静态变量举例:System.out,Math.PI。

  • 使用 static 修饰方法:静态方法 (类方法/class method)

    • 随着类的加载而加载,可以通过 “类.静态方法” 的方式进行调用。
    • 类可以访问静态方法,但不能访问非静态方法 (非静态方法在对象产生时才生成),对象可以访问非静态方法,也能访问静态方法 (不推荐)。
    • 静态方法中,只能调用静态属性或静态方法,它们的生命周期是一致的。非静态方法中,既可以调用非静态属性或非静态方法,也能调用静态属性或静态方法。
  • static 使用的注意点:

    • 在静态方法内,不能使用 this 关键字、super 关键字。(this 和 super 指向当前类对象和父类对象,需要创建实例对象后才有这些概念。)

      1
      2
      3
      4
      5
      public static void show() {
      // 省略的是Chiese.,而不是this.
      walk();// 等同于Chinese.walk();
      System.out.println("nation: " + nation);// 等同于System.out.println(Chinese.nation);
      }
    • static 修饰的方法不能被重写。

    • 关于静态属性和静态方法的使用,从生命周期的角度去理解。

  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    public class Test {
    public static void main(String[] args) {
    Chinese c1 = new Chinese();
    c1.name = "姚明";
    c1.age = 40;

    Chinese c2 = new Chinese();
    c2.name = "马龙";
    c2.age = 30;

    // 通过c1对象修改nation的值,c2对象也能获得
    // 实际操作时,虽然编译能过通过,但不应该通过类的实例对象来访问静态成员
    c1.nation = "CHN";
    System.out.println(c2.nation);

    // 对象实例调用非静态方法
    c1.eat();

    // 类调用静态方法
    Chinese.show();

    // 通过c1对象也能调用非静态方法
    // 实际操作时,虽然编译能过通过,但不应该通过类的实例对象来访问静态成员
    c1.show();
    }
    }

    class Chinese {
    String name;
    int age;
    static String nation;

    public void eat() {
    System.out.println("吃饭");

    // 调用非静态结构
    this.info();
    System.out.println("name: " + this.name);

    // 调用静态结构
    walk();
    System.out.println("nation: " + nation);
    }

    public void info() {
    System.out.println("name: " + name + ", age: " + age + ", nation: " + nation);
    }

    public static void show() {
    System.out.println("我是中国人");

    // 不能调用非静态的结构
    // eat();
    // name = "Tom";

    // 调用静态的结构
    walk();
    System.out.println("nation: " + nation);// 省略的是Chiese.,而不是this.,等同于System.out.println(Chinese.nation);
    }

    public static void walk() {
    System.out.println("走路");
    }
    }
  • 类变量和实例变量内存解析:

    image-20210228204850958

类属性、类方法的设计思想:

  • 类属性作为该类各个对象之间共享的变量,在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性,相应的方法设置为类方法。
  • 如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。
  • 类中的常量,通常也声明为 static 的。
  • 操作静态属性的方法,通常设置为 static 的。
  • 工具类中的方法,习惯上声明为 static 的。

main() 方法的语法

由于 java 虚拟机需要调用类的 main() 方法,所以该方法的访问权限必须是 public,又因为 java 虚拟机在执行 main() 方法时不必创建对象,所以该方法必须是 static 的,该方法接收一个 String 类型的数组参数,该数组中保存执行 java 命令时传递给所运行的类的参数。

又因为 main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

main() 方法的使用说明:

  • main() 方法是程序的入口。
  • main() 方法也是一个普通的静态方法,在执行某个类的 main() 方法之前,需要先加载这个类,这个过程是早于 main() 方法中首行的执行语句的。
  • main() 方法可以作为程序与控制台交互的方式之一,其他的还可以使用 Scanner 类。

命令行参数用法举例:

image-20210301141727952

类的成员之四:代码块 (或初始化块)

  • 代码块的作用:对 java 类或对象进行初始化。

  • 代码块的分类:一个类中代码块若有修饰符,则只能被 static 修饰,称为静态代码块 (static block),没有使用 static 修饰的,为非静态代码块。

  • 静态代码块:

    • 内部可以有输出语句。
    • 随着类的加载而执行,而且只执行一次。(不同于静态方法,静态方法必须在被类显示的调用后,才会执行方法内的语句。)
    • 作用:初始化类的信息。
    • 如果一个类定义了多个静态代码块,则按照声明的先后顺序来执行。一般情况下,不建议定义多个。
    • 静态代码块的执行要优先于非静态代码块的执行,与声明的先后顺序无关。
    • 静态代码块中,只能调用静态的属性、静态的方法,不能调用非静态的属性、非静态的方法。
  • 非静态代码块:

    • 内部可以有输出语句。
    • 随着对象的创建而执行。(不同于非静态方法,非静态方法必须在被类的对象显示的调用后,才会执行方法内的语句。)
    • 每创建一个对象,就执行一次非静态代码块。且先于构造器执行。
    • 作用:可以再创建对象时,对对象的属性等进行初始化。
    • 如果一个类定义了多个非静态代码块,则按照声明的先后顺序来执行。一般情况下,不建议定义多个。
    • 非静态代码块中,可以调用静态的属性、静态的方法,也可以调用非静态的属性、非静态的方法。
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    public class BlockTest {
    public static void main(String[] args) {
    System.out.println("*********类加载*********");
    String desc = Person.desc;
    System.out.println(desc);

    System.out.println("*********对象加载*********");
    Person person = new Person();
    }
    }

    class Person {
    // 属性
    String name;
    int age;
    static String desc = "我是一个人";

    // 静态代码块
    static {
    System.out.println("我是一个静态代码块-1");

    // 调用静态结构
    desc = "我是一个中国人";// 对类的静态属性重新赋值
    info();
    // 不能调用非静态结构
    // name = "Tom";
    // eat();
    }

    static {
    System.out.println("我是一个静态代码块-2");
    }

    // 非静态代码块
    {
    System.out.println("我是一个非静态代码块-1");

    // 调用静态结构
    desc = "我是一个中国人";
    info();
    // 调用非静态结构
    name = "Tom";
    eat();
    }

    {
    System.out.println("我是一个非静态代码块-2");
    }


    // 构造器
    public Person() {

    }

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

    // 静态方法
    public static void info() {
    System.out.println("我是一个静态方法");
    }

    // 非静态方法
    public void eat() {
    System.out.println("我是一个非静态方法");
    }

    @Override
    public String toString() {
    return "Person{" +
    "name='" + name + '\'' +
    ", age=" + age +
    '}';
    }
    }

代码块及构造器的执行顺序:

  • 由父及子,静态先行。

  • 注意:调用 main() 方法时,需要先加载类,这个过程是早于 main() 方法中的首行执行语句的。

  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    class Root {
    static {
    System.out.println("Root的静态初始化块");
    }

    {
    System.out.println("Root的普通初始化块");
    }

    public Root() {
    System.out.println("Root的无参数的构造器");
    }
    }

    class Mid extends Root {
    static {
    System.out.println("Mid的静态初始化块");
    }

    {
    System.out.println("Mid的普通初始化块");
    }

    public Mid() {
    System.out.println("Mid的无参数的构造器");
    }

    public Mid(String msg) {
    // 通过this调用同一类中重载的构造器
    this();
    System.out.println("Mid的带参数构造器,其参数值:" + msg);
    }
    }

    class Leaf extends Mid {
    static {
    System.out.println("Leaf的静态初始化块");
    }

    {
    System.out.println("Leaf的普通初始化块");
    }

    public Leaf() {
    // 通过super调用父类中有一个字符串参数的构造器
    super("尚硅谷");
    System.out.println("Leaf的构造器");
    }
    }

    public class LeafTest {
    public static void main(String[] args) {
    new Leaf();
    System.out.println();
    new Leaf();
    }
    }
    输出结果:
    Root的静态初始化块
    Mid的静态初始化块
    Leaf的静态初始化块
    Root的普通初始化块
    Root的无参数的构造器
    Mid的普通初始化块
    Mid的无参数的构造器
    Mid的带参数构造器,其参数值:尚硅谷
    Leaf的普通初始化块
    Leaf的构造器

    Root的普通初始化块
    Root的无参数的构造器
    Mid的普通初始化块
    Mid的无参数的构造器
    Mid的带参数构造器,其参数值:尚硅谷
    Leaf的普通初始化块
    Leaf的构造器
  • 实例二:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    class Father {
    static {
    System.out.println("11111111111");
    }

    {
    System.out.println("22222222222");
    }

    public Father() {
    System.out.println("33333333333");

    }

    // main方法是一个静态方法,执行某个类的main方法之前,要先加载这个类,此处是Father类
    public static void main(String[] args) {
    System.out.println("77777777777");
    System.out.println("************************");
    new Son();
    System.out.println("************************");

    new Son();
    System.out.println("************************");
    new Father();
    }
    }

    class Son extends Father {
    static {
    System.out.println("44444444444");
    }

    {
    System.out.println("55555555555");
    }

    public Son() {
    System.out.println("66666666666");
    }

    // main方法是一个静态方法,执行某个类的main方法之前,要先加载这个类,此处是先加载Son类
    public static void main(String[] args) { // 由父及子 静态先行
    System.out.println("77777777777");
    System.out.println("************************");
    new Son();
    System.out.println("************************");

    new Son();
    System.out.println("************************");
    new Father();
    }
    }

    public class Test {
    // main方法是一个静态方法,执行某个类的main方法之前,要先加载这个类,此处是先加载Test类
    public static void main(String[] args) {
    System.out.println("77777777777");
    System.out.println("************************");
    new Son();
    System.out.println("************************");

    new Son();
    System.out.println("************************");
    new Father();
    }
    }

    调用 Father 类的 main() 方法,要先加载 Father 类。输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    F: 11111111111
    F/m: 77777777777
    F/m: ************************
    S: 44444444444
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    F/m: ************************
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    F/m: ************************
    F: 22222222222
    F: 33333333333

    调用 Son 类的 main() 方法,要先加载 Son 类。输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    F: 11111111111
    S: 44444444444
    S/m: 77777777777
    S/m: ************************
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    S/m: ************************
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    S/m: ************************
    F: 22222222222
    F: 33333333333

    调用 Test 类的 main() 方法,要先加载 Test 类。输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    T/m: 77777777777
    T/m: ************************
    F: 11111111111
    S: 44444444444
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    T/m: ************************
    F: 22222222222
    F: 33333333333
    S: 55555555555
    S: 66666666666
    T/m: ************************
    F: 22222222222
    F: 33333333333

关键字:final

final:最终的。

  • final 可以用来修饰的结构:类、方法、变量 (属性是成员变量,是变量的其中一种。)。

  • final 用来修饰类:此类不能被其他类所继承。例如:String 类、System 类、StringBuffer 类。

    image-20210302095756170
  • final 用来修饰方法:此方法不能被子类重写。例如:Object 类中的 getClass()

    image-20210302095827686
  • final 用来修饰变量:此时的 “变量” 称为常量,名称大写,且只能被赋值一次。

    image-20210302100322123
    • final 修饰成员变量:必须在声明时或代码块中或在每个构造器中显式赋值,否则编译不通过。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      public class FinalTest {
      // 1.显式初始化:所有对象的这个常量值都是相同的,可以考虑直接显式初始化
      final int WIDTH = 0;

      // 2.代码块中初始化:如果涉及到调用方法,或赋值操作较多,可以考虑代码块中初始化
      final int HEIGHT;

      {
      HEIGHT = show();
      }

      // 3.构造器中初始化:如果涉及到调用方法,或赋值操作较多,可以考虑代码块中初始化
      final int LEFT;

      public FinalTest() {
      LEFT = show();
      }

      public int show() {
      return 0;
      }

      public static void main(String[] args) {
      FinalTest finalTest = new FinalTest();
      }
      }
    • final 修饰局部变量:修饰方法内局部变量时,表明该变量是一个常量,不能被修改;修饰形参时,表明此形参是一个常量,当调用此方法时,给常量形参赋一个实参,一旦赋值以后,就只能在方法体内使用此形参,但不能被修改。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public class FinalTest {

      public int show() {
      // 1.修饰方法内局部变量:常量,不能被再次更改。
      final int NUM = 10;
      return NUM;
      }

      // 2.修饰形参:当方法被调用时,传入的实参,不能被再次更改。
      public void show(final int num) {
      System.out.println(num);
      }

      public static void main(String[] args) {
      FinalTest finalTest = new FinalTest();
      finalTest.show(20);
      }
      }
  • static final 用来修饰属性:全局常量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class FinalTest {

    static final int WIDTH = 0;

    static final int HEIGHT;

    static {
    HEIGHT = show();
    }

    public FinalTest() {

    }

    public static int show() {
    return 0;
    }

    public static void main(String[] args) {
    FinalTest finalTest = new FinalTest();
    }
    }

面试题:

1
2
3
4
5
6
public class Something {
public int addOne(final int x) {
// return ++x;// 编译不通过
return x + 1;// 正常
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Something {
public void addOne(final Other o) {
// o = new Other();// 编译不通过
o.i++;// 正常
}

public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
}
}

class Other {
public int i;
}

抽象类和抽象方法

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

抽象类应用:抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。

abstract 关键字的使用:

  • abstract:抽象的。

  • abstract 可以用来修饰的结构:类、方法。

  • abstract 修饰类:抽象类。

    • 抽象类不能实例化。
    • 抽象类中一定有构造器 (抽象类本身不能使用构造器),便于子类实例化时调用。
    • 开发中,会提供抽象类的子类,让子类对象实例化,完成相关操作。
  • abstract 修饰方法:抽象方法。

    • 抽象方法只有方法声明,没有方法体,以分号结束。比如:public abstract void talk();
    • 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法。
    • 若子类重写了父类 (不仅包括直接父类,也包括间接父类) 中的所有的抽象方法后,此子类方可实例化;若子类没有重写父类中的所有的抽象方法,则次子类也是一个抽象类,需要使用 abstract 修饰。
  • abstract 不能修饰变量、代码块、构造器。

  • abstract 不能修饰私有方法、静态方法、final 的方法、final 的类。

抽象类的匿名子类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class Test {
public static void method(Student student) {

}

public static void method1(Person person) {
person.eat();
}

public static void main(String[] args) {
// 匿名对象
method(new Student());

// 1.创建非匿名类的非匿名的对象
Worker worker = new Worker();
method1(worker);

// 2.创建非匿名类的匿名的对象
method1(new Worker());

// 3.创建匿名子类的非匿名的对象:p
Person p = new Person() {
@Override
public void eat() {
// 重写方法体
System.out.println("...");
}
};
method1(p);

// 4.创建匿名子类的匿名对象
method1(new Person() {
@Override
public void eat() {
// 重写方法体
System.out.println(",,,");
}
});
}
}

abstract class Person {
public abstract void eat();
}

class Student {

}

class Worker extends Person {
@Override
public void eat() {
// 重写方法体
System.out.println("、、、");
}
}

接口 (interface)

一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,java 不支持多重继承。有了接口,就可以得到多重继承的效果。另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有 is - a 的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3 机、手机、数码相机、移动硬盘等都支持 USB 连接。

接口就是规范,定义的是一组规则,体现了现实世界中 “如果你是/要…则必须能…” 的思想。继承是一个 “是不是” 的关系,而接口实现则是 “能不能” 的关系。

接口的本质是契约,标准,规范,就像我们的法律一样,制定好后大家都要遵守。

接口的使用:

  • 接口使用 interface 定义。

  • java 中,接口和类是并列的两个结构,或者可以理解为一种特殊的类。从本质上讲,接口是一种特殊的抽象类。

  • 如何定义接口:定义接口中的成员

    • JDK 7 及以前:只能定义全局常量和抽象方法。

      • 全局常量:接口中的所有成员变量都默认是由 public static final 修饰的。书写时,可以省略,但含义不变,常量不能被更改。
      • 抽象方法:接口中的所有抽象方法都默认是由 public abstract 修饰的。
    • JDK 8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。

      • 静态方法:使用 static 关键字修饰,默认为 public 的。

        • 只能通过接口直接调用,并执行其方法体。
      • 默认方法:使用 default 关键字修饰,默认为 public 的。

        • 可以通过实现类的对象来调用,如果实现类重写了接口中的默认方法,调用时,执行的是重写后的方法。

        • 如果子类 (或实现类) 继承的父类和实现的接口中,声明了同名同参数的mo人方法,那么子类在没有重写此方法的情况下, 默认调用的是父类中的同名同参数的方法 — 类优先原则。如果重写了,调用子类重写的方法。

        • 如果实现类实现了多个接口,而多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,编译不通过 — 接口冲突。如果要避免接口冲突,则在实现类中,必须重写此方法。

          image-20210303132000712 image-20210303132422993
        • 在子类 (或实现类) 的方法中,使用 “super.方法名” 调用父类的方法,使用 “接口名.super.方法名” 调用接口中的方法。

      • 实例:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
          public interface InterfaceA {
        // 静态方法
        static void method1() {
        System.out.println("接口A:静态方法1");
        }

        // 默认方法
        default void method2() {
        System.out.println("接口A:默认方法2");
        }

        default void method3() {
        System.out.println("接口A:默认方法3");
        }

        default void method4() {
        System.out.println("接口A:默认方法4");
        }

        default void method5() {
        System.out.println("接口A:默认方法5");
        }
        }
        1
        2
        3
        4
        5
          public interface InterfaceB {
        default void method5() {
        System.out.println("接口B:默认方法5");
        }
        }
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
          public class SubClassTest {
        public static void main(String[] args) {
        // 1.静态方法
        InterfaceA.method1();

        SubClass subClass = new SubClass();
        // 2.默认方法
        subClass.method2();
        // 3.重写的默认方法
        subClass.method3();
        // 4.调用的是父类中的method4()
        subClass.method4();
        }
        }

        class SuperClass {
        public void method4() {
        System.out.println("父类:方法4");
        }
        }

        class SubClass extends SuperClass implements InterfaceA, InterfaceB {
        // 重写接口InterfaceA中的method3()
        @Override
        public void method3() {
        System.out.println("实现类:方法3");
        }

        // 重写了父类SuperClass的method4()
        @Override
        public void method4() {
        System.out.println("实现类:方法4");
        }

        // InterfaceA和InterfaceB声明了同名同参的method5(),SubClass中必须重写此方法,否则接口冲突,编译不通过
        // 如果继承的父类SuperClass中也声明了同名同参的method5(),则不会出现接口冲突
        @Override
        public void method5() {
        System.out.println("实现类:方法5");
        }

        public void myMethod() {
        method2();// InterfaceA的method2()

        method3();// 重写的InterfaceA的method3()
        InterfaceA.super.method3();// InterfaceA的method3()

        method4();// 重写的SuperClass的method4()
        super.method4();// 父类SuperClass的method4()

        InterfaceA.super.method5();// InterfaceA的method5()
        InterfaceB.super.method5();// InterfaceB的method5()
        }
        }
  • 接口中不能定义构造器,意味着接口不可以实例化。

  • java 开发中,接口都通过让类去实现的方式 (implements) 来使用 (面向接口编程)。

  • 如果实现类覆盖了接口中 (包括直接接口和间接接口) 的所有抽象方法,则此实现类可以实例化。如果实现类没有覆盖接口 (包括直接接口和间接接口) 中所有的抽象方法,则此实现类仍为一个抽象类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    interface MyInterface{
    String s = "MyInterface";
    public void absM1();
    }

    interface SubInterface extends MyInterface{
    public void absM2();
    }

    // 实现类SubAdapter必须给出接口SubInterface以及父接口MyInterface中所有方法的实现。
    // 否则,SubAdapter仍需声明为abstract的。
    public class SubAdapter implements SubInterface{
    public void absM1(){
    System.out.println("absM1");
    }

    public void absM2(){
    System.out.println("absM2");
    }
    }
  • java 类可以实现多个接口,弥补了 java 单继承性的局限性。

    • 格式:class SubClass extends SuperClass implements InterfaceA, InterfaceB, InterfaceC {}
  • 接口与接口之间可以继承,而且可以多继承。

  • 与继承关系类似,接口与实现类之间体现了多态性。

  • 接口,实际上可以看作是一种规范。

  • 实例:

    image-20210303092504018
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    public class InterfaceTest {
    public static void main(String[] args) {
    System.out.println(Flyable.MAX_SPEED);
    System.out.println(Flyable.MIN_SPEED);

    Plane plane = new Plane();
    plane.fly();
    }
    }

    interface Flyable {
    // 全局常量,可以省略 public static final
    int MAX_SPEED = 7900;// 第一宇宙速度
    int MIN_SPEED = 1;

    // 抽象方法,可以省略 public abstract
    public abstract void fly();

    void stop();
    }

    interface Attackable {
    void attack();
    }

    // 全部实现接口中的方法,可以实例化
    class Plane implements Flyable {

    @Override
    public void fly() {
    System.out.println("飞机起飞");
    }

    @Override
    public void stop() {
    System.out.println("飞机降落");
    }
    }

    // 未全部实现接口中的方法,仍是一个抽象类
    abstract class Kite implements Flyable {
    @Override
    public void fly() {
    System.out.println("风筝在飞");
    }
    }

    // 实现多个接口
    class Bullet implements Flyable, Attackable {

    @Override
    public void fly() {
    System.out.println("子弹起飞");
    }

    @Override
    public void stop() {
    System.out.println("子弹停止");
    }

    @Override
    public void attack() {
    System.out.println("子弹具有攻击性");
    }
    }

面试题:

  • 抽象类与接口有哪些异同?

    image-20210303100025773

    接口能继承接口;

    抽象类能继承接口 (如不完全实现接口方法的类,还是抽象类);

    抽象类能继承非抽象类 (如抽象类的父类 Object)。

  • 排错:

    因为接口 A 和父类 B 是并列的,所以需要明确变量 x 的所属,如果 A 是 B 的父类,那么在 C 中就近原则,x 会认为是 B 的属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    interface A {
    int x = 0;
    int x1 = 2;
    }

    class B {
    int x = 1;
    int x2 = 3;
    }

    class C extends B implements A {
    public void pX() {
    System.out.println(x);// error: Reference to 'x' is ambiguous, both 'B.x' and 'A.x' match
    // System.out.println(A.x);// 0
    // System.out.println(super.x);// 1
    System.out.println(x1);// 2
    System.out.println(x2);// 3
    }

    public static void main(String[] args) {
    new C().pX();
    }
    }

    接口中的所有成员变量都默认是 public static final 的,不能在实现类中被重写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    interface Playable {
    void play();
    }

    interface Bounceable {
    void play();
    }

    interface Rollable extends Playable, Bounceable {
    Ball BALL = new Ball("PingPang");
    }

    class Ball implements Rollable {
    private String name;

    public String getName() {
    return name;
    }

    public Ball(String name) {
    this.name = name;
    }

    // play()方法被认为是即重写了接口Playable,又重写了接口Bounceable
    @Override
    public void play() {
    BALL = new Ball("Football");// error: Cannot assign a value to final variable 'BALL'
    System.out.println(BALL.getName());
    }
    }

接口匿名实现类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class InterfaceTest {
public static void main(String[] args) {
Computer computer = new Computer();

// 1.创建接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
computer.transferData(flash);

// 2.创建接口的非匿名实现类的匿名对象
computer.transferData(new Printer());

// 3.创建接口的匿名实现类的非匿名对象
USB phone = new USB() {

@Override
public void start() {
System.out.println("手机开始工作");
}

@Override
public void stop() {
System.out.println("手机停止工作");
}
};
computer.transferData(phone);

// 4.创建接口的匿名实现类的匿名对象
computer.transferData(new USB() {
@Override
public void start() {
System.out.println("mp3开始工作");
}

@Override
public void stop() {
System.out.println("mp3停止工作");
}
});
}
}

class Computer {
public void transferData(USB usb) {
usb.start();
transferDetails();
usb.stop();
}

private void transferDetails() {
System.out.println("具体传输数据的细节");
}
}

interface USB {
void start();

void stop();
}

class Flash implements USB {

@Override
public void start() {
System.out.println("U盘开启工作");
}

@Override
public void stop() {
System.out.println("U盘停止工作");
}
}

class Printer implements USB {

@Override
public void start() {
System.out.println("打印机开启工作");
}

@Override
public void stop() {
System.out.println("打印机停止工作");
}
}

类的成员之五:内部类

当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。

  • 在 java 中,允许一个类 A 声明在另一个类 B 的内部,则类 A 称为内部类,类 B 称为外部类。

  • Inner class一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。Inner class 的名字不能与包含它的外部类类名相同。

  • 内部类的分类:成员内部类 (静态的、非静态的) vs 局部内部类 (代码块内、构造器内、方法内)

  • 成员内部类:

    • 一方面,作为外部类的成员:
      • 调用外部类的结构,注意生命周期,如静态成员内部类不能调用外部类非静态的方法。
      • 可以被 static 修饰,但此时就不能再使用外层类的非 static 的成员变量。注意,外部类不能被 static 修饰。
      • 可以被 private、protected、缺省和 public 四种权限修饰符修饰。注意,外部类不能被 private 和 protected 修饰。
    • 另一方面,作为一个类:
      • 类内可以定义属性、方法、构造器、代码块、内部类等。
      • 可以被 final 修饰,表示此类不能被继承,如果不使用 final,就可以被继承。
      • 可以被 abstract 修饰,表示此类不能被实例化,可以被其它的内部类继承。
      • 编译以后生成 OuterClass$InnerClass.class 字节码文件 (也适用于局部内部类)。
    • 非 static 的成员内部类中的成员不能声明为 static 的,只有在外部类或 static 的成员内部类中才可声明 static 成员。
    • 外部类访问成员内部类的成员,需要 “内部类.成员” 或 “内部类对象.成员” 的方式。
    • 成员内部类可以直接使用外部类的所有成员,包括私有的数据。
    • 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的。
  • 局部内部类:

    • 局部内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的 .class 文件,但是前面冠以外部类的类名和 $ 符号,以及数字编号。
    • 只能在声明它的方法或代码块中使用,而且是先声明后使用,除此之外的任何地方都不能使用该类。
    • 局部内部类的对象可以通过外部方法的返回值返回使用,返回值类型只能是局部内部类的父类或父接口类型。
    • 局部内部类可以使用外部类的成员,包括私有的。
    • 局部内部类可以使用外部方法的局部变量,但是必须是 final 的,final 可以省略 (jdk 8 及之后),但这个局部变量赋值后不能有再次修改操作,否则编译不通过。这是因为局部内部类和局部变量的声明周期不同所致。
    • 局部内部类和局部变量地位类似,不能使用 public,缺省,protected 和 private 修饰。
    • 局部内部类不能使用static修饰,因此也不能包含静态成员。
  • 关注如下的 3 个问题:

    • 如何实例化成员内部类的对象?

      • 静态成员内部类:外部类.静态内部类 变量名 = new 外部类.静态内部类();
      • 非静态成员内部类:外部类.非静态内部类 变量名 = new 外部类().new 非静态内部类();
    • 如何在成员内部类中区分调用外部类的结构?

      • 静态成员内部类,参考:

        1
        2
        3
        4
        5
        public void show(int age) {
        System.out.println("形参:" + age);
        System.out.println("静态成员内部类的静态属性:" + Brain.age);
        System.out.println("外部类的静态属性:" + Person.age);
        }
      • 非静态成员内部类,参考:

        1
        2
        3
        4
        5
        public void show(String name) {
        System.out.println("形参:" + name);
        System.out.println("非静态成员内部类的非静态属性:" + this.name);// 非静态成员内部类,不能定义static的变量
        System.out.println("外部类的非静态属性:" + Person.this.name);
        }
    • 开发者局部内部类的使用?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      public class InnerClassTest1 {
      // 这种局部内部类,开发中很少见
      public void method() {
      class AA {

      }
      }

      // 返回一个实现类Comparable接口的类的对象
      public Comparable getComparable() {
      // 创建一个实现了Comparable接口的类:局部内部类

      // 方式一:创建Comparable接口的非匿名实现类的匿名对象
      /*class MyComparable implements Comparable {
      @Override
      public int compareTo(Object o) {
      return 0;
      }
      }

      return new MyComparable();*/

      // 方式二:创建Comparable接口的匿名实现类的匿名对象
      return new Comparable() {
      @Override
      public int compareTo(Object o) {
      return 0;
      }
      };
      }
      }
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    public class InnerClassTest {
    public static void main(String[] args) {
    // 1.创建Brain实例---静态的成员内部类
    Person.Brain brain = new Person.Brain();
    brain.think();
    brain.show(8);

    // 2.创建Hand实例---非静态的成员内部类
    Person.Hand hand = new Person().new Hand();
    hand.grasp();
    hand.show("外来手");
    }
    }

    class Person {
    String name = "小明";
    static int age = 8;

    // 静态成员内部类
    static class Brain {
    static int age = 8;

    public Brain() {

    }

    public void think() {
    System.out.println("大脑想东西");
    }

    public void show(int age) {
    System.out.println("形参:" + age);
    System.out.println("静态成员内部类的静态属性:" + Brain.age);
    System.out.println("外部类的静态属性:" + Person.age);
    }
    }

    // 非静态成员内部类
    class Hand {
    String name = "内部手";

    public Hand() {

    }

    public void grasp() {
    System.out.println("手抓东西");
    // 调用Person外部类的方法
    Person.this.eat();// 等价于eat(),注意方法的生命周期
    }

    public void show(String name) {
    System.out.println("形参:" + name);
    System.out.println("非静态成员内部类的非静态属性:" + this.name);
    System.out.println("外部类的非静态属性:" + Person.this.name);
    }
    }

    static {
    // 静态代码块内局部内部类
    class AA {

    }
    }

    {
    // 非静态代码块内局部内部类
    class BB {

    }
    }

    public Person() {
    // 构造器内局部内部类
    class CC {

    }
    }

    public static void method1() {
    // 静态方法内局部内部类
    class DD {

    }
    }

    public void method() {
    // 非静态方法内局部内部类
    class EE {

    }
    }

    public void eat() {

    }
    }

匿名内部类:

  • 匿名内部类不能定义任何静态成员、方法和类,只能创建匿名内部类的一个实例。一个匿名内部类一定是在 new 的后面,用其隐含实现一个接口或实现一个类。

  • 格式:

    image-20210303172303512
  • 特点:

    • 匿名内部类必须继承父类或实现接口。
    • 匿名内部类只能有一个对象。
    • 匿名内部类对象只能使用多态形式引用
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    interface Product {
    public double getPrice();

    public String getName();
    }

    public class AnonymousTest {
    public void test(Product p) {
    System.out.println("购买了一个" + p.getName() + ",花掉了" + p.getPrice());
    }

    public static void main(String[] args) {
    AnonymousTest ta = new AnonymousTest();
    // 调用test方法时,需要传入一个Product参数,
    // 此处传入其匿名实现类的实例
    ta.test(new Product() {
    @Override
    public double getPrice() {
    return 567.8;
    }

    @Override
    public String getName() {
    return "AGP显卡";
    }
    });
    }
    }

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public Test() {
Inner s1 = new Inner();
s1.a = 10;
Inner s2 = new Inner();
s2.a = 20;
Test.Inner s3 = new Test.Inner();
System.out.println(s3.a);
}

class Inner {
public int a = 5;
}

public static void main(String[] args) {
Test t = new Test();
Inner r = t.new Inner();
System.out.println(r.a);
}
}
输出结果:
5
5

本文参考

https://www.gulixueyuan.com/goods/show/203?targetId=309&preview=0

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

settings.xml

settings.xml是Maven的全局配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
<?xml version="1.0" encoding="UTF-8"?>

<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<!--
| 官方文档:https://maven.apache.org/settings.html
|
| maven提供以下两种level的配置:
|
| 1. User Level. 当前用户独享的配置,通常在${user.home}/.m2/settings.xml目录下。
| 可在CLI命令行中通过以下参数设置:-s /path/to/user/settings.xml
|
| 2. Global Level. 同一台计算机上的所有maven用户共享的全局配置。通常在${maven.home}/conf/settings.xml目录下。
| 可在CLI命令行中通过以下参数设置:-gs /path/to/global/settings.xml
|
| 备注:
| 优先级:User Level > Global Level
| 默认情况,${user.home}/.m2目录下没有settings.xml文件,需手动复制${maven.home}/conf/settings.xml。
|-->

<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.conf}/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<!-- 本地仓库路径,默认值:${user.home}/.m2/repository -->
<!-- localRepositor y
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<localRepository>D:\Program Files\Maven\apache-maven-3.6.3-maven-repository</localRepository>

<!-- 当maven需要输入值的时候,是否交由用户输入,默认为true;false情况下maven将根据使用配置信息进行填充。 -->
<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->

<!-- 是否支持联网进行artifact下载、部署等操作,默认false。 -->
<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->

<!--
| 搜索插件时,如果groupId没有显式提供时,则以此处配置的groupId为默认值,
| 可以简单理解为默认导入这些groupId下的所有artifact(需要时才下载)。
| 默认情况下该列表包含了:org.apache.maven.plugins和org.codehaus.mojo。
|
| 查看插件信息:
| mvn help:describe -Dplugin=org.apache.maven.plugins:maven-compiler-plugin:3.5.1 -Ddetail
|-->
<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
| plugin 的 groupId
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>

<!-- 用来配置不同的代理,多代理profiles可以应对笔记本或移动设备的工作环境:通过简单的设置profile id就可以很容易的更换整个代理配置。 -->
<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
| 代理元素包含配置代理时需要的信息
<proxy>
| 代理的唯一定义符,用来区分不同的代理元素
<id>optional</id>
| 该代理是否是激活的那个。true则激活代理。当我们声明了一组代理,而某个时候只需要激活一个代理的时候,该元素就可以派上用处。
<active>true</active>
| 代理的协议
<protocol>http</protocol>
| 代理服务器认证的登录名
<username>proxyuser</username>
| 代理服务器认证登录密码
<password>proxypass</password>
| 代理的主机名
<host>proxy.host.net</host>
| 代理的端口
<port>80</port>
| 不该被代理的主机名列表。该列表的分隔符由代理服务器指定;例子中使用了竖线分隔符,使用逗号分隔也很常见。
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>

<!-- 进行远程服务器访问时所需的授权配置信息。通过系统唯一的server-id进行唯一关联。 -->
<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
| 方式一:使用用户名和密码
<server>
| 当前server的id,该id与distributionManagement中repository元素的id相匹配。
<id>deploymentRepo</id>
| 鉴权用户名
<username>repouser</username>
| 鉴权密码
<password>repopwd</password>
</server>
-->

<!-- Another sample, using keys to authenticate.
| 方式二:使用私钥
<server>
<id>siteServer</id>
| 鉴权时使用的私钥位置,默认是/home/hudson/.ssh/id_dsa。
<privateKey>/path/to/private/key</privateKey>
| 鉴权时使用的私钥密码,非必要,非必要时留空。
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->

<!-- 实例:对应pom.xml文件中配置的id为ChemAxon Public Repository的仓库。 -->
<server>
<id>ChemAxon Public Repository</id>
<username>huxiongfeng95@gmail.com</username>
<password>AKCp5dL3HsJftZjXR4wLS7UMnJvQL7oarx8sad8Wh21UV7xQUMmNcZ7TMEHaBVoSrM8jAv48Q</password>
</server>
</servers>

<!--
| 从远程仓库下载artifacts时,用于替代指定远程仓库的镜像服务器配置;
| 例如当无法连接上国外的仓库时,可以指定连接到国内的镜像服务器;
| 私服的配置推荐用profile配置而不是mirror。
|-->
<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!--
| mirrors匹配顺序:
| 多个mirror优先级:按照id字母顺序进行排列,即与编写的顺序无关。
| 在第一个mirror找不到artifact,不会继续查找下一个镜像。
| 只有当前一个mirror无法链接的时候,才会尝试链接下一个镜像,类似容灾备份。
|-->

<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->

<!-- maven中央仓库的aliyun镜像,maven中央仓库的id为central。 -->
<mirror>
<!-- 当前镜像的唯一标识符,id用来区分不同的mirror元素,同时会套用使用server中id相同授权配置链接到镜像。 -->
<id>alimaven</id>
<!-- 镜像名称,无特殊作用,可视为简述。 -->
<name>aliyun maven</name>
<!-- 镜像地址 -->
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<!-- 被镜像的服务器的id,必须与repository节点设置的id一致。但是"This must not match the mirror id"。
| mirrorOf 的配置语法:
| * = 匹配所有远程仓库。这样所有pom中定义的仓库都不生效。
| external:* = 匹配除localhost、使用file://协议外的所有远程仓库。
| repo1,repo2 = 匹配仓库repo1和repo2。
| *,!repo1 = 匹配所有远程仓库,repo1除外。
|-->
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>

<activation>
<jdk>1.4</jdk>
</activation>

<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->

<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>

<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>

<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
</profiles>

<!--
| 手动激活profiles的列表,按照profile被应用的顺序定义activeProfile。
| 任何activeProfile,不论环境设置如何,其对应的profile都会被激活,maven会忽略无效(找不到)的profile。
|-->
<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
</settings>

关于 profiles 节点的详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!--
| 构建方法的配置清单,maven将根据不同环境参数来使用这些构建配置。
| settings.xml中的profile元素是pom.xml中profile元素的裁剪版本。
| settings.xml负责的是整体的构建过程,pom.xml负责单独的项目对象构建过程。
| settings.xml只包含了id,activation,repositories,pluginRepositories和properties元素。
|
| 如果settings.xml中的profile被激活,它的值会覆盖任何其它定义在pom.xml中或profile.xml中的相同id的profile。
|
| 查看当前激活的profile:
| mvn help:active-profiles
|-->
<profiles>
<profile>

<!-- 该配置的唯一标识符 -->
<id>profile_id</id>

<!--
| profile的激活条件配置。
| 除此之外的其他激活方式:
| 1. 通过settings.xml文件中的activeProfile元素进行指定激活。
| 2. 在命令行,使用-P标记和逗号分隔的列表来显式的激活,如:mvn clean package -P myProfile
|-->
<activation>
<!-- 是否默认激活 -->
<activeByDefault>false</activeByDefault>

<!-- 内建的java版本检测,匹配规则:https://maven.apache.org/enforcer/enforcer-rules/versionRanges.html -->
<jdk>9.9</jdk>

<!-- 内建操作系统属性检测, 配置规则:https://maven.apache.org/enforcer/enforcer-rules/requireOS.html -->
<os>
<!-- 操作系统 -->
<name>Windows XP</name>
<!-- 操作系统家族 -->
<family>Windows</family>
<!-- 操作系统 -->
<arch>x86</arch>
<!-- 操作系统版本 -->
<version>5.1.2600</version>
</os>

<!--
| 如果maven检测到某一个属性(其值可以在POM中通过${名称}引用),并且其拥有对应的名称和值,Profile就会被激活。
| 如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段。
|-->
<property>
<!-- 属性名 -->
<name>mavenVersion</name>
<!-- 属性值 -->
<value>2.0.3</value>
</property>

<!-- 根据文件存在/不存在激活profile -->
<file>
<!-- 如果指定的文件存在,则激活profile。 -->
<exists>/path/to/active_on_exists</exists>
<!-- 如果指定的文件不存在,则激活profile。 -->
<missing>/path/to/active_on_missing</missing>
</file>
</activation>

<!-- 扩展属性设置。扩展属性可以在POM中的任何地方通过${扩展属性名}进行引用。
|
| 属性引用方式(包括扩展属性,共5种属性可以引用):
|
| env.x:引用shell环境变量,例如,"env.PATH"指代了$path环境变量(在Linux/Windows上是%PATH%)。
| project.x:引用pom.xml(根元素是project)中xml元素内容。例如${project.artifactId}可以获取pom.xml中设置的<artifactId />元素的内容。
| settings.x:引用setting.xml(根元素是setting)中xml元素内容,例如${settings.offline}。
| Java System Properties:所有可通过java.lang.System.getProperties()访问的属性都能在通过${property_name}访问,例如${java.home}。
| x:在<properties/>或者外部文件中设置的属性,都可以${someVar}的形式使用。
|
|-->
<properties>
<!-- 在当前profile被激活时,${profile.property}就可以被访问到了。 -->
<profile.property>this.property.is.accessible.when.current.profile.actived</profile.property>
</properties>

<!-- 远程仓库列表,settings.xml中的repositories不被直接支持,需要在profiles中配置。 -->
<repositories>
<!--
| releases vs snapshots
| maven针对releases和snapshots有不同的处理策略,POM可以在每个单独的仓库中,为每种类型的artifact采取不同的策略。
| 例如:
| 开发环境使用snapshots模式实时获取最新的快照版本进行构建
| 生成环境使用releases模式获取稳定版本进行构建
| 参见repositories/repository/releases元素。
|-->

<!--
| 依赖包不更新问题:
| 1. maven在下载依赖失败后会生成一个.lastUpdated为后缀的文件。如果这个文件存在,那么即使换一个有资源的仓库后,
| maven依然不会去下载新资源。可以通过-U参数进行强制更新、手动删除.lastUpdated 文件:
| find . -type f -name "*.lastUpdated" -exec echo {}" found and deleted" \; -exec rm -f {} \;
|
| 2. updatePolicy设置更新频率不对,导致没有触发maven检查本地artifact与远程artifact是否一致。
|-->
<repository>
<!-- 远程仓库唯一标识 -->
<id>maven_repository_id</id>
<!-- 远程仓库名称 -->
<name>maven_repository_name</name>
<!-- 远程仓库URL,按protocol://hostname/path形式。 -->
<url>http://host/maven</url>
<!--
| 用于定位和排序artifact的仓库布局类型-可以是default(默认)或者legacy(遗留)。
| Maven2为其仓库提供了一个默认的布局;然而,Maven1.x有一种不同的布局。
| 我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。
| -->
<layout>default</layout>
<!-- 如何处理远程仓库里发布版本的下载 -->
<releases>
<!-- 是否允许该仓库为artifact提供releases下载功能 -->
<enabled>true</enabled>
<!--
| 每次执行构建命令时,Maven会比较本地POM和远程POM的时间戳,该元素指定比较的频率。
| 有效选项是:
| always :每次构建都检查
| daily :默认,距上次构建检查时间超过一天
| interval: x :距上次构建检查超过x分钟
| never从不 :从不
|
| 重要:
| 设置为daily时,如果artifact一天更新了几次,在一天之内进行构建,也不会从仓库中重新获取最新版本。
|-->
<updatePolicy>always</updatePolicy>
<!-- 当maven验证artifact校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。 -->
<checksumPolicy>warn</checksumPolicy>
</releases>
<!-- 如何处理远程仓库里快照版本的下载 -->
<snapshots>
<!-- 是否允许该仓库为artifact提供snapshots下载功能 -->
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
</repository>

<!--
| 国内可用的maven仓库地址(updated @ 2019-02-08):
| http://maven.aliyun.com/nexus/content/groups/public
| http://maven.wso2.org/nexus/content/groups/public/
| http://jcenter.bintray.com/
| http://maven.springframework.org/release/
| http://repository.jboss.com/maven2/
| http://uk.maven.org/maven2/
| http://repo1.maven.org/maven2/
| http://maven.springframework.org/milestone
| http://maven.jeecg.org/nexus/content/repositories/
| http://repo.maven.apache.org/maven2
| http://repo.spring.io/release/
| http://repo.spring.io/snapshot/
| http://mavensync.zkoss.org/maven2/
| https://repository.apache.org/content/groups/public/
| https://repository.jboss.org/nexus/content/repositories/releases/
|-->
</repositories>

<!--
| maven插件的远程仓库配置。maven插件实际上是一种特殊类型的artifact。
| 插件仓库独立于artifact仓库。pluginRepositories元素的结构和repositories元素的结构类似。
|-->
<!--
<pluginRepositories>
<id />
<name />
<url />
<layout />
<pluginRepository>
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
</pluginRepository>
</pluginRepositories>
-->
</profile>
</profiles>

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<!-- 父项目的坐标。如果项目中没有规定某个元素的值,那么父项目中的对应值即为项目的默认值。
坐标包括groupID,artifactID和version。 -->
<parent>
<!-- 被继承的父项目的构件标识符 -->
<artifactId />
<!-- 被继承的父项目的全球唯一标识符 -->
<groupId />
<!-- 被继承的父项目的版本 -->
<version />
<!-- 父项目的pom.xml文件的相对路径。相对路径允许你选择一个不同的路径。默认值是:../pom.xml。
Maven首先在构建当前项目的地方寻找父项目的pom,其次在文件系统的这个位置(relativePath位置),
然后在本地仓库,最后在远程仓库寻找父项目的pom。 -->
<relativePath />
</parent>

<!-- 声明项目描述符遵循哪一个POM模型版本。模型本身的版本很少改变,虽然如此,但它仍然是必不可少的,
这是为了当Maven引入了新的特性或者其他模型变更的时候,确保稳定性。 -->
<modelVersion>4.0.0</modelVersion>

<!-- 项目的全球唯一标识符,通常使用全限定的包名区分该项目和其他项目。
并且构建时生成的路径也是由此生成,如com.mycompany.app生成的相对路径为:/com/mycompany/app -->
<groupId>asia.banseon</groupId>

<!-- 构件的标识符,它和groupID一起唯一标识一个构件。换句话说,你不能有两个不同的项目拥有同样的artifactID和groupID;
在某个特定的groupID下,artifactID也必须是唯一的。
构件是项目产生的或使用的一个东西,Maven为项目产生的构件包括:JARs,源码,二进制发布和WARs等。 -->
<artifactId>banseon-maven2</artifactId>

<!-- 项目产生的构件类型,例如jar、war、ear、pom。插件可以创建他们自己的构件类型,所以前面列的不是全部构件类型 -->
<packaging>jar</packaging>

<!-- 项目当前版本,格式为:主版本.次版本.增量版本-限定版本号 -->
<version>1.0-SNAPSHOT</version>

<!-- 项目的名称,Maven产生的文档用 -->
<name>banseon-maven</name>

<!-- 项目主页的URL,Maven产生的文档用 -->
<url>http://www.baidu.com/banseon</url>

<!-- 项目的详细描述,Maven产生的文档用。当这个元素能够用HTML格式描述时(例如,CDATA中的文本会被解析器忽略,
就可以包含HTML标签),不鼓励使用纯文本描述。如果你需要修改产生的web站点的索引页面,
你应该修改你自己的索引页文件,而不是调整这里的文档。 -->
<description>A maven project to study maven.</description>

<!-- 描述了这个项目构建环境中的前提条件。 -->
<prerequisites>
<!-- 构建该项目或使用该插件所需要的Maven的最低版本 -->
<maven />
</prerequisites>

<!-- 项目的问题管理系统(Bugzilla,Jira,Scarab,或任何你喜欢的问题管理系统)的名称和URL,本例为jira -->
<issueManagement>
<!-- 问题管理系统(例如jira)的名字 -->
<system>jira</system>
<!-- 该项目使用的问题管理系统的URL -->
<url>http://jira.xxxx.com/xxxx</url>
</issueManagement>

<!-- 项目持续集成信息 -->
<ciManagement>
<!-- 持续集成系统的名字,例如continuum -->
<system />
<!-- 该项目使用的持续集成系统的URL(如果持续集成系统有web接口的话) -->
<url />
<!-- 构建完成时,需要通知的开发者/用户的配置项。包括被通知者信息和通知条件(错误,失败,成功,警告) -->
<notifiers>
<!-- 配置一种方式,当构建中断时,以该方式通知用户/开发者 -->
<notifier>
<!-- 传送通知的途径 -->
<type />
<!-- 发生错误时是否通知 -->
<sendOnError />
<!-- 构建失败时是否通知 -->
<sendOnFailure />
<!-- 构建成功时是否通知 -->
<sendOnSuccess />
<!-- 发生警告时是否通知 -->
<sendOnWarning />
<!-- 不赞成使用。通知发送到哪里 -->
<address />
<!-- 扩展配置项 -->
<configuration />
</notifier>
</notifiers>
</ciManagement>

<!-- 项目创建年份,4位数字。当产生版权信息时需要使用这个值。 -->
<inceptionYear />

<!-- 项目相关邮件列表信息 -->
<mailingLists>
<!-- 该元素描述了项目相关的所有邮件列表。自动产生的网站引用这些信息。 -->
<mailingList>
<!-- 邮件的名称 -->
<name>Demo</name>
<!-- 发送邮件的地址或链接,如果是邮件地址,创建文档时,mailto:链接会被自动创建 -->
<post>Demo@126.com</post>
<!-- 订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto:链接会被自动创建 -->
<subscribe>Demo@126.com</subscribe>
<!-- 取消订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto:链接会被自动创建 -->
<unsubscribe>Demo@126.com</unsubscribe>
<!-- 你可以浏览邮件信息的URL -->
<archive>http://localhost:8080/demo/dev/</archive>
</mailingList>
</mailingLists>

<!-- 项目开发者列表 -->
<developers>
<!-- 某个项目开发者的信息 -->
<developer>
<!-- SCM里项目开发者的唯一标识符 -->
<id>HELLO WORLD</id>
<!-- 项目开发者的全名 -->
<name>youname</name>
<!-- 项目开发者的email -->
<email>youname@qq.com</email>
<!-- 项目开发者的主页的URL -->
<url />
<!-- 项目开发者在项目中扮演的角色,角色元素描述了各种角色 -->
<roles>
<role>Project Manager</role>
<role>Architect</role>
</roles>
<!-- 项目开发者所属组织 -->
<organization>demo</organization>
<!-- 项目开发者所属组织的URL -->
<organizationUrl>http://www.xxx.com/</organizationUrl>
<!-- 项目开发者属性,如即时消息如何处理等 -->
<properties>
<dept>No</dept>
</properties>
<!-- 项目开发者所在时区, -11到12范围内的整数。 -->
<timezone>+8</timezone>
</developer>
</developers>

<!-- 项目的其他贡献者列表 -->
<contributors>
<!-- 项目的其他贡献者。参见developers/developer元素 -->
<contributor>
<name />
<email />
<url />
<organization />
<organizationUrl />
<roles />
<timezone />
<properties />
</contributor>
</contributors>

<!-- 该元素描述了项目所有license列表。应该只列出该项目的license列表,不要列出依赖项目的license列表。
如果列出多个license,用户可以选择它们中的一个而不是接受所有license。 -->
<licenses>
<!-- 描述了项目的license,用于生成项目的web站点的license页面,其他一些报表和validation也会用到该元素。 -->
<license>
<!-- license用于法律上的名称 -->
<name>Apache 2</name>
<!-- 官方的license正文页面的URL -->
<url>http://www.xxxx.com/LICENSE-2.0.txt</url>
<!-- 项目分发的主要方式:repo,可以从Maven库下载manual,用户必须手动下载和安装依赖 -->
<distribution>repo</distribution>
<!-- 关于license的补充信息 -->
<comments>A business-friendly OSS license</comments>
</license>
</licenses>

<!-- SCM(Source Control Management)标签允许你配置你的代码库,供Maven web站点和其它插件使用。 -->
<scm>
<!-- SCM的URL,该URL描述了版本库和如何连接到版本库。欲知详情,请看SCMs提供的URL格式和列表。该连接只读。 -->
<connection>
scm:svn:http://svn.xxxx.com/maven/xxxxx-maven2-trunk(dao-trunk)
</connection>
<!-- 给开发者使用的,类似connection元素。即该连接不仅仅只读。 -->
<developerConnection>
scm:svn:http://svn.xxxx.com/maven/dao-trunk
</developerConnection>
<!-- 当前代码的标签,在开发阶段默认为HEAD -->
<tag />
<!-- 指向项目的可浏览SCM库(例如ViewVC或者Fisheye)的URL。 -->
<url>http://svn.xxxxx.com/</url>
</scm>

<!-- 描述项目所属组织的各种属性。Maven产生的文档用。 -->
<organization>
<!-- 组织的全名 -->
<name>demo</name>
<!-- 组织主页的URL -->
<url>http://www.xxxxxx.com/</url>
</organization>

<!-- 构建项目需要的信息 -->
<build>
<!-- 该元素设置了项目源码目录
当构建项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 -->
<sourceDirectory />
<!-- 该元素设置了项目脚本源码目录
该目录和源码目录不同:绝大多数情况下,该目录下的内容会被拷贝到输出目录(因为脚本是被解释的,而不是被编译的)。 -->
<scriptSourceDirectory />
<!-- 该元素设置了项目单元测试使用的源码目录
当测试项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 -->
<testSourceDirectory />
<!-- 被编译过的应用程序class文件存放的目录。 -->
<outputDirectory />
<!-- 被编译过的测试class文件存放的目录。 -->
<testOutputDirectory />
<!-- 使用来自该项目的一系列构建扩展 -->
<extensions>
<!-- 描述使用到的构建扩展。 -->
<extension>
<!-- 构建扩展的groupId -->
<groupId />
<!-- 构建扩展的artifactId -->
<artifactId />
<!-- 构建扩展的版本 -->
<version />
</extension>
</extensions>
<!-- 当项目没有规定目标(Maven2叫做阶段)时的默认值 -->
<defaultGoal />
<!-- 这个元素描述了项目相关的所有资源路径列表,例如和项目相关的属性文件,这些资源被包含在最终的打包文件里。 -->
<resources>
<!-- 这个元素描述了项目相关或测试相关的所有资源路径 -->
<resource>
<!-- 描述了资源的目标路径。该路径相对target/classes目录(例如${project.build.outputDirectory})。
举个例子,如果你想资源在特定的包里(org.apache.maven.messages),你就必须该元素设置为:
org/apache/maven/messages。然而,如果你只是想把资源放到源码目录结构里,就不需要该配置。 -->
<targetPath />
<!-- 是否使用参数值代替参数名。参数值取自properties元素或者文件里配置的属性,文件在filters元素里列出。 -->
<filtering />
<!-- 描述存放资源的目录,该路径相对POM路径 -->
<directory />
<!-- 包含的模式列表,例如:**/*.xml -->
<includes />
<!-- 排除的模式列表,例如:**/*.xml -->
<excludes />
</resource>
</resources>
<!-- 这个元素描述了单元测试相关的所有资源路径,例如和单元测试相关的属性文件。 -->
<testResources>
<!-- 这个元素描述了测试相关的所有资源路径,参见build/resources/resource元素的说明 -->
<testResource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</testResource>
</testResources>
<!-- 构建产生的所有文件存放的目录 -->
<directory />
<!-- 产生的构件的文件名,默认值是${artifactId}-${version}。 -->
<finalName />
<!-- 当filtering开关打开时,使用到的过滤器属性文件列表。 -->
<filters />
<!-- 子项目可以引用的默认插件信息。该插件配置项直到被引用时才会被解析或绑定到生命周期。
给定插件的任何本地配置都会覆盖这里的配置。 -->
<pluginManagement>
<!-- 使用的插件列表 -->
<plugins>
<!-- plugin元素包含描述插件所需要的信息。 -->
<plugin>
<!-- 插件在仓库里的groupID -->
<groupId />
<!-- 插件在仓库里的artifactID -->
<artifactId />
<!-- 被使用的插件的版本(或版本范围) -->
<version />
<!-- 是否从该插件下载Maven扩展,例如打包和类型处理器。
由于性能原因,只有在真需要下载时,该元素才被设置成enabled。 -->
<extensions />
<!-- 在构建生命周期中执行一组目标的配置。每个目标可能有不同的配置。 -->
<executions>
<!-- execution元素包含了插件执行需要的信息 -->
<execution>
<!-- 执行目标的标识符,用于标识构建过程中的目标,或者匹配继承过程中需要合并的执行目标 -->
<id />
<!-- 绑定了目标的构建生命周期阶段,如果省略,目标会被绑定到源数据里配置的默认阶段 -->
<phase />
<!-- 配置的执行目标 -->
<goals />
<!-- 配置是否被传播到子POM -->
<inherited />
<!-- 作为DOM对象的配置 -->
<configuration />
</execution>
</executions>
<!-- 项目引入插件所需要的额外依赖 -->
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
<!-- 任何配置是否被传播到子项目 -->
<inherited />
<!-- 作为DOM对象的配置 -->
<configuration />
</plugin>
</plugins>
</pluginManagement>
<!-- 使用的插件列表 -->
<plugins>
<!-- 参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</build>

<!-- 在列的项目构建profile,如果被激活,会修改构建处理。 -->
<profiles>
<!-- 根据环境参数或命令行参数激活某个构建处理 -->
<profile>
<!-- 构建配置的唯一标识符。即用于命令行激活,也用于在继承时合并具有相同标识符的profile。 -->
<id />
<!-- 自动触发profile的条件逻辑。Activation是profile的开启钥匙。profile的力量来自于它,
能够在某些特定的环境中自动使用某些特定的值;这些环境通过activation元素指定。
activation元素并不是激活profile的唯一方式。 -->
<activation>
<!-- profile默认是否激活的标志 -->
<activeByDefault />
<!-- 当匹配的jdk被检测到,profile被激活。
例如,"1.4"激活JDK1.4,1.4.0_2,而"!1.4"激活所有版本不是以1.4开头的JDK。 -->
<jdk />
<!-- 当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。 -->
<os>
<!-- 激活profile的操作系统的名字 -->
<name>Windows XP</name>
<!-- 激活profile的操作系统所属家族(如"windows") -->
<family>Windows</family>
<!-- 激活profile的操作系统体系结构 -->
<arch>x64</arch>
<!-- 激活profile的操作系统版本 -->
<version>6.1.7100</version>
</os>
<!-- 如果Maven检测到某一个属性(其值可以在POM中通过${名称}引用),其拥有对应的名称和值,Profile就会被激活。
如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段。 -->
<property>
<!-- 激活profile的属性的名称 -->
<name>mavenVersion</name>
<!-- 激活profile的属性的值 -->
<value>2.0.3</value>
</property>
<!-- 提供一个文件名,通过检测该文件的存在或不存在来激活profile。
exists:检查文件是否存在,如果存在则激活profile。
missing:检查文件是否存在,如果不存在则激活profile。 -->
<file>
<!-- 如果指定的文件存在,则激活profile。 -->
<exists>/usr/local/xxxx/xxxx-home/tomcat/maven-guide-zh-to-production/workspace/</exists>
<!-- 如果指定的文件不存在,则激活profile。 -->
<missing>/usr/local/xxxx/xxxx-home/tomcat/maven-guide-zh-to-production/workspace/</missing>
</file>
</activation>
<!-- 构建项目所需要的信息。参见build元素。 -->
<build>
<defaultGoal />
<resources>
<resource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</resource>
</resources>
<testResources>
<testResource>
<targetPath />
<filtering />
<directory />
<includes />
<excludes />
</testResource>
</testResources>
<directory />
<finalName />
<filters />
<pluginManagement>
<plugins>
<!-- 参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- 参见build/pluginManagement/plugins/plugin元素 -->
<plugin>
<groupId />
<artifactId />
<version />
<extensions />
<executions>
<execution>
<id />
<phase />
<goals />
<inherited />
<configuration />
</execution>
</executions>
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
<goals />
<inherited />
<configuration />
</plugin>
</plugins>
</build>
<!-- 模块(有时称作子项目)被构建成项目的一部分。列出的每个模块元素是指向该模块的目录的相对路径。 -->
<modules />
<!-- 发现依赖和扩展的远程仓库列表 -->
<repositories>
<!-- 参见repositories/repository元素 -->
<repository>
<id />
<name />
<url />
<layout />
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
</repository>
</repositories>
<!-- 发现插件的远程仓库列表,这些插件用于构建和报表 -->
<pluginRepositories>
<!-- 包含需要连接到远程插件仓库的信息。参见repositories/repository元素。 -->
<pluginRepository>
<id />
<name />
<url />
<layout />
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
</pluginRepository>
</pluginRepositories>
<!-- 该元素描述了项目相关的所有依赖。这些依赖组成了项目构建过程中的一个个环节。
它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。 -->
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
<!-- 不赞成使用。现在Maven忽略该元素。 -->
<reports />
<!-- 该元素包括使用报表插件产生报表的规范。当用户执行"mvn site",这些报表就会运行。
在页面导航栏能看到所有报表的链接。参见reporting 元素。 -->
<reporting>......</reporting>
<!-- 参见dependencyManagement元素 -->
<dependencyManagement>
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
</dependencyManagement>
<!-- 参见distributionManagement元素 -->
<distributionManagement>......</distributionManagement>
<!-- 参见properties元素 -->
<properties />
</profile>
</profiles>

<!-- 模块(有时称作子项目)被构建成项目的一部分。列出的每个模块元素是指向该模块的目录的相对路径。 -->
<modules />

<!-- 发现依赖和扩展的远程仓库列表,配置多个repository时,按顺序依次查找。 -->
<repositories>
<!-- 包含需要连接到远程仓库的信息 -->
<repository>
<!-- 远程仓库唯一标识符。可以用来匹配在settings.xml文件里配置的远程仓库。 -->
<id>banseon-repository-proxy</id>
<!-- 远程仓库名称 -->
<name>banseon-repository-proxy</name>
<!-- 远程仓库URL,按protocol://hostname/path形式 -->
<url>http://10.10.10.123:8080/repository/</url>
<!-- 用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。
Maven2为其仓库提供了一个默认的布局;然而,Maven1.x有一种不同的布局。
我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 -->
<layout>default</layout>
<!-- 如何处理远程仓库里发布版本的下载 -->
<releases>
<!-- true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
<enabled />
<!-- 该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。
选项:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 -->
<updatePolicy />
<!-- 当Maven验证构件校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。 -->
<checksumPolicy />
</releases>
<!-- 如何处理远程仓库里快照版本的下载。
有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。
例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素。 -->
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
</repository>
</repositories>

<!-- 发现插件的远程仓库列表,这些插件用于构建和报表。 -->
<pluginRepositories>
<!-- 包含需要连接到远程插件仓库的信息。参见repositories/repository元素。 -->
<pluginRepository>......</pluginRepository>
</pluginRepositories>

<!-- 该元素描述了项目相关的所有依赖。这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。
要获取更多信息,请看项目依赖机制。 -->
<dependencies>
<dependency>
<!-- 依赖的groupID -->
<groupId>org.apache.maven</groupId>
<!-- 依赖的artifactID -->
<artifactId>maven-artifact</artifactId>
<!-- 依赖的版本号。在Maven2里,也可以配置成版本号的范围。 -->
<version>3.8.1</version>
<!-- 依赖类型,默认类型是jar。它通常表示依赖的文件的扩展名,但也有例外。
一个类型可以被映射成另外一个扩展名或分类器。类型经常和使用的打包方式对应,尽管这也有例外。
一些类型的例子:jar,war,ejb-client和test-jar。
如果设置extensions为true,就可以在plugin里定义新的类型。所以前面的类型的例子不完整。 -->
<type>jar</type>
<!-- 依赖的分类器。分类器可以区分属于同一个POM,但不同构建方式的构件。分类器名被附加到文件名的版本号后面。
例如,如果你想要构建两个单独的构件成JAR,一个使用Java 1.4编译器,另一个使用Java 6编译器,
你就可以使用分类器来生成两个单独的JAR构件。 -->
<classifier></classifier>
<!-- 依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来。欲知详情请参考依赖机制。
- compile: 默认范围,用于编译
- provided: 类似于编译,但支持你期待jdk或者容器提供,类似于classpath
- runtime: 在执行时需要使用
- test: 用于test任务时使用
- system: 需要外在提供相应的元素。通过systemPath来取得
- systemPath: 仅用于范围为system。提供相应的路径
- optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用 -->
<scope>test</scope>
<!-- 仅供system范围使用。注意,不鼓励使用这个元素,并且在新的版本中该元素可能被覆盖掉。
该元素为依赖规定了文件系统上的路径。需要绝对路径而不是相对路径。
推荐使用属性匹配绝对路径,例如${java.home}。 -->
<systemPath></systemPath>
<!-- 当计算传递依赖时,从依赖构件列表里,列出被排除的依赖构件集。
即告诉maven你只依赖指定的项目,不依赖项目的依赖。此元素主要用于解决版本冲突问题。 -->
<exclusions>
<exclusion>
<artifactId>spring-core</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
<!-- 可选依赖,如果你在项目B中把C依赖声明为可选,则要在依赖于B的项目(例如项目A)中显式的引用对C的依赖。
可选依赖阻断依赖的传递性。 -->
<optional>true</optional>
</dependency>
</dependencies>

<!-- 不赞成使用,现在Maven忽略该元素 -->
<reports></reports>

<!-- 该元素描述使用报表插件产生报表的规范。当用户执行"mvn site",这些报表就会运行。在页面导航栏能看到所有报表的链接。 -->
<reporting>
<!-- true,则网站不包括默认的报表。这包括"项目信息"菜单中的报表。 -->
<excludeDefaults />
<!-- 所有产生的报表存放到哪里。默认值是${project.build.directory}/site。 -->
<outputDirectory />
<!-- 使用的报表插件和他们的配置 -->
<plugins>
<!-- plugin元素包含描述报表插件需要的信息 -->
<plugin>
<!-- 报表插件在仓库里的groupID -->
<groupId />
<!-- 报表插件在仓库里的artifactID -->
<artifactId />
<!-- 被使用的报表插件的版本(或版本范围) -->
<version />
<!-- 任何配置是否被传播到子项目 -->
<inherited />
<!-- 报表插件的配置 -->
<configuration />
<!-- 一组报表的多重规范,每个规范可能有不同的配置。一个规范(报表集)对应一个执行目标。
例如,有 1,2,3,4,5,6,7,8,9 个报表,
1,2,5 构成A报表集,对应一个执行目标,
2,5,8 构成B报表集,对应另一个执行目标。 -->
<reportSets>
<!-- 表示报表的一个集合,以及产生该集合的配置。 -->
<reportSet>
<!-- 报表集合的唯一标识符,POM继承时用到。 -->
<id />
<!-- 产生报表集合时,被使用的报表的配置。 -->
<configuration />
<!-- 配置是否被继承到子POMs -->
<inherited />
<!-- 这个集合里使用到哪些报表 -->
<reports />
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>

<!-- 继承自该项目的所有子项目的默认依赖信息。
这部分的依赖信息不会被立即解析,而是当子项目声明一个依赖(必须描述groupID和artifactID信息)时,如果groupID
和artifactID以外的一些信息没有描述,则通过groupID和artifactID匹配到这里的依赖,并使用这里的依赖信息。
比如锁定子项目的一些依赖的版本时,即可在父项目中定义。 -->
<dependencyManagement>
<dependencies>
<!-- 参见dependencies/dependency元素 -->
<dependency>......</dependency>
</dependencies>
</dependencyManagement>

<!-- 项目分发信息,在执行"mvn deploy"后表示要发布的位置。
有了这些信息就可以把网站部署到远程服务器或者把构件部署到远程仓库。 -->
<distributionManagement>
<!-- 部署项目产生的构件到远程仓库需要的信息 -->
<repository>
<!-- 是分配给快照一个唯一的版本号(由时间戳和构建流水号)?还是每次都使用相同的版本号?
参见repositories/repository元素 -->
<uniqueVersion />
<id>xxx-maven2</id>
<name>xxx maven2</name>
<url>file://${basedir}/target/deploy</url>
<layout />
</repository>
<!-- 构件的快照部署到哪里?如果没有配置该元素,默认部署到repository元素配置的仓库。
参见distributionManagement/repository元素 -->
<snapshotRepository>
<uniqueVersion />
<id>xxx-maven2</id>
<name>xxx-maven2 Snapshot Repository</name>
<url>scp://svn.xxxx.com/xxx:/usr/local/maven-snapshot</url>
<layout />
</snapshotRepository>
<!-- 部署项目的网站需要的信息 -->
<site>
<!-- 部署位置的唯一标识符,用来匹配站点和settings.xml文件里的配置 -->
<id>banseon-site</id>
<!-- 部署位置的名称 -->
<name>business api website</name>
<!-- 部署位置的URL,按protocol://hostname/path形式 -->
<url>scp://svn.baidu.com/xxx:/var/www/localhost/web</url>
</site>
<!-- 项目下载页面的URL。如果没有该元素,用户应该参考主页。
使用该元素的原因是:帮助定位那些不在仓库里的构件(由于license限制)。 -->
<downloadUrl />
<!-- 如果构件有了新的groupID和artifactID(构件移到了新的位置),这里列出构件的重定位信息。 -->
<relocation>
<!-- 构件新的groupID -->
<groupId />
<!-- 构件新的artifactID -->
<artifactId />
<!-- 构件新的版本号 -->
<version />
<!-- 显示给用户的,关于移动的额外信息,例如原因。 -->
<message />
</relocation>
<!-- 给出该构件在远程仓库的状态。不得在本地项目中设置该元素,因为这是工具自动更新的。有效的值有:
- none: 默认
- converted: 仓库管理员从Maven1 POM转换过来
- partner: 直接从伙伴Maven 2仓库同步过来
- deployed: 从Maven 2实例部署
- verified: 被核实时正确的和最终的 -->
<status />
</distributionManagement>

<!-- 以值替代名称,Properties可以在整个POM中使用,也可以作为触发条件(见settings.xml配置文件里activation元素的说明)。
格式是:<name>value</name>。 -->
<properties />
</project>

本文参考

https://www.cnblogs.com/iceJava/p/10356309.html

https://www.cnblogs.com/hongmoshui/p/10762272.html

https://www.cnblogs.com/cxzdy/p/5126087.html

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

maven 仓库分为本地仓库和远程仓库,而远程仓库又分为 maven 中央仓库、其他远程仓库和私服 (私有服务器)。

image-20210409155046796

maven 项目使用的仓库一般有如下几种方式:

  1. maven 中央仓库,这是默认的仓库。
  2. 镜像仓库,通过 sttings.xml 中的 settings.mirrors.mirror 配置。
  3. 全局 profile 仓库,通过 settings.xml 中的 settings.repositories.repository 配置。
  4. 项目仓库,通过 pom.xml 中的 project.repositories.repository 配置。
  5. 项目 profile 仓库,通过 pom.xml 中的 project.profiles.profile.repositories.repository 配置。
  6. 本地仓库。

如果所有仓库的配置都存在,那么依赖的搜索顺序也会变得异常复杂。

仓库的配置方式

本地仓库

maven 缺省的本地仓库地址为 ${user.home}/.m2/repository,也就是说,一个用户会对应的拥有一个本地仓库。

可以通过修改 ${user.home}/.m2/settings.xml,在 节点下添加配置:

1
<localRepository>D:\java\maven-repo</localRepository>

如果想让所有的用户使用统一的配置,那么可以修改 maven 主目录下的 setting.xml:${M2_HOME}/conf/setting.xml。

maven 中央仓库

在 maven 安装目录的 lib 目录下,有一个 maven-model-builder-3.6.1.jar,里面的 org/apache/maven/model/pom-4.0.0.xml 文件定义了 maven 默认中央仓库的地址:https://repo.maven.apache.org/maven2,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>

一般使用阿里云镜像仓库代替默认的 maven 中央仓库,配置方式有两种:

  • 第一种,全局配置

修改 ${M2_HOME}/conf/setting.xml 文件,在 节点下添加配置:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirrors>

修改全局配置后,所有使用此 maven 的工程都会生效。

< mirrorOf> 可以设置为哪个中央仓库做镜像,为名为 “central” 的中央仓库做镜像,写作 < mirrorOf>central< /mirrorOf>;为所有中央仓库做镜像,写作 < mirrorOf>*< /mirrorOf> (不建议)。maven 默认中央仓库的 id 为 central。id是唯一的。

  • 第二种,局部配置

在需要使用阿里云镜像仓库的 maven 工程的 pom.xml 文件中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<repositories>
<repository>
<id>aliyun</id>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>aliyun-plugin</id>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

修改局部配置后,只对当前工程有效。

私服

  • 第一种,全局配置

修改 ${M2_HOME}/conf/setting.xml 文件,在 节点下添加配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<profiles>
<profile>
<id>matgene-nexus</id>
<repositories>
<repository>
<id>nexus</id>
<url>http://localhost:8081/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nexus-plugin</id>
<url>http://localhost:8081/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>

然后,在 节点下添加激活配置 (通过配置的 profile 的 id 标识进行激活):

1
2
3
<activeProfiles>
<activeProfile>matgene-nexus</activeProfile>
</activeProfiles>
  • 第二种,局部配置

在需要使用私服的 maven 工程的 pom.xml 文件中添加。

上传:

settings.xml:

1
2
3
4
5
6
7
8
9
10
<server>
<id>releases</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>

pom.xml:

1
2
3
4
5
6
7
8
9
10
<distributionManagement>
<repository>
<id>releases</id>
<url>http://localhost:8081/nexus/content/repositories/releases/</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<url>http://localhost:8081/nexus/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>

下载:

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<repositories>
<repository>
<id>nexus</id>
<url>http://localhost:8081/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nexus-plugin</id>
<url>http://localhost:8081/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

在项目中,将私服地址更改为自己公司的实际地址。

依赖的下载顺序

准备测试环境

安装 jdk 和 maven。

使用如下命令创建测试项目:

1
yes | mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-webapp  -DinteractiveMode=true -DgroupId=com.pollyduan -DartifactId=myweb -Dversion=1.0 -Dpackage=com.pollyduan

创建完成后,为了避免后续测试干扰,先执行一次 compile。

1
2
cd myweb
mvn compile

最后,修改 pom.xml 文件,将 junit 版本号改为 4.12 。我们要使用这个 jar 来测试依赖的搜索顺序。

默认情况

首先确保 junit 4.12 不存在:

1
rm -rf ~/.m2/repository/junit/junit/4.12

默认情况下没有配置任何仓库,也就是说,既没更改 $M2_HOME/conf/settings.xml,也没有添加 ~/.m2/settings.xml。

执行编译,查看日志中拉取 junit 的仓库。

1
2
3
4
mvn compile

...
Downloaded from central: https://repo.maven.apache.org/maven2/junit/junit/4.12/junit-4.12.pom (24 kB at 11 kB/s)

从显示的仓库 id 可以看出:默认是从 maven 中央仓库拉取的 jar。

配置镜像仓库 settings_mirror

创建 ~/.m2/setttings.xml,配置 maven 中央仓库的镜像,如下:

1
2
3
4
5
6
7
8
9
<settings>
<mirrors>
<mirror>
<id>settings_mirror</id>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>

重新测试:

1
2
rm -rf ~/.m2/repository/junit/junit/4.12
mvn compile

在日志中查看下载依赖的仓库:

1
Downloaded from settings_mirror: https://maven.aliyun.com/repository/public/junit/junit/4.12/junit-4.12.pom (24 kB at 35 kB/s)

从显示的仓库 id 可以看出:是从 settings_mirror 中下载的 jar。

结论:settings_mirror 的优先级高于 central。

配置项目仓库 pom_repositories

在 project 中的 pom.xml 文件中,增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<repositories>
<repository>
<id>pom_repositories</id>
<name>local</name>
<url>http://10.18.29.128/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<sapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

由于改变了 id 的名字,所以仓库地址无所谓,使用相同的地址也不影响测试。

执行测试:

1
2
rm -rf ~/.m2/repository/junit/junit/4.12
mvn compile

在日志中查看下载依赖的仓库:

1
Downloaded from pom_repositories: http://10.18.29.128/nexus/content/groups/public/junit/junit/4.12/junit-4.12.pom (24 kB at 95 kB/s)

从显示的仓库 id 可以看出:jar 是从 pom_repositories 中下载的。

结论:pom_repositories 优先级高于 settings_mirror。

配置全局 profile 仓库 settings_profile_repo

在 ~/.m2/settings.xml 中 settings 的节点内增加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<profiles>
<profile>
<id>s_profile</id>
<repositories>
<repository>
<id>settings_profile_repo</id>
<name>netease</name>
<url>http://mirrors.163.com/maven/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>

执行测试:

1
2
rm -rf ~/.m2/repository/junit/junit/4.12
mvn compile -Ps_profile

在日志中查看下载依赖的仓库:

1
Downloaded from settings_profile_repo: http://mirrors.163.com/maven/repository/maven-public/junit/junit/4.12/junit-4.12.pom (24 kB at 63 kB/s)

从显示的仓库 id 可以看出:jar 是从 settings_profile_repo 中下载的。

结论:settings_profile_repo 优先级高于 pom_repositories 和 settings_mirror。

配置项目 profile 仓库 pom_profile_repo

在 project 中的 pom.xml 文件中,增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<profiles>
<profile>
<id>p_profile</id>
<repositories>
<repository>
<id>pom_profile_repo</id>
<name>local</name>
<url>http://10.18.29.128/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>

执行测试:

1
2
3
rm -rf ~/.m2/repository/junit/junit/4.12
mvn compile -Ps_profile,p_profile
mvn compile -Pp_profile,s_profile

在日志中查看下载依赖的仓库:

1
Downloaded from settings_profile_repo: http://mirrors.163.com/maven/repository/maven-public/junit/junit/4.12/junit-4.12.pom (24 kB at 68 kB/s)

从显示的仓库 id 可以看出:jar 是从 settings_profile_repo 中下载的。

结论:settings_profile_repo 优先级高于 pom_profile_repo。

进一步测试:

1
2
rm -rf ~/.m2/repository/junit/junit/4.12
mvn compile -Pp_profile

在日志中查看下载依赖的仓库:

1
Downloaded from pom_profile_repo: http://10.18.29.128/nexus/content/groups/public/junit/junit/4.12/junit-4.12.pom (24 kB at 106 kB/s)

从显示的仓库 id 可以看出:jar 是从 settings_profile_repo 中下载的。

结论:pom_profile_repo 优先级高于 pom_repositories。

本地仓库 local_repo

这不算测试了,只是一个结论,可以任意测试:只要 ~/.m2/repository 中包含依赖,无论怎么配置,都会优先使用 local 本地仓库中的 jar。

最终结论

  • settings_mirror 的优先级高于 central

  • settings_profile_repo 优先级高于 settings_mirror

  • settings_profile_repo 优先级高于 pom_repositories

  • settings_profile_repo 优先级高于 pom_profile_repo

  • pom_repositories 优先级高于 settings_mirror

  • pom_profile_repo 优先级高于 pom_repositories

通过上面的比较,可以得出各种仓库完整的搜索顺序链:

local_repo > settings_profile_repo > pom_profile_repo > pom_repositories > settings_mirror > central

简单来说,查找依赖的顺序大致如下:

  1. 在本地仓库中寻找,如果没有则进入下一步。

  2. 在全局配置的私服仓库 (settings.xml 中配置的并被激活) 中寻找,如果没有则进入下一步。

  3. 在项目自身配置的私服仓库 (pom.xml) 中寻找,如果没有则进入下一步。

  4. 在中央仓库中寻找,如果没有则终止寻找。

说明:

  1. 如果在找寻的过程中,发现该仓库有镜像设置,则用镜像的地址代替,即假设现在进行到要在 respository A 仓库中查找某个依赖,但 A 仓库配置了 mirror,则会转到从 A 的 mirror 中查找该依赖,不会再从 A 中查找。

  2. settings.xml 中配置的 profile (激活的) 下的 respository 优先级高于项目中 pom.xml 文件配置的 respository。

  3. 如果仓库的 id 设置成 “central”,则该仓库会覆盖 maven 默认的中央仓库配置。

本文参考

https://blog.csdn.net/asdfsfsdgdfgh/article/details/96576665

https://www.cnblogs.com/default/p/11856188.html

https://my.oschina.net/polly/blog/2120650

https://blog.csdn.net/fengdayuan/article/details/93089136

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

分模块构建 maven 工程分析

在现实生活中,汽车厂家进行汽车生产时,由于整个生产过程非常复杂和繁琐,工作量非常大,所以车场都会将整个汽车的部件分开生产,最终再将生产好的部件进行组装,形成一台完整的汽车:

1559550879535

类似的,随着项目功能的增加,项目本身会变得越来越庞大,这个时候,代码的良好管理和规划就会变得很重要。为了提高效率,根据业务的不同将揉作一团的业务代码分离出来,业务划分上分割清晰,提高代码复用率,例如:

1559550904100

上述功能的实现就是代码这一层级的变动,可以采用多项目模式和多模块模式:

  • 多项目:每个业务单独新建项目并编写相应逻辑
  • 多模块:业务聚合在一个项目中的不同模块中,然后通过依赖调用实现业务逻辑

maven 工程的继承

在 java 语言中,类之间是可以继承的,通过继承,子类就可以引用父类中非 private 的属性和方法。同样,在 maven 工程之间也可以继承,子工程继承父工程后,就可以使用在父工程中引入的依赖。继承的目的是为了消除重复代码。

1559550956068

maven 工程的聚合

在 maven 工程的 pom.xml 文件中,可以使用 标签将其他 maven 工程聚合到一起,聚合的目的是为了进行统一操作。

例如,拆分后的 maven 工程有多个,如果要进行打包,就需要针对每个工程分别执行打包命令,操作起来非常繁琐。这时就可以使用 标签将这些工程统一聚合到 maven 工程中,需要打包的时候,只需要在此工程中执行一次打包命令,其下被聚合的工程就都会被打包了。

1559551000245

分模块构建 maven 工程

构建父模块

新建 maven 项目:

image-20210113100427171

保留 pom.xml 文件,删除 src 目录:

image-20210113101242280

构建子模块

新建 module 1:

image-20210113101550741

新建 module 2:

image-20210113101831549

如果需要更多的模块,重复上述步骤。

父模块 pom 配置

公用的 pom 配置,可以放在父模块的 pom.xml 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.matgene.reaction-extractor-assistant</groupId>
<artifactId>reaction-extractor-assistant</artifactId>
<!-- parent必须使用pom格式打包并上传到仓库 -->
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>patent-loader</module>
<module>consumer-log</module>
<module>consumer-reaction</module>
<module>consumer-timeout</module>
</modules>

<!-- 全局版本管理 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.version>3.8.1</maven.compiler.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

<!-- 全局依赖管理 -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>com.github.danielwegener</groupId>
<artifactId>logback-kafka-appender</artifactId>
<version>0.2.0-RC1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

子模块 pom 配置

子模块单独使用的 pom 配置,放在子模块自己的 pom.xml 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>reaction-extractor-assistant</artifactId>
<groupId>cn.matgene.reaction-extractor-assistant</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>patent-loader</artifactId>

<properties>
<app.main.class>cn.matgene.patent.cn.matgene.patent.loader.PatentLoaderJob</app.main.class>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${app.main.class}</Main-Class>
<X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
<X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

上面构建的项目比较简单,各模块之间不存在依赖关系,同时,因为每个模块都需要打包,因此把打包的插件放在每一个子模块的 pom.xml 文件中。

一个 spring web 项目的实例

  1. 父工程 maven_parent 构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
 <properties>
<spring.version>5.0.5.RELEASE</spring.version>
<springmvc.version>5.0.5.RELEASE</springmvc.version>
<mybatis.version>3.4.5</mybatis.version>
</properties>

<!--锁定jar版本-->
<dependencyManagement>
<dependencies>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- springMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springmvc.version}</version>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
  1. 子工程 maven_pojo 构建
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
</dependencies>
  1. 子工程 maven_dao 构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 <dependencies>
<!-- maven_pojo的依赖 -->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Mybatis和mybatis与spring的整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!-- MySql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!-- druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- spring相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
  1. 子工程 maven_service 构建
1
2
3
4
5
6
7
8
<dependencies>
<!-- maven_dao的依赖 -->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_dao</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
  1. 子工程 maven_web 构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<!-- maven_service的依赖 -->
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>

<build>
<finalName>maven_web</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
  1. 项目整体结构如下:
  1. maven_parent 为父工程,其余工程为子工程,都继承父工程 maven_parent;

  2. maven_parent 工程将其子工程都进行了聚合 ;

  3. 子工程之间存在依赖关系,比如 maven_dao 依赖 maven_pojo,maven_service 依赖 maven_dao,maven_web 依赖 maven_service。

本文参考

https://juejin.cn/post/6844903970024980488

https://www.cnblogs.com/tianlong/p/10552848.html

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

No space left on device

有时候,在创建新文件,或者往磁盘写内容时,会提示 No space left on device 异常。

一般来说,linux 空间占满有如两种情况:

空间占满

通过 df -h 命令,查看空间的使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 3.9G 0 3.9G 0% /dev
tmpfs 799M 82M 718M 11% /run
/dev/mapper/lin--vg-root 491G 195G 272G 42% /
tmpfs 3.9G 8.0K 3.9G 1% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda1 472M 58M 390M 13% /boot
tmpfs 799M 0 799M 0% /run/user/1000
192.168.1.152:/mnt/chenlei 10T 5.0T 4.6T 53% /home/lin/share/storage_server_1
192.168.1.236:/mnt 10T 9.0T 520G 95% /home/lin/share/storage_server_2
192.168.1.106:/mnt 40T 24T 15T 63% /home/lin/share/storage_server_3
192.168.1.102:/home/lin/share 491G 195G 272G 42% /tmp/share

可以看出,各分区仍有较大的空间能够使用。如果某个分区的使用率达到了 100%,那也就无法再创建新文件,也无法再写入内容,需要删除一些文件。

inode 占满

通过 df -i 命令,查看 inode 的使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ df -ih
Filesystem Inodes IUsed IFree IUse% Mounted on
udev 993K 421 993K 1% /dev
tmpfs 998K 749 998K 1% /run
/dev/mapper/lin--vg-root 32M 1.6M 30M 6% /
tmpfs 998K 2 998K 1% /dev/shm
tmpfs 998K 3 998K 1% /run/lock
tmpfs 998K 16 998K 1% /sys/fs/cgroup
/dev/sda1 122K 303 122K 1% /boot
tmpfs 998K 4 998K 1% /run/user/1000
192.168.1.152:/mnt/chenlei 320M 42M 279M 13% /home/lin/share/storage_server_1
192.168.1.236:/mnt 320M 69M 252M 22% /home/lin/share/storage_server_2
192.168.1.106:/mnt 640M 641M 0M 100% /home/lin/share/storage_server_3
192.168.1.102:/home/lin/share 32M 1.6M 30M 6% /tmp/share

可以看出,每个分区都有一定大小的 inode 空间,但 /home/lin/share/storage_server_3 分区的 inode 空间使用率达到 100%。因此,再往此分区创建新文件或写入内容时,会提示 No space left on device 异常。

解决方法:将 /home/lin/share/storage_server_3 分区上一些不必要的文件删除。

理解 inode,要从文件储存说起。

文件储存在硬盘上,硬盘的最小存储单位叫做 “扇区” (Sector),每个扇区储存 512 字节 (相当于 0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个 “块” (Block)。这种由多个扇区组成的 “块”,是文件存取的最小单位。”块” 的大小,最常见的是 4 KB,即连续八个 Sector 组成一个 Block。

文件数据都储存在 “块” 中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做 inode,中文译名为 “索引节点”。

每一个文件都有对应的 inode,里面包含了与该文件有关的一些信息。

某些时候,尽管一个分区的磁盘占用率未满,但是 inode 已经用完,可能是因为该分区的目录下存在大量小文件导致。尽管小文件占用的磁盘空间并不大,但是数量太多,也会导致 inode 用尽。本例中就是因为 /home/lin/share/storage_server_3 分区存在大量的小文件。

常用日志处理工具

常见的 log 日志处理工具有:log4j、Logging、commons-logging、slf4j、logback。其中,commons-loggin、slf4j 是一种日志抽象门面,不是具体的日志框架;log4j、logback 是具体的日志实现框架。

一般使用 slf4j + logback 处理日志,也可以使用 slf4j + log4j、commons-logging + log4j 这两种日志组合框架。

日志级别

日志的输出都是分级别的,不同的场合设置不同的级别,以打印不同的日志。下面拿最普遍用的 log4j 日志框架来做个日志级别的说明,这个比较奇全,其他的日志框架也都大同小异。

log4j 的级别类 org.apache.log4j.Level 里面定义了日志级别,日志输出优先级由高到底分别为以下 8 种:

image-20210409153045151

日志级别 描述
OFF 关闭:最高级别,不输出日志。
FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。
ERROR 错误:输出错误,但应用还能继续运行。
WARN 警告:输出可能潜在的危险状况。
INFO 信息:输出应用运行过程的详细信息。
DEBUG 调试:输出更细致的对调试应用有用的信息。
TRACE 跟踪:输出更细致的程序运行轨迹。
ALL 所有:输出所有级别信息。

所以,日志优先级别标准顺序为:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

如果日志设置为 L ,一个级别为 P 的输出日志只有当 P >= L 时日志才会输出。

即如果日志级别 L 设置 INFO,只有 P 的输出级别为 INFO、WARN,后面的日志才会正常输出。

具体的输出关系可以参考下图:

image-20210409153121945

Lombok

Lombok 是一种 java 实用工具,可用来帮助开发人员消除 java 的冗长代码,尤其是对于简单的 java 对象 (POJO)。它通过注释实现这一目的。

引入

IntelliJ 安装:

image-20210409153203001

Lombok 是侵入性很高的一个 library。

maven 添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>

注解说明

常用注解:

img

  • @Getter@Setter

自动生成 getter 和 setter 方法。

img

  • @ToString

自动重写 toString() 方法,打印所有变量。也可以加其他参数,例如 @ToString(exclude=”id”) 排除 id 属性,或者 @ToString(callSuper=true, includeFieldNames=true) 调用父类的 toString() 方法,包含所有属性。

img

  • @EqualsAndHashCode

自动生成 equals(Object other)hashcode() 方法,包括所有非静态变量和非 transient 的变量。

img

如果某些变量不想要加进判断,可以通过 exclude 排除,也可以使用 of 指定某些字段:

img

java 中规定,当两个 object equals 时,它们的 hashcode 一定要相同,反之,当 hashcode 相同时,object 不一定 equals。所以 equals 和 hashcode 要一起 implement,免得出现违反 java 规定的情形。

  • @NoArgsConstructor@AllArgsConstructor@RequiredArgsConstructor

这三个很像,都是自动生成该类的 constructor,差別只在生成的 constructor 的参数不一样而已。

@NoArgsConstructor:生成一个沒有参数的 constructor。

img

在 java 中,如果沒有指定类的 constructor,java compiler 会自动生成一个无参构造器,但是如果自己写了 constructor 之后,java 就不会再自动生成无参构造器。但是,很多时候,无参构造器是必须的,因此,为避免不必要的麻烦,应在类上至少加上 @NoArgsConstrcutor

@AllArgsConstructor :生成一个包含所有参数的 constructor。

img

@RequiredArgsConstructor:生成一个包含 “特定参数” 的 constructor,特定参数指的是那些有加上 final 修饰词的变量。

img

如果所有的变量都沒有用 final 修饰,@RequiredArgsConstructor 会生成一个沒有参数的 constructor。

  • @Data

等于同时添加了以下注解:@Getter@Setter@ToString@EqualsAndHashCode@RequiredArgsConstructor

img

  • @Value

把所有的变量都设成 final,其他的就跟 @Data 类似,等于同时添加了以下注解:@Getter@ToString@EqualsAndHashCode@RequiredArgsConstructor

img

  • @Builder

自动生成流式 set 值写法。

img

注意,虽然只要加上 @Builder 注解,我们就能用流式写法快速设定 Object 的值,但是 setter 还是不应该舍弃的,因为 Spring 或是其他框架,有很多地方都会用到 Object 的 getter/setter 方法来对属性取值/赋值。

所以,通常是 @Data@Builder 会一起用在同个类上,既方便流式写 code,也方便框架做事。比如:

1
2
3
4
5
6
@Data
@Builder
public class User {
private Integer id;
private String name;
}
  • @Slf4j

自动生成该类的 log 静态常量,要打日志就可以直接打,不用再手动 new log 静态常量了。

img

除了 @Slf4j 之外,Lombok 也提供其他日志框架的几种注解,像是 @Log@Log4j 等,他们都可以创建一个静态常量 log,只是使用的 library 不一样而已。

1
2
3
4
5
@Log // 对应的log语句如下
private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());

@Log4j // 对应的log语句如下
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);

更多的参考:https://juejin.cn/post/6844903557016076302

Logback

引入

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

logback 依赖中,含有对 slf4j 的依赖。

节点

configuration 为主节点,其主要字节点如下:

property

定义变量值的标签,有两个属性,name 和 value,定义变量后,可以使 “${name}” 来使用变量。

1
<property name="logging.level" value="info"/>

appender

日志打印的组件,定义打印过滤的条件、打印输出方式、滚动策略、编码方式、打印格式等。

种类:

  • ConsoleAppender:把日志添加到控制台。

    1
    2
    3
    4
    5
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder charset="utf-8">
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-6level %logger{50} - %msg%n</pattern>
    </encoder>
    </appender>
  • FileAppender:把日志添加到文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <appender name="ReactionExtractorAppender" class="ch.qos.logback.core.FileAppender">
    <append>true</append>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>${logging.level}</level>
    </filter>
    <file>
    ${logging.path}/reaction-extractor.log
    </file>
    <encoder>
    <pattern>${message.format}</pattern>
    <charset>UTF-8</charset>
    </encoder>
    </appender>
  • RollingFileAppender:FileAppender 的子类,滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <appender name="ReactionExtractorRollingAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <append>true</append>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>${logging.level}</level>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <FileNamePattern>${logging.path}/reaction-extractork-%d{yyyy-MM-dd}.log</FileNamePattern>
    <MaxHistory>30</MaxHistory>
    </rollingPolicy>
    <encoder>
    <pattern>${message.format}</pattern>
    <charset>UTF-8</charset>
    </encoder>
    </appender>

属性:

  • name:指定 appender 的名称。

  • class:指定 appender 的全限定名。

子节点:

  • append:默认为 true,表示日志被追加到文件结尾,如果是 false,清空现存文件。

  • filter:过滤器,执行完一个过滤器后返回 DENY,NEUTRAL,ACCEPT 三个枚举值中的一个。

    filter 的返回值含义:

    1. DENY:日志将立即被抛弃不再经过其他过滤器。
    2. NEUTRAL:有序列表里的下个过滤器过接着处理日志。
    3. ACCEPT:日志会被立即处理,不再经过剩余过滤器。

    filter 的两种类型:

    1. ThresholdFilter:临界值过滤器,过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回 NEUTRAL,当日志级别低于临界值时,日志会被拒绝。

      1
      2
      3
      <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>INFO</level>
      </filter>
    2. LevelFilter:级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据 onMath (用于配置符合过滤条件的操作) 和 onMismatch (用于配置不符合过滤条件的操作) 接收或拒绝日志。

      1
      2
      3
      4
      5
      <filter class="ch.qos.logback.classic.filter.LevelFilter">   
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
      </filter>
  • file:指定被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。

  • rollingPolicy:滚动策略,只有 appender 的 class 是 RollingFileAppender 时才需要配置。

    1. TimeBasedRollingPolicy:根据时间来制定滚动策略,既负责滚动也负责触发滚动。

      1
      2
      3
      4
      5
      6
      7
      8
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- 日志文件输出的文件名:按天回滚 daily -->
      <FileNamePattern>
      ${logging.path}/glmapper-spring-boot/glmapper-loggerone.log.%d{yyyy-MM-dd HH:mm:ss.SSS}
      </FileNamePattern>
      <!-- 日志文件保留天数 -->
      <MaxHistory>30</MaxHistory>
      </rollingPolicy>

      每天生成一个日志文件,日志文件保存 30 天。

    2. FixedWindowRollingPolicy:根据固定窗口算法重命名文件的滚动策略。

  • encoder:对记录事件进行格式化。主要作用是:把日志信息转换成字节数组,以及把字节数组写入到输出流。

    1
    2
    3
    4
    5
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    <!-- 格式化输出:%d表示日期;%thread表示线程名;%-5level:级别从左显示5个字符宽度;%logger{50} 表示logger名字最长50个字符,否则按照句点分割;%msg:日志消息;%n是换行符 -->
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
    <charset>UTF-8</charset>
    </encoder>

logger

用来设置某一个包或者具体的某一个类的日志打印级别以及指定 appender。

属性:

  • name:指定受此 logger 约束的某一个包或者具体的某一个类。
  • level:设置打印级别 (TRACE,DEBUG,INFO,WARN,ERROR,ALL 和 OFF),还有一个值 INHERITED 或者同义词 NULL,代表强制执行上级的级别。如果没有设置此属性,那么当前 logger 将会继承上级的级别。
  • addtivity:设置是否向上级 logger 传递打印信息,默认为 true。
1
2
3
<logger name="com.glmapper.spring.boot.controller" level="${logging.level}" additivity="false">
<appender-ref ref="GLMAPPER-LOGGERONE" />
</logger>

com.glmapper.spring.boot.controller 这个包下的 ${logging.level} 级别的日志将会使用 GLMAPPER-LOGGERONE 来打印。

root

根 logger,也是一种 logger,但只有一个 level 属性。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<!-- 使用说明:
1. logback核心jar包:logback-core-1.2.3.jar,logback-classic-1.2.3.jar,slf4j-api-1.7.25.jar
1) logback官方建议配合slf4j使用
2) logback手动下载地址:https://repo1.maven.org/maven2/ch/qos/logback/
3) slf4j手动下载地址:https://www.mvnjar.com/org.slf4j/slf4j-api/1.7.25/detail.html
4) jar包可以从maven仓库快速获取
2. logback分为3个组件:logback-core,logback-classic和logback-access
1) 其中logback-core提供了logback的核心功能,是另外两个组件的基础
2) logback-classic实现了slf4j的API,所以当想配合slf4j使用时,需要将logback-classic加入classpath
3) logback-access是为了集成servlet环境而准备的,可提供HTTP-access的日志接口
3. 配置中KafkaAppender的jar包:logback-kafka-appender-0.2.0-RC1.jar
-->

<!-- 参考:
https://juejin.im/post/5b51f85c5188251af91a7525
https://my.oschina.net/Declan/blog/1793444
-->

<!-- 说明:logback.xml配置文件,需放置在项目的resources路径下 -->

<!-- configuration属性:
scan:热加载,当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false
packagingData:是否打印包的信息。默认值为false
-->

<configuration
debug="false"
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd"
>
<!-- property:定义变量值,两个属性,name和value -->
<property name="logging.path" value="./"/>
<property name="logging.level" value="INFO"/>
<!-- 日志格式化:
%d:日期
%thread:线程名
%-5level:日志级别,从左显示5个字符宽度
%logger{50}:logger名字最长50个字符,超过的按照句点分割
%msg:日志消息
%n:换行符
%ex{full, DISPLAY_EX_EVAL}:异常信息,full表示全输出,可以替换为异常信息指定输出的行数
-->
<property name="message.format"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n%ex{full, DISPLAY_EX_EVAL}"/>
<!-- kafka topic -->
<property name="topic.name" value="log-collect"/>
<!-- 本地地址 -->
<property name="bootstrap.servers" value="192.168.1.71:9092"/>
<!-- 集群地址 -->
<!-- <property name="bootstrap.servers" value="hadoopdatanode1:9092,hadoopdatanode2:9092,hadoopdatanode3:9092"/> -->

<!-- appender种类:
ConsoleAppender:把日志添加到控制台
FileAppender:把日志添加到文件
RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。FileAppender的子类
-->

<!-- 控制台输出日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 自定义输出日志到文件 -->
<appender name="FileAppender" class="ch.qos.logback.core.FileAppender">
<!-- append:true,日志被追加到文件结尾;false,清空现存文件;默认是true -->
<append>true</append>
<!-- 级别过滤器:
ThresholdFilter:临界值过滤器,过滤掉低于指定临界值的日志
LevelFilter:级别过滤器,需配置onMatch和onMismatch
-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${logging.level}</level>
</filter>
<file>
${logging.path}/base.log
</file>
<encoder>
<pattern>${message.format}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 自定义异常输出日志文件 -->
<appender name="ErrorFileAppender" class="ch.qos.logback.core.FileAppender">
<append>true</append>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${logging.level}</level>
</filter>
<file>
${logging.path}/error-file.log
</file>
<encoder>
<pattern>${message.format}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 自定义输出日志:滚动记录日志 -->
<appender name="RollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<append>true</append>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${logging.level}</level>
</filter>
<!-- 滚动策略:每天生成一个日志文件,保存365天的日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件输出的文件名:按天回滚 daily -->
<FileNamePattern>${logging.path}/reaction-log-%d{yyyy-MM-dd HH:mm:ss.SSS}.log</FileNamePattern>
<!-- 日志文件保留天数 -->
<MaxHistory>365</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${message.format}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志文件最大的大小 -->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>50MB</MaxFileSize>
</triggeringPolicy>
</appender>

<!-- 输出日志到kafka,参考:https://github.com/danielwegener/logback-kafka-appender -->
<appender name="KafkaAppender" class="com.github.danielwegener.logback.kafka.KafkaAppender">
<encoder>
<pattern>${message.format}</pattern>
</encoder>
<topic>${topic.name}</topic>
<keyingStrategy class="com.github.danielwegener.logback.kafka.keying.NoKeyKeyingStrategy"/>
<deliveryStrategy class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy"/>
<!-- Optional parameter to use a fixed partition -->
<!-- <partition>0</partition> -->
<!-- Optional parameter to include log timestamps into the kafka message -->
<!-- <appendTimestamp>true</appendTimestamp> -->
<!-- each <producerConfig> translates to regular kafka-client config (format: key=value) -->
<!-- producer configs are documented here: https://kafka.apache.org/documentation.html#newproducerconfigs -->
<!-- bootstrap.servers is the only mandatory producerConfig -->
<producerConfig>bootstrap.servers=${bootstrap.servers}</producerConfig>
<!-- this is the fallback appender if kafka is not available. -->
<appender-ref ref="FileAppender"/>
</appender>

<!-- 异步输出日志
步骤:异步输出日志就是Logger.info负责往Queue(BlockingQueue)中放日志,然后再起个线程把Queue中的日志写到磁盘上
参考:https://blog.csdn.net/lkforce/article/details/76637071
-->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志。默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能。默认值为256 -->
<queueSize>100</queueSize>
<!-- 添加附加的appender,最多只能添加一个,此处指定后,在root下不要再指定该appender,否则会输出两次 -->
<appender-ref ref="KafkaAppender"/>
</appender>

<!--日志异步到数据库:未做测试,配置正确与否未知,先记录于此 -->
<!--<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
<dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
<driverClass>com.mysql.jdbc.Driver</driverClass>
<url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
<user>root</user>
<password>root</password>
</dataSource>
</connectionSource>
</appender>-->

<!-- 关闭指定包下的日志输出,name里面的内容可以是包路径,或者具体要忽略的文件名称 -->
<logger name="org.apache.flink" level="OFF"/>
<!-- 将指定包下指定级别的日志,输出到指定的appender中
addtivity:是否向上级logger传递打印信息。默认是true。若此包下的日志单独输出到文件中,应设置为false,否则在root日志也会记录一遍 -->
<logger name="org.apache.kafka" level="ERROR" addtivity="false">
<!-- 指定此包下的error级别信息,输出到指定的收集文件 -->
<appender-ref ref="ErrorFileAppender"/>
</logger>

<root level="${logging.level}">
<!--<appender-ref ref="STDOUT"/>-->
<!--<appender-ref ref="FileAppender"/>-->
<appender-ref ref="ASYNC"/>
</root>
</configuration>

根据实际情况,对 appender 进行取舍,实际使用时不要所有的都添加到 logback.xml 配置文件中。

本文参考

https://kucw.github.io/blog/2020/3/java-lombok/

https://juejin.cn/post/6844903641535479821

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

HDFS 常用 shell 命令

操作 HDFS 的 shell命令有三种:

  1. hadoop fs:适用于任何不同的文件系统,比如本地文件系统和 HDFS 文件系统。
  2. hadoop dfs:只适用于 HDFS 文件系统。
  3. hdfs dfs:只适用于 HDFS 文件系统。

官方不推荐使用第二种命令 hadoop dfs,有些 Hadoop 版本中已将这种命令弃用。

语法

1
hadoop fs [genericOptions] [commandOptions]

参数说明

HDFS 常用命令 说明
hadoop fs -ls 显示指定文件的详细信息
hadoop fs -cat 将指定文件的内容输出到标准输出
hadoop fs touchz 创建一个指定的空文件
hadoop fs -mkdir [-p] 创建指定的一个或多个文件夹,-p 选项用于递归创建
hadoop fs -cp 将文件从源路径复制到目标路径
hadoop fs -mv 将文件从源路径移动到目标路径
hadoop fs -rm 删除指定的文件,只删除非空目录和文件
hadoop fs -rm -r 删除指定的文件夹及其下的所有文件,-r 表示递归删除子目录
hadoop fs -chown 改变指定文件的所有者,该命令仅适用于超级用户
hadoop fs -chmod 将指定的文件权限更改为可执行文件,该命令仅适用于超级用户和文件所有者
hadoop fs -get 复制指定的文件到本地文件系统指定的文件或文件夹
hadoop fs -put 从本地文件系统中复制指定的单个或多个源文件到指定的目标文件系统
hadoop fs -moveFromLocal 与 -put 命令功能相同,但是文件上传结束后会删除源文件
hadoop fs -copyFromLocal 与 -put 命令功能相同,将本地源文件复制到路径指定的文件或文件夹中
hadoop fs -copyToLocal 与 -get命令功能相同,将目标文件复制到本地文件或文件夹中

hadoop网站:

https://xiaoxiaogua.github.io/2019/03/24/YARN-Scheduler/

https://blog.csdn.net/qq_26442553/article/details/117284107

https://blog.csdn.net/shudaqi2010/article/details/114528809

https://www.cnblogs.com/yinzhengjie/p/13383344.html

https://bbs.huaweicloud.com/blogs/218022

https://cloud.tencent.com/developer/article/1195056

kafka网站:

https://stackoverflow.com/questions/34188574/is-the-group-option-deprecated-from-kafka-console-consumer-tool-if-so-how-ca

https://blog.csdn.net/qq_29116427/article/details/80206125

flink网站:

https://www.jianshu.com/p/27fa3d590a62

https://zhuanlan.zhihu.com/p/50845911

https://blog.csdn.net/chentangdan2377/article/details/101000408

https://blog.csdn.net/L13763338360/article/details/110873662

https://blog.51cto.com/u_15080019/2653853

https://blog.csdn.net/weixin_33648811/article/details/112103174

https://cloud.tencent.com/developer/article/1500184

https://www.jianshu.com/p/aa00be723f23

https://www.cnblogs.com/gentlescholar/p/15044085.html

https://dragonlsl.blog.csdn.net/article/details/105823127

https://www.sohu.com/a/363674737_120342237

redis网站:

https://www.cnblogs.com/wei-zw/p/9163687.html

https://www.liaoxuefeng.com/wiki/1252599548343744/1282386499207201?luicode=10000011&lfid=1076031658384301&featurecode=newtitl&u=https%3A%2F%2Fwww.liaoxuefeng.com%2Fwiki%2F1252599548343744%2F1282386499207201

https://cloud.tencent.com/developer/article/1683498

线程池网站:

https://blog.csdn.net/u010235716/article/details/90059966

https://www.cnblogs.com/meijsuger/p/11492388.html

https://www.cnblogs.com/warehouse/p/10732965.html

SpringBoot:

https://ashiamd.github.io/docsify-notes/#/study/SpringBoot/README

线程池的理解

线程池是预先创建线程的一种技术,线程池在还没有任务到来之前,事先创建一定数量的线程,放入空闲队列中,然后对这些资源进行复用,从而减少频繁的创建和销毁对象。

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个 Runnable 对象或 Callable 对象传给线程池,线程池就会启动一个线程来执行它们的 run()call() 方法, 当 run()call() 方法执行结束后, 该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个 Runnable 对象的 run()call() 方法。

总结:由于系统创建和销毁线程都是需要时间和系统资源开销,为了提高性能,才考虑使用线程池。线程池会在系统启动时就创建大量的空闲线程,然后等待新的线程调用,线程执行结束并不会销毁,而是重新进入线程池,等待再次被调用。这样子就可以减少系统创建启动和销毁线程的时间,提高系统的性能。

线程池的使用

使用 Executors 创建线程池

Executor 是线程池的顶级接口,接口中只定义了一个方法 void execute(Runnable command);,线程池的操作方法都是定义在 ExecutorService 子接口中的,所以说 ExecutorService 是线程池真正的接口。

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

newFixedThreadPool

创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到是大值就会保持不变。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

1
2
3
4
5
6
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲 (60 秒不执行任务) 的线程。当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对钱程池大小做限制,线程池大小完全依赖于操作系统 (或者说 JVM) 能够创建的最大线程大小。

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

使用 ThreadPoolExecutor 创建线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

构造函数参数说明

corePoolSize:核心线程数大小,当线程数小于 corePoolSize 的时候,会创建线程执行 runnable。

maximumPoolSize:最大线程数, 当线程数大于等于 corePoolSize 的时候,会把 runnable 放入 workQueue 中。

keepAliveTime:保持存活时间,当线程数大于 corePoolSize 的时候,空闲线程能保持的最大时间。

unit:时间单位。

workQueue:保存任务的阻塞队列。

threadFactory:创建线程的工厂。

handler:拒绝策略。

任务执行顺序

  • 当线程数小于 corePoolSize 时,创建线程执行新任务。

  • 当线程数大于等于 corePoolSize,并且 workQueue 没有满时,新任务放入 workQueue 中。

  • 当线程数大于等于 corePoolSize,并且 workQueue 满时,新任务创建新线程运行,但线程总数要小于 maximumPoolSize。

  • 当线程总数等于 maximumPoolSize,并且 workQueue 满时,执行 handler 的 rejectedExecution,也就是拒绝策略。

img

阻塞队列

阻塞队列是一个在队列基础上又支持了两个附加操作的队列:

  1. 支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
  2. 支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。
阻塞队列的应用场景

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

阻塞队列的方法

在阻塞队列不可用的时候,上述两个附加操作提供了四种处理方法:

方法处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用
阻塞队列的类型

jdk 7 提供了 7 个阻塞队列,如下:

  1. ArrayBlockingQueue:数组结构组成的有界阻塞队列。

此队列按照先进先出 (FIFO) 的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平 (即先阻塞,先插入) 的。

  1. LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

此队列按照先出先进的原则对元素进行排序。

  1. PriorityBlockingQueue:支持优先级的无界阻塞队列。

  2. DelayQueue:支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。

  3. SynchronousQueue:不存储元素的阻塞队列,每一个 put 必须等待一个 take 操作,否则不能继续添加元素。并且他支持公平访问队列。

  4. LinkedTransferQueue:由链表结构组成的无界阻塞 TransferQueue 队列。

相对于其他阻塞队列,多了 tryTransfer 和 transfer 方法:

transfer方法:如果当前有消费者正在等待接收元素 (take 或者待时间限制的 poll 方法),transfer 可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的 tail 节点,并等到该元素被消费者消费了才返回。

tryTransfer方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回 false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回,而 transfer 方法是必须等到消费者消费了才返回。

  1. LinkedBlockingDeque:链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。

拒绝策略

当队列和线程池都满了,说明线程池处于饱和的状态,那么必须采取一种策略处理提交的新任务。ThreadPoolExecutor 默认有四个拒绝策略:

  • ThreadPoolExecutor.AbortPolicy():默认策略,直接抛出异常 RejectedExecutionException。

java.util.concurrent.RejectedExecutionException:

当线程池 ThreadPoolExecutor 执行方法 shutdown () 之后,再向线程池提交任务的时候,如果配置的拒绝策略是 AbortPolicy ,这个异常就会抛出来。

当设置的任务缓存队列过小的时候,或者说,线程池里面所有的线程都在干活(线程数等于 maxPoolSize),并且任务缓存队列也已经充满了等待的队列, 这个时候,再向它提交任务,也会抛出这个异常。

  • ThreadPoolExecutor.CallerRunsPolicy():直接调用 run () 方法并且阻塞执行。

  • ThreadPoolExecutor.DiscardPolicy():不处理,直接丢弃后来的任务。

  • ThreadPoolExecutor.DiscardOldestPolicy():丢弃在队列中队首的任务,并执行当前任务。

当然可以继承 RejectedExecutionHandler 来自定义拒绝策略。

线程池参数选择

CPU 密集型:线程池的大小推荐为 CPU 数量 +1。CPU 数量可以根据 Runtime.getRuntime().availableProcessors() 方法获取。

IO 密集型:CPU 数量 * CPU 利用率 * (1 + 线程等待时间 / 线程 CPU 时间)。

混合型:将任务分为 CPU 密集型和 IO 密集型,然后分别使用不同的线程池去处理,从而使每个线程池可以根据各自的工作负载来调整。

阻塞队列:推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生。

拒绝策略:默认采用的是 AbortPolicy 拒绝策略,直接在程序中抛出 RejectedExecutionException 异常,因为是运行时异常,不强制 catch,但这种处理方式不够优雅。处理拒绝策略有以下几种比较推荐:

  • 在程序中捕获 RejectedExecutionException 异常,在捕获异常中对任务进行处理。针对默认拒绝策略。
  • 使用 CallerRunsPolicy 拒绝策略,该策略会将任务交给调用 execute 的线程执行 (一般为主线程),此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在 TCP 队列中,TCP 队列满将会影响客户端,这是一种平缓的性能降低。
  • 自定义拒绝策略,只需要实现 RejectedExecutionHandler 接口即可。
  • 如果任务不是特别重要,使用 DiscardPolicy 和 DiscardOldestPolicy 拒绝策略将任务丢弃也是可以的。

如果使用 Executors 的静态方法创建 ThreadPoolExecutor 对象,可以通过使用 Semaphore 对任务的执行进行限流也可以避免出现 OOM 异常。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPoolExecutor {
public static void main(String[] args) {
long startTimeMillis = System.currentTimeMillis();

// 构造一个线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 6, 3,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3)
);

for (int i = 1; i <= 10; i++) {
try {
String task = "task = " + i;
System.out.println("创建任务并提交到线程池中:" + task);
threadPool.execute(new ThreadPoolTask(task));
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}

try {
// 等待所有线程执行完毕当前任务
threadPool.shutdown();

boolean loop = true;
do {
// 等待所有线程执行完毕,当前任务结束
loop = !threadPool.awaitTermination(2, TimeUnit.SECONDS);// 等待2秒
} while (loop);

if (!loop) {
System.out.println("所有线程执行完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("耗时:" + (System.currentTimeMillis() - startTimeMillis));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.Serializable;

public class ThreadPoolTask implements Runnable, Serializable {
private String attachData;

public ThreadPoolTask(String tasks) {
this.attachData = tasks;
}

public void run() {
try {
System.out.println("开始执行:" + attachData + "任务,使用的线程池,线程名称:"
+ Thread.currentThread().getName() + "\r\n");
} catch (Exception e) {
e.printStackTrace();
}
attachData = null;
}
}

运行结果,可以看到线程 pool-1-thread-1 到 pool-1-thread-5 循环使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
创建任务并提交到线程池中:task = 1
开始执行:task = 1任务,使用的线程池,线程名称:pool-1-thread-1

创建任务并提交到线程池中:task = 2
开始执行:task = 2任务,使用的线程池,线程名称:pool-1-thread-2

创建任务并提交到线程池中:task = 3
开始执行:task = 3任务,使用的线程池,线程名称:pool-1-thread-3

创建任务并提交到线程池中:task = 4
开始执行:task = 4任务,使用的线程池,线程名称:pool-1-thread-4

创建任务并提交到线程池中:task = 5
开始执行:task = 5任务,使用的线程池,线程名称:pool-1-thread-5

创建任务并提交到线程池中:task = 6
开始执行:task = 6任务,使用的线程池,线程名称:pool-1-thread-1

创建任务并提交到线程池中:task = 7
开始执行:task = 7任务,使用的线程池,线程名称:pool-1-thread-2

创建任务并提交到线程池中:task = 8
开始执行:task = 8任务,使用的线程池,线程名称:pool-1-thread-3

创建任务并提交到线程池中:task = 9
开始执行:task = 9任务,使用的线程池,线程名称:pool-1-thread-4

创建任务并提交到线程池中:task = 10
开始执行:task = 10任务,使用的线程池,线程名称:pool-1-thread-5

所有线程执行完毕
耗时:1014

本文参考

https://segmentfault.com/a/1190000011527245

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

String 的特性

String:字符串,使用双引号引起来表示。

  • String 是一个 final 类,不可被继承。

  • String 继承了 Serializable、Comparable 和 CharSequence 接口。

    1
    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}
    • 实现 Serializable 接口:表示字符串是支持可序列化的。
    • 实现 Comparable 接口 :表示 String 可以比较大小。
  • String 内部定义了 final char value[] 用于存储字符串数据。

  • String 代表不可变的字符序列 — 不可变性。 体现在:

    • 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。
    • 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值。
    • 当调用 String 的 replace() 修改原字符串中指定的字符或字符串时,也需要重新指定内存区域赋值。
  • 通过字面量的定义 (区别于 new) 方式给一个字符串赋值,此时的字符串值声明在字符串常量池中。

  • 字符串常量池中不会存储相同内容的字符串。

  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class Test {
    public static void main(String[] args) {
    String s1 = "abc";// 字面量的定义方式
    String s2 = "abc";
    System.out.println(s1 == s2);// true
    s1 = "hello";
    System.out.println(s1 == s2);// false
    System.out.println(s1);// hello
    System.out.println(s2);// abc

    System.out.println("**************************");
    String s3 = "abc";
    s3 += "def";
    System.out.println(s3);// abcdef
    System.out.println(s2);// abc ---> 原abc没变

    System.out.println("**************************");
    String s4 = "abc";
    String s5 = s4.replace("a", "m");
    System.out.println(s4);// abc ---> 原abc没变
    System.out.println(s5);// mbc
    }
    }
image-20210311165147346

String 对象的创建

  • 方式一:通过字面量定义的方式,此时的字符串数据声明在方法区中的字符串常量池中。

  • 方式二:通过 “new + 构造器” 的方式,此时变量保存的地址值,是字符串数据在堆空间中开辟空间以后所对应的地址值。

  • 常用的几种创建方式:

    image-20210311172851224
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    public class Test {
    public static void main(String[] args) {
    // 通过字面量定义的方式:此时的s1和s2的数据javaEE,声明在方法区中的字符串常量池中
    String s1 = "javaEE";
    String s2 = "javaEE";

    // 通过"new+构造器"的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后所对应的地址值
    String s3 = new String("javaEE");
    String s4 = new String("javaEE");

    System.out.println(s1 == s2);// true
    System.out.println(s1 == s3);// false
    System.out.println(s1 == s4);// false
    System.out.println(s3 == s4);// false

    System.out.println("*************************");
    Person p1 = new Person("Tom", 12);// 通过字面量定义的方式定义的name
    Person p2 = new Person("Tom", 12);
    System.out.println(p1.name.equals(p2.name));// true
    System.out.println(p1.name == p2.name);// true

    p1.name = "Jerry";
    System.out.println(p2.name);// Tom

    System.out.println("*************************");
    Person p3 = new Person(new String("Tom"), 12);// 通过"new+构造器"的方式定义的name
    Person p4 = new Person(new String("Tom"), 12);
    System.out.println(p3.name.equals(p4.name));// true
    System.out.println(p3.name == p4.name);// false
    }
    }

    class Person {
    String name;
    int age;

    public Person() {

    }

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

    内存解析说明:

    image-20210311173259265
  • 字符串初始化的过程:

    image-20210311194020782 image-20210311194118841
  • 字符串拼接:

    • 如果是常量与常量的拼接,则结果在常量池,且常量池中不会存在相同内容的常量。
    • 如果其中有一个是变量,则结果在堆中。
    • 如果拼接的结果调用 intern() 方法,则返回值在常量池中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class Test {
    public static void main(String[] args) {
    String s1 = "javaEE";
    String s2 = "hadoop";
    String s3 = "javaEEhadoop";
    String s4 = "javaEE" + "hadoop";
    String s5 = s1 + "hadoop";
    String s6 = "javaEE" + s2;
    String s7 = s1 + s2;
    String s8 = (s1 + s2).intern();

    System.out.println(s3 == s4);// true
    System.out.println(s3 == s5);// false
    System.out.println(s3 == s6);// false
    System.out.println(s3 == s7);// false
    System.out.println(s3 == s8);// true
    System.out.println(s5 == s6);// false
    System.out.println(s5 == s7);// false
    System.out.println(s5 == s8);// false
    System.out.println(s6 == s7);// false
    System.out.println(s6 == s8);// false
    System.out.println(s7 == s8);// false
    }
    }

    内存解析说明:

    image-20210311212720801

    特殊的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "javaEEhadoop";
    String s2 = "javaEE";
    String s3 = s2 + "hadoop";
    System.out.println(s1 == s3);// false
    final String s4 = "javaEE";
    String s5 = s4 + "hadoop";
    System.out.println(s1 == s5);// true

    System.out.println(s2 == s4);// true
    }
    }

    s4 变量被 final 修饰,实际上也就是常量,等同于 s2。

  • 面试题

    • String s = new String("abc"); 方式创建对象,在内存中创建了几个对象?

      • 两个:一个是堆空间中 new 的结构,另一个是 char[] 对应的常量池中的数据 “abc”。
    • String str1 = "abc";String str2 = new String("abc"); 的区别?

      • 字符串常量存储在字符串常量池,目的是共享。
      • 字符串非常量对象存储在堆中。
      image-20210311194809127
    • 下面程序的运行结果是:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public class StringTest {
      String str = new String("good");
      char[] ch = {'t', 'e', 's', 't'};

      public void change(String str, char ch[]) {
      str = "test ok";
      ch[0] = 'b';
      }

      public static void main(String[] args) {
      StringTest ex = new StringTest();
      ex.change(ex.str, ex.ch);
      System.out.println(ex.str);// good
      System.out.println(ex.ch);// best
      }
      }

      值传递机制和 String 的不可变性。

String 的常用方法

  • int length():返回字符串的长度,return value.length

  • char charAt(int index):返回某索引处的字符,return value[index]

  • boolean isEmpty():判断是否是空字符串,return value.length == 0

  • String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    String s2 = s1.toLowerCase();// s1不可变,仍未原来的字符串
    System.out.println(s2);// helloworld
    }
    }
  • String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    String s2 = s1.toUpperCase();// s1不可变,仍未原来的字符串
    System.out.println(s2);// HELLOWORLD
    }
    }
  • String trim():返回字符串的副本,忽略前导空白和尾部空白。

    1
    2
    3
    4
    5
    6
    7
    8
    public class StringTest {
    public static void main(String[] args) {
    String s1 = " he llo world ";
    String s2 = s1.trim();
    System.out.println("---" + s1 + "---");// --- he llo world ---
    System.out.println("---" + s2 + "---");// ---he llo world---
    }
    }
  • boolean equals(Object obj):比较字符串的内容是否相同。

  • boolean equalsIgnoreCase(String anotherString):比较字符串的内容是否相同,忽略大小写。

  • String concat(String str):将指定字符串连接到此字符串的结尾,等价于用 “+”。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "abc";
    String s2 = s1.concat("def");
    System.out.println(s2);// abcdef
    }
    }
  • int compareTo(String anotherString):比较两个字符串的大小。

  • String substring(int beginIndex):返回一个新的字符串,截取当前字符串从 beginIndex 开始到最后的一个子字符串。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    String s2 = s1.substring(2);
    System.out.println(s2);// lloWorld
    }
    }
  • String substring(int beginIndex, int endIndex):返回一个新字符串,截取当前字符串从 beginIndex 开始到 endIndex (不包含) 结束的一个子字符串 — 左闭右开,[beginIndex, endIndex)

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    String s2 = s1.substring(2, 6);
    System.out.println(s2);// lloW
    }
    }
  • boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.endsWith("ld"));// true
    }
    }
  • boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.startsWith("ll"));// false
    }
    }
  • boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.startsWith("ll", 2));// true
    }
    }
  • boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.contains("wo"));// false
    }
    }
  • int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引,未找到返回 -1。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.indexOf("lo"));// 3
    }
    }
  • int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始,未找到返回 -1。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "HelloWorld";
    System.out.println(s1.indexOf("lo", 5));// -1
    }
    }
  • int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引,未找到返回 -1。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "hellorworld";
    System.out.println(s1.lastIndexOf("or"));// 7
    }
    }

    面试题:什么情况下,indexOf(str)lastIndexOf(str) 返回值相同?

    情况一:存在唯一的一个 str。情况二:不存在 str。

  • int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索,未找到返回 -1。

    1
    2
    3
    4
    5
    6
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "hellorworld";
    System.out.println(s1.lastIndexOf("or", 6));// 4
    }
    }
  • String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "hellorworld";
    String s2 = s1.replace("o", "D");
    System.out.println(s2);// hellDrwDrld
    }
    }
  • String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

    1
    2
    3
    4
    5
    6
    7
    public class StringTest {
    public static void main(String[] args) {
    String s1 = "hellorworld";
    String s2 = s1.replace("or", "DE");
    System.out.println(s2);// hellDEwDEld
    }
    }
  • String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class StringTest {
    public static void main(String[] args) {
    String str = "12hello34world5java7891mysql456";
    // 把字符串中的数字替换成,,如果结果中开头和结尾有,的话去掉
    String s1 = str.replaceAll("\\d+", ",");
    System.out.println(s1);// ,hello,world,java,mysql,
    String s2 = s1.replaceAll("^,|,$", "");
    System.out.println(s2);// hello,world,java,mysql
    }
    }
  • String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    public class StringTest {
    public static void main(String[] args) {
    String str = "12hello34world5java7891mysql456";
    // 把字符串中的数字替换成,,如果结果中开头和结尾有,的话去掉
    String s1 = str.replaceFirst("\\d+", ",");
    System.out.println(s1);// ,hello34world5java7891mysql456
    }
    }
  • boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class StringTest {
    public static void main(String[] args) {
    String str = "1234d5";
    // 判断str字符串中是否全部由数字组成,即有1-n个数字组成
    boolean matches = str.matches("\\d+");
    System.out.println(matches);// false
    String tel = "0571-4534289";
    // 判断这是否是一个杭州的固定电话
    boolean result = tel.matches("0571-\\d{7,8}");
    System.out.println(result);// true
    }
    }
  • String[] split(String regex):根据匹配给定的正则表达式来拆分此字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class StringTest {
    public static void main(String[] args) {
    String str = "hello|world|java";
    String[] strs = str.split("\\|");
    for (String value : strs) {
    System.out.println(value);
    }
    System.out.println();
    String str2 = "hello.world.java";
    String[] strs2 = str2.split("\\.");
    for (String s : strs2) {
    System.out.println(s);
    }
    System.out.println();
    String str3 = "hello-world-java";
    String[] strs3 = str3.split("-");
    for (String s : strs3) {
    System.out.println(s);
    }
    }
    }
    输出结果:
    hello
    world
    java

    hello
    world
    java

    hello
    world
    java
  • String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部都放到最后一个元素中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class StringTest {
    public static void main(String[] args) {
    String str = "hello|world|java";
    String[] strs = str.split("\\|",2);
    for (String value : strs) {
    System.out.println(value);
    }
    }
    }
    输出结果:
    hello
    world|java
  • substring()indexOf() 结合使用:

    • 截取第二个 “-“ 之前的字符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Test {
      public static void main(String[] args) {
      String s = "application-2005-US20050154023A1-20050714";
      String r = s.substring(0, s.indexOf("-", s.indexOf("-") + 1));
      System.out.println(r);
      }
      }
      输出结果:
      application-2005
    • 截取第二个 “-“ 之后的字符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Test {
      public static void main(String[] args) {
      String s = "application-2005-US20050154023A1-20050714";
      String r = s.substring(s.indexOf("-", s.indexOf("-") + 1) + 1);
      System.out.println(r);
      }
      }
      输出结果:
      US20050154023A1-20050714
    • 截取倒数第二个 “-“ 之前的字符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Test {
      public static void main(String[] args) {
      String s = "application-2005-US20050154023A1-20050714";
      String r = s.substring(0, s.lastIndexOf("-", s.lastIndexOf("-") - 1));
      System.out.println(r);
      }
      }
      输出结果:
      application-2005
    • 截取倒数第二个 “-“ 之后的字符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class Test {
      public static void main(String[] args) {
      String s = "application-2005-US20050154023A1-20050714";
      String r = s.substring(s.lastIndexOf("-", s.lastIndexOf("-") - 1) + 1);
      System.out.println(r);
      }
      }
      输出结果:
      US20050154023A1-20050714

String 与其他结构之间的转换

String 与基本数据类型/包装类之间的转换

  • 字符串转换为基本数据类型/包装类
    • Integer 包装类的 public static int parseInt(String s):可以将由 “数字” 字符组成的字符串,转换为整型。
    • 类似地,使用 java.lang 包中的 Byte、Short、Long、Float、Double 类调相应的类方法可以将由 “数字” 字符组成的字符串,转换为相应的基本数据类型。
  • 基本数据类型/包装类转换为字符串
    • 调用 String 类的 public String valueOf(int n) 可将 int 型转换为字符串。
    • 相应的 valueOf(byte b)valueOf(long l)valueOf(float f)valueOf(doubled)valueOf(boolean b) 可将参数的相应类型转换为字符串。

String 与字符数组 (char[]) 之间的转换

  • 字符串转换为字符数组

    • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class StringTest {
      public static void main(String[] args) {
      String str = "hello|world|java";
      char[] chars = str.toCharArray();
      for (char c : chars) {
      System.out.println(c);
      }
      }
      }
    • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

  • 字符数组转换为字符串

    • String 类的构造器:String(char[])String(char[], int offset, intlength) 分别用字符数组中的全部字符和部分字符创建字符串对象。

      1
      2
      3
      4
      5
      6
      7
      public class StringTest {
      public static void main(String[] args) {
      char[] arr = new char[]{'a', 'b', 'c'};
      String str = new String(arr);
      System.out.println(str);
      }
      }

String 与字节数组 (byte[]) 之间的转换

  • 字符串转换为字节数组

    • 编码:String —> byte[],字符串 —> 字节,看得懂的 —> 看不懂的二进制数据。
    • public byte[] getBytes():使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到新的 byte 数组中。
    • public byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到新的 byte 数组中。
  • 字节数组转换为字符串

    • 解码:byte[] —> String,字节 —> 字符串,看不懂的二进制数据 —> 看得懂的。编码的逆过程。
    • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
    • String(byte[] ,int offset ,int length): :用指定的字节数组的一部分,即从数组起始位置 offset 开始,取 length 个字节构造一个字符串对象。
  • 实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class StringTest {
    public static void main(String[] args) {
    String str1 = "abc123ABC中国";
    byte[] bytes = str1.getBytes();// 使用默认的字符集进行编码,此处是UTF-8
    System.out.println(Arrays.toString(bytes));// [97, 98, 99, 49, 50, 51, 65, 66, 67, -28, -72, -83, -27, -101, -67]

    byte[] gbks = null;
    try {
    gbks = str1.getBytes("GBK");// 使用GBK进行编码
    System.out.println(Arrays.toString(gbks));// [97, 98, 99, 49, 50, 51, 65, 66, 67, -42, -48, -71, -6]
    } catch (UnsupportedEncodingException e) {
    e.printStackTrace();
    }

    System.out.println("*********************************");
    String str2 = new String(bytes);// 使用默认的字符集进行解码,此处是UTF-8
    System.out.println(str2);// abc123ABC中国

    String str4 = new String(gbks);
    System.out.println(str4);// abc123ABC�й�,出现乱码,原因:编码集和解码集不一致

    try {
    String gbk = new String(gbks, "GBK");
    System.out.println(gbk);// abc123ABC中国,因为编码集和解码集一致,所以不会出现乱码
    } catch (UnsupportedEncodingException e) {
    e.printStackTrace();
    }
    }
    }

    解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。

String 相关的算法题目

  • 模拟一个 trim 方法,去除字符串两端的空格。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class Test {
    public static String myTrim(String str) {
    if (str != null) {
    // 用于记录从前往后首次索引位置不是空格的位置的索引
    int start = 0;
    // 用于记录从后往前首次索引位置不是空格的位置的索引
    int end = str.length() - 1;

    while (start < end && str.charAt(start) == ' ') {
    start++;
    }

    while (start < end && str.charAt(end) == ' ') {
    end--;
    }
    if (str.charAt(start) == ' ') {
    return "";
    }

    return str.substring(start, end + 1);
    }
    return null;
    }

    public static void main(String[] args) {
    String s = myTrim(" abc 123 d ");
    System.out.println(s);
    }
    }
  • 将一个字符串中指定部分进行反转。比如 “abcdefg” 反转为 “abfedcg”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Test {
    // 方式一:
    public static String reverse1(String str, int start, int end) {
    if (str != null) {
    char[] charArray = str.toCharArray();
    for (int i = start, j = end; i < j; i++, j--) {
    char temp = charArray[i];
    charArray[i] = charArray[j];
    charArray[j] = temp;
    }
    return new String(charArray);
    }
    return null;
    }

    // 方式二:
    public static String reverse2(String str, int start, int end) {
    String newStr = str.substring(0, start);
    for (int i = end; i >= start; i--) {
    newStr += str.charAt(i);
    }
    newStr += str.substring(end + 1);
    return newStr;
    }

    // 方式三:推荐(相较于方式二做的改进)
    public static String reverse3(String str, int start, int end) {
    StringBuilder newStr = new StringBuilder(str.length());
    newStr.append(str, 0, start);
    for (int i = end; i >= start; i--) {
    newStr.append(str.charAt(i));
    }
    newStr.append(str.substring(end + 1));
    return newStr.toString();

    }

    public static void main(String[] args) {
    String str = "abcdefg";
    String str1 = reverse3(str, 2, 5);
    System.out.println(str1);// abfedcg
    }
    }
  • 获取一个字符串在另一个字符串中出现的次数。比如:获取 “ab” 在 “abkkcadkabkebfkabkskab” 中出现的次数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public class Test {
    public static int getCount(String mainStr, String subStr) {
    if (mainStr.length() >= subStr.length()) {
    int count = 0;
    int index = 0;
    /*// 方式一:
    while ((index = mainStr.indexOf(subStr)) != -1) {
    count++;
    mainStr = mainStr.substring(index + subStr.length());
    }*/
    // 改进:
    while ((index = mainStr.indexOf(subStr, index)) != -1) {
    index += subStr.length();
    count++;
    }
    return count;
    }
    return 0;
    }

    public static void main(String[] args) {
    String str1 = "cdabkkcadkabkebfkabkskab";
    String str2 = "ab";
    int count = getCount(str1, str2);
    System.out.println(count);
    }
    }
  • 获取两个字符串中最大相同子串。比如:str1 = "abcwerthelloyuiodef"; str2 = "cvhellobnm";。提示:将短的那个串进行长度依次递减的子串与较长的串比较。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    public class Test {
    // 如果只存在一个最大长度的相同子串
    public static String getMaxSameSubString(String str1, String str2) {
    if (str1 != null && str2 != null) {
    String maxStr = (str1.length() > str2.length()) ? str1 : str2;
    String minStr = (str1.length() > str2.length()) ? str2 : str1;
    int len = minStr.length();
    for (int i = 0; i < len; i++) {// 此层循环决定要去几个字符
    for (int x = 0, y = len - i; y <= len; x++, y++) {
    if (maxStr.contains(minStr.substring(x, y))) {
    return minStr.substring(x, y);
    }
    }
    }
    }
    return null;
    }

    // 如果存在多个长度相同的最大相同子串
    // 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
    public static String[] getMaxSameSubString1(String str1, String str2) {
    if (str1 != null && str2 != null) {
    StringBuilder strs = new StringBuilder();
    String maxString = (str1.length() > str2.length()) ? str1 : str2;
    String minString = (str1.length() > str2.length()) ? str2 : str1;
    int len = minString.length();
    for (int i = 0; i < len; i++) {
    for (int x = 0, y = len - i; y <= len; x++, y++) {
    String subString = minString.substring(x, y);
    if (maxString.contains(subString)) {
    strs.append(subString).append(",");
    }
    }
    if (strs.length() != 0) {
    break;
    }
    }
    return strs.toString().replaceAll(",$", "").split(",");
    }
    return null;
    }

    // 如果存在多个长度相同的最大相同子串:使用ArrayList
    public static List<String> getMaxSameSubString2(String str1, String str2) {
    if (str1 != null && str2 != null) {
    List<String> list = new ArrayList<>();
    String maxString = (str1.length() > str2.length()) ? str1 : str2;
    String minString = (str1.length() > str2.length()) ? str2 : str1;
    int len = minString.length();
    for (int i = 0; i < len; i++) {
    for (int x = 0, y = len - i; y <= len; x++, y++) {
    String subString = minString.substring(x, y);
    if (maxString.contains(subString)) {
    list.add(subString);
    }
    }
    if (list.size() != 0) {
    break;
    }
    }
    return list;
    }
    return null;
    }

    public static void main(String[] args) {
    String str1 = "abcwerthelloyuiodef";
    String str2 = "cvhellobnmiodef";
    String[] strs = getMaxSameSubString1(str1, str2);
    System.out.println(Arrays.toString(strs));
    }
    }
  • 对字符串中的字符进行自然顺序排序。提示:① 字符串变成字符数组;② 对数组排序,选择,冒泡,Arrays.sort();;③ 将排序后的数组变成字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test {
    public static void main(String[] args) {
    String str = "abcwerthelloyuiodef";
    char[] arr = str.toCharArray();
    Arrays.sort(arr);
    String newStr = new String(arr);
    System.out.println(newStr);
    }
    }

String、StringBuffer 和 StringBuilder

  • java.lang.StringBuffer 代表可变的字符序列,jdk 1.0 中声明,可以对字符串内容进行增删,此时不会产生新的对象。作为参数传递时,方法内部可以改变值。

    1
    public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {}

    java.lang.StringBuilderjava.lang.StringBuffer 非常类似,也代表可变的字符序列,二者提供相关功能的方法也比较类似。

    1
    public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {}
    image-20210313133051803
  • StringBuffer 类和 StringBuilder 类不同于 String,其对象必须使用构造器生成。常用以下三个构造器:

    • StringBuffer()/StringBuilder():初始容量为 16 的字符串缓冲区。

      1
      2
      3
      4
      5
      6
      7
      /**
      * Constructs a string buffer with no characters in it and an
      * initial capacity of 16 characters.
      */
      public StringBuffer() {
      super(16);
      }
      1
      2
      3
      4
      5
      6
      7
      /**
      * Constructs a string builder with no characters in it and an
      * initial capacity of 16 characters.
      */
      public StringBuilder() {
      super(16);
      }
    • StringBuffer(int capacity)/StringBuilder(int capacity):构造指定容量的字符串缓冲区。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /**
      * Constructs a string buffer with no characters in it and
      * the specified initial capacity.
      *
      * @param capacity the initial capacity.
      * @exception NegativeArraySizeException if the {@code capacity}
      * argument is less than {@code 0}.
      */
      public StringBuffer(int capacity) {
      super(capacity);
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /**
      * Constructs a string builder with no characters in it and an
      * initial capacity specified by the {@code capacity} argument.
      *
      * @param capacity the initial capacity.
      * @throws NegativeArraySizeException if the {@code capacity}
      * argument is less than {@code 0}.
      */
      public StringBuilder(int capacity) {
      super(capacity);
      }
    • StringBuffer(String str)/StringBuilder(String str):将内容初始化为指定字符串内容。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /**
      * Constructs a string buffer initialized to the contents of the
      * specified string. The initial capacity of the string buffer is
      * {@code 16} plus the length of the string argument.
      *
      * @param str the initial contents of the buffer.
      */
      public StringBuffer(String str) {
      super(str.length() + 16);
      append(str);
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /**
      * Constructs a string builder initialized to the contents of the
      * specified string. The initial capacity of the string builder is
      * {@code 16} plus the length of the string argument.
      *
      * @param str the initial contents of the buffer.
      */
      public StringBuilder(String str) {
      super(str.length() + 16);
      append(str);
      }
  • StringBuffer 类的常用方法,很多方法与 String 相同,StringBuilder 类的常用方法参考 StringBuffer。

    • StringBuffer 类和 StringBuilder 类的方法的主要区别,举例如下:

      • StringBuffer 类 — 同步方法:

        1
        2
        3
        4
        5
        6
        @Override
        public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
        }
      • StringBuilder 类 — 非同步方法:

        1
        2
        3
        4
        5
        @Override
        public StringBuilder append(String str) {
        super.append(str);
        return this;
        }
    • StringBuffer append(xxx):提供了参数可为多种类型的 append() 方法,用于进行字符串拼接。

      image-20210313150854764

      面试题,输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public class ExceptionTest {
      public static void main(String[] args) {
      String str = null;
      StringBuilder sb = new StringBuilder();
      System.out.println(sb);
      sb.append(str);
      System.out.println(sb.length());
      System.out.println(sb);
      StringBuilder sb1 = new StringBuilder(str);
      System.out.println(sb1);
      }
      }
      输出结果:

      4
      null
      Exception in thread "main" java.lang.NullPointerException
      at java.lang.StringBuilder.<init>(StringBuilder.java:112)
      at cn.xisun.java.base.ExceptionTest.main(ExceptionTest.java:17)

      原因:

      ① java 里面,null 不占字节。如果一个引用指向 null,该应用就不再指向堆内存中的任何对象。并且,这个对象引用的大小是 4 个字节。

      append() 方法如果传入 null 参数,最终执行以下方法,因此上面第 7 行和第 8 行输出结果为 4 和 null (字符串 null)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      private AbstractStringBuilder appendNull() {
      int c = count;
      ensureCapacityInternal(c + 4);
      final char[] value = this.value;
      value[c++] = 'n';
      value[c++] = 'u';
      value[c++] = 'l';
      value[c++] = 'l';
      count = c;
      return this;
      }

      new StringBuilder(str) 的代码中:super(str.length() + 16);,调用 null 的 length() 方法,会发生空指针异常。

    • StringBuffer delete(int start,int end):删除指定位置的内容。

    • StringBuffer replace(int start, int end, String str):把 [start,end) 位置替换为 str。

    • StringBuffer insert(int offset, xxx):在指定位置插入多种类型的参数。

      image-20210313151452492

      1
      2
      3
      4
      5
      6
      7
      public class Test {
      public static void main(String[] args) {
      StringBuffer sb = new StringBuffer("abc123");
      sb.insert(3, "ABC");
      System.out.println(sb);// abcABC123
      }
      }
    • StringBuffer reverse():把当前字符序列逆转。

      1
      2
      3
      4
      5
      6
      7
      public class Test {
      public static void main(String[] args) {
      StringBuffer sb = new StringBuffer("abc123");
      sb.reverse();
      System.out.println(sb);// 321cba
      }
      }
    • int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引,未找到返回 -1。

    • String substring(int start):返回一个新的字符串,截取当前字符串从 start 开始到最后的一个子字符串。

    • String substring(int start,int end):返回一个新字符串,截取当前字符串从 start 开始到 end (不包含) 结束的一个子字符串 — 左闭右开,[start, end)

      1
      2
      3
      4
      5
      6
      7
      public class Test {
      public static void main(String[] args) {
      StringBuffer sb = new StringBuffer("abc123abc");
      String sb2 = sb.substring(1, 5);
      System.out.println(sb2);// bc12
      }
      }
    • int length():返回字符串的长度。

      1
      2
      3
      4
      @Override
      public int length() {
      return count;
      }
    • char charAt(int n ):返回某索引处的字符。

    • void setCharAt(int n ,char ch):在指定索引处插入字符。

      1
      2
      3
      4
      5
      6
      7
      public class Test {
      public static void main(String[] args) {
      StringBuffer sb = new StringBuffer("abc123abc");
      sb.setCharAt(1, '中');
      System.out.println(sb);// a中c123abc
      }
      }
    • 方法总结:

      • 增:append,删:delete,改:setCharAt/replace,查:charAt,插:insert,长度:length,遍历:for + charAt/toString。

      • append、delete、replace、insert 和 reverse 这些方法,支持方法链操作,方法链的原理为:

        1615717860(1)
  • String、StringBuffer 和 StringBuilder 的异同?

    • String:Since jdk 1.0,不可变的字符序列;底层使用 final char[] 存储。
    • StringBuffer:Since jdk 1.0,可变的字符序列;线程安全的,效率低;底层使用 char[] 存储。
    • StringBuilder:Since jdk 1.5,可变的字符序列;线程不安全的,效率高;底层使用 char[] 存储。
  • StringBuffer 和 StringBuilder 的扩容问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Test {
    public static void main(String[] args) {
    String str = new String();// final char[] value = new char[0];

    String str1 = new String("abc");// final char[] value = new char[]{'a', 'b', 'c'};

    StringBuffer sb = new StringBuffer();// char[] value = new char[16]; 底层创建了一个长度是16的char数组
    System.out.println(sb.length());// 0
    sb.append('a');// value[0] = 'a';
    sb.append('b');// value[1] = 'b';
    sb.append('c');// value[1] = 'c';
    System.out.println(sb.length());// 3

    StringBuffer sb1 = new StringBuffer("abc");// char[] value = new char["abc".length() + 16];
    System.out.println(sb1.length());// 3
    }
    }

    扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。

    扩容方法:

    1
    2
    3
    4
    5
    6
    7
    private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
    value = Arrays.copyOf(value,
    newCapacity(minimumCapacity));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * Returns a capacity at least as large as the given minimum capacity.
    * Returns the current capacity increased by the same amount + 2 if
    * that suffices.
    * Will not return a capacity greater than {@code MAX_ARRAY_SIZE}
    * unless the given minimum capacity is greater than that.
    *
    * @param minCapacity the desired minimum capacity
    * @throws OutOfMemoryError if minCapacity is less than zero or
    * greater than Integer.MAX_VALUE
    */
    private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
    newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
    ? hugeCapacity(minCapacity)
    : newCapacity;
    }

    默认情况下,扩容为原来容量的 2 倍 + 2,同时将原来数组中的元素复制到新的数组中。

    指导意义:开发中,如果知道创建的字符串的长度,建议使用 StringBuffer(int capacity)StringBuilder(int capacity),即可能得避免扩容的发生,这样可以提高效率。

  • String、StringBuffer 和 StringBuilder 三者的效率测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public class Test {
    public static void main(String[] args) {
    // 初始设置
    String text = "";
    StringBuffer buffer = new StringBuffer("");
    StringBuilder builder = new StringBuilder("");
    long startTime = 0L;
    long endTime = 0L;

    // 开始对比
    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
    text = text + i;
    }
    endTime = System.currentTimeMillis();
    System.out.println("String的执行时间:" + (endTime - startTime));

    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
    buffer.append(String.valueOf(i));
    }
    endTime = System.currentTimeMillis();
    System.out.println("StringBuffer的执行时间:" + (endTime - startTime));

    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
    builder.append(String.valueOf(i));
    }
    endTime = System.currentTimeMillis();
    System.out.println("StringBuilder的执行时间:" + (endTime - startTime));
    }
    }
    输出结果:
    StringBuffer的执行时间:6
    StringBuilder的执行时间:3
    String的执行时间:1713

    效率从高到低排列:StringBuilder > StringBuffer > String。

本文参考

https://www.gulixueyuan.com/goods/show/203?targetId=309&preview=0

声明:写作本文初衷是个人学习记录,鉴于本人学识有限,如有侵权或不当之处,请联系 wdshfut@163.com

查询当前库 key 的个数

info可以看到所有库的key数量

dbsize则是当前库key的数量

keys *这种数据量小还可以,大的时候可以直接搞死生产环境。

dbsize和keys *统计的key数可能是不一样的,如果没记错的话,keys *统计的是当前db有效的key,而dbsize统计的是所有未被销毁的key(有效和未被销毁是不一样的,具体可以了解redis的过期策略)