一、概述
1.1 数组与集合
数组就是一种常见容器,它可以存放对象对象或基本类型数据。
集合也是一种容器,它可以存放对象或基本类型的包装类(Integer、Double、Char等等)。
数组的优点: 是一种简单的线性序列,可以快速访问数组元素,效率高。如果从效率和类型检查的角度来说,数组是最好的。
数组的缺点: 不灵活。容量需要事先定义好,不能随着需求的变化而扩容。
但是我们的开发又不可能离开数组,所以最初就只能依靠一些数据结构来实现动态的数组处理,其中最为重要的两个结构:链表、数组,但是面对这些数据结构的实现又不得不面对如下的问题:?
-
数据结构的代码实现困难,对于一般的开发者是无法进行使用的。
-
对于链表或二叉树当进行更新处理的时候维护是非常麻烦的。
-
对于链表或二叉树还需要尽可能保证其操作的性能。
正是因为这样的原因,所以从JDK1.2开始Java引入了集合的概念,主要就是对常见的数据结构进行完整的实现包装,并且提供了一些列接口和实现类来帮助用户减少数据结构所带来的开发困难。
最初的集合实现由于Java本身的技术所限,所以对数据的控制并不严格,全部采用了Object类型进行数据的接收;在JDK1.5之后由于泛型技术的推广,集合框架也得到了良好的改进,可以直接利用泛型来保存相同类型的数据;随着数据量的不断增加,从JDK1.8开始集合框架中的实现算法也得到了良好的性能提升。
在整个集合框架里面提供有如下几个核心接口:Collection、List、Set、Map、Iterator、Enumeration、Queue、ListIterator。
1.2 泛型
泛型是 JDK1.5之后增加的,它可以帮助我们建立类型安全的集合。提高了代码可读性和安全性。
泛型的本质就是 “数据类型的参数化”。 我们可以把 “泛型” 理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。这样,在存储数据、读取数据时都避免了大量的类型判断,非常便捷。
自定义泛型
我们可以在类的声明处增加泛型列表,如:<T,E,V>。
public class MyCollection<E> { //E:表示泛型
Object[] objs = new Object[5];
public void set(E obj, int index) {
objs[index] = obj;
}
public E get(int index) {
return (E) objs[index];
}
}
public class TestGenerics {
public static void main(String[] args) {
//这里的"String"就是实际传入的数据类型
MyCollection<String> mc = new MyCollection<String>();
mc.set("张三", 0);
mc.set("李四", 1);
//加了泛型,直接返回String类型数据,不需要强制转换
String str = mc.get(0);
System.out.println(str);
}
}
泛型 E
可以理解成占位符 , 表示 “未知的某个数据类型” ,需要我们在真正调用的时候传入这个 “数据类型” 。
集合中的泛型
集合框架相关的类都定义了泛型,这样在集合中存储数据、读取数据时都避免了大量的类型判断,非常便捷。
//以下代码中List、Set、Map、Iterator都是与集合相关的接口
List list = new ArrayList();
Map map = new HashMap();
Set set = new HashSet();
Iterator iterator = set.iterator();
通过阅读源码,我们发现Collection、List、Set、Map、Iterator接口都定义了泛型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2aKJyE8h-1656678014184)(Java集合框架.assets/image-20220628105326757.png)]
二、Collection接口
java.util.Collection是单值集合操作的最大的父接口,在该接口中定义了单值数据的所有操作。
Collection常用的核心方法:
方法名称 | 功能 |
---|---|
boolean add(E e) | 添加数据 |
boolean addAll(Collection<? extends E> c) | 追加一组数据,到本集合 |
void clear() | 清空集合,让根节点为空,同时执行GC处理 |
boolean contains(Object o) | 在当前集合中,该数据是否存在 (需要equals方法支持) |
boolean isEmpty() | 集合是否为空 |
boolean remove(Object o) | 删除数据(需要equals方法支持) |
int size() | 获取集合中元素的数量 |
Object[] toArray() | 将集合变成Object数组返回 |
Iterator iterator() | 将集合变成Iterator接口对象 |
在进行集合操作的时候有两个方法最为常用:【添加数据】add()、【输出数据】iterator(),在JDK1.5版本之前Collection只是一个独立的接口,但是从JDK1.5之后提供了Iterable父接口,并且在JDK1.8之后 Iterable 接口也得到一些扩充。
但是往往我们玩的都是Collection的两个子接口: List(有序可重复)、Set(无序不可以重复)接口。
2.1 List接口
List是Collection的子接口,特点:它是一个有序集合,可以保存重复的元素数据。
该接口的定义如下:
public interface List<E> extends Collection<E> {
...
}
List对Collection接口进行了扩充:
方法名称 | 功能 |
---|---|
E get(int index) | 获取指定索引上的数据 |
E set(int index) | 修改指定索引上的数据 |
ListIterator listIterator() | 返回ListIterator接口对象 |
List接口有三个实现类:ArrayList(使用最多)、LinkedList、Vector。
ArrayList
ArrayList是List接口的实现类,底层基于数组实现(顺序存储、元素可重复)。
ArrayList特点:查询效率高O(1),增删效率低,线程不安全。
ArrayList继承结构如下所示:
1、ArrayList定义如下:
// 继承了AbstractList抽象类
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//默认元素集合
private static final Object[] EMPTY_ELEMENTDATA = {};
//无参构造实例默认元素集合
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的数组
transient Object[] elementData;
//记录ArrayList中存储的元素的个数
private int size;
...
}
2、ArrayList的构造方法:
// 默认构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //空数组
}
// 可以指定容量大小的构造器
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);
}
}
// 传入一个Collection对象
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
三个构造方法目的都是为初始化elementData,一般我们使用无参构造器即可。如果你已经确定了容量大小可以使用第二种构造方法(超过10个就用这种方法),避免频繁扩容。
3、add()方法:
//从尾部插入数据
public boolean add(E e) {
// 判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 在数组后面添加元素e
elementData[size++] = e;
return true;
}
ensureCapacityInternal()方法:判断是否扩容。
// 得到最小的扩容量
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// /如果所需容量大于现数组容量,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
4、grow()方法是ArrayList扩容的核心方法:
// 扩容机制
private void grow(int minCapacity) {
// 老容量长度
int oldCapacity = elementData.length;
// 新容量长度 = 老容量长度+(老容量长度/2),右移一位相当于除以2
// 先扩容1.5倍,当增加后还是不够用,则直接使用所需要的长度作为数组的长度。
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数组拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
如果在实例化ArrayList对象的时候没有传递初始化的长度,那么它默认会使用一个空数组;当插入数据发现数组容量不够时,则会判断当前增长的容量与默认的容量大小,较大的一个数值作为新的数组开辟。所以得出一个结论:
版本 | 说明 |
---|---|
JDK1.9之后 | ArrayList默认的构造器,只会使用默认的空数组,使用的时候才会开辟数组(比如 add 后进行扩容),默认开辟长度为10。 |
JDK1.9之前 | ArrayList默认的构造器,实际上就会默认开辟大小为10的数组。 |
5、remove()方法:
// 删除指定内容的数据
public boolean remove(Object o) {
if (o == null) {
// 删除数组中为null的数据
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index); // 删除
return true;
}
} else {
// 删除数组中指定的数据
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index); // 删除
return true;
}
}
return false;
}
通过循环遍历元素是否相等然后进行删除。因为ArrayList 允许空值,所以源码这里进行了多一次的判断是否为null的情况。可以看到核心删除方法是fastRemove。
private void fastRemove(int index) {
modCount++;
// 删除元素的位置
int numMoved = size - index - 1;5
if (numMoved > 0)
//数组拷贝,元素向前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// size-1,最后一个元素赋值null
elementData[--size] = null;
}
6、基于数组简单实现ArrayList:
public class MyArrayList {
// 存储数据的数组
public Object[] arr;
// 数组中存储元素的个数
private int size;
// 数组的容量
private int capacity;
/**
* 无参构造器,数组默认容量大小为10
*/
public MyArrayList() {
this(10);
}
/**
* 有参构造器,自定义数组的容量大小
*
* @param capacity 初始容量
*/
public MyArrayList(int capacity) {
arr = new Object[capacity];
this.capacity = capacity;
}
/**
* 在数组末尾追加元素
*
* @param data 添加的数据
* @return
*/
public boolean add(Object data) {
// 判断是否需要扩容
ensureCapacity(size + 1);
arr[size++] = data;
return true;
}
/**
* 在指定位置添加元素
*
* @param index 索引
* @param data 数据
* @return
*/
public boolean add(int index, Object data) {
// 判断是否需要扩容
ensureCapacity(size + 1);
// 元素位移
/*
[1,2,3,4]
[1,2,?,3,4]
*/
System.arraycopy(arr, index, arr, index + 1, size - index);
arr[index] = data;
size++;
return true;
}
/**
* 扩容方法
*
* @param needCapacity 接收的容量
*/
private void ensureCapacity(int needCapacity) {
// 如果元素个数大于容量,则扩容
if (needCapacity > capacity) {
capacity = capacity + (needCapacity >> 1);
Object[] newArr = new Object[capacity];
// 数组拷贝
System.arraycopy(arr, 0, newArr, 0, arr.length);
arr = newArr;
}
}
/**
* 重写toString方法
*
* @return
*/
public String toString() {
StringBuilder builder = new StringBuilder();
if (size == 0) {
builder.append("[]");
} else {
builder.append("[");
for (int i = 0; i < size; i++) {
builder.append(arr[i] + ",");
}
builder.setCharAt(builder.length() - 1, ']');
}
return builder.toString();
}
/**
* 删除数组末尾的元素
*
* @return
*/
public Object remove() {
if (size > 0) {
return arr[size--];
} else {
return null;
}
}
/**
* 删除数组指定位置的元素
*
* @param index 索引
* @return
*/
public Object remove(int index) {
// 判断下标是否合法
boolean flag = checkRange(index);
if (flag) {
// 拿到这个元素
Object res = arr[index];
// 元素向前移动一位
/*
[1,(2),3,4]
[1,3,4]
*/
System.arraycopy(arr, index + 1, arr, index, size - index - 1);
size--;
return res;
}
return null;
}
/**
* 判断下标是否合法
*
* @param index
* @return
*/
private boolean checkRange(int index) {
return (index >= 0 && index <= size - 1);
}
/**
* 获取元素的个数
*
* @return
*/
public int size() {
return size;
}
/**
* 判断是否有数据
*
* @return
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 修改指定位置的元素
*
* @param index
* @param data
* @return
*/
public boolean set(int index, Object data) {
// 判断下标是否合法
boolean flag = checkRange(index);
if (flag) {
arr[index] = data;
return true;
}
return false;
}
/**
* 获取指定位置的元素
*
* @param index
* @return
*/
public Object get(int index) {
// 判断下标是否合法
boolean flag = checkRange(index);
if (flag) {
return arr[index];
}
return null;
}
public static void main(String[] args) {
MyArrayList arr1 = new MyArrayList();
arr1.add("111");
arr1.add(2);
arr1.add(2, 222);
arr1.add('a');
arr1.remove(1);
System.out.println("arr1 --> size:" + arr1.size + "\tcapacity:" + arr1.capacity);
System.out.println(arr1);
arr1.set(0, 'a');
System.out.println(arr1.get(0));
MyArrayList arr2 = new MyArrayList(5);
System.out.println("arr2 --> size:" + arr2.size + "\tcapacity:" + arr2.capacity);
System.out.println(arr2);
System.out.println(arr2.isEmpty());
}
}
LinkedList
LinkedList也是List接口的实现类,底层基于链表实现。
特点:插入或删除效率高,查找效率低O(n),不用考虑容量的问题,线程不安全。
LinkedList继承结构如下所示:
示例:使用LinkedList实现集合操作。
public class LinkedListDemo {
public static void main(String[] args) {
List<String> list = new LinkedList<>();
list.add("CSDN");
list.add("CNBLOG");
list.add("GitHub");
list.add("B站");
list.add("CSDN");
System.out.println(list);
}
}
可以发现LinkedList和ArrayList的使用方式是一样的,但它们的实现机制是不同的。
1、LinkedList的定义如下:
2、add()方法
// 添加元素e
public boolean add(E e) {
// 接追加到末尾
linkLast(e);
return true;
}
linkLast()方法:从尾部开始追加节点
void linkLast(E e) {
// 暂存尾节点
final Node<E> l = last;
// 新建节点,l是新节点的上一个节点,e是新节点的值,null是新节点的下一个节点
final Node<E> newNode = new Node<>(l, e, null);
// 把新节点赋值给尾节点
last = newNode;
// 如果原尾节点是null,代表链表为空,把新节点赋值给头结点
if (l == null)
first = newNode;
else
// 新节点变成原尾节点的下一个节点
l.next = newNode;
size++;
modCount++;
}
3、remove()方法
// 按值删除
public boolean remove(Object o) {
// 如果值为null,就遍历链表。如果链表的节点值也是null,就删除该节点
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 如果值不为null,就遍历链表,跟链表节点值比较,如果相等,就删除该节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
// 核心删除方法
unlink(x);
return true;
}
}
}
return false;
}
// 按下标删除
public E remove(int index) {
// 校验下标是否越界
if (!(index >= 0 && index < size)) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
return unlink(node(index));//核心删除方法
}
// 获取该下标的节点
Node<E> node(int 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;
}
}
删除核心方法:
// 删除节点核心方法
E unlink(Node<E> x) {
// 暂存当前节点数据、下一个节点和上一个节点
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 如果上一个节点为null,表示要删除的节点是头结点,下一个节点就变成头结点
if (prev == null) {
first = next;
} else {
// 否则,就断开和上一个节点的连接
prev.next = next;
x.prev = null;
}
// 如果下一个节点为null,表示要删除的节点是尾节点,上一个节点就变成尾节点
if (next == null) {
last = prev;
} else {
// 否则,就断开和下一个节点的连接
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
4、get()方法
// 根据下标查找
public E get(int index) {
// 校验下标是否越界
if (!(index >= 0 && index < size)) {
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// node()方法:如果下标是链表的前半部分,就从头开始查找,否则从尾部开始查找
return node(index).item;
}
5、单项链表:
节点操作关系:
class Node<E> {
// 当前节点存储的数据
private E data;
// 存储下一个节点的引用
private Node next;
public Node(E data) {
this.data = data;
}
public E getData() {
return this.data;
}
public Node getNext() {
return this.next;
}
public void setNext(Node<E> next) {
this.next = next;
}
}
public class LinkDemo {
public static void main(String[] args) {
Node<String> n1 = new Node<>("火车头");
Node<String> n2 = new Node<>("车厢1");
Node<String> n3 = new Node<>("车厢2");
Node<String> n4 = new Node<>("车厢3");
Node<String> n5 = new Node<>("车厢4");
n1.setNext(n2);
n2.setNext(n3);
n3.setNext(n4);
n4.setNext(n5);
print(n1);
}
public static void print(Node<?> node) {
// 如果node不为null,就打印当前节点的数据
if (node != null) {
System.out.println(node.getData());
// 递归调用
print((Node<?>) node.getNext());
}
}
}
控制台输出结果:
火车头
车厢1
车厢2
车厢3
车厢4
简单实现单项链表:
package com.baidou.oop.list;
/**
* 实现单项链表
*
* @author 白豆五
* @version 2022/6/28 22:01
* @since JDK8
*/
// 定义一个接口封装链表常用操作,并设置泛型避免安全隐患
interface MyLink<E> {
// 添加数据
public void add(E e);
// 获取数据的个数
public int size();
// 空集合判断
public boolean isEmpty();
// 将集合元素以数组方式返回
public Object[] toArray();
// 获取数据
public E get(int index);
// 修改数据
public void set(int index, E data);
// 判断指定数据是否存在
public boolean contains(E data);
// 删除指定内容的数据
public void remove(E data);
// 清空链表
public void clean();
}
class MyLinkImpl<E> implements MyLink<E> {
// 根节点
private Node root;
// 保存数据的个数
private int count;
// 操作数组的脚标
private int foot;
// 用来保存返回的数据
private Object[] returnData;
/**
* (尾部)添加数据
*
* @param e
*/
@Override
public void add(E e) {
// 如果添加的数据为null,就直接返回
if (e == null) {
return;
}
// 关联处理
// 创建一个新的节点
Node newNode = new Node<>(e);
// 如果根节点为null,表示链表为空,则把新节点的数赋值给根节点
if (this.root == null) {
this.root = newNode;
} else {
// 根节点存在
// 将新节点保存到合适的位置
this.root.addNode(newNode);
}
this.count++; // 插入成功就追加count
}
/**
* 获取数据的大小
*
* @return
*/
@Override
public int size() {
return this.count;
}
/**
* 空集合判断
*
* @return
*/
@Override
public boolean isEmpty() {
return this.count == 0;
}
/**
* 将集合元素以数组方式返回
*
* @return
*/
@Override
public Object[] toArray() {
// 空集合判断
if (this.root == null) {
return null;
}
// 脚标清零
this.foot = 0;
// 根据已有的大小开辟数组
this.returnData = new Object[this.count];
// 利用Node类进行递归数据
this.root.toArrayNode();
return this.returnData;
}
/**
* 根据索引获取数据
*
* @param index
* @return
*/
@Override
public E get(int index) {
// 判断索引应是否在指定的范围内
if (index > this.count) {
return null;
}
// 重置脚标foot
this.foot = 0;
// 处理通过下标获取Node中的数据
return (E) this.root.getNode(index);
}
/**
* 修改指定索引数据
*
* @param index
* @param data
*/
@Override
public void set(int index, E data) {
// 判断索引应是否在指定的范围内
if (index > this.count) {
return; // 方法结束
}
// 重置脚标foot
this.foot = 0;
// 修改数据
this.root.setNode(index, data);
}
/**
* 判断数据是否存在
*
* @param data
* @return
*/
@Override
public boolean contains(E data) {
if (data == null) {
return false;
}
// 进行数据判断
return this.root.containNode(data);
}
/**
* 删除指定内容的数据
*
* @param data
*/
@Override
public void remove(E data) {
// 判断数据是否存
if (this.contains(data)) {
// 如果删除的数据和根节点的数据相同,将后继节点变为根节点
if (data.equals(this.root.data)) {
this.root = this.root.next;
} else {
// 如果删除的不是根节点,进行节点删除处理
this.root.removeNode(this.root, data);
}
this.count--;
}
}
/**
* 清空链表
*/
@Override
public void clean() {
// 这样后续的所有节点都没了
this.root = null;
this.count = 0;
}
/**
* 保存节点的数据关系
* 内部类的私有属性,外部类可以直接访问
*
* @param <E>
*/
private class Node<E> {
// 存储当前节点中的数据
private E data;
// 保存下一个节点的引用
private Node next;
// 初始化当前节点的数据
public Node(E data) {
this.data = data;
}
/**
* 保存新的Node数据
*
* @param newNode
*/
public void addNode(Node newNode) {
/*
第一次调用:this = MyLinkImpl.root
第二次调用:this = MyLinkImpl.root.next
第三次调用:this = MyLinkImpl.root.next.next
*/
// 如果当前节点的下一个节点为空,保存当前节点
if (this.next == null) {
this.next = newNode;
} else {
// 通过递归找到合适的节点并插入
this.next.addNode(newNode);
}
}
/**
* 递归获取数据,存放到returnData中
*/
public void toArrayNode() {
// returnData[foot]=data;
MyLinkImpl.this.returnData[MyLinkImpl.this.foot++] = this.data;
if (this.next != null) {
this.next.toArrayNode();
}
}
/**
* 处理通过下标获取Node中的数据
*
* @return
*/
public E getNode(int index) {
// 索引相同,返回当前数据
if (MyLinkImpl.this.foot++ == index) {
return this.data;
} else {
// 不相同,向下处理
return (E) this.next.getNode(index);
}
}
/**
* 修改数据
*
* @param index
* @param data
*/
public void setNode(int index, E data) {
// 索引相同,返回当前数据
if (MyLinkImpl.this.foot++ == index) {
this.data = data;
} else {
// 不相同,向下处理
this.next.setNode(index, data);
}
}
/**
* 比较数据
*
* @param data
* @return
*/
public boolean containNode(E data) {
// 如果内容相同,返回true
if (data.equals(this.data)) {
return true;
} else {
// 如果没有后续节点,返回false
if (this.next == null) {
return false;
} else {
// 有后继节点就向下继续判断
return this.next.containNode(data);
}
}
}
/**
* 删除节点
*
* @param previous 前驱节点
* @param data
*/
public void removeNode(Node previous, E data) {
// 如果删除当前节点,将上一个节点与下一个节点进行连接
/*
[A]->[B]->[C]
[A]->[C]
*/
if (data.equals(this.data)) {
previous.next = this.next;
} else {
// 有后续节点
if (this.next != null) {
this.next.removeNode(this, data);
}
}
}
}
}
public class LinkTest {
public static void main(String[] args) {
MyLink<String> link = new MyLinkImpl<>();
link.add("hello");
link.add("world");
link.add("java");
link.clean();
Object[] results = link.toArray();
if (results != null) {
for (Object result : results) {
System.out.println(result);
}
}
}
}
2.2 Map 接口