具体代码在 ArrayList , 欢迎
star
跳转到目录
数组是一种顺序存储
的线性表,所有元素的内存地址都是连续的。
int[] array = new int[]{20, 30, 40}
在很多编程语言中,数组有个致命的缺点, 无法动态修改容量。
实际开发中我们希望数组的容量是动态变化的。
跳转到目录
public class ArrayList<E> {
private int size;
private E[] elements;
// 元素的数量
int size();
// 是否为空
boolean isEmpty();
// 是否包含某个元素
boolean contains(E element);
// 添加元素到最后面
void add(E element);
// 返回index位置对应的元素
E get(int index);
// 设置index位置的元素
E set(int index, E element);
// 往index位置添加元素
void add(int index, E element);
// 删除index位置对应的元素
E remove(int index);
// 查看元素的位置
int indexOf(E element);
// 清除所有元素
void clear();
}
跳转到目录
跳转到目录
public class ArrayList<E> {
/**
* 元素的数量
*/
private int size;
/**
* 所有元素
*/
private E[] elements;
/**
* 数组的默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 找不到元素返回-1
*/
private static final int ELEMENT_NOT_FOUNT = -1;
public ArrayList() {
// 默认容量
//elements = new int[DEFAULT_CAPACITY];
this(DEFAULT_CAPACITY); // 调用下面的构造器
}
public ArrayList(int capacity) {
// 设置默认容量为 10
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
// 因为泛型(所以传一个Object数组,然后通过强转)
elements = (E[]) new Object[capacity];
}
}
跳转到目录
在最后一个元素的后面添加新元素
和将元素插入到某个位置(非最后面)
两种情况。新元素需要添加到的索引
等于当前数组元素的个数
,在ArrayList中size
属性就是当前数组元素的个数
,所以就是将新元素添加到数组的size
位置上,然后size加1
。public void add(int index, E element) {
elements[index] = element;
size++;
}
跳转到目录
不能小于0, 也不能大于size
。/**
* 根据index插入元素时,判断index是否有效
*
* @param index
*/
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size) {
indexOutOfBounds(index);
}
}
/**
* 封装数组越界异常
*
* @param index
*/
private void indexOutOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
跳转到目录
动态扩容思路:
通过默认容量创建的数组,是在堆空间中随机
生成的地址;如此一来再申请空间拼接到该数组后,这种方式不可能
实现;
我们只能再创建一个大容量
的数组,然后将之前数组中的元素移动
到这个数组中;然后将引用
指向新数组即可!
由于数组elements
最大的容量只有10
,所以当数组存满元素时,就需要对数组进行扩容。
因为数组是无法动态扩容的,所以需要创建一个新的数组
,这个数组的容量要比之前数组的容量大。
该方法确保默认容量为多少,为了验证是否超过给定的默认容量
,然后进行判断是否要扩容;这里size+1
为数组当前数量+1, 因为每次add
都会增加一个容量。
/**
* 确保至少要有capacity个容量
*
* @param capacity
*/
private void ensureCapacity(int capacity) {
// 获取数组当前容量
int oldCapacity = elements.length;
// 判断是否要扩容
if (oldCapacity >= capacity)
return; // 此时不扩容
// 这种方式是扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
E[] newElements = (E[]) new Object[newCapacity];
// 将原来数组中的元素移动到新数组中
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
// 将数组引用指向新数组
elements = newElements;
}
add
函数,需要在添加新元素之前,判断数组越界
和扩容
。/**
* 在index位置插入一个元素
*
* @param index
* @param element
*/
public void add(int index, E element) {
// 在添加元素的时候,判断index是否有效
rangeCheckForAdd(index);
ensureCapacity(size + 1);
// 注意: 插入元素后,元素是从后开始往后挪
for (int i = size - 1; i >= index; i--) {
elements[i + 1] = elements[i];
}
elements[index] = element;
size++;
}
在最后一个元素的后面添加新元素
,即添加元素到尾部
的实现方式如下/**
* 添加元素到尾部
*
* @param element
*/
public void add(E element) {
// elements[size++] = element;
// 传入数组数量(相当于在最后插入元素)
add(size, element);
}
跳转到目录
移除指定位置的元素
,并将后面的元素向前移动
。3
的元素时,只需要将后面的元素向前移动,然后在去掉最后一个元素,size减1
即可。跳转到目录
不能小于0, 也不能大于等于size
所以我们在删除元素之前需要先进行索引检查。private void indexOutOfBounds(int index) {
throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}
private void rangeCheck(int index) {
if (index < 0 || index >= size) {
outOfBounds(index);
}
}
跳转到目录
存的浪费
。缩容
。数组中容量小于某个值
时,创建新的数组,然后将原有数组中的元素存入新数组
即可。/**
* 数组缩容
*/
public void trim() {
// 获取当前数组的容量
int capacity = elements.length;
// 当size大于等于容量的一半, 或则容量已经小于默认容量(10)时, 直接返回
if (size >= capacity >> 1 || capacity < CAPACITY_DEFAULT) return;
// 计算新的容量 = 原有容量的一半
int newCapacity = capacity >> 1;
// 创建新数组
E[] newElements = (E[]) new Object[newCapacity];
// 将原数组元素存入新数组
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
// 引用新数组
elements = newElements;
}
remove
方法实现如下/**
* 删除index位置的元素
*
* @param index
* @return 被删除的元素
*/
public E remove(int index) {
rangeCheck(index);
E delEle = elements[index];
// 当删除一个元素时,需要挪动后面元素的范围
for (int i = index + 1; i <= size - 1; i++) {
elements[i - 1] = elements[i];
}
size--;
// 同clear的细节,当从后往前以后时,最后一个的地址需要释放
elements[size] = null;
// 判断数组是否需要缩容
trim();
return delEle;
}
/**
* 删除传入的元素
* @param element
*/
public void remove(E element){
remove(indexOf(element));
}
跳转到目录
null
,只有这样才能真正的释放对象,然后size置为0
。/**
* 清除所有元素
*/
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
跳转到目录
替换
掉即可,同样需要注意一下索引是否越界
。/**
* 设置index位置的元素
*
* @param index
* @param element
* @return 原来的元素
*/
public E set(int index, E element) {
rangeCheck(index);
E oldEle = elements[index];
elements[index] = element;
return oldEle;
}
跳转到目录
索引
是否越界
即可。/**
* 获取index位置的元素
*
* @param index
* @return
*/
public E get(int index) {
// 约束Index
rangeCheck(index);
return elements[index];
}
跳转到目录
null
,而null是不能调用equals
方法的,所以需要对传入的元素进行判断,如果查找的元素是null
,需要单独处理。ELEMENT_ON_FOUND
的值。/**
* 查看元素的索引
*
* @param element
* @return
*/
public int indexOf(E element) {
if (element == null) {
// 循环判断如果element为null,直接返回null的索引
for (int i = 0; i < size; i++) {
if (elements[i] == null)
return i;
}
} else {
for (int i = 0; i < size; i++) {
// 因为element肯定不为null了,所以放在前面;避免空指针异常
if (element.equals(elements[i]))
return i;
}
}
return ELEMENT_NOT_FOUNT;
}
跳转到目录
ELEMENT_ON_FOUND
即可。/**
* 是否包含某个元素
*
* @param element
* @return
*/
public boolean contains(E element) {
// 如果element元素可以找到
return indexOf(element) != ELEMENT_NOT_FOUNT;
}
跳转到目录
/**
* 元素的数量
*
* @return
*/
public int size() {
return size;
}
跳转到目录
size
的值是否为0
即可/**
* 是否为空
*
* @return
*/
public boolean inEmpty() {
return size == 0;
}
跳转到目录
toString
方法, 来打印ArrayList
中的元素。@Override
public String toString() {
// 打印格式: size=3, [10, 20, 30]
// 使用StringBuilder 效率高一些
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
string.append(elements[i]);
if (i != size - 1) {
string.append(", ");
}
}
string.append("]");
return string.toString();
}
这样就实现了动态数组的基本操作。
跳转到目录
在ArrayList中,如果要删除索引0
位置的元素,则需要将索引0
之后的元素全部往前移一位
。
在ArrayList中增加一个记录首元素位置的属性
。
删除索引0
位置的元素,我们只需要将first
属性改为1
。
-在索引0
位置添加元素,则只需要将first
属性改为0
。
8
这个位置,并将first
改为8
。8
下一个元素时,对索引取模
,则拿到索引0
位置的元素。前半段
数据或后半段
数据。2
处插入元素99
,可以选择将元素22
,33
左移,然后插入99
即可。扩容
和缩容
同样可以优化。跳转到目录
跳转到目录
引用指向新数组
即可!跳转到目录
该方法确保默认容量为多少, 为了验证是否超过给定的默认容量,然后进行判断是否要扩容; 这里size+1为数组当前数量+1, 因为每次add都会增加一个容量。
ensureCapacity(size + 1);
跳转到目录
使用泛型, 使动态数组可以添加任何
类型的数据。
elements = (E[]) new Object[capacity];
跳转到目录
因为之前存储的都是int数据
, 直接设置size=0
时, 开辟的存储int类型的空间就不能被访问, 当add后, 就可以访问后面的空间, 所以此时的空间可以重复利用
;
当使用泛型
后, 动态数组中存储的都是对象
类型, 实际存储的都是对象的地址
, 每一个对象的地址又有一块空间引用
着; 此时如果仍设置 size=0, 当clear后,开辟的存储空间中的地址
没有被销毁
, 地址仍被对象空间
引用着; 这样以来存储对象的空间就不会被释放; 但是存储地址
的数组可以重复利用; 所以要将地址数组都置为null,
然后size=0
, 这样以来,引用该地址的对象空间也就释放
了!
跳转到目录
==
比较, 因为比较的是地址值
,一般重写equals
方法自己定义比较内容即可;null
的时候,indexOf的比较处理: 如果那null.equals来比较会出现空指针
异常;