Java 丢了好多年,最近在拣起来,首先固然是了解这么多年来它的变化,因而发现了 Java 8 的
java.util.stream
。在学习和试验的过程当中,相比较于 C# 和 javascript,有那么些心得,做文以记之。javascript早些时间写过一篇《ES6 的 for..of 和 Generator,从伪数组 jQuery 对象提及》,和这个主题有点关系。其实我记得还有一篇讲 C# 的,没找到,也许只是想过,没写成,成了虚假记忆。html
之因此把 C#、JavaScript 和 Java 三种语言的实现写在一块儿,主要是为了放在一块儿有一个类比,可能会有助于理解。java
C# 的集合数据基类是 Collection<T>,它实现了 ICollection<T>接口,而 ICollection<T>
又从 IEnumerable<T> 接口继承——实际上要讨论的内容都基于 IEnumerable<T>
接口。另外还有一个非泛型的 IEnumerable
接口,不过建议你们尽可能使用泛型,因此这个非泛型的接口就当我没说。顺便提一句,数组也是实现了 IEnumerable<T>
接口的。System.Linq
中提供的扩展大大方便了集合处理过程。算法
JavaScript 最多见的集合数据类型就是数组,自 ES6 发布之后,这个范围扩展到了 iterable 对象。不过这里要讨论的内容都是在 Array.prototype 中实现的。除此以外,underscore、lodash 这些第三方库中也实现了不少集合数据处理的方法,但不在本文讨论内容以内。segmentfault
Java 的集合类型由 Collection<E> 接口定义。本文讨论的内容是 Java 8 的特性,在 java.util.stream
包中实现,由 Collection<E>.stream()
引入。api
- 后面示例中的部分 C# 语句可能须要支持 6.0 语言版本的编译器,如 Visual Studio 2015 或者 Visual Studio "15"
- JavaScript 代码都使用了 ES6 语法,目前大部分浏览器支持,Node 5 也彻底支持。
- Java 要求 Java 8(或 1.8)版本
给定一个名称列表,数组类型, ["Andy", "Jackson", "Yoo"]
,要求遍历出到的控制台。数组
对于集合来讲,最经常使用的就是遍历,不过 for
,foreach
, while
之类你们都耳熟能详了,再也不多说。这里说的是 forEach()
方法。浏览器
很遗憾,C# 的 Linq 扩展 里没有提供 ForEach()
方法,不过 All(IEnumerable<T>, Func<T, Boolean>)
和 Any(IEnumerable<T>, Func<T, Boolean>)
均可以代替。这两个方法的区别就在于第二个参数 Func<T, Boolean>
的返回值。这两个方法都会遍历集合,对集合中的每一个元素依次调用第二个参数,Func<T, Boolean>
所指的委托方法,并检查其返回值,All()
检查到 false
停止遍历,而 Any()
检查到 true
停止遍历。数据结构
All()
的意思是,全部元素都符合条件则返回true
,全部只要有一个不符合条件,返回了false
,则停止遍历,返回false
;Any()
的意思是只要发现有元素符合条件则返回true
。oracle
Func<T, Boolean>
是一个公用委托。Func<...>
系列公用委托都用于委托带有返回值的的方法,全部Func<..., TResult>
都是最后一个参数TResult
表明返回值类型。
所以,C# 的遍历输出能够这样实现
string[] names = { "Andy", "Jackson", "Yoo" }; names.All(name => { Console.WriteLine(name); return true; });
string[] names = { "Andy", "Jackson", "Yoo" }; names.Any(name => { Console.WriteLine(name); return false; });
有 Lambda 就是好
JavaScript 的 Array 实现了 forEach
实例方法,即 Array.prototype.forEach()。
对于 JavaScript 的数组,能够这样遍历
var names = ["Andy", "Jackson", "Yoo"]; names.forEach(name => { console.log(name); });
对于 JavaScript 的伪数组,能够这样
var names = { 0: "Andy", 1: "Jackson", 2: "Yoo", length: 3 }; [].forEach.call(names, name => { console.log(name); });
jQuery 是一个经常使用的 JavaScript 库,它封装的对象都是基于伪数组的,因此 jQuery 中常常用到遍历。除了网页元素集合外,jQuery 也能够遍历普通数组,有两种方式
能够直接把数组做为第一个参数,处理函数做为第二个参数调用 $.each()
。
const names = ["Andy", "Jackson", "Yoo"]; $.each(names, (i, name) => { console.log(name); });
也能够把数组封装成一个 jQuery 对象($(names)
),再在这个 jQuery 对象上调用 eash()
方法。
const names = ["Andy", "Jackson", "Yoo"]; $(names).each((i, name) => { console.log(name); });
两种方法的处理函数都同样,可是要注意,这和原生 forEach()
的处理函数有点不一样。jQuery 的 each()
处理函数,第一个参数是序号,第二个参数是数组元素;而原生 forEach()
的处理函数正好相反,第一个参数是数组元素,第二个参数才是序号。
另外,$.each()
对伪数组一样适用,不须要经过 call()
来调用。
String[] names = { "Andy", "Jackson", "Yoo" }; List<String> list = Arrays.asList(names); list.forEach(name -> { System.out.println(name); });
给出一组整数,须要将其中能被 3 整除选出来
[46, 74, 20, 37, 98, 93, 98, 48, 33, 15]
指望结果
[93, 48, 33, 15]
Where()
扩展int[] data = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; int[] result = data.Where(n => n % 3 == 0).ToArray();
注意:Where()
的结果即不是数组也不是 List,须要经过 ToArray()
生成数组,或者经过 ToList()
生成列表。Linq 要在 ToArray()
或者 ToList()
或者其它某些操做的时候才会真正遍历,依次执行 Where()
参数提供的那个筛选函数。
const data = [46, 74, 20, 37, 98, 93, 98, 48, 33, 15]; const result = data.filter(n => { return n % 3 === 0; });
Java 中能够经过 java.util.stream.IntStream.of()
来从数组生成 stream 对象
final int[] data = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; int[] result = IntStream.of(data) .filter(n -> n % 3 == 0) .toArray();
须要注意的是,Arrays.asList(data).stream()
看起来也能够生成 stream 对象,可是经过调试会发现,这是一个 Stream<int[]>
而不是 Stream<Integer>
。缘由是 asList(T ...a)
其参数可变参数,并且要求参数类型是类,因此 asList(data)
是把 data
做为一个 int[]
类型参数而不是 int
类型的参数数据。若是要从 int[]
生成 List<Integer>
,还得经过 IntStream
来处理
List<Integer> list = IntStream.of(data) .boxed() .collect(Collectors.toList());
映射处理是指将某种类型的集合,将其元素依次映射成另外一种类型,产生一个新类型的集合。新集合中的每一个元素都与原集中的一样位置的元素有对应关系。
这里提出一个精典的问题:成绩转等级,不过为了简化代码(switch 或多重 if 语句代码比较长),改成判断成绩是否及格,60 分为及格线。
偷个懒,就用上个问题的输入 [46, 74, 20, 37, 98, 93, 98, 48, 33, 15]
,
指望结果:
["REJECT","PASS","REJECT","REJECT","PASS","PASS","PASS","REJECT","REJECT","REJECT"]
Select()
来进行映射处理。int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; string[] levels = scores .Select(score => score >= 60 ? "PASS" : "REJECT") .ToArray();
const scores = [46, 74, 20, 37, 98, 93, 98, 48, 33, 15]; const levels = scores.map(score => { return score >= 60 ? "PASS" : "REJECT"; });
mapToObj()
等方法处理映射final int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; String[] levels = IntStream.of(scores) .mapToObj(score -> score >= 60 ? "PASS" : "REJECT") .toArray(String[]::new);
与“筛选”示例不一样,在“筛选”示例中,因为筛选结果是 IntStream
,能够直接调用 InStream::toArray()
来获得 int[]
。
但在这个示例中,mapToObj()
获得的是一个 Stream<String>
,类型擦除后就是 Stream
,因此 Stream::toArray()
默认获得的是一个 Object[]
而不是 String[]
。若是想获得 String[]
,须要为 toArray()
指定 String[]
的构造函数,即 String[]::new
。
查找表在数据结构里的意义仍是比较宽的,其中经过哈希算法实现的称为哈希表。C# 中一般是用 Directory<T>
,不过它是否是经过哈希实现我就不清楚了。不过 Java 中的 HashMap
和 Hashtable
,从名称就看得出来是实现。JavaScript 的字面对象据称也是哈希实现。
如今有一个姓名列表,是按学号从 1~7 排列的,须要创建一个查找到,使之能经过姓名很容易找到对应的学号。
["Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen"]
指望结果
Andy => 1 Jackson => 2 Yoo => 3 Rose => 4 Lena => 5 James => 6 Stephen => 7
string[] names = { "Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen" }; int i = 1; Dictionary<string, int> map = names.ToDictionary(n => n, n => i++);
C# Linq 扩展提供的若干方法都没有将序号传递给处理函数,因此上例中采用了临时变量计数的方式来进行。不过有一个看起来好看一点的办法,用 Enumerable.Range() 先生成一个序号的序列,再基于这个序列来处理
string[] names = { "Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen" }; IEnumerable<int> indexes = Enumerable.Range(0, names.Length); Dictionary<string, int> map = indexes.ToDictionary(i => names[i], i => i + 1);
JavaScript 没有提供从 []
到 {}
的转换函数,不过要作这个转换也不是好麻烦,用 forEach
遍历便可
var map = (function() { var m = {}; names.forEach((name, i) => { m[name] = i + 1; }); return m; })();
为了避免让临时变量污染外面的做用域,上面的示例中采用了 IEFE 的写法。不过,若是用 Array.prototype.reduce 则可让代码更简洁一些
var map = names.reduce((m, name, i) => { m[name] = i + 1; return m; }, {});
Java 的处理函数也没有传入序号,因此在 Java 中的实例和 C# 相似。不过,第一种方法不可用,由于 Java Lambda 的实现至关因而匿名类对接口的实现,只能访问局部的 final
变量,i
要执行 i++
操做,显然不是 final
的,因此只能用第二种办法
final String[] names = { "Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen" }; Map<String, Integer> map = IntStream.range(0, names.length) .boxed() .collect(Collectors.toMap(i -> names[i], i -> i + 1));
我只能说
.boxed()
是个大坑啊,必定要记得调。
汇总处理就是合计啊,平均数啊之类的,使用方式都差很少,因此以合计(Sum)为例。
汇总处理实际上是聚合处理的一个特例,因此就同一个问题,再用普通的聚合处理方式再实现一次。
已知全班成绩,求班总分,再次用到了那个数组
[46, 74, 20, 37, 98, 93, 98, 48, 33, 15]
指望结果:562
C# 能够直接使用 Sum()
方法求和
int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; int sum = scores.Sum();
聚合实现方式(用 Aggregate()
)
int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; int sum = scores.Aggregate(0, (total, score) => { return total + score; });
聚合实现方式要灵活得多,好比,改为乘法就能够算阶乘。固然用于其它更复杂的状况也不在话下。前面生成查找表的 JavaScript 部分就是采用聚合来实现的。
const scores = [46, 74, 20, 37, 98, 93, 98, 48, 33, 15]; const sum = scores.reduce((total, score) => { return total + score; }, 0);
注意 C# 的初始值在前,JavaScript 的初始值在后,这是有区别的。参数顺序嘛,注意一下就好了。
IntStream
提供了 sum()
方法
final int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; final int sum = IntStream.of(scores).sum();
一样也能够用 reduce
处理
final int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 }; final int sum = IntStream.of(scores) .reduce(0, (total, score) -> total + score);
已知全班 7 我的,按学号 从 1~7 分别是
["Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen"]
这 7 我的的成绩按学号序,分别是
[66, 74, 43, 93, 98, 88, 83]
有 Student
数组结构
Student { number: int name: string score: int }
要求获得全班 7 人的 student 数组,且该数组按分数从高到低排序
sealed class Student { public int Number { get; } public string Name { get; } public int Score { get; } public Student(int number, string name, int score) { Number = number; Name = name; Score = score; } public override string ToString() => $"[{Number}] {Name} : {Score}"; }
Student[] students = Enumerable.Range(0, names.Length) .Select(i => new Student(i + 1, names[i], scores[i])) .OrderByDescending(s => s.Score) .ToArray();
注意 C# 中排序有 OrderBy
和 OrderByDescending
两个方法,通常状况下只须要给一个映射函数,从原数据里找到要用于比较的数据便可使用其 >
、<
等运算符进行比较。若是比例起来比较复杂的,须要提供第二个参数,一个 IComparer<T>
的实现
class Student { constructor(number, name, score) { this._number = number; this._name = name; this._score = score; } get number() { return this._number; } get name() { return this._name; } get score() { return this._score; } toString() { return `[${this.number}] ${this.name} : ${this.score}`; } }
const names = ["Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen"]; const scores = [66, 74, 43, 93, 98, 88, 83]; var students = names .map((name, i) => new Student(i + 1, name, scores[i])) .sort((a, b) => { return b.score - a.score; });
JavaScript 的排序则是直接给个比较函数,根据返回的数值小于0、等于0或大于0来判断是小于、等于仍是大于。
final class Student { private int number; private String name; private int score; public Student(int number, String name, int score) { this.number = number; this.name = name; this.score = score; } public int getNumber() { return number; } public String getName() { return name; } public int getScore() { return score; } @Override public String toString() { return String.format("[%d] %s : %d", getNumber(), getName(), getScore()); } }
final String[] names = { "Andy", "Jackson", "Yoo", "Rose", "Lena", "James", "Stephen" }; final int[] scores = { 66, 74, 43, 93, 98, 88, 83 }; Student[] students = IntStream.range(0, names.length) .mapToObj(i -> new Student(i + 1, names[i], scores[i])) .sorted((a, b) -> b.getScore() - a.getScore()) .toArray(Student[]::new);