1.树集
TreeSet类与散列集合十分相似,不过,它比散列集合有所改进。树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。例如,假设插入3个字符串,然后访问添加的所有元素。
public static void main(String[] args) {
SortedSet<String> sorter = new TreeSet<>();
sorter.add("bob");
sorter.add("amy");
sorter.add("carl");
for(String s : sorter) System.out.println(s);
}
这时,每个值将按照顺序打印出来。正如TreeSet类名所示,排序是用树结构完成的(当前实现使用的红黑树)。每次讲一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
将一个元素添加到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表的正确位置上相比,还是快了很多。如果树中包含n个元素,查找新元素的正确位置平均需要log2n次比较。例如,如果一棵树包含了1000个元素,添加一个新元素大概需要比较10次。因此,讲一个元素添加到TreeSet中要比添加到HashSet中慢,不过TreeSet可以自动地对元素进行排序。
常用API
//构造一个空树集合
TreeSet()
//构造一个树集,并将集合中的所有元素添加到树集中
TreeSet(Collection<? extends E>elements)
2.映射表(Map)
集合可以快速地查找现有的元素,但是要查看一个元素,需要有要查找元素的精确副本。这不是一个非常通用的查找方式。通常,我们知道某些键的信息,并想要查找与之对应的元素。映射表(map)数据结构就是为此设计的。映射表用来存放键值对。如果提供了键,就能够查找到值。Java类库为映射表提供了两个通用的实现:HashMap 和 TreeMap,这两个类都实现了Map接口。
散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
应该选择散列映射表还是树映射表呢?与集合一样,散列稍微快一些,如果不需要按照排列顺序访问键,最好选择散列。
每当往映射表里添加对象时,必须同时提供一个键。想要检索一个对象,必须提供一个键。如果在映射表里没有与给定键对应的信息,get将返回null。
键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值会覆盖第一个值。实际上,put将返回用这个键参数存储的上一个值。remove方法用于从映射表删除给定键对应的元素。size方法用于返回映射表中的元素数。
集合框架并没有将映射表本身视为一个集合。然后,可以获得映射表的视图,这是一组实现了Collection接口对象,或者它的子接口的视图。有3个视图,分别是:键集合、值集合、键值对集合。键与键值对形成一个集合,这是因为在映射表中的一个键只能有一个副本。下面三个方法将返回3个视图:
Set<K> keySet();
Collection<K> values();
Set<Map.Entry<K,V>> entrySet()
注意,keySet既不是HashSet,也不是TreeSet,而是实现了Set接口的某个类的对象。Set接口扩展了Collection接口。因此,可以与使用任何集合一样使用keySet。
Set<String> keys = map.keySet();
for(String key : keys){
System.out.println("key的值:" + key);
}
【提示】:如果想要同时查看键与值,就可以通过枚举各个条目(entries)查看,以避免对值进行查找。可以使用下面代码实现:
for(Map.Entry<String,Object> entry : map.entrySet()){
String key = entry.getKey();
Object obj = entry.getvalue();
System.out.println("键是:" + key);
System.out.pringln("值是:" + obj.toString());
}
如果调用迭代器的remove方法,实际上就从映射表删除了键以及对应的值。但是不能将元素添加到键集合的视图中。如果只添加键而不添加值是毫无意义的。如果视图调用add方法,将会抛出UnsupportedOperationException异常。
【例】首先将将至对添加到映射表中,再从映射表中删除一个键,同时与之相对应的值夜被删除。接下来,修改与某一个键对应的值,并调用get方法查看这个值。最后,循环映射表
public class HashMapTest {
public static void main(String[] args) {
Map<String,User> map = new HashMap();
map.put("1001",new User("张1",20));
map.put("1002",new User("张2",21));
map.put("1003",new User("张3",22));
map.put("1004",new User("张4",23));
map.put("1005",new User("张5",24));
System.out.println("修改前,map:"+map);
map.remove("1002");
map.put("1001",new User("张6",25));
System.out.println("修改后,map" + map);
for(Map.Entry<String,User> entry : map.entrySet()){
String key = entry.getKey();
User user = entry.getValue();
System.out.println("key = " + key + ";value = " + user);
}
}
}
class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
常用api
//获取与键对应的值,如果在映射中没有这个对象则返回null。键可以为null
V get(Object key);
//将键与对应的值关系插入到映射表中。如果这个键已经存在,新的对象将取代与这个键对应的旧对象。
//这个方法将返回键对应的旧值。如果这个键以前没有出现过,则返回null。
//键可以为null,但值不能为null。
V put(K key,V value);
//将给定映射表中的所有条目添加到这个映射表中
void putAll(Map<? extends K,? extends V> entries);
//如果映射表中已经存在这个键,返回true
boolean containsKey(Object key);
//如果映射表中已经存在这个值,返回true
boolean containsValue(Object value);
//返回Map.Entry对象的集合视图,即映射表中的键值对。可以从这个集合中删除元素。
//同时也从映射表中删除了它们。但是不能添加任何元素
Set<Map,Entry<K,V>> entrySet();
//返回映射表中的所有键的集合视图。可以从这个集合中删除元素,同时也从映射表中删除了它们。
//但是,不能添加任何元素。
Set<K> keySet();
//返回映射表中所有值的集合视图,可以从这个集合中删除元素,同时也从映射表中删除它们。
//但是,不能添加任何元素。
Collection values();
//返回这个条目的键或值
K getKey();
v getValue();
//设置在映射表中与新值对应的值,并返回旧值
V setValue(V newValue);
//用给定的容量和装填因子构造一个空散列映射表(装填因子是一个0.0~1.0之间的数值。这个数值决定
//散列表填充的百分比,一旦到了这个比例,就要将其散列到更大的表中)。默认装填因子是0.75
HashMap();
HashMap(int initicalCapacity);
HashMap(int initicalCapacity,float loadFactor);
//构造一个树映射表,并使用一个指定的比较器对键进行排序
TreeMap(Comparator<? super K> c);
//构造一个树映射表,并将某个映射表中的所有题目添加到树映射表中
TreeMap(Map<? extends K,? extends V> entries);
//构造一个树映射表,将某个有序映射表中的所有条目添加到树映射表中,
//并使用与给定的有序映射表相同的比较器
TreeMap(SortedMap<? extends K,? extends v> entries);
//返回对键进行排序的比较器。如果键用Comparable接口的compareT0方法进行比较,返回null
Copmarator<? super K> comparator();
//返回映射表中最小元素和最大元素
K firstKey();
K lastKey();
3.集合框架
Java集合类库构成了集合类的框架。它为集合的实现着定义了大量的接口的抽象类,并且对其中的某些机制给予了描述,例如,迭代协议。正如前面所做的那样,可以使用这些集合类,而不必了解框架。但是,如果想要实现用于多种集合类的泛型算法,或者想要增加新的集合类型,了解一些框架的知识是很有帮助的。
如上图所示:集合有两个基本的接口:Collection和Map。可以使用下面的方法向集合中插入元素:
boolean add(E element);
由于映射表保存的是键值对,可以使用put方法进行插入
V put(k key,V value);
想要从集合中读取某个元素,就需要使用迭代器访问它们。也可以用get方法从映射表读取值:
V get(K key);
List是一个有序集合。元素可以添加到容器中某个特定的位置。将对象放置在某个位置上可以采用两种方法:使用整数索引或使用列表迭代器。List接口定义了几个用于随机访问的方法:
void add(int index,E element);
E get(int index);
void remove(int index);
List接口在提供这些随机访问方法时,并不关心它们对某种特定的实现是否高效。为了避免执行成本较高的随机访问操作,Java SE1.4引入了一个标记接口RandomAccess。这个接口没有任何方法,只用来检测一个特定的集合是否支持高效的随机访问。ArrayList和Vector类都实现了RandomAccess接口。
【注释】:从理论上看,有一个独立的Array接口,它扩展于List接口,并声明了随机访问方法是很合理的。如果确实有这样一个独立的Array接口,那些需要随机访问的算法就可以使用Array参数,而且不会无意中将它们应用于随机访问速度很慢的集合了。
ListIterator接口定义了一个方法,用于将一个元素添加到迭代器所处位置的前面:
void add(E element);
要想获取和删除给定位置的元素,只需要调用Iterator接口中的next方法和remove方法即可。
Set与Collection接口是一样的,只是其方法的行为有着更加严谨的定义。集合的add方法拒绝添加重复的元素。集合的equals方法定义两个集合相等的条件是它们包含相同的元素但顺序不必相同。hashCode方法定义应该保证具有相同元素的集合将会得到相同的散列码。
SortedSet和SortedMap接口暴露了用于排序的比较器对象,并且定义的方法可以获取集合的子集视图。最后,Java SE1.6引入接口NavigableSet和NavigableMap,其中包含了几个用于在有序集合和映射表中查找和遍历的方法。TreeSet和TreeMap类实现了这几个接口。
集合中类的关系如下图:
4.视图与包装器
上面的图看出,用如此多的接口和抽象类来实现数量并不多的具体集合类似乎没有太大的必要。然后,上图并没有展示全部的情况。通过使用视图可以获得其他的实现了集合接口和映射表接口的对象。映射表类的keySet方法就是一个这样的示例。初看起来,好像这个方法创建了一个新集合。但是并非如此,取而代之的是:keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射表进行操作。这种集合称为视图。
视图技术在集合框架中有许多非常有用的,下面将讨论这些应用。
(1)轻量级集合包装器
Arrays类的静态方法asList将返回一个包装了普通Java数组的Listh包装器。这个方法可以将数组传递给一个期望得到列表或集合的方法。例如:
Card[] cardDeck = new Card[52];
List<Card> list = Arrays.asList(cardDeck);
返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的get和set方法。改变数组大小的所有方法都会抛出一个UnsupportedOperationException异常。
从Java SE 5.0开始,asList方法声明为一个具有可变数量参数的方法。除了可以传递一个数组之外,还可以将各个元素直接传递给这个方法。例如:
List<String> names = Arrays.asList("amy","bob","carl");
这个方法调用:
Collection.nCopies(n,anObject);
将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个anObject的错觉。
例如,下面的调用将创建一个包含100个字符串的List,每个串都没设置为”DEFAULT“:
List<String> settings = Collections.nCopies(100,"DEFAULT");
由于字符串对象只存储了一次,所以付出的存储代价很小。这是视图技术的一种巧妙的应用。
【注释】:Collections类包含很多实用方法,这些方法的参数和返回值都是集合。不要将它与Collection接口混淆。
如果调用下面方法:
Collections.singleton(anObject);
将返回一个视图对象。这个对象实现了Set接口(与产生List的ncopies方法不同)。返回的对象实现了一个不可修改的单元集合,而不需要付出建立数据结构的开销。
(2)子范围
可以为很多集合建立子范围视图。例如,假设有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得一个列表的子范围视图。
List group2 = staff.subList(10,20);
第一个索引包含在内,第二个索引不包含在内。这与String 类的subString操作中的参数情况相同。
可以将任何操作应用于子范围,并且能够自动地反应整个列表的情况。例如,可以删除整个子范围:
group2.clear();
现在,元素自动地从staff列表清除了,并且group2为空。对于有序集合和映射表,可以使用排序顺序而不是元素位置建立子范围。SortedSet接口声明了3个方法:
SortedSet<E> subSet(E from,E to);
SortedSet<E> headSet(E to);
SortedSet tailSet(E from);
这些方法将返回大于等于from并且小于to的所有元素子集。有序映射表也有类似的方法:
SortedMap<K,V> subMap(K from,K to);
SortedMap<K,V> headMap(K to);
SortedMap<K,V> tailMap(K from);
返回映射表视图,该映射表包含键落在指定范围内的所有元素。Java SE 6引入的Navigable接口赋予子范围操作更多的控制能力。可以指定是否包括边界:
NavigableSet<E> subSet(E from,boolean fromInclusive,E to,boolean toInclusive);
NavigableSet<E> heanSet(E to,boolean toInclusive)
NavigableSet<E> tailSet(E from,boolean fromInclusive)
5.批量操作
可以使用类库中的批量操作避免频繁的使用迭代器。假设希望找出两个集合的交集。首先,要建立一个新集合,用于存放结果。
Set<String> result = new HashSet<>(a);
这里利用了这样一个事实:每个集合都有一个构造器,其参数是保存初始值的另一个集合。接着,调用retianAll方法:
result.retianAll(b);
result中保存了既在a中出现,也在b中出现的元素。这时已经构成了交集,而且没有使用循环。