集合框架(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的方法,这类方法操作的是集合。

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()方法,所以它们是不重复的元素。

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]

clear():清空该集合中的所有元素。
contains(object obj):是否包含某对象元素。判断的依据仍然是equals()方法。

Collection coll = new ArrayList();
coll.add(new Integer(128));
System.out.println(coll.contains(new Integer(128))); //true

isEmpty():集合是否不包含任何元素。
size():返回该集合中元素的个数。
equals(Object obj):比较两个集合是否完全相等。依据是集合中的所有元素都能通过各自的equals得到相等的比较。
addAll(Collection c):将整个集合c中的元素都添加到该集合中。
containsAll(Collection c):该集合是否包含了c集合中的所有元素,即集合c是否是该集合的子集。
removeAll(Collection c):删除该集合中那些也包含在c集合中的元素。即删除该集合和c集合的交集元素。
retainAll(Collection c):和removeAll()相反,仅保留该集合中和集合c交集部分的元素。
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迭代器。该迭代器接口提供了如下几种方法:

hasNext():是否有下一个元素
hasPrevious():是否有前一个元素,用于逆向遍历
next():获取下一个元素
previour():获取前一个元素,用于逆向遍历
add(element):插入元素。注:这是迭代器提供的add(),而非List提供的add()
remove():移除next()或previous()获取到的元素。注:这是迭代器提供的remove(),而非List提供的remove()
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存储到集合中。

插入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()的比较方法不一样,使得这两类元素之间无法比较大小,也就无法决定数值类的元素插入到二叉树的哪个节点。

插入实现了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

使用比较器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

总结

以上就是本文关于集合框架(Collections Framework)详解及代码示例的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:

java集合中的list详解

详解java各种集合的线程安全

Java集合框架源码分析之LinkedHashMap详解

如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

您可能感兴趣的文章:

  • java集合框架 arrayblockingqueue应用分析
  • 关于Java集合框架面试题(含答案)上
  • 关于Java集合框架面试题(含答案)下
  • 关于Java集合框架的总结
  • Java集合框架ArrayList源码分析(一)
  • 一道Java集合框架题 多种解题思路
  • Java集合框架之Collection接口详解
  • Java集合框架LinkedList详解及实例
(0)

相关推荐

  • 关于Java集合框架面试题(含答案)上

    1.Java集合框架是什么?说出一些集合框架的优点? 每种编程语言中都有集合,最初的Java版本包含几种集合类:Vector.Stack.HashTable和Array.随着集合的广泛使用,Java1.2提出了囊括所有集合接口.实现和算法的集合框架.在保证线程安全的情况下使用泛型和并发集合类,Java已经经历了很久.它还包括在Java并发包中,阻塞接口以及它们的实现.集合框架的部分优点如下: (1)使用核心集合类降低开发成本,而非实现我们自己的集合类. (2)随着使用经过严格测试的集合框架类,代

  • 一道Java集合框架题 多种解题思路

    问题:某班30个学生的学号为20070301-20070330,全部选修了Java程序设计课程,给出所有同学的成绩(可用随机数产生,范围60-100),请编写程序将本班各位同学的成绩按照从低到高排序打印输出. 要求:分别用List.Map.Set来实现,打印的信息包括学号.姓名和成绩. 1.使用List集合来实现 import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; impor

  • Java集合框架之Collection接口详解

    Java是一门面向对象的语言,那么我们写程序的时候最经常操作的便是对象了,为此,Java提供了一些专门用来处理对象的类库,这些类库的集合我们称之为集合框架.Java集合工具包位于Java.util包下,包含了很多常用的数据结构,如数组.链表.栈.队列.集合.哈希表等.学习Java集合框架下大致可以分为如下五个部分:List列表.Set集合.Map映射.迭代器(Iterator.Enumeration).工具类(Arrays.Collections). Java的集合类主要由两个接口派生而出:Co

  • 关于Java集合框架面试题(含答案)下

    21.HashMap和HashTable有何不同? (1)HashMap允许key和value为null,而HashTable不允许. (2)HashTable是同步的,而HashMap不是.所以HashMap适合单线程环境,HashTable适合多线程环境. (3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的. (4)HashMap

  • java集合框架 arrayblockingqueue应用分析

    Queue ------------ 1.ArrayDeque, (数组双端队列) 2.PriorityQueue, (优先级队列) 3.ConcurrentLinkedQueue, (基于链表的并发队列) 4.DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口) 5.ArrayBlockingQueue, (基于数组的并发阻塞队列) 6.LinkedBlockingQueue, (基于链表的FIFO阻塞队列) 7.LinkedBlockingDeque, (

  • Java集合框架LinkedList详解及实例

    Java集合框架LinkedList详解 LinkedList定义 package java.util; public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{ transient int size = 0; transient Node<E> first;

  • 关于Java集合框架的总结

    本篇文章先从整体介绍了Java集合框架包含的接口和类,然后总结了集合框架中的一些基本知识和关键点,并结合实例进行简单分析.当我们把一个对象放入集合中后,系统会把所有集合元素都当成Object类的实例进行处理.从JDK1.5以后,这种状态得到了改进:可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型. 一.综述 所有集合类都位于java.util包下.集合中只能保存对象(保存对象的引用变量).(数组既可以保存基本类型的数据也可以保存对象). 当我们把一个对象放入集合中后,系统会把所

  • Java集合框架ArrayList源码分析(一)

    ArrayList底层维护的是一个动态数组,每个ArrayList实例都有一个容量.该容量是指用来存储列表元素的数组的大小.它总是至少等于列表的大小.随着向 ArrayList 中不断添加元素,其容量也自动增长. ArrayList不是同步的(也就是说不是线程安全的),如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步,在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList

  • 集合框架(Collections Framework)详解及代码示例

    简介 集合和数组的区别: 数组存储基础数据类型,且每一个数组都只能存储一种数据类型的数据,空间不可变. 集合存储对象,一个集合中可以存储多种类型的对象.空间可变. 严格地说,集合是存储对象的引用,每个对象都称为集合的元素.根据存储时数据结构的不同,分为几类集合.但对象不管存储到什么类型的集合中,既然集合能存储任何类型的对象,这些对象在存储时都必须向上转型为Object类型,也就是说,集合中的元素都是Object类型的对象. 既然是集合,无论分为几类,它都有集合的共性,也就是说虽然存储时数据结构不

  • SpringMVC中的拦截器详解及代码示例

    本文研究的主要是SpringMVC中的拦截器的介绍及实例代码,配置等内容,具体如下. Springmvc的处理器拦截器类似于Servlet 开发中的过滤器Filter,用于对处理器进行预处理和后处理.本文主要总结一下springmvc中拦截器是如何定义的,以及测试拦截器的执行情况和使用方法. 1. springmvc拦截器的定义和配置 1.1 springmvc拦截器的定义 在springmvc中,定义拦截器要实现HandlerInterceptor接口,并实现该接口中提供的三个方法,如下: /

  • Java中的静态内部类详解及代码示例

    1. 什么是静态内部类 在Java中有静态代码块.静态变量.静态方法,当然也有静态类,但Java中的静态类只能是Java的内部类,也称为静态嵌套类.静态内部类的定义如下: public class OuterClass { static class StaticInnerClass { ... } } 在介绍静态内部类之前,首先要弄清楚静态内部类与Java其它内部类的区别. 2. 内部类 什么是内部类?将一个类的定义放在另一个类的内部,就是内部类.Java的内部类主要分为成员内部类.局部内部类.

  • Java之dao模式详解及代码示例

    什么是dao模式? DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作.在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中.用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法.在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储.DAO模式实

  • Java中Volatile关键字详解及代码示例

    一.基本概念 先补充一下概念:Java内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的

  • Java逃逸分析详解及代码示例

    概念引入 我们都知道,Java 创建的对象都是被分配到堆内存上,但是事实并不是这么绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中创建出来的对象并一定分别在所认为的堆上.这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)线程私有的缓存区. 基本概念介绍 逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法.通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的

  • Java集合框架之Map详解

    目录 1.Map的实现 2.HashMap和Hashtable的区别 3.介绍下对象的hashCode()和equals(),使用场景 4.HashMap和TreeMap应该怎么选择,使用场景 5.Set和Map的关系TODO 6.常见Map的排序规则是怎样的? 7.如果需要线程安全,且效率高的Map,应该怎么做? 8.介绍下HashMap 9.什么是Hash碰撞?常见的解决办法有哪些,hashmap采用哪种方法? 10.HashMap底层是数组+链表+红黑树,为什么要用这几类结构呢? 11.为

  • Java中SimpleDateFormat日期格式转换详解及代码示例

    SimpleDateFormat是处理日期格式转换的类. 官方API_1.8关于SimpleDateFormat继承于DateFormate截图: SimpleDateFormat的构造器如下: SimpleDateFormat中的格式定义,常用的用红色框圈出: 中文解释: y : 年 M : 年中的月份 D : 年中的天数 d : 月中的天数 w : 年中的周数 W : 月中的周数 a : 上下/下午 H : 一天中的小时数(0-23) h : 一天中的小时数(0-12) m : 小时中的分钟

  • Java操作集合工具类Collections使用详解

    这篇文章主要介绍了java操作集合工具类Collections使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Collections是一个操作Set.List和Map等集合的工具类. Collections中提供了大量方法对集合元素进行排序.查询和修改等操作,还提供了对集合对象设置不可变.对集合对象实现同步控制等方法. 排序操作: reverse(List):反转List中元素的顺序: shuffle(List):对List集合元素进行

  • python集合删除多种方法详解

    这篇文章主要介绍了python集合删除多种方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 删除指定的元素 A={'a','c','b','d','e'} print("原集合:",A) A.remove('a') # 不存在会报错 print("删除a后:",A) A.discard('b') # 不存在不会报错 print("删除b后:",A) A.pop() print("

随机推荐