引言
相信无论是谁,第一次接触Python,肯定会为她的优雅简洁感到震撼(她的性能咱先抛一边去),然而本人主要使用C和Java开发,Python虽然感觉很好,但是碍于一些环境因素,没机会去使用,况且语言只是一种工具,现在像Python这种高级动态语言又确实不少,掌握其本质思想才是关键,所以本文通过分析Java和Python的一些语法特性,用Java语言设计实现了Python语言中的强大而实用的生成器。本文只是使用Java的设计模式模仿Python的生成器,真正的生成器是由continuation实现的。
从range开始
玩过Python的都知道,在循环的时候都提倡使用如下风格的语法:
for item in IteratbleObj:
doSomething(item)
好在J2SE5后也引入了这种循环方式foreach,让Java增色不少,写起来更加方便舒服了:
for(elementType item : IterableObj)
doSomething(item);
但是,在初学Python的时候,肯定会碰到这个实用的内置函数:range()
在Python2里它的作用是通过指定范围和步长生成一个左闭右开的等差整数序列:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(1,10,2))
[1, 3, 5, 7, 9]
>>> list(range(1,10,-2))
[]
>>> list(range(1,-10,-2))
[1, -1, -3, -5, -7, -9]
>>> list(range(1,-10,2))
[]
>>> list(range(1,10,0))
Traceback (most recent call last):
File "<pyshell#19>", line 1, in <module>
list(range(1,10,0))
ValueError: range() arg 3 must not be zero
>>>
range()默认start为0,默认step为1,step为0则抛出异常。
如果指定范围是一个递增序列但是step为负数,或者指定范围是一个递减序列但step为正数,将返回长度为0的序列。
这个函数经常在Python中用来和for一起使用迭代下标:
for i in range(10):
print(i)
可以在Java中轻松实现range:
/**
* 仿Python range,生成一个整数等差序列(数组)
*
* @param from
* @param to
* @param step
* @return
*/
public static int[] range(int from, int to, int step) {
if(step == 0) //步长为0非法
throw new IllegalArgumentException(String.valueOf(step));
int[] sequence = null;
if(!(from<to ^ step>0)) { //递增序列则步长必须为正数,反之亦然
sequence = new int[(int) Math.ceil((to - from) * 1.0 / step)]; //预算长度
for (int i = 0, len = sequence.length; i < len; from += step)
sequence[i++] = from;
} else
sequence = new int[0];
return sequence;
}
/**
* 生成一个整数等差序列(数组), 默认步长为1
*
* @param from
* @param to
* @return
*/
public static int[] range(int from, int to) {
return range(from, to, 1);
}
/**
* 生成一个整数等差序列(数组),默认从0开始,步长为1
*
* @param to
* @return
*/
public static int[] range(int to) {
return range(0, to, 1);
}
这样子可以在Java中写出和Python一样的for语句:
for(int i : range(10)) //静态导入range方法
System.out.print(i+", ");
System.out.println();
System.out.println(Arrays.toString(range(1,10,2)));
System.out.println(Arrays.toString(range(1,10,-2)));
System.out.println(Arrays.toString(range(1,-10,-2)));
System.out.println(Arrays.toString(range(1,-10,2)));
System.out.println(Arrays.toString(range(1,10,0)));
后台输出:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
[1, 3, 5, 7, 9]
[]
[1, -1, -3, -5, -7, -9]
[]
Exception in thread “main” java.lang.IllegalArgumentException: 0
这和传统C风格循环相比更加的简洁方便。
但是很明显,这样子是在堆中生成了一个数组对象,迭代完之后马上就丢弃了,所以效率肯定是不如原始的for(int i=0; i<10; i++)。
另外,这个range函数只能生成等差序列,功能十分有限,而且它还是一口气在内存中分配的空间,要是只迭代了序列前面几个数就中途退出了,则会浪费大量的存储空间和初始内存分配时间。
观察range函数生成序列的过程,每个元素的值都是在前一个元素的基础上得到的,那么如果能将最近生成的元素保存起来,用来推算后续元素,那么就可以做到即时生产,不会出现半点浪费,节省空间。
用Java设计生成器Generator
Python内部就实现了这种机制,叫做生成器generator,它不仅可以生成某种规律的序列,还可以生成各种复杂的对象。
#菲波那契数列生成器
def fib(n):
count, a1, a2 = 0, 0, 1 #对应赋值
while count < n:
yield a2 #中断并返回结果,下次调用会从此处开始执行
a1, a2 = a2, a1 + a2
count += 1
for i in fib(10):
print(i)
后台输出:
1
1
2
3
5
8
13
21
34
55
现在我们想让Java也有这个功能。
分析:
(1)既然生成器要能够在foreach语句中迭代,而foreach规定迭代的对象必须是Iterable对象,那么生成器必须实现Java内置的Iterable接口。
(2)实现了Iterable接口则必须重写iterator()方法产生一个迭代器,并且重写它的hasNext(), next()和remove方法,而这些方法正好也适用于生成器,所以可以利用迭代器来实现生成器。
(3)不要让调用者看见任何迭代器iterator的字眼,因为用迭代器实现生成器的功能在调用者看来实在是匪夷所思,应该把产生迭代器的实现隐藏起来。
综合上述的分析,可以大致确定生成器的设计:
(1)设计一个Generable抽象类去实现Iterable接口,定义一个模板方法public abstract void generator();
负责创建生成器对象,用public修饰而不是protected,原因是希望提供额外的方法能够自由获取其生成器对象通过next()手动调用,同时把iterator方法给重写了,里面调用generator方法,并且将其修饰为final,防止子类重写。
import java.util.Iterator;
public abstract class Generable<E> implements Iterable<E>{
public abstract Generator<E> generator(); //模板方法
@Override
final public Iterator<E> iterator() { //此方法不能被子类重写
return generator();
}
}
(2)既然有generator()方法,那么自然需要设计一个抽象类Generator,去实现Iterator接口,重写hasNext(), next()方法,而remove()方法在生成器中没有用,抛弃。
import java.util.Iterator;
public abstract class Generator<E> implements Iterator<E>{
@Override
final public void remove() { //此操作不支持,子类也不能重写
new UnsupportedOperationException();
}
}
那么真正的生成器到底应该怎么用?
考虑到Python的生成器长得就像个函数,那么Java中就用个方法来表示。
//Java菲波那契生成器
//Generable对象可以在foreach语句中迭代
//使用迭代器惯用法,两个匿名内部类
public static Generable<Integer> fib(final int n){
return new Generable<Integer>() {
int count = n, a1=0, a2=1;
@Override
public Generator<Integer> generator() {
return new Generator<Integer>() {
@Override
public Integer next() {
if (count <= 0)
throw new NoSuchElementException();
count--;
int res = a2;
a2 += a1;
a1 = res;
return res;
}
@Override
public boolean hasNext() {
return count > 0;
}
};
}
};
}
for(int i : fib(10)) //静态导入
System.out.print(i+", ");
后台输出:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
程序分析:
foreach首先会调用fib()方法(整个循环下来也就一次)获取到Generable对象(也就是Iterable对象),然后调用其iterator()方法获取迭代器,但是iterator()方法里返回的是一个生成器对象(也就是Iterator对象),之后foreach不断地调用生成器的hasNext()判断是否还有需要迭代的元素,同时再调用next()方法取得新元素。同时当前生成的元素值被保存在外部类供下次使用,这样就成功地用迭代器实现了和Python具有同样功能的生成器,其实现细节被隐藏了起来。
生成器使用案例
xrange
前面说到Python2的range()函数是一口气生成一个序列,在数量大的时候非常浪费空间,因此Python2内置了一个和range()功能相同的函数`xrange()(Python3中range是用xrange实现的),但是它是一个生成器,其序列元素是即时产生的。
/**
* 仿Python xrange,生成等差序列的生成器函数
* @param from
* @param to
* @param step
* @return
*/
public static Generable<Integer> xrange(final int from, final int to, final int step){
if(step == 0)
throw new IllegalArgumentException(String.valueOf(step));
return new Generable<Integer>() {
int n = (int) Math.ceil((to - from) * 1.0 / step),
start = from;
int i = !(from<to ^ step>0) ? 0 : n; //如果步长和序列单调性一致则进行正常生产,否则生产结束
@Override
public Generator<Integer> generator() {
return new Generator<Integer>() {
@Override
public Integer next() {
if (i >= n)
throw new NoSuchElementException();
i++;
int t = start;
start += step;
return t;
}
@Override
public boolean hasNext() {
return i < n;
}
};
}
};
}
/**
* 默认从0开始,默认步长1
* @param to
* @return
*/
public static Generable<Integer> xrange(final int to){
return xrange(0, to, 1);
}
/**
* 默认步长1
* @param from
* @param to
* @return
*/
public static Generable<Integer> xrange(final int from, final int to){
return xrange(from, to ,1);
}
for(int i : xrange(10))
System.out.print(i+", ");
System.out.println();
for(int i : xrange(1, 10, 2))
System.out.print(i+", ");
System.out.println();
for(int i : xrange(1, 10, -2))
System.out.print(i+", ");
System.out.println();
for(int i : xrange(1, -10, -2))
System.out.print(i+", ");
System.out.println();
for(int i : xrange(1, -10, 2))
System.out.print(i+", ");
System.out.println();
for(int i : xrange(1, 10, 0))
System.out.print(i+", ");
System.out.println();
后台输出:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
1, 3, 5, 7, 9,
//这一行没结果
1, -1, -3, -5, -7, -9,
//这一行没结果
Exception in thread “main” java.lang.IllegalArgumentException: 0
- 杨辉三角生成器
一头一尾都有一个1,中间的数字是由前一行推算出来
Python生成器:
def triangle(n):
result=[1]
while n>0:
yield result
result = [1] + [ result[x-1] + result[x] for x in range(1,len(result)) ] + [1]
n -= 1
for i in triangle(10):
print(i)
后台输出:
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
Java生成器:
public static Generable<List<Integer>> triangle(final int n){
return new Generable<List<Integer>>() {
int count = n;
List<Integer> result = new ArrayList<>();
@Override
public Generator<List<Integer>> generator() {
result.add(1);
return new Generator<List<Integer>>() {
@Override
public List<Integer> next() {
if(count <= 0)
throw new NoSuchElementException();
count--;
List<Integer> t = result;
result = new ArrayList<>();
result.add(1);
for(int i=1; i<t.size(); i++)
result.add(t.get(i)+t.get(i-1));
result.add(1);
return t;
}
@Override
public boolean hasNext() {
return count > 0;
}
};
}
};
}
for(List<Integer> l : triangle(10))
System.out.println(l);
后台输出:
[1]
[1, 1]
[1, 2, 1]
[1, 3, 3, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]
其它生成奇数偶数素数等各种数列都是没有问题的。
- 当做工厂使用
import net.zealot.util.Generable;
import net.zealot.util.Generator;
public class Product {
private int pid;
public Product(int pid){
this.pid = pid;
}
public String toString(){
return "产品"+pid+"";
}
public static Generable<Product> proGen(){
return new Generable<Product>(){
int count = 0;
@Override
public Generator<Product> generator() {
return new Generator<Product>(){
@Override
public boolean hasNext() {
return true;
}
@Override
public Product next() {
return new Product(count++);
}
};
}
};
}
public static void main(String[] args) {
Generator<Product> factory = Product.proGen().generator();
System.out.println(factory.next());
System.out.println(factory.next());
System.out.println(factory.next());
System.out.println(factory.next());
System.out.println(factory.next());
}
}
后台输出:
产品0
产品1
产品2
产品3
产品4
生成器配合Guava的Iterables工具类使用
在Python中可以对生成器使用map,reduce,filter函数,都需要传入一个函数作为参数,作用到生成器产生的每一个元素上。
Java8之前不支持函数式编程,但是Guava中近似实现了一些函数式接口Predicate和Function,而Iterables工具类更是能够和生成器配合得天衣无缝:
//Iterables工具类中所有方法都接收Iterable对象,面向接口的威力不言而喻
//这里列出常用的两个
//过滤生成器生成的元素
Iterables.filter(Iterable<?> unfiltered, Predicate<? super T> predicate);
//转换生成器生成的元素
Iterables.transform(Iterable<F> fromIterable, Function<? super F, ? extends T> function)
//逻辑可以非常复杂,这里随意举例,让菲波那契生成器只生成偶数
Predicate<Integer> func = new Predicate<Integer>(){
@Override
public boolean apply(Integer arg0) {
return arg0 % 2 == 0;
}
};
for(int i : Iterables.filter(fib(20), func))
System.out.print(i+" ");
- 用生成器填充容器
Collections工具类里有个没有什么用途的fill方法,作用是用同一个对象去填充一个容器,如果改成用生成器去填充,应该会比较有用:
/**
* 用Generator对象产生元素填充容器并返回,需要指定生成元素的数量
* @param coll
* @param gen
* @param n
* @return
*/
public static <T> Collection<T> fill(Collection<T> coll, Generator<T> gen, int n){
for(int i=0; i<n; i++)
coll.add(gen.next());
return coll;
}
/**
* 用Generable对象产生元素填充容器并返回,不需要指定生成元素的数量,Generable会自动处理边界
* @param coll
* @param gen
* @return
*/
public static <T> Collection<T> fill(Collection<T> coll, Generable<T> gen){
for(T t : gen)
coll.add(t);
return coll;
}
List<Integer> l = new ArrayList<>();
fill(l, fib(10));
System.out.println(l);
后台输出:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
当然Guava的Iterables工具类里有这个方法,接收Iterable对象作为参数
List<Integer> list = Lists.newArrayList();
Iterables.addAll(list, fib(10));
System.out.println(list);
输出结果和上面一样
总结
由于Java本身语法的原因,写出来的代码总是要比Python多很多,没有那么优雅,不过好歹还是实现了可以迭代的生成器,由于还没有接触过Java8,用Java8应该可以做得更优雅;另外,由于设计水平实在太低,不知道能不能进一步将生成器变化的部分分离出去,如果有高手有什么奇妙的想法,恳请告知一二。现在还是先去玩玩再说吧。
最后,由于平时只用Java写业务逻辑,整天在框架上对着数据库苦撸的(CRUD)人都麻木了,虽然CRUD也能搞出花样来,但是本人还是更喜欢自由创造的感觉,虽然现在提倡不要造重复的轮子(恕我孤陋寡闻,我在网上没有找到用Java模仿Python的例子),但我觉得这多半是人们为自己日益浮躁的内心所找的借口,最起码要了解轮子的制造过程对吧。通过这个生成器的设计,再一次感受到了面向抽象的威力,这使得我们可以很容易的在已有的结构上进行灵活扩展,提高代码的复用率。