👉 在线笔记:https://du1in9.github.io/javase.github.io/
Day 5 递归 & 异常
1. 递归的介绍和使用
递归:方法直接或者间接调用本身
注意事项:递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出现象
public static void main(String[] args) {methodA();}
public static void methodA() {methodB();}
public static void methodB() {methodC();}
public static void methodC() {methodA();}
-
有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问第二十个月的兔子对数为多少?
规律:从第三个月开始,兔子的对数是前两个月相加的和
public static void main(String[] args) {
System.out.println(get(20));
}
public static int get(int month) {
if (month == 1 || month == 2) {
return 1;
} else {
return get(month - 2) + get(month - 1);
}
}
-
猴子第一天摘下若干个桃子当即吃了一半,还不过瘾,又多吃了一个第二天早上又将剩下的桃子吃掉一半,又多吃了一个以后每天早上都吃了前一天剩的一半零一个到第10天早上想再吃时,见只剩下一个桃子了求第一天共摘了多少桃子?
规律:前一天桃子数量 = (后一天桃子数量 + 1) * 2
public static void main(String[] args) {
System.out.println(get(1));
}
public static int get(int day) {
if (day == 10) {
return 1;
} else {
return (get(day + 1) + 1) * 2;
}
}
2. 异常介绍
异常:代码在编译或者执行的过程中可能出现的错误。
// 阅读异常信息 : 从下往上看 1. 找异常错误位置, 2. 异常名称, 3. 异常原因
int[] arr = new int[Integer.MAX_VALUE];
// OutOfMemoryError: Requested array size exceeds VM limit
int[] arr = {11, 22, 33};
System.out.println(arr[10]);
// ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 3
System.out.println(10 / 0);
// ArithmeticException: / by zero
public static void main(String[] args) throws ParseException, FileNotFoundException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
Date date = sdf.parse("2008年8月8日");
FileReader fr = new FileReader("D:\\a.txt");
}
3. 异常的两种处理方式
-
默认处理方式
-
虚拟机会在出现异常的代码那里自动的创建一个异常对象:ArithmeticException
-
异常会从方法中出现的点这里抛出给调用者,调用者最终抛出给JVM
-
虚拟机虚拟机接收到异常对象后,先在控制台直接输出异常信息数据
-
终止 Java 程序的运行,后续代码没有机会执行了,因为程序已经噶了
-
-
异常处理方式 try...catch...
能够将抛出的异常对象捕获,然后执行异常的处理方案
好处:程序可以继续往下执行
快捷键:ctrl + alt + t
try {
int[] arr = null;
System.out.println(arr[10]);
System.out.println(10 / 0);
} catch (ArithmeticException e) { // ArithmeticException e = new ArithmeticException();
System.out.println("捕获了运算异常");
} catch (NullPointerException e) { // NullPointerException e = new NullPointerException();
System.out.println("捕获了空指针异常");
} catch (Exception e) {
System.out.println("捕获了异常");
}
while (true) {
try {
age = Integer.parseInt(sc.nextLine());
break;
} catch (NumberFormatException e) {
System.out.println("年龄输入有误, 请重新输入整数年龄: ");
}
}
-
异常处理方式 throws 抛出
throws:用在方法上,作用是声明,声明这个方法中有可能会出现异常
快捷键:alt + enter
catch (Exception e) { // 无法精准捕获, 太笼统
System.out.println(e.getMessage());
}
// Student.java:
public void setAge(int age) {
if(age >= 0 && age <= 120){
this.age = age;
} else {
throw new Exception("年龄范围有误");
}
}
4. 自定义异常
自定义异常的必要?
- Java无法为这个世界上全部的问题提供异常类。
- 如果企业想通过异常的方式来管理自己的某个业务问题,就需要自定义异常类了。
自定义异常的分类
-
自定义编译时异常:定义一个异常类继承 Exception,重写构造器
-
自定义运行时异常:定义一个异常类继承 RuntimeException,重写构造器
快捷键:右键 → generate → constructor
public class StudentAgeException extends RuntimeException {
public StudentAgeException() {}
public StudentAgeException(String message) {
super(message);
}
}
catch (StudentAgeException e) { // 精准捕获
System.out.println(e.getMessage());
}
// Student.java:
public Student(String name, int age) {
this.name = name;
setAge(age);
}
public void setAge(int age) {
if(age >= 0 && age <= 120){
this.age = age;
} else {
throw new StudentAgeException("年龄范围有误, 需要0~120之间的年龄");
}
}
Throwable的常用方法
方法名 | 说明 |
---|---|
public String getMessage() | 获取异常的错误原因 |
public void printStackTrace() | 展示完整的异常错误信息 |
try {
System.out.println(10 / 0);
} catch (ArithmeticException e) {
System.out.println(e.getMessage()); // / by zero
e.printStackTrace(); // java.lang.ArithmeticException: / by zero
}
细节:子类重写父类方法时,不能 throws 抛出父类没有或者比父类更大的异常 (所以可以用 try...catch...)
Day 6 集合
第一部分
1.1 集合体系结构介绍
1.2 Collection 的使用
/*
Collection的常用方法 :
public boolean add(E e) : 把给定的对象添加到当前集合中
public void clear() : 清空集合中所有的元素
public boolean isEmpty() : 判断当前集合是否为空
public boolean remove(E e) : 把给定的对象在当前集合中删除
public boolean contains(Object obj) : 判断当前集合中是否包含给定的对象
public int size() : 返回集合中元素的个数(集合的长度)
*/
Collection<Student> c = new ArrayList<>();
c.add(new Student("张三", 23));
c.add(new Student("李四", 24));
c.add(new Student("王五", 25));
System.out.println(c.remove(new Student("张三", 23))); // true
System.out.println(c.contains(new Student("李四", 24))); // true
public class Student {
private String name;
private int age;
// remove()和contains()底层依赖对象的equals方法
public boolean equals(Object o) {...}
}
1.3 集合的通用遍历方式
迭代器源码分析
/*
public Iterator<E> iterator() : 获取遍历集合的迭代器
public E next() : 从集合中获取一个元素
public boolean hasNext() : 如果仍有元素可以迭代,则返回 true
注意: 如果next()方法调用次数过多, 会出现NoSuchElementException
*/
Collection<Student> c = new ArrayList<>();
c.add(new Student("张三", 23));
c.add(new Student("李四", 24));
c.add(new Student("王五", 25));
// 1. 使用迭代器遍历集合
Iterator<Student> it = c.iterator();
while (it.hasNext()) {
Student stu = it.next();
System.out.println(stu.getName() + "---" + stu.getAge());
}
// 2. 使用增强for循环遍历集合
for (Student stu : c) {
System.out.println(stu);
}
// 3. 使用foreach方法遍历集合
c.forEach(stu -> System.out.println(stu));
1.4 List 集合
- List 因为支持索引,所以多了很多索引操作的独特api
方法名称 | 说明 |
---|---|
void add(int index,E element) | 在此集合中的指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("张三");
list.set(0, "赵六"); // [赵六, 李四, 张三]
list.remove(1); // [赵六, 张三]
System.out.println(list.get(0)); // 赵六
List<Integer> list2 = new ArrayList<>();
list2.add(111); // Integer e = 111;
list2.add(222); // Integer e = 222;
list2.remove(Integer.valueOf(222));
- List集合特有的遍历方式
// 1. 普通for循环
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
// 2. ListIterator (特有的迭代器)
ListIterator<String> it = list.listIterator();
while(it.hasPrevious()){
String s = it.previous();
System.out.println(s);
}
- 并发修改异常 : ConcurrentModificationException
/*
场景: 使用[迭代器]遍历集合的过程中, 调用了[集合对象]的添加, 删除方法, 就会出现此异常
解决方案: 迭代器的遍历过程中, 不允许使用集合对象的添加或删除, 那就使用迭代器, 自己的添加或删除方法
删除 : 使用 Iterator 自带的 remove 方法
添加 : 使用 ListIterator 自带的 add 方法
*/
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
String s = it.next();
if ("温油".equals(s)) {
it.add("哈哈");
}
}
特例: 使用集合的删除方法删除倒数第二个元素, 就不会出现错误
1.5 栈、队列、链表
- 栈、队列
- 数组
- 链表
1.6 ArrayList 类
ArrayList 底层是基于数组实现的,根据查询元素快,增删相对慢
-
如果只是创建了集合容器,没有进行过添加,底层数组默认长度为 0。
-
ArrayList 底层是数组结构的,数组默认长度为10。
- 当数组添加满了之后,会自动扩容为1.5倍。
1.7 LinkedList 类
LinkedList 底层基于双链表实现的,查询元素慢,增删首尾元素是非常快的
/*
LinkedList 特有方法 :
public void addFirst(E e) : 头部添加
public void addLast(E e) : 尾部添加
public E getFirst() : 获取第一个
public E getLast() : 获取最后一个
public E removeFirst() : 删除第一个
public E removeLast() : 删除最后一个
*/
LinkedList<String> list = new LinkedList<>();
list.add("张三");
list.add("李四");
list.add("王五"); // [张三, 李四, 王五]
// LinkedList 也有 get 方法,表面看起来是根据索引获取元素,
// 实际上是选择从头部正序(离头近)查找,或从尾部倒序(离尾近)查找。
String s = list.get(1); // 李四
System.out.println(list.getFirst()); // 张三
System.out.println(list.getLast()); // 王五
list.removeFirst(); // [李四, 王五]
list.removeLast(); // [李四]
第二部分
2.1 泛型
泛型介绍:JDK5引入的, 可以在编译阶段约束操作的数据类型, 并进行检查
泛型的好处:统一数据类型;将运行期的错误提升到了编译期
注意事项:泛型中只能编写引用数据类型
- 泛型类
/*
常见的泛型标识符 : E V K T
E : Element
T : Type
K : Key(键)
V : Value(值)
清楚不同的泛型, 在什么时机能确定到具体的类型
泛型类 : 创建对象的时候
*/
Student<Integer> stu = new Student<>();
class Student<E> {
private E e;
public E getE() {return e;}
public void setE(E e) {this.e = e;}
}
- 泛型方法
/*
1. 非静态的方法 : 内部的泛型, 会根据类的泛型去匹配
2. 静态的方法 : 静态方法中如果加入了泛型, 必须声明出自己独立的泛型
- 时机: 在调用方法, 传入实际参数的时候, 确定到具体的类型
*/
String[] arr1 = {"张三", "李四", "王五"};
Double[] arr2 = {11.1, 22.2, 33.3};
printArray(arr1);
printArray(arr2);
public static <T> void printArray(T[] arr) {
System.out.print("[");
for (int i = 0; i < arr.length - 1; i++) {
System.out.print(arr[i] + ", ");
}
System.out.println(arr[arr.length - 1] + "]");
}
- 泛型接口
/*
泛型接口
1. 实现类, 实现接口的时候确定到具体的类型
2. 实现类实现接口, 没有指定具体类型, 就让接口的泛型, 跟着类的泛型去匹配
*/
InterBImpl<Integer> i = new InterBImpl<>();
interface Inter<E> {
void show(E e);
}
class InterAImpl implements Inter<String> {
@Override
public void show(String s) {}
}
class InterBImpl<E> implements Inter<E>{
@Override
public void show(E e) {}
}
- 泛型通配符
/*
? : 任意类型
? extends E : 可以传入的是E, 或者是E的子类
? super E : 可以传入的是E, 或者是E的父类
*/
ArrayList<Coder> list1 = new ArrayList<>();
ArrayList<Manager> list2 = new ArrayList<>();
ArrayList<String> list3 = new ArrayList<>();
method(list1);
method(list2);
method(list3); // error
public static void method(ArrayList<? super Employee> list){...}
abstract class Employee {...}
class Coder extends Employee {...}
class Manager extends Employee {...}
2.2 平衡二叉树
- 二叉树,弊端:乱序,不便于查找
- 二叉搜索树,弊端:若不够平衡,和单链表一样效率低
- 平衡二叉树,规则:任意节点左右子树高度差不超过1
当添加一个节点之后,该树不再是一颗平衡二叉树,会触发旋转。
- 左左:当根节点左子树的左子树有节点插入,导致二叉树不平衡(图1、图2)
- 左右:当根节点左子树的右子树有节点插入,导致二叉树不平衡(图3)
- 右左:当根节点右子树的左子树有节点插入,导致二叉树不平衡
- 右右:当根节点右子树的右子树有节点插入,导致二叉树不平衡
2.3 红黑树
- 红黑规则
-
添加结点的规则
默认加红节点;根节点必须为黑色;父黑色无操作,父红色操作如下:
-
情况1,加入节点15,父红色,叔叔红色情况:
① 父17变黑,叔叔19变黑,祖父18变红
② 祖父18非根,对祖父继续进行判断:结束
-
情况2,加入节点14,父红色,叔叔黑色,当前节点是左孩子情况:
① 父15变黑,祖父17变红
② 以祖父17作为支点右旋(结束)
-
情况3,加入节点16,父红色,叔叔黑色,当前节点是右孩子情况:
① 以父15作为支点左旋,
② 对父15继续进行判断:情况2(父红色,叔叔黑色,当前节点是左孩子)
③ 父16变黑,祖父17变红
④ 以祖父17作为支点右旋(结束)
2.4 TreeSet 集合
作用 : 对集合中的元素进行排序操作 (底层红黑树实现)
-
自然排序
-
类实现 Comparable 接口
-
重写 compareTo 方法
-
根据方法的返回值, 来组织排序规则
负数 : 左边走 正数 : 右边走 0 : 不存
-
public class Student implements Comparable<Student>{
@Override
public int compareTo(Student o) {
int ageResult = o.age - this.age; // 倒序排列
// int ageResult = this.age - o.age; 正序排列
int nameResult = ageResult == 0 ? o.name.compareTo(this.name) : ageResult;
int result = nameResult == 0 ? 1 : nameResult;
return result;
}
}
-
比较器排序
-
在 TreeSet 的构造方法中, 传入 Compartor 接口的实现类对象
-
重写 compare 方法
-
根据方法的返回值, 来组织排序规则
负数 : 左边走 正数 : 右边走 0 : 不存
-
TreeSet<Student> stu = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int ageResult = o1.age - o2.age; // 正序排列
int nameResult = ageResult == 0 ? o1.name.compareTo(o2.name) : ageResult;
int result = nameResult == 0 ? 1 : nameResult;
return result;
}
});
-
两种排序方式比较
如果同具备自然排序, 和比较器排序, 会优先按照比较器进行排序操作
2.5 HashSet 集合类
HashSet 介绍
- HashSet 集合底层采取哈希表存储数据
- 哈希表是一种对于增删改查数据性能都较好的结构
HashSet 为了保证元素唯一性,需要同时重写对象中的 hashCode 方法和 equals 方法
/*
当添加对象的时候, 会先调用对象的hashCode方法计算出一个应该存入的索引位置, 查看该位置上是否存在元素
1. 不存在:直接存
2. 存在:调用equals方法比较内容
false : 存 true : 不存
*/
hs.add(new Student("张三",23));
hs.add(new Student("李四",23));
hs.add(new Student("王五",23));
hs.add(new Student("王五",23));
public class Student implements Comparable<Student>{
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
-
Set集合的底层原理是什么样的
JDK8之前的,哈希表:底层使用数组 + 链表(头插法)组成:
JDK8开始后,哈希表:底层采用数组 + 链表(尾插法) + 红黑树组成:
// 桶下标(索引)的计算
int aHash = "a".hashCode() ^ ("a".hashCode() >>> 16); // 二次哈希
System.out.println((16 - 1) & aHash); // 1
System.out.println(aHash % 16); // 1
int bHash = "b".hashCode() ^ ("b".hashCode() >>> 16);
System.out.println((16 - 1) & bHash); // 2
System.out.println(bHash % 16); // 2
-
哈希表的详细流程
① 创建一个默认长度16,默认加载因为0.75的数组,数组名table
② 根据元素的哈希值跟数组的长度计算出应存入的位置
③ 判断当前位置是否为null,如果是null直接存入,如果位置不为null,表示有元素, 则调用equals方法比较属性值,如果一样,则不存,如果不一样,则存入数组
④ 当数组存满到16*0.75=12时,就自动扩容,每次扩容原先的两倍
⑤ 当链表挂载元素超过了8个 (阈值) ,检查数组长度 : 没有到达64, 扩容数组;到达了64, 会转换为红黑树
2.6 LinkedHashSet 集合类
有序、不重复、无索引。
原理:底层数据结构是依然哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序。
// LinkedHashSet 特点: 去重, 并保证[存取顺序]
LinkedHashSet<String> lhs = new LinkedHashSet<>();
lhs.add("b");
lhs.add("a");
lhs.add("c");
lhs.add("c"); // // [b, a, c]
-
如果想要集合中的元素可重复
用 ArrayList 集合,基于数组的。(用的最多)
-
如果想要集合中的元素可重复,而且当前的增删操作明显多于查
用 LinkedList 集合,基于链表的。
-
如果想对集合中的元素去重
用 HashSet 集合,基于哈希表的。(用的最多)
-
如果想对集合中的元素去重,而且保证存取顺序
用 LinkedHashSet 集合,基于哈希表和双链表,效率低于HashSet。
-
如果想对集合中的元素进行排序
用 TreeSet 集合,基于红黑树。后续也可以用List集合实现排序。
2.7 Collections 集合工具类
-
可变参数
可变参数用在形参中可以接收多个数据。
可变参数在方法内部本质上就是一个数组;格式:数据类型...参数名称
传输参数非常灵活,方便,可以不传输参数,可以传输1个或者多个,也可以传输一个数组
public static void main(String[] args) {
get(1, 2, 3);
get(1, 2, 3, 4);
}
// 注意事项:一个形参列表中可变参数只能有一个;必须放在列表的最后面
public static void get(int a, int... nums) {
for (int num : nums) {
System.out.println(num);
}
}
-
Collections 集合工具类
java.utils.Collections : 是集合工具类;Collections并不属于集合,是用来操作集合的工具类。
方法名称 | 说明 |
---|---|
public static <T> boolean addAll(Collection<? super T> c, T... elements) | 给集合对象批量添加元素 |
public static void shuffle(List<?> list) | 打乱List集合元素的顺序 |
public static <T> int binarySearch (List<T> list, T key) | 以二分查找法查找元素 |
public static <T> void max/min(Collection<T> coll) | 根据默认的自然排序获取最大/小值 |
public static <T> void swap(List<?> list, int i, int j) | 交换集合中指定位置的元素 |
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d"); // [a, b, c, d]
System.out.println(Collections.binarySearch(list, "b")); // 1
Collections.shuffle(list); // [a, d, b, c]
ArrayList<Student> nums = new ArrayList<>();
Collections.addAll(nums, new Student("张三", 23), new Student("王五", 25), new Student("李四", 24));
System.out.println(Collections.max(nums)); // Student{name='王五', age=25}
Collections.swap(nums, 0, 2);
// [Student{name='李四', age=24}, Student{name='王五', age=25}, Student{name='张三', age=23}]
public class Student implements Comparable<Student>{
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
}
Collections 排序相关API;使用范围:只能对于List集合的排序
方法名称 | 说明 |
---|---|
public static <T> void sort(List<T> list) | 将集合中元素按照默认规则排序 |
public static <T> void sort(List<T> list,Comparator<? super T> c) | 将集合中元素按照指定规则排序 |
ArrayList<Integer> box = new ArrayList<>();
Collections.addAll(box, 1, 3, 5, 2, 4);
Collections.sort(box); // [1, 2, 3, 4, 5]
Collections.sort(box, (o1, o2) -> o2 - o1); // [5, 4, 3, 2, 1]
2.8 Map 集合
-
Map 集合是一种双列集合,每个元素包含两个数据
-
Map 集合的每个元素的格式:key = value(键值对元素)
key (键) : 不允许重复
value (值) : 允许重复键和值是一一对应的,每个键只能找到自己对应的值
-
key + value 这个整体 我们称之为 “键值对” 或者 “键值对对象” 在Java中使用Entry对象表示
使用场景:Map <店铺对象,List集合<商品对象>>
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<String, String> map = new HashMap<>();
map.put("张三", "北京");
map.put("李四", "北京");
map.put("王五", "上海"); // {李四=北京, 张三=北京, 王五=上海}
map.remove("王五"); // {李四=北京, 张三=北京}
System.out.println(map.isEmpty()); // false
System.out.println(map.size()); // 2
System.out.println(map.containsKey("张三")); // true
System.out.println(map.containsValue("上海")); // false
map.clear();
- TreeMap : 键排序(红黑树)
- HashMap : 键唯一(哈希表)
- LinkedHashMap : 键(哈希表 + 双向链表)
/*
双列集合底层的数据结构, 都是针对于键有效, 跟值没有关系.
TreeMap : 键排序 (实现Comparable接口, 重写compareTo方法)
HashMap : 键唯一 (重写hashCode和equals方法)
LinkedHashMap : 键唯一, 且可以保证存取顺序
*/
public class Person implements Comparable<Person> {
@Override
public int compareTo(Person o) {
return this.age - o.age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
2.9 Map 集合的遍历方式
- Map 集合的三种遍历方式
// 1. 通过键找值
Set<String> keySet = hm.keySet(); // 获取到所有的键
for (String key : keySet) { // 遍历set集合, 获取每一个键
System.out.println(key + "---" + hm.get(key)); // 调用map集合的get方法, 根据键查找对应的值
}
// 2. 通过键值对对象获取键和值
Set<Map.Entry<String, String>> entrySet = hm.entrySet(); // 获取到所有的键值对对象
for (Map.Entry<String, String> entry : entrySet) { // 遍历set集合获取每一个键值对对象
System.out.println(entry.getKey() + "---" + entry.getValue()); // 通过键值对对象, 获取键和值
}
// 3. 通过foreach方法遍历
hm.forEach((k, v) -> System.out.println(k + "---" + v));
- Map 集合练习
// 定义一个字符串,请统计每一个字符出现的次数
String info = "aababcabcdabcde";
TreeMap<Character, Integer> tm = new TreeMap<>();
char[] charArray = info.toCharArray();
for (char c : charArray) {
if (!tm.containsKey(c)) {
tm.put(c, 1);
} else {
tm.put(c, tm.get(c) + 1);
}
}
StringBuilder sb = new StringBuilder();
tm.forEach((key, value) -> sb.append(key).append("(").append(value).append(")"));
System.out.println(sb); // a(5)b(4)c(3)d(2)e(1)
// 定义一个Map集合,键表示省份,值表示市, 遍历结果
HashMap<String, List<String>> hm = new HashMap<>();
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, "南京市", "扬州市", "苏州市", "无锡市", "常州市");
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "武汉市", "孝感市", "十堰市", "宜昌市", "鄂州市");
ArrayList<String> list3 = new ArrayList<>();
Collections.addAll(list3, "成都市", "绵阳市", "自贡市", "攀枝花市", "泸州市");
hm.put("江苏省", list1);
hm.put("湖北省", list2);
hm.put("四川省", list3);
Set<Map.Entry<String, List<String>>> entrySet = hm.entrySet();
hm.forEach((province, citys) -> {
System.out.print(province + " = ");
int n = citys.size() - 1;
for (int i = 0; i < n; i++) {
System.out.print(citys.get(i) + ", ");
}
System.out.println(citys.get(n));
});
Day 7 Stream 流 & File 类
1. Stream 流
Stream 流介绍:配合Lambda表达式,简化集合和数组操作(操作不会修改数据源)
-
将数据到流中(获取流对象)
名称 说明 default Stream<E> stream() 获取当前集合对象的 Stream 流 static <T> Stream<T> stream(T[] array) 将传入的数组封装到 Stream 流对象中 static <T> Stream<T> of(T... values) 把一堆零散的数据封装到 Stream 流对象中 list.stream().forEach(s -> System.out.println(s)); set.stream().forEach(s -> System.out.println(s)); map.entrySet().stream().forEach(s -> System.out.println(s)); Arrays.stream(arr).forEach(s -> System.out.println(s)); Stream.of(1, 2, 3, 4, 5, 6).forEach(s -> System.out.println(s));
-
中间操作方法
名称 说明 Stream<T> filter(Predicate<? super T> predicate) 用于对流中的数据进行过滤 Stream<T> limit(long maxSize) 获取前几个元素 Stream<T> skip(long n) 跳过前几个元素 Stream<T> distinct() 去除流中重复的元素依赖 (hashCode 和 equals方法) static <T> Stream<T> concat(Stream a, Stream b) 合并a和b两个流为一个流 // 需求1: 取前3个数据在控制台输出 list.stream().limit(3).forEach(s -> System.out.println(s)); // 需求2: 跳过3个元素, 把剩下的元素在控制台输出 list.stream().skip(3).forEach(s -> System.out.println(s)); // 需求3: 跳过2个元素, 把剩下的元素中前2个在控制台输出 list.stream().skip(2).limit(2).forEach(s -> System.out.println(s)); // 需求4: 取前4个数据组成一个流 Stream<String> s1 = list.stream().limit(4); // 需求5: 跳过2个数据组成一个流 Stream<String> s2 = list.stream().skip(2); // 需求6: 合并需求4和需求5得到的流, 并把结果在控制台输出 Stream<String> s3 = Stream.concat(s1, s2); // 需求7: 合并需求4和需求5得到的流, 并把结果在控制台输出,要求字符串元素不能重复 Stream<String> s4 = s3.distinct(); // 注意事项: 流对象已经被消费过, 就不允许再次消费了. // e.g. s3.forEach(s -> System.out.println(s));
-
终结操作方法
名称 说明 void forEach(Consumer action) 对此流的每个元素执行遍历操作 long count() 返回此流中的元素数 long count = Stream.of(1, 2, 3, 4, 5, 6).filter(s -> s % 2 == 0).count(); System.out.println(count); // 3 // 因为count()没有返回Stream类型, 所以终结
Stream 收集操作
-
把 Stream 流操作后的结果数据转回到集合
名称 说明 R collect(Collector collector) 开始收集Stream流,指定收集器 -
Collectors 工具类提供了具体的收集方式
名称 说明 public static <T> Collector toList() 把元素收集到List集合中 public static <T> Collector toSet() 把元素收集到Set集合中 public static Collector toMap(Function keyMapper , Function valueMapper) 把元素收集到Map集合中 Stream<Integer> ls = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).filter(s -> s % 2 == 0); System.out.println(ls.collect(Collectors.toList())); // [2, 4, 6, 8, 10] Stream<Integer> ls2 = Stream.of(2, 3, 4, 5, 6, 7, 8, 9, 10, 10).filter(s -> s % 2 == 0); System.out.println(ls2.collect(Collectors.toSet())); // [2, 4, 6, 8, 10] ArrayList<String> list = new ArrayList<>(); list.add("张三,23"); list.add("李四,24"); list.add("王五,25"); Map<String, Integer> map = list.stream().filter(new Predicate<String>() { @Override public boolean test(String s) { return Integer.parseInt(s.split(",")[1]) >= 24; } }).collect(Collectors.toMap(new Function<String, String>() { @Override public String apply(String s) { return s.split(",")[0]; } }, new Function<String, Integer>() { @Override public Integer apply(String s) { return Integer.parseInt(s.split(",")[1]); } })); System.out.println(map); // {李四=24, 王五=25}
2. 案例
/*
现在有两个 ArrayList 集合,分别存储6名男演员和6名女演员,要求完成如下的操作:
1. 男演员只要名字为3个字的前两人
2. 女演员只要姓林的,并且不要第一个
3. 把过滤后的男演员姓名和女演员姓名合并到一起
4. 把上一步操作后的元素作为构造方法的参数创建演员对象,遍历数据
*/
Stream<String> s1 = manList.stream().filter(s -> s.length() == 3).limit(2);
Stream<String> s2 = womanList.stream().filter(s -> s.startsWith("林")).skip(1);
Stream<String> s3 = Stream.concat(s1, s2);
s3.forEach(new Consumer<String>() {
@Override
public void accept(String name) {
Actor a = new Actor(name);
System.out.println(a);
}
});
3. File 类
File类代表操作系统的文件对象(文件、文件夹)
-
File 类创建对象
方法名称 说明 public File(String pathname) 根据文件路径创建文件对象 public File(String parent, String child) 根据父路径名字符串和子路径名字符串创建文件对象 public File(File parent, String child) 根据父路径对应文件对象和子路径名字符串创建文件对象 File f1 = new File("D:\\A.txt"); f1.createNewFile(); File f2 = new File("D:\\B.txt"); File f3 = new File("D:\\", "A.txt"); File f4 = new File(new File("C:\\"), "A.txt"); System.out.println(f2.exists()); // false System.out.println(f3.exists()); // true System.out.println(f4.exists()); // false
-
相对路径和绝对路径
绝对路径: 从盘符根目录开始,一直到某个具体的文件或文件夹
相对路径: 相对于当前项目
-
File 类的常用方法
方法名称 说明 public boolean isDirectory() 判断此路径名表示的File是否为文件夹 public boolean isFile() 判断此路径名表示的File是否为文件 public boolean exists() 判断此路径名表示的File是否存在 public long length() 返回文件的大小(字节数量) public String getAbsolutePath() 返回文件的绝对路径 public String getPath() 返回定义文件时使用的路径 public String getName() 返回文件的名称,带后缀 public long lastModified() 返回文件的最后修改时间(时间毫秒值) File f1 = new File("D:\\A.txt"); File f2 = new File("D:\\test"); File f3 = new File("A.txt"); System.out.println(f1.isDirectory()); // false System.out.println(f1.isFile()); // true System.out.println(f1.exists()); // true System.out.println(f1.length()); // 0 System.out.println(f3.getAbsolutePath()); // C:\Develop\JavaStudy\Advanced-codes\A.txt System.out.println(f1.getName()); // A.txt System.out.println(f2.getName()); // test long time = f1.lastModified(); System.out.println(new Date(time)); // Fri May 10 15:21:53 CST 2024
-
File 类的创建和删除
方法名称 说明 public boolean createNewFile() 创建一个新的空的文件 public boolean mkdir() 只能创建一级文件夹 public boolean mkdirs() 可以创建多级文件夹 public boolean delete() 删除由此抽象路径名表示的文件或空文件夹 File f1 = new File("src\\com\\itheima\\day12\\B.txt"); File f2 = new File("src\\com\\itheima\\day12\\aaa"); File f3 = new File("src\\com\\itheima\\day12\\C.txt"); System.out.println(f1.createNewFile()); // true System.out.println(f2.mkdirs()); // false System.out.println(f3.mkdirs()); // true System.out.println(f1.delete()); // true System.out.println(f2.delete()); // false // delete 方法删除文件夹, 只能删除空的文件夹
-
File 类的遍历方法
方法名称 说明 public File[] listFiles() 获取当前目录下所有的 “一级文件对象” 返回 File 数组 - 当调用者File表示的路径不存在时,返回null
- 当调用者File表示的路径是文件时,返回null
- 当调用者File表示的路径是一个空文件夹时,返回一个长度为0的数组
- 当调用者File表示的路径是需要权限才能访问的文件夹时,返回null
// 需求: 键盘录入一个文件夹路径,找出这个文件夹下所有的 .java 文件 printJavaFile(dir); public static void printJavaFile(File dir) { File[] files = dir.listFiles(); for (File file : files) { if (file.isFile()) { if (file.getName().endsWith(".java")) { System.out.println(file); } } else if (file.listFiles() != null) { printJavaFile(file); } } }
4. 案例
// 需求: 设计一个方法, 删除文件夹
deleteDir(new File("D:\\test2"));
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();
}
// 需求:键盘录入一个文件夹路径,统计文件夹中每种文件的个数并打印(考虑子文件夹)
static HashMap<String, Integer> hm = new HashMap<>();
static int count = 0;
public static void main(String[] args) {
File dir = FileTest1.getDir();
getCount(dir);
hm.forEach((key, value) -> System.out.println(key + ":" + value + "个"));
System.out.println("没有后缀名文件的个数为:" + count);
}
public static void getCount(File dir) {
File[] files = dir.listFiles();
for (File file : files) {
// 1. 若file是文件
if (file.isFile()) {
String fileName = file.getName();
// a. 若file有后缀名
if (fileName.contains(".")) {
String[] sArr = fileName.split("\\.");
String type = sArr[sArr.length - 1];
if (!hm.containsKey(type)) {
hm.put(type, 1);
} else {
hm.put(type, hm.get(type) + 1);
}
} else { // b. 若file没有后缀名
count++;
}
} // 2. 若file是非空文件夹
else if (file.listFiles() != null) {
getCount(file);
}
}
}
Day 8 IO 流
1. FileOutputStream 字节输出流
构造方法 | 说明 |
---|---|
FileOutputStream (String name) | 输出流关联文件, 文件路径以字符串形式给出 |
FileOutputStream (String name, boolean append) | 第二个参数是追加写入的开关 |
FileOutputStream (File file) | 输出流关联文件, 文件路径以File对象形式给出 |
FileOutputStream (File file, boolean append) | 第二个参数是追加写入的开关 |
成员方法 | 说明 |
---|---|
void write (int b) | 写出单个字节 |
void write (byte[] b) | 写出一个字节数组 |
void write (byte[] b, int off, int len) | 写出字节数组的一部分 |
/*
输出流关联文件, 文件如果不存在: 会自动创建出来
如果文件存在: 会清空现有的内容, 然后再进行写入操作
*/
FileOutputStream fos = new FileOutputStream("D:\\A.txt", true);
fos.write(97); // a
fos.write(98); // ab
fos.write(99); // abc
byte[] bys = {97, 98, 99};
fos.write(bys); // abcabc
fos.write(bys, 1, 2); // abcabcbc
fos.write("你好你好".getBytes()); // abcabcbc你好你好
fos.close();
// 注: 流对象使用完毕后, 记得调用 close 方法关闭不然会占用资源
IO 流的异常处理方式:
-
JDK7 版本之前
FileOutputStream fos = null; try { fos = new FileOutputStream("D:\\B.txt"); fos.write("abc".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { if(fos != null){ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } }
-
JDK7 版本之后
try(FileOutputStream fos = new FileOutputStream("D:\\B.txt");) { fos.write("abc".getBytes()); } catch (IOException e) { e.printStackTrace(); } // 注: try() 中自动关流的对象, 需要实现 AutoCloseable 接口
2. FileInputStream 字节输入流
构造方法 | 说明 |
---|---|
FileInputStream(String name) | 输入流关联文件, 文件路径以字符串形式给出 |
FileInputStream(File file) | 输入流关联文件, 文件路径以File对象形式给出 |
成员方法 | 说明 |
---|---|
int read() | 读取一个字节并返回, 如果到达文件结尾则返回 -1 |
int read(byte[] b) | 将读取到字节, 放到传入的数组返回读取到的有效字节个数如果到达文件结尾则返回 -1 |
// 注:如果文件不存在会抛出 FileNotFoundException 异常,如果是文件夹会拒绝访问
FileInputStream fis = new FileInputStream("D:\\A.txt");
int i;
while ((i = fis.read()) != -1) {
System.out.println((char) i);
}
fis.close();
构造方法 | 说明 |
---|---|
public String(byte[] bytes, int offset, int length) | 将字节数组转换为字符串参数 1 : 字节数组参数 2 : 起始索引参数 3 : 转换的个数 |
// 将 D:\嘿嘿.png,拷贝到 E:\ 根目录下
FileInputStream fis = new FileInputStream("D:\\嘿嘿.png");
FileOutputStream fos = new FileOutputStream("E:\\嘿嘿.png");
byte[] bys = new byte[1024];
int len;
while( (len = fis.read(bys)) != -1){
fos.write(bys, 0, len);
}
fis.close();
fos.close();
3. FileReader 字符输入流
- FileReader 字符输入流
用于读取纯文本文件,解决中文乱码问题
构造方法 | 说明 |
---|---|
FileReader(String fileName) | 字符输入流关联文件,路径以字符串形式给出 |
FileReader(File file) | 字符输入流关联文件,路径以File对象形式给出 |
成员方法 | 说明 |
---|---|
public int read() | 读取单个字符 |
public int read(char[] cbuf) | 读取一个字符数组, 返回读取到的有效字符个数 |
FileReader fr = new FileReader("D:\\A.txt");
int i;
while ((i = fr.read()) != -1) {
System.out.print((char)i);
}
char[] chs = new char[1024];
int len;
while( (len = fr.read(chs)) != -1 ){
System.out.println(new String(chs, 0, len));
}
fr.close();
-
字符集和字符编码
字符集:是指多个字符的集合 (GBK, Unicode...)
字符编码:字符编码是指一种映射规则
-
英文字符:占用一个字节, 使用正数表示
-
中文字符:GBK 占用2个字节;Unicode (UTF-8) 占用3个字节
成员方法 说明 public byte[] getBytes() 使用平台默认字符编码方式, 对字符串编码 public byte[] getBytes(String charsetName) 使用使用字符编码方式, 对字符串编码 构造方法 说明 public String(byte[] bytes) 使用平台默认字符编码方式, 对字符串解码 public String(byte[] bytes, String charsetName) 使用使用字符编码方式, 对字符串解码 /* 平台默认字符编码 : Unicode - UTF-8的形式 重点记忆: 中文字符, 通常都是由负数的字节进行组成的. 特殊情况: 可能会出现正数, 但是就算有正数, 第一个字节肯定是负数 注意事项: 今后如果出现乱码问题, 大概率是因为编解码方式不一致所导致的. */ String s = "你好,你好"; byte[] bytes = s.getBytes(); byte[] gbks = s.getBytes("gbk"); System.out.println(Arrays.toString(bytes)); System.out.println(Arrays.toString(gbks)); // [-28, -67, -96, -27, -91, -67, 44, -28, -67, -96, -27, -91, -67] // [-60, -29, -70, -61, 44, -60, -29, -70, -61] byte[] gbkBytes = {-60, -29, -70, -61, 44, -60, -29, -70, -61}; System.out.println(new String(gbkBytes, "GBK")); // 你好,你好
-
4. FileWriter 字符输出流
-
FileWriter 字符输出流
构造方法 说明 FileWriter(String fileName) 字符输出流关联文件,路径以字符串形式给出 FileWriter(String fileName, boolean append) 参数2: 追加写入的开关 FileWriter(File file) 字符输出流关联文件,路径以File对象形式给出 FileWriter(File file, boolean append) 参数2: 追加写入的开关 成员方法 说明 public void write(int c) 写出单个字符 public void write(char[] cbuf) 写出一个字符数组 public write(char[] cbuf, int off, int len) 写出字符数组的一部分 public void write(String str) 写出字符串 public void write(String str, int off, int len) 写出字符串的一部分 FileWriter fw = new FileWriter("D:\\C.txt"); char[] chs = {'a','b','c'}; fw.write('a'); // a fw.write(chs); // aabc fw.write(chs, 0, 2); // aabcab fw.write("你好你好~"); // aabcab你好你好~ fw.write("哈哈哈哈哈", 0, 2); // aabcab你好你好~哈哈 fw.flush(); // 注: 字符输出流写出数据, 需要调用flush或close方法, 数据才会写出 fw.write("人活一世, 草木一秋\r\n"); fw.write("今晚不减肥, 我要吃肉!"); fw.close();
-
案例
/* 需求1: 图片文件加密解密 加密思路:改变原始文件中的字节,就无法打开了 字节 ^ 2 解密思路:将文件中的字节还原成原始字节即可 字节 ^ 2 */ FileInputStream fis = new FileInputStream("D:\\A.jpg"); ArrayList<Integer> list = new ArrayList<>(); int i; while((i = fis.read()) != -1){ list.add(i); } fis.close(); FileOutputStream fos = new FileOutputStream("D:\\A.jpg"); for (Integer myByte : list) { fos.write(myByte ^ 2); } fos.close();
// 需求2: 统计文件中每一个字符出现的次数,随后展示在控制台 HashMap<Character, Integer> hm = new HashMap<>(); FileReader fr = new FileReader("D:\\A.txt"); // abcabcbc你好你好 int i; while ((i = fr.read()) != -1) { char c = (char) i; if (!hm.containsKey(c)) { hm.put(c, 1); } else { hm.put(c, hm.get(c) + 1); } } fr.close(); StringBuilder sb = new StringBuilder(); hm.forEach((k, v) -> sb.append(k).append("(").append(v).append(")")); System.out.println(sb); // 你(2)a(2)b(3)c(3)好(2)
// 需求3: 拷贝一个文件夹, 考虑子文件夹 File src = new File("D:\\test"); File dest = new File("E:\\"); if (src.equals(dest)) { System.out.println("目标文件夹是源文件夹的子文件夹"); } else { copyDir(src, dest); } public static void copyDir(File src, File dest) throws IOException { File newDir = new File(dest, src.getName()); // 例: newDir = E:\\test newDir.mkdirs(); File[] files = src.listFiles(); for (File file : files) { if (file.isFile()) { // 如果是文件, 则拷贝到newDir copy(file, newDir); } else { copyDir(file, newDir); // 如果是文件夹, 则递归调用 } } } private static void copy(File file, File newDir) throws IOException { FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(new File(newDir, file.getName())); // 例: fis = D:\\test\\3.png fos = E:\\test\\3.png int len; byte[] bys = new byte[1024]; while ((len = fis.read(bys)) != -1) { fos.write(bys, 0, len); } fis.close(); fos.close(); }
5. 字符缓冲流
缓冲流不具备读写功能, 它们只是对普通的流对象进行包装;真正和文件建立关联的, 还是普通的流对象
构造方法 | 说明 |
---|---|
BufferedReader(Reader reader) | 对传入的字符输入流进行包装 |
BufferedWriter(Writer writer) | 对传入的字符输出流进行包装 |
字符缓冲流中特有的方法:
成员方法 | 说明 |
---|---|
public String readLine() | 读取一行字符串, 读取到末尾返回 null |
public void newLine() | 写出换行符 (具有跨平台性) |
/*
案例: 请对 D:\出师表.txt 文件进行排序操作
3.侍中、侍郎郭攸之、费祎、董允等,...
8.愿陛下托臣以讨贼兴复之效,不效,...
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,...
*/
TreeSet<String> ts = new TreeSet<>();
BufferedReader br = new BufferedReader(new FileReader("D:\\出师表.txt"));
String line;
while((line = br.readLine()) != null){
ts.add(line);
}
br.close();
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\出师表.txt"));
for (String content : ts) {
bw.write(content);
bw.newLine();
}
bw.close();
6. 转换流 & 序列化流
- 转换流
/*
构造方法:
InputStreamReader(InputStream in, String CharsetName)
OutputStreamWriter(OutputStream in, String CharsetName)
*/
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\Test.txt"), "gbk");
int i;
while((i = isr.read()) != -1){ // 你好
System.out.print((char)i);
}
isr.close();
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\Test.txt", true), "GBK");
osw.write("哈哈"); // 你好哈哈
osw.close();
- 序列化流
可以在流中,以字节的形式直接读写对象
构造方法 | 说明 |
---|---|
public ObjectInputStream (InputStream in) | 对象输入流关联文件, 关联方式使用字节输入流 |
public ObjectOutputStream (OutputStream out) | 对象输出流关联文件, 关联方式使用字节输出流 |
需求: 现有3个学生对象, 并将对象序列化到流中, 随后完成反序列化操作
// solution 1:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stu.txt"));
oos.writeObject(stu1);
oos.writeObject(stu2);
oos.writeObject(stu3);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stu.txt"));
while (true) {
try {
Object o = ois.readObject();
System.out.println(o);
} catch (EOFException e) {
break;
}
}
ois.close();
// solution 2:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("stu.txt"));
ArrayList<Student> list = new ArrayList<>();
list.add(stu1);
list.add(stu2);
list.add(stu3);
oos.writeObject(list);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("stu.txt"));
ArrayList<Student> list2 = (ArrayList<Student>) ois.readObject();
for (Student stu : list2) {
System.out.println(stu);
}
ois.close();
序列化流的操作流程:
// 今后若要进行序列化操作,推荐手动编写 serialVersionUID
public class Student implements Serializable {
private static final long serialVersionUID = 123L;
...
}
7. 打印流 & Properties 集合
-
打印流
打印流可以实现方便、高效的打印数据到文件中去,并且可以指定字符编码
构造器 说明 public PrintStream (OutputStream os) 打印流直接通向字节输出流管道 public PrintStream (File f) 打印流直接通向文件对象 public PrintStream (String filepath) 打印流直接通向文件路径 public PrintWriter (OutputStream os) 打印流直接通向字节输出流管道 public PrintWriter (Writer w) 打印流直接通向字符输出流管道 public PrintWriter (File f) 打印流直接通向文件对象 public PrintWriter (String filepath) 打印流直接通向文件路径 /* write() : 写出一个字节, 不建议使用, 无法原样写入 print() : 原样写入数据, 无换行 println() : 原样写入数据, 带有换行 */ System.out.println("你好"); // 你好 System.err.println("哈哈"); // 哈哈(红字) PrintStream ps = new PrintStream(new FileOutputStream("E.txt", true)); // PrintStream ps = new PrintStream("E.txt"); // PrintStream ps = new PrintStream("F.txt", "gbk"); ps.print(97); ps.println(false); ps.println("大家好"); ps.close(); PrintWriter pw = new PrintWriter(new FileWriter("F.txt"), true); pw.println("你好");
打印流的优势 : 使用方便,PrintStream、PrintWriter;两个流都具备自动刷出、自动换行的功能
-
Properties 集合
Properties 其实就是一个Map集合,常用于加载配置文件
内部存在着两个方法,可以很方便的将集合中的键值对写入文件,也可以方便的从文件中读取
- Properties 作为集合的使用
方法 说明 Object setProperty(String key, String value) 添加(修改)一个键值对 String getProperty(String key) 根据键获取值 Set<String> stringPropertyNames() 获取集合中所有的键 Properties prop = new Properties(); prop.setProperty("username", "admin"); prop.setProperty("password", "1234"); System.out.println(prop.getProperty("username")); System.out.println(prop.getProperty("password")); Set<String> keySet = prop.stringPropertyNames(); for (String key : keySet) { System.out.println(key + "---" + prop.getProperty(key)); }
- Properties 和 IO 有关的方法
方法 说明 void load(InputStream inStream) 从流中加载数据到集合(字节流) void load(Reader reader) 从流中加载数据到集合(字符流) void store(OutputStream out, String comments) 将集合的键值对写出到文件(字节流) void store(Writer writer, String comments) 将集合的键值对写出到文件(字符流) Properties prop = new Properties(); prop.setProperty("username", "admin"); prop.setProperty("password", "1234"); FileWriter fos = new FileWriter("config.properties"); prop.store(fos, null); fos.close(); Properties prop2 = new Properties(); FileInputStream fis = new FileInputStream("config.properties"); prop2.load(fis); fis.close();
Day 9 多线程
1. 进程和线程
-
进程(Process):程序的执行过程
并行:在同一时刻,有多个指令在多个CPU上【同时】执行
并发:在同一时刻,有多个指令在单个CPU上【交替】执行
-
独立性:每一个进程都有自己的空间,在没有经过进程本身允许的情况下,一个进程不可以直接访问其它的的进程空间
-
动态性:进程是动态产生,动态消亡的
-
并发性:任何进程都可以同其它进程一起并发执行
多进程同时工作:对于一个 CPU 而言,它是在多个进程间轮换执行的
-
-
线程(Thread):进程可以同时执行多个任务,每个任务就是线程
- 随着处理器上的核心数量越来越多,现在大多数计算机都比以往更加擅长并行计算
- 一个线程,在一个时刻,只能运行在一个处理器核心上
多线程的意义:提高执行效率,同时处理多个任务
2. Java 开启线程的方式
-
继承 Thread 类
MyThread mt = new MyThread(); mt.start(); // 调用start方法开启线程 class MyThread extends Thread { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("线程任务执行了" + i); } } }
-
实现 Runnable 接口
MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); // 创建线程对象, 将资源传入 t.start(); // 使用线程对象调用start开启线程 class MyRunnable implements Runnable { @Override public void run() { for (int i = 1; i <= 200; i++) { System.out.println("线程任务执行了" + i); } } }
-
实现 Callable 接口
MyCallable mc = new MyCallable(); FutureTask<Integer> task = new FutureTask<>(mc); // 创建线程任务对象, 封装线程资源 Thread t = new Thread(task); // 创建线程对象, 传入线程任务 t.start(); // 使用线程对象调用start开启线程 System.out.println("task1获取到的结果为:" + task.get()); class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } return sum; } }
-
Java程序默认是多线程的, 程序启动后默认会存在两条线程:主线程、垃圾回收线程
MyThread mt = new MyThread(); mt.start(); for (int i = 1; i <= 20000; i++) { // main线程、mt线程交替执行 System.out.println("main线程执行了"); }
for (int i = 1; i <= 500000; i++) { // 垃圾回收线程 new Demo(); } class Demo { @Override protected void finalize() throws Throwable { System.out.println("垃圾被清理了"); } }
3. Thread 类的常见方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
MyThread mt1 = new MyThread();
mt1.setName("fox: ");
MyThread mt2 = new MyThread("fox: ");
class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
}
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr, "monkey: ");
t.start(); // monkey: 线程执行了
System.out.println(Thread.currentThread().getName() + ": 线程执行了");
// main: 线程执行了
class MyRunnable extends Object implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程执行了");
}
}
方法名称 | 说明 |
---|---|
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
for (int i = 5; i >= 1; i--) {
System.out.println("倒计时" + i + "秒");
Thread.sleep(1000);
}
线程的调度方式
-
抢占式调度(随机)
-
非抢占式调度(轮流使用)
方法名称 | 说明 |
---|---|
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 2000; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}, "线程A: ");
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 2000; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}, "线程B: ");
System.out.println(t1.getPriority()); // 5
t1.setPriority(1);
t2.setPriority(10); // 优先级: 1 ~ 10 默认为5
t1.start();
t2.start(); // t2更有可能率先结束
Thread t1 = new Thread(() -> {
for(int i = 1; i <= 20; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}, "线程C: ");
Thread t2 = new Thread(() -> {
for(int i = 1; i <= 2000; i++){
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}, "守护线程D: ");
t2.setDaemon(true);
t1.start();
t2.start(); // D线程随着C的消亡而消亡
4. 线程的安全和同步
-
安全问题出现的条件
是多线程环境;有共享数据;有多条语句操作共享数据
-
将多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程可以执行
同步可以解决多线程的数据安全问题,但是也会降低程序的运行效率
- 同步代码块
// 需求:电影院共有2000张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
TicketTask task = new TicketTask();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
Thread t3 = new Thread(task);
t1.start();
t2.start();
t3.start();
class TicketTask implements Runnable {
private int tickets = 2000;
private Object o = new Object();
@Override
public void run() {
while (true) {
synchronized (o) { // 快捷键:ctrl + alt + t
if (tickets == 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
tickets--;
}
}
}
}
注意事项:锁对象可以是任意对象,但是需要保证多条线程的锁对象,是同一把锁
TicketTask t1 = new TicketTask();
TicketTask t2 = new TicketTask();
TicketTask t3 = new TicketTask();
t1.start();
t2.start();
t3.start();
class TicketTask extends Thread {
private static int tickets = 2000; // 加上static
private static Object o = new Object(); // 加上static
}
- 同步方法
public void run() {
while (true) {
if(method()) break;
}
}
private synchronized boolean method() {
...
}
-
Lock 锁
使用 Lock 锁,我们可以更清晰的看到哪里加了锁,哪里释放了锁
private ReentrantLock lock = new ReentrantLock(); // 互斥锁
public void run() {
while (true) {
try {
lock.lock();
...
} finally {
lock.unlock();
}
}
}
-
死锁
由于两个或者多个线程互相持有对方所需要的资源导致这些线程处于等待状态,无法前往执行
5. 线程通信
线程通信:确保线程能够按照预定的顺序执行,并且能够安全地访问共享资源
- 两条线程通信
注意事项:这些方法需要使用锁对象调用
成员方法 | 说明 |
---|---|
void wait() | 使当前线程等待 |
void notify(); | 随机唤醒单个等待的线程 |
void notifyAll(); | 唤醒所有等待的线程 |
Printer p = new Printer();
new Thread(new Runnable() {...}).start(); // 匿名内部类print1
new Thread(new Runnable() {...}).start(); // 匿名内部类print2
class Printer {
int flag = 1;
public void print1() throws InterruptedException {
if(flag != 1){
Printer.class.wait(); // 锁对象调用
}
System.out.print("传");
System.out.print("智\n");
flag = 2;
Printer.class.notify();
}
public void print2() throws InterruptedException {同理}
}
Q: sleep 方法和 wait 方法的区别?
A: sleep 方法是线程休眠, 时间到了自动醒来, sleep 方法在休眠的时候, 不会释放锁
wait 方法是线程等待, 需要由其它线程进行 notify 唤醒, wait方法在等待期间, 会释放锁
- 三条线程通信
等待唤醒机制:使用 ReentrantLock 实现同步,并获取 Condition 对象
成员方法 | 说明 |
---|---|
void await() | 指定线程等待 |
void signal(); | 指定唤醒单个等待的线程 |
Printer p = new Printer();
new Thread(new Runnable() {...}).start(); // 匿名内部类print1
new Thread(new Runnable() {...}).start(); // 匿名内部类print2
new Thread(new Runnable() {...}).start(); // 匿名内部类print3
class Printer {
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
int flag = 1;
public void print1() throws InterruptedException {
lock.lock();
if (flag != 1) {
c1.await();
}
System.out.print("传");
System.out.print("智\n");
flag = 2;
c2.signal();
lock.unlock();
}
public void print2() throws InterruptedException {同理}
public void print3() throws InterruptedException {同理}
}
- 生产者消费者模式
new Thread(new Producer()).start();
new Thread(new Consumer()).start();
// 为了解耦生产者和消费者的关系,通常会采用共享的数据区域 (缓冲区)
public class WareHouse {
public static boolean mark = false;
public static ReentrantLock lock = new ReentrantLock();
public static Condition producer = lock.newCondition();
public static Condition consumer = lock.newCondition();
}
// 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
public class Producer implements Runnable {
@Override
public void run() {
while (true) {
WareHouse.lock.lock();
if(!WareHouse.mark){
System.out.println("生产者线程生产包子...");
WareHouse.mark = true;
WareHouse.consumer.signal();
} else {
try {
WareHouse.producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
WareHouse.lock.unlock();
}
}
}
// 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
public class Consumer implements Runnable {同理}
6. 线程生命周期
线程被创建并启动以后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态
Java 中的线程状态被定义在了 java.lang.Thread.State 枚举类:
状态 | 具体含义 |
---|---|
NEW(新建) | 创建线程对象 |
RUNNABLE(就绪) | start 方法被调用,但是还没有抢到 CPU 执行权 |
BLOCKED(阻塞) | 线程开始运行,但是没有获取到锁对象 |
WAITING(等待) | wait 方法 |
TIMED_WAITING(计时等待) | sleep 方法 |
TERMINATED(结束状态) | 代码全部运行完毕 |
7. 线程池
-
理解线程池的好处
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互
当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程,就会严重浪费系统资源
将线程对象交给线程池维护,可以降低系统成本 ,从而提升程序的性能
- 使用 JDK 提供的线程池
Executors 中提供静态方法来创建线程池
方法 | 介绍 |
---|---|
static ExecutorService newCachedThreadPool () | 创建一个默认的线程池 |
static newFixedThreadPool (int nThreads) | 创建一个指定最多线程数量的线程池 |
// 1. 获取线程池对象
ExecutorService pool = Executors.newFixedThreadPool(10);
// 2. 提交线程任务
for (int i = 1; i <= 100; i++) {
pool.submit(() -> System.out.println(Thread.currentThread().getName()));
}
pool.shutdown();
- 自定义线程池
/*
参数5: 任务队列
1) 有界队列 new ArrayBlockingQueue<>(10)
2) 无界队列 new LinkedBlockingDeque<>()
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 5, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for(int i = 1; i <= 16; i++){
pool.submit(() -> System.out.println(Thread.currentThread().getName()));
}
// 临时线程什么时候创建? 线程任务数 > 核心线程数 + 任务队列的数量
// 什么时候会开启拒绝策略? 线程任务数 > 最大线程数 + 任务队列的数量
8. 单例设计模式
-
单例指单个实例,保证类的对象在内存中只有一份
-
使用场景:
如果创建一个对象需要消耗的资源过多,比如 I/O 与数据库的连接
并且这个对象完全是可以复用的, 我们就可以考虑将其设计为单例的对象
// a. 饿汉式
Single s = Single.getInstance();
class Single {
private Single() {}
private static Single s = new Single();
public static Single getInstance() {
return s;
}
}
// b. 懒汉式 (延迟加载模式)
Single s = Single.getInstance();
class Single {
private Single() {}
private static Single s;
public static Single getInstance() {
// 懒汉式为什么推荐双重检查锁?
if (s == null) { // 1. 避免效率浪费
synchronized (Single.class) {
if (s == null) { // 2. 避免创建出多个对象
s = new Single();
}
}
}
return s;
}
}
参考链接:https://www.bilibili.com/video/BV1Fv4y1q7ZH?p=1&vd_source=ed621eaa6bcf9bf6acb7d0527c30489a
网友评论