四、数组

数组概述

  • 数组是引用数据类型,隐式继承Object,因此可以调用Object类中的方法
  • 数组对象存储在堆内存中

数组的特点

  • 数组长度一旦确定不可改变
  • 所有数组对象都有length属性,用来获取数组元素个数

优点:

  • 根据下标查找某个元素的效率极高

缺点:

  • 随机增删的效率低,需要后移/前移很多元素
  • 无法存储大量数据,因为很难在内存上找到非常大的一块连续内存

一维数组

静态初始化一维数组

已经知道数组中的值时使用

1
2
3
4
// 第一种
int[] arr = {11,22,33}; 或者 int arr[] = {11,22,33}; //后者不建议
// 第二种
int[] arr = new int[] {11,22,33};

用第一种就好了!

JDK5 新特性:增强for循环 / for-each 循环

1
2
3
for(元素数据类型 变量名:数组名){ // 变量名代表数组中的每个元素,可以自己取名

}

优点:代码简洁

缺点:没有下标

动态初始化

不知道数组中具体存储哪些元素时使用

1
数据类型[] 变量名 = new 数据类型[长度]

数组长度确定,数组中存储的每个元素将采用默认值。

数组中如何存储不同类的对象

创建父类类型的数组,即可存子类的对象

eg.

1
2
3
Apple a = new Apple();
Bird b = new Bird();
Object[] objs = {a,b};

存的是对象的地址

关于main方法的形参args

  • 作用:接收命令行参数用的

  • JVM负责调用main方法时用的 ——JVM负责给main方法准备一个String[ ]一维数组对象

  • java fileName abc def xyz命令行参数:abc def xyz,JVM会将命令行参数以空格进行拆分,生成一个新的数组对象。

    String[ ] args = {“abc”,”def”,”xyz”};

​ 命令行参数有什么用?

​ 需求:使用该系统的时候,需要提供正确的口令(用户名和密码),非法用户直接退出系统。

​ 当两个字符串进行equals比较时,如果其中有一个字符串是字面量,建议将字面量写到前面。即:"string".equals(variable),避免出现空指针异常。

可变长度的参数

function1(int ... nums)

  • 语法格式:数据类型...
  • 在形参列表中,可变长度的参数只能有一个,且只能在参数列表的末尾
  • 可变长度的参数可以当做数组来看待 可通过这种方式访问:nums[0],nums[1]

一维数组的扩容

  • 数组长度一旦确定不可改变

  • 只能新建一个更大的数组,然后将原数组的数据全部拷贝到新数组中,可以使用System.arraycopy()

  • 数组扩容会影响程序的执行效率,因此尽可能预测数据量,减小扩容次数。

二维数组

静态初始化

1
2
3
int[][] arr = new int [][]{{1,2,3},{1,2,3,4,5},{1}};
int[][] arr = {{1,2,3},{1,2,3},{1}};
// 可以等长,也可以不等长

动态初始化

1
2
3
4
// 等长
int[][] arr = new int [3][4];
// 不等长
int[][] arr = new int [3][];

Arrays 工具类

image-20250426165506063

  • 自定义类型做比较的话,这个自定义类型必须实现Comparable接口,并实现compareTo方法。使用sort进行排序时也需要实现该方法。

image-20250504092347101

  • int[] Arrays.copyOf()是系统自动在内部新建一个数组,将原来的数组复制到新建的数组中,并返回新建的数组。而System.arraycopy()没有新建数组,是直接将内容复制到另一个数组中的。而且arraycopy是native方法,由c++代码实现,因此arraycopy的拷贝速度更快

五、异常

什么是异常?

java程序执行过程中的意外、错误、不正确的情况

异常在java中的形式

类和对象的形式存在。

定义异常其实本质上就是定义一个类。

异常如果发生的话,在底层其实通过了这个类new了一个对象。

image-20250504104320928

自定义异常

  1. 自定义异常的类需要继承Exception或者RuntimeException,如果继承的是Exception就认为这个异常是编译时异常。
  2. 提供两个构造方法,一个是无参数的,一个是带有String参数的,并且在构造方法中调用super(String);

处理异常的两种方法

  1. 抛出异常:在类的声明中添加 throws 异常类名

    如果有些方法不允许使用thorws,也可以使用try catch然后在catch里面使用throw

  2. 捕捉异常

1
2
3
4
5
6
7
8
try{
// 尝试执行的代码
}catch(异常类型1 变量名){

}catch(异常类型2 变量名){

}
......

注:异常类型1,2,3,… 一定是从小到大的,否则如果第一个就是父类,那永远都不可能运行后面的异常处理代码了。

throw和throws的区别:

  1. throw是运行时的语句,真正地抛出一个异常实例
  2. throws是编译时的声明,告诉编译器和调用者,如果出现这些问题就抛出。

JAVA7 新特性 —— 异常统一处理方式

1.

1
2
3
4
5
6
try{
// 尝试执行的代码
}catch(异常类型1 | 异常类型2 变量名){

}
......

异常对象的方法

  • getMessage
  • printStackTrace
1
2
3
4
5
6
7
8
9
10
11
try{
// 尝试执行的代码
}catch(IllegalNameException e){
// 这个方法可以获取当时创建异常对象时给异常构造方法传递的String message参数的值
String message = e.getMessage();

// 打印异常的堆栈信息
e.printStackTrace();
}
......
后面执行的代码

异常的堆栈信息:

  • 异常信息的打印是符合栈这个数据结构的,因此优先看最上面的异常行数,最上面是最后执行的代码

  • 打印异常堆栈信息可能出现在“后面执行的代码”前面,也可能在后面。因为高版本的底层是用多线程并行打印的。

finally 语句块

放在该语句块中的代码是一定会执行的(无论前面的程序是否有异常),一般在finally语句块中完成资源的释放。

顺序:try…(catch)…finally

继承问题

子类继承父类后,重写了父类的方法,重写之后不能抛出更多的异常,可以更少。

六、常用类

字符串 String

为什么string 字面量不可变?

因为底层代码中string是用byte数组存的,而byte数组是private final修饰的,因此无法修改它的值。(java8及之前是char数组)

字符串拼接

  • 如果拼接的两个字符串中有一个是变量,那么拼接后的新字符串不会放到字符串常量池中。而是在堆中。

底层在进行拼接时,会创建一个StringBuilder对象,进行字符串拼接。最后自动调用StringBuilder对象中的toString()方法,再将StringBuilder对象转换成String对象。

  • 两个字符串字面量拼接会在编译阶段做优化,在编译阶段进行拼接(可以这么理解,但不准确)因此字符串常量池中只有拼接后的内容

怎么把字符串手动放进字符串常量池?

1
2
String m = "test";
String str = m.intern(); // 将"test"放入字符串常量池,并且将"test"对象的地址返回。如果字符串常量池已经存在"test",那么就直接返回地址。

只能加东西,不能删东西。

String类常用的构造方法

image-20250504170631964

String常用方法

image-20250504172720448

image-20250504174128601

image-20250505133142362

正则表达式

image-20250505134512560

image-20250505134535133

String 中正则表达式相关的方法

image-20250505135051207

StringBuffer 与 StringBuilder 可变长度字符串

  • 这两个类是专门为频繁进行字符串拼接而准备的
  • StringBuffer是先出现的,Java5时新增了StringBuilder。StringBuffer是线程安全的,而StringBuilder效率更高。
  • 两者底层都是byte[]数组,并且没有被final修饰,因此可以扩容。
  • 优化策略:创建对象时预估好字符串的长度,给定一个合适的初始化容量,减少底层数组扩容的次数。
  • StringBuilder默认初始化容量:16
  • StringBuilder扩容策略:每次扩容为原来的两倍+2

为什么频繁拼接字符串时使用StringBuilder/StringBuffer更好?

使用“+”进行拼接,底层每次都会创建一个StringBuilder对象,然后再调用toString方法,10000次拼接就要创建10000次对象,同时给垃圾回收也造成了很大的压力。

而StringBuilder的append不创建新对象,直接在原来的位置进行拼接,且不调用toString方法,只有用print输出的时候才调用一次,因此节省了大量的时间。

包装类

image-20250506204324943

包装类中的6个数字类型都继承了Number类

image-20250506204542750

装箱boxing:将基本数据类型包装成引用数据类型 Integer i = new Integer(100);

拆箱:int num = i.intValue()

Integer 常用方法

image-20250506205907163

String、int、Integer 三者相互转换

image-20250506211343316

自动装箱/拆箱(JAVA5新特性)

编译阶段的功能,底层仍然是之前的装箱/拆箱。只是让你编程的时候方便一点。

自动装箱

Integer x = 100;

自动拆箱

int num = x;

整数型常量池

[-128~127]这些数字太常用了,为了提高效率,Java提供了一个整数型常量池。

这个常量池是一个数组:Integer[ ] integerCache;

数组中存储了256个Integer的引用,只要没有超出这个范围的数字,直接从整数型常量池中取。

BigInteger

大数字:

  • 超过long了使用java.math.BigInteger
  • 他的父类是Number
  • 他是引用数据类型

常用方法:

image-20250506213606778

BigDecimal

浮点型超过double就使用BigDecimal

构造方法:BigDecimal(String val)

常用方法:

image-20250507104150446

DecimalFormat

该类是专门用来对数字进行格式化的。

常用数字格式:

  • ###,###.## 三个数字为一组,组和组之间使用逗号隔开,保留两位小数
  • ###,###.0000 三个数字为一组,组和组之间使用逗号隔开,保留4位小数,不够补0

构造方法:DecimalFormat(String pattern)

常用方法:String format(数字)

日期相关API

获取时间

1
2
3
4
5
6
7
8
9
// java.util.Date 日期API
// 获取系统当前时间
Date date = new Date()

// 获取指定的时间
Date date1 = new Date(输入毫秒数) //1970年0时0分0秒 + 输入的毫秒数

// 获取1970到当前的毫秒数,这是java.lang.System类的方法。
long time = System.currentTimeMillis();

日期格式化

1
2
3
4
5
6
7
8
9
import java.util.Date;
import java.text.SimpleDateFormat;

public class DateTest01{
public static void main(String[] args){
SimpleDateFormat sdf = new SimpleDateFormat(输入格式的字符串) // 各种格式见文档
String str = sdf.format(输入要转换的时间) // 日期转格式化字符串
}
}

将String转化成Date

1
2
3
4
String strDate = "2008-08-08 08:08:08 888";
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");

Date date = sdf2.parse(strDate); // 用自己创建的格式去解析字符串中的日期

java.util.Calend ar 日历类

获取当前时间的日历对象

1
Calendar c = Calendar.getInstance();

获取日历中的某部分

image-20250507111805395

修改日历中的内容

image-20250507113921982

日历的新API(java8)

日期

传统的日期API存在线程安全问题,于是java8提供了一套全新的日期API

image-20250507114255117

image-20250507114954817

时间戳

image-20250507115444014

计算时间间隔、日期间隔

image-20250507115627185

image-20250507115736786

时间矫正器

image-20250507115810587

日期格式化

image-20250507115949312

注意:这里使用LocalDateTime去调用parse方法,还需要把格式作为参数传入。

数学类 Math

回顾:工具类的方法都是静态的,直接使用类名调用。

image-20250507141230707

枚举类

优点

  • 可读性强
  • 做了类型的限定,在编译阶段就可以确定类型是否正确,不正确会报错

定义

1
2
3
enum 枚举类型名 {
枚举值1,枚举值2,枚举值3,枚举值4
}

特点:

image-20250507142735026

高级用法

image-20250507143828351

Random 随机数生成器

image-20250507144042746

1
2
Random random = new Random();
int num = random.nextInt(101); // 生成一个[0,101)的随机数

System类

image-20250507165944744

UUID 通用唯一标识符

UUID是一种软件构建的标准,用来生成具有唯一性的ID。

image-20250507170341895

1
2
UUID uuid = UUID.randomUUID();
String s = uuid.toString();

七、集合

集合概述

  • 集合是一种容器,用来组织和管理数据。

  • Java的集合框架对应的这套类库其实就是对各种数据结构的实现

  • 集合存储的是引用

  • 默认情况下,如果不使用泛型,集合中可以存储任何类型的引用。

Java集合框架分为两部分:

  1. Collection结构:元素以单个的形式存储
  2. Map结构:元素以键值对的映射关系存储

Collection 关系图

image-20250509151443189

Collection接口的通用方法

image-20250507205015673

Collection的通用遍历/迭代方式

面向接口编程

1
2
3
4
5
6
7
8
9
10
11
Collection col = new ArrayList();

// 第一步:获取集合的迭代器对象
Iterator it = col.iterator();

// 第二步:判断光标当前指向的位置是否有元素
while(it.hasNext()){
// 第三步:光标返回当前指向的内容,并移动到下一个元素
Object obj = it.next();
System.out.println(obj);
}

SequencedCollection接口

所有的有序集合都实现了SequencedCollection接口

image-20250508113038478

泛型

  • java5新特性,是编译阶段的功能。

泛型初体验

  • 程序编写时看帮助文档中是否有”<>”符号,如果有这个符号就可以使用泛型。
  • 创建一个集合,要求这个集合中只能存放某种类型的对象,就可以使用泛型
1
2
3
4
5
6
7
8
Collection<User> users = new ArrayList<User>();

Iterator<User> it = users.iterator();
while(it.hasNext()){
User user = it.next();
user.pay()
}
// 如果不用泛型,it.next()返回的类型是Object,还需要向下转型才能使用子类独有的方法。而使用了泛型后,迭代器返回的类型就自动向下转型为子类了。

泛型的作用

image-20250509192841450

钻石表达式 (Java7新特性)

1
Collection<User> users = new ArrayList<>() // 后面尖括号中的内容可以省略

泛型擦除与补偿(了解)

image-20250508140247873

泛型的定义

在类上自定义泛型

1
2
3
4
5
6
7
8
public class vip<NameType, AgeType>{ // 在声明类时写上泛型名称
public vip(NameType name, AgeType age){
this.name = name;
this.age = age;
}
private NameType name;
private AgeType age;
}

在类上定义的泛型,在静态方法中无法使用。(因为静态方法直接通过类名调用,此时还没有通过声明类的对象来指定泛型的类型。)

在静态方法上定义泛型

1
2
3
4
5
6
7
8
9
10
11
12
public class test{ 
public static <T> void print(T element){ // 在使用前需要先定义泛型
System.out.println(element);
}

public static void main(String[] args){
String a = "Hello World!";
test.print(a);
}
}


在接口上定义泛型

和类定义泛型差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface MyCompare<T>{
public int compare(T element);

}
// 第一种实现接口的方式:此时我已经知道泛型要用什么类型了
public class Product implements MyCompare<int>{
@Override
public int compare(int a){
比较的代码;
}
}

// 第二种实现接口的方式:此时还不知道泛型用什么类型
public class test<T> implements MyCompare<T>{ // 再给类定义一个泛型,然后等创建对象时再确定泛型的类型
@Override
public int compare(T a){ // 或者这个时候就不要用泛型了,直接把参数的类型写成 Object
比较的代码;
}
}

泛型的使用

泛型通配符

无限定通配符

<?> 此处表示后面填写的泛型可以是任意数据类型。

image-20250508151444447

上限通配符

<? extends Number> 表示泛型必须为Number及其子类

下限通配符

<? super Number> 表示泛型必须为Number及其父类

集合的并发修改问题 fail-fast 机制

  • 集合中设置了一个modCount属性,用来记录修改的次数,使用集合对象执行增删改的操作时,modCount就会自动加1。

  • 获取迭代器对象时,会给迭代器对象初始化一个expectedModCount属性,并且将modCount的值赋值给expectedModCount。

    int expectedModCount = modCount;

  • 当使用集合对象删除元素时,modCount会加1,但是迭代器中的expectedModCount没有加1。而当迭代起对象的next()方法执行时,会检测expectedModCount和modCount是否相等,如果不相等,就会抛出ConcurrentModificationException异常

  • 而如果使用迭代起删除元素时,modCount和expectedModCount都会加1.这样next()方法在检测时就是相等的,不会出现异常。

注:即使没有使用多线程编程,但是用迭代器去遍历的同时使用集合去删除元素,这个行为将被认为并发修改。

所以,迭代集合时,要使用 迭代器对象.remove(),移除的是当前光标所执行的元素。

List 接口

特点

有序、可重复

常见的实现类

  • ArrayList 数组
  • Vector、Stack 数组(线程安全的)
  • LinkedList 双向链表

List接口特有的方法

image-20250508164614700

List特有的迭代方式

image-20250508211934510

注:调用迭代器的remove和set方法的前提是之前调用了next或者previous方法获取了一个元素,remove和set是作用于之前获取的那个元素上的。

List接口使用Comparator排序

回顾数组中自定义类型是如何排序的?

  • 所有自定义类型排序时必须指定排序规则,实现Comparable接口,并重写compareTo方法。 重写是override

List集合的排序

  • default void sort(Comparator<? super E> c);
  • sort方法需要一个参数:java.util.Comparator ,我们把它叫做比较器,它是一个接口。
  • 如何给自定义类型指定比较规则?可以对Comparator提供一个实现类,并重写compare方法来指定比较规则
  • 这个实现类也可以看采用匿名内部类的方式。

对数组的排序是在类里面重写比较规则,对List集合的排序是单独设定一个比较规则并在需要时使用。

ArrayList 类

回顾:数组的优缺点

优点

数组在内存中是连续存储的,有下标就有偏移量,可以通过偏移量计算出对应元素的内存地址。检索效率高,时间复杂度O(1)

缺点

  1. 不能存储大数据(因为内存地址是连续的)
  2. 随机增删元素耗时很长

使用场景

需要频繁检索元素,很少进行随机增删的情况。

ArrrayList扩容策略

  1. 当调用无参构造方法时,初始化容量为0。
  2. 当第一次调用add方法时,将ArrayList容量初始化为10个长度。
  3. 后续扩容时,底层会创建一个新的数组,然后使用数组拷贝。新数组的容量是原容量的1.5倍。

Vector 类(*不怎么使用了)

image-20250509151626055

LinkedList 双向链表类

image-20250509161904153

栈 数据结构

image-20250509162034647

image-20250509162149438

队列 数据结构

image-20250509163049904

入队:offer

出队:poll

三种Set

image-20250509164743958

map和set的关系

map是键值对,把键那一列单独拿出来,就是set集合。

Map

image-20250509165852628

Map 接口的常用方法

image-20250509193744059

Map 集合的遍历

方法一:获取Map集合的所有key,然后遍历每个key,通过key获取value

1
2
3
4
5
6
7
8
9
10
11
12
13
Set<Integer> keys = maps.keySet();

// 写法一
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
Integer key = it.next();
System.out.println(key + "=" + maps.get(key))
}

// 写法二
for(Integer key : keys){
System.out.println(key + "=" + maps.get(key))
}

方法二:获取Map的内部类Map.Entry (效率更高,常用这个)

不需要再通过key去找value了

image-20250509195622488

1
2
3
4
5
6
7
8
9
10
11
12
13
Set<Map.Entry<Integer,String>> entries = maps.entrySet();

// 写法一
Iterator it = entries.iterator();
while(it.hasNext()){
Map.Entry<Integer,String> entry = it.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}

// 写法二
for(Map.Entry<Integer,String> entry : entries){
System.out.println(entry.getKey() + "=" + entry.getValue());
}

HashMap

哈希表存储原理

image-20250509210206127

!!hashCode和equals方法要同时重写

使用equals的前提条件是两个元素计算得到的索引值是相同的,在同一个链表中。那么保证这两个元素使用hashCode()返回的结果是相同的才能准确的保证索引值相同。

因此,存放在HashMap集合key部分的元素,以及存放在HashSet集合中的元素,需要同时重写hashCode和equals方法

HashMap在Java8后的改进

初始化时机

java8之前,构造方法执行初始化table数组

java8之后,第一次调用put方法时初始化table数组

插入方法

java8之前:头插法

java8之后:尾插法

数据结构

java8之前:数组+单向链表

java8之后:数组+单向链表/红黑树

  • 如果结点数量>=8,且table长度>=64,单向链表转为红黑树
  • 当删除红黑树上的结点,使节点数量<=6时,红黑树转换为单向链表

HashMap的容量永远是2的次幂

原因:

  1. 提高哈希计算的效率(位运算的效率比%取模运算效率高)

    当length为2的次幂时,length-1的二进制低位全是1,此时hash & (length - 1) 相当于 保留 hash 的低 n,结果与hash%length一致,使用位运算效率更高。

  2. 减少哈希冲突,让散列分布更加均匀

    假设length是偶数,length-1结果一定是奇数,它的二进制中的最后一位一定是1,和别人相与可能是0或1。如果length是奇数,length-1是偶数,那么二进制最后一位是0,和别人相与只能是0,那么最后table有一半都是空的,存不了东西。

HashMap的初始化容量设置

  1. 当哈希表中的元素越来越多时,散列碰撞的几率就会越来越高,导致单链表过长,降低了哈希表的性能,此时要进行哈希表扩容
  2. 而一旦进行扩容,由于length改变,所有元素的hash值都会改变,效率比较低,所以在初始化的时候最好设置好数组大小,避免过多次数的扩容。
  3. 扩容时间点:当哈希表中的元素个数超过数组大小*0.75后进行扩容,新数组大小为2*原数组大小

LinkedHashMap

  1. LinkedHashMap是HashMap集合的子类
  2. 用法和HashMap几乎一样
  3. 只不过LinkedHashMap可以保证元素的插入顺序
  4. 底层数据结构:哈希表+双向链表(记录顺序)

image-20250511152438610

Hashtable(效率低,不常用)

image-20250511153454341

image-20250511154424681

image-20250511154408581

Properties 属性类

image-20250511155031484

TreeMap

排序二叉树

按照左小右大存储,按照中序遍历自动得到升序排列的元素。

缺点:如果插入的节点集本来就是有序的,那么最后得到的二叉树其实就是一个普通链表,检索效率很差。

image-20250511155728257

平衡二叉树

image-20250511160248989

红黑二叉树

一棵自平衡的排序二叉树

构造方法

一个是没有参数的,一个是需要传比较器的

put() 方法

先调用比较器,如果比较器是NULL,就使用类中的compareTo方法进行比较。

因此有两种方式来修改比较方法。

法一:实现Comparable<>接口,并重写compareTo方法

适用于比较规则不会改变的情况,比如数字、字符串的比较

法二:再写一个类去实现Comparator<>接口,重写compare方法,在创建对象时将比较器传递给TreeMap

适用于比较规则会改变的情况

总结:哪些集合不能添加NULL

  • Hashtable的key、value

  • Properties的key、value

  • TreeMap的key

​ -> TreeSet不能添加null

Collections 工具类

image-20250512120540553

八、IO流

IO流概述

分类

根据流向分

输入流(read)、输出流(write)

根据读写数据的形式分

  • 字节流:一次读取一个字节。适合读取非文本数据,比如图片、音频、视频等。

  • 字符流:一次读取一个字符。只适合读取普通文本,不适合读取二进制文件。因为字符流统一使用Unicode编码,可以避免出现编码混乱的问题。

根据流在IO操作中的作用和实现方式分

  • 节点流:负责数据源和数据目的地的连接,是IO中最基本的组成部分。
  • 处理流:处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理。

IO流体系结构

image-20250512150234321

InputStream 字节输入流

OutputStream 字节输出流

Reader 字符输入流

Writer 字符输出流

  • 所有流都实现了Closable接口,都有close()方法,流用完要关闭。

  • 所有的输出流都实现了Flushable,都有flush()方法,flush方法的作用是,将缓存全部写出并清空。

FileInputStream 类

称为文件字节输入流,是一个万能流,任何文件都能读,但还是建议读二进制文件,例如图片、声音、视频。

常用构造方法

FileInputStream(String name)通过文件路径构建一个文件字节输入流对象。

注意: 反斜杠需要使用转义字符,即两个反斜杠 \\

也可使用一个正的斜杠 /

使用方法

int read();

调用一次read()方法就读取一个字节,返回读到的字节本身。如果读不到任何数据则返回-1

int read(byte[] b);

一次最多可以读到b.length个字节(只要文件内容足够多),返回值是读取到的字节数。读取的内容存在b数组中。

int read(byte[] b, int off, int len);

一次读取len个字节,将读到的数据从byte数组的off位置开始放

long skip(long n);

跳过n个字节

int available();

获取流中剩余的预估计字节数。

可以用这个初始化数组长度,这样就不需要使用循环来判断是否还有可读取的内容。

void close();

关闭流

FileOutputStream

文件字节输出流,负责写。

常用构造方法

  1. FileOutputStream(String name) 创建一个文件字节输出流对象,这个流在使用时,会先将原文件内容全部清空,然后写入。
  2. FileOutputStream(String name, boolean append)

​ 创建一个文件字节输出流对象,当append是true时,不会清空原文件的内容,在原文件末尾追加。

​ 当append是false时,会清空原文件的内容,在原文件末尾追加。

常用方法

void close();

void flush();

刷新

void write(int b);

写一个字节

void write(byte[] b);

将整个byte字节数组写入

void write(byte[] b, int off, int len);

将byte字节数组的一部分写入

TryWithResources 资源自动关闭 Java7新特性

凡是实现了AutoCloseable接口的流都可以使用try-with-resources,都会自动关闭。

格式:

1
2
3
4
5
6
7
8
9
try(   
声明流;
声明流;
声明流;
声明流){ // 最后一个不用写分号

}catch(){

}

image-20250512214659872

FileReader 读取普通文本

image-20250513154453660

FileWriter

image-20250513155041546

注意:只能复制普通文本文件!!!

路径

绝对路径、相对路径、类路径

1
2
String path = Thread.currentThread().getContextClassLoader().getResource("filename").getPath();
System.out.println(path);

Thread.currentThread() 获取当前线程

Thread.currentThread().getContextClassLoader() 获取当前线程的类加载器

getResource("filename") 从类的根路径下开始加载资源

src文件夹是类路径的根路径

优点:通用,在进行系统移植的时候,仍然可以使用。

注:这种方式只能从类路径中加载资源,如果这个资源在类路径之外,就无法访问到。

BufferedInputStream/BufferedOutputStream

对缓冲流的理解

image-20250514101835840

使用

image-20250514103000835

1️⃣为什么这里仍然需要使用数组呢?

这个数组是接收缓冲区中的大数组中的内容,它本身不和文件进行交互。

标记

mark() 在当前位置打上标记

reset() 回到上一次打标记的位置

一个文件中最多只有一个标记

调用顺序:先调用mark,再调用reset

如何解决乱码问题

image-20250514111017372

所有输入输出底层都需要使用字节流,而字符流是将字节流包装后得到的。进行了这种包装操作的流叫包装流。

  • 使用InputStreamReader/OutputStreamWriter时可以指定解码的字符集。

  • 常用构造方法:

    1. InputStreamReader(InputStream in) 采用平台默认的字符集进行解码
    2. InputStreamReader(InputStream in, String charsetName) 采用指定的字符集进行解码
  • FileReader是InputStreamReader的子类,是一个包装流。

​ FileWriter同理。

  • InputStreamReader/OutputStreamWriter 的创建需要传入字节流,而FileReader/FileWriter 的创建直接输入文件地址即可。

数据流

  • 将java程序中的数据直接写入文件,写进去就是二进制。

  • 效率很高——写的过程不用转码

  • DataOutputStream写到文件中的数据,只能由DataInputStream来读取

  • 读取顺序必须按照写入顺序!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 写入
DataOutputStream dos = new DataOutputStream(new FileOutputStream("filename"));

byte b1 = 127;
short s1 = 222;

dos.writeByte(b1);
dos.writeShort(s1);

dos.flush();
dos.close();

// 读取
DataInputStream dis = new DataInputStream(new FileInputStream("filename"));

byte b2 = dis.readByte();
short s2 = dis.readShort();
dis.close();

对象的序列化与反序列化

序列化:将对象变成二进制文件

1
2
3
4
5
6
7
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("filename"));
Date nowDate = new Date();
// 序列化Serial
oos.writeObject(nowTime);

oos.flush();
oos.close();

反序列化:将字节序列转换成JVM中的java对象

1
2
3
4
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("filename"));
//反序列化
Object o = ois.readObject(); // 如果明确知道对象的类型,可以强转。
ois.close();

如果是多个对象,那就把这些对象放在集合中。

要参与序列化与反序列化的对象,必须实现 java.io.Serializable 接口。该接口是一个标志接口,没有任何方法。

  • ObjectOutputStream也有关于数据输出的方法,比如writeInt()writeBoolean()等,和DataOutputStream中的方法一样。

序列化版本号

  • 为了保证序列化的安全,只有同一个类的对象才能序列化和反序列化。在java中 通过 类名 + 序列化版本号(serialVersionUID)来判断。

  • 当类的内容修改后,serialVersionUID会改变,java程序不允许序列化版本号不同的类进行反序列化。

  • 那如果几个月后,对这个类进行了升级,增加了一些内容怎么办?

​ 如果确定这个类确实是之前的那个类,类本身是合法的,可以将序列化版本号写死

1
private static final long serialVersionUID = 1231231231231L;

serial注解

1
2
3
4
import java.io.Serial

@Serial // 会自动检查下面的序列号代码是否拼错,在这里alt+回车可以自动生成一个序列号版本。
private static final long serialVersionUID = 1231231231231L;

transient关键字

transient关键字修饰的属性不会参与序列化。

所以进行反序列化的时候这个属性会赋默认值。

打印流 PrintStream/PrintWriter

PrintStream

主要用于打印,提供便携的打印方法和格式化输出。主要打印内容到文件或控制台。

不需要手动刷新。

构造方法

PrintStream(OutputStream out);

PrintStream(String filename);

常用方法

print(Type x);

println(Type x);

image-20250514160807745

PrintWriter

比PrintStream多一个构造方法:PrintWriter(Writer);

标准输入流 System.in

用来接收用户在控制台上的输入。

1
2
3
4
5
6
7
8
InputStream in = System.in;

byte[] bytes = new byte[1024];
int readCount = in.read(bytes);

for(int i = 0; i < readCount; i++){
System.out.println(bytes[i]); // 这个是逐个输出每个字节的内容,不适合中文等内容
}

对于标准输入流来说,也可以改变数据源。不让其从控制台读数据,而是从文件中/网络中读取数据。

1
2
3
4
5
6
7
8
9
10
// 修改标准输入流的数据源
System.setIn(new FileInputStream("filename"));

InputStream in = System.in;

byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes)) != -1){
System.out.print(new String(bytes,0,readCount));
}

标准输出流 System.out

用于输出内容到控制台。

改变输出方向:(常用于记录日志)

1
2
3
System.setOut(new PrintStream("filename"));

System.out.println("zhangsan");

File类

文件/目录的抽象表示形式。

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
File file = new File("e:/filename");

// 如果不存在就以新文件的形式创建
if(!file.exists()){
file.createNewFile();
}

// 如果不存在就以目录的形式创建
if(!file.exists()){
file.mkdir();
}

// 如果不存在就创建多层文件夹
File file2 = new File("e:/a/b/c/d");
if(!file2.exist()){
file2.mkdirs();
}

常见方法见文档。

1
2
3
4
5
6
7
8
File file = new File("e:/directoryAddress");
File[] files = file.listFiles() // 直接获取所有文档
File[] files2 = file.listFiles(new FilenameFilter()){
@Override
public boolean accept(File dir, String name){
return name.endsWith(".txt"); // 进行判断,如果结果不是txt就返回false,就不选中这些文件
}
}

读取属性配置文件

  • xxx.properties 文件称为属性配置文件
  • 属性配置文件可以配置一些简单的信息,例如连接数据库的信息通常配置到属性文件中。这样可以做到在不修改java代码的前提下,切换数据库。
  • 属性配置文件的格式:

​ key1 = value1

​ key2 = value2

​ …

​ 注:使用#进行注释,key不能重复,否则value会被覆盖。等号两边不能有空格。

1
2
3
4
5
6
7
8
9
10
11
String path = Thread.currentThread().getContextClassLoader().getResource("filename").getPath();
FileReader reader = new FileReader(path);

// 创建一个Map集合(属性类对象)
Properties pro = new Properties();

// 将属性配置文件中的配置信息加载到Properties对象中。
pro.load(reader)

String driver = pro.getProperty("driver");
String url = pro.getProperty("url");

ResourceBundle进行资源绑定

image-20250514215334331

装饰器设计模式

符合OCP的情况下怎么完成对类功能的扩展?

  • 使用子类对父类进行方法扩展。但这种方法会导致两个问题:代码耦合度高、类爆炸问题(会有很多类)
  • 装饰器设计模式:可以做到在不修改原有代码的基础上,完成功能扩展,符合OCP原则,并且避免了使用继承带来的类爆炸问题。

装饰器设计模式中涉及的角色:

  1. 抽象的装饰者
  2. 具体的装饰者1、具体的装饰者2
  3. 被装饰者
  4. 装饰者和被装饰者的公共接口/公共抽象类

IO流中使用了大量的装饰器设计模式。

image-20250515164725487

压缩流

压缩流的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class GZIPOutputStreamTest {
public static void main(String[] args) throws Exception{

// 创建文件字节输入流(读某个文件,这个文件将来就是被压缩的。)
FileInputStream in = new FileInputStream("e:/test.txt");

// 创建一个GZIP压缩流对象
GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream("e:/test.txt.gz"));

// 开始压缩(一边读一边写)
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes)) != -1){
gzip.write(bytes, 0, readCount);
}

// 非常重要的代码需要调用
// 刷新并且最终生成压缩文件。
gzip.finish();

// 关闭流
in.close();
gzip.close();
}
}

解压缩流的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GZIPInputStreamTest {
public static void main(String[] args) throws Exception {
// 创建GZIP解压缩流对象
GZIPInputStream gzip = new GZIPInputStream(new FileInputStream("e:/test.txt.gz"));

// 创建文件字节输出流
FileOutputStream out = new FileOutputStream("e:/test.txt");

// 一边读一边写
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = gzip.read(bytes)) != -1){
out.write(bytes, 0, readCount);
}

// 关闭流
gzip.close();
// 节点流关闭的时候会自动刷新,包装流是需要手动刷新的。
out.close();
}
}

注:节点流关闭时会自动刷新,包装流需要手动刷新。

字节数组流

  1. ByteArrayInputStream、ByteArrayOutputStream都是内存操作流,不需要打开和关闭文件等操作。这些流是非常常用的,可以将它们看作开发中的常用工具,能够方便地读写字节数组、图像数据等内存中的数据。
  2. 都是节点流。

image-20250516105141032

使用对象流装饰字节数组流

!!为什么要这样做?

你使用字节数组流直接写入、读出可能只能读取普通的字节数组,还需要自己实现一些转换成复杂类型(各种类)的方法,而包装流已经在内部包含了很多将复杂类型序列化的方法,一行代码就可以帮你直接序列化复杂类型然后写入字节流

对象深克隆

目前为止对象拷贝方式:

  1. 调用Object的clone方法,默认是浅克隆,需要深克隆的话,就需要重写clone方法
  2. 可以通过序列化和反序列化完成对象的克隆(深克隆)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DeepCloneTest {
public static void main(String[] args) throws Exception{
// 准备对象
Address addr = new Address("北京", "朝阳");
User user = new User("zhangsan", 20, addr);

// 将Java对象写到一个byte数组中。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(user);

oos.flush();

// 从byte数组中读取数据恢复java对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);

// 这就是那个经过深拷贝之后的新对象
User user2 = (User) ois.readObject();

user2.getAddr().setCity("南京");

System.out.println(user);
System.out.println(user2);
}
}

九、多线程

概述

多线程

  1. 进程:操作系统中的一段程序,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。
  2. 线程:进程中的一个执行单元。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同操作,提高程序的执行效率。一个进程可以有多个线程。
  3. 静态变量、实例变量是在堆中的,所以是共享的。

image-20250516141939273

并发

使用单核CPU时,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果。

image-20250516143428949

并行

多核CPU,同一时刻,多条指令在多个CPU上同时执行。(无论微观还是宏观)

image-20250516143534577

并发与并行

  1. CPU比较繁忙时,如果开启了多个线程,则只能为一个线程分配仅有的CPU资源,多线程会竞争CPU资源。
  2. 在CPU资源比较充足时,一个进程内的多个线程可以被分配到不同的CPU资源,实现并行。

多线程实现的是并发还是并行?如上所述,看运行时CPU的资源,都有可能

线程的调度模型

多个线程抢夺一个CPU内核的执行权,需要线程调度策略。

分时调度模型

所有线程轮流使用CPU的执行权,并且平均分配每个线程占用的CPU时间

抢占式调度模型

让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就随机选择一个线程获得CPU的执行权。

JAVA采用的就是抢占式调度。

实现多线程的方法

第一种

  1. 编写一个类继承java.lang.Thread
  2. 重写run方法
  3. new线程对象
  4. 调用线程对象的start()方法来启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadTest{
public static void main(String[] args){
MyThread mt = new MyThread();

mt.start();
}
}

class MyThread extends Thread{
@Override
public void run(){
多线程执行的内容;
}
}

start方法的任务是启动一个新线程,分配一个新的栈空间就结束了。

java永远满足一个语法规则:必须自上而下依次逐行运行。

image-20250516145644299

第二种

  1. 编写一个类实现java.lang.Runnable接口
  2. 实现接口中的run方法 (此处不能thorws异常)
  3. new线程对象(把实现Runnable接口的类传给Thread构造方法)
  4. 调用线程对象的start()方法来启动线程

这种方式更好,因为以后还可以继承别的类。而第一种已经使用掉继承一个类的名额了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTest{
public static void main(String[] args){
Thread t = new Thread(new MyRunnable());
t.start();
}
}

class MyRunnable implements Runnable{
@Override
public void run(){
多线程执行的内容;
}
}

这种方式还可以使用匿名内部类:

1.

1
2
3
4
5
6
7
8
9
10
11
public class ThreadTest{
public static void main(String[] args){
Thread t = new Thread(new Runnable(){
@Override
public void run(){
多线程执行的内容;
}
});
t.start();
}
}

​ 2.

1
2
3
4
5
6
7
8
9
10
11
public class ThreadTest{
public static void main(String[] args){
new Thread(new Runnable(){
@Override
public void run(){
多线程执行的内容;
}
}).start();

}
}

线程常用的三个方法

  1. String getName(); 获取线程对象的名字
  2. void setName(String threadName); 修改线程的名字
  3. static Thread currentThread(); 获取当前线程对象的引用

除了使用setName修改线程的名字,还可以使用有参构造方法。但是需要在类中实现这个有参构造方法。

1
2
3
public MyThread(String threadName){
super(threadName);
}

线程生命周期的7个状态

  1. 新建状态 NEW
  2. 就绪状态
  3. 运行状态 (2-3 官方统称为可运行状态RUNNABLE)
  4. 超时等待状态 TIMED_WAITING
  5. 等待状态 WAITING
  6. 阻塞状态 BLOCKED
  7. 终止状态 TERMINATED

image-20250517163614959

线程的休眠

Thread.sleep(毫秒数);

在规定的时间内,当前线程没有权利抢夺CPU时间片了。

中断线程的休眠

  • interrupt()是一个实例方法。

  • 线程对象.interrupt();可以中断线程的休眠。(当然要放在另一个线程里使用才能起作用)

  • 底层原理是利用了异常处理机制。

    当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterrupttedException,然后捕捉异常,终止睡眠。

停止运行线程

线程对象.stop() 已经不建议使用

一般是设置一个标记,然后在线程的循环中使用if语句判断这个标记。

比如 boolean run = true; 当达到某个条件后将run改为false,然后if(run){ 运行的内容 } else{return;}

return后就终止这个线程了。

守护线程

在java中,线程被分为两类:守护线程、用户线程

  • 所有用户线程结束后,守护线程自动退出/结束。

  • 在JVM中,有一个隐藏的守护线程一直在守护着,它就是GC线程。

将线程设置为守护线程:

线程对象.setDaemon(true);

定时任务

java.util.Timer 定时器

java.util.TimerTask 定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建定时器对象(本质上就是一个线程)
Timer timer = new Timer(true); // 这里的true表示设置为守护线程

// 指定定时任务SimpleDateFormat
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2024-01-27 10:22:00");

// 匿名内部类的方式
timer.schedule(new TimerTask() {
int count = 0;
@Override
public void run() {
// 执行任务
Date now = new Date();
String strTime = sdf.format(now);
System.out.println(strTime + ": " + count++);
}
},firstTime,1000*5)

线程合并

join() 方法是一个实例方法

t.join() 是让当前线程进入阻塞状态,直到t线程结束,当前线程的阻塞状态结束。

个人理解:就是先让t线程打断当前线程自己运行,如果设置的时间结束或者t线程在时间结束前已经运行完了,那当前线程就继续执行。

image-20250517113216785

线程优先级

最低1(Thread.MIN_PRIORITY),最高10(Thread.MAX_PRIORITY)

t.setPriority(传入优先级数值)

让位

静态方法:Thread.yield()

让当前线程让位。让位不会让其进入阻塞状态,只是放弃当前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。

线程安全问题

什么情况下需要考虑线程安全问题?

  1. 多线程并发
  2. 有共享的数据
  3. 共享数据涉及修改操作

一般情况下

局部变量不存在线程安全问题。(尤其是基本数据类型,但如果是引用数据类型就另说了。)

实例变量、静态变量可能存在线程安全问题。他们存放在堆中,堆是多线程共享的。

线程同步机制——互斥锁

线程排队执行

现有t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。

1
2
3
synchronized(obj){ // obj为共享对象,在银行取款的例子中,这个共享对象就是账户
// 同步代码块
}

假设t1先抢到了CPU时间片,t1线程找到共享对象obj的对象锁后占有这把锁,t2只能在同步代码块之外等待,等t1线程执行完同步代码块之后,才会释放之前占有的对象锁。

synchronized又被称为互斥锁。

synchronized也可以作为标识符直接写在方法(实例方法、静态方法)声明上,

静态方法检测的是类锁,实例方法检测的是对象锁。

线程异步机制

线程并发执行

各自执行各自的,谁也不需要等对方。

效率高但可能存在安全隐患。

线程通信

涉及到的三个方法:

wait()notify()notifyAll()

  • 以上三个方法都是Object类的方法。

image-20250518151102310

  • 调用wait方法和notify方法是通过共享对象去调用的。

例如:obj.wait()的效果:在obj对象上活跃的所有线程进入无期限等待,直到调用了该共享对象的notify方法进行唤醒,唤醒后会接着上一次调用wait方法的位置继续执行。

  • obj.wait() 调用后会释放之前占用的对象锁。
  • obj.notify() 唤醒优先级最高的等待线程,如果优先级一样,就随机唤醒一个。
  • obj.notifyAll() 唤醒所有在该共享对象上等待的线程

image-20250518154905701

最完整的生命周期

image-20250518155732527

懒汉式单例模式的线程安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class SingletonTest {

// 静态变量
private static Singleton s1;
private static Singleton s2;

public static void main(String[] args) {

// 获取某个类。这是反射机制中的内容。
/*Class stringClass = String.class;
Class singletonClass = Singleton.class;
Class dateClass = java.util.Date.class;*/

// 创建线程对象t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});

// 创建线程对象t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
s2 = Singleton.getSingleton();
}
});

// 启动线程
t1.start();
t2.start();

try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

// 判断这两个Singleton对象是否一样。
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

}
}

/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;

private Singleton() {
System.out.println("构造方法执行了!");
}

// 非线程安全的。
/*public static Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/

// 线程安全的:第一种方案(同步方法),找类锁。
/*public static synchronized Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/

// 线程安全的:第二种方案(同步代码块),找的类锁
/*public static Singleton getSingleton() {
// 这里有一个知识点是反射机制中的内容。可以获取某个类。
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
return singleton;
}*/

// 线程安全的:这个方案对上一个方案进行优化,提升效率。
/*public static Singleton getSingleton() {
if(singleton == null){
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
}
return singleton;
}*/

// 使用Lock来实现线程安全
// Lock是接口,从JDK5开始引入的。
// Lock接口下有一个实现类:可重入锁(ReentrantLock)
// 注意:要想使用ReentrantLock达到线程安全,假设要让t1 t2 t3线程同步,就需要让t1 t2 t3共享同一个lock。
// Lock 和 synchronized 哪个好?Lock更好。为什么?因为更加灵活。synchronized代码块的大括号必须包住所有语句,而unlock()可以任意插入到一些语句中,但一定要记得执行unlock()
private static final ReentrantLock lock = new ReentrantLock();

public static Singleton getSingleton() {
if(singleton == null){

try {
// 加锁
lock.lock();
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
} finally {
// 解锁(需要100%保证解锁,怎么办?finally)
lock.unlock();
}

}
return singleton;
}
}

Lock 和 synchronized 哪个好?

Lock更好,因为更加灵活。synchronized代码块的大括号必须包住所有语句,而unlock()可以任意插入到一些语句中,但一定要记得执行unlock()

创建线程的第三种方法——未来任务

优点:可以拿到线程执行结束的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建“未来任务”对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 处理业务......
Thread.sleep(1000 * 5);
return 1;
}
});

// 创建线程对象
Thread t = new Thread(task);
t.setName("t");

// 启动线程
t.start();

try {
// 获取“未来任务”线程的返回值
// 阻塞当前线程,等待“未来任务”结束并返回值。
// 拿到返回值,当前线程的阻塞才会解除。继续执行。
Integer i = task.get();
System.out.println(i);
} catch (Exception e) {
e.printStackTrace();
}
}

创建线程的第四种方式——线程池

服务器启动时,创建N个线程对象,直接放到线程池中,需要的时候把任务交给线程池即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个线程池对象(线程池中有3个线程)
ExecutorService executorService = Executors.newFixedThreadPool(3);

// 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
});

// 最后记得关闭线程池
executorService.shutdown();

十、反射 reflect

概述

  • 后续学的大量java框架都是基于反射机制实现的。
  • 反射机制可以让程序更加灵活
  • 反射机制最核心的几个类:

​ java.lang.Class : Class类型的实例代表硬盘上某个class文件,或者说代表某一种类型

​ java.lang.reflect.Filed : 实例代表类中的属性/字段

​ java.lang.reflect.Constructor : 它的实例代表类中的构造方法

​ java.lang.reflect.Method : 它的实例代表类中的方法

获取Class的四种方式

第一种

Class c = Class.forName("完整的全限定类名");

注:

  1. 全限定类名是带有包名的,不可省略
  2. 这是个字符串参数
  3. 如果这个类根本不存在,会报异常:java.lang.ClassNotFoundException
  4. 这个方法的执行会导致类的加载动作的发生

第二种

Class c = obj.getClass();

第三种

Class c = 类名.class;

第四种——使用类加载器

1
2
3
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

Class<?> aClass = systemClassLoader.loadClass("完整的全限定类名")

Class.forName()classLoader.loadClass() 的区别:

Class.forName() :类加载时会进行初始化(静态变量赋值、静态代码块执行)。

classLoader.loadClass():类加载时不会进行初始化,直到第一次使用该类。

通过反射机制实例化对象

直接对类使用newInstance方法

1
2
3
Class userClass = Class.forName("test.User");

User user = (User)userClass.newInstance();

使用反射机制,只要修改属性配置文件就可以完成不同对象的实例化。非常灵活

1
2
3
4
5
ResourceBundle bundle = ResourceBundle.getBundle("test.classInfo");

String className = bundle.getString("className");
Class classObj = Class.forName(className);
Object obj = classObj.newInstance();

使用这种方式必须要有一个无参数构造方法。如果没有会出现异常。

Java9时被标注已过时,不建议使用。

使用构造方法实例化对象

无参构造:

1
2
3
4
5
Class userClass = Class.forName("test.User");
// 获取无参数构造方法
Constructor defaultCon = userClass.getDeclaredConstructor();
// 通过无参数构造方法实例化对象
Object obj = defaultCon.newInstance();

有参构造:

1
2
3
4
Class userClass = Class.forName("test.User");
// 获取有参构造方法
Constructor threeArgsCon = userClass.getDeclaredConstructor(String.class, double.class, String.class); // 根据参数的类型,写上对应的类
Object obj = threeArgsCon.newInstance("001215", 698.5, "未完成");

通过反射为对象属性赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Class clazz = Class.forName("com.powernode.javase.reflect.Customer");

// 获取对应的Field
Field ageField = clazz.getDeclaredField("age");

// 调用方法打破封装(原来类里设置的age是private)
ageField.setAccessible(true);

// 修改属性的值
// 给对象属性赋值三要素:给哪个对象 的 哪个属性 赋什么值
ageField.set(customer, 30);

// 读取属性的值
System.out.println("年龄:" + ageField.get(customer));

// 通过反射机制给name属性赋值,和读取name属性的值
Field nameField = clazz.getDeclaredField("name");
// 修改属性name的值
nameField.set(customer, "李四");
// 读取属性name的值
System.out.println(nameField.get(customer));

反射某一个类的方法

类加载的过程

image-20250521121116429

image-20250521121330986

虚拟机的三个类加载器

image-20250521152802090

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 通过自定义的类获取的类加载器是:应用类加载器。
ClassLoader appClassLoader = ReflectTest15.class.getClassLoader();
System.out.println("应用类加载器:" + appClassLoader);

// 获取应用类加载器
ClassLoader appClassLoader2 = ClassLoader.getSystemClassLoader();
System.out.println("应用类加载器:" + appClassLoader2);

// 获取应用类加载器
ClassLoader appClassLoader3 = Thread.currentThread().getContextClassLoader();
System.out.println("应用类加载器:" + appClassLoader3);

// 通过 getParent() 方法可以获取当前类加载器的 “父 类加载器”。
// 获取平台类加载器。
System.out.println("平台类加载器:" + appClassLoader.getParent());

// 获取启动类加载器。
// 注意:启动类加载器负责加载的是JDK核心类库,这个类加载器的名字看不到,直接输出的时候,结果是null。
System.out.println("启动类加载器:" + appClassLoader.getParent().getParent());

双亲委派机制

  1. 某个类加载器接收到加载类的任务时,通常委托给“父 类加载器”进行加载
  2. 最大的“父 类加载器”无法加载时,一级一级向下委托加载任务

作用:

  1. 保护程序的安全
  2. 防止类加载重复

image-20250521153555059

获取泛型

获取父类的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取类
Class<Cat> catClass = Cat.class;

// 获取当前类的父类泛型
Type genericSuperclass = catClass.getGenericSuperclass();
//System.out.println(genericSuperclass instanceof Class);
//System.out.println(genericSuperclass instanceof ParameterizedType);

// 如果父类使用了泛型
if(genericSuperclass instanceof ParameterizedType){
// 转型为参数化类型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
// 获取泛型数组
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 遍历泛型数组
for(Type a : actualTypeArguments){
// 获取泛型的具体类型名
System.out.println(a.getTypeName());
}
}

获取接口的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
public static void main(String[] args) {
Class<Mouse> mouseClass = Mouse.class;
// 获取接口上的泛型 类可以单继承、多实现,因此实现一个接口算一个Type,实现多个接口就需要数组了。每个接口上的泛型就是一个Type
Type[] genericInterfaces = mouseClass.getGenericInterfaces();
for (Type g : genericInterfaces) {
// 使用了泛型
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}
}
}

public class Mouse implements Flyable<String, Integer>, Comparable<Mouse>{
@Override
public int compareTo(Mouse o) {
return 0;
}
}

获取属性上的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User {
private Map<Integer, String> map;
}

public class Test {
public static void main(String[] args) throws Exception{
// 获取这个类
Class<User> userClass = User.class;
// 需要先获取属性
Field mapField = userClass.getDeclaredField("map"); // 获取公开的以及私有的
// 获取属性上的泛型
Type genericType = mapField.getGenericType();
// 用泛型了
if(genericType instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) genericType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}
}

获取方法参数、返回值上的泛型

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

public Map<Integer, Integer> m(List<String> list, List<Integer> list2){
return null;
}

}

public class Test {
public static void main(String[] args) throws Exception{
// 获取类
Class<MyClass> myClassClass = MyClass.class;

// 获取方法
Method mMethod = myClassClass.getDeclaredMethod("m", List.class, List.class);

// 获取方法参数上的泛型
Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
for(Type g : genericParameterTypes){
// 如果这个参数使用了泛型
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}

// 获取方法返回值上的泛型
Type genericReturnType = mMethod.getGenericReturnType();
if(genericReturnType instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}
}

获取构造方法函数上的泛型

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

public User(Map<String ,Integer> map){
}

}

public class Test {
public static void main(String[] args) throws Exception{
Class<User> userClass = User.class;
Constructor<User> con = userClass.getDeclaredConstructor(Map.class);
Type[] genericParameterTypes = con.getGenericParameterTypes();
for(Type g :genericParameterTypes){
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}
}
}

十一、注解

概述

什么是注解?

  1. JDK1.5引入
  2. 可以标注在类上、方法上、属性上、构造方法上、方法参数上等……
  3. 注解可以做到在不改变代码逻辑的前提下在代码中嵌入补充信息

注解与注释

注解:给编译器或者其他程序看的,程序根据注解来决定不同的处理方式

注释:给程序员看

框架

框架 = 反射 + 注解 + 设计模式

内置的注解

@Deprecated

用来标记过时的元素,在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素比如过时的类、过时的方法、过时的属性等。

@Override

修饰实例方法,则该方法必须是个重写方法,否则就会编译失败。

@SuppressWarnings(抑制警告的注解)

在实际开发中,建议尽量不要忽略警告,而是真正的去解决警告。

  • @SuppressWarnings(“rawtypes”):抑制未使用泛型的警告

  • @SuppressWarnings(“resource”):抑制未关闭资源的警告

  • @SuppressWarnings(“deprecation”):抑制使用了已过时资源时的警告

  • @SuppressWarnings(“all”):抑制所有警告

@FunctionalInterface

“函数式接口”的注解,这个是 JDK1.8 版本引入的新特性。使用@FunctionalInterface标注的接口,则该接口就有且只能存在一个抽象方法,否则就会发生编译错误。

(注意:接口中的默认方法或静态方法可以有多个。)

自定义注解

如何自定义

创建一个annotation类型的文件,在文件中写入注解的定义。

1
2
public @interface MyAnnotation {
}

注解中定义属性

属性的类型只能是:

  1. byte,short,int,long,float,double,boolean,char
  2. String、Class、枚举类型、注解类型
  3. 以上所有类型的一维数组形式
1
2
3
4
5
6
7
8
public @interface MyAnnotation {
String a;
int b default 10; // 可以使用default语句指定默认值
int flag;
}

//注解的使用
@interface(a="test",flag=0) //带默认值的属性可以不在此赋值
  • 如果属性只有一个,并且属性名是value,那使用注解的时候可以省略value这个属性名。
1
2
3
4
public @interface MyAnnotation {
String value;
}
@interface("test")
  • 如果属性是一个数组,使用注解时,数组值只有一个,数组的大括号可以省略。
1
2
3
4
5
public @interface MyAnnotation {
String[] value;
}
@interface("test1")
@interface({"test1","test2"})

元注解

@Retention:设置注解的保持性

注解存在阶段是保留在源代码(编译期),字节码(类加载)或者运行时(JVM中运行)

  • @Retention(RetentionPolicy.SOURCE):注解仅存在于源代码中,在字节码文件中不包含。

  • @Retention(RetentionPolicy.CLASS):注解在字节码文件中存在,但运行时无法获得(默认)。

  • @Retention(RetentionPolicy.RUNTIME):注解在字节码文件中存在,且运行时可通过反射获取。

@Target:设置注解可以使用的位置

  • @Target(ElementType.TYPE):作用于接口、类、枚举、注解

  • @Target(ElementType.FIELD):作用于属性、枚举的常量

  • @Target(ElementType.METHOD):作用于方法

  • @Target(ElementType.PARAMETER):作用于方法参数

  • @Target(ElementType.CONSTRUCTOR):作用于构造方法

  • @Target(ElementType.LOCAL_VARIABLE):作用于局部变量

  • @Target(ElementType.ANNOTATION_TYPE):作用于注解

  • @Target(ElementType.PACKAGE):作用于包

  • @Target(ElementType.TYPE_PARAMETER):作用于泛型,即泛型方法、泛型类和泛型接口。

  • @Target(ElementType.TYPE_USE):作用于任意类型。

@Documented:设置注解会被包含在API文档中

使用javadoc.exe工具可以从程序源代码中抽取类、方法、属性等注释形成一个源代码配套的API帮助文档,而该工具抽取时默认不包括注解内容。如果注解被@Documented标注,那么就能被javadoc.exe工具提取到API文档。

image-20250521194851842

@Inherited:被标注的注解支持继承

使用后子类会继承父类的注解。

@Repeatable:设置后可以在一个地方重复使用同一注解(java8)

@Repeatable(原注解的复数形式)

但是需要再声明一个原来注解的复数形式,并在其中包含原注解类型的数组。

1
2
3
4
5
6
7
8
9
public class Test {

@Author(name = "张三")
@Author(name = "李四")
public void doSome(){

}

}
1
2
3
4
5
6
7
8
9
@Repeatable(Authors.class)
public @interface Author {

/**
* 作者的名字
* @return 作者的名字
*/
String name();
}
1
2
3
4
5
public @interface Authors {

Author[] value();

}

反射注解

获取类上的所有注解

Annotation[] annotations = clazz.getAnnotations();

获取类上指定的某个注解

clazz.isAnnotationPresent(AnnotationTest01.class)

AnnotationTest01 an = clazz.getAnnotation(AnnotationTest01.class);

获取属性上的所有注解

Annotation[] annotations = field.getAnnotations();

获取属性上指定的某个注解

field.isAnnotationPresent(AnnotationTest02.class)

AnnotationTest02 an = field.getAnnotation(AnnotationTest02.class);

获取方法上的所有注解

Annotation[] annotations = method.getAnnotations();

获取方法上指定的某个注解

method.isAnnotationPresent(AnnotationTest02.class)

AnnotationTest02 an = method.getAnnotation(AnnotationTest02.class);

十二、网络编程

概述

网络编程的三个基本要素:

  1. IP地址:定位网络中的某台计算机
  2. 端口号port:定位计算机上的某个进程(某个应用)
  3. 通信协议:通过IP地址和端口号定位后,如何保证数据可靠高效的传输,就需要依靠通信协议。

IP地址

  • IPv4:4字节,xxx.xxx.xxx.xxx 每个xxx表示8位二进制数,范围是0-255

​ 前三个字节用于表示网络(省市区),最后一个字节用于表示主机(家门牌号)

​ 一些IP地址被保留或者被私有机构使用,不能用于公网的地址分配;还有一些IP地址被用作多播地址,仅用于特定的应用场景。因此实际可以使用的IPv4地址少于总量。

  • IPv6:16字节,由8组十六进制数表示,如 3ffe:3201:1401:1280:c8ff:fd54:db39:1984

  • 本机地址:127.0.0.1,主机名:localhost

  • 192.168.0.0-192.168.255.255为私有地址,属于非注册地址,专门为组织、机构内部使用。(用于局域网)

端口号port

用两个字节(无符号)表示的,取值范围0-65535,计算机端口号可以分为三大类:

  1. 公认端口:0-1023,被预先定义的服务通信占用(如http占用80,FTP占用21,Telnet占用23等)
  2. 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
  3. 动态/私有端口:49152~65535。

通常情况下,服务器程序使用固定的端口号来监听客户端的请求,而客户端则使用随机端口连接服务器。

OSI参考模型

image-20250522114020569

TCP/IP参考模型

image-20250522114318348

网络编程基础类

InetAddress类

  1. java.net.IntAddress类用来封装计算机的IP地址和DNS(没有端口信息),它包括一个主机名和一个IP地址,是java对IP地址的高层表示。大多数其它网络类都要用到这个类,包括Socket、ServerSocket、URL、DatagramSocket、DatagramPacket等
  2. 常用静态方法
    • static InetAddress getLocalHost() 得到本机的InetAddress对象,其中封装了IP地址和主机名
    • lstatic InetAddress getByName(String host) 传入目标主机的名字或IP地址得到对应的InetAddress对象,其中封装了IP地址和主机名(底层会自动连接DNS服务器进行域名解析)
  3. 常用实例方法
    • lpublic String getHostAddress() 获取IP地址
    • lpublic String getHostName() 获取主机名/域名

URL类

  1. URL是统一资源定位符,是互联网上资源位置和访问方法的一种简介表示。每个文件具有唯一的URL。
  2. URL由4部分组成:协议、存放资源的主机域名、端口号、资源文件名。如果未指定端口号,则使用协议默认的端口。HTTP协议的默认端口为80。
  3. URL的标准格式:<协议>://<域名或IP>:<端口>/<路径>,其中端口和路径有时可以省略。
  4. 为了方便程序员编程,JDK提供了java.net.URL类,该类封装了大量复杂的涉及从远程站点获取信息的细节,可以使用它的各种方法对URL对象进行分割、合并等处理

构造方法

1
URL url = new URL("http://127.0.0.1:8080/oa/index.html?name=zhangsan#tip");

常用方法

获取协议:url.getProtocol() 获取域名:url.getHost()

获取默认端口:url.getDefaultPort() 获取端口:url.getPort()

获取路径:url.getPath() 获取资源:url.getFile()

获取数据:url.getQuery() 获取锚点:url.getRef()

openStream():可以打开到此URL的连接并返回一个用于从该连接读入的InputStream,实现最简单的爬虫。

TCP 与 UDP 协议

Socket 套接字

  • Socket是传输层供给应用层的编程接口。使用Socket编程可以开发客户端和服务器应用程序,可以在本地网络上进行通信,也可以通过互联网在全球范围内通信。

  • TCP协议和UDP协议是传输层的两种协议。Socket编程分为TCP编程和UDP编程两类。

TCP、UDP协议

image-20250522210035291

TCP 三次握手(通道打开)

image-20250522210727148

  1. 客户端发送SYN(同步)数据包,包含客户端的初始序列号(ISN)
  2. 服务器收到SYN数据包后,发送SYN-ACK(同步确认)数据包,包含服务器的初始序列号(ISN)和对客户端ISN的确认号(ACK)
  3. 客户端收到SYN-ACK数据包后,发送ACK(确认)数据包,包含对服务器ISN的确认号(ACK)

三次握手完成后,客户端和服务器就可以开始交换数据了。

三次握手的意义:不会丢失、重复、乱序,保证数据在两个设备之间可靠地传输。

四次挥手(通道关闭)

image-20250522211206176

  1. 客户端发送FIN(结束)数据包,表示客户端已经完成数据传输,希望关闭连接。
  2. 服务器收到FIN数据包后,发送ACK(确认)数据包,表示服务器已经收到客户端的FIN数据包,同意关闭连接。
  3. 服务器发送FIN数据包,表示服务器已经完成数据传输,希望关闭连接。
  4. 客户端收到FIN数据包,发送ACK(确认)数据包。表示客户端已经收到服务器的FIN数据包,并同意关闭连接。

四次挥手完成后,客户端和服务器之间的连接就关闭了。

四次挥手的意义:不会丢失、重复、乱序,保证数据在两个设备之间可靠地传输。

基于TCP协议的编程

概述

image-20250522213820740

  1. 在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client),而在第一次通讯中等待连接的程序被称作服务端(Server)。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
  2. 套接字与主机地址和端口号相关联,主机地址就是客户端或服务器程序所在的主机的IP地址,端口地址是指客户端或服务器程序使用的主机的通信端口。在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样客户端和服务器通过套接字所建立连接并使用IO流进行通信。

Socket类

Socket实现客户端套接字。

构造方法:

public Socket(InetAddress a, int p) 创建套接字并连接到指定IP地址的指定端口号

Socket类实例方法:

public InetAddress getInetAddress() 返回此套接字连接到的远程 IP 地址

public InputStream getInputStream() 返回此套接字的输入流(接收网络消息)

public OutputStream getOutputStream() 返回此套接字的输出流(发送网络消息)

public void shutdownInput() 禁用此套接字的输入流

public void shutdownOutput() 禁用此套接字的输出流

public synchronized void close() 关闭此套接字(默认会关闭IO流)

ServerSocket类

ServerSocket类实现服务器套接字。服务器套接字等待请求通过网络传入,基于该请求执行某些操作,然后向请求者返回结果。

构造方法:

public ServerSocket(int port)

ServerSocket类实例方法:

public Socket accept() 侦听要连接到此套接字并接受它

public InetAddress getInetAddress() 返回此服务器套接字的本地地址

public void close() 关闭此套接字

十三、lambda表达式

  • 面向对象的思想
    • 只做一件事情,找一个能解决这个事情的对象,然后调用对象的方法完成这件事情。
  • 函数式编程思想
    • 只要能获得结果,谁去做的,怎么做的都不重要,重视结果,忽略实现过程

Lambda和匿名内部类的区别

  • 所需类型不同
    • 匿名内部类:可以是接口、抽象类、具体类
    • Lambda表达式:只能是接口
  • 使用限制不同
    • 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。
    • 如果接口中有多个抽象方法,就只能使用匿名内部类,而不能使用Lambda表达式。
  • 实现原理不同
    • 匿名内部类:编译之后,会生成一个单独的.class字节码文件
    • Lambda表达式:编译之后,不会生成一个单独的.class字节码文件

Lambda表达式的语法

1
2
3
4
5
(形参列表) -> {

方法体

}

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Integer> list = Arrays.asList(100,200,350,300);
// 对其进行排序
// 法一
Collections.sort(list);

// 法二:匿名内部类
Collections.sort(list, new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return o2-o1;
}
})

// 法三:Lambda表达式
Collections.sort(list,(Integer o1, Integer o2) -> {return b-a;})
// 或者
Comparator<Integer> comparator = (Integer a, Integer b) -> {return b-a};
Collections.sort(list,comparator);

Lambda 表达式的语法精简

四种情况:

  1. 形参类型可以省略,如果需要省略,则每个形参的类型都要省略。
  2. 如果形参列表只有一个形参,那么形参类型和小括号都可以省略。
  3. 如果方法体重只有一行语句,那么方法体的大括号也可以省略。
  4. 如果方法体中只有一条return语句,那么大括号可以省略,且必须去掉return关键字。

四个基本的函数式接口

名字 接口名 对应的抽象方法
消费 Consumer void accept(T t);
生产 Supplier T get();
转换 Function<T, R> R apply(T t);
判断 Predicate boolean test(T t);

Lambda表达式的方法引用(简化Lambda表达式)

方法引用的概述

我们在使用Lambda表达式的时候,如果Lambda表达式的方法体中除了调用现有方法之外什么都不做,满足这样的条件就有机会使用方法引用来实现。
在以下的代码中,在重写的apply()方法中仅仅只调用了现有Math类round()方法,也就意味着Lambda表达式中仅仅只调用了现有Math类round()方法,那么该Lambda表达式就可以升级为方法引用,案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 需求:实现小数取整的操作
// 方式一:使用匿名对象来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(function1.apply(3.14));

// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));

// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));

对于方法引用,我们可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
在Lambda表达式的方法引用中,主要有实例方法引用、静态方法引用、特殊方法引用和构造方法引用、数组引用这五种情况,接下来我们就对这五种情况进行讲解。

实例方法引用

语法:对象 :: 实例方法
特点:在Lambda表达式的方法体中,通过“对象”来调用指定的某个“实例方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过对象调用某个实例方法的返回值类型和形参列表 保持一致。
【示例】实例化Consumer接口的实现类对象,并在重写的accept()方法中输出形参的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一:使用匿名内部类来实现
Consumer<String> consumer1 = new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
consumer1.accept("hello world");

// 方式二:使用Lambda表达式来实现
Consumer<String> consumer2 = str -> System.out.println(str);
consumer2.accept("hello world");

// 方式三:使用方法引用来实现
Consumer<String> consumer3 = System.out :: println;
consumer3.accept("hello world");

【示例】实例化Supplier接口的实现类对象,并在重写方法中返回Teacher对象的姓名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Teacher teacher = new Teacher("ande", 18);
// 方式一:使用匿名内部类来实现
Supplier<String> supplier1 = new Supplier<String>() {
@Override
public String get() {
return teacher.getName();
}
};
System.out.println(supplier1.get());

// 方式二:使用Lambda表达式来实现
Supplier<String> supplier2 = () -> teacher.getName();
System.out.println(supplier2.get());

// 方式三:使用方法引用来实现
Supplier<String> supplier3 = teacher :: getName;
System.out.println(supplier3.get());

静态方法引用

语法:类 :: 静态方法
特点:在Lambda表达式的方法体中,通过“类名”来调用指定的某个“静态方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过类名调用某个静态方法的返回值类型和形参列表保持一致。

【示例】实例化Function接口的实现类对象,并在重写的方法中返回小数取整的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一:使用匿名内部类来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(function1.apply(3.14));

// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));

// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));

特殊方法引用

语法:类名 :: 实例方法
特点:在Lambda表达式的方法体中,通过方法的第一个形参来调用指定的某个“实例方法”。
要求:把函数式接口中抽象方法的第一个形参作为方法的调用者对象,并且从第二个形参开始(或无参)可以对应到被调用实例方法的参数列表中,并且返回值类型保持一致。
【示例】使用Comparator比较器,来判断两个小数的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一:使用匿名内部类来实现
Comparator<Double> comparator1 = new Comparator<Double>() {
@Override
public int compare(Double o1, Double o2) {
return o1.compareTo(o2);
}
};
System.out.println(comparator1.compare(10.0, 20.0));

// 方式二:使用Lambda表达式来实现
Comparator<Double> comparator2 = (o1, o2) -> o1.compareTo(o2);
System.out.println(comparator2.compare(10.0, 20.0));

// 方式三:使用方法引用来实现
Comparator<Double> comparator3 = Double :: compareTo;
System.out.println(comparator3.compare(10.0, 20.0));

需求:实例化Function接口的实现类对象,然后获得传入Teacher对象的姓名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方式一:使用匿名内部类来实现
Teacher teacher = new Teacher("ande", 18);
Function<Teacher, String> function1 = new Function<Teacher, String>() {
@Override
public String apply(Teacher teacher) {
return teacher.getName();
}
};
System.out.println(function1.apply(teacher));

// 方式二:使用Lambda表达式来实现
Function<Teacher, String> function2 = e -> e.getName();
System.out.println(function2.apply(teacher));

// 方式三:使用方法引用来实现
Function<Teacher, String > function3 = Teacher :: getName;
System.out.println(function3.apply(teacher));

构造方法引用

语法:类名 :: new
特点:在Lambda表达式的方法体中,返回指定“类名”来创建出来的对象。
要求:创建对象所调用构造方法形参列表 和 函数式接口中的方法的形参列表 保持一致,并且方法的返回值类型和创建对象的类型保持一致。
【示例】实例化Supplier接口的实现类对象,然后调用重写方法返回Teacher对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方式一:使用匿名内部类来实现
Supplier<Teacher> supplier1 = new Supplier<Teacher>() {
@Override
public Teacher get() {
return new Teacher();
}
};
System.out.println(supplier1.get());

// 方式二:使用Lambda表达式来实现
Supplier<Teacher> supplier2 = () -> new Teacher();
System.out.println(supplier2.get());

// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类的无参构造方法
Supplier<Teacher> supplier3 = Teacher :: new;
System.out.println(supplier3.get());

【示例】实例化Function接口的实现类对象,然后调用重写方法返回Teacher对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方式一:使用匿名内部类来实现
Function<String, Teacher> function1 = new Function<String, Teacher>() {
@Override
public Teacher apply(String name) {
return new Teacher(name);
}
};
System.out.println(function1.apply("ande"));

// 方式二:使用Lambda表达式来实现
Function<String, Teacher> function2 = name -> new Teacher(name);
System.out.println(function2.apply("ande"));

// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类name参数的构造方法
Function<String, Teacher> function3 = Teacher :: new;
System.out.println(function3.apply("ande"));

数组引用

语法:数组类型 :: new
特点:在Lambda表达式的方法体中,创建并返回指定类型的“数组”。
要求:重写的方法有且只有一个整数型的参数,并且该参数就是用于设置数组的空间长度,并且重写方法的返回值类型和创建数组的类型保持一致。
【示例】实例化Function接口的实现类对象,并在重写方法中返回指定长度的int类型数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方式一:使用匿名内部类来实
Function<Integer, int[]> function1 = new Function<Integer, int[]>() {
@Override
public int[] apply(Integer integer) {
return new int[integer];
}
};
System.out.println(Arrays.toString(function1.apply(10)));

// 方式二:使用Lambda表达式来实现
Function<Integer, int[]> function2 = num -> new int[num];
System.out.println(Arrays.toString(function2.apply(20)));

// 方式三:使用方法引用来实现
Function<Integer, int[]> function3 = int[] :: new;
System.out.println(Arrays.toString(function3.apply(30)));

Lambda在集合当中的使用

为了能够让Lambda和Java的集合类集更好的一起使用,集合当中也新增了部分方法,以便与Lambda表达式对接,要用Lambda操作集合就一定要看懂源码。

forEach()方法

在Collection集合和Map集合中,都提供了forEach()方法用于遍历集合。
在Collection集合中,提供的forEach()方法的形参为Consumer接口(消费型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】遍历List集合中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Integer> list = Arrays.asList(11, 22, 33, 44, 55);
// 方式一:使用匿名内部类来实现
list.forEach(new Consumer<Integer>() {
/**
* 获得遍历出来的元素
* @param element 遍历出来的元素
*/
@Override
public void accept(Integer element) {
System.out.println(element);
}
});

// 方式二:使用Lambda表达式来实现
list.forEach(element -> System.out.println(element));

// 方式三:使用方法引用来实现
list.forEach(System.out :: println);

【示例】遍历Set集合中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.forEach(new Consumer<String>() {
/**
* 获得遍历出来的元素
* @param element 遍历出来的元素
*/
@Override
public void accept(String element) {
System.out.println(element);
}
});
// 方式二:使用Lambda表达式来实现
hashSet.forEach(element -> System.out.println(element));

// 方式三:使用方法引用来实现
hashSet.forEach(System.out :: println);

在Map集合中,提供的forEach()方法的形参为BiConsumer接口,而BiConsumer接口属于两个参数的消费型接口,通过该方法再配合Lambda表达式就可以遍历Map集合中的元素。
【示例】遍历Map集合中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 实例化Map集合并添加键值对
HashMap<String, String> map = new HashMap<>();
map.put("张三", "成都");
map.put("李四", "重庆");
map.put("王五", "西安");
// 方式一:使用匿名内部类来实现
map.forEach(new BiConsumer<String, String>() {
/**
* 获得遍历出来的key和value
* @param key 键
* @param value 值
*/
@Override
public void accept(String key, String value) {
System.out.println("key:" + key + ",value:" + value);
}
});
// 方式二:使用Lambda表达式来实现
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));

removeIf()方法

在Collection集合中,提供的removeIf()方法的形参为Predicate接口(判断型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】删除List集合中的某个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建List集合并添加元素
List<String> list = new ArrayList<>(Arrays.asList("aa", "bb", "cc", "dd"));
// 方式一:使用匿名内部类来实现
list.removeIf(new Predicate<String>() {
/**
* 删除指定的某个元素
* @param element 用于保存遍历出来的某个元素
* @return 返回true,代表删除;返回false,代表不删除
*/
@Override
public boolean test(String element) {
return "bb".equals(element);
}
});
System.out.println(list); // 输出:[aa, cc, dd]

// 方式二:使用Lambda表达式来实现
list.removeIf("cc" :: equals);
System.out.println(list); // 输出:[aa, dd]

【示例】删除Set集合中的某个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.removeIf(new Predicate<String>() {
/**
* 删除指定的某个元素
* @param element 用于保存遍历出来的某个元素
* @return 返回true,代表删除;返回false,代表不删除
*/
@Override
public boolean test(String element) {
return "bb".equals(element);
}
});
System.out.println(hashSet); // 输出:[aa, cc, dd]

// 方式二:使用Lambda表达式来实现
hashSet.removeIf("cc" :: equals);
System.out.println(hashSet); // 输出:[aa, dd]