天天看點

是時候優雅地和NullPointException說再見了

作者:架構師成長曆程

NullPointException應該算是每一個碼農都很熟悉的家夥了吧?誰的代碼不曾抛過幾個空指針異常呢...

比如:你寫了段如下的代碼:

public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    Company company = employee.getTeam().getDepartment().getCompany();
    System.out.println(company);
}

private Employee getEmployee() {
    Employee employee = new Employee();
    employee.setEmployeeName("JiaGouWuDao");
    employee.setTeam(new Team("DevTeam4"));
    return employee;
}
複制代碼           

運作程式,你可能就等不到你需要的結果,而是要喜提NullPointException了...

作為JAVA開發中最典型的異常類型,甚至可能是很多程式員入行之後收到的第一份異常大禮包類型,NullPointException也似乎成為了一種魔咒,迫使程式員在敲出的每一行代碼的時候都需要去思考下是否需要去做一下判空操作,久而久之,代碼中便充斥着大量的null檢查邏輯。

于是呢,上面的代碼會變成下面這樣:

public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    if (employee == null) {
        // do something here...
        return;
    }
    Team team = employee.getTeam();
    if (team == null) {
        // do something here...
        return;
    }
    Department department = team.getDepartment();
    if (department == null) {
        // do something here...
        return;
    }
    Company company = department.getCompany();
    System.out.println(company);
}
複制代碼           

是不是大家的項目中都有見過這種寫法的?每行代碼中都流露着對NullPointException的恐懼有木有?是不是像極了一顆被深深傷害過的心在小心翼翼的保護着自己?

是時候優雅地和NullPointException說再見了

null的困擾

通過上面代碼示例,我們可以發現使用null可能會帶來的一系列困擾:

  • 空指針異常,導緻代碼運作時變得不可靠,稍不留神可能就崩了
  • 使代碼膨脹,導緻代碼中充斥大量的null檢查與保護,使代碼可讀性降低
是時候優雅地和NullPointException說再見了

此外,null還有一個明顯的弊端:

  • 含義不明确,比如一個方法傳回了null,調用方不清楚到底是因為邏輯有問題導緻為null,還是說null其實也是一種可以接受的正常傳回值類型?

是以說,一個比較好的編碼習慣,是盡量避免在程式中使用null,可以按照具體的場景分開差別對待:

是時候優雅地和NullPointException說再見了
  • 确定是因為代碼或者邏輯層面處理錯誤導緻的無值,通過throw異常的方式,強制調用方感覺并進行處理對待
  • 如果null代表業務上的一種正常可選值,可以考慮傳回Optional來替代。

當然咯,有時候即使我們自己的代碼不傳回null,也難免會遇到調用别人的接口傳回null的情況,這種時候我們真的就隻能不停的去判空來保護自己嗎?有沒有更優雅的應對政策來避免自己掉坑呢?下面呢,我們一起探讨下null的一些優雅應對政策。

是時候優雅地和NullPointException說再見了

Optional應對null處理

Optional一定比return null安全嗎

前面我們提到了說使用Optional來替代null,減少調用端的判空操作壓力,防止調用端出現空指針異常。

那麼,使用傳回Optional對象就一定會比return null更靠譜嗎?

答案是:也不一定,關鍵要看怎麼用!

比如:下面的代碼,getContent()方法傳回了個Optional對象,然後testCallOptional()方法作為調用方,擷取到傳回值後的操作方式:

public void testCallOptional() {
    Optional<Content> optional = getContent();
    System.out.println("-------下面代碼會報異常--------");
    try {
        // 【錯誤用法】直接從Optional對象中get()實際參數,這種效果與傳回null對象然後直接調用是一樣的效果
        Content content = optional.get();
        System.out.println(content);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("-------上面代碼會報異常--------");
}

private Optional<Content> getContent() {
    return Optional.ofNullable(null);
}
複制代碼           

上述代碼運作之後會發現報錯了:

-------下面代碼會報異常--------
java.util.NoSuchElementException: No value present
	at java.util.Optional.get(Optional.java:135)
	at com.veezean.skills.optional.OptionalService.testCallOptional(OptionalService.java:47)
	at com.veezean.skills.optional.OptionalService.main(OptionalService.java:58)
-------上面代碼會報異常--------
複制代碼           

既然直接調用Optional.get()報錯,那就是調用前加個判斷就好咯?

public void testCallOptional2() {
    Optional<Content> optional = getContent();
    // 使用前先判斷下元素是否存在
    if (optional.isPresent()) {
        Content content = optional.get();
        System.out.println(content);
    }
}
複制代碼           

執行一下,果然不報錯了。但是,這樣真的就是解決方法嗎?這樣跟直接傳回null然後使用前判空(下面的寫法)其實也沒啥差別,也并不會讓調用方使用起來更加的優雅與靠譜:

public void testNullReturn2() {
    Content content = getContent2();
    if (content != null) {
        System.out.println(content.getValue());
    }
}
複制代碼           

那怎麼樣才是正确的使用方式呢,下面一起來看下。

是時候優雅地和NullPointException說再見了

全面認識下Optional

建立Optional對象

Optional<T>對象,可以用來表示一個T類型對象的封裝,或者也可以表示不是任何對象。Optional類提供了幾個靜态方法供對象的建構:

方法名 功能含義描述
empty() 構造一個無任何實際對象值的空Optional對象(可以了解為業務層面的null)
of(T t) 根據給定的對象,構造一個此對象的封裝Optional對象,注意入參t不能為null,否則會空指針
ofNullable(T t) 根據傳入的入參t的值構造Optional封裝對象,如果傳入的t為null,則等同于調用empty()方法,如果t不為null,則等同于調用of(T t)方法

在項目中,我們可以選擇使用上面的方法,實作Optional對象的封裝:

public void testCreateOptional() {
    // 使用Optional.of構造出具體對象的封裝Optional對象
    System.out.println(Optional.of(new Content("111","JiaGouWuDao")));
    // 使用Optional.empty構造一個不代表任何對象的空Optional值
    System.out.println(Optional.empty());
    System.out.println(Optional.ofNullable(null));
    System.out.println(Optional.ofNullable(new Content("222","JiaGouWuDao22")));
}
複制代碼           

輸出結果:

Optional[Content{id='111', value='JiaGouWuDao'}]
Optional.empty
Optional.empty
Optional[Content{id='222', value='JiaGouWuDao22'}]
複制代碼           

這裡需要注意下of方法如果傳入null會抛空指針異常,是以比較建議大家使用ofNullable方法,可以省去調用前的額外判空操作,也可以避免無意中觸發空指針問題:

是時候優雅地和NullPointException說再見了
是時候優雅地和NullPointException說再見了

Optional常用方法了解

在具體讨論應該如何正确使用Optional的方法前,先來了解下Optional提供的一些方法:

方法名 含義說明
isPresent 如果Optional實際有具體對象值,則傳回true,否則傳回false。
ifPresent 這是一個函數式程式設計風格的API接口,入參是一個函數,即如果Optional對象有實際對象值,則會執行傳入的入參函數邏輯,如果不存在實際對象值,則不會執行傳入的入參函數邏輯。
get 傳回Optional封裝的實際對象T資料,注意,如果實際對象資料不存在,會抛異常而非傳回null
orElse 與get方法類似,都是擷取Optional實際的對象值,差別在于orElse必須傳入一個預設值,當Optional沒有實際值的時候傳回預設值而非抛異常
orElseGet 可以了解為orElse方法的更新版,差別在于orElse僅允許傳入一個固定的預設值,而orElseGet的入參是一個函數方法,當Optional無實際值時,會執行給定的入參函數,傳回動态值。
orElseThrow 與orElse類似,差別在于如果沒有擷取到,會抛出一個指定的異常。
filter 判定目前Optional的實際對象是否符合入參函數的過濾規則,如果符合則傳回目前Optional對象,如果不符合則傳回空Optional
map 接收一個入參函數,允許将Optional中的實際對象值處理轉換為另一實際對象值(這個入參函數的傳回值為T),并生成傳回此新類型的Optional對象,如果生成的新對象為null,則傳回一個空Optional對象
flatMap 與map類似,差別點在于入參函數的傳回值類型有差別(此處入參函數的傳回值為Optional<T>)

看到這裡的map與flatMap方法,不知道大家會不會聯想到Stream流對象操作的時候也有這兩個方法的身影呢(不了解的同學可以戳這個連結抓緊補補課:吃透JAVA的Stream流操作)?的确,它們的作用也是類似的,都是用來将一個對象處理轉換為另一個對象類型的:

是時候優雅地和NullPointException說再見了

對于Optional而言,map與flatMap最終的實作效果其實都是一樣的,僅僅隻是入參的要求不一樣,也即兩種不同寫法,兩者差別點可以通過下圖來了解:

是時候優雅地和NullPointException說再見了

實際使用的時候,可以根據需要選擇使用map或者flatMap:

public void testMapAndFlatMap() {
    Optional<User> userOptional = getUser();
    Optional<Employee> employeeOptional = userOptional.map(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map與flatMap的差別點:此處return的是具體對象類型
        return employee;
    });
    System.out.println(employeeOptional);

    Optional<Employee> employeeOptional2 = userOptional.flatMap(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map與flatMap的差別點:此處return的是具體對象的Optional封裝類型
        return Optional.of(employee);
    });
    System.out.println(employeeOptional2);
}
複制代碼           

從輸出結果可以看出,兩種不同的寫法,實作是相同的效果:

Optional[Employee(employeeName=JiaGouWuDao)]
Optional[Employee(employeeName=JiaGouWuDao)]
複制代碼           
是時候優雅地和NullPointException說再見了

Optional使用場景

減少繁瑣的判空操作

再回到本篇文章最開始的那段代碼例子,如果我們代碼裡面不去逐個做判空保護的話,我們可以如何來實作呢?看下面的實作思路:

public void getCompanyFromEmployeeTest() {
    Employee employeeDetail = getEmployee();
    String companyName = Optional.ofNullable(employeeDetail)
            .map(employee -> employee.getTeam())
            .map(team -> team.getDepartment())
            .map(department -> department.getCompany())
            .map(company -> company.getCompanyName())
            .orElse("No Company");
    System.out.println(companyName);
}
複制代碼           

先通過map的方式一層一層的去進行類型轉換,最後使用orElse去擷取Optional中最終處理後的值,并給定了資料缺失場景的預設值。是不是看着比一堆if判空操作要舒服多了?

适用場景: 需要通過某個比較長的調用鍊路一層一層去調用擷取某個值的時候,使用上述方法,可以避免空指針以及減少冗長的判斷邏輯。
是時候優雅地和NullPointException說再見了

需要有值兜底的資料擷取場景

編碼的時候,經常會遇到一些資料擷取的場景,需要先通過一些處理邏輯嘗試擷取一個資料,如果沒有擷取到需要的資料,還需要傳回一個預設值,或者是執行另一處理邏輯繼續嘗試擷取。

比如從請求頭中擷取用戶端IP的邏輯,按照正常邏輯,代碼會寫成下面這樣:

public String getClientIp(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    if (!StringUtils.isEmpty(clientIp)) {
        return clientIp;
    }
    clientIp = request.getHeader("X-Real-IP");
    return clientIp;
}
複制代碼           

但是借助Optional來實作,可以這樣寫:

public String getClientIp2(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    return Optional.ofNullable(clientIp).orElseGet(() -> request.getHeader("X-Real-IP"));
}
複制代碼           
适用場景: 優先執行某個操作嘗試擷取資料,如果沒擷取到則去執行另一邏輯擷取,或者傳回預設值的場景。
是時候優雅地和NullPointException說再見了

替代可能為null的方法傳回值

下面是一段從項目代碼中截取的片段:

public FileInfo queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return new FileInfo(entity.getName(), entity.getFilePath(), false);
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true);
    }
    return null;
}
複制代碼           

可以看到最終的return分支中,有一種可能會傳回null,這個方法作為項目中被高頻調用的一個方法,意味着所有的調用端都必須要做判空保護。可以使用Optional進行優化處理:

public Optional<FileInfo> queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return Optional.ofNullable(new FileInfo(entity.getName(), entity.getFilePath(), false));
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return Optional.ofNullable(new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true));
    }
    return Optional.empty();
}
複制代碼           

這樣的話,就可以有效的防止調用端踩雷啦~

适用場景: 實作某個方法的時候,如果方法的傳回值可能會為null,則考慮将方法的傳回值改為Optional類型,原先傳回null的場景,使用Optional.empty()替代。
是時候優雅地和NullPointException說再見了

包裝資料實體中非必須字段

首先明确一下,Optional的意思是可選的,也即用于辨別下某個屬性可有可無的特性。啥叫可有可無?看下面代碼:

public class PostDetail {
    private String title;
    private User postUser;
    private String content;
    private Optional<Date> lastModifyTime = Optional.empty();
    private Optional<Attachment> attachment = Optional.empty();
}
複制代碼           

上面是一個文章詳情資料類,對于一個論壇文章資料而言,文章的标題、内容、發帖人這些都是屬于必須的字段,而文章的修改時間、文章的附件其實是屬于可選字段(因為不是所有的文章都會被修改、也不是所有文章都會帶附件),是以針對這種可有可無的字段,就可以聲明定義的時候使用Optional進行封裝。

是時候優雅地和NullPointException說再見了

使用Optional進行封裝之後有兩個明顯的優勢:

  • 強烈的業務屬性說明,明确的讓人知曉這個是一個可選字段,等同于資料庫建表語句裡面設定nullable辨別一樣的效果;
  • 調用端使用的時候也省去了判空操作。
适用場景: 資料實體定義的時候,對于可選參數,采用Optional封裝類型替代。

使用抛異常替代return null

相比于傳回一個Optional封裝的對象,直接抛異常具有強烈的警醒意味,意在表達此處存在預期之外的不合理情況,要求編碼的時候,調用端必須要予以專門處理。

public Team getTeamInfo() throws TestException {
    Employee employee = getEmployee();
    Team team = employee.getTeam();
    if (team == null) {
        throw new TestException("team is missing");
    }
    return team;
}
複制代碼           

相比直接return null,顯然抛異常的含義更加明确。

是時候優雅地和NullPointException說再見了

JDK與開源架構的實踐

JDK提供的很多方法裡面,其實都是遵循着本文中描述的這種傳回值處理思路的,很少會看到直接傳回null的——不止JDK,很多大型的開源架構源碼中,也很少會看到直接return null的情況。

比如com.sun.jmx.snmp.agent中的一段代碼:

public SnmpMibSubRequest nextElement() throws NoSuchElementException  {
    if (iter == 0) {
        if (handler.sublist != null) {
            iter++;
            return hlist.getSubRequest(handler);
        }
    }
    iter ++;
    if (iter > size) throw new NoSuchElementException();
    SnmpMibSubRequest result = hlist.getSubRequest(handler,entry);
    entry++;
    return result;
}
複制代碼           

再比如Spring中org.springframework.data.jpa.repository.support包下面的方法例子:

public Optional<T> findById(ID id) {
	Assert.notNull(id, ID_MUST_NOT_BE_NULL);
	Class<T> domainType = getDomainClass();
	if (metadata == null) {
		return Optional.ofNullable(em.find(domainType, id));
	}
	LockModeType type = metadata.getLockModeType();
	Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();
	return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}
複制代碼           
是時候優雅地和NullPointException說再見了

java 避免出現NullPointerException(空指針)的方法總結

Java應用中抛出的空指針異常是解決空指針的最好方式,也是寫出能順利工作的健壯程式的關鍵。俗話說“預防勝于治療”,對于這麼令人讨厭的空指針異常,這句話也是成立的。值得慶幸的是運用一些防禦性的編碼技巧,跟蹤應用中多個部分之間的聯系,你可以将Java中的空指針異常控制在一個很好的水準上。順便說一句,這是Javarevisited上的第二個空指針異常的文章。在上個文章中我們讨論了Java中導緻空指針異常的常見原因,而在本教程中我們将會學習一些Java的程式設計技巧和最佳實踐。這些技巧可以幫助你避免Java中的空指針異常。遵從這些技巧同樣可以減少Java代碼中到處都有的非空檢查的數量。作為一個有經驗的Java程式員,你可能已經知道其中的一部分技巧并且應用在你的項目中。但對于新手和中級開發人員來說,這将是很值得學習的。順便說一句,如果你知道其它的避免空指針異常和減少空指針檢查的Java技巧,請和我們分享。

這些都是簡單的技巧,很容易應用,但是對代碼品質和健壯性有顯著影響。根據我的經驗,隻有第一個技巧可以顯著改善代碼品質。如我之前所講,如果你知道任何避免空指針異常和減少空指針檢查的Java技巧,你可以通過評論本文來和分享。

1) 從已知的String對象中調用equals()和equalsIgnoreCase()方法,而非未知對象。

總是從已知的非空String對象中調用equals()方法。因為equals()方法是對稱的,調用a.equals(b)和調用b.equals(a)是完全相同的,這也是為什麼程式員對于對象a和b這麼不上心。如果調用者是空指針,這種調用可能導緻一個空指針異常

Object unknownObject = null;

//錯誤方式 ? 可能導緻 NullPointerException
if(unknownObject.equals("knownObject")){
  System.err.println("This may result in NullPointerException if unknownObject is null");
}

//正确方式 - 即便 unknownObject是null也能避免NullPointerException
if("knownObject".equals(unknownObject)){
  System.err.println("better coding avoided NullPointerException");
}

           

這是避免空指針異常最簡單的Java技巧,但能夠導緻巨大的改進,因為equals()是一個常見方法。

2) 當valueOf()和toString()傳回相同的結果時,甯願使用前者。

因為調用null對象的toString()會抛出空指針異常,如果我們能夠使用valueOf()獲得相同的值,那甯願使用valueOf(),傳遞一個null給valueOf()将會傳回“null”,尤其是在那些包裝類,像Integer、Float、Double和BigDecimal。

BigDecimal bd = getPrice();
System.out.println(String.valueOf(bd)); //不會抛出空指針異常
System.out.println(bd.toString()); //抛出 "Exception in thread "main" java.lang.NullPointerException"
           

3) 使用null安全的方法和庫 有很多開源庫已經為您做了繁重的空指針檢查工作。其中最常用的一個的是Apache commons 中的StringUtils。你可以使用StringUtils.isBlank(),isNumeric(),isWhiteSpace()以及其他的工具方法而不用擔心空指針異常。

//StringUtils方法是空指針安全的,他們不會抛出空指針異常
System.out.println(StringUtils.isEmpty(null));
System.out.println(StringUtils.isBlank(null));
System.out.println(StringUtils.isNumeric(null));
System.out.println(StringUtils.isAllUpperCase(null));

Output:
true
true
false
false

           

但是在做出結論之前,不要忘記閱讀空指針方法的類的文檔。這是另一個不需要下大功夫就能得到很大改進的Java最佳實踐。

4) 避免從方法中傳回空指針,而是傳回空collection或者空數組。

這個Java最佳實踐或技巧由Joshua Bloch在他的書Effective Java中提到。這是另外一個可以更好的使用Java程式設計的技巧。通過傳回一個空collection或者空數組,你可以確定在調用如size(),length()的時候不會因為空指針異常崩潰。Collections類提供了友善的空List,Set和Map: Collections.EMPTY_LIST,Collections.EMPTY_SET,Collections.EMPTY_MAP。這裡是執行個體。

public List getOrders(Customer customer){
  List result = Collections.EMPTY_LIST;
  return result;
}
           

你同樣可以使用Collections.EMPTY_SET和Collections.EMPTY_MAP來代替空指針。

5) 使用annotation@NotNull 和 @Nullable

在寫程式的時候你可以定義是否可為空指針。通過使用像@NotNull和@Nullable之類的annotation來聲明一個方法是否是空指針安全的。現代的編譯器、IDE或者工具可以讀此annotation并幫你添加忘記的空指針檢查,或者向你提示出不必要的亂七八糟的空指針檢查。IntelliJ和findbugs已經支援了這些annotation。這些annotation同樣是JSR 305的一部分,但即便IDE或工具中沒有,這個annotation本身可以作為文檔。看到@NotNull和@Nullable,程式員自己可以決定是否做空指針檢查。順便說一句,這個技巧對Java程式員來說相對比較新,要采用需要一段時間。

6) 避免你的代碼中不必要的自動包裝和自動解包。

且不管其他如建立臨時對象的缺點,如果wrapper類對象是null,自動包裝同樣容易導緻空指針異常。例如如果person對象沒有電話号碼的話會傳回null,如下代碼會因為空指針異常崩潰。

Person ram = new Person("ram");
int phone = ram.getPhone();
           

當使用自動包裝和自動解包的時候,不僅僅是等号,< > 同樣會抛出空指針異常。你可以通過這篇文章來學習更多的Java中的自動包裝和拆包的陷阱。

7) 遵從Contract并定義合理的預設值。

在Java中避免空指針異常的一個最好的方法是簡單的定義contract并遵從它們。大部分空指針異常的出現是因為使用不完整的資訊建立對象或者未提供所有的依賴項。如果你不允許建立不完整的對象并優雅地拒絕這些請求,你可以在接下來的工作者預防大量的空指針異常。類似的,如果對象允許建立,你需要給他們定義一個合理的預設值。例如一個Employee對象不能在建立的時候沒有id和name,但是是否有電話号碼是可選的。現在如果Employee沒有電話号碼,你可以傳回一個預設值(例如0)來代替傳回null。但是必須謹慎選擇,喲有時候檢查空指針比調用無效号碼要友善。同樣的,通過定義什麼可以是null,什麼不能為null,調用者可以作出明智的決定。failing fast或接受null同樣是一個你需要進行選擇并貫徹的,重要的設計決策

8)定義資料庫中的字段是否可為空。

如果你在使用資料庫來儲存你的域名對象,如Customers,Orders 等,你需要在資料庫本身定義是否為空的限制。因為資料庫會從很多代碼中擷取資料,資料庫中有是否為空的檢查可以確定你的資料健全。在資料空中維護null限制同樣可以幫助你減少Java代碼中的空指針檢查。當從資料庫中加載一個對象是你會明确,哪些字段是可以為null的,而哪些不能,這可以使你代碼中不必要的!= null檢查最少化。

9) 使用空對象模式(Null Object Pattern)

還有一種方法來避免Java中的空指針異常。如果一個方法傳回對象,在調用者中執行一些操作,例如Collection.iterator()方法傳回疊代器,其調用者執行周遊。假設如果一個調用者并沒有任何疊代器,其可以傳回空對象(Null object)而非null。空對象是一個特殊的對象,其在不同的上下文中有不同的意義。例如一個空的疊代器調用hasNext()傳回false時,可以是一個空對象。同樣的在傳回Container和Collection類型方法的例子中,空對象可以被用來代替null作為傳回值。我打算另寫一篇文章來講空對象模式,分享幾個Java空對象的例子。

這就是全部了,這是幾個易于遵從的避免空指針異常的Java技巧和最佳實踐。你可以欣賞到這些技巧将非常有用,且不太難實作。如果你有其他比秒這個異常的技巧,而又沒包含在這裡,請通過評論來和我們分享,我将收錄在這裡。

如有疑問請留言或者到本站社群交流讨論,感謝閱讀,希望能幫助到大家,謝謝大家對本站的支援!

總結

好啦,關于編碼中對null的一些應對處理政策與思路呢,這裡就給大家分享到這裡,希望可以對大家有所啟發,通過不斷的細節優化與改進,最終擺脫被空指針擺布的局面~

那麼,對上面提到的一些内容與場景,你是否也有遇到相關的情況呢?你是怎麼處理的呢?歡迎多切磋交流下~

繼續閱讀