ARTS-2020-06-12 Tips: 每個開發者都應該知道的事

1 Algorithm

1.1 判斷鏈表有環

這2道題都和判斷鏈表是否有環相關,看下給的例子

Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the second node.

list cycle

對於這樣一個帶環的鏈表,判斷是否有環,最簡單的做法就是遍歷所有的節點,如果遇到重複的節點,則說明有環,由此解法便是,用一個 visitedSet 來裝已遍歷過的節點,當遇到重複的節點,則說明有環,第一次遍歷到的重複節點便是環的入口,這個解法需要額外提供一個 Set 空間,若是要求使用 O(1)空間複雜度,就有了另外一個思路:

使用一個快指針和一個慢指針來遍歷 list, 如果鏈表有環,則必然存在在某一個節點上2個指針相遇。

public boolean hasCycle(ListNode head) {
        	if (head == null || head.next == null) {
                return false;
            }
            ListNode slow = head;
            ListNode fast = head.next;
            while (slow != fast) {
                if (fast == null || fast.next == null) {
                    return false;
                }
                slow = slow.next;
                fast = fast.next.next;
            }
            return true;
    }

如果要找出環的入口位置呢?這裏就涉及一點點數學的上換算了:

假設鏈表 head 到環入口的節點數爲 a(不包含入口節點),環的長度爲b, 慢指針走過的路程爲 s,快指針走過的路程爲f, 當2個指針第一次相遇時,則有如下關係:

  • 第一次相遇:
  1. f = 2s (快指針每次走2步,慢指針每次走1步,所以相遇時路程是 2倍)
  2. f = s + k*b

這個公式就不太好理解, 假設相遇時的節點距離入口的距離爲d, Slow 繞圈爲 m, Fast 繞圈爲 n, 則:

  • s = a + d + m*b;
  • f = a + d + n*b;

換算一下就得出,f = (n-m)*b + s; (n>m, 快指針繞圈更多),即 f = s + k * b;

我們將1,2 消一下就得到,s = k * b, 所以說慢指針走了環的 k 圈,若此刻有一個節點從 head 處開始走,它每次路過環入口的時機應該在 a + k * b(k 爲繞圈數),由於慢指針 s=k * b, 所以讓一個節點從頭開始走,當它們相遇時,即爲環的入口了。

1.2 反轉鏈表

還有一類經典的鏈表題是反轉鏈表,自己做題的過程中發現要注意的點:

  • 如何準確的處理節點之間的關係,先鏈接後面的關係(先使節點有多個父節點),再修改節點鏈接
  • 知道哪個是 head 節點,比如全部反轉的 list 來說,最後一個節點是新的Head, 對 swap nodes in pairs來說新的 head 是第2個節點,對reverse nodes in k-gourp來說,新的 head 是第一組的最後一個節點
  1. 那麼對於全反轉來說 prev 走到最後一個節點就是新的 Head
ListNode prev=null;
ListNode cur=head;
while(cur != null){
    ListNode next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
}
return prev;
  1. 對於不是最後指針的位置是新 head的,都使用 dummy 來鏈接新的 Head
ListNode dummy= new ListNode(-1);
dummy.next = head;

當 reverse 完畢後, dummy.next指向新的 head.

2 Review

Introduction to Java Bytecode

https://dzone.com/articles/introduction-to-java-bytecode

這是一遍相對來說比較簡單的Java 字節碼入門文章,一開始作者講到了閱讀Java 字節碼比較枯燥無味,但是通過自己的故事說明了字節碼的作用,事情是這樣的,這哥們在很早之前做了一次功能變更,目的是爲了測試修復一個潛在的性能問題, 並且打好了 JAR 包部署到服務器上。不幸的是,他並沒有將源碼提交到版本控制系統上,後來不知什麼原因這段代碼找不到了,一點痕跡都沒有了(做了什麼新的功能,記得一定要提交代碼到Git 上),幾個月後當他想用到那段代碼的時候,悲劇就發生了。還好這哥們在遠程機器上部署的 JAR 包還在,於是他用反編譯的工具來找到源碼,更不幸的是,反編譯工具在關鍵的Class上崩潰了,也就是他正好要找的關鍵代碼!

在嘗試了無數次之後,他放棄了用反編譯工具,所幸的是,這哥們比較熟悉 Java 字節碼,並且還記得他的代碼是從哪開始的,於是通過閱讀Java 字節碼知道了自己做了哪些修改,最後他痛苦的總結到 😆

I made sure to learn from my mistake and preserve them this time!

Java 字節碼是一種介於高級語言和底層代碼之間的中間產物,它屏蔽了操作系統指令架構之間的區別,定義了JVM 虛擬機能夠識別的統一格式,對於所有平臺來說,字節碼都是通用的。

Java 字節碼有點類似於機器碼,但是卻更爲簡單易懂,這得益於 JVM 虛擬機使用的指令集架構比較簡單,並且有十分完善的文檔。

文章簡單介紹了一下 JVM 虛擬機支持的數據類型,比如我們常見的基本數據類型:byteshortintlongcharfloatdoubleboolean以及returnAddress,還有引用類型,包括class,array,interface. 對boolean類型來說並沒有相對應的字節碼指令,而是轉換成int 類型來操作,除了returnAddress代表指向下一個指令外,其他類型都可以在 Java 中找到對應的類型.

字節碼指令集的簡化得益於Sun公司採用了基於棧的虛擬機架構,這是一種不同於基於寄存器的架構。JVM 進程內存劃分成了不同的內存區域,但是卻只要檢查 JVM 虛擬機棧就能滿足遵循字節碼的指令集的要求。

對於JVM 虛擬機內存的劃分的介紹,這篇文章說的比較簡單,想了解更多可以閱讀周志明的《深入理解Java虛擬機(第三版)》以及 JVM 的虛擬機規範。

然後作者舉了幾個簡單的Java 代碼編譯後的字節碼,分析了字節碼是如何操作本地變量表和操作數棧的,這裏要注意的是invokestatic指令如何進行的方法調用,將操作數棧上的數據傳遞給下一個棧幀。還有就是new一個對象的時候會涉及到的指令,先是new指令創建一個在堆上的對象以及將引用對象push到操作數棧頂,然後dup指令拷貝了這個引用,也就是說此時在操作數棧上有2個實例對象的引用,然後壓入構造函數需要的參數和實例引用,用invokespecial調用構造函數。

最後作者說明,一般情況下並不需要完全掌握字節碼指令的詳細用法和具體的指令流程來讀懂程序執行的是什麼。比如爲了搞清楚使用 Java Stream 來讀取文件的時候是否會正確關閉流,通過查看字節碼發現有一段類似於 try-with-resource的邏輯就可以知道結果了。

public static void main(java.lang.String[]) throws java.lang.Exception;
 descriptor: ([Ljava/lang/String;)V
 flags: (0x0009) ACC_PUBLIC, ACC_STATIC
 Code:
   stack=2, locals=8, args_size=1
      0: ldc           #2                  // class test/Test
      2: ldc           #3                  // String input.txt
      4: invokevirtual #4                  // Method java/lang/Class.getResource:(Ljava/lang/String;)Ljava/net/URL;
      7: invokevirtual #5                  // Method java/net/URL.toURI:()Ljava/net/URI;
     10: invokestatic  #6                  // Method java/nio/file/Paths.get:(Ljava/net/URI;)Ljava/nio/file/Path;
     13: astore_1
     14: new           #7                  // class java/lang/StringBuilder
     17: dup
     18: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
     21: astore_2
     22: aload_1
     23: invokestatic  #9                  // Method java/nio/file/Files.lines:(Ljava/nio/file/Path;)Ljava/util/stream/Stream;
     26: astore_3
     27: aconst_null
     28: astore        4
     30: aload_3
     31: aload_2
     32: invokedynamic #10,  0             // InvokeDynamic #0:accept:(Ljava/lang/StringBuilder;)Ljava/util/function/Consumer;
     37: invokeinterface #11,  2           // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V
     42: aload_3
     43: ifnull        131
     46: aload         4
     48: ifnull        72
     51: aload_3
     52: invokeinterface #12,  1           // InterfaceMethod java/util/stream/Stream.close:()V
     57: goto          131
     60: astore        5
     62: aload         4
     64: aload         5
     66: invokevirtual #14                 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
     69: goto          131
     72: aload_3
     73: invokeinterface #12,  1           // InterfaceMethod java/util/stream/Stream.close:()V
     78: goto          131
     81: astore        5
     83: aload         5
     85: astore        4
     87: aload         5
     89: athrow
     90: astore        6
     92: aload_3
     93: ifnull        128
     96: aload         4
     98: ifnull        122
    101: aload_3
    102: invokeinterface #12,  1           // InterfaceMethod java/util/stream/Stream.close:()V
    107: goto          128
    110: astore        7
    112: aload         4
    114: aload         7
    116: invokevirtual #14                 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
    119: goto          128
    122: aload_3
    123: invokeinterface #12,  1           // InterfaceMethod java/util/stream/Stream.close:()V
    128: aload         6
    130: athrow
    131: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
    134: aload_2
    135: invokevirtual #16                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    138: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    141: return

我們可以看到 java/util/Stream的調用,通過invokedynamic處理了Consumer類型的對象,invokeinterface爲Stram.forEach 調用,後面會看到一些 Stream.close:()VMethod java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V的重複調用,此處就是try-with-resource用來自動關閉流的基本邏輯。

總的來說,這是一篇比較不錯的入門文章,作者以自身的案例來說明在日常開發中字節碼的作用,文章中所用的配圖也很值觀清晰。

3 Tips

What are some of the most basic things every programmer should know?這是在 Quora 上的一個話題,我摘錄了一些下來:

  1. If it’s not tested, it doesn’t work.

    未經測試的代碼無法保證正常工作

  2. Source control is your friend - make sure you use it.

    源碼管理會是你的朋友–確保你使用它 (請參考上面 Review 的那個老哥😆 )

  3. Just because you wrote it doesn’t mean you own it — don’t be offended if someone else on your team has to change your code.

    即使你寫了它,也不意味着你擁有它。如果團隊中的其他人不得不修改你的代碼,你也不要覺得被冒犯了

  4. Don’t reinvent the wheel, library code is there to help.

    不要造輪子,大多數時候用現成的庫就夠了

  5. The fastest code is code that’s never executed — look for early outs.

    最快的代碼是從未執行過的那些代碼–要提早發現它們

  6. Just because you didn’t write it doesn’t mean it’s crap.

    你沒寫過並不意味着它就是垃圾

  7. Source code is just a hint to the compiler about what you want to do, it won’t necessarily do it (e.g. You might declare a function as inline but the compiler doesn’t have to obey).

    源代碼只是告訴編譯器你想怎麼做,但是並不會真正這麼做(你可以將一個函數聲明爲內聯,但是編譯器不必遵守)

  8. Code that’s hard to understand is hard to maintain.

    難以閱讀的代碼就會難以維護

  9. Code that’s hard to maintain is next to useless.

    難以維護的代碼就趨近於無用的代碼

  10. “Whilst I’m editing this file I’ll just…” is a great way to introduce feature creep and bugs.

    修改時請儘量添加說明,爲了trouble shooting 更方便(不知道翻譯的對不對)

  11. The neater your code layout, the easier it is to read. The easier it is to read, the easier it is to understand and maintain.

    代碼越乾淨,越容易閱讀,越容易閱讀,就越容易理解和維護

  12. Code is not self documenting. Help others by adding comments to guide them. You may understand it now but what about in 5 years time?

    代碼並不是文檔,請添加必要的註釋來幫助他人,也許你目前可以理解代碼的含義,但是5年以後呢?

  13. Bad Code can and will come back to haunt you.

    垃圾代碼會回來報復你的( -)

  14. There is no such thing as a 5 minute job. It’ll always take at least half a day.

    沒有5分子就解決的事,至少都需要半天(多思考全面一點)

  15. Magic numbers are bad.

    請不要使用魔法數字,請使用常量

  16. Constants don’t take up storage, they’re compile time text substitutions.

    常量不佔用存儲空間,它們是編譯時的文本替換

  17. Project management will always want you to do twice as much in half the time.

    項目管理總是希望你在一半的時間裏做兩倍的事情(無奈)

  18. If there is a bug, the user will find it.

    用戶總會發現你的bug的

  19. A code review is not a criticism.

    Code review 並不是一種批評( 相反,會讓你更進一步)

  20. It’s not the quantity of code that matters, it’s the quality. Any idiot can bang out 40kloc but that doesn’t make it fit for purpose.

    重要的不是代碼的數量,而是代碼的質量。任何一個白癡都可以敲出40kloc,但這並不適合它的目的。

  21. The true cost of poorly written code is in the maintenance.

    垃圾代碼需要高昂的維護成本

  22. Eat your own dog food — fixing bugs in your own code helps you code better and improves your understanding.

    喫自己的狗糧-修復自己代碼中的錯誤可以幫助您更好地編寫代碼並增進理解

  23. Code rots over time.

    代碼會隨着時間的推移而腐爛

  24. If the user didn’t ask for a feature, don’t add it.

    用戶沒有要求,就不要加一些額外的功能(如無必要,勿增實體)

  25. If it’s not tested, it doesn’t work (yes, I know I’ve included that twice but it’s really important).

    重要的事情說三遍,作者說了2遍

4 Share

要能夠持續的輸出是一件很花費時間的事情,一是要去廣泛的閱讀,在閱讀之後還要有自己的思考在裏面,每週一篇 ARTS 在最開始的時候是需要花很多的時間去做這件事情,但是我也相信,在持續的習慣形成之後,後續的輸出就會變得自然而然,走出舒適區的過程必然是與人性相對抗的,學習是一件逆人性的事,在裏面找到有趣的東西就會變成一種享受。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章