浅谈java集合(2020.10.13更新中)


参考自: 疯狂Java讲义

  Java的集合类是一种特别有用的工具类,它可以用于存储数量不等的多个对象,并可以实现常用数据结构,如栈、队列等。除此之外,Java集合还可用于保存具有映射关系的关联数组。Java的集合大致上可分为: Set、List 和Map三种体系,其中Set代表无序、不可重复的集合; List 代表有序、重复的集合;而Map则代表具有映射关系的集合。从JDK1.5以后,Java 又增加了Queue体系集合,代表一种队列集合实现。
  Java集合就像一种容器,我们可以把多个对象(实际上是对象的引用,但习惯上都称对象)“丢进”该容器中。在JDK1.5之前,Java 集合就会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理,从JDK1.5增加了泛型以后,Java 集合可以记住容器中对象的数据类型,从而可以编写更简洁、健壮的代码。本章不会介绍泛型的知识,本章重点介绍Java的四种集合体系的功能和用法。本章将详细介绍Java四种集合体系的常规功能,并深入介绍各集合实现类所提供的独特功能,并深入分析各实现类的实现机制,以及用法上的细微差别,并给出不同应用场景选择哪种集合实现类的建议。

一. java集合概述

  在编程时,常常需要集中存放多个数据,例如前一章(疯狂java讲义第六章)习题中梭哈游戏里剩下的牌。当然我们可以使用数组来保存多个对象。但数组长度不可变化,一旦在初始化数组时指定了数组长度,则这个数组长度是不可变的,如果需要保存个数变化的数据,数组就有点无能为力了;而且数组无法保存具有映射关系的数据,如成绩表:语文-79,数学-80,这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。
  为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java 提供集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所有集合类都位于java.util包下。
  java中总的容器图:
在这里插入图片描述

  集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合里只能保存对象(实际上也是保存对象的引用变量,但通常习惯上认为集合里保存的是对象),基本类型会自动装箱。
  java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是java集合的根接口,这两个接口又包含了一些子接口或实现类。下图是继承体系:
在这里插入图片描述
  图7.1显示了Collection体系里的集合,图7.1中粗线圈出的Set和List接口是Collection接口派生的两个子接口,它们分别代表了无序集合和有序集合; Queue 是Java提供的队列实现,有点类似于List,后面章节还会有更详细的介绍,此处不再赘述。

  图7.2是Map体系的继承树,所有的Map实现类用于保存具有映射关系的数据(也就是前面介绍的关联数组)。
在这里插入图片描述
  图7.2中显示了Map接口的众多实现类,这些实现类在功能、用法上存在一定的差异,但它们都有一个功能特征: Map保存的每项数据都是key-value对,也就是由key和value 两个值组成。就像前面介绍的成绩单:语文-79,数学-80,每项成绩都由2个值组成:科目名和成绩。对于一张成绩表而言,科目通常不会重复,而成绩是可重复的,通常习惯根据科目来查阅成绩,而不会根据成绩来查阅科目。Map也与此类似,Map里的key是不可重复的,key用于标识集合里每项数据,如果需要查阅Map中数据时,总是根据Map的key来获取。
  根据图7.1和图7.2中粗线标识的3个接口,我们可以把Java的所有集合分成三大类,其中Set集合类似于一个罐子,把一个对象添加到Set 集合时,Set 集合无法记住添加这个元素的顺序,所以Set里的元素不能重复(否则系统无法准确识别这个元素); List 集合非常像一个数组,它可以记住每次添加元素的顺序,只是List 的长度可变。Map集合也像一个罐子,只是它里面的每项数据都由两个值组成。图7.3显示了这三种集合的示意图: .
在这里插入图片描述

  从图7.3中可以看出,如果访问List 集合中的元素,可以直接根据元素的索引来访问:如果需要访问Map集合中的元案,可以根据每项元素的key来访问其value;如果希望访问Set集合中的元素,则只能根据元素本身来访问(这也是Set集合里元素不允许重复的原因)。

  对于Set、List和Map三种集合,最常用的实现类在图7.1.7.2中以灰色区域覆盖,分别是HashSet、ArrayList和HashMap三个实现类。

注: 本章主要讲解没有实现并发控制的集合类,对于JDK1.5新增的具有并发控制的集合类,本书将在第16章与多线程一起介绍。

二. Collection和Iterator接口

  Collction接口是List、Set和Qucue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。Collection 接口里定义了如下操作集合元素的方法:

boolean add(Object o):该方法用于向集合里添加一个元素。如果集合对象被添加操作改变
了则返回true。
➢boolean addll(Collection c):该方法把集合C里的所有元素添加到指定集合里。如果集合
对象被添加操作改变了则返回true。
➢void clear():清除集合里的所有元素,将集合长度变为0。
➢boolean contains(Object o):返回集合里是否包含指定元素。
➢boolean containsAll(Collection c);返回集合里是否包含集合C里的所有元素。
➢boolean isEmpty():返回集合是否为空。当集合长度为0时返回true,否则返回false.
➢Iterator iterator():返回一个Iterator 对象,用于遍历集合里的元素。
➢boolean remove(Object o);删除集合中指定元素o,当集合中包含了一个或多个元素o时,
这些元素将被删除,该方法将返回true.boolean removeAll(Collection c):从集合中删除集合C里包含的所有元素(相当于用调用该
方法的集合减集合c),如果删除了一个或一个以上的元素,该方法返回true。
➢boolean retainAll(Collection c): 从集合中删除集合c里不包含的元素(相当于取得把调用该方法的集合变成该集合和集合c的交集),如果该操作改变了调用该方法的集合,该方法返回true。
➢int size():该方法返回集合里元素的个数(不是长度)。
➢Object[] toArray():该方法把集合转换成-一个数组,所有集合元素变成对应的数组元素。

  上面这些方法完全来自于 JavaAPI文档,读者应该自行参考API文档来查阅这些方法的详细信息,此处列出这些方法仅仅作为快速参考清单。

  下面程序将示范如何通过上面方法来操作Collection集合里的元素。

package array;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
public class TestCollection{

	public static void main(String[] args){
		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("轻量级J2EE企业应用实战") ;
		System.out.println("c集合的元素:"+ c);
		Collection books = new HashSet();
		books.add("轻量级J2EE企业应用实战") ;
		books.add("Struts2权威指南") ;
		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) ; 

		}
}

  上面程序中 创建了两个Collection对象,一个是c集合,一个是books集合,其中c集合是ArrayList, 而books集合是HashSet, 虽然它们使用的实现类不同。当把它们当成Collection来使用时,使用add、remove、 clear等方法来操作集合元素时没有任何区别。
  编译和运行上面程序,看到如下运行结果:
在这里插入图片描述
  当使用System.ou的println方法来输出集合对象时,将输出[ele1,ele2… .]的形式,这显然是因为Collection的实现类重写了toString()方法,所有Collction 集合实现类都重写了toString()方法,该方法可以一次性地输出集合中的所有元素。
  如果想依次访问集合里的每一个元素,则需要使用某种方式来遍历集合元素,下面介绍遍历集合元素的两种方法。

  普通情况下,当我们把一个对象“丢进”集合中后,集合会忘记这个对象的类型一也就是说,系统把所有集合元素都当成Object类的实例进行处理。从JDKI.5以后,这种状态得到了改进:可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型。关于泛型的介绍,请参考第8章介绍。或者我的另外一篇博客链接: 泛型小解.

2.1 集合的遍历方式

普遍分为 普通for循环、增强fore循环、迭代器(Iterator),但是像set、map之类的集合,不能使用普通for循环,set无序,没有提供get方法获取元素。map的话,集合是键值对形式存储值的,所以遍历Map集合无非就是获取键和值,根据实际需求,进行获取键和值。

2.2 使用Iterator接口遍历集合元素

  Iterator接口也是Java集合框架的成员,但它与Collection系列、Map系列的集合不一样:Collection系列集合、Map 系列集合主要用于盛装其他对象,而Iterator则主要用于遍历(即迭代访问) Collection集合中的元素,Iterator对象也被称为迭代器
  Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口,Iterator接口里定义了如下三个方法:

boolean hasNext():如果被迭代的集合元素还没有被遍历,则返回true.
➢Object next():返回集合里下一个元素。
➢void remove():删除集合里上一次next方法返回的元素。

  下面程序示范了通过Iterator 来遍历集合的元素:

package array;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class TestIterator {
    public static void main(String[] args) {
        //创建一个集合
        Collection books = new HashSet();
        books.add("轻量级J2EE企业应用实战");
        books.add("Struts2权威指南");
        books.add("基于J2EE的Ajax宝典");
        //获取books集合对应的选代器
        Iterator it = books.iterator();
        while (it.hasNext()) {
            //it. next ()方法返回的数据类型是object类型,需要强制类型转换
            String book = (String) it.next();
            System.out.println(book);
            if (book.equals("Struts2权威指南")) {
                //从集合中删除上一次next方法返回的元素
                it.remove();
            }
            //对book变量赋值,不会改变集合元素本身
            book = "测试字符串";//①
        }
        System.out.println(books);
    }
}

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

Iterator必须依附于Collection对象。有一个Iterator对象,则必然有一个与之关联的Collection对象。Iterator 提供了2个方法来迭代访问Collection 集合里的元素,并可通过remove方法来删除集合中上一次next方法返回的集合元素。

  上面代码的①行代码对迭代变量book进行赋值,但当我们再次输出books集合时,看到集合里的元素没有任何改变。这就可以得到一一个结论:当使用lterator 对集合元素进行迭代时,Iterator 并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何改变。

  解释: 这里用的是String类型,无论是包装类型定义的字符串String s=new String("123"),还是直接定义的String s="123",迭代得到的对象可以特殊理解为新建了一个对象, book = "测试字符串";//①只是变量指向改变了,所以原来的值并没有修改。而如果存的是一个对象,例如:

package array;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
class Dog{
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class TestIterator {

    public static void main(String[] args) {
        //创建一个集合
        Collection dogs = new HashSet();
        dogs.add(new Dog("藏獒",18));
        dogs.add(new Dog("狮子狗",18));
        dogs.add(new Dog("二哈",18));
        System.out.println(dogs);
        System.out.println("----------------");
        Iterator i = dogs.iterator();
        while (i.hasNext()){
            Dog d= (Dog) i.next();
            if ("狮子狗".equals(d.getName())){
                d.setAge(16);
            }
        }
        System.out.println(dogs);
    }
}

则可以改变其属性值,指向(地址值没有改变)
在这里插入图片描述
  当使用Iterator 来迭代访问Collection 集合元素时,Collection 集合果的元素不能被改变,只有通过Iterator 的remove方法来删除上一次next 方法返回的集合元素才可以。否则将公引发java.util.ConcurrentModificationException异常。下 面程序示范了这点:

package array;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
class Dog{
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class TestIterator {

    public static void main(String[] args) {
        //创建一个集合
        Collection dogs = new HashSet();
        dogs.add(new Dog("藏獒",18));
        dogs.add(new Dog("狮子狗",18));
        dogs.add(new Dog("二哈",18));
        System.out.println(dogs);
        System.out.println("----------------");
        Iterator i = dogs.iterator();
        while (i.hasNext()){
            Dog d= (Dog) i.next();
            if ("狮子狗".equals(d.getName())){
                d.setAge(16);
            }
//            i.remove();可以通过这个方法删除
                dogs.remove(d);//报错:java.util.ConcurrentModificationException
        }
        System.out.println(dogs);
    }
}

  上面程序中①处的代码位于Iterator 迭代块内,也就是在Iterator迭代Collection 集合过程中修改了Collection 集合,所以程序将在运行时引发异常。
  上面错误在多线程编程时尤其容易发生:程序的一条线程正在迭代访问Collection集合元素时,另一条线程修改了Collection 集合,这就会导致发生异常。

  Iterator迭代器采用的是快速失败( fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中其他线程修改),程序立即引发ConcurrentModifcationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。
  当然了,有个例外的情况,基本上可以忽略,看下面代码:

package array;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class TestIterator {
    public static void main(String[] args) {
        //创建一个集合
        Collection books = new HashSet();
        books.add("轻量级J2EE企业应用实战");
        books.add("Struts2权威指南");
//        books.add("基于J2EE的Ajax宝典");
        //获取books集合对应的选代器
        System.out.println(books);
        Iterator it = books.iterator();
        while (it.hasNext()) {
            //it. next ()方法返回的数据类型是object类型,需要强制类型转换
            String book = (String) it.next();
            System.out.println(book);
            if (book.equals("轻量级J2EE企业应用实战")) {
                //从集合中删除最后一个next方法返回的元素
                books.remove(book);  //结果是可以的 ,当然了 除非你能十分肯定知道它是最后一个元素,不然,删除其他的都会报错
            }
            //对book变量赋值,不会改变集合元素本身
            book = "测试字符串";//①
        }
        System.out.println(books);
    }
}

在这里插入图片描述

  从上图运行结果可知,从集合中删除最后一个next方法返回的元素,结果是可以的 ,当然了,除非你能十分肯定知道它是最后一个元素,不然,删除其他的都会报错。读者如果有兴趣,可以去看下源码。水平有限,就暂时不解释了。

2.3 使用foreach遍历集合

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

package array;

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class TestForeach {

    public static void main(String[] args) {
        //创建一个集合
        //创建一个集合
        Collection books = new HashSet();
        books.add("轻量级J2EE企业应用实战");
        books.add("Struts2权威指南");
        books.add("基于J2EE的Ajax宝典");
        System.out.println(books);
        for (Object o : books) {
            String b = (String) o;
//            if ("Struts2权威指南".equals(b)) {
//                System.out.println("删除失败");
//                books.remove(b);
//            }
            if ("基于J2EE的Ajax宝典".equals(b)) {
                System.out.println("删除最后一个对象成功" + b);
                books.remove(b);
            }
        }
    }
}

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

  注释的代码会提示删除失败,java.util.ConcurrentModificationException异常,删除最后一个成功,刚才用Iterator一样。其实增强for循环也是通Iterator,可以看下编译后的文件,内容一模一样!

三. set接口

  前面已经介绍过Set集合,它类似于一个罐子,一旦把对象“丢进”Set集合,集合里多个对象之间没有明显的顺序。Set 集合与Collection基本上完全一样,它没有提供任何额外的方法。实际上Set就是Collection,只是行为不同(Set 不允许包含重复元素)。
  Set集合不允许包含相同的元素,如果试图把两个相同元素加入同一个Set集合中,则添加操作失败,add 方法返回false,且新元素不会被加入。
  Set判断两个对象相同不是使用== 运算符,而是根据equals 方法。也就是说,如果只要两个对象用equals方法比较返回true, Set 就不会接受这两个对象;反之,只要两个对象用equals方法比较返回false,Set 就会接受这两个对象(甚至这两个对象是同一个对象,Set 也可把它们当成两个对象处理,后面程序可以看到这种极端的情况)。下面是使用普通Set的示例程序。

package array;

import java.util.HashSet;
import java.util.Set;

public class TestSet {
    public static void main(String[] args) {
        Set s=new HashSet();
        s.add(new String("Struts2权威指南"));
        //因为两个字符串对象通过equals方法比较相等,所以添加失败,返回false
        boolean b = s.add(new String("Struts2权威指南"));
        System.out.println(b);
        //下面输出看到集合只有一个元素
        System.out.println(s);
    }
}

  从上面程序中可以看出,books 集合两次添加的字符串对象明显不是同一个对象(因为两次都调用了new关键字来创建字符串对象),这两个字符串对象使用==运算符判断肯定返回false,但它们通过equals方法比较将返回true,所以添加失败。最后输出bocks集合时,将看到输出结果只有一个元素。
  上面介绍的是Set集合的通用知识,因此完全适合后面介绍的HashSet、TreeSet 和EnumSet三个实现类,只是三个实现类还各有特色。

3.1 HashSet

  HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
  HashSet具有以下的特点:

➢不能保证元素的排列顺序,顺序有可能发生变化。
➢HashSet不是同步的,如果多个线程同时访问一个HashSet,如果有2条或者2条以上线程同时修改了HashSet集合时,必须通过代码来保证其同步。
➢集合元素值可以是null,也是唯一一个。

补充: null值是所有引用类型的默认值,可以强制转换为任一对象类型猜想:java中存在一个潜在的Null类概念,是所有引用类型的变量的子类,String a = (String)null;说明存在一个潜在Null.toString方法。

  当向HashSet集合中存入一个元素时,HashSet 会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该HashCode值来决定该对象在HashSet中存储位置。如果有两个元素通过equals方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet 将会把它们存储在不同位置,也就可以添加成功。,
  简单地说,HashSet 集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。
关于==、equals和hashcode,可以参考这篇链接: 智造官.

  下面程序分别提供了三个类A、B和C,它们分别重写了equals、 hashCode 两个方法的一个或全部,通过下面程序可以让读者看到HashSet判断集合元素相同的标准。

package array.set;

import com.fjl.Main;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

//类A的equals方法总是返回true,但没有重写其hashCode ()方法
class A{
    @Override
    public boolean equals(Object o){
        return true;
    }
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B{
    String a;
    public B(String a) {
        this.a = a;
    }
    @Override
    public int hashCode(){
        return 1;
    }
}
//类C的hashCode()方法总是返回2,且有重写其equals()方法
class C{
    @Override
    public boolean equals(Object o){
        return true;
    }
    @Override
    public int hashCode(){
        return 2;
    }
}
public class TestSet {
    public static void main(String[] args) {
        Set set=new HashSet<>();
        A a1 = new A();
        A a2 = new A();
        B b1 = new B("123");
        B b2 = new B("234");
        C c1 = new C();
        C c2 = new C();
        set.add(a1);
        set.add(a2);
        set.add(b1);
        set.add(b2);
        set.add(c1);
        set.add(c2);
        System.out.println(set);

    }
}

上面程序中set集合中分别添加了2个A对象、2个B对象和2个C对象,其中C类重写了equals()方法总是返回true、 hashCode()方法总是返回2,这将导致HashSet将会把2个C对象当成同一个对象。运行上面程序,看到如下运行结果:在这里插入图片描述
  从上面程序可以看出,即使2个A对象通过equals比较返回true,但HashSet依然把它们当成2个对象;即使2个B对象的hashCode()返回相同值(都是1),但HashSet依然把它们当成2个对象。
  这里有一个问题需要注意:如果需要把一个对象放入HashSet 中时,如果重写该对象对应类的equals()方法,也应该重写其hashCode(方法,其规则是:如果2个对象通过equals方法比较返回true时,这两个对象的hashCode也应该相同。
  如果两个对象通过equals方法比较返回true,但这两个对象的hashCode0方法返回不同的hashCode时,这将导致HashSet将会把这两个对象保存在HashSet的不同位置,从而两个对象都可以添加成功,这与Set集合的规则有点出入。但是看下hashset的定义:只有hashcode和equals都相等,才认为对象相等。所以咱们要自己去理解下含义。 &emsp;&emsp;如果两个对象的hashCode(方法返回的hashCode相同,但它们通过equals方法比较返回false时将更麻烦:因为两个对象的hashCode 值相同,HashSet 将试图把它们保存在同一个位置,但实际上又不行(否则将只剩下一个对象),所以处理起来比较复杂;而且HashSet访问集合元素时也是根据元素的hashCode值来访问的,如果HashSet中包含两个元素有相同的hashCode值,将会导致性能下降。可以参考下面的话语来理解:

(1)如果两个对象根据equals()方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
(2)如果两个对象根据equals()方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生相同的整数结果。
(3)从而在集合操作的时候有如下规则:
  将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相等,如果不相等直接将该对象放入集合中。如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判断不相等,直接将该元素放入到集合中,否则不放入。
回过来说get的时候,HashMap也先调key.hashCode()算出数组下标,然后看equals如果是true就是找到了,所以就涉及了equals。

小结: 如果需要某个类的对象保存到HashSet集合中,重写这个类的equals()方法和 hashCode()方法时,应该尽量保证两个对象通过equals比较返回true时,它们的hashCode 方法返回值也相等。
在这里插入图片描述
  HashSet中每个能存储元素的“槽位(slot)” 通常称为“桶”(bucket),如果有多个元素的hashCode
相同,化它们通过equals方法比较返回false,就需要在一个“桶”里放多个元素,从而导致性能下降。
前而介绍了hasCod()方法对于HashSet 的重要性(实际上,对象的hashCode 值对于后面的HashMap同样重要),下面给出重写hashCode0方法的基本规则:
➢当两个对象通过equals方法比较返回true时,这两个对象的hashCode应该相等。
➢对象中用作equals比较标准的属性,都应该用来计算hashCode值。
在这里插入图片描述

package array.set;

import java.util.HashSet;
import java.util.Iterator;

class R {

    int count;

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

    @Override
    public String toString() {
        return "R(" +
                "count属性:" + count +
                ')';
    }

    @Override
    public boolean equals(Object o) {
        if(o instanceof R){
            R r= (R) o;
            if (r.count==this.count){
                return true;
            }
        }
        return false;
    }

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

public class TestHashSet2 {
    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();
//        hs.remove(first);
//        first = (R) it.next(); //再取元素 就会报这个老异常:java.util.ConcurrentModificationException
        //为第一个元素的count属性賦值
        first.count = -3;//①
        //再次输出HashSet集合,集合元素有重复元素
        System.out.println(hs);
        //删除count为-3的R对象
        hs.remove(new R(-3)); //
        //可以看到被删除了一个R元素
        System.out.println(hs);
        //输出false
        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)));
    }
}

上面程序中提供了R类,R类重写了equals(Object obj)方法和hashCode0方法, 这两个方法都是 根据R对象的count属性来判断的。上面程序的①行代码处改变了Set集合中第一个R对象的count 属性,这将导致该R对象与集合中其他对象相同。程序运行结果如下图所示:

在这里插入图片描述

  正如上图中所见到的,HashSet 集合中第一个元素和第二个元素完全相同,这表明两个元素已经重复,但因为HashSet在添加它们时已经把它们添加到了不同地方,所以HashSet完全可以容纳两个相同的元素。
但此时HashSet将会比较混乱:当试图删除count 为-3的R对象时,HashSet 会计算出该对象的hashCode值,从而找出该对象在集合中的保存位置(寻找该hash值处是否有值),如果有值,然后把此处的对象与传递过来的count为-3的R对象通过equals方法进行比较,如果相等则删除该对象一一HashSet 只有第2个元素才满足该条件(第1个元素它实际上保存在count为-2的R对象对应的位置,hashcode的位置上),所以第2个元素被删除。至于第1个count为-3的R对象,它保存在count为-2的R对象对应的位置,但即使使用equals方法拿它和count为-2的R对象比较依然返回false,这将导致HashSet不可能准确访问该元素。

当向HashSet中添加可变对象时,必须十分小心。如果修改HashSet集合中的对象, 有可能导致该对象与集合中其他对象相等,从而导致HashSet无法准确访问该对象。

LinkedHashSet

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

package array.set;

import java.util.LinkedHashSet;

public class TestLinkedHashSet {
    public static void main(String[] args) {
        LinkedHashSet books = new LinkedHashSet();
        books.add("Struts2权威指南");
        books.add("轻量级J2EE企业应用实战");
        System.out.println("第一次输出:"+books);
        //删除Struts2权威指南
        books.remove("Struts2权威指南");
        //重新添加Struts2 权威指南
        books.add("Struts2权威指南");
        System.out.println("第二次输出:"+books);
    }
}

编译、运行上面程序,看到如下输出:
在这里插入图片描述

上面的集合里,元素的顺序正好与添加顺序一致。

3.2 TreeSet

  TreeSet是SortedSet接口的唯一实现,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。与前面HashSet集合相比,TreeSet 还提供了如下几个额外的方法:

➢Comparator comparator():返回当前Set使用的Comparator,或者返回null,表示以自然方式排序。
➢Object first():返回集合中的第一个元素。
➢Object last():返回集合中的最末一个元素。
➢Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet的元素)。
➢Object higher (Object e):返回集合中位于指定元素之后的元索(即大于指定元素的最小元素,参考元素不需要是TreeSet的元素)。
➢SortedSet subSet(fromElement, toElement):返回此Set的子集合,范围从fromElement(包含)到toElement (不包含) .
➢SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成。
➢SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement的元索组成。

  下面程序测试了TreeSet 的通用用法:

package array.set;

import java.util.TreeSet;

public class TestTreeSetCommon {
    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());
        //输出集合里的最后一个元素
        System.out.println(nums.last());
        //返回小于4的子集,不包含4
        System.out.println(nums.headSet(4));
        //返回大于5的子集,如果Set中包含5,子集中还包含5
        System.out.println(nums.tailSet(5));
        //返回大于等于-3,小于4的子集。
        System.out.println(nums.subSet(-3, 4));
    }
}

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

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

自然排序

  TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列,这种方式就是白然排列。
  Java提供了一个Comparable接口,该接口果定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。当一个对象调用该方法与另一个对象进行比较,例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等:如果该方法返四一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2
  Java 的一些常用类已经实现了Comparable 接口,并提供了比较大小的标准。下面是实现了Comparable接口的常用类:

➢BigDecimal. BigInteger 以及所有数值型对应包装类:按它们对应的数值的大小进行比较。
➢Character:按字符的UNICODE值进行比较。
➢Boolean: true 对应的包装类实例大于false对应的包装类实例。
➢String:按字符串中字符的UNICODE值进行比较。
➢Date、Time:后面的时间、日期比前面的时间、日期大。

  如果试图把一个对象添加进TreeSet 时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常,如下程序示范了这个错误。

package array.set;

import jdk.internal.dynalink.beans.StaticClass;

import java.io.*;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.GZIPOutputStream;

class Err {
    String msg;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

public class TestTreeSetErr {
    public static void main(String[] args) {
        TreeSet ts=new TreeSet();
        ts.add(new Err());//2
        ts.add(new Err());//1
    }
}

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

本文参考自疯狂java讲义(2),不知道是不是版本的问题,网上也有好多说添加第一个元素时不比较。实际上,试图向TreeSet集合中添加第1个Err对象就报了异常java.lang.ClassCastException: array.set.Err cannot be cast to java.lang.Comparable,跟踪了源码,它是和自身进行了比较,返回值为0;源码如下:
在这里插入图片描述

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

package array.set;

import java.util.Date;
import java.util.TreeSet;

public class TestTreeSetError2 {
    public static void main(String[] args) {
        TreeSet ts=new TreeSet();
//向TreeSet集合中添加两个Err对象
        ts.add(new String("Struts权威指南"));
        ts.add(new Date()); //①
    }
}

结果:
在这里插入图片描述
  上面程序先向TreeSet集合中添加了一个字符串对象,这个操作完全正常。当添加第二个Date对象时,TreeSet就会调用该对象compareTo(Object obj)方法与集合中其他元素进行比较一Date对象的compareTo(Object obj)方法无法与字符串对象比较大小,所以上面程序将在①代码处引发异常。
  如果向TreeSet中添加对象时,如果该对象是程序员自定义类的对象,则可以向TreeSet中添加多种类型的对象,前提是用户自定义类实现了Comparable接口,实现该接口时实现compareTo(Object obj)方法时没有进行强制类型转换。但当试图操作TreeSet里的集合数据时,不同类型的元素依然会发生ClassCastException异常
  当把一个对象加入TreeSet集合中时, TreeSet调用该对象的compareTo(Objet obj)方法与容器中的其他对象比较大小,然后根据红黑树算法决定它的存储位置。如果两个对象通过compareTo(Object obj)比较相等,TreeSet 即认为它们应存储同一位置
  对于TreeSet 集合而言,它判断两个对象不相等的标准是:两个对象通过equals方法比较返回false ,或通过compareTo(Object obj)比较没有返回0(经过验证好像是和equals方法返回值无关)一一即使两个对象是同一个对象,TreeSet 也会把它当成两个对象进行处理。下面程序同一个对象添加到TreeSet中时,TreeSet也把它当成2个对象。

package array.set;

import java.util.Objects;
import java.util.TreeSet;

class Z implements Comparable {
    int age;

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

    @Override
    public boolean equals(Object o) {
        return false;
    }

    @Override
    public int compareTo(Object o) {
        return 1;
    }

}

public class TestTreeSet {
    public static void main(String[] args) {
        TreeSet set = new TreeSet();
        Z z=new Z(6);
        set.add(z);
        //输出true,表明添加成功
        System.out.println(set.add(z));//①
        //下面输出set集合,将看到有两个元素
        System.out.println(set);
        //修改set集合的第一个元素的age属性
        ((Z) (set.first())) .age = 9;
        //输出set集合的最后一个元素的age属性,将看到也变成了9
        System.out.println(((Z) (set.last())).age);
    }
}

结果如下:
在这里插入图片描述

这里是引用程序中①代码行把同一个对象再次添加到TreeSet集合中,因为z对象的equals()方法总是返回false,而且compareTo(Object obj)方法总是返回1(满足一个就行),这样TreeSet会认为z对象和它自己也不相同,因此TreeSet中添加两个z对象。
经过验证,发现只要compareTo(Object obj)方法返回值不为0即认为是不同的对象,和equals方法的返回值好像无关
图7.5显示了TreeSet在内存中的存储示意。

在这里插入图片描述

定制排序

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

package spring4.set;

import java.util.Comparator;
import java.util.TreeSet;
class M{
	int age;
	public M(int age) {
		this.age = age;
	}
	@Override
	public String toString() {
		return "M [age=" + age + "]";
	}
}
public class TestTreeSet {

	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		TreeSet ts=new TreeSet(
				new Comparator<M>() {
					@Override
					public int compare(M o1, M o2) {
						// TODO Auto-generated method stub
						if(o1.age>o2.age) {
							return -1;
						}else if (o1.age == o2.age) {
							return 0;
						}else {
							return 1;
						}
					}
				});
		ts.add(new M(5));
		ts.add(new M(3));
		ts.add(new M(9));
		ts.add(new M(2));
		ts.add(new M(2));
		System.out.println(ts);
	}
	
}

  上面创建treeSet时构造器中创建了一个Comparator的匿名内部类对象,该对象负责ts集合的排序规则,当我们把M对象添加到ts集合中时,无需M类实现Comparable接口,因为此时ts无需通过M对象比较大小,而是由ts关联的Comparator对象来负责集合元素的排序。运行结果如下:
在这里插入图片描述

3.3 EnumSet

  略

四. List接口

  List集合代表一个有序集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。因为List集合默认按元素的添加顺序设置元素的索引,例如第一次添加的元素索引为0,第二次添加的元素索引为1…

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):返回对象0在List集合中出现的位置索引。
➢int lastIndexOf(Object o);返回对象。在List集合中最后一次出现的位置索引。
➢Object remove(int index):删除并返回index索引处的元素。
➢Object set(int index, Object element):将index索引处的元素替换成element对象,返回新
元素。
➢List subList(int fromIndex, int tolndex):返回从索引fromIndex (包含)到索引tolndex (不
包含)处所有集合元素组成的子集合。

  所有List实现类都可以调用这些方法来操作集合元素,相对于Set 集合,List 可以根据索引来插入、替换和删除集合元素。下面程序示范了List 集合的常规用法:

package spring4.list;

import java.util.ArrayList;
import java.util.List;

public class TestList {

	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		List books=new ArrayList() ;
		//向books集合中添加三个元素
		books.add(new String ("轻量级J2EE企业应用实战")) ;
		books.add(new String ("Struts2权威指南")) ;
		books.add(new String("基于 J2EE的Ajax宝典")) ;
		System.out.println(books) ;			
		System.out.println("------------将新字符串对象插入在第二个位置--------------");
		//将新字符串对象插入在第二个位置
		books.add(1,new String ("ROR敏捷开发最佳实践")) ;
		for (int i=0;i< books.size(); i++ )
			System.out.println(books.get(i)) ;
		System.out.println("-----------删除第三个元素---------------");
		//删除第三个元素
		books.remove(2) ;
		System.out.println(books) ;
		System.out.println("------------判断指定元素在List集合中位置:输出1,表明位于第二位--------------");
		//判断指定元素在List集合中位置:输出1,表明位于第二位
		System.out.println(books.indexOf (new String ("ROR敏捷开发最佳实践"))); //①
		System.out.println("-----------将第二个元素替换成新的字符串对象---------------");
		//将第二个元素替换成新的字符串对象
		books.set(1,new String("Struts2权威指南")) ;
		System.out.println(books) ;
		System.out.println("-----------将books集合的第二个元素(包括)到第三个元素(不包括)截取成子集合---------------");
		//将books集合的第二个元素(包括)到第三个元素(不包括)截取成子集合
		System.out.println(books.subList(1,2));
		System.out.println("--------------------------");
	}

}

  上面的程序展示了List集合的用法,List集合可以根据位置索引去访问集合内元素,因此List集合增加了一种遍历方式:普通for循环,运行结果如下:
在这里插入图片描述

  上面运行结果清楚地看出List集合的用法。注意①行代码处,程序试图返回新字符串对象在List集合中的位置,实际上: List集合中并未包含该字符串对象。因为List集合添加字符串对象时,添加的是通过new关键字创建的新字符串对象,①行代码处也是通过new关键字创建的新字符串对象,两个字符串显然不是同一个对象,但List的indexOf方法依然可以返回1。那List判断两个对象相等的标准是什么呢? List 判断两个对象相等只要通过equals方法比较返回true即可。看下面程序:

package spring4.list;

import java.util.ArrayList;
import java.util.List;

class A{
	@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		return true;
	}
}

public class TestList2 {

	public static void main(String[] args) {
		List books=new ArrayList() ;
		//向books集合中添加三个元素
		books.add(new String ("轻量级J2EE企业应用实战")) ;
		books.add(new String ("Struts2权威指南")) ;
		books.add(new String("基于 J2EE的Ajax宝典")) ;
		System.out.println(books);
		System.out.println("---------删除新建对象A,则会导致集合中的第一个元素被删除---------");
		/* ① */
		books.remove(new A());
		System.out.println(books);
		System.out.println("---------删除新建对象A,则会导致集合中的第一个元素被删除---------");
		/* ① */
		books.remove(new A());
		System.out.println(books);
	}
}

运行结果如下:
在这里插入图片描述

  从上面运行结果可以看出,执行①行代码时,程序试图删除一个A对象,List 将会调用该A对象的equals 方法依次与集合元素进行比较,如果该equals方法以某个集合元素作为参数时返回true, List将会删除该元素一A 类重写了equals 方法,该方法总是返回true,所以我们看到每次从List集合中删除A对象,总是删除List 集合中第一个元素。

   注意:当调用List 的set(int index, Object element)方法来改变List集合指定索引处的元素时,指定的索引必须是List集合的有效索引。例如集合长度是4,就不能指定替换索引为4处的元素一也就是 说,set(int index, Object element)方法不会改变List集合的长度。

  与Set只提供了一个iterator()方法不同,List 还额外提供了一个listlterator()方法,该方法返回一个Listlterator对象,Listlterator 接口继承了Iterator 接口,提供了专门操作List的方法。Listlterator 接口在Iterator接口基础上增加了如下方法:

➢bcolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。
➢Object previous():返回该迭代器的上一个元素。
➢void add();在指定位置插入一个元素。

  拿Listlterator和普通Iterator 进行对比,不难发现Listerator增加了向前迭化的功能( lterator只能向后迭代),而且Listlterator还可通过add方法向List集介中添加元索( Iterator只能删除元素)。下面程序示范了Listterator 的用法。

package spring4.list;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class TestListIterator {

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void main(String[] args) {
		String[] books = {
				"Struts2权威指南",
				"轻量级J2EE企业应用实战"
		};
		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 ()) ;
		}
	}
}

  从上面程序中可以看出,使用Listterator迭代List集合时,开始也需要采用正向迭代,即需要先使用next()方法进行迭代,迭代过程中可以使用add(方法向上一次迭代元素的后面添加一一个新元素。

运行,上面程序,看到如下结果:
在这里插入图片描述
ps:必须先正向迭代使用next()方法后,前向迭代才能有结果,感兴趣的话可以看下源码,源码是正向迭代后,保留了最后一个元素的索引,反向迭代时就是根据索引依次减1向前迭代
在这里插入图片描述

4.1 ArrayList & Vector

   ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。
  ArrayList和Vector 类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态再分配的Object[]数组。每个ArrayList或Vector对象有一个capacity属性,这个capacity表示它们所封装的Object[]数组的长度。当向ArrayList 或Vector中添加元素时,它们的capacity会自动增加。
   对于通常编程场景,程序员无须关心ArrayList 或Vector的capacity属性。但如果向ArrayList 集合或Vector集合中添加大量元素时,可使用ensureCapacity方法一次性地增加capacity,这可以减少重分配的次数,从而提高性能。
   如果开始就知道ArrayList 集合或Vector 集合需要保存多少个元素,则可以在创建它们时就指定它们的capacity大小。如果创建空ArayList 或Vector集合时不指定capacity属性,该属性默认为10。
   除此之外,ArrayList 和Vector还提供了如下两个方法来操作capacity属性:

void ensureCapacity(int minCapacity): 将ArrayList或Vector集合的capacity增加
minCapacity.void trimToSize(): 调整ArrayList或Vector集合的capacity为列表当前大小。程序可调用该方法来减少ArrayList或Vector集合对象的存储空间。

   ArrayList和Vector在用法上几乎完全相同,但由于Vector是一个古老的集合(从JDK1.0就有了),最开始的时候,Java 还没有提供系统的集合框架,所以Vector里提供了一些方法名很长的方法:例如addElement(Object obj),实际上这个方法与add (Object obj)没有任何区别。从JDKI.2以后,Java 提供了系统的集合框架,就将Vector 改为实现List接口,作为List的实现之一,从而导致Vector里有一些功能重复的方法。

Vector里有一些功能重复的方法,这些方法中方法名更短的方法属于后来新增的方法,那些方法名更长的方法则是Vector原有的方法。Java 改写了Vector原有的方法,将其方法名缩短是为了简化编程。而ArrayList开始就作为List的主要实现类,因此没有那些方法名很长的方法。实际上,Vector具有很多缺点,通常尽量少用Vector实现类。
  除此之外,ArrayList 和Vector的显著区别是; ArrayList 是线程不安全的,当多条线程访问同一个ArayList集合时,如果有超过一条线程修改了ArayList 集合,则程序必须手动保证该集合的同步性。但Vector 集合则是线程安全的,无须程序保证该集合的同步性。因为Vector 是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,同样不推荐使用Vector实现类,后面会介绍一个Collections工具类,它可以将一个 ArrayList变成线程安全的。

  Vector还提供了一个Stack子类,它用于模拟了“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop” 出栈。与Java中其他集合一样,进栈
出栈的都是Object,因此从栈中取出元素后必须做类型转换,除非你只是使用Object具有的操作。所
以Stack类里提供了如下几个方法:

➢Object peek():返回“栈”的第一一个元素,但并不将该元素“pop” 出栈。
➢Object pop():返回“栈”的第一个元素, 并将该元素“pop”出栈。
➢void push(Object item):将一个元素“push"进栈,最后-一个进 “栈”的元素总是位于“栈”顶。

  除此之外,List 还有一个LinkedList 的实现,它是一“个基 于链表实现的List类,对于顺序访问集合中的元素进行了优化,特别是当插入、删除元素时速度非常快。因为LinkedList既实现了List 接口,也实现了Deque 接口(双向队列),在第五节会详细介绍LinkedList。

4.2 操作数组的Arrays工具类

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

package spring4.list;

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

public class FixedSizeList
{
	@SuppressWarnings("rawtypes")
	public static void main (String[] args) {
		List fixedList = Arrays.asList("Struts2权威指南","ROR 敏捷开发最佳实践");
		//获取fixedList的实现类,将输出Arrayss rrayList
		System.out.println(fixedList.getClass()) ;
		//遍历fixedList的集合元素
		for(int i = 0; i<fixedList.size();i++)
			System.out.println(fixedList.get(i)) ;
		//试图增加、删除元素都将引发Unsupportedope rationException异常
		fixedList.add("ROR敏捷开发最佳实践") ;
		fixedList.remove("Struts2权威指南") ;
	}
}

结果如下:在这里插入图片描述

  上面照片中圈着的两行代码对于普通List集合完全正常,但如果试图通过这两个方法来增加、删除ArraysSArrayList集合里的元素,将会引发异常。所以上面程序在编译时完全正常,但会在运行第一行粗体字标识的代码行处引发UnsupportedOperationException异常。

五. 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有两个常用的实现类: LinkedList 和PriorityQueue,下面分别介绍这两个实现类。

5.1 LinkedList

  LinkedList是一个比较奇怪的类,从图7.1中可以看出,它是List 接口的实现类一这 意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList 还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,Deque 接口里定义了一些可以双向操作队列的方法:

void addFirst(Object e):将指定元素插入该双向队列的开头。
➢void addLast(Object e):将指定元素插入该双向队列的末尾。
➢Iterator descendingIterator():返回以该双向队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
➢Object getFirst():获取、但不删除双向队列的第一个元素 。
➢Object getLast():获取、但不删除双向队列的最后一个元素。
➢boolean offerFirst(Object e):将指定的元素插入该双向队列的开头。
➢boolean offerL ast(Object e):将指定的元素插入该双向队列的末尾。
➢Object peekFirst():获取、但不删除该双向队列的第一-个元素; 如果此双端队列为空,则返回null。
➢Object peekLast(): 获取、但不删除该双向队列的最后一个元素; 如果此双端队列为空,则返回null.
➢Object pollFirst():获取、并删除该双向队列的第一个元素: 如果此双端队列为空,则返回null.
➢Object pollLast():获取、并删除该双向队列的最后-一个 元素;如果此双端队列为空,则返回null. 
➢Object pop(): pop 出该双向队列所表示的栈中第一个元素。
➢void push(Object e):- -个元素push进该双向队列所表示的栈中(即该双向队列的头部)。
➢Object removeFirs():获取、并删除该双向队列的第一个元素。
➢Object removeFirstOccurrence(Object o):删除该双向队列的第一次的出现元素 o。
➢removeLast(); 获取、并删除该双向队列的最后一个元素。
➢removeLastOccurrence(Object o):删除该双向队列的最后一次出现的元素o。

  从上面方法中可以看出,LinkedList 不仅可以当成双向队列使用,也可以当成“栈”使用,因为该类里还包含了pop (出栈)和push (入栈)两个方法。除此之外,LinkedList 实现了List 接口,所
以还被当成List使用。下面程序简单示范了LinkedList 集合的通常用法。

package spring4.list;

import java.util.LinkedList;

public class TestLinkedList
{
	@SuppressWarnings("unchecked")
	public static void main (String[] args) {
		LinkedList  books = new LinkedList();
		//将字符串元素加入队列的尾部①
		books.offer("Struts2权威指南") ;
		//将一个字符串元素入栈②
		books.push("轻量级J2EE企业应用实战");
		//将字符串元素添加到队列的头部③
		books.offerFirst("ROR敏捷开发最佳实践") ;
		for (int i = 0; i < books.size() ; i++ )
		{
			System.out.println(books.get(i));
		}
		System.out.println("-------访问、并不删除队列的第一个元素  -------");
		System.out.println(books.peekFirst()) ;
		System.out.println("-------访问、并不删除队列的最后一个元素  -------");
		System.out.println(books.peekLast()) ;
		System.out.println("-------采用出栈的方式将第一个元素pop出队列 -------");
		System.out.println(books.pop()) ;
		System.out.println("-------下面输出将看到队列中第一个元素被删除-------");
		System.out.println(books) ;
		System.out.println("-------访问、并删除队列的最后-个元素 -------");
		System.out.println(books.pollLast());
		System.out.println("-------下面输出将看到队列中只剩下中间一个元素:轻量级J2EE企业应用实战 -------");
		System. out .println (books) ;

	}
}

运行结果如下:
在这里插入图片描述

  上面程序中代码①-③分别示范了LinkedList 作为双向队列、栈和List集合的用法。由此可见,LinkedList 是一个功能非常强大集合类。

LinkedList 与ArrayList、Vector 的实现机制完全不同,ArrayList、 Vector 内部以数组的形式来保存集合中的元素,因此随机访问集合元素上有较好的性能:而LinkedList 内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能非常出色(只需改变指针所指的地址即可)。实际上,Vector 因为实现了线程同步功能,所以各方面性能都有所下降。

  对于所有内部基于数组的集合实现,例如ArrayList、Vector 等,使用随机访问的速度比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。

数组、ArrayList、Vector、LinkedList对比

  通常的编程过程中无须理会ArrayList和LinkedList之间的性能差异,只需要知道LinkedList集合不仅提供了List 的功能,还额外提供了的双向队列、栈的功能。但在一些性能非 常敏感的地方,可能需要慎重选择哪个List实现,表7.2显示列出了数组、ArrayList、 Vector、 LinkedList 的性能差异:
在这里插入图片描述
  从上表中可以看出:因为数组以一块连续内存区来保存所有数组元素,所以数组在随机访问时性能最好。所有内部以数组作为底层实现的集合在随机访问时也有较好性能;而内部以链表作为底层实现的集合在插入、删除操作时有很好的性能;进行迭代操作时,以链表作为底层实现的集合也比以数组作为底层实现的集合的性能好 。(我想这里说的意思,应该是同等集合的普通遍历和迭代器遍历的对比,查了些资料,好多这种说法,不知道哪里出了问题)下面程序给出各种对比:

package spring4.list;

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

public class Main {
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void main(String[] args) {
		
		ArrayList array_list=new ArrayList();
		for(int i=0;i<3333333;i++){
			array_list.add(i);
		}
 
		
		
		LinkedList linked_list=new LinkedList();
		for(int i=0;i<3333333;i++){
			linked_list.add(i);
		}
 
	
		
		long iterator_start1=System.currentTimeMillis();
		for(int i=0;i<10000;i++){
			array_list.get(i);
		}
		System.out.println("使用get()方法遍历ArrayList集合的元素所需时间:"+(System.currentTimeMillis()-iterator_start1));
		
		
		
		long linked_iterator_start1=System.currentTimeMillis();
		for(int i=0;i<10000;i++){
			linked_list.get(i);
		}
		System.out.println("使用get()方法遍历LinkedList集合的元素所需时间:"+(System.currentTimeMillis()-linked_iterator_start1));
		
	
		Iterator array_list_iterator=array_list.iterator();
		long iterator_start=System.currentTimeMillis();
		while(array_list_iterator.hasNext()){
			array_list_iterator.next();
		}
		System.out.println("迭代ArrayList集合的元素所需时间:"+(System.currentTimeMillis()-iterator_start));
		
		
		Iterator linked_list_iterator=linked_list.iterator();
		long linked_iterator_start=System.currentTimeMillis();
		while(linked_list_iterator.hasNext()){
			linked_list_iterator.next();
		}
		System.out.println("迭代LinkedList集合的元素所需时间:"+(System.currentTimeMillis()-linked_iterator_start));
	
		long arraylist_remove=System.currentTimeMillis();
		array_list.remove(34567);
		System.out.println("ArrayList删除集合元素所需时间:"+(System.currentTimeMillis()-arraylist_remove));
		
		
		long linkedlist_remove=System.currentTimeMillis();
		linked_list.remove(34567);
		System.out.println("LinkedList删除集合元素所需时间:"+(System.currentTimeMillis()-linkedlist_remove));
		
		
		long arraylist_add=System.currentTimeMillis();
		array_list.add(23456,"a");
		System.out.println("ArrayList插入集合元素所需时间:"+(System.currentTimeMillis()-arraylist_add));
		
		long linkedlist_add=System.currentTimeMillis();
		linked_list.add(23456, "a");
		System.out.println("LinkedList插入集合元素所需时间:"+(System.currentTimeMillis()-linkedlist_add));
	}
}

运行结果:

使用get()方法遍历ArrayList集合的元素所需时间:0
使用get()方法遍历LinkedList集合的元素所需时间:165
迭代ArrayList集合的元素所需时间:5
迭代LinkedList集合的元素所需时间:32
ArrayList删除集合元素所需时间:1
LinkedList删除集合元素所需时间:1
ArrayList插入集合元素所需时间:3
LinkedList插入集合元素所需时间:0
在这里插入图片描述

从上面的程序中可以看出:

(1)分别使用LinkedList和ArrayList来遍历集合元素所花费的时间差别非常大,因此当我们要去遍历List集合元素时,使用ArrayList来遍历性能会好很多,对于LinkedList推荐使用迭代器来遍历集合元素。 (2)当我们需要频繁的执行插入、删除集合元素时,应该使用LinkedList集合,因为ArrayList集合需要经常重写分配内部数组的大小,其时间开销比较大(当然这里说的是频繁,在实际开发中如果用的是ArrayList集合,执行插入、删除不是很频繁的话,可以使用ArrayList)。

5.2 PriorityQueue

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

package queue;

import java.util.PriorityQueue;

public class TestPriorityQueue {

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static void main(String[] args)
	{
		PriorityQueue pq = new PriorityQueue() ;
		//下面代码依次向pq中加入四个元素
		pq.offer(6) ;
		pq.offer(-3) ;
		pq.offer(9);
		pq.offer(0); 
		//输出pq队列,并不是按元素的加入顺序排列,
		//而是按元素的大小顺序排列,输出[-3, 0,9, 6]
		System.out.println(pq) ;
		//访问队列第一个元素,其实就是队列中最小的元素: -3
		System.out.println (pq.peek()) ;


	}
}

运行结果:在这里插入图片描述
直接输出PriorityQueue对象时,看不到PriorityQueue集合里元素的顺序,多 次调用该集合对象的remove方法才可看到元素的排列顺序(ps:具体可以看offer()的源码,能力有限暂时无法解释)。

  PriorityQueue不允许插入null 元素,它还需要对队列元素进行排序,队列元素有两种排序方式: .

➢自然排序: 采用自然顺序的PriorityQueue 集合中的元素必须实现了Comparable 接口,而 且应该是同一类的多个实例,否则可能导致ClassCastException 异常。
➢定 制排序:创建PriorityQueue 队列时,传入一个Comparator对象,该对象负责对队列中所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。
PriorityQueue队列对元素的要求与前面TreeSet对元素的要求基本一致,因此关于使用自然排序和
定制排序的详细介绍请参看本书3.2节的介绍。

六. Map接口

  Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value, key 和value 都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。
  key和value 之间存在单向一对一关系, 即通过指定的key,总能找到唯一的、 确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的value。如果把Map的两组值拆开来看,Map里的数据有如图6-1所示的结构。
在这里插入图片描述

  从图6-1中可以看出,如果把Map里的所有key放在一起来看,它们就组成了一个Set集合(所有的key没有顺序,key与key之间不能重复),实际上Map确实包含了一个keySet()方法,用于返回Map所有key组成的Set集合。

  不仅如此,Map里key集和Set集合里元素的存储形式也很像,Map子类和Set子类在名字上也惊人地相似:如Set接口下有HashSet、LinkedHashSet、SortedSet (接口)、TreeSet、EnumSet 等实现类和子接口,而Map接口下则有HashMap、LinkedHashMap、SortedMap (接口)、TreeMap、EnumMap 等实现类和子接口。正如它们名字所暗示的,Map的这些实现类和子接口中key集存储形式和对应Set集合中元素的存储形式完全相同。
  如果把Map所有value放在一起来看,它们又非常类似于一个List:元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象来作为索引。如果需从List集合中取出元素,需要提供该元素的数字索引;如果需要从Map中取出元素,需要提供该元素的key索引。因此,Map有时也被称为字典,或关联数组。Map接口中定义了如下常用的方法:

void clear():删除该Map对象中所有key-value对。
➢boolean containsKey(Object key):查询Map中是否包含指定key,如果包含则返回true。
➢boolean containsValue(Object value):查询Map中是否包含一个或多个value,如果包含则
返回true。
➢Set entrySet():返回Map中所包含的key-value对所组成的Set 集合,每个集合元素都是
Map.Entry(Entry是Map的内部类)对象。
➢Object get(Object key):返回指定key所对应的value;如果此Map中不包含该key,则返回null。
➢boolean isEmpty():查询该Map是否为空(即不包含任何key-value对),如果为空则返回true。
➢Set keySet(): 返回该Map中所有key所组成的Set集合。
➢Object put(Object key, Object value):添加一个key-value对,如果当前Map中已有一个与该key相等的key-value对,则新的key-value对会覆盖原来的key-value对。
➢void putAll(Map m):将指定Map中的key-value对复制到本Map中。
➢Object remove(Object key):删除指定key所对应的key-value对,返回被删除key所关联
的value,如果该key不存在,返回null.int size(): 返回该Map里的key-value 对的个数。
➢Collection values():返回该Map里所有value组成的Collection。

  Map接口提供了大量的实现类,典型实现如HashMap 和Hashtable 等,以及HashMap 的子类LinkedHashMap,还有SortedMap 子 接口及该接口的实现类TreeMap, 还有WeakHashMap 、IdentityHashMap等。下面将详细介绍Map接口实现类。

Map中包括-一个内部类: Entry。 该类封装了-一个key-value对,Entry包含三个方法: 
➢Object getKey();返回该Entry里包含的key值。
➢Object getValue():返回该Entry里包含的value值。
➢Object setValue(V value): 设置该Entry里包含的value值,并返回新设置的value值。
我们可以把Map理解成一个特殊的 Set,只是该Set里包含的集合元素是Entry对象,而不是普通对象。

6.1 HashMap & Hashtable

注:从Hashtable的类名上就可以看出它是一个古老的类,它的命名甚至没有遵守Java的命名规范:每个单词的首字母都应该大写。也许当初开发Hashtable 的工程师也没有注意到这一点,后来大量Java程序中使用了Hashtable 类,所以这个类名也就不能改为HashTable了,否则将导致大量程序需要改写。与Vector类似的,尽量少用Hashtable实现类,即使需要创建线程安全的Map实现类,也可以通过后面介绍的Collections工具类来把HashMap变成线程安全的,无须使用Hashtable实现类。
  HashMap和Hashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayList和Vector的关系:Hashtable 是一个古老的Map实现类,它从JDK1.0起就已经出现了,当它出现时,Java 还没有提供Map接口,所以它包含了两个烦琐的方法: elements() (类似于Map接口定义的values(方法)和keys() (类似于Map接口定义的keySet()方法),现在很少使用这两个方法(关于这两个方法的用法请参考下面章节的介绍)。

除此之外,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的情形。

package collection.map;

import java.util.HashMap;
import java.util.Map;

public class NullInHashMap {

	public static void main(String[] args) {
		HashMap map=new HashMap<>();
		map.put(null, null);
		map.put(null, null);//	① 不报错
		map.put("key", null);// ② 成功放进去
		System.out.println(map);
	}
	
}

  上面程序试图向HashMap中放入三个key-value对,其中①处代码处将无法将key-value对放入,因为Map中已经有一个key-value对的key为null,所以无法再放入key为null 的key-value对。②代
码处可以放入该key-value对,因为一个HashMap中可以有多项value为null。编译、运行.上面程序,
看到如下输出结果:
在这里插入图片描述
  根据上面输出结果可以看出,HashMap重写了toString()方法,实际所有Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式的字符串:{key1=valye1,key2=vale2…}。
  为了成功地在HashMap、Hashtable 中存储、获取对象,用作key的对象必须实现hashCode 方法和equals 方法。
  与HashSet集合不能保证元素的顺序一样, HashMap、 Hashtable 也不能保证其中key-value对的顺序。类似于HashSet 的是,HashMap、Hashtable 判断两个key相等的标准也是:两个key通过equals方法比较返回true,两个key的hashCode值也相等
  除此之外,HashMap、 Hashtable 中还包含一个containsValue方法用于判断是否包含指定value,那么HashMap、Hashtable 如何判断两个value 相等呢? HashMap、Hashtable 判断两个value相等的标准更简单:只要两个对象通过equals比较返回true即可。下面程序示范了Hashtable判断两个key相等的标准和两个value相等的标准。

package collection.map;

import java.util.Hashtable;

//定义类A,该类根据A对象的count属性来判断两个对象是否相等,计算hashCode值
//只要两个A对象的count相等,则它们通过equals比较也相等,其hashCode值也相等
class A
{
	int count;
	public A(int count) {
		this.count=count;
	}
	public boolean equals(Object obj) {
		if (obj== this)
			return true;
		if (obj!=null && obj.getClass()==A.class)
		{
			A a=(A)obj;
			if (this.count == a.count)
				return true; 
		}
		return false;
	}
	public int hashCode()
	{
		return this.count;
	}
}
//定义类B,B对象与任何对象通过equals方法比较都相等
class B{
	public boolean equals(Object obj)
	{
		return true;
	}
}
public class TestHashtable{
	
	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		Hashtable ht =new Hashtable<>();
		ht.put(new A(60000),"Struts2权威指南") ;
		ht. put(new A(87563),"轻量级 J2EE企业应用实战") ;
		ht. put(new A(1232),new B());
		System.out.println(ht);
		//只要两个对象通过equals比较返回true,Hashtable就认为它们是相等的value.
		//因为Hashtable中有一个B对象,它与任何对象通过equals比较都相等,所以下面输出true.
		System.out.println(ht.containsValue("测试字符串")); //①
		//只要两个A对象的count属性相等,它们通过equals比较返回true,且hashCode相等
		//Hashtable即认为它们是相同的key,所以下面输出true.
		System.out.println(ht.containsKey(new A(87563))); //②
		//下面语句可以删除最后一个key-value对
		ht.remove(new A(1232)); //③
		//通过返回key set 集合,来遍历
		for (Object key : ht.keySet()) {
			System.out.println(key+"-------->"+ht.get(key));
		}
	}
}

  上面程序定义了类A和类B,其中A类判断两个A对象相等的标准是count属性:只要两个A对象的count属性相等,则通过equals方法比较它们返回true,它们的hashCode也相等;而B对象则可以与任何对象相等。Hashtable判断value相等的标准是: value 与另外一个对象通过equals方法比较返回true即可,上面程序中的ht对象中包含了一个B对象,它与任何对象通过equals方法比较总是返回true,所以在①代码处返回true。事实上,不管传给ht对象containtsValue 方法参数是什么,程序总是返回true。
  根据Hasbtable判断两个key相等的标准,程序在②处也将输出true,因为虽然两个A对象虽然不是同一个对象,但它们通过equals 方法比较返回true,且hashCode值相等,Hashtable 即认为它们是同一个key。类似的是,程序在③处也可以删除对应的key-value对。
  程序最后还示范了如何遍历Map中的全部key-value对:调用Map对象的keySet方法返回全部key组成的Set对象,通过遍历该Set的元素(就是Map的全部key)就可以遍历Map中的所有key-value对。

当使用自定义类作为HashMap、Hashtable的key时,如果重写该类的equals(Object obj)和hashCode方法,应该保证两个方法的判断标准一致:当两个key通过equals方法比较返回true时,两个key的hashCode值也应该相同。因为HashMap、Hashtable保存key的方式与HashSet保存集合元素的方式完全相同,所以HashMap、Hashtable对key的要求与HashSet对集合元素的要求完全相同。

  与HashSet 类似的是,如果使用可变对象作为HashMap、Hashtable 的key,并且程序修改了作为key的可变对象,也可能引发与HashSet 类似的情形:程序再也无法准确访问到Map中被修改过key。看下面程序:

package collection.map;
import java.util.Hashtable;
import java.util.Iterator;
public class TestHashtableError{
	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		Hashtable ht = new Hashtable() ;
		//此处的A类与前一个程序的A类是同一个类
		ht.put(new A(6),"Struts2 权威指南");
		ht.put(new A(3),"轻量级 J2EE企业应用实战") ;
		System.out.println(ht);
		//获得Hashtable的key Set集合对应的Iterator迭代器
		Iterator it = ht.keySet().iterator() ;
		//取出Map中第一个key
		A first=(A)it.next();
		first.count=3; 
		//①
		//输出{A@1560b=Struts2权威指南,A01560b=轻 量级J2EE企业应用实战}
		System.out.println(ht);
		//只能删除没有被修改过的key所对应的key-value对
		ht.remove(new A(3)) ;
		System.out.println(ht) ;
		//无法获取剩下的value,下面两行代码都将输出nu1l.
		System.out.println(ht.get(new A(3))); //②
		System.out.println(ht.get(new A(6))); //③
	}
}

  因为上面程序使用了类A的对象作为key,而A对象是可变对象。当程序在①处修改了A对象后,实际上修改了Hashtable对象的key,这就导致该key不能被准确访问。当程序试图删除count为3的A对象时,只能删除没被修改的key所对应的key-value 对。程序②和③处的代码都不能访问“Struts2权威指南”字符串,这都是因为它对应的key被修改过的原因。具体可以看下图:

在这里插入图片描述
与HashSet类似的是,尽量不要使用可变对象作为HashMap. Hashtable 的key,如果确实需要使用可变对象作为HashMap. Hashtable 的key,则尽量不要在程序中修改作为key的可变对象。
  HashSet有一个子类是LinkedHashSet, HashMap则有一个子类:LinkedHashMap; LinkedHashMap也使用双向链表来维护key-value对的次序,该链表定义了迭代顺序,该迭代顺序与key-value对的插入顺序保持一致。
  LinkedHashMap可以避免需要对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可)。同时又可避免使用TreeMap所增加的成本。
  LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap 的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。下面程序示范了LinkedHashMap的功能:迭代输出LinkedHashMap的元素时,将会按添加key-value对相同顺序输出。

package collection.map;

import java.util.LinkedHashMap;

public class TestLinkedHashMap {

	public static void main(String[] args) {
		LinkedHashMap map=new LinkedHashMap<>();
		map.put("语文", 90);
		map.put("数学", 86);
		map.put("英语", 94);
		for (Object object : map.keySet()) {
			System.out.println(object+"----->"+map.get(object));
		}
	}
	
}

  编译、运行上面程序,即可看到LinkedHashMap的功能。

  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):从属性文件(以输入流表示)中加载属性名=属性值,把加载到的属性名=属性值对追加到Properties 里(由于Properties是Hashtable的子类,它 不保证key-value对之间的次序)。
➢void store(OutputStream out, String comments):将Properties中的key-value对写入指定
属性文件(以输出流表示)。

package collection.map;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class TestProperties {

	public static void main(String[] args) throws Exception {
		Properties p= new Properties();
		//向properties中添加属性
		p.setProperty("boy", "fjl");
		p.setProperty("girl", "cyl");
		//保存p到lover.ini

		/*store(OutputStream out, String comments)
		out:输出流
		comments:说明信息
		*/
		p.store(new FileOutputStream("lover.ini"), "lover");//①

		Properties p1= new Properties();
		//向properties中添加属性
		p1.setProperty("&", "&");
		//加载lover.ini 到p1中
		p1.load(new FileInputStream("lover.ini"));//②
		System.out.println(p1);
	}

}

  上面程序示范了Properties类的用法,其中①代码处将Properties 对象中的key-value对写入lover.ini文件;②代码处则从lover.ini 文件中读取属性,并添加到p1对象中。编译、运行上面程序,该程序输出结果如下:
在这里插入图片描述
  上面程序还在当前路径生成了-一个lover.ini文件,该文件的内容如下:
在这里插入图片描述

6.2 SortedMap & TreeMap

  正如Set接口派生出了SortedSet子接口,SortedSet接口有一个TreeSet实现类,Map 接口也派生了一个SortedMap子接口,SortedMap也有个TreeMap实现类。
  与TreeSet类似的是,TreeMap也是基于红黑树对TreeMap中所有key进行排序,从而保证TreeMap中所有key-value 对处于有序状态。TreeMap 也有两种排序方式:

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

  类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准也是两个key通过equals比较返回true,而通过compareTo方法返回0, TreeMap 即认为这两个key是相等的。如果想使用自定义的类作为TreeMap的key,且想让TreeMap良好地工作,重写该类的equals方法和compareTo方法时应有一致的返回结果: 即两个key通过equals方法比较返回true时,它们通过compareTo方法比较应该返回0。如果equals方法与compareTo方法的返回结果不一致, 要么该TreeMap与Map接口的规则有出入(当equals比较返回true, 但cormpareTo 比较不返回0时),要么TreeMap处理起来性能有所下降(当compareTo比较返回0,但equals比较不返回true时)。

与TreeSet类似的是,TreeMap 中也提供了系列根据key顺序来访问Map中key-value对方法:
➢Map.Entry firstEntry();返回该Map中最小key所对应的key-value对,如果该Map为空,
则返回nll。
➢Object firstKey():返回该Map中的最小key值,如果该Map为空,则返回null。
➢Map.Entry lastEntry():返回该Map中最大key所对应的key-value对,如果该Map为空,
或不存在这样的key-value 都返回null。
➢Object lastKey():返回该Map中的最大key值,如果该Map为空,或不存在这样的key都
返回null。
➢Map.Entry higherEntry(Object key):返回该Map中位于key后一位的key-value对(即大
于指定key的最小key所对应的key-value对)。如果该Map为空,则返回null。
➢Object higherKey(Object key):返回该Map中位于key后-位的key值( 即大于指定key
的最小key值)。如果该Map为空,或不存在这样的key-value都返回null。
➢Map.Entry lowerEntry(Object key):返回该Map中位于key前一位的 key-value 对(即小于指定key的最大key所对应的key-value对)。如果该Map为空,或不存在这样的key-value
都返回null。
➢Object lowerKey(Object key) :返回该Map中位于key前一位的key值(即小于指定key
的最大key值)。如果该Map为空,或不存在这样的key都返回null。
➢Object lowerKey(Object key) :返回该Map中位于key前一-位的 key值(即小于指定key
的最大key值)。如果该Map为空,或不存在这样的key都返回null。
➢NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回该Map的子Map,其key的范围从fromKey (是否包括取决于第二个参
数)到toKey (是否包括取决于第四个参数)。
➢SortedMap subMap(Object fromKey, Object toKey):返回该Map的子Map,其key的范围
从fromKey (包括)到toKey (不包括)。
➢SortedMap tailMap(Object fromkey):返回该Map的子Map,其key的范围是大于fromKey
(包括)的所有key。
➢NavigableMap tailMap(Object fromKey, boolean inclusive):返回该Map的子Map,其key的范围是大于fromKey ( 是否包括取决于第二个参数)的所有key.
➢SortedMap headMap(Object toKey):返回该Map的了Map,其key的范围是小于fromKey
(不包括)的所有key。
➢NavigableMap headMap(Object toKey, boolean inclusive):返回该Map的子Map,其key
的范围是小手fromKey (是否包括取决于第二个参数)的所有key。

表面上看起来这些方法很复杂,其实它们很简单:因为TreeMap中的key-value对是有序的,所以增加了访问第一个、前一个、后一个、最后一个key-value对的方法,并提供了几个从TreeMap中截取子TreeMap的方法。

6.3 WeakHashMap

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

package collection.map;

import java.util.Map;
import java.util.WeakHashMap;

public class TestWeakHashMap {
	@SuppressWarnings("unchecked")
	public static void main(String[] args) {
		
		WeakHashMap map= new WeakHashMap<>();
		map.put(new String("语文"),"90");
		map.put(new String("数学"),"88");
		map.put(new String("英语"),"92");
		map.put("fjl","& cyl");
		System.out.println(map);
		
		System.gc();
		System.runFinalization();
		System.out.println(map);
		
		
	}
	

}

运行结果如下:
在这里插入图片描述

  从上面运行结果中可以看出,当系统进行垃圾回收时,删除了WeakHashMap 对象的前三个key-value对。这是因为添加前三个key-value对(粗体字部分)时,这三个key都是匿名字符串对象,只有WeakHashMap保留了对它们的弱引用。WeakHashMap对象中的第四组key-value对的key是一个字符串直接量,系统会缓存这个字符串直接量(即系统保留了对该字符串对象的强引用),所以垃圾回收时不会回收它。

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值