Java集合框架之包装类,ArrayList与迭代器
⭐️前面的话⭐️
本篇文章带大家认识Java集合——ArrayList,在Java中ArrayList就是顺序表,底层是利用数组实现的,在博主历史文章中已经对顺序表进行了模拟实现,所以本篇文章会以ArrayList的简单模拟实现引出泛型和包装类,为后续泛型开一个小头,除此之外还会介绍Java中ArrayList类,以及迭代器。
📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年5月31日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《Java核心技术卷1》,📚《数据结构》,📚《Java编程思想》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
题外话:在正式开始之前,我们先来看一看集合框架,画上√的表示博主已经介绍完毕了,可以翻看JavaSE系列专栏进行寻找,画上⭐️表示本文所介绍的内容。
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.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;//隐式的拆包
像这样就叫做装包与拆包,为什么说它是隐式的呢,我们来查看这两句代码的反汇编:
因为上面两句代码分别调用了valueOf
和intValue
方法,并不是直接表示出来,所以说上面的装包与拆包是隐式的表达,而显示的装包是直接调用valueOf
方法或构造方法,拆包是直接调用intValue
,doubleValue
等方法。
Integer c = new Integer(2022);
Integer d = Integer.valueOf(2022);//显式装包
int e = c.intValue();
double f = c.doubleValue();//显式拆包
上面举例的是Integer
包装类的拆包装包的方法,其他包装类也是一样的,建议去翻看源码,里面有许多有用的方法,比如将一个字符串转数字,可以采用parseInt
或valueOf
方法。
对于装包还有一个问题,看如下代码会输出什么:
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
,但事实真的是如此吗?看看运行结果:
打脸了,127那个是真,128则变成了假,真是为什么呢?因为这是Integer
的装包,我们去valueOf
方法的源码一探究竟!
根据源码可知-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);
}
}
运行结果:
2.ArrayList类的使用
2.1ArrayList与List
如图为ArrayList的全部继承实现关系:
根据这张继承图,我们可以知道ArrayList支持克隆,序列化,for-each
遍历,随机访问,但是ArrayList不支持多线程,支持多线程并支持顺序表需要使用CopyOnWriteArrayList或者Vector。
我们注意到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个构造方法:
前两个比较简单,来演示一下稍微复杂的第三个构造方法吧!第三个构造方法所需传入的参数为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);
}
}
关于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));
}
2.3迭代器Iterator与ListIterator
对于ArrayList类,它实现了Iterator与ListIterator接口,其中ListIterator接口扩展了Iterator接口,因此ListIterator接口里面有Iterator接口中的所有方法,在此基础上ListIterator接口新增了add
,set
等方法。
那迭代器有什么用呢?对于ArrayList它可以用来遍历,用法和Scanner类相似, hasNext
方法用来判断迭代的下一个对象是否存在,next
方法用来迭代对象,迭代出对象后才可以使用迭代器中的remove
,set
,add
等方法去删除,修改迭代出的对象,或再迭代对象后插入元素。
注意必须先使用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 + " ");
}
}
好了,本文就分享到这里了,下次再见!
- 点赞
- 收藏
- 关注作者
评论(0)