疯狂Java讲义 第八章 Java集合

jefxff 153,675 2020-04-01

1. Java集合概述

  1. Java集合类是一种特别有用的工具类, 可用于存储数量不等的对象, 并可以实现常用数据结构, 如队列,栈;集合类都在 java.util 包下, Java5后, 在 java.util.concurrent 包下提供支持线程的集合类
  2. Java集合还可用于保存具有映射关系的关联数组
  3. Java集合分四种: Set(无序, 不可重复的集合), List(有序的, 重复的集合), Map(代表具有映射关系的集合), Queue(代表一种队列集合实现)
  4. Java的集合类主要由两个接口派生而出: CollectionMap , 他们是Java集合框架的根接口, 这两个接口包含一些子接口和实现类; Set和List和Queue接口是Collection接口派生的子接口
  5. 所有的Map实现类用于保存具有映射关系的数据(也就是关联数组), Map的实现类保存的都是 key-value 对, 也就是由 key 和 value 两个值组成, key 不可以重复, 而 value 可以重复
  6. 集合和数组的区别: 数组元素既可以是基本类型的值, 也可以是对象(实际上保存的是对象的引用变量); 而集合只能保存对象(实际上只是保存的对象的引用变量--记住)

四种集合最常用的实现类

HashSet, TreeSet, ArrayList, ArrayDeque, LinkedList, HashMap, treeMap

访问集合的元素

  1. List集合元素直接通过索引来访问; 因为List是有序的集合
  2. Map集合的元素通过每项元素的key来访问其value; 因为Map集合的key值是不可重复的
  3. Set集合中的元素只能根据元素本身来访问; 因为Set集合的元素无序可重复

2. Collection和Iterator接口

  1. 由于 Collection 接口是 Set, List, Queue接口的父接口, 所以 Collection 接口定义的方法可用于操作Set, List, Queue集合
  2. ★ 集合就像容器, 实现生活中容器的功能, 可添加对象, 删除对象, 清空对象, 判断容器是否为空等, 集合类就为这些功能提供了对应的方法

Collection 接口定义的方法

  1. boolean add(Object o)
    说明: 该方法用于向集合里添加一个元素, 如果集合对象被添加操作改变了(也就是添加成功), 就返回true
  2. boolean addAll(Collection c)
    说明: 该方法把集合c里所有的元素添加到指定的集合里, 如果集合对象被添加操作该变了, 就返回true
  3. void clear()
    说明: 清空集合里的所有元素, 将集合的长度变为0
  4. boolean contains(Object o)
    说明: 返回集合里是否包含指定元素
  5. boolean containsAll(Collection c)
    说明: 返回集合里是否包含集合c里的所有元素
  6. boolean isEmpty()
    说明: 返回集合是否为空, 当集合长度为0时, 返回true, 否则返回false
  7. Iterator iterator()
    说明: 返回一个 Iterator对象, 用于遍历集合里的元素
  8. boolean remove(Object o)
    说明: 删除集合中指定元素o, 当集合中包含了一个或多个元素o时, 该方法只删除第一个符合条件的元素, 该方法将返回true
  9. boolean removeAll(Collection c)
    说明: 从集合中删除集合c里包含的所有元素(相当于调用该方法的结合减集合c), 如果删除了一个或一个以上的元素, 则该方法返回true
  10. boolean retainAll(Collection c)
    说明: 从集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合和集合c的交集), 如果操作改变了调用方法的集合, 则该方法返回true
  11. int size()
    说明: 该方法返回集合里的元素个数
  12. Object[] toArray()
    说明: 该方法把集合转换成一个数数组, 所有的集合元素变成对应的数组元素

示例代码

//例子:
// CollectionTest.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
//
public class CollectionTest{
    public static void main(String[] args) {
        // 创建一个 Collection 类型的 ArrayList 有序集合(都是通过Collection和Map的实现类创建的)
        Collection c = new ArrayList();
        // 添加元素
        c.add("孙悟空");
        // 虽然集合里不能放基本类型, 但Java支持自动装箱
        c.add(6);
        System.out.println("c集合的元素个数为: " + c.size());
        // 删除指定元素
        c.remove(6);
        System.out.println("c集合的元素个数为: " + c.size());
        // 判断是否包含指定的字符串
        System.out.println("c集合是否包含\"孙悟空\"字符串: " + c.contains("孙悟空"));
        c.add("轻量级 Java EE 企业级应用实战");
        System.out.println("c集合的元素: " + c);
        // 创建无序可重复的Set集合
        Collection books = new HashSet();
        books.add("轻量级 Java EE 企业级应用实战");
        books.add("疯狂 Java 讲义");
        System.out.println("c集合是否包含books集合? " + c.containsAll(books));
        // 用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);
    }
}
// 程序说明:
//     1. 程序未使用泛型来限制集合里元素的种类, 编译会报警告
//     2. 所有的 Collection 实现类都重写了 toString() 方法, 该方法可以一次性地输出集合中的所有元素

2.1 使用Lambda表达式遍历集合

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

示例代码

// 例子:
// CollectionEach.java 
import java.util.Collection;
import java.util.HashSet;
// 
public class CollectionEach {
    public static void main(String[] args) {
        // 创建一个无序不可重复的hashSet集合
        Collection books = new HashSet();
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        // 调用 forEach()方法遍历(使用Lambda表达式遍历集合)
        books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

2.2 Java8 Iterator遍历集合元素

  1. Iterator 主要用于遍历(即迭代访问) Collection 集合中的元素, Iterator 对象也被称之为迭代器; 而 Collection 系列集合, Map系列集合主要用于盛装其他对象
  2. Iterator 接口隐藏了各种 Collection 实现类的底层细节, 向应用程序提供了遍历 Collection 集合元素的统一编程接口

Iterator 接口里定义的方法:(例子1)

  1. boolean hasNext()
    说明: 如果被迭代的集合元素还没有被遍历完, 则该方法返回true
  2. Object next()
    说明: 返回集合里的下一个元素
  3. void remove()
    说明: 删除集合里的下一个元素
  4. void forEachRemaining(Consumer action)
    说明: 这是Java8为 Iterator 新增的默认方法, 该方法可使用Lambda表达式来遍历集合元素

示例代码

// 例子1:
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class IteratorTest{
    public static void main(String[] args){
        // 创建一个无序不可重复的HashSet集合
        Collection books = new HashSet();
        // 向集合中添加元素
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        // 获取books集合对应的迭代器(获取集合的迭代器必须通过被迭代的集合对象)
        Iterator it = books.iterator();
        while (it.hasNext()){
            // it.next()方法返回的数据类型是Object类型, 因此需要强制类型转换
            String book = (String)it.next();
            if(book.equals("Python Note")){
                // 从集合中删除上一次 next() 方法返回的元素
                it.remove();
                // books.remove(book);  // ②
            }
            book = "测试字符串";   // ①
        }
        // out: [java Note, JavaScript Note, Css Note]
        System.out.println(books);
    }
}

例子1代码说明:

  1. Iterator 仅用于遍历集合. Iterator 本身并不提供盛装对象的能力, 如果需要创建 Iterator 对象(程序中的 it), 则必须有一个被迭代的集合(程序中的books), 没有集合的 Iterator 没有存在的价值
  2. Iterator必须依附与Collection对象, 若有一个Iterator对象, 则必然有一个与之关联的 Collection对象, Iterator 提供了两个方法来迭代访问 Collection 集合里的元素, 并可通过 remove() 方法来删除集合中上一次next()方法放回的集合元素
  3. ①处的代码并没有改变books集合中的元素, 当使用Iterator 对集合元素进行迭代时, Iterator 并不是把集合元素本身(集合元素时引用变量)传给了迭代变量, 而是把集合元素的值(引用变量指向的堆内存的值)传给了迭代变量, 所以修改迭代变量的值对集合元素本身没有影响
  4. 当使用 Iterator 迭代访问 Collection 集合元素时, Collection 集合里的元素不能被改变, 只有通过 Iterator 的 remove() 方法删除上一次 next() 方法返回的集合元素才可以, 否则将引发异常

2.3 使用Lambda表达式遍历Iterator

  1. Java8 为 Iterator 新增的一个 forEachRemaining(Consumer action) 方法, Consumer参数是函数式接口,
  2. 当程序调用 Iterator 的 forEachRemaining(Consumer action) 遍历集合元素时, 程序会依次将集合元素传给 Consumer 的 accept(T t)方法(该接口中唯一的抽象方法)

示例代码

// 例子:
public class IterctorEach{
    public static void main(String[] args){
        // 创建一个无序可不重复的HashSet集合
        Collection books = new HashSet();
        // 添加集合元素
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        // 获取books集合对应的迭代器
        Iterator it = books.iterator()
        // 使用Lambda表达式(目标类型是Consumer)来遍历集合元素
        it.forEachRemaining(obj -> System.out.println("迭代集合元素: " + obj));
    }
}

2.4 使用foreach循环遍历集合元素

示例代码

// 例子:
public class foreachTest{
    public static void main(String[] args){
        // 创建一个无序不可重复的HashSet集合
        Collection books = new HashSet();
        // 添加集合元素
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        for(Object obj : books){
            // 此处的book变量也不是集合元素本身
            // 将Object类型的变量强转为String类型
            String book = (String)obj;
            System.out.println(book);
            if(book.equals("Python Note")){
                // 下面的代码引发异常
                books.remove(book);   // ①
            }
        }
        System.out.println(books);
    }
}
// 程序说明:
//     1. foreach循环中的迭代变量也不是集合元素本身, 系统只是依次把集合元素的值赋给迭代变量. 因此在
//         foreach循环中修改迭代变量的值也没有实际意义
//     2. 当使用 foreach 循环迭代访问集合元素时, 该集合也不能被改变, 否则将引发异常, 如①处

2.5 使用Java8新增的 Predicate 操作集合

  1. removeIf(Predicate filter) 方法支持 批量删除 符合 filter 条件的所有元素; Predicate 也是函数式接口, 也可以使用Lambda表达式作为参数

示例代码

// 例子:
import java.util.Collection;
import java.util.HashSet;
import java.util.function.Predicate;
//
public class PredicateTest {
    public static void main(String[] args) {
        // 创建一个无序不可重复的HashSet集合
        Collection books = new HashSet();
        // 添加集合元素
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        //
        // 使用Lambda表达式(目标类型是Predicate)过滤集合 (删除长度大于12的元素)
        books.removeIf(ele -> ((String)ele).length() > 12);
        System.out.println(books);   // out: [java Note, Python Note, Css Note]
        // 统计书名中包含"Python"子串的图书数量
        System.out.println(calAll(books, ele -> ((String)ele).contains("Python"))); //out: 1
        // 统计书名中包含"Css"子串的图书数量
        System.out.println(calAll(books, ele -> ((String)ele).contains("Css")));  // out: 1
        // 统计书名字符串长度大于10的图书数量
        System.out.println(calAll(books, ele -> ((String)ele).length() > 10));  // out: 1
    }
    //
    public static int calAll(Collection books, Predicate p) {
        int total = 0;
        for (Object obj : books){
            // 使用 Predicate 的 test() 方法判断该对象是否满足 Predicate 指定条件
            if (p.test(obj)){
                total ++;
            }
        }
        return total;
    }
}
// 程序说明:
//     1. 上述程序先定义了一个calAll()方法, 该方法将会使用 Predicate 判断每个集合元素是否符合特定条件
//         该条件将通过 Predicate 参数动态传入, 上述四行输出行代码传入了四个Lambda表达式(目标类型都
//         是Predicate), 这样 calAll()方法就只会统计满足Predicate条件的图书

2.6 使用java8新增的 Stream 操作集合

  1. Java8 还新增了 Stream, IntStream, LongStream, DoubleStream 等流式API, 这些API代表多个支持串行和并行聚集操作的元素; Stream 是一个通用的流接口, 而IntStream, LongStream, DoubleStream则是代表元素类型为 int, long, double 的流
  2. Java8 还为上面的每个流API提供了对应的 Biulder, 如: Stream.Builder, IntStream.Builder, LongStream.Builder, DoubleStream.Builder, 可通过这些Builder 来创建对应的流
  3. Java8 允许使用流式API来操作集合, Collection接口提供了一个 stream()默认方法, 该方法可返回该集合对应的流, 接下来可通过流式API来操作集合元素, Stream可对集合元素进行整机的聚集操作

独立使用 Stream 的步骤:

  1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder
  2. 重复调用 Builder 的 add() 方法向该流中添加元素
  3. 调用 Builder 的 build()方法获取对应的 Stream
  4. 调用 Stream 的聚集方法

中间方法, 末端方法, 有状态方法, 短路方法:

  1. 中间方法: 中间操作允许流保持打开状态, 并允许直接调用后续方法. 下面程序中的map()方法
  2. 末端方法: 末端方法是对流的最终操作; 当某个Stream执行末端方法后, 该流将会被"消耗"且不可再用; 下面程序中的sum(), count(), average() 方法都是末端方法
  3. 有状态方法: 这种方法会给流增加一些新的属性, 比如元素的唯一性, 元素的最大数量, 保证元素以排序的方式被处理等, 有状态方法往往需要更大的性能开销
  4. 短路方法: 短路方法可以尽早结束对流的操作, 不必检查所有的元素

Stream 常用的中间方法:

  1. filter(Predicate predicate)
    说明: 过滤Stream 中不符合 predicate 的元素
  2. mapToXxx(ToXxxFunction mapper)
    说明: 使用 ToXxxFunction 对流中的元素执行一对一的转换, 该方法返回的新流中包含了 ToXxxFunction转换生成的所有元素
  3. peek(Consumer action)
    说明: 依次对每个元素执行一些操作, 该方法返回的流与原有流包含相同的元素, 主要用于调试
  4. distinct()
    说明: 该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true), 这是一个有状态的方法
  5. sorted()
    说明: 用于保证流中的元素在后续的访问中 处于有序状态, 是一个有状态的方法
  6. limit(long maxSize)
    说明: 用于保证对该流的后续访问中最大允许访问的元素个数. 有状态的,短路方法

Stream 常用的末端方法:

  1. forEach(Consumer action)
    说明: 遍历流中的元素, 对每个元素执行action
  2. toArray()
    说明: 将流中的所有元素转换为数组
  3. reduce()
    说明: 该方法有三个重载版本: 都用于通过某种操作来合并流中的元素
  4. min()
    说明: 返回流中所有元素的最小值
  5. max()
    说明: 返回流中所有元素的最大值
  6. count()
    说明: 返回流中所有元素的数量
  7. anyMatch(Prerdicate predicate)
    说明: 判断流中是否至少包含一个元素符合Predicate条件
  8. allMatch(Predicate predicate)
    说明: 判断流中是否每个元素都符合Predicate条件
  9. noneMatch(Predicate predicate)
    说明: 判断流中没有一个元素符合Predicate条件
  10. findFirst()
    说明: 返回流中的第一个元素
  11. findAny()
    说明: 返回流中的任意一个元素

示例代码

// 例子1:
public class IntStreamTest{
    public static void main(String[] args){
        IntStream is = IntStream.builder()
            .add(20)
            .add(13)
            .add(-2)
            .add(18)
            .build();
        // 
        // 下面调用聚集方法的代码每次执行执行一行
        System.out.println("is 所有元素的最大值: " + is.max().getAsInt());
        System.out.println("is 所有元素的最小值: " + is.min().getAsInt());
        System.out.println("is 所有元素的总和: " + is.sum());
        System.out.println("is 所有元素的总数: " + is.count());
        System.out.println("is 所有元素的平均值: " + is.average());
        System.out.println("is 所有元素的平方是否都大于20: " 
            + is.allMatch(ele -> ele * ele > 20) );
        System.out.println("is 是否包含任何元素的平方大于20: " 
            + is.anyMatch(ele -> ele * ele > 20) );
        // 将 is 映射成一个新的 Stream, 新的 Stream 的每个元素是原 Stream 元素的 2倍加1
        IntStream newIs = is.map(ele -> ele * 2 + 1);
        // 使用方法引用的方式来遍历集合元素 ============== 19/09/16 的晚上我已经忘了方法引用~~~
        newIs.forEach(System.out::println);  // out: 41 27 -3 37 
    }
}
// 例子2: 
import java.util.Collection;
import java.util.HashSet;
// 
public class CollectionStream {
    public static void main(String[] args) {
        // 创建一个集合
        Collection books = new HashSet();
        books.add("java Note");
        books.add("Python Note");
        books.add("JavaScript Note");
        books.add("Css Note");
        // 
        //统计书名包含 Note 子串的图书
        System.out.println(books.stream()
            .filter(ele -> ((String)ele).contains("Note")).count());  // out: 4
        //统计书名包含 Java 子串的图书
        System.out.println(books.stream()
            .filter(ele -> ((String)ele).contains("Java")).count());  // out: 1 
        // 统计书名字符串长度大于10的图书数量
        System.out.println(books.stream()
            .filter(ele -> ((String)ele).length() > 10).count());  // out: 2
        // 先调用 Collection 对象的 stream()方法将集合转换为Stream
        // 在调用 Stream 的 mapToInt() 方法获取原有Stream对应的 IntStream
        books.stream().mapToInt(ele -> ((String)ele).length())
            // 调用 forEach() 方法遍历 IntStream 中每个元素
            .forEach(System.out::print);  // out: 9 15 11 8
    }
}
// 程序说明:
//     1. 程序只要调用 Collection 的 stream() 方法即可返回该集合对应的Stream, 接下来就可以通过Stream
//         提供的方法对所有的集合元素进行处理, 可以简化代码
//     2. 上面程序最后一条输出语句, 先调用Collection 对象的 stream() 方法(即: books.stream())将集合
//         转换为 Stream对象, 然后调用 Stream对象的mapToInt() 方法将其转换为 IntStream; 这个过程中
//         mapToInt() 方法就是一个中间方法, 因此程序可以继续调用 IntStream 的 forEach 方法来遍历流
//         中的元素

3. Set集合

  1. Set集合不允许包含相同的元素, 如果试图把两个相同的元素加入同一个Set集合中, 则添加操作失败, add() 方法返回false, 且新元素不会被加入
  2. 着重学习Set接口的三个实现类: HashSet, TreeSet, EnumSet

3.1 HashSet 类

  1. HashSet是set接口的典型实现类, HashSet按Hash算法来存储集合中的元素
  2. 当向 HashSet 集合存一个元素时, Hashset 会调用该对象的 hashCode() 方法来获取该对象的HashCode值,然后根据该hashCode值决定该对象在 HashSet 中的存储位置
  3. 如果两个元素通过 equals()方法比较返回 true, 但他们的 hashCode()方法返回值不相等, HashSet 将会把他们存储在不同的位置, 依然可以添加成功; 即 HashSet 集合判断两个元素相等的标准是两个对象通过equals()方法比较相等, 并且两个对象的hashCode()方法的返回值也相等

HashSet特点

  1. HashSet 不能保证元素的排列顺序, 顺序可能与添加顺序不同, 顺序也有可能发生变化
  2. HashSet 不是同步的, 如果多个线程同时访问一个Hashset, 假设有两个或两个以上线程同时修改了 Hashset 集合时, 则必须通过代码来保证其同步
  3. HashSet 集合的元素可以是 null

示例代码

// 例子1:
// 通过类A,B,C, 他们分别重写了equals(),hashCode()两个方法的一个或全部, 通过这个例子:理解HashSet
//   集合判断两个元素相同的标准(equals()和hashCode()都相等)
// 类 A 的equals()方法总是返回true, 没有重写hashCode()方法
class A{
    public boolean equals(){
        return ture;
    }
}
// 类 B 的hashCode() 方法总是返回1, 但是没有重写euqals()方法
class B {
    public int hashCode(){
        return 1;
    }
}
// 类 C 的 hashCode()方法总是返回2, 且equals()方法总是返回true
class C {
    public boolean equals(){
        return true;
    }
    public int hashCode(){
        return 2;
    }
}
public class HashSetTest{
    public static void main(String[] args){
        HashSet 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);
    }
}

代码(例子1)说明

  1. 由于C类重写了equals()方法总是返回true, hashCode()方法总是返回2, 将导致HashSet把两个C对象当成同一个对象
  2. 即使两个A对象通过equals()方法返回true,但HashSet依然把他们当成两个对象; 即使两个B对象的hashCode返回相同值, 但HashSet依然把他们当成两个对象
  3. 注意: 如果两个对象通过equals()方法总是返回true,这两个对象的hashCode值也应该相等
  4. 类B的equals()返回true,但是hashCode不同;这将会导致HashSet会把两个对象保存在Hash表的不同位置从而是两个对象都可以添加成功, 这就与Set集合的规则起冲突了
  5. HashSet中每个能存储元素的"槽位"(slot)通常称为"桶"(bucket), 如果有多个元素的hashCode值相同但他们通过equals()方法比较返回false, 就需要在一个"桶"里放多个元素, 这样导致性能下降
  6. 如果向 HashSet 中添加一个可变对象后, 后面程序修改了该可变对象的实例变量, 则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致 HashSet 中包含两个相同的对象(下面例子2)

重写hashCode()方法的基本规则

  1. 在程序运行过程中, 同一个对象多次调用HashCode()方法应该返回相同的值
  2. 当两个对象通过equals()方法比较返回true时, 这两个对象的hashCode值也应该相等
  3. 对象中用作equals()方法比较标准的实例变量, 都应该用于计算hashCode值

hashCode 值的计算方式

实例变量类型      计算方式
boolean  hashCode = (f?0:1);
整数类型(四个)  hashCode = int(f);
long  hashCode = (int)(f^(f>>>32));
float  hashCode = Float.flaotToIntBits(f);
double  long i = Double.doubleToLongBits(f);   hashCode = (int)(i^(i>>>32));
引用类型  hashCode = f.hashCode();

重写hashCode()方法的步骤

  1. 把对象内每个有意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的
    hashCode值
  2. 用第一步计算出的多个hashCode值组合计算出一个hashCode值返回, 为避免直接相加产生偶然相等,可以通过为各实例变量的hashCode值乘以任意一个质数后再相加,如:return f1.hashCode() * 19 + (int)f2 * 32;

示例代码

// 例子2:
import java.util.HashSet;
import java.util.Iterator;
// 
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){
            R 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){
        HashSet 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 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
    }
}
// 程序说明:
//     1. 当向HashSet中添加可变对象时, 如果修改HashSet集合中的对象, 有可能导致该对象与集合中的其他对象
//         相等, 从而导致HashSet无法准确访问该对象

3.2 LinkedHashSet 类

  1. LinkedHashSet 集合也是根据元素的hashCode值来决定元素的存储位置, 但它同时 使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的 ; 即:当遍历 LinkedHashSet 集合里的元素时, LinkedHashSet将会按元素的添加顺序来访问集合里的元素(但它依然是HashSet,不允许集合元素重复)
  2. LinkedHashSet 需要维护元素的插入顺序, 因此性能略低于 HashSet 的性能, 但在迭代访问 Set 里的全部元素时将有很好的性能, 因此它以链表来维护内部顺序
  3. 一句话: LinkedHashSet 集合的元素,访问元素的顺序和添加元素的顺序一致

示例代码

// 例子:
import java.util.LinkedHashSet;
// 
public class LinkedHashSetTest {
    public static void main(String[] args){
        LinkedHashSet books = new LinkedHashSet();
        books.add("python");
        books.add("Java");
        System.out.println(books);    // out: [python, Java]
        // 删除 python 
        books.remove("python");
        // 重新添加 python 
        books.add("python");
        System.out.println(books);    // out: [Java, python]
    }
}

3.3 TreeSet 类

  1. TreeSet是SortedSet接口的实现类, TreeSet可以确保集合元素处于排序状态
  2. TreeSet采用红黑树的数据结构来存储集合元素, TreeSet支持两种排序方式, 自然排序和定制排序,默认情况下,TreeSet采用自然排序
  3. 因为TreeSet中的元素是有序的, 所以增加了访问第一个, 前一个, 后一个, 最后一个元素的方法, 和三个从TreeSet中截取子TreeSet的方法
  4. TreeSet 并不是根据插入的元素顺序来进行排序, 而是根据元素实际值的大小来进行排序

TreeSet 提供的方法

  1. Comparator comparator()
    说明: 如果 TreeSet 采用了定制排序, 则该方法返回定制排序所使用的 Comparator; 如果 TreeSet 采用了自然排序, 则返回null
  2. Object first()
    说明: 返回集合中的第一个元素
  3. Object last()
    说明: 返回集合中的最后一个元素
  4. Object lower(Object e)
    说明: 返回集合中位于指定元素之前的元素(即小于指定元素的最大元素, 参考元素不需要是TreeSet集合里的元素)
  5. Object higher(Object e)
    说明: 返回集合中位于指定元素之后的元素(即大于指定元素的最小元素, 参考元素不需要是TreeSet集合里的元素)
  6. SortedSet subSet(Object fromElement, Object toElement)
    说明: 返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)
  7. SortedSet headSet(Object toElement)
    说明: 返回此Set的子集, 由小于toElement的元素组成
  8. SortedSet tailSet(Object fromElement)
    说明: 返回此Set集合的子集, 由大于或等于fromElement的元素组成

示例代码

// 例子:
// TreeSet 方法应用
import java.util.TreeSet;
// 
public class TreeSetTest{
    public static void main(String[] args){
        TreeSet nums = new TreeSet();
        // 向nums添加四个Integer对象
        nums.add(5);
        nums.add(2);
        nums.add(10);
        nums.add(-9);
        // 输出集合元素看到集合元素已经排序
        System.out.println(nums);     // out: [-9, 2, 5, 10]
        // 输出集合里的第一个元素
        System.out.println(nums.first());     // out: -9
        // 输出集合里的最后一位元素
        System.out.println(nums.last());     // out: 10
        // 返回小于4的子集, 不包含4
        System.out.println(nums.headSet(4));     // out: [-9, 2]
        // 返回大于等于5的子集
        System.out.println(nums.tailSet(5));     // out: [5, 10]
        // 返回大于等于-3 小于4的子集
        System.out.println(nums.subSet(-3, 4));     // out: [2]
    }
}

自然排序

  1. 原理: treeSet调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系, 然后将集合元素按升序排列, 这种方式就是自然排序
  2. 向TreeSet集合中添加元素时, 只有第一个元素无需实现Comparable接口, 后面添加的所有元素都必须实现Comparable接口; 但是这样的话, 当试图从TreeSet中取出元素时, 依然会引发ClassCastException异常(例子1)
  3. 所以注意: 大部分类在实现compareTo(Object obj)方法时, 都需要将被比较对象 obj 强制转换成相同的类型, 因为只有两个相同类的两个实例才会比较大小; 当试图把一个对象添加到TreeSet集合时, TreeSet会调用该对象的 compareTo(Object obj)方法与集合中的其他元素进行比较, 这就要求集合中的其他元素与该元素是同一个类的实例, 也就是说, 向TreeSet中添加的应该是同一个类的对象, 否则会引发ClassCastException异常(例子2)
  4. 总结: TreeSet正常运作的前提就是, TreeSet只能添加同一种类型的对象
  5. TreeSet集合判断两个对象是否相等的唯一标准是: 两个对象通过compareTo(Object obj)方法比较是否返回0; 返回0则相等,否则不相等
  6. 如果向TreeSet中添加了一个可变对象, 并且后面程序修改了该可变对象的实例变量, 这将导致它与其他对象的大小顺序发生了改变, 但TreeSet不会再次调整他们的顺序, 甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0, (例子4)

关于 Comparable接口

  1. 该接口里定义了compareTo(Object obj)方法, 该方法返回一个整数值, 实现该接口的类必须实现该方法, 实现了该接口的类的对象就可以比较大小
  2. 当一个对象调用该方法与另一个对象比价大小的时候, 例如 obj1.compareTo(obj2), 如果该方法返回0,则表明两个对象相等, 如果返回一个正整数, 则表明obj1大于obj2, 返回一个负整数, 则说明obj1小于obj2(例子3)

Comparable接口的一些常用实现类

  1. BigDecimal, BigInteger 以及所有的数值型对应的包装类:
    说明: 按他们对应的数值大小进行比较
  2. Character
    说明: 按字符的 UNICODE 值进行比较
  3. Boolean
    说明: true对应的包装类实例大于false对应的包装类实例
  4. String
    说明: 按字符串中字符的 UNICODE 值进行比较
  5. Date, Time
    说明: 后面的时间, 日期比前面的时间日期大

示例代码

// 例子1:
// 试图将一个对象添加到TreeSet时, 没实现Comparable接口, 第二次添加时, 报错
class Err{}
public class TreeSetErrTest{
    public static void main(String[] args){
        TreeSet ts = new TreeSet();
        // 向TreeSet集合中添加两个Err对象
        ts.add(new Err());
        ts.add(new Err());  // 在第二次添加时会报错
    }
}
//
// 例子2:
public class TreeSetErrTest2{
    public static void main(String[] args){
        TreeSet ts = new TreeSet();
        // 向TreeSet集合中添加两个Err对象
        ts.add(new String("Python"));
        ts.add(new Date());   // 这行代码引发异常, 因为两个对象是不同类型
    }
}
//
// 例子3:
import java.util.TreeSet;

// Z 类实现Comparable接口
class Z implements Comparable{
    int age;
    //构造器
    public Z (int age){
        this.age = age;
    }
    //重写equals()方法, 总是返回True
    public boolean equals(Object obj){
        return true;
    }
    // 重写 compareTo(Object obj)方法, 总是返回1
    public int compareTo(Object obj){
        return 1;
    }
}
public class TreeSetTest2{
    public static void main(String[] args){
        TreeSet set = new TreeSet();
        Z z1 = new Z(6);
        set.add(z1);
        // 第二次添加同一个对象, 输出true, 表明添加成功
        System.out.println(set.add(z1));
        // 下面输出set集合, 将输出两个元素
        System.out.println(set);   // out: [chapter08.Z@2f333739, chapter08.Z@2f333739]
        // 修改set集合的第一个元素的age变量
        ((Z)(set.first())).age = 9;
        // 输出set集合的最后一个元素的age变量,将看到也变成了9
        System.out.println(((Z)(set.last())).age);    // out: 9
    }
}
// 程序说明:
//     1. 注意: 如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)
//         方法比较应该返回0(返回0表示相等)
// 
//例子4:
// R 类实现Comparable接口
class R implements Comparable{
    int count;
    //构造器
    puublic R(int count){
        this.count = count;
    }
    public String toString(){
        return "R[count: " + count + " ]";
    }
    // 重写equals()方法,通过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;
    }
    // 重写 compareTo(Object obj)方法, 根据count来比较大小
    public int compareTo(Object obj){
        R r = (R)obj;
        return count > r.count ? 1: count < r.count ? -1 : 0;
    }
}
public class TreeSetTest3{
    public static void main(String[] args){
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
        // 打印TreeSet集合, 集合元素是有序排列的
        System.out.println(ts);
        // 取出第一个元素
        R first = (R)ts.first();
        // 对第一个元素的count赋值
        first.count = 20;
        // 取出最后一个元素
        R  last = (R)ts.last();
        // 对最后一个元素的count赋值, 与第二个元素的count相同
        last.count = -2;
        // 再次输出将看到 TreeSet 里的元素处于无序状态, 且由重复的元素
        System.out.println(ts);
        // 删除实例变量被改变的元素, 删除失败
        System.out.println(ts.remove(new R(-2)));
        System.out.println(ts);
        // 删除实例变量没有被改变的元素, 删除成功
        System.out.println(ts.remove(new R(5)));
        System.out.println(ts);
    }    
}
// 程序说明:
//     1. 与 HashSet 类似的是, TreeSet中包含了可变对象, 当可变对象的实例变量被修改时, TreeSet
//         在处理这些对象时非常复杂, 而且容易错, 
//     2. 为了程序更加健壮, 尽量不要修改放入HashSet和TreeSet集合中元素的关键实例变量 

定制排序

  1. 通过Comparator接口实现定制排序, 该接口包含一个 int compare(T o1, T o2)方法, 该方法用于比较o1和o2的大小: 如果该方法返回正整数, 则表明o1大于o2, 如果该方法返回0,则表明o1等于o2; 如果该方法返回负整数, 则表明o1小于o2
  2. 要实现定制排序, 则需要在创建TreeSet集合对象时, 提供一个 Comparable 对象与该 TreeSet集合关联由该 Comparable 对象负责集合元素的排序逻辑; 由于 Comparable 是一个函数式接口, 可使用Lambda表达式来代替Comparator对象(例子1)
  3. 当通过 Comparator 对象(或lambda表达式)来实现TreeSet的定制排序时, 依然不可以向TreeSet中添加类型不同的对象, 否则也会引发ClassCastExxeption异常
  4. 使用定制排序时, TreeSet对集合元素排序不管集合元素本身的大小, 而是由 Comparator对象(或lambda表达式)负责集合元素的排序规则. TreeSet判断两个集合元素相等的标准是: 通过 Comparator (或者Lambda表达式)比较两个元素返回了0, 这样TreeSet不会把第二个元素添加到集合中

示例代码

// 例子1:
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 ts = new TreeSet((o1, o2) -> {
            M m1 = (M)o1;
            M m2 = (M)o2;
            // 根据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));
        ts.add(new M(-10));
        ts.add(new M(0));
        // out: [M[age: 9 ], M[age: 5 ], M[age: 0 ], M[age: -3 ], M[age: -10 ]]
        System.out.println(ts);
        }    
}
// 程序说明:
//     1. 上述例子中的Lambda表达式部分使用了目标类型为Comparator, 它负责ts集合的排序
//     2. 当把M对象添加到ts集合中时, 无须M类实现Comparable接口, 因此此时TreeSet无须通过M对象本身
//         来比较大小, 而是由与TreeSet关联的Lambda表达式来负责集合元素的排序

3.4 EnumSet 类

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

EnumSet类创建EnumSet对象的方法

  1. EnumSet allOf(Class elementType)
    说明: 创建一个包含指定枚举类里所有枚举值的EnumSet集合
  2. EnumSet complementOf(EnumSet s)
    说明: 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合, 新EnumSet集合包含原EnumSet集合所不包含的, 此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值)
  3. EnumSet copyOf(Collection c)
    说明: 使用一个普通集合来创建EnumSet集合(当复制Collection集合中的所有元素来创建新的EnumSet集合时要求Collection集合中的所有元素必须是同一个枚举类的枚举值)(例子2)
  4. EnumSet copyOf(EnumSet s)
    说明: 创建一个与指定 EnumSet 具有相同元素类型, 相同集合元素的EnumSet集合
  5. EnumSet noneOf(class elementType)
    说明: 创建一个元素类型为指定枚举类型的空EnumSet
  6. EnumSet of(E first, E... rest)
    说明: 创建一个包含一个或多个枚举值的EnumSet集合, 传入的多个枚举值必须属于同一个枚举类
  7. EnumSet range(E from, E to)
    说明: 创建一个包含从 from 到 to 枚举值范围内所有枚举值的EnumSet集合

示例代码

// 例子1:
// 如何使用EnumSet来保存枚举类的多个枚举值
import java.util.EnumSet;
// 
enum Season {
    SPRING, SUMMER, FALL, WINTER
}
public class EnumSetTest{
    public static void main(String[] args){
        // 创建一个EnumSet集合, 集合元素就是Season枚举类的全部枚举值
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1); // 输出[SPRING, SUMMER, FALL, WINTER]
        // 创建一个EnumSet空集合, 指定其集合元素是Season类的枚举类
        EnumSet 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 es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
        System.out.println(es3); // 输出[SUMMER, WINTER]
        EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        System.out.println(es4); // 输出[SUMMER, FALL, WINTER]
        // 新创建的EnumSet集合元素和es4集合元素有相同的类型
        // es5集合元素 + es4集合元素 = Season雷剧类的全部枚举值
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5); // 输出[SPRING]
    }
}
// 例子2:
public class EnumSetTest2{
    public static void main(String[] args){
        Collection c = new HashSet();
        // 清空集合元素
        c.clear();
        c.add(Season.FALL);
        c.add(Season.SPRING);
        // 复制Collection集合中的所有元素来创建EnumSet集合
        EnumSet es = EnumSet.copyOf(c);
        System.out.println(es); // 输出[SPRING, FALL]
        c.add("Python");
        c.add("Love Java");
        // 下面的代码出现异常: 因为c集合里的元素不是全部都为枚举值
        es = EnumSet.copyOf(c);
    }
}

3.5 各 Set 实现类的性能分析

  1. HashSet 的性能总是比TreeSet好(特别是最常用的添加, 查询元素等操作), 因为TreeSet需要额外的红黑树算法来维护集合元素的次序
  2. 只有当需要一个保持排序的Set时, 才应该使用TreeSet, 否则都应该使用 HashSet
  3. LinkedHashSet 对于普通的插入, 删除操作, LinkedHashSet 比 HashSet 要略微慢一点, 这是由于维护链表所带来的额外开销造成的, 但由于链表, 遍历LinkedHashSet会更快
  4. EnumSet 是所有的Set实现类中性能最好的, 但它只能保存同一个枚举类的枚举值作为集合元素
  5. 注意: Set 的三个实现类 HashSet, TreeSet, EnumSet都是线程不安全的, 多线程的情况下需要在创建时通过Collections工具类的 SynchronizedSortedSet方法来"包装"该Set集合, 以防止对Set集合的意外非同步访问如: SortedSet s = Collections.SynchronizedSortedSet(new TreeSet(...));

4. List 集合

  1. List集合代表的一个元素有序, 可重复的集合, 集合中每个元素都有其对应的顺序索引
  2. List 集合允许使用重复元素, 可以通过索引来访问指定位置的集合元素, List集合默认按照元素的添加顺序设置元素的索引
  3. List集合可以使用普通的for循环来遍历集合元素
  4. List 集合判断两个对象相等只要通过equals方法比较返回true即可(例子2)

4.1 Java8改进的List接口和ListIterator接口(例子1)

  1. void add(int index, Object element)
    说明: 将元素element插入到List集合的index索引处
  2. boolean addAll(int index, Collection c)
    说明: 将集合c包含的所有元素都插入到List集合的index索引处
  3. Object get(int index)
    说明: 返回List集合index索引处的元素
  4. int indexOf(Object o)
    说明: 返回对象 o 在List集合中第一次出现的位置索引
  5. int lastIndexOf(object o)
    说明: 返回对象o在List集合中最后一次出现的位置索引
  6. Object remove(int index)
    说明: 删除并返回index索引处的元素
  7. Object set(int index, Object element)
    说明: 将index所引出的元素替换成element对象, 返回被替换的旧元素(使用该方法时, 指定的索引必须是List集合的有效索引; 该方法并不会改变List集合的长度)
  8. List subList(int fromIndex, int toIndex)
    说明: 返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合
  9. void replaceAll(UnaryOperator operator)
    说明: 根据operator指定的计算规则重新设置List集合的所有元素(例子3)
  10. void sort(Comparator c)
    说明: 根据Comparator参数对List集合的元素排序(例子3)

示例代码

// 例子1:
import java.util.ArrayList;
import java.util.List;
// 
// List集合根据索引来访问元素的方法
public class LsitTest{
    public static void main(String[] args) {
        List books = new ArrayList();
        // 向books集合中添加三个元素
        books.add(new String("You Love Python"));
        books.add(new String("Yes I Love Java"));
        books.add(new String("Do U Want A Python Note"));
        System.out.println(books);
        // 将新字符串对象插入在第二个位置
        books.add(1, new String("Crazy Java Note"));
        for (int i = 0; i < books.size(); i++){
            System.out.println(books.get(i));
        }
        // 删除第三个元素
        books.remove(2);
        System.out.println(books);
        // 判断指定元素在List集合中的位置
        System.out.println(books.indexOf(new String("Crazy Java Note")));
        // 将第二个元素替换成新的字符串对象
        books.set(1, new String("Hello world"));
        System.out.println(books);
        // 将books集合的第二个元素(包括)
        // 到第三个元素(不包括)截取成子集合
        System.out.println(books.subList(1, 2));
    }
}
// 例子2:
import java.util.ArrayList;
import java.util.List;
// 
class A {
    public boolean equals(Object obj){
        return true;
    }
}
public class ListTest2{
    public static void main(String[] args){
        List books = new ArrayList();
        // 向books集合中添加三个元素
        books.add(new String("You Love Python"));
        books.add(new String("Yes I Love Java"));
        books.add(new String("Do U Want A Python Note"));
        System.out.println(books);
        // 删除集合中的A对象, 将导致第一个元素被删除
        books.remove(new A());    // ①
        System.out.println(books);
        // 删除集合中的A对象, 再次删除集合中的第一个元素
        books.remove(new A());     // ② 
        System.out.println(books);
    }
}
// 程序说明:
//     1. 执行①处的代码时, 程序试图删除一个A对象, List将会调用该A对象的equals()方法依次与该集合元素进
//         行比较, 如果该equals()方法以某个集合元素作为参数时返回true, List将会删除该元素, A类重写了
//         equals()方法, 该方法总是返回true, 所以每次从List集合中删除A对象时, 总是删除List集合中的第
//         一个元素
// 例子3:
import java.util.ArrayList;
import java.util.List;
// 
public class ListTest3 {
    public static void main(String[] args){
        List books = new ArrayList();
        // 向books集合中添加四个元素
        books.add(new String("Python"));
        books.add(new String("Java"));
        books.add(new String("JavaScript"));
        books.add(new String("Linux"));
        // 使用目标类型为comparator的Lambda表达式对List集合排序
        books.sort((o1, o2) -> ((String)o1).length() - ((String)o2).length());   // ①
        System.out.println(books);  // out: [Java, Linux, Python, JavaScript]
        // 使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
        // 该lambda表达式控制使用每个字符传的长度作为新的集合元素
        books.replaceAll(ele -> ((String)ele).length());    // ②
        System.out.println(books);  // out: [4, 5, 6, 10]
    }
}
// 程序说明:
//     1. ①处的代码控制对List集合进行排序, 传给sort()方法的Lambda表达式指定的排序规则是: 字符串长  
//         度越长, 字符串越大, 因此①处的代码执行完之后, List集合中的字符串会按由短到长的顺序排序
//     2. ②处的代码传给replaceAll()方法的Lambda表达式指定了替换集合元素的规则: 直接用集合元素(字
//         符串)的长度作为新的集合元素.

List集合的listIterator()方法

  1. 该方法返回一个ListIterator对象, ListIterator接口继承了Iterator接口, 提供了专门的操作List的方法
  2. 使用 ListIterator 迭代List集合时, 开始也需要采用正向迭代, 即先使用next()方法进行迭代, 在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素

ListIterator接口在Iterotor接口基础上增加的方法:(例子1)

  1. boolean hasPrevious()
    说明: 返回该迭代器关联的集合是否还有上一个元素
  2. Object previous()
    说明: 返回该迭代器的上一个元素
  3. void add(Object o)
    说明: 在指定位置插入一个元素

ListIterator 和 Iterator 的区别

  1. ListIterator增加了向前迭代的功能(Iterator只能向后迭代), 而且ListIterator还可通过add()方法向List集合中添加元素(Iterator只能删除元素)

示例代码

// 例子1:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
// 
public class ListIteratorTest {
    public static void main(String[] args){
        String[] books = new String[]{
            "Python", "Java", "JavScript", "Linux"
        };
        List bookList = new ArrayList();
        for (int i = 0; i < books.length; i++){
            bookList.add(books[i]);
        }
        ListIterator lit = bookList.listIterator();
        while(lit.hasNext()){
            System.out.println(lit.next());
            lit.add("----------分隔符-----------");
        }
        System.out.println("======下面开始反向迭代======");
        while(lit.hasPrevious()){
            System.out.println(lit.previous());
        }
    }
}

4.2 ArrayList 和 Vector 实现类

  1. ArrayList 和 Vector 是 List的实现类, 支持List接口的全部功能
  2. ArrayList 和 Vector 类都是基于数组实现的List类, 所以ArrayList 和 Vector类封装了一个动态的, 允许再分配的Object[] 数组
  3. ArrayList 或 Vector 对象使用 initialCapacity参数来设置该数组的长度, 当向ArrayList 或 Vector中添加大量元素时, 可使用 ensureCapacity(int minCapacity)方法一次性地增加 initialCapacity,这可以减少重分配的次数, 从而提高性能
  4. 如果知道 ArrayList 或 Vector 集合需要保存多少个元素, 则可以在创建他们时就指定 initialCapacity的大小; 如果创建空的 ArrayList 或 Vector 集合时不指定 initialCapacity参数, 则 Object[]数组的长度默认为10

ArrayList 和 Vector 重分配 Object[]数组的方法:

  1. void ensureCapacity(int minCapacity)
    说明: 将 ArrayList 或 Vector 集合的 Object[] 数组长度增加大于或等于 minCapacity 值
  2. void trimToSize()
    说明: 调整 ArrayList 或 Vector 集合的 Object[] 数组长度为当前元素的个数, 调用该方法可减少ArrayList 或 Vector 集合对象占用的存储空间

ArrayList 和 Vector 的区别:

  1. ArrayList 是线程不安全的, 当多个线程访问同一个 ArrayList 集合时, 如果有超过一个线程修改了 ArrayList 集合, 则程序必须手动保证该集合的同步性;
  2. Vector 集合则是线程安全的, 无须程序保证集合的同步性; 因为 Vector 是线程安全的, 所以性能略低与ArrayList集合
  3. 不推荐使用 Vector 的实现类
  4. Vector 提供一个 Stack子类, 用于模拟"栈"这种数据结构, "栈"通常是"后进先出"(LIFO)的容器; 不使用
  5. ArrayDeque 也是 List 的实现类, ArrayDeque 既实现了List接口, 也实现了Deque接口, 因此也可以作为栈来使用; 因为 ArrayDeque 底层是基于数组的实现, 所以性能也较好.

4.3 固定长度的 List

  1. 操作数组的工具类: Arrays, 提供一个 asList(Object... a)方法, 可以把一个数组或指定个数的对象转换成一个List集合, 这个List集合既不是ArrayList实现类的实例, 也不是Vector实现类的实例, 而是Arrays的内部类ArrayList的实例
  2. Arrays.ArrayList 是一个固定长度的List集合, 程序只能遍历访问该集合里的元素, 不能增加, 删除该集合里的元素

示例代码

import java.util.Arrays;
import java.util.List;

public class FixedSizeList {
    public static void main(String[] args){
        List fixedList = Arrays.asList("python", "java", "C");
        // 获取fixedList的实现类, 将输出 java.util.Arrays$ArrayList
        System.out.println(fixedList.getClass());
        // 使用方法引用遍历集合元素
        fixedList.forEach(System.out::println);
        // 试图增加, 删除元素都会引发 UnsupportedOperationException 异常
        // fixedList.add("New Python");
        // fixedList.remove("python");
    }
}

5. Queue 集合

  1. Queue 用于模拟队列这种数据结构, 队列通常指"先进先出"(FIFO)的容器, 队列的头部保存在队列中存放时间最长的元素, 队列的尾部保存在队列中存放时间最短的元素
  2. 新元素插入(offer)到队列的尾部,访问元素(poll)操作返回队列头部的元素; 通常队列不允许随机访问队列中的元素
  3. Queue 接口有一个 PriorityQueue 实现类; 也有一个 Deque 接口, Deque 接口代表一个"双端队列", 双端 队列可以同时从两端来添加, 删除元素, 因此 Deque 的实现类既可当成队列使用, 也可当成栈使用
  4. Deque 接口提供 ArrayDeque 和 LinkedList 两个实现类

Queue 接口中定义的方法:

  1. void add(Object e)
    说明: 将指定元素加入此队列的尾部
  2. Object element()
    说明: 获取队列头部的元素, 但是不删除该元素
  3. boolean offer(Object e)
    说明: 将指定元素加入此队列的尾部, 当使用有容量限制的队列时, 此方法通常比 add(Object e)方法更好
  4. Object peek()
    说明: 获取队列头部的元素, 但是不删除该元素, 如果此队列为空, 则返回null
  5. Object poll()
    说明: 获取队列头部的元素, 并删除该元素,如果此队列为空, 则返回null
  6. Object remove()
    说明: 获取队列头部的元素, 并删除该元素

5.1 PriorityQueue 实现类

  1. PriorityQueue 是一个比较标准的队列实现类, 因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序, 而是按照队列元素的大小进行重新排序
  2. 当调用 peek()方法或者poll()方法取出队列中的元素时, 并不是取出最先进入队列的元素, 而是取出队列中最小的元素
  3. PriorityQueue 不允许插入null 元素

PriorityQueue 的元素的两种排序方式:

  1. 自然排序: 采用自然顺序的 PriorityQueue 集合中的元素必须实现 Comparable 接口, 而且应该是同一个类的多个实例, 否则可能导致ClassCastException异常
  2. 定制排序: 创建 PriorityQueue 队列时, 传入一个 Comparator 对象, 该对象负责对队列中的所有元素进行排序; 采用定制排序时不要求队列元素实现 Comparable 接口

示例代码

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

    }
}

5.2 Deque 接口与 ArrayDeque 实现类

  1. Deque 接口是Queue 接口的子接口, 它代表一个双端队列, Deque 接口里定义了一些双端队列的方法, 这些方法允许从两端来操作队列的元素
  2. 通过 Deque 提供的 pop() 和 push() 方法, 还可以将双端队列当成一个栈来使用(例子1)
  3. ArrayDeque 是 Deque 的实现类, 是一个基于数组实现的双端队列, 创建 Deque 时同样可以指定一个numElements 参数, 该参数用于指定 Objcet[] 数组的长度; 如果不指定 numElements 参数的长度, Deque 底层数组的长度是 16
  4. ArrayList 和 ArrayDeque 两个集合类的实现机制基本相似, 他们底层都采用一个动态的, 可重分配的 Object[] 数组来存储集合元素, 当集合元素超出了该数组的容量时, 系统会在底层重新分配一个 Object[]数组来存储集合元素
  5. ArrayDeque 既可作为 "栈" 来使用, 也可以作为队列来使用, 当需要使用 "栈" 这种数据结构时, 优先使用ArrayDeque;

Deque 接口里定义的方法:

  1. void addFirst(Object obj)
    说明: 将指定元素插入到该双端队列的开头
  2. void addLast(Object obj)
    说明: 将指定元素插入到该双端队列的末尾
  3. Iterator descendingIterator()
    说明: 返回该双端队列对应的迭代器, 该迭代器将以逆向顺序来迭代队列中的元素
  4. Object getFirst()
    说明: 获取但不删除双端队列的第一个元素
  5. Object getLast()
    说明: 获取但不删除双端队列的最后一个元素
  6. boolean offerFirst(Object e)
    说明: 将指定元素插入到该双端队列的开头
  7. boolean offerLast(Object e)
    说明: 将指定元素插入到该双端队列的末尾
  8. Object peekFirst()
    说明: 获取但不删除该双端队列的第一个元素; 如果此双端队列为空, 则返回null
  9. Object peekLast()
    说明: 获取但不删除该双端队列的最后一个元素; 如果此双端队列为空, 则返回null
  10. Object pollFirst()
    说明: 获取并删除该双端队列的第一个元素; 如果此双端队列为空, 则返回null
  11. Object pollLast()
    说明: 获取并删除该双端队列的最后一个元素; 如果此双端队列为空, 则返回null
  12. Object pop()
    说明: (栈方法) pop 出该双端队列所表示的栈的栈顶元素, 相当于 removeFirst()
  13. void push(Object e)
    说明: (栈方法) 将一个元素 push 进该双端队列所表示的栈的栈顶, 相当于 addFirst(e)
  14. Object removeFirst()
    说明: 获取并删除该双端队列的第一个元素
  15. Objcet removeFirstOccurrence(Object o)
    说明: 删除该双端队列的最后一次出现的元素 o
  16. Objcet removeLast()
    说明: 获取并删除该双端队列的最后一个元素
  17. boolean removeLastOccurrence(Object o)
    说明: 删除该双端队列的最后一次出现的元素 o

示例代码

//例子1:
import java.util.ArrayDeque;

// 把 ArrayDeque 当成一个 "栈" 来使用 
public class ArrayDequeStack{
    public static void main(String[] args){
        ArrayDeque stack = new ArrayDeque();
        // 一次将三个元素 push 入 "栈"
        stack.push("Python");
        stack.push("Java");
        stack.push("JavaScript");
        System.out.println(stack);   // out: [JavaScript, Java, Python]
        // 访问第一个元素, 但并不将其pop出"栈", 输出: Python 
        System.out.println(stack.peek());   // out: JavaScript
        System.out.println(stack);   // out: [JavaScript, Java, Python]
        System.out.println(stack.pop());   // out: JavaScript
        System.out.println(stack);   // out: [Java, Python]
    }
}

// 例子2:
import java.util.ArrayDeque;

// 把 ArrayDeque 当成一个 "队列" 来使用 ("先进先出")
public class ArrayDequeQueue{
    public static void main(String[] args) {
        ArrayDeque queue = new ArrayDeque();
        queue.push("Python");
        queue.push("Java");
        queue.push("JavaScript");
        System.out.println(queue);   // out: [JavaScript, Java, Python]
        // 访问队列头部的元素, 但并不讲其 poll 出队列 "栈"
        System.out.println(queue.peek());  // out: JavaScript
        System.out.println(queue);   // out: [JavaScript, Java, Python]
        // poll 出第一个元素
        System.out.println(queue.poll());  // out: JavaScript
        System.out.println(queue);    // out: [Java, Python]
    }
}

5.3 LinkedList 实现类

  1. LinkedList 是 List 接口的实现类, 是一个List集合, 可以根据索引来随机访问集合中的元素
  2. LinkedList 也实现了Deque接口, 可以被当成双端队列来使用, 所以也可以被当作 "栈" 来使用, 也可以被当成队列来使用 (例子1)
  3. LinkedList 和 ArrayList, ArrayDeque 的实现机制完全不同, ArrayList, ArrayDeque 内部以数组的形式来保存集合中的元素, 因此随机访问集合元素时有较好的性能; 而 LinkedList 内部以链表的形式来保存集合中的元素, 因此随机访问集合元素时性能较差, 但是在插入, 删除元素时性能比较好(只需改变指针所指的地址即可). 虽然 Vector 也是以数组的形式来存储集合元素的, 但因为它实现了线程同步的功能(实现机制也不好), 所以各方面性能都比较差
  4. 对于所有内部基于数组的集合实现, 如 ArrayList, ArrayDeque 等, 使用随机访问的性能都比使用 Iterator迭代访问的性能要好, 因为随机访问会被映射成对数组元素的访问

示例代码

// 例子1:
import java.util.LinkedList;

public class LinkedListTest {
    public static void main(String[] args) {
        LinkedList books = new LinkedList();
        // 将字符串元素加入队列的尾部
        books.offer("Python");
        // 将一个字符串元素加入栈的顶部
        books.push("Java");
        System.out.println(books);
        // 将字符串元素添加到队列的头部(相当于栈的栈顶)
        books.offerFirst("JavaScript");
        // 以List的方式(按索引访问的方式)来遍历集合元素
        for (int 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());
        System.out.println(books);
    }
}

5.4 各种线性表的性能分析

  1. List 就是一个线性表接口, 而 LinkedList 和 ArrayList 是基于数组的线性表和基于链的线性表的两种典型实现; Queue 代表了队列, Deque 代表了双端队列(既可作为队列使用, 也可以作为栈使用)
  2. LinkedList 集合不仅提供了List的功能, 也提供了双端队列, 栈的功能
  3. ArrayList 的性能比 LinkedList 的性能要好

使用List集合注意点:

  1. 如果需要遍历List集合元素, 对于ArrayList, Vector 集合, 应该使用随机访问方法(get)来遍历集合元素, 这样性能更好; 对于LinkedList 集合, 则应该采用迭代器(Iterator)来遍历集合元素
  2. 如果需要经常执行插入, 删除操作来改变包含大量数据的 List 集合的大小, 可考虑使用 LinkedList 集合, 因为使用 ArrayList 和 Vector 集合可能需要经常重新分配内部数组的大小, 效果可能较差
  3. 如果有多个线程需要同时访问List集合中的元素, 可以考虑使用Collections将集合包装成线程安全的集合

6. Java 8 增强的 Map 集合

  1. Map 用于保存具有映射关系的数据, 因此Map集合里保存着两组值, 一组值用于保存Map里的Key, 另外一组值用于保存Map里的value
  2. Key 和 value 都可以是引用数据类型, key不允许重复(即同一个Map对象的任何两个key通过equals方法比较总是返回false)
  3. Map中的key和value之间存在单向的一对一关系, 即通过指定的key,总能找到唯一的, 确定的value; 从Map中取出数据时, 只要给出指定的key,你可以取出对应的value
  4. 如果把Map里的所有key放在一起来看, 他们就组成了一个Set集合(所有的key没有顺序, key和key之间不能重复), 所以Map包含了一个 keySet()方法, 用于返回Map里所有key组成的Set集合
  5. Map接口中有 HashMap, LinkedHashMap, SortedMap(接口), TreeMap, EnumMap等子接口和实现类
  6. 如果将Map中的所有value放在一起来看, 他们非常类似与一个List(元素与元素之间可以重复, 每个元素可以根据索引来查找), 只是Map中索引不再使用整数值, 而是以另外一个对象作为索引
  7. Map最典型的用法: 就是成对的添加, 删除key-value对, 接下来即可判断该Map中是否包含指定的key, 是否包含指定的value, 也可以通过Map提供的keySet()方法获取所有key组成的集合, 进而遍历Map中所有的key-value 对 (例子1)

Map接口中定义的方法:

  1. void clear()
    说明: 删除该Map对象中所有的key-value对
  2. boolean containsKey(Object Key)
    说明: 查询Map中是否包含指定的key, 如果包含则返回true
  3. boolean containsValue(Object value)
    说明: 查询Map中是否包含一个或多个value, 如果包含则返回true
  4. Set entrySet()
    说明: 返回Map中包含的 key-value 对所组成的Set集合, 每个集合元素都是 Map.Entry (Entry是Map的内部类)对象
  5. Object get(Object key)
    说明: 返回指定key所对应的value; 如果此Map中不包含该key, 则返回null
  6. boolean isEmpty()
    说明: 查询该Map是否为空(即不包含任何Key-value对), 如果为空则返回true
  7. Set KetSet()
    说明: 返回该Map中所有key组成的Set集合
  8. Object put(Object key, Object value)
    说明: t添加一个key-value 对, 如果当前Map中已有一个与该key相等的key-value对, 则新的 key-value对会覆盖原来的 key-value 对
  9. void putAll(Map m)
    说明: 将指定的Map中的key-value对复制到本Map中
  10. Object remove(Object key)
    说明: 删除指定key所对应的key-value对, 返回被删除key所关联的value, 如果key不存在, 则返回null
  11. boolean remove(Object key, Objcet value)
    说明: Java8增强的方法, 删除指定key,value所对应的key-value对, 如果从该Map中成功删除该key-value对, 该方法返回true, 否则返回false
  12. int size()
    说明: 返回该Map里的key-value对的个数
  13. Collection values()
    说明: 返回该Map里所有value组成的 Collection

Entry类包含的方法(Entry是一个内部类, 该类封装了一个key-value对):

  1. Objcet getKey()
    说明: 返回该Entry 里包含的key值
  2. Object getValue()
    说明: 返回该Entry里包含的value值
  3. Object SetValue(V value)
    说明: 设置该 Entry 里包含的value值, 并返回新设置的value值

示例代码

// 例子1:
import java.util.HashMap;
import java.util.Map;

public class MapTest{
    public static void main(String[] args) {
        Map map = new HashMap();
        // 成对的添加多个 key-value对
        map.put("Python", 50);
        map.put("Java", 30);
        map.put("JavaScript", 10);
        // 多次放入的 key-value 对中value可以重复
        map.put("Linux", 30);
        // 如果放入重复的key, 则新的value会覆盖原有的value
        // 如果新的value覆盖了原有的value, 该方法返回被覆盖的原有的value
        System.out.println(map.put("Python", 55));
        System.out.println(map);  // out:{Java=30, Linux=30, JavaScript=10, Python=55}
        // 判断是否包含指定的key
        System.out.println("是否包含 Python 的key: " + map.containsKey("Python"));
        // 判断是否包含指定的value
        System.out.println("是否包含 55 的value: " + map.containsValue(55));
        // 获取Map集合的所有key组成的集合, 通过遍历key来实现遍历所有的key-value对
        for (Object key : map.keySet()){
            // map.get(key)方法获取指定key对应的value
            System.out.println(key + " ---> " + map.get(key));
        }
        // 根据key来删除key-value对
        map.remove("Java"); 
        System.out.println(map);
    }
}
//程序说明:
    //1. 添加key-value时Map中已有重复的key, 那么新添加的value会覆盖该key原来对应的value,并将返回原来
    //    被覆盖的value
    //2. HashMap 重写了toString()方法, 实际上所有的Map实现类都重写了toString()方法, 调用Map对象的
    //    toString()方法总是返回固定格式的字符串: {key1=value1, hey2=value2...}

6.1 Java 8 为 Map 新增的方法

  1. Objcet 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
  2. Object computeIfAbsent(Object key, Function mappingFunction)
    说明: 如果传给该方法的key参数在Map中对应的value为null, 则使用 mappingFunction 根据key计算一个新的计算结果, 如果计算结果不为null, 则用计算结果覆盖原有的value; 如果原Map远离啊不包含该key, 那么该方法可能会添加一组key-value对
  3. Object computeIfPresent(Object key, BiFunction remappingFunction)
    说明: 如果传给该方法的key参数在Map中对应的value不为null, 该方法使用 remappingFunction 根据原 key,value计算一个新的结果, 如果计算结果不为null, 则使用该结果覆盖原来的value, 如果计算结果为null,删除原 key-value 对
  4. void forEach(BiConsumer action)
    说明: java8新增的, 可以更简洁的遍历Map的key-value对
  5. Object getOrDefault(Object key, V defaultValue)
    说明: 获取指定key对应的value,如果该key不存在, 则返回 defaultValue
  6. Object merge(Object key, Object value, BiFunction remappingFunction)
    说明: 该方法会先根据key参数获取该Map中对应的value; 如果获取的value为null, 则直接用传入的value覆盖原有的value(在这种情况下, 可能要添加一组key-value对); 如果获取的 value 为 null, 则使用 remappingFunction 函数根据原value, 新 value计算一个新的结果, 并用得到的结果去覆盖原有的value
  7. Object putIfAbsent(Object key, Object value)
    说明: 该方法自动检测指定key对应的value是否为null, 如果key对应的value为null, 该方法将会用新的value代替原来的null值
  8. Object replace(Object key, Object value)
    说明: 将Map中指定key对应的value替换成新value, 与传统的put()方法不同的是, 该方法不可能添加新的key-value 对, 如果尝试途欢的key在原Map中不存在, 该方法不是添加key-value对, 而是返回null
  9. replaceAll(BiFunction function)
    说明: 该方法使用 BiFunction 对原 key-value 对执行计算, 并将计算结果作为该 key-value 对的value值

示例代码

// 例子:
import java.util.HashMap;
import java.util.Map;

public class MapTest2 {
    public static void main(String[] args) {
        Map map = new HashMap();
        // 成对的添加多个key-value对
        map.put("Python", 50);
        map.put("Java", 30);
        map.put("JavaScript", 10);
        // 尝试替换key为 "newPython"的value, 由于原Map中没有对应的key
        // 因此Map没有改变, 不会添加新的key-value对
        map.replace("newPython", 55);
        System.out.println(map);  // out: {Java=30, JavaScript=10, Python=50}
        // 使用原value与传入参数计算出来的结果覆盖原有的value
        map.merge("Python", 50, (oldVal, param) -> (Integer)oldVal + (Integer)param);
        // Python 的值比原来增加了10
        System.out.println(map); // out: {Java=30, JavaScript=10, Python=100}
        // 当key为 "Java" 对应的value为null(或不存在)时, 使用计算的结果作为新value
        map.computeIfAbsent("newJava", (key) -> ((String)key).length());
        // map中添加了newJava=4这组key-value对
        System.out.println(map); // out: {Java=30, newJava=7, JavaScript=10, Python=100}
        // 当key为 "newJava"对应的vlaue存在时,使用计算的结果作为新value
        map.computeIfPresent("newJava", (key, value) -> (Integer)value * (Integer)value);
        // map 中newJava=4 变成 newJava=16
        System.out.println(map); // out: {Java=30, newJava=49, JavaScript=10, Python=100}
    }
}

6.2 Java8 改进的 HashMap 和 Hashtable 实现类

  1. HashMap 和 Hashtable 都是 Map 接口的典型实现类, 他们之间的关系类似于 ArrayList 和 Vector的关系
  2. Hashtable是一个古老的Map实现类, 它包含两个 elements()(类似于Map接口中定义的values()方法)和keys()(类似于Map接口中定义的keySet()方法), 现在很少使用这两个方法
  3. 为了成功地在 HashMap, Hashtable中存储, 获取对象, 用作key的对象必须实现hashCode()方法和equals()方法;
  4. HashMap, Hashtable, HashSet判断两个key相等的标准也是: 两个key通过equals()方法比较返回true, 两 个key的 hashCode值也相等(例子2)
  5. 当使用自定义类作为 HashMap, Hashtable的key时, 如果重写该类的equals(Object obj)和hashCode()方法, 则应该保证两个方法的判断标准一致:当两个 key 通过 equals() 方法比较返回 true 时, 两个key的hashCode()返回值也应该相同
  6. 与HashSet类似的是, 如果使用可变对象作为HashMap, Hashtable的key, 并且程序修改了作为key的可变对象则也可能出现与HashSet类似的情形: 程序再也无法准确访问到Map中被修改过的key(例子3)

HashMap 和 Hashtable 的区别:

  1. Hashtable 是一个线程安全的 Map 实现, 但HashMap是线程不安全的实现, 所以HashMap比Hashtable的性能高一点; 但如果有多个线程访问同一个Map对象时, 使用Hashtable实现类会更好;
  2. Hashtable不允许使用null作为key和value, 如果试图把null值放进Hashtable中, 将会引发NullPointerException异常; 但是HashMap可以使用null作为key或value
  3. 由于HashMap里的key不能重复, 所以HashMap里最多只有一个key-value对的key为null, 但可以有无数多个key-value对的value为null (例子1)
  4. 虽然HashMap线程不安全, 但是可以通过Collections工具类把HashMap变成线程安全
  5. HashMap 和 Hashtable 中包含一个 containsValue() 的方法,用于判断是否包含指定的value()
  6. HashMap 和 Hashtable 判断两个value相等的标准是: 两个对象通过equals()方法比较返回true (例子2)

示例代码

// 例子1:
public class NullInHashMap{
    public static void main(String[] args){
        HashMap 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);
    }
}

// 例子2:
import java.util.Hashtable;

class A {
    int count;
    public A(int count){
        this.count = count;
    }
    // 根据 count 的值来判断两个对象是否相等
    public boolean equals(Object obj){
        if (obj == this){
            return true;
        }
        if (obj != null && obj.getClass() == A.class){
            A a = (A)obj;
            return this.count == a.count;
        }
        return false;
    }
    // 根据count计算hashCode值
    public int hashCode(){
        return this.count;
    }
}
class B{
    // 重写equals()方法, B对象与任何对象通过equals()方法比较都返回true
    public boolean equals(Object obj){
        return true;
    }
}
public class HashtableTest{
    public static void main(String[] args){
        Hashtable ht = new Hashtable();
        ht.put(new A(6000), "疯狂Java讲义");
        ht.put(new A(87563), "轻量级Java EE 企业应用实战");
        ht.put(new A(1232), new B());
        System.out.println(ht);
        // 只要两个对象通过equals()方法比较返回true
        // Hashtable就认为他们是相等的value
        // 由于hashtable中有一个B对象
        // 它与任何对象通过equals()方法比较都想等, 所以下面输出true
        System.out.println(ht.containsValue("测试字符串"));   // out: ①
        // 只要两个A对象的count相等, 他们通过equals()方法比较返回ture, 且hashCode值相等
        // Hashtable即认为他们是相同的key, 所以下面输出true
        System.out.println(ht.containsKey(87563)); // out:②
        // 下面语句可以删除最后一对 key-value 对
        ht.remove(new A(1232));
        System.out.println(ht);
    }
}
// 程序说明:
//     1. A 类判断两个A对象相等的标准是equals()方法比较返回true即可, 上面程序中的ht对象中包含了一个B
//         对象, 他与任何对象通过equals()方法比较总是返回true, 所以在①处代码处返回true, 在这种情况下
//         不管传给ht对象的 containsValue()方法参数是什么, 程序总是返回true
//     2. 根据Hashtable判断两个key相等的标准, 程序在②处也将输出true,因为两个A对象虽然不是同一个对象, 
//         但他们通过equals()方法比较返回true, 且hashCode值相等, Hashtable即认为他们是同一个可以

// 例子3:
import java.util.HashMap;
import java.util.Iterator;

class A {
    int count;

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

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

    // 根据count计算hashCode值
    public int hashCode() {
        return this.count;
    }
}

public class HashMapErrorTest {
    public static void main(String[] args) {
        HashMap ht = new HashMap();
        ht.put(new A(6000), "疯狂Java讲义");
        ht.put(new A(87563), "轻量级Java EE 企业应用实战");
        // 获得Hashtable的key Set集合对应的 Iterator迭代器
        Iterator it = ht.keySet().iterator();
        // 取出Map中第一个key,并修改它的count值
        A first = (A)it.next();
        first.count = 87653; // out: ①
        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)));
        System.out.println(ht.get(new A(60000)));
    }
}

6.3 LinkedHashMap 实现类

  1. LinkedHashMap 使用双向链表来维护key-value对次序(其实只需要考虑key的次序), 该链表负责维护Map的迭代顺序, 迭代顺序与key-value的插入顺序保持一致
  2. LinkedHashMap 可以避免对 HashMap, Hashtable里的 key-value对进行排序(只要插入key-value对时保持顺序即可), 同时又可避免使用TreeMap 所增加的成本
  3. LinkedHashMap 需要维护元素的插入顺序, 因此性能略低于HashMap的性能, 但因为它以链表来维护内部顺序, 所以在迭代访问Map里的元素时将由较好的性能(例子1)

示例代码

// 例子1:
public class LinkedHashMapTest{
    public static void main(String[] args){
        LinkedHashMap 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));
    }
}

6.4 使用 Properties 读写属性文件

  1. Properties 类是 Hashtable 类的子类, 在处理属性文件时特别方便(如win平台的ini文件就是属性文件),
  2. Properties 类可以把Map对象和属性文件关联起来, 从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的 "属性名=属性值"加载到Map对象中
  3. Properties 类相当于一个key, value都是String类型的Map

Properties 类提供的三个方法:

  1. String getProperty(String key)
    说明: 获取 Properties 中指定属性名对应的属性值, 类似于Map的get(Object key)方法
  2. String getProperty(String key, String defaultValue)
    说明: 该方法与前一个方法基本相似, 该方法多一个功能, 如果Properties中不存在指定的key时, 则该方法指定默认值
  3. Objcet setProperty(String key, String value)
    说明: 设置属性值, 类似于Hashtable的put()方法
  4. void load(InputStream inStream)
    说明: 从属性文件(以输入流表示)中加载key-value对, 把加载到的key-value对追加到Properties里(Properties 是Hashtable的子类, 它不保证key-value对之间的次序)
  5. void store(OuputStream out, String comments)
    说明: 将 Properties 中的 key-value 对输出到指定的属性文件(以输出流表示)中

示例代码

// 例子
import java.io.*;
import java.util.Properties;

public class PropertiesTest {
    public static void main(String[] args) throws Exception{
        Properties props = new Properties();
        // 向 Properties 中添加属性
        props.setProperty("username", "jefxff");
        props.setProperty("password", "jefxff");
        // 将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);
    }
}

6.5 SortedMap 接口和 TreeMap 实现类

  1. treeMap 就是一个红黑树数据结构, 每个 key-value 即作为红黑树的一个节点;
  2. TreeMap 存储key-value对(节点)时, 需要根据key对节点进行排序; TreeMap可以保证所有的key-value对处于有序状态
  3. TreeMap判断两个key相等的标准: 两个key通过compareTo()方法返回0
  4. 如果使用自定义类作为TreeMap的key, 则应该保证: 两个key通过equals()方法比较返回true时, 他们通过CompareTo()方法比较应该返回0

TreeMap 两种排序方式:

  1. 自然排序: Treemap的所有key必须实现Comparable接口, 而且所有的key应该是同一类的对象, 否则将会抛出ClassCastException异常
  2. 定制排序: 创建 TreeMap 时, 传入一个 Comparator 对象, 该对象负责对 TreeMap中所有key进行排序, 采用定制排序时不要求Map的key实现Comparable接口

TreeMap类中的方法:

  1. Map.Entry firstEntry()
    说明: 返回该Map中最小的key所对应的key-value对, 如果Map为空, 则返回null
  2. Object firstKey()
    说明: 返回该Map中的最小的key值, 如果该Map为空, 则返回null
  3. Map.Entry lastEntry()
    说明: 返回该Map中最大的key所对应的key-value对, 如果Map为空或不存在这样的key-value, 则返回null
  4. Object lastKey()
    说明: 返回该Map中的最大的key值, 如果该Map为空或不存在这样的key, 则返回null
  5. Map.Entry higherEntry(Object key)
    说明: 返回该Map中位于key后一位的key-value对(即大于指定key的最小key所对应的key-value对), 如果该Map为空, 则返回null
  6. Object higherKey(Objcet ibj)
    说明: 返回该Map中位于key后一位的key值(即大于指定key的最小key值), 如果该Map为空或不存在这样的 key-value对, 则都返回null
  7. Object lowerEntry(Object key)
    说明: 返回该Map中位于key前一位的key-value对(即小于指定key的最大key值所对应的key-value对), 如果该Map为空或不存在这样的key-value对, 则都返回null
  8. Objcet lowerKey(Object obj)
    说明: 返回该Map中位于key前一位的key值(即小于指定key的最大key值), 如果该Map为空或不存在这样的 key值, 则都返回null
  9. NavigableMap subMap(Objcet fromKey, boolean fromInclusive, Object toKey, boolean toInclusive)
    说明: 返回该Map的子Map, 其key的范围是从fromKey(是否包括取决于第二个参数)到toKey(是否包括取决于第四个参数)
  10. SortedMap subMap(Object fromKey, Object toKey)
    说明: 返回该Map的子Map, 其key的范围是从fromKey(包括)到toKey(不包括)
  11. SortedMap tailMap(Object fromKey)
    说明: 返回该Map的子Map, 其key的范围是大于fromKey(包括)的所有key
  12. NavigableMap tailMap(Object fromKey, boolean inclusive)
    说明: 返回该Map的子Map, 其key的范围是大于fromkey(是否包括取决于第二个参数)的所有key
  13. SortedMap headMap(Object toKey)
    说明: 返回该Map的子Map, 其key的范围是小于toKey(不包括)的所有key
  14. NavigableMap headMap(Object fromKey, boolean inclusive)
    说明: 返回该Map的子Map, 其key的范围是小于toKey(是否包括取决于第二个参数)的所有key

示例代码

// 例子
import java.util.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 tm = new TreeMap();
        tm.put(new R(3), "Python");
        tm.put(new R(-5), "Java");
        tm.put(new R(9), "Linux");
        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.higherEntry(new R(2)));
        // 返回该TreeMap的比New R(2)小的最大的key-value 对 
        System.out.println(tm.lowerEntry(new R(2)));
        // 返回该 TreeMap 的子Map
        System.out.println(tm.subMap(new R(-1), new R(4)));
    }
}

6.6 WeakHashMap 实现类

  1. WeakHashMap 和 HashMap 的区别在于, HashMap 的 key保留了对实际对象的强引用, 这意味着只要该Hashmap对象不被销毁, 该HashMap的所有key所引用的对象就不会被垃圾回收, HashMap 也不会自动删除这些key所对应的key-value对; WeakHashMap 中的每个key对象只持有对实际对象的弱引用, 因此, 当垃圾回收了该key所对应的实际对象之后, WeakHashMap 会自动删除该key对应的 key-value 对

示例代码

// 例子1
public class WeakHashMapTest{
    public static void main(String[] args){
        WeakHashMap whm = new WeakHashMap();
        // 向 WeakHashMap 中添加三个 key-value对
        // 三个 key 都是匿名字符串对象(没有其他引用)
        whm.put(new String("语文"), new String("良好"));
        whm.put(new String("数学"), new String("及格"));
        whm.put(new String("英语"), new String("中等"));
        // 向 WeakHashMap 中添加一个 key-value 对 
        // 该 key 是一个系统缓存的字符串对象
        whm.put("java", new String("中等"));
        System.out.println(whm);
        // 通知系统回收垃圾
        System.gc();
        System.runFinalization();
        System.out.println(whm);
    }
}   
// 程序说明:
//     1. 添加前三个key-value对时, 这三个key都是匿名的字符串对象, WeakHashMap 只保留了他们的弱引用, 
//         这样垃圾回收时会自动的删除这三个 key-value对 
//     2. 第四句put代码因为key时一个字符串直接量(系统会自动保留对该字符串对象的强引用), 所以垃圾回收时
//         不会回收它

6.7 IdentityHashMap 实现类

  1. HashMap只要两个key通过equals()返回true,即可判定两个key相等, 而 IdentityHashMap 执行严格的相等(key1 == key2)时, 才认为两个key相等
  2. IdentityHashMap 提供了与 HashMap基本相似的方法, 也允许使用null作为key和value; 与 HashMap相似:IdentityHashMap 也不保证key-value对之间的顺序, 更不能保证他们的顺序随时间的退役保持不变

示例代码

// 例子
public class IdentityHashMapTest{
    public static void main(String[] args){
        IdentityHashMap ihm = new IdentityHashMap();
        // 下面两对key-value通过 == 比较不相等, 所以不是相同的key, 两个都可以添加
        ihm.put(new String("语文", 89));
        ihm.put(new String("语文", 78));
        // 下面的key是直接量, 所以通过 == 比较相等, 所以是相同的key, 只会添加一对key-value 
        ihm.put("java", 96));
        ihm.put("java", 89));
        System.out.println(ihm);
    }
} 

6.8 EnumMap 实现类

  1. EnumMap中的所有key都必须是单个枚举类的枚举值, 创建EnumMap时必须显式或隐式的指定它对应的枚举类
  2. EnumMap 在内部以数组的形式保存, 所以这种实现形式非常紧凑, 高效
  3. EnumMap 根据 key 的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对之间的顺序
  4. EnumMap 不允许使用null作为key, 但允许使用null作为value

示例代码

// 例子
enum Season{
    SPRING, SUMMER, FALL, WINTER
}
public class EnumMapTest{
    public static void main(String[] args){
        // 创建 EnumMap 对象, 该 EnumMap 的所有key都是 Season 枚举类的枚举值
        EnumMap em = new EnumMap(Season.class);
        em.put(Season.SUMMER, "夏日炎炎");
        em.put(Season.SPRING, "春暖花开");
        System.out.println(em);
    }
}

6.9 各Map实现类的性能分析

  1. HashMap 和 Hashtable 的实现机制几乎一样, 但是Hashtable比较古老,集成了线程安全, 因此 HashMap 通常比 Hashtable 快
  2. 由于TreeMap采用红黑树来管理key-value对, 所以TreeMap要比HashMap 和 Hashtable慢(尤其是插入, 删除key-value对时)
  3. TreeMap一个好处就是其中的key-value对总是处于排序状态; 应用场景: 当TreeMap被填充之后, 调用keySet()取得key组成的Set, 再使用 toArray()方法生成key的数组, 接下来使用Arrays()的 binarySearch()方法在已排序的数组中快速的查询对象
  4. 一般的应用场景应该多使用HashMap, 因为其底层也是通过数组来存储key-value对的; 如果程序需要一个排好序的Map时, 则可以使用TreeMap
  5. LinkedHashMap 比 HashMap 要慢, 因为 LinkedHashMap 需要维护链表来保持Map中key-value的添加顺序
  6. EnumMap 的性能最好, 但是它只能使用同一个枚举类的枚举值作为key

7. 操作集合的工具类 Collections

7.1 List集合元素的排序操作

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

示例代码

// 例子
import java.util.ArrayList;
import java.util.Collections;

public class SortTest {
    public static void main(String[] args){
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(3);
        nums.add(12);
        nums.add(-2);
        nums.add(99);
        nums.add(0);
        System.out.println(nums);  // out: [2, 3, 12, -2, 99, 0]
        // 反转
        Collections.reverse(nums);
        System.out.println(nums);  // out: [0, 99, -2, 12, 3, 2]
        // 自然排序
        Collections.sort(nums);
        System.out.println(nums);  // out: [-2, 0, 2, 3, 12, 99]
        // 洗牌
        Collections.shuffle(nums);
        System.out.println(nums);  // out: [0, 12, 2, -2, 99, 3]
    }
}

// 例子2:
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;


// 梭哈游戏
public class ShowHand{
    // 定义该游戏最多支持多少个玩家
    private final int PLAY_NUM = 5;
    // 定义扑克牌的所有花色和数值
    private String[] types = {"方块", "草花", "红心", "黑桃"};
    private String[] values = {"2", "3", "4", "5", 
        "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
    // cards 是一局游戏中剩下的扑克牌
    private List<String> cards = new LinkedList<String>();
    // 定义所有的玩家
    private String[] players = new String[PLAY_NUM];
    // 定义所有玩家手上的扑克牌
    private List<String>[] playerCards = new List[PLAY_NUM];
    /**
    * 初始化扑克牌, 放入52张牌
    * 并且使用shuffle方法模拟洗牌
    */
    public void initCards(){
        for (int i = 0; i < types.length; i++){
            for (int j = 0; j < values.length; j++){
                cards.add(types[i] + values[j]);
            }
        }
        // 随机排列
        Collections.shuffle(cards);
    }
    /**
    *  初始化玩家为每个玩家分配用户名
    */
    public void initPlayer(String... names){
        if (names.length > PLAY_NUM || names.length < 2){
            // 检验玩家数量
            System.out.println("玩家数量不对!");
        }
        else{
            // 初始化玩家用户名
            for (int i = 0; i < names.length; i ++){
                players[i] = names[i];
            }
        }
    }
    /**
    * 初始化玩家手上的牌, 开始游戏时每个玩家手上的扑克牌为空
    *  程序使用一个长度为0的LinkedList来表示
    */
    public void initPlayerCards(){
        for (int i = 0; i < players.length; i++){
            if (players[i] != null && !players[i].equals("")){
                playerCards[i] = new LinkedList<String>();
            }
        }
    }
    /**
    * 输出全部扑克牌, 测试
    */
    public void showAllCards(){
        for (String card : cards){
            System.out.println(card);
        }
    }
    /**
    * 派扑克牌
    * @param first 最先派给谁
    */
    public void deliverCard(String first){
        // 调用ArrayUtils 工具类的search方法
        // 查询处指定元素在数组中的索引
        int firstPos = ArrayUtils.search(players, first);
        for (int i = firstPos; i < PLAY_NUM; i++){
            if (players[i] != null){
                playerCards[i].add(cards.get(0));
                cards.remove(0);
            }
        }
        // 依次给位于该指定玩家之前的每位玩家派发扑克
        for (int i = 0; i < firstPos; i++){
            if (players[i] != null){
                playerCards[i].add(cards.get(0));
                cards.remove(0);
            }
        }
    }
    /**
    * 输出玩家手上的扑克牌
    * 实现该方法时, 应该控制每个玩家看不到别人的第一张牌, 没实现
    */
    public void showPlayerCards(){
        for (int i = 0; i < PLAY_NUM; i++){
            //当玩家不为空时
            if (players[i] != null){
                // 输出玩家
                System.out.println(players[i] + ": ");
                // 遍历玩家手上的扑克牌
                for (String card : playerCards[i]){
                    System.out.println(card + "\t");
                }
            }
            System.out.println("\n");
        }
    }
    public static void main(String[] args) {
        ShowHand sh = new ShowHand();
        sh.initPlayer("电脑玩家", "孙悟空");
        sh.initCards();
        sh.initPlayerCards();
        // 测试所偶遇扑克牌
        sh.showAllCards();
        System.out.println("---------------------------");
        // 下面从孙悟空开始派牌
        sh.deliverCard("孙悟空");
        sh.showPlayerCards();
        /**
        * 需要补充的内容:
        *     1. 牌面最大的玩家下注
        *     2. 其他玩家是否跟注
        *     3. 游戏是否只剩下一个玩家? 如果是, 则他胜利
        *     4. 如果已经是最后一张扑克牌, 则需要比较剩下玩家的牌面大小
        */
        // 再次从 "电脑玩家" 开始派牌
        sh.deliverCard("电脑玩家");
        sh.showPlayerCards();
    }
}

7.2 用于查找, 替换操作集合元素的类方法

  1. int binarySearch(List list, Object key)
    说明: 使用二分搜索发搜索指定List集合, 以获得指定对象在List集合中的索引; 前提是List必须是有序的
  2. Object max(Collection coll)
    说明: 根据元素的自然顺序, 返回给定集合中的最大元素
  3. Object max(Collection coll, Comparator comp)
    说明: 根据 Comparator 指定的顺序, 返回给定集合中的最大元素
  4. Object min(Collection coll)
    说明: 根据元素的自然顺序, 返回给定集合中的最小元素
  5. Object min(Collection coll, Comparator comp)
    说明: 根据 Comparator 指定的顺序, 返回给定集合中的最小元素
  6. void fill(List list, Object obj)
    说明: 使用指定元素obj替换指定List集合中的所有元素
  7. int frequency(Collection c, Object o)
    说明: 返回指定集合中指定元素出现的次数
  8. int indexOfSubList(List source, List target)
    说明: 返回子List对象在父List对象中第一次出现出现的位置索引, 如果父List没有出现这样的List,则返回-1
  9. int lastIndexOfSubList(List source, List target)
    说明: 返回子List对象在父List对象中最以一次出现出现的位置索引, 如果父List没有出现这样的List,则返回-1
  10. boolean replaceAll(List list, Object oldVar, Objcet newVar)
    说明: 使用一个新值newVar替换List对象的所有旧值oldVar

示例代码

// 例子
import java.util.ArrayList;
import java.util.Collections;

public class SearchTest {
    public static void main(String[] args){
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(3);
        nums.add(12);
        nums.add(-2);
        nums.add(99);
        nums.add(0);
        System.out.println(nums);  // out: [2, 3, 12, -2, 99, 0]
        System.out.println(Collections.max(nums));  // out: 99
        System.out.println(Collections.min(nums));  // out: -2
        //使用55来代替nums中的0
        Collections.replaceAll(nums, 0, 55);
        System.out.println(nums);  // out: [2, 3, 12, -2, 99, 55]
        // 判断-2在List集合中出现的次数
        System.out.println(Collections.frequency(nums, -2));  // out: [-2, 2, 3, 12, 55, 99]
        Collections.sort(nums);  // out:
        System.out.println(nums);
        System.out.println(Collections.binarySearch(nums, 99));  // out: 5
    }
}

7.3 同步控制

  1. Collections工具类提供了多个synchronizedXxx()方法, 该方法可以将指定集合包装成线程同步的集合, 从而可以解决多线程并发访问集合时的线程不安全问题
  2. Java中常用的集合框架中的实现类HashSet, TreeSet, ArrayList, ArrauDeque, LinkedList, HashMap, 和 TreeMap都是线程不安全的; 如果有多个线程访问他们, 而且有超过一个的线程试图修改它们, 则存在线程安全的问题

示例代码

// 例子      
// 创建四个线程安全(或线程同步)的集合对象
public class SynchronizedTest{
    public static void main(String[] args){
        Collection c = Collections.SynchronizedCollection(new ArrayList());
        List list = Collections.SynchronizedList(new ArrayList());
        Set s = Collections.synchronizedSet(new HashSet());
        Map a = Collections.synchronizedMap(new HashMap());
    }
}

7.4 设置不可变集合

Collections提供了三类方法来返回一个不可变的集合(只读版本)

  1. emptyXxx()
    说明: 返回一个空的, 不可变的集合对象, 此处的集合既可以是List, 也可以是SortedSet或Set, 还可以是Map或者SortedMap
  2. singletonXxx()
    说明: 返回一个指包含指定对象(只有一个或一项元素)的, 不可变的集合对象, 集合可以是List或者Map
  3. unmodifiableXxx()
    说明: 返回指定集合对象的不可变视图, 此处的集合既可以是List, 也可以是SortedSet或Set, 还可以是Map或者SortedMap

示例代码

// 例子 
public class UnmodifiableTest{
    public static void main(String[] args){
        // 创建一个空的, 不可变的List对象
        List unmodifiableList = Collections.emptyList();
        // 创建一个只有一个元素, 且不可改变的Set集合
        Set unmodifiableSet = Collections.singleton("Python");
        // 创建一个普通的Map对象
        Map scores = new HashMap();
        scores.put("语文", 80);
        scores.put("Java", 82);
        // 返回普通的Map对象对应的不可变版本
        Map unmodifiableMap = Collections.unmodifiableMap(scores);
        // 下面任意一行代码都将引发UnsupportedOperationException异常
        // unmodifiableList.add("测试元素");
        // unmodifiableSet.add("测试元素");
        // unmodifiableMap.put("英语", 59);
    }
}

# Java