计算机系统应用教程网站

网站首页 > 技术文章 正文

ArrayList & Vector (transient关键字)--JAVA成长之路

btikc 2024-09-11 01:59:05 技术文章 16 ℃ 0 评论

集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是数据的存储。集合的作用就是以一定的方式组织、存储数据。下面说说ArrayList,只捡干货聊。

ArrayList特点

1、ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。

2、ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l),函数返回一个线程安全的ArrayList类,底层方法内使用synchronized同步块进行控制;也可以使用concurrent并发包下的CopyOnWriteArrayList类,底层数组直接使用关键字volatile。Vector则在操作数组的方法上加上了关键字synchronized。

3、ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。

4、每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素, 其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前, 应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

ArrayList底层

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:

1) 私有属性:

ArrayList定义只定义类两个私有属性:

     /**       * The array buffer into which the elements of the ArrayList are stored.       * The capacity of the ArrayList is the length of this array buffer.       */ private transient Object[] elementData;          /**       * The size of the ArrayList (the number of elements it contains).       *       * @serial       */ private int size;

很容易理解,elementData存储ArrayList内的元素(应该是堆内存中元素的引用,而不是实际的元素 ),size表示它包含的元素的数量。

有个关键字需要解释:transient。

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。

有点抽象,看个例子应该能明白。

public class UserInfo implements Serializable {       private static final long serialVersionUID = 996890129747019948L;       private String name;       private transient String psw;          public UserInfo(String name, String psw) {           this.name = name;           this.psw = psw;       }          public String toString() {           return "name=" + name + ", psw=" + psw;       }   }      public class TestTransient {       public static void main(String[] args) {           UserInfo userInfo = new UserInfo("张三", "123456");           System.out.println(userInfo);           try {               // 序列化,被设置为transient的属性没有被序列化               ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(                       "UserInfo.out"));               o.writeObject(userInfo);               o.close();           } catch (Exception e) {               // TODO: handle exception               e.printStackTrace();           }           try {               // 重新读取内容               ObjectInputStream in = new ObjectInputStream(new FileInputStream(                       "UserInfo.out"));               UserInfo readUserInfo = (UserInfo) in.readObject();               //读取后psw的内容为null               System.out.println(readUserInfo.toString());           } catch (Exception e) {               // TODO: handle exception               e.printStackTrace();           }       }   }

被标记为transient的属性在对象被序列化的时候不会被保存。ntData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

    private void writeObject(java.io.ObjectOutputStream s)            throws java.io.IOException{        // Write out element count, and any hidden stuff        int expectedModCount = modCount;        s.defaultWriteObject();        // Write out array length        s.writeInt(elementData.length);        // Write out all elements in the proper order.        for (int i=0; i<size; i++)            s.writeObject(elementData[i]);            if (modCount != expectedModCount) {               throw new ConcurrentModificationException();            }        }     }

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

  • 加快了序列化的速度

  • 减小了序列化之后的文件大小

这种做法也是值得学习、借鉴的一种思路。接着回到ArrayList的分析中......

2) 构造方法:
ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的。

    // ArrayList带容量大小的构造函数。    public ArrayList(int initialCapacity) {            super();            if (initialCapacity < 0)                throw new IllegalArgumentException("Illegal Capacity: "+                                                   initialCapacity);            // 新建一个数组            this.elementData = new Object[initialCapacity];        }           // ArrayList无参构造函数。默认容量是10。    public ArrayList() {            this(10);        }           // 创建一个包含collection的ArrayList        public ArrayList(Collection<? extends E> c) {            elementData = c.toArray();            size = elementData.length;            if (elementData.getClass() != Object[].class)                elementData = Arrays.copyOf(elementData, size, Object[].class);        }

3) 元素存储与扩容:

ArrayList 提供了set(int index, E element)、add(E e)、add(int index, E element)、 addAll(Collection<? extends E> c)、 addAll(int index, Collection<? extends E> c)这些添加元素的方法。

20 // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。21 public E set(int index, E element) {  22    RangeCheck(index);  23 24    E oldValue = (E) elementData[index];  25    elementData[index] = element;  26    return oldValue;  27 }    28 // 将指定的元素添加到此列表的尾部。29 public boolean add(E e) {  30    ensureCapacity(size + 1);   31    elementData[size++] = e;  32    return true;  33 }    34 // 将指定的元素插入此列表中的指定位置。35 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。36 public void add(int index, E element) {  37    if (index > size || index < 0)  38       throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  39    // 如果数组长度不足,将进行扩容。40    ensureCapacity(size+1); // Increments modCount!!  41    // 将 elementData中从Index位置开始、长度为size-index的元素,  42    // 拷贝到从下标为index+1位置开始的新的elementData数组中。43    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。44    System.arraycopy(elementData, index, elementData, index + 1, size - index);  45    elementData[index] = element;  46    size++;  47 }    48 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。49 public boolean addAll(Collection<? extends E> c) {  50    Object[] a = c.toArray();  51    int numNew = a.length;  52    ensureCapacity(size + numNew); // Increments modCount  53    System.arraycopy(a, 0, elementData, size, numNew);  54    size += numNew;  55    return numNew != 0;  56 }    57 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。58 public boolean addAll(int index, Collection<? extends E> c) {  59    if (index > size || index < 0)  60      throw new IndexOutOfBoundsException(  61        "Index: " + index + ", Size: " + size);  62 63    Object[] a = c.toArray();  64    int numNew = a.length;  65    ensureCapacity(size + numNew); // Increments modCount  66 67    int numMoved = size - index;  68    if (numMoved > 0)  69      System.arraycopy(elementData, index, elementData, index + numNew, numMoved);  70 71    System.arraycopy(a, 0, elementData, index, numNew);  72    size += numNew;  73    return numNew != 0;     }

底层数组的大小不够了怎么办?答案就是扩容,这也就是为什么一直说ArrayList的底层是基于动态数组实现的原因,动态数组的意思就是指底层的数组大小并不是固定的,而是根据添加的元素大小进行一个判断,不够的话就动态扩容,扩容的代码就在ensureCapacity里面:

 public void ensureCapacity(int minCapacity) {    modCount++;    int oldCapacity = elementData.length;    if (minCapacity > oldCapacity) {        Object oldData[] = elementData;        int newCapacity = (oldCapacity * 3)/2 + 1;            if (newCapacity < minCapacity)               newCapacity = minCapacity;        // minCapacity is usually close to size, so this is a win:        elementData = Arrays.copyOf(elementData, newCapacity);    } }

看到扩容的时候把元素组大小先乘以3,再除以2,最后加1。可能有些人要问为什么?我们可以想:

  • 如果一次性扩容扩得太大,必然造成内存空间的浪费

  • 如果一次性扩容扩得不够,那么下一次扩容的操作必然比较快地会到来,这会降低程序运行效率,要知道扩容还是比价耗费性能的一个操作

所以扩容扩多少,是JDK开发人员在时间、空间上做的一个权衡,提供出来的一个比较合理的数值。最后调用到的是Arrays的copyOf方法,将元素组里面的内容复制到新的数组里面去:

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {           T[] copy = ((Object)newType == (Object)Object[].class)               ? (T[]) new Object[newLength]               : (T[]) Array.newInstance(newType.getComponentType(), newLength);           System.arraycopy(original, 0, copy, 0,                            Math.min(original.length, newLength));           return copy;    }

数组复制拷贝尽量使用System.arrayCopy或Arrays.copyof()方法,效率更高。

ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:

public void trimToSize() {      modCount++;      int oldCapacity = elementData.length;      if (size < oldCapacity) {          elementData = Arrays.copyOf(elementData, size);      }  }

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。

4) 元素读取:

// 返回此列表中指定位置上的元素。public E get(int index) {      RangeCheck(index);      return (E) elementData[index];  }

5) 元素删除:

ArrayList提供了根据下标或者指定对象两种方式的删除功能。对于ArrayList来说,这两种删除的方法差不多,都是调用的下面一段代码:

    int numMoved = size - index - 1;    if (numMoved > 0)        System.arraycopy(elementData, index+1, elementData, index,                 numMoved);    elementData[--size] = null; // Let gc do its work

把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置,最后一个位置的元素指定为null,这样让gc可以去回收它。

ArrayList的优缺点

  • 随机访问快。ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

  • 顺序添加快。ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

  • 删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

  • 插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

ArrayList和Vector区别

  • Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。ArrayList需要使用Collections.synchronizedList方法变成一个线程安全的List。

        List<String> synchronizedList = Collections.synchronizedList(list);    synchronizedList.add("aaa");    synchronizedList.add("bbb");    for (int i = 0; i < synchronizedList.size(); i++)    {        System.out.println(synchronizedList.get(i));    }

    Vector是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样

  • 增长因子不同。ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。

  • Vector提供indexOf(obj, start)接口,ArrayList没有。

ArrayList转静态数组toArray

有两个转化为静态数组的toArray方法。

第一个,调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。

public Object[] toArray() {       return Arrays.copyOf(elementData, size);  }

第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将 elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。

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;}

Fail-Fast机制:
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。具体介绍请参考这篇文章HashMap中的Fail-Fast机制。


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表