Java 常用集合类使用总结


Java 集合类有两种:单列集合和双列集合。

单列集合的顶层接口是 Collection ,JDK 不提供此接口的任何直接实现,它主要提供了 List 和 Set 两个更具体的子接口。
其中 List 接口的常用实现类为 ArrayList 和 LinkedList ,Set 的常用实现类为 HashSet 和 TreeSet 。

双列集合主要是 Map 接口,其常用实现类为 HashMap 和 TreeMap 。
下面我们就针对上面的常用集合类,快速介绍一下。


一、List 介绍

List 接口的常用实现类为 ArrayList 和 LinkedList ,其特点为:
ArrayList 底层是数组结构实现,查询快、增删慢。
LinkedList 底层是链表结构实现,查询慢、增删快。

下面我们主要以 ArrayList 为例进行代码演示:

import java.util.ArrayList;
import java.util.Iterator;

public class ArrayListDemo {
    public static void main(String[] args) {
        //List是有序集合,可以添加重复元素
        ArrayList list = new ArrayList<>();
        list.add("j");
        list.add("o");
        list.add("b");
        list.add("s");
        list.add("s");

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

        //增强 for 循环
        for (String str : list) {
            System.out.println(str);
        }

        //采用迭代器循环,删除集合中的每个元素
        Iterator iterator = list.iterator();
        String item;
        while (iterator.hasNext())
        {
            item = iterator.next();
            System.out.println(item);
            //通过迭代器,可以删除当前的元素
            iterator.remove();
        }

        //此时 List 为空列表,因为被迭代器删完了
        System.out.println(list);
    }
}

LinkedList 跟 ArrayList 的用法一样,这里就不介绍了,它比 ArrayList 多了一些特有的方法:
方法名 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(E e) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素


二、Set 介绍

这里是本篇博客的重点,我会详细介绍一下 Set 结合,以便简化后续的 Map 集合的介绍。

Set 的常用实现类为 HashSet 和 TreeSet ,需要注意的是:Set 不能存储重复的元素,不能使用普通的 for 循环遍历。
对于 Java 自带的数据类型(整数,字符串等)来说,HashSet 和 TreeSet 能够自动去重,不用我们实现去重规则。
对于 HashSet 和 TreeSet ,我会重点介绍 TreeSet ,最后再简要介绍一下 HashSet 。

我们先使用 TreeSet 存取字符串为例进行代码演示:

import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;

/**
 * Set集合的基本使用
 */
public class SetDemo {
    public static void main(String[] args) {
        //对于 Java 自带的数据类型(整数,字符串等),set 集合会自动去重
        TreeSet set = new TreeSet<>();
        set.add("jobs");
        set.add("monkey");
        set.add("alpha");
        set.add("wolfer");
        set.add("jobs"); //存储了一个重复的字符串

        //for (int i = 0; i < set.size(); i++) {
            //Set集合是没有索引的,所以不能使用普通的 for 循环进行遍历
        //}

        //采用迭代器遍历
        Iterator it = set.iterator();
        while (it.hasNext()){
            String s = it.next();
            System.out.println(s);
        }

        //采用增强 for 循环进行遍历
        for (String s : set) {
            System.out.println(s);
        }

        //此行代码打印的结果:[alpha, jobs, monkey, wolfer]
        System.out.println(set);

        /*
        通过上面打印出的结果可以发现两个重要现象:
        1 最终只打印了一次 jobs 字符串,
        这说明对于 Java 自带的数据类型(整数,字符串等),set 集合会自动去重

        2 打印出的内容,是按照字母的升序排序的,
        这说明 TreeSet 在存储元素后,会默认自动对集合内的元素按照升序排序。
        */
    }
}

从上面的代码中,我们需要特别注意 TreeSet 的一个重要特点:
TreeSet 在存储元素时,除了去重之外,还会 默认 对所存储的集合元素按照升序排序。
那么如果 TreeSet 中存储的是我们自定义的类对象,TreeSet 该如何进行去重排序处理呢?

答案就是:你必须自己实现 去重排序逻辑(至于升序,还是降序,由你自己定),否则 TreeSet 就会抛出异常,让你无法使用。
下面我们以两种方案来实现 TreeSet 存储我们自定义的类对象,并且自己实现去重排序逻辑。

(1)自定义类对象,需要实现 Comparable 接口的 compareTo 方法

我们以 Student 学生为例,假设案例场景为:TreeSet 存储 Student 对象,并且按照 age 年龄升序排序,然后打印出来。
我们自定义的 Student 类,必须实现 Comparable 接口的 compareTo 方法,然后才能实例化并添加到 TreeSet 中。

具体实现代码如下:

//我们自定义的 Student 学生类,需要实现 Comparable 接口的 compareTo 方法
public class Student implements Comparable{
    //字段:【姓名】和【年龄】
    private String name;
    private int age;

    //构造函数:采用 IDEA 自动生成【无参构造】和【全参构造】
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //对【姓名】和【年龄】字段,采用 IDEA 自动生成的 get 和 set 方法
    public String getName() { return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}

    //采用 IDEA 自动生成重写 toString 方法
    //方便打印【学生对象】,并进行查看
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //实现 Comparable 接口的 compareTo 方法,实现去重排序逻辑
    @Override
    public int compareTo(Student o) {
        //this 表示现在要存入的元素
        //o 表示已经存入到集合中的元素

        //按照学生的【年龄】进行升序排序(当然你也可以在这里采用降序)
        int result = this.age - o.age;
        //如果年龄相同,则按照【姓名】的字母升序排序(当然你也可以在这里采用降序)
        result = result == 0 ? this.name.compareTo(o.getName()) : result;

        //如果最终 result 等于 0 ,那就表示【姓名】和【年龄】都重复,也就是学生对象重复
        //此时 TreeSet 就不会进行存储,因为 Set 不允许出现重复的元素
        return result;
    }
}

public class TreeSetDemo {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet<>();

        Student s1 = new Student("jobs",28);
        Student s2 = new Student("alpha",27);
        Student s3 = new Student("monkey",29);
        Student s4 = new Student("wolfer",32);
        //故意添加一个重复的元素:【姓名】和【年龄】都相同的 student 对象
        Student s5 = new Student("jobs",28);

        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);

        for (Student stu : ts) {
            System.out.println(stu);
        }

        /*
        最后打印的结果为:
        Student{name='alpha', age=27}
        Student{name='jobs', age=28}
        Student{name='monkey', age=29}
        Student{name='wolfer', age=32}

        可以发现:重复的 jobs 学生对象,只打印了一条,并且按照年龄升序排列
        */
    }
}

(2)通过 TreeSet 构造函数,传入一个对象,该对象实现了 Comparator 接口中的 compare 方法

我们仍然以 Student 学生为例,将上面的案例场景再实现一遍,这样通过对比,就能够很容易理解。
这次的方案,Student 学生类对象不需要实现任何接口,只是作为一个实体类。
我们需要在 TreeSet 的构造函数中,传入一个对象,该对象实现了 Comparator 接口中的 compare 方法。

具体代码如下所示:

//我们自定义的 Student 学生类,纯粹就是一个实体类
public class Student {
    //字段:【姓名】和【年龄】
    private String name;
    private int age;

    //构造函数:采用 IDEA 自动生成【无参构造】和【全参构造】
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //对【姓名】和【年龄】字段,采用 IDEA 自动生成的 get 和 set 方法
    public String getName() { return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}

    //采用 IDEA 自动生成重写 toString 方法
    //方便打印【学生对象】,并进行查看
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TreeSetDemo {
    public static void main(String[] args) {
        //实例化 Comparator 接口的匿名对象,实现 compare 方法
        TreeSet ts = new TreeSet<>(new Comparator() {
            //实现 Comparator 接口的 compare 方法
            @Override
            public int compare(Student o1, Student o2) {
                //o1表示现在要存入的元素
                //o2表示已经存入到集合中的元素

                //按照学生的【年龄】进行升序排序(当然你也可以在这里采用降序)
                int result = o1.getAge() - o2.getAge();
                //如果年龄相同,则按照【姓名】的字母升序排序(当然你也可以在这里采用降序)
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;

                //如果最终 result 等于 0 ,那就表示【姓名】和【年龄】都重复,也就是学生对象重复
                //此时 TreeSet 就不会进行存储,因为 Set 不允许出现重复的元素
                return result;
            }
        });

        /*
        由于 Comparator 接口中只有一个 compare 方法需要我们实现
        接口中的其它方法,前面都有 default 关键字,有默认的代码实现
        因此我们可以简化上面的 TreeSet 实例化代码,采用 Lambda 表达式代替匿名对象实现方式

        TreeSet ts = new TreeSet<>((o1,o2) -> {
                //o1表示现在要存入的那个元素
                //o2表示已经存入到集合中的元素
                int result = o1.getAge() - o2.getAge();
                result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
                return result;
            }
        );
        */

        Student s1 = new Student("jobs",28);
        Student s2 = new Student("alpha",27);
        Student s3 = new Student("monkey",29);
        Student s4 = new Student("wolfer",32);
        //故意添加一个重复的元素:【姓名】和【年龄】都相同的 student 对象
        Student s5 = new Student("jobs",28);

        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);

        for (Student stu : ts) {
            System.out.println(stu);
        }
    }
}

(3)TreeSet 两种实现方案的比较

第一种方案:
通过自定义类对象,需要实现 Comparable 接口的 compareTo 方法

优点:
去重排序代码只需要在自定义对象类中写一次,后续很多地方使用 TreeSet 添加我们的自定义对象时,不需要再关注去重排序了。

缺点:
不够灵活,所有地方的代码,在使用 TreeSet 添加我们的自定义对象时,只能按照自定义类中的这一种规则去重排序。
第二种方案:
通过 TreeSet 构造函数,传入一个对象,该对象实现了 Comparator 接口中的 compare 方法

优点:
比较灵活,在使用 TreeSet 添加我们的自定义对象时,不同地方的代码,可以根据具体需要,自定义去重排序规则。

缺点:
比较繁琐,每次使用 TreeSet 添加我们的自定义对象时,都必须去实现去重排序规则。
最佳方案:
其实以上两种实现方案可以同时存在。优先使用第一种方案,如果第一种方案不能满足要求,再使用第二种方案。

还是拿上面的 Student 学生案例进行举例:
Student 类采用第一种方案实现 (假如是升序),这样任何地方如果需要用到 TreeSet 添加 Student 对象时,不需要关注去重排序规则。如果个别地方需要用到其它排序规则时(假如是降序),我们只需要采用第二种方案即可。虽然 Student 已经实现了第一种方案的接口方法,但是如果此时如果采用了第二种方案,TreeSet 构造函数中的去重排序规则的优先级,会高于自定义类中的去重规则,所以最终还是采用第二种方案的排序规则。因此两种方案可以共存,没有冲突。


(4)HashSet 存储自定义类对象

上面已经介绍过,对于 Java 自带的数据类型(整数,字符串等),set 集合会自动去重,HashSet 也不例外,这里就不演示了。
这里只重点介绍一下 HashSet 存储我们自定义的类对象时,我们需要如何实现去重。答案就是重写类中的 equals 方法和 hashCode 方法。这两种方法都可以使用 IDEA 自动生成,因此非常简单,我们还是以 Student 学生为例,进行代码演示:

//我们自定义的 Student 学生类,使用 IDEA 自动生成 equals 方法和 hashCode 方法
public class Student {
    //字段:【姓名】和【年龄】
    private String name;
    private int age;

    //构造函数:采用 IDEA 自动生成【无参构造】和【全参构造】
    public Student() {}
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //对【姓名】和【年龄】字段,采用 IDEA 自动生成的 get 和 set 方法
    public String getName() { return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
    public void setAge(int age) {this.age = age;}

    //采用 IDEA 自动生成重写 toString 方法
    //方便打印【学生对象】,并进行查看
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //采用 IDEA 自动生成重写 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;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    //采用 IDEA 自动生成重写 hashCode 方法
    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }
}

public class HashSetDemo {
    public static void main(String[] args) {
        HashSet hs = new HashSet<>();

        Student s1 = new Student("jobs",28);
        Student s2 = new Student("jobs",28);
        Student s3 = new Student("jobs",28);

        hs.add(s1);
        hs.add(s2);
        hs.add(s3);

        //由于 3 个 Student 对象的【姓名】和【年龄】相同,
        //因此 set 认为是相同的对象,不会重复添加
        //最终只会打印出一条 Student 数据
        for (Student student : hs) {
            System.out.println(student);
        }

        //打印结果为:Student{name='jobs', age=28}
    }
}


三、Map 介绍

Map 就是键值对的双列集合,其中键不能重复,值可以重复。
如果新增数据的键,与原有数据的键重复的话,就会修改该键对应的值。

Map 的常用实现类为 HashMap 和 TreeMap 。
对于 Java 自带的数据类型(整数,字符串等)作为键使用,Map 会自动判断是否重复。

我们还是以 TreeMap 为例,代码演示如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class MapDemo {
    public static void main(String[] args) {
        TreeMap tm = new TreeMap<>();
        tm.put("c","jobs");
        tm.put("d","monkey");
        tm.put("a","wolfer");
        tm.put("b","alpha");
        //故意添加一个重复的键,这样会导致该键对应的值被修改
        //最终 c 的值,原来为 jobs 被修改为 jobs888
        tm.put("c","jobs888");

        //遍历 map 集合中的 value 值
        Collection values = tm.values();
        for(String value : values) {
            System.out.println(value);
        }

        //第一种遍历 map 的方式
        Set keySet = tm.keySet();
        for (String key : keySet) {
            String value = tm.get(key);
            System.out.println(key + "," + value);
        }

        //第二种遍历 map 的方式
        Set> entrySet = tm.entrySet();
        for (Map.Entry me : entrySet) {
            String key = me.getKey();
            String value = me.getValue();
            System.out.println(key + "," + value);
        }
    }
}

对于 Map 集合来说,其键的集合,可以被认为是 Set 集合,有关 Set 集合,我们在上面已经详细介绍过了,因此 Map 这里就不多介绍了,直接上代码,如果你已经详细看过上面 Set 的介绍的话,下面的代码,自然可以能够看懂。

下面我们还是以 Student 学生为例,采用 Student 对象作为 Map 的键,分别采用 HashMap 和 TreeMap 进行代码演示:

//定义一个 Student 类
public class Student implements Comparable{
    private String name;
    private int age;

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

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

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

    //重写 toString 方法,方便打印查看 Student 对象内容
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //主要用来判断 Student 对象是否重复
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name);
    }

    //主要用来判断 Student 对象是否重复
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    //主要用来 TreeMap 的键排序
    @Override
    public int compareTo(Student o) {
        //按照年龄进行升序排序
        int result = this.getAge() - o.getAge();
        //年龄相同的情况下,按照姓名排序。
        result = result == 0 ? this.getName().compareTo(o.getName()) : result;
        //如果姓名和年龄都相同,Map 认为键重复,会修改该键的值
        return result;
    }
}

下面使用 Student 作为 Map 的键,进行代码演示:
import java.util.HashMap;
import java.util.Set;
import java.util.TreeMap;

public class MapDemo {
    public static void main(String[] args) {
        //HashMap 采用 Studnet 对象作为键的代码演示
        HashMap hm = new HashMap<>();
        Student s1 = new Student("jobs", 26);
        Student s2 = new Student("monkey", 28);
        Student s3 = new Student("wolfer", 30);
        Student s4 = new Student("alpha", 22);
        //故意实例化一个【姓名】和【年龄】重复的 Student 对象
        Student s5 = new Student("jobs", 26);

        hm.put(s1, "88分");
        hm.put(s2, "90分");
        hm.put(s3, "60分");
        hm.put(s4, "86分");
        //故意添加一个【姓名】和【年龄】重复的 Student 对象
        //这样对应键的值,就会被修改
        hm.put(s5, "100分");

        Set hmkeys = hm.keySet();
        for (Student key : hmkeys) {
            String value = hm.get(key);
            System.out.println(key + "----" + value);
        }

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

        //TreeMap 采用 Studnet 对象作为键的代码演示
        //TreeMap 键的去重排序方式,采用 Student 类中的年龄【升序】
        TreeMap tmasc = new TreeMap<>();
        tmasc.put(s1, "88分");
        tmasc.put(s2, "90分");
        tmasc.put(s3, "60分");
        tmasc.put(s4, "86分");
        //故意添加一个【姓名】和【年龄】重复的 Student 对象
        //这样对应键的值,就会被修改
        tmasc.put(s5, "100分");

        //打印出的结果,按照 Student 的年龄【升序】排列
        Set tmasckeys = tmasc.keySet();
        for (Student key : tmasckeys) {
            String value = tmasc.get(key);
            System.out.println(key + "----" + value);
        }

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

        //TreeMap 采用 Studnet 对象作为键的代码演示
        //TreeMap 键的去重排序方式,采用自定义方法,按照 Student 年龄【倒序】
        TreeMap tmdesc = new TreeMap<>((o1, o2) -> {
            int result = o2.getAge() - o1.getAge();
            result = result == 0 ? o2.getName().compareTo(o1.getName()) : result;
            return result;
        });
        tmdesc.put(s1, "88分");
        tmdesc.put(s2, "90分");
        tmdesc.put(s3, "60分");
        tmdesc.put(s4, "86分");
        //故意添加一个【姓名】和【年龄】重复的 Student 对象
        //这样对应键的值,就会被修改
        tmdesc.put(s5, "100分");

        //打印出的结果,按照 Student 的年龄【倒序】排列
        Set tmdesckeys = tmdesc.keySet();
        for (Student key : tmdesckeys) {
            String value = tmdesc.get(key);
            System.out.println(key + "----" + value);
        }
    }
}


到此为止,Java 常用集合类已经介绍完毕,希望对大家有帮助。上面的代码演示,都经过测试无误,比较具有实战意义。如果你能够轻松看懂,说明你对 Java 常用集合类已经掌握的炉火纯青了。希望大家能够轻松看懂上面的示例代码。



相关