数组容器(ArrayList)设计与Java实现

本篇文章主要跟大家介绍我们最常使用的一种容器ArrayListVector的原理,并且自己使用Java实现自己的数组容器MyArrayList,让自己写的容器能像ArrayList那样工作。在本篇文章当中首先介绍ArrayList的一些基本功能,然后去分析我们自己的容器MyArrayList应该如何进行设计,同时分析我们自己的具体实现方法,最后进行代码介绍!!!

ArrayList为我们提供了哪些功能?

我们来看一个简单的代码,随机生成100个随机数,查看生成随机数当中是否存在50这个数。

public class MyArrayList {

  public static void main(String[] args) {
Random random = new Random();
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(random.nextInt(5000));
}
for (int i = 0; i < 100; i++) {
if (list.get(i) == 50) {
System.out.println("包含数据 50");
}
}
list.set(5, 1000);// 设置下标为5的数据为100
list.remove(5);// 删除下标为5的数据
list.remove(new Integer(888));// 删除容器当中的第一个值为5的数据
}
}

上述代码包含了ArrayList最基本的一个功能,一个是add方法,向数组容器当中加入数据,另外一个方法是get从容器当中拿出数据,set方法改变容器里的数据,remove方法删除容器当中的数据。ArrayList的很多其他的方法都是围绕这四个最基本的方法展开的,因此我们在这里不仔细介绍其他的方法了,后面我们自己实现的时候遇到问题的时候自然会需要设计相应的方法,然后我们进行解决即可。

现在我们就需要去设计一个数组容器实现“增删改查”这四个基本功能。

设计原理分析

首先明白一点我们需要使用什么工具去实现这样一个容器。我们手里有的工具就是Java提供给我们的最基本的功能——数组(这个好像是废话,我们的标题就是数组容器)。

当我们在Java当中使用数组去存储数据时,数据在Java当中的内存布局大致如下图所示。

我们在设计数组容器这样一个数据结构的时候主要会遇到两个问题:

  • 我们申请数组的长度是多少。
  • 当数组满了之后怎么办,也就是我们的扩容机制。

对于这两个问题,首先我们数组的初始大小可以有默认值,在我们自己实现的MyArrayList当中设置为10,我们在使用类时也可以传递一个参数指定初始大小。第二个问题当我们的数组满的时候我们需要对数组进行扩容,在我们实现的MyArrayList当中我们采取的方式是,新数组的长度是原数组的两倍(这个跟JDKArrayList方式不一样,ArrayList扩容为原来的1.5倍)。

代码实现

为了让我们的类实现的更加简单我们在代码当中就不做很多非必要的逻辑判断并且抛出异常,我们的代码只要能表现出我们的思想即可。

  • 首先定义一个接口MyCollection,表示我们要实现哪些方法!
public interface MyCollection<E> {

  /**
* 往链表尾部加入一个数据
* @param o 加入到链表当中的数据
* @return
*/
boolean add(E o); /**
* 表示在第 index 位置插入数据 o
* @param index
* @param o
* @return
*/
boolean add(int index, E o); /**
* 从链表当中删除数据 o
* @param o
* @return
*/
boolean remove(E o); /**
* 从链表当中删除第 index 个数据
* @param index
* @return
*/
boolean remove(int index); /**
* 往链表尾部加入一个数据,功能和 add 一样
* @param o
* @return
*/
boolean append(E o); /**
* 返回链表当中数据的个数
* @return
*/
int size(); /**
* 表示链表是否为空
* @return
*/
boolean isEmpty(); /**
* 表示链表当中是否包含数据 o
* @param o
* @return
*/
boolean contain(E o); /**
* 设置下标为 index 的数据为 o
* @param index
* @param o
* @return
*/
boolean set(int index, E o);
}
  • 我们的构造函数,初始化过程。
  public MyArrayList(int initialCapacity) {
this();
// 增长数组的空间为 initialCapacity,即申请一个数组
// 且数组的长度为 initialCapacity
grow(initialCapacity);
} public MyArrayList() {
this.size = 0; // 容器当中的数据个数在开始时为 0
this.elementData = EMPTY_INSTANCE; // 将数组设置为空数组
}
  • 我们需要实现的最复杂的方法就是add了,这个方法是四个方法当中最复杂的,其余的方法都相对比较简单。

    • 进入add方法之后,我们需要找到符合要求的最小数组长度,这个值通常是容器当中元素的个数size + 1 ,也就是图中的minCapacity首先先比较这个值和现在数组的长度,如果长度不够的话则需要进行扩容,将数组的长度扩大到原来的两倍。
    • 如果不需要扩容,则直接讲元素放入到数组当中即可。

  @Override
public boolean add(E o) {
// 这个函数的主要作用就是确保数组的长度至少为 size + 1
ensureCapacity(size + 1);
// 新增加了一个数据,容器的大小需要 + 1
elementData[++size] = o;
return true;
} /**
* 这个函数的主要作用就是确保数组的长度至少为 capacity
* @param capacity
*/
public void ensureCapacity(int capacity) {
int candidateCapacity = findCapacity(capacity);
if (elementData.length < candidateCapacity)
grow(candidateCapacity);
} /**
* 这个函数的主要目的就是找到最终数组长度需求的容量
* @param minCapacity
* @return
*/
private int findCapacity(int minCapacity) {
/**
* 如果 if 条件为 true 即 elementData 还是初始化时设置的空数组
* 那么返回默认大小和需要大小的最大值
* 否则直接返回 minCapacity
*/
if (elementData == EMPTY_INSTANCE){
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
  • 我们为什么需要将ensureCapacity的访问限制权限设置为public?因为我们想让用户尽量去使用这个函数,因为如果我们如果写出下面这样的代码我们会一直申请内存空间,然后也需要将前面的数组释放掉,会给垃圾回收器造成更大的压力。
    ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i);
}

下面我们对ArrayList的方法进行测试:

import java.util.ArrayList;

class Person {

  String name;

  public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
} public class ArrayListTest { public static void main(String[] args) {
ArrayList<Person> o1 = new ArrayList<>();
o1.ensureCapacity(10000000);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
o1.add(new Person());
}
long end = System.currentTimeMillis();
System.out.println("end - start: " + (end - start));
ArrayList<Person> o2 = new ArrayList<>();
start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
o2.add(new Person());
}
end = System.currentTimeMillis();
System.out.println("end - start: " + (end - start));
}
}
// 输出结果
end - start: 1345
end - start: 4730

从上面的测试结果我们可以看出提前使用ensureCapacity方法之后,程序执行的时间更加短。

  • 插入数据的add方法。
  @Override
public boolean add(E o) {
// 这个函数的主要作用就是确保数组的长度至少为 size + 1
ensureCapacity(size + 1);
// 新增加了一个数据,容器的大小需要 + 1
elementData[size] = o;
size++;
return true;
}
  • add在指定下标插入数据。

    • 首先将插入下标后的数据往后移动一个位置
    • 然后在将数据放在指定下标的位置。

  /**
* 在下标 index 位置插入数据 o
* 首先先将 index 位置之后的数据往后移动一个位置
* 然后将 index 赋值为 o
* @param index
* @param o
* @return
*/
@Override
public boolean add(int index, E o) {
// 确保容器当中的数组长度至少为 size + 1
ensureCapacity(size + 1);
// 将 elementData index位置之后的数据往后移动一个位置
// 做一个原地拷贝
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 移动的数据个数为 size - index
elementData[index] = o;
size++;
return true;
}
  • 删除数据的方法remove

    • 首先先删除指定下标的数据。
    • 然后将指定下标后的数据往前移动一个位置
    • 在实际的操作过程中我们可以不删除,直接移动,这样也覆盖被插入位置的数据了。

  /**
* 移除下标为 index 的数据
* @param index
* @return
*/
@Override
public boolean remove(int index) {
// 需要被移动的数据个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; return true;
}
  • 移除容器当中具体的某个对象。
  /**
* 这个方法主要是用于溢出容器当中具体的某个数据
* 首先先通过 for 循环遍历容器当中的每个数据,
* 比较找到相同的数据对应的下标,然后通过下标移除方法
* @param o
* @return
*/
@Override
public boolean remove(E o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
remove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
remove(index);
return true;
}
}
return false;
}
  • set方法,这个方法就很简单了。
  @Override
public boolean set(int index, E o) {
elementData[index] = o;
return true;
}
  • 重写toString方法。
  @Override
public String toString() { if (size <= 0)
return "[]"; StringBuilder builder = new StringBuilder();
builder.append("[");
for (int index = 0; index < size; index++) {
builder.append(elementData[index].toString() + ", ");
}
builder.delete(builder.length() - 2, builder.length());
builder.append("]");
return builder.toString();
}
  • 测试代码
public static void main(String[] args) {
MyArrayList<Integer> list = new MyArrayList<>();
for (int i = 0; i < 15; i++) {
list.add(-i);
}
System.out.println(list.contain(5));
System.out.println(list);
list.remove(new Integer(-6));
System.out.println(list);
System.out.println(list.elementData.length); // 容器会扩容两倍,而默认容器长度为10,因此这里是 20
list.add(5, 99999);
System.out.println(list);
System.out.println(list.contain(99999));
}
// 代码输出
false
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14]
[0, -1, -2, -3, -4, -5, -7, -8, -9, -10, -11, -12, -13, -14]
20
[0, -1, -2, -3, -4, 99999, -5, -7, -8, -9, -10, -11, -12, -13, -14]
true

完整代码

import java.util.ArrayList;
import java.util.Arrays; public class MyArrayList<E> implements MyCollection<E> { /**
* 容器当中存储数据的个数
*/
private int size; /**
* 容器中数组的默认长度
*/
private static final int DEFAULT_CAPACITY = 10; /**
* 存放具体数据的数组,也就是我们容器当中真正存储数据的地方
*/
Object[] elementData; /**
* 当容器当中没有数据将 elementData 设置为这个值,这个值是所有实例一起共享的
*/
private static final Object[] EMPTY_INSTANCE = {}; public MyArrayList(int initialCapacity) {
this();
// 增长数组的空间为 initialCapacity,即申请一个数组
// 且数组的长度为 initialCapacity
grow(initialCapacity);
} public MyArrayList() {
this.size = 0; // 容器当中的数据个数在开始时为 0
this.elementData = EMPTY_INSTANCE; // 将数组设置为空数组
} /**
* 这个函数的主要作用就是确保数组的长度至少为 capacity
* @param capacity
*/
public void ensureCapacity(int capacity) {
int candidateCapacity = findCapacity(capacity);
if (elementData.length < candidateCapacity)
grow(candidateCapacity);
} /**
* 这个函数的主要目的就是找到最终数组长度需求的容量
* @param minCapacity
* @return
*/
private int findCapacity(int minCapacity) {
/**
* 如果 if 条件为 true 即 elementData 还是初始化时设置的空数组
* 那么返回默认大小和需要大小的最大值
* 否则直接返回 minCapacity
*/
if (elementData == EMPTY_INSTANCE){
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
} /**
* 该函数主要保证 elementData 的长度至少为 minCapacity
* 如果数组的长度小于 minCapacity 则需要进行扩容,反之
* @param minCapacity 数组的最短长度
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 新的数组长度为原来数组长度的两倍
int newCapacity = oldCapacity << 1; // 如果数组新数组的长度 newCapacity 小于所需要的长度 minCapacity
// 新申请的长度应该为 minCapacity
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
// 申请一个长度为 newCapacity 的数组,在将原来数组
// elementData 的数据拷贝到新数组当中
elementData = Arrays.copyOf(elementData, newCapacity);
} @Override
public boolean add(E o) {
// 这个函数的主要作用就是确保数组的长度至少为 size + 1
ensureCapacity(size + 1);
// 新增加了一个数据,容器的大小需要 + 1
elementData[size] = o;
size++;
return true;
} /**
* 在下标 index 位置插入数据 o
* 首先先将 index 位置之后的数据往后移动一个位置
* 然后将 index 赋值为 o
* @param index
* @param o
* @return
*/
@Override
public boolean add(int index, E o) {
// 确保容器当中的数组长度至少为 size + 1
ensureCapacity(size + 1);
// 将 elementData index位置之后的数据往后移动一个位置
// 做一个原地拷贝
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 移动的数据个数为 size - index
elementData[index] = o;
size++;
return true;
} /**
* 这个方法主要是用于溢出容器当中具体的某个数据
* 首先先通过 for 循环遍历容器当中的每个数据,
* 比较找到相同的数据对应的下标,然后通过下标移除方法
* @param o
* @return
*/
@Override
public boolean remove(E o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
remove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
remove(index);
return true;
}
}
return false;
} /**
* 移除下标为 index 的数据
* @param index
* @return
*/
@Override
public boolean remove(int index) {
// 需要被移动的数据个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; return true;
} @Override
public boolean append(E o) {
return add(o);
} @Override
public int size() {
return size;
} @Override
public boolean isEmpty() {
return size == 0;
} @Override
public boolean contain(E o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
return true;
}
}
return false;
} @Override
public String toString() { if (size <= 0)
return "[]"; StringBuilder builder = new StringBuilder();
builder.append("[");
for (int index = 0; index < size; index++) {
builder.append(elementData[index].toString() + ", ");
}
builder.delete(builder.length() - 2, builder.length());
builder.append("]");
return builder.toString();
} @Override
public boolean set(int index, E o) {
elementData[index] = o;
return true;
} public static void main(String[] args) {
MyArrayList<Integer> list = new MyArrayList<>();
for (int i = 0; i < 15; i++) {
list.add(-i);
}
System.out.println(list.contain(5));
System.out.println(list);
list.remove(new Integer(-6));
System.out.println(list);
System.out.println(list.elementData.length);
list.add(5, 99999);
System.out.println(list);
System.out.println(list.contain(99999));
}
}

本篇文章我们介绍了ArrayList的内部原理,并且我们实现了一个自己的简单数组容器MyArrayList,但是我们还有一些内容没有涉及,比如cloneequals和迭代器,这些内容我们下期分析ArrayList源码再进行分析,我是LeHung,我们下期再见!!!

关注公众号:一无是处的研究僧,了解更多计算机知识。

数组容器(ArrayList)设计与Java实现,看完这个你不懂ArrayList,你找我!!!的更多相关文章

  1. 看完此文还不懂NB-IoT,你就过来掐死我吧...【转】

    转自:https://www.cnblogs.com/pangguoming/p/9755916.html 看完此文还不懂NB-IoT,你就过来掐死我吧....... 1 1G-2G-3G-4G-5G ...

  2. 看完此文还不懂NB-IoT,你就过来掐死我吧...

    看完此文还不懂NB-IoT,你就过来掐死我吧....... 1 1G-2G-3G-4G-5G 不解释,看图,看看NB-IoT在哪里? 2 NB-IoT标准化历程 3GPP NB-IoT的标准化始于20 ...

  3. 【职业规划】该如何选择职业方向?性能?自动化?测开?,学习选择python、java?看完你会感谢我的~

    前言 随着近两年来互联网行业的飞速发展,互联网技术的从业人员也越来越多. 近两年来技术岗位中测试和前端工程师变成了程序员中最好招的岗位. 测试行业卷也越来越厉害了. 也正是因为如此,我们要把自己的路越 ...

  4. 新鲜出炉!JAVA线程池精华篇深度讲解,看完你还怕面试被问到吗?

    前言 前两天趁着假期在整理粉丝私信的时候看到一个粉丝朋友的私信跟我说自己现在正在复习准备面试,自己在复习到线程池这一块的时候有点卡壳,总感觉自己差了点什么.想要我帮他指导一下.这不趁着假期我也有时间我 ...

  5. 面试阿里,字节跳动99%会被问到的java线程和线程池,看完这篇你就懂了!

    前言: 最近也是在后台收到很多小伙伴私信问我线程和线程池这一块的问题,说自己在面试的时候老是被问到这一块的问题,被问的很头疼.前几天看到后帮几个小伙伴解决了问题,但是问的人有点多我一个个回答也回答不过 ...

  6. 深度分析:java8的新特性lambda和stream流,看完你学会了吗?

    1. lambda表达式 1.1 什么是lambda 以java为例,可以对一个java变量赋一个值,比如int a = 1,而对于一个方法,一块代码也是赋予给一个变量的,对于这块代码,或者说被赋给变 ...

  7. 【最短路径Floyd算法详解推导过程】看完这篇,你还能不懂Floyd算法?还不会?

    简介 Floyd-Warshall算法(Floyd-Warshall algorithm),是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似.该算法名称以 ...

  8. 深度解析:如何替换掉代码中的ifelse,我女朋友看完都会了!

    平时我们在写代码时,需要针对不同情况处理不同的业务逻辑,用得最多的就是if和else. 但是如果情况太多,就会出现一大堆的"if else",这就是为什么很多遗留系统中,一个函数可 ...

  9. 【aliyun】学java,看这里,不迷茫!1460道Java热门问题

    阿里极客公益活动: 或许你挑灯夜战只为一道难题 或许你百思不解只求一个答案 或许你绞尽脑汁只因一种未知 那么他们来了,阿里系技术专家来云栖问答为你解答技术难题了 他们用户自己手中的技术来帮助用户成长 ...

随机推荐

  1. 深度优先搜索 DFS 学习笔记

    深度优先搜索 学习笔记 引入 深度优先搜索 DFS 是图论中最基础,最重要的算法之一.DFS 是一种盲目搜寻法,也就是在每个点 \(u\) 上,任选一条边 DFS,直到回溯到 \(u\) 时才选择别的 ...

  2. web服务报错类型

    401:无权限(HttpStatus.UNAUTHORIZED) 404:页面找不到 405:不支持get/post请求,如只支持get请求但传了post请求 400:请求格式错误,如不为null但传 ...

  3. SD卡之二:SD总线访问模式

    SD 卡是以命令.回应.数据流进行通讯. 1.命令:命令的长度是48位,命令以'0'开始,第2位为'1'表示主机发往SD卡的命令,最后以CRC和结束位'1'结尾. 2.回应:回应的长度是48位或者13 ...

  4. 完全卸载nginx的详细步骤

    一个执着于技术的公众号 前言 在开局配置Nginx时有可能会配置错误,报各种错误代码.看不懂或者懒得去看这个报错时,其实最简单的方式是卸载并重装咯.今天就带大家一起学习下,如何彻底卸载nginx程序. ...

  5. 年年出妖事,一例由JSON解析导致的"薛定谔BUG"排查过程记录

    前言 做开发这么多年,也碰到无数的bug了.不过再复杂的bug,只要仔细去研读代码,加上debug,总能找到原因. 但是最近公司内碰到的这一个bug,这个bug初看很简单,但是非常妖孽,在一段时间内我 ...

  6. Golang 函数 方法 接口的简单介绍

    函数 函数是基本的代码块,通常我们会将一个功能封装成一个函数,方便我们调用,同时避免代码臃肿复杂. 函数的基本格式 func TestFunc(a int, b string) (int, strin ...

  7. 基于STM32+华为云IOT设计智能称重系统

    摘要:选择部署多个重量传感器和必要的算法.通过WiFi 通信模块.GPS定位模块,采集车辆称重数据一地理位置信息,并通过网络发送至云平台,设计图形化UI界面展示称重.地图位置等重要信息,实现对称重系统 ...

  8. Java泛型类型擦除问题

    以前就了解过Java泛型的实现是不完整的,最近在做一些代码重构的时候遇到一些Java泛型类型擦除的问题,简单的来说,Java泛型中所指定的类型在编译时会将其去除,因此List 和 List 在编译成字 ...

  9. linux篇-Parse error: syntax error, unexpected ‘new’ (T_NEW) in /usr/local/nginx/html/cacti/lib/adodb

    1首先这是基于lnmp模式进行的 2yum安装 yum -y install httpd mysql mysql-server php php-mysql php-json php-pdo 3lib库 ...

  10. 每天一个 HTTP 状态码 101

    101 Switching Protocols 当客户端的请求具有 Upgrade HTTP 首部,表示要求服务器切换到指定协议:此时服务器端就可以向客户端响应 101 Switching Proto ...