前言
学习Java的list时,发现只靠脑子记很容易忘掉,写在博客里更能加深印象,梳理逻辑。
请谨慎阅读,因为里面的内容是我的学习记录,可能有很多不对的地方。
一、Integer的一点了解
1.基本说明,和自动装箱的intValue/Valueof
Integer是基本类型的包装int->Integer。
关于Integer的赋值,如
Integer i = 32;//i = Integer.valueOf(32);
int t = i;//t = i.intValue()
32是int类型,赋值为Integer中会自动转换–好像被叫作自动装箱?自动调用了Integer.valueOf(32)
int t = i 调用了i.intValue();
valueOf(),源码如下
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
//若获取的i在缓存的[low,high]中,返回这个Integer的引用
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);//返回新建
}
使用了缓存池,根据这个返回方式,是存储了所有从low 到high的Integer对象,至于这个缓存池的作用,我还没有了解,查找(alt+7,ctrl+f12)了IntegerCache结构后,在定义处发现了几个不懂的地方:
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
如所示,为什么 low最小值是写在代码里的,而high却是可以配置的(在下面初始化的代码里)?为什么要用archivedCache初始化数组,而用常量数组cache访问?可能是为了避免修改缓存池的值。
之后在这段代码
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
可以看到,获取虚拟机设置的最大值后,取其为上界,但是必须包含[-128,127],缓存长度最长为MaxInt,居然是2^31-1,这么大的 缓存,震惊。
之后从CDS读取缓存。。还看不懂,但是看懂了如果没有读取到,会怎么样。
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
如所示,就创建新数组,并从low到high赋值,但是有个疑惑,为什么用这种for(i)j++的办法赋值?while(j<=high)c[i] = new Integer(j++);比这个差在哪呢?
而i.intValue()就直接返回它存储的常量value,这个常量创建时就存储了。
二、ArrayList的了解
0.ArrayList属性
如图是其属性,依次是
序列id
默认容量为10
默认空数组(有参构造,初始化容量)
默认空数组(无参构造,第一次加入元素扩容为默认容量)
存放元素的数组(比如雷元素,岩元素)
有效元素个数
1.add函数
直接调用的源码如下
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
看上面的注释说永远返回true是Collection.add规定的?点开发现写着:如果集合按调用改变了就返回真,emm,确实。
modCount是因为这个容器线程不安全,为了防止并发修改同一个容器出错,在迭代器和原容器里都放了个modCount,迭代器里叫expectedModCount,每次调用,如果二者的modcount,即修改次数不一致,即说明有别的线程修改了这个容器,迭代器可能失效。
请看Integer.itr.next()的代码:
public E next() {
checkForComodification();
和check…函数的代码
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这个函数定义为final,我本来以为是指函数里不会修改变量的值,结果查到是不能继承,很合理,原来是不能修改方法本身。
里面检查自己的修改次数和容器的一不一样,抛出并发修改异常。
再看调用的方法 add(e,data,size);
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
这个方法最后设置size = s+1,而不是size++,也许是为了并发?(被称为操作的原子性?)或者一些奇怪的考虑。
先判断s是不是等于存储数组的上限—我才发现size可能不定是存储数组的大小,而是数组内有效元素的个数,为此我先看了ArrayList的构造函数,又想了一会,想到C++里的capacity这个变量应该是java里的elementData.length,不用单独存放了,我真傻,真的。
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);
}
}
构造函数里,无参直接返回无参默认数组,EMPTY_ELEMENTDATA
和DEFAULTCAPACITY_EMPTY_ELEMENTDATA
的区别就是,前者返回于有参构造,后者于无参构造,都是空的,但是添加的时候就能比较地址判断怎么构造的了,这个说明了解于这里,里面写的无惨构造函数用词有点奇怪啊。
这个构造函数挺简单的,就判断大小,申请数组而已。
上面的add函数调用了grow(),是增长空间用的函数,代码如下
private Object[] grow() {
return grow(size + 1);
}
只是返回了比当前+1的大小,再点进去看
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
//取得存储数组长度
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//增长分为两种,如果初始未指定大小,证明用的是无参的,根据上面写的能知道是用是不是等于DEE判断的
//如果不是无参,或者是无参,但是仍需扩充容量---这时大小应该是DEFAULT_CAPACITY,10(如果一直一个一个加入的话)
//总之,无参的会在第一次需要扩容时试图赋值为10(默认容量),不够就为最小所需容量
//有参会执行下面的内容
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth最小增长 */
oldCapacity >> 1 /* preferred growth希望的增长 */);
//取得新数组的长度,注,此处还没申请数组,只判断了长度,根据参数和注释能看出,原长度为oldCapacity,
//最小增长是所需长度,期望增长是旧长度的0.5倍,加上旧长度,扩容1.5倍。
//之所以要在这判断,应该是防止溢出等等
return elementData = Arrays.copyOf(elementData, newCapacity);
//这个调用是真申请和复制数组。
} else {
//数组长度为0,就是没有原内容,如果是无参构造的,就执行这个
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
//但是如果数组长度是0,而且不是默认构造,居然也用上面那个--
//当然了,如果长度是0,就证明不希望使用默认值,声明为0
//这样的话,小数组是用默认的好还是有参的?
//默认的直接返回空数组,添加时才返回,
//如果定义了长度,构造时就返回那个长度的数组,
//而且如果定义的小,比如0,增长到10生成新数组的次数是 1-2-3-4-6-9-13一共7次
//应该是这样,但是IDEA的调试好像会把默认的大小0改成1,还会往数组里加些奇怪的东西,所以没有测试成功
//因为调试 调用的modcount在除构建以外第一次显示modcount已经是2,而且看elementData里已经有两个元素,好像是url,debug的jar包,证明已经加入了2个东西,至少已经操作过两次,而构建的initcapacity传的0被变成了1,不知道为啥。
//那个调试应该是在构建的时候调用了啥注入的东西,因为很奇怪的加入了debug的url。
}
}
再看newLength,
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
//这确保了旧长度和最小增长之和应该大于0
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
//用最小增长和期望增长中更大的判断期望的长度,注释说可能会溢出
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
//没溢出的话,返回长度
return prefLength;
} else {
//这部分是溢出了,
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
其中SOFT_MAX_ARRAY_LENGTH
定义为
public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
为什么是Max_value - 8
,注释写因为某些VM的实现限制(比如hotspot),可能在申请接近MAX_VALUE
大小的数组的时候就算有足够的空间,也会报vm限`制的内存溢出错误,是由于比如object的头大小之类的,所以最大值要选小一点。
如果溢出,看下hugeLength
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
//算出长度,为了别的地方调用,或者防止传进来溢出的值?
if (minLength < 0) { // overflow
//溢出就抛出异常---确实,溢出肯定大于SOFT_MAX_ARRAY_LENGTH
throw new OutOfMemoryError(
"Required array length " + oldLength + " + " + minGrowth + " is too large");
} else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
//emmmm,为啥要返回这个,可能是因为调用这个的都是接近溢出的那种?或者只返回minlen和SMALEN的最大可能值?
return SOFT_MAX_ARRAY_LENGTH;
} else {
//这是在smal和intmax之间的部分
return minLength;
}
}
回到上面的grow(int min…)
判断完新申请的数组长度之后,copyOf正式申请空间
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
直接调用函数,
@IntrinsicCandidate
//说明有hotspot的基于cpu指令的高效实现,而非下面的实现?不清楚
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//这句是申请新数组,还看不太懂为什么要判断
//总之是判断传进来的申请的是不是object类型的数组,是就返回object类型的,不然就调用生成函数
//newInstance点到最底没有实现,没有什么太重要的,差不多就看到这了。
//
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
//这几个居然是公有函数,怪不得要加那么多判断。
//也没有实现,应该就是把旧数组复制进新数组,还有个判断,只复制最多新数组长度的数量。
return copy;
}
总之,grow函数最后会将原数组的内容复制进新申请的更大的数组里,最后add函数
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
//如果数组满了
elementData = grow();
//申请长度+1大小的数组
elementData[s] = e;
//把新内容放进数组,居然不是elementData[size++]=e,想来是为了并发考虑
size = s + 1;
//更新size
}
总结
因此,add的过程就是,先增加modcount,防多线程修改错误,调用添加,如果当前大小不够,就申请增长,一次增长有几种情况,如果是无参构造并且第一次添加,就直接增长到10,如果不是无参构造,就扩容到1.5倍,至少+1,先判断扩容后长度是不是溢出,扩容后,将原数组复制到新数组,再加入新的元素。