概述
图片来自菜鸟教程
由图可知,Java集合框架主要由两部分组成,一个是集合Collectin,另一个是图Map,Collection接口有三个子接口,这三个子接口下面是一些实现的抽象类,再往下就是一些实现,之所以定义这么多的接口就是为了适应不同类型的操作。
1、Collection接口
查看Collection接口源码,其中定义了常用的add(),get(),remove(),retainAll()(取交集)等方法。
- Collection接口定义了sort()排序函数,其实现是调用Arrays.sort()实现的。
- Collection的indexOf和lastIndexOf查找第一次和最后一次出现指定元素的下标,从0开始。
2、深拷贝与浅拷贝
RandomAccess接口: 是一个标记接口,根据是否实现了这个接口采用不同的处理方式,当一个List拥有快速访问功能时,其遍历方法采用for循环最快速。而没有快速访问功能的List,遍历的时候采用Iterator迭代器最快速。
实现了Cloneable接口也是一个标记接口,在类中重写Object中的clone方法,然后通过类调用clone方法才能克隆成功,如果不实现这个接口,则会抛出CloneNotSupportedException(克隆不被支持)异常。
深拷贝与浅拷贝
- ①、浅拷贝
浅拷贝简单实现如下:
public class Person implements Cloneable{
public String name;
public Integer age;
public Address address;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
this.address = new Address();
}
public Person() {
}
public void setAddress(String province, String city) {
address.setAddress(province,city);
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String print() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
public class Address {
public String province;
public String city;
public void setAddress(String province,String city) {
this.province = province;
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p1 = new Person("admin",21);
p1.setAddress("江苏省","苏州市");
System.out.println("p1: " + p1 + " " + p1.print());
Person p2 = (Person) p1.clone();
System.out.println("p2: " + p2 + " " + p2.print());
p2.setAddress("江苏省","南京市");
System.out.println("p1: " + p1 + " " + p1.print());
System.out.println("p2: " + p2 + " " + p2.print());
}
}
/**
p1: com.penghui.collections.copy.Person@4554617c Person{name='admin', age=21, address=Address{province='江苏省', city='苏州市'}}
p2: com.penghui.collections.copy.Person@74a14482 Person{name='admin', age=21, address=Address{province='江苏省', city='苏州市'}}
p1: com.penghui.collections.copy.Person@4554617c Person{name='admin', age=21, address=Address{province='江苏省', city='南京市'}}
p2: com.penghui.collections.copy.Person@74a14482 Person{name='admin', age=21, address=Address{province='江苏省', city='南京市'}}
*/
根据结果可以发现,Person中的address经过clone之后只是复制了address的引用,内存地址还是指向之前的内存空间,当其中一个修改address值,另一个都会跟着变化。
创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
- ②、深拷贝
深拷贝实现在Person中将clone实现修改为如下:
@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person) super.clone();
p.address = (Address) address.clone();
return p;
}
并让Address类实现Cloneable接口,重写clone方法
同样的测试结果如下:
p1: com.penghui.collections.copy.Person@4554617c Person{name='admin', age=21, address=Address{province='江苏省', city='苏州市'}}
p2: com.penghui.collections.copy.Person@74a14482 Person{name='admin', age=21, address=Address{province='江苏省', city='苏州市'}}
p1: com.penghui.collections.copy.Person@4554617c Person{name='admin', age=21, address=Address{province='江苏省', city='苏州市'}}
p2: com.penghui.collections.copy.Person@74a14482 Person{name='admin', age=21, address=Address{province='江苏省', city='南京市'}}
也就是说所有属性都会独立拷贝一份,当修改其中一个对象时另一个不会受到影响。
另一种实现深拷贝的方式时是使用序列化,将Person和Address类都实现serializable接口,在Person添加如下方法:
public Object deepClone() throws Exception{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ooi = new ObjectInputStream(bis);
return ooi.readObject();
}
采用Person p2 = (Person) p1.deepClone();
调用即可。
参考文章:
Java的深拷贝和浅拷贝
3、ArrayList
3.1、底层数据结构
源码中有这样一个成员变量:
transient Object[] elementData;
这就是ArrayList的底层数据结构,至于为什么要使用transient修饰,原因在于序列化和反序列化。
transient用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。
对于ArrayList来说,序列化的时候会调用自身重写的writeObject()方法,将size和element写入ObjectOutputStream,反序列化的时候调用readObject()方法,从ObjectInputStream获取size和element,再恢复到elementData中。
不直接将elementData序列化的原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
3.2、构造函数
构造函数有三个,无参、指定容量、包含指定集合这三个。无参构造函数在初始化的时候长度为0,只有在真正存数据的时候才会将长度置为10。
3.3、核心方法
①、add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
具体流程:
- 首先检查长度是否够用调用ensureCapacityInternal()方法,因为添加元素长度要增长,所以+1;
- ensureCapacityInternal()实现如下:
第一次存值会将minCapacity置为10,之后会调用ensureExplicitCapacity()方法;private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
- ensureExplicitCapacity()方法
如果minCapacity长度大于elementData长度,则调用grow()函数扩容;private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
- grow()函数
可以看到它是将原长度扩充为原来的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(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++;
}
这个操作可能会涉及到原数组整体或部分元素的移动,效率不高,数组移动调用的是System.arraycopy()方法,
//src为原数组,srcPos从原数组复制的起始位置,dest目标数组,destPos复制到目标数组起始位置,length要复制的原数组中元素的数量
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
③、batchRemove(Collection<?> c, boolean complement),用于removeAll()和retainAll()方法
//删除或者保留指定集合中的元素
//用于两个方法,一个removeAll():它只清除指定集合中的元素,retainAll()用来测试两个集合是否有交集。
private boolean batchRemove(Collection<?> c, boolean complement) {
//将原集合,记名为A
final Object[] elementData = this.elementData;
//r用来控制循环,w是记录有多少个交集
int r = 0, w = 0;
boolean modified = false;
try {
//遍历 ArrayList 集合
for (; r < size; r++)
//参数中的集合c一次检测集合A中的元素是否有
if (c.contains(elementData[r]) == complement)
//有的话,就给集合A
elementData[w++] = elementData[r];
} finally {
//发生了异常,直接把 r 后面的复制到 w 后面
if (r != size) {
//将剩下的元素都赋值给集合A
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
//这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是为null。
//retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。
// 清除多余的元素,clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}