如何寫出同事看不懂的Java代碼?
原創:微信公眾號
碼農參上
,歡迎分享,轉載請保留出處。
哈嘍大家好啊,我是沒更新就是在家忙着帶娃的Hydra。
前幾天,正巧趕上組裡代碼review,一下午下來,感覺整個人都血壓拉滿了。五花八門的代碼讓我不禁感嘆,代碼規範這條道路還是任重而道遠…
那麼今天就來給大家總結一波Java中的代碼作死小技巧,熟練掌握這些小技巧後,保證能讓你寫出同事看不懂的代碼~
至於為啥要寫出同事看不懂的代碼,通過這次教訓,我發現好處還是挺多的,簡單舉幾個例子:
- 同事無法輕易修改你的代碼,避免團隊協作不當引入bug
- 塑造個人能力的不可替代性,規避被辭退的風險
- 代碼review時,幫助同事治療好多年的低血壓
好了,一本正經的胡說八道環節就此打住……廢話不多說了,下面正式開始。沒用的知識又要增加了…
壹、瞞天過海
我打賭你肯定想不到,有人居然會在注釋里下了毒。看看下面的代碼,簡單到main
方法中只有一行注釋。
public static void main(String[] args) {
// \u000d System.out.println("coder Hydra");
}
猜猜看,這段程序運行結果如何?執行後它居然會在控制台打印:
coder Hydra
看到這你是不是一臉懵逼,為什麼注釋中的代碼會被執行?
其實原理就在於大家熟悉的unicode
編碼,上面的\u000d
就是一個unicode
轉義字符,它所表示的是一個換行符。而java中的編譯器,不僅會編譯代碼,還會解析unicode
編碼將它替換成對應的字符。所以說,上面的代碼解析完後實際是這樣的:
public static void main(String[] args) {
//
System.out.println("coder Hydra");
}
這樣,就能解釋為什麼能夠執行注釋中的語句了。當然,如果你覺得上面的代碼不夠絕,想要再絕一點,那麼就可以把代碼寫成下面這個樣子。
public static void main(String[] args) {
int a=1;
// \u000d \u0061\u002b\u002b\u003b
System.out.println(a);
}
執行結果會打印2
,同理,因為後面的unicode
編碼的轉義後表示的是a++;
。
至於這麼寫有什麼好處,當然是用在某些不想讓別人看懂的地方,用來掩人耳目了,估計大家都看過下面這個笑話。
你這麼寫的話客戶如果懂點代碼,看一下就穿幫了啊,但是你如果寫成下面這樣,大部分估計都以為這是一段亂碼:
//\u000d\u0054\u0068\u0072\u0065\u0061\u0064\u002e\u0073\u006c\u0065\u0065\u0070\u0028\u0032\u0030\u0030\u0030\u0029\u003b
恕我直言,沒個幾十年的功力真看不出來這裡執行的是sleep
,簡直完美。
貳、捨近求遠
要想寫出別人看不懂的代碼,很重要的一個小技巧就是把簡單的東西複雜化。例如,判斷一個int
型數字的正負時明明可以寫成這樣:
public void judge(int x){
if (x>0){
//...
}else if (x<0){
//...
}
}
但是我偏不,放着簡單的代碼不用,我就是玩,非要寫成下面這樣:
public void judge2(int x){
if (x>>>31==0){
//...
}else if (x>>>31==1){
//...
}
}
怎麼樣,這麼寫的話是不是逼格一下子就支棱起來了!別人看到這多少得琢磨一會這塊到底寫了個啥玩意。
其實原理也很簡單,這裡用到的>>>
是無符號右移操作。舉個簡單的例子,以-3
為例,移位前先轉化為它的補碼:
11111111111111111111111111111101
無符號右移一位後變成下面的形式,這個數轉化為十進制後是2147483646
。
01111111111111111111111111111110
所以,當一個int
類型的數字在無符號右移31位後,其實在前面的31位高位全部是0,剩下的最低位是原來的符號位,因此可以用來判斷數字的正負。
基於這個小知識,我們還能整出不少活來。例如,放着好好的0不用,我們可以通過下面的方式定義一個0:
int ZERO=Integer.MAX_VALUE>>31>>1;
通過上面的知識,相信大家可以輕易理解,因為在將一個數字無符號右移32位後,二進制的所有位上全部是0,所以最終會得到0。那麼問題來了,我為什麼不直接用Integer.MAX_VALUE>>32
,一次性右移32位呢?
這是因為在對int
型的數字進行移位操作時,會對操作符右邊的參數進行模32的取余運算,因此如果直接寫32的話,那麼相當於什麼都不做,得到的還是原數值。
叄、顛倒黑白
古有趙高指鹿為馬,今有碼農顛倒真假。阻礙同事閱讀你代碼的有力武器之一,就是讓他在遇到條件判斷時失去基本判斷能力,陷入雲里霧裡,不知道接下來要走的是哪一個分支。
下面的代碼,我說會打印fasle
,是不是沒有人會信?
public class TrueTest {
public static void main(String[] args) {
Boolean reality = true;
if(reality) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
沒錯,只要大家了解布爾類型就知道這不符合邏輯,但是,經過下面的改造就可以讓它變為現實。
首先,在類中找個隱蔽的位置插入下面這段代碼:
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();
}
}
然後再運行上面的程序,你就會發現神奇地打印了false
。
其實原理也很簡單,首先通過反射拿到Boolean
類中定義的TRUE
這個變量:
public static final Boolean TRUE = new Boolean(true);
接着使用反射,去掉它的final
修飾符,最後再將它的值設為false
。而在之後再使用true
進行定義Boolean
類型的變量過程中,會進行自動裝箱,調用下面的方法:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
這時的b
為true
,而TRUE
實際上是false
,因此不滿足第一個表達式,最終會返回false
。
這樣一來就能解釋上面的打印結果了,不過切記,這麼寫的時候一定要找一個代碼中隱蔽的角落,不要被人發現,否則容易被打的很慘…
肆、化整為零
接下來要介紹的這個技巧就有點厲害了,可以將原有的一段串行邏輯改寫成判斷邏輯中的不同分支,並且保證最後能夠正常執行。
在開始前先提一個問題,有沒有一種方法,可以讓if
和else
中的語句都能執行,就像下面的這個例子中:
public static void judge(String param){
if (/*判斷條件*/){
System.out.println("step one");
}else {
System.out.println("step two");
}
}
如果我說只調用一次這個方法,就能同時輸出if
和else
中的打印語句,你肯定會說不可能,因為這違背了java中判斷邏輯的基本常識。
沒錯,在限定了上面的修飾語只調用『一次』方法的條件下,誰都無法做到。但是如果在判斷條件中動一點點手腳,就能夠實現上面提到的功能。看一下改造後的代碼:
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");
}
}
}
運行後控制台打印了:
step one
step two
驚不驚喜、意不意外?其實它能夠執行的秘密就在if
的判斷條件中。
當第一次調用judge()
方法時,不滿不或運算中的第一個條件,因此執行第二個條件,會執行匿名內部類內的實例化初始塊代碼,再次執行judge()
方法,此時滿足if
條件,因此執行第一句打印語句。
而實例化的新對象不滿足後面的equals()
方法中的條件,所以不滿足if
中的任意一個條件,因此會執行else
中的語句,執行第二句打印語句。
這樣就實現了表面上調用一次方法,同時執行if
和else
中的語句塊的功能。怎麼樣,用這種方式把一段整體的邏輯拆成兩塊,讓你的同事迷惑去吧。
伍、釜底抽薪
在程序員的世界裏,不同語言之間一直存在鄙視鏈,例如寫c的就看不起寫java的,因為直接操作內存啥的看上去就很高大上不是么?那麼我們今天就假裝自己是一個c語言程序員,來在java中操作一把內存。
具體要怎麼做呢,還是要使用java中的魔法類Unsafe
。看這個名字也可以明白,這玩意如果使用不當的話不是非常安全,所以獲取Unsafe
實例也比較麻煩,需要通過反射獲取:
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
在拿到這個對象後,我們就可以對內存為所欲為了。例如,我們在實現int a=1;
這樣的簡單賦值時,就可以搞複雜點,像下面這樣繞一個彎子:
void test(){
long addr = unsafe.allocateMemory(4);
unsafe.putInt(addr,1);
int a=unsafe.getInt(addr);
System.out.println(a);
unsafe.freeMemory(addr);
}
首先通過allocateMemory
方法申請4位元組的內存空間後,然後通過putInt
方法寫入一個1,再從這個地址讀取一個int
類型長度的變量,最終實現了把1賦值給a
的操作。
當然了,還有很多高級一點的用法,這裡簡單舉兩個例子。
void test(){
long addr = unsafe.allocateMemory(4);
unsafe.setMemory(addr,4, (byte) 1);
System.out.println(unsafe.getInt(addr));
unsafe.freeMemory(addr);
}
上面的代碼中,通過setMemory
方法向每個位元組寫入byte
類型的1,最後調用getInt
方法一次性讀取4個位元組作為一個int
型變量的值。這段代碼最終打印結果為16843009
,對應的二進制如下:
00000001 00000001 00000001 00000001
至於c語言中的內存複製,用Unsafe
搞起來也是信手拈來:
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);
}
上面的代碼中,通過reallocateMemory
方法重新分配了一塊8位元組長度的內存空間,並把addr
開頭的4位元組內存空間分兩次進複製到addr2
的內存空間中,上面的代碼會打印:
1
4294967297
這是因為新的8位元組內存空間addr2
中存儲的二進制數字是下面這樣,轉化為十進制的long
類型後正好對應4294967297
。
100000000000000000000000000000001
Unsafe除了能直接操作內存空間外,還有線程調度、對象操作、CAS操作等實用的功能,如果想詳細的了解一下,可以看看這篇Java雙刃劍之Unsafe類詳解,開啟新世界的大門。
最後
好了,沒用的知識介紹環節就此結束,相信大家在掌握了這些技巧後,都能自帶代碼混淆光環,寫出不一樣的拉轟代碼。
最後建議大家,在項目中這樣寫代碼的時候,搭配紅花油、跌打損傷酒一起使用,可能效果更佳。
那麼,這次的分享就到這裡,我是Hydra,下篇文章再見。
作者簡介,
碼農參上
,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。歡迎添加好友,進一步交流。