java 集合框架

来源:互联网 发布:linux dump内存命令 编辑:程序博客网 时间:2024/04/27 19:18

一、为什么需要集合框架?

        Java提供了很多持有对象的方法,包括数组、util包中的容器类。集合,在Java中十分重要,可能会在面试中出现类似于“Collection 和 Collections的区别”、“HashMap和Hashtable的区别”的针对集合框架问题,可见一斑。

        那么,JDK又为我们提供了一个怎样的集合框架呢?Java API为开发者提供了一系列的接口、抽象类和实现类,并具有非常丰富的操作方法可供调用。接下来,就让我们总览一下JDK提供的集合框架图。

二、集合框架的两组接口(Collection、Map)各有何特点?

                

                                                                           图1. 集合接口总览

                

        相信有UML基础的朋友,对此图一定不会陌生。此图主要分为两组接口:Collection和Map。那么它们的区别是什么?

        个人的理解是这样:Map是利用键值对来储存的,而Collection是用来储存一组对象的(更像平时的数学集合),两者的关注点不同。

        在接下来的博客里,将陆续逐个学习每个接口、抽象类和实现类的使用。


                                                                    Iterator

    在我的工作中,广泛使用了 List 类型的引用变量,而引用的是具体的类 ArrayList。我们通常会使用 for 循环遍历一个 List,每一次循环为 List 调用的 get 方法传入一个循环变量,这样就取到了具体位置的元素,并进行业务上的处理。那么,我们为什么需要 Iterator 接口?

    通过 for 循环遍历的方式,需要事先知道其内部结构,每种集合有各自的遍历方法,由此产生的是访问代码和集合本身形成了紧耦合,不利于复用。想象一下,当初使用了 ArrayList 处理数据,而现在告诉你要使用 LinkedList,而你的业务代码又复杂而繁琐……好了,现在有了Iterator接口,你再也不用关心你遍历的集合的具体实现,统一通过调用 Iterator 的方法,使用同一种逻辑就可以间接遍历整个集合,何乐而不为呢!

一、迭代器模式

    迭代器模式,它提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。具体的几点要求:

1、访问一个聚合对象的内容而无需暴露它的内部表示;

2、支持对聚合对象的多种遍历;

3、为遍历不同的聚合结构提供一个统一的接口。

    Iterator 接口正是做到了这几点,下面,看看 Iterator 接口定义的三个方法。

二、Iterator 接口方法

    1、boolean hasNext()

            文档:Returns true if the iteration has more elements. (In other words, returns true if next would return an element rather than throwing an exception.)

            常用在 next 方法之前,用于判断 next 方法是否能返回一个元素,不至于调用 next 方法是抛出异常。

    2、E next()

            文档:Returns the next element in the iteration.

            文档很容易理解,注意这里使用了泛型就可以了。

    3、void remove()

            文档:Removes from the underlying collection the last element returned by the iterator (optional operation). This method can be called only once per call to next. The behavior of an iterator is unspecified if the underlying collection is modified while the iteration is in progress in any way other than by calling this method.

            remove 方法将删除最后一次使用 next 方法返回的元素,需要注意每次调用了 next 方法后,此方法只能被调用一次,否则抛出 IllegalStateException 异常。

            如果进行迭代时用调用此方法之外的其他方式修改了该迭代器所指向的collection,则迭代器的行为是不确定的。也就是说,在迭代时使用 remove 方法修改了迭代器指向的 collection,迭代器的行为能确定,但调用其它方法却不是这样的。

三、ListIterator 接口

    ListIterator 继承了 Iterator。ListIterator 是系列表迭代器,允许程序员按任一方向遍历列表、迭代期间修改列表,并获得迭代器在列表中的当前位置。ListIterator 没有当前元素,它的光标位置始终位于调用 previous() 所返回的元素和调用 next() 所返回的元素之间。长度为 n 的列表的迭代器有 n+1 个可能的指针位置(第一个元素之前和最后一个元素之后)。

    下面逐一介绍 ListIterator 接口的方法:

    1、void add(E e)

        该方法可以将指定的对象元素 e 加入到列表。需要特别留意元素被加入列表的位置:

            (1)next 方法返回的下一个元素之前;

            (2)previous 方法返回的下一个元素之后;

            (3)列表为空时,成为列表唯一的元素。

    2、boolean hasNext()

        常用在 next 方法之前,用于判断 next 方法是否能返回一个元素,不至于调用 next 方法是抛出异常。

    3、boolean hasPrevious()

        逆向遍历列表方式下,用在 previous 方法之前,判断 previous 方法是否能返回一个元素,而不是抛出异常。

    4、E next()

        返回列表中的下一个元素。

    5、int nextIndex()

        返回对 next 后续调用所返回元素的索引。

        请看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明一个List
List<String> list = new ArrayList<String>();
 
// 添加元素
list.add("1");
list.add("2");
list.add("3");
 
// 获取迭代器
ListIterator<String> li = list.listIterator();
 
// 迭代遍历
while(li.hasNext()) {
    System.out.println(li.next() + ": " + li.nextIndex());
    li.remove();
}

1
2
3
4
结果如下:
11
21
31

        可见 nextIndex 返回的是(next 所返回元素的索引 + 1)。如果当前 next 返回的元素是列表最后一个元素,那么 nextIndex 返回的值就是整个列表的大小。

    6、E previous()

        返回列表中的上一个元素。

    7、int previousIndex()

        返回对 next 后续调用所返回元素的索引。

        请看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 声明List
List<String> list = new ArrayList<String>();
 
// 添加元素   
list.add("1");
list.add("2");
list.add("3");
 
// 获取迭代器
ListIterator<String> li = list.listIterator();
 
// 正向遍历
while(li.hasNext()) {
    System.out.println(li.next() + ": " + li.nextIndex());
}
 
// 逆向遍历
while(li.hasPrevious()) {
    System.out.println(li.previous() + ": " + li.previousIndex());
}

        

1
2
3
4
5
6
7
结果如下:
11
22
33
31
20
1: -1

        正向遍历是为了将光标移至列表尾部。可见,在逆向遍历方式中,previousIndex 返回的是(previous 所返回元素的索引 - 1)。如果当前 previous 返回的是列表中第一个元素,那么 previousIndex 返回的值等于-1。

    8、void remove()

        从列表中删除 next 或 previous 返回的最后一个元素。每一次调用 next 或 previous 方法之后只能调用一次此方法,另外,如果有 add 方法调用,那么 remove 方法必须在 add 方法之前被执行。

    9、void set(E e)

        set 方法提供的其实是一个修改功能,它将指定的元素 e 替换 next 或 previous 返回的最后一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 声明List
List<String> list = new ArrayList<String>();
 
// 添加元素
list.add("1");
list.add("2");
list.add("3");
 
// 获取迭代器
ListIterator<String> li = list.listIterator();
 
// 正向遍历并修改元素
System.out.println("修改前:");
while(li.hasNext()) {
    System.out.println(li.next());
    li.set("4");
}
 
// 逆向遍历并输出元素
System.out.println("修改后:");
while(li.hasPrevious()) {
    System.out.println(li.previous());
}

1
2
3
4
5
6
7
8
9
结果
修改前:
1
2
3
修改后:
4
4
4

四、Iterable 接口

    Iterator接口中定义的三个方法都依赖于迭代器的当前迭代位置,如果让这些集合直接实现 Iterator 接口,那么这些集合接口和类中势必需要定义一些储存当前迭代位置的成员变量。由此而产生的问题是,当在多处使用一个集合时,由于迭代位置的不确定性,调用该集合的 next 方法时返回的结果也是不可预知的。

    那么有什么办法呢?Iterable接口为我们提供了一个 iterator 方法,每当调用该方法时,便会返回一个新的迭代器,而这些迭代器是互不干扰的,这样便可以使用多个迭代器的方式来处理数据。如果希望更深一步研究Iterator和Iterable,可以去研究研究JDK中是如何实现了Iterable接口的。

    在CSDN论坛中一个关于两者区别的回答:

Iterable接口实现后的功能是“返回”一个迭代器,而Iterator接口实现后的功能是“使用”一个迭代器。

    本文的例子中,用到了 iterator 方法,因为ArrayList 实现了Iterable接口,调用 iterator 方法返回一个迭代器,在对迭代器进行操作即可。

    下面看看API中如何说明 iterator 方法:

        Iterator<T> iterator() —— Returns an iterator over a set of elements of type T.

                                                                Collection      

    Collection 接口是一个根部接口,以Collection 为父接口的这一组接口所存储的是一组对象,也可称作一组元素。那这些元素是否有序,是否允许重复,各种操作有无特殊性?根据这些问题,我们很容易理解为什么有子接口 List、Set和Queue了。

    更多信息,查阅 JDK 官方文档。

二、方法

    1、int size()

        这个方法在循环列表时经常使用,它得到集合中的对象个数,集合中元素个数>Integer.MAX_VALUE时,则返回Integer.MAX_VALUE。

    2、boolean isEmpty()

        判断集合中是否包含对象,不多说。

    3、boolean contains(Object o)

        判断集合中是否包含指定的对象。

        #当指定对象为null时,如果集合中至少有一个对象为null,那么调用此方法返回true;

        #当指定对象不为空时,如果有对象 e 满足 o.equals(e),那么调用此方法将返回true。

        #当指定对象为null,而调用该方法的集合对象不允许null元素存在时,则会抛出NullPointerException异常。

    4、Iterator<E> iterator

        此方法从Iterable接口中继承而来,返回的是在此集合上进行迭代的迭代器。由于Collection接口并不确定元素是否有序,即使有序,也无法确定是以何种方式排序,所以迭代的顺序不可预知。

    5、Object[] toArray()

        此方法将一组对象元素的存储方式转换成了数组,利用数组来管理这组对象。由迭代器方法 iterator 返回的对象顺序,也将作用到此方法。

        另外,调用此方法返回的数组可以任意修改而不会对原集合造成任何影响。

此方法充当了基于数组的 API 和基于Collection 的 API 之间的桥梁

    6、<T> T[] toArray(T[] a)

        把集合存储到指定的数组 a 中,但如果 a 的容量不够了,那么将建立一个与 a 数组有相同运行时类型的新数组用来存储集合中的元素。

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
// 声明一个List
List<String> list = new ArrayList<String>();
 
// 添加元素
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.add("6");
 
// 新建数组
String[] strArr = new String[5];
 
// 调用toArray(T[] a)方法
list.toArray(strArr);
 
// 输出数组
System.out.println("旧数组:");
for(String str : strArr) {
    System.out.println(str);
}
 
// 输出新数组
System.out.println("新数组:");
for(String str : newStrArr) {
    System.out.println(str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
结果如下:
旧数组:
null
null
null
null
null
新数组:
1
2
3
4
5
6

可以看到,strArr 的容量并不够容纳下集合元素,strArr不能胜任这份工作,toArray方法返回一个新的数组。下面将数组的容量增到到足以容纳下集合元素个数。

1
2
3
// 将数组的容量扩大,并初始化数组的一个元素
String[] strArr = new String[7];
strArr[0] = "IAmHere";
1
2
3
4
5
6
7
8
结果如下:
1
2
3
4
5
6
null

        可见,调用此方法会忽略掉初始化的数组元素,进而抢占数组,即使抢占后数组还有剩余容量,也不会分配给初始元素。

    7、boolean add(E e)

        向集合中添加指定元素 e。

        返回true的情况:

            (1)集合中没有此元素,集合允许添加指定类型 E;

            (2)集合中由此元素,但允许有重复元素;

        返回false的情况:集合不允许有重复元素,集合中有此元素。

    8、boolean remove(Object o)

        如果集合中有指定元素 o,则将 o 移除,并返回true,否则返回false。

    9、boolean containsAll(Collection<?> c)

        集合中包含集合 c 中的所有元素,则返回true。觉得苛刻吗?

    10、boolean addAll(Collection<? extends E> c)

        将指定集合 c 中的所有元素都添加到此集合中,集合若因此调用发生变化,则返回true。

        有趣的是,自己添加自己会发生什么呢?我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明一个List
List<String> list = new ArrayList<String>();
 
// 添加元素
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.add("6");
 
// 添加自己
list.addAll(list);
 
// 输出
for(String str : list) {
    System.out.println(str);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
结果:
1
2
3
4
5
6
1
2
3
4
5
6

        合理吗?官方文档说这种调用的行为是不确定的,那么我们最好别这么干!

    11、boolean removeAll(Collection<?> c)

        移除集合中那些也包含在指定集合 c 中的所有元素,若因此调用导致集合发生改变,则返回true。此调用移除的是两个集合的交集

    12、boolean retainAll(Collection<?> c)

        移除集合中那些不包含在指定集合 c 中的所有元素,若因此调用导致集合发生改变,则返回true。此调用保留的是两个集合的交集

    13、void clear()

        移除集合中的所有元素。

    14、boolean equals(Object o)

        比较集合和指定对象 o 是否相等。

    15、int hashCode()

        返回集合的哈希码值。(该学习下哈希码值了。)

                                                                                   

                                                 List

一、List?

    Collection接口往下继承产生了三个接口,它们有各自的特点,本文要学习的是具有“有序可重复”特点的 List 接口,它也叫做列表。

    在列表中,这使得我们能够精确定位需要进行操作的元素,因为我们有列表的索引,它和数组的索引一致,都是从0开始计数。在接下来要学习的方法中,我们可以发现很多方法都提供了索引参数,有些方法的返回值则是元素的索引。

    List 接口也继承了 ListIterator 接口,这使得 List 中有两个继承而来的获取迭代器的方法,而 List 根据自身特点,又重载了一个方法来获取从列表中指定位置开始的迭代器。

二、Why are there so many methods!

    不要慌乱,虽然 List 接口提供了很多方法,而纵观这些方法,发现可以分类学习,更容易快速掌握。

    (一)本身

        1、int size()

            返回集合本身的大小。

        2、int hashCode()

            返回集合的哈希码值。

    (二)插入

        1、boolean add(E e)

            此方法在列表的尾端加入新元素 e。

            列表发生改变,则返回true。

        2、void add(int index, E element)

            在指定的索引位置插入指定的元素 element,该 element 抢占了指定的索引位置。而原来索引位置上的元素以及该索引以后的元素,索引值都+1。

            不返回任何值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明一个List
List<String> list = new ArrayList<String>();
 
// 添加元素
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
list.add("6");
 
System.out.print("插入前:");
for(String str : list) {
    System.out.print(str + "->");
}
 
list.add(2"I'm the new guy!");
 
System.out.print("\n插入后:");
for(String str : list) {
    System.out.print(str + "->");
}
1
2
3
结果:
插入前:1->2->3->4->5->6->
插入后:1->2->I'm the new guy!->3->4->5->6->

        3、boolean addAll(Collection<? extends E> c)

            将指定集合 c 中的所有元素插入到列表的结尾,而新插入的这些元素的顺序是由指定集合 c 中迭代器方法返回的顺序决定的。

            列表发生改变后,返回true。

        4、boolean addAll(int index, Collection<? extends E> c)

            抢占 index 索引位置,从该位置开始将指定集合 c 中所有元素插入,插入成功后,新列表的索引也自然而然。

            列表发生改变后,返回true。

    (三)删除

        1、boolean remove(Object o)

            删除列表中第一次出现的指定元素 o(如果列表中存在元素 o,返回true,否则返回false)。

            例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<String> list = new ArrayList<String>();
 
list.add("1");
list.add("2");
list.add("3");
list.add("4");
         
System.out.println("删除元素【2】:" + list.remove("2"));
System.out.println("删除元素【5】:" + list.remove("5"));
 
System.out.print("删除后的列表:");
for(String str : list) {
    int index = list.indexOf(str);
    if(index != (list.size() - 1)) {
        System.out.print(str + "->");
    else {
        System.out.print(str);
    }
}
1
2
3
4
结果:
删除元素【2】:true
删除元素【5】:false
删除后的列表:1->3->4

        2、boolean removeAll(Collection<?> c)

            移除列表中所有包含在指定集合 c 中的元素,相当于移除了列表和集合 c 的交集。

            如果列表因调用该方法而发生改变,则返回true。

        3、boolean retainAll(Collection<?> c)

            和 removeAll 方法相反,retainAll 方法保留的是列表中所有包含在指定集合 c 中的元素,相当于保留了列表和集合 c 的交集。

            如果列表因调用该方法而发生改变,则返回true。

        4、E remove(int index)

            移除指定索引位置上的元素。

            这个方法的返回值很特殊,它返回的是什么?看代码:

1
2
3
4
5
6
7
8
List<String> list = new ArrayList<String>();
 
list.add("1");
list.add("2");
list.add("3");
list.add("4");
 
System.out.println("删除索引位置为【1】的元素,返回结果为:" + list.remove(1));
1
2
结果如下:
删除索引位置为【1】的元素,返回结果为:2

        5、void clear()

            清空该列表。

    (四)访问

        1、E get(int index)

            返回列表中索引为 index 的元素。

        2、List<E> subList(int fromIndex, int toIndex)

            可以用开闭区间的方式来看,将索引位置在 [fromIndex, toIndex) 中的元素按照原顺序提取出来,组成一个新的列表。

            对该方法返回的新列表的任何操作都不会对原列表产生影响。

            新列表的元素个数 = toIndex - fromIndex

    (五)修改

        1、E set(int index, E element)

            将指定索引位置上的元素替换为指定的元素 element。这个方法的返回值也比较特殊,和 remove(int index) 方法一样,都是返回替换之前的元素

    (六)比较、判断方法

        1、boolean contains(Object o)

            判断列表中是否存在指定元素 o。

        2、boolean containsAll(Collection<?> c)

            判断列表中是否存在指定集合 c 中的所有元素,相当于判断集合 c 是否为列表的子集

        3、boolean equals(Object o)

            比较列表和指定对象 o 是否相等,而相等的条件也比较苛刻,它要求指定对象也是一个列表,而两个列表要以相同的顺序包含相同的元素。

        4、boolean isEmpty()

            判断列表中是否有元素存在。

    (七)检索

        1、int indexOf(Object o)

            在列表中以正向顺序查找指定元素 o,返回第一个符合条件的元素索引。但如果列表中不存在该元素,那就返回-1.

        2、int lastIndexOf(Object o)

            和上一个方法类似,唯一的区别是:它返回的是最后一个符合条件的元素索引。

    (八)迭代器

        1、ListIterator<E> listIterator()

            返回列表迭代器,进而可以使用 ListIterator 接口中的方法。(ListIterator 接口在第二篇博客介绍过)

        2、ListIterator<E> listIterator(int index)

            也返回一个列表迭代器,但是迭代器的起始位置是列表的 index 索引位置。

            代码容易理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
List<String> list = new ArrayList<String>();
 
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
 
ListIterator<String> listIterator = list.listIterator(2);
 
for(; listIterator.hasNext(); ) {
    System.out.println(listIterator.next());
}
1
2
3
4
结果:
3
4
5

        3、Iterator<E> iterator()

            普通的返回迭代器的方法,该方法继承自 Iterable 接口。

    (九)转换

        1、Object[] toArray()

            将列表中的元素转换为由数组来存储,而元素的顺序可以按照特殊的要求来实现,一般情况是和列表的索引一一对应。

            该方法返回一个新数组,而对新数组的操作对原列表没有任何影响。

        2、<T> T[] toArray(T[] a)

            返回按适当顺序(从第一个元素到最后一个元素)包含列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。如果指定数组能容纳列表,则在其中返回该列表。否则,分配具有指定数组的运行时类型和此列表大小的新数组。



1 0
原创粉丝点击