Java基础知识
1 数据类型
- 基本数据类型
数据类型 | 关键字 | 取值范围 | 内存占用(字节/byte) |
---|---|---|---|
整数 | byte | -128~127 | 1 |
short | 2 | ||
int | 4 | ||
long | 8 | ||
小数 | float | 4 | |
double | 8 | ||
字符 | char | 0-65535 | 2 |
布尔 | Boolean | true,false | 1 |
- 引用数据类型
Java变量分为成员变量和局部变量
成员变量:声明后在类初始化时会赋予默认值,可以直接使用
局部变量:声明后必须赋值才可以使用,否则编译不通过
2 键盘录入数据
Scanner scanner = new Scanner(System.in);
//键盘录入字符串
String next = scanner.next();
//键盘录入整数
int i = scanner.nextInt();
//键盘录入小数
double v = scanner.nextDouble();
//键盘录入布尔值
boolean b = scanner.nextBoolean();
3 字符串拼接
System.out.println(1+2+"三"); // 3三
System.out.println("三"+1+2); // 三12
System.out.println("三"+(1+2)); // 三3
4 类型转换
-
隐式转换:
- 运算过程的隐式转换:取值范围小的数据和取值范围大进行运算,小的会先提升为大的(byte<short<int<long<float<double),再进行运算
- byte、short、char这三种类型先转换为int类型再进行运算
-
强制转换:
- 常量优化机制:给一个变量赋值,如果等于号的右边是常量的表达式并且没有一个变量,那么就会在编译阶段计算该表达式的结果,然后判断该表达式的结果是否在左边类型所表示范围内,如果在,那么就赋值成功,如果不在,那么就赋值失败。但是注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制
byte b1 = 1 + 2;
System.out.println(b1); // 输出结果 3
byte b1 = 3;
byte b2 = 4;
byte b3 = b1 + b2;
System.out.println(b3); // 程序报错
byte b1 = 127 + 2;
System.out.println(b4); // 程序报错,超出byte类型取值范围
- 扩展赋值运算符(+=、-=、*=、、/=、%=)隐含了强制类型转换
- 引用数据类型和基本数据类型转换需要通过包装类(装箱、拆箱)实现
5 逻辑运算符
位运算符:
&:与
|:或
!:非
^:异或,相同为false,不同为true
- 短路逻辑运算符:&&、||
- 优先级:&& > ||
5.1 逻辑运算符操作数值
System.out.println(10 & 2);
// 2
10:0000 1010
2: 0000 0010
结果 0000 0010 :2
一个数异或同一个数两次,结果还是那个数。且异或的顺序可变
异或满足交换律、结合律
因为一个数异或自己等于0000 0000,而0000 0000异或任何数字,结果是任何数字本身
a ^ a = 0
a ^ 0 = a
不声明第三个变量交换两个变量的值
int a = 10;int b = 20;
a=a^b; //a=10^20
b=a^b; //b=10^20^20; //b=10
a=a^b; //a=10^20^10; //a=20
6 switch语句
-
case后面的值只能是常量,不能是变量;不允许重复
-
switch语句()中可以接受的类型
- 基本数据类型:byte、short、int、char
- (不能用long, byte、char、short 类型在编译期默认提升为 int,long转为int会丢失精度)
-
引用数据类型:jdk5开始可以传入枚举;jdk7开始可以传入String
-
jdk14版本以后,case后面可以编写多个值,用逗号分隔
-
范围性判断建议用if,固定值匹配建议使用switch
7 break 和 continue
- break用于循环和switch中,continue用于循环中
- 循环嵌套,break和continue默认只能操作自己所在的那一层循环(操作指定层可以用标号)
/*
http: 标号
//www.baidu.com 注释
*/
http://www.baidu.com
8 数组
8.1 数组定义格式
int[] array1; //常用
double array2[];
//只是声明变量,并未创建出容器
8.2 数组静态初始化
- 指定数组元素,由系统计算数组长度
int[] array1 = new int[]{1,2,3,4}; //完整格式
int[] array2 = {1,2,3,4}; //简化格式,常用
System.out.println(array1); //[I@776ec8df
//776ec8df是array1在内存中的十六进制地址值,@是分隔符,[表示当前空间类型是数组,I表示是int类型
8.3 数组遍历
String[] array3 = {"张三", "李四", "王五"};
for (int i = 0; i < array3.length; i++) {
System.out.println(array3[i]);
}
//增强for
for (int i : arr3) {
System.out.println(i);
}
8.4 数组动态初始化
- 指定数组长度,由系统分配默认初始值
- 初始值
- 整数:0
- 小数:0.0
- 布尔:false
- 字符:'\u0000' ------>Unicode字符
- 引用数据类型:null
- 初始值
int[] arr = new int[10];
两种数组初始化方式使用场景
静态:已知具体数组元素
动态:只知道数组长度,不知道具体元素
8.5 数组内存图
- 栈:方法运行时进入的内存
- 堆:new出来的东西会在堆中开辟空间并产生地址值
- 方法区:字节码文件加载时进入的内存
- 本地方法栈
- 寄存器
数组下标越界异常:
空指针异常:当引用数据类型的变量,记录为null,就意味着和堆内存的连接被切断了,继续访问堆内存数据,就会触发空指针异常
8.6 数组反转
{1,2,3} - > {3,2,1}
- 创建新数组,遍历旧数组,依次把元素倒序赋值给新数组
- 循环数组.length/2次,通过临时变量把数组元素前后对调
- 模拟指针,创建start,end,分别指向数组第一个和最后一个元素,两个元素通过临时变量交换值,然后start++、end--,循环条件start < end
9 二维数组
9.1 数组定义格式
int[][] arr = {{1, 2, 3}, {1, 2, 3}};
//简化格式
9.2 遍历
int[][] arr = {{2, 3},
{4, 5, 6}};
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
10 方法
- idea方法抽取快捷键:Ctrl+Alt+M
10.1 通用定义格式
10.2 调用流程
方法没有被调用的时候,在方法区中的字节码文件中存放
方法被调用的时候,需要进入到栈内存中运行
10.3 方法重载
同一个类中,方法名相同、参数不同(参数类型不同,参数个数不同,参数顺序不同)
识别是否构成重载,只看方法名和参数,与返回值无关
10.4 方法参数传递
-
基本数据类型:数据值传递
-
引用数据类型:地址值传递
Java是值传递还是址传递:是值传递,因为地址值也是值
11 面向对象
类名 对象名 = new 类名();
11.1 对象内存图
- 创建多个对象,方法区中该类的字节码文件只加载一次
- new 对象()的成员变量存储在堆中,成员方法存储地址,指向方法区字节码中的成员方法
- 多个对象的内存空间相互独立,互不干扰,调用成员方法时,找的是同一份成员方法
11.2 成员变量和局部变量
区别 | 成员变量 | 局部变量 |
---|---|---|
类中位置 | 方法外 | 方法内 |
初始化值 | 有默认初始值 | 没有默认值,使用前必须先初始化 |
内存位置 | 堆内存 | 栈内存 |
生命周期 | 随着对象创建而产生,随着对象消失而消失 | 方法调用才存在,方法运行结束就消失 |
作用域 | 在自己所属的大括号中 | 在自己所属的大括号中 |
11.3 this
- this用法:调用自己本类的成员(this.本类成员变量、this.本类成员方法)
- this省略规则:
- 没有与局部变量同名的成员变量,可以省略this关键字
- this.本类成员方法() : this.可以直接省略
- this代表当前类对象的引用(地址),哪个对象调用方法,this代表哪一个对象
11.4 构造方法
-
构造方法(构造器):构建、创造对象的时候所执行的方法,方法名与类名相同,没有返回值类型,没有返回值(return)
-
每创建一次对象,就执行一次构造方法
-
构造方法作用:
- 创建对象
- 给对象的属性初始化
-
当一个类中没有写构造方法,系统会提供一个默认的无参构造方法;写有构造方法,系统就不提供
-
构造方法不允许手动调用
11.5 封装
- 隐藏实现细节,仅暴露公共访问方式
- 把属性抽取到类中是对数据的封装、把代码抽取到方法中是对功能的封装
被private修饰后只能在本类中被访问
默认修饰符:可以在同一类、同一包中被访问
protected:除了在同一类、同一包中被访问,也可以通过子类对象访问父类protected变量,
本质上protected只支持相同包内访问,通过继承使用子类对象实现不同包访问
访问的还是父类方法public:任意位置可以访问
11.6 标准JavaBean
- 成员变量全部使用private修饰
- 提供空参构造方法和带参构造方法
- 提供get和set方法
12 字符串
12.1 API
-
API:应用程序编程接口
-
JDK API使用
-
显示 -> 索引 -> 键入关键字进行查找
查看类所在的包
查看类的作用
查看构造方法,知道怎么创建对象
查看成员方法(方法描述,方法名、参数、返回值)
-
-
Scanner的方法next()和nextLine()区别
next():以空格或者Tab作为结束标记,可能接受数据不完整
nextLine():以回车换行作为结束标记,在接收之前调用next()、nextInt()...可能缺失录入机会
12.2 String
- 所有双引号的字符串,都是字符串对象,存储在方法区中的字符串常量池
- 字符串一旦创建,内容不可更改,可以共享
- 双引号的字符串对象存储在字符串常量池
- 在jdk8以前字符串常量池在方法区中,8以后迁移到了堆中
String a = "abc";
String b = "abc";
System.out.println(a == b); //true、在字符串常量池中存在"abc"后就共享,不再重新创建
-
构造方法
public String():使用空白字符串创建了一个字符串对象
public String(char[] value):将字符数组的每个字符拼成一个字符串返回
public String(String original):
String a = "abc"; String b = new String("abc"); System.out.println(a == b); //false,a存储的是常量池中"bcd"的地址值,b存储的是是堆中new String的地址值,b的存储内容是常量池中"bcd"的地址值
-
==和equals区别
-
==是操作符,equals是方法
-
==:基本类型比较的是值,引用类型比较的是地址值
-
equals:基本类型变量不能使用equals,引用类型默认情况(未重写方法)比较的是地址值,String类重写了该方法,比较的是值
-
-
String类成员方法
-
equalsIgnoreCase():忽略大小写比较
-
toCharArray():字符串转为字符数组。可用于字符串遍历(第1种方式)
如果是字符的0-9在进行比较运算时要加上单引号,因为在ASCII码表中,字符0('0')和十进制0(0)是不一样的
char a = '0'; if (a >= '0') { }
-
charAt(int index):返回指定索引处的字符值。可用于字符串遍历(第2种方式)
-
length():返回字符串的长度(字符串中字符个数)
char[] a = {'0', 'a'}; String b = "abc"; System.out.println(a.length); System.out.println(b.length()); //数组.length是在获取数组的属性;字符串.length()是在调用方法
-
substring(int beginIndex[, int endIndex]):用于字符串截取,传入一个int整数就从该索引开始截取到末尾,传入两个就从开始索引截取到结束索引,含头不含尾
String s = "abcde".substring(2); //cde String s1 = "abcde".substring(2, 4); //cd
-
replace(String target, String replacement):字符串替换方法,target是旧值,替换为replacement
-
split(String regex):根据给定正则表达式的匹配拆分此字符串,返回字符串数组;如果参数规则在字符串中不存在,会将字符串整体放在一个长度为1的字符串数组中
-
正则表达式
String a = "abc";
String b = "a";
String c = b + "bc";
System.out.println(a == c); //false,加号相当于new StringBuilder.append()
String a = "abc";
String c = "a" + "b" + "c";
System.out.println(a == c); //true、常量优化机制
12.3 StringBuilder
StringBuilder:一个可变的字符序列(字符序列(缓冲区)理解为容器),向容器中添加任意类型的数据,最终都是在完成字符串的拼接
-
构造方法
- StringBuilder():空参构造,初始以""创建容器,初始容量为16个字符,自动扩容
- StringBuilder(字符串):创建容器,初始带着字符串参数
- StringBuilder(字符序列):创建容器,初始带着字符序列转成的字符串
- StringBuilder(整型):创建容器,初始容量为传入的整形数据
-
成员方法
-
append(任意类型):向容器的末尾添加任意类型的数据,实现字符串的拼接,返回对象本身
-
length():返回容器长度(字符个数)
-
reverse():字符串反转,返回对象本身
-
toString():将StringBuilder对象转为String对象
String对象转为StringBuilder对象:
- 调用StringBuilder构造方法传入String对象:new StringBuilder(字符串)
- 调用StringBuilder空参构造方法,然后append(字符串):new StringBuilder().append(字符串)
-
字符串拼接
- 使用+完成字符串拼接,缺点是每使用一次加号就创建了一个StringBuilder对象
- 使用StringBuilder的append()方法,效率更高
System.currentTimeMillis():当前时间和午夜(1970 年 1 月 1 日)UTC 之间的差异(以毫秒为单位)。
链式编程:
StringBuilder和StringBuffer
StringBuilder 其实是和 StringBuffer 几乎一样,只不过 StringBuilder 是
非线程安全
的
深入理解String、StringBuilder 和 StringBuffer
https://cloud.tencent.com/developer/article/1645883
13 集合
集合:一种容器,大小可变(数组长度不可变),存储元素对象的地址
存储元素个数固定不变用数组,存储元素个数经常改变用集合
数组可以存储基本和引用数据类型,集合只能存储引用数据类型
集合分类:不同集合底层数据结构不同,适用场景不同
- 单列集合(Collection接口)
- List接口:存储有序,有索引,元素可以重复
- ArrayList:底层是数组,查询修改比较快,增删慢
- 增删慢:每一次增删、可能要大批量的移动数组内元素
- LinkedList:底层是双向链表,增删相对比较快,首尾操作极快
- ArrayList:底层是数组,查询修改比较快,增删慢
- Set接口:存储无序,没有索引,元素不能重复
- TreeSet:底层是红黑树,按照大小默认升序排序,不重复,无索引
- HashSet:底层是哈希表,无序,不重复,无索引
- LinkedHashSet:底层是哈希表和双向链表,有序、不重复、无索引
- List接口:存储有序,有索引,元素可以重复
- 双列集合(Map)
13.1 Collection
- Collection是单列集合的顶层接口,它的功能是全部单列集合都可以继承使用
- add:添加元素
- clear:清空集合
- remove:在集合中删除给定元素
- contains:判断当前集合中是否包含给定对象
- isEmpty:判断是否是空集合
- size:返回集合中元素个数
remove、contains方法依赖于equals方法的重写
- 集合通用遍历方式:
- 迭代器遍历 Iterator
- 获取迭代器对象 Iterator
it = 集合.iterator(); - 判断集合中是否还有元素 it.hasNext()
- 调用next方法取出元素(每次循环next方法尽量只调用一次)
- 获取迭代器对象 Iterator
- 增强for循环(迭代器的语法糖)
- 可以遍历集合也可以遍历数组
- JDK5之后出现,内部原理是一个Iterator迭代器,遍历集合相当于是迭代器的简化写法
- 实现Iterable接口的类才可以使用迭代器和增强for,Collection接口已经继承了Iterable接口。
- lambda表达式遍历(forEach)
- JDK8开始,结合lambda遍历集合
- default void forEach(Consumer<? super T> action):参数是Consumer接口的实现类对象,可以是匿名内部类和lambda表达式
- 迭代器遍历 Iterator
13.2 List
remove:没有自动装箱,需要手动装箱
List派系集合多了一种遍历方式:普通for循环
13.2.1 ArrayList
- ArrayList长度可变原理
- 当创建ArrayList集合容器的时候,底层是一个空数组长度为0,当调用add方法创建一个长度为10的数组
- 扩容原数组1.5倍大小的新数组(旧容量+旧容量位运算>>1)
- 将原数组数据,拷贝到新数组中
- 将新元素添加到新数组
泛型可以对集合中存储的数据类型做限制,只能写引用数据类型
-
成员方法
- add(E e):将指定的元素添加到此集合的末尾
- add(int index,E e):在集合的指定索引位置插入指定的元素(索引值只能是[0,集合.size()],即第一个元素和最后一个元素的后面新增)(插队新增)
- get(int index):返回指定索引处的元素
- size():返回集合中元素的个数
- remove(int index):删除指定索引处的元素,返回被删除的元素
- remove(Object o):删除指定的元素,返回是否删除成功
- set(int index,E e):修改指定索引处的元素,返回被修改的元素
-
集合遍历,做元素删除
正序遍历,删除元素后要 i--
倒序遍历,删除元素后不需 i--
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
if("test".equals(s)){
list.remove(i);
i--;
}
}
for(int i = list.size() - 1; i >= 0; i--){
String s = list.get(i);
if("test".equals(s)){
list.remove(i);
}
}
字符串之间做比较, 建议使用常量调用方法, 能够避免一些不必要的空指针异常
13.2.2 LinkedList
linkedList.get(i):不是通过索引获取元素,而是遍历
1, 判断你要查找的元素,离头部近,还是离尾部近(index < (size >> 1))
2, 离头部近:从头开始查找(正序遍历)
3, 离尾部近:从尾开始查找(倒序遍历)
并发修改异常(ConcurrentModificationException):迭代器遍历集合的时候,使用集合的添加或删除,修改的方法,操作集合中的元素,就会出现并发修改异常
解决方法:迭代器在遍历的时候,使用迭代器自己的删除方法
如果使用集合对象,删除的是倒数第二个元素,不会出现ConcurrentModificationException
原因:逃过了编译器的异常检查
集合在遍历的过程中进行删除操作
迭代器
- 无需手动 -- 操作,源码中自动执行
增强for
- 增强for是迭代器的语法糖,迭代器遍历集合的时候,使用集合的删除ConcurrentModificationException
forEach
- 不能进行删除
待补充
普通for
- 正序遍历,i--
- 倒序遍历
13.3 Set
13.3.1 泛型
泛型好处:对集合中存储的数据进行类型限制,只能写引用数据类型
避免了类型的强制转换(一个需要泛型的对象,没有指定泛型,默认是Object类型)
泛型通配符:?
E T K V
?和 T 的区别:
待补充
- 泛型类:在类名后面加
,在创建对象的时候指定数据类型 - 泛型方法
- 静态 :静态内容优先于对象存在,静态方法中如果加入泛型,需要声明出自己独立的泛型(在方法返回值前面加
);在方法调用的时候,才能够确定具体类型(根据实际参数去匹配); - 非静态:在类名后面加
或者方法返回值前面 ,需要在创建对象的时候,才能够确定具体的类型(根据类的泛型去匹配)
- 静态 :静态内容优先于对象存在,静态方法中如果加入泛型,需要声明出自己独立的泛型(在方法返回值前面加
- 泛型接口
- 实现类实现接口的时候,指定具体类型
- 将接口的实现类加入泛型,在创建实现类对象时确定具体类型(泛型传递)
- 泛型的限定
<? super E>
:可以传入类型E及其父类<? extends E>
:可以传入类型E及其子类
13.3.2 TreeSet
-
存取无序、无索引、"不可重复"
-
对集合中的数据进行排序,根据编码表中的数值进行排序操作,元素不能为null
-
TreeSet集合存储自定义对象,要是没有指定排序方式,会出现异常(ClassCastException)
- 让自己编写的类实现Comparable接口,指定要比较的泛型,重写compareTo方法使其具有可比性(返回值1为正序,-1为倒序,0则不存储-只存储根节点)
- 在compareTo方法中编写排序条件(主要排序条件和次要排序条件,如果所有条件都返回0(即相同元素),可以设置返回值为1或-1来保留元素)
-
TreeSet调用add方法会自动调用compareTo方法,根据返回值来决定元素存储方式
-
集合中存储的元素,是Java中己经写好的类,这些类中的排序规则,在源码中已经写好了。如果想改变源码中的排序规则,可以在创建TreeSet集合的时候使用有参构造,实现Comparator接口(比较器)。
如果同时具有自然排序,和比较器排序,会优先按照比较器指定的规则进行排序
- 类实现过Comparable接口:具有自然排序
- TreeSet构造方法中,传入Comparator接口的实现类对象:比较器排序
13.3.3 HashSet
-
保证元素唯一性(需要同时重写hashCode方法和equals方法),可以存null
-
重写hashCode方法时要让对象的所有属性都参与到运算中,尽量让属性不同的对象返回的哈希值不同
String sA="通话"; String sB="重地"; System.out.println(sA.hashCode()); //1179395 System.out.println(sB.hashCode()); //1179395
HashSet存储元素过程
- 添加元素先调用对象的hashCode方法,计算出一个应该存入的索引位置
- 去数组中查找该索引所在位置是否存在元素
- 不存在就直接存
- 存在就调用equals方法比较对象的内容
- 内容一样就不存
- 不一样就进行存储
-
HashSet底层的数据结构是哈希表结构
哈希表:
JDK8版本之前:数组+链表
JDK8版本之后:数组+链表 | 红黑树 -
hashCode方法
- 哈希值:JDK根据某种规则算出来的int类型的整数
@IntrinsicCandidate
public native int hashCode(); //调用C++代码计算出的一个随机数(常被称作地址值)
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
HashSet添加元素过程-JDK7
底层结构:数组+链表(结合哈希算法)
- 创建一个默认长度为16 的数组,数组名为table
- 根据元素的哈希值对数组长度进行取余操作,计算出应存的位置(哈希算法)
- 判断当前位置是否为null
- 为null直接存入
- 不为null,表示有元素,调用equals方法比较
- 如果equals返回true,表示元素内容相同,不存
- false则存入数组
- JDK7存储元素新元素占据老元素位置,新元素存储老元素地址(头插法)
- JDK8新元素添加在老元素后面,老元素存储新元素地址(尾插法)
哈希表是一种增删改查数据性能都较好的结构
HashSet添加元素过程-jdk1.8之后
底层结构:哈希表(数组+链表或者红黑树)
创建HashSet集合(底层是HashMap),内部会存在一个长度为16的数组
public HashSet() { map = new HashMap<>(); //使用默认初始容量16和默认加载因子0.75构造一个空HashMap }
调用add方法,会通过对象的hashCode方法计算出应存入的索引位置(哈希值%数组长度)
int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // ^ 异或 >>> 无符号右移 (h = key.hashCode()) ^ (h >>> 16) 的运算结果是返回哈希值,用该哈希值 % 数组长度计算出应存入的索引位置 (源码是(数组长度-1)& 哈希值,两者运算结果相同,但乘除效率低,所以选择 (数组长度-1)& 哈希值) h >>> 16:对哈希值进行扰动,进行二次哈希操作,可以一定程度减少数组元素位置链表挂载元素的数量
判断索引位置元素是否是null
- 是:存入
- 不是:说明有元素,调用equals方法比较内容
数组单个元素位置的链表存储元素过多会影响查询速度
- 扩容数组
- 条件:
- 当数组中的元素个数达到了16*0.75(加载因子)=12个,数组扩容原来2倍大小
- 当链表挂载的元素超过了8个,并且数组长度没有超过64,数组扩容原来2倍大小
- 链表转红黑树:
- 当链表挂载的元素超过了8个,并且数组长度达到了64,会将超过8个元素的链表转为红黑树
以上扩容或转红黑树后元素会重新存储,即在数组中的位置会改变
13.3.4 LinkedHashSet
-
存取有序(元素存入和取出顺序一致),不重复,无索引
-
底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。
13.3.5 HashSet集合保证元素唯一原理
HashSet元素唯一依赖于hashCode和equals方法。也就是说如果要存储自定义对象到HashSet集合是必需要重写hashCode和equals方法。
Hashset底层是HashMap,数据结构是哈希表,jdk7前是数组加链表结构,8之后是数组加链表或红黑树
在创建集合后调用HashSet的add方法向集合中添加元素时,内部会初始化一个长度为16的空数组,然后会进行哈希运算算出哈希值,再拿着哈希值模于当前数组长度得出当前元素应存入数组的索引位置(源码中是拿着哈希值和当前数组长度-1做与运算,计算出应存入的索引位置,两者计算结果是相等的,但相比之下计算机做乘除运算是比加减运算效率要高,所以选择后者)
具体哈希运算是调用该对象的Hashcode方法算出hashcode值,这是一次哈希操作,再将hashcode值无符号右移16位与原哈希值做异或运算,对原哈希值进行哈希扰动,这可以一定程度减少数组单个索引位置的链表挂载元素的数量(因为链表元素越多,查询效率越低)
jdk7哈希运算是5次异或和4次位运算
得到索引位置后,判断数组中该位置是否是null,是的话直接存储,不为null说明该位置有元素,调用equals方法与改索引位置的链表元素依次比较,内容相同不存储,这就保证了元素唯一,内容不同就存储,在链表中插入元素
- HashMap在jdk1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。
- 在jdk1.8中采用的是尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
扩容机制和链表转红黑树机制
jdk7扩容(数组长度 * 2)条件是(同时满足)
数组元素个数达到阈值(数组长度 * 负载因子)
要插入的索引位置已经存在元素(即哈希冲突)
jdk8转红黑树机制(满足其中一个条件就扩容)
单链表不会一直增加元素,当元素个数超过8个时,会尝试将单链表转化为红黑树存储。但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64才树化。否则,进行扩容操作。
数组元素个数达到阈值(数组长度 * 负载因子),也会扩容
HashMap在1.7和1.8之间的变化:
- 1.7中采用数组+链表,1.8采用的是数组+链表/红黑树,即在1.8中链表长度超过一定长度后就改成红黑树存储。
- 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
- 1.7是采用表头插入法插入链表,1.8采用的是尾部插入法。
- 在1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题;在1.8中采用尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了
- 1.7中是先扩容后插入新值的,1.8中是先插值再扩容
13.4 Collections
-
addAll:给集合对象批量添加元素
-
shuffle:打乱集合元素的顺序
-
sort:传入一个List集合,将集合中元素按照默认规则排序(不能直接对存储自定义类型的List集合进行排序,除非自定义类型实现了比较规则的接口Comparable)
-
sort:传入一个List集合和比较器Comparator的实现类对象(匿名内部类),将集合中元素按照指定规则排序
13.5 Map
-
双列集合(键值对集合),key:无序,不重复,无索引 value:不做要求
-
HashMap(使用最多,效率高)、LinkedHashMap(extends HashMap)、TreeMap:map集合的特点由key决定,和set集合相同
-
HashTable
-
Map集合后面重复的键会覆盖前面重复键的值
- Map API
- Map集合遍历方式
- 根据键找值:通过keySet方法获取所有键的集合,遍历所有键的集合同时通过get方法获取每个键对应的值
- 把键值对看成整体进行遍历:通过entrySet方法获取所有键值对对象的集合,遍历集合的同时,提取键和值
- foreEach:传入BiConsumer接口的实现类对象(匿名内部类,lambda表达式),获取键和值
HashMap和HashSet底层原理一样,都是哈希表结构
Set集合底层由Map实现,只是Set中的数据只要键数据,不要值数据
如果键要存储的数据是自定义对象,需要重写hashCode和equals方法来保证键的唯一性
13.6 不可变集合
-
不可变集合创建:调用接口中的静态方法 of()
- List.of(数据类型...) Set.of(数据类型...) Map.of的参数不是可变参数(最多可以传10个)
- Map.ofEntries(Map.entry()...)
jdk8后接口中允许定义默认方法和静态方法
Collections.addAll:传入的值是单列集合
-
不可变:不能增删改
- 数组只是长度不可变,内容可变
14 static
-
static修饰成员变量、成员方法;修饰成员变量,该成员变量被所有对象共享,修饰成员方法用于工具类的制作
工具类:内部的方法,常被static修饰,方便调用
私有构造方法,防止创建对象
一般会有文档注释解释说明工具类中方法的作用,参数,返回值
-
static修饰的成员被类的所有对象共享
-
随着类的加载而加载,优先于对象存在
-
可以通过类名调用,也可以通过对象名调用
-
使用IDEA生成帮助文档
字符编码设置:-encoding utf-8 -charset utf-8
15 继承
- 提高代码复用性
- 提高代码可维护性
- 是多态的前提
15.1 重写
- 子父类中,方法名、参数、返回值完全相同
- 父类中私有方法不能被重写
- 静态方法不能被重写(子类中可以存在与该静态方法完全相同的方法,只是隐藏了父类方法,并非重写)
- 子类重写方法时,访问权限必须大于等于父类
super.父类成员:在子父类中,没有重名的成员变量,也不涉及方法重写的情况,super.可以省略
15.2 继承中构造方法
- 子类所有构造方法第一行默认(隐式)调用父类无参构造,即子类完成初始化之前,一定要完成父类初始化
- 构造方法的第一条语句默认都是super()(写了this()调用其他构造方法就没有了super())
- this()和super()必须放在构造方法的第一行,二者不能共存
开闭原则:对扩展开放、对修改关闭
父类的私有内容子类其实也继承到了,但不能使用,访问权限不够
15.3 抽象类
- 抽象类:一种特殊的父类
- 抽象方法:抽取子类共性方法发现无法具体实现,该方法应该声明为抽象方法
- 含有抽象方法的类一定是抽象类,抽象类不一定有抽象方法
- 抽象类中可以有非抽象方法,给子类直接继承使用
- 抽象类有构造方法,不能实例化(可以初始化让子类使用父类的非抽象内容( super() ),不能实例化是因为抽象内容未实现)
- 抽象类的子类要么重写所有抽象方法,要么也声明为抽象类
abstract冲突关键字:final、static、private
15.4 接口
-
接口体现的思想是对规则的声明,Java中接口更多是对行为的抽象
-
接口不能实例化
-
接口的子类(实现类),要么重写接口中所有的抽象方法,要么是抽象类
-
接口可以多实现
-
接口中的方法可以省略public和abstract修饰符
-
接口中的成员变量只能是常量,默认修饰符public static final,可以省略
-
接口中没有构造方法
接口的权限修饰符public不写不代表省略,代表权限修饰符的默认
抽象类和接口的使用场景
抽象类:对事物做抽象(描述事物)
接口:对行为抽象(制定规则)
jdk8后接口中允许定义默认方法和静态方法(解决接口升级问题)
默认方法:default修饰(public可以省略,default不能省略),不强制被重写,可以被重写,重写时去掉default关键字
同时实现多个接口中相同默认方法,实现类需要强制重写,避免逻辑冲突。调用接口中默认方法:接口名.super.方法名()
静态方法:static修饰(public可以省略,static不能省略),只能通过接口名调用,不能通过实现类的类名和对象名调用
jdk9后接口中可以定义私有方法
private修饰
可以私有静态,不能私有默认
接口中default、static、private修饰的方法必须有方法体
- 接口和类之间的关系
- 类和类:只支持单继承,支持多层继承
- 类和接口:可以多实现,可以继承类的同时实现接口
- 接口和接口:可以多继承
15.5 多态
多态:同一个行为具有多个不同表现形式或形态的能力
-
多态前提:继承/实现关系、有方法重写、父类引用指向子类对象
-
多态创建对象之后,调用成员的访问特点
- 成员变量:编译看父类,运行看父类,因为是父类的引用,所以只能看到堆内存中super那一小块空间
- 成员方法:编译看父类,运行看子类。因为父类的方法可以是抽象方法,所以运行的是子类的方法。
- 静态方法:静态的内容推荐类名调用。就算写为对象名调用,编译的时候也会改成类名调用,即执行父.静态方法
- 多态优缺点
优:提高了代码的扩展性,指将方法形参设计为父类类型,就可以接受父类的任意子类对象
缺:不能调用子类的特有属性和行为
解决办法:1. 目的是使用子类内容就直接创建子类对象去调用
2. 向下转型
- 多态的转型
- 向上转型:父类引用指向子类对象 Animal a = new Dog();
- 向下转型:将子类的对象,重新赋值给子类引用 Dog d = (Dog)a;
可以会出现的异常:ClassCastException类型转换异常。在引用数据类型的强制转换中,实际类型和目的类型不匹配会出现该异常
16 final
-
修饰方法:该方法不能被重写
-
修饰变量:表明该变量不能再次被赋值(对于引用数据类型,是地址值不能改变,其上存储的内容可以改变),并且在声明时一定要初始化,即赋值
final修饰成员变量,默认值不被允许,只能在创建变量时赋值或者构造方法结束之前完成赋值(即在构造方法中完成变量赋值)
-
修饰类:该类不能被继承
java常量命名规则:全字母大写,单词之间下划线隔开
17 代码块
使用 {} 包裹的代码称之为代码块
- 局部代码块
- 在方法中定义,用于限定变量的作用域(局部代码块中的 变量在代码块执行结束就被释放,代码块外面无法访问到),及早释放,提高内存利用率
- 构造代码块
- 在类中方法外定义,构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。
编译后的.class文件
-
静态代码块
- 在类中方法外定义,使用static修饰,随着类的加载而执行,只执行一次,在类加载的时候做一些数据初始化的操作(在项目启动的时候加载配置文件)
-
同步代码块:多线程中使用
18 包
-
如果使用的类和当前类不再同一个包中,需要写导包代码
-
如果自己编写的类和Java提供的类重名,使用全类名创建对象
19 设计模式
19.1 适配器(Adapter)模式
- 问题:实现接口时并不想全部实现接口内的方法
写一个适配器类实现接口,对接口所有方法做空实现
声明适配器类为抽象类,防止创建对象
让原本的类继承适配器类,重写所需方法
19.2 模板设计模式
模板设计模式:
把抽象类整体看作一个模板,模板中不能决定的东西定义成抽象方法
让使用模板的类继承抽象类重写抽象方法实现需求
模板中不想被修改的方法可以使用final修饰
19.3 单例设计模式
- 保证类的对象在内存中,只有一个
饿汉式 :
- 私有化构造方法
- 定义一个 private static final 修饰的常量,常量值就是对象
- 定义一个方法返回对象
- 在外界调用方法获取对象
懒汉式:
public class Student {
private Student(){}
// 懒汉式
// 核心:延迟加载
private static Student s;
public static Student getInstance() {
if(s == null){
s = new Student();
}
return s ;
}
}
多线程操作的时候,有可能会创建出多个对象
public class Student {
private Student(){}
private static Student s ;
public static Student getInstance() {
synchronized (Student.class) {
if(s == null){
s = new Student();
}
}
}
return s ;
}
每个线程都先加锁后判断,效率低
public class Student {
private Student(){}
private static Student s ;
public static Student getInstance() {
// 如果现在对象已经有了,而且,锁关闭了
// 为了提高代码的效率,所以,我们要在外面,再加一个非空判断
if(s == null){ // 作用:提高效率
synchronized (Student.class) {
if(s == null){ // 作用:保证唯一
s = new Student();
}
}
}
return s ;
}
}
先判断,效率高,同时也避免线程不安全问题
20 内部类
- 成员内部类
- 在类中创建一个类
- 创建对象格式
外部类名.内部类名 对象名 = new 外部类名().new 内部类名();
- 内部类访问外部类成员可以直接访问(包括私有成员);外部类访问内部类成员需要创建内部类对象访问
MyOuter.this.num
- 静态内部类
- 访问静态内部类的非静态方法和静态方法
- 局部内部类
- 局部内部类放在方法、代码块、构造器等执行体中
public class ClassD {
public static void main(String[] args) {
new Test().methodA();
}
}
class Test {
public void methodA() {
class Inner {
public void methodB() {
System.out.println("Test类中的methodA方法中的内部类的methodB方法");
}
}
//局部内部类外面访问不到,只能在methodA方法被调用时Inner类才被加载,通过Inner对象调用methodB方法
new Inner().methodB();
}
}
- 匿名内部类
- 匿名内部类本质上是一个特殊的局部内部类(定义在方法内部),可以作为方法的实际参数进行传输
- 前提需要存在一个接口或类,可以将继承一个类(实现一个接口),方法重写,创建对象,三布合成一步完成
- new 类名(){}:继承这个类
- new 接口名(){}:实现这个接口
方法的形参是接口类型,方法的实参是该接口的实现类对象
- Lambda表达式
- Lambda表达式时JDK8开始后的一种新语法形式,用来简化匿名内部类的书写
- 只能简化函数式接口的匿名内部类的书写形式
- 函数式接口:有且仅有一个抽象方法的接口(可以在接口上添加@FunctionalInterface来检查是否是函数式接口)
- 一个函数式接口有且只有一个抽象方法。
- 默认方法不是抽象方法,因为它们已经实现了。
- 重写了超类Object类中任意一个public方法的方法并不算接口中的抽象方法
- 函数式接口:有且仅有一个抽象方法的接口(可以在接口上添加@FunctionalInterface来检查是否是函数式接口)
-
Lambda表达式省略写法
- 参数类型可以省略不写
- 如果只有一个参数,()也可以省略
- 如果Lambda表达式的方法体代码只有一行,可以省略大括号,同时要省略分号;此时如果这行语句是return,必须省略return不写
-
Lambda表达式和匿名内部类区别
- 所需类型不同:匿名内部类可以操作类和接口,Lambda表达式只能操作函数式接口
- 使用限制不同:如果接口中有且仅有一个抽象方法,lambda表达式和匿名内部类都可以使用;如果接口中多于一个抽象方 法,只能使用匿名内部类
- 实现原理模式:匿名内部类编译之后会产生一个单独的.class字节码文件,Lambda表达式不会产生
-
lambda表达式不是匿名内部类的语法糖
21 常用API
21.1 Object类
-
所有的类都直接或间接继承了Object类,Object的方法一切子类都可以直接使用
-
toString()
- Object的toString方法打印的是地址值(全类名@对象的十六进制哈希值)
- 打印对象名的时候,会自动调用该类的toString方法,如果打印的不是地址值,说明该类或其父类重写了toString方法
-
equals()
- Object的equals方法默认的比较逻辑是 == ,比的是地址值
Object的toString方法的意义是为了被子类重写,以返回对象的内容信息而不是地址信息
Object的equals方法的意义是为了被子类重写,以便子类自己来定制比较规则
idea生成的equals方法阅读
- Objects类(继承于Object,jdk1.7之后有的工具类)
- equals:与Object的equals区别是加入了非空判断,仍需子类重写equals方法制定比较规则,否则还是比较地址值
- isNull:判断变量是否为null,返回布尔值
Objects类的equals方法源码
21.2 Math
- abs():获取参数绝对值
- ceil():向上取整
- floor():向下取整
- round():四舍五入
- max():求两个参数较大值
- min():
- pow():求a的b次方
- random():返回值为double的随机值,范围 [0.0 , 1.0)
21.3 System
-
exit():终止当前运行的Java虚拟机,非零表示异常终止
-
currentTimeMills():返回以毫秒为单位的当前时间
-
arraycopy(Object src, int srcPos, Object dest, int destPos, int length):数组拷贝
21.4 BigDecimal
解决小数运算中,出现的不精确问题(十进制小数转二进制无法精确转换导致)
- 构造方法
- BigDecimal(double val):不推荐
- BigDecimal(String val):推荐(或者使用BigDecimal的valueOf()方法)
- 加减乘除(add、subtract、multiply、divide)
- divide方法可以传3个参数(被除数、保留小数位数、舍入模式【RoundingMode.UP、RoundingMode.DOWN、RoundingMode.HALF_UP】),只传一个被除数遇到除不尽的情况会ArithmeticException
- doubleValue():字符串转double
21.5 包装类
-
将基本类型数据手动转为包装类(手动装箱)
- 构造方法new Integer():过时,不推荐
- 静态方法valueOf():推荐
-
jdk1.5之后自动装箱:基本数据类型可以直接赋值给引用数据类型的包装类
Integer i = 5;
- 手动拆箱
Integer i = 5;
int j = i.intValue();
System.out.println(j + 100);
- 自动拆箱
Integer i = 5;
System.out.println(i + 100);
结论:引用数据类型的包装类和基本数据类型可以直接做运算,无需手动转换
面试题
Integer i = 127; //自动装箱
Integer j = 127;
System.out.println(i == j); //true
Integer k = 128;
Integer l = 128;
System.out.println(k == l); //false
- == 比较是地址值,整数的范围在 -128~127 之间,结果是true,不在此范围是false
- Integer中有一个数组Integer[] cache,存储了256个封装好的Integer对象。自动装箱实际还是调用了Integer.valueOf(),如果装箱的数据在byte取值范围内,会从cache返回该数据对应的包装类对象,否则创建新的对象返回。好处是减少了对象的频繁创建,节约内存空间。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
21.6 Arrays
-
toString:将数组中的元素按照指定的格式,拼接为字符串并返回(并不是Object的toString重写)
-
equals:比较两个数组内容是否相同
-
binarySearch:查找元素在数组中出现的索引位置(二分查找法,只能是正序数组)
-
sort:对数组进行升序排列
21.7 日期类
21.7.1 JDK1.8之前
-
Date
- 构造方法:大部分已经过时,被Calendar等类取代
- 空参构造:将当前时间封装为Date时间对象
- Date(long time):根据传入的毫秒值封装Date时间对象
- 成员方法
- getTime:获取Date对象毫秒值(区别与System.currentTimeMillis)
- setTime:给Date中time赋值
- 构造方法:大部分已经过时,被Calendar等类取代
-
SimpleDateFormat:日期格式化类
-
构造方法
- SimpleDateFormat(String pattern):根据传入的字符串模式格式化时间对象
new SimpleDateFormat("yy-MM-dd HH:mm:ss a")
- 空参构造:默认格式去格式化时间对象
-
成员方法
- format(Date date):将日期格式化成日期/时间字符串
- parse(String source):将参数中的日期字符串,解析为Date时间对象
-
-
Calendar(抽象类,通过静态方法getInstance获取当前时间该类的子类对象)
- 成员方法
- get(int field):根据传入的字段,获取对应的日历信息(field:Calendar.YEAR、Calendar.MONTH、Calendar.DAY_OF_MONTH、Calendar.Day_OF_WEEK、Calendar.DAY_OF_YEAR)
- 成员方法
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.get(Calendar.DAY_OF_WEEK));
//星期日是一周中的第一天
char[] weeks = {' ', '日', '一', '二', '三', '四', '五', '六',};
System.out.println(weeks[calendar.get(Calendar.DAY_OF_WEEK)]);
-
-
- set(int field , int value):修改指定字段为value
- set(int year, int month , int date):修改年月日
- add(int field , int amount):设置字段偏移量
- setTime(Date date):将date转换为Calendar对象
- getTime():将Calendar对象转为Date对象
-
//判断2020年是不是闰年,闰年二月有29天
Calendar calendar = Calendar.getInstance();
calendar.set(2020, 2, 1);
calendar.add(Calendar.DAY_OF_MONTH, -1);
System.out.println(calendar.get(Calendar.DAY_OF_MONTH));
21.7.2 JDK1.8之后
- 日期时间类
- LocalDateTime
- 静态方法
- now:获取当前时间对应的LocalDateTime对象
- of:获取指定时间的对象
- 非静态
- toLocalDate:转为LocalDate对象
- toLocalTime:转为LocalTime对象
- getXxx:获取年月日时分秒相关的方法
- withXxx:修改时间对象的某个属性
- plusXxx:增加时间对象的某个属性
- minusXxx:减小时间对象的某个属性
- equals、ifBefore、isAfter:判断两个对象是否相等,在之前,在之后
- 静态方法
- LocalDate
- LocalTime
- LocalDateTime
LocalDateTime、LocalTime、LocalDate都是不可变的,withXxx、plusXxx、minusXxx、equals、ifBefore、isAfter返回都是新的对象
-
日期格式化类
-
DateTimeFormatter:用于时间的格式化和解析,代替SimpleDateFormat
- static DateTimeFormatter ofPattern(格式):获取格式化对象
格式改为单字符
yyyy-M-d H-m-s
可以接受1933-08-13 05-09-03
和1933-8-3 5-9-3
;改为双字符则不能接受1933-8-3 5-9-3
,会DateTimeParseException
- format:按照指定方式格式化
-
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-M-d H-m-s");
//按指定格式解析字符串,返回LocalDateTime对象
LocalDateTime date = LocalDateTime.parse("1933-8-13 05-9-3", dateTimeFormatter);
System.out.println(date);
//按照指定格式对LocalDateTime对象进行格式化,返回字符串
String s = dateTimeFormatter.format(date);
System.out.println(s);
时间日期字符串解析对象:LocalDateTime(解析时间日期)、LocalDate(解析日期)、LocalTime(解析时间)调用parse(要解析的字符串,格式化对象)
-
时间类
-
Instant
- now:获取当前时间(世界标准时间)戳
-
ZoneId
- 静态方法
- getAvailableZoneIds:获取Java中支持的所有时区
- systemDefault:获取系统默认时区
- of:获取指定时区
- 静态方法
-
ZonedDateTime:带时区的时间对象
-
-
工具类
- Duration:用于计算两个时间间隔(秒,纳秒)
- Period:用于计算两个日期间隔(年月日)
- ChronoUnit:用于计算两个日期间隔(ChronoUnit.YEARS.between())
22 常见算法
22.1 冒泡排序
for(int i = 0 ; i < arr.length - 1 ; i++){
for(int j = 0 ; j < arr.length - 1 - i ; j++){
//比较arr[j]、arr[j+1],交换位置
}
}
22.2 选择排序
- 从0索引开始,拿着每一个索引上的元素跟后面的元素依次比较
for(int i = 0 ; i < arr.length - 1 ; i++){
for(int j = i+1 ; j < arr.length ; j++){
//比较arr[i]、arr[j],交换位置
}
}
22.3 二分查找
- 二分查找(折半查找):在有序数组中查找
public int search(int num, int[] arr) {
int min = 0, max = arr.length - 1;
while (min <= max) {
int mid = (min + max) / 2;
if (num > arr[mid]) {
min = mid + 1;
} else if (num < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}
23 正则表达式
- 正则表达式:一个字符串,用来定义规则对其他字符串进行校验
24 递归
- 直接递归:方法自身调用自己。
- 间接递归:A方法调用B方法,B方法调用C方法,C方法再调用A方法。
- 递归一定要有条件限定,保证递归能够停止下来,否则会形成死循环并发生栈内存溢出(StackOverflowError)。
- 禁止构造方法递归。
- 斐波那契数列
25 Exception
异常信息从下往上看
-
异常体系
- Throwable
- Error:系统级别问题、JVM退出等,代码无法控制
- Exception:java.lang包下,称为异常类,程序本身可以处理的问题
- RunTimeException:运行时异常,编译阶段不会报错。
- 除RunTimeException外:编译时异常(受检异常),编译不通过,需要编写预处理方案(当异常产生执行的逻辑)
- Throwable
-
异常处理
-
默认处理流程:当程序出现异常,虚拟机创建一个异常对象抛出给调用者,不断向上传递,最终抛出给JVM虚拟机,虚拟机在控制台打印异常信息,然后终止虚拟机运行,产生异常的后续代码不会运行。
-
throws:在方法的后面,编写throws关键字,再跟上异常类名
-
你调用的方法,如果抛出了异常,就需要继续声明throws抛出异常
-
如果一个方法中抛出的异常特别多,可以选择抛出一个大的异常(不推荐)
-
子类在重写父类方法的时候,不能抛出父类中没有的异常,或者是比父类更大的异常
-
父类方法没抛异常,子类重写父类方法如果有异常只能try-catch
-
-
try { } catch( ) { }:try中包含可能会出现异常的代码,catch小括号中写异常接收对象,大括号中写异常处理逻辑
-
好处:产生异常后后续代码不会终止执行
-
IDEA快捷键:选中try要包裹的代码,CTRL+ ALT+ T
-
一个try可以写多个catch语句,处理范围最大的异常需要放在最后面
-
catch中可以同时捕获多个异常,中间使用进行" | "分割(不建议)
catch (ArithmeticException | ArrayIndexOutofBoundsException e){}
原因:无法对异常进行精准的处理
-
-
throw new Exception():手动抛出异常
-
自定义异常
- 自定义编译时异常:定义一个异常类继承Exception,重写构造器,在出现异常的地方用throw new 自定义异常抛出
- 自定义运行时异常:定义一个异常类继承RuntimeException,重写构造器,在出现异常的地方用throw new 自定义异常抛出
-
异常对象方法
- e.getMessage
- e.toString
- e.printStackTrace
-
throw和throws区别
- throw用在方法内表示手动抛出异常,如果异常对象是非 RuntimeException 则需要在方法声明时加上该异常的抛出,即需要加上 throws 语句 或者 在方法体内 try catch 处理该异常,否则编译报错,执行到throw语句后面的语句不再执行
- throws用在方法的定义上,表示方法执行可能抛出异常,需要由方法调用者进行处理
- finally
- finally的代码一定会执行(就算之前执行了return)(System.exit()除外)
try ... catch ... finally ...:
try ... finally ...:
26 常见数据结构
-
栈:后进先出、先进后出
-
队列:先进先出、后进后出
-
数组:查询修改速度快、删除效率低、添加效率极低
-
链表:查询修改慢,增删比较快,元素在内存中不连续存储
- 单向链表:每一个节点,记录下一个节点的地址和元素内容
- 查询的时候,只能从头开始找
- 双向链表:每一个节点,记录下一个节点的地址和元素内容的同时,还记录上一个节点的地址
- 查询的时候,可以从头找也可以从尾找
- 单向链表:每一个节点,记录下一个节点的地址和元素内容
-
树:
- 二叉树:
- 二叉查找树
- 平衡二叉树
- 左旋
- 右旋
- 红黑树
- 二叉树:
27 可变参数
public void methodA(int... a) {
System.out.println(Arrays.toString(a));
}
- 本质是一个数组,传入参数个数不指定
- 如果参数中存在可变参数,和其他参数,可变参数要放在最后面
28 Stream
-
单列集合数据到Stream流
集合对象.stream():
-
双列集合数据到Stream流
Stream
s1 = hm.keySet().stream(); 不推荐
Streams2 = hm.values().stream();
不推荐 原因:键和值分开存储了
Stream<Map.Entry<String,String>> s1 = hm.entrySet().stream();
原因:键和值都在entry(键值对)对象中 -
数组数据到Stream流
Arrays.stream(数组);
注意:数组需要是引用数据类型 -
同种数据类型的多个值,到Stream流
Stream.of()
-
Stream方法
-
foreach:遍历
-
count:返回流中元素个数
-
filter:过滤,对流中的数据进行筛选
- 重写Predicate的test方法,
- 根据方法的返回值boolean 决定元素是否保留
-
limit:只取前几个元素
-
skip:跳过前几个元素
-
concat:合并两个流
-
distinct:去重
流对象被操作过就不能再次使用
foreach和count方法调用后不能再对流进行其他操作
-
对流的操作不会对流中数据源的数据产生改变,相当于是复制了数据到流中
-
Stream收集操作
-
collect:将流中的数据收集到集合中进行使用,传入参数为(Collections.toList / toSet / toMap)
- toSet:可以去重
- toMap:
ArrayList<String> list = new ArrayList<>(); list.add("zhangsan,23"); list.add("lisi,24"); list.add("wangwu,25"); list.add("wangwu2,28"); list.add("wangwu3,59"); Map<String, Integer> map = list.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { String[] cArr = s.split(","); return Integer.parseInt(cArr[1]) >= 24; } }).collect(Collectors.toMap(new Function<String, String>() { @Override public String apply(String s) { String[] cArr = s.split(","); return cArr[0]; } }, new Function<String, Integer>() { @Override public Integer apply(String s) { String[] cArr = s.split(","); return Integer.parseInt(cArr[1]); } })); System.out.println(map);
-
29 File
-
构造方法:创建File对象(与文件/文件夹是否存在无关)
- File("文件或文件夹路径")
- File("父级路径" , "子级路径")
- File(File对象 ,"子级路径")
-
绝对路径和相对路径
-
File成员方法
- File类创建文件方法
- 遍历方法
- listFiles:返回File对象数组,获取当前文件夹下所有的文件和文件夹(不包含子目录)
- listFiles:可以传一个文件过滤器接口的匿名内部类
File[] files = file.listFiles();
for (File f : files) {
if (f.isFile() && f.getName().endsWith(".java")) {
System.out.println(f);
}
}
File[] files = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.getName().endsWith(".java");
}
});
删除文件夹以及文件夹下所有文件
public static void deleteDir(File dir) {
File[] files = dir.listFiles();
for (File file : files) {
if (file.isFile()) {
file.delete();
} else {
if (file.listFiles() != null) {
deleteDir(file);
}
}
}
dir.delete();
}
30 IO
-
作用:用于数据传输
-
分类:
-
按流向分
- 输入流
- 输出流
-
按类型分
-
字节流:万能流,计算机中任何一个文件或数据,底层都是以字节(1Byte=8bit 1KB=1024Byte)的形式进行存储.
注意:字节流虽然可以操作任意类型的文件,但是操作纯文本文件的时候,有可能会出现中文乱码问题
-
字符流:读取纯文本文件,没有中文乱码问题
-
-
关闭流对象释放文件资源
30.1 字节流
-
FileOutputStream(File file [ , boolean append]):传入文件(可以是File对象,也可以是文件路径字符串),创建字节输出流(文件如果不存在,会自动创建;文件存在会清空内容再写入内容),Boolean写true表示开启追加写入。
-
write:传入int或byte[]
字符串调用getBytes获取字符串的字节数组,\r\n :表示换行(单个 \r 或 \n 表示mac或linux系统)
-
-
FileInputStream:只能是文件(文件夹会拒绝访问,抛出异常)
-
read:空参方法,一次读取一个字节,返回读取到的数据
传入byte[] ,一次读取数组长度个字节,返回读取到的字节个数
new String():传入byte数组可以转为字符串,传入(byte数组,转换开始索引,转换长度)可以指定要转换为字符串的指定数组元素
也可以传入byte数组和解码方式
-
FileInputStream fis = new FileInputStream("JavaSE-code\\src\\com\\advance\\day10_io\\info.txt");
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
fis.close();
JDK7及之前流的关闭
try {正常代码逻辑} catch(异常对象){异常处理} finally{关闭流对象}
无论能否捕获异常,finally都会执行
Scanner scanner = new Scanner(System.in);
FileOutputStream fileOutputStream = null;
try {
// int i = 1 / 0;
fileOutputStream = new FileOutputStream("JavaSE-code\\src\\com\\advance\\day10_io\\info.txt", true);
while (true) {
String s = scanner.nextLine();
if ("886".equals(s)) {
break;
} else {
fileOutputStream.write((s + "\r\n").getBytes());
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
JDK8及之后流的关闭
try (要关闭的流对象) {正常代码逻辑} catch(异常对象){异常处理}
要关闭的流需要实现 AutoCloseable接口才能通过编译
try (FileOutputStream fos = new FileOutputStream("JavaSE-code\\src\\com\\advance\\day10_io\\info.txt", true)) {
fos.write("ddd".getBytes());
} catch (IOException e) {
}
30.2 字节缓冲流
- 提高读写效率
- BufferedInputStream( InputStream is):
- BufferedOutputStream:
提高效率的原理:内置了一个长度为8192的字节数组,调用read方法时读取8192个字节的数据进内置数组
常用的是 普通流+自定义数组(代码简单,效率并不比缓冲流低)
- 拷贝方式
普通字节流, 单个字节拷贝 :每次读写一个字节,效率很低
普通字节流 + 自定义数组拷贝:从硬盘读取1024字节到内存数组,从内存数组写1024个字节到硬盘文件
缓冲流单个字节拷贝:一次读取8192个字节到内置数组,每read一次从内置数组中取出一个到输出流的内置数组,然后一次性写出8192个字节到硬盘文件
缓冲流 + 自定义数组拷贝
30.3 字符集
-
字符:一个字符是一个单位的字形、类字形单位或符号的基本信息。说的简单点字符是各种文字和符号的总称。一个字符可以是一个中文汉字、一个英文字母、一个阿拉伯数字、一个标点符号、一个图形符号或者控制符号等。
-
字符集:指多个字符的集合。不同的字符集包含的字符个数不一样、包含的字符不一样、对字符的编码方式也不一样。
-
字符编码:字符编码是指一种映射规则,根据这个映射规则可以将某个字符映射成其他形式的数据以便在计算机中存储和传输。字符到字节的一套对应关系。
Unicode字符集(万国码),UTF-8编码是Unicode的实现方式之一,每个汉字占3个字节,汉字的int类型表示形式是负数
GBK编码表:汉字编码,每个汉字占2个字节
ASCII码表:所有码表都囊括了 ASCII 码表
- 编码和解码
- 乱码原因:编解码方式不一致
30.4 字符流
-
字节流虽然可以操作任意类型的文件,但是读中文时,可能会出现乱码,字符流就可以解决读写过程中出现乱码的问题
-
FileReader、FileWriter
-
读数据方法
- public int read():一次读取一个字符
- public int read(char[] cbuf):一次读取一个字符数组
只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。
文件夹复制
public static void copyDir(File src, File dest) throws IOException {
File newDir = new File(dest, src.getName());
newDir.mkdirs();
// 遍历数据源的文件夹
File[] files = src.listFiles();
for (File file : files) {
if (file.isFile()) {
// 是文件的话, 直接拷贝
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(new File(newDir, file.getName()));
byte[] bys = new byte[8192];
int len;
while ((len = fis.read(bys)) != -1) {
fos.write(bys, 0, len);
}
fis.close();
fos.close();
} else {
// 递归调用
copyDir(file, newDir);
}
}
}
31 功能流
31.1 字符缓冲流
字符流写数据
- FileWriter
- write:可以写单个字符(int类型),字符串,字符串一部分,字符数组,字符数组一部分
字符流只能拷贝纯文本文件
字符输出流FileWriter并不是直接将数据写入文件,内部有一个1024的数组,需要调用flush或这close关闭流才能将数据写入文件
flush将缓冲区数据写入文件,可以继续写入
close关闭流,释放资源,关闭前会将缓冲区数据写入文件
-
字符缓冲流
- BufferedReader:
- readLine():一次读取一整行字符串(不包含“ \r\n ”),没有数据会返回null
- BufferedReader:
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D:\a.txt"),"GBK"));
- BufferedWriter:
- newLine():写出换行符,此方法可以跨平台( \r\n )
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("E:\\a.txt"),"gbk"));
流对象应晚开早关
31.2 转换流
- 可以将字节流转换为字符流进行使用
- 可以按照指定的编码表进行读写操作
分类
- 输入流
- InputStreamReader:按指定编码方式读取字节流,将其转换为字符流
- 输出流
- OutputStreamWriter:
31.3 序列化流
- ObjectOutputStream:
- writeObject:序列化对象(对应的类需要实现Serializable接口)
- ObjectInputStream:
- readObject:反序列化,从文件中读取对象信息
建议序列化只读写一次,将需要序列化的对象放在容器中进行序列化
修改对象对应的JavaBean类,再进行反序列化会抛出InvalidClassException
原因:修改类后serialVersionUID会发生改变,
解决:给类加一个servialVersionUID
transient修饰的成员变量不参与序列化过程
31.4 打印流
平时控制台打印输出,是调用 print 方法和 println 方法完成的,这两个方法都来自于 java.io.PrintStream 类
打印流可以自动刷新,自动换行
-
PrintStream
- print:
- println:可以将数据原样写出,有换行
System.setOut:设置系统的打印流流向
ps.write(97); --- a
ps.println(97) --- 97
- printWriter: 需要在构造方法里面开启自动刷新,并且在写数据调用println方法(print和write都不能自动刷新)
- PrintStream:不需要开启自动刷新
31.5 Properties
java.util.Properties 继承于 Hashtable来表示一个持久的属性集。
它使用键值结构存储数据,每个键及其对应值都是一个字符串。
内部提供了和IO流结合的方法, 能够很方便的从流中加载数据, 也能够方便的将数据写入到文件.
- 作为集合的方法
- 和IO相关的方法
1. 写出
Properties prop = new Properties();
prop.setProperty("username","admin");
prop.setProperty("password","123456");
FileOutputStream fos = new FileInputStream("config.properties");
prop.store(fos, "注释");
fos.close();
2. 读取
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("config.properties");
prop.load(fis);
fis.close();
String username = prop.getProperty("username");
System.out.println(username); // 根据键找到的值为, admin
String password = prop.getProperty("password");
System.out.println(password); // 根据键找到的值为, 123456
32 线程
进程和线程
- 进程的特性:
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。
。进程是操作系统的最小调度单元
。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他的进程的地址空间。 - 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行
- 并行:在同一时刻,有多个指令在多个CPU上【同时】执行
- 并发:在同一时刻,有多个指令在单个CPU上【交替】执行
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。
多进程同时执行的本质:并发
-
线程
- 操作系统可以同时执行多个任务,每个任务就是进程,进程可以同时执行多个任务,每个任务就是线程。
- 线程依赖于进程,线程是进程执行的最小单元。一个进程至少应该存在一个线程。
- 一个线程,在一个时刻,只能运行在一个处理器核心上
- 进程在执行的时候其实是执行的是进程中对应的线程。
- 使用多线程可以提高程序的执行效率
- 操作系统可以同时执行多个任务,每个任务就是进程,进程可以同时执行多个任务,每个任务就是线程。
-
Java程序默认是多线程
- 主线程
- GC线程
public class DemoB {
public static void main(String[] args) {
while (true) {
new DemoBA();
System.out.println("创建对象");
}
}
}
class DemoBA {
//GC执行会默认调用finalize方法
@Override
protected void finalize() throws Throwable {
System.out.println("GC执行");
}
}
32.1 线程创建方式
- 继承Thread类,重写run方法(封装被线程执行的代码),创建线程对象调用start执行线程(调用start方法开启线程,会自动调用run方法)
线程交替执行效果演示:
主线程和自定义线程
任务执行次数少有可能看不到交替执行
主线程任务放在自定义线程开启之前看不到交替执行
调用自定义线程的run方法不会开启线程,只是调用了一次方法
start和run方法
run():封装线程执行的代码,直接调用,相当于普通方法的调用
start():启动线程;然后由VM调用此线程的run方法
不常用,因为Java只支持单继承,有局限性
- 实现Runnable接口,重写run方法,创建线程对象传入线程任务对象(实现接口重写run的类的对象),调用start执行线程
class MyRunnable implements Runnable{
@Override
public void run(){
...
}
}
new Thread(new MyRunnable()).start;
- 实现Callable接口(泛型写返回值类型),重写call方法,将(实现接口重写run方法的类的)对象进一步封装为FutrureTask对象,创建线程对象传入FutrureTask对象,调用start执行线程
public class DemoC {
public static void main(String[] args) throws Exception {
MyCallable myCallable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(myCallable);
Thread thread = new Thread(task);
thread.start();
for (int j = 0; j < 2000; j++) {
System.out.println("main线程执行");
}
Integer i = task.get();
System.out.println(i);
//主线程放这里就不会交替执行
// for (int j = 0; j < 2000; j++) {
// System.out.println("main线程执行");
// }
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 2000; i++) {
System.out.println("MyCallable线程执行");
sum += i;
}
return sum;
}
}
- 相较于前两种创建线程方式,实现Callable接口可以有返回值,通过FutrureTask对象调用get方法接受返回值
- get方法具有阻塞性,只能在等待线程执行完毕获取返回值之后才能执行后续代码
三种实现方式的对比
-
实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
-
继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
线程池创建线程
32.2 线程相关方法
设置和获取线程名称
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为等于参数name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
线程休眠
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
-
线程调度
-
两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
-
Java使用的是抢占式调度模型
-
随机性
- 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
-
-
优先级相关方法
方法名 说明 final int getPriority() 返回此线程的优先级 final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
32.3 线程同步
-
卖票案例
- 出现问题:同一张票多次售出;出现0号票和负号票
- 问题原因:线程执行具有随机性,在卖票过程中丢失cpu的执行权,导致出现问题
-
线程不安全问题产生条件
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
-
线程不安全问题解决
-
同步代码块(synchronized)
synchronized(任意对象) { 多条语句操作共享数据的代码 }
优点:解决了线程不安全问题
缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
-
同步方法:就是把 synchronized 关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
- 非静态方法锁对象:this
- 静态方法锁对象:类名.class
-
Lock:JDK5以后提供了一个新的锁对象Lock,可以更清晰的表达如何加锁和释放锁
-
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁方法
方法名 说明 void lock() 获得锁 void unlock() 释放锁
-
-
32.4 死锁
- 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
- 死锁产生情况
- 资源有限
- 同步嵌套
32.5 线程间通信(等待唤醒机制)
Object类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 随机唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
sleep和wait方法的区别
sleep:线程休眠一定时间后自动唤醒,休眠期间持有锁对象不释放
wait:线程等待需要其他线程唤醒,等待期间释放锁对象
32.5 线程生命周期
- 线程状态
当线程被创建并启动以后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。
Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下
线程状态 | 具体含义 |
---|---|
NEW(新建) | 创建线程对象 |
RUNNABLE(就绪) | start 方法被调用,但是还没有抢到CPU执行权 |
BLOCKED(阻塞) | 线程开始运行,但是没有获取到锁对象 |
WAITING(等待) | wait 方法 |
TIMED_WAITING(计时等待) | sleep 方法 |
TERMINATED(结束状态) | 代码全部运行完毕 |
32.6 线程池
系统创建一个线程的成本是比较高的
因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程
对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。
针对这一种情况,为了提高性能,我们就可以采用线程池。
线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。
等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。
等待下一次任务的执行。
将线程对象交给线程池维护, 可以降低系统成本, 从而提升程序的性能
JDK 对线程池也进行了相关的实现,我们可以使用Executors中所提供的静态方法来创建线程池
方法 | 介绍 |
---|---|
static ExecutorService newCachedThreadPool ( ) |
创建一个默认的线程池 |
static newFixedThreadPool ( int nThreads ) |
创建一个指定最多线程数量的线程池 |
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程
这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;
另一方面线程的细节管理交给线程池处理,优化了资源的开销。
而线程池不允许使用Executors去创建,而要通过 ThreadPoolExecutor 方式
ThreadPoolExecutor
- 构造方法
构造方法 | 描述 |
---|---|
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) |
用给定的初始参数创建一个新的 ThreadPoolExecutor |
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null
临时线程什么时候创建
新任务提交时发现核心线程都在忙 ( 任务队列也满了 ),此时创建新的临时线程
即任务数量大于核心线程数和任务队列任务数的和,创建新的临时线程
什么时候开启拒绝策略
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
即任务数量大于最大线程数和任务队列任务数的和,开启拒绝策略
线程池-非默认任务拒绝策略
RejectedExecutionHandler 是 jdk 提供的一个任务拒绝策略接口,它下面存在4个子类。
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
注:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数
提交线程任务的方式 :
方法名 |
---|
void execute(Runnable command) |
Future |
推荐使用 submit , 因为能够同时接收 Runnable 和 Callable
33 网络编程
概念
-
服务器
-
互联网架构
- Browser / Server
- Client / Server
-
IP地址:(Internet Protocol)全称 【互联网协议地址】,是分配给上网设备的唯一标志。
- IPV4:4字节32bit,点分十进制表示
- IPV6:16字节128bit,冒分十六进制表示(省略前面的0,0位压缩表示法)
- 公网地址、内网地址(192.168开头,局域网组织机构内部使用)
- 127.0.0.1 / localhost:回送地址/本地回环地址,只会寻找当前所在本机
-
端口号
-
端口:应用程序在设备中唯一的标识。
-
端口号:用两个字节表示的整数,它的取值范围是0~65535。
其中0~1023之间的端口号用于一些知名的网络服务或者应用。
我们自己使用1024以上的端口号就可以了。
-
常见的端口号
- 8080 :tomcat 服务器
- 3306 :MySQL 数据库
-
-
注意:注意端口绑定, 不要出现端口冲突
-
-
协议
-
InetAddress
- getByName :确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
- getHostName:获取主机名
- getHostAddress :获取IP
-
UDP协议和TCP协议介绍
-
UDP :
-
用户数据报协议(User Datagram Protocol)
-
UDP是面向无连接通信协议。
速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据
-
-
TCP :
-
传输控制协议TCP(Transmission Control Protocol)
-
TCP协议是面向连接的通信协议。
速度慢,没有大小限制,数据安全。
-
-
33.1 UDP
-
UDP协议收发数据
-
发送数据
- 创建发送端对象 : DatagramSocket ds = new DatagramSocket();
- 创建数据包 : DatagramPacket dp = new DatagramPacket(字节数组, 数据个数, 接收端IP, 接收端端口);
- 发送数据 : ds.send(dp);
- 释放资源 : ds.close();
-
接收数据
- 创建接收端对象 : DatagramSocket ds = new DatagramSocket(端口);
- 创建数据包 : DatagramPacket dp = new DatagramPacket(字节数组, 数组的长度);
- 接收数据 : ds.receive(dp);
- 使用接收的数据 : System.out.println(new String(bys, 0, dp.getLength()));
- 释放资源 : ds.close();
-
33.2 TCP
TCP 协议发数据
-
Java中的 TCP 通信
- Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
- Java为客户端提供了 Socket 类,为服务器端提供了 ServerSocket 类
-
构造方法
方法名 说明 Socket(InetAddress address,int port) 创建流套接字并将其连接到指定IP指定端口号 Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号 -
相关方法
方法名 说明 InputStream getInputStream() 返回此套接字的输入流 OutputStream getOutputStream() 返回此套接字的输出流
TCP协议收数据
-
构造方法
方法名 说明 ServletSocket(int port) 创建绑定到指定端口的服务器套接字 -
相关方法
方法名 说明 Socket accept() 监听要连接到此的套接字并接受它 -
注意事项
- accept方法是阻塞的,作用就是等待客户端连接
- 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
- 针对客户端来讲,是往外写的,所以是输出流
针对服务器来讲,是往里读的,所以是输入流 - read方法也是阻塞的
- 客户端在关流的时候,还多了一个往服务器写结束标记的动作
- 最后一步断开连接,通过四次挥手协议保证连接终止
三次握手和四次挥手
- UDP : 面向无连接, 数据不安全, 速度快
- 有传输大小限制 , 一次最多只能发送64k
- TCP : 面向连接, 数据安全, 速度相对来说较慢
- 没有传输大小限制
三次握手和四次挥手
待补充
TCP案例:文件上传 - 待优化
-
案例需求
客户端:数据来自于本地文件,接收服务器反馈
服务器:接收到的数据写入本地文件,给出反馈
-
案例分析
- 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
- 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
- 客户端接受服务端的回馈信息
-
相关方法
方法名 说明 void shutdownInput() 将此套接字的输入流放置在“流的末尾” void shutdownOutput() 禁止用此套接字的输出流 -
代码实现
package com.itheima.tcp; import java.io.*; import java.net.Socket; public class Client { public static void main(String[] args) throws IOException { // 1. 本地输入流, 关联文件 FileInputStream fis = new FileInputStream("D:\\win.png"); // 2. 创建socket连接服务端 Socket socket = new Socket("127.0.0.1", 10010); // 3. 获取网络输出流对象 OutputStream os = socket.getOutputStream(); // 4. 读写 byte[] bys = new byte[8192]; int len; while ((len = fis.read(bys)) != -1) { os.write(bys, 0, len); } fis.close(); // 5. 给服务端一个结束标记 socket.shutdownOutput(); // 6. 关流 socket.close(); } }
package com.itheima.tcp; import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) throws IOException { // 1. 创建服务端对象, 指定端口号 ServerSocket server = new ServerSocket(10010); // 2. 响应客户端发送的请求, 获取socket对象 Socket socket = server.accept(); // 3. 创建本地输出流, 关流文件 FileOutputStream fos = new FileOutputStream("E:\\result.png"); // 4. 获取网络输入流对象 InputStream is = socket.getInputStream(); // 5. 读写 byte[] bys = new byte[8192]; int len; while((len = is.read(bys)) != -1){ fos.write(bys,0,len); } fos.close(); // 6. 关流 socket.close(); server.close(); } }
TCP案例:文件上传 - 优化
-
优化方案一
-
需求
服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。
-
解决方案
使用循环
-
服务代码改写
public class Server { public static void main(String[] args) throws IOException { // 1. 创建服务端对象, 指定端口号 ServerSocket server = new ServerSocket(10010); while (true) { // 2. 响应客户端发送的请求, 获取socket对象 Socket socket = server.accept(); // 3. 创建本地输出流, 关流文件 FileOutputStream fos = new FileOutputStream("E:\\result.png"); // 4. 获取网络输入流对象 InputStream is = socket.getInputStream(); // 5. 读写 byte[] bys = new byte[8192]; int len; while ((len = is.read(bys)) != -1) { fos.write(bys, 0, len); } fos.close(); } } }
-
-
优化方案二
-
需求
第二次上传文件的时候,会把第一次的文件给覆盖。
-
解决方案
UUID. randomUUID()方法生成随机的文件名
-
代码实现
public class Server { public static void main(String[] args) throws IOException { // 1. 创建服务端对象, 指定端口号 ServerSocket server = new ServerSocket(10010); while (true) { // 2. 响应客户端发送的请求, 获取socket对象 Socket socket = server.accept(); // 3. 创建本地输出流, 关流文件 FileOutputStream fos = new FileOutputStream("E:\\result" + UUID.randomUUID().toString() + ".png"); // 4. 获取网络输入流对象 InputStream is = socket.getInputStream(); // 5. 读写 byte[] bys = new byte[8192]; int len; while ((len = is.read(bys)) != -1) { fos.write(bys, 0, len); } fos.close(); } } }
-
-
优化方案三
-
需求
使用循环虽然可以让服务器处理多个客户端请求。但是还是无法同时跟多个客户端进行通信。
-
解决方案
开启多线程处理
-
代码实现
package com.itheima.tcp; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Server { public static void main(String[] args) throws IOException { // 1. 创建服务端对象, 指定端口号 ServerSocket server = new ServerSocket(10010); // 优化代码: 线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); while (true) { // 2. 响应客户端发送的请求, 获取socket对象 Socket socket = server.accept(); // 3. 提交线程任务 pool.submit(new SubmitFileTask(socket)); } } } class SubmitFileTask implements Runnable { private Socket socket; public SubmitFileTask(Socket socket) { this.socket = socket; } @Override public void run() { try { FileOutputStream fos = new FileOutputStream("E:\\result" + UUID.randomUUID().toString() + ".png"); // 4. 获取网络输入流对象 InputStream is = socket.getInputStream(); // 5. 读写 byte[] bys = new byte[8192]; int len; while ((len = is.read(bys)) != -1) { fos.write(bys, 0, len); } fos.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
34 日志
- 介绍 : 程序中的日志可以用来记录程序运行过程中的信息,并可以进行永久存储。
34.1 日志体系结构
JCL : Jakarta Commons Logging
Jakarta : Apache基金旗下的开源Java项目社区
34.2 Logback 快速入门
-
三个技术模块
模块名 | 介绍 |
---|---|
logback-core: | 该模块为其他两个模块提供基础代码,必须有。 |
logback-classic: | 完整实现了slf4j API的模块。 |
logback-access | logback-access 模块与 Tomcat 和 Jetty 等 Servlet 容器集成,以提供 HTTP 访问日志功能 |
第一步 : 引入 jar 包
jar 包 : 本质来说是压缩包, 内部存储的都是别人已经写好的代码
第二步 : 导入配置文件
第三步 : 获取日志对象使用
日志级别和配置文件详解
通过logback.xml 中的<appender>标签可以设置输出位置和日志信息的详细格式。
通常可以设置2个日志输出位置:一个是控制台、一个是系统文件中
35 枚举
-
格式
public enum 枚举名 { 枚举项1,枚举项2,枚举项3; } 注意: 定义枚举类要用关键字enum
-
示例代码
// 定义一个枚举类,用来表示春,夏,秋,冬这四个固定值 public enum Season { SPRING,SUMMER,AUTUMN,WINTER; }
枚举的特点
-
所有枚举类都是 Enum 的子类
-
我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
-
每一个枚举项其实就是该枚举的一个对象
-
枚举也是类, 可以定义成员变量
-
枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略
-
枚举类可以有构造器,但必须是 private 的,它默认的也是 private 的。
- 枚举项的用法比较特殊:枚举("");
-
枚举类也可以有抽象方法,但是枚举项必须重写该方法
public enum Season {
SPRING("春"){
//如果枚举类中有抽象方法
//那么在枚举项中必须要全部重写
@Override
public void show() {
System.out.println(this.name);
}
},
SUMMER("夏"){
@Override
public void show() {
System.out.println(this.name);
}
},
AUTUMN("秋"){
@Override
public void show() {
System.out.println(this.name);
}
},
WINTER("冬"){
@Override
public void show() {
System.out.println(this.name);
}
};
public String name;
//空参构造
//private Season(){}
//有参构造
private Season(String name){
this.name = name;
}
//抽象方法
public abstract void show();
}
public class EnumDemo {
public static void main(String[] args) {
//我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
System.out.println(Season.AUTUMN);
System.out.println(Season.WINTER);
//每一个枚举项其实就是该枚举的一个对象
Season spring = Season.SPRING;
}
}
- 枚举 : 可以理解是一种多例设计模式
单例设计模式: 保证类的对象, 在内存中只有一份
枚举 : 保证类的对象, 在内存中, 只有固定的几个
36 类加载器
-
作用:负责将.class(硬盘中存储的物理文件)加载到内存中
-
加载时机:用到即加载
- 创建类的实例(对象)
Student stu = new Student();
- 调用类的静态方法
Arrays.toString(arr);
- 访问类或者接口的静态变量,或者为该类静态变量赋值
interface Inter { int NUM = 10; } System.out.println(Inter.NUM); ------------------------------------------------------------------------------ class A { public static int num; } A.num = 20; System.out.println(A.num);
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
Class clazz = Class.forName("com.itheima.domain.Student"); clazz.newInstance();
- 初始化继承体系
class Fu {} class Zi extends Fu {} Zi z = new Zi(); 1). Zi 字节码文件 2). Fu 字节码文件
- 直接使用 java.exe 命令来运行某个主类
java HelloWorld.java
36.1 类加载过程
加载 :
验证 :
准备 :
解析 :
public class A {
public static void main(String[] args) throws IOException {
// 加载B类的类加载器
ClassLoader classLoader = B.class.getClassLoader();
// 观察: 看看C类有没有加载, 如果没加载, 成员变量的C是个啥...
// 为了让程序不停, 保留进程
System.in.read();
}
}
class B {
C c = new C();
}
class C {
}
jps : jps是jdk提供的一个查看当前java进程的小工具
HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据
命令 : java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
public class A {
public static void main(String[] args) throws IOException {
// 加载B类的类加载器
ClassLoader classLoader = B.class.getClassLoader();
// 观察: 看看C类有没有加载, 如果没加载, 成员变量的C是个啥...
new B();
// 为了让程序不停, 保留进程
System.in.read();
}
}
class B {
C c = new C();
}
class C {
}
初始化
小结
- 当一个类被使用的时候,才会加载到内存
- 类加载的过程: 加载、验证、准备、解析、初始化
36.2 类加载器的分类
类的字节码对象.getClassLoader(); 获取该类的, 类加载器对象.
- Bootstrap class loader:虚拟机的内置类加载器,通常表示为null
- C++ 实现, 获取到的只能是 null
- Platform class loader:平台类加载器,负责加载JDK中一些特殊的模块 负责加载 lib\modules 内部的类
- JDK9 之前是 :(Extension Class Loader) 扩展类加载器 : 负责加载 jre\lib\ext 目录下的类
- Application class loader:负责加载自己写的类
- 自定义类加载器 : 上级为 Application
- 目前不做讲解 , 自己设计框架的时候才会用得到
下列代码在 JDK 17 中会出错,因为已经没有 ext 这个包了
package com.itheima.loader;
import sun.net.spi.nameservice.dns.DNSNameService;
public class TestLoad {
public static void main(String[] args) {
ClassLoader classloader1 = String.class.getClassLoader();
System.out.println(classloader1);
ClassLoader classloader2 = DNSNameService.class.getClassLoader();
System.out.println(classloader2);
ClassLoader classloader3 = Student.class.getClassLoader();
System.out.println(classloader3);
}
}
class Student {
}
36.3 双亲委派机制
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
好处 :
- 双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载
- 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
- 保证了 Java 的核心 API 不被篡改
假设我们自己编写了一个 Object 类
1. java.lang.Object类 (真的)
2. java.lang.Object类 (假的)
双亲委派模型可以保证加载的是 JRE 里的那个 Object 类, 而不是我们自己写的
AppClassLoader ---> PlatformClassLoader ---> BootstrapClassLoader
BootstrapClassLoader : 发现已经加载过了Object类了, 就不会加载我们自己写的了
常用方法
方法名 | 作用 |
---|---|
public static ClassLoader getSystemClassLoader() | 获取系统类加载器 |
public InputStream getResourceAsStream(String name) | 加载某一个资源文件 |
public class TestStream {
public static void main(String[] args) throws IOException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
InputStream is = classLoader.getResourceAsStream("prop.properties");
Properties prop = new Properties();
prop.load(is);
System.out.println(prop);
is.close();
}
}
37 反射
- 反射
是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意属性和方法;
这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。大白话的理解 : 反射其实就是对类的一种解刨
37.1 获取Class类对象的三种方式
- 类名.class属性
- 对象名.getClass()方法
- Class.forName(全类名)方法
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
//1.Class类中的静态方法forName("全类名")
//全类名:包名 + 类名
Class clazz = Class.forName("com.itheima.myreflect2.Student");
System.out.println(clazz);
//2.通过class属性来获取
Class clazz2 = Student.class;
System.out.println(clazz2);
//3.利用对象的getClass方法来获取class对象
//getClass方法是定义在Object类中.
Student s = new Student();
Class clazz3 = s.getClass();
System.out.println(clazz3);
System.out.println(clazz == clazz2);
System.out.println(clazz2 == clazz3);
}
}
37.2 反射获取构造方法
- Class类获取构造方法对象的方法
-
方法介绍
方法名 说明 Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组 Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组 Constructor getConstructor(Class<?>... parameterTypes) 返回单个公共构造方法对象 Constructor getDeclaredConstructor(Class<?>... parameterTypes) 返回单个构造方法对象 -
示例代码
public class Student { private String name; private int age; //私有的有参构造方法 private Student(String name) { System.out.println("name的值为:" + name); System.out.println("private...Student...有参构造方法"); } //公共的无参构造方法 public Student() { System.out.println("public...Student...无参构造方法"); } //公共的有参构造方法 public Student(String name, int age) { System.out.println("name的值为:" + name + "age的值为:" + age); System.out.println("public...Student...有参构造方法"); } } public class ReflectDemo1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException { //method1(); //method2(); //method3(); //method4(); } private static void method4() throws ClassNotFoundException, NoSuchMethodException { // Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes): // 返回单个构造方法对象 //1.获取Class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); Constructor constructor = clazz.getDeclaredConstructor(String.class); System.out.println(constructor); } private static void method3() throws ClassNotFoundException, NoSuchMethodException { // Constructor<T> getConstructor(Class<?>... parameterTypes): // 返回单个公共构造方法对象 //1.获取Class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); //小括号中,一定要跟构造方法的形参保持一致. Constructor constructor1 = clazz.getConstructor(); System.out.println(constructor1); Constructor constructor2 = clazz.getConstructor(String.class, int.class); System.out.println(constructor2); //因为Student类中,没有只有一个int的构造,所以这里会报错. Constructor constructor3 = clazz.getConstructor(int.class); System.out.println(constructor3); } private static void method2() throws ClassNotFoundException { // Constructor<?>[] getDeclaredConstructors(): // 返回所有构造方法对象的数组 //1.获取Class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); Constructor[] constructors = clazz.getDeclaredConstructors(); for (Constructor constructor : constructors) { System.out.println(constructor); } } private static void method1() throws ClassNotFoundException { // Constructor<?>[] getConstructors(): // 返回所有公共构造方法对象的数组 //1.获取Class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); Constructor[] constructors = clazz.getConstructors(); for (Constructor constructor : constructors) { System.out.println(constructor); } } }
Constructor类用于创建对象的方法
-
方法介绍
方法名 说明 T newInstance(Object...initargs) 根据指定的构造方法创建对象 setAccessible(boolean flag) 设置为true,表示取消访问检查 -
示例代码
// Student类同上一个示例,这里就不在重复提供了 public class ReflectDemo2 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //T newInstance(Object... initargs):根据指定的构造方法创建对象 //method1(); //method2(); //method3(); //method4(); } private static void method4() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { //获取一个私有的构造方法并创建对象 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); //2.获取一个私有化的构造方法. Constructor constructor = clazz.getDeclaredConstructor(String.class); //被private修饰的成员,不能直接使用的 //如果用反射强行获取并使用,需要临时取消访问检查 constructor.setAccessible(true); //3.直接创建对象 Student student = (Student) constructor.newInstance("zhangsan"); System.out.println(student); } private static void method3() throws ClassNotFoundException, InstantiationException, IllegalAccessException { //简写格式 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); //2.在Class类中,有一个newInstance方法,可以利用空参直接创建一个对象 Student student = (Student) clazz.newInstance();//这个方法现在已经过时了,了解一下 System.out.println(student); } private static void method2() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); //2.获取构造方法对象 Constructor constructor = clazz.getConstructor(); //3.利用空参来创建Student的对象 Student student = (Student) constructor.newInstance(); System.out.println(student); } private static void method1() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect3.Student"); //2.获取构造方法对象 Constructor constructor = clazz.getConstructor(String.class, int.class); //3.利用newInstance创建Student的对象 Student student = (Student) constructor.newInstance("zhangsan", 23); System.out.println(student); } }
37.3 反射获取成员属性
- Class类获取成员变量对象的方法
-
方法分类
方法名 说明 Field[] getFields() 返回所有公共成员变量对象的数组 Field[] getDeclaredFields() 返回所有成员变量对象的数组 Field getField(String name) 返回单个公共成员变量对象 Field getDeclaredField(String name) 返回单个成员变量对象 -
示例代码
public class Student { public String name; public int age; public String gender; private int money = 300; @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + ", money=" + money + '}'; } } public class ReflectDemo1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException { // method1(); //method2(); //method3(); //method4(); } private static void method4() throws ClassNotFoundException, NoSuchFieldException { // Field getDeclaredField(String name):返回单个成员变量对象 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取money成员变量 Field field = clazz.getDeclaredField("money"); //3.打印一下 System.out.println(field); } private static void method3() throws ClassNotFoundException, NoSuchFieldException { // Field getField(String name):返回单个公共成员变量对象 //想要获取的成员变量必须是真实存在的 //且必须是public修饰的. //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取name这个成员变量 //Field field = clazz.getField("name"); //Field field = clazz.getField("name1"); Field field = clazz.getField("money"); //3.打印一下 System.out.println(field); } private static void method2() throws ClassNotFoundException { // Field[] getDeclaredFields():返回所有成员变量对象的数组 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取所有的Field对象 Field[] fields = clazz.getDeclaredFields(); //3.遍历 for (Field field : fields) { System.out.println(field); } } private static void method1() throws ClassNotFoundException { // Field[] getFields():返回所有公共成员变量对象的数组 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取Field对象. Field[] fields = clazz.getFields(); //3.遍历 for (Field field : fields) { System.out.println(field); } } }
Field类用于给成员变量赋值的方法
-
方法介绍
方法名 说明 void set(Object obj, Object value) 赋值 Object get(Object obj) 获取值 -
示例代码
// Student类同上一个示例,这里就不在重复提供了 public class ReflectDemo2 { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException { // Object get(Object obj) 返回由该 Field表示的字段在指定对象上的值。 //method1(); //method2(); } private static void method2() throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException { //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取成员变量Field的对象 Field field = clazz.getDeclaredField("money"); //3.取消一下访问检查 field.setAccessible(true); //4.调用get方法来获取值 //4.1创建一个对象 Student student = (Student) clazz.newInstance(); //4.2获取指定对象的money的值 Object o = field.get(student); //5.打印一下 System.out.println(o); } private static void method1() throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException { // void set(Object obj, Object value):给obj对象的成员变量赋值为value //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect4.Student"); //2.获取name这个Field对象 Field field = clazz.getField("name"); //3.利用set方法进行赋值. //3.1先创建一个Student对象 Student student = (Student) clazz.newInstance(); //3.2有了对象才可以给指定对象进行赋值 field.set(student,"zhangsan"); System.out.println(student); } }
37.4 反射获取成员方法
- Class类获取成员方法对象的方法
-
方法分类
方法名 说明 Method[] getMethods() 返回所有公共成员方法对象的数组,包括继承的 Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括继承的 Method getMethod(String name, Class<?>... parameterTypes) 返回单个公共成员方法对象 Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回单个成员方法对象 -
示例代码
public class Student { //私有的,无参无返回值 private void show() { System.out.println("私有的show方法,无参无返回值"); } //公共的,无参无返回值 public void function1() { System.out.println("function1方法,无参无返回值"); } //公共的,有参无返回值 public void function2(String name) { System.out.println("function2方法,有参无返回值,参数为" + name); } //公共的,无参有返回值 public String function3() { System.out.println("function3方法,无参有返回值"); return "aaa"; } //公共的,有参有返回值 public String function4(String name) { System.out.println("function4方法,有参有返回值,参数为" + name); return "aaa"; } } public class ReflectDemo1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException { //method1(); //method2(); //method3(); //method4(); //method5(); } private static void method5() throws ClassNotFoundException, NoSuchMethodException { // Method getDeclaredMethod(String name, Class<?>... parameterTypes): // 返回单个成员方法对象 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取一个成员方法show Method method = clazz.getDeclaredMethod("show"); //3.打印一下 System.out.println(method); } private static void method4() throws ClassNotFoundException, NoSuchMethodException { //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取一个有形参的方法function2 Method method = clazz.getMethod("function2", String.class); //3.打印一下 System.out.println(method); } private static void method3() throws ClassNotFoundException, NoSuchMethodException { // Method getMethod(String name, Class<?>... parameterTypes) : // 返回单个公共成员方法对象 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取成员方法function1 Method method1 = clazz.getMethod("function1"); //3.打印一下 System.out.println(method1); } private static void method2() throws ClassNotFoundException { // Method[] getDeclaredMethods(): // 返回所有成员方法对象的数组,不包括继承的 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取Method对象 Method[] methods = clazz.getDeclaredMethods(); //3.遍历一下数组 for (Method method : methods) { System.out.println(method); } } private static void method1() throws ClassNotFoundException { // Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的 //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取成员方法对象 Method[] methods = clazz.getMethods(); //3.遍历 for (Method method : methods) { System.out.println(method); } } }
- Method类用于执行方法的方法
-
方法介绍
方法名 说明 Object invoke(Object obj, Object... args) 运行方法 参数一: 用obj对象调用该方法
参数二: 调用方法的传递的参数(如果没有就不写)
返回值: 方法的返回值(如果没有就不写)
-
示例代码
public class ReflectDemo2 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { // Object invoke(Object obj, Object... args):运行方法 // 参数一:用obj对象调用该方法 // 参数二:调用方法的传递的参数(如果没有就不写) // 返回值:方法的返回值(如果没有就不写) //1.获取class对象 Class clazz = Class.forName("com.itheima.myreflect5.Student"); //2.获取里面的Method对象 function4 Method method = clazz.getMethod("function4", String.class); //3.运行function4方法就可以了 //3.1创建一个Student对象,当做方法的调用者 Student student = (Student) clazz.newInstance(); //3.2运行方法 Object result = method.invoke(student, "zhangsan"); //4.打印一下返回值 System.out.println(result); } }
案例 : Integer集合添加String元素
- 需求 : 请向一个泛型为 Integer 的集合, 添加一个 String 字符串
- 思路 : Java 中的泛型是假的, 只在编译的时候有效
package com.itheima.test;
import java.lang.reflect.Method;
import java.util.ArrayList;
public class ReflectTest4 {
/*
需求: 请向一个泛型为 Integer 的集合, 添加一个 String 字符串
普及: Java中的泛型是假的, 只在编译期间有效, 运行的时候, 就没有泛型了.
运行的时候: 肯定过了编译阶段, 肯定有字节码对象.
结论 : Java中的泛型, 到运行期间, 就会被擦除掉.
*/
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<>();
Class listClass = list.getClass();
// 集合中, add方法的, 成员方法对象
Method addMethod = listClass.getMethod("add", Object.class);
// 调用成员方法
addMethod.invoke(list, "abc");
System.out.println(list);
}
}
反射的使用场景 :
-
用于制作框架
-
问题 : 框架是什么 ?
- 简单理解 : 半成品软件,它需要我,我也需要它
38 方法引用
介绍 :
- 方法引用是 JDK8 开始出现的, 主要的作用, 是对Lambda表达式进行进一步的简化
格式 :
方法引用使用一对冒号 ::
方法引用通过方法的名字来指向一个方法。
方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 引用前 :
public class A {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"a","b","c","d");
list.forEach(s -> System.out.println(s));
}
}
- 引用后 :
public class A {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d");
list.forEach(System.out::println);
}
}
- 引用静态方法 :
public class A {
/*
所谓方法引用, 其实就是方法的调用
之前: A.method();
现在: A::method();
就是一个格式的转变而已
------------------------------------------
参数呢?
根据可推导, 可省略原则, 参数只有一个 s
method方法, 也恰好只要一个 s
那方法调用起来, 这一个参数, 只可能进入 method 方法中.
*/
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d");
list.forEach(s -> A.method(s));
list.forEach(A::method);
}
public static void method(String s) {
System.out.println(s.toUpperCase());
}
}
- 引用普通成员方法 :
1. 创建对象
2. 对象名::方法名
public class A {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d");
list.forEach(s -> System.out.println(s));
A a = new A();
list.forEach(a::method);
}
public void method(String s) {
System.out.println(s.toUpperCase());
}
}
39 单元测试
对软件中的最小可测试单元进行检查和验证
- Junit
junit测试类必须是public,无参无返回值
Junit 的优点 :
- JUnit 可以灵活的选择执行哪些测试方法,可以一键执行全部测试方法。
- 单元测试中的某个方法测试失败了,不影响其他方法的测试。
- 运行成功是绿色,运行失败是红色
注解:
@Test
@Before
@After
40 XML
配置文件
配置文件好处 :
- 可以让项目中使用的数据, 灵活的加载和多变, 实现解耦
分类 :
- Properties : 常用于一对一的存储
- 键值对
- username=root
- password=123456
- 键值对
- xml : 常用于一对多的存储
XML:可扩展标记语言 (Extensible Markup Language, XML) ,标准通用标记语言的子集,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。
- 标记语言:通过标签来描述数据的一门语言(标签有时我们也将其称之为元素)
- 可扩展:标签的名字是可以自己定义的
优势 :
- 作为软件的配置文件
- 用于进行存储数据和传输数据
40.1 XML 语法
-
创建
- 就是创建一个XML类型的文件,要求文件的后缀必须使用xml,如hello_world.xml
-
文档声明
文档声明必须是第一行第一列
- version:该属性是必须存在的
- encoding:该属性不是必须的
- 打开当前xml文件的时候应该是使用什么字符编码表(一般取值都是UTF-8)
- standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no,默认yes不依赖
-
标签规则
- 必须存在一个根标签,有且只能有一个
-
标签由一对尖括号和合法标识符组成
<student>
-
标签必须成对出现
<student> </student> 前边的是开始标签,后边的是结束标签
-
特殊的标签可以不成对, 但是必须有结束标记
<address/>
-
标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来
<student id="1"> </student>
-
标签需要正确的嵌套
这是正确的: <student id="1"> <name>张三</name> </student> 这是错误的: <student id="1"> <name>张三 </student> </name>
- 细节
40.2 XML 约束
介绍 😗***
- 用来限定xml文件中可使用的标签以及属性
分类 :
- DTD 约束:简单的约束
- schema 约束:复杂的约束
DTD 约束
编写DTD约束
-
步骤
-
创建一个文件,这个文件的后缀名为.dtd
-
看xml文件中使用了哪些元素
可以定义元素 -
判断元素是简单元素还是复杂元素
简单元素:没有子元素。
复杂元素:有子元素的元素;
-
-
代码实现
<!ELEMENT persons (person)> <!ELEMENT person (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)>
-
引入DTD约束
-
引入本地dtd
<!DOCTYPE 根元素名称 SYSTEM 'DTD文件的路径'>
-
在xml文件内部引入
<!DOCTYPE 根元素名称 [ dtd文件内容 ]>
-
引入网络dtd
<!DOCTYPE 根元素的名称 PUBLIC "DTD文件名称" "DTD文档的URL">
-
引入本地DTD约束
// 这是persondtd.dtd文件中的内容,已经提前写好 <!ELEMENT persons (person)> <!ELEMENT person (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> // 在person1.xml文件中引入persondtd.dtd约束 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE persons SYSTEM 'persondtd.dtd'> <persons> <person> <name>张三</name> <age>23</age> </person> </persons>
-
在xml文件内部引入
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE persons [ <!ELEMENT persons (person)> <!ELEMENT person (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> ]> <persons> <person> <name>张三</name> <age>23</age> </person> </persons>
-
引入网络dtd
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE persons PUBLIC "dtd文件的名称" "dtd文档的URL"> <persons> <person> <name>张三</name> <age>23</age> </person> </persons>
-
Schema 约束
-
schema 和 dtd 的区别
- schema约束文件也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd
- 一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名)
- dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型
- schema 语法更加的复杂
- 编写schema约束
1. 创建一个文件,这个文件的后缀名为.xsd
2. 定义文档声明
3. schema文件的根标签为: <schema>
4. 在<schema>中定义属性:
xmlns=http://www.w3.org/2001/XMLSchema
5. 在<schema>中定义属性:
targetNamespace = 唯一的url地址,指定当前这个schema文件的名称空间
理解: 起个名, 顺带打广告
6. 在<schema>中定义属性:
elementFormDefault="qualified“,表示当前schema文件是一个质量良好的文件
7. 通过element定义元素
8. 判断当前元素是简单元素还是复杂元素
示例 :
引入 :
<stus xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.mhys.com"
xsi:schemaLocation="http://www.mhys.com stus.xsd"
>
<student>
<name>张三</name>
<age>23</age>
</student>
</stus>
40.3 XML解析
- 解析的两种方式
- SAX 解析
- DOM 解析
准备动作
- 导入 dom4j-1.6.1.jar 包
- 获取 Document 对象
SAXReader reader = new SAXReader();
Document document = reader.read(new File("day15-code\\src\\xml\\stus.xml"));
System.out.println(document);
方法介绍
示例代码
package com.itheima.start;
import com.itheima.domain.Student;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) throws DocumentException, FileNotFoundException {
SAXReader reader = new SAXReader();
Document document = reader.read(new File("mC\\src\\xml\\stus.xml"));
ArrayList<Student> list = new ArrayList<>();
// 1. 获取根标签
Element rootElement = document.getRootElement();
// 2. 获取根标签下的所有子标签
List<Element> student = rootElement.elements("student");
// 3. 遍历集合
for (Element element : student) {
// 4. 获取标签属性
String id = element.attributeValue("id");
// 5. 获取name标签的值
String name = element.element("name").getText();
// 6. 获取age标签的值
String age = element.element("age").getText();
// 7. 封装为Student学生对象
Student stu = new Student(name, Integer.parseInt(age));
// 8. 添加到集合
list.add(stu);
}
list.forEach(s -> System.out.println(s));
}
}
41 注解
介绍 :
Annotation表示注解,是JDK1.5的新特性。
注解的主要作用:对我们的程序进行标注。
理解:注释是给人看的,注解是给虚拟机看的
通过注解可以给类增加额外的信息。
注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能。
JDK中常见的注解 :
-
@Override:表示方法的重写
-
@Deprecated:表示修饰的方法已过时
-
@SuppressWarnings("all"):压制警告
除此之外,还需要掌握第三方框架中提供的注解:
比如:Junit
@Test 表示运行测试方法
@Before 表示在Test之前运行,进行初始化
@After 表示在Test之后运行,进行收尾
自定义注解 ( 了解 )
-
自定义注解单独存在没有意义的,一般会跟反射结合起来使用。
-
格式 :
public @interface 注解名称 {
public 属性类型 属性名 () default 默认值 ;
}
public @interface Anno {
String show() default "show..." ;
}
属性类型 :
- 基本数据类型
- String
- Class
- 注解
- 枚举
- 以上类型的一维数组
public @interface MyAnno {
public static final int num1 = 100;
public static final String num2 = "abc";
public static final MyAnno num3 = null;
public static final Class num4 = String.class;
public static final int[] num5 = {};
public abstract String show1() default "show1";
public abstract int show2() default 132;
public abstract MyAnno2 show3() default @MyAnno2;
public abstract Class show4() default String.class;
public abstract int[] show5() default {1, 2, 3};
}
public @interface MyAnno {
int num1 = 100;
String num2 = "abc";
MyAnno num3 = null;
Class num4 = String.class;
int[] num5 = {};
String show1() default "123";
int show2() default 132;
MyAnno2 show3() default @MyAnno2;
Class show4() default String.class;
int[] show5() default {1, 2, 3};
}
注解的使用 : (掌握)
- 类
- 方法
成员变量 : 很少这么做
在使用注解时, 如果注解的属性没有给出默认值, 需要手动给出
@Anno(name="张三")
- 如果数组中只有一个属性值 , 在使用时{}是可以省略的
特殊的属性 (掌握)
定义注解中如果有多个属性没有赋值 , 使用时需要全部赋值
定义注解中如果只有一个属性名字为value没有赋值 , 使用时直接给出值 , 不需要写属性名
@Anno("给value赋值");
案例
-
需求
自定义一个注解@Test,用于指定类的方法上,如果某一个类的方法上使用了该注解,就执行该方法
-
实现步骤
- 自定义一个注解Test,并在类中的某几个方法上加上注解
- 在测试类中,获取注解所在的类的Class对象
- 获取类中所有的方法对象
- 遍历每一个方法对象,判断是否有对应的注解
//表示Test这个注解的存活时间
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Test {
}
public class UseTest {
//没有使用Test注解
public void show(){
System.out.println("UseTest....show....");
}
//使用Test注解
@Test
public void method(){
System.out.println("UseTest....method....");
}
//使用Test注解
@Test
public void function(){
System.out.println("UseTest....function....");
}
}
public class AnnoDemo {
public static void main(String[] args) throws tException {
// 1.通过反射获取UseTest类的字节码文件对象
Class clazz = Class.forName("com.itheima.myanno3.UseTest");
// 创建对象
UseTest useTest = (UseTest) clazz.newInstance();
// 2.通过反射获取这个类里面所有的方法对象
Method[] methods = clazz.getMethods();
// 3.遍历数组,得到每一个方法对象
for (Method method : methods) {
// method依次表示每一个方法对象。
// isAnnotationPresent(Class<? extends Annotation> annotationClass)
// 判断当前方法上是否有指定的注解。
// 参数:注解的字节码文件对象
// 返回值:布尔结果。 true 存在 false 不存在
if(method.isAnnotationPresent(Test.class)){
method.invoke(useTest);
}
}
}
}
元注解 (了解)
-
概述
元注解就是描述注解的注解
-
元注解介绍
元注解名 说明 @Target 指定了注解能在哪里使用 @Retention 可以理解为保留时间(生命周期)
Target :
作用:用来标识注解使用的位置,如果没有使用该注解标识,则自定义的注解可以使用在任意位置。
可使用的值定义在ElementType枚举类中,常用值如下
- TYPE,类,接口
- FIELD, 成员变量
- METHOD, 成员方法
- PARAMETER, 方法参数
- CONSTRUCTOR, 构造方法
- LOCAL_VARIABLE, 局部变量
Retention:
作用:用来标识注解的生命周期(有效范围)
可使用的值定义在RetentionPolicy枚举类中,常用值如下
- SOURCE:注解只作用在源码阶段,生成的字节码文件中不存在
- CLASS:注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值
- RUNTIME:注解作用在源码阶段,字节码文件阶段,运行阶段
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本