Java集合
数组是一种数据结构,用来存储同一类型值,数组不是面向对象的,集合弥补了数组的缺点,更加灵活使用。
- 数组能存放基本数据类型和对象,而集合类存放的都是对象的引用(对象在内存中的存储地址,一个哈希值),而非对象本身。
- 一个数组只能存放一种类型的数据,但是一个集合可以存放的类型不止一种,如List
- 数组一旦创建长度是固定的,集合类容量可以动态改变。
- 数组无法判断实际存储了多少元素,length只告诉了数组的容量,而集合的size()方法可以确切得到元素的个数。
- 数组顺序存储元素,而集合不一定。
- 集合以类的形式存在,具有封装、继承和多态等类的特性,通过简单的方法可以实现各种复杂的操作,大大提高软件开发效率。
Java集合框架
Java集合类库将接口与实现类分离。
Collection接口和Map接口是集合框架的两个根接口。
Collection接口
Collection接口扩展了Iterable接口,因此对于标准类库中的任何集合都可以使用"for each"循环。
Collection的子接口:
- Set接口,具有实现类HashSet和LinkedHashSet
- Set的子接口SortedSet接口,具有实现类TreeSet
- List接口,具有实现类LinkedList、Vector和ArrayList
List有序、可重复。
ArrayList由于底层是数组,所以各个元素在内存中存储位置是连续的,查询快,插入与删除慢。
LinkedList由于底层是链表,各结点通过指针连接起来,但是各个结点在内存中的位置不一定连续,插入与删除快,查询慢。
**Set无序、唯一。
ArrayList
ArrayList底层是Object类型动态数组实现的,与Java数组相比,其容量能够动态增长,本质上是定义新的更大的数组,将旧的数组内容拷贝到新数组来实现扩容。
//默认初始化容量为10
private static final int DEFAULT_CAPACITY = 10;
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
ArrayList中的size()方法是获取的集合的当前元素数量,并不是集合的容量(底层动态数组的长度)。源码中科院发现私有属性elementData用于存放数组元素,通过反射机制获取得到elementData的值并通过elementData.length就可以得到集合的容量。
public static Integer getCapacity(ArrayList list) {
Integer length = null;
Class c = list.getClass();
Field f;
try {
f = c.getDeclaredField("elementData");
f.setAccessible(true);
Object[] o = (Object[]) f.get(list);
length = o.length;
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return length;
}
新创建一个集合对象,不添加元素是,其容量和大小都是0,添加一个元素之后,其容量为10,大小为1。
ArrayList list = new ArrayList<>();
System.out.println("容量:" + getCapacity(list));
System.out.println("大小:" + list.size());
list.add(8);
System.out.println("容量:" + getCapacity(list));
System.out.println("大小:" + list.size());
当往该集合对象中添加的元素为11个,即超过了默认容量大小时,会进行扩容,新的容量为原来容量的1.5倍。例如容量依次为10,15,22....。
ArrayList继承了AbstractList类,实现了List、RandomAccess、Cloneable、java.io.Serializable接口。
ArrayList实现了RandomAccess接口,RandomAccess是一个标志接口,表明实现这个接口的List结合是支持快速随机访问的,在ArrayList中,可以通过元素的序号快速获取元素对象,即快速随机访问。
ArrayList实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
ArrayList实现了java.io.Serializable接口,意味着ArrayList支持序列化,能通过序列化去传输。
ArrayList线程不安全,效率高。
toArray()方法用法
List list = new ArrayList<>();
list.add("good");
list.add("thing");
String[] str = (String[]) list.toArray();
System.out.println(str.toString());
以上使用会报错 java.base/[Ljava.lang.Object; cannot be cast to java.base/[Ljava.lang.String;。
List list = new ArrayList<>();
list.add("good");
list.add("thing");
String[] str = new String[list.size()];
list.toArray(str);
以上使用可以正确将集合对象转成一个新的数组。
LinkedList
LinkedList底层是一个双向链表,实现了List、Deque、Cloneable、java.io.Serializable接口。
LinkedList底层的链表结构使它支持高校的插入和删除操作,另外实现了Deque接口,使得LinkedList类具有队列的特性。
LinkedList线程不安全,如果使其对象线程安全,可调用静态类Collections类中的synchronizedList方法:
List list = Collections.synchronizedList(new LinkedList(...));
可利用LinkedList实现栈(stack)、队列(queue)、双向队列(doubie-ended queue)。常用方法有addFirst()、addLast()、getFirst()、getLast()、removeFirst()、removeLast()等。
源码分析
LinkedList类中有一个内部私有类Node:
private static class Node {
E item; //结点值
Node next; //后继结点
Node prev; //前驱结点
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
用已有的集合创建链表的构造方法:
public LinkedList(Collection extends E> c) {
this();
addAll(c);
}
具体使用方法:
List list = new ArrayList<>();
list.add("good");
list.add("thing");
LinkedList list2 = new LinkedList(list);
System.out.println(list2.getFirst() + list2.getLast());
将已有集合插入到链表尾部:
public boolean addAll(Collection extends E> c) {
return addAll(size, c);
}
将已有集合插入到链表指定位置:
- 检查index范围是否在size之内;
- toArray()方法把集合的数据存入对象数组中;
- 得到插入位置的前驱和后继结点;
- 遍历数据,插入到指定位置。
public boolean addAll(int index, Collection extends E> c) {
checkPositionIndex(index); //检查index范围是否在size之内
Object[] a = c.toArray(); //把集合的数据存入对象数组中
int numNew = a.length;
if (numNew == 0)
return false;
Node pred, succ; //插入位置的前驱和后继结点
if (index == size) { //如果插入位置为尾部,前驱结点为last,后继结点为null
succ = null;
pred = last;
} else { //否则调用node()方法得到后继结点,再得到前驱结点
succ = node(index);
pred = succ.prev;
}
//遍历数据将其插入链表中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node newNode = new Node<>(pred, e, null); //创建新结点
if (pred == null) //插入位置在链表头部
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//插入位置在链表尾部
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
具体使用方法如下:
List list = new ArrayList<>();
list.add("good");
list.add("thing");
List list2 = new LinkedList<>();
list2.add("Southeast");
list2.add("University");
list2.addAll(1, list);
for (String s : list2)
System.out.println(s);
输出list2为["Southeast","good","thing","University"]。
获取头结点(index = 0)数据的方法有getFirst()、element()、peek()、peekFirst()。
前两种方法在链表为空时抛出NoSuchElementException异常,后两种方法返回null。
获取尾结点(index = -1)数据方法有getLast()和peekLast()
前者在链表为空是抛出NoSuchElementException异常,后者返回null。
根据对象获取索引的方法有:
public int indexOf(Object o) {...} //从头遍历
public int lastIndexOf(Object o) {...} //从尾遍历
删除结点:
删除头结点的方法有E remove()、E removeFirst()和E pop()。
删除尾结点的方法有E removeLast()和E pollLast(),前者在链表为空时抛出NoSuchElementException异常,后者返回null。
删除指定元素的方法boolean remove(Object o),一次只能匹配并删除一个。
删除指定位置元素的方法E remove(int index)。
Vector
Vector与ArrayList类似,底层也都是数组实现,但是相关方法都加了同步检查,因此线程安全,效率低。