Java基础(黑马)

提示忽略大小写

image-20231201222339340

运算符

字符串和字符的加操作

字符+数字或者字符+数字时,会把字符通过ASCII码表查询到对应的数字再进行计算。

image-20231203214853071

逻辑运算符

短路运算符

例如在登录程序中,判断用户名**&&**密码

1、用户名正确,需要判断密码

2、用户名错误,无需判断密码

image-20231130222117356

数组

image-20231203221258452

数组的定义

image-20231203221737720

数组的静态初始化

完整格式

数据类型[] 数组名 = new 数据类型[] {元素1, 元素2 …}

简化格式

数据类型[] 数组名 = {1,2,3,4,5}

1
2
int[] arr = new int[] {1,2,3,4,5};
int[] arr = {1,2,3,4,5};

idea中打印地址值

[I@1b6d3586

  • [ 表示当前是一个数组

  • I 表示当前数组中的元素都是int类型的

  • @ 表示一个间隔符号(固定格式)

  • 1b6d3586 这部分才是数组真正的地址值(十六进制)

arr.fori 可以在idea中快速的遍历数组

数组的动态初始化

数据类型[] 数组名 = new 数据类型[数组长度]

1
int[] arr = new int[10];

在创建的时候,由我们自己制定数组的长度,由虚拟机给出默认的初始化值

数组默认初始化值

整数类型:默认初始化值是0

小数类型:默认初始化值是0.0

字符类型:默认初始化值是/u0000 展现形式是一个空格

布尔类型:默认初始化值是false

引用数据类型:默认初始化值是null

image-20231204103755487

数组的内存图

Java的内存分配

image-20231204131458724

  • 栈(内存)
    • 方法运行时使用的内存,比如main方法运行,进入方法栈中执行
  • 堆(内存)
    • 存储对象或者数组,new创建来的都是存储在堆内存
  • 方法区
    • 存储可以运行的.class文件
  • 本地方法栈
    • JVM在使用操作系统功能的时候使用,与开发无关
  • 寄存器
    • 给CPU使用,与开发无关

image-20231204131655243

image-20231204132211905

数组的常见问题

  • 索引越界问题

注意访问数组的下标是在合法区间的

数组常见操作

  • 求最值
  • 求和
  • 交换数据
  • 打乱数据(抽奖优化)

image-20231204105834849

  • 生成随机数

Java中生成随机数使用

1
2
Random r = new Random();
int number = r.nextInt(100)+1;

Random括号中的参数是范围,左闭右开

二维数组

二维数组静态初始化

初始化的时候可以写成多行,一行一个一维数组,这样方便我们进行查看

image-20231204214318774

二维数组的动态初始化

image-20231204220823419

image-20231204220705518

image-20231204220753402

方法

方法是程序中最小的执行单元

方法命名规范:首字母小写和驼峰命名相结合 例如getResult()

方法的格式

方法的定义与调用

  • 最简单的方法调用
  • 带参数的方法定义
  • 带返回值的方法定义

带参数方法的定义

image-20231204140009375

带参数方法的调用

image-20231204140024968

形参和实参

  • 形参:指方法定义中的参数
  • 实参:方法调用中的参数

带返回值的方法定义

带返回值的方法调用

  • 直接调用
  • 赋值调用
  • 输出调用

image-20231204144450184

  • 方法与方法之间是平级关系,不能互相嵌套定义

方法的重载

在同一个类中,定义了多个同名方法,这些同名的方法具有同种功能。

每个方法有不同的参数类型或者参数个数,这些同名的方法构成了重载关系

方法名相同,参数不同的方法 | ==与返回值无关==

参数不同:个数不同、类型不同、顺序不同

定义方法时候可以把相同功能的定义成一样的名字:

  • 定义方法时候可以不用那么多单词
  • 调用方法时候也不需要那么麻烦了

输出语句

println(“abc”) //先打印abc再进行换行

print(“abc”) //只打印abc不进行换行

方法的基本内存原理

  1. 方法的基本内存原理
  2. 方法传递基本数据类型内存原理
  3. 方法传递引用数据类型的内存原理

什么是基本数据类型

  • 整数类型
  • 浮点数类型
  • 布尔类型
  • 字符类型

什么是引用数据类型

  • 除了上面的其他所有类型

image-20231204170715357

image-20231204170916665

方法值传递

面向对象

image-20231205111214793

类和对象

  • 类(设计图):是对象共同特征的描述
  • 对象:是真实的具体的东西

需要先设计类,在实例化对象

image-20231205111701386

定义类的补充注意事项

  • 用来描述一类事物的类,专业叫做 ==Javabean类==
    • 在Javabean类中是不写main方法的
  • 在之前编写的main方法类,叫做==测试类==
    • 可以在测试类中创建javabean类的对象并进行赋值调用

image-20231205112812409

image-20231205112844516

类名首字母要大写、英文、有意义,满足驼峰规则,不能使用关键字,满足标识符规定

image-20231205114351233

封装

image-20231205130854753

private关键字

  • 是一个权限修饰符
  • 可以修饰(成员变量和成员方法)

image-20231205132524442

就近原则和this关键字

this的作用是可以区分成员变量和局部变量

构造方法

作用:在创建对象的时候给成员变量进行赋值

image-20231205193751144

构造方法的定义

  • 如果没有定义构造方法,系统会给出默认的无参构造方法
    • 这时候可以直接使用new Teacher()这样的方式去定义对象
  • 如果定义了有参的构造方法,系统则不会提供默认的构造方法
    • 如果这时候使用new Teacher()空参去定义对象则会报错

有参构造和无参构造没有返回值,并且这两者属于构造方法的重载

因为后面无论是否使用,都需要将两种构造方法都写出来

image-20231205195748933

image-20231205195824760

构造方法

  • 无参构造方法
    • 初始化对象时,成员变量的数据均采用默认值。
    • 初始化对象时,同时可以为对象进行赋值。
  • 有参构造方法

标准的Javabean类

插件==PTG== 可以快速生成一个标注的Javabean类

image-20231205200105002

对象内存图

  • 一个对象的内存图
  • 多个对象的内存图
  • 两个变量指向同一个对象的内存图
  • this的内存原理
  • 基本数据类型和引用数据类型的区别
  • 局部变量和成员变量的区别

一个对象的内存图

image-20231205201646276

image-20231205202524019

main方法执行完毕之后,程序会退出栈,之前定义的对象所占用的堆内存空间也就变成了垃圾

两个对象的内存图

第二次实例化同一个对象的时候不会再重复加载class文件

其余的过程和一个对象的内存图差不多

两个引用指向同一个对象

修改其中一个引用会对另一个引用进行覆盖

this的内存原理

如果没有使用this,在变量进行使用的时候会根据就近原则进行选择

this 可以区分局部变量和成员变量

this ==的本质是表示所在方法调用者的地址值==

image-20231205204949671

成员变量和局部变量的区别

成员变量:类中方法外的变量

  • 位于堆内存中

局部变量:方法中的变量(对象中的方法内)

  • 位于栈内存中(具体是在栈中方法内)

image-20231205205423943

综合案例

字符串

API帮助文档

image-20231207131510925

字符串在开发中的场景

  • 登录
    • 账号
    • 密码 校验
  • 字符串敏感词替换 ***
  • 人民币转大写

字符串相关API

  • String
  • ==StringBuilder==
  • StringJoiner
  • StringBuffer
  • Pattern
  • Matcher

字符串的底层原理

字符串的练习题

String

java.lang.String 代表字符串类,java程序中的所有字符串文字(例如”abc”)都是此类的对象

字符串的内容是不会发生改变的,他的对象在创建后不能被更改

字符串为什么要设计成不可变类型?

  • 保存字符串的数组被final关键字修饰并且为私有,同时String类没有提供/暴露修改这个字符串的方法。
  • String类被final关键字修饰导致其不能被继承,进而避免了子类破坏String不可变

String中的对象是不可变的,也可以理解为常量,线程安全。

StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对于String类型进行改变的时候,都会生成一个新的String对象,然后指针指向新的String对象。StringBuffer每次会对Stringbuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下,StringBuffer性能比StringBuilder低10%-15%左右,但是在性能上是更加安全的。

总结:

  • 少用String
  • 单线程下对字符串缓冲区操作大量数据:适用StringBuilder
  • 多线程下对字符串缓冲区操作大量数据:使用StringBuffer

image-20231207131915963

image-20231207131954075

创建String对象的两种方式

  • 直接赋值

    • String name = "abc"
      
      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

      + new

      + 通过空参构建一个空白的字符串
      + 通过带参构造方法构初始化一个字符串对象
      + 通过字符数组来创建字符串对象
      + 通过字节数组来创建字符串对象(字符所对应的ASCII码表中的数字!)




      ![image-20231207132048098](https://cdn.jsdelivr.net/gh/GuoXianSen/pic-bed@main/imgs/202312071332083.png)

      ```java
      //1、使用直接赋值的方式获取一个字符对象
      String s1 = "abc";
      System.out.println(s1);
      //2、使用new的方式来获取一个字符串对象
      //空参构造:可以获取一个空白的字符串对象
      String s2 = new String();
      System.out.println(s2);

      //传递一个字符串,根据传递进来的字符串创建一个新的字符串对象
      String s3 = new String("abc");
      System.out.println(s3);

      //传递进来一个字符数组,根据字符数组的内容创建一个新的字符串对象
      char[] chs = new char[]{'a','b','c','d'};
      String s4 = new String(chs);
      System.out.println(s4);

      //传递进来一个字节数组,根据字节数组的内容创建一个字符串对象 转换为ASCII码表中对应的字符
      //应用场景:在网络中以后传递的都是字节信息 要把字节信息转换成字符串就可以用到这个方法
      byte[] bytes = new byte[]{97,98,99,100};
      String s5 = new String(bytes);
      System.out.println(s5);

字符串创建的内存分析

image-20231207133312860

StringTable(串池)在JDK7之后就从方法区挪到了堆内存

在使用直接赋值方式创建字符串的时候,如果发现串池中存在该字符串,则会直接将已存在的字符串拿过来==复用==节省空间,如果不存在才会创建一个新的字符串

image-20231207133532188

手动new出来的字符串对象内存分析

如果出现重复的字符串则会多次占用内存空间

image-20231207134049016

1
2
3
4
5
6
String aa = "abc";
String bb = "abc";
System.out.println(aa == bb);
String aaa = new String("abc");
String bbb = new String("abc");
System.out.println(aaa == bbb);

上面的这个代码中:

==如果是比较引用数据类型,则会比较两个引用是否相等,java在进行编译的时候如果是字符串常量会放在常量池中,因此aa和bb都是引用的字符串常量池中的abc,所以==比较的引用对象是相等的.

对于第二个部分,是创建了两个新的字符串对象,这个字符串对象会被存储在堆内存中的不同位置,因此aaa和bbb的引用是不同的,所以会输出false。

如果说在项目中需要比较两个对象的值是否相等,可以使用继承自父类的equals()方法进行比较,String对象也重写了这个方法,会进行值的比较。

总结

使用直接赋值的方式创建字符串简单并且还会节约内存

Java的常用方法(比较)

==号比较的是什么内容?

  • 如果比较的是基本数据类型,则比较的是两者之间的==数据值==是否相等
  • 如果比较的是引用数据类型,则比较的是两者之间的==地址值==是否相等

这里的引用数据类型包括

  • 接口
  • 数组
  • 枚举类型
  • 注解类型
  • 字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.字符串;

public class StringDemo02 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2); //true

String s3 = new String("abc");
String s4 = "abc";
System.out.println(s3==s4); //false
// 比较字符串对象中的内容是否相等
System.out.println(s3.equals(s4)); //true

}
}

比较方法

  • boolean equals(要比较的字符串) // 完全一样的结果才是true,否则为false

  • boolean equalsIgnoreCase(要比较的字符串) // 忽略大小写的比较 只能是忽略英文状态下的大小写

    • 可以用于验证码的比较

    键盘录入的字符串信息在底层源码中是new出来的,存在堆内存中,所以和直接赋值的字符串对象用==比较 结果为false

1
2
3
4
5
6
7
8
9
10
11
12
package com.字符串;

import java.util.Scanner;

public class StringDemo03 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String s1 = sc.next();
String s2 = "abc";
System.out.println(s1==s2);
}
}

上面的例子结果为false

所以以后再比较字符串内容的时候,需要使用字符串的方法

image-20231207135747775

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
package com.字符串;

import java.util.Scanner;

public class StringDemo04 {
public static void main(String[] args) {
String username = "111";
String passwd = "111";
int flag = 3; // 三次输入机会
Scanner sc = new Scanner(System.in);
while (flag > 0) {
flag--;
System.out.print("请输入用户名:");
String inputUsername = sc.next();
System.out.print("请输入密码:");
String inputPasswd = sc.next();
//短路运算符
if (username.equals(inputUsername) && passwd.equals(inputPasswd)) {
System.out.println("登录成功!");
break;
} else {
if (flag > 0) {
System.out.println("用户名或密码错误,请重新登录,还剩" + flag + "次机会!");
}else {
System.out.println("您的账户被锁定,请联系管理!");
}
}
}
}
}

练习

遍历字符串中的字符

  • charAt(int index)
  • length()

image-20231207141223070

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.字符串;

import java.util.Scanner;

public class StringDemo05 {
public static void main(String[] args) {
//1.键盘录入字符串
Scanner sc = new Scanner(System.in);
System.out.print("请输入一个字符串:");
String str = sc.next();
//2.进行遍历
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
System.out.println("第" + i + "个字符为:" + ch);
}
}
}

统计字符个数

image-20231207141701381

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
package com.字符串;

import java.util.Scanner;

public class StringDemo06 {
/**
* 统计字符串中的大小写字母和数字的个数 (仅限字母)
*
* @param args
*/
public static void main(String[] args) {
// System.out.println('a'>'b');
Scanner sc = new Scanner(System.in);
System.out.print("请输入一个字符串:");
String str = sc.next();

int big = 0;
int small = 0;
int number = 0;

for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (ch >= 'a' && ch <= 'z') {
small++;
} else if (ch >= 'A' && ch <= 'Z') {
big++;
} else {
number++;
}
}
System.out.println("大写字母有"+big+"个,"+"小写字母有"+small+"个,"+"数字有"+number+"个。");
}
}

拼接字符串和反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.字符串;

public class StringDemo08 {
public static void main(String[] args) {
String str = "abc123";
String res = reverseString(str);
System.out.println(res);
}

public static String reverseString(String str){
String res = "";
for (int i = str.length()-1; i >=0; i--) {
res += str.charAt(i);
}
return res;
}
}

金额转换

image-20231207155247595

手机号加密

15011111871 –> 150****1871

使用到substring方法

substring共有两种重载

  • 该方法作用是获取从beginIndex开始到字符串末尾的子字符串并返回

    1
    public String substring(int beginIndex)
  • 该方法作用是获取从beginIndex开始到endIndex之间的子字符串,注意是==左闭右开==,类似于Python中的切片

    1
    public String substring(int beginIndex, int endIndex)
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
package com.字符串;

import java.util.Scanner;

public class StringDemo09 {
public static void main(String[] args) {
String res = "";
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入您的手机号:");
String phonenumber = sc.next();
if (phonenumber.length() == 11) {
res = phoneModify(phonenumber);
System.out.println("加密后的手机号为:" + res);
break;
} else {
System.out.println("您输入的手机号格式不对!");
}
}
}

public static String phoneModify(String phonenumber) {
String start = phonenumber.substring(0, 3);
String end = phonenumber.substring(7);
return start + "****" + end;

}
}

敏感词替换

image-20231207164918544

String replace(旧值, 新值) 替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.字符串;

public class StringDemo10 {
/**
* 敏感词替换
* @param args
*/
public static void main(String[] args) {
//1、获取说到的话
String talk = "你真好,以后不要再玩了,TMD,CNM!";
//2、定义敏感词库
String[] arr = {"TMD","CNM","SB","MLGB"};
for (int i = 0; i < arr.length; i++) {
talk = talk.replace(arr[i],"****");
}

System.out.println(talk);
}
}

字符串拼接

StringBuilder

1
2
3
4
5
6
7
8
9
10
11
package com.字符串;

public class StringDemo11 {
public static void main(String[] args) {
String s = "";
for (int i = 0; i < 10000000; i++) {
s = s+"abc";
}
System.out.println(s);
}
}

在上面这个代码中进行字符串拼接会消耗很长的时间,但是如果使用StringBuilder可以很快的得到最终的结果

StringBuilder可以看成是一个容器,创建之后里面的内容是可变的

  • 作用:提高字符串操作效率

image-20231207171103441

Stringbuilder的构造方法

  • 空参构造:创建一个空白的可变字符串对象,不包含任何内容
  • 有参构造:根据字符串内容,来创建可变字符串对象

image-20231207171138575

StringBuilder的常用方法

  • append(任意类型) 往容器中添加内容,并返回容器本身
  • reverse() 反转容器中的内容
  • length() 返回长度(字符出现的个数)
  • toString() 将StringBuilder类型转换为String

image-20231207171317248

StringBuilder是Java已经写好的类,Java在底层对他做了一些特殊处理,打印对象不是地址值而是==属性值==

练习

对称字符串

image-20231207172936473

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
package com.字符串;

import java.util.Scanner;

public class StringBuilderDemo03 {
/**
* 判断字符串是否对称
*
* @param args
*/
public static void main(String[] args) {
String str = "123321a";
System.out.println(judgeString(str));
StringBuilder sb = new StringBuilder("123321");
System.out.println(judgeString2(sb));
System.out.println("================================");

Scanner sc = new Scanner(System.in);
System.out.print("请输入一个字符串:");
String inputstr = sc.next();
String res = new StringBuilder().append(inputstr).reverse().toString();
System.out.println(inputstr.equals(res));
}

public static boolean judgeString(String str) {
int left = 0;
int right = str.length() - 1;
while (left < right) {
if (str.charAt(left) != str.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}

public static boolean judgeString2(StringBuilder sb){
StringBuilder newsb = sb.reverse();
return newsb.equals(sb);
}

}

使用StringBuilder的场景

  • 字符串的拼接
  • 字符串的反转

拼接字符串

image-20231207173940210

image-20231207181551456

Stringjoiner

Stringjoiner跟StringBuilder一样可以看成是一个容器,创建之后里面的内容是可变的

  • 作用:提高字符串的操作效率,代码编写简洁,但是目前市场上很少人用
  • JDK8以后才有的

构造方法

image-20231207191344247

1
2
StringJoiner sj = new StringJoiner("---");
StringJoiner sj = new StringJoiner(",","[","]");

成员方法

  • add()
  • length()
  • toString() :返回一个字符串,字符串就是拼接之后的结果

image-20231207191555277

1
2
3
4
5
6
7
8
9
10
11
12
package com.字符串;

import java.util.StringJoiner;

public class StringjoinerDemo01 {
public static void main(String[] args) {
//1.创建一个对象,并指定中间的间隔符号
StringJoiner sj = new StringJoiner(",","[","]");
sj.add("aaa").add("bbb").add("ccc");
System.out.println(sj);
}
}

image-20231207192326166

字符串对象的总结

  1. String
  2. StringBuilder
  3. StringJoiner

字符串相关类的底层实现原理

  • 字符串存储的内存原理

    • 直接赋值可以服用字符串常量池中的字符串
    • new出来的不会复用,而是开辟一个新的空间(在堆内存中)
  • ==号比较的到底是什么?

    • 比较基本数据类型的时候比较数据值
    • 比较引用数据类型的时候比较地址值
  • 字符串拼接的底层原理

image-20231207193118571

image-20231207193529735

image-20231207193554024

image-20231207193947303

JDK8之后的拼接原理

image-20231207194206504

字符串拼接的时候不要直接+,会在底层创建多个对象,浪费时间和性能

  • StringBuilder提高效率的原理图

所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存

image-20231207194554171

image-20231207194753052

  • StringBuilder源码分析

image-20231207195128824

容量:最多装多少

长度:已经装了多少

  1. 初始化时候默认的容量是16 ==长度为16字节数组==
  2. 添加的内容长度小于16,则会直接存
  3. 添加的内容长度大于16会进行扩容(老容量*2 + 2 = 34)
  4. 如果扩容之后还不够,则以实际长度为准

toString()方法的底层是new了一个字符串对象

练习

  • 调整字符串

image-20231207201453668

修改字符串中的内容思路:

  • subString
  • 转换为字符数组来实现

集合

存放多个元素 –> 数组

数组长度不可变,一旦定义了就不能改变,而集合就可以解决这个问题,集体的特点就是可以进行自动扩容

==自动扩容==

数组可以存放基本数据类型和引用数据类型

ArrayList

集合 vs 数组

  • 长度
  • 存储类型
    • 数组可以存放基本数据类型和引用数据类型
    • 集合只能存引用数据类型,如果需要存基本数据类型的话,需要改成所对应的包装类

image-20231205210214373

集合包括

  • ArrayList

image-20231205210231887

ArrayList

<E> 泛型

image-20231205210702202

ArrayList构造方法

  • ArrayList()
    • 构造一个初始容量为10的空列表
  • ArrayList(int initialCapacity)
    • 构造具有指定初始容量的空列表
  • ArrayList(Collection<? extends E> c)

ArrayList成员方法

image-20231205210746377

image-20240304155249479

基本数据类型对应的包装类

image-20231205212425609

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.集合;

import java.util.ArrayList;

public class ArrayListDemo04 {
public static void main(String[] args) {
ArrayList<Character> list = new ArrayList<>();
list.add('a');
list.add('b');
list.add('c');
list.add('d');
list.add('e');

System.out.println(list);
}
}

创建自定义对象的ArrayList

image-20231205213010427

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
package com.集合;

public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public Student() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.集合;

import java.util.ArrayList;

public class ArrayListDemo05 {
public static void main(String[] args) {
//1.创建集合
ArrayList<Student> list = new ArrayList<>();
//2.创建学生对象
Student s1 = new Student("张三",24);
Student s2 = new Student("李四",25);
Student s3 = new Student("王五",26);
//3.添加元素
list.add(s1);
list.add(s2);
list.add(s3);

for (int i = 0; i < list.size(); i++) {
Student stu = list.get(i);
System.out.println(stu.getAge()+stu.getName());
}

}
}

IDEA快捷键

==Ctrl + P 可以查看有什么参数==

==Ctrl + Alt + T 查看包裹函数==

Ctrl + Alt + V 自动生成左边

Ctrl + N 搜索界面 查找相关的类或者包 点击即可查看源代码

shift + F6 批量修改变量名

Ctrl + Shift + U 小写转大写

Ctrl + H 查看继承树

IDEA设置修改

注释的时候从每一行开头进行注释修改为只从第一个字符前面开始

勾选掉方框中的勾和下面截图保持一致即可

在这里插入图片描述

设置鼠标滚轮加Ctrl进行缩放

面向对象进阶

image-20231206205114664

static

static表示静态,是java中的一个修饰符,可以修饰成员方法、成员变量

静态变量

  • 被该类所有对象共享
  • 不属于对象,属于类
  • 随着类的加载而加载,优先于对象而存在

调用方式

  • 类名调用**==(推荐)==**
  • 对象名调用

static内存图

image-20231206211910028

==共享的属性可以被定义为静态==

  • 比如同一个班级中每一个学生的老师姓名是可以使用static方法设置为静态变量的

image-20231206212017754

teacherName可以被定义为静态

静态方法和工具类

特点

  • 多用在测试类和工具类
  • Javabean类中很少会用

调用方式

  • 类名调用(推荐)
  • 对象名调用
  • javabean类

    • 用来描述一类事物的类,如Student、Teacher、Dog、Cat
  • 测试类

    • 之前写的main方法可以称之为测试类
  • 工具类

    • 帮我们做一些事情,但是不描述任何事物的类
      • 类名见名知意 例如 printArr 表示打印数组
      • 私有化构造方法 private
      • 方法定义为静态

练习

定义数组工具类

需求:在实际开发中,经常会遇到一些数组使用的工具类

请按照如下要求编写一个数组的工具类:ArrayUtil

image-20231206213156251

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
package com.面向对象进阶;

public class ArrayUtil {
private ArrayUtil() {
}

/**
* 打印数组工具类方法
* @param arr
* @return
*/
public static String printArr(int[] arr) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < arr.length; i++) {
if (i == arr.length - 1) {
sb.append(arr[i]);
} else {
sb.append(arr[i]).append(", ");
}
}
sb.append("]");
return sb.toString();
}

/**
* 计算数组平均数工具类方法
* @param arr
* @return
*/
public static double getAverage(double[] arr) {
double sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum / arr.length;
}
}

学生工具类

image-20231208132527030

Static注意事项

  • ==静态方法只能访问静态变量和静态方法==
    • 不能访问成员变量和成员方法
    • 不能访问定义在同一个类中的成员变量和成员方法
  • 非静态方法可以访问静态变量或静态方法,也可以访问非静态的成员变量和非静态成员方法
  • 静态方法中是没有this关键字的

this:表示当前方法调用者的地址值

这个this是由虚拟机赋值的,在调用的时候也不需要加上这个参数

非静态变量也被称之为实例变量

image-20231208150814971

image-20231208151222017

image-20231208151357262

单例设计模式是在多线程阶段进行讲解

重新认识main方法

image-20231208151603731

如果想要在idea中添加参数运行程序,可以在edit configuration中进行配置

image-20231208151858441

继承

封装:对象代表什么,就封装对应的数据并提供数据对应的行为

image-20231208152022072

image-20231208152234063

image-20231208152250256

image-20231208152308564

在Java中提供了一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系

1
public class Student extends Person{}
  • Student称之为子类(派生类),Person称为父类(基类或超类)

==继承的好处:==

  • 可以把多个子类中重复的代码抽取到父类中,提高代码的复用性
  • 子类可以在父类的基础上,增加其他的功能,使子类更强大

继承需要学习的点:

  • 自己设计
  • 使用别人的代码(使用别人写好的)

什么时候用继承?

①当类和类之间存在相同的内容,②并且满足子类是父类的一种,就可以考虑使用继承来优化代码

例如学生和老师类都可以继承自Person

image-20231208153053657

image-20231208153902869

继承的特点

Java只支持单继承,不支持多继承,但支持==多层继承==

  • 单继承:一个子类只能继承一个父类

  • 不支持多继承:一个子类不能同事继承多个父类

image-20231208154546575

在上面这个例子中,如果Java有多继承就会导致方法的混乱

  • 多层继承:子类A继承父类B,父类B可以继承父类C

C是A的间接父类,B是A的直接父类

每一个类都直接或者间接继承自Object

如果自己定义的一个类没有继承自某个父类,Java虚拟机在运行的过程中会自动添加一个Object父类

image-20231208154928418

子类可以使用直接父类或者间接父类中的内容,但是叔叔类中的内容是不可以使用的

image-20231208155030322

设计继承类

image-20231208160332658

父类中方法的权限修饰符如果是private,则子类就无法进行继承了

private 私有的:只能在本类中进行访问

注意事项:

  • 子类只能访问父类中非自由的成员(成员变量 | 成员方法)

子类可以从父类中继承哪些内容?(内存图/内存分析工具)

注意继承和调用是两个概念 |

image-20231208161336929

  • 构造方法是否可以被继承?

    • 不管是私有还是非私有的都无法继承
    • 构造方法的规定就是方法名必须和类名一致,继承过来的话类名就不相同了

如果一个类中没有构造方法,虚拟机会自动给你添加一个默认的空参构造

  • 成员变量是否可以继承

    • 不管是私有的成员变量还是非私有的成员变量,子类都是可以继承的
    • 私有的成员变量是可以使用get set方法进行使用
  • 成员方法是否可以继承

    • 虚方法表 可以被继承
    • 否则不能被继承

虚方法表

非private修饰的方法

非final修饰的方法

非static修饰的方法

image-20231208154032889

继承中成员变量的访问特点

  • 就近原则

现在局部位置找,然后在本类成员变量找,父类成员位置找,逐级向上

this 表示当前类的属性或者方法 指向的是调用类的实际内存地址 不写this也是默认是当前类

super 表示调用父类的方法

  • 如果出现重名的成员变量
    • name
      • 从局部的位置往上去找
    • this.name
      • 从本类成员位置往上找
    • super.name
      • 从父类的成员位置往上找

继承中成员方法的访问特点

就近原则

方法的重写:

重写的本质是覆盖了虚方法表中的方法而已

应用场景:当父类的方法不能满足子类现在的需求时,需要进行方法的重写

子类中出现和父类一模一样的方法声明,我们就称之为方法的重写

@Override重写注解

  1. @Override是放在重写后的方法上的,校验子类重写时语法是否正确
  2. 加上注解后如果有红色的波浪线,表示语法错误
  3. 建议重写的方法都加上@Override注解,代码安全、优雅

方法重写的注意事项和要求:

  • 重写方法的名称、形参列表必须和父类一致
  • 子类重写父类方法时,访问权限子类必须大于等于父类(空着不写<protected<public)
  • 子类重写父类方法时,返回值类型子类必须小于等于父类
  • 建议:重写的方法尽量和父类保持一致
  • 只有被添加到虚方法表中的方法才能被重写

image-20231211102920665

image-20231211104741907

继承中构造方法的特点

  • 父类中的构造方法不会被子类继承
  • 子类中的所有构造方法都会默认先访问父类中的无参构造,再执行自己

为什么?

  • 子类在初始化的时候,有可能会调用父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
  • 子类初始化之前一定要调用父类构造方法先完成父类数据空间的初始化

怎么调用父类的构造方法的?

  • 子类构造方法的第一行语句默认都是 super() 不写也存在且必须在第一行
  • 如果想要调用父类有参构造,必须手动写super进行调用

image-20231211105542753

继承中构造方法的访问特点

  • 子类不能继承父类的构造方法,但可以通过super调用
  • 子类构造方法的第一行,有一个默认的super()
  • 默认先访问父类中无参构造方法,在执行自己
  • 如果要访问农夫类的有参构造,必须手动书写(参考上图,参数写在super的括号中)

this、super使用总结

  • this 理解为一个变量 表示当前方法调用者的地址值
  • super 表示父类的存储空间

image-20231211110154442

image-20231211111037142

image-20231211111714824

多态

对象多种形态

什么是多态

  • 同类型的对象表现出的不同形态

多态的表现形式

  • 父类类型 对象名称 = 子类对象

多态的前提

  • 继承实现关系

    • 例如使用List<String> list = new ArrayList<>();这样的方式去定义一个list
  • 有父类引用指向子类对象 等于号左边是父类 右边是子类

  • 有方法的重写

image-20231211112043584

代码冗余

image-20231211112207850

现在需要一个通用的注册方法

image-20231211112233751

多态的好处:

  • 使用父类型作为参数,可以接收所有的子类对象
  • 体现多态的扩展性与便利性

多态调用成员的特点

  • ==变量调用:编译看左边,运行也看左边==
  • ==方法调用:编译看左边,运行看右边==

多态的优点和弊端

  • 在多态形势下,右边对象可以实现解耦合,便于扩展和维护
1
2
Person p = new Student();
p.work(); //业务逻辑发生改变时,后续代码无需修改
  • 定义方法的时候,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性和便利

多态的弊端

  • 不能调用子类的特有功能

编译看左边,检查父类是否有这个方法,如果没有则直接报错

解决方案:将调用者A直接变成子类型即可。转换的时候不能随便转,会报错

image-20231211150852695

instance of 判断是否是所对应的类型

image-20231211151217760

引用类型的类型转换

  • 自动类型转换 (由小变大–> 将子类对象赋值给父类对象)
  • 强制类型转换 (父类转换为子类对象)
    • 可以转为真正子类类型,从而调用子类独有的功能
    • 强制类型转换的时候转换类型与真实对象类型不一致会报错
    • 转换的时候用instanceof关键字进行判断

包就是文件夹。用来管理各种不同功能的java类,方柏霓后期代码维护

  • 包名规则:公司域名反写 + 包的作用,需要全部英文年小写,见名知意 com.itheima.domain

    • domain 表示文件夹内都存放javabean类

    使用import导入包

image-20231211210944422

使用其他类的规则

  • 使用同一个包中的类,不需要导包
  • 使用java.lang包中的类时,不需要导包
  • 其他情况都需要导包
  • 如果同时使用两个包中的同名类,需要使用全类名

image-20231211212119446

final

不能被修改的

  • 方法
    • 表明该方法是最终方法,==不能被重写==
    • 最终类,==不能被继承==
  • 变量
    • 常量,==只能被赋值一次==
    • Math包中的PI 表示圆周率就用final修饰

在实际开发中常量一般作为系统的配置信息,方便维护,提高可读性

常量的命名规范:

  • 单个单词:全部大写
  • 多个单词:全部大写,单词之间用下划线隔开

细节:

  • final修饰的变量是基本类型:那么变量存储的地址值不能发生改变
  • final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,对象内部可以改变

image-20231211215612100

对象的地址值不能修改,但是对象内部的成员属性是可以修改的

权限修饰符

  • 权限修饰符:用来控制一个成员能够被访问的范围
  • 可以修饰成员变量、方法、构造方法、内部类

Java中有四种权限修饰符

作用范围从小到大 private < 空着不写 < protected < public

private 私房钱,只能自己用

默认/缺省/空着不写 : 只能在本包中使用

protected:

image-20231212145710104

使用规则

实际开发中一般只用private和public

  • 成员变量私有
  • 方法公开

特例:如果方法中代码是抽取其他方法中共性代码的,这个方法一般也私有

代码块

{

}

括号中的代码,称之为代码块,共有如下几种情况:

  • 局部代码块
  • 构造代码块
  • 静态代码块

局部代码块

提前结束变量的生命周期 ==已淘汰==

构造代码块

  1. 写在成员位置的代码块
  2. 作用:可以把多个构造反复噶种的重复代码抽取出来
  3. 执行时机:在创建本类对象的时候,会先执行构造代码块再执行构造方法

渐渐淘汰了… 不够灵活

image-20231212152326355

优化方法:
image-20231212152406876

静态代码块(*)

格式:static{}

特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发,==只执行一次==

适用场景:在类加载的时候,做一些数据初始化的时候使用

学生管理系统的时候可以初始化一些用户数据,

抽象类

抽象方法子类必须要重写,否则子类报错

抽象方法所在的类是抽象类

将共性的方法抽取到父类之后,由于每一个子类执行的内容不一样,父类中无法确定具体的方法体,就可以定义抽象方法

一个类中如果存在抽象方法,那么该类就必须声明为抽象类

抽象类的定义格式

抽象方法的定义格式

  • public abstract 返回值类型 方法名(参数列表);

抽象类的定义格式:

  • public abstract class 类名 {}

抽象类和抽象方法的注意事项

  • 抽象类不能实例化
  • 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类,可以有非抽象方法
  • 抽象类可以有构造方法
  • 抽象类的子类
    • 要么重写抽象类中的所有抽象方法
    • 要么是抽象类

共性的内容放在父类,这样可以使得团队开发的时候 方法更加规范

抽象类的定义方法

子类继承抽象类之后如何重写抽象方法

抽象类的作用

  • 定义抽象方法,作为一个规范
  • 作为模板,强制子类去实现特定的行为
  • 代码的复用和扩展
  • 实现多态性

image-20231212194641135

抽象类可以先定子类方法书写的格式

接口

为什么要有接口?

游泳

理解为一个

接口的应用

  • ==接口就是一个规则,是对行为的抽象==

接口和抽象类的区别?

接口的定义和使用

  • 接口使用关键字interface来进行定义的
    • public ==interface== 接口名 {}
  • 接口不能实例化
  • 接口和类之间是实现关系,通过implements关键字来表示
    • public class 类名 ==implements== 接口名 {}
  • 接口的子类(实现类)
    • 要么实现接口中的所有抽象方法
    • 要么是抽象类

注意1:接口和类之间是实现关系,可以单实现,也可以多实现。

1
public class 类名 implements 接口名1, 接口名2 {}

注意2:实现类可以在继承一个类的同时实现多个接口

1
public class 类名 extends 父类 implements 接口名1, 接口名2 {}

注意:

接口是一种纯粹的抽象类型,只能包含抽象方法和常量

接口不能包含成员变量,但可以包含常量 stacic final类型的变量

一个类可以同时继承一个类并实现多个接口

接口中成员的特点

  • 成员变量

    • 只能是常量
    • 默认修饰符 public static final (Java自动添加的 通过内存分析工具可以查看)
  • 构造方法

    • 没有构造方法
  • 成员方法

    • 只能是抽象方法
    • 默认修饰符 public abstract 如果没有写 会自动添加
  • JDK7以前:接口中只能定义抽象方法

  • JDK8的新特性:接口中可以定义有方法体的方法

  • JDK9的新特性:接口中可以定义私有方法

image-20231213204835976

接口和类之间的关系

  • 类和类之间的关系
    • 继承关系,只能单继承,不能多继承,但是可以多层继承
  • 类和接口的关系
    • 实现关系,可以单实现,也可以多实现,还可以在继承一个类的时候同时实现多个接口
  • 接口和接口的关系
    • 继承关系,可以单继承,也可以==多继承==
    • 如果接口是实现最下面的接口的时候,就需要实现整个体系中的所有抽象方法

接口练习

一个父类中所有的子类需要是同一种事物

image-20231213211720861

接口中新增方法

在jdk7之前如果都是使用抽象方法的话,如果在父接口中新增一个抽象方法,就需要在所有继承自其的子接口中实现该抽象方法,导致牵一发而动全身。

image-20231213212322978

image-20231213212632689

JDK8以后

  • 允许在接口中定义默认方法,需要使用关键字default修饰
    • 作用:解决接口升级的问题
  • 接口中默认方法的定义格式:
    • 格式:public ==default== 返回值类型 方法名(参数列表) { }
    • 示例:public ==default== void show()P{ }

接口中默认方法的注意事项:

  • 默认方法不是抽象方法,所以不强制被重写,重写的时候去掉default关键字

  • public可以省略,default不能省略 省略了会被当成是抽象方法

  • 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写

    静态方法

  • 静态方法不能被重写

  • 如果有一个同名的函数 且没有Override,只是有一个同名的方法而已

重写是指子类把父类继承下来的虚方法表里面的方法进行覆盖了,这才叫重写

JDK9以后得私有方法

image-20231214095112172

私有方法不能被重写

image-20231214095710778

接口应用

跑步、游泳、说英语

image-20231214100426709

image-20231214100438560

  • 接口代表规则,是行为的抽象。想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了。
  • 当一个方法的参数是接口时,可以额传递接口所有实现类的对象,这种方式称之为接口多态

适配器设计模式

设计模式是一套被反复使用,多数人知晓、经过分类边牧的、代码设计经验的总结

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码的可靠性、程序的重用性。

简单理解:设计模式就是各种套路

适配器设计模式:解决接口与接口实现类之间的矛盾问题

在实现类和接口之间添加了一个第三者,并且将其设定为abstract类型接口,所有的方法全部空实现

在实现类中只实现该接口,然后只使用method5即可。

Inter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.适配器模式;

public interface Inter {
public abstract void method1();
public abstract void method2();
public abstract void method3();
public abstract void method4();
public abstract void method5();
public abstract void method6();
public abstract void method7();
public abstract void method8();
public abstract void method9();
public abstract void method10();
}

现在的需求是我需要使用当前接口中的第五个抽象方法, 在实现类中,就需要将其余的抽象方法全部实现,完全没有必要。这时候引入一个适配器

InterAdapter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.适配器模式;

public abstract class InterAdapter implements Inter{
@Override
public void method1() {}
@Override
public void method2() {}
@Override
public void method3() {}
@Override
public void method4() {}
@Override
public void method5() {}
@Override
public void method6() {}
@Override
public void method7() {}
@Override
public void method8() {}
@Override
public void method9() {}
@Override
public void method10() {}
}

在适配器中重写Inter中的全部方法,然后在真正需要使用的类中继承这个实现类重写method5即可。适配器中只是对接口的空实现,外界创建它的对象是没有意义的,因此将其设置为抽象类类型。

InterImpl.java

1
2
3
4
5
6
7
8
9
10
package com.适配器模式;

public class Intermpl extends InterAdapter{
//需要用到哪个方法就重写哪个方法

@Override
public void method5() {
System.out.println("Method5");
}
}

当一个接口中抽象方法过多,但是我只需要使用其中的一部分的时候,就可以使用适配器模式。

书写模式

  • 编写中间类xxxAdapter,实现对应的接口
  • 对接口中的抽象方法进行空实现
  • 让真正的实现类继承中间类,并重写需要使用到的方法
  • 为了避免其他类创建适配器类的对象,中间的适配器类需要使用abstract进行修饰
  • 实现类如果有父类的话,可以让中间类继承其父类

内部类

  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类

类的五大成员

属性、方法、构造方法、代码块、内部类

在一个类里面,再定义一个类,在A类的内部定义B类,B类就被称为内部类

1
2
3
4
5
public class Outer {
public class Inner {

}
}

image-20231214102744677

为什么要学习内部类?

image-20231214102923712

  • 内部表示的事物是外部类的一部分
  • 内部类单独出现没有意义

内部类的访问特点:

  • 内部类可以直接访问外部类成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.oop内部类;

import java.util.ArrayList;

public class Car {
private String carName;
int carAge;
String carColor;
public void show() {
System.out.println(carName);
Engine e = new Engine();
System.out.println(e.engineName);
}

class Engine {
String engineName;
int engineAge;

public void show() {
System.out.println(engineName);
System.out.println(carName);
}
}
}

B类表示的事物是A类的一部分,且B单独存在没有意义

比如汽车发动机,ArrayList的迭代器,人的心脏等

image-20231214104142326

内部类分类

  • 成员内部类(了解)
  • 静态内部类(了解)
  • 局部内部类(了解)
  • 匿名内部类==(掌握)==

成员内部类

  1. 成员内部类的代码如何写?
  2. 如何创建内部类对象
  3. 成员内部类如何获取外部类的成员变量

image-20231214105317494

获取内部类对象的方法

  • 在外部类中编写方法,对外提供内部类对象
    • 当内部类对象的修饰符为private的时候使用
  • 直接创建格式:外部类名.内部类名 = 对象名 = 外部类对象.内部类对象
    • 没有使用private修饰内部类对象的时候使用
1
2
3
4
5
6
7
package com.oop内部类;

public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner(); //获取成员内部类的对象
}
}

如果修饰符被定为private,则可以在外部类中创建一个返回值类型为内部类的方法,该方法就是用来创建内部类的对象实例。然后在测试类中就可以使用getInstance()方法进行调用。或者直接修改其修饰符类型为public

image-20231214110306235

内部类内存图

image-20231214141954339

内部类对象中会记录外部类对象的地址值

对于下面的程序,想要打印30、20、10,应该如何实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.oop内部类;

public class Out2er {
private int a = 10;

class Inner {
private int a = 20;

public void show() {
int a = 30;
System.out.println(a);
}
}
}

注意到在局部变量中的a=10,在内部类中的a=20,在外部类中的成员变量a=10

如果想要输出30,直接打印a即可

如果想要输出20,就需要在前面加上this.a,因为this指向的是002地址对应的的对象,其中的a=20

如果想要输出10,就需要使用Outer.this.a,因为Outer.this指向的是外部类对象,其成员变量a=10

image-20231214143114508

静态内部类

image-20231214143149167

静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的需要创建对象

注意事项:

  • 静态内部类也是成员内部类的一种
  • 静态内部类只能访问外部类中的静态变量和静态方法

创建静态内部类对象的格式:

外部类名.内部类名 对象名 = new 外部类名.内部类名()

调用静态方法的格式:

外部类名.内部类名.方法名()

类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.oopinterfacedemo02;

public class Outer {
//静态内部类
static class Inner{
public void show1(){
System.out.println("非静态的方法被调用了!");
}

public static void show2(){
System.out.println("《静态》的方法被调用了!");
}
}
}

测试类

1
2
3
4
5
6
7
8
9
10
package com.oopinterfacedemo02;

public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer.Inner();
oi.show1();
//oi.show2();
Outer.Inner.show2();
}
}

new的是外部类.内部类对象

局部内部类

  1. 将内部类定义在方法里面的就叫做局部内部类,类似于方法里面的局部变量
  2. 外界无法直接使用,需要在方法内部创建对象并使用
  3. 该类可以直接访问外部类的成员,也可以访问方法内的局部变量

匿名内部类

隐藏名字的内部类

如果前面是类就是继承关系,如果前面是接口就是实现关系,之后需要在自己内部重写所有的抽象

什么是匿名内部类?

  • 隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置

匿名内部类的格式?

  • image-20231214152024714

格式的细节?

包含了继承或者实现,方法重写和创建对象

整体就是一个类的子类或者接口的实现类对象

使用场景是什么?

当方法的参数是接口或者类的时候,以接口为例,可以传递这个接口的实现类对象

image-20231214152223868

面向对象项目(拼图游戏)

Arrays常见API

  • Arrays.sort()

该方法默认是进行升序排序,传入一个数组即可。当然也可以进行降序排序,需要传入第二个参数表示规则

lambda表达式

最明显的作用就是简化匿名内部类

例如进行在进行排序的时候,刚刚在进行升序排序的时候,可以用lambda表达式替代

函数式编程

Lambda表达式是JDK8开始后的一种新的语法形式

注意点:

  • Lambda表达式可以用来简化匿名内部类的书写
  • Lambda表达式只能简化函数式接口的匿名内部类的写法
  • 函数式接口:
    • 有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@Functionalface注解

image-20240309202828284

Lambda的省略规则:

  • 参数类型可以省略不写
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略
  • 如果Lambda表达式的方法体只有一行,大括号、分号、return可以省略不写

集合进阶

  • 集合体系结构
  • Collections集合

集合体系结构

下面的部分中,斜体加粗为接口,正常的格式为实现类

  • 单列集合(**Collection**
    • List
      • ArrayList
      • LinkedList
      • Vector
    • Set
      • HashSet
        • LinkedHashSet
      • TreeSet
  • 双列集合(Map)

双列集合的特点:

  • 双列集合一次需要存一对数据,分别为键和值
  • 键不能重复,值可以重复
  • 键和值是一一对应的,每一个键只能找到自己对应的值
  • 键+值这个整体,我们称之为”键值对“或者”键值对对象“,在Java中叫做”Entry“对象

List系列集合:添加的元素是有序【存和取的顺序是一样的】、可重复、有索引

Set系列集合:添加的元素是无序、不重复(可以利用这个特点进行去重)、无索引

Collection是单列集合的祖宗接口,它的功能是全部单列集合都是可以继承使用的。

image-20240304160209440

注意:Collection是一个接口,我们不能直接创建它的对象,只能创建它的实现类的对象

Java 集合框架概览

实现类:ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Collection<String> coll = new ArrayList<>();

// 1、添加元素
// 细节1:如果我们要往List系列集合中添加数据,那么方法永远返回true,因为List系列是允许元素重复的
// 细节2:如果我们要往Set系列集合中添加数据,如果当前添加元素不存在,方法返回true,表示添加成功
// 如果当前要添加的元素已经存在,方法返回false,表示添加失败,因为Set系列的集合不允许重复。
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
System.out.println(coll);
// 2、清空
// coll.clear();
// System.out.println(coll);
// 3、删除
System.out.println(coll.remove("aaaa"));
System.out.println(coll);

Collection的遍历方式

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

迭代器遍历

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

image-20240304162213501

五种遍历方式对比

  • 迭代器遍历

    • 在比那里的过程中需要删除元素,请使用迭代器。

    • 
      
      1
      2
      3
      4
      5
      6
      7

      + 列表迭代器

      + 在遍历的过程中需要添加元素,请使用列表迭代器。

      + ```

  • 增强for遍历

    • 仅仅想遍历,那么使用这个或者lambda都可以

    • 
      
      1
      2
      3
      4
      5

      + Lambda表达式

      + ```java
      list.forEach(s -> System.out.println(s));
  • 普通for

    • 如果遍历的时候想要操作索引,可以使用普通for

    • 
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22





      实现类 | 数据结构

      ## ArrayList源码分析

      ArrayList是一种有序的集合

      底层原理

      + 利用空参创建的集合,Java会在底层创建一个默认长度为**0**的数组
      + 添加第一个元素时,底层会创建一个新的长度为**10**的数组
      + 存满时,会扩容**1.5**倍
      + 如果一次添加多个元素,**1.5**倍放不下,则会创建新数组的长度以实际大小为准

      里面有个成员变量**size**,表示**元素个数**和**下一次元素存入的位置**

      ```java
      oldCapacity >> 1

Map常见API

Map是双列集合的顶层接口,他的功能是全部双列集合都可以继承使用的

image-20240312194407477

image-20240307201125322

方法名称 说明
V put(K key, V value) 添加元素
V remove(Object key) 根据键删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数

在添加数据的时候,如果键不存在,则直接将键值对对象添加到map集合中;如果键是存在的,那么会把原有的键值对对象覆盖,会把覆盖的值进行返回。

1
2
3
4
5
6
// 2.添加元素
map.put("123", "123");
String put = map.put("123", "222");
System.out.println(put);
map.put("123", "123");
System.out.println(map);

例如上面的代码第一行输出的内容就是原本被覆盖的内容**”123”**

put方法如果在进行覆盖的时候是有返回值的,返回值是被覆盖的元素值。如果是第一次存的时候,返回值为null

Map的遍历方式

键找值

1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, String> map = new HashMap<>();
String put = map.put("111", "111");
map.put("222", "222");
map.put("333", "333");
System.out.println(put);

// System.out.println(map.get("111"));

Set<String> keys = map.keySet();
for (String key : keys) {
String value = map.get(key);
System.out.println(key + "=" + value);
}

常用api

方法名 说明
V get(Object key) 根据键获取值
Set keySet() 获取所有键的集合
Collection values() 获取所有值的集合
Set<Map.Entry<K,V>> entrySet() 获取所有键值对对象的集合

键值对

其中最后一个方法返回值类型是一个包含键值对的对象,用getKey()获取键,用getValue()获取值。

1
2
3
4
5
6
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " = " + value);
}

lambda表达式

即forEach遍历

修改为lambda表达式之前先可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
Map<String, String> map = new HashMap<>();
map.put("鲁迅", "11111");
map.put("222", "222222");
map.put("333", "333333");

map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String s, String s2) {
System.out.println(s + " = " + s2);
}
});

其中BiConsumer是一个函数式接口,满足修改为lambda表达式的要求

HashMap

HashMap是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录为null,不支持线程同步。

HashMap中的元素实际上是对象,一些常见的基本类型需要使用给他们所对应的包装类。

HashMap是无序的,不会记录插入的顺序

HashMap继承与AbstractMap,实现了Map、Cloneable、Java.io.Serializable

img

HashMap的key与value类型可以相同也可以不同,可以是字符串(String)类型,也可以是整型(Integer)

HashMap特点

  • HashMap是Map里面的一个实现类

  • 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了

  • 特点都是由键决定的:无序、不重复、无索引

  • HashMap跟HashSet底层原理是一模一样的,都是哈希表结构

HashMap底层原理

底层是长度为16,默认加载因子为0.75的数组

利用键计算哈希值,跟值无关。

当链表的长度超过8 & 数组长度>=64,自动转成红黑树

HashMap底层是哈希表结构

依赖hashCode方法和equals方法保持键的唯一

如果键存储的是自定义对象,需要重写hashCode和equals方法

如果值存储的是自定义对象,不需要重写hashCode和equals方法

image-20240312195755389

遍历方法

  • lambda
  • entrySet

案例

需求:

image-20240312200912775

核心:HashMap的键位置如果存储的事自定义对象,需要重写hashCode和equals方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.集合;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

public class MapDemo03 {
public static void main(String[] args) {
// 1.创建HashMap对象
HashMap<Student, String> hm = new HashMap<>();

// 2.创建三个学生对象
Student s1 = new Student("zhangsan", 23);
Student s2 = new Student("wangwu", 24);
Student s3 = new Student("lsii", 25);
Student s4 = new Student("lsii", 25);

// 3.添加元素
hm.put(s1, "江苏");
hm.put(s2, "浙江");
hm.put(s3, "福建");
hm.put(s3, "山东");


Set<Student> keys = hm.keySet();
for (Student key : keys) {
String value = hm.get(key);
System.out.println(key + " = " + value);
}
System.out.println("======================================");
for (Map.Entry<Student, String> studentStringEntry : hm.entrySet()) {
Student key = studentStringEntry.getKey();
String value = studentStringEntry.getValue();
System.out.println(key + " = " + value);
}
System.out.println("======================================");
hm.forEach(
(student, s) -> System.out.println(student + " = " + s)
);

}
}

image-20240312200921761

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
package com.集合;


import java.util.*;

public class MapDemo04 {
public static void main(String[] args) {
// 定义一个数组,存储四个经典
String[] arr = {"A", "B", "C", "D"};
// 利用随机数模拟80个同学进行投票,并把投票的结果存储起来
ArrayList<String> list = new ArrayList<>();
Random r = new Random();
for (int i = 0; i < 80; i++) {
int index = r.nextInt(arr.length);
// System.out.println(arr[index]);
list.add(arr[index]);
}
HashMap<String, Integer> hm = new HashMap<>();
for (String name : list) {
if (hm.containsKey(name)) {
// 先获取当前经典已经被投票的次数
int count = hm.get(name);
count++;
// 把新的次数在添加到集合中
hm.put(name, count);
} else {
// 不存在
hm.put(name, 1);
}
}

// hm.forEach((key, value) -> System.out.println(key + " = " + value));
System.out.println(hm);

// 求最大值
int max = 0;
Set<Map.Entry<String, Integer>> entries = hm.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
int count = entry.getValue();
if (count > max) {
max = count;
}
}
System.out.println(max);

// 判断哪个景点和最大值一样
for (Map.Entry<String, Integer> entry : entries) {
int count = entry.getValue();
if (count == max) {
System.out.println(entry.getKey());
}
}

}
}

LinkedHashMap

特点:

  • 由键决定:有序、不重复、无索引

  • 这里的有序是指保证存储和取出的元素顺序一致

  • 原理:底层数据结构是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序

数组+双向链表

LRU缓存–>用Python实现过的代码也可以基于LinkedHashMap进行代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.集合;

import java.util.LinkedHashMap;

public class LinkedHashMapDemo01 {
public static void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(3, 5);
System.out.println(map);
}
}

put()方法有两个功能:

  • 添加
  • 覆盖

TreeMap

image-20240312213820871

特点:

  • TreeMap跟TreeSet底层原理一样,都是红黑树结构的。
  • 由键决定特性:不重复、无索引、可排序
  • 可排序:对键进行排序
  • 注意默认按照键从小到大进行排序,也可以自己规定键的排序规则

代码书写两种排序规则:

  • 实现Comparable接口,指定比较规则
  • 创建集合时传递Comparator比较器对象,指定比较规则。【两个都写了会以第二种为准】

Java中数据结构总结

https://www.runoob.com/java/java-data-structures.html

数组

列表

ArrayList

LinkedList

集合

HashSet

HashMap

TreeMap

队列

泛型

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

泛型的格式:<数据类型>

注意:泛型只能支持引用数据类型

1
ArrayList<String> list = new ArrayList<>();

jdk5之前集合是可以存放任意类型的数据元素的,

多态的弊端是不能访问子类的特有功能

泛型的好处

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

==Java中的泛型是伪泛型==

泛型的擦除

泛型的细节

  • 泛型中不能写基本数据类(只能写成其对应的包装类)
  • 指定泛型的具体类型后,传递数据时,可以传入该类型或者其子类类型
  • 如果不写泛型,类型默认是Object

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

  • 类后面
    • 泛型类
  • 方法上面
    • 泛型方法
  • 接口上面
    • 泛型接口

泛型类

使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类

泛型方法

方法中形参类型不确定,可以使用类名后面定义的泛型

image-20240307154226291

方法中参数类型不确定时,

方案1:使用类名后面定义的泛型,所有方法都可以使用

方案2:在方法申明上定义自己的泛型,只有本方法能用,需要使用在修饰符的后面(注意是最后一个修饰符的后面)

1
2
3
4
5
6
public <E> void addAll(ArrayList<E> list){

}
public static <E> void addAll(ArrayList<E> list){

}

泛型方法中的类型是在调用方法的时候就确定了

练习

定义一个工具类:ListUtil

类中定义一个静态方法addAll,用来添加多个集合的元素

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
package com.集合;

import java.util.ArrayList;

public class ListUtil {
private ListUtil() {
}

;

/**
* 参数一:集合
* 参数二:要添加的元素
*/
public static <E> void addAll(ArrayList<E> list, E... e) {
for (E element : e) {
list.add(element);
}
}

public static <E> void addAll(ArrayList<E> list, E e1, E e2, E e3, E e4) {
list.add(e1);
list.add(e2);
list.add(e3);
list.add(e4);
}

}
1
2
3
4
5
6
7
8
9
10
11
package com.集合;

import java.util.ArrayList;

public class MyArrayLIstDemo01 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
ListUtil.addAll(list,111,222,333,444);
System.out.println(list);
}
}

泛型接口

格式:

修饰符 interface 接口名<类型> {

}

举例:

public interface List {

}

重点:如何使用一个带泛型的接口

方式1:实现类给出具体类型

方式2:实现类延续泛型,创建对象时再确定

泛型的继承和通配符

  • 泛型不具备继承性,但是数据具备继承性

利用泛型方法有一个小弊端,此时它可以接收任意的数据类型

但是我想要的是方法可以接收不确定的数据类型,但是希望只能传递Ye Fu Zi

此时就可以使用泛型的通配符 ?

它可以进行类型的限定

  • ? extends E:表示可以传递E或者E所有子类类型
  • ? super E:表示可以传递E或者E所有父类类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GenericsDemo01 {
public static void main(String[] args) {
// 泛型不具备继承性,但是数据具备继承性
ArrayList<Ye> list1 = new ArrayList<>();
ArrayList<Fu> list2 = new ArrayList<>();
ArrayList<Zi> list3 = new ArrayList<>();
method(list1);
method(list2); // 这个会报错 泛型不具备继承性

list1.add(new Ye());
list1.add(new Fu()); // 数据具备继承性
list1.add(new Zi()); // 数据具备继承性

}

public static void method(ArrayList<? extends Ye> list) {

}
}

应用场景:

  1. 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类泛型方法泛型接口
  2. 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以使用泛型通配符。

关键点:可以限定类型的范围。

可变参数

  • 可变参数在本质上就是一个数组
  • 作用:在形参中接收多个参数
  • 格式:数据类型… 参数名称
  • 举例说明:int… a

注意事项:

  • 在方法的形参中最多可以写一个可变参数
  • 在方法当中如果除了可变参数以外,还有其他形参,那么可变参数要写在最后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ArgsDemo03 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(getSum(arr));
}

public static int getSum(int... arr) {
System.out.println(arr); // [I@1b6d3586 数组
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
}

集合工具类

Collections是集合工具类

常用API

image-20240318210708519

方法名称 说明
addAll 批量添加元素
shuffle 打乱List集合元素顺序

集合嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.集合;

import java.util.*;
import java.util.function.BiConsumer;

public class 集合嵌套 {
public static void main(String[] args) {
HashMap<String, ArrayList<String>> hm = new HashMap<>();
ArrayList<String> city1 = new ArrayList<>();
Collections.addAll(city1, "南京市", "无锡市", "徐州市", "淮安市");
ArrayList<String> city2 = new ArrayList<>();
Collections.addAll(city2, "武汉市", "孝感市", "shiyan徐州市", "ezhou");
ArrayList<String> city3 = new ArrayList<>();
Collections.addAll(city3, "石家庄", "2222", "3333", "444");

hm.put("江苏省", city1);
hm.put("湖北省", city2);
hm.put("河北省", city3);

hm.forEach((key, value) -> {
StringJoiner sj = new StringJoiner(", ", "", "");
for (String s : value) {
sj.add(s);
}
System.out.println(key + " = " + sj);
});

}
}

集合进阶

创建不可变集合

不可变集合:不能被修改的集合? tuple?

应用场景:

  • 如果某个数据不能被修改,将其放进不可变集合比较防御性;
  • 当集合对象被不可信的库调用时,不可变形式是安全的。

使用.of()方法可以直接创建一个不可变的集合,这一点可以用在不进行修改的集合的使用场景,如斗地主的牌数固定就可以使用这个方法进行创建。

创建list不可变集合

创建Map不可变集合

细节:

  • 键不能重复
  • Map里面的of方法参数是有上限的,最多只能传递20个参数,即10个键值对
    • 为什么会这样进行设计呢?
    • 因为如果需要设计成可变长度的,那么键和值都需要这么设计,但是在Java中形参最多只能有一个可变参数所以无法进行这样设计,但是对于List和Set集合而言,就可以进行这样的设计了,因为他们不是键值对类型的
    • 如果需要添加的键值对个数超过了10个,则可以使用Map.ofEntries()方法

Map集合中有一个方法Map.copyOf(),这个方法可以直接传递一个集合进去,但是该方法是jdk10之后才有的,所以如果版本低的话只能自己手动创建。

使用Map.ofEntries()方法自己手动创建。

1
2
3
4
5
6
7
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
if (map instanceof ImmutableCollections.AbstractImmutableMap) {
return (Map<K,V>)map;
} else {
return (Map<K,V>)Map.ofEntries(map.entrySet().toArray(new Entry[0]));
}
}
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
package 集合进阶;

import java.util.List;
import java.util.Map;
import java.util.Set;

public class 不可变集合Demo02 {
public static void main(String[] args) {
Map<String, String> map = Map.of("zhangsan", "nanjing", "lisi", "beijing", "wnagwu", "tianjing");

Set<String> keys = map.keySet();
for (String key : keys) {
String value = map.get(key);
System.out.println(key + " = " + value);
}
System.out.println("-------------------------");

Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + " = " + value);
}

}
}

stream流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package 集合进阶;

import java.util.ArrayList;
import java.util.List;

public class StreamDemo01 {
public static void main(String[] args) {
/**
* 按照下面的要求完成集合的创建和遍历
*
* - 创建一个集合,存储多个字符串元素
* - 把集合中所有以"张"开头的元素存储到一个新的集合
* - 把"张"开头的集合中的长度为3的元素存储到一个新的集合
* - 遍历上一步得到的集合
*/
ArrayList<String> list1 = new ArrayList<>(List.of("张三丰", "张无忌", "张翠山", "王二麻子", "张良", "谢广坤"));
System.out.println(list1);

list1.stream().filter(name -> name.startsWith("张")).filter(name -> name.length() == 3).forEach(name -> System.out.println(name));


}
}

stream流的作用:结合Lambda表达式,简化集合、数组的操作

使用步骤:

  • 先得到stream流,并把数据放上去
  • 使用stream流中的API进行各种操作
    • 中间方法:过滤、转换,方法调用完毕后,还可以调用其他方法
    • 终结方法:统计、打印,最后一步,调用完毕之后,不能调用其他方法

使用步骤

先得到一条stream流,并把数据放上去

image-20240319132550921

单列集合获取stream流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package 集合进阶;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class StreamDemo02 {
public static void main(String[] args) {
/**
* 单列集合获取Stream流
*/
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// Stream<String> stream = list.stream();
// stream.forEach(new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// });
list.stream().forEach(s -> System.out.println(s));
}
}

双列集合获取stream流

双列集合中是没有stream方法的,但是可以使用keySet()方法或者entrySet()方法之后再调用相应的stream流方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package 集合进阶;

import java.util.HashMap;

public class StreamDemo03 {
public static void main(String[] args) {
/**
* 双列集合获取Stream流
*/
HashMap<String, Integer> map = new HashMap<>();
map.put("aaa", 111);
map.put("bbb", 222);
map.put("ccc", 333);
map.put("ddd", 444);

// 第一种获取Stream流的方法
map.keySet().stream().forEach(s -> System.out.println(s));

// 第二种获取Stream流的方法
map.entrySet().stream().forEach(entry -> System.out.println(entry.getKey() + " = " + entry.getValue()));


}
}

所以对于双列集合而言,总共有两种获取stream流的方法

数组获取stream流方法

数组中默认也是没有获取stream流方法的,但是我们可以通过使用Arrays工具类中的stream流方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package 集合进阶;

import java.util.Arrays;
import java.util.HashMap;

public class StreamDemo04 {
public static void main(String[] args) {
/**
* 数组获取Stream流
*/
// 1.创建数组
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 2.获取stream流
Arrays.stream(arr).forEach(a -> System.out.println(a));

}
}

零散数据获取stream流方法

这里需要注意零散的数据类型必须都是相同的,不可以是不同的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package 集合进阶;

import java.util.Arrays;
import java.util.stream.Stream;

public class StreamDemo05 {
public static void main(String[] args) {
/**
* 零散数据获取Stream流
*/
Stream.of(1,2,3,4,5,6,7).forEach(s-> System.out.println(s));
Stream.of("1,2,3,4,5,6,7","123123","adads ").forEach(s-> System.out.println(s));
}
}

注意:

stream接口中静态方法of的细节

方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组

但是数组必须是引用数据类型的,如果传递基本数据类型,是会把整个数组当做成一个元素放到Stream流中

stream流的中间方法

image-20240319135408725

注意1:中间方法返回新的stream流,原来的stream流只能使用一次,建议使用链式编程

注意2:修改stream流中的数据,不会影响原来的集合或者数组中的数据

  • filter

  • limit

这里括号中的参数就是表示几个的含义。所以下面这行代码就会打印list中的前三个数据

1
2
3
4
5
6
7
8
ArrayList<String> list = new ArrayList<>();
list.add("张三丰");
list.add("张无忌");
list.add("张翠山");
list.add("王二麻子");
list.add("张良");
list.add("谢广坤");
list.stream().limit(3).forEach(s -> System.out.println(s));
  • skip

跳过括号中个数的元素,下面这行代码将会跳过list中前四个元素,打印剩余的元素

1
list.stream().skip(4).forEach(s-> System.out.println(s));

练习:打印出张翠山、王二麻子、张良

1
list.stream().skip(2).limit(3).forEach(s -> System.out.println(s));
  • distinct:元素去重,依赖hashCode和equals方法,如果需要使用自定义数据类型的时候,需要重写这两个方法
  • concat:合并a和b两个流为一个流
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 StreamDemo03 {
public static void main(String[] args) {
//创建一个集合,存储多个字符串元素
ArrayList<String> list = new ArrayList<String>();

list.add("林青霞");
list.add("张曼玉");
list.add("王祖贤");
list.add("柳岩");
list.add("张敏");
list.add("张无忌");

//需求1:取前4个数据组成一个流
Stream<String> s1 = list.stream().limit(4);

//需求2:跳过2个数据组成一个流
Stream<String> s2 = list.stream().skip(2);

//需求3:合并需求1和需求2得到的流,并把结果在控制台输出
// Stream.concat(s1,s2).forEach(s-> System.out.println(s));

//需求4:合并需求1和需求2得到的流,并把结果在控制台输出,要求字符串元素不能重复
Stream.concat(s1,s2).distinct().forEach(s-> System.out.println(s));
}
}
  • map: 转换流中的数据类型
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
package 集合进阶;

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;

public class StreamDemo08 {
public static void main(String[] args) {
/**
* map
* 需求:获取字符串中的数值并打印输出
*/
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌-20","张无忌-30","张无忌-40","张无忌-50","张无忌-60","赵敏-10");
// Function的第一个类型:流中的原本类型;第二个数据类型:想要转换后的类型
list.stream().map(new Function<String, Integer>() {

@Override
// apply方法的形参s表示流里面的每一个数据
public Integer apply(String s) {
String[] arr = s.split("-");
String ageString = arr[1];
int age = Integer.parseInt(ageString);
return age;
}
}).forEach(s-> System.out.println(s));
}
}

stream流的终结方法

image-20240319151817482

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
package 集合进阶;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.function.IntFunction;

public class StreamDemo09 {
public static void main(String[] args) {
/**
* void forEach(Consumer action) 遍历
* long count() 统计
* toArray() 收集流中的数据,放到数组中
*/

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏");

list.stream().forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});

long count = list.stream().count();
System.out.println(count);

// toArray() 收集流中的数据,放到数组中

Object[] objects = list.stream().toArray();
System.out.println(Arrays.toString(objects));
//IntFunction的泛型:具体类型的数组
//apply的形参:流中数据的个数,要跟数组的长度保持一致
//apply的返回值:具体类型的数组
//方法体:就是创建数组


// toArray方法的参数的作用:负责创建一个指定类型的数组
// toArray方法的底层,会依次得到流里面的每一个数字,并把数据放到数组当中
// toArray方法的返回值:是一个装着流里面素有数据的数组
String[] strings = list.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
System.out.println(Arrays.toString(strings));

String[] strings1 = list.stream().toArray(value -> new String[value]);
System.out.println(Arrays.toString(strings1));

}
}
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
package 集合进阶;

import com.sun.jdi.Value;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.stream.Collectors;

public class StreamDemo10 {
public static void main(String[] args) {
/**
* collect(Collector collector) 收集六中的数据,放到集合中(List Set Map)
*/
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "0张无忌-男-15", "1张无忌-女-15", "2张无忌-男-15", "3张无忌-女-15", "4张无忌-男-15", "5张无忌-女-15");
// 收集List集合中 所有的男性数据
// 这里在进行比较的时候,一般情况下会把确定数据放在前面,因为不确定的数据有可能会是null而导致空指针异常
list.stream().filter(s -> "男".equals(s.split("-")[1])).forEach(s -> System.out.println(s));
List<String> myList = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
System.out.println(myList);

System.out.println("-----------------------------------------");
// 收集到set集合当中
Set<String> mySet = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
System.out.println(mySet);

// 这两者的区别就是使用set可以去重,而使用list无法去重

System.out.println("-----------------------------------------");
// 收集到map集合中
// 谁作为键? 谁作为值 本题的需求是 姓名和年龄分别作为键和值
Map<String, Integer> myMap = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
/**
* toMap 参数一表示键的生成规则
* 参数二表示值的生成规则
* 参数一:
* Function泛型一:表示流中每一个数据的类型
* 泛型二:表示Map集合中键的数据类型
* 方法apply形参:依次表示发流里面的每一个数据
* 方法体:生成键的代码
* 返回值:已生成得键
* 参数二:
* Function泛型一:表示流中每一个数据的类型
* 泛型二:表示Map集合中值的数据类型
* 方法apply形参:依次表示发流里面的每一个数据
* 方法体:生成值的代码
* 返回值:已生成得值
*/
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0];
}
},
new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]);
}
}));
System.out.println(myMap);
// System.out.println(42 == 42.0);

System.out.println("-----------------------------------------");
System.out.println("Lambda表达式形式:");
list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(
s -> s.split("-")[0], s -> s.split("-")[2]
)).forEach((key, value) -> System.out.println(key + " = " + value));
}
}

如果我们需要收集到Map集合当中,键是不能够重复的,要确保唯一性。

总结

image-20240319172326994

stream流练习

方法引用

多线程&JUC

image-20240319205904028

什么是多线程

并发与并行

线程

线程是操作系统中能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

应用软件中互相独立,可以同时运行的功能

CPU处理速度和内存读写速度不匹配,这是进程调度的根本原因

进程

进程是程序的基本执行实体

任务管理器中可以查看到运行的软件就是一个进程

有了多线程,就可以让程序做多件事情

image-20240319215044507

并发与并行

并发

同一时刻,多个指令在CPU上交替执行

并行

在同一时刻,多个指令在多个CPU上同时执行

多线程的实现方式

  • 继承Thread类的方式实现
  • 实现Runnable接口的方式实现
  • 利用Callable接口和Future接口方式实现

继承Thread类的方式实现

  1. 首先编写一个多线程类继承自Thread类
  2. 在该类中重写run()方法,将需要多进程进行的任务写在方法体中
  3. 编写测试类,进行测试

MyThread.java

1
2
3
4
5
6
7
8
9
10
11
package 多线程;

public class MyThread extends Thread {
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " : Hello World");
}
}
}

ThreadDemo01.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package 多线程;

public class ThreadDemo01 {
public static void main(String[] args) {
/**
* 多线程的第一种启动方式:
* 1.自己定义一个类继承Thread
* 2.重写run方法
* 3.创建子类的对象,并启动线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();

}
}

实现Runnable接口类

MyThread02.java

1
2
3
4
5
6
7
8
9
10
11
12
package 多线程;

public class MyThread02 implements Runnable {
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 100; i++) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " HelloWolrd");
}
}
}

ThreadDemo02.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package 多线程;

public class ThreadDemo02 {
public static void main(String[] args) {
/**
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.重写里面的run方法
* 3.创建自己类的对象
* 4.创建一个Thread类的对象,并开启线程
*/
MyThread02 mythread02 = new MyThread02();
Thread t1 = new Thread(mythread02);
Thread t2 = new Thread(mythread02);
t1.setName("111线程1");
t2.setName("222线程1");
t1.start();
t2.start();
}
}

利用Callable接口和Future接口方式实现

  • 多线程的第三种实现方式
  • 特点:可以获取到多线程运行的结果
    • 创建一个类MyThread实现callable接口,此时需要指定泛型的类型
    • 重写call(是有返回值的,表示多线程运行的结果),需要指定跟刚刚一样的泛型类型
    • 创建MyThread对象(表示多线程要执行的任务)
    • 创建FutureTask对象(作用是管理多线程运行的结果)
    • 创建Thread类对象,并启动(表示线程)

MyThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package 多线程;

import java.util.concurrent.Callable;

public class MyThread03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求1-100之间的和
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}

ThreadDemo03.java

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
package 多线程;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 多线程的第三种实现方式
* 特点:可以获取到多线程运行的结果
* 1.创建一个类MyCallable实现callable接口
* 2.重写call(是有返回值的,表示多线程运行的结果)
* 3.创建MyCallable对象(表示多线程要执行的任务)
* 4.创建FutureTask对象(作用是管理多线程运行的结果)
* 5.创建Thread类对象,并启动(表示线程)
*/
MyThread03 myThread03 = new MyThread03();
FutureTask<Integer> futureTask = new FutureTask<>(myThread03);
Thread t1 = new Thread(futureTask);
t1.start();

// 获取到多线程的结果
Integer result = futureTask.get();
System.out.println(result);

}
}

多线程三种实现方式对比

分成两类

  • 不可以获取多线程的结果
    • 继承Thread类
    • 实现Runnable接口
  • 可以获取多线程的结果
    • 实现Callable接口

image-20240320101053706

多线程中的成员方法

image-20240320101235797

前四个方法的练习:

MyThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package 多线程.a04threadMethod;

public class MyThread extends Thread {
public MyThread() {
}

public MyThread(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + " @ " + i);
}
}
}

ThreadMethodDemo01.java

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
package 多线程.a04threadMethod;

public class ThreadMethodDemo01 {
public static void main(String[] args) throws InterruptedException {
/**
* String getName()
* void setName(String name) 设置线程的名字(构造方法也可以设置名字)
* 1.如果我们没有给线程设置名字,线程也是有默认名字的,格式:Thread-X (X为序号,从0开始)
* 2.如果我们要给线程设置名字,可以使用setName这个方法,也可以直接使用构造方法进行设置,需要在子类中super()父类的构造方法
* static Thread currentThread() 获取到当前进程对象
* 细节:
* 当JVM虚拟机启动之后,会自动的启动多条线程
* 其中有一条线程就叫main线程
* 它的作用就是去调用main方法并执行里面的代码
* 所以之前我们写的所有的代码其实都是运行在main线程中的
*
* static void sleep(long time) 让线程休眠指定时间,单位为毫秒
* 细节:
* 1.哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
* 2.方法的参数:就表示睡眠的时间,单位为毫秒
* 1秒 = 1000毫秒
* 3.当时间到了之后,线程就会自动醒来,继续执行下面的其他代码
*/

MyThread mt = new MyThread("飞机");
MyThread mt2 = new MyThread("坦克");

mt.start();
mt2.start();

// Thread t = Thread.currentThread();
// System.out.println(t.getName());

// System.out.println("111111111");
// Thread.sleep(5000);
// System.out.println("222222222");


}
}

线程的优先级:最小是1,最大是10,默认是5,优先级越大,抢占到CPU的概率就会越大

守护线程:备胎线程

礼让线程

插入线程

线程的优先级

image-20240320104150840

线程的调度

  • 抢占式调度(随机性)

线程的优先级:最小是1,最大是10,默认是5,优先级越大,抢占到CPU的概率就会越大

1
2
3
public static final int NORM_PRIORITY = 5;

public static final int MAX_PRIORITY = 10;

MyRunnable.java

1
2
3
4
5
6
7
8
9
10
package 多线程.a05threadMethod;

public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "----" + i);
}
}
}

ThreadDemo.java

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
package 多线程.a05threadMethod;

public class ThreadDemo {
public static void main(String[] args) {
/**
* setPriority(int newPriority) 设置线程的优先级
* final int getPriority() 获取线程的优先级
*/
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread t1 = new Thread(mr, "飞机");
Thread t2 = new Thread(mr, "坦克");

System.out.println(t1.getPriority());
System.out.println(t2.getPriority());

System.out.println(Thread.currentThread().getPriority());

t1.setPriority(1);
t2.setPriority(10);

t1.start();
t2.start();
}
}

守护线程

final void setDaemon(boolean on) 设置为守护线程

  •  细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
  •  通俗易懂:当女神线程结束了,备胎线程也就没必要存在了

Thread1.java

1
2
3
4
5
6
7
8
9
10
package 多线程.a06threadMethod3;

public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}

Thread2.java

1
2
3
4
5
6
7
8
9
10
package 多线程.a06threadMethod3;

public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}

ThreadDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package 多线程.a06threadMethod3;

public class ThreadDemo {
public static void main(String[] args) {
/**
* final void setDaemon(boolean on) 设置为守护线程
* 细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
* 通俗易懂:
* 当女神线程结束了,备胎线程也就没必要存在了
*/
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();

t1.setName("女神");
t2.setName("备胎");

// 把第二个线程设置为守护线程
t2.setDaemon(true);

t1.start();
t2.start();

}
}

应用场景:

对于聊天的线程,如果我们把他关闭了,那么传输文件的线程也就没必要存在了

image-20240320112034100

礼让线程

public static void yield()

出让CPU执行权之后,是有可能在抢夺到的,所以这个方法的作用是尽可能的让结果更加均匀,但是不一定能保证完全均匀。

插入线程

可以改变线程执行的顺序

例如在主线程中之前执行自己的线程

t.join():表示把t这个线程,插入到当前线程之前。

下例中:

t:土豆线程

当前线程:main线程

MyThread.java

1
2
3
4
5
6
7
8
9
10
package 多线程.a08threadMethod5;

public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}

ThreadDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package 多线程.a08threadMethod5;

public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.setName("土豆");
t.start();
t.join();

for (int i = 0; i < 10; i++) {
System.out.println("main线程" + (i+1));
}
}
}

上面的代码中如果不加上t.join()的话,程序会先执行main线程的输出语句;但是如果我们需要改变线程执行顺序,先让土豆线程执行的话,就可以使用t.join()语句来改变线程的执行顺序。这个就是插入线程/插队线程

image-20240320125942828

线程的生命周期

image-20240320131430097

  • 线程初始状态:NEW
  • 线程运行状态:RUNNABLE
  • 线程阻塞状态:BLOCKED
  • 线程等待状态:WATTING
  • 超时等待状态:TIMED_WATTING
  • 线程终止状态:TERMINATED

线程安全性问题

需求:某个电影院正在上映国产大片, 共有100张票,有3个售票窗口,设计一个程序模拟电影院卖票

卖票案例中的问题:

  • 可能会存在卖300张票的情况,每个线程都卖了100张票
    • 在类中将ticket定义为static静态类型,这样这个类的所有实例化对象都共享一个数据
  • 使用static进行修改后,仍然会出现卖相同的重复票或者超卖的情况,因为可能在ticket++之后,cpu执行权就被其他线程夺走了,然后其他线程也ticket++,然后再输出
  • 卖重复票的原因:CPU在执行的时候会有随机性
  • 超卖的原因:CPU在执行的时候会有随机性

image-20240320135954612

同步代码块

修改思路:操作共享数据的代码代码块如果能加锁,在当前执行的时候,其他线程只能进行等待

同步代码块:把操作共享数据的代码锁起来。

格式:

1
2
3
synchronized(锁){
操作共享数据的代码
}

特点:

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

MyThread.java

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
package 多线程.a09threadSafe;

import java.awt.*;

public class MyThread extends Thread {
public MyThread() {
}

public MyThread(String name) {
super(name);
}

// 锁对象一定要是唯一的
static Object obj = new Object();

// 加上static,表示这个类所有的对象,都共享ticket数据
static int ticket = 0;

@Override
public void run() {
// 卖票的代码逻辑
while (true) {
// 同步代码块
synchronized (obj) {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票!");
} else {
break;
}
}
}
}
}

ThreadDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package 多线程.a09threadSafe;

import 多线程.a09threadSafe.MyThread;

public class ThreadDemo {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread("111窗口111");
MyThread t2 = new MyThread("222窗口222");
MyThread t3 = new MyThread("333窗口333");

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

小细节:

  • synchronized不能放在循环的外面,否则线程1会将所有的票卖完
  • synchronized锁对象一定要是唯一的,

同步方法

就是把synchronized关键字加到方法上

格式如下:

image-20240320140936324

同步静态方法的锁对象一般是对象的字节码 即类名.class

利用同步方法完成

  •  技巧:先写同步代码块,在抽取成方法
1

在字符串中有StringBuilder和StringBuffer,这两者的所有方法都是一模一样的,唯一的区别就是在StringBuffer的所有方法的前面都加上了synchronized,也就是说StringBuffer是线程安全的,而StringBuilder是非线程安全的。

单线程:直接无脑只用StringBuilder

多线程:需要考虑数据安全性的前提下,使用StringBuffer

Lock锁

JDK5之后提供了一个新的锁对象Lock,这样可以更清晰的表达如何加锁和释放锁。

Lock()中提供了获得锁和释放锁的方法

void lock():获得锁

void unlock():释放锁

手动上锁、手动释放锁

Lock是接口不能直接实例化,这里采用他的实现类ReentrantLock来实例化

ReentrantLock的构造方法

ReentrantLock():创建一个ReentrantLock的实例

image-20240320144357289

1

死锁

写锁的时候不要让两个锁嵌套起来

1

生产者消费者|等待唤醒机制

生产者消费者模型师一个十分经典的多线程协作的模式

wait()

notify()

image-20240321141446981

wait()当前线程等待,直到被其他线程唤醒

notify() 随机唤醒单个线程

notifyAll() 唤醒所有线程

消费者等待

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

put数据时:放不进去,会等着,也叫做阻塞

take数据时:取出第一个数据,取不到会等着,也叫做阻塞。

阻塞队列的继承结构

image-20240321150515479

多线程的六种状态

Java虚拟机中是没有定义运行状态的。因为当线程抢到CPU执行权的时候,Java虚拟机就将当前线程交给操作系统进行管理,虚拟机不管就不进行定义了。

类比:手机被偷了,后续手机坏了需要维修就与你无关了。

image-20240321152622986

image-20240321152552399

image-20240321152457751

线程池

线程池是有上限的

线程池的核心原理

  • 创建一个池子,池子是空的

  • 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下次再提交任务时,不需要创建新的线程,直线复用已有的线程即可。

  • 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

线程池代码实现

  • 创建线程池
  • 提交任务
  • 所有的任务全部执行完毕,关闭线程池【实际开发中一般不会关闭的,服务器是24小时不关】

线程池代码实现

第一个说是没有上限的线程池,实际上是是int的最大值2的31次方,21亿多

public static ExecutorService newCachedThreadPool() 创建没有上限的线程池

public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池

MyRunnable.java

1
2
3
4
5
6
7
8
9
10
11
package 多线程_线程池;

public class MyRunnable implements Runnable {

@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}

MyThreadDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package 多线程_线程池;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadDemo {
public static void main(String[] args) {
// 1.获取线程池对象
ExecutorService pool = Executors.newCachedThreadPool();

// 2.提交任务
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());

// 3.销毁线程池
pool.shutdown();
}
}

自定义线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 创建自定义线程池
* ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
* 参数一:核心线程数量,不能小于0
* 参数二:最大线程数量,不能小于0,最大数量>=核心线程数量
* 参数三:空闲线程最大存活时间,不能小于0
* 参数四:时间单位,用TimeUnit指定
* 参数五:任务队列,不能为null
* 参数六:创建线程工厂,不能为null
* 参数七:任务的拒绝策略,不能为null
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,
6,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), //任务队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
/**
* 这里为什么定义拒绝策略为内部类?
* 内部类是依赖外部类存在的,但是单独出现是没有任何意义的,并且内部类的本身又是一个独立的个体
*/
);

内部类:

image-20240323133528823

自定义线程池的工作原理

image-20240323134742179+

  • 当核心线程满时,再提交任务就会排队
  • 当核心线程满,队列也满了,就会创先临时线程(非核心线程)
  • 当核心线程、队列和临时线程(非核心线程)都满了,就会触发任务拒绝策略。

核心线程和临时线程

只有在核心线程都被用完了并且队列长度已经排满了才会启动非核心线程(临时线程)

自定义线程池(任务拒绝策略)

线程池多大比较合适?

image-20240323140910215

最大并行数:

4核8线程

1
2
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);

查看最大核心线程 为8

  • CPU密集型运算

计算比较多,读取数据库IO比较少属于这种类型,最大并行数+1,+1的原因是保障当前项目因为页缺失故障或其他故障导致线程暂停,这个额外的线程就可以顶上去,保证CPU时钟周期不被浪费

  • IO密集型运算

操作数据库、RPC、IO操作的时候,CPU闲置下来

image-20240323141846680

异常

异常体系介绍

java.lang.Throwable

  • Error
  • Exception
    • RuntimeException
    • 其他异常

Exception:叫做异常,代表程序可能出现的问题

运行时异常:RuntimeError及其子类,编译阶段不会出现异常题型。运行时出现的异常(如数组索引越界异常)

编译时异常:编译阶段就会出现异常提醒的

image-20240328133556336

反射

反射允许对封装类的字段、方法和构造方法的信息进行编程访问

例如idea自动提示一个类中有哪些方法,或者方法括号中的形参参数类型是哪些?

通俗的话讲,反射就是可以从类中拿东西

image-20240323143357686

为什么要有反射?

image-20240323144742904

Redis的Java客户端

静态初始化块,在类加载时执行,用于初始化静态成员变量或执行一些静态操作。

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
package com.heima;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class JedisConnectionFactory {
private static final JedisPool jedisPool;

static {
// 配置连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(8);
poolConfig.setMaxIdle(8);
poolConfig.setMinIdle(0);
poolConfig.setMaxWaitMillis(1000);

// 创建连接池对象
jedisPool = new JedisPool(poolConfig, "1.94.65.33", 6379, 1000, "123321");
}

public static Jedis getJedis() {
return jedisPool.getResource();
}
}

单例模式

JedisConnectionFactory 类中的 jedisPool 属性被声明为 static,并在【静态代码块】的静态初始化块中初始化。这样做确保了在整个应用程序生命周期中只有一个 Jedis 连接池实例。

工厂模式

类中的getJedis()方法充当了工厂方法,用于创建Jedis对象。它封装了Jedis连接池的创建和配置细节,并提供了一个统一的接口来获取Jedis对象。