Java:集合以及集合进阶 --黑马笔记

一、集合概述和分类

1.1 集合的分类

除了ArrayList集合,Java还提供了很多种其他的集合,如下图所示:

我想你的第一感觉是这些集合好多呀!但是,我们学习时会对这些集合进行分类学习,如下图所示:一类是单列集合元素是一个一个的,另一类是双列集合元素是一对一对的。

Collection是单列集合的根接口,Collection接口下面又有两个子接口List接口、Set接口,List和Set下面分别有不同的实现类,如下图所示:

上图中各种集合的特点如下图所示:

可以自己写代码验证一下,各种集合的特点:

//简单确认一下Collection集合的特点
ArrayList<String> list = new ArrayList<>(); //存取顺序一致,可以重复,有索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
System.out.println(list); //[java1, java2, java1, java2] 

HashSet<String> list = new HashSet<>(); //存取顺序不一致,不重复,无索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
list.add("java3");
System.out.println(list); //[java3, java2, java1] 

1.2 Collection集合的常用方法

接下来,我们学习一下Collection集合的一些常用方法,这些方法所有Collection实现类都可以使用。 这里我们以创建ArrayList为例,来演示:

Collection<String> c = new ArrayList<>();
//1.public boolean add(E e): 添加元素到集合
c.add("java1");
c.add("java1");
c.add("java2");
c.add("java2");
c.add("java3");
System.out.println(c); //打印: [java1, java1, java2, java2, java3]

//2.public int size(): 获取集合的大小
System.out.println(c.size()); //5

//3.public boolean contains(Object obj): 判断集合中是否包含某个元素
System.out.println(c.contains("java1")); //true
System.out.println(c.contains("Java1")); //false

//4.pubilc boolean remove(E e): 删除某个元素,如果有多个重复元素只能删除第一个
System.out.println(c.remove("java1")); //true
System.out.println(c); //打印: [java1,java2, java2, java3]

//5.public void clear(): 清空集合的元素
c.clear(); 
System.out.println(c); //打印:[]

//6.public boolean isEmpty(): 判断集合是否为空 是空返回true 反之返回false
System.out.println(c.isEmpty()); //true

//7.public Object[] toArray(): 把集合转换为数组
Object[] array = c.toArray();
System.out.println(Arrays.toString(array)); //[java1,java2, java2, java3]

//8.如果想把集合转换为指定类型的数组,可以使用下面的代码
String[] array1 = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(array1)); //[java1,java2, java2, java3]

//9.还可以把一个集合中的元素,添加到另一个集合中
Collection<String> c1 = new ArrayList<>();
c1.add("java1");
c1.add("java2");
Collection<String> c2 = new ArrayList<>();
c2.add("java3");
c2.add("java4");
c1.addAll(c2); //把c2集合中的全部元素,添加到c1集合中去
System.out.println(c1); //[java1, java2, java3, java4]

二、Collection遍历方式

接下来我们学习一下Collection集合的遍历方式。有同学说:“集合的遍历之前不是学过吗?就用普通的for循环啊? “ 没错!之前是学过集合遍历,但是之前学习过的遍历方式,只能遍历List集合,不能遍历Set集合,因为以前的普通for循环遍历需要索引,只有List集合有索引,而Set集合没有索引。

所以我们需要有一种通用的遍历方式,能够遍历所有集合。

2.1 迭代器遍历集合

接下来学习的迭代器就是一种集合的通用遍历方式。

代码写法如下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");
System.out.println(c); //[赵敏, 小昭, 素素, 灭绝]

//第一步:先获取迭代器对象
//解释:Iterator就是迭代器对象,用于遍历集合的工具)
Iterator<String> it = c.iterator();

//第二步:用于判断当前位置是否有元素可以获取
//解释:hasNext()方法返回true,说明有元素可以获取;反之没有
while(it.hasNext()){
    //第三步:获取当前位置的元素,然后自动指向下一个元素.
    String e = it.next();
    System.out.println(e);
}

2.2 增强for遍历集合

刚才我们学习了迭代器遍历集合,但是这个代码其实还有一种更加简化的写法,叫做增强for循环。

格式如下:

需要注意的是,增强for

不光可以遍历集合,还可以遍历数组。接下来我们用代码演示一下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//1.使用增强for遍历集合
for(String s: c){
    System.out.println(s); 
}

//2.再尝试使用增强for遍历数组
String[] arr = {"迪丽热巴", "古力娜扎", "稀奇哈哈"};
for(String name: arr){
    System.out.println(name);
}

2.3 forEach遍历集合

在JDK8版本以后还提供了一个forEach方法也可以遍历集合,如果下图所示:

我们发现forEach方法的参数是一个Consumer接口,而Consumer是一个函数式接口,所以可以传递Lambda表达式:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//调用forEach方法
//由于参数是一个Consumer接口,所以可以传递匿名内部类
c.forEach(new Consumer<String>{
    @Override
    public void accept(String s){
        System.out.println(s);
    }
});


//也可以使用lambda表达式对匿名内部类进行简化
c.forEach(s->System.out.println(s)); //[赵敏, 小昭, 素素, 灭绝]

三、List系列集合

接下来我们学习Collection下面的一个子体系List集合。如下图所示:

3.1 List集合的常用方法

List集合是有索引的,所以多了一些有索引操作的方法。

接下来,我们用代码演示一下效果:

//1.创建一个ArrayList集合对象(有序、有索引、可以重复)
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("至尊宝");
list.add("牛夫人"); 
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//2.public void add(int index, E element): 在某个索引位置插入元素
list.add(2, "紫霞仙子");
System.out.println(list); //[蜘蛛精, 至尊宝, 紫霞仙子, 至尊宝, 牛夫人]

//3.public E remove(int index): 根据索引删除元素, 返回被删除的元素
System.out.println(list.remove(2)); //紫霞仙子
System.out.println(list);//[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//4.public E get(int index): 返回集合中指定位置的元素
System.out.println(list.get(3));

//5.public E set(int index, E e): 修改索引位置处的元素,修改后,会返回原数据
System.out.println(list.set(3,"牛魔王")); //牛夫人
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛魔王]

3.2 List集合的遍历方式

List集合相比于前面的Collection多了一种可以通过索引遍历的方式,所以List集合遍历方式一共有四种:

List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("糖宝宝");

//1.普通for循环
for(int i = 0; i< list.size(); i++){
    //i = 0, 1, 2
    String e = list.get(i);
    System.out.println(e);
}

//2.增强for遍历
for(String s : list){
    System.out.println(s);
}

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

//4.lambda表达式遍历
list.forEach(s->System.out.println(s));

3.3 ArrayList底层的原理

ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。

特点:

我们知道数组的长度是固定的,但是集合的长度是可变的,这是怎么做到的呢?原理如下:

数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。

3.4 LinkedList底层原理

LinkedList底层是链表结构,链表结构是由一个一个的节点组成,一个节点由数据值、下一个元素的地址组成。

LinkedList集合是基于双向链表实现的,所以相对于ArrayList新增了一些可以针对头尾进行操作的方法:

3.5 LinkedList集合的应用场景

刚才我们学习了LinkedList集合,那么LInkedList集合有什么用呢?可以用它来设计栈结构、队列结构。

举例:

入队列可以调用LinkedList集合的addLast方法,出队列可以调用removeFirst()方法。

压栈(push) 等价于 addFirst(),弹栈(pop) 等价于 removeFirst()。

四、Set系列集合

4.1 认识Set集合的特点

Set集合是属于Collection体系下的另一个分支,它的特点如下图所示:

下面我们用代码简单演示一下,每一种Set集合的特点。

//Set<Integer> set = new HashSet<>();	//无序、无索引、不重复
//Set<Integer> set = new LinkedHashSet<>(); //有序、无索引、不重复
Set<Integer> set = new TreeSet<>(); //可排序(升序)、无索引、不重复
set.add(666);
set.add(555);
set.add(555);
set.add(888);
set.add(888);
set.add(777);
set.add(777);
System.out.println(set); //[555, 666, 777, 888]

4.2 HashSet集合底层原理

接下来,为了让同学们更加透彻的理解HashSet为什么可以去重,我们来看一下它的底层原理。

HashSet集合底层是基于哈希表实现的,哈希表根据JDK版本的不同,也是有点区别的:

JDK8以前:哈希表 = 数组+链表

JDK8以后:哈希表 = 数组+链表+红黑树

 我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取  元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引位置。

在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:   

4.3 HashSet去重原理

前面我们学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。

要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。

public class Student{
    private String name; //姓名
    private int age; //年龄
    private double height; //身高
 
    //无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    
    //按快捷键生成hashCode和equals方法
    //alt+insert 选择 hashCode and equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        if (Double.compare(student.height, height) != 0) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        temp = Double.doubleToLongBits(height);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height +
                '}';
    }
}

接着,写一个测试类,往HashSet集合中存储Student对象。

public class Test{
    public static void main(String[] args){
        Set<Student> students = new HashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
        
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        
        for(Student s : students){
            System.out.println(s.toString());
        }
    }
}

打印结果如下,我们发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。

Student{name='牛魔王', age=48, height=169.6}
Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}

4.4 LinkedHashSet底层原理

LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:

4.5 TreeSet集合

最后,我们学习一下TreeSet集合。TreeSet集合的特点是可以对元素进行排序,但是必须指定元素的排序规则。

如果往集合中存储String类型的元素,或者Integer类型的元素,它们本身就具备排序规则,所以直接就可以排序。

如果往TreeSet集合中存储自定义类型的元素,比如说Student类型,则需要我们自己指定排序规则,否则会出现异常。

我们想要告诉TreeSet集合按照指定的规则排序,有两种办法:

第一种:让元素的类实现Comparable接口,重写compareTo方法

第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象

我们先来演示第一种排序方式:

//第一步:先让Student类,实现Comparable接口
//注意:Student类的对象是作为TreeSet集合的元素的
public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;
	//无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    
    //第二步:重写compareTo方法
    //按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。
    /*
    原理:
    在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的
    结果是正数、负数、还是零,决定元素放在后面、前面还是不存。
    */
    @Override
    public int compareTo(Student o) {
        //this:表示将要添加进去的Student对象
        //o: 表示集合中已有的Student对象
        return this.age-o.age;
    }
}

接下来演示第二种排序方式:

//创建TreeSet集合时,传递比较器对象排序
/*
原理:当调用add方法时,底层会先用比较器,根据Comparator的compare方是正数、负数、还是零,决定谁在后,谁在前,谁不存。
*/
//下面代码中是按照学生的年龄升序排序
Set<Student> students = new TreeSet<>(new Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2){
        //需求:按照学生的身高排序
        return Double.compare(o1,o2); 
    }
});

//创建4个Student对象
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("紫霞",23, 169.8);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);

//添加Studnet对象到集合
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
System.out.println(students); 

1.6 并发修改异常

还有一个小问题需要给同学们补充说明一下,那就是在使用迭代器遍历集合时,可能存在并发修改异常。

迭代器遍历机制,规定迭代器遍历集合的同时,不允许集合自己去增删元素,否则就会出现异常。

怎么解决这个问题呢?不使用集合的删除方法,而是使用迭代器的删除方法,代码如下:

List<String> list = new ArrayList<>();
list.add("王麻子");
list.add("小李子");
list.add("李爱花");
list.add("张全蛋");
list.add("晓李");
list.add("李玉刚");
System.out.println(list); // [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]

//需求:找出集合中带"李"字的姓名,并从集合中删除
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String name = it.next();
    if(name.contains("李")){
        //list.remove(name);
        it.remove(); //当前迭代器指向谁,就删除谁
    }
}
System.out.println(list);

五、Collection的其他操作

为了更加方便的对Collection集合进行操作,今天我们还要学习一个操作Collection集合的工具类,叫做Collections。但是Collections工具类中需要用到一个没有学过的小知识点,叫做可变参数,所以必须先学习这个前置知识可变参数,再学习Collections工具类,最后再利用这个工具类做一个综合案例。

5.1 可变参数

可变参数是一种特殊的形式参数,定义在方法、构造器的形参列表处,它可以让方法接收多个同类型的实际参数。可变参数在方法内部,本质上是一个数组。

接下来,我们编写代码来演示一下:

public class ParamTest{
    public static void main(String[] args){
        //不传递参数,下面的nums长度则为0, 打印元素是[]
        test();	
        
        //传递3个参数,下面的nums长度为3,打印元素是[10, 20, 30]
        test(10,20,30); 
        
        //传递一个数组,下面数组长度为4,打印元素是[10,20,30,40] 
        int[] arr = new int[]{10,20,30,40}
        test(arr); 
    }
    
    public static void test(int...nums){
        //可变参数在方法内部,本质上是一个数组
        System.out.println(nums.length);
        System.out.println(Arrays.toString(nums));
        System.out.println("----------------");
    }
}

最后还有一些错误写法,需要让大家写代码时注意一下,不要这么写哦!!!

1.一个形参列表中,只能有一个可变参数;否则会报错。

2.一个形参列表中如果多个参数,可变参数需要写在最后,否则会报错。

5.2 Collections工具类

有了可变参数的基础,我们再学习Collections这个工具类就好理解了,因为这个工具类的方法中会用到可变参数。

注意Collections并不是集合,它比Collection多了一个s,一般后缀为s的类很多都是工具类。这里的Collections是用来操作Collection的工具类。它提供了一些好用的静态方法,如下:

我们把这些方法用代码来演示一下:

public class CollectionsTest{
    public static void main(String[] args){
        //1.public static <T> boolean addAll(Collection<? super T> c, T...e)
        List<String> names = new ArrayList<>();
        Collections.addAll(names, "张三","王五","李四", "张麻子");
        System.out.println(names);//[张三, 王五, 李四, 张麻子]

        //2.public static void shuffle(List<?> list):对集合打乱顺序
        Collections.shuffle(names);
        System.out.println(names);//[张三, 王五, 张麻子, 李四]

        //3.public static <T> void short(List<T list): 对List集合排序
        List<Integer> list = new ArrayList<>();
        list.add(3);
        list.add(5);
        list.add(2);
        Collections.sort(list);
        System.out.println(list);//[2, 3, 5]
    }
}

上面我们往集合中存储的元素要么是Stirng类型,要么是Integer类型,他们本来就有一种自然顺序所以可以直接排序。但是如果我们往List集合中存储Student对象,这个时候想要对List集合进行排序自定义比较规则的。指定排序规则有两种方式,如下:

让元素实现Comparable接口,重写compareTo方法

比如现在想要往集合中存储Studdent对象,首先需要准备一个Student类,实现Comparable接口。

public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;
    
     //排序时:底层会自动调用此方法,this和o表示需要比较的两个对象
    @Override
    public int compareTo(Student o){
        //需求:按照年龄升序排序
        //如果返回正数:说明左边对象的年龄>右边对象的年龄
        //如果返回负数:说明左边对象的年龄<右边对象的年龄,
        //如果返回0:说明左边对象的年龄和右边对象的年龄相同
        return this.age - o.age;
    }
    
    //...getter、setter、constructor..
}

然后再使用Collections.sort(list集合)对List集合排序,如下:

//3.public static <T> void short(List<T list): 对List集合排序
List<Student> students = new ArrayList<>();
students.add(new Student("蜘蛛精",23,169.7));
students.add(new Student("紫霞",22,169.8));
students.add(new Student("紫霞",22,169.8));
students.add(new Student("至尊宝",26,169.5));

/*
原理:sort方法底层会遍历students集合中的每一个元素,采用排序算法,将任意两个元素两两比较;
	每次比较时,会用一个Student对象调用compareTo方法和另一个Student对象进行比较;
	根据compareTo方法返回的结果是正数、负数,零来决定谁大,谁小,谁相等,重新排序元素的位置
	
	注意:这些都是sort方法底层自动完成的,想要完全理解,必须要懂排序算法才行;
*/
Collections.sort(students);	
System.out.println(students);

使用sort方法,传递比较器

/*
原理:sort方法底层会遍历students集合中的每一个元素,采用排序算法,将任意两个元素两两比较;
	每次比较,会将比较的两个元素传递给Comparator比较器对象的compare方法的两个参数o1和o2,
	根据compare方法的返回结果是正数,负数,或者0来决定谁大,谁小,谁相等,重新排序元素的位置
	
	注意:这些都是sort方法底层自动完成的,不需要我们完全理解,想要理解它必须要懂排序算法才行.
*/
Collections.sort(students, new Comparator<Student>(){
    @Override
    public int compare(Student o1, Student o2){
        return o1.getAge()-o2.getAge();
    }
});	
System.out.println(students);

5.3 斗地主案例

第一步:为了表示每一张牌有哪些属性,首先应该新建一个扑克牌的类
第二步:启动游戏时,就应该提前准备好54张牌
第三步:接着再完全洗牌、发牌、捋牌、看牌的业务逻辑

先来完成第一步,定义一个扑克类Card

package com.itheima.test01;

public class Card {
    private String number;
    private String color;
    // 每张牌是存在大小的。
    private int size; // 0 1 2 ....

    public Card() {
    }

    public Card(String number, String color, int size) {
        this.number = number;
        this.color = color;
        this.size = size;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getSize() {
        return size;
    }

    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public String toString() {
        return color + number ;
    }
}

 再完成第二步,定义一个房间类,初始化房间时准备好54张牌,定义一个启动游戏的方法,完成洗牌、发牌、捋牌、看牌的业务逻辑 。

package com.itheima.test01;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Room {
    // 必须有一副牌。
    private final List<Card> allCards = new ArrayList<>();

    public Room() {
        // 1、做出54张牌,存入到集合allCards
        // a、点数:个数确定了,类型确定。
        String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
        // b、花色:个数确定了,类型确定。
        String[] colors = {"♠", "♥", "♣", "♦"};
        int size = 0; // 表示每张牌的大小
        // c、遍历点数,再遍历花色,组织牌
        for (String number : numbers) {
            // number = "3"
            size++; // 1 2 ....
            for (String color : colors) {
                // 得到一张牌
                Card c = new Card(number, color, size);
                allCards.add(c); // 存入了牌
            }
        }
        // 单独存入小大王的。
        Card c1 = new Card("", "🃏", ++size);
        Card c2 = new Card("", "👲", ++size);
        Collections.addAll(allCards, c1, c2);
        System.out.println("新牌:" + allCards);
    }

    public void start() {
        // 1、洗牌: allCards
        Collections.shuffle(allCards);
        System.out.println("洗牌后:" + allCards);

        // 2、发牌,首先肯定要定义 三个玩家。 List(ArrayList)  Set(TreeSet)
        List<Card> linHuChong = new ArrayList<>();
        List<Card> jiuMoZhi = new ArrayList<>();
        List<Card> renYingYing = new ArrayList<>();
        // 正式发牌给这三个玩家,依次发出51张牌,剩余3张做为底牌。
        // allCards = [♥3, ♣10, ♣4, ♥K, ♦Q, ♣2, 🃏, ♣8, ....
        //             0     1   2   3   4   5   6 ...   % 3
        for (int i = 0; i < allCards.size() - 3; i++) {
            Card c = allCards.get(i);
            // 判断牌发给谁
            if (i % 3 == 0) {
                // 请啊冲接牌
                linHuChong.add(c);
            } else if (i % 3 == 1) {
                // 请啊鸠来接牌
                jiuMoZhi.add(c);
            } else {
                // 请盈盈接牌
                renYingYing.add(c);
            }
        }

        // 3、对3个玩家的牌进行排序
        sortCards(linHuChong);
        sortCards(jiuMoZhi);
        sortCards(renYingYing);
        // 4、看牌
        System.out.println("啊冲:" + linHuChong);
        System.out.println("啊鸠:" + jiuMoZhi);
        System.out.println("盈盈:" + renYingYing);
        List<Card> lastThreeCards = allCards.subList(allCards.size() - 3, allCards.size()); // 51 52 53
        System.out.println("底牌:" + lastThreeCards);
        jiuMoZhi.addAll(lastThreeCards);
        sortCards(jiuMoZhi);
        System.out.println("啊鸠抢到地主后:" + jiuMoZhi);
    }

    /**
     * 集中进行排序
     *
     * @param cards
     */
    private void sortCards(List<Card> cards) {
        Collections.sort(cards, new Comparator<Card>() {
            @Override
            public int compare(Card o1, Card o2) {
                // return o1.getSize() - o2.getSize(); // 升序排序
                return o2.getSize() - o1.getSize(); // 降序排序
            }
        });

    }
}

不要忘记了写测试类了:

package com.itheima.test01;

public class GameDemo {
    public static void main(String[] args) {
        //  1、牌类。
        //  2、房间
        Room m = new Room();
        //  3、启动游戏
        m.start();
    }
}

运行结果图:

六、Map集合

前面我们已经把单列集合学习完了,接下来我们要学习的是双列集合。首先我们还是先认识一下什么是双列集合。

所谓双列集合,就是说集合中的元素是一对一对的。Map集合中的每一个元素是以key=value的形式存在的,一个key=value就称之为一个键值对,而且在Java中有一个类叫Entry类,Entry的对象用来表示键值对对象。

所有的Map集合有如下的特点:键不能重复,值可以重复,每一个键只能找到自己对应的值。

Map集合也有很多种,在Java中使用不同的类来表示的,每一种Map集合其键的特点是有些差异的,值是键的一个附属值,所以我们只关注键的特点就可以了。

6.1 Map集合的常用方法

由于Map是所有双列集合的父接口,所以我们只需要学习Map接口中每一个方法是什么含义,那么所有的Map集合方法你就都会用了。

public class MapTest2 {
    public static void main(String[] args) {
        // 1.添加元素: 无序,不重复,无索引。
        Map<String, Integer> map = new HashMap<>();
        map.put("手表", 100);
        map.put("手表", 220);
        map.put("手机", 2);
        map.put("Java", 2);
        map.put(null, null);
        System.out.println(map);
        // map = {null=null, 手表=220, Java=2, 手机=2}

        // 2.public int size():获取集合的大小
        System.out.println(map.size());

        // 3、public void clear():清空集合
        //map.clear();
        //System.out.println(map);

        // 4.public boolean isEmpty(): 判断集合是否为空,为空返回true ,反之!
        System.out.println(map.isEmpty());

        // 5.public V get(Object key):根据键获取对应值
        int v1 = map.get("手表");
        System.out.println(v1);
        System.out.println(map.get("手机")); // 2
        System.out.println(map.get("张三")); // null

        // 6. public V remove(Object key):根据键删除整个元素(删除键会返回键的值)
        System.out.println(map.remove("手表"));
        System.out.println(map);

        // 7.public  boolean containsKey(Object key): 判断是否包含某个键 ,包含返回true ,反之
        System.out.println(map.containsKey("手表")); // false
        System.out.println(map.containsKey("手机")); // true
        System.out.println(map.containsKey("java")); // false
        System.out.println(map.containsKey("Java")); // true

        // 8.public boolean containsValue(Object value): 判断是否包含某个值。
        System.out.println(map.containsValue(2)); // true
        System.out.println(map.containsValue("2")); // false

        // 9.public Set<K> keySet(): 获取Map集合的全部键。
        Set<String> keys = map.keySet();
        System.out.println(keys);

        // 10.public Collection<V> values(); 获取Map集合的全部值。
        Collection<Integer> values = map.values();
        System.out.println(values);

        // 11.把其他Map集合的数据倒入到自己集合中来。(拓展)
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("java1",  10);
        map1.put("java2",  20);
        Map<String, Integer> map2 = new HashMap<>();
        map2.put("java3",  10);
        map2.put("java2",  222);
        map1.putAll(map2); // putAll:把map2集合中的元素全部倒入一份到map1集合中去。
        System.out.println(map1);
        System.out.println(map2);
    }
}

6.2 Map集合遍历方式1

Map集合一共有三种遍历方式,我们先来学习第一种,他需要用到下面的两个方法:

public class MapTest1 {
    public static void main(String[] args) {
        // 准备一个Map集合。
        Map<String, Double> map = new HashMap<>();
        map.put("蜘蛛精", 162.5);
        map.put("蜘蛛精", 169.8);
        map.put("紫霞", 165.8);
        map.put("至尊宝", 169.5);
        map.put("牛魔王", 183.6);
        System.out.println(map);
        // map = {蜘蛛精=169.8, 牛魔王=183.6, 至尊宝=169.5, 紫霞=165.8}

        // 1、获取Map集合的全部键
        Set<String> keys = map.keySet();
        // System.out.println(keys);
        // [蜘蛛精, 牛魔王, 至尊宝, 紫霞]
        //         key
        // 2、遍历全部的键,根据键获取其对应的值
        for (String key : keys) {
            // 根据键获取对应的值
            double value = map.get(key);
            System.out.println(key + "=====>" + value);
        }
    }
}

6.3 Map集合遍历方式2

接下来我们学习Map集合的第二种遍历方式,这种遍历方式更加符合面向对象的思维。

前面我们给大家介绍过,Map集合是用来存储键值对的,而每一个键值对实际上是一个Entry对象。

这里Map集合的第二种方式,是直接获取每一个Entry对象,把Entry存储到Set集合中去,再通过Entry对象获取键和值。

public class MapTest2 {
    public static void main(String[] args) {
        Map<String, Double> map = new HashMap<>();
        map.put("蜘蛛精", 169.8);
        map.put("紫霞", 165.8);
        map.put("至尊宝", 169.5);
        map.put("牛魔王", 183.6);
        System.out.println(map);
        // map = {蜘蛛精=169.8, 牛魔王=183.6, 至尊宝=169.5, 紫霞=165.8}
        // entries = [(蜘蛛精=169.8), (牛魔王=183.6), (至尊宝=169.5), (紫霞=165.8)]
        // entry = (蜘蛛精=169.8)
        // entry = (牛魔王=183.6)
        // ...
		
        // 1、调用Map集合提供entrySet方法,把Map集合转换成键值对类型的Set集合
        Set<Map.Entry<String, Double>> entries = map.entrySet();
        for (Map.Entry<String, Double> entry : entries) {
            String key = entry.getKey();
            double value = entry.getValue();
            System.out.println(key + "---->" + value);
        }
    }
}

6.4 Map集合遍历方式3

Map集合的第三种遍历方式,需要用到下面的一个方法forEach,而这个方法是JDK8版本以后才有的。调用起来非常简单,最好是结合的lambda表达式一起使用。

public class MapTest3 {
    public static void main(String[] args) {
        Map<String, Double> map = new HashMap<>();
        map.put("蜘蛛精", 169.8);
        map.put("紫霞", 165.8);
        map.put("至尊宝", 169.5);
        map.put("牛魔王", 183.6);
        System.out.println(map);
        // map = {蜘蛛精=169.8, 牛魔王=183.6, 至尊宝=169.5, 紫霞=165.8}


		//遍历map集合,传递匿名内部类
        map.forEach(new BiConsumer<String, Double>() {
            @Override
            public void accept(String k, Double v) {
                System.out.println(k + "---->" + v);
            }
        });
		//遍历map集合,传递Lambda表达式
        map.forEach(( k,  v) -> {
            System.out.println(k + "---->" + v);
        });
    }
}

6.5 Map集合案例

 先分析需求,再考虑怎么用代码实现

1.首先可以将80个学生选择的景点放到一个集合中去(也就是说,集合中的元素是80个任意的ABCD元素)
2.准备一个Map集合用来存储景点,以及景点被选择的次数
3.遍历80个学生选择景点的集合,得到每一个景点,判断Map集合中是否包含该景点
    如果不包含,则存储"景点=1"
    如果包含,则存获取该景点原先的值,再存储"景点=原来的值+1"; 此时新值会覆盖旧值

public class MapDemo4 {
    public static void main(String[] args) {
        // 1、把80个学生选择的景点数据拿到程序中来。
        List<String> data = new ArrayList<>();
        String[] selects = {"A", "B", "C", "D"};
        Random r = new Random();
        for (int i = 1; i <= 80; i++) {
            // 每次模拟一个学生选择一个景点,存入到集合中去。
            int index = r.nextInt(4); // 0 1 2 3
            data.add(selects[index]);
        }
        System.out.println(data);

        // 2、开始统计每个景点的投票人数
        // 准备一个Map集合用于统计最终的结果
        Map<String, Integer> result = new HashMap<>();

        // 3、开始遍历80个景点数据
        for (String s : data) {
            // 问问Map集合中是否存在该景点
            if(result.containsKey(s)){
                // 说明这个景点之前统计过。其值+1. 存入到Map集合中去
                result.put(s, result.get(s) + 1);
            }else {
                // 说明这个景点是第一次统计,存入"景点=1"
                result.put(s, 1);
            }
        }
        System.out.println(result);
    }
}

6.6 HashMap

首先,我们学习HashMap集合的底层原理。前面我们学习过HashSet的底层原理,实际上HashMap底层原理和HashSet是一样的。为什么这么说呢?因为我们往HashSet集合中添加元素时,实际上是把元素添加到了HashMap集合中。

下面是Map集合的体系结构,HashMap集合的特点是由键决定的: 它的键是无序、不能重复,而且没有索引的。也是各种Map集合中也是用得最多的一种集合。

刚才我们说,HashSet底层就是HashMap,我们可以看源码验证这一点,我们可以看到,创建HashSet集合时,底层帮你创建了HashMap集合;往HashSet集合中添加元素时,底层却是调用了Map集合的put方法把元素作为了键来存储。所以实际上根本没有什么HashSet集合,把HashMap的集合的值忽略不看就是HashSet集合。

HashSet的原理我们之前已经学过了,所以HashMap是一样的,底层是哈希表结构。

HashMap底层数据结构: 哈希表结构
    JDK8之前的哈希表 = 数组+链表
    JDK8之后的哈希表 = 数组+链表+红黑树
    哈希表是一种增删改查数据,性能相对都较好的数据结构
    
往HashMap集合中键值对数据时,底层步骤如下
    第1步:当你第一次往HashMap集合中存储键值对时,底层会创建一个长度为16的数组
    第2步:然后将键和值封装成一个对象,叫做Entry对象
    第3步:再根据Entry对象的键计算hashCode值(和值无关)
    第4步:利用hashCode值和数组的长度做一个类似求余数的算法,会得到一个索引位置
    第5步:判断这个索引的位置是否为null,如果为null,就直接将这个Entry对象存储到这个索引位置
           如果不为null,则还需要进行第6步的判断
    第6步:继续调用equals方法判断两个对象键是否相同
          如果equals返回false,则以链表的形式往下挂
          如果equals方法true,则认为键重复,此时新的键值对会替换就的键值对。
    
HashMap底层需要注意这几点:
    1.底层数组默认长度为16,如果数组中有超过12个位置已经存储了元素,则会对数组进行扩容2倍
      数组扩容的加载因子是0.75,意思是:16*0.75=12     
       
    2.数组的同一个索引位置有多个元素、并且在8个元素以内(包括8),则以链表的形式存储
        JDK7版本:链表采用头插法(新元素往链表的头部添加)
        JDK8版本:链表采用尾插法(新元素往那个链表的尾部添加)
        
    3.数组的同一个索引位置有多个元素、并且超过了8个,则以红黑树形式存储

从HashMap底层存储键值对的过程中我们发现:决定键是否重复依赖与两个方法,一个是hashCode方法、一个是equals方法。由两个键计算得到的hashCode值相同,并且两个键使用equals比较为true,就认为键重复。

6.7 LinkedHashMap

LinkedHashMap集合的特点也是由键决定的:有序的、不重复、无索引。

LinkedHashMap的底层原理,和LinkedHashSet底层原理是一样的。底层由一个双向链表来维护键的存储顺序。

6.8 TreeMap

TreeMap集合的特点也是由键决定的,默认按照键的升序排列,键不重复,也是无索引的。

TreeMap集合的底层原理和TreeSet也是一样的,底层都是红黑树实现的。所以可以对键进行排序。

比如往TreeMap集合中存储Student对象作为键,排序方法有两种。

排序方式1:写一个Student类,让Student类实现Comparable接口

//第一步:先让Student类,实现Comparable接口
public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;
	//无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    
    //按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。
    /*
    原理:
    在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的
    结果是正数、负数、还是零,决定元素放在后面、前面还是不存。
    */
    @Override
    public int compareTo(Student o) {
        //this:表示将要添加进去的Student对象
        //o: 表示集合中已有的Student对象
        return this.age-o.age;
    }
}

排序方式2:在创建TreeMap集合时,直接传递Comparator比较器对象。

public class Test3TreeMap {
    public static void main(String[] args) {
        Map<Student, String> map = new TreeMap<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return Double.compare(o1.getHeight(), o2.getHeight());
            }
        });
//        Map<Student, String> map = new TreeMap<>(( o1,  o2) ->   Double.compare(o2.getHeight(), o1.getHeight()));
        map.put(new Student("蜘蛛精", 25, 168.5), "盘丝洞");
        map.put(new Student("蜘蛛精", 25, 168.5), "水帘洞");
        map.put(new Student("至尊宝", 23, 163.5), "水帘洞");
        map.put(new Student("牛魔王", 28, 183.5), "牛头山");
        System.out.println(map);
    }
}

6.9 集合嵌套

各位同学,到现在为止我们把Map集合和Collection集合的都已经学习完了。但是在实际开发中可能还会存在一种特殊的用法。就是把一个集合当做元素,存储到另一个集合中去,我们把这种用法称之为集合嵌套。

下面通过一个案例给大家演示一下

案例分析:

1.从需求中我们可以看到,有三个省份,每一个省份有多个城市
    我们可以用一个Map集合的键表示省份名称,而值表示省份有哪些城市
2.而又因为一个省份有多个城市,同一个省份的多个城市可以再用一个List集合来存储。
    所以Map集合的键是String类型,而值是List集合类型
    HashMap<String, List<String>> map = new HashMap<>();

代码如下:

public class Test {
    public static void main(String[] args) {
        // 1、定义一个Map集合存储全部的省份信息,和其对应的城市信息。
        Map<String, List<String>> map = new HashMap<>();

        List<String> cities1 = new ArrayList<>();
        Collections.addAll(cities1, "南京市","扬州市","苏州市" ,"无锡市","常州市");
        map.put("江苏省", cities1);

        List<String> cities2 = new ArrayList<>();
        Collections.addAll(cities2, "武汉市","孝感市","十堰市","宜昌市","鄂州市");
        map.put("湖北省", cities2);

        List<String> cities3 = new ArrayList<>();
        Collections.addAll(cities3, "石家庄市","唐山市", "邢台市", "保定市", "张家口市");
        map.put("河北省", cities3);
        System.out.println(map);//{江苏省=[南京市, 扬州市, 苏州市, 无锡市, 常州市], 湖北省=[武汉市, 孝感市, 十堰市, 宜昌市, 鄂州市], 河北省=[石家庄市, 唐山市, 邢台市, 保定市, 张家口市]}

        List<String> cities = map.get("湖北省");
        for (String city : cities) {
            System.out.println(city);
        }

        map.forEach((p, c) -> {
            System.out.println(p + "----->" + c);//江苏省----->[南京市, 扬州市, 苏州市, 无锡市, 常州市]
            //湖北省----->[武汉市, 孝感市, 十堰市, 宜昌市, 鄂州市]
            //河北省----->[石家庄市, 唐山市, 邢台市, 保定市, 张家口市]
        });
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/383219.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring AI - 使用向量数据库实现检索式AI对话

Spring AI - 使用向量数据库实现检索式AI对话 Spring AI 并不仅限于针对大语言模型对话API进行了统一封装&#xff0c;它还可以通过简单的方式实现LangChain的一些功能。本篇将带领读者实现一个简单的检索式AI对话接口。 一、需求背景 在一些场景下&#xff0c;我们想让AI根据…

【python】网络爬虫与信息提取--requests库

导学 当一个软件想获得数据&#xff0c;那么我们只有把网站当成api就可以 requests库:自动爬取HTML页面&#xff0c;自动网络请求提交 robots协议&#xff1a;网络爬虫排除标准&#xff08;网络爬虫的规则&#xff09; beautiful soup库&#xff1a;解析HTML页面 工具&…

【安装记录】安装 netperf 和 perf

这是一篇发疯随笔X.X 我的环境是虚拟机debian12&#xff0c;出于种种原因&#xff0c;之前直接使用apt-get install netperf apt-get install perf指令直接安装&#xff0c;报错找不到包 然后上网搜了一堆教程&#xff0c;有说下载netperf源码编译的&#xff0c;那些教程里面有…

SPSS双变量相关分析

双变量相关分析通过计算皮尔逊简单相关系数、斯皮尔曼等级相关系数、肯德尔等级相关系数及其显著性水平展开。其中皮尔逊简单相关系数是一种线性关联度量&#xff0c;适用于变量为定量连续变量且服从正态分布、相关关系为线性时的情形。如果变量不是正态分布的&#xff0c;或具…

Windows安全中心显示页面不可用

2024年2月过年当天重装电脑之后&#xff0c;第二天&#xff08;还是第三天&#xff09;安全中心开始提示如标题所示的问题。 问题环境 Windows 11 家庭中文版23H2安装日期2024/‎2/‎10 我解决之前没有截图&#xff0c;所以此处放一个别人的图做示例。 实际解决方式 搜索了…

假期刷题打卡--Day29

1、MT1224棋盘 求一个N*N棋盘中的方块总数。 格式 输入格式&#xff1a; 输入整型N 输出格式&#xff1a; 输出整型 样例 1 输入&#xff1a; 2输出&#xff1a; 5备注 考虑到取值范围&#xff0c;可用long整型定义变量 分析过程 这个题目的意思是&#xff0c;在这…

失去中国市场的三星仍是全球第一,但中国手机无法失去海外市场

随着2023年分析机构公布全球手机市场和中国手机市场的数据&#xff0c;业界终于看清中国市场早已没有以前那么重要&#xff0c;三星、苹果这些国际品牌对中国市场的依赖没有他们想象的那么严重&#xff0c;相反中国手机对海外市场比以往任何时候都要更依赖了。 三星在2023年被苹…

【Linux】模块参数

&#x1f525;博客主页&#xff1a;PannLZ &#x1f38b;系列专栏&#xff1a;《Linux系统之路》 &#x1f94a;不要让自己再留有遗憾&#xff0c;加油吧&#xff01; 模块参数 像用户程序一样&#xff0c;内核模块也可以接受命令行参数。首先应该声明用于保存命令行参数值的变…

Spring Native 解放 JVM

一、Spring Native 是什么 Spring Native可以通过GraalVM将Spring应用程序编译成原生镜像&#xff0c;提供了一种新的方式来部署Spring应用。与Java虚拟机相比&#xff0c;原生镜像可以在许多场景下降低工作负载&#xff0c;包括微服务&#xff0c;函数式服务&#xff0c;非常…

一、西瓜书——绪论

第一章 绪论 1.独立同分布 通常 假设 样本空间 中 全 体样 本 服 从 一 个 未 知 “ 分 布 ” ( d i s t r i b u t i o n ) D , 我们获得的每个样本都是独立地从这个分布上采样获得的&#xff0c; 即 “ 独 立同 分布 ” ( i n d e p e n d e n t a n d i d e n t ic a …

配置VMware实现从服务器到虚拟机的一键启动脚本

正文共&#xff1a;1666 字 15 图&#xff0c;预估阅读时间&#xff1a;2 分钟 首先祝大家新年快乐&#xff01;略备薄礼&#xff0c;18000个红包封面来讨个开年好彩头&#xff01; 虽然之前将服务器放到了公网&#xff08;成本增加了100块&#xff0c;内网服务器上公网解决方案…

二叉树和堆(优先队列)

前言&#xff1a; 本章会讲解二叉树及其一些相关练习题&#xff0c;和堆是什么。 二叉树&#xff1a; 二叉树的一些概念&#xff1a; 一棵二叉树是有限节点的集合&#xff0c;该集合可能为空。二叉树的特点是每一个节点最多有两个子树&#xff0c;即二叉树不存在度大于2的节点…

红包封面的流量是真的大啊

你好&#xff0c;我是小生&#xff0c;一个程序员转型做自媒体副业中~ 昨天&#xff0c;是农历 2023 年最后一天&#xff0c;到下午饭后的时候&#xff0c;红包封面流量推上了高峰期&#xff0c;非常的狂暴&#xff0c;有要烟花封面的、有要金龙封面的、有要表白封面的等等。 小…

在计算机/移动设备上恢复已删除视频的 10 个数据恢复工具

视频在网上疯传&#xff0c;我们都观看或创建视频&#xff0c;并将我们最喜欢的视频保存在硬盘上。如果我们丢失了一些重要的视频&#xff0c;那将是非常令人心碎的。但是今天&#xff0c;恢复已删除的视频变得更加容易。删除的视频在被新数据覆盖之前并没有真正从您的存储驱动…

单片机在物联网中的应用

单片机&#xff0c;这个小巧的电子设备&#xff0c;可能听起来有点技术性&#xff0c;但它实际上是物联网世界中的一个超级英雄。简单来说&#xff0c;单片机就像是各种智能设备的大脑&#xff0c;它能让设备“思考”和“行动”。由于其体积小、成本低、功耗低、易于编程等特点…

Web前端-移动web开发_rem布局

文章目录 移动web开发之rem布局1.0 rem基础1.1 rem单位(重点)1.2 em单位(了解)1.3 媒体查询什么是媒体查询媒体查询语法规范 1.4 less 基础维护css弊端Less 介绍Less安装Less 使用之变量使用node编译less的指令Less 编译 vocode Less 插件Less 嵌套Less 运算Less中的Mixin混入L…

使用 MinIO 超级充电 TileDB 引擎

MinIO 是一个强大的主要 TileDB 后端&#xff0c;因为两者都是为性能和规模而构建的。MinIO 是一个单一的 Go 二进制文件&#xff0c;可以在许多不同类型的云和本地环境中启动。它非常轻量级&#xff0c;但也具有复制和加密等功能&#xff0c;并且提供与各种应用程序的集成。Mi…

这MySQL错误日志异常也太猛了吧

作者&#xff1a;田逸&#xff08;formyz&#xff09; 一台核心业务数据库&#xff0c;版本为MySQL 8.34 社区服务器版。从上线以来&#xff0c;这个数据库服务器的错误日志增增加非常迅猛&#xff08;如下图所示&#xff09;&#xff0c;每24小时能增加到10多个G的容量。 因为…

前后端分离nodejs+vue动态网站的图书借阅管理系统35ih5

读者模块 1)注册&#xff1a;读者输入账号、密码、确认密码、姓名、手机、身份证、邮箱&#xff0c;点击注册按钮&#xff0c;完成注册。 2)登录&#xff1a;普通读者成功输入读者账号和密码&#xff0c;点击登录按钮。 3)读者主页面&#xff1a;读者登录成功后&#xff0c;选择…

C++重新入门-循环

目录 1.循环类型 while循环&#xff1a; for循环 基于范围的for循环(C11) do...while 循环 2.循环控制语句 3.无限循环 有的时候&#xff0c;可能需要多次执行同一块代码。一般情况下&#xff0c;语句是顺序执行的&#xff1a;函数中的第一个语句先执行&#xff0c;接着…