Java 丟了好多年,最近在揀起來,首先當然是瞭解這麼多年來它的變化,於是發現了 Java 8 的
java.util.stream
。在學習和試驗的過程中,相比較於 C# 和 javascript,有那麼些心得,作文以記之。早些時間寫過一篇《ES6 的 for..of 和 Generator,從偽陣列 jQuery 物件說起》,和這個主題有點關係。其實我記得還有一篇講 C# 的,沒找到,也許只是想過,沒寫成,成了虛假記憶。
前言
之所以把 C#、JavaScript 和 Java 三種語言的實現寫在一起,主要是為了放在一起有一個類比,可能會有助於理解。
集合資料
C# 的集合資料基類是 Collection<T>,它實現了 ICollection<T>介面,而 ICollection<T>
又從 IEnumerable<T> 介面繼承——實際上要討論的內容都基於 IEnumerable<T>
介面。另外還有一個非泛型的 IEnumerable
介面,不過建議大家儘量使用泛型,所以這個非泛型的介面就當我沒說。順便提一句,陣列也是實現了 IEnumerable<T>
介面的。System.Linq
中提供的擴充套件大大方便了集合處理過程。
JavaScript 最常見的集合資料型別就是陣列,自 ES6 釋出以後,這個範圍擴充套件到了 iterable 物件。不過這裡要討論的內容都是在 Array.prototype 中實現的。除此之外,underscore、lodash 這些第三方庫中也實現了很多集合資料處理的方法,但不在本文討論內容之內。
Java 的集合型別由 Collection<E> 介面定義。本文討論的內容是 Java 8 的特性,在 java.util.stream
包中實現,由 Collection<E>.stream()
引入。
示例語言版本
- 後面示例中的部分 C# 語句可能需要支援 6.0 語言版本的編譯器,如 Visual Studio 2015 或者 Visual Studio “15”
- JavaScript 程式碼都使用了 ES6 語法,目前大部分瀏覽器支援,Node 5 也完全支援。
- Java 要求 Java 8(或 1.8)版本
遍歷
問題提出
給定一個名稱列表,陣列型別, ["Andy", "Jackson", "Yoo"]
,要求遍歷出到的控制檯。
C# 的遍歷
對於集合來說,最常用的就是遍歷,不過 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
。
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 的遍歷
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 的遍歷
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()
來呼叫。
Java 的遍歷
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]
C# 中過濾使用 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()
引數提供的那個篩選函式。
JavaScript 中有 Array.prototype.filter
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.*
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"]
C# 通過 Select()
來進行對映處理。
int[] scores = { 46, 74, 20, 37, 98, 93, 98, 48, 33, 15 };
string[] levels = scores
.Select(score => score >= 60 ? "PASS" : "REJECT")
.ToArray();
JavaScript 通過 Array.prototype.map 來進行對映處理。
const scores = [46, 74, 20, 37, 98, 93, 98, 48, 33, 15];
const levels = scores.map(score => {
return score >= 60 ? "PASS" : "REJECT";
});
Java 的 Stream 提供了 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
C# 使用 ToDictionary()
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 的兩種處理辦法
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 的 Collectors
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# 的實現
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 部分就是採用聚合來實現的。
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 的初始值在後,這是有區別的。引數順序嘛,注意一下就行了。
Java 中使用 Stream::reduce 進行聚合處理
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 陣列,且該陣列按分數從高到低排序
C# 實現
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>
的實現
JavaScript 實現
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來判斷是小於、等於還是大於。
Java 實現
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);