看完这个文章就可以对 ArrayList 有自己的见解了

本文详细介绍了ArrayList的实现原理,包括其构造方法、add操作的扩容机制、remove方法的实现,以及ArrayList与其他数据结构如LinkedList的区别。分析了ArrayList的最大容量限制及其与JVM的关系,探讨了其在不同操作下的性能表现和线程安全性。
摘要由CSDN通过智能技术生成

ArrayList 也是我们在写代码的过程中很常使用的一种集合类。让大家从使用易操作的数组变成使用 ArrayList 的缘由我想应该是 add 方法,能够“无忧无虑”的向 ArrayList 里存放数据,还能像数组一样用“下标”和 get 方法获得数据。那么这篇文章就对 ArrayList 这个集合类做一次简单的理解吧。

一、定义

先来看看这个类的作者 Josh Bloch 和 Neal Gafter 是怎么描述他们的作品的:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)
The size, isEmpty, get, set, iterator, and listIterator operations run in constant time. The add operation runs in amortized constant time, that is, adding n elements requires O(n) time. All of the other operations run in linear time (roughly speaking). The constant factor is low compared to that for the LinkedList implementation.
Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList,its capacity grows automatically. The details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost.

很好,又是英文。我们从中提取一下大概的内容:

  • 它是可调整大小的数组的实现。
  • 它的部分操作消耗的时间都是固定的,add操作的时间复杂度是 O(N),其他的操作都是线性的时间消耗。
  • ArrayList 的容量和列表一样大,而且容量会自动增长。

我们再去看他的签名

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

继承的 AbstractList,实现了 List, RandomAccess, Cloneable, Serializable 接口,说明它是 支持快速随机访问,能被clone,支持序列化操作 的。

二、成员设计

在 ArrayList 里有这么 7 个变量存在:

private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储实际数据的数组
transient Object[] elementData;
private int size;
// ArrayList 存在最大容量限制
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

从上我们可知, ArrayList 是基于数组实现的。比较重要的成员就是 DEFAULT_CAPACITYelementData 两个。成员变量没有什么能单独拎出来解释的,那么就直接来看看方法吧。

tips:EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 长的很像啊,为什么要区分开呢

三、方法解读

我们在这里不对所有的方法进行解析,只理解使用频率极高的几个方法的实现方式


1. 构造方法

ArrayList 为我们提供了 3 种构造方法。

第一种就是我们经常使用的无参构造:

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

构造器会默认帮我们把 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这个数组的内容是空的,看上去没有任何问题。


第二种是自定义初始容量的有参构造:

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

一般来说我们设计这种自定义初始容量的时候,都只会区分是否小于0,比如说 HashMap 的自定义初始容量方法里面是这么一句:

if (initialCapacity < 0)

而 ArrayList 还特地强调了 initialCapacity 值为0的时候的特殊处理,这一点在扩容的方面会有所体现。


第三种是常见的传入 Collection 的构造方法

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

2. add

add 方法有两个重载实现,先来看最简单的实现方式

2.1 add(E e)

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

第一步是扩容,然后把传入的 e 放到数组的末尾,之后对 size 进行 ++ 操作。返回值是 boolean


再去看扩容所使用的方法:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

它会去调用 calculateCapacity 方法,并传入当前的数组和尝试扩容的大小:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

在这里,两个均为空数组的成员变量的作用就体现了:

无参构造会“产生”一个空的数组,有参但是传入初始容量为0的时候也会“产生”一个空的数组。但这两个空的数组的扩容方式是不一样的:无参构造出的数组的扩容方式在执行扩容的操作时,最小会直接扩容到 DEFAULT_CAPACITY 的大小也就是10的长度。而如果我们人为的传入初始容量0,那么在执行扩容操作时就会逐个元素扩容

比如说:

// 利用无参构造和 设置初始容量为0 两种方式创建两个 ArrayList
ArrayList<String> arrayList1 = new ArrayList<>();
ArrayList<String> arrayList2 = new ArrayList<>(0);

// 分别进行add操作
arrayList1.add("str");
arrayList2.add("str");

那么这两个 add 在执行到这段代码时会产生差异:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

其中,arrayList1 因为是无参构造,elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,所以会走if内的return 此时 minCapacity 是 1,而 DEFAULT_CAPACITY 是10, 取 max 之后就是10,也就是说 elementData.length = 10注意,不是 arrayList1.size() =10

arrayList2 是有参并传入初始容量为0,那么就不会执行if内的return,而是去执行下面的 return minCapacity,也就是直接返回 1 。

到这一步只是计算量扩容后的容量大小,下面才是真正的扩容

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

首先是修改 modCount ,记录结构被修改的次数。

然后判断扩容的目标大小和当前大小,确定是否进行扩容操作,如果需要则调用 grow 方法

那么接下来肯定就是贴 grow 的源码了,同时要把成员里的一个数值拿过来

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

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

执行流程:

  1. 记录当前数组长度
  2. 将记录下的值 * 1.5 当作扩容后的长度值 (old>>1相当于 old/2,然后再加上自身,相当于 old + old/2 = old×1.5
  3. 将计算的长度值与传入的 minCapacity 作比较,取更大的值当作最终长度值。
  4. 比较最终长度值和 MAX_ARRAY_SIZE 比较,如果超过 MAX_ARRAY_SIZE ,则进行 hugeCapacity 操作
  5. 最终使用 copyOf 方法新建一个数组,然后把原 elementData 的数据复制进去,并把复制后的数组当作 elementData。扩容完成

在这里产生了2个新的问题

1. 什么是 hugeCapacity()

其实就是一段很简单的、含有参数的合法性检验的三元表达式函数。这个方法仅有 grow 函数调用。

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}
2. MAX_ARRAY_SIZE的作用

我们不难发现,在一开始的成员设计里有这么一句话:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

它似乎规定了 ArrayList 的最大容量是 int 的最大值 - 8

但是在 hugeCapacity 里面,当传入值超过 MAX_ARRAY_SIZE 的时候,它就直接返回 Integer.MAX_VALUE。这是什么操作呢?为什么不一开始就让最大容量是int的最大值呢?何必多此一举

尝试在这个模块写有关内容,但是在了解有关内容之后发现并非一句两句就能解释清楚,所以请直接跳转到目录 【其他】 来了解这部分内容


第二步是对 elementData 数组进行赋值操作,值得注意的是 size++ 这个操作。与上文内容相同,size表达的含义是 ArrayList 的元素个数,实际的容量大小应该是 elementData.length

elementData[size++] = e;

第三步是返回 true

2.2 add(int idx,E e)

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 是否合理:

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

第二步与 add(E e) 一样,都是扩容

第三步:调用 arraycopy 方法,把存放在 elementData 中,下标在 Index 之后的所有数据,移动到 Index + 1 开始的位置。

第四步:把 index 位置的数据改成传入的 element

第五步:size++

第六步:返回 true

3. remove

remove 其实有 3 个方法实现,一个是基于下标码的删除,一个是基于对象的删除,一个是 fastRemove

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

首先进行 index 的合法性检验。然后操作 modCount ,记录结构被修改的次数。在之后获取 index 对应的值:

E elementData(int index) {
    return (E) elementData[index];
}

随后判断 index 下标的后面是否还存在值,如果存在值则把他们向前移动一位。

之后让包含元素个数 size-- ,并把数组最后一个位置的值清空,目的是让 GC 释放掉有关内容。

随后返回原 index 下标对应的值

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

如果传入的 Object 是 null,那么就使用 == 来匹配对应的下标,否则用 equals 来匹配下标。拿到下标之后使用 fastRemove 进行元素删除。

fastRemove仅对 remove(Object o) 使用,本身是 private 的

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
}

内容与 remove(int index) 的内容一致,不再赘述。

4. get

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

这两个函数的功能已经介绍过了,而且执行流程也十分易懂。

5. set

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

也是十分易懂的内容

6. 手动扩容

除了在调用 add 方法时,系统自动实现的扩容以外,我们可以人为的对 elementData 的大小进行扩容。

public static void main(String[] args) {
    ArrayList<String> arrayList1 = new ArrayList<>();
    arrayList1.add("str");
    arrayList1.ensureCapacity(50);
}
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);
    }
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这个方法其实和 add 的自动扩容的流程差不多,大家简单一看就可以了。

7. 其他方法

其他的方法就不做一一分析,针对常用方法抽出来给大家一句话概括一下,有兴趣可以自行去理解源码

// 直接返回值
public int size() {
        return size;
    }
// 简单判断
public boolean isEmpty() {
        return size == 0;
    }
// 查询第一个出现的下标码 没出现是-1
public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
// 其实就是数组的遍历
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;
}
// 其实也是数组的遍历
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;
    }
// 遍历置空
public void clear() {
    modCount++;

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

    size = 0;
}

四、常见问题

1. ArrayList与LinkedList的区别

  1. 底层数据结构不一样,一个是基于数组,一个是基于链表。
  2. ArrayList 查找和访问元素的速度快,但是新增删除慢,LinkedList则与之相反。原因也是数据结构的问题(数组和链表的问题)
  3. ArrayList 需要一块连续的内存空间,LinkedList 则不需要。原因还是数组和链表的问题
  4. ArrayList 支持快速随机访问(利用序号迅速获得值),LinkedList 不支持
  5. ArrayList 的结尾会预留一定的容量空间,这会产生额外的内存占用。LinkedList 则是每个节点都存放了前驱指针、后继指针和数据,本身就需要消耗大量内存空间
  6. 他们两个都不是线程安全的

2. 一直向ArrayList添加元素会怎样

ArrayList 是基于数组实现的,而数组总是定长的。

在一般情况下,我们不断地向 ArrayList 添加元素,只会让 ArrayList 不断扩容。所谓扩容其实就是创建一个容量是现在容量的1.5倍大小的数组,然后把数组里的数据复制过去。不断的扩容意味着在数据量很大的时候,会在扩容的过程中消耗大量的资源,效率会降低。

ArrayList 是存在最大容量的,如果不断地添加元素,最后会再尝试扩容时超出int上限,抛出异常 OutOfMemoryError

3. ArrayList的默认构造函数会不会初始化数组的容量

不会 只有在第一次add的时候会初始化容量为 10

4. ArrayList(int initialCapacity)的带参构造函数会不会初始化数组的容量

在传入值不为0的时候会直接开辟数组空间

if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];

为0的时候则不会,小于0的时候会抛出异常

5. 什么时候扩容1.5倍后,仍然不能满足容量要求

使用 addAll 方法的时候可能会发生上述情况。使用 addAll 方法时,扩容会按照正常流程先扩大 1.5倍,然后判断是否达到了传入值的要求,如果正常扩大1.5倍后的容量仍然小于所需容量,则直接扩大到传入的容量值(也就是原数组长度+addAll里的数组长度之和)

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 考察的就是这个 if 分支 ————————————————
    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);
}

6. 如何实现指定index的add方法

先判断index时候合法,然后判断容量决定扩容,之后把index开始往后的数据复制到index+1开始的位置,最后把index对应的值更改成传入的新值。这种操作自然是比较慢的操作(指复制的过程)

7. 如何删除一个元素

首先获取元素的下标(可以是传入的,也可以是遍历得来的),然后把这个下标index后面所有数据复制到index开始的位置,最后把数组的最后一个位置设为null

8. JDK 1.7和1.8 有区别吗

有。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是 1.8 版本才有的,1.7 只要传入了合法的初始容量参数(大于等于0),就会开辟出对应的内存空间。而 1.8 只有在执行 add 方法的时候才会执行类似操作。

9. ArrayList 是线程安全的吗

不是。Vector是线程安全的

10. 不安全为什么要使用

因为正常的使用环境下更多的操作是查询,不会有大量的增加修改删除。如果有频繁的增删可以使用基于链表实现的 LinkedList。

11. 怎么让它线程安全

  1. 操作方法用 synchronized 修饰 —— SynchronizedList、Vector的实现原理
  2. 修改时复制出一个新的数组,修改的操作在这个数组里实现,最后让原有数组指向这个数组 —— CopyOnWriteArrayList的实现原理

tips:SynchronizedList的所有方法都带有同步锁,性能并不好。CopyOnWriteArrayList则是性能最优的方案。

12. 谈谈CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全且读操作无锁的ArrayList。在写操作时会复制一份新的List,在新的List上完成写操作,然后再将原引用指向新的List。CopyOnWriteArrayList允许线程并发访问读操作,这个时候是没有加锁限制的,性能较高。而写操作的时候,则首先将容器复制一份,然后在新的副本上执行写操作,这个时候写操作是上锁的。结束之后再将原容器的引用指向新容器。注意,在上锁执行写操作的过程中,如果有需要读操作,会作用在原容器上。因此上锁的写操作不会影响到并发访问的读操作。

优点:读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。在遍历传统的List时,若中途有别的线程对其进行修改,则会抛出 ConcurrentModificationException 异常。而 CopyOnWriteArrayList 由于其"读写分离"的思想,遍历和修改操作分别作用在不同的List容器,所以在使用迭代器进行遍历时候,也就不会抛出 ConcurrentModificationException 异常了。

缺点:一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC。二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据

13. ArrayList用来做队列合适么

不合适,队列一般是 FIFO 。尽管我们可以通过一些方式让 ArrayList 实现在头部删除数据,在尾部增加数据。但是 ArrayList 与数据有关的操作均涉及到数组赋值的问题,效率是十分低下的。

14. 数组可以用来做队列吗

可以。ArrayBlockingQueue 的内部实现就是一个环形队列,是一个基于定长数组实现的定长队列。

15. ArrayList的遍历和LinkedList遍历性能比较如何

ArrayList 的性能更胜一筹,依然是数据结构的问题,ArrayList 使用的是内存连续的数组,而 LinkedList 使用的是链表。

五、其他

我们在这个模块讨论一下 MAX_ARRAY_SIZE 这个东西

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

这两段代码实实在在的令我产生了困扰,在了解了有关资料之后才发现 —— Java 别有洞天

在讲解具体原因之前,我们先以学习者的角度去暴力查看不合法的操作会引来什么后果,下面我们编写这样的代码:

ArrayList<String> arrayList = new ArrayList<>(Integer.MAX_VALUE - 1);

我们尝试直接有参构造并传入初始容量为 Integer.MAX_VALUE - 1,会发生什么呢 —— 在控制台里输出了这样的内容:

Exception in thread “main” java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at java.util.ArrayList.(ArrayList.java:153)
at dubug.study.main(study.java:11)

Requested array size exceeds VM limit. 译文:请求的数组大小超过VM限制

这是什么东西,我在学 Java,我在学 Java 的集合类其中的一个 ArrayList ,为什么会告诉我在 VM 上出了问题?接下来我们要从 数组 来重新看这个问题。

什么是数组

我们可以创建并组装数组,利用整型的下标码作为索引值访问它的元素,并且数组的尺寸不能改变。这是我们对数组的认知,实际上是够用的,但是这点内容无法回答我们的问题,所以我们要对数组进一步进行思考。

在《Think in Java》这本书里是这么说的:

数组是第一级对象

无论使用哪种类型的数组,数组标识符其实只是一个引用,指向在堆中创建的一个真实对象,这个(数组)对象用以保存指向其他对象的引用。可以作为数组初始化语法的一部分隐式地创建此对象,或者用new表达式显式地创建。只读成员length是数组对象的一部分(事实上,这是唯一一个可以访问的字段或方法),表示此数组对象可以存储多少元素。“[]”语法是访问数组对象唯一的方式。

什么是第一级对象?我们很容易的想到有关对象的一些类方面的操作,不妨对数组也试一下。

int[] array = new int[10];
System.out.println("array的父类\t->\t" + array.getClass().getSuperclass());
System.out.println("array的类名\t->\t" + array.getClass().getName());

在控制台里我们拿到了这样的信息:

array的父类 -> class java.lang.Object
array的类名 -> [I

是的,你一定认为我手抖了没有复制完整,但这就是完整的信息

在这里插入图片描述

这说明在JDK中不存在这个类,因为类名 “[I” 就不是合法的标识符

“Java是一个面向对象的语言,数组也是一个对象”,在看文章之前的你应该也听说过这句话。但是会不会发觉,数组对象和其他对象的操作方式也不太一样?创建数组用的是[],而其他的对象是不用这个符号的。数组没有 class 文件,不像常用的 String 是有 String.class 的,数组是在运行时生成的东西。

如果大家编码的时候手快了,可能会打出来 Array 这个东西,它是 java.lang.reflect 包下的。但是它的标识是:

public final class Array { /***/ }

final 修饰,说明数组也不是 Array 的子类

我们再尝试获取数组的成员:

public static void main(String[] args) {
    int[] array = new int[10];
    Class clazz = array.getClass();
    System.out.println(clazz.getDeclaredFields().length);
    System.out.println(clazz.getDeclaredMethods().length);
    System.out.println(clazz.getDeclaredConstructors().length);
}

输出的结果是3个0。其实很多人根本没用过这种代码,包括我也是,上面的代码我也是复制的别人的。但是我们从字面上不难理解出来:

数组没有任何成员变量、成员方法、构造函数。数组一般怎么操作?很常见的一个操作是 arr.length ,显然也是没有的,不然不会输出0啊。我们再去试着输出 String 的成员变量:

public static void main(String[] args) {
    String str = "str";
    Class cl = str.getClass();
    Field[] fields = cl.getDeclaredFields();
    for (Field e : fields)
        System.out.println(e.getName());
}

value
hash
serialVersionUID
serialPersistentFields
CASE_INSENSITIVE_ORDER

那么数组的实现应当是与 JVM 有所关系的了,以至于我们尝试在更高的层次去访问数组时看不到任何的内容而这些内容是真实存在的。到这里我们就不再进一步深入探究Java的数组的底层是怎么实现的了,说来也有趣,我们总说数组是xxx的底层,却从来没研究过数组的底层,有机会还是要看看数组是怎么实现的。

在上面的阐述中,我们知道这么个信息:数组没有成员变量,但是数组有一个只读成员length。Java里数组不是类,没有对应的class文件,数组是由 JVM 经过一系列操作产生的;在 JVM 中获取数组的长度的字节码指令是arraylength,在数组的对象头里存在字段 _length,需要获得数组长度直接访问这个字段就可以了。

在源码里有作者留下的注释,这段注释或许能让我们理解最初的问题 MAX_ARRAY_SIZE 为什么要 -8

The maximum size of array to allocate (unless necessary).
Some VMs reserve some header words in an array.
Attempts to allocate larger arrays may result in
OutOfMemoryError: Requested array size exceeds VM limit

有一些VM为了保存数组的对象头,所以不允许可分配的数组大小达到 int 最大值,但是并非所有VM都是如此,所以便有了我们认为非常无趣的代码:

if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

它的意义是:如果VM不需要保存数组的对象头,那么 newCapacity 的值就可以取到 Integer.MAX_VALUE。

下面再介绍一下对象头,认识了对象头的概念,这篇文字的问题应该就一扫而空了。

数组除了自身要存储数据以外,还有存有一定量的对象头,大家可以想象一下TCP报文有关的内容或者是网页请求的方向。Java的每个对象都有对象头,在HotSpot虚拟机中,对象头的大小不会超过 32byte ,32byte 相当于 8 int,所以最大容量 -8 才不会导致存储溢出。

Java在堆内存中的存储布局如下:

图片

其中,对象头包括:

图片

8 int 的长度是 32byte 的长度,而这 32byte 就包含了:

8 bytes(Mark Word的最大占用) + 8 bytes(Klass Pointer的最大占用) + 4 bytes(数组长度)+ 8 bytes(引用指针的最大占用:数组中存放的是对象的引用) + 4 bytes(padding:为了方便寻址,JVM要求对象大小要求是8的倍数,不够就填充)

*,但是并非所有VM都是如此,所以便有了我们认为非常无趣的代码:

if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

它的意义是:如果VM不需要保存数组的对象头,那么 newCapacity 的值就可以取到 Integer.MAX_VALUE。

下面再介绍一下对象头,认识了对象头的概念,这篇文字的问题应该就一扫而空了。

数组除了自身要存储数据以外,还有存有一定量的对象头,大家可以想象一下TCP报文有关的内容或者是网页请求的方向。Java的每个对象都有对象头,在HotSpot虚拟机中,对象头的大小不会超过 32byte ,32byte 相当于 8 int,所以最大容量 -8 才不会导致存储溢出。

Java在堆内存中的存储布局如下:

图片

其中,对象头包括:

图片

8 int 的长度是 32byte 的长度,而这 32byte 就包含了:

8 bytes(Mark Word的最大占用) + 8 bytes(Klass Pointer的最大占用) + 4 bytes(数组长度)+ 8 bytes(引用指针的最大占用:数组中存放的是对象的引用) + 4 bytes(padding:为了方便寻址,JVM要求对象大小要求是8的倍数,不够就填充)

有关对象头信息参考:https://blog.csdn.net/fisherish/article/details/117134717

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值