龙空技术网

在内存只有10M的空间中申请一块5M的数组空间,会导致OOM吗?

追逐仰望星空 2375

前言:

眼前同学们对“c语言数组最大长度不够怎么办”可能比较着重,姐妹们都需要分析一些“c语言数组最大长度不够怎么办”的相关知识。那么小编在网上收集了一些有关“c语言数组最大长度不够怎么办””的相关知识,希望大家能喜欢,朋友们一起来了解一下吧!

面试三连

面试官:使用过集合吗?能说说都使用过哪些吗?

小明:当然使用过,使用比较多的就是ArrayList与HashMap,还有LinkedList、HashTable、ConcurrentHashMap等等。

面试官:用的不少啊,那来说说你对ArrayList的理解吧。

小明:ArrayList是一个基于数组实现的集合,主要特点在于随机访问速度较快,但是插入删除速度较慢。

面试官:那你知道为什么随机访问速度较快,插入删除速度较慢吗?

小明:不知道。

面试官: 现在内存还有10M内存,现在想申请一块5M大小的ArrayList空间,程序会抛出OOM吗?

小明:不会。

面试官:出去的时候记得把门带上,谢谢!

小明在面试在面试的时候被问到了ArrayList,但是他只回答到了一部分,比如刚刚的那个问题:为什么随机访问速度较快,插入删除速度较慢?小明就蒙蔽了,因为小明被面试题的时候只是记住结论,而并没有探索为什么,所以再面试的时候就gg了,这也给了我们一个警告,我们在看资料的时候一定不能只看结论,否则就只能和小明一样回家等通知了。

初识ArrayList

ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了ICollection和IList接口,灵活的设置数组的大小等好处。

也就是说,ArrayList其实就是一个数组,一般的数组长度是不允许发生改变的,但是ArrayList实现了数组的长度改变,所以叫动态数组,那你好奇他是怎么实现的动态数组吗?请随我一起剥开ArrayList的神秘面纱。

我相信很多人在开发中或多或少都会使用到ArrayList,比如接收数据库返回的列表,前端的批量保存等等,所以ArrayList在我们开发中还是比较重要的存在,所以今天我就来讲一讲它的源码解析。

ArrayList源码解析ArrayList成员变量

/**     * Default initial capacity.     */    private static final int DEFAULT_CAPACITY = 10;    /**     * Shared empty array instance used for empty instances.     */    private static final Object[] EMPTY_ELEMENTDATA = {};    /**     * Shared empty array instance used for default sized empty instances. We     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when     * first element is added.     */    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    /**     * The array buffer into which the elements of the ArrayList are stored.     * The capacity of the ArrayList is the length of this array buffer. Any     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA     * will be expanded to DEFAULT_CAPACITY when the first element is added.     */    transient Object[] elementData; // non-private to simplify nested class access    /**     * The size of the ArrayList (the number of elements it contains).     *     * @serial     */    private int size;

DEFAULT_CAPACITY:数组初始默认大小,大小等于10

EMPTY_ELEMENTDATA:使用有参构造时,但是数组大小为0或者数组为空的时候使用。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA :使用无参构造的默认数组值,也就是elementData

elementData:动态数组

size:数组大小

实例化ArrayList

ArrayList的实例化一共有三种方式

1.无参构造

public ArrayList() {        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    }

注意这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是不是我们刚刚说的,无参构造的时候elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

使用方法

ArrayList<String> list = new ArrayList<String>();System.out.println("集合:"+list);
集合:[]Process finished with exit code 0
2.有参构造

有参构造分两种情况,第一种是给定数组的初始化大小,第二种是拷贝其他集合

指定数组大小

public ArrayList(int initialCapacity) {        if (initialCapacity > 0) {            this.elementData = new Object[initialCapacity];        } else if (initialCapacity == 0) {            this.elementData = EMPTY_ELEMENTDATA;        } else {            throw new IllegalArgumentException("Illegal Capacity: "+                                               initialCapacity);        }    }

initialCapacity:初始化数组的大小,如果initialCapacity大于o,那么就会创建一个长度为initialCapacity的数组,等于0,就会将EMPTY_ELEMENTDATA赋值给elementData,否则怕抛出异常。

使用方法

//有参构造        ArrayList<String> list2 = new ArrayList<String>(50);        System.out.println("集合:" + list2);
集合:[]Process finished with exit code 0

这里就有疑问了,上面两种创建方式返回的结果都是一样的,为什么ArrayList还要给出一个指定大小的构造呢?肯定是有原因的,这个我们在后面讲。

数组拷贝

public ArrayList(Collection<? extends E> c) {        elementData = c.toArray();        if ((size = elementData.length) != 0) {            // c.toArray might (incorrectly) not return Object[] (see 6260652)            if (elementData.getClass() != Object[].class)                elementData = Arrays.copyOf(elementData, size, Object[].class);        } else {            // replace with empty array.            this.elementData = EMPTY_ELEMENTDATA;        }    }

我们来看看,首先将需要拷贝的集合转成数组,然后判断需要拷贝的数组大小是否等于0,等于0直接给一个空数组:EMPTY_ELEMENTDATA,需如果需要拷贝到饿数组大于0并且和当前的数组不是同一个对象,那么就执行拷贝,请注意这里的拷贝属于浅拷贝,为什么这么说呢?请看下面代码

List<User> list3 = new ArrayList<User>();        //初始化User对象        User user = new User();        user.setUserName("小明");        user.setSex(1);        user.setAge(18);        list3.add(user);        System.out.println("list3:"+list3);        //集合拷贝        ArrayList<User> list4 = new ArrayList<User>(list3);        System.out.println("拷贝完之后的list4:"+list4);        //集合拷贝完成之后修改User对象的值        user.setAge(20);        System.out.println("修改User对象年龄之后的lsit4:"+list4);

明白我为什么这么写吗?因为我刚刚说了这里的集合拷贝指的是浅拷贝,所以我打印了还没有背拷贝的list3、拷贝完之后的list4以及修改User对象年龄之后的lsit4,你能猜到他们对应的输出结果吗?自己可以在脑海中想象一下,然后请看下面输出结果

list3:[User(userName=小明, age=18, sex=1)]拷贝完之后的list4:[User(userName=小明, age=18, sex=1)]修改User对象年龄之后的lsit4:[User(userName=小明, age=20, sex=1)]Process finished with exit code 0

我们可以看到list3和拷贝完之后的list4是一模一样的,但是修改User对象年龄之后的lsit4却发生了改变,那就是年龄变成了20,我们并没有对list4的对象做修改,他为啥改变了呢?这就是java浅拷贝和深拷贝的知识了

ArrayList的构造函数基本上讲的差不多了,但是这里还是没有引出动态数组的概念啊,他还是一个死的,那是什么时候他会变成动态的呢?客官不要心急,我们接着往下看。

ArrayList方法add()

ArrayList一共给我们提供了两个add(),我们一起来看一下吧。

第一个:add(E e)

public boolean add(E e) {        ensureCapacityInternal(size + 1);  // Increments modCount!!        elementData[size++] = e;        return true;    }
//每次添加的时候都需要判断一下数组的长度还够不够,如果不够就需要另外处理 private void ensureCapacityInternal(int minCapacity) {        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }//数组初始化的大小与需要插入位置的大小比较,返回大的那一个public static int max(int a, int b) {        return (a >= b) ? a : b;    }//判断是否需要扩容,如果插入的位置已经大于数组的大小,那么进行扩容操作private void ensureExplicitCapacity(int minCapacity) {        modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }//扩容,将数组扩大原来的1.5倍,并且将原来的数组拷贝到新数组,再将新数组复制给原数组private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        int newCapacity = oldCapacity + (oldCapacity >> 1);        if (newCapacity - minCapacity < 0)            newCapacity = minCapacity;        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        // minCapacity is usually close to size, so this is a win:        elementData = Arrays.copyOf(elementData, newCapacity);    }

add()的源码大致就是这样的,每次添加的时候都会判断插入的位置是否大于了数组的大小,如果大于就进行扩容处理,将数组扩大原来的1.5倍( oldCapacity + (oldCapacity >> 1)),但是这里有一点需要特别注意一下,如果扩容的大小已经超过了ArrayList指定的最大数值,那会发生什么呢?

@Native public static final int   MAX_VALUE = 0x7fffffff;private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;private static int hugeCapacity(int minCapacity) {        if (minCapacity < 0) // overflow            throw new OutOfMemoryError();        return (minCapacity > MAX_ARRAY_SIZE) ?            Integer.MAX_VALUE :            MAX_ARRAY_SIZE;    }

如果扩容的大小已经超过了ArrayList指定的最大数值,他会先判断插入的位置是否已经大于了ArrayList允许的最大数值,如果大于,直接返回:MAX_VALUE,否者返回MAX_ARRAY_SIZE,这里一定要注意一个是扩容后的大小,一个是插入位置,一定不要搞错,这里就是ArrayList为什么被称为动态数组。

使用方法

//无参构造        ArrayList<String> list = new ArrayList<String>();        list.add("小明");        list.add("卖托儿索的小火柴");        System.out.println("list:" + list);
//无参构造        ArrayList<String> list = new ArrayList<String>();        list.add("小明");        list.add("卖托儿索的小火柴");        System.out.println("list:" + list);

我这里初始化的时候创建了一个无参构造,所以数组的初始大小为:10,添加两个元素的时候并不会触发扩容机制。

第二种:add(int index, E element)

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

故名思意,看参数就应该能大致的猜出这个方法是干什么的,没错,他就是插入指定位置元素,他的插入和第一个差不多,唯一的区别就是第一个是往后添加,这里是按index添加到这指定下表位置,然后将其他的元素往后移,也就是System.arraycopy(elementData, index, elementData, index + 1, size - index)。

 ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("卖托儿索的小火柴");        list.add("海阔天空");        list.add(5,"逆天而行");        System.out.println("list:" + list);

你们可以猜到执行的结果吗?执行结果就是报错,为什么呢?源码里面有这么一个方法

 private void rangeCheckForAdd(int index) {        if (index > size || index < 0)            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));    }

判断插入的位置是否大于elementData的数组长度或者是否小于0,由于我这里只添加了两个元素,所以size应该是3,我们添加的下标却是5,所以就会抛出异常

Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 5, Size: 2	at java.util.ArrayList.rangeCheckForAdd(ArrayList.java:661)	at java.util.ArrayList.add(ArrayList.java:473)	at com.ymy.list.MyArrayList.main(MyArrayList.java:18)Process finished with exit code 1

我们修改一下代码,将下表修改成1

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("卖托儿索的小火柴");        list.add("海阔天空");        list.add(1,"逆天而行");        System.out.println("list:" + list);

这个时候我们再来看运行结果

list:[小明, 逆天而行, 卖托儿索的小火柴, 海阔天空]Process finished with exit code 0

我们发现逆天而行被添加到了下标为1的位置,而卖托儿索的小火柴,和海阔天空相应的往后移了一位。

trimToSize()

之前没有说清楚size与elementData的关系,size表示的是elementData数组中已经存放了多少元素,而elementData.length表示ArrayList的初始数组大小,请不要搞混.。

public void trimToSize() {        modCount++;        if (size < elementData.length) {            elementData = (size == 0)              ? EMPTY_ELEMENTDATA              : Arrays.copyOf(elementData, size);        }    }

这个方法就是判断已经存在数组中的元素个数(size)和数组初始化的大小(elementData.length)做对比,如果小于初始化值就去掉多余的,返回一个elementData大小等于size,实现的方式就是通过拷贝的形式。

  ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("卖托儿索的小火柴");        list.trimToSize();        System.out.println("list:" + list);

为了能看到我说的,我们断点调试走一波

我们发现走到断点的那一行size=2,elementData.length= 10,下面就是判断了,很明显2<10,所以这里会执行数据拷贝,拷贝完成之后我们再看结果

清除了多余没用的元素下标,但是这个方法大家在使用的时候还是慎重比较好,如果你清除完成之后又想添加数据,这个时候ArrayList就会执行扩容操作了,这是需要进行数据拷贝的,慎重哦。

ensureCapacity(int minCapacity)

源码如下

public void ensureCapacity(int minCapacity) {        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)            // any size if not default element table            ? 0            // larger than default for default empty table. It's already            // supposed to be at default size.            : DEFAULT_CAPACITY;        if (minCapacity > minExpand) {            ensureExplicitCapacity(minCapacity);        }    }

大致意思:当你初始化了一个大小为10的初始数组之后,并添加了5条数据,这个时候你发现10可能不够,要是数组大小在大一点就好了,ensureCapacity就是解决这个问题的,它会扩大你指定的大小,但是扩大之后数组的大小是不是你指定的大小这个是不确定的,因为ensureExplicitCapacity(minCapacity);的源码在上面也看到了,他会现在原来的数组大小的基础上扩大1.5倍,然后在和你传入的数值做对比,如果大于你传入的,那么使用旧数组(elementData)大小的1.5倍作为新数组的大小,如果小于你传入的数值,这个时候就会以你传入的大小作为数组(elementData)的大小,这点一定要搞清楚哦,不然的话,你会发现你明明设置了值,但是最后数组的大小却和你设置的不一样,就会感觉是你的代码写的有问题。

我们先来看一个扩容小于原数组大小1.5倍的数值:12

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("卖托儿索的小火柴");        list.ensureCapacity(12);        System.out.println("list:" + list);

我们发现elementData的大小并不是我们传入的12,而是15,要注意哦

我们再来看看扩容大于原始数组大小1.5倍的数值:20

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("卖托儿索的小火柴");        list.ensureCapacity(20);        System.out.println("list:" + list);

在这里我在贴出一下导致这两种原因的代码在哪里

size()

返回当前ArrayList已经添加了多少条元素,这个不用多说,相信大家都知道。

isEmpty()

public boolean isEmpty() {        return size == 0;    }

判断ArrayList是否添加了数据,但是这点需要注意一下,这里只能判断是否存在元素,不能判断ArrayList是否为空,这点需要注意,如果使用这个方法判断空的话就报错哦。

contains(Object o)

public boolean contains(Object o) {        return indexOf(o) >= 0;    }

判断ArrayList所有元素中是否存在当前元素,如果是对象,判断的就是引用地址了,这里需要注意,如果我们的ArrayList的泛型是对象,那么最好重写一下equals和hashcode方法,举个例子

没有重写equals()与 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);        //用户插入集合的数据        User user1 = new User();        user1.setUserName("小明");        user1.setSex(1);        user1.setAge(18);        //用于对比的数据        User user2 = new User();        user2.setUserName("小明");        user2.setSex(1);        user2.setAge(18);        list.add(user1);        System.out.println("是否包含user1:" + list.contains(user1));        System.out.println("是否包含user2:" + list.contains(user2));
是否包含user1:true是否包含user2:falseProcess finished with exit code 0

看到输出结果了吧,判断是否包含user1结果为:true;判断是否包含user2的结果为:false,那是因为往list中添加的是user1,所以比较user1的时候他们都是同一个引用地址,所以返回true,而user2是新new出来的,他们是两个完全不相同的对象,内存地址肯定也不相同,所以这个时候肯定就返回false,因为user1和user2里面存放的数据都是一样的,有时候我们只需要判断内容是否相等,并不需要判断内存地址是否相等的时候需要怎么做呢?

重写equals()与 hashCode()改造一下我们的User对象

package com.ymy.entity;import lombok.Getter;import lombok.Setter;import lombok.ToString;import java.util.Objects;@Getter@Setter@ToStringpublic class User {    private String userName;    private Integer age;    private Integer sex;    @Override    public boolean equals(Object o) {        if (this == o) return true;        if (o == null || getClass() != o.getClass()) return false;        User user = (User) o;        return Objects.equals(userName, user.userName) &&                Objects.equals(age, user.age) &&                Objects.equals(sex, user.sex);    }    @Override    public int hashCode() {        return Objects.hash(userName, age, sex);    }}

测试代码还是不变,我们在运行查看结果

是否包含user1:true是否包含user2:trueProcess finished with exit code 0

总结就是一句话,ArrayList引用对象的时候如果没有重写equals()与 hashCode()对比的就是内存地址,如果重写了equals()与 hashCode(),对比的就是实实在在的数据。请拿小本本记好,这个要考。

indexOf(Object o)

查找元素所在的下标,如果查找的是对象,默认比较的是内存地址这点和contains(Object o)一样。

源码如下

public int indexOf(Object o) {        if (o == null) {            for (int i = 0; i < size; i++)                if (elementData[i]==null)                    return i;        } else {            for (int i = 0; i < size; i++)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }

如果查找的内容为空,那么这个就会返回第一个元素为空的下标,否者返回数组中第一次出现查找元素的下标。

没有重写equals()与 hashCode()

 ArrayList<User> list = new ArrayList<User>(10);        //用户插入集合的数据        User user1 = new User();        user1.setUserName("小明");        user1.setSex(1);        user1.setAge(18);        //用于对比的数据        User user2 = new User();        user2.setUserName("小明");        user2.setSex(1);        user2.setAge(18);        list.add(user1);        System.out.println("是否包含user1:" + list.indexOf(user1));        System.out.println("是否包含user2:" + list.indexOf(user2));
是否包含user1:0是否包含user2:-1Process finished with exit code 0

很明显查找user1的时候是同一个内存地址,所以返回了对应的下标,而user2与user1不是同一个内存地址,所以返回了-1。

重写equals()与 hashCode()

重写的方法和contains()一样,我们直接看结果即可

是否包含user1:0是否包含user2:0Process finished with exit code 0

所以一定要区分你需要查找的是值相同还是地址相同,不然就会导致bug哦。

lastIndexOf(Object o)

与indexOf()效果一样,都是查找元素所在的下标,但是又有一点区别,那就是lastIndexOf()返回的是最后一次出现的下标位置。

public int lastIndexOf(Object o) {        if (o == null) {            for (int i = size-1; i >= 0; i--)                if (elementData[i]==null)                    return i;        } else {            for (int i = size-1; i >= 0; i--)                if (o.equals(elementData[i]))                    return i;        }        return -1;    }

这个使用和indexOf一样,这里就不做demo展示了。

clone()

克隆一个ArrayList

源码如下

public Object clone() {        try {            ArrayList<?> v = (ArrayList<?>) super.clone();            v.elementData = Arrays.copyOf(elementData, size);            v.modCount = 0;            return v;        } catch (CloneNotSupportedException e) {            // this shouldn't happen, since we are Cloneable            throw new InternalError(e);        }    }

使用方式

 ArrayList<User> list = new ArrayList<User>(10);        //用户插入集合的数据        User user1 = new User();        user1.setUserName("小明");        user1.setSex(1);        user1.setAge(18);        list.add(user1);        System.out.println("list1:"+list);        ArrayList<User> list2 = (ArrayList<User>) list.clone();        System.out.println("list2:"+list2);

运行结果

list1:[User(userName=小明, age=18, sex=1)]list2:[User(userName=小明, age=18, sex=1)]Process finished with exit code 0

将list拷贝到list2,但是这里需要注意一点,这里的拷贝属于浅拷贝,list2和list1共享一个User对象,这是需要特别注意的。

toArray()

将ArrayList转换成数组

源码

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

使用方式

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list1:" + list);        Object[] array = list.toArray();        System.out.println("array:" + array);        array[2] = "海阔天空";

运行结果

list1:[小明, 逆天而行]array:[Ljava.lang.Object;@3a71f4ddException in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2	at com.ymy.list.MyArrayList.main(MyArrayList.java:17)Process finished with exit code 1

ArrayList转数组没有问题,但是在数组赋值的时候却报错了,这一点需要注意,这里的数组长度就是ArrayList的数组实际长度,ArrayList的长度是2,下标最大为1,但是我们赋值的时候给的下标是2,所以就会抛出数组越界的错误。

get(int index)

根据下表获取元素信息

源码

public E get(int index) {        rangeCheck(index);        return elementData(index);    }

第一步校验下标是否越界,然后返回对应下标元素信息。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list1:" + list);        String name = list.get(1);        System.out.println("name:"+name);

运行结果

Connected to the target VM, address: '127.0.0.1:62855', transport: 'socket'list1:[小明, 逆天而行]name:逆天而行Disconnected from the target VM, address: '127.0.0.1:62855', transport: 'socket'Process finished with exit code 0

这里面的下标一定不能大于elementData的size,否者就会抛出数组越界。

set(int index, E element)

在指定下标添加元素

源码

public E set(int index, E element) {        rangeCheck(index);        E oldValue = elementData(index);        elementData[index] = element;        return oldValue;    }

添加的下标不能越界,它会将你的元素添加到数组的指定下标,并且返回被替换的元素,这里是替换哦,被替换的元素不会往后移,这点需要特别注意。

使用方法

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list:" + list);        String name = list.set(1,"海阔天空");        System.out.println("name:"+name);        System.out.println("修改后的list:"+list);

结果

list:[小明, 逆天而行]name:逆天而行修改后的list:[小明, 海阔天空]Process finished with exit code 0
remove(int index)

删除指定下标的元素,并返回被删除的元素值

源码

public E remove(int index) {        rangeCheck(index);        modCount++;        E oldValue = elementData(index);        int numMoved = size - index - 1;        if (numMoved > 0)            System.arraycopy(elementData, index+1, elementData, index,                             numMoved);        elementData[--size] = null; // clear to let GC do its work        return oldValue;    }

他在删除了指定下标之后,那这个下标的位置就会处于空缺,这个时候ArrayList做了一件事,那就是将数组进行重新排序,实现的方式就是数据拷贝,使用一个新的数组接受两段数据,一段是删除下标之前的数据,一段是删除下表之后的数据,整合到一个新的数组,然后赋值到原数组中。这里只需要了解一下即可,最后返回了被删除的元素值。

使用方法

ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list:" + list);        String name = list.remove(1);        System.out.println("name:"+name);        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]name:逆天而行删除后的list:[小明]Process finished with exit code 0
remove(Object o)

通过元素值删除数组中存在的元素,这种删除比较耗时间,为什么这么说呢?请看源码

 public boolean remove(Object o) {        if (o == null) {            for (int index = 0; index < size; index++)                if (elementData[index] == null) {                    fastRemove(index);                    return true;                }        } else {            for (int index = 0; index < size; index++)                if (o.equals(elementData[index])) {                    fastRemove(index);                    return true;                }        }        return false;    }private void fastRemove(int index) {        modCount++;        int numMoved = size - index - 1;        if (numMoved > 0)            System.arraycopy(elementData, index+1, elementData, index,                             numMoved);        elementData[--size] = null; // clear to let GC do its work    }

首先,它会判断删除的元素是否为空,如果是空,那么它将删除数组中第一个空元素,然后直接返回,如果你要删除的元素不为空,那这个时候就会循环数组,找到你要删除的第一个元素进行删除,但是删除的时候有需要做数据拷贝,如果不做的话,数组下标就会错乱,最后返回删除结果。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list:" + list);        boolean remove = list.remove("逆天而行");        System.out.println("是否删除成功:"+remove);        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]是否删除成功:true删除后的list:[小明]Process finished with exit code 0
clear()

这个方法比较简单,就是将数组中所有的元素都设置为null,然后将size设置为0。

源码

public void clear() {        modCount++;        // clear to let GC do its work        for (int i = 0; i < size; i++)            elementData[i] = null;        size = 0;    }

使用方法

 ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list:" + list);        list.clear();        System.out.println("删除后的list:"+list);

运行结果

list:[小明, 逆天而行]删除后的list:[]Process finished with exit code 0
addAll(Collection<? extends E> c)

将其他的集合添加到当前集合,这里添加方式是通过拷贝实现的。

源码如下

 public boolean addAll(Collection<? extends E> c) {        Object[] a = c.toArray();        int numNew = a.length;        ensureCapacityInternal(size + numNew);  // Increments modCount        System.arraycopy(a, 0, elementData, size, numNew);        size += numNew;        return numNew != 0;    }

ensureCapacityInternal(size + numNew);这行代码是不是经常看到,不用我多说想必大家也知道了,没错,就是判断当前的集合是否可以装下这些数据,是否需要扩容,接下来就是数据添加了,添加的方式就是通过数据拷贝,这里的拷贝同样属于浅拷贝。

使用方法

 ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        System.out.println("list:" + list);        ArrayList<String> list2 = new ArrayList<String>();        list2.add("随风起舞");        lis2t.add("穷凶极恶");        list.addAll(list2);        System.out.println("添加之后的lsit:"+list);

运行结果

list:[小明, 逆天而行]添加之后的lsit:[小明, 逆天而行, 随风起舞, 穷凶极恶]Process finished with exit code 0
addAll(int index, Collection<? extends E> c)

这个方法其实和上面那个也是大同小异,就是添加集合,但是这里的添加方式有点区别,这里可以指定下标。

源码如下

 public boolean addAll(int index, Collection<? extends E> c) {        rangeCheckForAdd(index);        Object[] a = c.toArray();        int numNew = a.length;        ensureCapacityInternal(size + numNew);  // Increments modCount        int numMoved = size - index;        if (numMoved > 0)            System.arraycopy(elementData, index, elementData, index + numNew,                             numMoved);        System.arraycopy(a, 0, elementData, index, numNew);        size += numNew;        return numNew != 0;    }

它会往你指定的下标添加集合元素,原本属于当前下标的元素向后移动,移动方式也是通过数据拷贝事项的。

使用方法

  ArrayList<String> list = new ArrayList<String>(10);        list.add("小明");        list.add("逆天而行");        list.add("铁血无双");        System.out.println("list:" + list);        ArrayList<String> list2 = new ArrayList<String>();        list2.add("随风起舞");        list2.add("穷凶极恶");        list.addAll(1,list2);        System.out.println("添加之后的lsit:"+list);

运行结果

list:[小明, 逆天而行, 铁血无双]添加之后的lsit:[小明, 随风起舞, 穷凶极恶, 逆天而行, 铁血无双]Process finished with exit code 0

我们插入的下标位置为1,这个时候ArrayList就将list2这两个元素从下标1开始往后田间,冲突的元素就往后移,直到没有冲突为止。

这里就暂时先说这么多吧,这些都是一些比较常用的方法,看了肯定会对你有所帮助。

开篇解答

在文章开头的时候我们说到小明面试的时候被问到在一块内存只有10M的空间中申请一块5M的数组空间,会导致OOM吗?

这个答案是:不确定,为什么这么说呢?原因很简单,那是因为数组在内存中存放的地址都是连续的,比如:00xx01、00xx02、00xx03 … 00xxnn,虽然说内存还有10M,但是不能保证连续的内存空间还剩5M,如果连续空间不足5M,那么在创建ArrayList的时候就会抛出OOM,这个时候你就会疑问了,既然数组要求内存地址是连续的,那是什么导致内存地址不连续呢?这个就涉及到链表了,链表存储的数据在内存中的地址是随机的,关于链表这个就不展开了,否者又得讲半天,所以只需要记住:数组申请内存空间的时候要求内存地址是连续的,如果连续的内存地址空间不足,那么在创建数组的时候就会抛出OOM。

为什么ArrayList的随机访问速度较快,删除,新增速度较慢呢?

答:ArayList是随机访问速度较快,并不是访问速度较快,这一点一定要搞清楚,为什么呢?很简单,刚才再源码分析的时候可以看出,通过下标获取元素时间复杂度为:O(1),但是通过元素查找元素的话,时间复杂度就上升到:O(n)了,原因是因为需要循环比较,不像下标可以直接获取,新增速度较慢是因为每次新增的时候都会需要判断数组的大小是否还足够添加元素,是否需要扩容,扩容的时候就需要数据拷贝,删除分两种情况,通过下标删除:删除较快,但是删除之后数组就不连续了,这个时候就需要对数组做处理,处理的方式就是数据拷贝,拷贝到一个新的数组,然后再放回到原数组,通过元素名删除和通过下标删除类似,但是多了一个循环,对比元素名是否相同,所以删除、新增相对来说较慢。

总结

虽然我们在日常开发中经常使用ArrayList,但是我们对它的原理熟悉吗?如果不熟悉就因为一个细节就会让你的程序变慢或者内存溢出。

自动扩容:如果我们创建ArrayList的时候知道了大概的长度的时候尽量指明数组长度,否者在数据添加的时候就会频繁触发扩容,然而扩容就会导致数据拷贝,虽然数据拷贝属于浅拷贝,但是频繁的数据拷贝同样会消耗我们的性能,所以在实例化的时候最好给出数组初始长度,避免频繁扩容。

手动扩容(ensureCapacity):手动扩容的时候需要注意一点,手动扩容的最终数组大小有可能不是你指定的大小,他有一个校验规则,第一,将元素组长度扩大1.5倍,然后在和你传入的扩容数值做对比,谁大用谁。

删除、修改(元素删除):这两个相对来说比较耗时,为什么这么说呢?原因就是删除的时候会循环整个数组,最好情况第一次就找到了你要操作的数据,但是最坏情况是你循环了一遍数组才找到你要操作的元素,所以删除、修改的时间复杂度为:O(n),并且操作完成之后还伴随这一次数据拷贝,所以删除的时候能用下标就用下标,是在找不到下标在使用元素删除。

查询:随机访问的速度较快,那是因为根据下标能很快的找到对应的元素,时间复杂度为:O(1)。

线程安全性问题:ArrayList不是线程安全的,这点想必大家都知道,这里就不再啰嗦了。

总的来说就是尽量指定数组长度,避免频繁扩容,少使用元素删除,所以在选型的时候一定要注意使用,虽然ArrayList简单,但是使用不当,也会给项目造成很大损失。

作者:卖托儿索的小火柴

原文链接:

标签: #c语言数组最大长度不够怎么办