Java语言详解

一、程序与编码

1、机器数和真值

1. 机器数

一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机中用一个数的最高位存放符号,正数为 0、负数为 1。

比如,十进制中的数 +3 ,若计算机字长为 8 位,则转换成二进制就是 00000011。如果是 -3 ,就是 10000011 。那么,这里的 00000011 和 10000011 就是机器数。

2. 真值

因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位 1 代表负,其真正数值是 -3 而不是形式值 131(10000011 转换成十进制等于 131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。

例:0000 0001 的真值 = +000 0001 = +1,1000 0001 的真值 = -000 0001 = -1

2、原码、反码和补码

1. 原码

原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值,比如 8 位二进制:

[+1]原码 = 0000 0001

[-1]原码 = 1000 0001

因为第一位是符号位,所以8位二进制数的取值范围就是:[1111 1111 , 0111 1111],即:[-127 , 127] 。

原码是人脑最容易理解和计算的表示方式。

2. 反码

反码的表示方法是:正数的反码是其本身;而负数的反码是在其原码的基础上,符号位不变,其余各个位取反。

[+1] = [00000001] 原 = [00000001] 反

[-1] = [10000001] 原 = [11111110] 反

可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值,通常要将其转换成原码再计算。

3. 补码

补码的表示方法是:正数的补码就是其本身;负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后 +1(即在反码的基础上 +1)。

[+1] = [00000001] 原 = [00000001] 反 = [00000001] 补

[-1] = [10000001] 原 = [11111110] 反 = [11111111] 补

对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成原码在计算其数值。

4. 原码、反码和补码的作用

现在我们知道了计算机可以有三种编码方式表示一个数。对于正数,三种编码方式的结果都相同。例:

[+1] = [00000001] 原 = [00000001] 反 = [00000001] 补

而对于负数,其原码,反码和补码则都不相同。例:

[-1] = [10000001] 原 = [11111110] 反 = [11111111] 补

1)既然原码才是被人脑直接识别并用于计算表示方式,为何还会有反码和补码呢?

首先,因为人脑可以知道第一位是符号位,在计算的时候我们会根据符号位,选择对真值区域的加减(真值的概念在本文最开头)。但是对于计算机,加减乘除已经是最基础的运算,要设计得尽量简单。要辨别“符号位”显然会让计算机的基础电路设计变得十分复杂,于是人们想出了将符号位也参与运算的方法。根据运算法则,减去一个正数等于加上一个负数,即: 1-1 = 1 + (-1) = 0 ,所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。

于是人们开始探索将符号位参与运算,并且只保留加法的方法。以下以十进制计算表达式 1 - 1 = 0 为例。

首先来看原码:

1 - 1 = 1 + (-1) = [00000001] 原 + [10000001] 原 = [10000010] 原 = - 2

如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。

为了解决原码做减法的问题,出现了反码:

1 - 1 = 1 + (-1) 

= [0000 0001] 原 + [1000 0001] 原 

= [0000 0001] 反 + [1111 1110] 反 

= [1111 1111] 反

= [1000 0000] 把最终的反码结果转换回原码 

= -0

发现用反码计算减法,结果的真值部分是正确的。而唯一的问题其实就出现在“0”这个特殊的数值上。虽然人们理解上 +0 和 -0 是一样的,但是 0 带符号是没有任何意义的。而且会有 [0000 0000]原 和 [1000 0000]原 两个编码表示 0。

于是补码的出现,解决了 0 的符号以及两个编码的问题:

1 - 1 = 1 + (-1) 

= [0000 0001] 原 + [1000 0001] 原 

= [0000 0001] 补 + [1111 1111] 补 

= [0000 0000] 补

= [0000 0000] 原

这样 0 用 [0000 0000] 表示,而以前出现问题的 -0 则不存在了。

2)使用补码,不仅仅修复了 0 的符号以及存在两个编码的问题,而且还能够多表示一个最低数。

(-1) + (-127)

= [1000 0001] 原 + [1111 1111] 原 

= [1111 1111] 补 + [1000 0001] 补 

= [1000 0000] 补

-1 - 127 的结果应该是 -128,在用补码运算的结果中,[1000 0000]补 就是 -128。但是注意因为实际上是使用以前的 -0 的补码来表示 -128,所以 -128 并没有原码和反码表示(对 -128 的补码表示 [1000 0000]补 算出来的原码是 [0000 0000]原,这是不正确的)。

这就是为什么8位二进制,使用原码或反码表示的范围为 [-127, +127],而使用补码表示的范围是 [-128, 127]。

因为机器使用补码,所以对于编程中常用到的 32 位 int 类型,可以表示范围是 [-2^31,2^31-1],因为第一位表示的是符号位,使用补码表示时又可以多保存一个最小值。

3、进制转换

1. 十进制转二进制

1)十进制整数转二进制整数

十进制整数转换为二进制整数采用“除 2 反向取余”法。具体做法是:使用“短除法”,用 2 整除十进制整数,可以得到一个商和余数;再用 2 去除商,又会得到一个商和余数;如此循环进行,直到商为 0 时为止,然后从下向上读取每一次的余数。

如:将 789 转换为二进制

789 / 2 = 394 …… 1
394 / 2 = 197 …… 0
197 / 2 = 98 …… 1
98 / 2 = 49 …… 0
49 / 2 = 24 …… 1
24 / 2 = 12 …… 0
12 / 2 = 6 …… 0
6 / 2 = 3 …… 0
3 / 2 = 1 …… 1
1 / 2 = 0 …… 1

从下向上读取每一次的余数,把它们连接为字符串,就是答案:789 = 1100010101(B)

2)十进制小数转二进制小数

十进制小数转换成二进制小数采用“乘 2 取整,顺序排列”法。具体做法是:用 2 乘十进制小数,可以得到积,将积的整数部分取出,再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时 0 或 1 为二进制的最后一位。或者达到所要求的精度为止。

然后把取出的整数部分按顺序排列起来,先取的整数作为二进制小数的高位有效位,后取的整数作为低位有效位。

程序实现:

# 读取一个十进制数字
while 1:
    try:
        num = float(input("请输入一个十进制的数字:"))
        break
    except:
        continue

# 整数部分的计算
int_result = 0
# 如果整数部分为0,无需继续计算
if int(num) == 0:
    pass
else:
    int_part = int(str(num)[:str(num).find(".")])
    # 用于存储每一次计算的余数
    int_result = []
    # 开始循环除以2
    while int_part > 0:
        int_part, remainder = divmod(int_part, 2)  # 获取商和余数
        int_result.append(remainder)
    int_result = int("".join(list(map(str, int_result[::-1]))))

# 小数部分的计算
float_part = float(str(num)[str(num).find("."):])
float_result = 0
# 如果小数部分为0,无需继续计算
if float_part == 0:
    # 最终结果=整数部分+小数部分
    final_result = int_result
else:
    # 用于存储每一次计算的整数部分
    float_result = []
    while 1:
        tmp_result = float_part * 2
        # 取出整数部分(0或1)
        float_result.append(int(tmp_result))
        if tmp_result == int(tmp_result):
            break
        float_part = float("0."+str(tmp_result)[str(tmp_result).find(".")+1:])
    float_result = float("0."+"".join(list(map(str, float_result))))

# 最终结果为整数部分+小数部分
print(int_result+float_result)

2. 二进制转十进制

转换方法:

小数点前或者整数要从右到左用二进制的每个数去乘以 2 的相应次方并递增,小数点后则是从左往右乘以二的相应负次方并递减。

例,二进制数 1101.01 转化成十进制:

1101.01(B)= 1*2^0+0*2^1+1*2^2+1*2^3 + 0*2^(-1)+1*2^(-2) = 1+0+4+8+0+0.25 = 13.25

程序实现:

# 读取一个二进制数字
while 1:
    try:
        bin_num = float(input("请输入二进制数字:"))
        break
    except:
        continue
# 整数部分
int_bin = str(bin_num)[:str(bin_num).find(".")][::-1]
result = 0
for i in range(len(int_bin)):
    if int_bin[i] == "1":
        result += 2**(i)
# 小数部分
float_bin = str(bin_num)[str(bin_num).find(".")+1:]
for i in range(len(float_bin)):
    if float_bin[i] == "1":
        result += 2**(-(i+1))
# 最终结果=整数部分+小数部分
print(result)

3. 内置函数进行进制转换

# 十进制转其他进制
hex(n)  # 10进制的n转16进制
oct(n)  # 10进制的n转8进制
bin(n)  # 10进制的n转2进制

# 其他进制转十进制
int("16", base=16)  # 将16进制的16 转成10进制
int("7", base=8)   # 将8进制的7 转成10进制
int("1", base=2)  # 将2进制的1 转成10进制

4、编码

1. 为什么要编码

不知道大家有没有想过一个问题,那就是为什么要编码?我们能不能不编码?要回答这个问题必须要回到计算机是如何表示我们人类能够理解的符号的问题,这些符号也就是我们人类使用的语言。

由于人类的语言有太多,因而表示这些语言的符号太多,无法用计算机中一个基本的存储单元—— byte 来表示,因而必须要经过拆分或一些翻译工作,才能让计算机能理解。

我们可以把计算机能够理解的语言假定为英语,其它语言要能够在计算机中使用必须经过一次翻译,把它翻译成英语,这个翻译的过程就是编码。所以可以想象只要不是说英语的国家要能够使用计算机就必须要经过编码。

这看起来有些霸道,但是这就是现状,这也和我们国家现在在大力推广汉语一样,希望其它国家都会说汉语,以后其它的语言都翻译成汉语,我们可以把计算机中存储信息的最小单位改成汉字,这样我们就不存在编码问题了。 

所以总的来说,编码的原因可以总结为: 

  • 计算机中存储信息的最小单元是一个字节即 8 个 bit,所以能表示的字符范围是 0~255 个;
  • 人类要表示的符号太多,无法用一个字节来完全表示;
  • 要解决这个矛盾必须需要一个新的数据结构 char,从 char 到 byte 必须编码。 

2. 如何“翻译”

明白了各种语言需要交流,经过翻译是必要的,那又如何来翻译呢?计算中提拱了多种翻译方式,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它们都可以被看作为字典,它们规定了转化的规则,按照这个规则就可以让计算机正确的表示我们的字符。

目前的编码格式很多,例如 GB2312、GBK、UTF-8、UTF-16 这几种格式都可以表示一个汉字,那我们到底选择哪种编码格式来存储汉字呢?这就要考虑到其它因素了,是存储空间重要还是编码的效率重要,根据这些因素来正确选择编码格式。

5、常见编码格式

1. ASCII 编码 

我们知道,计算机内部,所有信息最终都是一个二进制值。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从00000000到11111111。

上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今。

ASCII 码一共规定了128个字符的编码,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来,比如空格 SPACE 是 32(二进制00100000),大写的字母 A 是 65(二进制01000001)。

这 128 个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的一位统一规定为0。

2. 非 ASCII 编码

英语用 128 个符号编码就够了,但是用来表示其他语言,128 个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的 é 的编码为 130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多 256 个符号。

但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130 在法语编码中代表了 é,在希伯来语编码中却代表了字母 Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127 表示的符号是一样的,不一样的只是 128--255 的这一段。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示 256 种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。

虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的 Unicode 和 UTF-8 是毫无关系的。

1)ISO-8859-1 

128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。 

2)GB2312 

它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。

3)GBK 

全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。 

4)GB18030 

全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。 

3. Unicode

如上所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。

可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode(Universal Code 统一码),就像它的名字都表示的,这是一种所有符号的编码。

Unicode 当然是一个很大的集合,现在的规模可以容纳 100 多万个符号。每个符号的编码都不一样,比如,U+0639 表示阿拉伯字母 Ain,U+0041 表示英语的大写字母 A,U+4E25 表示汉字严。具体的符号对应表,可以查询unicode.org,或者专门的(中日韩)汉字对应表

Unicode 的问题:

需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。

比如,汉字“严”的 Unicode 是十六进制数 4E25,转换成二进制数足足有 15 位(100111000100101),也就是说,这个符号的表示至少需要 2 个字节。表示其他更大的符号,可能需要 3 个字节或者 4 个字节,甚至更多。

这里就有两个严重的问题:

  1. 如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示 3 个符号呢?
  2. 我们已经知道,英文字母只用 1 个字节表示就够了,如果 Unicode 统一规定,每个符号用 3 个或 4 个字节表示,那么每个英文字母前都必然有 2 到 3 个字节是 0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:

  1. 出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。
  2. Unicode 在很长一段时间内无法推广,直到互联网的出现。

4. UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~6 个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8 的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
  2. 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。

从下面的表格可以看出,ASCII 编码实际上可以被看成是 UTF-8 编码的一部分,所以,大量只支持 ASCII 编码的历史遗留软件可以在 UTF-8 编码下继续工作。

现在,捋一捋 ASCII 编码和 Unicode 编码的区别:ASCII 编码是 1 个字节,而 Unicode 编码通常是 2 个字节。

如果统一成 Unicode 编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用 Unicode 编码比 ASCII 编码需要多一倍的存储空间,在存储和传输上就十分不划算。

所以,本着节约的精神,又出现了把 Unicode 编码转化为“可变长编码”的 UTF-8 编码。UTF-8 编码把一个 Unicode 字符根据不同的数字大小编码成 1-6 个字节,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4-6 个字节。如果你要传输的文本包含大量英文字符,用 UTF-8 编码就能节省空间。

下表总结了编码规则,其中字母 x 表示可用编码的位:

 根据上表,解读 UTF-8 编码非常简单:

  1. 如果一个字节的第一位是 0,则这个字节单独就是一个字符;
  2. 如果第一位是 1,则连续有多少个 1,就表示当前字符占用多少个字节。

下面,还是以汉字“严”为例,演示如何实现 UTF-8 编码:

  1. “严”的 Unicode 是 4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx。
  2. 然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的 x,多出的位补 0。这样就得到了,“严”的 UTF-8 编码是 11100100 10111000 10100101,转换成十六进制就是 E4B8A5。

5. Unicode 和 UTF-8 之间的转换

通过上述例子,可以看到“严”的 Unicode 码是 4E25,UTF-8 编码是 E4B8A5,两者是不一样的。它们之间的转换可以通过程序实现。

Windows 平台,有一个最简单的转化方法,就是使用内置的记事本小程序 notepad.exe。打开文件后,点击文件菜单中的另存为命令,会跳出一个对话框,在最底部有一个编码的下拉条:

里面有四个选项,分别是 ANSI,Unicode,Unicode big endian 和 UTF-8:

  1. ANSI 是默认的编码方式。对于英文文件是 ASCII 编码,对于简体中文文件是 GB2312 编码(只针对 Windows 简体中文版,如果是繁体中文版会采用 Big5 码)。
  2. Unicode 编码这里指的是 notepad.exe 使用的 UCS-2 编码方式,即直接用两个字节存入字符的 Unicode 码,这个选项用的 little endian 格式。
  3. Unicode big endian 编码与上一个选项相对应。在下一节会解释 little endian 和 big endian 的涵义。
  4. UTF-8 编码,也就是上一节谈到的编码方法。

选择完“编码方式”后,点击"保存"按钮,文件的编码方式就立刻转换好了。

6. Little endian 和 Big endian

上一节已经提到,UCS-2 格式可以存储 Unicode 码(码点不超过0xFFFF)。以汉字严为例,Unicode 码是 4E25,需要用两个字节存储,一个字节是 4E,另一个字节是 25。存储的时候,4E 在前,25 在后,这就是 Big endian 方式;25在前,4E在后,这是 Little endian 方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是"大头方式"(Big endian),第二个字节在前就是"小头方式"(Little endian)。那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用 FE FF 表示。这正好是两个字节,而且 FF 比 FE 大 1。

  • 如果一个文本文件的头两个字节是 FE FF,就表示该文件采用大头方式;
  • 如果头两个字节是 FF FE,就表示该文件采用小头方式。

下面,举一个实例:

打开"记事本"程序 notepad.exe,新建一个文本文件,内容就是一个严字,依次采用 ANSI、Unicode、Unicode big endian 和 UTF-8 编码方式保存。

然后,用文本编辑软件 UltraEdit 中的“十六进制功能”,观察该文件的内部编码方式。

  1. ANSI:文件的编码就是两个字节 D1 CF,这正是严的 GB2312 编码,这也暗示 GB2312 是采用大头方式存储的。
  2. Unicode:编码是四个字节 FF FE 25 4E,其中 FF FE 表明是小头方式存储,真正的编码是 4E25。
  3. Unicode big endian:编码是四个字节 FE FF 4E 25,其中 FE FF 表明是大头方式存储。
  4. UTF-8:编码是六个字节 EF BB BF E4 B8 A5,前三个字节 EF BB BF 表示这是UTF-8编码,后三个 E4B8A5 就是严的具体编码,它的存储顺序与编码顺序是一致的。

6、Python中的编码与解码原理

1. 计算机系统通用的字符编码工作方式

搞清楚了 ASCII、Unicode 和 UTF-8 的关系,我们就可以总结一下目前的计算机系统通用的字符编码工作方式:

1)在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF-8 编码。

 从上图可以看出不同字节编码之间是可以通过 Unicode 来实现相互转换的。

  • 编码(encode):在 Unicode 中,每一个字符都有一个唯一的数字表示,那么将 Unicode 字符串转换为特定字符编码(ASCII、UTF-8、GBK)对应的字节串的过程和规则就是编码。
  • 解码(decode):将特定字符编码(ASCII、UTF-8、GBK)的字节串转换为对应的 Unicode 字符串的过程和规则就是解码。

简单理解:编码是给计算机底层用的,解码是显示给人看的。

涉及到编码的地方一般都在字符到字节或者字节到字符的转换上,而需要这种转换的场景主要是在操作 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O(以 Web 应用为例介绍)。

2)如下图所示,用记事本编辑的时候,从文件读取的 UTF-8 字符被转换为 Unicode 字符到内存里,编辑完成后,保存的时候再把 Unicode 转换为 UTF-8 保存到文件。

3)如下图所示,浏览网页的时候,服务器会把动态生成的 Unicode 内容转换为 UTF-8 再传输到浏览器。 

所以我们会看到很多网页的源码上会有类似<meta charset="UTF-8" />的信息,表示该网页正是用的 UTF-8 编码。 

2. Python 源代码文件的执行过程

我们都知道,磁盘上的文件都是以二进制格式存放的,其中文本文件都是以某种特定编码的字节形式存放的。对于程序源代码文件的字符编码是由编辑器指定的,比如我们使用 Pycharm 来编写 Python 程序时会指定工程编码和文件编码为 UTF-8,那么 Python 代码被保存到磁盘时就会被转换为 UTF-8 编码对应的字节(encode过程)后写入磁盘。

当执行 Python 代码文件中的代码时,Python 解释器在读取 Python 代码文件中的字节串之后,需要将其转换为 Unicode 字符串(decode 过程)之后才执行后续操作。

3. python 中的默认编码

如果我们没有在代码文件指定字符编码,Python 解释器会使用哪种字符编码把从代码文件中读取到的字节转换为 Unicode 字符串呢?就像我们配置某些软件时,有很多默认选项一样,需要在 Python 解释器内部设置默认的字符编码来解决这个问题,这就是“默认编码”。

Python2 和 Python3 的解释器使用的默认编码是不一样的,我们可以通过 sys.getdefaultencoding() 来获取默认编码:

>>> # Python2
>>> import sys
>>> sys.getdefaultencoding()
'ascii'

>>> # Python3
>>> import sys
>>> sys.getdefaultencoding()
'utf-8'
  • 对于 Python2 来讲,Python 解释器在读取到中文字符的字节码时,会先查看当前代码文件头部是否指明字符编码是什么。如果没有指定,则使用默认字符编码 ASCII 进行解码,导致中文字符解码失败。
  • 对于 Python3 来讲,执行过程是一样的,只是 Python3 的解释器以 UTF-8 作为默认编码,但是这并不表示可以完全兼容中文问题。比如我们在 Windows 上进行开发时,Python 工程及代码文件都使用的是默认的 GBK 编码,也就是说 Python 代码文件是被转换成 GBK 格式的字节码保存到磁盘中的。Python3 的解释器执行该代码文件时,试图用 UTF-8 进行解码操作时,同样会解码失败。

4. Python2/3 对字符串的支持

1)Python2

Python2中对字符串的支持由以下三个类提供:

class basestring(object)
class str(basestring)
class unicode(basestring)

str 和 unicode 都是 basestring 的子类。严格意义上说:

  • str 其实是字节串,它是 unicode 经过编码后的字节组成的序列。
  • 对 UTF-8 编码的 str “汉”使用 len() 函数时,结果是 3,因为 UTF-8 编码的“汉” == “\xE6\xB1\x89”。
  • unicode 才是真正意义上的字符串,对字节串 str 使用正确的字符编码进行解码后获得,并且 len(u'汉') == 1。
  • 从上图中也可看出,ASCII 编码实际上可以被看成是 UTF-8 编码的一部分。

因此,Python2 中的字符串进行字符编码转换过程是:

  • 字节串(Python2的str默认是字节串) --> decode('原来的字符编码') --> Unicode字符串 --> encode('新的字符编码') --> 字节串
#!/usr/bin/env python2
#-*- coding:utf-8 -*-

a = '你好'
b = u'你好'
print(type(a), len(a))  # output:(<type'str'>, 6)
print(type(b), len(b))  # output:(<type'unicode'>, 2)

utf_8_a = '我爱中国'
gbk_a = utf_8_a.decode('utf-8').encode('gbk')
print(gbk_a.decode('gbk'))  # 输出结果:我爱中国

2)Python3

Python3 中对字符串的支持进行了实现类层次的上简化,去掉了 unicode 类,添加了一个 bytes 类。从表面上来看,可认为 Python3 中的 str 和 unicode 合二为一了。

class bytes(object)
class str(object)

实际上,Python3 中已经意识到之前的错误,开始明确区分字符串与字节,因此 Python3 中的 str 已经是真正的字符串,而字节是用单独的 bytes 类来表示。

也就是说,Python3 默认定义的就是字符串,实现了对 Unicode 的内置支持,减轻了程序员对字符串处理的负担。

1 a = '你好'
2 b = u'你好'
3 c = '你好'.encode('gbk') 
4 print(type(a), len(a))  # output:<class'str'> 2
5 print(type(b), len(b))  # output:<class'str'> 2
6 print(type(c), len(c))  # output:<class'bytes'> 4

由于 Python3 中定义的字符串默认就是 unicode,因此不需要先解码,可以直接编码成新的字符编码:

  • 字符串(str 就是 Unicode 字符串) --> encode('新的字符编码') --> 字节串

5. Python 中的字符编码转换函数

对于单个字符的编码,Python 提供了 ord() 函数获取字符的整数表示,chr() 函数把编码转换为对应的字符:

>>> ord("A")
65
>>> ord("Z")
90
>>> chr(97)
'a'
>>> chr(122)
'z'
>>> ord("中")
20013

如果知道字符的整数编码,还可以用十六进制这么写 str:

>>> '\u4e2d\u6587'
'中文'

两种写法完全是等价的。

由于 Python 的字符串类型是 str,在内存中以 Unicode 表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把 str 变为以字节为单位的 bytes。

Python 对 bytes 类型的数据用带 b 前缀的单引号或双引号表示,例如 x = b'ABC' 。要注意区分 'ABC' 和 b'ABC',前者是 str,后者虽然内容显示得和前者一样,但 bytes 的每个字符都只占用一个字节。

以 Unicode 表示的 str 通过 encode() 方法可以编码为指定的 bytes,例如:

>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

 检查编码格式:

>>> import chardet
>>>
>>> chardet.detect("中国".encode("utf-8"))
{'encoding': 'utf-8', 'confidence': 0.7525, 'language': ''}
>>> chardet.detect("中国".encode("gbk"))
{'encoding': 'IBM855', 'confidence': 0.7679697235616183, 'language': 'Russian'}
>>>
>>> # 以二进制格式打开文件
>>> with open("e:\\pra_file.txt", "rb") as f:
...     chardet.detect(f.read())
...
{'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}

二、Java简介

1、Java 简介

  • Java 是美国 Sun 公司(Stanford University Network)在 1995 年推出的计算机语言。
  • Java 之父:詹姆斯·高斯林(James Gosling)
  • 2009 年,Sun 公司被甲骨文公司收购。

Java 语言的三个版本:

  • JavaSE: Java 语言的标准版,用于桌面应用的开发,是以下两个版本的基础。
  • JavaME: Java 语言的小型版,用于嵌入式消费类电子设备。
  • JavaEE: Java 语言的企业版,用于 Web 方向的网站开发。

2、Java 语言跨平台原理

Java 程序并非是直接运行的,而是 Java 编译器将 Java 源程序编译成与平台无关的字节码文件(class 文件),然后由 Java 虚拟机(JVM)对字节码文件解释执行。

所以在不同的操作系统下,只需安装不同的 Java 虚拟机即可实现 Java 程序的跨平台。

3、JRE 和 JDK

  • JVM(Java Virtual Machine):Java 虚拟机。
  • JRE(Java Runtime Environment):Java 运行环境,包含了 JVM 和 Java 的核心类库(Java API)。
  • JDK(Java Development Kit):Java 开发工具,包含了 JRE 和开发工具。

总结:只需安装 JDK 即可,它包含了 Java 的运行环境和虚拟机。

三、Java开发环境安装与配置

1、JDK 安装配置

通过 Oracle 官方网站下载对应版本的 JDK。

进入选右侧的J2SE然后点击DOWNLOAD 选择下面的JDK下载,然后选择对应的平台下载。

1. JDK1.8下载

选择windows x64 点击右边的下载下载对应版本的jdk1.8。

2. 安装JDK

在D盘先新建2个文件夹作为JDK与JRE的下载路径,不要用默认的下载路径。

 

安装更改默认的安装路径:

之后安装JDK,JDK安装之后要安装JRE,选择刚新建的JRE文件夹作为JRE安装目录,之后安装JRE。 

3. 配置环境变量

选择计算机,右击选择属性,之后进入选择高级系统配置。

之后点击环境变量:

在系统变量中点击新建: 

首先第一个,配置 JAVA_HOME 值是 D:\jdk1.8 也就是jdk的安装目录; 

接下来是 classpath 类加载路径 值:

.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

注意前面的点 .;    

. 点代表所有 ,复制上面的粘贴即可。

最后一个是 path变量修改(主要是告诉操作系统某些路径下有一些命令);这个path变量已经存在,我们要在这个值的最前面加 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;  

4. 测试JDK是否安装成功

cmd 下输入java 运行的命令:

cmd 下输入javac编译的命令: 

输入java -version版本命令:

5. JDK 安装目录说明

目录名称说明
bin该路径下存放了 JDK 的各种工具命令。javac 和 java 就放在这个目录。
conf该路径下存放了 JDK 的相关配置文件。
include该路径下存放了一些平台特定的头文件。
jmods该路径下存放了 JDK 的各种模块。
legal该路径下存放了 JDK 各模块的授权文档。
lib该路径下存放了 JDK 工具的一些补充 JAR 包。

2、Tomcat安装配置

1. Tomcat8下载

Apache Tomcat® - Apache Tomcat 8 Software Downloads

2. Tomcat配置

解压tomcat到当前的文件:

TOMCAT环境变量配置:

TOMCAT_HOME : C:\Users\Administrator\Desktop\apache-tomcat-8.0.50

Path : ;%TOMCAT_HOME%\bin

以上即可,不影响tomcat的使用 

3. 运行Tomcat8

“开始”->“运行”->输入cmd,在命令提示符中输入 startup.bat,之后会弹出tomcat命令框,输出启动日志;打开浏览器输入http://localhost:8080/ ,如果进入tomcat欢迎界面,那么恭喜你,配置成功。

详细的配置也可以配置为:

  • 变量名: CATALINA_BASE     变量值: D:\apache-tomcat-7.0.63(Tomcat解压到的目录)
  • 变量名: CATALINA_HOME     变量值:D:\apache-tomcat-7.0.63
  • 变量名: CATALINA_TMPDIR     变量值:D:\apache-tomcat-7.0.63\temp
  • 变量名: Path    变量值:D:\apache-tomcat-7.0.63\bin

 不配置也可以。

3、mysql安装配置

1. Mysql数据库安装

zip格式是自己解压,解压缩之后其实MySQL就可以使用了,但是要进行配置。

解压之后可以将该文件夹改名,放到合适的位置,放到E:\install\mysql-5.6.26-winx64\路径中。当然你也可以放到自己想放的任意位置。

2. 配置环境变量

我的电脑->属性->高级->环境变量。

选择PATH,在其后面添加:

你的mysql bin文件夹的路径 (如: E:\install\mysql-5.6.26-winx64\bin )。

PATH=.......; E:\install\mysql-5.6.26-winx64\bin

注意是追加,不是覆盖:

;E:\install\mysql-5.6.26-winx64\bin

windows环境变量配置参考:

D:\set_up\Python36-32\Scripts\;D:\set_up\Python36-32\;C:\ProgramData\Oracle\Java\javapath;%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\\;%JAVA_HOME%\bin;D:\set_up\TortoiseSVN\bin;C:\Program Files\MySQL\MySQL Utilities 1.6\;C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\;C:\Program Files\Microsoft SQL Server\100\Tools\Binn\;C:\Program Files\Microsoft SQL Server\100\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\100\Tools\Binn\VSShell\Common7\IDE\;C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\;C:\Program Files (x86)\Microsoft SQL Server\100\DTS\Binn\;%M2_HOME%\bin;%TOMCAT_HOME%\bin

配置完环境变量之后先别忙着启动mysql,以管理员身份运行cmd(一定要用管理员身份运行,不然权限不够)。

输入:cd E:\install\mysql-5.6.26-winx64\bin 进入mysql的bin文件夹(不管有没有配置过环境变量,也要进入bin文件夹,否则之后启动服务仍然会报错误2)。

输入mysqld -install(如果不用管理员身份运行,将会因为权限不够而出现错误:Install/Remove of the Service Denied!)。

安装成功:

安装成功后就要启动服务了,继续在cmd中输入:net start mysql(如图),服务启动成功! 

此时很多人会出现错误,请看注意:

如果出现“错误2 系统找不到文件”,检查一下是否修改过配置文件或者是否进入在bin目录下操作,如果配置文件修改正确并且进入了bin文件夹,需要先停止服务,net stop mysql。再删除mysql(输入 mysqld -remove)再重新安装(输入 mysqld -install)。

重新安装好之后,一定要再重新启动mysql要不会连接不上。

进入bin目录,输入net start mysql:

服务启动成功之后,就可以登录了。

第一次登陆的时候要重置密码,否则登陆不进去。

先退回上一级目录进入data目录:

cd E:\install\mysql-5.6.26-winx64\data

输入mysql -uroot --skip-password 跳过密码登陆: 

进入mysql之后修改root的密码为root6 (密码自己设置):

set password for root@localhost = password('root6');

然后退出exit:

3. 登陆mysql

mysql -uroot -p

输入刚才设置的密码即可登陆成功。

4. 命令提示符操作备份

Microsoft Windows [版本 6.1.7601]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

C:\Users\Administrator>e:

E:\>cd install

E:\install>cd mysql-5.6.26-winx64

E:\install\mysql-5.6.26-winx64>cd data

E:\install\mysql-5.6.26-winx64\data>mysql -uroot --skip-password
Warning: Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.6.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> alter user 'root'@'localhost' identified by 'root';
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near 'iden
ified by 'root'' at line 1
mysql> set password for root@localhost = password('root');
Query OK, 0 rows affected (0.00 sec)

mysql> exit;
Bye

E:\install\mysql-5.6.26-winx64\data>mysql -uroot -p
Enter password: ****
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> exit;

4、mysql msi安装

1. mysql下载

根据红框点击:

所安装的目录中出现上述文件,则准备工作结束。 

2. mysql安装

首先单击MySQL5.5.62的安装文件,出现该数据库的安装向导界面,单击“next”继续安装,如图所示:

在出现选择安装类型的窗口中,有“typical(默认)”、“Complete(完全)”、“Custom(用户自定义)”三个选项,我们选择“Custom”,因为通过自定义可以更加的让我们去熟悉它的安装过程。 

在出现自定义安装界面中选择mysql数据库的安装路径,这里我设置的是“d:\Program File\MySQL”,单击“next”继续安装,如图所示:  

单击“Install”按钮之后出现如下正在安装的界面,经过很少的时间,MySQL数据库安装完成,出现完成MySQL安装的界面,如图所示: 

在打开的配置类型窗口中选择配置的方式,“Detailed  Configuration(手动精确配置)”、“Standard Configuration(标准配置)”,为了熟悉过程,我们选择“Detailed Configuration(手动精确配置)”,单击“next”继续,如图所示: 

在出现的窗口中,选择服务器的类型,“Developer Machine(开发测试类)”、“Server Machine(服务器类型)”、“Dedicated MySQL Server Machine(专门的数据库服务器)”,我们仅仅是用来学习和测试,默认就行,单击“next”继续,如图所示: 

在出现的配置界面中选择mysql数据库的用途,“Multifunctional Database(通用多功能型)”、“Transactional Database Only(服务器类型)”、“Non-Transactional Database Only(非事务处理型)”,这里我选择的是第一项, 通用安装,单击“next”继续配置,如图所示: 

在出现的界面中,进行对InnoDB Tablespace进行配置,就是为InnoDB 数据库文件选择一个存储空间,如果修改了,要记住位置,重装的时候要选择一样的地方,否则可能会造成数据库损坏,当然,对数据库做个备份就没问题了,如图所示:  

在出现的界面中,进行对InnoDB Tablespace进行配置,就是为InnoDB 数据库文件选择一个存储空间,如果修改了,要记住位置,重装的时候要选择一样的地方,否则可能会造成数据库损坏,当然,对数据库做个备份就没问题了,如图所示: 

在打开的页面中设置是否启用TCP/IP连接,设定端口,如果不启用,就只能在自己的机器上访问mysql数据库了,这也是连接java的操作,默认的端口是3306,并启用严格的语法设置,如果后面出现错误,可以将“Add firewall exception for this port ”这个选项选上,单击“next”继续,如图所示: 

我们选择utf-8,如果在这里没有选择UTF-8这个编码的化,在使用JDBC连接数据库的时候,便会出现乱码。 

在打开的页面中选择是否将mysql安装为windows服务,还可以指定Service Name(服务标识名称),是否将mysql的bin目录加入到Windows PATH(加入后,就可以直接使用bin下的文件,而不用指出目录名,比如连接,“mysql–u username –p password;”就可以了,单击“next”继续配置,如图所示:

在打开的页面中设置是否要修改默认root用户(超级管理员)的密码(默认为空),“New root password”,如果要修改,就在此填入新密码,并启用root远程访问的功能,不要创建匿名用户,单击“next”继续配置,如图所示:  

在服务中将mysql数据库启动,并在命令窗口中输入“mysql–h localhost –u root -p”或者是“mysql -h localhost -uroot -p密码”,接着在出现的提示中输入用户的密码,如图所示: 

数据库启动并登录成功!

5、mysql客户端SQLyog安装

1. SQLyog安装

运行程序:

选择下一步进行安装: 

选择安装位置点击安装: 

安装后即可运行: 

2. 运行SQLyog

我是在安装数据库后,第一次运行SQLyog出现的这种情况。 

下面是解决方法:

在cmd下输入:

net stop mysql

第一步:停止服务。

第二步:进入mysql安装的bin目录,再重启服务。

根据用户名和密码登陆:

第三步:然后再给root用户授权。

grant all on *.* to 'root'@'localhost' identified by '123456';

第四步:刷新一下即可。

flush privileges;

3. 登录SQLyog

输入mysql的用户名和自己设置的密码:

  • 端口号默认是3306
  • sql的主机地址为localhost

即可登陆: 

6、数据库创建与备份

1. 创建数据库

安装sqlyog之后,需要自己创建一个数据库。

选择最上面点击创建数据库:

创建一个prodatashow的数据库: 

prodatashow数据库就创建好了: 

2. mysql数据库的备份

1)导出mysql数据库

选择要导出的数据:

第一步:cd进入到 MySQL中的bin文件夹的目录。

如我输入的命令行:cd C:\Program Files\MySQL\MySQL Server 4.1\bin

第二步:导出数据库mysqldump -u 用户名 -p 数据库名 > 导出的文件名。

如我输入的命令行:mysqldump -u root -p news > news.sql   (输入后会让你输入进入MySQL的密码)(如果导出单张表的话在数据库名后面输入表名即可)。

2)在自己的数据库中导入数据库文件

  1. 1. 将要导入的.sql文件移至bin文件下,这样的路径比较方便;
  2. 2. 同上面导出的第1步,进入bin目录;
  3. 3. 进入MySQL:mysql -u 用户名 -p;
  4. 4. 创建一个新的数据库,已经建好的prodatashow数据库;
  5. 5. 输入:mysql>use 目标数据库名,如mysql>use prodatashow;
  6. 6. 导入文件:mysql>source 导入的文件名,如:mysql>source news.sql;

7、tomcat部署项目

1. War包

War包一般是在进行Web开发时,通常是一个网站Project下的所有源码的集合,里面包含前台HTML/CSS/JS的代码,也包含Java的代码。

当开发人员在自己的开发机器上调试所有代码并通过后,为了交给测试人员测试和未来进行产品发布,都需要将开发人员的源码打包成War进行发布。

War包可以放在Tomcat下的webapps或者word目录下,随着tomcat服务器的启动,它可以自动被解压。

2. Tomcat服务器

Tomcat服务器是一个免费的开放源代码的Web应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP程序的首选,最新的Servlet和JSP规范总是能在Tomcat中得到体现。

3. 配置Java运行环境

1)下载并安装JDK

从官网上下载最新的JDK:Java Downloads | Oracle ,下载后安装,选择想把JDK安装到的目录。JRE是包含在JDK中的,所以不需要再另外安装JRE了。

2)设置JDK环境变量

右击“计算机”,点击“属性”,点击弹出窗口中左侧的“高级系统设置”,在弹出的选项卡中选择“高级->环境变量”。

假设你本地JAVA的JDK安装的位置为:C:\Program Files\Java\jdk1.7.0_45。

在这里,新建2个环境变量,编辑1个已有的环境变量。如下:

新建变量名:JAVA_HOME;

变量值:你安装JDK的安装目录,在这里为C:\Program Files\Java\jdk1.7.0_45。

新建变量名:CLASSPATH

变量值:

.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;%TOMCAT_HOME%\BIN

(注意最前面有个.号)

编辑环境变量的路径:

变量名:Path;

变量值:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;

(将此处的字符串粘贴到变量值的最前面)

3)验证是否JDK环境变量设置成功

点击开始并输入CMD,在命令行分别输入:java; javac; java –version.

如果分别显示如下信息,说明你的Java环境变量已经配置成功。

输入Java,显示:

输入Javac,显示: 

输入java –version,显示: 

4. 部署Tomcat服务器

1)下载Tomcat到本地硬盘

从官网上下载Tomcat服务器。官网上下载的文件都是绿色免安装的。

下载地址为:Apache Downloads

下载后解压缩,如E:\apache-tomcat-7.0.26。

2)设置Tomcat环境变量

依然是点开电脑的环境变量对话框。

新建一个环境变量:

变量名:TOMCAT_HOME

变量值:你的TOMCAT解压后的目录,如E:\apache-tomcat-7.0.26。

3)验证Tomcat环境变量的配置是否成功

运行Tomcat解压目录下的 bin/startup.bat,启动Tomcat服务器。在任何一款浏览器的地址栏中输入http://localhost:8080 ,如果界面显示如下图,则说明Tomcat的环境变量配置成功。

tomcat启动的窗口为:

5. 部署Web项目的War包到Tomcat服务器

1)FTP获取war包和sql脚本

从本地FTP服务器上下载Daily Building出的最新的项目包。解压后一般由两个文件组成,database文件夹和projectName.war包。

运行database文件中的xxxxx.sql脚本文件,便可以生成最新的数据库和表结构。

2)配置Web项目的虚拟目录

将projectName.war包,复制到Tomcat的webapp下。这样当配置好后的访问路径便为:http://localhost:8080/projectName/login.jsp

在访问之前,需要修改tomcat服务器的配置文件,打开:

tomcat解压目录\conf\context.xml。将运行该web项目时,需要配置的数据库连接字符串增加到该xml文件中。增加后的context.xml为:

<Context>

<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>

<!-- Uncomment this to disable session persistence across Tomcat restarts -->

<!--
<Manager pathname="" />
-->

<!-- Uncomment this to enable Comet connection tacking (provides events
on session expiration as well as webapp lifecycle) -->

<!--
<Valve className="org.apache.catalina.valves.CometConnectionManagerValve" />
-->

(这里填写本Web项目运行时,需要连接的数据库配置。)

</Context>

3)访问web项目的登录页

连接串设置完毕后,便可以基于Tomcat服务器来访问web项目了。

首先运行Tomcat的bin目录下的startup.bat,当Tomcat启动完毕后,

在浏览器输入:localhost:8080/projectName/login.jsp时,如果出现该Web项目的login界面时,则表明war包已成功地部署到tomcat服务器上,并可成功访问了。

6. Tomcat数据库连接池的配置方法总结

数据库连接是一种关键的有限的昂贵的资源,这在多用户网页应用程序中体现的尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标,数据库连接池正是针对这个问题提出的.

数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏,这样可以明显提高对数据库操作的性能.

数据库连接池在初始化的时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的,无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中.

数据库连接池的最小连接数和最大连接数的设置要考虑到下列几个因素:

1.最小连接数是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费.

2.最大连接数是连接池申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待对列中,这会影响之后的数据库操作

如果最小连接数与最大连接数相差太大,那么最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接,不过,这些小于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时被释放.

实例使用的Tomcat版本为6.0

方法一: 在Tomcat的conf/context.xml中配置

在Tomcat\apache-tomcat-6.0.33\conf目录下的context.xml文件中配置默认值如下:

<?xml version='1.0' encoding='utf-8'?>
<Context>
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>

配置连接池:

<?xml version='1.0' encoding='utf-8'?>

<Context>
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <!--配置oracle数据库的连接池-->
    <Resource name="jdbc/oracleds"
        author="Container"
        type="javax.sql.DataSource"
        maxActive="100"
        maxIdle="30"
        maxWait="10000"
        username="scott"
        password="tiger"
        driverClassName="oracle.jdbc.dirver.OracleDriver"
        url="jdbc:oracle:thin:@127.0.0.1:1521:ora9" />

    <!--配置mysql数据库的连接池, 
        需要做的额外步骤是将mysql的Java驱动类放到tomcat的lib目录下        
        maxIdle 连接池中最多可空闲maxIdle个连接 
        minIdle 连接池中最少空闲maxIdle个连接 
        initialSize 初始化连接数目 
        maxWait 连接池中连接用完时,新的请求等待时间,毫秒 
        username 数据库用户名
        password 数据库密码
        -->
    <Resource name="jdbc/mysqlds" 
        auth="Container" 
        type="javax.sql.DataSource" 
        username="root" 
        password="root" 
        maxIdle="30" 
        maxWait="10000" 
        maxActive="100"
        driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://127.0.0.1:3306/db_blog" />

</Context>

配置好后需要注意的两个步骤:

  1. 将对应数据库的驱动类放到tomcat的lib目录下
  2. 重新启动tomcat服务器,让配置生效

在web应用程序的web.xml中设置数据源参考,如下:

在<web-app></web-app>节点中加入下面内容。

<resource-ref>

      <description>mysql数据库连接池</description>
      <!-- 参考数据源名字,同Tomcat中配置的Resource节点中name属性值"jdbc/mysqlds"一致 -->
      <res-ref-name>jdbc/mysqlds</res-ref-name>
      <!-- 资源类型 -->
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
      <res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>

错误解决:

javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial

    at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:645)
    at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:288)
    at javax.naming.InitialContext.getURLOrDefaultInitCtx(InitialContext.java:325)
    at javax.naming.InitialContext.lookup(InitialContext.java:392)
    at com.iblog.util.DBPoolUtil.<clinit>(DBPoolUtil.java:34)

解决方案:

上面的异常信息是配置文件中JNDI没有初始化造成的。

如果下面的问题都不存在:

  1. 要去检查下配置文件中连接数据库的URL参数是否正确2.以及是否导入了正常的包3.检查在Tomcat中conf/server.xml文件,检查是否设置useNaming="false",如果是,去掉
  2. 那就是通过main方法测试的,这个数据源不支持这样的测试方法,程序要运行在Tomcat中才能找到相应的数据源.[我在测试时犯这样的错导致上面错误出现]
<%@ page language="java" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>     

<%@ page import="java.sql.*" %>     
<%@ page import="javax.naming.*" %>     
<%@ page import="javax.sql.DataSource" %>
<html>     
<head>     
<title>Tomcat6.0 JNDI!</title>    
</head>    
  <body>      
   Tomcat连接池测试,获取数据源 <br>     
    <%     
        try {      
            //初始化查找命名空间
            Context ctx = new InitialContext();  
            //参数java:/comp/env为固定路径   
            Context envContext = (Context)ctx.lookup("java:/comp/env"); 
            //参数jdbc/mysqlds为数据源和JNDI绑定的名字
            DataSource ds = (DataSource)envContext.lookup("jdbc/mysqlds"); 
            Connection conn = ds.getConnection();     
            conn.close();     
            out.println("<span style='color:red;'>JNDI测试成功<span>");     
        } catch (NamingException e) {     
            e.printStackTrace();     
        } catch (SQLException e) {     
            e.printStackTrace();     
        }     
    %>     
  </body>     
</html>   

方法二:在Tomcat的conf/server.xml中配置

打开tomcat的conf/server.xml文件,找到<GlobalNamingResources></GlobalNamingResources>节点,默认的内容如下:

<GlobalNamingResources>

    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>

在该节点中加入相关的池配置信息,如下:

<GlobalNamingResources>

             <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />

             <!--配置mysql数据库的连接池, 
                需要做的额外步骤是将mysql的Java驱动类放到tomcat的lib目录下        
               -->
             <Resource name="jdbc/mysqlds" 
              auth="Container" 
              type="javax.sql.DataSource" 
              username="root" 
              password="root" 
              maxIdle="30" 
              maxWait="10000" 
              maxActive="100"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://127.0.0.1:3306/db_blog" />
  </GlobalNamingResources>

在tomcat的conf/context.xml文件中的<Context></Context>节点中加入如下内容:

<ResourceLink name="jdbc/mysqlds" global="jdbc/mysqlds" type="javax.sql.DataSource"/>

然后在web项目中的WEB-INF目录下的web.xml中配置:

<resource-ref>

      <description>mysql数据库连接池</description>
      <!-- 参考数据源名字,同Tomcat中配置的Resource节点中name属性值"jdbc/mysqlds"一致 -->
      <res-ref-name>jdbc/mysqlds</res-ref-name>
      <!-- 资源类型 -->
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
      <res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>

同样配置好后,需要重新启动服务器,让配置生效。

方法三:在Tomcat的conf/server.xml中配置虚拟目录时配置 

在配置虚拟目录时,也就是在配置conf下面的server.xml时,在context标签内添加池配置。

在说该方法之前,先说一下,如何用tomcat配置虚拟目录。

在tomcat\conf下server.xml中找到:

<Host name="localhost"  appBase="webapPS"
            unpackWARs="true" autoDeploy="true"
            xmlValidation="false" xmlNamespaceAware="false">
</Host>

在其中添加:

<Context path="/website" docBase="F:/myweb" reloadable="true"></Context>

注意:

  • docBase要改成你的项目目录。
  • path为虚拟路径,访问时的路径,注意:一定要加“/” debug建议设置为0
  • reloadable设置为true。  

配置好后重新启动tomcat。

实例中如下配置:

<Context path="/website" docBase="D:/program files/Tomcat/apache-tomcat-6.0.33/webapps/iblog.war" reloadable="true">
</Context>

接下来添加池配置,如下:

<!--配置虚拟目录-->

<Context path="/website" docBase="D:/program files/Tomcat/apache-tomcat-6.0.33/webapps/iblog.war" reloadable="true">
            <Resource name="jdbc/mysqlds" 
            auth="Container" 
            type="javax.sql.DataSource" 
            username="root" 
            password="root" 
            maxIdle="30" 
            maxWait="10000" 
            maxActive="100"
            driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://127.0.0.1:3306/db_blog"
            />
</Context>

启动服务器,测试,注意因为我们配置了path值为”/website”,所以访问的路径应该为website。

方法四:在Web项目中的META-INF目录下新建一个文件context.xml,写入配置

注意:是META-INF目录下,不是WEB-INF目录下:

<?xml version='1.0' encoding='utf-8'?>

<Context>
    <Resource name="jdbc/mysqlds" 
        auth="Container" 
        type="javax.sql.DataSource" 
        username="root" 
        password="root" 
        maxIdle="30" 
        maxWait="10000" 
        maxActive="100"
        driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://127.0.0.1:3306/db_blog"
        logAbandoned="true" />
</Context>

7. tomcat配置多个项目通过IP加端口号访问

一个tomcat部署多个项目并通过不同的端口访问。

第一步:修改 $TOMCAT_HOME\conf\server.xml文件。

  • 复制Service节点,去掉<Connector port="8009"...这个节点
  • 新增Service节点的name属性依次修改为Catalina1、Catalina2……
  • 新增Service节点的Connector节点port属性依次修改为8001、8002……(根据机器配置未占用端口,这里按顺序为方便)
  • 新增Service节点的Host节点appBase属性依次修改为webapps1、webapps2……

下面是新增两个Service节点的配置:

<Connector port="8081" maxHttpHeaderSize="8192"  
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"  
               enableLookups="false" redirectPort="8443" acceptCount="100"  
               connectionTimeout="20000" disableUploadTimeout="true" />  
  
    <Engine name="Catalina1" defaultHost="localhost">  
  
      <Realm className="org.apache.catalina.realm.UserDatabaseRealm"  
             resourceName="UserDatabase"/>  
         
      <Host name="localhost" appBase="webapps1"  
       unpackWARs="true" autoDeploy="true"  
       xmlValidation="false" xmlNamespaceAware="false">     
<Context path="" docBase="/user/local/Tomcat7/webapps1/Menu" debug="0" reloadable="true" />  
            
      </Host>  
  
    </Engine>  
  
  
  </Service>

  <Service name="Catalina2">  
      
    <Connector port="8082" maxHttpHeaderSize="8192"  
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"  
               enableLookups="false" redirectPort="8443" acceptCount="100"  
               connectionTimeout="20000" disableUploadTimeout="true" />  
  
    <Engine name="Catalina2" defaultHost="localhost">    
  
      <Realm className="org.apache.catalina.realm.UserDatabaseRealm"  
             resourceName="UserDatabase"/>

      <Host name="localhost" appBase="webapps2"
       unpackWARs="true" autoDeploy="true"  
       xmlValidation="false" xmlNamespaceAware="false"> 
        <Context path="" docBase="/user/local/Tomcat7/webapps2/Menu" debug="0" reloadable="true" />   <!--项目访问路径是ip加端口号-->

      </Host>  

    </Engine>    
    
  </Service>

第二步:在$TOMCAT_HOME目录下新建文件夹webapps1、webapps2……(目录里包含ROOT子目录),里面分别放不同项目(测试只就简单复制$TOMCAT_HOME\webapps\ROOT目录)。

第三步:复制$TOMCAT_HOME\confi目录下的Catalina生成多个副本,并依次命名为Catalina1、Catalina2…… 

第四步:启动Tomcat测试 

第五步:浏览器中一次访问不同端口 

为了证明是三个不同的项目,我修改了标题分别一第一个、第二个、第三个 。

8、Maven安装配置

1. Maven 简介

在学习 Maven 之前,我们先来看一下没使用 Maven 构建的项目都有哪些问题。假设你现在做了一个 CRM 的系统,项目中肯定要用到一些 jar 包,比如说 mybatis、log4j、JUnit 等。除了这些之外,还有可能用到我们同事开发的其他的东西,比如说别人做了一个财务模块或做了一个结算的模块,你在这里边有可能要用到这些东西。

假设有一天,我们项目中的 mybatis 进行了一个升级,但是它内部使用的 JUnit 没有升级。升级以后的 mybatis 假如要用 JUnit5,而项目中目前用的是 4.0 的,会不会冲突?必然会出问题!这个时候管理起来会比较麻烦,你需要各种调整。更有甚者,假如同事做的这些东西升级了但又没有通知你,这个时候,就会出现几种严重的问题:

  1. jar 包不统一、jar 包不兼容
  2. 工程升级维护过程操作繁琐
  3. 除此之外,还会有其它的一系列问题。

那么要想解决这些问题,就可以用到 Maven 了。

那 Maven 是什么

Maven 的本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM)

Maven 是用 Java 语言编写的。它管理的东西统统以面向对象的形式进行设计,最终他把一个项目看成一个对象,而这个对象叫做POM(project object model),即项目对象模型。

我们说一个项目就是一个对象,那么作为对象的行为和属性都有哪些呢?

Maven 说我们需要编写一个 pom.xml 文件,Maven 通过加载这个配置文件就可以知道我们项目的相关信息了。因此,Maven 离不开一个叫 pom.xml 的文件,因为这个文件代表就一个项目。

思考:如果我们做 8 个项目,那么对应的是 1 个还是 8 个文件?答:8 个!

那 Maven 是如何帮我们进行项目资源管理的呢?这就需要用到 Maven 中的第二个东西:依赖管理,这也是它的第二个核心。

所谓依赖管理就是指 Maven 对项目所有依赖资源的一种管理,它和项目之间是一种双向关系,即当我们做项目的时候,Maven 的依赖管理可以帮助你去管理你所需要的其他资源;当其他的项目需要依赖我们项目的时候,Maven 也会把我们的项目当作一种资源去进行管理。这就是一种双向关系。

那 Maven 的依赖管理的资源存在哪儿呢?主要有三个位置:本地仓库、私服、中央仓库

  • 本地仓库顾名思义就是存储在本地的一种资源仓库;
  • 如果本地仓库中没有相关资源,可以去私服上获取,私服也是一个资源仓库,只不过不在本地,是一种远程仓库;
  • 如果私服上也没有相关资源,可以去中央仓库去获取,中央仓库也是一种远程仓库。

Maven 除了帮我们管理项目资源之外,还能帮助我们对项目进行构建,管理项目的整个生命周期。当然它的这些功能需要使用一些相关的插件来完成,并且整个生命周期过程中,插件是需要配合使用的,单独一个插件无法完成完整的生命周期。

2. Maven 作用及结构

Maven 的作用可以总结成三点:

  1. 项目构建:提供标准的、跨平台的自动化构建项目的方式。
  2. 依赖管理:方便快捷地管理项目依赖的资源(jar 包),避免资源间的版本冲突等问题。
  3. 统一开发结构:提供标准的、统一的项目开发结构。如下图所示:

各目录存放资源说明:

  • src/main/java:项目 Java 源码。
  • src/main/resources:项目的相关配置文件(比如 Mybatis 配置、XML 映射配置、自定义配置文件等)。
  • src/main/webapp:Web 资源(比如 HTML、CSS、JS 等)。
  • src/test/java:测试代码。
  • src/test/resources:测试相关配置文件。
  • src/pom.xml:项目 pom 文件。

3. Maven 环境搭建

maven 官网:Maven – Welcome to Apache Maven

1)登录官网下载解压版本

进入官方下载页面,如下所示:

最新版本: 

其他版本: 

2)解压缩

3)配置环境变量 MAVEN_HOME

  • 注意:变量名必须是 MAVEN_HOME

4)验证安装成功

启动命令行,输入:mvn --version

5)IDEA 配置 Maven

4. Maven 核心概念

1)仓库 

仓库用于存储资源,主要是各种 jar 包。

  • 中央仓库:Maven 团队自身维护的仓库,属于开源的。

  • 私服:各公司/部门等小范围内存储资源的仓库,私服也可以从中央仓库获取资源。

  • 本地仓库:开发者自己电脑上存储资源的仓库,也可从远程仓库获取资源。

私服的作用:

  1. 保存具有版权的资源,比如购买或自主研发的 jar 。
  2. 一定范围内共享资源,能做到仅对内而不对外开放。

2)坐标

Maven 的仓库里存储了各种各样的资源(jar 包),那我们是如何找到这些资源的呢?为此,我们需要知道它们具体的一个位置才能知道如何找到它们,这个就叫坐标。

坐标:使用唯一标识描述仓库中资源的位置,唯一性地定义资源位置。通过该标识可以将资源的识别与下载工作交由机器完成。

Central Repository:

那 Maven 中的坐标是如何构成的呢?主要组成如下:

  • groupId:定义当前资源隶属组织名称(通常是域名反写,如:org.mybatis、com.baidu)。

  • artifactId:定义当前资源的名称(通常是项目或模块名称,如:crm,sms)。

  • version:定义当前资源的版本号。

packaging:定义资源的打包方式,取值一般有如下三种:

  1. jar:该资源打成 jar 包,默认是 jar
  2. war:该资源打成 war 包
  3. pom:该资源是一个父资源(表明使用 maven 分模块管理),打包时只生成一个 pom.xml 而不生成 jar 或其他包结构。

如果要查询 Maven 某一个资源的坐标,通常我们可以去 Maven 的仓库进行查询:https://mvnrepository.com/ ,在该网站中可直接搜索想要的资源,然后就能得到该资源的坐标。

示例:

  • 输入资源名称进行检索:

  • 点击你想要的资源进行查看:

  • 选择版本查看坐标:

5. 仓库配置

开发者要在自己电脑上做开发,首先要做的就是配置本地仓库。

默认情况下 Maven 本地仓库的位置在哪儿呢?

1)全局配置

我们可以选择在全局进行配置,在 Maven 的配置文件conf/settings.xml中可以找到它的说明:

  <!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ${user.home}/.m2/repository
  <localRepository>/path/to/local/repo</localRepository>
  -->

也就是在系统盘当前用户目录下的.m2/repository,比如我当前的系统用户是zs,则默认的本地仓库仓库位置在C:\Users\zs\.m2\repository。

因为我们平时开发项目所有的资源会比较多,而且各种资源还有好多的版本,资源与资源之间还有相互依赖的这种情况,因此本地仓库保存的内容会非常的多,它的体积会很大,如果放在 C 盘下不太合适。

因此我们可以自己来指定一个位置作为本地仓库的位置,这个指定同样是需要来修改 Maven 的配置文件conf/settings.xml的<localRepository>/path/to/local/repo</localRepository>标签。

这个标签中配置的值就是我们本地仓库的位置,例如:

  <!-- localRepository
   | 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:\maven-repository</localRepository>

2)用户配置

如果是局部用户配置,那么在仓库的同级目录也可以包含一个settings.xml配置文件,在里面也可以进行指定。

注意:局部用户配置优先于全局配置(遇见相同配置项的时候)。

注意:Maven 默认连接的远程仓库位置是中央仓库。

此站点并不在国内,因此有时候下载速度非常慢,因此我们可以配置一个国内站点镜像,可用于加速下载资源。

我们在conf/settings.xml配置文件中找到<mirrors>标签,在这组标签下添加镜像的配置,如下:

<mirror>
    <id>nexus-aliyun</id>
    <mirrorOf>central</mirrorOf>
    <name>Nexus aliyun</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>

6. Maven 构建命令

Maven 的构建命令以mvn开头,后面添加功能参数,可以一次性执行多个命令,用空格分离:

  • mvn compile:编译
  • mvn clean:清理
  • mvn test:测试
    • -DskipTests,不执行测试用例,但编译测试用例类生成相应的 class 文件至 target/test-classes 下
    • -Dmaven.test.skip=true,不执行测试用例,也不编译测试用例类
  • mvn package:打包
  • mvn install:安装到本地仓库

7. IDEA 配置 Maven

  • 不使用原型创建项目

1)在 IDEA 中配置 Maven

2)创建 Maven 工程

3)填写本项目的坐标

4)检查各目录颜色标记是否正确

5)IDEA 右侧有一个 Maven 管理界面,可点开查看

  • 使用原型创建项目(普通 Java 工程)

1)创建 Maven 项目的时候选择使用原型骨架

2)创建完成后发现通过这种方式缺少一些目录

如下图:

为此,我们需要手动去补全目录,并且要对补全的目录进行标记(切记): 

  • Web 工程

1)选择 Web 对应的原型骨架

有很多的 webapp 原型骨架,选择哪个基本都差不多,包括前面创建普通项目也是一样, quickstart 原型也有很多。

2)和前面创建普通项目一样,通过原型创建 Web 项目得到的目录结构是不全的,因此需要我们自行补全,同时要标记正确

最终需要得到如下结构: 

3)Web 工程创建好之后需要启动运行,那么需要使用一个 tomcat 插件来运行我们的项目,在pom.xml中添加插件的坐标即可

最终改好的pom.xml如下: 

<?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>com.itheima</groupId>
  <artifactId>web01</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>web01 Maven Webapp</name>
  <url>http://www.example.com</url>

  <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>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <finalName>web01</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.1</version>
      </plugin>
    </plugins>
  </build>
</project>

4)插件配置好后,在 IDEA 右侧maven-project操作面板上可以看到该插件,并且可以利用该插件启动项目

运行后该插件会给我们一个可运行地址: 

如果我们想更换端口,只需要在pom.xml中配置该插件即可: 

<plugins>
    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.1</version>
        <configuration>
            <port>80</port>
        </configuration>
    </plugin>
<plugins>

5)同时为了运行方便,我们也可以创建运行模板

8. Maven 依赖管理

1)依赖配置与依赖传递

依赖是指在当前项目中运行所需的 jar,依赖配置的格式如下图:

2)依赖传递

依赖具有传递性,分两种:

  1. 直接依赖:在当前项目中通过依赖配置建立的依赖关系。

  2. 间接依赖:被依赖的资源如果依赖其他资源,则表明当前项目间接依赖其他资源。

注意:直接依赖和间接依赖其实也是一个相对关系。

依赖传递的冲突问题:

在依赖传递过程中产生了冲突时,有三种优先法则:

  1. 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,反之则越高。

  2. 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖靠后的。

  3. 特殊优先:当同级配置了相同资源的不同版本时,后配置的覆盖先配置的。

可选依赖: 

排除依赖: 

9. 依赖范围

依赖的 jar 默认情况可以在任何地方可用,可以通过scope标签设定其作用范围。

这里的范围主要是指以下三种范围:

  1. 主程序范围有效(src/main 目录范围内)

  2. 测试程序范围内有效(src/test 目录范围内)

  3. 是否参与打包(package 指令范围内)

此外,scope标签的取值有四种:compile、test、provided、runtime。

这四种取值与范围的对应情况如下:

依赖范围的传递性: 

10. Maven生命周期

Maven 构建的生命周期,描述的是一次构建过程中经历了多少个事件。

比如我们项目最常用的一套流程如下:

1)clean:清理工作

  1. pre-clean:执行一些在 clean 之前的工作。
  2. clean:移除上一次构建产生的所有文件。
  3. post-clean:执行一些在 clean 之后立刻完成的工作。

2)default:核心工作。例如编译、测试、打包、部署等

这里面的事件非常得多,如下图:

对于 default 生命周期,每个事件在执行之前都会将之前的所有事件依次执行一遍。

3)site:产生报告,发布站点等

  • pre-site:执行一些在生成站点文档之前的工作。
  • site:生成项目的站点文档。
  • post-site:执行一些在生成站点文档之后完成的工作,为部署做准备。
  • site-deploy:将生成的站点文档部署到特定的服务器上。

11. Maven 插件

前面我们讲了 Maven 生命周期中的相关事件,那这些事件是谁来执行的呢?答案是 Maven 的插件。

插件:

  • 插件与生命周期内的阶段绑定,在执行到对应生命周期时执行对应的插件。
  • Maven 默认在各个生命周期上都绑定了预先设定的插件来完成相应功能。
  • 插件还可以完成一些自定义功能。

插件的配置方式如下:

在 Maven 官网中有对插件的介绍:Maven – Available Plugins

12. maven-surefire-plugin

1)maven-surefire-plugin简介

如果你执行过 mvn test 或者执行其他 maven 命令时跑了测试用例,你就已经用过 maven-surefire-plugin 了。maven-surefire-plugin 是 maven 里执行测试用例的插件,不显示配置就会用默认配置。这个插件的 surefire:test 命令会默认绑定 maven 执行的 test 阶段。

2)maven的生命周期有哪些阶段

[validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy]

3)maven-surefire-plugin的使用

如果说 maven 已经有了 maven-surefire-plugin 的默认配置,我们还有必要了解 maven-surefire-plugin 的配置么?答案是肯定的。虽说 maven-surefire-plugin 有默认配置,但是当需要修改一些测试执行的策略时,就有必要我们去重新配置这个插件了。

插件自动匹配:

最简单的配置方式就不配置或者是只声明插件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19</version>
</plugin>

这个时候 maven-surefire-plugin 会按照如下逻辑去寻找 JUnit 的版本并执行测试用例。

if the JUnit version in the project >= 4.7 and the parallel attribute has ANY value
    use junit47 provider
if JUnit >= 4.0 is present
    use junit4 provider
else
    use junit3.8.1

插件手动匹配

    <build>
        <plugins>
            <!-- 该插件能够在运行后自动在target目录生成allure测试结果目录 -->
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M5</version>
                <configuration>
                    <includes>
                        <!-- 默认测试文件的命名规则:
                            "**/Test*.java"
                            "**/*Test.java"
                            "**/*Tests.java"
                            "**/*TestCase.java"
                            如果现有测试文件不符合以上命名,可以在 pom.xml 添加自定义规则
                        -->
                        <include>**/**.java</include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>

四、Java语法基础与数据结构

1、Java 语法

“Hello World”示例:

main() 方法详解: 

  • public:表示公共的。权限是最大的,在任何情况下都可以访问。
    • 原因:为了保证 JVM 在任何情况下都可以访问到 main 方法。
  • static:可以使 JVM 调用 main 方法更加方便,而不需要通过对象调用。
    • 不使用 static 修饰的麻烦:
      1. 需要创建对象调用。
      2. JVM 不知道如何创建对象,因为创建对象有些是需要参数的。
  • void:因为返回的数据是给 JVM 的,而 JVM 使用这个数据是没有意义的。
  • main:函数名。
  • arguments:担心某些程序在启动时需要参数。

问题 1:一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?

  • 可以有多个类,但只能有一个 public 的类,并且 public 的类名必须与文件名相一致。一个文件中可以只有非 public 类,如果只有一个非 public 类,那么此类可以跟文件名不同。

问题 2:为什么一个 Java 源文件中只能有一个 public 类?

  • 在 Java 编程思想(第四版)一书中有这样 3 段话(6.4 类的访问权限):
    1. 每个编译单元(文件)都只能有一个 public 类,这表示,每个编译单元都有单一的公共接口,用 public 类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的 public 类,编译器就会给出错误信息。
    2. public 类的名称必须完全与含有该编译单元的文件名相同,包含大小写。如果不匹配,同样将得到编译错误。
    3. 虽然不是很常用,但编译单元内完全不带 public 类也是可能的。在这种情况下,可以随意对文件命名。

2、注释

注释是对代码的解释和说明文字,可以提高程序的可读性,因此在程序中添加必要的注释文字十分重要。

Java 中的注释分为三种:

1)单行注释:单行注释的格式是使用//,从//开始至本行结尾的文字将作为注释文字。

// 单行注释

2)多行注释:多行注释的格式是使用/*和*/将一段较长的注释括起来。

/*
多行注释
注意:多行注释不能嵌套使用
*/

3)文档注释:文档注释以/**开始,以*/结束,是 Java 特有的注释,其中注释内容可以被 JDK 提供的工具 javadoc 所解析,生成一套以网页文件形式体现的该程序的说明文档。

Javadoc –d 指定存储文档的路径 -version –author(可选参数) 目标文件

3、关键字

Java 的关键字对 Java 的编译器有特殊的意义,他们常被用来表示一种数据类型,或者表示程序的结构等。关键字不能用作变量名、方法名、类名、包名。

关键字的特点:

  • 关键字的字母全部小写。
  • 常用的代码编辑器对关键字都有高亮显示,比如 public、class、static 等。
  • 注意:main 不是关键字,但可以理解为比关键字更为关键的单词,是 JVM 唯一识别的单词。

4、标识符

标识符是程序员在编写 Java 程序时,自定义的一些名字,例如 helloworld 程序里关键字 class 后跟的“HelloWorld”,就是我们定义的类名。类名就属于标识符的一种。

标识符除了应用在类名上,还可以用在变量、函数名、包名上。

标识符必须遵循以下规则:

  • 标识符由英文字母(a~zA~Z)、数字(0~9)、下划线(_)和美元符号($)组成。
  • 不能以数字开头,不能是关键字。
  • 区分大小写。
  • 标识符可以为任意长度。

Java 中的标识符命名规范:

  • 项目名:多个单词组成时所有字母小写(例:workdesk、jobserver)
  • 包名:多个单词组成时所有字母小写(例:package、com.util)
  • 类名和接口名:多个单词组成时所有单词的首字母大写(例:HelloWorld)
  • 变量名和函数名:多个单词组成时第一个单词首字母小写,其他单词首字母大写(例:lastAccessTime、getTime)。
  • 常量名:多个单词组成时,字母全部大写,多个单词之间使用_分隔(例:INTEGER_CACHE)

注意:上述只是为了增加代码规范性、可读性而做的一种约定,但在定义标识符时最好还是见名知意,提高代码阅读性。

5、Java数据类型

Java 是一个强类型语言,其数据必须明确数据类型。

在 Java 中,数据类型包括基本数据类型和引用数据类型两种。

1. 基本数据类型

Java 的基本数据类型有 4 类 8 种:

四类八种内存占用
(字节)
取值范围说明
整数类型byte1最小值是 -128(-2^7)
最大值是 127(2^7-1)
默认值是 0
byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一。
short2最小值是 -32768(-2^15)
最大值是 32767(2^(15-1))
默认值是 0
short 数据类型也可以像 byte 那样节省空间。一个 short 变量是 int 型变量所占空间的二分之一。
int4最小值是 -2,147,483,648(-2^31)
最大值是 2,147,483,647(2^(31-1))
默认值是 0
整数默认是 int 类型(因此 byte、short 和 char 类型数据在参与整数运算均会自动转换为 int 类型)
long8最小值是 -9,223,372,036,854,775,808(-2^63)
最大值是 9,223,372,036,854,775,807(2^(63-1))
默认值是 0L
这种类型主要使用在需要比较大整数的系统上;
"L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。
浮点类型float4默认值是 0.0ffloat 数据类型是单精度、32 位、符合IEEE 754 标准的浮点数;
范围规模可变,保留 7 位小数;
float 在储存大型浮点数组的时候可节省内存空间;
浮点数不能用来表示精确的值,如货币;
double(默认类型)8默认值是 0.0ddouble 数据类型是双精度、64 位、符合 IEEE 754 标准的浮点数;
范围规模可变,保留 15 位小数;
浮点数的默认类型为 double 类型;
double类型同样不能表示精确的值,如货币。
字符类型char2最小值是 \u0000(十进制等效值为 0) 
最大值是 \uffff(即为 65535)
char 类型是一个单一的 16 位 Unicode 字符;
char 数据类型可以存储任何字符。
布尔类型boolean1只有两个取值:true 和 false
默认值是 false
boolean 数据类型表示一位的信息。

2. 引用数据类型

引用数据类型有类(class)、接口(Interface)、数组(Array)等。

// 声明两个 Book 的引用变量并创建两个 Book 对象,然后将 Book 对象赋值给引用变量。
Book b = new Book();
Book c = new Book();

b = c;  // 把变量 c 赋值给变量 b,此时 b、c 对应 Book2,而 Book1 已经没有引用,会被垃圾回收。

c = null;  // 代表它不再引用任何事物,但还是个可以被指定引用其他Book的引用变量。

3. 隐式类型转换

隐式类型转换是指把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。

这种转换方式是自动的,直接书写即可。例如:

double num = 10;  // 将 int 类型的 10 直接赋值给 double 类型
System.out.println(num);  // 输出 10.0

整数默认是 int 类型,因此 byte、short 和 char 类型数据在参与整数运算均会自动转换为 int 类型。

即多个不同数据类型的数据在运算的时候,结果取决于大的数据类型。

4. 强制类型转换

强制类型转换是指把一个表示数据范围大的数值或者变量赋值给另一个表示数据范围小的变量。

强制类型转换格式:目标数据类型 变量名 = (目标数据类型)值或者变量;

double num1 = 5.5;
int num2 = (int) num1;  // 将 double 类型的 num1 强制转换为 int 类型
System.out.println(num2);  // 输出 5(小数位直接舍弃)

byte a = 3;
byte b = 4;
byte c = a + b;  // 报错。因为两个 byte 变量相加,会先提升为 int 类型
byte d = 3 + 4;  // 正确。常量优化机制

常量优化机制:编译器在编译的时候能确认常量的值,但不能确认变量的值(变量存储的值只有在运行的时候才会在内存分配空间)。

在上例中,在编译时,整数常量的计算会直接算出结果,并且会自动判断该结果是否在 byte 取值范围内,在则编译通过,不在则编译失败。

查看数据类型:

    public static void main(String[] args) {
        int n1 = 1;
        Integer n2 = 1;
        String s = "1";
        Book book = new Book();
        // System.out.println(n1.getClass().getName());  此行报错;需使用下行的间接方法
        System.out.println(getType(n1));  // class java.lang.Integer
        System.out.println(n2.getClass().getName());  // java.lang.Integer
        System.out.println(s.getClass().getName());  // java.lang.String
    }

    public static String getType(Object o){  // 获取变量类型方法
        return o.getClass().toString();  // 使用int类型的getClass()方法
    }

6、变量

变量的定义:在程序运行过程中,其值可以发生改变的量。

从本质上讲,变量是内存中的一小块区域,其值可以在一定范围内变化。

变量的定义方式有如下 3 种:

1. 声明变量并赋值

数据类型 变量名 = 初始化值;
int age = 18;
System.out.println(age);

2. 先声明,后赋值(在使用前赋值即可)

数据类型 变量名;
变量名 = 初始化值;
double money;
money = 55.5;
System.out.println(money);

3. 在同一行定义多个同一种数据类型的变量,中间使用逗号隔开。但不建议使用这种方式,因为降低了程序的可读性。

int a = 10, b = 20;  // 定义int类型的变量a和b,中间使用逗号隔开
System.out.println(a);
System.out.println(b);

int c, d;  // 声明int类型的变量c和d,中间使用逗号隔开
c = 30;
d = 40;
System.out.println(c);
System.out.println(d);

变量的修改:

int a = 10;
a = 30;  // 变量前面不加数据类型时,表示修改已存在的变量的值。
System.out.println(a);

7、运算符

算术运算符:

小数运算:

// 整数操作只能得到整数,要想得到小数,必须有浮点数参与运算。
int a = 10;
int b = 3;
System.out.println(a / b);  // 输出结果 3
System.out.println(a % b);  // 输出结果 1

取余的结果正负:

int num = -10;  // 运算结果正负取决于被除数的正负

System.out.println(num % 2);  // 0
System.out.println(num % -3);  // -1
System.out.println(num % 4);  // -2
System.out.println(num % 5);  // 0
System.out.println(num % -6);  // -4

字符的+操作:

char 类型参与算术运算,使用的是计算机底层对应的十进制数值:

  • 'a' -- 97
  • 'A' -- 65
  • '0' -- 48
// 可以通过使用字符与整数做算术运算,得出字符对应的数值是多少
char ch1 = 'a';
System.out.println(ch1 + 1);  // 输出 98(97 + 1 = 98)

char ch2 = 'A';
System.out.println(ch2 + 1);  // 输出 66(65 + 1 = 66)

char ch3 = '0';
System.out.println(ch3 + 1);  // 输出 49(48 + 1 = 49)

数据类型隐式提升:

算术表达式中包含不同的基本数据类型的值的时候,整个算术表达式的类型会自动进行提升。

提升规则:

  • byte、short 和 char 类型自动提升到 int 类型,不管是否有其他类型参与运算。
  • 整个表达式的类型自动提升到与表达式中最高等级的操作数相同的类型。等级顺序:byte、short、char --> int --> long --> float --> double
byte b1 = 10;
byte b2 = 20;
// byte b3 = b1 + b2;  // 该行报错,因为 byte 类型参与算术运算会自动提示为  int,而 int 赋值给 byte 可能会导致精度损失
int i3 = b1 + b2;  // 应该使用 int 接收
byte b3 = (byte) (b1 + b2);  // 或者将结果强制转换为 byte 类型

int num1 = 10;
double num2 = 20.0;
double num3 = num1 + num2;  // 使用 double 接收,因为 num1 会自动提升为 double 类型

字符串的+操作:

在“+”操作中,如果出现了字符串,就是连接运算符,否则就是算术运算。当连续进行“+”操作时,从左到右逐个执行。

System.out.println(1 + 99 + "年");  // 输出:100年
System.out.println(1 + 2 + "+" + 3 + 4);  // 输出:3+34
// 可以使用小括号改变运算的优先级
System.out.println(1 + 2 + "+" + (3 + 4));  // 输出:3+7

数值拆分:

需求:键盘录入一个三位数,将其个位、十位、百位数的值分别打印在控制台。

import java.util.Scanner;

public class Day1 {
    public static void main(String[] arg){
        // 1. 使用Scanner键盘录入一个三位数
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个三位数:");
        int num = sc.nextInt();
        // 2. 个位的计算:数值 % 10
        int ge = num % 10;
        // 3. 十位的计算:数值 / 10 % 10
        int shi = num / 10 % 10;
        // 4. 百位的计算:数值 / 100
        int bai = num / 100;
        // 5. 将个位、十位、百位拼接上正确的字符串, 打印即可
        System.out.println("整数"+num+"个位为:"+ge);
        System.out.println("整数"+num+"十位为:"+shi);
        System.out.println("整数"+num+"百位为:"+bai);
    }
}

自增自减运算符:

符号作用说明
++自增变量的值加 1
--自减变量的值减 1

注意事项:

  • ++ 和 -- 既可以放在变量的后边,也可以放在变量的前边。
  • 单独使用的时候, ++ 和 -- 无论是放在变量的前边还是后边,结果是一样的。
  • 参与操作的时候,如果 ++/-- 放在变量的后边,则先拿变量参与操作,后拿变量做 ++ 或者 -- 运算;如果 ++/-- 放在变量的前边,则先拿变量做 ++ 或者 -- 运算,后拿变量参与操作。

最常见的用法:

int i = 10;
i++;  // 单独使用
System.out.println("i:"+i);  // i:11

int j = 10;
++j;  // 单独使用
System.out.println("j:"+j);  // j:11

int x = 10;
int y = x++;  // 赋值运算,++ 在后边,所以是使用 x 原来的值赋值给 y,x 本身自增 1
System.out.println("x:"+x+ ", y:"+y);  // x:11,y:10

int m = 10;
int n = ++m;  // 赋值运算,++ 在前边,所以是使用 m 自增后的值赋值给 n,m 本身自增 1
System.out.println("m:"+m+", m:"+m);  // m:11,m:11

思考题:

int x = 10;
int y = x++ + x++ + x++;
System.out.println(y);  // y 的值是多少?
/*
解析,三个表达式都是 ++ 在后,所以每次使用的都是自增前的值,而程序是从左至右执行的,所以第一次自增时,使用的是 10 进行计算;第二次自增时,x 的值已经自增到 11 了,所以第二次使用的是 11;然后第三次再次自增。
所以整个式子应该是:int y = 10 + 11 + 12,最终输出结果为 33。
*/

注意:通过此示例可以进一步理解自增和自减的规律,但实际开发中不建议写这样的代码,小心挨打!

8、赋值运算符

赋值运算符的作用是将一个表达式的值赋给左边,左边必须是可修改的,不能是常量。

符号作用说明
=赋值a = 10,将 10 赋值给变量 a
+=加后赋值a += b,将 a+b 的值给 a
-=减后赋值a -= b,将 a-b 的值给 a
*=乘后赋值a *= b,将 a×b 的值给 a
/=除后赋值a /= b,将 a÷b 的商给 a
%=取余后赋值a %= b,将 a÷b 的余数给 a

注意:赋值运算符隐含了强制类型转换。

short s = 10;
s = s + 10;  // 此行代码报错,因为运算中 s 提升为了 int 类型

s += 10;  // 此行代码不报错,因为隐含了强制类型转换,相当于 s = (short) (s+10);

9、关系运算符

符号说明
==比较基本数据类型时,比较的是值内容 
比较引用数据类型时,比较的是对象地址 
成立为 true,不成立为 false
!=a != b,判断 a 和 b 的值是否不相等,成立为 true,不成立为 false
>a > b,判断 a 是否大于 b,成立为 true,不成立为 false
>=a >= b,判断 a 是否大于等于 b,成立为 true,不成立为 false
<a < b,判断 a 是否小于 b,成立为 true,不成立为 false
<=a <= b,判断 a 是否小于等于 b,成立为 true,不成立为 false
int a = 10;
int b = 20;
System.out.println(a == b);  // false
System.out.println(a != b);  // true
System.out.println(a > b);  // false
System.out.println(a >= b);  // false
System.out.println(a < b);  // true
System.out.println(a <= b);  // true

// 关系运算的结果肯定是 boolean 类型,所以也可以将运算结果赋值给 boolean 类型的变量
boolean flag = a > b;
System.out.println(flag);  // false

10、逻辑运算符

逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,以判断程序中的表达式是否成立,判断的结果是 true 或 false。

符号作用说明
&逻辑与a&b,若 a 和 b 都是 true,结果为 true,否则为 false
|逻辑或a|b,若 a 和 b 都是 false,结果为 false,否则为 true
^逻辑异或a^b,若 a 和 b 结果不同,结果为 true,相同为 false
!逻辑非!a,则结果和 a 的结果正好相反

异或运算符的特点: 

    // 一个数被另外一个数,异或两次, 该数本身不变
    public static void main(String[] args) {
        System.out.println(10 ^ 5 ^ 10);  // 5
    }

示例:已知两个整数变量 a = 10、b = 20,使用程序实现这两个变量的数据交换。

public class Test {

    public static void main(String[] args) {
        int[] result1 = change1(10, 20);
        int[] result2 = change2(10, 20);
        int[] result3 = change3(10, 20);
        System.out.println(result1[0]+" "+result1[1]);  // 20 10
        System.out.println(result2[0]+" "+result2[1]);  // 20 10
        System.out.println(result3[0]+" "+result3[1]);  // 20 10
    }

    // 方法1:利用第三方变量
    public static int[] change1(int a, int b) {
        int tmp = b;
        b = a;
        a = tmp;
        int[] arr = {a, b};
        return arr;
    }

    // 方法2:利用算术运算符
    public static int[] change2(int a, int b) {
        a = a + b;
        b = a - b;
        a = a - b;
        int[] arr = {a, b};
        return arr;
    }

    // 方法3:利用异或运算符
    public static int[] change3(int a, int b) {
        a = a ^ b;
        b = a ^ b;  // = a ^ b ^ b = a
        a = a ^ b;  // = a ^ b ^ a ^ b ^ b = b
        int[] arr = {a, b};
        return arr;
    }

}

11、短路逻辑运算符 

符号作用说明
&&短路与作用和 & 相同,但是有短路效果
||短路或作用和 | 相同,但是有短路效果

在逻辑与运算中,只要有一个表达式的值为 false,那么结果就可以判定为 false 了,没有必要将所有表达式的值都计算出来,而短路与操作就有这样的效果,这样可以提高效率。同理在逻辑或运算中,一旦发现值为 true,右边的表达式将不再参与运算。

  • 逻辑与 &,无论左边真假,右边都要执行。

  • 短路与 &&,如果左边为真,右边执行;如果左边为假,右边不执行。

  • 逻辑或 |,无论左边真假,右边都要执行。

  • 短路或 ||,如果左边为假,右边执行;如果左边为真,右边不执行。

int x = 3;
int y = 4;
System.out.println((x++ > 4) & (y++ > 5));  // 两个表达都会运算
System.out.println(x);  // 4
System.out.println(y);  // 5

System.out.println((x++ > 4) && (y++ > 5));  // 左边已经可以确定结果为 false,则右边不参与运算
System.out.println(x);  // 4
System.out.println(y);  // 4

12、三元运算符

语法:

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

解释:如果条件表达式成立或者满足,则执行前面的表达式 1,否则执行后面的表达式 2。

举例:

int a = 10;
int b = 20;
int c = a > b ? a : b;  // 判断 a > b 是否为真,如果为真则取 a 的值,如果为假则取 b 的值

三元运算符案例:

需求:一座寺庙里住着三个和尚,已知他们的身高分别为 150cm、210cm、165cm,请用程序实现获取这三个和尚的最高身高。

public class OperatorTest02 {
    public static void main(String[] args) {
    //1. 定义三个变量用于保存和尚的身高,单位为 cm,这里仅仅体现数值即可。
    int height1 = 150;
    int height2 = 210;
    int height3 = 165;
    //2. 用三元运算符获取前两个和尚的较高身高值,并用临时身高变量保存起来。
    int tempHeight = height1 > height2 ? height1 : height2;
    //3. 用三元运算符获取临时身高值和第三个和尚身高较高值,并用最大身高变量保存。
    int maxHeight = tempHeight > height3 ? tempHeight : height3;
    //4. 输出结果
    System.out.println("maxHeight:" + maxHeight);
    }
}

13、位移运算符

位运算符指的是二进制位的运算,先将十进制数转成二进制后再进行运算。在二进制位运算中,1 表示 true,0 表示 false。

示例: 

public class Test {
    /*

        << 有符号左移运算,二进制位向左移动, 左边符号位丢弃, 右边补齐0
    运算规律: 向左移动几位, 就是乘以2的几次幂

    12 << 2

    (0)0000000 00000000 00000000 000011000  // 12的二进制

       -----------------------------------------------------------------------------
        >> 有符号右移运算,二进制位向右移动, 使用符号位进行补位
    运算规律: 向右移动几位, 就是除以2的几次幂

    000000000 00000000 00000000 0000001(1)  // 3的二进制

       -----------------------------------------------------------------------------

    >>> 无符号右移运算符,  无论符号位是0还是1,都补0

    010000000 00000000 00000000 00000110  // -6的二进制

     */
    public static void main(String[] args) {
        System.out.println(12 << 1);  // 24
        System.out.println(12 << 2);  // 48
    }
    
}

五、Java null空值

对于 Java 程序员来说,空指针一直是恼人的问题,我们在开发中经常会受到 NullPointerException 的蹂躏和壁咚。Java 的发明者也承认这是一个巨大的设计错误。

那么关于 null,我们应该知道下面这几件事情来有效地了解 null,从而避免很多由 null 引起的错误。

  1. 大小写敏感

  2. null 是任何引用类型的初始值

  3. null 既不是对象也不是类型,它是一种特殊的值,你可以将它赋值给任何引用类型

  4. null 不能赋值给基本数据类型

  5. 将 null 赋给包装类,自动拆箱会报 NPE

  6. 带有 null 的引用类型变量,instanceof 会报 false

  7. 静态变量为 null 时,调用静态方法不会抛出 NPE

  8. 使用 null 值安全的方法

  9. 使用 == 或 != 判断 null

1、大小写敏感

首先,null 是 Java 中的关键字,像是 public、static、final 等。它是大小写敏感的,你不能将 null 写成 Null 或 NULL,这样编辑器将不能识别它们然后报错。

不过这个问题已经几乎不会出现,因为 eclipse 和 Idea 已经给出了编辑器提示。

2、null 是任何引用类型的初始值

null 是所有引用类型的默认值,Java 中的任何引用变量都将 null 作为默认值,也就是说所有 Object 类下的引用类型默认值都是 null。这对所有的引用变量都适用。

就像是基本类型的默认值一样,例如 int 的默认值是 0,boolean 的默认值是 false 。

3、null 只是一种特殊的值

null 既不是对象也不是类型,它是一种特殊的值,你可以将它赋值给任何引用类型。

public static void main(String[] args){ 
    String str = null;
    Integer itr = null;
    Double dou = null;
    
    Integer integer = (Integer) null; 
    String string = (String) null; 
    
    Systemout.printlnC"integer=" + integer);
    System.out.printlnc"string=" + string); 
}

你可以看到在编译期和运行期内,将 null 转换成任何的引用类型都是可行的,并且不会抛出空指针异常。

null 只能赋值给引用变量,不能赋值给基本类型变量。

持有 null 的包装类在进行自动拆箱的时候,不能完成转换,会抛出空指针异常,并且 null 也不能和基本数据类型进行对比:

public static void main(String[] args){ 
    int i = 0;
    Integer itr = null;
    System.out.println(itr == i); 
}

使用了带有 null 值的引用类型变量,instanceof 操作会返回 false:

public static void main(String[] args){ 

    Integer isNull = null;
    // instanceof = isInstance 方法 
    if(isNull instanceof Integer){
        System.out.println("isNull is instanceof Integer");
    }else{
        System.out.println("isNull is not instanceof Integer");
    }
}

这是 instanceof 操作符一个很重要的特性,使得对类型强制转换检查很有用。

静态变量为 null 时,调用静态方法不会抛出 NullPointerException,因为静态方法使用了静态绑定。

4、使用 Null-Safe 方法

你应该使用 null-safe 安全的方法,java 类库中有很多工具类都提供了静态方法,例如基本数据类型的包装类如 Integer,Double 等。

public class NullSafeMethod {

    private static String number; 
	
    public static void maincstring args){ 
        String s = String.valueOf(number);
        String string = number.toString();
        System.out.println("s=" + s); 
        System.out.println("string=" + string); 
    }
}

number 没有赋值,所以默认为 null,使用 String.value(number) 静态方法没有抛出空指针异常,但是使用 toString() 却抛出了空指针异常。所以尽量使用对象的静态方法。

5、null 判断

你可以使用 == 或者 != 操作来比较 null 值,但是不能使用其他算法或者逻辑操作,例如小于或者大于。

跟 SQL 不一样,在 Java 中 null ==null 将返回 true,如下所示:

public class CompareNull { 

    private static string str1;
    private static string str2;

    public static void main(String[] args){
        System.out.println("str1 == str2?" + str1==str2);
        System.out.println(null == null);
    } 
}

六、Java数组详解

1、数组的定义格式

数组是存储同类型数据,且数组本身长度固定的容器。

// 第一种定义格式:数据类型[] 数组名
int[] arr;
double[] arr;
char[] arr;

// 第二种定义格式:数据类型 数组名[]
int arr[];
double arr[];
char arr[];

2、数组的动态初始化

数组动态初始化是指只给定数组的长度,而由系统给出默认的初始值。

格式:

数据类型[] 数组名 = new 数据类型[数组长度];
public class Test {
    public static void main(String[] args) {
        int[] iArray = new int[5];
        System.out.println(iArray);  // [I@4554617c
        byte[] bArray = new byte[5];  // [B@74a14482
        System.out.println(bArray);
    }
}

[I@4554617c打印说明:

  • @:分隔符。
  • [:当前的空间是一个数组类型。
  • I:当前数组容器中所存储的数据类型。
  • 4554617c:十六进制内存地址。

3、数组索引

每一个存储到数组的元素,都会自动拥有一个编号,从 0 开始。

这个自动编号称为数组索引(index),可以通过数组的索引访问到数组中的元素。

访问数组元素格式:数组名[索引];

public class Test {
    public static void main(String[] args) {

        int[] arr = new int[3];
        System.out.println(arr);  // 数组的内存地址  [I@4554617c

        // 数组名[索引]:访问数组容器中的空间位置
        System.out.println(arr[0]);  // 0  系统自动分配的默认初始化值
        System.out.println(arr[1]);  // 0
        System.out.println(arr[2]);  // 0

        System.out.println("--------------");

        // 给指定索引位的元素赋值:数组名[索引] = 值
        arr[0] = 11;
        arr[1] = 22;
        arr[2] = 33;

        System.out.println(arr[0]);  // 11
        System.out.println(arr[1]);  // 22
        System.out.println(arr[2]);  // 33
    }
}

4、数组的内存分配

内存是计算机中的重要原件,是临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的,必须放进内存中才能运行,且运行完毕后会清空内存。

Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。

Java 中的内存分配:

一个数组的内存分配:

两个数组的内存分配: 

多个数组指向相同的内存: 

5、数组的静态初始化

数组的静态初始化是指在创建数组时,就直接将元素确定。

// 完整版格式
数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, ...};

// 简化版格式
数据类型[] 数组名 = {元素1, 元素2, ...};

示例:

    public static void main(String[] args) {
        // 数据类型[] 数组名 = new 数据类型[]{数据1, 数据2, 数据3...};
        int[] arr = new int[]{11, 22, 33};
        System.out.println(arr[0]);
        System.out.println(arr[1]);
        System.out.println(arr[2]);

        // 数据类型[] 数组名 = {数据1, 数据2, 数据3...};
        int[] arr2 = {44, 55, 66};
        System.out.println(arr2);
        System.out.println(arr2[0]);
        System.out.println(arr2[1]);
        System.out.println(arr2[2]);
    }

6、数组的遍历

数组遍历,就是将数组中的每个元素分别获取出来。遍历也是数组操作中的基石。

public class Test {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4};
	// for 循环实现数组遍历
        for(int i=0; i<arr.length; i++){
            System.out.println(arr[i]);
        }
    }
}

获取数组中的最大值:

public class Test {
    public static void main(String[] args) {
        int[] arr = {11, 4, 65, 34, 76, 23};
        // 初始化最大值为首位元素
        int maxValue = arr[0];
        // 从索引[1]开始的元素依次与当前最大值比较
        for(int i=1; i<arr.length; i++){
            // 若当前元素值比最大值大,则将其赋值给最大值
            if(arr[i] > maxValue){
                maxValue = arr[i];
            }
        }
        // 打印最终结果
        System.out.println(maxValue);
    }
}

数组元素求和:

键盘录入 5 个整数,存储到数组中,并对数组求和。

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        // 初始化长度为 5 的数组
        int[] arr = new int[5];
        // 初始化结果
        int sumResult = 0;
        Scanner sc = new Scanner(System.in);
        for(int i=0; i<5; i++){
            System.out.println("请输入需要存储的第"+(i+1)+"个整数:");
            arr[i] = sc.nextInt();
            sumResult += arr[i];
        }
        // 打印最终结果
        System.out.println(sumResult);
    }
}

数组的元素索引位查找:

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        // 初始化长度为 5 的数组
        int[] arr = {12, 43, 55, 77, 34, 54, 23, 52, 76};
        // 初始化索引,若元素不存在则返回索引值为-1
        int index = -1;
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入需要查找的元素:");
        int targetValue = sc.nextInt();
        for(int i=0; i<arr.length; i++){
            if(arr[i]==targetValue){
                System.out.println("索引值为:"+i);
                break;
            }
        }
    }
}

示例:已知一个数组 arr = {19, 28, 37, 46, 50}; 用程序实现把数组中的元素值交换,交换后的数组 arr = {50, 46, 37, 28, 19},并在控制台输出交换后的数组元素。

public class Test {
    public static void main(String[] args) {
        int[] arr = {19, 28, 37, 46, 50};
        int start = 0;
        int end = arr.length - 1;
        while(start < end){  // for(; start<end; start++)
            int tmp = arr[start];
            arr[start] = arr[end];
            arr[end] = tmp;
            start++;
            end--;
        }
        // 打印最终结果
        for(int i=0; i<arr.length; i++){
            System.out.print(arr[i]+" ");  // 50 46 37 28 19
        }
    }
}

7、二维数组

二维数组也是一种容器,不同于一维数组,该容器存储的元素是一维数组。

1. 动态初始化

格式:数据类型[][] 变量名 = new 数据类型[m][n];

  • m 表示这个二维数组,可以存放多少个一维数组。
  • n 表示每一个一维数组,可以存放多少个元素。
// 示例
int[][] arr = new int[3][3];

2. 静态初始化

完整格式:数据类型[][] 变量名 = new 数据类型[][]{{元素1, 元素2, ...}, {元素1, 元素2, ...}, ...};

简化格式:数据类型[][] 变量名 = {{元素1, 元素2, ...}, {元素1, 元素2, ...}, ...};

示例:

public class Test {
    public static void main(String[] args) {
		int[][] arr = {{11, 22, 33}, {44, 55, 66}};
        System.out.println(arr[0][2]);

        int[] arr1 = {11, 22, 33};
        int[] arr2 = {44, 55, 66};
        int[][] array = {arr1, arr2};
        System.out.println(array[0][2]);
    }
}

示例:二维数组遍历。

public class Test {

    public static void main(String[] args) {
        int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
        for(int i=0; i<arr.length; i++){
            for(int j=0; j<arr[0].length; j++){
                System.out.print(arr[i][j]+" ");
            }
            System.out.println();
        }
    }
}

8、Arrays(数组工具类)

Arrays 的常用方法:

  1. 二分查找(数组需要有序)

    • public static int binarySearch(int[], int args)
    • public static int binarySearch(double[], double args)
  2. 数组排序

    • public static void sort(int[])
    • public static void sort(char[])
  3. 数组的字符串形式

    • public static String toString(int[])
  4. 复制数组

    • public static T[] copyOf(T[] original, int newLength)
      • original:源数组
      • newLength:新数组的长度
  5. 复制数组的一部分

    • public static T[] copyOfRange(T[] original, fromIndex, toIndex)
      • original:源数组
      • fromIndex:开始截取的索引位
      • toIndex:结束截取的索引位(不包含)
  6. 比较两个数组的元素值(包括元素顺序)是否完全一致

    • public static boolean equals(T[], T[])
  7. 将数组转成集合(集合转数组:list.toArray())

    • public static List<T> List asList(T[])
  8. 将数组转成流
    public static Stream Arrays.stream()

示例:

import java.util.Arrays;

public class Test {

    public static void main(String[] args) {

        // public static String toString(int[] a):返回指定数组的内容的字符串表示形式
        int [] arr1 = {3, 2, 4, 6, 7};
        System.out.println(Arrays.toString(arr1));  // 字符串:[3, 2, 4, 6, 7]

        // public static void sort(int[] a):按照数字顺序排列指定的数组
        int [] arr2 = {3, 2, 4, 6, 7};
        Arrays.sort(arr2);
        System.out.println(Arrays.toString(arr2));  // 字符串:[2, 3, 4, 6, 7]

        // public static int binarySearch(int[] a, int key):利用二分查找返回指定元素的索引
        // 1. 数组必须有序
        // 2. 如果要查找的元素存在,那么返回的是这个元素实际的索引
        // 3. 如果要查找的元素不存在,那么返回的是 (-插入点-1)
            // 插入点:如果这个元素在数组中,他应该在哪个索引上
        int [] arr3 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int index = Arrays.binarySearch(arr3, 0);
        System.out.println(index);  // -1

        // public static List<T> List asList(T[]):数组转集合
        // 方式一
        List<String> list = Arrays.asList("a", "b", "c");
        System.out.println(list);  // ["a", "b", "c"]
        // 方式二
        int[] a1 = new int[]{1, 2, 3};
        Integer[] a2 = new Integer[]{1, 2, 3};
        String[] s1 = new String[]{"1", "2", "3"};
        System.out.println(Arrays.asList(a1));  // [[I@3af49f1c] ,存储的是int[]这个数组对象
        System.out.println(Arrays.asList(a2));  // [1, 2, 3] ,存储的是每个数组的元素
        System.out.println(Arrays.asList(s1));  // [1, 2, 3] ,存储的是每个数组的元素

        // Arrays.asList(T[]).stream()
        // 求数组总和
        int [] arr4 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int total = Arrays.stream(arr4).sum();
        // 数组遍历
        Arrays.asList(a1).stream().forEach(x -> System.out.println(x.getClass().getName()));  // [I
        Arrays.asList(a2).stream().forEach(x -> System.out.println(x.getClass().getName()));  // java.lang.Integer
        Arrays.asList(s1).stream().forEach(x -> System.out.println(x.getClass().getName()));  // java.lang.String
    }
}

七、Java字符串详解

1、String 概述

  • String 类在 java.lang 包下,所以使用的时候不需要导包。

  • String 类代表字符串,Java 程序中的所有字符串文字(例如"abc")都被实现为此类的实例。也就是说,Java 程序中所有的双引号字符串,都是 String 类的对象。

  • 字符串不可变,它们的值在创建后不能被更改。

  • String 这个类比较特殊,打印其对象名的时候,不会出现内存地址,而是该对象所记录的真实内容。

2、字符串的创建

String 类的创建有两种方式:一种是直接使用双引号赋值,另一种是使用 new 关键字创建对象。

1. 直接创建

String 字符串名 = "字符串内容";

2. 使用 new 关键字创建

String 类有多种重载的构造方法,具体如下:

  • String():创建一个空内容的字符串对象。
  • String(byte[] bytes):使用一个字节数组构建一个字符串对象。
  • String(byte[] bytes, int offset, int length):使用一个字节数组构建一个字符串对象,并指定开始的索引值与解码的个数。
    • offset:指定从数组中哪个索引值开始解码。
    • length:要解码多少个元素。
  • String(char[] value):使用一个字符数组构建一个字符串对象。
  • String(char[] value, int offset, int count):使用一个字符数组构建一个字符串对象,并指定开始的索引值与解码的个数。
  • String(int[] codePoints, int offset, int count):与字节数组一样用法。
  • String(String original):用字符串构建字符串对象。
public class StringConstructor {

    public static void main(String[] args) {
        // public String():创建一个空白字符串对象,不含有任何内容
        String s1 = new String();
        System.out.println(s1);  // ""

        // public String(char[] chs):根据字符数组的内容,来创建字符串对象
        char[] chs = {'a', 'b', 'c'};
        String s2 = new String(chs);
        System.out.println(s2);  // "abc"

        // public String(String original):根据传入的字符串内容,来创建字符串对象
        String s3 = new String("123");
        System.out.println(s3);  // "123"
    }
}

创建字符串对象的区别对比:

  • 通过构造方法创建:通过 new 创建的字符串对象,每一次 new 都会申请一个内存空间,且即时内容相同,地址值也不同。

  • 直接赋值方式创建:以 "" 方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM 都只会存在一个该 String 对象,并在字符串池中维护。

字符串的比较:

  • ==:比较基本数据类型时,比较的是值内容;比较引用数据类型时,比较的是对象地址。
  • String 类的 public boolean equals(String s):比较两个字符串的值内容是否相同。

public static void test(String str){
     // 使用技巧:str 有可能指向空(null)对象,放前面的话可能会出现空指针异常。
     // 因此常将常量放前面作为 equals 的调用者,杜绝空指针异常。
     if("中国".equals(str)){  
          System.out.println("回答正确");     
     }else{
          System.out.println("回答错误");
     }
}

3、String 常用方法

获取方法说明
int length()获取字符串的长度
char charAt(int index)返回指定索引处的 char 值
int indexOf(String str)查找子串第一次出现的索引值。若子串不存在,则返回 -1
indexOf(String str, int fromIndex)从指定的索引值开始查找子串
int lastIndexOf(String str)返回指定子字符串在此字符串中最右边出现处的索引,如果此字符串中没有这样的字符,则返回 -1
int lastIndexOf(String str, int fromIndex)返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索,如果此字符串中没有这样的字符,则返回 -1
String concat(String str)实现将一个字符串(参数 str)连接到另一个字符串后面
判断方法说明
boolean startsWith(String str)是否以指定字符开头
boolean endsWith(String str)是否以指定字符结束
boolean isEmpty()是否长度为 0
boolean contains(charSequences)是否包含子串 
(多态:charSequences 参数属于接口,String 是它的实现类)
boolean equals(Object anObject)判断两个字符串的内容是否一致
boolean equalsIgnoreCase(String anotherString)忽略大小写,判断两个字符串的内容是否一致
boolean matches(String regex)进行正则表达式的验证,若匹配则返回 true
操作方法说明
String replace(CharSequence target, CharSequence replacement)将字符串中的旧值替换成新值,得到新的字符串
String[] split(String regex)根据指定的字符进行切割
String substring(int beginIndex)从传入的索引处截取,截取到末尾,得到新的字符串
public String substring(int beginIndex, int endIndex)根据开始和结束索引进行截取,得到新的字符串(包头不包尾)
String toUpperCase()转大写
String toLowerCase()转小写
String trim()去掉字符串前后的空格字符
数组转换方法说明
Char[] toCharArray()将字符串转换为字符数组
Byte[] getBytes()将字符串转换为字节数组

示例:

    public static void main(String[] args) {
        // 字符串转数组
        byte[] a = "abc".getBytes();
        char[] b = "abc".toCharArray();
        System.out.println(a);  // [B@1b6d3586
        System.out.println(b);  // abc

        // 数组的字符串表示形式
        System.out.println(Arrays.toString(a));  // [97, 98, 99]
        System.out.println(Arrays.toString(b));  // [a, b, c]

        // 数组转字符串
        StringBuffer stringBuffer = new StringBuffer();
        for (char s : b) {
            stringBuffer.append(s + "");
        }
        System.out.println(stringBuffer.toString());
        
    }

4、字符串的类型转换

数据类型字符串转其他数据类型的方法其他数据类型转字符串的方法1其他数据类型转字符串的方法2
byteByte.parseByte(str)String.valueOf(byte bt)Byte.toString(byte bt)
intInteger.parseInt(str)String.valueOf(int i)Int.toString(int i)
longLong.parseLong(str)String.valueOf(long l)Long.toSting(long l)
doubledouble.parseDouble(str)String.valueOf(double d)Double.toString(double b)
floatFloat.parseFloat(str)String.valueOf(float f)Float.toString(float f)
charstr.charAt(int index)String.valueOf(char c)Character.toString(char c)
booleanBoolean.getBoolean(str)String.valueOf(boolean b)Boolean.toString(boolean b)
char[]str.toCharArray()String(char[] value)遍历字符元素进行字符串拼接
byte[]str.getBytes()String(byte[] value)

5、String 类不可变的好处

  1. 不可变对象可以提高 String Pool(字符串常量池)的效率。如果知道一个对象是不可变的,那么需要复制对象的内容时不用复制它本身而只是复制它的地址,复制地址(通常一个指针的大小)只需要很小的内存,效率也很好。同时,对其他引用同一个对象的变量也不会造成影响。

  2. 因为字符串时不可变的,所以是线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。

  3. 因为字符串是不可变的,所以在创建它的时候哈希码就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其他键对象。这就是 HashMap 中的键往往都使用字符串的原因。

6、格式化字符串的方法

String.format() 方法使用指定的格式字符串和参数,并返回一个格式化字符串。

1. 常规类型格式化:format(String format, Object…args)

  • format:格式字符串。
  • args:格式字符串中由格式说明符引用的参数。参数数目是可变的(可以为 0)。
转换符说 明示例
%b、%B格式化为布尔类型false
%h、%H格式化为哈希码A05A5198
%s、%S格式化为字符串类型"abc"
%c、%C格式化为字符类型'w'
%d格式化为十进制数26
%0格式化为八进制数12
%x、%X格式化为十六进制数4b1
%e格式化为用计算机科学计数法表示的十进制数1.700000e+01
%a格式化为带有效位数和指数的十六进制浮点值0X1.C000000000001P4

示例:

String s1 = String.format("%d", 400/2);  // 200
String s2 = String.format("%b", 3>5);  // false

2. 日期时间格式化:format(Locale, String format, Object…args)

  • Locale:格式化过程中要应用的语言环境。如果为空则不进行本地化。
  • format:格式字符串。
  • args:格式字符串中由格式说明符引用的参数。如果还有格式说明符以外的参数,则忽略这些额外的参数。参数的数目是可变的,可以为 0。
转换符说明示例
%te一个月中的某一天(1~31)12
%tb指定语言环境的月份简称Jan(英文)、一月(中文)
%tB指定语言环境的月份全称February(英文)、二月(中文)
%tA指定语言环境的星期几全称Monday(英文)、星期一(中文)
%ta指定语言环境的星期几简称Mon(英文)、星期一(中文)
%tc包括全部日期和时间信息星期三十月 25 13:37:22 CST 2008
%tY4 位年份2008
%tj一年中的第几天(001~366)060
%tm月份05
%td一个月中的第几天(01~31)07
%ty两位年份08

示例:

Date date = new Date();
Locale form = Locale.US;
String year = String.format(form, "%tY", date);
String month = String.format(form, "%tB", date);
String day = String.format(form, "%td", date);
System.out.println("当前年份:"+year);  // 当前年份:2021
System.out.println("当前月份:"+month);  // 当前月份:November
System.out.println("当前日份:"+day);  // 当前日份:06

7、StringBuffer 类

回顾字符串的特点:

  1. 字符串是常量,它们的值在创建之后不能更改。
  2. 字符串的内容一旦发生了变化,那么马上会创建一个新的对象。

因此,字符串的内容不适宜频繁修改,因为一旦修改马上就会创建一个新的对象。所以如果需要修改字符串的内容,建议使用字符串缓冲类——StringBuffer:一个存储字符的容器

StringBuffer 底层是依赖一个字符数组才能存储字符数据的,该字符数组默认的初始容量是 16。如果字符数组的长度不够用,自动增长一倍(再加 2)。

StringBuffer 常用方法:

添加方法说明
StringBuffer append(String/boolean/...)可以添加任意类型的数据到容器中
StringBuffer insert(int offset, boolean/...)指定索引值位置来插入数据
删除方法说明
StringBuffer delete(int start, int end)指定开始索引值与结束索引值来删除容器中的数据
StringBuffer deleteCharAt(int index)指定索引值删除一个字符
修改方法说明
StringBuffer replace(int start, int index, String str)指定开始索引值与结束索引值替换新的字符串内容
StringBuffer reverse()翻转内容
void setCharAt(int index, char ch)指定索引位替换字符
String substring(int start, int end)指定开始索引值与结束索引值截取内容
void ensureCapacity(int minimunCapacity)指定字符串缓冲类的字符数组长度(基本不用)
String toString()把字符串缓冲类的内容转换成字符串
查询方法说明
indexOf(String str, int fromIndex)查找子串第一次出现的索引值
lastIndexOf(String str)查找子串最后一次出现的索引值
capacity()查询当前(底层)字符数组的长度
length()查询当前字符串长度
charAt(int Index)根据索引值查询字符

8、StringBuilder 类

StringBuilder 也是一个可变的字符串类,我们可以把它看成是一个容器,这里的可变指的是StringBuilder 对象中的内容是可变的。

StringBuilder 常用方法与 StringBuffer 常用方法一致。

public class Test{
    public static void main(String[] args) {
        // 创建对象
        StringBuilder sb = new StringBuilder();

        // public StringBuilder append(任意类型):添加数据,并返回对象本身
//        StringBuilder sb2 = sb.append("hello");
//
//        System.out.println("sb:" + sb);
//        System.out.println("sb2:" + sb2);
//        System.out.println(sb == sb2);

//        sb.append("hello");
//        sb.append("world");
//        sb.append("java");
//        sb.append(100);

        // 链式编程
        sb.append("hello").append("world").append("java").append(100);
        System.out.println("sb:" + sb);  // helloworldjava100

        // public StringBuilder reverse():返回逆序的字符序列
        sb.reverse();
        System.out.println("sb:" + sb);  // 001avajdlrowolleh
		
        // StringBuilder 转换为 String
        StringBuilder sb2 = new StringBuilder();
        sb2.append("hello");
        String s2 = sb2.toString();
        System.out.println(s2);

        // String 转换为 StringBuilder
        String s3 = "hello";
        StringBuilder sb3 = new StringBuilder(s2);
        System.out.println(sb2);
    }
}

9、String、StringBuffer、StringBuilder 对比

  1. String 为字符串常量,而 StringBuffer、StringBuilder 均为字符串变量,即 String 一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。

  2. StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全,因此执行速度较高。

    • 一个 StringBuffer 对象在字符串缓冲区被多个线程使用时,其很多方法都带有 Synchronized 关键字,所以可以保证线程是安全的;而 StringBuilder 的 append() 方法中则没有 Synchronized 关键字,所以不能保证线程安全。
    • 因此如果要进行的操作是多线程的,那么建议使用 StringBuffer;单线程下则建议使用速度较快的 StringBuilder。

八、Java流程控制语句

1、if 条件语句

语法格式:

if (关系表达式1) {
    语句体1;
} else if (关系表达式2) {
    语句体2;
} else {
    语句体n+1;
}

示例:小明快要期末考试了,小明爸爸对他说,会根据他不同的考试成绩,送他不同的礼物,假如你可以控制小明的得分,请用程序实现小明到底该获得什么样的礼物,并在控制台输出。

分析:

  1. 小明的考试成绩未知,可以使用键盘录入的方式获取值。
  2. 由于奖励种类较多,属于多种判断,因此采用 if...else 分支语句实现。
  3. 为每种判断设置对应的条件。
  4. 为每种判断设置对应的奖励。
import java.util.Scanner;

public class Test{
    public static void main(String[] args){
        // 录入考试成绩
        Scanner sc = new Scanner(System.In);
        System.out.println("请输入您的成绩:");
        int score = sc.nextInt();
        // 判断输入的成绩是否合法
        if (score >= 0 && score <= 100) {
            if (score >= 90) {
                System.out.println("奖励:自行车一辆");
            } else if (score >= 70) {
                System.out.println("奖励:游乐场一回");
            } else {
                System.out.println("继续努力吧");
            }
        }else{
            System.out.println("输入的成绩不合法!");
        }
    }
}

2、switch 分支语句

语法格式:

switch (表达式) {
	case 1:
		语句体1;
		break;
	case 2:
		语句体2;
		break;
	...
	default:
		语句体n+1;
		break;
}

执行流程:

  1. 首先计算出表达式的值。
  2. 和 case 依次比较,一旦有对应的值,就会执行相应区域的语句,在执行的过程中,遇到 break 就会结束。
  3. 如果所有的 case 都和表达式的值不匹配,就会执行 default 语句体部分,然后程序结束。

示例 1:键盘录入星期数,显示今天的减肥活动。

周一:跑步  
周二:游泳  
周三:慢走  
周四:动感单车
周五:拳击  
周六:爬山  
周日:好好吃一顿 

示例代码:

import java.util.Scanner;

public class Day{
    public static void main(String[] args){
        // 录入星期数据
        Scanner sc = new Scanner(System.in);
        System.out.println("今天星期几:");
        int weekDay = sc.nextInt();
        // 判断 weekDay
        switch(weekDay){
            // 在不同的 case 中,输出对应的减肥计划
            case 1:
                System.out.println("跑步");
                break;
            case 2:
                System.out.println("游泳");
                break;
            case 3:
                System.out.println("慢走");
                break;
            case 4:
                System.out.println("动感单车");
                break;
            case 5:
                System.out.println("拳击");
                break;
            case 6:
                System.out.println("爬山");
                break;
            case 7:
                System.out.println("好好吃一顿");
                break;
            default:
                System.out.println("您的输入有误");
                break;
        }
    }
}

示例 2:case 穿透。

根据键盘录入的星期数,对应输出工作日或休息日。

/*
case 穿透是如何产生的:
	如果 switch 语句中,case 省略了 break 语句,就会开始 case 穿透。

现象:
	当开始 case 穿透,后续的 case 就不会具有匹配效果,其内部语句都会执行,
	直到看见 break,或者将整体 switch 语句执行完毕,才会结束。
*/
import java.util.Scanner;

public class Test{
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入星期数:");
        int week = sc.nextInt();
        switch(week){
            case 1:
            case 2:
            case 3:
            case 4:
            case 5:
                System.out.println("工作日");
                break;
            case 6:
            case 7:
                System.out.println("休息日");
                break;
            default:
                System.out.println("您的输入有误");
                break;
        }
    }
}

3、for 循环

语法格式:

for (初始语句; 循环体条件表达式; 循环体执行后语句) {
    循环体语句;
}
  • 循环体条件表达式可以使用 for 语句外的变量。
  • 循环体执行后的语句可以有多个表达式,用逗号分开。

示例:多条件表达式。

int divided = 100;
int divisor = 3;

int found = 0;

for (int i=0; i<100 & found<10; i++, found++) {
    divided++;
}

示例:每行打印两个水仙花数。

水仙花数:指的是一个三位数,其个位、十位、百位上的数字的立方和等于原数。如 153 = 3*3*3 + 5*5*5 + 1*1*1。

public class Test {
    public static void main(String[] args){
		
        int times = 0;
		
        for(int i=100; i<=1000; i++){
            // 求出各位置上的数值
            int ones = i % 10;
            int tens = i / 10 % 10;
            int hundreds = i / 100 % 10;
            // 计算立方和后与原始数字比较是否相等
            if(ones*ones*ones + tens*tens*tens + hundreds*hundreds*hundreds == i){
                // 打印元素但不换行
                System.out.print(i+" ");
                times++;
                // 如果是偶数,则进行换行
                if(times % 2 == 0){
                    System.out.println();
                }
            }
        }
    }
}

4、while 循环

语法:

初始化语句;
while (条件判断语句) {
	循环体语句;
}

示例:世界最高山峰是珠穆朗玛峰(8844.43 米 = 8844430 毫米),假如有一张足够大的纸,它的厚度是 0.1 毫米。请问,折叠多少次,可以折成珠穆朗玛峰的高度?

public class Test {
    public static void main(String[] args){
        // 海拔
        int top = 8844430;
        // 现纸张厚度
        double current = 0.1;
        // 折叠次数
        int count = 0;
        while(current < top){
            current *= 2;
            count += 1;
        }
        System.out.println("折叠次数:"+count);
    }
}

5、do while 循环

语法格式:

初始化语句;
do {
    循环体语句;
}while(条件判断语句);

示例:在控制台输出 5 次 "HelloWorld" 。

public class Test {
    public static void main(String[] args){
        // for 循环 实现
        for(int i=1; i<=5; i++){
            System.out.println("Hello World");
        }
        System.out.println("---------------");
        // do while 循环 实现
        int i = 0;
        do{
            System.out.println("Hello world");
            i++;
        }while(i <= 5);
    }
}

6、三种循环的区别

三种循环的区别:

  • for 循环和 while 循环:先判断条件是否成立,然后决定是否执行循环体(先判断后执行)。
  • do...while 循环:先执行一次循环体,然后判断条件是否成立,是否继续执行循环体(先执行后判断)。

for 循环和 while 的区别:

  • 条件控制语句所控制的自增变量,因为归属 for 循环的语法结构中,在 for 循环结束后,就不能再次被访问到了。
  • 条件控制语句所控制的自增变量,对于 while 循环来说不归属其语法结构中,在 while 循环结束后,该变量还可以继续使用。

7、死循环

// for 死循环:
for(;;){

}

// while 死循环:
while(true){

}

// do..while 死循环:
do{

}while(true);

8、break、continue

  • break:跳出循环,结束循环。(break 语句只能在循环和 switch 中进行使用)
  • continue:跳过本次循环,继续下次循环。(continue 只能在循环中进行使用)

示例:程序运行后,用户可多次查询星期几所对应的减肥计划,直到输入 0,程序结束。

import java.util.Scanner;

public class Test {
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        // 给 while 循环起别名
        loop:while(true){
            System.out.println("请输入星期几:");
            int weekDay = sc.nextInt();
            if(weekDay==0){
                System.out.println("输入为0,结束程序");
            }
            switch (weekDay){
                case 0:
                    System.out.println("感谢您的使用");
                    break loop;  // 结束整个 while 循环(若没有别名,则只是 break case 0,并继续死循环)
                case 1:
                    System.out.println("跑步");
                    break;
                case 2:
                    System.out.println("游泳");
                    break;
                case 3:
                    System.out.println("慢走");
                    break;
                case 4:
                    System.out.println("动感单车");
                    break;
                case 5:
                    System.out.println("拳击");
                    break;
                case 6:
                    System.out.println("爬山");
                    break;
                case 7:
                    System.out.println("好好吃一顿");
                    break;
                default:
                    System.out.println("您的输入有误");
                    break;
            }
        }
    }
}

九、Java包与权限修饰符

1、Package包

1. 包的作用

  1. 解决类名重复产生冲突的问题(后编译的类名会把前面的类名覆盖掉)。
  2. 便于软件版本的发布。

2. 包的定义格式

  • 都是小写字母。
  • 多级包之间使用"."进行分割。
  • 多级包的定义规范:公司的网站地址翻转(去掉 www)。
  • 比如百度的网站址为 www.baidu.com,那么所定义的包的结构就是:com.baidu.自定义包名。

注意事项:

  • package 语句必须是程序的第一条可执行的代码。
  • package 语句在一个 java 文件中只能有一个。
  • 如果没有 package,默认表示无包名。
  • 如果一个类加上了包语句,那么该类的完整类名就是:包名.类名

3. 生成包文件夹的编译

javac -d <class文件存放路径> <java源文件名>

4. 导包

作用:简化书写(误区:把一个类导入到内存中)。

格式:

import 包名.类名;  // 推荐使用
import 包名.*;  // 会导致结构不清晰

注意事项:

  1. 一个 java 文件中可以出现多句导包语句。
  2. *号通配符可以匹配任何的类。
  3. java.lang 包(包括了 String、System 等类)是默认导入了 java 文件中的。

2、类与类之间的访问

  • 同一个包下的访问:不需要导包,直接使用即可。

  • 不同包下的访问:

    1. import 导包后访问
    2. 通过全类名(包名+类名)访问
  • 注意:import、package、class 三个关键字的摆放位置存在顺序关系:

    • package 必须是程序的第一条可执行的代码。
    • import 需要写在 package 下面。
    • class 需要在 import 下面。

3、权限修饰符

  • protected:只可以被子类访问,不管子类是不是和父类在同一个包中,即子类限制修饰符。
  • default:只可被同一个包中的其他类访问,而不管其他类是不是子类,即包限制修饰符。

4、jar 包

打 jar 包:使用 JDK 的 jar.exe。

jar 包的作用:

  1. 方便用户快速运行一个项目。
  2. 提供工具类给别人使用。

格式:jar cvf <文件名.jar> <class文件或者文件夹>

注意事项:

  1. 一个程序打完 jar 包后必须要在清单文件上指定入口类,格式:Main-Class: 包名.类名
  2. jar 包双击运行仅对于图形化界面的程序起作用,对控制台的程序不起作用。
  3. 如果要使用 jar 包里面的类,必须先设置 classpath 路径。

十、Java方法

1、方法的定义

方法(method)是指将具有独立功能的代码块组织成为一个整体(代码集),使其具有特定功能。

  • 方法必须先创建才可以使用,创建的过程称为方法定义。
  • 方法创建后需要被手动使用后才会执行,使用的过程称为方法调用。

每个方法在被调用时,都会进入栈内存,并且拥有自己独立的内存空间。直到方法内部代码执行完毕后,则会从栈内存中弹栈消失。

2、方法的调用

1. 无参数的方法定义和调用

// 方法定义
public static void 方法名(){
    // 方法体;
}

// 方法调用
方法名();

2. 带参数的方法定义和调用

// 方法定义
public static void isEvenNumber(int number){  // 参数由数据类型和变量名组成
    ...
}
public static void getMax(int num1, int num2){
    ...
}

// 方法调用
isEvenNumber(10);
getMax(10, 20);
  • 形参:方法定义中的参数,等同于变量定义格式。
  • 实参:方法调用中的参数,等同于使用变量或常量。

示例:输入一个参数,判断是奇数还是偶数。

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入一个整数:");
        int num = sc.nextInt();
        isEvenNumber(num);
    }

    public static void isEvenNumber(int num){
        if(num % 2 == 0){
            System.out.println("偶数");
        }else{
            System.out.println("奇数");
        }
    }
}

示例:打印 n-m 之间所有的奇数。

import java.util.Scanner;

public class Test {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入开始的整数:");
        int startNum = sc.nextInt();
        System.out.println("请输入结束的整数:");
        int endNum = sc.nextInt();
        isOddNumber(startNum, endNum);
    }

    public static void isOddNumber(int n, int m){
        for(int i=n; i<=m; i++){
            if(i % 2 != 0){
                System.out.println(i);
            }
        }
    }
}

3. 带参数和返回值的方法定义和调用

定义:

public static 返回值的类型 方法名(参数){ 
    return 返回值;
}

// 示例 1
public static boolean isEvenNumber(int number){
    return true;
}

// 示例 2
public static int getMax(int a, int b){
    return  100;
}
  • 注意:return 的返回值类型要与方法定义上的数据类型相匹配,否则程序会报错。

调用:

// 格式
数据类型 接受返回值的变量名 = 方法名(参数);

// 示例
boolean flag = isEvenNumber(5);
  • 注意:方法的返回值通常会使用变量接收,否则该返回值将无意义。

3、方法的通用格式

public static 返回值类型 方法名(参数){
   方法体;
   return 返回值;
}

解释:

  • public static:修饰符
  • 返回值类型:方法操作完毕之后返回的数据的数据类型。如果方法操作完毕,没有数据返回,这里写 void,且方法体中一般不写 return。
  • 方法名:调用方法时候使用的标识。
  • 参数:由数据类型和变量名组成,多个参数之间用逗号隔开。
  • 方法体:完成功能的代码块。
  • return:如果方法操作完毕,有数据返回,则用于把数据返回给调用者。

注意:

  • 方法不能嵌套定义。
  • void 表示无返回值,可以省略 return;也可以单独书写 return,但后面不能跟数据或代码。
  • 只能 return 一个返回值。

4、方法的重载

当一个类中有几个相同的方法名时,Java 如何知道你调用的是哪个方法呢?这里记住一点即可,每个重载的方法都有独一无二的参数列表。其中包括参数的类型、顺序、参数数量等。请记住以下重载的条件:

  • 方法名称必须相同。
  • 参数列表必须不同(个数不同、或类型不同、或参数类型排列顺序不同)。
  • 方法的返回类型可以相同也可以不相同。
  • 仅仅返回类型不同不足以成为方法的重载。
  • 重载是发生在编译时的,因为编译器可以根据参数的类型来选择使用哪个方法。

示例:

// 正确示例 1
public class MethodDemo {
    public static void fn(int a){
        //方法体
    }
    public static int fn(double a){
        //方法体
    }
}

// 正确示例 2
public class MethodDemo {
    public static float fn(int a){
        //方法体
    }
    public static int fn(int a, int b){
        //方法体
    }
}

// 错误示例 1:重载与返回值无关
public class MethodDemo {
    public static void fn(int a) {
        //方法体
    }
    public static int fn(int a) {
        //方法体
    }
}

// 错误示例 2:两个类的 fn 方法
public class MethodDemo01 {
    public static void fn(int a){
        //方法体
    }
} 
public class MethodDemo02 {
    public static int fn(double a){ 
        //方法体
    }
}

5、方法的参数传递

示例 1:传递基本数据类型。

  • 结论:对于基本数据类型的传参,在方法内部的改变,不会影响实际参数本身。
  • 依据:每个方法在栈内存中,都会有独立的栈空间,方法运行结束后就会弹栈消失。
public class Test {
    
    public static void main(String[] args) {
        // 方法参数传递为基本数据类型:传入方法中的, 是具体的数值。
        int number = 100;
        System.out.println("调用change方法前:" + number);  // 100
        change(number);
        System.out.println("调用change方法后:" + number);  // 100
    }
    
    public static void change(int number) {
        number = 200;
    }
    
}

示例 2:传递引用数据类型。

  • 结论:对于引用类型的参数,在方法内部的改变,会影响实际参数本身。
  • 结论依据:引用数据类型的传参,传入的是地址值,会造成两个引用同时指向同一片内存的效果。所以即使方法弹栈,堆内存中的数据也已经是改变后的结果。
public class Test {

    public static void main(String[] args) {
        // 方法参数传递为基本数据类型:传入方法中的, 是具体的数值。
        int[] arr = {100, 200, 300};
        System.out.println("调用change方法前:" + arr[1]);  // 200
        change(arr);
        System.out.println("调用change方法后:" + arr[1]);  // 400
    }

    public static void change(int[] arr) {
        arr[1] = 400;
    }

}

6、可变参数

可变参数介绍:

  • 可变参数又称参数个数可变,当用作方法的形参出现时,那么该方法的参数个数就是可变的了。
  • 当方法的参数类型已经确定,但个数不确定时,我们就可以使用可变参数。

可变参数的定义格式:

修饰符 返回值类型 方法名(数据类型… 变量名) {
    // 方法体
}

可变参数的注意事项:

  • 这里的变量其实是一个数组。
  • 如果一个方法有多个参数并包含可变参数,则可变参数要放在最后。

代码示例:

public class ArgsDemo {
    public static void main(String[] args) {
        System.out.println(sum(10, 20));
        System.out.println(sum(10, 20, 30));
        System.out.println(sum(10, 20, 30, 40));
    }

//    public static int sum(int b, int... a) {
//        return 0;
//    }

    public static int sum(int... a) {
        int sum = 0;
        for(int i : a) {  // 增强for循环
            sum += i;
        }
        return sum;
    }
}

十一、Java面向对象编程

1、类和对象

面向对象和面向过程都是解决问题的一种思路。

  • 面向过程

    • 是一种以过程为中心的编程思想,实现功能的每一步都是自己实现的。面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,尽量忽略面向对象的复杂语法,即面向过程是“强调做什么,而不是以什么形式去做”。
    • 开发过程的思路是将数据与方法按照执行的逻辑顺序组织在一起,数据与方法分开考虑,也就是拿数据做操作。
  • 面向对象

    • 是一种以对象为中心的编程思想,通过指挥对象实现具体的功能,强调“必须通过对象的形式来做事情”。
    • 将数据(成员变量)与功能(成员方法)绑定到一起,进行封装,以增强代码的模块化和重用性,这样能够减少重复代码的编写过程,提高开发效率。

面向对象编程的两个重要概念:类和对象

    • 类是具有相同属性和行为的事物的统称(或统称为抽象)。
    • 类是抽象的,在使用的时候通常会找到这个类的一个具体的存在来使用。
    • 一个类可以找到多个对象。
  • 对象

    • 某一个具体事物的存在,在现实世界中可以是看得见摸得着的。
    • 可以直接使用。
    • 类和对象之间的关系:类就是创建对象的模板。

类和对象之间的关系:类就是创建对象的模板。

1. 类的定义

类由属性和行为两部分组成:

  • 属性:在类中通过成员变量来体现(类中方法外的变量)。
  • 行为:在类中通过成员方法来体现(去掉 static 关键字即可)。

类的定义步骤:

  1. 定义类;
  2. 编写类的成员变量;
  3. 编写类的成员方法。
public class Student {

    // 成员变量(属性)
    String name;
    int age;

    // 成员方法(行为)
    public void study(){  // 注意:没有 static 关键字
        System.out.println("学习");
    }
}

2. 对象的创建和使用

创建对象的格式:

  • 类名 对象名 = new 类名();

调用成员的格式:

  • 对象名.成员变量
  • 对象名.成员方法();
public class TestStudent {

    public static void main(String[] args) {
        // 类名 对象名 = new 类名();
        Student stu = new Student();
        // 对象名.变量名
        // 初始化的默认值
        System.out.println(stu.name);  // null
        System.out.println(stu.age);   // 0

        stu.name = "张三";
        stu.age = 23;

        System.out.println(stu.name);  // 张三
        System.out.println(stu.age);   // 23

        // 调用:对象名.方法名();
        stu.study();
        System.out.println(stu);  // 打印全类名,即:包名+类名
    }
}

2、对象内存

Java 虚拟机要运行程序,必须要对内存进行空间的分配和管理。

Java 中的内存分配:

单个对象的内存: 

多个对象的内存: 

总结:多个对象在堆内存中,都有不同的内存划分,成员变量存储在各自的内存区域中,而成员方法则多个对象共用一份。

多个对象指向相同的内存:

总结:当多个对象的引用指向同一个内存空间,变量所记录的地址值是一样的。此时只要有任何一个对象修改了内存中的数据,随后无论使用哪一个对象进行数据获取,都是修改后的数据。 

查看对象内存地址

引入 maven 依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

代码:

import org.openjdk.jol.vm.VM;

VM.current().addressOf(Object o);  // 获取内存地址

3、成员变量和局部变量

类中位置不同:

  • 成员变量:位于方法外部。
  • 局部变量:位于方法内部。

内存中位置不同:

  • 成员变量:位于堆内存。
  • 局部变量:位于栈内存。

生命周期不同:

  • 成员变量:随着对象的存在而存在,随着对象的消失而消失。
  • 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失。

初始化值不同:

  • 成员变量:有默认初始值。
  • 局部变量:没有默认初始化值,必须先定义和赋值才能使用。

4、封装

1. 封装思想

封装的概述:

  • 封装是面向对象三大特征之一(封装、继承、多态)。
  • 封装是指将对象的属性和行为进行包装,不需要让外界知道具体的实现细节。

封装的原则:

  • 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问。
  • 实现方式:将成员变量修饰为 private,并提供对应的 getXxx()/setXxx() 方法。

封装的好处:

  • 通过方法来控制成员变量的操作,提高了代码的安全性。
  • 用方法将代码进行封装,提高了代码的复用性。

2. private 关键字

概述:private 是一个权限修饰符,可以用来修饰成员(成员变量、成员方法)。

特点 : 被 private 修饰的成员,只能在本类进行访问。而针对 private 修饰的成员变量,如果需要被其他类使用,则可以提供相应的方法:

  • 提供get变量名()方法,用于获取成员变量的值,方法用 public 修饰。

  • 提供set变量名(参数)方法,用于设置成员变量的值,方法用 public 修饰。

示例:

public class Student {

    private String name;
    private int age;

    // 提供修改name属性的封装方法
    public void setName(String a){
        name = a;
    }

    public void getName(){
        System.out.println("姓名:"+name);
    }

    // 提供修改age属性的封装方法
    public void setAge(int a){
        if(age<0 || age>120) {
            System.out.println("年龄【"+age+"】设置有误");
        } else {
            age = a;
        }
    }

    public void getAge(){
        System.out.println("年龄:"+age);
    }

    public void personalFile(){
        System.out.println("姓名:"+name+","+"年龄:"+age);
    }
}


// 学生测试类
class StudentTest{
    public static void main(String[] args) {
        Student student = new Student();
        // student.age = 12;  此行会报错
        student.setAge(12);
        student.getAge();  // 12
        student.setName("小明");
        student.personalFile();  // 姓名:小明,年龄:12
    }
}

3. this 关键字

this 关键字代表了当前调用方法的引用,即哪个对象调用的方法,this 就代表哪一个对象

Java 的 this 只能用于方法体中。当一个对象创建后,Java 虚拟机就会给这个对象分配一个引用自身的指针,这个指针的名字就是 this。因此,this 只能在类种的非静态方法中使用,静态方法和静态代码块中不能出现 this,并且 this 只能和特定的对象关联,而不和类关联,即同一个类的不同对象有不同的 this。

作用:

  1. 如果存在成员变量与局部变量同名时,在方法内部默认访问的是局部变量,那么可以通过 this 关键字访问成员变量。

    • 方法的形参如果与成员变量同名,不带 this 修饰的变量指的是形参,而不是成员变量。
    • 方法的形参没有与成员变量同名,不带 this 修饰的变量指的是成员变量(编译器会自动在该变量的前面添加 this 关键字)。
  2. 用于在构造方法中引用满足指定参数类型的构造方法,只能引用一个构造方法且必须位于开始的位置。

注意:

  1. this 关键字调用其他构造函数时,this 关键字必须要是构造函数中的第一句语句。
  2. this 关键字在构造函数中不能出现相互调用的情况,因为是个死循环。

示例:

// 经典用法
class Student{

    int id;
    String name;

    public Student(int id, String name){
        this.id = id;  // 局部变量的id给成员变量的id赋值
        this name = name;
    }

     public Student(String name){
        this.name = name;
     }
}
// 以上示例的两个 this.name = name 可简便处理(尽量优化重复代码),即作用 2
class Student{

    int id;
    String name;

    public Student(int id, String name){
        this(name);  // 调用其他构造方法,且必须放在第一句
        // this();  // 无参的构造函数被调用
        this.id = id;  // 局部变量的id给成员变量的id赋值
        System.out.println("两个参数的构造函数被调用了");
    }

     public Student(String name){
          this.name = name;  // 已在双参的构造函数中调用
          System.out.println("一个参数的构造函数被调用了");
     }

     public Student(){
          System.out.println("无参的构造函数被调用了");   // 已在双参的构造函数中调用
     }
}

this 内存原理:

5、构造方法

格式:

  • 方法名必须与类名一致。
  • 没有返回值类型,不能使用 void 进行修饰。
  • 没有返回值(不能有 retrun 带回结果数据)。

执行时机:

  • 创建对象的时候调用,每创建一次对象,就会执行一次构造方法。
  • 不能手动调用构造方法。

构造方法的创建:

  • 如果没有定义构造方法,系统将给出一个默认的无参数构造方法(Java 编译器添加的无参构造函数的权限修饰符与类的权限修饰符一致);如果定义了构造方法,系统将不再提供默认的构造方法。建议:无论是否使用,都手动书写无参数构造方法,和带参数构造方法。
  • 构造方法不会被继承。
  • 在构造方法中才能调用重载的构造方法,语法为this.(实参列表),且必须为第一行,后面可以继续有代码。
  • 在调用重载的构造方法时,不可以使用成员变量。因为从语义上讲,这个对象还没被初始化完成。

示例:标准类的代码编写和使用。

package com.demo;

// 学生类: 封装数据
public class Student {
    
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    public void show(){
        System.out.println(name + "..." + age);
    }
}


// 学生测试类
class TestStudent {
    
    public static void main(String[] args) {
        // 1. 无参数构造方法创建对象,通过 setXxx 方法给成员变量进行赋值
        Student stu1 = new Student();
        stu1.setName("张三");
        stu1.setAge(23);
        stu1.show();

        // 2. 通过带参数构造方法, 直接给成员变量赋值
        Student stu2 = new Student("李四", 24);
        stu2.show();
    }
}

6、static关键字

static 是静态的意思,可以修饰成员方法或成员变量。

static 修饰的特点:

  • 被类的所有对象共享(是我们判断是否使用静态关键字的条件)。
  • 随着类的加载而加载,优先于对象存在(对象需要类被加载后,才能创建)。
    • 静态方法与非静态方法的字节码文件是同时存在内存中的;
    • 只是静态的成员变量是优先于对象存在的。
  • 静态成员变量只会在数据共享区中维护一份,而非静态成员变量的数据会在每个对象中都维护一份。
  • 可以通过类名调用(推荐:方便且省内存),也可以通过对象名调用。
  • static 什么时候修饰一个方法:如果一个方法没有直接访问到非静态的成员时,那么就可以使用 static 修饰。一般用于工具类型的方法。

示例:

class Student{
    String name;
    static String country = "中国";

    public Student(String name){
        this.name = name;
    }
}

public class Test{
    public static void main(String[] args) {
        Student s1 = new Student("张三");
        Student s2 = new Student("李四");
        s1.country = "小日本";  // 修改了数据共享区的country值,并且数据只有一份
        System.out.println("姓名:"+s1.name+"国籍:"+s1.country);  // 因此此时country是"小日本"
        System.out.println("姓名:"+s2.name+"国籍:"+s2.country);  // 因此此时country是"小日本"
    }
}

static 注意事项:

  1. 静态方法可以调用类名或者对象进行调用;而非静态方法只能通过对象进行调用。
  2. 静态方法可以直接访问静态的成员,不能直接访问非静态的成员(变量与方法);但可以在静态方法里创建对象来访问非静态的数据。
  3. 非静态方法可以直接访问静态与非静态的成员。
  4. 静态方法不能出现 this 或者 super 关键字(this 与 super 都是指引用的对象空间,即非静态变量)。

使用案例:统计一个类被使用了多少次创建对象,该类对外显示被创建的次数。

class Caculate{

    static int count = 0;

    {
        count++;  // 无论调用哪个构造函数,都会先执行构造代码块
    }

    public Caculate(String name){
        String name = name;
    }

    public Caculate(){

    }

    public void showCount(){
        System.out.println("创建了"+count+"次对象");
    }
}

public class Test{
    public static void main(String[] args) {
        Caculate a = new Caculate();
        Caculate b = new Caculate();
        Caculate c = new Caculate();
        c.showCount();
    }
}

7、代码块

在 Java 中,使用 { } 括起来的代码被称为代码块。

1. 局部代码块

  • 位置: 方法中定义。
  • 作用: 限定变量的生命周期,及早释放,提高内存利用率。

示例代码:

public class Test {

    public static void main(String[] args) {
        {
            int a = 10;
            System.out.println(a);
        }
       // System.out.println(a);  找不到变量a
    }
}

2. 构造代码块

  • 位置: 类中方法外定义。
  • 特点: 每次构造方法执行时,都会执行该代码块中的代码,并且在构造方法执行前执行。
  • 作用: 将多个构造方法中相同的代码,抽取到构造代码块中,提高代码的复用性。

示例代码:

public class Test {

    public static void main(String[] args) {
        Student stu1 = new Student();  // 先打印“好好学习”
        Student stu2 = new Student(10);  // 先打印“好好学习”
    }
}

class Student {

    {
        System.out.println("好好学习");
    }

    public Student(){
        System.out.println("无参构造方法");
    }

    public Student(int a){
        System.out.println("带参构造方法");
    }

}

3. 静态代码块

  • 位置: 类中方法外定义。
  • 特点: 需要通过 static 关键字修饰,随着类的加载而加载,并且只执行一次。
  • 作用: 在类加载的时候做一些数据初始化的操作。
public class Test {

    public static void main(String[] args) {
        Student stu1 = new Student();  // 会打印“好好学习”
        Student stu2 = new Student(10);  // 不会打印“好好学习”
    }
}

class Student {

    static {
        System.out.println("好好学习");
    }

    public Student(){
        System.out.println("无参构造方法");
    }

    public Student(int a){
        System.out.println("带参构造方法");
    }
}

8、初始化

1. 成员初始化

Java 会尽量保证每个成员变量在使用前都会获得初始化。

  • 默认值的初始化:
类型初始值
booleanfalse
char/u0000
byte(byte)0
short(short)0
int0
long0L
float0.0f
double0.0d
引用类型null
  • 指定值的初始化:
int a = 11

也就是说,指定 a 的初始化值不是 0,而是 11。其他基本类型和对象类型也是一样的。

2. 初始化顺序

以下代码的执行结果反映了各种属性/方法的初始化顺序:

  1. 静态属性
  2. 静态代码块
  3. 普通属性
  4. 普通代码块
  5. 构造函数
public class LifeCycle {

    // 静态属性
    private static String staticField = getStaticField();

    // 静态代码块
    static {
        System.out.println(staticField);
        System.out.println("静态代码块初始化");
    }

    // 普通属性
    private String field = getField();

    // 普通代码块
    {
        System.out.println(field);
    }

    // 构造方法
    public LifeCycle() {
        System.out.println("构造方法初始化");
    }

    public static String getStaticField() {
        String statiFiled = "Static Field Initial";
        return statiFiled;
    }

    public static String getField() {
        String filed = "Field Initial";
        return filed;
    }

    // 主函数
    public static void main(String[] argc) {
        new LifeCycle();
    }

}

9、继承

1. 继承简介

继承是面向对象的三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义以及追加属性和方法。

实现继承的格式:继承通过 extends 实现

  • 格式:class 子类 extends 父类 { }
  • 举例:class Dog extends Animal { }

继承的好处:继承可以让类与类之间产生关系——父子关系。产生子父类后,子类则可以使用父类中非私有的成员。

示例:

// 父类
public class Father {
    public void show() {
        System.out.println("show方法被调用");
    }
}

// 子类
public class Son extends Father {
    public void method() {
        System.out.println("method方法被调用");
    }
}
public class Test {
    public static void main(String[] args) {
        //创建对象,调用方法
        Father f = new Father();
        f.show();

        Son s = new Son();
        s.method();
        s.show();
    }
}

继承的好处和弊端:

继承的好处:

  • 提高了代码的复用性(多个类相同的成员可以放到同一个父类中)。
  • 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)。

继承的弊端:

  • 继承让类与类之间产生了关系,类的耦合性增强了。当父类发生变化时,子类的实现也不得不跟着变化,因此削弱了子类的独立性。

继承的应用场景:

  • 使用继承,需要考虑类与类之间是否存在 is..a 的关系,不能盲目使用继承。
  • is..a 的关系,即表示谁是谁的一种。例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类。

2. Java 中继承的特点

  1. 只支持单继承,不支持多继承
    • 错误范例:class A extends B, C { }
  2. 类支持多层继承。

多层继承示例:

public class GrandFather {

    public void drink() {
        System.out.println("爷爷爱喝酒");
    }

}

public class Father extends GrandFather {

    public void smoke() {
        System.out.println("爸爸爱抽烟");
    }

}

public class Son extends Father {
	// 此时,Son类中就同时拥有drink方法以及smoke方法
}

3. 继承中的成员访问特点

在子类方法中访问一个变量,采用的是就近原则。

  1. 子类局部范围找;
  2. 子类成员范围找;
  3. 父类成员范围找;
  4. 如果都没有就报错(不考虑多层父类)。

示例代码:

class Father {
    int num = 10;
}

class Son extends Father {
    int num = 20;
    public void show(){
        int num = 30;
        System.out.println(num);
    }
}

public class Test {
    public static void main(String[] args) {
        Son s = new Son();
        s.show();  // 输出show方法中的局部变量:30
    }
}

4. 继承中的成员方法访问特点

通过子类对象访问一个方法,也是采用就近原则:

  1. 子类成员范围找;
  2. 父类成员范围找;
  3. 如果都没有就报错(不考虑多级父类)。

5. super 关键字

this 和 super 关键字:

  • this:代表本类对象的引用。
  • super:代表父类对象的引用。

作用:

  1. 子父类存在同名的成员时,在子类中默认是访问子类的成员,可以通过 super 关键字指定访问父类的成员。
  2. 创建子类对象时,默认会先调用父类无参的构造函数,可以通过 super 关键字调用指定的父类构造方法。

super 调用父类构造方法时的注意事项:

  1. 如果在子类的构造方法上没有指定调用父类的构造方法,那么 Java 编译器会在子类的构造方法上自动添加 super()。
  2. 必须是子类构造函数中的第一个语句。
  3. super 与 this 不能同时出现在同一个构造函数中调用其他的构造函数,因为两个语句都必须位于第一句。

super 与 this 的区别:

  1. 代表的事物不一致:
    • super 关键字代表的是父类空间的引用。
    • this 关键字代表的是本类对象的引用。
  2. 使用前提不一致:
    • super 必须要有继承关系才能使用。
    • this 不需要存在继承关系也可使用。
  3. 调用成员的区别:
    • super 是调用父类的成员。
    • this 是调用本类的成员。

6. 继承中的构造方法访问特点

注意:子类中所有的构造方法默认都会访问父类中的无参构造方法。

  • 子类会继承父类中的数据,可能还会使用父类的数据。所以,子类在初始化之前,一定要先完成父类数据的初始化,原因在于,每一个子类构造方法的第一条语句默认都是:super()。

问题:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?

  • 方法一:通过使用 super(...) 关键字显式地调用父类的带参构造方法。
  • 方法二:子类通过 this(...) 调用本类的其他构造方法,本类其他构造方法再通过 super(...) 手动调用父类的带参构造方法。

注意:this(…) 或 super(…) 必须放在构造方法的第一行有效语句,并且二者不能共存。

7. super 内存图

对象在堆内存中,会单独存在一块 super 区域,用来存放父类的数据。

8. 方法重写

方法重写的概念:子类出现了和父类中一模一样的方法声明(方法名和参数列表均一样)。

方法重写的应用场景:当子类即需要父类的功能,又需要有自己特有的功能时,可以重写父类中的方法,这样即沿袭了父类的功能,又定义了子类特有的功能。

Override 注解:用来检测当前的方法是否是重写的方法,起到【校验】的作用。

方法重写的注意事项:

  1. 私有方法不能被重写(父类私有成员子类是不能继承的)。
  2. 子类方法的访问权限不能更低(public > 默认 > 私有)。
  3. 静态方法不能被重写。如果子类也有相同的静态方法,则并不是重写的父类的方法。
  4. 构造方法不能被重写。
  5. 子类访问权限修饰符不能严于父类。

示例:

public class Father {
    private void show() {
        System.out.println("Father中show()方法被调用");
    }

    void method() {
        System.out.println("Father中method()方法被调用");
    }
}

public class Son extends Father {

    /* 编译【出错】,子类不能重写父类私有的方法*/
    @Override
    private void show() {
        System.out.println("Son中show()方法被调用");
    }
   
    /* 编译【出错】,子类重写父类方法的时候,访问权限需要大于等于父类 */
    @Override
    private void method() {
        System.out.println("Son中method()方法被调用");
    }

    /* 编译【通过】,子类重写父类方法的时候,访问权限需要大于等于父类 */
    @Override
    public void method() {
        System.out.println("Son中method()方法被调用");
    }
}

9. 抽象类和抽象方法

抽象类的概述:

我们在描述一类事物的时候,发现这种事物确实存在着某种行为,但是这种行为目前并不是具体的,那么我们可以抽取这种行为的声明,但是不去实现该种行为,这时候这种行为称为抽象的行为,我们就需要使用抽象类。

抽象方法指一些只有方法声明,而没有具体方法体的方法。抽象方法一般存在于抽象类或接口中。

抽象类的好处:

声明抽象类的唯一目的是将来对该类进行扩充,即强制要求子类一定要实现指定的方法(一个方法只要有大括号,就是具体的实现)。

抽象类的特点:

抽象类和抽象方法必须使用 abstract 关键字修饰。

// 抽象类的定义
public abstract class 类名 {}

// 抽象方法的定义
public abstract void eat();
  • 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
  • 抽象类不能实例化。
  • 抽象类可以有构造方法。
  • 抽象类的子类:要么重写抽象类中的所有抽象方法,要么也是抽象类。

抽象类的使用细节:

  1. 如果一个方法没有方法体(即大括号),那么该方法必须要使用 abstract 修饰,把该函数修饰成抽象的方法。
  2. 如果一个类出现了抽象方法,那么该类也必须使用 abstract 修饰。
  3. 如果一个非抽象类继承了抽象类,那么必须要把抽象类的所有抽象方法全部实现。
  4. 抽象类可以存在非抽象方法。
  5. 抽象类可以不存在抽象方法(虽然语法支持不存在,但一般会存在抽象方法)。
  6. 抽象类是不能创建对象的。原因:因为抽象类中是存在抽象方法的,如果抽象类能创建对象,那么用抽象的对象来调用抽象方法是没有意义的。
  7. 抽象类是存在构造函数的,用来给子类创建对象时初始化父类的属性。

abstract 不能与以下关键字共同修饰一个方法:

  1. private(导致子类无法调用而无法重写抽象方法)
  2. static(导致可用类名创建对象)
  3. final(导致子类无法修改重写抽象方法)

抽象类的示例:

  • 案例需求:

    • 定义猫类(Cat)和狗类(Dog)
    • 猫类成员方法:eat(猫吃鱼)drink(喝水…)
    • 狗类成员方法:eat(狗吃肉)drink(喝水…)
  • 实现步骤:

    1. 猫类和狗类中存在共性内容,应向上抽取出一个动物类(Animal)。
    2. 父类 Animal 中,无法将 eat 方法具体实现描述清楚,所以定义为抽象方法。
    3. 抽象方法需要存活在抽象类中,应将 Animal 定义为抽象类。
    4. 让 Cat 和 Dog 分别继承 Animal,重写 eat 方法。
    5. 测试类中创建 Cat 和 Dog 对象,调用方法测试。
  • 代码实现:
// 动物类
public abstract class Animal {

    public Animal() {
    }

    public void drink(){
        System.out.println("喝水");
    }

    public abstract void eat();
}
// 猫类
public class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}
// 狗类
public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃肉");
    }
}
// 测试类
public static void main(String[] args) {
        Dog d = new Dog();
        d.eat();
        d.drink();

        Cat c = new Cat();
        c.drink();
        c.eat();

        // Animal a = new Animal();  报错:抽象类不能实例化
        // a.eat();
    }

10. 模板设计模式

设计模式:

  • 设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
  • 使用设计模式是为了代码可重用、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

模板设计模式:

  • 例如抽象类就可以看做成一个模板,模板中不能明确的东西定义成抽象方法。
  • 让使用模板的类(抽象类的子类)去重写抽象方法实现具体需求。

模板设计模式的优势:

  • 模板已经定义了通用结构,使用者只需要关心自己需要实现的功能即可。

示例代码:

* 模板类:

/*
    作文模板类
 */
public abstract class CompositionTemplate {

    public final void write(){
        System.out.println("<<我的爸爸>>");
        body();
        System.out.println("啊~ 这就是我的爸爸");
    }

    public abstract void body();
}

* 实现类 A:

public class Tom extends CompositionTemplate {

    @Override
    public void body() {
        System.out.println("那是一个秋天, 风儿那么缠绵,记忆中, " +
                "那天爸爸骑车接我放学回家,我的脚卡在了自行车链当中,爸爸蹬不动,他就站起来蹬...");
    }
}

* 实现类 B:

public class Tony extends CompositionTemplate {
    @Override
    public void body() {

    }

    /*public void write(){

    }*/
}

* 测试类:

public class Test {
    public static void main(String[] args) {
        Tom t = new Tom();
        t.write();
    }
}

10、final关键字

final 表示最终的意思,可以修饰类、成员方法和成员变量。

  • final 修饰类:该类不能被继承(不能有子类,但可以有父类),且 final 类中的所有成员方法都会被隐式低指定为 final 方法。
  • final 修饰方法:该方法不能被重写。
  • final 修饰变量:表明该变量是一个常量,不能再次赋值。
    • 变量是基本类型:不能改变的是值。
    • 变量是引用类型:不能改变的是地址值,但地址里面的内容是可以改变的。

示例:

public static void main(String[] args){
    final Student s = new Student(23);
  	s = new Student(24);  // 错误:不能改变s的地址值
 	s.setAge(24);  // 正确
}

11、接口

1. 接口概述

接口相当于就是对外的一种约定和标准。这里拿操作系统举例子,为什么会有操作系统?就会为了屏蔽软件的复杂性和硬件的简单性之间的差异,为软件提供统一的标准。

Java 接口存在的两个意义:

  1. 定义约束规范。
  2. 用来做功能的扩展。
  3. 程序的解耦(低耦合)。

接口的特点:

  • 接口用关键字 interface 修饰:public interface 接口名 {}

  • 类实现接口用 implements 表示:public class 类名 implements 接口名 {}

  • 接口不能实例化,只可以创建接口的实现类对象。

  • 接口的子类:要么重写接口中的所有抽象方法;要么子类也是抽象类(可以实现,也可以不实现接口中的方法)。

接口的成员特点:

  • 成员变量:只能是常量,默认修饰符:public static final(即使少写修饰符,编译器也会自动添加)。

  • 构造方法:没有,因为成员变量已经是固定的,方法又是抽象的而不能调用,因此构造方法没意义。

  • 成员方法:只能是抽象方法,默认修饰符:public abstract

  • 修饰符:接口中只能使用两种访问修饰符,一种是public,它对整个项目可见;一种是default缺省值,它只具有包访问权限。

类和接口的关系:

  • 类与类的关系:继承关系。只能单继承,但可以多层继承。

  • 接口与接口的关系:继承关系。可以单继承,也可以多继承。

  • 类与接口的关系:实现关系。可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口。

疑问:Java 为什么不支持多继承类,而支持多实现接口?

  1. 若直接继承多个类,且多个父类有同名的方法时,不知调用哪个;
  2. 支持多实现接口是因为即使多个接口有同名的方法,但在类中实现后,是调用在类中已经具体实现的方法。

2. 接口的组成

  • 常量:public static final

  • 抽象方法:public abstract

  • 默认方法(Java 8)

  • 静态方法(Java 8)

  • 私有方法(Java 9)

接口的默认方法:

定义格式:public default 返回值类型 方法名(参数列表) { }

// 示例
public default void show3() { 
}

作用:解决接口升级的问题

  • 以前,当创建了一个接口,并且已经被大量的类实现时,如果需要再扩充这个接口的功能加新的方法,就会导致所有已经实现的子类需要重写这个方法。
  • 而如果在接口中已实现了默认方法,就不会有这个问题(不覆盖的话就执行默认方法)。所以从 JDK8 开始就新加了接口的默认方法,便于接口的扩展。

接口中默认方法的规则:

  • public 可以省略,default 不能省略。
  • 接口中既可以定义抽象方法,又可以定义默认方法,默认方法不是抽象方法。
  • 子类实现接口的时候,可以直接调用接口中的默认方法,即继承了接口中的默认方法。
  • 默认方法不是抽象方法,所以不强制被重写。但是可以被重写,重写的时候去掉 default 关键字。
  • 如果实现了多个接口,多个接口中存在相同的默认方法声明,子类就必须对该默认方法进行重写。

3. 接口的静态方法

定义格式:public static 返回值类型 方法名(参数列表) { }

public static void show() {
}

注意事项:

  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用。
  • public 可以省略,static 不能省略。

4. 接口的私有方法

私有方法的产生原因:

  • Java 9 中新增了带方法体的私有方法,这其实在 Java 8 中就埋下了伏笔:Java 8 允许在接口中定义带方法体的默认方法和静态方法。
  • 这样可能就会引发一个问题:当两个默认方法或者静态方法中包含一段相同的代码实现时,程序必然考虑将这段实现代码抽取成一个共性方法,而这个共性方法是不需要让别人使用的,因此要用私有给隐藏起来,这就是 Java 9 增加私有方法的必然性。

定义格式:

  • 格式一:private 返回值类型 方法名(参数列表) { }
private void show() {  
}
  • 格式二:private static 返回值类型 方法名(参数列表) { }
private static void method() {  
}

注意事项:

  • 默认方法可以调用私有的静态方法和非静态方法。
  • 静态方法只能调用私有的静态方法。

5. 接口与抽象类的异同

相同点

  1. 都可以被继承。
  2. 都不能直接实例化。
  3. 都可以包含抽象方法。
  4. 派生类必须实现未实现的方法。

不同点

  1. 接口支持多继承;抽象类不支持多继承。
  2. 一个类只能继承一个抽象类,而一个类可以实现多个接口。
  3. 接口中的成员变量只能是常量(public static final 类型);抽象类中的成员变量可以是各种类型。
  4. 接口只能定义抽象方法;抽象类既可以定义抽象方法,也可以定义实现方法。
  5. 接口中不能含有静态代码块以及静态方法;抽象类则可以有静态代码块和静态方法。

12、多态

1. 多态的概述

多态:指同一种类型的对象执行同一个方法时可以表现出不同的行为特征。

多态是把子类型对象主观地看作是其父类型的对象,因此其父类型就可以是很多种类型。

  • 即父类的引用变量指向子类的对象,或者是接口的引用变量指向了接口实现类的对象。
  • 定义方法的时候,使用父类型作为参数;在调用方法的时候,使用具体的子类型参与操作。

使用前提:必须存在继承或者实现的关系。

多态的好处:

  • 好处:提高了程序的扩展性。
    1. 多态用于形参类型的时候,可以接收更多类型的数据。
    2. 多态用于返回值类型的时候,可以返回更多类型的数据。

代码示例:

abstract class MyShape {

    public abstract void getArea();

    public abstract void getLength();
}

class Circle extends MyShape {

    public static final double PI = 3.14;

    double r;

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

    public void getArea(){
        System.out.println("圆形的面积:"+PI*r*r);
    }

    public void getLength() {
        System.out.println("圆形的周长:"+PI*2*r);
    }
}

class Rect extends MyShape {

    int width;
    int height;

    public Rect(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public void getArea() {
        System.out.println("矩形的面积:"+width*height);
    }

    public void getLength() {
        System.out.println("矩形的周长:"+2*(width+height));
    }
}

/*
即使后来增加了梯形的需求,也无需改动其余代码,直接创建对象即可,即提高代码的拓展性。
class 梯形 extends MyShape{
           ……
           ……
}
*/

public class Test {
    public static void main(String[] args) {

        // 需求1 调用
        Circle c = new Circle(4.0);
        print(c);  // 即 MyShape s = new Circle(4.0);
        Rect r = new Rect(3, 4);
        print(r);
        // 梯形 t = new 梯形(3, 4);
        //print(t);

        // 需求2 调用
        MyShape s = getShape(1);  // 调用了使用多态的方法,定义的变量类型要与返回值类型一致
        s.getArea();
        s.getLength();
    }

    // 需求1:定义一个函数可以接收任意类型的图形对象,并且打印图形面积与周长
    public static void print(MyShape s){
        s.getArea();
        s.getLength();
    }

    // 需求2:定义一个函数可以返回任意类型的图形对象
    public static MyShape getShape(int i){
        if(i==0){
            return new Circle(4.0);
        }else{
            return new Rect(3, 4);
        }
    }
}

多态中的成员访问特点:

  1. 子父类存在同名的成员时,访问的都是父类的成员,除了是同名的非静态方法时才是访问子类的。
  2. 如果需要访问子类独有的成员方法,那么需要进行(引用变量)类型强制转换。

JAVA 编译器:编译看左边,运行不一定看右边。

  • 编译看左边:编译时会检查引用变量所属的类是否具备指定的成员,如果不具备则马上编译报错。

  • 运行不一定看右边:如果子类中没有覆盖指定的方法,就去父类里找,父类里没有,就去父类的父类找,只要能让一个引用指向这个对象,就说明这个对象肯定是这个类型或者其子类的一个实例(否则赋值会报 ClassCastException),总归有父类兜底。

  • 总结:能调用哪些方法,是引用决定的;具体执行哪个类的方法,是引用指向的对象决定的。这就是覆盖的精髓,覆盖是多态的一种,是最重要的一种。

代码示例:定义一个函数可以接收任意类型的动物对象,在函数内部要调用到动物独有的方法。

class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }
}

class Cat extends Animal {

    public Cat(String name) {
        super(name);
    }

    //猫独有的方法
    public void get() {
        System.out.println(name+"捉老鼠..");
    }
}

class Mouse extends Animal {

    public Mouse(String name) {
        super(name);
    }

    //老鼠独有的方法
    public void run() {
         System.out.println(name+"打洞逃跑..");
    }
}

public class Test {

    public static void main(String[] args) {
        Animal a = new Cat("汤姆猫");
        getUniqueWay(a);
    }

    // 需求:定义一个函数可以接收任意类型的动物对象,在函数内部要调用到动物独有的方法。
    public static void getUniqueWay(Animal a) {
        if (a instanceof Mouse) {
            Mouse m = (Mouse) a;
            m.run();
        } else if(a instanceof Cat) {
            Cat c = (Cat) a;
            c.get();
        }
    }
}

2. 多态的转型

  • 向上转型:父类引用指向子类对象就是向上转型。

  • 向下转型:子类型 对象名 = (子类型) 父类引用;

代码示例:

class Father {
    public void show(){
        System.out.println("Father..show...");
    }
}

class Son extends Father {
    @Override
    public void show() {
        System.out.println("Son..show...");
    }

    public void method(){
        System.out.println("我是子类特有的方法, method");
    }
}

public class Test {
    public static void main(String[] args) {
        // 1. 向上转型:父类引用指向子类对象
        Father f = new Son();
        f.show();
        
        // 多态的弊端: 不能调用子类特有的成员
        // f.method();  // 此行报错
        // 解决方式一: 直接创建子类对象
        // 解决方式二:向下转型

        // 2. 向下转型:从父类类型,转换回子类类型
        Son s = (Son) f;
        s.method();  // "我是子类特有的方法, method"
    }
}

多态转型存在的风险和解决方案:

  • 风险:如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现 ClassCastException。

  • 解决方案:先使用 instanceof 关键字判断类型。

  • 使用方法:变量名 instanceof 类型

  • 通俗理解:判断 instanceof 关键字左边的变量是否属于右边的类型,返回 boolean 类型的结果。

示例:

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

class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("狗吃肉");
    }

    public void watchHome(){
        System.out.println("看家");
    }
}

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

public class Test {
    public static void main(String[] args) {
        useAnimal(new Dog());
        useAnimal(new Cat());
    }

    public static void useAnimal(Animal a){  // Animal a = new Dog();
                                             // Animal a = new Cat();
        a.eat();
        // a.watchHome();  // 无法通用:因为 Cat 没有 watchHome 方法,会报 ClassCastException 类型转换异常

        // 解决方案:先判断 a 变量的类型是否是 Dog
        if(a instanceof Dog){
			// 向下转型
            Dog dog = (Dog) a;
            dog.watchHome();
        }
    }
}

13、内部类

内部类的概念:在一个类中定义一个类。例如,在一个 A 类的内部定义了一个 B 类,那么 B 类就被称为内部类。

内部类的 class 文件:外部类$内部类.class,其好处:便于区分该 class 文件是属于哪个外部类的。

1. 内部类的基本使用

内部类的定义格式:

// 格式
class 外部类名 {
	修饰符 class 内部类名{

	}
}

// 示例
class Outer {
    public class Inner {

    }
}

内部类的访问特点:

  • 内部类可以直接访问外部类的成员,包括私有。
  • 外部类要访问内部类的成员,必须先创建内部类的对象。

示例:

public class Outer {
    private int num = 10;

	public class Inner {
        public void show() {
            System.out.println(num);
        }
    }

    public void method() {
        Inner i = new Inner();
        i.show();
    }
}

2. 成员内部类

成员内部类的定义位置:跟成员变量是一个位置。

应用场景:事物 A 里存在另外一个事物 B,而事物 B 需要经常访问事物 A 的成员,那么就使用成员内部类。

外部访问成员内部类的格式:外部类名.内部类名 对象名 = 外部类对象.内部类对象;

// 示例
Outer.Inner oi = new Outer().new Inner();

私有成员内部类:

将一个类设计为内部类的目的,大多数都是不想让外界去访问,所以内部类的定义应该私有化。私有化之后,再提供一个可以让外界调用的方法,由该方法内部创建内部类对象并调用。

静态成员内部类:

  • 静态成员内部类的访问格式:外部类名.内部类名 对象名 = new 外部类名.内部类名();

  • 静态成员内部类中的静态方法:外部类名.内部类名.方法名();

综合示例:

class Outer {

    int x = 100;

    // 成员内部类
    static class Inner {

        static int x = 10;

        static public void print(){
            System.out.println("x="+Outer.this.x);  // 输出外部类的同名成员变量
        }
    }
}

class Test {
    public static void main(String[] args) {

        //常规访问方法
        Outer.Inner i = new Outer().new Inner();
        i.print();

        //使用类名访问静态内部类的静态成员
        System.out.println(Outer.Inner.x);  // 静态成员变量
        Outer.Inner.print();  //静态成员函数

        //创建静态内部类的对象
        Outer.Inner i = new Outer.Inner();
        i.print();
    }
}

注意事项:

  1. 如果外部类与内部类存在同名的成员变量,那么在内部类中默认访问的是内部类的成员变量(可以通过外部类.this.成员变量名指定访问外部类的成员)。
  2. 私有的成员内部类只能在外部类提供一个方法创建内部类的对象进行访问,不能在其他类创建对象。
  3. 成员内部类一旦出现静态成员,那么该内部类也必须使用 static 修饰(原因:静态的成员数据是不需要对象才能访问的)。

3. 局部内部类

局部内部类的定义位置:局部内部类是在方法中定义的类。

局部内部类的使用方式

  • 局部内部类,外界是无法直接使用的,需要在方法内部创建对象并使用。
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量。

注意:

  • 如果局部内部类访问一个局部变量,那么该局部变量需加上 final 修饰。

  • 原因:方法执行完毕后,局部变量消失,而内部类对象直到垃圾回收才消失,即内部类对象生命周期比局部变量长,给人感觉局部变量的生命周期被延长了,所以要访问局部变量的复制品。

示例代码:

class Outer {

    int x = 100;

    public void print(){

        // 局部变量
        final int x = 10;  // 加上final后能给局部内部类访问
        
        // 局部内部类
        class Inner {

            public void getInner(){
                System.out.println("我是局部内部类..");
            }
        }

        Inner i = new Inner();
        i.getInner();
    }
}

class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();
    }
}

4. 匿名内部类

概念:没有类名的类。匿名内部类只是没有类名,其他的一概成员都是具备的。

好处:简化书写。

使用前提:必须存在继承或者实现关系才能使用。

应用场景:匿名内部类一般用于实参。

定义格式:new 已存在的类名() { 重写方法 } 或 new 已存在的接口名() { 重写方法 }

new Inter(){
  @Override
  public void method(){}
}

匿名内部类的本质:是一个继承了该类或者实现了该接口的子类匿名对象。

匿名内部类的细节:匿名内部类可以通过多态的形式接收。

Inter i = new Inter(){
  @Override
    public void method(){
        
    }
}

示例:继承关系下的匿名内部类。

abstract class Animal{

    public abstract Animal run();   //返回Animal类型

    public abstract void sleep();
}

class Outer{

    //需求:在方法内部定义一个类继承Animal类,然后同时调用run与sleep方法。
    public void print(){
        /*
        //局部内部类的做法:
        class Dog extends Animal{

            public void run(){
                System.out.println("斑点狗在跑..");
            }

            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            //若出现独有的方法,只能使用局部内部类的访问方法
            public void bite(){
                System.out.println("斑点狗在咬咬..");
            }
        }
        Dog d = new Dog();
        d.run();
        d.sleep();
        */

        /*
        //匿名内部类的做法:
        //匿名内部类与Animal是继承的关系;目前创建的是子类的对象。
        //方法一:
        Animal a = new Animal(){    //多态

            //实现父类的两个抽象方法
            public void run(){
                System.out.println("斑点狗在跑..");
            }
            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            /*若出现独有的方法,只能使用局部内部类的访问方法。
              因为匿名内部类没有类名,无法创建内部类的对象来进行强制类型转换。
            public void bite(){
                 System.out.println("斑点狗在咬咬..");
            }*/ 
        };
        a.run();
        a.sleep();
        */

        //方法二:
        new Animal(){   

            //实现父类的两个抽象方法
            public Animal run(){
                System.out.println("斑点狗在跑..");
                return this;  //返回目前调用的对象
            }
            public void sleep(){
                System.out.println("斑点狗在睡觉..");
            }

            /*若出现独有的方法,只能使用局部内部类的访问方法
            public void bite(){
                System.out.println("斑点狗在咬咬..");
            }
            */
        }.run().sleep();   //即this.sleep();

   }
}

class Test{
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();
    }
}

示例:实现关系下的匿名内部类。

interface Dao{

    public void add();
}

class Outer{

    public void print(){
        //创建一个匿名内部类对象
        new Dao(){

            public void add(){
                System.out.println("添加成功");
            }
        }.add();
    }
}

class Test{
    public static void main(String[] args) {
        Outer o = new Outer();
        o.print();               
    }
}

匿名内部类在开发中的使用:

当发现某个方法需要使用到某个接口或抽象类的子类对象,我们就可以传递一个匿名内部类过去,来简化传统的代码。

interface Dao{

    public void add();
}

/*
class Outer{
    匿名内部类直接在实参创建即可。
}
*/

class Test{
    public static void main(String[] args) {
        test(new Dao(){

            public void add(){
                 System.out.println("添加成功");
            }
        });
    }

    public static void test(Dao d){  // 形参是一个接口类型,只能传接口的实现类
         d.add();
    }
}

14、函数式编程

1. 函数式编程思想

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”。

面向对象思想强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做”。

一个大型程序就是一个顶层函数调用若干底层函数,这些被调用的函数又可以调用其他函数,即大任务被一层层拆解并执行。所以函数就是面向过程的程序设计的基本单元。

Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数。

而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

我们首先要搞明白计算机(Computer)和计算(Compute)的概念。

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。

而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。

Java平台从Java 8开始,支持函数式编程。

2. Lambda表达式

1)Lambda表达式简介

在了解Lambda之前,我们先回顾一下Java的方法。

Java的方法分为实例方法,例如Integer定义的equals()方法:

public final class Integer {
    boolean equals(Object o) {
        ...
    }
}

以及静态方法,例如Integer定义的parseInt()方法:

public final class Integer {
    public static int parseInt(String s) {
        ...
    }
}

无论是实例方法,还是静态方法,本质上都相当于过程式语言的函数。

例如C函数:

char* strcpy(char* dest, char* src)

只不过Java的实例方法隐含地传入了一个this变量,即实例方法总是有一个隐含参数this。

函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。

在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:

  • Comparator
  • Runnable
  • Callable

以Comparator为例,我们想要调用Arrays.sort()时,可以传入一个Comparator实例,以匿名类方式编写如下:

String[] array = ...
Arrays.sort(array, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }
});

上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:

// Lambda
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, (s1, s2) -> {
            return s1.compareTo(s2);
        });
        System.out.println(String.join(", ", array));
    }
}

2)Lambda表达式语法

 观察Lambda表达式的写法,它只需要写出方法定义:

(s1, s2) -> {
    return s1.compareTo(s2);
}

其中,参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { ... }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁。

如果只有一行return xxx的代码,完全可以用更简单的写法:

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));

返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错。

Lambda 表达式的标准格式:

格式:(形式参数) -> {代码块}

  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可。
  • ->:由英文中画线大于符号组成,固定写法,代表指向动作。
  • 代码块:是我们具体要做的事情。

Lambda 表达式的使用前提:

  • 有一个接口。
  • 接口中有且仅有一个抽象方法。

3)FunctionalInterface

我们把只定义了单方法的接口称之为FunctionalInterface,用注解@FunctionalInterface标记。例如,Callable接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

再来看Comparator接口:

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}

虽然Comparator接口有很多方法,但只有一个抽象方法int compare(T o1, T o2),其他的方法都是default方法或static方法。另外注意到boolean equals(Object obj)是Object定义的方法,不算在接口方法内。因此,Comparator也是一个FunctionalInterface。

总结:

  • 单方法接口被称为FunctionalInterface。
  • 接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。
  • Lambda表达式的参数和返回值均可由编译器自动推断。

4)方法引用

使用Lambda表达式,我们就可以不必编写FunctionalInterface接口的实现类,从而简化代码:

Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
});

实际上,除了Lambda表达式,我们还可以直接传入方法引用。

例如:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, Main::cmp);
        System.out.println(String.join(", ", array));
    }

    static int cmp(String s1, String s2) {
        return s1.compareTo(s2);
    }
}

上述代码在Arrays.sort()中直接传入了静态方法cmp的引用,用Main::cmp表示。

因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。

因为Comparator<String>接口定义的方法是int compare(String, String),和静态方法int cmp(String, String)相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入:

Arrays.sort(array, Main::cmp);

​注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。

我们再看看如何引用实例方法。

Arrays.sort(array, Main::cmp),​如果我们把代码改写如下:

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
        Arrays.sort(array, String::compareTo);
        System.out.println(String.join(", ", array));
    }
}

不但可以编译通过,而且运行结果也是一样的,这说明String.compareTo()方法也符合Lambda定义。

观察String.compareTo()的方法定义:

public final class String {
    public int compareTo(String o) {
        ...
    }
}

这个方法的签名只有一个参数,为什么和int Comparator<String>.compare(String, String)能匹配呢?

因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:

public static int compareTo(this, String o);

所以,String.compareTo()方法也可作为方法引用传入。

除了可以引用静态方法和实例方法,我们还可以引用构造方法。

我们来看一个例子:如果要把一个List<String>转换为List<Person>,应该怎么办?

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = ???

传统的做法是先定义一个ArrayList<Person>,然后用for循环填充这个List:

List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
    persons.add(new Person(name));
}

要更简单地实现String到Person的转换,我们可以引用Person的构造方法:

// 引用构造方法
import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = List.of("Bob", "Alice", "Tim");
        List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
        System.out.println(persons);
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
    public String toString() {
        return "Person:" + this.name;
    }
}

后面我们会讲到Stream的map()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

把泛型对应上就是方法签名Person apply(String),即传入参数String,返回类型Person。而Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是类名::new,因此,此处传入Person::new。 

FunctionalInterface允许传入:

  • 接口的实现类(传统写法,代码较繁琐);
  • Lambda表达式(只需列出参数名,由编译器推断类型);
  • 符合方法签名的静态方法;
  • 符合方法签名的实例方法(实例类型被看做第一个参数类型);
  • 符合方法签名的构造方法(实例类型被看做返回类型)。

FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。

5)Lambda表达式的基础使用

示例:

// 游泳接口
interface Swimming {
    void swim();
}

// 测试类
public class TestSwimming {
    public static void main(String[] args) {

        // 方式一:通过匿名内部类实现
        goSwimming(new Swimming() {
            @Override
            public void swim() {
                System.out.println("铁汁, 我们去游泳吧");
            }
        });

        /*  方式二:通过 Lambda 表达式实现
            理解: Lambda 表达式是对匿名内部类的优化
         */
        goSwimming(() -> System.out.println("铁汁, 我们去游泳吧"));
    }

    /**
     * 使用到接口的方法
     */
    public static void goSwimming(Swimming swimming) {
        swimming.swim();
    }
}

示例 1:无参无返回值的抽象方法。

  • 定义一个接口 (Eatable),里面定义一个抽象方法:void eat();
  • 定义一个测试类 (EatableDemo),在测试类中提供两个方法
    • 一个方法是:useEatable(Eatable e)
    • 一个方法是主方法,在主方法中调用 useEatable 方法
// 接口
public interface Eatable {
    void eat();
}

// 实现类
public class EatableImpl implements Eatable {
    @Override
    public void eat() {
        System.out.println("一天一苹果,医生远离我");
    }
}
//测试类
public class EatableDemo {
	// 在主方法中调用 useEatable 方法
    public static void main(String[] args) {

        // 实现类的方式
        Eatable e = new EatableImpl();
        useEatable(e);

        // 匿名内部类的方式
        useEatable(new Eatable() {
            @Override
            public void eat() {
                System.out.println("一天一苹果,医生远离我");
            }
        });

        // Lambda 表达式的方式
        useEatable(() -> {
            System.out.println("一天一苹果,医生远离我");
        });
    }

    private static void useEatable(Eatable e) {
        e.eat();
    }
}

示例 2:有参无返回值的抽象方法。

interface Flyable {
    void fly(String s);
}

public class Test {

    public static void main(String[] args) {

        // 匿名内部类
        useFlyable(new Flyable() {
            @Override
            public void fly(String s) {
                System.out.println(s);
                System.out.println("飞机自驾游");
            }
        });
        System.out.println("--------");

        // Lambda
        useFlyable((String s) -> {
            System.out.println(s);
            System.out.println("飞机自驾游");
        });

    }

    private static void useFlyable(Flyable f) {
        f.fly("风和日丽,晴空万里");
    }
}

示例 3:有参有返回值的抽象方法。

public interface Addable {
    int add(int x, int y);
}

public class AddableDemo {

    public static void main(String[] args) {
        // lambda 实现 Addable
        useAddable((int x, int y) -> {
            return x + y;
        });
    }

    private static void useAddable(Addable a) {
        int sum = a.add(10, 20);
        System.out.println(sum);
    }
}

6)Lambda 表达式的省略模式

省略的规则:

  • 参数类型可以省略。
  • 如果参数有且仅有一个,那么小括号可以省略。
  • 如果代码块的语句只有一条,可以省略大括号和分号,和 return 关键字。
public interface Addable {
    int add(int x, int y);
}

public interface Flyable {
    void fly(String s);
}

public class LambdaDemo {

    public static void main(String[] args) {
//        useAddable((int x, int y) -> {
//            return x + y;
//        });
        // 参数的类型可以省略
        useAddable((x, y) -> {
            return x + y;
        });

//        useFlyable((String s) -> {
//            System.out.println(s);
//        });
//		  // 如果参数有且仅有一个,那么小括号可以省略
//        useFlyable(s -> {
//            System.out.println(s);
//        });

        // 如果代码块的语句只有一条,可以省略大括号和分号
        useFlyable(s -> System.out.println(s));

        // 如果代码块的语句只有一条,可以省略大括号和分号,如果有return,return也要省略掉
        useAddable((x, y) -> x + y);
    }

    private static void useFlyable(Flyable f) {
        f.fly("风和日丽,晴空万里");
    }

    private static void useAddable(Addable a) {
        int sum = a.add(10, 20);
        System.out.println(sum);
    }
}

7)Lambda 表达式和匿名内部类的区别

所需类型不同:

  • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。
  • Lambda 表达式:只能是接口。

使用限制不同:

  • 如果接口中有且仅有一个抽象方法,可以使用 Lambda 表达式,也可以使用匿名内部类。
  • 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用 Lambda 表达式。

实现原理不同:

  • 匿名内部类:编译之后,产生一个单独的 .class 字节码文件。
  • Lambda 表达式:编译之后,没有一个单独的 .class 字节码文件,对应的字节码是在运行的时候动态生成。

3. Stream

1)Stream简介

java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。

划重点:这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。两者对比如下:

java.iojava.util.stream
存储顺序读写的bytechar顺序输出的任意Java对象实例
用途序列化至文件或网络内存计算/业务逻辑

有同学会问:一个顺序输出的Java对象序列,不就是一个List容器吗?

再次划重点:这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。

换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:

java.util.Listjava.util.stream
元素已分配并存储在内存可能未分配,实时计算
用途操作一组已存在的Java对象惰性计算

Stream看上去有点不好理解,但我们举个例子就明白了。

如果我们要表示一个全体自然数的集合,显然,用List是不可能写出来的,因为自然数是无限的,内存再大也没法放到List中:

List<BigInteger> list = ??? // 全体自然数?

但是,用Stream可以做到。写法如下:

Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数

我们先不考虑createNaturalStream()这个方法是如何实现的,我们看看如何使用这个Stream。

首先,我们可以对每个自然数做一个平方,这样我们就把这个Stream转换成了另一个Stream:

Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // 全体自然数的平方

因为这个streamNxN也有无限多个元素,要打印它,必须首先把无限多个元素变成有限个元素,可以用limit()方法截取前100个元素,最后用forEach()处理每个元素,这样,我们就打印出了前100个自然数的平方:

Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
        .limit(100)
        .forEach(System.out::println);

我们总结一下Stream的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。

Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。

最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。

Stream<BigInteger> naturals = createNaturalStream(); // 不计算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
Stream<BigInteger> s3 = s2.limit(100); // 不计算
s3.forEach(System.out::println); // 计算

惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。

例如,创建一个全体自然数的Stream,不会进行计算,把它转换为上述s2这个Stream,也不会进行计算。再把s2这个无限Stream转换为s3这个有限的Stream,也不会进行计算。只有最后,调用forEach确实需要Stream输出的元素时,才进行计算。我们通常把Stream的操作写成链式操作,代码更简洁:

createNaturalStream()
    .map(BigInteger::multiply)
    .limit(100)
    .forEach(System.out::println);

因此,Stream API的基本用法就是:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:

int result = createNaturalStream() // 创建Stream
             .filter(n -> n % 2 == 0) // 任意个转换
             .map(n -> n * n) // 任意个转换
             .limit(100) // 任意个转换
             .sum(); // 最终计算结果

Stream API的特点是:

  • Stream API提供了一套新的流式处理的抽象序列;
  • Stream API支持函数式编程和链式操作;
  • Stream可以表示无限序列,并且大多数情况下是惰性求值的。

2)创建Stream

要使用Stream,就必须先创建它。

创建Stream的方法有 :

  • 通过指定元素、指定数组、指定Collection创建Stream;
  • 通过Supplier创建Stream,可以是无限序列;
  • 通过其他类的相关方法创建;

(1)Stream.of()

创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("A", "B", "C", "D");
        // forEach()方法相当于内部循环调用,
        // 可传入符合Consumer接口的void accept(T t)的方法引用:
        stream.forEach(System.out::println);
    }
}

虽然这种方式基本上没啥实质性用途,但测试的时候很方便。

(2)基于数组或Collection

第二种创建Stream的方法是基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
        Stream<String> stream2 = List.of("X", "Y", "Z").stream();
        stream1.forEach(System.out::println);
        stream2.forEach(System.out::println);
    }
}

把数组变成Stream使用Arrays.stream()方法。对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream。

上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。

(3)基于Supplier

创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:

Stream<String> s = Stream.generate(Supplier<String> sp);

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。

例如,我们编写一个能不断生成自然数的Supplier,它的代码非常简单,每次调用get()方法,就生成下一个自然数:

import java.util.function.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<Integer> natual = Stream.generate(new NatualSupplier());
        // 注意:无限序列必须先变成有限序列再打印:
        natual.limit(20).forEach(System.out::println);
    }
}

class NatualSupplier implements Supplier<Integer> {
    int n = 0;
    public Integer get() {
        n++;
        return n;
    }
}

上述代码我们用一个Supplier<Integer>模拟了一个无限序列(当然受int范围限制不是真的无限大)。如果用List表示,即便在int范围内,也会占用巨大的内存,而Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。

对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()方法可以截取前面若干个元素,这样就变成了一个有限序列,对这个有限序列调用forEach()或者count()操作就没有问题。

(4)其他方法

创建Stream的第三种方法是通过一些API提供的接口,直接获得Stream。

例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    ...
}

此方法对于按行遍历文本文件十分有用。

另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:

Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);

3)基本类型

基本类型的Stream有IntStream、LongStream和DoubleStream。

因为Java的范型不支持基本类型,所以我们无法用Stream<int>这样的类型,会发生编译错误。为了保存int,只能使用Stream<Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:

// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);

4)map

Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。

map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream,可以将一种元素类型转换成另一种元素类型。

所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:

可见,map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。 

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);

如果我们查看Stream的源码,会发现map()方法接收的对象是Function接口对象,它定义了一个apply()方法,负责把一个T类型转换成R类型:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

其中,Function的定义是:

@FunctionalInterface
public interface Function<T, R> {
    // 将T类型转换为R:
    R apply(T t);
}

利用map(),不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
                .stream()
                .map(String::trim) // 去空格
                .map(String::toLowerCase) // 变小写
                .forEach(System.out::println); // 打印
    }
}

通过若干步map转换,可以写出逻辑简单、清晰的代码。

5)filter

Stream.filter()是Stream的另一个常用转换方法。使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream。

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。

例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:

用IntStream写出上述逻辑,代码如下: 

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .filter(n -> n % 2 != 0)
                .forEach(System.out::println);
    }
}

从结果可知,经过filter()后生成的Stream元素可能变少。

filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

@FunctionalInterface
public interface Predicate<T> {
    // 判断元素t是否符合条件:
    boolean test(T t);
}

filter()除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:

import java.time.*;
import java.util.function.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream.generate(new LocalDateSupplier())
                .limit(31)
                .filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
                .forEach(System.out::println);
    }
}

class LocalDateSupplier implements Supplier<LocalDate> {
    LocalDate start = LocalDate.of(2020, 1, 1);
    int n = -1;
    public LocalDate get() {
        n++;
        return start.plusDays(n);
    }
}

6)reduce

map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并。

reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

我们来看一个简单的聚合方法:

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
        System.out.println(sum); // 45
    }
}

reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:

@FunctionalInterface
public interface BinaryOperator<T> {
    // Bi操作:两个输入,一个输出
    T apply(T t, T u);
}

上述代码看上去不好理解,但我们用for循环改写一下,就容易理解了:

Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
    sum = (sum, n) -> sum + n;
}

可见,reduce()操作首先初始化结果为指定值(这里是0),紧接着,reduce()对每个元素依次调用(acc, n) -> acc + n,其中,acc是上次计算的结果:

// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9

因此,实际上这个reduce()操作是一个求和。

如果去掉初始值,我们会得到一个Optional<Integer>:

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
    System.out.println(opt.get());
}

这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。

利用reduce(),我们可以把求和改成求积,代码也十分简单:

import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
        System.out.println(s); // 362880
    }
}

注意:计算求积时,初始值必须设置为1。

除了可以对数值进行累积计算外,灵活运用reduce()也可以对Java对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过map()和reduce()操作聚合成一个Map<String, String>:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        // 按行读取配置文件:
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    return Map.of(ss[0], ss[1]);
                })
                // 把所有Map聚合到一个Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                    m.putAll(kv);
                    return m;
                });
        // 打印结果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });
    }
}

7)输出集合

Stream可以输出为集合,Stream通过collect()方法可以方便地输出为List、Set、Map,还可以分组输出。

前面介绍了Stream的几个常见操作:map()、filter()、reduce()。这些操作对Stream来说可以分为两类,一类是转换操作,即把一个Stream转换为另一个Stream,例如map()和filter(),另一类是聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()。

区分这两种操作是非常重要的,因为对于Stream来说,对其进行转换操作并不会触发任何计算!我们可以做个实验:

import java.util.function.Supplier; 
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args)     {
        Stream<Long> s1 = Stream.generate(new NatualSupplier());
        Stream<Long> s2 = s1.map(n -> n * n);
        Stream<Long> s3 = s2.map(n -> n - 1);
        System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842
    }
}

class NatualSupplier implements Supplier<Long> {
    long n = 0;
    public Long get() {
        n++;
        return n;
    }
}

因为s1是一个Long类型的序列,它的元素高达922亿亿个,但执行上述代码,既不会有任何内存增长,也不会有任何计算,因为转换操作只是保存了转换规则,无论我们对一个Stream转换多少次,都不会有任何实际计算发生。

而聚合操作则不一样,聚合操作会立刻促使Stream输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream进行聚合操作,会触发一系列连锁反应:

Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);

我们对s4进行reduce()聚合计算,会不断请求s4输出它的每一个元素。因为s4的上游是s3,它又会向s3请求元素,导致s3向s2请求元素,s2向s1请求元素,最终,s1从Supplier实例中请求到真正的元素,并经过一系列转换,最终被reduce()聚合出结果。

可见,聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。

reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。

下面的代码演示了如何将一组String先过滤掉空字符串,然后把非空字符串保存到List中:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
        List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
        System.out.println(list);
    }
}

把Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        // 把元素s映射为key:
                        s -> s.substring(0, s.indexOf(':')),
                        // 把元素s映射为value:
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}

Stream还有一个强大的分组功能,可以按组输出。我们看下面的例子:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List,上述代码运行结果如下:

{
    A=[Apple, Avocado, Apricots],
    B=[Banana, Blackberry],
    C=[Coconut, Cherry]
}

可见,结果一共有3组,按"A","B","C"分组,每一组都是一个List。

假设有这样一个Student类,包含学生姓名、班级和成绩:

class Student {
    int gradeId; // 年级
    int classId; // 班级
    String name; // 名字
    int score; // 分数
}

如果我们有一个Stream<Student>,利用分组输出,可以非常简单地按年级或班级把Student归类。

8)其他操作

我们把Stream提供的操作分为两类:转换操作和聚合操作。除了前面介绍的常用操作外,Stream还提供了一系列非常有用的方法。

(1)排序

对Stream的元素进行排序十分简单,只需调用sorted()方法:

import java.util.*;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Orange", "apple", "Banana")
            .stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(list);
    }
}

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可:

List<String> list = List.of("Orange", "apple", "Banana")
    .stream()
    .sorted(String::compareToIgnoreCase)
    .collect(Collectors.toList());

注意sorted()只是一个转换操作,它会返回一个新的Stream。

(2)去重

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]

(3)截取

截取操作常用于把一个无限的Stream转换成有限的Stream,skip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:

List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳过A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]

截取操作也是一个转换操作,将返回新的Stream。

(4)合并

将两个Stream合并为一个Stream可以使用Stream的静态方法concat():

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]

(5)flatMap

如果Stream的元素是集合:

Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));

而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap():

Stream<Integer> i = s.flatMap(list -> list.stream());

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream:

(6)并行

通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。

把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:

Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
                   .sorted() // 可以进行并行排序
                   .toArray(String[]::new);

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。

(7)其他聚合方法

除了reduce()和collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {
    System.out.println("Hello, " + str);
});

总结:

Stream提供的常用操作有:

  • 转换操作:map(),filter(),sorted(),distinct();
  • 合并操作:concat(),flatMap();
  • 并行处理:parallel();
  • 聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();
  • 其他操作:allMatch(), anyMatch(), forEach();

十二、Java常用API

API(Application Programming Interface):应用程序编程接口

Java 中的 API:指的就是 JDK 中提供的各种功能的 Java 类。这些类将底层的实现封装了起来,我们不需要关心这些类是如何实现的,只需要知道这些类如何使用即可。我们可以通过 API 帮助文档来学习这些 API 如何使用。

API 在线官方文档

如何使用 API 帮助文档 :

  1. 打开帮助文档
  2. 找到索引选项卡中的输入框
  3. 在输入框中输入 API,如 Random
  4. 看类在哪个包下
  5. 看类的描述
  6. 看构造方法
  7. 看成员方法

1、Scanner 类

常用方法:

  • nextInt():仅接收整数数据,其结束标记:回车换行符

  • next():接收数据不限类型,其结束标记:空格、tab 键

  • nextLine():接收数据不限类型,其结束标记:回车换行符

import java.util.Scanner;

public class DemoScanner {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.println("请输入整数:");
        int num = sc.nextInt();
        System.out.println("请输入字符串:");
        String s = sc.nextLine();

        System.out.println(num);
        System.out.println(s);
    }
}

2、Math 类

Math 类包含了执行基本数字运算的方法。

Math 类中无构造方法,但内部的方法都是静态的,因此可以通过类名.静态方法进行调用。

Math 类的常用方法:

方法名说明
public static int abs(int a)返回参数的绝对值
public static double ceil(double a)向上取整,返回 double 类型
public static double floor(double a)向下取整,返回 double 类型
public static int round(float a)按照四舍五入返回最接近参数的 int
public static int max(int a, int b)返回两个 int 值中的较大值
public static int min(int a, int b)返回两个 int 值中的较小值
public static double pow (double a, double b)返回 a 的 b 次幂的值
public static double random()返回值为 double 的正值:[0.0, 1.0)

3、System 类

System 类的常用方法:

方法名说明public static void exit(int status)终止当前运行的 JVM,非零参数表示异常终止public static long currentTimeMillis()返回当前时间(以毫秒为单位)public static int identityHashCode(Object)返回对象的内存地址,不管该对象的类是否重写了 hashCode() 方法

示例代码:在控制台输出 1-10000,计算这段代码执行了多少毫秒。

public class SystemDemo {
    public static void main(String[] args) {
        // 获取开始的时间节点
        long start = System.currentTimeMillis();
        for (int i = 1; i <= 10000; i++) {
            System.out.println(i);
        }
        // 获取代码运行结束后的时间节点
        long end = System.currentTimeMillis();
        System.out.println("共耗时:" + (end - start) + "毫秒");
    }
}

4、Object 类

Object 概述:Object 是类的层次结构的根,每个类都可以将 Object 作为超类,即所有类都直接或者间接的继承自该类。换句话说,该类所具备的方法,所有类都会有一份。

查看方法源码的方式:选中方法,按下Ctrl + B。

常用方法:

方法名说明
public String toString()返回对象的字符串表示形式。建议所有子类重写该方法。(IDEA 可自动生成打印所有成员变量的重写方法)
public boolean equals(Object)比较对象地址是否相同。默认比较地址,重写可以比较内容。(IDEA 可自动生成比较内容的重写方法)
public static int hasCode(Object)该方法返回对象的哈希码。但是 hashCode() 可以重写,所以 hashCode() 不同不一定就代表内存地址不同
而 System.identityHashCode(对象) 方法可以返回对象的内存地址,不管该对象的类是否重写了 hashCode() 方法

1. toString 方法

toString 方法的作用:返回对象的字符串表示形式。

自动重写 toString 方法的方式

  • Alt + Insert 选择 toString
  • 在类的空白区域,右键 -> Generate -> 选择 toString

代码示例:

class Student extends Object {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

public class ObjectDemo {
    public static void main(String[] args) {
        Student s = new Student();
        s.setName("林青霞");
        s.setAge(30);
        System.out.println(s);  // Student{name='林青霞', age=30}
        System.out.println(s.toString());  // Student{name='林青霞', age=30}
    }
}

2. equals 方法

equals 方法的作用:比较对象地址是否相同。默认比较地址,重写可以比较内容。

equals 与 == 的区别:

  • ==:比较基本数据类型时,比较的是具体的值;比较引用数据类型时,比较的是对象地址。
  • 字符串对象的equals(String s):比较两个字符串的内容是否一致。

  • == 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象,比较的是真正意义上的指针操作;而 equals 用来比较两个字符串对象的内容是否一致。
  • 由于所有的类都是继承自 java.lang.Object 类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是 Object 类中的方法,而 Object 中的 equals 方法返回的却是 == 的判断。
  • String s = "abcd" 是一种非常特殊的形式,和 new 有本质的区别。它是 java 中唯一不需要 new 就可以产生对象的途径。以 String s="abcd"; 形式的赋值在 java 中叫直接量,它是在常量池中而不是像 new 一样放在堆中。这种形式的字符串,在 JVM 内部发生字符串拘留,即当声明这样的一个字符串后,JVM 会在常量池中先查找有没有一个值为 "abcd" 的对象,如果有,就会把它赋给当前引用,即原来那个引用和现在这个引用指点向了同一对象;如果没有,则在常量池中新创建一个"abcd",下一次如果有 String s1 = "abcd"; 又会将 s1 指向 "abcd" 这个对象。即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。
  • 而 String s = new String("abcd"); 和其它任何对象一样,每调用一次就产生一个对象,只要它们调用。也可以这么理解: String str = "hello"; 会先在内存中找是不是有 "hello" 这个对象,如果有,就让 str 指向那个 "hello" 。如果内存里没有 "hello" ,就创建一个新的对象保存 "hello" 。而 String str=new String ("hello"); 就是不管内存里是不是已经有 "hello" 这个对象,都新建一个对象保存 "hello" 。

自动重写 equals 方法的方式

  • 方式一:alt + insert 选择 equals() and hashCode(),IntelliJ Default,一路 next,finish 即可。
  • 方式二:在类的空白区域,右键 -> Generate -> 选择 equals() and hashCode(),后面的同上。

自动重写完 equals 方法后,即可比较字符串内容。

示例:

class Student {

    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public boolean equals(Object o) {
        // this -- s1
        // o -- s2
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;  // student -- s2

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
}

public class ObjectDemo {
    public static void main(String[] args) {
        Student s1 = new Student();
        s1.setName("林青霞");
        s1.setAge(30);

        Student s2 = new Student();
        s2.setName("林青霞");
        s2.setAge(30);

        // 需求:比较两个对象的内容是否相同
        System.out.println(s1.equals(s2));
    }
}

思考题:

public class Test {
    public static void main(String[] args) {
        String s1 = "abc";
        StringBuilder sb = new StringBuilder("abc");

        // 调用的是 String 类中的 equals 方法,其首先会判断传入的对象是否是字符串类型
        // sb 需要先转成 String 类,否则还没有比较内容就直接返回 false
        System.out.println(s1.equals(sb));  // false

        // StringBuilder 类中也没有重写 equals 方法,用的是 Object 类的,故比较的是对象地址
        System.out.println(sb.equals(s1));  // false
    }
}

3. hashCode 方法

hashCode():返回对象的哈希码。

Java 中的规范:一般重写了一个类的 equals 方法后,都会重写它的 hashCode 方法。

综合示例:

class Person{

     int id;
     String name;

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

     public Person(){
     }

     //toString():
     //目前需要直接输出一个对象的时候,输出的格式为:编号:... 姓名:...
     //目前Object的toString方法无法满足子类的需求,因此需要重写子类的toString方法。
     public String toString(){
          return "编号:"+this.id+" 姓名:"+this.name;
     }

     //equals():     
     //Object的equals方法默认是比较两个对象的内存地址,目前需要比较的是两个对象的ID,所以需要重写Object的equals方法。
     public boolean equals(Object obj){ //多态,指向的是Person对象
          Person p = (Person) obj;  //id是Person子类独有的成员,因此需要强制类型转换
          return this.id == p.id; //调用对象的id与实参id的比较
          // 等于:return this.id == ((Person)obj).id;
     }

     //hashCode():
     //java中的规范:一般重写了一个类的equals方法后,都会重写它的hashCode方法。
     public int hashCode(){
          return this.id;
     }
}

public class Test {

     public static void main(String[] args) {

     Person p = new Person(110,"狗娃");
     System.out.println(p); //本来输出完整类名+@+对象的哈希码
                            //重写后现输出 编号:110 姓名:狗娃

     Person p1 = new Person(110,"狗娃");
     Person p2 = new Person(110,"陈大富");
     //需求:在现实中只要两个人的身份证一致,那么就是同一个人
     System.out.println("P1与P2是同一个人吗?"+p1.equals(p2));  //重写equals后比较的是id,true

     System.out.println("P1的哈希码:"+p1.hashCode());  //本来为不同的哈希码
     System.out.println("P2的哈希码:"+p2.hashCode());  //重写后为相同的哈希码
     }
}

5、Objects 类

常用方法:

方法名说明
public static String toString(对象)返回参数中对象的字符串表示形式
public static String toString(对象, 默认字符串)返回对象的字符串表示形式。如果对象为空,则返回默认字符串
public static Boolean isNull(对象)判断对象是否为空
public static Boolean nonNull(对象)判断对象是否不为空

6、BigDecimal 类

作用:可以用来进行精确计算。

构造方法:

方法名说明
BigDecimal(double val)参数为 double
BigDecimal(String val)参数为 String

常用方法:

方法名说明
public BigDecimal add(另一个 BigDecimal 对象)加法
public BigDecimal subtract(另一个 BigDecimal 对象)减法
public BigDecimal multiply(另一个 BigDecimal 对象)乘法
public BigDecimal divide(另一个 BigDecimal 对象)除法
public BigDecimal divide(另一个 BigDecimal 对象,精确几位,舍入模式)除法

总结:

  • 创建 BigDecimal 的对象,构造方法使用参数类型为字符串的。
  • 四则运算中的除法,如果除不尽则使用 divide 的三个参数的方法。

BigDecimal divide = bd1.divide(参与运算的对象, 小数点后精确到多少位, 舍入模式)

  • 参数 1:表示参与运算的 BigDecimal 对象。
  • 参数 2:表示小数点后面精确到多少位。
  • 参数 3:舍入模式:
    • BigDecimal.ROUND_UP:进一法
    • BigDecimal.ROUND_FLOOR:去尾法
    • BigDecimal.ROUND_HALF_UP:四舍五入

7、包装类

1. 基本数据类型的包装类

基本数据类型包装类的作用:

  • 将基本数据类型封装成对象的好处在于,可以在对象中定义更多的功能方法操作该数据。
  • 常用的操作之一:用于基本数据类型与字符串之间的转换。

基本数据类型对应的包装类:

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

示例:

    // 把字符串转换成int类型
    String str = "12";
    int a = Integer.parseInt(str);
    System.out.println(a);

    // 把数字转换成字符串
    System.out.println(Integer.toString(a));

    // 把整数转换成对应的进制形式
    System.out.println(Integer.toBinaryString(2));  // 转化为二进制
    System.out.println(Integer.toOctalString(10));  // 转化为八进制
    System.out.println(Integer.toHexString(10));    // 转化为十六进制

    // 可以把字符串当成对应的进制数据
    String data = "10";
    Integer.parseInt(data, 2);  // 把十进制的数据转化成二进制
    Integer.parseInt(data, 36);  // 最高能转化成36进制

示例:Integer 类缓冲数组。

Integer 类内部维护了一个缓冲数组,该缓冲数组存储的 -128 到 127 这些数据在一个数组中。如果需要获取的数据在这个范围之内,那么就直接从该缓冲区中获取。

   // 引用的数据类型
   Integer e = 129;
   Integer f = 129;
   System.out.println("a与b是同一个对象吗?"+(a==b));  // false
   // 若两者都在-127至128内,结果则为 true

2. 自动拆箱和自动装箱

  • 自动装箱:把基本数据类型转换为对应的包装类类型。
  • 自动拆箱:把包装类类型转换为对应的基本数据类型。

示例:

    Integer i = 100;  // 自动装箱
    i += 200;  // i += 200 是自动拆箱;i = i + 200 是自动装箱

    // 集合只能存储对象类型,而 1、2、3 是基本数据类型,若在 JDK1.5 之前会报错
    ArrayList list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    // JDK1.5 之前需要写成 list.add(new Integer(1));

    // 自动拆箱
    Integer c = new Integer(3);
    int b = c;
    // JDK1.5 之前需要写成 int b = c.intValue();

8、工具类的设计思想

  • 构造方法用 private 修饰(无法 new 新对象)。
  • 成员用 public static 修饰(直接通过类名访问静态方法)。

1. 时间日期类

1)Date 类

  • 计算机中时间原点:1970 年 1 月 1 日 00:00:00

  • Date 概述:Date 代表了一个特定时间(精确到毫秒)的对象。

  • Date 类构造方法:

方法名说明
public Date()当前时间
public Date(long time)时间原点+传入的时间长度(毫秒)
import java.util.Date;

public class Test {

    public static void main(String[] args) {
        Date d1 = new Date();
        System.out.println(d1);  // Tue Sep 21 00:29:28 CST 2021
        
        long date = 1000*60*60;
        Date d2 = new Date(date);
        System.out.println(d2);  // Thu Jan 01 09:00:00 CST 1970
    }
}

Date 常用方法:

方法名说明
public long getTime()从 1970年1月1日 00:00:00 到现在的毫秒值
public void setTime(long time)设置时间,给的是毫秒值
import java.util.Date;

public class Test{
    public static void main(String[] args) {
        //创建日期对象
        Date d = new Date();

        // public long getTime():获取的是日期对象从 1970年1月1日 00:00:00 到现在的毫秒值
        System.out.println(d.getTime());  // 1632155655406

        // public void setTime(long time):设置时间,给的是毫秒值
        long time = System.currentTimeMillis();
        d.setTime(time);
        System.out.println(d);  // Tue Sep 21 00:34:15 CST 2021
    }
}

2)SimpleDateFormat 类

SimpleDateFormat 是一个具体的类,用于以区域设置敏感的方式格式化和解析日期。

SimpleDateFormat 类的构造方法:

方法名说明
public SimpleDateFormat()构造一个 SimpleDateFormat,使用默认模式和日期格式
public SimpleDateFormat(String pattern)构造一个 SimpleDateFormat,使用给定的模式和默认的日期格式

SimpleDateFormat 类的常用方法:

方法名说明
public final String format(Date date)将日期对象格式化成日期/时间格式的字符串对象
public Date parse(String source)将日期/时间格式的字符串对象解析成日期对象

代码示例:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {

    public static void main(String[] args) throws ParseException {
        // 格式化:从 Date 到 String
        Date d = new Date();
        // SimpleDateFormat sdf = new SimpleDateFormat();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        String s = sdf.format(d);
        System.out.println(s);  // 2021年09月21日 00:41:33
        System.out.println("--------");

        // 从 String 到 Date
        String ss = "2048-08-09 11:11:11";
        // ParseException
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date dd = sdf2.parse(ss);
        System.out.println(dd);  // Sun Aug 09 11:11:11 CST 2048
    }
}

2. JDK8 日期时间类

JDK8 新增的日期时间类:

  • LocalDate:表示日期(年月日)
  • LocalTime:表示时间(时分秒)
  • LocalDateTime:表示时间+日期(年月日时分秒)

1)LocalDateTime 类

LocalDateTime 创建方法:

方法名说明
public static LocalDateTime now()获取当前系统时间
public static LocalDateTime of (年, 月 , 日, 时, 分, 秒)使用指定年月日和时分秒初始化一个 LocalDateTime 对象

代码示例:

import java.time.LocalDateTime;

public class Test {

    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);  // 2021-09-21T00:51:38.077

        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 11);
        System.out.println(localDateTime);  // 2020-11-11T11:11:11
    }
}

LocalDateTime 获取方法:

方法名说明
public int getYear()获取年
public int getMonthValue()获取月份(1-12)
public int getDayOfMonth()获取月份中的第几天(1-31)
public int getDayOfYear()获取一年中的第几天(1-366)
public DayOfWeek getDayOfWeek()获取星期
public int getMinute()获取分钟
public int getHour()获取小时

代码示例:

import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.Month;

public class Test {
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 11, 11, 20);

        // public int getYear():获取年
        int year = localDateTime.getYear();
        System.out.println("年为" +year);  // 2020

        // public int getMonthValue():获取月份(1-12)
        int month = localDateTime.getMonthValue();
        System.out.println("月份为" + month);  // 11

        Month month1 = localDateTime.getMonth();
        System.out.println(month1);  // NOVEMBER

        // public int getDayOfMonth():获取月份中的第几天(1-31)
        int day = localDateTime.getDayOfMonth();
        System.out.println("日期为" + day);  // 11

        // public int getDayOfYear():获取一年中的第几天(1-366)
        int dayOfYear = localDateTime.getDayOfYear();
        System.out.println("这是一年中的第" + dayOfYear + "天");  // 316

        // public DayOfWeek getDayOfWeek():获取星期
        DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();
        System.out.println("星期为" + dayOfWeek);  // WEDNESDAY

        // public int getMinute():获取分钟
        int minute = localDateTime.getMinute();
        System.out.println("分钟为" + minute);  // 11
        
        // public int getHour():获取小时
        int hour = localDateTime.getHour();
        System.out.println("小时为" + hour);  // 11
    }
}

LocalDateTime 转换方法:

方法名说明
public LocalDate toLocalDate()转换成为一个 LocalDate 对象
public LocalTime toLocalTime()转换成为一个 LocalTime 对象

代码示例:

import java.time.*;

public class Test {
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.of(2020, 12, 12, 8, 10, 12);

        // public LocalDate toLocalDate():转换成为一个 LocalDate 对象
        LocalDate localDate = localDateTime.toLocalDate();
        System.out.println(localDate);  // 2020-12-12

        // public LocalTime toLocalTime():转换成为一个 LocalTime 对象
        LocalTime localTime = localDateTime.toLocalTime();
        System.out.println(localTime);  // 08:10:12

    }
}

LocalDateTime 格式化和解析:

方法名说明
public String format (指定格式)把一个 LocalDateTime 格式化成为一个字符串
public LocalDateTime parse (准备解析的字符串, 解析格式)把一个日期字符串解析成为一个 LocalDateTime 对象
public static DateTimeFormatter ofPattern(String pattern)使用指定的日期模板获取一个日期格式化器 DateTimeFormatter 对象

代码示例:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Test {
    public static void main(String[] args) {
        method1();
        method2();
    }

    private static void method2() {
        //public static LocalDateTime parse (准备解析的字符串, 解析格式):把一个日期字符串解析成为一个LocalDateTime对象
        String s = "2020年11月12日 13:14:15";
        DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        LocalDateTime parse = LocalDateTime.parse(s, pattern);
        System.out.println(parse);  // 2020-11-12T13:14:15
    }

    private static void method1() {
        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 12, 13, 14, 15);
        System.out.println(localDateTime);  // 2020-11-12T13:14:15
        //public String format (指定格式):把一个LocalDateTime格式化成为一个字符串
        DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
        String s = localDateTime.format(pattern);
        System.out.println(s);  // 2020年11月12日 13:14:15
    }
}

LocalDateTime 增加时间的方法:

方法名说明
public LocalDateTime plusYears (long years)增加年
public LocalDateTime plusMonths(long months)增加月
public LocalDateTime plusDays(long days)增加日
public LocalDateTime plusHours(long hours)增加时
public LocalDateTime plusMinutes(long minutes)增加分
public LocalDateTime plusSeconds(long seconds)增加秒
public LocalDateTime plusWeeks(long weeks)增加周

代码示例:

import java.time.LocalDateTime;

public class Test {
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 13, 14, 15);
        
        // public LocalDateTime plusYears (long years):添加或者减去年
        LocalDateTime newLocalDateTime = localDateTime.plusYears(1);
        System.out.println(newLocalDateTime);  // 2021-11-11T13:14:15

        LocalDateTime newLocalDateTime2 = localDateTime.plusYears(-1);
        System.out.println(newLocalDateTime2);  // 2019-11-11T13:14:15
    }
}

LocalDateTime 减少时间的方法:

方法名说明
public LocalDateTime minusYears (long years)减去年
public LocalDateTime minusMonths(long months)减去月
public LocalDateTime minusDays(long days)减去日
public LocalDateTime minusHours(long hours)减去时
public LocalDateTime minusMinutes(long minutes)减去分
public LocalDateTime minusSeconds(long seconds)减去秒
public LocalDateTime minusWeeks(long weeks)减去周

代码示例:

import java.time.LocalDateTime;

public class Test {
    public static void main(String[] args) {
        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 13, 14, 15);

        // public LocalDateTime minusYears (long years):减去年
        LocalDateTime newLocalDateTime = localDateTime.minusYears(1);
        System.out.println(newLocalDateTime);  // 2019-11-11T13:14:15

        LocalDateTime newLocalDateTime2 = localDateTime.minusYears(-1);
        System.out.println(newLocalDateTime2);  // 2021-11-11T13:14:15
    }
}

LocalDateTime 修改方法:

方法名说明
public LocalDateTime withYear(int year)直接修改年
public LocalDateTime withMonth(int month)直接修改月
public LocalDateTime withDayOfMonth(int dayofmonth)直接修改日期(一个月中的第几天)
public LocalDateTime withDayOfYear(int dayOfYear)直接修改日期(一年中的第几天)
public LocalDateTime withHour(int hour)直接修改小时
public LocalDateTime withMinute(int minute)直接修改分钟
public LocalDateTime withSecond(int second)直接修改秒

代码示例:

import java.time.LocalDateTime;

public class Test {
    public static void main(String[] args) {

        LocalDateTime localDateTime = LocalDateTime.of(2020, 11, 11, 13, 14, 15);
        // 修改年
        LocalDateTime newLocalDateTime = localDateTime.withYear(2048);  // 2048-11-11T13:14:15
        System.out.println(newLocalDateTime);
        // 修改月
        LocalDateTime newLocalDateTime2 = localDateTime.withMonth(2);  // 2020-02-11T13:14:15
        System.out.println(newLocalDateTime2);

    }
}

3. Period 类

方法名说明
public static Period between(开始时间, 结束时间)计算两个“时间"的间隔
public int getYears()获得这段时间的年数
public int getMonths()获得此期间的总月数
public int getDays()获得此期间的天数
public long toTotalMonths()获取此期间的总月数

代码示例:

import java.time.LocalDate;
import java.time.Period;

public class Test {
    public static void main(String[] args) {

        LocalDate localDate1 = LocalDate.of(2020, 1, 1);
        LocalDate localDate2 = LocalDate.of(2048, 12, 12);
        
        // public static Period between(开始时间, 结束时间):计算两个"时间"的间隔
        Period period = Period.between(localDate1, localDate2);
        System.out.println(period);  // P28Y11M11D 表示相差28年11个月11天

        //public int getYears():获得这段时间的年数
        System.out.println(period.getYears());  // 28
        //public int getMonths():获得此期间的月数
        System.out.println(period.getMonths());  // 11
        //public int getDays():获得此期间的天数
        System.out.println(period.getDays());  // 11
        //public long toTotalMonths():获取此期间的总月数
        System.out.println(period.toTotalMonths());  // 347

    }
}

4. Duration 类

方法名说明
public static Durationbetween(开始时间, 结束时间)计算两个“时间"的间隔
public long toSeconds()获得此时间间隔的秒
public int toMillis()获得此时间间隔的毫秒
public int toNanos()获得此时间间隔的纳秒

代码示例:

import java.time.Duration;
import java.time.LocalDateTime;

public class Test {
    public static void main(String[] args) {

        LocalDateTime localDateTime1 = LocalDateTime.of(2020, 1, 1, 13, 14, 15);
        LocalDateTime localDateTime2 = LocalDateTime.of(2020, 1, 2, 11, 12, 13);

        // public static Duration between(开始时间,结束时间)  计算两个“时间”的间隔
        Duration duration = Duration.between(localDateTime1, localDateTime2);
        System.out.println(duration);  // PT21H57M58S

        // public int toMillis():获得此时间间隔的毫秒
        System.out.println(duration.toMillis());  // 79078000
        // public int toNanos():获得此时间间隔的纳秒
        System.out.println(duration.toNanos());  // 79078000000000

    }
}
~~

十三、Java正则表达式

在了解正则表达式之前,我们先看几个非常常见的问题:

  • 如何判断字符串是否是有效的电话号码?例如:010-1234567,123ABC456,13510001000等;

  • 如何判断字符串是否是有效的电子邮件地址?例如:test@example.com,test#example等;

  • 如何判断字符串是否是有效的时间?例如:12:34,09:60,99:99等。

正则表达式(regex):通常被用来检索、替换那些符合某个模式(规则)的文本,Java标准库java.util.regex内建了正则表达式引擎。

格式:

字符串.matches(匹配规则);  // 并且会返回 boolean

 例如,判断手机号,我们用正则表达式\d{11}:

boolean isValidMobileNumber(String s) {
    return s.matches("\\d{11}");
}

使用正则表达式的好处有哪些?一个正则表达式就是一个描述规则的字符串,所以,只需要编写正确的规则,我们就可以让正则表达式引擎去判断目标字符串是否符合规则。

正则表达式是一套标准,它可以用于任何语言。Java标准库的java.util.regex包内置了正则表达式引擎,在Java程序中使用正则表达式非常简单。

举个例子:要判断用户输入的年份是否是20##年,我们先写出规则如下:

一共有4个字符,分别是:2,0,0~9任意数字,0~9任意数字。

对应的正则表达式就是:20\d\d,其中\d表示任意一个数字。

把正则表达式转换为Java字符串就变成了20\\d\\d,注意Java字符串用\\表示\。

最后,用正则表达式匹配一个字符串的代码如下:

public class Main {
    public static void main(String[] args) {
        String regex = "20\\d\\d";
        System.out.println("2019".matches(regex)); // true
        System.out.println("2100".matches(regex)); // false
    }
}

1、元字符

1. 表示字符

元字符匹配规则说明
.匹配任何字符(与行结束符可能匹配也可能不匹配)
\d匹配数字,即 [0-9]
\D匹配非数字,即 [^0-9]
\s匹配空白字符,即 [ \t\n\x0B\f\r]
\S匹配非空白字符即 [^\s]
\w匹配单词字符即 [a-zA-Z_0-9]
\W匹配非单词字符即 [^\w]

注意:任何预定义字符没有加上数量词之前只能匹配一个字符。

2. 表示数量

元字符匹配规则说明
?一次或一次也没有
*零次或多次
+一次或多次
{n}恰好 n 次
{n,}至少 n 次
{n,m}至少 n 次,但不超过 m 次

示例:

"123".matches("\\d{1,4}");  // true

3. 表示范围

元字符匹配规则说明
[abc]a、b 或 c
[^abc]除了 a、b、c 外的任何字符(\r、\n 为 true)
[a-zA-Z]a-z 或 A-Z
[a-d[m-p]](并集)a-d 或者 m-p
[a-z&&[def]](交集)d、e 或 f

注意:不管范围词多长,没有加上数量词之前只能匹配一个字符。

4. 表示边界

元字符匹配规则说明
^匹配开头
$匹配结尾
\b表示单词的开始或者结束的边界,不匹配任何字符(包括不匹配空字符)

示例:

"hello,world".matches("hello\\b,world");
"xxx".matches("[^ts]he");  // 匹配he,但要排除the和she

案例:匹配 QQ 号

要求:

  1. 必须 5~15 位数字
  2. 不能以 0 开头
  3. 必须都是数字
public static void main(String[] args) {
    Pattern pattern = Pattern.compile("^[^0]\\d{4,14}$");
    Matcher m1 = pattern.matcher("11111");
    Matcher m2 = pattern.matcher("012345678912345");
    Matcher m3 = pattern.matcher("123456789012345");
    if (m1.find()){
        System.out.println(m1.group());
    }
    if (m2.find()){  //fase
        System.out.println(m2.group());
    }
    if (m3.find()){
        System.out.println(m3.group());
    }
}

5. 前/后向匹配

元字符匹配规则说明
(?=pattern)正向肯定匹配。在任何匹配 pattern 的字符串开始处匹配查找字符串。
(?!pattern)正向否定匹配。在任何不匹配 pattern 的字符串开始处匹配查找字符串。
(?<=pattern)反向肯定匹配。与正向肯定匹配类似,只是方向相反。
(?<!pattern)反向否定匹配。与正向否定匹配类似,只是方向相反。
"Windows(?=95|98|NT|2000)"  // 能够匹配 Windows2000 中的 Windows,但不能匹配 Windows3.1 中的 Windows

"Windows(?!95|98|NT|2000)"  // 不能匹配 Windows2000 中的 Windows,但能匹配 Windows3.1 中的 Windows

"(?<=95|98|NT|2000)Windows"  // 能够匹配 2000Windows 中的 Windows,但不能匹配 3.1Windows 中的 Windows

"(?<!95|98|NT|2000)Windows"  // 不能匹配 2000Windows 中的 Windows,但能匹配 3.1Windows 中的 Windows

2、应用

1. 匹配:matches()

案例:编写一个正则表达式匹配手机号。

public static void matchPhoneNum(String phone) {
    // 只能以 1 开头,第二位为 3、5、8,总长度 11 位
    phone.matches("1[358]\\d{9}")? "合法手机号": "非法手机号");
}

案例:匹配固话。

public static void matchTelNum(String tel) {
     // 区号:首位是 0,长度是 3-5
     // 主机号:首位不能是 0,长度是 7-8
     System.out.println(tel.matches("0\\d{2,4}-[1-9]\\d{6,7}")? "合法电话号码": "非法电话号码");
}

2. 查找并返回:find()、group()

使用正则查找的步骤:

  1. 指定为字符串的正则表达式必须首先编译为此类的实例。
  2. 将得到的正则对象匹配任意的字符串用于创建 Matcher 对象。
  3. 依照正则表达式,该对象可以与任意字符序列匹配,执行匹配所涉及的所有状态都驻留在元字符中(即所有信息都保留在元字符中),所以多个元字符可以共享同一模式。

经典的调用顺序是:

Pattern p = Pattern.compile("正则");  // Pattern:正则对象
Matcher m = p.matcher("aaaaaab");  // Matcher:元字符对象

元字符要用到的方法:

  1. find():通知元字符去匹配字符串,如果查找到符合规则的字符串则返回 true,反之返回 false。
  2. group():获取符合规则的字符串。

注意:使用 group()、start()、end() 方法时,必须先调用 find() 方法让匹配去查找符合规则的字符串,否则报错。

案例:找出 3 个字母组成的单词。

String str = "da jia de jia qi wan bi liao, hai kai xin ma";

// 1. 编写需求的正则表达式。
String reg = "\\b[a-zA-Z]{3}\\b";
// 2. 把字符串的正则编译成Pattern对象。
Pattern p = Pattern.compile(reg);
// 3. 使用正则对象匹配字符串用于创建一个Matcher对象。
Matcher m = p.matcher(str);
// System.out.println("有符合规则的字符串吗?"+m.find());  // 返回true
// System.out.println("获取结果:"+m.group());  // 只返回一个结果
while(m.find()){
    System.out.println(m.group());  // 通过循环获取所有结果
}

案例:匹配邮箱。

public static void main(String[] args) {
    String str = "有事没事请联系:243214@213.com  有事没事请联系:vgvrge@126.net "
        + "有事没事请联系:vgvrge@123.com.cn"
        + "有事没事请联系:dv3_2343@asA.cn";

    String reg = "[a-zA-Z1-9]\\w{5,17}@[a-zA-Z0-9]{2,}(\\.(com|cn|net)){1,2}";
    // (com|cn|net) 是或者的写法
    // (\\.(com|cn|net)){1,2} 表示 \\.(com|cn|net) 这个整体出现 1-2 次

    Pattern p = Pattern.compile(reg);
    Matcher m = p.matcher(str);
    while(m.find()){
        System.out.println(m.group());
    }
}

3. 切割:split()

案例:按照空格切割。

String str = "   不断学习  为了     将来";
String[] arr = str.split(" +");  // 至少出现一次空格

案例:根据重叠次进行切割。

如果正则表达式的内容需要被复用,那么需要对正则的内容进行分组,分组的目的是为了提高正则的复用性,组号从 1 开始。

String str = "大家家明天天玩得得得得开心";
String[] arr = str.split("(.)\\1+");  // 大明玩开心

4. 替换:replaceAll(String regex, String replacement)

  • replaceAll(String regex, String replacement):替换全部匹配到的子字符串
  • replaceFirst(String regex, String replacement)::替换第一个匹配到的字符串

案例:论坛防止打广告。

String str = "如有需要请联系我:13565678964";
String reg = "1[358]\\d{9}";
str.replaceAll(reg, "******");

案例:把重叠词替换成单个单词。

String str = "我我要要要做做做项项项项目目";
// 如果需要在 replaceAll 方法正则的外部引用组的内容,那么是使用"$组号"。
str.replaceAll("(.)\\1+", "$1");

**案例:把“我...我我...喜欢欢欢...编编程程程...”转换成“我喜欢编程”。

String raw = "我...我我...喜欢欢欢...编编程程程...";
String tmp = raw.replaceAll("\\.", "");
String result = tmp.replaceAll("(.)\\1+", "$1");
System.out.println(result);  // 我喜欢编程

案例:反转字母字符。

输入任意一个字符串,取出所有字母(顺序不能改变),把大写换成小写,小写换成大写。

String s = "shi1r4FIFI23sdf2VIH";
// 提取所有字母
String letters = s.replaceAll("[^a-zA-Z]+", "");
StringBuilder builder = new StringBuilder();
for (char c:letters.toCharArray()) {
    if (c<='z'&&c>='a') {
        builder.append((c+"").toUpperCase());
    } else {
        builder.append((c+"").toLowerCase());
    }
}
System.out.println(builder.toString());

5. 匹配开头:lookingAt()

lookingAt() 对前面的字符串进行匹配,只有匹配到的字符串在最前面时才返回 true。

Pattern p = Pattern.compile("\\d+");

Matcher m = p.matcher("22bb23");
m.lookingAt();  // 返回true,因为\d能匹配开头的22

Matcher m2 = p.matcher("bb2223");
m2.lookingAt();  // 返回false,因为\d不能匹配开头的bb

6. 获取匹配索引、分组

获取匹配索引:

Matcher.start():返回匹配到的子字符串在字符串中的索引位置。
Matcher.end():返回匹配到的子字符串的最后一个字符的后一个字符在字符串中的索引位置。

分组:

Matcher.start()、Matcher.end()、Matcher.group() 均有重载方法,它们是 Matcher.start(int i)、Matcher.end(int i)、Matcher.group(int i),专用于分组操作。

Matcher 类还有一个 groupCount() 用于返回有多少组。

注意:

只有当匹配成功时,才可以使用 Matcher.start()、Matcher.end()、Matcher.group() 三个方法,否则会抛出异常,也就是当 matches()、lookingAt()、find() 其中任意一个方法返回 true 时,才可以使用。

示例:

Pattern p = Pattern.comple("([a-z]+)(\\d+)");
Matcher m = p.matcher("aaa2223bb");

m.find();  // 匹配aaa2223
m.groupCount();  // 返回2,因为有两组
m.group(1);  // aaa
m.group(2);  // 2223
m.start(1);  // 返回0,即第一组匹配到的子字符串在字符串中的索引号
m.start(2);  // 返回3
m.end(1);  // 返回3,即第一组匹配到的子字符串的最后一个字符的后一个字符在字符串中的索引号
m.end(2);  // 返回7

示例:将所有的数字提取出来。

Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher("我的QQ是:123456,我的电话是:654321,我的邮箱是:13312@163.com");

while(m.find()) {
    System.out.println(m.group());
    System.out.print("start:"+m.start());
    System.out.println(" end:"+m.end());
}

执行结果:

123456
start:6 end:12
654321
start:19 end:25
13312
start:32 end:37
163
start:38 end:41

7. 忽略大小写

// 方法一:直接用正则,(?!)表示整体忽略大小写。
// "^d(?!)oc"表示“oc”忽略大小写,"^d((?!)o)c"表示只有“o”忽略大小写
String regex1 = "^(?i)doc$";
System.out.println("doc".matches(regex1));  // true
System.out.println("DoC".matches(regex1));  // true

// 方法二:采用Pattern编译忽略大小写
String regex = "^doc$";
String s = "DoC";
Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
System.out.println(p.matcher(s).find());  // true

十四、Java异常

1、异常简介

异常,就是程序出现了不正常的情况。

如果程序出现了问题,我们没有做任何处理,那么最终 JVM 会做默认的处理,其处理方式有如下两个步骤:

  1. 把异常的名称,错误原因及异常出现的位置等信息输出在了控制台。
  2. 程序停止执行。

控制台在打印异常信息时,会打印异常类名、异常出现的原因、异常出现的位置等信息。我们在 debug 时,可以根据提示,找到异常出现的位置,分析原因,修改异常代码。

异常体系:

除了 RuntimeException 和其子类,以及 error 和其子类,其他所有异常都是 checkedException 。

  • Error:是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM(Java 虚拟机)出现的问题。这些错误是不可检查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况,比如 OutOfMemoryError 和 StackOverflowError 异常的出现会有几种情况。
  • Exception:是需要通过代码处理(声明或捕获)的。

如何区分 Error 与 Exception:

  • 如果程序出现了不正常的信息的类名是以 Error 结尾的(除了一个 ThreadDeath),那么肯定是一个错误。
  • 如果是以 Exception 结尾的,那么肯定是一个异常。

编译时异常和运行时异常:

  • 编译时异常(CheckedException)

    • 都是 Exception 类及其子类。
    • 如果一个方法内部抛出了一个编译时异常对象,那么方法上一定要声明,调用者也必须处理。
    • 除了运行时异常 就是编译时异常。
  • 运行时异常(RuntimeException)

    • 都是 RuntimeException 类及其子类。
    • 如果一个方法内部抛出了一个运行时异常对象,那么方法上可以声明也可以不声明,调用者可以处理也可以不处理。
  • 问:为什么 JAVA 编译器会如此严格要求编译时异常,对运行时异常如此宽松?
    • 因为运行时异常都是可以通过良好的编程习惯去避免(如事先进行条件判断语句)。
    • 而编译时异常很多时候是没法通过编程去避免的(如磁盘不够导致 I/O 出错等)。

常见异常:

RuntimeException:

序号异常名称异常描述
1ArrayindexOutOfBoundsException数组越界异常
2NullPointerException空指针异常
3IllegalArqumentException非法参数异常
4NegativeArraySizeException数组长度为负异常
5llleaalStateException非法状态异常
6ClassCastException类型转换异常

UncheckedException:

序号异常名称异常描述
1NoSuchFieldException表示该类没有指定名称抛出来的异常
2NoSuchMethodException表示该类没有指定方法抛出来的异常
3IllegalAccessException不允许访问某个类的异常
4ClassNotFoundException类没有找到执出异常

2、throws 方式处理异常

格式:

public void 方法() throws 异常类名 {
    
}
  • 编译时异常必须要显式进行处理,有两种处理方案:try … catch … 或者 throws。如果采用 throws 这种方案,那么要在方法上进行显示声明,将来谁调用这个方法谁就要处理。

  • 运行时异常因为在运行时才会发生,所以在方法后面可以不写,出现运行时异常时则默认交给 JVM 处理。

示例:

public class ExceptionDemo {
    public static void main(String[] args) throws ParseException{
        System.out.println("开始");
//        method();
          method2();
        System.out.println("结束");
    }

    // 编译时异常(日期格式解析错误)
    public static void method2() throws ParseException {
        String s = "2048-08-09";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date d = sdf.parse(s);
        System.out.println(d);
    }

    // 运行时异常(索引越界)
    public static void method() throws ArrayIndexOutOfBoundsException {
        int[] arr = {1, 2, 3};
        System.out.println(arr[3]);
    }
}

3、try … catch 方式处理异常

格式:

try {
	可能出现异常的代码;
} catch(异常类名 变量名) {
	异常的处理代码;
}

执行流程:

  1. 程序从 try 里面的代码开始执行。
  2. 出现异常,就会跳转到对应的 catch 里面去执行。
  3. 执行完毕之后,程序还可以继续往下执行。

示例代码:

public class ExceptionDemo {
    public static void main(String[] args) {
        System.out.println("开始");
        method();
        System.out.println("结束");
    }

    public static void method() {
        try {
            int[] arr = {1, 2, 3};
            System.out.println(arr[3]);
            System.out.println("这里能够访问到吗");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("你访问的数组索引不存在,请回去修改为正确的索引");
        }
    }
}

注意:

  1. 如果 try 中没有遇到问题,怎么执行?

    • 会把 try 中所有的代码全部执行完毕,不会执行 catch 里面的代码。
  2. 如果 try 中遇到了问题,那么 try 下面的代码还会执行吗?

    • 那么会直接跳转到对应的 catch 语句中,try 中接下来的代码就不会再执行了。
    • 当 catch 里面的语句全部执行完毕,表示整个体系全部执行完全,继续执行下面的代码。
  3. 如果出现的问题没有被捕获,那么程序如何运行?

    • 那么 try...catch 就相当于没有写,那么也就是自己没有处理,默认交给虚拟机处理。
  4. 同时有可能出现多个异常怎么处理?

    • 出现多个异常,那么就写多个 catch 就可以了。
    • 注意点:如果多个异常之间存在子父类关系,那么父类一定要写在下面((若从子到父,后面的异常即是废话)。

4、finally

finally 保证了 try...catch 代码块中即使有异常,也能执行 finally 代码块。

public static void main(String[] args) {
    try{
        int i = 1/0;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;  // 即使在catch又有异常,finally代码块也能执行
    } finally {
        System.out.println("无finally会执行吗");
    }
}

总结:

  1. 与 finally 相对应的 try 语句得到执行的情况下,finally 才有可能执行。
  2. finally 执行前,若程序或线程终止,则 finally 不会执行。

5、throw 抛出异常

格式:throw new 异常();

注意

  1. 如果一个方法的内部抛出了一个编译时异常对象,那么必须要在此方法上声明抛出。
  2. 如果调用了一个声明抛出异常的方法,那么调用者必须要处理。
  3. 如果一个方法内部抛出了一个异常对象,那么 throw 语句后面的代码都不会执行了(一个方法遇到 throw 语句,这个方法也会马上停止执行)。
  4. 在一种情况(条件)下,只能抛出一个异常对象。

throws 和 throw 的区别:

throwsthrow
用在方法声明上用在方法内部
后跟异常类型后跟异常对象
后可声明多个异常类型的异常后只能有一个异常对象

示例代码:

public class ExceptionDemo8 {
    public static void main(String[] args) {
        //int [] arr = {1,2,3,4,5};
        int [] arr = null;
        printArr(arr);  // 此行会接收到一个异常,因此在此还需要自己处理一下异常
    }

    private static void printArr(int[] arr) {
        if(arr == null){
            // 调用者知道成功打印了吗?
            System.out.println("参数不能为null");
			// 当参数为 null 的时候,手动创建了一个异常对象,抛给调用者
            throw new NullPointerException();  
        }else{
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
        }
    }

}

6、Throwable 成员方法

常用方法:

方法名说明
public String getMessage()返回此 throwable 的详细消息字符串
public String toString()返回此可抛出的简短描述
public void printStackTrace()把异常的错误信息输出在控制台

示例代码:

public class ExceptionDemo {
	
    public static void main(String[] args) {
        System.out.println("开始");
        method();
        System.out.println("结束");
    }

    public static void method() {
        try {
            int[] arr = {1, 2, 3};
            System.out.println(arr[3]);  // new ArrayIndexOutOfBoundsException();
            System.out.println("这里能够访问到吗");
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();

            // public String getMessage():返回此 throwable 的详细消息字符串
            System.out.println(e.getMessage());  //Index 3 out of bounds for length 3

            // public String toString():返回此可抛出的简短描述
            System.out.println(e.toString());  // java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3

            // public void printStackTrace():把异常的错误信息输出在控制台
            e.printStackTrace();  // 打印信息如下:
//            java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
//            at com.itheima_02.ExceptionDemo02.method(ExceptionDemo02.java:18)
//            at com.itheima_02.ExceptionDemo02.main(ExceptionDemo02.java:11)
        }
    }
}

7、自定义异常

当 Java 中提供的异常不能满足我们的需求时,我们就可以自定义异常。

实现步骤:

  1. 定义异常类
  2. 写继承关系
  3. 提供空参构造
  4. 提供带参构造

代码实现:

  • 异常类:
public class AgeOutOfBoundsException extends RuntimeException {
    public AgeOutOfBoundsException() {
    }

    public AgeOutOfBoundsException(String message) {
        super(message);
    }
}
  • 学生类:
public class Student {
	
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if(age >= 18 && age <= 25){
            this.age = age;
        }else{
            //如果Java中提供的异常不能满足我们的需求,我们可以使用自定义的异常
            throw new AgeOutOfBoundsException("年龄超出了范围");
        }
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • 测试类:
public class ExceptionDemo12 {
    public static void main(String[] args) {
        // 键盘录入学生的姓名和年龄,其中年龄为 18 - 25岁
        // 超出这个范围是异常数据不能赋值,需要重新录入,一直录到正确为止。

        Student s = new Student();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入姓名:");
        String name = sc.nextLine();
        s.setName(name);
       while(true){
           System.out.println("请输入年龄:");
           String ageStr = sc.nextLine();
           try {
               int age = Integer.parseInt(ageStr);
               s.setAge(age);
               break;
           } catch (NumberFormatException e) {
               System.out.println("输入有误,请输入一个整数");
               continue;
           } catch (AgeOutOfBoundsException e) {
               System.out.println(e.toString());
               System.out.println("输入有误,请输入一个符合范围的年龄");
               continue;
           }
           /*if(age >= 18 && age <=25){
               s.setAge(age);
               break;
           }else{
               System.out.println("输入有误,请输入符合要求的年龄");
               continue;
           }*/
       }
        System.out.println(s);
    }
}

十五、Java泛型与集合

1、集合简介

数组和集合的区别:

  • 相同点:

    • 都是容器,可以存储多个数据。
  • 不同点:

    • 存储长度:数组的长度是不可变的;集合的长度是可变的。

    • 存储类型:数组可以存基本数据类型和引用数据类型;集合只能存引用数据类型,而如果要存基本数据类型,则需要存对应的包装类。

集合体系结构:

集合实现类特征:

2、泛型

1. 泛型概述

泛型定义: 

  • 泛型是 JDK5 中引入的特性,它提供了编译时的类型安全检测机制。

  • 泛型其实就是一种参数化的集合,它限制了你添加进集合的类型。泛型的设计之处就是希望对象或方法具有最广泛的表达能力。

  • 多态也可以看作是泛型的机制。一个类继承了父类,那么就能通过它的父类找到对应的子类,但是不能通过其他类来找到具体要找的这个类。

  • 泛型就是允许类、方法、接口对类型进行抽象,在允许向目标中传递多种数据类型的同时限定数据类型,确保数据类型的唯一性。这在集合类型中是非常常见的。

泛型的定义格式:

  • <类型>:指定一种类型的格式。尖括号里面可以任意书写,一般只写一个字母。例如:<E>、<T>
  • <类型1, 类型2, …>:指定多种类型的格式,多种类型之间用逗号隔开。例如:<E,T>、<K,V>
// 示例:<String> 表示该容器只能存储字符串类型的数据
ArrayList<String> list = new ArrayList<>();

// 其余写法
ArrayList<String> list = new ArrayList<String>();
// 以下两种写法主要是为了新老版本的兼容性问题
ArrayList list = new ArrayList<String>();
ArrayList<String> list = new ArrayList();

注意:

  1. 泛型没有多态的概念,左右两边的数据类型必须要一致,或者只是写一边的泛型类型。
  2. 在泛型中不能使用基本数据类型。如果需要使用基本数据类型,那么就要使用基本数据类型所对应的包装类型。

示例:不加泛型,则默认是 Object 类型。

示例:加泛型。 

泛型的好处:

  1. 解决获取数据元素时,需要注意强制类型转换的问题。
  2. 泛型提供了编译期的类型安全,确保只能把正确类型的对象放入集合中,避免了在运行时出现 ClassCastException。
  3. 把方法写成泛型 <T>,这样就不用针对不同的数据类型(例如 int、double、float)分别写方法,只要写一个方法就可以了,提高了代码的复用性。

自定义泛型:

  • 自定义泛类,就是一个数据类型的占位符或者是一个数据类型的变量。
  • 自定义泛型只要符合标识符的命名规则即可。一般习惯使用大写字母 T(type)或 E(element)。

2. 泛型类

定义格式:

修饰符 class 类名<类型> {}

泛型类的注意事项:

  1. 在类上自定义泛型的具体数据类型,是在创建实例对象的时候确定的。
  2. 如果在类上已经声明了自定义泛型,那么在创建实例对象的时候,如果没有指定泛型的具体数据类型,则默认为 Object 类型。
  3. 在类上自定义泛型不能作用于静态方法。如果静态方法需要使用自定义泛型,只能在方法上自定义泛型。

示例代码:

  • 泛型类:
public class Generic<T> {
    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        Generic<String> g1 = new Generic<String>();
        g1.setT("杨幂");
        System.out.println(g1.getT());

        Generic<Integer> g2 = new Generic<Integer>();
        g2.setT(30);
        System.out.println(g2.getT());

        Generic<Boolean> g3 = new Generic<Boolean>();
        g3.setT(true);
        System.out.println(g3.getT());
    }
}

3. 泛型方法

定义格式:

修饰符 <类型> 返回值类型 方法名(类型 变量名) {}

注意:在方法上自定义泛型,这个自定义泛型的具体数据类型是在调用该方法的时候传入实参确定的。

示例:

  • 带有泛型方法的类:
public class Generic {
    public <T> void show(T t) {
        System.out.println(t);
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        Generic g = new Generic();
        g.show("小丸子");
        g.show(30);
        g.show(true);
        g.show(12.34);
    }
}

4. 泛型接口

定义格式:

修饰符 interface 接口名<类型> {}

泛型接口的注意事项:

  1. 接口上自定义的泛型的具体数据类型,是在实现一个接口的时候指定的。
  2. 在接口上自定义的泛型如果在实现接口的时候没有指定具体的数据类型,那么默认为 Object 类型。

示例代码:

  • 泛型接口:
public interface Generic<T> {
    void show(T t);
}
  • 泛型接口实现类 方式一:定义实现类时和接口相同泛型,在创建实现类对象时再明确泛型的具体类型。
public class GenericImpl1<T> implements Generic<T> {
    @Override
    public void show(T t) {
        System.out.println(t);
    }
}
  • 泛型接口实现类 方式二:定义实现类时直接明确泛型的具体类型。
public class GenericImpl2 implements Generic<Integer> {
    @Override
    public void show(Integer t) {
        System.out.println(t);
    }
}
  • 测试类:
public class GenericDemo {
    public static void main(String[] args) {
        GenericImpl1<String> g1 = new GenericImpl<String>();
        g1.show("林青霞");
        GenericImpl1<Integer> g2 = new GenericImpl<Integer>();
        g2.show(30);

        GenericImpl2 g3 = new GenericImpl2();
        g3.show(10);
    }
}

5. 类型通配符

类型通配符:<?>

  • ArrayList<?>:表示元素类型未知的 ArrayList,它的元素可以匹配任何的类型。
  • 但是并不能把元素添加到 ArrayList 中了,获取出来的也是父类类型。

类型通配符上限:<? extends 类型>

  • ArrayListList <? extends Number>:它表示的类型是 Number 或者其子类型。

类型通配符下限:<? super 类型>

  • ArrayListList <? super Number>:它表示的类型是 Number 或者其父类型。

泛型通配符的使用:

public class GenericDemo4 {
    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<Number> list3 = new ArrayList<>();
        ArrayList<Object> list4 = new ArrayList<>();

        method(list1);
        method(list2);
        method(list3);
        method(list4);

        getElement1(list1);
        getElement1(list2);  // 报错
        getElement1(list3);
        getElement1(list4);  // 报错

        getElement2(list1);  // 报错
        getElement2(list2);  // 报错
        getElement2(list3);
        getElement2(list4);
    }

    // 泛型通配符: 此时的泛型可以是任意类型
    public static void method(ArrayList<?> list){}
    // 泛型的上限: 此时的泛型必须是 Number 类型或者 Number 类型的子类
    public static void getElement1(ArrayList<? extends Number> list){}
    // 泛型的下限: 此时的泛型必须是 Number 类型或者 Number 类型的父类
    public static void getElement2(ArrayList<? super Number> list){}

}

3、Collection

1. Collection 概述

  • Collection 是单列集合的顶层接口,它表示一组对象,这些对象也称为 Collection 的元素。
  • JDK 不提供此接口的任何直接实现,它提供更具体的子接口(如 Set 和 List)实现。

2. Collection 常用方法

方法名说明
boolean add(E e)添加元素
boolean addAll(Collection<E> c)将另一个集合(c)的元素全部添加到本集合中
boolean remove(Object o)从集合中移除指定的元素
boolean removeIf(Predicate<? super E> filter)根据条件进行移除
void clear()清空集合中的元素
boolean contains(Object o)判断集合中是否存在指定的元素
boolean containsAll(Collection<E> c)判断一个集合是否包含另一个集合(c)的所有元素(与元素顺序无关,与元素内的元素顺序有关)
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中元素的个数
Object[] toArray()把集合中的元素全部存储到一个 Object 的数组中返回
void forEach()实现集合类的遍历

toArray() 示例:

public static void main(String[] args) {

     Collection c = new ArrayList();
     c.add(new Person(110, "狗娃"));
     c.add(new Person(111, "狗剩"));

     // 从Object数组中取出的元素只能使用Object类型声明变量接收
     // 如果需要其他的类型则需要先进行强制类型转换
     Object[] arr = c.toArray();
     // 需求:把编号是110的人的信息输出
     for(int i=0; i<arr.length; i++){  // 遍历数组
         Person p = (Person) arr[i];
         if(p.id==110){
              System.out.println(p);
         }
     }
}

3. Collection 遍历

1)迭代器

迭代器介绍:

  • Iterator 是 Collection 集合的超级接口。
  • 迭代器是 Collection 集合的专用遍历方式。
  • Iterator<E> iterator(): 返回此集合中元素的迭代器,通过集合对象的 iterator() 方法得到。

Iterator 中的常用方法:

  • boolean hasNext(): 判断当前位置是否有元素可以被取出。
  • <E> next():获取当前位置的元素,并将迭代器对象移向下一个索引位置。
  • void remove(): 删除迭代器对象当前指向的元素。

示例:Collection 集合的遍历。

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Collection<String> c = new ArrayList<>();

        // 添加元素
        c.add("hello");
        c.add("world");
        c.add("java");
        c.add("javaee");

        // Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
        Iterator<String> it = c.iterator();  // 多态:返回Iterator的实现类对象

        //用 while 循环改进元素的判断和获取
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
    }
}

示例:迭代器的删除方法。

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("d");

        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String s = it.next();
            if("b".equals(s)){
                // 指向谁,那么此时就删除谁
                it.remove();
            }
        }
        System.out.println(list);
    }
}

2)forEach()

先看一个 forEach() 方法遍历 List 集合的例子:

List<String> list =Lists.newArrayList("a","b","c","d");

// 遍历方式1(其中anyThing可以用其它字符替换)
list.forEach((anyThing)->System.out.println(anyThing));
// 遍历方式2
list.forEach(any->System.out.println(any));

// 匹配输出"b"
list.forEach(item->{
    if("b".equals(item)){
        System.out.println(item);
    }
);

forEach() 方法是 Iterable<T> 接口中的一个方法。Java 容器中,所有的 Collection 子类(List、Set)都会实现 Iteratable 接口以实现 foreach 功能。

forEach() 方法同样可以遍历存储其它对象的 List 集合:

List<User> list = Lists.newArrayList(new User("aa",10), new User("bb", 11), new User("cc", 12));
// 遍历
// list.forEach(any->System.out.println(any));
// 匹配输出:匹配项可以为 list 集合元素的属性(成员变量)
list.forEach(any->{
    if(new User("bb",11).equals(any)){
        System.out.println(any);
    }
});

3)增强 for 循环

介绍:

  • 它是 JDK5 之后出现的,其原理是一个 Iterator 迭代器。
  • 实现 Iterable 接口的类才可以使用迭代器和增强 for 循环。
  • 作用是简化了数组和 Collection 集合的遍历。

使用方法:

    for(集合/数组中元素的数据类型 变量名: 集合/数组名) {
        // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可
    }

示例:

import java.util.ArrayList;

public class MyCollectonDemo {
    public static void main(String[] args) {
        ArrayList<String> list =  new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        // 1. 数据类型一定是集合或者数组中元素的类型
        // 2. str 表示一个变量名,在循环的过程中,依次表示集合或者数组中的每一个元素
        // 3. list 就是要遍历的集合或者数组
        for(String str: list){
            System.out.println(str);
        }
    }
}

4、List

1. List 概述

List 集合的特点:

  • 存取有序
  • 元素可以重复
  • 有索引

List 的实现类特点:

  • ArrayList:底层是数组结构实现,查询快、增删慢。

  • LinkedList:底层是链表结构实现,查询慢、增删快。

2. List 特有方法

List 集合继承了 Collection 接口,因此 Collection 有的方法 List 都有,即只要学 List 特有的方法。

添加方法说明
void add(int index, E element)把元素添加到集合中的指定索引值上
void addAll(int index, Collection<? extends E> c)把集合元素添加到集合中的指定索引值上
获取方法说明
E get(int index)返回指定索引处的元素
E indexOf(Object o)获取元素所在的第一个索引
E lastIndexOf(Object o)获取元素所在的最后一个索引
List subList(int fromIndex, int toIndex)根据开始索引和结束索引获取子集合(包头不包尾)
修改方法说明
void set(int index, E element)使用指定的元素替换指定索引值位置的元素
迭代方法说明
ListIterator listIterator()返回的是 List 特有的迭代器

listIterator 具备 Iterator 的方法,其特有方法如下:

listIterator 特有方法说明
add(E e)把指定元素插入到当前指针指向的位置上
set(E e)使用指定的元素替换最后一次返回的值
hasPrevious()判断当前位置是否存在上一个元素
Previous()当前指针先向上移动一个单位,然后再取出当前指针指向的元素

示例:List 集合的三种遍历方式。

public static void main(String[] args){
     List list = new ArrayList();
     list.add("狗娃");
     list.add("狗剩");
     list.add("陈大狗");
     list.add("赵本山");

     // list集合的get方法
     for(int i=0; i<list.size(); i++){
         System.out.println("集合的元素:"+list.get(i));
     }

     // list迭代器正序遍历
     ListIterator it = list.listIterator();
     while(it.hasNext()){
         System.out.println("集合的元素:"+it.next());
         it.add("aa");
     }
     // 最终结果:[1, aa, 2, aa, 3, aa]
     // 但遍历打印结果仍是1、2、3,不会输出 aa,否则的话将是死循环
	
     // list迭代器逆序遍历
     while(it.hasPrevious()){
         System.out.println("集合的元素:"+it.previous());
     }
}

迭代器在遍历元素时的注意事项:在迭代器迭代元素(迭代器创建到使用结束的时间)的过程中,不允许使用集合对象改变集合中的元素个数,如果需要添加或者删除只能使用迭代器的方法进行操作。

ArrayList<String> list = new ArrayList<>();
list.add("a");

// 报错的情况:
ListIterator it = list.listIterator();
list.add("aa");  // 迭代元素过程中使用了集合对象的添加操作
it.next();
// 如果使用了集合对象改变集合的元素个数,就会出现 ConcurrentModificationException 异常

// 不报错的情况:
ListIterator it = list.listIterator();
it.add("aa");  // 迭代元素过程中使用了迭代器对象的添加操作
it.next();

3. List 实现类

1)ArrayList

ArrayList 是实现了 List 接口的可扩容数组(动态数组),它的内部是基于数组实现的。它的具体定义如下:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {...}
  • ArrayList 可以实现所有可选择的列表操作,允许存储所有类型的元素(包括 null)。ArrayList 还提供了内部存储 list 的方法,它能够完全替代 Vector,只有一点例外:ArrayList 不是线程安全的容器。
  • ArrayList 有一个容量的概念,这个数组的容量就是 List 用来存储元素的容量。
  • ArrayList 不是线程安全的容器,如果多个线程中至少有两个线程修改了 ArrayList 的结构的话就会导致线程安全问题,作为替代条件可以使用线程安全的 List,应使用Collections.synchronizedList:
List list = Collections.synchronizedList(new ArrayList(...))
  • ArrayList 具有 fail-fast 快速失败机制,能够对 ArrayList 作出失败检测。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生 fail-fast,即抛出 ConcurrentModificationException 异常。

问:使用 ArrayList 无参的构造函数创建一个对象时,默认容量是多少?如果长度不够时又自动增长多少?

:ArrayList 底层是维护了一个 Object 数组实现的,使用无参构造函数时,Object 数组默认容量是 10;当长度不够时自动增长 0.5 倍。

特点:

  1. 查询快:因为数组中的元素与元素之间的内存地址是连续的。
  2. 增删慢:因为 ArrayList 在增加元素时首先要检查长度够不够用,若不够,还要把旧数组的内容拷贝到新申请的数组中;而删除时,也要进行移位。因此如果是处理大数据量的场景则不建议用 ArrayList,效率过低。

特有的方法(不常用):

  • ensureCapacity(int minCapacity):指定容量。但一般用构造方法指定容量。
  • trimToSize():删除多余的容量。

ArrayList 应用场景:

  • 如果目前的数据是查询比较多,增删比较少的时候,那么就使用 ArrayList 存储这批数据。
  • 如:高校的图书馆(学生借阅多;书更新少)

代码示例:

import java.util.ArrayList;

public class Test {
    public static void main(String[] args) {

        // 创建集合
        ArrayList<String> array = new ArrayList<String>();

        // 添加元素
        array.add("hello");
        array.add("world");
        array.add("java");

        System.out.println(array.remove("world"));  // true
        System.out.println(array.remove("javaee"));  // false

        System.out.println(array.remove(1));  // java
        // System.out.println(array.remove(3));  // IndexOutOfBoundsException

        array.add("hello");
        array.add("world");
        array.add("java");

        System.out.println(array.set(1, "javaee"));  // hello
        // System.out.println(array.set(3,"javaee"));  // IndexOutOfBoundsException

        System.out.println(array.get(0));  // hello
        System.out.println(array.get(1));  // javaee
        System.out.println(array.get(2));  // world
        // System.out.println(array.get(4));  // IndexOutOfBoundsException

        System.out.println(array.size());  // 4

        // 输出集合
        System.out.println("array:" + array);  // array:[hello, javaee, world, java]
    }
}

2)Vector

Vector 同 ArrayList 一样,都是基于数组实现的,只不过 Vector 是一个线程安全的容器,它会对内部的每个方法都简单粗暴地上锁,避免多线程引起的安全性问题,但是通常这种同步方式需要的开销比较大。因此,访问元素的效率要远远低于 ArrayList。

还有一点在于扩容上,ArrayList 扩容后的数组长度会增加 50%,而 Vector 的扩容长度后数组会增加一倍。

3)LinkedList

LinkedList 是一个双向链表,允许存储任何元素(包括 null)。它的主要特性如下:

  • LinkedList 所有的操作都可以表现为双向性的,索引到链表的操作将遍历从头到屋,视哪个距离近为遍历顺序。
  • 注意这个实现也不是线程安全的,如果多个线程并发访问链表,并且至少其中的一个线程修改了链表的结构,那么这个链表必须进行外部加锁。或者使用:
List list = Collections.synchronizedList(new LinkedList(...)) 

特有方法:

方法名说明
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素

数据结构相关方法:

方法名说明
public void push()从集合头部添加元素(模拟堆栈先进后出的存储方式)
public E pop()从集合头部取出元素(模拟堆栈先进后出的存储方式)
public void offer()从集合头部添加元素(模拟队列先进先出的存储方式)
public E poll()从集合尾部取出元素(模拟队列先进先出的存储方式)

迭代器方法:

方法名说明
public Iterator descendingIterator()返回逆序的迭代器对象

示例:实现栈操作。

public static void main(String[] args){
     LinkedList list = new LinkedList();
     list.push("狗娃");
     list.push("狗剩");
     list.push("美美");

     //此为成功遍历全部的方法
     int size =list.size();
     for(int i = 0; i<size; i++){
         System.out.println(list.pop());
     }

     /* 因为pop会删除元素,因此size()会不断变化,导致输出少一个
     for(int i = 0; i<list.size(); i++){
         System.out.println(list.pop());
     }
     */
}

4)Stack

堆栈是我们常说的后入先出(吃了吐)的容器。它继承了 Vector 类,提供了通常用的 push 和 pop 操作、在栈顶的 peek 方法、测试 stack 是否为空的 empty 方法,和一个寻找与栈顶距离的 search 方法。

第一次创建栈时不包含任何元素。

一个更完善、可靠性更强的 LIFO 栈操作由 Deque 接口和它的实现提供,应该优先使用这个类:

Deque<Integer> stack = new ArrayDeque<Integer>()

4. Collections(List 集合工具类)

常见方法:

  • 对 list 进行二分查找(前提该集合是有序)

    • public static int binarySearch(List<T> list, T key):根据键值查找索引值。
    • public static int binarySearch(List<T> list, T key, Comparator):如果集合不具备自然顺序的元素,那么需要借助比较器。
  • 对 list 集合进行排序

    • public static void sort(List<T> list)
    • public static void sort(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
  • 对集合取最大值或最小值

    • public static T max(List<T> list)
    • public static T max(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
    • public static T min(List<T> list)
    • public static T min(List<T> list, comparator):如果集合不具备自然顺序的元素,那么需要传入比较器。
  • 对 list 进行反转

    • public static void reverse(List<T> list)
  • 将不同步的集合变成同步的集合

    • public static Set<T> synchronizedSet(Set<T> s)
    • public static Map<K, V> synchronizedMap(Map<K, V> m)
    • public static List<T> synchronizedList(List<T> list)
  • 打乱顺序

    • public static void shuffle(List<T> list)
  • 替换所有的元素

    • public static void fill(List<T> list, Object o):将 Collection 所有的元素替换为 Object 参数
  • 复制:将所有元素从一个列表复制到另一个列表中

    • public static <T> void copy(List<T> dest, List<T> src)
ArrayList<String> strings = new ArrayList<>();
strings.add("1");
strings.add("2");
strings.add("3");
ArrayList<String> result = new ArrayList<>();
result.add("0");
result.add("0");
result.add("0");
// 注意:目标集合大小需先要与源集合一致
Collections.copy(result, strings);
System.out.println(result);  // [1, 2, 3]

返回指定目标的第一次/最后一次的出现位置:

  • public static int indexOfSubList(List<T> source, List<T>)
  • public static int lastIndexOfSubList(List<T> source, List<T>)
ArrayList<String> strings = new ArrayList<>();
strings.add("1");
strings.add("2");
strings.add("3");
ArrayList<String> result = new ArrayList<>();
result.add("2");
result.add("3");
int index = Collections.indexOfSubList(strings, result);
System.out.println(index);  // 1

5、Set

Set 集合的特点:

  • 不可以存储重复元素。
  • 无序。
  • 没有索引,不能使用普通 for 循环遍历。

Set 无特有方法。

1. HashSet 实现类

HashSet 特点:

  • 底层数据结构是哈希表。
  • 存取无序。
  • 不可以存储重复元素。
  • 没有索引,不能使用普通 for 循环遍历。
  • 注意这个实现不是线程安全的。如果多线程并发访问 HashSet,并且至少一个线程修改了 set,必须进行外部加锁。或者使用Collections.synchronizedSet()方法重写。
  • 这个实现支持 fail-fast 机制。

哈希值:

  • 哈希值:是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。

  • 如何获取哈希值:Object 类中的 public int hashCode():返回对象的哈希值。

  • 哈希值的特点:

    • 同一个对象多次调用 hashCode() 方法返回的哈希值是相同的。
    • 默认情况下,不同对象的哈希值是不同的。而重写 hashCode() 方法可以实现让不同对象的哈希值相同。

哈希表结构:

  • JDK1.8 以前:数组 + 链表

JDK1.8 以后:

  • 节点个数少于等于 8 个:数组 + 链表
  • 节点个数多于 8 个:数组 + 红黑树

HashSet集合存储自定义类型元素时,要想实现元素的唯一,要求必须重写 hashCode 方法和 equals 方法。

  • 案例需求:

    • 创建一个存储学生对象的集合,存储多个学生对象,使用程序实现在控制台遍历该集合。
    • 要求:学生对象的成员变量值相同,我们就认为是同一个对象。
  • 代码实现:

// 学生类
public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}
// 测试类
public class HashSetDemo {
    public static void main(String[] args) {
        // 创建HashSet集合对象
        HashSet<Student> hs = new HashSet<Student>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);

        Student s4 = new Student("王祖贤", 33);

        // 把学生添加到集合
        hs.add(s1);
        hs.add(s2);
        hs.add(s3);
        hs.add(s4);

        // 遍历集合(增强 for)
        for (Student s : hs) {
            System.out.println(s.getName() + "," + s.getAge());
        }
    }
}

2. TreeSet 实现类

TreeSet 是一个基于 TreeMap 的 NavigableSet 实现。这些元素使用他们的自然排序或者在创建时提供的 Comparator 进行排序,具体取决于使用的构造函数。

  • TreeSet 可以将元素按照规则进行排序:
    • TreeSet():根据其元素的自然排序进行排序。
    • TreeSet(Comparator comparator):根据指定的比较器进行排序。
  • TreeSet 为基本操作 add、remove 和 contains 提供了 log(n) 的时间成本。
  • 注意 TreeSet 不是线程安全的。如果多线程并发访问 TreeSet,并且至少一个线程修改了 set,必须进行外部加锁。或者使用:
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...)) 
  • TreeSet 持有 fail-fast 机制。

示例:存储 Integer 类型的整数并遍历。

import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {

        TreeSet<Integer> ts = new TreeSet<>();
        ts.add(1);
        ts.add(2);
        ts.add(3);

        for(Integer i : ts){
            System.out.println(i);
        }
    }
}

代码示例:自然排序 Comparable 的使用。

  • 案例需求:

    • 存储学生对象并遍历,创建 TreeSet 集合使用无参构造方法。
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。
  • 实现步骤:

    1. 使用空参构造创建 TreeSet 集合:用 TreeSet 集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的。
    2. 自定义的 Student 类实现 Comparable 接口:自然排序,就是让元素所属的类实现 Comparable 接口,重写 compareTo(T o) 方法。
    3. 重写接口中的 compareTo 方法:重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写。
  • 代码实现:

// 学生类
public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

    @Override
    public int compareTo(Student o) {
        // 按照对象的年龄进行排序
        // 主要判断条件: 按照年龄从小到大排序
        int result = this.age - o.age;
        // 次要判断条件: 年龄相同时,按照姓名的字母顺序排序
        result = result == 0 ? this.name.compareTo(o.getName()) : result;
        return result;
    }
}
// 测试类
public class MyTreeSet {
    public static void main(String[] args) {
        // 创建集合对象
        TreeSet<Student> ts = new TreeSet<>();
        // 创建学生对象
        Student s1 = new Student("zhangsan", 28);
        Student s2 = new Student("lisi", 27);
        Student s3 = new Student("wangwu", 29);
        Student s4 = new Student("zhaoliu", 28);
        Student s5 = new Student("qianqi", 30);
        // 把学生添加到集合
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        // 遍历集合
        for (Student student: ts) {
            System.out.println(student);
        }
    }
}

示例:比较器排序 Comparator 的使用。

  • 案例需求:

    • 存储老师对象并遍历,创建 TreeSet 集合使用带参构造方法。
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序。
  • 实现步骤:

    • 用 TreeSet 集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的。
    • 比较器排序,就是让集合构造方法接收 Comparator 的实现类对象,重写 compare(T o1, T o2) 方法。
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写。
  • 代码实现:

// 老师类
public class Teacher {
    private String name;
    private int age;

    public Teacher() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 测试类
public class MyTreeSet {
    public static void main(String[] args) {

        // 创建集合对象
        TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
            @Override
            public int compare(Teacher o1, Teacher o2) {
                //o1 表示现在要存入的那个元素
                //o2 表示已经存入到集合中的元素
              
                // 主要条件
                int result = o1.getAge() - o2.getAge();
                // 次要条件
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        });
		
        // 创建老师对象
        Teacher t1 = new Teacher("zhangsan",23);
        Teacher t2 = new Teacher("lisi",22);
        Teacher t3 = new Teacher("wangwu",24);
        Teacher t4 = new Teacher("zhaoliu",24);
		
        // 把老师添加到集合
        ts.add(t1);
        ts.add(t2);
        ts.add(t3);
        ts.add(t4);
		
        // 遍历集合
        for (Teacher teacher : ts) {
            System.out.println(teacher);
        }
    }
}

两种比较方式总结:

  • 两种比较方式小结:
    • 自然排序:自定义类实现 Comparable 接口,重写 compareTo 方法,根据返回值进行排序。
    • 比较器排序:创建 TreeSet 对象的时候传递 Comparator 的实现类对象,重写compare方法,根据返回值进行排序。
    • 在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序。
  • 两种方式中关于返回值的规则:
    • 如果返回值为负数,表示当前存入的元素是较小值,存左边。
    • 如果返回值为 0,表示当前存入的元素跟集合中元素重复了,不存。
    • 如果返回值为正数,表示当前存入的元素是较大值,存右边。

6、Map

Map 集合的特点:

  • 双列集合,一个键对应一个值。
  • 键不可以重复,值可以重复。
  • 无序。
interface Map<K, V>  // K:键的类型;V:值的类型

Map 集合的常用方法:

添加方法说明
V put(K key, V value)添加元素
(如果之前没有存在该键,返回 null;如果之前存在该键,则返回原有的 value 值)
putall(Map<? extends K, ? extends V> m)把指定集合添加到集合中
获取方法说明
V get(K key)根据键获取对应的值
int size()获取 Map 中的键值对的个数
判断方法说明
boolean containsKey(K key)判断是否包含指定的键
boolean containsValue(V value)判断是否包含指定的值
boolean isEmpty()判断 Map 集合是否为空元素
(null:null 也能作为有效数据)
删除方法说明
void clear()清空集合中的所有数据
V remove(Object Key)根据键删除一条 Map 中的数据,返回的是该键对应的值
迭代方法说明
Set<K> keySet()把 Map 集合中的所有键都保存到一个 Set 集合中返回
Collection<V> values()把 Map 集合中的所有值都保存到一个 Collection 集合中返回
Set<Map.Entry<K, V>> entrySet()把 Map 集合中的所有键和值都保存到一个 Set 集合中返回

 示例:

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // V put(K key,V value):添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // V remove(Object key):根据键删除键值对元素
//        System.out.println(map.remove("郭靖"));
//        System.out.println(map.remove("郭襄"));

        // void clear():移除所有的键值对元素
//        map.clear();

        // boolean containsKey(Object key):判断集合是否包含指定的键
//        System.out.println(map.containsKey("郭靖"));
//        System.out.println(map.containsKey("郭襄"));

        // boolean isEmpty():判断集合是否为空
//        System.out.println(map.isEmpty());

        // int size():集合的长度,也就是集合中键值对的个数
        System.out.println(map.size());

        // 输出集合对象
        System.out.println(map);

        // V get(Object key):根据键获取值
//        System.out.println(map.get("张无忌"));
//        System.out.println(map.get("张三丰"));

        // Set<K> keySet():获取所有键的集合
//        Set<String> keySet = map.keySet();
//        for(String key : keySet) {
//            System.out.println(key);
//        }

        // Collection<V> values():获取所有值的集合
        Collection<String> values = map.values();
        for(String value : values) {
            System.out.println(value);
        }
    }
}

代码示例:Map 集合的遍历方式一。

  1. 获取所有键的集合:用 keySet() 方法实现。
  2. 遍历键的集合,获取到每一个键:用增强 for 实现。
  3. 根据键去找值:用 get(Object key) 方法实现。
public class MapDemo01 {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // 添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // 获取所有键的集合:用 keySet() 方法实现
        Set<String> keySet = map.keySet();
        // 遍历键的集合,获取到每一个键:用增强 for 实现
        for (String key : keySet) {
            // 根据键去找值:用 get(Object key) 方法实现
            String value = map.get(key);
            System.out.println(key + "," + value);
        }
    }
}

代码示例:Map 集合的遍历方式二。

  1. 获取所有键值对对象的集合
    • Set<Map.Entry<K, V>> entrySet():获取所有键值对对象的集合
  2. 遍历键值对对象的集合,得到每一个键值对对象
    • 用增强 for 实现,得到每一个 Map.Entry
  3. 根据键值对对象获取键和值
    • 用 getKey() 得到键
    • 用 getValue() 得到值
public class MapDemo {
    public static void main(String[] args) {
        // 创建集合对象
        Map<String, String> map = new HashMap<String, String>();

        // 添加元素
        map.put("张无忌", "赵敏");
        map.put("郭靖", "黄蓉");
        map.put("杨过", "小龙女");

        // 获取所有键值对对象的集合
        Set<Map.Entry<String, String>> entrySet = map.entrySet();
        // 遍历键值对对象的集合,得到每一个键值对对象
        for (Map.Entry<String, String> me : entrySet) {
            // 根据键值对对象获取键和值
            String key = me.getKey();
            String value = me.getValue();
            System.out.println(key + "," + value);
        }
    }
}

1. HashMap 实现类

HashMap 特点:

  • HashMap 是一个利用哈希表原理来存储元素的集合,并且允许空的 key-value 键值对。
  • 依赖 hashCode 方法和 equals 方法保证键的唯一。如果键要存储的是自定义对象,需要重写 hashCode 和 equals 方法。
  • HashMap 是非线程安全的,也就是说在多线程的环境下,可能会存在问题(而 Hashtable 是线程安全的容器)。可以使用Collections.synchronizedMap(new HashMap(...))来构造一个线程安全的 HashMap。
  • HashMap 也支持 fail-fast 机制。

HashMap 应用案例:

  • 案例需求:

    • 创建一个 HashMap 集合,键是学生对象(Student),值是居住地 (String)。存储多个元素,并遍历。
    • 要求保证键的唯一性:如果学生对象的成员变量值相同,我们就认为是同一个对象。
  • 代码实现:

// 学生类
public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}
// 测试类
public class HashMapDemo {
    public static void main(String[] args) {
        // 创建 HashMap 集合对象
        HashMap<Student, String> hm = new HashMap<Student, String>();

        // 创建学生对象
        Student s1 = new Student("林青霞", 30);
        Student s2 = new Student("张曼玉", 35);
        Student s3 = new Student("王祖贤", 33);
        Student s4 = new Student("王祖贤", 33);

        // 把学生添加到集合
        hm.put(s1, "西安");
        hm.put(s2, "武汉");
        hm.put(s3, "郑州");
        hm.put(s4, "北京");

        // 遍历集合
        Set<Student> keySet = hm.keySet();
        for (Student key : keySet) {
            String value = hm.get(key);
            System.out.println(key.getName() + "," + key.getAge() + "," + value);
        }
    }
}

2. TreeMap 实现类

TreeMap 特点:

  • TreeMap 类是一个基于 NavigableMap 实现的红黑树。这个 map 根据 key 自然排序存储,或者通过 Comparator 进行定制排序。
  • 如果键存储的是自定义对象,需要实现 Comparable 接口或者在创建 TreeMap 对象时候给出比较器排序规则。
  • TreeMap 为 containsKey、getput 和 remove 方法提供了 log(n) 的时间开销。
  • TreeMap 不是线程安全的。如果多线程并发访问 TreeMap,并且至少一个线程修改了 map,必须进行外部加锁。这通常通过在自然封装集合的某个对象上进行同步来实现,或者使用SortedMap m = Collections.synchronizedSortedMap(new TreeMap(..))
  • TreeMap 持有 fail-fast 机制。

TreeMap 应用案例:

  • 案例需求:

    • 创建一个 TreeMap 集合,键是学生对象(Student),值是籍贯(String),学生属性姓名和年龄,按照年龄进行排序并遍历。
    • 要求按照学生的年龄进行排序,如果年龄相同则按照姓名进行排序。
  • 代码实现:

// 学生类
public class Student implements Comparable<Student>{
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

    @Override
    public int compareTo(Student o) {
        // 按照年龄进行排序
        int result = o.getAge() - this.getAge();
        // 次要条件,按照姓名排序。
        result = result == 0 ? o.getName().compareTo(this.getName()) : result;
        return result;
    }
}
// 测试类
public class Test {
    public static void main(String[] args) {
        // 创建TreeMap集合对象
        TreeMap<Student,String> tm = new TreeMap<>();
      
        // 创建学生对象
        Student s1 = new Student("xiaohei", 23);
        Student s2 = new Student("dapang", 22);
        Student s3 = new Student("xiaomei", 22);
      
        // 将学生对象添加到TreeMap集合中
        tm.put(s1, "江苏");
        tm.put(s2, "北京");
        tm.put(s3, "天津");
      
        // 遍历TreeMap集合,打印每个学生的信息
        tm.forEach(
                (Student key, String value)->{
                    System.out.println(key + "---" + value);
                }
        );
    }
}

7、不可变集合

方法介绍:

  • 在 List、Set、Map 接口中,都存在 of 方法,可以用来创建一个不可变的集合。
    • 这个集合不能添加,不能删除,不能修改。
    • 但是可以结合集合的带参构造,实现集合的批量添加。
  • 在 Map 接口中,还有一个 ofEntries 方法可以提高代码的阅读性。
    • 首先会把键值对封装成一个 Entry 对象,再把这个 Entry 对象添加到集合当中。

示例代码:

public class MyVariableParameter4 {
    public static void main(String[] args) {
        // static <E> List<E> of(E…elements):创建一个具有指定元素的 List 集合对象
        // static <E> Set<E> of(E…elements):创建一个具有指定元素的 Set 集合对象
        // static <K, V> Map<K, V> of(E…elements):创建一个具有指定元素的 Map 集合对象

        // method1();
        // method2();
        // method3();
        // method4();

    }

    private static void method4() {
        Map<String, String> map = Map.ofEntries(
                Map.entry("zhangsan", "江苏"),
                Map.entry("lisi", "北京"));
        System.out.println(map);
    }

    private static void method3() {
        Map<String, String> map = Map.of("zhangsan", "江苏", "lisi", "北京", "wangwu", "天津");
        System.out.println(map);
    }

    private static void method2() {
        // 传递的参数当中,不能存在重复的元素。
        Set<String> set = Set.of("a", "b", "c", "d","a");
        System.out.println(set);
    }

    private static void method1() {
        List<String> list = List.of("a", "b", "c", "d");
        System.out.println(list);
        // list.add("Q");
        // list.remove("a");
        // list.set(0,"A");
        // System.out.println(list);

//        ArrayList<String> list2 = new ArrayList<>();
//        list2.add("aaa");
//        list2.add("aaa");
//        list2.add("aaa");
//        list2.add("aaa");

        // 集合的批量添加。
        // 首先是通过调用 List.of 方法来创建一个不可变的集合,of 方法的形参就是一个可变参数。
        // 再创建一个 ArrayList 集合,并把这个不可变的集合中所有的数据,都添加到 ArrayList 中。
        ArrayList<String> list3 = new ArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println(list3);
    }
}

十六、Java文件I/O

1、File类

File 类介绍:

  • 它是文件和目录的路径名的抽象表示。
  • 文件和目录是可以通过 File 封装成对象的。
  • 对于 File 而言,其封装的并不是一个真正存在的文件,仅仅是一个路径名而已。它可以是实际存在的,也可以是不存在的,将来是要通过具体的操作把这个路径的内容转换为具体的存在。

File 类构造方法:

方法名说明
File(String pathname)通过将给定的路径名字符串转换为抽象路径名来创建新的 File 实例
File(String parent, String child)通过父路径名字符串和子路径名字符串创建新的 File 实例
File(File parent, String child)通过父抽象路径名和子路径名字符串创建新的 File 实例

示例代码:

public class FileDemo {
    public static void main(String[] args) {
        // File(String pathname): 通过将给定的路径名字符串转换为抽象路径名来创建新的 File 实例
        File f1 = new File("E:\\test\\java.txt");
        System.out.println(f1);

        // File(String parent, String child): 通过父路径名字符串和子路径名字符串创建新的 File 实例
        File f2 = new File("E:\\test", "java.txt");
        System.out.println(f2);

        // File(File parent, String child): 通过父路径对象和子路径名字符串创建新的 File 实例
        File f3 = new File("E:\\test");
        File f4 = new File(f3, "java.txt");
        System.out.println(f4);
    }
}

File 类常用方法:

1)创建方法

方法名说明
public boolean createNewFile()当文件不存在时,创建一个由该路径名命名的新空文件
public boolean mkdir()创建由此路径名命名的目录
public boolean mkdirs()创建由此路径名命名的目录,包括任何必需但不存在的父目录
public boolean renameTo(File dest)如果目标文件与源文件在同一路径下,那么 renameTo 的作用是重命名(文件与文件夹);
如果不在同一路径下,那么 renameTo 的作用是剪切,并且不能操作文件夹

2)删除方法

方法说明
public boolean delete()删除由此抽象路径名表示的文件或目录。删除成功则返回 true
deleteOnExit()等到 JVM 退出时才删除文件,一般用于删除临时文件

3)判断方法

方法说明
public boolean isDirectory()测试此抽象路径名表示的 File 是否为目录
public boolean isFile()测试此抽象路径名表示的 File 是否为文件
public boolean exists()测试此抽象路径名表示的 File 是否真实存在
public boolean isHidden()测试此抽象路径名表示的 File 是否是一个隐藏的文件或文件夹
public boolean isAbsolute()测试此抽象路径名表示的 File 是否为绝对路径

4)获取方法

方法名说明
public String getAbsolutePath()返回绝对路径
public String getPath()返回绝对路径(传什么返回什么)
public String getName()获取文件或文件夹的名称,不包括上级路径
public String getAbsolutePath()获取绝对路径(与文件是否存在没关系)
public long length()获取文件或文件夹的字节大小(路径不存在则返回 0)
public String getParent()获取文件的父路径
public long lastModified()获取最后一次的修改时间(毫秒值)
public File[] listFiles()把当前文件夹下面的所有子文件名与子文件夹名都使用一个 File 对象描述,然后将这些对象存储到一个 File 数组中返回(子文件或子目录的绝对路径)。
(若对文件操作则返回 null)
public String[] list()把当前文件夹下面的所有子文件名与子文件夹名(包括隐藏文件与隐藏文件夹)存储到一个 String 数组中返回(文件或目录名称)。
(若对文件操作则返回 null)
public File[] list(FilenameFilter filter)返回指定路径中符合过滤条件的文件或文件夹(若对文件操作则返回 null)
public String[] listFiles(FilenameFilter filter)返回指定路径中符合过滤条件的文件或文件夹(若对文件操作则返回 null)

示例代码:

public class FileDemo {
    public static void main(String[] args) throws IOException {
        // 创建文件
        File f1 = new File("E:\\test\\java.txt");
        System.out.println(f1.createNewFile());
        System.out.println("--------");
        // 删除刚创建的文件
        f1.delete();

        // 创建单级目录
        File f2 = new File("E:\\test\\JavaSE");
        System.out.println(f2.mkdir());
        System.out.println("--------");

        // 创建多级目录
        File f3 = new File("E:\\test\\JavaWEB\\HTML");
//        System.out.println(f3.mkdir());  // 报错
        System.out.println(f3.mkdirs());
        System.out.println("--------");

        File f4 = new File("D:\\test\\test_delete.txt");
        System.out.println(f4.getAbsolutePath());  // D:\test\test_delete.txt
        System.out.println(f4.getAbsoluteFile());  // D:\test\test_delete.txt
        System.out.println(f4.getPath());  // D:\test\test_delete.txt
        System.out.println(f4.getName());  // test_delete.txt

        File f5 = new File("D:\\testa\\testb");
        System.out.println(f5.getName());  // testb

        File file = new File("D:\\softwares");
        File[] files = file.listFiles();
        for(File f : files){
            System.out.println(f);  // 输出绝对路径
        }

         File file = new File("D:\\softwares");
         String[] fileNames = file.list();
         for(String fileName:fileNames){
             System.out.println(fileName);  // 只有名称
         }
    }
}

案例:列出指定路径下所有的 java 文件。

  • 方式 1:使用 listFiles()
import java.io.File;

public class JavaBase {

    public static void main(String[] args) {
        findJavaFile1("d:\\JavaDemo");
    }

    public static void findJavaFile1(String pathName) {
        File file = new File(pathName);
        File[] files = file.listFiles();
        for(File f : files){
            if (f.isFile()&&f.getName().endsWith(".java")) {
                System.out.println(f);
            } else if (f.isDirectory()) {
                findJavaFile1(f.getPath());
            }
        }
    }
}
  • 方式 2:使用 listFiles(FilenameFilter filter)
// 自定义一个文件名过滤器
class MyFilter implements FilenameFilter{
     
     public boolean accept(File dir, String name){
          return name.endsWith(".java");
     }    
}
public class Test {
     public static void main(String[] args){
          File targetFile = new File("D:\\JavaPractise");
          listJava(targetFile);
     }
     
     public static void listJava(File dir){
          File[] files = dir.listFiles(new MyFilter());
          for(File file : files){
              System.out.println(file.getName());
          }
     }
}

2、字节流

1. IO 流概述和分类

IO 流介绍:

  • IO:输入/输出(Input/Output)
  • 流:是一种抽象概念,是对数据传输的总称。也就是说数据在设备间的传输称为流,流的本质是数据传输。
  • IO 流就是用来处理设备间数据传输问题的。常见的应用有文件复制、文件上传、文件下载等。

IO 流的分类:

  • 按照数据的流向:
    • 输入流:读数据
    • 输出流:写数据
  • 按照数据类型来分:
    • 字节流
      • 字节输入流
      • 字节输出流
    • 字符流
      • 字符输入流
      • 字符输出流

IO 流体系:

字节流:字节流读取的是文件中的二进制数据,并不会转换成我们看得懂的字符。

  • InputStream:所有字节输入流的超类、抽象类。

    • FileInputStream:读取文件的二进制数据。
    • BufferedInputStream:缓冲输入字节流。为了提高读取文件数据的效率。该类内部维护了一个 8KB 的字节数组。
  • OutputStream:所有字节输出流的超类、抽象类。

    • FileOutputStream:向文件输出数据。
    • BufferedOutputStream:缓冲输出字节流。内部也是维护了8KB的字节数组。

字符流:字符流会把读取到的二进制数据进行对应的编码与解码。字符流=字节流+编码/解码

  • Reader:所有字符输入流的超类、抽象类。

    • FileReader:读取文件的字符数据。
    • BufferedReader:缓冲输入字符流;出现的目的是为了提高读取文件字符的效率和拓展了 FileReader 的功能。该类内部维护了 8192 长度的字符数组。
  • Writer:所有字符输出流的超类、抽象类。

    • FileWriter:向文件输出字符数据;内部维护了一个 1KB 的字符数组。
    • BufferedWriter:缓冲输出字符流;出现的目的是为了提高写数据的效率和拓展了 FileWriter 的功能。该类只不过是内部维护了更大的 8192 长度的字符数组,与多了 newline 方法。

IO 流使用场景:

  • 如果操作的是纯文本文件,优先使用字符流。
  • 如果操作的是图片、视频、音频等二进制文件,优先使用字节流。
  • 如果不确定文件类型,优先使用字节流,字节流是万能的流。

2. 字节输出流

字节流抽象基类:

  • InputStream:这个抽象类是表示字节输入流的所有类的超类。
  • OutputStream:这个抽象类是表示字节输出流的所有类的超类。
  • 子类名特点:子类名称都是以其父类名作为子类名的后缀。

字节输出流:

  • FileOutputStream(String name):创建文件输出流以指定的名称写入文件。

使用字节输出流写数据的步骤:

  1. 创建字节输出流对象(调用系统功能创建文件和字节输出流对象,让字节输出流对象指向文件)。
  2. 调用字节输出流对象的写数据方法,把字符串数据转换成字节数组写出到文件。
  3. 释放资源(关闭此文件输出流,并释放与此流相关联的任何系统资源),原则:先开后关,后开先关。

注意事项:

  1. 如果目标文件不存在,那么会自动创建目标文件。
  2. 如果目标文件已存在,那么会先清空目标文件中的数据,然后再写入。
  3. 如果想追加内容,使用 new FileOutputStream(file,true),如果第二个参数为 true,则字节将写入文件的末尾而不是开头。
  4. 虽然 write() 方法接收 int 类型的数据,但是真正写出的只是一个字节的数据,只是把低 8 位的二进制数据写出,其他 24 位数据全部丢弃。
  5. write(buf, 0, 2):从字节数组的指定索引值开始写,写出两个字节。
  6. 每新创建一个 FileInputStream 对象时,默认情况下 FileOutputStream 的指针是指向了文件的开头位置。每写出一次,指针都会相应地移动。

示例代码:

import java.io.FileOutputStream;
import java.io.IOException;

public class JavaBase {
    public static void main(String[] args) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream("d:\\test_output_stream.txt", true);
            fileOutputStream.write(100);  // 实时写入"d"
            String str = "abcd";
            fileOutputStream.write(str.getBytes(), 1, 2);  // 追加写入"bc"
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3. 字节输入流

字节输入流:

  • FileInputStream(String name):通过与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的路径名 name 命名。

FileInputStream 读取方法:

  • public int read():一次读取一个字节,返回值是读取到的内容。返回 -1 则表示读取完毕。
  • public int read(byte[] b):一次读取 b 个字节大小的数据。内容是存储到缓冲数组(b)中,返回值是存储到缓冲数组中的字节个数。返回 -1 则表示读取完毕。

字节输入流读取数据的步骤:

  1. 创建字节输入流对象。
  2. 调用字节输入流对象的读数据方法。
  3. 释放资源(原则:先开后关,后开先关)。

示例:

import java.io.FileInputStream;
import java.io.IOException;

public class FileTest {
    public static void main(String[] args) throws IOException {
          // 1.读取目标文件
          File file = new File("D:\\abc.txt");
          // 2.建立数据的输入通道
          FileInputStream fileInputStream = new FileInputStream(file);

          // 保存每次读取到的字节个数
          int length = 0; 

          // 3.存储读取到的数据。缓冲数组的长度一般是1024的倍数
          byte[] buf = new byte[4];
          // read() 方法如果读取到了文件的末尾,就会返回 -1
          while((length=fileInputStream.read(buf)) != -1){ 
              System.out.println(new String(buf, 0, length));  // 每次读取的内容
          }

          // 4.关闭资源
          fileInputStream.close();
    }
}

示例:字节流复制文件。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class JavaBase {
    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            // 根据数据源创建字节输入流对象
            fileInputStream = new FileInputStream("E:\\mn.jpg");
            // 根据目的地创建字节输出流对象
            fileOutputStream = new FileOutputStream("myByteStream\\mn.jpg");

            // 读写数据,复制图片(一次读取一个字节数组,一次写入一个字节数组)
            byte[] bys = new byte[1024];
            int len;
            while ((len=fileInputStream.read(bys))!=-1) {
                fileOutputStream.write(bys, 0, len);
            }
        } catch (IOException e) {
/*            1. 首先要阻止后面的代码执行。因为前面的数据已经读取异常,后面的数据执行代码已无意义,并需要通知使用者这里出错了。
                 因此使用 throw,方法既会停止执行,并且也能抛出通知。
              2. 把IOException传递给RuntimeException包装一层,然后再抛出,目的是为了使用者使用变得更加灵活。
                 使用者对RuntimeException可处理也可不处理。
*/          System.out.println("读取文件出错...");
            throw new RuntimeException(e);
        } finally {
            // 关闭资源。原则:先开后关,后开先关
            try{
                if(fileOutputStream!=null){  // 如果读取的文件不存在,管道也就没建立起来,就会出现空指针异常,也就不需要关
                    fileOutputStream.close();
                    System.out.println("关闭输出流成功");
                }
            }catch(IOException e){  // 再出错一般就是硬件问题
                System.out.println("关闭输出流失败");
                throw new RuntimeException(e);
            }finally{
                try{
                    if(fileInputStream!=null){
                        fileInputStream.close();
                        System.out.println("关闭输入流成功");
                    }
                }catch(IOException e){
                    System.out.println("关闭输入流失败");
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

4. 字节缓冲流

方法名说明
BufferedOutputStream(OutputStream out)创建字节缓冲输出流对象
BufferedInputStream(InputStream in)创建字节缓冲输入流对象

1)BufferedInputStream

使用步骤:

  1. 找到目标文件。
  2. 建立数据的输入通道。
  3. 建立缓冲输入字节流(需要传递 InputStream,因此传递 FileInputStream)。
  4. 关闭资源(调用 BufferedInputStream 的 close 方法实际上关闭的是 FileInputStream)。

疑问 1:为什么创建 BufferedInputStream 的时候需要传递 FileInputStream?

  • 答:因为凡是缓冲流都不具备读写文件的能力,因此需要借助 FileInputStream 来读取文件的数据。

疑问 2:BufferedInputStream 的出现是为了提高读取文件的效率,但是 BufferedInputStream 的 read 方法每次只读取一个字节的数据,而 FileInputStream 每次也是读取一个字节的数据,那么 BufferedInputStream 如何提高效率?

  • 答:BufferedInputStream 是先将硬盘数据存入内部数组,再从数组中读取数据,相当于从内存中读取数据的速度;而 FileInputStream 相当于每次直接从硬盘读取数据。

2)BufferedOutputStream

使用步骤:

  1. 找到目标文件
  2. 建立数据的输出通道
  3. 建立缓冲输出字节流
  4. 把数据写出
  5. 把数据写到硬盘中,flush 与 close 均可。

注意:

使用 BufferedOutputStream 写数据的时候,write 方法是先把数据写到内部的字节数组中;如果需要把数据真正地写到硬盘上,需要调用 flush 方法或者 close 方法(而 FileOutputStream 的 write 则是实时写入文件内容),或者是内部维护的 8KB 的字节数组已经填满数据的时候。

综合示例:使用字节缓冲输入/输出流复制文件。

public class CopyAviDemo {

    public static void main(String[] args) throws IOException {
        method1();
        method2();
    }

    // 方式一:字节缓冲流一次读写一个字节数组
    public static void method2() throws IOException {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\字节流复制图片.avi"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("myByteStream\\字节流复制图片.avi"));
        byte[] bys = new byte[1024];
        int len;
        while ((len=bis.read(bys)) != -1) {
            bos.write(bys,0,len);
        }
        bos.close();
        bis.close();
    }

    // 方式二:字节缓冲流一次读写一个字节
    public static void method1() throws IOException {
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E:\\字节流复制图片.avi"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("myByteStream\\字节流复制图片.avi"));
        int by;
        while ((by=bis.read()) != -1) {
            bos.write(by);
        }
        bos.close();
        bis.close();
    }
}

3、字符流

字符流介绍:

  • 由于字节流操作中文不是特别得方便,所以 Java 就提供字符流。
  • 字符流会把读取到的二进制数据进行对应的编码与解码(字符流 = 字节流 + 编码/解码)。

中文的字节存储方式:

用字节流复制文本文件时,文本文件也会有中文,但是没有问题,原因是最终底层操作会自动进行字节拼接成中文,那么如何识别是中文的呢?

  • 字节流 write 能够写中文是因为借助了字符串的 getBytes 方法对字符串进行了编码(字符 ---> 数字);
  • 字节流 read 能够读中文是因为借助了字符串的 new String() 对字符串进行了解码(数字 ---> 字符);
  • 同时记事本本身具有解码的功能。

1. 编码

相关方法:

方法名说明
byte[] getBytes()使用平台的默认字符集将该 String 编码为一系列字节
byte[] getBytes(String charsetName)使用指定的字符集将该 String 编码为一系列字节
String(byte[] bytes)使用平台的默认字符集解码指定的字节数组来创建字符串
String(byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来创建字符串

代码示例:

public class StringDemo {
    public static void main(String[] args) throws UnsupportedEncodingException {
        // 定义一个字符串
        String s = "中国";

        // 编码
        // byte[] bys = s.getBytes();  // [-28, -72, -83, -27, -101, -67]
        // byte[] bys = s.getBytes("UTF-8");  // [-28, -72, -83, -27, -101, -67]
        byte[] bys = s.getBytes("GBK");  // [-42, -48, -71, -6]
        System.out.println(Arrays.toString(bys));

        // 解码
        // String ss = new String(bys);
        // String ss = new String(bys, "UTF-8");
        String ss = new String(bys, "GBK");
        System.out.println(ss);
    }
}

2. 字符输出流

介绍:

  • Writer: 用于写入字符流的抽象父类。
  • FileWriter: 用于写入字符流的常用子类。

构造方法:

方法名说明
FileWriter(File file)根据给定的 File 对象构造一个 FileWriter 对象
FileWriter(File file, boolean append)根据给定的 File 对象构造一个 FileWriter 对象
FileWriter(String fileName)根据给定的文件名构造一个 FileWriter 对象
FileWriter(String fileName, boolean append)根据给定的文件名以及指示是否附加写入数据的 boolean 值来构造 FileWriter 对象

成员方法:

方法名说明
void write(int c)写一个字符
void write(char[] cbuf)写入一个字符数组
void write(char[] cbuf, int off, int len)写入字符数组的一部分
void write(String str)写一个字符串
void write(String str, int off, int len)写一个字符串的一部分

刷新和关闭的方法:

方法名说明
flush()刷新流,之后还可以继续写数据
close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据

注意事项:

  1. Filewriter 内部是维护了一个 1KB 的字符数组的,写数据的时候会先写入到内部的字符数组中,如果需要把数据真正写到硬盘上,需要调用 flush 或 close 方法。
  2. 如果目标文件不存在,会自动创建目标文件。
  3. 如果在原有的文件上追加数据,需要使用 new FileWriter(file, true),只有创建新的 FileWriter 对象时才会把指针放在文件开头处。

示例:

public class OutputStreamWriterDemo {

    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("myCharStream\\a.txt");

        // void write(int c):写一个整数
//        fw.write(97);
//        fw.write(98);
//        fw.write(99);

        // void writ(char[] cbuf):写入一个字符数组
        char[] chs = {'a', 'b', 'c', 'd', 'e'};
//        fw.write(chs);

        // void write(char[] cbuf, int off, int len):写入字符数组的一部分
//        fw.write(chs, 0, chs.length);
//        fw.write(chs, 1, 3);

        // void write(String str):写一个字符串(具备编码的功能)
//        fw.write("abcde");

        // void write(String str, int off, int len):写字符串的一部分
//        fw.write("abcde", 0, "abcde".length());
        fw.write("abcde", 1, 3);

        // 释放资源
        fw.close();
    }
}

3. 字符输入流

介绍:

  • Reader: 用于读取字符流的抽象父类。
  • FileReader: 用于读取字符流的常用子类。

构造方法:

方法名说明
FileReader(File file)在给定从中读取数据的 File 的情况下创建一个新 FileReader
FileReader(String fileName)在给定从中读取数据的文件名的情况下创建一个新 FileReader

成员方法:

方法名说明
int read()一次读一个字符数据
int read(char[] cbuf)一次读一个字符数组数据

代码示例:

public class InputStreamReaderDemo {
    public static void main(String[] args) throws IOException {
   
        FileReader fr = new FileReader("myCharStream\\b.txt");

        // int read():一次读一个字符数据
//        int ch;
//        while ((ch=fr.read())!=-1) {
//            System.out.print((char)ch);
//        }

        // int read(char[] cbuf):一次读一个字符数组数据
        char[] chs = new char[1024];
        int len;
        while ((len = fr.read(chs)) != -1) {
            System.out.print(new String(chs, 0, len));
        }

        // 释放资源
        fr.close();
    }
}

注意:

Java 默认使用的是 GBK 编码表,如果 FileReader 读到的数据找不到对应的字符,那么会返回一个未知字符对应的数字,未知字符占一个字节。因此如果用字符流拷贝图片,图片会有部分数据丢失而打不开。

综合示例:字符输入/输出流 边读边写拷贝数据。

     public static void main(String[] args) throws IOException {  // 找到目标文件
          File inFile = new File("D:\\abc.txt");
          File outFile = new File("E:\\a.txt");
          // 建立数据的输入通道
          FileReader fileReader = new FileReader(inFile);
          FileWriter fileWriter = new FileWriter(outFile);
          char[] buf = new char[1024];
          if(fileReader.read(buf) != -1){
              fileWriter.write(buf);
          }
          fileWriter.close();
          fileReader.close();

     }

4. 字符缓冲流

字符缓冲流介绍:

  • BufferedWriter:将文本写入字符输出流并缓冲字符,以提供单个字符、数组和字符串的高效写入。可以指定缓冲区大小或者使用默认大小。默认值已足够大,可用于大多数用途。
  • BufferedReader:从字符输入流读取文本并缓冲字符,以提供字符、数组和行的高效读取。可以指定缓冲区大小或者使用默认大小。 默认值已足够大,可用于大多数用途。

构造方法:

方法名说明
BufferedWriter(Writer out)创建字符缓冲输出流对象
BufferedReader(Reader in)创建字符缓冲输入流对象

示例代码:

public class BufferedStreamDemo {
    public static void main(String[] args) throws IOException {
        // BufferedWriter(Writer out)
        BufferedWriter bw = new BufferedWriter(new FileWriter("myCharStream\\bw.txt"));
        bw.write("hello\r\n");
        bw.write("world\r\n");
        bw.close();

        // BufferedReader(Reader in)
        BufferedReader br = new BufferedReader(new FileReader("myCharStream\\bw.txt"));

        // 一次读取一个字符数据
//        int ch;
//        while ((ch=br.read())!=-1) {
//            System.out.print((char)ch);
//        }

        // 一次读取一个字符数组数据
        char[] chs = new char[1024];
        int len;
        while ((len=br.read(chs))!=-1) {
            System.out.print(new String(chs, 0, len));
        }
        br.close();
    }
}

5. 字符缓冲流特有方法

BufferedWriter:

方法名说明
void newLine()写入一个行分隔符(行分隔符字符串由系统属性定义)

BufferedReader:

方法名说明
String readLine()读一行文字。结果包含行的内容的字符串,不包括任何行终止字符。如果流的结尾已经到达,则为 null

示例:使用步骤与自己实现 readline。

     public static void main(String[] args) throws IOException {
          // 1.找到目标文件
          File file = new File("D:\\abc.txt");
          // 2.建立数据的输入通道
          FileReader fileReader = new FileReader(file);
          // 3.建立缓冲输入字符流
          BufferedReader bufferedReader = new BufferedReader(fileReader);
          // 4.读取数据

/*        char content = (char) bufferedReader.read();
          // 读取一个字符。
          System.out.println(content);*/
          // 使用BufferedReader拓展的功能readLine(): 一次读取一行,如果读到末尾返回null
          String line = null;
          while((line = bufferedReader.readLine())!=null){
              System.out.println(line);
          }  // 虽然readline每次读取一行数据,但是读到的line是不包含\r\n的
     }

     // 自己实现readLine方法
     public static String myReadLine(FileReader fileReader) throws IOException{
          // 创建一个字符串缓冲类用于存储读取到的数据
          StringBuilder sb = new StringBuilder();
          int content = 0;
          while((content=fileReader.read())!=-1){
              // 遇到\r则不读取数据,遇到\n则结束
              if(content =='\r'){
                   continue;
              }else if(content =='\n'){
                   break;
              }else{
                   // 普通字符
                   sb.append((char)content);
              }
          }
          // 代表已经读取完毕
          if(content==-1){
              return null;
          }
          return sb.toString();  // 如果没有内容,返回的是"",不是null
     }

     // 实现自己的readline方法循环输出全部数据
     while((line=myReaderLine(fileReader))! =null){
         System.out.println(line);
     }

示例:使用字符缓冲流读取文件中的数据,排序后再次写到本地文件。

public class CharStreamDemo14 {
    public static void main(String[] args) throws IOException {

        // 1. 把文件中的数据读取进来
        BufferedReader br = new BufferedReader(new FileReader("charstream\\sort.txt"));
        // 输出流一定不能写在这里,否则会先清空文件中的内容
        // BufferedWriter bw = new BufferedWriter(new FileWriter("charstream\\sort.txt"));

        String line = br.readLine();
        System.out.println("读取到的数据为" + line);
        br.close();

        // 2. 按照空格进行切割
        String[] split = line.split(" ");  // 9 1 2 5 3 10 4 6 7 8
        // 3. 把字符串数组变成int数组
        int [] arr = new int[split.length];
        // 遍历split数组,可以进行类型转换
        for (int i=0; i < split.length; i++) {
            String smallStr = split[i];
            // 类型转换
            int number = Integer.parseInt(smallStr);
            // 把转换后的结果存入到arr中
            arr[i] = number;
        }
        // 4. 排序
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));

        // 5. 把排序之后结果写回到本地 1 2 3 4...
        BufferedWriter bw = new BufferedWriter(new FileWriter("charstream\\sort.txt"));
        // 写出
        for (int i = 0; i < arr.length; i++) {
            bw.write(arr[i] + " ");
            bw.flush();
        }
        // 释放资源
        bw.close();
    }
}

4、转换流

  • InputStreamReader:是从字节流到字符流的桥梁,父类是 Reader。

    • 它读取字节,并使用指定的编码将其解码为字符。
    • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。
  • OutputStreamWriter:是从字符流到字节流的桥梁,父类是 Writer。

    • 使用指定的编码将写入的字符编码为字节。
    • 它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集。

作用:

  1. 如果目前所获取到的是一个字节流,且需要转换成字符流使用,这时就可以使用转换流。
  2. 使用转换流可以指定编码表进行读写文件(FileReader/Writer 不能指定码表)。

构造方法:

方法名说明
InputStreamReader(InputStream in)使用默认字符编码创建 InputStreamReader 对象
InputStreamReader(InputStream in, String chatset)使用指定的字符编码创建 InputStreamReader 对象
OutputStreamWriter(OutputStream out)使用默认字符编码创建 OutputStreamWriter 对象
OutputStreamWriter(OutputStream out, String charset)使用指定的字符编码创建 OutputStreamWriter 对象

示例:

public class Test {
     
     public static void main(String[] args) throws IOException {
          readTest();
          writeTest();
          writeTest2();
          readTest2();
     }
     
     // 输出字节流的转换流
     public static void writeTest() throws IOException{
          File file = new File("D:\\writeTest.txt");
          FileOutputStream fileOutputStream = new FileOutputStream(file);
          //fileOutputStream.write("abc".getBytes());
          OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
          outputStreamWriter.write("大家好");
          outputStreamWriter.close();
     }
     
     // 使用输出字节流的转换流指定码表写出数据
     public static void writeTest2() throws IOException{
          File file = new File("D:\\writeTest2.txt");
          FileOutputStream fileOutputStream = new FileOutputStream(file);
          //fileOutputStream.write("abc".getBytes());
          OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream,"UTF-8");
          outputStreamWriter.write("大家好,这用了UTF-8");
          outputStreamWriter.close();
     }
     
     // 输入字节流的转换流
     public static void readTest() throws IOException{
          InputStream inputStream = System.in;  // 获取了标准的输入流
          // System.out.println("读到的字符:"+(char)inputStream.read());  // 只能读一个字节
          
          // 为了使用BufferedReader的readline方法,要先把字节流转换成字符流
          InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
          BufferedReader bufferedReader = new BufferedReader(inputStreamReader);  // 需要输入Reader,即字符流
          String line = null;
          while((line=bufferedReader.readLine())!=null){
              System.out.println("读到的字符:"+line);
          }
     }
     
     // 使用输入字节流的转换流指定码表读取数据
     public static void readTest2() throws IOException{
          File file = new File("D:\\writeTest2.txt");
          FileInputStream fileInputStream = new FileInputStream(file);
          // 创建字节流的转换流并且指定码表进行读取
          InputStreamReader inutStreamReader = new InputStreamReader(fileInputStream,"UTF-8");
          char[] buf = new char[1024];
          int length = 0;
          while((length=inutStreamReader.read(buf))!=-1){
              System.out.println(new String(buf));
          }
     }

}

5、Properties

Properties 介绍:

  • 是一个 Map 体系的集合类。
  • 主要用于生成配置文件与读取配置文件的信息。
  • 元素中的键及其对应的值都是一个字符串。
  • 可以保存到流中或从流中加载。

常用方法:

方法名说明
Object setProperty(String key, String value)设置集合的键和值,都是 String 类型,底层调用 Hashtable 方法 put
String getProperty(String key)使用此属性列表中指定的键搜索对应的值
Set<String> stringPropertyNames()从该属性列表中返回一个不可修改的键值对,其中键及其对应的值是字符串
void load(Reader reader)从输入字符流读取属性列表(键和元素对)
void store(Writer writer, String comments)将此属性列表(键和元素对)写入此 Properties 表中,以适合使用 load(Reader) 方法的格式写入输出字符流

示例:

public class Test {
     public static void main(String[] args) throws FileNotFoundException, IOException {
          //creatProperties();
          readProperties();
     }
     
     // 读取配置文件的信息
     public static void readProperties() throws FileNotFoundException, IOException{
          // 1.创建Properties对象
          Properties properties = new Properties();
          // 2.把配置文件的信息加载到Properties中
          properties.load(new FileReader("D:\\aa.properties"));
          // 3.遍历查看Properties中的信息是否加载成功
          Set<Entry<Object, Object>> entrys = properties.entrySet();
          for(Entry<Object, Object> entry : entrys){
              System.out.println("键:"+entry.getKey()+" 值"+entry.getValue());
          }
          // 修改狗娃的密码
          properties.setProperty("狗娃", "1234");
          properties.store(new FileWriter("D:\\aa.properties"), "hehe");
     }

     // 生成配置文件
     public static void creatProperties() throws FileNotFoundException, IOException{
          // 1.创建Properties对象
          Properties properties = new Properties();
          properties.setProperty("狗娃", "123");

          properties.setProperty("狗剩", "234");
          // 2.使用Properties生成配置文件
          // 第一个参数是一个输出流对象;第二个参数是使用一个字符串描述这个配置文件的信息。
          // properties.store(new FileOutputStream("D:\\aa.properties"), "haha");
          properties.store(new FileWriter("D:\\aa.properties"), "haha");
     }
}

案例:使用配置文件记录软件使用次数,若试用超过 3 次则提醒购买正版。

public class Test {
     public static void main(String[] args) throws IOException {
		
          File file = new File("D:\\count.properties");
          if(!file.exists()){
              // 如果配置文件不存在,则创建配置信息
              file.createNewFile();
          }

          // 创建Properties对象
          Properties properties = new Properties();
          // FileOutputStream fileOutputStream = new FileOutputStream(file);
          // 注意:若使用上面语句会清空文件内容,因此永远都是使用一次,因此至少要放在load语句后面
          
          // 把配置文件的信息加载到Properties中
          properties.load(new FileInputStream(file));
          int count = 0;  // 定义该变量用于记录使用次数

          // 读取配置文件的运行次数
          String value = properties.getProperty("count");  // 通过key获取value
          if(value!=null){
              count = Integer.parseInt(value);
          }

          // 判断使用次数是否超过3次
          if(count==3){
              System.out.println("您的试用次数已超过3次,请购买正版!");
              System.exit(0);
          }

          count++;
          System.out.println("您已经试用了本软件第"+count+"次");
          properties.setProperty("count", count+"");

          // 生成配置文件
          properties.store(new FileOutputStream(file), "Run times");
     }
}

十七、Java类加载器

1、类加载器简介

类加载器:负责将 .class 文件(存储的物理文件)加载在到内存中。

2、类加载的过程

1. 类加载时机

类进行加载的时机有如下场景:

  • 创建类的实例(对象)。
  • 调用类的类方法。
  • 访问类或者接口的类变量,或者为该类变量赋值。
  • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
  • 初始化某个类的子类。
  • 直接使用 java.exe 命令来运行某个主类。

2. 类加载过程

  • 当一个类被使用的时候,才会加载到内存。
  • 类加载的过程:加载、链接(验证、准备、解析)、初始化。

1)加载

  1. 通过包名+类名,获取这个类(文件),准备用流进行传输;
  2. 将这个类加载到内存中;
  3. 加载完毕,创建一个 class 对象。

2)链接

  • 验证:确保 Class 文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。

  • 准备:负责为类的类变量(被 static 修饰的变量)分配内存,并设置默认初始化值。

  • 解析:将类的二进制数据流中的符号引用替换为直接引用(本类中如果用到了其他类,此时就需要找到对应的类)。

3)初始化

根据程序员通过程序制定的主观计划,去初始化类变量和其他资源(即静态变量赋值,及初始化其他资源)。

3、类加载的分类

分类:

  • Bootstrap class loader:虚拟机的内置类加载器,通常表示为 null,并且没有父类。
  • Platform class loader:平台类加载器,负责加载 JDK 中一些特殊的模块。
  • System class loader:系统类加载器,负责加载用户类路径上所指定的类库。

类加载器的继承关系:

  • System 的父加载器为 Platform。
  • Platform 的父加载器为 Bootstrap。

示例:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        // 获取系统类加载器的父加载器 -- 平台类加载器
        ClassLoader classLoader1 = systemClassLoader.getParent();

        // 获取平台类加载器的父加载器 -- 启动类加载器
        ClassLoader classLoader2 = classLoader1.getParent();

        System.out.println("系统类加载器" + systemClassLoader);
        System.out.println("平台类加载器" + classLoader1);
        System.out.println("启动类加载器" + classLoader2);

    }
}

4、双亲委派模式

双亲委派模式如下:

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

5、ClassLoader

方法名说明
public static ClassLoader getSystemClassLoader()获取系统类加载器
public InputStream getResourceAsStream(String name)加载某一个资源文件

示例:

public class ClassLoaderDemo {
    public static void main(String[] args) throws IOException {
        // static ClassLoader getSystemClassLoader():获取系统类加载器
        // InputStream getResourceAsStream(String name):加载某一个资源文件

        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        // 利用加载器去加载一个指定的文件
        // 参数:文件的路径(放在src的根目录下,默认去那里加载)
        // 返回值:字节流
        InputStream is = systemClassLoader.getResourceAsStream("prop.properties");

        Properties prop = new Properties();
        prop.load(is);

        System.out.println(prop);

        is.close();
    }
}

十八、Java反射

1、反射简介

反射是 Java 中一个非常重要且也是一个高级特性,基本上 Spring 等一系列框架都是基于反射的思想写成的。

什么是反射?

  1. 在程序的运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法
  2. 对于任意一个对象,都能够调用它的任意属性和方法
  3. 这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

反射主要提供了以下功能:

  1. 在运行时判断任意一个对象所属的类
  2. 在运行时构造任意一个类的对象
  3. 在运行时判断任意一个类所有的成员变量和方法。在运行时调用任意一个对象的方法

这么一看,反射就像是一个掌控全局的角色,不管你程序怎么运行,我都能够知道你这个类有哪些属性和方法,你这个对象是由谁调用的。

实现方式: 

2、获取 Class 类对象的三种方式

反射最重要的一个作用就是可以在运行时动态地创建类的对象,其中 Class 类是反射机制中最重要的类。

获取 Class 类对象的三种方式:

  1. 类名.class 属性
  2. 对象名.getClass()
  3. Class.forName(全类名)

package com.demo;

public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
package com.example.demo;
import com.demo.Student;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        // 方式一:Class类中的静态方法forName("全类名")
        // 全类名:包名 + 类名
        Class clazz1 = Class.forName("com.demo.Student");
        System.out.println(clazz1);  // class com.demo.Student

        // 方式二:通过class属性来获取
        Class clazz2 = Student.class;
        System.out.println(clazz2);  // class com.demo.Student

        // 方式三:利用对象的getClass方法来获取class对象
        //getClass方法是定义在Object类中.
        Student s = new Student();
        Class clazz3 = s.getClass();
        System.out.println(clazz3);  // class com.demo.Student

        System.out.println(clazz1 == clazz2);  // true
        System.out.println(clazz2 == clazz3);  // true
    }
}

3、反射获取 Class 类的对象

1. 反射获取构造方法对象

1)Class 类获取构造方法对象

方法名说明
Constructor<?>[] getConstructors()返回所有公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors()返回所有构造方法对象的数组
Constructor<T> getConstructor(Class<?>... parameterTypes)返回单个公共构造方法对象
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)返回单个构造方法对象

示例: 

package com.demo;

public class Student {

    private String name;
    private int age;

    // 私有的有参构造方法
    private Student(String name) {
        System.out.println("name的值为:" + name);
        System.out.println("private...Student...有参构造方法");
    }

    // 公共的无参构造方法
    public Student() {
        System.out.println("public...Student...无参构造方法");
    }

    // 公共的有参构造方法
    public Student(String name, int age) {
        System.out.println("name的值为:" + name + "age的值为:" + age);
        System.out.println("public...Student...有参构造方法");
    }
}
package com.example.demo;
import java.lang.reflect.Constructor;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
         method1();
         method2();
         method3();
         method4();
    }

    private static void method4() throws ClassNotFoundException, NoSuchMethodException {
        // Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回单个构造方法对象         
        // 获取Class对象
        Class clazz = Class.forName("com.demo.Student");
        Constructor constructor = clazz.getDeclaredConstructor(String.class);
        System.out.println(constructor);  // private com.demo.Student(java.lang.String)
    }

    private static void method3() throws ClassNotFoundException, NoSuchMethodException {
        // Constructor<T> getConstructor(Class<?>... parameterTypes):返回单个公共构造方法对象         
        // 获取Class对象
        Class clazz = Class.forName("com.demo.Student");
        // 小括号中一定要跟构造方法的形参保持一致
        Constructor constructor1 = clazz.getConstructor();
        System.out.println(constructor1);  // public com.demo.Student()

        Constructor constructor2 = clazz.getConstructor(String.class, int.class);
        System.out.println(constructor2);  // public com.demo.Student(java.lang.String,int)

        // 因为Student类中,没有只有一个int的构造,所以这里会报错.
        // Constructor constructor3 = clazz.getConstructor(int.class);
        // System.out.println(constructor3);
    }

    private static void method2() throws ClassNotFoundException {
        // Constructor<?>[] getDeclaredConstructors():返回所有构造方法对象的数组         
        // 获取Class对象
        Class clazz = Class.forName("com.demo.Student");

        Constructor[] constructors = clazz.getDeclaredConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
//            private com.demo.Student(java.lang.String)
//            public com.demo.Student()
//            public com.demo.Student(java.lang.String,int)
        }
    }

    private static void method1() throws ClassNotFoundException {
        // Constructor<?>[] getConstructors():返回所有公共构造方法对象的数组
        // 获取Class对象
        Class clazz = Class.forName("com.demo.Student");
        Constructor[] constructors = clazz.getConstructors();
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
            // public com.demo.Student()
            // public com.demo.Student(java.lang.String,int)
        }
    }
}

2)Constructor 类创建对象

方法名说明
T newInstance(Object...initargs)根据指定的构造方法创建对象
setAccessible(boolean flag)为 true 时表示取消访问检查

示例:

package com.example.demo;
import com.demo.Student;  // 与上个示例一致
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, 
            InvocationTargetException, InstantiationException {
         // T newInstance(Object... initargs):根据指定的构造方法创建对象
         method1();
         method2();
         method3();
         method4();

    }

    private static void method4() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
            IllegalAccessException, InvocationTargetException {
        // 获取一个私有的构造方法并创建对象
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取一个私有化的构造方法
        Constructor constructor = clazz.getDeclaredConstructor(String.class);

        // 被private修饰的成员,不能直接使用的
        // 如果用反射强行获取并使用,则需要临时取消访问检查
        constructor.setAccessible(true);

        // 3.直接创建对象
        Student student = (Student) constructor.newInstance("zhangsan");

        System.out.println(student);
        /*
            name的值为:zhangsan
            private...Student...有参构造方法
            com.demo.Student@677327b6
         */
    }

    private static void method3() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        // 简写格式
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.在Class类中有一个newInstance方法,可以利用空参直接创建一个对象
        Student student = (Student) clazz.newInstance();  // 这个方法现在已经过时了

        System.out.println(student);
        /*
            public...Student...无参构造方法
            com.demo.Student@1540e19d
         */
    }

    private static void method2() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
            IllegalAccessException, InvocationTargetException {
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取构造方法对象
        Constructor constructor = clazz.getConstructor();

        // 3.利用空参来创建Student的对象
        Student student = (Student) constructor.newInstance();

        System.out.println(student);
        /*
            public...Student...无参构造方法
            com.demo.Student@4554617c
         */
    }

    private static void method1() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
            IllegalAccessException, InvocationTargetException {
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取构造方法对象
        Constructor constructor = clazz.getConstructor(String.class, int.class);

        // 3.利用newInstance创建Student的对象
        Student student = (Student) constructor.newInstance("zhangsan", 23);

        System.out.println(student);
        /*
            name的值为:zhangsanage的值为:23
            public...Student...有参构造方法
            com.demo.Student@74a14482
         */
    }
}

2. 反射获取成员变量

1)Class 类获取成员变量

方法名说明
Field[] getFields()返回所有公共成员变量对象的数组
Field[] getDeclaredFields()返回所有成员变量对象的数组
Field getField(String name)返回单个公共成员变量对象
Field getDeclaredField(String name)返回单个成员变量对象

示例:

package com.demo;


public class Student {

    public String name;
    public int age;
    public String gender;
    private int money = 300;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                ", money=" + money +
                '}';
    }
}
package com.example.demo;
import java.lang.reflect.Field;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        method1();
        method2();
        method3();
        method4();
    }

    private static void method4() throws ClassNotFoundException, NoSuchFieldException {
        // Field getDeclaredField(String name):返回单个成员变量对象
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取money成员变量
        Field field = clazz.getDeclaredField("money");
        
        // 3.打印一下
        System.out.println(field);  // private int com.demo.Student.money
    }

    private static void method3() throws ClassNotFoundException, NoSuchFieldException {
        // Field getField(String name):返回单个公共成员变量对象
        // 想要获取的成员变量必须是真实存在的
        // 且必须是public修饰的
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取name这个成员变量
        Field field = clazz.getField("name");  // public java.lang.String com.demo.Student.name
        // Field field = clazz.getField("money");  // 报错,因为money是私有的

        // 3.打印一下
        System.out.println(field);
    }

    private static void method2() throws ClassNotFoundException {
        // Field[] getDeclaredFields():返回所有成员变量对象的数组
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取所有的Field对象
        Field[] fields = clazz.getDeclaredFields();

        // 3.遍历
        for (Field field : fields) {
            System.out.println(field);
        }
        /*
        public java.lang.String com.demo.Student.name
        public int com.demo.Student.age
        public java.lang.String com.demo.Student.gender
        private int com.demo.Student.money
         */
    }

    private static void method1() throws ClassNotFoundException {
        // Field[] getFields():返回所有公共成员变量对象的数组

        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取Field对象
        Field[] fields = clazz.getFields();

        // 3.遍历
        for (Field field : fields) {
            System.out.println(field);
        }
        /*
        public java.lang.String com.demo.Student.name
        public int com.demo.Student.age
        public java.lang.String com.demo.Student.gender
         */
    }
}

2)Field 类给成员变量赋值

方法名说明
void set(Object obj, Object value)赋值
Object get(Object obj)获取值

示例:

package com.example.demo;
import com.demo.Student;
import java.lang.reflect.Field;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException,
            InstantiationException {
        method1();
        method2();
    }

    private static void method2() throws ClassNotFoundException, NoSuchFieldException, InstantiationException,
            IllegalAccessException {
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        // 2.获取成员变量Field的对象
        Field field = clazz.getDeclaredField("money");

        // 3.取消一下访问检查
        field.setAccessible(true);

        // 4.调用get方法来获取值
        // 4.1 创建一个对象
        Student student = (Student) clazz.newInstance();
        // 4.2 获取指定对象的money的值
        Object o = field.get(student);

        System.out.println(o);  // 300
    }

    private static void method1() throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException {
        // void set(Object obj, Object value):给obj对象的成员变量赋值为value
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");

        //2.获取name这个Field对象
        Field field = clazz.getField("name");

        // 3.利用set方法进行赋值
        // 3.1 先创建一个Student对象
        Student student = (Student) clazz.newInstance();
        // 3.2 有了对象才可以给指定对象进行赋值
        field.set(student, "zhangsan");

        System.out.println(student);  // Student{name='zhangsan', age=0, gender='null', money=300}
    }
}

3. 反射获取成员方法

1)Class 类获取成员方法

方法名说明
Method[] getMethods()返回所有公共成员方法对象的数组,包括继承的
Method[] getDeclaredMethods()返回所有成员方法对象的数组,不包括继承的
Method getMethod(String name, Class<?>... parameterTypes)返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>... parameterTypes)返回单个成员方法对象

示例:

package com.demo;

public class Student {

    // 私有的,无参无返回值
    private void show() {
        System.out.println("私有的show方法,无参无返回值");
    }

    // 公共的,无参无返回值
    public void function1() {
        System.out.println("function1方法,无参无返回值");
    }

    // 公共的,有参无返回值
    public void function2(String name) {
        System.out.println("function2方法,有参无返回值,参数为" + name);
    }

    // 公共的,无参有返回值
    public String function3() {
        System.out.println("function3方法,无参有返回值");
        return "aaa";
    }

    // 公共的,有参有返回值
    public String function4(String name) {
        System.out.println("function4方法,有参有返回值,参数为" + name);
        return "aaa";
    }
}
package com.example.demo;
import java.lang.reflect.Method;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        method1();
        method2();
        method3();
        method4();
        method5();
    }

    private static void method5() throws ClassNotFoundException, NoSuchMethodException {
        // Method getDeclaredMethod(String name, Class<?>... parameterTypes):返回单个成员方法对象
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取一个成员方法show
        Method method = clazz.getDeclaredMethod("show");
        
        System.out.println(method);  // private void com.demo.Student.show()
    }

    private static void method4() throws ClassNotFoundException, NoSuchMethodException {
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取一个有形参的方法function2
        Method method = clazz.getMethod("function2", String.class);
       
        System.out.println(method);  // public void com.demo.Student.function2(java.lang.String)
    }

    private static void method3() throws ClassNotFoundException, NoSuchMethodException {
        // Method getMethod(String name, Class<?>... parameterTypes):返回单个公共成员方法对象
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取成员方法function1
        Method method1 = clazz.getMethod("function1");
        
        System.out.println(method1);  // public void com.demo.Student.function1()
    }

    private static void method2() throws ClassNotFoundException {
        // Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取Method对象
        Method[] methods = clazz.getDeclaredMethods();
        // 3.遍历一下数组
        for (Method method : methods) {
            System.out.println(method);
        }
        /*
        public java.lang.String com.demo.Student.function3()
        public java.lang.String com.demo.Student.function4(java.lang.String)
        private void com.demo.Student.show()
        public void com.demo.Student.function2(java.lang.String)
        public void com.demo.Student.function1()
         */
    }

    private static void method1() throws ClassNotFoundException {
        // Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取成员方法对象
        Method[] methods = clazz.getMethods();
        // 3.遍历
        for (Method method : methods) {
            System.out.println(method);
        }
        /*
        public void com.demo.Student.function2(java.lang.String)
        public void com.demo.Student.function1()
        public final void java.lang.Object.wait() throws java.lang.InterruptedException
        public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
        public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
        public boolean java.lang.Object.equals(java.lang.Object)
        public java.lang.String java.lang.Object.toString()
        public native int java.lang.Object.hashCode()
        public final native java.lang.Class java.lang.Object.getClass()
        public final native void java.lang.Object.notify()
        public final native void java.lang.Object.notifyAll()
         */
    }
}

2)Method 类执行方法

方法名说明
Object invoke(Object obj, Object... args)运行方法
  • 参数一: 用 obj 对象调用该方法。
  • 参数二: 调用方法的传递的参数(如果没有就不写)。
  • 返回值: 方法的返回值(如果没有就不写)。

示例:

package com.example.demo;
import com.demo.Student;  // 与上个示例一致
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class Test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
        InstantiationException, InvocationTargetException {

        // 1.获取class对象
        Class clazz = Class.forName("com.demo.Student");
        // 2.获取里面的Method对象  function4
        Method method = clazz.getMethod("function4", String.class);
        // 3.运行function4方法
        // 3.1 创建一个Student对象,当做方法的调用者
        Student student = (Student) clazz.newInstance();
        // 3.2 运行方法
        Object result = method.invoke(student, "zhangsan");

        System.out.println(result);  
        /*
            function4方法,有参有返回值,参数为zhangsan
            aaa
         */
    }
}

十九、Java枚举

1、枚举简介

某些方法所接收的数据必须在固定范围之内的,如方向、性别、季节、日期等。

枚举的定义格式:

// // 注意: 定义枚举类需要用关键字enum
public enum s {
    枚举项1, 枚举项2, 枚举项3;
}

示例:限制性别输入。

/*
JDK1.5 之前的解决方案:自定义一个类,然后私有化构造函数,在自定义类中创建本类的对象。
class Gender{

     String value;

     public static final Gender man = new Gender("男");
     public static final Gender woman = new Gender("女");

     private Gender(String value) {
          this.value = value;
     }
}
*/

// 枚举类解决方案
enum Gender{
     man("男"), woman("女");

     String value;

     private Gender(String value){
          this.value = value;
     }
}

class User{

     private Gender sex;
     String name;

     public Gender getSex() {
          return sex;
     }

     public void setSex(Gender sex) {
          this.sex = sex;
     }
}

public class Test {

     public static void main(String[] args) {
          User user = new User();
          user.name = "狗娃";
          user.setSex(Gender.man);
          System.out.println("名字:"+user.name+" 性别:"+user.getSex().value);
     }
}

枚举的特点:

  1. 枚举类也是一个特殊的类。
  2. 枚举值默认的修饰符是 public static final。
  3. 枚举值是枚举值所属的类的类型,枚举值是指向了本类的对象。
  4. 枚举值的构造方法默认的修饰符是 private。
  5. 枚举类可以定义自己的成员变量与成员函数。
  6. 枚举类可以自定义构造函数,但构造函数的修饰符必须是 private,同时枚举值也需传入相应的参数。
  7. 枚举类可以存在抽象的方法,但枚举值必须实现抽象方法。
  8. 枚举值必须位于类的第一个语句。

示例:

public enum Season {

    SPRING("春"){
        // 如果枚举类中有抽象方法
        // 那么在枚举项中必须要全部重写
        @Override
        public void show() {
            System.out.println(this.name);
        }
    },

    SUMMER("夏"){
        @Override
        public void show() {
            System.out.println(this.name);
        }
    },

    AUTUMN("秋"){
        @Override
        public void show() {
            System.out.println(this.name);
        }
    },

    WINTER("冬"){
        @Override
        public void show() {
            System.out.println(this.name);
        }
    };

    public String name;

    // 空参构造
    // private Season(){}
  
    // 有参构造
    private Season(String name){
        this.name = name;
    }
  
    // 抽象方法
    public abstract void show();
}

public class EnumDemo {
    public static void main(String[] args) {

        // 我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
        System.out.println(Season.SPRING);
        System.out.println(Season.SUMMER);
        System.out.println(Season.AUTUMN);
        System.out.println(Season.WINTER);
  
        // 每一个枚举项其实就是该枚举的一个对象
        Season spring = Season.SPRING;
    }
}

2、枚举方法

枚举常用方法:

方法名说明
String name()获取枚举项的名称
int ordinal()返回枚举项在枚举类中的索引值
int compareTo(E o)比较两个枚举项,返回的是索引值的差值
String toString()返回枚举常量的名称
static <T> T valueOf(Class<T> type,String name)获取指定枚举类中的指定名称的枚举值
T[] values()获得所有的枚举项

示例:

package com.example.demo;

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

public class Test {
    public static void main(String[] args) {
//        String name():获取枚举项的名称
        String name = Season.SPRING.name();
        System.out.println(name);  // SPRING
        System.out.println("-----------------------------");

//        int ordinal():返回枚举项在枚举类中的索引值
        int index1 = Season.SPRING.ordinal();  
        int index2 = Season.SUMMER.ordinal();
        int index3 = Season.AUTUMN.ordinal();
        int index4 = Season.WINTER.ordinal();
        System.out.println(index1);  // 0
        System.out.println(index2);  // 1
        System.out.println(index3);  // 2
        System.out.println(index4);  // 3
        System.out.println("-----------------------------");

//        int compareTo(E o):比较两个枚举项,返回的是索引值的差值
        int result = Season.SPRING.compareTo(Season.WINTER);
        System.out.println(result);  // -3
        System.out.println("-----------------------------");

//        String toString():返回枚举常量的名称
        String s = Season.SPRING.toString();
        System.out.println(s);  // SPRING
        System.out.println("-----------------------------");

//        static <T> T valueOf(Class<T> type,String name):获取指定枚举类中的指定名称的枚举值
        Season spring = Enum.valueOf(Season.class, "SPRING");
        System.out.println(spring);  // SPRING
        System.out.println(Season.SPRING == spring);  // true
        System.out.println("-----------------------------");

//        values():获得所有的枚举项
        Season[] values = Season.values();
        for (Season value : values) {
            System.out.println(value);
        }
        /*
        SPRING
        SUMMER
        AUTUMN
        WINTER
         */
    }
}

3、switch

switch 语句适用的数据类型:byte、char、short、int、String、枚举类型

注意:case 语句后跟的枚举值只需单写枚举值即可,不需要再声明该枚举值所属的枚举类。

示例:

enum Season{
     spring,summer,autumn,winter;
}

public class Demo {

     public static void main(String[] args) {
          Season season = Season.spring;
          System.out.println(season);  // spring
          switch(season){
              case spring:
                   System.out.println("春天");
                   break;
              case summer:
                   System.out.println("夏天");
                   break;
              case autumn:
                   System.out.println("秋天");
                   break;
              case winter:
                   System.out.println("冬天");
                   break;
          }
     }
}

二十、Java注解

1、注解简介

Java 注解(Annotation)又称为“元数据”,是指对我们的程序进行标注和解释。它为我们在代码中添加信息提供了一种形式化的方法。

注解和注释的区别:

  • 注释: 给程序员看的。
  • 注解: 给编译器看的。

使用注解进行配置的优势:使得代码更加简洁、方便。

2、自定义注解

格式:

public @interface 注解名称 {
	public 属性类型 属性名() default 默认值;
}

属性类型:

  • 基本数据类型
  • String
  • Class
  • 注解
  • 枚举
  • 以上类型的一维数组

示例:

package com.example.demo;

@interface Anno2 {
}

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

@interface Anno1 {

    // 定义一个基本类型的属性
    int a () default 23;

    // 定义一个String类型的属性
    public String name() default "demo";

    // 定义一个Class类型的属性
    public Class clazz() default Anno2.class;

    // 定义一个注解类型的属性
    public Anno2 anno() default @Anno2;

    // 定义一个枚举类型的属性
    public Season season() default Season.SPRING;

    // 以上类型的一维数组
    // int数组
    public int[] arr() default {1, 2, 3, 4, 5};

    // 枚举数组
    public Season[] seasons() default {Season.SPRING,Season.SUMMER};

    // 后期我们在使用注解的时候,如果我们只需要给注解的value属性赋值,那么value就可以省略
    public String value();

}

// 在使用注解时,如果注解里面的属性没有指定默认值,那么我们就需要手动给出注解属性的设置值
// @Anno1(name="demo")
// 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可
@Anno1("abc")
public class Test {
}

案例:自定义一个注解 @Test,如果某一个类的方法上使用了该注解,就执行该方法。

实现步骤:

  1. 自定义一个注解 Test,并在类中的某几个方法上加上注解;
  2. 在测试类中,获取注解所在的类的 Class 对象;
  3. 获取类中所有的方法对象;
  4. 遍历每一个方法对象,判断是否有对应的注解。

代码实现:

package com.example.demo;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// 表示Test这个注解的存活时间
@Retention(value = RetentionPolicy.RUNTIME)
@interface Test {
}

public class UseTest {

    // 没有使用Test注解
    public void show(){
        System.out.println("UseTest....show....");
    }

    // 使用Test注解
    @Test
    public void method(){
        System.out.println("UseTest....method....");
    }

    // 没有使用Test注解
    @Test
    public void function(){
        System.out.println("UseTest....function....");
    }
}
package com.example.demo;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


public class MyTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException,
            InvocationTargetException {
        // 1.通过反射获取UseTest类的字节码文件对象
        Class clazz = Class.forName("com.example.demo.UseTest");
        // 创建对象
        UseTest useTest = (UseTest) clazz.newInstance();

        // 2.通过反射获取这个类里面所有的方法对象
        Method[] methods = clazz.getDeclaredMethods();

        // 3.遍历数组,得到每一个方法对象
        for(Method method : methods) {
            // method依次表示每一个方法对象
            // isAnnotationPresent(Class<? extends Annotation> annotationClass)
            // 判断当前方法上是否有指定的注解
            // 参数:注解的字节码文件对象
            // 返回值:布尔结果
            if(method.isAnnotationPresent(Test.class)){
                method.invoke(useTest);
                /*
                UseTest....function....
                UseTest....method....
                 */
            }
        }
    }
}

3、元注解

元注解就是描述注解的注解:

元注解名说明
@Target指定了注解能在哪里使用
@Retention可以理解为保留时间(生命周期)
@Inherited表示修饰的自定义注解可以被子类继承
@Documented表示该自定义注解,会出现在 API 文档里面

示例:

@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})   // 指定注解使用的位置(成员变量、类、方法)
@Retention(RetentionPolicy.RUNTIME)  // 指定该注解的存活时间
//@Inherited  // 指定该注解可以被继承
public @interface Anno {
}

@Anno
public class Person {
}

public class Student extends Person {
    public void show(){
        System.out.println("student.......show..........");
    }
}

public class StudentDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 获取到Student类的字节码文件对象
        Class clazz = Class.forName("com.demo.Student");

        // 获取注解
        boolean result = clazz.isAnnotationPresent(Anno.class);
        System.out.println(result);
    }
}

4、lombok简化POJO

lombok(官网)提供了简单的注解形式,以简化或消除一些必须有但显得很臃肿的 Java 代码,尤其是针对 POJO 类。

1. lombok安装配置

步骤一:导入依赖

<!--简化代码的工具包-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

步骤二:安装 IDEA 插件(非必须)

如果不安装插件,只导入依赖,也可以正常使用,只是看不到生成的一些代码,如 getter、setter 方法。 

2. 常用注解

注解名称注解位置说明
@Data注解在类上提供所在类所有属性的 getting 和 setting 方法,以及 equals、canEqual、hashCode、toString 方法
@Setter注解在属性上为属性提供 setting 方法
@Getter注解在属性上为属性提供 getting 方法
@Slf4j注解在类上为类提供一个 属性名为log 的 slf4j日志对象
@NoArgsConstructor注解在类上为类提供一个无参的构造方法
@AllArgsConstructor注解在类上为类提供一个全参的构造方法
@Builder注解在类上使用 Builder 模式(即链式模式)构建对象

1)@Data

2)@Slf4j 

3)@AllArgsConstructor、@NoArgsConstructor

4)@Builder

二十一、Log4J日志框架

1、Log4J 简介

程序中的日志可以用来记录程序在运行时的所有信息,并可以进行持久化存储。

日志与输出语句的区别:

功能输出语句日志技术
取消输出需要修改代码,灵活性比较差不需要修改代码,灵活性比较好
输出位置只能是控制台可以将日志信息写入到文件或者数据库中
多线程和业务代码处于一个线程中多线程方式记录日志,不影响业务代码的性能

Java 日志体系:

什么是 Log4J:

  • Log4j 是 Apache 的一个开源项目。
  • 通过使用 Log4j,我们可以控制日志信息输送的目的地是控制台、文件等位置。
  • 我们也可以控制每一条日志的输出格式。
  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
  • 最令人感兴趣的就是,这些功能都可以只通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

2、入门案例

使用步骤:

  1. 导入 log4j 的相关 jar 包
  2. 编写 log4j 配置文件
  3. 在代码中获取日志的对象
  4. 按照级别设置记录日志信息

示例:

  • log4j 配置文件:命名为 log4j.properties,放在 src 根目录下
log4j.rootLogger=debug, consoleAppender, fileAppender

# 输出源为控制台的相关配置
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.ImmediateFlush=true
log4j.appender.consoleAppender.Target=System.out
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n

# 输出源为文件的相关配置
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.ImmediateFlush = true
log4j.appender.fileAppender.Append=true
log4j.appender.fileAppender.File=./log4j-log.log
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n
log4j.appender.fileAppender.encoding=UTF-8
  • 测试类:
package com.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 测试类
public class Log4JTest {

    // 方式一:使用log4j的api来获取日志的对象
    // 弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改
    // 不推荐使用
    // private static final Logger LOGGER = Logger.getLogger(Log4JTest.class);

    // 方式二:使用slf4j里面的api来获取日志的对象
    // 好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改
    // 推荐使用
    private static final Logger logger = LoggerFactory.getLogger(Log4JTest.class);

    public static void main(String[] args) {
        // 1.导入jar包
        // 2.编写配置文件
        // 3.在代码中获取日志的对象
        // 4.按照日志级别设置日志信息
        logger.debug("debug级别的日志");
        logger.info("info级别的日志");
        logger.warn("warn级别的日志");
        logger.error("error级别的日志");
        /*
        2021-10-04 23:32:46,965 main DEBUG Log4JTest:24 - debug级别的日志
        2021-10-04 23:32:46,973 main  INFO Log4JTest:25 - info级别的日志
        2021-10-04 23:32:46,973 main  WARN Log4JTest:26 - warn级别的日志
        2021-10-04 23:32:46,974 main ERROR Log4JTest:27 - error级别的日志
         */
    }
}

3、配置文件详解

1. 三个核心

  1. Loggers(记录器):设定日志的级别

    • Loggers 组件在此系统中常见的五个级别:DEBUG、INFO、WARN、ERROR 和 FATAL。
    • DEBUG < INFO < WARN < ERROR < FATAL。
    • 规则:只输出级别不低于设定级别的日志信息。
  2. Appenders(输出源):日志要输出的地方。如控制台(Console)、文件(Files)等。

    • org.apache.log4j.ConsoleAppender(控制台)
    • org.apache.log4j.FileAppender(文件)
  3. Layouts(布局):日志输出的格式。可以根据自己的喜好规定日志输出的格式,常用的布局管理器如下:

    • org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
    • org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
    • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

2. 配置根 Logger

  • 格式:log4j.rootLogger=日志级别, appenderName1, appenderName2, …

  • 日志级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL 或者自定义的级别。

  • appenderName1:就是指定日志信息要输出到哪里。可以同时指定多个输出目的地,用逗号隔开,例如:log4j.rootLogger=INFO, ca, fa

3. ConsoleAppender 配置项

  • ImmediateFlush=true

    • 表示所有消息都会被立即输出,设为 false 则不输出,默认值是 true。
  • Target=System.err

    • 默认值是 System.out。

4. FileAppender 配置项

  • ImmediateFlush=true

    • 表示所有消息都会被立即输出。设为 false 则不输出,默认值是 true。
  • Append=false

    • true 表示将消息添加到指定文件中,原来的消息不覆盖;
    • false 则将消息覆盖指定的文件内容,默认值是 true。
  • File=D:/logs/logging.log4j

    • 指定将消息输出到指定路径的 logging.log4j 文件中。

5. PatternLayout 配置项

  • ConversionPattern=%m%n:设定以怎样的格式显示消息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java GUI(Graphical User Interface)是Java语言提供的一套图形用户界面开发工具包,它可以帮助开发人员快速方便地创建Windows、Mac和Linux等操作系统下的图形用户界面应用程序。 Java GUI主要基于Swing和AWT两个工具包。其中,AWT(Abstract Window Toolkit)是Java最早提供的图形用户界面工具包。它提供了一些基本的图形组件,如按钮、文本框、标签等,并且支持布局管理器,可以快速创建简单的用户界面。但是,AWT的组件外观和行为在不同的操作系统下存在差异,且不支持透明度和半透明效果。 Swing是基于AWT的一套图形用户界面工具包,它提供了更多的组件和布局管理器,支持透明度和半透明效果,并且具有更好的跨平台性。Swing的外观和行为在不同操作系统下基本一致,同时也支持自定义外观。 Java GUI的开发过程通常包括以下步骤: 1. 创建主窗口(JFrame)或对话框(JDialog)。 2. 在主窗口或对话框中添加组件,如按钮、文本框、标签等。 3. 设置组件的属性和事件监听器,如字体、颜色、大小、点击事件等。 4. 使用布局管理器控制组件的位置和大小。 5. 处理用户输入或事件响应,如按钮点击事件、鼠标移动事件等。 Java GUI的开发需要一定的基础知识和编程经验,但是如果您掌握了Java语言和面向对象编程的基础,学习Java GUI也不会太难。同时,Java GUI还有很多优秀的第三方库和工具可以使用,如JavaFX和SwingX等,可以帮助开发人员更快速地创建复杂的图形用户界面应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wespten

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值