Java进阶
一、双列集合
1、特点:
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值可以重复
- 键和值是一一对应的,每一个键只能找到自己对应的值
- 键 + 值 这个整体我们称之为”键值对“ 或者”键值对对象“,在Java中叫做”Entry对象“
2、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() | 集合的长度,也就是集合中键值对的个数 |
public class A01_MapDemo1 {
public static void main(String[] args) {
/*
V put(K key, V value) 添加元素
V remove(Object key) 根据删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object value) 判断集合是否包含是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数
*/
// 1.创建Map集合的对象
Map<String, String> map = new HashMap<>();
// 2.添加元素
// put方法的细节:
// 添加/覆盖
// 在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合当中,方法返回null
// 在添加数据的时候,如果键存在,那么会将原有的键值对进行覆盖,并将被覆盖的值返回
String value1 = map.put("郭靖", "黄蓉");
System.out.println("value1 = " + value1);; // null
map.put("韦小宝", "沐剑屏");
map.put("尹志平", "小龙女");
String value2 = map.put("郭靖", "小鱼儿");
System.out.println("value2 = " + value2);; // 黄蓉
// 判断是否包含key
boolean keyResult = map.containsKey("郭靖");
System.out.println("keyResult = " + keyResult); // true
// 判断是否包含value
boolean valueResult = map.containsValue("小鱼儿");
System.out.println("valueResult = " + valueResult); // true
// 判断集合是否为空
boolean result1 = map.isEmpty(); // true
// 集合的长度
int size = map.size();
System.out.println(size); // 3
// 3.打印集合
System.out.println(map); // {韦小宝=沐剑屏, 尹志平=小龙女, 郭靖=小鱼儿}
// 删除
String result = map.remove("郭靖");
System.out.println("result = " + result); // 小鱼儿
System.out.println(map); // {韦小宝=沐剑屏, 尹志平=小龙女}
// 清空
map.clear();
System.out.println(map); // {}
}
}
3、Map集合遍历
-
第一种(键找值)
public class A02_MapDemo2 { public static void main(String[] args) { // Map集合的第一种遍历方式 // 1.创建Map集合对象 Map<String, String> map = new HashMap<>(); // 2.添加元素 map.put("郭靖", "黄蓉"); map.put("韦小宝", "沐剑屏"); map.put("尹志平", "小龙女"); // 3.通过键找值 // 3.1获取所有的键,把这些键放到一个单列集合当中 Set<String> keys = map.keySet(); for (String key : keys) { // 3.3利用map集合中的键来获取对应的值 String value = map.get(key); System.out.println(key + " = " + value); } } }
-
第二种(键值对)
public class A03_MapDemo3 { public static void main(String[] args) { // Map集合的第二种遍历方式(键值对) // 1.创建Map集合对象 Map<String, String> map = new HashMap<>(); // 2.添加元素 map.put("郭靖", "黄蓉"); map.put("韦小宝", "沐剑屏"); map.put("尹志平", "小龙女"); // 3.通过键值对对象进行遍历 // 3.1通过entrySet方法获取所有的键值对对象,返回一个set集合 Set<Map.Entry<String, String>> entries = map.entrySet(); // 3.2遍历entries集合,得到里面的每一个键值对对象 for (Map.Entry<String, String> entry : entries) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + " = " + value); } } }
-
第三种(Lambda表达式)
public class A04_MapDemo4 { public static void main(String[] args) { // Map集合的第二种遍历方式(Lambda表达式) // 1.创建Map集合对象 Map<String, String> map = new HashMap<>(); // 2.添加元素 map.put("郭靖", "黄蓉"); map.put("韦小宝", "沐剑屏"); map.put("尹志平", "小龙女"); // 3.利用lambda表达式进行遍历 // 底层: // forEach其实就是利用entrySet方法进行遍历,依次得到每一个键和值 map.forEach(new BiConsumer<String, String>() { @Override public void accept(String key, String value) { System.out.println(key + " = " + value); } }); System.out.println("-----------------------------"); // 简写 map.forEach((key, value) -> System.out.println(key + " = " + value)); } }
4、HashMap
4.1、特点
- HashMap是Map里面的一个实现类
- 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了
- 特点都是由键决定的:不重复、无索引、无序:不会按照key进行排序
- HashMap跟HashSet底层原理是一模一样的,都是哈希表结构
- 依赖hashcode方法和equals方法保证键的唯一。
- 如果键存储的是自定义对象,需要重写hashCode和equals方法,如果不重写,在插入相同的key时会变成一条新数据,而不是覆盖。
4.2、案例
public class A06_HashMapDemo2 {
public static void main(String[] args) {
/*
某个班级80名学生,现在需要组成秋游活动
班长提供了四个景点依次是(A,B,C,D)
每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多
*/
// 1.需要先让学生投票
// 定义一个数组,存储4个景点
String[] arr = {"A", "B", "C", "D"};
// 利用随机数模拟80个同学,并把投票结果存储起来
ArrayList<String> list = new ArrayList<>();
Random r = new Random();
for (int i = 0; i < 80; i++) {
int index = r.nextInt(arr.length);
list.add(arr[index]);
}
// 2.如果要统计的东西比较多,不方便使用计数器思想
// 我们可以定义map集合,利用集合进行统计
HashMap<String, Integer> hm = new HashMap<>();
for (String name : list) {
// 判断当前景点在map集合当中是否存在
if (hm.containsKey(name)) {
// 存在
// 先获取当前景点已经被投票的次数
int count = hm.get(name);
// 表示当前景点又投了一次
count++;
// 把新的次数再次添加到集合当中
hm.put(name,count);
} else {
// 不存在
hm.put(name,1);
}
}
System.out.println(hm);
// 3.求最大值
int max = 0;
Set<Map.Entry<String, Integer>> entries = hm.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
int count = entry.getValue();
if (count > max) {
max = count;
}
}
System.out.println(max);
// 4.判断哪个景点的次数跟最大值一样,打印出来
for (Map.Entry<String, Integer> entry : entries) {
Integer count = entry.getValue();
if (count == max) {
System.out.println("最大值:" + entry.getKey());
}
}
}
}
5、LinkedHashMap
5.1、特点
- 由键决定:不重复、无索引、有序:保证存储和取出的元素顺序一致
- 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序
5.2、案例
public class A07_LinkedHashMapDemo3 {
public static void main(String[] args) {
// 1.创建集合
LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();
// 2.添加元素,有序指的时存取的顺序是一样的
lhm.put("a",123);
lhm.put("d",567);
lhm.put("c",456);
lhm.put("b",345);
// 3.打印集合
System.out.println(lhm); // {a=123, d=567, c=456, b=345}
}
}
6、TreeMap
6.1、特点
- TreeMap跟TreeSet底层原理一样,都是红黑树结构的
- 由键决定特性:不重复,无索引,可排序
- 注意:默认按照键从小到大进行排序,就是升序,也可以自己规定键的排序顺序
代码书写两种排序规则:
- 实现Comparable接口,指定比较规则
- 创建集合时传递Comparator比较器对象,指定比较规则
6.2、案例1
public class A01_TreeMapDemo1 {
public static void main(String[] args) {
/*
TreeMap集合:基本应用
需求1:
键:整数表示id
值:字符串表示商品名称
要求:按照id的升序排列、按照id的降序排列
*/
// 1.创建集合对象
// 默认情况下都是按照升序排列的
// String 按照字母在ASCII码表中对应的数字升序进行排列
// abcde...
TreeMap<Integer, String> tm = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 根据key倒叙排序
// o1:表示当前要添加的元素
// o2:表示已经在红黑树中存在的元素
return o2 - o1;
}
});
// 2.添加元素
tm.put(2,"可口可乐");
tm.put(4,"百事可乐");
tm.put(5,"奥利奥");
tm.put(3,"江小白");
tm.put(1,"康师傅");
// 3.打印集合
System.out.println(tm); // {5=奥利奥, 4=百事可乐, 3=江小白, 2=可口可乐, 1=康师傅}
}
}
6.3、案例2:
-
Student.java
public class Student implements Comparable<Student>{ private String name; private int age; @Override public int compareTo(Student o) { // 要求:按照学生年龄的升序排序,年龄一样按照姓名的字母排列,同姓名同年龄的视为同一人 // this:表示当前要添加的元素 // o:表示已经在红黑树中存在的元素 // 返回值: // 负数:表示当前要添加的元素是小的,存左边 // 正数:表示当前要添加的元素是大的,存右边 // 0:表示当前要添加的元素已经存在,舍弃 int i = this.getAge() - o.getAge(); i = i == 0 ? this.getName().compareTo(o.getName()) : i; return i; } ... }
-
A02_TreeMapDemo2.java
public class A02_TreeMapDemo2 { public static void main(String[] args) { /* TreeMap集合:基本应用 需求2:学生对象 值:籍贯 要求:按照学生年龄的升序排序,年龄一样按照姓名的字母排列,同姓名同年龄的视为同一人 */ // 1.创建集合 TreeMap<Student, String> tm = new TreeMap<>(); // 2.创建三个学生对象 Student s1 = new Student("zhangsan", 23); Student s2 = new Student("lisi", 24); Student s3 = new Student("wangwu", 25); // 3.添加元素 tm.put(s1, "江苏"); tm.put(s2, "天津"); tm.put(s3, "北京"); // 4.打印集合 System.out.println(tm); } }
6.4、案例3:
public class A03_TreeMapDemo3 {
public static void main(String[] args) {
/*需求:
字符串:"adbadadgadbabdadada"
请统计字符串中每个字符出现的次数,并按照以下格式输出
输出结果:
a(5)b(4)c(3)d(2)e(1)
新的统计思想:利用map集合进行统计
如果题目中没有要求对结果进行排序,默认使用HashMap,效率高
如果题目中要求对结果进行排序,使用TreeMap
键:表示要统计的内容
值:表示次数
*/
// 1.定义字符串
String s = "adbadadgadbabdadada";
// 2.创建集合
TreeMap<Character, Integer> tm = new TreeMap<>();
// 3.遍历字符串得到里面的每一个字符
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 拿着c到集合中判断是否存在
// 存在,表示当前字符又出现了一次
// 不存在,表示当前字符是第一次出现
if (tm.containsKey(c)) {
// 存在
// 先把已经出现的次数拿出来
int count = tm.get(c);
// 当前字符又出现了一次
count++;
// 把自增之后的结果再添加到集合当中
tm.put(c, count);
} else {
// 不存在
tm.put(c, 1);
}
}
// 4.遍历集合,并按照指定的格式进行拼接
// a(5)b(4)c(3)d(2)e(1)
// StringBuilder sb = new StringBuilder();
// tm.forEach((key, value) -> sb.append(key).append("(").append(value).append(")"));
StringJoiner sj = new StringJoiner("","","");
tm.forEach((key, value) -> sj.add(key + "").add("(").add(value + "").add(")"));
System.out.println(sj);
}
}
7、总结
- TreeMap添加元素的时候,键不需要重写hashCode和equals方法,压根没用到hashCode和equals方法。使用红黑树规则添加的元素,是使用Comparable来比较key是否相等,返回0表示相等,自定义对象必须实现Comparable接口。
- HashMap是哈希表结构,JDK8开始由数组,链表,红黑树组成。虽然有红黑树,但HashMap的底层是使用哈希值和equals方法来比较key是否相等,所以不需要实现Comparable接口。
- HashMap添加自定义对象的时候,需要重写hashCode和equals方法,它是根据key的hash值决定添加元素的位置,需要使用hashCode和equals方法,不然会添加重复的key。
- HashMap和TreeMap使用put添加元素时会覆盖key相同的value值,返回旧值;使用putIfAbsent方法添加元素则不会覆盖旧值。
- TreeMap和HashMap一般而言,HashMap的效率要更高。
- 如何选择
- 默认:HashMap(效率最高)
- 如果要保证存取顺序一样:LinkedHashMap
- 如果要进行排序:TreeMap
二、不可变集合
不可变集合:JDK9出现的
# list
List<String> list = List.of("a","b","c".....);
# set 里面的数据保证唯一性
Set<String> set = Set.of("a","b","c".....);
# map 最多存10个key,换句话说参数个数最多20个
Map<String,String> map = Map.of("k1","v1","k2","v2"....);
# map 不限参数的的不可变集合
HashMap<String,String> hm = new HashMap<>();
Map<Object,Object> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));
# JDK10 支持以下的不可变集合的map写法
Map<String,String> map = Map.copyOf(hm);
三、Stream流
1、作用:
结合了Lambda表达式,简化集合、数组的操作。
2、使用步骤
- 先得到一条Stream流(流水线),并把数据放上去。
- 使用中间方法对流水线上的数据进行操作。
- 使用终结方法对流水线上的数据进行操作。
3、在各个数据结构中的使用
// stream流在各个数据类型中的使用
public class StreamDemo01 {
public static void main(String[] args) {
// 单列集合
ArrayList<Object> list = new ArrayList<>();
Collections.addAll(list, 2, 3, "ssa", 4, 2);
// 输出:2 3 ssa 4 2
list.stream().forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("===========================");
// 双列集合
HashMap<Object, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 12);
map.put("gender", "man");
// 输出:gender name age
map.keySet().stream().forEach(s -> System.out.print(s + " "));
System.out.println();
// 输出:gender=man name=zhangsan age=12
map.entrySet().stream().forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("=====================");
// 数组 建议使用Arrays.stream()
int[] arr1 = {1, 2, 3, 4, 5, 6, 7};
// 输出:1 2 3 4 5 6 7
Arrays.stream(arr1).forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("===================");
String[] arr2 = {"aaa", "bbb", "ccc"};
// 输出:aaa bbb ccc
Arrays.stream(arr2).forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("===================");
// 数组如果是基本数据类型,不会自动装箱,而是打印地址值
// 输出:[I@3b9a45b3
Stream.of(arr1).forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("===================");
// 引用类型使用没有问题
// 输出:aaa bbb ccc
Stream.of(arr2).forEach(s -> System.out.print(s + " "));
System.out.println();
System.out.println("===================");
// 一堆零散数据
// 输出:1 2 3 4 5 aaa bbb
Stream.of(1, 2, 3, 4, 5, "aaa", "bbb").forEach(s-> System.out.print(s + " "));
}
}
4、stream中的方法
4.1、中间方法
4.1.1、filter
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "abc", "aa", "acb", "cc", "ad", "bbd");
// filter 过滤以a开头的,过滤长度为3的
// 输出:aaa abc acb
list.stream().filter(s->s.startsWith("a")).filter(s->s.length()==3).forEach(s-> System.out.println(s));
4.1.2、limit、skip
// limit:限制个数,skip:跳过个数
ArrayList<Object> list2 = new ArrayList<>();
Collections.addAll(list2, "1", "2", "3", "4", "5", "6", "7");
// 跳过前2个数据,限制3个数据
// 输出:3 4 5
list2.stream().skip(2).limit(3).forEach(s -> System.out.print(s + " "));
4.1.3、distinct
如果list是自定义类型,需要重写hashCode和equals方法
// distinct:去重,如果list是自定义类型,需要重写hashCode和equals方法
ArrayList<Object> list3 = new ArrayList<>();
Collections.addAll(list3, "aaa", "bbb", "aaa", "bbb", "ccc", "ddd", "aaa");
// 输出:aaa bbb ccc ddd
list3.stream().distinct().forEach(s -> System.out.print(s + " "));
4.1.4、concat
两个流合并后数据类型会变成他们共同的父类,就无法使用本类的方法了,所以合并的流数据类型最好是一致的
// concat:合并两个stream流
ArrayList<String> list4 = new ArrayList<>();
Collections.addAll(list4, "ccc", "ddd");
ArrayList<Integer> list5 = new ArrayList<>();
Collections.addAll(list5, 111, 222);
// 输出:ccc ddd 111 222
Stream.concat(list4.stream(), list5.stream()).forEach(s -> System.out.print(s + " "));
4.1.5、map
// map:转换流中的数据类型
ArrayList<String> list6 = new ArrayList<>();
Collections.addAll(list6, "ccc-13", "ddd-14");
// 输出:13 14
list6.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(s -> System.out.print(s + " "));
4.1.6、注意
-
中间方法,返回新的Stream流只能使用一次,建议链式编程
Stream<String> stream1 = list6.stream(); Stream<String> stream2 = stream1.distinct(); // 报错:stream has already been operated upon or closed // stream1只能使用一次,再次使用会报错 // stream1.distinct();
-
修改stream流中的数据,不会影响原来集合或者数组中的数据
4.2、终结方法
终结方法返回的不是Stream流
4.2.1、forEach
// forEach:遍历
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, "aaa", "bbb", "ccc");
// 输出:aaa bbb ccc
list1.stream().forEach(s -> System.out.print(s + " "));
4.2.2、count
// count:统计
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "aaa", "bbb", "ccc");
// 统计数据数量
long count = list2.stream().count();
// 输出:3
System.out.println(count);
4.2.3、toArray
// toArray:收集流中的数据,放到数组中
ArrayList<String> list3 = new ArrayList<>();
Collections.addAll(list3, "aaa", "bbb", "ccc");
// value指的是list的长度
String[] array = list3.stream().toArray(value -> new String[value]);
// 输出:[aaa, bbb, ccc]
System.out.println(Arrays.toString(array));
4.2.4、collect
// collect:收集流中的数据,放到集合中
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌-男-15","周芷若-女-21","张翠山-男-15","赵敏-女-17");
// 1.把所有男性收集到List集合中
List<String> newList = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toList());
// 输出:[张无忌-男-15, 张翠山-男-15]
System.out.println(newList);
// 2.把所有男性收集到set集合 ---》可以去重
Set<String> newSet = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toSet());
// 输出:[张翠山-男-15, 张无忌-男-15]
System.out.println(newSet);
// 3.把所有男性收集到map集合 键:姓名 值:年龄
Map<String, Integer> newMap = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(
Collectors.toMap(s -> s.split("-")[0],
s -> Integer.parseInt(s.split("-")[2])));
// 输出:{张翠山=15, 张无忌=15}
System.out.println(newMap);
四、方法引用
1、介绍
-
方法引用:把已经存在的方法拿过来用,当做函数式接口中的抽象方法的方法体。
-
方法引用符:
::
-
方法应用时的注意事项:
- 需要有函数式接口
- 被引用方法最好已经存在,这样才够简洁,不然自己又要定义一个方法。
- 被引用方法的形参和返回值需要和抽象方法保持一致
- 被引用方法的功能要满足当前的需求
-
方法引用分类:
- 引用静态方法
- 引用成员方法
- 引用其他类的成员方法
- 引用本类的成员方法
- 引用父类的成员方法
- 引用构造方法
- 其他调用方式
- 使用类名引用成员方法
- 引用数组的构造方法
2、使用类名引用静态方法
格式:类名:静态方法
范例:Integer::parseInt
// 创建集合并添加元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "111", "222", "333", "444");
// 需求:将集合中的字符串转换成整型
// 输出:111 222 333 444
list.stream().map(Integer::parseInt).forEach(s -> System.out.print(s + " "));
3、使用对象引用成员方法
格式:对象:成员方法
- 其他类:
其他类对象::方法名
- 本类:
this::方法名
- 父类:
super::方法名
注意:静态方法中没有this和super关键字
-
创建StringOperation类
public class StringOperation { public boolean stringJudge(String s) { return s.startsWith("张") && s.length() == 3; } }
-
引用其他类成员方法
// 引用成员方法 public class FunctionQuote02 { public static void main(String[] args) { // 1.创建集合 ArrayList<String> list = new ArrayList<>(); // 2. 添加数据 Collections.addAll(list, "张无忌", "周芷若", "张三丰", "张强", "赵敏"); // 3.过滤数据:只要张开头,而且名字是三个字的 list.stream().filter(new StringOperation()::stringJudge).forEach(s -> System.out.println(s)); } }
4、使用类名引用构造方法
格式:类名::new
范例:Student::new
-
编写Student类
public class Student { String name; int age; // 重写构造方法供FunctionQuote03调用 public Student(String str) { String[] splits = str.split(","); this.name = splits[0]; this.age = Integer.parseInt(splits[1]); } .... }
-
需求实现
// 引用构造方法 public class FunctionQuote03 { public static void main(String[] args) { // 1.创建集合对象 ArrayList<String> list = new ArrayList<>(); // 2.添加数据 Collections.addAll(list,"张无忌,14","张强,16","赵敏,21","周芷若,24","张三丰,56"); // 3.将list中的数据封装到Student中,并收集到List集合中 list.stream().map(Student::new).forEach(s-> System.out.println(s)); } }
5、使用类名引用成员方法
格式:类名::成员方法
范例:String::substring
方法引用规则:
- 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致。
- 其他规则与上方介绍中一致
抽象方法参数详解:
- 第一个参数:表示被引用方法的调用者,决定了可以引用那些类中的方法,在Stream流当中,第一个参数一般都表示流里面的每一个数据。假设流里面的数据是字符串,那么使用这种方式进行方法引用,只能引用String这个类中的方法。
- 第二个参数到最后一个参数:跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要的是无参的成员方法。
局限性:
- 不能引用所有类中的成员方法,跟抽象方法的第一个参数有关,这个参数是什么类型,那么就只能引用这个类中的方法。
// 使用类名引用成员方法
public class FunctionQuote04 {
public static void main(String[] args) {
// 1.创建集合
ArrayList<String> list = new ArrayList<>();
// 2.添加数据
Collections.addAll(list, "aaa", "bbb", "ccc");
// 将数据变大写后输出
// 输出:AAA BBB CCC
list.stream().map(String::toUpperCase).forEach(s-> System.out.print(s + " "));
}
}
6、引用数组的构造方法
格式:数据类型[]::new
范例:int[]::new
// 引用数组的构造方法
public class FunctionQuote05 {
public static void main(String[] args) {
// 1.创建集合并添加元素
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6);
// 2.收集到数组中
Integer[] array = list.stream().toArray(Integer[]::new);
// 输出:[1, 2, 3, 4, 5, 6]
System.out.println(Arrays.toString(array));
}
}
7、综合案例
7.1、案例一
// 综合案例一
public class FunctionQuote06 {
public static void main(String[] args) {
// 1. 创建集合并添加元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌,14", "张强,16", "赵敏,21", "周芷若,24", "张三丰,56");
// 2.先把字符串变成Student对象,然后把Student对象收集起来
Student[] array = list.stream().map(Student::new).toArray(Student[]::new);
System.out.println(Arrays.toString(array));
}
}
7.2、案例二
// 综合案例二
public class FunctionQuote07 {
public static void main(String[] args) {
// 1.创建集合
ArrayList<Student> list = new ArrayList<>();
Collections.addAll(list,
new Student("zhangsan",23),
new Student("lisi",24),
new Student("wangwu",25));
// 3.获取姓名并放到数组中
String[] array = list.stream().map(Student::getName).toArray(String[]::new);
// 输出:[zhangsan, lisi, wangwu]
System.out.println(Arrays.toString(array));
}
}
五、异常
1、介绍
-
Error:代表系统级别的异常(属于严重问题),系统一旦出现问题,sun公司会把这些错误封装成Error对象。Error是sun公司给自己用 的,不是给我们程序员用的,因此开发人员不用管它。
-
Exception:叫做异常,是异常体系的父类,代表可能出现的问题。我们通常会用Exception以及它的子类来封装程序出现的问题。Exception分为两类:
- RuntimeException:运行时异常,RuntimeException及其子类,编译阶段不会出现异常提醒。运行时出现的异常,一般是由于参数传递错误带来的问题,如:数组索引越界异常。
- 编译时异常:没有继承RuntimeException的异常,直接继承于Exception,提醒程序员检查本地信息。编译阶段就会出现异常提醒的,如:日期解析异常。
-
异常举例:
// 异常举例 public class ExceptionDemo01 { public static void main(String[] args) throws ParseException { // 编译时异常(在编译阶段,必须要手动处理,否则代码会报错) String time = "2020年1月1日"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日"); // java: 未报告的异常错误java.text.ParseException; 必须对其进行捕获或声明以便抛出 Date date = sdf.parse(time); // 输出:Wed Jan 01 00:00:00 CST 2020 System.out.println(date); // 运行时异常(在编译阶段不需要处理,是代码运行时出现的异常) int[] arr = {1, 2, 3, 4, 5}; // 数组索引越界:ArrayIndexOutOfBoundsException System.out.println(arr[10]); } }
注意:编译阶段Java不会运行代码,智慧检查语法是否错误,或者做一些性能的优化
// 性能优化
String str = "a" + "b" + "c"; --> String str = "abc";
// 检查语法
int a = 2.2; --> 语法错误,编译失败
2、异常的作用
- 异常是用来查询bug的关键参考信息。
- 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况。
3、异常处理的方式
3.1、JVM默认处理方式
- 把异常的名称、异常的原因及异常出现的位置等信息输出在控制台。
- 程序停止运行,下面的代码不会再执行了。
3.2、自己处理(捕获异常)
-
格式:
try { // 可能出现异常的代码 } catch(异常类型 变量名) { // 处理异常 } catch(异常类型 变量名) { // 多个异常捕获 }.... ...
-
目的:当代码出现异常时,可以让程序继续往下执行。
注意:
-
如果我们要捕获多个异常,这些异常中如果存在父子关系的话,那么父类一定要写在下面
// 自己捕获异常 public class ExceptionDemo02 { public static void main(String[] args) { int[] arr = {1, 2, 3, 4, 5, 6}; try { System.out.println(arr[10]); // ArrayIndexOutOfBoundsException // 由于上方出现异常,所以try里面的代码执行就到此结束了,直接跳到catch中 System.out.println(2/0); // ArithmeticException String s = null; System.out.println(s.equals("abc")); // NullPointerException } catch (ArrayIndexOutOfBoundsException e) { System.out.println("数组索引越界"); } catch (ArithmeticException e) { System.out.println("算术异常"); } catch (NullPointerException e) { System.out.println("空指针异常"); } catch (Exception e) { System.out.println("万能捕获异常"); } System.out.println("最后执行"); } }
-
在JDK7之后,我们可以在catch中同时捕获多个异常,中间用
|
进行隔开,表示如果出现A异常或者B异常,采用同一种处理方法。// 自己捕获异常 public class ExceptionDemo02 { public static void main(String[] args) { // JDK7之后的异常捕获 int[] arr = {1, 2, 3, 4, 5, 6}; try { System.out.println(arr[10]); // ArrayIndexOutOfBoundsException // 由于上方出现异常,所以try里面的代码执行就到此结束了,直接跳到catch中 System.out.println(2/0); // ArithmeticException String s = null; System.out.println(s.equals("abc")); // NullPointerException // 里面的异常不能有父子关系 } catch (ArrayIndexOutOfBoundsException | ArithmeticException | NullPointerException e) { System.out.println("数组索引越界"); } catch (Exception e) { System.out.println("万能捕获异常"); } System.out.println("最后执行"); } }
-
如果
try
中遇到异常没有被捕获,相当于try...catch
白写了,最终还是会交给JVM进行处理,程序也就到此结束了。 -
如果
try
中遇到异常,就会直接跳到catch
中执行。try
中的其他代码就不会执行了。
4、Throwable的成员方法
方法名称 | 说明 |
---|---|
public String getMessage() | 返回throwable 的详细消息字符串 |
public String toString() | 返回此可抛出简短的异常描述 |
public void printStackTrace() | 把异常的错误信息以红色字体输出在控制台 |
// Throwable的成员方法
public class ExceptionDemo03 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6};
try {
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
// getMessage()
String message = e.getMessage();
System.out.println(message);
// toString()
String str = e.toString();
System.out.println(str);
// printStackTrace
e.printStackTrace();
}
// 输出字体颜色为红色,用来打印错误信息
System.err.println("打印红色字体");
}
}
5、throw和throws
-
throw:写在方法内,手动抛出异常,交给调用者,结束方法。
public void 方法(){ throw new NullPointerException(); }
-
throws:写在方法定义处,表示声明一个异常,告诉调用者,使用本方法可能会有哪些异常。编译时异常必须要写,运行时异常可以不写。
public void 方法() throws 异常类名1,异常类名2...{ }
6、自定义异常
作用:就是为了让控制台的报错信息更加的见名知意。
6.1、步骤
- 定义异常类
- 写继承关系:编译异常继承
Exception
;运行时异常继承RuntimeException
- 空参构造
- 带参构造
6.2、例子
public class NameFormatException extends RuntimeException{
public NameFormatException() {
}
public NameFormatException(String message) {
super(message);
}
}
六、File文件操作
1、File的构造方法
- 作用:将文件路径变成一个文件对象,这样才能调用文件的方法。
- File对象表示路径,可以是文件,也可以是文件夹。这个路径可以存在,也可以不存在。
方法名称 | 说明 |
---|---|
public File(String pathName) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 根据父路径字符串和子路径字符串创建文件对象 |
public File(File parent, String child) | 根据父路径文件对象和子路径字符串创建文件对象 |
// File构造方法举例说明
public class FileDemo01 {
public static void main(String[] args) {
// 方式一
String path1 = "E:\\Desktop\\图片\\a.jpg";
File file1 = new File(path1);
// 输出:E:\Desktop\图片\a.jpg
System.out.println(file1);
// 方式二
String path2 = "E:\\Desktop\\图片";
String child2 = "a.jpg";
File file2 = new File(path2, child2);
// 输出:E:\Desktop\图片\a.jpg
System.out.println(file2);
// 方式三
String path3 = "E:\\Desktop\\图片";
File file3 = new File(path3);
// 输出:E:\Desktop\图片
System.out.println(file3);
String child4 = "a.jpg";
File file4 = new File(file3, child4);
// 输出:E:\Desktop\图片\a.jpg
System.out.println(file4);
}
}
2、File常见的成员方法
2.1、判断、获取
方法名称 | 说明 |
---|---|
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() | 返回文件的最后修改时间(时间毫秒值) |
2.2、创建、删除
方法名称 | 说明 |
---|---|
public boolean createNewFile() | 创建一个新的空文件 |
public boolean mkdir() | 创建单级文件 |
public boolean mkdirs() | 创建多级文件夹 |
public boolean delete() | 删除文件、空文件夹,直接删除,不走回收站 |
2.3、获取并遍历
public File[] listFile()
:获取当前路径下所有内容。
注意:
- 当路径不存在、路径为文件、该路径的文件夹权限不足时,返回
null
- 当该路径为空文件夹时,返回长度为0的空数组。
- 返回的内容会包含隐藏文件。
方法名称 | 说明 |
---|---|
public static File[] listRoots() | 列出可用的文件系统根 |
public String[] list | 获取当前该路径下所以内容,返回String[] |
public String[] list(FilenameFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
public File[] listFiles(FileFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
public File[] listFiles(FilenameFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
// 文件获取和遍历
public class FileDemo02 {
public static void main(String[] args) {
// 列出可用的文件系统根
File[] roots = File.listRoots();
// 输出:[C:\, D:\, E:\]
System.out.println(Arrays.toString(roots));
// 获取当前该路径下所以内容
File file = new File("E:\\Desktop\\新建文件夹");
String[] files = file.list();
// 输出:[a.txt, aaa, b.txt, bbb, ccc]
System.out.println(Arrays.toString(files));
// 列出所有
File[] files1 = file.listFiles();
// [E:\Desktop\新建文件夹\a.txt, E:\Desktop\新建文件夹\ccc, ....]
System.out.println(Arrays.toString(files1));
// 利用文件名过滤器获取当前该路径下所有内容
// list(FilenameFilter filter)
String[] list1 = file.list(new FilenameFilter() {
/**
* 例子:E:\Desktop\新建文件夹\aaa
* @param dir:父路径 ==> E:\Desktop\新建文件夹
* @param name:子文件名 ==> aaa
* @return 为true表示保留,为false表示过滤掉
*/
@Override
public boolean accept(File dir, String name) {
return true;
}
});
// 输出:[a.txt, aaa, b.txt, bbb, ccc]
System.out.println(Arrays.toString(list1));
// listFiles(FileFilter filter)
file.listFiles(new FileFilter() {
@Override
// pathname: E:\Desktop\新建文件夹\aaa E:\Desktop\新建文件夹\b.txt 。。。。
public boolean accept(File pathname) {
return false;
}
});
file.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return false;
}
});
}
}
七、IO流-字节流
用于读写文件中的数据(可以读写文件,或者网络中的数据。。。)
1、介绍
-
什么是
IO
流存储和读取数据的解决方案
I
:input
输入流,读取到内存O
:output
输出流,写入到磁盘流:向流水一样传输数据
-
IO
流的作用用于读写数据(本地文件,网络)
-
IO
流按照流向可以分为哪两种流输出流:程序(内存)===》 文件(磁盘)
输入流:文件(磁盘)===》 程序(内存)
-
IO
流按照操作文件的类型可以分为哪两种流字节流:可以操作所有类型的文件
字符流:只能操作纯文本文件
-
什么是纯文本文件
使用Windows系统自带的记事本打开,并且能读懂的文件
txt
文件,md
文件,xml
文件,lrc
文件等
2、字节流
2.1、FileOutputStream
2.1.1、介绍
操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。
书写步骤:
- 创建字节输出流对象
- 写数据
- 释放资源
注意:
字节输出流的细节:
1.创建字节输出流对象
细节1:参数是字符串表示的路径或者File对象都是可以的
细节2:如果文件不存在,会创建一个新文件,但是要保证父级路径是存在的
细节3:如果文件已经存在,则会清空文件
2.写数据
write方法的参数是整数时,实际上写到本地文件中的是整数在ASCII上对应的字符
3.释放资源
每次使用完流之后都要释放资源,如果不释放会一直占用这个资源
// FileOutputStream
public class IODemo01 {
public static void main(String[] args) throws IOException {
/*
字节输出流的细节:
1.创建字节输出流对象
细节1:参数是字符串表示的路径或者File对象都是可以的
细节2:如果文件不存在,会创建一个新文件,但是要保证父级路径是存在的
细节3:如果文件已经存在,则会清空文件
2.写数据
write方法的参数是整数时,实际上写到本地文件中的是整数在ASCII上对应的字符
3.释放资源
每次使用完流之后都要释放资源,如果不释放会一直占用这个资源
*/
// 1. 创建对象
FileOutputStream fos = new FileOutputStream("mymap\\a.txt");
// 2.写出数据
fos.write(97);
// 3.释放资源
fos.close();
}
}
2.1.2、FileOutputStream写数据的3种方式
方法名称 | 说明 |
---|---|
void write(int b) | 一次写一个字节数据 |
void write(byte[] b) | 一次写一个字节数组数据 |
void write(byte[] b, int off, int len) | 一次写一个字节数组的部分数据,off 是起始索引,len 是长度 |
// FileOutputStream写入数据的3种方式
public class IODemo02 {
public static void main(String[] args) throws IOException {
// 1.创建对象
FileOutputStream fos = new FileOutputStream("./a.txt");
// 2.写出数据
// 方式1
fos.write(97); // a
// 方式2
byte[] bytes = {97, 98, 99, 100, 101};
fos.write(bytes); // abcde
// 方式3
fos.write(bytes,1,2); // bc
// 关闭资源
fos.close();
}
}
2.1.3、换行和续写
// FileOutputStream 换行和续写
public class IODemo03 {
public static void main(String[] args) throws IOException {
/*
换行写:
再次写出一个换行符就可以了
windows:\r\n
Linux:\n
Mac:\r
细节:
在windows操作系统中,java对回车键进行了优化。
虽然完整的是\r\n,但是我们写其中一个\r或者\n
java也可以实现换行,java在底层会补全
建议:
不要省略,还是写全了
续写:
如果想要续写,打开续写开关即可
开关默认位置:在创建FileOutputStream对象的第二个参数
默认为false:表示关闭续写,此时创建对象会清空文件
手动传递true:表示打开续写,此时创建对象不会清空文件
*/
// 1.创建FileOutputStream对象
FileOutputStream fos = new FileOutputStream("./a.txt", true);
// 2.写出数据
String str = "abcdefg";
byte[] bytes1 = str.getBytes();
fos.write(bytes1);
// 写入一个换行符
String wrap = "\r\n";
byte[] bytes2 = wrap.getBytes();
fos.write(bytes2);
// 再次写入
String str2 = "666";
byte[] bytes3 = str2.getBytes();
fos.write(bytes3);
// 关闭资源
fos.close();
}
}
2.2、FileInputStream
2.2.1、介绍
操作本地文件的字节输入流,可以把本地文件读取到程序中来。
书写步骤:
- 创建字节输入流对象
- 写数据
- 释放资源
注意:
FileInputStream书写细节
1.创建字节输入流对象
如果文件不存在,就直接报错
2.读取数据
细节1:一次读取一个字节,读出来的数据会转换成ASCII上的对应的数字
细节2:文件读完时,read方法返回-1
3.释放资源
每次使用完流必须释放资源
// FileInputStream
public class IODemo04 {
public static void main(String[] args) throws IOException {
// 1.创建输入流对象
FileInputStream fis = new FileInputStream("./a.txt");
// 读取数据:abc
int read1 = fis.read();
System.out.println(read1);// 97
int read2 = fis.read();
// 转成字符
System.out.println((char) read2);// b
int read3 = fis.read();
System.out.println(read3);// 99
int read4 = fis.read();
System.out.println(read4);// -1
// 关闭资源
fis.close();
}
}
2.2.2、FileInputStream的循环读取
// FileInputStream
public class IODemo05 {
public static void main(String[] args) throws IOException {
// 创建FileInputStream对象
FileInputStream fis = new FileInputStream("./a.txt");
int b;
// 循环读取
/**
* fis.read() 表示读取数据,而且是读取一个数据就移动一次指针
*/
while ((b = fis.read()) != -1){
System.out.print((char) b + " ");
}
// 释放资源
fis.close();
}
}
2.3.3、拷贝文件案例
// 文件拷贝
public class IODemo06 {
public static void main(String[] args) throws IOException {
// 1.创建对象
FileInputStream fis = new FileInputStream("./a.txt");
FileOutputStream fos = new FileOutputStream("./b.txt");
// 放读取到的内容
int b;
// 2.拷贝:边读边写
while ((b = fis.read()) != -1) {
fos.write(b);
}
// 3.关闭资源:先开的最后关闭
fos.close();
fis.close();
}
}
2.3.4、一次读取多个字节
// 读取读数据:一次读取多个字节数据,和数组长度有关
public class IODemo07 {
public static void main(String[] args) throws IOException {
// 创建对象
FileInputStream fis = new FileInputStream("./a.txt");
// 放读取数据的长度
int len;
// 放读取数据的内容
byte[] bytes = new byte[2];
// 开始循环读取
while ((len = fis.read(bytes)) != -1) {
String str = new String(bytes,0,len);
System.out.print(str);
}
// 关闭资源
fis.close();
}
}
注意:一次读一个字节数组的数据,每次读取会尽可能把数组装满。
2.3.5、文件拷贝改写
// 文件拷贝改写:一次读取一个字节数组
public class IODemo08 {
public static void main(String[] args) throws IOException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建对象
FileInputStream fis = new FileInputStream("./a.txt");
FileOutputStream fos = new FileOutputStream("./b.txt");
// 存放读取数据的长度
int len;
// 存放读取数据的内容
byte[] buffer = new byte[1024];
// 开始循环读取,循环写入
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer,0,len);
}
// 关闭资源
fis.close();
fos.close();
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("执行了" + (end - start) + "ms");
}
}
2.3.6、异常处理
// 异常处理
public class IODemo09 {
public static void main(String[] args){
// 记录开始时间
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 创建对象
fis = new FileInputStream("./a.txt");
fos = new FileOutputStream("./b.txt");
// 存放读取数据的长度
int len;
// 存放读取数据的内容
byte[] buffer = new byte[1024];
// 开始循环读取,循环写入
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("执行了" + (end - start) + "ms");
}
}
八、字符集详解
在计算机中,任意数据都是以二进制的形式来存储的。
字节:计算机最小的存储单元,1字节等于8个bit 00111000
。ASCII只有128个,但是一个字节可以有256种可能
1、字符集介绍
- GB2312字符集 :1980年发布,1981年5月1日实施的简体中文汉字编码国家标准。收录了7445个图形字符,其中包括6763个简体汉字。
- BIG5字符集 :台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施。
- GBK字符集 : 2000年3月17日发布,收录21003个汉字。包含国家标准GB13000-1种的全部中日韩汉字和BIG5编码中的所有汉字。Windows系统默认使用的就是GBK,系统显示ANSI。
- Unicode字符集 : 国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。
2、中英文编码规则(GBK)
GBK字符集完全兼容ASCII字符集
- 一个英文占一个字节,二进制第一位是0
- 一个中文占两个字节,二进制高位字节的第一位是1
2.1、中文
2.2、英文
3、中英文编码规则(Unicode)
Unicode
字符集的UTF-8
编码格式 UTF
(Unicode Transfer Format
)
- 一个英文占一个字节,二进制第一位是0,转成十进制是正数。
- 一个中文占三个字节,二进制第一位是1,第一个字节转成十进制是负数。
3.1、中文
3.1、英文
4、为什么乱码
-
使用字节流读取文本文件,每次读取一个字节,但是汉字不止一个字节,所以会乱码,但拷贝文件却不会,因为拷贝文件是将每一个字节以二进制方式写入文件,当你打开文件时,才会自动解码。所有只要编码方式和记事本的解码方式一致就不会乱码。
-
编码和解码方式不统一
5、Java中编码和解码的方法
// Java中编码和解码方式的实现
public class CharSetDemo01 {
public static void main(String[] args) throws UnsupportedEncodingException {
/*
Java中编码的方法
public byte[] getBytes() 使用默认方式进行编码
public byte[] getBytes(String charsetName) 使用指定方式进行编码
Java中的解码方法
String(byte[] bytes) 使用默认方式进行解码
String(byte[] bytes, String charsetName) 使用指定方式进行解码
*/
// 1.编码
String str = "ab大c鱼d";
byte[] bytes1 = str.getBytes();
// 输出:[97, 98, -27, -92, -89, 99, -23, -79, -68, 100]
System.out.println(Arrays.toString(bytes1));
byte[] bytes2 = str.getBytes("GBK");
// 输出:[97, 98, -76, -13, 99, -45, -29, 100]
System.out.println(Arrays.toString(bytes2));
// 2.解码
String str2 = new String(bytes1);
// 输出:ab大c鱼d
System.out.println(str2);
String str3 = new String(bytes1,"GBK");
// 输出:ab澶楸糳
System.out.println(str3);
}
}
九、IO流-字符流
1、介绍
字符流的底层其实就是字节流
字符流 = 字节流 + 字符集
特点:
- 输入流:一次读一个字节,遇到中文时,一次读多个字节。
- 输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中。
使用场景:对于纯文本文件进行读写操作。
2、字符流
2.1、FileReader
2.1.1、FileReader无参read读取
// FileReader read() 空参
public class CharSetDemo02 {
public static void main(String[] args) throws IOException {
/*
第一步:创建对象
public FileReader(File file) 创建字符输入流关联本地文件
public FileReader(String pathName) 创建字符输入流关联本地文件
第二步:读取数据
public int read() 读取数据,读到末尾返回-1
public int read(char[] buffer) 读取多个数据,读到文件末尾返回-1
第三步:释放资源
public void close() 释放资源/关流
*/
// 1.创建对象并关联本地文件
FileReader fr = new FileReader(new File("./a.txt"));
// 2.读取数据 read()
// 字符流底层也是字节流,默认也是一个字节一个字节的读取
// 如果遇到中文就会一次读取多个,GBK一次读取两个字节,UTF-8一次读取三个字节
/*
read() 细节:
1.read():默认也是一个字节一个字节的读取,如果遇到中文就会一次读取多个
2.在读取之后,方法的底层还会进行解码并转成十进制。最终把这个十进制作为返回值。
这个十进制的数据业表示在字符集上的数字
英文:文件里面的二进制数据 0110 0001
read方法进行读取,解码并转成十进制97
中文:文件里面的二进制数据 11100110 10110001 10001001
read方法进行读取,解码并转成十进制27721
如果项看到中文汉字,就是把这些十进制数据,再进行强转就可以了
*/
// 存放读取到的数据
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
// 3.释放资源
fr.close();
}
}
2.1.2、FileReader有参read读取
// FileReader read(chars) 有参
public class CharSetDemo03 {
public static void main(String[] args) throws IOException {
// 1.创建对象并关联本地文件
FileReader fr = new FileReader(new File("./a.txt"));
// 存放读取数据的长度
int len;
// 存放读取到的数据
char[] chars = new char[2];
// 2.读取数据
// fr.read(chars):读取数据,解码,强转三步合并了,把强转之后的字符放到数组当作了
while ((len = fr.read(chars)) != -1) {
System.out.print(new String(chars,0,len));
}
// 3.释放资源
fr.close();
}
}
2.1.3、字符输入流底层原理
- 创建字符输入流对象
- 底层:关联文件,并创建缓冲区(长度为8192的字节数组)
- 读取数据
- 底层:判断缓冲区中是否有数据可以读取
- 缓冲区中没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区,如果文件中也没有数据了就返回-1
- 缓冲区有数据:就从缓冲区读取
- 空参的read方法:一次读取一个字节,遇到中文一次读取多个字节,把字节解码并转成十进制返回
- 有参的read方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中
- 底层:判断缓冲区中是否有数据可以读取
2.2、FileWriter
2.2.1、书写细节
-
创建字符输出流对象
- 参数是字符串表示的路径或者File对象都可以
- 如果文件不存在会创建一个新文件,但是要保证父级路径是存在的
- 如果文件已经存在,则会清空文件,如果不想清空可以打开续写开关
-
写数据
- 如果
writer
方法的参数是整数,但是实际上写到本地文件中的是整数在字符集上对应的字符
- 如果
-
释放资源
// FileWriter
public class CharSetDemo04 {
public static void main(String[] args) throws IOException {
/*
第一步:创建对象
public FileWriter(File file) 创建字符输出流关联本地文件
public FileWriter(String pathName) 创建字符输出流关联本地文件
public FileWriter(File file,boolean append) 创建字符输出流关联本地文件,续写开关
public FileWriter(String pathName,boolean append) 创建字符输出流关联本地文件,续写开关
第二步:读取文件
void write(int c) 写出一个字符
void write(String str) 写出一个字符串
void write(String str,int off,int len) 写出一个字符串的一部分
void write(char[] cbuf) 写出一个字符数组
void write(char[] cbuf,int off,int len) 写出一个字符的一部分
第三步:释放资源
public void close() 释放资源/关流
*/
// 1.创建对象
FileWriter fw = new FileWriter("./a.txt", true);
// 2.写入数据
fw.write(25105);
// 2.写入数据
char[] chars = {'a', 'b', 'c', '我'};
fw.write(chars);
// 3.释放资源
fw.close();
}
}
2.2.2、字符输出流底层原理
fw.write(chars)
会将数据放到缓冲区(长度为8192的字节数组),当出现以下三种情况时,才会将数据写入到文件中
- 情况一:缓冲区装满了,数据到了8193个字节
- 情况二:手动执行
fw.flush()
,将缓冲区中的数据写到到文件中 - 情况三:释放资源时
fw.close()
,也会将缓冲区里面的数据写到文件中
fw.flush()
和fw.close()
的区别在于前者还可以写数据,后者无法再写数据,通道已经关闭。
十、IO-缓冲流
1、字节缓冲流
// 字符缓冲流
public class IOBufferDemo01 {
public static void main(String[] args) throws IOException {
/*
* 需求:
* 利用字节缓冲流拷贝文件
*
* 字节缓冲输入流的构造方法:
* public BufferedInputStream(InputStream is)
*
* 字节缓存输出流的构造方法:
* public BufferedInputStream(OutputStream os)
**/
// 1.创建缓冲流对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./b.txt"));
// 方式一:
// bis.read() 读取数据到BufferedInputStream的缓冲区
// bos.write(b) 一个字节一个字节的将数据放到BufferedOutputStream的缓冲区
/*int b; // 存放读取到的数据
while ((b = bis.read()) != -1) {
bos.write(b);
}*/
// 方式二:
// bis.read(bytes) 读取数据到BufferedInputStream的缓冲区
// bos.write(bytes, 0, len) 将一个bytes的字节数组的数据放到BufferedOutputStream的缓冲区
int len; // 存放读取到数据的长度
byte[] bytes = new byte[1024]; // 存放读取到的数据
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
// 3.释放资源
bos.close();
bis.close();
}
}
2、字符缓冲流
// 字符缓冲流
public class IOBufferDemo02 {
public static void main(String[] args) throws IOException {
/**
* 字符缓冲输入流:
* 构造方法:
* public BufferedReader(Reader r)
* 特有方法:
* public String readLine() 读一整行
*
* 字符缓冲输出流:
* 构造方法:
* public BufferedWriter(Writer r)
* 特有方法:
* public void newLine() 跨平台换行
*
*/
// 1.创建字符缓冲输入流对象
BufferedReader br = new BufferedReader(new FileReader("./a.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("./b.txt"));
// 2.读取数据:readLine一次读一整行,遇到回车换行结束,但是不会把换行读到内存中
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 换行
}
// 3.释放资源
bw.close();
br.close();
}
}
注意:br.readLine()
返回值是String
,当读完是时返回null
十一、IO-转换流
1、转换流继承体系
2、转换流原理和作用
- 由于
FileInputStream
按字节读取时,读到中文时乱码,但是FileInputStream
按byte[] bytes = new byte[3]
三个字节读取时不会乱码,在UTF-8
编码中,中文就是三个字节表示的。 - 转换流就是将文件中的数据按照字节读取,这样不管说中文还是英文都是一样的,然后指定字符集,进行解码,那么内存中的数据就是按字符存在,然后又指定想要的编码,将数据按指定的字符集编码成字节写入文件。
InputStreamReader
和OutputStreamWriter
JDK11的时候就淘汰了,现在使用FileReader
和FileWriter
。
3、示例
// 转换流 JDK11
public class IOBufferDemo03 {
public static void main(String[] args) throws IOException {
// 指定读取字符集
FileReader fr = new FileReader("./a.txt", Charset.forName("GBK"));
// 指定写入文件的字符集
FileWriter fw = new FileWriter("./b.txt", Charset.forName("UTF-8"));
int b;
while ((b = fr.read()) != -1) {
fw.write(b);
}
fw.close();
}
}
十二、IO-序列化流和反序列化流
创建一个Student的JavaBean,并且实现Serializable的接口
// Serializable接口里面没有抽象方法,是标记型接口
// 一旦实现类这个接口,那么就表示当前的Student类可以被序列化
public class Student implements Serializable {
private String name;
private int age;
......
}
1、序列化流 ObjectOutputStream
// 序列化流
public class IOObjectDemo01 {
public static void main(String[] args) throws IOException {
/*
需求:
利用序列化流/对象操作输出流,把一个对象写到本地文件中
构造方法:
public ObjectOutputStream(OutputStream out) 把基本流变成高级流
成员方法:
public final void writeObject(Object obj) 把对象序列化(写出)到文件中
*/
// 1.创建对象
Student student = new Student("zhangsan", 23);
// 2.创建序列化流的对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./c.txt"));
// 3.写出数据
oos.writeObject(student);
// 4.释放资源
oos.close();
}
}
注意:使用对象输出流将对象保存到文件时会出现NotSerializableException
异常,需要让JavaBean类实现Serializable
接口
2、反序列化流 ObjectOutputStream
// 序列化流
public class IOObjectDemo01 {
public static void main(String[] args) throws IOException {
/*
需求:
利用序列化流/对象操作输出流,把一个对象写到本地文件中
构造方法:
public ObjectOutputStream(OutputStream out) 把基本流变成高级流
成员方法:
public final void writeObject(Object obj) 把对象序列化(写出)到文件中
*/
// 1.创建对象
Student student = new Student("zhangsan", 23);
// 2.创建序列化流的对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./c.txt"));
// 3.写出数据
oos.writeObject(student);
// 4.释放资源
oos.close();
}
}
3、总结
- 使用对象输出流将对象保存到文件时会出现
NotSerializableException
异常,需要让JavaBean类实现Serializable
接口 - 序列化流写到文件中的数据是不能修改的,一旦修改就无法在次读回来。
- 序列化写到文件中后修改了JavaBean,再次反序列化,会抛出
InvalidClassException
异常。解决方法:给JavaBean类添加serialVersionUID
(序列号,版本号)。 - 如果一个对象中的某个成员变量不想被序列化,可以给该成员变量加
transient
关键字修饰,该关键字标记的成员变量不参与序列化过程。
// Serializable接口里面没有抽象方法,是标记型接口
// 一旦实现类这个接口,那么就表示当前的Student类可以被序列化
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// transient 瞬态关键字
// 作用:序列化时忽略该字段
private transient String address;
.....
}
十三、多线程
1、概念
- 进程:程序的基本执行实体。
- 线程:操作系统能够进行运算调度的最小单位。他被包含在进程之中,是进程中的实际运作单位。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
- 并行:在同一时刻,有多个指令在多个CPU上同时执行,CPU有多个核心。
2、多线程的实现方式
2.1、继承Thread类的方式进行实现
// 多线程的第一种启动方式 Thread
public class ThreadDemo01 {
public static void main(String[] args) {
/*
多线程的第一种启动方式:
1.自己定义一个类继承Thread
2.重写run方法
3.创建子类的对象,并启动线程
*/
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 设置线程的名字
t1.setName("线程1");
t2.setName("线程2");
// 启动线程
t1.start();
t2.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "执行了");
}
}
}
2.2、实现Runnable接口的方式进行实现
// 多线程的第二种启动方式 Runnable
public class ThreadDemo02 {
public static void main(String[] args) {
/*
多线程的第二种启动方式:
1.自己定义一个类实现Runnable接口
2.重写里面的run方法
3.创建自己的类的对象
4.创建一个Thread类的对象,并开启线程
*/
// 创建MyRunnable的对象,表示多线程要执行的任务
MyRunnable mr = new MyRunnable();
// 创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程设置名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
// 书写线程要执行的代码
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行了");
}
}
}
2.3、利用Callable接口和Future接口方式实现
// 多线程的第三种实现方式 Callable
public class ThreadDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
多线程的第三种实现方式:
特点:可以获取到多线程运行的结果
1.创建一个类MyCallable实现Callable接口
2.重写call(是有返回值的,表示多线程运行的结果)
3.创建MyCallable的对象(表示多线程要执行的任务)
4.创建FutureTask的对象(用来管理多线程运行的结果)
5.创建Thread类的对象,并启动(表示线程)
*/
// 创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
// 创建FutureTask的对象(用来管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建Thread类的对象
Thread t1 = new Thread(ft);
// 启动线程
t1.start();
// 获取多线程运行结果
Integer result1 = ft.get();
System.out.println(result1);
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 求1~100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
2.4、多线程三种实现方式对比
优点 | 缺点 | |
---|---|---|
继承Thread类 | 编程简单,可以直接使用Thread类中的方法 | 可扩展性较差,继承了它,就不能再继承别的类了 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类,还可以获取线程的返回值 | 编程相对复杂,不能直接使用Thread类中的方法 |
3、线程的优先级
线程的优先级默认都是5,优先级越高,调用的概率就大。
// 线程的优先级
public class ThreadDemo04 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
// 设置线程优先级,默认为5,优先级越高,优先调用,也是概率问题,都不一定,main函数的优先级默认也是5
myThread1.setPriority(4);
// 获取线程的优先级
int priority = myThread1.getPriority();
System.out.println(priority); // 4
}
}
class MyThread1 extends Thread {
@Override
public void run() {
System.out.println(getName() + "执行了");
}
}
4、守护线程
守护线程:当非守护线程结束时,会通知守护线程结束,这个通知过程也存在时间,会有延迟
// 设置守护线程
public class ThreadDemo05 {
public static void main(String[] args) {
/**
* 守护线程:当非守护线程结束时,会通知守护线程结束,这个通知过程也存在时间
*
*/
MyThread2 myThread2 = new MyThread2();
MyThread3 myThread3 = new MyThread3();
// 设置myThread2为守护线程
myThread2.setDaemon(true);
myThread2.setName("myThread2");
myThread3.setName("myThread3");
myThread2.start();
myThread3.start();
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
class MyThread3 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "执行了" + "@" + i);
}
}
}
5、礼让线程(了解) yield
礼让线程:Thread.yield();
表示把当前cup的执行权礼让出去,但还是会抢夺。就是看概率。
// 礼让线程 yield
public class ThreadDemo06 {
public static void main(String[] args) {
MyThread4 mt1 = new MyThread4();
MyThread5 mt2 = new MyThread5();
mt1.setName("飞机");
mt2.setName("坦克");
mt1.start();
mt2.start();
}
}
class MyThread4 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
Thread.yield();
}
// 礼让线程:表示把当前cpu的执行权给出去
}
}
class MyThread5 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
6、插入线程(了解) join
插入线程:使用了join
的线程会优先执行,join
要放在start
后面执行完成后才会执行没有使用join
的线程
// 插入线程/插队线程 join
public class ThreadDemo07 {
public static void main(String[] args) throws InterruptedException {
MyThread06 mt = new MyThread06();
mt.setName("土豆");
mt.start();
// 表示把mt这个线程插入到当前main函数线程的前面,它执行完了才会轮到main函数
mt.join();
// 接着执行main函数线程里面的代码
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "@" + i);
}
}
}
class MyThread06 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
7、线程的生命周期
8、同步代码块 synchronized
同步代码块:把操作共享数据的代码锁起来,锁默认是打开的,当有一个线程进去了,锁就会自动关闭,线程出来了,锁才会打开。
// 同步代码块 synchronized
public class ThreadDemo08 {
public static void main(String[] args) {
/**
* 模拟三个窗口买票
*/
MyThread7 mt1 = new MyThread7();
MyThread7 mt2 = new MyThread7();
MyThread7 mt3 = new MyThread7();
mt1.setName("窗口1");
mt2.setName("窗口2");
mt3.setName("窗口3");
mt1.start();
mt2.start();
mt3.start();
}
}
class MyThread7 extends Thread {
// static 表示这个类的所有对象都共享这个变量
static int ticket = 0;
//锁对象,一定要唯一
// static Object object = new Object();
@Override
public void run() {
while (true) {
// 同步代码块,参数是锁对象,必须唯一
synchronized (ThreadDemo08.class) {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
}
}
}
}
9、同步方法
同步方法就是把synchronized
关键字加到方法上
特点:
- 同步方法是锁住方法里面所有的代码
- 锁对象不能自己指定
- 非静态方法,锁住的是
this
- 静态方法,锁住的是当前类是字节码对象
- 非静态方法,锁住的是
// 同步方法
public class ThreadDemo09 {
public static void main(String[] args) {
MyRunnable09 mr = new MyRunnable09();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class MyRunnable09 implements Runnable {
int ticket=0;
@Override
public void run() {
while (true){
if (method()) return;
}
}
private synchronized boolean method() {
// 同步代码块(同步方法)
synchronized (MyRunnable09.class){
if (ticket == 100){
return true;
}else {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName()+"正在卖"+ticket+"张票");
}
}
return false;
}
}
拓展:
StringBuilder
线程不安全,方法没有加synchronized
- **
StringBuffer
线程安全,加了synchronized
**
10、Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
- Lock提供了获取锁和释放锁的方法,手动上锁,手动释放锁
void lock()
获得锁viod unlock()
释放锁
- Lock是接口,不能直接实例化,这里采用它的实现类
ReentrantLock
来实例化ReentrantLock
的构造方法 ReentrantLock()
:创建一个ReentrantLock
实例
// Lock
public class ThreadDemo10 {
public static void main(String[] args) {
MyRunnable09 mr = new MyRunnable09();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class MyRunnable10 implements Runnable {
int ticket = 0;
// 要加static,表示不管new多少个MyRunnable10类lock始终只有一个
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket == 100) {
break;
} else {
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName() + "" + ticket + "");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
11、生产者和消费者(等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作模式。
方法名称 | 说明 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify() | 随机唤醒单个线程 |
void notifyAll() | 唤醒所有线程 |
// 消费者和生产者
public class ThreadDemo11 extends Thread {
public static void main(String[] args) {
// 创建线程对象
Cook cook = new Cook();
Foodie foodie = new Foodie();
cook.start();
foodie.start();
}
}
// 桌子--线程的锁
class Desk {
/**
* 控制生产者和消费者执行的那把锁
*/
// 是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
// 总共吃多少碗
public static int count = 10;
// 锁对象
public static Object lock = new Object();
}
// 消费者--吃货
class Foodie extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
// 先判断桌子上是否有面条
if (Desk.foodFlag == 0) {
// 如果没有,就等待
try {
Desk.lock.wait(); // 让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 把吃的碗数-1
Desk.count--;
// 如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
// 吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
// 生产者--厨师
class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count == 0){
break;
}else {
if (Desk.foodFlag == 1){
// 如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 如果没有,就制作食物
System.out.println("厨师做了一碗面条");
// 修改桌子上的食物状态
Desk.foodFlag = 1;
// 叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
12、等待唤醒机制(阻塞队列方式实现)
// 阻塞队列实现等待唤醒机制
public class ThreadDemo12 {
public static void main(String[] args) {
/**
* 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
* 细节:
* 1.生产者和消费者必须使用同一个阻塞队列
* 2.take和put里面是有锁的,但是外面的代码没有锁,因此外部的打印语句不会按顺序执行
*/
// 1.创建阻塞队列的对象,并设置队列长度
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 2. 创建线程对象,并把阻塞队列传递过去
Cook2 cook2 = new Cook2(queue);
Foodie2 foodie2 = new Foodie2(queue);
cook2.start();
foodie2.start();
}
}
class Foodie2 extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie2(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断从阻塞队列中取出面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Cook2 extends Thread{
ArrayBlockingQueue<String> queue;
public Cook2(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断从阻塞队列中取出面条
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
13、多线程的六种状态
14、线程池
1、线程池主要核心原理
- 创建一个池子,池子中是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有线程即可。
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。
2、线程池代码实现
Executors
:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
-
自定义线程执行的任务
// 自定义线程执行的任务 public class MyRunnable implements Runnable { @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + "---" + i); } } }
-
newCachedThreadPool
:创建一个没有上限的线程池// 创建一个没有上限的线程池 public class PoolThread01 { public static void main(String[] args) throws InterruptedException { /** * 创建一个没有上限的线程池 * public static ExecutorService newCachedThreadPool(); */ // 1. 获取线程池对象 ExecutorService pool = Executors.newCachedThreadPool(); // 线程休眠 // Thread.sleep(1000); // 2.提交多个线程任务 pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); // 3.销毁线程池 pool.shutdown(); } }
-
newFixedThreadPool(int nThreads)
:创建有上限的线程池// 创建一个没有上限的线程池 public class PoolThread02 { public static void main(String[] args) throws InterruptedException { /** * 创建一个指定线程数的线程池 * public static ExecutorService newFixedThreadPool(int nThreads) */ // 1. 获取线程池对象 ExecutorService pool = Executors.newFixedThreadPool(3); // 线程休眠 // Thread.sleep(1000); // 2.提交多个线程任务 pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); // 3.销毁线程池 pool.shutdown(); } }
3、自定义线程池
比较灵活,可以自定义参数
// 自定义线程池
public class PoolThread03 {
public static void main(String[] args) {
/*
ThreadPoolExecutor pool = new ThreadPoolExecutor
(核心线程数,最大线程数,空闲线程最大存活时间,时间单位,任务队列,创建线程工厂,任务拒绝策略);
参数一:核心线程数 不能小于0
参数二:最大线程数 不能小于0 最大线程数=核心线程数+临时线程数
参数三: 不能小于0
参数四:时间单位 用TimeUnit指定
参数五:任务队列 不能为null
参数六:创建线程工厂 不能为null
参数七:任务拒绝策略 不能为null
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数
6, // 最大线程数
60, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 任务队列
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);
// 提交任务
// pool.submit();
}
}
任务拒绝策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认策略:丢弃任务并抛出RejectedExecutionException 异常 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但不抛出异常,这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务(队列先进去的肯定排的久),然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 调用入的run()方法绕过线程池直接执行 |
不断提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队。
- 当核心线程满、队伍满时,会创建临时线程。
- 当核心线程满、队伍满、临时线程满时,会触发任务拒绝策略。
4、线程池设置多大合适
查看电脑线程数
// 查看电脑线程数
public class PoolThread04 {
public static void main(String[] args) {
// 向Java虚拟机返回可用处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);
}
}
十四、网络编程
- 网络编程就是计算机跟计算机之间通过网络进行数据传输。
- Java中可以使用
java.net
包下的技术轻松开发出常见的应用程序。
1、创建软件架构
2、网络编程三要素
- IP:设备再网络中的地址,是唯一标识。
- 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、http、https、ftp。
- 端口号:应用程序在设备中唯一的标识。
3、IP
3.1、IPV4
3.2、IPV6
3.3、IP地址的分类形式
- 公网地址(万维网使用)和私有地址(局域网使用)
192.168.
开头的就是私有地址,范围即为192.168.0.0~192.168.255.255
,专门为组织机构内部使用,以此节省IP。
可以利用局域网IP解决IPV4不够的问题。
特殊IP地址:
127.0.0.1
也可以是localhost
:是回送地址,也称本地回环地址,也称本机IP,永远只会寻找当前所在本机。
InetAddress类的使用
// InetAddress类的使用
public class SocketDemo {
public static void main(String[] args) throws UnknownHostException {
/**
* public static InetAddress getByName(String host) 确认主机名称的IP地址。host可以是电脑名,也可以是IP地址
* public String getHostName() 获取此IP的主机名
* public String getHostAddress() 返回文本显示中的IP地址字符串
*/
// InetAddress address = InetAddress.getByName("localhost");
InetAddress address = InetAddress.getByName("10.61.159.153");
System.out.println(address); // /10.61.159.153
// 获取计算机名
// String hostName = address.getHostName();
// System.out.println(hostName);
// 获取IP
String ip = address.getHostAddress(); // 可能会获取不到
System.out.println(ip); // 10.61.159.153
}
}
4、端口号
- 应用程序在设备中唯一的标识
- 端口号:由两个字节标识的整数,取值范围:
0~65535
,其中0~1023
之间的端口号用于一些知名的网络服务或者应用。 - 一个端口号只能被一个应用程序使用
5、协议
计算机网络中,连接和通信的规则被称为网络通信协议。
- OSI参考模型(七层):世界互联协议标准,全球通信规范,单模型过于理想化,未能在因特网上进行广泛推广
- TCP/IP参考模型(或TCP/IP协议):事实上的国际化标准
5.1、UDP协议
- 用户数据报协议(User Datagram Protocol)
- UDP是面向无连接的通信协议。速度快,有大小限制一次最多发送64k,数据不安全,易丢失数据。
- 使用场景:在线视频、语音通话、网络会议
5.1.1、发送数据
// UDP发送数据
public class SocketDemo02 {
public static void main(String[] args) throws IOException {
// 1.创建 DatagramSocket 对象(快递公司)
// DatagramSocket空参:所有可用的端口中随机挑选一个使用
// DatagramSocket有参:指定端口
DatagramSocket ds = new DatagramSocket();
// 数据
String str = "你怎么样!!";
// 将数据转成字节
byte[] bytes = str.getBytes();
// 发送到哪台主机
InetAddress address = InetAddress.getByName("127.0.0.1");
// 目标端口
int port = 10086;
// 2.打包数据
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// 3.发送数据
ds.send(dp);
// 4.释放资源
ds.close();
}
}
5.1.2、接收数据
// UDP接收数据
public class SocketDemo03 {
public static void main(String[] args) throws IOException {
// 1.创建 DatagramSocket 对象(快递公司)
// 在接收时一定要绑定端口,而且要和发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);
// 2.接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
// 该方法是阻塞的,程序执行到这一步,会在这里死等,直到发送端发送数据
ds.receive(dp);
// 3.解析数据包
byte[] data = dp.getData();
int len = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();
System.out.println("接收到数据:" + new String(bytes,0,len));
System.out.println("该数据是从:" + address + "这台电脑中的" + port + "这个端口发出去的");
// 4.释放资源
ds.close();
}
}
5.1.3、聊天室
// UDP发送数据--聊天室
public class SocketDemo04 {
public static void main(String[] args) throws IOException {
// 1.创建 DatagramSocket 对象(快递公司)
// DatagramSocket空参:所有可用的端口中随机挑选一个使用
// DatagramSocket有参:指定端口
DatagramSocket ds = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入你要说的话:");
// 数据
String str = sc.nextLine();
if ("886".equals(str)){
break;
}
// 将数据转成字节
byte[] bytes = str.getBytes();
// 发送到哪台主机
InetAddress address = InetAddress.getByName("127.0.0.1");
// 目标端口
int port = 10086;
// 2.打包数据
DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
// 3.发送数据
ds.send(dp);
}
// 4.释放资源
ds.close();
}
}
// UDP接收数据--聊天室
public class SocketDemo05 {
public static void main(String[] args) throws IOException {
// 1.创建 DatagramSocket 对象(快递公司)
// 在接收时一定要绑定端口,而且要和发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);
// 2.接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
while (true) {
// 该方法是阻塞的,程序执行到这一步,会在这里死等,直到发送端发送数据
ds.receive(dp);
// 3.解析数据包
byte[] data = dp.getData();
int len = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();
System.out.println("ip为:" + ip + ",主机名为:" + name + "的人,发送了数据:" + new String(data,0,len));
}
}
}
5.1.4、UDP的三种通信方式
- 单播:一对一
- 组播:组播地址:
224.0.0.0~239.255.255.255
,其中224.0.0.0~224.0.0.255
为预留的组播地址 - 广播:广播地址:
255.255.255.255
实现(组播):
// 组播端发送数据
public class Broadcast01 {
public static void main(String[] args) throws IOException {
// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket();
// 创建DatagramPacket对象
String s = "你好,你好!!!";
byte[] bytes = s.getBytes();
// 指定组播地址
InetAddress address = InetAddress.getByName("224.0.0.1");
int port = 10000;
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);
// 调用MulticastSocket发送数据
ms.send(datagramPacket);
// 释放资源
ms.close();
}
}
// 组播端接收数据-1
public class Broadcast02 {
public static void main(String[] args) throws IOException {
// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);
// 将本机添加到224.0.0.1这一组播IP中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);
// 创建DatagramPacket数据包对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
// 接收数据
ms.receive(dp);
// 解析数据
byte[] data = dp.getData();
int len = dp.getLength();
String ip = dp.getAddress().getHostAddress();
// String name = dp.getAddress().getHostName();
// System.out.println("IP为:" + ip + ",主机名为:" + name + ",发送了数据:" + new String(data,0,len));
System.out.println("IP为:" + ip + ",发送了数据:" + new String(data,0,len));
}
}
注意:
- 组播接收端可用搞多个
- udp接收数据端,在获取主机名时好像会丢包,接收不到,多发送几次就可以了。
- 广播:将单播的IP改为
255.255.255.255
就行了
5.2、TCP协议
- 传输控制协议(Transmission Control Protocol)
- TCP协议是面向连接的通信协议。没有大小限制,数据安全
- 使用场景:文字消息、邮件、下载软件
TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,通信之前要保证连接已经建立,通过Socket产生IO流来进行网络通信。
5.2.1、发送数据
// TCP协议发送数据--输出流--客户端
public class Client {
public static void main(String[] args) throws IOException {
// 1. 创建Socket对象
// 细节:在创建对象的同时会连接服务端,如果连接不上,代码会报错
Socket socket = new Socket("127.0.0.1", 10000);
// 2.可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
// 3.写出数据
os.write("你好,你好".getBytes());
// 4.释放资源
socket.close();
}
}
5.2.2、接收数据
// TCP协议发送数据--输入流--服务端
public class Server {
public static void main(String[] args) throws IOException {
// 1.创建对象ServerSocket
ServerSocket ss = new ServerSocket(10000);
// 2.监听客户端的连接
Socket socket = ss.accept();
// 3.从连接通道中获取输入流
InputStream is = socket.getInputStream(); // 字节流,一个字节的读取
// 解决中文乱码
InputStreamReader isr = new InputStreamReader(is); // 转换流,将字节流转换成字符流
BufferedReader br = new BufferedReader(isr); //缓冲流,读的快
int b;
while ((b = br.read()) != -1) {
System.out.print((char) b);
}
// 4.释放资源
socket.close();
ss.close();
}
}
注意:发送端写出结束标记,接收端才会结束接收。
// 结束标记
socket.shutdownOutput();
十五、反射
反射允许对成员变量、成员方法和构造方法的信息进行编程访问。
反射的作用:
- 获取一个类里面所有的信息,获取到之后再执行其他的业务逻辑。
- 结合配置文件,动态的创建对象并调用方法。
1、获取class对象的三种方式
// 获取class对象的三种方式
public class Reflect01 {
public static void main(String[] args) throws ClassNotFoundException {
/**
* 获取class对象的三种方式
* 1.Class.forName("全类名");
* 2.类名.class
* 3.对象.getClass();
*/
// 1.第一种方式:最为常用
// 全类名:包名+类名
Class clazz1 = Class.forName("com.gxm.reflect.Student");
// 第二种方式:一般更多的是当作参数进行传递,比如 synchronized (Student.class)
Class clazz2 = Student.class;
// 第三种方式:当我们已经有了这个对象时,才可以使用
Student student = new Student();
Class clazz3 = student.getClass();
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
}
}
2、利用反射获取构造方法
Constructor<?>[] getConstructors() | 返回所有公共的构造方法的数组 |
---|---|
Constructor<?>[] getDeclaredConstructors() | 返回所有构造方法对象的数组 |
Constructor<?>[] getConstructors(Class<?>...parameterTypes) | 返回单个公共构造方法对象 |
Constructor<?>[] getDeclaredConstructor(Class<?>...parameterTypes) | 返回单个构造方法对象 |
// 通过反射获取构造方法
public class Reflect02 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 1.获取class字节码文件对象
Class clazz = Class.forName("com.gxm.reflect.Student");
// 2.获取构造方法
// 2.1 获取所有公共的构造方法
Constructor[] cons1 = clazz.getConstructors();
// 2.2 获取所有构造方法
Constructor[] cons2 = clazz.getDeclaredConstructors();
// 2.3 获取单个构造方法
Constructor cons3 = clazz.getDeclaredConstructor();
Constructor cons4 = clazz.getDeclaredConstructor(String.class);
Constructor cons5 = clazz.getDeclaredConstructor(int.class);
Constructor cons6 = clazz.getDeclaredConstructor(String.class,int.class);
// 3.获取构造方法的修饰符---以整数形式来表现
int modifiers = cons6.getModifiers();
System.out.println(modifiers); // 2
// 4.获取构造方法的所有参数
Parameter[] parameters = cons4.getParameters();
// 5.利用反射获取的构造方法创建对象
// 临时取消权限校验,这样私有的构造方法也能被调用
cons4.setAccessible(true);
Student stu = (Student) cons4.newInstance("张三", 23);
}
}
3、利用反射获取成员变量
Field[] getFields() | 返回所有公共成员变量对象的数组 |
---|---|
Field[] getDeclaredFields() | 返回所有成员变量对象的数组 |
Field[] getField(String name) | 返回单个公共成员变量对象 |
Field[] getDeclaredField(String name) | 返回单个成员变量对象 |
// 通过反射获取成员变量
public class Reflect03 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
// 1.获取字节码对象
Class clazz = Class.forName("com.gxm.reflect.Student");
// 2.获取成员变量
// 2.1获取所有公共成员变量
Field[] fields1 = clazz.getFields();
// 2.2获取所有成员变量
Field[] fields2 = clazz.getDeclaredFields();
// 2.3获取单个公共成员变量
Field fields3 = clazz.getField("gender");
System.out.println(fields3);
// 2.4获取单个成员变量
Field fields4 = clazz.getDeclaredField("name");
System.out.println(fields4);
// 3.获取权限修饰符
int modifiers = fields4.getModifiers();
// 4.获取成员变量的名字
String name = fields4.getName();
// 5.获取成员变量的数据类型
Class<?> type = fields4.getType();
// 6.获取成员变量记录的值
Student student = new Student("zhangsan", 23, "男");
// 临时取消权限校验
fields4.setAccessible(true);
// 获取值
String value = (String) fields4.get(student);
// 7.修改成员变量的值
fields4.set(student,"lisi");
}
}
4、利用反射获取成员方法
Method[] getMethods() | 返回所有公共成员方法的对象的数组,包括继承的 |
---|---|
Method[] getDeclaredMethods() | 返回所有成员方法对象的数组,不包括继承的 |
Method[] getMethod() | 返回单个公共成员方法对象 |
Method[] getDeclaredMethod() | 返回单个成员方法对象 |
// 利用反射获取成员方法
public class Reflect04 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 1.获取字节码对象
Class<?> clazz = Class.forName("com.gxm.reflect.Student");
// 2.获取成员方法
// 2.1获取所有公共的成员方法,包括继承的
Method[] methods1 = clazz.getMethods();
// 2.2获取所有的成员方法,包括私有的,但不包括继承的
Method[] methods2 = clazz.getDeclaredMethods();
// 2.3获取单个公共的成员方法
Method sleep = clazz.getMethod("sleep");
// 2.4获取单个成员方法
Method eat1 = clazz.getDeclaredMethod("eat", String.class);
Method eat2 = clazz.getDeclaredMethod("eat", String.class,int.class);
// 3.获取方法的修饰符
int modifiers = eat1.getModifiers();
// 4.获取方法的名字
String name = eat1.getName();
// 5.获取方法的形参
Parameter[] parameters = eat1.getParameters();
// 5.获取方法抛出的异常
Class<?>[] exceptionTypes = eat1.getExceptionTypes();
// 6.运行方法
/**
* Object invoke(Object obj,Object...args):运行方法
* 参数一:用obj对象调用该方法
* 参数二:调用方法的传递的参数(如果没有就不写)
* 返回值:方法的返回值(如果没有就不写)
*/
Student student = new Student();
eat1.setAccessible(true);
eat1.invoke(student,"汉堡包");
}
}
十六、动态代理
为什么需要代理?
- 代理可以无侵入式的给对象增强其他的功能。
代理长什么样子?
- 代理李米娜就是对象要被代理的方法
Java通过什么来保证代理的样子?
- 通过接口保证,后面的对象和代理许哟啊实现同一个接口,接口中就是被代理的所有方法
如何为Java对象创建一个代理对象?
-
java.lang.reflect.Proxy
类提供了为对象产生代理对象的方法: -
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
- 参数一:由于指定用哪个类加载器去加载生成的代理类
- 参数二:指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法
- 参数三:用来指定生成的代理对象要干什么事情
案例
-
创建一个接口
public interface Star { // 我们可以把所有想要被代理的方法定义在接口当中 // 唱歌 public abstract String sing(String name); // 跳舞 public abstract void dance(); }
-
创建一个大明星实体类,并实现这个接口
public class BigStar implements Star{ private String name; public BigStar() { } public BigStar(String name) { this.name = name; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } public String toString() { return "BigStar{name = " + name + "}"; } @Override public String sing(String name) { System.out.println(this.name + "正在唱" + name); return "谢谢"; } @Override public void dance() { System.out.println(this.name + "正在跳舞"); } }
-
创建一个代理
// 创建一个代理 public class ProxyUtil { /** * * @param bigStar 被代理的明星对象 * @return 给明星创建的代理 */ public static Star createProxy(BigStar bigStar){ /** * 参数一:由于指定用哪个类加载器去加载生成的代理类 * 参数二:指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法 * 参数三:用来指定生成的代理对象要干什么事情 */ Star star = (Star) Proxy.newProxyInstance( ProxyUtil.class.getClassLoader(), //由于指定用哪个类加载器去加载生成的代理类 new Class[]{Star.class}, //指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法 new InvocationHandler() { //用来指定生成的代理对象要干什么事情 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * 参数一:代理的对象 * 参数二:要运行的方法 sing * 参数三:调用sing方法时,传递的实参 */ if ("sing".equals(method.getName())){ System.out.println("准备话筒,收钱"); } else if ("dance".equals(method.getName())){ System.out.println("准备场地,收钱"); } // 调用大明星里面的方法 return method.invoke(bigStar,args); } } ); return star; } }
-
测试
// 测试 public class Test { public static void main(String[] args) { // 1.获取代理的对象 BigStar bigStar = new BigStar("鸡哥"); Star proxy = ProxyUtil.createProxy(bigStar); // 2.调用唱歌的方法 String sing = proxy.sing("好说好说"); System.out.println(sing); // 3.调用跳舞的方法 proxy.dance(); } }