进阶篇

end0fsky / 2024-10-15 / 原文

目录
  • 36,37,38 双列集合,可变参数,Collections
    • Map
      • 遍历方式
        • 键找值
        • 键值对
        • Lambda表达式
      • HahMap
        • 使用put()方法添加元素存储的原理
          • 覆盖原理
          • 比较
        • 效率
        • 特殊方法
        • LinkedHashMap
      • HashTable
        • Properties
      • TreeMap
        • 排序方式
        • 特殊方法
    • 可变参数(Args)
    • Collections工具类
    • 一些其他的知识
      • 利用Ctrl + F12或Alt + 7时,部分方法或成员变量后面可能存在的文字含义
      • 成员变量与局部变量的效率
      • 对于java编写的类中,一些成员变量与返回值用于判断时,设计的情况
      • 位运算
  • 39 面向对象设计
  • 40,41 不可变集合,Stream流,方法引用
    • 不可变集合
      • 注意事项
    • Stream流
      • 中间方法
      • 终结方法
    • 方法引用
      • 引用静态方法
      • 引用成员方法
      • 引用构造方法
      • 其他调用方式
        • 类名引用成员方法
        • 引用数组的构造方法
  • 42异常
    • 编译时期异常
    • 运行时期异常
    • 作用
    • 查看
    • 处理方式
      • JVM默认的处理方式
      • 自己处理(捕获异常)
      • 抛出处理
        • throws
        • throw
    • 使用
      • 常见方法
      • 自定义异常
  • 43 File
    • 创建File文件对象
    • 常用方法
  • 44 IO流—基本流,字符集
    • 字节流
      • OutputStream
        • FileOutputStream
          • 书写步骤
          • 写入数据的方式
          • 换行与续写
          • 加密与解密
      • InputStream
        • FileInputStream
          • 书写步骤
          • 循环读取
          • 多字节读取
          • bom头
          • 易错
      • 文件拷贝
      • 捕获异常完整
        • 简化方案
    • 字符流
      • Reader
        • FileReader
          • 带参读取
          • 指定编码格式
          • 高级
      • Writer
        • FileWriter
          • 多字节写入数据
          • 续写
          • 刷新
          • 指定编码格式
      • 原理
        • 输入流原理
        • 输出流原理
    • 字符集
        • GBK
        • Unicode
        • 乱码原因
        • 编码
        • 解码
  • 45 IO流—高级流
    • 缓冲流
      • 字节缓冲流
        • 多字节读取与写入
        • 原理
      • 字符缓冲流
        • 两个方法
    • 转换流
      • InputStreamReader
      • OutputStreamWriter
    • 序列化流
      • 序列化流
      • 反序列化流
      • 细节
      • 多对象序列化与反序列化
    • 打印流
      • 字节打印流
      • 字符打印流
      • 占位符
      • 与System.out的关系
    • 解压缩流
    • 压缩流
    • Commons-io
      • 使用方式
    • Hutool工具包
  • 46,47 综合练习
    • 制造假数据
      • 网络爬取
    • 带权重随机算法
    • 登入注册
    • Properties配置文件
  • 48,49 多线程
    • 实现方式
      • 继承Thread类的方式实现
      • 实现Runnable接口的方式实现
      • 利用Callable接口和Future接口方式实现
    • 利用匿名内部类的方式实现
    • 成员方法
      • 线程优先级
      • 守护线程
      • 礼让线程
      • 插入线程
    • 线程的生命周期
    • 线程安全问题
      • 同步代码块
      • 同步方法
      • StringBuffer
      • Lock锁
      • 死锁
    • 等待唤醒机制
      • 利用阻塞队列方式实现
    • 多线程的六种运行状态
    • 线程栈
    • 线程池
      • 自定义线程池
      • 最大并行数
      • 线程池最大上限
    • JUC
  • 50,51 网络编程
    • 网络编程三要素
      • IP
        • IPv4
          • 地址分类
          • 特殊IP地址
          • cmd之ping
        • IPv6
        • InetAddress
      • 端口号
      • 协议
        • UDP协议
        • TCP协议
    • 利用UDP协议发送数据
      • UDP的三种通信方式
        • 单播
        • 组播
        • 广播
    • 利用TCP协议发送数据
      • 三次握手与四次挥手协议
        • 三次握手
        • 四次挥手
      • UUID
  • 52 反射
    • 获取class对象
    • 获取构造方法
      • 获取构造函数的信息
    • 获取字段
      • 获取字段的信息
    • 获取成员方法
      • 获取成员方法的信息

36,37,38 双列集合,可变参数,Collections

一次添加一对元素,如商品名与价格,其中商品名为,价格为

一对键与值称为键值对键值对对象Entry对象

特点

~一次存一对数据

~键不可重复,值可重复

~键和值一一对应

Map

双列集合中最顶级的接口

实现类有HashMap,HashTable,TreeMap

Map<String,String> m = new HashMap();
// put(K key,V value) 添加元素
m.put("key","value");
m.put("name", "age");
m.put("gender","sex");
// 添加相同的键时,该键对应的值会被替换,被替换的值会被返回
// 添加的键是不存在的话,返回null
String value = m.put("gender", "id");

System.out.println(value);// sex
System.out.println(m);// {gender=id, name=age, key=value}

// remove(Object key) 根据键删除键值对元素
// 返回被删除的值
String s = m.remove("gender");

System.out.println(s);// id
System.out.println(m);// {name=age, key=value}

// containsKey(Object key) 判断是否包含键
boolean flag1 = m.containsKey("123");
boolean flag2 = m.containsKey("key");

System.out.println(flag1);// false
System.out.println(flag2);// true

// containsValue(Object value) 判断是否包含值
boolean flag3 = m.containsValue("value");
boolean flag4 = m.containsValue("va");

System.out.println(flag3);// true
System.out.println(flag4);// false

// isEmpty() 判断是否为空
boolean result = m.isEmpty();

System.out.println(s);// false

// size() 获取Entry对象个数
int len = m.size();

System.out.println(len);// 2

// clear() 移除所有Entry对象
m.clear();

System.out.println(m);// {}

遍历方式

键找值
// 通过keySet()获取所有的键构成的集合
// 然后对该集合进行遍历
// 再通过get(K key)获取值
Map<String,String> m = new HashMap<>();

m.put("key","value");
m.put("subject","math");
m.put("sport","football");

// 通过键找值

Set<String> keys = m.keySet();

for (String key : keys) {
    String value = m.get(key);
    System.out.println(key + " = " + value);
}
键值对
// 通过entrySet获取键值对集合
// 然后对集合遍历
Map<String,String> m = new HashMap<>();

m.put("标枪选手","马超");
m.put("人物挂件","明世隐");
m.put("御龙骑士","尹志平");

// 通过键值对遍历
// Entry是Map类的内部接口
Set<Map.Entry<String, String>> entries = m.entrySet();

for (Map.Entry<String, String> entry : entries) {
    // 获得单独的key与value值
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + " = " + value);
    // 或直接打印
    System.out.println(entry);
}
Lambda表达式
// 通过Map的forEach方法进行遍历
// 底层利用 第二种方法 与 增强for 进行遍历
Map<String,String> map = new HashMap<>();

map.put("鲁迅","这句话一定是我说的");
map.put("曹操","不可能,绝对不可能");
map.put("刘备","接着奏乐接着舞");
map.put("柯镇恶","看我眼色行事");

// 利用Lambda表达式进行遍历

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));

HahMap

特点

~是Map的实现类

~特点都由键决定:无序,不重复,无索引

~数据结构:哈希表,与HashSet相似

使用put()方法添加元素存储的原理

利用计算出键的哈希值与值无关,避免哈希值的重复,所以特点都由键决定

在创建出的table数组中存储数据时,若对应索引已经存在元素,就会用equals方法比较键,与值无关

~如果键一样,覆盖

~如果键不一样,JDK8以前,新元素添加数组,老元素挂链表;JDK8以后,新元素挂链表

除键相同时覆盖(根据使用的方法决定)外,基本与HashSet相似

覆盖原理

覆盖是指将Entry对象的值覆盖,而不是将其或其对应的节点的地址值给覆盖

如 0x0011 A 123 ==> 0x0011 A 456

比较

HashMap的键不需要实现Comparable接口或传递比较器的,原因:红黑树利用的是键值对对象的哈希值的大小进行判断的

效率

一般而言HashMapde效率更高,除非所有元素都挂在一个链表中

只要利用公式计算出其在数组中的位置即可,若是挂在该位置上,还需再查找一下

特殊方法

使用putIfAbsent()方法进行不覆盖的添加元素

LinkedHashMap

继承HashMap

有序,不重复,无索引

与LinkedHashSet机制相似,添加了一个双链表,用以存储存入顺序

HashTable

IO学

Properties

继承HashTable

IO学

TreeMap

与红黑树结构相似,数据类型:红黑树

可排序:对键排序

默认按从小到大的顺序排序,也可以自定义

排序方式

实现Comparable方法

创建集合时,传递Comparator比较器对象

TreeMap中的键不需要重写HashCode()与equals()方法

特殊方法

使用putIfAbsent()方法,不覆盖的添加数据

可变参数(Args)

JDK5提出

public static void main(String[] args) {
    getSum();
    
    getSum(1,2,3,4,5,6,7,8,9,10);
}
// 格式:数据类型... 参数名
public static int getSum(int... arr){
    // 获取的arr为数组
    // java创建的数组,会将所有的参数给该数组,并将其地址值赋给arr
    int sum = 0;
    
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    
    return sum;
}
// 注意:在方法的形参中最多只能定义一个可变参数,且必须定义在最后面
void test1(int...i,int... args){} // 报错
void test2(int... args,int i){} // 报错
void test3(int i,int... args){}

也可以传入对应类型的数组,但传入了数组后。这个可变参数就不再接收其他数据了,再传递数据会报错

Collections工具类

不是集合,是工具类

Arrays是数组的工具类,Collections是集合的工具类

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

// addAll(Collection<T> c,T... elements) 批量添加
Collections.addAll(list,"abc","bcd","qwe","123","456","qwer","df");

// shuffle(List<?> list) list) 打乱顺序
Collections.shuffle(list);

// sort(List<T> list) 排序
Collections.sort(list);

// sort(List<T> list,Comparator<T> c) 按照指定顺序排序
Collections.sort(list, (o1, o2) -> {
        for (int i = 0;i < Math.min(o1.length(),o2.length());i++){
            char c1 = o1.charAt(i);
            char c2 = o2.charAt(i);
            if (c1 - c2 != 0){
                return c1 - c2;
            }
        }
        return o1.length() - o2.length();
    }
);

// binarySearch(List<T> list,T key) 以二分法查找数据
// 返回索引值
// 利用String的compareTo()方法进行比较
// 要求:有序集合为升序(与compareTo方法的排序相同),使用这个查找方法时,推荐使用自然排序
int i = Collections.binarySearch(list, "456");
System.out.println(i);

// copy(List<T> dest,List<T> src) 拷贝集合中的元素
// 浅拷贝
// 要求dest的size要大于等于src
ArrayList<String> list1 = new ArrayList<>();
// 为了使list1的size等于list
Collections.addAll(list1,"111","222","333","444","555","666","777");
Collections.copy(list1,list);

// fill(List<T> list,T obj) 使指定的元素填充集合
Collections.fill(list,"111");

// max/min(Collection<T> coll) 获取最大/小值
// 利用元素的compareTo方法进行获取
// 要求:必须实现Comparable接口
// 返回元素,不是索引
System.out.println(Collections.min(list));

// swap(List<T> list,int i,int j) 交货指定索引的元素
Collections.swap(list,0,1);

System.out.println(list);

一些其他的知识

利用Ctrl + F12或Alt + 7时,部分方法或成员变量后面可能存在的文字含义

↑AbstractMap:重写的父类AbstractMap中的方法

↑Map:重写了Map接口的方法

→Map:是继承了Map中的方法

成员变量与局部变量的效率

成员变量保存在中,方法在栈中,每次调用成员变量时,都会去堆中找,效率低

因此,在方法中定义局部变量,最后在把局部变量成员变量,以提高效率

对于java编写的类中,一些成员变量与返回值用于判断时,设计的情况

boolean类型的变量,一般控制两面情况

如TreeMap中的replaceOld变量,用于控制添加键值对时,遇到相同的键值对是否覆盖

int类型的变量,一般控制至少三种情况

如Comparator比较器中的方法,返回值为int类型,小于0,等于0,大于0对应的操作都不同

位运算

  • 当x = 2的n次方时,使用

    (x - 1) & y 时,等效于 y % x

  • 当使用

    x & 1时,等效于 x % 2,用以判断是否为偶数

39 面向对象设计

先确认对象所具有的特点,然后依照其特点编写对象所具有的属性与方法

40,41 不可变集合,Stream流,方法引用

不可变集合

不能被修改的集合,长度与内容均不可改

好处:被不信任的库调用时,不可变形式更安全

:斗地主中的牌盒,保证牌盒中的牌的数量与种类不变,确保公平性

List,Set,Map接口中,都存在静态的of方法,用以获取不可变集合,形参为可变参数

但是返回的是List,Set,Map类型,即含有add与put等方法,但使用会报错,运行时报错

List<String> list = List.of("张三","李四","王五","赵六");

Set<String> set = Set.of("张三","李四","王五","赵六");

// Map的of方法中最多传十对键值对,第一个元素与第二个元素是一对键值对,第一个为键
// 依次类推,最多十对键值对
Map<String, String> map = Map.of("张三", "南京", "李四", "北京");

当获取不可变的Set集合时,一定要保证of()方法中的元素唯一性,否则会报错,运行时报错

获取不可变的Map时,传入的键要唯一,不能重复,运行时报错

当要传入的键值对超过十个,使用ofEntries()方法

Set<Map.Entry<String, String>> entries = hm.entrySet();

// 将Set变为数组,使用了指定类型的toArray()方法
// new Map.Entry[0]是为了让java知道要转化为什么类型
// toArray方法在底层会比较集合与传入的数组的长度
// 若集合长度更大,此时会根据集合长度来创建同类型的数组
// 若数组的长度大于等于集合,会直接用传入的数组
Map.Entry[] arr = entries.toArray(new Map.Entry[0]);

Map m = Map.ofEntries(arr);

或者使用copyof()

JDK10出现

Map<String, String> immuMap = Map.copyOf(hm);
// copyOf()底层调用了ofEntries()方法

注意事项

若存储的为基本数据类型,不可改变

若存储的是引用数据类型,不可修改地址值,可以修改内部的值

Stream流

:可当作流水线,在流水线中会对集合进行一系列的操作,获取符合所有条件的元素

可使用链式编程,结合Lambda表达式,从而简化集合,数组的操作

首先先获取一条Stream流水线,通过其中的方法进行操作

Stream的方法分为中间方法与终结方法,中间方法用以过滤数据,终结方法用以获取数据

中间方法返回的一般还是Stream流

Stream要指定泛型

事例:

// 打印长度为三,开头为张的元素
list1.stream().filter(name -> name.startsWith("张")).filter(name -> name.length() == 3).forEach(name -> System.out.println(name));

filter()方法中传入的是lambda表达式,返回值是boolean,用以获取符合条件的元素,并对其进行其他操作

通过以下方法获取Stream流

// 1.
// 单列集合 stream() 属于Collection的默认方法
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"a","b","c","d","e");

Stream<String> stream1 = list.stream();
stream1.forEach(s -> System.out.println(s));

// 2.
// 双列集合 无 无法直接使用stream流
// 通过keySet()与entrySet()方法获取单列集合后再获取stream流
HashMap<String,Integer> hm = new HashMap<>();
hm.put("aaa",111);
hm.put("bbb",222);
hm.put("ccc",333);
hm.put("ddd",444);

hm.keySet().stream().forEach(s -> System.out.println(s));

hm.entrySet().stream().forEach(e -> System.out.println(e));

// 3.
// 数组 Arrays.stream(T[] array) 利用Arrays的静态方法
int[] arr = {1,2,3,4,5,6,7,8,9,10};
IntStream stream2 = Arrays.stream(arr);

stream2.forEach(i -> System.out.println(i));

// 4.
// 零散的数据 Stream.of(T... values) 利用Stream的静态方法
// 要求:传入的数据是同种的
Stream.of(1,2,3,4,5).forEach(i -> System.out.println(i));
// 该方法也可以传入数组
Stream.of(arr).forEach(i -> System.out.println(i));
// 但是获取的若是基本数据类型的数组,打印的结果就是arr的地址值
// 原因:把整个数组当作一个元素,放入到stream流中

中间方法

中间方法会返回新的Stream流,可以使用链式编程

修改Stream流中的数据,不会影响原集合或数组的元素

对一个流使用后,就不能再对该流操作了

Stream<String> stream1 = list.stream();
Stream<String> stream2 = stream1.filter(s -> s.startsWith("张"));
// 再次对使用过的流操作会报错
Stream<String> stream3 = stream1.filter(s -> s.startsWith("张"));

常用中间方法

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张无忌","张无忌","周芷若","赵敏","张强","张丰三","张翠山","张良","王二麻子","谢广坤");

// filter() 过滤不满足条件的元素
// 如果返回值为true,保留,否则过滤
list.stream().filter(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.startsWith("张");
    }
}).forEach(s -> System.out.println(s));

list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));

// limit() 获取前几个元素
list.stream().limit(3).forEach(s -> System.out.println(s));

// skip() 跳过前几个元素
list.stream().skip(4).forEach(s -> System.out.println(s));

// distinct() 元素去重,依赖于hashCode()与equals()方法
// 利用hashSet去重
list.stream().distinct().forEach(s -> System.out.println(s));

// concat(Stream a,Stream b) 合并a,b为一个流
// 如果数据类型不一致,合并后的数据为两个数据类型的共同父类
ArrayList<String> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list1,"张无忌","周芷若","赵敏","张强","张丰三","张翠山","张良","王二麻子","谢广坤");
Collections.addAll(list2,"周芷若","赵敏");

Stream.concat(list1.stream(),list2.stream()).forEach(s -> System.out.println(s));
// 打印了lsit1与list2中的所有数据
System.out.println("===============================");
// map() 转换流中的数据类型
// 也可以再对元素进行操作时使用,相当于遍历,但并不会终结
ArrayList<String> list3 = new ArrayList<>();
Collections.addAll(list3,"张无忌-15","周芷若-14","赵敏-13","张强-20","张丰三-100");
// 只获取里面的年龄并打印
// 第一个数据类型为流中原本的数据类型,第二个为要转换的数据类型
// 返回值为返回后的数据
// split会从满足条件的位置切开,并将两边的数据存入数组,但不包含满足条件的字符
list3.stream().map(new Function<String, Integer>() {
    @Override
    public Integer apply(String s) {
        String[] arr = s.split("-");
        return Integer.parseInt(arr[1]);
    }
}).forEach(i -> System.out.println(i));

list3.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(i -> System.out.println(i));

终结方法

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌-男-15","周芷若-女-14","赵敏-女-13","张强-男-20","张丰三-男-100");

// forEach() 遍历
// s为流中的每一个数据
list.stream().forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

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

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

// toArray() 收集流中的数据,放到数组中
// 空参返回Object数组
Object[] arr1 = list.stream().toArray();
// 泛型:具体类型的数组,不能为基本数据类型
// 形参:流中的数据个数,要跟数组长度保持一致
// 返回值:具体的数据类型
// 方法体:创建数组
// toArray()的参数就是为了创建一个数组,在其底层会将流中的元素放到返回的数组中
// 最后再将该数组返回
String[] arr2 = list.stream().toArray(new IntFunction<String[]>() {
    @Override
    public String[] apply(int value) {
        return new String[value];
    }
});

String[] arr3 = list.stream().toArray(value -> new String[value]);

// collect() 收集流中的数据,放到集合中
// equals()方法前的数据尽量为固定数据,以防方法前的数据为null,从而报错
// 收集到List
List<String> newList = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toList());
// 收集到Set,会去重
// 创建的HashSet
Set<String> set = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toSet());
// 收集到Map
// 要决定谁是键,谁是值
// toMap()两个参数,第一个:键的生成规则,第二个:值的生成规则
// 泛型1:流中数据类型,泛型2:键/值的数据类型
// 同map()方法的参数
// 当存入的键值的键已经存在,会报错
Map<String, Integer> map1 = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toMap(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.split("-")[0];
            }
        }, new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return Integer.parseInt(s.split("-")[2]);
            }
        }));
// Lambda表达式
Map<String, Integer> map2 = list.stream()
        .filter(s -> "男".equals(s.split("-")[1]))
        .collect(Collectors.toMap(
                s -> s.split("-")[0], 
                s -> Integer.parseInt(s.split("-")[2])));

方法引用

把已经有的方法拿过来用,当作函数式接口中的抽象方法的方法体

要求

~引用处必须是函数式接口

~被引用方法的形参与返回值须跟抽象方法一致

~被引用方法必须存在

~被引用的方法要能满足当前的需求

如:

Integer[] arr = {3, 5, 4, 1, 6, 2};
// 匿名内部类
Arrays.sort(arr, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});

// Lambda表达式
Arrays.sort(arr, (o1, o2) -> o2 - o1);

// 方法引用
Arrays.sort(arr,FunctionDemo01::subtraction);

其中 :: 是方法引用符

引用静态方法

格式:类型::静态方法

list.stream().map(Integer::parseInt).forEach(s -> System.out.println(s));

引用成员方法

格式:对象::成员方法

本类:this,引用出不能是静态方法

父类:super,引用处不能是静态方法

list.stream()
    .filter(new StringOperation()::stringAdapt)
    .forEach(s -> System.out.println(s));

本类

jButton.addActionListener(this::click);

父类

jButton.addActionListener(super::click);

引用构造方法

格式:类名::new

用以创建对象

引用构造方法时,不用管返回值,构造方法运行完后,就会创建一个新的对象了,这个低语宣告就会被返回了

List<Student> students = list.stream()
    .map(Student::new)
    .collect(Collectors.toList());

class Student{i
    String name;
    int age;
    
    public Student(String s){
        String[] arr = s.split(",");
        name = arr[0];
        age = Integer.parseInt(arr[1]);
    }
}

其他调用方式

类名引用成员方法

格式:类名::成员方法

要求

~函数式接口

~存在

~被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值一致

~满足需求

这里的抽象方法是函数式接口中被重写的抽象方法

抽象方法的形参

~第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法(通过第一个参数调用其成员方法),再Stream流中,第一个参数一般表示流中的每一个数据,只能引用第一个参数的类中的成员方法,静态方法不玩这种形式调用的

~其他参数:与抽象方法的第二个形参到最后一个形参保持一致,若没第二个形参,被引用方法需无参

list.stream().map(String::toUpperCase);
// 没有参数,但又返回值的成员方法
s.toUpperCase();

局限性:只能引用抽象方法的第一个参数对应的类中的方法

引用数组的构造方法

格式:数据类型[]::new

再stream流中使用toArray方法时使用,以指定数组的数据类型

长度与流中的数据个数相同

Integer[] arr = list.stream().toArray(Integer[]::new);

42异常

不是为了不出现异常,而是出现异常后,怎么处理

java.lang.Throwable的子类为ErrorException

Error是系统级别的错误,是sun公司自己用的,不许要管

Exception是异常,是异常体系的最上层父类

RuntimeExceptionExeption的子类,指运行时出现的异常,运行时异常都继承与它

其他异常是编译时出现的异常,也是Exception的子类,编译时异常直接继承与Exception

编译时期异常

除了RuntimeException及其子类,继承于Exception

必须手动处理,否则无法运行,idea会提示报错

如SimpleDateTime在将字符串转换为Date对象时,会抛出ParseException,需进行处理才能运行

编译期间只会检查语法是否错误与性能的优化

用于提醒程序员检查本地信息

运行时期异常

RuntimeException及其子类

idea不会提示,只会在运行时报错

如索引越界异常ArrayIndexOutOfBoundsException,算术异常ArithmeticException

不是用于提醒,而是代码出错

作用

1.用来查询bug的关键参考信息

2.异常可以作为方法内部的一种特殊返回值,用以告知调用者底层执行情况

查看

查看在哪出错时,要从下往上看

处理方式

JVM默认的处理方式

异常名称,异常原因及异常出现的位置等信息输出在控制台,并停止程序

自己处理(捕获异常)

目的:当代码出现异常时不会停止虚拟机,能继续往下执行

/*
* 格式
* try{
*   可能出现异常的代码;
* } catch(异常类名 变量名) {
*   处理异常的代码;
* }
* */

try{
    System.out.println(1 / 0);
}catch (ArithmeticException e){
    System.out.println("算术异常");
}

在执行会出现异常的代码时,若出现异常,则会创建一个异常类型对象,并停止执行try中的代码,并将创建的异常对象于catch小括号中的类型比较

若一致,或是小括号中的异常类型的子类,则会执行catch方法中的代码,可执行try...catch下面的代码

否则抛出异常并停止程序运行

若有多个异常语句可写多个catch语句

try{
    System.out.println(arr[1]);
    System.out.println(1 / 0);
}catch (ArrayIndexOutOfBoundsException e){
    System.out.println(e);
}catch (ArithmeticException e) {
    System.out.println(e);
}

将所有有可能产生的异常都捕获,以便获取bug产生的原因,或针对不同的异常做出不同的操作

如果这些异常有父子关系父类一定要写在下面,以便捕获所有的异常,否则父类会捕获子类的,即多态

JDK7以后的书写方式

int[] arr = new int[1];

try{
    System.out.println(arr[1]);
    System.out.println(1 / 0);
}catch (ArithmeticException | ArrayIndexOutOfBoundsException e){
    System.out.println(e);
}

用于多个异常的处理方式相同的情况

其中子类与父类不能共同存在,否则报错

我认为的原因:父类可以接收子类对象,所以写子类无用

java中的原因:语句中的替代无法通过子类化关联

如下:

try{
    System.out.println(arr[1]);
    System.out.println(1 / 0);
}catch (ArithmeticException | ArrayIndexOutOfBoundsException | Exception e){
    System.out.println(e);
}

会报错

抛出处理

要结合捕获异常使用,这个只是用与提醒调用者会出现的异常,用于异常捕获时的处理

是在本函数中不想处理的时候使用的,将该异常返回给调用者,由调用者处理

throws

写在方法定义出,声明一个异常,用以告诉调用者可能会产生的异常

格式:修饰符 返回值 方法() throws 异常类名1,异常类名2... {}

编译时期异常:必须要写,若重写的方法中父类没有抛出,子类就不能抛出,只能try

运行时期异常:可以不写

throw

写在方法里,结束方法,

格式:throw new 异常类名();

使用

抛出一个RuntimeException异常

throw new RuntimeException();

此时相当于return,结束了当前方法

常见方法

int[] arr = {1};

try {
    System.out.println(arr[1]);
} catch (ArrayIndexOutOfBoundsException e) {
    // getMessage() 返回此throwable的详细消息字符串
    // 只是获取为什么报错
    // Index 1 out of bounds for length 1
    System.out.println(e.getMessage());
    
    // toString() 返回此throwable的简短描述
    // 只是获取什么类型的错误以及为什么报错
    // java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
    System.out.println(e.toString());

    // printStackTrace() 把异常的错误信息输出在控制台
    // 红色字体,包含getMessage与toString
    // 包括 什么类型的错误以及为什么报错 以及在哪报错
    // 不会停止程序运行的
    e.printStackTrace();
    
}
// 以红色字体打印,一般是java官方或第三方使用的,我们一般不使用
System.err.println("123");

注意:该语句与printStackTrace()的执行顺序可能会与代码中的顺序不同,与多线程有关,但是不会提前执行,只有可能在下面的语句的后面才输出结果,或按顺序的输出

自定义异常

定义异常类,继承关系,空参构造与带参构造

为了让控制台的报错信息见名知意

// 运行时异常继承RuntimeException
public class NameFormatException extends RuntimeException {
    public NameFormatException() {
    }
    
    public NameFormatException(String message) {
        super(message);
    }
}

43 File

File对象就是一个文件或文件夹的路径,可以存在,也可以不存在

创建File文件对象

1.
// File(String pathname) 根据路径创建文件对象
String str = "C:\\Users\\Administrator\\Desktop\\a.txt";
File f1 = new File(str);
// 打印后就是str(单\)
// 将其变为File对象,是为了使用它的方法

2.
// File(String parent,String child) 根据父路径名字符串与子路径名字符串创建文件对象
// 父路径:父级路径,指去除a.txt后的,如C:\Users\Administrator\Desktop
// 由java拼接的原因:windows与linux的间隔符不同,Windows为\,linux为/
String parent = "C:\\Users\\Administrator\\Desktop";
String child = "a.txt";
File f2 = new File(parent,child);

3.
// File(File parent,String child) 根据父路径对应的文件对象与子路径名字符串创建文件对象
File parent2 = new File(parent);
File f3 = new File(parent2,child);

// 三者相同
System.out.println(f1);
System.out.println(f2);
System.out.println(f3);

常用方法

判断,获取

// Test文件夹中只有a.txt文件
File f1 = new File("D:\\Test");
File f2 = new File("D:\\Test\\a.txt");
File f3 = new File("D:\\Test\\b.txt");// 不存在

1.
// isDirectory() 判断是否为文件夹
sout(f1.isDirectory());// true
sout(f2.isDirectory());// false
sout(f3.isDirectory());// false

2.
// isFile() 判断是否为文件
sout(f1.isFile());// false
sout(f2.isFile());// true
sout(f3.isFile());// false

3.
// exists() 判断此路径是否存在
sout(f1.exists());// true
sout(f2.exists());// true
sout(f3.exists());// false

4.
// length() 返回文件的大小(字节数量)
// 无法获取文件夹的大小
// 要获取所有的文件夹大小,需要对里面所有的文件遍历获取大小并累加
sout(f1.length());// 0
sout(f2.length());// 84
sout(f3.length());// 0

5.
// getAbsolutePath() 返回绝对路径
sout(f1.getAbsolutePath());// D:\Test
sout(f2.getAbsolutePath());// D:\Test\a.txt
sout(f3.getAbsolutePath());// D:\Test\b.txt

6.
// getPath() 返回定义文件时使用的路径
// 因为创建时使用的是绝对路径,所以获取的也是绝对路径
sout(f1.getPath());// D:\Test
sout(f2.getPath());// D:\Test\a.txt
sout(f3.getPath());// D:\Test\b.txt

7.
// getName() 返回文件的名称,带后缀
sout(f1.getName());// Test
sout(f2.getName());// a.txt
sout(f3.getName());// b.txt

8.
// lastModified() 返回文件最后修改时间(毫秒)
sout(f1.lastModified());// 1691980196469
sout(f2.lastModified());// 1691980258008
sout(f3.lastModified());// 0
// 因为b.txt不存在,所以f3的修改时间为0

创建,删除

1.
// createNewFile() 创建一个空的文件
// 细节1:返回是否创建成功,如果存在,返回false
// 细节2:若父路径不存在,则报错
// 细节3:不会创建文件夹,但会创建无后缀的文件
// 细节4:若存在同名的文件夹,且创建的文件无后缀,则不会创建,返回false
File f1 = new File("D:\\Test\\b.txt");
sout(f1.exists());// false

boolean b1 = f1.createNewFile();
sout(b1);// true
sout(f1.exists());// true

2.
// mkdir() 创建单级文件夹
// 细节1:若存在同名无后缀的文件,则不会创建该文件夹,返回false
// 细节2:若父级路径不存在,返回false
// 细节3:不会创建文件,就算加了后缀,也只会生成"有后缀"的文件夹
File f2 = new File("D:\\Test\\a");
boolean b2 = f2.mkdir();
sout(b2);

3.
// mkdirs() 创建多级文件夹
// 细节1:可以创建单级文件夹,底层运用创建单级文件夹
File f3 = new File("D:\\Test\\c\\d\\e");
boolean b3 = f3.mkdirs();
sout(b3);

4.
// delete() 删除文件,空文件夹
// 细节1:直接删除,不会到回收站
// 细节2:删除非空文件夹,返回false
// 细节3:删除文件或空文件夹时,无论是否存在,均会返回true
File f4 = new File("D:\\Test\\a");
boolean b4 = f4.delete();
sout(b4);

获取并遍历

重点

// listFiles() 获取当前路径下所有内容
// 细节1:当调用者File表示的路径不存在时,返回null
// 细节2:当调用者File表示的路径是文件时,返回null
// 细节3:当调用者File表示的路径是一个空文件夹时,返回长度为0的数组
// 细节4:当调用者File表示的路径是一个含有隐藏文件的文件夹时,返回包含隐藏文件的数组
// 细节5:当调用者File表示的路径是一个需要权限才能访问的文件夹时,返回null
File f = new File("D:\\Test\\a.txt");
File[] files = f.listFiles();

了解

1.
// static listRoots() 获取可用的文件系统根
// 获取系统中所有的盘符,包括DvD驱动器
// 遍历整个硬盘时使用
File[] arr1 = File.listRoots();
sout(Arrays.toString(arr1));

2.
// list() 获取当前路径下所有内容,只获取名字,而不是File对象
File f1 = new File("D:\\Test");
String[] arr2 = f1.list();
for (String s : arr2) {
    sout(s);
}

3.
// list(FilenameFilter filter) 利用文件名过滤器获取当前路径下所有文件
// true当前保留,false舍弃
// 可用Lambda
File f2 = new File("D:\\Test");
String[] arr3 = f2.list(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        File src = new File(dir,name);
        return src.isFile() && name.endsWith(".txt");
    }
});
sout(Arrays.toString(arr3));

4.
// listFiles() 获取当前路径下所有的内容
File f3 = new File("D:\\Test");
File[] arr4 = f3.listFiles();

5.
// listFiles(FileFilter filter) 利用文件名过滤器获取当前路径下所有的内容
// 可用Lambda
File f4 = new File("D:\\Test");
File[] arr5 = f3.listFiles(new FileFilter() {
    @Override
    public boolean accept(File pathname) {
        return pathname.isFile() && pathname.getName().endsWith(".txt");
    }
});

sout(Arrays.toString(arr5));

6.
// listFiles(FilenameFilter filter) 利用文件名过滤器获取当前路径下所有的内容
// 可用Lambda
File f5 = new File("D:\\Test");
File[] arr6 = f5.listFiles(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        File src = new File(dir, name);
        return src.isFile() && name.endsWith(".txt");
    }
});

sout(Arrays.toString(arr6));

44 IO流—基本流,字符集

IO流:存储和读取数据的解决方案

:输出,output

:输入,input

按流的方向分类:输入流,输出流

按操作文件的类型:字节流(操作所有类型的文件),字符流(操作纯文本文件

纯文本文件:利用Windows自带的记事本打开能看懂

基本流:字节流,字符流

IO流体系:字节流,字符流

字节流:InputStream,OutputStream

字符流:Reader,Writer

以上四类均为抽象类

以下为四个抽象类的子类

InputStream:FileInputStream

OutputStream:FileOutputStream

字节流

OutputStream

字节输出流,写入数据

FileOutputStream

操作本地文件的字节输出流,把程序中的数据写到本地文件中

书写步骤

(1)创建字节输出流对象

  1. 参数为字符串时,会根据传入的字符串创建一个File对象
  2. 若文件不存在,会进行创建,但若父级路径不存在,报错
  3. 若传入的是文件夹,则会拒绝访问,并报错
  4. 如果文件存在,则会使用默认的模式(不追加数据),清空文件

(2)写入数据

  1. 写入的是字符,而不是真正的数字

(3)释放资源

  1. 如果不释放,则java会一直占用这个文件,直到程序关闭

如:

// 第一步:创建字节输出流对象
// 有个编译时期异常,让调用者检测指定的文件是否存在,不存在会创建
FileOutputStream fos = new FileOutputStream("D:\\Test\\a.txt");

// 第二步:写入数据
// 写入的是字节,97对应的是a
// 有编译时期异常,要抛出
fos.write(97);

// 第三步:释放资源
fos.close();
写入数据的方式
FileOutputStream fos = new FileOutputStream("a.txt");

1.
// write(int b) 一次写一个字符数据
fos.write(97);
fos.write(98);

2.
// write(byte[] b) 一次写一个字符数组数据
String s = "hello world!";
byte[] bytes1 = s.getBytes();
fos.write(bytes1);

3.
// off为起始索引,len为个数
// write(byte[] b,int off,int len) 一次写一个字符数组的部分数据
byte[] bytes2 = {97,98,99,100,101};
fos.write(bytes2,1,2);

fos.close();
换行与续写

换行

// 换行
// Windows: \r\n
// 早期DOS系统的回车只是将光标移动到改行的开头,所以换行有回车+换行两步操作,现在仍沿用
// Linux: \n
// Mac: \r

// 但在Windows操作系统中,java做了优化,只需写\r或\n,java会不全
// 建议:写全

// 创建换行对应的字符串,方便使用换行
String wrap = "\r\n";
// 创建字节输出流对象
FileOutputStream fos = new FileOutputStream("a.txt");

// 写入数据
String str = "kankelaoyezuishuai";
fos.write(str.getBytes());

fos.write(wrap.getBytes());

fos.write(new byte[]{'6','6','6'});

// 释放资源
fos.close();

续写

// 续写
// 写在最后一行
// 创建字节输出流对象
// true为追加模式是否开启,默认为false,即每次创建对象时清空
FileOutputStream fos = new FileOutputStream("a.txt",true);

String str = "hello world!";
fos.write(str.getBytes());

// 释放资源
fos.close();
加密与解密

在write的时候将数据利用异或一个数字的方式加密,再次加密时就成了解密

int code = 0xFFFFFF;
File temp = new File(file.getAbsolutePath() + "-副本");

FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(temp);

int len;

// 先存储到临时文件中
while ((len = fis.read()) != -1) {
    fos.write(len ^ code);
}

fos.close();
fis.close();

InputStream

字节输入流,读取数据

FileInputStream

操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中

书写步骤

(1)创建字节输入流对象

1. 如果文件不存在,直接报错,原因:创建空文件无意义

(2)读取数据

  1. 一次读一个字节,读出的是数据在ASCII码表上对应的数字
  2. 读到末尾了,read方法返回 -1

(3)释放资源

// a.txt中为abcde
// 创建字节输入流对象
FileInputStream fis = new FileInputStream("a.txt");


// 读取数据
int b1 = fis.read();
System.out.println(b1);// 97

int b2 = fis.read();
System.out.println(b2);// 98

int b3 = fis.read();
System.out.println(b3);// 99

int b4 = fis.read();
System.out.println(b4);// 100

int b5 = fis.read();
System.out.println(b5);// 101

int b6 = fis.read();
System.out.println(b6);// -1

// 释放资源
fis.close();
循环读取
// 创建字节输入流对象
FileInputStream fis = new FileInputStream("a.txt");

// 读取数据
// 用于记录每次读取到的数据
int b;
// 直到读到末尾结束读取
while((b = fis.read()) != -1){
    System.out.print((char)b);
}

// 释放资源
fis.close();
多字节读取
// a.txt中为26个英文字母
// read(byte[] buffer) 一次读一个字节数组数据
// 长度一般为1024的整数倍
FileInputStream fis = new FileInputStream("a.txt");

int speed = 10;
// 第一次读取
byte[] bytes = new byte[speed];
// 返回值为本次读取到多少数据,并把数据放到数组中
// 如果返回值为-1,则表示到末尾了,且数组中的数据均为原来的值
int len1 = fis.read(bytes);

System.out.println(new String(bytes,0,len1));
System.out.println(len1);

// 第二次读取
int len2 = fis.read(bytes);

System.out.println(new String(bytes,0,len2));
System.out.println(len2);

// 第三次读取
int len3 = fis.read(bytes);

// 利用String的构造方法,使存入的数据是本次获取的
System.out.println(new String(bytes,0,len3));
System.out.println(len3);// 6

// 第四次读取
bytes = new byte[speed];
int len4 = fis.read(bytes);

System.out.println(len4);// -1

fis.close();
bom头

在本地新建的文件开头可能有一个隐含的bom头,记录了文件的信息,保存时,保存成带有BOM头的UTF-8格式,就会有bom头,可被输入流读取到

idea中保存的txt文件默认没有bom头

易错

使用多个输入流对象对同一个文件进行操作时,每一个对象均会从开头开始,这样就会造成数据混乱,乱码等,尽量避免

文件拷贝

释放资源的规则:先创建的后关

小文件拷贝

// 创建字节输入流与字节输出流对象
FileInputStream fis = new FileInputStream("D:\\Test\\b\\movie.mp4");
FileOutputStream fos = new FileOutputStream("D:\\Test\\d\\movie.mp4");
// 读取并写入数据
int b;
while((b = fis.read()) != -1){
    fos.write(b);
}

// 释放资源
fos.close();
fis.close();

由于一次只能读取一个字节,导致了读取的速度十分的慢

大文件拷贝

FileInputStream fis = new FileInputStream("D:\\Test\\b\\movie.mp4");
FileOutputStream fos = new FileOutputStream("D:\\Test\\d\\movie.mp4");

int maxSpeed = 1024 * 1024 * 5;
byte[] bytes = new byte[maxSpeed];
int len;
int time = 0;

while ((len = fis.read(bytes)) != -1){
    fos.write(bytes,0,len);
}

fos.close();
fis.close();

捕获异常完整

目的:由于io流中大部分代码都有可能报错,为了避免释放资源的代码执行不到,所以使用,其他同理

格式

try{

}catch(Exception e){

}finally{

}

finally中的代码一定会被执行,除非虚拟机停止

实际上开发中都会将异常抛出,然后统一由spring框架处理

FileInputStream fis = null;
FileOutputStream fos = null;
try{
    fis = new FileInputStream("D:\\Test\\b\\movie.mp4");
    fos = new FileOutputStream("D:\\Test\\d\\movie.mp4");

    int maxSpeed = 1024 * 1024 * 5;
    byte[] bytes = new byte[maxSpeed];
    int len;
    
    while ((len = fis.read(bytes)) != -1){
        fos.write(bytes,0,len);
    }
    
}catch (IOException e){
    e.printStackTrace();
}finally{
    try {
        if (fos != null)
            fos.close();
    }catch (IOException e){
        e.printStackTrace();
    }
    
    try{
        if (fis != null)
            fis.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}
简化方案

实现了AutoCloseable接口,在特定情况下才可以释放

JDK7方案

只有实现了autoCloseable接口的类才可以

但难以阅读

格式

try(创建流对象1;创建流对象2){
    可能出现异常的代码;
}catch(异常类名 变量名){
    异常处理的代码;
}
try(FileInputStream fis = new FileInputStream("D:\\Test\\b\\movie.mp4");
    FileOutputStream fos = new FileOutputStream("D:\\Test\\d\\movie.mp4")){
    
    int len;
    byte[] bytes = new byte[1024 * 1024 * 5];
    while ((len = fis.read(bytes)) != -1){
        fos.write(bytes,0,len);
    }
}catch (IOException e){
    e.printStackTrace();
}

JDK9方案

为了解决难以阅读的问题

格式

创建流对象1;
创建流对象2;
try(流1;流2){
    可能出现异常的代码;
}catch(异常类名 变量名){
    异常的处理代码:
}
FileInputStream fis = new FileInputStream("D:\\Test\\b\\movie.mp4");
FileOutputStream fos = new FileOutputStream("D:\\Test\\d\\movie.mp4");

try(fis;fos){
    int len;
    byte[] bytes = new byte[1024 * 1024 * 5];
    while ((len = fis.read(bytes)) != -1){
        fos.write(bytes,0,len);
    }
}catch (IOException e){
    e.printStackTrace();
}

字符流

Reader

默认一次读一个字节,遇到中文读多个字节

FileReader

书写步骤

(1)创建字符输入流对象

(2)读取数据

  1. 按字节进行读取,遇到中文,一次都多个字节,读取后解码,返回整数
  2. 读到末尾,返回-1

(3)释放资源(关流)

// 创建字符输入流对象
FileReader fr = new FileReader("a.txt");

// 读取数据
// read方法
int len;

while((len = fr.read()) != -1){
    System.out.print((char)len);
}

// 关流
fr.close();
带参读取
// 创建字符输入流对象
FileReader fr = new FileReader("a.txt");

// 读取数据
// 底层将解码后的后的数据强转
char[] chars = new char[2];
int len;
while((len = fr.read(chars)) != -1){
    System.out.print(new String(chars,0,len));
}

// 关流
fr.close();
指定编码格式

利用FileReader的构造方法实现

FileReader fr = new FileReader("gbkfile.txt", Charset.forName("GBK"));

int ch;
while((ch = fr.read()) != -1){
    System.out.print((char)ch);
}

fr.close();
高级

在利用FileReader读取纯文本时,需要对读取到的文本做操作时,可利用String的方法加Stream流的方式,高效,且又简便

如下,将文本中的2-1-9-4-7-8排序,但要在数字间加-

// 获取数据
StringBuilder sb = new StringBuilder();
FileReader fr = new FileReader(file);

int len;
char[] chars = new char[1024 * 1024 * 5];

while ((len = fr.read(chars)) != -1) {
    sb.append(chars, 0, len);
}

fr.close();

// 排序
// 利用Stream流对得到的StringBuilder对象中的数据进行排序并转为数组
Integer[] arr = Arrays
        .stream(sb.toString().split("-"))
        .map(Integer::parseInt)
        .sorted()
        .toArray(Integer[]::new);
// 利用Arrays的toString方法,使得数组变为[x, x, ..., x]格式的字符串
String s = Arrays.toString(arr);
// 将, 替换为-
s = s.replace(", ","-");
// 将字符串开头与结尾的括号去除
s = s.substring(1,s.length() - 1);
// 最终实现了数据的排序,然后再利用FileWriter中的String参数的write方法存储即可

Writer

FileWriter

书写步骤

(1)创建字符输出流对象

  1. 保证父级路径存在,文件不存在会创建
  2. 文件存在会清空文件,除非打开续写开关

(2)写入数据

  1. write的参数是整数,但实际上写到本地文件中的是整数在字符集上对应的字符

(3)释放资源

// 创建字符输出流对象
FileWriter fw = new FileWriter("a.txt");

// 写入数据
fw.write('我');

// 释放资源(关流)
fw.close();
多字节写入数据

通过字符串写入

FileWriter fw = new FileWriter("a.txt");

fw.write("你好威啊???");

fw.close();

部分字符串写入

FileWriter fw = new FileWriter("a.txt");

// 细节1:第二个参数为起始索引
// 细节2:长度加起始索引的大小大于字符串长度时,报错
fw.write("hello world",0,10);

fw.close();

字符数组写入

FileWriter fw = new FileWriter("a.txt");


char[] chars = {'1','2','3','我'};
fw.write(chars);

fw.close();
续写
// 创建字符输出流对象
FileWriter fw = new FileWriter("a.txt",true);

// 写入数据
fw.write('我');

// 释放资源(关流)
fw.close();
刷新

将缓冲区的数据在未满或未释放资源时写入到文件中

FileWriter fw = new FileWriter("a.txt");

fw.write("123");


fw.flush();


fw.close();

当刷新后,缓冲区并不会清空,而是在在一次写入时,将内容覆盖,但不会清空,如输入流的再次填充缓冲区

指定编码格式

利用FileWriter的构造方法实现

FileWriter fw = new FileWriter("b.txt", Charset.forName("GBK"));

fw.write("你好我不好哦");

fw.close();

原理

输入流原理

读取数据时,会将数据源与内存间建立一个通道,并在内存中创建一个长度为8192的数组,称为缓冲区

每次读取时,都会判断缓存区中是否有数据可以被读取,没有就从文件中获取数据,尽可能填满缓冲区,然后每次读取都会从缓冲区中读取数据,效率更高

但若缓冲区的数据都被读取了,缓冲区再次从文件中获取数据(不会重新创建数组,会将原来的值覆盖,但是若文件中无元素了,则不会覆盖原来的),若文件中没有数据可读,即到末尾了,返回-1

如读取前

文件123

缓冲区(假设)12

再次读取后

文件123

缓冲区(假设)32

但字节流没有缓冲区

有参read会有强转的行为

若先创建FileReder然后读取,再创建FileWrite(会清空文件),然后再用FileReader对象读取,能读取到数据,但只是缓冲区中的数据,缓冲区读完后,再次读取,会返回-1

输出流原理

写入数据时,将目的地与内存建立通道,同时创建一个长度未8192的字节数组,称为缓存区

先将数据写到缓冲区中,当缓冲区装满了,或手动刷新(flush方法),或释放资源时(程序关闭时并不会写到本地,但会释放资源)写到本地文件

字符集

:ASCII字符集

计算机最小的存储单位为一个字节,即八个比特位

以ASCII码举例

编码:将要存储的数据通过ASCII码表转换为十进制数,然后后转为二进制的形式

解码:将二进制的数据直接转为十进制的数据,然后通过ASCII码表转换

GB2312字符集:1980年发布,简体中文汉字编码国家标准

BIG5字符集:台湾地区繁体中文标准字符集,1984年实施

GBK:2000年发布,包括简繁汉字,日韩文字,简体中文Windows默认使用的GBK,但系统显示的是ANSI

ANSI是中日韩等国家的各种字符集的统称

Unicode字符集:1994年发布,国际标准字符集,又称万国码,足以跨国际交换信息

这里主要学GBK与Unicode

GBK

规则1:英文字母为一个字节,汉字两个字节存储,第一位字节为高位字节,第二位字节为低位字节

规则2:高位字节二进制一定以1开头,为了与英文区分

Unicode

Unicode兼容ASCII字符集,但编码方案不同

UTF:Unicode Transfer Format

UTF-16:最先提出,用2-4个字节保存,最常用的就是转为16个比特位

UTF-32:固定使用四个字节,32个比特位存储

UTF-8:用1-4个字节保存,最常用的是8个比特位

UTF-8规则

ASCII中的为一个字节,简体中文为3个字节

一个字节前面补0至八位

三个字节第一个字节前面加1110,第二个字节加10,第三个字节加10,实际可用为16个比特位

乱码原因

原因1:读取数据时未读取整个汉字

解决方案:不要用字节流读取文本文件

原因2:编码和解码时的方式不统一

解决方案:统一编码与解码的方式

拷贝不乱码原因:并没有对字节进行解码,所以不会乱码

编码

字符对应的数字变字节,在java中字符变为对应的数字涉及隐转(char变int)

String str = "ai你哟";
// getBytes() 通过默认方式进行编码,默认:UTF-8(idea) GBK(eclipse)
byte[] bytes1 = str.getBytes();
System.out.println(Arrays.toString(bytes1));

// getBytes(String charsetName) 通过指定方式编码
// 可能有本地没有的编码方式,所以会抛出编译时期异常
byte[] bytes2 = str.getBytes("GBK");
System.out.println(Arrays.toString(bytes2));
解码

字节变字符对应的数字,在java中字符对应的数据变为字符,涉及强转(int变char)

// String(byte[] bytes) 通过默认方式解码,默认:UTF-8(idea) GBK(eclipse)
String str1 = new String(bytes1);
System.out.println(str1);
String str2 = new String(bytes2);
System.out.println(str2);// 乱码,原因:编码解码方式不同

// String(byte[] bytes,String charsetName) 通过指定方式解码
String str3 = new String(bytes1,"GBK");
System.out.println(str3);
String str4 = new String(bytes2,"GBK");
System.out.println(str4);

45 IO流—高级流

高级流:缓冲流,转换流,序列化流,打印流,压缩流

缓冲流

缓冲流:字节缓冲流,字符缓冲流

字节缓冲流:BufferedInputStream,BufferedOutputStream

字符缓冲流:BufferedReader,BufferedWriter

字节缓冲流

原理:底层自带8192的长度的缓冲区

字节流包装为字节缓冲流,缓冲流本身不能读取与写入数据,是基本流在读取与写入数据

// BufferedInputStream
FileInputStream fis = new FileInputStream("a.txt");
// 包装fis
BufferedInputStream bis = new BufferedInputStream(fis);

// BufferedOutputStream
FileOutputStream fos = new FileOutputStream("b.txt");
// 包装fos
BufferedOutputStream bos = new BufferedOutputStream(fos);

// 缓冲流底层利用了基本流的写入与读取方法
int len;
while((len = bis.read()) != -1){
    bos.write(len);
}

// 释放资源,只用关缓冲流,缓冲流在底部会关闭基本流
bos.close();
bis.close();
多字节读取与写入
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));

byte[] bytes = new byte[1024];
int len;

while((len = bis.read(bytes)) != -1){
    bos.write(bytes,0,len);
}


bos.close();
bis.close();
原理

缓冲流节省了与硬盘打交道的时间,内存运行速度快,而硬盘读取与写入速度慢,所以缓冲流通过缓冲区在内存中操作,避免了字节流每次读取都要对硬盘操作,只有在缓冲区满或刷新关闭时,才会对硬盘操作,极大的节省了运行时间,提高了运行效率

利用定义的len变量实现了在内存中将缓冲输入流的缓冲区中的数据在内存中转移到缓冲输出流的缓冲区中

字符缓冲流

原理:底层自带8192的长度的缓冲区

字符流包装为字符缓冲流,缓冲流本身不能读取与写入数据,是基本流在读取与写入数据

虽然字符缓冲流对效率并不显著,但由于其中的两个方法,十分重要

两个方法

输入流 readLine(),读取一行数据,若没有数据刻度了,返回null

// 字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("a.txt"));

String s = br.readLine();
System.out.println(s);

br.close();

readLine()虽然会读一行,但不会读换行,存储时需手动换行

循环读取

// 字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("a.txt"));

String s;
while((s = br.readLine()) != null) {
    System.out.println(s);
}

br.close();

输出流 newLine(),跨平台的换行

// 字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));

bw.write("123");
bw.newLine();
bw.write("456");

bw.close();

在java中,一个字符占两个字节(字符使用UTF-16),所以长度为8192的字符缓冲区所占内存为16KB

转换流

字符流:InputStreamReader,OutputStreamWriter

是字符流与字节流之间的桥梁,用以包装基本流

应用场景

  1. 指定字符集读写(JDK11淘汰了),现使用基本流字符流的多参构造方法即可实现,不必转换
  2. 字节流想要使用字符流中的方法(如write(String str),readLine())

InputStreamReader

将字节流转换为字符流

指定编码已在JDK11已被淘汰

// 创建对象并指定编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("gbkfile.txt"),"GBK");

int ch;
while((ch = isr.read()) != -1){
    System.out.print((char)ch);
}

isr.close();

字节流使用字符流特有方法——ReadLine()

// 先创建字节流
FileInputStream fis = new FileInputStream("gbkfile.txt");
// 先将字节流包装成字符流
InputStreamReader isr = new InputStreamReader(fis,"GBK");
// 将字符流包装成字符缓冲流
BufferedReader br = new BufferedReader(isr);

String s = br.readLine();
System.out.println(s);

br.close();

OutputStreamWriter

将字符流转换为字节流

指定编码已在JDK11已被淘汰

// 创建对象,并指定输出的编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("b.txt"),"GBK");

osw.write("你好我不好");

osw.close();

序列化流

用来包装基本流,属于字节流

序列化流:输出流,ObjectOutputStream

反序列化流:输入流,ObjectInputStream

序列化流

将java中的对象写到本地文件中,但是看不懂,避免用户修改数据

又称对象操作输出流

小细节:直接输出会报NotSerializableException异常,需使该类实现Serializable接口(没有抽象方法的接口,称为标记型接口)

Student stu = new Student("张三",23);
// 创建对象操作输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
// 将对象写道本地文件中
oos.writeObject(stu);
// 释放资源
oos.close();

反序列化流

将序列化本地文件中的对象读取到程序中

又称对象操作输入流

// 创建对象操作输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
// 读取序列化对象,返回Object类型,如需原类型,需强转
Object o = ois.readObject();

System.out.println(o);// Student{name = 张三, age = 23}
// 释放资源
ois.close();

细节

当对实现了Serializable接口的类做操作时,该类的序列号会改变,除非指定序列号

当反序列化时,文件中得到的序列号与对应类的序列号不同,会报错

序列号定义标准格式:private static final long serialVersionUID = xxxL;

一般写类中成员变量的最上面

若没有定义序列号,java会通过javabean类中的属性与方法计算javabean,可能会导致上方的情况发生

可以通过开启idea的警告功能中的有关Serializable的警告,实现没有序列号会警告的功能,然后通过Alt+Enter自动添加并计算(推荐)

或者抄java中其他的类中的序列号,但要修改值(不推荐)

在写完整个javabean后再计算序列号,避免出现重复

若不想某个数据被序列化,需使用transient关键字,如

private transient String address;

transient:瞬态关键字,不会把当前的属性序列化本地文件中

一旦序列化中的文件被修改了,就不能再反序列化了,会报错

多对象序列化与反序列化

多对象序列化

细节:一次只能写入一个对象,每个对象都写在同一行了

Student stu1 = new Student("张三",23,"南京");
Student stu2 = new Student("李四",24,"北京");
Student stu3 = new Student("王五",25,"东京");
Student stu4 = new Student("赵六",26,"西京");

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));

oos.writeObject(stu1);
oos.writeObject(stu2);
oos.writeObject(stu3);
oos.writeObject(stu4);

oos.close();

多对象反序列化

细节:readObject方法一次只能读一个对象

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));

// 一次只能读一个对象
Student s1 = (Student)ois.readObject();
Student s2 = (Student)ois.readObject();
Student s3 = (Student)ois.readObject();

System.out.println(s1);
System.out.println(s2);
System.out.println(s3);

ois.close();

当没有对象了,仍然使用readObject方法,会报EOFException(End Of File Exception)

不能主动制造异常

优化

不知道有多少个对象被读取时,可以将多个对象存储到集合中,然后序列化这个集合

这样即避免了异常,又能读取所有的对象

序列化

ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张三",23,"南京"));
list.add(new Student("李四",24,"北京"));
list.add(new Student("王五",25,"东京"));

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));

oos.writeObject(list);

oos.close();

反序列化

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));

ArrayList<Student> list = (ArrayList<Student>)ois.readObject();

for (Student s : list) {
    System.out.println(s);
}

ois.close();

打印流

只有输出流

字节流:PrintStream

字符流:PrintWriter

特点

  1. 打印流只能写不能读
  2. 打印流有特有的方法可以实现,数据鸳鸯写出(打印97就写出97,而不是97)
  3. 打印流特有的写出方法,可以实现自动刷新,自动换行,先换行,再刷新

字节打印流

可以包装OutputStream子类,文件,字符串(传入文件与字符串,会在底层创建FileOutputStream)

// 创建字节打印流的对象
// 第二个参数为是否自动刷新,开不开启都一样,原因:没有缓冲区
PrintStream ps1 = new PrintStream(new FileOutputStream("b.txt"),true,"UTF-8");
PrintStream ps2 = new PrintStream("b.txt");
PrintStream ps3 = new PrintStream(new File("b.txt"));
// 写入数据
ps1.println(97);
ps1.print(true);
ps1.println();
ps1.printf("%s 爱上了 %s","阿珍","阿强");
// 释放资源
ps3.close();
ps2.close();
ps1.close();

可以将其看作是一个System.out,调用其中方法打印再控制台上一样,打印流在文件中打印,,System.out在控制台中打印

其中的write方法输出后必须要手动刷新才行

字符打印流

要使用自动刷新,手动开启

构造方法与成员方法基本与字节打印流相同,但字符打印流可以关联字符输出流与字节输出流

// 创建字符打印流的对象
// 均创建最长的,可以从右到左一次减少到一个参数
PrintWriter pw1 = new PrintWriter(new FileWriter("a.txt"),true);
PrintWriter pw2 = new PrintWriter(new FileOutputStream("a.txt"),true, Charset.forName("UTF-8"));
PrintWriter pw3 = new PrintWriter("a.txt","UTF-8");
PrintWriter pw4 = new PrintWriter(new File("a.txt"),"UTF-8");

pw1.println("今天是星期三");
pw1.print(true);
pw1.printf("%s 爱上了 %s","阿珍","阿强");

pw4.close();
pw3.close();
pw2.close();
pw1.close();

占位符

printf()方法中使用

%s 字符串

%n 换行

%c 将字符变为大写

%b boolean类型的占位符

%d 小数的占位符

%x 将数字转为十六进制

……

与System.out的关系

out是System类中的静态变量,是PrintStream的对象,虚拟机启动后out会自动创建,默认指向控制台

out为系统中的标准输出流,可以将out赋值给其他变量,并由其他变量使用其中的方法在控制台中打印

将out关闭后就不能再打印了

解压缩流

输入流 ZipInputStream

只能识别zip

// 创建解压缩流对象
ZipInputStream zip = new ZipInputStream(new FileInputStream(src));

// 利用zip中的元素ZipEntry进行操作
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null){
    if(entry.isDirectory()) {
        File dir = new File(dest, entry.toString());
        dir.mkdirs();
    }else {
        // 拷贝文件
        File file = new File(dest,entry.toString());
        FileOutputStream fos = new FileOutputStream(file);
        
        // 每次nextEntry时,都会将文件打开,以供拷贝,最后要将这个entry关流
        int b;
        while((b = zip.read()) != -1){
            fos.write(b);
        }
        fos.close();
        zip.closeEntry();
    }
}
// 将zip对象关流
zip.close();

压缩流

输出流 ZipOutputStream

压缩本质,把每一个文件/文件夹看作ZipEntry对象放到压缩包中

单文件压缩

ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest,"a.zip")));

// 参数为在压缩包中该文件或文件夹的名字
ZipEntry entry = new ZipEntry(src.getName());
// 将entry对象放到zos中,并对该entry对象进行操作
zos.putNextEntry(entry);

// 将数据写入到entry对象
FileInputStream fis = new FileInputStream(src);

int b;
while ((b = fis.read()) != -1){
    zos.write(b);
}

// 关流
fis.close();
zos.closeEntry();
zos.close();

若在创建entry时传递的参数为 文件夹\...\文件名,则会在zip文件中创建文件夹

public static void main(String[] args) throws IOException {
    File src = new File("D:\\Test");
    File dest = new File(src.getParent(), src.getName() + ".zip");
    // 将压缩输出流对象创建到外面,是防止压缩输出流对象每次都会重新创建
    FileOutputStream fos = new FileOutputStream(dest);
    ZipOutputStream zos = new ZipOutputStream(fos);
    
    toZip(src,zos,src.getName());
    // 关流
    zos.close();
    
}

// 将文件名传入,可实现遍历时,多级文件夹的形成
public static void toZip(File src, ZipOutputStream zos,String name) throws IOException {
    File[] files = src.listFiles();
    
    if (files == null)
        return;
    
    if (files.length == 0){
        // 如果是空文件夹,直接创建
        ZipEntry entry = new ZipEntry(name + "\\");
        zos.putNextEntry(entry);
        zos.closeEntry();
        return;
    }
    // 不是空文件夹,利用文件创建,或递归创建
    for (File file : files) {
        if (file.isFile()){
            // 是文件,拷贝
            ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
            zos.putNextEntry(entry);
            
            FileInputStream fis = new FileInputStream(file);
            int len;
            byte[] bytes = new byte[1024 * 1024 * 5];
            while ((len = fis.read(bytes)) != -1){
                zos.write(bytes,0,len);
            }
            
            fis.close();
            zos.closeEntry();
        }else {
            // 是文件夹,进行递归
            toZip(file,zos,name + "\\" + file.getName());
        }
    }
}

Commons-io

是apache开源基金组织提供的一组关于IO操作的开源工具包,是Commons中的一种

用以提高IO流的开发效率

使用方式

  1. 现在项目中创建一个文件夹:lib
  2. 将jar包复制到lib文件夹
  3. 右键点击jar包,选择 Add as Library -> 点击OK
  4. 在类中导包使用(org.apache.commons.io)

包含类:FileUtils,IOUtils....

使用如

File src = new File("D:\\Test");
File dest = new File("D:\\Project");
// 将src文件夹中所有内容拷贝到dest文件夹中
FileUtils.copyDirectory(src,dest);
// 将src文件夹拷贝到dest文件夹中
FileUtils.copyDirectoryToDirectory(src,dest);

利用资料中的md文档了解常用的方法

Hutool工具包

又称糊涂包

https://hutool.cn/docs/#/ 查看Hutool的api帮助文档

抽时间查看源码,并学习api

46,47 综合练习

制造假数据

网络爬取

获取姓氏

https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0

获取男生名字

http://www.haoming8.cn/baobao/10881.html

获取女生名字

http://www.haoming8.cn/baobao/7641.html

package com.nomit.study.io.day46;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class SpiderDemo01 {
    public static void main(String[] args) throws IOException {
        // 1.记录网址
        String lastNameNet = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
        String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
        String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";
        
        // 2.爬取数据
        // 爬取到的是网页源码
        String lastName = webCrawler(lastNameNet);
        String boyName = webCrawler(boyNameNet);
        String girlName = webCrawler(girlNameNet);
        
        // 3.通过正则表达式,把满足要求的数据获取出
        // [\u4E00-\u9FA5]为汉字的范围
        ArrayList<String> lastNameTempList = getData(lastName, "(.{4})([,。])", 1);
        ArrayList<String> boyNameTempList = getData(boyName, "([\\u4E00-\\u9FA5]{2})([、。])", 1);
        ArrayList<String> girlNameTempList = getData(girlName, "(.. ){4}..", 0);
        
        // 4.将每个姓/名分开
        // 处理姓氏
        ArrayList<String> lastNameList = new ArrayList<>();
        for (String s : lastNameTempList) {
            for (int i = 0; i < s.length(); i++) {
                lastNameList.add(s.charAt(i) + "");
            }
        }
        
        // 处理男生名
        ArrayList<String> boyNameList = new ArrayList<>();
        boyNameTempList.stream().distinct().forEach(boyNameList::add);
        
        // 处理女生名
        ArrayList<String> girlNameList = new ArrayList<>();
        for (String s : girlNameTempList) {
            String[] arr = s.split(" ");
            Collections.addAll(girlNameList, arr);
        }
        
        // 5.处理数据
        // 想要的数据:姓名(唯一)-性别-年龄
    
        ArrayList<String> list = getInfos(lastNameList, boyNameList, girlNameList, 30, 20);
        
        
        // 6.持久化存储
        BufferedWriter bw = new BufferedWriter(new FileWriter("name.txt"));
    
        for (String s : list) {
            bw.write(s);
            bw.newLine();
        }
        
        bw.close();
    }
    
    // 获取数据,格式:姓名(唯一)-性别-年龄
    public static ArrayList<String> getInfos(ArrayList<String> lastNameList
            , ArrayList<String> boyNameList, ArrayList<String> girlNameList, int boyNum, int girlNum) {
        
        // 生成不重复的名字
        HashSet<String> boyhs = new HashSet<>();
        
        while (boyhs.size() < boyNum) {
            Collections.shuffle(lastNameList);
            Collections.shuffle(boyNameList);
            
            boyhs.add(lastNameList.get(0) + boyNameList.get(0));
        }
        
        HashSet<String> girlhs = new HashSet<>();
        
        while (girlhs.size() < girlNum) {
            Collections.shuffle(lastNameList);
            Collections.shuffle(girlNameList);
            
            girlhs.add(lastNameList.get(0) + girlNameList.get(0));
        }
        
        // 完成格式
        Random r = new Random();
        ArrayList<String> list = new ArrayList<>();
        
        for (String boyName : boyhs) {
            int age = r.nextInt(10) + 18;
            list.add(boyName + "-男-" + age);
        }
        
        for (String girlName : girlhs) {
            int age = r.nextInt(8) + 18;
            list.add(girlName + "-女-" + age);
        }
        
        // 打乱数据
        Collections.shuffle(list);
        
        return list;
    }
    
    // 通过正则表达式,获取字符串中的数据
    // index是获取第几组数据,目的是不获取标点符号
    private static ArrayList<String> getData(String str, String regex, int index) {
        ArrayList<String> list = new ArrayList<>();
        
        Pattern compile = Pattern.compile(regex);
        Matcher matcher = compile.matcher(str);
        
        while (matcher.find()) {
            // index为第几组,0为所有,1为第一组
            String group = matcher.group(index);
            list.add(group);
        }
        
        return list;
    }
    
    // 通过网址爬取网页源码
    private static String webCrawler(String net) throws IOException {
        // 1.用于拼接获取到的数据
        StringBuilder sb = new StringBuilder();
        
        // 2.创建一个url对象
        URL url = new URL(net);
        
        // 3.连接网址
        // 保证网址能访问,有网络
        URLConnection conn = url.openConnection();
        
        // 4.读取数据,利用IO流读取
        InputStream is = conn.getInputStream();
        // 由于网站上可能有中文,所以利用字符流来读取
        InputStreamReader isr = new InputStreamReader(is);
        int len;
        char[] chars = new char[1024];
        while ((len = isr.read(chars)) != -1) {
            sb.append(chars, 0, len);
        }
        
        // 5.释放资源
        isr.close();
        
        // 6.返回拼接后的数据
        return sb.toString();
    }
}

l利用Hutoo包爬取数据

package com.nomit.study.io.day46;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HttpUtil;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;

public class SpiderDemo02 {
    public static void main(String[] args) throws IOException {
        // 1.定义网址
        String lastNameNet = "https://hanyu.baidu.com/shici/detail?pid=0b2f26d4c0ddb3ee693fdb1137ee1b0d&from=kg0";
        String boyNameNet = "http://www.haoming8.cn/baobao/10881.html";
        String girlNameNet = "http://www.haoming8.cn/baobao/7641.html";
        
        // 2.爬取数据
        String lastName = HttpUtil.get(lastNameNet);
        String boyName = HttpUtil.get(boyNameNet);
        String girlName = HttpUtil.get(girlNameNet);
        
        
        // 3.利用正则表达式获取数据
        List<String> lastNameTempList = ReUtil.findAll("(.{4})([,。])", lastName, 1);
        List<String> boyNameTempList = ReUtil.findAll("([\\u4E00-\\u9FA5]{2})([、。])", boyName, 1);
        List<String> girlNameTempList = ReUtil.findAll("(.. ){4}..", girlName, 0);
        
        // 4.将每个姓/名分开
        // 处理姓氏
        ArrayList<String> lastNameList = new ArrayList<>();
        for (String s : lastNameTempList) {
            for (int i = 0; i < s.length(); i++) {
                lastNameList.add(s.charAt(i) + "");
            }
        }
    
        // 处理男生名
        ArrayList<String> boyNameList = new ArrayList<>();
        boyNameTempList.stream().distinct().forEach(boyNameList::add);
    
        // 处理女生名
        ArrayList<String> girlNameList = new ArrayList<>();
        for (String s : girlNameTempList) {
            String[] arr = s.split(" ");
            Collections.addAll(girlNameList, arr);
        }
        // 5.处理数据
        // 想要的数据:姓名(唯一)-性别-年龄
    
        ArrayList<String> list = getInfos(lastNameList, boyNameList, girlNameList, 30, 20);
        
        // 6.持久化存储
        // Hutool包的相对路径不是相对于当前项目,而是相对于当前文件的(在out文件夹中)
        FileUtil.writeLines(list,"name.txt","UTF-8");
        
    }
    
    public static ArrayList<String> getInfos(ArrayList<String> lastNameList
            , ArrayList<String> boyNameList, ArrayList<String> girlNameList, int boyNum, int girlNum) {
        
        // 生成不重复的名字
        HashSet<String> boyhs = new HashSet<>();
        
        while (boyhs.size() < boyNum) {
            Collections.shuffle(lastNameList);
            Collections.shuffle(boyNameList);
            
            boyhs.add(lastNameList.get(0) + boyNameList.get(0));
        }
        
        HashSet<String> girlhs = new HashSet<>();
        
        while (girlhs.size() < girlNum) {
            Collections.shuffle(lastNameList);
            Collections.shuffle(girlNameList);
            
            girlhs.add(lastNameList.get(0) + girlNameList.get(0));
        }
        
        // 完成格式
        Random r = new Random();
        ArrayList<String> list = new ArrayList<>();
        
        for (String boyName : boyhs) {
            int age = r.nextInt(10) + 18;
            list.add(boyName + "-男-" + age);
        }
        
        for (String girlName : girlhs) {
            int age = r.nextInt(8) + 18;
            list.add(girlName + "-女-" + age);
        }
        
        // 打乱数据
        Collections.shuffle(list);
        
        return list;
    }
}

带权重随机算法

为每一个对象设置一个权重,利用权重 / 总权重获取对象在0~1之间的那个区段,当随机到该区段,即获取该对象

public static void main(String[] args) throws IOException {
    File nameTXT = new File("data\\name.txt");
    
    ArrayList<Student> list = new ArrayList<>();
    BufferedReader br = new BufferedReader(new FileReader(nameTXT));
    String line;
    while ((line = br.readLine()) != null) {
        String[] arr = line.split("-");
        Student stu = new Student(arr[0], arr[1], Integer.parseInt(arr[2]), Double.parseDouble(arr[3]));
        list.add(stu);
    }
    br.close();
    // 获得总权重
    double weight = getAllWeight(list);
    // 获取每个元素的权重占比
    double[] arr = getEveryWeight(weight, list);
    // 获取每个元素权重区间
    arr = getProportion(arr);
    
    double d = Math.random();
    // 返回 -插入点 - 1
    int i = -Arrays.binarySearch(arr, d) - 1;

    Student stu = list.get(i);
    System.out.println(stu.getName());
    
    stu.setWeight(stu.getWeight() / 2);
    
    BufferedWriter bw = new BufferedWriter(new FileWriter(nameTXT));
    for (Student s : list) {
        // 将toString改为所需格式name-gender-age-weight
        bw.write(s.toString());
        bw.newLine();
    }
    bw.close();
}

public static double getAllWeight(ArrayList<Student> list) {
    double weight = 0;
    for (Student student : list) {
        weight += student.getWeight();
    }
    return weight;
}

public static double[] getEveryWeight(double weightAll, ArrayList<Student> list) {
    double[] arr = new double[list.size()];
    int len = list.size();
    for (int i = 0; i < len; i++) {
        arr[i] = list.get(i).getWeight() / weightAll;
    }
    return arr;
}

public static double[] getProportion(double[] arr) {
    int len = arr.length;
    for (int i = 1; i < len; i++) {
        arr[i] += arr[i - 1];
    }
    return arr;
}

登入注册

每一组数据间用&隔开,如账号密码:username=zhangsan&password=123

与http有关

Properties配置文件

软件配置,即设置

将需要修改的设置写到配置文件中,不需要改动代码,修改配置文件即可

后缀名:properties

文件中的信息都是按键值对存储的

等号前后不加空格,后面相当于字符串,写文件路径时用两个\

使用配置文件,可以做到修改配置文件,不需重启软件,就可以改变配置,除一些改变页面的配置

为了方变从配置文件中读取与写入数据,java专门写了一个类,叫Properties,继承于HashTable

有一些特有方法,可将集合中的数据按键值对的形式存储在配置文件,或读取

Properties没有泛型,可添加任意的数据类型,但一般只会添加字符串

Properties prop = new Properties();

store()方法写出数据

Properties prop = new Properties();

prop.put("aaa","bbb");
prop.put("bbb","ccc");
prop.put("ddd","eee");
prop.put("fff","iii");

// store 将集合中的数据保存在配置文件中
// 参数一为输出流,OuputStream或Writer,第二个参数是文件的注释
// 注释是写在开头的,第一行是通过store写的,第二行是store自动添加的
// #test
// #Fri Aug 18 11:22:08 CST 2023
FileOutputStream fos = new FileOutputStream("a.properties");
prop.store(fos,"");
fos.close();

load()方法读入数据

Properties prop = new Properties();
// load 将properties文件中的数据读取出来
// 参数:输入流,InputStream或Reader
FileInputStream fis = new FileInputStream("a.properties");
prop.load(fis);
fis.close();

System.out.println(prop);

48,49 多线程

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

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

线程的简单理解,应用软件中相互独立,可以同时运行的功能

并发:在同一时刻,有多个指令在单个CPU交替执行

并行:在同一时刻,有多个指令在多个CPU同时执行

并发与并行可能会同时存在

实现方式

继承Thread类的方式实现

优点:编程简单,在子类中可直接使用Thread方法,不用通过Thread.currentThread();方法获取后再调用

缺点:可扩展性较差,不能再继承其他的类了

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 方便查看是那个线程调用的
t1.setName("线程1");
t2.setName("线程2");
// t1,t2交替运行
t1.start();
t2.start();

MyThread类

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

实现Runnable接口的方式实现

优点:扩展性强,可以继承其他类

缺点:编程复杂,不能直接使用Thread类中的方法

// 创建MyRun对象,表示线程要执行的任务
MyRun mr = new MyRun();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
 
t1.setName("线程1");
t2.setName("线程2");

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

MyRun类

public class MyRun implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 获取当前线程的对象
            Thread thread = Thread.currentThread();
            // 用以标记当前线程,查看多线程运行情况
            System.out.println("hello " + thread.getName());
        }
    }
}

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

由于第一种与第二种的run方法没有返回值,所以有了第三种方式

优点:扩展性强,可以继承其他类,可以获取线程运行后的返回值

缺点:编程复杂,不能直接使用Thread类中的方法

// 创建MyCallable对象,,表示多线程要执行的任务
MyCallable mc = new MyCallable();
// 创建FutureTask的对象(Future的实现类),作用管理多线程运行的结果
// 泛型为线程运行的返回值类型
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建Thread类的对象,表示线程
Thread t1 = new Thread(ft);


t1.start();
// 有编译时期异常
Integer sum = ft.get();
System.out.println(sum);

MyCallable类

// 泛型是call方法的返回值类型
public 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;
    }
}

利用匿名内部类的方式实现

优点:编程简单

缺点:只能用一次

Thread t = new Thread(){
    @Override
    public void run() {
        System.out.println("123");
    }
};
t.start();

成员方法

MyThread mt = new MyThread();
// getName() 返回此线程的名字
// 默认名字为 Thread-x x为第几个创建的线程,始为0
System.out.println(mt.getName());// Thread-0

// setName(String name) 设置此线程的名字
mt.setName("aaa");
// 在创建Thread对象时,可以直接设置名字
Thread t = new Thread("飞机");
System.out.println(t.getName());

// static currentThread() 获取当前线程的对象
// 哪条线程执行到这个方法,就会获得这个线程的对象
// 要在run或call方法中使用
// 由于程序运行需要开启线程,而main方法是主入口,所以main也有线程,名字就是main
Thread mainT = Thread.currentThread();
System.out.println(mainT.getName());// main

// static sleep(long time) 让线程休眠指定的时间,单位为毫秒
// 哪条线程执行到这个方法,哪个线程就会休眠
// 有编译时期异常
// 将main线程休眠,时间结束后继续会运行
Thread.sleep(1000);
System.out.println("123");

线程优先级

线程调度模式:抢占式调度,非抢占式调度

抢占式调度:java采用,执行顺序随机,优先级与随机有关,优先级越大,越易执行

非抢占式调度:按顺序执行,执行时间差不多

由于抢占式调度的特点,优先级低的也有可能限制性完毕

MyRun mr = new MyRun();
Thread t1 = new Thread(mr,"thread1");
Thread t2 = new Thread(mr,"thread2");

// getPriority() 获取线程的优先级
System.out.println(t1.getPriority());// 5
System.out.println(t2.getPriority());// 5
System.out.println(Thread.currentThread().getPriority());// 5


// setPriority(int newPriority) 设置线程的优先级,最小为1,最大为10,默认为5
t1.setPriority(1);
t2.setPriority(10);

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

守护线程

当其他线程执行完毕,守护线程也会结束执行,可能没有执行完毕

并非立刻结束执行,而是接收到指令后才会关闭,这期间守护线程还是会执行的

守护线程可能会先执行完毕

// setDaemon(boolean on) 设置为守护线程
// mt1执行10次循环
// mt2执行100次循环
MyThread1 mt1 = new MyThread1();
MyThread2 mt2 = new MyThread2();

mt1.setName("女神");
mt2.setName("备胎");
// 设置为守护线程
mt2.setDaemon(true);

mt1.start();
mt2.start();

礼让线程

将CPU的执行权让出,让其他线程执行

// static yeild() 礼让线程

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

t1.setName("飞机");
t2.setName("坦克");

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

MyThread类

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("hello " + getName());
            // 表示让出当前CPU的执行权,使运行形成t1 t2 t1 t2....的状况
            // 但只是尽可能的均匀,可以使特定的线程较后面执行
            Thread.yield();
        }
    }
}

插入线程

可以使特定线程较前面执行,但却使运行变得有顺序,如同在main方法中运行对象的方法,按顺序执行

// join() 插入线程
// 要求:土豆线程先执行完,再执行main线程
MyThread t = new MyThread();
t.setName("土豆");
t.start();

// 有编译时期异常
t.join();


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

线程的生命周期

新建:创建线程对象

就绪:start(),有执行资格,没有执行权(没抢到CPU)

运行:有执行资格,有执行权,若又被抢走,便会就绪状态

死亡:run()执行完毕,编程垃圾

堵塞:没有执行资格与执行权

sleep方法结束后,并不会立刻执行下面的代码,而是进入就绪状态

线程安全问题

由于多线程的独立性,会导致安全问题

执行代码时,执行权随时有可能被抢走

如:一个电影院在卖100张票,共有三个窗口在卖票,多个线程同时抢票,很有可能导致一张票被多个线程抢到,还有可能会有超出范围的票被卖出,原因:当其他线程执行时,票还有剩余,但是执行权被抢走后,票被抢走了,所以票就会多出了

MyThread t1 = new MyThread("第一窗口");
MyThread t2 = new MyThread("第二窗口");
MyThread t3 = new MyThread("第三窗口");

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

MyThread类

public class MyThread extends Thread {
    static int ticket = 1;
    
    public MyThread(String name){
        setName(name);
    }
    
    @Override
    public void run() {
        while (ticket <= 100){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "正在卖第" + ticket++ + "张票");
        }
    }
}

同步代码块

用于解决上述的安全问题

当一个线程在执行同步代码块时,即使执行权被其他的线程抢走了,也无法执行同步代码块,只有执行同步代码块的线程执行完后,其他线程才可执行

关键字synchronized

格式:synchronized (锁对象){要锁的代码}

细节1:synchronized不能写在循环外面,避免一个线程将程序执行完

细节2:锁对象类型是任意的,但要是唯一的,即静态的,使得所有线程的锁是同一个,一般写的是该类的字节码文件对象,原因:字节码文件对象是唯一的

字节码文件对象类名.class,如MyThread.class

MyThread类

public class MyThread extends Thread {
    static int ticket = 1;
    // 锁对象,必须是唯一,即静态的
    static Object obj = new Object();
    
    public MyThread(String name) {
        setName(name);
    }
    
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                // 为了避免其他线程在不满足条件的情况下依旧能进入死循环,执行代码
                // 将判断语句放入锁中,只有进入锁的线程才可以判断
                if (ticket <= 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "正在卖第" + ticket++ + "张票");
                }else {
                    break;
                }
            }
        }
    }
}

同步方法

将synchronized直接加到方法上

格式:修饰符 synchronized 返回值类型 方法名(方法参数){...}

特点1:锁住方法中的所有代码

特点2:锁对象不能自己指定,由java指定,若方法是非静态的,锁对象为this,若是静态的,则是当前类的字节码文件对象

MyRun mr = new MyRun();

Thread t1 = new Thread(mr,"第一窗口");
Thread t2 = new Thread(mr,"第二窗口");
Thread t3 = new Thread(mr,"第三窗口");

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

MyRun类

public class MyRun implements Runnable {
    // 共用一个对象,MyRun的对象只用创建一个
    int ticket = 0;
    
    @Override
    public void run() {
        while (true){
            if (!getTicket())
                break;
        }
    }
    
    public synchronized boolean getTicket(){
        Thread thread = Thread.currentThread();
        if (++ticket <= 100){
            try{
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + "正在抢第" + ticket + "张票");
            return true;
        }else 
            return false;
    }
    
}

StringBuffer

与StringBuilder一样,方法是一样的,但是StringBuffer可用于多线程,是安全的,StringBuilder是不安全的

StringBuffer所有的方法都有加synchronized,单线程用StringBuilder,多线程用StringBuffer

Lock锁

JDK5提出

比synchronized有更广泛的锁定操作,可以手动上锁与解锁,Lock是接口,需使用它的实现类ReentrantLock实例化

MyThread类

public class MyThread extends Thread {
    static int ticket = 1;
    // MyThread要创建很多次,所以要保证锁的唯一性
    static Lock lock = new ReentrantLock();
    
    public MyThread(String name) {
        setName(name);
    }
    
    @Override
    public void run() {
        while (true) {
            // 上锁
            lock.lock();
            
            try {
                if (ticket > 100)
                    break;
                
                Thread.sleep(10);
                System.out.println(getName() + "正在卖第" + ticket++ + "张票");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 避免因为执行了break后跳出循环,而无法执行解锁操作
                // 保证锁一定会被释放
                lock.unlock();
            }
        }
    }
}

死锁

是一个错误

原因:锁的嵌套

MyThread1类

public class MyThread1 extends Thread {
    static Object objA = new Object();
    static Object objB = new Object();
    
    @Override
    public void run() {
        while (true){
            if("线程A".equals(getName())){
                
                synchronized (objA) {
                    System.out.println("A拿了A锁");
                    
                    synchronized (objB) {
                        
                        System.out.println("A拿了B锁");
                    }
                    
                }
                
            }else if ("线程B".equals(getName())){
                
                synchronized (objB) {
                    System.out.println("B拿了B锁");
                    
                    synchronized (objA) {
                        System.out.println("B拿了A锁");
                    }
                    
                }
            }
        }
    }
}

程序卡死,但不停止,A拿了A锁,B拿了B锁,导致A与B无法拿到另一个锁

等待唤醒机制

又称生产者和消费者

用来打破随机的机制,其中A线程可称为消费者,B线程可称为生产者

运行模式:ABABABABABAB......

生产者用以生产数据,消费者则是消费数据

消费者等待

若一开始由消费者获得执行权,但是没有可以供其消费的数据,所以消费者会先等待,即wait,等到被其他线程唤醒为止

然后执行权就给生产者,生产者生产完数据后,就会唤醒消费者,即notify

生产者等待

若生产者在数据未被消费前由获得了执行权,就会等待,即wait,等到被其他线程唤醒为止

然后执行权就交给消费者了,消费者消费完数据后,就会唤醒生产者,即notify

Cook cook = new Cook();
Foodie foodie = new Foodie();

cook.setName("厨师");
foodie.setName("吃货");

cook.start();
foodie.start();

生产者

//生产者
public class Cook extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                // 达到上限了,不再继续循环了
                if (Desk.count == 0)
                    break;
                
                if (Desk.foodFlag == 1){
                    // 还有数据可以被消费,等待
                    try {
                        Desk.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    Desk.foodFlag = 1;
    
                    System.out.println("厨师正在做面条,还能再做" + (Desk.count - 1) + "碗面条");
                    
                    Desk.lock.notifyAll();
                }
            }
        }
    }
}

消费者

// 消费者
public class Foodie extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                // 达到上限了,不再继续循环了
                if (Desk.count == 0)
                    break;
                
                if (Desk.foodFlag == 0) {
                    // 没有就等待
                    // 要用锁对象来等待
                    // 用于将锁对象与当前线程绑定,以便唤醒时只唤醒当前线程
                    try {
                        Desk.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    // 还有数据可以被消费
                    Desk.foodFlag = 0;
                    Desk.count--;
                    System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗面条");
    
                    Desk.lock.notifyAll();
                }
            }
        }
    }
}

控制器

// 用于控制生产者与消费者的执行
public class Desk {
    // 是否有数据
    // int类型能被用于控制多条线程的执行,每一个值对应一个线程的执行
    // 这样写,代码具有通用性
    public static int foodFlag = 0;
    
    // 表示消费者最多执行的次数
    public static int count = 10;
    
    // 创建锁
    public static Object lock = new Object();
}

利用阻塞队列方式实现

等着,什么都做不了,就是阻塞

生产者与消费者必须共用一个阻塞队列

阻塞队列的继承结构

Iterable:顶层接口

Collection

Queue:队列

BlockingQueue:阻塞队列

以上四个均为接口,从上往下一一继承

实现类ArrayBlockingQueueLinkedBlockingQueue

ArrayBlockingQueue:底层时数组,有界,创建时要指定长度

LinkedBlockingQueue:底层是链表,无界,但不是真正的无界,最大值为2147483647,计算机容不下,所以几乎算是无界

ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

// 通过创建对象时传递阻塞队列,以便消费者与生产者的阻塞队列是同一个
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);

c.start();
f.start();

生产者

public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;
    
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    
    @Override
    public void run() {
        while (true){
            try {
                // put方法内部有锁
                // put方法中有wait方法,当装满后就会wait
                queue.put("面条");
                System.out.println("厨师放了一条面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者

public class Foodie extends Thread {
    ArrayBlockingQueue<String> queue;
    
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    
    @Override
    public void run() {
        while (true){
            try {
                // take方法内部有锁
                // 当队列中没有数据时,就会wait
                String take = queue.take();
                System.out.println(take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程的六种运行状态

新建:创建线程对象

就绪:有执行资格,但没有执行权

等待:遇到wait方法就会进入该状态,遇到notify回到就绪状态

计时等待:遇到sleep方法进入该状态,计时结束回到就绪状态

阻塞:没有执行资格与执行权,没有得到锁会进入阻塞状态,得到锁后运行

死亡:线程运行完毕,变成垃圾

sleep方法细节:遇到sleep方法后,线程会将执行权让出,并进入即使等待状态,时间结束后会带就绪状态,当参数为0时,sleep依旧会将执行权让出,但是会立即进入就绪状态,所以在运行状态时,sleep(0),线程会进入就绪状态

运行:不属于java规定的六种状态,当线程在就绪状态抢到执行权,会被JVM交给系统运行,所以运行不属于JVM的六种状态

线程栈

栈不是唯一的,堆是唯一的,每一个栈都对应一个线程

线程池

ExecutorService

为了解决线程创建时的浪费时间,以及线程结束后就消失,以至于浪费资源的问题,线程池就出现了

线程池有上限,可自己设置

线程池的创建:利用Executors工具类创建

只能利用Runnable与Callable的实现类创建线程

线程的默认名字为pool-1-thread-1,第一个数字是第几个线程池,第二个数字是该线程池里第几个线程

细节:当线程执行完后,线程会被还给线程池,此时再提交任务,线程会利用空闲的线程提交任务

但若其他线程都在执行,未被返回,则会新建线程,然后利用他提交任务

但若是线程池达到上限了,任务就会先排队等待,等有空闲的线程时,再执行

// newCachedThreadPool() 创建一个上限为2147483647,可认为无限
ExecutorService pool1 = Executors.newCachedThreadPool();

// 提交任务,即运行任务
pool1.submit(new MyRun());
Thread.sleep(100);

// 销毁线程池
pool1.shutdown();

由于达到上限后就不会再创建新的线程了,所以线程的名字也就不会出现超过上限的

// newFixedThreadPool(int nThreads) 创建一个有上限的线程池
ExecutorService pool2 = Executors.newFixedThreadPool(3);

// 提交任务
pool2.submit(new MyRun());

// 销毁线程池
pool2.shutdown();

自定义线程池

创建临时线程的时机:当核心线程达到上限阻塞队列长度也达到上限时,仍有需要处理的任务,就会创建临时线程处理这些任务

所以 任务执行的顺序 不是按照 提交顺序 执行的

任务拒绝策略:当核心线程,临时线程,阻塞队列都已经达到上限时,仍有需要执行的任务,就会触发任务拒绝策略,丢弃一些任务

以下四个均为ThreadPoolExecutor中的静态内部类

ThreadPoolExecutor.

AbortPolicy默认策略,丢弃任务并抛出RejectedExecutionException

DiscardPolicy:丢弃任务,但不抛出异常,不推荐的做法

DiscardOldestPolicy:抛弃阻塞队列中等待最久的任务,然后把当前任务加入到队列中

CallerRunsPolicy:调用任务的run()方法,直接执行

内部类原因:四种策略依赖TreadPoolExecutor,且单独存在无意义,是一个独立的个体

ThreadPoolExecutor的构造方法的七个参数

1. 核心线程数量(不可以小于0)
2. 线程是中最大线程数量(最大数量>=核心线程数量,多余的是临时线程的最大数量)
3. 临时线程空闲多久后会被销毁(值)
4. 临时线程空闲多久后会被销毁(单位,用TimeUnit指定)
5. 阻塞队列(ArrayBlockingQueue,LinkedBlockingQueue)
6. 创建线程的方式
7. 要执行的任务过多时的任务拒绝策略
// ThreadPoolExecutor
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        3, //核心线程数
        6, // 最大线程数
        1, // 空闲时间
        TimeUnit.MINUTES, // 空闲时间单位
        new ArrayBlockingQueue<Runnable>(3), // 阻塞队列
        Executors.defaultThreadFactory(), // 创建线程工厂
        new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
);

pool.submit(new MyRun());

pool.shutdown();

最大并行数

4核8线程

超线程技术:将4核虚拟成8个,即8线程

8线程即最大并行数为8

或通过Runtine.getRuntime().availableProcessors()获取可用的线程数,及最大并行数

线程池最大上限

CPU密集型运算:最大并行数 + 1

I/O密集型运算:最大并行数 * 期望CPU利用率 * (CPU计算时间 + 等待时间) / CPU计算时间

举例:从本地文件中读取两个数据,并相加,读取数据耗时1s,计算也耗时1s,那么总时间为2s,CPU计算时间为1s

CPU计算时间与等待时间,需要用工具thread dump测试获取

计算比较多,用CPU密集型运算,获取最优的运行效率

文件操作比较多,用I/O密集型运算,一般I/O型最多

CPU密集型运算加1的原因:当前项目由于页缺失故障,或由于其他原因,导致线程暂停,那么额外的线程就可以代替,保证CPU的运行效率

页缺失故障:指软件试图访问虚拟地址空间,但是未被加载在物理内存中的一个分页时,由CPU发出的中断

JUC

查看day32的资料

面试时需要会

50,51 网络编程

网络编程是计算机与计算机之间通过网络传输数据

常见的软件架构:BS,CS

CS:Client/Server,即客户端与服务器,如QQ,Steam

BS:Browser/Server,即浏览器与服务器,通称网页端

BS优点:不需要开发客服端,开发,部署,维护,更新非常简单

BS缺点:受限于网络传输,越是占用大的图片,音频,越是加载慢

CS优点:画面精美,用户体验好

CS缺点:需要开发客户端,开发,安装,部署,维护麻烦

网络编程三要素

IP设备在网络中的地址,是唯一的标识

端口号软件在设备中的唯一标识,用来确定接收数据的软件,一个端口号只能被一个软件绑定

协议数据在网络中传输的规则,常见的协议由UDP、TCP、http、https、ftp

IP

全称:Internet Protocol,是互联网协议地址,也称IP地址

是分配给上网设备的数字标签

常见分类:IPv4,IPv6

IPv4

目前市场上的主流方案

全称:Internet Protocol version 4,互联网通信协议第四版

正式发布的是第四版,前三版没有正式发布,仅在实验室测试

采用32位地址长度,分成4组

点分十进制:每8位为一组,每组间用点隔开,方便看

在ip中没有负数,所以每一组的取值范围为0~255

如:192.168.1.5

地址分类

分为公网地址(万维网使用)和私有地址(局域网使用)

以192.168.开头的就是私有地址,专门为组织机构内部使用,以节省IP

特殊IP地址

127.0.0.1,也可以是localhost,是回送地址,也称本地回环地址,也称本机IP,永远只会找到本机

自己给自己发信息时,使用127.0.0.1,而不是局域网ip,原因:局域网ip可能会因为所在位置而不同

cmd之ping

通过ping 网址 或 ping ip地址,可以检测是否能与目标网络发送数据

IPv6

由于IPv4的表示数量有限,在2019年11月26日全部分配完毕,所以出现了IPv6

IPv5未发布就被淘汰了

全称:Internet Protocol version 6

采用128位地址长度,每16位一组,共8组

冒分十六进制:以十六进制表示,每组间用冒号隔开

如:2409:8a34:2456:6c20:85d1:6cf2:14ee:db57

0位压缩表示法:当十六进制表示为为0时,可以省略,记录为FF01::1101

InetAddress

java用来表示ip的类

子类有Inet4Address,Inet6Address

当获取该类对象时,java会根据本机的ip协议版本创建它对应的子类

// static getByName(String host) 通过ip地址或主机名获取对象
InetAddress address1 = InetAddress.getByName("192.168.1.5");
System.out.println(address1);// /192.168.1.5

InetAddress address2 = InetAddress.getByName("DESKTOP-0IP3PGU");
System.out.println(address2);// DESKTOP-0IP3PGU/192.168.1.5


// getHostName()
// 若局域网中没有该ip,返回的时ip地址
System.out.println(address1.getHostName());// DESKTOP-0IP3PGU


// getHostAddress()
System.out.println(address1.getHostAddress());// 92.168.1.5

端口号

应用程序在设备中的唯一表示

用两个字节表示的整数,取值范围0~65535

其中0~1023之间的端口号用于一些知名的网络服务或程序

协议

连接和通信的规则

OSI参考模型:世界互联协议标准,全球通新规范,但过于理想化,未广泛推广,共7层

TCP/IP参考模型(TCP/IP协议):事实上的国际标准,共分4层:应用层,传输层,网络层,物理链路层

应用层:HTTP、FTP、Telnet、DNS...

传输层:TCP、UDP...

网络层:IP、ICMP、ARP...

物理链路层:硬件设备

UDP协议

用户数据报协议(User Datagram Protocol)

UDP是面向无连接通信协议

特点:速度块,一次最多发送64K,数据不安全,易丢失数据

面向无连接:不会检查对方是否在线,直接发送,可能接收不到

用处:网络会议,视频通话,在线视频

TCP协议

传输控制协议TCP(Transmission Control Protocol)

TCP协议是面向连接的通信协议

特点:速度慢,没有大小限制,数据安全

面向连接:检查对方是否在线,连接成功才会发送数据

用户:下载软件,文字聊天,发送邮件

利用UDP协议发送数据

DatagramSocket:发送数据的对象

DatagramPacket:打包数据

一定要先运行接收端,再运行发送端,否则接收不到数据

接收数据

// 1.创建DatagramSocket对象
// 接收时一定要绑定端口
// 且端口号要与发送的端口号(打包数据时绑定的端口)保持一致
DatagramSocket ds = new DatagramSocket(10086);

// 2.接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);

ds.receive(dp);

byte[] data = dp.getData();
int length = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();

System.out.println("从" + address + "发的数据为" + new String(data,0,length) + ",该数据是从端口" + port + "发送的");


// 释放资源
ds.close();

发送数据

// 1.创建DatagramSocket对象
// 在创建时会绑定一个端口
// 若是空参,则是随机端口,也可指定端口
DatagramSocket ds = new DatagramSocket();

// 2.打包数据
// 利用字节数组传输
String str = "hello world!";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;

DatagramPacket dp = new DatagramPacket(bytes,bytes.length, address,port);

// 3.发送数据
ds.send(dp);


// 4.释放资源
ds.close();

运行结果

从/127.0.0.1发的数据为hello world!,该数据是从端口59715发送的

端口是创建DatagramSocket对象时随机的

细节

若没接收到数据,程序就会停止在ds.receive(dp)方法,知道接收到数据才会往下执行

UDP的三种通信方式

单播

一对一

之前写的就是单播

DatagramSocket

组播

一对一组

组播地址:224.0.0.0~239.255.255.255

其中224.0.0.0~224.0.0.255为预留的组播地址,只能用这些

MulticastSocket

发送端

// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket();;

// 创建DatagramPacket对象
String str = "hello MulticastSocket!";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length,address,port);

// 发送信息
ms.send(dp);


// 释放资源
ms.close();

接收端

// 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);

// 将本机加入到224.0.0.1的这一组当中
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 length = dp.getLength();

// 下面两个效果相同
System.out.println(new String(data,0,length));
System.out.println(new String(bytes,0,length));

ms.close();
广播

一对全

广播地址:255.255.255.255

DatagramSocket

与单播基本相同,只是发送时的ip不同

发送端

DatagramSocket ds = new DatagramSocket();

// 打包数据
String str = "hello MulticastSocket!";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("255.255.255.255");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length,address,port);

// 发送数据
ds.send(dp);

ds.close();

接收端

DatagramSocket ds = new DatagramSocket(10000);

// 创建DatagramPacket对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);

// 利用本机ip接收来自255.255.255.255的广播
ds.receive(dp);

System.out.println(new String(bytes,0,dp.getLength()));

ds.close();

细节:若输入流或输出流被关闭后(close),那么socket对象也会被关闭,要用socket对象中的shutOutput或shutInput方法关闭输入流或输出流

若用循环接收数据的话,直到另一方的output对象被关闭后才会停止接收数据,否则会一直接收

利用TCP协议发送数据

通信之前一定要保证连接已经建立

通过Socket产生的IO流来进行通信

客户端

// 客户端
// 创建Socket对象
// 创建时会同时连接服务端,如果连接不上会报错
// 要先启动服务器端
Socket socket = new Socket("127.0.0.1",10000);

// 获取输出流
OutputStream os = socket.getOutputStream();

os.write("你好啊".getBytes());

// 释放资源
os.close();
socket.close();

服务器端

// 服务器端
// 创建时要绑定端口
ServerSocket ss = new ServerSocket(10000);

// 监听客户端的连接
// 若无客户端连接,卡在当前位置,若连接成功,往下继续运行
Socket socket = ss.accept();


// 获取输入流对象
InputStream is = socket.getInputStream();

int b;
while ((b = is.read()) != -1){
    System.out.println((char)b);
}

// 释放资源
is.close();
socket.close();
ss.close();

但是由于InputStream的原因,会出现乱码情况

解决方案:将InputStream包装成字符流

socket底层在关闭时会将输入流与输出流

三次握手与四次挥手协议

创建Socket对象时会使用三次握手协议,以保证连接的建立

关闭Socket对象时会进行四次挥手协议,以保证通道中的数据都已经被读取完毕了

三次握手
  1. 首先客户端会发出连接请求,等待服务器确认
  2. 服务器向客户端返回一个响应,告知客户端收到了请求
  3. 客户端向服务器端再次发出确认消息,连接建立

确保连接的建立

四次挥手
  1. 客户端向服务器发出取消连接请求
  2. 服务器向客户端返回一个响应,表示收到客户端的取消请求
  3. 待服务器处理完毕数据后,向客户端发送确实取消信息
  4. 客户端向服务器端再次发送确认信息,连接取消

确保连接的断开,且数据处理完毕

UUID

用于数据传输时,保存文件时,文件名的命名,防止重复名字出现

52 反射

反射允许对封装类的字段,方法与构造函数的所有信息进行编程访问

如idea中自动提示,形参提示的功能就是用反射

可获取的信息

字段:修饰符,名字,类型,值

构造函数:修饰符,名字,形参,也用来创建对象

成员方法:修饰符,名字,形参,返回值,异常,注解(@override等),也可以运行该方法

暴力反射:通过暂时修改访问权限,以达到访问私有的字段或方法的目的

遵循java中的万物皆对象原则,构造方法也是一个对象,是Constructor的对象

字段是Field的对象成员方法是Method的对象

要从class字节码文件中获取

获取class对象

源代码阶段:java文件编译为class字节码文件

加载阶段:将class字节码文件加载到内存当中

运行阶段:在内存中创建对象

由于字节码文件的唯一性,所以获取的class文件对象的地址值是相同的

Class.forName("全类名");

使用阶段:源代码阶段,更为常用

全类名:包名+类名

以com开始

详细如下

Class<?> klass1 = Class.forName("com.nomit.study.day52.Student");
System.out.println(klass);

泛型可以删除

类名.class

使用阶段:加载阶段,一般当作参数使用,如线程锁传递的对象

详细如下

Class<Student> klass2 = Student.class;
System.out.println(klass2);

泛型可以删除

对象.getClass();

使用阶段:运行阶段,当已经有对象时才可以使用

详细如下

Student s = new Student();
Class<? extends Student> klass3 = s.getClass();
System.out.println(klass3);

获取构造方法

// 以下的打印结果,类名均是全类名,如com.nomit.study.day52.Student
// 空间有限,所以省略
Class<?> klass = Class.forName("com.nomit.study.day52.Student");
// getConstructors() 获取公共的所有构造函数的对象的数组
Constructor<?>[] cons1 = klass.getConstructors();
System.out.println(Arrays.toString(cons1));
// [public Student()]

// getDeclaredConstructions() 获取所有构造函数的对象的数组
Constructor<?>[] cons2 = klass.getDeclaredConstructors();
System.out.println(Arrays.toString(cons2));
// [public Student(), private Student(java.lang.String,int), protected Student(int), Student(java.lang.String)]
// 最后一个是不写修饰符的

// getConstructor(Class<?>...parameterTypes) 获取单个公共的构造函数的对象
Constructor<?> con3 = klass.getConstructor();
System.out.println(con3);
// public Student()

// getDeclaredConstructor(Class<?>...parameterTypes) 获取单个构造函数的数组
Constructor<?> con4 = klass.getDeclaredConstructor(String.class);
System.out.println(con4);
// Student(java.lang.String)
// 基本数据类型也可以
Constructor<?> con5 = klass.getDeclaredConstructor(int.class);
System.out.println(con5);
// protected Student(int)

获取构造函数的信息

Class<?> klass = Class.forName("com.nomit.study.day52.Student");
Constructor<?> con = klass.getDeclaredConstructor(String.class,int.class);

// getModifiers() 获取权限修饰符,返回整数
// 空白 0 public 1 private 2 protected 4
// 按照2的次方来定义,这样效率更高,原因:左移右移效率高
// 在api帮助文档中查找常量字段值,可以获取到对应的意义
int modifiers = con.getModifiers();
System.out.println(modifiers);

// getParameterCount() 获取参数数量


// getParameterTypes() 获取参数的类型


// getParameters() 获取该构造方法中所有的参数
Parameter[] parameters = con.getParameters();
System.out.println(Arrays.toString(parameters));
// [java.lang.String arg0]


// newInstance(参数) 创建对象
// 要求:参数必须是获取时传入的类型
// 不能用private等修饰符修饰的构造方法创建
// 但可以使用setAccessible(true)临时取消权限的认证
// 但取消权限验证被称为暴力反射
con.newInstance("张三",23);// 报错

con.setAccessible(true);
con.newInstance("张三",23);

获取字段

// 同样省略全类名
Class<?> klass = Class.forName("com.nomit.study.day52.Student");
// getFields() 返回所有公共字段对象的数组
Field[] fields1 = klass.getFields();
System.out.println(Arrays.toString(fields1));
// [public Student.address]

// getDeclaredFields() 返回所有字段对象的数组
Field[] fields2 = klass.getDeclaredFields();
System.out.println(Arrays.toString(fields2));


// getField(String name) 返回单个公共字段对象
// 不存在会报错,不是公共的也会报错,NoSuchFieldException
Field address = klass.getField("address");
System.out.println(address);

// getDeclaredField(String name) 返回单个字段对象
Field gender = klass.getDeclaredField("gender");
System.out.println(gender);

获取字段的信息

Class<?> klass = Class.forName("com.nomit.study.day52.Student");
Field gender = klass.getDeclaredField("gender");

// getModifiers() 获取修饰符
// 与构造函数的相同
int modifiers = gender.getModifiers();
System.out.println(modifiers);

// getName() 获取变量名
String name = gender.getName();
System.out.println(name);

// getType() 获取数据类型
Class<?> type = gender.getType();
System.out.println(type);

// get(对象) 获取对象的变量的值
// 但受修饰符的限制
// 使用setAccessible(true)
Student student = new Student("张三", 23, "男");

gender.setAccessible(true);
Object value = gender.get(student);
System.out.println(value);

// set(对象,value) 修改对象的变量的值
gender.set(student,"女");
System.out.println(student);

获取成员方法

Class<?> klass = Class.forName("com.nomit.study.day52.Student");
// getMethods() 获取所有公共的成员方法对象的数组,包括继承的
Method[] methods1 = klass.getMethods();
System.out.println(Arrays.toString(methods1));
// 有如:public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
// 只是数组中的其中一个方法,这是继承的

// getDeclaredMethods() 获取所有的成员方法对象的数组,不包括继承的
Method[] methods2 = klass.getDeclaredMethods();
System.out.println(Arrays.toString(methods2));

// getMethod(String name,Class<?>...parameterTypes)
Method eat = klass.getMethod("eat", String.class);
System.out.println(eat);

// getDelcaredMethod(String name,Class<?>...parameterTypes)
// 同理,不过多展示

获取成员方法的信息

Class<?> klass = Class.forName("com.nomit.study.day52.Student");
Method m = klass.getMethod("eat", String.class);

// getModifiers()
int modifiers = m.getModifiers();
System.out.println(modifiers);

// getName()
String name = m.getName();
System.out.println(name);

// 形参
int parameterCount = m.getParameterCount();
Class<?>[] parameterTypes = m.getParameterTypes();
Parameter[] parameters = m.getParameters();

// getExceptionTypes() 获取抛出的异常类型
Class<?>[] exceptionTypes = m.getExceptionTypes();

// invoke(Object obj,Object...args) 运行方法
// 返回值为方法的返回值,
// 第一个参数是方法的调用者,第二个参数是实参
// 若是私有,取消访问权限
m.setAccessible(true);
Object invoke = m.invoke(new Student(), "面条");
System.out.println(invoke);