Java集合框架之包装类,ArrayList与迭代器

举报
未见花闻 发表于 2022/05/31 17:48:35 2022/05/31
【摘要】 本篇文章带大家认识Java集合——ArrayList,在Java中ArrayList就是顺序表,底层是利用数组实现的,在博主历史文章中已经对顺序表进行了模拟实现,所以本篇文章会以ArrayList的简单模拟实现引出泛型和包装类,为后续泛型开一个小头,除此之外还会介绍Java中ArrayList类,以及迭代器。

⭐️前面的话⭐️

本篇文章带大家认识Java集合——ArrayList,在Java中ArrayList就是顺序表,底层是利用数组实现的,在博主历史文章中已经对顺序表进行了模拟实现,所以本篇文章会以ArrayList的简单模拟实现引出泛型和包装类,为后续泛型开一个小头,除此之外还会介绍Java中ArrayList类,以及迭代器。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年5月31日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《Java核心技术卷1》,📚《数据结构》,📚《Java编程思想》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


题外话:在正式开始之前,我们先来看一看集合框架,画上√的表示博主已经介绍完毕了,可以翻看JavaSE系列专栏进行寻找,画上⭐️表示本文所介绍的内容。
1-1


1.ArrayList的简单模拟

1.1初见泛型

1.1.1泛型的作用

对于泛型,在前面介绍比较器的时候已经提到过,使用泛型可以使类参数化,可以将类型进行参数化。那究竟泛型是什么呢?
泛型表示一个不具体的类,Java 泛型的参数只可以代表类,不能代表个别对象。
Java中使用<>可以实现类型的参数化,尖括号中使用一个大写字母,常用T E,来表示一个泛型类型,相当于方法参数中的“形参”。
我们来看一段没有使用泛型定义顺序表结构的代码:

public class MyArrayList {
    public int[] elementData;
    public int size;
}

显然有一个缺陷,就是只能储存整型类的数据,不能实现其他类型的数据,当然这里你会想将int改为Object,因为Object是所有类的父类,所以可以接受所有类型的参数,但是这样会混乱,因为在一个Object数组中可以存放多种类型的数据,这样这个数组就成了一个大杂烩,里面整型,浮点型,字符串等样样都能放,完全乱套了,于是最好的解决方案是使用泛型,将类型作为参数一样,指定该数组里面存放什么样的数据类型。
使用泛型后的代码:

public class MyArrayList<E> {
    public E[] elementData;
    public int size;
}

但是Java中不允许创建泛型数组,因为创建泛型数组后往往需要对数组进行强制类型转换,对数组类型强转时,仅仅只是将数组的引用类型强转,其数组内部元素的类型并没有发生改变(或者说数组类型是一种单独的类型,包括Object[]也是Object的子类,但其他类型的数组并不是Object[]的子类,所以对数组进行强转可能会发生异常),因此Java中对数组进行强转可能会发生异常,真正使用泛型定义数组需要用到反射,但本文不牵涉反射,等后续介绍反射时,再来解决这个问题。
1-2
目前使用强转的形式让泛型顺序表的模拟能够继续进行下去。
1-3
编译器也给出了警告,现在就暂时这么写吧,这种写法并不是完全正确的写法,但是在这里能够保证顺序表能够继续模拟下去。

1.1.2泛型擦除机制

除此之外,泛型是在编译期内进行检查类型信息的,而数组是在运行期进行检查的,这也进一步说明了直接创建泛型数组会报错的。泛型在编译期会对泛型符号(大写的T,E等)进行擦除,尖括号里面的类型不参与类型的组成,会在编译时期被擦除替换成Object,这就是泛型中的擦除机制。

1.2包装类

1.2.1基本数据类型的包装类

由于泛型中尖括号内的内容会被擦除成Object,所以传入的必须是类不能是基本数据类型,但是Java中有8种基本数据类型,难道这些数据类型不能使用泛型进行参数传递吗?答案是否定的,这8种数据类型都有其对应的包装类:

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

1.2.2装包与拆包

所谓装包与拆包,使用一个代码你就明白了:

Integer a = 2022;//隐式的装包
int b = a;//隐式的拆包

像这样就叫做装包与拆包,为什么说它是隐式的呢,我们来查看这两句代码的反汇编:
1-4
因为上面两句代码分别调用了valueOfintValue方法,并不是直接表示出来,所以说上面的装包与拆包是隐式的表达,而显示的装包是直接调用valueOf方法或构造方法,拆包是直接调用intValuedoubleValue等方法。

Integer c = new Integer(2022);
Integer d = Integer.valueOf(2022);//显式装包
        
int e = c.intValue();
double f = c.doubleValue();//显式拆包

上面举例的是Integer包装类的拆包装包的方法,其他包装类也是一样的,建议去翻看源码,里面有许多有用的方法,比如将一个字符串转数字,可以采用parseIntvalueOf方法。

对于装包还有一个问题,看如下代码会输出什么:

public class Test {
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;
        Integer c = 128;
        Integer d = 128;
        System.out.println(a==b);
        System.out.println(c==d);
    }
}

相信很多同学都会说均输出true,但事实真的是如此吗?看看运行结果:

1-6
打脸了,127那个是真,128则变成了假,真是为什么呢?因为这是Integer的装包,我们去valueOf方法的源码一探究竟!
1-7
1-8
根据源码可知-128~127范围内,创建的Integer对象会指向缓冲区的对象(Integer常量池),超过这个范围就会new新对象,因此上面的运行结果是true与false。

1.3简单模拟ArrayList(顺序表)

知道了泛型与包装类,来简单模拟一下ArrayList的增删查改吧。

顺序表的基本结构: 一个数组(泛型)+元素有效个数索引

public class MyArrayList<E> {
    public E[] elementData;
    public int size;
}

顺序表插入元素:

    //插入元素
    public boolean add(E val) {
        if (isFull()) {
            this.elementData = Arrays.copyOf(elementData, 2 * elementData.length);
        }
        elementData[size++] = val;
        return true;
    }
     //检查顺序表是否满
    public  boolean isFull() {
        return this.size >= elementData.length;
    }

顺序表删除元素:

    public boolean remove(int index) {
        if (index < 0 || index >= size) return false;
        for (int i = size - 1; i > index; i--) {
            elementData[i- 1] = elementData[i];
        }
        size--;
        return true;
    }

顺序表查与改:

	//修改元素
    public boolean set(int index, E val) {
        if (index < 0 || index >= size) return false;
        elementData[index] = val;
        return true;
    }
    //查找元素
    public int search(E k) {
        for (int i = 0; i < size; i++) {
            if (k.equals(elementData[i])) {
                return i;
            }
        }
        return -1;
    }

顺序表toString方法:

 @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder("[");
        for (int i = 0; i < size - 1; i++) {
            stringBuilder.append(elementData[i]);
            stringBuilder.append(",");
        }
        stringBuilder.append(elementData[size-1]);
        stringBuilder.append("]");
        return stringBuilder.toString();
    }

测试代码:

public class Test {
    public static void main(String[] args) {
        MyArrayList<Integer> ls1 = new MyArrayList<>();
        MyArrayList<String> ls2 = new MyArrayList<>();
        ls1.add(2022);
        ls1.add(2333);
        ls1.add(6666);
        System.out.println(ls1);
        ls2.add("天");
        ls2.add("天");
        ls2.add("开");
        ls2.add("心");
        System.out.println(ls2);
        ls1.remove(1);
        ls2.remove(2);
        System.out.println(ls1);
        System.out.println(ls2);
        System.out.println(ls1.search(2024));
        System.out.println(ls2.search("心"));
        ls1.set(0, 4399);
        ls2.set(1,"乐");
        System.out.println(ls1);
        System.out.println(ls2);
    }
}

运行结果:
1-5

2.ArrayList类的使用

2.1ArrayList与List

如图为ArrayList的全部继承实现关系:
2-1
根据这张继承图,我们可以知道ArrayList支持克隆,序列化,for-each遍历,随机访问,但是ArrayList不支持多线程,支持多线程并支持顺序表需要使用CopyOnWriteArrayList或者Vector。
2-2

我们注意到ArrayList实现了List接口,List表示线性表,可以接受ArrayList(顺序表),以及LinkedList(链表,也可以作队列与栈)的引用,所以List接口可以使用顺序表,链表,栈与队列多种线性的数据结构。

//List用法:List<类> 引用名 = new 实现List接口的类;
List<Integer> list = new ArrayList<>();
List<String> linklist = new LinkedList<>();

List接口常用方法:不再演示了,因为大部分都是增删查改,而且和ArrayList中大部分方法用法相同,后文将会演示ArrayList类部分方法的使用。

方法 解释
boolean add(E e) 尾插 对象e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) 将对象c 中的元素尾插到线性表中
E remove(int index) 删除 索引为index 位置元素
boolean remove(Object o) 删除遇到的第一个 元素o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空线性表
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 线性表

2.2ArrayList常用方法

2.2.1构造方法

因为ArrayList实现了List接口,所以List里面所有的方法在ArrayList都可以使用。先来说一说ArrayList的构造方法吧,该类有3个构造方法:
3-1

前两个比较简单,来演示一下稍微复杂的第三个构造方法吧!第三个构造方法所需传入的参数为Collection类,可以理解为传入一个集合引用参数来创建一个顺序表,新建顺序表会根据传入参数来构建顺序表,当然传入的参数不一定是Collection,只要实现了Collection接口的所有类都可以。

public class TestDemo {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(10);
        list.add(12);
        ArrayList<Integer> arrayList1 = new ArrayList<>(list);//使用LinkedList对象构造
        System.out.println(arrayList1);
        ArrayList<Integer> arrayList2 = new ArrayList<>(arrayList1);//使用ArrayList对象构造
        System.out.println(arrayList2);
    }
}

3-2
关于ArrayList底层扩容机制的结论:
如果ArrayList调用不带参数的构造方法,那么顺序表初始大小为0,只有第一次插入时,顺序表容量才会扩容至10,也就是说顺序表从0开始创建时,初始默认创建扩容容量为10,后面再次扩容是以上一次容量的1.5倍进行扩容。
如果ArrayList调用自定义给定容量的构造方法,那么顺序表初始大小为你给定的容量,后续扩容按1.5倍原容量进行扩容。

2.2.2常用方法

方法 解释
boolean add(E e) 尾插 对象e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) 将Collection对象c 中的元素尾插到线性表中
E remove(int index) 删除 索引为index 位置元素
boolean remove(Object o) 删除遇到的第一个 元素o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空线性表
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 线性表
public Object[] toArray() 将顺序表转换成数组
public Object clone() 复制一个ArrayList副本

上面的增删查改方法我就演示了,我挑几个相对来说比较难和面生的几个方法来演示。

方法 解释
boolean addAll(Collection<? extends E> c) 将Collection对象c 中的元素尾插到线性表中
List<E> subList(int fromIndex, int toIndex) 截取部分 线性表
public Object[] toArray() 将顺序表转换成数组
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(2022);
        arrayList.add(1024);
        arrayList.add(2333);
        arrayList.add(1314);//arrayList = [2022,1024,2333,1314]
        //AddAll方法
        ArrayList<Integer> data = new ArrayList<>();
        data.addAll(arrayList);
        System.out.println(data);
        //subList方法
        List<Integer> sub = data.subList(1,3);//左开右闭,剪切第2,3个元素
        System.out.println(sub);
        //toArray方法
        Object[] arr = data.toArray();//转数组
        System.out.println(Arrays.toString(arr));
    }

3-3

2.3迭代器Iterator与ListIterator

对于ArrayList类,它实现了Iterator与ListIterator接口,其中ListIterator接口扩展了Iterator接口,因此ListIterator接口里面有Iterator接口中的所有方法,在此基础上ListIterator接口新增了addset等方法。
4-1

4-2
4-3
那迭代器有什么用呢?对于ArrayList它可以用来遍历,用法和Scanner类相似, hasNext方法用来判断迭代的下一个对象是否存在,next方法用来迭代对象,迭代出对象后才可以使用迭代器中的removesetadd等方法去删除,修改迭代出的对象,或再迭代对象后插入元素。

注意必须先使用next迭代出对象,再进行操作,否则会出现IllegalStateException异常。

ArrayList中常用迭代器方法:

方法 解释
boolean hasNext() 判断待迭代对象是否存在,存在为true,否则为false
E next() 迭代对象
void remove() 删除迭代出的对象
void set(E e) 讲迭代出的对象修改为e
void add(E e) 在迭代对象后插入

本表前三个方法Iterator与ListIterator接口均有,最后两个仅仅只有ListIterator接口有。
演示一下迭代器的基本使用方式吧,就以使用迭代器遍历打印顺序表元素为例。

    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(2022);
        arrayList.add(1024);
        arrayList.add(2333);
        arrayList.add(1314);
        ListIterator<Integer> it = arrayList.listIterator();
        while (it.hasNext()) {
            Integer ret = it.next();
            System.out.print(ret + "  ");
        }
    }

4-4
好了,本文就分享到这里了,下次再见!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。