以 Java 作为语言,贯穿语言、框架、数据库、缓存、运维等整个后端架构。这一篇作为起始篇,目的是要初学者即使摆脱书本,也能从零掌握整个后端生态。你需要的,就是和我一起走完整个旅途。感谢陪伴~
1. Java 简介
-
Java 语言概述
- Java 是一门面向对象的、安全的、跨平台的静态语言。
-
Java 中 JDK、JRE、JVM 三者之间的关系
- JDK 是 Java 开发工具包,它包含了 JRE、JAVA 工具(编译工具 javac.exe 和打包工具 jar.exe)以及 Java 基础类库。主要用于程序员开发应用。
- JRE 是 Java 的运行时环境,它包含了 JVM 以及 Java 核心类库。只能运行 class 而没有编译功能。
- JVM 是 Java 虚拟机,它负责执行 Java 代码。
-
Java 中 public class 与 class
- 一个文件中可以含有多个 class ,但只能含有一个 public class 。
- public class 的名称必须与文件名相同。
- public class 修饰的类名可以被其他包访问。class 修饰的类名只能用于当前包中。
- public class 必须包含 main() 方法。一个文件只有 class 时,则可以在任意类中放置 main() 方法。
-
Java 标识符与关键字
- Java 标识符由字母、数字、美元符号 $ 、下划线 _ 组成,且第一个字符不能是数字。其中字母区分大小写。
- 包名所有字母必须小写。类名和接口名的每个单词首字母要大写。常量名所有字母大写,单词之间用下划线连接。变量名和方法名的第一个单词首字母小写,第二个单词起首字母大写。
- Java 共有 51 个关键字,以及 11 个保留字,这些关键字和保留字均不能作为标识符使用。
- 关键字 之 数据类型: boolean、int、long、short、byte、float、double、char、class、interface、enum、void
- 关键字 之 流程控制: if、else、do、while、for、switch、case、default、break、continue、return
- 关键字 之 异常处理: try、catch、finally、throw、throws
- 关键字 之 修饰符: public、protected、private、final、void、static、strict、abstract、transient、synchronized、volatile、native
- 关键字 之 类与类之间关系: extends、implements
- 关键字 之 建立实例及引用实例: this、super、instanceof、new
- 关键字 之 导包: package、import
- 保留字: goto、const、byValue、cast、future、generic、inner、operator、outer、rest、var
- 显式常量值,既不属于关键字,也不属于保留字,它们同样不能作为标识符使用。
- 显式常量值: true、false、friendly、null
-
Java 变量与常量
- 变量和常量的命名就是 Java 标识符。
- 变量和常量必须先声明后使用。
- 低级变量可以直接转换为高级变量。
- 使用 fianl 关键字来定义常量。
- 声明常量时要同时赋予一个初始值。
-
Java 数据类型
- 基本数据类型
- 整数类型: byte、short、int、long
- 浮点数类型: float、double
- 字符类型: char
- 布尔类型: boolean
- 基本数据类型对应的包装类
- boolean -> Boolean
- char -> Character
- byte -> Byte
- short -> Short
- int -> Integer
- long -> Long
- float -> Float
- double -> Double
- char[] -> String
- 数据类型自动转换
- 较大的类型(如 int)要保存较小的类型(如 byte)时,取值范围是足够的,不需要强制转换。
- 若要把 String 类型的数据,强制转换为基本数据类型,可以使用基本数据类型对应的包装类实现
- 数据类型提升规则
- byte、short、char 将被提升到 int
- 基本数据类型
-
Java 运算符
- 注意优先级,不能确定优先级的,请使用括号。
-
Java 表达式
- 又常量、变量 或 其他操作数与运算符所组合而成。
- 当一个表达式中包含多个基本数据类型的值时,整个算术表达式的数据类型将自动发生提升。
- 如果表达式包含字符串,应当把 + 号当做字符串连接运算符,而非加法运算符。
- 有些表达式不能算单独的 Java 语句(而 C 语言的任意表达式均可作为一个语句),像
3 + 4
、a > b ? a : b
、new MyCalss()
、arr[index]
。
-
Java 语句
- 语句是指令式编程语言的最小的组成单位。
- Java 语句的类型包括: 空语句、表达式语句、复合语句、方法调用语句。
-
Java 代码块
- 块通常用于将两条或以上语句组合在一起,使其在格式上更像一条语句。
-
Java中的注释
- 单行注释:
//
- 多行注释:
/**/
- 文档注释:
/** * */
- 单行注释:
2. Java 控制语句
-
Java if else
- 条件语句。if 块仅在与其关联的布尔表达式为 true 时执行。
-
Java switch 语句
- 条件分支语句,与 case 联合使用。
- switch 语句中的变量类型可以是: byte、short、int、char 或 String。
- case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或字面常量。
- 当遇到 break 语句时,switch 语句终止。
- switch 语句可以包含一个 default 分支,该分支一般是 switch 语句中的最后一个分支。
-
Java for 循环
- for 循环是一个使用计数器来实现的循环。
- for 后面的括号中,第一个语句是变量声明语句,可以声明一个或多个整形变量;第二个语句是条件判断,与 while 循环的条件判断一样;第三个语句是迭代语句,可以是任意语句,但一般用于递增或递减变量。
- for 循环可以缺省变量声明语句、循环条件、迭代语句中的一个或多个。
- 增强 for 循环格式:
for (变量声明: 数组或列表) {循环体}
-
Java while 循环
- while 循环是最简单的循环形式。它先判断条件,再执行循环体。
- do while 循环则先执行循环体,再判断条件。
-
Java break 语句
- break 语句出现在一个循环语句中时,用于跳出当前循环。
-
Java continue 语句
- continue 语句出现在一个循环语句中时,用于跳过当次循环。
3. Java 数组
-
Java 数组
- Java 数组的每个元素的数据类型必须相同。
- 数组是引用数据类型。
- 声明时同时创建数组:
DataType[] arrayName = new DataType[数组长度];
,这里的数组长度的值类型必须是整型。 - 数组也可以先声明,再创建。
- 数组创建后,所有元素都初始化为默认值,整型数组的元素都为 0,浮点型都为 0.0,布尔型都为 false,引用类型都为 null。
- 声明时同时静态初始化数组:
DataType[] arrayName = {元素1, 元素2, 元素3, 元素n};
- Java 数组引用元素时,不能超过其下标最大值,否则会报错。
-
多维数组
- 声明一个二维数组:
DataType[][] arrayName;
- 声明并创建一个二维数组:
DataType[][] arrayName = new DataType[行数][列数];
- 在创建数组的时候,我们也可以先不指定列的数量,再在后面进行逐行创建。例如创建一个 3 行的二维数组:
DataType[][] arrayName = new DataType[3][];
,接着第二行为 5 列:DataType[1] = new DataType[5];
- 声明并静态初始化一个 3 行 2 列的二维整型数组:
int[][] intArray = {{1,2}, {3,4}, {5,6}};
- 声明一个二维数组:
-
Java 数组复制
- 直接把一个数组名赋值给另一个数组,那么该另一个数组的名称相当于当前数组的别名,因为指向的是同一个引用。
- 通过遍历赋值: 通过是使用 for 循环逐一赋值。
- 通过 Object 类中的 clone() 方法复制:
arr2 = arr1.clone();
- 使用 System.arraycopy() 方法复制:
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
,这里要求 arr2 的长度不能小于 arr1 的长度。 - 使用 Arrays.copyOf() 方法复制:
arr2 = Arrays.copyOf(arr1, arr1.length);
- 使用 Arrays.copyOfRange() 方法复制:
arr2 = Arrays.copyOfRange(arr1, 0, arr1.length);
4. Java 面向对象
-
Java 类和对象
- 类是一个 Java 程序的基本单位。类其实是一个抽象概念,可以把它理解成"模版"。
- 对象是一个具体的事物,是一个类的实例。
- 在代码中,事物的静态特征被抽象成属性,事物的动态行为被抽象成方法。
- 每个类都可以由以下元素组成:
- 成员属性: 也称为"字段"、"成员变量"或"实例变量"。属性是用以保存每个对象的数据的变量。
- 成员方法: 也称为"实例方法"。成员方法是对象执行的操作。
- 静态变量: 也称为"类属性"。是同一个类的任何对象所共有的,即一个类不管被实例化成多少个对象,每个静态变量在类中仅存在一次。
- 静态方法: 也称为"类方法"。静态方法是不影响特定对象的方法。
- 内部类: 可以将一个类包含在另一个类中,常用于该类仅提供给声明它的类使用的情况。
- 构造方法: 生成新对象的特殊方法。使用 new 关键字 + 构造方法名() 来创建对象。当类内部没有显式定义构造方法时,系统会自动添加无参的构造方法。
- 参数化类型: 也称为"泛型"。其中的命名规范为 T - type、E - element、K - key、V - value、N - number、? - 不确定的 Java 类型。泛型有 3 种使用方式: 泛型类、泛型接口、泛型方法。
- 对象在内存中的布局一般分为三块区域,分别是 对象头、实例数据、对齐填充 。
-
Java 方法
访问修饰符 返回类型 方法名(参数列表) { 若干语句; return 方法返回值; }
- 参数变量的作用域在方法内部。
- 当方法返回值为 void 时,可以省略 return 语句。
- 根据方法是否带有参数、是否有返回值,可以分为 4 大类。
-
Java 方法重载
- 指在一个类中定义多个同名的方法:方法名相同,参数类型或参数个数不同。
- 通常来说,方法重载的返回值类型都是相同的。
-
Java 构造方法
- 构造方法的名字必须和类名一致。
- 构造方法分有参构造和无参构造。
- 构造方法没有返回值,返回的是一个类。
- 构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。
-
Java 字符串
- 字符串(String)属于引用数据类型。
- 字符串有一个重要特性: 不可变性。
- "==" 用于比较地址是否相同。equals 方法用于比较字符串字面量是否相同。
- StringBuilder: 这个类可以在原字符串的基础上进行增删改,并且不会新开辟内存空间。这就弥补了有些场景下 String 的不友好性。内部维护了一个 char[] value 没有用 final 修饰,所以它是可以在原字符串基础上做修改的。
- StringBuffer: 和 StringBuilder 是一样的,只是 StringBuffer 对于字符串的增删改方法都加上了 synchronized 关键字,这样一来对于字符串的操作就是线程安全的,由于线程安全所以其性能也次于 StringBuilder。
-
Java 访问权限
- 四种访问权限(由小到大)
- private: 私有的。被其修饰的属性或方法只能被该类的对象访问,其子类不能访问,更不能跨包访问。
- default: 又叫"默认访问权限"或"包访问权限"一般省略。该模式下,只允许在同一个包中进行访问。
- protected: 一般称之为"保护访问权限"。被其修饰的属性或方法只能被类本身及子类访问,即使子类在不同的包中也可以访问。一方面,与其说子类访问了父类的 protected 成员,不如说子类访问了自己的从父类继承来的 protected 成员。另一方面,如果该子类与父类不在同一个包里,那么通过父类的对象实例是不能访问父类的 protected 成员的。
- public: 公共的。允许跨类访问,允许跨包访问。
- 只有默认访问权限和 public 能够用来修饰(外部)类。
- 四种访问权限(由小到大)
-
Java this 关键字
- 在方法内部,this 关键字是当前对象的默认引用。
- 当成员变量&局部变量重名时,优先使用局部变量。因此可以使用
this.属性
来表面这是一个成员变量,与局部变量区分开来。 - 使用 this 关键字可以用来访问本类的实例属性、实例方法、构造器:
- 访问本类实例属性:
this.属性
- 访问本类实例方法:
this.方法名(参数列表)
- 访问本类构造器:
this(参数列表)
- 访问本类实例属性:
this(参数列表)
的注意事项:- 只能在构造器中使用
this(参数列表);
。即在一个构造器中访问本类的另外一个构造器(默认是super();
)。 - 显式使用
this();
时,默认的super();
就被覆盖。 this(参数列表);
和super(参数列表);
在构造器中有且只能存在一个,并且必须位于构造器第一行。
- 只能在构造器中使用
- 类中的静态方法 static method 中不能使用 this 。
-
Java final 关键字
- 用 final 修饰一个类时,表明这个类不能被继承。final 类中的成员变量可以根据需要设为 final。final 类中的所有成员方法都会被隐式地指定为 final 方法。
- 用 final 修饰一个方法时,表明这个方法不能被重写。一是为了把方法锁定,以防任何继承类修改它的含义;二是为了低版本 Java 的方法执行效率。
- 类的 private 方法会隐式地被指定为 final 方法。
- 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能再更改;如果是引用类型变量,则在对其初始化之后便不能再让其指向另一个对象。
- 用 final 作用于类的成员变量时,成员变量必须在定义时或者构造器中进行初始化赋值。而局部变量则只需要保证在使用之前被初始化赋值即可。
- static 作用于成员变量用来表示只保存一份副本,而 final 的作用是用来保证变量不可变。
-
Java 递归
- 就是在一个函数中调用自身。
-
Java instanceof 操作符
- instanceof 运算符的前一个操作数是一个引用变量,后一个操作数通常是一个类(或者接口)。即判断操作符的左右操作数是否有继承或实现关系。
- 泛型擦除了类型信息,所以使用 instanceof 检查某个实例是否是特定类型的泛型类是不可行的。
-
Java 继承
- 目的是为了提高代码复用性。
- 使用关键字 extends 声明一个类继承另一个类。
- 使用关键字 implements 声明一个类实现一个或多个接口。子类实现接口的时候必须重写接口中的方法。
- 子类可以访问父类的非私有成员。
- 子类可以重写(覆盖)父类的方法,以实现自己的特定行为。
- 子类不能继承父类的构造方法,但子类会调用父类的构造方法。因为构造方法语法是与类同名。
- 向上转型:
Father f = new Son();
父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。 - 向下转型:
Son s = (Son)f;
父类引用变量实际引用必须是子类对象才能成功转。这就可以调用一些子类特有而父类没有的方法。 - 父子类初始化先后顺序为:
- 父类中静态成员变量和静态代码块
- 子类中静态成员变量和静态代码块
- 父类中普通成员变量和代码块,父类的构造函数
- 子类中普通成员变量和代码块,子类的构造函数
-
Java 方法重写
- 就是子类中出现和父类中一模一样的方法(包括返回值类型,方法名,参数列表),它建立在继承的基础上。你可以理解为方法的外壳不变,但是核心内容重写。
@Override
注解显示声明该方法为注解方法,可以帮你检查重写方法的语法正确性。- 子类方法不能缩小父类方法的访问权限。
- 子类方法不能抛出比父类方法更多的异常。即继承当中子类抛出的异常必须是父类抛出的异常或父类抛出异常的子异常。
- 父类的私有方法不能被子类重写。
- 父类的静态方法不能被子类重写为非静态方法。父类的非静态方法不能被子类重写为静态方法。子类可以定义于父类的静态方法同名的静态方法,以便在子类中隐藏父类的静态方法。
- 父类的抽象方法可以被子类通过两种途径重写(即实现和重写)。
-
Java super 关键字
- super表示父类对象,是指向父类的引用:
- 访问父类对象中的成员变量:
super.属性
- 调用父类对象中定义的方法:
super.方法(参数列表)
- 调用父类构造方法:
super(参数列表)
- 访问父类对象中的成员变量:
- 子类构造方法必须调用
super(参数列表);
即父类的构造方法。 - 子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。
- super表示父类对象,是指向父类的引用:
-
抽象类 & 抽象方法
- 抽象类: 抽象类是可以表达概念但是无法构造实体的类。有抽象方法的类必须是抽象类,抽象类不一定必须含有抽象方法。抽象类必须在类前用 abstract 关键字修饰。
- 子类可以继承于抽象类,但是一定要实现父类们所有 abstract 的方法。如果不能完全实现,那么子类也必须被定义为抽象类。即只有实现父类的所有抽象方法,才能是完整类。
- 抽象方法: 有很多不同类的方法是相似的,但是具体内容又不太一样,所以我们只能抽取他的声明。即抽象方法可以表达概念但没有具体实现。
- 抽象方法必须为 public 或者 protected (因为如果为 private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为 public。
-
Java 接口
- 接口中的变量会被隐式地指定为 public static final 变量(并且只能是 public static final 变量,用 private 修饰会报编译错误)
- 接口中的方法会被隐式地指定为 public abstract 方法且只能是 public abstract 方法(用其他关键字,比如 private、protected、static、 final 等修饰会报编译错误)
- 接口中的方法必须都是抽象方法。
- 接口中不能含有静态代码块以及静态方法。
- 一个非抽象类遵循了某个接口,就必须实现该接口中的所有方法。
-
Java 多态
- 一个父类可能有若干子类,各子类实现父类方法有多种多样,调用父类方法时,父类引用变量指向不同子类实例而执行不同方法,这就是所谓父类方法是多态的。
-
Java 封装
- 封装是对类的属性和方法进行封装,只对外暴露方法而不暴露具体使用细节,所以我们一般设计类成员变量时候大多设为私有而通过一些 get、set 方法去读写。
-
嵌套&内部类
- 广泛意义上的内部类:
- 成员内部类
- 即位于另一个类的内部的类。
- 如果内部类要访问外部类的同名成员,需要使用
外部类.this.成员变量
或外部类.this.成员方法
这种形式。 - 在外部类中如果要访问成员内部类的成员,则必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问。
- 成员内部类是依附外部类而存在的。创建成员内部类对象的方式:
Outter outter = new Outter(); Outter.Inner inner = outter.new Inner();
。 - 内部类可以拥有 private、protected、public、包访问(默认)共四种访问权限。
- 局部内部类
- 是指定义在一个方法或一个作用域里面的类。
- 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。
- 匿名内部类
- 匿名内部类是唯一一种没有构造器的类。
- 匿名内部类也是不能有访问修饰符和 static 修饰符的。
- 大部分匿名内部类用于接口回调。
- 匿名内部类在编译的时候由系统自动起名为 Outter$1.class。
- 静态内部类
- 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字 static。
- 静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非 static 成员变量或者方法。
- 成员内部类
- 广泛意义上的内部类:
-
Java static 关键字
- 一个类中使用 static 修饰变量或者方法的话,它们可以直接通过类访问,不需要创建一个类的对象来访问成员。
- 构造方法不允许声明为 static 的。
- 静态方法中不存在当前对象,因而不能使用 this,当然也不能使用 super。
- 静态方法不能被非静态方法重写(覆盖)。
- 静态方法能被静态方法重写(覆盖)。
- 在 Java 中, static 关键字不会影响到变量或者方法的作用域。
- 在 Java 中, static 是不允许用来修饰局部变量的。
-
Java 匿名内部类
- 匿名内部类是唯一一种没有构造器的类。
- 大部分匿名内部类用于接口回调。
- 匿名内部类在编译的时候由系统自动起名为 Outter$1.class 。
- 一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
-
Java 单例
- 静态常量(饿汉式) —— 在类装载时就完成实例化。缺点就是没有达到 Lazy Loading 的效果。
public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }
- 静态代码块(饿汉式) —— 与静态常量类似,但它将类实例化的过程放在了静态代码块中。
public class Singleton { private static Singleton instance; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
- 单线程同步实例化(懒汉式) —— 非线程安全,只能在单线程下使用。
或public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; } }
- 多线程同步实例化(懒汉式) —— 效率低下,不推荐使用。
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 双重检查 —— 线程安全,延迟加载,效率较高,推荐使用
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
- 静态内部类 —— JVM 保证线程安全,延迟加载,效率高,推荐使用
public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
- 枚举 —— JVM 保证线程安全,同时防止反序列化重新创建新的对象,最佳模式,推荐使用
public enum Singleton { INSTANCE; public void doSomething() { // do something } } // 其他类使用 Singleton.INSTANCE.doSomething() 调用即可
- 静态常量(饿汉式) —— 在类装载时就完成实例化。缺点就是没有达到 Lazy Loading 的效果。
-
Java 枚举类
- 枚举的主要目的是为了加强编译时类型的安全性。
- 枚举类是一组预定义常量的集合,使用 enum 关键字声明这个类,常量名称官方建议大写。
public enum Singleton { INSTANCE; }
- 枚举就是一个特殊的类,因此也可以像普通的类一样拥有方法和属性。
public enum Direction { EAST, WEST, NORTH, SOUTH; protected String printDirection() { String message = "You are moving in " + this + " direction"; System.out.println( message ); return message; } }
- enum 的每一个 key 可以拥有若干个属于自己的属性
public enum Week { MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(7, "星期日"); Week(int index, String weekName) { this.index = index; this.weekName = weekName; } private int index; private String weekName; public int getIndex() { return index; } public String getWeekName() { return weekName; } }
- 枚举的常用方法:
- name() —— 返回枚举常量的名称
- values() —— 返回一个包含枚举中所有常量的数组
- valueOf(String name) —— 根据名称获取枚举常量
- EnumSet:
- EnumSet 是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。
- 与其他 Set 接口的实现类 HashSet/TreeSet 不同的是,EnumSet 在内部实现是位向量。
- EnumSet 不允许使用 null 元素。
- EnumSet 不是线程安全的,在多线程环境下需注意数据同步问题。
- 例子:
public class Test { public static void main(String[] args) { Set enumSet = EnumSet.of(Week.MON, Week.TUE, Week.WED); } }
- EnumMap:
- 与 EnumSet 类似,EnumMap 是一个特殊的 Map,Map 的 Key 必须是枚举类型。
- EnumMap 内部是通过数组实现的,效率比普通的 Map 更高一些。
- EnumMap 的 key 值不能为 null,并且 EnumMap 也不是线程安全的。
- 例子:
public class Test { public static void main(String[] args) { Map enumMap = new EnumMap(Week.class); enumMap.put(Week.MON, "Monday"); enumMap.put(Week.TUE, "Tuesday"); } }
-
Java 枚举构造方法
- 可以使用构造方法为每个枚举值赋予若干个属性。
-
Java 枚举字符串
- 可以使用枚举构造方法,把不同的字符串赋予对应的枚举值,使用例如 value 字段承接该字符串。
public enum Week { MON("星期一"), TUE("星期二"), WED("星期三"); private String value; private Week(String value) { this.value = value; } public String getValue() { return value; } }
- 可以使用枚举构造方法,把不同的字符串赋予对应的枚举值,使用例如 value 字段承接该字符串。
-
Java 反射
- 核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。
- 反射主要使用场景在于编译时不知道类或对象的具体信息。
- Class —— 反射的入口
- Class 类用于获取与类相关的信息
- Class 类提供了获取类信息的相关方法
- Class 类继承自 Object 类,是所有类的共同的图纸
- Class 类的对象称为类对象
- Class 类的常用方法:
- getFields() —— 获取类的 public 类型的属性
- getDeclaredFields() —— 获取类的所有属性
- getField(String name) —— 获取类的 public 类型的指定名称的属性
- getDeclaredField(String name) —— 获取类的指定属性
- getMethods() —— 获取类的 public 类型的方法(包括继承的方法)
- getDeclaredMethods() —— 获取类的所有方法
- getMethod(String name, Class[] args) —— 获取类的 public 类型的指定名称的方法
- getDeclaredMethod(String name) —— 获取类的指定方法
- getConstructors() —— 获取类的 public 类型的构造方法
- getDeclaredConstructors() —— 获取类的所有构造方法
- getConstructor(Class[] args) —— 获取类的 public 类型的特定构造方法
- getDeclaredConstructor(Class[] args) —— 获取类的特定构造方法
- newInstance() —— 创建类的无参实例对象
- getName() —— 获取类的完整名称
- getPackage() —— 获取类所属的包信息
- getSuperclass() —— 获取类的父类对应的 Class 对象
- 获取一个类的类对象的 3 种方式:
类名.class
对象名.getClass()
Class.forName("类的完整名称")
- 使用反射创建对象:
- 通过 Class 的 newInstance() 方法
- 要求该 Class 对象的对应类有无参构造方法。
- 执行 newInstance() 实际上就是执行无参构造方法来创建该类的实例。
- 通过 Constructor 的 newInstance() 方法
- 先使用 Class 对象获取指定的 Constructor 对象。
- 再调用 Constructor 对象的 newInstance() 创建 Class 对象对应类的对象。
- 通过该方法可选择使用指定构造方法来创建对象。
- 通过 Class 的 newInstance() 方法
- 使用反射操作属性:
- getXxx(Object obj) —— 获取 obj 对象的该 Field 的属性值。此处的 "Xxx" 对应 8 个基本数据类型,如果该属性类型是引用类型则直接使用 get(Object obj) 即可。
- setXxx(Object obj, Xxx value) —— 设置 obj 对象的该 Field 的属性值。此处的 "Xxx" 对应 8 个基本数据类型,如果该属性类型是引用类型则直接使用 set(Object obj, Object value) 即可。
- setAccessible(Boolean flag) —— 设置该 Field 的访问控制权限。若 flag 为 true 则直接取消该属性的访问控制权限,即使是 private 属性也可以进行访问。
- 非反射 与 反射 操作属性对比:
package net.czy; class Dog { String nickName; int age; } public class Test { public static void main(String[] args) { // 非反射 Dog dog = new Dog(); dog.nickName = "小黑"; dog.age = 3; System.out.println(dog.nickName + "今年" + dog.age + "岁"); // 反射 String className = "net.czy.Dog"; Class clazz = Class.forName(className); Object dog = clazz.getConstructor().newInstance(); // 相当于 Object dog = clazz.newInstance(); Field f1 = clazz.getField("nickName"); Field f2 = clazz.getDeclaredField("age"); // 相当于 Field f2 = clazz.getField("age"); f1.set(dog, "小黑") f2.setAccessible(true); f2.setInt(dog, 3); System.out.println(f1.get(dog) + "今年" + f2.get(dog) + "岁"); } }
- 非反射 与 反射 执行方法对比:
package net.czy; class Dog { void shout() { System.out.println("汪汪汪"); } int add(int a, int b) { return a + b; } } public class Test { public static void main(String[] args) { // 非反射 Dog dog = new Dog(); dog.shout(); int result = dog.add(1, 2); // 反射 String className = "net.czy.Dog"; Class clazz = Class.forName(className); Object dog = clazz.getConstructor().newInstance(); // 相当于 Object dog = clazz.newInstance(); Method m1 = clazz.getMethod("shout"); m1.invoke(dog); Method m2 = clazz.getMethod("add", int.class, int.class); Object result = m2.invoke(dog, 1, 2); } }
5. Java 异常处理
-
Java 异常简介
- 异常本质上是程序上的错误,包括程序逻辑错误和系统错误。因为 Java 是面向对象的编程语言,所以异常本身也是一个错误,程序发生异常就会产生一个异常对象。
- Throwable 类是 Java 语言中所有错误或异常的顶层父类,其他异常类都继承于该类。Throwable 类有两个重要的子类:Exception(异常) 和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
- Java 中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。
- 捕获了异常缺不进行处理,这是我们在写代码时候的大忌。
-
Java 异常处理
- 按照异常的构成分类:
- Error 是 Throwable 的子类,通常情况下应用程序 「不应该试图捕获的严重问题」。
- Exception 以及它的子类,代表程序运行时发送的各种不期望发生的事件。可以被 Java 异常处理机制使用,是异常处理的核心。
- 异常的主要分类:
- 非检查性异常(unchecked exception) —— 包括 Error 和 RuntimeException 以及他们的子类。Java语言在编译时,不会提示和发现这样的异常,不要求在程序中处理这些异常。
- 检查性异常(checked exception) —— 除了 Error 和 RuntimeException 的其它异常。Java 语言强制要求程序员为这样的异常做预备处理工作(使用 try…catch…finally 或者 throws)。
- 父类的方法没有声明异常,子类在重写该方法的时候不能声明异常。
- 如果父类的方法声明一个异常 exception1,则子类在重写该方法的时候声明的异常不能是 exception1 的父类。
- 如果父类的方法声明的异常类型只有非运行时异常(运行时异常),则子类在重写该方法的时候声明的异常也只能有非运行时异常(运行时异常),不能含有运行时异常(非运行时异常)。
- 只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程,因为异常捕获的代价非常高昂。
- 切忌使用空 catch 块。
- 按照异常的构成分类:
-
Java try catch
- try 块:
- 存放可能发生异常的代码。
- 如果执行完 try 且不发生异常,则接着去执行 finally 块中的代码和 finally 后面的代码(如果有的话)。
- 如果程序发生异常,则立即从当前语句尝试跳转去匹配对应的 catch 块。
- 一个 try 至少要有一个 catch 块,否则, 至少要有一个 finally 块。
- catch 块:
- 用于捕获并处理一个特定的异常,或者这异常类型的子类。
- catch 后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个 catch 块来处理异常(只有第一个匹配的 catch 会得到执行)。
- 在 catch 块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个 catch 块中的局部变量,其它块不能访问。
- 如果 try 中没有发生异常,则所有的 catch 块将被忽略。
- finally 块:
- 不是必须的,通常是可选的。
- 无论异常是否发生,异常是否匹配被处理,finally 中的代码都会执行。
- finally 不是用来处理异常的,finally 不会捕获和处理异常。
- finally 主要做一些清理工作,如流的关闭,数据库连接的关闭等。
- finally 块不管异常是否发生,只要对应的 try 执行了,则它一定也执行。只有一种方法让 finally 块不执行:
System.exit();
。 - finally 块一定执行的原理是: 编译器会将 finally 块中的代码复制两份并分别添加在 try 和 catch 的后面。至于 try 和 catch 块的 return 语句,则会将 return 右侧的计算结果先压栈,然后等 finally 块执行完成后,再把之前压栈的值从栈弹出,返回给调用者。
- 如果 finally 块中也有 return 语句,则 finally 块中的 return 语句将覆盖 try 和 catch 块中的 return 语句。
- 执行顺序一:
public static void main(String[] args) { int result = test1(); System.out.println(result); } public static int test1() { int i = 1; try { i++; System.out.println("try block, i = " + i); return i; } catch (Exception e) { i++; System.out.println("catch block i = " + i); return i; } finally { i = 10; System.out.println("finally block i = " + i); } } /** 最终的输出结果如下 */ // try block, i = 2 // finally block i = 10 // 2
- 执行顺序二:
public static void main(String[] args) { int result = test2(); System.out.println(result); } public static int test2() { int i = 1; try { i++; System.out.println("try block, i = " + i); return i; } catch (Exception e) { i++; System.out.println("catch block i = " + i); return i; } finally { i = 10; System.out.println("finally block i = " + i); return i; } } /** 最终的输出结果如下 */ // try block, i = 2 // finally block i = 10 // 10
- try 块:
-
Java throw 和 throws
- throw 关键字:
- throw 语句用于手动显式的抛出一个异常,throw 语句的后面必须是一个异常对象。
- throw 语句必须写在方法中,执行 throw 语句的地方就是一个异常抛出点。
- throw 的一个重要作用是异常类型的转换。
- throws 关键字:
- 如果一个方法内部的代码会抛出检查性异常(checked exception),而方法自己又没有对这些异常完全处理掉,则 java 的编译器会要求你必须在方法的签名上使用 throws 关键字声明这些可能抛出的异常,否则编译不通过。
- throws 关键字仅仅是将方法中可能出现的异常向调用者抛出,而自己则不具体处理。
- 采取这种异常处理的原因可能是: 方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。
- throw 关键字:
-
Java 捕获多个异常
- 不能利用基类 Exception 捕捉所有潜在的异常,这样会丢失原始异常的有效信息。应当采用多异常捕获的方式,针对不同类型的异常做出不同的处理。
- throws 后面可以跟着多个异常类型。
-
Java 自定义异常
- 可以选择继承 Throwable,Exception 或它们的子类,甚至不需要实现和重写父类的任何方法即可完成一个异常类型的定义。
- 自定义的异常应该总是包含如下的构造函数:
- 一个无参构造函数
- 一个带有 String 参数的构造函数,并传递给父类的构造函数
- 一个带有 String 参数和 Throwable 参数,并都传递给父类构造函数
- 一个带有 Throwable 参数的构造函数,并传递给父类的构造函数
- 以 IOException 为例:
public class IOException extends Exception { static final long serialVersionUID = 7818375828146090155L; public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); } }
- 假设父类 throws IOException,子类就必须 throws IOException 或者 IOException 的子类。
-
Java try-with-resources
- 创建 try-with-resources 语句的初衷是旨在减轻开发人员释放 try 块中使用的资源的义务。
- 使用 try-with-resources 语句的目的是使代码更清晰易读、更易于管理,尤其是当我们处理许多 try 块时。
- Java7 开始支持的 try-with-resources 语句示例:
// 使用 try...catch...finally 语句 BufferredWriter writer = null; try { writer = new BufferedWriter(new FileWriter(fileName)); writer.write(str); } catch (IOException e) { System.out.println("Error writing to file: "); e.printStackTrace(); } finally { try { if (writer != null) { writer.close(); } } catch (IOException e) { System.out.println("Error closing file: "); e.printStackTrace(); } } // 使用 try-with-resources 语句 try (BufferredWriter writer = new BufferedWriter(new FileWriter(fileName))) { writer.write(str); } catch (IOException e) { System.out.println("Error writing to file: "); e.printStackTrace(); }
- Java9 优化后的 try-with-resources 语句示例(不必在 try-with-resources 语句中声明资源):
BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); try (writer) { writer.write(str); } catch(IOException e) { System.out.println("Error writing to file: "); e.printStackTrace(); }
- 如果要使用多个文件,则可以在 try() 语句中打开文件,并用分号将它们分开(JVM 会按照相反的声明顺序一一关闭这些资源):
try ( BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); Scanner scanner = new Scanner(System.in) ) { if (scanner.hasNextLine()) { writer.write(scanner.nextLine()); } } catch(IOException e) { System.out.println("Error writing to file: "); e.printStackTrace(); }
- 一个资源类(通常是各种类型的编写器,读取器,套接字,输出或输入流等)如果要支持在 try() 语句声明,就必须实现 AutoCloseable 接口(甚至重写 .close() 方法)。
-
Java 注解
- 使用 @interface 关键字来定义:
public @interface MyAnnotation { }
- 注解的使用:
@MyAnnotation public class MyClass { }
- 注解的属性:
- 注解的属性也叫做成员变量。
- 注解只有成员变量,没有方法。
- 注解的成员变量在注解的定义中以"无形参的方法"形式来声明。
- 注解的属性赋值的方式是在注解的括号内以 value=xxx 形式,多个属性之间用逗号 "," 隔开。
- 在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。
- 注解中属性可以有默认值,默认值需要用 default 关键值指定。
- 如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内。
- 如果一个注解没有任何属性,那么在应用这个注解的时候,括号都可以省略。
- 注解的获取:
- 注解通过反射获取(以类的注解为例):
- 可以通过 Class 对象的 isAnnotationPresent() 方法来判断一个类是否拥有某个注解。
- 可以通过 Class 对象的 getAnnotations() 方法来获取一个类的所有注解。
- 可以通过 Class 对象的 getAnnotation() 方法来获取一个类的指定类型的注解。
- 若获取到的 Annotation 不为 null,就可以接着调用它们的属性方法了。
- 如果一个注解要在运行时被成功提取,那么
@Retention(RetentionPolicy.RUNTIME)
是必须的。
- 注解通过反射获取(以类的注解为例):
- 注解的作用:
- 给编译器以及 APT(Annotation Processing Tool) 使用。
- 使用 @interface 关键字来定义:
-
Java 注解类型
- 元注解
- @Retention —— 这个注解的存活时间。可能的取值包括:
- RetentionPolicy.SOURCE —— 注解只在源代码中保留
- RetentionPolicy.CLASS —— 注解在编译时保留,运行时丢弃
- RetentionPolicy.RUNTIME —— 注解在编译时和运行时都保留,会被加载到 JVM 中,所以程序运行时还可以获取当前注解
- @Documented —— 这个注解本身是否应该出现在 JavaDoc 中。
- @Target —— 这个注解应用到的地方。可能的取值包括:
- ElementType.ANNOTATION_TYPE —— 可以给一个注解进行注解
- ElementType.CONSTRUCTOR —— 可以给构造方法进行注解
- ElementType.FIELD —— 可以给属性进行注解
- ElementType.LOCAL_VARIABLE —— 可以给局部变量进行注解
- ElementType.METHOD —— 可以给方法进行注解
- ElementType.PACKAGE —— 可以给一个包进行注解
- ElementType.PARAMETER —— 可以给一个方法内的参数进行注解
- ElementType.TYPE —— 可以给一个类型进行注解,比如类、接口、枚举
- @Inherited —— 这个注解是否会被应用到无注解子类(如果父类已经被这个注解注解了的话)。当然,子类的子类也是同样规则。
- @Repeatable —— 这个注解能否被多次应用到同一个地方。@Repeatable 后面括号中的类相当于一个容器注解(所谓容器注解,就是用来存放其它注解的地方,它本身也是一个注解)。
- @Retention —— 这个注解的存活时间。可能的取值包括:
- 容器注解
- 里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组,注意是数组
- 例子:
// Persons 是一个容器注解 @interface Persons { Person[] value(); } // Person 是一个被 @Repeatable 注解过的注解 @Repeatable(Persons.class) @interface Person { String role default ""; } // 使用 @Person(role="coder") @Person(role="tester") @Person(role="cooker") public class SuperMan { }
- Java 预置注解
- @Deprecated —— 标记当前元素已过时。
- @Override —— 说明当前方法是重写父类方法。
- @SuppressWarnings —— 用于忽略编译器的警告信息。
class Hero { @Deprecated public void fly() { } } // 声明忽略 deprecated 的警告,这样在 IDE 中就不会出现 fly() 方法已过期的提示 @SuppressWarnings("deprecation") public class HeroTest { public static void main(String[] args) { Hero hero = new Hero(); hero.fly(); } }
- @SafeVarargs —— 参数安全类型注解。目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。
- @FunctionalInterface —— 函数式接口注解。函数式接口可以很容易转换为 Lambda 表达式。
- 元注解
-
Java 日志
- SpringBoot 日志
- 默认使用 logback 作为日志实现。
- 使用 SLF4J 作为日志门面。
- 将 JUL 也转换成 SLF4J。
- 也可以使用 log4j2 作为日志门面,但最终也是通过 SLF4J 调用 logback。
- 日志的行为级别,包括(从高到低,当选择其中一个级别,该级别往下的行为均不会再被打印出来):
- OFF:
- FATAL:
- ERROR:
- WARN:
- INFO:
- DEBUG:
- ALL:
- 依赖版本建议
- slf4j-api: 1.7.x
- logback-core: 1.2.x
- logback-classic: 1.2.x
- LogBack 的各个组件及其作用
- logback-core: 提供 LogBack 的核心功能,是其他组件的基础。
- logback-classic: 实现了slf4j 的 API,当程序要配合 slf4j 使用时,则需要引入该包。
- logback-access: 为集成 Servlet 环境而准备的,可提供 HTTP-ACCESS 的日志接口。
- 使用 logger:
Object entry = new SomeObject(); // 打印对象 logger.debug("The entry is {}.", entry);
- LogBack 的配置项
- configuration
- scan —— 表示配置文件发生变化时是否重新加载该文件。默认值为 true。
- scanPeriod —— 表示配置文件发生变化时,重新加载该文件的时间间隔,默认单位为毫秒,默认的时间间隔为 1 分钟。可以设置为 "60 seconds"。
- debug —— 表示是否打印 LogBack 的内部日志信息,实时查看 LogBack 运行状态。默认值为 false。
- contextName —— 应用上下文名称,用于标识应用,默认为 "default"。如果多个应用输出到同一个地方,就有必要使用 %contextName 来区别。
- property —— 定义变量,非必须。通常会定义 日志文件名称前缀、日志路径、日志输出格式 作为公共变量。
- name —— 变量名
- value —— 变量值
- logger —— 设置一个类或者某个包的日志输出级别,以及关联的 appender。可以设置多个。
- name —— 要输出日志的类名或者包名。
- level —— 日志输出级别,请参考"日志的行为级别"
- additivity —— 是否将日志向上级(即 root)传递,若上级(即 root)也配置了相同的输出(如 CONSOLE)的话,则会在相应的地方(如 控制台)输出两次。默认值为 true。
- appender-ref —— 用于指定日志输出到哪个 appender。可以设置多个。
- ref —— 指定 appender 的名称。
- root —— 也是一个 logger 元素,但它是根 logger,是所有 logger 的上级。只可设置一个。
- level —— 用来设置打印级别,请参考"日志的行为级别"。大小写无关。
- appender —— 负责写日志的组件。用于配置日志格式、如何过滤、文件处理等。可以设置多个。
- name —— appender 的名称
- class —— 指定要实例化的 appender 类的全限定名。默认有以下几种:
- ch.qos.logback.core.ConsoleAppender —— 日志输出到控制台。使用该类时 appender 有以下子节点:
<encoder>
,<target>
,<filter>
- ch.qos.logback.core.FileAppender —— 日志输出到文件。使用该类时 appender 有以下子节点:
<file>
,<encoder>
,<encoder>
,<prudent>
- ch.qos.logback.core.rolling.RollingFileAppender —— 滚动记录文件,且支持日志分割。使用该类时 appender 有以下子节点:
<file>
,<encoder>
,<rollingPolicy>
,<triggeringPolicy>
- ch.qos.logback.core.ConsoleAppender —— 日志输出到控制台。使用该类时 appender 有以下子节点:
- encoder —— 对日志进行格式化。
- pattern —— 日志输出的格式。
- target —— 字符串 System.out(默认) 或 System.err。
- filter —— 日志过滤器。
- file —— 被写入的日志文件名称,可以是相对目录,也可以是绝对目录。如果上级目录不存在会自动创建,没有默认值。
- append —— 是否采用追加的模式写入文件,默认为 true。如果是 false,则会先清空现存文件。
- preduent —— 日志是否会被安全的写入文件(效率低),即使其他的 FileAppender 也在向此文件做写入操作。默认为 false。
- rollingPolicy —— 滚动记录文件的策略。涉及文件移动和重命名。
- class —— 指定要实例化的策略类的全限定名。默认有以下几种:
- ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy —— 基于文件大小和时间滚动策略。
- ch.qos.logback.core.rolling.FixedWindowRollingPolicy —— 基于文件个数的滚动策略。
- fileNamePattern —— 指定滚动记录文件的名称。
%d
可以包含一个 java.text.SimpleDateFormat 格式的日期,%d
的默认格式是 yyyy-MM-dd。 - maxHistory —— 指定滚动记录文件的最大保留时间,单位为天。即控制保留的归档文件总的数量,超出数量就删除旧文件。第一条件,可选属性。
- totalSizeCap —— 控制所有归档文件总的大小,当达到这个大小后,旧的归档文件将会被异步地删除。第二条件,依赖第一条件,可选属性。
- cleanHistoryOnStart —— 每次 appender 启动时,是否删除归档文件。默认为 false。
- minIndex —— 窗口索引最小值。
- maxIndex —— 窗口索引最大值。当用户指定的窗口过大时,会自动将窗口设置为 20。
- class —— 指定要实例化的策略类的全限定名。默认有以下几种:
- triggeringPolicy —— 告知 RollingFileAppender 合适激活滚动。
- configuration
- LogBack 在 Spring 项目中的额外配置项
- configuration
- springProperty —— 从 Spring 的配置文件中获取变量的值
- source —— 指定配置文件的名称,默认为 application.properties。
- name —— 变量名。
- scope —— 规定设置属性的作用域,可能的值包括 "context" 和 "local"。local 是设置到 interpretationContext 的属性 map 中,局部使用;context 是设置到 interpretationContext 的上下文中(即 log 的上下文),全局使用。
- springProperty —— 从 Spring 的配置文件中获取变量的值
- configuration
- SpringBoot 日志
-
Java 断言
- 语法:
assert 布尔表达式
—— 如果布尔表达式的值为 false,则抛出错误 AssertionError - 语法:
assert 布尔表达式 : 字符串
—— 如果布尔表达式的值为 false,则抛出错误,同时提供一个字符串作为额外的错误信息 - 如果 JVM 如果关闭了断言检查(默认关闭),则 assert 关键字不会生效,代码会从下一句开始继续执行。所以永远不要假设它们会被执行。
- 可以使用 -ea 或 -enableassertions 选项来开启断言检查。
- Java 中断言很少被使用,更好的方法是编写单元测试。
- 语法:
6. Java 集合
-
Java 集合框架
- 所有集合类都位于 java.util 包下。
- List 集合是有序集合,集合中的元素可以重复,访问集合中的元素可以根据元素的索引来访问。
- Set 集合是无序集合,集合中的元素不可以重复,访问集合中的元素只能根据元素本身来访问(也是集合里元素不允许重复的原因)。
- Map 集合中保存 Key-value 对形式的元素,访问时只能根据每项元素的 key 来访问其 value。
-
Java 集合接口
- java.util.Iterator
- java.util.Collection
- List
- ArrayList
- LinkedList
- Queue
- LinkedList
- Set
- HashSet
- List
- java.util.Map
- HashMap
- java.util.Collection
- java.util.Iterator
-
Java List 接口
- 集合中的元素允许重复。
- 有序,各元素插入的顺序就是各元素的顺序。
- 元素可通过索引来访问或设置。
-
Java ArrayList
- 数组结构。是一个动态数组。
- 线程不安全。
- 查找速度快,插入和删除速度慢。
-
Java Vector
- 数组结构。
- 线程安全。
- 效率低。
- 与 ArrayList 相似,但 Vector 是同步的(线程安全),它的操作与 ArrayList 几乎一样。已被 ArrayList 取代。
-
Java Stack
- 继承自 Vector,实现一个堆栈。
7. Java 队列
-
Java 队列接口
- Queue 是一个接口,它继承自 Collection 接口,因此可以用来存储一组对象。
- Queue 的特点是按照 FIFO 的原则进行元素的插入和删除。
- Queue 常用于异步处理、消息队列、任务队列等场景。
- Queue 的衍生结构
- Queue
- LinkedList
- PriorityQueue
- ArrayDeque
- Queue
- 基本方法
- add(E e) - 在队列的尾部添加一个元素,如果队列已满,则会抛出 IllegalStateException 异常。
- offer(E e) - 在队列的尾部添加一个元素,如果队列已满,则返回 false。
- remove() - 移除队列头部的元素,如果队列为空,则会抛出 NoSuchElementException 异常。
- poll() - 移除队列头部的元素,如果队列为空,则返回 null。
- element() - 返回队列头部的元素,如果队列为空,则会抛出 NoSuchElementException 异常。
- peek() - 返回队列头部的元素,如果队列为空,则返回 null。
- 优点:
- 可以有效地控制任务的执行顺序,确保任务按照一定的顺序进行执行。
- 可以有效地减轻线程的压力,降低线程之间的竞争,提高程序的并发性能。
- 可以有效地控制任务的调度时间,根据任务的优先级进行处理,提高程序的效率。
- 缺点:
- 可能会增加程序的内存消耗,当队列中元素过多时,可能会导致内存溢出等问题。
- 可能会增加程序的复杂度,需要对任务进行优先级等方面的处理,否则可能会导致任务无法按照预期的顺序进行执行。
-
Java 优先级队列
- PriorityQueue,可以根据元素的大小自动进行排序。
-
Java 双端队列接口
- Deque(双端队列) 是一种结合了普通的 Queue 和 Stack 的数据结构,因此你可以将新元素添加到行的尾部和头部,删除的情况亦相同。
- Deque 提供了对可变大小数组的支持。即提供了 push() 和 pop() 方法
- Deque 不支持多个线程的当前访问。
- 在没有外部同步的情况下,双端队列不是线程安全的。
- 数组双端队列中不允许有 null 元素。
- 对于 Queue 支持的 add()、offer()、remove()、poll()、peek() 方法,Deque 还分别支持对应的 *First() 和 *Last() 方法。对于 element() 方法,Deque 也提供了 getFirst() 和 getLast() 方法来获取第一个或最后一个元素。
- 建议使用 Deque 的实现(如 ArrayDeque)来替代遗留的 Stack 类。
-
Java LinkedList
- LinkedList 是一个双向链表,基于链表,实现了 Deque 接口和 List 接口,因此可以用于实现队列、双端队列和列表等数据结构。
- LinkedList 插入效率高是相对的,因为它省去了 ArrayList 插入数据可能的数组扩容和数据元素移动时所造成的开销,但数据扩容和数据元素移动却并不是时时刻刻都在发生的。
-
Java 数组队列
- ArrayDeque 基于数组,实现了 Deque 接口,可用于实现队列、双端队列等数据结构。
- ArrayDeque 类将元素内部存储在数组中。如果元素的数量超过数组的体积,则分配一个新数组,并将所有元素移过去。这意味着 ArrayDeque 根据需要增长。
-
Java 阻塞队列接口
- 阻塞队列的顶级接口是 java.util.concurrent.BlockingQueue ,它继承了 Queue。
- BlockingQueue 不接受 null 的插入,否则将抛出空指针异常。
- BlockingQueue 可能是有界的,如果在插入的时候发现队列满了,将会阻塞,而无界队列则有 Integer.MAX_VALUE 大的容量,并不是真的无界。
- BlockingQueue 通常用来作为生产者-消费者的队列的,但是它也支持 Collection 接口提供的方法,比如使用 remove(x) 来删除一个元素,但是这类操作并不是很高效,因此尽量在少数情况下使用。
- BlockingQueue 的实现都是线程安全的,所有队列的操作或使用内置锁或是其他形式的并发控制来保证原子。但是一些批量操作如:addAll、containsAll、retainAll 和 removeAll 不一定是原子的。
- 当队列满时,put 试图插入元素,将会一直阻塞插入的生产者线程,同理,队列为空时,如果消费者线程从队列里 take 获取元素,也会阻塞,直到队列不为空。
- 阻塞队列有 7 种实现类,其中最常用的是 ArrayBlockingQueue 和 LinkedBlockingQueue。
-
Java ArrayBlockingQueue
- ArrayBlockingQueue 是由数组构成的有界阻塞队列。
-
Java LinkedBlockingQueue
- 由链表构成的界限可选的阻塞队列,如不指定边界,则为 Integer.MAX_VALUE。
8. Java Map
-
Java Map 接口
- Map 用于保存具有映射关系(key-value)的数据
- Map 中的 key 和 value 都可以是任何引用类型的数据
- Map 接口的方法包括:
- clear() —— 清空 Map 集合
- containsKey(Object key) —— 判断指定的 key 是否存在
- containsValue(Object value) —— 判断指定的 value 是否存在
- entrySet() —— 将 Map 对象转换为 Set 集合
- equals(Object o) —— 对象比较
- get(Object key) —— 根据 key 取得 value
- hashcode() —— 返回哈希码
- isEmpty() —— 判断集合是否为空
- keySet() —— 取得所有的 key 并作为一个 Set 集合返回
- put(K key, V value) —— 向集合中加入元素
- putAll(Map t) —— 将一个 Map 集合中的内容加入到另一个 Map
- remove(Object key) —— 根据 key 删除 value 并返回 value
- size() —— 取出集合的长度(整型)
- values() —— 取出全部的 value 作为一个 Collection 返回
-
Java HashMap
- HashMap 内部类采用 Node
- 无序存放,key 不允许重复,线程不安全,高效
- JDK7 及以前的版本: HashMap 由数组 + 链表结构组成
- JDK8 及以后的版本: HashMap 由数组 + 链表 + 红黑树结构组成
- HashMap 是 Map 接口使用频率最高的实现类
- HashMap 判断两个 key 相等的标准: 两个 key 通过 equals() 方法返回 true,hashCode 值也相等
- HashMap 判断两个 value 相等的标准: 两个 value 通过 equals() 方法返回 true
-
Java LinkedHashMap
- LinkedHashMap 内部类采用 Entry
- LinkedHashMap 是 HashMap 的子类
- 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
- 与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序: 此迭代顺序与 key-value 对的插入顺序一致
-
Java WeakHashMap
- Java 引用共分为 4 种:
- 强引用(Strong References) —— 如果一个对象通过强引用链可达,它就不会被 gc 掉。
- 软引用(Soft References) —— 一个只被软引用的对象,如果内存不足了,会在下次 gc 的时候被处理掉。
- 弱引用(Weak References) —— 一个只被弱引用的对象,会在下次 gc 的时候被处理掉。
- 虚引用(Phantom References) —— 对对象的引用非常弱,弱到你不能通过 get 方法获取到对象(只会返回 null)。用来跟踪某个对象何时进入引用队列。
- 弱引用(是对于 key 来说),可及时(自动)回收
- 一旦弱引用开始返回 null,它指向的对象肯定已经被 gc 掉了
- 非线程安全,单纯作为 Map 没有 HashMap 好(比如 HashMap 在单链表过长时会自动转化为红黑树)
- 实现二级内存缓存时会经常用到 WeakHashMap
- Java 引用共分为 4 种:
-
Java EnumMap
- 一个用于存储 key 为枚举类型的 Map,底层使用数组(K、V 双数组)实现。
- 在 Java 中创建 EnumMap 时,必须指定枚举类型作为泛型类型。
public enum Color { RED, GREEN, BLUE } // 创建 EnumMap 对象时必须指定枚举类型作为泛型类型 EnumMap<Color, String> colorMap = new EnumMap<Color, String>(Color.class); // 使用 put 方法添加或修改元素 colorMap.put(Color.RED, "红色"); colorMap.put(Color.GREEN, "绿色"); colorMap.put(Color.BLUE, "蓝色");
- 速度快,省空间。
- 每个枚举值都有属性 ordinal (从 0 开始,有界),所以 EnumMap 的底层不需要通过链表实现。
-
Java SortedMap 接口
- 主要用于提供有序的 Map 实现。
- 不允许出现空键或空值。
- 键是按自然排序的,也可以按指定的比较器排序。
-
Java NavigableMap 接口
- NavigableMap 接口是 SortedMap 的子接口,额外提供了几个 SortedMap 接口没有的方法:
- descendingKeySet() —— 获取一个所有 key 的集合,顺序是倒序。
- descendingMap() —— 获取一个与原 Map 逆序的 Map (仍是同一个 Map,操作会相互影响)。
- headMap() —— 获取一个 Map,包含原 Map 中 key 小于指定 key 的所有键值对。如果第二个参数为 true 则表示包含等于。
- tailMap() —— 获取一个 Map,包含原 Map 中 key 大于指定 key 的所有键值对。如果第二个参数为 true 则表示包含等于。
- subMap() —— 获取一个 Map,包含从 fromKey (不包含)到 toKey (不包含)的所有键值对。如果有四个参数且第二、四个参数为 true 则表示包含等于。
- ceilingKey() —— 获取最接近的大于等于 key 的键。
- floorKey() —— 获取最接近的小于等于 key 的键。
- higherKey() —— 获取最接近的比 key 大的键。
- lowerKey() —— 获取最接近的比 key 小的键。
- ceilingEntry() —— 获取最接近的大于等于 key 的键值对。
- floorEntry() —— 获取最接近的小于等于 key 的键值对。
- higherEntry() —— 获取最接近的比 key 大的键值对。
- lowerEntry() —— 获取最接近的比 key 小的键值对。
- pollingFirstEntry() —— 移除第一个 entry (key + value) 并返回。如果 NavigableMap 是空的话则返回 null。
- pollingLastEntry() —— 移除最后一个 entry (key + value) 并返回。如果 NavigableMap 是空的话则返回 null。
- NavigableMap 接口是 SortedMap 的子接口,额外提供了几个 SortedMap 接口没有的方法:
-
Java TreeMap
- 实现了 SortedMap 接口,即实现了有序存放(默认按照 key 的值排序,所有的 key 必须是同一个类的对象),key 值不允许重复
- 实现了 NavigableMap 接口
- 可重写 comparator 方法来根据 value 进行排序
- TreeMap 底层采用红黑树结构存储数据
- TreeMap 判断两个 key 相等的标准: 两个 key 通过 compareTo() 或 compare() 方法返回 0
-
Java ConcurrentMap 接口
- 该接口提供了线程安全的映射,可在保持同步的同时让并发效率也比较高。即多个线程可以一次访问该映射,而不会影响映射中条目的一致性。
- ConcurrentMap 的专有方法:
- putIfAbsent() —— 如果指定的 key 尚未与任何值关联,则将指定的键/值插入到映射中,否则返回 key 对应的 value。
- compute() —— 计算指定键及其当前映射值的映射(如果找不到当前映射,则为 null)。用于自动更新 ConcurrentMap 中给定键的值。
- computeIfAbsent() —— 如果指定的键尚未与任何值关联,则将指定的键/值插入到映射中,否则返回 key 对应的 value。
- computeIfPresent() —— 如果指定的键存在,则使用指定的函数计算其值,否则将指定的键/值插入到映射中。
- forEach() —— 遍历 ConcurrentMap 中的所有键值对并执行指定的操作。
- merge() —— 如果指定的键存在,则使用指定的函数计算其值,否则将指定的键/值插入到映射中。
- 如果在 Java 8 的环境下使用 ConcurrentHashMap,一定要注意是否会并发对同一个 key 调用 computeIfAbsent,如果存在需要先尝试调用 get。如何在 JDK 1.8 中避免 computeIfAbsent() 的性能问题:
SQLExecutionUnitBuilder result; // 每次从 Map 中获取 value 前,都先用 get 做一次检查,value 不存在才使用 computeIfAbsent 放入 value。 if (null == (result = TYPE_TO_BUILDER_MAP.get(type))) { result = TYPE_TO_BUILDER_MAP.computeIfAbsent(type, key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class, key, new Properties())); } return result;
-
Java ConcurrentHashMap
- 与 HashMap 类似,唯一的区别就是其中的核心数据如 value,以及链表都是 volatile 修饰的,保证了获取时的可见性。
- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock,不会像 HashTable 那样不管是 put 还是 get 操作都需要同步处理。理论上 ConcurrentHashMap 支持 CurrencyLevel(Segment 数组数量) 的线程并发。每当线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
- ConcurrentHashMap 是线程安全的 Map,包含两个静态内部类 HashEntry 和 Segment:
- HashEntry —— 用来封装映射表的键/值对。
- Segment —— 用来充当锁的角色。每个 Segment 对象守护整个散列映射表的若干个桶(即 HashEntry 对象,若干个 HashEntry 对象组成 HashEntry 数组,相当于一个 HashMap)。每个桶是由若干个 HashEntry 对象链接起来的链表。
- ConcurrentHashMap 保证线程安全的方案:
- JDK 1.7: ReentrantLock + Segment + HashEntry
- JDK 1.8: synchronized + CAS + HashEntry + 红黑树
9. Java Set
-
Java Set 接口
- Set 是一个不包含重复元素的无序集合。
- 不允许存储重复元素,最多包含一个 null 元素。
- 没有索引,因此不能使用普通的 for 循环进行遍历,但可以使用迭代器。
- Set 接口的专有方法:
- int size() —— 获取此集合中的元素数量。
- boolean isEmpty() —— 如果此集合不包含任何元素,则返回true。
- boolean contains(Object o) —— 如果此集合包含指定的元素,则返回true。
- Iterator iterator() —— 返回在此集合中元素上进行迭代的迭代器。
- Object[] toArray() —— 将此集合中的元素作为数组返回。
- boolean add(E e) —— 将指定的元素添加到此集合中(可选操作)。
- boolean remove(Object o) —— 从此集合中删除指定的单个元素(如果存在)(可选操作)。
- boolean addAll(Collection<? extends E> c) —— 将指定集合中的所有元素添加到此集合中(可选操作)。
- boolean retainAll(Collection<?> c) —— 仅保留此集合中包含在指定集合中的元素(可选操作)。
- boolean removeAll(Collection<?> c) —— 删除此集合中包含在指定集合中的所有元素(可选操作)。
- void clear() —— 从此集合中删除所有元素(可选操作)。
- boolean containsAll(Collection<?> c) —— 如果此集合包含指定集合中的所有元素,则返回true。
- boolean removeIf(Predicate<? super E> filter) —— 删除符合给定谓词的此集合中的所有元素(可选操作)。
- boolean equals(Object o) —— 比较指定对象与此集合是否相等。
- int hashCode() —— 返回此集合的哈希码值。如果两个集合相等,则它们的哈希码也必须相等。
- Set 接口的常用场景:
- 去重。
- 集合运算。
- 缓存。
-
Java HashSet
- 基于哈希表(即 HashMap 实例)实现,说白了就是在 HashMap 上封装了一层,具体工作由 HashMap 完成。
- 不保证元素的迭代顺序(不保证该顺序很久不变)。
- 允许使用 null 元素。
-
Java EnumSet
- EnumSet 是一个抽象类。因此必须使用 EnumSet 提供的静态方法来创建实例:
- allOf(Class elementType) 用于创建一个包含指定枚举类型的全部枚举变量的集合。
enum Size { SMALL, MEDIUM, LARGE, XLARGE } EnumSet sizes = EnumSet.allOf(Size.class); // [SMALL, MEDIUM, LARGE, XLARGE]
- noneOf(Class elementType) 用于创建一个指定枚举类型的空集。
EnumSet sizes = EnumSet.noneOf(Size.class); // []
- range(E from, E to) 用于创建一个指定枚举类型的有序集合,包含从 from 到 to 的所有枚举变量。
EnumSet sizes = EnumSet.range(Size.SMALL, Size.LARGE); // [SMALL, MEDIUM, LARGE]
- of(E... e) 用于创建一个包含指定元素的集合。
EnumSet sizes = EnumSet.of(Size.SMALL, Size.LARGE); // [SMALL, LARGE]
- allOf(Class elementType) 用于创建一个包含指定枚举类型的全部枚举变量的集合。
- EnumSet 有两个实现类 RegularEnumSet、JumboEnumSet,当枚举类型的枚举变量小于等于 64 的时候使用 RegularEnumSet,否则就使用 JumboEnumSet (这两个类我们是没有办法使用的,因为它们不是 public 类)。大多时候,我们用的都是 RegularEnumSet。
- EnumSet 最有价值的是其内部实现原理,采用的是 Bit 来实现。
- EnumSet 的所有创建方法其实都是调用了 noneOf 创建方法,先创建一个空的 EnumSet (RegularEnumSet 或者JumboEnumSet),然后通过添加元素的方式将数据添加进去。
- EnumSet 是一个抽象类。因此必须使用 EnumSet 提供的静态方法来创建实例:
-
Java LinkedHashSet
- 在 LinkedHashMap 上封装了一层,具体工作由 LinkedHashMap 完成。
- LinkedHashSet 是由哈希表和链表共同实现的一个有序 Set,是 HashSet 的子类。
-
Java SortedSet 接口
- 默认按照升序维护集合中元素。
- 根据元素的自然顺序或根据 SortedSet 创建时提供的 Comparator 进行排序
- 范围试图,即允许对已排序集进行任意范围操作:
- SortedSet subSet(E from, E to) —— 返回从 from (包括)到 to (不包括)的子集。
- SortedSet headSet(E to) —— 返回 SortedSet 的初始部分,直到但不包括 to。
- SortedSet tailSet(E from) —— 返回 SortedSet 的后续部分,从 from (包括)开始。
- 端点,即返回有序集合中的第一个或最后一个元素:
- E first() —— 返回集合中的第一个元素。
- E last() —— 返回集合中的最后一个元素。
- 比较器访问,即返回用于对集合进行排序的 Comparator(如果有):
- Comparator<? super E> comparator() —— 返回用于对集合进行排序的 Comparator。如果集合根据其元素的自然顺序排序,则为 null。
- SortedSet 从 Set 继承的操作在有序集和普通集上的行为大致相同,除了 Iterator() 和 toArray() 方法都会按照顺序进行操作。
-
Java NavigableSet 接口
- NavigableSet 接口是在 SortedSet 接口的基础上做了一些补充:
- 新增了控制参数,用来控制两边的开闭。
- 支持反向遍历(SortedSet 仅支持获取最后一位元素)。
- 支持负无穷大到某个数这样的区间的求解。
- NavigableSet 不支持 null。
- NavigableSet 接口是在 SortedSet 接口的基础上做了一些补充:
-
Java TreeSet
- 在 TreeMap 上封装了一层,具体工作由 TreeMap 完成(TreeSet 在添加元素时,会把元素放入 TreeMap 中的 key 上来确保元素的唯一性,并让其 value 指向一个空对象)。
- 内部基于红黑树实现的一个有序集合。
- 添加到 TreeSet 的元素必须是可排序的。
- TreeSet 不允许使用 null 元素。
- TreeSet 提供了倒序输出的方法:
- descendingSet() —— 返回一个逆序的 Set 视图。
- descendingIterator() —— 返回一个逆序迭代器。
- 当 TreeSet 的泛型对象不是 Java 的基本类型的包装类时,对象需要重写 Comparable#compareTo() 方法
-
Java 集合算法
-
Collections.sort(List list[, Comparator comparator]) —— 列表排序
-
Collections.bianrySearch(List list, T key[, Comparator comparator]) —— 二分搜索键
-
Collections.shuffle(List list[, Random rnd]) —— 对一个列表进行随机排序
-
Collections.reverse(List list) —— 对一个列表进行反转
-
Collections.swap(List list, int i, int j) —— 对一个列表的两个元素进行互换
-
Collections.rotate(List list, int count) —— 列表各元素从左往右循环位移 count 次
-
Collections.asLifoQueue(Deque deque) —— 创建 Deque 的 LIFO 队列视图
-
Collections.newSetFromMap(Map<E, Boolean> map) —— 将 Map 的实例用作 Set 实现
-
Collections.unmodifiableCollection(Collection<? extends T> c) —— 获取集合的只读视图
-
Collections.unmodifiableList(List<? extends T> list) —— 获取列表的只读视图
-
Collections.unmodifiableMap(Map<? extends K, ? extends V> m) —— 获取映射的只读视图
-
Collections.unmodifiableSet(Set<? extends T> s) —— 获取集合的只读视图
-
...
-
Collections.synchronizedCollection(Collection c) —— 获取集合的同步视图
-
Collections.synchronizedList(List list) —— 获取列表的同步视图
-
Collections.synchronizedMap(Map<K,V> m) —— 获取映射的同步视图
-
Collections.synchronizedSet(Set s) —— 获取集合的同步视图
-
...
-
Collections.checkedCollection(Collection c, Class type) —— 获取特定类型的已检查集合
-
Collections.checkedList(List list, Class type) —— 获取特定类型的已检查列表
-
Collections.checkedMap(Map<K,V> m, Class keyType, Class valueType) —— 获取特定类型的已检查映射
-
Collections.checkedSet(Set s, Class type) —— 获取特定类型的已检查集合
-
...
-
案例
// 集合的泛型被绕过了 Set<String> s = new HashSet<>(); s.add("Hello"); Set s2 = s; s2.add(new Integer(123)); // Legal // 集合的泛型无法被绕过 Set<String> checkedSet = Collections.checkedSet(new HashSet<String>(), String.class); Set s2 = checkedSet; s2.add(new Integer(123)); // Throws ClassCastException
-
Collections.emptyList() —— 创建一个不可变的空列表
-
Collections.emptyMap() —— 创建一个不可变的空映射
-
Collections.emptySet() —— 创建一个不可变的空集合
-
Collections.emptyIterator() —— 创建一个不可变的空迭代器
-
...
-
Collections.singleton(T o) —— 创建一个只有一个元素的集合
-
Collections.singletonList(T o) —— 创建一个只有一个元素的列表
-
Collections.singletonMap(K key, V value) —— 创建一个只有一个元素的映射
-
案例
Set<String> singletonSet = Collections.singleton("Lonely"); singletonSet.add("Hello"); // Throws a runtime exception
-
-
Java 迭代器接口
- 用于遍历集合(如 列表、映射、集合 等)。
- 迭代器接口的最常用方法:
- next() —— 返回迭代器的下一个元素,并将迭代器的指针移到下一个位置。
- hasNext() —— 用于判断集合中是否还有下一个元素可访问。
- remove() —— 从集合中删除迭代器最后访问的元素。
- 集合对象想获取一个迭代器可以使用 iterator() 方法。
- 让迭代器逐个返回集合中所有元素最简单的方法是使用 while 循环:
while(it.hasNext()) { System.out.println(it.next()); }
- 迭代器是一种单向遍历机制,即只能从前往后遍历集合中的元素,不能往回遍历。
- 在使用迭代器遍历集合时,不能直接修改集合中的元素,而是需要使用迭代器的 remove() 方法来删除当前元素。
-
Java ListIterator 接口
- ListIterator 提供了双向访问列表元素的功能。只用于 List 对象。
- 列表对象想获取一个迭代器可以使用 listIterator() 方法。
- ListIterator 迭代器的最常用方法:
- hasNext() —— 如果列表中存在元素,则返回 true
- next() —— 返回列表的下一个元素
- nextIndex() —— 返回 next() 方法将返回的元素的索引
- previous() —— 返回列表的前一个元素
- previousIndex() —— 返回 previous() 方法将返回的元素的索引
- remove() —— 删除由 next() 或 previous() 返回的元素
- set() —— 将 next() 或 previous() 返回的元素替换为指定的元素