动态数组(集合的底层实现)

本文介绍了动态数组在Java中的实现,包括其底层使用数组的机制,重点讲解了动态数组的可变性、扩容策略、插入、删除操作,以及使用函数式接口、迭代器和流进行遍历的方法。同时讨论了在实际应用中的注意事项和测试案例。
摘要由CSDN通过智能技术生成

动态数组(集合的底层实现)

前言

​ 这两天学了动态数组,也就是集合,只不过是用底层数组来实现功能,在这其中包含了很多JavaSE的知识点,在这也算是复习一下,相关知识点都整理了一下,之前在学JavaSE时没怎么做过笔记,有的知识点都忘记了,也花了不少时间去重新学习,文章有不足或错误之处,还请见谅。

动态数组简介

​ 在Java中,数组是一种用于存储固定大小的相同类型元素的数据结构。数组是一种引用类型,它存储了元素的引用,而不是元素本身(对于对象类型)或直接存储值(对于基本类型)。以下是Java中数组的一些主要特点:

数组特点:

  1. 固定大小:数组在创建时必须指定大小,并且之后不能更改。
  2. 存储同类型元素:数组中的所有元素必须是相同的数据类型。
  3. 索引访问:可以通过索引(从0开始)访问数组中的元素。
  4. 内存连续:数组在内存中占据连续的空间。
  5. 自动初始化:数组在创建时会自动被初始化为其数据类型的默认值(如int数组默认为0,boolean数组默认为false,对象数组默认为null)。

​ 很明显,数组的缺点很明显,就是不能改变数组的长度,但在很多时候,我们需要增加新的元素,这时候使用数组存储数据显然不能满足我们的需求,所以我们引入动态数组来解决这个问题。

动态数组

​ 显然Java中也考虑到了这一点,所以Java中有一种名为集合的数据结构,可以完美解决上述问题,但其实集合的底层逻辑也是使用数组来实现的,所以我们这次使用动态数组来解决上述问题。

​ 动态数组的特点主要体现在其灵活性和可扩展性上。与传统的静态数组相比,动态数组能够根据实际需要动态地增加或减少其元素数量。这种灵活性使得动态数组在处理不确定大小的数据集时非常有用。

具体来说,动态数组的特点包括:

  1. 可变性:动态数组的大小不是固定的,可以根据需要随时调整。当需要添加新元素时,动态数组可以自动扩展其容量以容纳新元素;当需要删除元素时,动态数组可以缩小其容量以释放内存空间。
  2. 连续性:尽管动态数组的大小可以变化,但在内存中,动态数组的元素仍然是连续存储的。这使得访问动态数组中的元素时,可以使用与静态数组相同的索引方式。
  3. 高效性:动态数组在添加或删除元素时,通常会尽量保持其内部数据的连续性,以减少内存碎片和提高访问效率。为了实现这一点,动态数组在扩容时通常会分配一块更大的连续内存空间,并将原有数据复制到新空间中;在缩容时,则可能会释放部分内存空间或将数据移动到更紧凑的空间中。
1.定义一个动态数组

​ 首先思考动态数组需要满足什么,需要哪些参数,除了基本的索引,似乎还需要一个代表数组长度的capacity,对于这个capacity,我们先给定它一个初始值,当存储空间不够时,自动扩容。

​ 对于数组的空间占用,有以下特点

​ Java 中数组结构为:

  1. 8 字节 markword
  2. 4字节 class 指针(压缩 class 指针的情况)
  3. 4字节 数组大小(决定了数组最大容量是 2^32)
  4. 数组元素+对齐字节(Java 中所有对象大小都是8字节的整数倍 12 ,不足的要用对齐字节补足)

代码实现如下

public class Dynamicarrays{
    //逻辑大小
    private int size = 0;
    //容量
    private int capacity = 8;
    private int[] array = new int[capacity];  
    }
2.动态数组的插入

​ 先考虑最基本的,在数组的最后插入数据,代码如下。

public void addLast(int element){
       array[size] = element;
       size++;
    }

​ 接下来就要考虑另一种情况,我们要在任意位置插入元素,如果这个位置本身有元素的话,那么这个元素以及它之后的元素就要向后移动,代码如下

//在数组的指定位置插入元素
public void add(int index,int element){
    //首先判断这个数据是合法的,索引位置大于或等于零,且不能超过数组的容量
    if(index >= 0 && index < size){
        //对于移动元素,我们直接向后复制
        System.arraycopy(array,index,array,index+1,size-index);
    }
    //在原来元素的位置赋新值
    array[index] = element;
    size++;
}

​ 写完这段代码后,我们发现在指定位置插入,与在最后插入的代码有重复的部分,那我们可以简化一下,直接调用add方法。

//在数组最后添加元素,
    public void addLast(int element){
//        array[size] = element;
//        size++;
        add(size,element);

    }
3.动态数组的遍历

​ 遍历,对于以前刚学Java的我来说就是用一个for循环,把所有数据元素都打印出来,但在经过后面的学习后,遍历可能不只是要打印元素,我可能不需要打印它,我可能使用遍历去做一些其他的事情,比如把数据元素存储到数据库等等。那么这里就介绍三种不同的遍历方法,在学习这三种遍历方法之前,首先对JavaSE的部分只是进行一个简单的复习。

函数式接口遍历

​ 函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,可以被隐式转换为 lambda 表达式。

​ 函数式接口有很多,这里就先只介绍一个简单的Consumer接口。

Consumer 接口是 Java 8 中引入的一个函数式接口,它位于 java.util.function 包中。Consumer 接口代表了一个接受单一输入参数并且没有返回值的操作。它经常用于对某个对象执行某种操作,而不必关心该操作的具体实现。

Consumer 接口的声明如下:

@FunctionalInterface  
public interface Consumer<T> {  
    void accept(T t);  
}

​ 这里,T 是一个泛型参数,代表 Consumer 可以接受任何类型的参数。accept 方法就是 Consumer 的唯一抽象方法,它接受一个类型为 T 的参数,并没有返回值。

​ 在使用这个接口时,会自动调用accept接口,那么我们就可以把打印的工作交给方法使用者,具体使用如下。

//函数式接口遍历
public void foreach(Consumer<Integer> consumer){
    for (int i = 0; i < size; i++) {
        consumer.accept(array[i]);
    }
}

​ 编写一个测试类

public void test2(){
    Dynamicarrays dynamicarrays = new Dynamicarrays();
    dynamicarrays.addLast(1);
    dynamicarrays.addLast(2);
    dynamicarrays.addLast(3);
    dynamicarrays.addLast(4);
    dynamicarrays.foreach((element)->{
        System.out.println(element);
    });
}
迭代器遍历

​ 迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式。

​ Iterator中的常用方法:

  1. boolean hasNext(),这个方法是判断当前位置是否有元素,有元素返回true,没有元素返回false。
  2. E next(),这个方法是获取当前位置的元素,并且将迭代器对象移向下一个位置。

​ 在这里,我们选择实现一个名为Iterable的接口,那么它必须提供一个iterator()方法,该方法返回一个Iterator对象,用于遍历该类的元素。当你实现Iterable接口时,你正在告诉其他代码:“我的对象可以被遍历,你可以通过调用我的iterator()方法来获取一个Iterator对象,并使用这个对象来遍历我。”

    //迭代器遍历
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            int i = 0;
            //询问有没有下一个元素,有返回true
            @Override
            public boolean hasNext() {
                return i<size;
            }
            //返回当前元素,并移动到下一个元素
            @Override
            public Integer next() {
                return array[i++];
            }
        };
    }    

编写一个测试类来对代码进行测试

@Test
public void test3(){
    Dynamicarrays dynamicarrays = new Dynamicarrays();
    dynamicarrays.addLast(1);
    dynamicarrays.addLast(2);
    dynamicarrays.addLast(3);
    dynamicarrays.addLast(4);
    for (Integer element : dynamicarrays){
        System.out.println(element);
    }
}

在这里使用了增强for循环的方法来进行遍历,在这里也简单复习一下增强for循环的相关知识

​ 增强for循环(Enhanced for loop),也被称为"for-each"循环,是Java 5及以后版本中引入的一种简化遍历数组或集合元素的方式。这种循环结构用于迭代数组或实现了Iterable接口的集合对象(如List, Set等)。

​ 当你使用增强for循环遍历一个集合或数组时,你不需要担心索引或迭代器的管理。Java编译器会为你处理这些细节。

​ 对于在这个测试代码中使用的增强for循环做一个简单解释:

  • Integer element:这是循环变量的声明。在每次迭代中,element变量都会被赋予dynamicarrays集合中的下一个元素的值。
  • ::这个冒号是增强for循环的语法标志,它告诉Java编译器接下来的变量是用于接收集合或数组中每个元素的。
  • dynamicarrays:这是你要遍历的集合或数组。它必须实现了Iterable接口(如果是自定义类型)或者是一个数组。
  • System.out.println(element);:这是循环体中的代码,它会在每次迭代中执行。在这个例子中,它打印出当前element的值。

当循环开始时,Java会自动调用dynamicarraysiterator()方法,获取一个Iterator对象。然后,循环会反复调用Iteratornext()方法来获取下一个元素,直到没有更多元素为止。每次调用next()时,都会将返回的元素值赋给element变量,并执行循环体中的代码。

stream流遍历

​ 在这个动态数组遍历中,我们使用IntStream,因为这个动态数组设置的都是int类型的元素,由于流的知识点过多,就不再一一赘述,只简单复习一下流的定义以及IntStream的方法。

​ 在Java中,流(Stream)是Java 8及以后版本中引入的一个新特性,它允许你以声明性方式处理数据集合(即你可以描述你想要做什么,而不是描述如何去做)。流API主要用于集合的并行或串行处理,并支持链式操作。

Java中的流主要分为两种类型:

  1. 原始流(Primitive Streams):这些流专门用于处理基本数据类型(如int, long, double)。Java提供了IntStream, LongStream, 和 DoubleStream 这三种原始流。
  2. 对象流(Object Streams):这些流用于处理对象。你可以通过任何实现了java.util.stream.Stream接口的对象来创建对象流,最常见的是通过集合类(如List, Set)的stream()方法。

流操作通常分为两类:

  • 中间操作(Intermediate Operations):这些操作返回一个新的流,允许进一步的操作链。例如,filter(), map(), sorted(), limit(), skip()等。
  • 终端操作(Terminal Operations):这些操作返回一个结果或一个副作用,并且会结束流的处理。例如,forEach(), reduce(), collect(), count(), sum()等。

示例代码如下

//stream流遍历
public IntStream stream(){
    return IntStream.of(Arrays.copyOfRange(array,0,size));
}

​ 在编写测试类时,发现有一个小小的问题,如果这个stream()方法,我直接返回return IntStream.of(array);那就会出现一个问题,因为我的有效值只有四个,但我这个数组的容量是八个,因此当我在测试方法中进行循环遍历时,结果把后面的无效元素也打印了出来,所以对代码修改一下,of方法中的参数改为这个数组的有效值部分。

@Test
public void test4(){
    Dynamicarrays dynamicarrays = new Dynamicarrays();
    dynamicarrays.addLast(1);
    dynamicarrays.addLast(2);
    dynamicarrays.addLast(3);
    dynamicarrays.addLast(4);
    dynamicarrays.stream().forEach((element)->{
        System.out.println(element);
    });
}
4.动态数组的删除

​ 删除代码比较简单,直接上示例代码

//动态数组的删除
public int remove(int index){
   int removed = array[index];
   if(index<5){
        System.arraycopy(array,index+1,array,index,size-index-1);
   }
   size--;
   return removed;
}

​ 但在测试的时候出现了一个问题,我使用Junit框架进行测试,想用assertIterableEquals()这个断言来比较删除后的结果是否相等,代码应该为assertIterableEquals(List.of(1,2,4,5),dynamicarrays);但由于我使用的jdk版本为1.8,所以不支持List.of()这个方法,但是当我更换为jdk17后,这个方法仍然不能使用,也不知道是哪里出了问题,如果有大佬知道,请在评论区指点一二。

测试代码

@Test
public void test5(){
    Dynamicarrays dynamicarrays = new Dynamicarrays();
    dynamicarrays.addLast(1);
    dynamicarrays.addLast(2);
    dynamicarrays.addLast(3);
    dynamicarrays.addLast(4);
    dynamicarrays.addLast(5);
    int removed = dynamicarrays.remove(2);
    assertEquals(3,removed);
    assertIterableEquals(List.of(1,2,4,5),dynamicarrays);
}
5.动态数组的扩容

​ 在之前的代码中,都没有考虑超出容量的情况,下面在进行添加元素之前,对容量进行检查,确定是否需要扩容。

​ 在此之前,我们可以对初始化代码进行一个小改动,把array数组先定义为一个空数组,如果需要往里面添加元素,再对数组进行扩容,代码如下

private void ckeckAndGrow() {
    if (size == 0){
        array = new int [capacity];
    }
    else if (size == capacity){
        //扩容
        capacity += capacity>>1;
        int [] newArray = new int[capacity];
        System.arraycopy(array,0,newArray,0,size);
        array = newArray;
    }
}

测试代码

 @Test
    public void test6() {
        Dynamicarrays dynamicarrays = new Dynamicarrays();
        for (int i = 0; i < 9; i++) {
            dynamicarrays.addLast(i + 1);
        }
        dynamicarrays.foreach((element) -> {
            System.out.println(element);
        });
    }
  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值