Java进阶

Java进阶

一、双列集合

1、特点:

  • 双列集合一次需要存一对数据,分别为键和值
  • 键不能重复,值可以重复
  • 键和值是一一对应的,每一个键只能找到自己对应的值
  • 键 + 值 这个整体我们称之为”键值对“ 或者”键值对对象“,在Java中叫做”Entry对象“

2、Map的常见API

Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的

方法名称说明
V put(K key, V value)添加元素
V remove(Object key)根据键删除元素
void clear()移除所有的键值对元素
boolean containsKey(Object key)判断集合是否包含指定的键
boolean containsValue(Object value)判断集合是否包含是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中键值对的个数
public class A01_MapDemo1 {
    public static void main(String[] args) {
        /*
        V put(K key, V value)                       添加元素
        V remove(Object key)                        根据删除键值对元素
        void clear()                                移除所有的键值对元素
        boolean containsKey(Object key)             判断集合是否包含指定的键
        boolean containsValue(Object value)         判断集合是否包含是否包含指定的值
        boolean isEmpty()                           判断集合是否为空
        int size()                                  集合的长度,也就是集合中键值对的个数
        */

        // 1.创建Map集合的对象
        Map<String, String> map = new HashMap<>();

        // 2.添加元素
        // put方法的细节:
        // 添加/覆盖
        // 在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合当中,方法返回null
        // 在添加数据的时候,如果键存在,那么会将原有的键值对进行覆盖,并将被覆盖的值返回


        String value1 = map.put("郭靖", "黄蓉");
        System.out.println("value1 = " + value1);; // null
        map.put("韦小宝", "沐剑屏");
        map.put("尹志平", "小龙女");

        String value2 = map.put("郭靖", "小鱼儿");
        System.out.println("value2 = " + value2);; // 黄蓉

        // 判断是否包含key
        boolean keyResult = map.containsKey("郭靖");
        System.out.println("keyResult = " + keyResult); // true

        // 判断是否包含value
        boolean valueResult = map.containsValue("小鱼儿");
        System.out.println("valueResult = " + valueResult); // true
        
        // 判断集合是否为空
        boolean result1 = map.isEmpty(); // true

        // 集合的长度
        int size = map.size();
        System.out.println(size); // 3
        
        // 3.打印集合
        System.out.println(map); // {韦小宝=沐剑屏, 尹志平=小龙女, 郭靖=小鱼儿}

        // 删除
        String result = map.remove("郭靖");
        System.out.println("result = " + result); // 小鱼儿
        System.out.println(map); // {韦小宝=沐剑屏, 尹志平=小龙女}

        // 清空
        map.clear();
        System.out.println(map); // {}
    }
}

3、Map集合遍历

  1. 第一种(键找值)

    public class A02_MapDemo2 {
        public static void main(String[] args) {
            // Map集合的第一种遍历方式
    
    
            // 1.创建Map集合对象
            Map<String, String> map = new HashMap<>();
    
            // 2.添加元素
            map.put("郭靖", "黄蓉");
            map.put("韦小宝", "沐剑屏");
            map.put("尹志平", "小龙女");
    
            // 3.通过键找值
            // 3.1获取所有的键,把这些键放到一个单列集合当中
            Set<String> keys = map.keySet();
            for (String key : keys) {
                // 3.3利用map集合中的键来获取对应的值
                String value = map.get(key);
                System.out.println(key + " = " + value);
            }
    
        }
    } 
    
  2. 第二种(键值对)

    public class A03_MapDemo3 {
        public static void main(String[] args) {
            // Map集合的第二种遍历方式(键值对)
    
    
            // 1.创建Map集合对象
            Map<String, String> map = new HashMap<>();
    
            // 2.添加元素
            map.put("郭靖", "黄蓉");
            map.put("韦小宝", "沐剑屏");
            map.put("尹志平", "小龙女");
    
            // 3.通过键值对对象进行遍历
            // 3.1通过entrySet方法获取所有的键值对对象,返回一个set集合
            Set<Map.Entry<String, String>> entries = map.entrySet();
            // 3.2遍历entries集合,得到里面的每一个键值对对象
            for (Map.Entry<String, String> entry : entries) {
                String key = entry.getKey();
                String value = entry.getValue();
                System.out.println(key + " = " + value);
            }
        }
    }
    
  3. 第三种(Lambda表达式)

    public class A04_MapDemo4 {
        public static void main(String[] args) {
            // Map集合的第二种遍历方式(Lambda表达式)
    
    
            // 1.创建Map集合对象
            Map<String, String> map = new HashMap<>();
    
            // 2.添加元素
            map.put("郭靖", "黄蓉");
            map.put("韦小宝", "沐剑屏");
            map.put("尹志平", "小龙女");
    
            // 3.利用lambda表达式进行遍历
            // 底层:
            // forEach其实就是利用entrySet方法进行遍历,依次得到每一个键和值
            map.forEach(new BiConsumer<String, String>() {
                @Override
                public void accept(String key, String value) {
                    System.out.println(key + " = " + value);
                }
            });
            System.out.println("-----------------------------");
    
            // 简写
            map.forEach((key, value) -> System.out.println(key + " = " + value));
        }
    }
    

4、HashMap

4.1、特点
  1. HashMap是Map里面的一个实现类
  2. 没有额外需要学习的特有方法,直接使用Map里面的方法就可以了
  3. 特点都是由键决定的:不重复、无索引、无序:不会按照key进行排序
  4. HashMap跟HashSet底层原理是一模一样的,都是哈希表结构
  5. 依赖hashcode方法和equals方法保证键的唯一。
  6. 如果键存储的是自定义对象,需要重写hashCode和equals方法,如果不重写,在插入相同的key时会变成一条新数据,而不是覆盖。
4.2、案例
public class A06_HashMapDemo2 {
    public static void main(String[] args) {
        /*
        某个班级80名学生,现在需要组成秋游活动
        班长提供了四个景点依次是(A,B,C,D)
        每个学生只能选择一个景点,请统计出最终哪个景点想去的人数最多
         */

        // 1.需要先让学生投票
        // 定义一个数组,存储4个景点
        String[] arr = {"A", "B", "C", "D"};
        // 利用随机数模拟80个同学,并把投票结果存储起来
        ArrayList<String> list = new ArrayList<>();
        Random r = new Random();
        for (int i = 0; i < 80; i++) {
            int index = r.nextInt(arr.length);
            list.add(arr[index]);
        }

        // 2.如果要统计的东西比较多,不方便使用计数器思想
        // 我们可以定义map集合,利用集合进行统计
        HashMap<String, Integer> hm = new HashMap<>();
        for (String name : list) {
            // 判断当前景点在map集合当中是否存在
            if (hm.containsKey(name)) {
                // 存在
                // 先获取当前景点已经被投票的次数
                int count = hm.get(name);
                // 表示当前景点又投了一次
                count++;
                // 把新的次数再次添加到集合当中
                hm.put(name,count);
            } else {
                // 不存在
                hm.put(name,1);
            }
        }
        System.out.println(hm);

        // 3.求最大值
        int max = 0;
        Set<Map.Entry<String, Integer>> entries = hm.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            int count = entry.getValue();
            if (count > max) {
                max = count;
            }
        }
        System.out.println(max);

        // 4.判断哪个景点的次数跟最大值一样,打印出来
        for (Map.Entry<String, Integer> entry : entries) {
            Integer count = entry.getValue();
            if (count == max) {
                System.out.println("最大值:" + entry.getKey());
            }
        }
    }
}

5、LinkedHashMap

5.1、特点
  • 由键决定:不重复、无索引、有序:保证存储和取出的元素顺序一致
  • 原理:底层数据结构依然是哈希表,只是每个键值对元素又额外多了一个双链表的机制记录存储的顺序
5.2、案例
public class A07_LinkedHashMapDemo3 {
    public static void main(String[] args) {
        // 1.创建集合
        LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();
        // 2.添加元素,有序指的时存取的顺序是一样的
        lhm.put("a",123);
        lhm.put("d",567);
        lhm.put("c",456);
        lhm.put("b",345);
        
        // 3.打印集合
        System.out.println(lhm); // {a=123, d=567, c=456, b=345}
    }
}

6、TreeMap

6.1、特点
  • TreeMap跟TreeSet底层原理一样,都是红黑树结构的
  • 由键决定特性:不重复,无索引,可排序
  • 注意:默认按照键从小到大进行排序,就是升序,也可以自己规定键的排序顺序

代码书写两种排序规则:

  • 实现Comparable接口,指定比较规则
  • 创建集合时传递Comparator比较器对象,指定比较规则
6.2、案例1
public class A01_TreeMapDemo1 {
    public static void main(String[] args) {
        /*
        TreeMap集合:基本应用
        需求1:
            键:整数表示id
            值:字符串表示商品名称
            要求:按照id的升序排列、按照id的降序排列
        */

        // 1.创建集合对象
        // 默认情况下都是按照升序排列的
        // String 按照字母在ASCII码表中对应的数字升序进行排列
        // abcde...
        TreeMap<Integer, String> tm = new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                // 根据key倒叙排序
                // o1:表示当前要添加的元素
                // o2:表示已经在红黑树中存在的元素
                return o2 - o1;
            }
        });

        // 2.添加元素
        tm.put(2,"可口可乐");
        tm.put(4,"百事可乐");
        tm.put(5,"奥利奥");
        tm.put(3,"江小白");
        tm.put(1,"康师傅");

        // 3.打印集合
        System.out.println(tm); // {5=奥利奥, 4=百事可乐, 3=江小白, 2=可口可乐, 1=康师傅}
    }
}
6.3、案例2:
  • Student.java

    public class Student implements Comparable<Student>{
        private String name;
        private int age;
    
        @Override
        public int compareTo(Student o) {
            // 要求:按照学生年龄的升序排序,年龄一样按照姓名的字母排列,同姓名同年龄的视为同一人
    
            // this:表示当前要添加的元素
            // o:表示已经在红黑树中存在的元素
    
            // 返回值:
            // 负数:表示当前要添加的元素是小的,存左边
            // 正数:表示当前要添加的元素是大的,存右边
            // 0:表示当前要添加的元素已经存在,舍弃
    
            int i = this.getAge() - o.getAge();
            i = i == 0 ? this.getName().compareTo(o.getName()) : i;
            return i;
        }
        ...
    }
    
  • A02_TreeMapDemo2.java

    public class A02_TreeMapDemo2 {
        public static void main(String[] args) {
                /*
            TreeMap集合:基本应用
            需求2:学生对象
            值:籍贯
            要求:按照学生年龄的升序排序,年龄一样按照姓名的字母排列,同姓名同年龄的视为同一人
         */
    
            // 1.创建集合
            TreeMap<Student, String> tm = new TreeMap<>();
    
            // 2.创建三个学生对象
            Student s1 = new Student("zhangsan", 23);
            Student s2 = new Student("lisi", 24);
            Student s3 = new Student("wangwu", 25);
    
            // 3.添加元素
            tm.put(s1, "江苏");
            tm.put(s2, "天津");
            tm.put(s3, "北京");
    
            // 4.打印集合
            System.out.println(tm);
        }
    }
    
6.4、案例3:
public class A03_TreeMapDemo3 {
    public static void main(String[] args) {
        /*需求:
            字符串:"adbadadgadbabdadada"
            请统计字符串中每个字符出现的次数,并按照以下格式输出
            输出结果:
            a(5)b(4)c(3)d(2)e(1)

            新的统计思想:利用map集合进行统计

            如果题目中没有要求对结果进行排序,默认使用HashMap,效率高
            如果题目中要求对结果进行排序,使用TreeMap

            键:表示要统计的内容
            值:表示次数

         */

        // 1.定义字符串
        String s = "adbadadgadbabdadada";

        // 2.创建集合
        TreeMap<Character, Integer> tm = new TreeMap<>();

        // 3.遍历字符串得到里面的每一个字符
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // 拿着c到集合中判断是否存在
            // 存在,表示当前字符又出现了一次
            // 不存在,表示当前字符是第一次出现
            if (tm.containsKey(c)) {
                // 存在
                // 先把已经出现的次数拿出来
                int count = tm.get(c);
                // 当前字符又出现了一次
                count++;
                // 把自增之后的结果再添加到集合当中
                tm.put(c, count);
            } else {
                // 不存在
                tm.put(c, 1);
            }
        }

        // 4.遍历集合,并按照指定的格式进行拼接
        // a(5)b(4)c(3)d(2)e(1)
        // StringBuilder sb = new StringBuilder();
        // tm.forEach((key, value) -> sb.append(key).append("(").append(value).append(")"));

        StringJoiner sj = new StringJoiner("","","");
        tm.forEach((key, value) -> sj.add(key + "").add("(").add(value + "").add(")"));

        System.out.println(sj);
    }
}

7、总结

  1. TreeMap添加元素的时候,键不需要重写hashCode和equals方法,压根没用到hashCode和equals方法。使用红黑树规则添加的元素,是使用Comparable来比较key是否相等,返回0表示相等,自定义对象必须实现Comparable接口。
  2. HashMap是哈希表结构,JDK8开始由数组,链表,红黑树组成。虽然有红黑树,但HashMap的底层是使用哈希值和equals方法来比较key是否相等,所以不需要实现Comparable接口。
  3. HashMap添加自定义对象的时候,需要重写hashCode和equals方法,它是根据key的hash值决定添加元素的位置,需要使用hashCode和equals方法,不然会添加重复的key。
  4. HashMap和TreeMap使用put添加元素时会覆盖key相同的value值,返回旧值;使用putIfAbsent方法添加元素则不会覆盖旧值。
  5. TreeMap和HashMap一般而言,HashMap的效率要更高。
  6. 如何选择
    1. 默认:HashMap(效率最高)
    2. 如果要保证存取顺序一样:LinkedHashMap
    3. 如果要进行排序:TreeMap

二、不可变集合

不可变集合:JDK9出现的

# list
List<String> list = List.of("a","b","c".....);

# set  里面的数据保证唯一性
Set<String> set = Set.of("a","b","c".....);

# map 最多存10个key,换句话说参数个数最多20Map<String,String> map = Map.of("k1","v1","k2","v2"....);

# map 不限参数的的不可变集合
HashMap<String,String> hm = new HashMap<>();
Map<Object,Object> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0]));
# JDK10 支持以下的不可变集合的map写法
Map<String,String> map = Map.copyOf(hm);

三、Stream流

1、作用:

结合了Lambda表达式,简化集合、数组的操作。

2、使用步骤

  1. 先得到一条Stream流(流水线),并把数据放上去。
  2. 使用中间方法对流水线上的数据进行操作。
  3. 使用终结方法对流水线上的数据进行操作。

3、在各个数据结构中的使用

// stream流在各个数据类型中的使用
public class StreamDemo01 {
    public static void main(String[] args) {
        // 单列集合
        ArrayList<Object> list = new ArrayList<>();
        Collections.addAll(list, 2, 3, "ssa", 4, 2);
        // 输出:2 3 ssa 4 2
        list.stream().forEach(s -> System.out.print(s + " "));
        System.out.println();

        System.out.println("===========================");
        // 双列集合
        HashMap<Object, Object> map = new HashMap<>();
        map.put("name", "zhangsan");
        map.put("age", 12);
        map.put("gender", "man");
        // 输出:gender name age
        map.keySet().stream().forEach(s -> System.out.print(s + " "));
        System.out.println();
        // 输出:gender=man name=zhangsan age=12 
        map.entrySet().stream().forEach(s -> System.out.print(s + " "));

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

        // 数组 建议使用Arrays.stream()
        int[] arr1 = {1, 2, 3, 4, 5, 6, 7};
        // 输出:1 2 3 4 5 6 7 
        Arrays.stream(arr1).forEach(s -> System.out.print(s + " "));
        System.out.println();
        System.out.println("===================");
        String[] arr2 = {"aaa", "bbb", "ccc"};
        // 输出:aaa bbb ccc
        Arrays.stream(arr2).forEach(s -> System.out.print(s + " "));
        System.out.println();
        System.out.println("===================");
        // 数组如果是基本数据类型,不会自动装箱,而是打印地址值
        // 输出:[I@3b9a45b3
        Stream.of(arr1).forEach(s -> System.out.print(s + " "));
        System.out.println();
        System.out.println("===================");
        // 引用类型使用没有问题
        // 输出:aaa bbb ccc
        Stream.of(arr2).forEach(s -> System.out.print(s + " "));

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

        // 一堆零散数据
        // 输出:1 2 3 4 5 aaa bbb 
        Stream.of(1, 2, 3, 4, 5, "aaa", "bbb").forEach(s-> System.out.print(s + " "));

    }
}

4、stream中的方法

4.1、中间方法
4.1.1、filter
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "abc", "aa", "acb", "cc", "ad", "bbd");
// filter 过滤以a开头的,过滤长度为3的    
// 输出:aaa abc acb 
list.stream().filter(s->s.startsWith("a")).filter(s->s.length()==3).forEach(s-> System.out.println(s));  
4.1.2、limit、skip
// limit:限制个数,skip:跳过个数
ArrayList<Object> list2 = new ArrayList<>();
Collections.addAll(list2, "1", "2", "3", "4", "5", "6", "7");
// 跳过前2个数据,限制3个数据
// 输出:3 4 5 
list2.stream().skip(2).limit(3).forEach(s -> System.out.print(s + " "));
4.1.3、distinct

如果list是自定义类型,需要重写hashCode和equals方法

// distinct:去重,如果list是自定义类型,需要重写hashCode和equals方法
ArrayList<Object> list3 = new ArrayList<>();
Collections.addAll(list3, "aaa", "bbb", "aaa", "bbb", "ccc", "ddd", "aaa");
// 输出:aaa bbb ccc ddd 
list3.stream().distinct().forEach(s -> System.out.print(s + " "));
4.1.4、concat

两个流合并后数据类型会变成他们共同的父类,就无法使用本类的方法了,所以合并的流数据类型最好是一致的

// concat:合并两个stream流
ArrayList<String> list4 = new ArrayList<>();
Collections.addAll(list4, "ccc", "ddd");
ArrayList<Integer> list5 = new ArrayList<>();
Collections.addAll(list5, 111, 222);
// 输出:ccc ddd 111 222
Stream.concat(list4.stream(), list5.stream()).forEach(s -> System.out.print(s + " "));
4.1.5、map
// map:转换流中的数据类型
ArrayList<String> list6 = new ArrayList<>();
Collections.addAll(list6, "ccc-13", "ddd-14");
// 输出:13 14
list6.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(s -> System.out.print(s + " "));
4.1.6、注意
  • 中间方法,返回新的Stream流只能使用一次,建议链式编程

    Stream<String> stream1 = list6.stream();
    Stream<String> stream2 = stream1.distinct();
    // 报错:stream has already been operated upon or closed
    // stream1只能使用一次,再次使用会报错
    // stream1.distinct();
    
  • 修改stream流中的数据,不会影响原来集合或者数组中的数据

4.2、终结方法

终结方法返回的不是Stream流

4.2.1、forEach
// forEach:遍历
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, "aaa", "bbb", "ccc");
// 输出:aaa bbb ccc
list1.stream().forEach(s -> System.out.print(s + " "));
4.2.2、count
// count:统计
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "aaa", "bbb", "ccc");
// 统计数据数量
long count = list2.stream().count();
// 输出:3
System.out.println(count);
4.2.3、toArray
// toArray:收集流中的数据,放到数组中
ArrayList<String> list3 = new ArrayList<>();
Collections.addAll(list3, "aaa", "bbb", "ccc");
// value指的是list的长度
String[] array = list3.stream().toArray(value -> new String[value]);
// 输出:[aaa, bbb, ccc]
System.out.println(Arrays.toString(array));
4.2.4、collect
// collect:收集流中的数据,放到集合中
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌-男-15","周芷若-女-21","张翠山-男-15","赵敏-女-17");
// 1.把所有男性收集到List集合中
List<String> newList = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toList());
// 输出:[张无忌-男-15, 张翠山-男-15]
System.out.println(newList);
// 2.把所有男性收集到set集合 ---》可以去重
Set<String> newSet = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toSet());
// 输出:[张翠山-男-15, 张无忌-男-15]
System.out.println(newSet);
// 3.把所有男性收集到map集合   键:姓名   值:年龄
Map<String, Integer> newMap = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(
    Collectors.toMap(s -> s.split("-")[0], 
                     s -> Integer.parseInt(s.split("-")[2])));
// 输出:{张翠山=15, 张无忌=15}
System.out.println(newMap);

四、方法引用

1、介绍

  • 方法引用:把已经存在的方法拿过来用,当做函数式接口中的抽象方法的方法体。

  • 方法引用符:::

  • 方法应用时的注意事项:

    • 需要有函数式接口
    • 被引用方法最好已经存在,这样才够简洁,不然自己又要定义一个方法。
    • 被引用方法的形参和返回值需要和抽象方法保持一致
    • 被引用方法的功能要满足当前的需求
  • 方法引用分类:

    • 引用静态方法
    • 引用成员方法
      • 引用其他类的成员方法
      • 引用本类的成员方法
      • 引用父类的成员方法
    • 引用构造方法
    • 其他调用方式
      • 使用类名引用成员方法
      • 引用数组的构造方法

2、使用类名引用静态方法

格式:类名:静态方法

范例:Integer::parseInt

// 创建集合并添加元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "111", "222", "333", "444");
// 需求:将集合中的字符串转换成整型
// 输出:111 222 333 444
list.stream().map(Integer::parseInt).forEach(s -> System.out.print(s + " "));

3、使用对象引用成员方法

格式:对象:成员方法

  1. 其他类:其他类对象::方法名
  2. 本类:this::方法名
  3. 父类:super::方法名

注意:静态方法中没有this和super关键字

  • 创建StringOperation类

    public class StringOperation {
    
        public boolean stringJudge(String s) {
            return s.startsWith("张") && s.length() == 3;
        }
    }
    
  • 引用其他类成员方法

    // 引用成员方法
    public class FunctionQuote02 {
    
        public static void main(String[] args) {
            // 1.创建集合
            ArrayList<String> list = new ArrayList<>();
            // 2. 添加数据
            Collections.addAll(list, "张无忌", "周芷若", "张三丰", "张强", "赵敏");
            // 3.过滤数据:只要张开头,而且名字是三个字的
            list.stream().filter(new StringOperation()::stringJudge).forEach(s -> System.out.println(s));
        }
    }
    

4、使用类名引用构造方法

格式:类名::new

范例:Student::new

  • 编写Student类

    public class Student {
        String name;
        int age;
    
        // 重写构造方法供FunctionQuote03调用
        public Student(String str) {
            String[] splits = str.split(",");
            this.name = splits[0];
            this.age = Integer.parseInt(splits[1]);
        } 
        ....
    }
    
  • 需求实现

    // 引用构造方法
    public class FunctionQuote03 {
        public static void main(String[] args) {
            // 1.创建集合对象
            ArrayList<String> list = new ArrayList<>();
            // 2.添加数据
            Collections.addAll(list,"张无忌,14","张强,16","赵敏,21","周芷若,24","张三丰,56");
            // 3.将list中的数据封装到Student中,并收集到List集合中
            list.stream().map(Student::new).forEach(s-> System.out.println(s));
        }
    }
    

5、使用类名引用成员方法

格式:类名::成员方法

范例:String::substring

方法引用规则:

  • 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致。
  • 其他规则与上方介绍中一致

抽象方法参数详解:

  • 第一个参数:表示被引用方法的调用者,决定了可以引用那些类中的方法,在Stream流当中,第一个参数一般都表示流里面的每一个数据。假设流里面的数据是字符串,那么使用这种方式进行方法引用,只能引用String这个类中的方法。
  • 第二个参数到最后一个参数:跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要的是无参的成员方法。

局限性:

  • 不能引用所有类中的成员方法,跟抽象方法的第一个参数有关,这个参数是什么类型,那么就只能引用这个类中的方法。
// 使用类名引用成员方法
public class FunctionQuote04 {
    public static void main(String[] args) {
        // 1.创建集合
        ArrayList<String> list = new ArrayList<>();
        // 2.添加数据
        Collections.addAll(list, "aaa", "bbb", "ccc");
        // 将数据变大写后输出
        // 输出:AAA BBB CCC
        list.stream().map(String::toUpperCase).forEach(s-> System.out.print(s + " "));
    }
}

6、引用数组的构造方法

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

范例:int[]::new

// 引用数组的构造方法
public class FunctionQuote05 {
    public static void main(String[] args) {
        // 1.创建集合并添加元素
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5, 6);
        // 2.收集到数组中
        Integer[] array = list.stream().toArray(Integer[]::new);
        // 输出:[1, 2, 3, 4, 5, 6]
        System.out.println(Arrays.toString(array));
    }
}

7、综合案例

7.1、案例一
// 综合案例一
public class FunctionQuote06 {
    public static void main(String[] args) {
        // 1. 创建集合并添加元素
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌,14", "张强,16", "赵敏,21", "周芷若,24", "张三丰,56");
        // 2.先把字符串变成Student对象,然后把Student对象收集起来
        Student[] array = list.stream().map(Student::new).toArray(Student[]::new);
        System.out.println(Arrays.toString(array));
    }
}
7.2、案例二
// 综合案例二
public class FunctionQuote07 {
    public static void main(String[] args) {
        // 1.创建集合
        ArrayList<Student> list = new ArrayList<>();
        Collections.addAll(list,
                new Student("zhangsan",23),
                new Student("lisi",24),
                new Student("wangwu",25));
        // 3.获取姓名并放到数组中
        String[] array = list.stream().map(Student::getName).toArray(String[]::new);
        // 输出:[zhangsan, lisi, wangwu]
        System.out.println(Arrays.toString(array));
    }
}

五、异常

1、介绍

在这里插入图片描述

  • Error:代表系统级别的异常(属于严重问题),系统一旦出现问题,sun公司会把这些错误封装成Error对象。Error是sun公司给自己用 的,不是给我们程序员用的,因此开发人员不用管它。

  • Exception:叫做异常,是异常体系的父类,代表可能出现的问题。我们通常会用Exception以及它的子类来封装程序出现的问题。Exception分为两类:

    • RuntimeException:运行时异常,RuntimeException及其子类,编译阶段不会出现异常提醒。运行时出现的异常,一般是由于参数传递错误带来的问题,如:数组索引越界异常。
    • 编译时异常:没有继承RuntimeException的异常,直接继承于Exception,提醒程序员检查本地信息。编译阶段就会出现异常提醒的,如:日期解析异常。
  • 异常举例:

    // 异常举例
    public class ExceptionDemo01 {
        public static void main(String[] args) throws ParseException {
    
            // 编译时异常(在编译阶段,必须要手动处理,否则代码会报错)
            String time = "2020年1月1日";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
            // java: 未报告的异常错误java.text.ParseException; 必须对其进行捕获或声明以便抛出
            Date date = sdf.parse(time);
            // 输出:Wed Jan 01 00:00:00 CST 2020
            System.out.println(date);
    
            // 运行时异常(在编译阶段不需要处理,是代码运行时出现的异常)
            int[] arr = {1, 2, 3, 4, 5};
            // 数组索引越界:ArrayIndexOutOfBoundsException
            System.out.println(arr[10]);
        }
    }
    

注意:编译阶段Java不会运行代码,智慧检查语法是否错误,或者做一些性能的优化

// 性能优化
String str = "a" + "b" + "c";  --> String str = "abc";
// 检查语法
int a = 2.2; --> 语法错误,编译失败

2、异常的作用

  1. 异常是用来查询bug的关键参考信息。
  2. 异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况。

3、异常处理的方式

3.1、JVM默认处理方式
  • 把异常的名称、异常的原因及异常出现的位置等信息输出在控制台。
  • 程序停止运行,下面的代码不会再执行了。
3.2、自己处理(捕获异常)
  • 格式:

    try {
        // 可能出现异常的代码
    } catch(异常类型 变量名) {
        // 处理异常
    } catch(异常类型 变量名) {
        // 多个异常捕获
    }....
        ...
    
  • 目的:当代码出现异常时,可以让程序继续往下执行。

注意:

  1. 如果我们要捕获多个异常,这些异常中如果存在父子关系的话,那么父类一定要写在下面

    // 自己捕获异常
    public class ExceptionDemo02 {
        public static void main(String[] args) {
            int[] arr = {1, 2, 3, 4, 5, 6};
            try {
                System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
                // 由于上方出现异常,所以try里面的代码执行就到此结束了,直接跳到catch中
                System.out.println(2/0);  // ArithmeticException
                String s = null;
                System.out.println(s.equals("abc")); // NullPointerException
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("数组索引越界");
            } catch (ArithmeticException e) {
                System.out.println("算术异常");
            } catch (NullPointerException e) {
                System.out.println("空指针异常");
            } catch (Exception e) {
                System.out.println("万能捕获异常");
            }
            System.out.println("最后执行");
        }
    }
    
  2. 在JDK7之后,我们可以在catch中同时捕获多个异常,中间用|进行隔开,表示如果出现A异常或者B异常,采用同一种处理方法。

    // 自己捕获异常
    public class ExceptionDemo02 {
        public static void main(String[] args) {
            // JDK7之后的异常捕获
            int[] arr = {1, 2, 3, 4, 5, 6};
            try {
                System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
                // 由于上方出现异常,所以try里面的代码执行就到此结束了,直接跳到catch中
                System.out.println(2/0);  // ArithmeticException
                String s = null;
                System.out.println(s.equals("abc")); // NullPointerException
                // 里面的异常不能有父子关系
            } catch (ArrayIndexOutOfBoundsException | ArithmeticException | NullPointerException e) {
                System.out.println("数组索引越界");
            } catch (Exception e) {
                
                System.out.println("万能捕获异常");
            }
            System.out.println("最后执行");
        }
    }
    
  3. 如果try中遇到异常没有被捕获,相当于try...catch白写了,最终还是会交给JVM进行处理,程序也就到此结束了。

  4. 如果try 中遇到异常,就会直接跳到catch中执行。try中的其他代码就不会执行了。

4、Throwable的成员方法

方法名称说明
public String getMessage()返回throwable的详细消息字符串
public String toString()返回此可抛出简短的异常描述
public void printStackTrace()把异常的错误信息以红色字体输出在控制台
// Throwable的成员方法
public class ExceptionDemo03 {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6};

        try {
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            // getMessage()
            String message = e.getMessage();
            System.out.println(message);
            // toString()
            String str = e.toString();
            System.out.println(str);
            // printStackTrace
            e.printStackTrace();
        }
        
        // 输出字体颜色为红色,用来打印错误信息
        System.err.println("打印红色字体");
    }
}

5、throw和throws

  • throw:写在方法内,手动抛出异常,交给调用者,结束方法。

    public void 方法(){
        throw new NullPointerException();
    }
    
  • throws:写在方法定义处,表示声明一个异常,告诉调用者,使用本方法可能会有哪些异常。编译时异常必须要写,运行时异常可以不写。

    public void 方法() throws 异常类名1,异常类名2...{
        
    }
    

6、自定义异常

作用:就是为了让控制台的报错信息更加的见名知意。

6.1、步骤
  1. 定义异常类
  2. 写继承关系:编译异常继承Exception;运行时异常继承RuntimeException
  3. 空参构造
  4. 带参构造
6.2、例子
public class NameFormatException extends RuntimeException{
    public NameFormatException() {
    }

    public NameFormatException(String message) {
        super(message);
    }
}

六、File文件操作

1、File的构造方法

  • 作用:将文件路径变成一个文件对象,这样才能调用文件的方法。
  • File对象表示路径,可以是文件,也可以是文件夹。这个路径可以存在,也可以不存在。
方法名称说明
public File(String pathName)根据文件路径创建文件对象
public File(String parent, String child)根据父路径字符串和子路径字符串创建文件对象
public File(File parent, String child)根据父路径文件对象和子路径字符串创建文件对象
// File构造方法举例说明
public class FileDemo01 {
    public static void main(String[] args) {

        // 方式一
        String path1 = "E:\\Desktop\\图片\\a.jpg";
        File file1 = new File(path1);
        // 输出:E:\Desktop\图片\a.jpg
        System.out.println(file1);

        // 方式二
        String path2 = "E:\\Desktop\\图片";
        String child2 = "a.jpg";
        File file2 = new File(path2, child2);
        // 输出:E:\Desktop\图片\a.jpg
        System.out.println(file2);

        // 方式三
        String path3 = "E:\\Desktop\\图片";
        File file3 = new File(path3);
        // 输出:E:\Desktop\图片
        System.out.println(file3);
        String child4 = "a.jpg";
        File file4 = new File(file3, child4);
        // 输出:E:\Desktop\图片\a.jpg
        System.out.println(file4);
        
    }
}

2、File常见的成员方法

2.1、判断、获取
方法名称说明
public boolean isDirectory()判断此路径名表示的File是否为文件夹
public boolean isFile()判断此路径名表示的File是否为文件
public boolean exists()判断此路径名表示的File是否存在
public long length()返回文件的大小(字节数量)
public String getAbsolutePath()返回文件的绝对路径
public String getPath()返回定义文件时使用的路径
public String getName()返回文件的名称,带后缀
public long lastModified()返回文件的最后修改时间(时间毫秒值)
2.2、创建、删除
方法名称说明
public boolean createNewFile()创建一个新的空文件
public boolean mkdir()创建单级文件
public boolean mkdirs()创建多级文件夹
public boolean delete()删除文件、空文件夹,直接删除,不走回收站
2.3、获取并遍历

public File[] listFile() :获取当前路径下所有内容。

注意:

  • 当路径不存在、路径为文件、该路径的文件夹权限不足时,返回null
  • 当该路径为空文件夹时,返回长度为0的空数组。
  • 返回的内容会包含隐藏文件。
方法名称说明
public static File[] listRoots()列出可用的文件系统根
public String[] list获取当前该路径下所以内容,返回String[]
public String[] list(FilenameFilter filter)利用文件名过滤器获取当前该路径下所有内容
public File[] listFiles(FileFilter filter)利用文件名过滤器获取当前该路径下所有内容
public File[] listFiles(FilenameFilter filter)利用文件名过滤器获取当前该路径下所有内容
// 文件获取和遍历
public class FileDemo02 {
    public static void main(String[] args) {
        // 列出可用的文件系统根
        File[] roots = File.listRoots();
        // 输出:[C:\, D:\, E:\]
        System.out.println(Arrays.toString(roots));

        // 获取当前该路径下所以内容
        File file = new File("E:\\Desktop\\新建文件夹");
        String[] files = file.list();
        // 输出:[a.txt, aaa, b.txt, bbb, ccc]
        System.out.println(Arrays.toString(files));

        // 列出所有
        File[] files1 = file.listFiles();
        // [E:\Desktop\新建文件夹\a.txt, E:\Desktop\新建文件夹\ccc, ....]
        System.out.println(Arrays.toString(files1));

        // 利用文件名过滤器获取当前该路径下所有内容
        // list(FilenameFilter filter)
        String[] list1 = file.list(new FilenameFilter() {

            /**
             * 例子:E:\Desktop\新建文件夹\aaa
             * @param dir:父路径 ==> E:\Desktop\新建文件夹
             * @param name:子文件名 ==> aaa
             * @return 为true表示保留,为false表示过滤掉
             */

            @Override
            public boolean accept(File dir, String name) {
                return true;
            }
        });
        // 输出:[a.txt, aaa, b.txt, bbb, ccc]
        System.out.println(Arrays.toString(list1));
        // listFiles(FileFilter filter)
       file.listFiles(new FileFilter() {
           @Override
           // pathname: E:\Desktop\新建文件夹\aaa  E:\Desktop\新建文件夹\b.txt 。。。。
           public boolean accept(File pathname) {
               return false;
           }
       });
       
       file.listFiles(new FilenameFilter() {
           @Override
           public boolean accept(File dir, String name) {
               return false;
           }
       });
    }
}

七、IO流-字节流

用于读写文件中的数据(可以读写文件,或者网络中的数据。。。)

1、介绍

  1. 什么是IO

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

    Iinput 输入流,读取到内存

    Ooutput 输出流,写入到磁盘

    流:向流水一样传输数据

  2. IO流的作用

    用于读写数据(本地文件,网络)

  3. IO流按照流向可以分为哪两种流

    输出流:程序(内存)===》 文件(磁盘)

    输入流:文件(磁盘)===》 程序(内存)

  4. IO流按照操作文件的类型可以分为哪两种流

    字节流:可以操作所有类型的文件

    字符流:只能操作纯文本文件

  5. 什么是纯文本文件

    使用Windows系统自带的记事本打开,并且能读懂的文件

    txt文件,md文件,xml文件,lrc文件等

在这里插入图片描述

2、字节流

在这里插入图片描述

2.1、FileOutputStream
2.1.1、介绍

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

书写步骤:

  1. 创建字节输出流对象
  2. 写数据
  3. 释放资源

注意:

字节输出流的细节:
            1.创建字节输出流对象
                细节1:参数是字符串表示的路径或者File对象都是可以的
                细节2:如果文件不存在,会创建一个新文件,但是要保证父级路径是存在的
                细节3:如果文件已经存在,则会清空文件
            2.写数据
                write方法的参数是整数时,实际上写到本地文件中的是整数在ASCII上对应的字符
            3.释放资源
                每次使用完流之后都要释放资源,如果不释放会一直占用这个资源
// FileOutputStream
public class IODemo01 {
    public static void main(String[] args) throws IOException {

/*
        字节输出流的细节:
            1.创建字节输出流对象
                细节1:参数是字符串表示的路径或者File对象都是可以的
                细节2:如果文件不存在,会创建一个新文件,但是要保证父级路径是存在的
                细节3:如果文件已经存在,则会清空文件
            2.写数据
                write方法的参数是整数时,实际上写到本地文件中的是整数在ASCII上对应的字符
            3.释放资源
                每次使用完流之后都要释放资源,如果不释放会一直占用这个资源


*/

        // 1. 创建对象
        FileOutputStream fos = new FileOutputStream("mymap\\a.txt");
        // 2.写出数据
        fos.write(97);
        // 3.释放资源
        fos.close();
    }
}
2.1.2、FileOutputStream写数据的3种方式
方法名称说明
void write(int b)一次写一个字节数据
void write(byte[] b)一次写一个字节数组数据
void write(byte[] b, int off, int len)一次写一个字节数组的部分数据,off是起始索引,len是长度
// FileOutputStream写入数据的3种方式
public class IODemo02 {
    public static void main(String[] args) throws IOException {
        // 1.创建对象
        FileOutputStream fos = new FileOutputStream("./a.txt");

        // 2.写出数据
        // 方式1
        fos.write(97); // a
        // 方式2
        byte[] bytes = {97, 98, 99, 100, 101};
        fos.write(bytes); // abcde
        // 方式3
        fos.write(bytes,1,2); // bc

        // 关闭资源
        fos.close();
    }
}
2.1.3、换行和续写
// FileOutputStream 换行和续写
public class IODemo03 {
    public static void main(String[] args) throws IOException {

/*
        换行写:
            再次写出一个换行符就可以了
            windows:\r\n
            Linux:\n
            Mac:\r
        细节:
            在windows操作系统中,java对回车键进行了优化。
            虽然完整的是\r\n,但是我们写其中一个\r或者\n
            java也可以实现换行,java在底层会补全
        建议:
            不要省略,还是写全了

        续写:
            如果想要续写,打开续写开关即可
            开关默认位置:在创建FileOutputStream对象的第二个参数
            默认为false:表示关闭续写,此时创建对象会清空文件
            手动传递true:表示打开续写,此时创建对象不会清空文件
*/

        // 1.创建FileOutputStream对象
        FileOutputStream fos = new FileOutputStream("./a.txt", true);
        // 2.写出数据
        String str = "abcdefg";
        byte[] bytes1 = str.getBytes();
        fos.write(bytes1);

        // 写入一个换行符
        String wrap = "\r\n";
        byte[] bytes2 = wrap.getBytes();
        fos.write(bytes2);

        // 再次写入
        String str2 = "666";
        byte[] bytes3 = str2.getBytes();
        fos.write(bytes3);

        // 关闭资源
        fos.close();
    }
}
2.2、FileInputStream
2.2.1、介绍

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

书写步骤:

  1. 创建字节输入流对象
  2. 写数据
  3. 释放资源

注意:

FileInputStream书写细节
	1.创建字节输入流对象
		如果文件不存在,就直接报错
    2.读取数据
    	细节1:一次读取一个字节,读出来的数据会转换成ASCII上的对应的数字
    	细节2:文件读完时,read方法返回-1
    3.释放资源
    	每次使用完流必须释放资源
// FileInputStream
public class IODemo04 {
    public static void main(String[] args) throws IOException {


        // 1.创建输入流对象
        FileInputStream fis = new FileInputStream("./a.txt");
        // 读取数据:abc
        int read1 = fis.read();
        System.out.println(read1);// 97

        int read2 = fis.read();
        // 转成字符
        System.out.println((char) read2);// b

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

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

        // 关闭资源
        fis.close();
    }
}
2.2.2、FileInputStream的循环读取
// FileInputStream
public class IODemo05 {
    public static void main(String[] args) throws IOException {
        // 创建FileInputStream对象
        FileInputStream fis = new FileInputStream("./a.txt");
        int b;
        // 循环读取
        /**
         * fis.read() 表示读取数据,而且是读取一个数据就移动一次指针
         */
        while ((b = fis.read()) != -1){
            System.out.print((char) b + " ");
        }
        // 释放资源
        fis.close();
    }
}

2.3.3、拷贝文件案例
// 文件拷贝
public class IODemo06 {
    public static void main(String[] args) throws IOException {
        // 1.创建对象
        FileInputStream fis = new FileInputStream("./a.txt");
        FileOutputStream fos = new FileOutputStream("./b.txt");
        // 放读取到的内容
        int b;
        // 2.拷贝:边读边写
        while ((b = fis.read()) != -1) {
            fos.write(b);
        }
        // 3.关闭资源:先开的最后关闭
        fos.close();
        fis.close();
    }
}
2.3.4、一次读取多个字节
// 读取读数据:一次读取多个字节数据,和数组长度有关
public class IODemo07 {
    public static void main(String[] args) throws IOException {
        // 创建对象
        FileInputStream fis = new FileInputStream("./a.txt");
        // 放读取数据的长度
        int len;
        // 放读取数据的内容
        byte[] bytes = new byte[2];
        // 开始循环读取
        while ((len = fis.read(bytes)) != -1) {
            String str = new String(bytes,0,len);
            System.out.print(str);
        }
        // 关闭资源
        fis.close();
    }
}

注意:一次读一个字节数组的数据,每次读取会尽可能把数组装满。

2.3.5、文件拷贝改写
// 文件拷贝改写:一次读取一个字节数组
public class IODemo08 {
    public static void main(String[] args) throws IOException {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建对象
        FileInputStream fis = new FileInputStream("./a.txt");
        FileOutputStream fos = new FileOutputStream("./b.txt");
        // 存放读取数据的长度
        int len;
        // 存放读取数据的内容
        byte[] buffer = new byte[1024];
        // 开始循环读取,循环写入
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer,0,len);
        }
        // 关闭资源
        fis.close();
        fos.close();
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("执行了" + (end - start) + "ms");
    }
}
2.3.6、异常处理
// 异常处理
public class IODemo09 {
    public static void main(String[] args){
        // 记录开始时间
        long start = System.currentTimeMillis();

        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 创建对象
            fis = new FileInputStream("./a.txt");
            fos = new FileOutputStream("./b.txt");
            // 存放读取数据的长度
            int len;
            // 存放读取数据的内容
            byte[] buffer = new byte[1024];
            // 开始循环读取,循环写入
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer,0,len);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("执行了" + (end - start) + "ms");
    }
}

在这里插入图片描述

八、字符集详解

在计算机中,任意数据都是以二进制的形式来存储的。

字节:计算机最小的存储单元,1字节等于8个bit 00111000 。ASCII只有128个,但是一个字节可以有256种可能

1、字符集介绍

  1. GB2312字符集 :1980年发布,1981年5月1日实施的简体中文汉字编码国家标准。收录了7445个图形字符,其中包括6763个简体汉字。
  2. BIG5字符集 :台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施。
  3. GBK字符集 : 2000年3月17日发布,收录21003个汉字。包含国家标准GB13000-1种的全部中日韩汉字和BIG5编码中的所有汉字。Windows系统默认使用的就是GBK,系统显示ANSI。
  4. Unicode字符集 : 国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。

2、中英文编码规则(GBK)

GBK字符集完全兼容ASCII字符集

  • 一个英文占一个字节,二进制第一位是0
  • 一个中文占两个字节,二进制高位字节的第一位是1

在这里插入图片描述

2.1、中文

在这里插入图片描述

2.2、英文

在这里插入图片描述

3、中英文编码规则(Unicode)

Unicode字符集的UTF-8编码格式 UTFUnicode Transfer Format

  • 一个英文占一个字节,二进制第一位是0,转成十进制是正数。
  • 一个中文占三个字节,二进制第一位是1,第一个字节转成十进制是负数。
3.1、中文

在这里插入图片描述

3.1、英文

在这里插入图片描述

4、为什么乱码

  1. 使用字节流读取文本文件,每次读取一个字节,但是汉字不止一个字节,所以会乱码,但拷贝文件却不会,因为拷贝文件是将每一个字节以二进制方式写入文件,当你打开文件时,才会自动解码。所有只要编码方式和记事本的解码方式一致就不会乱码。

    在这里插入图片描述

  2. 编码和解码方式不统一

    在这里插入图片描述

5、Java中编码和解码的方法

// Java中编码和解码方式的实现
public class CharSetDemo01 {
    public static void main(String[] args) throws UnsupportedEncodingException {
        /*
        Java中编码的方法
            public byte[] getBytes()                        使用默认方式进行编码
            public byte[] getBytes(String charsetName)      使用指定方式进行编码
        Java中的解码方法
            String(byte[] bytes)                            使用默认方式进行解码
            String(byte[] bytes, String charsetName)        使用指定方式进行解码
         */

        // 1.编码
        String str = "ab大c鱼d";
        byte[] bytes1 = str.getBytes();
        // 输出:[97, 98, -27, -92, -89, 99, -23, -79, -68, 100]
        System.out.println(Arrays.toString(bytes1));

        byte[] bytes2 = str.getBytes("GBK");
        // 输出:[97, 98, -76, -13, 99, -45, -29, 100]
        System.out.println(Arrays.toString(bytes2));

        // 2.解码
        String str2 = new String(bytes1);
        // 输出:ab大c鱼d
        System.out.println(str2);

        String str3 = new String(bytes1,"GBK");
        // 输出:ab澶楸糳
        System.out.println(str3);
    }
}

九、IO流-字符流

1、介绍

字符流的底层其实就是字节流

字符流 = 字节流 + 字符集

特点:

  • 输入流:一次读一个字节,遇到中文时,一次读多个字节。
  • 输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中。

使用场景:对于纯文本文件进行读写操作。

在这里插入图片描述

2、字符流

在这里插入图片描述

2.1、FileReader
2.1.1、FileReader无参read读取
// FileReader  read() 空参
public class CharSetDemo02 {
    public static void main(String[] args) throws IOException {
        /*
            第一步:创建对象
            public FileReader(File file)        创建字符输入流关联本地文件
            public FileReader(String pathName)  创建字符输入流关联本地文件

            第二步:读取数据
            public int read()                   读取数据,读到末尾返回-1
            public int read(char[] buffer)      读取多个数据,读到文件末尾返回-1

            第三步:释放资源
            public void close()                 释放资源/关流
         */

        // 1.创建对象并关联本地文件
        FileReader fr = new FileReader(new File("./a.txt"));
        // 2.读取数据  read()
        // 字符流底层也是字节流,默认也是一个字节一个字节的读取
        // 如果遇到中文就会一次读取多个,GBK一次读取两个字节,UTF-8一次读取三个字节

        /*
            read() 细节:
            1.read():默认也是一个字节一个字节的读取,如果遇到中文就会一次读取多个
            2.在读取之后,方法的底层还会进行解码并转成十进制。最终把这个十进制作为返回值。
                这个十进制的数据业表示在字符集上的数字
                英文:文件里面的二进制数据 0110 0001
                        read方法进行读取,解码并转成十进制97
                中文:文件里面的二进制数据 11100110 10110001 10001001
                    read方法进行读取,解码并转成十进制27721
            如果项看到中文汉字,就是把这些十进制数据,再进行强转就可以了
         */
		// 存放读取到的数据
        int ch;
        while ((ch = fr.read()) != -1) {
            System.out.print((char) ch);
        }

        // 3.释放资源
        fr.close();
    }
}
2.1.2、FileReader有参read读取
// FileReader  read(chars)  有参
public class CharSetDemo03 {
    public static void main(String[] args) throws IOException {
        // 1.创建对象并关联本地文件
        FileReader fr = new FileReader(new File("./a.txt"));
        // 存放读取数据的长度
        int len;
        // 存放读取到的数据
        char[] chars = new char[2];
        // 2.读取数据
        // fr.read(chars):读取数据,解码,强转三步合并了,把强转之后的字符放到数组当作了
        while ((len = fr.read(chars)) != -1) {
            System.out.print(new String(chars,0,len));
        }
        // 3.释放资源
        fr.close();
    }
}
2.1.3、字符输入流底层原理
  1. 创建字符输入流对象
    • 底层:关联文件,并创建缓冲区(长度为8192的字节数组)
  2. 读取数据
    • 底层:判断缓冲区中是否有数据可以读取
      • 缓冲区中没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区,如果文件中也没有数据了就返回-1
      • 缓冲区有数据:就从缓冲区读取
        • 空参的read方法:一次读取一个字节,遇到中文一次读取多个字节,把字节解码并转成十进制返回
        • 有参的read方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中
2.2、FileWriter
2.2.1、书写细节
  1. 创建字符输出流对象

    • 参数是字符串表示的路径或者File对象都可以
    • 如果文件不存在会创建一个新文件,但是要保证父级路径是存在的
    • 如果文件已经存在,则会清空文件,如果不想清空可以打开续写开关
  2. 写数据

    • 如果writer方法的参数是整数,但是实际上写到本地文件中的是整数在字符集上对应的字符
  3. 释放资源

// FileWriter
public class CharSetDemo04 {
    public static void main(String[] args) throws IOException {
        /*
            第一步:创建对象
                public FileWriter(File file)                        创建字符输出流关联本地文件
                public FileWriter(String pathName)                  创建字符输出流关联本地文件
                public FileWriter(File file,boolean append)         创建字符输出流关联本地文件,续写开关
                public FileWriter(String pathName,boolean append)   创建字符输出流关联本地文件,续写开关
            第二步:读取文件
                void write(int c)                                   写出一个字符
                void write(String str)                              写出一个字符串
                void write(String str,int off,int len)              写出一个字符串的一部分
                void write(char[] cbuf)                             写出一个字符数组
                void write(char[] cbuf,int off,int len)             写出一个字符的一部分
            第三步:释放资源
                public void close()       释放资源/关流
         */

        // 1.创建对象
        FileWriter fw = new FileWriter("./a.txt", true);
        // 2.写入数据
        fw.write(25105);
        // 2.写入数据
        char[] chars = {'a', 'b', 'c', '我'};
        fw.write(chars);
        // 3.释放资源
        fw.close();
    }
}
2.2.2、字符输出流底层原理

fw.write(chars) 会将数据放到缓冲区(长度为8192的字节数组),当出现以下三种情况时,才会将数据写入到文件中

  • 情况一:缓冲区装满了,数据到了8193个字节
  • 情况二:手动执行fw.flush(),将缓冲区中的数据写到到文件中
  • 情况三:释放资源时fw.close(),也会将缓冲区里面的数据写到文件中

fw.flush()fw.close()的区别在于前者还可以写数据,后者无法再写数据,通道已经关闭。

十、IO-缓冲流

在这里插入图片描述

1、字节缓冲流

在这里插入图片描述

// 字符缓冲流
public class IOBufferDemo01 {
    public static void main(String[] args) throws IOException {
        /*
         *  需求:
         *      利用字节缓冲流拷贝文件
         *
         *  字节缓冲输入流的构造方法:
         *      public BufferedInputStream(InputStream is)
         *
         *  字节缓存输出流的构造方法:
         *     public BufferedInputStream(OutputStream os)
         **/

        // 1.创建缓冲流对象
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./a.txt"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./b.txt"));

        // 方式一:
        // bis.read() 读取数据到BufferedInputStream的缓冲区
        // bos.write(b) 一个字节一个字节的将数据放到BufferedOutputStream的缓冲区
        /*int b; // 存放读取到的数据
        while ((b = bis.read()) != -1) {
            bos.write(b);
        }*/

        // 方式二:
        // bis.read(bytes) 读取数据到BufferedInputStream的缓冲区
        // bos.write(bytes, 0, len) 将一个bytes的字节数组的数据放到BufferedOutputStream的缓冲区
        int len;  // 存放读取到数据的长度
        byte[] bytes = new byte[1024]; // 存放读取到的数据
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        // 3.释放资源
        bos.close();
        bis.close();
    }
}

2、字符缓冲流

// 字符缓冲流
public class IOBufferDemo02 {
    public static void main(String[] args) throws IOException {
        /**
         *  字符缓冲输入流:
         *      构造方法:
         *          public BufferedReader(Reader r)
         *      特有方法:
         *          public String readLine()  读一整行
         *
         *  字符缓冲输出流:
         *      构造方法:
         *          public BufferedWriter(Writer r)
         *      特有方法:
         *          public void newLine()  跨平台换行
         *
         */

        // 1.创建字符缓冲输入流对象
        BufferedReader br = new BufferedReader(new FileReader("./a.txt"));
        // 创建字符缓冲输出流
        BufferedWriter bw = new BufferedWriter(new FileWriter("./b.txt"));
        // 2.读取数据:readLine一次读一整行,遇到回车换行结束,但是不会把换行读到内存中
        String line;
        while ((line = br.readLine()) != null) {
            bw.write(line);
            bw.newLine(); // 换行
        }
        // 3.释放资源
        bw.close();
        br.close();
    }
}

注意:br.readLine()返回值是String,当读完是时返回null

十一、IO-转换流

1、转换流继承体系

在这里插入图片描述

2、转换流原理和作用

  • 由于FileInputStream按字节读取时,读到中文时乱码,但是FileInputStreambyte[] bytes = new byte[3]三个字节读取时不会乱码,在UTF-8编码中,中文就是三个字节表示的。
  • 转换流就是将文件中的数据按照字节读取,这样不管说中文还是英文都是一样的,然后指定字符集,进行解码,那么内存中的数据就是按字符存在,然后又指定想要的编码,将数据按指定的字符集编码成字节写入文件。
  • InputStreamReaderOutputStreamWriterJDK11的时候就淘汰了,现在使用FileReaderFileWriter

在这里插入图片描述

3、示例

// 转换流  JDK11
public class IOBufferDemo03 {
    public static void main(String[] args) throws IOException {
        // 指定读取字符集
        FileReader fr = new FileReader("./a.txt", Charset.forName("GBK"));
        // 指定写入文件的字符集
        FileWriter fw = new FileWriter("./b.txt", Charset.forName("UTF-8"));

        int b;
        while ((b = fr.read()) != -1) {
            fw.write(b);
        }
        fw.close();
    }
}

十二、IO-序列化流和反序列化流

创建一个Student的JavaBean,并且实现Serializable的接口

// Serializable接口里面没有抽象方法,是标记型接口
// 一旦实现类这个接口,那么就表示当前的Student类可以被序列化
public class Student implements Serializable {
    private String name;
    private int age;
    ......
}

1、序列化流 ObjectOutputStream

// 序列化流
public class IOObjectDemo01 {
    public static void main(String[] args) throws IOException {
        /*
            需求:
                利用序列化流/对象操作输出流,把一个对象写到本地文件中
            构造方法:
                public ObjectOutputStream(OutputStream out)     把基本流变成高级流
            成员方法:
                public final void writeObject(Object obj)       把对象序列化(写出)到文件中
         */

        // 1.创建对象
        Student student = new Student("zhangsan", 23);
        // 2.创建序列化流的对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./c.txt"));
        // 3.写出数据
        oos.writeObject(student);
        // 4.释放资源
        oos.close();
    }
}

注意:使用对象输出流将对象保存到文件时会出现NotSerializableException异常,需要让JavaBean类实现Serializable接口

2、反序列化流 ObjectOutputStream

// 序列化流
public class IOObjectDemo01 {
    public static void main(String[] args) throws IOException {
        /*
            需求:
                利用序列化流/对象操作输出流,把一个对象写到本地文件中
            构造方法:
                public ObjectOutputStream(OutputStream out)     把基本流变成高级流
            成员方法:
                public final void writeObject(Object obj)       把对象序列化(写出)到文件中
         */

        // 1.创建对象
        Student student = new Student("zhangsan", 23);
        // 2.创建序列化流的对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./c.txt"));
        // 3.写出数据
        oos.writeObject(student);
        // 4.释放资源
        oos.close();
    }
}

3、总结

  1. 使用对象输出流将对象保存到文件时会出现NotSerializableException异常,需要让JavaBean类实现Serializable接口
  2. 序列化流写到文件中的数据是不能修改的,一旦修改就无法在次读回来。
  3. 序列化写到文件中后修改了JavaBean,再次反序列化,会抛出InvalidClassException异常。解决方法:给JavaBean类添加serialVersionUID(序列号,版本号)。
  4. 如果一个对象中的某个成员变量不想被序列化,可以给该成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程。
// Serializable接口里面没有抽象方法,是标记型接口
// 一旦实现类这个接口,那么就表示当前的Student类可以被序列化
public class Student implements Serializable {

    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    // transient  瞬态关键字
    // 作用:序列化时忽略该字段
    private transient String address;
    .....
}

十三、多线程

1、概念

  • 进程:程序的基本执行实体。
  • 线程:操作系统能够进行运算调度的最小单位。他被包含在进程之中,是进程中的实际运作单位。
  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。
  • 并行:在同一时刻,有多个指令在多个CPU上同时执行,CPU有多个核心。

2、多线程的实现方式

2.1、继承Thread类的方式进行实现
// 多线程的第一种启动方式  Thread
public class ThreadDemo01 {
    public static void main(String[] args) {
        /*
            多线程的第一种启动方式:
                1.自己定义一个类继承Thread
                2.重写run方法
                3.创建子类的对象,并启动线程
         */
        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        // 设置线程的名字
        t1.setName("线程1");
        t2.setName("线程2");
        // 启动线程
        t1.start();
        t2.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        // 书写线程要执行的代码
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "执行了");
        }
    }
}
2.2、实现Runnable接口的方式进行实现
// 多线程的第二种启动方式 Runnable
public class ThreadDemo02 {
    public static void main(String[] args) {
        /*
            多线程的第二种启动方式:
                1.自己定义一个类实现Runnable接口
                2.重写里面的run方法
                3.创建自己的类的对象
                4.创建一个Thread类的对象,并开启线程
         */

        // 创建MyRunnable的对象,表示多线程要执行的任务
        MyRunnable mr = new MyRunnable();

        // 创建线程对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        // 开启线程
        t1.start();
        t2.start();
    }
}

class MyRunnable implements Runnable{

    @Override
    public void run() {
        // 书写线程要执行的代码
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "执行了");
        }
    }
}
2.3、利用Callable接口和Future接口方式实现
// 多线程的第三种实现方式  Callable
public class ThreadDemo03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
            多线程的第三种实现方式:
                特点:可以获取到多线程运行的结果

                1.创建一个类MyCallable实现Callable接口
                2.重写call(是有返回值的,表示多线程运行的结果)
                3.创建MyCallable的对象(表示多线程要执行的任务)
                4.创建FutureTask的对象(用来管理多线程运行的结果)
                5.创建Thread类的对象,并启动(表示线程)
         */

        // 创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();
        // 创建FutureTask的对象(用来管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);
        // 创建Thread类的对象
        Thread t1 = new Thread(ft);
        // 启动线程
        t1.start();
        // 获取多线程运行结果
        Integer result1 = ft.get();
        System.out.println(result1);
    }
}

class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        // 求1~100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}
2.4、多线程三种实现方式对比
优点缺点
继承Thread类编程简单,可以直接使用Thread类中的方法可扩展性较差,继承了它,就不能再继承别的类了
实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能直接使用Thread类中的方法
实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类,还可以获取线程的返回值编程相对复杂,不能直接使用Thread类中的方法

3、线程的优先级

线程的优先级默认都是5,优先级越高,调用的概率就大。

// 线程的优先级
public class ThreadDemo04 {
    public static void main(String[] args) {
        MyThread1 myThread1 = new MyThread1();
        // 设置线程优先级,默认为5,优先级越高,优先调用,也是概率问题,都不一定,main函数的优先级默认也是5
        myThread1.setPriority(4);
        // 获取线程的优先级
        int priority = myThread1.getPriority();
        System.out.println(priority); // 4
    }
}

class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + "执行了");
    }
}

4、守护线程

守护线程:当非守护线程结束时,会通知守护线程结束,这个通知过程也存在时间,会有延迟

// 设置守护线程
public class ThreadDemo05 {
    public static void main(String[] args) {
        /**
         * 守护线程:当非守护线程结束时,会通知守护线程结束,这个通知过程也存在时间
         *
         */
        MyThread2 myThread2 = new MyThread2();
        MyThread3 myThread3 = new MyThread3();
        // 设置myThread2为守护线程
        myThread2.setDaemon(true);

        myThread2.setName("myThread2");
        myThread3.setName("myThread3");

        myThread2.start();
        myThread3.start();
    }
}


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

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

5、礼让线程(了解) yield

礼让线程:Thread.yield(); 表示把当前cup的执行权礼让出去,但还是会抢夺。就是看概率。

// 礼让线程  yield
public class ThreadDemo06 {
    public static void main(String[] args) {
        MyThread4 mt1 = new MyThread4();
        MyThread5 mt2 = new MyThread5();

        mt1.setName("飞机");
        mt2.setName("坦克");

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

class MyThread4 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@" + i);
            Thread.yield();
        }
        // 礼让线程:表示把当前cpu的执行权给出去

    }
}


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

    }
}

6、插入线程(了解) join

插入线程:使用了join的线程会优先执行,join要放在start后面执行完成后才会执行没有使用join的线程

// 插入线程/插队线程  join
public class ThreadDemo07 {
    public static void main(String[] args) throws InterruptedException {
        MyThread06 mt = new MyThread06();
        mt.setName("土豆");
        mt.start();
        // 表示把mt这个线程插入到当前main函数线程的前面,它执行完了才会轮到main函数
        mt.join();
        // 接着执行main函数线程里面的代码
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "@" + i);
        }
    }
}

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

7、线程的生命周期

在这里插入图片描述

8、同步代码块 synchronized

同步代码块:把操作共享数据的代码锁起来,锁默认是打开的,当有一个线程进去了,锁就会自动关闭,线程出来了,锁才会打开。

// 同步代码块 synchronized
public class ThreadDemo08 {
    public static void main(String[] args) {
        /**
         * 模拟三个窗口买票
         */
        MyThread7 mt1 = new MyThread7();
        MyThread7 mt2 = new MyThread7();
        MyThread7 mt3 = new MyThread7();

        mt1.setName("窗口1");
        mt2.setName("窗口2");
        mt3.setName("窗口3");

        mt1.start();
        mt2.start();
        mt3.start();
    }
}


class MyThread7 extends Thread {
    // static 表示这个类的所有对象都共享这个变量
    static int ticket = 0;
    //锁对象,一定要唯一
    // static Object object = new Object();

    @Override
    public void run() {
        while (true) {
            // 同步代码块,参数是锁对象,必须唯一
            synchronized (ThreadDemo08.class) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
                } else {
                    break;
                }
            }
        }
    }
}

9、同步方法

同步方法就是把synchronized关键字加到方法上

特点:

  • 同步方法是锁住方法里面所有的代码
  • 锁对象不能自己指定
    • 非静态方法,锁住的是this
    • 静态方法,锁住的是当前类是字节码对象
// 同步方法
public class ThreadDemo09 {
    public static void main(String[] args) {
        MyRunnable09 mr = new MyRunnable09();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

}

class MyRunnable09 implements Runnable {

    int ticket=0;

    @Override
    public void run() {
        while (true){
            if (method()) return;
        }
    }

    private synchronized boolean method() {
        // 同步代码块(同步方法)
        synchronized (MyRunnable09.class){
            if (ticket == 100){
                return true;
            }else {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(Thread.currentThread().getName()+"正在卖"+ticket+"张票");
            }
        }
        return false;
    }
}

拓展:

  • StringBuilder线程不安全,方法没有加synchronized
  • **StringBuffer线程安全,加了synchronized **

10、Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • Lock提供了获取锁和释放锁的方法,手动上锁,手动释放锁
    • void lock() 获得锁
    • viod unlock() 释放锁
  • Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock 来实例化ReentrantLock 的构造方法
  • ReentrantLock() :创建一个ReentrantLock实例
// Lock
public class ThreadDemo10 {
    public static void main(String[] args) {
        MyRunnable09 mr = new MyRunnable09();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

class MyRunnable10 implements Runnable {

    int ticket = 0;

    // 要加static,表示不管new多少个MyRunnable10类lock始终只有一个
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket == 100) {
                    break;
                } else {
                    Thread.sleep(10);
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "" + ticket + "");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

11、生产者和消费者(等待唤醒机制)

生产者消费者模式是一个十分经典的多线程协作模式。

方法名称说明
void wait()当前线程等待,直到被其他线程唤醒
void notify()随机唤醒单个线程
void notifyAll()唤醒所有线程
// 消费者和生产者
public class ThreadDemo11 extends Thread {
    public static void main(String[] args) {
        // 创建线程对象
        Cook cook = new Cook();
        Foodie foodie = new Foodie();

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

}

// 桌子--线程的锁
class Desk {
    /**
     * 控制生产者和消费者执行的那把锁
     */

    // 是否有面条  0:没有面条  1:有面条
    public static int foodFlag = 0;

    // 总共吃多少碗
    public static int count = 10;

    // 锁对象
    public static Object lock = new Object();
}

// 消费者--吃货
class Foodie extends Thread{
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    // 先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        // 如果没有,就等待
                        try {
                            Desk.lock.wait(); // 让当前线程跟锁进行绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        // 把吃的碗数-1
                        Desk.count--;
                        // 如果有,就开吃
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
                        // 吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();
                        // 修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

// 生产者--厨师
class Cook extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                if (Desk.count == 0){
                    break;
                }else {
                    if (Desk.foodFlag == 1){
                        // 如果有,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        // 如果没有,就制作食物
                        System.out.println("厨师做了一碗面条");
                        // 修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        // 叫醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

12、等待唤醒机制(阻塞队列方式实现)

// 阻塞队列实现等待唤醒机制
public class ThreadDemo12 {
    public static void main(String[] args) {
        /**
         *  需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
         *  细节:
         *      1.生产者和消费者必须使用同一个阻塞队列
         *      2.take和put里面是有锁的,但是外面的代码没有锁,因此外部的打印语句不会按顺序执行
         */

        // 1.创建阻塞队列的对象,并设置队列长度
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        // 2. 创建线程对象,并把阻塞队列传递过去
        Cook2 cook2 = new Cook2(queue);
        Foodie2 foodie2 = new Foodie2(queue);

        cook2.start();
        foodie2.start();
    }
}

class Foodie2 extends Thread{

    ArrayBlockingQueue<String> queue;

    public Foodie2(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            // 不断从阻塞队列中取出面条
            try {
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Cook2 extends Thread{

    ArrayBlockingQueue<String> queue;

    public Cook2(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            // 不断从阻塞队列中取出面条
            try {
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

13、多线程的六种状态

在这里插入图片描述

在这里插入图片描述

14、线程池

1、线程池主要核心原理
  1. 创建一个池子,池子中是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有线程即可。
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。
2、线程池代码实现

Executors线程池的工具类通过调用方法返回不同类型的线程池对象。

方法名称说明
public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads)创建有上限的线程池
  • 自定义线程执行的任务

    // 自定义线程执行的任务
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
    
  • newCachedThreadPool:创建一个没有上限的线程池

    // 创建一个没有上限的线程池
    public class PoolThread01 {
        public static void main(String[] args) throws InterruptedException {
            /**
             * 创建一个没有上限的线程池
             *      public static ExecutorService newCachedThreadPool();
             */
    
            // 1. 获取线程池对象
            ExecutorService pool = Executors.newCachedThreadPool();
    
            // 线程休眠
            // Thread.sleep(1000);
    
            // 2.提交多个线程任务
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
    
            // 3.销毁线程池
            pool.shutdown();
        }
    }
    
  • newFixedThreadPool(int nThreads):创建有上限的线程池

    // 创建一个没有上限的线程池
    public class PoolThread02 {
        public static void main(String[] args) throws InterruptedException {
            /**
             * 创建一个指定线程数的线程池
             *      public static ExecutorService newFixedThreadPool(int nThreads)
             */
    
            // 1. 获取线程池对象
            ExecutorService pool = Executors.newFixedThreadPool(3);
    
            // 线程休眠
            // Thread.sleep(1000);
    
            // 2.提交多个线程任务
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
            pool.submit(new MyRunnable());
    
            // 3.销毁线程池
            pool.shutdown();
        }
    }
    
3、自定义线程池

比较灵活,可以自定义参数

// 自定义线程池
public class PoolThread03 {
    public static void main(String[] args) {
        /*
            ThreadPoolExecutor pool = new ThreadPoolExecutor
            (核心线程数,最大线程数,空闲线程最大存活时间,时间单位,任务队列,创建线程工厂,任务拒绝策略);

            参数一:核心线程数               不能小于0
            参数二:最大线程数               不能小于0   最大线程数=核心线程数+临时线程数
            参数三:       不能小于0
            参数四:时间单位                 用TimeUnit指定
            参数五:任务队列                 不能为null
            参数六:创建线程工厂              不能为null
            参数七:任务拒绝策略              不能为null

         */

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, // 核心线程数
                6, // 最大线程数
                60, // 空闲线程最大存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(3), // 任务队列
                Executors.defaultThreadFactory(), // 创建线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
        );

        // 提交任务
        // pool.submit();
    }
}
任务拒绝策略说明
ThreadPoolExecutor.AbortPolicy默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy丢弃任务,但不抛出异常,这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务(队列先进去的肯定排的久),然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy调用入的run()方法绕过线程池直接执行

不断提交任务,会有以下三个临界点:

  1. 当核心线程满时,再提交任务就会排队。
  2. 当核心线程满、队伍满时,会创建临时线程。
  3. 当核心线程满、队伍满、临时线程满时,会触发任务拒绝策略。
4、线程池设置多大合适

在这里插入图片描述

查看电脑线程数

// 查看电脑线程数
public class PoolThread04 {
    public static void main(String[] args) {
        // 向Java虚拟机返回可用处理器的数目
        int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);
    }
}

十四、网络编程

  • 网络编程就是计算机跟计算机之间通过网络进行数据传输。
  • Java中可以使用java.net包下的技术轻松开发出常见的应用程序。

1、创建软件架构

在这里插入图片描述

2、网络编程三要素

  • IP:设备再网络中的地址,是唯一标识。
  • 协议:数据在网络中传输的规则,常见的协议有UDP、TCP、http、https、ftp。
  • 端口号:应用程序在设备中唯一的标识。

3、IP

3.1、IPV4

在这里插入图片描述

3.2、IPV6

在这里插入图片描述

3.3、IP地址的分类形式
  • 公网地址(万维网使用)和私有地址(局域网使用)
  • 192.168.开头的就是私有地址,范围即为192.168.0.0~192.168.255.255,专门为组织机构内部使用,以此节省IP。

可以利用局域网IP解决IPV4不够的问题。

特殊IP地址:

127.0.0.1也可以是localhost :是回送地址,也称本地回环地址,也称本机IP,永远只会寻找当前所在本机。

InetAddress类的使用

// InetAddress类的使用
public class SocketDemo {
    public static void main(String[] args) throws UnknownHostException {
        /**
         *  public static InetAddress getByName(String host)  确认主机名称的IP地址。host可以是电脑名,也可以是IP地址
         *  public String getHostName()     获取此IP的主机名
         *  public String getHostAddress()   返回文本显示中的IP地址字符串
         */

        // InetAddress address = InetAddress.getByName("localhost");
        InetAddress address = InetAddress.getByName("10.61.159.153");
        System.out.println(address); // /10.61.159.153

        // 获取计算机名
        // String hostName = address.getHostName();
        // System.out.println(hostName);

        // 获取IP
        String ip = address.getHostAddress(); // 可能会获取不到
        System.out.println(ip);  // 10.61.159.153
    }
}

4、端口号

  • 应用程序在设备中唯一的标识
  • 端口号:由两个字节标识的整数,取值范围:0~65535,其中0~1023之间的端口号用于一些知名的网络服务或者应用。
  • 一个端口号只能被一个应用程序使用

5、协议

计算机网络中,连接和通信的规则被称为网络通信协议。

  • OSI参考模型(七层):世界互联协议标准,全球通信规范,单模型过于理想化,未能在因特网上进行广泛推广
  • TCP/IP参考模型(或TCP/IP协议):事实上的国际化标准

在这里插入图片描述

5.1、UDP协议
  • 用户数据报协议(User Datagram Protocol)
  • UDP是面向无连接的通信协议。速度快,有大小限制一次最多发送64k,数据不安全,易丢失数据。
  • 使用场景:在线视频、语音通话、网络会议
5.1.1、发送数据
// UDP发送数据
public class SocketDemo02 {
    public static void main(String[] args) throws IOException {

        // 1.创建 DatagramSocket 对象(快递公司)
        // DatagramSocket空参:所有可用的端口中随机挑选一个使用
        // DatagramSocket有参:指定端口
        DatagramSocket ds = new DatagramSocket();

        // 数据
        String str = "你怎么样!!";
        // 将数据转成字节
        byte[] bytes = str.getBytes();
        // 发送到哪台主机
        InetAddress address = InetAddress.getByName("127.0.0.1");
        // 目标端口
        int port = 10086;

        // 2.打包数据
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);

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

        // 4.释放资源
        ds.close();
    }
}
5.1.2、接收数据
// UDP接收数据
public class SocketDemo03 {
    public static void main(String[] args) throws IOException {

        // 1.创建 DatagramSocket 对象(快递公司)
        // 在接收时一定要绑定端口,而且要和发送的端口保持一致
        DatagramSocket ds = new DatagramSocket(10086);

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

        // 该方法是阻塞的,程序执行到这一步,会在这里死等,直到发送端发送数据
        ds.receive(dp);

        // 3.解析数据包
        byte[] data = dp.getData();
        int len = dp.getLength();
        InetAddress address = dp.getAddress();
        int port = dp.getPort();

        System.out.println("接收到数据:" + new String(bytes,0,len));
        System.out.println("该数据是从:" + address + "这台电脑中的" + port + "这个端口发出去的");

        // 4.释放资源
        ds.close();
    }
}
5.1.3、聊天室
// UDP发送数据--聊天室
public class SocketDemo04 {
    public static void main(String[] args) throws IOException {

        // 1.创建 DatagramSocket 对象(快递公司)
        // DatagramSocket空参:所有可用的端口中随机挑选一个使用
        // DatagramSocket有参:指定端口
        DatagramSocket ds = new DatagramSocket();

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("请输入你要说的话:");
            // 数据
            String str = sc.nextLine();
            if ("886".equals(str)){
                break;
            }
            // 将数据转成字节
            byte[] bytes = str.getBytes();
            // 发送到哪台主机
            InetAddress address = InetAddress.getByName("127.0.0.1");
            // 目标端口
            int port = 10086;

            // 2.打包数据
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);

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

        // 4.释放资源
        ds.close();
    }
}
// UDP接收数据--聊天室
public class SocketDemo05 {
    public static void main(String[] args) throws IOException {

        // 1.创建 DatagramSocket 对象(快递公司)
        // 在接收时一定要绑定端口,而且要和发送的端口保持一致
        DatagramSocket ds = new DatagramSocket(10086);

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

        while (true) {
            // 该方法是阻塞的,程序执行到这一步,会在这里死等,直到发送端发送数据
            ds.receive(dp);

            // 3.解析数据包
            byte[] data = dp.getData();
            int len = dp.getLength();
            String ip = dp.getAddress().getHostAddress();
            String name = dp.getAddress().getHostName();

            System.out.println("ip为:" + ip + ",主机名为:" + name + "的人,发送了数据:" + new String(data,0,len));
        }
    }
}
5.1.4、UDP的三种通信方式
  • 单播:一对一
  • 组播:组播地址:224.0.0.0~239.255.255.255,其中224.0.0.0~224.0.0.255为预留的组播地址
  • 广播:广播地址:255.255.255.255

实现(组播):

// 组播端发送数据
public class Broadcast01 {
    public static void main(String[] args) throws IOException {
        // 创建MulticastSocket对象
        MulticastSocket ms = new MulticastSocket();

        // 创建DatagramPacket对象
        String s = "你好,你好!!!";
        byte[] bytes = s.getBytes();
        // 指定组播地址
        InetAddress address = InetAddress.getByName("224.0.0.1");
        int port = 10000;

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

        // 调用MulticastSocket发送数据
        ms.send(datagramPacket);

        // 释放资源
        ms.close();
    }
}
// 组播端接收数据-1
public class Broadcast02 {
    public static void main(String[] args) throws IOException {
        // 创建MulticastSocket对象
        MulticastSocket ms = new MulticastSocket(10000);

        // 将本机添加到224.0.0.1这一组播IP中
        InetAddress address = InetAddress.getByName("224.0.0.1");
        ms.joinGroup(address);

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

        // 接收数据
        ms.receive(dp);

        // 解析数据
        byte[] data = dp.getData();
        int len = dp.getLength();
        String ip = dp.getAddress().getHostAddress();
//        String name = dp.getAddress().getHostName();

//        System.out.println("IP为:" + ip + ",主机名为:" + name +  ",发送了数据:" + new String(data,0,len));
        System.out.println("IP为:" + ip + ",发送了数据:" + new String(data,0,len));
    }
}

注意:

  • 组播接收端可用搞多个
  • udp接收数据端,在获取主机名时好像会丢包,接收不到,多发送几次就可以了。
  • 广播:将单播的IP改为255.255.255.255就行了
5.2、TCP协议
  • 传输控制协议(Transmission Control Protocol)
  • TCP协议是面向连接的通信协议。没有大小限制,数据安全
  • 使用场景:文字消息、邮件、下载软件

TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,通信之前要保证连接已经建立,通过Socket产生IO流来进行网络通信。

5.2.1、发送数据
// TCP协议发送数据--输出流--客户端
public class Client {
    public static void main(String[] args) throws IOException {
        // 1. 创建Socket对象
        // 细节:在创建对象的同时会连接服务端,如果连接不上,代码会报错
        Socket socket = new Socket("127.0.0.1", 10000);

        // 2.可以从连接通道中获取输出流
        OutputStream os = socket.getOutputStream();
        // 3.写出数据
        os.write("你好,你好".getBytes());

        // 4.释放资源
        socket.close();
    }
}
5.2.2、接收数据
// TCP协议发送数据--输入流--服务端
public class Server {
    public static void main(String[] args) throws IOException {
        // 1.创建对象ServerSocket
        ServerSocket ss = new ServerSocket(10000);
        // 2.监听客户端的连接
        Socket socket = ss.accept();
        // 3.从连接通道中获取输入流
        InputStream is = socket.getInputStream(); // 字节流,一个字节的读取
        // 解决中文乱码
        InputStreamReader isr = new InputStreamReader(is); // 转换流,将字节流转换成字符流
        BufferedReader br = new BufferedReader(isr); //缓冲流,读的快
        int b;
        while ((b = br.read()) != -1) {
            System.out.print((char) b);
        }
        // 4.释放资源
        socket.close();
        ss.close();
    }
}

注意:发送端写出结束标记,接收端才会结束接收。

// 结束标记
socket.shutdownOutput();

十五、反射

反射允许对成员变量、成员方法和构造方法的信息进行编程访问。

反射的作用:

  • 获取一个类里面所有的信息,获取到之后再执行其他的业务逻辑。
  • 结合配置文件,动态的创建对象并调用方法。

1、获取class对象的三种方式

// 获取class对象的三种方式
public class Reflect01 {
    public static void main(String[] args) throws ClassNotFoundException {
        /**
         * 获取class对象的三种方式
         *      1.Class.forName("全类名");
         *      2.类名.class
         *      3.对象.getClass();
         */

        // 1.第一种方式:最为常用
        // 全类名:包名+类名
        Class clazz1 = Class.forName("com.gxm.reflect.Student");

        // 第二种方式:一般更多的是当作参数进行传递,比如 synchronized (Student.class)
        Class clazz2 = Student.class;

        // 第三种方式:当我们已经有了这个对象时,才可以使用
        Student student = new Student();
        Class clazz3 = student.getClass();

        System.out.println(clazz1 == clazz2); // true
        System.out.println(clazz2 == clazz3); // true
    }
}

2、利用反射获取构造方法

Constructor<?>[] getConstructors()返回所有公共的构造方法的数组
Constructor<?>[] getDeclaredConstructors()返回所有构造方法对象的数组
Constructor<?>[] getConstructors(Class<?>...parameterTypes)返回单个公共构造方法对象
Constructor<?>[] getDeclaredConstructor(Class<?>...parameterTypes)返回单个构造方法对象
// 通过反射获取构造方法
public class Reflect02 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 1.获取class字节码文件对象
        Class clazz = Class.forName("com.gxm.reflect.Student");

        // 2.获取构造方法
        // 2.1 获取所有公共的构造方法
        Constructor[] cons1 = clazz.getConstructors();
        // 2.2 获取所有构造方法
        Constructor[] cons2 = clazz.getDeclaredConstructors();
        // 2.3 获取单个构造方法
        Constructor cons3 = clazz.getDeclaredConstructor();
        Constructor cons4 = clazz.getDeclaredConstructor(String.class);
        Constructor cons5 = clazz.getDeclaredConstructor(int.class);
        Constructor cons6 = clazz.getDeclaredConstructor(String.class,int.class);

        // 3.获取构造方法的修饰符---以整数形式来表现
        int modifiers = cons6.getModifiers();
        System.out.println(modifiers); // 2

        // 4.获取构造方法的所有参数
        Parameter[] parameters = cons4.getParameters();

        // 5.利用反射获取的构造方法创建对象
        // 临时取消权限校验,这样私有的构造方法也能被调用
        cons4.setAccessible(true);
        Student stu = (Student) cons4.newInstance("张三", 23);
    }
}

3、利用反射获取成员变量

Field[] getFields()返回所有公共成员变量对象的数组
Field[] getDeclaredFields()返回所有成员变量对象的数组
Field[] getField(String name)返回单个公共成员变量对象
Field[] getDeclaredField(String name)返回单个成员变量对象
// 通过反射获取成员变量
public class Reflect03 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        // 1.获取字节码对象
        Class clazz = Class.forName("com.gxm.reflect.Student");

        // 2.获取成员变量
        // 2.1获取所有公共成员变量
        Field[] fields1 = clazz.getFields();
        // 2.2获取所有成员变量
        Field[] fields2 = clazz.getDeclaredFields();
        // 2.3获取单个公共成员变量
        Field fields3 = clazz.getField("gender");
        System.out.println(fields3);
        // 2.4获取单个成员变量
        Field fields4 = clazz.getDeclaredField("name");
        System.out.println(fields4);

        // 3.获取权限修饰符
        int modifiers = fields4.getModifiers();
        
        // 4.获取成员变量的名字
        String name = fields4.getName();
        
        // 5.获取成员变量的数据类型
        Class<?> type = fields4.getType();
        
        // 6.获取成员变量记录的值
        Student student = new Student("zhangsan", 23, "男");
        // 临时取消权限校验
        fields4.setAccessible(true);
        // 获取值
        String value = (String) fields4.get(student);
        
        // 7.修改成员变量的值
        fields4.set(student,"lisi");
    }
}

4、利用反射获取成员方法

Method[] getMethods()返回所有公共成员方法的对象的数组,包括继承的
Method[] getDeclaredMethods()返回所有成员方法对象的数组,不包括继承的
Method[] getMethod()返回单个公共成员方法对象
Method[] getDeclaredMethod()返回单个成员方法对象
// 利用反射获取成员方法
public class Reflect04 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 1.获取字节码对象
        Class<?> clazz = Class.forName("com.gxm.reflect.Student");

        // 2.获取成员方法
        // 2.1获取所有公共的成员方法,包括继承的
        Method[] methods1 = clazz.getMethods();
        // 2.2获取所有的成员方法,包括私有的,但不包括继承的
        Method[] methods2 = clazz.getDeclaredMethods();
        // 2.3获取单个公共的成员方法
        Method sleep = clazz.getMethod("sleep");
        // 2.4获取单个成员方法
        Method eat1 = clazz.getDeclaredMethod("eat", String.class);
        Method eat2 = clazz.getDeclaredMethod("eat", String.class,int.class);

        // 3.获取方法的修饰符

        int modifiers = eat1.getModifiers();

        // 4.获取方法的名字
        String name = eat1.getName();

        // 5.获取方法的形参
        Parameter[] parameters = eat1.getParameters();

        // 5.获取方法抛出的异常
        Class<?>[] exceptionTypes = eat1.getExceptionTypes();

        // 6.运行方法
        /**
         * Object invoke(Object obj,Object...args):运行方法
         *  参数一:用obj对象调用该方法
         *  参数二:调用方法的传递的参数(如果没有就不写)
         *  返回值:方法的返回值(如果没有就不写)
         */
        Student student = new Student();
        eat1.setAccessible(true);
        eat1.invoke(student,"汉堡包");
    }
}

十六、动态代理

为什么需要代理?

  • 代理可以无侵入式的给对象增强其他的功能。

代理长什么样子?

  • 代理李米娜就是对象要被代理的方法

Java通过什么来保证代理的样子?

  • 通过接口保证,后面的对象和代理许哟啊实现同一个接口,接口中就是被代理的所有方法

如何为Java对象创建一个代理对象?

  • java.lang.reflect.Proxy类提供了为对象产生代理对象的方法:

  • public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

    • 参数一:由于指定用哪个类加载器去加载生成的代理类
    • 参数二:指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法
    • 参数三:用来指定生成的代理对象要干什么事情

案例

  • 创建一个接口

    public interface Star {
        // 我们可以把所有想要被代理的方法定义在接口当中
    
        // 唱歌
        public abstract String sing(String name);
    
        // 跳舞
        public abstract void dance();
    }
    
  • 创建一个大明星实体类,并实现这个接口

    public class BigStar implements Star{
        private String name;
    
        public BigStar() {
        }
    
        public BigStar(String name) {
            this.name = name;
        }
    
        /**
         * 获取
         * @return name
         */
        public String getName() {
            return name;
        }
    
        /**
         * 设置
         * @param name
         */
        public void setName(String name) {
            this.name = name;
        }
    
        public String toString() {
            return "BigStar{name = " + name + "}";
        }
    
        @Override
        public String sing(String name) {
            System.out.println(this.name + "正在唱" + name);
            return "谢谢";
        }
    
        @Override
        public void dance() {
            System.out.println(this.name + "正在跳舞");
        }
    }
    
  • 创建一个代理

    // 创建一个代理
    public class ProxyUtil {
    
        /**
         *
         * @param bigStar 被代理的明星对象
         * @return 给明星创建的代理
         */
        public static Star createProxy(BigStar bigStar){
    
            /**
             * 参数一:由于指定用哪个类加载器去加载生成的代理类
             * 参数二:指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法
             * 参数三:用来指定生成的代理对象要干什么事情
             */
    
            Star star = (Star) Proxy.newProxyInstance(
                    ProxyUtil.class.getClassLoader(), //由于指定用哪个类加载器去加载生成的代理类
                    new Class[]{Star.class}, //指定接口,这些接口用于指定生成的代理长什么样,也就是有哪些方法
                    new InvocationHandler() { //用来指定生成的代理对象要干什么事情
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            /**
                             * 参数一:代理的对象
                             * 参数二:要运行的方法 sing
                             * 参数三:调用sing方法时,传递的实参
                             */
                            if ("sing".equals(method.getName())){
                                System.out.println("准备话筒,收钱");
                            } else if ("dance".equals(method.getName())){
                                System.out.println("准备场地,收钱");
                            }
                            // 调用大明星里面的方法
                            return method.invoke(bigStar,args);
    
                        }
                    }
            );
            return star;
        }
    }
    
  • 测试

    // 测试
    public class Test {
        public static void main(String[] args) {
            // 1.获取代理的对象
            BigStar bigStar = new BigStar("鸡哥");
            Star proxy = ProxyUtil.createProxy(bigStar);
    
            // 2.调用唱歌的方法
            String sing = proxy.sing("好说好说");
            System.out.println(sing);
    
            // 3.调用跳舞的方法
            proxy.dance();
        }
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值