JAVA初学下(仅做笔记)

目录

一. Map集合(双列集合)

1.特点

2.常见API

2.1基本功能

2.2Map集合的获取功能

3.对于接口内的接口很懵,回顾一下过去的内部类

3.1成员内部类

 3.2ArrayList的Iterator  是成员内部类

3.2内部类怎样 使用外部类 的成员变量,成员方法。

4.遍历方式

4.1通过 键 去找 值

4.2通过 键值对对象 来获取 键和值

4.3通过lambda表达式遍历 双列集合

二. Map接口 的实现类

 1.HashMap

1.1HashMap特点

1.2 练习1

1.3 回顾 怎样重写 equals()方法

1.4发现了个 private的 有趣的事

 2.LinkedHashMap

2.1特点

 3.TreeMap

3.1简单介绍

3.2练习1

 3.3 看视频时的纠错

 3.4练习2

 3.5小总结

4.HashMap源码(记得手动实现)

5.TreeMap源码

三.可变参数和集合工具类

1.1简单使用

1.2注意点

1.3总结

2.Collections  工具类

2.1 Collections.addAll(实现Collections 接口的类或子类 )

2.2 Collections.shuffle();

2.3public static void sort(List list, Comparator c) 排序

2.4 不要弄混 接口的多态  与   泛型的继承

2.5Collections.swap();  交换集合中两个元素的值

2.6 对于Collections.copy(),有一个小坑

3.练习

3.1不重复的随机点名

3.2带权重的随机点名项目

3.3集合的嵌套

四.斗地主综合项目

1.控制台

五. 不可变集合

1.List接口的 静态of方法

2.Set接口的of方法

3.Map接口的of()方法和ofEntries()方法,copyOf方法

3.1未超过10个键值对

3.2超过10个键值对

六.Stream流

1.Stream流的作用 和 获取方式

1.1单列集合的stream流的获取

1.2双列集合获取stream 流的方式

1.3数组获取stream流

1.4零散数据获取steam流

2.Stream流的中间方法

2.1filter  过滤

2.2skip

2.3limit

2.4distinct去重  (依赖equals方法和hashCode方法)

2.5 Stream类的静态方法  concat  (易错)

2.6 map方法

3.终结方法

3.1 forEach 和 count

3.2 toArray()    :有些难度

3.3Stream的collect()收集方法

七.方法引用

1.引用静态方法

2.引用其他类的成员方法

3.引用本类或父类的成员方法

4.引用构造器的方法

5.类名::非静态成员方法(很神奇,有些特殊,有他独特的规则) 

6.引用数组的构造方法​编辑

 7.总结

有所感悟:

八.异常

1.异常体系介绍

 2.编译时异常和运行时异常

3.异常在代码中的两个作用

 4.异常的处理方式

5.灵魂四问

 6.Throwable的成员方法

7.抛出异常

 8.小总结​编辑

9.自定义异常

九.File

1.File的概述和构造方法

 2.File的成员方法(获取,判断)

 2.1     File类 length()成员方法的注意点

 2.2 getName()注意点

 2.3 lastModified方法 与 时间类的结合

3.创建,删除

 3.1创建文件 createNewFile

 3.2创建单级文件夹和多级文件夹​编辑

3.3删除文件,文件夹

 4.获取并遍历

4.1 listRoots()  获取所有盘符

4.2 list()

4.3 list(FilenameFilter filter)

4.4 listFiles()

获取当前路径下的所有内容。注意点在上面。

4.5 listFiles(FilenameFilter  filter)

4.6 listFiles(FileFilter  filter)

5.练习

十.IO流

1.IO流概述

2.字节输出流(书写到外部的文件上)

 2.1字节输出流 主要用法

2.2 写 数据的三种方式

2.3 换行与续写

3.字节输入流的基本用法

3.1循环读取

4. 文件拷贝

4.1一个字节一个字节的拷贝

 4.1一次读取多个字节

5.字符集详解

5.1概述

5.2 GBK

 5.3Unicode字符集

5.4 ANSI编码

6.为什么会有乱码

7.编码和解码

8.字符输入流

8.1FileReader

9.字符输出流 FileWritter

 10.字符输入流和输出流原理

11.综合练习

练习1:拷贝文件夹

练习2:加密

练习3(回首往事):

② String类 的 replace 和 replaceAll

12.字节缓冲流

12.1 简要介绍

12.2 字节缓冲输入输出流​编辑​编辑

12.3 字符缓冲输入输出流

 12.4 字符输入输出流的特有方法

12.5小总结

12.6 综合练习3(有意思)

13.转换流

13.1转换流基本用法

13.2综合练习

13.3综合练习(太妙了。这种多态)

13.4 额外扩展

14.序列化流

14.1课上

14.2 实操一下

15.反序列化

16.序列化和反序列化的使用细节

瞬态关键字 :  transient

16.2 综合练习:读写多个对象    (值得一看)

17.打印流

17.1字节打印流(底层没有缓冲区)

​编辑

 ​编辑

 17.2 字符打印流

17.3 打印流与sout 的特殊关系

17.4总结

18.解压缩流

19.压缩流

 19.1压缩单个文件

正确的在下面

19.2 压缩文件夹

20.工具包

20.1 hutool

20.1 common.io


一. Map集合(双列集合)

1.特点

键不能重复,值可以重复

Map接口位于最高层

2.常见API

2.1基本功能

    

    

①注意V put(K key,V value)这个方法,

当加入 的键值对元素的键(key) 不存在时,就会将 键值对 加到双列集合中,此时返回值为null

当加入 的键值对元素 的键(key)已在双列集合中存在时 ,就会 覆盖原有 键(key) 对应的值(value) ,并将value返回。

2.2Map集合的获取功能

3.对于接口内的接口很懵,回顾一下过去的内部类

3.1成员内部类

①两种 获取 成员内部类对象的方式

②Iterator迭代器是 集合的内部类,通过调用方法来获取对象。

 

 

 3.2ArrayList的Iterator  是成员内部类

3.2内部类怎样 使用外部类 的成员变量,成员方法。

注意Outer是一个类名,它是可以改变的,用 类名.this 来调用外部类的成员变量或方法

 3.3静态内部类

记得加new 后加括号,因为 此时是 创建内部类的对象,可以想象,平常创建对象的时候都会加括号,跟构造方法有关。

4.遍历方式

4.1通过 键 去找 值

通过 Map的keySet()方法可以获得 包含所有key的一个单列集合

 public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("海绵宝宝", 1);
        map.put("派大星", 2);
        map.put("章鱼哥", 3);
        //键找值
        //1.获取所有的键(key),放在一个单列集合中
        Set<String> myKeySet = map.keySet();
        //通过迭代器遍历单列集合,并用Map的get(KEY k)方法来获取
        Iterator<String> it = myKeySet.iterator();
        while (it.hasNext()) {
            String key = it.next();
            Integer value = map.get(key);
            System.out.println("key:" + key + "value:" + value);
        }
        //通过增强for遍历
        for (String key : myKeySet) {
            Integer value = map.get(key);
            System.out.println("key:" + key + "value:" + value);
        }
        //使用lambda表达式
        myKeySet.forEach(
                key -> {
                    Integer value = map.get(key);
                    System.out.println("key:" + key + "value:" + value);
                }
        );
    }

4.2通过 键值对对象 来获取 键和值

 public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("海绵宝宝", 1);
        map.put("派大星", 2);
        map.put("章鱼哥", 3);
        //Entry接口是 Map接口的内部接口,所以要用Map.Entry
        //这和内部类很像的,外部类.内部类  对象名=new 外部类().内部类()
        Set<Map.Entry<String, Integer>> entries = map.entrySet();
        //通过迭代器去遍历单列集合,不过这时候里面的类型是 Entry接口,
        // 这里使用接口的多态,为了方便学习该接口的一些方法
        Iterator<Map.Entry<String, Integer>> it = entries.iterator();
        while (it.hasNext()) {

            Map.Entry<String, Integer> entry = it.next();
            Integer value = entry.getValue();
            String key = entry.getKey();
            System.out.println(key + " = " + value);
        }
//增强for遍历 遍历单列集合
        for (Map.Entry<String, Integer> entry : entries) {
            Integer value = entry.getValue();
            String key = entry.getKey();
            System.out.println(key + " = " + value);
        }
        //lambda表达式遍历单列集合
        entries.forEach(
                stringIntegerEntry -> {
                    System.out.println(stringIntegerEntry.getKey() + " = " + stringIntegerEntry.getValue());

                });
    }

4.3通过lambda表达式遍历 双列集合

前面我们通过 键找值     键值对对象    两种方式 来获取 获取一个单列集合 并对其进行遍历。

现在,我们可以直接 里面 Map的forEach()方式来直接获取key和value。

对于Map的forEach的底层原理,其实是利用 增强for来完成的。

 Map<String, Integer> map = new HashMap<>();
        map.put("海绵宝宝", 1);
        map.put("派大星", 2);
        map.put("章鱼哥", 3);

map.forEach(new BiConsumer<String, Integer>() {
            @Override
            public void accept(String key, Integer value) {
                System.out.println(key + " = " + value);
            }
        });

二. Map接口 的实现类

 1.HashMap

1.1HashMap特点

 ①利用 key去计算哈希值,如果Key是 自定义引用类型 因此需要重写key的hashCode方法,如果不是自定义类型,不需要重写。

②在同一索引下有多个元素,当被添加的元素的哈希值通过计算得到的index为该索引时,就会 比较键(key)是否相同,若相同则 将要添加的元素的值(value)会 覆盖  原来集合中存在的键(key)的值。 

若key与索引下的所有元素的key不同,则会将key挂在链表下面。

在单列集合中 ,通过比较属性值是否相等来去重。

因此当自定义数据类型时,需要重写键(key)的equals方法。

1.2 练习1

1.3 回顾 怎样重写 equals()方法

困惑如下:

我自己定义的类的重写方法的代码好像跟 java自己的类的equals()方法的代码不太一样,

java中使用  变量 instanceof 类型 ,来判断他属于哪个类型,但我定义的类用的是 getClass()

1.4发现了个 private的 有趣的事

当我使用Object o作为形参时 ,强转后,可以直接 obj.name ,不需要使用 getName();

很有趣,因为此时equals方法 处于BeautySpot类的内部,因此对于任意的BeautySpot的对象,都可以在其中 直接 使用 对象.属性  来访问属性,毕竟这是在 类的内部 。

 

 2.LinkedHashMap

2.1特点

 3.TreeMap

3.1简单介绍

3.2练习1

对于升序排序,使用 键对应的类 中默认的compareTo()方法

对于打破  实现comparable接口 的类的 compareTo()方法,可以在创建对象时 用 comparator比较器

 

 3.3 看视频时的纠错

如下图,但返回值为0时,并不是舍弃,而是覆盖,被添加元素的key与 TreeSet集合中某个元素的key相等时,就会覆盖其 Value

 3.4练习2

 3.5小总结

4.HashMap源码(记得手动实现)

先进入HashMap类,按ctrl  f12  查看本类的method,filed和子类

5.TreeMap源码

三.可变参数和集合工具类

1.1简单使用

 public static void main(String[] args) {
        //计算n个数的值  (两种方法)
        //1.创建长度为n的数组
        int arr[] = {1, 2, 3, 4, 5, 6};
        //可变参数作为形参,
        System.out.println(getSum(arr));
        System.out.println(sum(1, 2, 3, 4, 5));
        System.out.println(sum(arr));


    }

    public static int getSum(int[] arr) {
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        return sum;
    }
    //可变参数格式:  类型 ...  变量名
    //可变参数底层是一个数组,只不过不需要我们创建,java创建好了

    public static int sum(int... arr) {
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }
        return sum;
    }

1.2注意点

①可变参数,在方法中只能有 一个可变参数。

②当有多个形参时,可变参数 必须放在后面。

1.3总结

2.Collections  工具类

2.1 Collections.addAll(实现Collections 接口的类或子类 )

①源码用了 ?super T      ,这是因为泛型不能继承,得用?super  来解决这种错误。

②用了 可变参数,很有趣

ArrayList<Student> stuList = new ArrayList<>();
        Student s = new Student("bit", 10);
        Student s2 = new Student("catch", 10);
        Student s3 = new Student("apple", 10);
        //Collections.addAll()  批量添加实现Collections接口的集合
        // 为什么能一次添加多个?因为他用了剩余参数
        Collections.addAll(stuList, s, s2, s3);

2.2 Collections.shuffle();

//shuffle()打乱List集合元素的顺序

先看下源码:

shuffle_threshold   当集合大小超过它时,就要转化为数组来 进行swap()交换

   创建Random等会 会用到

 底层 方法重载

 

 通过交换两个数的位置进行打乱。

动手实践下:

 ArrayList<Student> stuList = new ArrayList<>();
        Student s = new Student("bit", 10);
        Student s2 = new Student("catch", 10);
        Student s3 = new Student("apple", 10);
        //Collections.addAll()  批量添加实现Collections接口的集合
        // 为什么能一次添加多个?因为他用了剩余参数
        Collections.addAll(stuList, s, s2, s3);
        System.out.println(stuList);
        //shuffle()打乱List集合元素的顺序
        Collections.shuffle(stuList);
        System.out.println(stuList);
        Collections.shuffle(stuList, new Random());
        System.out.println(stuList);

2.3public static <T> void sort(List<T> list, Comparator<? super T> c) 排序

①只能对 实现List 接口的类 使用 Collections.sort()方法。

Collections,sort()有两个重载方法,

第一个是 默认排序 ,有一个参数(实现comparable接口,重写compareTo()方法)

第二个是 指定排序,有两个参数,使用比较器comparator。

②也可以使用实现List 接口的类自带的sort方法,例如ArrayList的sort方法,LinkdeList的sort()方法,但它要求必须要有参数。

当传进去的参数值为null,按默认排序(<>中的类实现comparable接口,重写compareTo()方法)

当传进去的参数值为实现comparator接口的类,则通过重写其compare()方法,进行排序。

2.4 不要弄混 接口的多态  与   泛型的继承

2.5Collections.swap();  交换集合中两个元素的值

①源码:

   @SuppressWarnings({"rawtypes", "unchecked"})
    public static void swap(List<?> list, int i, int j) {
        // instead of using a raw type here, it's possible to capture
        // the wildcard but it will require a call to a supplementary
        // private method
        final List l = list;
        l.set(i, l.set(j, l.get(i)));
    }

2.6 对于Collections.copy(),有一个小坑

如果 源集合 的长度  大于 目的集合的长度  就会报错。

有点反人类。

3.练习

3.1不重复的随机点名

Collections的addAll()方法  和 集合自带的addAll()方法 不太相似。

一个使用可变参数,另一个传入 要添加的集合

 //1.定义集合
        ArrayList<String> list = new ArrayList<>();
        //2.添加数据
        Collections.addAll(list, "范闲", "范建", "范统", "杜子腾", "杜琦燕", "宋合泛", "侯笼藤", "朱益群", "朱穆朗玛峰", "袁明媛");
        //3.随机点名
        Random r = new Random();
        //把全部人员都点一次名

        ArrayList<String> copyList = new ArrayList<>();
        int listSize = list.size();
        for (int j = 0; j < 3; j++) {

            for (int i = 0; i < listSize; i++) {
                int index = r.nextInt(list.size());
                String name = list.remove(index);
                copyList.add(name);
                System.out.println(name);
            }
            System.out.println("-------------------");
            list.addAll(copyList);
        }

3.2带权重的随机点名项目

暂时不写

3.3集合的嵌套

 public static void main(String[] args) {
        /* 需求
        定义一个Map集合,键用表示省份名称province,值表示市city,但是市会有多个。
        添加完毕后,遍历结果格式如下:
                江苏省 = 南京市,扬州市,苏州市,无锡市,常州市
                湖北省 = 武汉市,孝感市,十堰市,宜昌市,鄂州市
                河北省 = 石家庄市,唐山市,邢台市,保定市,张家口市*/


        //1.创建Map集合
        HashMap<String, ArrayList<String>> hm = new HashMap<>();

        //2.创建单列集合存储市
        ArrayList<String> city1 = new ArrayList<>();
        city1.add("南京市");
        city1.add("扬州市");
        city1.add("苏州市");
        city1.add("无锡市");
        city1.add("常州市");

        ArrayList<String> city2 = new ArrayList<>();
        city2.add("武汉市");
        city2.add("孝感市");
        city2.add("十堰市");
        city2.add("宜昌市");
        city2.add("鄂州市");

        ArrayList<String> city3 = new ArrayList<>();
        city3.add("石家庄市");
        city3.add("唐山市");
        city3.add("邢台市");
        city3.add("保定市");
        city3.add("张家口市");

        //3.把省份和多个市添加到map集合
        hm.put("江苏省", city1);
        hm.put("湖北省", city2);
        hm.put("河北省", city3);

        System.out.println(hm);
        for (Map.Entry<String, ArrayList<String>> entry : hm.entrySet()) {
            String key = entry.getKey();
            ArrayList<String> value = entry.getValue();

            //空字符串表示不添加前缀和后缀
            StringJoiner sk = new StringJoiner(",", "", "");
            for (String s : value) {
                sk.add(s);
            }
            System.out.println(key + "=" + sk);

        }

    }

四.斗地主综合项目

1.控制台

 

2.图像界面

集合的嵌套,像是数组一样,很有趣。

//ArrayList集合的嵌套像是数组 一样。0索引上是一个集合,1索引上也是一个集合。
    ArrayList<ArrayList<Poker>> currentList = new ArrayList<>();

五. 不可变集合

of方法是接口中的静态方法

1.List接口的 静态of方法

 int maxSize = 10;
        ArrayList<String> arrayList = new ArrayList<>();
        for (int i = 0; i < maxSize; i++) {
            arrayList.add("海绵宝宝");
        }
        //创建不可变集合
        //1.因为List接口的形参为可变参数,可以无限传进去 引用数据类型,也可以传进去一个引用数据类型的数组
        List<String> immutableList = List.of("a", "b", "c", "d");
        System.out.println(immutableList);
        //2.给不可变数组传进去 一个数组 。
        //可以将 ArrayList类型的集合 转化为 数组。它的转化过程很有趣,
        //arrayList.toArray()  如果不写参数的话,会返回一个Object数组
        //arrayList.toArray(new String[0]) 则会返回一个String[]数组,
        // 底层会判断new String[0] 这个创建数组的长度是否大于集合的大小,看下源码就懂了。
        List<String> immutableList2 = List.of(arrayList.toArray(new String[0]));
        System.out.println(immutableList2);

ArrayList集合 的toArray()方法的源码

2.Set接口的of方法

public class SetDemo {
    public static void main(String[] args) {
        int maxSize = 10;
        HashSet<String> hashSet = new HashSet<>();
        for (int i = 0; i < maxSize; i++) {
            hashSet.add("a" + i);
        }
        System.out.println(hashSet);
        //像List接口一样,它的参数为可变参数。
        //可以传进去 一个引用数据类型数组,或传进去很多很多的同一引用数据类型的值
        Set<String> immutableSet = Set.of("a", "b", "c", "d", "e", "f", "g");
        Set<String> immutableSet2 = Set.of(hashSet.toArray(new String[0]));

    }
}

3.Map接口的of()方法和ofEntries()方法,copyOf方法

3.1未超过10个键值对

当超过10个键值对后,就不能使用of()方法了

public class ImmutableDemo3 {
    public static void main(String[] args) {
       /*
        创建Map的不可变集合
            细节1:
                键是不能重复的
            细节2:
                Map里面的of方法,参数是有上限的,最多只能传递20个参数,10个键值对
            细节3:
                如果我们要传递多个键值对对象,数量大于10个,在Map接口中还有一个方法
        */

        //一旦创建完毕之后,是无法进行修改的,在下面的代码中,只能进行查询操作
        Map<String, String> map = Map.of("张三", "南京", "张三", "北京", "王五", "上海",
                "赵六", "广州", "孙七", "深圳", "周八", "杭州",
                "吴九", "宁波", "郑十", "苏州", "刘一", "无锡",
                "陈二", "嘉兴");

        Set<String> keys = map.keySet();
        for (String key : keys) {
            String value = map.get(key);
            System.out.println(key + "=" + value);
        }

        System.out.println("--------------------------");

        Set<Map.Entry<String, String>> entries = map.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key + "=" + value);
        }
        System.out.println("--------------------------");
    }
}

3.2超过10个键值对

public class ImmutableDemo4 {
    public static void main(String[] args) {

        /*
            创建Map的不可变集合,键值对的数量超过10个
        */

        //1.创建一个普通的Map集合
        HashMap<String, String> hm = new HashMap<>();
        hm.put("张三", "南京");
        hm.put("李四", "北京");
        hm.put("王五", "上海");
        hm.put("赵六", "北京");
        hm.put("孙七", "深圳");
        hm.put("周八", "杭州");
        hm.put("吴九", "宁波");
        hm.put("郑十", "苏州");
        hm.put("刘一", "无锡");
        hm.put("陈二", "嘉兴");
        hm.put("aaa", "111");

        //2.利用上面的数据来获取一个不可变的集合
/*
        //获取到所有的键值对对象(Entry对象)
        Set<Map.Entry<String, String>> entries = hm.entrySet();
        //把entries变成一个数组
        Map.Entry[] arr1 = new Map.Entry[0];
        //toArray方法在底层会比较集合的长度跟数组的长度两者的大小
        //如果集合的长度 > 数组的长度 :数据在数组中放不下,此时会根据实际数据的个数,重新创建数组
        //如果集合的长度 <= 数组的长度:数据在数组中放的下,此时不会创建新的数组,而是直接用
        Map.Entry[] arr2 = entries.toArray(arr1);
        //不可变的map集合
        Map map = Map.ofEntries(arr2);
        map.put("bbb","222");*/


        //Map<Object, Object> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));

        Map<String, String> map = Map.copyOf(hm);
        map.put("bbb","222");
    }
}

六.Stream流

1.Stream流的作用 和 获取方式

 

 

1.1单列集合的stream流的获取

Collection接口中的默认方法

 public static void main(String[] args) {
        //单列集合获取Stream流
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "a", "b", "c", "d");
        //获取到一条流水线,并把数据放到流水线上
        Stream<String> stream1 = list.stream();
        //使用终结方法 打印一下流水线上的所有数据
        stream1.forEach(s -> System.out.println(s));

        //上面的过程可以简化,Stream流的中间方法返回值还是Stream<T>
        //但使用终结方法,返回值为void,后面不能跟使用链式编程
        //简化过程
        list.stream().forEach(s -> System.out.println(s));
    }

1.2双列集合获取stream 流的方式

 public static void main(String[] args) {
        //创建双列集合
        HashMap<String, Integer> hm = new HashMap<>();
        hm.put("aaa", 1);
        hm.put("bbb", 2);
        hm.put("ccc", 3);
        hm.put("ddd", 4);

        //1.第一种获取双列集合的方式,使用双列集合的keySet()方法,然后调用单列集合的stream()
        //可通过键找值。
        hm.keySet().stream().forEach(s -> System.out.println(s));

        //2.第二种方式,使用双列集合的entrySet()方法,
        //得到键值对对象
        hm.entrySet().stream().forEach(s -> System.out.println(s));
        
    }

1.3数组获取stream流

引用和基本 数据类型 都可以使用  Arrays工具类的 steam(数组)方法获取Stream流

//数组通过Arrays工具类的静态方法获取Stream流
        //引用数据类型和基本数据类型 的数组 都可以用。
        //与后面零散数据 获取Stream流相区别
        int[] arr = {2, 4, 3, 6, 9, 7, 1};
        Arrays.stream(arr)
                .forEach(i -> System.out.println(i));

1.4零散数据获取steam流

  public static void main(String[] args) {
        //零散数据的类型需要一致,而且是引用数据类型
        //不能狗使用 基本数据类型
        Stream.of("a", "b", "c", "d").forEach(s -> System.out.println(s));
    }

2.Stream流的中间方法

2.1filter  过滤

public class MyStream3 {
    public static void main(String[] args) {
//        Stream<T> filter(Predicate predicate):过滤
//        Predicate接口中的方法	boolean test(T t):对给定的参数进行判断,返回一个布尔值

        ArrayList<String> list = new ArrayList<>();
        list.add("张三丰");
        list.add("张无忌");
        list.add("张翠山");
        list.add("王二麻子");
        list.add("张良");
        list.add("谢广坤");

        //filter方法获取流中的 每一个数据.
        //而test方法中的s,就依次表示流中的每一个数据.
        //我们只要在test方法中对s进行判断就可以了.
        //如果判断的结果为true,则当前的数据留下
        //如果判断的结果为false,则当前数据就不要.
//        list.stream().filter(
//                new Predicate<String>() {
//                    @Override
//                    public boolean test(String s) {
//                        boolean result = s.startsWith("张");
//                        return result;
//                    }
//                }
//        ).forEach(s-> System.out.println(s));

        //因为Predicate接口中只有一个抽象方法test
        //所以我们可以使用lambda表达式来简化
//        list.stream().filter(
//                (String s)->{
//                    boolean result = s.startsWith("张");
//                        return result;
//                }
//        ).forEach(s-> System.out.println(s));

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

    }
}

2.2skip

跳过前几个元素,跟索引无关。

2.3limit

截至到前几个元素

2.4distinct去重  (依赖equals方法和hashCode方法)

2.5 Stream类的静态方法  concat  (易错)

错误示范,如下图,我直接把集合传进去。

 正确做法:获取集合的stream流,并将其作为参数传进去

2.6 map方法

把一种数据类型,转化为另一种数据类型,并且数据的内容也可以改变。

 public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();

        Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8);

        //list.stream().distinct().forEach(i -> System.out.println(i));

        list.stream().map(new Function<Integer, String>() {


            /**
             * @param integer the function argument
             * @return
             */
            @Override
            public String apply(Integer integer) {
                return integer.toString() + "a";
            }
        }).forEach(s -> System.out.println(s));

3.终结方法

3.1 forEach 和 count

  ArrayList<String> list = new ArrayList<>();
        list.add("张三丰");
        list.add("张无忌");
        list.add("张翠山");
        list.add("王二麻子");
        list.add("张良");
        list.add("谢广坤");
        //count方法
        long size = list.stream().count();
        System.out.println(size);
        //forEach()终结方法  ,众所周知

3.2 toArray()    :有些难度

容易写错new InputFunction<? extends  Objecr>  ,

3.3Stream的collect()收集方法

 

 ①可将流中的数据收集到  List集合

   List<String> list2 = list
                .stream()
                .collect(Collectors.toList());
        //上面这个 collect(Collectors.toList()),是可以简化为 toList()的,虽然很简便,但它是jdk10的特性

②将流中的数据收集到      Set集合

 //将Stream流中的数据放到Set集合当中
//list是ArrayList<String> 类型
        Set<String> set = list.stream()
                .collect(Collectors.toSet());

③将流中的数据收集到      Map集合

 public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("张三丰");
        list.add("张无忌");
        list.add("张翠山");
        list.add("王二麻子");
        list.add("张良");
        list.add("谢广坤");

 //将Stream流中的数据放到Map结合中
        Map<String, Integer> map = list.stream().collect(Collectors.toMap(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        }, new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
             
                return 1;
            }
        }));

七.方法引用

 

1.引用静态方法

 

2.引用其他类的成员方法

对于其他类 ,需要 创建其他类的对象,然后  其他类对象::方法名

 

 

3.引用本类或父类的成员方法

 静态方法里也没有super关键字。

 如果在静态方法使用 方法引用,则不能直接使用 this::方法名 ,而是需要像对其他类的方法引用,需要创建类的对象,再用对象::方法

4.引用构造器的方法

在引用方法时,方法需要满足三个条件

①方法须已存在

②方法的形参和返回值与 将要重写的抽象方法里的一致

③方法的功能满足目前的需求

可能有点困惑,构造方法怎么会有返回值呢?

其实,只要抽象方法的返回值的 类型 与构造方法创建的对象的类型一致就行。

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public interface StudentBuilder {
    Student build(String name,int age);
}

public class StudentDemo {
    public static void main(String[] args) {

		//Lambda简化写法
        useStudentBuilder((name,age) -> new Student(name,age));

        //引用构造器
        useStudentBuilder(Student::new);

    }

    private static void useStudentBuilder(StudentBuilder sb) {
        Student s = sb.build("林青霞", 30);
        System.out.println(s.getName() + "," + s.getAge());
    }
}

5.类名::非静态成员方法(很神奇,有些特殊,有他独特的规则) 

抽象方法第一个参数的类型决定了它可以  通过 它的类::它的非静态成员方法

6.引用数组的构造方法

 7.总结

有所感悟:

接口的实现类   是可以作为接口的实参传进去。

像下面的addActionListener(this)的this,便是实现类对象的地址。

当我们使用匿名内部类时,就相当于 创建了接口 的实现类对象,重写了方法。

八.异常

1.异常体系介绍

 

 2.编译时异常和运行时异常

 

3.异常在代码中的两个作用

 

 4.异常的处理方式

5.灵魂四问

 注意是一个 |  符号

 6.Throwable的成员方法

 printStackTrace()是最常用的方法

下面我写的代码不太对,Exception类是 Throwable的子类,其实不用Throwable t=e;

直接用 e.printStackTrace() 就行。

 

7.抛出异常

throws 和throw有点难分清。

throw 好像只起到 字面 作用,让程序员知道可能会有某个异常发生,但与程序的执行无关。

 8.小总结

 

9.自定义异常

 

九.File

1.File的概述和构造方法

 2.File的成员方法(获取,判断)

 2.1     File类 length()成员方法的注意点

通过length()方法不能获取文件夹的大小

 2.2 getName()注意点

 2.3 lastModified方法 与 时间类的结合

①Instant类 只能获取 标准时间

②因此需要用 ZonedDateTime 类 去改变时区。

       String path = "C:\\Users\\la2003\\OneDrive\\桌面\\test.txt";
        File f1 = new File(path);
        long time = f1.lastModified();
        Instant i = Instant.ofEpochMilli(time);
        ZonedDateTime zt = i.atZone(ZoneId.systemDefault());
        System.out.println(zt);

3.创建,删除

 3.1创建文件 createNewFile

我刚开始写时有点懵,如果是创建新文件的话,path这个路径一定是不存在的,因此在使用f.exists()方法时,返回值为fasle

创建的一定是文件,如果没有后缀名,则会创建一个没有后缀名的文件夹。

 3.2创建单级文件夹和多级文件夹

 在同一目录下的 文件 和 文件夹 不能重名。

3.3删除文件,文件夹

 4.获取并遍历

 

注意:

listFiles() 和list() 虽然都是 获取当前路径下 的所有内容

但是listFiles()会将      当前的路径当前路径下的文件文件夹名 打印出来。

而list()   只会 把          当前路径下文件文件夹名 打印出来

4.1 listRoots()  获取所有盘符

4.2 list()

4.3 list(FilenameFilter filter)

 

4.4 listFiles()

获取当前路径下的所有内容。注意点在上面。

4.5 listFiles(FilenameFilter  filter)

参数1:父级路径 ,

       2:文件夹名/文件名

4.6 listFiles(FileFilter  filter)

参数1 :父级路径 +文件夹名/文件名

5.练习

①忘记  利用getName()方法 获取文件的名字

②对于 listFiles()方法,

当它是文件时,它调用此方法会返回null

当它是文件夹,文件夹下没有内容时,他会返回一个长度为0的数组。

当它是文件夹,而文件夹下的内容没有权限访问时,它会返回null

③删除文件夹时,好像不用做非空判断,因为它是先一级一级的删除文件

④利用length()方法获得 文件大小

⑤正则表达式中  .表示一个字符, 要想表示真正的点 必须用 \\.

                                         递归查找文件(下图)

 递归删除文件

获得目录下所有文件总大小

public class Demo7 {
    public static void main(String[] args) {
        //String path = "C:\\Users\\la2003\\OneDrive\\桌面\\二十大征文";
        String path = "C:\\Users\\la2003\\OneDrive\\桌面\\二十大征文";
        File f = new File(path);
        int sum = getExistSpace(f);
        System.out.println(sum / 1024 / 1024);
    }

    public static int getExistSpace(File path) {
        int sum = 0;
        File[] arr = path.listFiles();
        for (File file : arr) {
            if (file.isFile()) {
                sum += file.length();
            } else {
                sum += getExistSpace(file);
            }
        }

        return sum;

    }

十.IO流

1.IO流概述

 

2.字节输出流(书写到外部的文件上)

 2.1字节输出流 主要用法

 

2.2数据的三种方式

2.3 换行与续写

 

public class Demo1 {
    public static void main(String[] args) throws IOException {
        String path = "directoryTest/liu.txt";
        File f = new File(path);
        //如果外部文件中有内容,通过更改第二个参数为true,然后写入时就不会清空原来文件中的内容,会追加上去。
        FileOutputStream fos = new FileOutputStream(f, true);
        byte[] b = {66, 67, 68, 69, 70};
        //每次写入都会刷新文件内的值
        fos.write(b);

        fos.write(new byte[]{'\r', '\n'});
        fos.write(b);
        fos.close();

    }

3.字节输入流的基本用法

3.1循环读取

 

4. 文件拷贝

4.1一个字节一个字节的拷贝

占用内存小,但效率低。

 public static void main(String[] args) throws IOException {
        //定义输入流
        FileInputStream fis = new FileInputStream("C:\\Users\\la2003\\OneDrive\\桌面\\软件工程课件\\清风图标.png");

        //定义输出流
        FileOutputStream fos = new FileOutputStream("directoryTest/1.png");

        int read;
        //读入数据
        while ((read = fis.read()) != -1) {
            //写入数据
            fos.write((char) read);

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

    }

 4.1一次读取多个字节

由于数组 会占用内存,因此数组的长度不应太长。

 但注意 byte[ ] buffer的覆盖问题 ,每次读取外部文件,都会将byte[ ]中的内容覆盖。

但是,当byte[ ] buffer  只能覆盖第一个位置的值, 那么其它位置的值仍为原来的,未被覆盖。

所以,要时刻关注 读入 byte[ ] buffer 中  元素的长度。

 public static void main(String[] args) throws IOException {
        //定义输入流
        FileInputStream fis = new FileInputStream("directoryTest/liu.txt");

        //定义输出流
        FileOutputStream fos = new FileOutputStream("directoryTest/1.txt");

        byte[] buffer = new byte[3];
        int len;
        //读入数据
        while ((len = fis.read(buffer)) != -1) {
            //写入数据
            //记得使用 三个参数的write 方法 ,从0开始,所要拷贝长度为len。(注意,len不是末尾索引)
            fos.write(buffer, 0, len);
            System.out.println(len);
        }

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

    }

5.字符集详解

5.1概述

 

5.2 GBK

 5.3Unicode字符集

出现UTF-8编码规则

 

5.4 ANSI编码

ANSI(American National Standards Institute)编码通常指美国国家标准协会制定的字符编码标准。在计算机领域中,ANSI编码是指Windows操作系统在本地代码页中所使用的字符编码。

在Windows中,ANSI编码通常代表单字节编码(如ISO-8859、Windows-1252等),其中每个字符都被编码成一个字节,范围为0-255。对于那些位于128~255之外的字符,它们的编码值范围依赖于所使用的国家或地区代码页的不同,例如中文的GB2312或者GBK编码。

同时,在Windows系统中,通常也会使用Unicode字符编码标准来代表双字节或者多字节字符。Unicode是一个标准,可以表示几乎所有的字符,包括各种文字、符号、数字等,而ANSI编码只包含一部分字符,并且受制于本地代码页的不同。

需要注意的是,ANSI编码并不是一个标准的名称,而仅仅是一个通用名称,可能会根据特定的上下文或者平台的不同而产生不同的解释和含义。

6.为什么会有乱码

7.编码和解码

利用String类的构造方法 和 getBytes()方法

java默认编码为UTF-8

 String s = "ai你哟";
        //使用GBK编码
        byte[] byte1 = s.getBytes("GBK");
        //使用默认UTF-8编码
        byte[] byte2 = s.getBytes();
        System.out.println(Arrays.toString(byte1));
        System.out.println(Arrays.toString(byte2));

        //使用GBK进行解码
        String s1 = new String(byte1, "GBK");
        System.out.println(s1);
        //使用默认UTF-8解码(出现乱码)
        s1 = new String(byte1);
        System.out.println(s1);

8.字符输入流

 

8.1FileReader

 

无参的read()方法

有参的read()方法

9.字符输出流 FileWritter

 

 

 如果用FileOutputStream 来写单个字节,是不可以用25105的,因为单个字节的范围为-128到127.

 10.字符输入流和输出流原理

字节输入流和字节输出流 并没有 缓冲区

下面是 FileWritter的原理。

FileWrriter也会创建一个缓冲区,刚开始并不会把写入的字符写入目的文件,而是先写在缓冲区中。

当遇到,缓冲区满了;调用flush方法;调用close方法 会将缓冲区 的字符 写入目的文件 。

 

11.综合练习

练习1:拷贝文件夹

public class Test01 {
    public static void main(String[] args) throws IOException {
        //拷贝一个文件夹,考虑子文件夹

        //1.创建对象表示数据源
        File src = new File("D:\\aaa\\src");
        //2.创建对象表示目的地
        File dest = new File("D:\\aaa\\dest");

        //3.调用方法开始拷贝
        copydir(src,dest);



    }

    /*
    * 作用:拷贝文件夹
    * 参数一:数据源
    * 参数二:目的地
    *
    * */
    private static void copydir(File src, File dest) throws IOException {
        dest.mkdirs();
        //递归
        //1.进入数据源
        File[] files = src.listFiles();
        //2.遍历数组
        for (File file : files) {
            if(file.isFile()){
                //3.判断文件,拷贝
                FileInputStream fis = new FileInputStream(file);
                FileOutputStream fos = new FileOutputStream(new File(dest,file.getName()));
                byte[] bytes = new byte[1024];
                int len;
                while((len = fis.read(bytes)) != -1){
                    fos.write(bytes,0,len);
                }
                fos.close();
                fis.close();
            }else {
                //4.判断文件夹,递归
                copydir(file, new File(dest,file.getName()));
            }
        }
    }
}

练习2:加密

一个数和另一个数 异或 的结果,再次异或,得到原数字。

先得到对girl.jpg异或,得到加密图片ency.jpg

 对ency.jpg异或, 得到解密 图片redu.jpg

练习3(回首往事):

Stream流 不会改变  原有 引用类型的值,需要创建一个变量去接收。

//Stream流 不会改变原有引用类型 的值。需要赋值给另一个 引用类型
 

 

② String类 的 replace 和 replaceAll

自己看下源码就可以了。

replace 只能把对应的所有单个字符 替换成 你想要的。

而replaceAll ()  利用正则表达式,替换符合正则表示规则的字符串

12.字节缓冲流

对于续写开关。不能通过字节缓冲流来更改,需要更改传进去的参数的续写开关。

我们会传进去 FileOutputStream 这样的字节输出流 对象,然后创建其对象 更改字节输出流 的构造方法的参数。

12.1 简要介绍

12.2 字节缓冲输入输出流

12.3 字符缓冲输入输出流

 

 12.4 字符输入输出流的特有方法

12.5小总结

12.6 综合练习3(有意思)

不然就会出现   创建输出流  会清空文件的情况。

下图会输出的为null,因为  BufferedWriter bw  =new() ……   会清空文件

public class Practice3 {
    public static void main(String[] args) throws IOException {
        File f = new File("vip.txt");

        BufferedReader br = new BufferedReader(new FileReader(f));
        int i = br.readLine();
        //  System.out.println("i=" + (char) i);
        br.close();
        BufferedWriter bw = new BufferedWriter(new FileWriter(f));
        if (i >= '1') {
            bw.write(i - 1);
        } else {
            //如果不写’0‘ 的话,text里的内容变为空,每次新建一个 BufferedWriter ,都会清空文件内容
            bw.write('0');
            System.out.println("请开启vip功能");
        }

        bw.close();
    }
}

13.转换流

13.1转换流基本用法

 

 应用场景:

13.2综合练习

需求1:

public class High {
    public static void main(String[] args) throws IOException {
        File f = new File("D:\\黑马资料\\day29-IO(其他流)\\资料\\gbkfile.txt");


//        //联想起 String类 ,String的getBytes()方法可以指定字符集
//        String s = "红外线";
//        byte[] a = s.getBytes();
//        System.out.println(Arrays.toString(a));

        //1.利用转换流 可以指定字符集!!!!!!!!!!!(jdk8)
        InputStreamReader isr = new InputStreamReader(new FileInputStream(f), "GBK");
        char[] chars = new char[100];
        int len;
        while ((len = isr.read(chars)) != -1) {
            System.out.print(new String(chars));
        }
        isr.close();
        //2.利用FileReader 的构造方法指定字符集
        FileReader fr = new FileReader(f, Charset.forName("GBk"));
        int c;
        while ((c = fr.read()) != -1) {
            System.out.print((char) c);
        }
        ;
        fr.close();
    }
}

需求2:

 需求3:

public class High2 {
    public static void main(String[] args) throws IOException {
        //将GPK编码的文件,改为UTF-8编码
        File f = new File("D:\\黑马资料\\day29-IO(其他流)\\资料\\gbkfile.txt");
        //1.jdk11前的方法
        //因为要读入GBK编码的文件,并且InputStreamReader默认接收UTF-8编码.必须与源文件编码一致
        InputStreamReader isr = new InputStreamReader(new FileInputStream(f), "GBK");
        //写出时,可以指定编码为UTF-8
        OutputStreamWriter osr = new OutputStreamWriter(new FileOutputStream("grin.txt"), "UTF-8");
        int c;
        while ((c = isr.read()) != -1) {
            osr.write(c);
        }
        isr.close();
        osr.close();


}
}
        //替代方案; jdk11

13.3综合练习(太妙了。这种多态)

13.4 额外扩展

 

14.序列化流

继承于OutputStream 抽象类

14.1课上

 

 Serializable 是标记性接口

14.2 实操一下

public class SerializableDemo {
    public static void main(String[] args) throws IOException {
        //创建序列化流的对象/ 对象操作输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("xiaohei.txt"));
        Student s = new Student("小黑", 3);
        oos.writeObject(s);
        oos.close();
    }
}

15.反序列化

public class SerializableDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建序列化流的对象/ 对象操作输出流
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("xiaohei.txt"));
//        Student s = new Student("小黑", 3);
//        oos.writeObject(s);
//        oos.close();

        //创建反序列流的对象/ 对象操作输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("xiaohei.txt"));
        Student s2 = (Student) ois.readObject();
        System.out.println(s2);
    }
}

16.序列化和反序列化的使用细节

 

java根据类的内容创建一个序列号,如果在代码中修改了类中的内容(像构造方法,成员变量,成员方法),序列号会发生改变。

如果使用ObjectInputStream 类的readObject()来得到对象时,就会报错,因为此时序列号不一样。

 为了解决上述问题,需要  在类中定义一个不变的 序列号

建议使用java自动生成的。

瞬态关键字 :  transient

练习:

建议 把序列号 晚点写,因为 序列号是 根据类中的内容来 生成的。

16.2 综合练习:读写多个对象    (值得一看)

注意,它直接把ArrayList集合 给写出,不用逐个对象的往外写。

public class SerTest {
	public static void main(String[] args) throws Exception {
		// 创建 学生对象
		Student student = new Student("老王", "laow");
		Student student2 = new Student("老张", "laoz");
		Student student3 = new Student("老李", "laol");

		ArrayList<Student> arrayList = new ArrayList<>();
		arrayList.add(student);
		arrayList.add(student2);
		arrayList.add(student3);
		// 序列化操作
		// serializ(arrayList);
		
		// 反序列化  
		ObjectInputStream ois  = new ObjectInputStream(new FileInputStream("list.txt"));
		// 读取对象,强转为ArrayList类型
		ArrayList<Student> list  = (ArrayList<Student>)ois.readObject();
		
      	for (int i = 0; i < list.size(); i++ ){
          	Student s = list.get(i);
        	System.out.println(s.getName()+"--"+ s.getPwd());
      	}
	}

	private static void serializ(ArrayList<Student> arrayList) throws Exception {
		// 创建 序列化流 
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
		// 写出对象
		oos.writeObject(arrayList);
		// 释放资源
		oos.close();
	}
}

17.打印流

17.1字节打印流(底层没有缓冲区)

 

 17.2 字符打印流

既可以用 字节输出流作为参数,也可以用字符输出流作为参数

 

 

17.3 打印流与sout 的特殊关系

在System类中 有个PrintStream流 定义 的  变量out, 可以调用打印流的print()println()等一系列方法。

17.4总结

18.解压缩流

/*
*   解压缩流
*
* */
public class ZipStreamDemo1 {
    public static void main(String[] args) throws IOException {

        //1.创建一个File表示要解压的压缩包
        File src = new File("D:\\aaa.zip");
        //2.创建一个File表示解压的目的地
        File dest = new File("D:\\");

        //调用方法
        unzip(src,dest);

    }

    //定义一个方法用来解压
    public static void unzip(File src,File dest) throws IOException {
        //解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中
        //创建一个解压缩流用来读取压缩包中的数据
        ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
        //要先获取到压缩包里面的每一个zipentry对象
        //表示当前在压缩包中获取到的文件或者文件夹
        ZipEntry entry;
        while((entry = zip.getNextEntry()) != null){
            System.out.println(entry);
            if(entry.isDirectory()){
                //文件夹:需要在目的地dest处创建一个同样的文件夹
                File file = new File(dest,entry.toString());
                file.mkdirs();
            }else{
                //文件:需要读取到压缩包中的文件,并把他存放到目的地dest文件夹中(按照层级目录进行存放)
                FileOutputStream fos = new FileOutputStream(new File(dest,entry.toString()));
                int b;
                while((b = zip.read()) != -1){
                    //写到目的地
                    fos.write(b);
                }
                fos.close();
                //表示在压缩包中的一个文件处理完毕了。
                zip.closeEntry();
            }
        }
        zip.close();
    }
}

自己实操:

public class ZipDemo {
    public static void main(String[] args) throws IOException {
        File src = new File("D:\\第一次用IDEA\\MyApi\\directoryTest.zip");
        File dest = new File("D:\\第一次用IDEA\\MyApi");
        deZip(src, dest);
    }

    //解压缩src到目的文件夹下
    public static void deZip(File src, File dest) throws IOException {
        // 创建Zip字节输入流
        ZipInputStream zis = new ZipInputStream(new FileInputStream(src));
        //解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中
        //       创建一个解压缩流用来读取压缩包中的数据
        //要先获取到压缩包里面的每一个ZipEntry类的对象!!!!
        //表示当前在压缩包中获取到的文件或者文件夹
        ZipEntry entry;
        //不断获取ZipEntry类的对象
        while ((entry = zis.getNextEntry()) != null) {
            //如果entry 是文件夹
            if (entry.isDirectory()) {
                File f = new File(dest, entry.getName());
                System.out.println(f.mkdirs());

            } else {
                //利用ZipEntry类的对象的 read方法,如果该对象是文件的话,可读取该文件
                File f = new File(dest, entry.getName());
                FileOutputStream fos = new FileOutputStream(f);
                byte[] c = new byte[1024];
                int len = 0;
                while ((len = zis.read(c)) != -1) {
                    fos.write(c);
                }
                fos.close();

            }
        }
        System.out.println(zis.getNextEntry());
        zis.closeEntry();

    }
}

19.压缩流

 19.1压缩单个文件

纠错,下面是我出错的地方

public static void toZip(File src, File dest) throws IOException {
        //!!!!!!!!!!!!下面这一行写错了,不能只关联一个路径,还要跟 一个 ”名字.zip“,表示放到这个压缩文件中
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        //--------------------------------------
        //创建ZipEntry 类创建的对象
        // 参数:压缩包里面的路径
        ZipEntry entry = new ZipEntry("directoryTest/1.txt");
        zos.putNextEntry(entry);
        //4.把src文件中的数据写到压缩包当中
        zos.closeEntry();
        zos.close();

    }
}

正确的在下面

 public class ZipStreamDemo2 {
    public static void main(String[] args) throws IOException {
        /*
         *   压缩流
         *      需求:
         *          把D:\\a.txt打包成一个压缩包
         * */
        //1.创建File对象表示要压缩的文件
        File src = new File("D:\\a.txt");
        //2.创建File对象表示压缩包的位置
        File dest = new File("D:\\");
        //3.调用方法用来压缩
        toZip(src,dest);
    }

    /*
    *   作用:压缩
    *   参数一:表示要压缩的文件
    *   参数二:表示压缩包的位置
    * */
    public static void toZip(File src,File dest) throws IOException {
        //1.创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest,"a.zip")));
        //2.创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
        //参数:压缩包里面的路径
        ZipEntry entry = new ZipEntry("aaa\\bbb\\a.txt");
        //3.把ZipEntry对象放到压缩包当中
        zos.putNextEntry(entry);
        //4.把src文件中的数据写到压缩包当中
        FileInputStream fis = new FileInputStream(src);
        int b;
        while((b = fis.read()) != -1){
            zos.write(b);
        }
        zos.closeEntry();
        zos.close();
    }
}

19.2 压缩文件夹

 上面这个可以创建多层文件夹,创建后的便为  aaa\\bbb\\a.text

但是你看下面这个,由于file的路径为绝对路径,但我们不想要盘符,怎样解决?看最后的答案。

我写的代码:

public class ZipDemo3 {
    public static void main(String[] args) throws IOException {
        File src = new File("directoryTest");
        // 哎哟,这个dest有点妙。当在new FileOutputStream(dest) 时,会创建这样一个zip结尾的文件
        File dest = new File(src.getParent(), src.getName() + ".zip");
        //一般直接放在同一目录下
        //关联压缩流
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        System.out.println(src.getParent());
        dirToZip(src, zos, src.getName());
    }

    public static void dirToZip(File src, ZipOutputStream dest, String name) throws IOException {
        //将src文件夹下的所有文件放到 ZipOutputStream dest 指定的.zip压缩文件中
        File[] files = src.listFiles();
        if (files == null) {
            return;
        }
        for (File file : files) {
            //如果file是文件,则拷贝到压缩文件中
            if (file.isFile()) {
                FileInputStream fis = new FileInputStream(file);
                //注意参数是压缩包中的路径
                ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
                dest.putNextEntry(entry);

                byte[] bytes = new byte[1024];
                int len = 0;
                while ((len = fis.read(bytes)) != -1) {
                    dest.write(bytes, 0, len);
                }
                System.out.println(name + "\\" + file.getName());
                fis.close();
                //如果我不关闭 entry 会发生什么?
                  dest.closeEntry();
            } else {
                //递归
                //注意是把文件夹当作参数传进去
                dirToZip(file, dest, name + "\\" + file.getName());
            }

        }
    }
}

下面是答案:

public class ZipStreamDemo3 {
    public static void main(String[] args) throws IOException {
        /*
         *   压缩流
         *      需求:
         *          把D:\\aaa文件夹压缩成一个压缩包
         * */
        //1.创建File对象表示要压缩的文件夹
        File src = new File("D:\\aaa");
        //2.创建File对象表示压缩包放在哪里(压缩包的父级路径)
        File destParent = src.getParentFile();//D:\\
        //3.创建File对象表示压缩包的路径
        File dest = new File(destParent,src.getName() + ".zip");
        //4.创建压缩流关联压缩包
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));
        //5.获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
        toZip(src,zos,src.getName());//aaa
        //6.释放资源
        zos.close();
    }

    /*
    *   作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
    *   参数一:数据源
    *   参数二:压缩流
    *   参数三:压缩包内部的路径
    * */
    public static void toZip(File src,ZipOutputStream zos,String name) throws IOException {
        //1.进入src文件夹
        File[] files = src.listFiles();
        //2.遍历数组
        for (File file : files) {
            if(file.isFile()){
                //3.判断-文件,变成ZipEntry对象,放入到压缩包当中
                ZipEntry entry = new ZipEntry(name + "\\" + file.getName());//aaa\\no1\\a.txt
                zos.putNextEntry(entry);
                //读取文件中的数据,写到压缩包
                FileInputStream fis = new FileInputStream(file);
                int b;
                while((b = fis.read()) != -1){
                    zos.write(b);
                }
                fis.close();
                zos.closeEntry();
            }else{
                //4.判断-文件夹,递归
                toZip(file,zos,name + "\\" + file.getName());
                //     no1            aaa   \\   no1
            }
        }
    }
}

20.工具包

20.1 hutool

20.1 common.io

21.综合练习

1.综合练习01 (爬取形式)

2.利用FileOutstream来清空文件的内容。

下面这个例子是机器人写的。

我认为,当创建FileOutputStream 对象后,文件里的内容便会随之消失。

import java.io.*;

public class ClearFileContent {
    public static void main(String[] args) throws IOException {
        File file = new File("example.txt");

        // 打开文件,文件打开方式为追加模式("append")
        FileOutputStream fos = new FileOutputStream(file, false);

        // 覆盖文件内容,从而清除文件内容
        fos.write(new byte[0]);

        // 关闭文件
        fos.close();
    }
}

3.带权重的随机算法(用到二分查找)

用到了概率论的知识,很有趣。

package com.itheima.myiotest6;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;

public class Test {
    public static void main(String[] args) throws IOException {
        //1.把文件中所有的学生信息读取到内存中
        ArrayList<Student> list = new ArrayList<>();
        BufferedReader br = new BufferedReader(new FileReader("myiotest\\src\\com\\itheima\\myiotest6\\names.txt"));
        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();

        //2.计算权重的总和
        double weight = 0;
        for (Student stu : list) {
            weight = weight + stu.getWeight();
        }

        //3.计算每一个人的实际占比
        //[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
        double[] arr = new double[list.size()];
        int index = 0;
        for (Student stu : list) {
            arr[index] = stu.getWeight() / weight;
            index++;
        }

        //4.计算每一个人的权重占比范围
        for (int i = 1; i < arr.length; i++) {
            arr[i] = arr[i] + arr[i - 1];
        }

        //5.随机抽取
        //获取一个0.0~1.0之间的随机数
        double number = Math.random();
        //判断number在arr中的位置
        //二分查找法
        //方法回返回: - 插入点 - 1
        //获取number这个数据在数组当中的插入点位置
        int result = -Arrays.binarySearch(arr, number) - 1;
        Student stu = list.get(result);
        System.out.println(stu);

        //6.修改当前学生的权重
        double w = stu.getWeight() / 2;
        stu.setWeight(w);

       //7.把集合中的数据再次写到文件中
        BufferedWriter bw = new BufferedWriter(new FileWriter("myiotest\\src\\com\\itheima\\myiotest6\\names.txt"));
        for (Student s : list) {
            bw.write(s.toString());
            bw.newLine();
        }
        bw.close();


    }
}

4.RUN 下的路径,working directory

 5.  正则表达式

 ?i 忽略大小写

6.Properties

load()和 store

关联IO流

 

读取properties文件里的数据

十一、多线程

1.什么是多线程

2.多线程的实现方式

2.1 继承Thread类的方式

public class ThreadDemo {
    public static void main(String[] args) {
     MyThread t1=new MyThread();
     MyThread t2=new MyThread();
     //Thread类具有setName()方法
     t1.setName("海绵宝宝");
     t2.setName("派大星");
//开启线程,会自动调用线程中的run()方法
        t1.start();
        t2.start();
    }
}

2.2 实现Runnable接口的类

 2.3利用Callable和Futrue接口

---------------------------------------------------------------------------------

机器人帮我解答了

 

-----------------------------------------------------------------------------------

!!!!!!!!!!!!!!!!!!!!!!!!    我知道了,去看下面的线程优先级的代码,必须创建两个实现Callable接口的类,这样才能创建两个进程。

我用这个方式时,虽然能获取线程的结果,但是好像没法给线程命名,代码如下,令人很头大。

结果一会是小黄,一会是小黑。并不会交替出现。

public class Test {
    public static void main(String[] args) {
        MyCallable mc=new MyCallable();
        FutureTask<Integer> ft=new FutureTask<>(mc);
        Thread t1=new Thread(ft);
        Thread t2=new Thread(ft);
        t1.setName("小黑");
        t2.setName("小黄");
        t1.start();
        t2.start();
    }
}

public class MyCallable implements Callable<Integer> {
    

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread());
        for (int i = 0; i < 50; i++) {
           Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+"我爱你!");
        }
        return 6;
    }
}

因为java只支持继承一个类,因此继承Thread类的扩展性差。

3.线程常用方法

3.1线程休眠

3.2线程优先级

 

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
        return "线程执行完毕了";
    }
}
public class Demo {
    public static void main(String[] args) {
        //优先级: 1 - 10 默认值:5
        MyCallable mc = new MyCallable();

        FutureTask<String> ft = new FutureTask<>(mc);

        Thread t1 = new Thread(ft);
        t1.setName("飞机");
        t1.setPriority(10);
        //System.out.println(t1.getPriority());//5
        t1.start();

        MyCallable mc2 = new MyCallable();

        FutureTask<String> ft2 = new FutureTask<>(mc2);

        Thread t2 = new Thread(ft2);
        t2.setName("坦克");
        t2.setPriority(1);
        //System.out.println(t2.getPriority());//5
        t2.start();
    }
}

 3.3守护线程(备胎线程)

public class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        //把第二个线程设置为守护线程
        //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
        t2.setDaemon(true);

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

 3.4同步代码块解决数据安全问题   (注意是代码块 )

代码演示:

public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
                //t1进来后,就会把这段代码给锁起来
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                        //t1休息100毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //窗口1正在出售第100张票
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--; //tickets = 99;
                }
            }
            //t1出来了,这段代码的锁就被释放了
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();

        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

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

3.5 同步方法 解决数据安全问题

技巧:先写同步代码块,再把同步代码块抽出来当作 同步方法。

注意:同步方法的锁不是我们指定的,是系统指定的。

 

 下图的代码:你可能会奇怪,为什么 tichekt变量 这时候不用static修饰,因为下图用的实现Runnable接口的类 实现多线程,在创键其对象时,只创建了一次,然后把其对象作为参数传到Thread t=new Thread(参数),Thread t2=new Thread(参数),因此两个线程操作同一个对象,因此就不用加static关键字了。

 3.6  StringBuffer

StringBuilder和StringBuffer里的内容是完全一致的。但StringBuffer里的每一个方法都 加上了synchronized关键字,保证多线程环境下数据的安全。

因此,单线程环境下,使用StringBuider,多线程环境下使用StringBuffer

3.7 Lock锁

① 释放锁的时候要小心,结合finally 食用更佳。

②不要进行锁的嵌套

 来看看阿玮是怎样正确释放锁的

3.8 死锁

千万不要进行 锁的嵌套

3.9  等待唤醒机制

 3.10 阻塞队列

阻塞队列的take(),put()方法已经加锁了,就不用在它们外面加锁了,可能会出现锁的嵌套。

3.11 线程的状态

3.12 练习1

突然忘记lock锁是怎么用的了,需要创建 Lock类的实现类对象,ReentrantLock

3.13  抢红包(BigDecimal)精确计算

package com.itheima.test4case2;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

public class MyThread extends Thread{

    //总金额
    static BigDecimal money = BigDecimal.valueOf(100.0);
    //个数
    static int count = 3;
    //最小抽奖金额
    static final BigDecimal MIN = BigDecimal.valueOf(0.01);

    @Override
    public void run() {
        synchronized (MyThread.class){
            if(count == 0){
                System.out.println(getName() + "没有抢到红包!");
            }else{
                //中奖金额
                BigDecimal prize;
                if(count == 1){
                    prize = money;
                }else{
                    //获取抽奖范围
                    double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
                    Random r = new Random();
                    //抽奖金额
                    prize = BigDecimal.valueOf(r.nextDouble(bounds));
                }
                //设置抽中红包,小数点保留两位,四舍五入
                prize = prize.setScale(2,RoundingMode.HALF_UP);
                //在总金额中去掉对应的钱
                money = money.subtract(prize);
                //红包少了一个
                count--;
                //输出红包信息
                System.out.println(getName() + "抽中了" + prize + "元");
            }
        }
    }
}

3.14  抽奖(shuffle)

public class MyThread extends Thread {
    private static ArrayList<Integer> list = new ArrayList<>();

    static {
        Collections.addAll(list, 10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700);
    }

    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                int size = list.size();
                if (size == 0) {
                    break;
                } else {
                    //产生一个随机的索引
                    //也可以使用Collections.shuffle();
                    Random r = new Random();
                    int index = r.nextInt(list.size());
                    int prize = list.get(index);
                    System.out.println(this.getName() + "又产生了一个" + prize + "大奖");
                    list.remove(index);
                }
            }
        }
    }
}

3.15 多线程  栈

 

 3.16 线程池

线程池一般不关闭,因为服务器一般24小时不关闭。

 对于三种方式实现的多线程,都可以传进去,可以看看它的构造方法。

3.17 自定义线程池

 

 

 往里面提交线程时,和前面一样,使用 submit()方法。

关于第7个参数,是任务拒绝策略,是静态内部类。

关于第4个参数,任务队列,gpt有话说。

3.18 最大并行数

获取 自己电脑的最大运行数

 

 

 对于I/O密集型运算,cpu计算时间和等待时间 可以利用  一个叫做 thread dump的工具完成。

这样便可以算出  线程池  的大小。

3.19 看阿伟的文档

十一、网络编程

1.接受并反馈

这个练习解决了  上传文件 时我所遇到的问题

 注意下面这张图片的注释。

2. 上传文件

在上传文件时,发生一个问题,就是当使用 socket.getInputStream()和getOutputStream()方法得到一个 输入或输出流时,不能使用输入/输出流对象 .close(),这样会导致socket连接关闭。

3.使用UUID解决文件名重复

4.多线程的服务端

①把所要进行的操作放到线程里面,这真的是很妙的一步。

②把socket写到构造方法里,创建对象  的时候直接把socket传进去,哎哟,不错。

public class Client {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //1. 创建Socket对象,并连接服务器
        Socket socket = new Socket("127.0.0.1",10000);

        //2.读取本地文件中的数据,并写到服务器当中
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //往服务器写出结束标记
        socket.shutdownOutput();


        //3.接收服务器的回写数据
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();
        System.out.println(line);


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

    }
}
public class Server {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //1.创建对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            //2.等待客户端来连接
            Socket socket = ss.accept();

            //开启一条线程
            //一个用户就对应服务端的一条线程
            new Thread(new MyRunnable(socket)).start();
        }

    }
}


public class MyRunnable implements Runnable{

    Socket socket;

    public MyRunnable(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //3.读取数据并保存到本地文件中
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            bos.close();
            //4.回写数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.释放资源
           if(socket != null){
               try {
                   socket.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
        }
    }
}

5.利用线程池(改善显式创建线程 )

 

public class Client {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //1. 创建Socket对象,并连接服务器
        Socket socket = new Socket("127.0.0.1",10000);

        //2.读取本地文件中的数据,并写到服务器当中
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //往服务器写出结束标记
        socket.shutdownOutput();


        //3.接收服务器的回写数据
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();
        System.out.println(line);


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

    }
}
public class Server {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程数量
                16,//线程池总大小
                60,//空闲时间
                TimeUnit.SECONDS,//空闲时间(单位)
                new ArrayBlockingQueue<>(2),//队列
                Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象
                new ThreadPoolExecutor.AbortPolicy()//阻塞队列
        );



        //1.创建对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            //2.等待客户端来连接
            Socket socket = ss.accept();

            //开启一条线程
            //一个用户就对应服务端的一条线程
            //new Thread(new MyRunnable(socket)).start();
            pool.submit(new MyRunnable(socket));
        }

    }
}
public class MyRunnable implements Runnable{

    Socket socket;

    public MyRunnable(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //3.读取数据并保存到本地文件中
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            bos.close();
            //4.回写数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.释放资源
           if(socket != null){
               try {
                   socket.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
        }
    }
}

6.网络编程大作业(待完成)

十二、反射和代理

1.反射的概述

 

 

2.获取class对象的三种方式

 

3.反射获取构造方法

获取到 构造方法后,可以从中拿各种东西。利用Constructor类的方法。

 

 

 暴力反射:临时取消权限修饰符的约束

 Class clazz = Class.forName("Day03.Student");
        //注意:只有Declared 才能获得所有权限修饰符的方法
        Constructor c = clazz.getDeclaredConstructor(int.class, String.class);
        System.out.println(c);
        //虽然 构造方法是私有的,但是可以利用暴力反射 来创建对象
        c.setAccessible(true);
        Student s = (Student) c.newInstance(6, "小黑");
        System.out.println(s.getAge() + s.getName());

 4.反射获得成员变量

获取值使用  Filed.getObject()

5.反射获得成员方法

跟其他的不太一样,当使用 getMethod()方法时,由于方法的重载,因此既要给出 参数既要给出 方法名,也要给出  参数类型

6.反射的作用

7.总结

8.动态代理

 这个代码我还没实现,等待中---------------------

后记:

1.异常补充

机器人回答:throw与throws的区别

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值