Java基础
Java基础
Java特性
封装
封装: 封装就是通过抽象,将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体
数据被保护在类的内部, 尽可能隐藏内部的实现细节, 只保留一部分对外接口使之与外部发生联系
其他对象只能通过已经授权的操作来与这个封装的对象进行交互.也就是说对象无法也无需知道对象内部的细节, 但可以通过该对象对外提供的接口来访问该对象
封装的好处
1.良好的封装能够减少耦合
2.类内部的结构可以自由修改
3.可以对成员进行更精确的控制
4.隐藏信息,实现细节
继承
继承: 子类继承父类的属性和方法,使得子类对象具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法
继承的好处
1.不仅可以直接使用父类的方法,还能拓展自己的独有方法
2.继承只能是单继承,即一个儿子只能有一个爸爸
3.可以通过多实现,达到多继承的目的,但是Java中没有多继承的说法
如何实现多继承?
1.内部类:可以继承一个与外部类无关的,保证内部类的独立性,正是基于这一点,可以达到多继承的效果。
即类A中有内部类B1,C1, 其中B1可以继承类B,C1可以继承类C, 类A也可以调用类B,类C的方法
2.多层继承:子类继承父类,父类再继承其他的类,这样子类拥有所有被继承类的属性和方法
3.实现接口
如何实现继承?
1.使用extends关键字, class 子类名 extends 父类名{}
2.子类继承父类后,就拥有父类的非私有的属性和方法
3.使用 implements 关键字可以变相使 Java 拥有多继承的特性,使用范围为类实现接口的情况,一个类可以实现多个接口(接口与接口之间用逗号分开)
继承的特点:
1.父类的构造方法不能被继承
2.子类的构造过程,必须调用父类的构造方法. Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加
3.如果子类的构造过程没有显性的调用父类的构造方法,则系统默认调用父类的无参构造方法
方法的重载和重写
方法的重载和重写
1.Override:方法重写
1.1.发生在子类与父类之间
1.2.重写的方法必须和父类中的方法有着相同的名字
1.3.重写的方法必须和父类中的方法有着相同的参数
1.4.只能重写继承过来的方法,即只能重写public, protected, default修饰的方法,不能重写private修饰的方法
1.5.final, static修饰的方法不能重写.
final修饰的方法都不能被继承,自然不能被重写
静态方法可用于父类以及子类的所有实例, 重写的目的在于根据对象的类型不同而表现出多态,而静态方法不需要创建对象就可以使用。没有了对象,重写所需要的“对象的类型”也就没有存在的意义了
1.6.重写的方法必须要有相同的参数列表
1.7.重写的方法必须返回相同的类型
1.8.重写的方法不能使用限制等级更严格的权限修饰符
1.9.重写的方法不能抛出比父类等级更高的异常
1.10.可以在子类中通过super关键字调用父类中被重写的方法
1.11.构造方法不能被重写
1.12.如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写
1.13.synchronized, strictfp关键字对于重写方法没有要求
2.Overload:方法重载, 如果一个类有多个名字相同但参数个数, 参数类型, 参数位置不同的方法
this, super关键字
this,super关键字
this
1.作为引用变量,指向当前对象
2.this()可以调用当前类的构造方法
3.this可以作为参数在方法中传递
4.this可以作为参数在构造方法中传递
5.this可以作为方法的返回值,返回当前类的对象
super
1.指向父类对象
2.调用父类的方法
3.super()调用父类的构造方法
Object类和转型
Object类和转型
Object:
1.Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
2.Java 中,所有的对象都拥有 Object 的默认方法。
3.Object 类有一个构造方法,并且是无参构造方法
向上转型:
通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换
父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法
向下转型:
通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号()强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型
子类引用变量指向父类引用变量指向的对象后(一个 Son()对象),就完成向下转型,就可以调用一些子类特有而父类没有的方法
多态
多态: 指在面向对象编程中,同一个类的对象在不同情况下表现出来的不同行为和状态
1.子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
2.子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
3.可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。
多态的前提条件有三个:
1.子类继承父类
2.子类重写父类的方法
3.父类引用指向子类的对象
子父类初始化顺序
1.父类中静态成员变量和静态代码块
2.子类中静态成员变量和静态代码块
3.父类中普通成员变量和代码块,父类的构造方法
4.子类中普通成员变量和代码块,子类的构造方法
总的来说,就是静态>非静态,父类>子类,非构造方法>构造方法。
同一类别(例如普通变量和普通代码块)成员变量和代码块执行从前到后
访问权限修饰符
一共四种访问权限修饰符
public, protected, default, private
修饰类:
类只能使用public和default
public:用于修饰类,表示该类对其他所有的类都可见
dafault:用来修饰类的话,表示该类只对同一个包中的其他类可见
修饰方法和变量
1.dafault:默认访问权限(包访问权限), 如果一个类的方法或变量被包访问权限修饰,也就意味着只能在同一个包中的其他类中显示地调用该类的方法或者变量,在不同包中的类中不能显式地调用该类的方法或变量
2.private:如果一个类的方法或者变量被 private 修饰,那么这个类的方法或者变量只能在该类本身中被访问,在类外以及其他类中都不能显式的进行访问
3.protected:如果一个类的方法或者变量被 protected 修饰,对于同一个包的类,这个类的方法或变量是可以被访问的。对于不同包的类,只有继承于该类的类才可以访问到该类的方法或者变量
4.public:被 public 修饰的方法或者变量,在任何地方都是可见的
非访问权限修饰符
static, final, abstract
static:
1.static 翻译为“静态的”,能够与变量,方法和类一起使用,称为静态变量,静态方法(也称为类变量、类方法)。如果在一个类中使用 static 修饰变量或者方法的话,它们可以直接通过类访问,不需要创建一个类的对象来访问成员
final: 表示"最后的、最终的"含义
final变量:
变量一旦赋值后,不能被重新赋值。被 final 修饰的实例变量必须显式指定初始值(即不能只声明)。final 修饰符通常和 static 修饰符一起使用来创建类常量
final方法:
父类中的 final 方法可以被子类继承,但是不能被子类重写。声明 final 方法的主要目的是防止该方法的内容被修改
final类:
final 类不能被继承,没有类能够继承 final 类的任何特性
abstract:英文名为“抽象的”,主要用来修饰类和方法,称为抽象类和抽象方法
抽象方法:
有很多不同类的方法是相似的,但是具体内容又不太一样,所以我们只能抽取他的声明,没有具体的方法体,即抽象方法可以表达概念但无法具体实现
抽象类:
有抽象方法的类必须是抽象类,抽象类可以表达概念但是无法构造实体的类
Java三大版本
JDK, JRE, JVM
JDK(Java Development Kit): 是用于开发 Java 应用程序的软件环境。里面包含运行时环境(JRE)和其他 Java 开发所需的工具,比如说解释器(java)、编译器(javac)、文档生成器(javadoc)等等。
JRE(Java Runtime Environment): 是用于运行 Java 应用程序的软件环境。也就是说,如果只想运行 Java 程序而不需要开发 Java 程序的话,只需要安装 JRE 就可以了。
JVM(Java Virtual Machine): 也就是 Java 虚拟机,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成,屏蔽了不同操作系统(macOS、Windows、Linux)的差异性,使得 Java 能够“一次编译,到处运行”

注释
// 单行注释
/*
多行注释,多行注释使用/**/
*/
/**
* 文档注释
* 文档注释使用/** */
* 常用在类,变量,方法上表示用途
*/
标识符和关键字
1.abstract: 用于声明抽象类和抽象方法
2.boolean: 基本数据类型,用来标识真(true)假(false),常用于判断条件、循环控制和逻辑运算等场景
3.break: 用于跳出循环结构(如 for、while 和 do-while 循环)或 switch 语句。
3.1.当遇到 break 语句时,程序将立即跳出当前循环或 switch 语句,继续执行紧跟在循环或 switch 语句后面的代码
4.byte: 基本数据类型,用于表示一个 8 位(1 字节)有符号整数。
4.1.它的值范围是 -128(-2^7)到 127(2^7 - 1)。
4.2.由于 byte 类型占用的空间较小,它通常用于处理大量的数据,如文件读写、网络传输等场景,以节省内存空间
5.case: 通常与 switch 语句一起使用。
5.1.switch 语句允许根据某个变量的值来选择执行不同的代码块。在 switch 语句中,case 用于标识每个可能的值和对应的代码块
6.catch: 用于捕获 try 语句中的异常。
6.1.在 try 块中可能会抛出异常,而在 catch 块中可以捕获这些异常并进行处理。
6.2.catch 块可以有多个,每个 catch 块可以捕获特定类型的异常。
6.3.在 catch 块中,可以根据需要进行异常处理,例如输出错误信息、进行日志记录、恢复程序状态等
7.char: 基本数据类型,用于声明一个字符类型的变量。
7.1.char 类型的变量可以存储任意的 Unicode 字符,可以使用单引号将字符括起来来表示
8.class: 表示一个类
9.continue: 用于继续下一个循环,可以在指定条件下跳过其余代码
10.default: 权限修饰符, 用于指定 switch 语句中除去 case 条件之外的默认代码块
11.do: 通常和 while 关键字配合使用,do 后紧跟循环体
12.double: 基本数据类型,用于声明一个双精度浮点类型的变量
13.else: 用于指示 if 语句中的备用分支
14.enum: 用于定义一组固定的常量(枚举)
15.extends: 用于指示一个类是从另一个类或接口继承的
16.final: 用于表示某个变量、方法或类是最终的,不能被修改或继承
16.1.final 变量:表示一个常量,一旦被赋值,其值就不能再被修改。这在声明不可变的值时非常有用
16.2.final 方法表示一个不能被子类重写的方法。这在设计类时,确保某个方法的实现不会被子类修改时非常有用
16.3.final 类表示一个不能被继承的类。这在设计类时,确保其不会被其他类继承时非常有用。String 类就是 final 的
17.finally: 和 try-catch 配合使用,表示无论是否处理异常,总是执行 finally 块中的代码
18.float: 基本数据类型,表示单精度浮点数
18.1.在 Java 中,浮点数默认是 double 类型
18.2.如果要使用 float 类型的数据,需要在数字后面加上一个 f 或者 F,表示这是一个 float 类型的字面量。
18.3.另外,也可以使用科学计数法表示浮点数
19.for: 用于声明一个 for 循环,如果循环次数是固定的,建议使用 for 循环
20.if: 用于指定条件,如果条件为真,则执行对应代码
21.implements: 用于实现接口
22.import: 用于导入对应的类或者接口
23.instanceof: 用于判断对象是否属于某个类型(class)
24.int: 基本数据类型, 用来表示一个整数
25.interface: 用于声明接口。会定义一组方法的签名(即方法名、参数列表和返回值类型),但没有方法体。其他类可以实现接口,并提供方法的具体实现
26.long: 用于表示长整数值
27.native: 用于声明一个本地方法,本地方法是指在 Java 代码中声明但在本地代码(通常是 C 或 C++ 代码)中实现的方法,它通常用于与操作系统或其他本地库进行交互
28.new: 用于创建一个新的对象
29.null: 如果一个变量是空的(什么引用也没有指向),就可以将它赋值为 null,和空指针异常息息相关
30.package: 用于声明类所在的包
31.private: 权限修饰符, 表示方法或变量只对当前类可见
32.protected: 访问权限修饰符,表示方法或变量对同一包内的类和所有子类可见
33.public: 访问权限修饰符,除了可以声明方法和变量(所有类可见),还可以声明类。main() 方法必须声明为 public
34.return: 用于从方法中返回一个值或终止方法的执行。return 语句可以将方法的计算结果返回给调用者,或者在方法执行到某个特定条件时提前结束方法
35.short: 基本数据类型,用于表示短整数,占用 2 个字节(16 位)的内存空间
36.static: 表示该变量或方法是静态变量或静态方法
37.strictfp: 并不常见,通常用于修饰一个方法,用于限制浮点数计算的精度和舍入行为。当你在类、接口或方法上使用 strictfp 时,该范围内的所有浮点数计算将遵循 IEEE 754 标准的规定,以确保跨平台的浮点数计算的一致性
38.super: 可用于调用父类的方法或者字段
39.switch: 用于根据某个变量的值选择执行不同的代码块。
39.1.switch 语句通常与 case 和 default 一起使用。
39.2.每个 case 子句表示一个可能的值和对应的代码块,而 default 子句用于处理不在 case 子句中的值
40.synchronized: 用于指定多线程代码中的同步方法、变量或者代码块
41.this: 可用于在方法或构造方法中引用当前对象
42.throw: 主动抛出异常
43.throws: 用于声明异常
44.transient: 修饰的字段不会被序列化
45.try: 用于包裹要捕获异常的代码块
46.void: 用于指定方法没有返回值
47.volatile: 保证不同线程对它修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的
48.while: 如果循环次数不固定,建议使用 while 循环
49.goto和const:
49.1.goto 在 C语言中叫做‘无限跳转’语句,在 Java 中,不再使用 goto 语句,因为无限跳转会破坏程序结构
49.2.const 在 C语言中是声明常量的关键字,在 Java 中可以使用 public static final 三个关键字的组合来达到常量的效果
数据类型

变量分为局部变量、成员变量、静态变量。
当变量是局部变量的时候,必须得先初始化,否则编译器不允许你使用它
当变量是成员变量或者静态变量时,可以不进行初始化,它们会有一个默认值
| 数据类型 | 默认值 | 大小 | 范围 |
|---|---|---|---|
| boolean | false | 1位 | true / false |
| char | ‘\u0000’ | 2 字节 | 0 到 65,535 |
| byte | 0 | 1 字节 | -128 到 127(包括 -128 和 127) |
| short | 0 | 2 字节 | -32,768 和 32,767 之间,包含 32,767 |
| int | 0 | 4 字节 | -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之间 |
| long | 0L | 8 字节 | -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之间 |
| float | 0.0f | 4 字节 | 1.4E-45 到 3.4E+38(单精度浮点数的有效数字大约为 6 到 7 位) |
| double | 0.0 | 8 字节 | 大约 ±4.9E-324 到 ±1.7976931348623157E308(双精度浮点数的有效数字大约为 15 到 17 位) |
数据类型转换
自动类型转换
自动类型转换(自动类型提升)是 Java 编译器在不需要显式转换的情况下,将一种基本数据类型自动转换为另一种基本数据类型的过程
规则:
1.如果任一操作数是 double 类型,其他操作数将被转换为 double 类型。
2.否则,如果任一操作数是 float 类型,其他操作数将被转换为 float 类型。
3.否则,如果任一操作数是 long 类型,其他操作数将被转换为 long 类型。
4.否则,所有操作数将被转换为 int 类型
较大的数据类型可以容纳较小数据类型的所有可能值。
byte -> short -> int -> long -> float -> double
char -> int -> long -> float -> double
强制类型转换
Java 中将一种数据类型显式转换为另一种数据类型的过程
注意事项:
1.将较大的数据类型转换为较小的数据类型。
2.将浮点数转换为整数。
3.将字符类型转换为数值类型
4.强制类型转换可能会导致数据丢失或精度降低,因为目标类型可能无法容纳原始类型的所有可能值
在进行强制类型转换时,需要确保转换后的值仍然在目标类型的范围内
double -> float -> long -> int -> char -> short -> byte
变量, 常量, 作用域
Java变量就相当于是一个容器,可以保存程序在运行过程中的值,在声明的时候会定义对应的数据类型.
变量根据作用域的范围又可以分为三种类型: 局部变量, 成员变量, 静态变量
局部变量
在方法体内声明的变量被称为局部变量,该变量只能在该方法内使用,类中的其他方法并不知道该变量
声明局部变量时需要注意的细节:
1.局部变量声明在方法, 构造方法, 或者语句块中
2.局部变量在方法, 构造方法, 语句块被执行的时候创建,当它们执行完成后, 将会被销毁
3.访问修饰符不能用于局部变量
4.局部变量只在声明他的方法, 构造方法, 语句块中可见
5.局部变量是在栈上分配的
6.局部变量没有默认值, 所以局部变量被声明后,必须经过初始化,才可以使用
成员变量
在类内部但在方法体外声明的变量称为成员变量,或者实例变量,或者字段。
之所以称为实例变量,是因为该变量只能通过类的实例(对象)来访问
声明成员变量时需要注意的细节:
1.成员变量声明在一个类中,但在方法, 构造方法, 语句块之外
2.当一个对象被实例化之后,每个成员变量的值就跟着确定
3.成员变量在对象创建的时候创建,在对象销毁的时候销毁
4.成员变量的值应该至少被一个方法, 构造方法, 语句块引用,是的外部能够通过这些方式获取实例变量信息
5.成员变量可以声明在使用前或者使用后
6.访问修饰符可以修饰成员变量
7.成员变量对于类中的方法, 构造方法, 语句块是可见的
8.一般情况下应该把成员变量设为私有, 通过使用访问修饰符可以是成员变量对于子类可见
9.成员变量具有默认值: 数值类型变量默认值是0, 布尔类型默认值是false, 引用类型变量默认值是null
10.变量的值可以在声明时指定,也可以在构造方法中指定
静态变量
通过 static 关键字声明的变量被称为静态变量(类变量),它可以直接被类访问
声明静态变量时需要注意的细节:
1.静态变量在类中以static关键字声明,但必须在方法, 构造方法, 语句块之外
2.无论一个类创建了多少对象,类只拥有静态变量的一份拷贝
3.静态变量除了被声明为常量外,很少使用
4.静态变量储存在静态存储区
5.静态变量在程序开始时创建,在程序结束时销毁
6.与成员变量具有相似的可见性,但是为了对类的使用者可见,大多数静态变量声明为public类型
7.静态变量的默认值和实例变量相似
8.静态变量还可以在静态代码块中初始化
常量
使用 final 关键字修饰的成员变量。常量的值一旦给定就无法改变,常量名必须大写
运算符
算术运算符
+, -, *, /, %, ++, --
前缀自增自减法(++a,--a): 先进行自增或者自减运算,再进行表达式运算。
后缀自增自减法(a++,a--): 先进行表达式运算,再进行自增或者自减运算
关系运算符
==, !=, >, >=, <, <=
位运算符
Java定义了位运算符,应用于整数类型(int),长整型(long),短整型(short),字符型(char),和字节型(byte)等类型。
位运算符作用在所有的位上,并且按位运算
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A | B = 0011 1101
A ^ B = 0011 0001
~A= 1100 0011
& 如果相对应位都是1,则结果为1,否则为0 (A&B),得到12,即0000 1100
| 如果相对应位都是0,则结果为0,否则为1 (A | B)得到61,即 0011 1101
^ 如果相对应位值相同,则结果为0,否则为1 (A ^ B)得到49,即 0011 0001
〜 按位取反运算符翻转操作数的每一位,即0变成1,1变成0。 (〜A)得到-61,即1100 0011
<< 按位左移运算符。左操作数按位左移右操作数指定的位数。 A << 2得到240,即 1111 0000
>> 按位右移运算符。左操作数按位右移右操作数指定的位数。 A >> 2得到15即 1111
>>> 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。
逻辑运算符
假设布尔变量A为真,变量B为假
&& 称为逻辑与运算符。当且仅当两个操作数都为真,条件才为真。 (A && B)为假。
|| 称为逻辑或操作符。如果任何两个操作数任何一个为真,条件为真。 (A || B)为真。
! 称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。 !(A && B)为真。
赋值运算符
= 简单的赋值运算符,将右操作数的值赋给左侧操作数 C = A + B将把A + B得到的值赋给C
+ = 加和赋值操作符,它把左操作数和右操作数相加赋值给左操作数 C + = A等价于C = C + A
- = 减和赋值操作符,它把左操作数和右操作数相减赋值给左操作数 C - = A等价于C = C - A
* = 乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数 C * = A等价于C = C * A
/ = 除和赋值操作符,它把左操作数和右操作数相除赋值给左操作数 C / = A,C 与 A 同类型时等价于 C = C / A
(%)= 取模和赋值操作符,它把左操作数和右操作数取模后赋值给左操作数 C%= A等价于C = C%A
<< = 左移位赋值运算符 C << = 2等价于C = C << 2
>> = 右移位赋值运算符 C >> = 2等价于C = C >> 2
&= 按位与赋值运算符 C&= 2等价于C = C&2
^ = 按位异或赋值操作符 C ^ = 2等价于C = C ^ 2
| = 按位或赋值操作符 C | = 2等价于C = C | 2
条件运算符
variable x = (expression) ? value if true : value if false
instanceof 运算符
该运算符用于操作对象实例,检查该对象是否是一个特定类型(类类型或接口类型)。
instanceof运算符使用格式如下:
(Object reference variable) instanceof (class/interface type)
包机制
1.Java 中的包主要是为了防止类文件命名冲突以及方便进行代码组织和管理;
2.对于一个 Java 源代码文件,如果存在 public 类的话,只能有一个 public 类,且此时源代码文件的名称必须和 public 类的名称完全相同。
3.另外,如果还存在其他类,这些类在包外是不可见的。如果源代码文件没有 public 类,则源代码文件的名称可以随意命名。
Java流程控制
if-else语句
if-else语句
1.if语句 格式:
if(布尔表达式){
// 如果条件为 true,则执行这块代码
}
2.if-else语句 格式:
if(布尔表达式){
// 条件为 true 时执行的代码块
}else{
// 条件为 false 时执行的代码块
}
如果执行语句比较简单的话,可以使用三元运算符来代替 if-else 语句
如果条件为 true,返回 ? 后面 : 前面的值;
如果条件为 false,返回 : 后面的值
3.if-else-if语句 格式:
if(条件1){
// 条件1 为 true 时执行的代码
}else if(条件2){
// 条件2 为 true 时执行的代码
}else if(条件3){
// 条件3 为 true 时执行的代码
}
...
else{
// 以上条件均为 false 时执行的代码
}
4.if嵌套语句 格式:
if(外侧条件){
// 外侧条件为 true 时执行的代码
if(内侧条件){
// 内侧条件为 true 时执行的代码
}
}
switch语句
switch语句
switch 语句用来判断变量与多个值之间的相等性
变量的类型可以是:
1.byte、short、char、int:基本整数类型。
2.String:字符串类型。
3.枚举类型:自定义的枚举类型。
4.包装类:如 Byte、Short、Character、Integer。
5.但 switch 不支持 long、float、double 类型,这是因为:
5.1.long 是 64 位整数,不在 switch 一开始设计的范围内(32 位的 int 在大多数情况下就够用了)。
5.2.float 和 double 是浮点数,浮点数的比较不如整数简单和直接,存在精度误差。
格式:
switch(变量) {
case 可选值1:
// 可选值1匹配后执行的代码;
break; // 该关键字是可选项
case 可选值2:
// 可选值2匹配后执行的代码;
break; // 该关键字是可选项
......
default: // 该关键字是可选项
// 所有可选值都不匹配后执行的代码
}
注意事项:
1.变量可以有 1 个或者 N 个值。
2.值类型必须和变量类型是一致的,并且值是确定的。
3.值必须是唯一的,不能重复,否则编译会出错。
4.break 关键字是可选的,如果没有,则执行下一个 case,如果有,则跳出 switch 语句。
5.default 关键字也是可选的
for循环
for循环
普通for循环
组成:
1.初始变量:循环开始执行时的初始条件
2.条件:循环每次执行时要判断的条件,如果为 true,就执行循环体;如果为 false,就跳出循环。当然了,条件是可选的,如果没有条件,则会一直循环
3.循环体:循环每次要执行的代码块,直到条件变为 false
4.自增/自减:初始变量变化的方式
格式:
for(初始变量;条件;自增/自减){
// 循环体
}
for-each
for-each 循环通常用于遍历数组和集合,它的使用规则比普通的 for 循环还要简单,不需要初始变量,不需要条件,不需要下标来自增或者自减
格式:
for(元素类型 元素 : 数组或集合){
// 要执行的代码
}
死循环
格式:
for(;;){
System.out.println("停不下来。。。。");
}
while循环
while循环
格式:
while(条件){
//循环体
}
死循环:
while (true) {
System.out.println("停不下来。。。。");
}
do-while循环
do-while循环
格式:
do{
// 循环体
}while(条件);
死循环:
do {
System.out.println("停不下来。。。。");
} while (true);
三种循环的区别
for循环, while循环, do-while循环区别
1.for循环,循环次数是固定的
2.while循环,循环次数不固定
3.do-while循环,循环次数不固定,并且至少要执行一次循环
关键字
break
1.break 关键字通常用于中断循环或 switch 语句,它在指定条件下中断程序的当前流程。如果是内部循环,则仅中断内部循环
2.可以将 break 关键字用于所有类型循环语句中,比如说 for 循环、while 循环,以及 do-while 循环
continue
1.当我们需要在 for 循环或者 (do)while 循环中立即跳转到下一个循环时,就可以使用 continue 关键字
2.通常用于跳过指定条件下的循环体,如果循环是嵌套的,仅跳过当前循环
Java方法
如何声明方法
如何声明方法?
格式:
访问权限 返回类型 方法名 (参数类型 形参) {
// 方法体
}
1.访问权限
1.1.public:该方法可以被所有类访问。
1.2.private:该方法只能在定义它的类中访问。
1.3.protected:该方法可以被同一个包中的类,或者不同包中的子类访问。
1.4.default:如果一个方法没有使用任何访问权限修饰符,那么它是 package-private 的,意味着该方法只能被同一个包中的类可见
2.返回类型:方法返回的数据类型,可以是基本数据类型、对象和集合,如果不需要返回数据,则使用 void 关键字
3.方法名
3.1.方法名最好反应出方法的功能
3.2.方法名最好是一个动词,并且以小写字母开头。如果方法名包含两个以上单词,那么第一个单词最好是动词,然后是形容词或者名词,并且要以驼峰式的命名方式命名
3.3.一个方法可能与同一个类中的另外一个方法同名,这被称为方法重载
4.参数:参数被放在一个圆括号内,如果有多个参数,可以使用逗号隔开。参数包含两个部分,参数类型和参数名。如果方法没有参数,圆括号是空的
实例方法
实例方法
1.没有使用 static 关键字修饰,但在类中声明的方法被称为实例方法
2.在调用实例方法之前,必须创建类的对象
静态方法
静态方法
1.static 关键字修饰的方法就叫做静态方法
2.当我们调用静态方法的时候,就不需要 new 出来类的对象,就可以直接调用静态方法
抽象方法
抽象方法
1.没有方法体的方法被称为抽象方法,它总是在抽象类中声明
2.如果类有抽象方法的话,这个类就必须是抽象的
3.可以使用 abstract 关键字创建抽象方法和抽象类。
4.当一个类继承了抽象类后,就必须重写抽象方法
可变参数
可变参数
1.可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(is-a)的值作为参数
2.可变参数必须要在参数列表的最后一位
3.当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法
数组
数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。
数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的
数组元素的类型可以是基本数据类型(比如说 int、double),也可以是引用数据类型(比如说 String),包括自定义类型
数组的声明和初始化
数组的声明和初始化
数组的声明
1.int[] anArray;
2.int anOtherArray[];
3.二者的区别就在于[]的位置,第一种方式用的比较多
数组的初始化
1.int[] anArray = new int[10];
(初始化数组的时候,使用了new关键字,说明数组确实是一个对象,基本数据类型是不用的(基本数据的包装类型是可以 new 的,包装类型就是对象))
数组中的每个元素都会被初始化为默认值,int 类型的就为 0,Object 类型的就为 null。 不同数据类型的默认值不同
2.int anOtherArray[] = new int[] {1, 2, 3, 4, 5};
数组的常用操作
数组的常用操作
1.通过索引访问数组的元素
anOtherArray[0] = 1;
1.1如果索引的值超出了数组的界限,就会抛出 ArrayIndexOutOfBoundException。
1.2.由于数组的索引是从 0 开始,所以最大索引为 length - 1,不要使用超出这个范围内的索引访问数组,否则就会抛出数组越界的异常
2.使用for循环遍历数组
int anOtherArray[] = new int[] {1, 2, 3, 4, 5};
for (int i = 0; i < anOtherArray.length; i++) {
System.out.println(anOtherArray[i]);
}
3.使用for-each循环
for (int element : anOtherArray) {
System.out.println(element);
}
可变参数和数组
在 Java 中,可变参数用于将任意数量的参数传递给方法
void varargsMethod(String... varargs) {}
该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。
反编译之后
transient void varargsMethod(String as[]){}
所以我们可以直接将数组作为参数传递给该方法
VarargsDemo demo = new VarargsDemo();
String[] anArray = new String[] {"沉默", "一枚有趣的程序员"};
demo.varargsMethod(anArray);
也可以直接传递多个字符串,通过逗号隔开的方式
demo.varargsMethod("沉默", "一枚有趣的程序员");
数组和List
1.可以通过遍历的方式,将数组中的元素添加进集合中
int[] anArray = new int[] {1, 2, 3, 4, 5};
List<Integer> aList = new ArrayList<>();
for (int element : anArray) {
aList.add(element);
}
2.通过Arrays类,将数组中的元素添加进集合中
List<Integer> aList = Arrays.asList(anArray);
或者
List<Integer> aList = Arrays.stream(anArray).boxed().collect(Collectors.toList());
注意:
1.Arrays.asList 方法返回的 ArrayList 并不是 java.util.ArrayList,它其实是 Arrays 类的一个内部类
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable{}
2.如果需要添加元素或者删除元素的话,需要把它转成 java.util.ArrayList
new ArrayList<>(Arrays.asList(anArray));
3.Java 8 新增了 Stream 流的概念,这就意味着我们也可以将数组转成 Stream 进行操作。
String[] anArray = new String[] {"沉默", "一枚有趣的程序员", "好好珍重他"};
Stream<String> aStream = Arrays.stream(anArray);
3.
数组的排序和查找
排序:
如果想对数组进行排序的话,可以使用 Arrays 类提供的 sort() 方法。
1.基本数据类型按照升序排列
2.实现了 Comparable 接口的对象按照 compareTo() 的排序
举例:
String[] yetAnotherArray = new String[] {"A", "E", "Z", "B", "C"};
Arrays.sort(yetAnotherArray, 1, 3, Comparator.comparing(String::toString).reversed());
只对 1-3 位置上的元素进行反序,所以结果: [A, Z, E, B, C]
查找:
1.最直接的方式就是通过遍历的方式
int[] anArray = new int[] {5, 2, 1, 4, 8};
for (int i = 0; i < anArray.length; i++) {
if (anArray[i] == 4) {
System.out.println("找到了 " + i);
break;
}
}
2.如果数组提前进行了排序,就可以使用二分查找法,这样效率就会更高一些。
Arrays.binarySearch() 方法可供我们使用,它需要传递一个数组,和要查找的元素
int[] anArray = new int[] {1, 2, 3, 4, 5};
int index = Arrays.binarySearch(anArray, 4);
数组复制

Arrays.copyOfRange() 方法就是用来复制数组的
它底层调用的是 System.arraycopy() 方法,这个方法是一个 native 方法,它是用 C/C++ 实现的,效率非常高
System.arraycopy 方法的定义如下所示
/**
* src:源数组。
* srcPos:源数组的起始位置。
* dest:目标数组。
* destPos:目标数组的起始位置。
* length:要复制的元素数量。
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
用法如下:
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};
// 创建一个新数组,长度为两个数组长度之和
int[] mergedArray = new int[array1.length + array2.length];
// 复制第一个数组到新数组
System.arraycopy(array1, 0, mergedArray, 0, array1.length);
System.out.println(Arrays.toString(mergedArray));
// 复制第二个数组到新数组
System.arraycopy(array2, 0, mergedArray, array1.length, array2.length);
System.out.println(Arrays.toString(mergedArray));
输出结果如下所示:
[1, 2, 3, 0, 0, 0]
[1, 2, 3, 4, 5, 6]
==================================
也可以使用循环复制数组
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};
// 创建一个新数组,长度为两个数组长度之和
int[] mergedArray = new int[array1.length + array2.length];
// 复制第一个数组到新数组
int index = 0;
for (int element : array1) {
mergedArray[index++] = element;
}
// 复制第二个数组到新数组
for (int element : array2) {
mergedArray[index++] = element;
}
数组越界
ArrayIndexOutOfBoundsException 异常
因为数组的索引是从 0 开始的,所以最大索引为 length - 1, 操作数组之前,一定要注意索引的范围
面向对象
面向过程和面向对象
面向过程:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注“怎么做”,即完成任务的具体细节。
面向对象编程OOP:Object Oriented Programming,是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
面向切面编程AOP:Aspect Oriented Programming,基于OOP延伸出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果
类
对象可以是现实中看得见的任何物体, Java 通过类(class)来定义这些物体
类中可以包含:
字段(Filed)
方法(Method)
构造方法(Constructor),默认有一个空参构造器
成员变量:在类内部但在方法外部
成员变量有时候也叫做实例变量,在编译时不占用内存空间,在运行时获取内存,也就是说,只有在对象实例化(new Person())后,字段才会获取到内存,这也正是它被称作“实例”变量的原因。
临时变量(局部变量):方法内部的叫临时变量。
声明对象
创建 Java 对象时,需要用到 new 关键字
Person person = new Person();
所有对象在创建的时候都会在堆内存中分配空间。
实际开发中,我们通常不在当前类中直接创建对象并使用它,而是放在使用对象的类中
对象初始化
初始化对象,也就是对字段进行赋值
1.通过对象的引用变量
public class Person {
private String name;
private int age;
private int sex;
public static void main(String[] args) {
Person person = new Person();
person.name = "沉默王二";
person.age = 18;
person.sex = 1;
System.out.println(person.name);
System.out.println(person.age);
System.out.println(person.sex);
}
}
person 被称为对象 Person 的引用变量
2.通过方法初始化
public class Person {
private String name;
private int age;
private int sex;
public void initialize(String n, int a, int s) {
name = n;
age = a;
sex = s;
}
public static void main(String[] args) {
Person person = new Person();
person.initialize("沉默",18,1);
System.out.println(person.name);
System.out.println(person.age);
System.out.println(person.sex);
}
}
在 Person 类中新增方法 initialize(),然后在新建对象后传参进行初始化(person.initialize("沉默", 18, 1))
3.通过构造方法初始化
public class Person {
private String name;
private int age;
private int sex;
public Person(String name, int age, int sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public static void main(String[] args) {
Person person = new Person("沉默王二", 18, 1);
System.out.println(person.name);
System.out.println(person.age);
System.out.println(person.sex);
}
}
这也是最标准的一种做法,直接在 new 的时候把参数传递过去。
匿名对象
匿名对象意味着没有引用变量,它只能在创建的时候被使用一次。
new Person();
可以直接通过匿名对象调用方法:
new Person().initialize("沉默", 18, 1);
Object类和对象
Object类
Object类方法主要分为六类:
1.对象比较
hashCode()
native 方法,用于返回对象的哈希码
按照约定,相等的对象必须具有相等的哈希码。如果重写了 equals 方法,就应该重写 hashCode 方法。
可以使用 Objects.hash() 方法来生成哈希码
public int hashCode() {
return Objects.hash(name, age);
}
equals(Object obj)
用于比较 2 个对象的内存地址是否相等
public boolean equals(Object obj) {
return (this == obj);
}
如果比较的是两个对象的值是否相等,就要重写该方法
2.对象拷贝
clone()
naitive 方法,返回此对象的一个副本。默认实现只做浅拷贝,且类必须实现 Cloneable 接口。
Object 本身没有实现 Cloneable 接口,所以在不重写 clone 方法的情况下直接直接调用该方法会发生CloneNotSupportedException 异常。
3.对象转字符串
toString()
返回对象的字符串表示。默认实现返回类名@哈希码的十六进制表示,但通常会被重写以返回更有意义的信息
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
可以交给 Lombok,使用 @Data 注解,它会自动生成 toString 方法
4.多线程调度
每个对象都可以调用 Object 的 wait/notify 方法来实现等待/通知机制
wait()
public final void wait() throws InterruptedException:调用该方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法
wait(long timeout)
public final native void wait(long timeout) throws InterruptedException:等待 timeout 毫秒,如果在 timeout 毫秒内没有被唤醒,会自动唤醒
wait(long timeout, int nanos)
public final void wait(long timeout, int nanos) throws InterruptedException:更加精确了,等待 timeout 毫秒和 nanos 纳秒,如果在 timeout 毫秒和 nanos 纳秒内没有被唤醒,会自动唤醒。
notify()
public final native void notify():唤醒在此对象监视器上等待的单个线程。如果有多个线程等待,选择一个线程被唤醒
notifyAll()
public final native void notifyAll():唤醒在此对象监视器上等待的所有线程
5.反射
getClass()
public final native Class<?> getClass():用于获取对象的类信息,如类名
6.垃圾回收
finalize()
protected void finalize() throws Throwable:当垃圾回收器决定回收对象占用的内存时调用此方法。用于清理资源,但 Java 不推荐使用,因为它不可预测且容易导致问题,Java 9 开始已被弃用
异常
异常是指中断程序正常执行的一个不确定的事件。
当异常发生时,程序的正常执行流程就会被打断。
一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了
有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程
除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了
Exception和Error的区别
Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。
Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救
异常分类
checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;
unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出
Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型

NoClassDefFoundError 和 ClassNotFoundException 有什么区别
它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。
NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。
ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了
throw 和 throws 两个关键字的区别
throw 关键字,用于主动地抛出异常
throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可
使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理
1.throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。
2.throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象
3.throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。
4.throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常
try-catch-finally
try {
// 可能发生异常的代码
}catch {
// 异常处理
}finally {
// 必须执行的代码
}
1.一个 try 块后面可以跟多个 catch 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行
2.如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误
3.当有多个 catch 的时候,也可以放在一起,用竖划线 | 隔开
4.在没有 try-with-resources 之前,finally 块常用来关闭一些连接资源
4.1.finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做
4.2.finally 块不是必选项,有 try 块的时候不一定要有 finally 块
4.3.如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹
4.4.即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行
常用的类
内部类
将一个类定义在另外一个类里面或者一个方法里面,这样的类叫做内部类
成员内部类
成员内部类是最常见的内部类
class Wanger {
int age = 18;
class Wangxiaoer {
int age = 81;
}
}
成员内部类可以无限制访问外部类的所有成员属性。
外部类想要访问内部类的成员必须先创建一个成员内部类的对象,再通过这个对象来访问
如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的
不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问
外部类.this.成员变量
外部类.this.成员方法
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。
静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似
并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内
局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的
匿名内部类
匿名内部类是唯一一种没有构造方法的类
匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写
大部分匿名内部类用于接口回调
包装类
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成
包装类均位于java.lang包下
基本数据类型 --> 包装类
byte --> Byte
boolean --> Boolean
short --> Short
char --> Character
int --> Integer
long --> Long
float --> Float
double --> Double
自动装箱和拆箱
自动装箱和拆箱就是将基本数据类型和包装类之间进行自动的互相转换。JDK1.5 后, Java 引入了自动装箱(autoboxing)/拆箱(unboxing)
自动装箱: 基本类型的数据处于需要对象的环境中时,会自动转为“对象”
自动拆箱:每当需要一个值时,对象会自动转成基本数据类型,没必要再去显式调用 intValue()、 doubleValue()等转型方法
自动装箱过程是通过调用包装类的 valueOf()方法实现的
而自动拆箱过程是通过调用 包装类的 xxxValue()方法实现的(xxx 代表对应的基本数据类型,如 intValue()、 doubleValue()等)。
自动装箱与拆箱的功能事实上是编译器来帮的忙,编译器在编译时依据您所编写的语法,决定是否进行装箱或拆箱动作
Interget缓冲池
整型、char类型所对应的包装类,在自动装箱时,对于-128~127之间的值会进行缓存 处理,其目的是提高效率。
缓存处理的原理为:如果数据在-128~127这个区间,那么在类加载时就已经为该区间 的每个数值创建了对象,并将这256个对象存放到一个名为cache的数组中。每当自动装箱 过程发生时(或者手动调用valueOf()时),就会先判断数据是否在该区间,如果在则直接 获取数组中对应的包装类对象的引用,如果不在该区间,则会通过new调用包装类的构造方法来创建对象
Integer 类相关源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
1.IntegerCache类为Integer类的一个静态内部类,仅供Integer类使用。
2.一般情况下 IntegerCache.low为-128,IntegerCache.high为127, IntegerCache.cache为内部类的一个静态属性
IntegerCache 类相关源码
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
静态代码块的目的就是初始化数组cache的,这个过程会在类加载时完成
包装类在自动装箱时为了提高效率,对于-128~127 之间的值会进行缓存处理。超过范围后,对象之间不能再使用==进行数值的比较,而是使用 equals 方法
String类
String 类对象代表不可变的 Unicode 字符序列,因此我们可以将 String 对象称为“不可变对象”, 指的是对象内部的成员变量的值无法再改变
String类为什么是不可变的?
1.String类在定义时,用了final关键字修饰,这就是一个最终类,使得String类不能被继承,避免了类的核心逻辑被修改
2.在定义并创建String类型的字符串时,在计算机中是将其转换成了char数组进行存放的,而源码中用于保存数据的数组value在声明时用final进行修饰,这就导致了String的指向不可变
String源码解析
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}
1.String类是使用final修饰的, 意味着他不能被子类继承
2.String类实现了Serializable接口, 意味着他可以序列化
3.String类实现了Comparable接口, 意味着最好不要使用 "=="来比较两个字符串是否相等, 应该使用compareTo()方法进行比较
4.String类, StringBuilder, StringBuffer都实现了CharSequence接口, 由于String是不可变的, 所以遇到字符串拼接的时候, 可以考虑使用StringBuilder, StringBuffer
Java8 : private final char value[];
Java9之前, String使用char[]实现的, 之后改成了byte[]实现, 并增加了coder来表示编码, 这样做的好处是在 Latin1 字符为主的程序里,可以把 String 占用的内存减少一半
Java11 :
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
}
从 char[] 到 byte[],最主要的目的是节省字符串占用的内存空间。内存占用减少带来的另外一个好处,就是 GC 次数也会减少
Latin1
Latin1(Latin-1)是一种单字节字符集(即每个字符只使用一个字节的编码方式),也称为 ISO-8859-1(国际标准化组织 8859-1),它包含了西欧语言中使用的所有字符,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等等。在 Latin1 编码中,每个字符使用一个 8 位(即一个字节)的编码,可以表示 256 种不同的字符,其中包括 ASCII 字符集中的所有字符,即 0x00 到 0x7F,以及其他西欧语言中的特殊字符,例如 é、ü、ñ 等等。由于 Latin1 只使用一个字节表示一个字符,因此在存储和传输文本时具有较小的存储空间和较快的速度
jdk9为什么将String底层实现从char[]变成byte[]
原因就是出于节省String占用jvm的内存空间
1.通过jmap -histo:live pid | head -n 10 命令就可以查看到堆内对象示例的统计信息、ClassLoader 的信息以及 finalizer 队列, 并且可以看到一个Java系统中, 堆内存中存活最多的对象就是String类型, 所以优化String的占用空间是很有必要的
2.一般开开发使用的文本类型字符串使用一个字节就可以表示了
3.char类型的数据在jvm中占用了两个字节的空间,使用的是UTF-16编码, 所以使用char[]来表示String就导致了即使String中的字符单个字节就能表示,还是得占用了两个字节,而实际开发中使用频率最高的却是单字节的字符
4.优化为byte[], 并提供ISO-8859-1/Latin-1编码可能(Latin-1就是ISO-8859-1), Latin-1编码是用单个字节来表示字符,比两个字节的utf-16节省了一半空间, 并且在String类中多了一个编码标志位coder,用来表示使用的是utf-16编码,还是Latin-1编码, java会根据字符串的内容自动设置相应的编码,要么UTF16,要么LATIN1
字符串常量池
String s = new String("哦吼");
使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘哦吼’这个字符串对象
如果有,就不会在字符串常量池中创建‘哦吼’这个对象了,直接在堆中创建一个‘哦吼’的字符串对象,然后将堆中这个‘哦吼’的对象地址返回赋值给变量 s。
如果没有,先在字符串常量池中创建一个‘哦吼’的字符串对象,然后再在堆中创建一个‘哦吼’的字符串对象,然后将堆中这个‘哦吼’的字符串对象地址返回赋值给变量 s
在 Java 中,栈上存储的是基本数据类型的变量和对象的引用,而对象本身则存储在堆上。
一个是字符串对象 "哦吼",它被添加到了字符串常量池中
另一个是通过 new String() 构造方法创建的字符串对象 "哦吼",它被分配在堆内存中,同时引用变量 s 存储在栈上,它指向堆内存中的字符串对象 "哦吼"


字符串常量池的作用
通常情况下,我们会采用双引号的方式来创建字符串对象,而不是通过 new 关键字的方式
String s = "哦吼";
当执行 String s = "哦吼" 时,Java 虚拟机会先在字符串常量池中查找有没有“哦吼”这个字符串对象
如果有,则不创建任何对象,直接将字符串常量池中这个“哦吼”的对象地址返回,赋给变量 s;
如果没有,在字符串常量池中创建“哦吼”这个对象,然后将其地址返回,赋给变量 s。
Java 虚拟机创建了一个字符串对象 "哦吼",它被添加到了字符串常量池中,同时引用变量 s 存储在栈上,它指向字符串常量池中的字符串对象 "哦吼"
new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象


StringBuilder和StringBuffer
二者都是为了解决字符串拼接时耗费性能的问题而产生的
1.StringBuffer是线程安全的, 因为操作字符串的方法加了syncchronized关键字进行同步, 主要是为了多线程环境下的安全问题, 所以在非多线程环境下, 执行效率会很低, 因为加了没必要的锁
2.StringBuilder适合在单线程环境下使用, 效率很高. 如果要在多线程环境下修改字符串,可以使用 ThreadLocal 来避免多线程冲突
=========================================================================================================
StringBuilder的内部实现
toString()方法解析
public String toString() {
return new String(value, 0, count);
}
value 用于存储 StringBuilder 对象中包含的字符序列
count 是一个 int 类型的变量,表示字符序列的长度。
toString() 方法会调用 new String(value, 0, count),使用 value 数组中从 0 开始的前 count 个元素创建一个新的字符串对象,并将其返回
其中value是一个char[]
/**
* The value is used for character storage.
*/
char[] value;
在 StringBuilder 对象创建时,会为 value 分配一定的内存空间(初始容量 16),用于存储字符串
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() {
super(16);
}
随着字符串的拼接,value 数组的长度会不断增加,因此在 StringBuilder 对象的实现中,value 数组的长度是可以动态扩展的,就像ArrayList那样
=========================================================================================================
append()方法解析
public StringBuilder append(String str) {
super.append(str);
return this;
}
实际上调用了AdstractStringBuilder中的append(String str)方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
append(String str) 方法将指定字符串追加到当前字符序列中。如果指定字符串为 null,则追加字符串 "null";否则会检查指定字符串的长度,然后根据当前字符序列中的字符数和指定字符串的长度来判断是否需要扩容
如果需要扩容,则会调用 ensureCapacityInternal(int minimumCapacity) 方法。扩容之后,将指定字符串的字符拷贝到字符序列中
private void ensureCapacityInternal(int minimumCapacity) {
// 不够用了,扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
// 扩容策略:新容量为旧容量的两倍加上 2
int newCapacity = value.length * 2 + 2;
// 如果新容量小于指定的最小容量,则新容量为指定的最小容量
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
// 如果新容量小于 0,则新容量为 Integer.MAX_VALUE
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
// 将字符序列的容量扩容到新容量的大小
value = Arrays.copyOf(value, newCapacity);
}
ensureCapacityInternal(int minimumCapacity) 方法用于确保当前字符序列的容量至少等于指定的最小容量minimumCapacity。如果当前容量小于指定的容量,就会为字符序列分配一个新的内部数组。新容量的计算方式如下:
如果指定的最小容量大于当前容量,则新容量为两倍的旧容量加上 2。
为什么要加 2 呢?
对于非常小的字符串(比如空的或只有一个字符的 StringBuilder),仅仅将容量加倍可能仍然不足以容纳更多的字符。
在这种情况下,+ 2 提供了一个最小的增长量,确保即使对于很小的初始容量,扩容后也能至少添加一些字符而不需要立即再次扩容。
如果指定的最小容量小于等于当前容量,则不会进行扩容,直接返回当前对象。
在进行扩容之前,ensureCapacityInternal(int minimumCapacity) 方法会先检查当前字符序列的容量是否足够,
如果不足就会调用 expandCapacity(int minimumCapacity) 方法进行扩容。
expandCapacity(int minimumCapacity) 方法首先计算出新容量,然后使用 Arrays.copyOf(char[] original, int newLength) 方法将原字符数组扩容到新容量的大小
=======================================================================================================
reverse() : 用于反转当前字符序列中的字符
public StringBuilder reverse() {
super.reverse();
return this;
}
也是调用父类的方法
public AbstractStringBuilder reverse() {
int n = count - 1; // 字符序列的最后一个字符的索引
// 遍历字符串的前半部分
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j; // 计算相对于 j 对称的字符的索引
char cj = value[j]; // 获取当前位置的字符
char ck = value[k]; // 获取对称位置的字符
value[j] = ck; // 交换字符
value[k] = cj; // 交换字符
}
return this; // 返回反转后的字符串构建器对象
}
初始化:
n 是字符串中最后一个字符的索引。
字符串反转:
方法通过一个 for 循环遍历字符串的前半部分和后半部分,这是一个非常巧妙的点,比从头到尾遍历省了一半的时间。(n-1) >> 1 是 (n-1) / 2 的位运算表示,也就是字符串的前半部分的最后一个字符的索引。
在每次迭代中,计算出与当前索引 j 对称的索引 k,并交换这两个索引位置的字符
Date类
java.util包提供了Date类来封装当前的日期和时间
Date类提供了两个构造函数来实例化Date对象
1.Date() 使用当前时间和日期来初始化对象
2.Date(long millisec) 接收一个参数, 该参数是从1970年1月1日起的毫秒数
System类的currentTimeMillis():获取当前系统时间对应的毫秒数
常用的方法
1.boolean after(Date date)
若当调用此方法的Date对象在指定日期之后返回true,否则返回false。
2.boolean before(Date date)
若当调用此方法的Date对象在指定日期之前返回true,否则返回false。
3.Object clone( )
返回此对象的副本。
4.int compareTo(Date date)
比较当调用此方法的Date对象和指定日期。两者相等时候返回0。调用对象在指定日期之前则返回负数。调用对象在指定日期之后则返回正数。
5.int compareTo(Object obj)
若obj是Date类型则操作等同于compareTo(Date) 。否则它抛出ClassCastException。
6.boolean equals(Object date)
当调用此方法的Date对象和指定日期相等时候返回true,否则返回false。
7.long getTime( )
返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
8.int hashCode( )
返回此对象的哈希码值。
9.void setTime(long time)
用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期。
10.String toString( )
把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)。
日期比较
Java使用以下三种方法来比较两个日期:
1.使用 getTime() 方法获取两个日期(自1970年1月1日经历的毫秒数值),然后比较这两个值。
2.使用方法 before(),after() 和 equals()。例如,一个月的12号比18号早,则 new Date(99, 2, 12).before(new Date (99, 2, 18)) 返回true。
3.使用 compareTo() 方法,它是由 Comparable 接口定义的,Date 类实现了这个接口。
测试时间间隔
import java.util.*;
public class DiffDemo {
public static void main(String[] args) {
try {
long start = System.currentTimeMillis( );
System.out.println(new Date( ) + "\n");
Thread.sleep(5*60*10);
System.out.println(new Date( ) + "\n");
long end = System.currentTimeMillis( );
long diff = end - start;
System.out.println("Difference is : " + diff);
} catch (Exception e) {
System.out.println("Got an exception!");
}
}
}
Calendar类:日历类的使用:抽象类
创建一个代表系统当前日期的Calendar对象,底层调用的还是实例化 Calendar的子类 GregorianCalendar
Calendar c = Calendar.getInstance();//默认是当前日期
创建一个指定日期的Calendar对象
//创建一个代表2009年6月12日的Calendar对象
Calendar c1 = Calendar.getInstance();
c1.set(2009, 6 - 1, 12);
SimpleDateFormat类:用于格式化、解析
可以按照指定的格式将date对象格式为字符串
使用SimpleDateFormat可以对Date日期格式化
//创建SimpleDateFormat对象, 通过构造方法指定模式串,常用的格式符: y年, M月, d日, H小时, m分钟, s秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//调用SimpleDateFormat对象的format( Date)可以把日期转换为字符串
String s = sdf.format(date);
System.out.println( s );//2021-08-14 16:07:07
也可以按照指定的格式将字符串解析为Date
把表示日期的字符串转换为Date日期对象
String text = "2086年5月16日 8:28:58";
//根据字符串格式创建SimpleDateFormat对象
SimpleDateFormat another = new SimpleDateFormat("yyyy年M月dd日 H:mm:ss");
//调用SimpleDateFormat对象的parse(String)可以把字符串解析为Date对象, 该方法有检查异常需要预处理,当前选择抛出处理, 程序运行后,如果这一行产生了异常,说明another对象的格式符与text字符串不匹配
Date date3 = another.parse(text);
System.out.println( date3 );
System类
System 类位于 java.lang 包下,是一个 final 类,意味着它不能被继承。并且其所有构造方法都是私有的,这使得我们无法创建 System 类的实例,只能通过类名来调用其静态方法和访问静态字段
静态字段
标准输入输出流相关
System.in
类型:InputStream
作用:代表标准输入流,默认情况下与键盘输入关联。在控制台程序中,可用于接收用户输入的数据。
示例:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class SystemInExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
System.out.print("请输入你的姓名: ");
String name = reader.readLine();
System.out.println("你输入的姓名是: " + name);
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out
类型:PrintStream
作用:代表标准输出流,默认情况下与控制台输出关联。用于向控制台打印各种信息。
示例:
public class SystemOutExample {
public static void main(String[] args) {
System.out.println("这是使用 System.out 输出的信息。");
}
}
System.err
类型:PrintStream
作用:代表标准错误输出流,同样默认关联到控制台,但主要用于输出错误信息。与 System.out 不同,它通常会以不同的颜色或格式显示,方便用户区分正常输出和错误信息。
示例:
public class SystemErrExample {
public static void main(String[] args) {
try {
int result = 1 / 0;
} catch (ArithmeticException e) {
System.err.println("发生错误: " + e.getMessage());
}
}
}
常用静态方法
数组操作
arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
作用:将源数组中从指定位置开始的一定数量的元素复制到目标数组的指定位置。这是一个本地方法,底层使用高效的内存复制操作,性能较高。
参数说明:
src:源数组。
srcPos:源数组中开始复制的起始位置。
dest:目标数组。
destPos:目标数组中开始粘贴的起始位置。
length:要复制的元素数量。
时间操作
currentTimeMillis()
作用:返回当前系统时间与 1970 年 1 月 1 日 00:00:00 UTC 之间的毫秒数,也称为时间戳。常用于性能测试、计时等场景。
nanoTime()
作用:返回当前系统的高精度时间,单位为纳秒。该方法返回的时间值是相对于某个固定但未指定的起点的,主要用于测量短时间间隔,精度比 currentTimeMillis() 更高。
系统操作
exit(int status)
作用:终止当前正在运行的 Java 虚拟机。参数 status 为 0 表示正常终止,非零值表示异常终止
gc()
作用:请求 Java 虚拟机运行垃圾回收器,尝试回收未使用的对象以释放内存。但这只是一个请求,Java 虚拟机不一定会立即执行垃圾回收操作。
runFinalization()
作用:请求 Java 虚拟机运行所有对象的 finalize() 方法。当一个对象被垃圾回收之前,Java 虚拟机会调用其 finalize() 方法进行一些资源清理操作。
属性操作
getProperty(String key)
作用:获取指定键的系统属性值。系统属性包含了很多关于 Java 虚拟机和操作系统的信息,如 Java 版本、操作系统名称等
getProperty(String key, String def)
作用:获取指定键的系统属性值,如果该属性不存在,则返回默认值 def
getProperties()
作用:返回一个 Properties 对象,包含了所有的系统属性。可以通过遍历该对象来查看所有系统属性
setProperty(String key, String value)
作用:设置指定键的系统属性值
安全管理
getSecurityManager()
作用:返回当前 Java 虚拟机的安全管理器,如果没有安装安全管理器,则返回 null。安全管理器用于控制 Java 程序对系统资源的访问权限
setSecurityManager(SecurityManager s)
作用:设置 Java 虚拟机的安全管理器。如果参数 s 为 null,则移除当前的安全管理器
其他方法
identityHashCode(Object x)
作用:返回指定对象的哈希码,该哈希码是基于对象的内存地址计算的,与对象的 hashCode() 方法可能不同。即使对象重写了 hashCode() 方法,identityHashCode() 仍然返回基于内存地址的哈希码
集合框架
架构图

框架介绍
集合框架分为两大分支
1.Collection, 主要是由List, Set, Queue组成
1.1.List代表有序, 可重复的集合, 典型代表就是封装了动态数组的ArrayList和封装了链表的LinkedList
1.2.Set代表无序, 不可重复的集合, 典型代表就是HashSet和TreeSet
1.3.Queue代表队列, 典型代表就是双端队列ArrayDeque, 以及优先级队列PriorityQueue
2.Map, 代表键值对的集合, 典型代表就是HashMap
List
特点就是存取有序, 可存放重复的数据, 可以使用下标对元素进行操作
ArrayList
ArrayList实现了List接口, 并且是基于数组实现的
数组的大小是固定的, 一旦创建的时候指定了大小, 就不能再调整了. 也就是说, 如果数组满了, 就不能再添加任何元素了
ArrayList在数组的基础上实现了自动扩容, 并且提供了比数组更丰富的预定义方法(各种CRUD)
创建ArrayList
=========================================创建空集合=======================================================
ArrayList<String> alist = new ArrayList<String>();
可以通过上面的语句来创建一个字符串类型的ArrayList, 通过尖括号来限定ArrayList中元素的类型, 如果尝试添加其他类型的元素, 将会产生编译错误
更加简化的写法:
List<String> alist = new ArrayList<>();
由于ArrayList实现了List接口, 所以alist变量的类型可以是List类型
new关键字声明后的尖括号中可以不再指定元素的类型, 因为编译器可以通过前面尖括号中的类型进行智能判断
此时会调用无参构造方法创建一个空的数组, 常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值为{}
/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为10的空列表
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
=========================================创建指定容量大小的集合=============================================
如果确定ArrayList中元素的个数, 在创建的时候,还可以指定初始大小
List<String> alist = new ArrayList<>(20);
这样做的好处是, 可以有效避免在添加新的元素时进行不必要的扩容
此时调用的是下面的构造方法
/**
* Constructs an empty list with the specified initial capacity.
* 构造具有指定初始容量的空列表
*
* @param initialCapacity the initial capacity of the list, 列表的初始容量
* @throws IllegalArgumentException if the specified initial capacity
* is negative, 如果指定的初始容量为负, 则会抛出异常
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
=========================================创建包含其他集合的集合=============================================
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
* 按照指定集合的迭代器返回的顺序,构造一个包含指定集合元素的列表
*
* @param c the collection whose elements are to be placed into this list, 其元素将放置到此列表中的集合
* @throws NullPointerException if the specified collection is null, 如果指定的集合为空
*/
public ArrayList(Collection<? extends E> c) {
// 1. 将传入的 Collection 转换成 Object 数组
Object[] a = c.toArray();
// 2. 如果数组不为空
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
// 3. 如果传入的就是 ArrayList,直接复用数组(节省拷贝)
elementData = a;
} else {
// 4. 否则进行一次复制,保证类型正确
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
// 如果传入的集合为空(size = 0),就直接使用一个静态的空数组 EMPTY_ELEMENTDATA,节省内存空间
elementData = EMPTY_ELEMENTDATA;
}
}
向ArrayList中添加数据: add()
/**
* Appends the specified element to the end of this list.
* 将指定的元素追加到此列表的末尾
*
* @param e element to be appended to this list, 元素添加到此列表
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
1.add方法一共两个, 以add(E e)举例, 这是一个尾插操作
如果容量足够, 就插入, 如果容量不足, 就先扩容之后在插入
始终返回true, 是因为在内存允许的范围内, ArrayList总是默认支持添加元素
2.首先是确认内部容量ensureCapacityInternal(size + 1);
size是ArrayList的成员变量, private int size; 默认值是0
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以确保它至少可以容纳最小容量参数指定的元素数量
* @param minCapacity the desired minimum capacity, 所需的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
3.ensureCapacityInternal()
接受一个参数, minCapacity, 代表所需要的最小容量
calculateCapacity(), 计算一个真正的要用的容量
再调用 ensureExplicitCapacity() 去判断是否需要扩容
4.calculateCapacity()
如果当前数组是"空默认数组", 也就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 即new ArrayList<>()的时候, 未指定初始长度, 数组长度为0
那就给一个默认容量, 一般是10
否则就返回minCapacity
4.1.
/**
* Default initial capacity.
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
* 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
* 用于默认大小的空实例的共享空数组实例。我们将其与EMPTY_ELEMENTDATA区分开来,以便知道添加第一个元素时要膨胀多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
5.ensureExplicitCapacity()
modCount++; 记录一次修改次数 (详情请看: modCount变量解析)
如果 minCapacity 大于当前数组的实际长度,则触发扩容逻辑:grow(minCapacity)
6.grow(minCapacity) - 增加容量以确保它至少可以容纳最小容量参数指定的元素数量
private void grow(int minCapacity) {
// overflow-conscious code
// 获取当前数组的长度
int oldCapacity = elementData.length;
// 新容量为旧容量的 1.5 倍,>> 1 等于除以 2
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 如果按 1.5 倍扩容后还不够用(比如用户突然 add 了 100 个元素),就把新容量设为用户要求的 minCapacity
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果新容量超过了数组的最大限制(一般是 Integer.MAX_VALUE - 8), 就调用 hugeCapacity(minCapacity) 来处理极端情况(避免溢出)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 最终通过 Arrays.copyOf 创建一个新数组,把老数组数据复制过去, 替换掉原来的 elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
============================================堆栈过程图示============================================
add(element)
└── if (size == elementData.length) // 判断是否需要扩容
├── grow(minCapacity) // 扩容
│ └── newCapacity = oldCapacity + (oldCapacity >> 1) // 计算新的数组容量
│ └── Arrays.copyOf(elementData, newCapacity) // 创建新的数组
├── elementData[size++] = element; // 添加新元素
└── return true; // 添加成功
modCount变量解析
// modCount 这个变量就是ArrayList中集合结构的修改次数【实际修改次数】,指的是新增、删除(不包括修改)操作
这个列表在结构上被修改的次数。结构修改是指改变列表的大小,或者以某种方式扰乱列表,从而使进行中的迭代可能产生不正确的结果。
官方解释:
该字段由iterator和listtiterator方法返回的迭代器和列表迭代器实现使用。如果该字段的值发生意外变化,迭代器(或列表迭代器)将抛出ConcurrentModificationException,以响应next、remove、previous、set或add操作。这提供了快速故障行为,而不是在迭代期间面对并发修改时的不确定性行为。
子类使用此字段是可选的。如果子类希望提供快速失败迭代器(和列表迭代器),那么它只需要在其add(int, E)和remove(int)方法(以及它覆盖的导致列表结构修改的任何其他方法)中增加该字段。单个调用add(int, E)或remove(int)必须向该字段添加不超过一个,否则迭代器(和列表迭代器)将抛出虚假的concurrentmodificationexception。如果实现不希望提供快速失败迭代器,则可以忽略此字段
如果在遍历集合的过程中抛出并发修改异常ConcurrentModificationException, 那么大概率是和此变量有关
异常抛出在ArrayList类中的checkForComodification()方法中
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这个方法实际上就是当modCount不等于expectedModCount的时候, 就会抛出并发修改异常ConcurrentModificationException
modCount我们第一行说过
expectedModCount: 是ArrayList中内部类Itr的一个成员变量,当我们调用iteroter()获取迭代器方法时,会创建内部类Itr的对 象,并给其成员变量expectedModCount赋值为ArrayList对象成员变量的值modCount【预期修改次数】。
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
...
}
当我们获取到集合的迭代器之后,Itr对象创建成功后,expectedModCount 的值就确定了,就是modCount的值,在迭代期间不允许改变了
modCount 初始值为0, 每当集合中添加一个元素或者删除一个元素时,modCount变量的值都会加一,表示集合中结构修改次数多了一次,ArrayList中的修改方法set()并不会导致modCount变量发生变化
Itr源码解析
private class Itr implements Iterator<E> {
// cursor的初始值为0, 每次取出一个元素, cursor值会+1,以便下一次能指向下一个元素,直到cursor值等于集合的长度为止
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
// 初始化预期修改次数为实际修改次数modcount
int expectedModCount = modCount;
Itr() {}
// 判断是否还有下一个元素, 通过比较游标cursor是否等于数组的长度
// 因为集合中最后一个元素的索引是size-1, 只有cursor值不等于size, 证明还有下一个元素,此时hasNext()返回true
//如果cursor值等于size, 那么证明已经迭代到最后一个元素了, 返回false
public boolean hasNext() {
return cursor != size;
}
// 拿出集合中的下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 并发修改异常出现的根源
// ConcurrentModificationException异常就是从这抛出的
// 当迭代器通过next()返回元素之前都会检查集合中的modcount和最初赋值给迭代器的expectedModcount是否相等
checkForComodification();
int i = cursor;
// 判断, 如果大于集合的长度, 说明没有元素了
if (i >= size)
throw new NoSuchElementException();
// 将集合存储数据数组的地址赋值给局部变量elementData
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 每次获取完下一个元素后, 游标向后移动一位
cursor = i + 1;
// 返回当前游标对应的元素
return (E) elementData[lastRet = i];
}
// 迭代器自带的删除方法
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 校验是否产生并发修改异常
checkForComodification();
try {
// 真正删除元素的方法还是调用AyyayList的删除方法
// 根据索引进行删除
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 每次删除完, 会重新将expectedModecount重新赋值, 值就是实际修改次数modcount的值
// 这就保证了, 实际修改次数modcount一定会等于预期修改次数expectedModCount, 所以不会产生并发修改异常
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
集合每次调用add方法时,实际修改次数的值modCount都会自增1
在获取迭代器的时候,集合只会执行一次将实际修改集合的次数modCount的值赋值给预期修改的次数变量expectedModCount
集合在删除元素的时候,也会针对实际修改次数modCount的变量进行自增操作
当要删除的元素在集合中的倒数第二个元素的时候,删除元素不会产生并发修改异常
原因:因为在调用hasNext()方法的时候,cursor = size是相等的,hasNext()方法会返回false, 所以不会执行next()方法,也就不会调用checkForComodification()方法,就不会发生并发修改异常
向ArrayList的指定位置添加元素: add(int index, E element)
/**
* 将指定元素插入此列表中的指定位置。将当前在该位置的元素(如果有的话)和任何后续元素向右移动(在它们的索引上加1)
*
* @param index 要插入元素的位置
* @param element 要插入的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public void add(int index, E element) {
rangeCheckForAdd(index); // 检查索引是否越界
ensureCapacityInternal(size + 1); // 确保容量足够,如果需要扩容就扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 将 index 及其后面的元素向后移动一位
elementData[index] = element; // 将元素插入到指定位置
size++; // 元素个数加一
}
add(int index, E element)方法会调用到一个非常重要的本地方法 System.arraycopy(),它会对数组进行复制(要插入位置上的元素往后复制)
/**
* @param src 表示要复制的源数组
* @param srcPos 表示源数组中要复制的起始位置
* @param dest 表示要复制到的目标数组
* @param destPos 表示目标数组中复制的起始位置
* @param length 表示要复制的元素个
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
更新ArrayList中的元素: set()
/**
* 用指定元素替换指定位置的元素。
*
* @param index 要替换的元素的索引
* @param element 要存储在指定位置的元素
* @return 先前在指定位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public E set(int index, E element) {
rangeCheck(index); // 检查索引是否越界
E oldValue = elementData(index); // 获取原来在指定位置上的元素
elementData[index] = element; // 将新元素替换到指定位置上
return oldValue; // 返回原来在指定位置上的元素
}
该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值
删除ArrayList中的元素
remove(int index) 方法用于删除指定下标位置上的元素,remove(Object o) 方法用于删除指定值的元素。
/**
* 删除指定位置的元素。
*
* @param index 要删除的元素的索引
* @return 先前在指定位置的元素
* @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
*/
public E remove(int index) {
rangeCheck(index); // 检查索引是否越界
modCount++; // 记录实际操作次数
E oldValue = elementData(index); // 获取要删除的元素
int numMoved = size - index - 1; // 计算需要移动的元素个数
if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间
elementData[--size] = null; // clear to let GC do its work
// 返回被删除的元素
return oldValue;
}
/**
* 删除列表中第一次出现的指定元素(如果存在)。
*
* @param o 要删除的元素
* @return 如果列表包含指定元素,则返回 true;否则返回 false
*/
public boolean remove(Object o) {
if (o == null) {
// 遍历列表
for (int index = 0; index < size; index++)
// 如果找到了 null 元素
if (elementData[index] == null) {
// 调用 fastRemove 方法快速删除元素
fastRemove(index);
// 返回 true,表示成功删除元素
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
元素为null 的时候使用 == 操作符判断,非 null 的时候使用 equals() 方法,然后调用 fastRemove() 方法
有相同元素时,只会删除第一个
/**
* 快速删除指定位置的元素。
*
* @param index 要删除的元素的索引
*/
private void fastRemove(int index) {
modCount++; // 记录实际操作次数
int numMoved = size - index - 1; // 计算需要移动的元素个数
if (numMoved > 0) // 如果需要移动元素,就用 System.arraycopy 方法实现
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组末尾的元素置为 null,让 GC 回收该元素占用的空间
elementData[--size] = null; // clear to let GC do its work
}
查找ArrayList中的元素
如果要正序查找一个元素,可以使用 indexOf() 方法;如果要倒序查找一个元素,可以使用 lastIndexOf() 方法
/**
* 返回指定元素在列表中第一次出现的位置。
* 如果列表不包含该元素,则返回 -1。
*
* @param o 要查找的元素
* @return 指定元素在列表中第一次出现的位置;如果列表不包含该元素,则返回 -1
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
如果元素为 null 的时候使用“==”操作符,否则使用 equals() 方法
/**
* lastIndexOf() 方法和 indexOf() 方法类似,不过遍历的时候从最后开始。
*
* 返回指定元素在列表中最后一次出现的位置。
* 如果列表不包含该元素,则返回 -1。
*
* @param o 要查找的元素
* @return 指定元素在列表中最后一次出现的位置;如果列表不包含该元素,则返回 -1
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
contains() 方法可以判断 ArrayList 中是否包含某个元素,其内部就是通过 indexOf() 方法实现的
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
二分查找法
如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快
Collections 类的 sort() 方法可以对 ArrayList 进行排序,该方法会按照字母顺序对 String 类型的列表进行排序。如果是自定义类型的列表,还可以指定 Comparator 进行排序
int index = Collections.binarySearch(list, "b");
总结
ArrayList,如果有个中文名的话,应该叫动态数组,也就是可增长的数组,可调整大小的数组。动态数组克服了静态数组的限制,静态数组的容量是固定的,只能在首次创建的时候指定。而动态数组会随着元素的增加自动调整大小,更符合实际的开发需求
LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList实现了List接口, 所以是有序, 可重复的集合
同时也实现了Deque(双端队列)接口,支持栈、队列和双端队列操作
链表
链表大致分为三个种:
第一种叫做“单向链表”,只有一个后指针,指向下一个数据;
第二种叫做“双向链表”,有两个指针,后指针指向下一个数据,前指针指向上一个数据。
第三种叫做“二叉树”,把后指针去掉,换成左右指针。
/**
* LinkedList中的内部Node类
*/
private static class Node<E> {
// 节点中存储的元素
E item;
// 指向下一个节点的指针
Node<E> next;
// 指向上一个节点的指针
Node<E> prev;
/**
* 构造一个新的节点。
*
* @param prev 前一个节点
* @param element 节点中要存储的元素
* @param next 后一个节点
*/
Node(Node<E> prev, E element, Node<E> next) {
// 存储元素
this.item = element;
// 设置下一个节点
this.next = next;
// 设置上一个节点
this.prev = prev;
}
}
对于第一个节点来说,prev 为 null;
对于最后一个节点来说,next 为 null;
其余的节点呢,prev 指向前一个,next 指向后一个
初始化LinkedList
LinkedList<String> list = new LinkedList();
public LinkedList() {}
LinkedList和ArrayList不同的是:
ArrayList初始化的时候可以指定大小,也可以不指定,等到添加第一个元素的时候进行第一次扩容。
而LinkedList,没有大小,只要内存够大,LinkedList就可以无穷大
向LinkedList中添加元素
add(E e)
/**
* Appends the specified element to the end of this list.
* 将指定的元素追加到此列表的末尾。
*
* <p>This method is equivalent to {@link #addLast}.
* 这个方法等价于addLast。
*
* @param e element to be appended to this list, 要添加的元素
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 在列表的尾部添加元素
linkLast(e);
// 添加元素成功,返回 true
return true;
}
/**
* Links e as last element.
* 在列表的尾部添加指定的元素。
*/
void linkLast(E e) {
// 获取链表的最后一个元素
final Node<E> l = last;
// 使用要添加的元素创建一个新的节点,并将其设置为链表的最后一个节点
final Node<E> newNode = new Node<>(l, e, null);
// 将新的节点设置为链表的最后一个节点
last = newNode;
if (l == null)
// 如果链表为空,则将新节点设置为头节点
first = newNode;
else
// 否则将新节点链接到链表的尾部
l.next = newNode;
// 增加链表的元素个数
size++;
// 增加实际操作次数
modCount++;
}
添加第一个元素的时候, first, last都为null
然后新建一个节点newNode, 他的prev和next都是null
然后把last和first都赋值为newNode
此时还不能称之为链表, 因为前后节点都是断裂的
添加第二个元素的时候, first和last都指向的是第一个节点
然后新建一个节点newNode, 他的prev指向的是第一个节点, next为null
然后把第一个节点的next赋值为newNode
此时链表还是不完整
添加第三个元素的时候, first指向的是第一个节点, last指向的是最后一个节点
然后新建一个节点newNode, 他的prev指向的是第二个节点, next为null
然后把第二个节点的next赋值为newNode
此时链表已经完整了
addFirst(E e)
/**
* Inserts the specified element at the beginning of this list.
* 在此列表的开头插入指定的元素
*
* @param e the element to add
*/
public void addFirst(E e) {
linkFirst(e);
}
/**
* Links e as first element.
*/
private void linkFirst(E e) {
// 获取第一个元素
final Node<E> f = first;
// 创建一个新的节点,并将其设置为链表的第一个节点
final Node<E> newNode = new Node<>(null, e, f);
// 将新的节点设置为链表的第一个节点
first = newNode;
if (f == null)
// 如果链表为空,则将新节点设置为尾节点
last = newNode;
else
// 否则将新节点链接到链表的头部
f.prev = newNode;
// 增加链表的元素个数
size++;
// 增加修改次数
modCount++;
}
addLast(E e)
public void addLast(E e) {
linkLast(e);
}
更新LinkedList中的元素
/**
* Replaces the element at the specified position in this list with the
* specified element.
* 将此列表中指定位置的元素替换为指定元素, 并返回原来的元素
*
* @param index index of the element to replace, 要替换元素的位置(从 0 开始)
* @param element element to be stored at the specified position, 要插入的元素
* @return the element previously at the specified position, 替换前的元素
* @throws IndexOutOfBoundsException {@inheritDoc}, 如果索引超出范围(index < 0 || index >= size())
*/
public E set(int index, E element) {
// 检查索引是否超出范围
checkElementIndex(index);
// 获取要替换的节点
Node<E> x = node(index);
// 获取要替换节点的元素
E oldVal = x.item;
// 将要替换的节点的元素设置为指定元素
x.item = element;
// 返回替换前的元素
return oldVal;
}
/**
* Returns the (non-null) Node at the specified element index.
* 返回指定元素索引处的(非空)节点
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// 如果索引在链表的前半部分
if (index < (size >> 1)) {
Node<E> x = first;
// 从头节点开始向后遍历链表,直到找到指定位置的节点
for (int i = 0; i < index; i++)
x = x.next;
// 返回指定位置的节点
return x;
} else {
// 如果索引在链表的后半部分
Node<E> x = last;
// 从尾节点开始向前遍历链表,直到找到指定位置的节点
for (int i = size - 1; i > index; i--)
x = x.prev;
// 返回指定位置的节点
return x;
}
}
size >> 1:也就是右移一位,相当于除以 2
换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历,这样可以提高效率,最大能提高一半的效率
找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动
删除LinkedList中的元素
remove(Object o)
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
如果传入的是 null,那么就遍历链表,找到第一个 item == null 的节点,调用 unlink(x) 删除它。
如果传入的不是 null,就使用 equals() 方法逐个比较 item 与传入对象是否相等,找到了也调用 unlink(x) 删除。
如果遍历完整个链表都没找到匹配的项,返回 false。
unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。
E unlink(Node<E> x) {
// assert x != null;
// 获取要删除节点的元素
final E element = x.item;
// 获取要删除节点的下一个节点
final Node<E> next = x.next;
// 获取要删除节点的上一个节点
final Node<E> prev = x.prev;
// 如果要删除节点是第一个节点
if (prev == null) {
// 将链表的头节点设置为要删除节点的下一个节点
first = next;
} else {
// 将要删除节点的上一个节点指向要删除节点的下一个节点
prev.next = next;
// 将要删除节点的上一个节点设置为空
x.prev = null;
}
// 如果要删除节点是最后一个节点
if (next == null) {
// 将链表的尾节点设置为要删除节点的上一个节点
last = prev;
} else {
// 将要删除节点的下一个节点指向要删除节点的上一个节点
next.prev = prev;
// 将要删除节点的下一个节点设置为空
x.next = null;
}
// 将要删除节点的元素设置为空
x.item = null;
// 减少链表的元素个数
size--;
// 记录实际操作次数
modCount++;
// 返回被删除节点的元素
return element;
}
remove(int)
内部其实调用的是 unlink 方法。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
remove()和removeFirst()
public E remove() {
return removeFirst();
}
/**
* 从链表中删除第一个元素并返回它。
* 如果链表为空,则抛出 NoSuchElementException 异常。
*
* @return 从链表中删除的第一个元素
* @throws NoSuchElementException 如果链表为空
*/
public E removeFirst() {
// 获取链表的第一个节点
final Node<E> f = first;
// 如果链表为空
if (f == null)
throw new NoSuchElementException();
// 调用 unlinkFirst 方法删除第一个节点并返回它的元素
return unlinkFirst(f);
}
/**
* 删除链表中的第一个节点并返回它的元素。
*
* @param f 要删除的第一个节点
* @return 被删除节点的元素
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
// 获取要删除的节点的元素
final E element = f.item;
// 获取要删除的节点的下一个节点
final Node<E> next = f.next;
// 将要删除的节点的元素设置为 null
f.item = null;
// 将要删除的节点的下一个节点设置为 null
f.next = null; // help GC
// 将链表的头节点设置为要删除的节点的下一个节点
first = next;
// 如果链表只有一个节点
if (next == null)
// 将链表的尾节点设置为 null
last = null;
else
// 将要删除节点的下一个节点的前驱设置为 null
next.prev = null;
// 减少链表的大小
size--;
// 增加实际操作次数
modCount++;
// 返回被删除节点的元素
return element;
}
removeLast()
/**
* Removes and returns the last element from this list.
* 删除并返回最后一个元素
*
* @return the last element from this list
* @throws NoSuchElementException if this list is empty
*/
public E removeLast() {
// 获取最后一个元素
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
// 调用方法
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
// 获取要删除的节点元素
final E element = l.item;
// 获取要删除节点的上一个节点
final Node<E> prev = l.prev;
// 将要删除的节点的元素设为null
l.item = null;
// 将要删除的节点的上一个节点设为null
l.prev = null; // help GC
// 将链表的尾节点设置为要删除节点的上一个节点
last = prev;
// 如果链表只有一个节点
if (prev == null)
// 将链表的头节点设为null
first = null;
else
// 将要删除节点的上一个节点的后驱设置为null
prev.next = null;
// 减少链表的数量
size--;
// 增加实际操作次数
modCount++;
// 返回被删除节点的元素
return element;
}
查找LinkedList中的元素
indexOf(Object):查找某个元素所在的位置
// 返回链表中首次出现指定元素的位置,如果不存在该元素则返回 -1
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
get(int):查找某个位置上的元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
ArrayList和LinkedList区别
1. ArrayList是实现了基于动态数组的数据结构,而LinkedList是基于链表的数据结构;
2. 对于随机访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针;
3. 对于添加和删除操作add和remove,一般大家都会说LinkedList要比ArrayList快,因为ArrayList要移动数据。但是实际情况并非这样,对于添加或删除,LinkedList和ArrayList并不能明确说明谁快谁慢
当数据量较小时,测试程序中,大约小于30的时候,两者效率差不多,没有显著区别;
当数据量较大时,大约在容量的1/10处开始,LinkedList的效率就开始没有ArrayList效率高了,特别到一半以及后半的位置插入时,LinkedList效率明显要低于ArrayList,而且数据量越大,越明显
Vector和Stack
1. 什么是Vector和Stack?
Vector是Java中的一个动态数组,它实现了List接口,并且可以自动扩容。Vector允许在任意位置插入、删除和访问元素。
Stack是Vector的子类,它实现了栈的数据结构。栈是一种后进先出(LIFO)的数据结构,只能在栈顶进行插入和删除操作。
2. 为什么需要Vector和Stack?
Vector:由于Vector是动态数组,它可以根据需要自动调整大小,因此非常适合存储和操作可变数量的元素。
Stack:栈是一种常见的数据结构,在很多场景下都有用处。例如,当我们需要按照特定的顺序处理元素时,可以使用栈来保存中间结果。
3. Vector和Stack的实现原理?
Vector内部使用一个Object类型的数组来存储元素,当数组空间不足时,会创建一个更大的数组并将所有元素复制到新数组中。这个过程称为扩容。
默认情况下,每次扩容会使数组的大小增加一倍。
Stack继承自Vector,所以它也使用数组来存储元素。
与Vector不同的是,Stack限制了只能在栈顶进行插入和删除操作。通过继承Vector,Stack获得了Vector的所有方法,但它只暴露了栈相关的操作。
4. Vector和Stack的优点
Vector:具有动态扩容功能,可以自动调整大小以适应可变数量的元素。支持在任意位置插入、删除和访问元素。
Stack:继承自Vector,提供了栈的特性,方便实现后进先出的数据结构。
5. Vector和Stack的缺点
Vector:由于Vector内部使用数组来存储元素,在插入或删除元素时,可能需要移动其他元素的位置,导致性能下降。
此外,Vector是线程安全的,但在多线程环境下使用时会带来额外的开销。
Stack:由于继承自Vector,Stack也具有与Vector相同的缺点。
另外,栈的大小是固定的,当栈满时无法再插入新的元素。
6. Vector和Stack的使用注意事项
在Java中,推荐使用ArrayList代替Vector,因为ArrayList不是线程安全的,并且性能更好。
在实现后进先出的数据结构时,可以考虑使用Deque接口的实现类LinkedList,它既支持栈操作,又支持队列操作。
7. 总结
Vector和Stack都是Java集合框架中的一部分,用于存储和操作可变数量的元素。
Vector是一个动态数组,而Stack是Vector的子类,实现了栈的数据结构。
Vector和Stack在某些场景下非常有用,但在大多数情况下,推荐使用ArrayList或LinkedList来代替它们。
栈
定义
栈(Stack)是一种常见的数据结构, 具有后进先出(LIFO, Last In First Out)的特性, 即最后入栈的元素最先出栈
栈通常用来存储临时性的数据, 如方法调用过程中的局部变量, 操作数栈等
java中, 栈广泛用于方法调用和内存管理的过程中
Java中的栈帧
在JVM(Java虚拟机)中, 每个方法在运行时都会创建一个对应的栈帧(Stack Frame), 栈帧用于存储方法的局部变量, 操作数栈, 动态链接, 返回地址等信息
栈帧的结构如下:
1.局部变量表 Local Variable Table
局部变量表用于存储方法参数和方法内部定义的局部变量.
局部变量表中的每个槽位都可以存储一个基本类型的值或对象引用
在方法调用的时候, 参数和本地变量的值会被压入局部变量表
在方法执行的时候, 可以通过索引来访问局部变量表中的值
2.操作数栈 Operand Stack
操作数栈是用于执行方法时进行计算的临时数据存储过程
操作数栈的元素可以是任意的Java数据类型, 包括基本类型和对象引用
在方法执行过程中, 操作数栈用于存储方法执行过程中的计算结果, 方法参数, 以及临时变量等数据
3.动态链接 Dynamic Linking
动态链接指向运行时常量池中该方法的符号引用的指针
在Java中, 动态链接主要用于解析方法调用的目标地址, 以便在运行时能够正确调用方法
4.方法返回地址
方法返回地址是指向方法调用者的指令地址, 当方法执行完毕后, JVM会使用返回地址恢复执行方法调用者的指令
栈帧的创建和销毁是在方法调用和返回过程中自动进行的, 每当发生方法调用时, JVM会为该方法创建一个新的栈帧并将其推入调用栈(Call Stack), 当方法返回时, 对应的栈帧会被销毁, 栈帧指针会回到前一个方法的栈帧
栈帧的动态创建和销毁确保了方法的独立性和互相调用的正确性
栈的基本方法
push(Object item):将元素item压入栈顶。
pop():弹出栈顶元素,并将其从栈中删除。
peek():返回栈顶元素,但不删除它。
isEmpty():判断栈是否为空,返回布尔值。
search(Object item):搜索元素item在栈中的位置(从栈顶开始),如果找到则返回其距离栈顶的位置(栈顶为1),如果未找到则返回-1。
clear():对当前栈进行清空。
栈的常见使用
括号匹配
给定一个包含括号字符的字符串,判断括号是否匹配,例如 “((()))” 是匹配的,而 “(()” 则不匹配。可以使用栈来实现括号匹配的算法
import java.util.Stack;
public class BracketMatching {
public static boolean isBracketMatch(String input) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
if (ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
} else if (ch == ')' || ch == ']' || ch == '}') {
if (stack.isEmpty()) {
return false;
}
char top = stack.pop();
if ((ch == ')' && top != '(') || (ch == ']' && top != '[') || (ch == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
String input1 = "((()))";
String input2 = "(()";
System.out.println(input1 + " is matched: " + isBracketMatch(input1));
System.out.println(input2 + " is matched: " + isBracketMatch(input2));
}
}
Set
Set集合其实就是一个接口,HashSet和TreeSet实现了Set接口,所有Set所具备的方法HashSet和TreeSet也是具备的。
特点:
set集合是无序的,不重复的(无序的意思是不会按照我们增加进集合的顺序)
遍历通过foreach,迭代器,无法通过下标,因为set集合没有下标
初始容量为16,负载因子0.75倍,扩容量增加1倍
HashSet:基于哈希表实现,提供快速的插入、删除和查找操作。它不保证元素的顺序。
TreeSet:基于红黑树实现,提供有序的不重复元素集合,按照元素的自然顺序或自定义顺序进行排序。
LinkedHashSet:基于哈希表和双向链表实现,保留元素插入的顺序
HashSet
新建HashSet
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
HashSet底层其实就是HashMap, 保证了线程不安全
向HashSet中添加元素
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
首先add()返回值是boolean, 表明当前添加的元素是否出现了重复, 里面直接调用的是map.put(k, v)
put()返回的是: 如果key出现了重复, 则返回上次存放的value, 并且覆盖value.
假如是第一次存放, put()返回的是null, add返回的是true
元素重复,则put()返回的是present对象, add返回的是false
因为HashSet底层存储是基于HashMap, value需要一个固定值, 所以就搞了一个present, 从始至终都是相同的虚值
修改元素
HashSet并没有提高修改元素的方法,直接修改元素可能会导致重复项,与HashSet的特点违背,因此要实现修改元素可采用如下方法:判断元素是否存在,存在则删除,之后添加新元素,最终实现修改元素的效果
删除元素
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
查找元素
因为set集合没有索引, 所以不能直接使用get()获取指定的元素, 因此可以通过迭代器, 增强for遍历每一个元素, 然后获取指定的元素
TreeSet
底层是通过TreeMap<E, Object>实现的, 其中所有元素作为key存储, value是一个固定的Dummy对象PRESENT
元素按照自然顺序(Comparable) 或构造时传入的Comparator排序
Integer能排序(有默认顺序), String能排序(有默认顺序), 自定义的类存储的时候出现异常(没有顺序)
对于自定义的类型,想要在TreeSet中实现排序,必须实现Comparable接口或者编写Comparator比较器,制定其比较规则!JDK自己提供的数据类型不用程序员实现Comparable接口或者编写Comparator比较器是因为它的底层源码都实现了Comparable接口,具有了比较规则,故可以排序!
Comparable接口中重写的CompareTo方法的返回值很重要
返回0表示相同,value会覆盖
返回>0,会继续在右子树上找
返回<0,会继续在左子树上找
在TreeSet中实现自定义类型的排序有两种方法
方式一:放在TreeSet集合中的元素需要实现java.lang.Comparable接口
public class TreeSetTest04 {
public static void main(String[] args) {
Person1 p1 = new Person1(32);
Person1 p2 = new Person1(20);
Person1 p3 = new Person1(25);
TreeSet<Person1> ts = new TreeSet<>();
ts.add(p1);
ts.add(p2);
ts.add(p3);
for (Person1 x:
ts) {
System.out.println(x);
}
}
}
/**
* 放在TreeSet集合中的元素需要实现java.lang.Comparable接口
* 并且实现compareTo方法。equals可以不写
*/
class Person1 implements Comparable<Person1>{
int age;
public Person1(int age){
this.age = age;
}
// 重写toString方法
@Override
public String toString() {
return "Person1{" +
"age=" + age +
'}';
}
/**
* 需要在这个比较的方法中编写比较的逻辑或者比较的规则,按照什么进行比较
* 拿着参数k和集合中的每一个key进行比较,返回值可能是大于0 小于0 或者等于0
* 比较规则最终还是由程序员指定的; 例如按照年龄升序,或者按照年龄降序。
* @param o
* @return
*/
@Override
public int compareTo(Person1 o) { // c1.compareTo(c2)
return this.age-o.age; // >0 =0 <0 都有可能
}
}
方式二:在构造器TreeSet或者TreeMap集合的时候给它传一个比较器对象
public class TreeSetTest06 {
public static void main(String[] args) {
// 此时创建TreeSet集合的时候,需要使用这个比较器。
// TreeSet<WuGui> wuGui = new TreeSet<>(); // 这样不行,没有通过构造方法传递一个比较器进去。
// 给构造方法传递一个比较器
TreeSet<WuGui> wuGui = new TreeSet<>(new WuGuiComparator()); // 底层源码可知其中一个构造方法的参数为比较器
// 大家可以使用匿名内部类的方式
wuGui.add(new WuGui(100));
wuGui.add(new WuGui(10));
wuGui.add(new WuGui(1000));
for (WuGui wugui:
wuGui) {
System.out.println(wugui);
}
}
}
class WuGui {
int age;
public WuGui(int age) {
this.age = age;
}
@Override
public String toString() {
return "WuGui{" +
"age=" + age +
'}';
}
}
// 单独再这里编写一个比较器
// 比较器实现java.util.Comparator接口 (Comparable是java.lang包下的。Comparator是java.util包下的。)
class WuGuiComparator implements Comparator<WuGui>{
@Override
public int compare(WuGui o1, WuGui o2) {
// 指定比较规则
// 按照年龄排序
return o1.age-o2.age;
}
}
总结:
比较规则经常变换: Comparator 接口的设计符合OCP原则(可切换)
比较规则较固定: Comparable
新建TreeSet
private transient NavigableMap<E,Object> m, TreeSet 的底层实现依赖 TreeMap。
private static final Object PRESENT, 占位符对象,用作 TreeMap 的 value。
public TreeSet() {
this(new TreeMap<E,Object>());
}
添加元素
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
底层调用的是 TreeMap.put(key, value)
因为 TreeSet 只关心键,所以 value 一律为一个固定对象 PRESENT
TreeMap.put() 返回 null 说明 key 原本不存在,添加成功
返回 true 表示添加成功,false 表示元素已存在(Set 不允许重复)
删除元素
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
TreeMap.remove(key) 返回旧值(如果有)
如果返回的是 PRESENT,说明之前存在此 key,移除成功
其他方法
public boolean contains(Object o) 判断是否包含元素
public boolean isEmpty() 判断集合是否为空
public int size() 返回元素个数
public void clear() 清空集合
public Iterator<E> iterator() 获取升序迭代器
public Iterator<E> descendingIterator() 获取降序迭代器
public Object clone() 克隆当前集合
public Comparator<? super E> comparator() 获取比较器
public SortedSet<E> subSet(E fromElement, E toElement) 获取子集
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) 获取子集(带边界)
public SortedSet<E> headSet(E toElement) 获取头子集
public NavigableSet<E> headSet(E toElement, boolean inclusive) 获取头子集(带边界)
public SortedSet<E> tailSet(E fromElement) 获取尾子集
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) 获取尾子集(带边界)
public E first() 获取第一个元素
public E last() 获取最后一个元素
树
LinkedHashSet
LinkedHashSet 类是一个既保证元素唯一性又维护插入顺序的集合类
与 HashSet 不同,LinkedHashSet 通过维护一个双向链表来记录元素的插入顺序
主要特点:
唯一性:LinkedHashSet 继承了 HashSet 的特性,确保集合中没有重复元素。
有序性:通过双向链表记录插入顺序,迭代时按插入顺序访问元素。
性能:虽然性能稍低于 HashSet,但仍然提供了常数时间的基本操作(插入、删除、查找)
构造方法
// 创建一个空的 LinkedHashSet
LinkedHashSet<E> set = new LinkedHashSet<>();
// 创建一个具有指定初始容量和加载因子的 LinkedHashSet
LinkedHashSet<E> set = new LinkedHashSet<>(int initialCapacity, float loadFactor);
// 创建一个包含指定集合的 LinkedHashSet
LinkedHashSet<E> set = new LinkedHashSet<>(Collection<? extends E> c);
常用方法
add 方法用于将指定的元素添加到集合中,如果集合中已经存在该元素,则不进行任何操作
public boolean add(E e)
public boolean remove(Object o)
用于从集合中移除指定的元素。
public boolean contains(Object o)
用于判断集合中是否包含指定的元素。
public int size()
用于获取集合中的元素数量。
public void clear()
用于移除集合中的所有元素。
public Iterator<E> iterator()
LinkedHashSet 提供了 iterator 方法用于获取集合的迭代器,以便按插入顺序遍历元素。
内部实现原理
LinkedHashSet 的内部实现主要基于 HashSet 和 LinkedHashMap。HashSet 提供了基本的集合操作,而 LinkedHashMap 负责维护元素的插入顺序
LinkedHashSet 继承自 HashSet,而 HashSet 使用一个 HashMap 来存储元素。LinkedHashSet 使用 LinkedHashMap 代替 HashMap,以便在存储元素的同时记录其插入顺序
public class LinkedHashSet<E> extends HashSet<E> {
public LinkedHashSet() {
super(new LinkedHashMap<>());
}
}
LinkedHashMap 通过双向链表记录元素的插入顺序。每次插入元素时,都会将元素添加到链表的末尾,从而保证迭代时按插入顺序访问元素
Queue
Queue接口是Java集合框架中定义的一个接口,它代表了一个先进先出(FIFO)的队列。Queue接口继承自Collection接口,它定义了一组方法来操作队列中的元素
(1)添加元素:
boolean add(E element): 将指定的元素添加到队列的末尾,如果成功则返回true,如果队列已满则抛出异常。
boolean offer(E element): 将指定的元素添加到队列的末尾,如果成功则返回true,如果队列已满则返回false。
(2)移除元素:
E remove(): 移除并返回队列头部的元素,如果队列为空则抛出异常。
E poll(): 移除并返回队列头部的元素,如果队列为空则返回null。
(3)获取头部元素:
E element(): 获取队列头部的元素,但不移除它,如果队列为空则抛出异常。
E peek(): 获取队列头部的元素,但不移除它,如果队列为空则返回null。
(4)队列大小:
int size(): 返回队列中的元素个数。
boolean isEmpty(): 判断队列是否为空。
Queue接口还有一些其他方法,如clear()用于清空队列中的所有元素,contains(Object o)用于判断队列是否包含指定元素等。
Queue接口的常见实现类包括LinkedList、ArrayDeque和PriorityQueue等。
LinkedList实现了Queue接口,并且还可以作为栈使用,它是一个双向链表。
ArrayDeque是一个双端队列,它同时实现了Queue和Deque接口。
PriorityQueue是一个基于优先级的队列,它允许元素按照优先级顺序被插入和删除。
通过使用Queue接口,我们可以方便地进行队列操作,如入队、出队、查看队列头部元素等。它在处理任务调度、消息传递等场景中非常有用。
Java中常见的队列
ArrayList:ArrayList可以被用作队列,通过在列表末尾添加元素,并使用remove(0)方法从列表的开头删除元素。但是,由于在列表的开头删除元素会导致后续元素的移动,因此对于大量的插入和删除操作来说,ArrayList的性能可能不是最佳选择。
LinkedList:LinkedList也可以用作队列。LinkedList实现了Queue接口,可以使用offer()方法在队列的末尾添加元素,使用poll()方法从队列的开头删除并返回元素。LinkedList对于插入和删除操作具有较好的性能,因为它使用了指针来链接元素,而不需要移动其他元素。
ArrayBlockingQueue:ArrayBlockingQueue是一个有界阻塞队列,底层使用数组实现。它有一个固定的容量,并且在插入或删除元素时可能会阻塞线程,直到满足特定的条件。
LinkedBlockingQueue:LinkedBlockingQueue是一个可选有界或无界的阻塞队列,底层使用链表实现。它具有类似于ArrayBlockingQueue的功能,但在内部实现上略有不同。
PriorityBlockingQueue:PriorityBlockingQueue是一个支持优先级的无界阻塞队列。元素按照它们的优先级顺序被插入和删除。
ConcurrentLinkedQueue:ConcurrentLinkedQueue是一个非阻塞无界队列,它适用于多线程环境。它使用链表实现,并且提供了高效的并发操作。
Deque 接口分析
Deque接口是Java集合框架中定义的一个接口,它代表了一个双端队列(Double Ended Queue)。Deque是"双端队列"的缩写。Deque接口继承自Queue接口,并在其基础上提供了在队列两端进行添加、删除和检索元素的操作。Deque可以在队列的头部和尾部同时进行元素的插入和删除,因此可以作为队列、栈或双向队列使用。
Deque接口定义了以下主要方法和特性:
(1)添加元素:
void addFirst(E element): 将指定元素添加到双端队列的头部。
void addLast(E element): 将指定元素添加到双端队列的尾部。
boolean offerFirst(E element): 将指定元素添加到双端队列的头部,如果成功则返回true,如果队列已满则返回false。
boolean offerLast(E element): 将指定元素添加到双端队列的尾部,如果成功则返回true,如果队列已满则返回false。
(2)移除元素:
E removeFirst(): 移除并返回双端队列的头部元素,如果队列为空则抛出异常。
E removeLast(): 移除并返回双端队列的尾部元素,如果队列为空则抛出异常。
E pollFirst(): 移除并返回双端队列的头部元素,如果队列为空则返回null。
E pollLast(): 移除并返回双端队列的尾部元素,如果队列为空则返回null。
(3)获取头部和尾部元素:
E getFirst(): 获取双端队列的头部元素,但不移除它,如果队列为空则抛出异常。
E getLast(): 获取双端队列的尾部元素,但不移除它,如果队列为空则抛出异常。
E peekFirst(): 获取双端队列的头部元素,但不移除它,如果队列为空则返回null。
E peekLast(): 获取双端队列的尾部元素,但不移除它,如果队列为空则返回null。
Deque接口还提供了一些其他方法,如size()用于返回双端队列中的元素个数,isEmpty()用于判断双端队列是否为空,clear()用于清空双端队列中的所有元素等。
Deque接口的常见实现类包括ArrayDeque和LinkedList。
ArrayDeque是一个基于数组实现的双端队列,支持高效的随机访问和动态扩展。
LinkedList是一个基于链表实现的双端队列,支持高效的插入和删除操作。
通过使用Deque接口,我们可以方便地进行双端队列操作,如在队列的头部和尾部插入和删除元素,获取头部和尾部元素,以及判断队列是否为空。Deque在许多场景下都很有用,比如实现LRU缓存、实现任务调度等。
另外,需要注意的是,Deque接口还可以用作栈(LIFO)的数据结构。通过在队列头部执行插入和删除操作,可以实现栈的功能。常见的栈操作可以使用Deque接口中的以下方法来实现:
void push(E element): 将元素推入栈顶,等同于addFirst(E element)。
E pop(): 弹出并返回栈顶元素,等同于removeFirst()。
E peek(): 获取栈顶元素,等同于peekFirst()。
所以,Deque接口是一个非常有用的接口,提供了双端队列的功能,既可以在队列的头部进行操作,也可以在尾部进行操作。它是Queue接口的扩展,可以方便地实现队列、栈和双向队列的功能,并提供了丰富的方法来操作和访问队列中的元素。
PriorityQueue 的实现原理详解
PriorityQueue的实现原理是基于二叉堆(Binary Heap),它是一种特殊的完全二叉树结构,具有以下性质:
1.最小堆性质:在最小堆中,每个节点的值都小于或等于其子节点的值。也就是说,堆的根节点是最小的元素。
PriorityQueue使用一个数组来存储元素,并通过二叉堆的形式来组织这些元素。数组中的元素按照特定的顺序排列,满足最小堆的性质。在数组中,根节点位于索引0处,而对于任意位置i的节点,它的左子节点位于索引2i+1处,右子节点位于索引2i+2处。
当元素被添加到PriorityQueue时,它会被放置在数组的末尾,并按照以下步骤进行调整,以维护最小堆的性质:
2.上滤(Up-Heap)操作:新插入的元素会与其父节点进行比较。如果新插入的元素的优先级比父节点的优先级低(或者更大),则它会与父节点进行交换,直到满足最小堆的性质。
当从PriorityQueue中删除元素时,队列头部的元素被移除,并将数组的最后一个元素移动到头部位置。然后,这个元素会与其子节点进行比较,以保持最小堆的性质。
3.下滤(Down-Heap)操作:被移动到头部位置的元素会与其子节点进行比较。如果它的优先级比其中一个或两个子节点的优先级高(或者更小),则它会与较小的子节点进行交换。这个过程会递归地向下进行,直到满足最小堆的性质。
通过上述的上滤和下滤操作,PriorityQueue可以保持最小堆的性质,使得具有最高优先级的元素总是位于队列的头部。
PriorityQueue的插入和删除操作的时间复杂度都是O(logN),其中N是队列中的元素个数。这是因为这些操作涉及到堆的调整,需要按照树的高度来进行操作。同时,PriorityQueue还支持高效的查找具有最高优先级的元素,时间复杂度为O(1)。
需要注意的是,PriorityQueue允许元素具有相同的优先级,但它们的顺序不一定是确定的。在这种情况下,PriorityQueue的行为是不保证的,具有相同优先级的元素可能会以任意顺序被取出。
Map
Map<K, V> 是一个泛型接口,其中 K 表示键的类型,V 表示值的类型
特点:
1.map保存的是键值对, 键要求保持唯一, 值可以重复, 键(key)必须是唯一的,重复的键会被覆盖
2.Map 不继承 Collection 接口,但与 Collection 家族(如 List 和 Set)密切相关
3.主要用来存储和快速查找键值对数据, 例如字典, 配置文件, 计数器等
HashMap
HashMap实现了Map接口, 可以根据键快速的查找对应的值, 通过哈希函数将键映射到哈希表中的一个索引位置, 从而实现快速访问
特点:
1.HashMap中的键和值都可以为null, 如果键为null, 则将该键映射到哈希表的第一个位置
2.可以使用迭代器或者forEach方法遍历HashMap中的键值对
3.HashMap有一个初始容量和负载因子.
初始容量是指哈希表的初始大小
负载因子是指哈希表在扩容之前可以存储的键值对数量与哈希表大小的比率
默认的初始容量是16, 负载因子是0.75
4.HashMap是无序的
常用方法
// 增加元素
HashMap<String, Integer> map = new HashMap<>();
map.put("沉默", 20);
map.put("呦呦", 25);
// 删除元素
map.remove("沉默");
// 修改元素
// 因为 HashMap 的键是唯一的,所以再次 put 的时候会覆盖掉之前的键值对
map.put("沉默", 30);
// 查找元素
map.get("沉默")
HashMap经常被用来作为缓存, 索引等场景
可以将用户ID作为键, 用户信息作为值, 将用户信息缓存到HashMap中, 以便快速查找
可以将关键字作为键, 文档ID列表作为值, 将文档索引缓存到HashMap中, 以便快速搜索文档
HashMap的实现原理是基于哈希表的, 他的底层是一个数组, 数组的每一个位置可能是一个链表或红黑树, 也可能只是一个键值对.
当添加一个键值对时, HashMap会根据键的哈希值计算出该键对应的数组下标(索引), 然后将键值对插入到对应的位置
当通过key查找value的时候, HashMap也会根据key的哈希值计算出数组下标, 并查找对应的值
hash()源码解析
// 将 key 的 hashCode 值进行处理,得到最终的哈希值。
static final int hash(Object key) {
int h;
// h = key.hashCode()
// h ^ (h >>> 16) 是为了把高位参与进来
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
>>> 是无符号右移,也就是说把高16位“移动”到低位,然后和原来的低位做异或操作(^)。
最终返回的 result,才是实际用来决定桶位置的 hash 值
这是为了解决哈希分布不均匀的问题
put()源码解析
// 这个是提供给外部使用的添加的方法, 真正的逻辑在putVal()中
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 1. 是否需要初始化 table?
* 2. 计算桶索引 index = (n - 1) & hash
* 3. 如果没有冲突,直接插入
* 4. 如果有冲突:
* 4.1 同一个 key → 覆盖
* 4.2 是红黑树 → 调用树的 putTreeVal()
* 4.3 是链表 → 遍历、追加、判断是否树化
* 5. 更新 size 和判断是否需要扩容
*
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 如果 table 是 null 或长度为 0,先初始化(resize)
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算桶索引位置
// i = (n - 1) & hash 通过扰动哈希计算桶下标
// if ((p = tab[i]) == null) 桶为空,直接放
if ((p = tab[i = (n - 1) & hash]) == null)
// 3. 该桶为空,直接放入新节点
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 4. 桶中已经有元素,判断是否是“同一个 key”,是则覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果该节点的 hash 值一样,且 key 相等(支持 equals) → 说明是同一个 key,要覆盖原 value → 把原节点赋给 e 等待后面处理
e = p;
else if (p instanceof TreeNode) // 5. 是链表或红黑树,遍历后插入
// 如果这个桶是一个 红黑树结构(即链表太长被“树化”了),那就使用 TreeNode 的方式插入。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 如果到达尾部还没找到,就插入新节点
p.next = newNode(hash, key, value, null); // 加入链尾
if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否转红黑树, 默认链表长度超 8
// 如果链表长度超过阈值(默认为 8),触发树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 遍历链表,如果找到相同的 key,跳出(待更新)
break;
p = e;
}
}
// 6. 找到同 key,直接覆盖值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 7. 增加结构修改次数
++modCount;
// 8. 检查是否需要扩容(超过阈值)
if (++size > threshold)
resize();
// 9. 插入完成的回调
afterNodeInsertion(evict);
return null;
}
/**
* put()之后, 扩容的关键代码
* 为什么长度一定是 2 的幂?
* 为什么扩容不需要重新计算 hash
* 如何将旧桶元素迁移到新桶中
*
* 将 HashMap 的数组 table 扩容为原来的 2 倍,并将原有元素重新分布到新数组中。
*
* 步骤 说明
* 1️⃣ 计算新容量、新阈值
* 2️⃣ 新建数组,替换旧的
* 3️⃣ 遍历旧数组,逐个迁移
* 4️⃣ 单节点直接搬,链表分组迁移,红黑树用 split() 拆分
* 5️⃣ 迁移完成,返回新数组
*/
final Node<K,V>[] resize() {
// oldTab:旧的哈希表数组
Node<K,V>[] oldTab = table;
// oldCap:旧容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr:旧的扩容阈值
int oldThr = threshold;
// newCap 新容量; newThr 新阈值
int newCap, newThr = 0;
// 如果旧容量大于0, 说明已经进行过初始化了
if (oldCap > 0) {
// 判断是否已经达到最大容量(2³⁰)
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果是,就不扩了,阈值设最大,直接返回原数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则正常扩容:容量翻倍(oldCap << 1)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) // 旧容量翻倍之后为新容量, 新容量小于2的30次方, 并且旧容量大于等于16(1 << 4)
// 如果旧容量大于默认值(16),阈值也翻倍
newThr = oldThr << 1; // double threshold
}
// 初始容量是懒加载的
else if (oldThr > 0) // initial capacity was placed in threshold
// 如果还没初始化(table 为空),但是阈值有值(来自构造函数) → 用阈值作为初始容量
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 用默认初始容量
// 如果连阈值都没设(put() 时第一次调用) → 用默认值 newCap = 16, 负载因子 0.75 → newThr = 12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的 resize 上限
if (newThr == 0) {
// 如果还没设置新的阈值,这里根据容量和负载因子计算 → 常见情况就是 16 * 0.75 = 12
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新阈值赋值给成员变量 threshold
threshold = newThr;
// 新建一个更大的数组(容量是原来的两倍), 替换掉旧的 table
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新数组 newTab 赋值给成员变量 table
table = newTab;
// 遍历旧数组,把每一个桶的链表/红黑树挪到新数组中。
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 如果该元素不为空
// 将旧数组中该位置的元素置为 null,以便垃圾回收
oldTab[j] = null;
if (e.next == null)
// 没有冲突的节点,直接搬到新数组里
// 不用重新计算hash, 因为容量是 2 的幂,只要看 低位变了没
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 如果是红黑树,走特殊拆分逻辑 split(),分成两个树
// 因为红黑树太复杂了,HashMap 不直接拆节点
// split 会拆成两个树,挂在不同桶位
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表的情况,重新拆分
Node<K,V> loHead = null, loTail = null; // 低位链表的头结点和尾结点
Node<K,V> hiHead = null, hiTail = null; // 高位链表的头结点和尾结点
Node<K,V> next;
do {
next = e.next;
// 实现了链表的“低高位分组”
if ((e.hash & oldCap) == 0) { // 如果该元素在低位链表中
// 如果是 0 → 放在原索引
if (loTail == null) // 如果低位链表还没有结点
loHead = e; // 将该元素作为低位链表的头结点
else
loTail.next = e; // 如果低位链表已经有结点,将该元素加入低位链表的尾部
loTail = e; // 更新低位链表的尾结点
}
else { // 如果该元素在高位链表中
// 如果是 1 → 放在“原索引 + oldCap”的新位置
if (hiTail == null) // 如果高位链表还没有结点
hiHead = e; // 将该元素作为高位链表的头结点
else
hiTail.next = e; // 如果高位链表已经有结点,将该元素加入高位链表的尾部
hiTail = e; // 更新高位链表的尾结点
}
} while ((e = next) != null);
// 最后把链表两组分别挂到新数组的位置上
if (loTail != null) { // 如果低位链表不为空
loTail.next = null; // 将低位链表的尾结点指向 null,以便垃圾回收
newTab[j] = loHead; // 将低位链表作为新数组对应位置的元素
}
if (hiTail != null) { // 如果高位链表不为空
hiTail.next = null; // 将高位链表的尾结点指向 null,以便垃圾回收
newTab[j + oldCap] = hiHead; // 将高位链表作为新数组对应位置的元素
}
}
}
}
}
return newTab;
}
整体解析:
1、获取原来的数组 table、数组长度 oldCap 和阈值 oldThr。
2、如果原来的数组 table 不为空,则根据扩容规则计算新数组长度 newCap 和新阈值 newThr,然后将原数组中的元素复制到新数组中。
3、如果原来的数组 table 为空但阈值 oldThr 不为零,则说明是通过带参数构造方法创建的 HashMap,此时将阈值作为新数组长度 newCap。
4、如果原来的数组 table 和阈值 oldThr 都为零,则说明是通过无参数构造方法创建的 HashMap,此时将默认初始容量 DEFAULT_INITIAL_CAPACITY(16)和默认负载因子 DEFAULT_LOAD_FACTOR(0.75)计算出新数组长度 newCap 和新阈值 newThr。
5、计算新阈值 threshold,并将其赋值给成员变量 threshold。
6、创建新数组 newTab,并将其赋值给成员变量 table。
7、如果旧数组 oldTab 不为空,则遍历旧数组的每个元素,将其复制到新数组中。
8、返回新数组 newTab。
当我们往 HashMap 中不断添加元素时,HashMap 会自动进行扩容操作(条件是元素数量达到负载因子(load factor)乘以数组长度时),以保证其存储的元素数量不会超出其容量限制。
在进行扩容操作时,HashMap 会先将数组的长度扩大一倍,然后将原来的元素重新散列到新的数组中。
由于元素的位置是通过 key 的 hash 和数组长度进行与运算得到的,因此在数组长度扩大后,元素的位置也会发生一些改变。一部分索引不变,另一部分索引为“原索引+旧容量”。
/**
* 扩容为什么不重新计算hash?
*
* 因为 HashMap 的 hash 映射算法是:index = hash & (length - 1)
* 扩容后,只有一位(高一位)发生变化,所以只需要判断:e.hash & oldCap == 0
* 如果是 0 → 继续留在原来的桶
* 如果是 1 → 移动到 原桶 + oldCap
* 这就是 HashMap 迁移时的高效逻辑,节省了再算 hash 的成本!
*/
/**
* 当一个桶上的链表长度超过阈值(默认是 8)时,如果数组长度也超过 64,就会将该链表转换成 红黑树结构
*
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 判断是否需要先扩容, 如果数组容量还没达到 64(MIN_TREEIFY_CAPACITY),不树化,先扩容
// 只有在数组够大时,才会转树,避免小数组里频繁树化,影响性能
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) { // 找到该桶位(链表头节点)
// e:原链表头节点
// 准备构造 TreeNode 类型的新链表:hd 是头,tl 是尾
TreeNode<K,V> hd = null, tl = null;
// 将链表节点替换为 TreeNode 类型
do {
// 用 replacementTreeNode(...) 把每个 Node 转成 TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
// 同时维护 prev 和 next,组装成双向链表形式
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null); // 注意:还没有建成树结构,这只是链表结构的 TreeNode 节点
if ((tab[index] = hd) != null)
// 把头节点设到桶中,并调用 treeify() 方法进行真正的“建树”
// hd.treeify(tab):从头节点开始,把双向链表 TreeNode 转成真正的红黑树结构
hd.treeify(tab);
}
}
从当前桶的头节点开始,把每个节点插入红黑树中(按照红黑树规则), 红黑树是按 hash 和 key 顺序排列
插入时会保持红黑树性质(平衡性、颜色)
使用的是左旋/右旋 + 变色的标准红黑树算法
最后调用 moveRootToFront(...),把 root 移到桶头(优化树访问)
步骤 | 描述
1️⃣ | 判断数组长度,<64 就扩容,不树化
2️⃣ | 获取当前桶位的链表
3️⃣ | 把链表节点转换为 TreeNode,组成双向链表
4️⃣ | 调用 treeify() 将其转换为红黑树结构
5️⃣ | 将 root 节点移动到桶的最前面,方便快速访问
为什么不一开始就用红黑树?
红黑树插入/删除/调整开销大,适合大数据量情况
链表结构更简单、更轻量,适合中小集合
只有链表太长时才考虑使用红黑树(折中之选)
加载因子为什么是0.75
HashMap 的加载因子(load factor,直译为加载因子,意译为负载因子)是指哈希表中填充元素的个数与桶的数量的比值,当元素个数达到负载因子与桶的数量的乘积时,就需要进行扩容。这个值一般选择 0.75,是因为这个值可以在时间和空间成本之间做到一个折中,使得哈希表的性能达到较好的表现。
如果负载因子过大,填充因子较多,那么哈希表中的元素就会越来越多地聚集在少数的桶中,这就导致了冲突的增加,这些冲突会导致查找、插入和删除操作的效率下降。同时,这也会导致需要更频繁地进行扩容,进一步降低了性能。
如果负载因子过小,那么桶的数量会很多,虽然可以减少冲突,但是在空间利用上面也会有浪费,因此选择 0.75 是为了取得一个平衡点,即在时间和空间成本之间取得一个比较好的平衡点。
总之,选择 0.75 这个值是为了在时间和空间成本之间达到一个较好的平衡点,既可以保证哈希表的性能表现,又能够充分利用空间。
get()源码分析
public V get(Object key) {
Node<K,V> e;
// hash(key):先对 key 进行扰动处理,得出 hash 值
// getNode(...):是实际查找节点的核心逻辑, 如果找到了,就返回对应的 value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// tab:当前的 table 数组; first:桶的第一个节点(链表或树根); e:临时节点; k:当前节点的 key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 定位桶位, tab[(n - 1) & hash]:用 hash 值计算数组索引, 如果该桶非空,说明有节点,继续查找
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node, 先快速检查头节点
((k = first.key) == key || (key != null && key.equals(k))))
// 这是最常见的快速路径:在链表头或树根就找到了目标节点
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode) // 如果是红黑树节点,用树的查找方法
// getTreeNode(...) 是红黑树的查找逻辑,时间复杂度 O(log n)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 遍历链表查找, 顺着 e.next 一直找下去,直到找到或遍历完
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 如果找不到, 返回null
return null;
}
为什么 get 快?
用数组 + 位运算定位,时间复杂度 O(1)
桶内结构:
链表结构查找:最坏 O(n),一般很短
红黑树结构:最坏 O(log n)
hash 经过扰动处理(减少冲突),分布均匀
remove()源码分析
public V remove(Object key) {
Node<K,V> e;
// 对 key 做 hash, 调用核心逻辑 removeNode(...) 删除节点, 如果找到了,就返回 value,否则返回 null
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key, key 的 hash 值
* @param key the key, 要删除的 key
* @param value the value to match if matchValue, else ignored, 如果你只删除 value 匹配的 key(一般传 null)
* @param matchValue if true only remove if value is equal, 是否同时比对 value(一般 false)
* @param movable if false do not move other nodes while removing, 是否需要树结构维护(一般 true)
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) { // 根据 hash 计算索引,定位桶
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; // 检查桶的第一个节点是否就是要删除的 key, 如果是,就标记它为待删除的 node
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果是红黑树结构,用红黑树方式查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 如果是链表结构,顺着 next 一步步查找, 找到后将 node 指向它,同时保留前一个节点 p
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 判断是否要根据 value 精确匹配(一般为 false)
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 如果是红黑树节点,调用 removeTreeNode() 维护红黑树结构, 会处理红黑树的结构调整(变色、旋转、降级为链表等)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果是桶头节点,直接把桶的第一个元素指向它的下一个
tab[index] = node.next;
else
// 如果是链表中间节点,就把上一个节点的 next 指向它的 next
p.next = node.next;
// 修改 size 和 modCount,返回被删除的节点
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结 remove() 流程
remove(key) →
➤ 计算 hash,定位桶 index
➤ 如果是链表:
▸ 遍历链表,找 key 相等的节点,断链
➤ 如果是红黑树:
▸ 调用 removeTreeNode 处理红黑树节点
➤ size--,返回被删节点的 value
树结构下的删除操作会发生什么?
当树内节点数量少于 6 时,HashMap 会自动退化为链表(这叫做“untreeify”)
总结核心点
特性 | 说明
支持链表和红黑树两种结构 | 桶可能是链表,也可能是红黑树
删除分三种情况 | 桶头节点 / 链表中间节点 / 红黑树节点
红黑树会自动维护结构 | 删除后会做旋转、变色,保持平衡;节点太少时会退化为链表
modCount++ | 用于 fail-fast 机制,保证迭代时能检测结构变化
线程不安全
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
HashMap的线程不安全主要体现在下面两个方面:
在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
TreeMap
TreeMap实现了SortedMap接口, 可以自动将键按照自然顺序或指定的比较器顺序排序, 并保证其元素的顺序
内部使用红黑树来实现键的排序和查找
与 HashMap 不同的是,TreeMap 会按照键的顺序来进行排序。
平衡二叉树
红黑树(英语:Red–black tree)是一种自平衡的二叉查找树(Binary Search Tree),结构复杂,但却有着良好的性能,完成查找、插入和删除的时间复杂度均为 log(n)
二叉查找树是一种常见的树形结构,它的每个节点都包含一个键值对。每个节点的左子树节点的键值小于该节点的键值,右子树节点的键值大于该节点的键值,这个特性使得二叉查找树非常适合进行数据的查找和排序操作
下面是一个简单的手绘图,展示了一个二叉查找树的结构:
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
在上面这个二叉查找树中,根节点是 8,左子树节点包括 3、1、6、4 和 7,右子树节点包括 10、14 和 13
3<8<10
1<3<6
4<6<7
10<14
13<14
这是一颗典型的二叉查找树:
1)左子树上所有节点的值均小于或等于它的根结点的值。
2)右子树上所有节点的值均大于或等于它的根结点的值。
3)左、右子树也分别为二叉查找树。
二叉查找树用来查找非常方面,从根节点开始遍历,
如果当前节点的键值等于要查找的键值,则查找成功;
如果要查找的键值小于当前节点的键值,则继续遍历左子树;
如果要查找的键值大于当前节点的键值,则继续遍历右子树。如果遍历到叶子节点仍然没有找到,则查找失败。
插入操作也非常简单,从根节点开始遍历,
如果要插入的键值小于当前节点的键值,则将其插入到左子树中;
如果要插入的键值大于当前节点的键值,则将其插入到右子树中。
如果要插入的键值已经存在于树中,则更新该节点的值。
删除操作稍微复杂一些,需要考虑多种情况,包括
要删除的节点是叶子节点、
要删除的节点只有一个子节点、
要删除的节点有两个子节点等等。
总之,二叉查找树是一种非常常用的数据结构,它可以帮助我们实现数据的查找、排序和删除等操作。
不过,二叉查找树有一个明显的不足,就是容易变成瘸子,就是一侧多,一侧少,比如说这样:
6
/ \
4 8
/ / \
3 7 9
/
1
在上面这个不平衡的二叉查找树中,左子树比右子树高。根节点是 6,左子树节点包括 4、3 和 1,右子树节点包括 8、7 和 9。
由于左子树比右子树高,这个不平衡的二叉查找树可能会导致查找、插入和删除操作的效率下降。
还有更极端的情况:
1
\
2
\
3
\
4
\
5
\
6
在上面这个极度不平衡的二叉查找树中,所有节点都只有一个右子节点,根节点是 1,右子树节点包括 2、3、4、5 和 6。
这种极度不平衡的二叉查找树会导致查找、插入和删除操作的效率急剧下降,因为每次操作都只能在右子树中进行,而左子树几乎没有被利用到
于是就有了平衡二叉树,左右两个子树的高度差的绝对值不超过 1
8
/ \
4 12
/ \ / \
2 6 10 14
/ \ / \
5 7 13 15
根节点是 8,左子树节点包括 4、2、6、5 和 7,右子树节点包括 12、10、14、13 和 15。左子树和右子树的高度差不超过1,因此它是一个平衡二叉查找树。
平衡二叉树就像是一棵树形秤,它的左右两边的重量要尽可能的平衡。当我们往平衡二叉树中插入一个节点时,平衡二叉树会自动调整节点的位置,以保证树的左右两边的高度差不超过1。类似地,当我们删除一个节点时,平衡二叉树也会自动调整节点的位置,以保证树的左右两边的高度差不超过1。
常见的平衡二叉树包括AVL树、红黑树等等,它们都是通过旋转操作来调整树的平衡,使得左子树和右子树的高度尽可能接近。
AVL树
8
/ \
4 12
/ \ / \
2 6 10 14
/ \
5 7
AVL树是一种高度平衡的二叉查找树,它要求左子树和右子树的高度差不超过1。由于AVL树的平衡度比较高,因此在进行插入和删除操作时需要进行更多的旋转操作来保持平衡,但是在查找操作时效率较高。
AVL树适用于读操作比较多的场景。
例如,对于一个需要频繁进行查找操作的场景,如字典树、哈希表等数据结构,可以使用AVL树来进行优化。另外,AVL树也适用于需要保证数据有序性的场景,如数据库中的索引。
红黑树
R 即 Red「红」、B 即 Black「黑」
8B
/ \
4R 12R
/ \ / \
2B 6B 10B 14B
/ \
5R 7R
红黑树,顾名思义,就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持二叉树的平衡,它要求任意一条路径上的黑色节点数目相同,同时还需要满足一些其他特定的条件,如红色节点的父节点必须为黑色节点等。
1)每个节点都只能是红色或者黑色
2)根节点是黑色
3)每个叶节点(NIL 节点,空节点)是黑色的。
4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。
5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
由于红黑树的平衡度比AVL树稍低,因此在进行插入和删除操作时需要进行的旋转操作较少,但是在查找操作时效率仍然较高。
红黑树适用于读写操作比较均衡的场景
自然顺序
public V put(K key, V value) {
Entry<K,V> t = root; // 将根节点赋值给变量t
if (t == null) { // 如果根节点为null,说明TreeMap为空
compare(key, key); // type (and possibly null) check,检查key的类型是否合法
root = new Entry<>(key, value, null); // 创建一个新节点作为根节点
size = 1; // size设置为1
return null; // 返回null,表示插入成功
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths,根据使用的比较方法进行查找
Comparator<? super K> cpr = comparator; // 获取比较器
if (cpr != null) { // 如果使用了Comparator
do {
parent = t; // 将当前节点赋值给parent
cmp = cpr.compare(key, t.key); // 使用Comparator比较key和t的键的大小
if (cmp < 0) // 如果key小于t的键
t = t.left; // 在t的左子树中查找
else if (cmp > 0) // 如果key大于t的键
t = t.right; // 在t的右子树中查找
else // 如果key等于t的键
return t.setValue(value); // 直接更新t的值
} while (t != null);
}
else { // 如果没有使用Comparator
if (key == null) // 如果key为null
throw new NullPointerException(); // 抛出NullPointerException异常
Comparable<? super K> k = (Comparable<? super K>) key; // 将key强制转换为Comparable类型
do {
parent = t; // 将当前节点赋值给parent
cmp = k.compareTo(t.key); // 使用Comparable比较key和t的键的大小
if (cmp < 0) // 如果key小于t的键
t = t.left; // 在t的左子树中查找
else if (cmp > 0) // 如果key大于t的键
t = t.right; // 在t的右子树中查找
else // 如果key等于t的键
return t.setValue(value); // 直接更新t的值
} while (t != null);
}
// 如果没有找到相同的键,需要创建一个新节点插入到TreeMap中
Entry<K,V> e = new Entry<>(key, value, parent); // 创建一个新节点
if (cmp < 0) // 如果key小于parent的键
parent.left = e; // 将e作为parent的左子节点
else
parent.right = e; // 将e作为parent的右子节点
fixAfterInsertion(e); // 插入节点后需要进行平衡操作
size++; // size加1
return null; // 返回null,表示插入成功
}
首先定义一个Entry类型的变量t,用于表示当前的根节点;
如果t为null,说明TreeMap为空,直接创建一个新的节点作为根节点,并将size设置为1;
如果t不为null,说明需要在TreeMap中查找键所对应的节点。
因为TreeMap中的元素是有序的,所以可以使用二分查找的方式来查找节点;
如果TreeMap中使用了Comparator来进行排序,则使用Comparator进行比较,否则使用Comparable进行比较。
如果查找到了相同的键,则直接更新键所对应的值;
如果没有查找到相同的键,则创建一个新的节点,并将其插入到TreeMap中。然后使用fixAfterInsertion()方法来修正插入节点后的平衡状态;
最后将TreeMap的size加1,然后返回null。如果更新了键所对应的值,则返回原先的值。
自定义排序
如果自然顺序满足不了, 那就需要在声明 TreeMap 对象的时候指定排序规则
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
Comparator.reverseOrder() 返回的是 Collections.ReverseComparator 对象,就是用来反转顺序的,非常方便
private static class ReverseComparator
implements Comparator<Comparable<Object>>, Serializable {
// 单例模式,用于表示逆序比较器
static final ReverseComparator REVERSE_ORDER
= new ReverseComparator();
// 实现比较方法,对两个实现了Comparable接口的对象进行逆序比较
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1); // 调用c2的compareTo()方法,以c1为参数,实现逆序比较
}
// 反序列化时,返回Collections.reverseOrder(),保证单例模式
private Object readResolve() {
return Collections.reverseOrder();
}
// 返回正序比较器
@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.naturalOrder();
}
}
排序的好处
TreeMap 的元素是经过排序的,可以找出最大的那个,最小的那个,或者找出所有大于或者小于某个值的键
firstKey() 返回最小的 key(树中最左节点)。
lastKey() 返回最大的 key(树中最右节点)。
ceilingKey(K key) 返回大于等于给定 key 的最小 key。
floorKey(K key) 返回小于等于给定 key 的最大 key。
higherKey(K key) 返回严格大于给定 key 的最小 key。
lowerKey(K key) 返回严格小于给定 key 的最大 key。
headMap(K toKey) 返回小于 toKey 的子映射。
tailMap(K fromKey) 返回大于等于 fromKey 的子映射。
subMap(K fromKey, K toKey) 返回在两个 key 范围之间的映射(左闭右开)。
descendingMap() 返回 key 倒序排列的视图。
descendingKeySet() 返回倒序的 key 集合。
pollFirstEntry() 移除并返回最小的 entry(左端节点)。
pollLastEntry() 移除并返回最大的 entry(右端节点)。
comparator() 返回用于排序的比较器(若未指定,则为自然顺序)。
可以用作优先队列替代(支持动态删除最小或最大元素)。
适合需要快速查找范围区间的场景,比如缓存系统、滑动窗口算法、事件时间窗口统计等。
在实现一些前驱/后继查找算法时非常方便(如最近值查找)
LinkedHashMap
HashMap是无序的, 如果我们需要使用到有序的Map, 就要用到LinkedHashMap
LinkedHashMap是HashMap的子类, 它使用链表来记录插入/访问元素的顺序
LinkedHashMap可以看做是HashMap + LinkedList的合体, 使用哈希表来存储数据, 又用了双向链表来维持顺序
由于 LinkedHashMap 要维护双向链表,所以 LinkedHashMap 在插入、删除操作的时候,花费的时间要比 HashMap 多一些
它的头节点表示最早插入或访问的元素,尾节点表示最晚插入或访问的元素。这个链表的作用就是让 LinkedHashMap 可以保持键值对的顺序,并且可以按照顺序遍历键值对
LinkedHashMap 还提供了两个构造方法来指定排序方式,分别是按照插入顺序排序和按照访问顺序排序。在按照访问顺序排序的情况下,每次访问一个键值对,都会将该键值对移到链表的尾部,以保证最近访问的元素在最后面。如果需要删除最早加入的元素,可以通过重写 removeEldestEntry() 方法来实现
插入顺序
1.null会插入到HashMap的第一位, 不管是null在第几位插入进去的, 遍历的时候都会跑到第一位, 而LinkedHashMap 是可以维持插入顺序的。
2.LinkedHashMap 并未重写 HashMap 的 put() 方法,而是重写了 put() 方法需要调用的内部方法 newNode()
这是 HashMap 的。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
这是 LinkedHashMap 的。
HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<>(hash, key, value, e);
linkNodeLast(p);
return p;
}
LinkedHashMap.Entry 继承了 HashMap.Node,并且追加了两个字段 before 和 after,用来维持键值对的关系。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
在 LinkedHashMap 中,链表中的节点顺序是按照插入顺序维护的。
当使用 put() 方法向 LinkedHashMap 中添加键值对时,会将新节点插入到链表的尾部,并更新 before 和 after 属性,以保证链表的顺序关系——由 linkNodeLast() 方法来完成
/**
* 将指定节点插入到链表的尾部
*
* @param p 要插入的节点
*/
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail; // 获取链表的尾节点
tail = p; // 将 p 设为尾节点
if (last == null)
head = p; // 如果链表为空,则将 p 设为头节点
else {
p.before = last; // 将 p 的前驱节点设为链表的尾节点
last.after = p; // 将链表的尾节点的后继节点设为 p
}
}
LinkedHashMap
在添加第一个元素的时候,会把 head 赋值为第一个元素,
等到第二个元素添加进来的时候,会把第二个元素的 before 赋值为第一个元素,第一个元素的 afer 赋值为第二个元素。
这就保证了键值对是按照插入顺序排列的
访问顺序
LinkedHashMap 不仅能够维持插入顺序,还能够维持访问顺序。访问包括调用 get() 方法、remove() 方法和 put() 方法
要维护访问顺序,需要我们在声明 LinkedHashMap 的时候指定三个参数。
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
第一个: 初始容量
第二个: 负载因子
第三个: 第三个参数如果为 true 的话,就表示 LinkedHashMap 要维护访问顺序;否则,维护插入顺序。默认是 false
LRU缓存
LinkedHashMap 继承自 HashMap,并额外维护了一个双向链表,用于记录键值对的顺序。
这个顺序默认是插入顺序(也就是你 put 的顺序),但是如果你在创建 LinkedHashMap 时将 accessOrder 设置为 true,那么它会按照最近访问顺序(Least Recently Accessed)进行排序
可以使用 LinkedHashMap 来实现 LRU 缓存,LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰
在插入元素的时候,需要调用 put() 方法,该方法最后会调用 afterNodeInsertion() 方法,这个方法被 LinkedHashMap 重写了
/**
* 在插入节点后,如果需要,可能会删除最早加入的元素
*
* @param evict 是否需要删除最早加入的元素
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) { // 如果需要删除最早加入的元素
K key = first.key; // 获取要删除元素的键
removeNode(hash(key), key, null, false, true); // 调用 removeNode() 方法删除元素
}
}
removeEldestEntry() 方法会判断第一个元素是否超出了可容纳的最大范围,如果超出,那就会调用 removeNode() 方法对最不经常访问的那个元素进行删除。
get()源码解析
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
/**
* 在访问节点后,将节点移动到链表的尾部
*
* @param e 要移动的节点
*/
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) { // 如果按访问顺序排序,并且访问的节点不是尾节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null; // 将要移动的节点的后继节点设为 null
if (b == null)
head = a; // 如果要移动的节点没有前驱节点,则将要移动的节点设为头节点
else
b.after = a; // 将要移动的节点的前驱节点的后继节点设为要移动的节点的后继节点
if (a != null)
a.before = b; // 如果要移动的节点有后继节点,则将要移动的节点的后继节点的前驱节点设为要移动的节点的前驱节点
else
last = b; // 如果要移动的节点没有后继节点,则将要移动的节点的前驱节点设为尾节点
if (last == null)
head = p; // 如果尾节点为空,则将要移动的节点设为头节点
else {
p.before = last; // 将要移动的节点的前驱节点设为尾节点
last.after = p; // 将尾节点的后继节点设为要移动的节点
}
tail = p; // 将要移动的节点设为尾节点
++modCount; // 修改计数器
}
}
Java中的泛型
参考: https://blog.csdn.net/s10461/article/details/53941091?fromshare=blogdetail&sharetype=blogdetail&sharerId=53941091&sharerefer=PC&sharesource=m0_73058114&sharefrom=from_link
Java泛型是JDK5引入的新特性, 泛型提供了编译时类型安全监测机制, 该机制允许程序员在编译时检测到非法的类型
泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数, 在不创建新的类型的情况下, 通过泛型指定的不同类型来控制形参具体限制的类型
泛型, 即"参数化类型", 将类型由原来的具体的类型参数化, 类似于方法中的变量参数, 此时类型也定义成参数形式(可以称之为类型形参), 然后在使用/调用的时候, 传入具体的类型(类型实参)
其实就是定义方法的时候是形参, 调用方法的时候传递实参
Java中的泛型标记符:
E - Element, 在集合中使用, 因为集合中存放的是元素
T - Type, Java类
K - Key, 键
V - Value, 值
N - Number, 数值类型
? - 表示不确定的Java类型
在泛型使用过程中, 操作的数据类型被指定为一个参数, 这种参数类型可以用在类, 接口, 方法中,分别被称为"泛型类", "泛型接口", "泛型方法"
泛型特性
泛型只在编译阶段有效, 在编译后, 程序会采取去泛型化的措施
在编译过程中, 正确检验泛型结果后, 会将泛型的相关信息擦除, 并且在对象进去和离开方法的边界处添加类型检查和类型转换的方法, 也就是说, 泛型信息不会进入到运行时阶段
泛型类型在逻辑上看, 可以看成是多个不同的类型, 实际上都是相同的基本类型
泛型类
泛型类型用于类的定义中, 被称为泛型类.
通过泛型可以完成对一组类的操作对外开放相同的接口
最典型的就是各种容器类: List, Set, Map
泛型类的基本写法
class 类名称 <泛型标识: 可以写成任意标识号, 标识指定的泛型的类型> {
private 泛型标识 var; // 泛型标识就是成员变量类型
...
}
一个普通的泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);
//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue
定义泛型类的时候, 并不一定要传入泛型类型实参
在使用泛型的时候, 如果传入泛型实参, 则会根据传入的泛型实参做相应的限制, 此时泛型才会起到本应起到的限制作用
如果不传入泛型类型实参的话, 在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型
泛型的类型参数只能是类类型,不能是简单类型
不能对确切的泛型类型使用instanceof操作, 会报编译异常
Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);
Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false
泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型通配符
泛型的上下边界添加,必须与泛型的声明在一起
? extends 类 : 设置通配符上界
<? extends 上界>, 可以传入的类型是上界或者上界的子类
通配符的上界,不能进行写入数据,只能进行读取数据
? super 类 : 设置通配符下界
<? super 下界>, 可以传入的类型是下界或者下界的父类
通配符的下界,不能进行读取数据,只能写入数据
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
使用如下:
Object obj = genericMethod(Class.forName("com.test.test"));
泛型方法的基本用法
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
类中的泛型方法
泛型方法可以出现在任何地方和任何场景中使用, 当泛型方法出现在泛型类中是一种特殊情况
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
泛型方法和可变参数
public <T> void printMsg( T... args){
for(T t : args){
Log.d("泛型测试","t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);
静态方法和泛型
在类中的静态方法使用泛型: 静态方法无法访问类上定义的泛型; 如果静态方法操作的引用数据类型不确定的时候, 必须要将泛型方法定义在方法上
即: 如果静态方法想要使用泛型的话, 必须要将静态方法也定义成泛型方法
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
总结
泛型方法能使方法独立于类而产生变化
如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法
另外对于一个static的方法,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法
Java迭代器Iterator和Iterable有什么区别?
(Iterable)able 表示这个 List 是支持迭代的,而 (Iterator)tor 表示这个 List 是如何迭代的。
如果需要进行单向遍历、修改等操作,可以选择使用Iterator接口;
如果需要进行双向遍历、只读操作等操作,则可以选择使用Iterable接口
对于集合的遍历有三种方式
1.for循环
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
2.迭代器
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + ",");
}
3.for-each
for (String str : list) {
System.out.print(str + ",");
}
for-each其实反编译之后也是使用的迭代器Iterator
Iterator var3 = list.iterator();
while(var3.hasNext()) {
String str = (String)var3.next();
System.out.print(str + ",");
}
for-each只是一个语法糖, 为了让开发者少写点代码, 看起来更简洁一些
Iterator是个接口, JDK1.2就存在了, 用来改进Enumeration接口
1.允许删除元素, 增加了remove方法
2.优化了方法名
Iterator源码
public interface Iterator<E> {
// 判断集合中是否存在下一个对象
boolean hasNext();
// 返回集合中的下一个对象,并将访问指针移动一位
E next();
// 删除集合中调用next()方法返回的对象
default void remove() {
throw new UnsupportedOperationException("remove");
}
/**
* JDK 1.8 时,Iterable 接口中新增了 forEach 方法。
* 该方法接受一个 Consumer 对象作为参数,用于对集合中的每个元素执行指定的操作。
* 该方法的实现方式是使用 for-each 循环遍历集合中的元素,对于每个元素,调用 Consumer 对象的 accept 方法执行指定的操 * 作。
*/
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
}
forEach()实现时首先会对 action 参数进行非空检查,如果为 null 则抛出 NullPointerException 异常。
然后使用 for-each 循环遍历集合中的元素,并对每个元素调用 action.accept(t) 方法执行指定的操作。
由于 Iterable 接口是 Java 集合框架中所有集合类型的基本接口,因此该方法可以被所有实现了 Iterable 接口的集合类型使用。
它对 Iterable 的每个元素执行给定操作,具体指定的操作需要自己写Consumer接口通过accept方法回调出来
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(integer -> System.out.println(integer));
通俗易懂点就是:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});

List的关系图谱中并没有直接使用Iterator, 而是使用了Iterable做了过渡
public interface Iterable<T> {
Iterator<T> iterator();
}
以ArrayList为例
ArrayList重写了 Iterable 接口的 iterator 方法
public Iterator<E> iterator() {
return new Itr();
}
返回的对象 Itr 是个内部类,实现了 Iterator 接口,并且按照自己的方式重写了 hasNext、next、remove 等方法
/**
* ArrayList 迭代器的实现,内部类。
*/
private class Itr implements Iterator<E> {
/**
* 游标位置,即下一个元素的索引。
*/
int cursor;
/**
* 上一个元素的索引。
*/
int lastRet = -1;
/**
* 预期的结构性修改次数。
*/
int expectedModCount = modCount;
/**
* 判断是否还有下一个元素。
*
* @return 如果还有下一个元素,则返回 true,否则返回 false。
*/
public boolean hasNext() {
return cursor != size;
}
/**
* 获取下一个元素。
*
* @return 列表中的下一个元素。
* @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。
*/
@SuppressWarnings("unchecked")
public E next() {
// 获取 ArrayList 对象的内部数组
Object[] elementData = ArrayList.this.elementData;
// 记录当前迭代器的位置
int i = cursor;
if (i >= size) {
throw new NoSuchElementException();
}
// 将游标位置加 1,为下一次迭代做准备
cursor = i + 1;
// 记录上一个元素的索引
return (E) elementData[lastRet = i];
}
/**
* 删除最后一个返回的元素。
* 迭代器只能删除最后一次调用 next 方法返回的元素。
*
* @throws ConcurrentModificationException 如果在最后一次调用 next 方法之后列表结构被修改,则抛出 ConcurrentModificationException 异常。
* @throws IllegalStateException 如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。
*/
public void remove() {
// 检查在最后一次调用 next 方法之后是否进行了结构性修改
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
// 如果上一次调用 next 方法之前没有调用 remove 方法,则抛出 IllegalStateException 异常
if (lastRet < 0) {
throw new IllegalStateException();
}
try {
// 调用 ArrayList 对象的 remove(int index) 方法删除上一个元素
ArrayList.this.remove(lastRet);
// 将游标位置设置为上一个元素的位置
cursor = lastRet;
// 将上一个元素的索引设置为 -1,表示没有上一个元素
lastRet = -1;
// 更新预期的结构性修改次数
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
LinkedList,除了支持正序的遍历方式,还支持逆序的遍历方式——DescendingIterator:
/**
* ArrayList 逆向迭代器的实现,内部类。
*/
private class DescendingIterator implements Iterator<E> {
/**
* 使用 ListItr 对象进行逆向遍历。
*/
private final ListItr itr = new ListItr(size());
/**
* 判断是否还有下一个元素。
*
* @return 如果还有下一个元素,则返回 true,否则返回 false。
*/
public boolean hasNext() {
return itr.hasPrevious();
}
/**
* 获取下一个元素。
*
* @return 列表中的下一个元素。
* @throws NoSuchElementException 如果没有下一个元素,则抛出 NoSuchElementException 异常。
*/
public E next() {
return itr.previous();
}
/**
* 删除最后一个返回的元素。
* 迭代器只能删除最后一次调用 next 方法返回的元素。
*
* @throws UnsupportedOperationException 如果列表不支持删除操作,则抛出 UnsupportedOperationException 异常。
* @throws IllegalStateException 如果在调用 next 方法之前没有调用 remove 方法,或者在同一次迭代中多次调用 remove 方法,则抛出 IllegalStateException 异常。
*/
public void remove() {
itr.remove();
}
}
Java Comparable和Comparator的区别?
1.一个类实现了 Comparable 接口,意味着该类的对象可以直接进行比较(排序),但比较(排序)的方式只有一种,很单一。
2.一个类如果想要保持原样,又需要进行不同方式的比较(排序),就可以定制比较器(实现 Comparator 接口)。
3.Comparable 接口在 java.lang 包下,而 Comparator 接口在 java.util 包下,算不上是亲兄弟,但可以称得上是表(堂)兄弟。
4.总而言之,如果对象的排序需要基于自然顺序,请选择 Comparable,如果需要按照对象的不同属性进行排序,请选择 Comparator。
IO流

初始IO
IO, 即in和out, 也就是输入和输出, 指应用程序和外部设备之间的数据传递, 常见的外部设备包括文件, 管道, 网络连接
IO流就是用来读取内存或者文件中或者网络中的数据的
Input, 称为输入流, 负责将磁盘/网络中的数据读到内存(程序)中去
output, 称为输出流, 负责将内存(程序中产生的)数据写到磁盘/网络中去
Java是通过流进行处理IO的
我们可以将内存中的数据通过io流存储到磁盘中的某个文件中, 也可以通过io流将磁盘中的某个文件中的数据读取到内存中
流(Stream), 是一个抽象的概念, 是指一连串的数据(字符或字节), 是以先进先出的方式发送信息的通道
当程序需要读取数据的时候, 就会开启一个通向数据源的流, 这个数据源可以是文件, 内存, 或者是网络连接
当程序需要写入数据的时候, 就会开启一个通向目的地的流
流的特性:
1.先进先出: 最先写入输出流的数据最先被输入流读取到
2.顺序存取: 可以one by one的往流中写入一串字节, 读出时也将按写入顺序读取一串字节, 不能随机访问中间的数据(RrandomAccessFile除外)
3.只读或只写: 每个流只能是输入流或输出流的一种, 不能同时具备两个功能
输入流只能进行读操作
输出流只能进行写操作
在一个数据传输通道中, 如果既要写入数据, 又要读取数据, 则要分别提供两个流
传输方式(流中最小单位)划分
传输方式有两种,字节和字符
字节: (byte)是计算机中用来表示存储容量的一个计量单位,通常情况下,一个字节有 8 位(bit)
字符: (char)可以是计算机中使用的字母、数字、和符号,比如说 A 1 $ 这些
通常情况下, 一个字母或者一个字符占用一个字节,一个汉字占用两个字节
具体还要看字符编码
在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;
在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节
字节流用来处理二进制文件, 如图片, MP3, 视频
字符流用来处理文本文件, 文本文件可以看作是一种特殊的二进制文件, 只不过经过了编码, 便于阅读
换句话说, 字节流可以处理一切文件, 而字符流只能处理文本
字节流和字符流的区别:
1.字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
2.字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
核心抽象类
核心的就是 4 个抽象类:InputStream、OutputStream、Reader、Writer
抽象类的方法也很多, 最核心的就是read 和write
字节输入流InputStream: 以内存为基准, 来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流
字节输出流outputStream: 以内存为基准, 把内存中的数据以字节的形式写出到磁盘文件或者网络中去的流
字符输入流Reader: 以内存为基准, 来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流
字符输出流Writer: 以内存为基准, 把内存中的数据以字符的形式写出到磁盘文件或者网络介质中去的流
InputStream
int read():读取数据
int read(byte b[], int off, int len):从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中
long skip(long n):跳过指定个数的字节
int available():返回可读的字节数
void close():关闭流,释放资源
outputStream
void write(int b): 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲)
void write(byte b[], int off, int len): 将数组 b 中的从 off 位置开始,长度为 len 的字节写入
void flush(): 强制刷新,将缓冲区的数据写入
void close():关闭流
Reader
int read():读取单个字符
int read(char cbuf[], int off, int len):从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中
long skip(long n):跳过指定个数的字符
int ready():是否可以读了
void close():关闭流,释放资源
Writer
void write(int c): 写入一个字符
void write( char cbuf[], int off, int len): 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入
void flush(): 强制刷新,将缓冲区的数据写入
void close():关闭流
字节流非常适合做一切文件的复制操作
任何文件的底层都是字节, 字节流做复制, 是一字不漏的转移完全部字节, 只要复制后的文件格式一致就没有问题
文件字节输入流 FileInputStream
| 构造器 | 说明 |
|---|---|
| public FileInputStream(File file) | 创建字节输入流管道与源文件接通 |
| public FileInputStream(String pathname) | 创建字节输入流管道与源文件接通 |
| 方法名称 | 说明 |
| public int read() | 每次读取一个字节返回, 如果发现没有数据可读会返回-1 |
| public int read(byte[] buffer) | 每次用一个字节数组去读取数据, 返回字节数组读取了多少个字节, 如果发现没有数据可读会返回-1 |
文件字节输入流每次读取一个字节的方式注意事项:
- 这种读取方式性能很差, 因为读取数据的时候, 是从硬盘中读取的, 需要频繁调用系统资源读取字节!
- 如果读取汉字的话, 输出是会乱码的, 并且无法避免
- 流使用之后, 必须关闭, 释放系统资源, 否则会出问题
文件字节输入流每次读取一个字节数组的方式注意事项:
- 这种读取方式可以控制每次读取数据的大小
- 每次读取的时候, 要保证读取多少, 就倒出多少, 否则会出问题
- 这种方式读取汉字也避免不了输出会乱码的问题
InputStream is = new FileInputStream("pathname")
byte[] buffer = new byte[3];
int len = is.read(buffer);
String rs = new String(buffer, 0, len); // 确保读取多少就倒出多少
解决文件字节输入流读取中文乱码问题
方式一: 一次性读取完全部字节
自己定义一个字节数组与被读取的文件大小一样大, 然后使用该字节数组, 一次读完文件的全部字节
InputStream is = new FileInputStream("pathname");
//准备一个字节数组, 大小与文件的大小一致
long size = new File("pathname").length();
byte[] buffer = new byte[(int)size];
int len = is.read(buffer);
sout(new String(buffer))
方式二: Java官方提供了方法
Java官方为InputStream提供了如下方法, 可以直接将文件的全部字节读取到一个字节数组中返回
public byte[] readAllBytes() throws IOException; 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回
byte[] buffer = is.readAllBytes();
sout(new String(buffer));
如果文件过大, 创建的字节数组也会过大, 可能会引起内存溢出的问题, 因此, 读写文本内容更适合用字符流. 字节流适合做数据的转移, 如: 文件复制等;
文件字节输出流FileOutputStream
| 构造器 | 说明 |
|---|---|
| public FileoutputStream(File file) | 创建字节输出流管道与源文件对象接通 |
| public FileoutputStream(String filepath) | 创建字节输出流管道与源文件路径接通 |
| public FileoutputStream(File file, boolean append) | 创建字节输出流管道与源文件对象接通, 可追加数据 |
| public FileoutputStream(String filepath, boolean append) | 创建字节输出流管道与源文件路径接通, 可追加数据 |
| 方法名称 | 说明 |
| public void write(int a) | 写一个字节出去 |
| public void write(byte[] buffer) | 写一个字节数组出去 |
| public void write(byte[] buffer, int pos, int len) | 写一个字节数组的一部分出去, pos哪个地方开始, len写多少出去 |
| public void close() throws IOException | 关闭流 |
注意事项:
- 文件字节输出流向目标写出数据的时候, 会自动创建文件
- 使用write(int a)这个方法的时候, 如果写汉字出去, 会出现乱码情况, 因为这个方法默认写一个字节出去
字符流
文件字符输入流FileReader-读字符数据进来
| 构造器 | 说明 |
|---|---|
| public FileReader(File file) | 创建字符输入流管道与源文件接通 |
| public FileReader(String pathname) | 创建字符输入流管道与源文件接通 |
| 方法名称 | 说明 |
| public int read() | 每次读取一个字符返回, 如果发现没有数据可读会返回-1 |
| public int read(char[] buffer) | 每次用一个字符数组去读取数据, 返回字符数组读取了多少个字符, 如果发现没有数据可读会返回-1 |
文件字符输出流Filewriter-写字符数据出去
| 构造器 | 说明 |
|---|---|
| public FileWriter(File file) | 创建字节输出流管道与源文件对象接通 |
| public FileWriter(String pathname) | 创建字节输出流管道与源文件路径接通 |
| public FileWriter(File file, boolean append) | 创建字节输出流管道与源文件对象接通, 可追加数据 |
| public FileWriter(String filepath, boolean append) | 创建字节输出流管道与源文件路径接通, 可追加数据 |
| 方法名称 | 说明 |
| void write(int c) | 写一个字符 |
| void write(String str) | 写一个字符串 |
| void write(String str, int off, int len) | 写一个字符串的一部分 |
| void write(char[] cbuf) | 写入一个字符数组 |
| void write(char[] cbuf, int off, int len) | 写入字符数组的一部分 |
注意事项:
- 字符输出流写出数据之后, 必须刷新流, 或者关闭流, 写出的数据才能生效
- 字符输出流都是先将要写的数据放到一个系统缓冲区中, 而并非是文件中, 等到刷新流或者关闭流的时候, 才会将缓冲区的文件写入到文件中
- 刷新流可以继续使用, 关闭流不能继续使用, 关闭流包含刷新流
| 方法名称 | 说明 |
|---|---|
| public void flush() throws IOException | 刷新流, 就是将内存中缓存的数据立即写入到文件中去生效 |
| public void close() throws IOException | 关闭流的操作, 包含刷新 |
缓冲流
对原始流进行包装, 以提高原始流读写数据的性能
字节缓冲输入流自带8kb的缓冲池, 字节缓冲输出流也自带8kb的缓冲池
字节缓冲流
字节缓冲 输入流 - BufferedInputStream
| 构造器 | 说明 |
|---|---|
| public BufferedInputStream(InputStream is) | 把低级的字节输入流包装成一个高级的缓冲字节输入流, 从而提高读数据的性能 |
字节缓冲输出流 - BufferedOutputStream
| 构造器 | 说明 |
|---|---|
| public BufferedOutputStream(InputStream is) | 把低级的字节输出流包装成一个高级的缓冲字节输出流, 从而提高写数据的性能 |
字符缓冲流
字符缓冲输入流自带8kb的缓冲池, 字符 缓冲输出流也自带8kb的缓冲池
字符缓冲输入流 - BufferedReader
| 构造器 | 说明 |
|---|---|
| public BufferedReader(Reader r) | 把低级的字符输入流包装成字符缓冲输入流管道, 从而提高字符输入流读字符数据的性能 |
| 方法 | 说明 |
| public String readLine() | 读取一行数据返回, 如果没有数据可读, 会返回null |
字符缓冲输出流 - BufferedWriter
| 构造器 | 说明 |
|---|---|
| public BufferedWriter(Writer r) | 把低级的字符输出流包装成字符缓冲输出流管道, 从而提高字符输出流写字符数据的性能 |
| 方法 | 说明 |
| public String newLine() | 换行 |
原始流, 缓冲流的性能分析
使用字节复制文件的时候, 性能都很差
使用字节数组的形式复制文件时, 如果原始流和缓冲流使用的字节数组大小都相同的时候, 性能相差不大
字节数组并非越大越好, 达到一定程度之后, 性能就无法提升了
转换流
如果代码编码和被读取的文本文件的编码是一致的, 使用字符流读取文本文件时不会出现乱码
如果代码编码和被读取的文本文件的编码是不一致的, 使用字符流读取文本文件时就会出现乱码
转换流就是为了解决不同编码时, 字符流读取/写出文本内容乱码的问题
解决思路: 先获取文件的原始字节流, 再将其按真实的字符集编码转成字符流, 就不会出现乱码情况了
字符输入转换流 - InputStreamReader
| 构造器 | 说明 |
|---|---|
| public InputStreamReader(InputStream is) | 把原始的字节输入流, 按照代码默认编码转成字符输入流(与直接使用FileReader的效果一致) |
| public InputStreamReader(InputStream is, String charset) | 把原始的字节输入流, 按照指定字符集编码转成字符输入流 |
字符输出转换流 - OutputStreamWrite
怎么控制写出去的字符使用什么字符集编码?
1. 调用String提供的getBytes方法解决
String data = "这是中文";
byte[] bytes = data.getBytes("GBK");
2.使用字符输出转换流实现
| 构造器 | 说明 |
|---|---|
| public OutputStreamWriter(OutputStream os) | 把原始的字节输出流, 按照代码默认编码转成字符输出流(与直接使用FileWriter的效果一致) |
| public OutputStreamWriter(OutputStream os, String charset) | 把原始的字节输出流, 按照指定字符集编码转成字符输出流 |
打印流
PrintStream(字节)/PringtWriter(字符)
作用: 打印流可以实现更方便, 更高效的打印数据出去, 能实现打印啥出去就是啥出去
打印流内部自带缓冲池
二者的区别:
1.打印数据的功能上是一模一样的, 都是使用方便, 性能高效(核心优势)
2.PrintStream继承自字节输出流OutputStream, 因此支持写字节数据的方法
3.PrintWriter继承自字符输出流Writer, 因此支持写字符数据出去
PrintStream
| 构造器 | 说明 |
|---|---|
| public PrintStream(OutputStream/File/String) | 打印流直接通向字节输出流/文件/文件路径 |
| public PrintStream(String fileName, Charset charset) | 可以直接指定写出去的字符编码 |
| public PrintStream(OutputStream out, boolean autoFlush) | 可以指定实现自动刷新 |
| public PrintStream(OutputStream out, boolean autoFlush, String encoding) | 可以指定实现自动刷新, 并可以指定字符的编码 |
| 方法 | 说明 |
| public void println(Xxx xx) | 打印任意类型的数据出去 |
| public void write(int/byte/byte[]一部分) | 可以支持写字节数据出去 |
PringtWriter
| 构造器 | 说明 |
|---|---|
| public PrintWriter(OutputStream/Writer/File/String) | 打印流直接通向字节输出流/文件/文件路径 |
| public PrintWriter(String fileName, Charset charset) | 可以指定写出去的字符编码 |
| public PrintWriter(OutputStream os/Writer, boolean autoFlush) | 可以指定实现自动刷新 |
| public PrintWriter(OutputStream os, boolean autoFlush, String encoding) | 可以指定实现自动刷新, 并可以指定字符的编码 |
| 方法 | 说明 |
| public void println(Xxx xx) | 打印任意类型的数据出去 |
| public void write(int/String/char[]/…) | 可以支持写字符数据出去 |
打印流的应用
输出语句的重定向
public static void main(String[] args) {
System.out.println("这是第一句话");
System.out.println("这是第二句话");
try (
PrintStream ps = new PrintStream("/Users/td/IdeaProjects/java-test/src/main/resources/fis.txt")
){
System.setOut(ps);
System.out.println("这是第三句话");
System.out.println("这是第四句话");
} catch (Exception e) {
e.printStackTrace();
}
}
数据流
数据输入流 - DataInputStream
用于读取数据输出流写出去的数据
| 构造器 | 说明 |
|---|---|
| public DataInputStream(InputStream is) | 创建新数据输入流包装基础的字节输入流 |
| 方法 | 说明 |
| public final byte readByte() throws IOException | 读取字节数据返回 |
| public final int reamInt() throws IOException | 读取int类型数据返回 |
| public final double readDouble() throws IOException | 读取double类型数据返回 |
| public final String readUTF() throws IOException | 读取字符串数据(UTF-8)返回 |
| public int readINt()/ read(byte[]) | 支持读字节数据进来 |
数据输出流 - DataOutputStream
允许把数据和其类型一并写出去
| 构造器 | 说明 |
|---|---|
| public DataOutputStream(OutputStream out) | 创建新数据输出流包装基础的字节输出流 |
| 方法 | 说明 |
| public final void writeByte(int v) throws IOException | 把byte类型的数据写入基础的字节输出流 |
| public final void writeInt(int v) throws IOException | 把int类型的数据写入基础的字节输出流 |
| public final void writeDouble(Double v) throws IOException | 把double类型的数据写入基础的字节输出流 |
| public final void writeUTF(String str) throws IOException | 把字符串数据以UTF-8编码成字节写入基础的字节输出流 |
| public void write(int/byte[]/byte[]一部分) throws IOException | 支持写字节数据出去 |
序列化流
transient 用这个标记成员变量, 成员变量将不参与序列化
如果要一次性序列化多个对象, 怎么做?
用一个ArrayList集合存储多个对象, 然后直接对集合进行序列化即可
因为ArrayList集合已经实现了序列化接口, 所以可以直接序列化
对象序列化: 把Java对象写入到文件中
对象反序列化: 从文件中把Java对象读取出来
对象字节输入流 - ObjectInputStream
可以把Java对象进行反序列化: 把存储在文件中的Java对象读入到内存中来
| 构造器 | 说明 |
|---|---|
| public ObjectInputStream(InputStream is) | 创建对象字节输入流, 包装基础的字节输入流 |
| 方法 | 说明 |
| public final Object readObject() | 把存储在文件中的Java对象读出来 |
对象字节输出流 - ObjectOutputStream
可以把Java对象进行序列化: 把Java对象存入到文件中去
对象如果参与序列化, 必须要实现序列化接口
| 构造器 | 说明 |
|---|---|
| public ObjectOutputStream(OutputStream out) | 创建对象字节输出流,包装基础的字节输出流 |
| 方法 | 说明 |
| public final void writeObject(Object o) throws IOException | 把对象写出去 |
释放资源的方式
方式一: try-catch-finally
方式二: try-with-resource
JDK7提供的更简单的资源释放方案: try-with-resource
try(定义资源1; 定义资源2;...){
可能出现异常的代码;
}catch(异常类名 变量名){
异常的处理代码;
}
该资源使用完之后, 会自动调用close(), 完成资源的释放
try()中, 只能放资源对象, 而资源对象都会实现AutoCloseable接口
资源都会有close()
按照操作对象(流的方向)划分
IO, 就是输入输出(Input/Output):
Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等
Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。
所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部,比如说文件。
文件操作算是 IO 中最典型的操作了,也是最频繁的操作。那其实你可以换个角度来思考,比如说按照 IO 的操作对象来思考,IO 就可以分类为:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。

文件
File
文件是非常重要的存储方式, 存储在计算机硬盘中
即便断电, 或者程序终止了, 存储在硬盘文件中的数据也不会丢失
File是java.io包下的类, File类的对象, 用于代表当前操作系统的文件(可以是文件, 也可以是文件夹)
注意: File类只能对文件本事进行操作, 不能读写文件里面的存储的数据
File代表文本, 而IO流用来读取数据
创建File类的对象
创建一个File类对象, 指代某个具体的文件/文件夹
File封装的仅仅是一个路径名, 这个路径名可以是存在的也可以是不存在的
| 构造器 | 说明 |
|---|---|
| public File(String pathName) | 根据文件路径创建文件对象 |
| public File(String parent, String child) | 根据父路径和子路径名字创建文件对象 |
| public File(File parent, String child) | 根据文件父路径对应的文件对象和子路径名字创建文件对象 |
路径分隔符的写法:
// 可以采用/的分隔符
File file1 = new File("src/main/resources/fis.txt");
// 也可以采用\\的分隔符
File file2 = new File("src\\main\\resources\\fis.txt");
// 也可以采用File.separator来分隔符, File.separator是系统默认的分隔符
File file3 = new File("src" + File.separator + "main" + File.separator + "resources" + File.separator + "fis.txt");
获取文件大小: File.length();
也可以通过文件夹的路径创建File对象, 用来记录文件夹的大小
通过文件夹路径创建的File对象, 使用length()的时候, 获取的是文件夹本身的大小, 而不是文件夹下所有文件的总和
注意: File对象可以指向一个不存在的路径
我们可以通过File.exists()判断文件路径是否存在, 存在则为true, 不存在则为false
我们还可以通过File.mkdir(), File.createNewFile(), 将不存在的文件创建出来
如果文件在java项目的工程模块下, 我们尽量使用相对路径, 即不带盘符的路径(默认是从工程下寻找的), 从工程名字开始的路径
(绝对路径是带盘符的)
判断文件类型, 获取文件信息(文件大小, 文件名, 修改时间)
| 方法名称 | 说明 |
|---|---|
| public boolean exists() | 判断当前文件对象, 对应的文件路径是否存在, 存在则返回true |
| public boolean isFile() | 判断当前文件对象指代的是否是文件, 是文件返回true, 反之返回false |
| public boolean isDirectory() | 判断当前文件对象指代的是否是文件夹, 是文件夹返回true, 反之返回false |
| public String getName() | 获取文件的名称(包括后缀) |
| public long length() | 获取文件的大小, 返回字节个数 |
| public long lastModified() | 获取文件的最后修改时间 |
| public String getPath() | 获取创建文件对象时, 使用的路径 |
| public String getAbsolutePath() | 获取文件的绝对路径 |
创建文件, 删除文件
| 方法名称 | 说明 |
|---|---|
| public boolean createNewFile() | 创建一个新文件(文件内容为空), 创建成功返回true, 反之 |
| public boolean mkdir() | 用于创建文件夹, 注意: 只能创建一级文件夹 |
| public boolean mkdirs() | 用于创建文件夹, 注意: 可以创建多级文件夹 |
| public boolean delete() | 删除文件, 或者空文件, 注意: 不能删除非空文件夹(删除的文件不会进入回收站) |
遍历文件夹
| 方法名称 | 说明 |
|---|---|
| public String[] list() | 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回 |
| public File[] listFiles() | 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回 |
使用ListFiles方法时的注意事项:
1.当主调是文件, 或者路径不存在时, 返回null
2.当主调是空文件夹时, 返回一个长度为0的数组
3.当主调是一个有内容的文件夹时, 将里面所有一级文件和文件夹的路径放在File数组中返回
4.当主调是一个文件夹, 且里面有隐藏文件时, 将里面所有文件和文件夹的路径放在File数组中返回, 包含隐藏文件
5.当主调是一个文件夹, 但是没有权限访问该文件夹时, 返回null
方法递归
递归是一种算法, 在程序设计语言中广泛使用
从形式上说: 方法调用自身的形式称为方法递归(recursion)
递归的形式
直接递归: 方法自己调用自己
间接递归: 方法调用其他方法, 其他方法又回调方法自己
递归如果没有控制好终止, 会出现递归死循环, 导致栈内存溢出错误
字符集
美国人发明了ASCII(美国信息交换标准代码)字符集, 包括英文和字符, 使用1个字节存储1个字符, 首位是0, 一共128个字符
GBK(汉字内码扩展规范, 国标), 汉字编码字符集, 包含两万多个汉字等字符, GBK中一个中文字符编码成两个字节的形式存储, 并且由于兼容了ASCII字符集, 所以GBK规定, 汉字的第一个字节的第一位必须是1
Unicode字符集(统一码, 也叫万国码), 是由国际组织制定的, 可以容纳世界上所有的文字, 符号的字符集
UTF-32, 使用4个字节表示一个字符, 占存储空间, 通信效率变低
UTF-8, 是Unicode字符集的一种编码方案, 采取可变长编码方案, 共分为四个长度区: 1个字节, 2个字节, 3个字节, 4个字节
英文字符, 数字等只占一个字节(兼容标准ASCII编码, 汉字字符占用3个字节)
UTF-8编码方式(二进制)
0xxxxxxx (ASCII码)
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
ASCII字符集: 只有英文, 数字, 符号等, 占1个字节
GBK字符集: 汉字占2个字节, 英文, 数字占1个字节
UTF-8字符集: 汉字占3个字节, 英文, 数字占1个字节
注意1: 字符编码时使用的字符集, 和解码时使用的字符集必须一致, 否则会出现乱码
注意2: 英文, 数字一般不会乱码, 因为很多字符集都兼容了ASCII编码
字符集编码解码
Java代码完成对字符的编码
| String提供如下方法 | 说明 |
|---|---|
| byte[] getBytes() | 使用平台的默认字符集将该String编码为一系列字节, 将结果存储到新的字节数组中 |
| byte[] getBytes(String charsetName) | 使用指定的字符集将该String编码为一系列字节, 将结果存储到新的字节数组中 |
Java代码完成对字符的解码
| String提供如下方法 | 说明 |
|---|---|
| String(byte[] bytes) | 通过使用平台的默认字符集解码指定的字节数组来构造新的String |
| String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来构造新的String |
IO框架
框架
框架是为了解决某类问题, 编写的一套类, 接口等, 可以理解成一个半成品, 大多框架都是第三方研发的
好处就是在框架的基础上开发, 可以得到优秀的软件架构, 并能提高开发效率
框架的形式一般是把类, 接口等编译成class形式, 再压缩成一个.jar结尾的文件发行出去
IO框架
IO框架是封装了Java提供的对文件, 数据进行操作的代码, 对外提供了更简单的方式来对文件进行操作, 对数据进行读写等
Common-io框架
| FileUtils类提供的部分方法 | 说明 |
|---|---|
| public static void copyFile(final File srcFile, final File destFile) | 复制文件 |
| public static void copyDirectory(final File srcDir, final File destDir) | 复制文件夹 |
| public static void deleteDirectory(final File directory) | 删除文件夹 |
| public static String readFileToString(final File file, final Charset charsetName) | 读数据 |
| public static void writeStringToFile(final File file, final String data, final Charset charset, final boolean append) | 写数据 |
| IOUtils类提供的部分方法 | 说明 |
| public static int copy(final InputStream inputStream, final OutputStream outputStream) | 复制文件 |
| public static int copy(final Reader reader, final Writer writer) | 复制文件 |
| public static void write(final String data, final OutputStream output, final Charset charset) | 写数据 |
网络编程
概述
计算机网络
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备, 通过通信线路连接起来, 在网络操作系统, 网络管理软件及网络通信协议的管理和协调下, 实现资源共享和信息传递的计算机系统
网络编程的目的
传播交流信息, 数据交换, 通信
想要达到这个目的需要什么
1.如何准确的定位到网络中的一台主机?
IP:端口, 定位到这个计算机上的某个资源
2.找到这个主机之后, 该如何传递资源?
javaweb: 网页编程, B/S
网络编程: TCP/IP协议, C/S
网络通信的要素
如何实现网络的通信?
1.通信双方的地址: ip:端口
中国的ip分布情况
2.规则: 网络通信协议

网络编程针对的是传输层, 对应的是TCP, UDP协议
IP
java.net.InetAddress 此类表示Internet协议(IP)地址
static InetAddress[] getAllByName(String host) 给定主机的名称, 根据系统上配置的名称服务返回其IP地址数组
作用: 唯一定位一台网络上的计算机
127.0.0.1: 本机的ip地址, localhost
IP地址的分类
- IP地址分类(IPV4/IPV6)
- 公网(互联网)/私网(局域网)
- ABCD类地址
- 192.168.xx.xx 专门给组织内部使用
域名: 解决记忆IP的问题
public static void main(String[] args) {
try {
// 获取本机ip
InetAddress inetAddress1 = InetAddress.getByName("127.0.0.1");
System.out.println(inetAddress1);
InetAddress inetAddress2 = InetAddress.getLocalHost();
System.out.println(inetAddress2);
InetAddress inetAddress3 = InetAddress.getByName("localhost");
System.out.println(inetAddress3);
// 获取网络地址
InetAddress inetAddress = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress);
System.out.println("==================");
//常用方法
//此对象的原始IP地址
System.out.println(inetAddress.getAddress());
//字符串格式的原始IP地址(IP)
System.out.println(inetAddress.getHostAddress());
//获取此IP地址的完全限定域名
System.out.println(inetAddress.getCanonicalHostName());
//获取此IP地址的主机名(域名)
System.out.println(inetAddress.getHostName());
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
----结果----
/127.0.0.1
MacBookAir/192.168.31.230
localhost/127.0.0.1
www.baidu.com/223.109.82.212
==================
[B@3cd1a2f1
223.109.82.212
223.109.82.212
www.baidu.com
端口
端口表示计算机上的一个程序的进程
- 不同的进程有不同的端口号, 用来区分不同的软件
- 被规定 0~65536
- TCP和UDP协议, 分别可以使用0~65535个端口号, 不同的协议下端口号可以重复, 相同的协议下端口号不允许重复
- 端口分类
- 公有端口: 0~1023
- HTTP : 80
- HTTPS : 443
- FTP : 21
- Telent : 23
- 程序注册端口 : 1024~49151, 分配给用户或者程序
- Tomcat: 8080
- MySQL: 3306
- Oracle: 1521
- 动态, 私有: 49152~65535
- 公有端口: 0~1023
//查看所有的端口
netstat -ano
//查看指定的端口
netstat -ano | findstr "5900"
//查看指定端口的进程
tasklist | findstr "8697"
ipconfig
public static void main(String[] args) {
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
System.out.println(inetSocketAddress);
InetSocketAddress socketAddress = new InetSocketAddress("localhost", 8080);
System.out.println(socketAddress);
System.out.println(inetSocketAddress.getAddress());
//地址
System.out.println(inetSocketAddress.getHostName());
// 端口
System.out.println(inetSocketAddress.getPort());
System.out.println(inetSocketAddress.getHostString());
}
----结果----
/127.0.0.1:8080
localhost/127.0.0.1:8080
/127.0.0.1
localhost
8080
localhost
通信协议
待补充
TCP
客户端
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
try {
// 1. 要知道服务器的ip:port
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
int port = 9999;
// 2. 创建一个socket连接
socket = new Socket(inetAddress, port);
//3. 发送消息, 使用IO流
os = socket.getOutputStream();
os.write("发送一条信息".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (Objects.nonNull(os)) {
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (Objects.nonNull(socket)) {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
服务端
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket accept = null;
InputStream is = null;
ByteArrayOutputStream bos = null;
try {
// 1. 先有一个服务器地址
serverSocket = new ServerSocket(9999);
// 2. 等待客户端连接
accept = serverSocket.accept();
// 3. 读取客户端的消息
is = accept.getInputStream();
// 管道流
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println(bos.toString());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (Objects.nonNull(bos)) {
try {
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (Objects.nonNull(is)) {
try {
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (Objects.nonNull(accept)) {
try {
accept.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (Objects.nonNull(serverSocket)) {
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
文件上传
客户端
public static void main(String[] args) throws Exception {
//1. 创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("localhost"), 9000);
//2. 创建一个输出流
OutputStream outputStream = socket.getOutputStream();
//3. 读取文件
FileInputStream fis = new FileInputStream(new File("/Users/td/IdeaProjects/java-test/src/main/java/文件路径.txt"));
//4. 写出文件
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
// 通知服务器, 已经发送完毕
socket.shutdownOutput();
// 确定服务器都接收完毕, 才断开连接
InputStream is = socket.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while ((len2 = is.read(buffer2)) != -1) {
byteArrayOutputStream.write(buffer2, 0, len2);
}
System.out.println(byteArrayOutputStream.toString());
//5. 关闭资源
byteArrayOutputStream.close();
is.close();
fis.close();
outputStream.close();
socket.close();
}
服务端
public static void main(String[] args) throws IOException {
//1. 创建服务
ServerSocket serverSocket = new ServerSocket(9000);
//2. 监听客户端连接
Socket accept = serverSocket.accept();
//3.获取输入流
InputStream is = accept.getInputStream();
//4. 文件传输
FileOutputStream fos = new FileOutputStream(new File("/Users/td/IdeaProjects/java-test/src/main/java/复制到哪个位置.txt"));
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
// 通知客户端, 已经接收完毕了
OutputStream os = accept.getOutputStream();
os.write("已经接收完毕, 可以断开连接".getBytes());
// 关闭资源
os.close();
fos.close();
is.close();
accept.close();
serverSocket.close();
}
Tomcat
服务端
- 自定义的服务端 B
- Tomcat B
客户端
- 自定义的服务端 C
- 浏览器 B
UDP
发送消息
发送端
public static void main(String[] args) throws Exception {
// UDP不需要连接服务器
//1. 建立一个Socket
DatagramSocket datagramSocket = new DatagramSocket();
//2. 建个包
String msg = "hello, 服务器!";
InetAddress inetAddress = InetAddress.getByName("localhost");
int port = 9999;
DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.length(), inetAddress, port);
//3. 发送包
datagramSocket.send(packet);
//关闭流
datagramSocket.close();
}
接收端
public static void main(String[] args) throws Exception {
// 需要等待客户端的连接
// 开放端口
DatagramSocket datagramSocket = new DatagramSocket(9999);
// 接收数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
// 阻塞接收
datagramSocket.receive(packet);
// 从哪里发送过来的
System.out.println(packet.getAddress().getHostAddress());
System.out.println(new String(packet.getData(), 0, packet.getLength()));
datagramSocket.close();
}
聊天实现
客户端1
public static void main(String[] args) throws Exception{
DatagramSocket socket = new DatagramSocket(9999);
// 准备数据, 控制台读取System.in
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String data = reader.readLine();
byte[] datas = new byte[1024];
DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 8888));
socket.send(packet);
if (data.equals("bye")) {
break;
}
}
socket.close();
}
客户端2
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(8888);
while (true) {
// 准备接收数据
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
socket.receive(packet);
// 断开连接
byte[] data = packet.getData();
String s = new String(data, 0, data.length);
System.out.println(s);
if (s.equals("bye")) {
break;
}
}
socket.close();
}
在线咨询: 既是发送方, 也是接收方
TalkSend
public class TalkSend implements Runnable{
DatagramSocket socket = null;
BufferedReader reader = null;
private int fromPort;
private String toIP;
private int toPort;
public TalkSend(int fromPort, String toIP, int toPort) {
this.fromPort = fromPort;
this.toIP = toIP;
this.toPort = toPort;
try {
socket = new DatagramSocket(fromPort);
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (true) {
try {
String data = reader.readLine();
byte[] datas = new byte[1024];
DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("localhost", 8888));
socket.send(packet);
if (data.equals("bye")) {
break;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
socket.close();
}
}
TalkReceive
public class TalkReceive implements Runnable{
DatagramSocket socket = null;
private int port;
private String msgFrom;
public TalkReceive(int port, String msgFrom) {
this.port = port;
this.msgFrom = msgFrom;
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (true) {
try {
// 准备接收数据
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
socket.receive(packet);
// 断开连接
byte[] data = packet.getData();
String receiveData = new String(data, 0, data.length);
System.out.println(msgFrom + ": " + receiveData);
if (receiveData.equals("bye")) {
break;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
socket.close();
}
}
TalkStudent
public class TalkStudent {
public static void main(String[] args) {
new Thread(new TalkSend(7777, "localhost", 9999)).start();
new Thread(new TalkReceive(8888, "老师")).start();
}
}
TalkTeacher
public class TalkTeacher {
public static void main(String[] args) {
new Thread(new TalkSend(5555, "localhost", 8888)).start();
new Thread(new TalkReceive(9999, "学生")).start();
}
}
URL
URL, 统一资源定位符
http://www.baidu.com/ 协议://IP:PORT/项目名/资源
public static void main(String[] args) throws MalformedURLException {
URL url = new URL("http://localhost:8080/helloWord/index/html?username=xxx&password=***");
// 获取协议
System.out.println(url.getProtocol());
// 主机
System.out.println(url.getHost());
// 端口
System.out.println(url.getPort());
// 文件
System.out.println(url.getPath());
// 全路径
System.out.println(url.getFile());
// 参数
System.out.println(url.getQuery());
}
----结果----
http
localhost
8080
/helloWord/index/html
/helloWord/index/html?username=xxx&password=***
username=xxx&password=***
DNS域名解析
通过URL下载文件
public static void main(String[] args) throws IOException {
// 下载地址
URL url = new URL("网络上的资源地址");
// 连接上这个资源
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream inputStream = urlConnection.getInputStream();
FileOutputStream fos = new FileOutputStream("存储到本机的文件路径");
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
inputStream.close();
urlConnection.disconnect();
}
多线程
Java.Thread
线程简介
任务
多任务: 边吃饭边玩手机, 边开车边打点滴 …
现实中有很多这种例子, 看起来是多个任务都在做, 其实本质上我们的大脑在同一时间依旧只做了一件事情
进程Process
在操作系统中运行的程序就是进程, 比如QQ, 播放器, 游戏, IDE…
一个进程可以有很多线程, 比如在视频中同时听声音, 看图像, 看弹幕等等
进程和线程
- 程序是指令和数据的有序集合, 其本身没有任何运行的含义, 是一个静态的概念
- 而进程则是执行程序的一次执行过程, 它是一个动态的概念. 是系统资源分配的单位
- 通常在一个进程中可以包含多个线程, 一个进程中至少有一个线程, 否则就没有存在的意义
- 线程是CPU调度和执行的单位
- 注意:很多多线程都是模拟出来的, 真正的多线程是指有多个cpu, 即多核, 如服务器. 如果是模拟出来的多线程, 即在一个cpu的情况下, 在同一个时间点, cpu只能执行一个代码, 因为切换的很快, 所以就有同时执行的错觉
多线程
原来道路窄, 车辆之间无法同时经过, 效率极低
为了提高道路的效率, 能够充分利用道路, 可以加多个车道, 就不会造成道路阻塞的问题
多条车道同时有车经过, 这就是多线程
核心概念
- 线程就是独立的执行路径
- 在程序运行时, 即使没有自己创建线程, 后台也会有多个线程, 如主线程, gc线程
- main()称之为主线程, 为系统的入口, 用于执行整个程序
- 在一个进程中, 如果开辟了多个线程, 线程的运行由调度器安排调度, 调度器是与操作系统紧密相关的, 先后顺序是不能人为进行干预的
- 对同一份资源进行操作时, 会存在资源抢夺的问题, 需要加入并发控制
- 线程会带来额外的开销, 如cpu调度时间, 并发控制开销
- 每个线程在自己的工作内存交互, 内存控制不当会造成数据不一致
线程实现(重点)
Thread, Runnable, Callable
三种创建方式
继承Thread类(重点)
Thread也实现了Runnable接口
每个线程都有优先权, 具有较高优先级的线程优先于优先级较低的线程执行, 每个线程可能会也可能不会被标记成守护线程.
当在某个线程中运行的代码创建一个新的Thread对象时, 新线程的优先级最初设置为等于创建线程的优先级, 并且当且仅当创建线程是守护线程的时候才是守护线程
线程不一定立即执行, 需要等待cpu调度
创建线程的方法
- 自定义线程类继承Thread类
- 重写run(), 编写线程执行体
- 创建线程对象, 调用start()启动线程
/**
* 创建线程的方式一: 继承Thread类, 重写Thread类, 重写run(), 调用start()开启线程
* 注意: 线程开启并不一定立即执行, 有cpu调度执行
*/
public class TestThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我是run()------" + i);
}
}
public static void main(String[] args) {
TestThread1 testThread1 = new TestThread1();
testThread1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("我这个是主线程-----" + i);
}
}
}
网图下载
/**
* 网图下载
*/
public class TestThread2 extends Thread {
// 网络图片地址
private String url;
// 保存的文件名
private String name;
public TestThread2 (String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownload webDownload = new WebDownload();
webDownload.downloadr(url, name);
System.out.println("下载了文件名为:" + name);
}
public static void main(String[] args) {
TestThread2 thread1 = new TestThread2("文件路径1", "下载之后的文件名1");
TestThread2 thread2 = new TestThread2("文件路径2", "下载之后的文件名2");
TestThread2 thread3 = new TestThread2("文件路径3", "下载之后的文件名3");
thread1.start();
thread2.start();
thread3.start();
}
}
class WebDownload {
public void downloadr(String url, String name) {
try {
FildUtils.copyURLToFild(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常, 下载文件失败, 文件名为:" + name);
}
}
}
实现Runnable接口(重点)
创建线程的方法
- 定义MyRunnable类, 实现Runnable接口
- 实现run(), 编写线程执行体
- 创建线程对象, 调用start()方法启动线程
推荐使用Runnable对象, 因为Java单继承具有局限性
/**
* 创建线程的方式二: 实现Runnable接口, 重写run(), 新建Thread接口, 将线程类对象丢入其中调用start()
*/
public class TestThread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("我是run()------" + i);
}
}
public static void main(String[] args) {
TestThread3 testThread3 = new TestThread3();
new Thread(testThread3).start();
for (int i = 0; i < 1000; i++) {
System.out.println("我这个是主线程-----" + i);
}
}
}
两种方式的总结
- 继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程, 子类对象.start()
- 不建议使用, 避免OOP单继承局限性
- 实现Runnable接口
- 实现接口Runnable具有多线程能力
- 启动线程: 传入目标对象 + Thread对象,start()
- 推荐使用, 避免单继承局限性, 灵活方便, 方便同一对象被多个线程使用
并发问题
/**
* 多个线程同时操作同一个资源对象
* 买火车票
* 发现问题: 多个线程同时操作同一个资源对象的时候, 会出现线程不安全的问题, 数据紊乱
*/
public class TestThread4 implements Runnable{
// 总票数
private Integer ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
// 模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票");
}
}
public static void main(String[] args) {
TestThread4 thread = new TestThread4();
new Thread(thread, "小明").start();
new Thread(thread, "老师").start();
new Thread(thread, "黄牛党").start();
}
}
龟兔赛跑
规则:
1.首先需要有赛道距离, 并且龟兔要离终点越来越近
2.判断比赛是否结束
3.打印出胜利者
4.兔子需要睡觉, 使用延时模拟兔子睡觉
代码实现
/**
* 龟兔赛跑
*/
public class Race implements Runnable{
// 胜利者
private String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
// 如果是兔子, 需要睡觉
if (Thread.currentThread().getName().equals("兔子") && i%10 == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 判断比赛是否结束
boolean flag = gameOver(i);
if (flag) {
break;
}
System.out.println(Thread.currentThread().getName() + "----->跑了" + i + "步");
}
}
// 判断比赛是否结束
private boolean gameOver(int step) {
if (winner != null) {
return true;
}
if (step >= 100) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}
实现Callable接口(了解)
创建线程的方法
- 1.实现Callable接口, 需要返回值类型
- 2.重写call方法, 需要抛出异常
- 3.创建目标对象
- 4.创建执行服务: ExecutorService service = Executors.newFixedThreadPool(1);
- 5.提交执行: Future result1 = service.submit(t1);
- 6.获取结果: boolean r1 = result1.get();
- 7.关闭服务: service.shutdownNow();
具体代码实现 - 改造网图下载代码
/**
* 线程创建方式三: 实现Callable接口
* 好处:可以定义返回值, 可以抛出异常
*/
public class TestThread5 implements Callable<Boolean> {
// 网络图片地址
private String url;
// 保存的文件名
private String name;
public TestThread5 (String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() {
WebDownload webDownload = new WebDownload();
webDownload.downloadr(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestThread5 thread1 = new TestThread5("文件路径1", "下载之后的文件名1");
TestThread5 thread2 = new TestThread5("文件路径2", "下载之后的文件名2");
TestThread5 thread3 = new TestThread5("文件路径3", "下载之后的文件名3");
// 创建执行服务:
ExecutorService service = Executors.newFixedThreadPool(1);
// 提交执行
Future<Boolean> result1 = service.submit(thread1);
Future<Boolean> result2 = service.submit(thread2);
Future<Boolean> result3 = service.submit(thread3);
// 获取结果
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();
// 关闭服务
service.shutdownNow();
}
}
class WebDownload {
public void downloadr(String url, String name) {
try {
FildUtils.copyURLToFild(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常, 下载文件失败, 文件名为:" + name);
}
}
}
静态代理模式
以结婚举例:
你: 真实角色
婚庆公司: 代理你, 帮你处理结婚的事情
结婚: 实现结婚接口即可
具体代码实现
/**
* 静态代理模式总结:
* 真实对象和代理对象都要实现同一个接口
* 代理对象必须要代理真实对象
* 好处:
* 代理对象可以做很多真实对象做不了的事情
* 真实对象可以专注做自己的事情
*/
public class StaticProxy {
public static void main(String[] args) {
WeddingCompany weddingCompany = new WeddingCompany(new Man());
weddingCompany.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
// 真实角色
class Man implements Marry {
@Override
public void HappyMarry() {
System.out.println("真实对象结婚了");
}
}
// 代理角色
class WeddingCompany implements Marry {
// 需要代理的真是对象
private Man target;
public WeddingCompany(Man target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
private void before() {
System.out.println("结婚之前, 布置会场");
}
private void after() {
System.out.println("结婚之后, 收尾款");
}
}
Lambda表达式
/Users/td/Downloads/尚硅谷java培训/函数式编程
线程状态


五大状态
- 创建状态
- Thread thread = new Thread();
- 线程对象一旦创建就进入到了新生状态
- 当调用start()的时候, 线程立即进入就绪状态, 但并不意味着立即调度执行
- 就绪状态
- 就绪状态下, 如果cpu调度此线程, 该线程就进入运行状态
- 运行状态
- 进入运行状态, 线程才真正执行线程体的代码块
- 阻塞状态
- 当调用sleep(), wait() 或者同步锁定时, 线程进入阻塞状态, 就是代码不往下执行, 阻塞事件解除后, 重新进入就绪状态, 等待cou调度执行
- 死亡状态
- 线程中断或者结束, 一旦进入死亡状态, 就不能再次启动
线程方法
| 方法 | 说明 |
|---|---|
| setPriority(int newPriority) | 更改线程的优先级 |
| static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
| void join() | 等待该线程终止 |
| static void yield() | 暂停当前正在执行的线程对象, 并执行其他线程 |
| void interrupt() | 中断线程, 尽量不使用这种方式 |
| boolean isAlive() | 测试线程是否处于活动状态 |
停止线程
- 不推荐使用JDK提供的stop(), destroy()
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量, 当flag=false, 则终止线程运行
具体代码实现
/**
* 线程停止
*/
public class ThreadStop implements Runnable{
// 设置标志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag) {
System.out.println("线程正在执行....i = " + i++);
}
}
public void stop(){
flag = false;
}
public static void main(String[] args) {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop).start();
for (int i = 0; i < 1000; i++) {
if (i == 900) {
threadStop.stop();
System.out.println("线程该停止了");
}
System.out.println("主线程在运行....i = " + i);
}
}
}
线程休眠
- sleep(时间) 指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时, 倒计时等
- 每一个对象都有一个锁, sleep不会释放锁
具体代码实现
/**
* 线程休眠
*/
public class ThreadSleep {
public static void main(String[] args) {
try {
// tenDown();
now();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 打印当前时间
public static void now() throws InterruptedException {
// 获取系统时间
Date now = new Date(System.currentTimeMillis());
while (true) {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(now));
Thread.sleep(1000);
// 更新时间
now = new Date(System.currentTimeMillis());
}
}
// 模拟倒计时
public static void tenDown() throws InterruptedException {
int i = 10;
while (true) {
System.out.println(i--);
Thread.sleep(1000);
if (i == 0) {
break;
}
}
}
}
线程礼让 - yield
- 礼让线程, 让当前正在执行的线程暂停, 但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度, 礼让不一定成功, 看CPU心情
具体代码实现
/**
* 线程礼让
*/
public class ThreadYield {
static class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程正在执行");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "线程停止执行");
}
}
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield, "A").start();
new Thread(myYield, "B").start();
}
}
线程阻塞 - join
- join合并线程, 待此线程执行完成后, 再执行其他线程, 其他线程阻塞
- 可以想象成插队
具体代码实现
/**
* 线程join, 就相当于插队
*/
public class ThreadJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 500; i++) {
System.out.println("线程VIP来了, i="+i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
Thread thread = new Thread(threadJoin);
thread.start();
for (int i = 0; i < 2000; i++) {
if (i==200) {
thread.join();
}
System.out.println("主线程在执行, i = " + i);
}
}
}
线程状态观测
-
线程状态 Thread.State
线程可以处于以下状态之一:
-
NEW
尚未启动的线程处于此状态
-
RUNNABLE
在Java虚拟机中执行的线程处于此状态
-
BLOCKED
被阻塞等待监视器锁定的线程处于此状态
-
WAITING
正在等待另一个线程执行特定动作的线程处于此状态
-
TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
-
TERMINATED
已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态, 这些状态是不反映任何操作系统的虚拟机状态
具体代码实现
/** *观测线程状态 */ public class ThreadState { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("------------"); }); // 观察线程状态 Thread.State state = thread.getState(); //New System.out.println(state); // 运行状态 thread.start(); state = thread.getState(); // run System.out.println(state); // 只有线程不死亡, 就一直执行 while (state != Thread.State.TERMINATED) { Thread.sleep(100); state = thread.getState(); System.out.println(state); } } } -
线程优先级
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程, 线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级用数字表示, 范围从1~10
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORH_PRIORITY = 5;
- 使用以下方式改变或获取优先级
- getPriority()
- setPriority(int xxx)
- 设置优先级建议在start()调度之前
- 优先级低只是意味着获得调度的概率低, 并不是优先级低就不会被调用了, 这都是看CPU的调度
具体代码实现
/**
* 线程优先级
*/
public class ThreadPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority());
MyPriority priority = new MyPriority();
Thread t1 = new Thread(priority);
Thread t2 = new Thread(priority);
Thread t3 = new Thread(priority);
Thread t4 = new Thread(priority);
Thread t5 = new Thread(priority);
Thread t6 = new Thread(priority);
//先设置优先级, 在启动
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
// t5.setPriority(-1);
// t5.start();
//
// t6.setPriority(11);
// t6.start();
}
}
class MyPriority implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---->" + Thread.currentThread().getPriority());
}
}
守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕, 比如后台记录操作日志, 监控内存, 垃圾回收等待…
具体代码实现
/**
* 守护线程
*/
public class ThreadDaemon {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread thread = new Thread(god);
// 将某个线程设置成守护线程, 默认新建的线程都是用户线程
thread.setDaemon(true);
thread.start();
new Thread(you).start();
}
}
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("上帝一直都在");
}
}
}
// 用户线程
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("你还活着, 你活了" + i + "天");
}
System.out.println("你死了");
}
}
线程同步(重点)
多个线程操作同一个资源
并发
- 同一个对象被多个线程同时操作
- 上万个人同时抢100张票
- 两个银行同时取钱
线程同步
- 现实生活中, 我们会遇到同一个资源, 多个人都想使用的问题, 比如食堂排队打饭, 每个人都想吃饭, 最好的解决办法就是排队(队列)
- 处理多线程问题时, 多个线程访问同一个对象, 并且某些线程还想修改这个对象, 这个时候我们就需要线程同步
- 线程同步其实就是一种等待机制, 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面线程使用完毕, 下一个线程再使用
- 队列 + 锁, 才能保证线程同步的安全性
- 由于同一进程的多个线程共享同一块存储空间, 在带来方便的同时, 也带来了访问冲突问题, 为了保证数据在方法中被访问时的正确性, 在访问时加入锁机制 synchronized, 当一个线程获得对象的排它锁, 独占资源, 其他线程必须等待, 使用后释放锁即可. 存在以下问题:
- 一个线程持有锁, 会导致其他所有需要此锁的线程挂起
- 在多线程竞争下, 加锁, 释放锁会导致比较多的上下文切换 和 调度延时, 引起性能问题
- 如果一个优先级高的线程, 等待优先级低的线程释放锁, 会导致优先级倒置, 引起性能问题
不安全的案例
案例一: 不安全的买票
package org.example.javatest.sync;
/**
* 不安全的买票
*/
public class NosafeBuyTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "A").start();
new Thread(ticket, "B").start();
new Thread(ticket, "C").start();
}
}
class Ticket implements Runnable {
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void buy() throws InterruptedException {
// 判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟延时
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票");
}
}
案例二: 不安全的取钱
/**
* 不安全的取款
*/
public class unsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "you");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");
you.start();
girlFriend.start();
}
}
//银行: 模拟取钱
class Drawing extends Thread {
Account account;
// 取了多少钱
int drawingMoney;
// 手里的钱
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
// 取钱
@Override
public void run() {
// 判断有咩有钱
if (account.money - drawingMoney <= 0) {
System.out.println(Thread.currentThread().getName() + "钱不够, 取不了");
return;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 卡内余额
account.money = account.money - drawingMoney;
//手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
// 账户
class Account {
// 余额
int money;
// 卡名
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
案例三: 不安全的集合
/**
* 线程不安全的集合
*/
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size());
}
}
同步方法
- 由于我们可以通过private关键字来保证数据对象只能被方法访问, 所以我们只需要针对方法提出一套机制, 这套机制就是synchronized关键字, 它包括两种用法: synchronized方法 和synchronized块
- 同步方法: public synchronized void method(int args) {}
- synchronized方法控制对 “对象” 的访问, 每个对象对应一把锁, 每个synchronized方法都必须获得调用该方法的对象的锁才能执行, 否则线程会阻塞, 方法一旦执行, 就独占该锁, 直到该方法返回才能释放锁, 后面被阻塞的线程才能获得这个锁, 继续执行此方法
- 缺陷: 若将一个大的方法申明为synchronized 将会影响效率
同步块
- 同步块: synchronized(Obj obj){}
- Obj称之为同步监视器
- Obj可以是任何对象, 但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器, 因为同步方法的同步监视器就是this, 就是这个对象本身, 或者是class
- 同步监视器的执行过程
- 第一个线程访问, 锁定同步监视器, 执行其中代码
- 第二个线程访问, 发现同步监视器被锁定, 无法访问
- 第一个线程访问完毕, 解锁同步监视器
- 第二个线程访问, 发现同步监视器没有锁, 然后锁定并访问
修改三个案例, 变成安全的
案例一: 安全的买票
package org.example.javatest.sync;
/**
* 安全的买票
*/
public class NosafeBuyTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "A").start();
new Thread(ticket, "B").start();
new Thread(ticket, "C").start();
}
}
class Ticket implements Runnable {
private int ticketNums = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized void buy() throws InterruptedException {
// 判断是否有票
if (ticketNums <= 0) {
flag = false;
return;
}
// 模拟延时
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNums-- + "张票");
}
}
案例二: 安全的取钱
package org.example.javatest.sync;
/**
* 安全的取款
*/
public class unsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "you");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");
you.start();
girlFriend.start();
}
}
//银行: 模拟取钱
class Drawing extends Thread {
Account account;
// 取了多少钱
int drawingMoney;
// 手里的钱
int nowMoney;
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
// 取钱
@Override
public void run() {
synchronized (account) {
// 判断有咩有钱
if (account.money - drawingMoney <= 0) {
System.out.println(Thread.currentThread().getName() + "钱不够, 取不了");
return;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 卡内余额
account.money = account.money - drawingMoney;
//手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name + "余额为:" + account.money);
System.out.println(this.getName() + "手里的钱:" + nowMoney);
}
}
}
// 账户
class Account {
// 余额
int money;
// 卡名
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
案例三: 安全的集合
package org.example.javatest.sync;
import java.util.ArrayList;
import java.util.List;
/**
* 线程安全的集合
*/
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
}).start();
}
System.out.println(list.size());
}
}
JUC.CopyOnWriteArrayList
package org.example.javatest.juc;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author linxiao.cai
* @company td
* @create 2025-05-20 21:53
*/
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(list.size());
}
}
死锁
- 多个线程各自占用一些共享资源, 并且互相等待其他线程占有的资源才能运行, 而导致两个或者多个线程都在等待对方释放资源, 都停止执行的情形.
- 某个同步块同时拥有两个以上对象的锁时, 就有可能发生死锁的问题
具体代码实现
package org.example.javatest.lock;
/**
* 死锁: 多个线程互相抱着对方需要的资源, 然后形成僵持
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0, "邓邓");
MakeUp girl2 = new MakeUp(1, "白雪公主");
girl1.start();
girl2.start();
}
}
// 口红
class Lipstick {
}
// 镜子
class Mirror {
}
class MakeUp extends Thread {
// 使用static修饰, 保证资源只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void makeup() throws InterruptedException {
if (choice ==0) {
synchronized (lipstick) { // 获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
解决方式
package org.example.javatest.lock;
/**
* 死锁: 多个线程互相抱着对方需要的资源, 然后形成僵持
*/
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0, "邓邓");
MakeUp girl2 = new MakeUp(1, "白雪公主");
girl1.start();
girl2.start();
}
}
// 口红
class Lipstick {
}
// 镜子
class Mirror {
}
class MakeUp extends Thread {
// 使用static修饰, 保证资源只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void makeup() throws InterruptedException {
if (choice ==0) {
synchronized (lipstick) { // 获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
}
} else {
synchronized (mirror) { // 获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
死锁避免的方法
- 产生死锁的四个必要条件:
- 互斥条件: 一个资源每次只能被一个进程使用
- 请求与保持条件: 一个进程因请求资源而阻塞时, 对已获得的资源保持不放
- 不剥夺条件: 进程已获得的资源, 在未使用完之前, 不能强行剥夺
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
- 上面列出的死锁的四个必要条件, 我们只需要破坏其中的任意一个或多个条件, 就可以避免死锁产生
Lock锁
- 从JDK5.0开始, Java提供了更强大的线程同步机制 - 通过显式定义同步锁对象来实现同步. 同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具. 锁提供了对共享资源的独占访问, 每次只能有一个线程对Lock对象加锁, 线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类(可重入锁)实现了Lock, 它拥有与synchronized相同的并发性和内存语义, 在实现线程安全的控制中, 比较常用的是ReentranrLock, 可以显式加锁, 释放锁
具体代码实现
package org.example.javatest.lock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable {
private static final Logger log = LoggerFactory.getLogger(TestLock2.class);
int ticketNums = 10;
// 定义锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticketNums > 0) {
Thread.sleep(1000);
System.out.println(ticketNums--);
} else{
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
具体格式
class A {
// 定义锁
private final ReentrantLock lock = new ReentrantLock();
public void method() {
try {
lock.lock();
//保证线程安全的代码
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
//如果同步代码有异常, 要将unlock()写入到finally语句块
}
}
}
}
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁), synchronized是隐式锁, 出了作用域自动释放
- Lock只有代码块锁, synchronized有代码块锁和方法锁
- 使用Lock锁, JVM将花费较少的时间来调度线程, 性能更好, 并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序
- Lock > 同步代码块(已经进入方法体, 分配了相应的资源) > 同步方法(在方法体之外)
线程通信问题
应用场景: 生产者和消费者问题
- 假设仓库中只能存放一件产品, 生产者将生产出来的产品放进仓库, 消费者将仓库中的产品取走消费
- 如果仓库中没有产品, 则生产者将产品放入仓库, 否则停止生产并等待, 直到仓库中的产品被消费者取走为止
- 如果仓库中放有产品, 则消费者可以将产品取走消费, 否则停止消费并等待, 直到仓库中再次放入产品为止
分析
这是一个线程同步问题, 生产者和消费者共享同一个资源, 并且生产者和消费者之间互相依赖, 互为条件
- 对于生产者, 没有生产产品之前, 要通知消费者等待, 而生产了产品之后, 有需要马上通知消费者消费
- 对于消费者, 在消费之后, 要通知生产者已经结束消费, 需要生产新的产品以供消费
- 在生产者和消费者问题中, 仅有synchronized是不够的
- synchronized 可阻止并发更新同一个共享资源, 实现了同步
- synchronized 不能用来实现不同线程之间的消息传递(通信)
解决方法
Java提供了方法解决线程之间的通信问题
| 方法名 | 说明 |
|---|---|
| wait() | 表示线程一直等待, 直到其他线程通知, 与sleep不同, wait()会释放锁 |
| wait(long timeout) | 指定等待的毫秒数 |
| notify() | 唤醒一个处于等待状态的线程 |
| notifyAll() | 唤醒同一个对象上所有调用wait()的线程, 优先级别高的线程优先调度 |
注意: 这些方法都是Object类的方法, 都只能在同步方法或者同步代码块中使用, 否则会抛出异常: IllegalMonitorStateExeception
解决方式一:
并发协作模型"生产者/消费者模式" - 管程法
- 生产者: 负责生产数据的模块(可能是方法, 对象, 线程, 进程)
- 消费者: 负责处理数据的模块(可能是方法, 对象, 线程, 进程)
- 缓冲区: 消费者不能直接使用生产者的数据, 他们之间有个缓冲区
- 生产者将生产好的数据放入缓冲区, 消费者从缓冲区中拿出数据

具体代码实现
package org.example.javatest.thread.last;
/**
* 利用管程法实现线程通信
* 需要生产者, 消费者, 产品, 缓冲区
*/
public class TestPC {
public static void main(String[] args) {
SyncContainer container = new SyncContainer();
new Thread(new Product(container)).start();
new Thread(new Consumer(container)).start();
}
}
// 生产者
class Product implements Runnable {
SyncContainer container;
public Product(SyncContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了" + i + "只🐔");
}
}
}
// 消费者
class Consumer implements Runnable {
SyncContainer container;
public Consumer(SyncContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了-->" + container.pop().id + "只🐔");
}
}
}
// 产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SyncContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者生产产品
public synchronized void push (Chicken chicken) {
// 如果容器满了, 就需要等待消费者消费
if (count == chickens.length) {
// 通知消费者消费, 生产者等待
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 如果没有满, 就需要生产产品
chickens[count] = chicken;
count++;
// 通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//判断能否消费
if (count == 0) {
// 等待生产者生产, 消费者等待
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 如果可以消费
count--;
Chicken chicken = chickens[count];
// 通知生产者生产
this.notifyAll();
return chicken;
}
}
解决方式二: 信号灯法
具体代码实现
package org.example.javatest.thread.last;
/**
* 信号灯法解决线程通信问题
*/
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Thread(new Player(tv)).start();
new Thread(new Watcher(tv)).start();
}
}
// 生产者-演员
class Player implements Runnable {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2 == 0) {
this.tv.play("节目1");
} else {
this.tv.play("节目2");
}
}
}
}
//消费者-观众
class Watcher implements Runnable {
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
// 产品 - 节目
class TV {
// 演员表演, 观众等待 T
// 观众观看, 演员等待 F
String voice; // 表演的节目
boolean flag = true;
// 表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("演员表演了:" + voice);
// 通知观众观看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("观看了:" + voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
高级主题
线程池
- 背景: 经常创建和销毁, 使用量特别大的资源, 比如并发情况下的线程, 对性能影响很大
- 思路: 提前创建好多个线程, 放入线程池中, 使用时直接获取, 使用完放回池中, 可以避免频繁创建销毁, 实现重复利用
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低了资源消耗(重复利用线程池中的线程, 不需要每次都创建)
- 便于线程管理
- corePoolSize: 核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时, 最多保持多长时间后终止
使用线程池
- JDK5.0起提供了线程池相关API: ExecutorService 和 Executors
- ExecutorService : 真正的线程池接口, 常见的子类ThreadPoolExecutor
- void executor(Runnable command): 执行任务/命令, 没有返回值, 一般用来执行Runnable
- Future submit(Callable task) : 执行任务, 有返回值, 一般用来执行Callable
- void shutdown() : 关闭线程池
- Executors : 工具类, 线程池的工厂类, 用于创建并返回不同类型的线程池
package org.example.javatest.thread.last;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
*/
public class TestPool {
public static void main(String[] args) {
//创建服务, 创建线程池
// newFixedThreadPool 参数为池子的大小
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 执行
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
// 关闭连接
executorService.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
注解和反射
注解
Java.annotation
注解入门
什么是注解
- Annotation是JDK5.0引入的技术
- Annotation的作用:
- 不是程序本身, 可以对程序做出解释
- 可以被其他程序(比如:编译器等)读取
- Annotation的格式
- 注解是以==@注释名==在代码中存在的, 还可以添加一些参数值
- 例如: @SuppressWarnings(value=“unchecked”)
- Annotation在哪里使用?
- 可以附加在package, class, method, field上面, 相当于给他们添加了额外的辅助信息, 我们可以通过反射机制编程实现对这些元数据的访问
内置注解
- @Override: 定义在java.lang.Override中, 此注解只适用于修饰方法, 表示一个方法声明打算重写超类中的另一个方法声明
- @Deprecated: 定义在java.lang.Deprecated中, 此注解可以用来修饰方法, 属性, 类, 表示不鼓励程序员使用这样的元素, 通常是因为它很危险或者存在更好的选择
- @SuppressWarnings: 定义在java.lang.SuppressWarnings中, 用来抑制编译时的警告信息
- 与前两个注释有所不同, 需要添加一个参数才能正常使用, 这些参数都是已经定义好的, 我们之间选择使用即可
- @SuppressWarnings(“all”)
- @SuppressWarnings(“unchecked”)
- @SuppressWarnings(value={“unchecked”, “deprecation”})
自定义注解
- 使用@interface自定义注解时, 自动继承了java.lang.annotation.Annotation接口
- 分析
- @interface用来声明一个注解, 格式: public @interface 注解名 { 定义内容 }
- 其中的每一个方法实际上是声明了一个配置参数
- 方法的名称就是参数的名称
- 返回值类型就是参数的类型(返回值只能是基本类型, Class, String, Enum)
- 可以通过default来声明参数的默认值
- 如果只有一个参数成员, 一般参数名为value
- 注解元素必须要有值, 我们定义注解元素时, 经常使用空字符串, 0作为默认值
具体代码实现
package org.example.javatest.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解
*/
public class Test01 {
// 注解可以显式的赋值, 如果没有默认值, 就必须给注解赋值
@MyAnnotation(name = "呦呵")
public void test01() {
}
@MyAnnotation2("呦呵")
public void test02() {
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation {
String name();
int age() default 0;
int id() default -1; // 如果默认值为-1, 代表不存在
String[] schools() default {"清华大学", "北京大学"};
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation2 {
// 如果只有一个参数, 参数名必须是value, 否则会报错
String value();
}
元注解
- 元注解的作用就是负责注解其他注解, Java定义了4个标准的meta-annotation类型, 他们被用来提供对其他annotation类型做说明
- 这些类型和它们所支持的类在java.lang.annotation包中可以找到. (@Target, @Retention, @Documented, @Inherited)
- @Target : 用于描述注解的适用范围(即: 被描述的注解可以用在什么地方)
- @Retention : 表示需要在什么级别保存该注释信息, 用于描述注解的生命周期
- SOURCE < CLASS < RUNTIME
- @Documented : 说明该注解将被包含在javadoc中
- @Inherited : 说明子类可以继承父类中的该注解
反射
java.Reflection
静态语言 VS 动态语言
- 动态语言
- 是一类在运行时可以改变其结构的语言: 例如新的函数, 对象, 甚至代码可以被引用, 已有的函数可以被删除或是其他结构上的变化.
- 通俗点说就是在运行时代码可以根据某些条件改变自身结构
- 主要动态语言: Object-C, C#, JavaScript, PHP, Python等
- 静态语言
- 与动态语言相对应的, 运行时结构不可变的语言就是静态语言, 如Java, C, C++
- Java不是动态语言, 但Java可以被称为"准动态语言". 即Java有一定的动态性
- 我们可以利用反射机制获得类似动态语言的特性, Java的动态性让编程的时候更加灵活
Java反射机制概述
Java Reflection
- Reflection(反射) 是Java被视为动态语言的关键, 反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息, 并能直接操作任意对象的内部属性及方法
- Class c = Class.forName(“Java.lang.string”)
- 加载完类之后, 在堆内存的方法区中就产生了一个Class类型的对象, (一个类只有一个Class对象) , 这个对象就包含了完整的类的结构信息. 我们可以通过这个对象看到类的结构, 这个对象就像是一面镜子, 透过这个镜子看到类的结构, 所以我们形象的称之为: 反射
- 正常方式: 引入需要的"包类"名称 —>通过new实例化 —> 取得实例对象
- 反射方式: 实例化对象 —> getClass() —> 得到完整的"包类"名称
Java反射机制提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时处理注解
- 生成动态代理
反射的优缺点
- 优点
- 可以实现动态创建对象和编译, 体现出很大的灵活性
- 缺点
- 对性能有影响, 使用反射基本上是一种解释操作, 我们可以告诉JVM, 我们希望做什么, 并且它能够满足我们的要求, 这类操作总是慢于直接执行相同的操作
理解Class类并获取Class实例
反射相关的主要API
- java.lang.Class : 代表一个类
- java.lang.reflect.Method : 代表类的方法
- java.lang.reflect.Field : 代表类的成员变量
- java.lang.reflect.Constructor : 代表类的构造器
Class类
在Object类中定义了以下方法, 此方法将被所有的子类继承
public final Class getClass()
以上的方法返回值的类型是一个Class类, 此类是Java反射的源头, 实际上所谓反射从程序的运行结果来看就是: 可以通过对象反射求出类的名称
对象通过Class类可以得到的信息: 某个类的属性, 方法, 构造器, 某个类到底实现了哪些接口
对于每个类而言, JRE都为其保留了一个不变的Class类型的对象
一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息
- Class本身也是一个类
- Class对象只能由系统建立对象, 我们只能得到这个Class对象
- 一个加载的类在JVM中只会有一个Class实例
- 一个Class对象对应的是一个加载到JVM中的一个.class文件
- 每个类的实例都会记得自己是由哪个Class实例所生成
- 通过Class可以完整的得到一个类中所有的被加载的结构
- Class类是Reflection的根源, 针对任何你想动态加载, 运行的类, 唯有先获得相应的Class对象
代码实现
package org.example.javatest.reflect;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
//t通过反射获取类的Class对象
Class c1 = Class.forName("org.example.javatest.reflect.User");
System.out.println(c1);
Class c2 = Class.forName("org.example.javatest.reflect.User");
Class c3 = Class.forName("org.example.javatest.reflect.User");
Class c4 = Class.forName("org.example.javatest.reflect.User");
//一个类在内存中只有一个Class对象
//一个类被加载后, 类的整体结构都会被封装在Class对象中
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
System.out.println(c4.hashCode());
}
}
class User {
private int id;
private int age;
private String name;
public User() {
}
public User(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
Class类的常用方法
| 方法名 | 说明 |
|---|---|
| static ClassForName(String name) | 返回指定类名name的Class对象 |
| Object newInstance() | 调用缺省构造函数, 返回Class对象的一个实例 |
| getName() | 返回此Class对象所表示的实体(类, 接口, 数组类或void的名称) |
| Class getSuperClass() | 返回当前Class对象的父类的Class对象 |
| Class[] getInterfaces() | 获取当前Class对象的接口 |
| ClassLoader getClassLoader() | 返回该类的类加载器 |
| Constructor[] getConstructors() | 返回一个包含某些Constructor对象的数组 |
| Method getMethod(string name, Class…T) | 返回一个Method对象, 此对象的形参类型为paramType |
| Field[] getDeclaredFields() | 返回Field对象的一个数组 |
获取Class类的实例
-
若已知具体的类, 通过类的class属性获取, 该方法最为安全可靠, 程序性能最高
Class clazz = Person.class;
-
已知某个类的实力, 调用该实例的getClass()获取Class对象
Class clazz = person.getClass();
-
已知一个类的全类名, 且该类在类路径下, 可通过Class类的静态方法forName()获取, 抛出ClassNotFoundException
Class clazz = Class.forNam(“类的全路径名”);
-
内置基本数据类型可以直接用类名.Type
-
还可以利用ClassLoader
具体代码实现
package org.example.javatest.reflect;
/**
* 验证创建Class的方法
*/
public class Test2 {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Student();
System.out.println("这个人是:" + person.name);
// 通过对象获得
Class c1 = person.getClass();
System.out.println(c1.hashCode());
// 通过foeName获得
Class c2 = Class.forName("org.example.javatest.reflect.Student");
System.out.println(c2.hashCode());
// 通过类名.class获取
Class<Student> c3 = Student.class;
System.out.println(c3.hashCode());
// 内置基本数据类型的包装类都有一个Type属性
Class<Integer> c4 = Integer.TYPE;
System.out.println(c4);
// 获得父类类型
Class superclass = c1.getSuperclass();
System.out.println(superclass);
}
}
class Person {
public String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
class Student extends Person {
public Student() {
this.name = "学生";
}
}
class Teacher extends Person {
public Teacher() {
this.name = "老师";
}
}
哪些类型可以有Class对象
- class : 外部类, 成员内部类, 静态内部类, 局部内部类, 匿名内部类
- interface : 接口
- [] : 数组
- enum : 枚举
- annotation : 注解@interface
- primitive type : 基本数据类型
- void
具体代码实现
package org.example.javatest.reflect;
import java.lang.annotation.ElementType;
/**
* 所以类型的Class
*/
public class Test3 {
public static void main(String[] args) {
// 类
Class c1 = Object.class;
// 接口
Class c2 = Comparable.class;
// 一维数组
Class c3 = String[].class;
// 二维数组
Class c4 = int[][].class;
// 注解
Class c5 = Override.class;
// 枚举
Class c6 = ElementType.class;
// 基本数据类型
Class c7 = Integer.class;
// void
Class c8 = void.class;
// Class
Class c9 = Class.class;
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
System.out.println(c4);
System.out.println(c5);
System.out.println(c6);
System.out.println(c7);
System.out.println(c8);
System.out.println(c9);
// 只有元素类型与维度一样, 数组空间不管多大都是同一个Class
int[] a = new int[10];
int[] b = new int[100];
System.out.println(a.getClass().hashCode());
System.out.println(b.getClass().hashCode());
}
}
-----结果-----
class java.lang.Object
interface java.lang.Comparable
class [Ljava.lang.String;
class [[I
interface java.lang.Override
class java.lang.annotation.ElementType
class java.lang.Integer
void
class java.lang.Class
1300109446
1300109446
类的加载与ClassLoader
Java内存分析
- Java内存
- 堆
- 存放new的对象和数组
- 可以被所有的线程共享, 不会存放别的对象引用
- 栈
- 存放基本数据类型(会包含这个基本数据类型的具体数值)
- 引用对象的变量(会存放这个引用在堆里面的具体地址)
- 方法区
- 方法区就是特殊的堆
- 可以被所有的线程共享
- 包含了所有的class和static变量
- 堆
类的加载过程
当程序主动使用某个类时, 如果该类还未被加载到内存中, 则系统会通过如下三个步骤来对该类进行初始化
类的加载(Load) ----> 类的链接(Link) ----> 类的初始化(Initialize)
- 类的加载 : 将类的class文件读入到内存, 并为之创建一个java.lang.Class对象. 此过程由类加载器完成
- 类的链接 : 将类的二进制数据合并到JRE中
- 类的初始化 : JVM负责对类进行初始化
类的加载与ClassLoader的理解
-
加载
将class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区的运行时数据结构, 然后生成一个代表这个类的java.lang.Class对象
-
链接
将Java类的二进制代码合并到JVM的运行状态之中的过程
- 验证 : 确保加载的类信息符合JVM规范, 没有安全方面的问题
- 准备 : 正式为类变量(static)分配内存并设置类变量默认初始值的阶段, 这些内存都将在方法区中进行分配
- 解析 : 虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
-
初始化
- 执行类构造器()方法的过程. 类构造器()方法是由编译器自动收集类中所有的类变量的赋值动作和静态代码块中的语句合并产生的. (类构造器是构造类信息的, 不是构造该类对象的构造器)
- 当初始化一个类的时候, 如果发现其父类还没有进行初始化, 则需要先触发其父类的初始化
- 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步
具体代码
package org.example.javatest.reflect; /** * 执行结果: * A类静态代码块初始化 * A类的无参构造器初始化 * 100 */ public class TestClass { public static void main(String[] args) { A a = new A(); System.out.println(A.m); } } class A { static { System.out.println("A类静态代码块初始化"); m = 300; } static int m = 100; public A() { System.out.println("A类的无参构造器初始化"); } }
分析类的初始化
- 类的主动引用 (一定会发生类的初始化)
- 当虚拟机启动, 先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量) 和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个类, 如果其父类没有被初始化, 则先会初始化它的父类
- 类的被动引用 (不会发生类的初始化)
- 当访问一个静态域时, 只有真正声明这个域的类才会被初始化. 如: 当通过子类引用父类的静态变量, 不会导致子类初始化
- 通过数组定义类引用, 不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量是在链接阶段就存入调用类的常量池中了)
具体代码实现
package org.example.javatest.reflect;
/**
* 类的初始化
*/
public class Test4 {
static {
System.out.println("main类被加载");
}
public static void main(String[] args) throws ClassNotFoundException {
// 类的主动引用
// Son son = new Son();
/*
main类被加载
father被加载
son类被加载
*/
// 通过反射调用, 也会产生主动引用
// Class.forName("org.example.javatest.reflect.Son");
/*
main类被加载
father被加载
son类被加载
*/
// 不会产生类的引用的方法
//通过子类引用父类的静态变量, 不会导致子类初始化
// System.out.println(Son.b);
/*
main类被加载
father被加载
2
*/
//通过数组定义类引用, 不会触发此类的初始化
// Son[] sons = new Son[10];
/*
main类被加载
*/
//引用常量不会触发此类的初始化
System.out.println(Son.A);
/*
main类被加载
200
*/
}
}
class Father {
static int b = 2;
static {
System.out.println("father被加载");
}
}
class Son extends Father {
static int m = 100;
static final int A = 200;
static {
System.out.println("son类被加载");
m = 300;
}
}
类加载器的作用
- 类加载器的作用: 将Class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区的运行时数据结构, 然后在堆中生成一个代表这个类的java.lang.Class对象, 作为方法区中类数据的访问入口
- 类缓存: 标准的JavaSE类加载器可以按照要求查找类, 但一旦某个类被加载到类加载器中, 他将维持加载(缓存)一段时间, 不过JVM垃圾回收机制可以回收这些Class对象

JVM规范定义了如下类型的类加载器

具体代码实现
package org.example.javatest.reflect;
/**
* ClassLoader
*/
public class Test5 {
public static void main(String[] args) throws ClassNotFoundException {
// 获取系统类的加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
// 获取系统类加载器的父类加载器 --> 扩展类加载器
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent);
//获取扩展类加载器的父类加载器 --> 跟加载器(C/C++)
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);
/*
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4d7e1886
null
*/
// 测试当前类是哪个类加载器加载的
ClassLoader classLoader = Class.forName("org.example.javatest.reflect.Test5").getClassLoader();
System.out.println(classLoader);
// 测试JDK内置类是谁加载的
classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
/*
sun.misc.Launcher$AppClassLoader@18b4aac2
null
*/
// 获取系统类加载器可以加载的类的路径
System.out.println(System.getProperty("java.class.path"));
/*
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/cat.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/crs-agent.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/dnsns.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/jaccess.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/localedata.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/nashorn.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunec.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/zipfs.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/management-agent.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:
/Users/td/IdeaProjects/java-test/target/classes:
/Users/td/JavaEnvironment/repository/org/springframework/boot/spring-boot-starter/2.6.13/spring-boot-starter-2.6.13.jar:
/Users/td/JavaEnvironment/repository/org/springframework/boot/spring-boot/2.6.13/spring-boot-2.6.13.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-context/5.3.23/spring-context-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-aop/5.3.23/spring-aop-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-beans/5.3.23/spring-beans-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-expression/5.3.23/spring-expression-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/springframework/boot/spring-boot-autoconfigure/2.6.13/spring-boot-autoconfigure-2.6.13.jar:
/Users/td/JavaEnvironment/repository/org/springframework/boot/spring-boot-starter-logging/2.6.13/spring-boot-starter-logging-2.6.13.jar:
/Users/td/JavaEnvironment/repository/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar:
/Users/td/JavaEnvironment/repository/ch/qos/logback/logback-core/1.2.11/logback-core-1.2.11.jar:
/Users/td/JavaEnvironment/repository/org/apache/logging/log4j/log4j-to-slf4j/2.17.2/log4j-to-slf4j-2.17.2.jar:
/Users/td/JavaEnvironment/repository/org/apache/logging/log4j/log4j-api/2.17.2/log4j-api-2.17.2.jar:
/Users/td/JavaEnvironment/repository/org/slf4j/jul-to-slf4j/1.7.36/jul-to-slf4j-1.7.36.jar:
/Users/td/JavaEnvironment/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-core/5.3.23/spring-core-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/springframework/spring-jcl/5.3.23/spring-jcl-5.3.23.jar:
/Users/td/JavaEnvironment/repository/org/yaml/snakeyaml/1.29/snakeyaml-1.29.jar:
/Users/td/JavaEnvironment/repository/org/projectlombok/lombok/1.18.24/lombok-1.18.24.jar:
/Users/td/JavaEnvironment/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar:
/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
*/
}
}
创建运行时类的对象
获取运行时类的完整结构
- 通过反射获取运行时类的完整结构
- Field, Method, Constructor, Superclass, interface, Annotation
- 实现的全部接口
- 所继承的父类
- 全部的构造器
- 全部的方法
- 全部的Field
- 注解
具体代码实现
package org.example.javatest.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 获取类的信息
*/
public class Test6 {
public static void main(String[] args) {
User user = new User();
Class c1 = user.getClass();
// 获得类的名字
System.out.println(c1.getName()); // 获得包名 + 类名
System.out.println(c1.getSimpleName()); // 获得类名
/*
org.example.javatest.reflect.User
User
*/
System.out.println("=====================");
// 获得类的属性
Field[] fields = c1.getFields(); // 只能找到public的属性
for (Field field : fields) {
System.out.println("getFields : " + field);
}
System.out.println("-------------");
fields = c1.getDeclaredFields(); // 找到全部的属性
for (Field field : fields) {
System.out.println("getDeclaredFields : " + field);
}
/*
-------------
getDeclaredFields : private int org.example.javatest.reflect.User.id
getDeclaredFields : private int org.example.javatest.reflect.User.age
getDeclaredFields : private java.lang.String org.example.javatest.reflect.User.name
*/
System.out.println("=====================");
try {
Field name = c1.getField("name");
System.out.println(name);
} catch (NoSuchFieldException e) {
System.out.println("找不到这个属性");
}
System.out.println("-------------");
try {
Field name = c1.getDeclaredField("name");
System.out.println(name);
} catch (NoSuchFieldException e) {
System.out.println("找不到这个属性");
}
/*
找不到这个属性
-------------
private java.lang.String org.example.javatest.reflect.User.name
*/
System.out.println("=====================");
// 获得类的方法
Method[] methods = c1.getMethods(); // 获得本类及其父类的全部public方法
for (Method method : methods) {
System.out.println("getMethods : " + method);
}
System.out.println("-------------");
methods = c1.getDeclaredMethods(); // 获得本类的全部方法
for (Method method : methods) {
System.out.println("getDeclaredMethods : " + method);
}
/*
getMethods : public java.lang.String org.example.javatest.reflect.User.toString()
getMethods : public java.lang.String org.example.javatest.reflect.User.getName()
getMethods : public void org.example.javatest.reflect.User.setName(java.lang.String)
getMethods : public int org.example.javatest.reflect.User.getId()
getMethods : public void org.example.javatest.reflect.User.setId(int)
getMethods : public int org.example.javatest.reflect.User.getAge()
getMethods : public void org.example.javatest.reflect.User.setAge(int)
getMethods : public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
getMethods : public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
getMethods : public final void java.lang.Object.wait() throws java.lang.InterruptedException
getMethods : public boolean java.lang.Object.equals(java.lang.Object)
getMethods : public native int java.lang.Object.hashCode()
getMethods : public final native java.lang.Class java.lang.Object.getClass()
getMethods : public final native void java.lang.Object.notify()
getMethods : public final native void java.lang.Object.notifyAll()
-------------
getDeclaredMethods : public java.lang.String org.example.javatest.reflect.User.toString()
getDeclaredMethods : public java.lang.String org.example.javatest.reflect.User.getName()
getDeclaredMethods : public void org.example.javatest.reflect.User.setName(java.lang.String)
getDeclaredMethods : public int org.example.javatest.reflect.User.getId()
getDeclaredMethods : public void org.example.javatest.reflect.User.setId(int)
getDeclaredMethods : public int org.example.javatest.reflect.User.getAge()
getDeclaredMethods : public void org.example.javatest.reflect.User.setAge(int)
*/
System.out.println("=====================");
// 获取指定方法
try {
Method getName = c1.getMethod("getName", null);
System.out.println(getName);
} catch (NoSuchMethodException e) {
System.out.println("找不到这个方法");
}
System.out.println("-------------");
try {
Method setName = c1.getMethod("setName", String.class);
System.out.println(setName);
} catch (NoSuchMethodException e) {
System.out.println("找不到这个方法");
}
/*
public java.lang.String org.example.javatest.reflect.User.getName()
-------------
public void org.example.javatest.reflect.User.setName(java.lang.String)
*/
System.out.println("=====================");
// 获取全部的构造器
Constructor[] constructors = c1.getConstructors();
for (Constructor constructor : constructors) {
System.out.println("getConstructors" + constructor);
}
System.out.println("-------------");
constructors = c1.getDeclaredConstructors();
for (Constructor constructor : constructors) {
System.out.println("getDeclaredConstructors" + constructor);
}
/*
getConstructorspublic org.example.javatest.reflect.User()
getConstructorspublic org.example.javatest.reflect.User(int,int,java.lang.String)
-------------
getDeclaredConstructorspublic org.example.javatest.reflect.User()
getDeclaredConstructorspublic org.example.javatest.reflect.User(int,int,java.lang.String)
*/
System.out.println("=====================");
// 获取指定的构造器
try {
Constructor declaredConstructor = c1.getDeclaredConstructor(int.class, int.class, String.class);
System.out.println("指定: " + declaredConstructor);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
/*
指定: public org.example.javatest.reflect.User(int,int,java.lang.String)
*/
}
}
创建运行时类的对象
- 创建类的对象: 调用Class对象的newInstance()方法
- 类必须有一个无参构造器
- 类的构造器权限需要足够
- 只要在操作的时候, 明确的调用类中的构造器, 并将参数传递进去之后, 才可以进行实例化操作
- 步骤如下:
- 通过Class类的getDeclaredConstructor(Class… parameterTypes)取得本类的指定形参类型的构造器
- 向构造器的形参中传递一个对象数组进去, 里面包含了构造器中所需的各个参数
- 通过Constructor实例化对象
- 步骤如下:
调用指定的方法
- 通过反射, 调用类中的方法, 通过Method类完成
- 通过Class类的getMethod(string name, Class … parameterTypes) 方法取得一个Method对象, 并设置此方法操作时所需要的参数类型
- 之后使用Obejct invoke(Object obj, Object[]… args)进行调用, 并向方法中传递要设置的obj对象的参数信息
- Object invoke(Object obj, Object[]… args)
- Object对应原方法的返回值, 若原方法无返回值, 此时返回null
- 若原方法若为静态方法, 此时形参Object obj可为null
- 若原方法形参列表为空, 则Object[] args为null
- 若原方法声明为private, 则需要在调用invoke()之前, 显式调用方法对象的setAccessible(true), 将可访问private方法
- setAccessible
- Method和Field, Constructor对象都有setAccessible()方法
- setAccessible作用是启动和进制访问安全检查的开关
- 参数值为true, 则指示反射的对象在使用时应该取消Java语言访问检查
- 提高反射的效率. 如果代码中必须用反射, 而该句代码需要频繁的被调用, 那么请设置成true
- 使得原本无法访问的私有成员也可以访问
- 参数值为false则指示反射的对象应该实施Java语言访问检查
具体代码实现
package org.example.javatest.reflect;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 动态创建对象, 通过反射
*/
public class Test7 {
public static void main(String[] args) throws Exception {
// 获得Class对象
Class c1 = Class.forName("org.example.javatest.reflect.User");
// 构造一个对象
// User user = (User) c1.newInstance();// 本质是调用了无参构造器
// System.out.println(user); //User{id=0, age=0, name='null'}
// 通过构造器创建对象
// Constructor constructor = c1.getDeclaredConstructor(int.class, int.class, String.class);
// User user = (User) constructor.newInstance(1, 18, "hiatus");
// System.out.println(user); // User{id=1, age=18, name='hiatus'}
//通过反射调用普通方法
// User user = (User) c1.newInstance();
// Method setName = c1.getDeclaredMethod("setName", String.class);
// setName.invoke(user, "hiatus");
// System.out.println(user.getName()); // hiatus
// 通过反射操作属性
User user = (User) c1.newInstance();
Field name = c1.getDeclaredField("name");
// name.set(user, "斯蒂芬");
// System.out.println(user.getName());
/*
Exception in thread "main" java.lang.IllegalAccessException: Class org.example.javatest.reflect.Test7 can not access a member of class org.example.javatest.reflect.User with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Field.set(Field.java:761)
at org.example.javatest.reflect.Test7.main(Test7.java:34)
*/
// 不能直接操作私有属性, 我们需要关闭Java的安全检测, 属性或者方法的setAccessible(true)
name.setAccessible(true);
name.set(user, "斯蒂芬");
System.out.println(user.getName()); //斯蒂芬
}
}
性能对比分析
package org.example.javatest.reflect;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 性能检测
*/
public class Test8 {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
test1();
test2();
test3();
}
// 普通方式
public static void test1(){
User user = new User();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
user.getName();
}
long endTime = System.currentTimeMillis();
System.out.println("普通方式调用10亿次:" + (endTime - startTime) + "ms");
}
// 反射方式
public static void test2() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class userClass = user.getClass();
Method getName = userClass.getMethod("getName", null);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user, null);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式调用10亿次:" + (endTime - startTime) + "ms");
}
// 反射方式, 关闭安全检测
public static void test3() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
User user = new User();
Class userClass = user.getClass();
Method getName = userClass.getMethod("getName", null);
getName.setAccessible(true);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user, null);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式调用10亿次:" + (endTime - startTime) + "ms");
}
}
调用运行时类的指定结构
反射操作泛型
- Java采用泛型擦除的机制来引入泛型, Java中的泛型仅仅是给编译器javac使用的, 确保数据的安全性和免去强制类型转换问题, 但是, 一旦编译完成, 所有和泛型有关的类型全部擦除
- 为了通过反射操作这些类型, Java新增了ParameterrizedType, GenericArrayType, TypeVariable, WildcardType 几种类型来代表不能被归一到Class类中的类型, 但是又和原始类型齐名的类型
- ParameterrizedType : 表示一种参数化类型, 比如Collection
- GenericArrayType : 表示一种元素类型是参数化类型, 或者类型变量的数组类型
- TypeVariable : 是各种类型变量的公共父接口
- WildcardType : 代表一种通配符类型表达式
具体代码实现
package org.example.javatest.reflect;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
/**
* 通过反射操作泛型
*/
public class Test9 {
public static void main(String[] args) throws Exception {
Method method = Test9.class.getMethod("test1", Map.class, List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
System.out.println("## : " + genericParameterType);
if (genericParameterType instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println("真实的: " + actualTypeArgument);
}
}
}
System.out.println("=============");
method = Test9.class.getMethod("test2");
Type genericReturnType = method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
}
}
public static void test1(Map<String, User> map, List<User> list){
System.out.println("test1");
}
public static Map<String, User> test2(){
System.out.println("test1");
return null;
}
}
-----结果------
## : java.util.Map<java.lang.String, org.example.javatest.reflect.User>
真实的: class java.lang.String
真实的: class org.example.javatest.reflect.User
## : java.util.List<org.example.javatest.reflect.User>
真实的: class org.example.javatest.reflect.User
=============
class java.lang.String
class org.example.javatest.reflect.User
反射操作注解
- getAnnotations
- getAnnotation
具体代码实现
package org.example.javatest.reflect;
import java.lang.annotation.*;
import java.lang.reflect.Field;
/**
* 反射操作注解
*/
public class Test10 {
public static void main(String[] args) throws Exception {
Class c1 = Student2.class;
// 通过反射获取注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);// @org.example.javatest.reflect.TablePDD(value=db_student)
}
// 获得注解的value的值
TablePDD tablePdd = (TablePDD) c1.getAnnotation(TablePDD.class);
System.out.println(tablePdd.value()); // db_student
// 获得类指定的注解
Field name = c1.getDeclaredField("name");
FieldPDD nameAnnotation = name.getAnnotation(FieldPDD.class);
System.out.println(nameAnnotation.columnName()); //db_name
System.out.println(nameAnnotation.length()); //100
System.out.println(nameAnnotation.type()); //varchar
}
}
@TablePDD("db_student")
class Student2{
@FieldPDD(columnName = "db_id", type = "int", length = 100)
private int id;
@FieldPDD(columnName = "db_age", type = "int", length = 100)
private int age;
@FieldPDD(columnName = "db_name", type = "varchar", length = 100)
private String name;
public Student2() {
}
public Student2(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student2{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TablePDD{
String value();
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FieldPDD{
String columnName();
String type();
int length();
}



被折叠的 条评论
为什么被折叠?



