小黑子—Java从入门到入土过程:第八章

Java零基础入门8.0

Java系列第八章

1. 双列集合 Map

单列集合与双列集合特点的对比:

  • 单列集合每次添加元素的时候,一次只能添加一个元素
  • 双列集合每次添加元素的时候,一次需要添加两个元素,也可以说成是一对元素

在这里插入图片描述
在双列集合中,有两个关键的概念,

在这里插入图片描述
上图左边一列就是键,右边一列就是值
键是不可以重复的,值可以重复

  • 键和值之间是一 一对应的关系,每一个键只能对应自己的值
  • 一个键和值在java就被称为键值对,或者叫做键值对对象(Entry)

下图中就用三个键值对对象:
在这里插入图片描述
双列集合特点的总结:
在这里插入图片描述

1.1 Map 集合中常见的API

Map接口有两个泛型,一个是键的泛型,一个是值的泛型
在这里插入图片描述

在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        //1.创建Map集合的对象
        Map<String,String> m = new HashMap<>();

        //2.添加元素 put
        //put方法的细节
        // 添加/覆盖
        //在添加数据的时候,如果键不存在,那么直接吧键值对对象添加到map集合当中,方法返回null
        //在添加数据的时候,如果键是存在的,那么会把原有的键值对对象覆盖,会把·被覆盖的值进行返回
        String value1 = m.put("麻瓜", "小老板");
        System.out.println(value1);
        System.out.println(m);

        m.put("愣头青","铁头娃");
        m.put("石乐志","小青年");
        System.out.println(m);

        String value2 = m.put("石乐志", "李在赣神魔");
        System.out.println("改的是:"+value2);
        System.out.println(m);

        //3.删除 remove
        String result = m.remove("愣头青");
        System.out.println("删除的是:"+result+"所在的键值");
        System.out.println(m);

        //4.清空 clear
        //m.clear(); 直接为空 {}

        //5.判断是否包含 containsKey
        boolean keyResult = m.containsKey("愣头青");
        System.out.println(keyResult);

        boolean res2 = m.containsValue("小老板");
        System.out.println(res2);

        boolean res3 = m.isEmpty();//判断是否清空了,如果没有返回fales,反之返回true
        System.out.println(res3);

        System.out.println(m.size());//返回双列集合的长度
    }
}

在这里插入图片描述

1.2 Map 集合的遍历方式

有三种遍历方式

  1. 键找值
  2. 键值对
  3. Lambda表达式
1.2 - I 第一种遍历方式:键找值KeySet 方法

先把所有的键提出到一个单列集合中,然后遍历单列集合,得到每一个键,然后通过get方法得到每个键所对应的值

  1. Map的keySet方法可以把Map集合的所有键存到一个Set集合中
    格式:Map集合名.keySet();,返回值是一个Set集合
    public static void main(String[] args) {
        Map<String,String>map=new HashMap<>();
        map.put("one","1");
        map.put("two","2");
        map.put("three","3");
        map.put("four","4");
		
		Set<String> keys= map.keySet();		
    }

在这里插入图片描述

  1. Map的get(Object key)方法可以得到键所对应的值
    格式:Map集合名.get(键名);,返回值是键所对应的值
    public static void main(String[] args) {
        Map<String,String>map=new HashMap<>();
        map.put("one","1");
        map.put("two","2");
        map.put("three","3");
        map.put("four","4");

		sout(map.get("one"));//1

在这里插入图片描述
练习:

在这里插入图片描述

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        //1.创建Map集合的对象
        Map<String,String> m = new HashMap<>();
        //2.添加元素
        m.put("麻瓜", "小老板");
        m.put("愣头青","铁头娃");
        m.put("石乐志","小青年");
        System.out.println(m);

        //3.通过键找值
        //3.1 获取所有的键,把这些键放到一个单列集合当中
        Set<String> keys = m.keySet();

        //3.2 通过增强for遍历单列集合,得到每一个键
        for (String key : keys) {
            System.out.println(key);
        }
        System.out.println("======================");

        //4.通过迭代器遍历键找值
        Iterator<String> it = keys.iterator();
        while (it.hasNext()){
            String s = it.next();
            System.out.println(s);
            System.out.println(s+"="+m.get(s));
        }
        System.out.println("======================");

        //5.通过lambd表达式遍历键找值
        keys.forEach(s-> System.out.println(s));

    }
}

在这里插入图片描述

1.2 - II 第二种遍历方式:键值对 entrySet 方法

依次获取Map集合里的每一个键值对对象(Entry),然后用键值对对象(Entry)通过getKey方法获取键,通过getValue方法获取值
上述的Entry实际上是Map接口的一个内部接口

Map的entrySet方法可以把所有的键值对存到一个Set集合中
格式:Map集合名.entry()返回值是一个set集合
注意,这里返回的set集合的泛型是Entry类型,即Set< Map.Entry< E,E > >

//Entry实际上是Map接口的一个内部接口,所以表达Entry时要用Map.来调用一下
Set<Map.Entry<String, String>> entries = map.entrySet();

//当然。这里的Map.也可以不写,但是要在代码最上面进行一个导包
import java.util.Map.Entry
Set<Entry<String, String>> entries = map.entrySet();

练习:
在这里插入图片描述

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        //1.创建Map集合的对象
        Map<String,String> m = new HashMap<>();
        //2.添加元素
        m.put("麻瓜", "小老板");
        m.put("愣头青","铁头娃");
        m.put("石乐志","小青年");
        System.out.println(m);

        //3.通过键值对对象进行遍历
        //3.1 通过一个方法获取所有的键值对对象,返回一个set集合
        Set<Map.Entry<String,String>> entries = m.entrySet();
        //3.2 遍历entries这个集合,去得到里面的每一个键值对对象
        //一、增强for
        System.out.println("返回的set集合:"+m.entrySet());
        for (Map.Entry<String, String> entry : entries) {
            System.out.println(entry);
        }
        System.out.println("======================");

        //二、迭代器
        Iterator<Map.Entry<String,String>> it = entries.iterator();
        while (it.hasNext()){
            Map.Entry<String,String> en = it.next();
            System.out.println(en);
        }
        System.out.println("======================");
        
        //三、lambda表达式
        entries.forEach((s)-> System.out.println(s));

    }
}

在这里插入图片描述

1.2 - III 第三种遍历方式:lambda表达式

Map的forEach方法
default void forEach(BiConsumer<? super K, ?super V>action)
可以结合lambda遍历Map集合
BiConsumer是一个函数式接口

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        //1.创建Map集合的对象
        Map<String,String> m = new HashMap<>();
        //2.添加元素
        //键:人物的名字
        //值:话
        m.put("麻瓜", "小老板李在赣神魔");
        m.put("愣头青","铁头娃李在赣神魔");
        m.put("石乐志","小青年李在赣神魔");
        System.out.println(m);

        //3.利用lambda表达式进行遍历
        m.forEach(new BiConsumer<String, String>() {
            @Override
            public void accept(String key, String value) {
                System.out.println(key + "=" + value);
            }
        });
        System.out.println("=================================");
        //底层:
        //forEach其实就是利用第二种方式进行遍历,一次得到每一个键和值
        //再调用accept方法
        m.forEach((String key, String value)-> System.out.println(key + "=" + value));

    }
}

在这里插入图片描述
foreach底层源码:
在这里插入图片描述

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        //1.创建Map集合的对象
        Map<String,String> m = new HashMap<>();
        //2.添加元素
        m.put("麻瓜", "小老板");
        m.put("愣头青","铁头娃");
        m.put("石乐志","小青年");
        System.out.println(m);

        //Set<Map.Entry<String,String>> entries = m.entrySet();这个方法不用,直接写在foreach冒号右边,就和源码一样了
        //所以与源码里面相比只是少一个变量而已
        for (Map.Entry<String, String> entry : m.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"="+value);
        }

    }
}

1.3 HashMap

注意:HashMap的键如果存储的是自定义对象,那么就需要重写hashCode和equals方法

在这里插入图片描述
在底层,创建一个默认长度为16,默认加载因子为0.75的数组,默认初始化值为null,利用put方法添加数据,put方法的底层首先会创建一个Entry对象(Entry1),Entry对象里记录着键和值
在这里插入图片描述

注意:只利用键计算哈希值,跟值无关,即要保证键的唯一

利用哈希值计算出在数组中应存入的索引,
在这里插入图片描述

如果索引处为null,就把数据存入

如果存入位置的值不是null,就表示存入位置有元素,

然后调用equals方法比较的属性值(只比较键的属性值)
在这里插入图片描述

如果键的属性值一样,那么新的Entry对象(Entry2)就会覆盖原来的Entry对象(Entry1),这就是put方法隐藏的覆盖功能
在这里插入图片描述

如果不一样,
jdk8之前,存入时是新元素存入数组,老的元素挂在新元素的下面
在这里插入图片描述

jdk8之后,新元素直接挂在老元素的下面
在这里插入图片描述

额外:jdk8开始,当链表的长度超过8而且数组的长度超过64,链表就会自动转成红黑树

在这里插入图片描述
小结:
在这里插入图片描述
练习一:
在这里插入图片描述
由于他这里题目要求同姓名,同年龄是同一个学生,而学生对象在键的位置,HashMap的底层也是哈希表,所以要想使同姓名,同年龄是同一个学生的话就要在学生类重写hashCode方法,使hashCode方法判断由地址值转变为属性值

核心点:

HashMap的键位置如果存储的是自定义对象,需要重写hashCode和lequals方法

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

public class A01_HashMapDemo1 {
    public static void main(String[] args) {
        //1.创建HashMap的对象、
        HashMap<Student,String> hm = new HashMap<>();
        //2.创建三个学生对象
        Student s1 = new Student("zhangsan",23);
        Student s2 = new Student("magua",24);
        Student s3 = new Student("lisi",25);
        Student s4 = new Student("magua",24);
        //3.添加对象
        hm.put(s1,"wuhu");
        hm.put(s2,"guangdoor");
        hm.put(s3,"xinlimuli");
        hm.put(s4,"guangdoor");

        //4.遍历集合
        Set<Student> keys = hm.keySet();
        for (Student key : keys) {
            String value = hm.get(key);
            System.out.println(key+"="+value);
        }
        System.out.println("==========================");

        //键加值遍历
        Set<Map.Entry<Student, String>> entries = hm.entrySet();
        for (Map.Entry<Student, String> entry : entries) {
            Student key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+"="+value);
        }
        System.out.println("==========================");
        //lambda表达式
        hm.forEach((Student student, String s)->{
                System.out.println(student+"="+s);
            }
        );
    }
}


在这里插入图片描述

练习二:
在这里插入图片描述
像这种统计计数的,一般都是定义一个计数器
但如果是任意选择自己想去的景点,景点的数量又很多,到底定义多少个计数器才行?

想这种统计的内容比较多,或者压根不知道要统计多少种的时候,此时就可以用Map集合进行统计。

核心:

创建一个Map集合,键为:景点,值为:投票次数,然后判断集合中是否包含该景点,不包含表示第一次出现,包含表示已经出现过了
在这里插入图片描述

import java.util.*;

public class A01_HashMapDemo1 {
    public static void main(String[] args) {
        //1.需要先让同学们投票
        //定义一个数组,储存4个景点
        String[] arr = {"A", "B", "C", "D"};
        //利用随机数模拟80个同学的投票,并把投票的结果存储起来
        ArrayList<String> list = new ArrayList<>();
        Random r = new Random();
        //生成学生的80个投票
        for (int i = 0; i < 80; i++) {
            //利用数组长度,在0~3内生成一个随机的索引
            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);
            }
        }

        //3.求最大值
        // A ???
        // B ???
        // C ???
        // D ???
        //那么拿谁做参照物呢?A B C D 都不合适,看它们最小投票必定是0
        //就拿0做参照物
        int max = 0;
        Set<Map.Entry<String, Integer>> entries = hm.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            Integer count = entry.getValue();
            if(count>max){
                max = count;
            }
        }
        System.out.println("最大值:"+max);

        //4.判断哪个景点的次数跟最大值一样,如果一样,打印出来
        //千万注意上面3的for循环与4的不能叠加在一起写
        //只有最大值确定了才能去做比较
        for (Map.Entry<String, Integer> entry : entries) {
            Integer count = entry.getValue();
            if(count==max){
                System.out.println("投票最多的是:"+entry.getKey()+"-"+max);
            }
        }

    }
}

在这里插入图片描述

1.3.1 LinkedHashMap

无特殊方法,直接使用Map的方法

在这里插入图片描述
即每个元素都会像双向链表的结点一样,除了存储着数据外,还存储着其他元素的地址值,存入的第一个元素就是头结点,最后一个元素就是尾结点,遍历时从头结点开始,按照添加的顺序遍历,到尾结点结束

这样一来得到遍历的数据,就和添加的数据一模一样了

在这里插入图片描述
代码实现:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> lhm = new LinkedHashMap<>();


        lhm.put("b", 789);
        lhm.put("b",111);
        lhm.put("c", 456);
        lhm.put("a", 123);
        lhm.put("a",123);

        System.out.println(lhm);
    }
}
1.3.2 HashMap源码分析

选中HashMap,然后ctrl+b快捷切换源码页面
在这里插入图片描述
先了解ieda中源码的一些东西
在这里插入图片描述

在源码中ctrl+f12可以打开一个菜单
在这里插入图片描述
蓝色的圆圈c(class),代表,红色的圆圈m(method)代表着方法
上面四个方法名和类名一样的,就是构造方法,下面一堆就是成员方法

比如这里的afterNodeRemoval就是方法名,(Node< K,V >)就是形参,void就是返回值

在这里插入图片描述

就表示重写的父类或接口中的方法
在这里插入图片描述
表示从父类中继承下来的方法

  • 点击这个灰色方法,那么就会跳转到Map这个接口当中,相当于该方法是继承Map接口里的方法

在这里插入图片描述
黄色的圆圈f(field) 就表示属性值,有可能是成员变量,也有可能是常量
在这里插入图片描述
这些就是HashMap下的内部类
在这里插入图片描述
HashMap里的 Node
在这里插入图片描述

在HashMap中,每一个元素都是一个Node的对象,而且在源码中,Node是Map.Entry的实现类,所以,也称一个Node(键值对)为Entry对象

四个成员变量:

  1. hash:哈希值
  2. key:键
  3. value:值
  4. next:记录链表里下一个元素(结点)的地址值

如果是红黑树,那么红黑树里的结点元素叫做TreeNode:
在这里插入图片描述

TreeNode也是HashMap的内部类,有五个成员变量

  • parent:记录父节点的地址值
    left:记录左子节点的地址值
    right:记录右子节点的地址值
    prev:暂时不了解
    red:判断是红色还是黑色,true为红色,false为黑色

除此之外,红黑树的元素还有

  • hash:哈希值
    key:键
    value:值
    next:记录链表里下一个元素(结点)的地址值

HashMap底层是由数组,链表和红黑树组成

HashMap的成员变量:
table,组成HashMap底层的数组
在这里插入图片描述
里面装的元素就是Node对象

这个常量表达了数组的默认长度是16,位运算,左移1个乘以2,移2个乘以2^2,
移动4个乘以2^4
在这里插入图片描述
这个常量就是默认的加载因子0.75
在这里插入图片描述
HashMap的构造方法
空参构造,将加载因子0.75赋值给loadFactor,此时,底层的数组还没有创建。即使用空参构造创建HashMap集合时,底层还没有数组

在这里插入图片描述

当使用put方法向集合中添加第一个元素时,底层就创建数组了
在这里插入图片描述

  • put方法中的putVal有五个参数,
    hash(key):键的哈希值
    key:键
    value:值
    onlyAbsent:默认为false,表示重复的键所对应的值会被覆盖,为true时表示不被覆盖。
    evict:暂时不用了解

resize方法的作用:

//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,
//加载因子为0.75的数组
//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,
//并把数据(链表或红黑树的数据)全部转移到新的哈希表中

源码详解:

1.看源码之前需要了解的一些内容

Node<K,V>[] table   哈希表结构中数组的名字

DEFAULT_INITIAL_CAPACITY:   数组默认长度16

DEFAULT_LOAD_FACTOR:        默认加载因子0.75



HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
    包含:  
			int hash;         //键的哈希值
            final K key;      //键
            V value;          //值
            Node<K,V> next;   //下一个节点的地址值
			
			
1.2 红黑树中的键值对对象
	包含:
			int hash;         		//键的哈希值
            final K key;      		//键
            V value;         	 	//值
            TreeNode<K,V> parent;  	//父节点的地址值
			TreeNode<K,V> left;		//左子节点的地址值
			TreeNode<K,V> right;	//右子节点的地址值
			boolean red;			//节点的颜色
					
1.3 数组中的键值对对象
	分情况讨论
		如果数组里的元素下挂的是链表
		那就包含:
			int hash;         //键的哈希值
            final K key;      //键
            V value;          //值
            Node<K,V> next;   //下一个节点的地址值
        如果数组里的元素下挂的是红黑树
        那就包含:
			int hash;         		//键的哈希值
            final K key;      		//键
            V value;         	 	//值
            TreeNode<K,V> parent;  	//父节点的地址值
			TreeNode<K,V> left;		//左子节点的地址值
			TreeNode<K,V> right;	//右子节点的地址值
			boolean red;			//节点的颜色        				

2.添加元素
//空参构造,只是给集合一个加载因子,连数组都没创建
HashMap<String,Integer> hm = new HashMap<>();
//使用put方法后,HashMap集合在底层创建数组
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);

添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖


//put源码
//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}


//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
//		   true,表示老元素的值保留,不会覆盖
//		   false,表示老元素的值不保留,会进行覆盖
//putVal是put方法里调用的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
	    //定义一个局部变量,用来记录哈希表中数组的地址值。
	    //因为成员变量定义在堆里,而方法运行在栈里,如果不定义数组记录元素的话
	    //会在调用数组里的Node结点时就会反复在堆和栈之间重复运行,
	    //影响效率
        Node<K,V>[] tab;
		
		//临时的第三方变量,用来记录键值对对象的地址值
        Node<K,V> p;
        
		//表示当前数组的长度
		int n;
		
		//表示索引
        int i;
		
		//把哈希表中数组的地址值,赋值给局部变量tab
		tab = table;

        if (tab == null || (n = tab.length) == 0){
        	//resize方法的作用
			//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,
			//加载因子为0.75的数组
			//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
			//如果没有达到扩容条件,底层不会做任何操作
			//如果达到了扩容条件,底层会把数组扩容为原先的两倍,
			//并把数据(链表或红黑树的数据)全部转移到新的哈希表中
			tab = resize();
			//表示把当前数组的长度赋值给n
            n = tab.length;
        }

		//拿着数组的长度跟键的哈希值进行计算,
		//计算出当前键值对对象,在数组中应存入的位置
		//这里的i就是index,即元素应存入的索引
		i = (n - 1) & hash;
		//获取数组中对应元素的数据
		p = tab[i];
		
        if (p == null){
//添加第一个元素

			//底层会创建一个键值对对象,直接放到数组当中
            tab[i] = newNode(hash, key, value, null);
        }else {
//添加其他元素
        
            Node<K,V> e;
            K k;
			
			//等号的左边:数组中键值对的哈希值
			//等号的右边:当前要添加键值对的哈希值
			//如果键不一样,此时返回false
			//如果键一样,返回true
			boolean b1 = p.hash == hash;
			
            if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;
            } else if (p instanceof TreeNode){
				//判断数组中获取出来的键值对是不是红黑树中的节点
				//如果是,则调用方法putTreeVal,
				//把当前的节点按照红黑树的规则添加到树当中。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            } else {
				//如果从数组中获取出来的键值对不是红黑树中的节点
				//表示此时下面挂的是链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
						//此时就会创建一个新的节点,挂在下面形成链表
                        p.next = newNode(hash, key, value, null);
						//判断当前链表长度是否超过8,
						//如果超过8,就会调用方法treeifyBin
						//treeifyBin方法的底层还会继续判断
						//判断数组的长度是否大于等于64
						//如果同时满足这两个条件,就会把这个链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
					//e:			  0x0044  ddd  444
					//要添加的元素: 0x0055   ddd   555
					//如果哈希值一样,
					//就会调用equals方法比较内部的属性值是否相同
                    if (e.hash == hash && ((k = e.key)
                     == key || (key != null && key.equals(k)))){
						 break;
					}

                    p = e;
                }
            }
			
			//如果e为null,表示当前不需要覆盖任何元素
			//如果e不为null,表示当前的键是一样的,值会被覆盖
			//e:0x0044  ddd  555
			//要添加的元素: 0x0055   ddd   555
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null){
					
					//等号的右边:当前要添加的值
					//等号的左边:0x0044的值
					//即覆盖的是老的键值对的值,而不是把整个键值对给覆盖
					e.value = value;
				}
                afterNodeAccess(e);
                return oldValue;
            }
        }
		
        //threshold:记录的就是数组的长度 * 0.75,
        //哈希表的扩容时机 即threshold:16 * 0.75 = 12
        if (++size > threshold){
			 resize();
		}
        
		//表示当前没有覆盖任何元素,返回null
        return null;
    }

1.4 TreeMap

无特殊方法,直接使用Map的方法

在这里插入图片描述
TreeMap的两种比较方式(Comparable,comparator)

  1. 在自定义的JavaBean类中实现Comparable接口,指定比较规则
  2. 创建集合时传递Comparator对象,指定比较规则

在这里插入图片描述

1.4.1 TreeMap练习
练习一:按照id的降序排列

在这里插入图片描述

练习一:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        //1.创建集合对象
        //Integer Double 默认情况下都是按照升序排列的
        //String 按照字母在AscII码表种对应的数字升序进行排列
        //abcdefg...

        TreeMap<Integer,String> tm = new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                //o1:当前要添加的元素
                //o2:表示已经在红黑树中存在的元素
                //返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
                //返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
                //返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
                return o2 - o1;
            }
        });
        //在需求2中降序排列,第一种方式已经不满足
        //那么就在第二种方式种,在创建集合对象中传递比较器的对象
        // 忘记名字,ctrl + p快捷提示
        tm.put(1,"可乐");
        tm.put(4,"雪碧");
        tm.put(3,"橙子");
        tm.put(2,"雪糕");

        System.out.println(tm);
    }
}

在这里插入图片描述

练习二:按照年龄的升序排列

在这里插入图片描述

Student类:
public class Student implements Comparable<Student>{
    private String name;
    private int age;


    public Student() {
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }


    public String toString() {
        return "Student{name = " + name + ", age = " + 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;
    }
}

测试类:
import java.util.*;

public class A01_HashMapDemo1 {
    public static void main(String[] args) {
        //创建集合
        TreeMap<Student,String> tm = new TreeMap<>();//但是TreeMap是要建立排序的,而如果Student没有建立排序的话,就直接报错
        //那么就在Student类当中,implement Comparable<E> 重写排序
        //2.创建三个学生对象
        Student s1 = new Student("麻瓜",21);
        Student s2 = new Student("张三",22);
        Student s3 = new Student("李四",23);

        //3,添加元素
        tm.put(s1,"鸡场");
        tm.put(s2,"guangdoor");
        tm.put(s3,"xinrimu里");

        System.out.println(tm);

    }
}

在这里插入图片描述

练习三:统计个数

在这里插入图片描述

import java.util.StringJoiner;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        /*
        输出结果:
        a (5)b (4) c (3) d (2) e ( 1)
        新的统计思想:利用map集合进行统计
        如果题目中没有要求对结果进行排序,默认使用HashMap如果题目中要求对结果进行排序,请使用TreeMap

        键:表示要统计的内容
        值:表示次数
         */
        String s = "aaabcdddcccabebbe";
        //1.创建集合
        TreeMap<Character,Integer> tm = new TreeMap<>();
        //2.遍历字符串得到里面的每一个字符
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);//这是遍历s的每个字符
            //拿着c到集合中判断是否存在
            //存在,表示当前字符又出现了一次
            // 不存在,表示当前字符是第一次出现
            if(tm.containsKey(c)){
                //存在
                //先把已经出现的次数拿出来
                int count = tm.get(c);
                //当前字符又出现了一次
                count++;
                //把自增之后的结果再添加到集合当中
                tm.put(c,count);
            }else{
                //不存在
                tm.put(c,1);
            }
        }
        //4.遍历集合,并按照指定的格式进行拼接
        //第一种方式:StringBuilder
        StringBuilder sb = new StringBuilder();
        tm.forEach((key,value)->sb.append(key).append("(").append(value).append(")"));
        System.out.println(sb);

        //第二种方式:StringJoiner
        StringJoiner sj = new StringJoiner("","","");
        tm.forEach((key,value)->sj.add(key+"").add("(").add(value+"").add(")"));
        System.out.println(sj);

    }
}

在这里插入图片描述
总结:
在这里插入图片描述

1.4.2 TreeSet源码分析

TreeMap底层为红黑树,
红黑树每一个元素内部的属性分别为:
在这里插入图片描述

  • key:键
    value:值
    parent:记录父节点的地址值
    left:记录左子节点的地址值
    right:记录右子节点的地址值
    color:判断是红色还是黑色,true为黑色,false为红色,TreeMap红黑树结点的初始值为黑色,TreeMap还会进行默认的调整,默认调整为红色

  • TreeMap集合的一些成员变量:
    在这里插入图片描述
    comparator:表示比较的规则
    root:记录红黑树根结点的地址值
    size:表示集合的长度,也表示红黑树中结点的个数

  • TreeMap的空参构造:
    在这里插入图片描述

  • TreeMap的带参构造:
    在这里插入图片描述
    就是创建TreeMap的时候要传递比较器对象,然后把传递过来的比较器对象传递给成员变量comparator

1.TreeMap中每一个节点的内部属性
K key;					//键
V value;				//值
Entry<K,V> left;		//左子节点
Entry<K,V> right;		//右子节点
Entry<K,V> parent;		//父节点
boolean color;			//节点的颜色




2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
   
    //比较器对象
    private final Comparator<? super K> comparator;

	//根节点
    private transient Entry<K,V> root;

	//集合的长度
    private transient int size = 0;

   

3.空参构造
	//空参构造就是没有传递比较器对象
	 public TreeMap() {
        comparator = null;
    }
	
	
	
4.带参构造
	//带参构造就是传递了比较器对象。
	public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
	
	
5.添加元素
	public V put(K key, V value) {
        return put(key, value, true);
    }
//put(key, value, true);
参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
		true:覆盖
		false:不覆盖
		
	private V put(K key, V value, boolean replaceOld) {
		//获取根节点的地址值,赋值给局部变量t
        Entry<K,V> t = root;
		//判断根节点是否为null
		//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
		//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
        if (t == null) {
			//方法的底层,会创建一个Entry对象,把他当做根节点
            addEntryToEmptyMap(key, value);
			//表示此时没有覆盖任何的元素
            return null;
        }
		//表示两个元素的键比较之后的结果
        int cmp;
		//表示当前要添加节点的父节点
        Entry<K,V> parent;
		
		//表示当前的比较规则
		//如果我们是采取默认的自然排序,
		//那么此时comparator记录的是null,cpr记录的也是null
		
		//如果我们是采取比较器的排序方式,
		//那么此时comparator记录的是就是比较器
        Comparator<? super K> cpr = comparator;
		//表示判断当前是否有比较器对象
		//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
		//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else {
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        } else {
			//把键进行强转,强转成Comparable类型的
			//要求:键必须要实现Comparable接口,如果没有实现这个接口
			//此时在强转的时候,就会报错。
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
				//把根节点当做当前节点的父节点
                parent = t;
				//调用compareTo方法,比较根节点和当前要添加节点的大小关系
                cmp = k.compareTo(t.key);
				
                if (cmp < 0)
					//如果比较的结果为负数
					//那么继续到根节点的左边去找
                    t = t.left;
                else if (cmp > 0)
					//如果比较的结果为正数
					//那么继续到根节点的右边去找
                    t = t.right;
                else {
					//如果比较的结果为0,会覆盖
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        }
		//就会把当前节点按照指定的规则进行添加
        addEntry(key, value, parent, cmp < 0);
        return null;
    }	
	
	
	
	 private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (addToLeft)
            parent.left = e;
        else
            parent.right = e;
		//添加完毕之后,需要按照红黑树的规则进行调整
        fixAfterInsertion(e);
        size++;
        modCount++;
    }
	
	
	
	private void fixAfterInsertion(Entry<K,V> x) {
		//因为红黑树的节点默认就是红色的
        x.color = RED;

//按照红黑规则进行调整,可以看一下红黑规则的笔记
		
		//parentOf:获取x的父节点
		//parentOf(parentOf(x)):获取x的爷爷节点
		//leftOf:获取左子节点
        while (x != null && x != root && x.parent.color == RED) {
			
			
			//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
			//目的:为了获取当前节点的叔叔节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
				//表示当前节点的父节点是爷爷节点的左子节点
				//那么下面就可以用rightOf获取到当前节点的叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
					//叔叔节点为红色的处理方案
					
					//把父节点设置为黑色
                    setColor(parentOf(x), BLACK);
					//把叔叔节点设置为黑色
                    setColor(y, BLACK);
					//把爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
					
					//把爷爷节点设置为当前节点
                    x = parentOf(parentOf(x));
                } else {
					
					//叔叔节点为黑色的处理方案
					
					
					//表示判断当前节点是否为父节点的右子节点
                    if (x == rightOf(parentOf(x))) {
						
						//表示当前节点是父节点的右子节点
                        x = parentOf(x);
						//左旋
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
				//表示当前节点的父节点是爷爷节点的右子节点
				//那么下面就可以用leftOf获取到当前节点的叔叔节点
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
		
		//把根节点设置为黑色
        root.color = BLACK;
    }
	

课堂思考问题:

  • TreeMap添加元素的时候,键是否需要重写hashCode和equals方法?

此时是不需要重写的,因为在TreeMap源码中添加元素时没有用到hashCOde和equals方法

HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成的

  • 既然有红黑树,HashMap的键是否需要实现Compareable接口或者传递比较器对象呢?

不需要。 因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的

  • TreeMap和HashMap谁的效率更高?

如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap(底层红黑树)的效率要更高

但是这种情况出现的几率非常的少。 一般而言,还是HashMap(底层数组,链表,红黑树)的效率要更高

  • 你觉得在Map集合中,java会提供一个如果键重复了,不会覆盖的put方法呢?
    有,putIfAbsent这个方法就表示键重复时不会覆盖,与put相反
    此时putIfAbsent本身不重要。

传递一个思想:
代码中的逻辑都有两面性,如果我们只知道了其中的A面, 而且代码中还发现了有变量可以控制两面性的发生。
那么该逻辑一定会有B面。

习惯:

boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
int类型的变量控制,一般至少有三面,因为int可以取多个值

  • 三种双列集合,以后如何选择?
    HashMap LinkedHashMap TreeMap
    默认:HashMap(效率最高)
    如果要保证存取有序:LinkedHashMap
    如果要进行排序:TreeMap

1.6 可变参数 …

在这里插入图片描述
这种方式比较麻烦,因为每次都要创建一个数组储存,在放到创建的方法中调用

所以jdk5以后提出了一个特性,可变参数即方法形参的个数是可以发生变化的,可以是0,1,2,3……个
格式:属性类型...名字,例如:int...a

可变参数底层就是一个数组,java自动创建好的,所以,可变参数本质上就是一个数组

可变参数的细节:

  1. 在方法的形参中,最多只能写一个可变参数 错误的写法: public static int getSum(int...a,int...b)
  2. 在方法中,如果除了可变参数以外还有其他的形参,那么可变参数要写在最后 public static int getSum(int a,int b,int...number)

在这里插入图片描述

public class ArgsDemo1 {
    public static void main(String[] args) {
        int sum = getSum(1,2,3,4,5,6);
        System.out.println(sum);
        System.out.println(getSum(1,2,3));//6
        System.out.println(getSum(1,2,3,9,10,29));//54
    }
    //可变参数
    public static int getSum(int ...args){
        int sum =0;
        for (int i : args) {
            sum += i;
        }
        return sum;
    }
}

小结:
在这里插入图片描述

1.7 Collections

在这里插入图片描述
Collections 常用的API:
在这里插入图片描述
在运用Collections(集合的工具类)里面的方法时,基本上所有的方法都有静态标记
所以在调用其方法时就用类名.就行

例子:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        //addAll,批量添加
        Collections.addAll(list,"abc","dce","123","456","qwer","zxvmn");
        System.out.println(list);

        //shuffle(),打乱
        Collections.shuffle(list);
        System.out.println(list);

        //sort排序,分两种,一种是默认排序,另外一种是指定方式排序
        //方式一:
        Collections.sort(list);
        System.out.println(list);

        //binarySearch,以二分查找法查找元素,注意顺序的才可以,倒序的反而不行
        int index = Collections.binarySearch(list, "456");
        System.out.println(index);


        //方式二:
        Collections.sort(list, new Comparator<String>() {
            //o1:表示当前要添加的元素
           //o2:表示已经在集合中存在的元素
            //返回值如果是负数,则会认为要添加的元素是小的,存在集合的左边
           //返回值如果是正数,则会认为要添加的元素是大的,存在集合的右边
            @Override
            public int compare(String o1, String o2) {
                for (int i = 0; i < o2.length(); i++) {
                    //此时为逆序排序
                    if(o1.charAt(i)>o2.charAt(i)){
                        return -1;
                    }else{
                        return 1;
                    }
                }
                return 0;
            }
        });
        System.out.println(list);

        //copy,复制元素,
        //Collections.copy(list2,list1); 把list1中的元素拷贝到list2中
        //会覆盖原来的元素
        //注意:如果list1的长度大于list2的长度,就会报错!!!!!!
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"avc","w","p","o","s","y","t","48");
        Collections.copy(list1,list);
        System.out.println(list1);

        //fill,使用指定的数据填充集合
        //把集合中的所有数据都改成指定数据
        Collections.fill(list,"麻瓜");
        System.out.println(list);

        //max/min,获取最大/小值,通过ASCII码来判断
        System.out.println(Collections.max(list1));
        System.out.println(Collections.min(list1));

        //swap,交换集合中指定位置的元素
        Collections.swap(list1,0,5);
        System.out.println(list1);
    }
}

在这里插入图片描述

1.8 综合练习

一、自动点名器1

在这里插入图片描述

import java.util.*;

public class Main {
    public static void main(String[] args) {
       /*
       班级里有N个学生,学生属性:姓名,年龄,性别
       实现随机点名器
        */
        //1.定义集合
        ArrayList<String> list = new ArrayList<>();
        //2.添加数据
        Collections.addAll(list,"麻瓜","张三","李四","范闲","范键","范统");
        //3.随机点名
        //法一:
        //核心思想:如果是在集合里,那么就用随机函数限制在集合的长度里面,随机出来里面的索引
        //然后用集合的get(索引)就可以获取
        Random r =new Random();
        int index = r.nextInt(list.size());
        String name = list.get(index);
        System.out.println(name);

        //法二:
        Collections.shuffle(list);
        System.out.println(list.get(0));

    }
}

在这里插入图片描述

二、自动点名器2

在这里插入图片描述

那么要怎么去代码实现概率问题呢?

我们可以在集合当中去添加7个"1"和3个"0",其在整体中就占70%30%,然后打乱里面的数字,打乱完之后就从里面抽取0或1

如果抽到了1,那就是有70%的概率随机到了
如果抽到了0,那就是30%,这样就可以解决概率问题了

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,1,1,1,1,1,1,1);
        Collections.addAll(list,0,0,0);
        //打乱数字,使得数据获得随机性
        Collections.shuffle(list);
        System.out.println(list);

        Random r = new Random();
        int index = r.nextInt(list.size());
        int number = list.get(index);
        //已经获得随机概率的数字
        System.out.println(number);

        ArrayList<String> manList = new ArrayList<>();
        ArrayList<String> womanList = new ArrayList<>();
        Collections.addAll(manList,"m1","m2","m3","m4","m5","m6","m7","m8");
        Collections.addAll(womanList,"w1","w2","w3","w4");

        if(number ==1){
            //此判断是从manList里面抽取
            int manIndex = r.nextInt(manList.size());
            String name1 = manList.get(manIndex);
            System.out.println(name1);
        }else{
            //此判断是从womanList里面抽取
            int WomanIndex = r.nextInt(womanList.size());
            String name2 = womanList.get(WomanIndex);
            System.out.println(name2);
        }
    }
}

在这里插入图片描述

三、自动点名器3

IDEA快捷方式 shift + f6 批量改名
在这里插入图片描述
核心:被随机到了就不会再被随机到的做法是,每次随机到了这个索引,再删除就行了

import java.util.*;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"m1","m2","m3","m4","m5","m6","m7","m8","w9","w10");

        //创建一个临时的集合,用来储存被点到同学的名字
        ArrayList<String> list2 = new ArrayList<>();
        //外循环:循环的轮数
        for (int j = 1; j <= 10; j++) {
            System.out.println("===========第"+j+"轮开始了=================");
            //获取集合的长度
            int list1length = list1.size();
            //随机点名
            Random r = new Random();
            //内循环表示随机抽取的过程
            for (int i = 0; i < list1length; i++) {
                //第一次随机的时候:i = 0 长度10
                //第二次随机的时候: i= 1 长度9
                //第三次随机的时候:i = 2 长度8
                //第四次随机的时候: i= 3 长度7
                //第五次随机的时候: i=4 长度6
                //第六次随机的时候: i=5 长度5  5<5 fales,循环终止
                int index = r.nextInt(list1.size());
                //选中删除
                String name = list1.remove(index);
                list2.add(name);
                System.out.println(name);

            }
            //此时表示一轮点名结束
            //此时list1空了,list2 10个学生的名字
            //我们就可以把list2里面的数据加到list1当中,再把list2清空,这样再一次外循环时就可以再次作为空集合进行存储了
            list1.addAll(list2);
            list2.clear();
            System.out.println(list1);
            System.out.println(list2);
        }
    }
}

在这里插入图片描述

四、自动点名器4

在这里插入图片描述

五、自动点名器5

在这里插入图片描述

六、Mao集合案例-省和市

在这里插入图片描述

import java.util.*;

public class Main {
    public static void main(String[] args) {
        //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("张家口市");
        //把省份和多个市添加到map集合
        hm.put("江苏省",city1);
        hm.put("湖北省",city2);
        hm.put("河北省",city3);

        /*江苏省=南京市,扬州市,苏州市,无锡市,常州市
        湖北省=武汉市,孝感市,十堰市,宜昌市,鄂州市
        河北省=石家庄市,唐山市,邢台市,保定市,张家口市*/
        Set<Map.Entry<String, ArrayList<String>>> entries = hm.entrySet();
        for (Map.Entry<String, ArrayList<String>> entry : entries) {
            //entry依次表示每一个键值对对象
            //然后将其对象里面的键和值分开
            String key = entry.getKey();
            ArrayList<String> value = entry.getValue();
            //然后到值的时候,利用StringJoinner的间隔将值分开
            StringJoiner sj = new StringJoiner(",",""," ");
            //第一个:间隔符号,第二个开始符号,第三个结束符号
            //最后遍历出来每个值,插入到sj中
            for (String city : value) {
                sj.add(city);
            }
            System.out.println(key+" = "+sj);
        }

    }
}

在这里插入图片描述

1.9 阶段项目

1.9.1 斗地主游戏1(控制台版)

在这里插入图片描述

PokerGame类:
import java.util.ArrayList;
import java.util.Collections;

public class PokerGame {
    //牌盒
    //拼接花色图形+数字元素比如❤3,♠4
    static ArrayList<String> list = new ArrayList<>(); //这里加静态的原因是下方有静态代码块,静态只能访问静态
    //准备牌放在构造方法外面,为了不然每次都准备一次,这样就重复浪费了

    //静态代码块
    //特点:随着类的加载而加载的,而且只执行一次
    static {
        /*
              "♠,♣,❤,♦"
             "3","4","5","6","7",,"8","9","10","3","Q","K","A","2"
         */
        String[] color = {"♠","♣","❤","♦"};
        String[] number = {"3","4","5","6","7","8","9","10","3","Q","K","A","2"};


        for (String c : color) {
            // c 表示每一种花色
            for (String n : number) {
                //n 表示每一个数字
                //组合起来加到牌盒当中
                list.add(c+n);
            }
        }
        list.add("小王");
        list.add("大王");
        //准备牌工作完成
    }

    //所以在这里直接写空参构造方法就可以了
    public PokerGame(){
        //洗牌
        Collections.shuffle(list);
        System.out.println(list);

        //发牌的对象
        //先决定好发牌对象
        //斗地主发牌给三个人,但是最重要的是要有一个底牌
        ArrayList<String> lord = new ArrayList<>();
        ArrayList<String> player1 = new ArrayList<>();
        ArrayList<String> player2 = new ArrayList<>();
        ArrayList<String> player3 = new ArrayList<>();

        //然后给每个发牌对象,遍历得到牌盒里面的牌
        for (int i = 0; i < list.size(); i++) {
            //i:就表示牌集合里面的索引
            //把索引拿出来
            String poker = list.get(i);
            //在斗地主的过程当中,前3张牌要留给底牌
            //所以索引0-2要留下
            if(i<=2){
                lord.add(poker);
                continue;
            }

            //給三民玩家轮流发牌
            //采用取模来分配,为什么呢?
            //因为“%3”在运算的到时候有三种余数,可以用来三等分
            //用索引i%3,如果等于0就发给玩家1
            //如果等于1,发给玩家2;如果为2,就发给玩家3
            if(i%3==0){
                player1.add(poker);
            }else if(i%3==1){
                player2.add(poker);
            }else{
                player3.add(poker);
            }
        }

        //看牌
        //怎么看?在外面定义一个方法然后调用
        //看牌无非就是遍历集合
        lookPoker("底牌",lord);
        lookPoker("magua",player1);
        lookPoker("zhangsan",player2);
        lookPoker("lisi",player3);
    }

    //那么要定义什么类型的方法呢,不返回值
    //形参为斗地主玩家的名字和牌
    public void lookPoker(String name,ArrayList<String> list){
        /*
        参数一:玩家的名字
        参数二:每位玩家的牌
         */
        System.out.print(name+":");
        for (String poker : list) {
            System.out.print(poker+" ");
        }
        System.out.println();
    }
}

APP测试类:
public class App {
    /*
        完成控制台版的三步骤:
             准备牌
             洗牌
             发牌

             "♠,♣,❤,♦"
             "3","4","5","6","7",,"8","9","10","3","Q","K","A","2"
     */
    public static void main(String[] args) {
        new PokerGame();//创建对象实际上会调用里面的空参构造
    }


}

在这里插入图片描述

1.9.2 斗地主游戏2 第一种排序方式

在这里插入图片描述
以十二张牌为例子,牌是字符串,它里面的花色数字组合里面斗地主的规则谁大谁小该怎么判定呢?
在这里插入图片描述
所以我们可以按照牌指定的规则,进行手动的排列,再跟12345678910这样的数字一一对应,那么对应起来之后,数字越大,所对应的牌也就越大

在这里插入图片描述

比如:
怎么认为大王是最大的呢,其实就是通过下面的序号来判断的。
大王是序号数字12,红心2是序号数字10

在这里插入图片描述

所以,先找一个map集合把这种对应关系去存起来。那么再map集合当中,序号就作为键,上面牌花色数字组合就作为值

与此同时,创建一个单列集合,用来单独地存储牌的序号,那么这样准备牌的动作才算是做好了

  • 到洗牌了,其实打乱序号就行了,那么发牌发的也就是序号。前面牌盒发的三个序号作为底牌,其余序号就依次发给三个玩家
  • 发完之后序号是没有顺序的,所以要进行排序,完成后在一开始的map集合当中去找对应序号的牌就可以了

在这里插入图片描述

在这里插入图片描述

PokerGame2类:
import java.util.*;

public class PokerGame2 {
    //牌盒 Map
    //此时只有把牌跟序号产生对应关系就可以了,不需要按照序号进行排序
    //所以只有HashMao就可以了
    static HashMap<Integer,String> hm = new HashMap<>();//用于对应序号与牌面
    static ArrayList<Integer> list = new ArrayList<>();//用于添加序号

    //TreeMap按键的方式从小到大存储
    //◆3 ◆4  ◆5 ◆6 ◆7
    //1   2    3   4    5

    //HashMap 这个侧重点注重排和数字之间的对应关系
    //◆3 ◆5  ◆4 ◆7 ◆6
    //1   3    2    5   4

    //如果用TreeMap进行存储 玩家一:145
    //◆3 ◆6 ◆7
    //如果用HashMap进行存储
    static {
        String[] color = {"♦","♣","❤","♠"};
        String[] number = {"3","4","5","6","7","8","9","10","3","Q","K","A","2"};

        //用来定义牌的序号
        int serialNumber = 1;

        /*
         {1=♠3, 2=♠4, 3=♠5, 4=♠6, 5=♠7, 6=♠8, 7=♠9, 8=♠10, 9=♠3, 10=♠Q, 11=♠K, 12=♠A, 13=♠2, 14=♣3,
         15=♣4, 16=♣5, 17=♣6, 18=♣7, 19=♣8, 20=♣9, 21=♣10, 22=♣3, 23=♣Q, 24=♣K, 25=♣A, 26=♣2, 27=❤3,
         28=❤4, 29=❤5, 30=❤6, 31=❤7, 32=❤8, 33=❤9, 34=❤10, 35=❤3, 36=❤Q, 37=❤K, 38=❤A, 39=❤2,
         40=♦3, 41=♦4, 42=♦5, 43=♦6, 44=♦7, 45=♦8, 46=♦9, 47=♦10, 48=♦3, 49=♦Q, 50=♦K, 51=♦A,
         52=♦2, 53=小王, 54=大王}

         但是这样外循环遍历花色,内循环遍历数字,花色数字排序不是按顺序的,只按花色来排
         玩家:1 2 3 4 5 14
              ♠3 ♠4 ♠5 ♠6 ♠7 ♣3

              我们想要的排列是:♠3 ♣3 ♠4 ♠5 ♠6 ♠7
              把外循环改成数字开始,内循环改成花色开始就oK
         */
        for (String n : number) {
            // c 表示每一种花色
            for (String c : color) {
                //n 表示每一个数字
                //组合起来加到牌盒当中
                hm.put(serialNumber,c+n);
                list.add(serialNumber);
                serialNumber++;
            }
        }
        hm.put(serialNumber,"小王");
        list.add(serialNumber);
        serialNumber++;

        hm.put(serialNumber,"大王");
        list.add(serialNumber);
        //因为大王是最后一张牌了,所以大王在添加的时候序号就可以不用变化了
        System.out.println(hm);
        System.out.println(list);
    }

    public PokerGame2(){
        //洗牌
        Collections.shuffle(list);

        //发牌
        //这里才用TreeSet是因为它默认进行数字排序
        //如果用的是ArrayList添加到里面的数据是乱的,又要多一步进行排序
        TreeSet<Integer> lord = new TreeSet<>();
        TreeSet<Integer> player1 = new TreeSet<>();
        TreeSet<Integer> player2 = new TreeSet<>();
        TreeSet<Integer> player3 = new TreeSet<>();

        for (int i = 0; i < list.size(); i++) {
            //i:依次表示集合中的每一个索引
            //list.get(i)元素:牌的序号

            int serialNumber = list.get(i);

            if(i<=2){
                lord.add(serialNumber);
                continue;
            }

            if(i%3 ==0){
                player1.add(serialNumber);
            }else if(i%3==1){
                player2.add(serialNumber);
            }else{
                player3.add(serialNumber);
            }
        }

        //看牌
        lookPoker("底牌",lord);
        lookPoker("magua",player1);
        lookPoker("zhangsan",player2);
        lookPoker("lisi",player3);
    }
    /*
    参数一:玩家的名字
    参数二:牌的序号
     */
    public void lookPoker(String name,TreeSet<Integer> ts){
        System.out.println(name+":");
        //遍历TreeSet单列集合得到每一个序号,再拿着序号到map集合中去找真正的牌
        for (Integer serialNumber : ts) {
            String poker = hm.get(serialNumber);
            System.out.print(poker+":");
        }
        System.out.println();
    }

}

APP2类:
public class APP2 {
    public static void main(String[] args) {
        new PokerGame2();
    }
}

在这里插入图片描述

1.9.3 斗地主游戏3 第二种排序方式

下面的价值就是人为规定的,有了价值之后,就按照价值进行排序
在这里插入图片描述
在这里插入图片描述

PokerGame3类:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;

public class PokerGame3 {
    //牌盒
    //拼接花色图形+数字元素比如❤3,♠4
    static ArrayList<String> list = new ArrayList<>(); //这里加静态的原因是下方有静态代码块,静态只能访问静态
    static HashMap<String,Integer> hm = new HashMap<>();

    //静态代码块
    //特点:随着类的加载而加载的,而且只执行一次
    static {
        /*
              "♠,♣,❤,♦"
             "3","4","5","6","7",,"8","9","10","3","Q","K","A","2"
         */
        String[] color = {"♦","♣","❤","♠"};
        String[] number = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};


        for (String c : color) {
            // c 表示每一种花色
            for (String n : number) {
                //n 表示每一个数字
                //组合起来加到牌盒当中
                list.add(c+n);
            }
        }
        list.add(" 小王");
        list.add(" 大王");
        //准备牌工作完成

        //指定牌的价值
        //牌上的数字到map集合中判断是否存在
        //存在,获取价值
        //不存在,本身的数字就是价值
        hm.put("J",11);
        hm.put("Q",12);
        hm.put("K",13);
        hm.put("A",14);
        hm.put("2",15);
        hm.put("小王",50);
        hm.put("大王",100);

    }

    //所以在这里直接写空参构造方法就可以了
    public PokerGame3(){
        //洗牌
        Collections.shuffle(list);
        System.out.println(list);

        //发牌的对象
        //先决定好发牌对象
        //斗地主发牌给三个人,但是最重要的是要有一个底牌
        ArrayList<String> lord = new ArrayList<>();
        ArrayList<String> player1 = new ArrayList<>();
        ArrayList<String> player2 = new ArrayList<>();
        ArrayList<String> player3 = new ArrayList<>();

        //然后给每个发牌对象,遍历得到牌盒里面的牌
        for (int i = 0; i < list.size(); i++) {
            //i:就表示牌集合里面的索引
            //把索引拿出来
            String poker = list.get(i);
            //在斗地主的过程当中,前3张牌要留给底牌
            //所以索引0-2要留下
            if(i<=2){
                lord.add(poker);
                continue;
            }

            //給三民玩家轮流发牌
            //采用取模来分配,为什么呢?
            //因为“%3”在运算的到时候有三种余数,可以用来三等分
            //用索引i%3,如果等于0就发给玩家1
            //如果等于1,发给玩家2;如果为2,就发给玩家3
            if(i%3==0){
                player1.add(poker);
            }else if(i%3==1){
                player2.add(poker);
            }else{
                player3.add(poker);
            }
        }
        //排序
        order(lord);
        order(player1);
        order(player2);
        order(player3);


        //看牌
        //怎么看?在外面定义一个方法然后调用
        //看牌无非就是遍历集合
        lookPoker("底牌",lord);
        lookPoker("magua",player1);
        lookPoker("zhangsan",player2);
        lookPoker("lisi",player3);
    }

    //那么要定义什么类型的方法呢,不返回值
    //形参为斗地主玩家的名字和牌
    public void lookPoker(String name,ArrayList<String> list){
        /*
        参数一:玩家的名字
        参数二:每位玩家的牌
         */
        System.out.print(name+":");
        for (String poker : list) {
            System.out.print(poker+" ");
        }
        System.out.println();
    }

    //利用牌的价值进行排序
    //参数:集合
    //比如:♥5 ♥3 ♥6 ♥7 ♥9
    //当插入排序时,先把前面的第一张牌认为是有序序列,后面的所以认为是无序序列
    //然后遍历无序序列的每一张牌,把其插入到有序序列当中
    public void order(ArrayList<String> list){
        Collections.sort(list, new Comparator<String>() {
            //Array.sort(插入排序+二分查找)
            @Override
            public int compare(String o1, String o2) {
                //o1:表示当前要插入到有序序列中的牌,比如♥3
                //o2:表示在有序序列中已经存在的牌。比如♥5

                //负数:o1小,那么就插入到前面
                //整数:o1大,插入到后面去
                //如果0,o1的数字与o2的是一样的,需要按照花色再次进行排序

                //1.计算o1的花色和价值 ♥3
                String color1 = o1.substring(0, 1);
                //那么价值怎么计算呢,除了要计算o1的价值,还要计算o2的价值
                int value1 = getValue(o1);

                //2.计算o2的花色和价值
                String color2 = o2.substring(0, 1);
                int value2 = getValue(o2);

                //3.笔记o1和o2的价值
                int i  = value1 - value2;
                return i==0?color1.compareTo(color2):i;
            }
        });
    }

    //计算牌的价值
    //参数:牌
    //返回值:价值
    public int getValue(String poker){//♥3 小王
        //获取牌上的数字
        String number = poker.substring(1);//把这里截取出来的结果,让这个结果再map集合中存在 “ 大王”
        //空格截取不到,只能截取到大王,那么大王到里面的集合当中判断集合就存在了

        //拿着数字到map集合中判断是否存在
        if(hm.containsKey(number)){
            //如果在这里大小王存在,就不会走到下方的类型转化中去
            //存在,获取价值
            return hm.get(number);
        }else{
            //不存在,类型转换
            //但是如果大王小王在这里的话,汉字是无法转化为数字的
            //往上面推
            return Integer.parseInt(number);
        }

    }
}

APP3类:
public class App3 {
    public static void main(String[] args) {
        new PokerGame3();
    }
}

1.10 不可变集合

不可变集合:

指的是不可以被修改的集合,比如不可以修改长度、不可以修改内容等

在这里插入图片描述
创建不可变集合的书写格式:
在这里插入图片描述
例子:

import java.lang.invoke.CallSite;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        //创建不可变的list集合
        List<String> list = List.of("麻瓜","小老板","愣头青","铁头娃");

        System.out.println(list.get(0));
        System.out.println(list.get(1));
        System.out.println(list.get(2));
        System.out.println(list.get(3));
        System.out.println("============================");

        //增强for循环遍历
        for (String s : list) {
            System.out.println(s);
        }
        System.out.println("============================");

        //迭代器遍历
        Iterator<String> it = list.iterator();
        while(it.hasNext()){
            String s = it.next();
            System.out.println(s);
        }
        System.out.println("============================");

        //普通for遍历
        for (int i = 0; i < list.size(); i++) {
            String s = list.get(i);
            System.out.println(s);
        }
        System.out.println("---------------------------------------");
        //不可变集合一旦创建完毕之后,是无法进行修改的,在下面的代码中,只能进行查询操作
        //list.remove("麻瓜")报错
        //list.add("aaa"); 报错
        //list.set(0,"aaa"); 报错


        //创建不可变的set集合
        //细节:set集合里面的元素是唯一的,一定要保证其唯一性
        Set<String> set = Set.of("magua","zhangsan","lisi");

        for (String s : set) {
            System.out.println(s);
        }
        System.out.println("---------------------------------------");

        //创建不可变的map集合
        //细节:键是必须唯一的,不能重复
        //      Map里面的of方法,参数是有上限的,最多只能传递20个参数,10个键值对
        Map<String,String> map = Map.of("张三","guangdoor","李四","新日暮里","魔男","幻想乡");
        //keyset获取键
        Set<String> keys = map.keySet();
        for (String key : keys) {
            String value = map.get(key);
            System.out.println(key+"="+value);
        }
        System.out.println("==========================");

        //entrySet获取整体
        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("==========================");
    }
}

提问:创建一个能接收多个键和值的方法
当类型不确定时:用泛型方法,比如键为K和值为V

创建Map的不可变集合
细节1:

  • 键是不能重复的

细节2:

  • Map里面的of方法,参数是有上限的,最多只能传递20个参数,10个键值对

在这里插入图片描述
报错原因:

如果一个形参里有可变参数,那么形参就必须要写到最后。但是一个键形参里面又不能有多个可变参数,只能有一个,所以这两个可变参数是不能共存的

解决方案:
把键和值看做成一个整体,然后在传递给of方法就行了

法一:

import java.lang.invoke.CallSite;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        //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[] arr = entries.toArray(new Map.Entry[0]);
        Map.Entry[] arr1 = new Map.Entry[0];//给它个数组
        Map.Entry[] arr2 = entries.toArray(arr1);//将集合中的所有数据放在数组当中
        //toArray方法在底层会比较集合的长度跟数组的长度两者的大小
        //如果集合的长度 > 数组的长度:数据在数组中放不下,此时会根据实际数据的个数,重新创建数组
        //如果集合的长度 <=数组的长度:数据在数组中放的下,此时不会创建新的数组,而是直接用

        //不可变的map集合
        Map map = Map.ofEntries(arr2);
        //ofEntries方法里面的形参就是可变参数,可变参数里头就是一个数组,那么传递过去就是可以的
        

    }
}

法二:链式简化

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

法三:不可变map集合简化

Map<String, String> map = Map.copyOf(hm);

小结:
在这里插入图片描述

2. Stream流

案例:
在这里插入图片描述
不用stream流的麻烦操作:

import java.util.ArrayList;

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

        ArrayList<String> list1 = new ArrayList<>();
        list1.add("麻瓜");
        list1.add("捞得一");
        list1.add("小老板");
        list1.add("小青年");
        list1.add("厨师");

        //1.把所有以“小”开头的元素再存储到新集合中
        ArrayList<String> list2 = new ArrayList<>();
        for (String name : list1) {
            if(name.startsWith("小")){
                list2.add(name);
            }
        }
        System.out.println(list2);
        //2.把“张”开头的,长度为3的元素再次存储到新集合中
        ArrayList<String> list3 = new ArrayList<>();
        for (String name : list2) {
            if(name.length()==3){
                list3.add(name);
            }
        }
        System.out.println(list3);
    }
}

在这里插入图片描述
使用stream流:
一行代码直接搞定

        list1.stream().filter(name->name.startsWith("小")).filter(name->name.length()==3).forEach(name-> System.out.println(name));

在这里插入图片描述

2.1 初识stream流

什么是流:
理解成工厂的流水线
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

stream流的作用:结合了lambda表达式,简化集合、数组的操作

在这里插入图片描述

Stream流的使用步骤
双列集合无法直接使用stream流,要使用keySet或者entrySet转化为单列集合才能使用
在这里插入图片描述
例子:
1.单列集合获取stream流

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.stream.Stream;

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

        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"a","b","c","d");
        /*
        //获取到一条流水线,并把集合中的数据放到流水线上
        Stream<String> stream1 = list.stream();
        //使用终结方法打印一下流水线上的所有数据
        stream1.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                //s:依次表示流水线上的每一个数据
                System.out.println(s);
            }
        }); */
        list.stream().forEach(s-> System.out.println(s));
    }
}

在这里插入图片描述
2.双列集合获取stream流

public class Main {
    public static void main(String[] args) {
        HashMap<String,Integer> hm = new HashMap<>();
        hm.put("aaaa",111);
        hm.put("bbbb",222);
        hm.put("cccc",333);
        hm.put("dddd",444);

        //第一种keySet获取stream流
        hm.keySet().stream().forEach(s-> System.out.println(s));
        //第二种entrySet获取stream流
        System.out.println(":==============");
        hm.entrySet().stream().forEach(s-> System.out.println(s));

    }
}

在这里插入图片描述
3.数组获取stream流

public class Main {
    public static void main(String[] args) {
       int[] arr1 = {1,2,3,4,5,6,7,8,9};
       String[] arr2 = {"a","b","c"};

        Arrays.stream(arr1).forEach(s-> System.out.println(s));
        System.out.println("========================");

        Arrays.stream(arr2).forEach(s-> System.out.print(s));

    }
}

在这里插入图片描述
4.一堆的零散数据获取stream流
前提条件:必须要是同种数据类型的

    public static void main(String[] args) {
       Stream.of(1,2,3,4,5).forEach(s-> System.out.println(s));
       Stream.of("a","b","c","d").forEach(s-> System.out.print(s));

    }

在这里插入图片描述

注意:
在这里插入图片描述

2.2 Steam流的中间方法

在这里插入图片描述

  • filter 过滤
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"麻瓜","张三","小老板","小青年","愣头青","小三");

        //法一
        list.stream().filter(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                //如果返回值为true,表示当前数据留下
                //如果返回值为false,表示当前数据舍弃不变
                return s.startsWith("小");
            }
        }).forEach(s-> System.out.println(s));
        System.out.println("===========================");

        //法二:链式编程
        list.stream()
                .filter(s->s.startsWith("小"))
                .filter(s->s.length()==3)
                .forEach(s-> System.out.println(s));

        System.out.println(list);//经过stream流过滤之后,原本的list并没有受到影响

    }
}

在这里插入图片描述

  • limit 获取前几个元素
public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"麻瓜","张三","小老板","小青年","愣头青","小三");

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

    }
}

在这里插入图片描述

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

在这里插入图片描述

  • distinct 元素去重(底层依赖hashCode和equals方法)
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"麻瓜","张三","小老板","小青年","愣头青","小三","小老板","小老板");

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

    }

在这里插入图片描述

  • concat 合并a和b两个流为一个流
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"麻瓜","张三","小老板","小青年","愣头青","小三","小老板","小老板");

        ArrayList<String> list2 = new ArrayList<>();
        Collections.addAll(list2,"guangdoor","van");

        Stream.concat(list1.stream(),list2.stream()).forEach(s-> System.out.println(s));

    }

在这里插入图片描述

  • map 转换流中的数据类型
public class Main {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"麻瓜-1","张三-2","小老板-3","小青年-4","愣头青-5","小三-6");
        //需求:只获取里面的数字并进行打印
        //String-> int

        //第一个类型:流中原本的数据类型
        //第二个数据类型:要转成之后的类型

        //apply的形参s:依次表示流里面的每一个数据
        //返回值:表示转换之后的数据

        //当map方法执行完毕之后,流上的数据就变成了整数
        //所以在下面forEach当中,s依次表示流里面的每一个数据,这个数据现在就是整数了
        list1.stream().map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s){
                String[] arr = s.split("-");
                //把-分开后,剩下名字和数字
                //所以,arr[0]表示前面的名字,arr[1]表示后面的数字
                String ageString = arr[1];
                int age = Integer.parseInt(ageString);
                return age;
            }
        }).forEach(s-> System.out.println(s));
        System.out.println("===================");

        //法二:
        list1.stream()
                .map(s->Integer.parseInt(s.split("-")[1])).forEach(s-> System.out.println(s));

    }
}

在这里插入图片描述

2.3 Steam流的终结方法

在这里插入图片描述

2.3.1 forEach 遍历
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"麻瓜","张三","小老板","小青年","愣头青","小三");

        //voidforEach(Consumer action)   遍历
        //法一:
        //Consumer的泛型:表示流中数据的类型
        //accept方法的形参s:依次表示流里面的每一个数据
        //方法体:对每一个数据的处理操作(打印)
        list1.stream().forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });
        System.out.println("=================================");
        //法二:
        list1.stream().forEach(s-> System.out.println(s));



    }

在这里插入图片描述

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

在这里插入图片描述

2.3.3 toArray 收集流数据放进数组
        //toArray有两个,一个空参的收集Object类型,一个具体类型的
        Object[] arr1 = list1.stream().toArray();
        System.out.println(Arrays.toString(arr1));
    //toArray方法的完整解析:
        //IntFunction的泛型:具体类型的数组
        //apply的形参:流中的数据个数,要跟数组的长度保持一致
        //apply的返回值:具体类型的数组
        //方法体:就是创建数组

        //toArray方法的参数的作用:负责创建一个指定类型的数组
        //toArray方法的底层,会依次得到流里面的每一个数据,并把数据放到数组当中
        //toArray方法的返回值:是一个装着流里面所有数据的数组

        String[] arr = list1.stream().toArray(new IntFunction<String[]>() {
            @Override
            public String[] apply(int value) {
                return new String[value];
            }
        });
        System.out.println(Arrays.toString(arr));

在这里插入图片描述

2.3.4 collect 收集流数据放进集合

collect(Collector collector) 收集流中的数据,放到集合中(List Set Map)

注意点:

如果要收集到Map集合当中,键不能重复,否则会报错

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        Collections.addAll(list1,"a-男-15","t-男-15","b-女-14","c-女-16","d-男-17","e-男-18","f-女-13",
        "g-男-19");

        //收集List集合当中
        //需求:
        //把所有的男性收集起来
        List<String> newList1 = list1.stream()
                .filter(s->"男".equals(s.split("-")[1]))
                .collect(Collectors.toList());

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

        //收集Set集合当中
        //需求:
        //同上
        //set集合中元素不会重复
        Set<String> newList2 = list1.stream()
                .filter(s->"男".equals(s.split("-")[1]))
                /*
                toMap:参数一表示键的生成规则
                        参数二表示值的生成规则

                参数一:也就是collect里面上面那一堆
                    Function泛型一:表示流中每一个数据的类型
                            泛型二:表示Map集合中键的数据类型

                            方法apply形参:依次表示流里面的每一个数据
                                   方法体:生成键的代码
                                   返回值:已经生成的键

                参数二:
                    Function泛型一:表示流中每一个数据的类型
                            泛型二:表示Map集合中键的数据类型

                            方法apply形参:依次表示流里面的每一个数据
                                   方法体:生成键的代码
                                   返回值:已经生成的值

                 */
                .collect(Collectors.toSet());

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

        //收集Map集合当中
        //谁作为键,谁作为值
        //把所有的男性收集起来
        //键:姓名  值:年龄

        Map<Object, Integer> map = list1.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toMap(new Function<String, Object>() {
                                              @Override
                                              public String apply(String s) {
                                                  //a-男-15
                                                  return s.split("-")[0];
                                              }
                                          },
                        new Function<String, Integer>() {
                            @Override
                            public Integer apply(String s) {
                                return Integer.parseInt(s.split("-")[2]);
                            }
                        }
                ));
        System.out.println(map);

        //其lambda表达式:
        Map<String, Integer> map2 = list1.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(
                        Collectors.toMap(
                        s -> s.split("-")[0],
                        s -> Integer.parseInt(s.split("-")[2])
                        )
                );
        System.out.println(map2);


    }
}

在这里插入图片描述
小结:

在这里插入图片描述

2.3.5 apply方法

apply的形参:

流中的数据个数,要跟数组的长度保持一致

apply的返回值:

具体类型的数组

2.4 综合练习

2.4.1 数据过滤

在这里插入图片描述

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,1,2,3,4,5,6,7,8,9);
        //进行判断,如果是偶数,则返回true,true就保留

        //将结果保存起来,就是收集的意思,一般都是收集到集合当中
        List<Integer> newlist = list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList());
        System.out.println(newlist);
    }

在这里插入图片描述

2.4.2 字符串过滤并收集

在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("zhangsan,23");
        list.add("lisi,24");
        list.add("wangwu,25");
        //保留年龄大于等于24的人
        //要分割字符串里面的逗号s.split(","),分割后取索引[1]
        //把取到到字符串数字转换为整型,然后判断>=24,把符合条件的集合过滤出来
        //原式表达
        list.stream()
                .filter(s->Integer.parseInt(s.split(",")[1])>=24)
                .collect(Collectors.toMap(
                        new Function<String, Object>() {
                            @Override
                            public String apply(String s){
                                return s.split(",")[0];
                            }
                        }, new Function<String, Integer>() {
                            @Override
                            public Integer apply(String s){
                                return Integer.parseInt(s.split(",")[1]);
                            }
                        }
                ));
        //lambda表达式
        Map<String, Integer> newlist = list.stream()
                .filter(s -> Integer.parseInt(s.split(",")[1]) >= 24)
                .collect(Collectors.toMap(
                        s -> s.split(",")[0],
                        s -> Integer.parseInt(s.split(",")[1])
                ));
        System.out.println(newlist);

    }
}

在这里插入图片描述

2.4.3 自定义对象过滤并收集

在这里插入图片描述

Actor类:创建省略

测试类
public class Test {
    public static void main(String[] args) {
        //1.创建两个ArrayList集合
        ArrayList<String> manList = new ArrayList<>();
        ArrayList<String> womanList = new ArrayList<>();
        //2.添加数据
        Collections.addAll(manList,"小黑子,24","叶剃咸,23","刘不甜,22","吴签,24","谷嘉,30","肖梁梁,27");
        Collections.addAll(womanList,"赵小颖,35","杨颖,36","高元元,43","张天天,31","刘诗,35","杨小幂,33");
        //3.男演员只要3个字的前两人
        Stream<String> stream1 = manList.stream()
                .filter(s -> s.split(",")[0].length() >= 3)
                .limit(2);

        //4.女演员只要姓杨的,并且不要第一个
        Stream<String> stream2 = womanList.stream()
                .filter(s -> s.split(",")[0].startsWith("杨"))
                .skip(1);

        //5.把过滤后的男演员姓名和女演员的姓名合并到一起
        //演员信息封装成Actor对象 String->Actor对象(类型转换)
//        Stream.concat(stream1,stream2)
//                .map(new Function<String, Actor>() {
//                    @Override
//                    public Actor apply(String s){
//                        //"小黑子,24"
//                        String name = s.split(",")[0];
//                        int age = Integer.parseInt(s.split(",")[1]);
//                        return new Actor(name,age);
//                    }
//                }).forEach(s-> System.out.println(s));
        
        //lambda表达式
        List<Actor> list = Stream.concat(stream1, stream2)
                .map(
                        s -> new Actor(
                                s.split(",")[0],
                                Integer.parseInt(s.split(",")[1])
                        )
                ).collect(Collectors.toList());//把数据存放到数组当中
        System.out.println(list);

    }
}

在这里插入图片描述

3.方法引用

在这里插入图片描述
在这里插入图片描述

方法引用

  1. 引用处需要是函数式接口
  2. 被引用的方法需要已经存在
  3. 被引用方法的形参和返回值需要跟抽象方法的形参和返回值保持一致
  4. 被引用方法的功能需要满足当前的要求
public class Main {
    public static void main(String[] args) {
        //需求:创建一个数组,进行倒序排序
        Integer[] arr = {3,5,4,1,6,2};
        //匿名内部类
        Arrays.sort(arr, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2-o1;
            }
        });

        //lambda表达式
        //因为第二个参数的类型Comparator是一个函数式接口
        Arrays.sort(arr,(Integer o1,Integer o2)->{
            return o2 - o1;
        });
        //lambda表达式简化版
        Arrays.sort(arr,(o1,o2)->o2-o1);

        //表示引用FuctionDemo1类里面的subtraction方法
        //把这个方法当做抽象方法的方法体
        Arrays.sort(arr,Main::subtraction);
        System.out.println(Arrays.toString(arr));


    }

    //可以是Java已经写好的,也可以是一些第三方的工具类
    public static int subtraction(int num1,int num2){
        return num2 - num1;
    }
}

在这里插入图片描述
小结:
在这里插入图片描述

3.1 引用静态方法

在这里插入图片描述
引用静态方法
格式:类名::静态方法
范例:Integer::parseInt

静态方法中没有this!!
在这里插入图片描述
在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        //1.创建
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"1","2","3","4","5");

        //转换成int类型
        list.stream().map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s){
                int i = Integer.parseInt(s);
                return i;
            }
        }).forEach(s-> System.out.println(s));
        System.out.println("==================================");
        //此时上面这样的function方法我不想写,用别人相同的代码拿过来


        //1.方法需要已经存在
        //2.方法的形参和返回值需要跟抽象方法的形参和返回值保持一致
        //3.方法的功能需要把形参的字符串转换成整数
        //那么就是parseInt了
        list.stream().map(Integer::parseInt).forEach(s-> System.out.println(s));

    }
}

在这里插入图片描述

3.2 引用其他类的成员方法

在这里插入图片描述
在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        //1.创建
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"张无忌","周芷若","赵敏","张强","张三丰");
        //3.过滤数据(只要以张开头,而且名字是3个字的)
        list.stream().filter(s->s.startsWith("张")).filter(s->s.length()==3);

        //匿名类写法
        list.stream().filter(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.startsWith("张")&&s.length()==3;
            }
        }).forEach(s-> System.out.println(s));
        System.out.println("====================");

        //静态方法中没有this,故要重新在本类new一个本类的对象出来引用
        list.stream().filter(new Main()::stringJudge)
                .forEach(s-> System.out.println(s));


    }

    public boolean stringJudge(String s){
        return s.startsWith("张")&&s.length()==3;
    }
}

在这里插入图片描述

3.3 引用本类和父类的成员方法

在这里插入图片描述

3.4 引用构造方法

在这里插入图片描述
目的:为了创建对象

练习:
在这里插入图片描述
在这里插入图片描述

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


    public Student() {
    }

    public Student(String str){
        //这里str不是代表name,而是依次表示流里面的每一个数据
        String[] arr = str.split(",");
        this.name = arr[0];
        this.age = Integer.parseInt(arr[1]);
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}

测试类:
   public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"麻瓜,15","小老板,34","石乐志,18");
        //封装成Student对象并收集到List集合中
        //String -> Student
        //方法引用
        List<Student> list2 = list.stream().map(Student::new).collect(Collectors.toList());
        System.out.println(list2);

    }

在这里插入图片描述

3.5 类名引用构造方法

在这里插入图片描述
该方法引用的规则:

  1. 需要有函数式接口
  2. 被引用的方法必须已经存在
  3. 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致
  4. 被引用的功能需要满足当下的需求

抽象方法形参的详解:
在这里插入图片描述
其局限性:

  1. 不能引用所有类中的成员方法
  2. 是跟抽象方法的第一个参数有关,这个参数是什么类型的,那么久只能引用这个类中的方法

练习:
在这里插入图片描述

public class FunctionDemo2 {
    public static void main(String[] args) {
        //1.创建集合对象
        ArrayList<String> list = new ArrayList<>();
        //2.添加数据
        Collections.addAll(list,"aaa","bbb","ccc","ddd");
        //3.变成大写后进行输出
        //String-> String
        list.stream().map(new Function<String, String>() {
            @Override
            public String apply(String s){
                return s.toUpperCase();
            }
        }).forEach(s-> System.out.println(s));
        System.out.println("-------------------------------");

        //类名引用方法简化
        list.stream().map(String::toUpperCase).forEach(s-> System.out.println(s));
    }
}

在这里插入图片描述

在这里插入图片描述

  • 符合规则3且跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法是无参的成员方法
    在这里插入图片描述

  • 类名不是什么方法都可以引用的,有规定,主要看第一个参数
    被引用方法的调用者,决定了可以引用哪些类中的方法,故已经表明使用String类型的
    在这里插入图片描述

3.6 引用数组的构造方法

在这里插入图片描述
细节:创建数组的类型时,需要跟流中数据的类型保持一致

练习:
在这里插入图片描述

 public class FunctionDemo2 {
    public static void main(String[] args) {
        //1.创建集合对象
        ArrayList<Integer> list = new ArrayList<>();
        //2.添加数据
        Collections.addAll(list,1,2,3,4,5);
        //收集到数组当中
        //匿名内部类
        Integer[] arr = list.stream().toArray(new IntFunction<Integer[]>() {
            @Override
            public Integer[] apply(int value) {
                return new Integer[value];
            }
        });
        System.out.println(Arrays.toString(arr));

        //引用数组的构造方法
        Object[] arr2 = list.stream().toArray(Integer[]::new);
        System.out.println(Arrays.toString(arr2));

    }
}

在这里插入图片描述
小结:
在这里插入图片描述
在这里插入图片描述

3.7 方法引用练习

3.7.1 练习1

在这里插入图片描述

student类:
public class Student {
    private String name;
    private int age;


    public Student() {
    }

    public Student(String str){
        //这里str不是代表name,而是依次表示流里面的每一个数据
        String[] arr = str.split(",");
        this.name = arr[0];
        this.age = Integer.parseInt(arr[1]);
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}

测试类:
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list,"张无忌,15","周芷若,14","赵敏,13","张强,20"
                ,"张三丰,100","张翠山,40","张良,35","王二麻子,37","谢广坤,41");
        //先把字符串变成Student对象,然后再把Student对象收集起来
        Student[] arr = list.stream().map(Student::new).toArray(Student[]::new);
        //一开始的时候,流里面的数据一定是字符串,当map方法执行完毕时,已经把字符串对象变为了Student对象
        //所以map后面toArray收集时,就不能创建String对象了,要对应Student对象
        System.out.println(Arrays.toString(arr));

    }

在这里插入图片描述

3.7.2 练习2

方法细节:

  1. 现在有没有一个方法符合当前的需求
  2. 如果有这样的方法,这个方法是否满足引用的规则
    静态 类名::方法名
    成员方法 对象名::方法名(本类this,父类super)
    构造方法 类名::new

在这里插入图片描述

  public static void main(String[] args) {
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("zhangsan",23));
        list.add(new Student("magua",24));
        list.add(new Student("lisi",25));
        //获取姓名并放到数组当中
        //刚开始的时候,想不到引用什么方法,那么就可以时候匿名内部类
        String[] namelist = list.stream().map(new Function<Student, Object>() {
            @Override
            public String apply(Student student) {
                return student.getName();
            }
        }).toArray(String[]::new);
        System.out.println(Arrays.toString(namelist));

        //方法引用简化
        String[] arr = list.stream().map(Student::getName).toArray(String[]::new);
        System.out.println(Arrays.toString(arr));

    }

在这里插入图片描述

3.7.3 练习3

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();

        // 添加学生对象
        students.add(new Student("张三", 23));
        students.add(new Student("李四", 24));

        // 将每个学生对象的姓名和年龄拼接成字符串,并放到数组当中,使用方法引用完成
        String[] namesAndAges = students.stream()
                .map(Student::getNameAndAge)
                .toArray(String[]::new);

        // 输出结果
        for (String nameAndAge : namesAndAges) {
            System.out.println(nameAndAge);
        }
    }
}

class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getNameAndAge() {
        return name + "-" + age;
    }
}

在这里插入图片描述

4.异常

异常:

异常就是代表程序出现的问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
1.error:硬件类的问题

2.Exception:叫做异常,代表程序可能出现的问题。
我们通常会用Exception以及他的子类来封装程序出现的问题。

运行时异常:RuntimeException及其子类,编译阶段不会出现异常提醒。
运行时出现的异常(如:数组索引越界异常)

1)编译时异常:没有继承RuntimeExcpetion的异常,直接继承于Excpetion。
编译阶段就会错误提示
2)运行时异常:RuntimeException本身和子类。
3)编译阶段没有错误提示,运行时出现的

在这里插入图片描述
小结:

在这里插入图片描述

4.1 编译时和运行时异常

String name = "2030年1月1日";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
Date date = sdf.parse(time);
System.out.println(date);

parse正常情况下会报错,需要在main函数的后面加上throws ParseException才能将报错取消
这就是编译时异常,在编译阶段,必须要手动处理,否则代码报错

int[] arr = {1,2,3,4,5};
System.out.println(arr[10]);

数组越界异常,标准的运行时异常,在编译阶段是不需要处理的,是代码运行时出现的异常

在这里插入图片描述
为什么不把所有的异常归为一类呢?
在编译阶段,java不会运行diamagnetic,只会检查语法是否错误,或者做一些性能的优化,更多的是提醒程序员检查本地信息

运行时异常就是代码出错而导致程序出现问题
在这里插入图片描述
小结:

在这里插入图片描述

4.2 异常在代码中的两个作用

在这里插入图片描述
异常:空指针异常

student类:
public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}

测试类:
    public static void main(String[] args) {
        Student[] arr = new Student[3];//null null null
        String name = arr[0].getName();
        System.out.println(name);
    }

在这里插入图片描述
异常:索引越界

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


    public Student() {
    }

    public Student(String str){//"张三,23"
        String[] arr = str.split("-");
        //arr 0 :张三,23
        this.name = arr[0];
        this.age = Integer.parseInt(arr[1]);//索引越界,把"-"改成 , 就行了
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}

测试类:public class ExceptionDemo1 {
    public static void main(String[] args) {
        Student stu = new Student("张三,23");
        System.out.println(stu);
    }
}

在这里插入图片描述
异常:输入超出范围
在这里插入图片描述
在这里插入图片描述

修改:

public void setAge(int age){
	if(age < 18 || age > 40){
		throw new RuntimeException();
	}else {
		this.age=age;
	}
}

学生类中的赋值年龄成员函数,如果是年龄小于18或者大于40的打印输出语句提示错误,只是在控制台打印出来,并不能直接告诉调用处(代码编写处)
这样就可以有两种解决方法,第一种自己处理掉问题,或者第二种打印在控制台上

4.3 异常的处理方式

  1. JVM虚拟机默认处理异常的方式
  2. 自己处理(捕获异常)
  3. 抛出异常(交给调用者)
4.3.1 JVM默认的处理方案

在这里插入图片描述

    public static void main(String[] args) {
        System.out.println("狂踹瘸子那条好腿");
        System.out.println(2/0);
        System.out.println("是秃子终会发光");
        System.out.println("火鸡味锅巴");
    }

在这里插入图片描述

4.3.2 捕获异常

在这里插入图片描述
好处:

可以让程序继续往下执行,不会停止

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6};
        try {
        //可能出现异常的代码
            System.out.println(arr[10]);
            //此处出现了异常,程序就会在这里创建一个ArrayIndexOutOfBoundsException对象
            //然后new ArrayIndexOutOfBoundsException();
            //拿着这个对象到catch的小括号对比,看括号中的变量是否可以接收这个对象
            //如果能被接收,就表示该异常就被捕获,执行catch里面的代码
            //当catch里面所有的代码执行完毕,继续执行try...catch体系下面的代码
        }catch (ArrayIndexOutOfBoundsException e){
        //如果出现了ArrayIndexOutOfBoundsException异常,执行
            System.out.println("索引越界了");
        }
        System.out.println("看看我执行了吗");
    }

在这里插入图片描述

在这里插入图片描述

4.3.2 - I 如果try中没有遇到问题,怎么执行?
  • 会把try里面的代码全部执行完毕,不会执行catch里面的代码

注意:只有当出现了异常才会执行catch里面的代码

4.3.2 - II 如果try中可能会遇到多个问题,怎么执行?
  • 要写多个catch与之对应

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

了解性:在JDK7之后,我们可以在catch中同时捕获多个异常,中间用|进行隔开
表示如果出现了A异常或者B异常的话,采取同一种处理方案

4.3.2 - III 如果try中遇到的问题没有被捕获,怎么执行?
  • 相当于try…catch的代码白写了,最终还是会交给虚拟机进行处理
4.3.2 - IIIV 如果try中遇到了问题,那么try下面的其他代码还会执行吗?

下面的代码就不会运行了,直接跳转到对应的catch当中,执行catch里面的语句体

  • 但是如果没有对应catch与之匹配,那么还是会交给虚拟机进行处理

小结:
在这里插入图片描述

4.3.3 异常中的常见方法

在这里插入图片描述
快捷键 ctrl + alt + t 选择try……catch包裹

1.e.getMessage

   public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6};
        try {
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            String message = e.getMessage();
            System.out.println(message);
        }
        System.out.println("看看我执行了吗");
    }

在这里插入图片描述

2.e.toString

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6};
        try {
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            String str = e.toString();
            System.out.println(str);//出现了异常的名字和异常的信息
        }
        System.out.println("看看我执行了吗");
    }

在这里插入图片描述
3.e.printStackTrace

在底层是利用system.err.println进行输出把异常的错误信息以红色字体输出在控制台

细节:仅仅是打印信息,不会停止程序运行

 public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6};
        try {
            System.out.println(arr[10]);
        } catch (ArrayIndexOutOfBoundsException e) {
            e.printStackTrace();//这个方法是没有返回值的,直接调用即可
        }
        System.out.println("看看我执行了吗");

        //以红色字体打印的报错
        System.err.println(123);
    }

在这里插入图片描述

4.3.4 抛出异常

在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        int[] arr = {};
        int max = 0;
        try{
            max = getMax(arr);
        }catch (NullPointerException e){
            System.out.println("空指针异常");
        }catch (ArrayIndexOutOfBoundsException e){
            System.out.println("索引越界异常");
        }
        System.out.println(max);
    }

    public static int getMax(int[] arr) /*throws NullPointerException, ArrayIndexOutOfBoundsException 运行时异常不需要手动声明,可以不写*/{
        if(arr == null){
            //手动创建一个异常,并把这个异常交给方法的调用者处理
            //此时方法就会结束,下面的代码不会再执行了
            throw new NullPointerException();
        }
        if(arr.length == 0){
            throw new ArrayIndexOutOfBoundsException();
        }

        System.out.println("看看我执行了吗?");
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if(arr[i]>max){
                max = arr[i];
            }
        }
        return max;
    }

}

在这里插入图片描述
小结:

在这里插入图片描述

4.4 异常的综合练习

在这里插入图片描述

studnet类:public class Student {
    private String name;
    private int age;


    public Student() {
    }

    public Student(String str){//"张三,23"
        String[] arr = str.split("-");
        //arr 0 :张三,23
        this.name = arr[0];
        this.age = Integer.parseInt(arr[1]);//索引越界,把"-"改成 , 就行了
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Student{name = " + name + ", age = " + age + "}";
    }
}

测试类:
public class ExceptionDemo1 {
    public static void main(String[] args) {
        //1.创建键盘录入的对象
        Scanner sc = new Scanner(System.in);
        //2.创建对象
        GirlFriend gf = new GirlFriend();
        //3.接收姓名
        //接收时最有可能出现异常,所以把其放在try中

        while (true) {
            //使用死循环,为了错误时重新录入
            try {

                System.out.println("请输入女友的名字:");
                String name = sc.nextLine();
                gf.setName(name);
                //接收年龄
                System.out.println("请输入女友年龄:");
                String ageStr = sc.nextLine();
                int age = Integer.parseInt(ageStr);
                gf.setAge(age);
                //如果所有数据正确,那么跳出循环
                break;
            } catch (NumberFormatException e) {
                System.out.println("年龄的格式有误,请输入数字");
//                continue;
            }//RuntimeException一定要写在下面,因为NumberFormatException它的爷爷就是RuntimeException
            catch (RuntimeException e) {
                System.out.println("姓名的长度或者年龄的范围有误");
//                continue;
            }
        }
        System.out.println(gf);

    }
}

在这里插入图片描述

4.5 自定义异常

自定义异常的意义就是为了让控制台的报错信息更加的见名知意

创建自定义异常

  1. 定义异常类
  2. 写继承关系
  3. 空参构造
  4. 带参构造
自定义异常NameFormatException类:
public class NameFormatException extends RuntimeException{
    //技巧:
    //NameFormat:当前异常的名字,表示姓名格式化问题
    //Exception:表示当前类是一个异常类

    //运行时: RuntimeException核心就表示由于参数错误而导致的问题
    //编译时: Exception核心·提醒程序员检查本地信息
    public NameFormatException() {
    }

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

自定义异常AgeOutOfBoundsException类:
public class AgeOutOfBoundsException extends RuntimeException{
    public AgeOutOfBoundsException() {
    }

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


GirlFriend类:public class GirlFriend {
    private String name;
    private int age;

    public GirlFriend() {
    }

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

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        int len = name.length();
        if(len<3||len>10){
            throw new NameFormatException(name+"格式有误,长度应该为:3~10");
        }
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        if(age<18||age>40){
            throw new AgeOutOfBoundsException(age+"超出了范围");
            //把异常抛给调用者
        }
        this.age = age;
    }

    public String toString() {
        return "GirlFriend{name = " + name + ", age = " + age + "}";
    }
}

public class ExceptionDemo1 {
    public static void main(String[] args) {
        //1.创建键盘录入的对象
        Scanner sc = new Scanner(System.in);
        //2.创建对象
        GirlFriend gf = new GirlFriend();
        //3.接收姓名
        //接收时最有可能出现异常,所以把其放在try中

        while (true) {
            //使用死循环,为了错误时重新录入
            try {

                System.out.println("请输入女友的名字:");
                String name = sc.nextLine();
                gf.setName(name);
                //接收年龄
                System.out.println("请输入女友年龄:");
                String ageStr = sc.nextLine();
                int age = Integer.parseInt(ageStr);
                gf.setAge(age);
                //如果所有数据正确,那么跳出循环
                break;
            } catch (NumberFormatException e) {
                System.out.println("年龄的格式有误,请输入数字");
//                continue;
            }
            catch (NameFormatException e) {
                e.printStackTrace();
            }
            catch (AgeOutOfBoundsException e) {
                e.printStackTrace();
            }

        }
        System.out.println(gf);

    }
}

在这里插入图片描述

5. File

File对象就表示一个路径,可以是文件的路径、也可以是文件夹的路径
这个路径可以是存在的,也允许是不存在的

在这里插入图片描述

“C:\Users\xiaoyou\Desktop\a.txt”为例

  • 父级路径就是“C:\Users\xiaoyou\Desktop”
  • 子级路径就是“a.txt”

5.1 File的构造方法

在这里插入图片描述

  public static void main(String[] args) {
        //1.根据字符串表示的路径,变成File对象
        String str = "C:\\Users\\xiaoyou\\Desktop\\a.txt";
        File f1 = new File(str);
        System.out.println(f1);

        //2.父级路径:C:\Users\xiaoyou\Desktop
        //子级路径:a.txt
        String parent =  "C:\\Users\\alienware\\Desktop";
        String child = "a.txt";
        File f2 = new File(parent,child);
        System.out.println(f2);

        File f3 = new File(parent+"\\"+child);
        System.out.println(f3);

        //3.把一个File表示的路径和String表示路径进行拼接
        File parent2 = new File("C:\\Users\\alienware\\Desktop");
        String child2 = "b.txt";
        File f4 = new File(parent2,child2);
        System.out.println(f4);
    }

小结:
在这里插入图片描述

5.2 File的常见成员方法

5.2.1 判断和获取相关的

在这里插入图片描述
文件图:
在这里插入图片描述

1.判断的方法

    public static void main(String[] args) {
        //1.对一个文件的路径进行判断
        File F1 = new File("F:\\aaa\\a.txt");
        System.out.println(F1.isDirectory());//false
        System.out.println(F1.isFile());//true
        System.out.println(F1.exists());//true
        System.out.println("--------------------------");

        //2.对一个文件夹的路径进行判断
        File f2 = new File("F:\\aaa\\bbb");
        System.out.println(f2.isDirectory());//true
        System.out.println(f2.isFile());//false
        System.out.println(f2.exists());//true

        //3.对一个不存在的路径进行判断
        //shift + f6 一键改名
        File f3 = new File("F:\\aaa\\c.txt");
        System.out.println(f3.isDirectory());//false
        System.out.println(f3.isFile());//false
        System.out.println(f3.exists());//false
    }

2.获取的方法

public static void main(String[] args) {
        //1.length返回文件的大小(字节数量)
        //细节1: 这个方法只能获取文件的大小,单位是字节
        //如果单位我们要是M,G,可以不断的除以1024
        //细节2: 这个方法无法获取文件夹的大小
        //如果我们要获取一个文件夹的大小,需要把这个文件夹里面所有的文件大小都累加在一起
        File F1 = new File("F:\\aaa\\a.txt");
        long len = F1.length();
        System.out.println(len);//0
        //文件夹
        File f2 = new File("F:\\aaa\\bbb");
        long len2 = f2.length();
        System.out.println(len2);
        System.out.println("----------------------------");

        //2.getAbsolutePath 返回文件的绝对路径
        File f3 = new File("F:\\aaa\\a.txt");
        String path1 = f3.getAbsolutePath();
        System.out.println(path1);
        //在当前模块下又新建一个a.txt文件
        File f4 = new File("javaprogram1\\a.txt");
        String path2 = f4.getAbsolutePath();
        System.out.println(path2);
        System.out.println("----------------------------");

        //3.getPath 返回定义文件时使用的路径
        File f5 = new File("F:\\aaa\\a.txt");
        String path3 = f5.getPath();
        System.out.println(path3);
        //在当前模块下又新建一个a.txt文件
        File f6 = new File("javaprogram1\\a.txt");
        String path4 = f6.getPath();
        System.out.println(path4);
        System.out.println("----------------------------");

        //4.getName 获取名字
        //a.txt
        //  a 文件名
        //  txt 后缀名、扩展名
        File f7 = new File("F:\\aaa\\a.txt");
        String name1 = f7.getName();
        System.out.println(name1);
        //文件夹
        File f8 = new File("F:\\aaa\\bbb");
        String name2 = f8.getName();
        System.out.println(name2);
        System.out.println("----------------------");

        //5.lastModified 返回文件的最后修改实际(时间毫秒值)
        File f9 = new File("F:\\aaa\\a.txt");
        long time = f9.lastModified();
        System.out.println(time);
        System.out.println("-----------------------");
        //如何把时间的毫秒值变成字符串表示的时间呢?
        //yyyy年MM月dd日 HH:mm:ss
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        String timeStr = sdf.format(time);
        System.out.println(timeStr);


    }

在这里插入图片描述

5.2.2 创建和删除相关的

在这里插入图片描述
1.创建相关的

    public static void main(String[] args) throws IOException {
        //1.createNewFile 创建一个新的空文件
        //细节1:如果当前路径表示的文件是不存在的,则创建成功,返回true
        //      如果当前路径表示的文件是存在的,则创建失败,返回false
        //细节2:如果父级路径是不存在的,那么方法会有异常IOException
        //细节3:createNewFile方法创建的一定是文件,如果路径中不包含后缀名,则创建一个没有后缀的文件
        File f1 = new File("F:\\aaa\\c.txt");
        boolean b = f1.createNewFile();//代码写完会有红色异常,alt回车选择签名抛出即可
        System.out.println(b);//true

        //2.mkdir    make Directory. 文件夹(目录)
        //细节1:windows当中路径是唯一的,如果当前路径已经存在,则创建失败,返回false
        //细节2:mkdir方法只能创建单级文件夹,无法创建多级文件夹
        File f2 = new File("F:\\aaa\\aaa\\bbb\\ccc");
        boolean b2 = f2.mkdir();
        System.out.println(b2);//false

        //3.mkdirs  创建多级文件夹
        //细节:既可以创建单级的,又可以创建多级的文件夹
        File f3 = new File("F:\\aaa\\aaaa\\bbb\\ccc");
        boolean b3 = f3.mkdirs();
        System.out.println(b3);//true
        
    }

在这里插入图片描述

2.删除相关的

    public static void main(String[] args) throws IOException {
        //1.delete     删除文件、空文件
        //细节1:
        //      如果刖除的是文件,则直接删除,不走回收站。
        //      如果别除的是空文件夹,则直接删除,不走回收站
        //      如果别除的是有内容的文件夹,则删除失败

        File f1 = new File("F:\\aaa\\bbb");
        //2.删除
        boolean b = f1.delete();
        System.out.println(b);

    }
5.2.3 获取并遍历相关的

在这里插入图片描述

    public static void main(String[] args){
        File f = new File("F:\\aaa");
        //listFiles方法
        //作用:获取aaa文件夹里面的所有内容,把所有的内容放到数组中返回
        File[] files = f.listFiles();
        for (File file : files) {
            //file依次表示aaa文件夹里面的每一个文件或者文件夹
            System.out.println(file);
        }

    }

在这里插入图片描述

在这里插入图片描述

5.2.4 所有遍历并获取的方法

在这里插入图片描述

   public static void main(String[] args){
        //1.listRoots  获取系统中所有的盘符
        File[] arr = File.listRoots();
        System.out.println(Arrays.toString(arr));
        System.out.println("----------------------------------");

        //2.list()  获取当前该路径下所有内容(仅仅能获取名字)
        File f1 = new File("F:\\aaa");
        String[] arr2 = f1.list();
        for (String s : arr2) {
            System.out.println(s);
        }
        System.out.println("--------------------------");

        //3.list(FilenameFilter filter)  利用文件名过滤器获取当前该路径下所有内容
        //需求:我现在要获取F:\\aaa 文件夹里面所有的txt文件
        File f2 = new File("F:\\aaa");
        //accept方法的形参,依次表示aaa文件夹里面的每一个文件或者文件夹路径的路径
        /*
        参数一:父级路径
        参数二:子级路径
        返回值:如果返回值true,就表示当前路径保留
                如果返回值false,就表示当前路径舍弃不要
         */
        String[] arr3 = f2.list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                File src = new File(dir,name);
                return src.isFile()&&name.endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(arr3));
        System.out.println("-----------------------------");

        //4.listFiles 获取当前该路径下所有内容
        File f3 = new File("F:\\aaa");
        //需求:打印里面所有的内容
        File[] arr4 = f3.listFiles();
        for (File file : arr4) {
            if(file.isFile()&&file.getName().endsWith(".txt")){
                System.out.println(file);
            }
        }
        System.out.println("-----------------------------");

        //5.listFiles(FileFilter filter) 利用文件名过滤器获取当前该路径下所有内容
        File f4 = new File("F:\\aaa");
        //调用listFiles
        File[] arr5 = f4.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isFile()&&pathname.getName().endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(arr5));
        System.out.println("-----------------------------");

        //6.listFiles(FilenameFilter filter)
        //与上面方法5不同的是,上面形参表示的是完整的路径
        //这个形参把一个大的路径拆开,第一个参数表示父级路径,第二个参数表示子级路径
        File[] arr6 = f4.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                File src = new File(dir, name);
                return src.isFile() && name.endsWith(".txt");
            }
        });
        System.out.println(Arrays.toString(arr6));

    }

在这里插入图片描述

5.2.5 综合练习
练习一:创建文件

在这里插入图片描述

    public static void main(String[] args) throws IOException {
        //1.创建b.txt的父级路径
        File file = new File("javaprogram1");

        //2.创建父级路径
        //如果bbb是存在的,那么此时创建失败
        //如果bbb是不存在的,那么此时创建成功
        file.mkdirs();

        //3.拼接父级路径和子级路径
        File src = new File(file, "b.txt");
        boolean b = src.createNewFile();
        if(b){
            System.out.println("创建成功");
        }else {
            System.out.println("创建失败");
        }
    }
//创建成功,那么如果再运行依次就会创建失败
练习二:单个文件夹查找文件

在这里插入图片描述

public class Main {
    public static void main(String[] args) throws IOException {
        File file = new File("F:\\aaa");
        boolean b = haveAVI(file);
        System.out.println(b);
    }

    /*
    作用:用来找到某一个文件夹中,是否有以avi结尾的电影
    形参:要查找的文件夹
    返回值:查找的结果 存在true 不存在false
     */
    public static boolean haveAVI(File file){//F:\\aaa
        //1.进入aaa文件夹,而且要获取里面的所有内容
        File[] files = file.listFiles();
        //2.遍历数组获取里面的每一个元素
        for (File f : files) {
            if(f.isFile()&&f.getName().endsWith(".avi")){
                return true;
            }
        }
        //3.如果循环结束之后还没有找到,直接返回true
        return false;

    }
}

//false
练习三:遍历硬盘查找文件夹

在这里插入图片描述
采用递归,把大问题拆成一个个小问题
在这里插入图片描述
所有文件夹的套路:

  1. 进入文件夹
  2. 遍历数组
  3. 判断,如果是文件,就可以执行题目的业务逻辑
  4. 再判断,如果是文件夹,就可以递归
    重要:再次调用本方法的时候,参数一个要是src的次一级路径
  5. 注意:

当有获取文件夹数组遇到没有权限访问的时候,可能为空,那么遍历的时候就会导致空指针异常,所以遍历的时候要做一个非空判断执行

public class Main {
    public static void main(String[] args) {
        File src =new File("F:\\");
        FindAVI(src);
        
        findAVI();
    }

    public static void findAVI(){
        //获取本地所有的盘符
        File src =new File("F:\\");
        File[] arr = src.listRoots();
        for (File file : arr) {
            FindAVI(file);
        }
    }

    public static void FindAVI(File src){
        //1.进入文件夹src
        File[] files = src.listFiles();
        if(files != null){
            //2.遍历数组,依次得到src里面的每一个文件或文件夹
            for (File file : files) {

                if(file.isFile()){
                    //3.判断,如果是文件,就可以执行题目的业务逻辑
                    String name = file.getName();
                    if(name.endsWith(".txt")){
                        System.out.println(file);
                    }
                }else{
                    //4.再判断,如果是文件夹,就可以递归
                    FindAVI(file);
                }
            }
        }
    }
}

在这里插入图片描述

练习四:删除文件夹

在这里插入图片描述
删除一个多级文件夹:
如果我们要删除一个有内容的文件夹

  1. 先删除文件夹里面所有的内容
  2. 再删除自己
public class Main {
    public static void main(String[] args) {
        File src = new File("F:\\aaa\\ccc");
        Delete(src);
    }

    public static void Delete(File src){
        //1.先删除文件夹里面的所有内容
        //进入src
        File[] files = src.listFiles();
        if(files!=null){
            //遍历文件数组
            for (File file : files) {
                //判断,如果是文件则删除
                if(file.isFile()){
                    file.delete();
                    System.out.println("删除成功");
                }else {
                    //如果是文件夹则递归
                    Delete(file);
                }
            }

        }
          //2.遍历完后,再删除自己
            src.delete();
    }

}

图解示例:
在这里插入图片描述

练习五:统计文件夹大小

在这里插入图片描述
作用:统计一个文件夹的总大小
参数:表示要统计的那个文件夹
返回值:统计之后的结果
文件夹的总大小:说白了,文件夹里面的所有文件的大小

图示:
在这里插入图片描述

public class Main {
    public static void main(String[] args) {
        File src = new File("F:\\aaa\\ccc");
        System.out.println(getLen(src)+"字节");
    }

    public static long getLen(File src){
        //1.定义变量进行累加统计
        long len = 0;
        //2.进入src文件夹
        File[] files = src.listFiles();
        //3.遍历
        for (File file : files) {
            //4.判断
            if (file.isFile()){
                //是文件,我们就把当前文件的大小累加到len当中
                len = len + file.length();
            }else {
                //不是文件,是文件夹就递归len+getLEN
                //如果直接递归调用getLen(file)不累加,那么每次递归时len都会刷新,无法看到总长度
                len = len + getLen(file);
            }
        }
        //循环结束后返回len
        return len;
    }


}

在这里插入图片描述

在这里插入图片描述

练习六:统计各种文件夹数量

在这里插入图片描述
作用:统计一个文件夹中每种文件的个数
参数:要统计的那个文件夹
返回值:用来统计map集合

  • 键:后缀名 值:次数
    a.txt
    a.a.txt
    aaa(不需要统计的)
public class Main {
    public static void main(String[] args) {
        File file = new File("F:\\aaa");
        HashMap<String, Integer> hm = getCount(file);
        System.out.println(hm);
    }

    public static HashMap<String,Integer> getCount(File src){
        //1.定义集合用来统计
        HashMap<String,Integer> hm = new HashMap<>();
        //2.进入src文件夹
        File[] files = src.listFiles();
        //3.遍历数组
        for (File file : files) {
            //4.判断,如果是文件,统计
            if(file.isFile()){
                //a.txt
                String name = file.getName();
                //按照 \\. 进行切割,.前后分开
                String[] arr = name.split("\\.");
                //arr如果没有后缀名,数组长度为1,这种情况是不需要判断的
                //arr的长度>=2,说明最后一个索引上的元素才是后缀名
                if(arr.length>=2){
                    String endName = arr[arr.length - 1];//最终索引上的元素
                    //把后缀名到集合里面进行判断
                    if(hm.containsKey(endName)){
                        //存在,就把已经出现的次数拿出来,进行自增
                        int count = hm.get(endName);
                        count++;
                        //再把自增的结果放进map集合里的键值中
                        hm.put(endName,count);
                    }else{
                        //不存在,表示当前文件是第一次出现
                        hm.put(endName,1);
                    }
                }
            }else{
                //5.如果是文件夹,递归
                //sonMap里面是子文件中每一种文件的个数
                HashMap<String,Integer> sonMap = getCount(file);
                //假如:hm:txt=1 jpg=2 doc=3
                //sonMap: txt=3 jpg=1
                //其实遍历sonMap把里面的值累加到hm当中
                Set<Map.Entry<String, Integer>> entries = sonMap.entrySet();
                for (Map.Entry<String, Integer> entry : entries) {
                    String key = entry.getKey();//sonMap的key
                    System.out.println(entry);//txt=1
                    System.out.println(key);//txt
                    System.out.println(sonMap);//{txt=1}
                    System.out.println(hm);//{txt=3}
                    System.out.println(hm.get(key));//3

                    int value = entry.getValue();
                    System.out.println(value);//1
                    System.out.println("---------------------------");

                    //拿着key放到hm集合中判断是否存在
                    if(hm.containsKey(key)){
                        //存在,则把该value和hm的value进行累加
                        //比如hm的txt 1 + sonMap的txt 3
                        int count = hm.get(key);
                        System.out.println(count);
                        System.out.println(hm);
                        System.out.println(value);

                        count = count + value;//核心
                        hm.put(key,count);
                    }else {
                        //键不存在,直接放进hm集合中
                        hm.put(key,value);
                    }
                }

            }
        }
        return hm;
    }
}

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值