Java讲义第八章学习笔记

chapter  8   Java集合

8.1  类的加载、连接和初始化

  集合类主要负责保存、盛装其他数据,因此集合类也称为容器类。所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent包下提供了一些多线程支持的集合类。

  Java的集合类主要由两个接口派生而出:CollectionMap,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。

 

 

 

 

 

 

 

8.2 Java 11增强的Collection和Iterator接口

  Collection接口里定义了如下操作集合元素的方法。
    ➢ boolean add(Object o):该方法用于向集合里添加一个元素。
    ➢ boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合里。
    ➢ 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():该方法返回集合里元素的个数
  Java 11为Collection新增了一个toArray(IntFunction)方法,使用该方法的主要目的就是利用泛型。对于传统的toArray()方法而言,不管Collection本身是否使用泛型,toArray()的返回值总是Object[];但新增的toArray(IntFunction)方法不同,当Collection使用泛型时,toArray(IntFunction)可以返回特定类型的数组。
 
public class CollectionTest {
    public static void main(String[] args) {
        Collection 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 books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        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);

    }
}

输出结果:

c集合的元素个数为:2
c集合的元素个数为:1
c集合是否包含"孙悟空"字符串:true
c集合的元素:[孙悟空, 轻量级java ee企业应用实战]
c集合是否包含books集合?false
c集合的元素:[孙悟空]
c集合里的元素:[]
books 集合的元素:[]

 

--8.2.1  使用Lambda表达式遍历集合

  Java 8为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口。正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素。

public class CollectionEach {
    public static void main(String[] args) {
        //创建一个集合
        Collection books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");
        //调用forEach()方法遍历集合
        books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

运行结果:

迭代集合元素:疯狂Android讲义
迭代集合元素:轻量级java ee企业应用实战
迭代集合元素:疯狂Java讲义

 

--8.2.2  使用Iterator遍历集合元素

 

  Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。

 

  Iterator接口里定义了如下4个方法。

 

    ➢ boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true。

 

    ➢ Object next():返回集合里的下一个元素。

 

    ➢ void remove():删除集合里上一次next方法返回的元素。

 

    ➢ void forEachRemaining(Consumer action),这是Java 8为Iterator新增的默认方法,该方法可使用Lambda表达式来遍历集合元素。

 

  Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。
 
遍历:
public class IteratorTest {
    public static void main(String[] args) {
        //创建集合、添加元素的代码与钱一个程序相同
        Collection books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");

        Iterator it = books.iterator();
        while (it.hasNext()){
            //it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
            String book = (String)it.next();
            System.out.println(book);
            if(book.equals("疯狂Java讲义")){
                //从集合中删除上一次next()方法返回的元素
                it.remove();
            }
            //对book 变量赋值,不会改变集合元素本身
            book = "测试字符串";  //1
        }
        System.out.println(books);
    }
}

 结果:

疯狂Android讲义
轻量级java ee企业应用实战
疯狂Java讲义
[疯狂Android讲义, 轻量级java ee企业应用实战]

  从上面代码可以看出,Iterator 仅用于遍历集合,Iterator本身并不提供盛装对象的能力,如果需要创建Iterator对象,则必须有一个被迭代的集合。没有集合的Iterator仿佛无本之木,没有存在价值。

  当使用Iterator对集合元素进行迭代时,Iterator并不是把集合元素本身传递给了迭代变量,而是把集合元素的值传递给了迭代变量。

  当使用Iterator 迭代访问 Collection 集合元素时,Collection 集合里的元素不能被改变,只能通过Iterator 的 remove()方法删除上一次 next()方法返回的集合元素才可以;否则将会引发java.util.ConcurrentModificationException异常。

 

public class IteratorErrorTest {
    public static void main(String[] args) {
        Collection books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");

        Iterator it = books.iterator();
        while (it.hasNext()) {
            //it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
            String book = (String) it.next();
            System.out.println(book);
            if (book.equals("疯狂Android讲义")) {
                //使用 Iterator 迭代过程中,不可修改集合元素,下面代码引发异常
                books.remove(book);
            }
        }
    }
}

运行结果:

疯狂Android讲义
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
    at java.util.HashMap$KeyIterator.next(HashMap.java:1469)
    at IteratorErrorTest.main(IteratorErrorTest.java:15)

 

--8.2.3  使用Lambda表达式遍历Iterator

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

public class IteratorEach {
    public static void main(String[] args) {
        Collection books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");

        Iterator it = books.iterator();
        //使用Lambda表达式
        it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

结果:

迭代集合元素:疯狂Android讲义
迭代集合元素:轻量级java ee企业应用实战
迭代集合元素:疯狂Java讲义

 

--8.2.4  使用foreach循环遍历集合元素

  Java 5提供的foreach循环迭代访问集合元素更加便捷。

public class ForeachTest {
    public static void main(String[] args) {
        Collection books = new HashSet();
        books.add("轻量级java ee企业应用实战");
        books.add("疯狂Java讲义");
        books.add("疯狂Android讲义");

        for (Object obj : books){
            //此处的book变量也不是集合元素本身
            String book = (String)obj;
            System.out.println(book);
            if(book.equals("疯狂Android讲义")){
                //下面代码会引发ConcurrentModificationException异常
                books.remove(book);
            }
        }
    }
}

 

--8.2.5  使用Predicate操作集合

  Java 8为Collection集合新增了一个removeIf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,因此可使用Lambda表达式作为参数。

 

--8.2.6 使用Stream操作集合

  Java 8还新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。
  Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。
  独立使用Stream的步骤如下:
    ①使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
    ②重复调用Builder的add()方法向该流中添加多个元素。
    ③调用Builder的build()方法获取对应的Stream。
    ④调用Stream的聚集方法。
  在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。
public class InputStreamTest {
    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);
        //使用方法引用的方式来遍历集合元素
        newIs.forEach(System.out::println);
    }
}
 
  Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”(intermediate),也可以是“末端的”(terminal)。
    ➢ 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。
    ➢ 末端方法:末端方法是对流的最终操作。
  除此之外,关于流的方法还有如下两个特征。
    ➢ 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
    ➢ 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
 
  下面简单介绍一下Stream常用的中间方法。
   ➢ filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。
   ➢ mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
   ➢ peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法用于调试。
   ➢ distinct():该方法用于排序流中所有重复元素(判断元素重复的标准是使用equals()比较返回true),这是一个有状态的方法。
   ➢ sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
   ➢ limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
 
  下面简单介绍一下Stream常用的末端方法。
   ➢ forEach(Consumer action):遍历流中所有元素,对每个元素执行action。
   ➢ toArray():将流中所有元素转换为一个数组
   ➢ reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
   ➢ min():返回流中所有元素的最小值。
   ➢ max():返回流中所有元素的最大值。
   ➢ count():返回流中所有元素的数量。
   ➢ anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
 
  除此之外,Java8允许使用流式API来操作集合。
public class CollectionStream {
    public static void main(String[] args) {
        //创建 books集合
        Collection books = new HashSet();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂IOS讲义"));
        books.add(new String("疯狂Ajax讲义"));
        books.add(new String("疯狂Android讲义"));
        //统计书名包含”疯狂“字串的图书数量
        System.out.println(books.stream().filter(ele -> ((String)ele).contains("疯狂"))
        .count());  //4
        //统计书名包含"Java"字串的图书数量
        System.out.println(books.stream().filter(ele -> ((String)ele).contains("Java"))
                .count());  //2
        //统计书名字符长度大于10的图书数量
        System.out.println(books.stream().filter(ele -> ((String)ele).length() > 10)
        .count());  //输出2
        //先调用Collection对象的stream()方法将集合转换为Stream
        //再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
        books.stream().mapToInt(ele -> ((String)ele).length()).forEach(System.out::print);  //Lambda表达式,方法引用
    }
}

输出结果:

4
2
2
8711168

 

8.3 Set集合

  Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。

 

--8.3.1  HashSet类

  HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。
HashSet具有以下特点。
  ➢ 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  ➢ HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。
  ➢ 集合元素值可以是null。
 
  当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象 hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但他们的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) {
            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);
        }
    }

存储结果:

[B@1, B@1, C@2, A@7f31245a, A@14ae5a5]

  上述结果看出 A 类存储两个, B类存储两个,C类只存储一个。

  HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,且两个对象的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) {
            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;  //1
        //再次输出HashSet集合,集合元素有重复元素
        System.out.println(hs);
        //删除count为-3的R对象
        hs.remove((new R(-3)));  //2
        //可以看到删除了一个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
    }
}

运行结果:

[R[count:-2], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:-3], R[count:5], R[count:9]]
[R[count:-3], R[count:5], R[count:9]]
hs是否包含count 为-3的R对象?false
hs是否包含count 为-2的R对象?false

  上面程序提供了R类,R类重写了equals(Object obj)方法和hashCode()方法,这两个方法都是根据R对象的count 实例变量来判断的。上面程序 //1 处改变了Set集合中第一个R对象的count实例变量的值,这将导致该R对象与集合中的其他对象相同。

  如运行结果,HashSet集合中第一个元素和第二个元素完全相同,这表明两个元素已经重复。此时HashSet会比较混乱:当试图删除count为 -3 的R对象时,HashSet会计算出该对象的HashCode值,从而找出该对象在集合中的保存位置,然后把此处的对象与count为-3的R对象通过equals()方法进行比较,如果相等则删除该对象 ——HashSet 只有第2个元素才满足该条件(第一个元素实际上保存在count =  -2的R对象对应的位置),所以第二个元素被删除。至于第一个count为-3的R对象,他保存在count为-2的R对象对应的位置,但使用equals()方法拿他和count()为-2的R对象比较时有返回false —— 这将导致HashSet 不可能准确访问该元素。

 

--8.3.2  LinkedHashSet类

  HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

public class LinkedHashSetTest {
    public static void main(String[] args) {
        LinkedHashSet books = new LinkedHashSet();
        books.add("疯狂Java讲义");
        books.add("轻量级java ee企业应用实战");
        System.out.println(books);
        //删除 疯狂Java讲义
        books.remove("疯狂Java讲义");
        //重新添加 疯狂Java讲义
        books.add("疯狂Java讲义");
        System.out.println(books);
    }
}

输出结果:

[疯狂Java讲义, 轻量级java ee企业应用实战]
[轻量级java ee企业应用实战, 疯狂Java讲义]

  输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致。

--8.3.3  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的元素组成。

 

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet 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]
    }
}

  与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。

  1.自然排序

  TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。

  Java提供了一个Comparable 接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个负整数,则表明obj1 小于 obj2。

  如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常。

public class Err {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        //向TreeSet集合中添加两个Err对象
        ts.add(new Err());
        ts.add(new Err());
    }
}

  上面程序试图向TreeSet集合中添加两个Err对象,添加第一个对象时,TreeSet里没有任何元素,所以不会出现任何问题;当添加第二个Err对象时,TreeSet就会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较——如果其对应的类没有实现Comparable接口,则会引发ClassCastException异常。

  还有一点:大部分类在实现compareTo(Object obj)方法时,都需要将被比较对象obj强制类型转换成相同类型,因为只有相同类的两个实例才会比较大小。

  如果向TreeSet中添加一个可变对象后,并且后面程序修改了该可变对象的实例变量,这将导致它与其他对象的大小顺序发生了改变,但TreeSet不会再次调整他们的顺序,甚至可能导致TreeSet 中保存的这两个对象通过compareTo(Object obj)方法比较返回0。

class R1 implements Comparable{
    int count;
    public R1(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() == R1.class){
            R1 r = (R1)obj;
            return r.count == this.count;
        }
        return false;
    }

    public int compareTo(Object obj) {
        R1 r = (R1)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 R1(5));
        ts.add(new R1(-3));
        ts.add(new R1(9));
        ts.add(new R1(-2));
        //打印TreeSet 集合,集合元素是有序排列的
        System.out.println(ts);  //1
        //取出第一个元素
        R1 first = (R1)ts.first();
        //对第一个元素的count赋值,与第二个元素的count相同
        first.count = 20;
        //取出最后一个元素
        R1 last = (R1)ts.last();
        //对最后一个元素的count赋值,与第二个元素的count相同
        last.count = -2;
        //再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts); //2
        //删除实例变量被改变的元素,删除失败
        System.out.println(ts.remove(new R1(-2))); //3
        System.out.println(ts);
        //删除实例变量没有被改动的元素,删除成功
        System.out.println(ts.remove(new R1(5)));  //4
        System.out.println(ts);
    }
}

运行结果:

[R[count:-3], R[count:-2], R[count:5], R[count:9]]
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
false
[R[count:20], R[count:-2], R[count:5], R[count:-2]]
true
[R[count:20], R[count:-2], R[count:-2]]

  通过上面程序得知,尽量不要修改TreeSet,HashSet中的实例变量。

 

  2.定制排序

  TreeSet 的自然排序是根据集合元素的大小,TreeSet将他们以升序排序。如果需要实现定制排序,例如以降序排序,则可以通过Comparator接口的帮助。该接口里包含一个 int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2。

  如果需要实现定制排序,则需要在创建TreeSet 集表达式来代替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 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));
        System.out.println(ts);
    }

}

运行结果:

[M[age:9], M[age:5], M[age:-3]]

 

--8.3.4  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集合。
 
常规用法:
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]
    }
}

 

复制:

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 enumSet = EnumSet.copyOf(c);  //1
        System.out.println(enumSet);  //输出[SPRING,FALL]
        c.add("疯狂Java讲义");
        c.add("轻量级 JAVA EE 企业应用实战");
        //下面代码出现异常:因为c集合里的元素不是全部都为枚举值
        enumSet = EnumSet.copyOf(c);  //2
    }
}

运行结果:

[SPRING, FALL]
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Enum
    at java.util.RegularEnumSet.add(RegularEnumSet.java:36)
    at java.util.EnumSet.copyOf(EnumSet.java:179)
    at EnumSetTest2.main(EnumSetTest2.java:17)

 

--8.3.5  各Set实现类的性能分析

  HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
  HashSet还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。
  EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
  必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。
  如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类的synchronizedSortedSet方法来“包装”该Set集合。
 
8.4  List集合
  List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。
 
--8.4.1 改进的List接口和ListIterator接口
  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集合的元素排序。
 
public class ListTest {
    public static void main(String[] args) {
        List books = new ArrayList();
        //向books集合中添加三个元素
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂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讲义")));  //1
        //将第二个元素替换成新的字符串对象
        books.set(1,new String("疯狂Java讲义"));
        System.out.println(books);
        //将books集合的第二个元素(包括)
        //到第三个元素(不包括)截取成子集合
        System.out.println(books.subList(1,2));
    }
}

运行结果:

[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
轻量级Java EE企业应用实战
疯狂Ajax讲义
疯狂Java讲义
疯狂Android讲义
[轻量级Java EE企业应用实战, 疯狂Ajax讲义, 疯狂Android讲义]
1
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义]

 

  Java 8为List集合增加了sort()和replaceAll()两个常用的默认方法,其中sort()方法需要一个Comparator对象来控制元素排序,程序可使用Lambda表达式来作为参数;而replaceAll()方法则需要一个UnaryOperator来替换所有集合元素,UnaryOperator也是一个函数式接口,因此程序也可使用Lambda表达式作为参数。

public class ListTest3 {
    public static void main(String[] args) {
        List books = new ArrayList();
        //向books 集合中添加4个元素
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂Android讲义"));
        books.add(new String("疯狂IOS讲义"));
        //使用目标类型为Comparator的Lambda表达式对List集合排序
        books.sort((o1,o2)->((String)o1).length() - ((String)o2).length());
        System.out.println(books);
        //使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
        //该Lambda表达式 控制使用每个字符串的长度作为新的集合元素
        books.replaceAll(ele ->((String)ele).length());
        System.out.println(books);  //输出[7,8,11,16]
    }
}
 
  与Set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。
  ListIterator接口在Iterator接口基础上增加了如下方法。
    ➢ boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
    ➢ Object previous():返回该迭代器的上一个元素。  //对比 Iterator新增
    ➢ void add(Object o):在指定位置插入一个元素。
 
public class ListIteratorTest {
    public static void main(String[] args) {
        String[] books = {
                "疯狂Java讲义","疯狂IOS讲义",
                "轻量级Java EE企业应用实战"
        };
        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());
        }
    }
}

运行结果:

疯狂Java讲义
疯狂IOS讲义
轻量级Java EE企业应用实战
======下面开始反向迭代=======
-------分隔符--------
轻量级Java EE企业应用实战
-------分隔符--------
疯狂IOS讲义
-------分隔符--------
疯狂Java讲义

 

--8.4.2  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参数,则Object[]数组的长度默认为10。
 
  除此之外,ArrayList和Vector还提供了如下两个方法来重新分配Object[]数组。
    ➢ void ensureCapacity(int minCapacity):将ArrayList或Vector集合的Object[]数组长度增加大于或等于minCapacity值。
    ➢ void trimToSize():调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。调用该方法可减少ArrayList或Vector集合对象占用的存储空间。
  
  ArrayList和Vector在用法上几乎完全相同,ArrayList和Vector的显著区别是:
  ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但Vector集合则是线程安全的,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也同样不推荐使用Vector实现类。后面会介绍一个Collections工具类,它可以将一个ArrayList变成线程安全的。
 
  Vector还提供了一个Stack子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。
  Stack类里提供了如下几个方法。
    ➢ Object peek():返回“栈”的第一个元素,但并不将该元素“pop”出栈。
    ➢ Object pop():返回“栈”的第一个元素,并将该元素“pop”出栈。
    ➢ void push(Object item):将一个元素“push”进栈,最后一个进“栈”的元素总是位于“栈”顶。需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它同样是线程安全的、性能较差的,因此应该尽量少用Stack类。如果程序需要使用“栈”这种数据结构,建议使用后面将要介绍的ArrayDeque代替它。
 
--8.4.3 固定长度的List
  前面讲数组时介绍了一个操作数组的工具类:Arrays,该工具类里提供了asList(Object...a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。
public class FixedSizeList {
    public static void main(String[] args) {
        List fixedList = Arrays.asList("疯狂Java讲义","轻量级Java EE企业应用实战");
        //获取 fixedList 的实现类,将输出Arrays$ArrayList
        System.out.println(fixedList.getClass());
        //使用方法引用遍历集合元素
        fixedList.forEach(System.out::println);
        //试图增加、删除元素都会引发UnsupportedOperationException异常
        fixedList.add("疯狂Android讲义");
        fixedList.remove("疯狂Java讲义");
    }
}

 

8.5 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():获取队列头部的元素,并删除该元素。
  Queue接口有一个PriorityQueue实现类。除此之外,Queue还有一个Deque接口,Deque代表一个“双端队列”,双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列使用,也可当成栈使用。Java为Deque提供了ArrayDeque和LinkedList两个实现类。
 
--8.5.1  PriorityQueue实现类
  PriorityQueue是一个比较标准的队列实现类。PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。
 
public class PriorityQueueTest {
    public static void main(String[] args) {
        PriorityQueue 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());
    }
}

运行结果:

[-3, 6, 20, 18]
-3
  PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,PriorityQueue的元素有两种排序方式。
  
  ➢ 自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
  ➢ 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。PriorityQueue队列对元素的要求与TreeSet对元素的要求基本一致。
 
--8.5.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不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。

  Deque的方法与Queue的方法对照表如表8.2所示。

  Deque的方法与Stack的方法对照表如表8.3所示。

 
  Deque接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。
 
堆栈:
public class ArrayDequeStack {
    public static void main(String[] args) {
        ArrayDeque stack = new ArrayDeque();
        //依次将三个元素 push 入"栈"
        stack.push("疯狂Java讲义");
        stack.push("轻量级 Java EE 企业应用实战");
        stack.push("疯狂Android讲义");
        //输出:[疯狂Java讲义,轻量级 Java EE 企业应用实战,疯狂Android讲义]
        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);
    }
}

队列:

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

 

 
--8.5.3  LinkedList实现类
  LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成“栈”来使用,也可以当成队列使用。
public class LinkListTest {
    public static void main(String[] args) {
        LinkedList books = new LinkedList();
        //将字符串元素加入队列的尾部
        books.offer("疯狂Java讲义");
        //将一个字符串元素加入到栈的顶部
        books.push("轻量级 Java EE 企业应用实战");
        //将字符串元素添加到队列头部(相当于栈的顶部)
        books.offerFirst("疯狂Android讲义");
        //以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());
        //下面输出:[轻量级 Java EE 企业应用实战]
        System.out.println(books);
    }
}

输出结果:

遍历中:疯狂Android讲义
遍历中:轻量级 Java EE 企业应用实战
遍历中:疯狂Java讲义
疯狂Android讲义
疯狂Java讲义
疯狂Android讲义
[轻量级 Java EE 企业应用实战, 疯狂Java讲义]
疯狂Java讲义
[轻量级 Java EE 企业应用实战]

对比:

  ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色(只需改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能(而且实现机制也不好),所以各方面性能都比较差。

 

--8.5.4  各种线性表的性能分析

  Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。
  Queue代表了队列,Deque代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。初学者可以无须理会ArrayList和LinkedList之间的性能差异,只需要知道LinkedList集合不仅提供了List的功能,还提供了双端队列、栈的功能就行。一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑使用ArrayList。
  关于使用List集合有如下建议。
    ➢ 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素。
    ➢ 如果需要经常执行插入、删除操作来改变包含大量数据的List集合的大小,可考虑使用LinkedList集合。使用ArrayList、Vector集合可能需要经常重新分配内部数组的大小,效果可能较差。
    ➢ 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。
 
8.6 增强的Map集合
  Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的value。
 
  常用方法:
    1.void clear(): 删除该Map对象中的所有key-value对。
    2.boolean containsKey(Object key):查询Map中是否包含指定的key,如果包含则返回true。
    3.boolean containsValue(Object value):查询Map中是否包含一个或多个valu额,如果包含则返回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是否为空,如果为空,返回true。
    7.Set keySet():返回该 Map 中所有key 组成的 Set 集合。
    8.Object put(Object key, Object value): 添加一个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,Object value):这是Java8新增的方法,删除指定的key、value所对应的key-value对。成功删除则返回true。
    12.int size(): 返回该Map里的key-value对个数。
    13.Collection values(): 返回该Map里所有value组成的Collection。
 
  Map 接口提供了大量的实现类,典型实现如 HashMap 和 HashTable 等、HashMap 的子类LinkedHashMap,还有SortedMap 子接口及该接口的实现类TreeMap,以及WeakHashMap、IdentityHashMap等。
 
public class MapTest {
    public static void main(String[] args) {
        Map 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(Object 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对
    }
}

运行结果:

10
{疯狂Ajax讲义=79, 轻量级 Java EE 企业应用实战=99, 疯狂IOS讲义=99, 疯狂Java讲义=109}
是否包含值为 疯狂IOS讲义 key: true
是否包含值为 99 value: true
疯狂Ajax讲义-->79
轻量级 Java EE 企业应用实战-->99
疯狂IOS讲义-->99
疯狂Java讲义-->109
{轻量级 Java EE 企业应用实战=99, 疯狂IOS讲义=99, 疯狂Java讲义=109}

 

--8.6.1  Java 8为Map新增的方法

  Java 8除为Map增加了remove(Object key,Object value)默认方法之外,还增加了如下方法。
  •   Object compute(Object key, BiFunction remappingFunction) 
  •   Object computeIfAbsent(Object key, Function mappingFunction)
  •   Object computeIfPresent(Object key, BiFunction remappingFunction)
  •   void forEach(BiConsumer action)
  •   Object getOrDefault(Object key, V defaultValue)等。
public class MapTest2 {
    public static void main(String[] args) {
        Map map = new HashMap();
        //成对放入多个key-value对
        map.put("疯狂Java讲义",109);
        map.put("疯狂IOS讲义",99);
        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
    }
}

运行结果:

{疯狂Ajax讲义=79, 疯狂IOS讲义=99, 疯狂Java讲义=109}
{疯狂Ajax讲义=79, 疯狂IOS讲义=109, 疯狂Java讲义=109}
{Java=4, 疯狂Ajax讲义=79, 疯狂IOS讲义=109, 疯狂Java讲义=109}
{Java=16, 疯狂Ajax讲义=79, 疯狂IOS讲义=109, 疯狂Java讲义=109}

 

--8.6.2  改进的HashMap和Hashtable实现类 

  Java 8改进了HashMap的实现,使用HashMap存在key冲突时依然具有较好的性能。
  此外,Hashtable和HashMap存在两点典型区别。
    ➢ Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
    ➢ Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为key或value。由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个key-value对的value为null。下面程序示范了用null值作为HashMap的key和value的情形。
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);  //1   无法放入
        //将一个value 为null值的key-value对放入HashMap中
        hm.put("a",null);  //2    可以放入
        //输出Map对象
        System.out.println(hm);
    }
}

运行结果:

{null=null, a=null}

为了成功的在HashMap、Hashtable中存储、获取对象,用做key的对象必须实现hashCode()方法和equals()方法。

  与HashSet集合不能保证元素的顺序一样,HashMap、Hashtable也不能保证其中key-value对的顺序。类似于HashSet,HashMap,Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。

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(60000),"疯狂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("测试字符串"));  //1输出true
        //只要两个A对象的count相等,它们通过equals()方法比较返回true,且hashCode值相等
        //Hashtable即认为他们是相同的key,所以下面输出true
        System.out.println(ht.containsValue(new A(87563)));  //输出true
        //下面语句可以删除最后一个 key-value对
        ht.remove(new A(1232)); //3
        System.out.println(ht);
    }
}

运行结果:

{lfw.A@ea60=疯狂Java讲义, lfw.A@1560b=轻量级Java EE企业应用实战, lfw.A@4d0=lfw.B@14ae5a5}
true
true
{lfw.A@ea60=疯狂Java讲义, lfw.A@1560b=轻量级Java EE企业应用实战}

Hashtable判断value相等的标准是:value与另外一个对象通过equals()方法比较返回true即可。

 

--8.6.3  LinkedHashMap实现类

  HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。

  LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。

  LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

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));
    }
}

运行结果:

语文--->80
英文--->82
数学--->76

LinkedHashMapTest可以记住Map添加顺序。

 

--8.6.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的子类,它不保证key-value对之间的次序)。
  ➢ 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");  //1
        //新建一个Properties对象
        Properties prop2 = new Properties();
        //添加属性
        prop2.setProperty("gender","male");
        //将a.ini文件中的key-value对追加到props2中
        prop2.load(new FileInputStream("a.ini"));  //2
        System.out.println(prop2);
    }
}

该程序会在当前目录下生成:{password=123456, gender=male, username=yeeku}                     a .ini文件

 

--8.6.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接口的规则就会冲突。
 
相关方法:
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),"轻量级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)));
    }
}

运行结果:

{R[count=-5]=疯狂Java讲义, R[count=3]=轻量级Java EE 企业应用实战, R[count=9]=疯狂Android讲义}
R[count=-5]=疯狂Java讲义
R[count=9]
R[count=3]
R[count=-5]=疯狂Java讲义
{R[count=3]=轻量级Java EE 企业应用实战}

 

--8.6.6  WeakHashMap实现类

  WeakHashMap与HashMap的用法基本相似。与HashMap的区别在于,HashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的key-value对;但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key所对应的key-value对。

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("中等"));  //1
        //输出whm对象,将看到4个key-value对
        System.out.println(whm);
        //通知系统立即进行垃圾回收
        System.gc();
        System.runFinalization();
        //在通常情况下,将只看到一个key-value对
        System.out.println(whm);
    }
}

运行结果:

{英文=中等, java=中等, 数学=及格, 语文=良好}
{java=中等}

从上面运行结果可以看出,当系统进行垃圾回收时,删除了WeakHashMap对象的前三个key-value对。这是因为添加前三个 key-value对时,这三个key都是匿名的字符串对象,WeakHashMap只保留了对他们的弱引用,这样垃圾回收时会自动删除这三个key-value对。

  WeakHashMap对象中第4个组key-value对的key是一个字符串直接量。(系统会自动保留对该字符串对象的强引用),所以垃圾回收时不会回收它。

--8.6.7  IdentityHashMap实现类

  在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1和key2通过equals()方法比较返回true,且它们的hashCode值相等即可。
  IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为key和value。与HashMap相似:IdentityHashMap也不保证key-value对之间的顺序,更不能保证它们的顺序随时间的推移保持不变。
public class IdentityHashMapTest {
    public static void main(String[] args) {
        IdentityHashMap ihm = new IdentityHashMap();
        //下面两行代码将会向IdentityHashMap对象中添加两个key-value对
        ihm.put(new String("语文"),89);
        ihm.put(new String("语文"),78);
        //下面两行代码只会向IdentifyHashMap对象中添加一个key-value对
        ihm.put("java",93);
        ihm.put("java",98);
        System.out.println(ihm);
    }
}

运行结果:

{语文=89, java=98, 语文=78}

  上面程序中前两个key-value是匿名字符串对象,通过==比较不相等。后面两个是常量池管理的字符串直接量,通过==比较返回true。

 

--8.6.8  EnumMap实现类

  EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。
  EnumMap具有如下特征。
    ➢ EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效。
    ➢ EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。当程序通过keySet()、entrySet()、values()等方法遍历EnumMap时可以看到这种顺序。
    ➢ EnumMap不允许使用null作为key,但允许使用null作为value。如果试图使用null作为key时将抛出NullPointerException异常。如果只是查询是否包含值为null的key,或只是删除值为null的key,都不会抛出异常。与创建普通的Map有所区别的是,创建EnumMap时必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。
enum Season{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumMapTest {
    public static void main(String[] args) {
        //创建 EnumMap 对象,该EnumMap的所有key都是Season枚举类的枚举值
        EnumMap enumMap = new EnumMap(Season.class);
        enumMap.put(Season.SUMMER,"夏日炎炎");
        enumMap.put(Season.SPRING,"春暖花开");
        System.out.println(enumMap);  //{SPRING=春暖花开, SUMMER=夏日炎炎}
    }
}

 

--8.6.9  各Map实现类的性能分析

  对于Map的常用实现类而言,虽然HashMap和Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常比Hashtable要快。
  TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
  LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。
  IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用==而不是equals()方法来判断元素相等。
  EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。
 
8.7 HashSet和HashMap的性能选项
  对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。
  hash表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能:hash算法可以根据hashCode值计算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态是open的:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如图8.8所示是hash表保存各元素,且发生“hash冲突”的示意图。
  因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性。
    ➢ 容量(capacity):hash表中桶的数量。
    ➢ 初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet都允许在构造器中指定初始化容量。
    ➢ 尺寸(size):当前hash表中记录的数量。
    ➢ 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的hash表,依此类推。轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)。除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
 
8.8  操作集合的工具类:Collections
  Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。
 
--8.8.1  排序操作Collections
  提供了如下常用的类方法用于对List集合元素进行排序。
    ➢ 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个元素“整体”移到后面。该方法不会改变集合的长度。
public class SortTest {
    public static void main(String[] args) {
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(-5);
        nums.add(3);
        nums.add(0);
        System.out.println(nums);  //输出:[2,-5,3,0]
        Collections.reverse(nums); //反转
        System.out.println(nums);//输出:[0,3,-5,2]
        Collections.sort(nums); //自然排序
        System.out.println(nums);//输出:[-5,0,2,3]
        Collections.shuffle(nums); //随机排序
        System.out.println(nums);  //每次输出的次序不固定
    }
}
 
--8.8.2  查找、替换操作
  Collections还提供了如下常用的用于查找、替换集合元素的类方法。
    ➢ 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。
public class SearchTest {
    public static void main(String[] args) {
        ArrayList nums = new ArrayList();
        nums.add(2);
        nums.add(-5);
        nums.add(3);
        nums.add(0);
        System.out.println(nums);  //输出[2,-5,3,0]
        System.out.println(Collections.max(nums));  //输出最大元素 3
        System.out.println(Collections.min(nums));  //输出最小元素 -5
        Collections.replaceAll(nums,0,1);  //将nums中的0使用1替换
        System.out.println(nums);  //[2,-5,3,1]
        //判断-5在List集合中出现的次数,返回1
        System.out.println(Collections.frequency(nums,-5));
        Collections.sort(nums); //排序
        System.out.println(nums);//[-5,1,2,3]
        //只有排序后的List集合才可用二分法查询,输出 3(索引)
        System.out.println(Collections.binarySearch(nums,3));
    }
}
 
--8.8.3  同步控制
  Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。Collections提供了多个类方法可以把它们包装成线程同步的集合。
public class SynchronizedTest {
    public static void main(String[] args) {
        //下面程序创建了4个线程安全的集合对象
        Collection c = Collections.synchronizedCollection(new ArrayList<>());
        List list = Collections.synchronizedList(new ArrayList<>());
        Set s = Collections.synchronizedSet(new HashSet<>());
        Map m = Collections.synchronizedMap(new HashMap<>());
    }
}
 
--8.8.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 unmodifiableList = Collections.emptyList();
        //创建一个只有一个元素,且不可变的Set对象
        Set unmodifiableSet = Collections.singleton("疯狂Java讲义");
        //创建一个普通的Map对象
        Map scores = new HashMap();
        scores.put("语文",80);
        scores.put("Java",82);
        //返回普通的Map对象对应的不可变版本
        Map unmodifiableMap = Collections.unmodifiableMap(scores);
        //下面任意一行代码都会引发UnsupportedOperationException异常
        unmodifiableList.add("测试技术");//1  
        unmodifiableSet.add("测试元素");//2
        unmodifiableMap.put("语文",90); //3

    }
}
 
--8.8.5  Java 9新增的不可变集合
  Java 9终于增加这个功能了——以前假如要创建一个包含6个元素的Set集合,程序需要先创建Set集合,然后调用6次add()方法向Set集合中添加元素。Java 9对此进行了简化,程序直接调用Set、List、Map的of()方法即可创建包含N个元素的不可变集合,这样一行代码就可创建包含N个元素的集合。不可变意味着程序不能向集合中添加元素,也不能从集合中删除元素。

8.9 烦琐的接口:Enumeration
  Enumeration接口是Iterator迭代器的“古老版本”,从JDK 1.0开始,Enumeration接口就已经存在了(Iterator从JDK 1.2才出现)。
  Enumeration接口只有两个名字很长的方法。
    ➢ boolean hasMoreElements():如果此迭代器还有剩下的元素,则返回true。
    ➢ Object nextElement():返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。通过这两个方法不难发现,Enumeration接口中的方法名称冗长,难以记忆,而且没有提供Iterator的remove()方法。
  如果现在编写Java程序,应该尽量采用Iterator迭代器,而不是用Enumeration迭代器。

 

posted on 2020-07-16 18:02  lfw123  阅读(150)  评论(0编辑  收藏  举报

导航