• ArrayList,vector,LinkedList


    集合类中线程安全的就只有,vector,hashtable,concurrentHashmap

    ArrayList:

    ArrayList 实现于 ListRandomAccess 接口。可以插入空数据,也支持随机访问。

    ArrayList的初始容量为10,这里我们主要了解一下ArrayList的扩容机制

    对于一个普通的数组,我们一个一个添加的时候,size就会每加一个就扩容一次

    添加到末尾

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // 在size=10的时候就会开始扩充了,第一次扩充到15,就不用每次加一这样扩容了
            //这里看到ArrayList添加元素的实质就相当于为数组赋值
            elementData[size++] = e;
            return true;
    }

     在任意位置插入

    public void add(int index, E element) {
            rangeCheckForAdd(index);
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //复制,向后移动
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }

    扩容校验:

    1.得到最小扩容量

     private void ensureCapacityInternal(int minCapacity) {
          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  // 获取默认的容量和传入参数的较大值
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
          }
    
          ensureExplicitCapacity(minCapacity);
     }

    2.判断是否需要扩容

    private void ensureExplicitCapacity(int minCapacity) {
            modCount++;//版本号增加
            if (minCapacity - elementData.length > 0)
                //调用grow方法进行扩容,调用此方法代表已经开始扩容了
                grow(minCapacity);
    }
        
    private void grow(int minCapacity) { // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1); //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity;//没有超过的话就按定义的最大容量 //再检查新容量是否超出了ArrayList所定义的最大容量, //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }

    对于扩容的总结:

    每次都会扩容到1.5倍,不会每次添加都会扩容了,效率会增加很多,然后在大量的数据的时候多用ensureCapacity这个方法,这个方法是用来给用户用的,防止反复的扩容可以增加效率。

    ArrayList序列化问题:

    由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient 修饰,可以防止被自动序列化。

    transient Object[] elementData;

    elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用。

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
        // 防止序列化期间有修改
        int expectedModCount = modCount;
        // 写出非transient非static属性(会写出size属性)
        s.defaultWriteObject();
    
        // 写出元素个数
        s.writeInt(size);
    
        // 依次写出元素
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        // 如果有修改,抛出异常
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        // 声明为空数组
        elementData = EMPTY_ELEMENTDATA;
    
        // 读入非transient非static属性(会读取size属性)
        s.defaultReadObject();
    
        // 读入元素个数,没什么用,只是因为写出的时候写了size属性,读的时候也要按顺序来读
        s.readInt();
    
        if (size > 0) {
            // 计算容量
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            // 检查是否需要扩容
            ensureCapacityInternal(size);
    
            Object[] a = elementData;
            // 依次读取元素到数组中
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

    关于ArrayList的克隆的问题:

    其中的源码:

        public Object clone() {
            try {
                ArrayList<?> v = (ArrayList<?>) super.clone();
                //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
                v.elementData = Arrays.copyOf(elementData, size);
                v.modCount = 0;
                return v;
            } catch (CloneNotSupportedException e) {
                // 这不应该发生,因为我们是可以克隆的
                throw new InternalError(e);
            }
        }

    用一个例子理解ArrayList的复制:

    public static void main(String[] args) {
            ArrayList<Person> a=new ArrayList<>();
            Person b=new Person("lijun",21);
            a.add(b);
            ArrayList<Person> c=(ArrayList<Person>) a.clone();
            System.out.println(a==c);
            System.out.println(a.toArray(new Person[0])==c.toArray(new Person[0]));
            System.out.println(a.get(0)==c.get(0));
        }

    false

    false

    true

    其中基本上可以算是深复制了,已经重新生成了ArrayLIst了,但是ArrayList里面的数组是深复制,因为copyOf重新生成了一个新的数组,但是数组指向的仍然是原来数组里面的对象。

    public static void main(String[] args) {
            ArrayList<Person> a=new ArrayList<>();
            Person b=new Person("lijun",21);
            ArrayList<Person> c=(ArrayList<Person>) a.clone();
            c.get(0).setName("yang");
            System.out.println(a.get(0));    
        }

    输出:yang,所以进一步验证了我上面的结论。

    ArrayList里面有几个求并交差的函数:

            // 并集
            // list1.addAll(list2);
            // 交集
            //list1.retainAll(list2);
            // 差集
            // list1.removeAll(list2);

    batchremove源码:

    https://blog.csdn.net/weixin_40841731/article/details/85263889

    关于ArrayList的几个点:

    1.ArrayList中,默认的大小是10,当你使用new ArrayList();时并不会立即为数组分配大小为10的空间,而是等插入第一个元素时才会真正分配,这样做是为了节约内存空间。

    2.维护内部数组时,使用的是Arrays.copyOf()方法和System.arraycopy()方法。

    3.在扩容时,默认的扩容因子是1.5,每次需要扩容时,会将原数组大小的1.5倍和实际需要的数组空间进行比较,从中取最大值作为数组大小。然后新建一个数组,把原数组中的所有元素复制到新数组中去。所以扩容其实是最耗费时间的操作,不仅仅需要重新分配空间,而且需要重新赋值。

    4.多用ensureCapacity,防止频繁扩容。

    5.modCount标识,使用迭代器的不要尝试修改,会报错。

    vector:

    基本上就上在ArrayList的所有方法上加了Synchronized关键字

    LinkedList:

    LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; 

    LinkedList主要的优点还是插入删除很方便(仅仅就是移动指针)。

    另外可以很容易的实现队列,栈这些数据结构。

    对于查询,有利用到双向链表的特性:

    public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
    
    Node<E> node(int index) {
            // assert isElementIndex(index);
    
            if (index < (size >> 1)) {
                Node<E> x = first;
                for (int i = 0; i < index; i++)
                    x = x.next;
                return x;
            } else {
                Node<E> x = last;
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                return x;
            }
    }

    上述代码,利用了双向链表的特性,如果index离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。

    node()会以O(n/2)的性能去获取一个结点:如果索引值大于链表大小的一半,那么将从尾结点开始遍历

  • 相关阅读:
    Linux下安装软件遇见的问题汇总
    使用AlarmManager定期执行工作
    android打开文件、保存对话框、创建新文件夹对话框(转载)
    一些算法的整理
    安卓adb调试命令常见的问题
    java下的串口通信-RXTX
    Unity 协程运行时的监控和优化
    Unity3D 协程的介绍和使用
    游戏服务器:到底使用UDP还是TCP
    Unity 可重复随机数
  • 原文地址:https://www.cnblogs.com/peterleee/p/10530615.html
Copyright © 2020-2023  润新知