原創(chuàng):微信公眾號(hào) 碼農(nóng)參上,歡迎分享,轉(zhuǎn)載請(qǐng)保留出處。
哈嘍大家好啊,我是沒(méi)更新就是在家忙著帶娃的Hydra。
前幾天,正巧趕上組里代碼review,一下午下來(lái),感覺(jué)整個(gè)人都血壓拉滿了。五花八門(mén)的代碼讓我不禁感嘆,代碼規(guī)范這條道路還是任重而道遠(yuǎn)…
那么今天就來(lái)給大家總結(jié)一波Java中的代碼作死小技巧,熟練掌握這些小技巧后,保證能讓你寫(xiě)出同事看不懂的代碼~
至于為啥要寫(xiě)出同事看不懂的代碼,通過(guò)這次教訓(xùn),我發(fā)現(xiàn)好處還是挺多的,簡(jiǎn)單舉幾個(gè)例子:
- 同事無(wú)法輕易修改你的代碼,避免團(tuán)隊(duì)協(xié)作不當(dāng)引入bug
- 塑造個(gè)人能力的不可替代性,規(guī)避被辭退的風(fēng)險(xiǎn)
- 代碼review時(shí),幫助同事治療好多年的低血壓
好了,一本正經(jīng)的胡說(shuō)八道環(huán)節(jié)就此打住……廢話不多說(shuō)了,下面正式開(kāi)始。沒(méi)用的知識(shí)又要增加了…
壹、瞞天過(guò)海
我打賭你肯定想不到,有人居然會(huì)在注釋里下了毒??纯聪旅娴拇a,簡(jiǎn)單到main方法中只有一行注釋。
public static void main(String[] args) { // System.out.println(“coder Hydra”);}
猜猜看,這段程序運(yùn)行結(jié)果如何?執(zhí)行后它居然會(huì)在控制臺(tái)打?。?/p>
coder Hydra
看到這你是不是一臉懵逼,為什么注釋中的代碼會(huì)被執(zhí)行?
其實(shí)原理就在于大家熟悉的unicode編碼,上面的就是一個(gè)unicode轉(zhuǎn)義字符,它所表示的是一個(gè)換行符。而java中的編譯器,不僅會(huì)編譯代碼,還會(huì)解析unicode編碼將它替換成對(duì)應(yīng)的字符。所以說(shuō),上面的代碼解析完后實(shí)際是這樣的:
public static void main(String[] args) { // System.out.println(“coder Hydra”);}
這樣,就能解釋為什么能夠執(zhí)行注釋中的語(yǔ)句了。當(dāng)然,如果你覺(jué)得上面的代碼不夠絕,想要再絕一點(diǎn),那么就可以把代碼寫(xiě)成下面這個(gè)樣子。
public static void main(String[] args) { int a=1; // a++; System.out.println(a);}
執(zhí)行結(jié)果會(huì)打印2,同理,因?yàn)楹竺娴膗nicode編碼的轉(zhuǎn)義后表示的是a++;。
至于這么寫(xiě)有什么好處,當(dāng)然是用在某些不想讓別人看懂的地方,用來(lái)掩人耳目了,估計(jì)大家都看過(guò)下面這個(gè)笑話。
你這么寫(xiě)的話客戶如果懂點(diǎn)代碼,看一下就穿幫了啊,但是你如果寫(xiě)成下面這樣,大部分估計(jì)都以為這是一段亂碼:
//Thread.sleep(2000);
恕我直言,沒(méi)個(gè)幾十年的功力真看不出來(lái)這里執(zhí)行的是sleep,簡(jiǎn)直完美。
貳、舍近求遠(yuǎn)
要想寫(xiě)出別人看不懂的代碼,很重要的一個(gè)小技巧就是把簡(jiǎn)單的東西復(fù)雜化。例如,判斷一個(gè)int型數(shù)字的正負(fù)時(shí)明明可以寫(xiě)成這樣:
public void judge(int x){ if (x>0){ //… }else if (x<0){ //… }}
但是我偏不,放著簡(jiǎn)單的代碼不用,我就是玩,非要寫(xiě)成下面這樣:
public void judge2(int x){ if (x>>>31==0){ //… }else if (x>>>31==1){ //… }}
怎么樣,這么寫(xiě)的話是不是逼格一下子就支棱起來(lái)了!別人看到這多少得琢磨一會(huì)這塊到底寫(xiě)了個(gè)啥玩意。
其實(shí)原理也很簡(jiǎn)單,這里用到的>>>是無(wú)符號(hào)右移操作。舉個(gè)簡(jiǎn)單的例子,以-3為例,移位前先轉(zhuǎn)化為它的補(bǔ)碼:
11111111111111111111111111111101
無(wú)符號(hào)右移一位后變成下面的形式,這個(gè)數(shù)轉(zhuǎn)化為十進(jìn)制后是2147483646。
01111111111111111111111111111110
所以,當(dāng)一個(gè)int類(lèi)型的數(shù)字在無(wú)符號(hào)右移31位后,其實(shí)在前面的31位高位全部是0,剩下的最低位是原來(lái)的符號(hào)位,因此可以用來(lái)判斷數(shù)字的正負(fù)。
基于這個(gè)小知識(shí),我們還能整出不少活來(lái)。例如,放著好好的0不用,我們可以通過(guò)下面的方式定義一個(gè)0:
int ZERO=Integer.MAX_VALUE>>31>>1;
通過(guò)上面的知識(shí),相信大家可以輕易理解,因?yàn)樵趯⒁粋€(gè)數(shù)字無(wú)符號(hào)右移32位后,二進(jìn)制的所有位上全部是0,所以最終會(huì)得到0。那么問(wèn)題來(lái)了,我為什么不直接用Integer.MAX_VALUE>>32,一次性右移32位呢?
這是因?yàn)樵趯?duì)int型的數(shù)字進(jìn)行移位操作時(shí),會(huì)對(duì)操作符右邊的參數(shù)進(jìn)行模32的取余運(yùn)算,因此如果直接寫(xiě)32的話,那么相當(dāng)于什么都不做,得到的還是原數(shù)值。
叁、顛倒黑白
古有趙高指鹿為馬,今有碼農(nóng)顛倒真假。阻礙同事閱讀你代碼的有力武器之一,就是讓他在遇到條件判斷時(shí)失去基本判斷能力,陷入云里霧里,不知道接下來(lái)要走的是哪一個(gè)分支。
下面的代碼,我說(shuō)會(huì)打印fasle,是不是沒(méi)有人會(huì)信?
public class TrueTest { public static void main(String[] args) { Boolean reality = true; if(reality) { System.out.println(“true”); } else { System.out.println(“false”); } }}
沒(méi)錯(cuò),只要大家了解布爾類(lèi)型就知道這不符合邏輯,但是,經(jīng)過(guò)下面的改造就可以讓它變?yōu)楝F(xiàn)實(shí)。
首先,在類(lèi)中找個(gè)隱蔽的位置插入下面這段代碼:
static { try { Field trueField = Boolean.class.getDeclaredField(“TRUE”); trueField.setAccessible(true); Field modifiersField = Field.class.getDeclaredField(“modifiers”); modifiersField.setAccessible(true); modifiersField.setInt(trueField, trueField.getModifiers() & ~Modifier.FINAL); trueField.set(null, false); } catch(IllegalAccessException | NoSuchFieldException e) { e.printStackTrace(); }}
然后再運(yùn)行上面的程序,你就會(huì)發(fā)現(xiàn)神奇地打印了false。
其實(shí)原理也很簡(jiǎn)單,首先通過(guò)反射拿到Boolean類(lèi)中定義的TRUE這個(gè)變量:
public static final Boolean TRUE = new Boolean(true);
接著使用反射,去掉它的final修飾符,最后再將它的值設(shè)為false。而在之后再使用true進(jìn)行定義Boolean類(lèi)型的變量過(guò)程中,會(huì)進(jìn)行自動(dòng)裝箱,調(diào)用下面的方法:
public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE);}
這時(shí)的b為true,而TRUE實(shí)際上是false,因此不滿足第一個(gè)表達(dá)式,最終會(huì)返回false。
這樣一來(lái)就能解釋上面的打印結(jié)果了,不過(guò)切記,這么寫(xiě)的時(shí)候一定要找一個(gè)代碼中隱蔽的角落,不要被人發(fā)現(xiàn),否則容易被打的很慘…
肆、化整為零
接下來(lái)要介紹的這個(gè)技巧就有點(diǎn)厲害了,可以將原有的一段串行邏輯改寫(xiě)成判斷邏輯中的不同分支,并且保證最后能夠正常執(zhí)行。
在開(kāi)始前先提一個(gè)問(wèn)題,有沒(méi)有一種方法,可以讓if和else中的語(yǔ)句都能執(zhí)行,就像下面的這個(gè)例子中:
public static void judge(String param){ if (/*判斷條件*/){ System.out.println(“step one”); }else { System.out.println(“step two”); }}
如果我說(shuō)只調(diào)用一次這個(gè)方法,就能同時(shí)輸出if和else中的打印語(yǔ)句,你肯定會(huì)說(shuō)不可能,因?yàn)檫@違背了java中判斷邏輯的基本常識(shí)。
沒(méi)錯(cuò),在限定了上面的修飾語(yǔ)只調(diào)用『一次』方法的條件下,誰(shuí)都無(wú)法做到。但是如果在判斷條件中動(dòng)一點(diǎn)點(diǎn)手腳,就能夠?qū)崿F(xiàn)上面提到的功能??匆幌赂脑旌蟮拇a:
public class IfTest { public static void main(String[] args) { judge(“Hydra”); } public static void judge(String param){ if (param==null || new IfTest(){{ IfTest.check(null); }}.equals(“Hydra”)){ System.out.println(“step one”); }else { System.out.println(“step two”); } }}
運(yùn)行后控制臺(tái)打印了:
step onestep two
驚不驚喜、意不意外?其實(shí)它能夠執(zhí)行的秘密就在if的判斷條件中。
當(dāng)?shù)谝淮握{(diào)用judge()方法時(shí),不滿不或運(yùn)算中的第一個(gè)條件,因此執(zhí)行第二個(gè)條件,會(huì)執(zhí)行匿名內(nèi)部類(lèi)內(nèi)的實(shí)例化初始?jí)K代碼,再次執(zhí)行judge()方法,此時(shí)滿足if條件,因此執(zhí)行第一句打印語(yǔ)句。
而實(shí)例化的新對(duì)象不滿足后面的equals()方法中的條件,所以不滿足if中的任意一個(gè)條件,因此會(huì)執(zhí)行else中的語(yǔ)句,執(zhí)行第二句打印語(yǔ)句。
這樣就實(shí)現(xiàn)了表面上調(diào)用一次方法,同時(shí)執(zhí)行if和else中的語(yǔ)句塊的功能。怎么樣,用這種方式把一段整體的邏輯拆成兩塊,讓你的同事迷惑去吧。
伍、釜底抽薪
在程序員的世界里,不同語(yǔ)言之間一直存在鄙視鏈,例如寫(xiě)c的就看不起寫(xiě)java的,因?yàn)橹苯硬僮鲀?nèi)存啥的看上去就很高大上不是么?那么我們今天就假裝自己是一個(gè)c語(yǔ)言程序員,來(lái)在java中操作一把內(nèi)存。
具體要怎么做呢,還是要使用java中的魔法類(lèi)Unsafe。看這個(gè)名字也可以明白,這玩意如果使用不當(dāng)?shù)脑挷皇欠浅0踩垣@取Unsafe實(shí)例也比較麻煩,需要通過(guò)反射獲取:
Field unsafeField = Unsafe.class.getDeclaredField(“theUnsafe”);unsafeField.setAccessible(true);Unsafe unsafe =(Unsafe) unsafeField.get(null);
在拿到這個(gè)對(duì)象后,我們就可以對(duì)內(nèi)存為所欲為了。例如,我們?cè)趯?shí)現(xiàn)int a=1;這樣的簡(jiǎn)單賦值時(shí),就可以搞復(fù)雜點(diǎn),像下面這樣繞一個(gè)彎子:
void test(){ long addr = unsafe.allocateMemory(4); unsafe.putInt(addr,1); int a=unsafe.getInt(addr); System.out.println(a); unsafe.freeMemory(addr);}
首先通過(guò)allocateMemory方法申請(qǐng)4字節(jié)的內(nèi)存空間后,然后通過(guò)putInt方法寫(xiě)入一個(gè)1,再?gòu)倪@個(gè)地址讀取一個(gè)int類(lèi)型長(zhǎng)度的變量,最終實(shí)現(xiàn)了把1賦值給a的操作。
當(dāng)然了,還有很多高級(jí)一點(diǎn)的用法,這里簡(jiǎn)單舉兩個(gè)例子。
void test(){ long addr = unsafe.allocateMemory(4); unsafe.setMemory(addr,4, (byte) 1); System.out.println(unsafe.getInt(addr)); unsafe.freeMemory(addr);}
上面的代碼中,通過(guò)setMemory方法向每個(gè)字節(jié)寫(xiě)入byte類(lèi)型的1,最后調(diào)用getInt方法一次性讀取4個(gè)字節(jié)作為一個(gè)int型變量的值。這段代碼最終打印結(jié)果為16843009,對(duì)應(yīng)的二進(jìn)制如下:
00000001 00000001 00000001 00000001
至于c語(yǔ)言中的內(nèi)存復(fù)制,用Unsafe搞起來(lái)也是信手拈來(lái):
void test2(){ long addr = unsafe.allocateMemory(4); long addr2 = unsafe.reallocateMemory(addr, 4 * 2); unsafe.putInt(addr, 1); for (int i = 0; i < 2; i++) { unsafe.copyMemory(addr,addr2+4*i,4); } System.out.println(unsafe.getInt(addr)); System.out.println(unsafe.getLong(addr2)); unsafe.freeMemory(addr); unsafe.freeMemory(addr2);}
上面的代碼中,通過(guò)reallocateMemory方法重新分配了一塊8字節(jié)長(zhǎng)度的內(nèi)存空間,并把a(bǔ)ddr開(kāi)頭的4字節(jié)內(nèi)存空間分兩次進(jìn)復(fù)制到addr2的內(nèi)存空間中,上面的代碼會(huì)打?。?/p>
14294967297
這是因?yàn)樾碌?字節(jié)內(nèi)存空間addr2中存儲(chǔ)的二進(jìn)制數(shù)字是下面這樣,轉(zhuǎn)化為十進(jìn)制的long類(lèi)型后正好對(duì)應(yīng)4294967297。
100000000000000000000000000000001
Unsafe除了能直接操作內(nèi)存空間外,還有線程調(diào)度、對(duì)象操作、CAS操作等實(shí)用的功能,如果想詳細(xì)的了解一下,可以看看這篇Java雙刃劍之Unsafe類(lèi)詳解,開(kāi)啟新世界的大門(mén)。
最后
好了,沒(méi)用的知識(shí)介紹環(huán)節(jié)就此結(jié)束,相信大家在掌握了這些技巧后,都能自帶代碼混淆光環(huán),寫(xiě)出不一樣的拉轟代碼。
最后建議大家,在項(xiàng)目中這樣寫(xiě)代碼的時(shí)候,搭配紅花油、跌打損傷酒一起使用,可能效果更佳。
那么,這次的分享就到這里,我是Hydra,下篇文章再見(jiàn)。
作者簡(jiǎn)介,碼農(nóng)參上,一個(gè)熱愛(ài)分享的公眾號(hào),有趣、深入、直接,與你聊聊技術(shù)。歡迎添加好友,進(jìn)一步交流。