【Java基础】List、Set、Map测试总结

409 阅读8分钟

经过一番描述性的介绍,今天进入List、Set、Map的最后一个环节了,先上思维导图

话题一:Java集合框架是什么?有什么优点?

每种编程语言中都有集合,最初Java只包含了Vector、Stack、HashTable和Array这几种集合。随着集合的广泛使用,从Java1.2开始才提出了囊括所有集合接口、实现和算法的集合框架。

使用集合框架的优点如下:

  1. 使用核心集合类降低开发成本,而非实现我们自己的集合类。
  2. 随着使用经过严格测试的集合框架类,代码质量会得到提高。
  3. 通过使用JDK附带的集合类,可以降低代码维护成本
  4. 复用性和可操作性。

话题二:集合框架中的泛型有什么特点?

  1. Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。
  2. 泛型允许我们为集合提供一个可容纳的对象类型,因此,如果你添加其他类型的任何元素,它会在编译时报错
  3. 这避免了在运行时出现ClassCastException
  4. 泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符
  5. 它给运行时带来好处,因为不会产生类型检查的字节码指令。

话题三:面试中问List、Set、Map的关系应该怎么说?

  1. 首先他们都是Java集合框架中的一部分,其次是这三个都是接口
  2. List和Set继承自Collection接口,Map没有
  3. List实现类和Set实现类最大的区别是元素是否可以重复,List实现类一定是有序的集合,Set实现类有无序的也有有序的。
  4. 这里的有序和无序是相对于存取顺序来定义的,存入顺序等于取出顺序就认为是有序的,而无序的Set实现类其内部也是有自己定义的顺序,比如HashSet
  5. List主要有ArrayList、Vector和LinkedList三个常用实现类,区别在于
    1. ArrayList和Vector是基于数组实现的,LinkedList是基于双向链表实现的
    2. 只有Vector实现了线程安全
    3. 数组和双向链表的数据结构不同导致性能不同(后面会有例子)
  6. Set主要有HashSet、TreeSet、LinkedHashSet三个常用实现类,区别在于
    1. HashSet基于哈希表,TreeSet基于二叉树,LinkedHashSet继承自HashSet,是在哈希表的基础上又引入了双向链表
    2. 都没有实现线程安全
    3. 三种数据结构也会导致性能不同
  7. List的实现类都实现了List接口并继承自AbstractList抽象类,Set的实现类都实现了Set接口并继承自AbstractSet抽象类,所以他们各自有一套自己的同名CRUD操作
  8. Map接口没有继承自Collection接口是因为没有意义,接口的本质是一套代码规范,Map的本质是键值对,而Collection的本质是一组对象。
  9. Map主要有HashMap、TreeMap和LinkedHashMap三个常用实现类,区别在于
    1. HashMap基于哈希表,TreeMap基于二叉树,LinkedHashMap继承自HashMap,是在哈希表的基础上又引入了双向链表
    2. 都没有实现线程安全
    3. 三种数据结构也会导致性能不同
  10. Set的三个实现类和Map的三个实现类有差不多的性质和区别,是因为HashSet、TreeSet、LinkedHashSet的底层分别是HashMap、TreeMap、LinkedHashMap。

实测:性能测试

这里我们分别对同接口下的实现类进行单线程的性能测试

ArrayList、Vector、LinkedList

public class ListDemo {


    public static void main(String[] args) {
        // 一次只测试一个实现类
        List<Integer> list = new ArrayList<>();
        //List<Integer> list = new Vector<>();
        //List<Integer> list = new LinkedList<>();


        test(list);

    }

    /**
     * 入口
     *
     * @param list
     */
    public static void test(List<Integer> list) {
        testAddTime(list, 1000000);
        testIndexUpTime(list);
        testForEachTime(list);
        testIteratorTime(list);
        testIndexDownTime(list);
        testReadRandomTime(list);
        testRandomDelTime(list);
        testRandomAddTime(list);
        testRandomSetTime(list);
    }

    /**
     * 顺序添加测试
     *
     * @param list
     */
    public static void testAddTime(List<Integer> list, int size) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 迭代器遍历测试
     *
     * @param list
     */
    public static void testIteratorTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        Iterator<Integer> it = list.iterator();
        while (it.hasNext()) {
            Integer integer = it.next();
            // 空遍历
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "迭代器遍历耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * ForEach遍历测试
     *
     * @param list
     */
    public static void testForEachTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        for (Integer i : list) {
            // 空遍历
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "ForEach遍历耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 索引递增遍历测试
     *
     * @param list
     */
    public static void testIndexUpTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            // 空循环
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "递增索引遍历耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 索引递减遍历测试
     *
     * @param list
     */
    public static void testIndexDownTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        for (int i = list.size() - 1; i > 0; i--) {
            // 空循环
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "递减索引遍历耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 随机读取测试
     *
     * @param list
     */
    public static void testReadRandomTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        Random random = new Random();
        Integer integer = 0;
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(list.size());
            integer = list.get(randomInt);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "随机读取一万次耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 随机删除测试
     *
     * @param list
     */
    public static void testRandomDelTime(List<Integer> list) {
        long startTime = System.currentTimeMillis();
        Random random = new Random();
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(list.size());
            list.remove(randomInt);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "随机删除一万个元素耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 随机插入对象
     * @param list
     */
    public static void testRandomAddTime(List<Integer> list){
        long startTime = System.currentTimeMillis();
        Random random = new Random();
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(list.size());
            list.add(randomInt,randomInt);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "随机插入一万个元素耗时: " + (endTime - startTime) + "ms");
    }

    /**
     * 随机更改对象
     * @param list
     */
    public static void testRandomSetTime(List<Integer> list){
        long startTime = System.currentTimeMillis();
        Random random = new Random();
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(list.size());
            list.set(randomInt,randomInt);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(list.getClass() + "随机更改一万个元素耗时: " + (endTime - startTime) + "ms");
    }

}

基于单一变量的测试方法,即我每次只改变一个变量的情况下(环境相同、代码相同、只有调用的实现类不同),我得到了下面三个结果

ArrayList

Vector

LinkedList

说实话,看到这个结果我有点惊讶,因为在这次正式测试之前,我前面的几篇文章的结论都是基于理论来考虑的,但这个测试结果出来后,LinkedList的性能在ArrayList面前居然毫无优势。而且这还是在我把数组集合的样本数调到了100万,一开始我直接上500万样本数的时候,在测试到LinkedList的随机操作的时候,我一度以为我的电脑卡了,半天没有出一个结果,连续测了3遍,直到我把样本数调到了100万,才看到随机操作的结果,之后在100万样本的情况下我又连续测试了5遍,得到了这个ArrayList完胜LinkedList的结果。

HashSet、TreeSet、LinkedHashSet

public class SetDemo {


    public static void main(String[] args) {
        // 随机类
        Random random = new Random();
        // 100万样本数
        int size = 1000000;
        // 一次只测试一个实现类
        Set<Integer> set = new HashSet<>();
        //Set<Integer> set = new TreeSet<>();
        //Set<Integer> set = new LinkedHashSet<>();

        // 顺序插入
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            set.add(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(set.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");

        // 迭代器遍历
        startTime = System.currentTimeMillis();
        Iterator<Integer> it = set.iterator();
        while (it.hasNext()) {
            // 空遍历
            Integer integer = it.next();
        }
        endTime = System.currentTimeMillis();
        System.out.println(set.getClass() + "迭代器遍历共耗时: " + (endTime - startTime) + "ms");

        // ForEach遍历
        startTime = System.currentTimeMillis();
        for (Integer i : set) {
            //空循环
        }
        endTime = System.currentTimeMillis();
        System.out.println(set.getClass() + "ForEach遍历共耗时: " + (endTime - startTime) + "ms");

        // 随机更新(set不重复,插入小的就是更新)
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(set.size());
            set.add(randomInt);
        }
        endTime = System.currentTimeMillis();
        System.out.println(set.getClass() + "随机更新10000个元素共耗时: " + (endTime - startTime) + "ms");

        // 随机删除
        startTime = System.currentTimeMillis();
        Integer integer = 0;
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(set.size());
            set.remove(randomInt);
        }
        endTime = System.currentTimeMillis();
        System.out.println(set.getClass() + "随机删除10000个元素共耗时: " + (endTime - startTime) + "ms");

    }


}

HashSet

TreeSet

LinkedHashSet

结果还算符合之前几篇文章的推断,HashSet插入速度最快,LinkedHashSet的查找速度最快,而TreeSet是一种适中的选择。

HashMap、TreeMap、LinkedHashMap

public class MapDemo {


    public static void main(String[] args) {
        // 随机类
        Random random = new Random();
        String emptyString = "";
        // 100万样本数
        int size = 1000000;
        // 一次只测试一个实现类
        Map<String, String> map = new HashMap<>();
        //Map<String, String> map = new TreeMap<>();
        //Map<String, String> map = new LinkedHashMap<>();

        // 顺序插入
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            map.put(i + emptyString, i + emptyString);
        }
        long endTime = System.currentTimeMillis();
        System.out.println(map.getClass() + "插入" + size + "个元素共耗时: " + (endTime - startTime) + "ms");

        // 随机获取
        startTime = System.currentTimeMillis();
        String valueStr = "";
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(map.size());
            valueStr = map.get(randomInt + emptyString);
        }
        endTime = System.currentTimeMillis();
        System.out.println(map.getClass() + "随机读取一万次耗时: " + (endTime - startTime) + "ms");

        // 随机删除
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            int randomInt = random.nextInt(map.size());
            map.remove(randomInt + emptyString);
        }
        endTime = System.currentTimeMillis();
        System.out.println(map.getClass() + "随机删除一万个耗时: " + (endTime - startTime) + "ms");

        // 转化keySet
        startTime = System.currentTimeMillis();
        Set<String> stringSet = map.keySet();
        endTime = System.currentTimeMillis();
        System.out.println(map.getClass() + "转化keySet耗时: " + (endTime - startTime) + "ms");

        // 转化values
        startTime = System.currentTimeMillis();
        Collection<String> values = map.values();
        endTime = System.currentTimeMillis();
        System.out.println(map.getClass() + "转化values耗时: " + (endTime - startTime) + "ms");


    }
}

HashMap

TreeMap

LinkedHashMap

似乎除了插入的效率TreeMap要略低一点,其他的都没什么太大的区别

小结

到这一步Java容器入门差不多就写完了,测试的代码其实还能再完善,比如加上内存量的查看,可以判断出不同的容器对资源消耗的情况,然后样本量也可以增大到300万或者500万,但我觉得其实没太有必要,我想应该没人会一次性取这么多对象放在一个容器里面,所以100万的样本量我觉得其实足够了,然后还有复杂对象作为容器元素也可以测试一下。这里我用的单一变量的原则来测试,所以不能在一篇文章里全部测完。

测试代码如下:github.com/MagicH666/J…