Java集合

Java集合类可用于存储数量不等的对象,并可以实现常用的数据结构如栈,队列等,Java集合还可以用于保存具有映射关系的关联数组

Java集合分为Set,List,Map,Queue四种体系,其中:
Set代表无序不可重复的集合;
List代表有序可以重复的集合;
Map代表具有映射关系的集合;
Queue代表一种队列集合的实现.

Java5增加了泛型之后,Java集合可以记住容器中对象的数据类型,从而可以编写出更加简介,健壮的代码.在Android开发中集合+泛型的情况是很常见的.

为什么要用到集合??

数组长度固定,数组索引连续,数组无法自由增加数组的长度.数组无法保存数量变化的数据,数组无法保存具有映射关系的数据.为了保存数量不确定的数据以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类(也被称为容器类).

集合和数组的区别:数组中可以保存基本类型的值,也可以是对象(实际上保存的是对象的引用变量);集合里保存的只能是对象(实际上保存的是对象的引用变量,通常习惯上认为是集合里保存的对象)

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

Collection集合继承体系

Map集合继承体系

Map众多的实现类都具有一个共同的特征就是:Map保存的每项数据都是Key-Value对,也就是keyvalue两个值组成.Map中的value可以重复但是key一般是不会重复(可以类比于通过科目查询成绩,科目不允许重复,如果重复了还怎么查成绩),key用于标识集合里的每项数据,如果需要查询Map中的数据时,总是根据Map的key来获取.
三种集合示意图

如果想要访问Set集合的元素,则只能根据元素本身来访问,这也是Set集合里元素不能重复的原因.(Set集合有点像一个罐子,把一个对象添加到Set集合时,Set集合无法记住添加这个元素的顺序,所以Set里的元素不能重复)
如果想要访问List集合里的元素,可以直接根据元素的索引来访问(List结合非常像一个数组,且List的长度可变);
如果想要访问Map集合中的元素,可以根据每项元素的key来访问其value(Map集合像一个罐子,它里面的每项数据都由两个值组成);

Collection和Iterator接口

Collection用法:添加元素,删除元素,返回Collection集合的元素个数以及清空整个集合.(方法无须死记硬背,只需牢记一点:集合类就像容器,现实生活中的容器有哪些功能,集合提供了哪些对应的方法)

遍历集合的方法

1使用Lambda表达式遍历集合

2使用java8增强的Iterator遍历结合元素

3使用Lambda表达式遍历Iterator

4使用foreach循环遍历集合元素(重点)

import java.util.*;
public class ForeachTest
{
    public static void main(String[] args)
    {
        // 创建集合、添加元素的代码与前一个程序相同
        Collection books = new HashSet();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂Android讲义"));
        for (Object obj : books)
        {
            // 此处的book变量也不是集合元素本身
                        //可以认为book是引用变量
                        //obj是迭代变量,foreach循环中修改迭代变量的值也没有任何实际意义.
            String book = (String)obj;
            System.out.println(book);
            if (book.equals("疯狂Android讲义"))
            {
                // 下面代码会引发ConcurrentModificationException异常
                books.remove(book);     //①
            }
        }
        System.out.println(books);
    }
}

使用foreach循环来迭代访问Collection集合里的元素更加简洁,这正是JDK1.5的foreach循环带来的优势.
foreach循环中的迭代变量也不是集合元素本身,系统只是依次把集合元素赋给迭代变量,因此foreach循环中修改迭代变量的值也没有任何实际意义.
books.remove(book);使用foreach循环迭代访问集合元素时,该集合也不能被改变,否则将引ConcurrentModificationException异常

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

6使用Java8新增的Stream操作集合

Set集合

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

HashSet类

多数情况下使用Set集合就是这个实现类,HashSetHash算法来存储集合中的元素,具有很好的存取和查找性能.---------(hash算法的价值在于速度,当需要查询集合中某个元素时,hash算法可以直接根据该元素的hashCode值计算出该元素的存储位置)

HashSet特点:

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

HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置.如果两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功.

HashSet集合通过判断两个元素相等的标准是:两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等.HashSet访问集合元素时也是根据元素的hashCode值来快速定位的,如果HashSet中两个以上元素具有相同的hashCode,将会导致性能下降.

HashSet中性能下降的原因:HashSet中每个能存储元素的槽位通常称为桶,如果有多个hashCode值相同,但它们通过equals()方法相比返回false,就需要在一个桶里放多个元素,这样会导致性能下降

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

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

重写hashCode()的一般步骤
见P294

总结:equals()方法判断两个对象是否相同,hashCode()方法判断两个对象在HashSet中的位置.并注意要与Set集合的规则相一致起来即:无序不重复

LinkedHashSet类(HashSet的子类)

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集合也是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序,使得元素看起来是以插入的顺序保存的.当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素.

因为LinkedHashSet需要维护元素的插入顺序,因此性能略低于HahSet的性能,在迭代访问Set里的全部元素时将有很好的性能,因此它以链表来维护内部的顺序.

虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复

TreeSet类

TreeSetSortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态.(sorted单词的意思:分类的,挑选的)

笔试常用的方法:
因为TreeSet中的元素时有序的,所以增加了访问第一个,前一个,后一个,最后一个元素的方法,并提供了三个从TreeSet中截取TreeSet的方法.

TreeSet的通用用法:

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]
    }
}
[-9, 2, 5, 10]
-9
10
[-9, 2]
[5, 10]
[2]

TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素的实际大小进行排序的.

HashSet采用hash算法来决定元素的存储位置的不同,TreeSet采用红黑树的数据结构来存储集合元素.TreeSet支持两种排序方法:一种是自然排序,一种是定制排序.默认状态下TreeSet采用自然排序.

自然排序

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式属于自然排序.
Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小.

image.png

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

class Err{}
public class TreeSetErrorTest
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        // 向TreeSet集合中添加两个Err对象
        //只有一个元素无须实现Comparable接口
        ts.add(new Err());
        //后面添加的所有元素都必须实现Comparable接口
        ts.add(new Err());  //①
    }
}

结果为:

Exception in thread "main" java.lang.ClassCastException: Err cannot be cast to j
ava.lang.Comparable
        at java.util.TreeMap.compare(TreeMap.java:1294)
        at java.util.TreeMap.put(TreeMap.java:538)
        at java.util.TreeSet.add(TreeSet.java:255)
        at TreeSetErrorTest.main(TreeSetErrorTest.java:21)

向TreeSet集合中添加两个Err对象,只有一个元素无须实现Comparable接口,后面添加的所有元素都必须实现Comparable接口,当然这样也不是一件好方法,当试图从TreeSet中取出元素时,依然会引发ClassCastException异常.

当试图把一个对象添加到TreeSet集合时,TreeSet会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较----这就要求集合中的其他元素与该元素是同一个类的实例.

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

原因便在于 Date对象的compareTo(Object obj)方法无法与字符串对象比较大小.必须要么都是Date对象要么都是字符串对象
如果想TreeSet中添加的对象时程序员自己定义的对象,则可以向TreeSet中添加多种不同类型的对象,前提是用户自定义实现了Comparable接口,且实现compareTo(Object obj)方法没有进行强制类型转换.但当取出TreeSet中的集合元素时,不同类型的元素依然会发生ClassCastException异常.

总结:如果希望TreeSet能正常工作,TreeSet只能添加同一种类型的对象.

当把一个对象添加到TreeSet集合中时,TreeSet调用该对象的compareTo(Object obj)方法与容器中的其他对象相比较,然后根据红黑树结构找到它的存储位置.如果两个对象通过compareTo(Object obj)方法比较相等,那么新对象无法添加到TreeSet集合中去.-----------与Set集合无序不可重复的特性相对应起来

对于TreeSet集合来说判断两个元素相等的唯一标准是:两个对象通过compareTo(Object obj)方法方法比较是否返回0,如果返回0那么TreeSet会认为它们相等.否则认为他们不相等.

class Z implements Comparable
{
    int age;
    public Z(int age)
    {
        this.age = age;
    }
    // 重写equals()方法,总是返回true
    public boolean equals(Object obj)
    {
        return true;
    }
    // 重写了compareTo(Object obj)方法,总是返回1
    public int compareTo(Object obj)
    {
        return 1;
    }
}
public class TreeSetTest2
{
    public static void main(String[] args)
    {
        TreeSet set = new TreeSet();
        Z z1 = new Z(6);
        set.add(z1);
        // 第二次添加同一个对象,输出true,表明添加成功
        System.out.println(set.add(z1));    //①
        // 下面输出set集合,将看到有两个元素
        System.out.println(set);
        // 修改set集合的第一个元素的age变量
         ((Z)(set.first())).age = 9;
        // 输出set集合的最后一个元素的age变量,将看到也变成了9
        System.out.println(((Z)(set.last())).age);
    }
}

当需要把一个对象放入到TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致的结果:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)方法方法比较应返回0.要跟Set集合的规则对应起来

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

class R implements Comparable
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R[count:" + count + "]";
    }
    // 重写equals方法,根据count来判断是否相等
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if(obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return r.count == this.count;
        }
        return false;
    }
    // 重写compareTo方法,根据count来比较大小
    public int compareTo(Object obj)
    {
        R r = (R)obj;
        return count > r.count ? 1 :
            count < r.count ? -1 : 0;
    }
}
public class TreeSetTest3
{
    public static void main(String[] args)
    {
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
        // 打印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);    // ①
        // 取出第一个元素
        R first = (R)ts.first();
        // 对第一个元素的count赋值
        first.count = 20;
        // 取出最后一个元素
        R last = (R)ts.last();
        // 对最后一个元素的count赋值,与第二个元素的count相同
        last.count = -2;
        // 再次输出将看到TreeSet里的元素处于无序状态,且有重复元素
        System.out.println(ts);   // ②
        // 删除实例变量被改变的元素,删除失败
        System.out.println(ts.remove(new R(-2)));   // ③
        System.out.println(ts);
        // 删除实例变量没有被改变的元素,删除成功
        System.out.println(ts.remove(new R(5)));    // ④
        System.out.println(ts);
    }
}

为了让程序变得更加健壮推荐不要修改HashSet和TreeSet结合中元素的关键实例变量.

定制排序

如果需要实现定制排序,比如降序排序,可以通过Comparator接口帮助.该接口里定义了一个int compare(T a,T b)方法,该方法用来比较a和b的大小,如果该方法返回正整数,表明a>b;如果该方法返回0,表明a=b;如果返回负整数,表明a<b.

如果需要定制排序,需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合相关联,由该Comparator对象负责集合元素的排序逻辑.由于该Comparator是一个函数式接口,因此可以用Lambda表达式来代替Comparator对象.

class M
{
    int age;
    public M(int age)
    {
        this.age = age;
    }
    public String toString()
    {
        return "M[age:" + age + "]";
    }
}
public class TreeSetTest4
{
    public static void main(String[] args)
    {
        // 此处Lambda表达式的目标类型是Comparator
        TreeSet 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);
    }
}

由TreeSet关联的Lambda表达式来负责集合元素的排序.


image.png

EnumSet类

EnumSet是一个专门为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式指定,EnumSet的集合元素也都是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序.
EnumSet在内部以位向量的形式存储
EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常.
程序应该通过EnumSet提供的类方法来创建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]
    }
}

各Set实现类的性能分析

HashSetTreeSet比较:HashSet的性能总是比TreeSet好(特别是常用的添加,查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的顺序,只有当想要一个保持次序的Set时,才应该使用TreeSet,否则都应该用HashSet

LinkedHashSet:对于普通的插入删除操作,LinkedHashSet比HashSet要略微慢一点,因为它要维护链表所带来的额外的开销,但由于有了链表,遍历LinkedHashSet会更快

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

Set类中的HashSet,TreeSet,EnumSet都是线程不安全的,如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性.可以通过Collection工具类的synchronizedSortedSet方法来包装该Set集合,此操作最好在创建时进行,以防止对Set集合的意外非同步访问.

List集合

List集合代表一种有序,可以重复的集合,集合中的每一个元素都有其对应的顺序索引.List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素.List可以根据位置索引来访问集合中的元素,因此List增加了一种新的遍历集合的方法:使用普通的for循环来遍历集合元素.List默认按照元素的添加顺序设置元素的索引,例如第一次添加的元素索引是0,第二次添加的元素索引是1......

List接口和ListIterator

List作为Collection接口的子接口,可以使用Collection接口里的全部方法.由于List有序集合,List集合里增加了一些根据索引来操作集合元素的方法.

image.png

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讲义"))); //①
        //将第二个元素替换成新的字符串对象
        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讲义]

List判断两个对象相等的标准是:只要通过equals()方法比较返回true即可.所以①处代码才会有此现象.再看如下程序:

class A
{
    public boolean equals(Object obj)
    {
        return true;
    }
}
public class ListTest2
{
    public static void main(String[] args)
    {
        List books = new ArrayList();
        books.add(new String("轻量级Java EE企业应用实战"));
        books.add(new String("疯狂Java讲义"));
        books.add(new String("疯狂Android讲义"));
        System.out.println(books);
        // 删除集合中A对象,将导致第一个元素被删除
        books.remove(new A());     // ①
        System.out.println(books);
        // 删除集合中A对象,再次删除集合中第一个元素
        books.remove(new A());     // ②
        System.out.println(books);
    }
}
[轻量级Java EE企业应用实战, 疯狂Java讲义, 疯狂Android讲义]
[疯狂Java讲义, 疯狂Android讲义]
[疯狂Android讲义]

分析:当程序试图删除一个A对象时,List会调用该A对象的equals()方法依次与集合中的元素进行比较,如果该equals()方法以某个集合元素作为参数时返回true,List将会删除该元素--------A类重写了equals()方法,该方法总是返回true,所以每次从List集合中删除A对象时,总是删除第一个元素.

image.png

Java8List集合新增的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]
    }
}
[疯狂iOS讲义, 疯狂Java讲义, 疯狂Android讲义, 轻量级Java EE企业应用实战]
[7, 8, 11, 16]
请按任意键继续. . .

List提供的listIterator()方法


image.png

ListIterator用法:

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讲义

使用ListIterator迭代List集合时,开始也需要采用正向迭代,即先用next()方法进行迭代,在迭代过程中使用add()方法向上一个迭代元素的后面添加一个新元素.

ArrayList和Vector实现类(ArrayList是重点!!)

ArrayList和Vector是基于数组实现的List类,这两个类中封装了一个动态的,允许再分配的Object[]数组.ArrayList和Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出了该数组的长度的时候,它们的initialCapacity会自动增加.

可以创建它们的时候就指定initialCapacity的大小.如果创建空的ArrayList或Vector集合时不指定initialCapacity参数,则Object[]数组的默认长度为10.

//创建空的ArrayList不指定initialCapacity参数,则Object[]数组的默认长度为10.
ArrayList list=new ArrayList();
//创建ArrayList的时候就指定initialCapacity的大小为20
ArrayList list=new ArrayList(20);
image.png

ArrayList和Vector的区别:ArrayList是线程不安全的,当多个线程同时访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;Vector集合时线程安全的,无须程序保证该集合的同步性,因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低.实际上,即使Vector是线程安全的我们也很少用,因为这个方法太古老了,虽然它采用模拟栈的结构但是我们也不用它,如果程序用到栈这种结构,我们一般考虑ArrayDeque

固定长度的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讲义");
    }
}
class java.util.Arrays$ArrayList
疯狂Java讲义
轻量级Java EE企业应用实战
Exception in thread "main" java.lang.UnsupportedOperationException
        at java.util.AbstractList.add(AbstractList.java:148)
        at java.util.AbstractList.add(AbstractList.java:108)
        at FixedSizeList.main(FixedSizeList.java:24)

Queue集合(队列)

Queue通常用来模拟队列这种数据结构,队列通常是指"先进先出"(FIFO)的容器,队列头部保存的是在队列中存放时间最长的元素,队列尾部保存再队列中存放时间最短的元素.新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素.队列通常不允许随机访问队列中的元素

image.png

Queue集合有一个PriorityQueue实现类,Queue还有一个Deque接口,Deque代表一个"双端队列",双端队列可以同时向两端添加删除元素,因此Deque的实现类可以既当做队列使用,也可以当做栈来使用.Java为Deque提供了ArrayDequeLinkedList两个实现类.

当模拟队列时要求Deque的实现类在队首删除元素,队尾添加元素模拟先入先出的规则
当模拟栈时要求Deque的实现类push,poll元素,模拟先入后出的规则

PriorityQueue(用的很多吗??)

PriorityQueue是一个比较标准的队列的实现类:PriorityQueue保存的队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行排序.因此当调用peak()方法或者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());
        System.out.println(pq.poll());
        System.out.println(pq.poll());
        System.out.println(pq.poll());
        System.out.println(pq); 
    }
}
[-3, 6, 20, 18]
-3
6
18
20
[]

PriorityQueue不允许插入null元素,它还需要对队列元素进行排序.

PriorityQueue有两种排序方式(和TreeSet很相似):
自然排序:采用自然排序的PriorityQueue集合中的元素必须实现了Comparable接口,并且应该是同一个类的多个实例,否则会抛出ClassCastException异常
定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序,采用定制排序时不要求队列元素实现Comparable接口

Deque与ArrayDeque实现类

Deque接口是Queue接口的子接口,它代表了一个双端队列,Deque接口中定义了一些双端队列的方法来从两端`来操作队列的元素.

image.png

Deque接口提供了一个典型的实现类:ArrayDeque,它是一个基于数组实现双端队列,创建Deque时,同样需要指定一个numElements参数,该参数用于指定Object[]数组的长度,如果没有指定,Deque底层数组的长度是16,ArrayList10

ArrayListArrayDeque两个集合类的实现机制基本类似,它们底层都采用一个动态的,可重新分配的Object[]数组来存储集合元素,当集合元素超出了该数组的容量时,系统就会在底层重新分配一个Object[]数组来存储集合元素.

ArrayDeque模拟栈来使用:

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

当程序中需要使用"栈"这种数据结构时,推介使用ArrayDeque

ArrayDeque当做队列使用,按照先入先出的方式操作集合

public class ArrayDequeQueue
{
    public static void main(String[] args)
    {
        ArrayDeque queue = new ArrayDeque();
        // 依次将三个元素加入队列
        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());
        // 依然输出:[疯狂Java讲义, 轻量级Java EE企业应用实战, 疯狂Android讲义]
        System.out.println(queue);
        // poll出第一个元素,输出:疯狂Java讲义
        System.out.println(queue.poll());
        // 输出:[轻量级Java EE企业应用实战, 疯狂Android讲义]
        System.out.println(queue);
    }
}

上面的程序显示了ArrayDeque不仅可以作为栈使用还可以作为队列使用

LinkedList实现类

LinkedList既是List接口的实现类又是Deque接口的实现类;
作为List接口的实现类,意味着它可以根据索引来随机访问集合中的元素.
作为Deque接口的实现类,意味着它可以被当做是双端队列来使用,既能当做栈也能当做队列

LinkedList集合的用法代码如下:

import java.util.*;
public class LinkedListTest
{
    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企业应用实战]
请按任意键继续. . .

总结:
队列的头部相当于栈的顶部(很重要)
队列相关的方法:
将元素插入双端队列我们一般用offer(Object e)方法: offerFirst(),offerLast()
获取但不删除双端队列的元素我们一般用peek()方法:peekFirst(), peekLast()
获取且删除双端队列的元素我们一般用poll()方法:pollLast(),pollFirst()
栈相关的方法:
pop出该双端队列所表示的栈的栈顶元素:pop()
将一个元素push进该双端队列所表示的栈的栈顶:push(Object e)

我们必须掌握的方法有offer() push() pop() offerFirst() offerLast() peekFirst() peekLast() pollLast() pollFirst()

LinkedListArrayList,ArrayDeque的实现机制完全不同,ArrayList和ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问具有较好的性能;而LinkedList内部是以链表的形式来保存集合里的元素,因此随机访问集合元素的性能较差,但在插入,删除元素时性能比较出色(只需改变指针所指的地址即可)

对于所有的内部基于数组的集合实现,例如ArrayList和ArrayDeque,使用随机方法的性能都要比Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问.

各种线性表的性能分析

Java提供的List就是一个线性表的接口,而ArrayList,LinkedList又是线性表的两种典型的实现:基于数组的线性表和基于链表的线性表.
Queue代表了队列,Deque又代表双端队列(既可以作为队列使用,又可以作为栈来使用)

我们要知道LinkedList集合不仅提供了List的功能还提供了双端队列,栈的功能

由于数组以一块连续的内存区来保存所有的数组元素,所以数组在随机遍历集合的元素的时候性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比价好;内部以链表作为底层实现的集合在执行插入,删除操作时具有较好的性能.总体来说ArrayListLinkedList的性能要好,大部分时候应该考虑使用ArrayList

如果需要遍历List集合的元素,对于ArrayList,Vector集合,应该采用随机访问的方法(get)来遍历集合元素,这样性能最好,对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素
如果需要经常插入删除操作来改变包含大量数据的List集合的大小,可以考虑使用LinkedList集合
如果有多个线程同时访问List集合的元素,开发者可以考虑使用Collection将集合包装成线程安全的集合.

Map集合

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


image.png

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

Set接口下有HashSet,LinkedHashSet,SortedSet(接口),TreeSet,EnumSet等子接口和实现类
Map接口下有HashMap,LinkedHashMap,SortedMap(接口),TreeMap,EnumMap等子接口和实现类
正如名字所示:Map的这些实现类和子接口中的key集的存储形式和对应的Set集合中元素的存储形式完全相同.

从Java源码来看,Java是先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合.

如果把Map中的所有value放在一起来看,它们又非常类似于是一个List:元素和元素之间可以允许重复,每个元素都可以根据索引来查找,只是Map中的索引不再使用整数值,而是以另一个对象作为索引.
如果需要从List集合中取出元素,则需要提供该元素的数字索引;如果需要从Map中取出元素,需要提供该元素的key索引.Map也被称为字典,或关联数组.


image.png

Map集合最典型的的用法就是成对的添加,删除key-value对,接下来即可判断该Map中是否包含指定key,是否包含指定的value.也可以通过Map提供的keySet()方法获取所有的key组成的集合,进而遍历Map中所有的key-value对.
Map典型功能的程序:

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, 疯狂iOS讲义=99, 轻量级Java EE企业应用实战=99, 疯狂Java讲义=109
}
是否包含值为 疯狂iOS讲义 key:true
是否包含值为 99 value:true
疯狂Ajax讲义-->79
疯狂iOS讲义-->99
轻量级Java EE企业应用实战-->99
疯狂Java讲义-->109
{疯狂iOS讲义=99, 轻量级Java EE企业应用实战=99, 疯狂Java讲义=109}

Map中包括一个内部类Entry,该内部类封装了一个key-value对.Entry包括如下三个方法:
Object getKey():返回该Entry里包含的key
Object getValue():返回该Entry里包含的value
Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value

HashMap重写了toString()方法,实际上所有的Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式的字符串:{key1=value1,key2=value2...}

Java8新增为Map新增的方法:

import java.util.*;
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}

HashMap和Hashtable实现类

HashMapHashtable都是Map接口的典型实现类,它们之间的关系类似于ArrayListVector的关系:Hashtable是一个古老的Map实现类,它包含两个繁琐的方法,即elements()keys().

Java8改进了HashMap的实现,使用HashMap存在key冲突的时依然具有良好的性能.

HashMap和Hashtable存在的不同之处

  • 1.Hashtable线程安全的,跟Vector一样,都是线程安全的.HashMap是线程不安全的,所以HashMapHashtable性能高一点;如果有多个线程访问同一个HashMap对象时,使用Hashtable实现类会更好,如果一定要坚持用HashMap,需用Collection集合对其进行相应的同步操作
  • 2.Hashtable不允许使用null作为keyvalue,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为keyvalue
    由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的keynull,但可以有无数多个key-value对的valuenull.
public class NullInHashMap
{
    public static void main(String[] args)
    {
        HashMap hm = new HashMap();
        // 试图将两个key为null的key-value对放入HashMap中
        hm.put(null , null);
        hm.put(null , null);    // ①
        // 将一个value为null的key-value对放入HashMap中
        hm.put("a" , null);    // ②
        // 输出Map对象
        System.out.println(hm);
    }
}

结果为:

{null=null, a=null}

①代码处无法将key-value对放入,因为Map中已经有一个key-value对的keynull,所以无法再放入keynull值的key-value

尽量少用Hashtable实现类,即使需要创建线程安全的Map实现类,也无须使用Hashtable实现类,可以通过Collection工具类将HashMap变为线程安全的.

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

HashSet不能保证元素的顺序一样,HashMap和Hashtable也不能保证其中key-value对的顺序.

类似于HashSet:
HashMap.Hashtable判断两个key相等的标准是:两个key通过equals()方法比较返回true,两个keyhashCode值也相等.

HashMap,Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value.
HashMap,Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可.(跟List很像)

下面程序示范了Hashtable判断两个key相等的标准和两个value相等的标准.

import java.util.*;
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("测试字符串")); // ① 输出true
        // 只要两个A对象的count相等,它们通过equals比较返回true,且hashCode相等
        // Hashtable即认为它们是相同的key,所以下面输出true。
        //两个A对象虽然不是同一个A对象,但它们通过equals()方法比较返回true且hashCode值相等
        System.out.println(ht.containsKey(new A(87563)));   // ② 输出true
        // 下面语句可以删除最后一个key-value对
        ht.remove(new A(1232));    //③
        System.out.println(ht);
    }
}

如果重写该类的hashCode()equals()方法,则应该保证两个方法的判断标准一致-----当两个key通过equals()方法比较返回true时,两个keyhashCode()返回值也应该相同.因为HashMap,Hashtable保存key的方式与HashSet保存集合元素的方式完全相同,所以HashMap,Hashtablekey的要求与HashSet对集合元素的要求要完全相同.

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

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

HashSet类似的是,尽量不要使用可变对象作为HashMap,Hashtablekey,如果确实需要可变对象作为HashMap,Hashtable的key,则尽量不要在程序中修改作为key的可变对象

LinkedHashMap

HashSet中有个LinkedHashMap子类,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对
         //这是Java8为Map新增的forEach()方法来遍历Map集合
         //LinkedHashMap可以记住key-value对的添加顺序
        scores.forEach((key, value) -> System.out.println(key + "-->" + value));
    }
}

使用Properties读写属性文件

Properties相当于一个key,value都是String类型的Map
Properties类时Hashtable的子类,该对象在处理属性文件的时候特别方便.Properties类把Map对象和属性文件关联在一起,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件的"属性名=属性值"加载到Map对象中.
常用方法演示:

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

Properties还可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对.

SortedMap接口和TreeMap实现类

Set接口派生出SortSet接口一样,SortSet有一个TreeSet实现类一样.Map接口也有一个SortMap子接口,SortMap有一个TreeMap实现类,

TreeMap就是一个红黑树的数据结构,每个key-value对即作为红黑树的一个节点,TreeMap存储key-value对(节点)时,需要根据key对结点排序,TreeMap可以保证所有的key-value对处于有序状态.

TreeMap也有两种排序方式(和TreeSet相似):
自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象
定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序.采用定制排序时.采用定制排序时不要求Mapkey实现Comparable接口

TreeSet判断两个元素相等的标准相似,TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的.

如果想要使用自定义类作为TreeMapkey,且让TreeMap正常工作,重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,它们通过compareTo()方法比较应该返回0.如果equals()方法与compareTo()方法的返回结果不一致,TreeMap与Map接口的规则就会冲突

SetMap的关系十分密切,Java是先实现了HashMap,TreeMap等集合,然后通过包装一个所有的value都为nullMap集合实现了Set集合类.(重要!!!)

因为TreeMap中的key-value对是有序的,所以增加了第一个,前一个,后一个,最后一个key-value对的方法,并提供了几个从TreeMap中截取子TreeMap的方法(左闭右开)
如下程序时TreeMap的几种常用方法:

class R implements Comparable
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R[count:" + count + "]";
    }
    // 根据count来判断两个对象是否相等。
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return r.count == this.count;
        }
        return false;
    }
    // 根据count属性值来判断两个对象的大小。
    public int compareTo(Object obj)
    {
        R r = (R)obj;
        return count > r.count ? 1 :
            count < r.count ? -1 : 0;
    }
}
public class TreeMapTest
{
    public static void main(String[] args)
    {
        TreeMap 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)));
    }
}

WeakHashMap实现类

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

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("中等"));    // ①
        // 输出whm对象,将看到4个key-value对。
        System.out.println(whm);
        // 通知系统立即进行垃圾回收
        System.gc();
        System.runFinalization();
        // 通常情况下,将只看到一个key-value对。
        System.out.println(whm);
    }
}
{英文=中等, java=中等, 数学=及格, 语文=良好}
{java=中等}

添加的这三个key都是匿名的字符串对象,WeakHashMap只保留了对它们的弱引用,这样垃圾回收时自动删除这三个key-value对
如果想要使用WeakHashMapkey来保留对象的弱引用,则不要让该key所引用的对象具有任何强引用,否则将失去使用WeakHashMap的意义.

IdentityHashMap实现类

IdentityHashMap的实现机制和HashMap基本类似,但它在处理两个key相等的时候比较独特:在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1key2通过equals()方法比较返回true,且它们的hashCode值相等即可.

IdentityHashMap是一个特殊的Map实现,该类实现Map接口时,它有意违反Map的通常规范;IdentityHashMap要求两个key严格相等才认为两个key相等

IdentityHashMap也允许使用null作为keyvalue.与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);
        // 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
        ihm.put("java" , 93);
        ihm.put("java" , 98);
        System.out.println(ihm);
    }
}

前两个key-value对中的key是新创建的字符串对象,它们通过==比较不相等,所以IdentityHashMap会把它们当成2个key来处理;后2个key-value对中的key都是字符串直接量,而且它们的字符串序列完全相同,Java使用常量池来管理字符串直接量,所以它们通过==比较返回true,IdentityHashMap会认为它们是同一个key,因此只有一次可以添加成功

EnumMap实现类

EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值.创建EnumMap时必须显式或隐式指定它对应的枚举类

EnumMap具有如下特征:

  • 1.EnumMap在内部以数组形式保存
  • 2.EnumMap根据key的自然顺序(即枚举值在枚举值在枚举类中的定义顺序)来维护key-value对的顺序,
  • 3.EnumMap不允许使用null作为key,但允许使用null作为value.
    创建普通的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);
    }
}

创建该EnumMap对象时指定它的key只能是Season枚举类的枚举值.如果向EnumMap中添加两个key-value对后,两个key-value对将会以Season枚举值的自然顺序排序.

{SPRING=春暖花开, SUMMER=夏日炎炎}

Map实现类的性能分析

虽然HashMapHashtable实现机制基本上一样,但由于Hashtable是一个古老的,线程安全的实现类,所以HashMap要比Hashtable

TreeMap通常要比HashMapHashtable(尤其是在插入,删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)

TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作,当TreeMap被填充之后,就可以调用keySet(),去的key组成的Set集合,然后使用toArray()方法生成key的数组,接下来使用ArraysbinarySearch()方法在已排序的数组中快速地查找对象

一般情况下,可以多考虑使用HashMap,因为HashMap正是为快速查询设计的,HashMap底层其实也是采用数组来存储key-value对.如果程序需要一个总是排好序的Map时,则可以考虑一下使用TreeMap

LinkedHashMapHashMap要慢一点,因为它需要维护链表来保持Mapkey-value时的添加顺序.

IdentityHashMap没有什么特别出彩之处,只不过它采用与HashMap基本类似的实现,只是它使用==而不是equals()方法来判断元素相等.

EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key.

HashSet和HashMap的性能选项(重点)

对于HashSet及其子类而言,采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;
对于HashMap,Hashtable及其子类而言,它们采用hash算法来决定Mapkey的存储位置,并通过hash算法来增加key集合的大小

hash表可以存储元素的位置被称为"桶",在通常情况下,单个"桶"里存储一个元素,此时具有最好的性能:hash算法可以根据hashCode值计算出"桶"的存储位置,接着从"桶"中取出元素.但hash表的状态是open的:在发生hash冲突的情况下,单个桶会存储多个元素,这些元素以链表的形式存储,必须按顺序搜索.

如图表示hash表保存各个元素,且发生hash冲突的情况


image.png

因为HashSet,HashMap,Hashtable都是采用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSetHashMaphash表具有如下属性:

容量:hash表中桶的数量
初始化容量:创建hash表时桶的数量.HashMapHashSet都允许在构造器中指定初始化容量
尺寸:当前hash表中记录的数量
负载因子:负载因子等于尺寸/容量.负载因子等于0表示空的hash表.0.5表示半满的hash表,轻负载的hash表具有冲突少,适合插入和查询的特点(但是在使用Iterator迭代元素时反应较慢)

负载极限:负载极限是一个0~1的数值,负载极限决定了hash表的最大填满程度.当hash表中的负载因子达到指定的"负载极限"时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称rehashing

HashSet,HashMap,Hashtable的构造器都允许指定一个负载极限,它们默认的负载极限是0.75,这表明当该hash表的3/4已经被填满时,hash表会rehashing

负载极限的默认值0.75是时间和空间成本的一个折中:较高的负载极限可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMapget()put()方法都用到了查询);较低的负载极限会提高查询数据的性能,但会增加hash表所占用的内存开销.程序员可以根据实际情况来调整HashSetHashMap的负载极限值.

如果开始就知道HashSetHashMap,Hashtable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSetHashMap,Hashtable所包含的最大记录数除以"负载极限",就不会发生rehashing.使用足够大的初始化容量创建HashSet和HashMap,Hashtable时,可以更高效的增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置的过高.

操纵集合的工具类:Collections

Collections工具类提供了大量方法对集合元素进行排序,查询和修改等操作.还提供了将集合对象设置为不可变,对集合对象实现同步控制等方法.

排序

注意都是类方法
下面程序简单示范了Collection工具类来操纵List集合

import java.util.*;
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); // 将List集合元素的次序反转
        System.out.println(nums); // 输出:[0, 3, -5, 2]
        Collections.sort(nums); // 将List集合元素的按自然顺序排序
        System.out.println(nums); // 输出:[-5, 0, 2, 3]
        Collections.shuffle(nums); // 将List集合元素的按随机顺序排序,模拟洗牌shuffle动作
        System.out.println(nums); // 每次输出的次序不固定
    }
}

查找和替换

下面程序简单示范了Collection工具类的用法

import java.util.*;
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); // 对nums集合排序
        System.out.println(nums); // 输出:[-5, 1, 2, 3]
        //只有排序后的List集合才可用二分法查询,输出3
        System.out.println(Collections.binarySearch(nums , 3));
    }
}

同步操作

Collections类提供了多个synchronizedXxx()方法,该方法可以将指定的集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全的问题.

Java中的常用集合框架中的实现类:HashSet,TreeSet,ArrayList,ArrayDeque,LinkedList,HashMapTreeMap都是线程不安全的,如果有多个线程同时访问它们,而且又超过一个线程试图修改它们,则存在线程安全问题.
Collections提供了多个类方法可以将它们包装成线程同步的集合.
如下程序示范了创建4个线程安全的集合对象

import java.util.*;
public class SynchronizedTest
{
    public static void main(String[] args)
    {
        // 下面程序创建了四个线程安全的集合对象
        Collection c = Collections
            .synchronizedCollection(new ArrayList());
        List list = Collections.synchronizedList(new ArrayList());
        Set s = Collections.synchronizedSet(new HashSet());
        Map m = Collections.synchronizedMap(new HashMap());
    }
}

上面程序中直接将新创建的集合对象传给了CollectionssynchronizedXxx方法,这样就可以直接获取List,SetMap线程安全实现版本.

设置不可变集合

Collections提供了三个类方法来返回一个不可变的集合.返回值是该集合的"只读"版本,不可变集合只能访问集合元素,不可修改集合元素.
如下代码

import java.util.*;
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("测试元素");   //①
        unmodifiableSet.add("测试元素");    //②
        unmodifiableMap.put("语文" , 90);   //③
    }
}

繁琐的接口:Enumeration

略太古老了不想用 .....

推荐阅读更多精彩内容

  • 上一篇文章介绍了Set集合的通用知识。Set集合中包含了三个比较重要的实现类:HashSet、TreeSet和En...
    Ruheng阅读 10,391评论 3 54
  • 10.1 Set集合 Set接口继承Collection接口,没有提供额外的方法。Set集合不允许包含相同...
    王振琦阅读 77评论 0 0
  • 集合概述 集合用来储存数量不等的对象,且只能保存对象,实际保存的是对象的引用变量 主要由两个接口派生而出,Coll...
    Utte阅读 43评论 0 0
  • 3.3 集合 一方面, 面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另...
    闫子扬阅读 224评论 0 1
  • 集合概述 •Java提供集合类,集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。所有集合类都位于jav...
    IT_唐小探阅读 45评论 0 0