拓展了个新业务枚举类型,资损了
分享是最有效的學(xué)習(xí)方式。
案例背景
翻車了,為了cover線上一個(gè)業(yè)務(wù)場(chǎng)景,小貓新增了一個(gè)新的枚舉類型,盲目自信就沒(méi)有測(cè)試發(fā)生產(chǎn)了,由于是底層服務(wù),上層調(diào)用導(dǎo)致計(jì)算邏輯有誤,造成資損。老板很生氣,后果很嚴(yán)重。
產(chǎn)品提出了一個(gè)新的業(yè)務(wù)場(chǎng)景,新增一種套餐費(fèi)用的計(jì)算方式,由于業(yè)務(wù)比較著急,小貓覺(jué)得功能點(diǎn)比較小,開(kāi)發(fā)完就決定迅速上線。不廢話貼代碼。
public enum BizCodeEnums {
BIZ_CODE0(50),
BIZ_CODE1(100),
BIZ_CODE2(150); //新拓展
private Integer code;
BizCodeEnums(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
}
套餐計(jì)費(fèi)方式是一種枚舉類型,每一種枚舉代表一種套餐方式,因?yàn)樯婕暗牡劫Y金相關(guān)業(yè)務(wù),小貓想要穩(wěn)妥,于是拓展了一個(gè)新的業(yè)務(wù)類型BIZ_CODE2,接下來(lái)只要當(dāng)上層傳入指定的Code的時(shí)候,就可以進(jìn)行計(jì)費(fèi)了。下面為大概的演示代碼,
public class NumCompare {
public static void main(String[] args) {
Integer inputBizCode = 150; //上層業(yè)務(wù)
if(BizCodeEnums.BIZ_CODE0.getCode() == inputBizCode) {
method0();
}else if(BizCodeEnums.BIZ_CODE1.getCode() == inputBizCode) {
method1();
//新拓展業(yè)務(wù)
}else if (BizCodeEnums.BIZ_CODE2.getCode() == inputBizCode) {
method2();
}
}
private static void method0(){
System.out.println("method0 execute");
}
private static void method1(){
System.out.println("method1 execute");
}
private static void method2(){
System.out.println("method2 execute");
}
}
上述可見(jiàn),代碼也沒(méi)有經(jīng)過(guò)什么比較好的設(shè)計(jì),純屬堆業(yè)務(wù)代碼,為了穩(wěn)妥起見(jiàn),小貓就照著以前的老代碼拓展出來(lái)了新的業(yè)務(wù)代碼,見(jiàn)上述備注。也沒(méi)有經(jīng)過(guò)仔細(xì)的測(cè)試,然后欣然上線了。事后發(fā)現(xiàn)壓根他新的業(yè)務(wù)代碼就沒(méi)有生效,走的套餐計(jì)算邏輯還是默認(rèn)的套餐計(jì)算邏輯。
容咱們盤(pán)一下這個(gè)技術(shù)細(xì)節(jié),這可能也是很多初中級(jí)開(kāi)發(fā)遇到的坑。
復(fù)盤(pán)分析
接下來(lái),我們就來(lái)好好盤(pán)盤(pán)里面涉及的技術(shù)細(xì)節(jié)。其實(shí)造成這個(gè)事故的原因底層涉及兩種原因,
- 開(kāi)發(fā)人員并沒(méi)有對(duì)Integer底層的原理吃透
- 開(kāi)發(fā)人員對(duì)值比較以及地址比較沒(méi)有掌握好
Intger底層分析
從上述代碼中,我們先看一下發(fā)生了什么。
當(dāng)Integer變量inputBizCode被賦值的時(shí)候,其實(shí)java默認(rèn)會(huì)調(diào)用Integer.valueOf()方法進(jìn)行裝箱操作。
Integer inputBizCode = 100
裝箱變成
Integer inputBizCode = Integer.valueOf(100)
接下來(lái)我們來(lái)扒一下Integer的源碼看一下實(shí)現(xiàn)。源代碼如下
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
我們點(diǎn)開(kāi) IntegerCache.low 以及IntegerCache.high的時(shí)候就會(huì)發(fā)現(xiàn)其中對(duì)應(yīng)著兩個(gè)值,分別是最小值為-128 最大的值為127,那么如此看來(lái),如果目標(biāo)值在-128~127之間的時(shí)候,那么直接會(huì)從cache數(shù)組中取值,否則就會(huì)新建對(duì)象。
我們?cè)倏匆幌翴ntegerCache中的cache是怎么被緩存進(jìn)去的。
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
...此處省略無(wú)關(guān)代碼
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
}
上述其實(shí)我們不難發(fā)現(xiàn),原來(lái)IntegerCache是Integer這個(gè)類的靜態(tài)內(nèi)部類,里面的數(shù)組進(jìn)行初始化的時(shí)候其實(shí)就是在Integer進(jìn)行初始化進(jìn)行類加載的時(shí)候就被緩存進(jìn)去了,被static修飾的屬性會(huì)存儲(chǔ)到我們的棧內(nèi)存中。在上面枚舉BizCodeEnums.BIZ_CODE1.getCode()也是Integer類型,說(shuō)白了當(dāng)值在-127~128之間的時(shí)候,jvm拿到的其實(shí)是同一個(gè)地址的值。所以兩個(gè)值當(dāng)前相等。
當(dāng)然我們從上面的源碼中其實(shí)不難發(fā)現(xiàn)其實(shí)最大值128并不是一成不變的,也可以通過(guò)自定義設(shè)置變成其他范圍,具體的應(yīng)該是上述的這個(gè)配置:
java.lang.Integer.IntegerCache.high
本人自己親測(cè)設(shè)置了一下,如下圖,是生效了的。
那么Integer為什么是-127~128進(jìn)行緩存了呢?翻了一下Java API中,大概是這么解釋的:
Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range.
上述大概意思就是-128~127數(shù)據(jù)在int范圍內(nèi)使用最頻繁,為了減少頻繁創(chuàng)建對(duì)象帶來(lái)的內(nèi)存消耗,這里其實(shí)采用了以空間換時(shí)間的涉及理念,也就是設(shè)計(jì)模式中的享元模式。
其實(shí)在JDK中享元模式的應(yīng)用不僅僅只是局限于Integer,其實(shí)很多其他基礎(chǔ)類型的包裝類也有使用,咱們來(lái)看一下比較:
此處其實(shí)也是面試中的一個(gè)高頻考點(diǎn),需要大家注意,另外的話關(guān)于享元模式此處不展開(kāi)討論,后續(xù)老貓會(huì)穿插到設(shè)計(jì)模式中和大家一起學(xué)習(xí)使用。
值比較以及對(duì)象比較
我們?cè)賮?lái)看一下兩種比較方式。
“==”比較
- 基本數(shù)據(jù)類型:byte,short,char,int,long,double,float,blooean,它們之間的比較,比較是它們的值;
- 引用數(shù)據(jù)類型:使用==比較的時(shí)候,比較的則是它們?cè)趦?nèi)存中的地址(heap上的地址)。
業(yè)務(wù)代碼中賦值為150的時(shí)候,底層代碼重新new出來(lái)一個(gè)新的Integer對(duì)象,那么此時(shí)new出來(lái)的那個(gè)對(duì)象的值在棧內(nèi)存中其實(shí)是新分配的一塊地址,和之前的緩存中的地址完全不同。兩分值進(jìn)行等號(hào)比較的時(shí)候當(dāng)然不會(huì)相等,所以也就不會(huì)走到method2方法塊中。
“equals”比較
equals方法本質(zhì)其實(shí)是屬于Object方法:
public boolean equals(Object obj) {
return (this == obj);
}
但是從上面這段代碼中我們可以明顯地看到 默認(rèn)的Object對(duì)象的equals方法其實(shí)和“==”是一樣的,比較的都是引用地址是否一致。
我們測(cè)試一下將上述的==變成equals的時(shí)候,其實(shí)代碼就沒(méi)有什么問(wèn)題了
if (BizCodeEnums.BIZ_CODE2.getCode() == inputBizCode)
改成
if (BizCodeEnums.BIZ_CODE2.getCode().equals(inputBizCode))
那么這個(gè)又是為什么呢?其實(shí)在一般情況下對(duì)象在集成Object對(duì)象的時(shí)候都會(huì)去重寫(xiě)equals方法,Integer類型中的equals也不例外。我們來(lái)看一下重寫(xiě)后的代碼:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
上述我們看到如果使用Integer中的equals進(jìn)行比較的時(shí)候,最終比較的是基本類型值,就上述代碼比較的其實(shí)就是150==150?那么這種情況下,返回的就自然是true了,那么所以對(duì)應(yīng)的mthod也會(huì)執(zhí)行到了。
“hashCode”
既然已經(jīng)聊到equals重寫(xiě)了,那么我們不得不再聊一下hashCode重寫(xiě)。可能經(jīng)常會(huì)有面試官這么問(wèn)“為什么重寫(xiě) equals方法時(shí)一定要重寫(xiě)hashCode方法?”。
其實(shí)重寫(xiě)equals方法時(shí)一定要重寫(xiě)hashCode方法的原因是為了保證對(duì)象在使用散列集合(如HashMap、HashSet等)時(shí)能夠正確地進(jìn)行存儲(chǔ)和查找。
在Java中,hashCode方法用于計(jì)算對(duì)象的哈希碼,而equals方法用于判斷兩個(gè)對(duì)象是否相等。在散列集合中,對(duì)象的哈希碼被用作索引,通過(guò)哈希碼可以快速定位到存儲(chǔ)的位置,然后再通過(guò)equals方法判斷是否是相同的對(duì)象。
我們知道HashMap中的key是不能重復(fù)的,如果重復(fù)添加,后添加的會(huì)覆蓋前面的內(nèi)容。那么我們看看HashMap是如何來(lái)確定key的唯一性的(估計(jì)會(huì)有小伙伴對(duì)底層HashMap的完整實(shí)現(xiàn)感興趣,另外也是面試的高頻題,不過(guò)在此我們不展開(kāi),老貓后續(xù)盡量在其他文章中展開(kāi)分析)。老貓的JDK版本是java17,我們一起看下源碼
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
查看代碼發(fā)現(xiàn),它是通過(guò)計(jì)算Map key的hashCode值來(lái)確定在鏈表中的存儲(chǔ)位置的。那么這樣就可以推測(cè)出,如果我們重寫(xiě)了equals但是沒(méi)重寫(xiě)hashCode,那么可能存在元素重復(fù)的矛盾情況。
咱們舉個(gè)例子簡(jiǎn)單實(shí)驗(yàn)一下:
public class Person {
private Integer age;
private String name;
public Person(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(age, person.age) && Objects.equals(name, person.name);
}
// @Override
// public int hashCode() {
// return Objects.hash(age, name);
// }
}
public class TestPerson {
public static void main(String[] args) {
Person p1 = new Person(18,"ktdaddy");
Person p2 = new Person(18,"ktdaddy");
HashMap<Person,Object> map = new HashMap<>();
map.put(p1, "1");
System.out.println("equals:" + p1.equals(p2));
System.out.println(map.get(p2));
}
}
上述的結(jié)果輸出為
equals:true
null
由于沒(méi)有重寫(xiě)hashCode方法,p1和p2的hashCode方法返回的哈希碼不同,導(dǎo)致它們?cè)贖ashMap中被當(dāng)作不同的鍵,因此無(wú)法正確地獲取到值。如果重寫(xiě)了hashCode方法,使得相等的對(duì)象返回相同的哈希碼,就可以正確地進(jìn)行存儲(chǔ)和查找操作。
案例總結(jié)
其實(shí)當(dāng)我們?cè)谌粘>S護(hù)的代碼的時(shí)候要勇于去質(zhì)疑現(xiàn)有代碼體系,如果發(fā)現(xiàn)不合理的地方,隱藏的坑點(diǎn),咱們還是需要立刻將其填好,以免發(fā)生類似小貓遇到的這種情況。
另外的話,寫(xiě)代碼還是不能停留于會(huì)寫(xiě),必要的時(shí)候還是得翻看底層的源碼實(shí)現(xiàn)。只有這樣才能知其所以然,未來(lái)也才能夠更好地用好大神封裝的一些代碼。或者可以自主封裝一些好用的工具給他人使用。
派生面試題
上面的案例中涉及到的知識(shí)點(diǎn)可能會(huì)牽扯到這樣的面試題。
問(wèn)題1: 如何自定義一個(gè)類的equals方法?
答案: 要自定義一個(gè)類的equals方法,可以按照以下步驟進(jìn)行:
- 在類中創(chuàng)建一個(gè)equals方法的覆蓋(override)。
- 確保方法簽名為public boolean equals(Object obj),并且參數(shù)類型是Object。
- 在equals方法中,首先使用==運(yùn)算符比較對(duì)象的引用,如果引用相同,返回true。
- 如果引用不同,檢查傳遞給方法的對(duì)象是否屬于相同的類。
- 如果屬于相同的類,將傳遞的對(duì)象強(qiáng)制轉(zhuǎn)換為相同類型,然后比較對(duì)象的字段,以確定它們是否相等。
- 最后,返回比較結(jié)果,通常是true或false。
問(wèn)題2:equals 和 hashCode 之間有什么關(guān)系?
答案:
equals 和 hashCode 在Java中通常一起使用,以維護(hù)對(duì)象在散列集合(如HashMap和HashSet)中的正確行為。
如果兩個(gè)對(duì)象相等(根據(jù)equals方法的定義),那么它們的hashCode值應(yīng)該相同。
也就是說(shuō),如果重寫(xiě)了一個(gè)類的equals方法,通常也需要重寫(xiě)hashCode方法,以便它們保持一致。
這是因?yàn)樯⒘屑鲜褂脤?duì)象的hashCode值來(lái)確定它們?cè)趦?nèi)部存儲(chǔ)結(jié)構(gòu)中的位置。
問(wèn)題3:== 在哪些情況下比較的是對(duì)象內(nèi)容而不是引用?
答案:
在Java中,== 運(yùn)算符通常比較的是對(duì)象的引用。但在以下情況下,== 可以比較對(duì)象的內(nèi)容而不是引用:
對(duì)于基本數(shù)據(jù)類型(如int、char等),== 比較的是它們的值,而不是引用。
字符串常量池:對(duì)于字符串字面值,Java使用常量池來(lái)存儲(chǔ)它們,因此相同的字符串字面值使用==比較通常會(huì)返回true。
我是老貓,10Year+資深研發(fā)老鳥(niǎo),讓我們一起聊聊技術(shù),聊聊人生。
個(gè)人公眾號(hào),“程序員老貓”
總結(jié)
以上是生活随笔為你收集整理的拓展了个新业务枚举类型,资损了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: spss 变量标签和变量值标签有什么区别
- 下一篇: 2015qq等级隐藏最新方法