1. List接口
在实际开发中,List接口的使用频率在Collection接口的子接口中很高,在进行集合处理的时候,有限考虑Llist接口。
List接口中重要的扩展方法:
public E get(int index) // 根据索引取得数据
public E set(int index, E element) // 根据指定索引修改相应元素
List接口与Collection接口相比较最大的特点在于List接口有一个get()方法,可以根据索引取得内容。由于List本身还是接口,是接口就必须有子类,在List接口下由三个常用的子类是我们必须了解的:ArrayList、Vector、LinkedList.
以上类与接口的使用关系图如下图所示:
1.1 ArrayList子类(优先考虑)
ArrayList是一个针对List接口的数组实现。
举例: ArrayList中方法的基本使用
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("hello");
list.set(2, "test"); //根据指定索引修改相应元素
for(int i = 0; i < list.size(); i++){
System.out.println(list.get(i) + "、");
}
System.out.println(list.size()); //list元素个数
System.out.println(list.contains("world")); //是否存在指定元素
System.out.println(list.remove("hello")); //删除"hello",只删除第一个遇到的
list.clear(); //清空元素
System.out.println(list.size());
System.out.println(list);
}
注意: 接口中要保存自定义类对象,自定义的类必须覆写equals()方法。
在自定义类对象是,使用list.contains(Object o)方法判断ArrayList是否包含一个元素对象(针对于对象的属性值相同,但对象地址不同的情况),当我们没有覆写equal()方法时,虽然对象属性值相同,但由于此类仍然调用的是Object类的equals()方法,这个方法只比较两个对象的地址是否相同,相同返回true,不同返回false,因此我们的结果返回了false。
// 源码分析
// contains()方法
public boolean contains(Object o) {
return indexOf(o) >= 0; // 调用了indexOf()
}
// indexOf()方法:返回对象的下标:对象存在,返回下标;不存在,返回-1
public int indexOf(Object o) {
// 如果所传对象为null,遍历list比较判断
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]))//使用equals方法比较
return i;
}
return -1;
}
//equals()方法
public boolean equals(Object obj) {
return (this == obj); //比较的时两元素地址是否相同,相同返回true,不同返回false
}
类集contains()、remove()等方法需要调用equals()方法来判断元素是否相等。
**举例:**创建一个Person类,覆写equals()方法,将该类对象存在ArrayList中,并调用contains(Object obj)方法判断obj对象是否存在于ArrayList中。
import java.util.ArrayList;
import java.util.List;
class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 判断对象的内容相同
@Override
public boolean equals(Object obj) {
// 同一对象,肯定相同
if(this == obj){
return true;
}
// 所传对象为null,肯定不相同
if(obj == null){
return false;
}
// 不是该类及其子类的对象,肯定不相同
if(!(obj instanceof Person)){
return false;
}
// 不为空,且时该类及其子类的对象
// 向下转型变为Person对象来比较属性
Person per = (Person)obj;
return this.age == per.age && this.name.equals(per.name);
}
@Override
public String toString() {
return "Person1{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Collection接口 {
public static void main(String[] args) {
Person per1 = new Person("张三", 18);
Person per2 = new Person("李四", 19);
Person per3 = new Person("王五", 20);
List<Person> list = new ArrayList<>();
list.add(per1);
list.add(per2);
list.add(per3);
System.out.println(list.contains(new Person("张三", 18)));
}
}
运行结果返回true。
分析ArrayList源码:
1. ArrayList的无参构造方法剖析:
List<String> list1 = new ArrayList<>();
// 当我们new了一个ArrayList对象时,就创建一个个ArrayList的对象,我们一般采用的是无参构造,我们查看一下源码会发现ArrayList有点“懒”。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
transient Object[] elementData;//是一个数组
// 我们看到无参构造方法会发现,ArrayList底层实现是一个数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 空数组,没有任何元素
// 无参构造中将DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组赋值给我们的ArrayListc初始化数组,我们会发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组,数组长度为0。
// transient关键字,我们在序列化中学习过,如果想要将类中的某一属性在对象序列化时不生成二进制字节码,就会使用transient关键字。
2. ArrayList的add()方法剖析
建立了动态数组,就需要给里面存储元素,所以我们在这里详细的了解一下它是如何添加元素的?它被称为动态数组,它的动态是如何体现的?它的扩容策略是如何实现?
// 调用add()方法给list中添加元素
list.add(new Person("Jack", 10));
// add()方法
public boolean add(E e) {
// 虽然我们不知道这个ensureCapacityInternal方法的作用是什么,但是我们通过它的名字意思,和后面的注释,我们会发现,它是确保数组动态的基础,给数组扩容
// size是属性,用于记录list中对象个数
// 当第一次添加元素时size=0,size+1=1
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// minCapacity=size+1
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 先观察上面方法中传的参数,calculateCapacity(elementData, minCapacity)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果数组为空数组,就返回DEFAULT_CAPACITY和minCapacity里面较大数
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY=10
return Math.max(DEFAULT_CAPACITY, minCapacity); // 取两者最大值
}
// 如果数组不为空数组就返回现在数组的size+1(上面有分析:minCapacity=size+1)
return minCapacity;
}
// 我们看完了calculateCapacity()方法后,知道了,它是一个计算当前数组容量的方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
// 数组长度小于数组所需容量,扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 扩容方法
}
// 扩容方法grow()方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容策略:数组长度变为原来的1.5倍
// 如果扩容之后容量还是不够,将现在需要的数组的容量赋给新容量。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果扩容后的容量大于ArrayList的最大容量,将ArrayList容量设为整型最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 开辟newCapacity容量的数组,并将元素拷贝新数组,并返回新数组,使ArrayList中的elementData数组指向新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
看了ArrayList的部分源码之后,总结一下:
- ArrayList底层实现是一个对象数组,无参构造声明一个ArrayList对象,初始长度为0
- 当数组长度不够用时,扩容策略:数组长度变为原来数组的1.5倍。
- 对集合的增删改查都是异步处理,性能较高,线程不安全。
- 第一次添加元素时,数组会开辟空间,大小为10
1.2 Vector
Vector时从JDK1.0提出的,而ArrayList是从JDK1.2提出的,二者的实现都是数组。二者的使用方式基本一致。
分析Vector源码:
1. Vector无参构造方法剖析
List list = new Vector();
// this关键字调用了Vector中一个参数的有参构造,并将传递参数为10
public Vector() {
this(10);
}
// 一个参数的有参构造
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
// 两个参数的有参构造
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 开辟数组空间:大小为10
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
// 通过上述代码的分析,我们可以了解到,Vector的底层实现也是一个对象数组,无参构造声明一个Vector对象时,初始化对象数组的长度为10。
2. Vector的add()方法剖析
// add()方法使用synchronized锁
public synchronized boolean add(E e) {
modCount++;
// elementCount:用于记录当前对象数组中有效对象的个数
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
// 判断对象数组的容量的是否够,不够,扩容;
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 扩容方法grow()方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// capacityIncrement:这个参数我们在无参构造时观察到了,默认为0
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// capacityIncrement=0,那么我们就可以知道Vector的扩容策略为:数组长度变为源数组的2倍
// 下面代码实现和ArrayList代码实现一致,在这不做过多讲解
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
看了Vector的部分源码之后,总结一下:
- Vector底层实现是一个对象数组,无参构造声明一个Vector对象,初始长度为10
- 当数组长度不够用时,扩容策略:数组长度变为原来数组的2倍。
- 对集合的修改都采用同步处理(直接在方法上使用内建锁),性能较低,线程安全。
1.3 请解释ArrayList和Vector的区别?
在我们看了二者的部分源码之后,对比会发现一些区别,我们现在可以总结一下它们:
1. 产生版本
a. Vector是JDK1.0产生的。
b. ArrayList是JDK1.2产生的。
2. 线程安全
a. Vector采用在方法上添加synchronized来保证线程安全,性能较低。
b. ArrayList采用异步处理,性能较高,线程不安全。
3. 初始化及扩容策略
a. Vector对象产生时就初始化数组大小为10,当数组不够用时,扩容为源数组的2倍。
b. ArrayList使用懒加载策略,在第一次添加元素时才初始化数组大小,当数组不够用时,扩容为源数组的1.5倍。
1.4 LinkedList
在List接口中还有一个LinkedList子类,这个子类如果向父接口转型的话,使用形式与之前没有任何区别。
LinkedList是基于链表实现的动态数组。
请解释ArrayList与LinkedList的区别?
- ArrayList里面存放的时一个数组,如果实例化此类对象传入类数组大小,则里面保存的数组就会开辟一个定长的数组,但是后面再进行数据保存的时候发现数组个数不够就会进行数组动态扩充。ArrayList在查找元素时比较方便。
- LinkedList是一个链表实现的,便于增删元素实现。
总结:ArrayList封装的是数组;LinkedList封装的是链表。ArrayList时间复杂度为1,而LinkedList的复杂度为n。