集合进阶

集合体系结构

  • Collection(单列集合):添加的元素是有序、可重复、有索引
    是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的。
  • Map(双列集合):添加的元素是无序、不重复、无索引

image-20230926160413296

Collection的遍历方式

  • 迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历

迭代器遍历

1
2
3
4
5
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
}

迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式。

细节注意点:

  1. 迭代器在遍历集合时不依赖索引。
  2. 如果当前位置没有元素,还要强行获取,报错NoSuchElementException。
  3. 迭代器遍历完毕,指针不会复位。
  4. 迭代器遍历时,不能用集合的方法进行增加或删除。

列表迭代器遍历

增强for遍历

增强for的底层就是迭代器,为了简化迭代器的代码书写的。
JDK5之后出现,其内部原理就是一个iterator迭代器。
所有的单列集合数组才能用增强for进行遍历。

格式:

1
2
3
for (String s : list) {
System.out.println(s);
}

细节:修改增强for中的变量,不会改变集合中原本的数据。它是一个第三方变量。

Lambda表达式遍历

1
coll.forEach(s -> System.out.println(s));

image-20231010143951282

普通for循环(因为List集合存在索引)

泛型

是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。

好处:

  1. 统一数据类型。
  2. 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。

细节:

  1. 只能支持引用数据类型,不能写基本数据类型。
  2. 指定泛型的具体类型后,传递数据时,可以传入该类型或者其子类类型。
  3. 如果不写泛型,类型默认是Object。

泛型可以在很多地方进行定义。

image-20231011163451900

多线程

并发:在同一时刻,有多个指令在单个CPU上交替执行。
并行:在同一时刻,有多个指令在单个CPU上同时执行。

多线程的三种实现方式

继承Thread类的方式

  1. 自己定义一个类继承Thread
  2. 重写run方法
  3. 创建子类的对象,并启动线程
1
2
3
4
5
6
7
8
9
public class MyThread extends() Thread {
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "HelloWorld");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

t1.setName("Thread 1");
t2.setName("Thread 2");

// 注意:不是t1.run();
t1.start();
t2.start();
}
}

实现Runnable接口的方式

  1. 自定定义一个类,实现Runnable接口
  2. 重写里面的run方法
  3. 创建这个类的对象
  4. 创建一个Thread类的对象,并开启线程
1
2
3
4
5
6
7
8
9
10
11
public class MyRun implements Runnable {
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
// 获取当前线程的对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + "HelloWorld");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThreadDemo {
public static void main(String[] args) {
// 创建MyRun的对象
// 表示多线程要执行的任务
MyRun mr = new MyRun();

// 创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);

// 给线程设置名字
t1.setName("Thread 1");
t2.setName("Thread 2");

// 开启线程
t1.start();
t2.start();
}
}

利用Callable接口和Future接口方式实现(作为对前两种的补充)

  1. 创建一个类MyCallable实现Callable接口
  2. 重写call(有返回值,表示多线程运行的结果)
  3. 创建MyCallable的对象(表示多线程要执行的任务)
  4. 创建Future接口的实现类FutureTask的对象(作用:管理多线程运行的结果)
  5. 创建Thread类的对象,并启动(表示线程)
1
2
3
4
5
6
7
8
9
10
11
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求1 ~ 100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + 1;
}
return sum;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadDemo {
public static void main(String[] args) {
// 创建MyCallable的对象
MyCallable mc new MyCallable();
// 创建FutureTask的对象
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建线程的对象
Thread t1 = new Thread(ft);
// 启动线程
t1.start();
// 获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}

三种实现方式的对比

  • 前两种方式无法获取多线程运行的结果,第三种则可以。

image-20231224231144784

Thread的常见成员方法

image-20231224233551289

  • 如果不给线程设置名字,它有有默认名字,格式为:Thread-X(X是序号,从0开始)。

  • JVM虚拟机启动后,会自动启动多条线程,其中有一条线程就是main线程,并执行里面的代码。

线程的优先级

计算机中线程的调度方式有两种:

  1. 抢占式调度 - 随机性。
  2. 非抢占式调度 - 轮流。

Java采用的是抢占式调度,存在随机性。

image-20231224235703844

最小值为1,默认值为5,最大值为10。

守护线程

image-20231225001146689

当其他的非守护线程执行完毕后,守护线程会陆续结束。

出让线程 / 礼让线程

出让当前CPU的执行权。(会让线程交替均匀一点,但不是绝对的。用的比较少。)

image-20231225001223643

插入线程 / 插队线程

image-20231225001345448

线程的生命周期

  • sleep()方法会让线程睡眠,睡眠时间到了之后,会立即执行后面的代码吗?
    不会。

image-20231225002436983

线程的安全问题

同步代码块

关键字:synchronized
格式:

1
2
3
synchronized (锁对象) {

}

特点:

  1. 锁默认打开,有一个线程进去了,锁自动关闭。
  2. 里面的代码全部执行完毕,线程出来,锁自动打开。

注意:

  1. 锁对象必须是唯一的。
    一般写当前类的字节码文件对象:当前类.class
    如果要写定义的Object类对象,那一定要加static修饰。
  2. synchronized的同步代码块不能写在循环外面

同步方法

把synchronized关键字加到方法上。
格式:

1
修饰符 synchronized 返回值类型 方法名(方法参数) {...}

特点:

  1. 锁住方法中的所有代码。
  2. 锁对象不能自己指定。
    方法是非静态的:this。
    方法是静态的:当前类的字节码文件对象。

多线程写法

  1. 循环
  2. 同步代码块 synchronized (锁对象) {}
  3. if (到了末尾)
  4. if (还没到末尾) { 执行核心逻辑 }
1
2
3
4
5
6
7
8
9
10
11
12
// 1. 循环
whlie (true) {
// 2. 同步代码块(可改成同步方法,Idea快捷键Ctrl+Alt+M)
synchronized (MyRunnable.class) {
if (ticket == 100) { // 3. 判断共享数据是否到了末尾,如果是
break;
} else { // 4. 如果否
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票。");
}
}
}

Lock锁

image-20231225014408655

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static Lock lock = new ReentrantLock();

public void run() {
// 1. 循环
while (true) {
// 2. 同步代码块
lock.lock();
try {
if (ticket == 100) { // 判断
break;
} else { // 判断
Thread.sleep(10);
ticket++;
System.out.println(...);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}

死锁

等待唤醒机制(生产者和消费者)

基本写法

image-20231230073007164

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadDemo {
public static void main(String[] args) {
Cook c = new Cook();
Foodie f = new Foodie();

c.setName("厨师");
c.setName("吃货");

c.start();
f.start();
}
}
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
public class Cook extends Thread {
@Override
public void run() {
while (ture) {
synchronized (Desk.lock) {
if (Desk.count == 0) break;
else {
// 判断桌子上是否有面条
if (Desk.foodFlag == 1) { // 如果有就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else { // 如果没有就开始做面条
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
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
public class Foodie extends Thread {
@Override
public void run() {
while (ture) {
synchronized (Desk.lock) {
if (Desk.count == 0) break;
else {
// 判断桌子上有没有面条
if (Desk.foodFlag == 0) { // 如果没有就等待
try {
Desk.lock.wait(); // 要让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else { // 如果有,就开始吃
// 把吃的总数-1
Desk.count--;

System.out.println("吃货在吃面条,还能再吃 " + Desk.count + "碗");
// 吃完后唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子上的食物状态
Desk.foodFlag = 0;
}
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
public class Desk { // 控制生产者和消费者的执行
// 0:没有面条 1:有面条
public static int foodFlag = 0;

// 总个数
public static int count = 10;

// 锁对象
public static Object lock = new Object();
}

阻塞队列实现等待唤醒机制

image-20231230081248698

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadDemo {
public static void main(String[] args) {
// 1. 创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

// 2. 创建线程的对象,并把阻塞队列传递进去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);

c.start();
f.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;

public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断地把面条放入阻塞队列
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;

public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断地从阻塞队列中获取面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

注意:这里可能出现连续打印同一个线程的语句的情况,这是因为打印语句是写在锁的外面,不用担心对共享数据的安全产生影响,只是说阅读起来不太方便罢了。

线程状态

image-20231230094634460

  • 新建状态 NEW -> 创建线程对象
  • 就绪状态 RUNNABLE -> start方法
  • 阻塞状态 BLOCKED -> 无法获得锁对象
  • 等待状态 WAITING -> wait方法
  • 计时等待状态 TIMED_WAITING -> sleep方法
  • 结束状态 TERMINATED -> 全部代码运行完毕

线程池

核心原理

  1. 创建一个空的池子。
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程就归还给池子,下一次提交任务时,就不需要创建新的线程,直接复用已有的线程即可。
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。

自定义线程池

image-20231230103632888

不断地提交任务,会有以下三个临界点:
① 当核心线程满时,再提交任务就会排队;
② 当核心线程满、队伍满时,就会创建临时线程;
③ 当核心线程满、队伍满、临时线程满时,就会触发任务拒绝策略。

1
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量, 最大线程数量, 空闲线程最大存活时间, 任务队列, 创建线程工厂, 任务的拒绝策略);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ThreadPoolExecutor pool = new ThreadPoolExecutor(
/**
* 参数1:核心线程数量(不能小于0)
* 参数2:最大线程数(不能小于0,最大数量 >= 核心线程数量)
* 参数3:空闲线程最大存活时间(不能小于0)
* 参数4:时间单位(用TimeUnit指定)
* 参数5:任务队列(不能为null)
* 参数6:创建线程工厂(不能为null)
* 参数7:任务的拒绝策略(不能为null)
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,
6,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
ThreadPoolExecutor
);
)

线程池多大才合适

  • CPU密集型运算:最大并行数 + 1
  • I/O密集型运算:最大并行数 * 期望CPU利用率 * (CPU计算时间 + CPU等待时间) / CPU计算时间

反射

反射允许对封装类的字段(成员变量)、方法和构造函数的信息进行编程访问。

image-20231224101002660

获取class对象的三种方式

  1. Class.forName("全类名")

最常用。
2. 类名.class
更多的是作为参数进行传递。
3. 对象.getClass()
当我们有了这个类的对象时,才可以使用。
image-20231224101535092

不能用获得的构造方法 / 反射直接创建对象,如果要这么做,要先调用一个方法:

1
2
// 暴力反射:表示临时取消权限校验
con.setAccessible(true);

反射获取构造方法

image-20231224105606873

反射获取成员变量

image-20231224105650675

反射获取成员方法

image-20231224111032236

注意:getMethods()获取的方法包括父类中所有的公共方法

Object invoke(Object obj, Object… args):运行方法
参数1:用obj对象调用该方法
参数2:调用方法传递的参数(如果没有就不写)
返回值:方法的返回值(如果没有就不写)

动态代理

思想分析

  1. 为什么需要代理?
    代理可以无侵入式地给代码增加其他功能。

调用者 -> 代理 -> 对象

  1. 代理长什么样?
    代理里面就是对象要被代理的方法。

  2. Java通过什么来保证代理的样子?
    通过接口保证,后面的对象和代理需要实现同一个接口。接口中就是被代理的所有方法。

代码实现

image-20231224114456866

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 Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}

interface Hello {
void morning(String name);
}