Java基础(黑马)
提示忽略大小写
运算符
字符串和字符的加操作
字符+数字或者字符+数字时,会把字符通过ASCII码表查询到对应的数字再进行计算。
逻辑运算符
短路运算符
例如在登录程序中,判断用户名**&&**密码
1、用户名正确,需要判断密码
2、用户名错误,无需判断密码
数组
数组的定义
数组的静态初始化
完整格式
数据类型[] 数组名 = new 数据类型[] {元素1, 元素2 …}
简化格式
数据类型[] 数组名 = {1,2,3,4,5}
1 | int[] arr = new int[] {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
数组的内存图
Java的内存分配
- 栈(内存)
- 方法运行时使用的内存,比如main方法运行,进入方法栈中执行
- 堆(内存)
- 存储对象或者数组,new创建来的都是存储在堆内存
- 方法区
- 存储可以运行的
.class
文件
- 存储可以运行的
- 本地方法栈
- JVM在使用操作系统功能的时候使用,与开发无关
- 寄存器
- 给CPU使用,与开发无关
数组的常见问题
- 索引越界问题
注意访问数组的下标是在合法区间的
数组常见操作
- 求最值
- 求和
- 交换数据
- 打乱数据(抽奖优化)
- 生成随机数
Java中生成随机数使用
1 | Random r = new Random(); |
Random括号中的参数是范围,左闭右开
二维数组
二维数组静态初始化
初始化的时候可以写成多行,一行一个一维数组,这样方便我们进行查看
二维数组的动态初始化
方法
方法是程序中最小的执行单元
方法命名规范:首字母小写和驼峰命名相结合 例如getResult()
方法的格式
方法的定义与调用
- 最简单的方法调用
- 带参数的方法定义
- 带返回值的方法定义
带参数方法的定义
带参数方法的调用
形参和实参
- 形参:指方法定义中的参数
- 实参:方法调用中的参数
带返回值的方法定义
带返回值的方法调用
- 直接调用
- 赋值调用
- 输出调用
- 方法与方法之间是平级关系,不能互相嵌套定义
方法的重载
在同一个类中,定义了多个同名方法,这些同名的方法具有同种功能。
每个方法有不同的参数类型或者参数个数,这些同名的方法构成了重载关系
方法名相同,参数不同的方法 | ==与返回值无关==
参数不同:个数不同、类型不同、顺序不同
定义方法时候可以把相同功能的定义成一样的名字:
- 定义方法时候可以不用那么多单词
- 调用方法时候也不需要那么麻烦了
输出语句
println(“abc”) //先打印abc再进行换行
print(“abc”) //只打印abc不进行换行
方法的基本内存原理
- 方法的基本内存原理
- 方法传递基本数据类型内存原理
- 方法传递引用数据类型的内存原理
什么是基本数据类型
- 整数类型
- 浮点数类型
- 布尔类型
- 字符类型
什么是引用数据类型
- 除了上面的其他所有类型
方法值传递
面向对象
类和对象
- 类(设计图):是对象共同特征的描述
- 对象:是真实的具体的东西
需要先设计类,在实例化对象
定义类的补充注意事项
- 用来描述一类事物的类,专业叫做 ==Javabean类==
- 在Javabean类中是不写main方法的
- 在之前编写的main方法类,叫做==测试类==
- 可以在测试类中创建javabean类的对象并进行赋值调用
类名首字母要大写、英文、有意义,满足驼峰规则,不能使用关键字,满足标识符规定
封装
private关键字
- 是一个权限修饰符
- 可以修饰(成员变量和成员方法)
就近原则和this
关键字
this的作用是可以区分成员变量和局部变量
构造方法
作用:在创建对象的时候给成员变量进行赋值
构造方法的定义
- 如果没有定义构造方法,系统会给出默认的无参构造方法
- 这时候可以直接使用new Teacher()这样的方式去定义对象
- 如果定义了有参的构造方法,系统则不会提供默认的构造方法
- 如果这时候使用new Teacher()空参去定义对象则会报错
有参构造和无参构造没有返回值,并且这两者属于构造方法的重载
因为后面无论是否使用,都需要将两种构造方法都写出来
构造方法
- 无参构造方法
- 初始化对象时,成员变量的数据均采用默认值。
- 初始化对象时,同时可以为对象进行赋值。
- 有参构造方法
标准的Javabean类
插件==PTG== 可以快速生成一个标注的Javabean类
对象内存图
- 一个对象的内存图
- 多个对象的内存图
- 两个变量指向同一个对象的内存图
- this的内存原理
- 基本数据类型和引用数据类型的区别
- 局部变量和成员变量的区别
一个对象的内存图
main方法执行完毕之后,程序会退出栈,之前定义的对象所占用的堆内存空间也就变成了垃圾
两个对象的内存图
第二次实例化同一个对象的时候不会再重复加载class文件
其余的过程和一个对象的内存图差不多
两个引用指向同一个对象
修改其中一个引用会对另一个引用进行覆盖
this的内存原理
如果没有使用this,在变量进行使用的时候会根据就近原则进行选择
this 可以区分局部变量和成员变量
this ==的本质是表示所在方法调用者的地址值==
成员变量和局部变量的区别
成员变量:类中方法外的变量
- 位于堆内存中
局部变量:方法中的变量(对象中的方法内)
- 位于栈内存中(具体是在栈中方法内)
综合案例
字符串
API帮助文档
字符串在开发中的场景
- 登录
- 账号
- 密码 校验
- 字符串敏感词替换 ***
- 人民币转大写
字符串相关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
创建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);
字符串创建的内存分析
StringTable(串池)在JDK7之后就从方法区挪到了堆内存
在使用直接赋值方式创建字符串的时候,如果发现串池中存在该字符串,则会直接将已存在的字符串拿过来==复用==节省空间,如果不存在才会创建一个新的字符串
手动new出来的字符串对象内存分析
如果出现重复的字符串则会多次占用内存空间
1 | String aa = "abc"; |
上面的这个代码中:
==
如果是比较引用数据类型,则会比较两个引用是否相等,java在进行编译的时候如果是字符串常量会放在常量池中,因此aa和bb都是引用的字符串常量池中的abc,所以==
比较的引用对象是相等的.
对于第二个部分,是创建了两个新的字符串对象,这个字符串对象会被存储在堆内存中的不同位置,因此aaa和bbb的引用是不同的,所以会输出false。
如果说在项目中需要比较两个对象的值是否相等,可以使用继承自父类的equals()
方法进行比较,String对象也重写了这个方法,会进行值的比较。
总结
使用直接赋值的方式创建字符串简单并且还会节约内存
Java的常用方法(比较)
==
号比较的是什么内容?
- 如果比较的是基本数据类型,则比较的是两者之间的==数据值==是否相等
- 如果比较的是引用数据类型,则比较的是两者之间的==地址值==是否相等
这里的引用数据类型包括
- 类
- 接口
- 数组
- 枚举类型
- 注解类型
- 字符串
1 | package com.字符串; |
比较方法
boolean equals(要比较的字符串) // 完全一样的结果才是true,否则为false
boolean equalsIgnoreCase(要比较的字符串) // 忽略大小写的比较 只能是忽略英文状态下的大小写
- 可以用于验证码的比较
键盘录入的字符串信息在底层源码中是new出来的,存在堆内存中,所以和直接赋值的字符串对象用
==
比较 结果为false
1 | package com.字符串; |
上面的例子结果为false
所以以后再比较字符串内容的时候,需要使用字符串的方法
1 | package com.字符串; |
练习
遍历字符串中的字符
- charAt(int index)
- length()
1 | package com.字符串; |
统计字符个数
1 | package com.字符串; |
拼接字符串和反转
1 | package com.字符串; |
金额转换
手机号加密
15011111871 –> 150****1871
使用到substring
方法
substring
共有两种重载
该方法作用是获取从beginIndex开始到字符串末尾的子字符串并返回
1
public String substring(int beginIndex)
该方法作用是获取从beginIndex开始到endIndex之间的子字符串,注意是==左闭右开==,类似于Python中的切片
1
public String substring(int beginIndex, int endIndex)
1 | package com.字符串; |
敏感词替换
String replace(旧值, 新值) 替换
1 | package com.字符串; |
字符串拼接
StringBuilder
1 | package com.字符串; |
在上面这个代码中进行字符串拼接会消耗很长的时间,但是如果使用StringBuilder可以很快的得到最终的结果
StringBuilder可以看成是一个容器,创建之后里面的内容是可变的
- 作用:提高字符串操作效率
Stringbuilder的构造方法
- 空参构造:创建一个空白的可变字符串对象,不包含任何内容
- 有参构造:根据字符串内容,来创建可变字符串对象
StringBuilder的常用方法
- append(任意类型) 往容器中添加内容,并返回容器本身
- reverse() 反转容器中的内容
- length() 返回长度(字符出现的个数)
- toString() 将StringBuilder类型转换为String
StringBuilder是Java已经写好的类,Java在底层对他做了一些特殊处理,打印对象不是地址值而是==属性值==
练习
对称字符串
1 | package com.字符串; |
使用StringBuilder的场景
- 字符串的拼接
- 字符串的反转
拼接字符串
Stringjoiner
Stringjoiner跟StringBuilder一样可以看成是一个容器,创建之后里面的内容是可变的
- 作用:提高字符串的操作效率,代码编写简洁,但是目前市场上很少人用
- JDK8以后才有的
构造方法
1 | StringJoiner sj = new StringJoiner("---"); |
成员方法
- add()
- length()
- toString() :返回一个字符串,字符串就是拼接之后的结果
1 | package com.字符串; |
字符串对象的总结
- String
- StringBuilder
- StringJoiner
字符串相关类的底层实现原理
字符串存储的内存原理
- 直接赋值可以服用字符串常量池中的字符串
- new出来的不会复用,而是开辟一个新的空间(在堆内存中)
==
号比较的到底是什么?- 比较基本数据类型的时候比较数据值
- 比较引用数据类型的时候比较地址值
字符串拼接的底层原理
JDK8之后的拼接原理
字符串拼接的时候不要直接+,会在底层创建多个对象,浪费时间和性能
所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存
容量:最多装多少
长度:已经装了多少
- 初始化时候默认的容量是16 ==长度为16字节数组==
- 添加的内容长度小于16,则会直接存
- 添加的内容长度大于16会进行扩容(老容量*2 + 2 = 34)
- 如果扩容之后还不够,则以实际长度为准
toString()方法的底层是new了一个字符串对象
练习
修改字符串中的内容思路:
- subString
- 转换为字符数组来实现
集合
存放多个元素 –> 数组
数组长度不可变,一旦定义了就不能改变,而集合就可以解决这个问题,集体的特点就是可以进行自动扩容
==自动扩容==
数组可以存放基本数据类型和引用数据类型
ArrayList
集合 vs 数组
- 长度
- 存储类型
- 数组可以存放基本数据类型和引用数据类型
- 集合只能存引用数据类型,如果需要存基本数据类型的话,需要改成所对应的包装类
集合包括
- ArrayList
ArrayList
<E> 泛型
ArrayList构造方法
- ArrayList()
- 构造一个初始容量为10的空列表
- ArrayList(int initialCapacity)
- 构造具有指定初始容量的空列表
- ArrayList(Collection<? extends E> c)
ArrayList成员方法
基本数据类型对应的包装类
1 | package com.集合; |
创建自定义对象的ArrayList
、
1 | package com.集合; |
1 | package com.集合; |
IDEA快捷键
==Ctrl + P 可以查看有什么参数==
==Ctrl + Alt + T 查看包裹函数==
Ctrl + Alt + V 自动生成左边
Ctrl + N 搜索界面 查找相关的类或者包 点击即可查看源代码
shift + F6 批量修改变量名
Ctrl + Shift + U 小写转大写
Ctrl + H 查看继承树
IDEA设置修改
注释的时候从每一行开头进行注释修改为只从第一个字符前面开始
勾选掉方框中的勾和下面截图保持一致即可
设置鼠标滚轮加Ctrl进行缩放
面向对象进阶
static
static表示静态,是java中的一个修饰符,可以修饰成员方法、成员变量
静态变量
- 被该类所有对象共享
- 不属于对象,属于类
- 随着类的加载而加载,优先于对象而存在
调用方式
- 类名调用**==(推荐)==**
- 对象名调用
static内存图
==共享的属性可以被定义为静态==
- 比如同一个班级中每一个学生的老师姓名是可以使用static方法设置为静态变量的
teacherName
可以被定义为静态
静态方法和工具类
特点
- 多用在测试类和工具类
- Javabean类中很少会用
调用方式
- 类名调用(推荐)
- 对象名调用
javabean类
- 用来描述一类事物的类,如Student、Teacher、Dog、Cat
测试类
- 之前写的main方法可以称之为测试类
工具类
- 帮我们做一些事情,但是不描述任何事物的类
- 类名见名知意 例如 printArr 表示打印数组
- 私有化构造方法 private
- 方法定义为静态
- 帮我们做一些事情,但是不描述任何事物的类
练习
定义数组工具类
需求:在实际开发中,经常会遇到一些数组使用的工具类
请按照如下要求编写一个数组的工具类:ArrayUtil
1 | package com.面向对象进阶; |
学生工具类
Static注意事项
- ==静态方法只能访问静态变量和静态方法==
- 不能访问成员变量和成员方法
- 不能访问定义在同一个类中的成员变量和成员方法
- 非静态方法可以访问静态变量或静态方法,也可以访问非静态的成员变量和非静态成员方法
- 静态方法中是没有
this
关键字的
this:表示当前方法调用者的地址值
这个this是由虚拟机赋值的,在调用的时候也不需要加上这个参数
非静态变量也被称之为实例变量
单例设计模式是在多线程阶段进行讲解
重新认识main方法
如果想要在idea中添加参数运行程序,可以在edit configuration中进行配置
继承
封装:对象代表什么,就封装对应的数据并提供数据对应的行为
在Java中提供了一个关键字extends
,用这个关键字,我们可以让一个类和另一个类建立起继承关系
1 | public class Student extends Person{} |
- Student称之为子类(派生类),Person称为父类(基类或超类)
==继承的好处:==
- 可以把多个子类中重复的代码抽取到父类中,提高代码的复用性
- 子类可以在父类的基础上,增加其他的功能,使子类更强大
继承需要学习的点:
- 自己设计
- 使用别人的代码(使用别人写好的)
什么时候用继承?
①当类和类之间存在相同的内容,②并且满足子类是父类的一种,就可以考虑使用继承来优化代码
例如学生和老师类都可以继承自Person
类
继承的特点
Java只支持单继承,不支持多继承,但支持==多层继承==
在上面这个例子中,如果Java有多继承就会导致方法的混乱
C是A的间接父类,B是A的直接父类
每一个类都直接或者间接继承自Object
如果自己定义的一个类没有继承自某个父类,Java虚拟机在运行的过程中会自动添加一个Object父类
子类可以使用直接父类或者间接父类中的内容,但是叔叔类中的内容是不可以使用的
设计继承类
父类中方法的权限修饰符如果是private,则子类就无法进行继承了
private 私有的:只能在本类中进行访问
注意事项:
- 子类只能访问父类中非自由的成员(成员变量 | 成员方法)
子类可以从父类中继承哪些内容?(内存图/内存分析工具)
注意继承和调用是两个概念 |
如果一个类中没有构造方法,虚拟机会自动给你添加一个默认的空参构造
虚方法表
非private修饰的方法
非final修饰的方法
非static修饰的方法
继承中成员变量的访问特点
- 就近原则
现在局部位置找,然后在本类成员变量找,父类成员位置找,逐级向上
this 表示当前类的属性或者方法 指向的是调用类的实际内存地址 不写this也是默认是当前类
super 表示调用父类的方法
- 如果出现重名的成员变量
- name
- 从局部的位置往上去找
- this.name
- 从本类成员位置往上找
- super.name
- 从父类的成员位置往上找
- name
继承中成员方法的访问特点
就近原则
方法的重写:
重写的本质是覆盖了虚方法表中的方法而已
应用场景:当父类的方法不能满足子类现在的需求时,需要进行方法的重写
子类中出现和父类一模一样的方法声明,我们就称之为方法的重写
@Override重写注解
- @Override是放在重写后的方法上的,校验子类重写时语法是否正确
- 加上注解后如果有红色的波浪线,表示语法错误
- 建议重写的方法都加上@Override注解,代码安全、优雅
方法重写的注意事项和要求:
- 重写方法的名称、形参列表必须和父类一致
- 子类重写父类方法时,访问权限子类必须大于等于父类(空着不写<protected<public)
- 子类重写父类方法时,返回值类型子类必须小于等于父类
- 建议:重写的方法尽量和父类保持一致
- 只有被添加到虚方法表中的方法才能被重写
继承中构造方法的特点
- 父类中的构造方法不会被子类继承
- 子类中的所有构造方法都会默认先访问父类中的无参构造,再执行自己
为什么?
- 子类在初始化的时候,有可能会调用父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
- 子类初始化之前一定要调用父类构造方法先完成父类数据空间的初始化
怎么调用父类的构造方法的?
- 子类构造方法的第一行语句默认都是 super() 不写也存在且必须在第一行
- 如果想要调用父类有参构造,必须手动写super进行调用
继承中构造方法的访问特点
- 子类不能继承父类的构造方法,但可以通过super调用
- 子类构造方法的第一行,有一个默认的super()
- 默认先访问父类中无参构造方法,在执行自己
- 如果要访问农夫类的有参构造,必须手动书写(参考上图,参数写在super的括号中)
this、super使用总结
- this 理解为一个变量 表示当前方法调用者的地址值
- super 表示父类的存储空间
多态
对象多种形态
什么是多态
- 同类型的对象表现出的不同形态
多态的表现形式
- 父类类型 对象名称 = 子类对象
多态的前提
有继承、实现关系
- 例如使用List<String> list = new ArrayList<>();这样的方式去定义一个list
有父类引用指向子类对象 等于号左边是父类 右边是子类
有方法的重写
代码冗余
现在需要一个通用的注册方法
多态的好处:
- 使用父类型作为参数,可以接收所有的子类对象
- 体现多态的扩展性与便利性
多态调用成员的特点
- ==变量调用:编译看左边,运行也看左边==
- ==方法调用:编译看左边,运行看右边==
多态的优点和弊端
- 在多态形势下,右边对象可以实现解耦合,便于扩展和维护
1 | Person p = new Student(); |
- 定义方法的时候,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性和便利
多态的弊端
- 不能调用子类的特有功能
编译看左边,检查父类是否有这个方法,如果没有则直接报错
解决方案:将调用者A直接变成子类型即可。转换的时候不能随便转,会报错
instance of
判断是否是所对应的类型
引用类型的类型转换
- 自动类型转换 (由小变大–> 将子类对象赋值给父类对象)
- 强制类型转换 (父类转换为子类对象)
- 可以转为真正子类类型,从而调用子类独有的功能
- 强制类型转换的时候转换类型与真实对象类型不一致会报错
- 转换的时候用
instanceof
关键字进行判断
包
包就是文件夹。用来管理各种不同功能的java类,方柏霓后期代码维护
包名规则:公司域名反写 + 包的作用,需要全部英文年小写,见名知意 com.itheima.domain
- domain 表示文件夹内都存放javabean类
使用import导入包
使用其他类的规则
- 使用同一个包中的类,不需要导包
- 使用java.lang包中的类时,不需要导包
- 其他情况都需要导包
- 如果同时使用两个包中的同名类,需要使用全类名
final
不能被修改的
- 方法
- 表明该方法是最终方法,==不能被重写==
- 类
- 最终类,==不能被继承==
- 变量
- 常量,==只能被赋值一次==
- Math包中的PI 表示圆周率就用final修饰
在实际开发中常量一般作为系统的配置信息,方便维护,提高可读性
常量的命名规范:
- 单个单词:全部大写
- 多个单词:全部大写,单词之间用下划线隔开
细节:
- final修饰的变量是基本类型:那么变量存储的地址值不能发生改变
- final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,对象内部可以改变
对象的地址值不能修改,但是对象内部的成员属性是可以修改的
权限修饰符
- 权限修饰符:用来控制一个成员能够被访问的范围
- 可以修饰成员变量、方法、构造方法、内部类
Java中有四种权限修饰符
作用范围从小到大 private < 空着不写 < protected < public
private 私房钱,只能自己用
默认/缺省/空着不写 : 只能在本包中使用
protected:
使用规则
实际开发中一般只用private和public
- 成员变量私有
- 方法公开
特例:如果方法中代码是抽取其他方法中共性代码的,这个方法一般也私有
代码块
{
}
括号中的代码,称之为代码块,共有如下几种情况:
- 局部代码块
- 构造代码块
- 静态代码块
局部代码块
提前结束变量的生命周期 ==已淘汰==
构造代码块
- 写在成员位置的代码块
- 作用:可以把多个构造反复噶种的重复代码抽取出来
- 执行时机:在创建本类对象的时候,会先执行构造代码块再执行构造方法
渐渐淘汰了… 不够灵活
优化方法:
静态代码块(*)
格式:static{}
特点:需要通过static关键字修饰,随着类的加载而加载,并且自动触发,==只执行一次==
适用场景:在类加载的时候,做一些数据初始化的时候使用
学生管理系统的时候可以初始化一些用户数据,
抽象类
抽象方法子类必须要重写,否则子类报错
抽象方法所在的类是抽象类
将共性的方法抽取到父类之后,由于每一个子类执行的内容不一样,父类中无法确定具体的方法体,就可以定义抽象方法
一个类中如果存在抽象方法,那么该类就必须声明为抽象类
抽象类的定义格式
抽象方法的定义格式
- public abstract 返回值类型 方法名(参数列表);
抽象类的定义格式:
- public abstract class 类名 {}
抽象类和抽象方法的注意事项
- 抽象类不能实例化
- 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类,可以有非抽象方法
- 抽象类可以有构造方法
- 抽象类的子类
- 要么重写抽象类中的所有抽象方法
- 要么是抽象类
共性的内容放在父类,这样可以使得团队开发的时候 方法更加规范
抽象类的定义方法
子类继承抽象类之后如何重写抽象方法
抽象类的作用
- 定义抽象方法,作为一个规范
- 作为模板,强制子类去实现特定的行为
- 代码的复用和扩展
- 实现多态性
抽象类可以先定子类方法书写的格式
接口
为什么要有接口?
游泳
理解为一个
接口的应用
==接口就是一个规则,是对行为的抽象==
接口和抽象类的区别?
接口的定义和使用
- 接口使用关键字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的新特性:接口中可以定义私有方法
接口和类之间的关系
- 类和类之间的关系
- 继承关系,只能单继承,不能多继承,但是可以多层继承
- 类和接口的关系
- 实现关系,可以单实现,也可以多实现,还可以在继承一个类的时候同时实现多个接口
- 接口和接口的关系
- 继承关系,可以单继承,也可以==多继承==
- 如果接口是实现最下面的接口的时候,就需要实现整个体系中的所有抽象方法
接口练习
一个父类中所有的子类需要是同一种事物
接口中新增方法
在jdk7之前如果都是使用抽象方法的话,如果在父接口中新增一个抽象方法,就需要在所有继承自其的子接口中实现该抽象方法,导致牵一发而动全身。
JDK8以后
- 允许在接口中定义默认方法,需要使用关键字default修饰
- 作用:解决接口升级的问题
- 接口中默认方法的定义格式:
- 格式:public ==default== 返回值类型 方法名(参数列表) { }
- 示例:public ==default== void show()P{ }
接口中默认方法的注意事项:
默认方法不是抽象方法,所以不强制被重写,重写的时候去掉default关键字
public可以省略,default不能省略 省略了会被当成是抽象方法
如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写
静态方法
静态方法不能被重写
如果有一个同名的函数 且没有Override,只是有一个同名的方法而已
重写是指子类把父类继承下来的虚方法表里面的方法进行覆盖了,这才叫重写
JDK9以后得私有方法
私有方法不能被重写
接口应用
跑步、游泳、说英语
- 接口代表规则,是行为的抽象。想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了。
- 当一个方法的参数是接口时,可以额传递接口所有实现类的对象,这种方式称之为接口多态
适配器设计模式
设计模式是一套被反复使用,多数人知晓、经过分类边牧的、代码设计经验的总结
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码的可靠性、程序的重用性。
简单理解:设计模式就是各种套路
适配器设计模式:解决接口与接口实现类之间的矛盾问题
在实现类和接口之间添加了一个第三者,并且将其设定为abstract类型接口,所有的方法全部空实现
在实现类中只实现该接口,然后只使用method5即可。
Inter.java
1 | package com.适配器模式; |
现在的需求是我需要使用当前接口中的第五个抽象方法, 在实现类中,就需要将其余的抽象方法全部实现,完全没有必要。这时候引入一个适配器
InterAdapter.java
1 | package com.适配器模式; |
在适配器中重写Inter中的全部方法,然后在真正需要使用的类中继承这个实现类重写method5即可。适配器中只是对接口的空实现,外界创建它的对象是没有意义的,因此将其设置为抽象类类型。
InterImpl.java
1 | package com.适配器模式; |
当一个接口中抽象方法过多,但是我只需要使用其中的一部分的时候,就可以使用适配器模式。
书写模式
- 编写中间类xxxAdapter,实现对应的接口
- 对接口中的抽象方法进行空实现
- 让真正的实现类继承中间类,并重写需要使用到的方法
- 为了避免其他类创建适配器类的对象,中间的适配器类需要使用abstract进行修饰
- 实现类如果有父类的话,可以让中间类继承其父类
内部类
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
类的五大成员
属性、方法、构造方法、代码块、内部类
在一个类里面,再定义一个类,在A类的内部定义B类,B类就被称为内部类
1 | public class Outer { |
为什么要学习内部类?
- 内部表示的事物是外部类的一部分
- 内部类单独出现没有意义
内部类的访问特点:
- 内部类可以直接访问外部类成员,包括私有
- 外部类要访问内部类的成员,必须创建对象
1 | package com.oop内部类; |
B类表示的事物是A类的一部分,且B单独存在没有意义
比如汽车发动机,ArrayList的迭代器,人的心脏等
内部类分类
- 成员内部类(了解)
- 静态内部类(了解)
- 局部内部类(了解)
- 匿名内部类==(掌握)==
成员内部类
- 成员内部类的代码如何写?
- 如何创建内部类对象
- 成员内部类如何获取外部类的成员变量
获取内部类对象的方法
- 在外部类中编写方法,对外提供内部类对象
- 当内部类对象的修饰符为private的时候使用
- 直接创建格式:外部类名.内部类名 = 对象名 = 外部类对象.内部类对象
- 没有使用private修饰内部类对象的时候使用
1 | package com.oop内部类; |
如果修饰符被定为private,则可以在外部类中创建一个返回值类型为内部类的方法,该方法就是用来创建内部类的对象实例。然后在测试类中就可以使用getInstance()方法进行调用。或者直接修改其修饰符类型为public
内部类内存图
内部类对象中会记录外部类对象的地址值
对于下面的程序,想要打印30、20、10,应该如何实现
1 | package com.oop内部类; |
注意到在局部变量中的a=10
,在内部类中的a=20
,在外部类中的成员变量a=10
如果想要输出30,直接打印a即可
如果想要输出20,就需要在前面加上this.a,因为this指向的是002地址对应的的对象,其中的a=20
如果想要输出10,就需要使用Outer.this.a,因为Outer.this指向的是外部类对象,其成员变量a=10
静态内部类
静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的需要创建对象
注意事项:
- 静态内部类也是成员内部类的一种
- 静态内部类只能访问外部类中的静态变量和静态方法
创建静态内部类对象的格式:
外部类名.内部类名 对象名 = new 外部类名.内部类名()
调用静态方法的格式:
外部类名.内部类名.方法名()
类对象
1 | package com.oopinterfacedemo02; |
测试类
1 | package com.oopinterfacedemo02; |
new的是外部类.内部类对象
局部内部类
- 将内部类定义在方法里面的就叫做局部内部类,类似于方法里面的局部变量
- 外界无法直接使用,需要在方法内部创建对象并使用
- 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
匿名内部类
隐藏名字的内部类
如果前面是类就是继承关系,如果前面是接口就是实现关系,之后需要在自己内部重写所有的抽象
什么是匿名内部类?
- 隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置
匿名内部类的格式?
格式的细节?
包含了继承或者实现,方法重写和创建对象
整体就是一个类的子类或者接口的实现类对象
使用场景是什么?
当方法的参数是接口或者类的时候,以接口为例,可以传递这个接口的实现类对象
面向对象项目(拼图游戏)
Arrays常见API
- Arrays.sort()
该方法默认是进行升序排序,传入一个数组即可。当然也可以进行降序排序,需要传入第二个参数表示规则
lambda表达式
最明显的作用就是简化匿名内部类
例如进行在进行排序的时候,刚刚在进行升序排序的时候,可以用lambda表达式替代
函数式编程
Lambda表达式是JDK8开始后的一种新的语法形式
注意点:
- Lambda表达式可以用来简化匿名内部类的书写
- Lambda表达式只能简化函数式接口的匿名内部类的写法
- 函数式接口:
- 有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@Functionalface注解
Lambda的省略规则:
- 参数类型可以省略不写
- 如果只有一个参数,参数类型可以省略,同时
()
也可以省略 - 如果Lambda表达式的方法体只有一行,大括号、分号、return可以省略不写
集合进阶
- 集合体系结构
- Collections集合
集合体系结构
下面的部分中,斜体加粗为接口,正常的格式为实现类
- 单列集合(**Collection**)
- List
- ArrayList
- LinkedList
- Vector
- Set
- HashSet
- LinkedHashSet
- TreeSet
- HashSet
- List
- 双列集合(Map)
双列集合的特点:
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值可以重复
- 键和值是一一对应的,每一个键只能找到自己对应的值
- 键+值这个整体,我们称之为”键值对“或者”键值对对象“,在Java中叫做”Entry“对象
List系列集合:添加的元素是有序【存和取的顺序是一样的】、可重复、有索引
Set系列集合:添加的元素是无序、不重复(可以利用这个特点进行去重)、无索引
Collection是单列集合的祖宗接口,它的功能是全部单列集合都是可以继承使用的。
注意:Collection是一个接口,我们不能直接创建它的对象,只能创建它的实现类的对象
实现类:ArrayList
1 | Collection<String> coll = new ArrayList<>(); |
Collection的遍历方式
- 迭代器遍历
- 增强for遍历
- Lambda表达式遍历
迭代器遍历
迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式。
五种遍历方式对比
迭代器遍历
在比那里的过程中需要删除元素,请使用迭代器。
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是双列集合的顶层接口,他的功能是全部双列集合都可以继承使用的
方法名称 | 说明 |
---|---|
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.添加元素 |
例如上面的代码第一行输出的内容就是原本被覆盖的内容**”123”**
put
方法如果在进行覆盖的时候是有返回值的,返回值是被覆盖的元素值。如果是第一次存的时候,返回值为null
Map的遍历方式
键找值
1 | Map<String, String> map = new HashMap<>(); |
常用api
方法名 | 说明 |
---|---|
V get(Object key) | 根据键获取值 |
Set |
获取所有键的集合 |
Collection |
获取所有值的集合 |
Set<Map.Entry<K,V>> entrySet() | 获取所有键值对对象的集合 |
键值对
其中最后一个方法返回值类型是一个包含键值对的对象,用getKey()
获取键,用getValue()
获取值。
1 | Set<Map.Entry<String, String>> entries = map.entrySet(); |
lambda表达式
即forEach遍历
修改为lambda表达式之前先可以改成这样:
1 | Map<String, String> map = new HashMap<>(); |
其中BiConsumer是一个函数式接口,满足修改为lambda表达式的要求
HashMap
HashMap是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录为null,不支持线程同步。
HashMap中的元素实际上是对象,一些常见的基本类型需要使用给他们所对应的包装类。
HashMap是无序的,不会记录插入的顺序
HashMap继承与AbstractMap,实现了Map、Cloneable、Java.io.Serializable
HashMap的key与value类型可以相同也可以不同,可以是字符串(String)类型,也可以是整型(Integer)
HashMap特点
HashMap是Map里面的一个实现类
没有额外需要学习的特有方法,直接使用Map里面的方法就可以了
特点都是由键决定的:无序、不重复、无索引
HashMap跟HashSet底层原理是一模一样的,都是哈希表结构
HashMap底层原理
底层是长度为16,默认加载因子为0.75的数组
利用键计算哈希值,跟值无关。
当链表的长度超过8 & 数组长度>=64,自动转成红黑树
HashMap底层是哈希表结构
依赖hashCode方法和equals方法保持键的唯一
如果键存储的是自定义对象,需要重写hashCode和equals方法
如果值存储的是自定义对象,不需要重写hashCode和equals方法
遍历方法
- lambda
- entrySet
案例
需求:
核心:HashMap的键位置如果存储的事自定义对象,需要重写hashCode和equals方法
1 | package com.集合; |
1 | package com.集合; |
LinkedHashMap
特点:
由键决定:有序、不重复、无索引
这里的有序是指保证存储和取出的元素顺序一致
原理:底层数据结构是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序
数组+双向链表
LRU缓存–>用Python实现过的代码也可以基于LinkedHashMap进行代码实现
1 | package com.集合; |
put()
方法有两个功能:
- 添加
- 覆盖
TreeMap
特点:
- 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
泛型可以在很多地方进行定义
- 类后面
- 泛型类
- 方法上面
- 泛型方法
- 接口上面
- 泛型接口
泛型类
使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
泛型方法
方法中形参类型不确定,可以使用类名后面定义的泛型
方法中参数类型不确定时,
方案1:使用类名后面定义的泛型,所有方法都可以使用
方案2:在方法申明上定义自己的泛型,只有本方法能用,需要使用在修饰符的后面(注意是最后一个修饰符的后面)
1 | public <E> void addAll(ArrayList<E> list){ |
泛型方法中的类型是在调用方法的时候就确定了
练习
定义一个工具类:ListUtil
类中定义一个静态方法addAll,用来添加多个集合的元素
1 | package com.集合; |
1 | package com.集合; |
泛型接口
格式:
修饰符 interface 接口名<类型> {
}
举例:
public interface List
}
重点:如何使用一个带泛型的接口
方式1:实现类给出具体类型
方式2:实现类延续泛型,创建对象时再确定
泛型的继承和通配符
- 泛型不具备继承性,但是数据具备继承性
利用泛型方法有一个小弊端,此时它可以接收任意的数据类型
但是我想要的是方法可以接收不确定的数据类型,但是希望只能传递Ye Fu Zi
此时就可以使用泛型的通配符 ?
它可以进行类型的限定
- ? extends E:表示可以传递E或者E所有子类类型
- ? super E:表示可以传递E或者E所有父类类型
1 | public class GenericsDemo01 { |
应用场景:
- 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
- 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以使用泛型通配符。
关键点:可以限定类型的范围。
可变参数
- 可变参数在本质上就是一个数组
- 作用:在形参中接收多个参数
- 格式:数据类型… 参数名称
- 举例说明:int… a
注意事项:
- 在方法的形参中最多可以写一个可变参数
- 在方法当中如果除了可变参数以外,还有其他形参,那么可变参数要写在最后
1 | public class ArgsDemo03 { |
集合工具类
Collections是集合工具类
常用API
方法名称 | 说明 |
---|---|
addAll | 批量添加元素 |
shuffle | 打乱List集合元素顺序 |
集合嵌套
1 | package com.集合; |
集合进阶
创建不可变集合
不可变集合:不能被修改的集合? tuple?
应用场景:
- 如果某个数据不能被修改,将其放进不可变集合比较防御性;
- 当集合对象被不可信的库调用时,不可变形式是安全的。
使用.of()
方法可以直接创建一个不可变的集合,这一点可以用在不进行修改的集合的使用场景,如斗地主的牌数固定就可以使用这个方法进行创建。
创建list不可变集合
创建Map不可变集合
细节:
- 键不能重复
- Map里面的of方法参数是有上限的,最多只能传递20个参数,即10个键值对?
- 为什么会这样进行设计呢?
- 因为如果需要设计成可变长度的,那么键和值都需要这么设计,但是在Java中形参最多只能有一个可变参数所以无法进行这样设计,但是对于List和Set集合而言,就可以进行这样的设计了,因为他们不是键值对类型的
- 如果需要添加的键值对个数超过了10个,则可以使用
Map.ofEntries()
方法
Map集合中有一个方法Map.copyOf()
,这个方法可以直接传递一个集合进去,但是该方法是jdk10之后才有的,所以如果版本低的话只能自己手动创建。
使用Map.ofEntries()
方法自己手动创建。
1 | static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) { |
1 | package 集合进阶; |
stream流
1 | package 集合进阶; |
stream流的作用:结合Lambda表达式,简化集合、数组的操作
使用步骤:
- 先得到stream流,并把数据放上去
- 使用stream流中的API进行各种操作
- 中间方法:过滤、转换,方法调用完毕后,还可以调用其他方法
- 终结方法:统计、打印,最后一步,调用完毕之后,不能调用其他方法
使用步骤
先得到一条stream流,并把数据放上去
单列集合获取stream流
1 | package 集合进阶; |
双列集合获取stream流
双列集合中是没有stream方法的,但是可以使用keySet()
方法或者entrySet()
方法之后再调用相应的stream流方法
1 | package 集合进阶; |
所以对于双列集合而言,总共有两种获取stream流的方法
数组获取stream流方法
数组中默认也是没有获取stream流方法的,但是我们可以通过使用Arrays工具类中的stream流方法
1 | package 集合进阶; |
零散数据获取stream流方法
这里需要注意零散的数据类型必须都是相同的,不可以是不同的数据类型
1 | package 集合进阶; |
注意:
stream接口中静态方法of的细节
方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组
但是数组必须是引用数据类型的,如果传递基本数据类型,是会把整个数组当做成一个元素放到Stream流中
stream流的中间方法
注意1:中间方法返回新的stream流,原来的stream流只能使用一次,建议使用链式编程
注意2:修改stream流中的数据,不会影响原来的集合或者数组中的数据
filter
limit
这里括号中的参数就是表示几个的含义。所以下面这行代码就会打印list中的前三个数据
1 | ArrayList<String> list = new ArrayList<>(); |
- 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 | public class StreamDemo03 { |
- map: 转换流中的数据类型
1 | package 集合进阶; |
stream流的终结方法
1 | package 集合进阶; |
1 | package 集合进阶; |
如果我们需要收集到Map集合当中,键是不能够重复的,要确保唯一性。
总结
stream流练习
方法引用
多线程&JUC
什么是多线程
并发与并行
线程
线程是操作系统中能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
应用软件中互相独立,可以同时运行的功能
CPU处理速度和内存读写速度不匹配,这是进程调度的根本原因
进程
进程是程序的基本执行实体
任务管理器中可以查看到运行的软件就是一个进程
有了多线程,就可以让程序做多件事情
并发与并行
并发
同一时刻,多个指令在CPU上交替执行
并行
在同一时刻,多个指令在多个CPU上同时执行
多线程的实现方式
- 继承Thread类的方式实现
- 实现Runnable接口的方式实现
- 利用Callable接口和Future接口方式实现
继承Thread类的方式实现
- 首先编写一个多线程类继承自Thread类
- 在该类中重写
run()
方法,将需要多进程进行的任务写在方法体中 - 编写测试类,进行测试
MyThread.java
1 | package 多线程; |
ThreadDemo01.java
1 | package 多线程; |
实现Runnable接口类
MyThread02.java
1 | package 多线程; |
ThreadDemo02.java
1 | package 多线程; |
利用Callable接口和Future接口方式实现
- 多线程的第三种实现方式
- 特点:可以获取到多线程运行的结果
- 创建一个类MyThread实现callable接口,此时需要指定泛型的类型
- 重写call(是有返回值的,表示多线程运行的结果),需要指定跟刚刚一样的泛型类型
- 创建MyThread对象(表示多线程要执行的任务)
- 创建FutureTask对象(作用是管理多线程运行的结果)
- 创建Thread类对象,并启动(表示线程)
MyThread.java
1 | package 多线程; |
ThreadDemo03.java
1 | package 多线程; |
多线程三种实现方式对比
分成两类
- 不可以获取多线程的结果
- 继承Thread类
- 实现Runnable接口
- 可以获取多线程的结果
- 实现Callable接口
多线程中的成员方法
前四个方法的练习:
MyThread.java
1 | package 多线程.a04threadMethod; |
ThreadMethodDemo01.java
1 | package 多线程.a04threadMethod; |
线程的优先级:最小是1,最大是10,默认是5,优先级越大,抢占到CPU的概率就会越大
守护线程:备胎线程
礼让线程
插入线程
线程的优先级
线程的调度
- 抢占式调度(随机性)
线程的优先级:最小是1,最大是10,默认是5,优先级越大,抢占到CPU的概率就会越大
1 | public static final int NORM_PRIORITY = 5; |
MyRunnable.java
1 | package 多线程.a05threadMethod; |
ThreadDemo.java
1 | package 多线程.a05threadMethod; |
守护线程
final void setDaemon(boolean on)
设置为守护线程
细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
通俗易懂:当女神线程结束了,备胎线程也就没必要存在了
Thread1.java
1 | package 多线程.a06threadMethod3; |
Thread2.java
1 | package 多线程.a06threadMethod3; |
ThreadDemo.java
1 | package 多线程.a06threadMethod3; |
应用场景:
对于聊天的线程,如果我们把他关闭了,那么传输文件的线程也就没必要存在了
礼让线程
public static void yield()
出让CPU执行权之后,是有可能在抢夺到的,所以这个方法的作用是尽可能的让结果更加均匀,但是不一定能保证完全均匀。
插入线程
可以改变线程执行的顺序
例如在主线程中之前执行自己的线程
t.join():表示把t这个线程,插入到当前线程之前。
下例中:
t:土豆线程
当前线程:main线程
MyThread.java
1 | package 多线程.a08threadMethod5; |
ThreadDemo.java
1 | package 多线程.a08threadMethod5; |
上面的代码中如果不加上t.join()
的话,程序会先执行main线程的输出语句;但是如果我们需要改变线程执行顺序,先让土豆线程执行的话,就可以使用t.join()语句来改变线程的执行顺序。这个就是插入线程/插队线程
线程的生命周期
- 线程初始状态:NEW
- 线程运行状态:RUNNABLE
- 线程阻塞状态:BLOCKED
- 线程等待状态:WATTING
- 超时等待状态:TIMED_WATTING
- 线程终止状态:TERMINATED
线程安全性问题
需求:某个电影院正在上映国产大片, 共有100张票,有3个售票窗口,设计一个程序模拟电影院卖票
卖票案例中的问题:
- 可能会存在卖300张票的情况,每个线程都卖了100张票
- 在类中将ticket定义为
static
静态类型,这样这个类的所有实例化对象都共享一个数据
- 在类中将ticket定义为
- 使用static进行修改后,仍然会出现卖相同的重复票或者超卖的情况,因为可能在ticket++之后,cpu执行权就被其他线程夺走了,然后其他线程也ticket++,然后再输出
- 卖重复票的原因:CPU在执行的时候会有随机性
- 超卖的原因:CPU在执行的时候会有随机性
同步代码块
修改思路:操作共享数据的代码代码块如果能加锁,在当前执行的时候,其他线程只能进行等待
同步代码块:把操作共享数据的代码锁起来。
格式:
1 | synchronized(锁){ |
特点:
- 锁默认打开,有一个线程进去了,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开
MyThread.java
1 | package 多线程.a09threadSafe; |
ThreadDemo.java
1 | package 多线程.a09threadSafe; |
小细节:
- synchronized不能放在循环的外面,否则线程1会将所有的票卖完
- synchronized锁对象一定要是唯一的,
同步方法
就是把synchronized
关键字加到方法上
格式如下:
同步静态方法的锁对象一般是对象的字节码 即类名.class
利用同步方法完成
技巧:先写同步代码块,在抽取成方法
1 |
在字符串中有StringBuilder和StringBuffer,这两者的所有方法都是一模一样的,唯一的区别就是在StringBuffer的所有方法的前面都加上了synchronized,也就是说StringBuffer是线程安全的,而StringBuilder是非线程安全的。
单线程:直接无脑只用StringBuilder
多线程:需要考虑数据安全性的前提下,使用StringBuffer
Lock锁
JDK5之后提供了一个新的锁对象Lock,这样可以更清晰的表达如何加锁和释放锁。
Lock()中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁
手动上锁、手动释放锁
Lock是接口不能直接实例化,这里采用他的实现类ReentrantLock
来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
1 |
死锁
写锁的时候不要让两个锁嵌套起来
1 |
生产者消费者|等待唤醒机制
生产者消费者模型师一个十分经典的多线程协作的模式
wait()
notify()
wait()当前线程等待,直到被其他线程唤醒
notify() 随机唤醒单个线程
notifyAll() 唤醒所有线程
消费者等待
阻塞队列实现等待唤醒机制
put数据时:放不进去,会等着,也叫做阻塞
take数据时:取出第一个数据,取不到会等着,也叫做阻塞。
阻塞队列的继承结构
多线程的六种状态
Java虚拟机中是没有定义运行状态的。因为当线程抢到CPU执行权的时候,Java虚拟机就将当前线程交给操作系统进行管理,虚拟机不管就不进行定义了。
类比:手机被偷了,后续手机坏了需要维修就与你无关了。
线程池
线程池是有上限的
线程池的核心原理
创建一个池子,池子是空的
提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下次再提交任务时,不需要创建新的线程,直线复用已有的线程即可。
如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
线程池代码实现
- 创建线程池
- 提交任务
- 所有的任务全部执行完毕,关闭线程池【实际开发中一般不会关闭的,服务器是24小时不关】
第一个说是没有上限的线程池,实际上是是int
的最大值2的31次方,21亿多
public static ExecutorService newCachedThreadPool() 创建没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池
MyRunnable.java
1 | package 多线程_线程池; |
MyThreadDemo.java
1 | package 多线程_线程池; |
自定义线程池
1 | /** |
内部类:
自定义线程池的工作原理
+
- 当核心线程满时,再提交任务就会排队
- 当核心线程满,队列也满了,就会创先临时线程(非核心线程)
- 当核心线程、队列和临时线程(非核心线程)都满了,就会触发任务拒绝策略。
核心线程和临时线程
只有在核心线程都被用完了并且队列长度已经排满了才会启动非核心线程(临时线程)
自定义线程池(任务拒绝策略)
线程池多大比较合适?
最大并行数:
4核8线程
1 | int count = Runtime.getRuntime().availableProcessors(); |
查看最大核心线程 为8
- CPU密集型运算
计算比较多,读取数据库IO比较少属于这种类型,最大并行数+1,+1的原因是保障当前项目因为页缺失故障或其他故障导致线程暂停,这个额外的线程就可以顶上去,保证CPU时钟周期不被浪费
- IO密集型运算
操作数据库、RPC、IO操作的时候,CPU闲置下来
异常
异常体系介绍
java.lang.Throwable
- Error
- Exception
- RuntimeException
- 其他异常
Exception:叫做异常,代表程序可能出现的问题
运行时异常:RuntimeError及其子类,编译阶段不会出现异常题型。运行时出现的异常(如数组索引越界异常)
编译时异常:编译阶段就会出现异常提醒的
反射
反射允许对封装类的字段、方法和构造方法的信息进行编程访问
例如idea自动提示一个类中有哪些方法,或者方法括号中的形参参数类型是哪些?
通俗的话讲,反射就是可以从类中拿东西
为什么要有反射?
Redis的Java客户端
静态初始化块,在类加载时执行,用于初始化静态成员变量或执行一些静态操作。
1 | package com.heima; |
单例模式
JedisConnectionFactory
类中的 jedisPool
属性被声明为 static
,并在【静态代码块】的静态初始化块中初始化。这样做确保了在整个应用程序生命周期中只有一个 Jedis 连接池实例。
工厂模式
类中的getJedis()方法充当了工厂方法,用于创建Jedis对象。它封装了Jedis连接池的创建和配置细节,并提供了一个统一的接口来获取Jedis对象。