常用集合
Java 中为了方便用户操作各个数据结构, 所以引入了类集的概念,有时候就可以把类集称为 java 对数据结构的实现。首先先简单介绍有哪些常用的数据结构:
常用的数据结构
栈(Stack)
栈(stac),又称堆栈, 栈(stack)是限定仅在表尾进行插入和删除操作的线性表。我们把允许插 入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为先进后出 的线性表 。
- 先进后出:先进入栈的元素后取出。进栈存元素又叫压栈(push),取出元素又叫弹栈(pop)。
- 栈的入口、出口的都是栈的顶端位置。
队列(queue)
队列(queue),简称队, 队列是一种特殊的线性表,是运算受到限制的一种线性表,只允许在表的 一端进行插入,而在另一端进行删除元素的线性表。队尾(rear)是允许插入的一端。队头(front)是 允许删除的一端。空队列是不含元素的空表
- 先进先出:先进入队列存储的元素先取出。
- 队列的入口、出口各占一侧。
数组(array)
数组(Array)是有序的存放相同元素的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。可以通过数组下标进行数据的存储和取出。
- 查找元素快:数组通过索引(下标)可以快速访问指定元素。
- 增删元素慢:指定索引增加或删除元素,需要创建一个新的数组,在指定位置增加元素或删除元素,把原数组根据索引复制到型数组对于索引的位置。
链表(linked list)
链表(linked list),由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动 态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的 指针域。我们常说的链表结构有单向链表与双向链表。
- 增删元素快
- 查找元素慢
单链表
单链表 (Linked List):由各个内存结构通过一个 Next 指针链接在一起组成,每一个内存结构都存在后继内存结构(链尾除外),内存结构由数据域和 Next 指针域组成。
单向数据结构用代码表示如下:
public class Node {
Object data; //存储的数据
Node next; //存储的下一个节点的地址
}
双向链表
双向链表 [Double Linked List]:由各个内存结构通过指针 Next 和指针 Prev 链接在一 起组成,每一个内存结构都存在前驱内存结构和后继内存结构(链头没有前驱,链尾没有后 继),内存结构由数据域、Prev 指针域和 Next 指针域组成。
链表数据结构用代码实现如下:
public class Node {
Node pev; //存储的下一个节点的地址
Object data; //存储的数据
Node next; //存储的下一个节点的地址
}
循环链表
循环链表 单向循环链表 [Circular Linked List] : 由各个内存结构通过一个指针 Next 链接在一起 组成,每一个内存结构都存在后继内存结构,内存结构由数据域和 Next 指针域组成。
双向循环链表 [Double Circular Linked List] : 由各个内存结构通过指针 Next 和指针 Prev 链接在一起组成,每一个内存结构都存在前驱内存结构和后继内存结构,内存结构由 数据域、Prev 指针域和 Next 指针域组成。
二叉树(tree)
二叉树:binary tree ,是每个结点不超过2的有序树(tree) 。二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。
二叉树又有平衡二叉树、红黑树等特殊的数据结构,在这里不做详细说明。
二叉树数据结构用代码实现:
class Node2{
Object data; //存放数据
Node2 leftNode; //存放左节点
Node2 rightNode; //存放右节点
}
选择合适的数据结构进行数据存储能大大提升效率,为便于使用数据结构,java实现不同的数据结构,并引入类集 的概念,结构如下图。
Collection
Collection 接口是在整个 Java 类集中保存单值的最大操作父接口,里面每次操作的时候都只能保存一个对象的数据。此接口定义:
public interface Collection<E> extends Iterable<E> // 引入的范型,便于类集的更安全操作
定义的方法如下:
方法 | 描述 |
---|---|
``````int size(); | 返回集合中元素的数量 |
boolean isEmpty(); | 如果该集合不包含元素则返回true |
boolean contains(Object o); | 如果集合包含指定的元素,返回true |
Iterator iterator(); | 返回集合的迭代器Iterator |
Object[] toArray(); | 将集合转换为数组,包含集合所有元素 |
T[] toArray(T[] a); | 将集合转换为指定类型的数组,包含集合所有元素 |
boolean add(E e); | 集合添加元素,添加成功返回true |
boolean remove(Object o); | 从集合移除指定元素,成功返回true |
boolean containsAll(Collection<?> c); | 如果集合包含指定集合全部元素则返回true |
boolean addAll(Collection<? extends E> c); | 将指定集合中的所有元素添加到此集合,成功返回true |
boolean removeAll(Collection<?> c); | 移除指定集合中的所有元素,成功返回true |
boolean retainAll(Collection<?> c); | 仅保留指定集合中的所有元素,移除其他元素,成功返回true |
void clear(); | 清空集合 |
boolean equals(Object o); | 比较指定对象与此集合是否相等,相等返回true |
int hashCode(); | 返回集合的哈希值 |
在编写程序时一般不会直接使用Collection,而是使用其子接口:List和Set
List接口继承了Colelction
public interface List<E> extends Collection<E>
Set接口继承了Collection
public interface Set<E> extends Collection<E>
List
List接口是Colletion接口的子类,继承了Collection所有方法,也根据自身特点(元素有序,元素可重复),也有一些特有的方法:
方法 | 描述 |
---|---|
boolean addAll(int index, Collection<? extends E> c); | 在指定位置插入指定集合的所有元素 |
void add(int index, E element); | 在指定位置插入元素 |
E get(int index); | 获取指定索引的元素 |
E set(int index, E element); | 将列表中指定位置的元素替换为指定的元素,返回被替换的元素 |
E remove(int index); | 删除指定索引的元素,返回被删除的元素 |
int indexOf(Object o); | 返回元素第一次出现的索引位置。不存在返回-1; |
int lastIndexOf(Object o); | 返回元素最后一次出现的索引位置。不存在返回-1; |
ListIterator listIterator(); | 返回ListIterator实例 |
ListIterator listIterator(int index); | 从指定索引位置返回ListIterator实例 |
List subList(int fromIndex, int toIndex); | 返回指定返回的子集合(包含起始索引,不包含终点索引) |
List为接口不能直接使用,需使用其实现类,常用实现类有:ArrayList、Vector、LinkedList,其中:ArrayList和Vector是数组实现,查找快,增删慢;LinkedList是链表实现,查找慢,增删快。需根据实际业务数据需求选择合适类。
ArrayList
根据源码,ArralList继承了 AbstractList 类。AbstractList 是 List 接口的子类。AbstractList 是个抽象类,适配器设计模式。暂不对设计模式进行说明。设计模式学习地址:Java设计模式:23种设计模式全面解析
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
构造方法 | 描述 |
---|---|
ArrayList(); | 构造一个初始长度为10的空列表 |
ArrayList(int initialCapacity) ; | 构造一个指定大小的空列表 |
ArrayList(Collection<? extends E> c); | 构造一个包含指定集合元素的列表 |
这里有个问题,根据ArrayList无参构造源码,最初构造的ArrayList是一个空的数组,长度为0,为什么API描述是构造一个长度为10的空列表呢?因为构造的ArrayList第一次进行进行添加元素时进行了扩容,变成了默认长度10,结合源码理解。
例:
public class ArrayListTest {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>(); //初始容量为0
arrayList.add(10); //第一次添加元素进行了扩容,变成了10
}
}
初始源码:
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
源码解读:
ArrayList底层是一个数组,初始构造的是一个为空的数组,容量为0
-------------------------------------------------------------------------------------
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
源码解读:
添加元素,返回true;
-------------------------------------------------------------------------------------
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
源码解读:
添加元素的算法:先进行容量判断,s == elementData.length ? elementData = grow(): ;
列表存满(底层数组存满)时需进行扩容(对于初始创建的列表,底层的数组长度为0,也需要进行扩容)。否则正常储存,size加1。(size是已储存元素的个数)
-------------------------------------------------------------------------------------
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
源码解读:
所需最小的长度为当前长度size+1,用newCapacity(minCapacity)方法计算新的长度。调用Arrays.copyOf(elementData,newCapacity(minCapacity))进行底层数组扩容。
------------------------------------------------------------------------------------
private static final int DEFAULT_CAPACITY = 10;
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0) ?
newCapacity :
hugeCapacity(minCapacity);
}
源码解读:
oldCapacity为列表原来的长度,newCapacity为原来列表长度的 1.5 倍
如果新长度小于等于就长度,判断原来的数组是否为0(是否为新创建),是再返回DEFAULT_CAPACITY=10
和minCapacity=1的最大值即是DEFAULT_CAPACITY=10。
不为0则判断是否容量是否溢出。
应用举例:
public class ArrayListDemo02 {
public static void main(String[] args) {
List<String> all = new ArrayList<String>(); // 实例化List对象,并指定泛型类型
all.add("hello "); // 增加内容,此方法从Collection接口继承而来
all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的
all.add("world"); // 增加内容,此方法从Collection接口继承而来
all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的
all.remove("world");// 删除指定的对象
System.out.print("集合中的内容是:");
for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来
System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的
}
}
}
Vector
Vector也是实现了List接口,用法与ArrayList基本一样
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
应用举例:
public class VectorDemo01 {
public static void main(String[] args) {
List<String> all = new Vector<String>(); // 实例化List对象,并指定泛型类型
all.add("hello "); // 增加内容,此方法从Collection接口继承而来
all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的
all.add("world"); // 增加内容,此方法从Collection接口继承而来
all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的
all.remove("world");// 删除指定的对象
System.out.print("集合中的内容是:");
for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来
System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的
}
}
}
ArrayList与Vector的区别:ArrayList是线程不安全的,Vector是线程安全的。
LinkedList
LinkedList底层是双向链表的实现,内部实现了双向链表结构:
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;
}
}
因此出来实现了List的方法外,还有大量操作收尾元素的方法,可用于实现列表、栈等数据结构。
方法 | 描述 |
---|---|
public E getFirst(); | 返回列表中的第一个元素,如果为null,抛出异常NoSuchElementException() |
public E getLast(); | 返回列表中的最后一个元素,如果为null,抛出异常NoSuchElementException() |
public E removeFirst(); | 移除并返回列表中的第一个元素,如果为null,抛出异常NoSuchElementException() |
public E removeLast(); | 移除并返回列表中的最后一个元素,如果为null,抛出异常NoSuchElementException() |
public void addFirst(E e) | 在列表的开头插入指定的元素 |
public void addLast(E e) | 将指定的元素追加到列表的末尾 |
public E peek(); | 返回列表中的第一个元素,如果为null,返回null |
public E element(); | 返回列表中的第一个元素,如果为null,抛出异常NoSuchElementException() |
public E poll(); | 移除并返回列表中的第一个元素,如果为null,返回null |
public E remove(); | 移除并返回列表中的第一个元素,如果为null,抛出异常NoSuchElementException() |
public boolean offer(E e); | 添加指定的元素作为这个列表的尾部(最后一个元素),返回true |
public boolean offerFirst(E e); | 在列表的前面插入指定的元素,返回true |
public boolean offerLast(E e); | 在列表的末尾插入指定的元素,发回true |
public E peekFirst(); | 返回列表中的第一个元素,如果为null,返回null |
public E peekLast(); | 返回列表中的最后一个元素,如果为null,返回null |
public E pollFirst(); | 移除并返回列表中的第一个元素,如果为null,返回null |
public E pollLast(); | 移除并返回列表中的最后一个元素,如果为null,返回null |
public void push(E e); | 将元素插入到列表的最前面 |
public E pop() ; | 移除并返回列表中的第一个元素,如果为null,抛出异常NoSuchElementException() |
Set
Set 接口也是 Collection 的子接口,与 List 接口最大的不同在于,Set 接口里面的内容是不允许重复的。 Set 接口并没有对 Collection 接口进行扩充,基本上还是与 Collection 接口保持一致。
Set为接口,常使用其实现类HashSet、TreeSet、LinkedHashSet(为HashSet的子类)。
HashSet
HashSet属于散列的存放类集,里面的内容是无序存放的。是底层是哈希表(HashMap)实现。
public HashSet() {
map = new HashMap<>();
}
HashSet不能存储重复元素,是否为重复元素:先比较hashcoed,hashcode不一样则是不重复;hashcode一样,则进行equals比较内容,equals不一样,则不重复;一样则重复。
add方法源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object();
源码解读:调用map.put进行添加,添加的元素e为map的key,而PRESENT为默认创建的对象。
HashSet是无序存放的,因此也没有索引,那如何进行遍历呢?
**方法1:**调用toArray方法转换成数组,在对数组进行循环遍历
**方法2:**使用迭代器进行遍历
**方法3:**使用增强for循环
从结果来看HashSet也能说明无序存储)
public class HashSetTest {
public static void main(String[] args) {
HashSet<Character> hashSet = new HashSet<>();
hashSet.add('a');
hashSet.add('v');
hashSet.add('b');
hashSet.add('d');
hashSet.add('e');
//法1:转换成数组进行遍历
Object[] nums = hashSet.toArray();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
System.out.println("----------------------分割线-----------------------");
//法2:使用迭代器进行遍历
Iterator<Character> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("----------------------分割线-----------------------");
//法3:使用增强for循环
for (Character character : hashSet) {
System.out.println(character);
}
}
}
LinkedHashSet
LinkedHashSet是HashSet的子类,其内部实现是LinkedHashMap,是哈希表和链表实现,具有可预测的迭代顺序。
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
将以上案例用LinkedHashSet实现,可以看到遍历打印输出的结果和添加的顺序保持一致。这就是具有可预测的迭代顺序。
public class HashSetTest {
public static void main(String[] args) {
HashSet<Character> hashSet = new LinkedHashSet<>();
hashSet.add('a');
hashSet.add('v');
hashSet.add('b');
hashSet.add('d');
hashSet.add('e');
//法1:转换成数组进行遍历
Object[] nums = hashSet.toArray();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
System.out.println("----------------------分割线-----------------------");
//法2:使用迭代器进行遍历
Iterator<Character> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("----------------------分割线-----------------------");
//法3:使用增强for循环
for (Character character : hashSet) {
System.out.println(character);
}
}
}
TreeSet
TreeSet存储的内容是具有自然顺序的,对于存储自定义的对象是,需实现Comparable接口。TreeSet底层是实现TreeMap。
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
public TreeSet() {
this(new TreeMap<>());
}
源码解读:
class TreeMap<K,V> implements NavigableMap<K,V>
interface NavigableMap<K,V> extends SortedMap<K,V>
SortedMap<K,V>接口中定义了Comparator<? super E> comparator()方法,保证了存储数据是有顺序的。
对于具有自然顺序的数字,字母等对象,可以直接存储;对于自定义的对象如Person,需实现Comparable接口,定义对象的比较规则。
**Comparable接口与Comparator接口比较:
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码 实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进 行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator:强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构 (如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
将以上案例用TreeSet实现,通过输出可以看到是具有自然顺序的。
public class HashSetTest {
public static void main(String[] args) {
TreeSet<Character> treeSet = new TreeSet<>();
treeSet.add('a');
treeSet.add('v');
treeSet.add('b');
treeSet.add('d');
treeSet.add('e');
//法1:转换成数组进行遍历
Object[] nums = treeSet.toArray();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
System.out.println("--------------------------分割线--------------------------");
//法2:使用迭代器进行遍历
Iterator<Character> iterator = treeSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("--------------------------分割线--------------------------");
//法3:使用增强for循环
for (Character character : treeSet) {
System.out.println(character);
}
}
}