@lemonguge
2015-07-10T02:25:15.000000Z
字数 12084
阅读 366
JAVA
因为内部的数据结构不同,有多种具体容器。通过不断的向上抽取,就形成了集合框架。通过查看体系的最顶层来了解体系的基本功能,而这个框架的最顶层为Collection接口。
Collection接口:单列集合,一次添加一个元素。 List接口:有序(存入和取出的顺序一致),元素都有索引(角标),元素可以重复。 ArrayList类:内部是数组数据结构,是不同步的。替代了Vector。查询的速度快。LinkedList类:内部是链表数据结构,是不同步的。增删元素的速度很快。Vector类:内部是数组数据结构,是同步的。增删,查询都很慢!其具有一个Stack子类,栈,这两个类都已过时!Set接口:元素不能重复,无序。 HashSet类:内部数据结构是哈希表 ,是不同步的。TreeSet类:内部结构是二叉树,可以对Set集合中的元素进行排序,是不同步的。LinkedHashSet类:内部是哈希表和链接列表,顺序存储,是不同步的。Queue接口:队列,LinkedList类实现该接口,该接口的大部分类将主要用于并发容器。Map接口:双列集合,一次添加一对元素。 HashMap类:内部结构是哈希表,不是同步的。允许null作为键,null作为值。TreeMap类:内部结构是二叉树,不是同步的。可以对Map集合中的键进行排序。LinkedHashMap类:内部是哈希表和链接列表,顺序存储,是不同步的。Hashtable类:内部结构是哈希表,是同步的。不允许null作为键,null作为值。现已过时!但是其子类Properties类:用来存储键值对型的配置文件的信息,可以和IO技术相结合。 在现在的代码中不应该使用过时的Vector、Hashtable和Stack(栈),对于栈和Queue(队列)的行为,由LinkedList提供支持。
Collection接口还实现一个超级接口Iterable,中文意思是“可迭代的”。字典里对迭代的解释:“迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。”
这个接口里只有一个方法,就是iterator(),返回一个迭代器Iterator。通过查看API,迭代器里面有三个方法:hasNext()、next()和remove()。其中remove()方法用于移除迭代器返回的最后一个元素,前面两个方法最常用。目的很明显,迭代器是用来取出元素的。
通过iterator()可以获取到迭代器,为什么需要迭代器呢?
因为每一个容器的数据结构都不同,该迭代器对象是在容器中进行内部实现的。该对象必须依赖于具体容器,对于使用容器者而言,具体的实现不重要,只要通过容器获取到该实现的迭代器的对象即可。迭代器就是内部类的使用体现。
public class IteratorDemo {public static void main(String[] args) {Collection<String> coll = new ArrayList<String>();coll.add("abc1");coll.add("abc2");coll.add("abc3");coll.add("abc4");for(Iterator<String> it = coll.iterator(); it.hasNext(); ){System.out.println(it.next());}}} /* Output:abc1abc2abc3abc4*///:~
在迭代器过程中,不要使用集合操作元素,容易出现异常。这是Java容器的一种保护机制,能够防止多个进程同时修改同一个容器的内容。
如果在迭代遍历某个容器的过程中,另一个进程介入其中,并且插入、删除或修改此容器内的某个对象,那么就会出现问题。为了防止容器的状态不一致。
import java.util.*;public class FailFast {public static void main(String[] args) {Collection<String> c = new ArrayList<String>();Iterator<String> it = c.iterator();c.add("An object");try {String s = it.next();} catch (ConcurrentModificationException e) {System.out.println(e);}}} /* Output:java.util.ConcurrentModificationException*///:~
看名称就能猜到,这应该是专属于List的迭代器,其功能也更加强大。
public class ListDemo2 {/*** @param args*/public static void main(String[] args) {List<String> list = new ArrayList<String>();list.add("abc1");list.add("abc2");list.add("abc3");System.out.println("list:"+list);ListIterator<String> it = list.listIterator();//获取列表迭代器对象// 它可以实现在迭代过程中完成对元素的增删改查。while(it.hasNext()){String str = it.next();if(str.equals("abc1")){it.set("abc9"); // 替换当前元素}}while(it.hasPrevious()){System.out.println("previous:"+it.previous());}System.out.println("list:"+list);}} /* Output:list:[abc1, abc2, abc3]previous:abc3previous:abc2previous:abc9list:[abc9, abc2, abc3]*///:~
在JDK1.6之后出现了一些新的方法来取代原来的。
getFirst(); // 获取但不移除,如果链表为空,抛出NoSuchElementException.getLast();// jdk1.6之后peekFirst(); // 获取但不移除,如果链表为空,返回null.peekLast():removeFirst(); // 获取并移除,如果链表为空,抛出NoSuchElementException.removeLast();// jdk1.6之后pollFirst(); // 获取并移除,如果链表为空,返回null.pollLast();
HashSet类,内部是哈希表结构,是通过对象的hashCode和equals方法来完成对象唯一性的。如果对象的hashCode值不同,那么不用判断equals方法,就直接存储到哈希表中;如果对象的hashCode值相同,那么要再次判断对象的equals方法是否为true。如果为true,视为相同元素,不存;如果为false,那么视为不同元素,就进行存储(哈希冲突)。记住:如果元素要存储到HashSet集合中,必须覆盖hashCode方法和equals方法,来建立对象判断是否相同的依据。TreeSet类,内部结构是二叉树,而且可以进行排序。判断元素唯一性的方式:就是根据比较方法的返回结果是否是0,是0,就是相同元素,不存。TreeSet类进行排序的两种方式: Comparable接口。覆盖compareTo方法。Comparator接口,覆盖compare方法。将该类对象作为参数传递给TreeSet集合的构造函数。前缀名就是该集合的数据结构,后缀名就是该集合所属的体系。
array:就要想到数组,就要想到查询快,有角标。link:就要想到链表,就要想到增删快,就要想到有add get remove+first last的方法 。tree:就要想到二叉树,就要想要排序,就要想到两个接口Comparable和Comparator。hash:就要想到哈希表,就要想到唯一性,就要想到元素需要覆盖hashcode方法和equals方法。而且通常这些常用的集合容器都是不同步的。
Java容器的插入方法并不检查类型,所以很可能会插入错误的类型:
List l_raw = new ArrayList<Integer>();l_raw.add(new Double(3)); // 运行没有任何异常抛出
上面的代码明显不符合我们所期望的,可以使用Collections工具类提供的类型检查的方法,比如checkedList返回一个在插入时检查类型的List:
List<Integer> l = Collections.checkedList(new ArrayList<Integer>(), Integer.class);List l_raw = l;// java.lang.ClassCastException: Attempt to insert class java.lang.Double// element into collection with element type class java.lang.Integer// ! l_raw.add(new Double(3)); // ERROR 运行抛出异常
Java类库里所有的具体容器类都继承自某个Abstract容器类。Abstract容器类存在的价值在于,当你需要写一个容器类,可以继承自这个Abstract容器类并覆盖你仅需要支持的方法,其它的不需要支持的方法可以继续抛出异常。这样你就不必耗费大量精力去实现容器接口的所有方法。
未获支持的操作异常,在容器的使用中,有两个需要注意的方法,因为它们的某些操作会发生这些异常。
Arrays.asList(..)与Collections.unmodifiableList(..)方法
public class Unsupported {static void test(String msg, List<String> list) {System.out.println("--- " + msg + " ---");Collection<String> c = list;Collection<String> subList = list.subList(1, 8);// Copy of the sublist:Collection<String> c2 = new ArrayList<String>(subList);try {c.retainAll(c2);} catch (Exception e) {System.out.println("retainAll(): " + e);}try {c.removeAll(c2);} catch (Exception e) {System.out.println("removeAll(): " + e);}try {c.clear();} catch (Exception e) {System.out.println("clear(): " + e);}try {c.add("X");} catch (Exception e) {System.out.println("add(): " + e);}try {c.addAll(c2);} catch (Exception e) {System.out.println("addAll(): " + e);}try {c.remove("C");} catch (Exception e) {System.out.println("remove(): " + e);}// The List.set() method modifies the value but// doesn't change the size of the data structure:try {list.set(0, "X");} catch (Exception e) {System.out.println("List.set(): " + e);}}public static void main(String[] args) {List<String> list = Arrays.asList("A B C D E F G H I J K L".split(" "));test("Modifiable Copy", new ArrayList<String>(list));test("Arrays.asList()", list);test("unmodifiableList()", Collections.unmodifiableList(new ArrayList<String>(list))); // 返回容器的只读版本}} /* Output:--- Modifiable Copy ------ Arrays.asList() ---retainAll(): java.lang.UnsupportedOperationExceptionremoveAll(): java.lang.UnsupportedOperationExceptionclear(): java.lang.UnsupportedOperationExceptionadd(): java.lang.UnsupportedOperationExceptionaddAll(): java.lang.UnsupportedOperationExceptionremove(): java.lang.UnsupportedOperationException--- unmodifiableList() ---retainAll(): java.lang.UnsupportedOperationExceptionremoveAll(): java.lang.UnsupportedOperationExceptionclear(): java.lang.UnsupportedOperationExceptionadd(): java.lang.UnsupportedOperationExceptionaddAll(): java.lang.UnsupportedOperationExceptionremove(): java.lang.UnsupportedOperationExceptionList.set(): java.lang.UnsupportedOperationException*///:~
Arrays.asList()返回固定尺寸的List,可以发现其中有一个set()方法,修改List中的元素是可以的,因为这并没有违法该List“尺寸固定”这一特性。
Collections类有办法能够自动同步整个容器
import java.util.*;public class Synchronization {public static void main(String[] args) {Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>());List<String> list = Collections.synchronizedList(new ArrayList<String>());Set<String> s = Collections.synchronizedSet(new HashSet<String>());Set<String> ss = Collections.synchronizedSortedSet(new TreeSet<String>());Map<String, String> m = Collections.synchronizedMap(new HashMap<String, String>());Map<String, String> sm = Collections.synchronizedSortedMap(new TreeMap<String, String>());}} ///:~
可以直接将新生成的容器传递给适当的“同步”方法。
SortedSet接口有一个子类TreeSet实现类,了解一下该接口的功能。里面有三个方法值得注意:
headSet(E toElement):返回此set的部分视图,其元素严格小于toElement。subSet(E fromElement, E toElement):返回此set的部分视图,其元素从 fromElement(包括)到toElement(不包括)。tailSet(E fromElement):返回此 set的部分视图,其元素大于等于fromElement。
public class SortedSetDemo {public static void main(String[] args) {SortedSet<String> sortedSet = new TreeSet<String>();Collections.addAll(sortedSet,"one two three four five six seven eight".split(" "));print(sortedSet);String low = sortedSet.first();String high = sortedSet.last();print(low);print(high);Iterator<String> it = sortedSet.iterator();for (int i = 0; i <= 6; i++) {if (i == 3)low = it.next();if (i == 6)high = it.next();elseit.next(); // i=3时,走了两次next()}print(low);print(high);print(sortedSet.subSet(low, high));print(sortedSet.headSet(high));print(sortedSet.tailSet(low));}} /* Output:[eight, five, four, one, seven, six, three, two]eighttwoonetwo[one, seven, six, three][eight, five, four, one, seven, six, three][one, seven, six, three, two]*///:~
SortedMap接口,在现阶段TreeMap类是它的唯一实现,里面的方法和SortedSet接口类似。
HashMap对速度进行了优化,因为HashMap使用了特殊的值,称作散列码。散列码是“相对唯一”的、用以代表对象的int值,它是通过将对象的某些信息进行转换而生成的。hashCode()是根类Object中的方法,因此所有Java对象都能用hashCode()方法产生散列码。
使用散列的目的在于:想要使用一个对象查找另一个对象。
散列使得查询得以快速进行,由于瓶颈位于键的查询速度,存储一组元素最快的数据结构就是数组。在JVM中就维护了一个巨大的内存数组,我们通过键对象的hashCode()生成一个散列码,将其作为数组的下标。
为了解决数组容量被固定的问题,不同的键可以产生相同的下标。也就是说,可能会有冲突。因此,数组多大不重要,任何键总能在数组中找到它的位置。
于是查询一个值的过程首先是计算散列码,然后将散列码作为角标查询数组。通常,冲突由外部链接处理:数组并不直接保存值,而是保存值的list,然后对list的值使用equals()方法进行线性查询。这部分的查询自然会比较慢,但是散列函数好的话,数组中发生冲突比较少,在数组的每一个位置就只有较少的值。所以对于HashMap,快速的跳到数组的某个位置,只对含有很少元素的list进行比较。
散列表中的“槽位”(slot)通常称为桶位(bucket)。
我们将表示实际散列表的数组名为bucket,在明白了散列之后,编写自己的hashCode()就更有意义。
对于链表list数组,数组的大小依赖于具体的HashMap对象的容量,而容量的改变与容器的充满程度和负载因子有关。hashCode()生成的结果,经过处理后成为了桶位的下标。
设计
hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。
如果在讲一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。所以如果你的hashCode方法依赖于对象中易变的数据,用户就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码,相当于产生了一个不同的键。
public class HashCodeTest {int age;String name;char sex;boolean flag;public HashCodeTest(int age, String name, char sex, boolean flag) {this.age = age;this.name = name;this.sex = sex;this.flag = flag;}public static void main(String[] args) {System.out.println(new HashCodeTest(10, "lemon", 'M', true).hashCode());System.out.println(new HashCodeTest(10, "lemon", 'M', true).hashCode());}} /* Output:13838846481701381926*///:~
默认的hashCode()使用的是对象的地址,有时候不同对象拥有相同的内容,我们会希望这样的对象的散列码相同。所以应该使用对象内有意义的识别信息。
// 对于String而言,散列码是基于String的内容的。public class StringHashCode {public static void main(String[] args) {System.out.println(new String("Hello").hashCode());System.out.println(new String("Hello").hashCode());}} /* Output:6960965069609650*///:~
要想使
hashCode()实用,必须速度快,并且有意义。
也就是说,它必须基于对象的内容生成散列码,我们更应该关注生成的速度,而不是唯一性,但是通过hashCode()和equals(),必须能够完全确定对象的身份。
好的hashCode()应该产生分布均匀的散列码。如果散列码都集中在一块,那么HashMap或者HashSet在某些区域的负载会很重,这样就不如分布均匀的散列函数快。不过幸运的是,我们只需要选择有意义的内容,高级编译器会自动帮我们生成覆盖后的hashCode()和equals()方法。
// 对于之前的HashCodeTest类,由编译器自动生成的hashCode()和equals()方法@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + age;result = prime * result + (flag ? 1231 : 1237);result = prime * result + ((name == null) ? 0 : name.hashCode());result = prime * result + sex;return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;HashCodeTest other = (HashCodeTest) obj;if (age != other.age)return false;if (flag != other.flag)return false;if (name == null) {if (other.name != null)return false;} else if (!name.equals(other.name))return false;if (sex != other.sex)return false;return true;}
我们可以通过手工调整HashMap来提高其性能,从而满足我们特定应用的需求。为了在调整HashMap时让你理解性能问题,某些术语是必需了解的:
HashMap和HashSet都具有允许你指定初始容量和负载因子的构造器。默认的负载因子是0.75,当负载情况达到指定负载因子的水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中(这被称为再散列)。
更高的负载因子可以降低表所需要的空间,但是会增加查找代价,因为查找使我们在大部分时间里所做的操作(包括get()和put())。如果你知道将要在HashMap中存储多少项,那么创建一个具有恰当大小的初始容量将避免自动在散列的开销。
容器类中有中特殊的Map,即WeakHashMap,它用来保存WeakReference。这是一种节约存储空间的技术,因为WeakHashMap允许垃圾回收器自动清理键和值。
import java.util.*;class Element {private String ident;public Element(String id) { ident = id; }public String toString() { return ident; }@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + ((ident == null) ? 0 : ident.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Element other = (Element) obj;if (ident == null) {if (other.ident != null)return false;} else if (!ident.equals(other.ident))return false;return true;}protected void finalize() {System.out.println("Finalizing " + getClass().getSimpleName() + " " + ident);}}class Key extends Element {public Key(String id) { super(id); }}class Value extends Element {public Value(String id) { super(id); }}public class CanonicalMapping {public static void main(String[] args) throws InterruptedException {int size = 10;Key[] keys = new Key[size];WeakHashMap<Key, Value> map = new WeakHashMap<Key, Value>();Key k = null;Value v = null;for (int i = 0; i < size; i++) {// 在for循环结束后,k最终指向的对象也会具有强引用,所以key=key(9)不会被回收k = new Key(Integer.toString(i));v = new Value(Integer.toString(i));if (i % 4 == 0)keys[i] = k; // 强引用使对象不被回收map.put(k, v);}System.out.println("before gc size:"+map.size());System.gc(); // 启动垃圾回收器Thread.sleep(200); // 使主线程睡眠System.out.println("after gc size:"+map.size());System.out.println(map.get(new Key("1"))+" "+map.get(new Key("9")));}} /*before gc size:10Finalizing Key 5Finalizing Key 2Finalizing Key 1Finalizing Key 7Finalizing Key 6Finalizing Key 3after gc size:4null 9*///:~