骏马金龙 (新博客:www.junmajinlong.com)

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!

java集合框架(Collections Framework)

简介

集合和数组的区别:

  • 数组存储基础数据类型,且每一个数组都只能存储一种数据类型的数据,空间不可变。
  • 集合存储对象,一个集合中可以存储多种类型的对象。空间可变。

严格地说,集合是存储对象的引用,每个对象都称为集合的元素。根据存储时数据结构的不同,分为几类集合。但对象不管存储到什么类型的集合中,既然集合能存储任何类型的对象,这些对象在存储时都必须向上转型为Object类型,也就是说,集合中的元素都是Object类型的对象

既然是集合,无论分为几类,它都有集合的共性,也就是说虽然存储时数据结构不一样,但该有的集合方法还是得有。在java中,Collection接口是集合框架的根接口,所有集合的类型都实现了此接口或从其子接口中继承。

Collection接口

根据数据结构的不同,一些collection允许有重复的元素,而另一些则不允许。一些collection是有序的,而另一些则是无序的。

Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的"子接口"如List和Set也就是说,无法直接new一个collection对象,而是只能new一个实现Collection类的子接口的对象,如new ArrayList();

所有的Collection类都必须至少提供两个构造方法:无参数构造方法构造一个空集合;带Collection参数的构造方法构造一个包含该Collection内容的集合。例如,ArrayList就有3个构造方法,其中之二就满足这两个构造方法的要求。

Collection是java.util包中的类,因此要实现集合的概念,需要先导入该包。

ArrayList继承自List接口,List接口又继承自Collection接口。ArrayList类存储的集合中,元素有序、可重复

import java.util.*;
Collection coll = new ArrayList();

因为Collection接口不允许直接实现,因此需要通过实现它的子类来实现集合的概念,此处创建的是ArrayList对象,使用了父类引用,好处是扩展性较好。

Collection有一些集合的通用性操作方法,分为两类:一类是普通方法;一类是带有All的方法,这类方法操作的是集合。

  1. add():向集合的尾部插入元素,返回值类型为boolean,插入成功返回true。注意集合只能存储对象(实际上是对象的引用)

    import java.util.*;
    //
    public class TestColl {
     public static void main(String[] args) {
       Collection coll = new ArrayList();
       coll.add("abcd");   //插入字符串对象
       coll.add(123);      //插入Int对象
       coll.add(123);
       coll.add(new Student("Gaoxiaof",23));  //插入Student对象
       coll.add(new Student("Gaoxiaof",23));  //插入另一个Student对象
       System.out.println(coll); //直接输出集合中的元素,得到结果[abcd,123,123,Gaoxiaof 23,Gaoxiaof 23]
     }
    }
    //
    class Student {
     private String name;
     private int age;
     Student(String name,int n) {
         this.name = name;
         this.age = n;
     }
     public String getName() {
         return this.name;
     }
     public int getAge() {
         return this.age;
     }
     public String toString() {
         return this.name + " " + this.age;
     }
    }
    

    上面插入的"abcd"和"123"都是经过自动装箱转换为对象后存储在集合中的。其中两个add(123)是重复的对象元素,因为判断集合中的元素是否重复的唯一方法是equals方法是否返回0。Integer已经重写过equals()。而后面的两个Student对象是不同对象,因为Student类中没有重写equals()方法,所以它们是不重复的元素。

  2. remove():删除集合中首次出现的元素。确定是否能删除某个元素,唯一的方法是通过equals()方法确定对象是否相等,相等时删除才返回true。

    Collection coll = new ArrayList();
    coll.add("abcd");
    coll.add(new Integer(128));
    coll.add(new Student("Gaoxiaofang",23));
    System.out.println(coll.remove(new Integer(128)));  //true
    coll.remove(new Student("Gaoxiaofang",23));         //false,因为没有重写equals()
    System.out.println(coll);   //return: [abcd,Gaoxiaofang 23]
    
  3. clear():清空该集合中的所有元素。
  4. contains(object obj):是否包含某对象元素。判断的依据仍然是equals()方法。
    Collection coll = new ArrayList();
    coll.add(new Integer(128));
    System.out.println(coll.contains(new Integer(128)));  //true
    
  5. isEmpty():集合是否不包含任何元素。
  6. size():返回该集合中元素的个数。
  7. equals(Object obj):比较两个集合是否完全相等。依据是集合中的所有元素都能通过各自的equals得到相等的比较。
  8. addAll(Collection c):将整个集合c中的元素都添加到该集合中。
  9. containsAll(Collection c):该集合是否包含了c集合中的所有元素,即集合c是否是该集合的子集。
  10. removeAll(Collection c):删除该集合中那些也包含在c集合中的元素。即删除该集合和c集合的交集元素。
  11. retainAll(Collection c):和removeAll()相反,仅保留该集合中和集合c交集部分的元素。
  12. iterator(Collection c):返回此集合中的迭代器,注意返回值类型为Iterator。迭代器用于遍历集合。见下文。

Iterator通用迭代器

因为不同类型的集合存储数据时数据结构不同,想要写一个通用的遍历集合的方法是不现实的。但无论是哪种类型的集合,只有集合自身对集合中的元素是最了解的,因此在实现Collection接口时,不同集合类都实现了自己独有的遍历方法,这称为集合的迭代器Iterator。其实Collection继承了java.lang.Iterable接口,该接口只提供了一个方法:iterator(),只要是实现了这个接口的类就表示具有迭代的能力,也就具有foreach增强遍历的能力。

迭代器自身是一个接口,通过Collection对象的iterator()方法就可以获取到对应集合类型的迭代器。例如:

Collection coll = new ArrayList();
Iterator it = coll.iterator();   //获取对应类型的集合的迭代器

Iterator接口提供了3个方法:

  • hasNext():判断是否有下一个元素。
  • Next():获取下一个元素。注意它返回的是Object(暂不考虑泛型)类型。
  • remove():移除迭代器最后返回的一个元素。此方法为Collection迭代过程中修改元素的唯一安全的方法。

虽然有不同种类型的集合,但迭代器的迭代方法是通用的。例如,要遍历coll集合中的元素。

import java.util.*;

public class TestColl {
    public static void main(String[] args) {
        Collection coll = new ArrayList();
        coll.add("abcd");
        coll.add(new Integer(129));
        coll.add(new Student("Gaoxiaofang",23));

        Iterator it = coll.iterator();   
         while (it.hasNext()) {             //Iterator遍历的方法
            System.out.println(it.next());  //return:abcd,129,Gaoxiaofang 23
        } 
    }
}

class Student {
    private String name;
    private int age;
    Student(String name,int n) {
        this.name = name;
        this.age = n;
    }
    public String getName() {
        return this.name;
    }
    public int getAge() {
        return this.age;
    }
    public String toString() {
        return this.name + " " + this.age;
    }
}

但是通常来说,上面的遍历方式虽然正确,但下面的遍历方式更佳。因为it对象只用于集合遍历,遍历结束后就应该消失,所以将其放入到for循环的内部,由于for循环的第三个表达式缺失,所以不断地循环第二个表达式即可。

for (Iterator it = coll.iterator();it.hasNext();) {
    System.out.println(it.next());
}

通过Iterator遍历到的元素是集合中的一个对象,对象也是有属性的。如何引用这些属性?只需将遍历出的元素作为对象来使用即可,但由于next()返回的元素都是Object对象,直接操作这个元素对象无法获取对应元素中的特有属性。因此必须先强制对象类型转换。

例如,获取coll中为Student对象元素的name属性,并删除非Student对象的元素。

Collection coll = new ArrayList();
coll.add("abcd");
coll.add(new Integer(129));
coll.add(new Student("Gaoxiaofang",23));

for (Iterator it = coll.iterator();it.hasNext();) {
    Object obj = it.next();
    if (obj instanceof Student) {
        Student s = (Student)obj;
        System.out.println(s.getName());  //return: Gaoxiaofang
    } else {
        it.remove();
    }
}
System.out.println(coll);                //return: [Gaoxiaofang 23]

因为集合中有些非Student对象元素,因此需要判断it.next()是否满足instanceof的要求,但不能直接写为下面的代码:

for (Iterator it = coll.iterator();it.hasNext();) {
    if (it.next() instanceof Student) {
        Student s = (Student)it.next();
        System.out.println(s.getName());
    }
}

因为每执行一次it.next(),元素的游标指针就向下滑动1,在这个写法中if判断表达式中使用了一次it.next(),在if的代码块中又调用了一次it.next()。所以应该将it.next()保存到对象变量中。而it.next()返回的类型是Object类型,因此定义Object obj = it.next()

只有remove()方法是Iterator迭代器迭代过程中修改集合元素且安全的方法以迭代时add()为例,当开始迭代时,迭代器线程获取到集合中元素的个数,当迭代过程中执行add()时,它将采用另一个线程来执行(因为add()方法不是Iterator接口提供的方法),结果是元素个数就增加了,且导致新增的元素无法确定是否应该作为迭代的一个元素。这是不安全的行为,因此会抛出ConcurrentModificationException异常。而remove()方法是迭代器自身的方法,它会使用迭代器线程来执行,因此它是安全的。

对于List类的集合来说,可以使用Iterator的子接口ListIterator来实现安全的迭代,该接口提供了不少增删改查List类集合的方法。

List接口

List接口实现了Collection接口。

List接口的数据结构特性是:

  • 1.有序列表,且带索引index。所谓有序指先后插入的顺序,即Index决定顺序。而向Set集合中插入数据会被打乱
  • 2.大小可变。
  • 3.数据可重复。
  • 4.因为有序和大小可变,使得它除了有Collection的特性,还有根据index精确增删改查某个元素的能力。
  • 5.实现List接口的两个常用类为:
    • (1).ArrayList:数组结构的有序列表;
      • 1).长度可变,可变的原因是在减少或添加元素时部分下标整体减一或加一,如果已分配数组空间不够,则新创建一个更大的数组,并拷贝原数组的内存(直接内存拷贝速度极快);
      • 2).查询速度快,增删速度慢查询快是因为内存空间连续,增删速度慢是因为下标移动。
      • 3).除了ArrayList是不同步列表,它几乎替代了Vector类。
    • (2).LinkedList:链表结构的有序列表;
      • 1).不同步;
      • 2).增删速度快,查询速度慢。增删速度快的原因是只需修改下链表中前后两个元素的索引指向即可;
      • 3).能够实现堆栈(后进先出LIFO,last in first out)、队列(queue,通常是FIFO,first in first out)和双端队列(double ends queue)。

ArrayList类的方法和List方法基本一致,所以下面介绍了List通用方法和ListIterator就没必要介绍ArrayList。但LinkedList比较特殊,所以独立介绍。

List接口通用方法

除了因为继承了Collection而具有的通用方法外,对于List接口也有它自己的通用方法。一般List的这些通用方法针对的是序列的概念。有了序列和下标索引值,可以精确地操控某个位置的元素,包括增删改查。

  • (1).增:add(index,element)
  • (2).删:remove(index)、remove(obj)删除列表中第一个obj元素
  • (3).改:set(index,element)
  • (4).查:get(index)
  • (5).indexOf(obj):返回列表中第一次出现obj元素的索引值,如不存在则返回-1
  • (6).lastIndexOf(obj)
  • (7).subList(start,end):返回列表中从start到end(不包括end边界)中间的元素组成列表。注意返回的是List类型。
  • (8).listIterator():返回从头开始遍历的List类集合的迭代器ListIterator。
  • (9).listIterator(index):返回从index位置开始遍历的List结合迭代器ListIterator。

因为有了get()方法,除了Iterator迭代方式,还可以使用get()方法遍历集合:

List l =  new ArrayList();
for (int i=0;i<l.size();i++) {
    System.out.println(l.get(i));
}

但注意,这种方法不安全,因为l.size()是即时改变的,如果增删了元素,size()也会随之改变。

示例:

import java.util.*;

public class TestList {
    public static void main(String[] args) {
        List ls =  new ArrayList();
        ls.add(new Student("Malong1",21));
        ls.add(new Student("Malong2",22));
        ls.add(1,new Student("Malong3",23));    //[Malong1 21,Malong3 23,Malong2 22]
        System.out.println(ls.indexOf(new Student("Malong3",23)));  // return:1
        ls.set(2,new Student("Gaoxiao1",22));   //[Malong1 21,Malong3 23,Gaoxiao1 22]

        for (Iterator it = l.iterator();it.hasNext();) {   //第一种迭代
            Student stu = (Student)it.next();
            if (stu.getAge() == 22) {
                it.remove();                        // the safe way to operate element
              //ls.add(new Student("Malong4",24));  //throw ConcurrentModificationException
            }
        }         //[Malong1 21,Malong3 23]
        System.out.println(l+"\n---------------");

        for (int i=0;i<ls.size();i++) {       //第二种迭代
            System.out.println(ls.get(i));
        }
    }
}

class Student {
    private String name;
    private int age;
    Student(String name,int n) {
        this.name = name;
        this.age = n;
    }
    public String getName() {return this.name;}
    public int getAge() {return this.age;}

    //override toString()
    public String toString() {
        return this.name + " " + this.age;
    }

    //override equals()
    public boolean equals(Object obj) {
        if (this == obj) {return true;}

        if (!(obj instanceof Student)) {
            throw new ClassCastException("Class error");
        }

        Student stu = (Student)obj;
        return this.name.equals(stu.name) && this.age == stu.age;
    }
}

上面的代码中,如果将ls.add(new Student("Malong4",24));的注释取消,将抛出异常,因为Iterator迭代器中唯一安全操作元素的方法是Iterator接口提供的remove(),而add()方法是List接口提供的,而非Iterator接口的方法。但对于List集合类来说,可以使用ListIterator迭代器,它提供的操作元素的方法更多,因为是迭代器提供的方法,因此它们操作元素时都是安全的。

List集合的迭代器ListIterator

通过listIterator()方法可以获取ListIterator迭代器。该迭代器接口提供了如下几种方法:

  1. hasNext():是否有下一个元素
  2. hasPrevious():是否有前一个元素,用于逆向遍历
  3. next():获取下一个元素
  4. previour():获取前一个元素,用于逆向遍历
  5. add(element):插入元素。注:这是迭代器提供的add(),而非List提供的add()
  6. remove():移除next()或previous()获取到的元素。注:这是迭代器提供的remove(),而非List提供的remove()
  7. set(element):设置next()或previour()获取到的元素。注:这是迭代器提供的set(),而非List提供的set()

例如:前文示例在Iterator迭代过程中使用List的add()添加元素抛出了异常,此处改用ListIterator迭代并使用ListIterator提供的add()方法添加元素。

List l =  new ArrayList();
l.add(new Student("Malong1",21));
l.add(new Student("Malong2",22));
l.add(1,new Student("Malong3",23)); //[Malong1 21,Malong3 23,Malong2 22]
l.set(2,new Student("Gaoxiao1",22));//[Malong1 21,Malong3 23,Gaoxiao1 22]

for (ListIterator li = l.listIterator();li.hasNext();) {
    Student stu = (Student)li.next();
    if (stu.getAge() == 22) {
        //l.add(new Student("Malong4",24));   //throw ConcurrentModificationException
        li.add(new Student("Malong4",24));
    }
}

LinkedList集合

LinkedList类的数据结构是链表类的集合。它可以实现堆栈、队列和双端队列的数据结构。其实实现这些数据结构都是通过LinkedList提供的方法按照不同逻辑实现的。

提供的其中几个方法如下:因为是实现了List接口,所以除了下面的方法,还有List接口的方法可用。

  • addFirst(element):向链表的首部插入元素
  • addLast(element):向链表的尾部插入元素
  • getFirst():获取链表的第一个元素
  • getLast():获取链表最后一个元素
  • removeFirst():移除并返回第一个元素,注意返回的是元素
  • removeLast():移除并返回最后一个元素,注意返回的是元素

LinkedList模拟队列数据结构

队列是先进先出FIFO的数据结构。封装的队列类MyQueue代码如下:

import java.util.*;

class MyQueue {
    private LinkedList mylist;

    MyQueue() {
        mylist = new LinkedList();
    }

    // add element to queue
    public void add(Object obj) {  
        mylist.addFirst(obj);           //Fisrt In
    }

    //get element from queue
    public Object get() {
        return mylist.removeLast();     //First Out
    }

    //queue is null?
    public boolean isNull() {
        return mylist.isEmpty();
    }

    //the size of queue
    public int size() {
        return mylist.size();
    }

    //remove element in queue by index
    public boolean remove(int index) {
        if(this.size()-1 < index) {
            throw new IndexOutOfBoundsException("index too large!");
        }
        mylist.remove(index);
        return true;
    }

    //remove the first appearance element in queue by Object
    public boolean remove(Object obj) {
        return mylist.remove(obj);
    }

    public String toString() {
        return mylist.toString();
    }
}

操作该队列数据结构程序代码如下:

import java.util.*;

public class FIFO {
    public static void main(String[] args) {
        MyQueue mq = new MyQueue();
        mq.add("Malong1");
        mq.add("Malong2");
        mq.add("Malong3");
        mq.add("Malong4");   //[Malong4,Malong3,Malong2,Malong1]
        System.out.println(mq.size());  //return:4
        mq.remove(2);                   //[Malong4,Malong3,Malong1]
        mq.remove("Malong1");           //[Malong4,Malong3]
        System.out.println(mq);

       while (!mq.isNull()) {
            System.out.println(mq.get());
        }
    }
}

Set接口

Set接口也实现了Collection接口。它既然能单独成类,它和List集合的数据结构一定是大有不同的。

Set接口的数据结构特性是:

  • 1.Set集合中的元素无序。这里的无序是相对于List而言的,List的有序表示有下标Index的顺序,而Set无需是指没有index也就没有顺序。
  • 2.Set集合中的元素不可重复
  • 3.因为无序,因此Set集合中取出元素的方法只有一种:迭代。
  • 4.实现Set接口的两个常见类为:
    • (1).HashSet:hash表数据结构;
      • 1).不同步;
      • 2).查询速度快;
      • 3).判断元素是否重复的唯一方法是:先调用hashcode()判断对象是否相同,相同者再调用equals()方法判断内容是否相同。所以,要将元素存储到此数据结构的集合中,必须重写hashcode()和equals()。
    • (2).TreeSet:二叉树数据结构;
      • 1).二叉树是用来排序的,因此该集合中的元素是有序的。这个有序和List的有序概念不同,此处的有序指的是存储时对元素进行排序,例如按照字母顺序,数字大小顺序等,而非index索引顺序。
      • 2).既然要排序,而equals()方法只能判断是否相等。因此数据存储到TreeSet集合中时需要能够判断大小。
      • 3).有两种方法用于构造有序的TreeSet集合:
        • a.待存储对象的类实现Comparable接口并重写它的compareTo()方法;
        • b.在构造TreeSet集合时指定一个比较器comparator。这个比较器需要实现Comparator接口并重写compare()方法。
    • (3).LinkedHashSet:链表形式的HashSet,仅在HashSet上添加了链表索引。因此此类集合有序(Linked)、查询速度快(HashSet)。不过很少使用该集合类型。

HashSet集合

HashSet的用法没什么可解释的,方法都继承自Set再继承自Collection。需要说明的是它的无序性、不可重复性、计算hash值时的方法以及判断重复性时的方法。

import java.util.*;

public class TestHashSet {
    public static void main(String[] args) {
        Set s = new HashSet();
        s.add("abcd4");
        s.add("abcd1");
        s.add("abcd2");
        s.add("abcd3");
        s.add("abcd1");  //重复

        for (Iterator it = s.iterator();it.hasNext();) {
            Object obj = it.next();
            System.out.println(obj);
        }
    }
}

得到的结果是无序且元素是不可重复的:

abcd2
abcd3
abcd4
abcd1

这里判断字符串对象是否重复的方法是先调用String的hashcode()进行判断,如果相同,再调用String的equals()方法。其中String的hashcode()方法在计算hash值时,是根据每个字符计算的,相同字符位置处的相同字符运算结果相同。

所以上面几个字符串对象中,前缀"abcd"子串部分的hash运算结果相同,最后一个字符决定了这些字符串对象是否相同。插入时有两个"abcd1",所以总共调用了一次String的equals()方法。

如果是存储自定义的对象,如Student对象,该对象定义方式如下:

class Student {
    String name;
    int age;
    Student(String name,int n) {
        this.name = name;
        this.age = n;
    }

    //override toString()
    public String toString() {
        return this.name + " " + this.age;
    }

    //override equals()
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Student)) {
            return false;
        }
        Student stu = (Student)obj;
        return this.name.equals(stu.name) && this.age == age;
    }
}

即使重写了equals(),插入属性相同的Student对象到HashSet中时,也会认为不重复的。

import java.util.*;

public class TestHashSet {
    public static void main(String[] args) {
        Set s = new HashSet();
        s.add(new Student("Malong1",21));
        s.add(new Student("Malong1",21));
        s.add(new Student("Malong1",21));

        for (Iterator it = s.iterator();it.hasNext();) {
            Object obj = it.next();
            System.out.println(obj);
        }
    }
}

结果:

Malong1 21
Malong1 21
Malong1 21

这是因为HastSet集合的底层首先调用Student的hashcode()方法,而Student没有重写该方法,而是继承自Object,所以每个对象的hashcode()都不相同而直接插入到集合中。

因此,需要重写Student的hashcode()方法。以下是一种重写方法:

public int hashCode() {
    return this.name.hashCode() + age*31; //31可以是任意数,但不能是1或0。
}

如果不加上"age*31",那么name部分的hash值有可能是相同的,但这很可能不是同一Student对象,所以应该加上age属性作为计算hash值的一部分元素。但不能直接加age,因为这样会导致"new Student("lisi3",23)"和"new Student("lisi2",24)"的hashcode相同(3+23=2+24),因此需要为age做一些修改,例如乘一个非0和1的整数。

在Student中重写hashCode()后,再插入下面这些Student对象,就能相对精确地判断是否为同一个Student元素。

s.add(new Student("lisi1",21));
s.add(new Student("lisi1",21));  //此处将调用equals(),且最终判断为重复对象
s.add(new Student("lisi2",24));
s.add(new Student("lisi3",23));  //此处将调用equals()
s.add(new Student("Gaoxiao1",23));
s.add(new Student("Gaoxiao2",21));
s.add(new Student("Gaoxiao3",22));

结果:

lisi1 21
Gaoxiao1 23
Gaoxiao3 22
lisi2 24
lisi3 23
Gaoxiao2 21

LinkedHashSet集合

链表顺序的HashSet集合,相比HashSet,只需多记录一个链表索引即可,这就使得它保证了存储顺序和插入顺序相同。实现方式除了new对象时和HashSet不一样,其他任何地方都是一样的。

import java.util.*;

public class TestHashSet {
    public static void main(String[] args) {
        Set s = new LinkedHashSet();

        s.add(new Student("lisi1",21));
        s.add(new Student("lisi1",21));
        s.add(new Student("lisi2",24));
        s.add(new Student("lisi3",23));
        s.add(new Student("Gaoxiao1",23));
        s.add(new Student("Gaoxiao3",21));
        s.add(new Student("Gaoxiao2",22));  

        for (Iterator it = s.iterator();it.hasNext();) {
            Object obj = it.next();
            System.out.println(obj);
        }
    }
}

结果:

lisi1 21
lisi2 24
lisi3 23
Gaoxiao1 23
Gaoxiao3 21
Gaoxiao2 22

TreeSet集合

TreeSet集合以二叉树数据结构存储元素。二叉树保证了元素之间是排过序且相互唯一的,因此实现TreeSet集合最核心的地方在于对象之间的比较。

比较对象有两种方式:一是在对象类中实现Comparable接口重写compareTo()方法;二是定义一个专门用于对象比较的比较器,实现这个比较器的方法是实现Comparator接口并重写compare()方法。其中Comparable接口提供的比较方法称为自然顺序,例如字母按照字典顺序,数值按照数值大小顺序。

无论是哪种方式,每个待插入的元素都需要先转型为Comparable,确定了将要存储在二叉树上的节点位置后,然后再转型为Object存储到集合中。

  1. 插入String类对象。
    由于String已经重写了compareTo(),因此下面插入String对象到TreeSet集合中没有任何问题。

    import java.util.*;
    //
    public class TestTreeSet {
     public static void main(String[] args) {
         Set t = new TreeSet();
    
         t.add("abcd2");
         t.add("abcd11");
         t.add("abcd3");
         t.add("abcd1");
         //t.add(23);
         //t.add(21);
         //t.add(21);
    
         for (Iterator it = t.iterator();it.hasNext();) {
             Object obj = it.next();
             System.out.println(obj);
         }
     }
    }
    

    但不能将上面"t.add(23)"等取消注释,虽然Integer类也重写了compareTo(),但在插入这些Integer类元素时,集合中已经存在String类的元素,String类的compareTo()和Integer的compareTo()的比较方法不一样,使得这两类元素之间无法比较大小,也就无法决定数值类的元素插入到二叉树的哪个节点。

  2. 插入实现了Comparable接口且重写了compareTo()的自定义对象。
    例如Student对象,如果没有重写compareTo()方法,将抛出异常,提示无法转型为Comparable。

    t.add(new Student("Malongshuai1",23));
    

    结果:

    Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
         at java.util.TreeMap.compare(Unknown Source)
         at java.util.TreeMap.put(Unknown Source)
         at java.util.TreeSet.add(Unknown Source)
         at TestTreeSet.main(TestTreeSet.java:8)
    

    所以,修改Student重写compareTo(),在重写应该考虑哪个作为主排序属性,哪个作为次要排序属性。例如以name为主排序属性,age为次排序属性。compareTo()返回正数则表示大于,返回负数则表示小于,返回0则表示等于。如下:

    class Student implements Comparable {
     String name;
     int age;
     Student(String name,int n) {
         this.name = name;
         this.age = n;
     }
    
     public String toString() {
         return this.name + " " + this.age;
     }
    
     public int compareTo(Object obj) {
         if (!(obj instanceof Student)) {
             throw new ClassCastException("Class cast wrong!");
         }
         Student stu = (Student)obj;
         // compare name first, then age
         int temp = this.name.compareTo(stu.name);
         return temp == 0 ? this.age - stu.age :temp;  
     }
    }
    

    于是插入Student时,将根据name中的字母顺序,相同时再根据age大小顺序,最后如果都相同,则认为元素重复,不应该插入到集合中。

    t.add(new Student("Malongshuai1",23));
    t.add(new Student("Malongshuai3",21));
    t.add(new Student("Malongshuai2",23));
    t.add(new Student("Malongshuai1",23)); //重复
    t.add(new Student("Malongshuai1",22));
    

    结果:

    Malongshuai1 22
    Malongshuai1 23
    Malongshuai2 23
    Malongshuai3 21
    
  3. 使用比较器comparator实现排序。此时TreeSet的构造方法为"TreeSet(Comparator comp)"。
    当使用了比较器后,插入数据时将默认使用比较器比较元素。
    比较器是一个实现了java.util.Comparator接口并重写了compare()方法的类,可以根据不同比较需求,创建不同的比较器。 例如创建一个根据age作为主排序属性,name作为次排序属性的比较器SortByAge,由于这个比较器是用来比较Student对象大小的,因此必须先转型为Student。

    import java.util.*;
    //
    public class SortByAge implements Comparator {
     public int compare(Object o1,Object o2) {
         //Cast to Student first
         if (!(o1 instanceof Student) || !(o2 instanceof Student)) {
             throw new ClassCastException("Wrong");
         }
         Student s1 = (Student)o1;
         Student s2 = (Student)o2;
         //compare age first, then name
         int temp = s1.age - s2.age;
         return temp == 0 ? s1.name.compareTo(s2.name) : temp;
     }
    }
    

    指定TreeSet的比较器为SortByAge,并插入一些Student对象:

    public class TestTreeSet {
     public static void main(String[] args) {
         Set t = new TreeSet(new SortByAge());
    
         t.add(new Student("Malongshuai1",23));
         t.add(new Student("Malongshuai3",21));
         t.add(new Student("Malongshuai2",23));
         t.add(new Student("Malongshuai1",23)); //重复
         t.add(new Student("Malongshuai1",22));
    
         for (Iterator it = t.iterator();it.hasNext();) {
             Object obj = it.next();
             System.out.println(obj);
         }
     }
    }
    

    当为TreeSet集合指定了比较器时,结果将先按照age顺序再按照name排序的,尽管Student类中仍然重写了compareTo()方法:

    Malongshuai3 21
    Malongshuai1 22
    Malongshuai1 23
    Malongshuai2 23
    

Map集合

List和Set集合都是只存一列数据,Map存储的是键值对key/value双列数据。key和value之间一一对应,实现一一对应的关系称为映射关系。其中key不可重复,而判断不可重复的依据考虑的是hashcode()和equals()。

Map常见子类有:

  • Hashtable:被HashMap替代,除了它不允许非null值以及线程同步的特性,其他和HashMap一致。
  • HashMap:允许Null值,线程不同步。判断key是否重复的方法是比较Hashcode()和equals()。
  • LinkedHashMap:HashMap是无序的。如果要实现前后顺序,可以采用LinkedHashMap。
  • TreeMap:二叉树。key是有大小排序的,判断大小的方式需要传递比较器。

以下是Map接口常用方法。

  • put(k,v):保存key/value到Map集合中,如果key已存在则覆盖。注意返回值为k对应的旧value,如果原本不存在该k,则返回null。
  • remove(k):移除k对应的key/value键值对。注意返回的是k对应的value,所以该方法是获取k对应的value,并删除该键值对。
  • get(k):获取k对应的value,如果不存在该key,则返回null。
  • keySet():返回Map中key部分组成的Set集合。之所以是Set集合,是因为key具有唯一性。注意不能再向此Set集合中add新元素,否则将表示该新的key没有对应的value。
  • values():返回Map中value部分组成的Collection集合。之所以是collection集合,是因为value没有任何规律(重复性、有序性)可言。
  • entrySet():返回该map集合中每一个键值对key/value组成的映射关系集合。相当于将每一对key/value打包为一个元素,这些元素组成Set集合中的元素。

以下是简单增、取键值对的使用示例:

Map<String,Integer> m = new HashMap<String,Integer>();
m.put("ftp",21);
m.put("ssh",22);
m.put("http",80);
m.put("mysql",3306);
System.out.println(m.get("ssh"));   //return 22
System.out.println(m.remove("ssh"));  //return 22
System.out.println(m.get("ssh"));   // return null

以下是keySet()使用示例。因为将key都存储到了Set集合中,因此需要保证Set集合的泛型类型需要和Key的类型一致。有了Set集合,就可以对其进行遍历以获取其内所有的key。

Set<String> set = m.keySet();
for(Iterator<String> it = set.iterator();it.hasNext();){
    System.out.println(m.get(it.next()));
}

//可简化为如下形式:
for(Iterator<String> it = m.keySet().iterator();it.hasNext();){
    System.out.println(m.get(it.next()));
}

以下是values()使用示例。其将Map中所有的value部分装入到了一个集合中,这个集合比较随意,因为它仅仅只是充当一个装东西的容器,没有什么要求和规则。

for(Iterator<Integer> it = m.values().iterator();it.hasNext();){
    System.out.println(it.next());
}

还有一个比较常用的是entrySet()方法。它是将映射关系装入到Set集合中,由此引出的问题是这个Set集合存储的映射关系是什么数据类型?答案是:Map.entry<K,V>。注意,它是一种数据类型,专门表示映射关系的数据类型,其每个元素保存时使用"="连接key和value,即"key=value"。它提供了两个常用的方法:getKey()和getValue()分别用于获取映射关系中的key部分和value部分。

Set<Map.Entry<String,Integer>> set = m.entrySet();
for(Iterator<Map.Entry<String,Integer>> it = set.iterator();it.hasNext();){
    Map.Entry<String,Integer> map = it.next();
    System.out.println(map);  //retrun [ftp=21,ssh=22,http=80,mysql=3306]
    //System.out.println(map.getKey());   //retrun [ftp,ssh,http,mysql]
    //System.out.println(map.getValue());  //retrun [21,22,80,3306]
}

关于keySet()、values()和entrySet()之间的区别,见下图。

HashMap

HashMap将key散列后存储到hash桶中,它不能存储相同的key。判断key是否重复的依据是对象的hashCode()和equals()方法。如果存储重复的key,将覆盖存储。

例如,下面存储Student对象和Student所在的年级。

import java.util.*;

class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    public int hashCode() {
        return this.name.hashCode()+this.age*31;
    }

    public boolean equals(Object obj) {
        if (this == obj) {return true;}
        if(!(obj instanceof Student)) {return false;}
        Student stu = (Student) obj;
        return stu.name.equals(this.name) && this.age == stu.getAge();
    }


public class MapHash {

    public static void main(String[] args) {
        HashMap<Student, String> hm = new HashMap<Student,String>();
        hm.put(new Student("malong",23),"高三");
        hm.put(new Student("malong1",24),"高二");
        hm.put(new Student("malong2",21),"高三");
        hm.put(new Student("malong3",26),"高一");
        hm.put(new Student("malong3",26),"高四");  //Student对象和上一条重复,因此将覆盖前一个Student

        for(Iterator<Student> it = hm.keySet().iterator();it.hasNext();) {
            Student stu = it.next();
            System.out.println(stu.getName()+"----"+stu.getAge()+"----"+hm.get(stu));
        }
    }
}

TreeMap

TreeMap存储的Key有大小顺序,在存储时会经过大小排序,排序的依据是根据指定的比较器进行排序。因为比较器的方法有两种:(1)在对象元素内部实现接口Comparable并重写CompareTo()方法(2)传递一个比较器给TreeMap构造方法TreeMap(Comparator comp)。如果两种方法都具备,则第二种方式优先级更高。

例如,在Student内部重写CompareTo()方法使其按姓名"排序(主)+年龄(次)",再定义一个按照姓名字符长度排序的比较器。

class Student implements Comparable {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String toString() {
        return "Student [name=" + name + ", age=" + age + "]";
    }

    public int compareTo(Student stu) {
        int temp = this.name.compareTo(stu.name);
        return temp == 0 ? this.age - stu.age : temp; 
    }
import java.util.*;

public class MapHash {
    public static void main(String[] args) {
        //按照姓名和年龄排序
        TreeMap<Student, String> hm = new TreeMap<Student, String>();   
        hm.put(new Student("wugv", 23), "高三");
        hm.put(new Student("Gapxiaofang", 24), "高二");
        hm.put(new Student("malongshuai", 21), "高三");
        hm.put(new Student("gaotuner", 26), "高一");
        hm.put(new Student("woniu", 26), "高四");

        for (Iterator<Student> it = hm.keySet().iterator(); it.hasNext();) {
            Student stu = it.next();
            System.out.println(stu.getName() + "----" + stu.getAge() + "----" + hm.get(stu));
        }
    }
}
import java.util.*;

public class MapHash {
    public static void main(String[] args) {
        //按照姓名字符串长度排序,长度相同的按年龄排序
        TreeMap<Student, String> hm = new TreeMap<Student, String>(new SortByLength());
        hm.put(new Student("wugv", 23), "高三");
        hm.put(new Student("Gapxiaofang", 24), "高二");
        hm.put(new Student("malongshuai", 21), "高三");
        hm.put(new Student("gaotuner", 26), "高一");
        hm.put(new Student("woniu", 26), "高四");

        for (Iterator<Student> it = hm.keySet().iterator(); it.hasNext();) {
            Student stu = it.next();
            System.out.println(stu.getName() + "----" + stu.getAge() + "----" + hm.get(stu));
        }
    }
}

//按姓名长度排序的比较器
class SortByLength implements Comparator<Student> {
    public int compare(Student stu1,Student stu2) {
        int temp = stu1.getName().length() - stu2.getName().length();
        return temp == 0 ? stu1.getAge() - stu2.getAge() : temp;
    }
}

Enumeration数据集合

这是枚举的数据类型,这种类型比较古老,在大多数时候都被Itreator替代了,因为后者具备Enumeration的迭代功能,且额外具备线程安全的元素删除功能remove()。

Enumeration的遍历方法大致如下:

Enumeration<String> enu;
while(enu.hasMoreElements()){
    enu.nextElement();
}

Collections工具类中提供了将Enumeration转换为ArrayList数据结构的方法list(),转换时按照枚举的顺序在ArrayList中索引编号,转换为ArrayList后就可以使用Iterator迭代器进行迭代。还提供了将Collection集合转换为Enumeration类型的方法enumeration()

posted @ 2017-11-06 01:58  骏马金龙  阅读(765)  评论(0编辑  收藏  举报