java常用集合及集合框架总结

今天这篇文章我们重点总结一下java中常用的集合,及他们的特性和适用的场合。常见集合框架结构如下图(不是全部,只包含常用的)


1、List
1.1、ArrayList
1.2、LinkedList
1.3、Vector
1.4、List接口下各种接口实现类的比较和应用
2、Set
2.1、HashSet
2.2、TreeSet
2.3、SortSet
2.4、LinkedHashSet
2.5、Set接口下的各种接口实现类的比较和应用
3、Map
3.1、HashMap
3.2、LinkedHashMap
3.3、TreeMap
3.4、WeakHashMap
3.5、SortedMap
3.6、Map接口下的各种接口实现类的比较和应用
4、Collections
以下是文章正文:

1、list接口继承自Collection,Collection接口为集合类的基本接口,继承自Iterable接口。

public interface Collection<AnyType> extends Interable<AnyType>{
    int size();
    boolean isEmpty();
    void clear();
    boolean contains(AnyType x);
    boolean add(AnyType x);
    boolean remove(AnyType x);
    java.util.Iterator<AnyType> iterator();
}

Iterable接口

public interface Iterable<T> {

    /**
     * Returns an iterator over a set of elements of type T.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();
}

Iterable接口中iterator()方法返回一个迭代器,也就保证了实现Collection接口的方法也就能用迭代器进行迭代。Iterator接口:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果达到了集合的末端,next将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法,如果迭代器还有多个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。如下:

Collection<String> c = ...
Iterator <String> it = c.iterator();
while(it.hasNext()){
    String element = it.next();
    do something with element
}

从javaSE5.0起,这个循环可以采用一种更优雅的缩写方式。用“for each”循环可以更加简练地表示的同样的循环操作:

for(String element : c){
    do something element
}

元素被访问的顺序取决于集合类型,如果为ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值就加1.然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。虽然可以确定在迭代过程中能够遍历到集合中的所有元素,但却无法预知元素被访问的次序。java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用


这样的话,我们在删除一个元素的时候,要首先调用it.next()方法,如果调用remove之前没有调用next将是不合法的,会抛出IllegalStateException异常,连续调用两次remove方法也是不合理的,即next方法和remove方法是具有相互依赖性的。

1.1、ArrayList是一个的实质就是一个可变数组,它的随机访问速度是最快的。但是对随机项的插入和删除操作代价是比较昂贵的,除非变动是在ArrayList末端,原因是从数组中间删除一个元素,所有的元素都要向数组的前端移动,同理,在数组中间插入一个元素也是如此。

1.2、LinkedList,有序,用链表实现的集合,数组是在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用,java语言中的链表实际上都是双向链接,每个节点还存放着指向前驱节点的引用。这样在链表中间删除一个元素是很轻松的操作,只需要对被删除元素附近的节点更新一下就可以了。

List<String> a = new LinkedList<String>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
ListIterator<String> aIter = a.listIterator();
//因为Iterator接口没有add方法,我们只能调用listIterato()方法返回一个实现了ListIteratoraIter接口的迭代器对象
aIter.next();
aIter.add("Juliet");

运行结果如下:

Amy
Juliet
Carl
Erica

每次执行添加操作,都会将新添加的元素添加到迭代器的前面,当用一个刚刚由Iterator返回,并且指向链表表头的迭代器调用add操作,新添加的元素将变成列表的新表头,当迭代器越过链表的最后一个元素时(即hasNext返回false),新添加的元素将变成列表的新表尾。有那个元素,则有n+1个位置可以添加新元素。

set方法用一个新元素取代调用next或previous方法返回的上一个元素:String oldValue = aIter.next(); aIter.set(newValue);

如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再被使用。链表迭代器的设计使它能够检测到这种修改,如果迭代器发现他的集合被另外一个迭代器修改了,或是该集合自身的方法修改了,就会抛出一个Concurrent ModificationExceptiony异常,例如:

List<String> list = ...
ListIterator it1 = list.listIterator();
ListIterator it2 = list.listIterator();
it1.next();
it1.remove();
it2.next();//throws Concurrent ModificationException

由于it2检测到这个链表被从外部修改了,所以对it2.next()的调用抛出了一个异常。添加元素add和删除元素remove为链表会跟踪的结构性修改,set操作就不被视为结构性修改,不会抛出异常。另:for(int i = 0; i< list.size(); i++)这种随机访问方式效率极低,每次查找一个元素都要从头重新搜索,是对LinkedList的一种错误使用方式

1.3、vector:也是一个动态数组,但vector是同步的,在程序中要求线程安全的情况下可以使用vector,如果对线程安全性没有限制,建议使用ArrayList,毕竟同步操作是需要耗费大量的时间的。

1.4、建议避免使用以整数索引表示链表中位置的所有方法,如果需要对集合进行随机访问,就使用数组或ArrayList,而不是链表,使用链表的唯一理由就是尽可能地减少在列表中间插入或删除元素所付出的代价,如果只有少数几个元素,就完全可以用ArrayList。

最后对于每个接口和类中具体的方法和属性解释,建议去看源码,这里就不再赘述了。

2、Set接口代表不允许重复元素的Collection,由SortedSet给出的一种特殊类型的Set保证其中的各项处于有序的状态。

2.1、链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要查看某个指定的元素却又忘记他的位置,就需要访问所有的元素,直到找到为止。如果集合中包含的元素很多,将会消耗很多时间。如果不在意元素的顺序,可以有几种能够快速查找元素的数据结构,其缺点是无法控制元素的出现次序。他们将按照有利于操作目的的原则组织数据。其中一种众所周知的数据结构就是散列表:散列表为每个对象计算一个整数,称为散列码hashCode,散列码是由对象的实例域产生的一个整数,如hashSet和HashMap。

散列表用链表数组实现,每个链表为一个单元,要想查看某个单元中对象的位置,就要先计算它的散列码,然后与单元数取余,所得到的的结果就是保存这个对象的单元的索引。有的时候单元已经被填满,这种情况我们称之为散列冲突。如果大致知道最终会有多少个元素要插入到散列表中,就可以设置单元数。通常将单元数设置为预计元素个数的75%~150%。单元数最好为素数(质数),以防健的集聚。现在标准类库使用的单元数是2的幂,默认为16(为表大小提供的任何值都将被自动转换为2的下一个幂)。当然,有的时候,我们并不能准确的预估要储存的单元数,如果散列表太满,就需要再散列,如果要对散列表再散列,就需要创建一个单元数更多的表,并将原来表中所有法人元素都插入到新表中,丢弃旧表,装填因子决定何时对散列表进行再散列,例:果装填因子为0.75(默认值),而表中超过75%的位置已经被填入元素,这个表就会用双倍的单元数自动进行再散列。

HashSet就是一种基于散列表的集,set没有重复元素,当add一个元素时,会先查看集中是否已经包含该元素,不包含再执行插入操作。下面我们进行一个简单的测试,可以看到HashSet里面的元素位置是随机的:

Set<String> words = new HashSet<String>();
long totalTime = 0;
Scanner in = new Scanner(System.in);
System.out.println("请输入字符串:");//按ctrl+z结束键盘输入
while(in.hasNext()){
    String word = in.next();
    long callTime = System.currentTimeMillis();
    words.add(word);
    callTime = System.currentTimeMillis() - callTime;
    totalTime += callTime;
}
System.out.print("\n");
Iterator<String> it = words.iterator();
for(int i = 1; i<=20 && it.hasNext();i++){
    System.out.print(it.next());
}
请输入字符串:
a
b
c
d
e
f
fdebca

可以看到元素是无序的,如果我们多测试几次会发现每次打印的顺序都是不一样的,另:如果集中的元素的散列码发生了变化,元素在数据结构中位置也会发生变化

Set set = new HashSet(int initialCapacity,float loadFactor);//initialCapacity初始单元数,loadFactor装填因子。

2.2、树集TreeSet,是一个有序集合,可以以任意顺序插入到集合中,在对集合进行遍历的时候,每个值将自动按照指定的顺序呈现,排序是按照树结构完成的。

将一个元素加入到树中,要比添加到散列表中慢很多,但是与将元素添加到数组或链表的正确的位置上相比还是快很多的,如果树中包含n个元素,查找新元素的位置平均需要log2n次比较。下面我们写一个例子进行一下比较:

Set<Integer> hashSet = new HashSet<Integer>(16,0.75F);//指定单元数与填充因子
Set<Integer> treeSet = new TreeSet<Integer>();
		
long hashSetStartTime = System.currentTimeMillis();
for(Integer i = 0;i<50000;i++){
    hashSet.add(i);
}
System.out.println("往HashSet中添加5000的元素需要的时间为(毫秒):"+ (System.currentTimeMillis()-hashSetStartTime));
		
long treeSetStartTime = System.currentTimeMillis();
for(Integer i = 0;i<50000;i++){
    treeSet.add(i);
}
System.out.println("往TreeSet中添加5000的元素需要的时间为(毫秒):"+ (System.currentTimeMillis()-treeSetStartTime));
往HashSet中添加5000的元素需要的时间为(毫秒):16
往TreeSet中添加5000的元素需要的时间为(毫秒):62

以上如果添加的元素再多一点效果应该会更明显。

那么TreeSet是如何对元素进行排序的呢?一般是对要加入到TreeSet集合中的元素实现Comparable接口,重写compareTo方法,如下所示:

class Item impplements Comparable<Item>{
    public int compareTo(Item other){
        return partNumber - other.partNumber;
    }
}

如果上述方法返回一个负值,说明元素位于other的前面。但是这样有一个限制,就是我们添加该元素的所有集合都只能按照这个顺序进行排列,如果一个集合中需要按照部件编号进行排序,另一个集合需要按照描述信息进行排序,又有时元素并未实现Comparable接口,这时候我们又该怎么办呢?这时候我们就需要用到Comparator接口,

class ItemComparator implements Comparator<Item>{
    public int compare(Item a,Item b){
        String descA = a.getDescription();
        String descB = b.getDescription();
        return descA.compareTo(descB);
    }
}
ItemComparator comp = new ItemComparator ();
SortedSet<Item> sortByDescription = new TreeSet<Item>(comp );

从java6起,TreeSet类实现了NavigableSet接口,这个接口增加了几个便于定位元素以及反向遍历的方法,具体情况请看API。

2.3、SortSet是一个接口,对Set接口进行了扩充:

Comparator<? super E> comparator()
返回用于对元素进行排序的比较器,如果元素用Comparable接口的compareTo方法进行比较则返回null。
E first()
E last()
返回有序集中的最小元素或最大元素。

NavigableSet接口实现了SortSet接口,在SortSet的基础上又进行了扩展。

2.4、LinkedHashSet,java1.4增加的类,用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中


如上图所示,LinkedHashSet还是散列表的形式进行存储的,只不过每个元素都指向前一个元素和后一个元素,迭代的时候会按照元素的插入顺序进行输出。

2.5、HashSet插入和查找某个指定元素速度快,但是无序,TreeSet插入和查找某个元素的速度略低于HashSet,但是TreeSet是有序的,可以自定义排序方式。LinkedHashSet克服了HashSet无序的缺点,使元素按照插入顺序排列。LinkedHashSet、HashSet、LinkedList之间的比较如下:

long linkStartTime = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
    link.add(i);
}
System.out.println("往LinkedHashSet中添加50000的元素需要的时间为(毫秒):"+ (System.currentTimeMillis()-linkStartTime));
		
long setStartTime = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
    set.add(i);
}
System.out.println("往HashSet中添加50000的元素需要的时间为(毫秒):"+ (System.currentTimeMillis()-setStartTime));
		
long listStartTime = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
    list.add(i);
}
System.out.println("往LinkedList中添加50000的元素需要的时间为(毫秒):"+ (System.currentTimeMillis()-listStartTime));
		
Iterator linkit = link.iterator();
long linkStartTime1 = System.currentTimeMillis();
while(linkit.hasNext()){
    linkit.next();
}
System.out.println("迭代LinkedHashSet需要的时间为(毫秒):"+ (System.currentTimeMillis()-linkStartTime1));
		
Iterator setit = set.iterator();
long setStartTime1 = System.currentTimeMillis();
while(setit.hasNext()){
    setit.next();
}
System.out.println("迭代HashSet需要的时间为(毫秒):"+ (System.currentTimeMillis()-setStartTime1));
		
Iterator listit = list.iterator();
long listStartTime1 = System.currentTimeMillis();
while(listit.hasNext()){
    listit.next();
}
System.out.println("迭代LinkedList需要的时间为(毫秒):"+ (System.currentTimeMillis()-listStartTime1));
往LinkedHashSet中添加50000的元素需要的时间为(毫秒):512
往HashSet中添加50000的元素需要的时间为(毫秒):158
往LinkedList中添加50000的元素需要的时间为(毫秒):344
迭代LinkedHashSet需要的时间为(毫秒):14
迭代HashSet需要的时间为(毫秒):14
迭代LinkedList需要的时间为(毫秒):11
往LinkedHashSet中添加50000的元素需要的时间为(毫秒):546
往HashSet中添加50000的元素需要的时间为(毫秒):193
往LinkedList中添加50000的元素需要的时间为(毫秒):365
迭代LinkedHashSet需要的时间为(毫秒):14
迭代HashSet需要的时间为(毫秒):14
迭代LinkedList需要的时间为(毫秒):12

运行两次的结果如上,我们可以看到插入元素LinkedHashSet最慢,LinkedList次之,HashSet是最快的。LinkedHashSet插入元素的速度几乎为LinkedList和HashSet的和。迭代速度LinkedHashSet和HashSet是相同的,且都比LinkedList要慢一点。

3、Map就是映射表,映射表用来存放键值对,键必须是唯一的,不能对同一个键存放两个不同的值,如果对同一个键调用两次put方法,第二个值会取代第一个值并返回旧的值。java类库为映射表提供了两个通用的实现HashMap和TreeMap,散列映射表对键进行散列,树映射表用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。集合框架并没有将映射表本身视为一个集合,然而我们可以获得映射表的视图,视图是实现了Cllection接口的对象的,从而用迭代器实现对Map的迭代。

V get(Object key):获取与键对应的值;返回与键对应的对象,如果在映射表中没有这个对象则返回null,键可以为null。
V put(K key,V value):将键与对应的值关系插入到映射表中。如果这个键已经存在,新的对象将取代与这个键对应的旧对象。这个方法将返回键对应的旧值。如果这个键以前没有出现过则返回null,键可以为null,但值不能为null。
void putAll(Map<? extends K, ? extends V> entries):将给定映射表中的所有条目添加到这个映射表中。
boolean containsKey(Object key):如果在映射表中已经有这个键,返回true
boolean containsValue(Object object):如果映射表中已经有这个值,返回true
Set<Map.Entry<K,V>> entrySet():返回Map.Entry对象的集视图,即映射表中的键值对,可以从这个集中删除元素,同时也从映射表中删除他们,但是不能添加任何元素
Set<K> keySet():返回映射表中所有的键的集视图。可以从这个集中删除元素,同时也从映射表中删除了它们。但是,不能添加任何元素该方法返回一个实现Set接口的类的对象,这个类的方法对原映射表进行操作。这种集合我们称之为视图。
Collection<V> values():返回映射表中所有值的集合视图。可以从这个集中删除元素,同时也从映射表中删除了它们,但是不能添加任何元素

Map.Entry<K,V>中包含getKey()、getValue()、setValue(V newValue)三个方法。

3.1、HashMap:new HashMap(int initialCapacity,float loadFactor)用给定的容量和装填因子构造一个空的散列映射表(装填因子是一个0.0~1.0之间的数值。这个数值决定散列表填充的百分比。一旦到了这个比例,就要将其再散列到更大的表中)

3.2、LinkedHashMap:用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当将对象插入到表中时,就会并入到双向链表中。

Map<String,Employee> staff = new LinkedHashMap<String,Employee>(128,0.75F,true);
staff.put("144-25-5464", new Employee("Amy Lee"));
staff.put("567-24-2546", new Employee("Harry Hacker"));
staff.put("157-62-7935", new Employee("Gary Cooper"));
staff.put("456-62-5527", new Employee("Francesca Cruz"));
		
Iterator<String> keyIt1 = staff.keySet().iterator();
System.out.println("访问元素前:");
while(keyIt1.hasNext()){
    System.out.println(keyIt1.next());
}
		
Employee employee = staff.get("144-25-5464");
		
Iterator<String> keyIt = staff.keySet().iterator();
System.out.println("访问元素后:");
while(keyIt.hasNext()){
    System.out.println(keyIt.next());
}
访问元素前:
144-25-5464
567-24-2546
157-62-7935
456-62-5527
访问元素后:
567-24-2546
157-62-7935
456-62-5527
144-25-5464

3.3、TreeMap:有序,按照插入顺序进行排列,我们经常用的输入法不就这个原理吗,按照访问顺序进行排列。最近访问过的排在最前面。

TreeMap map = new TreeMap();
map.put(new Item(1,"1"), "c");
map.put(new Item(2,"2"), "b");
map.put(new Item(3,"3"), "a");
map.put(new Item(4,"4"), "d");
map.put(new Item(5,"5"), "e");
		
Iterator it = map.entrySet().iterator();
while(it.hasNext()){
    Map.Entry<Item, String> entry = (Entry<Item, String>) it.next();
    System.out.println(entry.getValue());
}
c
b
a
d
e

3.4、WeakHashMap:弱散列映射表,该类型的映射表是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了,将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键/值对无法从映射表中删除。为什么垃圾回收器不能删除它呢?难道删除无用的对象不是垃圾回收器的工作吗?

遗憾的是,事情没有这么简单。垃圾回收器跟踪活动的对象。只要映射表对象是活动的,其中的所有单元也是活动的,它们就不能被回收。因此,需要由程序负责从长期存活的映射表中删除那些无用的值。或者使用weakHashMap完成这件事情。当对键的唯一引用来自于散列表元素时,这一数据结构将于垃圾回收器协同工作一起删除键/值对。

下面我们来剖析一下WeakHashMap的的内部运行机制,WeakHashMap使用弱引用保存键,在这里就是散列表键。通常,如果垃圾回收器发现某个特定的对象已经没有他人的引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用,一个弱引用被放入队列中意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应条目

3.5、SortedMap:是一个接口,对Map接口进行了扩充。

Comparator<? super K> comparator();
SortedMap<K,V> subMap(K fromKey, K toKey);
SortedMap<K,V> headMap(K toKey);
 SortedMap<K,V> tailMap(K fromKey);
K firstKey();
K lastKey();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();

主要是一些针对有序Map的一些操作方法。

3.6、Map接口下的各种接口实现类的比较和应用:HashMap散列映射表,随机,速度快;TreeMap红黑树结构映射表,有序(插入顺序),速度比散列映射表慢;WeakHashMap能及时回收不再被引用的键值对;LinkedHashMap链接散列映射表,有序(访问顺序),最近访问的总是会被放到链表尾部。

4、Collections:

ArrayList类和Vector类都实现了RandomAccess接口,RandomAccess接口没有任何方法,用来检测一个特定的集合是否支持高效的随机访问,所以ArrayList类和Vector类都支持高效随机访问

SortedSet和SortedMap接口暴露了用于排序的比较器对象。

Arrays类的静态方法asList将返回一个包装了普通java数组的List包装器,例如:Card[] cardDeck = new Card[52];List<Card> cardList = Arrays.asList(cardDeck );返回的对象不是ArrayList,它是一个视图对象,带有访问底层数组的get和set方法。改变数组的大小的所有方法都会抛出一个UnsupportedOperationException异常。

Collections.nCopies(n,anObject);将返回一个实现了LIst接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个Object的错觉。

Collections.singleton(anObject)则将返回一个视图对象,这个对象实现了Set接口,返回的对象实现了一个不可修改的单元素集,而不需要付出建立数据结构的开销。

/*
* 视图只是包装了接口而不是实际的集合对象
*/
Collection c = new ArrayList();
/*
* 将一个数组转换成集合视图,可以修改,但不能进行增加和删除等可以改变数组大小的操作
*/
String[] strs = new String[50];
List<String> list = Arrays.asList(strs);
		
/*
* 不可修改视图,这些集合对现有集合增加了一个运行时检查。如果发现视图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态
* */
Collection unmodifiableCollection = Collections.unmodifiableCollection(c);
List<String> unmodifiableList = Collections.unmodifiableList(list);
Map unmodifiableMap = Collections.unmodifiableMap(new HashMap());
Set unmodifiableSet = Collections.unmodifiableSet(new HashSet());
SortedMap unmodifiableSortedMap = Collections.unmodifiableSortedMap(new TreeMap());
SortedSet unmodifiableSortedSet = Collections.unmodifiableSortedSet(new TreeSet());
		
/*
 * 同步视图,多线程访问保证线程安全
 * */
Collection synchronizedCollection = Collections.synchronizedCollection(c);
List synchronizedList = Collections.synchronizedList(new ArrayList());
Map synchronizedMap = Collections.synchronizedMap(new HashMap());
Set synchronizedSet = Collections.synchronizedSet(new HashSet());
SortedMap synchronizedSortedMap = Collections.synchronizedSortedMap(new TreeMap());
SortedSet synchronizedSortedSet = Collections.synchronizedSortedSet(new TreeSet());
		
/*
 * 检查视图,检测插入对象是否属于给定类型,如果不属于给定类型,则抛出一个ClassCastException
 * */
Collection checkedCollection = Collections.checkedCollection(c, List.class);
List checkedList2 = Collections.checkedList(new LinkedList(), LinkedList.class);
Map checkedMap = Collections.checkedMap(new HashMap(), Object.class, Object.class);
Set checkedSet = Collections.checkedSet(new TreeSet(), Set.class);
SortedMap checkedSortedMap = Collections.checkedSortedMap(new TreeMap(), Object.class, Object.class);
SortedSet checkedSortedSet = Collections.checkedSortedSet(new TreeSet(), Set.class);
		
/*
* 构造一个对象视图,它既可以作为一个拥有n个元素的不可修改列表,又可以作为一个拥有单个元素的集。
* */
List<String> nlist = Collections.nCopies(0, "DEFAULT");
Set<String> set = Collections.singleton("Sara,how are you!");

越整理发现越多,java集合的内容还有很多,不论是往深度剖析还是往广度剖析,都是需要耗费很大精力的一件事情,本文当初设计目录的时候也是一种错误,文章包含内容太多了,估计读者你们都不想看了吧,总结经验,下次每篇文章只能剖析一个知识点,这样我写着不累,你们看着也不会累。

参考资料:《java核心技术 卷1》《数据结构与算法分析 java语言描述》--【美】马克.艾伦.维斯著

猜你喜欢

转载自blog.csdn.net/qq_35689573/article/details/80568983