想想你平時撰寫的一些應用程式,大部份是在處理一組資料,Java 對管理資料用的容器(Container)型態,在語法上提供直接支援的只有陣列,其餘則是以程式庫的形式存在,除了標準程式庫之外,還有各式開放原始碼程式庫可供選擇。
在這邊將會介紹 Java 中的陣列、
List
、Set
,在鍵(Key)值(Value)結構的型態上,則介紹 Map
。除了標準 API 的容器類別實作之外,還介紹了 guava-libraries 的 Collections 實作。
陣列型態
Java 目前唯一在語法上提供支援的是陣列,如果你需要容量固定且有序的容器,就可以使用陣列。例如Integer[] numbers = {1, 2, 3};
,可以建立容量為 3 的陣列而索引為 0、1、2 的陣列。 在 Java 中陣列是物件,雖然如此,基本上與陣列有關的屬性只有
length
,可用來取得陣列的容量。如果你想對陣列做任何其他操作,可以使用 java.util.Arrays
提供的 static
方法。像是使用 fill
於陣列中填充元素、使用 copyOf
來複製陣列等簡單操作,若是需要更多對容器的操作,建議使用其他容器型態。
List 型態
如果需要有序具索引特性的容器,且需要比陣列擁有更多功能,可以使用List
,這個型態定義在 java.util
套件中。如果已知有若干元素,可以使用 java.util.Arrays
提供的 static
方法 asList
來建立 List
實作物件。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3);
如果事先不知道要收集哪些元素,可以直接建立
List
的實作物件,你完全可以視需求採用適當資料結構實作的物件,像是內部實作採陣列的 ArrayList
、採鏈結的 LinkedList
等標準 API 中定義的物件,甚至是來自開放原始碼的 List
實作物件,像是後續會介紹的 guava-libraries。例如,如果你需要在收集之後,擁有較好的索引存取效率,可以採用 ArrayList
。
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<Integer> collector = new ArrayList<>();
collector.add(10);
collector.addAll(numbers);
collector.size(); // 4
在上面的範例中,由於宣告
collector
時已經指定了 List<Integer>
,表示要收集的元素都是 Integer
,因此建構實作物件 ArrayList
時,就不用於 <>
中再指定 Integer
型態,編譯器會自行由 collector
的型態中推斷,ArrayList
中裝載的型態是 Integer
,實際上這是 JDK7 之後的新功能,在之前版本,你必須明確地定義為 List<Integer> collector = new ArrayList<Integer>()
。
Set 型態
Set
型態是無序群集(Collection
),管理的元素不會重複,照例地,你可以擁需求選用適當的資料結構實作物件,像是基於雜湊(Hash)的 HashSet
,或者是基於紅黑樹實作的 TreeSet
。以下是使用 Set
來模彷 Python Tutorial 第二堂(2)中 set
的示範:
import java.util.*;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) {
Set<String> admins = new HashSet<>(Arrays.asList("Justin", "caterpillar"));
Set<String> users = new HashSet<>(Arrays.asList("momor", "hamini", "Justin"));
out.println(admins.contains("Justin")); // 是否在站長群?true
Set<String> intersection = new HashSet<>(admins);
intersection.retainAll(users); // 同時是站長群也是使用者群的?
out.println(intersection); // [Justin]
Set<String> union = new HashSet<>(admins);
union.addAll(users); // 是站長群或是使用者群的?
out.println(union); // [momor, hamini, Justin, caterpillar]
Set<String> adminsButNotUsers = new HashSet<>(admins);
adminsButNotUsers.removeAll(users); // 站長群但不使用者群的?
out.println(adminsButNotUsers); // [caterpillar]
Set<String> xor = new HashSet<>(union);
xor.removeAll(intersection); // xor
out.println(xor); // [momor, hamini, caterpillar]
out.println(admins.containsAll(users)); // ∈,false
out.println(users.containsAll(admins)); // false
}
}
除了
List
、Set
本身定義的操作之外,有些操作可以在 java.util.Collections
的 static
方法找到。
Map 型態
鍵值對應的物件,在 Java 中可使用Map
型態,你可以依需求選用適當的資料結構實作,像是 HashMap
、TreeMap
等,以下是模彷 Python Tutorial 第二堂(2)中 dict
的示範:
import java.util.*;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) {
Map<String, Integer> passwords = new HashMap<>();
passwords.put("Justin", 123456);
passwords.put("caterpillar", 93933);
out.println(passwords.get("Justin")); // 123456
passwords.put("Hamimi", 970221); // 增加一對鍵值
out.println(passwords); // {Justin=123456, caterpillar=93933, Hamimi=970221}
passwords.remove("caterpillar"); // 刪除一對鍵值
out.println(passwords); // {Justin=123456, Hamimi=970221}
out.println(passwords.entrySet()); // [Justin=123456, Hamimi=970221]
out.println(passwords.keySet()); // [Justin, Hamimi]
out.println(passwords.values()); // [123456, 970221]
}
}
Guava Collections
Java 標準 API 的容器物件,著重在功能性的定義與實作,少於考量易用性,加上 Java 本身又是靜態定型(Statically-typed)語言,不少地方都需要型態宣告,因而使用上就現今眼光來說,頗為不便。Java 的世界中有不少的開放原始碼程式庫,可以補強 Java 在這方面的不足,在這邊使用一下 guava-libraries 的 Collections,讓需求在操作上比較方便一些。練習 5:使用 Guava Collections
在這個練習中,將使用 Guava Collections 來改寫先前的一些範例,不過在急著到 Guava 官方網站下載相關 jar 檔案之前,別忘了我們有安裝 Gradle,它可以自動幫你下載 jar 檔案,為此,先在使用者目錄下建立一個 Collections 目錄與 Gradle 慣例的相關子目錄:
~$ mkdir Collections
~$ cd Collections
~/Collections$ mkdir -p src/main/java
~/Collections$ st2 build.gradle
最後一個指令建立了 build.gradle 檔案,你可以如下撰寫,使用
'java'
、'application'
的 plugin,設定 mainClassName
,下載 jar 檔案的來源,以及你想要下載的程式庫名稱、版本等。
apply plugin: 'java'
apply plugin:'application'
mainClassName = "Main"
repositories {
mavenCentral()
}
dependencies {
compile group: 'com.google.guava', name: 'guava', version: '15.0'
}
接著編輯 Main.java
~/Collections$ st2 src/main/java/Main.java
內容如下:
import java.util.*;
import com.google.common.collect.*;
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// List Demo
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<Integer> collector =
ImmutableList.<Integer>builder()
.add(10)
.addAll(numbers)
.build();
out.println(collector.size()); // 4
// Set Demo
Set<String> admins = ImmutableSet.of("Justin", "caterpillar");
Set<String> users = ImmutableSet.of("momor", "hamini", "Justin");
// 是否在站長群?true
out.println(admins.contains("Justin"));
// 同時是站長群也是使用者群的? [Justin]
out.println(Sets.intersection(admins, users));
// 是站長群或是使用者群的? [Justin, caterpillar, momor, hamini]
out.println(Sets.union(admins, users));
// 站長群但不使用者群的? [caterpillar]
out.println(Sets.difference(admins, users));
// xor [caterpillar, momor, hamini]
out.println(Sets.symmetricDifference(admins, users));
// ∈,false
out.println(admins.containsAll(users));
// false
out.println(users.containsAll(admins));
// Map Demo
Map<String, Integer> passwords =
ImmutableMap.of("Justin", 123456, "caterpillar", 93933);
out.println(passwords.get("Justin")); // 123456
out.println(passwords.entrySet()); // [Justin=123456, Hamimi=970221]
out.println(passwords.keySet()); // [Justin, Hamimi]
out.println(passwords.values()); // [123456, 970221]
}
}
比較一下與先前範例的差別,看看有沒有比較方便與清楚一些,然後執行
gradle run
:
~/Collections$ gradle run
第一次執行時,Gradle 會自動下載 jar 檔案,然後進行編譯與執行,你應該看到以下結果:
4
true
[Justin]
[Justin, caterpillar, momor, hamini]
[caterpillar]
[caterpillar, momor, hamini]
false
false
123456
[Justin=123456, caterpillar=93933]
[Justin, caterpillar]
[123456, 93933]
如果想要瞭解更多有關 Guava Collections 或更多 guava-libraries 的介紹,可以參考本站的 Guava 教學。
if、switch、while 與 for
流程語法中最簡單的if..else
分支判斷,在 Java 中是這樣寫的:
if(args.length > 1) {
out.println("Hello, " + args[0]);
} else {
out.println("Hello, Guest");
}
Java 中的
if..else
是陳述(Statement),有個類似運算式(Expression)形式是三元運算子 ?:
。?
前的條件式成立的話,會傳回 :
左邊的值,否則傳回 :
右邊的值。例如上面的程式也可以寫為:
out.printf("Hello, %s%n", args.length > 1 ? args[0] : "Guest")
如果想要比對的是某些值,在 Java 中可以使用
switch
,它可以用在整數、字串、字元等的比對。例如在 Java Tutorial 第二堂(1)中就看過字串的比對:
...
switch(args[1]) {
case "+": out.println(x.add(y)); break;
case "-": out.println(x.subtract(y)); break;
case "x": out.println(x.multiply(y)); break;
case "/": out.println(x.divide(y));
}
...
while
迴圈可以使用在結束條件不確定的情況下。例如求最大公因數可以如下:
public class GCD {
public static void main(String[] args) {
int m = Integer.parseInt(args[0]);
int n = Integer.parseInt(args[1]);
while(n != 0) {
int r = m % n;
m = n;
n = r;
}
System.out.printf("GCD: %d%n", m);
}
}
for
通常用來進行索引相關的迴圈,例如指定索引迭代命令列引數:
for(int i = 0; i < args.length; i++) {
out.println(args[i]);
}
從頭至尾循序存取是很常見的需求,這邊介紹到的陣列、
List
、Set
等,其實都可以使用 foreach 語法,將元素從頭至尾循序迭代:
for(String arg : args) {
out.println(arg);
}
實際上,只要是具備
java.lang.Iterable
行為的物件,都可以搭配 foreach 語法,像是 Java Tutorial 第一堂(4)中使用到的 Path
實例,就可以搭配 foreach 來迭代路徑資訊。 練習 6:使用 foreach 迭代路徑
在 Java 中,可以查看 API 文件,確認物件是否擁有某些行為。舉例來說,想瞭解
List
是否為一種 Iterable
,可以查看 List
的 API 文件: 在 All Superinterfaces 上,可以看到
Iterable
,這表示 List
也是一種 Iterable
。如果想知道總共有哪些物件具有 Iterable
的行為,則可以查看 Iterable
的 API 文件,看看它的 All Know Implementing Classes 有哪些。 類似地,查看一下
Path
的 API 文件,可以發現它也是一種 Iterable
,因此可以撰寫一個程式如下:
import java.nio.file.*;
public class ForEach {
public static void main(String[] args) {
for(Path parent : Paths.get(args[0])) {
System.out.println(parent);
}
}
}
這個程式可以接受命令列引數,你可以指定一個目錄,程式將會將每個子路徑顯示出來,你可以使用
javac
編譯、java
來執行,或者是使用至目前為止習得的 Gradle 工具來建構程式並執行。