前言:想改掉一些壞習慣嗎?讓咱們從 null、函數式編程以及 getter 和 setter 着手,看看如何改善代碼。程序員
做爲 Java 開發人員,咱們會使用一些習慣用法,典型的例子,如:返回 null 值、濫用 getter 和 setter,即便在沒有必要的狀況下也是如此。雖然在某些狀況下,這些用法多是適當的,但一般是習慣使然,或者是咱們爲了讓系統正常工做的權宜之計。在本文中,咱們將討論在 Java 初學者甚至高級開發人員中都常見的三種狀況,並探究它們是如何給咱們帶來麻煩的。應該指出的是,文中總結的規則並非不管什麼時候都應該始終遵照的硬性要求。有時候,可能有一個很好的理由來使用這些模式解決問題,可是總的來講,仍是應該相對的減小這些用法。首先,咱們將從 Null 這個關鍵字開始討論,它也是 Java 中使用最頻繁、但也是最具兩面性的關鍵字之一。web
1. Returning Null(返回 Null)數據庫
null 一直是開發者最好的朋友,也是最大的敵人,這在 Java 中也不例外。在高性能應用中,使用 null 是一種減小對象數量的可靠方法,它代表方法沒有要返回的值。與拋出異常不一樣,若是要通知客戶端不能獲取任何值,使用 null 是一種快速且低開銷的方法,它不須要捕獲整個堆棧跟蹤。編程
在高性能系統的環境以外,null 的存在會致使建立更繁瑣的 null 返回值檢查,從而破壞應用程序,並在解引用空對象時致使 NullPointerExceptions。在大多數應用程序中,返回 null 有三個主要緣由:api
表示列表中找不到元素;數組
表示即便沒有發生錯誤,也找不到有效值;安全
表示特殊狀況下的返回值。多線程
除非有任何性能方面的緣由,不然以上每一種狀況都有更好的解決方案,它們不使用 null,而且強制開發人員處理出現 null 的狀況。更重要的是,這些方法的客戶端不會爲該方法是否會在某些邊緣狀況下返回 null 而傷腦筋。在每種狀況下,咱們將設計一種不返回 null 值的簡潔方法。app
No Elements(集合中沒有元素的狀況)ide
在返回列表或其餘集合時,一般會看到返回空集合,以代表沒法找到該集合的元素。例如,咱們能夠建立一個服務來管理數據庫中的用戶,該服務相似於如下內容(爲了簡潔起見,省略了一些方法和類定義):
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return null;
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
if (users != null) {
for (User user: users) {
System.out.println("User found: " + user.getName());
}
}
由於咱們選擇在沒有用戶的狀況下返回 null 值,因此咱們迫使客戶端在遍歷用戶列表以前先處理這種狀況。若是咱們返回一個空列表來表示沒有找到用戶,那麼客戶端能夠徹底刪除空檢查並像往常同樣遍歷用戶。若是沒有用戶,則隱式跳過循環,而沒必要手動處理這種狀況;從本質上說,循環遍歷用戶列表的功能就像咱們爲空列表和填充列表所作的那樣,而不須要手動處理任何一種狀況:
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return Collections.emptyList();
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
for (User user: users) {
System.out.println("User found: " + user.getName());
}
在上面的例子中,咱們返回的是一個不可變的空列表。這是一個可接受的解決方案,只要咱們記錄該列表是不可變的而且不該該被修改(這樣作可能會拋出異常)。若是列表必須是可變的,咱們能夠返回一個空的可變列表,以下例所示:
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return new ArrayList<>(); // A mutable list
}
else {
return Arrays.asList(usersFromDb);
}
}
通常來講,當沒有發現任何元素的時候,應遵照如下規則:
返回一個空集合(或 list、set、queue 等等)代表找不到元素。
這樣作不只減小了客戶端必須執行的特殊狀況處理,並且還減小了接口中的不一致性(例如,咱們經常返回一個 list 對象,而不是其餘對象)。
Optional Value(可選值)
不少時候,咱們但願在沒有發生錯誤時通知客戶端不存在可選值,此時返回 null。例如,從 web 地址獲取參數。在某些狀況下,參數可能存在,但在其餘狀況下,它可能不存在。缺乏此參數並不必定表示錯誤,而是表示用戶不須要提供該參數時包含的功能(例如排序)。若是沒有參數,則返回 null;若是提供了參數,則返回參數值(爲了簡潔起見,刪除了一些方法):
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public String getSortingValue() {
if (urlContainsSortParameter(url)) {
return extractSortParameter(url);
}
else {
return null;
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
String sortingParam = url.getSortingValue();
if (sortingParam != null) {
UserSorter sorter = UserSorter.fromParameter(sortingParam);
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
當沒有提供參數時,返回 null,客戶端必須處理這種狀況,可是在getSortingValue 方法的簽名中,沒有任何地方聲明排序值是可選的。若是方法的參數是可選的,而且在沒有參數時,可能返回 null,要知道這個事實,咱們必須閱讀與該方法相關的文檔(若是提供了文檔)。
相反,咱們可使可選性顯式地返回一個 Optional 對象。正如咱們將看到的,當沒有參數存在時,客戶端仍然須要處理這種狀況,可是如今這個需求已經明確了。更重要的是,Optional 類提供了比簡單的 null 檢查更多的機制來處理丟失的參數。例如,咱們可使用 Optional 類提供的查詢方法(一種狀態測試方法)簡單地檢查參數是否存在:
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public Optional<String> getSortingValue() {
if (urlContainsSortParameter(url)) {
return Optional.of(extractSortParameter(url));
}
else {
return Optional.empty();
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
if (sortingParam.isPresent()) {
UserSorter sorter = UserSorter.fromParameter(sortingParam.get());
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
這與「空檢查」的狀況幾乎相同,可是咱們已經明確了參數的可選性(即客戶機在不調用 get() 的狀況下沒法訪問參數,若是可選參數爲空,則會拋出NoSuchElementException)。若是咱們不但願根據 web 地址中的可選參數返回用戶列表,而是以某種方式使用該參數,咱們可使用 ifPresentOrElse 方法來這樣作:
sortingParam.ifPresentOrElse(
param -> System.out.println("Parameter is :" + param),
() -> System.out.println("No parameter supplied.")
);
這極大下降了「空檢查」的影響。若是咱們但願在沒有提供參數時忽略參數,可使用 ifPresent 方法:
sortingParam.ifPresent(param -> System.out.println("Parameter is :" + param));
在這兩種狀況下,使用 Optional 對象要優於返回 null 以及顯式地強制客戶端處理返回值可能不存在的狀況,爲處理這個可選值提供了更多的途徑。考慮到這一點,咱們能夠制定如下規則:
若是返回值是可選的,則經過返回一個 Optional 來確保客戶端處理這種狀況,該可選的值在找到值時包含一個值,在找不到值時爲空
Special-Case Value(特殊狀況值)
最後一個常見用例是特殊用例,在這種狀況下沒法得到正常值,客戶端應該處理與其餘用例不一樣的極端狀況。例如,假設咱們有一個命令工廠,客戶端按期從命令工廠請求命令。若是沒有命令能夠得到,客戶端應該等待 1 秒鐘再請求。咱們能夠經過返回一個空命令來實現這一點,客戶端必須處理這個空命令,以下面的例子所示(爲了簡潔起見,沒有顯示一些方法):
public interface Command {
public void execute();
}
public class ReadCommand implements Command {
@Override
public void execute() {
System.out.println("Read");
}
}
public class WriteCommand implements Command {
@Override
public void execute() {
System.out.println("Write");
}
}
public class CommandFactory {
public Command getCommand() {
if (shouldRead()) {
return new ReadCommand();
}
else if (shouldWrite()) {
return new WriteCommand();
}
else {
return null;
}
}
}
CommandFactory factory = new CommandFactory();
while (true) {
Command command = factory.getCommand();
if (command != null) {
command.execute();
}
else {
Thread.sleep(1000);
}
}
因爲 CommandFactory 能夠返回空命令,客戶端有義務檢查接收到的命令是否爲空,若是爲空,則休眠1秒。這將建立一組必須由客戶端自行處理的條件邏輯。咱們能夠經過建立一個「空對象」(有時稱爲特殊狀況對象)來減小這種開銷。「空對象」將在 null 場景中執行的邏輯(休眠 1 秒)封裝到 null 狀況下返回的對象中。對於咱們的命令示例,這意味着建立一個在執行時休眠的SleepCommand:
public class SleepCommand implements Command {
@Override
public void execute() {
Thread.sleep(1000);
}
}
public class CommandFactory {
public Command getCommand() {
if (shouldRead()) {
return new ReadCommand();
}
else if (shouldWrite()) {
return new WriteCommand();
}
else {
return new SleepCommand();
}
}
}
CommandFactory factory = new CommandFactory();
while (true) {
Command command = factory.getCommand();
command.execute();
}
與返回空集合的狀況同樣,建立「空對象」容許客戶端隱式處理特殊狀況,就像它們是正常狀況同樣。但這並不老是可行的;在某些狀況下,處理特殊狀況的決定必須由客戶作出。這能夠經過容許客戶端提供默認值來處理,就像使用 Optional 類同樣。在 Optional 的狀況下,客戶端可使用 orElse 方法獲取包含的值或默認值:
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElse("ASC");
若是有一個提供的排序參數(例如,若是 Optional 包含一個值),這個值將被返回。若是不存在值,默認狀況下將返回「ASC」。Optional 類還容許客戶端在須要時建立默認值,以防默認建立過程開銷較大(即只在須要時建立默認值):
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElseGet(() -> {
// Expensive computation
});
結合「空對象」和默認值的用法,咱們能夠設計如下規則:
若是可能,使用「空對象」處理使用 null 關鍵字的狀況,或者容許客戶端提供默認值
2. Defaulting to Functional Programming(默認使用函數式編程)
自從在 JDK 8 中引入了 stream 和 lambda 表達式以後,就出現了向函數式編程遷移的趨勢,這理當如此。在 lambda 表達式和 stream 出現以前,執行函數式任務是很是麻煩的,而且會致使代碼可讀性的嚴重降低。例如,以下代碼用傳統方式過濾一個集合:
public class Foo {
private final int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Iterator<Foo> iterator = foos.iterator();
while(iterator.hasNext()) {
if (iterator.next().getValue() > 10) {
iterator.remove();
}
}
雖然這段代碼很緊湊,但它並無以一種明顯的方式告訴咱們,當知足某個條件時,咱們將嘗試刪除集合的元素。相反,它告訴咱們,當集合中有更多的元素時將遍歷集合,並將刪除值大於 10 的元素(咱們能夠假設正在進行篩選,可是刪除元素的部分被代碼的冗長所掩蓋)。咱們可使用函數式編程將這個邏輯壓縮爲一條語句:
foos.removeIf(foo -> foo.getValue() > 10);
這個語句不只比迭代方式更簡潔,並且準確的告訴咱們它的行爲。若是咱們爲 predicate 命名並將其傳遞給 removeIf 方法,甚至可使其更具可讀性:
Predicate<Foo> valueGreaterThan10 = foo -> foo.getValue() > 10;
foos.removeIf(valueGreaterThan10);
這段代碼的最後一行讀起來像一個英語句子,準確地告訴咱們語句在作什麼。對於看起來如此緊湊和極具可讀性的代碼,在任何須要迭代的狀況下嘗試使用函數式編程是很讓人嚮往的,但這是一種天真的想法。並非每種狀況都適合函數式編程。例如,若是咱們嘗試在一副牌中打印一組花色和牌面大小的排列組合(花色和牌面大小的每一種組合),咱們能夠建立如下內容(參見《Effective Java, 3rd Edition》得到這個示例的詳細內容):
public static enum Suit {
CLUB, DIAMOND, HEART, SPADE;
}
public static enum Rank {
ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;
}
Collection<Suit> suits = EnumSet.allOf(Suit.class);
Collection<Rank> ranks = EnumSet.allOf(Rank.class);
suits.stream()
.forEach(suit -> {
ranks.stream().forEach(rank -> System.out.println("Suit: " + suit + ", rank: " + rank));
});
雖然讀起來並不複雜,但這種實現並非最簡單的。很明顯,咱們正試圖強行使用 stream,而此時使用傳統迭代明顯更有利。若是咱們使用傳統的迭代方法,咱們能夠將 花色和等級的排列組合簡化爲:
for (Suit suit: suits) {
for (Rank rank: ranks) {
System.out.println("Suit: " + suit + ", rank: " + rank);
}
}
這種風格雖然不那麼浮華,但卻直截了當得多。咱們能夠很快地理解,咱們試圖遍歷每一個花色和等級,並將每一個等級與每一個花色配對。stream 表達式越大,函數式編程的乏味性就越明顯。以 Joshua Bloch 在《Effective Java, 3rd Edition》第 205 頁,第 45 項中建立的如下代碼片斷爲例,在用戶提供的路徑上查找字典中包含的指定長度內的全部詞組:
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
}
即便是經驗最豐富的 stream 使用者也可能會對這個實現感到迷茫。短期內很難理解代碼的意圖,須要大量的思考才能發現上面的 stream 操做試圖實現什麼。這並不意味着 stream 必定很複雜或太冗長,只是由於它們不老是最好的選擇。正如咱們在上面看到的,使用 removeIf 能夠將一組複雜的語句簡化爲一個易於理解的語句。所以,咱們不該該試圖用 stream 甚至 lambda 表達式替換傳統迭代的每一個使用場景。相反,在決定是使用函數式編程仍是使用傳統方式時,咱們應該遵循如下規則:
函數式編程和傳統的迭代都有其優勢和缺點:應該以簡易性和可讀性爲準來選擇
儘管在每一個可能的場景中使用 Java 最炫、最新的特性可能很讓人嚮往,但這並不老是最好的方法。有時候,老式的功能效果反而最好。
3. Creating Indiscriminate Getters and Setters(濫用 getter 和 setter)
新手程序員學到的第一件事是將與類相關的數據封裝在私有字段中,並經過公共方法暴露它們。在實際使用時,經過建立 getter 來訪問類的私有數據,建立 setter 來修改類的私有數據:
public class Foo {
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
雖然這對於新程序員來講是一個很好的學習實踐,但這種作法不能未經思索就應用在中級或高級編程。在實際中一般發生的狀況是,每一個私有字段都有一對 getter 和 setter 將類的內部內容暴露給外部實體。這會致使一些嚴重的問題,特別是在私有字段是可變的狀況下。這不只是 setter 的問題,甚至在只有 getter 時也是如此。如下面的類爲例,該類使用 getter 公開其惟一的字段:
public class Bar {
private Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
public Foo getFoo() {
return foo;
}
}
因爲咱們刪除了 setter 方法,這麼作可能看起來明智且無害,但並不是如此。假設另外一個類訪問 Bar 類型的對象,並在 Bar 對象不知道的狀況下更改 Foo 的底層值:
Foo foo = new Foo();
Bar bar = new Bar(foo);
// Another place in the code
bar.getFoo().setValue(-1);
在本例中,咱們更改了 Foo 對象的底層值,而沒有通知 Bar 對象。若是咱們提供的 Foo 對象的值破壞了 Bar 對象的一個不變量,這可能會致使一些嚴重的問題。舉個例子,若是咱們有一個不變量,它表示 Foo 的值不多是負的,那麼上面的代碼片斷將在不通知 Bar 對象的狀況下靜默修改這個不變量。當 Bar 對象使用它的 Foo 對象值時,事情可能會迅速向很差的方向發展,尤爲是若是 Bar 對象假設這是不變的,由於它沒有暴露 setter 直接從新分配它所保存的 Foo 對象。若是數據被嚴重更改,這甚至會致使系統失敗,以下面例子所示,數組的底層數據在無心中暴露:
public class ArrayReader {
private String[] array;
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
public void read() {
for (String e: array) {
System.out.println(e);
}
}
}
public class Reader {
private ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = arrayReader;
}
public ArrayReader getArrayReader() {
return arrayReader;
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
reader.getArrayReader().setArray(null);
reader.read();
執行此代碼將致使 NullPointerException,由於當 ArrayReader 的實例對象試圖遍歷數組時,與該對象關聯的數組爲 null。這個 NullPointerException 的使人不安之處在於,它可能在對 ArrayReader 進行更改好久以後才發生,甚至可能發生在徹底不一樣的場景中(例如在代碼的不一樣部分中,甚至在不一樣的線程中),這使得調試變得很是困難。
讀者若是仔細考慮,可能還會注意到,咱們能夠將私有的 ArrayReader 字段設置爲 final,由於咱們在經過構造函數賦值以後,沒有對它從新賦值的方法。雖然這看起來會使 ArrayReader 成爲常量,確保咱們返回的 ArrayReader 對象不會被更改,但事實並不是如此。若是將 final 添加到字段中只能確保字段自己沒有從新賦值(即,不能爲該字段建立 setter)而不會阻止對象自己的狀態被更改。或者咱們試圖將 final 添加到 getter 方法中,這也是徒勞的,由於方法上的 final 修飾符只意味着該方法不能被子類重寫。
咱們甚至能夠更進一步考慮,在 Reader 的構造函數中防護性地複製 ArrayReader 對象,確保在將對象提供給 Reader 對象以後,傳入該對象的對象不會被篡改。例如,應避免如下狀況發生:
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
arrayReader.setArray(null); // Change arrayReader after supplying it to Reader
reader.read(); // NullPointerException thrown
即便有了這三個更改(字段上增長 final 修飾符、getter 上增長 final 修飾符以及提供給構造函數的 ArrayReader 的防護性副本),咱們仍然沒有解決問題。問題不在於咱們暴露底層數據的方式,而是由於咱們是在一開始就是錯的。要解決這個問題,咱們必須中止公開類的內部數據,而是提供一種方法來更改底層數據,同時仍然遵循類不變量。下面的代碼解決了這個問題,同時引入了提供的 ArrayReader 的防護性副本,並將 ArrayReader 字段標記爲 final,由於沒有 setter,因此應該是這樣:
譯註:原文的以下代碼有一處錯誤,Reader 類中的 setArrayReaderArray 方法返回值類型應爲 void,該方法是爲了取代 setter,不該產生返回值。
public class ArrayReader {
public static ArrayReader copy(ArrayReader other) {
ArrayReader copy = new ArrayReader();
String[] originalArray = other.getArray();
copy.setArray(Arrays.copyOf(originalArray, originalArray.length));
return copy;
}
// ... Existing class ...
}
public class Reader {
private final ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = ArrayReader.copy(arrayReader);
}
public ArrayReader setArrayReaderArray(String[] array) {
arrayReader.setArray(Objects.requireNonNull(array));
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
reader.read();
Reader flawedReader = new Reader(arrayReader);
flawedReader.setArrayReaderArray(null); // NullPointerException thrown
若是咱們查看這個有缺陷的讀取器,它仍然會拋出 NullPointerException,但在不變量(讀取時使用非空數組)被破壞時,會當即拋出該異常,而不是在稍後的某個時間。這確保了不變量的快速失效,這使得調試和找到問題的根源變得容易得多。
咱們能夠進一步利用這一原則。若是不迫切須要更改類的狀態,那麼讓類的字段徹底不可訪問是一個好主意。例如,咱們能夠刪除全部可以修改 Reader 類實例對象狀態的方法,實現 Reader 類的徹底封裝:
public class Reader {
private final ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = ArrayReader.copy(arrayReader);
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
// No changes can be made to the Reader after instantiation
reader.read();
從邏輯上總結這個概念,若是可能的話,讓類不可變是一個好主意。所以,在實例化對象以後,對象的狀態永遠不會改變。例如,咱們能夠建立一個不可變的 Car 對象以下:
public class Car {
private final String make;
private final String model;
public Car(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
}
須要注意的是,若是類的字段不是基本數據類型,客戶端能夠如前所述那樣修改底層對象。所以,不可變對象應該返回這些對象的防護性副本,不容許客戶端修改不可變對象的內部狀態。可是請注意,防護性複製會下降性能,由於每次調用 getter 時都會建立一個新對象。對於這個缺陷,不該該過早地進行優化(忽視不可變性,以保證可能的性能提升),可是應該注意到這一點。下面的代碼片斷提供了一個方法返回值的防護性複製示例:
public class Transmission {
private String type;
public static Transmission copy(Transmission other) {
Transmission copy = new Transmission();
copy.setType(other.getType);
return copy;
}
public String setType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public class Car {
private final String make;
private final String model;
private final Transmission transmission;
public Car(String make, String model, Transmission transmission) {
this.make = make;
this.model = model;
this.transmission = Transmission.copy(transmission);
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public Transmission getTransmission() {
return Transmission.copy(transmission);
}
}
這給咱們提示瞭如下原則:
使類不可變,除非迫切須要更改類的狀態。不可變類的全部字段都應該標記爲 private 和 final,以確保不會對字段執行從新賦值,也不會對字段的內部狀態提供間接訪問
不變性還帶來了一些很是重要的優勢,例如類可以在多線程上下文中輕鬆使用(即兩個線程能夠共享對象,而不用擔憂一個線程會在另外一個線程訪問該狀態時更改該對象的狀態)。總的來講,在不少實際狀況下咱們能夠建立不可變的類,要比咱們意識到的要多不少,只是咱們習慣了添加了 getter 或 setter。
Conclusion(結論)
咱們建立的許多應用程序最終都能正常工做,可是在大量應用程序中,咱們無心引入的一些問題可能只會在最極端的狀況下出現。在某些狀況下,咱們作事情是出於方便,甚至是出於習慣,而不多注意這些習慣在咱們使用的場景中是否實用(或安全)。在本文中,咱們深刻研究了在實際應用中最多見的三種問題,如:空返回值、函數式編程的魅力、草率的 getter 和 setter,以及一些實用的替代方法。雖然本文中的規則不是絕對的,可是它們確實爲一些在實際應用中遇到的罕見問題提供了看法,並可能有助於在從此避開一些費勁的問題。