參考答案
如果將對象作為Map中的key,需要是實(shí)現(xiàn)該對象的equals方法和hashCode方法;現(xiàn)在一般通過lombok可以簡單得實(shí)現(xiàn),并且可以選擇具體需要哪些字段參與equals和hashCode方法的計(jì)算。
知識點(diǎn)梳理
Java類型系統(tǒng)中分為基礎(chǔ)類型和引用類型,引用類型中所有的對象都有一個父類——java.lang.Object?;怬bject提供了一些可擴(kuò)展的方法:equals、hashCode、toString、clone和finalize。開發(fā)者在覆蓋這些方法的時候,要遵循一定的約定,如果使用不當(dāng)就會造成bug。
equals方法
如果類有自己的“邏輯相等”概念,而且父類的equals方法又無法滿足期望的時候,就應(yīng)該覆蓋equals方法。在開發(fā)中我們有時候會將一個自定義的對象作為map中的key,或者將一個自定義的對象加入到集合中,這時候就需要覆蓋equals方法。
hashCode方法
覆蓋equals方法的時候,要同時覆蓋hashCode方法。這里一起看一個案例。假設(shè)我定義一個用戶信息類,代碼如下所示:
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(exclude = {"phoneNumber", "nickName", "sign", "address", "avatarUrl"})
public class UserInfo {
private long userId;
private String phoneNumber;
private String nickName;
private String sign;
private String address;
private String avatarUrl;
}
這里使用@EqualsAndHashCode 注解生成equals和hashCode方法,并排除了除userId以外的其他字段,表示該用戶信息對象的唯一性只跟userId這個字段有關(guān)。如果該類是繼承了某個自定義的類,需要考慮父類的字段,那么還可以使用@EqualsAndHashCode 中的callSuper字段,設(shè)置為true就會連父類的字段一起考慮,默認(rèn)是只考慮當(dāng)前類中的字段。關(guān)注lombok的用法,這里不展開講了。
假設(shè)有一個場景,需要過濾確保某個列表里的用戶對象是沒有重復(fù)的,那么我們就需要確定用戶對象的唯一id是什么?在這里是userId,可以使用集合來進(jìn)行重復(fù)對象的過濾,代碼如下所示:
import java.util.HashSet;
import java.util.Set;
public class ObjectExample {
public static void main(String[] args) {
UserInfo userInfo1 = new UserInfo();
userInfo1.setAddress("abc");
userInfo1.setAvatarUrl("http://");
userInfo1.setNickName("aaa");
userInfo1.setPhoneNumber("1768909876");
userInfo1.setSign("");
userInfo1.setUserId(289976L);
UserInfo userInfo2 = new UserInfo();
userInfo2.setAddress("abc");
userInfo2.setAvatarUrl("http://");
userInfo2.setNickName("aaa");
userInfo2.setPhoneNumber("1768909876");
userInfo2.setSign("");
userInfo2.setUserId(289978L);
UserInfo userInfo3 = new UserInfo();
userInfo3.setAddress("abcddddd");
userInfo3.setAvatarUrl("http://");
userInfo3.setNickName("aaadddd");
userInfo3.setPhoneNumber("176890999996");
userInfo3.setSign("xxxxx");
userInfo3.setUserId(289978L);
Set<UserInfo> userInfoSet = new HashSet<UserInfo>();
userInfoSet.add(userInfo1);
userInfoSet.add(userInfo2);
userInfoSet.add(userInfo3);
System.out.println(userInfoSet.size());
}
}
這個案例的執(zhí)行結(jié)果也很明顯,輸出的值是:2。
toString方法
所有自定義的類都要覆蓋toString方法,我會使用lombok的@ToString 注解來幫我生成toString方法。使用toString方法可以將對象的字段都以可讀的形式展示出來。這樣在打印日志的時候,要打印某個對象,就不會打印出一個對象的地址,類似于UserInfo@1768b4 。
clone方法
我在開發(fā)中沒有用過這個方法。要完成對象的拷貝,只需要區(qū)分自己是要深拷貝還是淺拷貝。一般我會使用拷貝構(gòu)造器或靜態(tài)工廠方法作為替代方案。
public UserInfo(UserInfo userInfo);
或
public static UserInfo newInstance(UserInfo userInfo)
finalize方法
根據(jù)Java問到,finalize方法被設(shè)計(jì)出來是用于釋放非Java資源,但是由于jvm的運(yùn)行機(jī)制導(dǎo)致有很大可能不會調(diào)用到對象的finalize方法,或者調(diào)用的時機(jī)和順序是不確定的,所以這個方法并沒有達(dá)到其設(shè)計(jì)目標(biāo)。Java9中這個方法已經(jīng)被廢棄了,不過現(xiàn)在很多面試還是會問到這個方法背后的原理,需要理解幾個概念:
- 自定義類的對象,就是我們自定義的類,該類覆蓋了finalize方法
- Finalizer對象,在新建一個覆蓋了finalize方法的類的對象的時候,就會伴生一個Finalizer對象,并將該對象加入到一個雙向列表中
- 雙向列表:ReferenceQueue<Object> queue,F(xiàn)inalizer對象創(chuàng)建出來后,就會被加入到這個雙向列表
- FinalizerThread對象,F(xiàn)inalizer線程是3號線程,它的作用就是不斷從上面哪個隊(duì)列中取Finalizer對象,然后調(diào)用它的runFinalzier方法。
在Java應(yīng)用中,如果對finalize方法使用不合理,有時候會引發(fā)一類問題——Finalizer隊(duì)列過長,導(dǎo)致一些對象的finalze方法調(diào)用延遲,如果程序在這個方法中進(jìn)行了某些對時間敏感的資源的釋放,那么就會有問題。
參考資料
- 《Effective Java中文版(2)》
- finalize方法
- 深入分析Object.finalize方法的實(shí)現(xiàn)原理
- https://www./java-finalize
- https:///questions/5175811/finalize-method-in-java
本號專注于后端技術(shù)、JVM問題排查和優(yōu)化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發(fā)者的工作和成長經(jīng)驗(yàn),期待你能在這里有所收獲。
|