集合
1. 集合的概念和作用
- 集合:是一个存储对象的容器。
- 集合的作用:由于java数组是定长容器,在无法提前知道元素数量的情况下易造成资源浪费或空间不足等不便,而集合是能够动态改变大小的容器,能够避免这种不便。
集合只能存储引用数据类型,若要存储基本数据类型可以通过使用基本数据类型的包装类来实现。集合可以存储不同类型的元素,但为了方便对集合元素的操作,一般存储同一类型。
2. 集合和数组的区别
- 数组:数组是定长容器,数组大小在初始化时就确定且不能再修改;数组可以存储基本数据类型和引用数据类型;数组中的元素类型必须统一。
- 集合:集合是变长容器,集合的大小可以随着存储元素的数量的增多而改变;集合只能存储引用数据类型,但可以通过基本数据类的包装类来存储基本数据类型;集合可以存储不同类型的元素。
3. 集合框架体系介绍
Java集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键-值对映射。Collection接口有两种子类型,List和Set,再往下是抽象类,最后是具体实现类,常用的具体实现类有ArrayList、LinkedList、HashMap等。
4. 集合框架之Collection接口
Colletion接口是集合的顶层接口,不能被实例化,常用的子接口有List和Set。Collection接口继承了Iterable接口,因此其底层实现类都能够通过Iterator迭代器来遍历集合。
- List集合:List集合是有序、可重复的集合,List对应实现的ListIterator接口提供了往前遍历、添加元素、设置元素的方法,List集合较常用的实现类有LinkedList和ArrayList。
- Set集合:Set集合是元素不可重复的集合,常用实现类有HashSet、TreeSet、LinkedHashSet。
5. 泛型的使用
泛型,即“参数化类型”,是将具体类型参数化,类似于方法中的形式参数,在使用时传入具体的类型。
package com.myh.day0715;
class Test<T> {
public T a;
Test(T a) {
this.a = a;
}
public String toString() {
return a.toString();
}
}
public class TTest {
public static void main(String[] args) {
Test<String> test1 = new Test<>("aaa");
Test<Integer> test2 = new Test<>(100);
System.out.println(test1);
System.out.println(test2);
}
}
aaa
100
6. ArrayList和LinkedList的方法使用
ArrayList是基于数组的集合实现类,LinkedList是基于链表的集合实现类,常用方法有:
- 构造方法:ArrayList有三种构造方法,第一种是无参构造方法,默认创建一个空数组;第二种是传入一个整型参数的构造方法,传入的整形参数是数组的初始大小;第三种是传入一个集合类参数的构造方法,若传入的集合为空则创建一个空数组,不为空则对传入的集合进行拷贝。LinkedList有两种构造方法,第一种是无参构造方法,创建一个空链表;第二种是传入一个集合类参数的构造方法,将传入集合中的所有元素都加入到链表中。
package com.myh.day0715;
import java.util.ArrayList;
import java.util.List;
public class ArrayListTest {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>(10);
List<String> list3 = new ArrayList<>(list1);
List<String> list4 = new LinkedList<>();
List<String> list5 = new LinkedList<>(list1);
}
}
- add方法:在集合中添加元素。ArrayList的add方法有两种,一种是在元素集合末端添加新元素,一种是在指定位置插入新元素。LinkedList的add方法是在链表尾部加入新元素。此外,ArrayList和LinkedList的addAll()方法能将一个集合的元素全部添加至集合中。
package com.myh.day0715;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ArrayListTest {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
System.out.println(list1);
list1.add(1,"ccc");
System.out.println(list1);
List<String> list2 = new LinkedList<>();
list2.add("ddd");
System.out.println(list2);
list2.addAll(list1);
System.out.println(list2);
}
}
[aaa, bbb]
[aaa, ccc, bbb]
[ddd]
[ddd, aaa, ccc, bbb]
- get方法:获取对应索引的元素,传入一个整型参数作为索引,返回对应位置的元素。
package com.myh.day0715;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ArrayListTest {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
List<String> list2 = new LinkedList<>(list1);
System.out.println(list1.get(1));
System.out.println(list2.get(1));
}
}
bbb
bbb
7. ArrayList和LinkedList各自的工作原理分析
* ArrayList
- 实现接口及成员变量:ArrayList类实现了RandomAccess, java.io.Serializable接口,表明了ArrayList对象是一个可随机访问、可序列化的对象。ArrayList类的成员变量有两个,一个是用来存储集合中的元素的Object数组,还有一个是用来记录集合中元素数量的整数。
transient Object[] elementData; // 存储集合中的元素
private int size; //ArrayList的元素个数
- 构造方法:由源码可知,ArrayList类中有两个空数组常量分别是EMPTY_ELEMENTDATA 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA。在调用构造方法时如果调用的是无参构造方法则会通过DEFAULTCAPACITY_EMPTY_ELEMENTDATA来创建数组,如果调用的是有参构造方法且数组初始长度为0则会通过EMPTY_ELEMENTDATA来创建数组。当往空数组中添加元素时,如果该空数组是由无参构造创建的,那么数组的容量会变为10,如果不是,则使用ArrayList中的扩容方法对空数组进行扩容。
//传入初始数组长度的有参构造函数
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);
}
}
//无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//传入集合元素的有参构造函数
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
- add方法:由于ArrayList内部的实现是基于数组的,所以当加入新元素后元素的个数大于内部数组的大小时,就需要对数组进行扩容。扩容方法是新创建一个容量为旧数组长度1.5倍的新数组,把旧数组的元素复制到新数组上,将类内部的数组变量指向新数组。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//新数组容量为旧数组的1.5倍
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);
}
- get方法:如果传入的索引在集合大小的范围内,则直接返回该索引位置的元素,否则抛出异常。
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
- 序列化:由ArrayList所实现的接口可知,ArrayList对象是可序列化的,但存储集合元素的数组elementData又被transient修饰,表明elmentData不能被序列化,所以ArrayList对象元素的序列化是通过遍历数组将数组内的元素逐一进行序列化来实现的。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// 先写入元素大小
s.writeInt(size);
// 将元素逐一序列化
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
* LinkedList
- 实现接口及成员变量:LinkedList类没有实现RandomAccess接口,但实现了java.io.Serializable接口和Deque接口,表明了LinkedList对象是一个不可随机访问、可序列化的对象,且LinkedList链表的首尾两端都能插入或删除数据。LinkedList类的成员函数有三个,其中两个是链表的头节点和尾节点,还有一个是用于记录集合元素个数的整数。链表节点的类型是一个内部类Node,每个Node节点包含了该节点元素的信息、前一个节点、后一个节点。
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
E item;
Node<E> next; // 记录该节点的前一个节点
Node<E> prev; // 记录该节点的后一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
- 构造方法:LinkedList的无参构造方法的方法体为空,因为集合元素为空的时LinkedList就是一个空链表。LinkedList的有参构造函数的参数是一个集合,在调用有参构造方法时需要把集合参数中的全部元素添加到链表尾部。
//无参构造函数
public LinkedList() {
}
//有参构造函数
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
- add方法:由于LinkedList是基于链表实现的集合,所以不存在由于元素增多而需要对数组进行扩容的情况。在加入新元素时,LinkedList只需把新元素添加至链表尾部即可。
public boolean add(E e) {
linkLast(e);
return true;
}
- get方法:由于链表不能随机访问某个位置的元素,所以在进行get操作时需要对链表进行遍历。如果传入的索引小于元素个数的一半,则说明所需要获取的元素更靠近链表头部,从头部开始遍历,反之则说明所需要获取的元素更靠近链表尾部,从尾部开始遍历。
Node<E> node(int index) {
// assert isElementIndex(index);
//如果索引小于元素个数的一半,则从头部开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { //反之则从尾部开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
- 序列化:与ArrayList类似,LinkedList的序列化并不是将整个链表进行序列化,而是遍历链表把每个节点所存储的元素进行序列化。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// 成员变量size是用transient修饰的,且在readObject方法中读入的size也没有实际作用
s.writeInt(size);
// 从头遍历链表,把每个节点存储的元素进行序列化
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}
8. Vector和Stack使用介绍
* Vector
Vector类是一个基于数组的List实现类,有四个构造函数,常用的方法有插入数据、删除数据、搜索数据。Vector类的操作方法几乎都用synchronized修饰。
package com.myh.day0715;
import java.util.Vector;
public class VectorTest {
public static void main(String[] args) {
//无参构造函数,默认初始容量为10
Vector vector1 = new Vector();
//传入初始容量大小的有参构造函数
Vector vector2 = new Vector(10);
//传入初始容量大小和每次扩充的扩充值的有参构造函数
Vector vector3 = new Vector(10, 0);
//传入初始元素集合的有参构造函数
Vector vector4 = new Vector(vector1);
//添加元素
vector1.addElement("a");
//添加元素并返回布尔型值
boolean add = vector1.add("b");
//在索引0处插入元素"c"
vector1.insertElementAt("c",0);
//将索引0处的元素设置为"ccc"
vector1.setElementAt("ccc", 0);
//删除元素"a",若存在多个则删除第一个
vector1.removeElement("a");
//删除所有对象
vector1.removeAllElements();
//删除指定位置的对象
vector1.removeElementAt(0);
//返回第一个元素"a"的下标
int a = vector1.indexOf("a");
//返回从位置1开始搜索的第一个元素"a"的下标
int a1 = vector1.indexOf("a", 1);
//从尾部开始搜索元素"a"
int a2 = vector1.lastIndexOf("a");
}
}
* Stack
Stack是Vector的子类,在Vector的基础上新增了push、pop、peek方法,且Stack类的方法也都是线程安全的(全部用synchronized修饰)。Stack栈是“先进后出”的原理,push()函数是将元素加入栈顶,pop()函数是把栈顶元素移除并返回,peek()方法是返回栈顶元素。
package com.myh.day0715;
import java.util.Stack;
public class StackTest {
public static void main(String[] args) {
Stack stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack);
stack.pop();
System.out.println(stack);
System.out.println(stack.peek());
}
}
[1, 2, 3]
[1, 2]
2
9. 使用多种方式遍历集合
List集合的遍历可以使用迭代器也可以通过下标获取。在使用迭代器遍历集合时要注意不要改变集合中元素的个数,否则可能会抛出IllegalStateException异常。
package com.myh.day0715;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TravelTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
// 1 使用迭代器遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()){
int i = (Integer) iterator.next();
System.out.print(i + " ");
}
System.out.println();
// 2 使用下标遍历
for(int i=0;i<list.size();i++){
System.out.print(list.get(i) + " ");
}
}
}
1 2 3
1 2 3
10. 迭代器的使用和工作原理
由于Set集合类是无序集合,无法像List集合一样通过循环get(index)来遍历集合,所以java提供了迭代器来对无序集合进行遍历。
迭代器遍历有while和for两种写法:
package com.myh.day0715;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class IteratorTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("1");
set.add("2");
set.add("3");
set.add("4");
Iterator<String> iterator = set.iterator();
// while写法
while (iterator.hasNext()) {
String next = iterator.next();
System.out.print(next + " ");
}
System.out.println();
// for写法
for (Iterator<String> iterator1 = set.iterator(); iterator1.hasNext(); ) {
String next = iterator1.next();
System.out.print(next + " ");
}
}
}
1 2 3 4
1 2 3 4
以ArrayList实现类为例,ArrayList的迭代器是通过一个内部类Itr实现的,Itr类实现了Iterator接口。Itr实现迭代器的三个核心方法是hasNext(),next(),remove()。hasNext()用于判断是否有没有元素没有被遍历,通过判断游标是否等于当前长度来实现;next()用于返回游标当前位置的元素,且游标位置+1;remove()用于删除游标左边的元素。
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
//如果游标左侧没有元素则抛出异常
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
11. HashSet和LinkedHashSet各自的工作原理分析
LinkedHashSet类是HashSet的子类,且没有对HashSet的操作方法进行重写。
HashSet类的实现是基于一个HashMap对象。
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
HashSet的操作方法有add(),remove(),clear(),contains()等均是基于HashMap对象的操作。
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
12. 集合框架之Map接口
Map集合存储的是键值对的映射,键唯一且每个键只能映射到一个值。Map接口提供的方法有添加元素、删除元素、判断是否包含指定的键或值、判断集合是否为空、获取集合中的所有键、获取集合中的所有值、根据键获取值、获取集合中键值对的对数,其实现类主要有HashMap,TreeMap,LinkedHashMap。
13. TreeMap的方法使用和排序方式
TreeMap类的主要操作方法有put()方法、get()方法、remove()方法。put()方法是往集合中加入键值对,如果集合中存在相同的key则更新key所对应的value;get()方法获取集合中key所对应的value;remove()方法会先在集合中找到对应的键值对然后进行删除。
package com.myh.day0715;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapTest {
public static void main(String[] args) {
Map map = new TreeMap();
map.put("a", 1);
map.put("b", 2);
System.out.println(map);
System.out.println(map.get("a"));
map.remove("b");
System.out.println(map);
}
}
{a=1, b=2}
1
{a=1}
TreeMap的排序方式是按照key的字典顺序来排序
14. 分析Set和Map之间的区别与联系
- 区别:Set存储的是一个个元素,Map存储的是一个个键值对。
- 联系:都可以存储不重复的值,有许多相同的操作方法,HashSet是HashMap的子类。
15. Collections工具类的使用
Colletions工具类有很多方法,下面举了几个Colletions工具类方法使用的例子:
package com.myh.day0715;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionsUtilTest {
public static void main(String[] args) {
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
System.out.println(list);
//反转list中的元素
Collections.reverse(list);
System.out.println(list);
//对list中的元素进行随机排序
Collections.shuffle(list);
System.out.println(list);
//对list中的元素升序排序
Collections.sort(list);
System.out.println(list);
//对list中第0个元素和第2个元素进行交换
Collections.swap(list,0,2);
System.out.println(list);
//将list的元素复制到list1中
List list1 = new ArrayList();
for (int i = 0; i < list.size(); ++i) {
list1.add(i);
}
Collections.copy(list1, list);
System.out.println(list1);
}
}
[1, 2, 3, 4]
[4, 3, 2, 1]
[1, 4, 2, 3]
[1, 2, 3, 4]
[3, 2, 1, 4]
[3, 2, 1, 4]