数组与集合的相互转换

来源:互联网 发布:lumia800软件下载 编辑:程序博客网 时间:2024/04/24 12:00

在实际开发过程中,为了处理的方便或者接口类型的要求,我们经常需要在集合与数组之间进行相互转换,JDK为我们提供了方便的工具类和相应的方法来完成这个工作。Arrays.asList()方法与Collection.toArray()方法一起充当了基于数组的API与基于 Collection的API之间相互转化的桥梁,但是由于不甚了解其实现的原理,很多同学在使用的过程中经常会被一些问题困扰,在此通过源码解读总结一下相关的知识。

1. 集合转数组

Collection接口中定义了两个方法用于将集合中的元素转成数组中的元素,所有实现了Collection接口的集合类型,比如ArrayList,HashSet都必须实现这两个方法,虽然各类集合的具体实现方式可能会不同,但是这两个方法的功能是确定的。下面以ArrayList<String>转String[]为例,我们可以分别查看其在JDK1.7中的源码

ArrayList.toArray()

/** * Returns an array containing all of the elements in this list * in proper sequence (from first to last element). *  * ... * */public Object[] toArray() {    return Arrays.copyOf(elementData, size);}

从上述代码可以看到,toArray()方法直接把ArrayList底层数组elementData中的元素全部拷贝到一个新数组中并返回,不管ArrayList中存储的是哪种类型的元素,该方法的返回类型都是Object[]数组,如果调用者试图将Object[]转成具体对象数组类型,比如

ArrayList<String> list = new ArrayList<>();String[] strings = (String[]) list.toArray();

那么就会抛出ClassCastException,因为这是不支持的强转方式。那么我们如何才能得到想要的具体对象类型的数组呢?另一个重载的toArray()方法可以解决。


ArrayList.toArray(T[] a)

我们先来看看该方法的源码

/** * Returns an array containing all of the elements in this list in proper * sequence (from first to last element); the runtime type of the returned * array is that of the specified array.  If the list fits in the * specified array, it is returned therein.  Otherwise, a new array is * allocated with the runtime type of the specified array and the size of * this list. * */@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) {    if (a.length < size)        // Make a new array of a's runtime type, but my contents:        return (T[]) Arrays.copyOf(elementData, size, a.getClass());    System.arraycopy(elementData, 0, a, 0, size);    if (a.length > size)        a[size] = null;    return a;}

该方法是一个泛型方法,支持任何对象类型。要求调用者传递一个指定类型的数组对象a,如果a装不下ArrayList的所有元素,那么就会新建一个长度合适的数组,数组的类型就是a的运行时类型(通过反射得到a中元素的类型);如果传入的数组a装得下所有元素,那么直接把集合中的所有元素都拷贝到数组a中,并且如果数组的长度超过了集合的size,那么拷贝完成之后要把数组中剩余空间的首元素置空,目的是用于标记集合元素的结束位置,类似于c语言中字符串的结束标志“\0”的作用。

通过对比上述两个方法,可以明显感觉到JDK 1.5引入泛型之后带来的方便之处,所以在明确知道集合中元素的类型并且想要使用泛型,那么推荐使用第二个转换方法。


2. 数组转集合

由于数组是一种特殊的类型,为了更加方便地操作它,JDK提供了一个强大的工具类Arrays,它提供了一个静态方法asList()来实现数组到集合的直接转换。

Arrays.asList()

首先看源码

/** * Returns a fixed-size list backed by the specified array.  (Changes to * the returned list "write through" to the array.)  This method acts * as bridge between array-based and collection-based APIs, in * combination with {@link Collection#toArray}.  The returned list is * serializable and implements {@link RandomAccess}. * */@SafeVarargspublic static <T> List<T> asList(T... a) {    return new ArrayList<>(a);}

这个方法看起来非常方便好用,但是实际上使用它会有诸多限制。方法体只有一句话,就是使用传递进来的可变参数a(可以是对象类型的数组或者一个对象列表,比如字符串列表{“a”, “b”, “c”})重新new了一个ArrayList再直接返回,但是我们知道java.util.ArrayList类并没有相应版本的构造函数。通过F3键继续查看源码,我们发现这个ArrayList其实并不是我们常用的那个ArrayList,它是Arrays的一个私有内部类java.util.Arrays.ArrayList,它确实提供了一个合适的构造方法ArrayList(E[] array)

private static class ArrayList<E> extends AbstractList<E>    implements RandomAccess, java.io.Serializable{    private static final long serialVersionUID = -2764017481108945198L;    private final E[] a;    ArrayList(E[] array) {        if (array==null)            throw new NullPointerException();        a = array;    }    public int size() {        return a.length;    }    public Object[] toArray() {        return a.clone();    }    public <T> T[] toArray(T[] a) {        int size = size();        if (a.length < size)            return Arrays.copyOf(this.a, size,                                 (Class<? extends T[]>) a.getClass());        System.arraycopy(this.a, 0, a, 0, size);        if (a.length > size)            a[size] = null;        return a;    }    public E get(int index) {        return a[index];    }    public E set(int index, E element) {        E oldValue = a[index];        a[index] = element;        return oldValue;    }    public int indexOf(Object o) {        if (o==null) {            for (int i=0; i<a.length; i++)                if (a[i]==null)                    return i;        } else {            for (int i=0; i<a.length; i++)                if (o.equals(a[i]))                    return i;        }        return -1;    }    public boolean contains(Object o) {        return indexOf(o) != -1;    }}

这样看来,似乎已经解决了从数组到集合的转化问题,实际上转化确实已经实现了,但是遗留的问题是,我们并不能对这个返回的Arrays$ArrayList的对象进行结构修改操作,当调用add或remove方法对集合做增删操作时,会抛出不支持此操作的异常,具体原因我们可以通过分析它的继承体系来探究。

从上面的源码可以看到Arrays$ArrayList继承了抽象类AbstractList,而AbstractList实现了List接口,并且实现了List接口中的大部分骨干方法。我们常用的ArrayList、Vector等集合类都继承自AbstractList,他们各自需要把AbstractList中没有实现的那些List方法根据自身的操作特点单独实现。这么做的目的其实就是为了更好地使用多态的特性!我们可以通过List引用来指向子类对象ArrayLaist或者Vector,调用List中的方法就可以通过动态绑定去调用子类中重写的方法。然而,AbstractList所实现的List骨干方法中并不包括add、remove这些操作,因为修改不同的集合类没有统一的操作方式,所以这类方法必须要留给具体的集合类自己去实现,那么AbstractList真的就没有实现add和remove方法吗?我们再来看一下AbstractList的源码

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {    /**     * Sole constructor.  (For invocation by subclass constructors, typically     * implicit.)     */    protected AbstractList() {    }    public boolean add(E e) {        add(size(), e);        return true;    }    abstract public E get(int index);    public E set(int index, E element) {        throw new UnsupportedOperationException();    }    public void add(int index, E element) {        throw new UnsupportedOperationException();    }    public E remove(int index) {        throw new UnsupportedOperationException();    }    ...

可以清楚地看到,AbstractList没有给出get方法的实现,那么这就要求任何继承了他的子类必须要给出get方法的实现,除非这个子类也是抽象类,而对于set,add,remove方法,AbstractList直接抛出UnsupportedOperationException,这又是为什么呢?原因很简单,如果继承他的子类也没有重写这三个方法,那就说明该子类不支持相应的操作!如果子类实现了这三个方法,那么根据多态的特性,运行时执行的就是子类中的set,add,remove方法,所以问题全都回到了Arrays$ArrayList自身,我们再回过头去看他的源码,很明显它实现了get方法,重写了set方法,但却没有重写add和remove方法,所以当我们调用add和remove方法时,实际上执行的是其父类AbstractList中对应的方法,所以也就理所应当地抛出了UnsupportedOperationException异常。

一般而言,我们是需要对Arrays.asList()返回的集合对象做get、add等操作的,那么我们如何实现呢?答案也很简单,那就是利用这个返回的Arrays$ArrayList对象再new一个java.util.ArrayList对象:

String[] a = {"a", "b", "c"};List<String> list = new ArrayList<>(Arrays.asList(a));

另外,工具类Collections也提供了一个方法可以实现数组到集合的拷贝

String[] s = {"a", "b", "c"};List<String> list = new ArrayList<>();Set<String> set = new HashSet<>();Collections.addAll(list, s);Collections.addAll(set, s);

这个方法要求数组中的元素必须是对象类型的,那么如果是基本类型的数组,如何转换呢?


基本数据类型数组转集合

由于java的集合是用于统一组织管理多个对象的,其内部储存的只是各个对象的引用,而8种基本数据类型不是Object的子类,即不是对象类型,所以不能把它们放到集和之中。从JDK1.5开始,为了方便操作,java支持基本数据类型的autoboxing/unboxing,那么如果使用Arrays.asList()方法,能不能将int[]转成List<Integer>的集合呢? 让我们来试验一下

int[] a = { 1, 2, 3 };String[] b = { "a", "b", "c" };List listA = Arrays.asList(a);List listB = Arrays.asList(b);System.out.println(listA);System.out.println(listA.get(0).getClass().getName());System.out.println(listA.size());System.out.println(listB);System.out.println(listB.get(0).getClass().getName());System.out.println(listB.size());

输出结果如下

[[I@31b47bff][I1[a, b, c]java.lang.String3

可以看到数组b中存储的是String类型,结果符合预期,实现了对象数组到集合的转换,而对于数组a,asList()也能正确执行,但是结果却很怪异,首先listA被打印出来的字符串是[I@31b47bff,看形式就知道这就是Object中定义的toString()方法的结果,也即是getClass().getName() + ‘@’ + Integer.toHexString(hashCode()),那么[I是什么呢?其实这个符号表示的是一维数组,再看listA.size()等于1,说明listA中只有一个对象,这对象就是数组a,没错,数组本身也是一个对象类型的,asList()方法直接把int[]数组a当做一个完整的对象了,然后把这个唯一的对象放到了List集合中。在大部分情况下,这种转换并不符合我们的本意,而且不报任何警告或异常,所以在使用时要特别注意这一点。

那么我们如何实现把一个int[]数组转成集合呢?

有两种基本选择,要么先把int[]数组转成Integer[]数组,然后使用asList()方法,要么就用for循环把数组元素一个一个地添加到集合中,这个过程会自动装箱。

至此,数组与collection集合之间的相互转换的方式和细节就比较清晰了。

0 0
原创粉丝点击