由於我們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,於是打算做一些翻譯工作,自己學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有許多練習題,但是沒有標准答案,所給出的答案均為譯者所寫,有錯誤的地方還請指出。
審校:李秋豪
V1.0 Fri Mar 2 16:48:58 CST 2018
譯者注:本章主要講解了Java的一些基礎知識,以及Python和Java的一些區別與聯系,熟悉的讀者可以略過本章。
Reading 2: Java 基礎
本節目標
- 學習Java基本的語法和語義
- 從Python過渡到Java(譯者注:MIT是先學的Python)
本課程目標
遠離bug | 易讀性 | 可改動性 |
---|---|---|
確保現在和將來都是正確的 | 使得未來的閱讀者(包括你自己)能夠讀懂代碼意圖 | 架構設計應該容許未來的改動 |
開始寫Java
閱讀 From Python to Java的前六節 (譯者注:這個網站(英文)上詳細介紹了Python和Java的異同和注意點,學過Python沒學過Java的同學可以認真看一看):
- 程序的結構與執行
- 數據類型及其表達
- 簡單聲明
- 終端輸入輸出
- 控制流
- 對象與接口
閱讀小練習
語言基礎
下面這塊代碼取自某一個函數中:
int a = 5; // (1)
if (a > 10) { // (2)
int b = 2; // (3)
} else { // (4)
int b = 4; // (5)
} // (6)
b *= 3; // (7)
哪一行會導致編譯時報錯?
7
修改bug
選擇出(多選)最簡單能改掉這個bug的操作:
-
[x] 在第一行后聲明
int b;
-
[ ] 在第二行之前賦值
b = 0;
-
[x] 第三行改為
b = 2;
-
[x] 第五行改為
b = 4;
-
[ ] 第七行改為聲明並賦值
int b *= 3;
我們按照上面的修改策略進行了修改,如果我們將else
塊注釋掉,會發生什么呢?
- [ ]
b
為 0 - [ ]
b
為 3 - [ ]
b
為 6 - [x] 編譯器會報錯,在我們運行程序之前
- [ ] 在我們運行程序的時候報錯,在我們到達最后一行之前
- [ ] 在我們運行程序的時候報錯,在我們到達最后一行的時候
數字與字符串
下面這個程序語句將華氏溫度轉化為攝氏溫度,在Python中能得到正確結果嗎?
fahrenheit = 212.0
celsius = (fahrenheit - 32) * 5/9
-
[x] 是
-
[ ] 否:整數除法運算會導致攝氏溫度變為0
-
[ ] 否:整數除法運算會導致攝氏溫度被向下取整
如果改用Java寫,第一行應該改為(譯者注:這里網站上給的是單選,但是譯者覺得存在多個答案):
-
[ ]
int fahrenheit = 212.0;
-
[ ]
Integer fahrenheit = 212.0;
-
[ ]
float fahrenheit = 212.0;
-
[ ]
Float fahrenheit = 212.0;
-
[x]
double fahrenheit = 212.0;
-
[x]
Double fahrenheit = 212.0;
第二行應該改為(???
是你上面選擇的類型):
-
[x]
??? celsius = (fahrenheit - 32) * 5/9;
-
[ ]
??? celsius = (fahrenheit - 32) * (5 / 9);
-
[x]
??? celsius = (fahrenheit - 32) * (5. / 9);
應該如何輸出?
-
[ ]
System.out.println(fahrenheit, " -> ", celsius);
-
[x]
System.out.println(fahrenheit + " -> " + celsius);
-
[ ]
System.out.println("%s -> %s" % (fahrenheit, celsius));
-
[x]
System.out.println(Double.toString(fahrenheit) + " -> " + Double.toString(celsius));
用快照圖理解值與對象
為了弄清楚一些隱秘的問題,我們會畫一些圖來進行解釋。快照圖(Snapshot diagrams)能代表程序運行時的各種狀態——它的棧(即方法和局部變量)和它的堆(即現在存在的對象)。
具體來講,使用快照圖有以下優點:
- 在課堂上和會議上與同學交流
- 解釋一些概念例如原始類型 vs. 對象類型不可更改的值 vs. 不可更改的引用, 指針別名, stack棧 vs. 堆heap, 抽象表達 vs. 具體表達.
- 能夠幫助你解釋你的工程的設計思想
- 為以后的課程做鋪墊(例如MIT 6.170中的對象模型)
雖然這些圖像都只是解釋Java中的一些概念,但是很多都可以延伸到別的現代語言中,例如Python, JavaScript, C++, Ruby.
原始值
原始值都是以常量來表達的。上面箭頭的來源可以是一個變量或者一個對象的內部區域(field)。
對象值
一個對象用一個圓圈表示。對象內部會有很多區域(field),這些區域又指向它們對應的值。同時這些區域也是有它們的類型的,例如int x
。
可更改的值 vs. 可被重新賦值的改變
通過快照圖我們可以視圖化可更改的值和可被重新賦值的改變之間的區別:
- 當你給一個變量或者一個區域(filed)賦值的時候,你實際上是改變了它指向的方向,即指向了另一個值。
- 當你修改一個可被更改的(mutable)值的時候——例如數組或者列表——你真正修改了這個值本身(譯者注:變量或者區域的指向並沒有變)
重新賦值和不可改變的(immutable)值
例如,如果我們有一個 String
變量 s
, 我們可以將它從 "a"
賦值為 "ab"
.
String s = "a";
s = s + "b";
String
就是一種不可改變的(immutable)值,這種類型的值在第一次確定后就不能改變。不可改變性是我們這門課程的一個重要設計原則,以后的課程中會詳細介紹的。
不可更改的對象(設計者希望它們一直是這個值)在快照圖中以雙圓圈的邊框表示,例如上面的字符串對象。
可更改的(mutable)值
與此相對應的, StringBuilder
(Java的一個內置類) 是一個可更改的字符串對象,它內置了許多改變其內容的方法:
StringBuilder sb = new StringBuilder("a");
sb.append("b");
可更改性和不可更改性(mutability and immutability)將會對我們的“安全健壯性”目標起到重要作用。
不可更改的引用
Java也提供了不可更改的引用:final
聲明,變量一旦被賦值就不能再次改變它的引用(指向的值或者對象)。
final int n = 5;
如果Java編譯器發現final
聲明的變量在運行中被賦值多次,它就會報錯。所以final
就是為不可更改的引用提供了靜態檢查。
在快照圖中,不可更改的引用(final
)用雙箭頭表示,例如上圖中的id
,Person
的id
引用不可改變,但是age卻是可改變的。
這里要特別注意一點,final
只是限定了引用不可變,我們可以將其引用到一個可更改的值 (例如final StringBuilder sb
),雖然引用不變,但引用的對象本身的內容可以改變。
同樣的,我們也可以將一個可更改的引用作用到一個不可更改的值(例如String s
),這個時候變量值的改變就是將引用改變。
Java 聚合類型
閱讀 From Python to Java上的Collections 章節(譯者注:英文)。
列表、集合、映射(Lists, Sets, and Maps)
Java中的列表和Python中很相似 。列表可以包含零個或多個對象,而且對象可以出現多次。我們可以在列表中刪除或添加元素。
一些 List
常見的操作:
Java | 描述 | Python |
---|---|---|
int count = lst.size(); |
計算列表中元素的個數 | count = len(lst) |
lst.add(e); |
在列表最后添加元素 | lst.append(e) |
if (lst.isEmpty()) ... |
測試列表是否為空 | if not lst: ... |
在快照圖中,我們用數字索引表示列表中的各個區域(filed),例如一個全是String對象的列表:
Java中的映射和Python中的字典類似 。在Python中,字典的keys必須是 可哈希的hashable ,Java也是類似。我們會在后面講解對象對等的時候說這個。
常用的 Map
操作 :
Java | 描述 | Python |
---|---|---|
map.put(key, val) |
添加映射 key → val | map[key] = val |
map.get(key) |
獲取key映射的值 | map[key] |
map.containsKey(key) |
測試key是否存在 | key in map |
map.remove(key) |
刪除key所在的映射 | del map[key] |
在快照圖中,我們將Map
表示為包含key/value對的對象。例如一個Map<String, Turtle> :
集合是一種含有零個或多個不重復對象的聚合類型 。和映射中的key相同,Python中的集合的元素也要求是可哈希的hashable ,Java也是類似。
常用的 Set
操作:
Java | 描述 | Python |
---|---|---|
s1.contains(e) |
測試集合中是否含有e | e in s1 |
s1.containsAll(s2) |
測試是否 s1 ⊇ s2 | s1.issuperset(s2) s1 >= s2 |
s1.removeAll(s2) |
在 s1 中去除s2的元素 | s1.difference_update(s2) s1 -= s2 |
在快照圖中,我們不用數字索引表示集合的元素(即元素沒有順序的概念),例如一個含有整數的集合:
List, Set, and Map的普遍聲明方法
Python 提供了創建列表和字典的方便方法:
lst = [ "a", "b", "c" ]
dict = { "apple": 5, "banana": 7 }
Java 不是這樣 它只為數組提供了類似的創建方法:
String[] arr = { "a", "b", "c" };
我們可以用the utility function Arrays.asList
從數組創建列表:
Arrays.asList(new String[] { "a", "b", "c" })
… 或者直接提供元素:
Arrays.asList("a", "b", "c")
要注意的是,如果一個 List
是用 Arrays.asList
創建的,它的長度就固定了。
在Python中,聚合類中的元素的類型可以不同,但是在Java中,我們能夠要求編譯器對操作進行靜態檢查,確保聚合類中的元素類型相同。例如:
List<String> cities; // a List of Strings
Set<Integer> numbers; // a Set of Integers
Map<String,Turtle> turtles; // a Map with String keys and Turtle values
由於Java要求元素的普遍性,我們不能直接使用原始類型作為元素的類型,例如Set<int>
,但是,正如前面所提到的, int
有一個對應的 Integer
”包裝“對象類型,我們可以用 Set<Integer> numbers
.
為了使用方便,Java會自動在原始類型和包裝過的對象類型中做一些轉換,所以如果我們聲明一個 List<Integer> sequence
,下面的這個代碼也能夠正常運行:
sequence.add(5); // add 5 to the sequence
int second = sequence.get(1); // get the second element
創建列表:ArrayList 與 LinkedList
我們馬上就會看到,Java區分了兩個概念:類型的規格說明——它的行為;類型的實現——代碼是是什么。
List
, Set
, 和 Map
都是接口 :他們定義了類型的工作,但是他們不提供具體的實現代碼。這有很多優點,其中一個就是我們能根據具體的環境使用更適合的實現方式。
例如List
的創建:
List<String> firstNames = new ArrayList<String>();
List<String> lastNames = new LinkedList<String>();
如果左右兩邊的類型參數都是一樣的,Java可以自動識別,這樣可以少打一些字:
List<String> firstNames = new ArrayList<>();
List<String> lastNames = new LinkedList<>();
ArrayList
和 LinkedList
是實現List
的其中兩種方法。他們都提供的List
要求的操作,而且這些操作的行為必須和文檔規定的相同。在上面的例子中, firstNames
和 lastNames
的行為一樣,也就是說,如果我們在一串代碼中將 ArrayList
vs. LinkedList
互換,代碼依然能夠正常工作。
不幸的是,這也是一種負擔,既然在Python我們不用關心列表的具體實現,為什么在Java中要關心呢?由於這只會導致程序性能的不同,在本門課程中我們不會做相應的要求。當你不確定時,使用ArrayList
。
創建集合和映射:HashSets 與 HashMaps
對於集合,我們默認使用HashSet
:
Set<Integer> numbers = new HashSet<>();
Java 也提供了 sorted sets ,它是用 TreeSet
實現的.
對於映射,我們默認使用 HashMap
:
Map<String,Turtle> turtles = new HashMap<>();
迭代
我們創建了以下聚合類變量:
List<String> cities = new ArrayList<>();
Set<Integer> numbers = new HashSet<>();
Map<String,Turtle> turtles = new HashMap<>();
一個常見的工作就是遍歷這些聚合類中的各個元素。
在Python中,我們可以這么寫:
for city in cities:
print(city)
for num in numbers:
print(num)
for key in turtles:
print("%s: %s" % (key, turtles[key]))
對於List
和 Set
,Java提供了類似的語法:
for (String city : cities) {
System.out.println(city);
}
for (int num : numbers) {
System.out.println(num);
}
我們不能對Map
進行完全一樣的操作,但是我們可以像上面那樣遍歷它的keys,結合映射對象提供的方法來遍歷所有的對(pairs):
for (String key : turtles.keySet()) {
System.out.println(key + ": " + turtles.get(key));
}
實際上,這個for
循環用到了 Iterator
, 我們會在后續的課程中講解這種設計。
警告: 一定要注意在循環的時候不要改變你的循環參量(他是可改變的值)!添加、刪除、或者替換都會影響你的循環甚至中斷你的程序,我們會在后面的章節中講解更多的細節。這對Python也是適用的。例如下面這串代碼:
numbers = [100,200,300]
for num in numbers:
numbers.remove(num) # danger!!! mutates the list we're iterating over
print(numbers) # list should be empty here -- is it?
使用數字索引進行迭代
Java也提供了一種使用數字索引進行迭代的方法(譯者注:C的標准寫法):
for (int ii = 0; ii < cities.size(); ii++) {
System.out.println(cities.get(ii));
}
除非你真的需要索引ii,否則我們不推薦這種寫法,它可能會引來一些難以發現的bug。
閱讀小練習
聚合類型
將下面使用數組聲明的變量用List
進行聲明(不用初始化):
String[] names;
-> List<String> names;
int[] numbers;
-> List<Integer> numbers;
char[][] grid;
-> List<List<Character>> grid;
“找出關鍵點”
在運行下列代碼后:
Map<String, Double> treasures = new HashMap<>();
String x = "palm";
treasures.put("beach", 25.);
treasures.put("palm", 50.);
treasures.put("cove", 75.);
treasures.put("x", 100.);
treasures.put("palm", treasures.get("palm") + treasures.size());
treasures.remove("beach");
double found = 0;
for (double treasure : treasures.values()) {
found += treasure;
}
以下操作霍變量的值分別為:
treasures.get(x)
-> 54.0
treasures.get("x")
-> 100.0
found
-> 229.0
枚舉類型
有時候一種類型中會存在一個既小又有限的不可變的值的集合,例如:
- 一年中的月份: January, February, …, November, December
- 一周中的每一天:Monday, Tuesday, …, Saturday, Sunday
- 指南針中的方向:north, south, east, west
- 可以配出的顏色:black, gray, red, …
當不可變的值的集合滿足“小”和“有限”這兩個條件時,將這個集合中的所有值統一定義為一個命名常量就是有意義的。在JAVA中,這樣的命名常量就稱為enumeration(枚舉類型) 並且使用關鍵字 enum
來構造。
public enum Month {
JANUARY, FEBRUARY, MARCH, APRIL,
MAY, JUNE, JULY, AUGUST,
SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}
public enum PenColor {
BLACK, GRAY, RED, PINK, ORANGE,
YELLOW, GREEN, CYAN, BLUE, MAGENTA;
}
你可以在聲明變量或函數的時候使用例如PenColor
這樣的枚舉類型名:
PenColor drawingColor;
像引用一個被命名的靜態常量一樣來引用枚舉類型的值:
drawingColor = PenColor.RED;
需要強調的是,枚舉類型是一個獨特的新類型。較老的語言,像Python2和java的早期版本,它們傾向於使用數字常量或者字符串來表示這樣的值的有限集。但是一個枚舉型變量更加“類型安全”,因為它可以發現一些類型錯誤,如類型不匹配:
int month = TUESDAY; // 如果month定義為整型值(TUESDAY也是一個整型值),那么這樣寫不會報錯(但是從語義上看是錯的,因為顯然不能將“周四”賦值給一個“月份”,這可能不符合作者的本意)
Month month = DayOfWeek.TUESDAY; // 如果month被定義為枚舉類型Month,那么這條語句將會觸發靜態錯誤 (static error)
或者拼寫錯誤:
String color = "REd"; // 不報錯,拼寫錯誤被忽略
PenColor drawingColor = PenColor.REd; // 當枚舉類型的值被拼寫錯時,會觸發靜態錯誤
Python3 現在也有枚舉類型, 和java的類似, 但是Python3沒有靜態類型檢查。
Java API 文檔
在之前的講義中已經多次使用java類的文檔鏈接,它們都是Java platform API的一部分。
API是 應用編程接口(application programming interface )的簡稱。比如Facebook開放了一個供你編程的API(實際上不止一個,因為需要對不同的語言和架構開放不同的API),那么你就可以用它來寫一個和Facebook交互的應用。
- java.lang.String 是
String
類型的全稱。我們僅僅使用"雙引號"
這樣的方式就可以創建一個String
類型的對象。 - java.lang.Integer 和其他原始包裝器類。在多數情況下,Java都會自動地在原始類型(如int)和它們被包裝(wrapped,或者稱為“封裝,boxed”)之后的類型之間相互轉換。
- java.util.List 就像Python中的列表,但是在Python中,列表是語言的一部分。在Java中,
List
需要用Java來具體實現。 - java.util.Map 就像Python的字典。
- java.io.File 用於表示硬盤上的文件。讓我們看看
File
對象提供的方法:我們可以測試這個文件是否可讀、刪除這個文件、查看這個文件最近一次被修改是什么時候... - java.io.FileReader 使我們能夠讀取文本文件。
- java.io.BufferedReader 讓我們高效地讀取文本文件。它還提供一個很有用的特性:一次讀取一整行。
讓我們更深入地看看 BufferedReader
的文檔。文檔中有很多我們還沒討論過的相關Java特性!保持頭腦清醒並且將注意力集中在下圖展示的信息中。
在這一頁的頂部是BufferedReader
的繼承關系和一系列已經實現的接口。一個BufferedReader
對象可以調用這些被列出的類型中定義的所有可用的方法(加上它自己定義的方法)。
接下來會看到它的 直接子類,對於一個接口來說就是一個實現類。這可以幫助我們獲取諸如HashMap
是Map
的直接子類這樣的信息。
再往下是對這個類的描述。有時候這些描述會有一些模棱兩可,但是如果你要了解一個類,這里就是你的第一選擇。
如果你想創建一個BufferedReader
,那么constructor summary版塊就是你要看的資料。構造函數並不是Java中唯一獲取一個新對象的方法,但它卻是最為普遍使用的。
接下來是BufferedReader
對象中所有我們可以調用的方法的列表。
在綜述下面是每個方法和構造函數的詳細描述。點擊一個構造函數或者方法即可看到詳細的描述。如果你想弄明白一個方法有什么作用,那你應該查看這里。
每一個詳細描述包括以下內容:
- 方法簽名(signature):我們能看到方法的返回值類型,方法名,以及參數。我們還可以看到一些異常,就目前而言,它們就是運行這個方法可能導致的錯誤。
- 完整的描述。
- 參數:描述這個方法接收的參數。
- 對方法返回值的描述。
規格說明
這些詳細的描述稱為 規格說明。它們使得我們能夠在不了解具體實現代碼的情況下使用諸如 String
, Map
, 或BufferedReader
這樣的工具。
讀、寫、理解和分析這些規格說名將會是我們在課程6.031中的主要內容之一,將會在幾節課以后開始講解。
閱讀小練習
讀Java文檔
閱讀Java API文檔來回答下列問題:
假設我們有一個 TreasureChest
類。在我們運行如下代碼之后:
Map<String, TreasureChest> treasures = new HashMap<>();
treasures.put("beach", new TreasureChest(25));
TreasureChest result = treasures.putIfAbsent("beach", new TreasureChest(75));
(譯者注:putIfAbsent
:If the specified key is not already associated with a value (or is mapped to null
) associates it with the given value and returns null
, else returns the current value.)
result
變量的值是什么?
- [x] 25 treasure
- [ ] 75 treasure
- [ ] another amount of treasure
- [ ]
null
Avast!
在運行下面這段代碼之后:
Map<String, String> translations = new HashMap<>();
translations.put("green", "verde");
??? result = translations.replace("green", "verde", "ahdar");
result
的值是什么?(譯者注:replace
:Replaces the entry for the specified key only if currently mapped to the specified value. true
if the value was replaced)
- [ ]
"green"
- [ ]
"verde"
- [ ]
"ahdar"
- [ ]
true
- [ ]
false
- [ ]
1
- [ ]
null
- [x] 以上沒有正確答案(譯者注:boolean)