打怪升级之小白的大数据之旅(十九)
Java面向对象进阶之集合二List集合
上次回顾
上一章,我对集合的基本概念以及常用方法、如何遍历等知识点进行了分享,我们本章学到的Collection的List集合,以及下一章介绍的Set集合都是继承自collection,像案例中的add等方法,也是继承自它,后面案例就不一一说明了。下面开始进入正题
List集合
概述
- 我们掌握了Collection接口的使用后,再来看看Collection接口中的子接口,他们都具备那些特性呢?
- java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合
List接口的特点
- List集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)
- 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。
- 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
- 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
- 举个栗子: 这就像银行门口客服,给每一个来办理业务的客户分配序号:第一个来的是“张三”,客服给他分配的是0;第二个来的是“李四”,客服给他分配的1;以此类推,最后一个序号应该是“总人数-1”
注:List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如“张三”可以领取两个号
List接口特有方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
1、添加元素
- void add(int index, E ele)
- boolean addAll(int index, Collection<? extends E> eles)
2、获取元素
- E get(int index)
- List subList(int fromIndex, int toIndex)
3、获取元素索引
- int indexOf(Object obj)
- int lastIndexOf(Object obj)
4、删除和替换元素
- E remove(int index)
- E set(int index, E ele)
注:
- List集合特有的方法都是跟索引相关
- 在JavaSE中List名称的类型有两个,一个是java.util.List集合接口,一个是java.awt.List图形界面的组件,别导错包了
老样子,综合练习–>示例代码:
public class ListDemo {
public static void main(String[] args) {
// 创建List集合对象
List<String> list = new ArrayList<String>();
// 往 尾部添加 指定元素
list.add("图图");
list.add("小美");
list.add("不高兴");
System.out.println(list);
// add(int index,String s) 往指定位置添加
list.add(1,"没头脑");
System.out.println(list);
// String remove(int index) 删除指定位置元素 返回被删除元素
// 删除索引位置为2的元素
System.out.println("删除索引位置为2的元素");
System.out.println(list.remove(2));
System.out.println(list);
// String set(int index,String s)
// 在指定位置 进行 元素替代(改)
// 修改指定位置元素
list.set(0, "三毛");
System.out.println(list);
// String get(int index) 获取指定位置元素
// 跟size() 方法一起用 来 遍历的
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//还可以使用增强for
for (String string : list) {
System.out.println(string);
}
}
}
ArrayList实现类
概述
我们前面的例子都是使用ArrayList实现类来演示集合的各种知识点,下面,我来详细介绍一下ArrayList实现类
- ArrayList是List接口的典型实现类
- ArrayList底层是数组,也可以说它的数据结构是顺序结构(是不是早都觉得它跟数组一样了),但它是使用长度可变的数组,其常用方法都来自collection和List接口
- 正因为底层使用了数组存储结构,所以它具有查询快,增删慢的特点
- 与Vector实现类比较:ArrayList是线程不安全的,效率相对高,虽然Vector线程安全,但几乎已经被弃用了(因此,我就不介绍Vector了)
ArrayList源码分析
- 既然它的底层是数组,那么它的可变长是怎么实现的呢?
(1). 构造方法: new ArrayList(): jdk6中,空参构造直接创建10长度的数组 jdk7(新版)jdk8中,默认初始容量0,在添加第一元素时初始化容量为10 new ArrayList(int initialCapacity): 指定初始化容量 (2). 添加元素:add(E e); 首次添加元素,初始化容量为10 每次添加修改modCount属性值 每次添加检查容量是否足够,容量不足时需要扩容,扩容大小为原容量的1.5倍 (3). 移除元素:remove(E e); 每次成功移除元素,修改modCount值 每次成功移除需要需要移动元素,以保证所以元素是连续存储的(删除操作效率低的原因
- 以添加元素举例:在源码中,当我们创建了一个ArrayList对象时,它会在内存中开辟一个初始容量为0的ArrayLIst数组空间,当添加了第一个元素,它会创建一个新的数组,并且将该数组空间长度扩容到10,然后复制原数组的元素内容,如果超过该长度,就会重新创建一个原数组长度基础上扩容1.5倍的新数组,而后再次对原数组元素进行复制
- 因此,它的特点就是查询效率很高,但是增删效率低
- 下面,我还是老样子,使用一个综合的实例来演示ArrayList的创建,方法使用和ArrayList的遍历:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; /* 创建ArrayList集合,使用泛型 分别存储Double,字符串,自定义对象Car: brand,color,price 使用不同方式进行遍历 * */ import java.lang.Math; public class Demo3 { // 主方法入口 public static void main(String[] args) { // List实现类Arraylist // 定义汽车对象集合 List<Car> carArray = new ArrayList<>(); carArray.add(new Car("牧马人", "橙色", 400000.00)); carArray.add(new Car("领克01", "黑色", 180000.00)); carArray.add(new Car("领克03", "渐变银", 200000.00)); carArray.add(new Car("红旗", "玫瑰红", 550000.00)); // 定义Double集合 List<Double> doubleArray = new ArrayList<>(); doubleArray.add(Math.random()); doubleArray.add(Math.random()); doubleArray.add(Math.random()); doubleArray.add(Math.random()); doubleArray.add(Math.random()); // 定义字符串集合 List<String> stringArray = new ArrayList<>(); stringArray.add("喵星人"); stringArray.add("汪星人"); stringArray.add("撸串星人"); stringArray.add("火星人"); stringArray.add("JK星人"); stringArray.add("挺尸星人"); stringArray.add("舒夫斯基星人"); // 对三个集合使用三种方式进行遍历 // carArray--->迭代器遍历 Iterator<Car> carIt = carArray.iterator(); while (carIt.hasNext()) { Car car = carIt.next(); System.out.println(car.toString()); } System.out.println("——————————————————我是分割线——————————————————"); // doubleArray--->增强for遍历 for (Double obj : doubleArray) { System.out.println(obj); } System.out.println("——————————————————我是分割线——————————————————"); // stringArray--->普通for遍历 for (int i = 0; i < stringArray.size(); i++) { System.out.println(stringArray.get(i)); } } } class Car { // 定义汽车属性 private String brand; // 品牌 private String color; // 颜色 private double price; // 价格 // 定义构造器 public Car(String brand, String color, double price) { this.brand = brand; this.color = color; this.price = price; } // 定义getter/setter方法 public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } // 重写toString @Override public String toString() { return "汽车:{" + "品牌='" + brand + '\'' + ", 颜色='" + color + '\'' + ", 价格=" + price + '}'; } }
数据结构
- 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型
- 首先我们看数据结构的整体框架图:
- 框架图例解释:
- 逻辑结构:描述元素之间的关联关系
- 物理结构:存储结构,有一定关系的元素数据如何存储
- 数据运算,对数据结构中的数据通过运算进行操作
- 数据结构跟下面要讲的泛型,我后面会详细讲解,大家先有个初步概念即可,正如上面所讲,数据结构就是存储数据而衍生出的存储逻辑,逻辑结构就是根据数据之间的关系进行存储,物理结构就是在真实的生活中,怎么按照逻辑结构将它们存储起来
泛型初体验
泛型我后面会单独来讲,因为在下面的案例中会遇到对实例对象进行转型的情况,为了偷懒,我就先简单安利一下泛型的使用。
- 还记得上一章中的的串串星人的案例么?:
public class IteratorDemo {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("串串星人");
coll.add("吐槽星人");
coll.add("汪星人");
//遍历
//使用迭代器 遍历 每个集合对象都有自己的迭代器
Iterator<String> it = coll.iterator();
// 泛型指的是 迭代出 元素的数据类型
while(it.hasNext()){ //判断是否有迭代元素
//String s = (String)it.next();//获取迭代出的元素,因为不知道it.next()的类型,因此需要向下转型
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
}
}
当使用了泛型后,就不需要进行向下转型这个操作了,是不是很方便?下面简单介绍一下用法:
- 泛型的使用:
当以这个语法格式对这个集合进行限定后,创建出来的集合,只能存放与限定类型相关的操作// 以创建类举例 Collection<限定的数据类型> 变量名 = new Collection<>();
- 示例代码:
import java.util.ArrayList;
import java.util.Collection;
public class Demo {
public static void main(String[] args) {
// 使用泛型定义这个集合只能操作String数据类型相关的操作
Collection<String> str1 = new ArrayList<>();
str1.add("abc");
str1.add(123);//因为限定了类型为String,所以这行会报异常
}
}
好了,泛型大概就介绍这么多,详细的后面会讲(我可以正大光明偷懒了~~)
LinkedList实现类
概述
- 前面在介绍ArrayList时还介绍了数据结构,这是为了引出LinkedList实现类
- LinkedList是List的另一个常用实现类
- LinkedList底层数据结构是链表结构(而且它是双向链表)
- 特点:增删块,查询慢
上图是单向链表,它有两部分,一个是数据主体data一个是指向下一个节点的next
双向链表的理解
- 首先说一下链表是什么,链表也称之为链式结构,ArrayList是顺序结构,它在数据存储时,一个连续的内存空间,就像这样
- LinkedList是这个样子的
- 图例解释:
- 节点: 我们将data pre next这三个的整体称之为节点
- pre:pre保存的是指向它的上一个节点的地址/坐标
- data:这个数据的主体
- next:next保存的是指向它的下一个节点的地址/坐标
- first: 指的是整个链表的第一个节点
- last:指的是整个链表的最后一个节点
- 正如图中那样,当我们需要添加一个元素时,只需要变动两个节点的指向就可以快速的添加数据,删除同理,而顺序结构需要将整个数组前移(删除)后移(添加)
LinkedList特有方法
- LinkedList除了继承List方法外,它还有针对首尾元素的操作方法
void addFirst(Object obj )
添加元素到第一个节点void addLast(Object obj )
添加元素到最后一个节点Object getFirst()
获取第一个节点元素Object getLast()
获取最后一个节点元素Object removeFirst()
删除第一个节点元素Object removeLast ()
删除最后一个节点元素
LinkedList源码分析
- 源码:
- LinkedList在源码中,使用Node表示一个完整的元素节点
- 使用添加元素举例,我们来对LInkedList的存储逻辑进行分析,主要是为了理解链式结构的逻辑,为后面的hash表做铺垫
- 根据源码图,我来说明一下添加元素的步骤(注:节点中包含了元素的数据和next,prev指向):
- 当添加第一个元素时,它的节点创建,并且它的next指向和prev指向都是null的,因为只有它一个,也因此,first节点和last节点都是它自己
- 好了第一个节点添加完毕,下面我只演示双向链表的那个链条的生成,其他都是重复的,我就不截图了
- 第二步比较绕,多调试几遍就懂了
- 第一步,因为已经创建了第一个节点了,其第一个节点first,最后一个节点last都是它自己,当添加第二个元素时,会将第一个节点的地址存储到 临时变量
l
中 - 第二步,会将last指向新创建的节点
- 第三步,判断
l
临时变量是否为空,不为空则将l
存储的节点 next指向到新创建的节点上,至此,last指向上一个节点,第一个节点的next就指向了第二个节点 - 此时,双向链表就成功建立了
- 第一步,因为已经创建了第一个节点了,其第一个节点first,最后一个节点last都是它自己,当添加第二个元素时,会将第一个节点的地址存储到 临时变量
- 示例代码:
package test01LinkedList;
import java.util.LinkedList;
// LinkedList源码分析
public class Demo {
public static void main(String[] args) {
LinkedList<String> str1 = new LinkedList<>();
// 集合普通方法
str1.add("hello");
str1.add("world");//我打断点的地方在这里
str1.add("hahaha");
// 特有方法
str1.addFirst("我是第一个");
str1.addLast("我是最后一个");
// 删除元素
str1.remove(1);
// 增强for遍历
for (String s : str1) {
System.out.println(s);
}
}
}
使用LinkedList实现队列和栈
-
LinkedList也实现了Deque接口(双端队列),此接口提供了实现队列和栈结构的方法
-
这算是LinkedList的核心用法了,所以我有必要再向大家介绍一下队列和栈的知识点
-
队列:是一种抽象的数据结构,数据存取特点为先进先出(FIFO),LinkedList中的队列方法:
- boolean offer(Object obj) : 入队
- Object poll() : 出队
- Object peek() : 检查
-
栈:是一种抽象的数据结构,数据存取特点为后进先出(LIFO),LinkedList中的栈方法:
- void push(E e) :压栈
- E pop() :弹栈
-
下面我举个实际的案例,让大家理解一下这两个特殊的数据结构:
队列(就像地铁安检):
package test02QueueStack.Demo;
import org.junit.Test;
import java.util.LinkedList;
/*
* 利用LinkedList实现队列
* */
public class Demo {
// 队列的实现
@Test
public void queueTest(){
LinkedList<String> list = new LinkedList<>();
// 模拟地铁安检
System.out.println("地铁安检口需要对每个乘客的背包进行检查,乘客开始将背包放入安检设备中。。。");
// 入队
System.out.println("--------------------------------------------");
list.offer("第一个包");
System.out.println(list);
list.offer("第二个包");
System.out.println(list);
list.offer("第三个包");
System.out.println(list);
System.out.println("--------------------------------------------");
// 出队
System.out.println("检查: "+list.poll()+"完毕,谢谢配合,请取走您的背包");
System.out.println("--------------------------------------------");
System.out.println(list);
System.out.println("--------------------------------------------");
System.out.println("检查: "+list.poll()+"完毕,谢谢配合,请取走您的背包");
System.out.println("--------------------------------------------");
System.out.println(list);
System.out.println("--------------------------------------------");
System.out.println("检查: "+list.poll()+"完毕,谢谢配合,请取走您的背包");
System.out.println("--------------------------------------------");
System.out.println(list);
System.out.println("--------------------------------------------");
}
栈:
package test02QueueStack.Demo;
import org.junit.Test;
import java.util.LinkedList;
/*
* 利用LinkedList实现栈
* */
public class Demo {
// 栈的实现
@Test
public void stackTest(){
LinkedList<String> list = new LinkedList<>();
// 压栈
System.out.println("开始向子弹的弹夹装子弹~~~");
list.push("第一发子弹");
System.out.println(list);
list.push("第二发子弹");
System.out.println(list);
list.push("第三发子弹");
System.out.println(list);
System.out.println("子弹装填完毕,准备扣扳机开枪...");
// 出栈
System.out.println("biu~~~~~");
System.out.println(list.pop()+"发出,命中目标");
System.out.println("剩余子弹:"+list);
System.out.println("--------------------------------------------");
System.out.println("biu~~~~~");
System.out.println(list.pop()+"发出,命中目标");
System.out.println("剩余子弹:"+list);
System.out.println("--------------------------------------------");
System.out.println("biu~~~~~");
System.out.println(list.pop()+"发出,命中目标");
System.out.println("剩余子弹:"+list);
System.out.println("--------------------------------------------");
}
}
ListIterator
- List 集合额外提供了一个 listIterator() 方法,该方法返回一个 ListIterator 对象, ListIterator 接口继承了 Iterator 接口
- 它是专门为List服务的迭代器接口
- ListIterator 的方法
- void add():通过迭代器添加元素到对应集合
- void set(Object obj):通过迭代器替换正迭代的元素
- void remove():通过迭代器删除刚迭代的元素
- boolean hasPrevious():如果以逆向遍历列表,往前是否还有元素。
- Object previous():返回列表中的前一个元素。
- int previousIndex():返回列表中的前一个元素的索引
- boolean hasNext()
- Object next()
- int nextIndex()
- 这个没什么太多的知识点,主要是增加了可以逆序的遍历,当有这个需求的时候调用它就好
- 示例代码:
public static void main(String[] args) { List<Student> c = new ArrayList<>(); c.add(new Student(1,"张三")); c.add(new Student(2,"李四")); c.add(new Student(3,"王五")); c.add(new Student(4,"赵六")); c.add(new Student(5,"钱七")); //从指定位置往前遍历 ListIterator<Student> listIterator = c.listIterator(c.size()); while(listIterator.hasPrevious()){ Student previous = listIterator.previous(); System.out.println(previous); } }
总结
- 本章主要对collection集合中的List集合以及其实现类ArryList、LinkedList进行介绍,也是下一章学习Set基础,而数据结构这块,是为了后面的List/Set/Map进行铺垫,我们先了解数据结构的构成以及基本概念,当介绍到后面的实现类时,我会根据案例来让大家理解数据结构的具体情况。
- 好了,本章内容就是这些,大家不要纠结源码的分析那里,源码只是为了更好的理解相关类的使用。老样子,对于我分享的内容,里面有很多知识点都是个人理解,可能会和真正的理论有些偏差,有问题随时后台留言吐槽。
- 下一章,Set集合,学完Set,整个集合框架中两大根接口中的Collection就介绍完毕了,我会在后面对整个Collection进行总结。