利用Java来手写ArrayList
Posted weixin_45747080
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用Java来手写ArrayList相关的知识,希望对你有一定的参考价值。
利用Java来手写ArrayList
几乎所有的语言都会有数组,Java也不例外。数组有个特点就是在初始化的时候必须确定长度,即使容量到达了也无法自动扩容,无法满足需求,所以我们可以利用动态数组(ArrayList)来实现可以自动扩容的数组。参考Java官方的ArrayList实现:java.util.ArrayList
。ArrayList的底层还是数组,相当于数组的强化版,能够进行自动扩容并且进行数据的增删改查。
注意:
以下利用动态数组来表示
ArrayList
,利用数组来表示Object[]
私有属性
最起码需要表示动态数组的大小size
,存放数据的基本数组elements[]
,所以应该有以下属性:
public class ArrayList<E> {
/**
* 动态数组大小
*/
private int size;
/**
* 动态数组
*/
private E[] elements;
}
注意这里使用到了泛型,是为了方便顺序存储相似的数据(类型相同的数据)
构造方法
无参构造方法:创建一个默认容量的数组;
有参构造方法:创建一个指定容量的数组。
既然涉及到默认,那我们可以指定一个常量为默认容量,可以按照自己的需求来,随便多少都可以。
/**
* 动态数组默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 无参构造,创建默认大小的数组
*/
public ArrayList(){
}
/**
* 有参构造,根据容量创建数组
* @param capacity 自定义容量
*/
public ArrayList(int capacity){
}
注意:
容量(capacity)是指该动态数组能够容纳多少个元素,而不代表它里面已经存了多少个元素,通常情况数组的长度
elements.length
和他相等。大小(size)是指该动态数组已经有多少个元素。
无参构造
创建默认容量的Object数组,再强制类型转换。
public ArrayList(){
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
有参构造
创建指定容量的数组,需要控制一下传入的容量大小,不允许为负数。
public ArrayList(int capacity){
if (capacity<=0) capacity = DEFAULT_CAPACITY; //如果传入容量不为自然数,则强制为默认容量
elements = (E[]) new Object[capacity];
}
基本方法
仿造java.util.ArrayList
,得出以下基本方法包含基本的添加删除查询元素等。
public class ArrayList<E> {
/**
* 动态数组大小
* @return 动态数组大小
*/
public int size(){}
/**
* 动态数组是否为空
* @return 动态数组是否为空
*/
public Boolean isEmpty(){}
/**
* 添加元素
* @param element 元素
*/
public void add(E element){}
/**
*
* 0 1 2 3 4 5 6 7 8 9
* 1 2 3 4 5 6 7 8
* 向指定位置添加元素
* @param index 位置
* @param element 元素
*/
public void add(int index,E element){}
/**
*
* 0 1 2 3 4 5 6 7 8 9
* a b c 1 d e f g h
* 移除指定位置的元素
* @param index 位置
*/
public void remove(int index){}
/**
* 删除指定元素
* @param element 元素
*/
public void remove(E element){}
/**
* 清空动态数组中的元素
*/
public void clear(){}
/**
* 修改指定位置的元素
* @param index 位置
* @param element 元素
*/
public void set(int index,E element){}
/**
* 获得指定位置的元素
* @param index 位置
* @return 元素
*/
public E get(int index){}
/**
* 判断数组是否包含该元素
* @param element 元素
* @return true包含,false不包含
*/
public Boolean contains(E element){}
/**
* 该元素第一次出现的下标
* @param element 元素
* @return 下标
*/
public int indexOf(E element){}
@Override
public String toString() {}
}
size()
返回动态数组的大小
public int size(){
return size;
}
isEmpty()
返回动态数组是否为空,判断size是否为0
public Boolean isEmpty(){
return size == 0;
}
toString()
打印动态数组,这个按照自己的需求来就可以了
@Override
public String toString() {
StringBuilder string = new StringBuilder();
string.append("ArrayList{");
string.append("size=" + size + ", elements=[");
for (int i = 0; i < size; i++) {
string.append(elements[i]);
if (i != size -1) { //最后一个元素末尾不添加逗号
string.append(", ");
}
}
string.append("]");
string.append("}");
return string.toString();
}
indexOf()和contains()
indexOf()返回的元素第一次出现的下标(循环数组,如果有该元素则返回下标,下标为-1则表示没有该元素)
public int indexOf(E element){
for (int i = 0; i < size; i++) {
if (elements[i].equals(element)) return i; //如果element存在返回下标
}
return -1;
}
contains()判断是否存在该元素,利用indexOf()来判断,如果存在下标则说明存在该元素,不存在下标则不存在该元素
public Boolean contains(E element){
return indexOf(element) >= 0;
}
get()
返回指定下标的数组元素
public E get(int index){
return elements[index];
}
set()
向指定下标塞入元素
public void set(int index,E element){
elements[index] = element;
}
add()
添加元素,向动态数组末尾添加元素。
public void add(E element){
elements[size] = element;
size++;
}
重载方法add(int index, E element)向指定位置添加元素,原来的元素会依次向后挪动。元素挪动的目的是为了空出index下标的元素,所以index下标及其后面的元素都需要依次往后挪动。挪动的顺序应是
注意第一是先挪动最后一个,如果你从index才是挪动后面的会被覆盖。依次挪动然后index下标处元素就会被空出来,然后塞入要插入的元素即可。
public void add(int index,E element){
for (int i = size; i > index ; i--) { //元素依次往后挪动
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
同理,其实add(E element)就是往动态数组末尾添加元素,所以可以直接调用重载方法,指定下标为末尾(size)就可以了。
public void add(E element){
add(size,element);
}
remove()
移除指定位置的元素,移除指定位置的元素之后,指定位置的元素会被删除,然后指定位置后面的元素会依次往前挪动。
注意,实际上不用硬性删除指定的元素,在Java的特性中,只要某一内存地址没有再被指向那么它就会被垃圾回收掉(视为删除),所以直接用后面的元素直接覆盖掉指定下标的元素,原来的元素的地址就没有被指向了,所以就会被删除,最后再将末尾的一个元素置为null即可。但是需要注意的是删除是先挪动前面再挪动后面,如果从后往前挪会被覆盖掉。
public void remove(int index){
for (int i = index; i < size-1 ; i++) { //元素依次往前挪
elements[i] = elements[i+1];
}
elements[size-1] = null; //将数组的最后一个元素置为null
size--;
}
clear()
清空动态数组,直接将所有位置置为null即可,那么之前的元素的地址就不没有被指向了,就会被删除。
public void clear(){
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
下标越界
在上述方法传入index的时候,可能会出现index越界的情况,比如为负数或者超出上限,所以需要控制index的有效范围,并且在每次传入进行检查就可以了,错误就抛出异常。
注意
add()和remove()、get()、set()的index范围有所区别。添加元素允许往动态数组的最后一个位置添加元素,所以index是可以访问到size的,但是删除、查询和修改都是在元素已经在动态数组中存在的基础上,所以index是不可以访问到size的,切记!!!
checkIndex()
检查非添加时的下标index,不能小于0或者不能大于等于size(0<index<size)
private void checkIndex(int index){
if (index<0||index>=size) indexOutOfBoundsException(index);
}
checkIndexAdd()
检查添加时下标index,不能小于0或者不能大于size(0<index ⩽ \\leqslant ⩽size)
private void checkIndexAdd(int index){
if (index<0||index>size) indexOutOfBoundsException(index);
}
indexOutOfBoundsException()
下标越界时抛出的自定义异常
private void indexOutOfBoundsException(int index){
throw new IndexOutOfBoundsException("index="+index+", size="+size);
}
动态扩容
动态数组相较于传统数组的优势就是能够动态扩容,但其实底层仍然是利用数组扩容,实质就是利用新的容量的数组来装填原来的数据,这当中会涉及到很多底层的内存消耗以及资源最优化的问题,这里不讲解,主要讲解如何实现。
扩容也只可能会在添加元素的时候发生,并且是添加元素时的动态数组的size也就是元素个数刚好达到容量这个时候就该扩容了:
- 判断动态数组元素个数已经达到数组容量,说明已经满了需要扩容了
- 新的数组容量为当前数组容量的1.5倍
- 以新的数组容量大小创建数组
- 旧数组依次向新的数组赋值
- 旧数组的指针指向新数组
public void add(int index,E element){
checkIndexAdd(index);
if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
int nowCapacity = elements.length; //当前动态数组容量就是数组长度
int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
newElements[i] = elements[i];
}
elements = newElements; //elements对象指向newElements
System.out.println(nowCapacity + "扩容至" + newCapacity);
}
for (int i = size; i > index ; i--) { //元素依次往后挪动
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
我们将数组扩容单独提出来(提高可维护性和可读性)
private void ensureCapacity(){
if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
int nowCapacity = elements.length; //当前动态数组容量就是数组长度
int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
newElements[i] = elements[i];
}
elements = newElements; //elements对象指向newElements
System.out.println(nowCapacity + "扩容至" + newCapacity);
}
}
综合:
private void ensureCapacity(){
if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
int nowCapacity = elements.length; //当前动态数组容量就是数组长度
int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
newElements[i] = elements[i];
}
elements = newElements; //elements对象指向newElements
System.out.println(nowCapacity + "扩容至" + newCapacity);
}
}
public void add(int index,E element){
checkIndexAdd(index);
ensureCapacity();
for (int i = size; i > index ; i--) { //元素依次往后挪动
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
总结
在解决了动态数组的大小、元素存放的数组等属性和一些基本的添加、删除、修改和查询方法后,以及对下标越界的处理和最核心的动态扩容部分解决后,一个最基本的动态数组ArrayList就大致完成了:
public class ArrayList<E> {
/**
* 动态数组大小
*/
private int size;
/**
* 动态数组
*/
private E[] elements;
/**
* 动态数组默认容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 无参构造,创建默认大小的数组
*/
public ArrayList(){
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
/**
* 有参构造,根据容量创建数组
* @param capacity 自定义容量
*/
public ArrayList(int capacity){
if (capacity<=0) capacity = DEFAULT_CAPACITY; //如果传入容量不为自然数,则强制为默认容量
elements = (E[]) new Object[capacity];
}
/**
* 动态数组大小
* @return 动态数组大小
*/
public int size(){
return size;
}
/**
* 动态数组是否为空
* @return 动态数组是否为空
*/
public Boolean isEmpty(){
return size == 0;
}
/**
* 下班超出范围的报错信息
* @param index
*/
private void indexOutOfBoundsException(int index){
throw new IndexOutOfBoundsException("index="+index+", size="+size);
}
/**
* 检查添加元素时候的index
* @param index
*/
private void checkIndexForAdd(int index){
if (index<0||index>size) indexOutOfBoundsException(index);
}
/**
* 检查删改查时的index
* @param index
*/
private void checkIndex(int index){
if (index<0||index>=size) indexOutOfBoundsException(index);
}
/**
* 确保容量(如果动态数组元素大小达到当前动态数组的容量满了则扩容)
*/
private void ensureCapacity(){
if (size == elements.length){ //如果动态数组得大小已经达到数组容量(已满)
int nowCapacity = elements.length; //当前动态数组容量就是数组长度
int newCapacity = nowCapacity + (nowCapacity >> 1); //扩容至当前容量得1.5倍
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) { //旧数组依次向新数组中赋值
newElements[i] = elements[i];
}
elements = newElements; //elements对象指向newElements
System.out.println(nowCapacity + "扩容至" + newCapacity);
}
}
/**
* 添加元素
* @param element 元素
*/
public void add(E element){
add(size,element);
}
/**
*
* 0 1 2 3 4 5 6 7 8 9
* 1 2 3 4 5 6 7 8
* 向指定位置添加元素
* @param index 位置
* @param element 元素
*/
public void add(int index,E element){
if (element == null) throw new NullPointerException("不允许添加null");
checkIndexForAdd(index);
ensureCapacity(); //确保数组容量(如果容量满了则扩容)
for (int i = size; i > index ; i--) { //元素依次往后挪动
elements[i] = elements[i - 1];
}
elements[index] = element;
size++;
}
/**
*
* 0 1 2 以上是关于利用Java来手写ArrayList的主要内容,如果未能解决你的问题,请参考以下文章