源码分析篇--Java集合操作(5)

2.6 ArrayList容器
ArrayList类实现了List接口,它继承自AbstractList抽象类,继承机构如下所示:

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

其中,AbstractList抽象类的继承结构如下所示:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> 

其中,AbstractCollection抽象类的集成结构如下所示:

public abstract class AbstractCollection<E> implements Collection<E>

由于ArrayList类实现了List接口,因此它具有List接口的全部方法。下面我们来介绍开发中常见的几个方法。
2.6.1 ArrayList()构造方法
默认的构造方法,可以通过new ArrayList()的方式获得一个list对象,在开发中比较常见。
2.6.2 ArrayList(int initialCapacity)
该构造器需要传入一个初始化容量。这个构造方法自己指定ArrayList的初始化长度。当初始化长度小于0的时候,抛出异常。在开发中,鼓励使用初始化list容器的构造器,这是因为list的底层是数组不断扩容的过程,默认第一次扩容大小是10,后面每一次扩容是前面扩容的1.5(3/2)倍;
//容量增大为原来的3/2倍:7(111)>>1 => 011(3) 7/2=3;移1位相当于除以2。
int newCapacity = oldCapacity + (oldCapacity >> 1);
//数组拷贝,并扩容
elementData = Arrays.copyOf(elementData, newCapacity);

经典考题

ArrayList list = new ArrayList(20)扩容了几次?(A)
A.0   B.1   C.2   D.3
因为该构造方法是直接初始化为容量20的容器,没有扩容的过程,底层代码如下所示。
public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

为什么说参数型构造函数在一定条件下要比无参构造方法效率要快呢?
默认情况下,无参构造方法的扩容量为10,当入参大于10时,由公式新容量 = 旧容量/2+旧容量+1可知,添加同样多的元素,初始化因子越大,扩容的次数越少,效率越高。但是并不是扩容因子越大就越好,下面的案例说明了这一点:

List<Integer> list5 = new ArrayList<Integer>();
long start1 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list5.add(i);
}
long end1 = System.currentTimeMillis();
System.out.println("list5:"+(end1-start1));//2394

List<Integer> list6 = new ArrayList<Integer>(5);

long start2 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list6.add(i);
}
long end2 = System.currentTimeMillis();
System.out.println("list6:"+(end2-start2));//2911

List<Integer> list7 = new ArrayList<Integer>(20);
long start3 = System.currentTimeMillis();
for(int i = 0;i<5000000;i++){
list7.add(i);
}
long end3 = System.currentTimeMillis();
System.out.println("list7:"+(end3-start3));//2293

从上面的结果我们可以看出来,在一定范围内,有参构造函数的扩容量因子越大,扩容次数减少,效率越高。当然,一般情况下,扩容量选择20比较合适,这取决于机器的性能。

扩容公式:
新容量 = 1.5*旧容量
考题2

下面的代码中,经历了几次扩容?(c)
List<Integer> list7 = new ArrayList<Integer>();
long start3 = System.currentTimeMillis();
for(int i = 0;i<31;i++){
list7.add(i);
}

A.1      B.2      C.3      D.4
说明:默认情况下,初始化容量为10,以后按1.5原有的容量进行扩容。因为31>10,因此,一定发生了扩容,设经历了至少x次扩容,那么1.5^x*10>=31,由此可以估算出x=3。扩容过程为:10*1.5=>15,15*1.5=>22,22*1.5=33。那么为什么说默认情况下,初始化容量为10,以后按1.5原有的容量进行扩容呢?我们可以看看底层的代码:
//默认的容量
private static final int DEFAULT_CAPACITY = 10;
//在add中调用ensureCapacityInternal()进行扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}
//确认容量minCapacity
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}
//Explicit明确的,确认容器的容量,通过判断当前的元素个数(包括新增)与数组的长度作对比,如果元素个数大于数组的长度,那就调用grow方法进行扩容:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//扩容方法,通过int newCapacity = oldCapacity + (oldCapacity >> 1);这句可以看出,默认情况下,初始化容量为10(通过minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);该句可以看出来,因为初始条件下,size+1=1<10,因此minCapacity= DEFAULT_CAPACITY=10),以后按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);
}

考题3

下面的代码中,经历了几次扩容?(B)
List<Integer> list7 = new ArrayList<Integer>(20);
long start3 = System.currentTimeMillis();
for(int i = 0;i<31;i++){
list7.add(i);
}

A.1      B.2      C.3       D.4
解:我们看底层的有参构造方法为:
public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
}
可知,此时size= initialCapacity;
利用公式新容量 = 1.5*旧容量可得:20*1.5=30,30*1.5=45;因此一共经历了两次扩容。我们可以通过反射来检验扩容的数组elementData:
List<Integer> list7 = new ArrayList<Integer>(20);
for(int i = 0;i<31;i++){
	list7.add(i);
}
System.out.println(list7);
Class<? extends List> cl = (Class<? extends List<Integer>>) list7.getClass();
try {
	Field f = cl.getDeclaredField("elementData");
	f.setAccessible(true);
	try {
		Object[] o = (Object[])f.get(list7);
		for(Object o1:o){
			System.out.print(o1+" ");
		}
	} catch (IllegalArgumentException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IllegalAccessException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
} catch (NoSuchFieldException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
} catch (SecurityException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 null null null null null null null null null null null null null null,由此我们可以看出来,初始化容量因子越大,扩容次数越少,但是,每次扩容开辟所需的内存空间也会比较大,elementData数组有时会发生大量的内存空间没有被占位的现象,因此,初始化容量并不是越大越好,一是可能浪费内存空间,二是开辟内存空间也会影响效率,因此初始化容量越大也会出效率低下的现象,一般而言,以20个初始化容量为最佳。

考题4

在初始化集合时,初始化容量因子数越大越好(错)。

考题5

ArrayList继承自下面哪两个类?(A)
A、AbstractList和AbstractCollection     B、List和AbstractCollection 
C、AbstractList和List                   D、Collection和Object
选A。我们来看一下ArrayList的继承关系:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> 
public abstract class AbstractCollection<E> implements Collection<E>
从上面的继承关系我们可以看出来,ArrayList直接继承自AbstractList 抽象类,实现了List接口,AbstractList又继承自AbstractCollection抽象类,实现了List接口,而AbstractCollection实现了Collection接口。因此,ArrayList类继承的类 有:AbstractList、AbstractCollection和Object;ArrayList实现了List接口,综上所述,答案为A。

2.6.3 ArrayList(Collection<? extends E> c)
在介绍这个构造器之前,我们先来了解一下范型符号的使用。
在创建集合对象的时候,要保证前后的泛型一致,或者创建集合时与前面的范型保持某种关系,如果不一致或不存在关系在编译时就会出错。例如,下面这句就会编译出错:
List listTest = new ArrayList();
为什么会编译出错呢?
这是因为ArrayList虽然是List的实现类,String是Object的实现类,但是ArrayList不是List的子类或实现类,因此会发生编译错误。我们来模拟一下这个过程:

package com.yx.yzh.test;
public interface Test<T>{}

package com.yx.yzh.test;
public class Test4<T> implements Test<T>{
	public static void main(String[] args) {}
}

我们看到,Test与Test4的范型都是T,因此在创建Test4的对象时,需要保持前后的范型的类型一致,否则会发生编译错误,例如,下面两种情况会发生编译错误:
Test test = new Test4();
Test test2 = new Test4();
需要保持一致:
Test test3 = new Test4();
我们再来看一下继承关系中的范型:

package com.yx.yzh.test;
public class Test1<E> {}

package com.yx.yzh.test;
public class Test5<T> extends Test1<T> {
	public static void main(String[] args) {
		//编译错误
		//Test1<String> test = new Test5<Oject>();
		//编译错误
		//Test1<Object> test2 = new Test5<String>();
		//正确
		Test1<String> test =  new Test5<String>();
	}
}

但是,要注意当两种或以上的范型遇上时,需要将范型提到第一个类型的范型上,且其中之一的范型与实现类或继承类的范型保持一致:

package com.yx.yzh.test;
public class Test1<E> {}

package com.yx.yzh.test;
public class Test3<E, E2> extends Test1<E2> {
	public static void main(String[] args) {
		Test1<String> t = new Test3<Object,String>();
	}
}

E2范型需要一一对应,因此Test1与Test3的第二范型保持一致,均为String类型,剩下的范型没有一一对应关系,因此可为任意类型。这里涉及到了一个概念:向上转型。向上转型是指父类或接口的引用指向子类或实现类。也就是说使用父类或接口来接收创建对象的引用。上面的例子就是向上转型。有向上转型自然也有向下转型。向下转型是指由于子类或实现类的引用不能直接指向父类或接口,需要强制转换(子类或实现类)才能使用子类或实现类来接收父类或接口。例如:

package com.yx.yzh.test;
public class Test3<E, E2> extends Test1<E2> {
	public static void main(String[] args) {
		//向上转型
		Test1<String> t = new Test3<Object,String>();
		//向下转型
		Test3<Object,String> t2 = (Test3<Object,String>)t;
	}

}

A、对于单字母类范型在java中约定:T表示类型(Type),E表示元素类型(Element)。
B、范型通配符?
泛型通配符?是指当有不明确类型的范型时,可以使用?来代替范型类型。需要注意的是,在创建类中需要明确指出某种类型,不能使用?范型通配符。因此?范型通配符只能使用在方法参数类型范型声明中或者接收引用的那一方:

package com.yx.yzh.test;
public class Test5<T> extends Test1<T> {	
	public void helloWorld(){}
	public void show(Test5<?> t){}
}

package com.yx.yzh.test;
public class Test6<T> extends Test5<T>{
	//不知道T声明est6是什么类型,但是创建Test6时必须明确给出类型
	//Test6<?> t = new Test6<String>();
	//Test5<?> t2 = new Test6<Integer>();
	//方法中可以使用?范型通配符声明,在调用时需要明确给出引用的具体范型类型
	public void show(Test5<?> t){
		t = this;
		t.helloWorld();
	}
	
	public void helloWorld(){
		System.out.println("hello woold!");
	}
	
	public static void main(String[] args) {
		Test5<?> t = new Test6<String>();
		//t接受了具体范型类型
		t.show(t);
	}
}

C、? extends E
? extends E表示该范型的类型的向上限定,也就是具体类型必须为E及其子类。同理,? extends E只能使用在方法参数类型范型声明中或者接收引用的那一方。

已知Student继承自People类

package com.yx.yzh.test;
public class People {}

package com.yx.yzh.test;
public class Student extends People{}

在声明范型时,可以这样理解? extends E中的?通配符:
package com.yx.yzh.test;
public  class Test8<T> extends Test7<T> {
	public static void main(String[] args) {
		//final类不能被继承,但? extends String这是个例外表达式
		Test7<? extends String> t = new Test8<String>();
		Test7<? extends Object> t2 = new Test8<String>();
		//错误,因为?的父类为String,范型向上限定为String,所以?为Object会编译错误
	     //Test7<? extends String> t3 = new Test8<Object>();
		//Student是People的子类,而Test7与Test8范型一致,因此?表示Student,正确,
		Test7<? extends People> t4 = new Test8<Student>();	
	}
	//传参时,传入的引用的范型需要是People或People的子类或是实现类
	public void show(Test7<? extends People> t1){
	}
}

在? extends E中?也可以是可以是实现类与接口的关系:
现已知ClassT是InterfaceT的实现类

package com.yx.yzh.test;
public interface InterfaceT {}

package com.yx.yzh.test;
public class ClassT implements InterfaceT{}

那么可以满足? extends E表达式为:
Test7<? extends InterfaceT> t5 = new Test8<ClassT>();
需要注意的是,在方法中声明类型时必须给出范型的具体类型,范型不能是E、T。例如,右边表达式会编译错误:Test7<? extends T> t5 = new Test8<ClassT>();但是在方法中可以声明参数的范型为? extends E这种形式。
package com.yx.yzh.test;

public  class Test8<T> extends Test7<T> {
	public void show(Test7<? extends People> t1){}
	//在调用时,需要给出T类型和?的具体类型,?就是相应的范型类型,且必须与T保持继承关系
	public void show2(Test7<? extends T> t1){}
}
但是,当方法中的范型范型为? extends T,在传入参数时,前后必须明确给出范型类型,而不能使用? extends T进行声明,否则在调用时会发生编译错误:
package com.yx.yzh.test;

public  class Test8<T> extends Test7<T> {
	public static void main(String[] args) {
		Test8<Student> t6 = new Test8<Student>();//明确类型
		t6.show2(t6);

	}
	public void show2(Test8<? extends T> t1){}
}
//可以编译通过,但是调用方法时会报错:
Test8<? extends People> t7 = new Test8<Student>();
T7.show2(t7);
应明确指出类型:
Test8<Student> t6 = new Test8<Student>();//明确类型
t6.show2(t6);

D、? super E
?super E表示该范型的类型的向下限定,也就是具体类型必须为E及其父类。同理,?super E只能使用在方法参数类型范型声明中(可以是父类或接口)或者接收引用的那一方(可以是父类或接口)。
package com.yx.yzh.test;

public  class Test8<T> extends Test7<T> {
	public static void main(String[] args) {
		//尽量不要这么声明,需要声明具体泛型类型
		Test7<? extends People> t10 = new Test8<Student>();
		
		Test8<Student> t8 = new Test8<Student>();
		t8.show3(t8);//? super Student
		
		Test8<People> t9 = new Test8<People>();
		t9.show3(t9);//? super People
		
	}	
	public void show3(Test8<? super T> t1){
		
	}
}

通过上面的理解,我们了解到,ArrayList(Collection<? extends E> c)传入的参数必须是一个具体范型类型的实现类的引用。例如:

List<String> listTest = new ArrayList<String>();
List<String> listTest2 = new ArrayList<String>(listTest);

你会意识到,ArrayList(Collection<? extends E> c)其实就是一个拷贝现有集合的构造器。通过底层代码可以验证这一点:

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

List<String> listTest = new ArrayList<String>();
listTest.add("hello world!");
List<String> listTest2 = new ArrayList<String>(listTest);
System.out.println();
for(String s : listTest2){
System.out.print(s+" ");
}
/**output:
hello world!
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值