文章出处:https://www.javazhiyin.com/tag/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F
作者:凸凹里歐
方法迭代:代的更迭,从初代到末代的遍历,指对某类集合中的每个元素按顺序取出的行为。举个例子,通常我们读小说是从前往后翻,一页接着一页地读,这样我们才可以了解一个连续完整的故事,那这就需要我们顺序地迭代整本书的每一页内容。
相信大家都用过集合类吧,最常用的比如List,Set,Map以及各种各样不同数据表示实现,总之是把某一批类似的元素按某种数据结构集合起来作为一个整体来引用,不至于元素丢的到处都是难以维护,当要用到每个元素的时候,我们需要将它们一个个的取出来,但是对不同的数据类型访问方式各有不同,于是我们就需要定义统一的迭代器来标准化这种遍历行为。
在此着重补充下:接口继承接口,接口不实现接口;抽象类继承接口 ;类继承类/抽象类。
为何会有各种各样的遍历方式呢?比如说弹夹,装填子弹的时候要一颗一颗的进行压栈,等到射击的时候就需要迭代操作,先出栈拿出最后装填的子弹再进行射击,然后反向往前遍历直到最初装填的子弹直到射完为止,此刻也代表着迭代的结束,整个过程像是内存栈的操作,先进后出,后进先出,当然这并不代表其迭代器非要先进后出,这里只是举例说明针对不同数据类型进行不同的迭代方式。
以上数据结构及迭代器其实都有现成的类去实现,那么我们这里来自定义一种全新的数据结构,可以防止碰瓷!?牛吹得有点大了,我们就以行车记录仪举例,大家先想想怎么来记录一段一段的视频呢?如果我们简单的利用ArrayList去记录,那它得有多大空间去支持一直拍摄视频?
我们知道其实它是循环覆写的,待空间不够用时,最新的视频总会去覆盖掉最老的视频,以首尾相接的环形结构解决空间有限的问题。好,开始代码实战,首先我们定义一个行车记录仪类。
public class DrivingRecorder {
private int index = -1;// 当前记录位置
private String[] records = new String[10];// 假设只能记录10条视频
public void append(String record) {
if (index == 9) {// 循环覆盖
index = 0;
} else {
index++;
}
records[index] = record;
}
public void display() {// 循环数组并显示所有10条记录
for (int i = 0; i < 10; i++) {
System.out.print("[" + i + "]:" + records[i] + " ");
}
}
public void displayInOrder() {//按顺序从新到旧显示10条记录
for (int i = index, loopCount = 0; loopCount < 10; i = i == 0 ? i = 9 : i - 1, loopCount++) {
System.out.print(records[i]+" ");
}
}
public static void main(String[] args) {
DrivingRecorder drivingRecorder = new DrivingRecorder();
for (int i = 0; i < 11; i++) {
drivingRecorder.append(String.valueOf(i));
}
drivingRecorder.display();
System.out.println();
drivingRecorder.displayInOrder();
System.out.println();
}
}
输出结果:
[0]:10 [1]:1 [2]:2 [3]:3 [4]:4 [5]:5 [6]:6 [7]:7 [8]:8 [9]:9
10 9 8 7 6 5 4 3 2 1
假设我们的记录仪存储空间只够录10段视频,我们定义一个原始的字符串数组(第3行)来模拟记录,并且用一个游标(第2行)来记录当前记录所在位置。当插入视频的时候(第5行)我们得先看有没有录满到头了,如果是的话就要把游标调整到头以后再记录视频。视频目前可以循环记录了,但总得给用户显示出来看吧,于是我们又提供了两个显示方法,一个是按默认数组顺序显示,一个是按用户习惯从新到旧地显示内容(逻辑稍微复杂了点但这里不是重点,读者可以略过),开始写用户类来使用这个记录仪。
public class Client {
public static void main(String[] args) {
DrivingRecorder dr = new DrivingRecorder();
//假设记录了12条视频
for (int i = 0; i < 12; i++) {
dr.append("视频_" + i);
}
dr.display();
/*按原始顺序显示,视频0与1分别被10与11覆盖了。
0: 视频_10
1: 视频_11
2: 视频_2
3: 视频_3
4: 视频_4
5: 视频_5
6: 视频_6
7: 视频_7
8: 视频_8
9: 视频_9
*/
dr.displayInOrder();
/*按顺序从新到旧显示
视频_11
视频_10
视频_9
视频_8
视频_7
视频_6
视频_5
视频_4
视频_3
视频_2
*/
}
}
输出结果:
[0]:视频_10 [1]:视频_11 [2]:视频_2 [3]:视频_3 [4]:视频_4 [5]:视频_5 [6]:视频_6 [7]:视频_7 [8]:视频_8 [9]:视频_9 视频_11 视频_10 视频_9 视频_8 视频_7 视频_6 视频_5 视频_4 视频_3 视频_2
我们以视频_0开始,假设空间已经记录到视频_11,一共12条视频会不会撑爆空间呢?我们来运行以下看会发生什么。奇迹出现了,视频_10和视频_11分别覆盖了最早记录的视频_0和视频_1,完美!产品可以量产了!
正当我们要举杯欢庆的时候客户开始吐槽了,你只是简单在屏幕上显示一下就完事了么?功能也太差了点!我要的是把原始视频拿出来给我,我好上报交警作为证据,总之你甭管我怎么加工处理,你总得把原始数据拿出来给我。
这可把我们难住了,这些数据都是在记录仪内部封装好的私有数据,如果直接改成public暴露出去,试想用户随意增加删除,完全不管游标位置,这会破坏掉内部逻辑机制,数据封装的意义何在?我们鬼斧神工设计将瞬间崩塌,用户数据安全无法保证,bug肆虐。
所以,我们绝不能更改数据的私有化封装,而之前暴露给用户的显示方法显得非常死板,扩展性极差,我们决定以迭代器取而代之,如此提供给用户遍历数据的功能,拿出去的数据用户便可以随意使用,这样就避免了用户染指内部机件的危险。首先我们需要定义一个迭代器接口标准来规范抽象,看代码。
package com.liuxd;
/**
* Created by Liuxd on 2018/11/7.
*/
public interface Iterator<E> {
E next();//返回下一个元素
boolean hasNext();//是否还有下一个元素
}
很简单吧?此接口标准定义了两个方法,next方法用于返回下一个数据元素,而hasNext用于询问迭代器是否还有下一个元素,当然我们也可以不定义这个接口,而是直接用JDK中util包自带的。接下来更改我们的行车记录仪,加入iterator方法用于获取迭代器,开始我们的迭代器实现。
package com.liuxd;
/**
* Created by Liuxd on 2018/11/7.
*/
public class DrivingRecorder2 {
private int index = -1;// 当前记录位置
private String[] records = new String[10];// 假设只能记录10条视频
public void append(String record) {
if (index == 9) {// 循环覆盖
index = 0;
} else {
index++;
}
records[index] = record;
}
public Iterator<String> iterator() {
return new Itr();
}
private class Itr implements Iterator<String> {
int cursor = index;// 迭代器游标,不染指原始游标。
int loopCount = 0;
@Override
public boolean hasNext() {
return loopCount < 10;
}
@Override
public String next() {
int i = cursor;// 记录即将返回的游标位置
if (cursor == 0) {
cursor = 9;
} else {
cursor--;
}
loopCount++;
return records[i];
}
}
;
}
这里我们加入内部类(第18行)来定义迭代器实现,为的是能轻松访问到记录仪私有数据集。内部类实现了两个标配方法hasNext与next,内部逻辑看起来简单多了,大家可以自行理解,这里就不做讲解了。最后重点来了,用户可以进行如下操作了。
package com.liuxd;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Liuxd on 2018/11/7.
*/
public class Client2 {
public static void main(String[] args) {
DrivingRecorder2 dr = new DrivingRecorder2();
// 假设记录了12条视频
for (int i = 0; i < 12; i++) {
dr.append("视频_" + i);
}
//用户要获取交通事故视频,定义事故列表。
List<String> accidents = new ArrayList<>();
//用户拿到迭代器
Iterator<String> it = dr.iterator();
while (it.hasNext()) {//如果还有下一条则继续迭代
String video = it.next();
System.out.println(video);
//用户翻看视频发现10和8可作为证据。
if ("视频_10".equals(video) || "视频_8".equals(video)) {
accidents.add(video);
}
}
//拿到两个视频集accidents交给交警查看。
System.out.println("事故证据:" + accidents);
/*
视频_11
视频_10
视频_9
视频_8
视频_7
视频_6
视频_5
视频_4
视频_3
视频_2
事故证据:[视频_10, 视频_8]
*/
}
}
输出结果:
视频_11
视频_10
视频_9
视频_8
视频_7
视频_6
视频_5
视频_4
视频_3
视频_2
事故证据:[视频_10, 视频_8]
用户拿到迭代器进行遍历查看,注意第18行,用户将这12条视频中的10和8拿出来拷贝U盘并交给交警作为呈堂证供判对方碰瓷,以证明自己的清白。
当然,我们这里只是保持极简说明问题,读者可以自行重构代码,尤其是实现迭代器的remove方法非常重要(注意游标的调整),这样用户便可以删除数据了。
总之,对于任何的集合类,既要保证内部数据表示不暴露给外部以防搞乱内部机制,还要提供给用户遍历并访问到每个数据的权限,迭代器模式则成就了鱼与熊掌兼得的可能,它提供了所有集合对外开放的统一标准接口,内政容不得干涉,但是经济依旧要开放。