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 在最开始的时候是需要花很多的时间去做这件事情,但是我也相信,在持续的习惯形成之后,后续的输出就会变得自然而然,走出舒适区的过程必然是与人性相对抗的,学习是一件逆人性的事,在里面找到有趣的东西就会变成一种享受。

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