java基础—Java集合

1、Java集合概述

  在编程时,常常需要集中存放多个数据,可以使用数组来保存多个对象,但数组长度不可变 化,一旦在初始化数组时指定了数组长度,这个数组长度就是不可变 的,如果需要保存数量变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据, 如成绩表:语文—79, 数学—80, 这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。
  为了保存数量不确定的数据, 以及保存具有映射关系的数据(也被称为关联数组), Java提供了集合类。 集合类主要负责保存、盛装 其他数据, 因此集合类也被称为容器类。 所有的集合类都位于 java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5 还在java.util.concurrent包下提供了一些多线程支持的集合类。
  集合类和数组不一样, 数组元素既可以是基本类型的值, 也可以是对象(实际上保存的是对象的引用变量);而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
  Java的集合类主要由两个接口派生而出:Collection和Map, Collection和Map是Java集合框架的根接口,这两个接口又包含了一些 子接口或实现类。
  下图所示是Collection接口、子接口及其实现类的继承树。
在这里插入图片描述
  下图所示是Map体系的继承树,所有的Map实现类用于保存具有 映射关系的数据(也就是前面介绍的关联数组)。
在这里插入图片描述
  Map保存的每项数据都是 key-value对,也就是由key和value两个值组成。就像前面介绍的成绩 单:语文—79, 数学—80, 每项成绩都由两个值组成, 即科目名和成 绩。对于一张成绩表而言,科目通常不会重复,而成绩是可重复的, 通常习惯根据科目来查阅成绩,而不会根据成绩来查阅科目。Map与此类似,Map里的key是不可重复的,key用于标识集合里的每项数据,如 果需要查阅Map中的数据时,总是根据Map的key来获取。
  可以把Java所有集合分 成三大类, 其中Set集合类似于一个罐子, 把一个对象添加到Set集合 时, Set集合无法记住添加这个元素的顺序, 所以Set里的元素不能重 复(否则系统无法准确识别这个元素);List集合非常像一个数组, 它可以记住每次添加元素的顺序、且List的长度可变。Map集合也像一 个罐子,只是它里面的每项数据都由两个值组成。示意图如下所示。
在这里插入图片描述
  从上图中可以看出,如果访问List集合中的元素,可以直接根据 元素的索引来访问;如果访问Map集合中的元素,可以根据每项元素的 key来访问其value;如果访问Set集合中的元素,则只能根据元素本身 来访问(这也是Set集合里元素不允许重复的原因)。

1.1、Collection接口

  Collection接口是List、Set和Queue接口的父接口, 该接口里定 义的方法既可用于操作Set集合, 也可用于操作List和Queue集合。 Collection接口里定义了如下操作集合元素的常用方法。
  ➢ boolean add(Object o):该方法用于向集合里添加一个元 素。如果集合对象被添加操作改变了,则返回true。
  ➢ boolean addAll(Collection c):该方法把集合c里的所有元 素添加到指定集合里。 如果集合对象被添加操作改变了, 则返 回true。
  ➢ void clear():清除集合里的所有元素,将集合长度变为0。
  ➢ boolean contains(Object o):返回集合里是否包含指定元 素。
  ➢ boolean containsAll(Collection c):返回集合里是否包含 集合c里的所有元素。
  ➢ boolean isEmpty():返回集合是否为空。当集合长度为0时返 回true,否则返回false。
  ➢ Iterator iterator():返回一个Iterator对象, 用于遍历集 合里的元素。
  ➢ boolean remove(Object o):删除集合中的指定元素o, 当集 合中包含了一个或多个元素o时,该方法只删除第一个符合条件 的元素,该方法将返回true。
  ➢ boolean removeAll(Collection c):从集合中删除集合c里包 含的所有元素(相当于用调用该方法的集合减集合c),如果删 除了一个或一个以上的元素,则该方法返回true。
  ➢ boolean retainAll(Collection c):从集合中删除集合c里不 包含的元素(相当于把调用该方法的集合变成该集合和集合c的 交集), 如果该操作改变了调用该方法的集合, 则该方法返回 true。
  ➢ int size():该方法返回集合里元素的个数。
  ➢ Object[] toArray():该方法把集合转换成一个数组,所有的 集合元素变成对应的数组元素。
  下面程序示范了如何通过上面方法来操作Collection集合里的元素。

public class CollectionTest {
    public static void main(String[] args) {
        Collection<Object> c = new ArrayList<>();
        // 添加元素
        c.add("孙悟空");
        // 虽然集合里不能放基本类型的值,但Java支持自动装箱
        c.add(6);
        System.out.println("c集合的元素个数为:" + c.size()); // 输出2
        // 删除指定元素
        c.remove(6);
        System.out.println("c集合的元素个数为:" + c.size()); // 输出1
        // 判断是否包含指定字符串
        System.out.println("c集合的是否包含\"孙悟空\"字符串:" + c.contains("孙悟空")); // 输出true
        c.add("Java EE实战");
        System.out.println("c集合的元素:" + c);

        Collection<String> books = new HashSet<>();
        books.add("Java EE实战");
        books.add("Spring实战");
        System.out.println("c集合是否完全包含books集合?" + c.containsAll(books)); // 输出false
        // 用c集合减去books集合里的元素
        c.removeAll(books);
        System.out.println("c集合的元素:" + c);
        // 删除c集合里所有元素
        c.clear();
        System.out.println("c集合的元素:" + c);
        // 控制books集合里只剩下c集合里也包含的元素
        books.retainAll(c);
        System.out.println("books集合的元素:" + books);

        // 该Collection使用了泛型,指定它的集合元素都是String
        Collection<String> strColl = List.of("Java", "Kotlin", "Swift", "Python");
        // toArray()方法参数是一个Lambda表达式,代表IntFunction对象
        // 此时toArray方法的返回值类型是String[],而不是Object[]
        String[] sa = strColl.toArray(String[]::new);
        System.out.println(Arrays.toString(sa));
    }
}

  上面程序中创建了两个Collection对象, 一个是c集合, 一个是 books集合,其中c集合是ArrayList,而books集合是HashSet。虽然它 们使用的实现类不同, 但当把它们当成Collection来使用时, 使用 add、remove、clear等方法来操作集合元素时没有任何区别。

1.2、遍历集合

  当使用System.out的println()方法来输出集合对象时, 将输出 [ele1,ele2,…]的形式,这显然是因为所有的Collection实现类都 重写了toString()方法, 该方法可以一次性地输出集合中的所有元 素。
  如果想依次访问集合里的每一个元素, 则需要使用某种方式来遍历集合元素,下面介绍遍历集合元素的几种方法。

1.2.1、使用Lambda表达式遍历集合

  Java 8为Iterable接口新增了一个forEach(Consumer action)默认方法, 该方法所需参数的类型是一个函数式接口, 而Iterable接口 是Collection接口的父接口, 因此Collection集合也可直接调用该方法。
  当程序调用Iterable的forEach(Consumer action)遍历集合元素 时,程序会依次将集合元素传给Consumer的accept(T t)方法(该接口 中唯一的抽象方法)。 正因为Consumer是函数式接口, 因此可以使用 Lambda表达式来遍历集合元素。
  如下程序示范了使用Lambda表达式来遍历集合元素。

public class CollectionEach {
    public static void main(String[] args) {
        // 创建一个集合
        Set<String> books = new HashSet<>();
        books.add("Java EE企业应用实战");
        books.add("Java 实战");
        books.add("Android实战");
        // 调用forEach()方法遍历集合
        books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
    }
}
1.2.2、使用Iterator遍历集合元素

  Iterator接口也是Java集合框架的成员, 但它与Collection系 列、Map系列的集合不一样:Collection系列集合、Map系列集合主要 用于盛装其他对象, 而Iterator则主要用于遍历(即迭代访问) Collection集合中的元素,Iterator对象也被称为迭代器。 Iterator接口隐藏了各种Collection实现类的底层细节, 向应用程序提供了遍历Collection集合元素的统一编程接口。 Iterator接口 里定义了如下4个方法。
  ➢ boolean hasNext():如果被迭代的集合元素还没有被遍历 完,则返回true。   ➢ Object next():返回集合里的下一个元素。
  ➢ void remove():删除集合里上一次next方法返回的元素。
  ➢ void forEachRemaining(Consumer action) , 这 是 Java 8 为 Iterator新增的默认方法, 该方法可使用Lambda表达式来遍历 集合元素。
  下面程序示范了通过Iterator接口来遍历集合元素。

public class IteratorTest {
    public static void main(String[] args) {
        // 创建集合、添加元素的代码与前一个程序相同
        Set<String> books = new HashSet<>();
        books.add("Java EE企业应用实战");
        books.add("Java 实战");
        books.add("Android实战");
        // 获取books集合对应的迭代器
        Iterator<String> iterator = books.iterator();
        while (iterator.hasNext()) {
            // it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
            String book = (String) iterator.next();
            System.out.println(book);
            if (book.equals("疯狂Java讲义")) {
                // 从集合中删除上一次next方法返回的元素
                iterator.remove();
            }
            // 对book变量赋值,不会改变集合元素本身
            book = "测试字符串";   // ①
        }
        System.out.println(books);
    }
}

  从上面代码中可以看出, Iterator仅用于遍历集合, Iterator本 身并不提供盛装对象的能力。 如果需要创建Iterator对象, 则必须有 一个被迭代的集合。 没有集合的Iterator仿佛无本之木, 没有存在的价值。
  上面程序中①号代码对迭代变量book进行赋值, 但当再次输出books集合时,会看到集合里的元素没有任何改变。这就可以得到 一个结论:当使用Iterator对集合元素进行迭代时, Iterator并不是 把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变 量,所以修改迭代变量的值对集合元素本身没有任何影响。
  当使用Iterator迭代访问Collection集合元素时, Collection集 合里的元素不能被改变, 只有通过Iterator的remove()方法删除上一 次 next() 方 法 返 回 的 集 合 元 素 才 可 以 ; 否 则 将 会 引 发 java.util.Concurrent ModificationException异常。下面程序示范 了这一点。

public class IteratorErrorTest {
    public static void main(String[] args) {
        // 创建集合、添加元素的代码与前一个程序相同
        Set<String> books = new HashSet<>();
        books.add("Java EE企业应用实战");
        books.add("Java 实战");
        books.add("Android实战");
        // 获取books集合对应的迭代器
        Iterator<String> iterator = books.iterator();
        while (iterator.hasNext()) {
            String book = (String) iterator.next();
            System.out.println(book);
            if (book.equals("Android实战")) {
                // 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常
                books.remove(book);
            }
        }
    }
}

  Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭 代过程中检测到该集合已经被修改(通常是程序中的其他线程修 改),程序立即引ConcurrentModificationException异常,而不是 显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

1.2.3、使用Lambda表达式遍历Iterator

  Java 8 为 Iterator 新 增 了 一 个 forEachRemaining(Consumer action)方法,该方法所需的Consumer参数同样也是函数式接口。当程序调用IteratorforEachRemaining(Consumer action)遍历集合元素时,程序会依次将集合元素传Consumer的accept(T t)方法(该接口 中唯一的抽象方法)。
  如下程序示范了使用Lambda表达式来遍历集合元素。

public class IteratorEach {
    public static void main(String[] args) {
        // 创建集合、添加元素的代码与前一个程序相同
        Set<String> books = new HashSet<>();
        books.add("Java EE企业应用实战");
        books.add("Java 实战");
        books.add("Android实战");
        // 获取books集合对应的迭代器
        Iterator<String> it = books.iterator();
        // 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
        it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
    }
}
1.2.4、使用foreach循环遍历集合元素

  除可使用Iterator接口迭代访问Collection集合里的元素之外, 使用Java 5提供的foreach循环迭代访问集合元素更加便捷。如下程序 示范了使用foreach循环来迭代访问集合元素。

public class ForeachTest {
    public static void main(String[] args) {
        // 创建集合、添加元素的代码与前一个程序相同
        Set<String> books = new HashSet<>();
        books.add("Java EE企业应用实战");
        books.add("Java 实战");
        books.add("Android实战");
        for (String obj : books) {
            // 此处的book变量也不是集合元素本身
            String book = (String) obj;
            System.out.println(book);
            if (book.equals("Android实战")) {
                // 下面代码会引发ConcurrentModificationException异常
                books.remove(book);     // ①
            }
        }
        System.out.println(books);
    }
}

  上面代码使用foreach循环来迭代访问Collection集合里的元素更 加简洁, 这正是JDK 1.5的foreach循环带来的优势。 与使用Iterator 接口迭代访问集合元素类似的是,foreach循环中的迭代变量也不是集 合元素本身, 系统只是依次把集合元素的值赋给迭代变量, 因此在 foreach循环中修改迭代变量的值也没有任何实际意义。
  同样,当使用foreach循环迭代访问集合元素时,该集合也不能被 改变, 否则将引发Concurrent ModificationException异常。 所以上 面程序中①行代码处将引发该异常。

2、Set集合

  前面已经介绍过Set集合,它类似于一个罐子,程序可以依次把多 个对象“丢进”Set集合, 而Set集合通常不能记住元素的添加顺序。 Set集合与Collection基本相同, 没有提供任何额外的方法。 实际上 Set就是Collection, 只是行为略有不同(Set不允许包含重复元 素)。
  Set集合不允许包含相同的元素,如果试图把两个相同的元素加入 同一个Set集合中,则添加操作失败,add()方法返回false,且新元素 不会被加入。
  上面介绍的是Set集合的通用知识, 因此完全适合后面介绍的 HashSet、TreeSet和EnumSet三个实现类, 只是三个实现类还各有特色。

2.1、HashSet类

  HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使 用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很 好的存取和查找性能。 HashSet具有以下特点。
  ➢ 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也 有可能发生变化。
  ➢ HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须 通过代码来保证其同步。
  ➢ 集合元素值可以是null。
  当向HashSet集合中存入一个元素时, HashSet会调用该对象的 hashCode()方法来得到该对象的hashCode值, 然后根据该hashCode值 决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方 法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将 会把它们存储在不同的位置,依然可以添加成功。 也就是说,HashSet集合判断两个元素相等的标准是两个对象通过 equals()方法比较相等, 并且两个对象的hashCode()方法返回值也相 等。
  下面程序分别提供了三个类A、B和C,它们分别重写了equals()、 hashCode()两个方法的一个或全部, 通过此程序可以让读者看到 HashSet判断集合元素相同的标准。

// 类A的equals方法总是返回true,但没有重写其hashCode()方法
class A {
    public boolean equals(Object obj) {
        return true;
    }
}

// 类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B {
    public int hashCode() {
        return 1;
    }
}

// 类C的hashCode()方法总是返回2,且重写其equals()方法总是返回true
class C {
    public int hashCode() {
        return 2;
    }

    public boolean equals(Object obj) {
        return true;
    }
}

public class HashSetTest {
    public static void main(String[] args) {
        Set<Object> books = new HashSet<>();
        // 分别向books集合中添加两个A对象,两个B对象,两个C对象
        books.add(new A());
        books.add(new A());
        books.add(new B());
        books.add(new B());
        books.add(new C());
        books.add(new C());
        System.out.println(books);
    }
}

  上面程序中向books集合中分别添加了两个A对象、两个B对象和两 个C对象, 其中C类重写了equals()方法总是返回true, hashCode()方 法总是返回2,这将导致HashSet把两个C对象当成同一个对象。运行上 面程序,看到如下运行结果:
在这里插入图片描述
  从上面程序可以看出,即使两个A对象通过equals()方法比较返回 true , 但 HashSet 依 然 把 它 们 当 成 两 个 对 象 ; 即 使 两 个 B 对 象 的 hashCode()返回相同值(都是1), 但HashSet依然把它们当成两个对 象。
  可见:当把一个对象放入HashSet中时,如果需要重写该对象对应 类的equals()方法, 则也应该重写其hashCode()方法。 规则是:如果 两个对象通过equals()方法比较返回true, 这两个对象的hashCode值 也应该相同。
  如果两个对象通过equals()方法比较返回true, 但这两个对象的 hashCode()方法返回不同的hashCode值时,这将导致HashSet会把这两 个对象保存在Hash表的不同位置, 从而使两个对象都可以添加成功, 这就与Set集合的规则冲突了。
  如果两个对象的hashCode()方法返回的hashCode值相同, 但它们 通 过 equals() 方 法 比 较 返 回 false 时 将 更 麻 烦 : 因 为 两 个 对 象 的 hashCode值相同,HashSet将试图把它们保存在同一个位置,但又不行 (否则将只剩下一个对象),所以实际上会在这个位置用链式结构来 保存多个对象;而HashSet访问集合元素时也是根据元素的hashCode值 来快速定位的, 如果HashSet中两个以上的元素具有相同的hashCode 值,将会导致性能下降。
  如果向HashSet中添加一个可变对象后,后面程序修改了该可变对 象的实例变量,则可能导致它与集合中的其他元素相同(即两个对象 通过equals()方法比较返回true, 两个对象的hashCode值也相等), 这就有可能导致HashSet中包含两个相同的对象。下面程序演示了这种情况。

class R {
    int count;

    public R(int count) {
        this.count = count;
    }

    public String toString() {
        return "R[count:" + count + "]";
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == R.class) {
            var r = (R) obj;
            return this.count == r.count;
        }
        return false;
    }

    public int hashCode() {
        return this.count;
    }
}

public class HashSetTest2 {
    public static void main(String[] args) {
        Set<Object> hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));
        // 打印HashSet集合,集合元素没有重复
        System.out.println(hs);
        // 取出第一个元素
        Iterator<Object> it = hs.iterator();
        R first = (R) it.next();
        // 为第一个元素的count实例变量赋值
        first.count = -3;     // ①
        // 再次输出HashSet集合,集合元素有重复元素
        System.out.println(hs);
        // 删除count为-3的R对象
        hs.remove(new R(-3));    // ②
        // 可以看到被删除了一个R元素
        System.out.println(hs);
        System.out.println("hs是否包含count为-3的R对象?" + hs.contains(new R(-3))); // 输出false
        System.out.println("hs是否包含count为-2的R对象?" + hs.contains(new R(-2))); // 输出false
    }
}

2.2、TreeSet类

  TreeSet是SortedSet接口的实现类, 正如SortedSet名字所暗示 的, TreeSet可以确保集合元素处于排序状态。 与HashSet集合相比, TreeSet还提供了如下几个额外的方法。
  ➢ Comparator comparator():如果TreeSet采用了定制排序,则 该方法返回定制排序所使用的Comparator;如果TreeSet采用了 自然排序,则返回null。
  ➢ Object first():返回集合中的第一个元素。
  ➢ Object last():返回集合中的最后一个元素。
  ➢ Object lower(Object e):返回集合中位于指定元素之前的元 素(即小于指定元素的最大元素,参考元素不需要是TreeSet集 合里的元素)。
  ➢ Object higher(Object e):返回集合中位于指定元素之后的 元素(即大于指定元素的最小元素,参考元素不需要是TreeSet 集合里的元素)。
  ➢ SortedSet subSet(Object fromElement, Object toElement):返回此Set的子集合, 范围从fromElement(包 含)到toElement(不包含)。
  ➢ SortedSet headSet(Object toElement):返回此Set的子集, 由小于toElement的元素组成。
  ➢ SortedSet tailSet(Object fromElement):返回此Set的子 集,由大于或等于fromElement的元素组成。
  下面程序测试了TreeSet的通用用法。

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet<Integer> nums = new TreeSet<>();
        // 向TreeSet中添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        // 输出集合元素,看到集合元素已经处于排序状态
        System.out.println(nums);
        // 输出集合里的第一个元素
        System.out.println(nums.first()); // 输出-9
        // 输出集合里的最后一个元素
        System.out.println(nums.last());  // 输出10
        // 返回小于4的子集,不包含4
        System.out.println(nums.headSet(4)); // 输出[-9, 2]
        // 返回大于5的子集,如果Set中包含5,子集中还包含5
        System.out.println(nums.tailSet(5)); // 输出 [5, 10]
        // 返回大于等于-3,小于4的子集。
        System.out.println(nums.subSet(-3, 4)); // 输出[2]
    }
}

  根据上面程序的运行结果即可看出,TreeSet并不是根据元素的插 入顺序进行排序的,而是根据元素实际值的大小来进行排序的。
  与 HashSet 集 合 采 用 hash 算 法 来 决 定 元 素 的 存 储 位 置 不 同 , TreeSet采用红黑树的数据结构来存储集合元素。 那么TreeSet进行排 序的规则是怎样的呢?TreeSet支持两种排序方法:自然排序和定制排 序。在默认情况下,TreeSet采用自然排序。

2.2.1、自然排序

  TreeSet会调用集合元素的compareTo(Object obj)方法来比较元 素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然 排序。
  Java 提 供 了 一 个 Comparable 接 口 , 该 接 口 里 定 义 了 一 个 compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的 类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一 个 对 象 调 用 该 方 法 与 另 一 个 对 象 进 行 比 较 时 , 例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等; 如果该方法返回一个正整数, 则表明obj1大于obj2;如果该方法返回 一个负整数,则表明obj1小于obj2。 Java的一些常用类已经实现了Comparable接口, 并提供了比较大 小的标准。下面是实现了Comparable接口的常用类。
  ➢ BigDecimal、BigInteger以及所有的数值型对应的包装类:按 它们对应的数值大小进行比较。
  ➢ Character:按字符的Unicode值进行比较。
  ➢ Boolean:true对应的包装类实例大于false对应的包装类实 例。
  ➢ String:依次比较字符串中每个字符的Unicode值。
  ➢ Date、Time:后面的时间、日期比前面的时间、日期大。
  如果试图把一个对象添加到TreeSet时, 则该对象的类必须实现 Comparable接口, 否则程序将会抛出异常。 如下程序示范了这个错误。

class Err {
}

public class TreeSetErrorTest {
    public static void main(String[] args) {
        TreeSet<Object> ts = new TreeSet<>();
        // 向TreeSet集合中添加Err对象
        // 自然排序时,Err没实现Comparable接口将会引发错误
        ts.add(new Err());
    }
}

  还有一点必须指出:大部分类在实现compareTo(Object obj)方法 时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同 类的两个实例才会比较大小。 当试图把一个对象添加到TreeSet集合 时, TreeSet会调用该对象的compareTo(Object obj)方法与集合中的 其他元素进行比较——这就要求集合中的其他元素与该元素是同一个 类的实例。也就是说,向TreeSet中添加的应该是同一个类的对象,否则也会引发ClassCastException异常。如下程序示范了这个错误。

public class TreeSetErrorTest2 {
    public static void main(String[] args) {
        TreeSet<Object> ts = new TreeSet<>();
        // 向TreeSet集合中添加两个对象
        ts.add(new String("Java"));
        ts.add(new Date());   // ①
    }
}

  当把一个对象加入TreeSet集合中时, TreeSet调用该对象的 compareTo(Object obj)方法与容器中的其他对象比较大小,然后根据 红黑树结构找到它的存储位置。 如果两个对象通过compareTo(Object obj)方法比较相等,新对象将无法添加到TreeSet集合中。
  对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是: 两个对象通过compareTo(Object obj)方法比较是否返回0——如果通 过compareTo(Object obj)方法比较返回0, TreeSet则会认为它们相 等;否则就认为它们不相等。
  由此需要注意一个问题:当需要把一个对象放入TreeSet中,重写 该对象对应类的equals()方法时, 应保证该方法与compareTo(Object obj)方法有一致的结果,其规则是:如果两个对象通过equals()方法 比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0。

2.2.2、定制排序

  TreeSet的自然排序是根据集合元素的大小, TreeSet将它们以升 序排列。 如果需要实现定制排序, 例如以降序排列, 则可以通过 Comparator接口的帮助。该接口里包含一个int compare(T o1, T o2) 方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表 明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负 整数,则表明o1小于o2。
  如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供 一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责 集合元素的排序逻辑。 由于Comparator是一个函数式接口, 因此可使 用Lambda表达式来代替Comparator对象。

class M {
    int age;

    public M(int age) {
        this.age = age;
    }

    public String toString() {
        return "M [age:" + age + "]";
    }
}

public class TreeSetTest4 {
    public static void main(String[] args) {
        // 此处Lambda表达式的目标类型是Comparator
        TreeSet<Object> ts = new TreeSet<>((o1, o2) ->
        {
            M m1 = (M) o1;
            M m2 = (M) o2;
            // 1——根据M对象的age属性来决定大小,age越大,M对象反而越小
            return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0;
        });
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        System.out.println(ts);
    }
}

  上面程序中1号代码使用了目标类型为Comparator的Lambda表 达式,它负责ts集合的排序。所以当把M对象添加到ts集合中时,无须 M类实现Comparable接口,因为此时TreeSet无须通过M对象本身来比较 大小,而是由与TreeSet关联的Lambda表达式来负责集合元素的排序。

2.3、EnumSet类

  EnumSet是一个专为枚举类设计的集合类, EnumSet中的所有元素 都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或 隐式地指定。 EnumSet的集合元素也是有序的, EnumSet以枚举值在 Enum类内的定义顺序来决定集合元素的顺序。
  EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高 效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行 批量操作(如调用containsAll() 和retainAll()方法)时,如果其参 数也是EnumSet集合,则该批量操作的执行速度也非常快。
  EnumSet集合不允许加入null元素, 如果试图插入null元素, EnumSet将抛出NullPointerException异常。 如果只是想判断EnumSet 是否包含null元素或试图删除null元素都不会抛出异常, 只是删除操 作将返回false,因为没有任何null元素被删除。
  EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过 它提供的类方法来创建EnumSet对象。 EnumSet类它提供了如下常用的 类方法来创建EnumSet对象。

  ➢ EnumSet allOf(Class elementType):创建一个包含指定枚举 类里所有枚举值的EnumSet集合。
  ➢ EnumSet complementOf(EnumSet s):创建一个其元素类型与 指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包 含原EnumSet集合所不包含的、此枚举类剩下的枚举值(即新 EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类 的所有枚举值)。
  ➢ EnumSet copyOf(Collection c):使用一个普通集合来创建 EnumSet集合。
  ➢ EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相 同元素类型、相同集合元素的EnumSet集合。
  ➢ EnumSet noneOf(Class elementType):创建一个元素类型为 指定枚举类型的空EnumSet。
  ➢ EnumSet of(E first, E…rest):创建一个包含一个或多个 枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举 类。
  ➢ EnumSet range(E from, E to):创建一个包含从from枚举值 到to枚举值范围内所有枚举值的EnumSet集合。
  下面程序示范了如何使用EnumSet来保存枚举类的多个枚举值。

enum Season {
    SPRING, SUMMER, FALL, WINTER
}

public class EnumSetTest {
    public static void main(String[] args) {
        // 创建一个EnumSet集合,集合元素就是Season枚举类的全部枚举值
        EnumSet<Season> es1 = EnumSet.allOf(Season.class);
        System.out.println(es1); // 输出[SPRING, SUMMER, FALL, WINTER]
        // 创建一个EnumSet空集合,指定其集合元素是Season类的枚举值。
        EnumSet<Season> es2 = EnumSet.noneOf(Season.class);
        System.out.println(es2); // 输出[]
        // 手动添加两个元素
        es2.add(Season.WINTER);
        es2.add(Season.SPRING);
        System.out.println(es2); // 输出[SPRING, WINTER]
        // 以指定枚举值创建EnumSet集合
        EnumSet<Season> es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
        System.out.println(es3); // 输出[SUMMER, WINTER]
        EnumSet<Season> es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        System.out.println(es4); // 输出[SUMMER, FALL, WINTER]
        // 新创建的EnumSet集合的元素和es4集合的元素有相同类型,
        // es5的集合元素 + es4集合元素 = Season枚举类的全部枚举值
        EnumSet<Season> es5 = EnumSet.complementOf(es4);
        System.out.println(es5); // 输出[SPRING]
    }
}

  EnumSet是所有Set实现类中性能最好的, 但它只能保存同一个枚 举类的枚举值作为集合元素。

  必须指出的是, Set的三个实现类HashSet、TreeSet和EnumSet都 是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过 一个线程修改了该Set集合, 则必须手动保证该Set集合的同步性。 通 常可以通过Collections工具类的synchronizedSortedSet方法来“包 装”该Set集合。 此操作最好在创建时进行, 以防止对Set集合的意外 非同步访问。例如:
在这里插入图片描述

3、List集合

  List集合代表一个元素有序、可重复的集合, 集合中每个元素都 有其对应的顺序索引。 List集合允许使用重复元素, 可以通过索引来 访问指定位置的集合元素。 List集合默认按元素的添加顺序设置元素 的索引, 例如第一次添加的元素索引为0, 第二次添加的元素索引为 1。
  List作为Collection接口的子接口, 当然可以使用Collection接 口里的全部方法。 而且由于List是有序集合, 因此List集合里增加了 一些根据索引来操作集合元素的方法。
  ➢ void add(int index, Object element):将元素element插入 到List集合的index处。
  ➢ boolean addAll(int index, Collection c):将集合c所包含 的所有元素都插入到List集合的index处。
  ➢ Object get(int index):返回集合index索引处的元素。
  ➢ int indexOf(Object o):返回对象o在List集合中第一次出现 的位置索引。
  ➢ int lastIndexOf(Object o):返回对象o在List集合中最后一 次出现的位置索引。
  ➢ Object remove(int index):删除并返回index索引处的元 素。
  ➢ Object set(int index, Object element):将index索引处的 元素替换成element对象,返回被替换的旧元素。
  ➢ List subList(int fromIndex, int toIndex):返回从索引 fromIndex(包含)到索引toIndex(不包含)处所有集合元素 组成的子集合。
  所有的List实现类都可以调用这些方法来操作集合元素。与Set集 合相比, List增加了根据索引来插入、替换和删除集合元素的方法。 除此之外,Java 8还为List接口添加了如下两个默认方法。
  ➢ void replaceAll(UnaryOperator operator):根据operator 指定的计算规则重新设置List集合的所有元素。
  ➢ void sort(Comparator c):根据Comparator参数对List集合的元素排序。
  下面程序示范了List集合的常规用法。

public class ListTest {
    public static void main(String[] args) {
        ArrayList<String> books = new ArrayList<>();
        // 向books集合中添加三个元素
        books.add("Java EE企业应用实战");
        books.add("Java");
        books.add("Android");
        System.out.println(books);
        // 将新字符串对象插入在第二个位置
        books.add(1, new String("Ajax"));
        for (int i = 0; i < books.size(); i++) {
            System.out.println(books.get(i));
        }
        // 删除第三个元素
        books.remove(2);
        System.out.println(books);
        // 判断指定元素在List集合中的位置:输出1,表明位于第二位
        System.out.println(books.indexOf(new String("Ajax"))); // ①
        //将第二个元素替换成新的字符串对象
        books.set(1, "Java");
        System.out.println(books);
        // 将books集合的第二个元素(包括)
        // 到第三个元素(不包括)截取成子集合
        System.out.println(books.subList(1, 2));
    }
}

  List集合可 以根据位置索引来访问集合中的元素, 因此List增加了一种新的遍历 集合元素的方法:使用普通的for循环来遍历集合元素。
  从上面运行结果清楚地看出List集合的用法。 注意①号代码处, 程序试图返回新字符串对象在List集合中的位置, 实际上List 集合中并未包含该字符串对象。 因为List集合添加字符串对象时, 添 加的是通过new关键字创建的新字符串对象,①号代码处也是通 过new关键字创建的新字符串对象,两个字符串显然不是同一个对象, 但List的indexOf方法依然可以返回1。 List判断两个对象相等的标准 是什么呢?List判断两个对象相等只要通过equals()方法比较返回 true即可。看下面程序。

class A {
    public boolean equals(Object obj) {
        return true;
    }
}

public class ListTest2 {
    public static void main(String[] args) {
        ArrayList<Object> books = new ArrayList<>();
        books.add("Java EE企业应用实战");
        books.add("Java8实战");
        books.add("Android实战");
        System.out.println(books);
        // 删除集合中的A对象,将导致第一个元素被删除
        books.remove(new A());     // ①
        System.out.println(books);
        // 删除集合中的A对象,再次删除集合中的第一个元素
        books.remove(new A());     // ②
        System.out.println(books);
    }
}

  编译、运行上面程序,看到如下运行结果:

[Java EE企业应用实战, Java8实战, Android实战]
[Java8实战, Android实战]
[Android实战]

  从上面运行结果可以看出, 执行①号码时, 程序试图删 除一个A对象, List将会调用该A对象的equals()方法依次与集合元素 进行比较, 如果该equals()方法以某个集合元素作为参数时返回 true,List将会删除该元素——A类重写了equals()方法,该方法总是 返回true。所以每次从List集合中删除A对象时,总是删除List集合中 的第一个元素。
  与Set只提供了一个iterator()方法不同,List还额外提供了一个 listIterator() 方 法 , 该 方 法 返 回 一 个 ListIterator 对 象 , ListIterator接口继承了Iterator接口, 提供了专门操作List的方 法。ListIterator接口在Iterator接口基础上增加了如下方法。
  ➢ boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
  ➢ Object previous():返回该迭代器的上一个元素。
  ➢ void add(Object o):在指定位置插入一个元素。
  拿 ListIterator 与 普 通 的 Iterator 进 行 对 比 , 不 难 发 现 ListIterator增加了向前迭代的功能(Iterator只能向后迭代), 而 且ListIterator还可通过add()方法向List集合中添加元素(Iterator 只能删除元素)。下面程序示范了ListIterator的用法。

public class ListIteratorTest {
    public static void main(String[] args) {
        String[] books = {"Java8实战", "spring实战", "Java EE企业应用实战"};
        ArrayList<String> bookList = new ArrayList<>();
        for (int i = 0; i < books.length; i++) {
            bookList.add(books[i]);
        }
        ListIterator<String> lit = bookList.listIterator();
        // 从前向后遍历
        while (lit.hasNext()) {
            System.out.println(lit.next());
            lit.add("-------分隔符-------");
        }
        System.out.println("=======下面开始反向迭代=======");
        // 从后向前遍历
        while (lit.hasPrevious()) {
            System.out.println(lit.previous());
        }
    }
}

3.1、ArrayList和Vector实现类

  ArrayList和Vector作为List类的两个典型实现,完全支持前面介 绍的List接口的全部功能。
  ArrayList 和 Vector 类 都 是 基 于 数 组 实 现 的 List 类 , 所 以 ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数 组。 ArrayList或Vector对象使用initialCapacity参数来设置该数组 的长度,当向ArrayList或Vector中添加元素超出了该数组的长度时, 它们的initialCapacity会自动增加。
  对于通常的编程场景, 程序员无须关心ArrayList或Vector的 initialCapacity。 但如果向ArrayList或Vector集合中添加大量元素 时, 可使用ensureCapacity(int minCapacity)方法一次性地增加 initialCapacity。这可以减少重分配的次数,从而提高性能。
  如果开始就知道ArrayList或Vector集合需要保存多少个元素,则 可 以 在 创 建 它 们 时 就 指 定 initialCapacity 大 小 。 如 果 创 建 空 的 ArrayList或Vector集合时不指定initialCapacity参数, 则数组的长度默认为10。
  ArrayList和Vector的显著区别是:ArrayList是线程 不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个 线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但 Vector集合则是线程安全的, 无须程序保证该集合的同步性。 因为 Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实 际上, 即使需要保证List集合线程安全, 也同样不推荐使用Vector实 现类。 后面会介绍一个Collections工具类, 它可以将一个ArrayList 变成线程安全的。

4、Queue集合集合

  Queue用于模拟队列这种数据结构, 队列通常是指“先进先出” (FIFO)的容器。 队列的头部保存在队列中存放时间最长的元素, 队 列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到 队列的尾部, 访问元素(poll)操作会返回队列头部的元素。 通常, 队列不允许随机访问队列中的元素。
  Queue接口中定义了如下几个方法。
  ➢ void add(Object e):将指定元素加入此队列的尾部。
  ➢ Object element():获取队列头部的元素, 但是不删除该元 素。
  ➢ boolean offer(Object e):将指定元素加入此队列的尾部。 当使用有容量限制的队列时,此方法通常比add(Object e)方 法更好。
  ➢ Object peek():获取队列头部的元素, 但是不删除该元素。 如果此队列为空,则返回null。
  ➢ Object poll():获取队列头部的元素, 并删除该元素。 如果 此队列为空,则返回null。
  ➢ Object remove():获取队列头部的元素,并删除该元素。

4.1 PriorityQueue实现类

  PriorityQueue是一个比较标准的队列实现类。之所以说它是比较 标准的队列实现,而不是绝对标准的队列实现,是因为PriorityQueue 保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大 小进行重新排序。 因此当调用peek()方法或者poll()方法取出队列中 的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的 元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规 则:先进先出(FIFO)。下面程序示范了PriorityQueue队列的用法。

public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        // 下面代码依次向pq中加入四个元素
        pq.offer(6);
        pq.offer(-3);
        pq.offer(20);
        pq.offer(18);
        // 输出pq队列,并不是按元素的加入顺序排列
        System.out.println(pq); // 输出[-3, 6, 20, 18]
        // 访问队列第一个元素,其实就是队列中最小的元素:-3
        System.out.println(pq.poll());
    }
}

  PriorityQueue队列对元素的要求与TreeSet对元素的要求基本一 致,PriorityQueue不允许插入null元素,它还需要对队列元素进行排 序,PriorityQueue的元素有两种排序方式。
  ➢ 自然排序:采用自然顺序的PriorityQueue集合中的元素必须 实现了Comparable接口, 而且应该是同一个类的多个实例, 否 则可能导致ClassCastException异常。
  ➢ 定制排序:创建PriorityQueue队列时, 传入一个Comparator 对象, 该对象负责对队列中的所有元素进行排序。 采用定制排 序时不要求队列元素实现Comparable接口。

4.2 Deque接口与ArrayDeque实现类

  Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接 口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的 元素。
  ➢ void addFirst(Object e):将指定元素插入该双端队列的开 头。
  ➢ void addLast(Object e):将指定元素插入该双端队列的末 尾。
  ➢ Iterator descendingIterator():返回该双端队列对应的迭 代器,该迭代器将以逆向顺序来迭代队列中的元素。
  ➢ Object getFirst():获取但不删除双端队列的第一个元素。
  ➢ Object getLast():获取但不删除双端队列的最后一个元素。
  ➢ boolean offerFirst(Object e):将指定元素插入该双端队列 的开头。
  ➢ boolean offerLast(Object e):将指定元素插入该双端队列 的末尾。
  ➢ Object peekFirst():获取但不删除该双端队列的第一个元 素;如果此双端队列为空,则返回null。
  ➢ Object peekLast():获取但不删除该双端队列的最后一个元 素;如果此双端队列为空,则返回null。
  ➢ Object pollFirst():获取并删除该双端队列的第一个元素; 如果此双端队列为空,则返回null。
  ➢ Object pollLast():获取并删除该双端队列的最后一个元 素;如果此双端队列为空,则返回null。
  ➢ Object pop()(栈方法):pop出该双端队列所表示的栈的栈 顶元素。相当于removeFirst()。
  ➢ void push(Object e)(栈方法):将一个元素push进该双端 队列所表示的栈的栈顶。相当于addFirst(e)。
  ➢ Object removeFirst():获取并删除该双端队列的第一个元 素。
  ➢ Object removeFirstOccurrence(Object o):删除该双端队列 的第一次出现的元素o。 ➢ Object removeLast():获取并删除该双端队列的最后一个元 素。
  ➢ boolean removeLastOccurrence(Object o):删除该双端队列 的最后一次出现的元素o。
  从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且 可以被当成栈来使用, 因为该类里还包含了pop(出栈)、push(入 栈)两个方法。
  Deque接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建Deque时同样可指定 一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
  ArrayList和ArrayDeque两个集合类的实现机制基本相似,它们 的底层都采用一个动态的、可重分配的Object[]数组来存储集合元 素,当集合元素超出了该数组的容量时,系统会在底层重新分配一 个Object[]数组来存储集合元素。
  Deque不仅可以当成双端队列使用,而且可以被当成栈来使用。
    Deque的方法与Queue的方法对照表
在这里插入图片描述
    Deque的方法与Stack的方法对照表
在这里插入图片描述

  下面程序示范了把ArrayDeque当成“栈”来使用。

public class ArrayDequeStack {
    public static void main(String[] args) {
        ArrayDeque<String> stack = new ArrayDeque<>();
        // 依次将三个元素push入"栈"
        stack.push("Java8实战");
        stack.push("Java EE企业应用实战");
        stack.push("Android实战");
        // 输出:[疯狂Android讲义, 轻量级Java EE企业应用实战, 疯狂Java讲义]
        System.out.println(stack);
        // 访问第一个元素,但并不将其pop出"栈",输出:疯狂Android讲义
        System.out.println(stack.peek());
        // 依然输出:[疯狂Android讲义, 疯狂Java讲义, 轻量级Java EE企业应用实战]
        System.out.println(stack);
        // pop出第一个元素,输出:疯狂Android讲义
        System.out.println(stack.pop());
        // 输出:[轻量级Java EE企业应用实战, 疯狂Java讲义]
        System.out.println(stack);
    }
}

  上面程序的运行结果显示了ArrayDeque作为栈的行为, 因此当程 序中需要使用“栈”这种数据结构时, 推荐使用ArrayDeque, 尽量避 免使用Stack——因为Stack是古老的集合,性能较差。
  当然ArrayDeque也可以当成队列使用, 此处ArrayDeque将按“先 进先出”的方式操作集合元素。例如如下程序。

public class ArrayDequeQueue {
    public static void main(String[] args) {
        ArrayDeque<String> queue = new ArrayDeque<>();
        // 依次将三个元素加入队列
        queue.push("Java8实战");
        queue.push("Java EE企业应用实战");
        queue.push("Android实战");
        // 输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
        System.out.println(queue);
        // 访问队列头部的元素,但并不将其poll出队列"栈",输出:疯狂Java讲义
        System.out.println(queue.peek());
        // 依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
        System.out.println(queue);
        // poll出第一个元素,输出:疯狂Java讲义
        System.out.println(queue.poll());
        // 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
        System.out.println(queue);
    }
}

4.3 LinkedList实现类

  LinkedList类是List接口的实现类——这意味着它是一个List集 合, 可以根据索引来随机访问集合中的元素。 除此之外, LinkedList 还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成 “栈”来使用,也可以当成队列使用。下面程序简单示范了 LinkedList集合的用法。

public class LinkedListTest {
    public static void main(String[] args) {
        LinkedList<String> books = new LinkedList();
        // 将字符串元素加入队列的尾部
        books.offer("Java实战");
        // 将一个字符串元素加入栈的顶部
        books.push("Java EE企业应用实战");
        // 将字符串元素添加到队列的头部(相当于栈的顶部)
        books.offerFirst("Android实战");
        // 以List的方式(按索引访问的方式)来遍历集合元素
        for (var i = 0; i < books.size(); i++) {
            System.out.println("遍历中:" + books.get(i));
        }
        // 访问、并不删除栈顶的元素
        System.out.println(books.peekFirst());
        // 访问、并不删除队列的最后一个元素
        System.out.println(books.peekLast());
        // 将栈顶的元素弹出“栈”
        System.out.println(books.pop());
        // 下面输出将看到队列中第一个元素被删除
        System.out.println(books);
        // 访问、并删除队列的最后一个元素
        System.out.println(books.pollLast());
        // 下面输出:[轻量级Java EE企业应用实战]
        System.out.println(books);
    }
}

  上面程序中代码分别示范了LinkedList作为List集合、双 端队列、栈的用法。 由此可见, LinkedList是一个功能非常强大的集 合类。
  LinkedList 与 ArrayList 、 ArrayDeque 的 实 现 机 制 完 全 不 同 , ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此 随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式 来保存集合中的元素, 因此随机访问集合元素时性能较差, 但在插 入、删除元素时性能比较出色(只需改变指针所指的地址即可)。需 要指出的是, 虽然Vector也是以数组的形式来存储集合元素的, 但因 为它实现了线程同步功能(而且实现机制也不好),所以各方面性能 都比较差。

4.4 各种线性表的性能分析

  Java提供的List就是一个线性表接口,而ArrayList、LinkedList 又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。 Queue代表了队列, Deque代表了双端队列(既可作为队列使用, 也可 作为栈使用),接下来对各种实现类的性能进行分析。
  初学者可以无须理会ArrayList和LinkedList之间的性能差异,只 需要知道LinkedList集合不仅提供了List的功能, 还提供了双端队 列、栈的功能就行。 但对于一个成熟的Java程序员, 在一些性能非常 敏感的地方,可能需要慎重选择哪个List实现。
  一般来说, 由于数组以一块连续内存区来保存所有的数组元素, 所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的 集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合 在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性 能 比 LinkedList 的 性 能 要 好 , 因 此 大 部 分 时 候 都 应 该 考 虑 使 用 ArrayList。
  关于使用List集合有如下建议。
  ➢ 如果需要遍历List集合元素, 对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素, 这样性能更好;对于LinkedList集合, 则应该采用迭代器(Iterator)来遍历集合元素。
  ➢ 如果需要经常执行插入、删除操作来改变包含大量数据的List集合的大小, 可考虑使用LinkedList集合。 使用ArrayList、 Vector集合可能需要经常重新分配内部数组的大小, 效果可能 较差。
  ➢ 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。

5、Map集合

  Map用于保存具有映射关系的数据, 因此Map集合里保存着两组 值, 一组值用于保存Map里的key, 另外一组值用于保存Map里的 value,key和value都可以是任何引用类型的数据。Map的key不允许重 复, 即同一个Map对象的任何两个key通过equals方法比较总是返回 false。
  如果把Map里的所有key放在一起来看,它们 就组成了一个Set集合(所有的key没有顺序, key与key之间不能重 复), 实际上Map确实包含了一个keySet()方法, 用于返回Map里所有 key组成的Set集合。
  不仅如此, Map里key集和Set集合里元素的存储形式也很像, Map 子类和Set子类在名字上也惊人地相似, 比如Set接口下有HashSet、 LinkedHashSet、SortedSet(接口)、TreeSet、EnumSet等子接口和 实现类, 而Map接口下则有HashMap、LinkedHashMap、SortedMap(接 口)、TreeMap、EnumMap等子接口和实现类。 正如它们的名字所暗示 的,Map的这些实现类和子接口中key集的存储形式和对应Set集合中元 素的存储形式完全相同。
  Set与Map之间的关系非常密切。 虽然Map中放的元素是keyvalue对,Set集合中放的元素是单个对象,但如果把key-value对中 的value当成key的附庸:key在哪里, value就跟在哪里。 这样就可 以像对待Set一样来对待Map了。 事实上, Map提供了一个Entry内部 类来封装key-value对, 而计算Entry存储时则只考虑Entry封装的 key。从Java源码来看, Java是先实现了Map, 然后通过包装一个所有value都为空对象的Map就实现了Set集合。
  如果把Map里的所有value放在一起来看, 它们又非常类似于一个 List:元素与元素之间可以重复, 每个元素可以根据索引来查找, 只 是Map中的索引不再使用整数值,而是以另一个对象作为索引。如果需 要从List集合中取出元素, 则需要提供该元素的数字索引;如果需要 从Map中取出元素,则需要提供该元素的key索引。因此,Map有时也被 称为字典,或关联数组。Map接口中定义了如下常用的方法。
  ➢ void clear():删除该Map对象中的所有key-value对。
  ➢ boolean containsKey(Object key):查询Map中是否包含指定 的key,如果包含则返回true。
  ➢ boolean containsValue(Object value):查询Map中是否包含 一个或多个value,如果包含则返回true。
  ➢ Set entrySet():返回Map中包含的key-value对所组成的Set 集合,每个集合元素都是Map.Entry(Entry是Map的内部类)对 象。
  ➢ Object get(Object key):返回指定key所对应的value;如果 此Map中不包含该key,则返回null。
  ➢ boolean isEmpty():查询该Map是否为空(即不包含任何keyvalue对),如果为空则返回true。
  ➢ Set keySet():返回该Map中所有key组成的Set集合。
  ➢ Object put(Object key, Object value) : 添 加 一 个 keyvalue对, 如果当前Map中已有一个与该key相等的key-value 对,则新的key-value对会覆盖原来的key-value对。
  ➢ void putAll(Map m):将指定Map中的key-value对复制到本 Map中。
  ➢ Object remove(Object key) : 删 除 指 定 key 所 对 应 的 keyvalue对, 返回被删除key所关联的value, 如果该key不存在, 则返回null。
  ➢ boolean remove(Object key, Object value):这是Java 8新 增的方法,删除指定key、value所对应的key-value对。如果从 该Map中成功地删除该key-value对, 该方法返回true, 否则返 回false。
  ➢ int size():返回该Map里的key-value对的个数。
  ➢ Collection values() : 返 回 该 Map 里 所 有 value 组 成 的 Collection。
  Map接口提供了大量的实现类, 典型实现如HashMap和Hashtable 等、HashMap的子类LinkedHashMap,还有SortedMap子接口及该接口的 实现类TreeMap,以及WeakHashMap、IdentityHashMap等。
  Map中包括一个内部类Entry, 该类封装了一个key-value对。 Entry包含如下三个方法。
  ➢ Object getKey():返回该Entry里包含的key值。
  ➢ Object getValue():返回该Entry里包含的value值。
  ➢ Object setValue(V value):设置该Entry里包含的value值, 并返回新设置的value值。
  下面程序示范了Map的基本功能。

public class MapTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        // 成对放入多个key-value对
        map.put("Java实战", 109);
        map.put("iOS实战", 10);
        map.put("Ajax实战", 79);
        // 多次放入的key-value对中value可以重复
        map.put("Java EE企业应用实战", 99);
        // 放入重复的key时,新的value会覆盖原有的value
        // 如果新的value覆盖了原有的value,该方法返回被覆盖的value
        System.out.println(map.put("iOS实战", 99)); // 输出10
        System.out.println(map); // 输出的Map集合包含4个key-value对
        // 判断是否包含指定key
        System.out.println("是否包含值为 iOS实战 的key:" + map.containsKey("iOS实战")); // 输出true
        // 判断是否包含指定value
        System.out.println("是否包含值为 99 的value:" + map.containsValue(99)); // 输出true
        // 获取Map集合的所有key组成的集合,通过遍历key来实现遍历所有key-value对
        for (String key : map.keySet()) {
            // map.get(key)方法获取指定key对应的value
            System.out.println(key + "-->" + map.get(key));
        }
        map.remove("Ajax实战"); // 根据key来删除key-value对。
        System.out.println(map); // 输出结果不再包含 Ajax实战=79 的key-value对
    }
}

5.1、Java 8为Map新增的方法

  ➢ Object compute(Object key, BiFunction remappingFunction):该方法使用remappingFunction根据原 key-value对计算一个新value。只要新value不为null,就使用 新value覆盖原value;如果原value不为null, 但新value为 null, 则删除原key-value对;如果原value、新value同时为 null,那么该方法不改变任何key-value对,直接返回null。
  ➢ Object computeIfAbsent(Object key, Function mappingFunction):如果传给该方法的key参数在Map中对应的 value为null,则使用mappingFunction根据key计算一个新的结 果,如果计算结果不为null,则用计算结果覆盖原有的value。 如果原Map原来不包括该key, 那么该方法可能会添加一组keyvalue对。
  ➢ Object computeIfPresent(Object key, BiFunction remappingFunction):如果传给该方法的key参数在Map中对应 的value不为null, 该方法将使用remappingFunction根据原 key、value计算一个新的结果, 如果计算结果不为null, 则使 用该结果覆盖原来的value;如果计算结果为null, 则删除原 key-value对。
  ➢ void forEach(BiConsumer action):该方法是Java 8为Map新 增的一个遍历key-value对的方法,通过该方法可以更简洁地遍 历Map的key-value对。
  ➢ Object getOrDefault(Object key, V defaultValue):获取 指 定 key 对 应 的 value 。 如 果 该 key 不 存 在 , 则 返 回 defaultValue。
  ➢ Object merge(Object key, Object value, BiFunction remappingFunction):该方法会先根据key参数获取该Map中对 应的value。如果获取的value为null,则直接用传入的value覆 盖原有的value(在这种情况下, 可能要添加一组key-value 对);如果获取的value不为null, 则使用remappingFunction 函数根据原value、新value计算一个新的结果, 并用得到的结 果去覆盖原有的value。
  ➢ Object putIfAbsent(Object key, Object value):该方法会 自动检测指定key对应的value是否为null, 如果该key对应的 value为null,该方法将会用新value代替原来的null值。
  ➢ Object replace(Object key, Object value):将Map中指定 key对应的value替换成新value。 与传统put()方法不同的是, 该方法不可能添加新的key-value对。 如果尝试替换的key在原 Map中不存在,该方法不会添加key-value对,而是返回null。
  ➢ boolean replace(K key, V oldValue, V newValue):将Map 中指定key-value对的原value替换成新value。 如果在Map中找 到指定的key-value对, 则执行替换并返回true, 否则返回 false。
  ➢ replaceAll(BiFunction function):该方法使用BiFunction 对原key-value对执行计算, 并将计算结果作为该key-value对 的value值。
  下面程序示范了Map常用默认方法的功能和用法。

public class MapTest2 {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        // 成对放入多个key-value对
        map.put("Java实战", 109);
        map.put("iOS实战", 10);
        map.put("Ajax实战", 79);
        // 尝试替换key为"XML实战"的value,由于原Map中没有对应的key,
        // 因此对Map没有改变,不会添加新的key-value对
        map.replace("XML实战", 66);
        System.out.println(map);
        // 使用原value与参数计算出来的结果覆盖原有的value
        map.merge("iOS实战", 10, (oldVal, param) -> (Integer) oldVal + (Integer) param);
        System.out.println(map); // "疯狂iOS讲义"的value增大了10
        // 当key为"Java"对应的value为null(或不存在时),使用计算的结果作为新value
        map.computeIfAbsent("Java", key -> ((String) key).length());
        System.out.println(map); // map中添加了 Java=4 这组key-value对
        // 当key为"Java"对应的value存在时,使用计算的结果作为新value
        map.computeIfPresent("Java", (key, value) -> (Integer) value * (Integer) value);
        System.out.println(map); // map中 Java=4 变成 Java=16
    }
}

5.2、HashMap和Hashtable实现类

  HashMap和Hashtable都是Map接口的典型实现类,它们之间的关系 完全类似于ArrayList和Vector的关系:Hashtable是一个古老的Map实 现类, 它从JDK 1.0起就已经出现了, 当它出现时, Java还没有提供 Map接口, 所以它包含了两个烦琐的方法, 即elements()(类似于Map 接口定义的values()方法和keys()方法),现在很少使用这两个方法和Hashtable了。即使需要创建线程安全的Map实现类, 也无须使 用Hashtable实现类, 可以通过后面介绍的Collections工具类把 HashMap变成线程安全的。
  Hashtable和HashMap存在两点典型区别。
  ➢ Hashtable是一个线程安全的Map实现, 但HashMap是线程不安 全的实现, 所以HashMap比Hashtable的性能高一点;但如果有 多个线程访问同一个Map对象时, 使用Hashtable实现类会更 好。
  ➢ Hashtable不允许使用null作为key和value, 如果试图把null 值放进Hashtable中,将会引发NullPointerException异常;但 HashMap可以使用null作为key或value。
  由于HashMap里的key不能重复,所以HashMap里最多只有一个keyvalue 对的 key为null, 但 可 以 有 无 数多 个 key-value对 的 value为 null。下面程序示范了用null值作为HashMap的key和value的情形。

public class NullInHashMap {
    public static void main(String[] args) {
        Map<Object, Object> hm = new HashMap();
        // 试图将两个key为null的key-value对放入HashMap中
        hm.put(null, null);
        hm.put(null, null);    // ①
        // 将一个value为null的key-value对放入HashMap中
        hm.put("a", null);    // ②
        // 输出Map对象
        System.out.println(hm);
    }
}

  上面程序试图向HashMap中放入三个key-value对, 其中①号代码处无法将key-value对放入,因为Map中已经有一个key-value对 的key为null值,所以无法再放入key为null值的key-value对。②号代码处可以放入该key-value对,因为一个HashMap中可以有多个value为 null值。
  为了成功地在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode()方法和equals()方法。
  与HashSet集合不能保证元素的顺序一样,HashMap、Hashtable也 不 能 保 证 其 中 key-value 对 的 顺 序 。 类 似 于 HashSet , HashMap 、 Hashtable判断两个key相等的标准也是:两个key通过equals()方法比 较返回true,两个key的hashCode值也相等。
  因此当使用自定义类作为HashMap、Hashtable的key时,如果重写该 类的equals(Object obj)和hashCode()方法, 则应该保证两个方法 的判断标准一致—当两个key通过equals()方法比较返回true时,两 个key的hashCode()返回值也应该相同。因为HashMap、Hashtable保 存 key 的 方 式 与 HashSet 保 存 集 合 元 素 的 方 式 完 全 相 同 , 所 以 HashMap、Hashtable对key的要求与HashSet对集合元素的要求完全 相同。
  除此之外,HashMap、Hashtable中还包含一个containsValue()方 法,用于判断是否包含指定的value。那么HashMap、Hashtable如何判 断两个value相等呢?HashMap、Hashtable判断两个value相等的标准 简单:只要两个对象通过equals()方法比较返回true即可。
  与HashSet类似的是,如果使用可变对象作为HashMap、Hashtable 的key,并且程序修改了作为key的可变对象,则也可能出现与HashSet 类似的情形:程序再也无法准确访问到Map中被修改过的key。 看下面程序。

public class A {
        private Integer count;

        public A(Integer count) {
            this.count = count;
        }

        public Integer getCount() {
            return count;
        }

        public void setCount(Integer count) {
            this.count = count;
        }
    }

public class HashMapErrorTest {
    public static void main(String[] args) {
        Map<Object, String> ht = new HashMap<>();
        // 此处的A类与前一个程序的A类是同一个类
        ht.put(new A(60000), "Java实战");
        ht.put(new A(87563), "Java EE企业应用实战");
        // 获得Hashtable的key Set集合对应的Iterator迭代器
        Iterator<Object> it = ht.keySet().iterator();
        // 取出Map中第一个key,并修改它的count值
        A first = (A) it.next();
        first.count = 87563;   // ①
        // 输出{A@1560b=Java实战, A@1560b=Java EE企业应用实战}
        System.out.println(ht);
        // 只能删除没有被修改过的key所对应的key-value对
        ht.remove(new A(87563));
        System.out.println(ht);
        // 无法获取剩下的value,下面两行代码都将输出null。
        System.out.println(ht.get(new A(87563)));   // ② 输出null
        System.out.println(ht.get(new A(60000)));   // ③ 输出null
    }
}

5.3、LinkedHashMap实现类

  HashSet 有 一 个 LinkedHashSet 子 类 , HashMap 也 有 一 个 LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value 对的顺序(其实只需要考虑key的顺序), 该链表负责维护Map的迭代 顺序,迭代顺序与key-value对的插入顺序保持一致。
  LinkedHashMap可以避免对HashMap、Hashtable里的key-value对 进行排序(只要插入key-value对时保持顺序即可),同时又可避免使 用TreeMap所增加的成本。
  LinkedHashMap 需 要 维 护 元 素 的 插 入 顺 序 , 因 此 性 能 略 低 于 HashMap的性能;但因为它以链表来维护内部顺序, 所以在迭代访问 Map里的全部元素时将有较好的性能。 下面程序示范了LinkedHashMap 的功能:迭代输出LinkedHashMap的元素时, 将会按添加key-value对 的顺序输出。

public class LinkedHashMapTest {
    public static void main(String[] args) {
        Map<String, Integer> scores = new LinkedHashMap();
        scores.put("语文", 80);
        scores.put("英文", 82);
        scores.put("数学", 76);
        // 调用forEach方法遍历scores里的所有key-value对
        scores.forEach((key, value) -> System.out.println(key + "-->" + value));
    }
}

5.4、Properties读写属性文件

  Properties类是Hashtable类的子类,正如它的名字所暗示的,该 对象在处理属性文件时特别方便(Windows操作平台上的ini文件就是 一种属性文件)。Properties类可以把Map对象和属性文件关联起来, 从而可以把Map对象中的key-value对写入属性文件中, 也可以把属性 文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属 性名、属性值只能是字符串类型, 所以Properties里的key、value都 是字符串类型。该类提供了如下三个方法来修改Properties里的key、 value值。
  ➢ String getProperty(String key):获取Properties中指定属 性名对应的属性值,类似于Map的get(Object key)方法。
  ➢ String getProperty(String key, String defaultValue) : 该方法与前一个方法基本相似。 该方法多一个功能, 如果 Properties中不存在指定的key时,则该方法指定默认值。
  ➢ Object setProperty(String key, String value):设置属性 值,类似于Hashtable的put()方法。
  除此之外,它还提供了两个读写属性文件的方法。
  ➢ void load(InputStream inStream):从属性文件(以输入流 表示)中加载key-value对, 把加载到的key-value对追加到 Properties里(Properties是Hashtable的子类,它不保证keyvalue对之间的次序)。
  ➢ void store(OutputStream out, String comments) : 将 Properties中的key-value对输出到指定的属性文件(以输出流 表示)中。

public class PropertiesTest {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        // 向Properties中增加属性
        props.setProperty("username", "yeeku");
        props.setProperty("password", "123456");
        // 将Properties中的key-value对保存到a.ini文件中
        props.store(new FileOutputStream("a.ini"), "comment line");   // ①
        // 新建一个Properties对象
        Properties props2 = new Properties();
        // 向Properties中增加属性
        props2.setProperty("gender", "male");
        // 将a.ini文件中的key-value对追加到props2中
        props2.load(new FileInputStream("a.ini"));   // ②
        System.out.println(props2);
    }
}

5.5、SortedMap接口和TreeMap实现类

  正如Set接口派生出SortedSet子接口, SortedSet接口有一个 TreeSet 实 现 类 一 样 , Map 接 口 也 派 生 出 一 个 SortedMap 子 接 口 , SortedMap接口也有一个TreeMap实现类。
  TreeMap就是一个红黑树数据结构, 每个key-value对即作为红黑 树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对 节点进行排序。 TreeMap可以保证所有的key-value对处于有序状态。 TreeMap也有两种排序方式。
  ➢ 自然排序:TreeMap的所有key必须实现Comparable接口,而且所 有 的 key 应 该 是 同 一 个 类 的 对 象 , 否 则 将 会 抛 出ClassCastException异常。
  ➢ 定制排序:创建TreeMap时, 传入一个Comparator对象, 该对象负责对TreeMap中的所有key进行排序。 采用定制排序时不要求Map的key实现Comparable接口。
  类似于TreeSet中判断两个元素相等的标准, TreeMap中判断两个 key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认 为这两个key是相等的。
  如果使用自定义类作为TreeMap的key, 且想让TreeMap良好地工 作,则重写该类的equals()方法和compareTo()方法时应保持一致的返 回 结 果 : 两 个 key 通 过 equals() 方 法 比 较 返 回 true 时 , 它 们 通 过 compareTo()方法比较应该返回0。如果equals()方法与compareTo()方 法的返回结果不一致,TreeMap与Map接口的规则就会冲突。
  与TreeSet类似的是,TreeMap中也提供了一系列根据key顺序访问 key-value对的方法。

  ➢ Map.Entry firstEntry():返回该Map中最小key所对应的keyvalue对,如果该Map为空,则返回null。
  ➢ Object firstKey():返回该Map中的最小key值,如果该Map为 空,则返回null。
  ➢ Map.Entry lastEntry():返回该Map中最大key所对应的keyvalue对,如果该Map为空或不存在这样的key-value对,则都返 回null。
  ➢ Object lastKey():返回该Map中的最大key值, 如果该Map为 空或不存在这样的key,则都返回null。
  ➢ Map.Entry higherEntry(Object key):返回该Map中位于key 后一位的key-value对(即大于指定key的最小key所对应的keyvalue对)。如果该Map为空,则返回null。
  ➢ Object higherKey(Object key):返回该Map中位于key后一位 的key值(即大于指定key的最小key值)。 如果该Map为空或不 存在这样的key-value对,则都返回null。
  ➢ Map.Entry lowerEntry(Object key):返回该Map中位于key前 一位的key-value对(即小于指定key的最大key所对应的keyvalue对)。如果该Map为空或不存在这样的key-value对,则都 返回null。
  ➢ Object lowerKey(Object key):返回该Map中位于key前一位 的key值(即小于指定key的最大key值)。 如果该Map为空或不 存在这样的key,则都返回null。
  ➢ NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回 该Map的子Map, 其key的范围是从fromKey(是否包括取决于第 二个参数)到toKey(是否包括取决于第四个参数)。
  ➢ SortedMap subMap(Object fromKey, Object toKey):返回该 Map的子Map,其key的范围是从fromKey(包括)到toKey(不包 括)。
  ➢ SortedMap tailMap(Object fromKey):返回该Map的子Map, 其key的范围是大于fromKey(包括)的所有key。
  ➢ NavigableMap tailMap(Object fromKey, boolean inclusive) : 返 回 该 Map 的 子 Map , 其 key 的 范 围 是 大 于 fromKey(是否包括取决于第二个参数)的所有key。
  ➢ SortedMap headMap(Object toKey):返回该Map的子Map, 其 key的范围是小于toKey(不包括)的所有key。
  ➢ NavigableMap headMap(Object toKey, boolean inclusive) : 返 回 该 Map 的 子 Map , 其 key 的 范 围 是 小 于 toKey(是否包括取决于第二个参数)的所有key。
  表面上看起来这些方法很复杂,其实它们很简单。因为TreeMap 中的key-value对是有序的,所以增加了访问第一个、前一个、后一 个、最后一个key-value对的方法, 并提供了几个从TreeMap中截取 子TreeMap的方法。
  下面以自然排序为例,介绍TreeMap的基本用法。

class R implements Comparable {
    int count;

    public R(int count) {
        this.count = count;
    }

    public String toString() {
        return "R[count:" + count + "]";
    }

    // 根据count来判断两个对象是否相等。
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == R.class) {
            R r = (R) obj;
            return r.count == this.count;
        }
        return false;
    }

    // 根据count属性值来判断两个对象的大小。
    public int compareTo(Object obj) {
        R r = (R) obj;
        return count > r.count ? 1 : count < r.count ? -1 : 0;
    }
}

public class TreeMapTest {
    public static void main(String[] args) {
        TreeMap<Object, String> tm = new TreeMap();
        tm.put(new R(3), "Java EE企业应用实战");
        tm.put(new R(-5), "Java实战");
        tm.put(new R(9), "Android实战");
        System.out.println(tm);
        // 返回该TreeMap的第一个Entry对象
        System.out.println(tm.firstEntry());
        // 返回该TreeMap的最后一个key值
        System.out.println(tm.lastKey());
        // 返回该TreeMap的比new R(2)大的最小key值。
        System.out.println(tm.higherKey(new R(2)));
        // 返回该TreeMap的比new R(2)小的最大的key-value对。
        System.out.println(tm.lowerEntry(new R(2)));
        // 返回该TreeMap的子TreeMap
        System.out.println(tm.subMap(new R(-1), new R(4)));
    }
}

6、操作集合的工具类:Collections

  Java 提 供 了 一 个 操 作 Set 、 List 和 Map 等 集 合 的 工 具 类 : Collections,该工具类里提供了大量方法对集合元素进行排序、查询 和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现 同步控制等方法。

6.1、排序操作

  ➢ void reverse(List list):反转指定List集合中元素的顺 序。
  ➢ void shuffle(List list):对List集合元素进行随机排序 (shuffle方法模拟了“洗牌”动作)。
  ➢ void sort(List list):根据元素的自然顺序对指定List集合 的元素按升序进行排序。
  ➢ void sort(List list, Comparator c):根据指定Comparator 产生的顺序对List集合元素进行排序。
  ➢ void swap(List list, int i, int j):将指定List集合中的 i处元素和j处元素进行交换。
  ➢ void rotate(List list, int distance):当distance为正数 时, 将list集合的后distance个元素“整体”移到前面;当 distance为负数时, 将list集合的前distance个元素“整体” 移到后面。该方法不会改变集合的长度。

6.2、查找、替换操作

  ➢ int binarySearch(List list, Object key):使用二分搜索 法搜索指定的List集合, 以获得指定对象在List集合中的索 引。 如果要使该方法可以正常工作, 则必须保证List中的元素 已经处于有序状态。
  ➢ Object max(Collection coll):根据元素的自然顺序, 返回 给定集合中的最大元素。
  ➢ Object max(Collection coll, Comparator comp) : 根 据 Comparator指定的顺序,返回给定集合中的最大元素。
  ➢ Object min(Collection coll):根据元素的自然顺序, 返回 给定集合中的最小元素。
  ➢ Object min(Collection coll, Comparator comp) : 根 据 Comparator指定的顺序,返回给定集合中的最小元素。
  ➢ void fill(List list, Object obj):使用指定元素obj替换 指定List集合中的所有元素。
  ➢ int frequency(Collection c, Object o):返回指定集合中 指定元素的出现次数。
  ➢ int indexOfSubList(List source, List target) : 返 回 子 List对象在父List对象中第一次出现的位置索引;如果父List 中没有出现这样的子List,则返回-1。
  ➢ int lastIndexOfSubList(List source, List target):返回 子List对象在父List对象中最后一次出现的位置索引;如果父 List中没有出现这样的子List,则返回-1。
  ➢ boolean replaceAll(List list, Object oldVal, Object newVal) : 使 用 一 个 新 值 newVal 替 换 List 对 象 的 所 有 旧 值 oldVal。

6.3、同步控制

  Collections类中提供了多个synchronizedXxx()方法, 该方法可 以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问 集合时的线程安全问题。
  Java 中 常 用 的 集 合 框 架 中 的 实 现 类 HashSet 、 TreeSet 、 ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不 安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改 它们,则存在线程安全的问题。Collections提供了多个类方法可以把 它们包装成线程同步的集合。
  下面的示例程序创建了4个线程安全的集合对象。

public class SynchronizedTest {
    public static void main(String[] args) {
        // 下面程序创建了四个线程安全的集合对象
        Collection<Object> objects = Collections.synchronizedCollection(new ArrayList<>());
        List<Object> list = Collections.synchronizedList(new ArrayList<>());
        Set<Object> set = Collections.synchronizedSet(new HashSet<>());
        Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>());
    }
}

6.4、设置不可变集合

  Collections提供了如下三类方法来返回一个不可变的集合。
  ➢ emptyXxx():返回一个空的、不可变的集合对象,此处的集合 既可以是List, 也可以是SortedSet、Set, 还可以是Map、 SortedMap等。
  ➢ singletonXxx():返回一个只包含指定对象(只有一个或一项 元素)的、不可变的集合对象, 此处的集合既可以是List, 还 可以是Map。
  ➢ unmodifiableXxx():返回指定集合对象的不可变视图, 此处 的集合既可以是List, 也可以是Set、SortedSet, 还可以是 Map、SorteMap等。
  上面三类方法的参数是原有的集合对象, 返回值是该集合的“只 读”版本。 通过Collections提供的三类方法, 可以生成“只读”的 Collection或Map。看下面程序。

public class UnmodifiableTest {
    public static void main(String[] args) {
        // 创建一个空的、不可改变的List对象
        List<Object> unmodifiableList = Collections.emptyList();
        // 创建一个只有一个元素,且不可改变的Set对象
        Set<String> unmodifiableSet = Collections.singleton("疯狂Java讲义");
        // 创建一个普通Map对象
        Map<String, Integer> scores = new HashMap();
        scores.put("语文", 80);
        scores.put("Java", 82);
        // 返回普通Map对象对应的不可变版本
        Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(scores);
        // 下面任意一行代码都将引发UnsupportedOperationException异常
        unmodifiableList.add("测试元素");   // ①
        unmodifiableSet.add("测试元素");    // ②
        unmodifiableMap.put("语文", 90);   // ③
    }
}

  不可变的集合对象只能访问集合元素,不可修改集合元素。所 以 上 面 程 序 中 ① ② ③ 处 的 代 码 都 将 引 发 UnsupportedOperationException异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值