一 容器的用途
如果对象的数量与生命周期都是固定的,自然我们也就不需要很复杂的数据结构。
我们可以通过创建引用来持有对象,如
Class clazz;
也可以通过数组来持有多个对象,如
Class[] clazs = new Class[10];
然而,一般情况下,我们并不知道要创建多少对象,或者以何种方式创建对象。数组显然只能创建固定长度的对象,为了使程序变得更加灵活与高效,Java类库提供了一套完整的容器类,具备完善的方法来解决上述问题。
从上图可以看到容器中有七大接口:
- Collection接口
- Map接口
- Set接口
- List接口
- Queue接口
- Iterator接口
- Comparable接口
其中List, Queue和Set接口继承了Collection接口,剩下的接口之间都是相互独立的,无继承关系。Iterater迭代器则是为了更灵活的迭代集合,与foreach一起使用。Comparable接口则用于比较。
除了接口之外,容器中还包含了List接口的实现类LinkedList,Set接口的实现类HashSet等;容器中还包含两个工具类/帮助类:Collections和Arrays,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
容器主要分为两种类型:
- Collection:用来保存单一的元素序列,这些元素都服从一条或者多条规则。List必须按照插入的顺序保存元素,而Set不能有重复的元素,Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
- Map:用来保存关联键值对,允许使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找某个对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在一起;或者称为"字典",因此可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。
二 Collection中添加元素
在java.util包中的Collections和Arrays类中包含了很多使用方法,可以再一个Collection中添加一组元素。
- Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个Lsit对象。
- Collections.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分隔的列表,将元素添加到Collection中。
如下演示一个例子:
///添加一组元素 import java.util.*; public class AddingGoups { public static void main(String[] args) { //方式一 List<Integer> list = Arrays.asList(1,2,3,4,5); list.set(1, 1); //list.add(21); 无法改变长度,因为Arrays.asList()的输出,其底层表示的是数组 Collection<Integer> collection = new ArrayList<Integer>(list); //方式二 Integer[] moreInts = {6,7,8,9,10}; Collections.addAll(collection,moreInts); Collections.addAll(collection,11,12,13,14,15); //输出 System.out.println(collection); } }
输出结果:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
注意:可以直接使用Arrays.asList()的输出,将其作为List,但是这种情况下,其底层表示的是数组,因此不能调整尺寸。所以使用add()、delete()会出现错误。此外Arrays.asList()它对其所产生的List类型如做出了最理想的假设,即由传入的元素类型所决定,如List<Integer> list = Arrays.asList(1,2,3,4,5),因此如果将其赋值给不同的List类型可能会引发问题。
Collections了类中还提供了另一个函数fill(),其函数原型为:public static <T> void fill(List<? super T>,Tobj):使用同一个元素填充list集合中的每一元素,示例如下:
//方式三 List<Integer> ints = new ArrayList<Integer>(); Collections.addAll(ints, 1,2,3,4,5); Collections.fill(ints, 45); //fill(List<? super T,T obj>) System.out.println(ints);
输出如下:
[45, 45, 45, 45, 45]
三 容器的打印
容器的输出,默认调用的是容器.toString()方法,如下:
///打印容器 import java.util.*; public class PrintingContainers { static Collection fill(Collection<String> collection) { collection.add("rat"); collection.add("cat"); collection.add("dog"); collection.add("pig"); return collection; } static Map fill(Map<String,String> map) { map.put("rat","Fuzzy"); map.put("cat","Rags"); map.put("dog","Bosco"); map.put("pig","Spot"); return map; } public static void main(String[] args) { System.out.println(fill(new ArrayList<String>())); System.out.println(fill(new LinkedList<String>())); System.out.println(fill(new HashSet<String>())); System.out.println(fill(new TreeSet<String>())); System.out.println(fill(new LinkedHashSet<String>())); System.out.println(fill(new HashMap<String,String>())); System.out.println(fill(new TreeMap<String,String>())); System.out.println(fill(new LinkedHashMap<String,String>())); } }
输出如下:
[rat, cat, dog, pig] [rat, cat, dog, pig] [rat, cat, dog, pig] [cat, dog, pig, rat] [rat, cat, dog, pig] {rat=Fuzzy, cat=Rags, dog=Bosco, pig=Spot} {cat=Rags, dog=Bosco, pig=Spot, rat=Fuzzy} {rat=Fuzzy, cat=Rags, dog=Bosco, pig=Spot}
四 List接口
相比于Collection接口,添加了新的方法,使得可以再List中间插入和移除元素。其实现类有以下两个:
- ArrayList:类似于动态数组,适用于大量随机访问的情况,但是插入和删除的代价很高;
- LinkedList:LinkedList除了实现了List接口,类似于链表,提供了优化的顺序访问,在插入和删除访问方面代价低廉,但是随即访问代价较高;由于LinkedList还实现了Queue接口,可以使其用作栈、队列或双端队列。
1、ArrayList
下面演示一个ArrayList的例子,将ArrayList向上转型为List:
import java.util.*; class Person{ public String name; public int age; public boolean sex; public Person(String name, int age, boolean sex) { super(); this.name = name; this.age = age; this.sex = sex; } public String toString(){ return name + "-" + age + "-" + sex; } } public class ListFeatures { public static void main(String[] args) { List<Person> pers = new ArrayList<Person>(); Person p1 = new Person("吴定会",50,true); pers.add(p1); Person p2 = new Person("沈艳霞",46,false); pers.add(p2); Person p3 = new Person("张三",17,true); pers.add(p3); Person p4 = new Person("李四",34,false); pers.add(p4); System.out.println("1:"+pers); //成员函数1 System.out.println("2:"+pers.contains(p1)); //成员函数2 Person p = pers.get(2); //成员函数3 System.out.println("3:" + p +" " + pers.indexOf(p)); //成员函数4 System.out.println("4:" + pers.remove(p)); System.out.println("5:"+pers); //成员函数5 List<Person> sub = pers.subList(0, 2); //不包含2 System.out.println("6:"+sub); //成员函数6 System.out.println("7:"+pers.containsAll(sub)); Collections.shuffle(sub); System.out.println("shuffled sublist:" + sub); //成员函数7 pers.removeAll(sub); System.out.println("8:"+pers); //成员函数8 System.out.println("9:"+pers.isEmpty()); } }
输出如下:
1:[吴定会-50-true, 沈艳霞-46-false, 张三-17-true, 李四-34-false] 2:true 3:张三-17-true 2 4:true 5:[吴定会-50-true, 沈艳霞-46-false, 李四-34-false] 6:[吴定会-50-true, 沈艳霞-46-false] 7:true shuffled sublist:[沈艳霞-46-false, 吴定会-50-true] 8:[李四-34-false] 9:false
在上面例子中我们使用了List的部分方法:
- contains():判断一个对象是否在列表中。
- remove():删除一个对象,如果你想移除一个对象,则可以将这个对象的引用传递给这个方法;
- indexOf();用来判断一个对象在List中所处位置的索引编号,不存在返回-1;
- subList():从列表中创建一个子集;
- containsAll():判断是否存在该子集;
- removeAll():移除一个子集;
- isEmpty():判断是否为空;
除了以上方法,List还有很多方法,比如set()、replace()、clear()、addAll()、toArray()等。
2、LinkedList
下面演示一个LinkedList的例子
import java.util.*; public class LinkedListFeatures { public static void main(String[] args) { LinkedList<Person> pers = new LinkedList<Person>(); Person p1 = new Person("吴定会",50,true); pers.add(p1); Person p2 = new Person("沈艳霞",46,false); pers.add(p2); Person p3 = new Person("张三",17,true); pers.add(p3); Person p4 = new Person("李四",34,false); pers.add(p4); System.out.println(pers); //成员函数1 返回第一个元素 System.out.println("getFirst():"+pers.getFirst()); //成员函数2 返回第一个元素 System.out.println("element():" + pers.element()); //成员函数3 返回第一个元素 System.out.println("peek():" + pers.peek()); //成员函数4 移除并返回第一个元素 System.out.println("remove():" + pers.remove()); //成员函数5 删除并返回第一个元素 System.out.println("removeFirst():" + pers.removeFirst()); //成员函数6 删除并返回第一个元素 System.out.println("poll():" + pers.poll()); System.out.println(pers); //成员函数7 追加元素到List头部 pers.addFirst(new Person("郑洋",25,true)); System.out.println("After addFirst():" + pers); //成员函数8 追加元素到List尾部 pers.offer(new Person("杨德亮",25,true)); System.out.println("After offer():" + pers); //成员函数9 追加元素到List尾部 pers.addLast(new Person("黄旭",25,true)); System.out.println("After addLast():" + pers); //成员函数10 删除并返回最后一个元素 System.out.println("After removeLast():" + pers.removeLast()); } }
输出:
[吴定会-50-true, 沈艳霞-46-false, 张三-17-true, 李四-34-false] getFirst():吴定会-50-true element():吴定会-50-true peek():吴定会-50-true remove():吴定会-50-true removeFirst():沈艳霞-46-false poll():张三-17-true [李四-34-false] After addFirst():[郑洋-25-true, 李四-34-false] After offer():[郑洋-25-true, 李四-34-false, 杨德亮-25-true] After addLast():[郑洋-25-true, 李四-34-false, 杨德亮-25-true, 黄旭-25-true] After removeLast():黄旭-25-true
注意:上面程序中的element()、offer()、peek()、poll()、等方法都是Queue接口的方法,具体内容在Queue接口中将会详细介绍。
五 Set接口
Set不保存重复的元素,如果试图将相同对象的多个实例添加到Set中,那么它会组织这种重复现象。Set可以很容易查询某个元素是否在某个Set中,正因为如此,查找就成为了Set中最重要的操作。
具有与Collection相同的方法。实际上Set就是Collection,只是行为不同。其实现类有以下两个:
- HashSet:使用了散列函数实现元素的存储,极大的提高了访问速度,存入HashSet的对象必须定义hashCode()。注意:HashSet元素存储的顺序看起来没有实际意义。
- TreeSet:使用红黑树来实现存储,好处就是可以在插入之后维持集合的有序性,按照比较结果升序保存元素。
此外还有一个类型LinkedHashSet,继承自HashSet:
- LinkedHashSet:使用链表保存元素插入顺序,但是它也使用了散列,保留了HashSet的访问速度。
1、HashSet
下面是使用存放Integer对象的HashSet的示例:
import java.util.*; public class SetOfInteger { public static void main(String[] args) { Random rand = new Random(47); Set<Integer> intset = new HashSet<Integer>(); for(int i=0;i<10000;i++) { intset.add(rand.nextInt(30)); } System.out.println(intset); } }
输出结果:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
在本例中,intset中插入10000次,但是由于不保存重复元素,最终输出结果数目<=30。
2、TreeSet
如果想对插入的元素进行排序,可以使用TreeSet。
import java.util.*; public class SortedSetOfInteger { public static void main(String[] args) { Random rand = new Random(47); Set<Integer> intset = new TreeSet<Integer>(); for(int i=0;i<10000;i++) { intset.add(rand.nextInt(30)); } System.out.println(intset); } }
输出如下:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
六 Queue接口
队列是一个典型的先进先出(FIFO)的容器,即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。队列常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在并发编程中特别重要。
Queue的实现有以下两个:
- LinkedList:LinkedList实现了Queue接口,提供了支持队列行为的方法;
- PriorityQueue:与普通队列不同,优先队列每次弹出的是优先级最高的元素,可以通过提供自己的Comparator来修改默认的优先级顺序。
1、LinkedList
下面的示例使用了Queue接口中与Queue相关的方法:
import java.util.*; public class QueueDemo { public static void printQ(Queue queue){ while(queue.peek() != null) { System.out.print(queue.remove() + " "); } System.out.println(); } public static void main(String[] args) { Queue<Integer> queue = new LinkedList<Integer>(); Random rand = new Random(47); for(int i=0;i<10;i++) { queue.offer(rand.nextInt(i+10)); } printQ(queue); Queue<Character> qc = new LinkedList<Character>(); for(char c:"LaJiShiPin".toCharArray()) { qc.offer(c); } printQ(qc); } }
输出如下:
8 1 1 1 5 14 3 1 0 1 L a J i S h i P i n
下面介绍一下上面用到的方法:
- offer():它在允许的情况下,将一个元素插入到队尾,或者返回false;
- peek():在不移除的情况下返回队头,在队列为空时返回null;
- element():在不移除的情况下返回队头,在队列为空时抛出NoSuchElementException异常;
- poll():移除并返回队头,在队列为空时返回null;
- remove():移除并返回队头,在队列为空时抛出NoSuchElementException异常;
自动装包机制会自动将nextInt()方法的int结果转换为Integer对象,将char c转换成qc所需的Character对象。Queue接口窄化了对LinkedList的方法的访问权限,以使得只有恰当的方法可以使用,因此能够访问的LinkedList方法会变少(这里我们可以将queue转换回LinkedList,但是最好不要这么做)。
2、PriorityQueue
先进先出描述的是最典型的队列规则。队列规则是指在给定一组队列中的所有元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个元素应该等待时间最长的元素。
优先级队列声明下一个弹出元素是最重要的元素(具有最高的优先级)。例如,在飞机场,当飞机临近起飞时,这架飞机的乘客可以再办理登记手续时排到队头。
当调用PriorityQueue中的offer()方法:会插入一个对象到队列中,并且这个对象会在队列中被排序。
PriorityQueue可以确保调用peek(),poll()和remove()方法时,获取的元素将是队列中优先级最高的元素。
下面演示一个示例:
import java.util.*; public class PriorityQueueDemo { public static void main(String[] args) { //默认从小到大排序 PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>(); Random rand = new Random(47); for(int i=0;i<10;i++) { //将一个元素插入到队尾 priorityQueue.offer(rand.nextInt(i+10)); } QueueDemo.printQ(priorityQueue); //默认从小到大排序 List<Integer> ints = Arrays.asList(25,22,20,18,14,9,3,1,1,2,3,9,14,18,21,23,25); priorityQueue = new PriorityQueue<Integer>(ints); QueueDemo.printQ(priorityQueue); //从大到小排序 priorityQueue = new PriorityQueue<Integer>(ints.size(),Collections.reverseOrder()); priorityQueue.addAll(ints); QueueDemo.printQ(priorityQueue); //按字母从小到大排序 String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION"; List<String> strings = Arrays.asList(fact.split(" ")); PriorityQueue<String> stringPQ = new PriorityQueue<String>(strings); QueueDemo.printQ(stringPQ); //按字母从大到小排序 stringPQ = new PriorityQueue<String>(strings.size(),Collections.reverseOrder()); stringPQ.addAll(strings); QueueDemo.printQ(stringPQ); //按字母从小到大排序 Set<Character> charSet = new HashSet<Character>(); for(char c:fact.toCharArray()) { charSet.add(c); } PriorityQueue<Character> characterPQ = new PriorityQueue<Character>(charSet); QueueDemo.printQ(characterPQ); } }
输出如下:
0 1 1 1 1 1 3 5 8 14 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 EDUCATION ESCHEW OBFUSCATION SHOULD SHOULD OBFUSCATION ESCHEW EDUCATION A B C D E F H I L N O S T U W
从输出结果可以看到,重复是允许的,在默认排序规则下最小的值拥有最高的优先级。此外,还演示了如何使用自己的Comparator对象来改变排序,我们在PriorityQueue<类型>构造器调用的时候使用了由Collections.reverseOrder()产生的反序的Comparator。
Integer、String、Character可以与PriorityQueu一起工作,因为这些类已经内建了自然排序,如果想在PriorityQueu中使用自己的类,就必须包含额外的功能以产生自然排序,或者提供自己的Comparator。
七 Map接口
Map的实现有以下两个:
- HashMap:通过散列函数来实现,可以用来快速访问;
- TreeMap:使用红黑树来实现,保持键值处于排序状态,访问速度没有HashMap快;
此外还有一个类型LinkedHashMap,继承自HashMap:
- LinkedHashMap:使用链表保存元素插入顺序,但是它也使用了散列,保留了HashMap的访问速度。
1、HashMap
下面演示一个HashMap的例子:
import java.util.*; public class HashMapDemo { public static void main(String[] args) { Random rand = new Random(47); Map<Integer,Integer> m = new HashMap<Integer,Integer>(); for(int i=0;i<10000;i++) { int r = rand.nextInt(20); //获取出现次数 如果不存在返回null Integer freq = m.get(r); m.put(r, freq == null?1:freq + 1); } System.out.println(m); } }
输出如下:
{0=481, 1=502, 2=489, 3=508, 4=481, 5=503, 6=519, 7=471, 8=468, 9=549, 10=513, 11=531, 12=521, 13=506, 14=477, 15=497, 16=533, 17=509, 18=478, 19=464}
下面的示例将使用String描述来查Person:,还通过containsKey()和containsValue()来测试一个Map,以便看它是否包含某个键或某个值:
import java.util.*; public class PersonHashMap { public static void main(String[] args) { Map<String,Person> perMap = new HashMap<String,Person>(); perMap.put("吴定会", new Person("吴定会",50,true)); perMap.put("沈艳霞", new Person("沈艳霞",46,false)); System.out.println(perMap); Person p = perMap.get("沈艳霞"); System.out.println(p); System.out.println(perMap.containsKey("吴定会")); System.out.println(perMap.containsValue(p)); System.out.println(perMap.keySet()); System.out.println(perMap.values()); } }
输出如下:
{吴定会=吴定会-50-true, 沈艳霞=沈艳霞-46-false}
沈艳霞-46-false
true
true
[吴定会, 沈艳霞]
[吴定会-50-true, 沈艳霞-46-false]
七 不同类的差异
下面的示例展示各种不同类在方法上的差异:
import java.util.*; ///一个Set实用工具 public class Sets { //合并两个Set集合 public static <T> Set<T> union(Set<T> a,Set<T> b){ Set<T> result = new HashSet<T>(a); result.addAll(b); return result; } //提取两个Set共有的部分 public static <T> Set<T> intersection(Set<T> a,Set<T> b) { Set<T> result = new HashSet<T>(a); result.retainAll(b); return result; } //Subtract subset from superset public static <T> Set<T> difference(Set<T> superset,Set<T> subset) { Set<T> result = new HashSet<T>(superset); result.removeAll(subset); return result; } //获取除了交集以外的元素 public static <T> Set<T> complements(Set<T> a,Set<T> b) { return difference(union(a,b),intersection(a,b)); } }
///一个工具类,可以用来打印出java.util包中各种Collection类与Map类之间的方法差异 import java.util.*; import java.lang.reflect.*; public class ContainerMethodDifferences { //获取指定Class对象的所有public方法(包括继承的) public static Set<String> methodSet(Class<?> type){ Set<String> result = new TreeSet<String>(); for(Method m:type.getMethods()) result.add(m.getName()); return result; } //输出Class对象的所有接口名 public static void interfaces(Class<?> type) { System.out.print("Interfaces in " + type.getSimpleName() + ":"); List<String> result = new ArrayList<String>(); for(Class<?> c:type.getInterfaces()) result.add(c.getSimpleName()); System.out.println(result); } //获取Object类的所有public方法 public static Set<String> object = methodSet(Object.class); //类第一次加载时执行 static { object.add("clone"); } //Subtract subset from superset public static void difference(Class<?> superset,Class<?> subset) { System.out.print(superset.getSimpleName() + " extends " + subset.getSimpleName() + ", adds:"); //移除Object中的方法 Set<String> comp = Sets.difference(methodSet(superset),methodSet(subset)); comp.removeAll(object); System.out.println(comp); interfaces(superset); System.out.println(); } public static void main(String[] args) { System.out.println("Collection:" + methodSet(Collection.class)); interfaces(Collection.class); difference(Set.class,Collection.class); difference(HashSet.class,Set.class); difference(LinkedHashSet.class,HashSet.class); difference(TreeSet.class,Set.class); difference(List.class,Collection.class); difference(ArrayList.class,List.class); difference(LinkedList.class,List.class); difference(Queue.class,Collection.class); difference(PriorityQueue.class,Queue.class); System.out.println("-----------------------------------------------------"); System.out.println("Map:" + methodSet(Map.class)); interfaces(Map.class); difference(HashMap.class,Map.class); difference(LinkedHashMap.class,HashMap.class); difference(SortedMap.class,Map.class); difference(TreeMap.class,Map.class); } }
输出如下:
Collection:[add, addAll, clear, contains, containsAll, equals, forEach, hashCode, isEmpty, iterator, parallelStream, remove, removeAll, removeIf, retainAll, size, spliterator, stream, toArray] Interfaces in Collection:[Iterable] Set extends Collection, adds:[] Interfaces in Set:[Collection] HashSet extends Set, adds:[] Interfaces in HashSet:[Set, Cloneable, Serializable] LinkedHashSet extends HashSet, adds:[] Interfaces in LinkedHashSet:[Set, Cloneable, Serializable] TreeSet extends Set, adds:[headSet, descendingIterator, descendingSet, pollLast, subSet, floor, tailSet, ceiling, last, lower, comparator, pollFirst, first, higher] Interfaces in TreeSet:[NavigableSet, Cloneable, Serializable] List extends Collection, adds:[replaceAll, get, indexOf, subList, set, sort, lastIndexOf, listIterator] Interfaces in List:[Collection] ArrayList extends List, adds:[trimToSize, ensureCapacity] Interfaces in ArrayList:[List, RandomAccess, Cloneable, Serializable] LinkedList extends List, adds:[offerFirst, poll, getLast, offer, getFirst, removeFirst, element, removeLastOccurrence, peekFirst, peekLast, push, pollFirst, removeFirstOccurrence, descendingIterator, pollLast, removeLast, pop, addLast, peek, offerLast, addFirst] Interfaces in LinkedList:[List, Deque, Cloneable, Serializable] Queue extends Collection, adds:[poll, peek, offer, element] Interfaces in Queue:[Collection] PriorityQueue extends Queue, adds:[comparator] Interfaces in PriorityQueue:[Serializable] ----------------------------------------------------- Map:[clear, compute, computeIfAbsent, computeIfPresent, containsKey, containsValue, entrySet, equals, forEach, get, getOrDefault, hashCode, isEmpty, keySet, merge, put, putAll, putIfAbsent, remove, replace, replaceAll, size, values] Interfaces in Map:[] HashMap extends Map, adds:[] Interfaces in HashMap:[Map, Cloneable, Serializable] LinkedHashMap extends HashMap, adds:[] Interfaces in LinkedHashMap:[Map] SortedMap extends Map, adds:[lastKey, subMap, comparator, firstKey, headMap, tailMap] Interfaces in SortedMap:[Map] TreeMap extends Map, adds:[descendingKeySet, navigableKeySet, higherEntry, higherKey, floorKey, subMap, ceilingKey, pollLastEntry, firstKey, lowerKey, headMap, tailMap, lowerEntry, ceilingEntry, descendingMap, pollFirstEntry, lastKey, firstEntry, floorEntry, comparator, lastEntry] Interfaces in TreeMap:[NavigableMap, Cloneable, Serializable]
参考文献:
[1] Java编程思想