CoreJava Volume Ⅰ

1 Java 程序设计概述

Java 的 11 个关键字:

  1. 简单性
  2. 面向对象
  3. 分布式
  4. 健壮性
  5. 安全性
  6. 体系结构中立
  7. 可移植性
  8. 解释性
  9. 高性能
  10. 多线程
  11. 动态性

2 Java 环境安装

术语:

  • Java Develepment Kit(JDK):编写 Java 程序的程序员使用的软件
  • Java Runtime Environment(JRE):运行 Java 程序的用户使用的软件
  • Standard Edition(SE):用于桌面或简单服务器应用的 Java 平台
  • Enterprise Edition(EE):用于复杂服务器应用的 Java 平台
  • Micro Edition(ME):用于手机和其他小型设备的 Java 平台
  • OpenJDK:Java SE 的开源实现

Java Downloads | Oracle

2.1 Windows

  1. 安装包下载并安装(需要账号),jdk-8u301-windows-x64.exe
  2. 配置 JAVA_HOME 系统变量,变量值为 C:\env\jdk1.8(JDK安装目录)
  3. 配置 PATH 系统变量,添加 %JAVA_HOME%\bin%JAVA_HOME%\jre\bin
  4. 测试是否配置完成,java -versionjavac -version

2.2 Linux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# (CentOS7)
# 1、下载安装包并上传到 Linux,jdk-8u301-linux-x64.tar.gz

# 2、解压在 opt 目录下
sudo tar zxf jdk-8u301-linux-x64.tar.gz -C /opt

# 3、配置全局变量
vim /etc/profile

# 3.1 最后添加三项
export JAVA_HOME=/opt/jdk1.8.0_301
export PATH=$JAVA_HOME/bin:$PATH
export PATH=$JAVA_HOME/jre/bin:$PATH

# 4、激活配置
source /etc/profile

# 5、测试
java -version
javac -version

2.3 HelloWorld

1、创建文件 HelloWorld.java 文件(严格区分大小写)

1
2
3
4
5
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello World, Java 8!");
}
}

2、使用 javac 编译 Java 文件,javac HelloWorld.java

3、执行(没有任何后缀,直接就是 HelloWorld),java HelloWorld

3 Java 基础程序设计

3.1 注释

  1. 单行注释:// 开始到本行结尾
  2. 多行注释:/* 开始,以 */ 结束
  3. 文档注释:/** 开始,以 */ 结束
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 这是一个文档注释
* @filename CommentsTest.java
* @version 1.01 2021-10-18
* @author ReaJason
*/
public class CommentsTest{
/* 每个 Java 程序都必须有一个 main 方法,声明如下*/
public static void main(String[] args){
// 这是一个单行注释,下面是打印一句话
System.out.println("We will not use 'Hello, World!'");
}
}

3.2 数据类型

3.2.1 整型

类型 存储需求 取值范围
int 4 字节 -2^31 ~ 2^31-1
short 2 字节 -2^7 ~ 2^7-1
long 8 字节 -2^63 ~ 2^63-1
byte 1 字节 -128 ~ 127
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 长整型有 l 或 L 后缀
long a = 4000000000L;

// 十六进制有 0x 或 0X 前缀
int b = 0xa1f;

// 八进制有 0 前缀,不建议使用
int c = 0173;

// 二进制有 0b 或 0B 前缀,java7 开始
int d = 0b1001;

// Java7 开始数字字面量可以加_,使之易读,编译器会去除这些下划线
int e = 0b1111_0100_0010_0100_0000;

3.2.2 浮点型

类型 存储需求 取值范围
float 4 字节
double 8 字节
1
2
3
4
5
6
7
8
9
// float 类型必须添加 f 或 F 后缀
float a = 1010.101010f;

// double 类型可添加 d 或 D 后缀,小数默认为 double 类型
double b = 10.101010231;

// 使用十六进制表示浮点数,指数采用十进制,尾数采用十六进制,指数的基数为 2
// 0.125 = 0x1.0p-3
double c = 0x1.0p-3;
  • 正无穷大,Double.POSITIVE_INFINITY
  • 负无穷大,Double.NEGATIVE_INFINITY
  • NaN,Double.NaN

浮点数不适合用于无法接受舍入误差的金融计算,应该使用 BigDecimal 类

3.2.3 char 类型

char 类型的字面量值需要用单引号括起来。char 类型的值也可以表示为十六进制值。

转义序列 名称 Unicode 值
\b 退格 \u0008
\t 制表 \u0009
\n 换行 \u00a
\r 回车 \u00d
\“ 双引号 \u0022
\‘ 单引号 \u0027
\\ 反斜杠 \u005c

Unicode 转义序列会在解析代码之前处理,例如 // \u00A0 is a newline,由于 \u00A0 会替换成一个换行符,因此会产生语法错误

char 类型描述了 UTF-16 编码中的一个代码单元,占 2 个字节

不建议使用 char 类型,除非需要处理 UTF-16 代码单元

3.2.4 boolean 类型

boolean 类型有两个值:false 和 true。整型值和布尔值不能相互转换

3.3 变量

声明变量为,变量类型+变量名,例如 int a;。变量名不能以数字开头的,由数字、字母、_、$组成。大小写敏感,长度没有限制。

  • 尽管 $ 是合法的命名字符,但不要个人使用,它只用在 Java 编译器或其它工具生成的名字中
  • 不能使用 Java 保留字作为变量名

3.3.1 变量初始化

声明变量后,必须使用赋值语句进行显式初始化,例如 int a = 10;,千万不要使用未初始化的变量。变量的声明尽可能靠近变量第一次使用的地方。

Java 10 开始如果变量能推断出类型可以使用 var 来声明变量。

3.3.2 常量

使用 final 指定常量,常量只能被赋值一次,无法修改,static final 指定类常量。

1
2
3
4
5
6
7
8
9
10
11
public class Constants{
// 这是一个类常量,可以通过 Constants.COUNT 访问
public static final double COUNT = 10;

public static void main(String[] args){
final double PI = 3.14;
System.out.println(PI + Constants.COUNT);
System.out.println(PI + COUNT);
// 两个都输出 13.14
}
}

3.4 运算符

算术运算符 +、-、*、/、% 表示加、减、乘、除、求余(取模)运算

1
2
3
4
5
6
7
// 当两个整数参与 / 运算为整数除法
int a = 10 / 3;

// 有小数才为浮点除法
int b = 10 / 3.0;

// 整数除 0 产生异常,浮点数除 0 会的都无穷大或 NaN 结果

3.4.1 数学函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MathTest{
public static void main(String[] args){
// 平方根
double a = Math.sqrt(4);
// 幂运算
double b = Math.pow(2, 2);
// 整数取余,返回 0 ~ 11 内的数
int c = Math.floorMod(13, 12);
// 近似常量值
System.out.println(Math.PI);

System.out.println(a);
System.out.println(b);
System.out.println(c);

}
}

3.4.2 数值类型转换

int,long 转为 float 以及 long 转 double 会有精度损失。

二元运算中,两个操作数有一个 double,另一个也转 double;否则,有一个 float,另一个转 float;否则,有一个 long,另一个转 long;否则两个都转为 int。

3.4.3 强制类型转换

强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。

1
2
3
// double 转 int,会有精度损失
double a = 9.997;
int b = (int)a;

3.4.4 赋值运算符

1
2
// 算术运算符与赋值运算符结合使用,+=、-=、/=、*=、%=
int x += 4;

3.4.5 自增自减运算符

1
2
3
4
5
6
7
8
// 自增,++n、n++
int m = 7;
int n = 7;
int a = 2 * ++m; // 16
int b = 2 * n++; // 14


// 自减,--n、n--

3.4.6 关系运算符

==、!=、<、<=、>=、&&(短路与)、||(短路或)

  • expression1 && expression2,如果第一个表达式会 false,就不会再管第二个表达式
  • expression1 || expression2,如果第一个表达式会 true,就不会再管第二个表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OperatorTest{
public static void main(String[] args){
int a = 10;
int b = 11;
if(a > 10 && (b+=1) > 12){
}
System.out.println(a); // 10
System.out.println(b); // 11
if((a+=1) > 11 || (b+=1) > 11){
}
System.out.println(a); // 11
System.out.println(b); // 12
}
}

3.4.7 位运算

&(and)、|(or)、^(xor)、~(not),这些运算符按位模式处理。& 和 | 也能返回布尔值,不采用短路方式求值。

>>(逻辑右移,除 2),<<(逻辑左移,乘 2),>>>(算术右移,符号位填充高位),不存在 <<< 运算符。

3.4.8 运算符级别

运算符 结合性
[]、() 左 -> 右
!、~、++、–、+(一元运算)、-(一元运算)、()(强制类型转换)、new 右 -> 左
*、/、% 左 -> 右
+、- 左 -> 右
<<、>>、>>> 左 -> 右
<、<=、>、>=、instanceof 左 -> 右
==、!= 左 -> 右
& 左 -> 右
^ 左 -> 右
| 左 -> 右
&& 左 -> 右
|| 左 -> 右
?: 右 -> 左
=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、>>>= 右 -> 左

3.4.9 枚举类型

1
2
3
enum Size{ SMALL, MEDIUM, LARGE};

Size s = Size.SMALL;

3.5 字符串

字符串从概念上来说即是 Unicode 字符序列,使用双引号括起来,字符串都是 String 类的一个实例。字符串不可变,无法修改。

不可修改的优点就是编译器可以让字符串共享,存在一个字符串池,如果字符串相同则直接引用。

实际上只有字符串字面量是共享的,而 + 或 substring 等操作产生的字符串不共享。

3.5.1 子串

1
2
3
4
5
// String substring(int beginIndex) 截取 [beginIndex, arr.length - 1]
// String substring(int beginIndex, int endIndex) 截取 [beginIndex, endIndex)
String s = "Hello";

String sub = s.substring(1, 3); // "el"

3.5.2 拼接

1
2
3
4
5
6
// +
String s1 = "Hello" + ",World!";
String s2 = "Hello" + "123";

// String join()
String all = String.join("/", "S", "L","M");

3.5.3 检测相等

千万不要使用 == 运算符比较字符串是否相等,这个运算符是比较两个字符串的地址是否相等

1
2
3
4
5
// equals()

// equalsIgnoreCase() 忽略大小写

// 只有字符串常量是共享的,+ 或 substring 等操作产生的结果都不是共享的

3.5.4 空串与 Null

1
2
3
4
5
// 空串即长度为 0 的字符串

// String 是类对象,引用类型,可以赋值为 null,表示没有和任何对象关联
// 判断字符串不为空且不为空串,需要先判断 null
if(str != null && str.length() != 0)

3.5.5 码点与代码单元

1
2
3
4
5
6
7
8
// length() 获取的是字符串的代码单元长度

// charAt(int index) 获取 index 位置的代码单元

// 码点即 Unicode 码点,有些 Unicode 码点需要两个代码单元表示
// codePointAt(int index) 获取 index 位置的码点
// 遍历字符串打印码点
int[] codePoints = str.codePoints.toArray();

3.5.6 String API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// String 位于 java.lang.String
/*
1、返回给定位置的代码单元
char charAt()
2、返回从给定位置开始的码点
int codePointAt(int index)
3、返回从 startIndex 代码点开始,位移 cpCount 后的码点索引
int offsetByCodePoints(int startIndex, int cpCount)
4、按照字段顺序,比较,字符串位于 other 之前返回负数,相等返回 0,之后返回正数
int compareTo(String other)
5、将字符串的码点作为一个流返回,toArray 放入数组中
IntStream codePoints()
6、用数组中从 offset 开始的 count 个码点构造新字符串
new String(int[] codePoints, int offset, int count)
7、字符串相等返回 true
boolean equals(Object other)
8、忽略大小写比较,相等返回 true
boolean euqlasIgnoreCase(String other)
9、以 prefix 为前缀返回 true
boolean startsWith(String prefix)
10、以 suffix 为后缀返回 true
boolean endsWith(String suffix)
11、返回字符串 str 或 代码点 cp 匹配的第一个字串开始,这个位置从 0 或 fromIndex 计算,如果原字符串不存在 str 返回 -1
int indexOf(String str)
int indexOf(String str, int fromIndex)
int indexOf(int cp)
int indexOf(int cp, int fromIndex)
12、返回字符串长度
int length()
13、返回 [startIndex, endIndex) 的代码点数量
int codePointCount(int startIndex, int endIndex)
14、返回新字符串,newString 代替原字符串中的所有 oldString
String replace(CharSquence oldString, CharSequence newString)
15、返回新字符串,范围为 [beginIndex, endIndex)
String substring(int beginIndex)
String substring(int beginIndex, int endIndex)
16、返回新字符串,全转小写
String toLowerCase()
17、返回新字符串,全转大写
String toUpperCase()
18、返回新字符串,删除原字符串前后的空格
String trim()
19、返回新字符串,使用 delimiter 连接所有元素
String join(CharSequence delimiter, CharSequence... elements)
*/

3.5.7 构建字符串

每次连接字符串都是构建一个新的 String 对象,可以使用 StringBuilder 高效创建字符串。StringBuffer 有线程同步机制,但效率低,StringBuilder 线程不安全,效率高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringBuilderTest{
public static void main(String[] args){
StringBuilder builder = new StringBuilder();
builder.append("welcome");
builder.append(" new");
builder.append(" world");
builder.append(", CoreJava");

builder.delete(8, 11);
builder.insert(7, " to the");

System.out.println(builder.length());
String completedString = builder.toString();
System.out.println(completedString);
}
}

3.6 输入输出

3.6.1 读取输入

使用 Scanner 类完成键盘读取输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
1、用给定的输入流创建一个 Scanner 对象
Scanner(InputStream in)
2、读取输入的下一行内容
String nextLine()
3、读取输入的下一个单词(空格为分隔符)
String next()
4、读取并转换下一个表示整数或浮点数的字符序列
double nextDouble()
5、检测输入中是否还有其他单词
boolean hasNext()
6、检测是否还有表示整数或浮点数的下一个字符序列
boolean hasNextDouble()
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.*;

public class InputTest{
public static void main(String[] args){
Scanner in = new Scanner(System.in);

System.out.print("What's your name?");
String name = in.nextLine();

System.out.print("How old are you?");
int age = in.nextInt();

System.out.print("How much are your salary every month?");
double salary = in.nextDouble();

System.out.println("Hello," + name + ".Next year, you'll be " + (age+1) + ".And if you don't take any money, you'll save "+ (salary * 12));
}
}

3.6.2 格式化输出

使用 printf 进行格式化输出,也可以使用 String.format()。转换符表如下:

转换符 类型 举例
d 十进制整数 159
x 十六进制整数 9f
o 八进制整数 237
e 定点浮点数 15.9
g 通用浮点数
a 十六进制浮点数 0x1.fccdp3
s 字符串 Hello
c 字符 H
b 布尔 True
h 散列码 42628b2
tx、Tx 日期时间(T 强制大写) 已经过时的
% 百分号 %
n 与平台有关的行分隔符

3.7 控制流程

Java 没有 goto 语句,但是 break 语句可以带标签,达到从内存循环跳出的目的。

3.7.1 块作用域

一个块中可以嵌套另一个块,但是不能在嵌套的两个块中声明同名的变量。

1
2
3
4
5
6
7
8
9
10
public class BlockTest{
public static void main(String[] args){
int n = 1;
{
int k = 1;
int n = 2; // 错误: 已在方法 main(String[])中定义了变量 n
}
System.out.println(n);
}
}

3.7.2 条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 条件语句格式
if(condition) statement;

// 执行多条使用块语句
if(condition){
statement1;
statement2;
...
}

// if-else,else 和 离他最近的 if 结合在一起
if(condition) statement1 else statement2;

// if-else 执行多条语句
if(condition){
statemen1;
statemen2;
}else{
statement3;
statement4;
}

// if-else if-else
if(condition1){
statement1;
}else if(condition2){
statement2;
}else{
statement3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.*;

public class IfTest{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
System.out.println("How much are your salary?");
double salary = in.nextDouble();
double target = 4000;
String performance = "";
if(salary >= 2 * target){
performance = "Excellent";
}else if(salary >= 1.5 * target){
performance = "Fine";
}else if(salary >= target){
performance = "Satisfactory";
}
else{
System.out.println("You're fired");
}
System.out.println(performance + "~");
}
}

3.7.3 循环语句

1
2
3
4
5
// while 循环一般格式
while(condition) statement;

// do/while 循环
do statement while(condition);

3.7.4 for 循环

  • for 语句第一部分是计数器初始化,第二部分是循环条件,第三部分是如何更新计数器
  • for 语句内部定义的变量外部无法使用,每个独立的 for 语句可以定义同名变量
1
2
3
4
5
6
7
8
9
10
11
12
public class ForTest{
public static void main(String[] args){
for(int i = 1; i <= 10; i++){
System.out.print(i + " ");
}
System.out.println();

for(int i = 0; i < 10; i++){
System.out.print(i + " ");
}
}
}

3.7.5 switch 语句

switch 语句从选择项匹配的 case 标签处开始执行直到遇到 break 语句或执行到 switch 语句结尾处。如果没有匹配的而有 default 语句,就执行 default 子句。如果某个 case 分支没有 break 语句,就有可能继续执行下一个 case 分支。

case 标签可以是:

  • 类型为 char、byte、short、int 的常量表达式
  • 枚举常量
  • 字符串字面量(Java SE 7 开始)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SwitchTest{
public static void main(String[] args){
Scanner in = new Scanner(System.in);
System.out.println("请输入你想使用的功能:(1,2)");
String choice = in.next();
switch(choice){
case "1":
System.out.println("你选择了 1 功能,什么也没发生");
break;
case "2":
System.out.println("你选择了 2 功能,什么也没发生");
break;
default:
System.out.println("没有该功能...");
}
}
}

3.7.6 中断控制语句

  • break:跳出当前循环
  • break tag:跳出循环,从内到外,跳出语句块,到 tag 位置
  • continue:跳出当前循环,继续下一次循环

3.8 大数值

java.math 包中有 BigInteger 和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 实现任意精度的整数运算,BigDecimal 实现任意精度的浮点运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.math.*;

public class BigTest{
public static void main(String[] args){
BigInteger a = BigInteger.valueOf(100);
BigInteger b = BigInteger.valueOf(200);
// 加
System.out.println(a.add(b));
// 减
System.out.println(a.subtract(b));
// 乘
System.out.println(a.multiply(b));
// 除
System.out.println(a.divide(b));
// 取余
System.out.println(a.mod(b));
// 比较
System.out.println(a.compareTo(b));
}
}

// BigDecimal 中 divide 需要指定舍入方式,RoundingMode.HALF_UP 即四舍五入

3.9 数组

数组是一种数据结构,用来存储同一类型值的集合。通过整型下标可以访问数组的每一个值。声明数组时,需要指出数组类型和数组变量的名字,int[] a 或 int a[],初始化使用 new 运算符,int[] a = new int[10],其中 10 表示数组长度,不要求是常量,数组一但初始化长度就不能再修改大小。

  • 创建数字数组时,所有元素都初始化 0
  • 创建 boolean 数组时,元素都初始化为 false
  • 创建对象数组,元素都初始化为 null,表示还未存放对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
创建一个长度为 10 的数组,可访问的范围为 0 ~ 9,索引 0 开始
arr.length 可以获取数组的元素个数
*/
public class IntArrayTest{
public static void main(String[] args){
int[] arr = new int[10];
// 也可以创建并赋予初始值
// int[] arr = new int[] {1, 4, 9, 16}
// int[] arr = {1, 4, 9, 16}

// 初始化数组
for(int i = 0; i < arr.length; i++){
arr[i] = i * i;
}

// arr[i] 通过索引取值,并打印出来
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i] + " ");
}
}
}

3.9.1 数组遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.*;

public class IntArrayTest{
public static void main(String[] args){
int[] arr = {1, 2, 3, 4, 5, 6, 7};

// 常规 for 循环
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i] + " ");
}
// 增强 for 循环
for(int a: arr){
System.out.print(a + " ");
}
// Arrays.toString()
System.out.println(Arrays.toString(arr));
}
}

3.9.2 数组拷贝

  • int[] a = new int[10]; int[] b = a; b 和 a 引用的是一个同一个数组
  • 如果需要所有值拷贝到新数组则需要使用 Arrays.copyOf() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.*;

public class IntArrayTest{
public static void main(String[] args){
int[] arr = {1, 2, 3, 4, 5, 6, 7};
int[] arr1 = arr;
// 两个打印为一个地址,即指向同一个数组引用,浅拷贝
System.out.println(arr);
System.out.println(arr1);
arr1[0] = 100;
// 修改 arr1 即修改 arr
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr1));

// 第二个参数为新数组的长度,大于原数组就默认初始化,小于就裁剪原数组
int[] arr2 = Arrays.copyOf(arr, 2 * arr.length);
int[] arr3 = Arrays.copyOf(arr, 3);
System.out.println(Arrays.toString(arr2));
System.out.println(Arrays.toString(arr3));
System.out.println(arr2);
System.out.println(arr);
arr2[0] = 100;
// 两个数组没有指向一个数组,因此单个修改不印象原数组,深拷贝
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arr2));

}
}

3.9.3 命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ArgsTest{
public static void main(String[] args){
for(int i = 0; i < args.length; i++){
System.out.println("args[" + i + "]: " + args[i]);
}
}
}

/*
$ java ArgsTest -h hello -a -b
args[0]: -h
args[1]: hello
args[2]: -a
args[3]: -b
*/

3.9.4 数组排序

可以使用 Arrays.sort(),对数组进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Arrays 中的常用方法
1、返回数组字符串
static String toString(type[] a)
2、拷贝数组,length 大于 a.length 则初始化,小于则只截取 length 长度。[start, end)
static type copeOf(type[] a, int length)
static type copeOfRange(type[] a, int start, int end).
3、使用优化的快速排序对数组进行排序
static void sort(type[] a)
4、二分查找,原数组必须有序,查找成功返回对应下标,查找失败返回负数 r,-r-1 则是查找元素可插入的位置
static int binarySearch(type[] a, type v)
static int binarySearch(type[] a, int start, int end, type v)
5、填充数组
static void fill(type[] a, type v)
6、如果两个数组大小相同,下标相同的元素也相等,就返回 true
static boolean equals(type[] a, type[] b)
*/

3.9.6 多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 声明二维数组
double[][] a;

// 初始化
double[][] a = nwe double[10][10];

double[][] a = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
}

// 遍历二维数组
import java.util.*;

public class MultiArrayTest{
public static void main(String[] args){
int[][] a = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

for(int i = 0; i < a.length; i++){
for(int j = 0; j < a[i].length; j++){
System.out.print(a[i][j] + " ");
}
System.out.println();
}

for(int[] nums: a){
for(int num: nums){
System.out.print(num + " ");
}
System.out.println();
}

System.out.println(Arrays.deepToString(a));

}
}

3.9.7 不规则数组

Java 实际上没有多维数组,只有一维数组。多维数组其实是“数组的数组”,a[i] 处存放的数组的引用,因此可以方便的构造“不规则”数组,即数组的每一行有不同的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

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

for(int[] nums: arr){
for(int num: nums){
System.out.print(num + " ");
}
System.out.println();
}
}
}
/*
0
0 0
0 0 0
0 0 0 0
0 0 0 0 0
*/

4 对象与类

4.1 OOP 概述

4.1.1 类

类是构造对象的模板或蓝图,由类构造对象的过程成为创建类的实例。

封装是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域,操纵数据的过程称为方法。封装的关键在于绝不能让类中的方法直接访问其他类的实例域。

继承是扩展一个已有的类,并且新类具有所扩展类的全部属性和方法,并且新类可以提供新类的新方法和数据域。Java 中所有类都继承 Object。

4.1.2 对象

  • 对象的行为 —— 可以对对象应用哪些方法?
  • 对象的状态 —— 当调用方法时,对象如何响应?
  • 对象标识 —— 如何区分具有相同行为与状态的不同对象

每个对象都保存着描述当前特征的信息。对象的状态必须通过调用方法实现(如果不是通过方法调用能改变对象的状态,封装则被破坏了)。作为类的实例,每个类的标识永远不同。

4.1.3 识别类

识别类的简单规则时分析问题的过程中寻找名词,而方法对应着动词。在创建类的时候,哪些是名词和动词是重要的完全取决于个人的开发经验。

4.1.4 类之间的关系

  • 依赖(uses-a):一个类的方法操纵另一个类的对象。应该尽可能将相互依赖的类减至最小
  • 聚合(has-a):类 A 的对象包含类 B 的对象,比如 Order 对象包含 Item 类
  • 继承(is-a):类 A 扩展 类 B,则类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能

通常使用 UML(Unified Modeling Language)统一建模语言绘制类图,描述类之间的关系。

4.2 预定义类

4.2.1 对象与对象变量

使用对象之前,必须构造对象并指定其初始状态。通过构造器构造实例,构造器是一个特殊方法,用来构造并初始化对象。

构造器的名字应该与类名相同,构造对象需要在构造器前加上 new 操作符。构造的对象可以赋给变量对此使用,声明一个类变量时如果没有初始化对象则无法调用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// new 出来一个 Date 对象
new Date();

// 调用方法
new Date().toString();

// 赋给一个变量多次使用
Date date = new Date();

// 声明 Date 对象变量
Date date;
date.toString(); // error date 变量没有引用对象,无法调用方法

// 对象变量仅仅是引用一个对象,对象变量可以设为 null,表示没有引用任何对象
date = null;
// 局部变量不会自动初始化为 null,必须调用 new 或手动设置为 null 进行初始化

4.2.2 LocalDate 类

LocalDate 类采用熟悉的日历表示法。

  • 更改器方法,调用该方法对象状态会发生改变
  • 访问器方法,只访问对象而不修改对象的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* LocalDate 
1、构造一个表示当前日期的对象
static LocalTime now()
2、构造一个给定日期的对象
static LocalTime of(int year, int month, int day)
3、获取日期的年月日
int getYear()
int getMonthValue()
int getDayOfMonth()
4、获取当前星期几,使用 getValue 的到 1 ~ 7 的数字
DayOfWeek getDayOfWeek()
5、日期加减
LocalDate plusDays(int n)
LocalDate minusDays(int n)
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.time.*;

public class CalendarTest{
public static void main(String[] args){
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int day = date.getDayOfMonth();

date = date.minusDays(day - 1);
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();

System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for(int i = 1; i < value; i++){
System.out.print(" ");
}
while(date.getMonthValue() == month){
System.out.printf("%3d", date.getDayOfMonth());
if(date.getDayOfMonth() == day){
System.out.print("*");
}else{
System.out.print(" ");
}
date = date.plusDays(1);
if(date.getDayOfWeek().getValue() == 1){
System.out.println();
}
}
if(date.getDayOfWeek().getValue() != 1){
System.out.println();
}
}
}
/*
$ java CalendarTest
Mon Tue Wed Thu Fri Sat Sun
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19* 20 21 22 23 24
25 26 27 28 29 30 31
*/

4.3 自定义类

4.3.1 Employee 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.time.*;

public class Employee{
private String name;
private double salary;
private LocalDate hireDay;

public Employee(){
}

public Employee(String n, double s, int year, int month, int day){
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}

public String getName(){
return name;
}

public double getSalary(){
return salary;
}

public LocalDate getHireDay(){
return hireDay;
}

public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}
}

类的所有方法都标记为 public,意味着任何类的任何方法都能调用这个方法。

三个实例域都用 private 修饰,意味着只有 Employee 自身能访问这些实例域,其他类方法不能。强烈建议实例域标记为 private。

4.3.2 构造器

构造器总是伴随着 new 操作符被调用,而不能对一个已经存在的对象调用构造器到达重新设置实例域的目的。

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有 0 个、1 个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着 new 操作符一起调用
  • 类默认会有一个无参数构造器方法,当定义了有参数的构造器方法,默认的无参构造器就无了,需要显式定义

4.3.3 隐式参数和显式参数

在每一个方法中,关键字 this 代表隐式参数,代表当前对象

1
2
3
4
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}

4.3.4 封装的优点

实例域进行私有化,提供域访问器和域更改器方法有两个优点,一除了类的方法之外,不会影响其他代码。二更改器方法能执行错误检查。不要返回引用可变的对象的访问器方法,如果需要可变对象引用应该克隆。

不要编写返回可变对象引用的访问器方法。若需要返回一个可变对象的引用,应将其克隆返回。

4.3.5 私有方法

将方法的修饰为 private,外部则无法调用,类的辅助方法通常不需要在外部调用声明为私有方法

4.3.6 final 实例域

将实例域定义为 final,构建对象时必须初始化这样的域,且后面的操作无法修改该域。

4.4 静态域和静态方法

4.4.1 静态域

如果实例域定义为 static,那么每个类只有一个这样的域,每个对象的所有实例域都有自己的一份拷贝。它属于类,不属于任何对象。

4.4.2 静态常量

static final 修饰,例如 Math.PI,System.out。

4.4.3 静态方法

静态方法是一种不能向对象实施操作的方法。建议使用类名调用静态方法不造成混淆。以下两种情况使用静态方法。

  • 一个方法不需要访问对象状态,其所需参数都是以通过显式参数提供
  • 一个方法只需要访问类的静态域

4.4.4 工厂方法

静态工厂方法,用于构造不同的对象。

4.4.5 main 方法

main 方法不对任何对象进行操作,静态 main 方法将执行并创建程序所需要的对象。main 可用来做单元测试。

4.5 方法参数

Java 中,总是采用按值调用。方法得到的是所有参数的拷贝。

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

4.6 对象构造

4.6.1 重载

多个方法,相同的名字、不同的参数,便产生了重载。要完整的描述一个方法,需要指定方法名和参数类型,返回类型不是其中的一部分,因此不能有两个名字相同、参数类型相同但返回类型不同的方法。

4.6.2 默认域初始化

如果构造器中没有给实例域初始化,则自动初始化:数值为 0、布尔为 false、对象引用为 null。

4.6.3 显式域初始化

在实例域定义时就给其赋一个值

1
2
3
class Employee{
private String name = "";
}

4.6.4 this 使用

1
2
3
4
5
6
7
8
9
10
11
// 参数名与实例域相同
public Employee(String name, double salary){
this.name = name;
this.salary = salary;
}

// 调用另一个构造器
public Employee(double salary){
this("Employee #" + nextId, salary);
nextId++;
}

4.6.5 初始化块

首先执行初始化块,然后再运行构造器。

静态初始化块,在类第一次加载的时候,会进行静态域的初始化。

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

private int id;
private String name = "";
private double salary;

static{
System.out.println("静态初始化代码块");
nextId = new Random().nextInt(10000);
}

{
System.out.println("初始化代码块");
id = nextId;
nextId++;
}

public Employee(String name, double salary){
System.out.println(name + "两个参数的构造方法");
this.name = name;
this.salary = salary;
}

public Employee(double salary){
this("Employee #" + nextId, salary);
}

public Employee(){}

public String getName(){
return name;
}

public double getSalary(){
return salary;
}

public int getId(){
return id;
}
}

4.7 包

4.7.1 类的导入

1
2
3
4
5
6
7
8
9
10
11
12
13
// import
import java.util.*;

// 只能使用 * 导入 一个包,不能 *.*


// 命名冲突的问题,可以导入实际使用的确定包
import java.util.*;
import java.sql.*;
import java.util.Date;

// 也可以使用完全包名
new java.util.Date();

4.7.2 静态导入

导入静态方法和静态字段

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

public class StaticImportTest{
public static void main(String[] args){
int a = 10;
System.out.println(pow(a, 2));
}
}

4.7.3 组织类

类文件开头,写上 package 包名;

1
2
javac top/reajason/PayrollApp.java
java top.reajason.PayrollApp

4.8 类路径

unix:/home/user/classdir:.:/home/user/archives/archive.jar

windows:c:\classdir;.;c:\archives\archive.jar

类路径所列出的目录和归档文件是搜寻类的起始点,默认类路径包含 . (当前目录)。

Java 6 开始可在 JAR 文件目录指定通配符,例如(archives 中所有 JAR 文件都包含到类路径中):

windows:c:\classdir;.;c:\archives\*

java.lang 包被默认导入。

4.8.1 设置类路径

java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyApp

不要将 CLASSPATH 设置成全局变量。

4.9 文档注释

javadoc 能将源文件生成一个 HTML 文档。

javadoc 将在以下中抽取信息:

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 共有的和受保护的域

4.9.1 类注释

类注释必须放在 import 语句之后,类定义之前。

1
2
3
4
5
6
/**
* 类注释
*/
public class Card{

}

4.9.2 方法注释

每一个方法注释必须放在所描述的方法之前。

  • @param 变量描述,可占据多行,一个方法的所有 @param 标记必须放在一起
  • @return 描述,可占据多行
  • @throws 类描述,表示方法可能抛出的异常
1
2
3
4
5
6
7
8
9
10
/**
* 增加雇工的工资
* @param byPercent 为 10 则是增长 10%
* @return 返回增加后的工资
*/
public double raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}

4.9.3 域注释

只建立文档需要对公共域(通常指静态常量)

1
2
3
4
/**
* 利率
*/
public static final int RATES = 5;

4.9.4 通用注释

  • @author 姓名:可以使用多个
  • @version 文本:版本描述
  • @since 文本:引入特性的描述
  • @deprecated 文本:不再使用的注释,并给出建议
  • @see 引用:可以添加多个,但必须放在一起

4.9.6 包与概述注释

包注释的两种方式(在包目录添加一个单独的文件):

  1. 提供一个 package.html。在标记 <body></body> 之间的所有文本都会抽取出来。
  2. 提供一个 package-info.java 文件。文件开头即写文档注释后面是 package 语句。

概述:

创建一个名为 overview.html 文件,这个文件位于所有源文件的父目录中。标记 <body></body> 之间的所有文本都会抽取出来。

4.9.7 注释抽取

1
2
3
4
5
javadoc -d docDirectory nameOfPackage

javadoc -d docDirectory nameOfPackage1 nameOfPackage2

javadoc -d docDirectory *.java

4.10 类设计技巧

  1. 一定要保证数据私有
  2. 一定要对数据初始化,手动初始化
  3. 不要在类中使用过多的基本类型
  4. 不是所有的域都需要独立的域访问器和域更改器
  5. 将职责过多的类进行分解
  6. 类名和方法名能够体现它们的职责
  7. 优先使用不可变的类

5 继承

5.1 类、超类和子类

5.1.1 定义子类

关键字 extends 表示继承,超类和子类是 Java 程序员最常用的两个术语。子类比超类拥有更多的功能。将通用方法放在超类中,而将特殊扩展方法放在子类中。

1
2
3
public class Manager extends Employee{

}

5.1.2 覆盖方法

子类可覆盖超类的方法(同名,同参数),子类不能直接访问超类的私有域,可通过 super 关键字调用父类的方法。super 不是一个对象的引用,只是指示编译器调用超类方法的特殊关键字。

5.1.3 子类构造器

使用 super 调用超类构造器的语句必须放在子类构造器的第一条语句,如果超类没有不带参数的构造器,子类又没有显示调用超类的其他构造器,编译器则会报错。

一个对象变量可以指示多个实际类型的现象被称为多态,在运行时能够自动地选择调用哪个方法的现象称为动态绑定。

5.1.4 继承层次

由一个公共超类派生出来的所有类的集合称为继承层次,在继承层次中,某个特定类到其祖先的路径称为该类的继承链。Java 不支持多继承。

5.1.5 多态

继承关系 is-a 的另一表述是置换法则,程序中出现超类对象的任何地方都可以用子类对象置换。Java 中,对象变量是多态的。子类数组的引用可以转换成超类数组的引用,而无需强制类型转换。

如果方法或构造器由 private 或 static 或 final 修饰,那么编译器能准确知道调用哪个方法,这种调用方式称为静态绑定。由于动态绑定的机制,运行时,调用方法先查询当前类对象的方法,然后查询所继承超类的方法。

5.1.6 final 类和方法

用 final 修饰的类无法被继承,用 final 修饰的方法,子类无法覆盖。声明为 final 的主要目的是确保它们在子类中不会改变语义。

5.1.7 强制类型转换

  • 只能在继承层次内进行类型转换
  • 在将超类转换成子类之前,应该使用 instanceof 进行检查

一般情况下应该尽量少用类型转换和 instanceof 运算符

5.1.8 抽象类

使用关键字 abstract 声明一个抽象类和抽象方法。为了程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法外,抽象类还可以包含字段和具体方法。

  • 类即使不含抽象方法,也能声明为抽象类
  • 抽象类不能实例化
  • 可定义抽象类的对象变量指向非抽象子类的对象

扩展抽象类的两种选择:

  • 子类中任由部分抽象方法,即子类仍为抽象类
  • 子类定义全部的抽象方法,子类不再是抽象类

5.1.9 受保护访问

超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,可以将方法或域声明为 protected。

  • private —— 仅对本类可见
  • public —— 对所有类可见
  • protected —— 对本包和所有子类可见
  • 默认 —— 对本包可见

5.2 Object

Object 是 Java 中所有类的超类。

5.2.1 equals 方法

equals 方法是用于检测一个对象是否等于另一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Objects.equals(a, b), a,b 都为 null 返回 true,某一个为 null 返回 false
public class Employee{
...
public boolean equals(Object otherObject){
if(this == otherObject){
return true;
}

if(otherObject == null){
return false;
}

if(getClass() != otherObject.getClass()){
return false;
}

Employee other = (Employee)otherObject;
return Objects.euqals(name, other.name).
&& salary == other.salary
&& Objects.equals(hireDay, other.hireDay);
}

}

public class Manager extends Employee{
...
public boolean equals(Object otherObject){
// 子类先调用超类的 equals 方法检测
if(!super.equals(otherObject)){
return false;
}
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}

Java 语言规范要求 equals 方法具有以下的特性:

  1. 自反性:对于任意非空引用 x,x.equals(x) 返回 true
  2. 对称性:对于任何引用 x、y,y.equals(x) 返回 true,x.equals(y) 也要返回 true
  3. 传递性:对于任何引用 x、y、z,x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 也要返回 true
  4. 一致性:
  5. 对于任意非空引用 x, x.equals(null) 返回 false

编写一个完美的 equals 方法的建议:

  1. 显式参数命名为 otherObject

  2. 检测 this 与 otherObject 是否引用同一个对象

    if(this == otherObject) return true;

  3. 检测 otherObject 是否为 null

    if(otherObject == null) return false;

  4. 比较 this 与 otherObject 是否是同一个类

    • 如果 equals 的语义在每个子类都有改变,使用 getClass 检测

      if(getClass() != otherObject.getClass() return false;

    • 如果所有子类都使用统一的语义,就是用 instanceof 检测

      if(!(otherObject instanceof ClassName)) return false;

  5. 将 otherObject 转换为相应的类类型变量

    ClassName other = (ClassName)otherObject;

  6. 将所需要比较的域进行比较,基础类型使用 ==,对象引用使用 Objects.equals()

    return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;

子类如果重新定义 equals 方法,就要先调用 super.equals(OtherObject) 检测

5.2.2 hashCode 方法

散列码是由对象导出的一个整型值。如果重新定义了 equals 方法,就必须重新定义 hashCode 方法,以便将对象插入到散列表中。

1
2
// Objects.hash(Object... objects) 返回由提供对象的散列码组合而得到的散列码
// Objects.hashCode(Object a) 如果 a 为 null 返回 0,否则返回 a.hashCode()

5.2.3 toString 方法

它用于返回对象值的字符串。只要对象与一个字符串通过操作符 + 连接起来,Java 编译器就自动调用 toString 方法。Object 类定义的 toString 方法,用来打印对象所属的类名和散列码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package equals;

import java.time.*;
import java.util.Objects;

public class Employee{
private String name;
private double salary;
private LocalDate hireDay;

public Employee(String name, double salary, int year, int month, int day){
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}

public String getName(){
return name;
}

public double getSalary(){
return salary;
}

public LocalDate getHireDya(){
return hireDay;
}

public void raiseSalary(double byPercent){
double raise = salary * byPercent / 100;
salary += raise;
}

public boolean equals(Object otherObject){
if(this == otherObject){
return true;
}

if(otherObject == null){
return false;
}

if(getClass() != otherObject.getClass()){
return false;
}

Employee other = (Employee)otherObject;

return Objects.equals(name, other.name) && salary == other.salary
&& Objects.equals(hireDay, other.hireDay);
}

public int hashCode(){
return Objects.hash(name, salary, hireDay);
}

public String toString(){
return getClass().getName() + "[name=" + name + ",salary=" + salary
+ ",hireDay=" + hireDay + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package equals;

public class Manager extends Employee{
private double bonus;

public Manager(String name, double salary, int year, int month, int day){
super(name, salary, year, month, day);
bonus = 0;
}

public double getSalary(){
double baseSalary = super.getSalary();
return baseSalary + bonus;
}

public void setBonus(double bonus){
this.bonus = bonus;
}

public boolean equals(Object otherObject){
if(!(super.equals(otherObject))){
return false;
}
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}

public int hashCode(){
return super.hashCode() + 17 * Double.hashCode(bonus);
}

public String toString(){
return super.toString() + "[bonus=" + bonus + "]";
}
}

5.3 泛型数组列表

ArrayList 是一个采用类型参数的泛型类,为了指定数组列表中保存的类型需要使用一对尖括号将类名括起来加在后面,例如 ArrayList<Employee>。ArrayList 是一个动态扩容数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
1、构造一个空数组列表
ArrayList<E>()
2、用指定容量构造一个空数组列表
ArrayList<E>(int initialCapacity)
3、在数组列表尾端添加一个元素,永远返回 true
boolean add(E obj)
4、返回存储在数组列表中当前元素数量
int size()
5、将数组列表的存储容量削减到当前尺寸
void trimToSize()
6、设置指定位置的元素值
void set(int index, E obj)
7、获取指定位置的元素值
E get(int index)
8、指定位置插入元素,后面的元素向后移
void add(int index, E obj)
9、删除指定位置元素并返回,后面的元素向前移
E remove(int index)
*/
import java.util.*;

public class ArrayListTest{
public static void main(String[] args){
ArrayList<Employee> staff = new ArrayList<>();

staff.add(new Employee("Carl", 75000, 1987, 12, 15));
staff.add(new Employee("Harry", 50000, 1987, 12, 15));
staff.add(new Employee("Tony", 40000, 1990, 3, 15));

for(Employee e : staff){
System.out.println(e);
}
}
}

5.4 对象包装器

对象包装器是不可变的,一旦创建就无法改变其中的值,而且对象包装类是 final,不允许有子类。Integer 类对应的基本类型是 int,尖括号中的类型参数不允许是基本类型。

1
2
3
4
5
6
7
ArrayList<Integer> list = new ArrayList<>();

// 编译器会自动翻译为 list.add(Integer.valueOf(3)),称为自动装箱
list.add(3);

// 编译器会自动翻译为 list.get(i).intValue(),称为自动拆箱
int n = list.get(i)

自动装箱规范要求 boolean、byte、char <= 127,介于 -128 ~ 127 之间的 short 和 int 被包装到固定的对象中(常量池?)。

装箱和拆箱是编译器要做的事情,而不是虚拟机。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Integer 常用 API,其他包装类也实现了相应的方法
1、以 int 类型返回 Integer 对象的值
int intValue()
2、返回数值类型的字符串,默认十进制,也可指定进制
static String toString(int i)
static String toString(int i, int radix)
3、返回字符串 s 的整型数值
static int parseInt(String s)
static int parseInt(String s, int radix)
4、将 s 表示的整型数值进行初始化乘一个新的 Integer 对象
static Integer valueOf(String s)
static Integer valueOf(String s, int radix)
*/

5.5 可变数量参数

Object... 与 Object[] 完全一样 ,因此 main 方法可以改写为public static void main(String... args)

5.6 枚举类

在比较枚举类型的值时,永远不需要调用 equals,直接使用 == 即可。枚举类型中可以添加构造器、方法和域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
1、返回指定名字、给定类的枚举常量
static Enum valueOf(Class enumClass, String name)
2、返回枚举常量名
String toString()
3、返回枚举常量在 enum 声明中的位置,从 0 开始
int ordinal()
4、如果枚举常量在 other 之前,返回负值,相等返回 0,之后返回正值
int caompareTo(E other)
*/
public class EnumTest{
public static void main(String... args){
Size size = Enum.valueOf(Size.class, "SMALL");
System.out.println(size);
System.out.println(size.getAbbreviation());
System.out.println(size == Size.SMALL);
}
}

enum Size{
SMALL("S"), MEDIUM("M"), LARGE("L");

private Size(String abbreviation){
this.abbreviation = abbreviation;
}

public String getAbbreviation(){
return abbreviation;
}

private String abbreviation;
}

5.7 反射

能够分析类能力的程序称为反射,反射机制可以用来:

  • 在运行时分析类的能力
  • 在运行时查看对象
  • 实现通用数组操作代码
  • 利用 Method 对象

5.7.1 Class 类

在运行期间,Java 运行时系统始终为所有对象维护一个称为运行时的类型标识,保存这个这些信息的类被称为 CLass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class ClassTest{
public static void main(String[] args) throws Exception{

Integer i = 10;
// getClass()
Class cl1 = i.getClass();
String className = "java.lang.Integer";
// Class.forName()
Class cl2 = Class.forName(className);
// 类.class
Class cl3 = Integer.class;

System.out.println(cl1);
System.out.println(cl2);
System.out.println(cl3);

}
}

5.7.2 分析类的能力

java.lang.relect 包中有三个类 Field、Method 和 Constructor 分别用于描述类的字段、方法和构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Class
1、获取类的 Field 对象数组
Field[] getFields() // 获取这个类或其超类的公共字段
Field getField(String name)
Field[] getDeclareFields() // 获取这个类的全部字段
Field getDeclareField(String name)
2、获取类的 Method 对象数组
Method[] getMethods()
Method[] getDeclareMethods()
3、获取类包名
String getPackageName()
4、获取数组类型
Class getComponentType()
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Field\Method\Constructor
1、返回当前对象所在的 Class 对象
Class getDelcaringClass()
2、返回修饰符
int getModifiers()
String getModifiers().toString()
3、返回名字
String getName()
4、获取方法参数
Class[] getParameterTypes()
5、获取 Method 对象返回值类型
Class getReturnType()
*/

访问私有字段、方法、构造器时,需要设置 setAccessible 为 true。

5.7.3 反射使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1、通过 forName 获取 Class 对象
Class clazz = Class.forName("top.reajason.test.Student");

// 获取公共无参构造 getDeclaredConstructor() 能获取到私有的
Constructor constructor1 = clazz.getConstructor();
System.out.println(constructor1);

// 获取公共有参构造 getDeclaredConstructors() 能获取到私有的
Constructor constructor2 = clazz.getConstructor(String.class, int.class);
System.out.println(constructor2);

// 获取所有公共构造方法的数组(无法获取私有的) getDeclaredConstructors() 能获取到私有的
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}

// 创建 Student 实例
Student s1 = (Student) constructor1.newInstance();
Student s2 = (Student) constructor2.newInstance("你好", 13);
System.out.println(s2); // Student{name='你好', age=13}

// 使用私有化需要取消访问检查(暴力反射)
constructor.setAccessible(true);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
Field[] getFields() 获取所有公共成员变量对象的数组
Field[] getDeclaredFields(); 获取所有成员变量对象的数组
Field getField(String name); 获取单个公共的成员变量对象
Field getDeclaredField(String name); 获取单个成员变量对象
int getModifuers(); 获取修饰符值
getType(); 属性的类型
*/

// void set(Object obj, Object value) 给指定对象的成员变量赋值

// Object get(Object obj) 获取指定对象的 Field 的值

Class clazz = Class.forName("top.reajason.test.Student");
Constructor constructor = clazz.getConstructor(String.class, int.class);
Student s1 = (Student) constructor.newInstance("xiaobai", 23);
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);
System.out.println(name.get(s1)); // xiaobai
name.set(s1, "nitama");
System.out.println(name.get(s1)); // nitama

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
Method[] getMethods() 获取所有公共成员方法对象的数组,包括继承的
Method[] hetDeclaredMethods() 获取所有成员方法对象的数组,不包含继承的
Method getMethod(String name, Class<?>...parameterTypes) 获取单个公共成员方法对象
Method hetDeclaredMethod(String name, Class<?>...parameterTypes) 获取单个成员方法对象
getName(); 获取方法名称
getModifiers(); 获取修饰符值
getReturnType(); 获取返回类型
getParameterTypes(); 获取参数类型数组
*/

// Object invoke(Object obj, Object...args) 调用方法

Class clazz = Class.forName("top.reajason.test.Student");
Student s1 = (Student) clazz.getConstructor(String.class, int.class).newInstance("xioabai", 13);
Method method = clazz.getMethod("getAge");
Object result = method.invoke(s1);
System.out.println(result); // 13

5.8 继承技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现 is-a 关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态而非类型信息
  7. 不要过多的使用反射

6 接口、lambda 表达式、内部类

6.1 接口

6.1.1 接口概念

接口不是类,而是对类的一组需求的描述。接口的所有方法默认是 public 而无需指定。接口中不能有实例域,Java SE8 之前不能在接口中实现方法。

类实现一个接口的两个步骤:

  • 让类声明为实现给定的接口,使用 implements
  • 对接口中的所有方法进行定义,实现接口时必须声明为 public

6.1.2 接口的特性

接口不是类,不能使用 new 实例化一个接口,但是可以声明接口变量,指向实现了接口的类对象,也可以使用 instanceof 检测一个对象是否实现了某个接口,接口可以扩展接口,接口中不能包含实例域或静态方法,但是可以包含常量 public static final,一个类只能拥有一个超类,但是可以实现多个接口。

Java SE 8 中,允许接口中增加静态方法,通常放在伴随类中

可以使用 default 声明默认方法,提供默认实现,主要用来接口演化升级,默认方法冲突的两种情况:

  1. 一个类实现了多个接口,并且多个接口有共同方法,此时需要类自己实现这个方法,解决冲突
  2. 一个类继承的超类和实现的接口中有重名方法,类优先原则,会自动忽略接口的方法。

6.2 接口示例

6.2.1 Comparator 接口

对一个对象数组进行排序的前提是这个对象必须是 Comparable 接口的类的实例。Arrays.sort 方法有两个版本一个是传入单个数组,一个是数组加一个比较器,比较器就是实现了 Comparator 接口的类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.*;

public class CompareStringTest{
public static void main(String[] args){
String[] friends = {"Peter", "Paul", "Mary", "ReaJason", "Silly"};

// 使用 String 类实现了 Comparable 接口的 compareTo 方法进行比较(按照字典顺序)
Arrays.sort(friends);
// ASCII 先后顺序
System.out.println(Arrays.toString(friends));

// 自定义比较器,进行排序
Arrays.sort(friends, new LengthComparator());
// 根据字符串的长度
System.out.println(Arrays.toString(friends));

}
}


class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.length() - second.length();
}
}

6.2.2 Cloneable 接口

Cloneable 接口是 Java 提供的一组标记接口之一,Object 的 clone 方法是 protected,因此支只支持子类调用 clone 方法克隆它自己的对象,必须重新定义为 clone 为 public 才能允许所有方法克隆对象。

  • 实现 Clonable 接口
  • 重新定义 clone 方法,并指定 public 访问修饰符

深拷贝需要拷贝可变实例域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee implements Cloneable{
private String name;
private double salary;
private Date hireDay;

...

public Employee clone() throws CloneNotSupportedException{
Employee cloned = (Employee)super.clone();

cloned.hireDay = (Date)hireDay.clone();

return cloned;
}
}

6.3 lambda 表达式

lambda 表达式是一个可传递的代码块,之后可以执行一次或多次。

6.3.1 lambda 表达式语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 声明参数类型,返回一行代码的执行结果
(String first, String second) ->
first.length() - second.length()

// 声明参数类型,代码块,返回结果,每一个分支都必须返回结果
(String first, String second) -> {
if(first.length() < second.length()){
return -1;
}else if(first.length() > second.length()){
return 1;
}else{
return 0;
}
}

// 无参数
() -> {
for(int i = 0; i < 10; i++){
System.out.println(i);
}
};

// 如果参数类型可推导则省略,如果只有单个参数且类型可推导,可省略括号

6.3.2 函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式,这种接口称为函数式接口。

1
2
3
4
5
6
7
// Comparator 就是一个函数式接口
Arrays.sort(words, (first, second) -> {
first.length() - second.length()
});

// java.util.function 中有许多函数式接口
list.removeIf(e -> e==null);

6.3.3 方法引用

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

System.out::println 等价于 x -> System.out.println(x)

String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y)

this\super:this::equals、super::greet

构造器引用:Person::new、Person[]::new

6.3.6 变量作用域

lambda 表达式看可以捕获外围作用域中的变量,且是最终变量(final,初始化之后不会再赋给新值),不过只能引用而不能修改。

6.3.7 Comparator

Comparator 接口中包含了静态方法创建比较器,camparing 方法即是一个键提取器函数。p242

1
2
3
4
5
6
7
8
9
10
11
// 比较名字
Arrays.sort(people, Comparator.comparing(Person::getName));

// 比较姓再比较名
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName());

// 比较名字长度
Arrays.sort(people, Comparator.compringInt(p -> p.getName().length()));

// null 默认比较器
Arrays.sort(people, compring(Person::getMiddleName(), nullFirst(naturalOrder()));

6.4 内部类

内部类是定义在另一个类中的类

  • 内部类方法可以访问该类定义所在的作用域中的数据
  • 内部类可以对同一个包中的其他类隐藏起来
  • 当想要定义一个回调函数且不想编写大量代码就可以使用匿名内部类

内部类中声明的所有静态域必须是 final,内部类不能有 static 方法(可以有但是不要写)

6.4.1 内部类的特殊语法

OuterClass.this 表示外围类的引用

OuterClass.InnerClass 在外围类作用于之外,引用内部类

6.4.2 局部内部类

在方法中定义局部内部类,局部内部类不能用 public 或 private 修饰,它的作用域被限定在这个局部内部类所在的块中。局部内部类访问局部变量必须是 final。

6.4.3 匿名内部类

没有名字的内部类,如果构造参数的小括号跟一个大括号,正在定义的就是匿名内部类

1
2
3
4
// 通用语法格式
new SuperType(construction parameters){
inner class methods and data
}

6.4.4 静态内部类

静态内部类不能访问外围类对象数据,静态内部类可以有静态域和方法,内部类不需要访问外围类对象的时候应该使用静态内部类。

6.5 代理

利用代理可以在运行时创建一个实现了一组给定接口的新类。

7 异常、断言、日志

7.1 处理错误

7.1.1 异常分类

所有异常都是由 Throwable 继承而来,又分为 Error 和 Exception。Error 描述了 Java 运行时系统的内部错误和资源耗尽错误。Exception 中分为 RuntimeException 和 其他异常 两个分支。Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受查异常,所有其他的异常称为受查异常。编译器将核查是否为所有的受查异常提供了异常处理器。

7.1.2 声明受查异常

下面四个情况应该抛出异常:

  1. 调用一个抛出受查异常的方法
  2. 程序运行过程中发现错误,使用 throw 抛出
  3. 程序出现错误
  4. Java 虚拟机和运行库出现的内部错误

一个方法有可能抛出多个受查异常类型,就需要在方法的首部使用 throws 列出所有的异常类,不应该声明从 RuntimeException 继承的非受查异常。

子类方法中应该比超类方法抛出更特定的异常,或者根本不抛出异常。如果超类没有抛出受查异常,子类也不能抛出受查异常。

7.1.3 创建异常类

  • 继承一个异常类
  • 编写一个构造器方法和一个带有详细描述信息的构造器
1
2
3
4
5
6
class FileFormatException extends IOException{
public FileFormatException(){}
public FileFormatException(String msg){
super(msg);
}
}

7.2 捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// try/catch 语句
try{
code
}catch(ExceptionType e){
handler for this type
}

// 捕获多个异常
try{
code
}catch(ExceptionType1 e){
handler for this type
}catch(ExceptionType2 e){
handler for this type
}

try{
code
}catch(ExceptionType1 | ExceptionType2 e){
handler for this type
}

7.2.1 再次抛出异常

1
2
3
4
5
6
7
8
catch(SQLException e){
Throwable se = new ServletException("database error");
se.iniiCase(e);
throw se;
}

// 获取原始的 e 错误
Throwable e = se.getCause()

7.2.2 finally

try 语句可以只有 finall 子句而没有 catch 语句, finnal 语句无论是否遇到异常都会执行,通常用于资源关闭。当 try 和 finally 中有 return 语句时,会走 finally 子句。

强烈建议使用 try/catch 和 try/finally 语句块

1
2
3
4
5
6
7
8
9
10
try{
try{

}finally{

}
}catch(){

}

7.2.3 try-with-resource

1
2
3
4
// 最简形式
try(Resource res = ...){

}

7.2.4 Throwable

1
2
3
4
5
6
7
8
9
10
11
12
/*
1、将这个对象设置为原因
Throwable initCause(Throwable cause)
2、获取产生这个异常的原因的异常对象
Throwable getCause()
3、获取构造这个对象时待用堆栈的跟踪
StackTraceElement[] getStackTrace()
4、为一个增加抑制异常
void addSuppressed(Throwable t)
5、获取异常的所有抑制异常
Throwable[] getSuppressed()
*/

7.3 使用异常机制技巧

  1. 异常处理不能代替简单的测试
  2. 不要过分细化异常
  3. 利用异常层次结构
  4. 不要压制异常
  5. 检查错误时,苛刻要比放任更好
  6. 不要羞于传递异常

7.4 断言

确信某个属性符合要求,并且代码的执行依赖这个属性,语法为 assert 条件assert 条件 : 表达式,表达式的目的时产生一个消息字符串。默认情况下,断言是被禁用的。

  • 开启断言,-enableassertions 或 -ea
  • 关闭断言,-disablesssertions 或 -da

断言只用于开发和测试阶段。

7.5 日志

7.5.1 日志对象

1
2
3
4
5
// 全局日志记录器
Logger.getGlobal().info("");

// getLogger 创建或获取记录器,声明为静态变量是防止被垃圾回收
private static final Logger myLogger = Logger.getLogger("top.reajason.corejava")

与包名类似,日志记录器名也具有层次结构,子记录器会继承父记录器的级别。7 个日志记录器级别如下:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST
1
2
3
4
5
6
7
// 设置日志级别,Level.ALL 开启所有级别,Level.OFF 关闭所有级别
logger.setLevel(Level.FINE);

// 记录日志
logger.waring(message);
logger.info(message);
logger.log(Level.FINE, message);

7.5.2 日志管理器配置

默认情况下,配置位于:jre/lib/logging.prperties

7.5.3 处理器

默认是 ConsoleHandler 控制台处理器

1
2
3
// 添加文件的处理器,还有其他处理器例如 StreamHandler
FileHandler handler = new FileHnadler();
logger.addHandler(handler);

自定义处理器需要扩展 Handler 类,并实现 publish、flush 和 close 方法。

7.5.4 过滤器

同一时刻只能有一个过滤器,通过实现 Fileter 接口并定义 isLoggable 方法自定义过滤器,使用 setFilter 方法添加过滤器。

7.5.5 格式化器

扩展 Formatter 类并实现 format 方法,进行格式化,使用 setFormatter 方法加入到处理器中。

7.6 调试技巧

  1. 打印或记录任意变量值
  2. 类中加入 main 方法进行单元测试
  3. 使用 JUnit 进行测试
  4. 日志代理
  5. 利用 Throwable 类提供的 printStackTrace 方法,打印堆栈情况,并重新抛出异常。
  6. 堆栈轨迹显示在 System.err 上,将错误信息保存在文件中
  7. 查看类的加载过程,使用 -verbose 启动虚拟机
  8. -Xlint 告诉编译器对普遍容易出现的代码问题进行检查
  9. jconsole processID 可以监控和管理程序
  10. jmap 可以获得堆的转储
  11. -Xprof 标志运行虚拟机,就会将进场被调用的方法的剖析信息发送到 System.out 中

8 泛型程序设计

泛型程序设计意味着编写的代码可以被很多类型的对象所重用。泛型提供了类型参数,使程序具有更好的可读性和安全性。

8.1 定义简单泛型类

泛型类就是具有一个或多个类型变量的类。

Java 中,E 表示集合的元素类型;K 和 V 分别表示表的关键字和值的类型;T(U 或 S)表示 “任意类型”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Pair<T>{
private T first;
private T second;

public Pair(){
first = null;
second = null;
}

public Pair(T first, T second){
this.first = first;
this.second = second;
}

public T getFirst(){
return first;
}

public T getSecond(){
return second;
}

public void setFirst(T value){
first = value;
}

public void setSecond(T value){
second = value;
}
}

类定义中的类型变量指定方法的返回类型以及域和局部变量的类型,可用具体的类型替换类型变量就可以实例化泛型类型。

1
2
3
4
5
6
7
8
9
10
Pair<String>;

/* 将 String 替换类型变量 T 得到 Piar 类
Pair<String>()
pair<String>(String, String)
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
*/

8.2 泛型方法

类型变量放在修饰符之后,返回值类型前,泛型方法可定义在普通类也可定义在泛型类中。

1
2
3
4
5
6
7
8
9
10
11
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}

// 调用时方法名前的尖括号放入具体类型
ArrayAlg.<String>getMiddle("John", "Q", "Public");

// 也可省略,后面的参数 String 足以推出 T 是 String
ArrayAlg.getMiddle("John", "Q", "Public");

8.3 类型变量的限定

<T extends BoundingType> 表示 T 应该是绑定类型的子类型,T 和绑定类型可以是类也可以是接口。一个类型变量或通配符可有多个限定,使用 & 分隔,限定中至多一个类,且类要放在第一个。

1
2
3
4
5
6
class ArrayAlg{
public static <T extends Comparable> Pair<T> minmax(T[] a){
...
return new Pair<>(a[0], a[a.length - 1]);
}
}

8.4 泛型代码与虚拟机

虚拟机没有泛型类型对象,所有对象都是普通类。

8.4.1 类型擦除

泛型类型都会自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定类型,替换为 Object)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Pair<T> 会变成以下情况
public class Pair{
private Object first;
private Object second;

public Pair(){
first = null;
second = null;
}

public Pair(Object first, Object second){
this.first = first;
this.second = second;
}

public Object getFirst(){
return first;
}

public Object getSecond(){
return second;
}

public void setFirst(Object value){
first = value;
}

public void setSecond(Object value){
second = value;
}
}

8.4.2 翻译泛型表达式

调用泛型方法或者存取泛型域时,编译器会自动插入强制类型转换。

8.4.3 翻译泛型方法

子类继承泛型类并且子类重写泛型类中的泛型方法制定了确定类型,为了防止类型擦除与多态发生冲突,编译器会在子类生成一个桥方法,虚拟机运行时会调用子类桥方法。

  • 虚拟机没有泛型,只有普通的类和方法
  • 所有参数类型都用它们的限定类型替换
  • 桥方法被合成用来保持多态
  • 为保持类型安全,必要时插入强制类型转换

8.4.4 调用遗留代码

设计泛型类型时,主要目标是运行泛型代码和遗留代码互操作。

@SuppressWarning("unchecked") 可以抑制警告

8.5 约束和局限性

8.5.1 不能用基本类型实例化类型参数

原因是类型擦除

8.5.2 运行时类型查询只适用于原始类型

虚拟机中的对象总是一个特定的非泛型方法,所以类型查询只产生原始类型。

8.5.3 不能创建参数化类型数组

类型擦除,会使 Pair<String>[] table 变成 Pair[] table,可以声明但是使用会有问题,会得到一个警告,可以使用注解抑制警告 @SuppressWarning("unchecked")@SafeVarargs

8.5.4 不能实例化类型变量

new T() 不能使用,因为类型擦除,T 变成 Object 了,可以使用构造器表达式解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用 Supplier<T> 表示一个无参数且返回值时 T 的函数
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get, constr.get());
}

// 调用
Pair<String> p = Pair.makePair(String::new);

// 使用反射
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.newInstance(), cl.newInstance());
}catch(Exception e){
return null;
}
}

// 调用
Pair<String> p = Pair.makePair(String.class);

8.5.5 不能构造泛型数组

8.5.6 泛型类的静态上下文中类型变量无效

8.5.7 不能抛出或捕获泛型类的实例

8.5.8 可以消除对受查异常的检查

8.5.9 注意擦除后的冲突

8.6 泛型类型的继承规则

Pair<Manager>Pair<Employee> 没有任何关系。可以将参数化类型转换为原始类型,泛型类可以扩展或实现其他的泛型类。

8.7 通配符类型

通配符类型,允许类型参数变化。Pair<? extends Employee> 表示类型参数是 Employee 的子类。Pair<? super Manager> 表示类型参数是 Manager 的超类。

? 表示无限定通配符。

9 集合

9.1 集合框架

集合接口与实现分离,集合有两个基本接口 Collection 和 Map。

9.1.1 Collection 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Iterator<E> iterator()
int Size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection<?> other)
boolean add(Object element)
boolean addAll(Collection<? extends E> other)
boolean remove(Object obj)
boolean removeAll(Collection<?> other)
default boolean removeIf(Predicate<? super E> filter)
void clear()
boolean retainAll(Collection<?> other)
Object[] toArray()
<T> T[] toArray(T[] arrayToFill)
*/

9.1.2 Iterator

迭代器认为是位于两个元素之间,remove 只能删除上次访问的元素,不能连续调用两次

1
2
3
4
5
/*
boolean hasNext()
E next()
void remove()
*/

9.2 具体的集合

集合类型 描述
ArrayList 一种可以动态增长和缩减的索引序列
LinkedList 一种可以在任何位置进行搞笑地插入和删除操作的有序序列
ArrayDeque 一种用循环数组实现的双端队列
HashSet 一种没有重复元素的无序集合
TreeSet 一种有序集
EnumSet 一种包含枚举类型的值
LinkedHashSet 一种可以记住元素插入次序的集
PriorityQueue 一种允许高效删除最小元素的集合
HashMap 一种存储键值关联的数据结构
TreeMap 一种键值有序排列的映射表
EnumMap 一种键值属于枚举类型的映射表
LinkedHashMap 一种可以记住键值项添加次序的映射表
WeakHashMap 一种其值无用武之地后可以被垃圾回收回收的映射表
IdentityHashMap 一种用 == 而不是用 equals 比较键值的映射表

9.2.1 链表

插入和删除操作高效,ListIterator 继承于 Iterator 支持添加、修改值和反向遍历。Java 设计上不合理,不要使用 get 获取链表上的元素,每次都需要从头遍历,应该使用迭代器。LinkedList 继承于 List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* List
ListIterator<E> listIterator()
ListIterator<E> listIterator(int index)
void add(int i, E element)
void addAll(int i, Collection<? extends E> elements)
E remove(int i)
E get(int i)
E set(int i, E element)
int indexOf(Object emelent)
int lastIndexOf(Object element)
*/

/* ListIterator
void add(E newElement)
void set(E newElement)
boolean hasPrevious()
E previous()
int nextiIndex()
int previousIndex()
*/

/*
LinkedList()
LinkedList(Collection<? extends E> elements)
void addFirst(E element)
void addLast(E element)
E getFirst()
E getLast()
E removeFirst()
E removeLast()
*/

9.2.2 数组链表

ArrayList 继承于 List,可以随机遍历数组

9.2.3 散列集

HashSet 继承于 Set,没有重复元素的集合

1
2
3
4
5
6
/*
HashSet()
HashSet(Collection<? extends E> element)
HashSet(int initialCapacity)
HashSet(int initialCapacity, float loadFactor)
*/

9.2.4 树集

TreeSet 有序集合,排序使用的红黑树结构,要使用树集,元素必须实现 Comparable 接口,或构造集时提供 Comparator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* TreeSet
TreeSet()
TreeSet(Comparator<? super E> comparator)
TreeSet(Collection<? extends E> elements)
TreeSet(SortedSet<E> s)
*/

/* SortedSet
Comparator<? super E> comparator()
E first()
E last()
*/

/* NavigableSet
E higher(E value)
E lower(E value)
E ceiling(E value)
E floor(E value)
E pollFirst()
E pollLast()
Iterator<E> descendingIterator()
*/

9.2.5 队列

双端队列,高效地在头部和尾部同时进行添加或删除元素,不支持在队列中间添加元素,ArrayDeque 和 LinkedList 有实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Queue 队列
boolean add(E element)
boolean offer(E element)
E remove()
E poll()
E element()
E peek()
*/

/* Deque 双端队列
void addFirst(E element)
void addLast(E element)
boolean offerFirst(E element)
boolean offerLast(E element)
E removeFirst()
E removeLast()
E pollFirst()
E pollLast()
E getFirst()
E getLast()
E peekFirst()
E peekLast()
*/

9.2.6 优先级队列

堆结构,小根堆,大根堆

1
2
3
4
5
/*
PriorityQueue()
PriorityQueue(int initialCapcity)
PriorityQueue(int initialCapcity, Comparator<? super E> c)
*/

9.3 映射

HashMap 和 TreeMap 都实现了 Map 接口。键必须是唯一的

9.3.1 基本映射操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Map
V get(Object key)
default V getOrDefault(Object key, V defaultValue)
V put(K key, V value)
void putAll(Map<? extends K, ? extends V> entries)
boolean containsKey(Object key)
boolean containsValue(Object value)
default void forEach(BiConsumer<? super K, ? super V> action)
*/

/* HashMap
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
*/

/* TreeMap
TreeMap()
TreeMap(Comparator<? super K> c)
TreeMap(Map<? extends K, ? extends V> entries)
TreeMap(SortedMap<? extends K, ? extends V> entries)
*/

9.3.2 更新映射项

1
2
3
4
/* Map
default V merge(K key, V value, BitFunction<? super V, ? super V,? extends V> remappingFunction)
default V compute(K key, BitFunction<? super K, ? super V, ? extends V> remappingFunction)
*/

9.3.3 映射视图

映射的视图(实现了 Collection 接口或某个子接口的对象)有三种:键集、值集合以及键值对集。

1
2
3
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K,V>> entrySet();

9.3.4 弱散列映射

WeakHashMap,当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同删除键值对。使用弱引用保存键。

9.3.5 链接散列集与映射

LinkedHashSet 和 LinkedHashMap 能记住插入元素的顺序。

9.3.6 枚举集与映射

EnumSet内部用位序列实现。EnumMap 是一个键类型为枚举类型的映射

9.3.7 标识散列映射

IdentityHashMap 使用 == 而不是 equals 比较两个对象

9.4 视图与包装类

keySet 方法返回一个实现 Set 接口的类对象,这个类的方法对原映射进行操作,这种集合称为视图。

9.4.1 轻量集合包装器

1
2
3
4
5
6
7
8
9
/* Arrays 返回数组列表视图
List<E> asList(E... array)
*/

/* Collections,获取不可修改集合
static <E> List<E> nCopies(int n, E value)
static <E> Set<E> singleton(E value)
static <E> List<E> singletonList(E value)
*/

9.4.2 子范围

1
2
3
4
5
6
7
8
/* List
subList(int firstIncluded, int firstExcluded)
*/

/*
subSet()
subMap()
*/

9.4.3 不可修改的视图

Collections 有方法获取集合不可修改视图,如果尝试修改则抛出异常。

1
2
3
4
/*
static <E> Collection unmodifiableCollection(Collection<>E c)
......
*/

9.4.4 同步视图

使用视图机制确保常规集合的线程安全。

1
2
3
4
/*
static <E> Collection<E> synchronizedCollection(Collection<E> c)
......
*/

9.4.5 受查视图

受查视图可以探测到集合不能探测到的代码问题,受查视图受限于虚拟机可以运行的运行时检查

1
2
3
4
/*
static <E> Collection<E> checkedCollection(Collection<E> c)
......
*/

9.5 算法

9.5.1 排序与混排

Arrays.sort()

9.5.2 二分查找

Collections.binarySearch()

9.5.3 Collections 其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Collections
T min(Collection<T> elements)
T max(Collection<T> elements)
void copy(List<? super T> to, List<T> from)
void fill(List<? super T> l, T value)
void swap(List<?> l, int i, int j)
void reverse(List<?> l)
旋转列表元素
void rotate(List<?> l, int d)
获取与 o 相同元素个数
int frequency(Collection<?> c, Object o)
两集合没有共同元素返回 true
boolean disjoint(Collection<?> cl, Collection<?> c2)
*/

9.5.4 集合与数组转换

数组转集合,Arrays.asList()

集合转数组,list.toArray() 返回 Object[],转为特定类型需要使用 list.toArray(new String[0]) 或 list.toArray(new String[list.size()]) 这种不会创建新数组

9.5.5 编写自己的算法

集合声明时应该尽可能使用接口而非具体的实现,返回集合的方法,可能还要返回接口,而不是返回类。

9.6 遗留的集合

9.6.1 Hashtable

Hashtable 与 HashMap 作用一样

9.6.2 枚举

hasMoreElements 和 nextElement 与迭代器的 hasNext 和 next 方法相似。

9.6.3 属性映射

  • 键值都是字符串
  • 表可以保存到文件,也可以从文件加载
  • 使用一个默认的辅助表
1
2
3
4
5
6
7
8
9
10
/* Properties
Properties()
Properties(Properties defaults)
String getProperty(String key)
String getProperty(String key, String defaultValue)
从输入流中加载属性映射
void load(InputStream in)
将属性映射存储到输出流中
void store(OutputStream out, String commentString)
*/

9.6.4 栈

1
2
3
4
5
/* Stack
E push(E item)
E pop()
E peek()
*/

9.6.5 位集

BitSet 存放一个位序列,高效

1
2
3
4
5
6
7
8
9
10
11
/*
BitSet(int initialCapacity)
int length()
boolean get(int bit)
void set(int bit)
void clear(int bit)
void add(BitSet set)
void or(BitSet set)
void xor(BitSet set)
void andNot(BitSet set)
*/

13 部署 Java 应用程序

13.1 JAR 文件

13.1.1 创建 JAR 文件

jar cvf JARFileName File1 File2

13.1.2 清单文件

jar cfm JARFileName MainifestFileName ...

13.1.3 可执行 JAR

使用 e 指定程序入口,或在清单中国指定

jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass ...

启动 jar:jar -jar MyProgram.jar

13.1.4 资源

文件的自动装载是利用资源加载特性完成的。

13.1.5 密封

在清单中加入 Sealed: true 则指定密封

13.2 应用首选项的存储

13.2.1 属性映射

使用 properties 存储属性,获取主目录:System.getProperties("user.home")

13.2.2 首选项 API

Preferences


CoreJava Volume Ⅰ
https://reajason.vercel.app/2021/11/08/CoreJava/
作者
ReaJason
发布于
2021年11月8日
许可协议