从Java和MapRedcue看“迭代”:Iertator和Iterable

写作目标:理解Java中迭代用法(迭代一词的用法很广泛,这里仅讨论Java编程中的用法)

前置知识:了解集合,用过迭代,理解Java中的引用

迭代是什么?

迭代(iteration)是重复 反馈 过程的活动,其目的通常是为了接近并且到达所需的目标或结果。每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。

数学中的迭代可以指 函数迭代 的过程,即反复地运用同一函数计算,前一次迭代得到的结果被用于作为下一次迭代的输入。

在计算机科学中,迭代是程序中对一组指令(或一定步骤)的重复。它既可以被用作通用的术语(与“重复”同义),也可以用来描述一种特定形式的具有可变状态的重复。

引自:维基百科

所有的定义问题是很难回答的,通俗的解释不严谨(也就是不‘准’确),经过时间验证的定理太抽象。所以对一件事物的理解,需要在学习过程中不断地加深领会(也要求不停地学习了)。这里给出现阶段的理解。

这里,把迭代理解为一个动词,那么它就像我们现实生活中数数(shǔ shù)一样,比如数书架上整齐排列的书,或者数盘子上的豆子。在程序中,我们数的是集合中的元素,数书架上的书就是List(有序),数豆子就是Set(无序)。

现实生活中,数数的场景大多是为了‘获取总数’,那程序中的迭代目的是什么?

以数组举例,它可以存储许多元素,可以通过for去获取每个元素,然后对它进行操作。对于集合来说需要通过迭代来‘获取每个元素’。

for (int i = 0; i < 5; i++) println(arr[i]);遍历数组,大致过程是,先获取i(这个过程可以查看参考资料),然后获取数组arr的地址,数组是一段连续的内存,可以用依次递增的i逐个访问它的所有元素。

为什么要有它?

这个问题可以延伸到集合,延伸到数据结构,甚至延伸到一切复杂事物上。

我们拿集合举例,数组和集合逻辑上很类似,看集合的源码,底层也是用数组来存储。既然已经有数组为什么要有集合?集合封装了数据结构和对这些数据的操作,让我们很方便的使用这些工具。

其实也可以没有集合,我们自己实现就可以了。但就像计算器实现了加法,但没有实现减法一样,我们也可以自己通过加法实现乘法,但很不方便。另外,在对集合的存储和操作上有特别多的细节,需要很多理论基础才能实现。

再进一步说,实际上很多复杂的程序(或者说一切复杂事物)都是从简单程序(事物)发展而来的,这些复杂程序由前人创造,为我们提供了想象不到(灵感)的便利。

复杂程序已经被写出来了,再从实现去看它的原理,是本末倒置浪费时间吗?不是,一是在不同情况下,固有的程序必然需要灵活的调整,而了解了原理我们才能去做这些调整;二是了解了这些原理,我们才更有可能像前人一样创造‘新’的程序;三是前人创造这些的时间远比我们学习的时间多,其中的一些灵感,甚至花费大量时间也得不到。

Iterable和Iterator

从源码看Collection的父接口是Iterable,这个单词可以翻译为‘可迭代的’,可以理解为集合的一种属性,就是说集合是可迭代的。另外一个单词是Iterator,我们在遍历集合时会用到它Iterator it = myColle.iterator();,它可以翻译为‘迭代器’,这个很体现了‘有事就找对象’,我们创建了一个‘迭代器’对象,我们想获取集合的值就让它帮我们‘迭代’集合。

可以总结成一句看似有点绕的话:迭代器(主语)迭代(动词)可迭代对象(宾语)。注意区分这三种表达的不同。

源码实现

ArrayList的迭代

我们选取一个较简单的有序集合ArrayList来看‘迭代’在代码中实现的,为了保证简洁,这里不加泛型。

ArrayList alph = new ArrayList();
alph.add("a");
alph.add("b");
alph.add("c");

Iterator alphIter = alph.iterator();

HashSet hashSet = new HashSet();
hashSet.add("1");

while (alphIter.hasNext()){
    System.out.println(alphIter.next());
}

这里是很常见的用法,1定义一个集合alph,2添加元素,3获取alph集合的迭代器alphIter,4利用迭代器迭代集合中的对象元素,5输出当前元素

这里我们想了解Iterator,看一下源码iterator()方法为我们做了什么

// ArrayList.java
public Iterator<E> iterator() {
    return new Itr();
}

这是ArrayList类中的一个方法,从源码中看到它返回了一个匿名对象,这个对象的类是ArrayList类中的一个内部类Itr类,这个内部类实现了Iterator接口。

// ArrayList.java
// $Itr // $代表内部类
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    public E next() {
        //...
        Object[] elementData = ArrayList.this.elementData;
        //...
    }

    public void remove() {
    }
}

这个Itr是如何与alph集合联系起来的?

  • 在ArrayList中size用来记录实例对象的元素数量,Object[] elementData用来存储实例对象的元素。
  • alph对象调用iterator()方法,这个方法返回ArrayList中一个内部类的实例对象,
  • 这个内部类通过通过size访问外部类ArrayList中的元素数量,Object[] elementData = ArrayList.this.elementData;访问外部类ArrayList中的所有元素。

总结一下。我们获取alph集合的迭代器alphIter,这个迭代器中维护了一个变量cursor默认值为0,代表目前的访问位置,利用next()方法访问alph集合的元素数组,根据cursor返回对应的值。在hasNext()通过对比cursor和数组长度判断是否还有元素。

HashSet的迭代

接下来我们看无序集合HashSet迭代的实现

HashSet colorfulBeans = new HashSet();
colorfulBeans.add("RedBean");
colorfulBeans.add("BlackBean");
colorfulBeans.add("YelloBean");

Iterator cbIter = colorfulBeans.iterator();

// ...

我们知道,HashSet的源码实现就是包装了一个HashMap,这里调用iterator()也是通过map。

// HashSet.java
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

// $KeySet
public final Iterator<K> iterator()     { return new KeyIterator(); }

// $KeyIterator
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

// $HashIterator
abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        // ...
        Node<K,V>[] t = table;
        // ...
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        // t = table
        // next = t[index++]
        // ...
        
        return e;
    }
}

我们获取colorfulBeans集合的迭代器cbIter,这个迭代器的实现类是KeyIterator,这个类只实现了next()方法,其它如hasNext()方法需要从它的父类HashIterator调用(实际上next()也是调用了父类的nextNode()方法)。

HashIterator中维护了一个变量current和next,代表目前正在访问的元素和接下来的元素,在构造函数中next和current都会指向table中第一个不为null的元素。利用nextNode()方法访问colorfulBeans的元素数组。在hasNext()通过看next是否为null判断是否还有元素。

实现Iterable?参考MapReduce

查一些资料,看很少有介绍手动实现迭代器和可迭代对象的,更没有实际场景。在学习Hadoop MapReduce时遇到了关于迭代的问题,可以弥补一下这里的不足。

问题描述:遍历values集合,将其中元素存放到addrBeans集合中,存放的全是相同的value,且为values中的最后一个元素。

ArrayList<AddrBean> addrBeans = new ArrayList<>();
for (addrBean v : values) {
    addrBeans.add(v);
}

我们找到迭代器的源码

// ReduceContextImpl
// $ValueIterable

protected class ValueIterable implements Iterable<VALUEIN> {
    private ReduceContextImpl<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.ValueIterator iterator = ReduceContextImpl.this.new ValueIterator();

    protected ValueIterable() {
    }

    public Iterator<VALUEIN> iterator() {
        return this.iterator;
    }
}

// $ValueIterator
 protected class ValueIterator implements ReduceContext.ValueIterator<VALUEIN> {
        private boolean inReset = false;
        private boolean clearMarkFlag = false;

        protected ValueIterator() {
        }

        public boolean hasNext() {
        }

        public VALUEIN next() {
            nextKeyValue();
            return value;
        }

        public void remove() {
            throw new UnsupportedOperationException("remove not implemented");
        }

        public void mark() throws IOException {}

        public void reset() throws IOException {}

        public void clearMark() throws IOException {}
    }

// ReduceContextImpl
// Advance to the next key/value pair.
public boolean nextKeyValue() throws IOException, InterruptedException {
    // ...
	DataInputBuffer nextVal = input.getValue(); // Gets the current raw value.
    buffer.reset(nextVal.getData(), nextVal.getPosition(), nextVal.getLength()
        - nextVal.getPosition());
    value = valueDeserializer.deserialize(value);
	// ...
}

// WritableSerialization.java
public Writable deserialize(Writable w) throws IOException {
    Writable writable;
    // ...
    writable = w; // w就是传入的value,writable指向value
    // ...
    writable.readFields(dataIn); // 相当于value.readFields(),我们自定义序列化时会重写readFields()方法
    return writable; // 最终返回的还是value
}

我们获取values的迭代器,这个迭代器的实现类是ValueIterator,它没有维护一个当前访问的变量,在调用next()方法时,直接访问并返回外部类ReduceContextImpl.value变量。在nextKeyValue()方法中改变value的信息(反序列化)。

但是注意到,我们在for (addrBean v : values)中遍历时每次迭代时取到的v是同一个地址,一直都是ReduceContextImpl.value这个对象,只不过每次迭代对象信息是有变化的。

也就是说:addrBeans集合中的每一个元素都是同一个地址,它们指向的是ReduceContextImpl.value这个对象,且这个对象的信息是我们遍历的values中的最后一个元素。

解决:在遍历时,创建一个新的临时对象并将遍历到的对象信息存到临时变量中,每次循环都是如此。这样addrBeans中存入的是指向不同对象的地址。

作用:迭代过程中不创建对象,提高效率节省内存。

总结

  • Collection的父接口是Iterable,意味着集合类型的对象都是可以迭代的;
  • 用iterator方法获取一个集合的迭代器,让它帮助我们迭代集合;
  • 迭代器(主语)迭代(动词)可迭代对象(宾语)。
  • 讨论了看原理/源码的意义
  • MapReduce中迭代的实现和问题

参考

Java for、foreach 循环底层实现原理,以及如何判断集合支持 foreach 循环_java for 内部逻辑-CSDN博客

Mapreduce程序中reduce的Iterable参数迭代出是同一个对象-腾讯云开发者社区-腾讯云

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值