<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Everlasting Pages</title><description>记录编程、学习与生活</description><link>https://blog.everlasting.xin</link><item><title>导论</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/00-introduction</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/00-introduction</guid><description>CS61B 中文教材导论，介绍课程学习方式、Java 编程环境、数据结构与算法主题以及配套练习的使用方法。</description><content:encoded>&lt;hr&gt;
&lt;p&gt;本书是 Josh Hug 版本的 UC Berkeley 数据结构课程 CS61B 的配套读物。课程 2021 年春季版本位于 &lt;a href=&quot;https://sp21.datastructur.es/&quot;&gt;https://sp21.datastructur.es/&lt;/a&gt;。此后可能已有更新版本，请参阅 &lt;a href=&quot;https://datastructur.es&quot;&gt;https://datastructur.es&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;本课程默认你已经牢固掌握编程基础。至少应熟悉面向对象编程、递归、列表和树等概念，也应当会在自己使用的操作系统中操作终端。如果你还没有这些经验，建议先学习 UC Berkeley 的编程入门课程 &lt;a href=&quot;http://cs61a.org/&quot;&gt;CS61A&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果你已经有 Java 经验，可以考虑直接从第 2 章开始；不过快速浏览第 1 章仍可能有所收获。&lt;/p&gt;
&lt;p&gt;许可说明：&lt;/p&gt;
&lt;p&gt;本课程的全部材料采用 Creative Commons 4.0 BY-NC-SA（知识共享署名—非商业性使用—相同方式共享 4.0）许可发布。你可以通过&lt;a href=&quot;https://creativecommons.org/licenses/by-nc-sa/4.0/&quot;&gt;此链接&lt;/a&gt;阅读通俗易懂的许可说明。简单来说，这些材料可以免费使用，也可以任意再发布或改编，但必须注明 Josh Hug 为原作者，并且你创作的衍生内容也必须采用相同许可。Chegg、CourseHero 等网站不得发布本课程材料。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 1 章 Java 入门</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/01-java-basics</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/01-java-basics</guid><description>CS61B Java 入门中文教程，讲解变量、控制流、函数、类、对象、静态成员、构造方法与数组等核心语法。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;1.1 基础要点&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Jfq90-4qsco&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先来看我们的第一个 Java 程序。运行下面的程序时，屏幕上会输出 “Hello world!”。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于从 Python 等语言转过来的同学，这段代码可能显得冗长得没有必要。不过这些写法都有充分理由，接下来的几周里我们会逐渐理解它们。先注意以下几个关键语法特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整个程序由一个类声明构成，类通过关键字 &lt;code&gt;public class&lt;/code&gt; 声明。在 Java 中，所有代码都位于类之中。&lt;/li&gt;
&lt;li&gt;真正执行的代码位于名为 &lt;code&gt;main&lt;/code&gt; 的方法中，它被声明为 &lt;code&gt;public static void main(String[] args)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;使用花括号 &lt;code&gt;{&lt;/code&gt; 和 &lt;code&gt;}&lt;/code&gt; 表示一段代码的开始和结束。&lt;/li&gt;
&lt;li&gt;每条语句必须以分号结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本书不是一本完整的 Java 语法教材，因此不会逐项详讲 Java 语法。需要查阅资料时，可以参考 Paul Hilfinger 的免费电子书 &lt;a href=&quot;http://www-inst.eecs.berkeley.edu/~cs61b/fa14/book1/java.pdf&quot;&gt;A Java Reference&lt;/a&gt;；如果更喜欢传统教材，也可以阅读 Kathy Sierra 与 Bert Bates 合著的 &lt;a href=&quot;http://www.headfirstlabs.com/books/hfjava/&quot;&gt;Head First Java&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;感兴趣的话，还可以看看&lt;a href=&quot;https://www.rosettacode.org/wiki/Hello_world/Text&quot;&gt;其他编程语言中的 Hello world!&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;运行 Java 程序&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Y2vC_SW00TE&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;执行 Java 程序最常见的方式，是依次运行两个程序。第一个是 Java 编译器 &lt;code&gt;javac&lt;/code&gt;，第二个是 Java 解释器 &lt;code&gt;java&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/images/85af6d04564b-compilation_figure.svg&quot; alt=&quot;编译流程&quot;&gt;&lt;/p&gt;
&lt;p&gt;例如，要运行 &lt;code&gt;HelloWorld.java&lt;/code&gt;，先在终端输入命令 &lt;code&gt;javac HelloWorld.java&lt;/code&gt;，然后输入 &lt;code&gt;java HelloWorld&lt;/code&gt;。结果大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac HelloWorld.java
$ java HelloWorld
Hello World! 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的 &lt;code&gt;$&lt;/code&gt; 表示终端的命令提示符。你电脑上的提示符很可能更长。&lt;/p&gt;
&lt;p&gt;你可能会注意到，编译时命令中包含 &lt;code&gt;.java&lt;/code&gt;，解释执行时却不写 &lt;code&gt;.class&lt;/code&gt;。规则就是如此（TIJTWII，意为 “This Is Just The Way It Is”）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 1.1.1。&lt;/strong&gt; 在电脑上创建一个名为 &lt;code&gt;HelloWorld.java&lt;/code&gt; 的文件，将上面的完整程序原样复制进去。尝试执行 &lt;code&gt;javac HelloWorld.java&lt;/code&gt;。表面上看起来什么也没有发生。&lt;/p&gt;
&lt;p&gt;但如果查看当前目录，会发现生成了一个名为 &lt;code&gt;HelloWorld.class&lt;/code&gt; 的新文件。稍后我们会说明它是什么。&lt;/p&gt;
&lt;p&gt;接着输入命令 &lt;code&gt;java HelloWorld&lt;/code&gt;，终端中应该会显示 “Hello World!”。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;纯属好玩。&lt;/em&gt; 尝试用记事本、TextEdit、Sublime、vim 或你喜欢的其他文本编辑器打开 &lt;code&gt;HelloWorld.class&lt;/code&gt;。你会看到一大堆只有 Java 解释器才看得懂的乱码。这就是 &lt;a href=&quot;https://en.wikipedia.org/wiki/Java_bytecode&quot;&gt;Java 字节码&lt;/a&gt;，本课程不会深入讨论它。&lt;/p&gt;
&lt;h3&gt;变量与循环&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/xX04gYy9en0&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面的程序会输出从 0 到 9 的整数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x &amp;#x3C; 10) {
            System.out.print(x + &quot; &quot;);
            x = x + 1;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行程序后会看到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac HelloNumbers.java
$ java HelloNumbers
$ 0 1 2 3 4 5 6 7 8 9 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段程序中有一些值得注意的特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量 &lt;code&gt;x&lt;/code&gt; 必须先声明再使用，&lt;em&gt;而且声明时必须给出类型！&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;循环体放在花括号中，被判断的布尔表达式放在圆括号中。&lt;/li&gt;
&lt;li&gt;输出语句使用 &lt;code&gt;System.out.print&lt;/code&gt;，而不是 &lt;code&gt;System.out.println&lt;/code&gt;，因此输出后不会另起一行。&lt;/li&gt;
&lt;li&gt;输出语句把一个数字与一个空格连接起来，从而避免数字挤在一起。删掉空格试试看会发生什么。&lt;/li&gt;
&lt;li&gt;程序运行结束后，命令提示符会紧跟在这些数字的同一行上。如果愿意，可以在后面的练习中修复这一点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些特征中最重要的是：变量具有声明时确定的类型。稍后我们会回到这一点，不过先做一道练习。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 1.1.2。&lt;/strong&gt; 修改 &lt;code&gt;HelloNumbers&lt;/code&gt;，让它输出从 0 到 9 各整数的累计和。例如，输出应以 &lt;code&gt;0 1 3 6 10...&lt;/code&gt; 开始，并以 &lt;code&gt;45&lt;/code&gt; 结束。&lt;/p&gt;
&lt;p&gt;另外，如果你比较在意输出格式，可以让程序在最后额外输出一个换行符。&lt;/p&gt;
&lt;h3&gt;Gradescope&lt;/h3&gt;
&lt;p&gt;本课程通过名为 &lt;a href=&quot;http://www.gradescope.com&quot;&gt;Gradescope&lt;/a&gt; 的网站评分。如果你正在正式修读与本书配套的加州大学课程，需要在该网站提交作业并获得成绩。如果只是自学，也可以使用 Gradescope 检查答案。注册时使用课程代码 &lt;code&gt;93PK75&lt;/code&gt;。有关 Gradescope 及作业提交方法的更多信息，请参阅 &lt;a href=&quot;https://github.com/joshhug/hug61b/tree/e1d84817521747a76f17d2ed077abab493505c3f/chap1/TBA/README.md&quot;&gt;Gradescope 指南（链接稍后补充）&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;静态类型&lt;/h3&gt;
&lt;p&gt;Java 最重要的特征之一，是所有变量和表达式都具有所谓的 &lt;code&gt;静态类型&lt;/code&gt;。Java 变量只能保存该类型的值，而且变量的类型一旦确定就永远不会改变。&lt;/p&gt;
&lt;p&gt;Java 编译器的一项核心功能是执行静态类型检查。例如，假设有下面的程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x &amp;#x3C; 10) {
            System.out.print(x + &quot; &quot;);
            x = x + 1;
        }
        x = &quot;horse&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后会看到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac HelloNumbers.java 
HelloNumbers.java:9: error: incompatible types: String cannot be converted to int
        x = &quot;horse&quot;;
                ^
1 error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器在程序真正运行之前就直接拒绝了它。这一点非常重要，因为这意味着现实世界中任何运行该程序的人，都绝不可能在运行时遇到这个类型错误。&lt;/p&gt;
&lt;p&gt;这与 Python 等动态类型语言形成鲜明对比：在动态类型语言中，用户可能在程序执行过程中才碰到类型错误。&lt;/p&gt;
&lt;p&gt;除了提供额外的错误检查，静态类型还会明确告诉程序员当前处理的对象究竟是什么类型。接下来的几周里，我们会看到这一点有多重要。这也是我个人最喜欢的 Java 特性之一。&lt;/p&gt;
&lt;p&gt;总结来说，静态类型具有以下优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器会确保所有类型彼此兼容，使程序员更容易调试代码。&lt;/li&gt;
&lt;li&gt;由于代码已经被保证不存在类型错误，编译后程序的用户不会再遇到类型错误。例如，Android 应用使用 Java 编写，通常只以 &lt;code&gt;.class&lt;/code&gt; 文件（也就是编译后的形式）发布。因为它们已经经过编译器检查，所以这类应用原则上不应因类型错误而崩溃。&lt;/li&gt;
&lt;li&gt;每个变量、参数和函数都有明确声明的类型，程序员因此更容易理解代码并进行推理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过静态类型也有缺点，我们会在后续章节中讨论。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充思考题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 Java 中可以写 &lt;code&gt;System.out.println(5 + &quot; &quot;);&lt;/code&gt;。但在 Python 中，不能像上面那样写 &lt;code&gt;print(5 + &quot;horse&quot;)&lt;/code&gt;。为什么？&lt;/p&gt;
&lt;p&gt;考虑以下两条 Java 语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String h = 5 + &quot;horse&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int h = 5 + &quot;horse&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一条语句可以通过编译，第二条则会产生编译错误。Java 是强类型语言；当你声明 &lt;code&gt;h&lt;/code&gt; 是字符串时，Java 可以把两个元素连接起来并得到字符串。但当 &lt;code&gt;h&lt;/code&gt; 是 &lt;code&gt;int&lt;/code&gt; 时，Java 无法把一个数字和一个字符串连接后再得到数字。&lt;/p&gt;
&lt;p&gt;Python 并没有事先限定结果类型，因此也无法猜测你想得到什么类型。&lt;code&gt;x = 5 + &quot;horse&quot;&lt;/code&gt; 究竟应该得到数字还是字符串？Python 不知道，所以会报错。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;System.out.println(5 + &quot;horse&quot;);&lt;/code&gt;，Java 会把参数解释为字符串拼接，因此输出结果是 &lt;code&gt;5horse&lt;/code&gt;。更实用的例子是 &lt;code&gt;System.out.println(5 + &quot; &quot;);&lt;/code&gt;，它会在 &lt;code&gt;5&lt;/code&gt; 后输出一个空格。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;System.out.println(5 + &quot;10&quot;);&lt;/code&gt; 会输出 &lt;code&gt;510&lt;/code&gt; 还是 &lt;code&gt;15&lt;/code&gt;？那么 &lt;code&gt;System.out.println(5 + 10);&lt;/code&gt; 又会输出什么？&lt;/p&gt;
&lt;h3&gt;在 Java 中定义函数&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ToJrue6Kg9A&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 Python 等语言中，函数可以在许多位置声明，包括其他函数之外。例如，下面的代码声明了一个返回两个参数中较大值的函数，然后用它计算并输出 8 与 10 中较大的那个数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def larger(x, y):
    if x &gt; y:
        return x
    return y

print(larger(8, 10))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于所有 Java 代码都属于某个类，我们必须把函数定义为某个类的成员。属于类的函数通常称为“方法”。本课程中会交替使用“函数”和“方法”这两个词。与上面代码等价的 Java 程序如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class LargerDemo {
    public static int larger(int x, int y) {
        if (x &gt; y) {
            return x;
        }
        return y;
    }

    public static void main(String[] args) {
        System.out.println(larger(8, 10));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的新语法是：方法通过关键字 &lt;code&gt;public static&lt;/code&gt; 声明，它大致相当于 Python 的 &lt;code&gt;def&lt;/code&gt;。下一章会介绍声明方法的其他方式。&lt;/p&gt;
&lt;p&gt;这段 Java 代码显然冗长得多。你可能认为这种编程语言会拖慢开发速度；至少在短期内，确实如此。可以先把这些额外内容看成我们尚未理解的安全装备。编写小程序时，它们似乎全是多余的；但当我们开始构建大型程序时，就会逐渐理解并欣赏这些额外复杂度的价值。&lt;/p&gt;
&lt;p&gt;打个比方，用 Python 编程有点像 &lt;a href=&quot;https://www.youtube.com/watch?v=NCByLWtM7y4&quot;&gt;Dan Osman 徒手攀登 Lover&apos;s Leap&lt;/a&gt;：速度可能很快，但也很危险。相比之下，Java 更像&lt;a href=&quot;https://www.youtube.com/watch?v=tr6UIfPEuI0&quot;&gt;这段视频&lt;/a&gt;中那样使用绳索、头盔等安全设备。&lt;/p&gt;
&lt;h3&gt;代码风格、注释与 Javadoc&lt;/h3&gt;
&lt;p&gt;代码可以在许多方面体现美感：可以简洁，可以巧妙，也可以高效。初学者最容易忽视的方面之一是代码风格。刚开始编程时，人们往往只顾着让程序跑起来，而不会考虑以后是否还要重新阅读它，或者是否需要长期维护它。&lt;/p&gt;
&lt;p&gt;本课程会非常重视代码的可读性。良好代码风格最重要的特征包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;风格一致，包括空格、变量命名、花括号风格等。&lt;/li&gt;
&lt;li&gt;大小适当，例如每行不要过宽，源文件不要过长。&lt;/li&gt;
&lt;li&gt;使用描述性名称，包括变量、函数和类。例如应使用 &lt;code&gt;year&lt;/code&gt; 或 &lt;code&gt;getUserName&lt;/code&gt;，而不是 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;f&lt;/code&gt; 之类缺乏含义的名称。&lt;/li&gt;
&lt;li&gt;避免重复代码：几乎不应出现两段内容大致相同、只在少数地方不同的重要代码块。&lt;/li&gt;
&lt;li&gt;在合适的位置写注释。Java 的单行注释使用 &lt;code&gt;//&lt;/code&gt;，块注释（也叫多行注释）使用 &lt;code&gt;/*&lt;/code&gt; 和 &lt;code&gt;*/&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;黄金法则是：写出的代码应当让一个陌生人也能轻松理解。&lt;/p&gt;
&lt;p&gt;这里是课程官方的&lt;a href=&quot;https://sp19.datastructur.es/materials/guides/style-guide.html&quot;&gt;代码风格指南&lt;/a&gt;，值得阅读。&lt;/p&gt;
&lt;p&gt;为了让代码更简单、更容易真正理解，我们常常愿意接受轻微的性能损失。后续章节会重点展示一些例子。&lt;/p&gt;
&lt;h4&gt;注释&lt;/h4&gt;
&lt;p&gt;我们鼓励编写具有自解释性的代码，也就是选择能让人一眼看出程序意图的变量名和函数名。不过仅靠命名并不总是足够。例如，实现复杂算法时，可能仍需通过注释说明代码。注释应当谨慎使用。随着经验增长，并阅读更多他人的代码，你会逐渐形成判断：什么时候最适合添加注释。&lt;/p&gt;
&lt;p&gt;特别需要注意的是，所有方法以及几乎所有类，都应使用所谓的 &lt;a href=&quot;https://en.wikipedia.org/wiki/Javadoc&quot;&gt;Javadoc&lt;/a&gt; 格式添加说明注释。Javadoc 注释的块注释开头会多一个星号，即 &lt;code&gt;/**&lt;/code&gt;；它通常（但并非总是）还包含描述性标签。本书不会详细介绍这些标签，可以通过上面的链接了解其工作方式。&lt;/p&gt;
&lt;p&gt;下面是不带标签的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class LargerDemo {
    /** Returns the larger of x and y. */           
    public static int larger(int x, int y) {
        if (x &gt; y) {
            return x;
        }
        return y;
    }

    public static void main(String[] args) {
        System.out.println(larger(8, 10));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;广泛使用的 &lt;a href=&quot;http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html&quot;&gt;Javadoc 工具&lt;/a&gt;可以根据代码生成 HTML 说明文档。后续章节会看到相关例子。&lt;/p&gt;
&lt;h4&gt;接下来做什么&lt;/h4&gt;
&lt;p&gt;每章结尾都会列出使用当前已学内容可以完成的练习（如果有），并按照建议完成顺序排列。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://sp18.datastructur.es/materials/hw/hw0/hw0.html&quot;&gt;作业 0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp18.datastructur.es/materials/lab/lab1setup/lab1setup&quot;&gt;实验 1b&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp18.datastructur.es/materials/lab/lab1/lab1.html&quot;&gt;实验 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp18.datastructur.es/materials/discussion/disc01.pdf&quot;&gt;讨论课 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;1.2 对象&lt;/h2&gt;
&lt;p&gt;如果你以前没有使用过 Java，建议在阅读本章之前先完成 &lt;a href=&quot;http://sp19.datastructur.es/materials/hw/hw0/hw0.html&quot;&gt;HW0&lt;/a&gt; 中的练习。它会介绍本书不再专门讲解的若干语法问题。&lt;/p&gt;
&lt;h3&gt;静态方法与非静态方法&lt;/h3&gt;
&lt;h4&gt;静态方法&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/xDPUHRjsgA4&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Java 中的所有代码都必须属于某个类（或者属于一种与类相似的结构，后面会学到）。大多数代码写在方法中。来看一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog {
    public static void makeNoise() {
        System.out.println(&quot;Bark!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果尝试直接运行 &lt;code&gt;Dog&lt;/code&gt; 类，只会得到一条错误信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ java Dog
Error: Main method not found in class Dog, please define the main method as:
       public static void main(String[] args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们定义的 &lt;code&gt;Dog&lt;/code&gt; 类本身不会执行任何事情。我们只是定义了 &lt;code&gt;Dog&lt;/code&gt; &lt;strong&gt;能够&lt;/strong&gt;做的一件事，也就是发出叫声。要真正运行这个类，可以像 1.1 节那样给 &lt;code&gt;Dog&lt;/code&gt; 添加一个 &lt;code&gt;main&lt;/code&gt; 方法；也可以创建一个单独的 &lt;a href=&quot;https://www.youtube.com/watch?v=Q-LE-jJQLTM&quot;&gt;&lt;code&gt;DogLauncher&lt;/code&gt;&lt;/a&gt; 类，用它来调用 &lt;code&gt;Dog&lt;/code&gt; 类中的方法。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DogLauncher {
    public static void main(String[] args) {
        Dog.makeNoise();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ java DogLauncher
Bark!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用另一个类的类，有时称为那个类的“客户端”。因此，&lt;code&gt;DogLauncher&lt;/code&gt; 是 &lt;code&gt;Dog&lt;/code&gt; 的客户端。这两种技术没有绝对优劣：有些情况下，给 &lt;code&gt;Dog&lt;/code&gt; 添加 &lt;code&gt;main&lt;/code&gt; 方法更合适；另一些情况下，创建 &lt;code&gt;DogLauncher&lt;/code&gt; 这样的客户端类更合适。随着课程中练习增多，两种方法各自的优势会逐渐变得清晰。&lt;/p&gt;
&lt;h4&gt;实例变量与对象实例化&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Hsor-iZ6-a4&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;狗与狗并不相同。有些狗会不停地尖叫，有些狗则会发出浑厚悠长的吼声，让所有听到这宏亮叫声的人心情愉悦。我们经常编写程序来模拟现实世界中的特征，而 Java 的语法正是为了方便这种模拟而设计的。&lt;/p&gt;
&lt;p&gt;为了表示形形色色的狗，一种办法是为每一类狗分别创建一个类。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TinyDog {
    public static void makeNoise() {
        System.out.println(&quot;yip yip yip yip&quot;);
    }
}

public class MalamuteDog {
    public static void makeNoise() {
        System.out.println(&quot;arooooooooooooooo!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你以前应该已经见过：类可以被实例化，而实例可以保存数据。于是我们可以采用更自然的方法——创建 &lt;code&gt;Dog&lt;/code&gt; 类的不同实例，并让 &lt;code&gt;Dog&lt;/code&gt; 方法的行为取决于某一只具体 &lt;code&gt;Dog&lt;/code&gt; 的属性。下面的类展示了这种做法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog {
    public int weightInPounds;

    public void makeNoise() {
        if (weightInPounds &amp;#x3C; 10) {
            System.out.println(&quot;yipyipyip!&quot;);
        } else if (weightInPounds &amp;#x3C; 30) {
            System.out.println(&quot;bark. bark.&quot;);
        } else {
            System.out.println(&quot;woof!&quot;);
        }
    }    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是使用这种 &lt;code&gt;Dog&lt;/code&gt; 的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DogLauncher {
    public static void main(String[] args) {
        Dog d;
        d = new Dog();
        d.weightInPounds = 20;
        d.makeNoise();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后，程序会创建一只体重为 20 磅的 &lt;code&gt;Dog&lt;/code&gt;，随后这只狗会发出悦耳的 “bark. bark.”。&lt;/p&gt;
&lt;p&gt;需要掌握的几个关键观察和术语如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 中的 &lt;code&gt;Object&lt;/code&gt;（对象）是任意类的一个实例。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dog&lt;/code&gt; 类拥有自己的变量，也称为_实例变量_或_非静态变量_。这些变量必须在类中声明；这与 Python 或 Matlab 等语言不同，后者允许在运行时添加新变量。&lt;/li&gt;
&lt;li&gt;我们在 &lt;code&gt;Dog&lt;/code&gt; 类中创建的方法没有关键字 &lt;code&gt;static&lt;/code&gt;。这种方法称为_实例方法_或_非静态方法_。&lt;/li&gt;
&lt;li&gt;要调用 &lt;code&gt;makeNoise&lt;/code&gt; 方法，必须先使用 &lt;code&gt;new&lt;/code&gt; 关键字_实例化_一只 &lt;code&gt;Dog&lt;/code&gt;，然后让某个具体的 &lt;code&gt;Dog&lt;/code&gt; 发出叫声。也就是说，调用的是 &lt;code&gt;d.makeNoise()&lt;/code&gt;，而不是 &lt;code&gt;Dog.makeNoise()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对象实例化后，可以被_赋值_给一个已经_声明_且类型合适的变量，例如 &lt;code&gt;d = new Dog();&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;类中的变量和方法统称为类的_成员_。&lt;/li&gt;
&lt;li&gt;通过_点号记法_访问类的成员。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Java 中的构造方法&lt;/h4&gt;
&lt;p&gt;在面向对象语言中，我们通常使用_构造方法_来构造对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DogLauncher {
    public static void main(String[] args) {
        Dog d = new Dog(20);
        d.makeNoise();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的实例化操作带有参数，省去了手动逐一给多个实例变量赋值的时间，也让代码更加整洁。要支持这种语法，只需像下面这样在 &lt;code&gt;Dog&lt;/code&gt; 类中添加一个“构造方法”：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog {
    public int weightInPounds;

    public Dog(int w) {
        weightInPounds = w;
    }

    public void makeNoise() {
        if (weightInPounds &amp;#x3C; 10) {
            System.out.println(&quot;yipyipyip!&quot;);
        } else if (weightInPounds &amp;#x3C; 30) {
            System.out.println(&quot;bark. bark.&quot;);
        } else {
            System.out.println(&quot;woof!&quot;);
        }    
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每当我们使用 &lt;code&gt;new&lt;/code&gt; 关键字并传入一个整数参数来创建 &lt;code&gt;Dog&lt;/code&gt; 时，签名为 &lt;code&gt;public Dog(int w)&lt;/code&gt; 的构造方法就会被调用。如果你来自 Python，可以把构造方法理解为与 &lt;code&gt;__init__&lt;/code&gt; 方法非常相似的机制。&lt;/p&gt;
&lt;h4&gt;术语总结&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ewMzZBy1RO4&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;数组实例化与对象数组&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/GgoszMH1ak8&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;正如在 HW0 中看到的，Java 数组同样使用 &lt;code&gt;new&lt;/code&gt; 关键字实例化。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ArrayDemo {
    public static void main(String[] args) {
        /* Create an array of five integers. */
        int[] someArray = new int[5];
        someArray[0] = 3;
        someArray[1] = 4;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似地，也可以创建用于保存已实例化对象的数组，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DogArrayDemo {
    public static void main(String[] args) {
        /* Create an array of two dogs. */
        Dog[] dogs = new Dog[2];
        dogs[0] = new Dog(8);
        dogs[1] = new Dog(20);

        /* Yipping will result, since dogs[0] has weight 8. */
        dogs[0].makeNoise();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，&lt;code&gt;new&lt;/code&gt; 在这里以两种不同方式使用：第一次创建一个能够保存两个 &lt;code&gt;Dog&lt;/code&gt; 对象的数组；之后两次分别创建真正的 &lt;code&gt;Dog&lt;/code&gt; 对象。&lt;/p&gt;
&lt;h3&gt;类方法与实例方法&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/l8Q6NXbAwNk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Java 允许我们定义两类方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类方法，也就是静态方法。&lt;/li&gt;
&lt;li&gt;实例方法，也就是非静态方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实例方法是只能由类的某个具体实例执行的操作；静态方法则是由类本身执行的操作。两者分别适用于不同场景。例如，&lt;code&gt;Math&lt;/code&gt; 类提供了静态方法 &lt;code&gt;sqrt&lt;/code&gt;，因此可以这样调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;x = Math.sqrt(100);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;sqrt&lt;/code&gt; 是实例方法，就必须使用下面这种笨拙的语法。幸运的是，&lt;code&gt;sqrt&lt;/code&gt; 是静态方法，实际程序中不需要这样写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Math m = new Math();
x = m.sqrt(100);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有时，一个类同时包含实例方法和静态方法是合理的。例如，假设我们希望比较两只狗。一种办法是添加一个用于比较 &lt;code&gt;Dog&lt;/code&gt; 的静态方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Dog maxDog(Dog d1, Dog d2) {
    if (d1.weightInPounds &gt; d2.weightInPounds) {
        return d1;
    }
    return d2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如可以这样调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Dog d = new Dog(15);
Dog d2 = new Dog(100);
Dog.maxDog(d, d2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里使用类名调用方法，因为它是静态方法。&lt;/p&gt;
&lt;p&gt;也可以把 &lt;code&gt;maxDog&lt;/code&gt; 实现为非静态方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Dog maxDog(Dog d2) {
    if (this.weightInPounds &gt; d2.weightInPounds) {
        return this;
    }
    return d2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面使用关键字 &lt;code&gt;this&lt;/code&gt; 表示当前对象。例如可以这样调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Dog d = new Dog(15);
Dog d2 = new Dog(100);
d.maxDog(d2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是通过一个具体实例变量来调用方法的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 1.2.1：&lt;/strong&gt; 下面的方法会做什么？如果不确定，请亲自试一试。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Dog maxDog(Dog d1, Dog d2) {
    if (weightInPounds &gt; d2.weightInPounds) {
        return this;
    }
    return d2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;静态变量&lt;/h4&gt;
&lt;p&gt;有时，让类拥有静态变量很有用。静态变量描述类本身固有的属性，而不是某个实例的属性。例如，可以记录狗的学名（二名法名称）是 “Canis familiaris”：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog {
    public int weightInPounds;
    public static String binomen = &quot;Canis familiaris&quot;;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问静态变量时，应当使用类名，而不是具体实例。例如应写 &lt;code&gt;Dog.binomen&lt;/code&gt;，而不是 &lt;code&gt;d.binomen&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从语法上说，Java 的确允许通过实例名称访问静态变量，但这种写法风格很差、容易引起混淆；在我看来，这甚至是 Java 设计者留下的一个错误。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 1.2.2：&lt;/strong&gt; 完成以下练习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视频：&lt;a href=&quot;https://youtu.be/8Gq-8mVbyFU&quot;&gt;链接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;幻灯片：&lt;a href=&quot;https://docs.google.com/presentation/d/10BFLHH8VaoYy7XaazwjaoTtLw3zvasX4HCssDruqw84/edit#slide=id.g6caa9a6fe_057&quot;&gt;链接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;解答视频：&lt;a href=&quot;https://youtu.be/Osuy8UEH03M&quot;&gt;链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void main(String[] args)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/1ViQUHlRoyM&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;学到这里，我们终于可以拆解并说明一直用于 &lt;code&gt;main&lt;/code&gt; 方法的声明。逐部分来看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public&lt;/code&gt;：到目前为止，我们的所有方法都以这个关键字开头。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;static&lt;/code&gt;：它是静态方法，不与任何特定实例关联。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void&lt;/code&gt;：它没有返回值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main&lt;/code&gt;：这是方法名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String[] args&lt;/code&gt;：这是传递给 &lt;code&gt;main&lt;/code&gt; 方法的一个参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;命令行参数&lt;/h4&gt;
&lt;p&gt;由于 &lt;code&gt;main&lt;/code&gt; 是由 Java 解释器本身调用，而不是由另一个 Java 类调用，因此这些参数必须由解释器提供。它们通常就是命令行参数。请看下面的 &lt;code&gt;ArgsDemo&lt;/code&gt; 程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ArgsDemo {
    public static void main(String[] args) {
        System.out.println(args[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该程序会输出第 0 个命令行参数。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ java ArgsDemo these are command line arguments
these
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，&lt;code&gt;args&lt;/code&gt; 是一个字符串数组，其中各元素为 &lt;code&gt;{&quot;these&quot;, &quot;are&quot;, &quot;command&quot;, &quot;line&quot;, &quot;arguments&quot;}&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;对命令行参数求和&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;练习 1.2.3：&lt;/strong&gt; 假设所有命令行参数都是数字，尝试编写一个程序把它们相加。解答可以查看课程视频或 GitHub 上提供的代码。&lt;/p&gt;
&lt;h3&gt;使用库&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/C6kbrsdNQaQ&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;程序员最重要的能力之一，是知道如何寻找并使用现有的库。在如今这个美好的时代，很多时候只要上网寻求帮助，就能省下大量编写代码和调试的工作。&lt;/p&gt;
&lt;p&gt;本课程允许这样做，但有以下限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要使用课程没有提供的库。&lt;/li&gt;
&lt;li&gt;注明资料来源。&lt;/li&gt;
&lt;li&gt;不要搜索某道具体作业题或项目题的现成解答。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，可以搜索 “convert String integer Java”，但不可以搜索 “nbody project berkeley”。&lt;/p&gt;
&lt;p&gt;有关合作与学术诚信政策的更多信息，请参阅课程教学大纲。&lt;/p&gt;
&lt;h4&gt;接下来做什么&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/proj/proj0/proj0&quot;&gt;项目 0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/discussion/disc02.pdf&quot;&gt;讨论课 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 2 章 列表</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/02-lists</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/02-lists</guid><description>CS61B 列表中文教程，涵盖引用、递归、链表、哨兵节点、双向链表、数组列表与动态扩容策略。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;2.1 海象之谜&lt;/h2&gt;
&lt;p&gt;在项目 0 中，我们使用数组记录空间中 N 个物体的位置。模拟开始后，有一件事并不容易做到：改变物体的数量。原因是 Java 数组的大小固定，创建之后永远不能改变。&lt;/p&gt;
&lt;p&gt;另一种办法是使用列表类型。你以前一定使用过列表这种数据结构。例如在 Python 中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;L = [3, 5, 6]
L.append(7)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 确实内置了 &lt;code&gt;List&lt;/code&gt; 类型，不过现在暂时不用它。本章将从零开始构建自己的列表，并在这一过程中学习 Java 的若干关键特性。&lt;/p&gt;
&lt;h3&gt;海象之谜&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/IRwO_wahcsU&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;旅程开始时，我们先来思考深奥的“海象之谜”。&lt;/p&gt;
&lt;p&gt;尝试预测运行下面的代码会发生什么。对 &lt;code&gt;b&lt;/code&gt; 的修改会影响 &lt;code&gt;a&lt;/code&gt; 吗？提示：如果你来自 Python，Java 的行为与 Python 相同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;
b.weight = 5;
System.out.println(a);
System.out.println(b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再预测下面的代码。对 &lt;code&gt;x&lt;/code&gt; 的修改会影响 &lt;code&gt;y&lt;/code&gt; 吗？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x = 5;
int y;
y = x;
x = 2;
System.out.println(&quot;x is: &quot; + x);
System.out.println(&quot;y is: &quot; + y);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在&lt;a href=&quot;http://cscircles.cemc.uwaterloo.ca/java_visualize/#code=public+class+PollQuestions+%7B%0A+++public+static+void+main%28String%5B%5D+args%29+%7B%0A++++++Walrus+a+%3D+new+Walrus%281000,+8.3%29%3B%0A++++++Walrus+b%3B%0A++++++b+%3D+a%3B%0A++++++b.weight+%3D+5%3B%0A++++++System.out.println%28a%29%3B%0A++++++System.out.println%28b%29%3B++++++%0A%0A++++++int+x+%3D+5%3B%0A++++++int+y%3B%0A++++++y+%3D+x%3B%0A++++++x+%3D+2%3B%0A++++++System.out.println%28%22x+is%3A+%22+%2B+x%29%3B%0A++++++System.out.println%28%22y+is%3A+%22+%2B+y%29%3B++++++%0A+++%7D%0A+++%0A+++public+static+class+Walrus+%7B%0A++++++public+int+weight%3B%0A++++++public+double+tuskSize%3B%0A++++++%0A++++++public+Walrus%28int+w,+double+ts%29+%7B%0A+++++++++weight+%3D+w%3B%0A+++++++++tuskSize+%3D+ts%3B%0A++++++%7D%0A%0A++++++public+String+toString%28%29+%7B%0A+++++++++return+String.format%28%22weight%3A+%25d,+tusk+size%3A+%25.2f%22,+weight,+tuskSize%29%3B%0A++++++%7D%0A+++%7D%0A%7D&amp;#x26;mode=edit&quot;&gt;这里查看运行过程与答案&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这个问题看起来细微，但“海象之谜”背后的核心思想会极大影响本课程中各种数据结构的效率。真正理解它，也能帮助你写出更安全、更可靠的代码。&lt;/p&gt;
&lt;h3&gt;比特&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/UJlfyXRm1ts&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;计算机中的所有信息，都以一串 0 和 1 的形式保存在_内存_中。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;72 常表示为 &lt;code&gt;01001000&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;205.75 常表示为 &lt;code&gt;01000011 01001101 11000000 00000000&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;字母 H 常表示为 &lt;code&gt;01001000&lt;/code&gt;（与 72 相同）。&lt;/li&gt;
&lt;li&gt;布尔值 &lt;code&gt;true&lt;/code&gt; 常表示为 &lt;code&gt;00000001&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本课程不会花太多时间讨论具体的二进制表示。例如，为什么 205.75 会被保存成上面那串看似随机的 32 位比特。理解这些具体表示方式属于后续课程 &lt;a href=&quot;http://www-inst.eecs.berkeley.edu/~cs61c/&quot;&gt;CS61C&lt;/a&gt; 的内容。&lt;/p&gt;
&lt;p&gt;虽然我们不会深入学习二进制语言，但应当知道：计算机底层大致就是这样工作的。&lt;/p&gt;
&lt;p&gt;一个有趣的现象是，72 与 H 都保存为 &lt;code&gt;01001000&lt;/code&gt;。于是问题来了：Java 代码怎样知道应当如何解释 &lt;code&gt;01001000&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;答案是：依靠类型。来看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;char c = &apos;H&apos;;
int x = c;
System.out.println(c);
System.out.println(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;H
72
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，变量 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;c&lt;/code&gt; 保存着相同的比特（严格来说只是几乎相同），但 Java 解释器在输出它们时采用了不同的解释方式。&lt;/p&gt;
&lt;p&gt;Java 有 8 种基本类型：&lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;short&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;long&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt; 和 &lt;code&gt;char&lt;/code&gt;。它们各有不同性质，课程会陆续讨论。不过 &lt;code&gt;short&lt;/code&gt; 与 &lt;code&gt;float&lt;/code&gt; 很可能永远都用不到。&lt;/p&gt;
&lt;h3&gt;声明变量（简化模型）&lt;/h3&gt;
&lt;p&gt;可以把计算机想象成拥有海量用于保存信息的内存比特，每个比特都有唯一地址。现代计算机通常拥有数十亿甚至更多这样的比特。&lt;/p&gt;
&lt;p&gt;声明某种类型的变量时，Java 会找到一段连续的内存，其比特数量恰好足以保存该类型的值。例如，声明一个 &lt;code&gt;int&lt;/code&gt; 会得到一块 32 位空间；声明一个 &lt;code&gt;byte&lt;/code&gt; 会得到一块 8 位空间。Java 的不同数据类型占用不同数量的比特，不过具体数字在本课程中并不十分重要。&lt;/p&gt;
&lt;p&gt;为了方便比喻，我们把这样的一块比特称为一个“盒子”。&lt;/p&gt;
&lt;p&gt;除了预留内存外，Java 解释器还会在内部表格中创建一项记录，把每个变量名映射到相应盒子第一个比特的位置。&lt;/p&gt;
&lt;p&gt;例如，声明 &lt;code&gt;int x&lt;/code&gt; 和 &lt;code&gt;double y&lt;/code&gt; 后，Java 可能决定用内存中的第 352 到 383 位保存 &lt;code&gt;x&lt;/code&gt;，用第 20800 到 20863 位保存 &lt;code&gt;y&lt;/code&gt;。解释器随后会记录：&lt;code&gt;int x&lt;/code&gt; 从第 352 位开始，&lt;code&gt;y&lt;/code&gt; 从第 20800 位开始。也就是说，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x;
double y;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，会得到分别为 32 位和 64 位的两个盒子，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b78fd85fa3cb-x_and_y_empty_bitwise.png&quot; alt=&quot;空的 x 与 y 比特盒&quot;&gt;&lt;/p&gt;
&lt;p&gt;Java 语言不会让你知道这些盒子的实际位置。例如，无法通过 Java 查出 &lt;code&gt;x&lt;/code&gt; 恰好位于第 352 位。换句话说，精确内存地址低于 Java 向程序员开放的抽象层级。C 等语言则不同，它们允许程序员询问某份数据的准确地址。因此，上图没有标出地址。&lt;/p&gt;
&lt;p&gt;Java 的这一特性是一种权衡。向程序员隐藏内存位置会减少控制能力，使某些&lt;a href=&quot;http://www.informit.com/articles/article.aspx?p=2246428&amp;#x26;seqNum=5&quot;&gt;优化方式&lt;/a&gt;无法实现；但它也避免了&lt;a href=&quot;http://www.informit.com/articles/article.aspx?p=2246428&amp;#x26;seqNum=1&quot;&gt;大量极其棘手的编程错误&lt;/a&gt;。在计算资源十分廉价的现代，这种权衡通常非常值得。正如 Donald Knuth 的名言：“大约 97% 的时间里，我们都应忘掉那些微小的效率问题；过早优化是万恶之源。”&lt;/p&gt;
&lt;p&gt;打个比方，你不能直接控制自己的心跳。虽然这限制了你在某些情形下对身体进行“优化”的能力，但也避免了不小心把心跳关闭之类的愚蠢错误。&lt;/p&gt;
&lt;p&gt;声明变量时，Java 不会立刻向预留的盒子中写入任何内容。换句话说，局部变量没有默认值。因此，在通过 &lt;code&gt;=&lt;/code&gt; 运算符向盒子中填入比特之前，Java 编译器会阻止你使用该变量。正因为如此，上图的盒子里没有画任何比特。&lt;/p&gt;
&lt;p&gt;给内存盒赋值时，它会被填入你指定的比特。例如执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;x = -1431195969;
y = 567213.112;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，上面的内存盒会被填成下图所示的样子。我把这种表示方式称为&lt;strong&gt;盒子记法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0273c74a8734-x_and_y_empty_filled.png&quot; alt=&quot;填入值的 x 与 y&quot;&gt;&lt;/p&gt;
&lt;p&gt;上方的比特表示 -1431195969，下方的比特表示 567213.112。为什么这些特定比特序列能表示这两个数字并不重要，它属于 CS61C 的内容。感兴趣的话，可以阅读维基百科上的&lt;a href=&quot;https://en.wikipedia.org/wiki/Two%27s_complement&quot;&gt;整数表示&lt;/a&gt;与&lt;a href=&quot;https://en.wikipedia.org/wiki/IEEE_floating_point&quot;&gt;双精度浮点数表示&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;注意：真实的内存分配比这里描述的更加复杂，也是 CS61C 的主题。不过对于 CS61B 来说，这个模型已经足够接近现实。&lt;/p&gt;
&lt;h4&gt;简化盒子记法&lt;/h4&gt;
&lt;p&gt;上一节的盒子记法有助于大致理解底层发生的事情，但在实践中并不好用，因为我们通常不会直接解读二进制比特。&lt;/p&gt;
&lt;p&gt;因此，之后不再用二进制写出内存盒的内容，而是改用人类可读的符号。本课程余下部分都会采用这种方式。例如执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x;
double y;
x = -1431195969;
y = 567213.112;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后，可以用下面这种我称为&lt;strong&gt;简化盒子记法&lt;/strong&gt;的形式表示程序环境：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/70665a3df1b0-x_and_y_simplified_box_notation.png&quot; alt=&quot;简化盒子记法&quot;&gt;&lt;/p&gt;
&lt;h3&gt;等号黄金法则（GRoE）&lt;/h3&gt;
&lt;p&gt;有了简化盒子记法，我们终于可以开始解开海象之谜。&lt;/p&gt;
&lt;p&gt;这个谜题其实有一个简单答案：当你写出 &lt;code&gt;y = x&lt;/code&gt; 时，你是在要求 Java 解释器把 &lt;code&gt;x&lt;/code&gt; 中的比特复制到 &lt;code&gt;y&lt;/code&gt; 中。这个“等号黄金法则”（Golden Rule of Equals，简称 GRoE）是理解海象之谜时最根本的真理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x = 5;
int y;
y = x;
x = 2;
System.out.println(&quot;x is: &quot; + x);
System.out.println(&quot;y is: &quot; + y);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Java 中，任何使用 &lt;code&gt;=&lt;/code&gt; 的赋值都遵循“复制比特”这一简单规则。可以通过&lt;a href=&quot;http://cscircles.cemc.uwaterloo.ca/java_visualize/#code=public+class+PollQuestions+%7B%0A+++public+static+void+main%28String%5B%5D+args%29+%7B%0A++++++int+x+%3D+5%3B%0A++++++int+y%3B%0A++++++y+%3D+x%3B%0A++++++x+%3D+2%3B%0A++++++System.out.println%28%22x+is%3A+%22+%2B+x%29%3B%0A++++++System.out.println%28%22y+is%3A+%22+%2B+y%29%3B++++++%0A+++%7D%0A%7D&amp;#x26;mode=display&amp;#x26;curInstr=0&quot;&gt;这个可视化链接&lt;/a&gt;观察它。&lt;/p&gt;
&lt;h3&gt;引用类型&lt;/h3&gt;
&lt;p&gt;前面提到 Java 有 8 种基本类型：&lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;short&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;long&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt;。除此之外的所有类型，包括数组，都不是基本类型，而是&lt;code&gt;引用类型&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;对象实例化&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/-eUMI5o31wY&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;new&lt;/code&gt; 实例化一个对象（例如 &lt;code&gt;Dog&lt;/code&gt;、&lt;code&gt;Walrus&lt;/code&gt; 或 &lt;code&gt;Planet&lt;/code&gt;）时，Java 首先为该类的每个实例变量分配一个盒子，并填入默认值。随后，构造方法通常会为每个盒子填入其他值，不过并非所有构造方法都一定这样做。&lt;/p&gt;
&lt;p&gt;例如，假设 &lt;code&gt;Walrus&lt;/code&gt; 类如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static class Walrus {
    public int weight;
    public double tuskSize;

    public Walrus(int w, double ts) {
          weight = w;
          tuskSize = ts;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 &lt;code&gt;new Walrus(1000, 8.3);&lt;/code&gt; 创建一只海象，那么最终会得到一只由两个盒子组成的 &lt;code&gt;Walrus&lt;/code&gt;，它们分别占 32 位与 64 位：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f2e9007e6c7b-anonymous_walrus.png&quot; alt=&quot;匿名海象对象&quot;&gt;&lt;/p&gt;
&lt;p&gt;在真实的 Java 实现中，每个对象还会产生一些额外开销，因此一只 &lt;code&gt;Walrus&lt;/code&gt; 实际占用的空间会略多于 96 位。不过本课程会忽略这些开销，因为我们不会直接与它们交互。&lt;/p&gt;
&lt;p&gt;此时创建出的海象是“匿名”的：对象已经存在，却没有保存在任何变量中。下面来看保存对象的变量。&lt;/p&gt;
&lt;h4&gt;引用变量声明&lt;/h4&gt;
&lt;p&gt;声明任意引用类型的变量（如 &lt;code&gt;Walrus&lt;/code&gt;、&lt;code&gt;Dog&lt;/code&gt;、&lt;code&gt;Planet&lt;/code&gt; 或数组）时，无论对象类型是什么，Java 都会分配一个 64 位的盒子。&lt;/p&gt;
&lt;p&gt;乍看之下，这似乎产生了“海象悖论”：上一节中的 &lt;code&gt;Walrus&lt;/code&gt; 需要超过 64 位才能保存。而且无论对象是什么类型，变量竟然都只获得 64 位空间，看上去很奇怪。&lt;/p&gt;
&lt;p&gt;下面这个事实可以轻松化解问题：64 位盒子里保存的并不是海象本身的数据，而是海象对象在内存中的地址。&lt;/p&gt;
&lt;p&gt;例如，假设执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Walrus someWalrus;
someWalrus = new Walrus(1000, 8.3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一行创建一个 64 位盒子。第二行创建新的 &lt;code&gt;Walrus&lt;/code&gt;，&lt;code&gt;new&lt;/code&gt; 运算符会返回它的地址。按照 GRoE，这些地址比特随后被复制到 &lt;code&gt;someWalrus&lt;/code&gt; 的盒子中。&lt;/p&gt;
&lt;p&gt;假设海象的 &lt;code&gt;weight&lt;/code&gt; 从内存第 &lt;code&gt;5051956592385990207&lt;/code&gt; 位开始保存，&lt;code&gt;tuskSize&lt;/code&gt; 从第 &lt;code&gt;5051956592385990239&lt;/code&gt; 位开始，那么 &lt;code&gt;Walrus&lt;/code&gt; 变量中可能保存数字 &lt;code&gt;5051956592385990207&lt;/code&gt;。它的 64 位二进制表示为 &lt;code&gt;0100011000011100001001111100000100011101110111000001111000111111&lt;/code&gt;，盒子记法如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/3117296e282c-someWalrus_bit_notation.png&quot; alt=&quot;someWalrus 的比特记法&quot;&gt;&lt;/p&gt;
&lt;p&gt;引用变量还可以被赋予特殊值 &lt;code&gt;null&lt;/code&gt;，它对应全 0 的地址。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f1be3b3d3e39-someWalrus_bit_notation_null.png&quot; alt=&quot;null 引用的比特记法&quot;&gt;&lt;/p&gt;
&lt;h4&gt;盒子与指针记法&lt;/h4&gt;
&lt;p&gt;和之前一样，引用变量内部的一串比特不容易解读，因此我们为引用变量定义一种简化记法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址全部为 0 时，用 &lt;code&gt;null&lt;/code&gt; 表示。&lt;/li&gt;
&lt;li&gt;地址非 0 时，用一根指向对象实例的&lt;strong&gt;箭头&lt;/strong&gt;表示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种表示方式有时也称为“盒子与指针记法”。&lt;/p&gt;
&lt;p&gt;上一节的两个例子可以表示为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/450180c89876-someWalrus_simplified_bit_notation.png&quot; alt=&quot;指向海象的引用&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b2f137542945-someWalrus_simplified_bit_notation_null.png&quot; alt=&quot;空引用&quot;&gt;&lt;/p&gt;
&lt;h4&gt;彻底解开海象之谜&lt;/h4&gt;
&lt;p&gt;现在终于能够完整、彻底地解开海象之谜了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行第一行后，得到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c128067ea022-mystery_of_the_walrus_resolved_step1.png&quot; alt=&quot;海象之谜第 1 步&quot;&gt;&lt;/p&gt;
&lt;p&gt;执行第二行后，得到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a465a9b1e62d-mystery_of_the_walrus_resolved_step2.png&quot; alt=&quot;海象之谜第 2 步&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意，此时 &lt;code&gt;b&lt;/code&gt; 是未定义的，而不是 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;根据 GRoE，最后一行只是把 &lt;code&gt;a&lt;/code&gt; 盒子中的比特复制到 &lt;code&gt;b&lt;/code&gt; 盒子中。用图形比喻来说，&lt;code&gt;b&lt;/code&gt; 会复制 &lt;code&gt;a&lt;/code&gt; 中的那根箭头，因此两个变量最终都指向同一个对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/914600e8de67-mystery_of_the_walrus_resolved_step3.png&quot; alt=&quot;海象之谜第 3 步&quot;&gt;&lt;/p&gt;
&lt;p&gt;就这么简单，没有更多隐藏的复杂性。&lt;/p&gt;
&lt;h3&gt;参数传递&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/3hiXeB8rNKA&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;向函数传递参数时，同样只是在复制比特。换句话说，GRoE 也适用于参数传递。复制比特通常称为“按值传递”。Java &lt;strong&gt;始终&lt;/strong&gt;采用按值传递。&lt;/p&gt;
&lt;p&gt;例如，考虑下面的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double average(double a, double b) {
    return (a + b) / 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设这样调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    double x = 5.5;
    double y = 10.5;
    double avg = average(x, y);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行这个方法的前两行后，&lt;code&gt;main&lt;/code&gt; 方法的作用域中会有两个名为 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的盒子，内容如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c1796b08bcfa-main_x_y.png&quot; alt=&quot;main 中的 x 与 y&quot;&gt;&lt;/p&gt;
&lt;p&gt;调用函数时，&lt;code&gt;average&lt;/code&gt; 函数拥有&lt;strong&gt;自己的&lt;/strong&gt;作用域，其中有两个新的盒子 &lt;code&gt;a&lt;/code&gt; 与 &lt;code&gt;b&lt;/code&gt;，参数的比特只是被_复制_进去。所谓“按值传递”，指的正是这一复制过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/59e4439609b7-average_a_b.png&quot; alt=&quot;average 中的 a 与 b&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;average&lt;/code&gt; 函数修改了 &lt;code&gt;a&lt;/code&gt;，&lt;code&gt;main&lt;/code&gt; 中的 &lt;code&gt;x&lt;/code&gt; 不会改变。根据 GRoE，我们只是向名为 &lt;code&gt;a&lt;/code&gt; 的盒子中写入了新的比特。&lt;/p&gt;
&lt;h4&gt;检验理解&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/zGdNjPZxIdA&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.1.1：&lt;/strong&gt; 假设有下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class PassByValueFigure {
    public static void main(String[] args) {
           Walrus walrus = new Walrus(3500, 10.5);
           int x = 9;

           doStuff(walrus, x);
           System.out.println(walrus);
           System.out.println(x);
    }

    public static void doStuff(Walrus W, int x) {
           W.weight = W.weight - 100;
           x = x - 5;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 &lt;code&gt;doStuff&lt;/code&gt; 会影响 &lt;code&gt;walrus&lt;/code&gt; 和/或 &lt;code&gt;x&lt;/code&gt; 吗？提示：只需使用 GRoE 就能解决。&lt;/p&gt;
&lt;h3&gt;数组实例化&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/t57Xi1G__Vc&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如前所述，保存数组的变量与其他引用变量完全一样。例如下面的声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[] x;
Planet[] planets;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两条声明都会创建一个 64 位的内存盒。&lt;code&gt;x&lt;/code&gt; 只能保存 &lt;code&gt;int&lt;/code&gt; 数组的地址，&lt;code&gt;planets&lt;/code&gt; 只能保存 &lt;code&gt;Planet&lt;/code&gt; 数组的地址。&lt;/p&gt;
&lt;p&gt;实例化数组与实例化对象非常相似。例如，按照下面的方式创建一个长度为 5 的整数数组：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x = new int[]{0, 1, 2, 95, 4};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 关键字会创建 5 个各占 32 位的盒子，并返回整个数组对象的地址，再将它赋给 &lt;code&gt;x&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果丢失了表示地址的比特，对象也可能随之“丢失”。例如，若某只 &lt;code&gt;Walrus&lt;/code&gt; 的地址只有一份副本，且保存在 &lt;code&gt;x&lt;/code&gt; 中，那么执行 &lt;code&gt;x = null&lt;/code&gt; 后，就会永远失去访问这只海象的能力。这不一定是坏事，因为你经常会确认某个对象已经用完，此时直接丢弃引用完全合理。本章稍后构建列表时就会看到这种情况。&lt;/p&gt;
&lt;h4&gt;破沙发定律&lt;/h4&gt;
&lt;p&gt;你可能会问：为什么要用这么多篇幅讨论一件看起来微不足道的事？如果你以前学过 Java，这种感觉可能尤其强烈。原因在于，学生很容易对这一问题形成一种半懂不懂的认识——代码能写出来，却没有真正理解底层究竟发生了什么。&lt;/p&gt;
&lt;p&gt;短期内这也许没有问题，但从长期来看，如果总是在没有充分理解的情况下做题，后面可能会因此彻底卡住。关于这个所谓的&lt;a href=&quot;https://mathwithbaddrawings.com/2015/04/08/the-math-ceiling-wheres-your-cognitive-breaking-point/&quot;&gt;“破沙发定律”&lt;/a&gt;，有一篇值得阅读的博客文章。&lt;/p&gt;
&lt;h3&gt;IntList&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/TzuAiXTZmYo&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;真正理解海象之谜后，我们就可以构建自己的列表类了。&lt;/p&gt;
&lt;p&gt;事实证明，一个非常基础的列表实现极其简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class IntList {
    public int first;
    public IntList rest;        

    public IntList(int f, IntList r) {
        first = f;
        rest = r;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能记得在 CS61A 中见过类似结构，它称为“链表”。&lt;/p&gt;
&lt;p&gt;不过这种列表用起来很难看。例如，要创建包含 5、10、15 的列表，可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList L = new IntList(5, null);
L.rest = new IntList(10, null);
L.rest.rest = new IntList(15, null);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以反向构建列表。代码稍微整洁一些，但更难理解：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原则上，&lt;code&gt;IntList&lt;/code&gt; 可以保存任意整数列表，但最终代码会非常丑陋，而且容易出错。我们将采用面向对象编程的常见策略：向类中添加辅助方法，用来完成基本操作。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;size&lt;/code&gt; 与 &lt;code&gt;iterativeSize&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;我们希望给 &lt;code&gt;IntList&lt;/code&gt; 类添加 &lt;code&gt;size&lt;/code&gt; 方法，使调用 &lt;code&gt;L.size()&lt;/code&gt; 时能够得到 &lt;code&gt;L&lt;/code&gt; 中的元素数量。&lt;/p&gt;
&lt;p&gt;继续阅读之前，可以尝试自己编写 &lt;code&gt;size&lt;/code&gt; 和 &lt;code&gt;iterativeSize&lt;/code&gt;。&lt;code&gt;size&lt;/code&gt; 应使用递归，而 &lt;code&gt;iterativeSize&lt;/code&gt; 不使用递归。先自己尝试，再看我的做法，通常能学到更多。下面两个视频演示了可能的实现过程。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/EEaP1oC1CFU&quot;&gt;递归实现视频&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/GL9Bg3Ej_94&quot;&gt;迭代实现视频&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我的 &lt;code&gt;size&lt;/code&gt; 方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Return the size of the list using... recursion! */
public int size() {
    if (rest == null) {
        return 1;
    }
    return 1 + this.rest.size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写递归代码时，最重要的是记住必须有基本情况。在这里，最合理的基本情况是 &lt;code&gt;rest&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;，这意味着当前列表长度为 1。&lt;/p&gt;
&lt;p&gt;练习：你可能会想，为什么不写成 &lt;code&gt;if (this == null) return 0;&lt;/code&gt;？这种写法为什么不能工作？&lt;/p&gt;
&lt;p&gt;答案：想想调用 &lt;code&gt;size&lt;/code&gt; 时发生了什么。方法是通过某个对象调用的，例如 &lt;code&gt;L.size()&lt;/code&gt;。如果 &lt;code&gt;L&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;，那么调用方法时就已经产生 &lt;code&gt;NullPointerException&lt;/code&gt;，根本不可能进入方法内部。&lt;/p&gt;
&lt;p&gt;我的 &lt;code&gt;iterativeSize&lt;/code&gt; 方法如下。编写数据结构的迭代代码时，我建议把指针变量命名为 &lt;code&gt;p&lt;/code&gt;，提醒自己它保存的是一个指针。必须使用额外指针，是因为 Java 不允许重新给 &lt;code&gt;this&lt;/code&gt; 赋值。&lt;a href=&quot;https://stackoverflow.com/questions/23021377/reassign-this-in-java-class&quot;&gt;这篇 Stack Overflow 帖子&lt;/a&gt;中的后续回答对原因作了简要说明。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Return the size of the list using no recursion! */
public int iterativeSize() {
    IntList p = this;
    int totalSize = 0;
    while (p != null) {
        totalSize += 1;
        p = p.rest;
    }
    return totalSize;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;get&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/qnmxD_21DNk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;size&lt;/code&gt; 方法可以得到列表长度，但目前还没有方便的方式取得列表中的第 &lt;code&gt;i&lt;/code&gt; 个元素。&lt;/p&gt;
&lt;p&gt;练习：编写方法 &lt;code&gt;get(int i)&lt;/code&gt;，返回列表的第 &lt;code&gt;i&lt;/code&gt; 个元素。例如，若 &lt;code&gt;L&lt;/code&gt; 是 &lt;code&gt;5 -&gt; 10 -&gt; 15&lt;/code&gt;，那么 &lt;code&gt;L.get(0)&lt;/code&gt; 应返回 5，&lt;code&gt;L.get(1)&lt;/code&gt; 应返回 10，&lt;code&gt;L.get(2)&lt;/code&gt; 应返回 15。对于过大或过小的非法 &lt;code&gt;i&lt;/code&gt;，代码如何表现都无所谓。&lt;/p&gt;
&lt;p&gt;解答请参阅上面的课程视频或 &lt;code&gt;lectureCode&lt;/code&gt; 仓库。&lt;/p&gt;
&lt;p&gt;注意，我们写出的这个方法需要线性时间。也就是说，如果列表长度为 1,000,000，取得最后一个元素所需时间会远多于在短列表中取元素。后续课程会介绍另一种列表实现，从而避免这个问题。&lt;/p&gt;
&lt;h4&gt;接下来做什么&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/lab/lab2setup/lab2setup&quot;&gt;实验环境配置&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/lab/lab2/lab2&quot;&gt;实验 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2.2 SLList&lt;/h2&gt;
&lt;p&gt;在 2.1 节中，我们构建了 &lt;code&gt;IntList&lt;/code&gt; 类。从技术上说，这种列表数据结构能够完成列表应做的所有事情；但在实际使用中，&lt;code&gt;IntList&lt;/code&gt; 相当别扭，会导致代码难以阅读和维护。&lt;/p&gt;
&lt;p&gt;根本问题在于，&lt;code&gt;IntList&lt;/code&gt; 是一种我称为&lt;strong&gt;裸递归&lt;/strong&gt;的数据结构。为了正确使用 &lt;code&gt;IntList&lt;/code&gt;，程序员即便只完成简单的列表操作，也必须理解并运用递归。这限制了它对初学者的适用性；而且根据 &lt;code&gt;IntList&lt;/code&gt; 类提供了哪些辅助方法，还可能引入一整类棘手的新错误。&lt;/p&gt;
&lt;p&gt;吸取 &lt;code&gt;IntList&lt;/code&gt; 的经验后，我们将构建一个新类 &lt;code&gt;SLList&lt;/code&gt;。它与现代编程语言中程序员真正使用的列表实现更加相似。我们会通过一连串逐步改进完成它。&lt;/p&gt;
&lt;h4&gt;改进 1：重新命名&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/1Rh3AdTxcik&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上一节的 &lt;code&gt;IntList&lt;/code&gt; 类如下，辅助方法已省略：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class IntList {
    public int first;
    public IntList rest;

    public IntList(int f, IntList r) {
        first = f;
        rest = r;
    }
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步只是重命名所有内容，并删掉辅助方法。这看起来也许不像进步，但请相信我，我是专业的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class IntNode {
    public int item;
    public IntNode next;

    public IntNode(int i, IntNode n) {
        item = i;
        next = n;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;改进 2：增加一层“行政机构”&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/63GyeGDLAbw&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;既然 &lt;code&gt;IntNode&lt;/code&gt; 很难直接操作，我们创建一个独立的 &lt;code&gt;SLList&lt;/code&gt; 类，让用户只与它交互。最基本的类如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
    public IntNode first;

    public SLList(int x) {
        first = new IntNode(x, null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在已经能隐约看出 &lt;code&gt;SLList&lt;/code&gt; 更好的原因。比较创建一个只含一个元素的 &lt;code&gt;IntList&lt;/code&gt; 和创建一个只含一个元素的 &lt;code&gt;SLList&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList L1 = new IntList(5, null);
SLList L2  = new SLList(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SLList&lt;/code&gt; 向用户隐藏了末尾存在一条 &lt;code&gt;null&lt;/code&gt; 链接的实现细节。当前的 &lt;code&gt;SLList&lt;/code&gt; 还不算实用，所以先添加两个简单的热身方法：&lt;code&gt;addFirst&lt;/code&gt; 和 &lt;code&gt;getFirst&lt;/code&gt;。继续阅读之前，可以尝试自己实现。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;addFirst&lt;/code&gt; 与 &lt;code&gt;getFirst&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/inV6cAEERRI&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果理解了 2.1 节，&lt;code&gt;addFirst&lt;/code&gt; 相当直接。使用 &lt;code&gt;IntList&lt;/code&gt; 时，我们通过 &lt;code&gt;L = new IntList(5, L)&lt;/code&gt; 在开头加入元素。因此实现为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
    public IntNode first;

    public SLList(int x) {
        first = new IntNode(x, null);
    }

    /** Adds an item to the front of the list. */
    public void addFirst(int x) {
        first = new IntNode(x, first);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getFirst&lt;/code&gt; 更简单，只需返回 &lt;code&gt;first.item&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Retrieves the front item from the list. */
public int getFirst() {
    return first.item;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的 &lt;code&gt;SLList&lt;/code&gt; 类用起来容易得多。比较下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SLList L = new SLList(15);
L.addFirst(10);
L.addFirst(5);
int x = L.getFirst();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与等价的 &lt;code&gt;IntList&lt;/code&gt; 代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
int x = L.first;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两种数据结构的图形对比如下，上方是 &lt;code&gt;IntList&lt;/code&gt;，下方是 &lt;code&gt;SLList&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/87049caa1804-IntList_vs_SLList.png&quot; alt=&quot;IntList 与 SLList 对比&quot;&gt;&lt;/p&gt;
&lt;p&gt;本质上，&lt;code&gt;SLList&lt;/code&gt; 类充当了列表用户与裸递归数据结构之间的中间人。前面的 &lt;code&gt;IntList&lt;/code&gt; 版本存在一个不太理想的可能：用户可以让变量直接指向 &lt;code&gt;IntList&lt;/code&gt; 的中间位置。正如奥维德所说，&lt;a href=&quot;https://en.wikipedia.org/wiki/Semele&quot;&gt;凡人直视神明便会死去&lt;/a&gt;，因此最好让 &lt;code&gt;SLList&lt;/code&gt; 担任我们与底层结构之间的中介。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.2.1：&lt;/strong&gt; 好奇的读者可能会反驳：只要给 &lt;code&gt;IntList&lt;/code&gt; 写一个 &lt;code&gt;addFirst&lt;/code&gt; 方法，它也会同样好用。请尝试为 &lt;code&gt;IntList&lt;/code&gt; 类编写 &lt;code&gt;addFirst&lt;/code&gt;。你会发现，最终的方法既难写又低效。&lt;/p&gt;
&lt;h4&gt;改进 3：&lt;code&gt;public&lt;/code&gt; 与 &lt;code&gt;private&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/SlCnrzn_bfM&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;遗憾的是，用户仍能绕过 &lt;code&gt;SLList&lt;/code&gt;，直接接触裸数据结构的原始力量以及随之而来的危险。程序员可以不经过经过儿童测试、母亲认可的 &lt;code&gt;addFirst&lt;/code&gt; 方法，而直接修改列表，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SLList L = new SLList(15);
L.addFirst(10);
L.first.next.next = L.first.next;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bfd2fe6d846d-bad_SLList.png&quot; alt=&quot;损坏的 SLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;结果是一个包含无限循环的畸形列表。为解决这个问题，可以修改 &lt;code&gt;SLList&lt;/code&gt;，使用 &lt;code&gt;private&lt;/code&gt; 关键字声明 &lt;code&gt;first&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
    private IntNode first;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;私有变量和私有方法只能由同一个 &lt;code&gt;.java&lt;/code&gt; 文件中的代码访问；在这里就是 &lt;code&gt;SLList.java&lt;/code&gt;。因此，下面的 &lt;code&gt;SLLTroubleMaker&lt;/code&gt; 类无法编译，并会产生 &lt;code&gt;first has private access in SLList&lt;/code&gt; 错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLLTroubleMaker {
    public static void main(String[] args) {
        SLList L = new SLList(15);
        L.addFirst(10);
        L.first.next.next = L.first.next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相比之下，&lt;code&gt;SLList.java&lt;/code&gt; 文件内部的任何代码仍可以访问 &lt;code&gt;first&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;限制访问权限可能显得有点傻。毕竟，&lt;code&gt;private&lt;/code&gt; 做的似乎只是让原本能编译的程序无法编译。然而在大型软件工程项目中，&lt;code&gt;private&lt;/code&gt; 是极其宝贵的信号：它告诉最终用户，某些代码细节可以忽略，因此无需理解。相应地，&lt;code&gt;public&lt;/code&gt; 应被理解为一种承诺：这个方法可供使用，而且将&lt;strong&gt;永远&lt;/strong&gt;像现在这样工作。&lt;/p&gt;
&lt;p&gt;可以把类比成汽车。汽车具有一些 &lt;code&gt;public&lt;/code&gt; 功能，例如油门与刹车踏板；引擎盖下面则藏着它们如何工作的 &lt;code&gt;private&lt;/code&gt; 细节。燃油车的油门可能控制喷油系统，电动车的油门可能调整电池向电机输送的功率。尽管不同汽车内部实现不同，我们期望所有油门踏板表现一致。随意改变这种公开行为会让用户极度困惑，甚至造成严重事故。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创建 &lt;code&gt;public&lt;/code&gt; 成员（方法或变量）时一定要谨慎，因为你实际上是在承诺：今后永远支持它当前的行为。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;改进 4：嵌套类&lt;/h4&gt;
&lt;p&gt;目前有两个 &lt;code&gt;.java&lt;/code&gt; 文件：&lt;code&gt;IntNode&lt;/code&gt; 和 &lt;code&gt;SLList&lt;/code&gt;。不过 &lt;code&gt;IntNode&lt;/code&gt; 其实只是 &lt;code&gt;SLList&lt;/code&gt; 故事中的配角。&lt;/p&gt;
&lt;p&gt;Java 正好允许在一个类内部嵌入另一个类的声明。语法直接而自然：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
       public class IntNode {
            public int item;
            public IntNode next;
            public IntNode(int i, IntNode n) {
                item = i;
                next = n;
            }
       }

       private IntNode first; 

       public SLList(int x) {
           first = new IntNode(x, null);
       } 
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嵌套类不会对代码性能产生有意义的影响，它只是组织代码的工具。更多内容请参阅 &lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html&quot;&gt;Oracle 官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果嵌套类完全不需要使用 &lt;code&gt;SLList&lt;/code&gt; 的任何实例方法或实例变量，可以把它声明为 &lt;code&gt;static&lt;/code&gt;，如下所示。把嵌套类声明为 &lt;code&gt;static&lt;/code&gt;，意味着静态嵌套类中的方法不能访问外部类的成员。在本例中，&lt;code&gt;IntNode&lt;/code&gt; 中的任何方法都不能访问 &lt;code&gt;first&lt;/code&gt;、&lt;code&gt;addFirst&lt;/code&gt; 或 &lt;code&gt;getFirst&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
       public static class IntNode {
            public int item;
            public IntNode next;
            public IntNode(int i, IntNode n) {
                item = i;
                next = n;
            }
       }

       private IntNode first;
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以节省一点内存，因为每个 &lt;code&gt;IntNode&lt;/code&gt; 不再需要记录如何访问包围它的 &lt;code&gt;SLList&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;换个角度看，上面的 &lt;code&gt;IntNode&lt;/code&gt; 类从未使用 &lt;code&gt;SLList&lt;/code&gt; 的 &lt;code&gt;first&lt;/code&gt; 变量，也没有使用 &lt;code&gt;SLList&lt;/code&gt; 的任何方法。因此可以加上 &lt;code&gt;static&lt;/code&gt;，使 &lt;code&gt;IntNode&lt;/code&gt; 不必持有对自己“老板”的引用，从而省下一小块内存。&lt;/p&gt;
&lt;p&gt;如果这段内容显得过于技术化、不易理解，请尝试练习 2.2.2。一个简单经验法则是：&lt;em&gt;如果嵌套类不使用外部类的任何实例成员，就把它声明为静态嵌套类。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.2.2：&lt;/strong&gt; 尽可能少地删除 &lt;code&gt;static&lt;/code&gt;，使&lt;a href=&quot;https://github.com/joshhug/hug61b/tree/e1d84817521747a76f17d2ed077abab493505c3f/chap2/exercises/Government.java&quot;&gt;这个程序&lt;/a&gt;能够编译。点击链接并确认网址发生变化后，请刷新页面。开始练习前务必阅读文件顶部的注释。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;addLast()&lt;/code&gt; 与 &lt;code&gt;size()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Gxq_LSsOPNc&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为了引出剩余的改进，并展示数据结构实现中的一些常见模式，我们添加 &lt;code&gt;addLast(int x)&lt;/code&gt; 和 &lt;code&gt;size()&lt;/code&gt; 方法。建议先下载&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode/blob/master/lists2/DIY/addLastAndSize/SLList.java&quot;&gt;起始代码&lt;/a&gt;，在继续阅读前自己尝试。尤其建议尝试递归实现 &lt;code&gt;size&lt;/code&gt;，其中有一个很有意思的挑战。&lt;/p&gt;
&lt;p&gt;我会迭代实现 &lt;code&gt;addLast&lt;/code&gt;，当然也可以递归实现。思路很直接：创建指针变量 &lt;code&gt;p&lt;/code&gt;，让它沿列表移动到末尾。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Adds an item to the end of the list. */
public void addLast(int x) {
    IntNode p = first;

    /* Advance p to the end of the list. */
    while (p.next != null) {
        p = p.next;
    }
    p.next = new IntNode(x, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相对地，我会递归实现 &lt;code&gt;size&lt;/code&gt;。它与 2.1 节中为 &lt;code&gt;IntList&lt;/code&gt; 实现的 &lt;code&gt;size&lt;/code&gt; 有些相似。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IntList&lt;/code&gt; 的递归调用很直接：&lt;code&gt;return 1 + this.rest.size()&lt;/code&gt;。但对 &lt;code&gt;SLList&lt;/code&gt; 来说，这种方式没有意义，因为 &lt;code&gt;SLList&lt;/code&gt; 没有 &lt;code&gt;rest&lt;/code&gt; 变量。我们改用中间人类常见的模式：创建一个私有辅助方法，让它直接与底层裸递归数据结构交互。&lt;/p&gt;
&lt;p&gt;得到的方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Returns the size of the list starting at IntNode p. */
private static int size(IntNode p) {
    if (p.next == null) {
        return 1;
    }

    return 1 + size(p.next);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了它，就能轻松计算整个列表的长度：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int size() {
    return size(first);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个都名为 &lt;code&gt;size&lt;/code&gt; 的方法。Java 允许这样做，因为它们的参数不同。名称相同但签名不同的方法称为&lt;strong&gt;重载方法&lt;/strong&gt;。更多信息请参阅 Java 的&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/javaOO/methods.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;另一种做法是在 &lt;code&gt;IntNode&lt;/code&gt; 类本身中创建一个非静态辅助方法。两种方式都没有问题，不过我个人更喜欢让 &lt;code&gt;IntNode&lt;/code&gt; 中完全不包含方法。&lt;/p&gt;
&lt;h3&gt;改进 5：缓存&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ebUw8fhhpKc&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;思考上面写出的 &lt;code&gt;size&lt;/code&gt; 方法。假设对长度为 1,000 的列表调用 &lt;code&gt;size&lt;/code&gt; 需要 2 秒，那么对长度为 1,000,000 的列表调用它，预计需要 2,000 秒，因为计算机必须经过 1,000 倍的元素才能抵达末尾。大型列表上的 &lt;code&gt;size&lt;/code&gt; 如此缓慢是不可接受的，因为我们明明可以做得更好。&lt;/p&gt;
&lt;p&gt;可以重写 &lt;code&gt;size&lt;/code&gt;，让它无论面对多大的列表都花费相同时间。&lt;/p&gt;
&lt;p&gt;做法很简单：在 &lt;code&gt;SLList&lt;/code&gt; 中增加一个 &lt;code&gt;size&lt;/code&gt; 变量，持续记录当前长度。代码如下。保存重要数据以加快后续读取的做法，有时称为&lt;strong&gt;缓存&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
    ... /* IntNode declaration omitted. */
    private IntNode first;
    private int size;

    public SLList(int x) {
        first = new IntNode(x, null);
        size = 1;
    }

    public void addFirst(int x) {
        first = new IntNode(x, first);
        size += 1;
    }

    public int size() {
        return size;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后，无论列表多大，&lt;code&gt;size&lt;/code&gt; 都快得惊人。当然，这会让 &lt;code&gt;addFirst&lt;/code&gt; 和 &lt;code&gt;addLast&lt;/code&gt; 稍微变慢，也会略微增加类的内存占用，但代价微不足道。在这里，为长度建立缓存显然值得。&lt;/p&gt;
&lt;h4&gt;改进 6：空列表&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/GaOL52PuYcw&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;与 2.1 节中简单的 &lt;code&gt;IntList&lt;/code&gt; 相比，&lt;code&gt;SLList&lt;/code&gt; 已经具有许多优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SLList&lt;/code&gt; 的用户永远不会看到 &lt;code&gt;IntNode&lt;/code&gt; 类。
&lt;ul&gt;
&lt;li&gt;使用更简单。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addFirst&lt;/code&gt; 更高效（见练习 2.2.1）。&lt;/li&gt;
&lt;li&gt;避免 &lt;code&gt;IntList&lt;/code&gt; 用户无意犯错或故意破坏结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt; 可以比 &lt;code&gt;IntList&lt;/code&gt; 中的实现更快。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另一个自然优势是：很容易实现创建空列表的构造方法。最自然的方案是，空列表令 &lt;code&gt;first&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public SLList() {
    first = null;
    size = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遗憾的是，向空列表插入元素时，这会导致 &lt;code&gt;addLast&lt;/code&gt; 崩溃。因为 &lt;code&gt;first&lt;/code&gt; 是 &lt;code&gt;null&lt;/code&gt;，下面代码在 &lt;code&gt;while (p.next != null)&lt;/code&gt; 中访问 &lt;code&gt;p.next&lt;/code&gt; 时，会产生空指针异常。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    size += 1;
    IntNode p = first;
    while (p.next != null) {
        p = p.next;
    }

    p.next = new IntNode(x, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 2.2.3：&lt;/strong&gt; 修复 &lt;code&gt;addLast&lt;/code&gt;。起始代码在&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode/blob/master/lists2/DIY/fixAddLast/SLList.java&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;改进 6b：哨兵结点&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/HafzSAm3DPE&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;修复 &lt;code&gt;addLast&lt;/code&gt; 的一种办法是为空列表添加特殊情况：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    size += 1;

    if (first == null) {
        first = new IntNode(x, null);
        return;
    }

    IntNode p = first;
    while (p.next != null) {
        p = p.next;
    }

    p.next = new IntNode(x, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码能够工作，但应尽可能避免这样的特殊情况。人的工作记忆容量有限，因此应尽量控制复杂性。对于 &lt;code&gt;SLList&lt;/code&gt; 这种简单数据结构，特殊情况还不多；而树等更复杂的数据结构一旦充满特殊情况，会变得难看得多。&lt;/p&gt;
&lt;p&gt;更整洁但没那么直观的方案，是让所有 &lt;code&gt;SLList&lt;/code&gt; 即使为空，也具有同一种结构。可以创建一个始终存在的特殊结点，称为&lt;strong&gt;哨兵结点&lt;/strong&gt;。哨兵结点会保存一个值，但我们完全不关心这个值是什么。&lt;/p&gt;
&lt;p&gt;例如，由 &lt;code&gt;SLList L = new SLList()&lt;/code&gt; 创建的空列表如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/60ab09473272-empty_sentinelized_SLList.png&quot; alt=&quot;带哨兵的空 SLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;包含 5、10、15 的 &lt;code&gt;SLList&lt;/code&gt; 如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/fbd461ae473b-three_item_sentenlized_SLList.png&quot; alt=&quot;带哨兵的三个元素 SLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中淡紫色的 &lt;code&gt;??&lt;/code&gt; 表示我们不关心该位置的值。Java 不允许把问号放进整数变量，所以实际代码中只需选择任意值，例如 -518273、63 或其他数字。&lt;/p&gt;
&lt;p&gt;采用哨兵后，&lt;code&gt;SLList&lt;/code&gt; 不再需要空列表特殊情况，因此可以直接从 &lt;code&gt;addLast&lt;/code&gt; 中删掉相关分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    size += 1;
    IntNode p = sentinel;
    while (p.next != null) {
        p = p.next;
    }

    p.next = new IntNode(x, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，代码整洁得多。&lt;/p&gt;
&lt;h4&gt;不变量&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Y8i0MpxC2yE&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不变量是关于数据结构的一个事实；只要代码没有错误，它就保证始终成立。&lt;/p&gt;
&lt;p&gt;带哨兵结点的 &lt;code&gt;SLList&lt;/code&gt; 至少具有以下不变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sentinel&lt;/code&gt; 引用始终指向哨兵结点。&lt;/li&gt;
&lt;li&gt;如果存在第一个实际元素，它始终位于 &lt;code&gt;sentinel.next.item&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt; 变量始终等于已加入列表的实际元素总数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不变量让我们更容易对代码进行推理，也为验证代码正确性提供了明确目标。&lt;/p&gt;
&lt;p&gt;要真正体会哨兵的便利，需要亲自深入实现。项目 1 会提供大量练习。不过建议完成本书下一节后再开始项目 1。&lt;/p&gt;
&lt;h5&gt;接下来做什么&lt;/h5&gt;
&lt;p&gt;本节没有额外任务。如果你正在修读 Berkeley 的正式课程，现在可以开始实验 2。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2.3 DLList&lt;/h2&gt;
&lt;p&gt;在 2.2 节中，我们构建了 &lt;code&gt;SLList&lt;/code&gt; 类。与更早的裸递归 &lt;code&gt;IntList&lt;/code&gt; 相比，它已经好得多。本节将结束链表部分的讨论，同时开始学习数组的基础知识，为后面基于数组的列表 &lt;code&gt;AList&lt;/code&gt; 做准备。在这一过程中，我们还会揭晓上一节为什么使用 &lt;code&gt;SLList&lt;/code&gt; 这个有些别扭的名字。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;addLast&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/P1grp1MDZQo&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;回顾上一节的 &lt;code&gt;addLast(int x)&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    size += 1;
    IntNode p = sentinel;
    while (p.next != null) {
        p = p.next;
    }

    p.next = new IntNode(x, null);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法的问题是速度较慢。面对很长的列表，&lt;code&gt;addLast&lt;/code&gt; 必须从头走完整个列表，这和 2.2 节中的 &lt;code&gt;size&lt;/code&gt; 问题很相似。我们同样可以增加一个 &lt;code&gt;last&lt;/code&gt; 变量来加速：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class SLList {
    private IntNode sentinel;
    private IntNode last;
    private int size;    

    public void addLast(int x) {
        last.next = new IntNode(x, null);
        last = last.next;
        size += 1;
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 2.3.1：&lt;/strong&gt; 请观察下面表示该 &lt;code&gt;SLList&lt;/code&gt; 实现的盒子与指针图，其中包含 &lt;code&gt;last&lt;/code&gt; 指针。假设我们希望支持 &lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;getLast&lt;/code&gt; 和 &lt;code&gt;removeLast&lt;/code&gt;。图中的结构能让这三个操作都快速完成吗？如果不能，哪些操作较慢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/238e705ddcab-sllist_last_pointer.png&quot; alt=&quot;带 last 指针的 SLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.3.1 答案：&lt;/strong&gt; &lt;code&gt;addLast&lt;/code&gt; 与 &lt;code&gt;getLast&lt;/code&gt; 会很快，但 &lt;code&gt;removeLast&lt;/code&gt; 很慢。原因是删除最后一个结点后，需要更新 &lt;code&gt;last&lt;/code&gt;，但当前结构无法方便地找到倒数第二个结点。&lt;/p&gt;
&lt;h3&gt;倒数第二个结点&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/qqekL43M8FU&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;练习 2.3.1 中结构的问题在于：删除列表最后一个元素的方法必然很慢。我们必须先找到倒数第二个元素，再把它的 &lt;code&gt;next&lt;/code&gt; 指针设为 &lt;code&gt;null&lt;/code&gt;。增加一个 &lt;code&gt;secondToLast&lt;/code&gt; 指针也无济于事，因为删除最后一个元素后，又必须找到倒数第三个元素，才能让 &lt;code&gt;secondToLast&lt;/code&gt; 和 &lt;code&gt;last&lt;/code&gt; 继续满足相应不变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.3.2：&lt;/strong&gt; 设计一种方案，使 &lt;code&gt;removeLast&lt;/code&gt; 无论列表多长都能在常数时间内完成。暂时不必写代码，项目 1 会完成实现。只需要思考应如何修改列表的结构，也就是它的实例变量。&lt;/p&gt;
&lt;p&gt;答案将在“改进 7”中说明。&lt;/p&gt;
&lt;h3&gt;改进 7：向后看&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/BspFdzVvYe8&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;解决问题最自然的方法，是为每个 &lt;code&gt;IntNode&lt;/code&gt; 添加一个指向前驱结点的指针：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class IntNode {
    public IntNode prev;
    public int item;
    public IntNode next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，现在每个结点都有两条链接。这种列表通常称为“双向链表”，简称 &lt;code&gt;DLList&lt;/code&gt;。与之相对，2.2 节中只有单向链接的列表称为“单向链表”，简称 &lt;code&gt;SLList&lt;/code&gt;。这就是之前那个名称的由来。&lt;/p&gt;
&lt;p&gt;增加额外指针会提高代码复杂度。这里不逐行带你实现，而是留到项目 1 中自行构建双向链表。下面的盒子与指针图分别展示长度为 0 和长度为 2 的双向链表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/52bbc4704e9a-dllist_basic_size_0.png&quot; alt=&quot;长度为 0 的基本 DLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/570dc348872f-dllist_basic_size_2.png&quot; alt=&quot;长度为 2 的基本 DLList&quot;&gt;&lt;/p&gt;
&lt;h3&gt;改进 8：升级哨兵&lt;/h3&gt;
&lt;p&gt;后向指针使列表能够在常数时间内完成首尾两端的添加、读取与删除。不过这种设计有一个微妙问题：&lt;code&gt;last&lt;/code&gt; 有时指向哨兵结点，有时又指向真实结点。和没有哨兵的 &lt;code&gt;SLList&lt;/code&gt; 一样，这会产生特殊情况，使代码比经过第 8 次、也是最后一次改进后的代码丑陋得多。（你能想到 &lt;code&gt;DLList&lt;/code&gt; 中哪些方法会出现特殊情况吗？）&lt;/p&gt;
&lt;p&gt;一种修复方式是在列表末尾再添加第二个哨兵结点。结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/39e64727b87c-dllist_double_sentinel_size_0.png&quot; alt=&quot;双哨兵且长度为 0 的 DLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ce19fa0015fe-dllist_double_sentinel_size_2.png&quot; alt=&quot;双哨兵且长度为 2 的 DLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;另一种办法是把列表实现为环形结构，让首尾指针共享同一个哨兵结点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2e81170887ad-dllist_circular_sentinel_size_0.png&quot; alt=&quot;环形哨兵且长度为 0 的 DLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1d3b67618b1e-dllist_circular_sentinel_size_2.png&quot; alt=&quot;环形哨兵且长度为 2 的 DLList&quot;&gt;&lt;/p&gt;
&lt;p&gt;双哨兵和环形哨兵两种方案都能工作，而且都能消除难看的特殊情况。我个人认为环形方案更加整洁、也更美观。这里不讨论具体实现细节，因为项目 1 会让你亲自探索其中一种或两种方案。&lt;/p&gt;
&lt;h3&gt;泛型 DLList&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Xt4dKEUokz4&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当前的 &lt;code&gt;DLList&lt;/code&gt; 有一个严重限制：只能保存整数。例如，假设希望创建一个字符串列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DLList d2 = new DLList(&quot;hello&quot;);
d2.addLast(&quot;world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码会失败，因为 &lt;code&gt;DLList&lt;/code&gt; 的构造方法和 &lt;code&gt;addLast&lt;/code&gt; 只接受整数参数。&lt;/p&gt;
&lt;p&gt;幸运的是，Java 的设计者在 2004 年为语言加入了&lt;strong&gt;泛型&lt;/strong&gt;。泛型的用途之一，就是让我们创建能够保存任意引用类型的数据结构。&lt;/p&gt;
&lt;p&gt;泛型语法一开始可能有点奇怪。基本思想是：在类声明的类名后面，用尖括号 &lt;code&gt;&amp;#x3C;&gt;&lt;/code&gt; 包住一个任意的类型占位符。之后凡是需要使用这个任意类型的地方，就写这个占位符。&lt;/p&gt;
&lt;p&gt;原来的 &lt;code&gt;DLList&lt;/code&gt; 声明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DLList {
    private IntNode sentinel;
    private int size;

    public class IntNode {
        public IntNode prev;
        public int item;
        public IntNode next;
        ...
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能够保存任意类型的泛型 &lt;code&gt;DLList&lt;/code&gt; 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DLList&amp;#x3C;BleepBlorp&gt; {
    private IntNode sentinel;
    private int size;

    public class IntNode {
        public IntNode prev;
        public BleepBlorp item;
        public IntNode next;
        ...
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;BleepBlorp&lt;/code&gt; 只是我随口编出的名称。你几乎可以换成任意其他名称，例如 &lt;code&gt;GloopGlop&lt;/code&gt;、&lt;code&gt;Horse&lt;/code&gt;、&lt;code&gt;TelbudorphMulticulus&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;定义好泛型版本的 &lt;code&gt;DLList&lt;/code&gt; 后，实例化时也必须使用特殊语法。声明变量时，把实际需要的类型写在尖括号中；实例化时使用空尖括号，也称“菱形运算符”。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DLList&amp;#x3C;String&gt; d2 = new DLList&amp;#x3C;&gt;(&quot;hello&quot;);
d2.addLast(&quot;world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;泛型只适用于引用类型，因此尖括号中不能写 &lt;code&gt;int&lt;/code&gt; 或 &lt;code&gt;double&lt;/code&gt; 等基本类型，例如不能写 &lt;code&gt;&amp;#x3C;int&gt;&lt;/code&gt;。必须使用相应的引用类型包装类。&lt;code&gt;int&lt;/code&gt; 对应 &lt;code&gt;Integer&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DLList&amp;#x3C;Integer&gt; d1 = new DLList&amp;#x3C;&gt;(5);
d1.insertFront(10);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;泛型还有更多细节，不过等你亲自使用一段时间后，本书后续章节再进行讨论。现在先遵循以下经验法则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在&lt;strong&gt;实现&lt;/strong&gt;数据结构的 &lt;code&gt;.java&lt;/code&gt; 文件中，只在文件顶部的类名后写一次泛型类型名称。&lt;/li&gt;
&lt;li&gt;在使用该数据结构的其他 &lt;code&gt;.java&lt;/code&gt; 文件中，声明变量时写出实际类型，实例化时使用空的菱形运算符。&lt;/li&gt;
&lt;li&gt;如果需要以基本类型为元素类型实例化泛型，请使用 &lt;code&gt;Integer&lt;/code&gt;、&lt;code&gt;Double&lt;/code&gt;、&lt;code&gt;Character&lt;/code&gt;、&lt;code&gt;Boolean&lt;/code&gt;、&lt;code&gt;Long&lt;/code&gt;、&lt;code&gt;Short&lt;/code&gt;、&lt;code&gt;Byte&lt;/code&gt; 或 &lt;code&gt;Float&lt;/code&gt;，而不是对应的基本类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个小细节：实例化时也可以在尖括号中再次写出类型。只要同一行也在声明变量，这并非必要。所以下面这行代码完全合法，只是右侧的 &lt;code&gt;Integer&lt;/code&gt; 属于重复信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DLList&amp;#x3C;Integer&gt; d1 = new DLList&amp;#x3C;Integer&gt;(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，你已经掌握项目 1 中实现 &lt;code&gt;LinkedListDeque&lt;/code&gt; 所需的全部知识。该项目会进一步巩固 2.1、2.2 和 2.3 节的所有内容。&lt;/p&gt;
&lt;h4&gt;接下来做什么&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/proj/proj1a/proj1a&quot;&gt;项目 1&lt;/a&gt; 的第一部分：实现 &lt;code&gt;LinkedListDeque.java&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2.4 数组&lt;/h2&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/0EXjvucFV6I&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;到目前为止，我们已经看到如何利用递归类定义创建可以扩展的列表类，包括 &lt;code&gt;IntList&lt;/code&gt;、&lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;DLList&lt;/code&gt;。接下来的两节将讨论如何使用数组构建列表类。&lt;/p&gt;
&lt;p&gt;本节默认你已经使用过数组，并不打算成为一份完整的数组语法指南。&lt;/p&gt;
&lt;h3&gt;数组基础&lt;/h3&gt;
&lt;p&gt;为了最终构建能够保存信息的列表，我们需要一种取得内存盒的方法。此前已经看到，可以通过变量声明和类实例化获得内存盒。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int x;&lt;/code&gt; 会得到一个保存 &lt;code&gt;int&lt;/code&gt; 的 32 位内存盒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Walrus w1;&lt;/code&gt; 会得到一个保存 &lt;code&gt;Walrus&lt;/code&gt; 引用的 64 位内存盒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Walrus w2 = new Walrus(30, 5.6);&lt;/code&gt; 总共会得到 3 个内存盒：一个保存 &lt;code&gt;Walrus&lt;/code&gt; 引用的 64 位盒，一个保存海象整数大小的 32 位盒，以及一个保存海象 &lt;code&gt;double tuskSize&lt;/code&gt; 的 64 位盒。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数组是一种特殊对象，由一串带编号的内存盒组成。类实例中的内存盒有名称，而数组中的内存盒则使用编号。要取得数组的第 &lt;code&gt;i&lt;/code&gt; 个元素，可以像 HW0 和项目 0 中那样使用方括号记法，例如用 &lt;code&gt;A[i]&lt;/code&gt; 取得 &lt;code&gt;A&lt;/code&gt; 的第 &lt;code&gt;i&lt;/code&gt; 个元素。&lt;/p&gt;
&lt;p&gt;数组由以下内容组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个固定的整数长度 N。&lt;/li&gt;
&lt;li&gt;N 个内存盒（N = &lt;code&gt;length&lt;/code&gt;），所有盒子的类型相同，编号从 0 到 N - 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与类不同，数组本身没有方法。&lt;/p&gt;
&lt;h4&gt;创建数组&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/nToxEh2PO48&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;创建数组有三种合法写法。可以运行下面的代码观察结果，也可以打开&lt;a href=&quot;http://pythontutor.com/iframe-embed.html#code=public%20class%20ArrayCreationDemo%20%7B%0A%20%20public%20static%20void%20main%28String%5B%5D%20args%29%20%7B%0A%20%20%20%20int%5B%5D%20x%3B%0A%20%20%20%20int%5B%5D%20y%3B%0A%20%20%20%20x%20%3D%20new%20int%5B3%5D%3B%0A%20%20%20%20y%20%3D%20new%20int%5B%5D%7B1,%202,%203,%204,%205%7D%3B%0A%20%20%20%20int%5B%5D%20z%20%3D%20%7B9,%2010,%2011,%2012,%2013%7D%3B%0A%09%7D%0A%7D&amp;#x26;codeDivHeight=400&amp;#x26;codeDivWidth=350&amp;#x26;cumulative=false&amp;#x26;curInstr=0&amp;#x26;heapPrimitives=false&amp;#x26;origin=opt-frontend.js&amp;#x26;py=java&amp;#x26;rawInputLstJSON=%5B%5D&amp;#x26;textReferences=false&quot;&gt;交互式可视化&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x = new int[3];&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y = new int[]{1, 2, 3, 4, 5};&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int[] z = {9, 10, 11, 12, 13};&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三种写法都会创建数组。&lt;/p&gt;
&lt;p&gt;第一种写法用于创建 &lt;code&gt;x&lt;/code&gt;：它会创建指定长度的数组，并用默认值填充每个内存盒。在这里会创建长度为 3 的数组，三个元素都被填入 &lt;code&gt;int&lt;/code&gt; 的默认值 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第二种写法用于创建 &lt;code&gt;y&lt;/code&gt;：数组大小会恰好容纳给出的初始值。因此这里创建长度为 5 的数组，内容正是指定的五个元素。&lt;/p&gt;
&lt;p&gt;第三种写法同时&lt;strong&gt;声明并创建&lt;/strong&gt; &lt;code&gt;z&lt;/code&gt;，行为与第二种相同。唯一差别是省略了 &lt;code&gt;new&lt;/code&gt;，而且只能在声明变量的同时使用。&lt;/p&gt;
&lt;p&gt;三种写法没有绝对优劣。&lt;/p&gt;
&lt;h4&gt;访问与修改数组&lt;/h4&gt;
&lt;p&gt;下面的代码展示了操作数组时会用到的全部关键语法。建议逐行执行，确认自己理解每一行发生的事情。可以打开&lt;a href=&quot;https://goo.gl/bertuh&quot;&gt;交互式可视化&lt;/a&gt;。除最后一行外，这些语法此前都已经见过。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[] z = null;
int[] x, y;

x = new int[]{1, 2, 3, 4, 5};
y = x;
x = new int[]{-1, 2, 5, 4, 99};
y = new int[3];
z = new int[0];
int xL = x.length;

String[] s = new String[6];
s[4] = &quot;ketchup&quot;;
s[x[3] - x[1]] = &quot;muffins&quot;;

int[] b = {9, 10, 11};
System.arraycopy(b, 0, x, 3, 2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后一行展示了一种把信息从一个数组复制到另一个数组的方法。&lt;code&gt;System.arraycopy&lt;/code&gt; 接受五个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源数组。&lt;/li&gt;
&lt;li&gt;从源数组的哪个位置开始。&lt;/li&gt;
&lt;li&gt;目标数组。&lt;/li&gt;
&lt;li&gt;从目标数组的哪个位置开始。&lt;/li&gt;
&lt;li&gt;要复制多少个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于熟悉 Python 的同学，&lt;code&gt;System.arraycopy(b, 0, x, 3, 2)&lt;/code&gt; 等价于 Python 中的 &lt;code&gt;x[3:5] = b[0:2]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;复制数组的另一种方式是使用循环。&lt;code&gt;arraycopy&lt;/code&gt; 通常比循环更快，代码也更紧凑；唯一缺点是它可能更难读。注意，Java 数组只在运行时进行边界检查。也就是说，下面的代码能够正常编译，却会在运行时崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[] x = {9, 10, 11, 12, 13};
int[] y = new int[2];
int i = 0;
while (i &amp;#x3C; x.length) {
    y[i] = x[i];
    i += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请在本地 Java 文件或&lt;a href=&quot;https://goo.gl/YHufJ6&quot;&gt;可视化工具&lt;/a&gt;中运行。程序崩溃时遇到的错误叫什么？这个错误名称是否合理？&lt;/p&gt;
&lt;h4&gt;Java 中的二维数组&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/VgKAzTxISPc&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Java 中所谓的二维数组，其实只是“数组的数组”。它们遵循我们已经学过的普通对象规则，不过这里再复习一次，确保理解其工作方式。&lt;/p&gt;
&lt;p&gt;数组的数组在语法上可能令人困惑。考虑 &lt;code&gt;int[][] bamboozle = new int[4][]&lt;/code&gt;。它创建一个名为 &lt;code&gt;bamboozle&lt;/code&gt; 的整数数组的数组。更准确地说，它创建恰好四个内存盒，每个盒子都可以指向一个长度尚未指定的整数数组。&lt;/p&gt;
&lt;p&gt;逐行运行下面的代码，看看结果是否符合直觉。可以打开&lt;a href=&quot;http://goo.gl/VS4cOK&quot;&gt;交互式可视化&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[][] pascalsTriangle;
pascalsTriangle = new int[4][];
int[] rowZero = pascalsTriangle[0];

pascalsTriangle[0] = new int[]{1};
pascalsTriangle[1] = new int[]{1, 1};
pascalsTriangle[2] = new int[]{1, 2, 1};
pascalsTriangle[3] = new int[]{1, 3, 3, 1};
int[] rowTwo = pascalsTriangle[2];
rowTwo[1] = -5;

int[][] matrix;
matrix = new int[4][];
matrix = new int[4][4];

int[][] pascalAgain = new int[][]{{1}, {1, 1},
                                 {1, 2, 1}, {1, 3, 3, 1}};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 2.4.1：&lt;/strong&gt; 运行下面的代码后，&lt;code&gt;x[0][0]&lt;/code&gt; 与 &lt;code&gt;w[0][0]&lt;/code&gt; 分别是什么？可以通过&lt;a href=&quot;http://goo.gl/fCZ9Dr&quot;&gt;这里&lt;/a&gt;检查答案。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[][] x = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

int[][] z = new int[3][];
z[0] = x[0];
z[1] = x[1];
z[2] = x[2];
z[0][0] = -z[0][0];

int[][] w = new int[3][3];
System.arraycopy(x[0], 0, w[0], 0, 3);
System.arraycopy(x[1], 0, w[1], 0, 3);
System.arraycopy(x[2], 0, w[2], 0, 3);
w[0][0] = -w[0][0];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数组与类&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/WMfYfCfuwNs&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;数组与类都可以用来组织一组内存盒。在两种情况下，内存盒数量都是固定的：数组长度无法改变，就像类中的字段不能在运行时随意增加或删除一样。&lt;/p&gt;
&lt;p&gt;数组内存盒与类内存盒的关键区别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组中的盒子使用编号，并通过 &lt;code&gt;[]&lt;/code&gt; 访问；类中的盒子具有名称，并通过点号访问。&lt;/li&gt;
&lt;li&gt;数组中的所有盒子必须是相同类型；类中的盒子可以具有不同类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些差异产生的一个重要影响是：使用 &lt;code&gt;[]&lt;/code&gt; 时，可以在运行时决定要访问哪个下标。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int indexOfInterest = askUserForInteger();
int[] x = {100, 101, 102, 103};
int k = x[indexOfInterest];
System.out.println(k);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行时可能得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac arrayDemo
$ java arrayDemo
What index do you want? 2
102
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相比之下，我们通常不能在运行时动态指定类字段。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String fieldOfInterest = &quot;mass&quot;;
Planet p = new Planet(6e24, &quot;earth&quot;);
double mass = p[fieldOfInterest];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试编译会得到语法错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac classDemo
FieldDemo.java:5: error: array required, but Planet found
        double mass = earth[fieldOfInterest];        
                               ^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改用点号记法也有同样问题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String fieldOfInterest = &quot;mass&quot;;
Planet p = new Planet(6e24, &quot;earth&quot;);
double mass = p.fieldOfInterest;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ javac classDemo
FieldDemo.java:5: error: cannot find symbol
        double mass = earth.fieldOfInterest;        
                           ^
  symbol:   variable fieldOfInterest
  location: variable earth of type Planet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际编程中不常遇到这一限制，但为了知识完整，值得指出。Java 确实有一种在运行时指定字段的机制，称为_反射_，不过在普通程序中使用反射通常被视为非常糟糕的代码风格。可以在&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html&quot;&gt;这里&lt;/a&gt;了解更多。&lt;strong&gt;任何 CS61B 程序都绝对不应使用反射&lt;/strong&gt;，课程也不会讨论它。&lt;/p&gt;
&lt;p&gt;一般来说，编程语言的设计目标之一，就是限制程序员的某些选择，从而让代码更容易推理。把这类能力限制在专门的反射 API 中，可以使普通 Java 程序更容易阅读与理解。&lt;/p&gt;
&lt;h4&gt;附录：Java 数组与其他语言的数组&lt;/h4&gt;
&lt;p&gt;与其他语言中的数组相比，Java 数组：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有“切片”的专门语法（Python 有）。&lt;/li&gt;
&lt;li&gt;不能缩小或扩展（Ruby 的数组可以）。&lt;/li&gt;
&lt;li&gt;没有成员方法（JavaScript 的数组有）。&lt;/li&gt;
&lt;li&gt;只能保存同一种类型的值（Python 列表没有这一限制）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2.5 AList&lt;/h2&gt;
&lt;p&gt;本节将构建一个名为 &lt;code&gt;AList&lt;/code&gt; 的新类。它与 &lt;code&gt;DLList&lt;/code&gt; 类似，可以保存任意长度的数据列表；不同之处在于，&lt;code&gt;AList&lt;/code&gt; 使用数组而不是链表保存数据。&lt;/p&gt;
&lt;h4&gt;链表性能谜题&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/lrK07ed_Yqo&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;假设要为 &lt;code&gt;DLList&lt;/code&gt; 编写一个新方法 &lt;code&gt;int get(int i)&lt;/code&gt;。与 &lt;code&gt;getLast&lt;/code&gt; 相比，为什么长列表上的 &lt;code&gt;get&lt;/code&gt; 会很慢？哪些输入会尤其慢？&lt;/p&gt;
&lt;p&gt;下面的图可能有助于思考：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1d3b67618b1e-dllist_circular_sentinel_size_2.png&quot; alt=&quot;环形哨兵 DLList&quot;&gt;&lt;/p&gt;
&lt;h4&gt;链表性能谜题答案&lt;/h4&gt;
&lt;p&gt;事实证明，如果使用 2.3 节描述的双向链表结构，那么无论设计得多么聪明，&lt;code&gt;get&lt;/code&gt; 通常都会比 &lt;code&gt;getBack&lt;/code&gt; 慢。&lt;/p&gt;
&lt;p&gt;原因是我们只持有列表第一个和最后一个元素的引用。要取得中间某个元素，始终必须从前端或后端沿着列表逐个移动。例如，要取得长度为 10,000 的列表中编号 417 的元素，就必须经过 417 条向前链接。&lt;/p&gt;
&lt;p&gt;最坏情况下，目标元素恰好位于正中间，需要经过与列表长度成比例的元素数量，具体约为总元素数的一半。也就是说，&lt;code&gt;get&lt;/code&gt; 的最坏运行时间与整个列表的大小呈线性关系。相比之下，&lt;code&gt;getBack&lt;/code&gt; 无论列表多大都只需要常数时间。课程后面会使用大 O 和大 Θ 记号正式定义运行时间；现在先采用直观理解。&lt;/p&gt;
&lt;h4&gt;第一次尝试：朴素的数组列表&lt;/h4&gt;
&lt;p&gt;在现代计算机上，访问数组的第 &lt;code&gt;i&lt;/code&gt; 个元素只需要常数时间。因此，基于数组的列表在 &lt;code&gt;get&lt;/code&gt; 操作上应当比链表实现快得多，因为它可以直接通过方括号取得目标元素。&lt;/p&gt;
&lt;p&gt;想知道数组访问&lt;strong&gt;为什么&lt;/strong&gt;是常数时间，可以阅读&lt;a href=&quot;https://www.quora.com/Why-does-accessing-an-array-element-take-constant-time&quot;&gt;这篇 Quora 回答&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可选练习 2.5.1：&lt;/strong&gt; 尝试构建一个支持 &lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;getLast&lt;/code&gt;、&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt; 的 &lt;code&gt;AList&lt;/code&gt; 类。它应能够处理长度不超过 100 的任意列表。起始代码见 &lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode/tree/master/lists4/DIY&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/2FmIfaHl2G4&quot;&gt;解答视频&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode/tree/master/lists4/naive&quot;&gt;我的解答&lt;/a&gt;具有以下几个方便的不变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下一个通过 &lt;code&gt;addLast&lt;/code&gt; 插入的元素位置始终是 &lt;code&gt;size&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AList&lt;/code&gt; 中的元素数量始终是 &lt;code&gt;size&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;列表最后一个元素的位置始终是 &lt;code&gt;size - 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他解法可能略有不同。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;removeLast&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;最后还需要支持 &lt;code&gt;removeLast&lt;/code&gt;。开始之前，先做一个关键观察：对列表进行的任何改变，都必须体现为实现中一个或多个内存盒的改变。&lt;/p&gt;
&lt;p&gt;这看似显而易见，却有更深含义。“列表”是抽象概念，而 &lt;code&gt;size&lt;/code&gt;、&lt;code&gt;items&lt;/code&gt; 和 &lt;code&gt;items[i]&lt;/code&gt; 这些内存盒则是这一概念的具体表示。用户通过我们提供的抽象操作（&lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;removeLast&lt;/code&gt;）改变列表时，必须以符合用户预期的方式修改这些内存盒。不变量为我们指明了应当怎样修改。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ptILllgNNGo&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可选练习 2.5.2：&lt;/strong&gt; 尝试编写 &lt;code&gt;removeLast&lt;/code&gt;。开始前先判断：为了在操作后继续满足不变量，&lt;code&gt;size&lt;/code&gt;、&lt;code&gt;items&lt;/code&gt; 和 &lt;code&gt;items[i]&lt;/code&gt; 中哪些需要改变？换句话说，怎样修改才能保证用户以后调用方法时仍得到预期行为？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/WK1Fg-bNoU8&quot;&gt;解答视频&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;朴素的数组扩容&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;可选练习 2.5.3：&lt;/strong&gt; 假设 &lt;code&gt;AList&lt;/code&gt; 处于下图所示状态。调用 &lt;code&gt;addLast(11)&lt;/code&gt; 会发生什么？应当如何处理？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/23e5e7f72dda-full_naive_alist.png&quot; alt=&quot;已满的朴素 AList&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/IonBhRlyIPk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 Java 中，答案是直接创建一个足够大的新数组来容纳新数据。例如，可以这样添加新元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int[] a = new int[size + 1];
System.arraycopy(items, 0, a, 0, size);
a[size] = 11;
items = a;
size = size + 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建新数组并把元素复制过去的过程通常称为“调整大小”或“扩容”。严格来说，这个名称并不准确，因为原数组的大小并没有改变；我们只是创建了一个尺寸更大的&lt;strong&gt;新&lt;/strong&gt;数组。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.5.4：&lt;/strong&gt; 尝试实现能够通过数组扩容工作的 &lt;code&gt;addLast(int i)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/tLcinQx5VnY&quot;&gt;解答视频&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;分析朴素扩容数组&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/oysadh63NxY&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;上一节尝试的方案性能非常糟糕。进行一个简单计算实验，调用 &lt;code&gt;addLast&lt;/code&gt; 100,000 次：&lt;code&gt;SLList&lt;/code&gt; 完成得快到几乎无法计时，而基于数组的朴素列表却需要数秒。&lt;/p&gt;
&lt;p&gt;要理解原因，请思考下面的练习。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.5.5：&lt;/strong&gt; 假设当前数组长度为 100。连续调用两次 &lt;code&gt;insertBack&lt;/code&gt;，整个过程中总共需要创建并填充多少个内存盒？假设一旦数组的最后一个引用丢失，垃圾回收便立刻发生，那么任意一个时刻最多同时存在多少个内存盒？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/pFWS1pGVn9w&quot;&gt;解答视频&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 2.5.6：&lt;/strong&gt; 从长度为 100 的数组开始，如果调用 &lt;code&gt;addLast&lt;/code&gt; 1,000 次，大约需要创建并填充多少个内存盒？&lt;/p&gt;
&lt;p&gt;创建大量内存盒并反复复制内容需要时间。下图展示总时间与操作次数的关系：上方是 &lt;code&gt;SLList&lt;/code&gt;，下方是朴素数组列表。&lt;code&gt;SLList&lt;/code&gt; 的曲线是一条直线，说明每次 &lt;code&gt;add&lt;/code&gt; 都增加相同的时间，也就是单次操作为常数时间。也可以这样理解：图像是线性的，因为常数的积分是一条直线。&lt;/p&gt;
&lt;p&gt;相比之下，朴素数组列表的图像是抛物线，说明每次操作需要线性时间，因为直线的积分是抛物线。这在现实中影响巨大。插入 100,000 个元素时，可以通过 N²/N 粗略计算时间倍率：数组列表需要 &lt;code&gt;(100,000²) / 100,000&lt;/code&gt;，也就是大约 100,000 倍的时间。这显然无法接受。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/59248367dc9e-insert_experiment.png&quot; alt=&quot;插入性能实验&quot;&gt;&lt;/p&gt;
&lt;h4&gt;几何扩容&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/8WtcaXATB-Y&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以通过让数组大小按乘法比例增长，而不是按加法增长，来解决性能问题。也就是说，不再每次&lt;strong&gt;增加&lt;/strong&gt;固定数量 &lt;code&gt;RFACTOR&lt;/code&gt; 的内存盒：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void insertBack(int x) {
    if (size == items.length) {
           resize(size + RFACTOR);
    }
    items[size] = x;
    size += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而是把内存盒数量&lt;strong&gt;乘以&lt;/strong&gt; &lt;code&gt;RFACTOR&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void insertBack(int x) {
    if (size == items.length) {
           resize(size * RFACTOR);
    }
    items[size] = x;
    size += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重复之前的计算实验后，新 &lt;code&gt;AList&lt;/code&gt; 完成 100,000 次插入所需时间短到几乎察觉不到。为什么这种方法如此高效，将留到本书最后一章进行完整分析。&lt;/p&gt;
&lt;h4&gt;内存性能&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;AList&lt;/code&gt; 几乎完成了，但还存在一个重要问题。假设先插入 1,000,000,000 个元素，之后删除 990,000,000 个。此时只使用 10,000,000 个内存盒，另外 99% 的空间完全闲置。&lt;/p&gt;
&lt;p&gt;为解决这一问题，当数组显得过于空时，也可以缩小它。具体来说，定义“使用率” R，等于列表的 &lt;code&gt;size&lt;/code&gt; 除以 &lt;code&gt;items&lt;/code&gt; 数组的长度。例如，下图中的使用率是 0.04。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1b6a8bbf02af-usage_ratio.png&quot; alt=&quot;使用率&quot;&gt;&lt;/p&gt;
&lt;p&gt;典型实现中，当 R 低于 0.25 时，把数组长度减半。&lt;/p&gt;
&lt;h4&gt;泛型 AList&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Qcrd80To-TM&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;和之前一样，可以修改 &lt;code&gt;AList&lt;/code&gt;，使它能够保存任意数据类型，而不仅是整数。仍然在类声明中使用尖括号语法，并在适当位置用任意类型参数替换整数类型。下面使用 &lt;code&gt;Glorp&lt;/code&gt; 作为类型参数。&lt;/p&gt;
&lt;p&gt;这里有一个重要的语法差异：由于 Java 泛型实现方式中的一个隐晦问题，Java 不允许直接创建泛型对象数组。也就是说，不能写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Glorp[] items = new Glorp[8];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必须使用下面这种不太自然的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Glorp[] items = (Glorp []) new Object[8];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会产生编译警告，但只能接受。后续章节会更详细地讨论这个问题。&lt;/p&gt;
&lt;p&gt;另一个变化是：对于被“删除”的元素，要把相应位置设为 &lt;code&gt;null&lt;/code&gt;。以前保存整数时，没有必要把已删除位置清零；但保存泛型对象时，需要清除对对象的引用，以避免“对象滞留”。回忆一下，Java 只会在对象的最后一个引用消失后销毁它。如果没有把这个引用设为 &lt;code&gt;null&lt;/code&gt;，Java 就不会垃圾回收曾经加入列表、但逻辑上已经删除的对象。&lt;/p&gt;
&lt;p&gt;这是一个微妙的性能错误，除非专门检查，否则可能很难观察到；但在某些情况下，它会造成严重的内存浪费。&lt;/p&gt;
&lt;h3&gt;接下来做什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/discussion/disc03.pdf&quot;&gt;讨论课 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/lab/lab2/lab2&quot;&gt;实验 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://sp19.datastructur.es/materials/proj/proj1a/proj1a&quot;&gt;项目 1A&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 3 章 测试</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/03-testing</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/03-testing</guid><description>CS61B 测试中文教程，介绍 JUnit、单元测试、测试驱动开发、异常测试以及调试程序的基本方法。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;3.1 一种新的方式&lt;/h2&gt;
&lt;p&gt;中高级程序员最重要的能力之一，是判断自己的代码是否正确。本章将讨论如何编写测试来评估代码正确性。在这一过程中，还会介绍一种名为选择排序的排序算法。&lt;/p&gt;
&lt;h3&gt;一种新的方式&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/wSuZmP-WE44&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;编写程序时，代码中可能存在错误。在课堂环境中，你通常通过用户交互、代码分析和自动评分器测试的某种组合来建立对代码正确性的信心。其中自动评分器往往最重要，尤其是因为它直接决定你能获得多少分。&lt;/p&gt;
&lt;p&gt;当然，自动评分器并不是魔法。它只是教师编写的代码，本质上与你写的代码没有太大不同。在现实世界中，这些测试通常由程序员自己编写，而不是由某个像 Josh Hug 一样友善的第三方替你完成。&lt;/p&gt;
&lt;p&gt;本章将探索如何编写自己的测试。目标是创建一个名为 &lt;code&gt;Sort&lt;/code&gt; 的类，其中提供方法 &lt;code&gt;sort(String[] x)&lt;/code&gt;，对数组 &lt;code&gt;x&lt;/code&gt; 中的字符串进行原地排序。&lt;/p&gt;
&lt;p&gt;我们要采用一种全新的思考方式：先编写 &lt;code&gt;testSort()&lt;/code&gt;，完成测试之后，才开始写真正的排序代码。&lt;/p&gt;
&lt;h3&gt;临时拼凑式测试&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/Y2_oyfQV52c&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为 &lt;code&gt;Sort.sort&lt;/code&gt; 编写测试并不复杂，只是比较繁琐。我们只需创建输入、调用 &lt;code&gt;sort&lt;/code&gt;，再检查方法输出是否正确。如果不正确，就输出第一个不匹配的位置并终止测试。例如，可以创建下面的测试类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TestSort {
    /** Tests the sort method of the Sort class. */
    public static void testSort() {
        String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
        String[] expected = {&quot;an&quot;, &quot;egg&quot;, &quot;have&quot;, &quot;i&quot;};
        Sort.sort(input);
        for (int i = 0; i &amp;#x3C; input.length; i += 1) {
            if (!input[i].equals(expected[i])) {
                System.out.println(&quot;Mismatch in position &quot; + i + &quot;, expected: &quot; + expected + &quot;, but got: &quot; + input[i] + &quot;.&quot;);
                break;
            }
        }
    }

    public static void main(String[] args) {
        testSort();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以创建一个空白的 &lt;code&gt;Sort.sort&lt;/code&gt; 方法来检验测试本身：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Sort {
    /** Sorts strings destructively. */
    public static void sort(String[] x) {        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用这个空方法运行 &lt;code&gt;testSort()&lt;/code&gt;，会得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Mismatch in position 0, expected: an, but got: i.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到错误信息是好事，这说明测试确实能够发现错误。更有意思的是，我们现在给自己创造了一个小游戏：目标是修改 &lt;code&gt;Sort.sort&lt;/code&gt;，直到错误信息消失。这在心理上是个小技巧，许多程序员会发现，这种自己创造的小谜题几乎令人上瘾。&lt;/p&gt;
&lt;p&gt;它很像课程有自动评分器时的情况：你会沉迷于让评分器向你表达认可。现在，你也有能力为自己的代码创造一个裁判；只有正确完成代码，才能赢得它的肯定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要说明：&lt;/strong&gt; 你可能会问：“为什么要遍历整个数组？为什么不直接使用 &lt;code&gt;==&lt;/code&gt; 判断两个数组相等？”原因是，比较两个对象是否相等时，不能简单使用 &lt;code&gt;==&lt;/code&gt;。&lt;code&gt;==&lt;/code&gt; 比较的是内存盒中原样保存的比特。例如 &lt;code&gt;input == expected&lt;/code&gt; 检查的是 &lt;code&gt;input&lt;/code&gt; 与 &lt;code&gt;expected&lt;/code&gt; 是否保存相同地址，而不是两个数组中的值是否相同。因此，&lt;code&gt;testSort&lt;/code&gt; 使用循环并输出第一个不匹配位置。也可以使用内置方法 &lt;code&gt;java.util.Arrays.equals&lt;/code&gt; 替代循环。&lt;/p&gt;
&lt;p&gt;上面的单个测试工作量还不算大，但若要编写一整套这种_临时拼凑式_测试，就必须写很多不同的循环和输出语句，会非常繁琐。下一节将看到 &lt;code&gt;org.junit&lt;/code&gt; 库如何节省大量工作。&lt;/p&gt;
&lt;h3&gt;JUnit 测试&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/NoNGCB6uJkY&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;org.junit&lt;/code&gt; 库提供了许多辅助方法和功能，用于简化测试编写。例如，上面的临时测试可以替换为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void testSort() {
    String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
    String[] expected = {&quot;an&quot;, &quot;egg&quot;, &quot;have&quot;, &quot;i&quot;};
    Sort.sort(input);
    org.junit.Assert.assertArrayEquals(expected, input);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这段代码简单得多&lt;/strong&gt;，完成的事情却几乎完全相同：如果数组不相等，它会指出第一个不匹配位置。例如，在什么都不做的 &lt;code&gt;Sort.sort&lt;/code&gt; 上运行 &lt;code&gt;testSort()&lt;/code&gt;，会得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Exception in thread &quot;main&quot; arrays first differed at element [0]; expected:&amp;#x3C;[an]&gt; but was:&amp;#x3C;[i]&gt;
    at org.junit.internal.ComparisonCriteria.arrayEquals(ComparisonCriteria.java:55)
    at org.junit.Assert.internalArrayEquals(Assert.java:532)
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出比之前的临时测试稍显难看，不过本章末尾会介绍如何让它更友好。&lt;/p&gt;
&lt;h3&gt;选择排序&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/aYuM9VQJcUs&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在编写 &lt;code&gt;Sort.sort&lt;/code&gt; 之前，需要先有一个排序算法。最简单的排序算法之一是“选择排序”。它包含三个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;找到最小元素。&lt;/li&gt;
&lt;li&gt;把它移动到最前面。&lt;/li&gt;
&lt;li&gt;对剩下的 N - 1 个元素继续执行选择排序，不再触碰已经放好的第一个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，假设数组为 &lt;code&gt;{6, 3, 7, 2, 8, 1}&lt;/code&gt;。其中最小元素是 &lt;code&gt;1&lt;/code&gt;，因此把 &lt;code&gt;1&lt;/code&gt; 移到最前面。一种自然办法是把 &lt;code&gt;1&lt;/code&gt; 插到开头，再把其他数字整体后移，得到 &lt;code&gt;{1, 6, 3, 7, 2, 8}&lt;/code&gt;；但效率高得多的办法，是直接交换 &lt;code&gt;1&lt;/code&gt; 与原来的第一个元素（这里是 &lt;code&gt;6&lt;/code&gt;），得到 &lt;code&gt;{1, 3, 7, 2, 8, 6}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;接着对剩余数字重复相同过程。&lt;code&gt;3, 7, 2, 8, 6&lt;/code&gt; 中最小的是 &lt;code&gt;2&lt;/code&gt;，交换到剩余部分的开头后得到 &lt;code&gt;{1, 2, 7, 3, 8, 6}&lt;/code&gt;。不断重复，依次得到 &lt;code&gt;{1, 2, 3, 7, 8, 6}&lt;/code&gt;、&lt;code&gt;{1, 2, 3, 6, 8, 7}&lt;/code&gt;，最终得到 &lt;code&gt;{1, 2, 3, 6, 7, 8}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以使用 2.4 节介绍的不变量，从数学上证明该排序算法对任意数组都正确，不过本书不展开证明。继续之前，建议自己写一个短数字数组并手动执行选择排序，确保真正理解它。&lt;/p&gt;
&lt;p&gt;知道选择排序如何工作后，可以在空白的 &lt;code&gt;Sort.sort&lt;/code&gt; 中先写几条简短注释，引导思考：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Sort {
    /** Sorts strings destructively. */
    public static void sort(String[] x) { 
           // find the smallest item
           // move it to the front
           // selection sort the rest (using recursion?)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后续小节会逐步完成选择排序实现。我会故意采用学生可能使用的解题方式，因此&lt;strong&gt;过程中会有意犯几个错误&lt;/strong&gt;。这些错误是好事，因为它们可以展示测试的价值。如果阅读时发现错误，不必担心，后面会逐一修正。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;findSmallest&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/RnTUJ9lDZLw&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最自然的起点是编写一个寻找列表中最小元素的方法。和 &lt;code&gt;Sort.sort&lt;/code&gt; 一样，在真正完成方法之前先写测试。首先创建一个假的 &lt;code&gt;findSmallest&lt;/code&gt;，让它随便返回某个值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Sort {
    /** Sorts strings destructively. */
    public static void sort(String[] x) { 
           // find the smallest item
           // move it to the front
           // selection sort the rest (using recursion?)
    }

    /** Returns the smallest string in x. */
    public static String findSmallest(String[] x) {
        return x[2];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这显然不是正确实现，但我们决定先完成测试，再认真思考 &lt;code&gt;findSmallest&lt;/code&gt; 的逻辑。借助 &lt;code&gt;org.junit&lt;/code&gt;，向 &lt;code&gt;TestSort&lt;/code&gt; 添加测试非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TestSort {
    ...
    public static void testFindSmallest() {
        String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
        String expected = &quot;an&quot;;

        String actual = Sort.findSmallest(input);
        org.junit.Assert.assertEquals(expected, actual);        
    }

    public static void main(String[] args) {
        testFindSmallest(); // note: we changed this from testSort!
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 &lt;code&gt;TestSort.testSort&lt;/code&gt; 一样，先运行 &lt;code&gt;TestSort.testFindSmallest&lt;/code&gt;，确保测试能够失败。但这一次测试居然通过了，没有任何信息出现，因为我们碰巧硬编码了正确返回值 &lt;code&gt;x[2]&lt;/code&gt;。把方法改成一个肯定错误的返回值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Returns the smallest string in x. */
public static String findSmallest(String[] x) {
    return x[3];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后运行测试，会得到错误。这是好事：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Exception in thread &quot;main&quot; java.lang.AssertionError: expected:&amp;#x3C;[an]&gt; but was:&amp;#x3C;[null]&gt;
    at org.junit.Assert.failNotEquals(Assert.juava:834)
    at TestSort.testFindSmallest(TestSort.java:9)
    at TestSort.main(TestSort.java:24)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和之前一样，我们又建立了一个小游戏：修改 &lt;code&gt;Sort.findSmallest&lt;/code&gt;，直到错误消失。这个目标比一次写好整个 &lt;code&gt;Sort.sort&lt;/code&gt; 更小，甚至可能更令人上瘾。&lt;/p&gt;
&lt;p&gt;顺便说一句：我恰好返回正确值 &lt;code&gt;x[2]&lt;/code&gt; 看起来像刻意安排，但在录制课程视频时，我真的无意中犯了完全相同的错误。&lt;/p&gt;
&lt;p&gt;现在真正编写 &lt;code&gt;findSmallest&lt;/code&gt;。它似乎相当简单。Java 初学者可能写出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**  Returns the smallest string in x. */
public static String findSmallest(String[] x) {
    String smallest = x[0];
    for (int i = 0; i &amp;#x3C; x.length; i += 1) {
        if (x[i] &amp;#x3C; smallest) {
            smallest = x[i];
        }
    }
    return smallest;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这会产生编译错误：“&lt;code&gt;&amp;#x3C;&lt;/code&gt; cannot be applied to &lt;code&gt;java.lang.String&lt;/code&gt;”。问题是 Java 不允许使用 &lt;code&gt;&amp;#x3C;&lt;/code&gt; 比较字符串。&lt;/p&gt;
&lt;p&gt;编程中遇到这种容易描述的问题时，通常最好使用搜索引擎。例如可以搜索 “less than strings Java”，结果可能包含&lt;a href=&quot;https://stackoverflow.com/questions/5153496/how-can-i-compare-two-strings-in-java-and-define-which-of-them-is-smaller-than-t&quot;&gt;这篇 Stack Overflow 帖子&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;其中一个热门回答解释：&lt;code&gt;str1.compareTo(str2)&lt;/code&gt; 在 &lt;code&gt;str1 &amp;#x3C; str2&lt;/code&gt; 时返回负数，两者相等时返回 0，&lt;code&gt;str1 &gt; str2&lt;/code&gt; 时返回正数。&lt;/p&gt;
&lt;p&gt;把这一点加入代码后，得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Returns the smallest string in x. 
  * @source Got help with string compares from https://goo.gl/a7yBU5. */
public static String findSmallest(String[] x) {
    String smallest = x[0];
    for (int i = 0; i &amp;#x3C; x.length; i += 1) {
        int cmp = x[i].compareTo(smallest);
        if (cmp &amp;#x3C; 0) {
            smallest = x[i];
        }
    }
    return smallest;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我们使用 &lt;code&gt;@source&lt;/code&gt; 标签注明资料来源。这里主要是给正式修读 CS61B 的同学做示范，现实工作中并不通常使用这种标签。&lt;/p&gt;
&lt;p&gt;由于使用了完全陌生的语法，我们可能不太确信 &lt;code&gt;findSmallest&lt;/code&gt; 是否正确。幸运的是，刚刚已经编写了测试。运行后没有输出，说明代码很可能正确。&lt;/p&gt;
&lt;p&gt;还可以增加更多测试用例来提高信心。例如把 &lt;code&gt;testFindSmallest&lt;/code&gt; 改成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void testFindSmallest() {
    String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
    String expected = &quot;an&quot;;

    String actual = Sort.findSmallest(input);
    org.junit.Assert.assertEquals(expected, actual);        

    String[] input2 = {&quot;there&quot;, &quot;are&quot;, &quot;many&quot;, &quot;pigs&quot;};
    String expected2 = &quot;are&quot;;

    String actual2 = Sort.findSmallest(input2);
    org.junit.Assert.assertEquals(expected2, actual2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行，测试仍然通过。我们依然不能百分之百确定方法正确，但与完全没有测试相比，信心已经大得多。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;swap&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/YjWK58NGRxw&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;回到下面的 &lt;code&gt;sort&lt;/code&gt; 方法，接下来需要一个把元素移到最前面的辅助方法，命名为 &lt;code&gt;swap&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
       // find the smallest item
       // move it to the front
       // selection sort the rest (using recursion?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写交换方法非常直接，你以前可能已经做过。正确实现可以是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void swap(String[] x, int a, int b) {
    String temp = x[a];
    x[a] = x[b];
    x[b] = temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但为了展示测试的用途，暂时故意引入一个错误。经验不足的程序员可能会写成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void swap(String[] x, int a, int b) {    
    x[a] = x[b];
    x[b] = x[a];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;借助 JUnit，为该方法编写测试很容易。下面还修改了 &lt;code&gt;main&lt;/code&gt;，让它调用 &lt;code&gt;testSwap&lt;/code&gt;，而不是 &lt;code&gt;testFindSmallest&lt;/code&gt; 或 &lt;code&gt;testSort&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TestSort {
    ...    

    /** Test the Sort.swap method. */
    public static void testSwap() {
        String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
        int a = 0;
        int b = 2;
        String[] expected = {&quot;an&quot;, &quot;have&quot;, &quot;i&quot;, &quot;egg&quot;};

        Sort.swap(input, a, b);
        org.junit.Assert.assertArrayEquals(expected, input);
    }

    public static void main(String[] args) {
        testSwap();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在错误的 &lt;code&gt;swap&lt;/code&gt; 上运行测试，果然得到错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Exception in thread &quot;main&quot; arrays first differed in element [2]; expected:&amp;#x3C;[i]&gt; but was:&amp;#x3C;[an]&gt;
    at TestSort.testSwap(TestSort.java:36)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要简要指出，只调用 &lt;code&gt;testSwap&lt;/code&gt;、不同时调用 &lt;code&gt;testSort&lt;/code&gt; 很重要。例如，如果 &lt;code&gt;main&lt;/code&gt; 如下，那么 &lt;code&gt;testSort&lt;/code&gt; 一失败，整个 &lt;code&gt;main&lt;/code&gt; 就会终止，&lt;code&gt;testSwap&lt;/code&gt; 永远不会运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    testSort();
    testFindSmallest();
    testSwap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本章末尾会介绍更优雅的多测试处理方式，无需手动指定运行哪些测试。&lt;/p&gt;
&lt;p&gt;现在已经有一个失败测试，可以用它辅助调试。一种方法是在 &lt;code&gt;swap&lt;/code&gt; 中设置断点，使用 IntelliJ 的可视化调试功能。想进一步了解并练习调试，请完成&lt;a href=&quot;https://sp19.datastructur.es/materials/lab/lab3/lab3&quot;&gt;实验 3&lt;/a&gt;。逐行执行代码后，错误会立刻变得清晰；加入本节开头的临时变量即可修复：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void swap(String[] x, int a, int b) {
    String temp = x[a];
    x[a] = x[b];
    x[b] = temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行，测试通过。&lt;/p&gt;
&lt;h3&gt;修改 &lt;code&gt;findSmallest&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/opqj7VyUDR8&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;方法的多个组成部分已经完成，现在可以尝试把它们连接起来形成 &lt;code&gt;Sort&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
       // find the smallest item
       // move it to the front
       // selection sort the rest (using recursion?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;findSmallest&lt;/code&gt; 和 &lt;code&gt;swap&lt;/code&gt; 各自怎样使用都很清楚，但一连接就会发现不匹配：&lt;code&gt;findSmallest&lt;/code&gt; 返回 &lt;code&gt;String&lt;/code&gt;，而 &lt;code&gt;swap&lt;/code&gt; 需要两个下标。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
       // find the smallest item
       String smallest = findSmallest(x);

       // move it to the front
       swap(x, 0, smallest);

       // selection sort the rest (using recursion?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，&lt;code&gt;findSmallest&lt;/code&gt; 应返回最小字符串的下标，而不是字符串本身。犯这种看似愚蠢的错误非常正常，也非常容易；如果你也遇到类似情况，不必焦虑。不断迭代设计本来就是写代码的一部分。&lt;/p&gt;
&lt;p&gt;这个设计很容易修改，只需让 &lt;code&gt;findSmallest&lt;/code&gt; 返回 &lt;code&gt;int&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int findSmallest(String[] x) {
    int smallestIndex = 0;
    for (int i = 0; i &amp;#x3C; x.length; i += 1) {
        int cmp = x[i].compareTo(x[smallestIndex]);
        if (cmp &amp;#x3C; 0) {
            smallestIndex = i;
        }
    }
    return smallestIndex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一次非微小改动，因此也应更新 &lt;code&gt;testFindSmallest&lt;/code&gt;，确认方法仍然工作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void testFindSmallest() {
    String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
    int expected = 2;

    int actual = Sort.findSmallest(input);
    org.junit.Assert.assertEquals(expected, actual);        

    String[] input2 = {&quot;there&quot;, &quot;are&quot;, &quot;many&quot;, &quot;pigs&quot;};
    int expected2 = 1;

    int actual2 = Sort.findSmallest(input);
    org.junit.Assert.assertEquals(expected2, actual2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让 &lt;code&gt;TestSort&lt;/code&gt; 运行该测试后，代码通过。现在回到 &lt;code&gt;sort&lt;/code&gt;，可以完成选择排序的前两步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
   // find the smallest item
   // move it to the front
   // selection sort the rest (using recursion?)
   int smallestIndex = findSmallest(x);
   swap(x, 0, smallestIndex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的事情，是以某种方式继续对剩余元素执行选择排序，也许可以使用递归。下一节处理这个问题。&lt;/p&gt;
&lt;p&gt;回顾目前的过程，值得注意的是：我们先创建测试，并利用测试确认基础方法可用，然后才尝试把它们用于其他任务。这是极其重要的思想，采用它会长期受益。&lt;/p&gt;
&lt;h3&gt;递归辅助方法&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/YcYUYZc-lOM&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先思考如何完成 &lt;code&gt;sort&lt;/code&gt; 所需的递归调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
   int smallestIndex = findSmallest(x);
   swap(x, 0, smallestIndex);
   // recursive call??
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;习惯 Python 等语言的同学，可能会想使用切片语法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
   int smallestIndex = findSmallest(x);
   swap(x, 0, smallestIndex);
   sort(x[1:])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但 Java 中不存在对子数组的这种引用，也就是说，不能简单传入数组下一个元素的地址。&lt;/p&gt;
&lt;p&gt;只处理大数组中的一部分，是很常见的问题。典型解决方案是创建私有辅助方法，增加一个或多个参数，标明要处理数组的哪一部分。例如，可以再写一个也名为 &lt;code&gt;sort&lt;/code&gt; 的私有辅助方法，只考虑从 &lt;code&gt;start&lt;/code&gt; 开始的元素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
    // TODO
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与公开的 &lt;code&gt;sort&lt;/code&gt; 相比，增加 &lt;code&gt;start&lt;/code&gt; 后，递归就容易多了。下一节会测试这个方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
   int smallestIndex = findSmallest(x);
   swap(x, start, smallestIndex);
   sort(x, start + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了辅助方法，还需要设置正确的初始调用。令 &lt;code&gt;start&lt;/code&gt; 为 0，就等于对整个数组排序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively. */
public static void sort(String[] x) { 
   sort(x, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们想对数组等并非天然递归的数据结构使用递归时，这是一种非常常见的方法。&lt;/p&gt;
&lt;h3&gt;调试并完成排序&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/ciP4dSH_CHA&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;testSort&lt;/code&gt; 后立刻遇到问题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Exception in thread &quot;main&quot; java.lang.ArrayIndexOutOfBoundsException: 4
    at Sort.swap(Sort.java:16)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 Java 调试器后发现，&lt;code&gt;start&lt;/code&gt; 不知为何达到了 4。仔细逐行执行，会发现我们忘记给递归 &lt;code&gt;sort&lt;/code&gt; 添加基本情况。修复很直接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
   if (start == x.length) {
       return;
   }
   int smallestIndex = findSmallest(x);
   swap(x, start, smallestIndex);
   sort(x, start + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行，又得到另一个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Exception in thread &quot;main&quot; arrays first differed at element [0]; 
   expected&amp;#x3C;[an]&gt; bit was:&amp;#x3C;[have]&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次合理使用 IntelliJ 调试器，可以找到某行代码的结果不符合预期。这里值得注意的是，调试时我采用了比你可能想到的更高抽象层级：更多使用 &lt;code&gt;Step Over&lt;/code&gt;，而不是 &lt;code&gt;Step Into&lt;/code&gt;。正如实验 3 所述，在更高抽象层级上调试，可以直接比较整个函数调用结果与预期，从而节省大量时间和精力。&lt;/p&gt;
&lt;p&gt;具体来说，对 &lt;code&gt;{&quot;an&quot;, &quot;have&quot;, &quot;i&quot;, &quot;egg&quot;}&lt;/code&gt; 的最后三个元素排序时，&lt;code&gt;findSmallest&lt;/code&gt; 给出的仍是第 0 个元素 &lt;code&gt;&quot;an&quot;&lt;/code&gt;，而不是第 3 个元素 &lt;code&gt;&quot;egg&quot;&lt;/code&gt;。仔细看定义就会发现，这并不意外：&lt;code&gt;findSmallest&lt;/code&gt; 检查整个数组，而不是只检查从 &lt;code&gt;start&lt;/code&gt; 开始的元素。这种设计缺陷非常常见，编写测试并使用调试器是修复它们的好方法。&lt;/p&gt;
&lt;p&gt;为修复代码，让 &lt;code&gt;findSmallest&lt;/code&gt; 接受第二个参数 &lt;code&gt;start&lt;/code&gt;，即 &lt;code&gt;findSmallest(String[] x, int start)&lt;/code&gt;。这样它只会在尚未排序的末尾部分寻找最小元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int findSmallest(String[] x, int start) {
    int smallestIndex = start;
    for (int i = start; i &amp;#x3C; x.length; i += 1) {
        int cmp = x[i].compareTo(x[smallestIndex]);
        if (cmp &amp;#x3C; 0) {
            smallestIndex = i;
        }
    }
    return smallestIndex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对一个基础构件作了重要修改，因此应确保修改正确。&lt;/p&gt;
&lt;p&gt;首先修改 &lt;code&gt;testFindSmallest&lt;/code&gt;，使用新参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void testFindSmallest() {
    String[] input = {&quot;i&quot;, &quot;have&quot;, &quot;an&quot;, &quot;egg&quot;};
    int expected = 2;

    int actual = Sort.findSmallest(input, 0);
    org.junit.Assert.assertEquals(expected, actual);        

    String[] input2 = {&quot;there&quot;, &quot;are&quot;, &quot;many&quot;, &quot;pigs&quot;};
    int expected2 = 2;

    int actual2 = Sort.findSmallest(input2, 2);
    org.junit.Assert.assertEquals(expected2, actual2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再修改 &lt;code&gt;TestSort.main&lt;/code&gt;，让它运行 &lt;code&gt;testFindSmallest&lt;/code&gt;。测试通过，强烈说明修改正确。&lt;/p&gt;
&lt;p&gt;接着让 &lt;code&gt;Sort.sort&lt;/code&gt; 在调用 &lt;code&gt;findSmallest&lt;/code&gt; 时使用新的 &lt;code&gt;start&lt;/code&gt; 参数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
   if (start == x.length) {
       return;
   }
   int smallestIndex = findSmallest(x, start);
   swap(x, start, smallestIndex);
   sort(x, start + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后让 &lt;code&gt;TestSort&lt;/code&gt; 运行 &lt;code&gt;TestSort.sort&lt;/code&gt;。成功了，方法可以正常工作。至此完成任务，也真正体验了本章开头所说的“新方式”。本章余下部分将反思这一过程。&lt;/p&gt;
&lt;h3&gt;对开发过程的反思&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/CbULOb4UBMY&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;编写和调试程序时，经常需要在不同上下文之间切换。试图同时把过多内容放在脑中，最坏会导致灾难，最好也会拖慢进度。&lt;/p&gt;
&lt;p&gt;自动化测试可以降低这种认知负担。例如，编写 &lt;code&gt;sort&lt;/code&gt; 的过程中发现 &lt;code&gt;findSmallest&lt;/code&gt; 有错误。我们可以切换上下文，专注于 &lt;code&gt;findSmallest&lt;/code&gt;，通过 &lt;code&gt;testFindSmallest&lt;/code&gt; 确认它正确，再回到 &lt;code&gt;sort&lt;/code&gt;。相比之下，更原始的做法是不断调用 &lt;code&gt;sort&lt;/code&gt;，试图通过整体算法的表现猜测 &lt;code&gt;findSmallest&lt;/code&gt; 是否正确。&lt;/p&gt;
&lt;p&gt;打个比方，你可以这样测试降落伞的拉绳：登上飞机、起飞、跳出去、拉动绳子，再看看降落伞是否打开；也可以站在地面直接拉一下看看。类似地，没必要通过运行完整 &lt;code&gt;sort&lt;/code&gt; 来试验 &lt;code&gt;findSmallest&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如前所述，测试还会让你对程序的基础组成部分更有信心。一旦出错，也更清楚应从哪里开始检查。&lt;/p&gt;
&lt;p&gt;最后，测试使重构代码更安全。假设你决定重写 &lt;code&gt;findSmallest&lt;/code&gt;，使它更快或更易读。可以放心修改，只需确认测试仍然通过。&lt;/p&gt;
&lt;h3&gt;更好的 JUnit 写法&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/hm5_pYuw-Dk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;先回顾今天看到的新语法：&lt;code&gt;org.junit.Assert.assertEquals(expected, actual)&lt;/code&gt;。这个名称很长的方法检查 &lt;code&gt;expected&lt;/code&gt; 与 &lt;code&gt;actual&lt;/code&gt; 是否相等；如果不相等，它会终止程序并给出详细错误信息。&lt;/p&gt;
&lt;p&gt;JUnit 除了 &lt;code&gt;assertEquals&lt;/code&gt; 还有很多方法，例如 &lt;code&gt;assertFalse&lt;/code&gt;、&lt;code&gt;assertNotNull&lt;/code&gt;、&lt;code&gt;fail&lt;/code&gt; 等，可以查阅 &lt;a href=&quot;http://junit.org/junit4/javadoc/4.12/org/junit/Assert.html&quot;&gt;JUnit 官方文档&lt;/a&gt;。JUnit 还有许多更复杂的功能，CS61B 不会介绍，但你可以自由使用。&lt;/p&gt;
&lt;p&gt;JUnit 已经带来明显改进，但之前的测试代码仍有些笨拙。本节余下部分介绍两项主要增强，让代码更整洁、更易用。从语法角度看，它们可能很神秘；目前先照着写，后续章节会解释其中一部分。&lt;/p&gt;
&lt;p&gt;第一项增强是使用“测试注解”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在每个测试方法前添加 &lt;code&gt;@org.junit.Test&lt;/code&gt;，后面不写分号。&lt;/li&gt;
&lt;li&gt;把每个测试方法改为非静态方法。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;TestSort&lt;/code&gt; 类中删除 &lt;code&gt;main&lt;/code&gt; 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完成这三步后，通过 JUnit 的 &lt;code&gt;Run -&gt; Run&lt;/code&gt; 命令重新运行代码，所有测试都会自动执行，不再需要手动调用。基于注解的方法有以下优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无需手动调用测试。&lt;/li&gt;
&lt;li&gt;所有测试都会执行，而不只是手动指定的测试。&lt;/li&gt;
&lt;li&gt;一个测试失败后，其他测试仍会继续运行。&lt;/li&gt;
&lt;li&gt;会显示运行了多少测试，以及多少测试通过。&lt;/li&gt;
&lt;li&gt;测试失败时，错误信息更美观。&lt;/li&gt;
&lt;li&gt;所有测试通过时会显示友好信息和绿色进度条，而不是完全没有输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二项增强可以缩短很长的方法名和注解名，使用的是“导入语句”。&lt;/p&gt;
&lt;p&gt;首先在文件顶部加入 &lt;code&gt;import org.junit.Test;&lt;/code&gt;。之后可以把所有 &lt;code&gt;@org.junit.Test&lt;/code&gt; 简写为 &lt;code&gt;@Test&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;再加入 &lt;code&gt;import static org.junit.Assert.*;&lt;/code&gt;。之后凡是出现 &lt;code&gt;org.junit.Assert.&lt;/code&gt; 的地方都可以省略。例如，&lt;code&gt;org.junit.Assert.assertEquals(expected2, actual2);&lt;/code&gt; 可以直接写成 &lt;code&gt;assertEquals(expected2, actual2);&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;后续课程会解释导入语句为什么这样工作。目前先使用并享受它带来的便利。&lt;/p&gt;
&lt;h3&gt;测试理念&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/pfGYUHUbfxA&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;正确性工具 1：自动评分器&lt;/h4&gt;
&lt;p&gt;回到最初。自动评分器很可能是你接触的第一个正确性工具。我们的自动评分器实际上基于 JUnit，并加入了一些自定义库。&lt;/p&gt;
&lt;p&gt;自动评分器有不少优点。最重要的是，它会替你验证正确性，省去自行编写全部测试这一繁琐而不一定有教学价值的任务。它还通过分数把评估过程游戏化，激励你追求正确结果。不过这也可能适得其反：学生为了最后几个并不会影响成绩或学习效果的分数，耗费过多时间。&lt;/p&gt;
&lt;p&gt;现实世界中通常没有自动评分器，过度依赖它会养成坏习惯。偶尔上传代码并等待评分器运行，也会破坏工作流。_自动评分器驱动开发_是这种做法的极端版本：学生一次写完所有代码，修复编译错误，然后提交给评分器；收到错误后，随便修改一些地方、加入输出语句，再次提交，不断重复。如果依赖自动评分器，你最终既无法控制工作流程，也无法真正控制自己的代码。&lt;/p&gt;
&lt;h4&gt;正确性工具 2：JUnit 测试&lt;/h4&gt;
&lt;p&gt;正如我们已经看到的，JUnit 打开了一个新世界。你不再依赖别人编写的评分器，而是为程序中的每个组成部分编写测试。每个这样的部分称为一个“单元”。这使你能信任代码的每个单元。它也能缩短调试时间，因为可以一次只关注一个代码单元，通常就是一个方法。单元测试还会迫使你明确每个单元究竟应完成什么任务。&lt;/p&gt;
&lt;p&gt;单元测试也有缺点。首先，编写全面测试需要时间。很容易写出不完整的单元测试，从而对代码产生虚假信心。对于依赖其他单元的单元，测试也比较困难，例如 &lt;code&gt;LinkedListDeque&lt;/code&gt; 中的 &lt;code&gt;addFirst&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;测试驱动开发（TDD）&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;TDD 是一种先写测试、再写代码的开发流程。步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确定一个新功能。&lt;/li&gt;
&lt;li&gt;为该功能编写单元测试。&lt;/li&gt;
&lt;li&gt;运行测试，它此时应当失败。&lt;/li&gt;
&lt;li&gt;编写能够通过测试的代码。成功！&lt;/li&gt;
&lt;li&gt;可选：重构代码，使它更快、更整洁等。此时已经有一套测试作为安全参照。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本课程不要求测试驱动开发，它也未必适合你的个人风格；但一般意义上的单元测试绝对是好主意。&lt;/p&gt;
&lt;h4&gt;正确性工具 3：集成测试&lt;/h4&gt;
&lt;p&gt;单元测试很好，但还应确保各个单元能够正确协作，而不是出现&lt;a href=&quot;https://media.giphy.com/media/3o7rbPDRHIHwbmcOBy/giphy.gif&quot;&gt;这个表情包&lt;/a&gt;中的情况。集成测试验证组件之间能否正确交互，JUnit 同样可以用于集成测试。可以把单元测试理解为最细粒度的层级，而集成测试位于更高一层抽象。&lt;/p&gt;
&lt;p&gt;集成测试的挑战在于：手动执行很繁琐，自动化又很困难。而且在较高抽象层级上，很容易漏掉细微或罕见的错误。&lt;/p&gt;
&lt;p&gt;总结来说：&lt;strong&gt;一定要写测试，但只在测试可能真正有用时写。&lt;/strong&gt; 借鉴 TDD，在某些情况下先写测试再写代码也非常有帮助。&lt;/p&gt;
&lt;h3&gt;接下来做什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/proj/proj1b/proj1b&quot;&gt;项目 1B&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 4 章 继承与接口</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/04-inheritance-and-interfaces</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/04-inheritance-and-interfaces</guid><description>CS61B 继承与接口中文教程，讲解接口实现、方法重写、动态方法选择、类型转换与高阶函数。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;4.1 导论与接口&lt;/h2&gt;
&lt;h4&gt;问题&lt;/h4&gt;
&lt;p&gt;回忆上周创建的两个列表类：&lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt;。查看它们的文档会发现，两者非常相似。事实上，它们提供的辅助方法完全一样。&lt;/p&gt;
&lt;p&gt;假设要编写一个 &lt;code&gt;WordUtils&lt;/code&gt; 类，其中包含可以对单词列表执行的函数，例如计算一个 &lt;code&gt;SLList&lt;/code&gt; 中最长字符串的方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.1.1：&lt;/strong&gt; 尝试自己编写该方法。它应接收一个字符串 &lt;code&gt;SLList&lt;/code&gt;，并返回列表中最长的字符串。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=IaEq_fogI08&amp;#x26;list=PL8FaHk7qbOD5zNY2dTJdL9R4Q2qKN98kN&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们得到的方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static String longest(SLList&amp;#x3C;String&gt; list) {
    int maxDex = 0;
    for (int i = 0; i &amp;#x3C; list.size(); i += 1) {
        String longestString = list.get(maxDex);
        String thisString = list.get(i);
        if (thisString.length() &gt; longestString.length()) {
            maxDex = i;
        }
    }
    return list.get(maxDex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;怎样让它也适用于 &lt;code&gt;AList&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;实际上，只需修改方法签名中的参数。把：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SLList&amp;#x3C;String&gt; list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;AList&amp;#x3C;String&gt; list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，&lt;code&gt;WordUtils&lt;/code&gt; 中就有两个名称完全相同的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static String longest(SLList&amp;#x3C;String&gt; list)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static String longest(AList&amp;#x3C;String&gt; list)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 允许这样做，这称为_方法重载_。调用 &lt;code&gt;WordUtils.longest&lt;/code&gt; 时，Java 会根据你传入的参数类型决定运行哪个方法。传入 &lt;code&gt;AList&lt;/code&gt; 就调用 &lt;code&gt;AList&lt;/code&gt; 版本；传入 &lt;code&gt;SLList&lt;/code&gt; 就调用 &lt;code&gt;SLList&lt;/code&gt; 版本。&lt;/p&gt;
&lt;p&gt;Java 能够处理名称相同、参数类型不同的方法确实很方便，但重载有几个缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非常重复而且难看，因为现在有两段几乎完全相同的代码。&lt;/li&gt;
&lt;li&gt;需要维护更多代码。若要对 &lt;code&gt;longest&lt;/code&gt; 做一个小修改，例如修复错误，必须在每种列表类型对应的方法中都修改一次。&lt;/li&gt;
&lt;li&gt;如果以后增加更多列表类型，每创建一个新列表类就必须再复制一份方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;上位词、下位词与接口继承&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=S-wcA94Oekc&amp;#x26;t=9s&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;无论在英语中还是现实生活中，词语与对象之间都存在逻辑层级。&lt;/p&gt;
&lt;p&gt;“狗”是贵宾犬、阿拉斯加犬、哈士奇等的_上位词_；反过来，贵宾犬、阿拉斯加犬和哈士奇都是“狗”的_下位词_。&lt;/p&gt;
&lt;p&gt;这些词构成一组“是一个（is-a）”关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;贵宾犬是一种狗。&lt;/li&gt;
&lt;li&gt;狗是一种犬科动物。&lt;/li&gt;
&lt;li&gt;犬科动物是一种食肉动物。&lt;/li&gt;
&lt;li&gt;食肉动物是一种动物。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0c7d438947c2-hierarchy.png&quot; alt=&quot;层级关系&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=hoYMyvWjCTg&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt; 也有相同层级：两者都是更一般的“列表”的下位类型。&lt;/p&gt;
&lt;p&gt;我们将在 Java 中正式表示这种关系。如果 &lt;code&gt;SLList&lt;/code&gt; 是 &lt;code&gt;List61B&lt;/code&gt; 的下位类型，那么 &lt;code&gt;SLList&lt;/code&gt; 类是 &lt;code&gt;List61B&lt;/code&gt; 的&lt;strong&gt;子类&lt;/strong&gt;，&lt;code&gt;List61B&lt;/code&gt; 则是 &lt;code&gt;SLList&lt;/code&gt; 的&lt;strong&gt;超类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;图 4.1.1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/d8be735c125e-subclass.png&quot; alt=&quot;子类关系&quot;&gt;&lt;/p&gt;
&lt;p&gt;要在 Java 中_表达_这种层级关系，需要完成两步：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 步：为一般意义上的列表上位类型定义一种类型，命名为 &lt;code&gt;List61B&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第 2 步：声明 &lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt; 是该类型的下位类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新的 &lt;code&gt;List61B&lt;/code&gt; 在 Java 中称为&lt;strong&gt;接口&lt;/strong&gt;。接口本质上是一份契约，规定一个列表必须能做什么，但不提供这些行为的具体实现。你能想到为什么吗？&lt;/p&gt;
&lt;p&gt;下面是 &lt;code&gt;List61B&lt;/code&gt; 接口。此时已经完成建立层级关系的第一步，也就是创建上位类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface List61B&amp;#x3C;Item&gt; {
    public void addFirst(Item x);
    public void add Last(Item y);
    public Item getFirst();
    public Item getLast();
    public Item removeLast();
    public Item get(int i);
    public void insert(Item x, int position);
    public int size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着完成第 2 步：声明 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;SLList&lt;/code&gt; 是 &lt;code&gt;List61B&lt;/code&gt; 的下位类型。在 Java 中，这种关系写在类定义中。&lt;/p&gt;
&lt;p&gt;原本的：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public class AList&amp;#x3C;Item&gt; {...}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;加入表示关系的关键字 &lt;code&gt;implements&lt;/code&gt; 后变为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public class AList&amp;#x3C;Item&gt; implements List61B&amp;#x3C;Item&gt; {...}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;implements List61B&amp;#x3C;Item&gt;&lt;/code&gt; 本质上是一项承诺。&lt;code&gt;AList&lt;/code&gt; 在说：“我保证拥有并定义 &lt;code&gt;List61B&lt;/code&gt; 接口中规定的全部属性和行为。”&lt;/p&gt;
&lt;p&gt;现在可以修改 &lt;code&gt;WordUtils&lt;/code&gt; 中的 &lt;code&gt;longest&lt;/code&gt;，让它接收 &lt;code&gt;List61B&lt;/code&gt;，因为 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;SLList&lt;/code&gt; 都和 &lt;code&gt;List61B&lt;/code&gt; 具有“是一个”关系。&lt;/p&gt;
&lt;h4&gt;重写&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=iOiNQ68H3Gk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们已经承诺在 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;SLList&lt;/code&gt; 中实现 &lt;code&gt;List61B&lt;/code&gt; 规定的方法，下面就来履行承诺。&lt;/p&gt;
&lt;p&gt;在子类中实现要求的方法时，最好在方法签名正上方加入 &lt;code&gt;@Override&lt;/code&gt; 标签；在 CS61B 中，这实际上是强制要求。下面只为其中一个方法添加了该标签：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void addFirst(Item x) {
    insert(x, 0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，即使没有写这个标签，你依然是在重写方法。因此从纯语法角度说并非必须添加。不过，标签会像安全装置一样告诉编译器：“我打算重写这个方法。”这样有什么用？它有点像一位校对员：如果重写过程出了问题，编译器会提醒你。&lt;/p&gt;
&lt;p&gt;假设你想重写 &lt;code&gt;addLast&lt;/code&gt;，却不小心拼成了 &lt;code&gt;addLsat&lt;/code&gt;。如果没有 &lt;code&gt;@Override&lt;/code&gt;，可能很久都发现不了错误，调试会更加痛苦；如果写了 &lt;code&gt;@Override&lt;/code&gt;，程序运行前编译器就会停止并要求修正。&lt;/p&gt;
&lt;h4&gt;接口继承&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Z2EAsOKSj2M&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;接口继承指的是：子类继承超类所规定的全部方法和行为。在“上位词与下位词”部分定义的 &lt;code&gt;List61B&lt;/code&gt; 中，接口包含所有方法签名，却没有具体实现；真正的实现由子类提供。&lt;/p&gt;
&lt;p&gt;这种继承还可以跨越多代。假设存在图 4.1.1 那样很长的超类/子类链，那么 &lt;code&gt;AList&lt;/code&gt; 不仅继承 &lt;code&gt;List61B&lt;/code&gt; 的方法，还继承它上方一直到最高超类的所有内容。换句话说，&lt;code&gt;AList&lt;/code&gt; 也从 &lt;code&gt;Collection&lt;/code&gt; 继承。&lt;/p&gt;
&lt;h3&gt;等号黄金法则（GRoE）&lt;/h3&gt;
&lt;p&gt;回忆第 2 章中的等号黄金法则：每当执行赋值 &lt;code&gt;a = b&lt;/code&gt;，都会把 &lt;code&gt;b&lt;/code&gt; 中的比特复制到 &lt;code&gt;a&lt;/code&gt; 中，并要求 &lt;code&gt;b&lt;/code&gt; 的类型与 &lt;code&gt;a&lt;/code&gt; 兼容。不能写 &lt;code&gt;Dog b = 1&lt;/code&gt; 或 &lt;code&gt;Dog b = new Cat()&lt;/code&gt;，因为 &lt;code&gt;1&lt;/code&gt; 不是 &lt;code&gt;Dog&lt;/code&gt;，&lt;code&gt;Cat&lt;/code&gt; 也不是 &lt;code&gt;Dog&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;把这条规则应用到本章之前的 &lt;code&gt;longest&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;public static String longest(List61B&amp;#x3C;String&gt; list)&lt;/code&gt; 接收一个 &lt;code&gt;List61B&lt;/code&gt;。我们说它也能接收 &lt;code&gt;AList&lt;/code&gt; 和 &lt;code&gt;SLList&lt;/code&gt;，但 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;List61B&lt;/code&gt; 明明是不同的类，为什么可行？因为 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;List61B&lt;/code&gt; 具有“是一个”关系，所以一个 &lt;code&gt;AList&lt;/code&gt; 应当能够放入 &lt;code&gt;List61B&lt;/code&gt; 类型的盒子中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.1.2：&lt;/strong&gt; 下面的代码会编译吗？如果会，运行时会发生什么？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    List61B&amp;#x3C;String&gt; someList = new SLList&amp;#x3C;String&gt;();
    someList.addFirst(&quot;elk&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能答案如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法编译。&lt;/li&gt;
&lt;li&gt;能够编译，但会在 &lt;code&gt;new&lt;/code&gt; 那一行产生错误。&lt;/li&gt;
&lt;li&gt;运行后创建一个 &lt;code&gt;SLList&lt;/code&gt;，其地址保存在 &lt;code&gt;someList&lt;/code&gt; 中；但调用 &lt;code&gt;someList.addFirst()&lt;/code&gt; 时崩溃，因为 &lt;code&gt;List&lt;/code&gt; 类没有实现 &lt;code&gt;addFirst&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;运行后创建一个 &lt;code&gt;SLList&lt;/code&gt;，其地址保存在 &lt;code&gt;someList&lt;/code&gt; 中；随后字符串 &lt;code&gt;&quot;elk&quot;&lt;/code&gt; 被插入 &lt;code&gt;someList&lt;/code&gt; 所引用的 &lt;code&gt;SLList&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实现继承&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Z2Z1LHPGsdk&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;之前的 &lt;code&gt;List61B&lt;/code&gt; 接口只有方法头，用于规定 &lt;code&gt;List61B&lt;/code&gt; &lt;strong&gt;应该做什么&lt;/strong&gt;。现在将看到，也可以直接在 &lt;code&gt;List61B&lt;/code&gt; 中编写已经包含实现的方法。这些方法会规定 &lt;code&gt;List61B&lt;/code&gt; 的下位类型&lt;strong&gt;应该怎样做&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;要做到这一点，必须在方法签名中加入 &lt;code&gt;default&lt;/code&gt; 关键字。&lt;/p&gt;
&lt;p&gt;例如在 &lt;code&gt;List61B&lt;/code&gt; 中定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;default public void print() {
    for (int i = 0; i &amp;#x3C; size(); i += 1) {
        System.out.print(get(i) + &quot; &quot;);
    }
    System.out.println();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么所有实现 &lt;code&gt;List61B&lt;/code&gt; 的类都能直接使用该方法。&lt;/p&gt;
&lt;p&gt;不过这个方法有一个小小的低效之处。你能发现吗？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=9M9exmhsjmc&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;SLList&lt;/code&gt;，每次调用 &lt;code&gt;get&lt;/code&gt; 都需要沿列表跳过许多结点。更好的办法是在沿链表前进的同时直接输出。&lt;/p&gt;
&lt;p&gt;我们希望 &lt;code&gt;SLList&lt;/code&gt; 采用不同于接口默认实现的输出方式，因此需要重写。在 &lt;code&gt;SLList&lt;/code&gt; 中实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void print() {
    for (Node p = sentinel.next; p != null; p = p.next) {
        System.out.print(p.item + &quot; &quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后，在 &lt;code&gt;SLList&lt;/code&gt; 上调用 &lt;code&gt;print()&lt;/code&gt; 时，会运行这个方法，而不是 &lt;code&gt;List61B&lt;/code&gt; 中的默认方法。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=eNtItRCIkBg&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;你可能会问：Java 怎样知道该调用哪个 &lt;code&gt;print()&lt;/code&gt;？这是个好问题。Java 依靠&lt;strong&gt;动态方法选择&lt;/strong&gt;完成这一点。&lt;/p&gt;
&lt;p&gt;我们知道，Java 变量具有类型：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;List61B&amp;#x3C;String&gt; lst = new SLList&amp;#x3C;String&gt;();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在上面的声明与实例化中，变量 &lt;code&gt;lst&lt;/code&gt; 的类型是 &lt;code&gt;List61B&lt;/code&gt;，这称为它的“静态类型”。&lt;/p&gt;
&lt;p&gt;对象本身也具有类型。&lt;code&gt;lst&lt;/code&gt; 指向的对象类型是 &lt;code&gt;SLList&lt;/code&gt;。这个对象本质上是 &lt;code&gt;SLList&lt;/code&gt;，因为通过 &lt;code&gt;SLList&lt;/code&gt; 构造方法创建；同时由于前面讨论的“是一个”关系，它也是 &lt;code&gt;List61B&lt;/code&gt;。对象实例化时真正使用的类型称为它的“动态类型”。&lt;/p&gt;
&lt;p&gt;补充说明：“动态类型”这个名称非常贴切。如果以后把 &lt;code&gt;lst&lt;/code&gt; 重新赋值为另一个类型的对象，例如一个 &lt;code&gt;AList&lt;/code&gt;，那么 &lt;code&gt;lst&lt;/code&gt; 的动态类型就会从 &lt;code&gt;SLList&lt;/code&gt; 变成 &lt;code&gt;AList&lt;/code&gt;。之所以称为动态，是因为它会随着变量当前引用的对象类型而改变。&lt;/p&gt;
&lt;p&gt;Java 运行一个被重写的方法时，会在对象的&lt;strong&gt;动态类型&lt;/strong&gt;中寻找合适的方法签名并执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：这条规则不适用于重载方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=WHYaqzTFh8A&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;假设同一个类中有两个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void peek(List61B&amp;#x3C;String&gt; list) {
    System.out.println(list.getLast());
}
public static void peek(SLList&amp;#x3C;String&gt; list) {
    System.out.println(list.getFirst());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SLList&amp;#x3C;String&gt; SP = new SLList&amp;#x3C;String&gt;();
List61B&amp;#x3C;String&gt; LP = SP;
SP.addLast(&quot;elk&quot;);
SP.addLast(&quot;are&quot;);
SP.addLast(&quot;cool&quot;);
peek(SP);
peek(LP);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次调用 &lt;code&gt;peek()&lt;/code&gt; 会使用第二个、参数为 &lt;code&gt;SLList&lt;/code&gt; 的版本。第二次调用会使用第一个、参数为 &lt;code&gt;List61B&lt;/code&gt; 的版本。原因是两个重载方法之间的区别只在参数类型。Java 判断调用哪个重载方法时，查看的是参数变量的&lt;strong&gt;静态类型&lt;/strong&gt;，并选择具有相同参数类型的方法。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=9KuVnIje2Ys&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;接口继承与实现继承&lt;/h4&gt;
&lt;p&gt;怎样区分“接口继承”和“实现继承”？可以使用下面这个简单标准：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接口继承（做什么）：只说明子类应当具备哪些能力。
&lt;ul&gt;
&lt;li&gt;例如：所有列表都应能够输出自己，但具体怎样输出由各自决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现继承（怎么做）：直接告诉子类应当怎样表现。
&lt;ul&gt;
&lt;li&gt;例如：列表必须按照这种特定方式输出——按顺序取得每个元素，再逐个输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建这些层级时，请记住子类与超类之间应当是“是一个”关系。&lt;code&gt;Cat&lt;/code&gt; 只有在猫&lt;strong&gt;是一种&lt;/strong&gt;动物的意义上才应实现 &lt;code&gt;Animal&lt;/code&gt;。不要使用“有一个”关系建立继承。猫&lt;strong&gt;有一只&lt;/strong&gt;爪子，但 &lt;code&gt;Cat&lt;/code&gt; 显然不应实现 &lt;code&gt;Claw&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最后，实现继承虽然听起来很方便，也存在一些缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;人会犯错，也无法记住一切；你可能重写过某个方法，却忘记自己这样做过。&lt;/li&gt;
&lt;li&gt;如果两个接口提供互相冲突的默认方法，冲突可能很难解决。&lt;/li&gt;
&lt;li&gt;它容易鼓励过度复杂的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;接下来做什么&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/discussion/disc04.pdf&quot;&gt;讨论课 4：继承&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4.2 &lt;code&gt;extends&lt;/code&gt;、类型转换与高阶函数&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;extends&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;前面我们已经看到，怎样使用 &lt;code&gt;implements&lt;/code&gt; 关键字与接口建立层次关系。那么，如果我们希望在两个类之间建立层次关系，该怎么做呢？&lt;/p&gt;
&lt;p&gt;假设我们想构建一个 &lt;code&gt;RotatingSLList&lt;/code&gt;。它拥有 &lt;code&gt;SLList&lt;/code&gt; 的所有功能，例如 &lt;code&gt;addFirst&lt;/code&gt;、&lt;code&gt;size&lt;/code&gt; 等，同时还增加一个 &lt;code&gt;rotateRight&lt;/code&gt; 操作：把最后一个元素移到列表最前面。&lt;/p&gt;
&lt;p&gt;一种做法是复制并粘贴 &lt;code&gt;SLList&lt;/code&gt; 的所有方法，然后再写一个 &lt;code&gt;rotateRight&lt;/code&gt;。但这样就没有利用继承的力量。继承允许子类复用已经定义好的类中的代码。因此，我们让 &lt;code&gt;RotatingSLList&lt;/code&gt; 继承 &lt;code&gt;SLList&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们可以在类声明中使用 &lt;code&gt;extends&lt;/code&gt; 关键字建立这种继承关系：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class RotatingSLList&amp;#x3C;Item&gt; extends SLList&amp;#x3C;Item&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就像 &lt;code&gt;AList&lt;/code&gt; 与 &lt;code&gt;List61B&lt;/code&gt; 之间存在“是一种（is-a）”关系一样，&lt;code&gt;RotatingSLList&lt;/code&gt; 也是一种 &lt;code&gt;SLList&lt;/code&gt;。&lt;code&gt;extends&lt;/code&gt; 让我们保留 &lt;code&gt;SLList&lt;/code&gt; 原有的功能，同时还可以修改它并添加新的能力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f87ca942f9aa-list_subclasses.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在既然 &lt;code&gt;RotatingSLList&lt;/code&gt; 已经继承了 &lt;code&gt;SLList&lt;/code&gt;，就来赋予它独有的旋转能力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.2.1。&lt;/strong&gt; 定义 &lt;code&gt;rotateRight&lt;/code&gt; 方法。它应当把现有列表中的每个元素向右移动一位，并把最后一个元素移到最前面。&lt;/p&gt;
&lt;p&gt;例如，对 &lt;code&gt;[5, 9, 15, 22]&lt;/code&gt; 调用 &lt;code&gt;rotateRight&lt;/code&gt; 后，应得到 &lt;code&gt;[22, 5, 9, 15]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;提示：是否有某些继承而来的方法可以帮助你完成它？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=990kImS-_nA&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一种实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void rotateRight() {
    Item x = removeLast();
    addFirst(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能已经注意到，我们能够直接使用在 &lt;code&gt;RotatingSLList&lt;/code&gt; 之外定义的方法，因为 &lt;code&gt;extends&lt;/code&gt; 使它从 &lt;code&gt;SLList&lt;/code&gt; 继承了这些方法。这也引出了一个问题：子类究竟继承了什么？&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;extends&lt;/code&gt; 后，子类会继承父类的所有&lt;strong&gt;成员&lt;/strong&gt;。成员包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有实例变量和静态变量；&lt;/li&gt;
&lt;li&gt;所有方法；&lt;/li&gt;
&lt;li&gt;所有嵌套类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但要注意：构造方法不会被继承，而且子类不能直接访问父类的私有成员。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;VengefulSLList&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;当有人对 &lt;code&gt;SLList&lt;/code&gt; 调用 &lt;code&gt;removeLast&lt;/code&gt; 时，被移除的值会被直接丢弃，再也看不到了。但假如这些被放逐的元素离开之后，开始策划一场针对我们的巨大叛乱呢？这种情况下，我们就需要记住所有被 &lt;code&gt;removeLast&lt;/code&gt; 移除的元素，以便以后找到并处理它们。&lt;/p&gt;
&lt;p&gt;于是，我们创建一个新类 &lt;code&gt;VengefulSLList&lt;/code&gt;，用于记住所有被 &lt;code&gt;removeLast&lt;/code&gt; 放逐的元素。&lt;/p&gt;
&lt;p&gt;和前面一样，我们在类声明中写明 &lt;code&gt;VengefulSLList&lt;/code&gt; 继承自 &lt;code&gt;SLList&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class VengefulSLList&amp;#x3C;Item&gt; extends SLList&amp;#x3C;Item&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，为 &lt;code&gt;VengefulSLList&lt;/code&gt; 添加一个 &lt;code&gt;printLostItems()&lt;/code&gt; 方法，用于打印所有被 &lt;code&gt;removeLast&lt;/code&gt; 移除的元素。我们可以增加一个实例变量，保存所有已删除的元素。如果使用一个 &lt;code&gt;SLList&lt;/code&gt; 来记录它们，就可以直接调用 &lt;code&gt;print()&lt;/code&gt; 输出全部元素。&lt;/p&gt;
&lt;p&gt;目前的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class VengefulSLList&amp;#x3C;Item&gt; extends SLList&amp;#x3C;Item&gt; {
    SLList&amp;#x3C;Item&gt; deletedItems;

    public void printLostItems() {
        deletedItems.print();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;VengefulSLList&lt;/code&gt; 的 &lt;code&gt;removeLast&lt;/code&gt; 应当完成和 &lt;code&gt;SLList.removeLast&lt;/code&gt; 完全相同的工作，但还要多做一步：把刚刚移除的元素加入 &lt;code&gt;deletedItems&lt;/code&gt;。为了复用代码，我们可以&lt;strong&gt;重写&lt;/strong&gt; &lt;code&gt;removeLast&lt;/code&gt;，并通过 &lt;code&gt;super&lt;/code&gt; 关键字调用父类 &lt;code&gt;SLList&lt;/code&gt; 中定义的 &lt;code&gt;removeLast&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.2.2。&lt;/strong&gt; 重写 &lt;code&gt;removeLast&lt;/code&gt;：移除最后一个元素，把它加入 &lt;code&gt;deletedItems&lt;/code&gt;，最后再返回该元素。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=RJ_OpzLeHeQ&quot;&gt;视频讲解&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;完整实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class VengefulSLList&amp;#x3C;Item&gt; extends SLList&amp;#x3C;Item&gt; {
    SLList&amp;#x3C;Item&gt; deletedItems;

    public VengefulSLList() {
        deletedItems = new SLList&amp;#x3C;Item&gt;();
    }

    @Override
    public Item removeLast() {
        Item x = super.removeLast();
        deletedItems.addLast(x);
        return x;
    }

    /** Prints deleted items. */
    public void printLostItems() {
        deletedItems.print();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;构造方法不会被继承&lt;/h4&gt;
&lt;p&gt;如前所述，子类会继承父类的所有成员，包括实例变量、静态变量、方法和嵌套类，但&lt;strong&gt;不会继承构造方法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然构造方法不会被继承，但 Java 要求：&lt;strong&gt;每个子类构造方法都必须先调用某个超类构造方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为什么会这样？回忆一下，&lt;code&gt;extends&lt;/code&gt; 定义的是子类与父类之间的“是一种”关系。如果 &lt;code&gt;VengefulSLList&lt;/code&gt; 是一种 &lt;code&gt;SLList&lt;/code&gt;，那么每个 &lt;code&gt;VengefulSLList&lt;/code&gt; 首先都必须按照 &lt;code&gt;SLList&lt;/code&gt; 的规则完成初始化。&lt;/p&gt;
&lt;p&gt;再看一个更直观的例子。假设有两个类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Human {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TA extends Human {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让 &lt;code&gt;TA&lt;/code&gt; 继承 &lt;code&gt;Human&lt;/code&gt; 是合理的，因为所有助教都是人。我们希望 &lt;code&gt;TA&lt;/code&gt; 继承人的属性和行为。&lt;/p&gt;
&lt;p&gt;如果运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TA christine = new TA();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么首先必须构造一个 &lt;code&gt;Human&lt;/code&gt; 部分，然后才能在此基础上赋予它 &lt;code&gt;TA&lt;/code&gt; 的特征。跳过人的构造过程，直接构造助教，在逻辑上是不完整的。&lt;/p&gt;
&lt;p&gt;因此，我们可以使用 &lt;code&gt;super&lt;/code&gt; 显式调用超类构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public VengefulSLList() {
    super();
    deletedItems = new SLList&amp;#x3C;Item&gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们没有显式书写，Java 会自动调用超类的&lt;strong&gt;无参数构造方法&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在这个例子中，写出 &lt;code&gt;super()&lt;/code&gt; 与省略它没有区别；它只是把 Java 原本隐式完成的操作明确写出来。但如果我们为 &lt;code&gt;VengefulSLList&lt;/code&gt; 定义另一个构造方法，Java 自动调用的无参构造方法就可能不是我们真正需要的。&lt;/p&gt;
&lt;p&gt;假设有一个接收初始元素的单参数构造方法。如果仍依赖 Java 隐式调用 &lt;code&gt;super()&lt;/code&gt;，传入的元素就不会被放入父类结构中。因此，必须把该元素显式传给正确的超类构造方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public VengefulSLList(Item x) {
    super(x);
    deletedItems = new SLList&amp;#x3C;Item&gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;Object&lt;/code&gt; 类&lt;/h4&gt;
&lt;p&gt;Java 中的每个类都是 &lt;code&gt;Object&lt;/code&gt; 类的后代，也就是说，每个类都会继承 &lt;code&gt;Object&lt;/code&gt;。即使类声明中没有显式写出 &lt;code&gt;extends Object&lt;/code&gt;，它也会隐式继承 &lt;code&gt;Object&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VengefulSLList&lt;/code&gt; 在类声明中显式 &lt;code&gt;extends SLList&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SLList&lt;/code&gt; 隐式 &lt;code&gt;extends Object&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，&lt;code&gt;SLList&lt;/code&gt; 会继承 &lt;code&gt;Object&lt;/code&gt; 的全部成员，而 &lt;code&gt;VengefulSLList&lt;/code&gt; 又会传递性地继承 &lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;Object&lt;/code&gt; 的成员。&lt;/p&gt;
&lt;p&gt;根据 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html&quot;&gt;&lt;code&gt;Object&lt;/code&gt; 类文档&lt;/a&gt;，&lt;code&gt;Object&lt;/code&gt; 提供了每个对象都应具备的操作，例如 &lt;code&gt;.equals(Object obj)&lt;/code&gt;、&lt;code&gt;.hashCode()&lt;/code&gt; 和 &lt;code&gt;toString()&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;“是一种”与“拥有一个”&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;重要提醒：&lt;/strong&gt; &lt;code&gt;extends&lt;/code&gt; 关键字定义的是“是一种（is-a）”关系，也就是上下位关系。一个常见错误，是把它用于“拥有一个（has-a）”关系，也就是整体与部分的关系。&lt;/p&gt;
&lt;p&gt;决定是否继承一个类时，最好先问自己：“是一种”这个说法是否成立。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Shower&lt;/code&gt;（淋浴器）是一种 &lt;code&gt;Bathroom&lt;/code&gt;（浴室）吗？不是。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VengefulSLList&lt;/code&gt; 是一种 &lt;code&gt;SLList&lt;/code&gt; 吗？是。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;封装&lt;/h3&gt;
&lt;p&gt;封装是面向对象编程的基本原则之一，也是程序员抵抗最大敌人——&lt;strong&gt;复杂性&lt;/strong&gt;——的重要手段。编写大型程序时，管理复杂性是我们必须面对的主要挑战之一。&lt;/p&gt;
&lt;p&gt;我们可以使用很多工具对抗复杂性，例如分层抽象（也就是建立抽象屏障）以及“为变化而设计”。后者强调：程序应当由模块化、可替换的组件构成，从而可以替换某一部分而不破坏整个系统。此外，&lt;strong&gt;隐藏其他人不需要知道的信息&lt;/strong&gt;，也是管理大型系统的基本方法。&lt;/p&gt;
&lt;p&gt;封装的根本思想，就是把内部信息隐藏起来。可以把它类比成人体细胞：细胞内部可能极其复杂，包含染色体、线粒体、核糖体等结构，但这些复杂性被完整地封装在一个模块中，对外呈现为一个整体。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/3c6990b0c70f-cell_encapsulated.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在计算机科学中，模块可以理解为一组协同工作的、共同完成某项任务或一组相关任务的方法。例如，一个表示列表的类就是一个模块。如果模块的实现细节被隐藏在内部，而外界只能通过有文档说明的接口与它交互，我们就说这个模块是封装良好的。&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;ArrayDeque&lt;/code&gt; 类为例。外部代码可以通过 &lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;removeLast&lt;/code&gt; 等公开方法使用它，但无需理解该数据结构内部复杂的实现细节。&lt;/p&gt;
&lt;h4&gt;抽象屏障&lt;/h4&gt;
&lt;p&gt;理想情况下，用户不应观察到所使用的数据结构的内部工作方式。幸运的是，Java 很容易强制建立抽象屏障。通过 &lt;code&gt;private&lt;/code&gt; 关键字，我们几乎可以阻止外部代码查看对象内部，从而避免底层复杂性暴露给外界。&lt;/p&gt;
&lt;h4&gt;继承怎样破坏封装&lt;/h4&gt;
&lt;p&gt;假设 &lt;code&gt;Dog&lt;/code&gt; 类中有下面两个方法。一种实现是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void bark() {
    System.out.println(&quot;bark&quot;);
}

public void barkMany(int N) {
    for (int i = 0; i &amp;#x3C; N; i += 1) {
        bark();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种实现是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void bark() {
    barkMany(1);
}

public void barkMany(int N) {
    for (int i = 0; i &amp;#x3C; N; i += 1) {
        System.out.println(&quot;bark&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从普通用户的角度看，这两种实现提供的功能完全相同。然而，假设我们定义一个 &lt;code&gt;Dog&lt;/code&gt; 的子类 &lt;code&gt;VerboseDog&lt;/code&gt;，并重写它的 &lt;code&gt;barkMany&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public void barkMany(int N) {
    System.out.println(&quot;As a dog, I say: &quot;);
    for (int i = 0; i &amp;#x3C; N; i += 1) {
        bark();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 4.2.3。&lt;/strong&gt; 对一个 &lt;code&gt;VerboseDog vd&lt;/code&gt;，在第一种 &lt;code&gt;Dog&lt;/code&gt; 实现下，&lt;code&gt;vd.barkMany(3)&lt;/code&gt; 会输出什么？在第二种实现下又会怎样？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A：&lt;code&gt;As a dog, I say: bark bark bark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;B：&lt;code&gt;bark bark bark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;C：其他结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在第一种实现中，输出是 A；而在第二种实现中，程序会陷入无限递归。&lt;code&gt;barkMany&lt;/code&gt; 调用 &lt;code&gt;bark()&lt;/code&gt;，&lt;code&gt;bark()&lt;/code&gt; 又调用 &lt;code&gt;barkMany(1)&lt;/code&gt;，后者再次调用 &lt;code&gt;bark()&lt;/code&gt;，如此无限重复。&lt;/p&gt;
&lt;p&gt;这说明：继承会让子类的重写行为影响父类内部的方法调用，从而使父类实现细节不再完全隐藏。看似等价的两种父类实现，在存在子类重写时可能产生截然不同的结果。&lt;/p&gt;
&lt;h3&gt;类型检查与类型转换&lt;/h3&gt;
&lt;p&gt;在讨论类型与转换之前，先回顾动态方法选择。动态方法查找指的是：程序运行时，根据对象的动态类型决定实际执行哪个方法。具体来说，如果 &lt;code&gt;VengefulSLList&lt;/code&gt; 重写了 &lt;code&gt;SLList&lt;/code&gt; 中的某个方法，那么运行时调用哪一个版本，取决于变量所引用对象的运行时类型，也就是动态类型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.2.4。&lt;/strong&gt; 对下面代码中的每一行，判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该行是否导致编译错误？&lt;/li&gt;
&lt;li&gt;哪些方法调用使用了动态方法选择？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/60936d73f594-dynamic_selection.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;逐行分析这个程序：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;VengefulSLList&amp;#x3C;Integer&gt; vsl = new VengefulSLList&amp;#x3C;Integer&gt;(9);
SLList&amp;#x3C;Integer&gt; sl = vsl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两行都可以正常编译。由于 &lt;code&gt;VengefulSLList&lt;/code&gt; 是一种 &lt;code&gt;SLList&lt;/code&gt;，所以可以把 &lt;code&gt;VengefulSLList&lt;/code&gt; 实例放进静态类型为 &lt;code&gt;SLList&lt;/code&gt; 的变量中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;sl.addLast(50);
sl.removeLast();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两行也能编译。&lt;code&gt;VengefulSLList&lt;/code&gt; 没有重写 &lt;code&gt;addLast&lt;/code&gt;，所以调用的是 &lt;code&gt;SLList&lt;/code&gt; 中的方法。&lt;code&gt;removeLast&lt;/code&gt; 则被 &lt;code&gt;VengefulSLList&lt;/code&gt; 重写了；&lt;code&gt;sl&lt;/code&gt; 的动态类型是 &lt;code&gt;VengefulSLList&lt;/code&gt;，因此动态方法选择会调用 &lt;code&gt;VengefulSLList&lt;/code&gt; 中的重写版本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;sl.printLostItems();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行会产生编译错误。编译器根据对象的&lt;strong&gt;静态类型&lt;/strong&gt;判断某个操作是否合法。&lt;code&gt;sl&lt;/code&gt; 的静态类型是 &lt;code&gt;SLList&lt;/code&gt;，而 &lt;code&gt;SLList&lt;/code&gt; 中没有定义 &lt;code&gt;printLostItems&lt;/code&gt;，所以即使 &lt;code&gt;sl&lt;/code&gt; 在运行时确实指向一个 &lt;code&gt;VengefulSLList&lt;/code&gt;，编译器也不允许调用该方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;VengefulSLList&amp;#x3C;Integer&gt; vsl2 = sl;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行同样会产生编译错误。编译器只看到 &lt;code&gt;sl&lt;/code&gt; 的静态类型是 &lt;code&gt;SLList&lt;/code&gt;，而并非每一个 &lt;code&gt;SLList&lt;/code&gt; 都一定是 &lt;code&gt;VengefulSLList&lt;/code&gt;，因此不能直接把它装进静态类型为 &lt;code&gt;VengefulSLList&lt;/code&gt; 的变量。&lt;/p&gt;
&lt;h4&gt;表达式的静态类型&lt;/h4&gt;
&lt;p&gt;和变量一样，使用 &lt;code&gt;new&lt;/code&gt; 得到的表达式也有编译期类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;SLList&amp;#x3C;Integer&gt; sl = new VengefulSLList&amp;#x3C;Integer&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右侧表达式的编译期类型是 &lt;code&gt;VengefulSLList&lt;/code&gt;。编译器检查到 &lt;code&gt;VengefulSLList&lt;/code&gt; 是一种 &lt;code&gt;SLList&lt;/code&gt;，因此允许赋值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;VengefulSLList&amp;#x3C;Integer&gt; vsl = new SLList&amp;#x3C;Integer&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右侧表达式的编译期类型是 &lt;code&gt;SLList&lt;/code&gt;。编译器检查 &lt;code&gt;SLList&lt;/code&gt; 是否一定是一种 &lt;code&gt;VengefulSLList&lt;/code&gt;，答案是否定的，因此产生编译错误。&lt;/p&gt;
&lt;p&gt;方法调用表达式的编译期类型，等于该方法声明的返回类型。假设有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Dog maxDog(Dog d1, Dog d2) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;maxDog&lt;/code&gt; 的返回类型声明为 &lt;code&gt;Dog&lt;/code&gt;，所以任何对 &lt;code&gt;maxDog&lt;/code&gt; 的调用表达式，其编译期类型都是 &lt;code&gt;Dog&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Poodle frank = new Poodle(&quot;Frank&quot;, 5);
Poodle frankJr = new Poodle(&quot;Frank Jr.&quot;, 15);

Dog largerDog = maxDog(frank, frankJr);
Poodle largerPoodle = maxDog(frank, frankJr); // 无法编译：右侧编译期类型是 Dog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 &lt;code&gt;Dog&lt;/code&gt; 类型的表达式赋给 &lt;code&gt;Poodle&lt;/code&gt; 变量会产生编译错误。&lt;code&gt;Poodle&lt;/code&gt; 是一种 &lt;code&gt;Dog&lt;/code&gt;，但一般的 &lt;code&gt;Dog&lt;/code&gt; 不一定是 &lt;code&gt;Poodle&lt;/code&gt;。即使我们作为读者清楚地知道 &lt;code&gt;frank&lt;/code&gt; 与 &lt;code&gt;frankJr&lt;/code&gt; 都是 &lt;code&gt;Poodle&lt;/code&gt;，编译器仍然只按照方法签名判断。&lt;/p&gt;
&lt;h4&gt;强制类型转换&lt;/h4&gt;
&lt;p&gt;Java 提供了一种特殊语法，让程序员告诉编译器：应把某个表达式视为特定的编译期类型。这称为&lt;strong&gt;类型转换（casting）&lt;/strong&gt;，通常也叫强制类型转换。&lt;/p&gt;
&lt;p&gt;回到前面的失败代码。因为我们知道 &lt;code&gt;frank&lt;/code&gt; 和 &lt;code&gt;frankJr&lt;/code&gt; 都是 &lt;code&gt;Poodle&lt;/code&gt;，可以写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Poodle largerPoodle = (Poodle) maxDog(frank, frankJr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;转换后，右侧表达式的编译期类型变为 &lt;code&gt;Poodle&lt;/code&gt;，因此代码可以通过编译。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;/strong&gt; 强制类型转换功能强大，但也很危险。它本质上是在告诉编译器：“暂时不要履行完整的类型检查职责，请相信我。”例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Poodle frank = new Poodle(&quot;Frank&quot;, 5);
Malamute frankSr = new Malamute(&quot;Frank Sr.&quot;, 100);

Poodle largerPoodle = (Poodle) maxDog(frank, frankSr); // 运行时异常
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里比较的是一只 &lt;code&gt;Poodle&lt;/code&gt; 和一只 &lt;code&gt;Malamute&lt;/code&gt;。如果没有强制转换，编译器不会允许把返回类型为 &lt;code&gt;Dog&lt;/code&gt; 的表达式直接赋给 &lt;code&gt;Poodle&lt;/code&gt;。强制转换让代码通过编译，但如果 &lt;code&gt;maxDog&lt;/code&gt; 在运行时返回的是 &lt;code&gt;Malamute&lt;/code&gt;，程序就会尝试把 &lt;code&gt;Malamute&lt;/code&gt; 当成 &lt;code&gt;Poodle&lt;/code&gt;，最终抛出 &lt;code&gt;ClassCastException&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;高阶函数&lt;/h3&gt;
&lt;p&gt;下面稍微偏离主线，介绍高阶函数。&lt;strong&gt;高阶函数&lt;/strong&gt;是把其他函数当作数据处理的函数。例如，下面的 Python 程序中，&lt;code&gt;do_twice&lt;/code&gt; 接收另一个函数作为输入，并把它连续应用到 &lt;code&gt;x&lt;/code&gt; 两次：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def tenX(x):
    return 10*x

def do_twice(f, x):
    return f(f(x))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 &lt;code&gt;print(do_twice(tenX, 2))&lt;/code&gt; 时，程序先把 &lt;code&gt;tenX&lt;/code&gt; 应用于 2，得到 20；再把 &lt;code&gt;tenX&lt;/code&gt; 应用于 20，最终得到 200。那么，在 Java 中怎样实现类似行为呢？&lt;/p&gt;
&lt;p&gt;在旧版 Java（Java 7 及更早版本）中，变量不能直接保存函数指针。也就是说，我们不能简单声明一个“函数类型”的变量，因为语言中没有直接对应的函数类型。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，可以利用接口继承。先定义一个接口，表示所有“接收一个整数并返回一个整数”的函数，称为 &lt;code&gt;IntUnaryFunction&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface IntUnaryFunction {
    int apply(int x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编写一个实现该接口的类，表示具体函数。下面的函数把输入整数乘以 10：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TenX implements IntUnaryFunction {
    /* Returns ten times the argument. */
    public int apply(int x) {
        return 10 * x;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里，我们已经用 Java 表示出了 Python 中的 &lt;code&gt;tenX&lt;/code&gt;。接着实现 &lt;code&gt;do_twice&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int do_twice(IntUnaryFunction f, int x) {
    return f.apply(f.apply(x));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 中对应的调用写作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(do_twice(new TenX(), 2));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;继承速查表&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;VengefulSLList extends SLList&lt;/code&gt; 表示：&lt;code&gt;VengefulSLList&lt;/code&gt; &lt;strong&gt;是一种&lt;/strong&gt; &lt;code&gt;SLList&lt;/code&gt;，并继承 &lt;code&gt;SLList&lt;/code&gt; 的成员：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变量；&lt;/li&gt;
&lt;li&gt;方法；&lt;/li&gt;
&lt;li&gt;嵌套类；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不包括构造方法。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;子类构造方法必须先调用超类构造方法。&lt;code&gt;super&lt;/code&gt; 关键字可以用于调用超类构造方法，也可以调用被子类重写的超类方法。&lt;/p&gt;
&lt;p&gt;重写方法的调用遵循两条核心规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器采取保守策略，只根据表达式的&lt;strong&gt;静态类型&lt;/strong&gt;允许操作；&lt;/li&gt;
&lt;li&gt;对于被重写的方法（不是重载方法），运行时实际调用哪个版本，由调用表达式所引用对象的&lt;strong&gt;动态类型&lt;/strong&gt;决定；&lt;/li&gt;
&lt;li&gt;可以通过强制类型转换覆盖编译器的静态类型判断，但错误的转换可能导致运行时异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4.3 子类型多态与高阶函数&lt;/h2&gt;
&lt;h3&gt;子类型多态&lt;/h3&gt;
&lt;p&gt;我们已经看到，继承使我们能够复用超类中的现有代码，同时通过重写超类方法或在子类中编写新方法，实现少量修改。继承还使我们能够利用&lt;strong&gt;多态&lt;/strong&gt;设计通用的数据结构和方法。&lt;/p&gt;
&lt;p&gt;“多态”的字面含义是“多种形态”。在 Java 中，多态指一个对象可以具有多种身份或类型。在面向对象编程中，一个对象既可以被看作自身类的实例，也可以被看作其超类、超类的超类等类型的实例。&lt;/p&gt;
&lt;p&gt;假设变量 &lt;code&gt;deque&lt;/code&gt; 的静态类型是 &lt;code&gt;Deque&lt;/code&gt;。调用 &lt;code&gt;deque.addFirst()&lt;/code&gt; 时，真正执行的方法会在运行时确定，取决于调用时 &lt;code&gt;deque&lt;/code&gt; 所引用对象的运行时类型，也就是动态类型。上一节已经介绍过，Java 使用动态方法选择决定调用哪个被重写的方法。&lt;/p&gt;
&lt;p&gt;假设我们想写一个 Python 程序，输出两个对象中较大者的字符串表示。可以采用两种方式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;显式高阶函数方式：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def print_larger(x, y, compare, stringify):
    if compare(x, y):
        return stringify(x)
    return stringify(y)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;子类型多态方式：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def print_larger(x, y):
    if x.largerThan(y):
        return x.str()
    return y.str()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在显式高阶函数方案中，我们把比较函数和字符串转换函数明确传入，从而以统一方式输出较大对象。相较之下，在子类型多态方案中，由对象&lt;strong&gt;自身&lt;/strong&gt;决定应该怎样比较和转换。实际调用的 &lt;code&gt;largerThan&lt;/code&gt; 实现取决于 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 究竟是什么对象。&lt;/p&gt;
&lt;h3&gt;通用 &lt;code&gt;max&lt;/code&gt; 函数&lt;/h3&gt;
&lt;p&gt;假设我们希望编写一个 &lt;code&gt;max&lt;/code&gt; 函数：它接收任意类型的数组，并返回数组中的最大元素。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.3.1。&lt;/strong&gt; 判断下面代码中有多少处编译错误。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Object max(Object[] items) {
    int maxDex = 0;
    for (int i = 0; i &amp;#x3C; items.length; i += 1) {
        if (items[i] &gt; items[maxDex]) {
            maxDex = i;
        }
    }
    return items[maxDex];
}

public static void main(String[] args) {
    Dog[] dogs = {new Dog(&quot;Elyse&quot;, 3), new Dog(&quot;Sture&quot;, 9), new Dog(&quot;Benjamin&quot;, 15)};
    Dog maxDog = (Dog) max(dogs);
    maxDog.bark();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码只有一处错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (items[i] &gt; items[maxDex]) {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会产生编译错误，因为这行代码假设 &lt;code&gt;&gt;&lt;/code&gt; 运算符可以用于任意 &lt;code&gt;Object&lt;/code&gt;，而事实并非如此。&lt;/p&gt;
&lt;p&gt;一种退让方案是在 &lt;code&gt;Dog&lt;/code&gt; 类中定义专门的 &lt;code&gt;maxDog&lt;/code&gt;，放弃编写一个能够接收任意类型数组的“唯一通用最大值函数”。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Dog maxDog(Dog[] dogs) {
    if (dogs == null || dogs.length == 0) {
        return null;
    }
    Dog maxDog = dogs[0];
    for (Dog d : dogs) {
        if (d.size &gt; maxDog.size) {
            maxDog = d;
        }
    }
    return maxDog;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码眼下可以工作，但如果我们放弃通用 &lt;code&gt;max&lt;/code&gt;，让每个类自行定义最大值方法，以后每增加一种类，就要重复编写 &lt;code&gt;maxCat&lt;/code&gt;、&lt;code&gt;maxPenguin&lt;/code&gt;、&lt;code&gt;maxWhale&lt;/code&gt; 等方法。这会产生大量重复劳动和冗余代码。&lt;/p&gt;
&lt;p&gt;根本问题是：&lt;code&gt;Object&lt;/code&gt; 之间不能直接使用 &lt;code&gt;&gt;&lt;/code&gt; 比较。这很合理，因为 Java 无法知道你究竟想按对象的字符串表示、大小，还是其他指标比较。在 Python 或 C++ 中，可以为不同类型重新定义 &lt;code&gt;&gt;&lt;/code&gt; 的含义；Java 不允许直接重载运算符，因此我们要借助接口继承。&lt;/p&gt;
&lt;p&gt;可以创建一个接口，保证所有实现类（例如 &lt;code&gt;Dog&lt;/code&gt;）都提供一个比较方法，我们称之为 &lt;code&gt;compareTo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/9920f3104cef-dog_comparable.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;先定义接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface OurComparable {
    public int compareTo(Object o);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规定 &lt;code&gt;compareTo&lt;/code&gt; 的行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;this &amp;#x3C; o&lt;/code&gt;，返回 -1；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;this&lt;/code&gt; 与 &lt;code&gt;o&lt;/code&gt; 相等，返回 0；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;this &gt; o&lt;/code&gt;，返回 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建 &lt;code&gt;OurComparable&lt;/code&gt; 后，我们可以要求 &lt;code&gt;Dog&lt;/code&gt; 实现 &lt;code&gt;compareTo&lt;/code&gt;。先在 &lt;code&gt;Dog&lt;/code&gt; 的类声明中加入 &lt;code&gt;implements OurComparable&lt;/code&gt;，再按照上述约定实现方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.3.2。&lt;/strong&gt; 为 &lt;code&gt;Dog&lt;/code&gt; 类实现 &lt;code&gt;compareTo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面使用实例变量 &lt;code&gt;size&lt;/code&gt; 进行比较：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog implements OurComparable {
    private String name;
    private int size;

    public Dog(String n, int s) {
        name = n;
        size = s;
    }

    public void bark() {
        System.out.println(name + &quot; says: bark&quot;);
    }

    public int compareTo(Object o) {
        Dog uddaDog = (Dog) o;
        if (this.size &amp;#x3C; uddaDog.size) {
            return -1;
        } else if (this.size == uddaDog.size) {
            return 0;
        }
        return 1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：由于 &lt;code&gt;compareTo&lt;/code&gt; 接收的是任意 &lt;code&gt;Object o&lt;/code&gt;，为了访问 &lt;code&gt;size&lt;/code&gt; 实例变量，我们必须先把输入&lt;strong&gt;强制转换&lt;/strong&gt;为 &lt;code&gt;Dog&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在可以把练习 4.3.1 中的 &lt;code&gt;max&lt;/code&gt; 泛化。它不再接收任意 &lt;code&gt;Object&lt;/code&gt; 数组，而是接收 &lt;code&gt;OurComparable&lt;/code&gt; 数组；我们可以确定，这些对象都实现了 &lt;code&gt;compareTo&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static OurComparable max(OurComparable[] items) {
    int maxDex = 0;
    for (int i = 0; i &amp;#x3C; items.length; i += 1) {
        int cmp = items[i].compareTo(items[maxDex]);
        if (cmp &gt; 0) {
            maxDex = i;
        }
    }
    return items[maxDex];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好。现在 &lt;code&gt;max&lt;/code&gt; 可以接收任意 &lt;code&gt;OurComparable&lt;/code&gt; 类型的对象数组，并返回其中的最大对象。不过，前面的 &lt;code&gt;compareTo&lt;/code&gt; 实现略显冗长。可以把约定修改为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;this &amp;#x3C; o&lt;/code&gt;，返回负数；&lt;/li&gt;
&lt;li&gt;如果两者相等，返回 0；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;this &gt; o&lt;/code&gt;，返回正数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是可以直接返回大小之差。若当前对象大小为 2，另一个对象大小为 5，&lt;code&gt;compareTo&lt;/code&gt; 返回 -3；负数就表示当前对象更小。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public int compareTo(Object o) {
    Dog uddaDog = (Dog) o;
    return this.size - uddaDog.size;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;借助继承，我们成功泛化了求最大值的函数。这种方案的优点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不必在每个类中重复编写最大值代码，例如无需 &lt;code&gt;Dog.maxDog(Dog[])&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;同一段代码可以较为优雅地处理多种类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;接口小测验&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;练习 4.3.3。&lt;/strong&gt; 已知 &lt;code&gt;Dog&lt;/code&gt; 类、&lt;code&gt;DogLauncher&lt;/code&gt; 类、&lt;code&gt;OurComparable&lt;/code&gt; 接口和 &lt;code&gt;Maximizer&lt;/code&gt; 类。如果从 &lt;code&gt;Dog&lt;/code&gt; 中删去 &lt;code&gt;compareTo()&lt;/code&gt;，哪个文件无法编译？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DogLauncher {
    public static void main(String[] args) {
        ...
        Dog[] dogs = new Dog[]{d1, d2, d3};
        System.out.println(Maximizer.max(dogs));
    }
}

public class Dog implements OurComparable {
    ...
    public int compareTo(Object o) {
        Dog uddaDog = (Dog) o;
        if (this.size &amp;#x3C; uddaDog.size) {
            return -1;
        } else if (this.size == uddaDog.size) {
            return 0;
        }
        return 1;
    }
    ...
}

public class Maximizer {
    public static OurComparable max(OurComparable[] items) {
        ...
        int cmp = items[i].compareTo(items[maxDex]);
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况下，&lt;code&gt;Dog&lt;/code&gt; 类会编译失败。声明 &lt;code&gt;implements OurComparable&lt;/code&gt; 就等于声称 &lt;code&gt;Dog&lt;/code&gt; 是一种 &lt;code&gt;OurComparable&lt;/code&gt;。编译器会检查这个承诺是否成立，并发现 &lt;code&gt;Dog&lt;/code&gt; 没有实现必需的 &lt;code&gt;compareTo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果改为从 &lt;code&gt;Dog&lt;/code&gt; 类声明中删去 &lt;code&gt;implements OurComparable&lt;/code&gt;，那么编译错误会出现在 &lt;code&gt;DogLauncher&lt;/code&gt; 的这一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(Maximizer.max(dogs));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;Dog&lt;/code&gt; 没有实现 &lt;code&gt;OurComparable&lt;/code&gt;，编译器就不会允许把 &lt;code&gt;Dog[]&lt;/code&gt; 传给 &lt;code&gt;Maximizer.max&lt;/code&gt;，因为 &lt;code&gt;max&lt;/code&gt; 只接受 &lt;code&gt;OurComparable[]&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Comparable&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们刚刚设计的 &lt;code&gt;OurComparable&lt;/code&gt; 可以工作，但并不完美：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;Object&lt;/code&gt; 与具体类型之间强制转换很别扭；&lt;/li&gt;
&lt;li&gt;这是我们自己发明的接口：
&lt;ul&gt;
&lt;li&gt;现有类（例如 &lt;code&gt;String&lt;/code&gt;）不会实现它；&lt;/li&gt;
&lt;li&gt;现有库也不会使用它，例如没有内置的 &lt;code&gt;max&lt;/code&gt; 专门接收 &lt;code&gt;OurComparable&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决办法是使用 Java 已经提供的 &lt;code&gt;Comparable&lt;/code&gt; 接口。&lt;code&gt;Comparable&lt;/code&gt; 被 Java 标准库和大量第三方库广泛使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Comparable&lt;/code&gt; 与 &lt;code&gt;OurComparable&lt;/code&gt; 很相似，但有一个关键差异：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/de5b376a5e16-comparable_interface.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Comparable&amp;#x3C;T&gt;&lt;/code&gt; 带有泛型参数。这样可以避免先接收 &lt;code&gt;Object&lt;/code&gt;，再把它强制转换为具体类型。我们把 &lt;code&gt;Dog&lt;/code&gt; 改为实现 &lt;code&gt;Comparable&amp;#x3C;Dog&gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Dog implements Comparable&amp;#x3C;Dog&gt; {
    ...
    public int compareTo(Dog uddaDog) {
        return this.size - uddaDog.size;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，只需把 &lt;code&gt;Maximizer&lt;/code&gt; 中的 &lt;code&gt;OurComparable&lt;/code&gt; 全部改为 &lt;code&gt;Comparable&lt;/code&gt;。此时，最大的狗就可以发出叫声了。&lt;/p&gt;
&lt;p&gt;我们不再使用自创接口，而是使用真正的内置接口 &lt;code&gt;Comparable&lt;/code&gt;。这样便可以直接利用所有已经围绕 &lt;code&gt;Comparable&lt;/code&gt; 构建好的库。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/d4dea57e5fde-comparable.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Comparator&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;刚刚介绍的 &lt;code&gt;Comparable&lt;/code&gt; 接口，把“与另一只狗比较”的能力嵌入每个 &lt;code&gt;Dog&lt;/code&gt; 对象中。接下来介绍一个非常相似的接口：&lt;code&gt;Comparator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;先定义一个术语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自然顺序（natural order）&lt;/strong&gt;：某个类的 &lt;code&gt;compareTo&lt;/code&gt; 方法所规定的默认排序方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在前面的例子中，我们按 &lt;code&gt;size&lt;/code&gt; 定义了狗的自然顺序。但如果希望使用其他方式排序，例如按名字的字母顺序，该怎么办？&lt;/p&gt;
&lt;p&gt;Java 的做法是使用 &lt;code&gt;Comparator&lt;/code&gt;。比较器本身是一个对象。我们可以在 &lt;code&gt;Dog&lt;/code&gt; 内部编写一个实现 &lt;code&gt;Comparator&lt;/code&gt; 的嵌套类。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Comparator&lt;/code&gt; 接口大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Comparator&amp;#x3C;T&gt; {
    int compare(T o1, T o2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任何实现 &lt;code&gt;Comparator&lt;/code&gt; 的类都必须实现 &lt;code&gt;compare&lt;/code&gt;。它的约定与 &lt;code&gt;compareTo&lt;/code&gt; 一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;o1 &amp;#x3C; o2&lt;/code&gt;，返回负数；&lt;/li&gt;
&lt;li&gt;如果二者相等，返回 0；&lt;/li&gt;
&lt;li&gt;如果 &lt;code&gt;o1 &gt; o2&lt;/code&gt;，返回正数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面给 &lt;code&gt;Dog&lt;/code&gt; 添加一个 &lt;code&gt;NameComparator&lt;/code&gt;。可以直接复用 &lt;code&gt;String&lt;/code&gt; 已经定义好的 &lt;code&gt;compareTo&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Comparator;

public class Dog implements Comparable&amp;#x3C;Dog&gt; {
    ...
    public int compareTo(Dog uddaDog) {
        return this.size - uddaDog.size;
    }

    private static class NameComparator implements Comparator&amp;#x3C;Dog&gt; {
        public int compare(Dog a, Dog b) {
            return a.name.compareTo(b.name);
        }
    }

    public static Comparator&amp;#x3C;Dog&gt; getNameComparator() {
        return new NameComparator();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把 &lt;code&gt;NameComparator&lt;/code&gt; 声明为静态嵌套类，因为获取一个名字比较器并不需要先实例化某只 &lt;code&gt;Dog&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以这样获得比较器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Comparator&amp;#x3C;Dog&gt; nc = Dog.getNameComparator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终，&lt;code&gt;Dog&lt;/code&gt; 类内部拥有一个私有的 &lt;code&gt;NameComparator&lt;/code&gt; 类，并提供一个返回该比较器的方法。调用者可以使用它按名字的字母顺序比较狗。&lt;/p&gt;
&lt;p&gt;从继承层次看，Java 内置了 &lt;code&gt;Comparator&lt;/code&gt; 接口；我们可以在 &lt;code&gt;Dog&lt;/code&gt; 中实现多种自己的比较器，例如 &lt;code&gt;NameComparator&lt;/code&gt;、&lt;code&gt;SizeComparator&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b788e2535eea-comparator.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;总结来说，Java 接口使我们能够实现&lt;strong&gt;回调&lt;/strong&gt;。有时，一个函数需要另一个尚未编写的函数提供帮助，例如 &lt;code&gt;max&lt;/code&gt; 需要 &lt;code&gt;compareTo&lt;/code&gt;。这个辅助函数就是回调函数。在一些语言中，可以直接把函数作为参数传递；在 Java 中，我们可以把所需函数包装在一个接口实现对象中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Comparable&lt;/code&gt; 表达的是：“我能够把自己与另一个同类型对象比较。”它嵌入对象自身，并定义该类型的&lt;strong&gt;自然顺序&lt;/strong&gt;。&lt;code&gt;Comparator&lt;/code&gt; 则更像一个独立的第三方机器，负责比较两个对象。一个类只能有一种 &lt;code&gt;compareTo&lt;/code&gt; 自然顺序；如果需要多种比较方式，就应使用多个 &lt;code&gt;Comparator&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4.4 Java 库与包&lt;/h2&gt;
&lt;h3&gt;抽象数据类型（ADT）&lt;/h3&gt;
&lt;p&gt;虽然此前没有一直明确使用这个名称，但我们实际上已经见过若干&lt;strong&gt;抽象数据类型&lt;/strong&gt;，例如 &lt;code&gt;List61B&lt;/code&gt; 和 &lt;code&gt;Deque&lt;/code&gt;。这里重点观察 &lt;code&gt;Deque&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0098828f55d9-deque.png&quot; alt=&quot;Deque&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们有一个 &lt;code&gt;Deque&lt;/code&gt; 接口，&lt;code&gt;ArrayDeque&lt;/code&gt; 和 &lt;code&gt;LinkedListDeque&lt;/code&gt; 都实现了它。&lt;code&gt;Deque&lt;/code&gt; 与这些实现类之间是什么关系？&lt;code&gt;Deque&lt;/code&gt; 只提供了一组方法，也就是行为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addFirst(T item);
public void addLast(T item);
public boolean isEmpty();
public int size();
public void printDeque();
public T removeFirst();
public T removeLast();
public T get(int index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些方法的具体代码由 &lt;code&gt;ArrayDeque&lt;/code&gt; 和 &lt;code&gt;LinkedListDeque&lt;/code&gt; 提供。&lt;/p&gt;
&lt;p&gt;在 Java 语法中，&lt;code&gt;Deque&lt;/code&gt; 是一个接口；从概念上说，它是一种&lt;strong&gt;抽象数据类型（Abstract Data Type，ADT）&lt;/strong&gt;。&lt;code&gt;Deque&lt;/code&gt; 只规定应具备哪些行为，却没有规定实现这些行为的具体方式，因此它是“抽象”的。&lt;/p&gt;
&lt;h3&gt;Java 标准库&lt;/h3&gt;
&lt;p&gt;Java 内置了很多可直接使用的抽象数据类型，它们被组织在 Java 标准库中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java.util&lt;/code&gt; 中最重要的三类 ADT 是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/List.html&quot;&gt;&lt;code&gt;List&lt;/code&gt;&lt;/a&gt;：有顺序的元素集合；
&lt;ul&gt;
&lt;li&gt;常用实现是 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html&quot;&gt;&lt;code&gt;ArrayList&lt;/code&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/7/docs/api/java/util/Set.html&quot;&gt;&lt;code&gt;Set&lt;/code&gt;&lt;/a&gt;：无顺序且元素严格唯一的集合，不允许重复；
&lt;ul&gt;
&lt;li&gt;常用实现是 &lt;a href=&quot;https://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html&quot;&gt;&lt;code&gt;HashSet&lt;/code&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/Map.html&quot;&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/a&gt;：键值对集合，通过键访问对应的值；
&lt;ul&gt;
&lt;li&gt;常用实现是 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html&quot;&gt;&lt;code&gt;HashMap&lt;/code&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请使用这三种 ADT 完成下面练习。阅读上面链接的 API 文档会非常有帮助。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.4.1。&lt;/strong&gt; 编写 &lt;code&gt;getWords&lt;/code&gt;，接收 &lt;code&gt;String inputFileName&lt;/code&gt;，把输入文件中的每个单词放入一个列表。回忆项目 0 中读取文件单词的方法。&lt;em&gt;提示：使用 &lt;code&gt;In&lt;/code&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 4.4.2。&lt;/strong&gt; 编写 &lt;code&gt;countUniqueWords&lt;/code&gt;，接收一个 &lt;code&gt;List&amp;#x3C;String&gt;&lt;/code&gt;，计算文件中有多少个&lt;strong&gt;不同的&lt;/strong&gt;单词。&lt;/p&gt;
&lt;p&gt;第一个练习使用列表，第二个练习使用集合：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static List&amp;#x3C;String&gt; getWords(String inputFileName) {
    List&amp;#x3C;String&gt; lst = new ArrayList&amp;#x3C;String&gt;();
    In in = new In();
    while (!in.isEmpty()) {
        lst.add(in.readString()); // 也可以先定义 cleanString() 清理字符串
    }
    return lst;
}

public static int countUniqueWords(List&amp;#x3C;String&gt; words) {
    Set&amp;#x3C;String&gt; ss = new HashSet&amp;#x3C;&gt;();
    for (String s : words) {
        ss.add(s);
    }
    return ss.size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 4.4.3。&lt;/strong&gt; 编写 &lt;code&gt;collectWordCount&lt;/code&gt;，接收 &lt;code&gt;List&amp;#x3C;String&gt; targets&lt;/code&gt; 和 &lt;code&gt;List&amp;#x3C;String&gt; words&lt;/code&gt;，找出每个目标单词在单词列表中出现的次数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Map&amp;#x3C;String, Integer&gt; collectWordCount(List&amp;#x3C;String&gt; words) {
    Map&amp;#x3C;String, Integer&gt; counts = new HashMap&amp;#x3C;String, Integer&gt;();
    for (String t : target) {
        counts.put(s, 0);
    }
    for (String s : words) {
        if (counts.containsKey(s)) {
            counts.put(word, counts.get(s) + 1);
        }
    }
    return counts;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用 &lt;code&gt;Map&lt;/code&gt;，因为它能够建立两个对象之间的对应关系。在本例中，我们需要把“单词”与“出现次数”关联起来。&lt;/p&gt;
&lt;p&gt;这三种 ADT 都继承自 &lt;code&gt;Collection&lt;/code&gt; 接口。&lt;code&gt;Collection&lt;/code&gt; 的定义非常宽泛：Java 文档说，集合“表示一组被称为元素的对象”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/da441549d85b-collection_hierarchy.png&quot; alt=&quot;集合层次&quot;&gt;&lt;/p&gt;
&lt;p&gt;上图中，白色方框表示接口，蓝色方框表示具体类。&lt;/p&gt;
&lt;h3&gt;Java 与 Python&lt;/h3&gt;
&lt;p&gt;Java 的语法相当冗长。下面的 Java 代码看起来比对应的 Python 代码笨重得多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/24b285073b81-java.png&quot; alt=&quot;Java 示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/cb52ed7d41dd-python.png&quot; alt=&quot;Python 示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过，Java 也有自己的优势：它提供了更多选择和工程控制。例如，Python 通常只有一种主要的字典类型，使用 &lt;code&gt;{}&lt;/code&gt; 创建；在 Java 中，如果要使用 &lt;code&gt;Map&lt;/code&gt; 这种 ADT，可以根据需求选择不同实现，例如 &lt;code&gt;HashMap&lt;/code&gt;、&lt;code&gt;TreeMap&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;CS61B 选择 Java，主要有以下原因。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从整个开发过程看，编写程序可能反而更省时间，原因包括：
&lt;ul&gt;
&lt;li&gt;静态类型提供类型检查，并引导程序员正确使用代码；&lt;/li&gt;
&lt;li&gt;偏向接口继承，使子类型多态更清晰；&lt;/li&gt;
&lt;li&gt;访问控制修饰符能建立更牢固的抽象屏障。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代码通常更加高效，原因包括：
&lt;ul&gt;
&lt;li&gt;程序员可以更精细地控制工程权衡；&lt;/li&gt;
&lt;li&gt;数组中的元素类型统一，有利于性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基础数据结构更接近底层硬件：
&lt;ul&gt;
&lt;li&gt;在 Python 中亲手实现 &lt;code&gt;ArrayDeque&lt;/code&gt; 会显得奇怪，因为语言已经隐藏了数组扩容；但真实硬件中并不存在可以自动改变长度的数组，这一点会在 CS61C 中进一步学习。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;抽象类&lt;/h3&gt;
&lt;p&gt;接口能够完成很多工作，它既支持接口继承，也可以通过 &lt;code&gt;default&lt;/code&gt; 方法提供实现继承。先回顾接口的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有方法都必须是公开的；&lt;/li&gt;
&lt;li&gt;所有变量都必须是 &lt;code&gt;public static final&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;接口不能被实例化；&lt;/li&gt;
&lt;li&gt;除非声明为 &lt;code&gt;default&lt;/code&gt;，否则方法默认是抽象的；&lt;/li&gt;
&lt;li&gt;一个类可以实现多个接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来介绍介于接口和具体类之间的结构：&lt;strong&gt;抽象类&lt;/strong&gt;。抽象类具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法可以是 &lt;code&gt;public&lt;/code&gt; 或 &lt;code&gt;private&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;可以拥有任意类型的变量；&lt;/li&gt;
&lt;li&gt;不能被实例化；&lt;/li&gt;
&lt;li&gt;方法默认有具体实现，除非显式声明为 &lt;code&gt;abstract&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;一个类只能继承一个抽象类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从能力上看，抽象类可以完成接口能做的事，并且还能做更多。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;当你拿不准时，优先尝试接口&lt;/strong&gt;，这样通常更有利于控制复杂性。&lt;/p&gt;
&lt;h3&gt;包&lt;/h3&gt;
&lt;p&gt;包名为程序中的对象提供&lt;strong&gt;规范名称（canonical name）&lt;/strong&gt;。“规范”意味着某个对象拥有唯一、明确的表示。&lt;/p&gt;
&lt;p&gt;为什么需要包名？Java 中可以存在多个同名类，我们必须区分它们。在工业项目里，常用做法是把组织的网站域名反向书写，再接上类所在的层次。例如，一个类的完整名称可能是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ug.joshh.animal.Dog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但这意味着每次实例化时都要输入完整名称：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ug.joshh.animal.Dog d = new ug.joshh.animal.Dog();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这非常麻烦。可以通过导入包来解决：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import ug.joshh.animal.Dog;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入后，就可以直接使用简短的类名 &lt;code&gt;Dog&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里只是对包的简要预览，课程后续还会继续讨论。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 5 章 泛型与自动装箱</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/05-generics-and-autoboxing</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/05-generics-and-autoboxing</guid><description>CS61B 泛型与自动装箱中文教程，介绍泛型类、泛型方法、类型参数、包装类型和 Java 自动装箱机制。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;5.1 自动装箱&lt;/h2&gt;
&lt;h3&gt;工业级 Java 语法&lt;/h3&gt;
&lt;p&gt;在前面的章节中，我们讨论了多种数据结构，以及 Java 怎样支持这些数据结构的实现。本章将补充介绍一些在工业级 Java 程序中经常出现的语言特性。&lt;/p&gt;
&lt;p&gt;这里并不打算完整讲解 Java，而是挑选最可能在本课程中用到的功能。&lt;/p&gt;
&lt;h3&gt;自动类型转换&lt;/h3&gt;
&lt;h4&gt;自动装箱与拆箱&lt;/h4&gt;
&lt;p&gt;上一章已经看到，我们可以使用 &lt;code&gt;&amp;#x3C;&gt;&lt;/code&gt; 定义带有泛型类型变量的类，例如 &lt;code&gt;LinkedListDeque&amp;#x3C;Item&gt;&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&amp;#x3C;Item&gt;&lt;/code&gt;。实例化泛型类时，必须用一个具体类替换泛型参数，也就是明确该数据结构将存放什么类型的元素。&lt;/p&gt;
&lt;p&gt;Java 有 8 种基本类型，除此之外的类型都是引用类型。Java 的一个限制是：基本类型不能直接作为泛型的实际类型参数。例如，&lt;code&gt;ArrayDeque&amp;#x3C;int&gt;&lt;/code&gt; 是语法错误，必须写成 &lt;code&gt;ArrayDeque&amp;#x3C;Integer&gt;&lt;/code&gt;。每种基本类型都有一个对应的引用类型，如图所示；这些引用类型称为&lt;strong&gt;包装类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4ddcb9a41da2-wrapper_classes.png&quot; alt=&quot;包装类&quot;&gt;&lt;/p&gt;
&lt;p&gt;乍看之下，这似乎意味着使用泛型数据结构时，必须手动在基本类型和引用类型之间转换。例如，我们可能以为需要这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BasicArrayList {
    public static void main(String[] args) {
        ArrayList&amp;#x3C;Integer&gt; L = new ArrayList&amp;#x3C;Integer&gt;();
        L.add(new Integer(5));
        L.add(new Integer(6));

        /* 把 Integer 转换为 int。 */
        int first = L.get(0).intValue();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样写很麻烦。幸运的是，Java 可以在基本类型与包装类型之间隐式转换，所以下面的代码完全合法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BasicArrayList {
    public static void main(String[] args) {
        ArrayList&amp;#x3C;Integer&gt; L = new ArrayList&amp;#x3C;Integer&gt;();
        L.add(5);
        L.add(6);
        int first = L.get(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它能够工作，是因为 Java 会在基本类型与对应引用类型之间自动进行“装箱”和“拆箱”。当 Java 期待包装类型（如 &lt;code&gt;Integer&lt;/code&gt;），而你提供基本类型（如 &lt;code&gt;int&lt;/code&gt;）时，它会自动把该整数装箱。&lt;/p&gt;
&lt;p&gt;例如，定义：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void blah(Integer x) {
    System.out.println(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x = 20;
blah(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 会隐式把 &lt;code&gt;int&lt;/code&gt; 值 20 包装成 &lt;code&gt;Integer&lt;/code&gt;。其效果近似于调用 &lt;code&gt;blah(Integer.valueOf(20))&lt;/code&gt;。这个过程称为&lt;strong&gt;自动装箱（autoboxing）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;反过来，如果 Java 期待基本类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void blahPrimitive(int x) {
    System.out.println(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而你提供对应的包装类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer x = Integer.valueOf(20);
blahPrimitive(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java 会自动取出包装对象中的基本类型值，这称为&lt;strong&gt;自动拆箱（unboxing）&lt;/strong&gt;。&lt;/p&gt;
&lt;h5&gt;注意事项&lt;/h5&gt;
&lt;p&gt;使用自动装箱和拆箱时，需要记住以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组不会自动装箱或拆箱。例如，&lt;code&gt;int[]&lt;/code&gt; 不能赋给 &lt;code&gt;Integer[]&lt;/code&gt; 变量；编译器不会允许这种转换。&lt;/li&gt;
&lt;li&gt;自动装箱和拆箱会带来可测量的性能开销。依赖自动转换的代码，通常比直接使用基本类型的代码慢。&lt;/li&gt;
&lt;li&gt;包装类型占用的内存远多于基本类型。在多数现代计算机上，不仅要保存一个 64 位对象引用，每个对象本身通常还需要额外的对象头，用于记录动态类型等信息。
&lt;ul&gt;
&lt;li&gt;关于内存占用，可以参考&lt;a href=&quot;http://www.javamex.com/tutorials/memory/object_memory_usage.shtml&quot;&gt;这篇资料&lt;/a&gt;或&lt;a href=&quot;http://blog.kiyanpro.com/2016/10/07/system_design/memory-usage-estimation-in-java/&quot;&gt;这篇资料&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;拓宽转换&lt;/h4&gt;
&lt;p&gt;与自动装箱和拆箱类似，Java 也会在需要时自动把基本类型&lt;strong&gt;拓宽（widening）&lt;/strong&gt;。具体来说，如果程序需要类型 &lt;code&gt;T2&lt;/code&gt;，实际得到类型 &lt;code&gt;T1&lt;/code&gt;，并且 &lt;code&gt;T2&lt;/code&gt; 能表示比 &lt;code&gt;T1&lt;/code&gt; 更大的取值范围，那么 Java 会把 &lt;code&gt;T1&lt;/code&gt; 隐式转换为 &lt;code&gt;T2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如，Java 中 &lt;code&gt;double&lt;/code&gt; 的表示范围比 &lt;code&gt;int&lt;/code&gt; 更宽。若有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void blahDouble(double x) {
    System.out.println(&quot;double: &quot; + x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用一个 &lt;code&gt;int&lt;/code&gt; 调用它：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int x = 20;
blahDouble(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其效果等价于 &lt;code&gt;blahDouble((double) x)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果要从更宽的类型转换为更窄的类型，则必须显式强制转换。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void blahInt(int x) {
    System.out.println(&quot;int: &quot; + x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若用 &lt;code&gt;double&lt;/code&gt; 调用，就要写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double x = 20;
blahInt((int) x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更完整的拓宽规则见 &lt;a href=&quot;http://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html&quot;&gt;Java 语言规范&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;5.2 不可变性&lt;/h2&gt;
&lt;p&gt;“不可变性”可能是一个你以前从未意识到存在的概念，但一旦理解它，就会发现它能显著简化编程生活。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不可变数据类型&lt;/strong&gt;是指：其实例在创建后，不能以任何可观察的方式发生变化。&lt;/p&gt;
&lt;p&gt;例如，Java 的 &lt;code&gt;String&lt;/code&gt; 对象是不可变的。无论对某个 &lt;code&gt;String&lt;/code&gt; 调用什么方法，原字符串对象都不会发生改变。因此，连接两个字符串时，两个原字符串都保持不变，程序会返回一个全新的 &lt;code&gt;String&lt;/code&gt; 对象。&lt;/p&gt;
&lt;p&gt;可变数据类型包括 &lt;code&gt;ArrayDeque&lt;/code&gt; 和 &lt;code&gt;Planet&lt;/code&gt;。我们可以向 &lt;code&gt;ArrayDeque&lt;/code&gt; 添加或删除元素，这些变化可以被外界观察到；同样，&lt;code&gt;Planet&lt;/code&gt; 的速度和位置也可能随时间改变。&lt;/p&gt;
&lt;p&gt;只要一个数据类型包含非私有变量，并且这些变量没有声明为 &lt;code&gt;final&lt;/code&gt;，它就是可变的。不过，这并不是可变性的唯一来源；还有很多其他方式也会让数据类型可变。非私有变量可以被外部方法修改，从而产生可观察变化。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;final&lt;/code&gt; 关键字可以用于变量，阻止它在第一次赋值后再次被赋值。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Date {
    public final int month;
    public final int day;
    public final int year;
    private boolean contrived = true;

    public Date(int m, int d, int y) {
        month = m;
        day = d;
        year = y;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个类是不可变的。创建一个 &lt;code&gt;Date&lt;/code&gt; 后，没有办法改变它的年月日属性。&lt;/p&gt;
&lt;p&gt;不可变数据类型的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;属性永远不会改变，因此可以预防一类错误，也更容易调试；&lt;/li&gt;
&lt;li&gt;可以可靠地依赖对象长期保持某种行为或特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若要“改变”某个属性，必须创建一个新对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;把一个&lt;strong&gt;引用&lt;/strong&gt;声明为 &lt;code&gt;final&lt;/code&gt;，并不会让它指向的对象变成不可变对象。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final ArrayDeque&amp;#x3C;String&gt; deque = new ArrayDeque&amp;#x3C;String&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;deque&lt;/code&gt; 变量不能被重新赋值为另一个引用，但它指向的 &lt;code&gt;ArrayDeque&lt;/code&gt; 仍然可以增加、删除元素。&lt;code&gt;ArrayDeque&lt;/code&gt; 本身始终是可变的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用反射 API，甚至可以修改私有变量。这里讨论的不可变性，默认程序没有使用这些特殊能力。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5.3 泛型&lt;/h2&gt;
&lt;h3&gt;创建另一个泛型类&lt;/h3&gt;
&lt;p&gt;前面已经创建过泛型列表，例如 &lt;code&gt;DLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt;。现在转向另一种数据类型：映射（Map）。映射把键与值关联起来。例如，“Josh 的考试成绩是 0”可以存储在一个把学生映射到考试成绩的结构中。Java 的 &lt;code&gt;Map&lt;/code&gt; 大致对应 Python 的字典。&lt;/p&gt;
&lt;p&gt;我们将创建 &lt;code&gt;ArrayMap&lt;/code&gt; 类，它实现 &lt;code&gt;Map61B&lt;/code&gt; 接口。&lt;code&gt;Map61B&lt;/code&gt; 是 Java 内置 &lt;code&gt;Map&lt;/code&gt; 接口的精简版本，包含以下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;put(key, value)：把键与值关联起来。
containsKey(key)：检查映射中是否存在该键。
get(key)：在键存在的前提下返回对应值。
keys()：返回所有键的列表。
size()：返回键的数量。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本练习暂时忽略扩容。需要注意，&lt;code&gt;Map61B&lt;/code&gt;（以及 Java 的 &lt;code&gt;Map&lt;/code&gt;）规定：每个键在同一时刻只能对应一个值。如果 Josh 当前映射到 0，后来发现成绩应为 100，那么原来的 0 会被覆盖为 100。&lt;/p&gt;
&lt;p&gt;你可以先自行尝试实现 &lt;code&gt;ArrayMap&lt;/code&gt;。下面给出完整参考实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package Map61B;

import java.util.List;
import java.util.ArrayList;

/**
 * Map61B 的数组实现。
 */
public class ArrayMap&amp;#x3C;K, V&gt; implements Map61B&amp;#x3C;K, V&gt; {

    private K[] keys;
    private V[] values;
    int size;

    public ArrayMap() {
        keys = (K[]) new Object[100];
        values = (V[]) new Object[100];
        size = 0;
    }

    /**
     * 如果键存在，返回它的索引；否则返回 -1。
     */
    private int keyIndex(K key) {
        for (int i = 0; i &amp;#x3C; size; i++) {
            if (keys[i].equals(key)) {
                return i;
            }
        }
        return -1;
    }

    public boolean containsKey(K key) {
        int index = keyIndex(key);
        return index &gt; -1;
    }

    public void put(K key, V value) {
        int index = keyIndex(key);
        if (index == -1) {
            keys[size] = key;
            values[size] = value;
            size += 1;
        } else {
            values[index] = value;
        }
    }

    public V get(K key) {
        int index = keyIndex(key);
        return values[index];
    }

    public int size() {
        return size;
    }

    public List&amp;#x3C;K&gt; keys() {
        List&amp;#x3C;K&gt; keyList = new ArrayList&amp;#x3C;&gt;();
        for (int i = 0; i &amp;#x3C; keys.length; i++) {
            keyList.add(keys[i]);
        }
        return keyList;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把泛型命名为 &lt;code&gt;K&lt;/code&gt; 和 &lt;code&gt;V&lt;/code&gt; 只是约定，并不是语法要求。完全可以改成 &lt;code&gt;Potato&lt;/code&gt;、&lt;code&gt;Sauce&lt;/code&gt; 或其他名字。但在 Java 中，用单个大写字母表示泛型参数十分常见。&lt;/p&gt;
&lt;p&gt;代码开头的 &lt;code&gt;package Map61B;&lt;/code&gt; 表示把 &lt;code&gt;ArrayMap&lt;/code&gt; 放入名为 &lt;code&gt;Map61B&lt;/code&gt; 的包。我们还从 &lt;code&gt;java.util&lt;/code&gt; 导入了 &lt;code&gt;List&lt;/code&gt; 和 &lt;code&gt;ArrayList&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.1。&lt;/strong&gt; 当前 &lt;code&gt;ArrayMap&lt;/code&gt; 实现中有一个错误，你能找到吗？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; &lt;code&gt;keys&lt;/code&gt; 方法中的循环应在 &lt;code&gt;i == size&lt;/code&gt; 时结束，而不是遍历到 &lt;code&gt;keys.length&lt;/code&gt;。否则会把未使用位置中的 &lt;code&gt;null&lt;/code&gt; 也加入结果。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ArrayMap&lt;/code&gt; 与自动装箱谜题&lt;/h3&gt;
&lt;p&gt;考虑下面的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void test() {
    ArrayMap&amp;#x3C;Integer, Integer&gt; am = new ArrayMap&amp;#x3C;Integer, Integer&gt;();
    am.put(2, 5);
    int expected = 5;
    assertEquals(expected, am.get(2));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码会产生编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ArrayMapTest.java:11: error: reference to assertEquals is ambiguous
    assertEquals(expected, am.get(2));
    ^
both method assertEquals(long, long) in Assert
and method assertEquals(Object, Object) in Assert match
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是 JUnit 的 &lt;code&gt;assertEquals&lt;/code&gt; 被重载了，例如有 &lt;code&gt;assertEquals(long expected, long actual)&lt;/code&gt; 和 &lt;code&gt;assertEquals(Object expected, Object actual)&lt;/code&gt;。调用 &lt;code&gt;assertEquals(expected, am.get(2))&lt;/code&gt; 时，Java 需要对一个参数执行装箱或拆箱，却无法唯一确定应该选哪个重载版本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.2。&lt;/strong&gt; 怎样才能明确调用 &lt;code&gt;assertEquals(long, long)&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;A. 把 &lt;code&gt;expected&lt;/code&gt; 拓宽为 &lt;code&gt;long&lt;/code&gt;；&lt;br&gt;
B. 把 &lt;code&gt;expected&lt;/code&gt; 自动装箱为 &lt;code&gt;Long&lt;/code&gt;；&lt;br&gt;
C. 把 &lt;code&gt;am.get(2)&lt;/code&gt; 拆箱；&lt;br&gt;
D. 将拆箱后的 &lt;code&gt;am.get(2)&lt;/code&gt; 再拓宽为 &lt;code&gt;long&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; A、C、D 都可行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.3。&lt;/strong&gt; 怎样让它调用 &lt;code&gt;assertEquals(Object, Object)&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 把 &lt;code&gt;expected&lt;/code&gt; 装箱为 &lt;code&gt;Integer&lt;/code&gt;，因为 &lt;code&gt;Integer&lt;/code&gt; 是一种 &lt;code&gt;Object&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.4。&lt;/strong&gt; 怎样使用显式类型转换让代码通过编译？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 把 &lt;code&gt;expected&lt;/code&gt; 转换为 &lt;code&gt;Integer&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;泛型方法&lt;/h3&gt;
&lt;p&gt;下一步创建 &lt;code&gt;MapHelper&lt;/code&gt; 类，包含两个方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;get(Map61B, key)&lt;/code&gt;：若键存在，返回对应值；否则返回 &lt;code&gt;null&lt;/code&gt;。
&lt;ul&gt;
&lt;li&gt;这很有用，因为当前 &lt;code&gt;ArrayMap.get&lt;/code&gt; 在键不存在时会用索引 -1 访问数组，从而抛出 &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxKey(Map61B)&lt;/code&gt;：返回映射中的最大键；仅适用于键可以比较的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;实现 &lt;code&gt;get&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 是一个静态方法，接收 &lt;code&gt;Map61B&lt;/code&gt; 实例和一个键。若键存在，返回对应值，否则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.5。&lt;/strong&gt; 尝试自己实现这个方法。&lt;/p&gt;
&lt;p&gt;一种非常受限的写法是把类型固定为 &lt;code&gt;String&lt;/code&gt; 和 &lt;code&gt;Integer&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static Integer get(Map61B&amp;#x3C;String, Integer&gt; map, String key) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这使方法只能接收 &lt;code&gt;Map61B&amp;#x3C;String, Integer&gt;&lt;/code&gt;，显然不够通用。我们希望它能接收任意具体泛型参数的 &lt;code&gt;Map61B&lt;/code&gt;。但下面的声明无法编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static V get(Map61B&amp;#x3C;K, V&gt; map, K key) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类声明中的泛型参数，会在用户实例化该类时确定。但这里需要的是只属于这个方法的泛型参数。我们不关心 &lt;code&gt;K&lt;/code&gt; 与 &lt;code&gt;V&lt;/code&gt; 究竟是哪种类型，只需要保证：输入映射使用某种 &lt;code&gt;K&lt;/code&gt; 作为键，并返回与之对应的 &lt;code&gt;V&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此需要&lt;strong&gt;泛型方法&lt;/strong&gt;。声明泛型方法时，要把形式类型参数写在返回类型之前：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;K, V&gt; V get(Map61B&amp;#x3C;K, V&gt; map, K key) {
    if (map.containsKey(key)) {
        return map.get(key);
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayMap&amp;#x3C;Integer, String&gt; isMap = new ArrayMap&amp;#x3C;Integer, String&gt;();
System.out.println(MapHelper.get(isMap, 5));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要显式写出本次调用的 &lt;code&gt;K&lt;/code&gt; 与 &lt;code&gt;V&lt;/code&gt;。Java 可以从 &lt;code&gt;isMap&lt;/code&gt; 推断出：键是 &lt;code&gt;Integer&lt;/code&gt;，值是 &lt;code&gt;String&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;实现 &lt;code&gt;maxKey&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.6。&lt;/strong&gt; 尝试自己实现这个方法。&lt;/p&gt;
&lt;p&gt;下面这段代码看似合理，但并不正确：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;K, V&gt; K maxKey(Map61B&amp;#x3C;K, V&gt; map) {
    List&amp;#x3C;K&gt; keyList = map.keys();
    K largest = keyList.get(0);
    for (K k : keyList) {
        if (k &gt; largest) {
            largest = k;
        }
    }
    return largest;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.7。&lt;/strong&gt; 错在哪里？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; &lt;code&gt;&gt;&lt;/code&gt; 不能用于比较任意 &lt;code&gt;K&lt;/code&gt; 对象。它只直接适用于某些基本类型，而泛型不能保证 &lt;code&gt;K&lt;/code&gt; 支持 &lt;code&gt;&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;于是尝试改写为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;K, V&gt; K maxKey(Map61B&amp;#x3C;K, V&gt; map) {
    List&amp;#x3C;K&gt; keyList = map.keys();
    K largest = keyList.get(0);
    for (K k : keyList) {
        if (k.compareTo(largest) &gt; 0) {
            largest = k;
        }
    }
    return largest;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 5.3.8。&lt;/strong&gt; 这仍然不正确，为什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 并不是所有对象都有 &lt;code&gt;compareTo&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;因此，要在方法声明中为泛型参数增加约束：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static &amp;#x3C;K extends Comparable&amp;#x3C;K&gt;, V&gt;
K maxKey(Map61B&amp;#x3C;K, V&gt; map) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;K extends Comparable&amp;#x3C;K&gt;&lt;/code&gt; 表示：键类型必须实现 &lt;code&gt;Comparable&amp;#x3C;K&gt;&lt;/code&gt;，也就是能够与其他 &lt;code&gt;K&lt;/code&gt; 比较。&lt;code&gt;Comparable&lt;/code&gt; 本身也是泛型接口，所以必须写出 &lt;code&gt;&amp;#x3C;K&gt;&lt;/code&gt;，明确我们希望 &lt;code&gt;K&lt;/code&gt; 与 &lt;code&gt;K&lt;/code&gt; 比较。&lt;/p&gt;
&lt;h3&gt;类型上界&lt;/h3&gt;
&lt;p&gt;你可能会问：&lt;code&gt;Comparable&lt;/code&gt; 明明是接口，为什么这里使用 &lt;code&gt;extends&lt;/code&gt;，而不是 &lt;code&gt;implements&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;这里的 &lt;code&gt;extends&lt;/code&gt; 与普通继承语境中的含义不同。&lt;/p&gt;
&lt;p&gt;当我们说 &lt;code&gt;Dog extends Animal&lt;/code&gt; 时，是在让 &lt;code&gt;Dog&lt;/code&gt; 获得 &lt;code&gt;Animal&lt;/code&gt; 的能力，即建立真正的继承关系。而当我们说 &lt;code&gt;K extends Comparable&amp;#x3C;K&gt;&lt;/code&gt; 时，只是在陈述约束：&lt;code&gt;K&lt;/code&gt; &lt;strong&gt;必须是&lt;/strong&gt; &lt;code&gt;Comparable&amp;#x3C;K&gt;&lt;/code&gt; 的子类型。我们不是在这里给 &lt;code&gt;K&lt;/code&gt; 添加能力，而是在限制可以作为 &lt;code&gt;K&lt;/code&gt; 的类型范围。&lt;/p&gt;
&lt;p&gt;这种用法称为&lt;strong&gt;类型上界（type upper bound）&lt;/strong&gt;。请记住：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在类继承中，&lt;code&gt;extends&lt;/code&gt; 建立继承并让子类获得超类能力；&lt;/li&gt;
&lt;li&gt;在泛型声明中，&lt;code&gt;extends&lt;/code&gt; 施加约束，要求类型参数必须属于某个上界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;用于泛型时，&lt;code&gt;extends&lt;/code&gt; 是限制条件，而不是赋予新能力。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;本章介绍了四项增强 Java 泛型能力的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本类型与包装类型之间的自动装箱和自动拆箱；&lt;/li&gt;
&lt;li&gt;基本类型之间的提升或拓宽转换；&lt;/li&gt;
&lt;li&gt;在返回类型前声明方法自己的泛型参数；&lt;/li&gt;
&lt;li&gt;在泛型方法中使用类型上界，例如 &lt;code&gt;K extends Comparable&amp;#x3C;K&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 6 章 异常、迭代器与 Object 方法</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/06-exceptions-iterators-object-methods</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/06-exceptions-iterators-object-methods</guid><description>CS61B Java 进阶中文教程，讲解异常处理、迭代器与 Iterable 接口，以及 Object 类常用方法。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;6.1 &lt;code&gt;List&lt;/code&gt;、&lt;code&gt;Set&lt;/code&gt; 与 &lt;code&gt;ArraySet&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;本节将学习怎样使用 Java 内置的 &lt;code&gt;List&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 数据结构，并亲手构建一个 &lt;code&gt;ArraySet&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;课程中已经实现过两种列表：&lt;code&gt;AList&lt;/code&gt; 和 &lt;code&gt;SLList&lt;/code&gt;。我们还创建了 &lt;code&gt;List61B&lt;/code&gt; 接口，用来规定二者必须实现的列表方法。代码见：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp19/blob/master/inheritance2/List61B.java&quot;&gt;List61B&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp19/blob/master/inheritance1/AList.java&quot;&gt;AList&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp19/blob/master/inheritance2/SLList.java&quot;&gt;SLList&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;List61B&lt;/code&gt; 的使用方式可能如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List61B&amp;#x3C;Integer&gt; L = new AList&amp;#x3C;&gt;();
L.addLast(5);
L.addLast(10);
L.addLast(15);
L.print();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实际 Java 程序中的列表&lt;/h3&gt;
&lt;p&gt;我们从零实现了列表，但 Java 已经提供内置的 &lt;code&gt;List&lt;/code&gt; 接口以及多种实现，例如 &lt;code&gt;ArrayList&lt;/code&gt;。请记住：&lt;code&gt;List&lt;/code&gt; 是接口，不能直接实例化，必须实例化它的某个实现类。&lt;/p&gt;
&lt;p&gt;可以使用类和接口的完整规范名称：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;java.util.List&amp;#x3C;Integer&gt; L = new java.util.ArrayList&amp;#x3C;&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过这样很冗长。和导入 JUnit 类似，我们可以导入 Java 标准库中的类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.List;
import java.util.ArrayList;

public class Example {
    public static void main(String[] args) {
        List&amp;#x3C;Integer&gt; L = new ArrayList&amp;#x3C;&gt;();
        L.add(5);
        L.add(10);
        System.out.println(L);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;集合&lt;/h3&gt;
&lt;p&gt;集合（Set）保存互不重复的元素，每个元素最多出现一次，而且集合本身不强调顺序。&lt;/p&gt;
&lt;h4&gt;Java&lt;/h4&gt;
&lt;p&gt;Java 提供 &lt;code&gt;Set&lt;/code&gt; 接口以及 &lt;code&gt;HashSet&lt;/code&gt; 等实现。如果不想每次写完整名称，需要先导入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Set;
import java.util.HashSet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;String&gt; s = new HashSet&amp;#x3C;&gt;();
s.add(&quot;Tokyo&quot;);
s.add(&quot;Lagos&quot;);
System.out.println(s.contains(&quot;Tokyo&quot;)); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Python&lt;/h4&gt;
&lt;p&gt;Python 中可以直接调用 &lt;code&gt;set()&lt;/code&gt;。检查是否包含某元素时，不使用方法，而使用关键字 &lt;code&gt;in&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s = set()
s.add(&quot;Tokyo&quot;)
s.add(&quot;Lagos&quot;)
print(&quot;Tokyo&quot; in s)  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;ArraySet&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们的目标是实现自己的集合 &lt;code&gt;ArraySet&lt;/code&gt;，提供以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add(value)&lt;/code&gt;：若值尚不存在，就把它加入集合；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(value)&lt;/code&gt;：检查集合是否包含该值；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size()&lt;/code&gt;：返回集合中的值数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以从&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp19/blob/af2325c600010a8894a6ce3a3ccf517547145ec1/exercises/DIY/inheritance4/ArraySet.java&quot;&gt;练习起始代码&lt;/a&gt;开始自行实现。&lt;/p&gt;
&lt;p&gt;目前的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Iterator;

public class ArraySet&amp;#x3C;T&gt; implements Iterable&amp;#x3C;T&gt; {
    private T[] items;
    private int size; // 下一个元素会被加入位置 size

    public ArraySet() {
        items = (T[]) new Object[100];
        size = 0;
    }

    /** 如果集合包含 x，返回 true。 */
    public boolean contains(T x) {
        for (int i = 0; i &amp;#x3C; size; i += 1) {
            if (items[i].equals(x)) {
                return true;
            }
        }
        return false;
    }

    /** 把 x 加入集合。 */
    public void add(T x) {
        if (contains(x)) {
            return;
        }
        items[size] = x;
        size += 1;
    }

    /** 返回集合中的元素数。 */
    public int size() {
        return size;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6.2 抛出异常&lt;/h2&gt;
&lt;p&gt;上一节的 &lt;code&gt;ArraySet&lt;/code&gt; 实现有一个小问题：如果把 &lt;code&gt;null&lt;/code&gt; 加入集合，之后可能出现 &lt;code&gt;NullPointerException&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;问题出在 &lt;code&gt;contains&lt;/code&gt; 中的 &lt;code&gt;items[i].equals(x)&lt;/code&gt;。如果 &lt;code&gt;items[i]&lt;/code&gt; 是 &lt;code&gt;null&lt;/code&gt;，代码实际上会调用 &lt;code&gt;null.equals(x)&lt;/code&gt;，从而抛出空指针异常。&lt;/p&gt;
&lt;p&gt;异常会中断正常控制流。我们也可以主动抛出自己的异常。Python 使用 &lt;code&gt;raise&lt;/code&gt;；Java 中异常本身是对象，抛出异常的一般形式为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;throw new ExceptionType(parameter1, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当用户试图向 &lt;code&gt;ArraySet&lt;/code&gt; 添加 &lt;code&gt;null&lt;/code&gt; 时，可以抛出 &lt;code&gt;IllegalArgumentException&lt;/code&gt;。它的构造方法接收一条字符串消息。&lt;/p&gt;
&lt;p&gt;更新后的 &lt;code&gt;add&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/**
 * 把 x 加入集合；如果 x 为 null，则抛出 IllegalArgumentException。
 */
public void add(T x) {
    if (x == null) {
        throw new IllegalArgumentException(&quot;can&apos;t add null&quot;);
    }
    if (contains(x)) {
        return;
    }
    items[size] = x;
    size += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论如何程序还是产生了异常，这样做为什么更好？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们掌握了代码的控制权，明确决定在何处停止正常流程；&lt;/li&gt;
&lt;li&gt;异常类型更准确，错误消息也更有助于调用者理解问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然，如果程序完全不崩溃可能更好。可以采用不同策略，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案一：传入 &lt;code&gt;null&lt;/code&gt; 时直接不添加；&lt;/li&gt;
&lt;li&gt;方案二：修改 &lt;code&gt;contains&lt;/code&gt;，明确处理 &lt;code&gt;items[i] == null&lt;/code&gt; 的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无论选择哪一种行为，都必须让使用者知道应该期待什么。这正是方法文档和注释非常重要的原因。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;6.3 迭代&lt;/h2&gt;
&lt;p&gt;Java 的 &lt;code&gt;HashSet&lt;/code&gt; 可以使用简洁的增强 &lt;code&gt;for&lt;/code&gt; 循环：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;String&gt; s = new HashSet&amp;#x3C;&gt;();
s.add(&quot;Tokyo&quot;);
s.add(&quot;Lagos&quot;);
for (String city : s) {
    System.out.println(city);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果对自己的 &lt;code&gt;ArraySet&lt;/code&gt; 使用同样语法，编译器会报错。怎样让它也支持这种功能？&lt;/p&gt;
&lt;h3&gt;增强 &lt;code&gt;for&lt;/code&gt; 循环&lt;/h3&gt;
&lt;p&gt;先理解增强 &lt;code&gt;for&lt;/code&gt; 循环背后发生了什么。下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;String&gt; s = new HashSet&amp;#x3C;&gt;();
...
for (String city : s) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大致会被转换为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Set&amp;#x3C;String&gt; s = new HashSet&amp;#x3C;&gt;();
...
Iterator&amp;#x3C;String&gt; seer = s.iterator();
while (seer.hasNext()) {
    String city = seer.next();
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去掉语法糖之后，关键是一个称为**迭代器（iterator）**的对象。&lt;/p&gt;
&lt;p&gt;例如，&lt;code&gt;List&lt;/code&gt; 接口提供一个返回迭代器对象的 &lt;code&gt;iterator()&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Iterator&amp;#x3C;E&gt; iterator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后可以使用该对象访问列表中的全部元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; friends = new ArrayList&amp;#x3C;Integer&gt;();
...
Iterator&amp;#x3C;Integer&gt; seer = friends.iterator();

while (seer.hasNext()) {
    System.out.println(seer.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它与增强 &lt;code&gt;for&lt;/code&gt; 循环的行为相同。&lt;/p&gt;
&lt;p&gt;迭代过程有三个关键步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 &lt;code&gt;friends.iterator()&lt;/code&gt; 获得新的迭代器；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;seer.hasNext()&lt;/code&gt; 判断是否还有未访问元素；有则返回 &lt;code&gt;true&lt;/code&gt;，全部处理完则返回 &lt;code&gt;false&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seer.next()&lt;/code&gt; 同时完成两件事：返回下一个元素，并把迭代器向前推进一位。因此，每个元素只会被该迭代器访问一次。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实现迭代器&lt;/h3&gt;
&lt;p&gt;下面讨论怎样让一个类支持迭代。先考虑编译器为了接受下面代码，需要知道什么：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; friends = new ArrayList&amp;#x3C;Integer&gt;();
Iterator&amp;#x3C;Integer&gt; seer = friends.iterator();

while (seer.hasNext()) {
    System.out.println(seer.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察调用相关方法的表达式静态类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;friends&lt;/code&gt; 的静态类型是 &lt;code&gt;List&lt;/code&gt;，并调用了 &lt;code&gt;iterator()&lt;/code&gt;。所以 &lt;code&gt;List&lt;/code&gt; 接口必须声明 &lt;code&gt;iterator()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seer&lt;/code&gt; 的静态类型是 &lt;code&gt;Iterator&lt;/code&gt;，并调用 &lt;code&gt;hasNext()&lt;/code&gt; 和 &lt;code&gt;next()&lt;/code&gt;。所以 &lt;code&gt;Iterator&lt;/code&gt; 接口必须声明这两个方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;List&lt;/code&gt; 接口继承 &lt;code&gt;Iterable&lt;/code&gt;，因而继承抽象的 &lt;code&gt;iterator()&lt;/code&gt; 方法。严格来说，是 &lt;code&gt;List extends Collection&lt;/code&gt;，而 &lt;code&gt;Collection extends Iterable&lt;/code&gt;，但可以先简化理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Iterable&amp;#x3C;T&gt; {
    Iterator&amp;#x3C;T&gt; iterator();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface List&amp;#x3C;T&gt; extends Iterable&amp;#x3C;T&gt; {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Iterator&lt;/code&gt; 接口则明确声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Iterator&amp;#x3C;T&gt; {
    boolean hasNext();
    T next();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;当 &lt;code&gt;hasNext()&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 后仍调用 &lt;code&gt;next()&lt;/code&gt; 会怎样？&lt;/h4&gt;
&lt;p&gt;接口本身不保证友好结果。常见约定是抛出 &lt;code&gt;NoSuchElementException&lt;/code&gt;。相关例子见&lt;a href=&quot;https://sp19.datastructur.es/materials/discussion/disc05sol.pdf&quot;&gt;讨论课 5&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;用户一定会先调用 &lt;code&gt;hasNext()&lt;/code&gt; 吗？&lt;/h4&gt;
&lt;p&gt;不一定。如果调用者准确知道序列长度，可能直接连续调用 &lt;code&gt;next()&lt;/code&gt;。因此，迭代器实现不能假定用户总会先检查；必要时可以在 &lt;code&gt;next()&lt;/code&gt; 内部调用 &lt;code&gt;hasNext()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;不同数据结构会以不同方式实现迭代。下面让 &lt;code&gt;ArraySet&lt;/code&gt; 支持遍历。先在 &lt;code&gt;ArraySet&lt;/code&gt; 内部定义嵌套类 &lt;code&gt;ArraySetIterator&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private class ArraySetIterator implements Iterator&amp;#x3C;T&gt; {
    private int wizPos;

    public ArraySetIterator() {
        wizPos = 0;
    }

    public boolean hasNext() {
        return wizPos &amp;#x3C; size;
    }

    public T next() {
        T returnItem = items[wizPos];
        wizPos += 1;
        return returnItem;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它用 &lt;code&gt;wizPos&lt;/code&gt; 记录当前数组位置，并实现 &lt;code&gt;hasNext()&lt;/code&gt; 与 &lt;code&gt;next()&lt;/code&gt;。如果数据结构换成链表，这两个方法的内部实现也会不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考练习：&lt;/strong&gt; 链表的 &lt;code&gt;hasNext()&lt;/code&gt; 和 &lt;code&gt;next()&lt;/code&gt; 应当怎样设计？&lt;/p&gt;
&lt;p&gt;现在可以手动使用迭代器：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArraySet&amp;#x3C;Integer&gt; aset = new ArraySet&amp;#x3C;&gt;();
aset.add(5);
aset.add(23);
aset.add(42);

Iterator&amp;#x3C;Integer&gt; iter = aset.iterator();
while (iter.hasNext()) {
    System.out.println(iter.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了支持增强 &lt;code&gt;for&lt;/code&gt; 循环，还要让 &lt;code&gt;ArraySet&lt;/code&gt; 实现 &lt;code&gt;Iterable&amp;#x3C;T&gt;&lt;/code&gt;。&lt;code&gt;Iterable&lt;/code&gt; 的核心方法是 &lt;code&gt;iterator()&lt;/code&gt;，只需返回刚刚编写的迭代器实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Iterator&amp;#x3C;T&gt; iterator() {
    return new ArraySetIterator();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是可以写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArraySet&amp;#x3C;Integer&gt; aset = new ArraySet&amp;#x3C;&gt;();
...
for (int i : aset) {
    System.out.println(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里出现了两个不同接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Iterable&lt;/code&gt;&lt;/strong&gt;：表示某个对象可以被遍历；要求提供 &lt;code&gt;iterator()&lt;/code&gt;，返回迭代器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Iterator&lt;/code&gt;&lt;/strong&gt;：表示真正执行遍历过程的对象；要求提供 &lt;code&gt;hasNext()&lt;/code&gt; 和 &lt;code&gt;next()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以把迭代器想象成安装在可迭代对象上的遍历机器。可迭代对象是被访问的数据容器，迭代器负责逐个取出元素。&lt;/p&gt;
&lt;p&gt;带完整迭代支持的 &lt;code&gt;ArraySet&lt;/code&gt; 如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Iterator;

public class ArraySet&amp;#x3C;T&gt; implements Iterable&amp;#x3C;T&gt; {
    private T[] items;
    private int size;

    public ArraySet() {
        items = (T[]) new Object[100];
        size = 0;
    }

    public boolean contains(T x) {
        for (int i = 0; i &amp;#x3C; size; i += 1) {
            if (items[i].equals(x)) {
                return true;
            }
        }
        return false;
    }

    public void add(T x) {
        if (x == null) {
            throw new IllegalArgumentException(&quot;can&apos;t add null&quot;);
        }
        if (contains(x)) {
            return;
        }
        items[size] = x;
        size += 1;
    }

    public int size() {
        return size;
    }

    public Iterator&amp;#x3C;T&gt; iterator() {
        return new ArraySetIterator();
    }

    private class ArraySetIterator implements Iterator&amp;#x3C;T&gt; {
        private int wizPos;

        public ArraySetIterator() {
            wizPos = 0;
        }

        public boolean hasNext() {
            return wizPos &amp;#x3C; size;
        }

        public T next() {
            T returnItem = items[wizPos];
            wizPos += 1;
            return returnItem;
        }
    }

    public static void main(String[] args) {
        ArraySet&amp;#x3C;Integer&gt; aset = new ArraySet&amp;#x3C;&gt;();
        aset.add(5);
        aset.add(23);
        aset.add(42);

        for (int i : aset) {
            System.out.println(i);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;6.4 &lt;code&gt;Object&lt;/code&gt; 方法&lt;/h2&gt;
&lt;p&gt;所有类最终都继承自 &lt;code&gt;Object&lt;/code&gt;。它提供的方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String toString()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean equals(Object obj)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Class&amp;#x3C;?&gt; getClass()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;int hashCode()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected Object clone()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;protected void finalize()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void notify()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void notifyAll()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void wait()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void wait(long timeout)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;void wait(long timeout, int nanos)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本节重点讨论前两个，并通过重写让它们表现出我们希望的行为。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;toString()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;toString()&lt;/code&gt; 返回对象的字符串表示。&lt;code&gt;System.out.println()&lt;/code&gt; 接收对象时，会隐式调用该对象的 &lt;code&gt;toString()&lt;/code&gt;，再打印返回的字符串。也就是说：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(dog);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大致相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String s = dog.toString();
System.out.println(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 默认的 &lt;code&gt;toString()&lt;/code&gt; 通常输出类名以及与对象身份相关的十六进制信息，并不是方便阅读的内容。&lt;code&gt;ArrayList&lt;/code&gt; 等标准库类重写了它，所以测试失败时可以看到类似 &lt;code&gt;[1, 2, 3, 4]&lt;/code&gt; 的表示，而不是难以理解的对象标识。&lt;/p&gt;
&lt;p&gt;对于我们自己编写的 &lt;code&gt;ArrayDeque&lt;/code&gt;、&lt;code&gt;LinkedListDeque&lt;/code&gt; 等类，如果希望打印出易读内容，就需要重写 &lt;code&gt;toString()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面为 &lt;code&gt;ArraySet&lt;/code&gt; 编写该方法。完整类中暂时留出 &lt;code&gt;toString&lt;/code&gt; 与 &lt;code&gt;equals&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.util.Iterator;

public class ArraySet&amp;#x3C;T&gt; implements Iterable&amp;#x3C;T&gt; {
    private T[] items;
    private int size;

    public ArraySet() {
        items = (T[]) new Object[100];
        size = 0;
    }

    public boolean contains(T x) {
        for (int i = 0; i &amp;#x3C; size; i += 1) {
            if (items[i].equals(x)) {
                return true;
            }
        }
        return false;
    }

    public void add(T x) {
        if (x == null) {
            throw new IllegalArgumentException(&quot;can&apos;t add null&quot;);
        }
        if (contains(x)) {
            return;
        }
        items[size] = x;
        size += 1;
    }

    public int size() {
        return size;
    }

    public Iterator&amp;#x3C;T&gt; iterator() {
        return new ArraySetIterator();
    }

    private class ArraySetIterator implements Iterator&amp;#x3C;T&gt; {
        private int wizPos;

        public ArraySetIterator() {
            wizPos = 0;
        }

        public boolean hasNext() {
            return wizPos &amp;#x3C; size;
        }

        public T next() {
            T returnItem = items[wizPos];
            wizPos += 1;
            return returnItem;
        }
    }

    @Override
    public String toString() {
        /* 待实现 */
    }

    @Override
    public boolean equals(Object other) {
        /* 待实现 */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 6.4.1。&lt;/strong&gt; 编写 &lt;code&gt;toString()&lt;/code&gt;，使 &lt;code&gt;ArraySet&lt;/code&gt; 被打印时，以花括号包围、逗号分隔元素，例如 &lt;code&gt;{1, 2, 3, 4}&lt;/code&gt;。请记住，&lt;code&gt;toString()&lt;/code&gt; 应返回字符串，而不是自行打印。&lt;/p&gt;
&lt;p&gt;一种简单实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public String toString() {
    String returnString = &quot;{&quot;;
    for (int i = 0; i &amp;#x3C; size; i += 1) {
        returnString += items[i];
        returnString += &quot;, &quot;;
    }
    returnString += &quot;}&quot;;
    return returnString;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它看起来简洁，但实际上非常低效。Java 的 &lt;code&gt;String&lt;/code&gt; 不可变，所以执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;returnString += items[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并不是修改原字符串，而是创建一个全新的字符串对象。创建和复制字符串所需时间与当前字符串长度成正比。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;附加问题：&lt;/strong&gt; 假设向字符串追加一个字符需要 1 秒，对大小为 5 的集合 &lt;code&gt;{1, 2, 3, 4, 5}&lt;/code&gt; 执行上述 &lt;code&gt;toString()&lt;/code&gt;，大约需要多久？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; 字符串会一次次重新创建，工作量累加，近似为 &lt;code&gt;1 + 2 + 3 + 4 + 5 + 6 + 7&lt;/code&gt; 个单位，而不是常数级追加。&lt;/p&gt;
&lt;p&gt;为解决这个问题，Java 提供 &lt;code&gt;StringBuilder&lt;/code&gt;。它维护一个可变的字符序列，可以反复向同一个对象追加内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 6.4.2。&lt;/strong&gt; 使用 &lt;code&gt;StringBuilder&lt;/code&gt; 重写 &lt;code&gt;toString()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public String toString() {
    StringBuilder returnSB = new StringBuilder(&quot;{&quot;);
    for (int i = 0; i &amp;#x3C; size - 1; i += 1) {
        returnSB.append(items[i].toString());
        returnSB.append(&quot;, &quot;);
    }
    if (size &gt; 0) {
        returnSB.append(items[size - 1]);
    }
    returnSB.append(&quot;}&quot;);
    return returnSB.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，我们成功重写了 &lt;code&gt;toString()&lt;/code&gt;。接下来重写另一个重要的 &lt;code&gt;Object&lt;/code&gt; 方法：&lt;code&gt;equals()&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;equals()&lt;/code&gt; 与 &lt;code&gt;==&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Java 中，&lt;code&gt;equals()&lt;/code&gt; 和 &lt;code&gt;==&lt;/code&gt; 的含义不同。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;==&lt;/code&gt; 检查两个变量中的位是否相同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对基本类型，它比较值；&lt;/li&gt;
&lt;li&gt;对引用类型，它比较引用，也就是两个变量是否指向内存中的同一个对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;假设有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Doge {
    public int age;
    public String name;

    public Doge(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public static void main(String[] args) {
        int x = 5;
        int y = 5;
        int z = 6;

        Doge fido = new Doge(5, &quot;Fido&quot;);
        Doge doggo = new Doge(6, &quot;Doggo&quot;);
        Doge fidoTwin = new Doge(5, &quot;Fido&quot;);
        Doge fidoRealTwin = fido;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其盒子与指针关系如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bf408e0e79f2-Doge.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习：&lt;/strong&gt; 下列表达式分别返回什么？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;x == y&lt;/code&gt;：&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x == z&lt;/code&gt;：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fido == doggo&lt;/code&gt;：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fido == fidoTwin&lt;/code&gt;：&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fido == fidoRealTwin&lt;/code&gt;：&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;fido&lt;/code&gt; 与 &lt;code&gt;fidoTwin&lt;/code&gt; 的属性完全相同，但它们指向两个不同对象，所以 &lt;code&gt;==&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;。这在测试中会造成问题。例如，测试列表时，期望列表通常是新创建的对象；如果用 &lt;code&gt;==&lt;/code&gt; 比较，即使元素相同也会得到 &lt;code&gt;false&lt;/code&gt;。这正是 &lt;code&gt;equals(Object o)&lt;/code&gt; 的用途。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;equals(Object o)&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Object&lt;/code&gt; 默认的 &lt;code&gt;equals(Object o)&lt;/code&gt; 与 &lt;code&gt;==&lt;/code&gt; 类似，比较对象身份。但我们可以重写它，自行定义什么叫“相等”。例如，两个列表只要包含相同且顺序一致的元素，就可以视为相等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 6.4.3。&lt;/strong&gt; 为 &lt;code&gt;ArraySet&lt;/code&gt; 编写 &lt;code&gt;equals&lt;/code&gt;。集合是无序且元素唯一的容器，所以两个集合只需包含相同元素即可视为相等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Override
public boolean equals(Object other) {
    if (this == other) {
        return true;
    }
    if (other == null) {
        return false;
    }
    if (other.getClass() != this.getClass()) {
        return false;
    }

    ArraySet&amp;#x3C;T&gt; o = (ArraySet&amp;#x3C;T&gt;) other;
    if (o.size() != this.size()) {
        return false;
    }
    for (T item : this) {
        if (!o.contains(item)) {
            return false;
        }
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开头的检查让方法能够正确处理 &lt;code&gt;null&lt;/code&gt; 和不同类型的对象。如果 &lt;code&gt;this == other&lt;/code&gt;，则立即返回 &lt;code&gt;true&lt;/code&gt;，避免多余遍历。&lt;/p&gt;
&lt;h4&gt;Java 中 &lt;code&gt;equals&lt;/code&gt; 的规则&lt;/h4&gt;
&lt;p&gt;重写 &lt;code&gt;equals&lt;/code&gt; 比看起来更容易出错，应遵守以下约定：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;equals&lt;/code&gt; 必须构成等价关系：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自反性&lt;/strong&gt;：&lt;code&gt;x.equals(x)&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对称性&lt;/strong&gt;：&lt;code&gt;x.equals(y)&lt;/code&gt; 当且仅当 &lt;code&gt;y.equals(x)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传递性&lt;/strong&gt;：若 &lt;code&gt;x.equals(y)&lt;/code&gt; 且 &lt;code&gt;y.equals(z)&lt;/code&gt;，则 &lt;code&gt;x.equals(z)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;参数必须是 &lt;code&gt;Object&lt;/code&gt;，才能真正重写原方法；&lt;/li&gt;
&lt;li&gt;必须保持一致：只要 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 没有改变，多次调用 &lt;code&gt;x.equals(y)&lt;/code&gt; 的结果就应相同；&lt;/li&gt;
&lt;li&gt;对任何非空对象 &lt;code&gt;x&lt;/code&gt;，&lt;code&gt;x.equals(null)&lt;/code&gt; 必须为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更多改进，包括更完善的 &lt;code&gt;toString&lt;/code&gt; 和 &lt;code&gt;ArraySet.of&lt;/code&gt;，可参考&lt;a href=&quot;https://github.com/Berkeley-CS61B/lectureCode-sp19/blob/af2325c600010a8894a6ce3a3ccf517547145ec1/inheritance4/ArraySet.java&quot;&gt;附加代码&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;附录：抛出异常（旧版）&lt;/h2&gt;
&lt;h3&gt;抛出异常&lt;/h3&gt;
&lt;p&gt;当程序发生严重问题时，我们通常希望打断正常控制流：继续执行可能没有意义，甚至根本不可能。此时，程序会抛出异常。&lt;/p&gt;
&lt;p&gt;先看一个熟悉的例子：数组索引越界。下面的代码把键 &lt;code&gt;&quot;hello&quot;&lt;/code&gt; 映射到值 5，然后尝试读取不存在的键 &lt;code&gt;&quot;yolp&quot;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    ArrayMap&amp;#x3C;String, Integer&gt; am = new ArrayMap&amp;#x3C;String, Integer&gt;();
    am.put(&quot;hello&quot;, 5);
    System.out.println(am.get(&quot;yolp&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行时，程序试图访问不存在的键并崩溃：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Exception in thread &quot;main&quot; java.lang.ArrayIndexOutOfBoundsException: -1
    at ArrayMap.get(ArrayMap.java:38)
    at ExceptionDemo.main(ExceptionDemo.java:6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个&lt;strong&gt;隐式异常&lt;/strong&gt;，由 Java 自己抛出。错误消息告诉我们出现了 &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;，但没有直接说明真正原因是键不存在。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;code&gt;throw&lt;/code&gt; 主动抛出异常，并给出更有意义的消息。这属于&lt;strong&gt;显式异常&lt;/strong&gt;，因为是程序员有意抛出的。&lt;/p&gt;
&lt;p&gt;例如，修改 &lt;code&gt;get&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public V get(K key) {
    int location = findKey(key);
    if (location &amp;#x3C; 0) {
        throw new IllegalArgumentException(
            &quot;Key &quot; + key + &quot; does not exist in map.&quot;);
    }
    return values[location];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在错误更明确：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Exception in thread &quot;main&quot; java.lang.IllegalArgumentException:
Key yolp does not exist in map.
    at ArrayMap.get(ArrayMap.java:40)
    at ExceptionDemo.main(ExceptionDemo.java:6)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;捕获异常&lt;/h3&gt;
&lt;p&gt;程序运行时可能出现各种异常事件，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;试图申请 383,124 GB 内存；&lt;/li&gt;
&lt;li&gt;把动态类型并非 &lt;code&gt;Dog&lt;/code&gt; 的对象强制转换为 &lt;code&gt;Dog&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;通过值为 &lt;code&gt;null&lt;/code&gt; 的引用调用方法；&lt;/li&gt;
&lt;li&gt;访问数组的 -1 号索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面的错误类型转换：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Object o = &quot;mulchor&quot;;
Planet x = (Planet) o;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会导致：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Exception in thread &quot;main&quot; java.lang.ClassCastException:
java.lang.String cannot be cast to Planet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以主动抛出异常：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    System.out.println(&quot;ayyy lmao&quot;);
    throw new RuntimeException(&quot;For no reason.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ayyy lmao
Exception in thread &quot;main&quot; java.lang.RuntimeException: For no reason.
    at Alien.main(Alien.java:4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;new RuntimeException(...)&lt;/code&gt; 看起来像实例化类，因为事实正是如此：异常也是普通 Java 对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/3ed88deccda0-exceptions.png&quot; alt=&quot;异常类层次&quot;&gt;&lt;/p&gt;
&lt;p&gt;异常不一定必须让程序崩溃。可以使用 &lt;code&gt;try&lt;/code&gt; 和 &lt;code&gt;catch&lt;/code&gt; &lt;strong&gt;捕获&lt;/strong&gt;异常，从而转移控制流并继续执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Dog d = new Dog(&quot;Lucy&quot;, &quot;Retriever&quot;, 80);
d.becomeAngry();

try {
    d.receivePat();
} catch (Exception e) {
    System.out.println(&quot;Tried to pat: &quot; + e);
}
System.out.println(d);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy is a displeased Retriever weighing 80.0 standard lb units.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抚摸生气的狗时，&lt;code&gt;receivePat&lt;/code&gt; 抛出异常；但异常被捕获，所以程序仍会执行最后一行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;catch&lt;/code&gt; 还可以采取补救行动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Dog d = new Dog(&quot;Lucy&quot;, &quot;Retriever&quot;, 80);
d.becomeAngry();

try {
    d.receivePat();
} catch (Exception e) {
    System.out.println(&quot;Tried to pat: &quot; + e);
    d.eatTreat(&quot;banana&quot;);
}
d.receivePat();
System.out.println(d);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出可能是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Tried to pat: java.lang.RuntimeException: grrr... snarl snarl
Lucy munches the banana
Lucy enjoys the pat.
Lucy is a happy Retriever weighing 80.0 standard lb units.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现实中，补救行动可能是在机器人操作要求天线就绪却失败时展开天线，也可能只是把错误写入日志，供以后分析。&lt;/p&gt;
&lt;h3&gt;异常处理的思想&lt;/h3&gt;
&lt;p&gt;异常不是处理错误的唯一方式，但它有一个重要优势：把错误处理从程序的主要逻辑中分离出来。&lt;/p&gt;
&lt;p&gt;考虑读取文件的伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;readFile:
    打开文件；
    确定文件大小；
    分配相应内存；
    把文件读入内存；
    关闭文件；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很多步骤都可能失败：文件不存在、内存不足、读取失败等。若不用异常，代码可能充满层层嵌套的条件判断：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;readFile:
    打开文件；
    如果打开成功：
        确定大小；
        如果成功获得大小：
            分配内存；
            如果内存足够：
                读取文件；
                如果读取失败：返回读取错误；
            否则：返回内存错误；
        否则：返回长度错误；
    否则：返回打开文件错误；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种代码非常混乱。使用异常后可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;readFile:
    try:
        打开文件；
        确定文件大小；
        分配内存；
        读取文件；
        关闭文件；
    catch 打开失败：执行对应处理；
    catch 获取大小失败：执行对应处理；
    catch 分配内存失败：执行对应处理；
    catch 读取失败：执行对应处理；
    catch 关闭失败：执行对应处理；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主逻辑按清晰叙事依次展开，错误则集中放在后面的处理分支中。好的代码像一段故事，结构清楚，因而更容易编写和维护。&lt;/p&gt;
&lt;h3&gt;未捕获异常&lt;/h3&gt;
&lt;p&gt;异常被抛出后，会沿调用栈向下传播。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/613d9f5fcd67-callstack.png&quot; alt=&quot;调用栈&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;peek()&lt;/code&gt; 没有捕获异常，它会传播给调用者 &lt;code&gt;sample()&lt;/code&gt;；可以把这理解为当前方法从调用栈弹出。若 &lt;code&gt;sample()&lt;/code&gt; 也不捕获，它会继续传播到 &lt;code&gt;main()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若异常一直到调用栈底部仍未被捕获，程序就会崩溃，并输出&lt;strong&gt;栈轨迹（stack trace）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;java.lang.RuntimeException in thread &quot;main&quot;:
    at ArrayRingBuffer.peek:63
    at GuitarString.sample:48
    at GuitarHeroLite.java:110
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栈轨迹表明：错误发生在 &lt;code&gt;ArrayRingBuffer.peek()&lt;/code&gt; 第 63 行；它由 &lt;code&gt;GuitarString.sample()&lt;/code&gt; 第 48 行调用，而后者又由 &lt;code&gt;GuitarHeroLite.java&lt;/code&gt; 的 &lt;code&gt;main&lt;/code&gt; 第 110 行调用。对于能够修改程序的开发者，这些信息非常重要。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;附录：受检异常与非受检异常（旧版）&lt;/h2&gt;
&lt;p&gt;此前看到的异常都发生在运行时。但有时程序甚至无法编译，编译器会说某个异常“必须被捕获或声明抛出”。&lt;/p&gt;
&lt;p&gt;原因是，有些异常被编译器视为必须显式处理的情况。它们称为&lt;strong&gt;受检异常（checked exception）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;考虑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    Eagle.gulgate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码看似合理，但可能得到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What.java:2: error: unreported exception IOException;
must be caught or declared to be thrown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看 &lt;code&gt;Eagle&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Eagle {
    public static void gulgate() {
        if (today.equals(&quot;Thursday&quot;)) {
            throw new IOException(&quot;hi&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;gulgate()&lt;/code&gt; 在星期四会抛出 &lt;code&gt;IOException&lt;/code&gt;。但 &lt;code&gt;IOException&lt;/code&gt; 是受检异常，必须以规定方式处理。&lt;/p&gt;
&lt;p&gt;如果改为 &lt;code&gt;RuntimeException&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class UncheckedExceptionDemo {
    public static void main(String[] args) {
        if (today.equals(&quot;Thursday&quot;)) {
            throw new RuntimeException(&quot;as a joke&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码可以正常编译，只会在星期四运行时崩溃。&lt;code&gt;RuntimeException&lt;/code&gt; 属于&lt;strong&gt;非受检异常（unchecked exception）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;怎样区分两者？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0c42e9987984-checked_exceptions.png&quot; alt=&quot;受检异常层次&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Error&lt;/code&gt;、&lt;code&gt;RuntimeException&lt;/code&gt; 以及它们的所有子类都是非受检异常。这些问题通常只能在运行时发现，而且往往无法在当前操作中恢复。例如，代码试图读取数组的 -1 号元素时，很难自动补救。&lt;/p&gt;
&lt;p&gt;其他大多数异常属于受检异常，其中许多存在合理恢复方式。例如遇到 &lt;code&gt;FileNotFoundException&lt;/code&gt; 时，可以要求用户重新输入文件名，因为可能只是拼写错误。&lt;/p&gt;
&lt;p&gt;Java 希望尽可能阻止程序因可修复问题直接崩溃，所以如果某个受检异常可能发生，却没有任何处理，编译器就拒绝编译。&lt;/p&gt;
&lt;p&gt;处理受检异常有两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;捕获（catch）&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明（specify）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;捕获&lt;/h4&gt;
&lt;p&gt;可以直接在 &lt;code&gt;gulgate()&lt;/code&gt; 内捕获：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void gulgate() {
    try {
        if (today.equals(&quot;Thursday&quot;)) {
            throw new IOException(&quot;hi&quot;);
        }
    } catch (Exception e) {
        System.out.println(&quot;psych!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;声明抛出&lt;/h4&gt;
&lt;p&gt;如果不想在 &lt;code&gt;gulgate()&lt;/code&gt; 内处理，可以把责任交给调用者，在方法声明中写出 &lt;code&gt;throws&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void gulgate() throws IOException {
    ...
    throw new IOException(&quot;hi&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但“声明”并没有真正处理异常，只是说明该方法可能把异常传播出去。调用 &lt;code&gt;gulgate()&lt;/code&gt; 的方法也因此变得危险：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    Eagle.gulgate(); // 仍无法编译
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用者同样必须二选一。&lt;/p&gt;
&lt;p&gt;捕获：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    try {
        Eagle.gulgate();
    } catch (IOException e) {
        System.out.println(&quot;Averted!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) throws IOException {
    Eagle.gulgate();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你能在当前方法中真正处理问题时，应该&lt;strong&gt;捕获&lt;/strong&gt;异常，不让它继续逃逸。&lt;/p&gt;
&lt;p&gt;当更上层调用者更适合处理问题时，应该&lt;strong&gt;声明&lt;/strong&gt;异常，明确告诉调用者该方法可能失败。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;附录：迭代（旧版）&lt;/h2&gt;
&lt;p&gt;Java 允许我们使用便捷的 &lt;code&gt;foreach&lt;/code&gt;，也称增强 &lt;code&gt;for&lt;/code&gt; 循环，遍历列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; friends = new ArrayList&amp;#x3C;Integer&gt;();
friends.add(5);
friends.add(23);
friends.add(42);
for (int x : friends) {
    System.out.println(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了让自己的类支持这种语法，需要理解背后的机制。关键对象是&lt;strong&gt;迭代器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;List&lt;/code&gt; 中可以定义返回迭代器的 &lt;code&gt;iterator()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Iterator&amp;#x3C;E&gt; iterator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后手动遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; friends = new ArrayList&amp;#x3C;Integer&gt;();
...
Iterator&amp;#x3C;Integer&gt; seer = friends.iterator();

while (seer.hasNext()) {
    System.out.println(seer.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它与增强 &lt;code&gt;for&lt;/code&gt; 循环行为相同：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;friends.iterator()&lt;/code&gt; 返回新迭代器；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hasNext()&lt;/code&gt; 判断是否还有未访问元素；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next()&lt;/code&gt; 返回下一个元素，并把迭代器向前推进。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;实现迭代器&lt;/h3&gt;
&lt;p&gt;为了编译下面代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Integer&gt; friends = new ArrayList&amp;#x3C;Integer&gt;();
Iterator&amp;#x3C;Integer&gt; seer = friends.iterator();

while (seer.hasNext()) {
    System.out.println(seer.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器需要确认：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;List&lt;/code&gt; 接口是否拥有 &lt;code&gt;iterator()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Iterator&lt;/code&gt; 接口是否拥有 &lt;code&gt;hasNext()&lt;/code&gt; 和 &lt;code&gt;next()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;List&lt;/code&gt; 继承 &lt;code&gt;Iterable&lt;/code&gt;，而 &lt;code&gt;Iterable&lt;/code&gt; 声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface Iterable&amp;#x3C;T&gt; {
    Iterator&amp;#x3C;T&gt; iterator();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface List&amp;#x3C;T&gt; extends Iterable&amp;#x3C;T&gt; {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Iterator&lt;/code&gt; 则声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package java.util;

public interface Iterator&amp;#x3C;T&gt; {
    boolean hasNext();
    T next();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体类负责提供实际遍历行为。下面让 &lt;code&gt;ArrayMap&lt;/code&gt; 能够遍历键。先在内部定义 &lt;code&gt;KeyIterator&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class KeyIterator {
    private int ptr;

    public KeyIterator() {
        ptr = 0;
    }

    public boolean hasNext() {
        return ptr != size;
    }

    public K next() {
        K returnItem = keys[ptr];
        ptr = ptr + 1;
        return returnItem;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ptr&lt;/code&gt; 记录当前位于键数组中的位置。对于不同数据结构，&lt;code&gt;hasNext()&lt;/code&gt; 与 &lt;code&gt;next()&lt;/code&gt; 的实现会不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考练习：&lt;/strong&gt; 链表迭代器应怎样实现这两个方法？&lt;/p&gt;
&lt;p&gt;现在可以这样遍历 &lt;code&gt;ArrayMap&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayMap&amp;#x3C;String, Integer&gt; am = new ArrayMap&amp;#x3C;String, Integer&gt;();
am.put(&quot;hello&quot;, 5);
am.put(&quot;syrups&quot;, 10);
ArrayMap.KeyIterator ami = am.new KeyIterator();

while (ami.hasNext()) {
    System.out.println(ami.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;am.new KeyIterator()&lt;/code&gt; 用于实例化非静态嵌套类。这样的嵌套类必须依附于特定外部类实例。一个与任何 &lt;code&gt;ArrayMap&lt;/code&gt; 都无关的键迭代器没有意义，因为它不知道该遍历哪张映射。因此，要通过某个具体 &lt;code&gt;am&lt;/code&gt; 创建它。&lt;/p&gt;
&lt;p&gt;为了支持增强 &lt;code&gt;for&lt;/code&gt;，还要让 &lt;code&gt;ArrayMap&lt;/code&gt; 实现 &lt;code&gt;Iterable&amp;#x3C;K&gt;&lt;/code&gt;，并重写 &lt;code&gt;iterator()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ArrayMap&amp;#x3C;K, V&gt; implements Iterable&amp;#x3C;K&gt; {
    @Override
    public Iterator&amp;#x3C;K&gt; iterator() {
        return new KeyIterator();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，还必须告诉 Java：&lt;code&gt;KeyIterator&lt;/code&gt; 确实是一种 &lt;code&gt;Iterator&amp;#x3C;K&gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class KeyIterator implements Iterator&amp;#x3C;K&gt; {
    private int ptr;

    public KeyIterator() {
        ptr = 0;
    }

    public boolean hasNext() {
        return ptr != size;
    }

    public K next() {
        K returnItem = keys[ptr];
        ptr += 1;
        return returnItem;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以使用增强 &lt;code&gt;for&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ArrayMap&amp;#x3C;String, Integer&gt; am = new ArrayMap&amp;#x3C;String, Integer&gt;();
for (String s : am) {
    System.out.println(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Iterable&lt;/code&gt; 表示某个类可以被遍历，要求 &lt;code&gt;iterator()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Iterator&lt;/code&gt; 表示执行遍历的对象，要求 &lt;code&gt;hasNext()&lt;/code&gt; 与 &lt;code&gt;next()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把二者组合起来，就能让自己的类支持简洁的增强 &lt;code&gt;for&lt;/code&gt; 循环。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 7 章 包与访问控制</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/07-packages-and-access-control</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/07-packages-and-access-control</guid><description>CS61B 包与访问控制中文教程，介绍 Java 包结构、导入规则、访问修饰符和模块化代码组织方式。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;7.1 包&lt;/h2&gt;
&lt;p&gt;世界上存在海量代码，不同项目很可能创建同名类。怎样组织这些类，减少访问和使用时的歧义？程序怎样知道你想使用自己的 &lt;code&gt;Dog.class&lt;/code&gt;，而不是 Josh Hug 的 &lt;code&gt;Dog.class&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;这正是**包（package）**的作用：包是组织类与接口的命名空间。通常，包名应以组织的网站域名反向书写作为开头。&lt;/p&gt;
&lt;p&gt;例如，Josh Hug 若要发布一个包含多种动物类的 &lt;code&gt;animal&lt;/code&gt; 包，可以命名为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ug.joshh.animal  // 他的网站是 joshh.ug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 CS61B 作业中，代码并不用于正式分发，所以不强制遵守该命名约定。&lt;/p&gt;
&lt;h3&gt;使用包&lt;/h3&gt;
&lt;p&gt;如果访问同一个包中的类，可以直接使用简单类名：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Dog d = new Dog(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果从包外访问，可以使用完整规范名称：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ug.joshh.animal.Dog d = new ug.joshh.animal.Dog(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了简化代码，可以先导入该类，再使用简单名称：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import ug.joshh.animal.Dog;

Dog d = new Dog(...);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建包&lt;/h3&gt;
&lt;p&gt;创建包需要两步。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在包中每个源文件顶部写出包声明：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package ug.joshh.animal;

public class Dog {
    private String name;
    private String breed;
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;把文件存放在与包名对应的目录结构中。例如，包 &lt;code&gt;ug.joshh.animal&lt;/code&gt; 应位于：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ug/joshh/animal
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;在 IntelliJ 中创建包&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;选择 &lt;code&gt;File → New Package&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;输入包名，例如 &lt;code&gt;ug.joshh.animal&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;向包中添加新的 Java 文件&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;右键包名；&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;New → Java Class&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;输入类名。IntelliJ 会自动把文件放到正确目录，并添加 &lt;code&gt;package ug.joshh.animal;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;把旧 Java 文件移入包&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;在文件顶部添加 &lt;code&gt;package 包名;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;.java&lt;/code&gt; 文件移动到对应目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;默认包&lt;/h3&gt;
&lt;p&gt;源文件顶部没有显式包声明的 Java 类，会自动属于&lt;strong&gt;默认包&lt;/strong&gt;。编写真正的程序时，应尽量避免使用默认包，除非只是非常小的演示程序。&lt;/p&gt;
&lt;p&gt;原因包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认包中的代码不能被其他有名包正常导入；&lt;/li&gt;
&lt;li&gt;所有未命名包中的类都混在一起，更容易发生类名冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，若在默认包中创建 &lt;code&gt;DogLauncher.java&lt;/code&gt;，包外代码无法通过一个合法的规范名称访问它：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;DogLauncher.launch();          // 在其他包中不能这样用
// default.DogLauncher.launch();  // 根本不存在名为 default 的包
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，实际 Java 源文件通常都应以明确的包声明开头。&lt;/p&gt;
&lt;h3&gt;JAR 文件&lt;/h3&gt;
&lt;p&gt;程序通常包含多个 &lt;code&gt;.class&lt;/code&gt; 文件。若要分发程序，与其把全部类文件连同特殊目录逐一发送，不如创建一个 JAR 文件，把它们打包在一起。单个 &lt;code&gt;.jar&lt;/code&gt; 文件会包含所有 &lt;code&gt;.class&lt;/code&gt; 文件以及一些附加元数据。&lt;/p&gt;
&lt;p&gt;JAR 本质上很像 ZIP 压缩包，可以解压并检查内部类文件，也可能通过反编译恢复近似源代码。JAR 并不能保护你的代码，因此不要把课程项目 JAR 分享给其他学生。&lt;/p&gt;
&lt;h4&gt;在 IntelliJ 中创建 JAR&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;进入 &lt;code&gt;File → Project Structure → Artifacts → JAR → From modules with dependencies&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;连续确认相关设置；&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;Build → Build Artifacts&lt;/code&gt;，JAR 通常会生成在 &lt;code&gt;Artifacts&lt;/code&gt; 目录；&lt;/li&gt;
&lt;li&gt;其他 Java 程序员可以把该 JAR 导入自己的项目。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;构建系统&lt;/h3&gt;
&lt;p&gt;如果每次创建项目都要手动导入大量库并放置文件，会非常麻烦。&lt;strong&gt;构建系统&lt;/strong&gt;可以自动完成项目结构、依赖获取、编译与打包等步骤。&lt;/p&gt;
&lt;p&gt;在大型团队和项目中，自动化项目搭建过程尤其重要。它在 CS61B 小项目中的优势没有那么明显，但部分课程项目使用过 Maven。其他常见构建系统还包括 Ant 和 Gradle。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7.2 访问控制&lt;/h2&gt;
&lt;h3&gt;访问修饰符&lt;/h3&gt;
&lt;p&gt;现在考虑：公开和私有成员在包与子类中究竟怎样工作？子类能访问父类的私有成员吗？同一包中的两个类能访问彼此的私有成员吗？&lt;/p&gt;
&lt;p&gt;Java 提供四种主要访问级别。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;private&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;只有声明该成员的类内部代码可以访问。子类、同包其他类以及所有外部类都不能访问。&lt;/p&gt;
&lt;p&gt;简而言之：&lt;strong&gt;只有这个类自己需要知道。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;包私有（package-private）&lt;/h4&gt;
&lt;p&gt;成员没有显式访问修饰符时，默认就是包私有。同一个包中的类可以访问它，但仅仅作为子类并不能获得访问权。&lt;/p&gt;
&lt;p&gt;这很有用，因为同一个包通常由同一组开发者维护，他们了解内部实现；而其他人可能继承你的类，却不应随意修改某些内部成员。&lt;/p&gt;
&lt;p&gt;简而言之：&lt;strong&gt;只有同一个包中的类可以访问。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;同一个包中的类以及子类都可以访问，但普通包外类不能访问。&lt;/p&gt;
&lt;p&gt;简而言之：&lt;strong&gt;子类型可能需要，但子类型的普通使用者不需要。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;public&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;任何地方都可以访问。包的使用者通常依赖公开成员。一旦公开发布，公开 API 的签名不应随意改变，因为它构成了对使用者的承诺。&lt;/p&gt;
&lt;p&gt;如果开发者希望逐步淘汰某个公开功能，通常不会立即删除，而是先标记为“已弃用（deprecated）”。&lt;/p&gt;
&lt;p&gt;简而言之：&lt;strong&gt;向全世界开放并作出兼容承诺。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 7.2.1。&lt;/strong&gt; 凭记忆画出访问权限表。列为：修饰符、同一类、同一包、子类、全世界；行为：&lt;code&gt;public&lt;/code&gt;、&lt;code&gt;protected&lt;/code&gt;、包私有、&lt;code&gt;private&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a572888e74c6-access_modifiers.png&quot; alt=&quot;访问修饰符表&quot;&gt;&lt;/p&gt;
&lt;h3&gt;访问控制的细节&lt;/h3&gt;
&lt;h4&gt;默认包&lt;/h4&gt;
&lt;p&gt;没有包声明的代码自动属于默认包。如果这些类拥有未写访问修饰符的包私有成员，由于它们都属于同一个未命名默认包，彼此仍然可以访问这些成员。&lt;/p&gt;
&lt;h4&gt;访问判断只依据静态类型&lt;/h4&gt;
&lt;p&gt;接口中的方法默认是 &lt;code&gt;public&lt;/code&gt;，不是包私有。另外，能否访问某个成员，编译器只根据表达式的&lt;strong&gt;静态类型&lt;/strong&gt;判断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 7.2.2。&lt;/strong&gt; 对下面代码，&lt;code&gt;demoAccess&lt;/code&gt; 中哪些行会在编译时出错？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package universe;

public interface BlackHole {
    void add(Object x); // 接口方法是 public，不是包私有
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package universe;

public class CreationUtils {
    public static BlackHole hirsute() {
        return new HasHair();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package universe;

class HasHair implements BlackHole {
    Object[] items;

    public void add(Object o) { ... }
    public Object get(int k) { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import static universe.CreationUtils.hirsute;

class Client {
    void demoAccess() {
        BlackHole b = hirsute();
        b.add(&quot;horse&quot;);
        b.get(0);
        HasHair hb = (HasHair) b;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;b.get(0);&lt;/code&gt; 会出错。&lt;code&gt;b&lt;/code&gt; 的静态类型是 &lt;code&gt;BlackHole&lt;/code&gt;，而该接口没有声明 &lt;code&gt;get&lt;/code&gt;。即使我们知道它运行时指向 &lt;code&gt;HasHair&lt;/code&gt;，编译器仍按静态类型检查。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HasHair hb = (HasHair) b;&lt;/code&gt; 也会出错。&lt;code&gt;HasHair&lt;/code&gt; 没有声明为 &lt;code&gt;public&lt;/code&gt;，因此它是包私有类。位于 &lt;code&gt;universe&lt;/code&gt; 包外的 &lt;code&gt;Client&lt;/code&gt; 连这个类名都无权访问，所以不能进行该类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 8 章 高效编程与渐近分析</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/08-efficient-programming-and-asymptotic-analysis</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/08-efficient-programming-and-asymptotic-analysis</guid><description>CS61B 渐近分析中文教程，讲解高效编程、运行时间估算、渐近符号与常见算法复杂度。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;8.1 封装、API 与 ADT&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;“工程师会用一角钱完成任何傻瓜花一美元才能完成的事。”——Paul Hilfinger&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;效率可以分成两类。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编程成本&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;开发程序需要多长时间？&lt;/li&gt;
&lt;li&gt;代码是否容易阅读、修改和维护？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行成本&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;程序运行需要多长时间？&lt;/li&gt;
&lt;li&gt;程序需要多少内存？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本节先关注降低编程成本。低编程成本不仅意味着更快写完代码，也意味着开发者更少受挫；而人在烦躁时通常写不出高质量代码。&lt;/p&gt;
&lt;p&gt;课程已经讨论过一些有帮助的 Java 特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;包：
&lt;ul&gt;
&lt;li&gt;优点：组织代码、提供包私有访问；&lt;/li&gt;
&lt;li&gt;缺点：结构比较具体，可能增加使用负担。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;静态类型检查：
&lt;ul&gt;
&lt;li&gt;优点：更早发现错误，代码更像一段清晰叙事；&lt;/li&gt;
&lt;li&gt;缺点：灵活性较低，有时需要强制类型转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;继承：
&lt;ul&gt;
&lt;li&gt;优点：代码复用；&lt;/li&gt;
&lt;li&gt;缺点：必须满足“是一种”关系；调试调用链可能麻烦；接口不能实例化，实现类必须实现要求的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本章继续探索其他设计手段。&lt;/p&gt;
&lt;h3&gt;封装&lt;/h3&gt;
&lt;p&gt;先定义两个术语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模块（module）&lt;/strong&gt;：一组协同工作、共同完成某项任务或一组相关任务的方法；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;封装良好（encapsulated）&lt;/strong&gt;：模块的实现被完整隐藏，外界只能通过有文档说明的接口访问它。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;p&gt;抽象数据类型的 &lt;strong&gt;API（Application Programming Interface，应用程序编程接口）&lt;/strong&gt;，就是其构造方法和公开方法的列表，以及每项功能的简短说明。&lt;/p&gt;
&lt;p&gt;API 同时包括语法规范与语义规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器验证&lt;strong&gt;语法&lt;/strong&gt;：API 中承诺的类型、方法和参数是否存在；&lt;/li&gt;
&lt;li&gt;测试帮助验证&lt;strong&gt;语义&lt;/strong&gt;：这些方法是否真的按约定工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;语义规范通常用自然语言描述，并可能带有使用示例。完全形式化、数学精确的规范虽然可行，但在普通软件项目中并不常见。&lt;/p&gt;
&lt;h3&gt;抽象数据类型&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ADT（Abstract Data Type，抽象数据类型）&lt;strong&gt;是由&lt;/strong&gt;行为&lt;/strong&gt;而不是具体实现定义的高层类型。&lt;/p&gt;
&lt;p&gt;例如，项目 1 中的 &lt;code&gt;Deque&lt;/code&gt; 是一种 ADT，它规定 &lt;code&gt;addFirst&lt;/code&gt;、&lt;code&gt;addLast&lt;/code&gt; 等操作。真正实现这些行为的数据结构可以是 &lt;code&gt;ArrayDeque&lt;/code&gt; 或 &lt;code&gt;LinkedListDeque&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;某些 ADT 是其他 ADT 的特殊形式。例如，栈和队列都可以看作行为受到更多约束的列表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 8.1.1。&lt;/strong&gt; 使用链表作为底层结构编写泛型 &lt;code&gt;Stack&amp;#x3C;Item&gt;&lt;/code&gt;，只需实现 &lt;code&gt;push(Item x)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;常见方案有三种。&lt;/p&gt;
&lt;h4&gt;继承&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class ExtensionStack&amp;#x3C;Item&gt; extends LinkedList&amp;#x3C;Item&gt; {
    public void push(Item x) {
        add(x);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方案让 &lt;code&gt;ExtensionStack&lt;/code&gt; 继承 &lt;code&gt;LinkedList&lt;/code&gt;，直接借用父类方法。&lt;/p&gt;
&lt;h4&gt;委托&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DelegationStack&amp;#x3C;Item&gt; {
    private LinkedList&amp;#x3C;Item&gt; L = new LinkedList&amp;#x3C;Item&gt;();

    public void push(Item x) {
        L.add(x);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方案在类内部持有一个链表，并把具体工作委托给它。&lt;/p&gt;
&lt;h4&gt;适配器&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class StackAdapter&amp;#x3C;Item&gt; {
    private List&amp;#x3C;Item&gt; L;

    public StackAdapter(List&amp;#x3C;Item&gt; worker) {
        L = worker;
    }

    public void push(Item x) {
        L.add(x);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它与委托相似，但构造方法接收任意 &lt;code&gt;List&lt;/code&gt; 实现，因此底层可以是 &lt;code&gt;LinkedList&lt;/code&gt;、&lt;code&gt;ArrayList&lt;/code&gt; 等。&lt;/p&gt;
&lt;p&gt;请始终注意“是一种”与“拥有一个”的区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;猫&lt;strong&gt;拥有&lt;/strong&gt;爪子；&lt;/li&gt;
&lt;li&gt;猫&lt;strong&gt;是一种&lt;/strong&gt;猫科动物。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;继承适用于真正的“是一种”关系；委托则适用于“当前对象使用另一个对象完成工作”，但并不希望把二者视作同一种类型的情况。&lt;/p&gt;
&lt;h4&gt;委托与继承&lt;/h4&gt;
&lt;p&gt;当你充分了解父类内部实现，并且子类确实可以被视为父类的一种特殊形式时，可以使用继承。&lt;/p&gt;
&lt;p&gt;当你只是希望复用另一个类的能力，却不想声称当前类就是那个类的一种时，委托通常更合适。委托降低耦合，也使底层实现更容易替换。&lt;/p&gt;
&lt;h3&gt;视图&lt;/h3&gt;
&lt;p&gt;**视图（view）**是已有对象的另一种表示。视图通常限制用户能够访问的范围，但通过视图完成的修改会作用于原对象。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;String&gt; L = new ArrayList&amp;#x3C;&gt;();
L.add(&quot;at&quot;);
L.add(&quot;ax&quot;);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若只想查看索引 1 到 4 的部分，可以使用 &lt;code&gt;subList&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;String&gt; SL = L.subList(1, 4);
SL.set(0, &quot;jug&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SL&lt;/code&gt; 不是独立副本，而是原列表的一段视图，所以修改 &lt;code&gt;SL&lt;/code&gt; 会改变 &lt;code&gt;L&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;视图的一个用途是复用通用算法。假设我们已经有“反转整个列表”的函数，现在只想反转原列表的一部分。无需给反转函数额外增加起止索引逻辑，只要先获得该区间的 &lt;code&gt;subList&lt;/code&gt; 视图，再对视图执行通用反转即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f51a7d8955ad-reverse_list1.png&quot; alt=&quot;反转列表的一部分&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/8bb63f0dc7b5-reverse_list2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;“返回一个 &lt;code&gt;List&lt;/code&gt;，但修改它又会影响另一个 &lt;code&gt;List&lt;/code&gt;”听起来有些奇怪。其实现依赖访问方法和内部类。&lt;/p&gt;
&lt;p&gt;概念代码类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Item&gt; subList(int start, int end) {
    return new SubList(start, end);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SubList&lt;/code&gt; 可以是外部列表类的内部类，并继承 &lt;code&gt;AbstractList&amp;#x3C;Item&gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private class SubList extends AbstractList&amp;#x3C;Item&gt; {
    private int start;
    private int end;

    SubList(int start, int end) {
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;AbstractList&lt;/code&gt; 实现 &lt;code&gt;List&lt;/code&gt;，&lt;code&gt;SubList&lt;/code&gt; 也是一种 &lt;code&gt;List&lt;/code&gt;。其 &lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;add&lt;/code&gt; 会把视图中的索引转换为原列表索引：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Item get(int k) {
    return AbstractList.this.get(start + k);
}

public void add(int k, Item x) {
    AbstractList.this.add(start + k, x);
    end += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;视图中的第 &lt;code&gt;k&lt;/code&gt; 个元素，就是原列表中的第 &lt;code&gt;start + k&lt;/code&gt; 个元素。因为方法最终调用外部原列表的操作，所以原列表会被真正修改。&lt;/p&gt;
&lt;h3&gt;要点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;API 很难设计，但一致的设计理念可以让代码更清晰、更容易维护；&lt;/li&gt;
&lt;li&gt;继承很诱人，却会增加耦合和复杂性。只有当类型关系和父子类属性都足够明确时才应使用；&lt;/li&gt;
&lt;li&gt;委托、适配器和视图往往能以更松耦合的方式复用功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8.2 渐近分析 I&lt;/h2&gt;
&lt;p&gt;编写高效程序可以从两个角度考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编程成本&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;开发需要多久？&lt;/li&gt;
&lt;li&gt;代码是否容易阅读和修改？&lt;/li&gt;
&lt;li&gt;是否容易长期维护和扩展？实际软件的大量成本来自维护，而不只是第一次开发。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行成本&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：程序运行需要多久？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空间复杂度&lt;/strong&gt;：程序需要多少内存？&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;算法成本示例&lt;/h3&gt;
&lt;p&gt;目标：判断一个&lt;strong&gt;有序数组&lt;/strong&gt;中是否存在重复元素。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;笨办法&lt;/strong&gt;：比较每一对元素，只要发现相等就返回 &lt;code&gt;true&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更好办法&lt;/strong&gt;：利用数组有序这一事实。若存在重复，它们一定相邻，因此只需比较相邻元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;笨办法显然进行了大量冗余工作，但究竟多多少？我们需要正式工具量化算法效率。&lt;/p&gt;
&lt;h3&gt;描述运行时间&lt;/h3&gt;
&lt;p&gt;下面比较两个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 比较所有元素对
public static boolean dup1(int[] A) {
    for (int i = 0; i &amp;#x3C; A.length; i += 1) {
        for (int j = i + 1; j &amp;#x3C; A.length; j += 1) {
            if (A[i] == A[j]) {
                return true;
            }
        }
    }
    return false;
}

// 只比较相邻元素
public static boolean dup2(int[] A) {
    for (int i = 0; i &amp;#x3C; A.length - 1; i += 1) {
        if (A[i] == A[i + 1]) {
            return true;
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;理想的运行时间描述应满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单且数学严谨；&lt;/li&gt;
&lt;li&gt;能清楚展示 &lt;code&gt;dup2&lt;/code&gt; 优于 &lt;code&gt;dup1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;衡量计算成本的方法&lt;/h3&gt;
&lt;h4&gt;方法一：直接计时&lt;/h4&gt;
&lt;p&gt;可以用秒表、Unix 的 &lt;code&gt;time&lt;/code&gt; 命令或计时类测量真实执行时间。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单；&lt;/li&gt;
&lt;li&gt;含义直观，直接得到实际耗时。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大输入可能测试很久；&lt;/li&gt;
&lt;li&gt;结果依赖机器、编译器、运行环境和具体输入；&lt;/li&gt;
&lt;li&gt;不一定能揭示算法本身的普遍规律。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法二 A：对固定规模输入计数&lt;/h4&gt;
&lt;p&gt;例如令 &lt;code&gt;N = 10,000&lt;/code&gt;，逐项统计赋值、比较、自增、数组访问等操作执行次数。&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大体独立于机器；&lt;/li&gt;
&lt;li&gt;能把输入对操作次数的影响纳入模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统计繁琐；&lt;/li&gt;
&lt;li&gt;固定的 10,000 很任意，无法直接说明其他规模；&lt;/li&gt;
&lt;li&gt;不同操作的实际耗时并不相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法二 B：使用符号 &lt;code&gt;N&lt;/code&gt; 计数&lt;/h4&gt;
&lt;p&gt;把操作次数表示为输入规模 &lt;code&gt;N&lt;/code&gt; 的函数。这样可以描述算法怎样随输入增长而扩展，但精确统计更加繁琐，而且仍不能给出真实秒数。&lt;/p&gt;
&lt;p&gt;对 &lt;code&gt;dup2&lt;/code&gt;，最坏情况下大致有：&lt;/p&gt;
&lt;p&gt;| 操作 | 符号计数 | &lt;code&gt;N=10000&lt;/code&gt; |
|---|---:|---:|
| &lt;code&gt;i = 0&lt;/code&gt; | 1 | 1 |
| &lt;code&gt;&amp;#x3C;&lt;/code&gt; | 0 到 &lt;code&gt;N&lt;/code&gt; | 0 到 10000 |
| &lt;code&gt;+= 1&lt;/code&gt; | 0 到 &lt;code&gt;N-1&lt;/code&gt; | 0 到 9999 |
| &lt;code&gt;==&lt;/code&gt; | 1 到 &lt;code&gt;N-1&lt;/code&gt; | 1 到 9999 |
| 数组访问 | 2 到 &lt;code&gt;2N-2&lt;/code&gt; | 2 到 19998 |&lt;/p&gt;
&lt;p&gt;这里只需要数量级正确，不必纠结相差一两个操作。&lt;/p&gt;
&lt;p&gt;相比之下，&lt;code&gt;dup1&lt;/code&gt; 的比较和数组访问次数大约随 &lt;code&gt;N²&lt;/code&gt; 增长；当 &lt;code&gt;N=10000&lt;/code&gt; 时，可能达到数千万甚至近亿次。&lt;code&gt;dup2&lt;/code&gt; 只随 &lt;code&gt;N&lt;/code&gt; 线性增长。&lt;/p&gt;
&lt;h3&gt;为什么扩展规律重要&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dup2&lt;/code&gt; 更好，不只是因为某个固定输入下操作更少，而是因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dup1&lt;/code&gt; 最坏情况近似二次函数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dup2&lt;/code&gt; 最坏情况近似直线；&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;N&lt;/code&gt; 变大时，&lt;code&gt;N²&lt;/code&gt; 比 &lt;code&gt;N&lt;/code&gt; 增长快得多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法常用于处理海量数据，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数十亿粒子的模拟；&lt;/li&gt;
&lt;li&gt;数十亿用户的社交网络；&lt;/li&gt;
&lt;li&gt;数十亿字节的视频编码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时我们最关心非常大的 &lt;code&gt;N&lt;/code&gt;，也就是&lt;strong&gt;渐近行为&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;常数在小输入下可能改变谁更快。例如 &lt;code&gt;2N²&lt;/code&gt; 在 &lt;code&gt;N=4&lt;/code&gt; 时比 &lt;code&gt;500N&lt;/code&gt; 小；但随着 &lt;code&gt;N&lt;/code&gt; 增长，二次项最终一定占主导。我们关心的是图像的“形状”，也叫&lt;strong&gt;增长阶&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;四个直观简化&lt;/h3&gt;
&lt;h4&gt;1. 只考虑最坏情况&lt;/h4&gt;
&lt;p&gt;比较算法时，经常先关注最坏情况，虽然课程后面也会讨论例外。&lt;/p&gt;
&lt;p&gt;例如，若某段代码的操作次数包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;100N² + 3N&lt;/code&gt; 次 &lt;code&gt;&amp;#x3C;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N³ + 1&lt;/code&gt; 次 &lt;code&gt;&gt;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;5000 次 &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么总体增长阶是 &lt;code&gt;N³&lt;/code&gt;，因为对足够大的 &lt;code&gt;N&lt;/code&gt;，三次项压倒其他项。&lt;/p&gt;
&lt;h4&gt;2. 只关注一种代表性操作&lt;/h4&gt;
&lt;p&gt;选择一种能够代表整体工作的操作作为代理，例如比较、数组访问或循环自增。这称为&lt;strong&gt;成本模型（cost model）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;应选择随输入规模变化、处于核心循环中的操作。像只执行一次的 &lt;code&gt;i = 0&lt;/code&gt;，通常不是好成本模型。&lt;/p&gt;
&lt;h4&gt;3. 忽略低阶项&lt;/h4&gt;
&lt;p&gt;例如 &lt;code&gt;N³ + 100N² + 3N&lt;/code&gt; 的增长主要由 &lt;code&gt;N³&lt;/code&gt; 决定。&lt;/p&gt;
&lt;h4&gt;4. 忽略乘法常数&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;3N²&lt;/code&gt;、&lt;code&gt;N²/2&lt;/code&gt; 和 &lt;code&gt;100N²&lt;/code&gt; 都属于同一种二次增长形状。选择单一成本模型时，我们本来就已经舍弃了不同操作常数耗时的信息，因此继续保留固定倍数通常没有意义。&lt;/p&gt;
&lt;p&gt;四步总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只考虑最坏情况；&lt;/li&gt;
&lt;li&gt;选择代表性操作；&lt;/li&gt;
&lt;li&gt;忽略低阶项；&lt;/li&gt;
&lt;li&gt;忽略乘法常数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对 &lt;code&gt;dup2&lt;/code&gt;，无论选择比较、自增还是数组访问作为成本模型，最坏情况增长阶都是 &lt;code&gt;N&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;简化后的分析流程&lt;/h3&gt;
&lt;p&gt;无需先构建所有操作的精确表格，可以直接：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择成本模型；&lt;/li&gt;
&lt;li&gt;求该操作次数的增长阶：
&lt;ul&gt;
&lt;li&gt;可以先精确计数再简化；&lt;/li&gt;
&lt;li&gt;也可以通过图形或结构直观判断，熟练后会更快。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;嵌套循环的精确计数&lt;/h4&gt;
&lt;p&gt;对 &lt;code&gt;dup1&lt;/code&gt;，令成本模型为 &lt;code&gt;==&lt;/code&gt; 的次数。最坏情况下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 + 2 + 3 + ... + (N - 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把它与逆序和相加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C = 1 + 2 + ... + (N - 1)
C = (N - 1) + (N - 2) + ... + 1
2C = N + N + ... + N   （共 N-1 项）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C = N(N - 1) / 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;忽略低阶项和常数后，增长阶为 &lt;code&gt;N²&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;几何论证&lt;/h4&gt;
&lt;p&gt;也可以把所有比较看成边长约为 &lt;code&gt;N&lt;/code&gt; 的直角三角形中的格点，其面积随 &lt;code&gt;N²&lt;/code&gt; 增长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c940ab90f707-8.1---chart.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;正式表示增长阶&lt;/h3&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Q(N) = 3N³ + N²
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用“去掉低阶项和常数”后，增长阶为 &lt;code&gt;N³&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;常见例子：&lt;/p&gt;
&lt;p&gt;| 函数 | 增长阶 |
|---|---|
| &lt;code&gt;N³ + 3N⁴&lt;/code&gt; | &lt;code&gt;N⁴&lt;/code&gt; |
| &lt;code&gt;1/N + N³&lt;/code&gt; | &lt;code&gt;N³&lt;/code&gt; |
| &lt;code&gt;1/N + 5&lt;/code&gt; | &lt;code&gt;1&lt;/code&gt; |
| &lt;code&gt;Neᴺ + N&lt;/code&gt; | &lt;code&gt;Neᴺ&lt;/code&gt; |
| &lt;code&gt;40sin(N) + 4N²&lt;/code&gt; | &lt;code&gt;N²&lt;/code&gt; |&lt;/p&gt;
&lt;h3&gt;大 Theta：&lt;code&gt;Θ&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;若函数 &lt;code&gt;R(N)&lt;/code&gt; 的增长阶为 &lt;code&gt;f(N)&lt;/code&gt;，写作：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;R(N) ∈ Θ(f(N))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;N³ + 3N⁴ ∈ Θ(N⁴)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1/N + N³ ∈ Θ(N³)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1/N + 5 ∈ Θ(1)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Neᴺ + N ∈ Θ(Neᴺ)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;40sin(N) + 4N² ∈ Θ(N²)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;正式定义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;R(N) ∈ Θ(f(N))&lt;/code&gt; 表示存在正常数 &lt;code&gt;k₁&lt;/code&gt;、&lt;code&gt;k₂&lt;/code&gt; 和某个 &lt;code&gt;N₀&lt;/code&gt;，使得对所有 &lt;code&gt;N ≥ N₀&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;k₁ f(N) ≤ R(N) ≤ k₂ f(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是 &lt;code&gt;R&lt;/code&gt; 最终被 &lt;code&gt;f&lt;/code&gt; 的两个常数倍从上下夹住。&lt;/p&gt;
&lt;p&gt;在普通运行时间分析中，不需要真的求出 &lt;code&gt;k₁&lt;/code&gt; 和 &lt;code&gt;k₂&lt;/code&gt;；仍按前面的成本模型与简化过程分析，只是用 &lt;code&gt;Θ&lt;/code&gt; 正式表示结果。&lt;/p&gt;
&lt;h3&gt;大 O：&lt;code&gt;O&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Θ&lt;/code&gt; 同时给出上界和下界，可以非正式地理解为渐近“等于”。&lt;code&gt;O&lt;/code&gt; 只给出上界，可以理解为渐近“小于等于”。&lt;/p&gt;
&lt;p&gt;例如，下列说法都正确：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N³ + 3N⁴ ∈ O(N⁴)
N³ + 3N⁴ ∈ O(N⁶)
N³ + 3N⁴ ∈ O(N!)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦某函数被 &lt;code&gt;N⁴&lt;/code&gt; 从上方界定，它当然也会被增长得更快的函数从上方界定。&lt;/p&gt;
&lt;h4&gt;正式定义&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;R(N) ∈ O(f(N))&lt;/code&gt; 表示存在正常数 &lt;code&gt;k₂&lt;/code&gt; 和某个 &lt;code&gt;N₀&lt;/code&gt;，使得对所有 &lt;code&gt;N ≥ N₀&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;R(N) ≤ k₂ f(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它不要求下界，因此比 &lt;code&gt;Θ&lt;/code&gt; 更宽松。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;可以把代码运行时间写成输入某个性质 &lt;code&gt;N&lt;/code&gt; 的函数 &lt;code&gt;R(N)&lt;/code&gt;，通常 &lt;code&gt;N&lt;/code&gt; 表示输入规模；&lt;/li&gt;
&lt;li&gt;实际分析通常不求精确 &lt;code&gt;R(N)&lt;/code&gt;，只关心增长阶；&lt;/li&gt;
&lt;li&gt;一种常见方法是：
&lt;ol&gt;
&lt;li&gt;选择代表性操作；&lt;/li&gt;
&lt;li&gt;令 &lt;code&gt;C(N)&lt;/code&gt; 为它执行的次数；&lt;/li&gt;
&lt;li&gt;求 &lt;code&gt;C(N) ∈ Θ(f(N))&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若单次操作为常数时间，则整体运行时间通常也属于 &lt;code&gt;Θ(f(N))&lt;/code&gt;；&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;很多时候分析最坏情况，但这不是绝对规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8.3 渐近分析 II&lt;/h2&gt;
&lt;h3&gt;循环示例一&lt;/h3&gt;
&lt;p&gt;继续通过更难的例子练习运行时间分析。这个主题需要大量实践，才能熟悉其中的结构和技巧。&lt;/p&gt;
&lt;p&gt;上一节的 &lt;code&gt;dup1&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int N = A.length;
for (int i = 0; i &amp;#x3C; N; i += 1)
    for (int j = i + 1; j &amp;#x3C; N; j += 1)
        if (A[i] == A[j])
            return true;
return false;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有两种常见分析方式。&lt;/p&gt;
&lt;h4&gt;操作计数&lt;/h4&gt;
&lt;p&gt;选 &lt;code&gt;==&lt;/code&gt; 次数作为成本模型。外层循环第一次执行时，内层比较 &lt;code&gt;N-1&lt;/code&gt; 次；随后是 &lt;code&gt;N-2&lt;/code&gt;、&lt;code&gt;N-3&lt;/code&gt;，直到 1。最坏情况下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C = 1 + 2 + ... + (N - 1) = N(N - 1) / 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它属于 &lt;code&gt;N²&lt;/code&gt; 家族，而单次比较是常数时间，因此最坏运行时间为 &lt;code&gt;Θ(N²)&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;几何分析&lt;/h4&gt;
&lt;p&gt;把 &lt;code&gt;(i, j)&lt;/code&gt; 组合画在网格中，实际比较区域是边长约为 &lt;code&gt;N-1&lt;/code&gt; 的直角三角形。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/fe8cdd7c490d-dup1_square.png&quot; alt=&quot;dup1 的几何分析&quot;&gt;&lt;/p&gt;
&lt;p&gt;三角形面积属于 &lt;code&gt;Θ(N²)&lt;/code&gt;，得到相同结论。&lt;/p&gt;
&lt;h3&gt;循环示例二&lt;/h3&gt;
&lt;p&gt;考虑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void printParty(int N) {
    for (int i = 1; i &amp;#x3C;= N; i = i * 2) {
        for (int j = 0; j &amp;#x3C; i; j += 1) {
            System.out.println(&quot;hello&quot;);
            int ZUG = 1 + 1;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;外层的 &lt;code&gt;i&lt;/code&gt; 每次乘 2；内层从 0 执行到当前 &lt;code&gt;i-1&lt;/code&gt;。循环体中的两个操作都是常数时间，所以只需统计打印次数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;N=1&lt;/code&gt;：打印 1 次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N=2&lt;/code&gt;：打印 &lt;code&gt;1+2=3&lt;/code&gt; 次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N=3&lt;/code&gt;：&lt;code&gt;i&lt;/code&gt; 仍只取 1、2，所以也是 3 次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N=4&lt;/code&gt;：打印 &lt;code&gt;1+2+4=7&lt;/code&gt; 次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/63d0b76aec58-loops2_1.png&quot; alt=&quot;循环计数 1&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/541c9a4c6f4d-loops2_2.png&quot; alt=&quot;循环计数 2&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/07ddadf65cbe-loops2_3.png&quot; alt=&quot;循环计数 3&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2e6b1dd8cd43-loops2_4.png&quot; alt=&quot;更完整的计数&quot;&gt;&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;N&lt;/code&gt; 是 2 的幂时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(N) = 1 + 2 + 4 + ... + N = 2N - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;忽略常数与低阶项后是 &lt;code&gt;Θ(N)&lt;/code&gt;。从图像上看，阶梯函数始终夹在两个线性函数之间：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/cea1a396ae8b-loops2_graph.png&quot; alt=&quot;线性界&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/d59cfed2c7f9-loops2_graph2.png&quot; alt=&quot;2N 参考线&quot;&gt;&lt;/p&gt;
&lt;p&gt;尽管代码含两层循环，运行时间仍是线性的。这说明：&lt;strong&gt;不能看到嵌套循环就机械判断为 &lt;code&gt;N²&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;没有神奇捷径&lt;/h3&gt;
&lt;p&gt;运行时间分析需要认真推理，没有一个万能表面规则。常用技巧包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求出精确和式；&lt;/li&gt;
&lt;li&gt;写出若干小输入示例；&lt;/li&gt;
&lt;li&gt;画图或递归树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个常见和式应当熟悉：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 + 2 + 3 + ... + Q = Q(Q + 1) / 2 = Θ(Q²)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 + 2 + 4 + 8 + ... + Q = 2Q - 1 = Θ(Q)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归&lt;/h3&gt;
&lt;p&gt;考虑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int f3(int n) {
    if (n &amp;#x3C;= 1) {
        return 1;
    }
    return f3(n - 1) + f3(n - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 &lt;code&gt;f3(4)&lt;/code&gt; 会生成两次 &lt;code&gt;f3(3)&lt;/code&gt;，每个 &lt;code&gt;f3(3)&lt;/code&gt; 又生成两次 &lt;code&gt;f3(2)&lt;/code&gt;，依此类推。递归调用形成一棵二叉树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ca056c28099f-asymptotics2_tree.png&quot; alt=&quot;递归树&quot;&gt;&lt;/p&gt;
&lt;p&gt;该函数返回 &lt;code&gt;2^(N-1)&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;直观方法&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;N&lt;/code&gt; 每增加 1，工作量大约翻倍，因此运行时间属于 &lt;code&gt;Θ(2ᴺ)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4a231c032302-asymptotics2_tree2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;代数方法&lt;/h4&gt;
&lt;p&gt;令 &lt;code&gt;C(N)&lt;/code&gt; 为函数调用次数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(1) = 1
C(2) = 1 + 2
C(3) = 1 + 2 + 4
...
C(N) = 1 + 2 + 4 + ... + 2^(N-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用等比和：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(N) = 2ᴺ - 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用本身的非递归工作为常数，所以总运行时间为 &lt;code&gt;Θ(2ᴺ)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也可以写递推关系：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(1) = 1
C(N) = 2C(N-1) + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;展开后得到相同结果。递推关系的系统求解不属于本课程要求。&lt;/p&gt;
&lt;h3&gt;二分查找&lt;/h3&gt;
&lt;p&gt;二分查找用于在&lt;strong&gt;有序&lt;/strong&gt;列表中寻找目标。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查中间元素；&lt;/li&gt;
&lt;li&gt;若目标更小，只保留左半部分；&lt;/li&gt;
&lt;li&gt;若目标更大，只保留右半部分；&lt;/li&gt;
&lt;li&gt;不断把候选范围减半，直到找到目标或范围为空。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最坏情况是目标根本不存在，需要一直排除到没有元素。&lt;/p&gt;
&lt;p&gt;候选数量变化为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N, N/2, N/4, ... , 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;减半约 &lt;code&gt;log₂N&lt;/code&gt; 次。每一步找中点和比较都是常数时间，所以直观运行时间为 &lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若精确统计递归调用次数：&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;N&lt;/code&gt; | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 次数 | 1 | 2 | 2 | 3 | 3 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |&lt;/p&gt;
&lt;p&gt;更精确地：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C(N) = floor(log₂N) + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;渐近分析中有：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;floor(f(N)) ∈ Θ(f(N))
ceil(f(N)) ∈ Θ(f(N))
log_p(N) ∈ Θ(log_q(N))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以对数底数和取整都不影响增长阶，最终仍为 &lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对数时间非常优秀，接近常数时间，并远好于线性扫描。例如：&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;N&lt;/code&gt; | &lt;code&gt;log₂N&lt;/code&gt; |
|---:|---:|
| 100 | 6.6 |
| 100,000 | 16.6 |
| 100,000,000 | 26.5 |
| 100,000,000,000 | 36.5 |
| 100,000,000,000,000 | 46.5 |&lt;/p&gt;
&lt;p&gt;即使数据规模增长一万亿倍，二分查找只增加几十次操作。&lt;/p&gt;
&lt;h3&gt;归并排序&lt;/h3&gt;
&lt;p&gt;先回顾选择排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在未排序部分找到最小元素，把它移到最前面并固定；&lt;/li&gt;
&lt;li&gt;对剩余未排序部分重复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;选择排序运行时间为 &lt;code&gt;Θ(N²)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了直观比较，可以使用任意时间单位（AU）。它不是现实秒数，只表示相对工作量。例如 &lt;code&gt;N=64&lt;/code&gt; 的选择排序，大致需要与 &lt;code&gt;64²&lt;/code&gt; 同阶的工作。&lt;/p&gt;
&lt;h4&gt;合并两个有序数组&lt;/h4&gt;
&lt;p&gt;若有两个已经有序的数组，可以在线性时间内合并：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个数组的最小剩余元素一定分别位于各自开头；&lt;/li&gt;
&lt;li&gt;比较两个开头，把较小者写入结果；&lt;/li&gt;
&lt;li&gt;重复直到一个数组为空，再复制另一个数组的剩余部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个元素只写入结果一次，因此合并运行时间为 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;从分治得到归并排序&lt;/h4&gt;
&lt;p&gt;把长度 64 的数组直接选择排序，代价约与 &lt;code&gt;64²&lt;/code&gt; 同阶。若先分成两个长度 32 的数组，分别排序再线性合并，工作量明显下降。继续不断对半拆分，最终得到长度 1 的数组，而单元素数组天然有序。&lt;/p&gt;
&lt;p&gt;归并排序的结构：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若列表大小为 1，直接返回；&lt;/li&gt;
&lt;li&gt;递归归并排序左半部分；&lt;/li&gt;
&lt;li&gt;递归归并排序右半部分；&lt;/li&gt;
&lt;li&gt;合并两个结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;分析每一层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顶层合并全部 &lt;code&gt;N&lt;/code&gt; 个元素，总工作 &lt;code&gt;Θ(N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;下一层有两个大小 &lt;code&gt;N/2&lt;/code&gt; 的合并，总工作仍为 &lt;code&gt;Θ(N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;再下一层有四个大小 &lt;code&gt;N/4&lt;/code&gt; 的合并，总工作仍为 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;共有约 &lt;code&gt;log₂N&lt;/code&gt; 层，所以总运行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Θ(N log N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它远好于 &lt;code&gt;Θ(N²)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1fc651dd512e-timetable.png&quot; alt=&quot;常见运行时间比较&quot;&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;运行时间分析没有魔法捷径；&lt;/li&gt;
&lt;li&gt;可以使用精确计数，也可以使用有充分依据的直观分析；&lt;/li&gt;
&lt;li&gt;需要熟悉自然数和与 2 的幂之和；&lt;/li&gt;
&lt;li&gt;本课程不要求书写严格数学证明，但要求能解释分析依据；&lt;/li&gt;
&lt;li&gt;大量练习能显著提高这项能力；&lt;/li&gt;
&lt;li&gt;同一问题的不同算法可能有巨大性能差异；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N²&lt;/code&gt; 与 &lt;code&gt;N log N&lt;/code&gt; 的差距极大；&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;N log N&lt;/code&gt; 改进为 &lt;code&gt;N&lt;/code&gt; 当然很好，但通常没有从 &lt;code&gt;N²&lt;/code&gt; 到 &lt;code&gt;N log N&lt;/code&gt; 那么根本。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;8.4 大 Omega 与摊还分析&lt;/h2&gt;
&lt;p&gt;本节结束渐近分析的基础讨论。部分内容会在课程后续再次展开。我们将进一步理解大 O，引入大 Omega，讨论摊还运行时间，最后简要接触实验分析与复杂性思想。&lt;/p&gt;
&lt;h3&gt;运行时间分析的细节&lt;/h3&gt;
&lt;p&gt;考虑：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean dup3(int[] a) {
    int N = a.length;
    for (int i = 0; i &amp;#x3C; N; i += 1) {
        for (int j = 0; j &amp;#x3C; N; j += 1) {
            if (a[i] == a[j]) {
                return true;
            }
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;问题：&lt;/strong&gt; &lt;code&gt;dup3&lt;/code&gt; 的运行时间增长阶是什么？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt; &lt;code&gt;Θ(1)&lt;/code&gt;。代码有错误：第一次循环时 &lt;code&gt;i=j=0&lt;/code&gt;，它把第一个元素与自身比较并立即返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;修正后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean dup4(int[] a) {
    int N = a.length;
    for (int i = 0; i &amp;#x3C; N; i += 1) {
        for (int j = i + 1; j &amp;#x3C; N; j += 1) {
            if (a[i] == a[j]) {
                return true;
            }
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次运行时间不仅依赖数组长度，还依赖数组内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最好情况：若前两个元素相同，立即返回，&lt;code&gt;Θ(1)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;最坏情况：若完全没有重复，执行所有元素对比较，&lt;code&gt;Θ(N²)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这体现了大 Theta 的一个使用细节：它描述某一明确情形下的精确增长阶。如果运行时间还依赖输入内容，就必须先说明最好、最坏或平均情形。&lt;/p&gt;
&lt;p&gt;大 O 可以给出统一上界：无论具体内容如何，&lt;code&gt;dup4&lt;/code&gt; 都不会比二次更差，所以可以说它是 &lt;code&gt;O(N²)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;不要滥用大 O&lt;/h3&gt;
&lt;p&gt;比较两句话：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;酒店最贵房间每晚 639 美元；&lt;/li&gt;
&lt;li&gt;酒店所有房间价格都不超过每晚 639 美元。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一句信息更多，因为它不仅给出上界，还说明这个上界确实达到。一个最贵房间只要 89 美元的廉价酒店也满足第二句。&lt;/p&gt;
&lt;p&gt;运行时间同理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;最坏运行时间是 &lt;code&gt;Θ(N²)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;运行时间是 &lt;code&gt;O(N²)&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一句更精确。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void printLength(int[] a) {
    System.out.println(a.length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个常数时间方法也是 &lt;code&gt;O(N²)&lt;/code&gt;，因为常数函数当然不超过二次上界。但它不是 &lt;code&gt;Θ(N²)&lt;/code&gt;。所以只给宽松大 O 可能掩盖巨大差异。&lt;/p&gt;
&lt;p&gt;现实交流中，人们经常在本应使用 Theta 时口语化地说“大 O”。请记住：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大 O &lt;strong&gt;不等于&lt;/strong&gt;最坏情况；&lt;/li&gt;
&lt;li&gt;它只是上界，但经常用于描述最坏情况上界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大 O 的价值包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;输入不同时运行时间不同，可以不逐个限定情形，直接给出统一上界；&lt;/li&gt;
&lt;li&gt;某些困难问题的精确复杂度未知，只能给出已知上界；&lt;/li&gt;
&lt;li&gt;证明上界通常比证明精确 Theta 容易。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;大 Omega：&lt;code&gt;Ω&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;大 O 描述上界；大 Omega 描述下界，可以理解为渐近“大于等于”。&lt;/p&gt;
&lt;p&gt;若：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N³ + 3N⁴ ∈ Θ(N⁴)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么以下也都正确：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N³ + 3N⁴ ∈ Ω(N⁴)
N³ + 3N⁴ ∈ Ω(N³)
N³ + 3N⁴ ∈ Ω(log N)
N³ + 3N⁴ ∈ Ω(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为该函数至少像 &lt;code&gt;N⁴&lt;/code&gt; 一样快地增长，也自然至少像更慢的函数那样增长。&lt;/p&gt;
&lt;p&gt;大 Omega 常见用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;证明大 Theta。若 &lt;code&gt;R(N) ∈ O(f(N))&lt;/code&gt; 且 &lt;code&gt;R(N) ∈ Ω(f(N))&lt;/code&gt;，则 &lt;code&gt;R(N) ∈ Θ(f(N))&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;证明问题本身的最低难度。例如，任何在普通数组中判断重复的正确算法至少要检查输入元素，因此存在 &lt;code&gt;Ω(N)&lt;/code&gt; 下界。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;| 记号 | 非正式含义 | 示例 |
|---|---|---|
| &lt;code&gt;Θ(f(N))&lt;/code&gt; | 增长阶等于 &lt;code&gt;f(N)&lt;/code&gt; | &lt;code&gt;N²/2&lt;/code&gt;、&lt;code&gt;2N²&lt;/code&gt;、&lt;code&gt;N²+38N&lt;/code&gt; 都属于 &lt;code&gt;Θ(N²)&lt;/code&gt; |
| &lt;code&gt;O(f(N))&lt;/code&gt; | 增长阶不超过 &lt;code&gt;f(N)&lt;/code&gt; | &lt;code&gt;log N&lt;/code&gt;、&lt;code&gt;N&lt;/code&gt;、&lt;code&gt;N²&lt;/code&gt; 都属于 &lt;code&gt;O(N²)&lt;/code&gt; |
| &lt;code&gt;Ω(f(N))&lt;/code&gt; | 增长阶不低于 &lt;code&gt;f(N)&lt;/code&gt; | &lt;code&gt;N²&lt;/code&gt;、&lt;code&gt;N³&lt;/code&gt;、&lt;code&gt;5ᴺ&lt;/code&gt; 都属于 &lt;code&gt;Ω(N²)&lt;/code&gt; |&lt;/p&gt;
&lt;h3&gt;摊还分析：直观解释&lt;/h3&gt;
&lt;h4&gt;Grigometh 的草料罐&lt;/h4&gt;
&lt;p&gt;Grigometh 是一只恶魔狗。作为交换能力的贡品，你需要定期向它提供一罐草料。它给出两种收费方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案一：每天吃 3 蒲式耳；&lt;/li&gt;
&lt;li&gt;方案二：每次吃得指数增加，但出现频率指数降低：
&lt;ul&gt;
&lt;li&gt;第 1 天吃 1；&lt;/li&gt;
&lt;li&gt;第 2 天再吃 2；&lt;/li&gt;
&lt;li&gt;第 4 天再吃 4；&lt;/li&gt;
&lt;li&gt;第 8 天再吃 8；&lt;/li&gt;
&lt;li&gt;依此类推。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果希望每天固定放入草料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方案一每天必须放 3；&lt;/li&gt;
&lt;li&gt;方案二每天只需放 2，就足以覆盖偶尔发生的大额消耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然方案二某些天开销巨大，但平均到每天后仍是常数。这称为&lt;strong&gt;摊还常数成本&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;AList&lt;/code&gt; 扩容&lt;/h4&gt;
&lt;p&gt;这与动态数组扩容非常相似。底层数组满时，&lt;code&gt;addLast&lt;/code&gt; 必须创建更大数组、复制旧元素，再写入新元素。&lt;/p&gt;
&lt;p&gt;固定增加容量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    if (size == items.length) {
        resize(size + RFACTOR);
    }
    items[size] = x;
    size += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按比例扩容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(int x) {
    if (size == items.length) {
        resize(size * RFACTOR);
    }
    items[size] = x;
    size += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一种方法会越来越频繁地复制巨大数组，性能很差。第二种几何扩容效果很好，Python 列表等动态数组也采用类似思想。&lt;/p&gt;
&lt;p&gt;假设扩容倍数为 2：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大多数添加只需一次写入，是 &lt;code&gt;Θ(1)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在容量为 1、2、4、8……时，某次添加要复制整个数组，单次可能是线性；&lt;/li&gt;
&lt;li&gt;但昂贵操作越来越稀疏，把成本平均到所有添加后，每次添加的摊还成本仍是 &lt;code&gt;Θ(1)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/8e8a479ed229-amortized_adds.png&quot; alt=&quot;摊还添加成本&quot;&gt;&lt;/p&gt;
&lt;h3&gt;摊还分析：更严格的解释&lt;/h3&gt;
&lt;p&gt;一般步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择成本模型；&lt;/li&gt;
&lt;li&gt;计算一系列操作的总成本或第 &lt;code&gt;i&lt;/code&gt; 次操作成本；&lt;/li&gt;
&lt;li&gt;证明平均的摊还成本被某个常数界定。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;假设动态数组初始容量为 1，只统计数组读写：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add(0)&lt;/code&gt;：写新元素，成本 1；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(1)&lt;/code&gt;：复制 1 个旧元素（1 读 1 写），再写新元素，成本 3；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(2)&lt;/code&gt;：复制 2 个旧元素，再写新元素，成本 5；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(3)&lt;/code&gt;：无需扩容，成本 1；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(4)&lt;/code&gt;：复制 4 个旧元素，再写新元素，成本 9。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 插入编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 新元素写入 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 扩容复制 | 0 | 2 | 4 | 0 | 8 | 0 | 0 | 0 | 16 | 0 | 0 | 0 | 0 | 0 |
| 本次总成本 | 1 | 3 | 5 | 1 | 9 | 1 | 1 | 1 | 17 | 1 | 1 | 1 | 1 | 1 |
| 累计成本 | 1 | 4 | 9 | 10 | 19 | 20 | 21 | 22 | 39 | 40 | 41 | 42 | 43 | 44 |&lt;/p&gt;
&lt;p&gt;平均成本始终保持在一个小常数附近，但只观察有限项不足以构成完整证明。&lt;/p&gt;
&lt;h4&gt;势能方法&lt;/h4&gt;
&lt;p&gt;令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cᵢ&lt;/code&gt;：第 &lt;code&gt;i&lt;/code&gt; 次操作的真实成本；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aᵢ&lt;/code&gt;：人为指定的摊还成本，对所有操作取同一个常数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Φᵢ&lt;/code&gt;：第 &lt;code&gt;i&lt;/code&gt; 次操作后的势能，即累计“预收成本减去真实成本”：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Φᵢ = Φᵢ₋₁ + aᵢ - cᵢ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能选择一个固定常数 &lt;code&gt;aᵢ&lt;/code&gt;，使势能从不为负，那么累计摊还成本始终不小于累计真实成本，因而 &lt;code&gt;aᵢ&lt;/code&gt; 是真实平均成本的上界。&lt;/p&gt;
&lt;p&gt;在草料例子中，势能可以理解为罐中剩余草料。每天放入固定数量，Grigometh 来时消耗。如果余额永不为负，就证明该固定日均投入足够。&lt;/p&gt;
&lt;p&gt;对动态数组，若令每次添加的摊还成本为 5：&lt;/p&gt;
&lt;p&gt;| 插入编号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 真实成本 &lt;code&gt;cᵢ&lt;/code&gt; | 1 | 3 | 5 | 1 | 9 | 1 | 1 | 1 | 17 | 1 | 1 | 1 | 1 | 1 |
| 摊还成本 &lt;code&gt;aᵢ&lt;/code&gt; | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 | 5 |
| 势能变化 | 4 | 2 | 0 | 4 | -4 | 4 | 4 | 4 | -12 | 4 | 4 | 4 | 4 | 4 |
| 势能 &lt;code&gt;Φᵢ&lt;/code&gt; | 4 | 6 | 6 | 10 | 6 | 10 | 14 | 18 | 6 | 10 | 14 | 18 | 22 | 26 |&lt;/p&gt;
&lt;p&gt;低成本添加积攒势能，随后支付偶尔出现的昂贵扩容。势能不为负，因此添加操作的摊还成本被常数界定。&lt;/p&gt;
&lt;p&gt;最终说明：几何扩容使动态数组 &lt;code&gt;add&lt;/code&gt; 具有摊还 &lt;code&gt;Θ(1)&lt;/code&gt; 时间。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;大 O 是上界，类似“渐近小于等于”；&lt;/li&gt;
&lt;li&gt;大 Omega 是下界，类似“渐近大于等于”；&lt;/li&gt;
&lt;li&gt;大 Theta 同时给出上下界，类似“渐近等于”；&lt;/li&gt;
&lt;li&gt;大 O 不代表最坏情况，大 Omega 也不代表最好情况；最好/最坏情况本身都可以使用 Theta 描述；&lt;/li&gt;
&lt;li&gt;日常交流常把“大 O”用在本应说“大 Theta”的地方；&lt;/li&gt;
&lt;li&gt;摊还分析证明一系列操作的平均成本；&lt;/li&gt;
&lt;li&gt;若选择固定 &lt;code&gt;aᵢ&lt;/code&gt;，并能保证势能 &lt;code&gt;Φᵢ&lt;/code&gt; 永不为负，则摊还成本是实际累计成本的上界。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 9 章 并查集</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/09-disjoint-sets</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/09-disjoint-sets</guid><description>CS61B 并查集中文教程，涵盖 Quick Find、Quick Union、加权合并、路径压缩与复杂度分析。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;9.1 介绍&lt;/h2&gt;
&lt;p&gt;如果两个集合没有共同元素，就称它们互不相交。**并查集（Disjoint Sets / Union-Find）**维护一组固定元素，并把它们划分为若干互不相交的集合。它支持两个核心操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;connect(x, y)&lt;/code&gt;：连接 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt;，也称 &lt;code&gt;union&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isConnected(x, y)&lt;/code&gt;：若 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;y&lt;/code&gt; 属于同一集合，返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;开始时，每个元素各自形成一个集合。调用 &lt;code&gt;connect&lt;/code&gt; 会合并集合。&lt;/p&gt;
&lt;p&gt;例如有 A、B、C、D 四个元素，初始状态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/5d822bef1102-intro1_resized.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;connect(A, B)&lt;/code&gt; 后：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/221e2cd285c7-intro2_resized.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;此时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;isConnected(A, B) -&gt; true
isConnected(A, C) -&gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再调用 &lt;code&gt;connect(A, D)&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bee2d7417a68-intro3_resized.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;程序找到 A 所在集合与 D 所在集合并合并，得到 &lt;code&gt;{A, B, D}&lt;/code&gt;，C 仍然独立。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;isConnected(A, D) -&gt; true
isConnected(A, C) -&gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先正式定义接口。接口只规定数据结构应具备&lt;strong&gt;什么行为&lt;/strong&gt;，不规定&lt;strong&gt;怎样实现&lt;/strong&gt;。这里先只处理非负整数元素；实际工程中可以给任意对象分配整数编号，所以这不构成本质限制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public interface DisjointSets {
    /** 连接 p 与 q。 */
    void connect(int p, int q);

    /** 判断 p 与 q 是否连通。 */
    boolean isConnected(int p, int q);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本章还将展示一个数据结构设计怎样逐步演化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Quick Find → Quick Union → 加权 Quick Union → 带路径压缩的加权 Quick Union
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到，设计选择会显著影响渐近运行时间与代码复杂度。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9.2 Quick Find&lt;/h2&gt;
&lt;p&gt;现在考虑怎样实现 &lt;code&gt;DisjointSets&lt;/code&gt; 所要求的行为。核心挑战是记录每个元素属于哪个集合。&lt;/p&gt;
&lt;h3&gt;集合列表方案&lt;/h3&gt;
&lt;p&gt;最直接的想法是使用集合列表：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;List&amp;#x3C;Set&amp;#x3C;Integer&gt;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若有 7 个尚未连接的元素：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[{0}, {1}, {2}, {3}, {4}, {5}, {6}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但执行 &lt;code&gt;connect(5, 6)&lt;/code&gt; 时，可能需要遍历接近 &lt;code&gt;N&lt;/code&gt; 个集合寻找 5，再遍历接近 &lt;code&gt;N&lt;/code&gt; 个集合寻找 6，运行时间是 &lt;code&gt;O(N)&lt;/code&gt;，代码也会比较复杂。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;初始设计决定了代码复杂度和运行时间。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Quick Find&lt;/h3&gt;
&lt;p&gt;换一种方案：只使用一个整数数组。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组&lt;strong&gt;索引&lt;/strong&gt;表示元素；&lt;/li&gt;
&lt;li&gt;索引处的&lt;strong&gt;值&lt;/strong&gt;表示该元素所属集合编号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如 &lt;code&gt;{0, 1, 2, 4}&lt;/code&gt;、&lt;code&gt;{3, 5}&lt;/code&gt;、&lt;code&gt;{6}&lt;/code&gt; 可以表示为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/eefc63f5a4fe-9.2.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;数组索引 0 到 6 是元素，&lt;code&gt;id[i]&lt;/code&gt; 是其集合编号。具体编号取什么并不重要，只要同一集合中的元素拥有相同编号即可。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;connect(x, y)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;假设 &lt;code&gt;id[2] = 4&lt;/code&gt;、&lt;code&gt;id[3] = 5&lt;/code&gt;。调用 &lt;code&gt;connect(2, 3)&lt;/code&gt; 后，原来编号为 4 和 5 的所有元素都应使用同一个编号。可以把所有 4 改为 5：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/e34b7812a6b9-9.2.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;为完成该操作，需要扫描整个数组并替换集合编号，所以是 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;isConnected(x, y)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;只需检查：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;id[x] == id[y]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是常数时间，因此该实现称为 &lt;strong&gt;Quick Find&lt;/strong&gt;：查询连通关系非常快。&lt;/p&gt;
&lt;p&gt;| 实现 | 构造 | &lt;code&gt;connect&lt;/code&gt; | &lt;code&gt;isConnected&lt;/code&gt; |
|---|---:|---:|---:|
| 集合列表 | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; |
| Quick Find | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(1)&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;N&lt;/code&gt; 是元素数量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class QuickFindDS implements DisjointSets {
    private int[] id;

    public QuickFindDS(int N) {
        id = new int[N];
        for (int i = 0; i &amp;#x3C; N; i += 1) {
            id[i] = i;
        }
    }

    public void connect(int p, int q) {
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i &amp;#x3C; id.length; i += 1) {
            if (id[i] == pid) {
                id[i] = qid;
            }
        }
    }

    public boolean isConnected(int p, int q) {
        return id[p] == id[q];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;9.3 Quick Union&lt;/h2&gt;
&lt;p&gt;如果我们更重视加快 &lt;code&gt;connect&lt;/code&gt;，可以仍然使用一个数组，但改变其含义：每个位置保存该元素的&lt;strong&gt;父结点索引&lt;/strong&gt;。根结点没有父结点，可以用负值标记。&lt;/p&gt;
&lt;p&gt;这样，每个集合都可以想象成一棵树。例如 &lt;code&gt;{0, 1, 2, 4}&lt;/code&gt;、&lt;code&gt;{3, 5}&lt;/code&gt;、&lt;code&gt;{6}&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b4e099cb6e6f-9.3.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;实际存储仍然只有数组，树只是对父指针关系的可视化。&lt;/p&gt;
&lt;p&gt;定义辅助方法 &lt;code&gt;find(item)&lt;/code&gt;，沿父指针向上找到根。例如上图中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;find(4) == 0
find(1) == 0
find(5) == 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个元素所属集合由其唯一根结点代表。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;connect(x, y)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;先找到两者所在树的根，再把一个根设为另一个根的子结点。&lt;/p&gt;
&lt;p&gt;例如 &lt;code&gt;connect(5, 2)&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;find(5) -&gt; 3&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find(2) -&gt; 0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;令 &lt;code&gt;parent[3] = 0&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/8a088212d965-9.3.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;元素 3 现在指向 0，两棵树合并为一棵。&lt;/p&gt;
&lt;p&gt;若 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;y&lt;/code&gt; 本来就是根，连接只需一次赋值，是 &lt;code&gt;Θ(1)&lt;/code&gt;，这就是 Quick Union 名称的由来。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;isConnected(x, y)&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;同一集合中的元素位于同一棵树，因此拥有同一个根：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;find(x) == find(y)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;性能问题&lt;/h3&gt;
&lt;p&gt;Quick Union 可能形成非常高的细长树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c7d6f1b37fb3-9.3.3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;最坏情况下，寻找根要经过所有 &lt;code&gt;N&lt;/code&gt; 个元素，是 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;code&gt;connect&lt;/code&gt; 和 &lt;code&gt;isConnected&lt;/code&gt; 都依赖 &lt;code&gt;find&lt;/code&gt;，所以二者最坏上界都是 &lt;code&gt;O(N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;| 实现 | 构造 | &lt;code&gt;connect&lt;/code&gt; | &lt;code&gt;isConnected&lt;/code&gt; |
|---|---:|---:|---:|
| Quick Find | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(1)&lt;/code&gt; |
| Quick Union | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;单看最坏上界，Quick Union 似乎更差。但若树保持平衡，两个操作都会相当快。下一节将保证树不会过高。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class QuickUnionDS implements DisjointSets {
    private int[] parent;

    public QuickUnionDS(int num) {
        parent = new int[num];
        for (int i = 0; i &amp;#x3C; num; i += 1) {
            parent[i] = -1;
        }
    }

    private int find(int p) {
        while (parent[p] &gt;= 0) {
            p = parent[p];
        }
        return p;
    }

    @Override
    public void connect(int p, int q) {
        int i = find(p);
        int j = find(q);
        parent[i] = j;
    }

    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;9.4 加权 Quick Union（WQU）&lt;/h2&gt;
&lt;p&gt;改进 Quick Union 的关键观察是：&lt;code&gt;find&lt;/code&gt; 必须沿树向上走到根，所以树越矮，操作越快。&lt;/p&gt;
&lt;p&gt;引入新规则：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每次 &lt;code&gt;connect&lt;/code&gt;，始终把&lt;strong&gt;较小树的根&lt;/strong&gt;连接到&lt;strong&gt;较大树的根&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个规则保证最大树高为 &lt;code&gt;Θ(log N)&lt;/code&gt;，其中 &lt;code&gt;N&lt;/code&gt; 是总元素数。&lt;/p&gt;
&lt;p&gt;考虑连接两棵树 &lt;code&gt;T1&lt;/code&gt; 和 &lt;code&gt;T2&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c17198313f16-9.4.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;有两种方向：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b2e33f764cdf-9.4.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;第二种更好，因为树高只有 2，而不是 3。它也符合新规则：大小为 3 的 &lt;code&gt;T2&lt;/code&gt; 被挂到大小为 6 的 &lt;code&gt;T1&lt;/code&gt; 下。&lt;/p&gt;
&lt;p&gt;为了判断树大小，需要在根结点记录权重。可以让根位置保存树大小的负数，例如大小 6 的树根保存 &lt;code&gt;-6&lt;/code&gt;，非根位置仍保存父结点索引。&lt;/p&gt;
&lt;h4&gt;为什么最大高度是 &lt;code&gt;log N&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;考虑树中的任意元素 &lt;code&gt;x&lt;/code&gt;。只有当 &lt;code&gt;x&lt;/code&gt; 所在的整棵树被挂到另一棵至少同样大的树下时，&lt;code&gt;x&lt;/code&gt; 的深度才增加 1。&lt;/p&gt;
&lt;p&gt;每发生一次深度增加，包含 &lt;code&gt;x&lt;/code&gt; 的新树大小至少翻倍。树最多从 1 翻倍到 &lt;code&gt;N&lt;/code&gt;，只能翻倍 &lt;code&gt;log₂N&lt;/code&gt; 次，因此任意元素深度最多增加 &lt;code&gt;log₂N&lt;/code&gt; 次。&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最大树高为 &lt;code&gt;Θ(log N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connect&lt;/code&gt; 与 &lt;code&gt;isConnected&lt;/code&gt; 都被 &lt;code&gt;O(log N)&lt;/code&gt; 界定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也可以按树高而不是树大小连接，但实现更复杂，最终仍只得到同样的对数高度保证。&lt;/p&gt;
&lt;p&gt;| 实现 | 构造 | &lt;code&gt;connect&lt;/code&gt; | &lt;code&gt;isConnected&lt;/code&gt; |
|---|---:|---:|---:|
| Quick Find | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;Θ(1)&lt;/code&gt; |
| Quick Union | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; |
| 加权 Quick Union | &lt;code&gt;Θ(N)&lt;/code&gt; | &lt;code&gt;O(log N)&lt;/code&gt; | &lt;code&gt;O(log N)&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;具体代码是&lt;a href=&quot;https://sp19.datastructur.es/materials/lab/lab6/lab6&quot;&gt;实验 6&lt;/a&gt;的任务。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9.5 带路径压缩的加权 Quick Union&lt;/h2&gt;
&lt;p&gt;加权 Quick Union 已经很好，但还能进一步改进。&lt;/p&gt;
&lt;p&gt;每次调用 &lt;code&gt;find(x)&lt;/code&gt;，本来就要沿路径从 &lt;code&gt;x&lt;/code&gt; 走到根。既然已经访问了路径上的所有结点，就可以顺便把它们全部直接连接到根，而不增加渐近复杂度。&lt;/p&gt;
&lt;p&gt;路径压缩会在每次 &lt;code&gt;find&lt;/code&gt; 后使树变矮。&lt;/p&gt;
&lt;p&gt;请注意：&lt;code&gt;connect(x, y)&lt;/code&gt; 与 &lt;code&gt;isConnected(x, y)&lt;/code&gt; 都会调用 &lt;code&gt;find(x)&lt;/code&gt; 和 &lt;code&gt;find(y)&lt;/code&gt;。经过足够多次操作后，绝大多数结点都会几乎直接指向根。&lt;/p&gt;
&lt;p&gt;因此，长期来看，&lt;code&gt;connect&lt;/code&gt; 和 &lt;code&gt;isConnected&lt;/code&gt; 的摊还运行时间接近常数。&lt;/p&gt;
&lt;p&gt;更精确的分析涉及反阿克曼函数 &lt;code&gt;α(N)&lt;/code&gt;。它增长极其缓慢，对任何现实输入都小于一个很小常数。因此通常把带路径压缩 WQU 的操作视为“实际常数时间”。&lt;/p&gt;
&lt;p&gt;| 实现 | &lt;code&gt;isConnected&lt;/code&gt; | &lt;code&gt;connect&lt;/code&gt; |
|---|---:|---:|
| Quick Find | &lt;code&gt;Θ(1)&lt;/code&gt; | &lt;code&gt;Θ(N)&lt;/code&gt; |
| Quick Union | &lt;code&gt;O(N)&lt;/code&gt; | &lt;code&gt;O(N)&lt;/code&gt; |
| 加权 Quick Union | &lt;code&gt;O(log N)&lt;/code&gt; | &lt;code&gt;O(log N)&lt;/code&gt; |
| 加权 Quick Union + 路径压缩 | 摊还 &lt;code&gt;O(α(N))&lt;/code&gt; | 摊还 &lt;code&gt;O(α(N))&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;α(N)&lt;/code&gt; 在长期表现上近似常数。具体实现同样属于&lt;a href=&quot;https://sp19.datastructur.es/materials/lab/lab6/lab6&quot;&gt;实验 6&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 10 章 抽象数据类型与树</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/10-adts-and-trees</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/10-adts-and-trees</guid><description>CS61B 抽象数据类型与树中文教程，介绍 ADT、二叉树、二叉搜索树以及树结构的基本操作。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;10.1 ADT 入门&lt;/h2&gt;
&lt;p&gt;**抽象数据类型（ADT）**只由它支持的操作定义，而不由具体实现定义。&lt;/p&gt;
&lt;p&gt;例如，项目 1A 中的 &lt;code&gt;ArrayDeque&lt;/code&gt; 和 &lt;code&gt;LinkedListDeque&lt;/code&gt; 提供相同方法，但内部代码完全不同。它们都是 &lt;code&gt;Deque&lt;/code&gt; ADT 的实现。&lt;/p&gt;
&lt;p&gt;ADT 与接口密切相关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从概念上，&lt;code&gt;Deque&lt;/code&gt; 是一组行为规范；&lt;/li&gt;
&lt;li&gt;在 Java 代码中，可以用 &lt;code&gt;Deque&lt;/code&gt; 接口表达该规范；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ArrayDeque&lt;/code&gt; 和 &lt;code&gt;LinkedListDeque&lt;/code&gt; 实现这个接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常见 ADT：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;栈（Stack）&lt;/strong&gt;：后进先出；
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push(x)&lt;/code&gt;：把 &lt;code&gt;x&lt;/code&gt; 放到栈顶；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt;：取出栈顶元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列表（List）&lt;/strong&gt;：有顺序的元素集合；
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add(i)&lt;/code&gt;：添加元素；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(i)&lt;/code&gt;：获得索引 &lt;code&gt;i&lt;/code&gt; 的元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集合（Set）&lt;/strong&gt;：无序且元素唯一；
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add(i)&lt;/code&gt;：添加元素；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(i)&lt;/code&gt;：判断是否包含某值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射（Map）&lt;/strong&gt;：键值对集合；
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;put(key, value)&lt;/code&gt;：加入或更新键值对；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(key)&lt;/code&gt;：获得键对应的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;List&lt;/code&gt;、&lt;code&gt;Set&lt;/code&gt; 等接口位于更大的集合框架中。下图白色为接口，蓝色为具体类：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bba029cd2c51-Screen-Shot-2019-02-27-at-1.54.27-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;ADT 让面向对象编程更灵活、更优雅。项目 1B 中，&lt;code&gt;OffByOne&lt;/code&gt; 与 &lt;code&gt;OffByN&lt;/code&gt; 可以互换，是因为它们实现同一接口。同理，调用者可以在不改变主要代码的情况下，在 &lt;code&gt;ArrayDeque&lt;/code&gt; 与 &lt;code&gt;LinkedListDeque&lt;/code&gt; 之间切换。&lt;/p&gt;
&lt;p&gt;后续章节将定义更多 ADT，并比较它们的不同实现。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;10.2 树&lt;/h2&gt;
&lt;p&gt;下面学习计算机科学中最重要的数据结构之一：树。&lt;/p&gt;
&lt;p&gt;链表很好用，但即使链表有序，查找元素仍可能很慢。若元素在末尾，就需要线性时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c57028055018-Screen-Shot-2019-02-28-at-12.55.40-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;有序数组可以使用二分查找，在 &lt;code&gt;Θ(log N)&lt;/code&gt; 时间内寻找元素。但链表无法常数时间访问中间位置，为了找到中间结点，本身就要线性遍历。&lt;/p&gt;
&lt;p&gt;一种改进是保存指向中间结点的引用，并允许向左右两个方向移动：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/faf76c3d2542-Screen-Shot-2019-02-28-at-12.51.06-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;还可以继续为每个左右部分保存中点引用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a54ddfca379e-Screen-Shot-2019-02-28-at-12.58.07-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;把结构纵向展开，就得到一棵树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bdfdee68b4c7-Screen-Shot-2019-02-28-at-12.59.39-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;该结构称为&lt;strong&gt;二叉树&lt;/strong&gt;，因为每个分支点最多分成两个方向。&lt;/p&gt;
&lt;h3&gt;树的性质&lt;/h3&gt;
&lt;p&gt;树由以下部分构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;结点；&lt;/li&gt;
&lt;li&gt;连接结点的边；&lt;/li&gt;
&lt;li&gt;任意两个结点之间只有一条简单路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在有根树中，我们指定一个没有父结点的&lt;strong&gt;根&lt;/strong&gt;。没有子结点的结点称为&lt;strong&gt;叶子&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下面都是合法的树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/e1f744ca554d-Screen-Shot-2019-02-28-at-9.25.43-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 10.2.1。&lt;/strong&gt; 给出一个不是树的结构。提示：可以让它出现环，或让两个结点之间存在多条路径。&lt;/p&gt;
&lt;p&gt;在一般树的约束上继续增加条件，就得到更具体的类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二叉树&lt;/strong&gt;：每个结点有 0、1 或 2 个子结点；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二叉搜索树（BST）&lt;/strong&gt;：对每个结点 &lt;code&gt;X&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;左子树中的每个键都小于 &lt;code&gt;X.key&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;右子树中的每个键都大于 &lt;code&gt;X.key&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个 BST 性质非常重要，后续会反复使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/833ce1e6b2f6-Screen-Shot-2019-02-28-at-9.36.01-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个简化的 BST 结点类：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private class BST&amp;#x3C;Key&gt; {
    private Key key;
    private BST&amp;#x3C;Key&gt; left;
    private BST&amp;#x3C;Key&gt; right;

    public BST(Key key, BST&amp;#x3C;Key&gt; left, BST&amp;#x3C;Key&gt; right) {
        this.key = key;
        this.left = left;
        this.right = right;
    }

    public BST(Key key) {
        this.key = key;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二叉搜索树操作&lt;/h3&gt;
&lt;h4&gt;查找&lt;/h4&gt;
&lt;p&gt;BST 性质让查找过程类似二分查找：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从根开始比较目标 &lt;code&gt;X&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;X&lt;/code&gt; 小于当前键，进入左子树；&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;X&lt;/code&gt; 大于当前键，进入右子树；&lt;/li&gt;
&lt;li&gt;若相等，找到目标；若走到 &lt;code&gt;null&lt;/code&gt;，说明不存在。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;练习 10.2.2。&lt;/strong&gt; 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static BST find(BST T, Key key)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一种递归写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static BST find(BST T, Key sk) {
    if (T == null) {
        return null;
    }
    if (sk.equals(T.key)) {
        return T;
    } else if (sk.compareTo(T.key) &amp;#x3C; 0) {
        return find(T.left, sk);
    } else {
        return find(T.right, sk);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若树比较平衡，高度为 &lt;code&gt;Θ(log N)&lt;/code&gt;，查找也只需对数时间。&lt;/p&gt;
&lt;h4&gt;插入&lt;/h4&gt;
&lt;p&gt;新键总是被插入到叶子位置。&lt;/p&gt;
&lt;p&gt;先按查找路径向下走：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若已经存在，通常不做任何事；&lt;/li&gt;
&lt;li&gt;若走到 &lt;code&gt;null&lt;/code&gt;，就在该位置创建新结点，从而保持 BST 性质。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;static BST insert(BST T, Key ik) {
    if (T == null) {
        return new BST(ik);
    }
    if (ik.compareTo(T.key) &amp;#x3C; 0) {
        T.left = insert(T.left, ik);
    } else if (ik.compareTo(T.key) &gt; 0) {
        T.right = insert(T.right, ik);
    }
    return T;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 10.2.3。&lt;/strong&gt; 想出不同插入顺序，使同一批键形成高度不同的树。极端情况下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合理顺序可以形成近似平衡树，高度 &lt;code&gt;Θ(log N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;按严格升序或降序插入可能形成链，高度 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;p&gt;删除后必须重建连接，并保持 BST 性质。分三种情况。&lt;/p&gt;
&lt;h5&gt;没有子结点&lt;/h5&gt;
&lt;p&gt;目标是叶子。把父结点中指向它的引用设为 &lt;code&gt;null&lt;/code&gt;，之后垃圾回收器会回收该结点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/6f6a95ee91f2-Screen-Shot-2019-02-28-at-10.34.53-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;只有一个子结点&lt;/h5&gt;
&lt;p&gt;让父结点直接指向目标的唯一子结点。由于 BST 性质对子树递归成立，这不会破坏顺序。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0f6aa602e985-Screen-Shot-2019-02-28-at-10.35.56-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;有两个子结点&lt;/h5&gt;
&lt;p&gt;不能随便选择一个子结点替换，否则可能破坏 BST 性质。替代结点必须：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大于左子树中的所有键；&lt;/li&gt;
&lt;li&gt;小于右子树中的所有键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;满足条件的典型选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;左子树中最右侧、也就是最大的结点；&lt;/li&gt;
&lt;li&gt;右子树中最左侧、也就是最小的结点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/34fb259f2a9f-Screen-Shot-2019-02-28-at-10.36.39-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;用选中的前驱或后继替换目标，再删除它原来的结点。该方法称为 &lt;strong&gt;Hibbard 删除&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/798d666f7ce9-Screen-Shot-2019-02-28-at-10.43.45-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;用 BST 实现 Set 与 Map&lt;/h3&gt;
&lt;p&gt;BST 可以实现 &lt;code&gt;Set&lt;/code&gt; ADT。数组集合的 &lt;code&gt;contains&lt;/code&gt; 最坏需要扫描全部元素，是 &lt;code&gt;Θ(N)&lt;/code&gt;；平衡 BST 能利用有序性质，把查找降到 &lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也可以让每个 BST 结点保存 &lt;code&gt;(key, value)&lt;/code&gt;，从而实现 &lt;code&gt;Map&lt;/code&gt;。树结构的位置由键决定，值作为对应数据一同保存。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ADT 由操作定义，而不是由实现定义；&lt;/li&gt;
&lt;li&gt;常见 ADT 包括并查集、映射、集合与列表；&lt;/li&gt;
&lt;li&gt;Java 提供 &lt;code&gt;Map&lt;/code&gt;、&lt;code&gt;Set&lt;/code&gt;、&lt;code&gt;List&lt;/code&gt; 接口及多种实现；&lt;/li&gt;
&lt;li&gt;集合或映射可以用数组实现，最坏操作 &lt;code&gt;Θ(N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;平衡 BST 的查找和插入为 &lt;code&gt;Θ(log N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;BST 查找与插入较直接；删除更复杂，常用 Hibbard 删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续练习：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/lab/lab7/lab7&quot;&gt;实验 7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sp19.datastructur.es/materials/discussion/disc07.pdf&quot;&gt;讨论课 7&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 11 章 平衡树</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/11-balanced-trees</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/11-balanced-trees</guid><description>CS61B 平衡树中文教程，讲解 2-3 树、旋转操作、左倾红黑树和保持搜索树平衡的方法。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;11.1 B 树入门&lt;/h2&gt;
&lt;h3&gt;二叉树高度&lt;/h3&gt;
&lt;p&gt;二叉搜索树最好与最坏结构的运行时间差异非常大：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最坏：&lt;code&gt;Θ(N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;最好：&lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果树非常细长，它实际上接近链表，操作需要线性时间；如果树枝繁叶茂，高度约为 &lt;code&gt;log N&lt;/code&gt;，操作就是对数时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c1173b000e78-Screen-Shot-2019-03-05-at-12.56.54-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;大 O 与最坏情况&lt;/h4&gt;
&lt;p&gt;大 O &lt;strong&gt;不等于&lt;/strong&gt;最坏情况。大 O 是上界；只要函数最终不超过该界，就属于这个大 O 集合。最坏情况描述则更具体。&lt;/p&gt;
&lt;p&gt;例如，若说“酒店最贵房间是 500 美元”，只有确实达到 500 美元的酒店符合；若说“所有房间价格是 &lt;code&gt;O(500)&lt;/code&gt;”，大量更便宜酒店也满足这个宽松上界。&lt;/p&gt;
&lt;p&gt;因此，即使 BST 最坏运行时间是 &lt;code&gt;Θ(N)&lt;/code&gt;，它也同时属于 &lt;code&gt;O(N²)&lt;/code&gt;。后者正确，但信息更少。&lt;/p&gt;
&lt;h3&gt;BST 性能术语&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;深度（depth）&lt;/strong&gt;：某结点与根之间的边数；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高度（height）&lt;/strong&gt;：树中结点的最大深度；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平均深度&lt;/strong&gt;：所有结点深度的平均值：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(Σ dᵢ nᵢ) / N
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;dᵢ&lt;/code&gt; 是深度，&lt;code&gt;nᵢ&lt;/code&gt; 是该深度的结点数。&lt;/p&gt;
&lt;p&gt;树的&lt;strong&gt;高度&lt;/strong&gt;决定最坏运行时间，因为最坏情况下目标位于最底层。平均深度则决定典型或平均查找成本。&lt;/p&gt;
&lt;h3&gt;插入顺序&lt;/h3&gt;
&lt;p&gt;BST 的插入顺序决定树高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 11.1.1。&lt;/strong&gt; 对整数 1 到 7：什么顺序产生最坏高度？什么顺序产生最好高度？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最坏：按 &lt;code&gt;1,2,3,4,5,6,7&lt;/code&gt; 插入，形成单链；&lt;/li&gt;
&lt;li&gt;最好：先 4，再 2、6，最后 1、3、5、7，形成满而平衡的树。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/85f72b4c21a2-Screen-Shot-2019-03-05-at-3.53.32-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不一定必须精心设计顺序。随机顺序插入 BST 时，期望平均深度和高度都是 &lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;但现实数据可能实时到达，程序只能按到达顺序插入，无法先随机打乱。下一节将学习始终自动保持平衡的树。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11.2 B 树&lt;/h2&gt;
&lt;p&gt;普通 BST 的问题在于：新键总是作为新的叶子插入，因此树高可能不断增加。即使一开始很平衡，某些插入顺序也会破坏结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/36a9a2ca16d8-Screen-Shot-2019-03-05-at-4.02.58-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个大胆想法是：不要立刻创建新叶子，而是把新键直接加入已有叶结点。这样树高不会增加。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/5d89fd7ebe83-Screen-Shot-2019-03-05-at-4.05.03-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但若一个结点无限容纳元素，查找结点内部某个键仍可能需要线性扫描，最终又退化为 &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;解决办法是限制单个结点可保存的键数。假设上限为 3 个键；若再插入一个导致出现 4 个键，就把结点拆分，并把中间键上推到父结点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ecc97f013a15-Screen-Shot-2019-03-05-at-4.12.17-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;例如包含 15 和 17 的结点有三个孩子，分别存放小于 15、介于 15 与 17、以及大于 17 的键。结点内部键保持有序，所以仍能利用搜索树思想。&lt;/p&gt;
&lt;p&gt;通过从中间拆分结点，可以始终保持所有叶子位于同一层。这类树称为 &lt;strong&gt;B 树&lt;/strong&gt;。常见特例包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2-3 树&lt;/strong&gt;：每个内部结点有 2 或 3 个孩子，即含 1 或 2 个键；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2-3-4 树&lt;/strong&gt;：每个内部结点有 2、3 或 4 个孩子，即含 1、2 或 3 个键。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;插入过程&lt;/h3&gt;
&lt;p&gt;对 2-3-4 树：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;像普通搜索树一样向下走，找到应插入的叶结点；&lt;/li&gt;
&lt;li&gt;把新键加入该叶结点；&lt;/li&gt;
&lt;li&gt;若结点溢出为 4 个键，就拆分，并把中间偏左键上推，重新分配孩子；&lt;/li&gt;
&lt;li&gt;若父结点因此溢出，继续向上拆分；&lt;/li&gt;
&lt;li&gt;直到某个父结点能够容纳，或根被拆分并生成新根。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;2-3 树过程类似，只是在临时出现 3 个键时把中间键上推。&lt;/p&gt;
&lt;p&gt;可通过&lt;a href=&quot;https://docs.google.com/presentation/d/1zhQDvbcDZ9RJgJl0bmqwFFlHP8ExbDFo36Q9ZWH9EgU/edit#slide=id.g508ece10b0_1_584&quot;&gt;可视化练习&lt;/a&gt;进一步熟悉。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11.3 B 树不变量与运行时间&lt;/h2&gt;
&lt;p&gt;普通 BST 的高度会受插入顺序强烈影响。B 树也可能因顺序得到不同具体高度，但它始终保持“矮而宽”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;练习 11.3.1。&lt;/strong&gt; 按顺序把 1 到 7 插入 B 树，观察高度。能否改变顺序进一步降低高度？&lt;/p&gt;
&lt;p&gt;一种能得到高度 1 的顺序是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;2, 3, 4, 5, 6, 1, 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2dc740182972-Screen-Shot-2019-03-05-at-4.35.18-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;B 树的重要不变量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有叶子到根的距离相同；&lt;/li&gt;
&lt;li&gt;含 &lt;code&gt;k&lt;/code&gt; 个键的非叶结点恰好有 &lt;code&gt;k+1&lt;/code&gt; 个孩子。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个条件共同保证树始终枝繁叶茂并保持完美平衡。&lt;/p&gt;
&lt;h3&gt;运行时间&lt;/h3&gt;
&lt;p&gt;设每个结点最多有 &lt;code&gt;L&lt;/code&gt; 个键，&lt;code&gt;L&lt;/code&gt; 是固定常数。&lt;/p&gt;
&lt;p&gt;最坏查找需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;沿树高访问 &lt;code&gt;O(log N)&lt;/code&gt; 个结点；&lt;/li&gt;
&lt;li&gt;在每个结点中检查至多 &lt;code&gt;L&lt;/code&gt; 个键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总工作为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;O(L log N) = O(log N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 &lt;code&gt;L&lt;/code&gt; 是常数。&lt;/p&gt;
&lt;p&gt;B 树删除更复杂，本课程正文不展开，可参考&lt;a href=&quot;https://docs.google.com/presentation/d/1zhQDvbcDZ9RJgJl0bmqwFFlHP8ExbDFo36Q9ZWH9EgU/edit#slide=id.g508ece10b0_1_1305&quot;&gt;附加幻灯片&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;BST 最好高度 &lt;code&gt;Θ(log N)&lt;/code&gt;，最坏高度 &lt;code&gt;Θ(N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;大 O 不等于最坏情况；&lt;/li&gt;
&lt;li&gt;B 树修改了二叉搜索树结构，避免线性高度；&lt;/li&gt;
&lt;li&gt;一个结点可保存 1 到 &lt;code&gt;L&lt;/code&gt; 个键；&lt;/li&gt;
&lt;li&gt;查找类似普通搜索树；&lt;/li&gt;
&lt;li&gt;插入把键加入已有叶结点，溢出时拆分；&lt;/li&gt;
&lt;li&gt;所有叶子保持同层，操作为 &lt;code&gt;O(log N)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;B 树实现更复杂，但能高效处理任意插入顺序。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;11.4 树旋转&lt;/h2&gt;
&lt;p&gt;B 树始终平衡，但实现很困难：结点能存多个键，拆分与孩子重排都较复杂。我们希望找到一种仍使用普通 BST 结点、却能够保持平衡的方法。&lt;/p&gt;
&lt;h3&gt;BST 的多种结构&lt;/h3&gt;
&lt;p&gt;同一组键可以组成多种都满足 BST 不变量的结构。下面各树都只包含 1、2、3：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f518c76661b7-Screen-Shot-2019-03-06-at-6.53.22-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不同插入顺序会得到不同结构。即使结点已经存在，也可以通过**旋转（rotation）**改变结构，同时保持所有键的中序顺序不变。&lt;/p&gt;
&lt;h3&gt;左旋与右旋&lt;/h3&gt;
&lt;p&gt;形式定义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rotateLeft(G)&lt;/code&gt;：令 &lt;code&gt;x&lt;/code&gt; 为 &lt;code&gt;G&lt;/code&gt; 的右孩子，把 &lt;code&gt;G&lt;/code&gt; 变成 &lt;code&gt;x&lt;/code&gt; 的左孩子；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rotateRight(G)&lt;/code&gt;：令 &lt;code&gt;x&lt;/code&gt; 为 &lt;code&gt;G&lt;/code&gt; 的左孩子，把 &lt;code&gt;G&lt;/code&gt; 变成 &lt;code&gt;x&lt;/code&gt; 的右孩子。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;左旋示意：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/765ac3f3006b-Screen-Shot-2019-03-06-at-10.25.18-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;设 &lt;code&gt;P&lt;/code&gt; 是 &lt;code&gt;G&lt;/code&gt; 的右孩子。左旋时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; 上升为子树新根；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P&lt;/code&gt; 原来的左子树交给 &lt;code&gt;G&lt;/code&gt; 作为右子树；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;G&lt;/code&gt; 下沉为 &lt;code&gt;P&lt;/code&gt; 的左孩子。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;旋转可以发生在非根结点：暂时断开该子树与父结点的连接，旋转，再把新子树根接回去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a401cefc317c-Screen-Shot-2019-03-06-at-10.37.17-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;简化实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    return x;
}

private Node rotateLeft(Node h) {
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;练习 11.4.2。&lt;/strong&gt; 把图中左树变成右树需要哪些旋转？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/175bba3ac789-Screen-Shot-2019-03-06-at-10.30.30-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;答案：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;rotateRight(3)
rotateLeft(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;旋转可以把一棵不平衡 BST 完全重新平衡。下一节将学习一种通过旋转自动维持平衡的树。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11.5 红黑树&lt;/h2&gt;
&lt;p&gt;2-3 树始终平衡，但实现复杂。能否用普通 BST 结点实现一种与 2-3 树结构等价、因而同样平衡的树？这就是红黑树的出发点。本节具体讨论&lt;strong&gt;左倾红黑树（LLRB）&lt;/strong&gt;，它对应 2-3 树。&lt;/p&gt;
&lt;h3&gt;从 2-3 树到 BST&lt;/h3&gt;
&lt;p&gt;2-3 树中的 2-结点本身就与普通 BST 结点相同，无需修改。&lt;/p&gt;
&lt;p&gt;对于含两个键的 3-结点，可以想象加入一个不保存信息的“胶水结点”，把两个键绑在一起：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/0b92a28b29b9-Screen-Shot-2019-03-06-at-10.51.15-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但额外结点浪费空间，也让代码丑陋。更好的办法是使用&lt;strong&gt;胶水边&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/304318233b30-Screen-Shot-2019-03-06-at-10.56.51-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;约定较小键作为较大键的左孩子，并把连接二者的特殊边标为红色；普通 BST 边标为黑色。于是得到&lt;strong&gt;左倾红黑树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每棵 2-3 树都唯一对应一棵 LLRB。一般红黑树则可对应 2-3-4 树。&lt;/p&gt;
&lt;h3&gt;LLRB 不变量&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;与 2-3 树一一对应；&lt;/li&gt;
&lt;li&gt;不允许某个结点同时参与两条连续向下的红边；&lt;/li&gt;
&lt;li&gt;不允许红色右边，红边必须左倾；&lt;/li&gt;
&lt;li&gt;从根到任意空叶子的路径包含相同数量的黑边；&lt;/li&gt;
&lt;li&gt;高度不超过对应 2-3 树高度的两倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;插入&lt;/h3&gt;
&lt;p&gt;理论上可以先向 2-3 树插入，再转换为 LLRB，但这失去了简化实现的意义。实际做法是先像普通 BST 一样递归插入，再用旋转与颜色翻转修复不变量。&lt;/p&gt;
&lt;h4&gt;任务一：新边颜色&lt;/h4&gt;
&lt;p&gt;2-3 树插入本质上是把新键加入某个叶结点，因此 LLRB 中新结点与父结点之间的边应设为红色。&lt;/p&gt;
&lt;h4&gt;任务二：修复右倾红边&lt;/h4&gt;
&lt;p&gt;LLRB 不允许红色右边。如果右孩子为红而左孩子不红，就对当前结点左旋：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a5fc2cf57bba-Screen-Shot-2019-03-06-at-11.14.41-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若左右孩子都为红，则暂时保留，交给颜色翻转处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/91d30f7a05e3-Screen-Shot-2019-03-06-at-11.33.14-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;任务三：修复连续左红边&lt;/h4&gt;
&lt;p&gt;若当前结点的左边为红，左孩子的左边也为红，就出现了非法的临时 4-结点。先右旋：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b2ba0dd82bc7-Screen-Shot-2019-03-06-at-11.36.00-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;随后翻转相关颜色，这等价于 2-3 树中把中间键向父结点上推并拆分临时 4-结点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/97fc799f5d3d-Screen-Shot-2019-03-06-at-11.37.57-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;修复规则汇总：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;插入新结点时使用红边；&lt;/li&gt;
&lt;li&gt;若出现右倾红边且左边不红：左旋；&lt;/li&gt;
&lt;li&gt;若出现两条连续左红边：右旋；&lt;/li&gt;
&lt;li&gt;若左右孩子都为红：颜色翻转，模拟结点拆分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可能需要从递归底部向上连续执行多次修复，直到整棵树重新满足不变量。&lt;/p&gt;
&lt;h3&gt;运行时间&lt;/h3&gt;
&lt;p&gt;LLRB 与 2-3 树一一对应，且高度最多是对应 2-3 树的两倍。因此查找和插入都为 &lt;code&gt;Θ(log N)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;抽象插入代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private Node put(Node h, Key key, Value val) {
    if (h == null) {
        return new Node(key, val, RED);
    }

    int cmp = key.compareTo(h.key);
    if (cmp &amp;#x3C; 0) {
        h.left = put(h.left, key, val);
    } else if (cmp &gt; 0) {
        h.right = put(h.right, key, val);
    } else {
        h.val = val;
    }

    if (isRed(h.right) &amp;#x26;&amp;#x26; !isRed(h.left)) {
        h = rotateLeft(h);
    }
    if (isRed(h.left) &amp;#x26;&amp;#x26; isRed(h.left.left)) {
        h = rotateRight(h);
    }
    if (isRed(h.left) &amp;#x26;&amp;#x26; isRed(h.right)) {
        flipColors(h);
    }

    return h;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码相当短，但背后维持了严格的数学对应关系。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;普通 BST 简单，但可能失衡并退化为线性时间；&lt;/li&gt;
&lt;li&gt;2-3 树始终平衡，但实现麻烦；&lt;/li&gt;
&lt;li&gt;LLRB 用普通 BST 结点和带颜色的边表示 2-3 树；&lt;/li&gt;
&lt;li&gt;插入实现相对简洁，删除仍然复杂；&lt;/li&gt;
&lt;li&gt;Java 的 &lt;a href=&quot;https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/999dbd4192d0f819cb5224f26e9e7fa75ca6f289/src/java.base/share/classes/java/util/TreeMap.java&quot;&gt;&lt;code&gt;TreeMap&lt;/code&gt;&lt;/a&gt; 使用红黑树，但不是左倾版本；&lt;/li&gt;
&lt;li&gt;LLRB 对应 2-3 树，一般红黑树可对应 2-3-4 树；&lt;/li&gt;
&lt;li&gt;实现比普通 BST 复杂，但提供稳定的 &lt;code&gt;Θ(log N)&lt;/code&gt; 性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 12 章 哈希</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/12-hashing</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/12-hashing</guid><description>CS61B 哈希中文教程，介绍哈希函数、哈希表、冲突处理、负载因子、扩容与性能分析。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;12.1 第一次尝试&lt;/h2&gt;
&lt;h3&gt;目前所学结构的问题&lt;/h3&gt;
&lt;p&gt;到目前为止，我们已经学习了几种能够高效判断元素是否存在的数据结构：先是二叉搜索树，随后又用 2-3 树让它保持平衡。&lt;/p&gt;
&lt;p&gt;不过，这些结构仍然存在限制——即使 2-3 树也不例外：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;元素必须能够比较。向 BST 中插入元素时，必须回答“它比根结点小还是大？”对于某些对象，这个问题根本没有意义。&lt;/li&gt;
&lt;li&gt;操作复杂度为 $\Theta(\log N)$。这已经很好，但也许还能更快。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;第一次尝试：&lt;code&gt;DataIndexedIntegerSet&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;我们先只解决第二个问题，把复杂度从 $\Theta(\log N)$ 降到 $\Theta(1)$；暂时不处理“元素是否可比较”。事实上，我们目前只考虑存储和查询 &lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一种直接的想法是：创建一个长度为 20 亿的 &lt;code&gt;boolean&lt;/code&gt; 数组，所有位置初始都是 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add(int x)&lt;/code&gt;：把数组第 &lt;code&gt;x&lt;/code&gt; 个位置设为 &lt;code&gt;true&lt;/code&gt;，耗时 $\Theta(1)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(int x)&lt;/code&gt;：直接返回数组第 &lt;code&gt;x&lt;/code&gt; 个位置的值，同样耗时 $\Theta(1)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DataIndexedIntegerSet {
    private boolean[] present;

    public DataIndexedIntegerSet() {
        present = new boolean[2000000000];
    }

    public void add(int x) {
        present[x] = true;
    }

    public boolean contains(int x) {
        return present[x];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来问题已经解决了，但实际上还有两个明显缺陷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极其浪费空间。&lt;/strong&gt; 若一个 &lt;code&gt;boolean&lt;/code&gt; 占 1 字节，则每个实例都需要约 2 GB 内存，而用户可能只插入寥寥几个元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只能处理整数。&lt;/strong&gt; 如果用户想插入 &lt;code&gt;String&lt;/code&gt;，甚至 &lt;code&gt;Dog&lt;/code&gt;，应该怎么办？&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;12.2 插入单词&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;DataIndexedIntegerSet&lt;/code&gt; 只能存整数。现在我们希望把字符串 &lt;code&gt;&quot;cat&quot;&lt;/code&gt; 放进去，并把新结构称为 &lt;code&gt;DataIndexedEnglishWordSet&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一个疯狂但自然的想法是：给每个字符串分配一个整数。例如 &lt;code&gt;cat -&gt; 1&lt;/code&gt;、&lt;code&gt;dog -&gt; 2&lt;/code&gt;、&lt;code&gt;turtle -&gt; 3&lt;/code&gt;。插入 &lt;code&gt;cat&lt;/code&gt; 时把 &lt;code&gt;present[1]&lt;/code&gt; 设为 &lt;code&gt;true&lt;/code&gt;；查询时再把 &lt;code&gt;cat&lt;/code&gt; 转成 1，并检查该位置。&lt;/p&gt;
&lt;p&gt;问题是，我们无法提前手工为所有可能的单词编号。必须设计一种通用规则，将任意字符串转换成整数。&lt;/p&gt;
&lt;h3&gt;策略一：只使用首字母&lt;/h3&gt;
&lt;p&gt;可以把首字符转换为数字，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat -&gt; c -&gt; 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dog -&gt; d -&gt; 4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;drum -&gt; d -&gt; 4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;dog&lt;/code&gt; 与 &lt;code&gt;drum&lt;/code&gt; 被映射到相同整数。两个不同输入得到相同结果，称为一次&lt;strong&gt;冲突（collision）&lt;/strong&gt;。我们暂时还不会处理冲突，因此先尝试避免冲突。&lt;/p&gt;
&lt;h3&gt;策略二：使用进位制避免冲突&lt;/h3&gt;
&lt;p&gt;十进制四位数 5149 可以写成：&lt;/p&gt;
&lt;p&gt;$$5\cdot10^3+1\cdot10^2+4\cdot10^1+9\cdot10^0$$&lt;/p&gt;
&lt;p&gt;任意四个十进制数字都能以这种方式得到唯一结果。这里的底数 10 非常关键，因为十进制恰好有十个数字 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;9&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;若错误地使用底数 2，就会发生冲突：&lt;/p&gt;
&lt;p&gt;$$1\cdot2^3+1\cdot2^2+1\cdot2^1+1\cdot2^0=15$$&lt;/p&gt;
&lt;p&gt;$$0\cdot2^3+3\cdot2^2+1\cdot2^1+1\cdot2^0=15$$&lt;/p&gt;
&lt;p&gt;英文字母表有 26 个小写字母。令 &lt;code&gt;a=1, b=2, ..., z=26&lt;/code&gt;，就可以把小写英文单词写成 26 进制数。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;$$\text{cat}=3\cdot26^2+1\cdot26^1+20\cdot26^0=2074$$&lt;/p&gt;
&lt;p&gt;**快速检查：**你会如何表示 &lt;code&gt;dog&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;这种方法为每个仅由小写英文字母组成的单词给出唯一整数，就像十进制为每个数提供唯一表示一样，因此不会产生冲突。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;DataIndexedEnglishWordSet&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DataIndexedEnglishWordSet {
    private boolean[] present;

    public DataIndexedEnglishWordSet() {
        present = new boolean[2000000000];
    }

    public void add(String s) {
        present[englishToInt(s)] = true;
    }

    public boolean contains(String s) {
        return present[englishToInt(s)];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;辅助方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int letterNum(String s, int i) {
    /** 把字符串第 i 个字符转换为字母编号：
      * &apos;a&apos; -&gt; 1, &apos;b&apos; -&gt; 2, ..., &apos;z&apos; -&gt; 26。 */
    int ithChar = s.charAt(i);
    if (ithChar &amp;#x3C; &apos;a&apos; || ithChar &gt; &apos;z&apos;) {
        throw new IllegalArgumentException();
    }
    return ithChar - &apos;a&apos; + 1;
}

public static int englishToInt(String s) {
    int intRep = 0;
    for (int i = 0; i &amp;#x3C; s.length(); i += 1) {
        intRep = intRep * 26;
        intRep += letterNum(s, i);
    }
    return intRep;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;当前进展&lt;/h3&gt;
&lt;p&gt;最初的目标包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;比 $\Theta(\log N)$ 更快。对于整数和小写英文单词，我们已经做到了。&lt;/li&gt;
&lt;li&gt;支持不可比较的对象。虽然目前只处理整数和单词，但算法从未依赖大小比较，因此方向是正确的。&lt;/li&gt;
&lt;li&gt;支持任意字符串，包括空格、其他语言和 emoji。&lt;/li&gt;
&lt;li&gt;解决巨大的空间浪费问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后两个问题仍然没有解决。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12.3 插入任意字符串与整数溢出&lt;/h2&gt;
&lt;h3&gt;超越单个英文单词&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/87c3e25e2698-ascii.png&quot; alt=&quot;ASCII 字符表&quot;&gt;&lt;/p&gt;
&lt;p&gt;ASCII 为每个字符分配了整数编号，最大编码大约为 126。因此，可以把任意 ASCII 字符串看成以 126 为底的数字：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int asciiToInt(String s) {
    int intRep = 0;
    for (int i = 0; i &amp;#x3C; s.length(); i += 1) {
        intRep = intRep * 126;
        intRep = intRep + s.charAt(i);
    }
    return intRep;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要支持中文，字符编号范围会更大，底数也必须相应增大。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ad26b13fe6d9-Screen-Shot-2019-03-08-at-12.49.36-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;仅仅存储一个三个汉字的词，理论上就可能需要超过 &lt;strong&gt;39 万亿&lt;/strong&gt; 个数组位置。这显然不可行。&lt;/p&gt;
&lt;h3&gt;整数溢出与哈希码&lt;/h3&gt;
&lt;h4&gt;溢出问题&lt;/h4&gt;
&lt;p&gt;Java &lt;code&gt;int&lt;/code&gt; 的最大值是 2,147,483,647，最小值是 -2,147,483,648。最大值再加 1 会回绕到最小值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ff74b24f6eea-Screen-Shot-2019-03-08-at-12.53.44-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;即使只处理 ASCII，数值也很快超过 &lt;code&gt;int&lt;/code&gt; 的范围。例如：&lt;/p&gt;
&lt;p&gt;$$\text{omens}_{126}=28,196,917,171$$&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;asciiToInt(&quot;omens&quot;)&lt;/code&gt; 实际会返回溢出后的负数。&lt;/p&gt;
&lt;p&gt;更糟糕的是，不同字符串可能因为溢出得到同一个结果。例如某些完全不同的长字符串会被映射到相同整数。于是插入其中一个后，查询另一个也可能错误地返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;无法逃避的事实&lt;/h4&gt;
&lt;p&gt;Java 中一共只有 $2^{32}=4,294,967,296$ 个不同的 &lt;code&gt;int&lt;/code&gt; 值，但能够创建的对象数量远远超过这个数。因此，不论转换算法多么聪明，冲突都不可避免。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们必须正面处理冲突。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;一个细微但重要的区别&lt;/h4&gt;
&lt;p&gt;问题并不是“溢出本身使我们无法把字符串转成整数”。溢出之后，我们仍然得到了一个整数。真正的问题是：溢出导致不同对象产生相同整数，而我们目前不会处理这种冲突。&lt;/p&gt;
&lt;h4&gt;哈希码&lt;/h4&gt;
&lt;p&gt;在计算机科学中，把一个对象转换为整数的过程称为计算对象的&lt;strong&gt;哈希码（hash code）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于其他对象，通常有两种做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Java 为所有对象提供的 &lt;code&gt;.hashCode()&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;为自己的类重写 &lt;code&gt;hashCode()&lt;/code&gt;。例如，可以组合 &lt;code&gt;Dog&lt;/code&gt; 的名字、年龄和品种来计算哈希码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;哈希码的性质&lt;/h3&gt;
&lt;p&gt;一个&lt;strong&gt;有效&lt;/strong&gt;的哈希码必须满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结果是整数。&lt;/li&gt;
&lt;li&gt;对同一个未改变的对象多次调用 &lt;code&gt;.hashCode()&lt;/code&gt;，结果必须相同。&lt;/li&gt;
&lt;li&gt;若两个对象通过 &lt;code&gt;.equals()&lt;/code&gt; 被认为相等，则它们必须拥有相同哈希码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个&lt;strong&gt;好的&lt;/strong&gt;哈希函数还应尽量让不同对象均匀分布到不同整数上。&lt;/p&gt;
&lt;p&gt;到这里，我们已经能够为任意对象计算哈希码，而不再局限于字符串。&lt;/p&gt;
&lt;h4&gt;尚未解决的问题&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;空间仍然过于庞大。&lt;/li&gt;
&lt;li&gt;已经证明冲突不可避免，但还没有真正处理它。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;12.4 处理冲突&lt;/h2&gt;
&lt;p&gt;现在正面处理冲突。核心思路是：数组的每个位置不再只存一个元素，而是存放一个元素列表，例如 &lt;code&gt;LinkedList&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;数组最初全部为空。若新元素的哈希码为 $h$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若索引 $h$ 处为空，就创建一个新的 &lt;code&gt;LinkedList&lt;/code&gt;，并把元素加入其中。&lt;/li&gt;
&lt;li&gt;若索引 $h$ 已经有列表，就先在列表中检查该元素是否存在；不存在时再追加。集合不能包含重复元素，因此不能跳过这次检查。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种做法称为&lt;strong&gt;拉链法（separate chaining）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;具体流程&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;add(item)&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;计算元素的哈希码，也就是目标索引。&lt;/li&gt;
&lt;li&gt;若该索引为空，创建列表并加入元素。&lt;/li&gt;
&lt;li&gt;若已有列表，遍历检查元素是否已存在；不存在时加入。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;code&gt;contains(item)&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;计算元素的哈希码。&lt;/li&gt;
&lt;li&gt;若对应索引为空，返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;否则遍历该索引上的列表；找到元素则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;运行时间&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4b1439a6fbe7-Screen-Shot-2019-03-08-at-1.19.34-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若目标桶中的链表长度为 $Q$，那么 &lt;code&gt;contains&lt;/code&gt; 需要检查最多 $Q$ 个元素，耗时 $\Theta(Q)$。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add&lt;/code&gt; 也需要 $\Theta(Q)$。虽然链表头插只需 $\Theta(1)$，但插入前仍必须确认元素没有重复。&lt;/p&gt;
&lt;h3&gt;有得有失&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;冲突问题已经解决。&lt;/li&gt;
&lt;li&gt;空间问题还没有解决。&lt;/li&gt;
&lt;li&gt;最坏情况下，所有元素可能拥有相同哈希码，全部落入一个长度为 $N$ 的链表，因此操作仍可能退化为 $\Theta(N)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解决空间问题&lt;/h3&gt;
&lt;p&gt;既然我们已经能够处理冲突，就没有必要保留几十亿个数组位置。可以只创建一个较小的数组，例如长度 100，然后把哈希码取模：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int index = Math.floorMod(item.hashCode(), 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，任意哈希码都会被压缩到 &lt;code&gt;0&lt;/code&gt; 到 &lt;code&gt;99&lt;/code&gt;。冲突会增加，但我们已经有链表来处理它。&lt;/p&gt;
&lt;p&gt;代价是：原本分布在数十亿个索引上的元素现在被压缩到 100 个桶，桶中的链表会更长。&lt;/p&gt;
&lt;h3&gt;当前状态&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;空间问题：已解决。&lt;/li&gt;
&lt;li&gt;冲突问题：已解决。&lt;/li&gt;
&lt;li&gt;性能问题：仍需改善，因为桶中的链表可能很长。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;12.5 哈希表与性能修正&lt;/h2&gt;
&lt;h3&gt;最终结构：&lt;code&gt;HashTable&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;目前构造出的数据结构就是&lt;strong&gt;哈希表&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;哈希函数把输入对象转换为整数哈希码。&lt;/li&gt;
&lt;li&gt;对哈希码取模，得到合法数组索引。&lt;/li&gt;
&lt;li&gt;把元素放入该索引对应的链表，并用链表处理冲突。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;contains&lt;/code&gt; 的过程类似：计算索引，然后在对应桶的链表中查找元素。&lt;/p&gt;
&lt;h3&gt;处理运行时间问题&lt;/h3&gt;
&lt;p&gt;假设哈希表有 5 个桶、100 个元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最理想时，元素均匀分布，每个桶约有 20 个元素。&lt;/li&gt;
&lt;li&gt;最坏时，所有元素都进入同一个桶，该桶有 100 个元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以从两个方向改善：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态扩容哈希表。&lt;/li&gt;
&lt;li&gt;设计更好的哈希函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;动态扩容&lt;/h4&gt;
&lt;p&gt;设桶数为 $M$，元素数为 $N$，定义**负载因子（load factor）**为：&lt;/p&gt;
&lt;p&gt;$$\text{load factor}=\frac{N}{M}$$&lt;/p&gt;
&lt;p&gt;若 $M$ 固定而 $N$ 不断增长，负载因子也会不断增大。常见策略是设置一个负载因子阈值；超过阈值后，把桶数扩大一倍。&lt;/p&gt;
&lt;p&gt;扩容步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个拥有 &lt;code&gt;2M&lt;/code&gt; 个桶的新哈希表。&lt;/li&gt;
&lt;li&gt;遍历旧表中的所有元素，逐个重新插入新表。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;必须重新插入，而不能直接复制桶，因为模数变了，元素对应的索引也可能改变。例如哈希码 13：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 4 个桶中：$13\bmod4=1$&lt;/li&gt;
&lt;li&gt;在 8 个桶中：$13\bmod8=5$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/76ccdee61f5a-Screen-Shot-2019-03-08-at-1.37.16-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在元素近似均匀分布的前提下，每个桶约有 $N/M$ 个元素，操作时间为 $\Theta(N/M)$。由于负载因子被一个常数阈值限制，因此：&lt;/p&gt;
&lt;p&gt;$$\Theta(N/M)=\Theta(1)$$&lt;/p&gt;
&lt;p&gt;扩容本身需要重新处理 $N$ 个元素，所以耗时 $\Theta(N)$。不过扩容并不会每次插入都发生；结合几何扩容，其平均摊还成本仍可保持常数级。&lt;/p&gt;
&lt;p&gt;重新散列时，我们已经知道旧表中不存在重复元素，因此可以省略“是否已存在”的检查，直接把元素插入桶头。&lt;/p&gt;
&lt;h4&gt;更好的哈希码&lt;/h4&gt;
&lt;p&gt;上述常数时间分析依赖于一个关键假设：元素能够较均匀地分布在桶中。若哈希函数很差，所有元素仍可能进入同一个桶，操作就会退化为 $\Theta(N)$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4892d1f77f59-Screen-Shot-2019-03-08-at-1.44.43-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;设计哈希函数的一些经验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用与前文类似的“进位制”累积策略。&lt;/li&gt;
&lt;li&gt;底数通常选择较小的质数。&lt;/li&gt;
&lt;li&gt;像 126 这样的底数可能与整数溢出的周期产生不良规律，让许多不同字符串得到同样的哈希码。&lt;/li&gt;
&lt;li&gt;质数有助于减少这类规律性冲突，同时较小的质数计算成本低。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bac739f68c3a-Screen-Shot-2019-03-08-at-1.49.51-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1786960e3f77-Screen-Shot-2019-03-08-at-1.49.56-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;我们从零逐步构造出了哈希表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哈希码把任意对象映射为整数。&lt;/li&gt;
&lt;li&gt;取模控制数组大小。&lt;/li&gt;
&lt;li&gt;拉链法处理冲突。&lt;/li&gt;
&lt;li&gt;动态扩容控制负载因子。&lt;/li&gt;
&lt;li&gt;良好的哈希函数让元素尽可能均匀分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在合理假设下，哈希表的插入和查询可达到摊还 $\Theta(1)$。&lt;/p&gt;
&lt;p&gt;下一步可完成课程作业 &lt;a href=&quot;https://sp19.datastructur.es/materials/hw/hw3/hw3&quot;&gt;HW3&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 13 章 堆与优先队列</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/13-heaps-and-priority-queues</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/13-heaps-and-priority-queues</guid><description>CS61B 堆与优先队列中文教程，讲解二叉堆、堆操作、优先队列接口及其渐近运行时间。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;13.1 优先队列接口&lt;/h2&gt;
&lt;p&gt;我们前面学习的二叉搜索树可以在 $\log N$ 时间内完成高效搜索，因为每一步都能排除一半元素。但如果我们更关心的是快速找到&lt;strong&gt;最小&lt;/strong&gt;或&lt;strong&gt;最大&lt;/strong&gt;元素，而不是搜索任意元素呢？&lt;/p&gt;
&lt;h3&gt;优先队列 ADT&lt;/h3&gt;
&lt;p&gt;优先队列（Priority Queue）是一种抽象数据类型。可以把它想成一个袋子：我们可以加入和移除物品，但只能直接访问其中优先级最高的元素。对于最小优先队列，优先级最高的就是最小元素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** 最小优先队列：跟踪并删除最小元素。 */
public interface MinPQ&amp;#x3C;Item&gt; {
    /** 向优先队列加入元素。 */
    void add(Item x);

    /** 返回最小元素，但不删除。 */
    Item getSmallest();

    /** 删除并返回最小元素。 */
    Item removeSmallest();

    /** 返回元素数量。 */
    int size();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用场景&lt;/h3&gt;
&lt;p&gt;假设我们持续收集一天内的短信，并希望用 &lt;code&gt;HarmoniousnessComparator&lt;/code&gt; 找出最“不和谐”的 $M$ 条消息。&lt;/p&gt;
&lt;p&gt;一种简单方法是保存当天全部 $N$ 条消息，排序后返回前 $M$ 条：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public List&amp;#x3C;String&gt; unharmoniousTexts(Sniffer sniffer, int M) {
    ArrayList&amp;#x3C;String&gt; allMessages = new ArrayList&amp;#x3C;&gt;();
    for (Timer timer = new Timer(); timer.hours() &amp;#x3C; 24; ) {
        allMessages.add(sniffer.getNextMessage());
    }

    Comparator&amp;#x3C;String&gt; cmptr = new HarmoniousnessComparator();
    Collections.sort(allMessages, cmptr.reversed());
    return allMessages.subList(0, M);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题是它需要 $\Theta(N)$ 空间，而我们真正关心的只有 $M$ 条消息。使用优先队列，可以在扫描消息的过程中始终只保留当前最值得关注的 $M$ 条，因此只需 $\Theta(M)$ 空间。&lt;/p&gt;
&lt;p&gt;**练习 13.1.1：**重写上述方法，使其只使用 $\Theta(M)$ 额外空间。&lt;/p&gt;
&lt;h3&gt;可能的实现&lt;/h3&gt;
&lt;p&gt;ADT 只定义行为，尚未规定底层结构。考虑已经学过的数据结构，其最坏情况复杂度如下。&lt;/p&gt;
&lt;h4&gt;有序数组&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：$\Theta(N)$，插入时需要移动元素。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSmallest&lt;/code&gt;：$\Theta(1)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;removeSmallest&lt;/code&gt;：$\Theta(N)$，删除后需要移动元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;茂密的 BST&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：$\Theta(\log N)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSmallest&lt;/code&gt;：$\Theta(\log N)$，需要沿左链下降。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;removeSmallest&lt;/code&gt;：$\Theta(\log N)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;哈希表&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：摊还 $\Theta(1)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSmallest&lt;/code&gt;：$\Theta(N)$，哈希表不保留顺序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;removeSmallest&lt;/code&gt;：$\Theta(N)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**练习 13.1.2：**解释每个复杂度，并思考如何修改其中一种结构来改善性能。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;优先队列是专门优化最小值或最大值访问的 ADT。&lt;/li&gt;
&lt;li&gt;针对问题选用更专门的结构，往往能节省空间。&lt;/li&gt;
&lt;li&gt;已知结构都不能同时高效完成三个核心操作；其中平衡 BST 最接近目标。&lt;/li&gt;
&lt;li&gt;下一节将介绍更合适的实现：堆。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;13.2 堆&lt;/h2&gt;
&lt;p&gt;前一节中，已知结构里最适合实现优先队列的是二叉搜索树。通过改变树的约束，我们还能进一步提高相关操作的效率。&lt;/p&gt;
&lt;p&gt;定义一个&lt;strong&gt;二叉最小堆（binary min-heap）&lt;/strong&gt;，它必须同时满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**最小堆性质：**每个结点都小于或等于它的两个孩子。&lt;/li&gt;
&lt;li&gt;**完全性：**除最底层外每一层都填满；最底层的结点尽量靠左排列。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/d87fa9ebceb0-heap-13.2.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中的绿色结构是合法堆，红色结构至少违反了一个条件。&lt;/p&gt;
&lt;h3&gt;堆操作&lt;/h3&gt;
&lt;p&gt;优先队列最关心三个操作：&lt;code&gt;add&lt;/code&gt;、&lt;code&gt;getSmallest&lt;/code&gt; 和 &lt;code&gt;removeSmallest&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;add&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;临时把新元素放在堆的末尾，以保持完全性。&lt;/li&gt;
&lt;li&gt;若它小于父结点，就与父结点交换。&lt;/li&gt;
&lt;li&gt;重复向上交换，直到堆序恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个过程称为&lt;strong&gt;上浮（swim）&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;getSmallest&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;直接返回根结点。最小堆性质保证根一定是最小元素，因此耗时 $\Theta(1)$。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;removeSmallest&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用堆中最后一个元素替换根。&lt;/li&gt;
&lt;li&gt;删除原来的最后位置。&lt;/li&gt;
&lt;li&gt;若当前结点大于孩子，就与两个孩子中较小的那个交换。&lt;/li&gt;
&lt;li&gt;重复向下交换，直到堆序恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个过程称为&lt;strong&gt;下沉（sink）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;**练习 13.2.1：**根据以上描述，为三个操作写出伪代码。&lt;/p&gt;
&lt;p&gt;**练习 13.2.2：**分别给出这些操作的最好和最坏情况复杂度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：最好 $\Theta(1)$，最坏 $\Theta(\log N)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getSmallest&lt;/code&gt;：$\Theta(1)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;removeSmallest&lt;/code&gt;：最好 $\Theta(1)$，最坏 $\Theta(\log N)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;树的表示方法&lt;/h3&gt;
&lt;p&gt;树可以用多种方式表示。&lt;/p&gt;
&lt;h4&gt;方法 1A：固定孩子指针&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Tree1A&amp;#x3C;Key&gt; {
    Key k;
    Tree1A&amp;#x3C;Key&gt; left;
    Tree1A&amp;#x3C;Key&gt; middle;
    Tree1A&amp;#x3C;Key&gt; right;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/be971c06f261-Screen-Shot-2019-03-09-at-9.54.04-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;结点直接保存孩子引用，结构直观，但每个结点的最大孩子数量固定。&lt;/p&gt;
&lt;h4&gt;方法 1B：孩子数组&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Tree1B&amp;#x3C;Key&gt; {
    Key k;
    Tree1B&amp;#x3C;Key&gt;[] children;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/54db55609872-Screen-Shot-2019-03-09-at-10.03.15-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;孩子数量可变，但遍历和内存管理更复杂。&lt;/p&gt;
&lt;h4&gt;方法 1C：第一个孩子与兄弟指针&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Tree1C&amp;#x3C;Key&gt; {
    Key k;
    Tree1C&amp;#x3C;Key&gt; favoredChild;
    Tree1C&amp;#x3C;Key&gt; sibling;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f1619b5c6b28-Screen-Shot-2019-03-09-at-10.08.44-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;每个结点只保存一个孩子和一个兄弟引用，也能表达任意宽度的树。&lt;/p&gt;
&lt;p&gt;以上方法都显式存储孩子引用。下面考虑不直接保存孩子指针的方案。&lt;/p&gt;
&lt;h4&gt;方法 2：键数组与父结点数组&lt;/h4&gt;
&lt;p&gt;类似加权 Quick Union，可以分别保存键与父结点编号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Tree2&amp;#x3C;Key&gt; {
    Key[] keys;
    int[] parents;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/9730197a8bcb-Screen-Shot-2019-03-09-at-10.15.11-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;观察可发现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;图中的树是完全树。&lt;/li&gt;
&lt;li&gt;父结点编号呈现规律性的重复模式。&lt;/li&gt;
&lt;li&gt;按层序读取树，顺序恰好与 &lt;code&gt;keys&lt;/code&gt; 数组一致。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，对于完全二叉树，&lt;code&gt;parents&lt;/code&gt; 数组其实是冗余的。&lt;/p&gt;
&lt;h4&gt;方法 3：只用数组存完全树&lt;/h4&gt;
&lt;p&gt;若树保证完全，就可以按层序把二维树结构压平到一维数组中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TreeC&amp;#x3C;Key&gt; {
    Key[] keys;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2de368c2344b-Screen-Shot-2019-03-09-at-10.26.05-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这正是数组堆的核心表示方法。&lt;/p&gt;
&lt;h3&gt;上浮代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void swim(int k) {
    if (keys[parent(k)].compareTo(keys[k]) &gt; 0) {
        swap(k, parent(k));
        swim(parent(k));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;parent(k)&lt;/code&gt; 根据数组位置计算父结点索引。&lt;/p&gt;
&lt;p&gt;**练习 13.2.3：**实现 &lt;code&gt;parent&lt;/code&gt;。进一步实现 &lt;code&gt;leftChild&lt;/code&gt; 和 &lt;code&gt;rightChild&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;13.3 实现细节&lt;/h2&gt;
&lt;p&gt;实际采用的数组堆与上一节的方法 3 基本相同。唯一的重要区别是：数组索引 0 留空，从索引 1 开始存放根。这样父子编号计算会非常简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;leftChild(k) = 2 * k&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rightChild(k) = 2 * k + 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parent(k) = k / 2&lt;/code&gt;（整数除法）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;与其他实现比较&lt;/h3&gt;
&lt;p&gt;| 操作 | 有序数组 | 茂密 BST | 哈希表 | 堆 |
|---|---:|---:|---:|---:|
| &lt;code&gt;add&lt;/code&gt; | $\Theta(N)$ | $\Theta(\log N)$ | $\Theta(1)$ | $\Theta(\log N)$ |
| &lt;code&gt;getSmallest&lt;/code&gt; | $\Theta(1)$ | $\Theta(\log N)$ | $\Theta(N)$ | $\Theta(1)$ |
| &lt;code&gt;removeSmallest&lt;/code&gt; | $\Theta(N)$ | $\Theta(\log N)$ | $\Theta(N)$ | $\Theta(\log N)$ |&lt;/p&gt;
&lt;p&gt;堆让三个核心优先队列操作都获得了很好的性能。&lt;/p&gt;
&lt;p&gt;需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数组可能扩容，因此堆中某些复杂度应按摊还分析理解。&lt;/li&gt;
&lt;li&gt;若 BST 额外保存指向最小结点的引用，也能让 &lt;code&gt;getSmallest&lt;/code&gt; 达到常数时间。&lt;/li&gt;
&lt;li&gt;数组堆通常只需直接指针树表示法约三分之一的内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;尚待决定的问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;优先队列如何知道元素的排列方式？例如 &lt;code&gt;Dog&lt;/code&gt; 应按体重还是品种排序？&lt;/li&gt;
&lt;li&gt;如何让同一种元素支持多种顺序？&lt;/li&gt;
&lt;li&gt;怎样把 &lt;code&gt;MinPQ&lt;/code&gt; 改成 &lt;code&gt;MaxPQ&lt;/code&gt;？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;常见答案是让元素实现 &lt;code&gt;Comparable&lt;/code&gt;，或在构造优先队列时传入 &lt;code&gt;Comparator&lt;/code&gt;。最大堆则反转比较关系即可。&lt;/p&gt;
&lt;p&gt;**练习 13.3.1：**完整回答以上三个问题。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 14 章 数据结构总结</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/14-data-structures-summary</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/14-data-structures-summary</guid><description>CS61B 数据结构总结中文教程，对比列表、集合、映射、树、哈希表、堆等结构的用途与复杂度。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;14.1 数据结构总结&lt;/h2&gt;
&lt;h3&gt;搜索问题&lt;/h3&gt;
&lt;p&gt;我们面对的基本问题是：给定持续到来的数据流，如何取回自己关心的信息？&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户在个人主页发布内容，但只向好友展示。&lt;/li&gt;
&lt;li&gt;根据成千上万个气象站的日志，显示某个日期和时刻的天气地图。&lt;/li&gt;
&lt;li&gt;宠物主人寻找“最好”的宠物店，而“最好”可能按价格、质量或氛围定义。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前学习的各种数据结构，本质上都在解决搜索问题。它们采用不同的组织方式，让特定场景下的检索更高效。&lt;/p&gt;
&lt;h4&gt;常见搜索 ADT&lt;/h4&gt;
&lt;p&gt;| 名称 | 存储操作 | 主要取回操作 | 取回依据 |
|---|---|---|---|
| List | &lt;code&gt;add(key)&lt;/code&gt;、&lt;code&gt;insert(key, index)&lt;/code&gt; | &lt;code&gt;get(index)&lt;/code&gt; | 索引 |
| Map | &lt;code&gt;put(key, value)&lt;/code&gt; | &lt;code&gt;get(key)&lt;/code&gt; | 键的身份 |
| Set | &lt;code&gt;add(key)&lt;/code&gt; | &lt;code&gt;contains(key)&lt;/code&gt; | 键的身份 |
| Priority Queue | &lt;code&gt;add(key)&lt;/code&gt; | &lt;code&gt;getSmallest()&lt;/code&gt; | 键的顺序或优先级 |
| Disjoint Sets | &lt;code&gt;connect(a, b)&lt;/code&gt; | &lt;code&gt;isConnected(a, b)&lt;/code&gt; | 两个元素的连通性 |&lt;/p&gt;
&lt;p&gt;这些都是&lt;strong&gt;抽象数据类型&lt;/strong&gt;：它们定义行为，而不是实现。前面各章已经讨论了许多可能的底层结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f514b1dead93-Screen-Shot-2019-03-13-at-11.37.10-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同一种实现可以服务多个 ADT，但并不是每种组合都高效。图中性能较差的实现提醒我们：能实现某种行为，不代表它就是合适的选择。&lt;/p&gt;
&lt;p&gt;**练习 14.1.1：**思考如何修改每种实现以适配目标 ADT。例如，哈希表怎样勉强实现优先队列？为什么性能不理想？&lt;/p&gt;
&lt;h3&gt;抽象的层次&lt;/h3&gt;
&lt;p&gt;抽象经常分层出现。一个 ADT 的实现本身，也可能依赖另一个 ADT。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先队列可以用堆序树实现；而堆序树又可以用直接结点指针、父结点数组或层序数组等多种方式表示。&lt;/li&gt;
&lt;li&gt;拉链哈希表由“桶数组”组成；每个桶又可以使用 &lt;code&gt;ArrayList&lt;/code&gt;、动态数组、链表或 BST。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，我们常常用一个 ADT 来构建另一个 ADT。每一层抽象只规定自己关心的行为，并把更底层的实现细节隐藏起来。这种分层让程序更容易替换实现、比较权衡和控制复杂度。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 15 章 Trie 字典树</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/15-tries</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/15-tries</guid><description>CS61B Trie 字典树中文教程，介绍字符串键集合、前缀查询、Trie 结构及其时间与空间权衡。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;15.1 Trie 入门&lt;/h2&gt;
&lt;p&gt;Trie（字典树、前缀树）是 &lt;code&gt;Set&lt;/code&gt; 和 &lt;code&gt;Map&lt;/code&gt; 的一种特殊实现。它针对能够拆分为“字符”、且不同键之间经常共享前缀的数据，提供了额外能力。&lt;/p&gt;
&lt;h3&gt;回顾已有实现&lt;/h3&gt;
&lt;p&gt;此前，Set 和 Map 主要使用平衡搜索树或拉链哈希表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;平衡搜索树：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;contains(x)&lt;/code&gt;：$\Theta(\log N)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(x)&lt;/code&gt;：$\Theta(\log N)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;可扩容拉链哈希表：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;contains(x)&lt;/code&gt;：在分布均匀的假设下为 $\Theta(1)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add(x)&lt;/code&gt;：在分布均匀且按摊还分析时为 $\Theta(1)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些复杂度已经非常优秀。但如果我们知道键具有特殊结构，还能使用更专门的方法。&lt;/p&gt;
&lt;p&gt;例如，若所有键都是 ASCII 字符，可直接用字符编码作为数组索引：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DataIndexedCharMap&amp;#x3C;V&gt; {
    private V[] items;

    public DataIndexedCharMap(int R) {
        items = (V[]) new Object[R];
    }

    public void put(char c, V val) {
        items[c] = val;
    }

    public V get(char c) {
        return items[c];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;R&lt;/code&gt; 是可能字符的数量，例如 ASCII 可取 128。知道键的范围后，这个实现非常简单高效。&lt;/p&gt;
&lt;p&gt;**练习 15.1.1：**实现一个只允许键为 0 到 100 的整数 Map，并让它比通用 &lt;code&gt;HashMap&lt;/code&gt; 更直接。&lt;/p&gt;
&lt;h3&gt;发明 Trie&lt;/h3&gt;
&lt;p&gt;Trie 适用于键可以拆成字符、且多个键共享前缀的场景，例如字符串。&lt;/p&gt;
&lt;p&gt;假设集合中有：&lt;code&gt;sam&lt;/code&gt;、&lt;code&gt;sad&lt;/code&gt;、&lt;code&gt;sap&lt;/code&gt;、&lt;code&gt;same&lt;/code&gt;、&lt;code&gt;a&lt;/code&gt;、&lt;code&gt;awls&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b5a1ed27a954-Screen-Shot-2019-03-14-at-12.38.42-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Trie 的关键思想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个结点只代表一个字符。&lt;/li&gt;
&lt;li&gt;多个键可以共享结点。&lt;/li&gt;
&lt;li&gt;相同前缀只存储一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如 &lt;code&gt;sam&lt;/code&gt;、&lt;code&gt;sad&lt;/code&gt;、&lt;code&gt;sap&lt;/code&gt; 和 &lt;code&gt;same&lt;/code&gt; 都共享前缀 &lt;code&gt;sa&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/6d195c983bda-Screen-Shot-2019-03-14-at-12.47.38-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;查找一个键时，从根开始按照字符串的字符逐层向下走。&lt;/p&gt;
&lt;p&gt;仅仅存在一条字符路径还不够，因为某个键可能只是另一个键的前缀。每个结点因此还需要记录“到这里是否构成完整键”，图中用蓝色表示终止结点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;contains(&quot;sam&quot;)&lt;/code&gt;：&lt;code&gt;true&lt;/code&gt;，终点是已标记结点。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(&quot;sa&quot;)&lt;/code&gt;：&lt;code&gt;false&lt;/code&gt;，路径存在但终点未标记。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(&quot;a&quot;)&lt;/code&gt;：&lt;code&gt;true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains(&quot;saq&quot;)&lt;/code&gt;：&lt;code&gt;false&lt;/code&gt;，中途没有对应孩子。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**练习 15.1.2：**向图中的 Trie 加入 &lt;code&gt;ants&lt;/code&gt;、&lt;code&gt;zebra&lt;/code&gt;、&lt;code&gt;potato&lt;/code&gt; 和 &lt;code&gt;sadness&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;**练习 15.1.3：**Trie 用作 Map 与用作 Set 时有什么差别？提示：Map 的终止结点还需要保存 value。&lt;/p&gt;
&lt;p&gt;可查看 &lt;a href=&quot;http://www.cs.princeton.edu/courses/archive/spring15/cos226/demo/52DemoTrie.mov&quot;&gt;Trie Map 动画&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;给通用问题增加合理约束，常常可以设计出更高效或功能更强的结构。&lt;/li&gt;
&lt;li&gt;ADT 与实现不同：并查集是 ADT，Quick Find、Quick Union、WQU 和 WQUPC 是不同实现。&lt;/li&gt;
&lt;li&gt;Trie 是专门针对字符串键的 Set/Map 实现。&lt;/li&gt;
&lt;li&gt;每个结点对应字符，共同前缀由多个键共享。&lt;/li&gt;
&lt;li&gt;搜索失败有两种原因：路径中断，或终点没有被标记为完整键。&lt;/li&gt;
&lt;li&gt;Trie 来自 “retrieval tree”。通常读作 “try”。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;15.2 实现与性能&lt;/h2&gt;
&lt;h3&gt;基本实现&lt;/h3&gt;
&lt;p&gt;第一次实现中，每个结点保存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个字符；&lt;/li&gt;
&lt;li&gt;是否为完整键的终点；&lt;/li&gt;
&lt;li&gt;指向孩子的映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于键字符已知，可以用 &lt;code&gt;DataIndexedCharMap&lt;/code&gt; 表示孩子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TrieSet {
    private static final int R = 128; // ASCII
    private Node root;

    private static class Node {
        private char ch;
        private boolean isKey;
        private DataIndexedCharMap&amp;#x3C;Node&gt; next;

        private Node(char c, boolean isKey, int R) {
            ch = c;
            this.isKey = isKey;
            next = new DataIndexedCharMap&amp;#x3C;&gt;(R);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题是空间浪费严重。若某个结点只有一个孩子，它仍然拥有 128 个引用位置，其中 127 个都是 &lt;code&gt;null&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;还可以删掉结点自己的 &lt;code&gt;ch&lt;/code&gt; 字段。字符已经由它在父结点 &lt;code&gt;next&lt;/code&gt; 映射中的键决定：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TrieSet {
    private static final int R = 128;
    private Node root;

    private static class Node {
        private boolean isKey;
        private DataIndexedCharMap&amp;#x3C;Node&gt; next;

        private Node(boolean isKey, int R) {
            this.isKey = isKey;
            next = new DataIndexedCharMap&amp;#x3C;&gt;(R);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**练习 15.2.1：**设计一种更节省孩子引用空间的方案。&lt;/p&gt;
&lt;h3&gt;性能&lt;/h3&gt;
&lt;p&gt;若 Trie 中有 $N$ 个键，只以 $N$ 为参数看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：$\Theta(1)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains&lt;/code&gt;：$\Theta(1)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因是操作时间不依赖键的总数量，只依赖当前键本身的长度。更准确地，令键长度为 $L$：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;：$\Theta(L)$&lt;/li&gt;
&lt;li&gt;&lt;code&gt;contains&lt;/code&gt;：$O(L)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与哈希表不同，这种性能不依赖“分布均匀”假设，也没有扩容带来的摊还条件。不过数组孩子映射会消耗大量空间。&lt;/p&gt;
&lt;h3&gt;孩子映射的选择&lt;/h3&gt;
&lt;h4&gt;方案一：直接字符数组 &lt;code&gt;DataIndexedCharMap&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;每个结点固定拥有 $R$ 个引用。&lt;/li&gt;
&lt;li&gt;查找孩子为 $\Theta(1)$。&lt;/li&gt;
&lt;li&gt;字母表较大、分支较少时非常浪费空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方案二：BST&lt;/h4&gt;
&lt;p&gt;只为实际存在的 $C$ 个孩子创建结点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空间约为 $C$ 条孩子链接。&lt;/li&gt;
&lt;li&gt;查找孩子为 $O(\log R)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方案三：哈希表&lt;/h4&gt;
&lt;p&gt;同样只保存实际孩子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空间约为 $C$ 条孩子链接。&lt;/li&gt;
&lt;li&gt;在合理哈希假设下，查询近似常数时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;源材料表中的复杂度可以概括为：&lt;/p&gt;
&lt;p&gt;| 孩子映射实现 | 每结点空间 | 查询孩子 |
|---|---:|---:|
| &lt;code&gt;DataIndexedCharMap&lt;/code&gt; | $R$ 个引用 | $\Theta(1)$ |
| BST | $C$ 个引用及结点开销 | $O(\log R)$ |
| 哈希表 | $C$ 个引用及桶开销 | 平均 $O(1)$ |&lt;/p&gt;
&lt;p&gt;这里 $R$ 是固定字母表大小，因此即使 $O(\log R)$ 也可以视作与键数量无关的常数。不过 BST 和哈希表的每条链接有更高对象开销。&lt;/p&gt;
&lt;p&gt;这一选择再次体现了抽象屏障：Trie 的结点需要的是一个 Map ADT，而孩子 Map 的具体实现可以根据空间与速度需求替换。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;15.3 字符串操作&lt;/h2&gt;
&lt;p&gt;Trie 的插入和查询复杂度看起来非常好，但它在普通操作上未必一定比 BST 或哈希表更快。Trie 必须逐字符走完整个字符串，而 BST 或哈希表在程序层面可以直接处理整个字符串对象。&lt;/p&gt;
&lt;p&gt;Trie 真正突出的地方，是能高效支持特殊字符串操作。&lt;/p&gt;
&lt;h3&gt;前缀匹配&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;longestPrefixOf&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;给定一个查询字符串，从根开始逐字符向下走，并记录最近一次遇到的完整键终点。无法继续时，返回最后记录的最长键前缀。&lt;/p&gt;
&lt;h4&gt;收集所有键&lt;/h4&gt;
&lt;p&gt;可以用深度优先搜索收集 Trie 中的所有键：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;collect():
    创建空结果列表 x
    对 root.next.keys() 中的每个字符 c：
        调用 colHelp(String.valueOf(c), x, root.next.get(c))
    返回 x

colHelp(String s, List&amp;#x3C;String&gt; x, Node n):
    如果 n.isKey：
        x.add(s)
    对 n.next.keys() 中的每个字符 c：
        调用 colHelp(s + c, x, n.next.get(c))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归参数 &lt;code&gt;s&lt;/code&gt; 表示从根到当前结点形成的字符串。只有当前结点标记为完整键时才加入结果，然后继续遍历所有孩子。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;keysWithPrefix(prefix)&lt;/code&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;先沿 &lt;code&gt;prefix&lt;/code&gt; 找到对应终点结点 &lt;code&gt;alpha&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若路径不存在，返回空列表。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;alpha&lt;/code&gt; 出发调用与 &lt;code&gt;collect&lt;/code&gt; 相同的递归辅助方法，并把已有前缀带入。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;keysWithPrefix(prefix):
    找到 prefix 的终点结点 alpha
    若 alpha 不存在，返回空列表
    创建空列表 x
    若 alpha.isKey，把 prefix 加入 x
    对 alpha.next.keys() 中每个字符 c：
        colHelp(prefix + c, x, alpha.next.get(c))
    返回 x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**练习 15.3.1：**为 &lt;code&gt;longestPrefixOf&lt;/code&gt; 写出完整伪代码。&lt;/p&gt;
&lt;h3&gt;自动补全&lt;/h3&gt;
&lt;p&gt;搜索框输入文字时出现的建议，可以用 Trie 实现。建立一个从字符串到分数的 Map：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;value 表示查询的重要性，例如使用频率。&lt;/li&gt;
&lt;li&gt;大量字符串共享前缀结点，减少重复存储。&lt;/li&gt;
&lt;li&gt;用户输入前缀 &lt;code&gt;x&lt;/code&gt; 后，调用 &lt;code&gt;keysWithPrefix(x)&lt;/code&gt;，再返回分数最高的 10 个字符串。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;直接收集所有匹配键在短前缀下可能非常昂贵，因为结果可能有数百万条，而我们只需要 10 条。&lt;/p&gt;
&lt;p&gt;一种优化是：在每个 Trie 结点中额外记录其子树内的最大分数。搜索时优先探索潜在最高分的孩子，并结合优先队列逐步取出最有希望的候选分支。&lt;/p&gt;
&lt;p&gt;另一种优化是合并只有单一孩子的冗余路径，让一个结点保存一段字符串而不是单个字符。这会得到&lt;strong&gt;基数 Trie（radix trie）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;**练习 15.3.2：**思考为每个结点保存子树最佳分数会增加哪些维护成本，以及如何用优先队列设计 Top-K 自动补全算法。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;如果所有键都是字符串，就可以使用 Trie 实现 Map 或 Set：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理论操作复杂度按键长 $L$ 计为 $\Theta(L)$，与键数量 $N$ 无关。&lt;/li&gt;
&lt;li&gt;孩子可以由字符数组、BST 或哈希表保存。&lt;/li&gt;
&lt;li&gt;实际速度不一定总胜过哈希表，但 Trie 天然支持：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;longestPrefixOf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;keysWithPrefix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;自动补全&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;| 结构 | 键要求 | &lt;code&gt;get/contains&lt;/code&gt; | &lt;code&gt;add&lt;/code&gt; |
|---|---|---:|---:|
| 平衡 BST | 可比较 | $\Theta(\log N)$ | $\Theta(\log N)$ |
| 可扩容拉链哈希表 | 可哈希 | 平均 $\Theta(1)$，依赖均匀分布 | 摊还平均 $\Theta(1)$ |
| 直接索引数组 | 有限字符范围 | $\Theta(1)$ | $\Theta(1)$ |
| Trie | 字符串 | $\Theta(L)$ | $\Theta(L)$ |&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 16 章 四叉树与 K-D 树</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/16-quadtrees-and-kd-trees</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/16-quadtrees-and-kd-trees</guid><description>CS61B 空间数据结构中文教程，讲解四叉树、K-D 树、最近邻搜索与多维空间划分。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;16.1 均匀空间划分&lt;/h2&gt;
&lt;h3&gt;动机&lt;/h3&gt;
&lt;p&gt;假设二维空间中有许多 &lt;code&gt;Body&lt;/code&gt; 对象，例如图中的黄色太阳点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f89c889f647d-Screen-Shot-2019-03-15-at-10.46.02-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可能需要回答两类问题。&lt;/p&gt;
&lt;h4&gt;二维范围查询&lt;/h4&gt;
&lt;p&gt;**某个矩形区域内有多少对象？**例如图中绿色矩形覆盖了哪些太阳。&lt;/p&gt;
&lt;h4&gt;最近邻查询&lt;/h4&gt;
&lt;p&gt;**距离某个对象最近的另一个对象是谁？**例如，哪颗太阳距离太空马最近。&lt;/p&gt;
&lt;h3&gt;第一次尝试：哈希表&lt;/h3&gt;
&lt;p&gt;若所有太阳存放在普通哈希表中，桶位置与空间坐标没有有用关系。为了找最近邻，必须检查全部 $N$ 个对象，因此耗时 $\Theta(N)$。&lt;/p&gt;
&lt;p&gt;我们希望利用对象的空间位置，避免每次都扫描全集。&lt;/p&gt;
&lt;h3&gt;第二次尝试：均匀划分&lt;/h3&gt;
&lt;p&gt;哈希表的问题在于桶号近似随机。可以让桶号只由坐标决定：在整个图像空间上覆盖一个固定网格，例如 $4\times4$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f25a5f2e3d80-Screen-Shot-2019-03-15-at-11.11.28-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种方法也称为&lt;strong&gt;空间哈希（spatial hashing）&lt;/strong&gt;。对象提供 &lt;code&gt;getX()&lt;/code&gt; 与 &lt;code&gt;getY()&lt;/code&gt;，结构根据坐标计算它属于哪个网格桶，而不是调用普通 &lt;code&gt;hashCode()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;于是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;范围查询只检查与目标矩形相交的桶，例如图中的 5、6、9、10。&lt;/li&gt;
&lt;li&gt;最近邻查询先检查查询点所在桶，再逐圈扩展到相邻桶。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果对象均匀分布，$4\times4$ 网格平均能把候选数量减少到原来的约 $1/16$。实践中会明显更快，但：&lt;/p&gt;
&lt;p&gt;$$\frac{N}{16}=\Theta(N)$$&lt;/p&gt;
&lt;p&gt;渐近复杂度仍然是线性的。固定网格还会面临另一个问题：某些区域非常拥挤，另一些区域几乎为空。下一节将让空间划分能够根据数据密度自适应。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;16.2 四叉树&lt;/h2&gt;
&lt;h3&gt;第三次尝试：四叉树&lt;/h3&gt;
&lt;h4&gt;只按 X 或只按 Y 建树&lt;/h4&gt;
&lt;p&gt;搜索树相对于哈希表的重要优势，是显式维护元素顺序。例如在 BST 中找最小值只需 $\Theta(\log N)$，而哈希表需要 $\Theta(N)$。&lt;/p&gt;
&lt;p&gt;但二维对象很难定义唯一的“大小”。一个点可能在 X 方向更小、Y 方向却更大。例如图中 Mars 的 X 坐标小于 Earth，但 Y 坐标大于 Earth。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/3053d66cd8ff-Screen-Shot-2019-03-15-at-11.32.09-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若只按 X 坐标构建 BST，就得到 X 树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/5be2d553b81b-Screen-Shot-2019-03-15-at-11.40.56-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/203964fe8bc0-Screen-Shot-2019-03-15-at-11.41.02-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当查询要求 &lt;code&gt;x &amp;#x3C; -1&lt;/code&gt; 时，从根向左后，整个右子树都可以跳过。这相当于把搜索空间限制在某个矩形内。安全地跳过不可能包含答案的子树，称为&lt;strong&gt;剪枝（pruning）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但 X 树对 Y 条件无能为力；按 Y 查询可能仍需遍历所有结点。Y 树则正好相反。因此只优化一个维度，会让另一个维度的查询退化为 $\Theta(N)$。&lt;/p&gt;
&lt;h3&gt;四叉树&lt;/h3&gt;
&lt;p&gt;四叉树同时沿 X 和 Y 方向划分空间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/7568188b6621-Screen-Shot-2019-03-16-at-1.33.04-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4077347586a8-Screen-Shot-2019-03-16-at-1.33.08-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;每个结点把自己负责的区域分成四个象限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;西北（NW）&lt;/li&gt;
&lt;li&gt;东北（NE）&lt;/li&gt;
&lt;li&gt;东南（SE）&lt;/li&gt;
&lt;li&gt;西南（SW）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若点 B 位于点 A 的东北象限，插入时就沿 A 的 NE 孩子方向继续。与 BST 一样，插入顺序会影响四叉树的拓扑结构。&lt;/p&gt;
&lt;p&gt;四叉树本质上是一种分层的空间划分。与固定网格不同，点密集的区域会被继续细分，稀疏区域则保持较粗的划分，因此常能获得更好的实际性能。&lt;/p&gt;
&lt;h3&gt;使用四叉树进行范围查询&lt;/h3&gt;
&lt;p&gt;每个结点拥有四个空间子区域。给定查询矩形，可以判断它与哪些象限相交，只递归探索可能相交的子树，其余象限全部剪枝。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/d273ea71fd43-Screen-Shot-2019-03-16-at-1.45.55-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1b60f972b5fb-Screen-Shot-2019-03-16-at-1.46.01-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;例如绿色矩形只位于当前结点的东北象限，就无需访问 NW、SE 和 SW 子树。&lt;/p&gt;
&lt;p&gt;四叉树非常适合二维空间，因为二维恰好产生四个象限。但若维度继续升高，每层孩子数量会指数增长。下一节介绍更容易推广到高维的 K-D 树。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;16.3 K-D 树&lt;/h2&gt;
&lt;p&gt;K-D 树把分层空间划分推广到 $K$ 个维度。它不会在一层同时切分所有维度，而是逐层轮换当前比较维度。&lt;/p&gt;
&lt;p&gt;在二维情况下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 层按 X 坐标划分；&lt;/li&gt;
&lt;li&gt;第 2 层按 Y 坐标划分；&lt;/li&gt;
&lt;li&gt;第 3 层再次按 X；&lt;/li&gt;
&lt;li&gt;第 4 层再次按 Y；&lt;/li&gt;
&lt;li&gt;依此类推。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/9fb7cffb0af0-Screen-Shot-2019-03-16-at-5.33.01-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/88b638c488d8-Screen-Shot-2019-03-16-at-1.57.42-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;第一张图强调树的层次和每层使用的划分轴，第二张图展示对应的二维空间分区。执行算法时，应主要依据&lt;strong&gt;树结构&lt;/strong&gt;，因为只有树结点记录当前层使用哪个维度。&lt;/p&gt;
&lt;p&gt;三维 K-D 树每三层轮换 X、Y、Z；更高维同理。无论维度多高，每次划分都只有“小于”和“大于等于”两侧，因此 K-D 树始终是二叉树。&lt;/p&gt;
&lt;p&gt;插入时若某个坐标与分割值相等，需要固定一种一致的破平局规则，例如总是进入右子树。&lt;/p&gt;
&lt;p&gt;可通过这些&lt;a href=&quot;https://docs.google.com/presentation/d/1WW56RnFa3g6UJEquuIBymMcu9k2nqLrOE1ZlnTYFebg/edit#slide=id.g54b6045b73_0_38&quot;&gt;演示幻灯片&lt;/a&gt;查看插入过程。&lt;/p&gt;
&lt;h3&gt;使用 K-D 树寻找最近邻&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a089537fe5f9-Screen-Shot-2019-03-16-at-5.42.50-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;给定查询点，最近邻搜索的基本过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从根开始，把当前点记录为 &lt;code&gt;best&lt;/code&gt;，其距离作为当前待击败分数。&lt;/li&gt;
&lt;li&gt;根据当前结点的划分轴，优先进入查询点所在的一侧。&lt;/li&gt;
&lt;li&gt;在递归返回时，计算查询点到另一侧空间边界的最短可能距离。&lt;/li&gt;
&lt;li&gt;若这个最短距离已经不小于当前最佳距离，则另一侧不可能出现更优点，直接剪枝。&lt;/li&gt;
&lt;li&gt;否则，另一侧仍有可能包含更近的点，必须继续搜索。&lt;/li&gt;
&lt;li&gt;递归结束后，&lt;code&gt;best&lt;/code&gt; 就是最近邻。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2f90d96269e1-Screen-Shot-2019-03-16-at-5.44.54-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;紫色虚线表示查询点到待检查区域边界的最短距离。它是决定能否剪枝的关键下界。&lt;/p&gt;
&lt;p&gt;逐步演示见&lt;a href=&quot;https://docs.google.com/presentation/d/1DNunK22t-4OU_9c-OBgKkMAdly9aZQkWuv_tBkDg1G4/edit&quot;&gt;最近邻幻灯片&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;固定网格简单，但无法适应密度不均匀的数据。&lt;/li&gt;
&lt;li&gt;四叉树同时按 X、Y 划分，适合二维范围查询。&lt;/li&gt;
&lt;li&gt;K-D 树逐层轮换维度，容易扩展到任意维度。&lt;/li&gt;
&lt;li&gt;范围查询和最近邻搜索都依赖空间下界进行剪枝。&lt;/li&gt;
&lt;li&gt;实际性能高度依赖树是否平衡以及数据的空间分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 17 章 树遍历与图</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/17-tree-traversals-and-graphs</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/17-tree-traversals-and-graphs</guid><description>CS61B 树遍历与图中文教程，介绍深度优先遍历、广度优先遍历以及图模型的基本概念。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;17.1 树的回顾&lt;/h2&gt;
&lt;h3&gt;什么是树？&lt;/h3&gt;
&lt;p&gt;一棵树由以下部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一组结点（node），也称顶点（vertex）。&lt;/li&gt;
&lt;li&gt;一组连接结点的边（edge）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束：任意两个结点之间恰好只有一条路径。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/198a09a8dd8d-Screen-Shot-2019-04-01-at-12.51.49-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中最左边只有一个结点、没有边，它仍然是一棵合法树。第二、第三个结构也是树。第四个结构不是树，因为顶部到底部之间存在两条不同路径。&lt;/p&gt;
&lt;p&gt;**练习 17.1.1：**说明第五个结构为什么不是树，并尝试通过删除或修改边把无效结构变成树。&lt;/p&gt;
&lt;h3&gt;什么是有根树？&lt;/h3&gt;
&lt;p&gt;有根树是在树中指定一个根结点，通常画在最上方。指定根之后，就有了父子关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;除根外，每个结点恰好有一个父结点。&lt;/li&gt;
&lt;li&gt;一个结点可以有零个或多个孩子。&lt;/li&gt;
&lt;li&gt;没有孩子的结点称为&lt;strong&gt;叶子（leaf）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果一个结点有两个父结点，就会产生不止一条通向根的路径，因此不再是树。&lt;/p&gt;
&lt;h3&gt;树有什么用途？&lt;/h3&gt;
&lt;p&gt;前面已经接触过许多树结构：搜索树、Trie、堆、并查集中的树等。它们帮助我们实现快速搜索、前缀匹配、连通性判断和优先级操作。&lt;/p&gt;
&lt;p&gt;树也广泛存在于现实系统中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组织架构图：总裁是根，副总裁是孩子，部门继续向下展开。&lt;/li&gt;
&lt;li&gt;文件系统目录：目录包含子目录和文件，形成层次结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/cb2e94956e66-org-chart-software-maker.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/07c4e0e5621a-Screen-Shot-2019-04-01-at-12.55.39-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;**练习 17.1.2：**再列举一些常见树结构，并思考它们的结点、边以及可能的实现方式。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;17.2 树遍历&lt;/h2&gt;
&lt;p&gt;列表有一个很自然的遍历顺序：从头到尾。但树没有唯一的线性顺序，因此存在多种常见**遍历（traversal）**方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;层序遍历。&lt;/li&gt;
&lt;li&gt;深度优先遍历：前序、中序、后序。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下面的树为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/bc20f35ce1d6-Screen-Shot-2019-03-17-at-3.53.23-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;层序遍历&lt;/h3&gt;
&lt;p&gt;逐层、从左到右访问：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 0 层：D&lt;/li&gt;
&lt;li&gt;第 1 层：B、F&lt;/li&gt;
&lt;li&gt;第 2 层：A、C、E、G&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;D B F A C E G
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常使用队列实现：先把根入队；每次取出队首结点并访问，再把它的孩子依次入队。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void levelOrder(Node root) {
    if (root == null) return;
    Queue&amp;#x3C;Node&gt; fringe = new ArrayDeque&amp;#x3C;&gt;();
    fringe.add(root);
    while (!fringe.isEmpty()) {
        Node x = fringe.remove();
        print(x.key);
        if (x.left != null) fringe.add(x.left);
        if (x.right != null) fringe.add(x.right);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**练习 17.2.1：**自己实现层序遍历，并思考如何按层分别输出。&lt;/p&gt;
&lt;h3&gt;前序遍历&lt;/h3&gt;
&lt;p&gt;顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问当前结点。&lt;/li&gt;
&lt;li&gt;递归遍历左子树。&lt;/li&gt;
&lt;li&gt;递归遍历右子树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;D B A C F E G
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void preOrder(BSTNode x) {
    if (x == null) return;
    print(x.key);
    preOrder(x.left);
    preOrder(x.right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“前序”中的“前”，表示当前结点在两个子树&lt;strong&gt;之前&lt;/strong&gt;访问。&lt;/p&gt;
&lt;h3&gt;中序遍历&lt;/h3&gt;
&lt;p&gt;顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归遍历左子树。&lt;/li&gt;
&lt;li&gt;访问当前结点。&lt;/li&gt;
&lt;li&gt;递归遍历右子树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A B C D E F G
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void inOrder(BSTNode x) {
    if (x == null) return;
    inOrder(x.left);
    print(x.key);
    inOrder(x.right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于二叉搜索树，中序遍历会按键的升序输出所有元素，这是它的重要性质。&lt;/p&gt;
&lt;p&gt;也可以把结果递归地理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[左子树的结果] 当前结点 [右子树的结果]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后序遍历&lt;/h3&gt;
&lt;p&gt;顺序是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;递归遍历左子树。&lt;/li&gt;
&lt;li&gt;递归遍历右子树。&lt;/li&gt;
&lt;li&gt;访问当前结点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A C B E G F D
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void postOrder(BSTNode x) {
    if (x == null) return;
    postOrder(x.left);
    postOrder(x.right);
    print(x.key);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后序遍历先处理孩子，再处理父结点，因此适合删除整棵树、计算子树信息或自底向上汇总结果。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;17.3 图&lt;/h2&gt;
&lt;p&gt;树非常有用，但“任意两个结点之间只有一条路径”的限制并不适合所有问题。去掉这个限制，就得到更一般的结构：图。&lt;/p&gt;
&lt;h3&gt;什么是图？&lt;/h3&gt;
&lt;p&gt;图由以下部分组成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一组结点或顶点。&lt;/li&gt;
&lt;li&gt;零条或多条边，每条边连接两个顶点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外没有树那样的额外限制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c467b64c1798-Screen-Shot-2019-04-01-at-1.18.57-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;图中所有绿色结构都是合法图。其中第二个同时也是树，其他则不是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所有树都是图，但并非所有图都是树。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;简单图&lt;/h3&gt;
&lt;p&gt;本课程默认讨论&lt;strong&gt;简单图（simple graph）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任意一对顶点之间至多有一条同类型的边。&lt;/li&gt;
&lt;li&gt;不允许顶点连接自身的自环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/eb0107c28514-Screen-Shot-2019-03-17-at-4.19.24-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若两个顶点之间允许多条平行边，这种结构称为&lt;strong&gt;多重图（multigraph）&lt;/strong&gt;。允许自环的图有时也被归入多重图的广义定义。&lt;/p&gt;
&lt;h3&gt;有向图与无向图&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;**无向图：**边 &lt;code&gt;(u, v)&lt;/code&gt; 可以从 &lt;code&gt;u&lt;/code&gt; 走到 &lt;code&gt;v&lt;/code&gt;，也可以从 &lt;code&gt;v&lt;/code&gt; 走到 &lt;code&gt;u&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;**有向图：**边 &lt;code&gt;(u, v)&lt;/code&gt; 只表示从 &lt;code&gt;u&lt;/code&gt; 指向 &lt;code&gt;v&lt;/code&gt;。除非另有 &lt;code&gt;(v, u)&lt;/code&gt;，否则不能反向走。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有向边通常画成箭头。&lt;/p&gt;
&lt;h3&gt;有环与无环&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;**无环图：**不存在从某个顶点出发，沿一系列边最终回到自身的环。&lt;/li&gt;
&lt;li&gt;**有环图：**至少存在一个环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在有向图中，沿路径时必须遵循箭头方向。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ed28b82cc75f-Screen-Shot-2019-03-17-at-4.24.36-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;例如，从 &lt;code&gt;a&lt;/code&gt; 出发能够沿不同边回到 &lt;code&gt;a&lt;/code&gt;，说明图有环。若箭头方向阻止返回，则不是有向环。&lt;/p&gt;
&lt;h3&gt;其他常用概念&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/fd010ab51038-Screen-Shot-2019-03-17-at-4.26.41-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后续常见术语包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顶点的邻居（neighbor）或相邻顶点。&lt;/li&gt;
&lt;li&gt;路径（path）：首尾相接的一系列边。&lt;/li&gt;
&lt;li&gt;路径长度：通常按边数计算；加权图中可按边权之和计算。&lt;/li&gt;
&lt;li&gt;度数（degree）：无向图中与顶点相连的边数。&lt;/li&gt;
&lt;li&gt;入度与出度：有向图中进入和离开顶点的边数。&lt;/li&gt;
&lt;li&gt;连通分量：彼此之间存在路径的一组最大顶点集合。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;17.4 图问题&lt;/h2&gt;
&lt;p&gt;图上可以提出许多问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**s-t 路径：**顶点 &lt;code&gt;s&lt;/code&gt; 与 &lt;code&gt;t&lt;/code&gt; 之间是否存在路径？&lt;/li&gt;
&lt;li&gt;**连通性：**图是否连通，即任意两个顶点之间都有路径？&lt;/li&gt;
&lt;li&gt;**双连通性：**是否存在一个顶点，删除它后图会断开？&lt;/li&gt;
&lt;li&gt;**最短 s-t 路径：**从 &lt;code&gt;s&lt;/code&gt; 到 &lt;code&gt;t&lt;/code&gt; 的最短路径是什么？&lt;/li&gt;
&lt;li&gt;**环检测：**图中是否存在环？&lt;/li&gt;
&lt;li&gt;**欧拉回路：**是否存在恰好使用每条边一次的回路？&lt;/li&gt;
&lt;li&gt;**哈密顿回路：**是否存在恰好访问每个顶点一次的回路？&lt;/li&gt;
&lt;li&gt;**平面性：**能否在平面上画出图且边互不交叉？&lt;/li&gt;
&lt;li&gt;**图同构：**两个图是否只是顶点名字不同，本质结构相同？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图问题的难度很难只凭外观看出。欧拉回路可以在线性于边数的时间内解决；而哈密顿回路没有已知的多项式时间通用算法，是经典的 NP 完全问题之一。&lt;/p&gt;
&lt;h3&gt;从 s-t 连通性开始&lt;/h3&gt;
&lt;p&gt;先解决最基本的问题：给定源点 &lt;code&gt;s&lt;/code&gt; 与目标点 &lt;code&gt;t&lt;/code&gt;，是否存在路径？&lt;/p&gt;
&lt;p&gt;最直接的递归思路：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;if s == t:
    return true

for child in neighbors(s):
    if connected(child, t):
        return true

return false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**练习 17.4.1：**分析这段算法是否正确，会不会终止。&lt;/p&gt;
&lt;p&gt;它在有环图上会失败。例如 &lt;code&gt;connected(0, 7)&lt;/code&gt; 访问邻居 1，随后 &lt;code&gt;connected(1, 7)&lt;/code&gt; 又返回访问 0，形成无限递归。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/49daff26367d-Screen-Shot-2019-03-17-at-4.37.32-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;记录已经访问的顶点&lt;/h3&gt;
&lt;p&gt;解决办法是维护 &lt;code&gt;marked&lt;/code&gt; 集合：每个顶点第一次访问时做标记，之后跳过已标记邻居。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mark s
if s == t:
    return true

for child in unmarkedNeighbors(s):
    if connected(child, t):
        return true

return false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更接近 Java 的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;boolean connected(Graph G, int s, int t, boolean[] marked) {
    marked[s] = true;
    if (s == t) return true;

    for (int v : G.neighbors(s)) {
        if (!marked[v] &amp;#x26;&amp;#x26; connected(G, v, t, marked)) {
            return true;
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种算法能够正确终止，因为每个顶点最多被访问一次。&lt;/p&gt;
&lt;h3&gt;我们刚刚发明了什么？&lt;/h3&gt;
&lt;p&gt;这就是图上的&lt;strong&gt;深度优先遍历（DFS）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标记当前顶点。&lt;/li&gt;
&lt;li&gt;选择一个尚未访问的邻居并深入递归。&lt;/li&gt;
&lt;li&gt;把这条分支走到底后再回退，访问下一个邻居。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;它会先沿某条“家族血统”不断深入，再访问当前顶点的第二个邻居。&lt;/p&gt;
&lt;p&gt;DFS 可以用递归实现，也可以显式使用栈。下一章将介绍相反的策略：先访问所有邻居，再访问距离更远的顶点，即广度优先搜索。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 18 章 图遍历与表示</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/18-graph-traversal-and-representation</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/18-graph-traversal-and-representation</guid><description>CS61B 图遍历与表示中文教程，讲解邻接表、邻接矩阵、DFS、BFS 与图算法实现方式。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;18.1 广度优先搜索（BFS）&lt;/h2&gt;
&lt;p&gt;上一章开发了图的深度优先搜索（DFS）。DFS 会先沿第一个邻居不断深入，走完整条分支后才考虑第二个邻居。&lt;/p&gt;
&lt;p&gt;广度优先搜索（Breadth-First Search，BFS）也称层序遍历。它先访问源点的所有直接邻居，再访问距离源点两条边的顶点，随后是三条边，以此类推。&lt;/p&gt;
&lt;h3&gt;BFS 伪代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;创建 fringe：一个含起点的队列，并标记起点。
当 fringe 非空时重复：
    从队首取出顶点 v。
    对 v 的每个未标记邻居 n：
        标记 n。
        把 n 加入队尾。
        edgeTo[n] = v。
        distTo[n] = distTo[v] + 1。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fringe&lt;/code&gt; 表示遍历的&lt;strong&gt;边界集合&lt;/strong&gt;：已经发现、但还在等待处理的顶点。BFS 使用队列，因此先发现的顶点先处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;edgeTo[n] = v&lt;/code&gt; 记录第一次发现 &lt;code&gt;n&lt;/code&gt; 时是从 &lt;code&gt;v&lt;/code&gt; 走过来的。沿 &lt;code&gt;edgeTo&lt;/code&gt; 反向追踪，可以恢复从起点到 &lt;code&gt;n&lt;/code&gt; 的路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;distTo[n]&lt;/code&gt; 记录从起点到 &lt;code&gt;n&lt;/code&gt; 的边数。若每条边长度为 1，那么从 &lt;code&gt;v&lt;/code&gt; 到邻居 &lt;code&gt;n&lt;/code&gt; 只需再走一步，所以 &lt;code&gt;distTo[n] = distTo[v] + 1&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BFS 按距离递增顺序发现顶点，因此在&lt;strong&gt;无权图&lt;/strong&gt;中，第一次发现某个顶点时得到的就是最短路径。&lt;/p&gt;
&lt;p&gt;示例过程见&lt;a href=&quot;https://docs.google.com/presentation/d/1JoYCelH4YE6IkSMq_LfTJMzJ00WxDj7rEa49gYmAtc4/edit#slide=id.g76e0dad85_2_380&quot;&gt;课程幻灯片&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;DFS 与 BFS&lt;/h3&gt;
&lt;p&gt;**问题 18.1：**哪种图遍历使用栈作为 fringe？&lt;/p&gt;
&lt;p&gt;**答案：**DFS。&lt;/p&gt;
&lt;p&gt;不过，DFS 与 BFS 的差别不只在 fringe：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BFS 通常在&lt;strong&gt;加入队列时&lt;/strong&gt;立刻标记顶点，防止同一个顶点重复入队。&lt;/li&gt;
&lt;li&gt;某些迭代式 DFS 写法是在&lt;strong&gt;弹出栈时&lt;/strong&gt;才标记，因此同一顶点在正式访问前可能被多个邻居重复压栈。&lt;/li&gt;
&lt;li&gt;递归 DFS 借助调用栈自然实现“深入后回退”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;迭代式 DFS 的典型伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;创建空栈 fringe
把起点压栈
当栈非空时：
    弹出顶点 v
    如果 v 未标记：
        标记 v
        访问 v
        对 v 的每个邻居 n：
            若 n 未标记，把 n 压栈
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DFS 适合连通性、环检测、拓扑结构分析等问题；BFS 适合无权最短路径和按层扩展的问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;18.2 图的表示&lt;/h2&gt;
&lt;p&gt;图算法在代码中的难易程度和效率，受到两个设计选择影响：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对外提供怎样的 API。&lt;/li&gt;
&lt;li&gt;底层用什么数据结构表示图。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些选择会显著影响运行时间、内存使用和算法实现复杂度。&lt;/p&gt;
&lt;h3&gt;Graph API&lt;/h3&gt;
&lt;p&gt;API 是类向使用者提供的方法列表，包括方法签名以及行为说明。&lt;/p&gt;
&lt;p&gt;常见做法是给每个顶点分配唯一整数编号。若原始顶点使用字符串或其他对象，可以额外维护一个对象到整数编号的 Map。这样图的核心 API 无需泛型，可以统一使用整数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Graph {
    public Graph(int V);              // 创建含 V 个顶点的空图
    public void addEdge(int v, int w);// 添加边 v-w
    public Iterable&amp;#x3C;Integer&gt; adj(int v); // v 的邻接顶点
    public int V();                   // 顶点数
    public int E();                   // 边数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端只依赖这些方法即可实现 DFS、BFS、最短路径等算法。API 是否提供恰当操作，会直接决定算法写起来是否自然。&lt;/p&gt;
&lt;h3&gt;图的底层表示&lt;/h3&gt;
&lt;h4&gt;邻接矩阵&lt;/h4&gt;
&lt;p&gt;使用一个 $V\times V$ 的二维数组。若存在从 &lt;code&gt;s&lt;/code&gt; 到 &lt;code&gt;t&lt;/code&gt; 的边，则 &lt;code&gt;matrix[s][t]&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 或 1。&lt;/p&gt;
&lt;p&gt;无向图的邻接矩阵沿主对角线对称。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/b5f6e4230eb1-Screen-Shot-2019-03-27-at-1.58.11-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ec912e8b1b76-Screen-Shot-2019-03-27-at-2.03.44-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;判断某条边是否存在：$\Theta(1)$。&lt;/li&gt;
&lt;li&gt;枚举一个顶点的所有邻居：$\Theta(V)$。&lt;/li&gt;
&lt;li&gt;空间：$\Theta(V^2)$。&lt;/li&gt;
&lt;li&gt;适合边非常多的稠密图。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;边集合&lt;/h4&gt;
&lt;p&gt;维护一个包含全部边的集合，每条边保存两个端点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/15a3a3d73374-Screen-Shot-2019-03-27-at-2.03.36-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/8cb11147d723-Screen-Shot-2019-03-27-at-2.03.44-AM-2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这种表示便于遍历全部边，但若要快速找某个顶点的所有邻居，通常需要扫描大量边。&lt;/p&gt;
&lt;h4&gt;邻接表&lt;/h4&gt;
&lt;p&gt;维护一个长度为 $V$ 的数组，每个位置保存一个邻居列表。存在边 &lt;code&gt;s -&gt; t&lt;/code&gt; 当且仅当 &lt;code&gt;adj[s]&lt;/code&gt; 中包含 &lt;code&gt;t&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/ae33c78d893d-Screen-Shot-2019-03-27-at-2.05.55-AM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/a91c68e56540-Screen-Shot-2019-03-27-at-2.03.44-AM-1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空间：$\Theta(V+E)$。&lt;/li&gt;
&lt;li&gt;枚举 &lt;code&gt;v&lt;/code&gt; 的邻居：$\Theta(\deg(v))$。&lt;/li&gt;
&lt;li&gt;判断特定边是否存在，取决于邻居容器；普通列表最坏为 $O(\deg(v))$。&lt;/li&gt;
&lt;li&gt;对现实中常见的稀疏图非常合适，因此最常用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;遍历效率&lt;/h3&gt;
&lt;p&gt;若使用邻接表，DFS 和 BFS 会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个顶点最多处理一次；&lt;/li&gt;
&lt;li&gt;每条边最多检查常数次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此总时间为：&lt;/p&gt;
&lt;p&gt;$$O(V+E)$$&lt;/p&gt;
&lt;p&gt;若使用邻接矩阵，为每个访问到的顶点都要扫描整行 $V$ 个位置，总时间为：&lt;/p&gt;
&lt;p&gt;$$O(V^2)$$&lt;/p&gt;
&lt;p&gt;课程&lt;a href=&quot;https://docs.google.com/presentation/d/143WntPl7CG5Po3utVK0jYSA0Jd6XKppT5h1juWEWhUU/edit#slide=id.g54593997ea_0_422&quot;&gt;表示法效率表&lt;/a&gt;总结了不同操作的权衡。不要只背结论，应理解每个复杂度来自实际访问了多少数组位置、邻居或边。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 19 章 最短路径</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/19-shortest-paths</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/19-shortest-paths</guid><description>CS61B 最短路径中文教程，介绍最短路径树、Dijkstra 算法、A* 搜索及边权条件。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;19.1 回顾&lt;/h2&gt;
&lt;p&gt;目前我们已经会：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从给定源点 $s$ 出发，找到通往所有可达顶点的路径。&lt;/li&gt;
&lt;li&gt;从 $s$ 出发，找到通往所有可达顶点的“最短”路径——但这里的“最短”需要进一步澄清。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DFS 与 BFS&lt;/h3&gt;
&lt;p&gt;DFS 和 BFS 都能判断可达性并构建路径树，但结果不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DFS 找到的是某条路径，不保证最短。&lt;/li&gt;
&lt;li&gt;BFS 在无权图中会找到边数最少的路径。&lt;/li&gt;
&lt;li&gt;使用邻接表时，两者时间复杂度都是 $O(V+E)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;空间方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DFS 对非常细长的图可能产生很深的递归栈，例如一条含 10,000 个顶点的链。&lt;/li&gt;
&lt;li&gt;BFS 对分支极多的图可能让队列同时保存大量顶点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;BFS 的“最短”是什么意思？&lt;/h3&gt;
&lt;p&gt;BFS 找到的是&lt;strong&gt;边数最少&lt;/strong&gt;的路径。若所有边的代价都相同，这正是我们想要的最短路径。&lt;/p&gt;
&lt;p&gt;但许多图的边带有权重：道路有长度或通行时间，网络链路有延迟，航班有价格。此时边数较少的路径不一定权重之和更小。&lt;/p&gt;
&lt;p&gt;例如，一条直接边权重为 10，而经过三条边的路线总权重只有 6。BFS 会偏爱一条边的路线，但真正的加权最短路径是三条边的路线。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/e75a693ac9b0-Screen-Shot-2019-03-23-at-7.25.41-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;下一节将研究非负权图上的单源最短路径算法：Dijkstra 算法。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;19.2 Dijkstra 算法&lt;/h2&gt;
&lt;h3&gt;先手工求解&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/80c9ecf733a4-Screen-Shot-2019-03-23-at-7.27.24-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;尝试完成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找一条从顶点 0 到顶点 5 的路径。&lt;/li&gt;
&lt;li&gt;找出以 0 为源点的最短路径树，即从 0 到每个顶点的最短路径。&lt;/li&gt;
&lt;li&gt;设计一个自动完成这件事的算法。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;答案图在本节末尾。&lt;/p&gt;
&lt;h3&gt;观察&lt;/h3&gt;
&lt;p&gt;加权图中的最短路径可能包含很多条边。我们要最小化的是路径上所有边权之和，而不是边数。&lt;/p&gt;
&lt;p&gt;从源点 $s$ 出发的**最短路径树（Shortest Path Tree）**可以这样理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对每个顶点 $v\ne s$，找到一条从 $s$ 到 $v$ 的最短路径。&lt;/li&gt;
&lt;li&gt;把这些路径使用的边合并起来。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若每个可达顶点在 &lt;code&gt;edgeTo&lt;/code&gt; 中只有一个前驱，则得到的结构有 $V-1$ 条父边，不含环，因此是一棵树。&lt;/p&gt;
&lt;h3&gt;Dijkstra 算法&lt;/h3&gt;
&lt;p&gt;Dijkstra 算法输入源点 $s$，输出从 $s$ 到所有可达顶点的最短路径树。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建最小优先队列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;distTo[s] = 0&lt;/code&gt;，其余顶点距离设为无穷大。&lt;/li&gt;
&lt;li&gt;把顶点按当前 &lt;code&gt;distTo&lt;/code&gt; 作为优先级加入队列。&lt;/li&gt;
&lt;li&gt;不断取出当前距离最小的未确定顶点 &lt;code&gt;v&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;v&lt;/code&gt; 的所有出边执行&lt;strong&gt;松弛（relaxation）&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;什么是松弛？&lt;/h4&gt;
&lt;p&gt;考虑边 $(v,w)$，其权重为 &lt;code&gt;weight(v, w)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当前已知从源点到 &lt;code&gt;w&lt;/code&gt; 的最好距离是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;distTo[w]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果改为先到 &lt;code&gt;v&lt;/code&gt;，再沿边 $(v,w)$ 到 &lt;code&gt;w&lt;/code&gt;，候选距离是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;distTo[v] + weight(v, w)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若候选更小，就更新：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;distTo[w] = distTo[v] + weight(v, w);
edgeTo[w] = v;
priorityQueue.changePriority(w, distTo[w]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个“计算候选、比较并可能更新”的过程就是松弛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/c6fd5e6fefe7-pz1muo.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;伪代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;dijkstra(source):
    distTo[source] = 0
    其余 distTo = infinity
    把所有顶点加入最小优先队列，优先级为 distTo

    while PQ 非空:
        v = PQ.removeSmallest()
        标记 v 的最短距离已经确定
        对 v 的每条出边 (v, w):
            relax(v, w)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;relax(v, w):
    如果 w 已经被确定：
        return

    candidate = distTo[v] + weight(v, w)
    如果 candidate &amp;#x3C; distTo[w]:
        distTo[w] = candidate
        edgeTo[w] = v
        PQ.changePriority(w, candidate)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现也可以不预先把所有顶点加入队列，而是在距离第一次变为有限值时加入，并允许旧条目留在队列中；弹出过期条目时跳过即可。&lt;/p&gt;
&lt;h3&gt;正确性条件&lt;/h3&gt;
&lt;p&gt;只要所有边权都&lt;strong&gt;非负&lt;/strong&gt;，Dijkstra 算法保证正确。&lt;/p&gt;
&lt;p&gt;直觉如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源点距离 0 显然最优。&lt;/li&gt;
&lt;li&gt;优先队列每次弹出当前 &lt;code&gt;distTo&lt;/code&gt; 最小的顶点 &lt;code&gt;v&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;假设还存在一条未发现的更短路径到 &lt;code&gt;v&lt;/code&gt;。这条路径必须先经过某个尚未弹出的顶点 &lt;code&gt;u&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;但 &lt;code&gt;u&lt;/code&gt; 当前距离不小于 &lt;code&gt;v&lt;/code&gt;，而后续边权又非负，因此经 &lt;code&gt;u&lt;/code&gt; 到 &lt;code&gt;v&lt;/code&gt; 不可能变得更短。&lt;/li&gt;
&lt;li&gt;所以 &lt;code&gt;v&lt;/code&gt; 一旦弹出，其距离就已是最终最短距离。&lt;/li&gt;
&lt;li&gt;对每次弹出重复同样论证，就得到完整正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这可以形式化为归纳证明。&lt;/p&gt;
&lt;h3&gt;为什么负边会破坏算法？&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/e95323938d1d-Screen-Shot-2019-03-23-at-8.10.37-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若存在负边，一个已经弹出并“确定”的顶点，之后可能通过负边获得更短路径。例如到顶点 33 的距离先被确定为 82，后来从距离 101 的顶点 34 经过权重 -67 的边，可得到 34。但普通 Dijkstra 不再更新已确定顶点，于是返回错误答案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dijkstra 对含负权边的图不保证正确。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个有趣的特殊情况：若所有负边都只从源点发出，源点第一次松弛后它们就已经被处理，之后剩余边非负，因此算法仍可正常工作。&lt;/p&gt;
&lt;h3&gt;一个重要不变量&lt;/h3&gt;
&lt;p&gt;顶点一旦从优先队列弹出，其 &lt;code&gt;distTo&lt;/code&gt; 就是最终最短距离，不会再改变。&lt;/p&gt;
&lt;p&gt;因此，如果只需要从源点到某个目标点的最短路径，而不是整棵最短路径树，那么目标点一旦弹出就可以立即停止。这称为&lt;strong&gt;提前终止（short-circuiting）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;手工题答案&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/70eff54b76df-Screen-Shot-2019-03-23-at-7.29.30-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/2dc4e3b68ab3-Screen-Shot-2019-03-23-at-7.30.04-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;19.3 A* 算法&lt;/h2&gt;
&lt;p&gt;Dijkstra 可以在目标点弹出时提前停止，但它仍可能探索大量与目标方向无关的顶点。&lt;/p&gt;
&lt;p&gt;可以把 Dijkstra 想成以源点为圆心不断扩大同心圆：先确定距离源点 1 单位的顶点，再确定 2 单位、3 单位……&lt;/p&gt;
&lt;p&gt;若从 Denver 寻找 New York，算法会同时向所有方向扩展。它可能先扫过 Las Vegas、Los Angeles、Dallas 等大量城市，尽管目标明显在东部。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/647a13eb5350-Screen-Shot-2019-03-23-at-8.29.56-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们希望利用“目标大致在哪个方向”的先验知识，让搜索更偏向目标。&lt;/p&gt;
&lt;h3&gt;A* 的核心思想&lt;/h3&gt;
&lt;p&gt;Dijkstra 的优先级是：&lt;/p&gt;
&lt;p&gt;$$g(v)=\text{从源点到 }v\text{ 的当前最好已知距离}$$&lt;/p&gt;
&lt;p&gt;A* 再加入一个启发式估计：&lt;/p&gt;
&lt;p&gt;$$h(v)=\text{从 }v\text{ 到目标的估计距离}$$&lt;/p&gt;
&lt;p&gt;优先队列使用：&lt;/p&gt;
&lt;p&gt;$$f(v)=g(v)+h(v)$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;g(v)&lt;/code&gt; 来自实际已走路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;h(v)&lt;/code&gt; 是对剩余代价的预测。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f(v)&lt;/code&gt; 是经过 &lt;code&gt;v&lt;/code&gt; 到达目标的估计总代价。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，A* 是把 Dijkstra 的真实已知距离与目标导向的启发信息结合起来。&lt;/p&gt;
&lt;p&gt;演示见&lt;a href=&quot;https://docs.google.com/presentation/d/177bRUTdCa60fjExdr9eO04NHm0MRfPtCzvEup1iMccM/edit#slide=id.g771336078_0_180&quot;&gt;A* 课程幻灯片&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;启发式从哪里来？&lt;/h3&gt;
&lt;p&gt;我们正是为了求真实最短距离才运行 A*，因此不可能事先知道准确的剩余距离。&lt;code&gt;h&lt;/code&gt; 必须是一个容易计算的近似。&lt;/p&gt;
&lt;p&gt;在地图问题中，可使用两地 GPS 坐标的直线距离。道路不一定能沿直线走，因此这通常不是实际道路距离，但它能提供方向和合理下界。&lt;/p&gt;
&lt;p&gt;其他例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网格地图使用曼哈顿距离或欧几里得距离。&lt;/li&gt;
&lt;li&gt;拼图问题使用错位方块数或每块到目标位置的距离和。&lt;/li&gt;
&lt;li&gt;游戏寻路使用几何距离。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;糟糕的启发式会怎样？&lt;/h3&gt;
&lt;p&gt;假设真正最短路径必须经过城市 $C$，但错误启发式把到 $C$ 的估计设为无穷大。A* 几乎永远不会选择这条路线，最终可能返回错误答案。&lt;/p&gt;
&lt;p&gt;为保证最优性，启发式通常需要两个性质。&lt;/p&gt;
&lt;h4&gt;可采纳性（admissibility）&lt;/h4&gt;
&lt;p&gt;启发式不能高估真实剩余距离：&lt;/p&gt;
&lt;p&gt;$$h(v)\le d(v,\text{target})$$&lt;/p&gt;
&lt;p&gt;也就是说，它必须是一个乐观下界。&lt;/p&gt;
&lt;h4&gt;一致性（consistency）&lt;/h4&gt;
&lt;p&gt;对每条边 $(v,w)$：&lt;/p&gt;
&lt;p&gt;$$h(v)\le \operatorname{weight}(v,w)+h(w)$$&lt;/p&gt;
&lt;p&gt;这类似三角不等式。一致性保证沿路径前进时，估计总成本不会出现不合理下降，也使顶点弹出后无需重新打开。&lt;/p&gt;
&lt;p&gt;一致启发式一定可采纳（在目标点 &lt;code&gt;h(target)=0&lt;/code&gt; 的常见设定下）。&lt;/p&gt;
&lt;h3&gt;与 Dijkstra 的关系&lt;/h3&gt;
&lt;p&gt;若令所有顶点的启发式都为 0：&lt;/p&gt;
&lt;p&gt;$$h(v)=0$$&lt;/p&gt;
&lt;p&gt;则 A* 的优先级退化为 &lt;code&gt;g(v)&lt;/code&gt;，它就是 Dijkstra 算法。&lt;/p&gt;
&lt;p&gt;启发式越准确，A* 往往探索越少的顶点；但计算启发式本身也有成本。实际设计需要在“估计质量”与“计算开销”之间权衡。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 20 章 最小生成树</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/20-minimum-spanning-trees</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/20-minimum-spanning-trees</guid><description>CS61B 最小生成树中文教程，讲解割性质、Prim 算法、Kruskal 算法与并查集的应用。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;20.1 最小生成树与割性质&lt;/h2&gt;
&lt;p&gt;**最小生成树（Minimum Spanning Tree，MST）**是连通加权无向图中一组总权重最小的边，它必须：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接图中的全部顶点，因此是“生成”的；&lt;/li&gt;
&lt;li&gt;保持连通；&lt;/li&gt;
&lt;li&gt;不含环，因此是一棵树；&lt;/li&gt;
&lt;li&gt;对 $V$ 个顶点恰好包含 $V-1$ 条边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本章将学习 Prim 和 Kruskal 两种寻找 MST 的算法。理解它们之前，先介绍证明安全边的核心工具：&lt;strong&gt;割性质（Cut Property）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;割&lt;/h3&gt;
&lt;p&gt;一个**割（cut）**把图的全部顶点分到两个非空集合中。&lt;/p&gt;
&lt;p&gt;一条边若两个端点分别属于割的两侧，就称为这条割的&lt;strong&gt;跨越边（crossing edge）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;割性质&lt;/h3&gt;
&lt;p&gt;对任意一个割，权重最小的跨越边都可以属于某棵最小生成树。若所有边权互不相同，则这条最轻跨越边必然属于唯一的 MST。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/04b021699e48-Screen-Shot-2019-04-14-at-8.57.22-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;交换论证&lt;/h3&gt;
&lt;p&gt;用反证法证明。设某个割的最轻跨越边为 $e$，并假设一棵 MST $T$ 不含 $e$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;向 $T$ 加入 $e$。由于树中任意两点已有唯一路径，加入一条边必然形成一个环。&lt;/li&gt;
&lt;li&gt;$e$ 从割的一侧跨到另一侧。环要回到起点，环中必然还存在另一条跨越边 $f$。&lt;/li&gt;
&lt;li&gt;删除 $f$，环被打断，图仍然连通并含 $V-1$ 条边，因此仍是一棵生成树。&lt;/li&gt;
&lt;li&gt;因为 $e$ 是最轻跨越边，$w(e)\le w(f)$，替换后总权重不会增加。&lt;/li&gt;
&lt;li&gt;若 $w(e)&amp;#x3C;w(f)$，新树甚至更轻，与 $T$ 是 MST 矛盾；若相等，也说明存在一棵包含 $e$ 的 MST。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，选择最轻跨越边是安全的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/f0f75b1613fa-Screen-Shot-2019-04-14-at-9.03.06-PM.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Prim 和 Kruskal 看起来不同，但它们每一步本质上都在构造一个割，并选择该割上的安全最轻边。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;20.2 Prim 与 Kruskal 算法&lt;/h2&gt;
&lt;h3&gt;Prim 算法&lt;/h3&gt;
&lt;p&gt;Prim 算法从任意起点开始，让一棵树逐步向外生长：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任选一个起始顶点加入树。&lt;/li&gt;
&lt;li&gt;在“一端位于当前树内、另一端位于树外”的所有边中，选择权重最小者。&lt;/li&gt;
&lt;li&gt;把该边及其外侧顶点加入树。&lt;/li&gt;
&lt;li&gt;重复，直到选出 $V-1$ 条边。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确性来自割性质：当前已在树中的顶点形成一个集合，其余顶点形成另一个集合。Prim 每一步都选择跨越这个割的最轻边，因此选择是安全的。&lt;/p&gt;
&lt;h4&gt;与 Dijkstra 的关系&lt;/h4&gt;
&lt;p&gt;Prim 与 Dijkstra 的控制结构非常相似，都使用优先队列维护尚未确定的顶点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dijkstra 的优先级：源点到该顶点的当前最短路径距离。&lt;/li&gt;
&lt;li&gt;Prim 的优先级：该顶点连接到当前生成树的最轻边权。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prim 维护：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;distTo[v]&lt;/code&gt;：当前树连接到 &lt;code&gt;v&lt;/code&gt; 的最轻边权。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgeTo[v]&lt;/code&gt;：实现这个最轻连接的边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用邻接表和二叉堆优先队列时，复杂度通常写作：&lt;/p&gt;
&lt;p&gt;$$O((V+E)\log V)$$&lt;/p&gt;
&lt;p&gt;对于连通图，$E\ge V-1$，常简化为 $O(E\log V)$。&lt;/p&gt;
&lt;h3&gt;Kruskal 算法&lt;/h3&gt;
&lt;p&gt;Kruskal 不从某个顶点扩展，而是让许多小连通分量逐步合并：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按权重从小到大排序全部边。&lt;/li&gt;
&lt;li&gt;依次考察每条边 $(u,v)$。&lt;/li&gt;
&lt;li&gt;若 &lt;code&gt;u&lt;/code&gt; 与 &lt;code&gt;v&lt;/code&gt; 当前不连通，加入这条边并合并两个分量。&lt;/li&gt;
&lt;li&gt;若已经连通，加入会形成环，因此跳过。&lt;/li&gt;
&lt;li&gt;选满 $V-1$ 条边后结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Kruskal 也依赖割性质。当前不同连通分量之间存在一个割；按权重顺序遇到的、能够连接两个不同分量的边，是相关割上的最轻安全边。&lt;/p&gt;
&lt;p&gt;Prim 与 Kruskal 可能得到不同的边集合，但总权重都最小。若 MST 唯一，两者结果相同。&lt;/p&gt;
&lt;h4&gt;用并查集检测环&lt;/h4&gt;
&lt;p&gt;维护加权 Quick Union + 路径压缩：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;for edge (u, v) in edges sorted by weight:
    if !isConnected(u, v):
        add edge to MST
        connect(u, v)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;isConnected&lt;/code&gt; 判断加入边是否会在当前森林中形成环，&lt;code&gt;connect&lt;/code&gt; 合并两个分量。&lt;/p&gt;
&lt;h4&gt;复杂度&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;排序全部边：$O(E\log E)$。&lt;/li&gt;
&lt;li&gt;每条边进行常数次并查集操作：总计近似 $O(E\alpha(V))$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此瓶颈通常是排序：&lt;/p&gt;
&lt;p&gt;$$O(E\log E)$$&lt;/p&gt;
&lt;p&gt;由于简单图中 $E\le V^2$，有 $\log E=O(\log V)$，也常写作 $O(E\log V)$。&lt;/p&gt;
&lt;p&gt;若边已经排序，则剩余工作接近线性：&lt;/p&gt;
&lt;p&gt;$$O(E\alpha(V))$$&lt;/p&gt;
&lt;p&gt;其中 $\alpha$ 是反 Ackermann 函数，在现实输入规模下可视作小于 5 的常数。&lt;/p&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Prim 更像“从一个点长出一棵树”，适合邻接表和稠密局部探索。&lt;/li&gt;
&lt;li&gt;Kruskal 更像“从轻边开始合并森林”，实现直观，天然结合并查集。&lt;/li&gt;
&lt;li&gt;两者都通过割性质保证正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>第 21 章 归约与分解</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/21-reductions-and-decomposition</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/chapters/21-reductions-and-decomposition</guid><description>CS61B 归约与分解中文教程，介绍问题归约、图算法应用、拓扑排序与复杂问题的分解方法。</description><content:encoded>&lt;hr&gt;
&lt;h2&gt;21.1 拓扑排序与 DAG&lt;/h2&gt;
&lt;p&gt;到目前为止，我们学习了编程实践、IDE、数据结构设计、渐近分析、各种 ADT 的实现，以及图算法。这些知识的重要价值之一，是许多现实问题都可以被建模成我们已经会解决的标准结构。&lt;/p&gt;
&lt;p&gt;本章将练习如何把复杂问题转化为已有算法能够处理的形式。&lt;/p&gt;
&lt;h3&gt;拓扑排序&lt;/h3&gt;
&lt;p&gt;假设有一组任务，其中某些任务必须在另一些任务之前完成。如何排列全部任务，使所有先决条件都得到满足？&lt;/p&gt;
&lt;p&gt;把每个任务表示为顶点；若任务 $v$ 必须先于任务 $w$，就添加有向边：&lt;/p&gt;
&lt;p&gt;$$v\rightarrow w$$&lt;/p&gt;
&lt;p&gt;问题于是转化为&lt;strong&gt;拓扑排序（topological sort）&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;拓扑排序是顶点的一种线性顺序，使每条有向边 $u\rightarrow v$ 的起点 $u$ 都出现在终点 $v$ 之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/87e175929b6c-21.1.1.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;**问题 21.1.1：**给出图中的合法拓扑序。&lt;/p&gt;
&lt;p&gt;示例答案包括：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[D, B, A, E, C, F]
[E, D, C, B, A, F]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拓扑序通常不唯一。&lt;/p&gt;
&lt;h3&gt;哪些图可以拓扑排序？&lt;/h3&gt;
&lt;p&gt;考虑含有有向环的图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/5d40c6910142-21.1.3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;若 D 必须在 B 前，B 又必须经过一系列依赖回到 D 前，就不可能安排任何线性顺序。无向边也可以看作双向依赖，会立即形成长度为 2 的有向环。&lt;/p&gt;
&lt;p&gt;因此拓扑排序只对**有向无环图（Directed Acyclic Graph，DAG）**定义。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/50c4dee6e3e2-21.1.4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;拓扑排序是 DAG 顶点的一种排列，使每条边 $u\rightarrow v$ 都满足 $u$ 在 $v$ 之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把顶点按拓扑序排成一行后，所有边都会从左指向右，因此拓扑排序也称图的&lt;strong&gt;线性化（linearization）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/e714bc7c090d-21.1.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;入度为 0 的顶点称为&lt;strong&gt;源点（source）&lt;/strong&gt;，它们可以作为拓扑序开头。&lt;/li&gt;
&lt;li&gt;出度为 0 的顶点称为&lt;strong&gt;汇点（sink）&lt;/strong&gt;，它们可以位于末尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基于 DFS 的算法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;依次从每个尚未标记的顶点启动 DFS；不同启动之间不要清空标记。&lt;/li&gt;
&lt;li&gt;在 DFS 中，先递归访问全部邻居，再把当前顶点加入后序列表。&lt;/li&gt;
&lt;li&gt;反转后序列表，得到拓扑序。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;topological(DAG):
    marked = 新的标记数组
    postOrder = 空列表

    for v in all vertices:
        if !marked[v]:
            dfs(v, marked, postOrder)

    return reverse(postOrder)


dfs(v, marked, postOrder):
    marked[v] = true
    for w in neighbors(v):
        if !marked[w]:
            dfs(w, marked, postOrder)
    postOrder.add(v)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;为什么正确？&lt;/h4&gt;
&lt;p&gt;顶点 &lt;code&gt;v&lt;/code&gt; 只有在所有可达后代都处理完之后，才进入后序列表。因此在后序中，后代位于 &lt;code&gt;v&lt;/code&gt; 前面；反转后，&lt;code&gt;v&lt;/code&gt; 就位于所有后代之前，满足每条有向边的先后约束。&lt;/p&gt;
&lt;p&gt;运行时间与 DFS 相同：&lt;/p&gt;
&lt;p&gt;$$O(V+E)$$&lt;/p&gt;
&lt;p&gt;实际实现还应使用递归栈状态检测环；若发现指向当前递归路径中顶点的边，则输入不是 DAG。&lt;/p&gt;
&lt;h3&gt;基于入度的算法：Kahn 算法&lt;/h3&gt;
&lt;p&gt;也可以使用 BFS 风格的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;计算每个顶点的入度。&lt;/li&gt;
&lt;li&gt;把所有入度为 0 的顶点加入队列。&lt;/li&gt;
&lt;li&gt;取出一个顶点加入拓扑序，并“删除”它的所有出边。&lt;/li&gt;
&lt;li&gt;每删除一条边，就把目标顶点入度减 1；若降为 0，则入队。&lt;/li&gt;
&lt;li&gt;重复直到队列为空。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若最终输出顶点数少于 $V$，说明图中存在环。&lt;/p&gt;
&lt;p&gt;若希望在多个合法顶点中始终选编号最小者，可使用最小优先队列代替普通队列。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;拓扑排序把 DAG 线性化。&lt;/li&gt;
&lt;li&gt;合法拓扑序可能不唯一。&lt;/li&gt;
&lt;li&gt;反向 DFS 后序和 Kahn 入度算法都能在 $O(V+E)$ 时间完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;21.2 DAG 上的最短路径&lt;/h2&gt;
&lt;p&gt;DAG 是有向无环图。它当然可以使用 Dijkstra 求最短路径，但 DAG 的结构允许一个更简单、更快的算法，而且能够正确处理负权边。&lt;/p&gt;
&lt;h3&gt;Dijkstra 为什么会被负权边破坏？&lt;/h3&gt;
&lt;p&gt;Dijkstra 依赖一个关键结论：顶点从优先队列弹出后，其最短距离已经确定。负权边可能在之后把一个已确定顶点的距离进一步降低，因此该结论失效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/39addbcc3cea-21.2.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从 A 出发时，Dijkstra 可能先确定 C，再确定 B，从而错过通过负边 &lt;code&gt;B -&gt; C&lt;/code&gt; 得到的更短路线。&lt;/p&gt;
&lt;p&gt;负边不代表 Dijkstra 每次都会失败：某些图上它碰巧仍能给出正确结果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/fdc8937dafd7-21.2.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;DAG 最短路径算法&lt;/h3&gt;
&lt;p&gt;核心方法只有两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求出 DAG 的一个拓扑序。&lt;/li&gt;
&lt;li&gt;按拓扑序访问顶点，并松弛该顶点的所有出边。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;松弛边 $u\rightarrow v$、权重为 $w$：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;if distTo[u] + w &amp;#x3C; distTo[v]:
    distTo[v] = distTo[u] + w
    edgeTo[v] = u
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;distTo[source] = 0
其他 distTo = infinity
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么负边也没问题？&lt;/h3&gt;
&lt;p&gt;拓扑序保证每条边都从较早顶点指向较晚顶点。当轮到顶点 &lt;code&gt;v&lt;/code&gt; 时，所有可能进入 &lt;code&gt;v&lt;/code&gt; 的前驱都已经处理并完成松弛，因此关于 &lt;code&gt;v&lt;/code&gt; 的全部信息都已到齐。&lt;/p&gt;
&lt;p&gt;图中没有环，所以之后不会出现一条从“未来顶点”绕回来继续改善 &lt;code&gt;v&lt;/code&gt; 的路径。边权可以为负，也不会破坏这个顺序。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;拓扑排序：$O(V+E)$。&lt;/li&gt;
&lt;li&gt;每个顶点访问一次、每条边松弛一次：$O(V+E)$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总复杂度：&lt;/p&gt;
&lt;p&gt;$$O(V+E)$$&lt;/p&gt;
&lt;p&gt;这比使用二叉堆的 Dijkstra：&lt;/p&gt;
&lt;p&gt;$$O((V+E)\log V)$$&lt;/p&gt;
&lt;p&gt;更快。&lt;/p&gt;
&lt;p&gt;若图不是 DAG 且可能含负边，可使用 Bellman–Ford 算法；它还能检测从源点可达的负权环，但不属于本课程重点。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;21.3 最长路径&lt;/h2&gt;
&lt;h3&gt;一般图中的最长简单路径&lt;/h3&gt;
&lt;p&gt;目标是从一个起点出发，找到到其他顶点的最长&lt;strong&gt;简单路径&lt;/strong&gt;，即路径中不能重复顶点。&lt;/p&gt;
&lt;p&gt;在一般图中，这个问题非常困难；已知通用精确算法在最坏情况下需要指数时间。&lt;/p&gt;
&lt;p&gt;不能简单地把所有边权取负后运行普通最短路径算法，因为一般图可能出现负环。若允许绕环，就可以无限次降低取负后的路径代价，对应原图中无限增大路径权重；而“简单路径”限制又使问题变成组合搜索。&lt;/p&gt;
&lt;h3&gt;DAG 上的最长路径&lt;/h3&gt;
&lt;p&gt;DAG 没有环，因此问题容易得多。可以把已有 DAG 最短路径算法当作黑盒：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造图 $G&apos;$，把原图每条边权取相反数。&lt;/li&gt;
&lt;li&gt;在 $G&apos;$ 上运行 DAG 最短路径算法。&lt;/li&gt;
&lt;li&gt;将得到的 &lt;code&gt;distTo&lt;/code&gt; 全部再取相反数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;edgeTo&lt;/code&gt; 已经给出原图最长路径，不需要改变。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;更直接的实现是仍按拓扑序处理顶点，但把松弛条件改为选择更大的距离：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;if distTo[u] + w &gt; distTo[v]:
    distTo[v] = distTo[u] + w
    edgeTo[v] = u
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不可达顶点初始值应设为负无穷大，源点设为 0。&lt;/p&gt;
&lt;p&gt;复杂度仍为：&lt;/p&gt;
&lt;p&gt;$$O(V+E)$$&lt;/p&gt;
&lt;p&gt;把“最长路径”转换为“取负权后的最短路径”，展示了一种重要的问题求解方法：使用已有算法作为黑盒解决新问题。这称为&lt;strong&gt;归约（reduction）&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;21.4 归约与分解&lt;/h2&gt;
&lt;p&gt;上一节为解决 DAG 最长路径，我们先构造一个边权取反的新图 $G&apos;$，把它交给 DAG 最短路径算法，再解释输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1b174adee1a0-21.4.2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个过程称为&lt;strong&gt;归约（reduction）&lt;/strong&gt;。因为 DAG 最短路径算法能够被用来解决 DAG 最长路径问题，所以说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DAG 最长路径归约到 DAG 最短路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;归约的正式含义&lt;/h3&gt;
&lt;p&gt;若任务 Q 的任意正确子程序都可以被用来解决任务 P，就说 &lt;strong&gt;P 归约到 Q&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通常包含三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**预处理：**把 P 的输入 $x$ 转换成 Q 的输入 $y$。&lt;/li&gt;
&lt;li&gt;**黑盒调用：**运行 Q 的算法得到结果。&lt;/li&gt;
&lt;li&gt;**后处理：**把 Q 的输出转换成 P 的答案。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/4346882d3320-21.4.1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;一个问题可能归约到多种不同问题。现实类比中，“爬上山”可以归约为乘缆车、骑车或其他能够把人送上山的任务。&lt;/p&gt;
&lt;h3&gt;示例：3SAT 归约到独立集&lt;/h3&gt;
&lt;p&gt;下面展示两个表面上不同的问题之间的联系。&lt;/p&gt;
&lt;h4&gt;独立集问题&lt;/h4&gt;
&lt;p&gt;图的**独立集（independent set）**是一组顶点，其中任意两个都不相邻。&lt;/p&gt;
&lt;p&gt;独立集判定问题：给定图和整数 $k$，是否存在大小至少为 $k$ 的独立集？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/1e460fbf33eb-1920px-Cube-maximal-independence.svg.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;3SAT 问题&lt;/h4&gt;
&lt;p&gt;3SAT 输入由若干子句组成，每个子句是三个文字（变量或变量的否定）的 OR，全部子句再用 AND 连接。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;(x1 || x2 || !x3)
&amp;#x26;&amp;#x26; (x1 || !x1 || x1)
&amp;#x26;&amp;#x26; (x2 || x3 || x4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题是：是否存在一组布尔变量赋值，使所有子句都为真？&lt;/p&gt;
&lt;p&gt;术语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文字（literal）：&lt;/strong&gt;&lt;code&gt;x1&lt;/code&gt; 或 &lt;code&gt;!x1&lt;/code&gt; 这样的变量/否定变量。&lt;/li&gt;
&lt;li&gt;**子句（clause）：**三个文字的析取（OR）。&lt;/li&gt;
&lt;li&gt;整个公式要求所有子句同时满足（AND）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;构造归约&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;主张：3SAT 归约到独立集。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 3SAT 公式有 $m$ 个子句。&lt;/p&gt;
&lt;h4&gt;1. 预处理为图&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;对每个子句创建三个顶点，分别代表三个文字。&lt;/li&gt;
&lt;li&gt;同一子句的三个顶点两两相连，形成三角形。&lt;/li&gt;
&lt;li&gt;若两个顶点表示互相矛盾的文字，例如 &lt;code&gt;x&lt;/code&gt; 与 &lt;code&gt;!x&lt;/code&gt;，在它们之间添加边。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/05b2aaebed7b-21.4.3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;2. 调用独立集算法&lt;/h4&gt;
&lt;p&gt;询问构造出的图是否存在大小为 $m$ 的独立集。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/245d3ada8d51-21.4.4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;3. 后处理&lt;/h4&gt;
&lt;p&gt;若找到大小为 $m$ 的独立集，把选中的文字设为真，并给其余变量补充任意一致赋值，就能得到原 3SAT 公式的满足解。&lt;/p&gt;
&lt;h3&gt;为什么这个构造正确？&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每个子句内部是三角形，独立集最多从其中选择一个顶点。&lt;/li&gt;
&lt;li&gt;独立集总大小要求为 $m$，而一共有 $m$ 个子句，因此必须恰好从每个子句选一个文字。&lt;/li&gt;
&lt;li&gt;互相否定的文字之间有边，所以独立集不能同时选择 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;!x&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;因此，选中的 $m$ 个顶点正好对应一组彼此一致、每个子句至少一个为真的文字。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反过来，若公式存在满足赋值，就可从每个子句挑一个为真的文字。由于赋值一致，不会同时挑到某变量及其否定；这些顶点构成大小为 $m$ 的独立集。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/images/44a23044e35c-21.4.5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3SAT 可满足  &amp;#x3C;=&gt;  构造图存在大小为子句数的独立集
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;归约不一定涉及图；它是一种普遍的问题转换思想。&lt;/p&gt;
&lt;h3&gt;归约与复杂度&lt;/h3&gt;
&lt;p&gt;归约不仅能复用算法，也能比较问题难度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若 P 能高效归约到 Q，且 Q 有高效算法，那么 P 也有高效算法。&lt;/li&gt;
&lt;li&gt;若一个已知困难问题 P 能高效归约到 Q，那么 Q 至少和 P 一样困难；否则 Q 的快速算法也会解决 P。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是 NP 完全性理论的核心语言。&lt;/p&gt;
&lt;h3&gt;分解&lt;/h3&gt;
&lt;p&gt;整门课程中我们也一直在做类似的事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List 的实现分解为数组或链表操作。&lt;/li&gt;
&lt;li&gt;渗流问题可借助并查集维护连通性。&lt;/li&gt;
&lt;li&gt;优先队列分解为堆及其上浮、下沉操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些不一定都是严格意义上的“归约”，因为往往不是只把另一个完整算法当作黑盒。更合适的词是&lt;strong&gt;分解（decomposition）&lt;/strong&gt;：把复杂任务拆成较小、边界清晰的部分，并用抽象隐藏底层细节。&lt;/p&gt;
&lt;p&gt;归约、分解与抽象共同构成计算机科学解决问题的核心方式。&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;原作：Josh Hug，UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版，仅供非商业学习；采用 CC BY-NC-SA 4.0 许可。
原始网站：https://joshhug.gitbooks.io/hug61b/content/&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title>Lab 1：IntelliJ、Java 与 Git</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-1-intellij-java-git</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-1-intellij-java-git</guid><description>CS61B Spring 2021 Lab 1：IntelliJ、Java 与 Git中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%BC%80%E5%A7%8B%E4%B9%8B%E5%89%8D&quot;&gt;开始之前&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#github-%E4%B8%8E-beacon&quot;&gt;GitHub 与 Beacon&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%85%B3%E4%BA%8E%E4%BD%A0%E7%9A%84%E4%BB%93%E5%BA%93%E7%9A%84%E6%9B%B4%E5%A4%9A%E7%BB%86%E8%8A%82&quot;&gt;关于你的仓库的更多细节&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-%E8%8E%B7%E5%8F%96%E8%B5%B7%E5%A7%8B%E6%96%87%E4%BB%B6&quot;&gt;A. 获取起始文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#b-%E5%9C%A8-intellij-%E4%B8%AD%E8%BF%90%E8%A1%8C%E4%BB%A3%E7%A0%81&quot;&gt;B. 在 IntelliJ 中运行代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c-%E9%85%8D%E7%BD%AE-snaps&quot;&gt;C. 配置 Snaps&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%8E%B7%E5%8F%96-snaps-%E4%BB%93%E5%BA%93&quot;&gt;获取 Snaps 仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AE%BE%E7%BD%AE%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;设置环境变量&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%89%E8%A3%85-snaps-%E6%8F%92%E4%BB%B6&quot;&gt;安装 Snaps 插件&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#mac-os-%E5%92%8C-linux&quot;&gt;Mac OS 和 Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#windows&quot;&gt;Windows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B5%8B%E8%AF%95%E4%BD%A0%E7%9A%84%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;测试你的环境变量&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#d-%E7%BC%96%E7%A8%8B%E7%BB%83%E4%B9%A0&quot;&gt;D. 编程练习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#e-%E5%B0%86%E4%BD%A0%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%8E%A8%E9%80%81%E5%88%B0-github&quot;&gt;E. 将你的工作推送到 GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#f-%E6%8F%90%E4%BA%A4-lab-1&quot;&gt;F. 提交 Lab 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#g-%E9%AA%8C%E8%AF%81-snaps-%E5%AE%89%E8%A3%85&quot;&gt;G. 验证 Snaps 安装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%9B%9E%E9%A1%BE&quot;&gt;回顾&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#josh-hug-%E7%9A%84%E9%85%8D%E8%89%B2%E6%96%B9%E6%A1%88%E5%8F%AF%E9%80%89&quot;&gt;Josh Hug 的配色方案（可选）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;开始之前&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在开始本实验之前，请先完成 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/lab1setup&quot;&gt;lab1setup&lt;/a&gt;，安装 61B 所需的软件。如果你在任何地方卡住，都可以在 Ed 上发帖、参加实验课，或来 Office Hours。&lt;/li&gt;
&lt;li&gt;请注意，第一周有大量配置步骤。不要灰心；如果卡住了，一定要寻求帮助！&lt;/li&gt;
&lt;li&gt;警告：本实验有一点长。如果你没有在实验课时段内完成，这是正常的，尤其是在你遇到棘手的配置问题、需要大量帮助时。&lt;/li&gt;
&lt;li&gt;你在完成本实验（或其他任何实验）时，可以与其他学生交谈，但最终所有输入、编程和命令操作都应当由你自己完成。本实验包含大量重要的配置信息，你需要独立于其他人亲自完成这些配置。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;GitHub 与 Beacon&lt;/h2&gt;
&lt;p&gt;CS61B 不使用 bCourses，而是使用一个名为 Beacon 的内部系统来集中管理你的成绩和学生信息。&lt;/p&gt;
&lt;p&gt;在本节中，我们会配置你的 Beacon 账户以及你的 CS 61B GitHub 仓库（“repo”）。提交所有编程作业时，你都会用到这个仓库。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;a href=&quot;https://github.com/&quot;&gt;GitHub.com&lt;/a&gt; 创建账户。如果你已经有账户，则不需要新建账户。&lt;/li&gt;
&lt;li&gt;前往 &lt;a href=&quot;https://sp21.beacon.datastructur.es/&quot;&gt;Beacon 网站&lt;/a&gt;，系统会引导你完成几个步骤，以完成 GitHub 仓库注册。请认真按照这些步骤操作！要完成 Google Form 测验，你必须登录自己的 Berkeley 账户。如果在执行这些步骤时出现任何错误，请立即告诉你的 TA。&lt;/li&gt;
&lt;li&gt;完成全部步骤后，你应该会收到一封电子邮件，邀请你协作使用课程 GitHub 仓库。接受两封邮件中的邀请，之后就可以了。这封邮件会发送到你创建 GitHub 账户时使用的邮箱，该邮箱不一定是你的 Berkeley 邮箱。太好了！不要按照 GitHub 所说的“你可能想执行”的说明操作——请改为按照本实验后面给出的说明操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;关于你的仓库的更多细节&lt;/h3&gt;
&lt;p&gt;你的仓库名称会包含一个对你而言唯一的编号！例如，如果你的仓库名是 &lt;code&gt;sp21-s42&lt;/code&gt;，那么登录 GitHub 后，你可以通过 https://github.com/Berkeley-CS61B-Student/sp21-s42 访问自己的私有仓库。如果你的仓库编号不是 &lt;code&gt;42&lt;/code&gt;，这个链接对你就不起作用。把 &lt;code&gt;42&lt;/code&gt; 替换成你自己的编号，即可在 GitHub 上查看自己的仓库。&lt;/p&gt;
&lt;p&gt;此外，讲师、TA 和 Tutor 也能查看你的仓库。这意味着，你在 Ed 上私下询问调试问题时，可以（而且应该）链接到自己的代码。其他学生都无法查看你的仓库。再次提醒，即使课程结束后，你也不得公开发布本课程的代码。这样做违反课程政策，你可能会受到纪律处分。&lt;/p&gt;
&lt;p&gt;如果你与实验伙伴合作，你还会收到一个单独的共享仓库，实验中应使用该仓库。更多细节请参阅 Course Info 页面中的 Partnerships Guide。&lt;/p&gt;
&lt;p&gt;此外，在 Beacon 中注册后，你还会收到另一个形如 &lt;code&gt;snaps-sp21-sXXX&lt;/code&gt; 的仓库的协作邀请——现在不用担心这个仓库的用途，只需接受邀请，确保自己能够访问它即可。本实验后面的章节会提供更多关于这个仓库的信息！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;A. 获取起始文件&lt;/h2&gt;
&lt;p&gt;对于本课程中的大多数作业（包括本实验），我们会提供一组起始文件。&lt;/p&gt;
&lt;p&gt;为了获取起始文件，我们将使用 Git 版本控制系统。你会在本实验后面以及整个课程中进一步学习 Git。现在，我们先简单地使用 Git 获取本实验的文件。&lt;/p&gt;
&lt;p&gt;如果你需要有关创建目录、创建文件、更改目录等操作的帮助，请参阅 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/lab1setup&quot;&gt;lab1setup&lt;/a&gt; 的 C 节。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先，告诉 Git 你的姓名和电子邮箱地址，以便 Git 能够正确地把提交归属给你。运行以下命令：&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;Your Name&lt;/code&gt; 替换成你的姓名，把 &lt;code&gt;you@berkeley.edu&lt;/code&gt; 替换成你的电子邮箱地址。你应该使用注册 GitHub 时使用的邮箱。如果它不是你的 &lt;code&gt;@berkeley.edu&lt;/code&gt; 邮箱，也没有关系。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git config --global user.email &quot;you@berkeley.edu&quot;
$ git config --global user.name &quot;Your Name&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;克隆你在 &lt;a href=&quot;https://github.com/Berkeley-CS61B-Student&quot;&gt;Berkeley-CS61B-Student 组织&lt;/a&gt;中的仓库。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在电脑的文件夹中，导航到你想要开始放置仓库的位置。在下面的示例中，我假设你希望把所有内容放在名为 &lt;code&gt;cs61b&lt;/code&gt; 的文件夹中，但你也可以选择其他名称。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd cs61b
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入以下命令克隆你的 GitHub 仓库。一定要把 &lt;code&gt;s**&lt;/code&gt; 替换成你注册仓库时获得的课程 ID。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git clone https://github.com/Berkeley-CS61B-Student/sp21-s**.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想使用 SSH 而不是 HTTPS（并&lt;a href=&quot;https://help.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh&quot;&gt;设置自己的 SSH 密钥&lt;/a&gt;），也完全可以。如果你不知道这些是什么意思，直接使用上面的命令即可。SSH 的优点是，你每次使用仓库时都不必输入 GitHub 密码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入你新创建的仓库！（一定要完成这一步，否则下面剩余步骤无法正常工作。）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cd sp21-s**
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加 &lt;code&gt;skeleton&lt;/code&gt; 远程仓库。你将从这个远程仓库“拉取”代码，以获得作业的起始代码。继续执行这些命令前，请确保你位于新创建的仓库文件夹中。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;输入以下命令添加 &lt;code&gt;skeleton&lt;/code&gt; 远程仓库。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git remote add skeleton https://github.com/Berkeley-CS61B/skeleton-sp21.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现在列出远程仓库时，应当同时看到 &lt;code&gt;origin&lt;/code&gt; 和 &lt;code&gt;skeleton&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git remote -v
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果你收到 &lt;code&gt;Not a git repository&lt;/code&gt; 错误，请确保自己位于 &lt;code&gt;sp21-s**&lt;/code&gt; 目录中。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现在，你必须从 &lt;code&gt;skeleton&lt;/code&gt; 远程仓库“拉取”，以获得 Lab 1 的起始代码。每当发布新的项目和作业时，你也会执行这一操作。为此，请使用整个 Git 工具箱中最吓人的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git pull skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会抓取名为 &lt;code&gt;skeleton&lt;/code&gt; 的仓库中的所有远程文件（该仓库位于 &lt;code&gt;https://github.com/Berkeley-CS61B/skeleton-sp21.git&lt;/code&gt;），并把它们复制到当前文件夹中。&lt;/p&gt;
&lt;p&gt;如果你收到类似 &lt;code&gt;fatal: refusing to merge unrelated histories&lt;/code&gt; 的错误，那么你可能在创建仓库时运行了 GitHub 建议的命令。要修复这一问题，本次可以改为运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git pull --rebase --allow-unrelated-histories skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只在这一次这样做。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果你列出当前目录中的文件，会看到现在有一个名为 &lt;code&gt;lab1&lt;/code&gt; 的文件夹。查看 &lt;code&gt;lab1&lt;/code&gt; 文件夹，你会看到 &lt;code&gt;HelloWorld.java&lt;/code&gt;、&lt;code&gt;HelloNumbers.java&lt;/code&gt;、&lt;code&gt;Collatz.java&lt;/code&gt; 以及其他文件；你会在本实验后面的部分中使用它们。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;B. 在 IntelliJ 中运行代码&lt;/h2&gt;
&lt;p&gt;IntelliJ 是一个 IDE（集成开发环境，Integrated Development Environment）。它包含文本编辑器，以及大量能够让编写代码变得更容易的额外功能。让我们先在 &lt;code&gt;lab1&lt;/code&gt; 文件夹上试用它。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开 IntelliJ 后，点击 &lt;strong&gt;Open&lt;/strong&gt; 选项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/e66e7290cf4f-intellij_start_menu.png&quot; alt=&quot;IntelliJ Start Menu&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找到并选择你的 &lt;code&gt;lab1&lt;/code&gt; 目录，然后按下 &lt;strong&gt;OK&lt;/strong&gt; 按钮。按下完成后，你应该会看到与下图非常相似的内容。你可能需要点击左上角 &lt;code&gt;lab1&lt;/code&gt; 旁边的小三角形，才能显示源文件（&lt;code&gt;HelloWorld&lt;/code&gt;、&lt;code&gt;HelloNumbers&lt;/code&gt;、&lt;code&gt;Collatz&lt;/code&gt;、&lt;code&gt;GetEnvironmentVariables&lt;/code&gt; 和 &lt;code&gt;CheckLabConfig&lt;/code&gt;）。如果你没有看到侧边栏，请前往 &lt;strong&gt;View → Tool Windows → Project&lt;/strong&gt;，或选择左侧工具栏中的 &lt;strong&gt;Project&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/ae0a56ec69ff-main_screen_when_done_lab1.png&quot; alt=&quot;IntelliJ with lab 1 open&quot;&gt;&lt;/p&gt;
&lt;p&gt;注意：第一次启动 IntelliJ 时，它可能需要一些时间来索引文件。这可能会花费几分钟。右下角应该有一个小进度条。在索引完成前，某些步骤可能无法工作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双击 &lt;code&gt;HelloNumbers&lt;/code&gt;，你会看到 &lt;code&gt;HelloNumbers&lt;/code&gt; 代码出现。你的配色方案不会与下图相同（下图使用的是 Josh Hug 官方批准的配色方案，名为 Sunburst）。如果你觉得自己想使用 Sunburst，请参阅本实验最后的可选配置。此处不应当出现任何错误（代码不应以红色高亮）。如果看到错误，请在 Ed 上发帖或询问实验课 TA。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/7f5a4ca20f39-main_screen_when_done_lab1_HelloNumbers_open.png&quot; alt=&quot;IntelliJ with HelloNumbers open&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来，让我们运行 &lt;code&gt;HelloNumbers&lt;/code&gt;。为此，请点击 &lt;strong&gt;Run&lt;/strong&gt;，然后点击 &lt;strong&gt;Run…&lt;/strong&gt; 选项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/1026cb90dd7d-main_screen_when_done_lab1_HelloNumbers_run_run.png&quot; alt=&quot;Preparing to run HelloNumbers&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这会显示两个选项，一个叫作 &lt;strong&gt;Edit Configuration&lt;/strong&gt;，另一个叫作 &lt;strong&gt;HelloNumbers&lt;/strong&gt;。你可能还会看到本实验后面将要使用的文件对应的其他选项。IntelliJ 支持各种复杂的 Java 程序运行配置选项，但默认配置已经满足我们的需要，因此直接点击写着 &lt;strong&gt;HelloNumbers&lt;/strong&gt; 的选项即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/0ddf7321a5c6-main_screen_when_done_lab1_HelloNumbers_run_run_after_click.png&quot; alt=&quot;Picking a run option&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;看看程序生成的输出！你应该会看到一些漂亮的数字被打印出来！Hello Numbers！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;C. 配置 Snaps&lt;/h2&gt;
&lt;p&gt;你有两个课程仓库：一个标准仓库（例如 &lt;code&gt;sp21-s33&lt;/code&gt;），以及一个 Snaps 仓库（&lt;code&gt;snaps-sp21-s33&lt;/code&gt;）。标准仓库中的提交需要手动创建；Snaps 仓库中的提交会自动创建。&lt;/p&gt;
&lt;p&gt;如果你忘记使用 Git 手动创建提交，Snaps 仓库会作为你的工作内容的安全备份。每完成一个项目后，你都会推送 Snaps 仓库；这使我们能够发布统计数据，例如学生完成每个项目（或项目中每个部分）所花费的平均时间。它还让我们能够找出项目中比预期更令人困惑或更耗时的部分。&lt;/p&gt;
&lt;p&gt;它还会让我们了解所布置的工作量是否过高；如果我们给你的负担过重，就可以调低课程难度。如果你强烈反对推送自己的 Snaps 仓库，请届时联系你的 Mentor GSI。课程第二周会提供更多关于 Mentor GSI 的信息。&lt;/p&gt;
&lt;p&gt;我们不希望你感觉自己一直被监视！因此，我们不会人工分析 Snaps 仓库，也不会使用 Snaps 仓库中的任何信息进行抄袭检测。也就是说，所有分析都会通过不知道你身份的批量分析工具完成，并且 Snaps 仓库中的任何内容都不会以任何方式被用于对你不利的事情。&lt;/p&gt;
&lt;p&gt;在学期结束时，你还将有机会选择加入一个研究项目。届时会提供详细信息。&lt;/p&gt;
&lt;p&gt;在本实验的这一节中，你会配置自己的电脑，使其能够创建 Snaps 备份。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;获取 Snaps 仓库&lt;/h3&gt;
&lt;p&gt;你应该已经收到并接受了一个 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库的协作邀请。如果你尚未接受，请不要继续，直到你接受为止。如果在电子邮箱收件箱中找不到邀请（请记住，它发送到与你的 GitHub 账户关联的邮箱，该邮箱不一定是你的 Berkeley 邮箱），请在 Ed 上发帖，或来实验课或 Office Hours，以便我们帮助你。&lt;/p&gt;
&lt;p&gt;首先，我们会把 Snaps 仓库克隆到你的电脑。在终端中（Windows 上使用 Git Bash），导航到主目录：&lt;code&gt;cd ~&lt;/code&gt;。然后运行以下命令，把 &lt;code&gt;***&lt;/code&gt; 替换成你的仓库编号：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/Berkeley-CS61B-Student/snaps-sp21-s***
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果运行 &lt;code&gt;ls&lt;/code&gt;，现在应该能够看到 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：绝对不要在这个文件夹中完成作业。如果你从 Snaps 文件夹提交代码，Gradescope 测试一定会失败。你只应当操作自己的 &lt;code&gt;sp21-s***&lt;/code&gt; 文件夹。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;设置环境变量&lt;/h3&gt;
&lt;p&gt;接下来，你需要设置一些环境变量。具体步骤取决于你的操作系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1/lab1SnapsSetupWindows.html&quot;&gt;Windows 说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1/lab1SnapsSetupMacAndLinux.html&quot;&gt;macOS 和 Linux 说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;安装 Snaps 插件&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;Mac OS 和 Linux&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;按照上面的说明设置环境变量后，重新启动 IntelliJ 和所有终端窗口（关闭它们，然后重新打开），使变量生效。&lt;/li&gt;
&lt;li&gt;前往 IntelliJ 欢迎窗口（你可能必须点击 &lt;strong&gt;File → Close Project&lt;/strong&gt; 才能回到欢迎窗口）。&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;Plugins&lt;/strong&gt;（左侧面板）→ &lt;strong&gt;Marketplace&lt;/strong&gt;（顶部中间）→ 搜索 &lt;code&gt;CS 61B Snaps&lt;/code&gt;，然后安装。&lt;/li&gt;
&lt;li&gt;IntelliJ 可能会要求你重启 IDE（会出现绿色的 &lt;strong&gt;Restart IDEA&lt;/strong&gt; 按钮）。如果出现，请重启。&lt;/li&gt;
&lt;li&gt;不要忽略下一步。&lt;/li&gt;
&lt;li&gt;退出 IntelliJ（无论它是否已经在第 4 步重启）。没错，即使你可能刚刚重启它，也要再次完全退出。&lt;/li&gt;
&lt;li&gt;打开终端窗口，通过运行 &lt;code&gt;idea&lt;/code&gt; 启动 IntelliJ。不要通过操作系统 GUI（例如 Mac OS 的 Finder）运行 IntelliJ。&lt;/li&gt;
&lt;li&gt;继续下一节，测试你的环境变量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;Windows&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;按照上面的说明设置环境变量后，重新启动 IntelliJ 和所有终端窗口（关闭它们，然后重新打开），使变量生效。&lt;/li&gt;
&lt;li&gt;前往 IntelliJ 欢迎窗口（你可能必须点击 &lt;strong&gt;File → Close Project&lt;/strong&gt; 才能回到欢迎窗口）。&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;Plugins&lt;/strong&gt;（左侧面板）→ &lt;strong&gt;Marketplace&lt;/strong&gt;（顶部中间）→ 搜索 &lt;code&gt;CS 61B Snaps&lt;/code&gt;，然后安装。&lt;/li&gt;
&lt;li&gt;IntelliJ 可能会要求你重启 IDE（会出现绿色的 &lt;strong&gt;Restart IDEA&lt;/strong&gt; 按钮）。如果出现，请重启。&lt;/li&gt;
&lt;li&gt;不要忽略下一步。&lt;/li&gt;
&lt;li&gt;再次重启 IntelliJ。没错，即使你可能刚刚重启它，也要完全退出，然后重新打开。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;测试你的环境变量&lt;/h4&gt;
&lt;p&gt;重新打开 IntelliJ 后，请确保 IntelliJ 中打开的是 &lt;code&gt;lab1&lt;/code&gt; 文件夹。打开 &lt;code&gt;CheckLabConfig&lt;/code&gt;，验证环境变量是否设置正确。然后点击 &lt;strong&gt;Run&lt;/strong&gt;，向下选择写着 &lt;strong&gt;Run…&lt;/strong&gt; 的选项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/3177f6d443ae-windows_about_to_run_checklabconfig.png&quot; alt=&quot;about to run check lab config&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在点击包含 &lt;code&gt;CheckLabConfig&lt;/code&gt; 的选项：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/148c74f05d2d-windows_check_lab_config_option.png&quot; alt=&quot;the check lab config option&quot;&gt;&lt;/p&gt;
&lt;p&gt;这应该会运行 &lt;code&gt;CheckLabConfig&lt;/code&gt; 类的 &lt;code&gt;main&lt;/code&gt; 方法。如果一切正常，你会看到一条消息，确认你已经完成 Lab 1 的配置！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;D. 编程练习&lt;/h2&gt;
&lt;p&gt;打开名为 &lt;code&gt;Collatz.java&lt;/code&gt; 的文件。尝试运行它，你会看到数字 &lt;code&gt;5&lt;/code&gt; 被打印出来。&lt;/p&gt;
&lt;p&gt;这个程序应当从给定数字开始，打印 &lt;a href=&quot;https://en.wikipedia.org/wiki/Collatz_conjecture&quot;&gt;Collatz 序列&lt;/a&gt;。Collatz 序列的定义如下：&lt;/p&gt;
&lt;p&gt;如果 &lt;code&gt;n&lt;/code&gt; 是偶数，下一个数字是 &lt;code&gt;n / 2&lt;/code&gt;。如果 &lt;code&gt;n&lt;/code&gt; 是奇数，下一个数字是 &lt;code&gt;3n + 1&lt;/code&gt;。如果 &lt;code&gt;n&lt;/code&gt; 是 &lt;code&gt;1&lt;/code&gt;，序列结束。&lt;/p&gt;
&lt;p&gt;例如，假设从 &lt;code&gt;5&lt;/code&gt; 开始。因为 &lt;code&gt;5&lt;/code&gt; 是奇数，下一个数字是 &lt;code&gt;3 × 5 + 1 = 16&lt;/code&gt;。因为 &lt;code&gt;16&lt;/code&gt; 是偶数，下一个数字是 &lt;code&gt;8&lt;/code&gt;。因为 &lt;code&gt;8&lt;/code&gt; 是偶数，下一个数字是 &lt;code&gt;4&lt;/code&gt;。因为 &lt;code&gt;4&lt;/code&gt; 是偶数，下一个数字是 &lt;code&gt;2&lt;/code&gt;。因为 &lt;code&gt;2&lt;/code&gt; 是偶数，下一个数字是 &lt;code&gt;1&lt;/code&gt;。到这里就结束了。序列是 &lt;code&gt;5, 16, 8, 4, 2, 1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你的第一个任务是编写如下方法：&lt;code&gt;public static int nextNumber(int n)&lt;/code&gt;，它返回下一个数字。例如，&lt;code&gt;nextNumber(5)&lt;/code&gt; 应当返回 &lt;code&gt;16&lt;/code&gt;。Gradescope 自动评分器会测试这个方法。务必使用注释提供方法描述。你的描述应当包含在 &lt;code&gt;/**&lt;/code&gt; 和 &lt;code&gt;*/&lt;/code&gt; 之间。包含在 &lt;code&gt;/**&lt;/code&gt; 和 &lt;code&gt;*/&lt;/code&gt; 之间的注释也叫作“Javadoc 注释”，或简称“Javadocs”。如果需要额外空间，这些注释可以跨越多行，例如 &lt;code&gt;nextNumber&lt;/code&gt; 的 Javadocs。&lt;/p&gt;
&lt;p&gt;Javadocs 可以包含可选标签，例如 &lt;code&gt;@param&lt;/code&gt;。在 61B 中，除 &lt;code&gt;@source&lt;/code&gt; 标签外，我们不要求你使用这些标签。每当你在项目中获得了实质性帮助时，都应使用 &lt;code&gt;@source&lt;/code&gt; 标签。HW 或 Lab 并不要求 &lt;code&gt;@source&lt;/code&gt; 标签，但我们仍建议你使用，因为引用来源是一种良好的学术和职业习惯。&lt;/p&gt;
&lt;p&gt;一些 Java 提示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%&lt;/code&gt; 运算符实现取余。例如，&lt;code&gt;x % 4&lt;/code&gt; 的值会是 &lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt; 或 &lt;code&gt;3&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;==&lt;/code&gt; 运算符比较两个值是否相等。代码片段 &lt;code&gt;if (n % 4 == 1)&lt;/code&gt; 的意思是：“如果 &lt;code&gt;n&lt;/code&gt; 除以 &lt;code&gt;4&lt;/code&gt; 的余数等于 &lt;code&gt;1&lt;/code&gt;”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编写 &lt;code&gt;nextNumber&lt;/code&gt; 后，补全 &lt;code&gt;main&lt;/code&gt; 方法，使其打印从 &lt;code&gt;n = 5&lt;/code&gt; 开始的 Collatz 序列。例如，如果 &lt;code&gt;n = 5&lt;/code&gt;，程序应当打印 &lt;code&gt;5 16 8 4 2 1&lt;/code&gt;。&lt;code&gt;1&lt;/code&gt; 后面多出一个空格也没有关系。&lt;/p&gt;
&lt;p&gt;趣闻：对于所有数字，Collatz 序列似乎都会终止于 &lt;code&gt;1&lt;/code&gt;。然而到目前为止，还没有人能够证明这对所有可能的起始值都成立，不过大约到 &lt;code&gt;2^68&lt;/code&gt; 为止的所有值都已经检查过了。正如 Wikipedia 文章中所述，数学家 Jeffrey Lagarias 指出，Collatz 猜想“是一个极其困难的问题，完全超出了当代数学的能力范围”。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;E. 将你的工作推送到 GitHub&lt;/h2&gt;
&lt;p&gt;在本课程中，我们会使用 Git 工具，把你的工作副本存储在所谓的“仓库”中。正如我们会在后面的实验中看到的那样，你也能够使用 Git 从仓库中取回旧版本的工作。事实上，Git 是现实世界中最流行的代码版本控制软件；如果你进入软件工程领域，几乎可以肯定每天都会使用这个工具。&lt;/p&gt;
&lt;p&gt;私营公司 GitHub 拥有能够存储 Git 仓库的服务器，其网站 github.com 提供了一个方便的 Web 界面来查看仓库。&lt;/p&gt;
&lt;p&gt;在本节中，我们会了解如何把你的仓库“推送”到 GitHub。&lt;/p&gt;
&lt;p&gt;首先，前往与你的仓库对应的 GitHub URL。例如，如果你的仓库编号是 &lt;code&gt;343&lt;/code&gt;，你应访问 https://github.com/Berkeley-CS61B-Student/sp21-s343。前往与你自己的仓库对应的 URL。你会看到那里什么也没有。如果尝试其他仓库编号，会发现自己没有访问权限。也就是说，只有你（以及课程工作人员）能够查看你的仓库。&lt;/p&gt;
&lt;p&gt;为了推送代码，我们将使用以下三个命令：&lt;code&gt;add&lt;/code&gt;、&lt;code&gt;push&lt;/code&gt; 和 &lt;code&gt;commit&lt;/code&gt;。现在不用担心细节，只需按照下面的步骤操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开终端窗口，导航到包含你的 &lt;code&gt;sp21-s***&lt;/code&gt; 仓库的文件夹，&lt;strong&gt;不要&lt;/strong&gt;进入 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库。如果输入 &lt;code&gt;ls&lt;/code&gt; 命令，应当能看到 &lt;code&gt;lab1&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入以下命令，确认你位于正确目录中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切正常，应当会看到类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;On branch master
Changes not staged for commit:
  (use &quot;git add &amp;#x3C;file&gt;...&quot; to update what will be committed)
  (use &quot;git restore &amp;#x3C;file&gt;...&quot; to discard changes in working directory)
    modified:   lab1/Collatz.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 正在告诉你，你对 &lt;code&gt;lab1/Collatz.java&lt;/code&gt; 做了某些更改，但 GitHub 尚未记录这些更改。确保其中显示的是 &lt;code&gt;lab1/Collatz.java&lt;/code&gt;，而不只是 &lt;code&gt;Collatz.java&lt;/code&gt;。如果只显示 &lt;code&gt;Collatz.java&lt;/code&gt;，应使用 &lt;code&gt;cd ..&lt;/code&gt; 返回上一级目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git add lab1/*
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入以下命令，确认 &lt;code&gt;add&lt;/code&gt; 正常工作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切正常，应当会看到类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;On branch master
Changes to be committed:
  (use &quot;git restore --staged &amp;#x3C;file&gt;...&quot; to unstage)
        modified:   lab1/Collatz.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，Git 正在告诉你，它已经知道你希望记录对 &lt;code&gt;Collatz.java&lt;/code&gt; 所做的更改。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git commit -m &quot;done with Collatz&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行这条命令后，Git 会记录你对 &lt;code&gt;Collatz.java&lt;/code&gt; 所做的更改。它还会把消息 &lt;code&gt;done with Collatz&lt;/code&gt; 与这份记录一起保存。如果你将来想查找过去的某项特定更改，这些消息会非常有用。我们会在后面的实验中进一步讨论。终端还会打印一些输出，你会看到类似下面的内容，不过插入和删除的数量可能不同。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[master e2c138b] done with Collatz
 1 file changed, 15 insertions(+), 1 deletion(-)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与之前一样，输入 &lt;code&gt;git status&lt;/code&gt; 命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;commit&lt;/code&gt; 正常工作，应当会看到类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;On branch master
nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“working tree” 是 “clean” 的，意味着你的所有工作都已经备份。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刷新 GitHub.com 上你的仓库 URL。你会看到更改&lt;strong&gt;仍然&lt;/strong&gt;没有出现。这是因为我们还差最后一步。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ git push origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刷新 GitHub.com 上你的仓库 URL。现在你应当能够看到对 &lt;code&gt;Collatz.java&lt;/code&gt; 的更改，以及你输入的消息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;请注意，正常情况下，你不会在每一条命令后都输入 &lt;code&gt;git status&lt;/code&gt;。这里这样做只是为了确保你正确输入了这些命令。真正将代码上传到 GitHub 所必需的三条命令总结如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add lab1/*
git commit -m &quot;done with Collatz&quot;
git push origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会想到很多问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与 Dropbox 这类自动创建备份的工具相比，为什么必须手动跟踪更改会更有用？&lt;/li&gt;
&lt;li&gt;这些单独的命令分别做什么？如果 &lt;code&gt;commit&lt;/code&gt; 记录了我的工作，但直到输入 &lt;code&gt;push&lt;/code&gt; 后它才可见，那么备份究竟存到了哪里？&lt;/li&gt;
&lt;li&gt;为什么需要三条命令，而不是一条？为什么不直接使用类似 &lt;code&gt;git upload -m &quot;done with Collatz&quot;&lt;/code&gt; 的单条命令？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;让我们依次回答这些问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问：为什么人们不直接使用 Dropbox（或类似工具）？&lt;/strong&gt;
Dropbox 会不定期地备份所有内容。编写程序时，我们往往希望在特定时刻、只对特定文件创建备份。例如，当你终于让程序正常工作时，可能希望立刻备份，以便之后把程序弄坏时能够回到这个版本。另外，能够添加消息也让备份更容易整理，从而帮助你找到所需版本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;add&lt;/code&gt; 做什么？&lt;/strong&gt;
&lt;code&gt;add&lt;/code&gt; 会标记一个文件（或一组文件），表明你希望备份它们。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;commit&lt;/code&gt; 做什么？&lt;/strong&gt;
&lt;code&gt;commit&lt;/code&gt; 会使用给定消息创建一个备份。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果 &lt;code&gt;commit&lt;/code&gt; 创建了备份，但没有备份到 GitHub，它把文件备份到了哪里？&lt;/strong&gt;
你自己的电脑上！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在自己的电脑上创建备份有什么用？&lt;/strong&gt;
如果你想回到旧版本，可以使用 Git 的其他命令完成。把备份存储在本地，可以非常快速地恢复旧备份，而且不需要互联网连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果我的电脑坏了，&lt;code&gt;commit&lt;/code&gt; 创建的备份不就丢了吗？&lt;/strong&gt;
是的。这正是为什么还有 &lt;code&gt;push&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么不把 &lt;code&gt;add&lt;/code&gt;、&lt;code&gt;commit&lt;/code&gt; 和 &lt;code&gt;push&lt;/code&gt; 合并成一条命令？&lt;/strong&gt;
有时你只想添加少量文件，因此可能只对这些文件调用 &lt;code&gt;add&lt;/code&gt;，最后再执行 &lt;code&gt;commit&lt;/code&gt;。有时你想创建提交，却不想推送，例如因为你没有互联网连接，或者因为你正在处理过于敏感、不能放在任何互联网网站上的代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何查看旧备份的历史，以及如何恢复它们？&lt;/strong&gt;
如果想查看旧提交，可以使用 &lt;code&gt;git log&lt;/code&gt;；可以使用 &lt;code&gt;git checkout&lt;/code&gt; 恢复代码的旧副本。我们之后会介绍这些命令。你也可以使用 GitHub.com 的 Web 界面浏览甚至下载旧副本。&lt;/p&gt;
&lt;p&gt;整个学期中，我们会逐渐更好地理解 Git。&lt;/p&gt;
&lt;p&gt;额外内容：给看过 1984 年电影《Ghostbusters》（我小时候真的每天都会看）的人打个比方，&lt;code&gt;add&lt;/code&gt; 命令有点像用质子加速器对准一个幽灵（或多个幽灵），&lt;code&gt;commit&lt;/code&gt; 命令有点像把幽灵拉进捕捉器，而 &lt;code&gt;push&lt;/code&gt; 命令有点像把捕捉器卸载到收容单元中。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;F. 提交 Lab 1&lt;/h2&gt;
&lt;p&gt;最后一步是使用 Gradescope 提交你的工作。Gradescope 是你提交 Homework、Lab 和 Project 作业的网站。要注册 Gradescope（如果尚未注册），请前往 &lt;a href=&quot;https://www.gradescope.com/&quot;&gt;gradescope.com&lt;/a&gt;，点击中间白色的 &lt;strong&gt;Sign up for free&lt;/strong&gt; 按钮。你应使用自己的 Berkeley 邮箱。你应该已经通过 Berkeley 邮箱加入了我们的 Gradescope 课程页面。&lt;/p&gt;
&lt;p&gt;如果你尚未被添加到 Gradescope，可以查看 Ed 上的欢迎帖子，获取课程加入代码。&lt;/p&gt;
&lt;p&gt;加入 Gradescope 上的课程后，点击 &lt;strong&gt;Lab 1: Welcome to Java&lt;/strong&gt; 提交代码。&lt;/p&gt;
&lt;p&gt;点击该按钮后，你会进入一个选择仓库和分支的页面（如下图所示）。第一次执行此操作时，必须把 GitHub 账户连接到 Gradescope（类似你在注册网站上所做的操作）。点击 &lt;strong&gt;Connect to GitHub&lt;/strong&gt; 按钮，并按照说明操作。&lt;/p&gt;
&lt;p&gt;现在选择你的仓库和分支。如果你的仓库是 &lt;code&gt;sp21-s57&lt;/code&gt;，就在上面的框中选择 &lt;code&gt;sp21-s57&lt;/code&gt;，并在下面的框中选择 &lt;code&gt;master&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以后，你可以创建自己的“分支”（后面某个 61B Lab 的可选部分会介绍），并选择让 Gradescope 评测该分支，不过 61B 不会要求你这样做。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/424292970faf-github_repo_and_branch_selection.png&quot; alt=&quot;Github Repo and Branch Selection&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在按下 &lt;strong&gt;Upload&lt;/strong&gt; 进行提交！&lt;/p&gt;
&lt;p&gt;请在 Ed 上报告你遇到的任何问题。欢迎附上完整错误消息和/或截图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要：我们强烈鼓励你频繁创建提交！缺乏适当的版本控制不能成为工作丢失的理由，尤其是在课程开始几周之后。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;G. 验证 Snaps 安装&lt;/h2&gt;
&lt;p&gt;在终端中逐条运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd $SNAPS_DIR
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，在 Gradescope 中，你会看到 Lab 1 还有一个名为 &lt;strong&gt;Lab 1A: Snaps Checkoff&lt;/strong&gt; 的评分器。点击该按钮，然后在 &lt;strong&gt;Submission Method&lt;/strong&gt; 下选择 &lt;strong&gt;Github&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这一次，请选择你的 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库，而&lt;strong&gt;不要&lt;/strong&gt;选择 &lt;code&gt;sp21-s***&lt;/code&gt; 仓库，然后点击 &lt;strong&gt;Submit&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果通过这些评分器测试，说明你已经正确配置了电脑。太棒了！如果没有通过，请更仔细地重复本实验 C 部分和/或本节中的步骤，或者来 Office Hours。要获得 Lab 1 的全部分数，必须通过这个评分器。&lt;/p&gt;
&lt;p&gt;请注意，这是少数几次你会使用 Snaps 仓库进行提交的情况之一；每当确有必要时，我们都会明确告诉你。除此之外，始终应当在 &lt;code&gt;sp21-s***&lt;/code&gt; 仓库中完成并提交作业。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;回顾&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;IntelliJ IDE 是本学期运行 Java 代码时将使用的工具。&lt;/li&gt;
&lt;li&gt;Git 是一个版本控制系统，它以提交的形式跟踪一组文件的历史。&lt;/li&gt;
&lt;li&gt;经常提交，并使用信息明确的提交消息。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;skeleton&lt;/code&gt; 远程仓库拉取，以获取或更新作业的起始代码。&lt;/li&gt;
&lt;li&gt;使用 Gradescope 提交 Homework、Lab 和 Project。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Josh Hug 的配色方案（可选）&lt;/h2&gt;
&lt;p&gt;我不太喜欢 IntelliJ 默认的颜色，因此制作了自己的配色方案。它非常接近 Sublime 中极其优秀的 Sunburst 主题。要使用我的主题，请下载 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1/hug_sunburst.jar&quot;&gt;hug_sunburst&lt;/a&gt;，然后在 IntelliJ 中使用 &lt;strong&gt;File → Manage IDE Settings → Import Settings&lt;/strong&gt; 导入。最后你看到的文字可能会很大，因为我录制视频时使用大号文字。要按照自己的喜好调整 IntelliJ 字体大小，请参阅&lt;a href=&quot;https://www.jetbrains.com/help/idea/settings-editor-font.html&quot;&gt;此链接&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1/lab1&quot;&gt;https://sp21.datastructur.es/materials/lab/lab1/lab1&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 1 Setup：配置计算机</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-1-setup</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-1-setup</guid><description>CS61B Spring 2021 Lab 1 Setup：配置计算机中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#a-%E5%AE%89%E8%A3%85%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8%E5%8F%AF%E9%80%89&quot;&gt;A. 安装文本编辑器（可选）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#b-%E9%85%8D%E7%BD%AE%E4%BD%A0%E7%9A%84%E7%94%B5%E8%84%91&quot;&gt;B. 配置你的电脑&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c-%E5%AD%A6%E4%B9%A0%E4%BD%BF%E7%94%A8%E7%BB%88%E7%AB%AF%E5%8F%AF%E9%80%89&quot;&gt;C. 学习使用终端（可选）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#d-%E5%AF%B9%E6%AD%A5%E9%AA%A4-b-%E4%B8%AD%E7%9A%84%E9%85%8D%E7%BD%AE%E8%BF%9B%E8%A1%8C%E8%AF%95%E8%BF%90%E8%A1%8C&quot;&gt;D. 对步骤 B 中的配置进行试运行&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#e-%E5%AE%89%E8%A3%85-intellij&quot;&gt;E. 安装 IntelliJ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#f-%E5%AE%89%E8%A3%85-intellij-cs-61b-%E6%8F%92%E4%BB%B6&quot;&gt;F. 安装 IntelliJ CS 61B 插件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#g-%E5%BA%86%E7%A5%9D&quot;&gt;G. 庆祝&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们鼓励你在来参加 Lab 1 之前，尽可能独立完成这里的配置。如果你卡住了，请来 Office Hours 或实验课寻求帮助。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;A. 安装文本编辑器（可选）&lt;/h2&gt;
&lt;p&gt;如果你还没有自己喜欢的文本编辑器，我们建议安装一个。&lt;/p&gt;
&lt;p&gt;如今最流行的三个 GUI 文本编辑器似乎是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sublime Text（免费，但在你付费前会不断提醒你）：https://www.sublimetext.com/&lt;/li&gt;
&lt;li&gt;Atom（免费）：https://atom.io/&lt;/li&gt;
&lt;li&gt;Visual Studio Code（免费）：https://code.visualstudio.com/&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要更全面地了解这些以及其他文本编辑器，请参阅&lt;a href=&quot;https://www.techradar.com/best/best-text-editors&quot;&gt;这篇文本编辑器评测&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;具体选择并不十分重要，因为整门课程中我们只会使用文本编辑器几次。大多数时候，我们会使用另一种叫作 IDE 的工具。&lt;/p&gt;
&lt;p&gt;你不必从上面的三个选项中选择。完全可以使用其他文本编辑器（系统内置文本编辑器、vim、emacs 等）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;B. 配置你的电脑&lt;/h2&gt;
&lt;p&gt;根据你的操作系统，为了给电脑配置好 61B 环境，我们需要做几件事情。&lt;/p&gt;
&lt;p&gt;你需要采取的具体步骤取决于你的操作系统。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/windows.html&quot;&gt;Windows 说明&lt;/a&gt;（&lt;a href=&quot;https://youtu.be/W6cBueC25mM&quot;&gt;视频讲解&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/mac.html&quot;&gt;macOS 说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/linux.html&quot;&gt;Linux 说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;只有在完成上面与你的操作系统对应的说明后，才继续下一节。Windows 上的高级用户也可以使用新的 Bash for Windows 功能，但我们不会提供官方操作说明。请注意，如果你使用 Bash for Windows，就需要安装两次 Java（一次安装在 Bash for Windows 内部，另一次安装在 Windows 本身中，并按照上面的说明操作）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;C. 学习使用终端（可选）&lt;/h2&gt;
&lt;p&gt;如果你已经知道如何打开并使用终端，请跳过本节。&lt;/p&gt;
&lt;p&gt;终端是一个应用程序，它既允许你运行各种程序，也允许你操作自己电脑中的文件。它是一个强大但也危险的工具，因此使用其中某些命令时请务必小心。在类 Unix 操作系统上，Terminal 应用程序会提供你所需的一切。例如在 macOS 上，你可以使用 Spotlight 搜索 Terminal 应用程序。&lt;/p&gt;
&lt;p&gt;下面是一些你可能会在本课程中用到的重要命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cd&lt;/code&gt;：更改你的工作目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd hw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会把你的目录切换到 &lt;code&gt;hw&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pwd&lt;/code&gt;：当前工作目录（present working directory）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不确定自己当前在哪里，这条命令会告诉你当前所在目录的完整绝对路径。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;.&lt;/code&gt;：表示你的当前目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会把你的目录切换到当前目录（也就是不做任何事情）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;..&lt;/code&gt;：表示当前目录上一级的父目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会把你的目录切换到它的父目录。如果你位于 &lt;code&gt;/workspace/day1/&lt;/code&gt;，该命令会把你放到 &lt;code&gt;/workspace/&lt;/code&gt; 中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ls&lt;/code&gt;：列出目录中的文件/文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会列出当前目录中的所有文件和文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会列出当前目录中的所有文件和文件夹，并显示时间戳和文件权限。它可以帮助你再次确认文件是否正确更新，或者更改文件的读、写、执行权限。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mkdir&lt;/code&gt;：创建目录&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir dirname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会在当前目录内创建一个名为 &lt;code&gt;dirname&lt;/code&gt; 的目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt;：删除文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm file1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会从当前目录中删除 &lt;code&gt;file1&lt;/code&gt;。如果 &lt;code&gt;file1&lt;/code&gt; 不存在，它将不起作用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm -r dir1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会递归删除 &lt;code&gt;dir1&lt;/code&gt; 目录。换句话说，它不仅会删除 &lt;code&gt;dir1&lt;/code&gt; 本身，还会删除 &lt;code&gt;dir1&lt;/code&gt; 中的所有文件和目录。使用这条命令时请小心！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cp&lt;/code&gt;：复制文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp lab1/original lab2/duplicate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会复制 &lt;code&gt;lab1&lt;/code&gt; 目录中的 &lt;code&gt;original&lt;/code&gt; 文件，并在 &lt;code&gt;lab2&lt;/code&gt; 目录中创建一个 &lt;code&gt;duplicate&lt;/code&gt; 文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt;：移动或重命名文件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mv lab1/original lab2/original
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会把 &lt;code&gt;original&lt;/code&gt; 从 &lt;code&gt;lab1&lt;/code&gt; 移动到 &lt;code&gt;lab2&lt;/code&gt;。与 &lt;code&gt;cp&lt;/code&gt; 不同，&lt;code&gt;mv&lt;/code&gt; 不会在 &lt;code&gt;lab1&lt;/code&gt; 目录中留下原来的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mv lab1/original lab1/newname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令不会移动文件，而是把它从 &lt;code&gt;original&lt;/code&gt; 重命名为 &lt;code&gt;newname&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在命令行中导航时，还有一些其他有用的技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的 shell 可以通过 Tab 补全来帮你补全文件名和目录名。当你输入了一个不完整的名称（并且对应的内容已经存在）时，尝试按 &lt;code&gt;Tab&lt;/code&gt; 键进行自动补全，或查看可能的名称列表。&lt;/li&gt;
&lt;li&gt;如果你想重新输入最近使用过的同一条指令，请按键盘上的 &lt;code&gt;↑&lt;/code&gt; 键，直到看到正确的指令。如果你正在重复执行指令，这可以节省输入时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;D. 对步骤 B 中的配置进行试运行&lt;/h2&gt;
&lt;p&gt;让我们确保所有东西都能正常工作。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先打开终端。输入以下命令，检查 Git 是否是一个可识别的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该会打印出 Git 的版本号。如果你看到 &lt;code&gt;git: command not found&lt;/code&gt; 或类似内容，请尝试打开一个新的终端窗口、重新启动电脑，或者重新安装 Git。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接下来，让我们检查 &lt;code&gt;javac&lt;/code&gt; 和 &lt;code&gt;java&lt;/code&gt; 是否正常工作。&lt;code&gt;javac&lt;/code&gt; 和 &lt;code&gt;java&lt;/code&gt; 支持命令行编译，也就是说，它们让你能够直接从命令行运行 Java 程序。在实践中，大多数开发者会通过 IntelliJ 这样的 IDE 运行 Java 程序，因此本学期除了测试你的配置外，我们不会经常使用命令行编译。首先，在终端运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir ~/temp
cd ~/temp
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;然后，在这个目录中打开你的操作系统文件浏览器。可以从命令行执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mac：&lt;code&gt;open .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows：&lt;code&gt;explorer .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ubuntu：&lt;code&gt;gnome-open .&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Linux Mint：&lt;code&gt;xdg-open .&lt;/code&gt; 或 &lt;code&gt;mate .&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在这个新打开的目录中，创建一个名为 &lt;code&gt;HelloWorld.java&lt;/code&gt; 的文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello world!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在终端中输入 &lt;code&gt;ls&lt;/code&gt;（列出这个目录中的文件/文件夹）。你应该会看到列出的 &lt;code&gt;HelloWorld.java&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行 &lt;code&gt;javac HelloWorld.java&lt;/code&gt;。如果这条命令产生了任何输出，那么你的配置可能出了问题。请尝试打开一个新的终端窗口或重新启动电脑。如果仍然不起作用，请参阅与你的操作系统对应的说明中的 Troubleshooting 一节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入 &lt;code&gt;ls&lt;/code&gt;，你应该会同时看到 &lt;code&gt;HelloWorld.java&lt;/code&gt; 和一个刚刚创建的 &lt;code&gt;HelloWorld.class&lt;/code&gt;（&lt;code&gt;javac&lt;/code&gt; 命令创建了这个文件）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行 &lt;code&gt;java HelloWorld&lt;/code&gt;。它应该为你打印 &lt;code&gt;Hello world!&lt;/code&gt;。如果没有打印，那么你的配置有问题！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;完成了！你也可以根据需要删除 &lt;code&gt;temp&lt;/code&gt; 文件夹及其内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面的截图展示了我们在执行步骤 4–7 时希望看到的结果。如果你看到了与此类似的内容，那么 Java 配置就完成了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/feeb486c088e-hello_world.png&quot; alt=&quot;hello_world&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;E. 安装 IntelliJ&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;从 &lt;a href=&quot;https://www.jetbrains.com/idea/download/&quot;&gt;JetBrains&lt;/a&gt; 网站下载 IntelliJ 的 Community Edition。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择适合你操作系统的版本后，点击下载，并等待几分钟让文件下载完成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行安装程序。如果你安装了旧版本的 IntelliJ，此时应当卸载旧版本，并用这个较新的版本替换它。你可以使用所有默认安装选项，只有一个例外：如果你使用 Windows，请务必勾选 &lt;strong&gt;Add launchers dir to the PATH&lt;/strong&gt;。如果你不小心漏掉了它，最简单的修复办法是卸载 IntelliJ，然后再次运行安装程序，并确保第二次勾选该选项。下面的图片只适用于 Windows。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/77b22d0f7282-path.png&quot; alt=&quot;Path&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;F. 安装 IntelliJ CS 61B 插件&lt;/h2&gt;
&lt;p&gt;启动 IntelliJ，开始配置过程。然后按照下面的步骤操作。&lt;/p&gt;
&lt;p&gt;继续之前，请确保你运行的是 IntelliJ &lt;code&gt;2020.3.1&lt;/code&gt; 或更高版本。较旧版本也可能能够工作，但我们自己没有测试过。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 Welcome 窗口中，点击左侧菜单里的 &lt;strong&gt;Plugins&lt;/strong&gt; 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/ca1375bcd3f1-plugin_setup1.png&quot; alt=&quot;Configure Plugin&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在出现的窗口中，点击 &lt;strong&gt;Marketplace&lt;/strong&gt;，然后在顶部的搜索栏中输入 &lt;code&gt;CS 61B&lt;/code&gt;。此时应该会出现 CS 61B 插件条目。如果你点击了自动补全建议，出现的窗口可能与下图略有不同——这没有问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击绿色的 &lt;strong&gt;Install&lt;/strong&gt; 按钮，等待插件下载并安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/73fbd695f7e3-plugin_setup2.png&quot; alt=&quot;Search CS 61B&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;现在搜索 &lt;code&gt;Java Visualizer&lt;/code&gt;，然后点击绿色的 &lt;strong&gt;Install&lt;/strong&gt; 按钮安装该插件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/d8ca628a8231-plugin_setup3.png&quot; alt=&quot;Search Java Visualizer&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果出现绿色的 &lt;strong&gt;Restart IDE&lt;/strong&gt; 按钮，请点击它以完成安装。如果你没有看到 &lt;strong&gt;Restart IDE&lt;/strong&gt; 按钮，直接继续即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要了解更多关于插件使用的信息，请阅读&lt;a href=&quot;https://sp21.datastructur.es/materials/guides/plugin.html&quot;&gt;插件指南&lt;/a&gt;。你现在不必阅读它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;G. 庆祝&lt;/h2&gt;
&lt;p&gt;呼！你终于完成所有配置了。现在可以继续 Lab 1。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1setup/lab1setup&quot;&gt;https://sp21.datastructur.es/materials/lab/lab1setup/lab1setup&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 12：Project 3 入门</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-12-project-3-rendering</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-12-project-3-rendering</guid><description>CS61B Spring 2021 Lab 12：Project 3 入门中文学习资料。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;**重要：**在 IntelliJ 中，应当在 &lt;code&gt;proj3&lt;/code&gt; 目录这一层打开项目，而不是只打开 &lt;code&gt;lab12&lt;/code&gt; 目录。否则 package 配置会出现问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#lab-%E5%89%8D%E5%87%86%E5%A4%87&quot;&gt;Lab 前准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E4%BB%8B&quot;&gt;简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E8%AE%A4%E8%AF%86-tile-rendering-engine&quot;&gt;第一部分：认识 Tile Rendering Engine&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#boring-world&quot;&gt;Boring World&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#random-world&quot;&gt;Random World&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC%E4%BA%8C%E9%83%A8%E5%88%86%E4%BD%BF%E7%94%A8-tile-rendering-engine&quot;&gt;第二部分：使用 Tile Rendering Engine&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#hex-world-%E7%AE%80%E4%BB%8B&quot;&gt;Hex World 简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%98%E5%88%B6%E4%B8%80%E4%B8%AA%E5%85%AD%E8%BE%B9%E5%BD%A2&quot;&gt;绘制一个六边形&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%98%E5%88%B6%E5%85%AD%E8%BE%B9%E5%BD%A2%E9%95%B6%E5%B5%8C%E5%9B%BE%E6%A1%88&quot;&gt;绘制六边形镶嵌图案&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%8E%B0%E5%9C%BA%E7%BC%96%E7%A8%8B%E6%BC%94%E7%A4%BA&quot;&gt;现场编程演示&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%A7%E7%BB%AD%E5%AE%8C%E6%88%90-project-3&quot;&gt;继续完成 Project 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%8F%90%E4%BA%A4&quot;&gt;提交&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Lab 前准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;运行 &lt;code&gt;git pull skeleton master&lt;/code&gt;，获取 Project 3、Lab 12 和 Lab 13 的起始文件。&lt;/li&gt;
&lt;li&gt;观看课程链接中的往届 Project 3 入门视频。视频中的名称和 API 与本学期略有不同，但总体设计仍然适用。&lt;/li&gt;
&lt;li&gt;阅读 Project 3 Phase 1 的规格。&lt;/li&gt;
&lt;li&gt;Project 3 是一场马拉松，而不是短跑。不要拖到最后才开始。&lt;/li&gt;
&lt;li&gt;提前联系 Project 3 搭档。最好和搭档共同完成本 Lab。&lt;/li&gt;
&lt;li&gt;整个 Project 3 必须始终与同一位搭档合作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;本 Lab 将让你熟悉 Project 3 中提供的 Tile Rendering Engine，并练习把一个复杂绘图任务分解成具有清晰抽象层次的辅助方法。&lt;/p&gt;
&lt;p&gt;第一部分会查看两个演示程序，了解怎样建立并显示一个由 &lt;code&gt;TETile&lt;/code&gt; 组成的二维世界。第二部分会要求你使用同一套引擎绘制六边形，并把多个六边形组合为镶嵌图案。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;第一部分：认识 Tile Rendering Engine&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Boring World&lt;/h3&gt;
&lt;p&gt;在 IntelliJ 中打开 &lt;strong&gt;&lt;code&gt;proj3&lt;/code&gt;&lt;/strong&gt;，而不是 &lt;code&gt;lab12&lt;/code&gt;。通过 &lt;code&gt;proj3/pom.xml&lt;/code&gt; 导入项目。对于本 Lab，只需关注 &lt;code&gt;byow.lab12&lt;/code&gt; package。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;BoringWorldDemo&lt;/code&gt;。这个演示程序创建一个非常简单的世界。它的过程可以分为三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化一个 &lt;code&gt;TERenderer&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建一个二维 &lt;code&gt;TETile[][]&lt;/code&gt; 数组；&lt;/li&gt;
&lt;li&gt;要求 renderer 显示这个数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;创建并初始化 renderer 的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TERenderer ter = new TERenderer();
ter.initialize(width, height);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的宽度和高度以 tile 数量为单位。每个 tile 是 16×16 像素。例如，&lt;code&gt;initialize(10, 20)&lt;/code&gt; 会建立一个 10×20 tile 的窗口，即 160×320 像素。&lt;/p&gt;
&lt;p&gt;你可以使用 &lt;code&gt;TETile&lt;/code&gt; 构造器创建自己的 tile，也可以使用 &lt;code&gt;Tileset.java&lt;/code&gt; 中已经创建好的 tile。下面的代码会创建一个空世界：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TETile[][] world = new TETile[WIDTH][HEIGHT];
for (int x = 0; x &amp;#x3C; WIDTH; x += 1) {
    for (int y = 0; y &amp;#x3C; HEIGHT; y += 1) {
        world[x][y] = Tileset.NOTHING;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后，可以覆盖数组中的某个区域：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (int x = 20; x &amp;#x3C; 35; x += 1) {
    for (int y = 5; y &amp;#x3C; 10; y += 1) {
        world[x][y] = Tileset.WALL;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，使用下面的调用显示世界：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ter.renderFrame(world);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改数组之后，屏幕不会自动更新。只有再次调用 &lt;code&gt;renderFrame&lt;/code&gt;，renderer 才会显示新状态。&lt;/p&gt;
&lt;p&gt;尝试把 &lt;code&gt;WALL&lt;/code&gt; 换成其他 tile，并修改循环边界，观察世界如何变化。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TETile&lt;/code&gt; 是不可变对象，因此不能通过下面这种方式修改某个 tile 的字符：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;world[x][y].character = &apos;X&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Random World&lt;/h3&gt;
&lt;p&gt;运行 &lt;code&gt;RandomWorldDemo.java&lt;/code&gt;。这个演示展示了三个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;java.util.Random&lt;/code&gt; 伪随机数生成器；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;switch&lt;/code&gt; 语句；&lt;/li&gt;
&lt;li&gt;把任务委托给多个方法，而不是把所有逻辑都塞进 &lt;code&gt;main&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;考虑下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Random r = new Random(1000);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;伪随机序列实际上是确定性的。相同的 seed 会产生完全相同的序列：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Random r = new Random(82731);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());

r = new Random(82731);
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
System.out.println(r.nextInt());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两组四个整数会完全相同。调用 &lt;code&gt;new Random()&lt;/code&gt; 而不传入 seed 时，Java 会根据时间等不断变化的值选择 seed。&lt;/p&gt;
&lt;p&gt;起始代码使用固定 seed &lt;code&gt;2873123&lt;/code&gt;，因此每次运行都会生成相同的世界。Project 3 必须利用这种确定性，使相同输入总是能够重建同一个世界。&lt;/p&gt;
&lt;p&gt;观察 &lt;code&gt;RandomWorldDemo&lt;/code&gt; 如何使用 &lt;code&gt;switch&lt;/code&gt;，以及它如何把不同任务交给独立方法。这里最重要的设计原则是：不断把复杂任务拆分为行为清楚的小方法，并建立分层抽象。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;第二部分：使用 Tile Rendering Engine&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Hex World 简介&lt;/h3&gt;
&lt;p&gt;接下来，在 &lt;code&gt;HexWorld&lt;/code&gt; 中创建一个由随机六边形地形组成的世界。最终结果应当类似一个六边形棋盘，并使用草地、花、沙漠、森林和山脉等不同 tile。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;绘制一个六边形&lt;/h3&gt;
&lt;p&gt;首先实现一个方法，它的作用类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;addHexagon(..., int s, ...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法应当在指定位置绘制边长为 &lt;code&gt;s&lt;/code&gt; 的六边形。&lt;/p&gt;
&lt;p&gt;你的实现应当：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持 &lt;code&gt;s = 2, 3, 4, 5, ...&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;让六边形最宽的“中间”始终由两行同样长度的 tile 组成，以便多个六边形能够无缝镶嵌；&lt;/li&gt;
&lt;li&gt;使用辅助方法拆分绘制逻辑和几何计算；&lt;/li&gt;
&lt;li&gt;可以为关键计算辅助方法编写 JUnit 测试；&lt;/li&gt;
&lt;li&gt;可以设计一个 &lt;code&gt;Hexagon&lt;/code&gt; 类，但应当认真思考一个六边形对象应当知道什么、应当能够做什么；&lt;/li&gt;
&lt;li&gt;自己设计 &lt;code&gt;addHexagon&lt;/code&gt; 的完整方法签名，这本身也是任务的一部分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果想生成随机颜色，可以参考：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TETile.colorVariant(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个任务可能会占用整节 Lab。没有做完并不要紧；重点是认真思考代码设计。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;绘制六边形镶嵌图案&lt;/h3&gt;
&lt;p&gt;完成单个六边形后，尝试把它们排列成目标图案。最终图案应当包含 &lt;strong&gt;19 个六边形&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;不要把全部工作塞进一个巨大的嵌套循环而不编写辅助方法。如果没有分层抽象，代码会难以理解和调试；在有限的 Office Hours 时间中，TA 也很难帮助你。&lt;/p&gt;
&lt;p&gt;思考可以使用哪些辅助方法。例如，一个方法可以绘制一列六边形，另一个高层方法可以组合多列。坐标和间距的计算也可以放在独立方法中。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;现场编程演示&lt;/h2&gt;
&lt;p&gt;观看课程提供的现场编程演示，观察工作人员如何一步一步拆分问题，并让低层绘图操作服务于高层世界生成逻辑。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;继续完成 Project 3&lt;/h2&gt;
&lt;p&gt;阅读 Phase 1 规格，并查看 &lt;code&gt;project3prep.md&lt;/code&gt;。与搭档或 TA 讨论后填写这个文件。&lt;/p&gt;
&lt;p&gt;Project 3 的世界生成虽然比 Hex World 更复杂，但核心过程相同：先识别能够独立测试的小任务，再把它们组合成更高层的世界生成器。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;提交填写完整的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;project3prep.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要完整填写并提交这个文件，就可以获得本 Lab 的满分。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab12/lab12&quot;&gt;https://sp21.datastructur.es/materials/lab/lab12/lab12&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 13：Project 3 交互</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-13-project-3-interactivity</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-13-project-3-interactivity</guid><description>CS61B Spring 2021 Lab 13：Project 3 交互中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BB%8B%E7%BB%8D&quot;&gt;介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AE%B0%E5%BF%86%E6%B8%B8%E6%88%8F&quot;&gt;记忆游戏&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#generaterandomstring&quot;&gt;&lt;code&gt;generateRandomString&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#drawframe&quot;&gt;&lt;code&gt;drawFrame&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#flashsequence&quot;&gt;&lt;code&gt;flashSequence&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#solicitncharsinput&quot;&gt;&lt;code&gt;solicitNCharsInput&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#startgame&quot;&gt;&lt;code&gt;startGame&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%9C%89%E5%B8%AE%E5%8A%A9%E7%9A%84-ui&quot;&gt;有帮助的 UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%8F%90%E4%BA%A4%E4%B8%8E%E8%AF%84%E5%88%86&quot;&gt;提交与评分&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;本实验会帮助你开始项目第二阶段：交互性。希望你在做本实验时，已经在世界生成方面取得了显著进展。我们并不要求你在完成本实验时已经结束第一阶段，但你（希望）应当已经接近完成。&lt;/p&gt;
&lt;p&gt;与 Lab 12 一样，本实验不会直接向你的项目贡献任何实际代码；它会帮助你更加熟悉项目所需的有用工具，并教你一些最终可能会使用的编程范式。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;记忆游戏&lt;/h2&gt;
&lt;p&gt;为了准备制作你的游戏，我们会使用 &lt;code&gt;StdDraw&lt;/code&gt; 和 &lt;code&gt;java.util.Random&lt;/code&gt; 构建一个简单的记忆游戏。这个游戏很像电子玩具 &lt;a href=&quot;https://en.wikipedia.org/wiki/Simon_(game)&quot;&gt;Simon&lt;/a&gt;，不过它在电脑上运行，并使用键盘而不是 4 个彩色按钮。游戏目标是：一个随机生成的目标字符串会逐个字母短暂显示在屏幕上，随后玩家需要把它输入出来。&lt;/p&gt;
&lt;p&gt;目标字符串起初只有一个字母；每当玩家成功输入一个字符串后，游戏都会通过加长目标字符串而变得更难。&lt;/p&gt;
&lt;p&gt;最终，我们希望 &lt;code&gt;MemoryGame.java&lt;/code&gt; 拥有一个能够启动可玩记忆游戏的 &lt;code&gt;main&lt;/code&gt; 方法；但与其直接跳进游戏实现，不如先尝试拆解运行游戏所需完成的任务。对于这个记忆游戏，大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建游戏窗口。&lt;/li&gt;
&lt;li&gt;随机生成目标字符串。&lt;/li&gt;
&lt;li&gt;每次一个字符地在屏幕上显示目标字符串。&lt;/li&gt;
&lt;li&gt;等待玩家输入，直到其输入的字符数量与目标字符串中的字符数量相同。&lt;/li&gt;
&lt;li&gt;如果玩家输入与目标字符串匹配，则回到第 2 步，但使用一个更长的随机目标字符串；如果不匹配，则打印游戏结束消息并退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一般而言，良好的编码实践是：先构建用途明确的小过程，再使用这些基础过程组合出更复杂的方法。最终，你只需要在 &lt;code&gt;main&lt;/code&gt; 方法中写几行代码，就能构建出像游戏或文本编辑器这样复杂的东西。查看 &lt;code&gt;MemoryGame.java&lt;/code&gt;，你会看到我们已经写好了几个方法头，每个方法会处理上面的一项任务。&lt;/p&gt;
&lt;p&gt;对于 Project 3，我们强烈建议你采用这种过程：识别游戏的步骤，并把它拆分为独立方法。它会为你的开发提供一条清晰的前进道路，也会为单元测试提供明确的分界点。&lt;/p&gt;
&lt;p&gt;实验结束时，你会得到一个功能类似下面 GIF 的程序：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/817e828aaa56-memory-game-example.gif&quot; alt=&quot;Memory Game Example&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;generateRandomString&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;第一个任务：我们需要能够随机生成一个指定长度的字符串。如上面简要提到的那样，应当使用 &lt;code&gt;java.util.Random&lt;/code&gt; 完成随机生成。&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;MemoryGame&lt;/code&gt; 构造函数，创建一个 &lt;code&gt;Random&lt;/code&gt; 对象，并使用程序的第一个参数作为种子。之后，补全 &lt;code&gt;generateRandomString&lt;/code&gt;，使它使用你的 &lt;code&gt;Random&lt;/code&gt; 对象生成一个随机字符串，其长度由输入 &lt;code&gt;n&lt;/code&gt; 指定。因为我们只希望生成由小写字符组成的字符串，所以为了方便你，我们提供了私有字段 &lt;code&gt;CHARACTERS&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可以选择直接使用 &lt;code&gt;java.util.Random&lt;/code&gt; 的方法，也可以使用 &lt;code&gt;byow.Core.RandomUtils&lt;/code&gt; 中有帮助的工具方法。所有工具方法都把一个 &lt;code&gt;Random&lt;/code&gt; 对象作为参数，因此它们在底层确实都使用了相应的 &lt;code&gt;java.util.Random&lt;/code&gt; 方法；如果你好奇这些工具如何工作，可以查看它们！&lt;/p&gt;
&lt;p&gt;在使用 &lt;code&gt;char&lt;/code&gt; 和 &lt;code&gt;String&lt;/code&gt; 类型的变量时，你可能会遇到问题。下面是关于它们的三个有用知识点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 Java 中，&lt;code&gt;Character&lt;/code&gt; 或 &lt;code&gt;char&lt;/code&gt; 使用单引号包裹，如 &lt;code&gt;&apos;B&apos;&lt;/code&gt;；而 &lt;code&gt;String&lt;/code&gt; 对象使用双引号包裹，如 &lt;code&gt;&quot;and can be longer&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以使用 &lt;code&gt;+&lt;/code&gt; 运算符把 &lt;code&gt;char&lt;/code&gt; 添加到 &lt;code&gt;String&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String favClass = &quot;CS 61&quot; + &apos;B&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以通过下面的方法把 &lt;code&gt;char&lt;/code&gt; 转换为 &lt;code&gt;String&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String B = Character.toString(&apos;B&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;drawFrame&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;现在有了随机字符串，我们需要能够每次一个字母地在屏幕上显示它。为此，我们会使用 Princeton 的 &lt;code&gt;StdDraw&lt;/code&gt; 库。它已经位于 &lt;code&gt;javalib&lt;/code&gt; 文件夹中，因此如果你通过 &lt;code&gt;pom.xml&lt;/code&gt; 正确导入了项目，就能够访问 &lt;code&gt;StdDraw&lt;/code&gt; 为我们提供的所有有用方法。与 Lab 12 不同，这一次你会直接使用 &lt;code&gt;StdDraw&lt;/code&gt;，而不是依赖瓦片引擎。&lt;/p&gt;
&lt;p&gt;我们使用 &lt;code&gt;StdDraw&lt;/code&gt; 库，是因为它相当轻量，并且容易上手；但使用它时，你应当了解这个库的一些怪异之处。尤其是，每当我们想更改屏幕上显示的内容时，都必须清除整个屏幕，并重新绘制希望显示的全部内容。因此，拥有这样一个方法会极其有用：它先清除画布，绘制下一帧所需的一切，然后显示画布。&lt;/p&gt;
&lt;p&gt;最终，&lt;code&gt;drawFrame&lt;/code&gt; 会成为我们的这个方法；但现在先保持简单。我们知道自己需要在屏幕上显示字符串，并且字符串应当足够醒目。补全 &lt;code&gt;drawFrame&lt;/code&gt;：让它清除画布，把字体设置为大号粗体（大小 &lt;code&gt;30&lt;/code&gt; 很合适），绘制输入字符串并使其位于画布中央，然后把画布显示在屏幕上。现在很适合查看 &lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html&quot;&gt;&lt;code&gt;StdDraw&lt;/code&gt;&lt;/a&gt; API，并弄清楚它的确切工作方式。&lt;/p&gt;
&lt;p&gt;一些值得查看的有用方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html#setFont-java.awt.Font-&quot;&gt;&lt;code&gt;StdDraw.setFont&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html#clear--&quot;&gt;&lt;code&gt;StdDraw.clear&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html#text-double-double-java.lang.String-&quot;&gt;&lt;code&gt;StdDraw.text&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html#setPenColor-java.awt.Color-&quot;&gt;&lt;code&gt;StdDraw.setPenColor&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html#show--&quot;&gt;&lt;code&gt;StdDraw.show&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;flashSequence&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;使用目前已经构建的内容，我们需要定义一个过程，每次一个字符地呈现目标字符串。编写 &lt;code&gt;flashSequence&lt;/code&gt;，让它接收输入字符串，并每次把一个字符显示在屏幕中央。每个字符应当在屏幕上显示 &lt;code&gt;1&lt;/code&gt; 秒；字符之间应当有短暂的 &lt;code&gt;0.5&lt;/code&gt; 秒间隔，在此期间屏幕为空白。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;solicitNCharsInput&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;每次一个字符地显示目标字符串后，我们需要等待玩家输入自己的字符串。对于这项任务，我们必须使用 &lt;code&gt;StdDraw&lt;/code&gt; 的按键监听 API，读取玩家输入的内容。本实验中相关的方法是 &lt;code&gt;hasNextKeyTyped&lt;/code&gt; 和 &lt;code&gt;nextKeyTyped&lt;/code&gt;。它们会与 &lt;code&gt;StdDraw&lt;/code&gt; 用来存储用户所有已按下并释放的按键的队列交互。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hasNextKeyTyped&lt;/code&gt; 会查看队列中是否还留有任何按键；&lt;code&gt;nextKeyTyped&lt;/code&gt; 则移除队列前端的按键并返回它。请注意，&lt;code&gt;nextKeyTyped&lt;/code&gt; 以 &lt;code&gt;char&lt;/code&gt; 的形式返回按键——这是 &lt;code&gt;StdDraw&lt;/code&gt; 的另一个怪异之处，并且它使我们无法使用键盘上那些不对应 &lt;code&gt;char&lt;/code&gt; 值的按键。&lt;/p&gt;
&lt;p&gt;熟悉 &lt;code&gt;StdDraw&lt;/code&gt; 如何处理键盘输入后，编写 &lt;code&gt;solicitNCharsInput&lt;/code&gt;：使用 &lt;code&gt;StdDraw&lt;/code&gt; 读取 &lt;code&gt;n&lt;/code&gt; 次按键，并返回与这些按键对应的字符串。此外，随着用户输入按键，目前已经构建出的字符串应当居中显示在屏幕上，让用户能够看到自己已经按下了什么。&lt;/p&gt;
&lt;p&gt;一个很不错的附加功能是：允许用户在输入错误时删除字符；但因为 &lt;code&gt;StdDraw&lt;/code&gt; 无法处理退格键，我们会直接忽略这个功能，并声称这是为了让游戏更难。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;startGame&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;就快完成了！现在我们已经定义了所有子过程，是时候把它们组合起来并运行游戏了。&lt;code&gt;startGame&lt;/code&gt; 方法应当启动游戏，并开始游戏循环，直到玩家无法正确输入目标字符串。&lt;code&gt;startGame&lt;/code&gt; 的代码应当看起来像是把下面这个过程翻译成代码：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从第 &lt;code&gt;1&lt;/code&gt; 轮开始游戏。&lt;/li&gt;
&lt;li&gt;在屏幕中央显示消息 &lt;code&gt;&quot;Round: &quot;&lt;/code&gt;，后面跟上轮数。&lt;/li&gt;
&lt;li&gt;生成一个随机字符串，其长度等于当前轮数。&lt;/li&gt;
&lt;li&gt;每次一个字母地显示随机字符串。&lt;/li&gt;
&lt;li&gt;等待玩家输入一个与目标字符串长度相同的字符串。&lt;/li&gt;
&lt;li&gt;检查玩家是否输入正确。
&lt;ul&gt;
&lt;li&gt;如果输入正确，把轮数增加 &lt;code&gt;1&lt;/code&gt;，然后从第 2 步重复。&lt;/li&gt;
&lt;li&gt;如果输入错误，结束游戏，并在屏幕中央显示消息 &lt;code&gt;&quot;Game Over! You made it to round:&quot;&lt;/code&gt;，后面跟上玩家失败时的轮数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成这些内容后，你应当能够运行 &lt;code&gt;MemoryGame.java&lt;/code&gt; 并玩自己的游戏！它非常简陋，当然也不漂亮，但下一节会稍微改善这一点。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;有帮助的 UI&lt;/h2&gt;
&lt;p&gt;此时，你已经有了一个可以玩的、功能正常的游戏，但它的呈现方式绝对算不上令人愉快。最重要的是，如果有人在完全不了解背景的情况下拿到这个游戏，就会很难弄清楚正在发生什么。因为游戏非常简单，我们可以通过在顶部显示一条消息来解决这个问题，告诉玩家应该做什么：&lt;code&gt;&quot;Watch!&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;Type!&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;既然已经做到这里，另一个值得显示的内容是当前轮数，让玩家知道目标字符串有多长；并且，因为我们是友善的人，最好还给玩家显示一句鼓励的话。最终，我们希望得到类似下面的内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/59f38de5d072-image.png&quot; alt=&quot;UI_Example&quot;&gt;&lt;/p&gt;
&lt;p&gt;因为这是对用户界面（UI）的更改，所以需要修改 &lt;code&gt;drawFrame&lt;/code&gt;，让它具备这一功能。向 &lt;code&gt;drawFrame&lt;/code&gt; 方法添加内容，使得只要游戏尚未结束，就在屏幕顶部绘制一个栏，其中包含：左侧显示轮数，即 &lt;code&gt;&quot;Round: &quot; + round number&lt;/code&gt;；中央显示当前任务（&lt;code&gt;&quot;Watch!&quot;&lt;/code&gt; 或 &lt;code&gt;&quot;Type!&quot;&lt;/code&gt;）；右侧显示一句鼓励的话。&lt;/p&gt;
&lt;p&gt;对于鼓励语，你应当从某个好话集合中随机选择一句显示。为了方便你，我们提供了包含一些短语的私有字段 &lt;code&gt;ENCOURAGEMENT&lt;/code&gt;，但你可以自由添加自己的短语。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交与评分&lt;/h2&gt;
&lt;p&gt;本实验没有 Gradescope 评分器。只要在 &lt;code&gt;4/16 11:59 PM&lt;/code&gt; 前提交 &lt;a href=&quot;https://forms.gle/1xbRNMjdyqZwZuWo9&quot;&gt;Phase 1 Review Form&lt;/a&gt;，你就会获得本实验的 &lt;code&gt;64&lt;/code&gt; 分。&lt;/p&gt;
&lt;p&gt;即使没有评分器，我们仍强烈建议你确保自己理解这里的所有方法，并创建一个完全能够工作的解答。这样，当你在完成 Project 3: BYOW 时对某个渲染方面感到困惑，就可以把本实验当作一个小型示例，快速提醒自己这个库是如何工作的。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab13/lab13&quot;&gt;https://sp21.datastructur.es/materials/lab/lab13/lab13&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 2：JUnit 与调试</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-2-junit-debugging</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-2-junit-debugging</guid><description>CS61B Spring 2021 Lab 2：JUnit 与调试中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#lab-%E5%89%8D%E5%87%86%E5%A4%87&quot;&gt;Lab 前准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E4%BB%8B&quot;&gt;简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B0%83%E8%AF%95%E5%99%A8%E5%9F%BA%E7%A1%80&quot;&gt;调试器基础&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%AD%E7%82%B9%E4%B8%8E-step-into&quot;&gt;断点与 Step Into&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#step-over-%E4%B8%8E-step-out&quot;&gt;Step Over 与 Step Out&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%BF%9B%E4%B8%80%E6%AD%A5%E6%8C%87%E5%AF%BC%E6%8F%90%E4%BE%9B%E7%BB%99%E9%9C%80%E8%A6%81%E7%9A%84%E4%BA%BA&quot;&gt;进一步指导（提供给需要的人）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%9B%9E%E9%A1%BE%E8%B0%83%E8%AF%95&quot;&gt;回顾：调试&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#junit-%E4%B8%8E%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95&quot;&gt;JUnit 与单元测试&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#junit-%E8%AF%AD%E6%B3%95&quot;&gt;JUnit 语法&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%9C%A8-intellij%E6%88%96%E5%85%B6%E4%BB%96-ide%E4%B8%AD%E8%BF%90%E8%A1%8C-junit-%E6%B5%8B%E8%AF%95&quot;&gt;在 IntelliJ（或其他 IDE）中运行 JUnit 测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%BA%94%E7%94%A8intlist&quot;&gt;应用：IntList&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B5%B7%E5%A7%8B%E4%BB%A3%E7%A0%81&quot;&gt;起始代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#a-%E9%83%A8%E5%88%86%E9%81%8D%E5%8E%86-intlist&quot;&gt;A 部分：遍历 IntList&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#b-%E9%83%A8%E5%88%86%E5%B5%8C%E5%A5%97%E8%BE%85%E5%8A%A9%E6%96%B9%E6%B3%95%E4%BB%A5%E5%8F%8A%E4%B8%BA%E4%BA%86%E8%B0%83%E8%AF%95%E8%80%8C%E9%87%8D%E6%9E%84&quot;&gt;B 部分：嵌套辅助方法，以及为了调试而重构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#c-%E9%83%A8%E5%88%86%E6%A3%98%E6%89%8B%E7%9A%84-intlist&quot;&gt;C 部分：棘手的 IntList&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%8F%90%E4%BA%A4&quot;&gt;提交&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%8C%E6%95%B4%E5%9B%9E%E9%A1%BE&quot;&gt;完整回顾&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#faq-%E4%B8%8E%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98&quot;&gt;FAQ 与常见问题&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#string-%E6%88%96-stringequals-%E4%B9%8B%E7%B1%BB%E7%9A%84%E5%86%85%E5%AE%B9%E5%8F%98%E7%BA%A2%E4%BA%86&quot;&gt;String 或 String.equals() 之类的内容变红了&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Lab 前准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;完成 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab2setup/lab2setup&quot;&gt;Lab 2 Setup&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;在你的仓库中运行 &lt;code&gt;git pull skeleton master&lt;/code&gt;。你应该会得到一个 &lt;code&gt;lab2/&lt;/code&gt; 文件夹。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在本 Lab 中，你将学习如何使用 IntelliJ 调试器，以及如何在 IntelliJ 中使用 JUnit 测试。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;调试器基础&lt;/h2&gt;
&lt;p&gt;重复 Lab 2 Setup 中的“Project Setup”流程。不过，这一次你应当“打开或导入”&lt;code&gt;lab2/pom.xml&lt;/code&gt; 文件，而不是 &lt;code&gt;lab2setup/pom.xml&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;导入完成后，你的 IntelliJ 应当大致如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/4f12c26bda85-folder_structure.png&quot; alt=&quot;文件夹结构&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;断点与 Step Into&lt;/h3&gt;
&lt;p&gt;我们先运行 &lt;code&gt;DebugExercise1&lt;/code&gt; 中的 &lt;code&gt;main&lt;/code&gt; 方法。在 IntelliJ 中打开这个文件，然后单击运行按钮。控制台中应当打印出三条语句，其中有一条应该会让你觉得明显不正确。假如你不确定怎样运行 &lt;code&gt;DebugExercise1&lt;/code&gt;，请在文件列表中右键单击它，然后单击下图所示的 &lt;code&gt;Run DebugExercise1.main&lt;/code&gt; 按钮：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/abc142b7bdab-run_button.png&quot; alt=&quot;运行按钮&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们的代码中某个地方存在 bug，但不要开始仔细阅读代码来寻找它！虽然这个特定的 bug 也许能被你直接看出来，但很多时候，如果不真正运行代码并在代码执行时探查正在发生的事情，bug 几乎不可能被看见。&lt;/p&gt;
&lt;p&gt;你们中的许多人都非常熟悉使用打印语句来探查程序运行时正在“思考”什么。打印语句对调试非常有用，但它们也有几个缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它们要求你修改代码（以便添加打印语句）。&lt;/li&gt;
&lt;li&gt;它们要求你明确说出自己想知道什么（因为你必须精确指定要打印的内容）。&lt;/li&gt;
&lt;li&gt;它们以一种可能很难阅读的格式提供结果，因为执行窗口中只会出现一大团文本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果使用调试器，寻找 bug 通常（但并非总是）会耗费更少的时间和脑力。IntelliJ 调试器允许你在代码执行到一半时暂停程序，逐行执行代码，甚至可以把链表之类的复杂数据结构组织方式可视化出来。&lt;/p&gt;
&lt;p&gt;调试器虽然强大，但必须正确使用，才能真正获得优势。我们鼓励你采用一种可以称为“科学调试”的方式，也就是使用一种与科学方法非常相似的过程进行调试！&lt;/p&gt;
&lt;p&gt;一般来说，你应当先对某段代码应该怎样运行提出假设，然后使用调试器判断这些假设是否成立。每获得一条新的证据，你就进一步修正自己的假设，直到最后不得不直接撞上那个 bug。&lt;/p&gt;
&lt;p&gt;第一个练习将介绍两个核心工具：&lt;code&gt;breakpoint&lt;/code&gt; 和 &lt;code&gt;step over&lt;/code&gt; 按钮。在左侧的 Project 视图中，右键单击（或双指单击）&lt;code&gt;DebugExercise1&lt;/code&gt; 文件；这一次不要选择 &lt;code&gt;Run&lt;/code&gt;，而要选择 &lt;code&gt;Debug&lt;/code&gt;。假如没有出现 Debug 选项，说明你没有正确导入 &lt;code&gt;lab2&lt;/code&gt; 项目（请查看 &lt;code&gt;lab2setup&lt;/code&gt; 中的说明）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/28c10098be02-debug_button.png&quot; alt=&quot;Debug 选项&quot;&gt;&lt;/p&gt;
&lt;p&gt;你会看到程序只是再次运行了一遍，看起来没有任何区别！这是因为我们还没有交给调试器任何有趣的事情。让我们通过“设置断点”来解决这个问题。滚动到写有 &lt;code&gt;int t3 = 3;&lt;/code&gt; 的那一行，然后单击行号右侧的位置。你应该会看到一个略微像停车标志的红点，这意味着我们现在已经设置了一个断点。如果再次以调试模式运行程序，它就会在这一行停下来。&lt;/p&gt;
&lt;p&gt;假如你不想通过右键单击来再次运行程序，也可以单击屏幕右上角的虫子图标。展示本段步骤的动画 GIF（来自以前的学期）可以在&lt;a href=&quot;https://gfycat.com/&quot;&gt;这个链接&lt;/a&gt;中看到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/a0faa0debe02-breakpoint.png&quot; alt=&quot;断点&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你单击调试按钮之后没有看到文本控制台（其中会显示类似 &lt;code&gt;round(10/2)&lt;/code&gt; 的内容），那么继续之前可能还需要执行一个额外步骤。在底部面板的信息窗口左上方，你应该能看到名为 “Debugger” 和 “Console”（以及 “Java Visualizer”）的标签页。单击并把 “Console” 窗口拖到下方面板的最右侧。这样就能同时显示调试器和控制台。&lt;/p&gt;
&lt;p&gt;展示这一过程的动画 GIF（来自以前的学期）可以在&lt;a href=&quot;https://gfycat.com/&quot;&gt;这个链接&lt;/a&gt;中看到。&lt;/p&gt;
&lt;p&gt;单击调试按钮后（并在必要时让控制台窗口可见），你应该会看到程序暂停在设置断点的那一行；底部还应该显示所有变量的列表，其中包括 &lt;code&gt;t&lt;/code&gt;、&lt;code&gt;b&lt;/code&gt;、&lt;code&gt;result&lt;/code&gt;、&lt;code&gt;t2&lt;/code&gt;、&lt;code&gt;b2&lt;/code&gt; 和 &lt;code&gt;result2&lt;/code&gt;。单击 “step into” 按钮可以让程序向前执行一步；它是一个如下所示、箭头朝下的按钮：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/9914ad1934d8-step_into.png&quot; alt=&quot;Step Into&quot;&gt;&lt;/p&gt;
&lt;p&gt;本 Lab 稍后会讨论其他按钮。请确保按下的是 &lt;code&gt;step into&lt;/code&gt;，而不是 &lt;code&gt;step over&lt;/code&gt;。Step Into 的箭头笔直朝下，而 Step Over 的箭头会先向右上方，再向右下方。&lt;/p&gt;
&lt;p&gt;每单击一次这个按钮，程序就会向前执行一步。每次单击之前，先对变量将会如何变化提出一个假设。&lt;/p&gt;
&lt;p&gt;注意：当前高亮显示的行是&lt;strong&gt;即将执行&lt;/strong&gt;的行，而不是刚刚已经执行完的行。&lt;/p&gt;
&lt;p&gt;重复这一过程，直到找到某一行，其结果与你的预期或代码编写者的预期不符。尝试判断为什么这一行没有做出你预期的事情。如果第一次错过了 bug，请单击停止按钮（红色方块），然后单击调试按钮，从头重新开始。找到 bug 后，你可以选择修复它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Step Over 与 Step Out&lt;/h3&gt;
&lt;p&gt;正如我们在构造和组合程序时依赖分层抽象一样，在调试程序时也应当依赖抽象。IntelliJ 中的 “step over” 按钮使这成为可能。前一个练习中的 “step into” 会显示程序字面意义上的下一步，而 “step over” 按钮则允许我们完成一次函数调用，却不展示这个函数内部的执行过程。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DebugExercise2&lt;/code&gt; 中的 &lt;code&gt;main&lt;/code&gt; 方法应当接收两个数组，计算两个数组逐元素的最大值，然后把得到的这些最大值相加。例如，假设两个数组是 &lt;code&gt;{2, 0, 10, 14}&lt;/code&gt; 和 &lt;code&gt;{-5, 5, 20, 30}&lt;/code&gt;。逐元素最大值为 &lt;code&gt;{2, 5, 20, 30}&lt;/code&gt;；例如，在第二个位置中，“0”和“5”中较大的数是 5。这个逐元素最大值数组的元素总和为 2 + 5 + 20 + 30 = 57。&lt;/p&gt;
&lt;p&gt;提供的代码中有两个不同的 bug。这个练习的任务是修复这两个 bug，但有一条特殊规则：你&lt;strong&gt;不应当&lt;/strong&gt;进入 &lt;code&gt;max&lt;/code&gt; 或 &lt;code&gt;add&lt;/code&gt; 函数，也不应当尝试理解它们。这些函数非常奇怪，它们使用某些语法（而且代码风格很差），以一种极其晦涩的方式完成本来很简单的工作。如果你不小心进入了这两个函数之一，请使用 “step out” 按钮（一个朝上的箭头）退出。&lt;/p&gt;
&lt;p&gt;即使不进入这些函数，你仍然应当能够判断它们是否有 bug。这正是抽象的美妙之处！即使我不了解鱼在分子层面是如何运作的，在某些情况下，我仍然能够清楚判断一条鱼已经死了。&lt;/p&gt;
&lt;p&gt;如果发现这些函数中的某一个存在 bug，应当完全重写它，而不是尝试修补它。&lt;/p&gt;
&lt;p&gt;现在我们已经告诉你 “step over” 的作用，请自己探索它究竟如何工作，并尝试找出这两个 bug。假如你遇到的问题是，右上角的运行（或调试）按钮一直运行 &lt;code&gt;DebugExercise1&lt;/code&gt;，请右键单击 &lt;code&gt;DebugExercise2&lt;/code&gt; 来运行它。&lt;/p&gt;
&lt;p&gt;如果卡住了，或者只是想获得更多指导，请阅读下面的说明。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;进一步指导（提供给需要的人）&lt;/h4&gt;
&lt;p&gt;首先尝试运行程序。&lt;code&gt;main&lt;/code&gt; 方法会计算一个答案并把它打印到控制台。尝试手动计算答案，你会看到打印出来的答案不正确。如果不知道怎样手动计算答案，请重新阅读上面对函数目标的说明，或者阅读所提供代码中的注释。&lt;/p&gt;
&lt;p&gt;接着，在 &lt;code&gt;main&lt;/code&gt; 中调用 &lt;code&gt;sumOfElementwiseMaxes&lt;/code&gt; 的那一行设置断点。然后单击调试按钮，再使用 Step Into 到达 &lt;code&gt;sumOfElementWiseMaxes&lt;/code&gt; 的第一行。接着，在调用 &lt;code&gt;arrayMax&lt;/code&gt; 的那一行使用 “step over” 按钮。输出有什么问题（如果有）？也就是说，它在哪些方面没有符合你的预期？注意，为了查看数组的内容，你可能需要在下方面板调试器窗口的 Variables 标签页中，单击变量名称旁边朝右的小三角形。&lt;/p&gt;
&lt;p&gt;如果你认为其中存在 bug，请进入 &lt;code&gt;arrayMax&lt;/code&gt;（而不是跨过它）并尝试找到 bug。提醒：不要进入 &lt;code&gt;max&lt;/code&gt;。你应当能够通过 Step Over 判断 &lt;code&gt;max&lt;/code&gt; 是否有 bug。如果 &lt;code&gt;max&lt;/code&gt; 有 bug，请把它完全替换掉。&lt;/p&gt;
&lt;p&gt;对 &lt;code&gt;arraySum&lt;/code&gt; 和 &lt;code&gt;add&lt;/code&gt; 重复同样的过程。修复两个 bug 后，再次确认 &lt;code&gt;sumOfElementwiseMaxes&lt;/code&gt; 方法能够针对给定输入正确工作。注意：这并不能证明 &lt;code&gt;sumOfElementwiseMaxes&lt;/code&gt; 是正确的，但现在没有必要编写额外测试来帮助验证这一点（下周将会做这件事）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;回顾：调试&lt;/h3&gt;
&lt;p&gt;到这里，你应当理解以下工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;断点；&lt;/li&gt;
&lt;li&gt;Step Over；&lt;/li&gt;
&lt;li&gt;Step Into；&lt;/li&gt;
&lt;li&gt;Step Out（尽管本 Lab 中你可能实际上没有使用这个功能）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过，这些内容只触及了调试器功能的表面！你可以自由进行试验。例如，你可以尝试弄清楚 “Watches” 标签页的作用。另一个我们不会讲解、但很方便的功能是 “Evaluate Expression” 按钮（Step Into/Over/Out 按钮所在那一行最后几个按钮中的一个，看起来像计算器）。在 Lab 3 和 Lab 4 中，我们还会展示一些额外的调试器功能。&lt;/p&gt;
&lt;p&gt;还务必查看课程的&lt;a href=&quot;https://sp21.datastructur.es/materials/guides/debugging-guide&quot;&gt;调试指南&lt;/a&gt;！其中讨论了本 Lab 没有覆盖的一些功能（例如条件断点）；当你需要调试帮助时，它仍然是一份非常好的总览。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;JUnit 与单元测试&lt;/h2&gt;
&lt;p&gt;现在，我们把注意力转向 Lecture 3 中介绍过的 JUnit 与单元测试。&lt;/p&gt;
&lt;p&gt;单元测试是一种严格测试代码中各个方法的好办法，并最终帮助你确保自己拥有一个能够工作的项目。&lt;/p&gt;
&lt;p&gt;单元测试中的 “Unit” 来自这样一种思想：可以把程序拆分为若干单元，也就是应用程序中最小的可测试部分。因此，单元测试会促使你使用良好的代码结构（每个方法应当只做“一件事”），并且允许你考虑每个方法的所有边界情况，再分别对它们进行测试。&lt;/p&gt;
&lt;p&gt;在本课程中，你将使用 JUnit 创建并运行测试，以确保代码的正确性。当 JUnit 测试失败时，你也会获得一个非常好的调试起点。此外，假如遇到了某个难以修复的糟糕 bug，你可以使用 Git 回退到根据 JUnit 测试判断、代码仍然能够正常工作的状态（到 Lab 4 时，我们会讨论如何把代码恢复到旧版本）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;JUnit 语法&lt;/h4&gt;
&lt;p&gt;正如 Lecture 中讨论的那样，JUnit 测试使用 Java 编写。&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;ArithmeticTest.java&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你首先会注意到顶部的 import（IntelliJ 有时会把它们缩短为 &lt;code&gt;import ...&lt;/code&gt;；只需单击 &lt;code&gt;...&lt;/code&gt;，即可展开并查看具体导入了什么）。这些 import 让你能够轻松访问运行 JUnit 测试所需的 JUnit 方法和功能。有关更多信息，请查看&lt;a href=&quot;https://www.youtube.com/&quot;&gt;测试 Lecture 视频&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;接下来，你会看到 &lt;code&gt;ArithmeticTest.java&lt;/code&gt; 中有两个方法：&lt;code&gt;testProduct&lt;/code&gt; 和 &lt;code&gt;testSum&lt;/code&gt;。这些方法遵循以下格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
public void testMethod() {
    assertEquals(&amp;#x3C;expected&gt;, &amp;#x3C;actual&gt;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;assertEquals&lt;/code&gt; 是 JUnit 测试中常用的方法。它会测试某个变量的实际值是否等于其预期值。&lt;/p&gt;
&lt;p&gt;创建 JUnit 测试文件时，应当在每个测试方法之前添加 &lt;code&gt;@Test&lt;/code&gt; 注解；一个测试方法中可以包含一个或多个 &lt;code&gt;assertEquals&lt;/code&gt; 或 &lt;code&gt;assertTrue&lt;/code&gt; 方法（由 JUnit 库提供）。所有测试都必须是非静态的。由于测试不会使用实例变量，而且你大概也不会实例化这个类，这一点可能显得很奇怪。不过，JUnit 的设计者决定测试就应当这样编写，所以我们照做即可。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;在 IntelliJ（或其他 IDE）中运行 JUnit 测试&lt;/h2&gt;
&lt;p&gt;注意：如果你决定不使用 IntelliJ，那么在运行 JUnit 测试这件事上，你需要自行解决。课程工作人员没有接受过其他 IDE、命令行编译工具或命令行执行工具的培训，也不会为选择这些工具的学生提供支持。&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;ArithmeticTest.java&lt;/code&gt; 后，单击 IntelliJ 顶部 &lt;code&gt;Run&lt;/code&gt; 菜单下的 &lt;code&gt;Run...&lt;/code&gt; 选项，如下面的截图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/e3d0cb27a620-lab3_run.png&quot; alt=&quot;Run 选项&quot;&gt;&lt;/p&gt;
&lt;p&gt;单击 “Run…” 后，你应该会看到若干个选项，外观大致如下。你列表中的项目数量可能不同。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/20840d2e6a42-lab3_run_menu.png&quot; alt=&quot;Run 菜单&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们关心的是红绿箭头旁边写有 “ArithmeticTest” 的那个选项（上图中编号 1 旁边的项目）。&lt;/p&gt;
&lt;p&gt;选择它，你应该会看到类似下面的结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/ff3e948349fe-default_renderer.png&quot; alt=&quot;默认测试渲染器&quot;&gt;&lt;/p&gt;
&lt;p&gt;这表示 &lt;code&gt;ArithmeticTest.java&lt;/code&gt; 第 25 行的测试失败了。测试预期 5 + 6 等于 11，但 &lt;code&gt;Arithmetic&lt;/code&gt; 类声称 5 + 6 等于 30。你会看到，尽管 &lt;code&gt;testSum&lt;/code&gt; 中包含多条 &lt;code&gt;assert&lt;/code&gt; 语句，却只显示了一次失败。&lt;/p&gt;
&lt;p&gt;这是因为 JUnit 测试会短路：一个方法中的某条断言一旦失败，它就会输出失败信息并转到下一个测试。&lt;/p&gt;
&lt;p&gt;尝试单击屏幕底部窗口中的 &lt;code&gt;ArithmeticTest.java:27&lt;/code&gt;，IntelliJ 会直接把你带到导致测试失败的那一行。在后续项目中运行自己的测试时，这个功能会很方便。&lt;/p&gt;
&lt;p&gt;现在修复这个 bug。你可以检查 &lt;code&gt;Arithmetic.java&lt;/code&gt; 并找出 bug，也可以使用 IntelliJ 调试器逐步执行代码，直到到达 bug 所在位置。&lt;/p&gt;
&lt;p&gt;修复后，重新运行测试；如果使用默认渲染器，你应该会看到一条漂亮而光荣的绿色横条。享受这种快感吧。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;应用：IntList&lt;/h2&gt;
&lt;p&gt;正如周一 Lecture 中讨论的那样，&lt;code&gt;IntList&lt;/code&gt; 是 CS61B 对整数裸递归链表的实现。每个 &lt;code&gt;IntList&lt;/code&gt; 都有一个 &lt;code&gt;first&lt;/code&gt; 变量和一个 &lt;code&gt;rest&lt;/code&gt; 变量。&lt;code&gt;first&lt;/code&gt; 是这个结点所包含的 &lt;code&gt;int&lt;/code&gt; 元素，而 &lt;code&gt;rest&lt;/code&gt; 是链表中的下一段（另一个 &lt;code&gt;IntList&lt;/code&gt;！）。&lt;/p&gt;
&lt;p&gt;我们创建了一个 &lt;code&gt;IntListExercises.java&lt;/code&gt; 文件，其中包含三个方法，并且每个方法都有 bug。本节的任务是找出并修复这些 bug！为了帮助你，我们添加了一些有用的起始代码和测试骨架，下面将对此进行说明。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;起始代码&lt;/h3&gt;
&lt;p&gt;相对于周一 Lecture 中的实现，我们在 &lt;code&gt;IntList&lt;/code&gt; 类中增加了两个方法：&lt;code&gt;print&lt;/code&gt; 和 &lt;code&gt;of&lt;/code&gt;。&lt;code&gt;of&lt;/code&gt; 方法是一种用于创建 &lt;code&gt;IntList&lt;/code&gt; 的便捷方法。下面快速演示它的使用方式。考虑你在 Lecture 中见过的以下代码，它会创建一个包含元素 1、2 和 3 的 &lt;code&gt;IntList&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList lst = new IntList(1, new IntList(2, new IntList(3, null)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这需要输入很多内容，而且相当令人困惑！&lt;code&gt;IntList.of&lt;/code&gt; 方法解决了这个问题。要创建一个包含元素 1、2 和 3 的 IntList，只需输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList lst = IntList.of(1, 2, 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是很棒？！它适用于包含任意数量元素的列表！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Creates an empty list!
IntList empty = IntList.of();

// Creates an IntList one element, 7
IntList oneElem = IntList.of(7);

// Creates an IntList with many elements
IntList manyElems = IntList.of(5, 4, 3, 2, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一个方法 &lt;code&gt;print&lt;/code&gt; 会返回 IntList 的 String 表示。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;IntList lst = IntList.of(1, 2, 3);
System.out.println(lst.toString())

// Output: 1 -&gt; 2 -&gt; 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;严格来说，这些方法没有为 &lt;code&gt;IntList&lt;/code&gt; 类添加任何真正的新功能，但它们分别提供了创建和显示 &lt;code&gt;IntList&lt;/code&gt; 的便捷方式。我们会使用这些便捷方法让测试变得更容易；在编写自己的 JUnit 测试来调试 &lt;code&gt;IntList&lt;/code&gt; 时，你也会练习使用它们！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;A 部分：遍历 IntList&lt;/h3&gt;
&lt;p&gt;这一部分将调试 &lt;code&gt;IntListExercises.java&lt;/code&gt; 中的 &lt;code&gt;addConstant&lt;/code&gt; 方法。这个方法的目标是接收一个 &lt;code&gt;IntList&lt;/code&gt;，并以可变方式把一个常量加到列表中的每个元素上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/* Expected Behavior */

IntList lst = IntList.of(1, 2, 3);

addConstant(lst, 1);
System.out.println(lst.toString());
// Output: 2 -&gt; 3 -&gt; 4

addConstant(lst, 4);
System.out.println(lst.toString());
// Output: 6 -&gt; 7 -&gt; 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;糟糕！起始代码中提供的 &lt;code&gt;addConstant&lt;/code&gt; 实现有 bug！我们在 &lt;code&gt;AddConstantTest.java&lt;/code&gt; 中提供了三个测试，可以帮助你隔离这个 bug。这些测试会使用上面提到的 &lt;code&gt;IntList.toString&lt;/code&gt; 和 &lt;code&gt;IntList.of&lt;/code&gt; 方法！使用 Java 调试器逐步执行每个测试，以帮助你隔离 bug。隔离出 bug 后，修复它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;B 部分：嵌套辅助方法，以及为了调试而重构&lt;/h3&gt;
&lt;p&gt;这一部分将调试 &lt;code&gt;IntListExercises.java&lt;/code&gt; 中的 &lt;code&gt;setToZeroIfMaxFEL&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;这个方法会执行一项非常奇怪的任务。具体来说，如果且仅当从 IntList 中某个结点开始的子列表的最大值首位数字与末位数字相同，它就会把该结点的值替换为 0。因此，方法名中的 &lt;code&gt;FEL&lt;/code&gt; 是 “first equals last” 的缩写。&lt;/p&gt;
&lt;p&gt;例如，如果传入 IntList &lt;code&gt;55 -&gt; 22 -&gt; 45 -&gt; 44 -&gt; 5&lt;/code&gt;，它会把 55、44 和 5 的值设为零，使列表变成 &lt;code&gt;0 -&gt; 22 -&gt; 45 -&gt; 0 -&gt; 0&lt;/code&gt;。原因如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 55 开始的 IntList 最大值是 55，其首位数字与末位数字相同，因此这个值被设为零。&lt;/li&gt;
&lt;li&gt;从 22 开始的 IntList 最大值是 45，其首位数字与末位数字不同，因此 22 不变。&lt;/li&gt;
&lt;li&gt;从 45 开始的 IntList 最大值是 45，其首位数字与末位数字不同，因此 45 不变。&lt;/li&gt;
&lt;li&gt;从 44 开始的 IntList 最大值是 44，其首位数字与末位数字相同，因此 44 被设为零。&lt;/li&gt;
&lt;li&gt;从 5 开始的 IntList 最大值是 5，其首位数字与末位数字相同，因此 5 被设为零。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了测试你的理解，请考虑 IntList &lt;code&gt;5 -&gt; 535 -&gt; 35 -&gt; 11 -&gt; 10 -&gt; 0&lt;/code&gt;。调用 &lt;code&gt;setToZeroIfMaxFEL&lt;/code&gt; 后，列表应当是什么？查看 &lt;code&gt;SetToZeroIfMaxFELTest&lt;/code&gt; 中的 &lt;code&gt;testZeroOutFELMaxes3&lt;/code&gt;，以核对课程给出的答案。&lt;/p&gt;
&lt;p&gt;运行测试后，你会看到这个方法有 bug。具体来说，测试 3 会失败。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;setToZeroIfMaxFEL&lt;/code&gt; 的第一行设置断点，并且只调试 &lt;code&gt;testZeroOutFELMaxes3&lt;/code&gt;。要调试单个测试，请打开 &lt;code&gt;SetToZeroIfMaxFELTest.java&lt;/code&gt;，找到方法 &lt;code&gt;testZeroOutFELMaxes3()&lt;/code&gt;，然后单击方法定义左侧的绿色箭头图标。&lt;/p&gt;
&lt;p&gt;如果这个测试以前已经运行过，绿色箭头图标可能会变成一个绿色对勾和绿色箭头的组合（如果测试以前通过），或者变成一个红色感叹号和绿色箭头的组合（如果测试以前失败）。下面展示了后面这两种情况的示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/e0e9ff68b04d-how-to-run-a-single-test.png&quot; alt=&quot;运行单个测试&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用几次 Step Into，它会把你带到写有 &lt;code&gt;if (firstDigitEqualsLastDigit(max(p)))&lt;/code&gt; 的那一行。第三次单击 Step Into 时，你会看到 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt; 和 &lt;code&gt;max&lt;/code&gt; 同时被高亮显示，如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/f7dba6bc793f-debuggerPickAFunction.png&quot; alt=&quot;调试器要求选择函数&quot;&gt;&lt;/p&gt;
&lt;p&gt;由于这里存在嵌套函数调用，IntelliJ 正在询问你想进入哪个函数。你可以任选其一。如果单击 &lt;code&gt;max&lt;/code&gt;，就会看到 &lt;code&gt;max&lt;/code&gt; 调用的所有细节。如果单击 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt;，则会跨过对 &lt;code&gt;max&lt;/code&gt; 的调用。&lt;/p&gt;
&lt;p&gt;我个人认为，这样的代码很难调试！在这种情况下，我使用的一种策略是重构代码，让它更适合调试。让我们试一试！&lt;/p&gt;
&lt;p&gt;把代码改成下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int currentMax = max(p);
boolean firstEqualsLast = firstDigitEqualsLastDigit(currentMax);
if (firstEqualsLast) {
    p.first = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成修改后，使用 Step Over 找出是哪一次对 &lt;code&gt;max&lt;/code&gt; 或 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt; 的调用产生了错误答案。重要：在你找到某次产生错误答案的 &lt;code&gt;max&lt;/code&gt; 或 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt; 调用之前，不要使用 Step Into。否则你只是在浪费时间，逐行查看每一行代码。也就是说，如果你在观察 &lt;code&gt;max&lt;/code&gt; 函数每次调用的每次迭代，就说明你没有正确使用调试器！&lt;/p&gt;
&lt;p&gt;找到产生奇怪结果的 &lt;code&gt;max&lt;/code&gt; 或 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt; 调用后，从头重新开始调试；这一次，当再次到达那个曾产生奇怪结果的参数时，请单击 Step Into，而不是 Step Out。注意：在 Lab 4 中，我们会讨论一种叫作“条件断点”的实用思想，它可以让你不必从头重新开始。&lt;/p&gt;
&lt;p&gt;识别出 bug 后，修复它。你也可以把重构后的代码恢复为单行版本，即 &lt;code&gt;if (firstDigitEqualsLastDigit(max(p)))&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;注意：在真实世界中，理想情况下，你会在 &lt;code&gt;setToZeroIfMaxFEL&lt;/code&gt; 方法中使用 &lt;code&gt;max&lt;/code&gt; 和 &lt;code&gt;firstDigitEqualsLastDigit&lt;/code&gt; 之前，分别测试它们。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;C 部分：棘手的 IntList&lt;/h3&gt;
&lt;p&gt;这一部分将调试 &lt;code&gt;IntListExercises.java&lt;/code&gt; 中的 &lt;code&gt;squarePrimes&lt;/code&gt; 方法。这个方法的目标是接收一个 &lt;code&gt;IntList&lt;/code&gt;，把其中所有质数元素平方，并让合数（非质数）元素保持不变。如果至少有一个元素被平方，它返回 &lt;code&gt;true&lt;/code&gt;；否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如，考虑一个包含元素 14、15、16、17 和 18 的 &lt;code&gt;IntList&lt;/code&gt;。运行 &lt;code&gt;squarePrimes&lt;/code&gt; 后，我们希望质数元素 17 被平方，而合数元素 14、15、16 和 18 保持不变。&lt;code&gt;squarePrimes&lt;/code&gt; 函数的输出应当是 &lt;code&gt;true&lt;/code&gt;，因为它应当修改 &lt;code&gt;IntList&lt;/code&gt;，使其变为 &lt;code&gt;14, 15, 16, 289, 18&lt;/code&gt;。用代码表示如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/* Expected Behavior */

IntList lst = IntList.of(14, 15, 16, 17, 18);
System.out.println(lst.toString());
// Output: 14 -&gt; 15 -&gt; 16 -&gt; 17 -&gt; 18

boolean changed = squarePrimes(lst);
System.out.println(lst.toString());
// Output: 14 -&gt; 15 -&gt; 16 -&gt; 289 -&gt; 18

System.out.println(changed);
// Output: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;squarePrimes&lt;/code&gt; 方法使用函数 &lt;code&gt;Primes.isPrime(int x)&lt;/code&gt; 作为辅助方法。&lt;code&gt;isPrime&lt;/code&gt; 的作用很简单：如果参数是质数就返回 &lt;code&gt;true&lt;/code&gt;，如果参数是合数就返回 &lt;code&gt;false&lt;/code&gt;。你不需要关心它如何判断一个数是不是质数——那相当复杂！（可选：感兴趣的读者可以在网上查阅 “Fermat Primality Test”。）相反，我们希望你把 &lt;code&gt;isPrime&lt;/code&gt; 函数视为黑盒。&lt;/p&gt;
&lt;p&gt;调试时，请对 &lt;code&gt;isPrime&lt;/code&gt; 函数使用 “Step Over”。这样你就能验证它的输入和输出是否正确，而无须关心它的实现。&lt;/p&gt;
&lt;p&gt;上面已经解释了 &lt;code&gt;squarePrimes&lt;/code&gt; 方法应当做什么。不幸的是，&lt;code&gt;squarePrimes&lt;/code&gt; 方法有 bug。你的任务是找到并修复这个 bug！为此，我们建议你：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个或多个 JUnit 测试，针对多种不同输入测试 &lt;code&gt;squarePrimes&lt;/code&gt; 方法。务必测试它是否正确更新传入的 &lt;code&gt;IntList&lt;/code&gt;，以及是否返回正确的 &lt;code&gt;boolean&lt;/code&gt; 值。&lt;/li&gt;
&lt;li&gt;一旦创建出了一个会让 &lt;code&gt;squarePrimes&lt;/code&gt; 失败的 JUnit 测试，你就取得了进展！太棒了！现在使用 Java 调试器逐步执行有问题的代码并隔离 bug。&lt;/li&gt;
&lt;li&gt;最后，为 bug 编写修复代码。对于这个特定 bug，修复只需要很少几行代码。找到 bug 比修复它困难得多！&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了帮助你开始这项任务，我们已经创建了一个 JUnit 测试：&lt;code&gt;SquarePrimesTest.testSquarePrimesSimple&lt;/code&gt;。这个方法会检查上面给出的示例（包含 14、15、16、17 和 18 的 &lt;code&gt;IntList&lt;/code&gt;）是否被 &lt;code&gt;squarePrimes&lt;/code&gt; 函数正确修改，以及 &lt;code&gt;squarePrimes&lt;/code&gt; 函数是否返回正确的值（在这个例子中是 &lt;code&gt;true&lt;/code&gt;）。不幸的是，这个测试会通过：你必须另外编写一个会失败的测试！你可以把 &lt;code&gt;testSquarePrimesSimple&lt;/code&gt; 作为编写 JUnit 测试的示例。&lt;/p&gt;
&lt;p&gt;祝你好运！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;和以前一样，把代码推送到 GitHub，并提交到 Gradescope 来测试代码。你会注意到，有一些测试显示为 “Hidden”。这意味着我们不会向你透露测试做了什么；如果测试失败，我们会故意给出一条含糊的错误消息。这是因为在本 Lab 中，我们希望你专注于学习如何自行调试，而不是依赖自动评分器提供的详细消息。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;完整回顾&lt;/h2&gt;
&lt;p&gt;本 Lab 介绍了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 IntelliJ 调试器中 Step Into、Step Over 和 Step Out（这些功能在项目中会很有用！）；&lt;/li&gt;
&lt;li&gt;单元测试（整体概念）；&lt;/li&gt;
&lt;li&gt;JUnit 的语法与细节；&lt;/li&gt;
&lt;li&gt;编写 JUnit 测试；&lt;/li&gt;
&lt;li&gt;使用 JUnit 进行调试；&lt;/li&gt;
&lt;li&gt;运行代码风格检查器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;FAQ 与常见问题&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;String 或 String.equals() 之类的内容变红了&lt;/h3&gt;
&lt;p&gt;这是 JDK 问题。请前往 &lt;code&gt;File &gt; Project Structure &gt; Project &gt; Project SDK&lt;/code&gt; 进行排查。如果你的 Java 版本是 15.0，就应当拥有 15.0 SDK，并把 “Project Language Level” 设为 Level 15。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab2/lab2&quot;&gt;https://sp21.datastructur.es/materials/lab/lab2/lab2&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 3：计时与随机测试</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-3-timing-randomized-tests</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-3-timing-randomized-tests</guid><description>CS61B Spring 2021 Lab 3：计时与随机测试中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E4%BB%8B&quot;&gt;简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#list61b-%E7%9A%84%E8%AE%A1%E6%97%B6%E6%B5%8B%E8%AF%95&quot;&gt;List61B 的计时测试&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%BA%E4%BD%BF%E7%94%A8%E7%B3%9F%E7%B3%95%E6%89%A9%E5%AE%B9%E7%AD%96%E7%95%A5%E7%9A%84-alist-%E6%9E%84%E9%80%A0%E8%BF%87%E7%A8%8B%E8%AE%A1%E6%97%B6&quot;&gt;为使用糟糕扩容策略的 AList 构造过程计时&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%BA%E4%BD%BF%E7%94%A8%E8%89%AF%E5%A5%BD%E6%89%A9%E5%AE%B9%E7%AD%96%E7%95%A5%E7%9A%84-alist-%E6%9E%84%E9%80%A0%E8%BF%87%E7%A8%8B%E8%AE%A1%E6%97%B6&quot;&gt;为使用良好扩容策略的 AList 构造过程计时&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%BA-sllist-%E7%9A%84-getlast-%E6%96%B9%E6%B3%95%E8%AE%A1%E6%97%B6&quot;&gt;为 SLList 的 getLast 方法计时&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%9A%8F%E6%9C%BA%E6%AF%94%E8%BE%83%E6%B5%8B%E8%AF%95&quot;&gt;随机比较测试&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E5%8D%95%E6%AF%94%E8%BE%83%E6%B5%8B%E8%AF%95&quot;&gt;简单比较测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%9A%8F%E6%9C%BA%E5%87%BD%E6%95%B0%E8%B0%83%E7%94%A8&quot;&gt;随机函数调用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%9D%A1%E4%BB%B6%E6%96%AD%E7%82%B9&quot;&gt;条件断点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B7%BB%E5%8A%A0%E6%9B%B4%E5%A4%9A%E9%9A%8F%E6%9C%BA%E8%B0%83%E7%94%A8&quot;&gt;添加更多随机调用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B7%BB%E5%8A%A0%E9%9A%8F%E6%9C%BA%E6%AF%94%E8%BE%83&quot;&gt;添加随机比较&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%BF%90%E8%A1%8C%E9%9A%8F%E6%9C%BA%E6%B5%8B%E8%AF%95&quot;&gt;运行随机测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BF%AE%E5%A4%8D-bug-%E4%B8%8E%E5%BC%82%E5%B8%B8%E6%96%AD%E7%82%B9&quot;&gt;修复 bug 与异常断点&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B8%85%E7%90%86&quot;&gt;清理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E8%AE%BA&quot;&gt;结论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%8F%90%E4%BA%A4&quot;&gt;提交&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在本 Lab 中，你将为 &lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt; 类创建计时测试。你还将为一个有 bug 的 &lt;code&gt;AList&lt;/code&gt; 实现创建随机比较测试。&lt;/p&gt;
&lt;p&gt;在这个过程中，我们还会探索调试器的三个新功能：条件断点、Resume 按钮和异常断点。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;List61B 的计时测试&lt;/h2&gt;
&lt;p&gt;对于本 Lab 的这一部分，请确保打开的是 &lt;code&gt;timingtest&lt;/code&gt; 包中的代码，而不是 &lt;code&gt;randomizedtest&lt;/code&gt; 包中的代码。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;为使用糟糕扩容策略的 AList 构造过程计时&lt;/h3&gt;
&lt;p&gt;正如 &lt;a href=&quot;https://docs.google.com/presentation/d/1ZKSPKdEjlLlzmf7LoQJlTUC3w0MPInSXy2DxTEva0yo/edit&quot;&gt;Lecture&lt;/a&gt; 中讨论的那样，乘法式扩容策略会带来快速的添加操作／良好的性能，而加法式扩容策略会带来缓慢的添加操作／糟糕的性能。在本 Lab 的这一部分中，我们将展示如何通过实验说明这一点。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;timingtest&lt;/code&gt; 包中，我们提供了 Lecture 中创建的 &lt;code&gt;AList&lt;/code&gt; 类，并使用下面这种糟糕的扩容策略：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(Item x) {
    if (size == items.length) {
        resize(size + 1);
    }

    items[size] = x;
    size = size + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本部分的目标是编写代码，制成一张表格，显示使用上面的 &lt;code&gt;addLast&lt;/code&gt; 方法创建不同大小的 &lt;code&gt;AList&lt;/code&gt; 所需的时间。这个计时测试的输出大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Timing table for addLast
           N     time (s)        # ops  microsec/op
------------------------------------------------------------
        1000         0.00         1000         0.20
        2000         0.01         2000         0.20
        4000         0.01         4000         1.20
        8000         0.04         8000         4.30
       16000         0.10        16000        10.00
       32000         0.50        32000        49.70
       64000         1.15        64000       114.80
      128000         3.74       128000       374.30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一列 &lt;code&gt;N&lt;/code&gt; 给出数据结构的大小（它包含多少个元素）。第二列 &lt;code&gt;time (s)&lt;/code&gt; 给出完成所有操作所需的时间。第三列 &lt;code&gt;# ops&lt;/code&gt; 给出计时实验期间对 &lt;code&gt;addLast&lt;/code&gt; 的调用次数。最后，第四列 &lt;code&gt;microsec/op&lt;/code&gt; 给出每次 &lt;code&gt;addLast&lt;/code&gt; 调用平均需要多少微秒。&lt;/p&gt;
&lt;p&gt;注意，在这个实验中，&lt;code&gt;N&lt;/code&gt; 和 &lt;code&gt;# ops&lt;/code&gt; 是重复信息，因为调用 &lt;code&gt;addLast&lt;/code&gt; 128,000 次后，得到的 &lt;code&gt;N&lt;/code&gt; 也会是 128,000。&lt;/p&gt;
&lt;p&gt;这里需要注意的重要现象是，&lt;code&gt;addLast&lt;/code&gt; 并不是“常数时间”的。也就是说，每次 &lt;code&gt;addLast&lt;/code&gt; 调用完成所需的时间会随列表大小显著变化：列表很长时需要 374.30 微秒，而列表很短时只需要 0.20 微秒。Project 1 中针对 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 类的自动评分测试，本质上就是这样工作的：对于那些本应当是常数时间的操作，我们会确保它们耗费的时间确实保持不变。&lt;/p&gt;
&lt;p&gt;你可能注意到，在 N = 1000 和 N = 2000 时，每次 &lt;code&gt;addLast&lt;/code&gt; 操作的时间相同。这在计时测试中很常见。对于较小的输入，结果不可靠，原因有两个：运行时间的方差很大（这是由缓存、进程切换、分支预测等问题造成的；如果学习 61C，你会了解这些内容），而且计时器的精度（毫秒）不足以分辨 N = 1000 和 N = 2000 之间的差异。&lt;/p&gt;
&lt;p&gt;这甚至可能导致 N = 1000 的运行时间大于 N = 2000。因此，在运行经验计时测试时，我们希望把注意力放在较大的 N 上，例如比较 N = 32000 与 N = 64000 时的行为。&lt;/p&gt;
&lt;p&gt;你可能还会注意到，在自己的计算机上得到的表格时间可能与上面所写的差别很大。没有关系，只要总体趋势相同即可。在 61C 中，你会具体了解到为什么同一段代码在不同硬件上可能花费大不相同的时间。在 61B（以及大多数以理论为基础的课程）中，我们只关心总体趋势，这种趋势会掩盖所用处理器类型等非理想因素。&lt;/p&gt;
&lt;p&gt;虽然对“总体趋势”进行推理可能显得有些棘手，但课程稍后会学习一种用于处理它的形式体系（渐近分析）。现在先使用你的直觉！&lt;/p&gt;
&lt;p&gt;既然已经理解上面的表格，请在 &lt;code&gt;TimeAList&lt;/code&gt; 类中添加函数 &lt;code&gt;public void timeAListConstruction&lt;/code&gt;，让它针对 &lt;code&gt;AList&lt;/code&gt; 生成上面的表格。注意：如果你的计算机稍慢，可以在 64,000 停止，而不是运行到 128,000。务必在 &lt;code&gt;TimeAList&lt;/code&gt; 类的 &lt;code&gt;main&lt;/code&gt; 方法中加入对 &lt;code&gt;timeAListConstruction&lt;/code&gt; 的调用。&lt;/p&gt;
&lt;p&gt;为了方便起见，我们提供了一个名为 &lt;code&gt;printTimingTable(AList&amp;#x3C;Integer&gt; Ns, AList&amp;#x3C;Double&gt; times, AList&amp;#x3C;Integer&gt; opCounts)&lt;/code&gt; 的方法，用于打印上面的表格。其中，&lt;code&gt;Ns&lt;/code&gt; 是第一列，&lt;code&gt;times&lt;/code&gt; 是第二列，&lt;code&gt;opCounts&lt;/code&gt; 是第三列。第四列（&lt;code&gt;microsec/op&lt;/code&gt;）会自动为你计算。时间值应当以秒为单位。你应当使用 &lt;code&gt;Stopwatch&lt;/code&gt; 类。请查看 &lt;code&gt;stopwatchDemo&lt;/code&gt; 中的示例。&lt;/p&gt;
&lt;p&gt;有趣的是，请注意我们正在使用被计时的同一种数据结构（&lt;code&gt;AList&lt;/code&gt;）来存储计时数据！我们通过使用这个数据结构的许多不同实例来做到这一点；正在接受计时的那个实例与存储计时数据的那些实例并不相同。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;为使用良好扩容策略的 AList 构造过程计时&lt;/h3&gt;
&lt;p&gt;现在修改 &lt;code&gt;AList&lt;/code&gt; 类，让扩容策略从加法式变为乘法式，然后重新运行 &lt;code&gt;timeAListConstruction&lt;/code&gt;。此时，即使 N = 128,000，&lt;code&gt;AList&lt;/code&gt; 对象也应当几乎瞬间完成构造，每次添加操作应该只耗费几分之一个微秒。&lt;/p&gt;
&lt;p&gt;可选：尝试把最大 N 提高到更大的值，例如一千万。你应该会看到，每次添加操作的时间仍然保持不变。&lt;/p&gt;
&lt;p&gt;可选：尝试使用不同的扩容因子，并观察运行时间如何变化。例如，如果使用 1.01 倍扩容，你仍然应当得到常数时间的 &lt;code&gt;addLast&lt;/code&gt; 操作！注意，要使用非整数因子，你需要把结果转换为最近的整数。例如，可以使用 &lt;code&gt;Math.round()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addLast(Item x) {
    if (size == items.length) {
        resize((int) (size * 1.01));
    }

    items[size] = x;
    size = size + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;为 SLList 的 getLast 方法计时&lt;/h3&gt;
&lt;p&gt;上面展示了怎样为一个数据结构的构造过程计时。不过，有时我们关心的是：一个方法的运行时间如何依赖于某个已经构造好的数据结构的大小。&lt;/p&gt;
&lt;p&gt;例如，在你的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 中，&lt;code&gt;addLast&lt;/code&gt; 操作应当很快……一次 &lt;code&gt;addLast&lt;/code&gt; 操作必须花费“常数时间”，也就是说，执行时间不应当依赖于 deque 的大小。&lt;/p&gt;
&lt;p&gt;在本 Lab 的这一部分中，我们将展示怎样通过实验测试一个方法的运行时间是否依赖于数据结构大小。&lt;/p&gt;
&lt;p&gt;假设我们想计算 &lt;code&gt;SLList&lt;/code&gt; 上 &lt;code&gt;getLast&lt;/code&gt; 每次操作所需的时间，并想知道这个运行时间如何依赖于 N。为此，需要遵循下面的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个 &lt;code&gt;SLList&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;向 &lt;code&gt;SLList&lt;/code&gt; 添加 N 个项目。&lt;/li&gt;
&lt;li&gt;启动计时器。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;SLList&lt;/code&gt; 执行 M 次 &lt;code&gt;getLast&lt;/code&gt; 操作。&lt;/li&gt;
&lt;li&gt;查看计时器。这样就得到了完成全部 M 次操作所需的总时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;非常重要的一点是：只有在第 2 步完成后，才能启动计时器。否则，计时测试会包含构建数据结构所需的运行时间，而我们只关心 &lt;code&gt;getLast&lt;/code&gt; 的运行时间如何依赖于 &lt;code&gt;SLList&lt;/code&gt; 的大小。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;TimeSLList&lt;/code&gt; 类中编辑函数 &lt;code&gt;timeGetLast&lt;/code&gt;，使其执行上面的过程，并生成一张类似下面的表格：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Timing table for getLast
           N     time (s)        # ops  microsec/op
------------------------------------------------------------
        1000         0.02        10000         1.70
        2000         0.03        10000         3.10
        4000         0.06        10000         6.20
        8000         0.13        10000        12.50
       16000         0.25        10000        25.00
       32000         0.53        10000        52.80
       64000         1.35        10000       135.30
      128000         2.57        10000       257.30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，&lt;code&gt;N&lt;/code&gt; 与 &lt;code&gt;# ops&lt;/code&gt; 两列不再相同。这是因为，无论列表大小如何，我们始终调用相同次数的 &lt;code&gt;getLast&lt;/code&gt;；也就是说，在上述过程的第 4 步中，&lt;code&gt;M = 10000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;注意，这些操作再次不是常数时间的！（如果你的结果表明操作是常数时间的，请确保你是在 &lt;code&gt;SLList&lt;/code&gt; 上而不是在 &lt;code&gt;AList&lt;/code&gt; 上运行测试！）这意味着，列表变大后，&lt;code&gt;getLast&lt;/code&gt; 操作会变慢。在真实世界的应用中，这会是一个严重的问题。例如，假设列表记录 ATM 交易，而调用 &lt;code&gt;getLast&lt;/code&gt; 操作是为了获取最近一笔交易并打印收据。&lt;/p&gt;
&lt;p&gt;每使用一次 ATM，下一张收据就会多花一点时间才能打印出来。最终，经过几个月或几年之后，列表会变得非常大，以至于 &lt;code&gt;getLast&lt;/code&gt; 操作慢到无法使用。虽然这是一个特意构造的例子，但类似的问题确实困扰过真实世界中的系统！&lt;/p&gt;
&lt;p&gt;因此，你在 Project 1 中构建的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 必须具有与数据结构大小无关的运行时间。换句话说，最后一列应当是某个近似不变的值。&lt;/p&gt;
&lt;p&gt;可选思考题：为什么 &lt;code&gt;getLast&lt;/code&gt; 如此缓慢？你的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 有什么特殊之处，能够让 &lt;code&gt;getLast&lt;/code&gt; 函数更快？&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;随机比较测试&lt;/h2&gt;
&lt;p&gt;对于本 Lab 的这一部分，请确保使用的是 &lt;code&gt;randomizedtest&lt;/code&gt; 包中的代码，而不是 &lt;code&gt;timingtest&lt;/code&gt; 包中的代码。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;简单比较测试&lt;/h4&gt;
&lt;p&gt;一种测试代码的技术是进行“比较测试”。在这种测试中，我们拥有同一个类的两种实现。其中一种实现已知（或被强烈认为）是正确的，另一种实现仍在开发中，尚未得到验证。&lt;/p&gt;
&lt;p&gt;例如，我们提供了 &lt;code&gt;AListNoResizing&lt;/code&gt; 类。这个类不支持任何扩容操作，只是使用一个硬编码为 1000 的数组大小。这意味着它在实践中没有多大用处，因为永远无法保存超过 1000 个项目。不过，由于它极其简单，我们对它能够正常工作抱有很高信心。&lt;/p&gt;
&lt;p&gt;相对地，我们还提供了 &lt;code&gt;BuggyAList&lt;/code&gt; 类。这个类有一个底层数组，会根据所存储的数据量扩大和缩小。由于正确实现扩缩容稍微有些棘手，我们对这个类的正确性更加怀疑。正如名称所暗示的那样，它确实在某处有一个 bug。本 Lab 剩余部分的目标就是找出这个 bug。&lt;/p&gt;
&lt;p&gt;让我们先编写一个练习 JUnit 测试作为热身。编写名为 &lt;code&gt;testThreeAddThreeRemove&lt;/code&gt; 的测试：向正确的 AList 实现和有 bug 的 AList 实现添加相同的值，然后检查后续三次 &lt;code&gt;removeLast&lt;/code&gt; 调用的结果是否相同。例如，可以先对两者都执行 &lt;code&gt;addLast(4)&lt;/code&gt;，再对两者都执行 &lt;code&gt;addLast(5)&lt;/code&gt;，然后对两者都执行 &lt;code&gt;addLast(6)&lt;/code&gt;。接着，对两者执行 &lt;code&gt;removeLast&lt;/code&gt;，并验证结果相等。再次对两者执行 &lt;code&gt;removeLast&lt;/code&gt;，并检查结果相等。&lt;/p&gt;
&lt;p&gt;最后，移除最后一个项目（即 4），并验证两者结果相等。完成后，或者在卡住时，可以查看&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab3/testThreeAddThreeRemove.txt&quot;&gt;这个链接中的代码&lt;/a&gt;获得一种解法，但请先尝试在不查看代码的情况下完成！&lt;/p&gt;
&lt;p&gt;运行测试，你会看到它应当通过。这个测试还不足以识别 &lt;code&gt;BuggyAList&lt;/code&gt; 中的 bug。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;随机函数调用&lt;/h4&gt;
&lt;p&gt;原则上，可以精心编写一组比较测试，最终找出 bug。不过，另一种可以配合使用的策略是采用随机方法：对两个实现进行随机调用，并使用 JUnit 方法验证它们始终返回相同的值。&lt;/p&gt;
&lt;p&gt;下面的代码是一个随机调用 &lt;code&gt;AList&lt;/code&gt; 方法的函数示例。它会在一个 &lt;code&gt;AListNoResizing&lt;/code&gt; 对象上随机调用 &lt;code&gt;addLast&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt;，总共对这两个函数之一进行 N 次调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;AListNoResizing&amp;#x3C;Integer&gt; L = new AListNoResizing&amp;#x3C;&gt;();

int N = 500;
for (int i = 0; i &amp;#x3C; N; i += 1) {
    int operationNumber = StdRandom.uniform(0, 2);
    if (operationNumber == 0) {
        // addLast
        int randVal = StdRandom.uniform(0, 100);
        L.addLast(randVal);
        System.out.println(&quot;addLast(&quot; + randVal + &quot;)&quot;);
    } else if (operationNumber == 1) {
        // size
        int size = L.size();
        System.out.println(&quot;size: &quot; + size);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;randomizedTest()&lt;/code&gt; 的新 JUnit 测试，复制并粘贴上面的代码，你应该会看到类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;size: 0
addLast(68)
size: 1
size: 1
addLast(12)
addLast(19)
addLast(79)
...
size: 265
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;条件断点&lt;/h4&gt;
&lt;p&gt;虽然下面的功能对今天寻找 bug 没有帮助，但让我们介绍两个新的调试器功能：“Resume” 和“条件断点”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在写有 &lt;code&gt;int operationNumber = StdRandom.uniform(0, 2);&lt;/code&gt; 的那一行设置断点。&lt;/li&gt;
&lt;li&gt;然后使用 Debug 选项，让 IntelliJ 在这一行停下来。如果不记得怎样使用 Debug 选项，请查看 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab2/lab2&quot;&gt;Lab 2&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;单击 Visualizer，你会看到一个含有许多许多 &lt;code&gt;null&lt;/code&gt; 的数组；这些位置最终会保存加入列表的数据。&lt;/li&gt;
&lt;li&gt;单击 Step Over，你会看到 &lt;code&gt;operationNumber&lt;/code&gt; 被设为 0 或 1。这是因为 &lt;code&gt;StdRandom.uniform(0, 2)&lt;/code&gt; 函数会返回范围 [0, 2) 中的一个随机整数，也就是说，不包括右侧参数。如果选中的数是 0，就会把一个随机数加入列表末尾。如果选中的数是 1，就会打印列表大小。&lt;/li&gt;
&lt;li&gt;单击调试器中的 &lt;code&gt;Resume&lt;/code&gt; 按钮（下图以黄色高亮），代码会继续运行，直到再次命中断点。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/c4a7e0af85d0-resume_button.png&quot; alt=&quot;Resume 按钮&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;尝试单击 Resume 几次，你会看到数组中的位置开始被值填充。注意，每次单击 Resume，代码都会一直运行（就像连续按了很多次 Step Over），直到再次回到断点。&lt;/li&gt;
&lt;li&gt;我们还可以离开 Visualizer，重新查看打印语句的输出。为此，再次单击 &lt;code&gt;Debugger&lt;/code&gt;（位于 &lt;code&gt;Java Visualizer&lt;/code&gt; 旁边），然后继续单击 Resume。在某些计算机上，你可能需要单击 &lt;code&gt;Console&lt;/code&gt;，而不是 &lt;code&gt;Debugger&lt;/code&gt;。每次单击 Resume 时，都会看到另一条打印语句，对应一次 &lt;code&gt;addLast&lt;/code&gt; 或 &lt;code&gt;size&lt;/code&gt; 调用。&lt;/li&gt;
&lt;li&gt;现在让我们尝试条件断点。右键单击断点，会看到一个写有 “Condition:” 的弹出框。在输入框中输入 &lt;code&gt;L.size() == 12&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/deda8b86fe36-conditional_breakpoint.png&quot; alt=&quot;条件断点&quot;&gt;&lt;/p&gt;
&lt;ol start=&quot;9&quot;&gt;
&lt;li&gt;单击 Resume，代码会一直运行到断点条件满足，也就是大小等于 12。试着执行并单击 Visualizer，你应该会看到此时大小为 12，数组中有 12 个项目。如果不小心执行过头，很遗憾，必须重新启动测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两个新功能（Resume 和条件断点）在 Lab 3 的剩余部分中不会再有用。不过，它们在未来的项目中可能会派上用场，而且 Lab 4 会要求你使用它们。此时应当移除这个条件断点，以免影响 Lab 的剩余部分。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;添加更多随机调用&lt;/h4&gt;
&lt;p&gt;修改随机测试，让它现在还可能执行另外两个操作：&lt;code&gt;getLast&lt;/code&gt; 和 &lt;code&gt;removeLast&lt;/code&gt;。注意，需要修改对 &lt;code&gt;StdRandom.uniform&lt;/code&gt; 的调用，让它在 0 到 3 之间选择一个数。&lt;/p&gt;
&lt;p&gt;重要：只有在 &lt;code&gt;L.size&lt;/code&gt; 大于 0 时，才应当调用 &lt;code&gt;getLast&lt;/code&gt; 和 &lt;code&gt;removeLast&lt;/code&gt;！如果大小为 0，这些方法会导致程序崩溃。换句话说，请使用一条 &lt;code&gt;if&lt;/code&gt; 语句，在大小为零时跳过对 &lt;code&gt;getLast&lt;/code&gt; 或 &lt;code&gt;removeLast&lt;/code&gt; 的调用。&lt;/p&gt;
&lt;p&gt;可选：加入这些方法后，使用断点让代码在调用 &lt;code&gt;removeLast&lt;/code&gt; 的那一行停下来，再使用调试器的 Step Over 功能说服自己 &lt;code&gt;removeLast&lt;/code&gt; 看起来工作正常。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;添加随机比较&lt;/h4&gt;
&lt;p&gt;上面构建的代码只会调用我们已知正确的实现。修改代码，让每次调用 &lt;code&gt;AListNoResizing&lt;/code&gt; 的方法时，也调用 &lt;code&gt;TestBuggyAList&lt;/code&gt; 中的相同方法。代码还应当比较每个具有返回值的方法所返回的值。&lt;/p&gt;
&lt;p&gt;如果卡住了，请查看&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab3/partialRandomizedComparisons.txt&quot;&gt;这个链接中的代码&lt;/a&gt;获得一个部分解法，但请先尝试在不查看代码的情况下完成！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;运行随机测试&lt;/h4&gt;
&lt;p&gt;尝试运行测试若干次。它有时可能通过，有时可能失败。现在尝试把 N 增大到 5000。此时它几乎每次都应当失败。&lt;/p&gt;
&lt;p&gt;这揭示了随机测试的一个重要特点：如果应用的是随机操作，而 bug 又相当隐蔽，那么随机操作序列可能检测不到 bug！有一些方法可以改进随机测试来避免这个问题，但它们超出了本课程的范围。&lt;/p&gt;
&lt;p&gt;另一点需要注意：随机测试不应当替代设计良好的单元测试！在可能的情况下，我个人通常更倾向于使用非随机测试，并把随机测试看作一种补充测试方法。关于这个问题的争论，可以查看&lt;a href=&quot;https://news.ycombinator.com/item?id=24349522&quot;&gt;这个讨论串&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;修复 bug 与异常断点&lt;/h4&gt;
&lt;p&gt;既然现在有了一个会失败的测试，我们就想了解它为什么失败。&lt;/p&gt;
&lt;p&gt;你会注意到，测试每次失败时，得到的消息大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 7

at randomizedtest.BuggyAList.resize(BuggyAList.java:31)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一种处理方式是在 &lt;code&gt;BuggyAList.java&lt;/code&gt; 第 31 行设置条件为 &lt;code&gt;i == items.length&lt;/code&gt; 的条件断点。这样完全可行，也欢迎你尝试。&lt;/p&gt;
&lt;p&gt;不过，我们将借此机会展示怎样设置一个“异常断点”，使代码崩溃时能够停下来，并把正在发生的事情可视化。&lt;/p&gt;
&lt;p&gt;要做到这一点，请单击 &lt;code&gt;Run -&gt; View Breakpoints&lt;/code&gt;。你应该会看到如下窗口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/ab1bb5215644-exception_breakpoint_1.png&quot; alt=&quot;查看断点窗口&quot;&gt;&lt;/p&gt;
&lt;p&gt;选中左侧写有 “Any exception” 的复选框，然后单击写有 “Condition:” 的位置，并在输入框中准确输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;this instanceof java.lang.ArrayIndexOutOfBoundsException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后，断点窗口应当如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/edda3a37aafd-exception_breakpoint_2.png&quot; alt=&quot;异常断点条件&quot;&gt;&lt;/p&gt;
&lt;p&gt;单击 Debug 按钮，代码应当恰好在异常即将发生的时刻停止。单击 Visualizer，并尝试弄清楚代码为什么崩溃。现在，真正的问题求解可以开始了！&lt;/p&gt;
&lt;p&gt;Visualizer 窗口中包含许多关于具体问题的线索。这是一个棘手而微妙的问题，但只要系统地进行分析，你应该能够识别它。如果卡住了，请高亮查看下面一段原页面中的隐藏文字：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;把注意力放在传给 &lt;code&gt;resize&lt;/code&gt; 函数的参数上。尝试弄清楚这个参数有什么问题，再利用这条信息，通过查看 &lt;code&gt;removeLast&lt;/code&gt; 的代码判断那个错误参数是怎样被传进去的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;识别出 bug 后，修复它。重新运行测试，验证 &lt;code&gt;BuggyAList&lt;/code&gt; 现在能够正确工作。&lt;/p&gt;
&lt;p&gt;如果遇到困难，务必向你的 GSI 或其他学生求助！&lt;/p&gt;
&lt;p&gt;**注意：**如果在没有指定条件的情况下使用这个调试功能，代码会在各种神秘位置停下来。请确保绝不要在未指定条件时选中 “Any Exception”。这是因为启动 JUnit 测试的过程会生成一系列异常，而这些异常最终都会被忽略。这远远超出了本课程的范围。如果已经使用完异常断点，应当取消选中左上角的 “Java Exceptions Breakpoints” 复选框。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;清理&lt;/h4&gt;
&lt;p&gt;最后：我们的 JUnit 测试中包含打印语句，用于生成所有随机调用的日志。虽然这对教学有帮助，但真实世界的测试不应当这样做，因为它只会产生一大团没有用处的文本。请移除 JUnit 测试中的所有打印语句。&lt;/p&gt;
&lt;p&gt;注意：通常而言，与其打印随机测试所进行的函数调用，不如把它们记录到日志中。要了解更多内容，请完成 Project 1 的额外加分作业。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;在本 Lab 中，我们学习了如何：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过实验测量构造一个数据结构所需的时间；&lt;/li&gt;
&lt;li&gt;通过实验测量一个数据结构的方法运行时间如何随数据结构大小变化；&lt;/li&gt;
&lt;li&gt;在同一个类的两种实现之间进行比较测试；&lt;/li&gt;
&lt;li&gt;随机调用类中的方法；&lt;/li&gt;
&lt;li&gt;在同一个类的两种实现之间进行随机比较测试；&lt;/li&gt;
&lt;li&gt;使用 IntelliJ 中的 Resume 按钮；&lt;/li&gt;
&lt;li&gt;为断点添加条件；&lt;/li&gt;
&lt;li&gt;创建异常断点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;与平常一样，把代码提交给自动评分器。自动评分器会检查计时测试的输出以及 &lt;code&gt;BuggyAList&lt;/code&gt; 类的正确性。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab3/lab3&quot;&gt;https://sp21.datastructur.es/materials/lab/lab3/lab3&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 4：Git 与调试</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-4-git-debugging</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-4-git-debugging</guid><description>CS61B Spring 2021 Lab 4：Git 与调试中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#lab-%E5%89%8D%E5%87%86%E5%A4%87&quot;&gt;Lab 前准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AE%80%E4%BB%8B&quot;&gt;简介&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#git-%E8%83%8C%E6%99%AF%E7%9F%A5%E8%AF%86&quot;&gt;Git 背景知识&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#git-%E7%BB%83%E4%B9%A0&quot;&gt;Git 练习&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-1-%E6%AD%A5&quot;&gt;第 1 步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-2-%E6%AD%A5&quot;&gt;第 2 步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-3-%E6%AD%A5&quot;&gt;第 3 步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-4-%E6%AD%A5&quot;&gt;第 4 步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-5-%E6%AD%A5&quot;&gt;第 5 步&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%AC%AC-6-%E6%AD%A5&quot;&gt;第 6 步&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%80%E4%B8%AA%E8%B0%83%E8%AF%95%E8%B0%9C%E9%A2%98&quot;&gt;一个调试谜题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%8F%90%E4%BA%A4&quot;&gt;提交&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%8C%E6%95%B4%E5%9B%9E%E9%A1%BE&quot;&gt;完整回顾&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Lab 前准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;**暂时不要从 skeleton 仓库拉取代码。**从 skeleton 仓库拉取会产生合并冲突，你稍后会在本 Lab 中学习怎样修复。即使你已经知道怎样解决合并冲突，我们也希望你使用一种非常具体的方式进行修复，所以请不要忍不住现在就 pull！&lt;/li&gt;
&lt;li&gt;如果已经从 skeleton 拉取过代码，请联系你的 TA，询问如何继续完成本 Lab。&lt;/li&gt;
&lt;li&gt;本 Lab 要求你已经在 GS 上的 &lt;code&gt;lab1&lt;/code&gt; 中获得满分。如果尚未满分，请在继续之前联系 TA。&lt;/li&gt;
&lt;li&gt;本 Lab 将使用 Git。开始之前，先确保你的仓库已与 GitHub 同步。只需运行：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git push origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果命令成功，就可以继续！如果没有成功，你很可能看到了类似&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab4/&quot;&gt;这个错误&lt;/a&gt;的消息。请按照链接中的说明修复错误。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;由于这是期中考试周，本 Lab 会比平常短一些。你将在本 Lab 中更深入地学习 Git，并通过一个很酷的练习再次练习调试。完成本 Lab 后，你应当会对 Git 工作流更加熟练，调试技能也应当更加精细！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Git 背景知识&lt;/h2&gt;
&lt;p&gt;这一部分需要观看一系列讲解 Git 主要概念的视频，然后亲手练习使用 Git。请观看下面全部六个视频。众所周知，Itai 说话比较慢，所以需要时可以随意提高播放速度！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=yWBzCAY_5UI&quot;&gt;Git Intro - Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=CnMpARAOhFg&quot;&gt;Git Intro - Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=t0tzTcZESWk&quot;&gt;Git Intro - Part 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ca1oCEMQGRQ&quot;&gt;Git Intro - Part 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=dZbj9gjjYv8&quot;&gt;Git Intro - Part 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=r0oHi0vXhLE&quot;&gt;Git Intro - Part 6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;具体来说，看完视频后，你应当理解以下概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地 Git 工作流：&lt;code&gt;git add&lt;/code&gt; 和 &lt;code&gt;git commit&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;git checkout&lt;/code&gt; 在不同提交之间移动并更新文件；&lt;/li&gt;
&lt;li&gt;Detached &lt;code&gt;HEAD&lt;/code&gt; 状态；&lt;/li&gt;
&lt;li&gt;远程仓库，例如托管在 &lt;a href=&quot;https://github.com/&quot;&gt;GitHub&lt;/a&gt; 上的仓库；&lt;/li&gt;
&lt;li&gt;本地 Git 与远程仓库的集成：&lt;code&gt;origin/master&lt;/code&gt; 和 &lt;code&gt;skeleton/master&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;怎样解决合并冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果对上述概念有任何疑问，可以参考 &lt;a href=&quot;https://sp19.datastructur.es/materials/guides/using-git.html&quot;&gt;Sarah 的 Git 指南&lt;/a&gt;、&lt;a href=&quot;https://sp19.datastructur.es/materials/guides/git-wtfs.html&quot;&gt;Git WTFS&lt;/a&gt;（Git Weird Technical Failure Scenarios——别想歪了！），也可以随时请 TA 解释这些概念。&lt;/p&gt;
&lt;p&gt;我们要在这里警告你：对网上找到的 Git 信息保持警惕，因为它们并不都来自可信来源。另外，当你在 Git 上遇到困难时，务必&lt;strong&gt;绝不要&lt;/strong&gt;复制网上找到的命令或朋友发给你的命令——有疑问时，一定要询问 TA。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Git 练习&lt;/h2&gt;
&lt;p&gt;现在该练习刚刚学到的内容了。请记住，本 Lab 要求你已经在 Gradescope 的 Lab 1 Autograder 中获得满分。如果尚未完成 Lab 1，请咨询 TA。准备好后，请使用下面的命令拉取起始代码；出现合并冲突时不要惊慌！请继续阅读以了解更多信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们故意在你的 &lt;code&gt;lab1&lt;/code&gt; 目录中引入了一个合并冲突。这个练习的目标是让你练习几个 Git 概念，其中包括合并冲突、Detached HEAD 状态，以及在你的 &lt;code&gt;sp21-s***&lt;/code&gt; 仓库中 checkout 文件。完成这个练习后，你应当会更熟悉在命令行中沿提交树移动，从而让文件保持在期望状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lab1&lt;/code&gt; 目录中的合并冲突位于 &lt;code&gt;lab1/Collatz.java&lt;/code&gt; 文件。你在 Lab 1 中更新过这个文件，让它打印从 &lt;code&gt;n = 5&lt;/code&gt; 开始的 &lt;a href=&quot;https://en.wikipedia.org/wiki/Collatz_conjecture&quot;&gt;Collatz 序列&lt;/a&gt;。在拉取 &lt;code&gt;lab4&lt;/code&gt; 起始代码之前，你的解法能够正确打印 Collatz 序列。&lt;code&gt;skeleton&lt;/code&gt; 仓库后来被更新，其中包含下面这个有 bug 的 &lt;code&gt;nextNumber(int n)&lt;/code&gt; 方法实现；该方法应当返回 Collatz 序列中 &lt;code&gt;n&lt;/code&gt; 之后的下一个数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/** Buggy implementation of nextNumber! */
public static int nextNumber(int n) {
    if (n  == 128) {
        return 1;
    } else if (n == 5) {
        return 3 * n + 1;
    } else {
        return n * 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们走得太快之前，请注意：如果完成作业期间的任何时刻遇到困难，务必询问 TA。出现问题时越早让他们参与，越容易帮助你恢复。你的任务如下。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 1 步&lt;/h3&gt;
&lt;p&gt;解决合并冲突，并确保冲突解决后的结果包含这个有 bug 的方法版本。换句话说，完成合并冲突处理之后，&lt;code&gt;Collatz.java&lt;/code&gt; 应当能够编译，并且包含有 bug 的 &lt;code&gt;nextNumber&lt;/code&gt; 实现。&lt;/p&gt;
&lt;p&gt;现在，如果运行 &lt;code&gt;git log&lt;/code&gt;（它会按顺序列出当前提交之前的提交），你应该会看到类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;commit 8f0deeaef048f33a209f6f2fe5927a6fb04cc6cc
Merge: 225d73e 7aa1b6f
Author: Neil Kulkarni &amp;#x3C;neil.kulkarni@berkeley.edu&gt;
Date:   Sun Feb 7 14:36:52 2021 -0700

    Merge branch &apos;master&apos; of https://github.com/Berkeley-CS61B/skeleton-sp21

    Fixed the merge conflict in lab4 to contain buggy Collatz!
commit 7aa1b6fc79cb752e1ed844cd9cdd8c9c21e7f3d4 (HEAD -&gt; master)
Author: Neil Kulkarni &amp;#x3C;neil.kulkarni@berkeley.edu&gt;
Date:   Sun Feb 7 14:26:58 2021 -0700

    Added Lab 4 Starter Files

...

commit 4050fd80377d85aaea6c7cdb486e581d8c422534
Author: Neil Kulkarni &amp;#x3C;neil.kulkarni@berkeley.edu&gt;
Date:   Sat Jan 30 22:56:58 2021 -0800

    Finished Lab 1! Collatz works!

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，你应当看到最近的提交是解决合并冲突后产生的合并提交，第二新的提交是 Neil 添加 Lab 4 起始文件的提交，也就是刚刚从 &lt;code&gt;skeleton&lt;/code&gt; 拉取的那个提交。你可能需要向前翻很久，才能看到完成 Lab 1 时创建的提交。例如，我完成 Lab 1 时创建的提交消息是 “Finished Lab 1! Collatz works!”。&lt;/p&gt;
&lt;p&gt;你的提交很可能具有不同的提交消息，也应当拥有不同的提交哈希、作者和日期。记下你完成 Lab 1 时那个提交的提交哈希。我们把这个提交称为 &lt;code&gt;lab1commit&lt;/code&gt;，以便在之后的步骤中引用它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 2 步&lt;/h3&gt;
&lt;p&gt;在 Gradescope 上提交到 “Lab 4A: Git Exercise Part A” 自动评分器。它会验证你是否正确解决了合并冲突，使 &lt;code&gt;Collatz.java&lt;/code&gt; 包含有 bug 的 &lt;code&gt;nextNumber&lt;/code&gt; 实现。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 3 步&lt;/h3&gt;
&lt;p&gt;尽管最近的提交在 &lt;code&gt;lab1/Collatz.java&lt;/code&gt; 中包含一个 bug，幸运的是，你曾经在某个时刻提交过正确的 &lt;code&gt;Collatz&lt;/code&gt; 实现，也就是 &lt;code&gt;lab1commit&lt;/code&gt;！由于 Git 中的提交是文件状态的快照，&lt;code&gt;lab1commit&lt;/code&gt; 保存了正确版本 &lt;code&gt;Collatz&lt;/code&gt; 的快照。不相信？让我们看一看！&lt;/p&gt;
&lt;p&gt;下一步是 checkout 到 &lt;code&gt;lab1commit&lt;/code&gt;。如果不记得这条命令的语法，请查看上面链接的资料。到达该提交后，输入 &lt;code&gt;git status&lt;/code&gt;。你应当会看到自己处于 Detached HEAD 状态。如果不记得这是什么，请查看上面的资料！&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NeilKulkarni@Neils-MacBook-Pro sp21-s58 % git status
HEAD detached at 4050fd8
nothing to commit, working tree clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请记住，在 Detached HEAD 状态中，你可以自由查看当前提交，但不应当进行任何更改。让我们看看 &lt;code&gt;lab1/Collatz.java&lt;/code&gt;。你应当会看到自己的旧解法！可以用任何喜欢的方式打开这个文件，但我推荐使用终端命令 &lt;code&gt;cat&lt;/code&gt;，它会打印文件内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;NeilKulkarni@Neils-MacBook-Pro sp21-s58 % cat lab1/Collatz.java
/** Class that prints the Collatz sequence starting from a given number.
 *  @author Neil Kulkarni
 */
public class Collatz {
    public static int nextNumber(int n) {
        return n % 2 == 0 ? n/2 : 3*n + 1;
    }

    public static void main(String[] args) {
        int n = 5;
        System.out.print(n + &quot; &quot;);
        while (n != 1) {
            n = nextNumber(n);
            System.out.print(n + &quot; &quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 4 步&lt;/h3&gt;
&lt;p&gt;现在，checkout 到最近的提交，以离开 Detached HEAD 状态。你应当验证：由于我们已经回到最近的提交，&lt;code&gt;lab1/Collatz.java&lt;/code&gt; 再次包含有 bug 的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;NeilKulkarni@Neils-MacBook-Pro sp21-s58 % cat lab1/Collatz.java
/** Class that prints the Collatz sequence starting from a given number.
 *  @author YOUR NAME HERE
 */
public class Collatz {

    /** Buggy implementation of nextNumber! */
    public static int nextNumber(int n) {
        if (n  == 128) {
            return 1;
        } else if (n == 5) {
            return 3 * n + 1;
        } else {
            return n * 2;
        }
    }

    public static void main(String[] args) {
        int n = 5;
        System.out.print(n + &quot; &quot;);
        while (n != 1) {
            n = nextNumber(n);
            System.out.print(n + &quot; &quot;);
        }
        System.out.println();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 5 步&lt;/h3&gt;
&lt;p&gt;我们已经验证 &lt;code&gt;lab1commit&lt;/code&gt; 中包含正确的 &lt;code&gt;Collatz.java&lt;/code&gt; 内容，也已经回到包含有 bug 的 &lt;code&gt;Collatz.java&lt;/code&gt; 实现的最近提交。现在，使用 &lt;code&gt;git checkout&lt;/code&gt; 把 &lt;code&gt;lab1/Collatz.java&lt;/code&gt; 恢复为它在 &lt;code&gt;lab1commit&lt;/code&gt; 中的状态。如果忘记怎样 checkout 文件，请复习本 Lab 开头的资料！&lt;/p&gt;
&lt;p&gt;当你 checkout 一个文件时，Git 会自动把该文件加入暂存区。checkout 之后立刻运行 &lt;code&gt;git status&lt;/code&gt;，应当返回类似下面的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;NeilKulkarni@Neils-MacBook-Pro sp21-s58 % git status
On branch master
Your branch is up to date with &apos;origin/master&apos;.

Changes to be committed:
  (use &quot;git reset HEAD &amp;#x3C;file&gt;...&quot; to unstage)

	modified:   lab1/Collatz.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再执行一次 &lt;code&gt;cat lab1/Collatz.java&lt;/code&gt;。你应当会看到解法现在已经更新！接下来提交并推送更改。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;第 6 步&lt;/h3&gt;
&lt;p&gt;提交到 Lab 4B: Git Exercise Part B 自动评分器，以获得这项作业的分数。&lt;/p&gt;
&lt;p&gt;最后说几句：我们刚刚完成的操作非常强大。我们利用了 Git 会存储文件快照这一事实，并用它恢复到更早的版本！而且在这个过程中，我们还学习了怎样解决合并冲突！&lt;/p&gt;
&lt;p&gt;这些信息不只是理论。当你完成项目时，会发现它确实具有实际用途。正如 Hug 教授经常说的那样，在项目中从头重新开始完全没有问题（而且受到鼓励！）。现在，我们拥有了做到这一点的手段！可以使用 &lt;code&gt;git checkout&lt;/code&gt; 命令，把项目文件 checkout 为更早的某个提交中、尚未对它们进行修改时的版本。具体来说，如果想从骨架版本重新开始某个项目文件（例如 Project 0 的 &lt;code&gt;Model.java&lt;/code&gt; 文件），可以运行下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout skeleton/master -- proj0/game2048/Model.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以自由尝试这条命令！一般来说，&lt;code&gt;git checkout skeleton/master -- &amp;#x3C;file&gt;&lt;/code&gt; 会把文件恢复为其骨架代码状态。请记住，你已经在 &lt;code&gt;proj0&lt;/code&gt; 的开发过程中创建了提交，因此总是可以把 &lt;code&gt;Model.java&lt;/code&gt; 恢复到任何曾经提交过的状态（包括项目完成时的状态）！如果想把 &lt;code&gt;Model.java&lt;/code&gt; 恢复为最近一次提交时（也就是完成后的）状态，可以运行下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout master -- proj0/game2048/Model.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来说，&lt;code&gt;git checkout master -- &amp;#x3C;file&gt;&lt;/code&gt; 会把文件恢复为最近提交中的状态。现在，你已经知道怎样重新开始一个项目；如果对重新开始的结果不满意，也知道怎样把重新开始后的项目恢复到它在 &lt;code&gt;master&lt;/code&gt; 中的状态！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;一个调试谜题&lt;/h2&gt;
&lt;p&gt;另一项需要学习的重要技能是怎样进行彻底的调试。正确进行调试时，即使你并不完全理解正在调试的代码，也应当能够迅速缩小 bug 可能位于何处的范围。请考虑下面的场景。&lt;/p&gt;
&lt;p&gt;你的公司 Flik Enterprises 发布了一个优秀的软件库 &lt;code&gt;Flik.java&lt;/code&gt;，它能够判断两个 Integer 是否相同。&lt;/p&gt;
&lt;p&gt;你收到了一封来自 “Horrible Steve” 的电子邮件，其中描述了他在使用这个库时遇到的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;Dear Flik Enterprises,

Your library is very bad. See the attached code. It should print out 500
but actually it&apos;s printing out 128.

(attachment: HorribleSteve.java)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用下面任意几种技术的组合，判断 bug 位于 Horrible Steve 的代码中，还是位于 Flik Enterprises 的库中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为 Flik 库编写 JUnit 测试。如果想这样做，需要在 &lt;code&gt;flik&lt;/code&gt; 目录中新建一个文件，并导入 &lt;code&gt;junit&lt;/code&gt;。请参考前面问题中的测试，了解怎样完成。&lt;/li&gt;
&lt;li&gt;使用 IntelliJ 调试器，特别是&lt;a href=&quot;https://www.jetbrains.com/help/idea/using-breakpoints.html&quot;&gt;条件断点&lt;/a&gt;或&lt;a href=&quot;https://www.jetbrains.com/help/idea/examining-suspended-program.html&quot;&gt;遇到异常时中断&lt;/a&gt;，这些内容在 Lab 3 中已经学过！&lt;/li&gt;
&lt;li&gt;使用打印语句。&lt;/li&gt;
&lt;li&gt;重构 Horrible Steve 的代码。重构意味着改变语法而不改变功能。由于 HS 的代码使用了很多奇怪的东西，这可能很难完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;找到 bug 后，修复它，并把代码提交到 Lab 4: Debugging 自动评分器。本部分自动评分器使用隐藏测试，因此无法从 AG 获得任何关于 bug 的信息。如果认为已经修复 bug，却仍然无法通过 AG，请咨询 TA。&lt;/p&gt;
&lt;p&gt;提示：JUnit 提供了 &lt;code&gt;assertTrue(boolean)&lt;/code&gt; 和 &lt;code&gt;assertTrue(String, boolean)&lt;/code&gt; 方法，它们可能会很有帮助。&lt;/p&gt;
&lt;p&gt;尝试对这个 bug 给出简短解释！由于 Lecture 中没有讲过这个确切的问题，Google 是你的朋友。和 Lab 搭档讨论，并向 TA 或 AI 确认答案是否正确（不计分）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;这份 Lab 作业有三个独立的自动评分器。Lab 4A: Git Exercise Part A 和 Lab 4B: Git Exercise Part B 用来确认你已经完成 Git 练习；Lab 4: Debugging 用来确认你找到了并修复 “A Debugging Mystery” 中的 bug。这个 AG 还会检查代码风格，因此请确保每个文件都通过风格检查！请记住，可以通过右键单击文件并选择 &lt;code&gt;Check Style&lt;/code&gt; 来检查风格。&lt;/p&gt;
&lt;p&gt;有关每个 AG 应当在什么时候提交的更多信息，请查看规格中对应的部分。&lt;/p&gt;
&lt;p&gt;延期申请：共有三个自动评分器（Lab 4、Lab 4A 和 Lab 4B）。在 Beacon 上，如果为 Lab 4 提交延期申请，它会同时应用于三个自动评分器。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;完整回顾&lt;/h2&gt;
&lt;p&gt;本 Lab 介绍了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git 基础；&lt;/li&gt;
&lt;li&gt;合并冲突；&lt;/li&gt;
&lt;li&gt;Detached HEAD 状态；&lt;/li&gt;
&lt;li&gt;使用 Git checkout 代码；&lt;/li&gt;
&lt;li&gt;使用 JUnit 进行调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab4/lab4&quot;&gt;https://sp21.datastructur.es/materials/lab/lab4/lab4&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 5：同伴代码审查</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-5-peer-code-review</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-5-peer-code-review</guid><description>CS61B Spring 2021 Lab 5：同伴代码审查中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BB%8B%E7%BB%8D&quot;&gt;介绍&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#linkedlistdeque-%E6%A6%82%E8%A7%88&quot;&gt;LinkedListDeque 概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#linkedlistdeque-%E5%90%8C%E4%BC%B4%E5%AE%A1%E6%9F%A5&quot;&gt;LinkedListDeque 同伴审查&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#arraydeque-%E6%A6%82%E8%A7%88&quot;&gt;ArrayDeque 概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#arraydeque-%E5%90%8C%E4%BC%B4%E5%AE%A1%E6%9F%A5&quot;&gt;ArrayDeque 同伴审查&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%87%AA%E6%88%91%E5%8F%8D%E6%80%9D%E4%B8%8E%E6%8F%90%E4%BA%A4&quot;&gt;自我反思与提交&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在本实验中，你会与实验课中的另外 1–3 名学生比较自己对 Project 1 的解答。本周要求准时出席。如果无法参加自己通常参加的实验课，可以改去另一节。如果（并且只有在）你完全无法参加任何一节实验课，请填写&lt;a href=&quot;https://forms.gle/fcFdFyFb6YtPyu957&quot;&gt;这个豁免表单&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;作为本实验的一部分，TA 会讲解 Project 1 解答的一部分。在你向 Gradescope 完成 Project 1 最终提交之前，不要参加本实验，否则你会因学术不诚信而被记录。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;LinkedListDeque 概览&lt;/h4&gt;
&lt;p&gt;你的 TA 会先简要介绍工作人员提供的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 解答。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;LinkedListDeque 同伴审查&lt;/h4&gt;
&lt;p&gt;与另外 1–3 名学生组成小组并比较解答。你们应当在 Discord 房间中与其他学生组队。不要害怕认识新同学！61B 中的程序员在编程经验和熟悉程度方面差异很大。这里的目标是互相帮助，让彼此变得更好。请友善一些；如果你的解答不够优雅，甚至丑得离谱，也不要感到难过。&lt;/p&gt;
&lt;p&gt;我当然也写过极其丑陋、毫不优雅的代码！如果你好奇，这里有&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab5/knaveos.html&quot;&gt;我在 1997 年写的一个 1600 行怪物&lt;/a&gt;，以及它运行时的&lt;a href=&quot;https://youtu.be/XgJUC0Le_Ew&quot;&gt;视频演示&lt;/a&gt;。&lt;code&gt;knaveos&lt;/code&gt; 不会出现在期中考试中。&lt;/p&gt;
&lt;p&gt;不要忍不住向同伴逐字解释自己的实现究竟如何工作。相反，应当把讨论集中在更具体的问题上。下面列出了一些建议问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;你遇到的最烦人的 Bug 是什么，又是如何修复的？你使用调试器了吗？你是通过添加特殊情况修复的吗？你是否做过“改一点然后祈祷”（即做一个很小的修改，然后希望自动评分器认可）？&lt;/li&gt;
&lt;li&gt;为了让代码更简单，你最后有没有删掉某些内容？如果有，删掉了什么？&lt;/li&gt;
&lt;li&gt;你的代码中有没有特殊情况？&lt;/li&gt;
&lt;li&gt;你有没有私有辅助方法？&lt;/li&gt;
&lt;li&gt;你的代码有没有在某些地方重复自身？私有辅助方法会有帮助吗？&lt;/li&gt;
&lt;li&gt;你有没有在某些地方调用或复用代码？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;讨论结束后，在 &lt;code&gt;self_reflection.txt&lt;/code&gt; 的前半部分填写你自己的反思。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;ArrayDeque 概览&lt;/h4&gt;
&lt;p&gt;你的 TA 会讲解 &lt;code&gt;ArrayDeque&lt;/code&gt; 解答。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;ArrayDeque 同伴审查&lt;/h4&gt;
&lt;p&gt;现在，再次结对，像讨论 &lt;code&gt;LinkedListDeque&lt;/code&gt; 那样讨论你们的 &lt;code&gt;ArrayDeque&lt;/code&gt; 解答。填写你的 &lt;code&gt;self_reflection.txt&lt;/code&gt;。我们建议你与讨论 &lt;code&gt;LinkedListDeque&lt;/code&gt; 时不同的人交流，但继续留在原小组中讨论 &lt;code&gt;ArrayDeque&lt;/code&gt; 也没有问题。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;自我反思与提交&lt;/h2&gt;
&lt;p&gt;确保你已经回答了起始代码中所提供的 &lt;code&gt;self_reflection.txt&lt;/code&gt; 文档里的至少 4 个问题。请 TA 检查你的 &lt;code&gt;self_reflection.txt&lt;/code&gt;，并给你一个需要填写到 &lt;code&gt;magic_word.txt&lt;/code&gt; 中的魔法词。推送到 GitHub，然后提交到 Gradescope。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab5/lab5&quot;&gt;https://sp21.datastructur.es/materials/lab/lab5/lab5&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 6：Project 2 入门</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-6-project-2</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-6-project-2</guid><description>CS61B Spring 2021 Lab 6：Project 2 入门中文学习资料。</description><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;本实验介绍 Project 2 Gitlet 所依赖的核心工程知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在命令行中编译和运行 Java 程序；&lt;/li&gt;
&lt;li&gt;理解程序的当前工作目录（CWD）；&lt;/li&gt;
&lt;li&gt;使用相对路径和绝对路径；&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;File&lt;/code&gt; 和课程提供的 &lt;code&gt;Utils.java&lt;/code&gt; 读写文件；&lt;/li&gt;
&lt;li&gt;使用 Java 序列化，让对象在程序退出后仍能保存；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;make&lt;/code&gt; 和集成测试；&lt;/li&gt;
&lt;li&gt;使用远程 JVM 调试命令行程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实验真正需要编写代码的部分从 &lt;strong&gt;Canine Capers&lt;/strong&gt; 开始，但前面的背景必须认真阅读，否则后面的持久化逻辑会很难理解。&lt;/p&gt;
&lt;p&gt;先获取骨架：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;持久化（概念）&lt;/h2&gt;
&lt;p&gt;此前课程中的程序只在运行期间保存状态。程序一结束，实例变量和静态变量都会消失。例如 Project 0 的 2048 无法在退出后重新加载之前的进度。&lt;/p&gt;
&lt;p&gt;现实程序通常要跨多次运行保存状态。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add Hello.java Friends.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使 &lt;code&gt;git add&lt;/code&gt; 已经退出，随后运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Git 仍然知道哪些文件被加入暂存区。即使关机一年，这些信息仍会存在。&lt;/p&gt;
&lt;p&gt;实现持久化的关键是&lt;strong&gt;文件系统&lt;/strong&gt;：程序把数据写入磁盘，下一次运行时再读回来。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Java 的静态变量不会跨进程保存。程序结束后，实例变量和静态变量全部丢失。要跨运行保留数据，必须将其写到文件系统中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Java 与编译&lt;/h2&gt;
&lt;p&gt;过去你可能一直点击 IntelliJ 的绿色运行按钮。Java 也可以直接从终端编译和运行。&lt;/p&gt;
&lt;p&gt;Java 源文件是 &lt;code&gt;.java&lt;/code&gt;；&lt;code&gt;javac&lt;/code&gt; 将其编译为包含 Java 字节码的 &lt;code&gt;.class&lt;/code&gt; 文件；&lt;code&gt;java&lt;/code&gt; 再执行编译后的类。&lt;/p&gt;
&lt;p&gt;确认 Java 版本至少为 15：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac -version
java -version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 用户必须使用 Git Bash，而不是以 &lt;code&gt;C:&lt;/code&gt; 开头的普通命令提示符。&lt;/p&gt;
&lt;p&gt;进入实验目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd $REPO_DIR
cd lab6/capers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译所有 Java 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac *.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;*.java&lt;/code&gt; 是通配符，表示当前目录下所有 &lt;code&gt;.java&lt;/code&gt; 文件。编译后会出现若干 &lt;code&gt;.class&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;查看编译后的字节码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat Main.class
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它对人类来说大多是不可读字符，但 JVM 能执行。&lt;/p&gt;
&lt;p&gt;直接运行下面命令会失败：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java Main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Error: Could not find or load main class Main
Caused by: java.lang.NoClassDefFoundError: capers/Main (wrong name: Main)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是 &lt;code&gt;Main.java&lt;/code&gt; 位于 &lt;code&gt;capers&lt;/code&gt; 包中。运行包内类时，必须在包目录的父目录运行，并使用完整规范名称：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ..
java capers.Main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;向 &lt;code&gt;main&lt;/code&gt; 传递参数&lt;/h3&gt;
&lt;p&gt;运行类时，实际调用的是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;main(String[] args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数直接写在类名后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java capers.Main story &quot;this is a single argument&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;args = new String[]{&quot;story&quot;, &quot;this is a single argument&quot;};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双引号可将包含空格的内容作为一个参数。&lt;/p&gt;
&lt;p&gt;用 IntelliJ 打开：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sp21-s***/lab6/pom.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;Main.java&lt;/code&gt; 中找到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (args.length == 0) {
    Utils.exitWithError(&quot;Must have at least one argument&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其后临时加入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(&quot;args: &quot; + Arrays.toString(args));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 &lt;code&gt;Arrays&lt;/code&gt; 变红，按 &lt;code&gt;Alt+Enter&lt;/code&gt;（macOS 为 &lt;code&gt;Option+Enter&lt;/code&gt;）导入 &lt;code&gt;java.util.Arrays&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;从终端运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;javac capers/Main.java
java capers.Main story &quot;this is a single argument&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预期：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;args: [story, this is a single argument]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实验正式实现时，要删除这条调试输出。&lt;/p&gt;
&lt;h2&gt;Make&lt;/h2&gt;
&lt;p&gt;Capers 和 Gitlet 都具有持久状态。一次测试往往需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动程序执行一个命令；&lt;/li&gt;
&lt;li&gt;等程序退出；&lt;/li&gt;
&lt;li&gt;再启动程序执行另一个命令；&lt;/li&gt;
&lt;li&gt;验证第二次运行是否记住第一次运行的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，本实验不主要依赖 JUnit，而使用课程提供的 Python 集成测试框架，并通过 Unix 工具 &lt;code&gt;make&lt;/code&gt; 执行。&lt;/p&gt;
&lt;p&gt;安装好 &lt;code&gt;make&lt;/code&gt; 和 Python 后，在 &lt;code&gt;lab6&lt;/code&gt; 及其子目录中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会编译项目中的 Java 文件。输出可能类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;/Library/Developer/CommandLineTools/usr/bin/make&quot; -C capers default
javac -g -Xlint:unchecked -Xlint:deprecation -cp &quot;..::;..;&quot; CapersRepository.java Dog.java Main.java Utils.java
touch sentinel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;清理 &lt;code&gt;.class&lt;/code&gt; 等生成文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;check&lt;/code&gt; 和 &lt;code&gt;clean&lt;/code&gt; 称为 Make target。&lt;/p&gt;
&lt;p&gt;如果出现 &lt;code&gt;python3: command not found&lt;/code&gt; 或权限错误，请查看文末 FAQ。&lt;/p&gt;
&lt;h2&gt;Java 中的文件与目录&lt;/h2&gt;
&lt;h3&gt;当前工作目录&lt;/h3&gt;
&lt;p&gt;Java 程序的当前工作目录，是你&lt;strong&gt;从哪个目录启动程序&lt;/strong&gt;。程序中可通过：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.getProperty(&quot;user.dir&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class Example {
    public static void main(String[] args) {
        System.out.println(System.getProperty(&quot;user.dir&quot;));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若你先进入文件所在目录再运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /home/Michelle/example
javac Example.java
java Example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出会是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/home/Michelle/example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 下则类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;C:\Users\Michelle\example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 IntelliJ 中，CWD 位于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Run → Edit Configurations → Working Directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在终端中可用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pwd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绝对路径与相对路径&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;绝对路径&lt;/strong&gt;：相对于文件系统根目录的位置。
&lt;ul&gt;
&lt;li&gt;Windows：&lt;code&gt;C:/Users/Michelle/example/Example.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;macOS/Linux：&lt;code&gt;/home/Michelle/example/Example.java&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;相对路径&lt;/strong&gt;：相对于当前工作目录的位置。
&lt;ul&gt;
&lt;li&gt;若 CWD 已是 &lt;code&gt;example&lt;/code&gt;，则路径可以只是 &lt;code&gt;Example.java&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若 CWD 是其父目录，则为 &lt;code&gt;example/Example.java&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt; 表示用户主目录，但主目录与文件系统根目录不是一回事。&lt;/p&gt;
&lt;p&gt;本实验中通常应使用相对路径，使所有生成内容位于 &lt;code&gt;lab6&lt;/code&gt; 的工作目录中，而不是写到计算机上的随机位置。&lt;/p&gt;
&lt;h3&gt;Java 中的文件与目录操作&lt;/h3&gt;
&lt;h4&gt;文件&lt;/h4&gt;
&lt;p&gt;Java 的 &lt;code&gt;File&lt;/code&gt; 类可表示文件或目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;File f = new File(&quot;dummy.txt&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只创建了一个指向路径的 Java 对象，并没有真的创建磁盘文件。&lt;/p&gt;
&lt;p&gt;创建文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;f.createNewFile();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;f.exists();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;课程提供的 &lt;code&gt;Utils.java&lt;/code&gt; 简化了文件读写。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Utils.writeContents(f, &quot;Hello World&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;目录&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;File d = new File(&quot;dummy&quot;);
d.mkdir();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以查看 &lt;code&gt;mkdirs()&lt;/code&gt;，它能创建多级目录。&lt;/p&gt;
&lt;p&gt;请重点熟悉 &lt;code&gt;Utils.java&lt;/code&gt; 中的读写、连接路径、序列化辅助方法。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;Java 中还有许多操作文件的方法。你可以阅读 &lt;code&gt;File&lt;/code&gt; 的 Javadocs 并在需要时搜索；本 Lab 和 Gitlet 应优先使用课程提供的 &lt;code&gt;Utils.java&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Serializable&lt;/h2&gt;
&lt;p&gt;如果要保存一个复杂对象，可以手工把它变成字符串，再自己解析回来，但这种做法很繁琐。&lt;/p&gt;
&lt;p&gt;Java 提供&lt;strong&gt;序列化&lt;/strong&gt;：把对象转成字节流写入文件；下一次运行时再反序列化成对象。&lt;/p&gt;
&lt;p&gt;要允许某个类被序列化，让它实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.io.Serializable;

public class Model implements Serializable {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Serializable&lt;/code&gt; 没有要求实现的方法，它只是一个标记接口。&lt;/p&gt;
&lt;p&gt;标准 Java 写法较长：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Model m = ....;
File outFile = new File(saveFileName);
try {
    ObjectOutputStream out =
        new ObjectOutputStream(new FileOutputStream(outFile));
    out.writeObject(m);
    out.close();
} catch (IOException excp) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读取：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Model m;
File inFile = new File(saveFileName);
try {
    ObjectInputStream inp =
        new ObjectInputStream(new FileInputStream(inFile));
    m = (Model) inp.readObject();
    inp.close();
} catch (IOException | ClassNotFoundException excp) {
    ...
    m = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;Utils&lt;/code&gt; 后，写对象只需：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;writeObject(outFile, m);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;m = readObject(inFile, Model.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;练习：Canine Capers&lt;/h2&gt;
&lt;p&gt;你要实现一个使用文件操作和序列化的小程序。&lt;/p&gt;
&lt;p&gt;骨架包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Main.java&lt;/code&gt;：程序入口，根据命令行参数调用正确方法；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CapersRepository.java&lt;/code&gt;：协调其他类，绝大多数 FIXME 位于这里；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dog.java&lt;/code&gt;：表示一只拥有姓名、品种和年龄的狗；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Utils.java&lt;/code&gt;：文件与序列化工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本实验不测试非法输入和错误情况；Gitlet 中则必须处理。&lt;/p&gt;
&lt;h3&gt;Main&lt;/h3&gt;
&lt;h4&gt;&lt;code&gt;story [text]&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;将 &lt;code&gt;text&lt;/code&gt; 和换行符 &lt;code&gt;&quot;\n&quot;&lt;/code&gt; 追加到 &lt;code&gt;.capers&lt;/code&gt; 目录中的故事文件，然后打印当前完整故事。打印内容必须包括刚追加的文字。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;dog [name] [breed] [age]&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;持久化创建一只指定姓名、品种和年龄的狗，并打印其 &lt;code&gt;toString()&lt;/code&gt;。可假设狗名唯一。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;birthday [name]&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;持久化增加指定狗的年龄，并打印狗的信息和生日祝福。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java capers.Main story &quot;Once upon a time, there was a beautiful dog.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Once upon a time, there was a beautiful dog.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二次运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java capers.Main story &quot;That dog was named Fjerf.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出必须包含之前保存的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Once upon a time, there was a beautiful dog.
That dog was named Fjerf.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建狗：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java capers.Main dog Mammoth &quot;German Spitz&quot; 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Woof! My name is Mammoth and I am a German Spitz! I am 10 years old! Woof!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生日：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java capers.Main birthday Qitmir
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Woof! My name is Qitmir and I am a Saluki! I am 4 years old! Woof!
Happy birthday! Woof! Woof!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;有用的 Utils 函数&lt;/h3&gt;
&lt;p&gt;以下函数是一个起点；你可能还需要其他函数，也可能不需要全部函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;writeContents&lt;/code&gt;：把字符串或字节数组写入文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readContentsAsString&lt;/code&gt;：把文件读为字符串。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readContents&lt;/code&gt;：把文件读为字节数组。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeObject&lt;/code&gt;：把可序列化对象写入文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readObject&lt;/code&gt;：从文件读取可序列化对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;join&lt;/code&gt;：安全地把多个路径部分组合为 &lt;code&gt;File&lt;/code&gt;，不要用字符串拼接创建路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;持久化目录&lt;/h3&gt;
&lt;p&gt;所有持久数据都应放在当前工作目录下的 &lt;code&gt;.capers&lt;/code&gt; 目录。点号前缀使其默认隐藏，用户不需要知道内部实现细节。&lt;/p&gt;
&lt;p&gt;推荐结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.capers/
├── dogs/      # 每只狗的持久化数据
└── story      # 当前故事文本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些文件和目录应由程序创建，不应手动创建。&lt;/p&gt;
&lt;p&gt;删除全部持久状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm -rf .capers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意不要误删源代码目录 &lt;code&gt;capers&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;建议的完成顺序&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;setUpPersistence&lt;/code&gt;：如果需要的目录和文件不存在，就创建它们；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeStory&lt;/code&gt;：读取旧故事、追加新文本，再覆盖写回完整内容；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dog&lt;/code&gt; 构造器和 &lt;code&gt;toString&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;saveDog&lt;/code&gt;：将 &lt;code&gt;Dog&lt;/code&gt; 序列化到文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dog.fromFile&lt;/code&gt;：根据姓名读取狗对象；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;haveBirthday&lt;/code&gt;：修改年龄后必须重新保存；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Main.main&lt;/code&gt;：解析命令行参数并分派命令。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;用法&lt;/h3&gt;
&lt;p&gt;从 &lt;code&gt;lab6&lt;/code&gt; 目录运行 &lt;code&gt;make&lt;/code&gt;，再使用 &lt;code&gt;java capers.Main [args]&lt;/code&gt; 执行命令。&lt;code&gt;story&lt;/code&gt; 的文本包含空格时应使用引号。&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;除了 &lt;code&gt;make check&lt;/code&gt;，还应手工测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make
java capers.Main story Hello
java capers.Main story World
ls -a .capers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试文件位于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;lab6/testing/our
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个 &lt;code&gt;.in&lt;/code&gt; 文件代表一个集成测试。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# Two uses of the `story` command
&gt; story &quot;Hello&quot;
Hello
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&gt; story &quot;World&quot;
Hello
World
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt; 开头为注释；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&gt;&lt;/code&gt; 后是传给 &lt;code&gt;capers.Main.main()&lt;/code&gt; 的参数；&lt;/li&gt;
&lt;li&gt;直到 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 的内容是预期输出；&lt;/li&gt;
&lt;li&gt;输出必须完全匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这类测试称为&lt;strong&gt;集成测试&lt;/strong&gt;：一次测试会跨多个类和多次程序运行，不像单元测试只隔离一个小组件。&lt;/p&gt;
&lt;h2&gt;提交&lt;/h2&gt;
&lt;p&gt;通过全部 &lt;code&gt;make check&lt;/code&gt; 后，你应修改了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;capers/Main.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capers/Dog.java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capers/CapersRepository.java&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;提交：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -m &quot;&amp;#x3C;commit message&gt;&quot;
git push origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 Gradescope 提交。本实验没有风格检查。&lt;/p&gt;
&lt;h2&gt;强制结尾：调试&lt;/h2&gt;
&lt;p&gt;完成并提交后，仍应做这一节。它会教你如何调试 Capers 和 Gitlet 的多次进程执行。&lt;/p&gt;
&lt;h3&gt;1. 配置远程 JVM 调试&lt;/h3&gt;
&lt;p&gt;先用 Git 将 Lab 6 临时恢复到初始骨架，以便观察失败测试；实验结束后务必切回自己的解答。&lt;/p&gt;
&lt;p&gt;在 IntelliJ 中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Lab 6；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Run → Edit Configurations&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;+&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;选择 &lt;code&gt;Remote JVM Debug&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;给配置起一个易识别的名称；&lt;/li&gt;
&lt;li&gt;保持课程说明中的默认端口与设置。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 从测试运行器启动调试&lt;/h3&gt;
&lt;p&gt;进入 &lt;code&gt;lab6/testing&lt;/code&gt;，运行某个测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 runner.py --debug our/test02-two-part-story.in
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要保留测试结束后的 &lt;code&gt;.capers&lt;/code&gt; 文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 runner.py --keep --debug our/test02-two-part-story.in
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;交互命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;：不调试当前命令，直接运行并转到下一条，类似 Step Over；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt;：调试当前命令，类似 Step Into；输入后在 IntelliJ 点击 Debug；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q&lt;/code&gt;：退出调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先不断输入 &lt;code&gt;n&lt;/code&gt;，找出究竟是哪一次程序执行首次产生错误。若启用了 &lt;code&gt;--keep&lt;/code&gt;，测试目录会被保存在类似：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;test02-two-part-story_0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到出错命令后，重新运行测试，在该命令处输入 &lt;code&gt;s&lt;/code&gt;，然后用 IntelliJ 连接远程 JVM，并使用正常的断点、单步和变量观察。&lt;/p&gt;
&lt;h3&gt;3. 持久化错误的特殊性&lt;/h3&gt;
&lt;p&gt;测试第二条命令输出错误，不一定说明第二次执行有 bug。可能第一次执行没有正确写入持久化数据，第二次执行只是读到了错误状态。&lt;/p&gt;
&lt;p&gt;调试时第一目标应是找到&lt;strong&gt;最早产生错误状态的那次执行&lt;/strong&gt;。否则你可能花很久调试一个本身正确的后续命令。&lt;/p&gt;
&lt;p&gt;Gitlet 使用完全相同的调试方式。&lt;/p&gt;
&lt;h2&gt;提示、FAQ 与常见误区&lt;/h2&gt;
&lt;h4&gt;提示&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setUpPersistence&lt;/code&gt;：确保所需目录/文件不存在时会被创建；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeStory&lt;/code&gt;：使用 &lt;code&gt;readContentsAsString&lt;/code&gt; 和 &lt;code&gt;writeContents&lt;/code&gt;。仅使用 &lt;code&gt;writeContents&lt;/code&gt; 会覆盖旧故事，不能直接“追加”；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;saveDog&lt;/code&gt;：使用 &lt;code&gt;writeObject&lt;/code&gt;，目标 &lt;code&gt;File&lt;/code&gt; 必须表示文件而不是目录；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fromFile&lt;/code&gt;：使用 &lt;code&gt;readObject&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改对象后若要求“持久化”，必须再次把修改后的对象写回文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;FAQ 与常见误区&lt;/h4&gt;
&lt;h3&gt;Python 命令差异&lt;/h3&gt;
&lt;p&gt;若你的系统运行 Python 的命令不是 &lt;code&gt;python3&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make check PYTHON=&amp;#x3C;你的 Python 命令&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make check PYTHON=py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可修改 &lt;code&gt;Makefile&lt;/code&gt; 中对应配置，避免每次重复输入。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;File&lt;/code&gt; 常见误解&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new File(...)&lt;/code&gt; 不会创建实际文件或目录；&lt;/li&gt;
&lt;li&gt;创建文件要调用 &lt;code&gt;createNewFile()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建目录要调用 &lt;code&gt;mkdir()&lt;/code&gt; 或 &lt;code&gt;mkdirs()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;File&lt;/code&gt; 对象可以表示文件，也可以表示目录；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Utils.join(d, s)&lt;/code&gt; 只是创建表示 &lt;code&gt;d&lt;/code&gt; 下 &lt;code&gt;s&lt;/code&gt; 路径的 &lt;code&gt;File&lt;/code&gt; 对象，不会创建实际文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeObject(file, object)&lt;/code&gt; 的第一个参数必须是文件路径；&lt;/li&gt;
&lt;li&gt;“持久地修改”意味着不仅修改内存中的对象，还必须把修改写回文件系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;致谢&lt;/h2&gt;
&lt;p&gt;Capers 最初由 Sean Dooher 于 2019 年秋季编写。规格与 Lab 改编由 Michelle Hwang 于 2020 年春季完成，之后由 Omar Khan 于 2021 年春季继续改编。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab6/lab6&quot;&gt;https://sp21.datastructur.es/materials/lab/lab6/lab6&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 7：BSTMap</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-7-bstmap</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-7-bstmap</guid><description>CS61B Spring 2021 Lab 7：BSTMap中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BB%8B%E7%BB%8D&quot;&gt;介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#bstmap&quot;&gt;BSTMap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%89%80%E4%BB%A5%E5%AE%83%E5%88%B0%E5%BA%95%E6%9C%89%E5%A4%9A%E5%BF%AB&quot;&gt;所以……它到底有多快？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%8F%AF%E9%80%89%E7%BB%83%E4%B9%A0&quot;&gt;可选练习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%9E%E9%AA%8C%E6%80%BB%E7%BB%93%E4%B8%8E%E6%8F%90%E4%BA%A4&quot;&gt;实验总结与提交&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%8F%AF%E9%80%89%E6%B8%90%E8%BF%91%E5%88%86%E6%9E%90%E9%A2%98&quot;&gt;可选渐近分析题&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在本实验中，你将创建 &lt;code&gt;BSTMap&lt;/code&gt;：它是 &lt;code&gt;Map61B&lt;/code&gt; 接口的一种基于 BST 的实现，表示一个基本的、基于树的映射。你会完全从零开始创建它，并把所提供的接口当作指南。&lt;/p&gt;
&lt;p&gt;完成实现后，你会把自己的实现性能与基于列表的 Map 实现 &lt;code&gt;ULLMap&lt;/code&gt; 以及 Java 内置的 &lt;code&gt;TreeMap&lt;/code&gt; 类（它也使用 BST）进行比较。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;BSTMap&lt;/h2&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;BSTMap&lt;/code&gt; 的类，使用 BST（二叉搜索树，Binary Search Tree）作为核心数据结构，实现 &lt;code&gt;Map61B&lt;/code&gt; 接口。你必须在名为 &lt;code&gt;BSTMap.java&lt;/code&gt; 的文件中完成它。除 &lt;code&gt;remove&lt;/code&gt;、&lt;code&gt;iterator&lt;/code&gt; 和 &lt;code&gt;keySet&lt;/code&gt; 外，你的实现必须实现 &lt;code&gt;Map61B&lt;/code&gt; 中给出的所有方法。对于这几个方法，应抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在创建 &lt;code&gt;BSTMap&lt;/code&gt; 类并实现 &lt;code&gt;Map61B&lt;/code&gt; 的所有方法之前，你的代码无法编译。你可以一次实现一个方法：先写出所有必需方法的方法签名，但在实现中抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;；等轮到实际编写某个方法时，再完成它。&lt;/p&gt;
&lt;p&gt;你的 &lt;code&gt;BSTMap&lt;/code&gt; 还应额外添加一个 &lt;code&gt;printInOrder()&lt;/code&gt; 方法（该方法没有在 &lt;code&gt;Map61B&lt;/code&gt; 接口中给出），按 Key 递增顺序打印 &lt;code&gt;BSTMap&lt;/code&gt;。我们不会测试这个方法的结果，但你会发现它有助于测试自己的实现！&lt;/p&gt;
&lt;p&gt;在实现中，你应当假定 &lt;code&gt;BSTMap&amp;#x3C;K, V&gt;&lt;/code&gt; 中的泛型键 &lt;code&gt;K&lt;/code&gt; 扩展了 &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html&quot;&gt;&lt;code&gt;Comparable&lt;/code&gt;&lt;/a&gt;。换句话说，你可以假定泛型键 &lt;code&gt;K&lt;/code&gt; 具有 &lt;code&gt;compareTo&lt;/code&gt; 方法。在 Java 中，可以使用&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/generics/bounded.html&quot;&gt;有界类型参数&lt;/a&gt;强制满足这一条件。考虑下面这个摘自 Oracle 文档的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;/*
 * Bounded type parameters allow you to invoke methods defined in the bounds:
 * The `isEven` method invokes the `intValue` method defined in the
 * `Integer` class through `n`.
 */

public class NaturalNumber&amp;#x3C;T extends Integer&gt; {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还建议你使用一个私有的嵌套 &lt;code&gt;BSTNode&lt;/code&gt; 类，以帮助完成实现。如何设计和使用这个内部类由你决定！&lt;/p&gt;
&lt;p&gt;你可以使用 &lt;code&gt;TestBSTMap.java&lt;/code&gt; 测试自己的实现。&lt;/p&gt;
&lt;p&gt;下面这些资源可能会有帮助：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lecture 16 的&lt;a href=&quot;https://docs.google.com/&quot;&gt;幻灯片&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;课程资源页面中《Data Structures Into Java》第 109 页和第 111 页的 BST 代码。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://algs4.cs.princeton.edu/&quot;&gt;可选教材&lt;/a&gt;中的 BST 代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ULLMap.java&lt;/code&gt;（已提供）：一个能够正常工作的、基于无序链表的 &lt;code&gt;Map61B&lt;/code&gt; 实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;所以……它到底有多快？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;InsertRandomSpeedTest.java&lt;/code&gt; 和 &lt;code&gt;InsertInOrderSpeedTest.java&lt;/code&gt; 中提供了两个交互式速度测试。在完成 &lt;code&gt;BSTMap&lt;/code&gt; 之前，不要尝试运行这些测试。准备好后，可以在 IntelliJ 中运行它们。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InsertRandomSpeedTest&lt;/code&gt; 类会测试你的 &lt;code&gt;BSTMap&lt;/code&gt;、已提供的 &lt;code&gt;ULLMap&lt;/code&gt;、Java 内置 &lt;code&gt;TreeMap&lt;/code&gt; 和 Java 内置 &lt;code&gt;HashMap&lt;/code&gt;（你会在下一个实验中进一步探索它）在插入元素时的速度。它会询问用户：要插入的每个 String 所需的长度，以及输入规模（要执行的插入次数）。然后，它会生成指定数量、指定长度的 String，并将它们作为 &lt;code&gt;&amp;#x3C;String, Integer&gt;&lt;/code&gt; 对插入映射中。&lt;/p&gt;
&lt;p&gt;尝试运行它，看看随着插入次数增加，你的数据结构与朴素实现和工业级实现相比如何扩展。请记住，在小样本上，渐近分析并不具有代表性；如果看到令人困惑的趋势，请确保输入足够大。把结果记录在名为 &lt;code&gt;speedTestResults.txt&lt;/code&gt; 的文件中。结果没有规定的标准格式，也没有规定所需的数据点数量。&lt;/p&gt;
&lt;p&gt;现在尝试运行 &lt;code&gt;InsertInOrderSpeedTest&lt;/code&gt;。它的行为与 &lt;code&gt;InsertRandomSpeedTest&lt;/code&gt; 类似，但这一次，&lt;code&gt;&amp;#x3C;String, Integer&gt;&lt;/code&gt; 键值对中的 String 会按照&lt;a href=&quot;https://en.wikipedia.org/wiki/Lexicographic_order&quot;&gt;字典序递增顺序&lt;/a&gt;插入。如果你观察到任何有趣现象（希望你观察到了），应当与其他学生和/或 TA 讨论。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;可选练习&lt;/h2&gt;
&lt;p&gt;这一部分不会评分，但你仍然可以从自动评分器获得反馈。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;BSTMap&lt;/code&gt; 类中实现 &lt;code&gt;iterator()&lt;/code&gt;、&lt;code&gt;keySet()&lt;/code&gt;、&lt;code&gt;remove(K key)&lt;/code&gt; 和 &lt;code&gt;remove(K key, V value)&lt;/code&gt;。实现 &lt;code&gt;iterator&lt;/code&gt; 方法时，应当返回一个遍历键的迭代器。实现 &lt;code&gt;remove()&lt;/code&gt; 相当有挑战性。作为额外挑战，请在不使用第二个实例变量存储键集合的情况下实现 &lt;code&gt;keySet()&lt;/code&gt; 和 &lt;code&gt;iterator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;remove&lt;/code&gt;，如果参数键在 &lt;code&gt;BSTMap&lt;/code&gt; 中不存在，应返回 &lt;code&gt;null&lt;/code&gt;。否则，删除键值对 &lt;code&gt;(key, value)&lt;/code&gt; 并返回 &lt;code&gt;value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;实验总结与提交&lt;/h2&gt;
&lt;p&gt;实验结束时，你的 TA 会讲解参考解答。如果你尚未完成实验，这会很有帮助，因为我们不希望你在实验课之外被这个实验困住太久。（这也是鼓励你参加实验课的一个理由！）&lt;/p&gt;
&lt;p&gt;确保提交完成的 &lt;code&gt;BSTMap.java&lt;/code&gt; 和 &lt;code&gt;speedTestResults.txt&lt;/code&gt;，并像往常一样通过 Git 和 Gradescope 提交。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;可选渐近分析题&lt;/h2&gt;
&lt;p&gt;给定 &lt;code&gt;B&lt;/code&gt;——一个包含 &lt;code&gt;N&lt;/code&gt; 个键值对的 &lt;code&gt;BSTMap&lt;/code&gt;——以及 &lt;code&gt;(K, V)&lt;/code&gt;——一个随机键值对——回答下列问题。&lt;/p&gt;
&lt;p&gt;除非另有说明，“大 O”界（例如 &lt;code&gt;O(N)&lt;/code&gt;）和“大 Θ”界（例如 &lt;code&gt;Θ(N)&lt;/code&gt;）指给定方法调用中的比较次数。&lt;/p&gt;
&lt;p&gt;对于第 1–7 题，说明陈述是真还是假。对于第 8 题，给出运行时间界。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;B.put(K, V)&lt;/code&gt; ∈ &lt;code&gt;O(log(N))&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;B.put(K, V)&lt;/code&gt; ∈ &lt;code&gt;Θ(log(N))&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;B.put(K, V)&lt;/code&gt; ∈ &lt;code&gt;Θ(N)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;B.put(K, V)&lt;/code&gt; ∈ &lt;code&gt;O(N)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;B.put(K, V)&lt;/code&gt; ∈ &lt;code&gt;O(N²)&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;令 &lt;code&gt;g(N)&lt;/code&gt; 表示：随机调用 &lt;code&gt;B.put(K, V)&lt;/code&gt; 共 &lt;code&gt;N&lt;/code&gt; 次，随后调用 &lt;code&gt;B.containsKey(K)&lt;/code&gt;，完成这些操作所需的平均比较次数。那么，&lt;code&gt;g(N) ~ 2(ln(N))&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：我们写作 &lt;code&gt;g(N) ~ f(N)&lt;/code&gt;，表示当 &lt;code&gt;N&lt;/code&gt; 变大时，&lt;code&gt;g(N) / f(N) -&gt; 1&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于键 &lt;code&gt;C != K&lt;/code&gt;，同时运行 &lt;code&gt;B.containsKey(K)&lt;/code&gt; 和 &lt;code&gt;B.containsKey(C)&lt;/code&gt; ∈ &lt;code&gt;Ω(log(N))&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设 &lt;code&gt;BSTMap b&lt;/code&gt; 由一个 &lt;code&gt;root&lt;/code&gt; Node（Key、Value 对）和两个名为 &lt;code&gt;left&lt;/code&gt; 与 &lt;code&gt;right&lt;/code&gt; 的 &lt;code&gt;BSTMap&lt;/code&gt; 子树组成。进一步假定，方法 &lt;code&gt;numberOfNodes(BSTMap b)&lt;/code&gt; 返回以 &lt;code&gt;b.root&lt;/code&gt; 为根的 &lt;code&gt;BSTMap&lt;/code&gt; 的节点数；它的运行时间是 &lt;code&gt;Θ(n)&lt;/code&gt;，其中 &lt;code&gt;n&lt;/code&gt; 是以 &lt;code&gt;b&lt;/code&gt; 为根的 &lt;code&gt;BSTMap&lt;/code&gt; 中的 Node 数量。对于某个正整数 &lt;code&gt;z&lt;/code&gt;，&lt;code&gt;mystery(b, z)&lt;/code&gt; 的运行时间（以大 O 记号表示）是多少？假定 &lt;code&gt;b&lt;/code&gt; 有 &lt;code&gt;N&lt;/code&gt; 个节点，请给出尽可能紧的界。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你的答案不应包含任何不必要的乘法常数或加法因子。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public Key mystery(BSTMap b, int z) {
    if (z &gt; numberOfNodes(b) || z &amp;#x3C;= 0)
        return null;
    if (numberOfNodes(b.left) == z-1)
        return b.root.key;
    else if (numberOfNodes(b.left) &gt; z)
        return mystery(b.left, z);
    else
        return mystery(b.right, z-numberOfNodes(b.left) - 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab7/lab7&quot;&gt;https://sp21.datastructur.es/materials/lab/lab7/lab7&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Lab 8：HashMap</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-8-hashmap</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/labs/lab-8-hashmap</guid><description>CS61B Spring 2021 Lab 8：HashMap中文学习资料。</description><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BB%8B%E7%BB%8D&quot;&gt;介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#myhashmap&quot;&gt;MyHashMap&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%A6%82%E8%A7%88&quot;&gt;概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B5%B7%E5%A7%8B%E4%BB%A3%E7%A0%81&quot;&gt;起始代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%9E%E7%8E%B0%E8%A6%81%E6%B1%82&quot;&gt;实现要求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B5%8B%E8%AF%95&quot;&gt;测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B5%84%E6%BA%90&quot;&gt;资源&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#hashmap-%E9%80%9F%E5%BA%A6%E6%B5%8B%E8%AF%95&quot;&gt;HashMap 速度测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%9B%B4%E6%8D%A2%E6%A1%B6%E7%B1%BB%E5%9E%8B%E9%80%9F%E5%BA%A6%E6%B5%8B%E8%AF%95&quot;&gt;更换桶类型：速度测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%8F%AF%E9%80%89%E7%BB%83%E4%B9%A0&quot;&gt;可选练习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%9E%E9%AA%8C%E6%80%BB%E7%BB%93%E4%B8%8E%E6%8F%90%E4%BA%A4&quot;&gt;实验总结与提交&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在本实验中，你会完成 &lt;code&gt;MyHashMap&lt;/code&gt;，它是 &lt;code&gt;Map61B&lt;/code&gt; 接口的一种实现，表示一个哈希映射。本实验与 Lab 7 非常相似，不过这一次我们构建的是 Hash Map，而不是 Tree Map。&lt;/p&gt;
&lt;p&gt;完成实现后，你会把自己的实现性能与基于列表的 Map 实现 &lt;code&gt;ULLMap&lt;/code&gt; 以及 Java 内置的 &lt;code&gt;HashMap&lt;/code&gt; 类（它也使用哈希表）进行比较。我们还会比较 &lt;code&gt;MyHashMap&lt;/code&gt; 使用不同数据结构作为桶时的性能。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;MyHashMap&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;概览&lt;/h3&gt;
&lt;p&gt;我们已经在 &lt;code&gt;MyHashMap.java&lt;/code&gt; 中创建了一个 &lt;code&gt;MyHashMap&lt;/code&gt; 类，只提供了非常少的起始代码。你的目标是实现 &lt;code&gt;MyHashMap&lt;/code&gt; 从 &lt;code&gt;Map61B&lt;/code&gt; 接口继承的所有方法，&lt;code&gt;remove&lt;/code&gt; 除外。对于 &lt;code&gt;remove&lt;/code&gt;，应当抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;。请注意，这一次你应实现 &lt;code&gt;keySet&lt;/code&gt; 和 &lt;code&gt;iterator&lt;/code&gt;；其中 &lt;code&gt;iterator&lt;/code&gt; 返回一个遍历所存储键的 &lt;code&gt;Iterator&lt;/code&gt;。这两个函数都可以按任意顺序返回键。&lt;/p&gt;
&lt;p&gt;对于这些方法，我们建议你简单地创建一个 &lt;code&gt;HashSet&lt;/code&gt; 实例变量，用它保存所有键。&lt;/p&gt;
&lt;p&gt;请注意，在你实现 &lt;code&gt;Map61B&lt;/code&gt; 的所有方法之前，代码无法编译。你可以一次实现一个方法：先写出所有必需方法的方法签名，但在实现中抛出 &lt;code&gt;UnsupportedOperationException&lt;/code&gt;；等轮到实际编写某个方法时，再完成它。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;起始代码&lt;/h3&gt;
&lt;p&gt;你可能还记得讲座中的内容：构建哈希表时，可以选择多种不同的数据结构作为桶。经典方法是选择 &lt;code&gt;LinkedList&lt;/code&gt;。但我们也可以选择 &lt;code&gt;ArrayList&lt;/code&gt;、&lt;code&gt;TreeSet&lt;/code&gt;，甚至其他更疯狂的数据结构，例如 &lt;code&gt;PriorityQueue&lt;/code&gt;，乃至另一个 &lt;code&gt;HashSet&lt;/code&gt;！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/9ca265a119da-ht-buckets.png&quot; alt=&quot;ht-buckets&quot;&gt;&lt;/p&gt;
&lt;p&gt;在本实验中，我们会尝试为哈希表中的每个桶使用不同的数据结构，并通过实验观察：把不同数据结构用作哈希表的桶，是否会产生渐近意义上的差异。&lt;/p&gt;
&lt;p&gt;你或许可以想象，如果我们在实现 &lt;code&gt;MyHashMap&lt;/code&gt; 时完全不加考虑，那么要把桶类型替换成另一种桶类型，就需要做大量的“查找 + 替换”。例如，如果想把所有 &lt;code&gt;ArrayList&lt;/code&gt; 桶换成 &lt;code&gt;LinkedList&lt;/code&gt; 桶，就必须查找所有出现的 &lt;code&gt;ArrayList&lt;/code&gt; 并替换成 &lt;code&gt;LinkedList&lt;/code&gt;。这并不理想。&lt;/p&gt;
&lt;p&gt;起始代码的目的，是提供一种更简单的方法，让我们能够在 &lt;code&gt;MyHashMap&lt;/code&gt; 中试用不同的桶类型。它通过本学期前面学过的多态和继承实现这一点。它还使用了工厂方法；工厂方法用于创建对象。我们会使用工厂方法创建桶。起始文件的继承结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Map61B.java
└── MyHashMap.java
    ├── MyHashMapALBuckets.java
    ├── MyHashMapHSBuckets.java
    ├── MyHashMapLLBuckets.java
    ├── MyHashMapPQBuckets.java
    └── MyHashMapTSBuckets.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MyHashMap&lt;/code&gt; 使用哈希表实现 &lt;code&gt;Map61B&lt;/code&gt; 接口。在起始代码中，我们给出了实例变量 &lt;code&gt;private Collection&amp;#x3C;Node&gt;[] buckets&lt;/code&gt;，它是哈希表的底层数据结构。让我们拆解一下这段代码的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;buckets&lt;/code&gt; 是 &lt;code&gt;MyHashMap&lt;/code&gt; 类中的一个 &lt;code&gt;private&lt;/code&gt; 变量。&lt;/li&gt;
&lt;li&gt;它是一个由 &lt;code&gt;Collection&amp;#x3C;Node&gt;&lt;/code&gt; 对象组成的数组（或表）；其中，每一个由 &lt;code&gt;Node&lt;/code&gt; 组成的 &lt;code&gt;Collection&lt;/code&gt; 都表示哈希表中的一个桶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Node&lt;/code&gt; 是我们提供的私有辅助类，用于存储单个键值映射。这个类的起始代码应当很容易理解，也不应当需要任何修改。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html&quot;&gt;&lt;code&gt;java.util.Collection&lt;/code&gt;&lt;/a&gt; 是大多数数据结构所实现的接口，表示一组对象。&lt;code&gt;Collection&lt;/code&gt; 接口支持向组中 &lt;code&gt;add&lt;/code&gt;、从组中 &lt;code&gt;remove&lt;/code&gt;，以及在组上 &lt;code&gt;iterate&lt;/code&gt; 等方法。&lt;code&gt;java.util&lt;/code&gt; 中的许多数据结构都实现了 &lt;code&gt;Collection&lt;/code&gt;，包括 &lt;code&gt;ArrayList&lt;/code&gt;、&lt;code&gt;LinkedList&lt;/code&gt;、&lt;code&gt;TreeSet&lt;/code&gt;、&lt;code&gt;HashSet&lt;/code&gt;、&lt;code&gt;PriorityQueue&lt;/code&gt; 等许多类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请注意，因为这些数据结构实现了 &lt;code&gt;Collection&lt;/code&gt;，所以借助多态，我们可以把它们赋给静态类型为 &lt;code&gt;Collection&lt;/code&gt; 的变量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因此，我们由 &lt;code&gt;Collection&amp;#x3C;Node&gt;&lt;/code&gt; 对象组成的数组，可以使用许多不同类型的数据结构实例化，例如 &lt;code&gt;LinkedList&amp;#x3C;Node&gt;&lt;/code&gt; 或 &lt;code&gt;ArrayList&amp;#x3C;Node&gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在创建新的 &lt;code&gt;Collection&amp;#x3C;Node&gt;[]&lt;/code&gt; 并把它存入 &lt;code&gt;buckets&lt;/code&gt; 变量时，请注意：在 Java 中，&lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html#createArrays&quot;&gt;不能创建参数化类型的数组&lt;/a&gt;。&lt;code&gt;Collection&amp;#x3C;Node&gt;&lt;/code&gt; 是参数化类型，因为我们使用 &lt;code&gt;Node&lt;/code&gt; 类对 &lt;code&gt;Collection&lt;/code&gt; 类进行了参数化。因此，对于任何给定的 &lt;code&gt;size&lt;/code&gt;，表达式 &lt;code&gt;new Collection&amp;#x3C;Node&gt;[size]&lt;/code&gt; 都是非法的。要绕过这一限制，应改为创建 &lt;code&gt;new Collection[size]&lt;/code&gt;，其中 &lt;code&gt;size&lt;/code&gt; 是所需大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Collection[]&lt;/code&gt; 的元素可以是任意类型的集合，例如 &lt;code&gt;Collection&amp;#x3C;Integer&gt;&lt;/code&gt; 或 &lt;code&gt;Collection&amp;#x3C;Node&gt;&lt;/code&gt;。对于本实验，我们只会向 &lt;code&gt;Collection[]&lt;/code&gt; 中添加 &lt;code&gt;Collection&amp;#x3C;Node&gt;&lt;/code&gt; 类型的元素。&lt;/p&gt;
&lt;p&gt;每个 &lt;code&gt;MyHashMap*Buckets&lt;/code&gt; 类都会使用一种不同的数据结构实例化 &lt;code&gt;buckets&lt;/code&gt;。例如，&lt;code&gt;MyHashMapLLBuckets&lt;/code&gt; 使用 &lt;code&gt;new LinkedList&amp;#x3C;Node&gt;()&lt;/code&gt; 实例化 &lt;code&gt;buckets&lt;/code&gt;。实现这一点的机制，是工厂方法 &lt;code&gt;protected Collection&amp;#x3C;Node&gt; createBucket()&lt;/code&gt;；它只是返回一个实现了 &lt;code&gt;Collection&lt;/code&gt; 的数据结构。对于 &lt;code&gt;MyHashMap.java&lt;/code&gt;，你可以选择任何喜欢的数据结构。&lt;/p&gt;
&lt;p&gt;例如，如果选择 &lt;code&gt;LinkedList&lt;/code&gt;，&lt;code&gt;createBucket&lt;/code&gt; 的方法体就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;protected Collection&amp;#x3C;Node&gt; createBucket() {
    return new LinkedList&amp;#x3C;&gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建新的桶数据结构时，不得使用 &lt;code&gt;new&lt;/code&gt; 运算符，而必须使用 &lt;code&gt;createBucket&lt;/code&gt; 方法。起初这可能显得毫无用处，但它允许 &lt;code&gt;MyHashMap*Buckets.java&lt;/code&gt; 类重写 &lt;code&gt;createBucket&lt;/code&gt; 方法，从而为每一个桶提供不同的数据结构。&lt;/p&gt;
&lt;p&gt;这样，我们最终会得到多个不同的类（&lt;code&gt;MyHashMapTSBuckets.java&lt;/code&gt;、&lt;code&gt;MyHashMapPQBuckets.java&lt;/code&gt; 等）；它们都使用你在 &lt;code&gt;MyHashMap&lt;/code&gt; 中编写的实现，但为桶提供不同的类型（&lt;code&gt;TreeSet&lt;/code&gt;、&lt;code&gt;PQ&lt;/code&gt; 等）。我们甚至可以拥有一个桶本身也是另一个哈希表的哈希表（&lt;code&gt;MyHashMapHSBuckets.java&lt;/code&gt;）！随后，我们可以在类似 Lab 7 中所见的速度测试中，直接比较各个 &lt;code&gt;MyHashMap*Buckets.java&lt;/code&gt; 类。&lt;/p&gt;
&lt;p&gt;我们还提供了额外的工厂方法：&lt;code&gt;createTable&lt;/code&gt; 用于创建哈希表的底层数组，&lt;code&gt;createNode&lt;/code&gt; 用于创建新的 &lt;code&gt;Node&lt;/code&gt; 对象。使用 &lt;code&gt;new&lt;/code&gt; 运算符而不是工厂方法来创建底层数组和 &lt;code&gt;Node&lt;/code&gt; 对象也没有问题；我们只是为了统一而添加了这些方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;实现要求&lt;/h3&gt;
&lt;p&gt;你应当实现以下构造函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public MyHashMap();
public MyHashMap(int initialSize);
public MyHashMap(int initialSize, double loadFactor);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是 &lt;code&gt;MyHashMap&lt;/code&gt; 的一些额外要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;你的哈希映射最初应当拥有等于 &lt;code&gt;initialSize&lt;/code&gt; 的桶数量。当负载因子超过所设置的 &lt;code&gt;loadFactor&lt;/code&gt; 时，应增加 &lt;code&gt;MyHashMap&lt;/code&gt; 的大小。回忆一下，负载因子可以计算为 &lt;code&gt;loadFactor = N / M&lt;/code&gt;，其中 &lt;code&gt;N&lt;/code&gt; 是映射中的元素数量，&lt;code&gt;M&lt;/code&gt; 是桶的数量。负载因子表示平均每个桶中的元素数量。&lt;/p&gt;
&lt;p&gt;如果没有给出 &lt;code&gt;initialSize&lt;/code&gt; 和 &lt;code&gt;loadFactor&lt;/code&gt;，应设置默认值 &lt;code&gt;initialSize = 16&lt;/code&gt; 和 &lt;code&gt;loadFactor = 0.75&lt;/code&gt;（与 Java 的&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html&quot;&gt;内置 HashMap&lt;/a&gt;相同）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;应使用拉链法（separate chaining）处理冲突。除 &lt;code&gt;ArrayList&lt;/code&gt;、&lt;code&gt;LinkedList&lt;/code&gt;、&lt;code&gt;Collection&lt;/code&gt;、&lt;code&gt;HashSet&lt;/code&gt;、&lt;code&gt;Iterator&lt;/code&gt; 和 &lt;code&gt;Set&lt;/code&gt; 外，不得导入任何库。这意味着对于 &lt;code&gt;MyHashMap.java&lt;/code&gt;，你应当使用 &lt;code&gt;ArrayList&lt;/code&gt;、&lt;code&gt;LinkedList&lt;/code&gt; 或 &lt;code&gt;HashSet&lt;/code&gt; 之一作为桶类型。有关应如何实现拉链法的更多细节，请参阅上面的“起始代码”一节。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因为我们使用 &lt;code&gt;Collection&amp;#x3C;Node&gt;[]&lt;/code&gt; 作为 &lt;code&gt;buckets&lt;/code&gt;，所以实现 &lt;code&gt;MyHashMap&lt;/code&gt; 时，只能使用 &lt;code&gt;Collection&lt;/code&gt; 接口所支持的方法。你唯一需要的方法是 &lt;code&gt;add&lt;/code&gt;、&lt;code&gt;remove&lt;/code&gt; 和 &lt;code&gt;iterator&lt;/code&gt;。在 &lt;code&gt;Collection&lt;/code&gt; 中搜索 &lt;code&gt;Node&lt;/code&gt; 时，只需遍历 &lt;code&gt;Collection&lt;/code&gt;，找到其 &lt;code&gt;key&lt;/code&gt; 与所搜索键 &lt;code&gt;.equals()&lt;/code&gt; 的 &lt;code&gt;Node&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调整大小时，务必进行乘法式扩容，而不是加法式扩容。不要求缩小容量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;假定插入对象的 &lt;code&gt;hashCode&lt;/code&gt; 能够很好地分散元素，你的所有 &lt;code&gt;MyHashMap&lt;/code&gt; 操作都应当具有摊还常数时间（回忆：Java 中每个 &lt;code&gt;Object&lt;/code&gt; 都有自己的 &lt;code&gt;hashCode()&lt;/code&gt; 方法）。注意：&lt;code&gt;hashCode()&lt;/code&gt; 可能返回负值！编写代码时请考虑这一点。有关如何干净地处理这种情况的提示，请查看下面链接的讲座幻灯片。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果多次插入同一个键，每次都应更新其值。你可以假定永远不会插入 &lt;code&gt;null&lt;/code&gt; 键。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;可以使用 &lt;code&gt;TestMyHashMap.java&lt;/code&gt; 测试实现。如果选择实现额外的 &lt;code&gt;remove&lt;/code&gt; 方法，我们在 &lt;code&gt;TestHashMapExtra.java&lt;/code&gt; 中提供了测试。如果你正确实现了泛型 &lt;code&gt;Collection&lt;/code&gt; 桶，还应当能够通过 &lt;code&gt;TestMyHashMapBuckets.java&lt;/code&gt; 中的测试。&lt;code&gt;TestHashMapBuckets.java&lt;/code&gt; 文件只是针对每个使用不同桶数据结构的 Map 子类，调用 &lt;code&gt;TestMyHashMap.java&lt;/code&gt; 中的方法。&lt;/p&gt;
&lt;p&gt;离开本节之前，确保你能够通过 &lt;code&gt;TestMyHashMap.java&lt;/code&gt; 和 &lt;code&gt;TestMyHashMapBuckets.java&lt;/code&gt; 中的测试。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;资源&lt;/h3&gt;
&lt;p&gt;下面这些资源可能会有帮助：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;课程参考资料页面中《Data Structures Into Java》第 136 页和第 137 页的 HashMap 代码。&lt;/li&gt;
&lt;li&gt;可选《Algorithms》教材的&lt;a href=&quot;https://algs4.cs.princeton.edu/34hash/&quot;&gt;第 3.4 章&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;可选教材中的 &lt;a href=&quot;https://algs4.cs.princeton.edu/code/&quot;&gt;HashTable 代码&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ULLMap.java&lt;/code&gt;（已提供）：一个能够正常工作的、基于无序链表的 &lt;code&gt;Map61B&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;关于 &lt;a href=&quot;https://docs.google.com/&quot;&gt;HashMaps&lt;/a&gt;、&lt;a href=&quot;https://docs.google.com/&quot;&gt;继承&lt;/a&gt;和&lt;a href=&quot;https://docs.google.com/&quot;&gt;子类型多态&lt;/a&gt;的讲座幻灯片。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;HashMap 速度测试&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;InsertRandomSpeedTest.java&lt;/code&gt; 和 &lt;code&gt;InsertInOrderSpeedTest.java&lt;/code&gt; 中提供了两个交互式速度测试。在完成 &lt;code&gt;MyHashMap&lt;/code&gt; 之前，不要尝试运行这些测试。准备好后，可以在 IntelliJ 中运行它们。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;InsertRandomSpeedTest&lt;/code&gt; 类会测试你的 &lt;code&gt;MyHashMap&lt;/code&gt;、已提供的 &lt;code&gt;ULLMap&lt;/code&gt; 和 Java 内置 &lt;code&gt;HashMap&lt;/code&gt; 在插入元素时的速度。它会询问用户输入规模 &lt;code&gt;N&lt;/code&gt;，然后生成 &lt;code&gt;N&lt;/code&gt; 个长度为 &lt;code&gt;10&lt;/code&gt; 的 String，并将它们作为 &lt;code&gt;&amp;#x3C;String, Integer&gt;&lt;/code&gt; 对插入映射中。&lt;/p&gt;
&lt;p&gt;尝试运行它，看看随着 &lt;code&gt;N&lt;/code&gt; 增大，你的数据结构与朴素实现和工业级实现相比如何扩展。把结果记录在所提供的 &lt;code&gt;lab8/speedTestResults.txt&lt;/code&gt; 文件中。结果没有规定的标准格式，也没有规定所需的数据点数量。&lt;/p&gt;
&lt;p&gt;现在尝试运行 &lt;code&gt;InsertInOrderSpeedTest&lt;/code&gt;。它的行为与 &lt;code&gt;InsertRandomSpeedTest&lt;/code&gt; 类似，但这一次，&lt;code&gt;&amp;#x3C;String, Integer&gt;&lt;/code&gt; 键值对中的 String 会按照&lt;a href=&quot;https://en.wikipedia.org/wiki/Lexicographic_order&quot;&gt;字典序递增顺序&lt;/a&gt;插入。请注意，与 Lab 7 不同，你的代码性能应当大致接近 Java 的内置解答——例如控制在大约 &lt;code&gt;10&lt;/code&gt; 倍以内。这告诉我们，与最先进的 &lt;code&gt;TreeMap&lt;/code&gt; 相比，最先进的 &lt;code&gt;HashMap&lt;/code&gt; 相对容易实现。&lt;/p&gt;
&lt;p&gt;什么时候使用 &lt;code&gt;BSTMap&lt;/code&gt;/&lt;code&gt;TreeMap&lt;/code&gt; 会比使用 &lt;code&gt;HashMap&lt;/code&gt; 更好？与你的实验同伴讨论这个问题，并把答案添加到 &lt;code&gt;speedTestResults.txt&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;更换桶类型：速度测试&lt;/h2&gt;
&lt;p&gt;如果你正确实现了泛型 &lt;code&gt;Collection&lt;/code&gt; 桶，大部分工作就已经完成了！我们可以直接比较不同的数据结构 &lt;code&gt;MyHashMap*Buckets.java&lt;/code&gt;。我们提供了 &lt;code&gt;speed/BucketsSpeedTest.java&lt;/code&gt;，它是一个交互式测试：首先询问用户一个整数 &lt;code&gt;L&lt;/code&gt;，表示后续操作中所使用 String 的长度；然后在循环中询问用户一个整数 &lt;code&gt;N&lt;/code&gt;，并对以下五种数据结构分别进行速度测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MyHashMapALBuckets&lt;/code&gt;，使用 &lt;code&gt;ArrayList&lt;/code&gt; 桶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyHashMapLLBuckets&lt;/code&gt;，使用 &lt;code&gt;LinkedList&lt;/code&gt; 桶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyHashMapTSBuckets&lt;/code&gt;，使用 &lt;code&gt;TreeSet&lt;/code&gt; 桶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyHashMapPQBuckets&lt;/code&gt;，使用 &lt;code&gt;PriorityQueue&lt;/code&gt; 桶。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyHashMapHSBuckets&lt;/code&gt;，使用 &lt;code&gt;HashSet&lt;/code&gt; 桶。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尝试运行它，比较不同实现如何随 &lt;code&gt;N&lt;/code&gt; 扩展。与你的实验同伴讨论结果，并把回答记录在 &lt;code&gt;speedTestResults.txt&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;你可能会注意到，我们的 &lt;code&gt;MyHashMapTSBuckets&lt;/code&gt; 和 &lt;code&gt;MyHashMapHSBuckets&lt;/code&gt; 实现通过遍历整个数据结构来搜索 &lt;code&gt;Node&lt;/code&gt;。但根据我们已经掌握的知识，树和哈希表支持比这更高效的查找。&lt;/p&gt;
&lt;p&gt;如果能够在 &lt;code&gt;TreeSet&lt;/code&gt; 上使用对数时间搜索，或者在 &lt;code&gt;HashSet&lt;/code&gt; 上使用常数时间搜索，我们的哈希表会加速吗？这里不需要实现任何新内容，只需与实验同伴讨论，并把想法记录在 &lt;code&gt;speedTestResults.txt&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;可选练习&lt;/h2&gt;
&lt;p&gt;这一部分不会评分，但你仍然可以从自动评分器获得反馈。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;MyHashMap&lt;/code&gt; 类中实现 &lt;code&gt;remove(K key)&lt;/code&gt; 和 &lt;code&gt;remove(K key, V value)&lt;/code&gt;。作为额外挑战，请在不使用第二个实例变量存储键集合的情况下实现 &lt;code&gt;keySet()&lt;/code&gt; 和 &lt;code&gt;iterator&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;remove&lt;/code&gt;，如果参数键在 &lt;code&gt;MyHashMap&lt;/code&gt; 中不存在，应返回 &lt;code&gt;null&lt;/code&gt;。否则，删除键值对 &lt;code&gt;(key, value)&lt;/code&gt; 并返回 &lt;code&gt;value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;实验总结与提交&lt;/h2&gt;
&lt;p&gt;实验结束时，你的 TA 会讲解参考解答。如果你尚未完成实验，这会很有帮助，因为我们不希望你在实验课之外被这个实验困住太久。（这也是鼓励你参加实验课的一个理由！）&lt;/p&gt;
&lt;p&gt;确保提交完成的 &lt;code&gt;MyHashMap.java&lt;/code&gt; 和 &lt;code&gt;speedTestResults.txt&lt;/code&gt;，并像往常一样通过 Git 和 Gradescope 提交。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab8/lab8&quot;&gt;https://sp21.datastructur.es/materials/lab/lab8/lab8&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 0：2048</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-0-2048</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-0-2048</guid><description>CS61B Spring 2021 Project 0：2048中文学习资料。</description><content:encoded>&lt;h2&gt;截止时间：2021 年 1 月 29 日&lt;/h2&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;本项目的高层概览视频位于：&lt;a href=&quot;https://youtu.be/Xzihuj_JZBI&quot;&gt;https://youtu.be/Xzihuj_JZBI&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;本项目的目的是让你有机会熟悉 Java，以及课程使用的各种工具，例如 IntelliJ IDE，以及用于编写和运行单元测试的 JUnit。虽然你会在 &lt;code&gt;proj0&lt;/code&gt; 文件夹中看到许多文件和大量代码，但任务只位于 &lt;code&gt;Model.java&lt;/code&gt; 中，而且仅限于四个方法。&lt;/p&gt;
&lt;p&gt;评分只取决于程序是否能够按照测试要求正确工作，以及你是否提交了指定内容。本项目没有隐藏测试。之后的作业还会对代码风格进行评分，但本项目不会。不过，我们仍然建议遵循课程的 &lt;a href=&quot;https://sp21.datastructur.es/materials/guides/style-guide&quot;&gt;style61b 指南&lt;/a&gt;，因为它会帮助你编写整洁的代码，只是本项目不会据此扣分。&lt;/p&gt;
&lt;p&gt;这份作业规格相当长，而且起始代码很多。我们建议你在开始编程之前完整阅读规格。刚开始时，它很可能会让你觉得不知所措。为了彻底理解内容，你可能需要多次重新阅读规格中的某些部分；而且在完成项目较早的部分之前，后面的某些内容可能不会完全说得通。&lt;/p&gt;
&lt;p&gt;最终，我们希望这段经历能让你获得一种力量感：你成功驾驭了这样一项庞大的任务。&lt;/p&gt;
&lt;h2&gt;游戏&lt;/h2&gt;
&lt;p&gt;你很可能见过，甚至玩过 “2048”。这是 Gabriele Cirulli 编写的一款单人电脑游戏，它又基于 Veewo Studio 更早的游戏 “1024”（可以查看他的 &lt;a href=&quot;https://gabrielecirulli.github.io/2048/&quot;&gt;2048 在线版本&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;在本项目中，你将构建这个游戏的核心逻辑。也就是说，我们已经完成了所有 GUI 代码、按键处理以及大量其他脚手架。你的工作将是完成其中最重要、也最有趣的部分。&lt;/p&gt;
&lt;p&gt;具体来说，你将填写 &lt;code&gt;Model.java&lt;/code&gt; 文件中的 4 个方法，它们控制用户按下某些按键之后会发生什么。&lt;/p&gt;
&lt;p&gt;游戏本身相当简单。游戏在一个 $4\times4$ 的方格网格上进行，每个方格可以为空，也可以包含一块写有某个整数的方块；这个整数是大于等于 2 的 2 的幂。在第一次移动之前，应用程序会在最初为空的棋盘上随机选择一个方格，并添加一块值为 2 或 4 的方块。2 或 4 的选择是随机的：选择 2 的概率为 75%，选择 4 的概率为 25%。&lt;/p&gt;
&lt;p&gt;然后，玩家通过方向键选择一个方向来倾斜棋盘：北、南、东或西。所有方块都会沿该方向滑动，直到运动方向上不再有空位（也可能一开始就没有空位）。一个方块可能会与另一个方块合并，从而为玩家获得分数。&lt;/p&gt;
&lt;p&gt;下面的 GIF 展示了进行几步移动后的效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/28aaf1f8c736-example-2048.gif&quot; alt=&quot;2048 示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;下面是上图所展示的、发生合并时需要遵循的完整规则。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;两块数值相同的方块会合并成一块数值为原来两倍的方块。&lt;/li&gt;
&lt;li&gt;一次倾斜中，由合并产生的方块不会再次合并。例如，如果有 &lt;code&gt;[X, 2, 2, 4]&lt;/code&gt;，其中 X 表示空位，并把方块向左移动，结果应当是 &lt;code&gt;[4, 4, X, X]&lt;/code&gt;，而不是 &lt;code&gt;[8, X, X, X]&lt;/code&gt;。这是因为最左侧的 4 已经参与过一次合并，所以不应当再次合并。&lt;/li&gt;
&lt;li&gt;如果沿运动方向有三个相邻方块具有相同数值，那么运动方向上靠前的两块会合并，而后面的那块不会。例如，如果有 &lt;code&gt;[X, 2, 2, 2]&lt;/code&gt; 并向左移动，结果应当是 &lt;code&gt;[4, 2, X, X]&lt;/code&gt;，而不是 &lt;code&gt;[2, 4, X, X]&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;作为这些规则的推论，如果沿运动方向有四块相邻方块具有相同数值，它们会形成两块合并后的方块。例如，如果有 &lt;code&gt;[4, 4, 4, 4]&lt;/code&gt; 并向左移动，结果是 &lt;code&gt;[8, 8, X, X]&lt;/code&gt;。这是因为根据规则 3，靠前的两块会合并，之后后面的两块也会合并；但根据规则 2，这两块新合并出的方块（本例中的 8）不会在这次倾斜中继续彼此合并。&lt;/p&gt;
&lt;p&gt;上面的动画 GIF 中包含了上述三条规则各自的应用，因此请多看几遍，确保充分理解这些规则。&lt;/p&gt;
&lt;p&gt;为了测试理解，你应当完成课程提供的 &lt;a href=&quot;https://forms.gle/&quot;&gt;Google Form 小测&lt;/a&gt;。这个小测不计入 61B 课程成绩。&lt;/p&gt;
&lt;p&gt;如果一次倾斜没有改变棋盘状态，就不会随机生成新方块。否则，程序会在一个空方格上添加一块随机生成的方块。注意：**你的代码不会负责添加任何新方块！**这一部分我们已经替你完成。&lt;/p&gt;
&lt;p&gt;你可能还会注意到屏幕底部有一个 “Score” 字段，它会随着移动而更新。分数不会每一步都发生变化，而只会在两块方块合并时变化。你的代码需要更新分数。&lt;/p&gt;
&lt;p&gt;每当两块方块合并成一块更大的方块，玩家会获得等于新方块数值的分数。当当前玩家没有任何可用移动（任何方向的倾斜都无法改变棋盘），或者一次移动形成了值为 2048 的方块时，游戏结束。你的代码负责检测游戏何时结束。&lt;/p&gt;
&lt;p&gt;“Max Score” 是用户在当前游戏会话中取得过的最高分。只有游戏结束时它才会更新，因此在上面的动画 GIF 示例中，它始终保持为 0。&lt;/p&gt;
&lt;h2&gt;作业理念与程序设计&lt;/h2&gt;
&lt;p&gt;本节规格的视频概览位于：&lt;a href=&quot;https://youtu.be/3YbIOga6ZdQ&quot;&gt;https://youtu.be/3YbIOga6ZdQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在这个项目中，我们提供了&lt;strong&gt;大量&lt;/strong&gt;起始代码，其中使用了许多尚未讲解的 Java 语法，甚至还有一些本课程永远不会讲到的语法。&lt;/p&gt;
&lt;p&gt;这里的想法是：在真实世界中，你经常会面对自己并未完全理解的代码库，需要通过试验和摸索来获得想要的结果。不要担心；下周进入 Project 1 时，你会有机会从头开始编写代码。&lt;/p&gt;
&lt;p&gt;下面将说明 Paul Hilfinger 创建的骨架代码背后的一些架构思想。你不需要理解每一个细节，不过这些内容可能会很有意思。&lt;/p&gt;
&lt;p&gt;骨架代码体现了两种常见设计模式：Model-View-Controller 模式（MVC）和 Observer 模式。&lt;/p&gt;
&lt;p&gt;MVC 模式把问题划分为三个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt; 表示正在被描述和操作的主题；在本项目中，它包含棋盘游戏的状态以及修改状态的规则。我们的 Model 位于 &lt;code&gt;Model&lt;/code&gt;、&lt;code&gt;Side&lt;/code&gt;、&lt;code&gt;Board&lt;/code&gt; 和 &lt;code&gt;Tile&lt;/code&gt; 类中。&lt;code&gt;Model&lt;/code&gt; 的实例变量可以完全确定游戏状态。注意：你只会修改 &lt;code&gt;Model&lt;/code&gt; 类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;View&lt;/strong&gt; 是模型的视图，它向用户显示游戏状态。我们的 View 位于 &lt;code&gt;GUI&lt;/code&gt; 和 &lt;code&gt;BoardWidget&lt;/code&gt; 类中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller&lt;/strong&gt; 是游戏控制器，它把用户操作转换为对模型的操作。我们的 Controller 主要位于 &lt;code&gt;Game&lt;/code&gt; 类中，不过它也会使用 GUI 类读取按键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MVC 模式不是 61B 的课程主题，考试或未来项目中也不会要求你了解或理解这种设计模式。&lt;/p&gt;
&lt;p&gt;骨架使用的第二种模式是 “Observer 模式”。简单地说，这意味着 Model 实际上不会主动把变化报告给 View。相反，View 会把自己注册为 &lt;code&gt;Model&lt;/code&gt; 对象的观察者。这是一个略微高级的话题，因此这里不提供更多信息。&lt;/p&gt;
&lt;p&gt;现在介绍你将接触的各个类。&lt;/p&gt;
&lt;h3&gt;Tile&lt;/h3&gt;
&lt;p&gt;这个类表示棋盘上带数字的方块。如果一个 &lt;code&gt;Tile&lt;/code&gt; 类型的变量为 &lt;code&gt;null&lt;/code&gt;，它会被视为棋盘上的空方格。你不需要创建任何 &lt;code&gt;Tile&lt;/code&gt; 对象，不过由于会在 &lt;code&gt;Model&lt;/code&gt; 类中使用它们，你需要理解它们。你需要使用这个类的唯一方法是 &lt;code&gt;.value()&lt;/code&gt;，它会返回给定方块的数值。例如，如果 &lt;code&gt;Tile t&lt;/code&gt; 对应一个数值为 8 的方块，那么 &lt;code&gt;t.value()&lt;/code&gt; 会返回 &lt;code&gt;8&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Side&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Side&lt;/code&gt; 类是一种特殊类型的类，称为 &lt;code&gt;Enum&lt;/code&gt;。枚举与普通类类似，但功能受到限制。具体来说，枚举只能取有限集合中的某个值。在这里，四个方向各自对应一个值：&lt;code&gt;NORTH&lt;/code&gt;、&lt;code&gt;SOUTH&lt;/code&gt;、&lt;code&gt;EAST&lt;/code&gt; 和 &lt;code&gt;WEST&lt;/code&gt;。你不需要使用这个类的任何方法，也不需要操作它的实例变量。&lt;/p&gt;
&lt;p&gt;枚举可以使用类似 &lt;code&gt;Side s = Side.NORTH&lt;/code&gt; 的语法赋值。注意，我们不是使用 &lt;code&gt;new&lt;/code&gt; 关键字，而是直接把 &lt;code&gt;Side&lt;/code&gt; 值设为四个值之一。类似地，假如有函数 &lt;code&gt;public static void printSide(Side s)&lt;/code&gt;，可以用 &lt;code&gt;printSide(Side.NORTH)&lt;/code&gt; 调用它，把值 &lt;code&gt;NORTH&lt;/code&gt; 传给函数。&lt;/p&gt;
&lt;p&gt;如果想进一步了解 Java 枚举，请查看 &lt;a href=&quot;https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html&quot;&gt;Java Enum 教程&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;Model&lt;/h3&gt;
&lt;p&gt;这个类表示游戏的完整状态。一个 &lt;code&gt;Model&lt;/code&gt; 对象表示一局 2048。它拥有表示棋盘状态的实例变量（例如所有 &lt;code&gt;Tile&lt;/code&gt; 对象的位置、分数等），还拥有多种方法。当你做到项目第四个也是最后一个任务（编写 &lt;code&gt;tilt&lt;/code&gt; 方法）时，一项挑战就是判断哪些方法和实例变量有用。&lt;/p&gt;
&lt;h3&gt;Board&lt;/h3&gt;
&lt;p&gt;这个类表示方块棋盘本身。你将使用它的三个方法：&lt;code&gt;setViewingPerspective&lt;/code&gt;、&lt;code&gt;tile&lt;/code&gt; 和 &lt;code&gt;move&lt;/code&gt;。作为可选试验，也可以使用 &lt;code&gt;getRandomNonNullTile&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;本作业中，你只会编辑 &lt;code&gt;Model.java&lt;/code&gt; 文件。Gradescope 只会获取你的 &lt;code&gt;Model.java&lt;/code&gt;，并使用其他文件的骨架版本；因此，如果修改了 &lt;code&gt;Tile.java&lt;/code&gt;，Gradescope 不会识别这些修改。&lt;/p&gt;
&lt;h2&gt;开始项目&lt;/h2&gt;
&lt;p&gt;首先，确保已经完成 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab1/lab1&quot;&gt;Lab 1&lt;/a&gt;。如果没有完成 Lab 1 要求的全部必要配置，就无法进行这个项目。&lt;/p&gt;
&lt;h3&gt;获取骨架文件&lt;/h3&gt;
&lt;p&gt;首先，确保仓库中的所有内容都已正确更新并提交。开始之前，在 &lt;code&gt;sp21-s***&lt;/code&gt; 目录中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令应当报告目录是干净的，而且没有任何需要添加和提交的未跟踪文件。如果存在，就把它们添加并提交。&lt;/p&gt;
&lt;p&gt;永远不要在没有完成这件事的情况下开始一个新项目。&lt;/p&gt;
&lt;p&gt;要获取骨架文件，请在 &lt;code&gt;sp21-s***&lt;/code&gt; 目录中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，你会在学生仓库中看到一个 &lt;code&gt;proj0&lt;/code&gt; 文件夹，其中包含所有骨架代码。&lt;/p&gt;
&lt;p&gt;在极少数我们必须更新骨架的情况下，可以使用同一条命令，把相同的更新合并到项目中。&lt;/p&gt;
&lt;h3&gt;重新开始：Skeleton&lt;/h3&gt;
&lt;p&gt;与其努力让当前代码工作，你有时可能想完全重新开始。通过 Git 可以做到这一点！只需在 &lt;code&gt;sp21-s***&lt;/code&gt; 目录中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git checkout skeleton/master -- proj0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：这条命令会清除 &lt;code&gt;proj0&lt;/code&gt; 目录中所有尚未提交的更改。因此，如果认为当前代码以后可能还有用，请先创建一个提交，再运行这条命令。之后，你可以使用类似命令把 &lt;code&gt;proj0&lt;/code&gt; 目录恢复为刚才创建的提交中的状态。&lt;/p&gt;
&lt;h3&gt;IntelliJ 配置&lt;/h3&gt;
&lt;p&gt;现在在 IntelliJ 中打开文件。首先启动 IntelliJ。它会显示最近项目列表，但由于尚未开始这项作业，项目不会出现在其中。要打开项目，请单击应用窗口右上角的 “Open” 按钮，这会打开操作系统的文件浏览器。进入学生仓库中的 &lt;code&gt;proj0&lt;/code&gt; 文件夹，然后单击打开：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/20022dee9862-intellij-pre-open.png&quot; alt=&quot;打开项目&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/81161dae4a08-intellij-open.png&quot; alt=&quot;选择项目目录&quot;&gt;&lt;/p&gt;
&lt;p&gt;屏幕左上角会显示 &lt;code&gt;proj0&lt;/code&gt; 目录中的文件和文件夹列表。如果没有看到，请单击 &lt;code&gt;proj0&lt;/code&gt; 文件夹旁的向下箭头来展开。它应当如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/1dd92ccfd0b8-list-files.png&quot; alt=&quot;文件列表&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.idea&lt;/code&gt; 文件夹由 IntelliJ 生成，用于存储各种设置，可以忽略。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;game2048&lt;/code&gt; 文件夹是所有 Java 文件的源目录。你需要完成的所有工作都位于这里。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;javalib&lt;/code&gt; 文件夹是一个 Java 库。它包含三个 &lt;code&gt;.jar&lt;/code&gt; 文件，即我们想使用的预编译文件。这三个文件让我们能够运行 JUnit，还提供一些 UC Berkeley 特有的功能，以便在漂亮的窗口中渲染游戏。&lt;/p&gt;
&lt;p&gt;IntelliJ 通常足够聪明，能够自动完成其余配置；不过，如果你的 IntelliJ 遇到困难，下面会逐步说明配置过程。&lt;/p&gt;
&lt;p&gt;首先，告诉 IntelliJ 我们使用 Java 15。进入 &lt;code&gt;File &gt; Project Structure&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/fefe3f9d8a78-opening-project-structure.png&quot; alt=&quot;打开 Project Structure&quot;&gt;&lt;/p&gt;
&lt;p&gt;单击 “Edit” 按钮左侧的方框，从中选择 JDK，也就是 Java 版本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/7b9cd3b85542-java15.png&quot; alt=&quot;Java 15&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来，告诉 IntelliJ 我们要使用 &lt;code&gt;javalib&lt;/code&gt; 文件夹中的 &lt;code&gt;.jar&lt;/code&gt; 文件。仍然位于 Project Structure 中时，在左侧单击 Project Settings 下的 “Library”。如果已经看到 &lt;code&gt;javalib&lt;/code&gt; 被添加，就不需要做任何事。否则，单击 “+” 按钮，再选择 “Java”，这会打开操作系统的文件浏览器；然后选择 &lt;code&gt;javalib&lt;/code&gt; 文件夹。最后，单击屏幕右下角的 “Apply”，再单击蓝色 “OK” 按钮。&lt;/p&gt;
&lt;p&gt;整个配置过程大致如下（抱歉，图片比较模糊）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/7e36c59cbafd-intellij-setup.gif&quot; alt=&quot;IntelliJ 配置&quot;&gt;&lt;/p&gt;
&lt;p&gt;为了确保配置正常，打开 &lt;code&gt;game2048&lt;/code&gt; 文件夹，并右键单击 Java 文件 &lt;code&gt;Main&lt;/code&gt;。你会看到若干选项，我们关心的是绿色的 “Run Main.main()” 按钮。它应当如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/3f7f193ca2a6-run-main.png&quot; alt=&quot;运行 Main&quot;&gt;&lt;/p&gt;
&lt;p&gt;单击它启动 2048 游戏。一个带有空棋盘的新窗口会出现。现在先关闭窗口；到规格中 “主要任务：构建游戏逻辑” 一节时，我们还会回来。&lt;/p&gt;
&lt;p&gt;如果什么都没有出现，说明配置不正确。重新完成上面的步骤，确保没有遗漏，不过不要在这件事上花超过 10 分钟。配置问题最好在 TA 帮助下解决，也就是说，应当在 Ed 上发帖或前往 Office Hours。如果在 Ed 上发帖，需要告诉我们已经完成和尝试过的全部操作，以便清楚了解错误。请附上所有内容的截图，尤其是收到的错误消息。&lt;/p&gt;
&lt;p&gt;你可能遇到一个奇怪现象：代码能够正确编译和运行，但 IntelliJ 仍然显示红色下划线。进入 &lt;code&gt;Model&lt;/code&gt; 类，找到 &lt;code&gt;addTile&lt;/code&gt; 方法。这个方法由我们提供，但你可能看到 &lt;code&gt;tile&lt;/code&gt; 变量下面有红线，并出现如下错误消息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/886b9446d10d-intellij-error.png&quot; alt=&quot;IntelliJ 错误&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过我们清楚地知道它是正确的，因为 1）代码能够运行，2）这是起始代码！IntelliJ 虽然非常强大，但有时也会像这样判断错误。要修复它，进入 &lt;code&gt;File &gt; Invalidate / Restart&lt;/code&gt;，再在出现的窗口中单击 “Invalidate and Restart”。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/63fe673b576c-invalidate-caches.png&quot; alt=&quot;Invalidate Caches&quot;&gt;&lt;/p&gt;
&lt;p&gt;IntelliJ 会重新索引 JDK，并从头配置项目，这需要一两分钟。完成后，源文件中应当不再有红色下划线。&lt;/p&gt;
&lt;p&gt;只有上述配置正常，才能完成项目，因此请把尽快完成配置作为首要任务。&lt;/p&gt;
&lt;h2&gt;你的任务&lt;/h2&gt;
&lt;p&gt;本项目的工作是修改并完成 &lt;code&gt;Model&lt;/code&gt; 类，具体包括 &lt;code&gt;emptySpaceExists&lt;/code&gt;、&lt;code&gt;maxTileExists&lt;/code&gt;、&lt;code&gt;atLeastOneMoveExists&lt;/code&gt; 和 &lt;code&gt;tilt&lt;/code&gt; 方法。其他所有内容已经为你实现。我们建议按这个顺序完成。前两个相对直接，第三个（&lt;code&gt;atLeastOneMoveExists&lt;/code&gt;）更困难，最后的 &lt;code&gt;tilt&lt;/code&gt; 方法可能会相当困难。我们预计 &lt;code&gt;tilt&lt;/code&gt; 需要 3 到 10 小时完成。&lt;/p&gt;
&lt;p&gt;前三个方法处理游戏结束条件，最后的 &lt;code&gt;tilt&lt;/code&gt; 方法会在用户按键后修改棋盘。阅读非常短的 &lt;code&gt;checkGameOver&lt;/code&gt; 方法体，可以了解这些方法如何用于判断游戏是否结束。&lt;/p&gt;
&lt;p&gt;先看前三个方法。&lt;/p&gt;
&lt;h3&gt;public static boolean emptySpaceExists(Board b)&lt;/h3&gt;
&lt;p&gt;如果给定棋盘中任何方块为 &lt;code&gt;null&lt;/code&gt;，这个方法应当返回 &lt;code&gt;true&lt;/code&gt;。本项目中&lt;strong&gt;绝对不要&lt;/strong&gt;以任何方式修改 &lt;code&gt;Board.java&lt;/code&gt;。在这个方法中，你会需要使用 &lt;code&gt;Board&lt;/code&gt; 类的 &lt;code&gt;tile(int col, int row)&lt;/code&gt; 和 &lt;code&gt;size()&lt;/code&gt; 方法，不需要其他方法。&lt;/p&gt;
&lt;p&gt;注意：我们在设计 &lt;code&gt;Board&lt;/code&gt; 类时使用了特殊关键字 &lt;code&gt;private&lt;/code&gt;，它不允许你直接访问 &lt;code&gt;Board&lt;/code&gt; 的实例变量。例如，如果尝试访问 &lt;code&gt;b.values[0][0]&lt;/code&gt;，代码不会工作。这是一件好事！它会迫使你学习使用 &lt;code&gt;tile&lt;/code&gt; 方法，而整个项目的其余部分也会一直使用这个方法。&lt;/p&gt;
&lt;p&gt;尝试打开 &lt;code&gt;TestEmptySpace.java&lt;/code&gt; 并运行测试。你应该会看到 6 个测试失败、2 个测试通过。正确编写 &lt;code&gt;emptySpaceExists&lt;/code&gt; 后，&lt;code&gt;TestEmptySpace&lt;/code&gt; 中全部 8 个测试都应当通过。&lt;/p&gt;
&lt;p&gt;有关怎样开始编写这个方法的快速概览，请查看课程提供的视频。&lt;/p&gt;
&lt;h3&gt;public static boolean maxTileExists(Board b)&lt;/h3&gt;
&lt;p&gt;如果棋盘中任何方块的数值等于获胜方块值 2048，这个方法应当返回 &lt;code&gt;true&lt;/code&gt;。注意，不要在代码中硬编码常量 2048，而应当使用 &lt;code&gt;MAX_PIECE&lt;/code&gt;，它是 &lt;code&gt;Model&lt;/code&gt; 类的一部分。换句话说，不要写 &lt;code&gt;if (x == 2048)&lt;/code&gt;，而应当写 &lt;code&gt;if (x == MAX_PIECE)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在代码中保留 &lt;code&gt;2048&lt;/code&gt; 这样的硬编码数字是一种不良编程实践，有时称为“魔法数字”。魔法数字的危险在于，如果修改了代码中某处的数字，却没有修改另一处，可能会得到意料之外的结果。使用 &lt;code&gt;MAX_PIECE&lt;/code&gt; 这样的变量，可以确保所有位置一起改变。&lt;/p&gt;
&lt;p&gt;编写方法后，&lt;code&gt;TestMaxTileExists.java&lt;/code&gt; 中的测试应当通过。&lt;/p&gt;
&lt;h3&gt;public static boolean atLeastOneMoveExists(Board b)&lt;/h3&gt;
&lt;p&gt;这个方法更有挑战性。如果存在任何有效移动，它应当返回 &lt;code&gt;true&lt;/code&gt;。“有效移动”是指：在玩 2048 时，如果用户能够按下某个按键（UP、DOWN、LEFT 或 RIGHT），并使至少一块方块移动，那么这次按键就是有效移动。&lt;/p&gt;
&lt;p&gt;存在有效移动的情况有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;棋盘上至少有一个空位。&lt;/li&gt;
&lt;li&gt;有两块相邻方块具有相同数值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，对于下面的棋盘，应当返回 &lt;code&gt;true&lt;/code&gt;，因为至少有一个空位。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|    |   2|    |
|   4|   4|   2|   2|
|    |   4|    |    |
|   2|   4|   4|   8|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于下面的棋盘，应当返回 &lt;code&gt;false&lt;/code&gt;。无论在 2048 中按下哪个按钮，什么都不会发生；也就是说，没有两块相邻方块具有相同数值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|   4|   2|   4|
|  16|   2|   4|   2|
|   2|   4|   2|   4|
|   4|   2|   4|   2|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于下面的棋盘，应当返回 &lt;code&gt;true&lt;/code&gt;，因为向左或向右移动会合并两块 64，而向上或向下移动会合并两块 32。换句话说，至少存在两块数值相同的相邻方块。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|   4|  64|  64|
|  16|   2|   4|   8|
|   2|   4|   2|  32|
|   4|   2|   4|  32|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写方法后，&lt;code&gt;TestAtLeastOneMoveExists.java&lt;/code&gt; 中的测试应当通过。&lt;/p&gt;
&lt;h2&gt;主要任务：构建游戏逻辑&lt;/h2&gt;
&lt;p&gt;作业的第四个也是最后一个部分是实现 &lt;code&gt;tilt&lt;/code&gt;。只有在 &lt;code&gt;TestEmptySpace&lt;/code&gt;、&lt;code&gt;TestMaxTileExists&lt;/code&gt; 和 &lt;code&gt;TestAtLeastOneMoveExists&lt;/code&gt; 的全部测试都通过之后，才应当开始这个方法。&lt;/p&gt;
&lt;p&gt;计算机科学本质上只关乎一件事：管理复杂性。编写 &lt;code&gt;tilt&lt;/code&gt; 方法是一段丰富的经历，会让你有机会亲自尝试管理复杂性。我必须警告你，这很可能会是一次令人沮丧的经历。你可能会尝试若干种最终失败的方法，然后不得不重新开始。&lt;/p&gt;
&lt;p&gt;在讨论 &lt;code&gt;tilt&lt;/code&gt; 应当怎样工作之前，先尝试运行游戏。&lt;/p&gt;
&lt;p&gt;打开 &lt;code&gt;Main&lt;/code&gt; 类并单击运行按钮。游戏窗口应当出现。尝试按方向键，你会看到什么都没有发生。这是因为尚未实现 &lt;code&gt;tilt&lt;/code&gt; 方法。完成 &lt;code&gt;tilt&lt;/code&gt; 后，就可以玩游戏了。&lt;/p&gt;
&lt;h3&gt;public boolean tilt(Side side)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tilt&lt;/code&gt; 方法负责真正移动棋盘上的所有方块。例如，如果棋盘是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|    |   2|    |
|   4|   4|   2|   2|
|    |   4|    |    |
|   2|   4|   4|   8|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并按下向上键，&lt;code&gt;tilt&lt;/code&gt; 会修改 &lt;code&gt;board&lt;/code&gt; 实例变量，使游戏状态变成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|   8|   4|   2|
|   4|   4|   4|   8|
|   2|    |    |    |
|    |    |    |    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了修改棋盘，还有两件事必须发生：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;必须更新 &lt;code&gt;score&lt;/code&gt; 实例变量，使其反映所有方块合并的总价值（如果有）。在上面的例子中，两块 4 合并为 8，两块 2 合并为 4，因此分数应当增加 8 + 4 = 12。&lt;/li&gt;
&lt;li&gt;如果棋盘有任何变化，必须把局部变量 &lt;code&gt;changed&lt;/code&gt; 设为 &lt;code&gt;true&lt;/code&gt;。这是因为在 &lt;code&gt;tilt&lt;/code&gt; 的骨架代码末尾可以看到，我们调用了 &lt;code&gt;setChanged()&lt;/code&gt; 方法；它会通知 GUI 有内容需要绘制。你自己不会调用 &lt;code&gt;setChanged&lt;/code&gt;，只需要修改局部变量 &lt;code&gt;changed&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;棋盘上的所有方块移动都必须使用 &lt;code&gt;Board&lt;/code&gt; 类提供的 &lt;code&gt;move&lt;/code&gt; 方法完成。访问棋盘上的所有方块都必须使用 &lt;code&gt;Board&lt;/code&gt; 类提供的 &lt;code&gt;tile&lt;/code&gt; 方法。由于 GUI 实现中的一些细节，在一次 &lt;code&gt;tilt&lt;/code&gt; 调用中，对给定方块应当只调用一次 &lt;code&gt;move&lt;/code&gt;。本文的 Tips 部分会进一步讨论这个限制。&lt;/p&gt;
&lt;p&gt;课程视频中提供了怎样开始编写这个方法的快速概览。&lt;/p&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;p&gt;我们强烈建议一开始只考虑向上方向，也就是参数 &lt;code&gt;side&lt;/code&gt; 等于 &lt;code&gt;Side.NORTH&lt;/code&gt; 的情况。为了支持这一过程，我们提供了 &lt;code&gt;TestUpOnly&lt;/code&gt; 类，其中包含四个测试：&lt;code&gt;testUpNoMerge&lt;/code&gt;、&lt;code&gt;testUpBasicMerge&lt;/code&gt;、&lt;code&gt;testUpTripleMerge&lt;/code&gt; 和 &lt;code&gt;testUpTrickyMerge&lt;/code&gt;。你会注意到，这些测试都只涉及一次向上移动。&lt;/p&gt;
&lt;p&gt;考虑怎样实现向上移动时，请注意以下内容。&lt;/p&gt;
&lt;p&gt;对于给定的一列，顶行（第 3 行）的方块保持原位。如果第 2 行上方的位置为空，它可以向上移动；如果上方方块与它数值相同，它也可以向上移动一格。换句话说，遍历各行时，从第 3 行开始向下遍历是安全的，因为一个方块移动一次之后，不可能还需要再次移动。&lt;/p&gt;
&lt;p&gt;这听起来可能不会太难，但实际上真的很难。准备好拿出记事本，推导许多例子。努力编写优雅的代码，尽管在这个问题中很难做到优雅。我们强烈建议创建一个或多个辅助方法来保持代码整洁。例如，可以编写一个辅助函数处理棋盘中的一列，因为每一列彼此独立。也可以编写一个能够返回目标行数值的辅助函数。&lt;/p&gt;
&lt;p&gt;提醒：对给定方块只能调用一次 &lt;code&gt;move&lt;/code&gt;。换句话说，假设棋盘如下，并按下向上键：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
|    |    |    |   2|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一种实现方式可能如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Tile t = board.tile(3, 0)
board.move(3, 1, t);
board.move(3, 2, t);
board.move(3, 3, t);
setChanged();
return true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，GUI 会被弄糊涂，因为同一块方块不应当在只调用一次 &lt;code&gt;setChanged&lt;/code&gt; 的情况下移动多次。相反，需要通过一次 &lt;code&gt;move&lt;/code&gt; 调用完成整个移动，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Tile t = board.tile(3, 0)
board.move(3, 3, t);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从某种意义上说，困难的部分在于判断每块方块最终应当位于哪一行。&lt;/p&gt;
&lt;p&gt;为了测试理解，你应当完成课程提供的 Google Form 小测。这个小测（以及后面的其他小测）完全可选，不计分，但我们强烈建议完成，因为它可以发现你对游戏机制的概念误解。可以任意多次尝试。&lt;/p&gt;
&lt;p&gt;要判断何时更新分数，请注意：如果把方块 &lt;code&gt;t&lt;/code&gt; 移动到列 &lt;code&gt;c&lt;/code&gt;、行 &lt;code&gt;r&lt;/code&gt; 会替换一块已存在的方块（也就是发生合并），&lt;code&gt;board.move(c, r, t)&lt;/code&gt; 方法会返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;看起来更糟糕的是，即使让向上方向的 &lt;code&gt;tilt&lt;/code&gt; 工作，仍然需要为另外三个方向完成相同的事情。如果采用朴素方法，会得到大量重复、只有少量修改的代码，并产生许多引入隐蔽错误的机会。&lt;/p&gt;
&lt;p&gt;对于这个问题，我们直接提供了一个干净解法的关键思想。它会让你只增加两行代码，就能处理另外三个方向！具体来说，&lt;code&gt;Board&lt;/code&gt; 类拥有 &lt;code&gt;setViewingPerspective(Side s)&lt;/code&gt; 函数，它会改变 &lt;code&gt;tile&lt;/code&gt; 和 &lt;code&gt;move&lt;/code&gt; 方法的行为，让它们表现得就像给定方向是 NORTH。&lt;/p&gt;
&lt;p&gt;例如，考虑下面的棋盘：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|    |    |    |    |
|  16|    |  16|    |
|    |    |    |    |
|    |    |    |   2|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果调用 &lt;code&gt;board.tile(0, 2)&lt;/code&gt;，会得到 &lt;code&gt;16&lt;/code&gt;，因为 16 位于第 0 列、第 2 行。如果调用 &lt;code&gt;board.setViewingPerspective(s)&lt;/code&gt;，其中 &lt;code&gt;s&lt;/code&gt; 是 &lt;code&gt;WEST&lt;/code&gt;，棋盘会表现得就像 WEST 是 NORTH；也就是说，就像你把头向左转了 90 度，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|    |    |  16|    |
|    |    |    |    |
|    |    |  16|    |
|   2|    |    |    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换句话说，之前的 &lt;code&gt;16&lt;/code&gt; 现在会位于 &lt;code&gt;board.tile(2, 3)&lt;/code&gt;。如果拥有正确实现的 &lt;code&gt;tilt&lt;/code&gt;，并调用 &lt;code&gt;board.tilt(Side.NORTH)&lt;/code&gt;，棋盘会变成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|   2|    |  32|    |
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要让棋盘恢复原始观察视角，只需调用 &lt;code&gt;board.setViewingPerspective(Side.NORTH)&lt;/code&gt;，这会让棋盘重新表现为 &lt;code&gt;NORTH&lt;/code&gt; 就是 &lt;code&gt;NORTH&lt;/code&gt;。执行后，棋盘的行为就像它是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|    |    |    |    |
|  32|    |    |    |
|    |    |    |    |
|   2|    |    |    |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，这与把原始棋盘中的方块滑向 &lt;code&gt;WEST&lt;/code&gt; 完全相同。&lt;/p&gt;
&lt;p&gt;**重要：**结束 &lt;code&gt;tilt&lt;/code&gt; 调用之前，务必使用 &lt;code&gt;board.setViewingPerspective&lt;/code&gt; 把观察视角设回 &lt;code&gt;Side.NORTH&lt;/code&gt;，否则会发生奇怪的事情。&lt;/p&gt;
&lt;p&gt;为了测试理解，请尝试第三个也是最后一个 Google Form 小测。可以任意多次尝试。&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;虽然未来我们会要求你能够测试自己的程序，但本项目提供了完整测试套件。&lt;/p&gt;
&lt;p&gt;测试分布在 5 个文件中：&lt;code&gt;TestEmptySpace&lt;/code&gt;、&lt;code&gt;TestMaxTileExists&lt;/code&gt;、&lt;code&gt;TestAtLeastOneMoveExists&lt;/code&gt;、&lt;code&gt;TestUpOnly&lt;/code&gt; 和 &lt;code&gt;TestModel&lt;/code&gt;。每个文件测试代码中的某个特定部分，只有 &lt;code&gt;TestModel&lt;/code&gt; 例外，它会协调测试你编写的全部内容。这种测试称为集成测试，在测试中非常重要。&lt;/p&gt;
&lt;p&gt;单元测试会隔离运行各部分，而集成测试会把所有部分一起运行，用于捕获由不同函数相互作用导致的隐蔽 bug。&lt;/p&gt;
&lt;p&gt;因此，在通过其余测试之前，不要尝试调试 &lt;code&gt;TestModel&lt;/code&gt;！事实上，下面讨论测试的顺序就是你应当尝试它们的顺序。&lt;/p&gt;
&lt;p&gt;接下来逐个查看这些测试，并说明怎样阅读错误消息。&lt;/p&gt;
&lt;h3&gt;TestEmptySpace&lt;/h3&gt;
&lt;p&gt;这些测试检查 &lt;code&gt;emptySpaceExists&lt;/code&gt; 方法的正确性。如果某个测试失败，错误消息大致如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/14d71c23ddba-test-empty-space-all-fail.png&quot; alt=&quot;TestEmptySpace 全部失败&quot;&gt;&lt;/p&gt;
&lt;p&gt;左侧会看到所有运行过的测试列表。黄色 X 表示测试失败，绿色对勾表示测试通过。右侧会看到一些有用的错误消息。要单独查看某个测试及其错误消息，请单击左侧的测试。例如，假设我们想查看 &lt;code&gt;testCompletelyEmpty&lt;/code&gt; 测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/d8b514a88d1a-test-completely-empty.png&quot; alt=&quot;testCompletelyEmpty&quot;&gt;&lt;/p&gt;
&lt;p&gt;右侧现在只显示这个测试的错误消息。第一行包含一条有用信息：&lt;code&gt;&quot;Board is full of empty space&quot;&lt;/code&gt;，后面跟着棋盘的 String 表示。可以清楚看到棋盘为空，但 &lt;code&gt;emptySpaceExists&lt;/code&gt; 方法返回了 &lt;code&gt;false&lt;/code&gt;，导致测试失败。如果某个测试失败，测试代码顶部的 Javadoc 注释也包含一些有用信息。&lt;/p&gt;
&lt;h3&gt;TestMaxTileExists&lt;/h3&gt;
&lt;p&gt;这些测试检查 &lt;code&gt;maxTileExists&lt;/code&gt; 方法的正确性。错误消息与 &lt;code&gt;TestEmptySpace&lt;/code&gt; 类似，仍然可以单击每个测试来单独查看。请记住，&lt;code&gt;maxTileExists&lt;/code&gt; 方法应当只寻找最大方块，不应当检查任何其他内容（例如不应当寻找空位）。如果你的方法这样做，就无法通过所有测试。&lt;/p&gt;
&lt;h3&gt;TestAtLeastOneMoveExists&lt;/h3&gt;
&lt;p&gt;这些测试检查 &lt;code&gt;atLeastOneMoveExists&lt;/code&gt; 方法的正确性。错误消息与上面两个测试类似。由于 &lt;code&gt;atLeastOneMoveExists&lt;/code&gt; 依赖 &lt;code&gt;emptySpaceExists&lt;/code&gt;，在 &lt;code&gt;TestEmptySpace&lt;/code&gt; 的全部测试通过之前，不应当期待这些测试能够通过。&lt;/p&gt;
&lt;h3&gt;TestUpOnly&lt;/h3&gt;
&lt;p&gt;这些测试检查 &lt;code&gt;tilt&lt;/code&gt; 方法的正确性，但只检查向上（&lt;code&gt;Side.NORTH&lt;/code&gt;）方向。这些测试的错误消息不同，来看一个例子。假设运行全部测试后发现 &lt;code&gt;testUpTrickyMerge&lt;/code&gt; 失败。单击这个测试后，会看到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/fcab3d7aee38-test-up-error-msg.png&quot; alt=&quot;testUpTrickyMerge 错误消息&quot;&gt;&lt;/p&gt;
&lt;p&gt;第一行会说明倾斜方向（在这些测试中始终是 North），之后显示倾斜前你的棋盘是什么样子、预期棋盘是什么样子，最后显示你的棋盘实际是什么样子。&lt;/p&gt;
&lt;p&gt;你会看到，在一次 &lt;code&gt;tilt&lt;/code&gt; 调用中，我们让同一块方块合并了两次，结果得到一块数值为 8 的方块，而不是两块数值均为 4 的方块。因此，棋盘表示底部所显示的 &lt;code&gt;score&lt;/code&gt; 也不正确。&lt;/p&gt;
&lt;p&gt;对于其他测试，可能很难立刻看出预期棋盘与实际棋盘之间的区别。对于这些测试，可以单击错误消息最底部蓝色的 “Click to see difference” 文本，在一个单独窗口中并排比较预期棋盘（左侧）与实际棋盘（右侧）。对于这个测试，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/f104bd6bdb2c-comparison.png&quot; alt=&quot;并排比较&quot;&gt;&lt;/p&gt;
&lt;p&gt;调试这些问题可能有些棘手，因为很难判断自己做错了什么。首先，应当判断违反了上面列出的三条规则中的哪一条。在这个例子中，可以看到违反的是规则 2，因为一块方块合并了多次。这些测试方法的 Javadoc 注释是很好的资源，因为它们会明确说明正在测试哪条规则或哪种配置。也可以通过查看移动前后的棋盘来判断违反了哪条规则。&lt;/p&gt;
&lt;p&gt;之后才是棘手的部分：重构现有代码，让它正确处理这条规则。我们建议用纸笔写下代码执行的步骤，先理解棋盘为什么会变成当前样子，再设计修复方法。这些测试只调用一次 &lt;code&gt;tilt&lt;/code&gt;，因此不必担心调试多次 &lt;code&gt;tilt&lt;/code&gt; 调用。&lt;/p&gt;
&lt;h3&gt;TestModel&lt;/h3&gt;
&lt;p&gt;这些测试会一起检查所有内容的正确性。大多数测试与 &lt;code&gt;TestUpOnly&lt;/code&gt; 中的测试类似，只调用一次 &lt;code&gt;tilt&lt;/code&gt;；但它们还包含 &lt;code&gt;gameOver&lt;/code&gt; 测试（会一起测试 &lt;code&gt;emptySpaceExists&lt;/code&gt;、&lt;code&gt;maxTileExists&lt;/code&gt; 和 &lt;code&gt;atLeastOneMoveExists&lt;/code&gt;），以及连续多次调用 &lt;code&gt;tilt&lt;/code&gt; 的测试。&lt;/p&gt;
&lt;p&gt;这些测试的错误消息与 &lt;code&gt;TestUpOnly&lt;/code&gt; 完全相同，Javadoc 注释同样有助于判断测试正在检查什么。&lt;/p&gt;
&lt;p&gt;不需要担心测试的实际代码；你不必理解或修改任何测试，不过欢迎阅读它们，了解测试是怎样编写的。如果非常有雄心，也可以添加自己的测试。&lt;/p&gt;
&lt;h2&gt;评分&lt;/h2&gt;
&lt;p&gt;满分项目会通过我们提供的全部单元测试。请记住，本项目没有隐藏测试，因此，如果通过了所有这些测试，你就拥有一个满分项目！&lt;/p&gt;
&lt;p&gt;Gradescope 上某些测试的权重略有不同，因此，如果通过了某个比例的测试，你的百分制分数可能会是另一个比例（很可能更高）。&lt;/p&gt;
&lt;p&gt;这是因为项目中的某些部分比其他部分更困难，而且我们知道这是许多学生第一次使用 Java，因此据此设置了各部分权重。&lt;/p&gt;
&lt;p&gt;下面列出不同完成程度大约能够获得的百分比：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只实现 &lt;code&gt;emptySpaceExists&lt;/code&gt; 或 &lt;code&gt;maxTileExists&lt;/code&gt;：约 27%。&lt;/li&gt;
&lt;li&gt;实现除 &lt;code&gt;tilt&lt;/code&gt; 之外的全部内容：约 47%。&lt;/li&gt;
&lt;li&gt;实现全部内容，但 &lt;code&gt;tilt&lt;/code&gt; 只支持 Up 方向：约 68%。&lt;/li&gt;
&lt;li&gt;实现全部内容，但不支持合并：约 64%。&lt;/li&gt;
&lt;li&gt;实现全部内容，但没有实现合并规则 2：约 93%。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到，实现合并规则 2 只占项目的约 7%。这是因为它是一条很难处理的规则，却只涉及游戏中很小一部分情况，所以我们相应地设置了权重。&lt;/p&gt;
&lt;h2&gt;额外加分&lt;/h2&gt;
&lt;p&gt;在周四晚上 11:59 之前（即项目截止前一天）把最终提交交到 Gradescope 的学生会获得 2 分额外加分。“最终提交”是 Gradescope 上被激活的提交，因此截止时间后仍然可以继续提交，但只有在周四 11:59 之前的提交被设为 active 时，才会获得这 2 分。额外分会在截止时间后应用，因此项目截止之前不会显示。&lt;/p&gt;
&lt;h2&gt;提交与版本控制&lt;/h2&gt;
&lt;p&gt;以较高频率把工作提交到仓库非常重要。当你把某些内容弄坏，或者你的狗吃掉项目时，版本控制是拯救自己的强大工具；但只有经常使用它，它才有用。可以每 15 分钟提交一次。尽管 Git 的行为像是为整个项目创建快照，它实际上只保存发生变化的内容。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git status&lt;/code&gt; 命令会告诉你从上次提交以来修改、删除或可能添加了哪些文件，还会告诉你有多少内容尚未发送到 GitHub 仓库。&lt;/p&gt;
&lt;p&gt;典型命令大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status                          # To see what needs to be added or committed.
git add &amp;#x3C;filepath&gt;                  # To add, or stage, any modified files.
git commit -m &quot;Commit message&quot;      # To commit changes.
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后可以继续项目，直到再次准备提交和推送，再重复上面的过程。养成频繁提交并编写有信息量的提交消息的习惯，符合你的最佳利益。这样，如果需要恢复到旧代码版本，不但可以做到，而且很容易。我们建议每当添加了一段重要代码或达到某个里程碑（例如通过一个新测试）时都创建提交。&lt;/p&gt;
&lt;p&gt;把代码推送到 GitHub（也就是运行 &lt;code&gt;git push&lt;/code&gt;）后，可以进入 Gradescope，找到 &lt;code&gt;proj0&lt;/code&gt; 作业并提交代码。请注意，Gradescope 使用的是最近一次推送的提交；如果在 Gradescope 提交前没有运行 &lt;code&gt;git push&lt;/code&gt;，测试的会是旧代码，而不是计算机上的最新代码。&lt;/p&gt;
&lt;h2&gt;获取帮助&lt;/h2&gt;
&lt;p&gt;少量挣扎和调试是正常的，甚至是健康的；但如果已经按照上面的所有建议尝试，仍然卡住数小时毫无进展，请向工作人员求助！&lt;/p&gt;
&lt;p&gt;在 CS 61B 中，获得帮助的两种方式是参加 Office Hours，或者在 Ed 上发帖，让 TA 或其他学生帮助你摆脱困境。&lt;/p&gt;
&lt;p&gt;请记住，在 Office Hours 中，TA 对每位学生最多花 10 分钟。为了加快进度，我们要求你带着一个能够清楚向 TA 表达的问题，而不是只说某个测试没有通过。例如，如果某个测试没有通过，先判断具体是哪一部分没有通过，以及为什么存在差异。也许 &lt;code&gt;score&lt;/code&gt; 没有正确更新，或者合并没有按应有方式发生。这样会加快帮助过程，甚至可能让你自己找到 bug。&lt;/p&gt;
&lt;p&gt;如果在 Ed 上发帖，请阅读课程的 Ed 政策，确保没有意外发布部分解法并违反学术诚信政策。除此之外，欢迎在 megathread 中进行有建设性的讨论。发帖前请先搜索问题，因为许多学生会遇到非常相似的 bug！&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj0/proj0&quot;&gt;https://sp21.datastructur.es/materials/proj/proj0/proj0&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 1：数据结构</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-1-data-structures</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-1-data-structures</guid><description>CS61B Spring 2021 Project 1：数据结构中文学习资料。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;截止日期：2021 年 2 月 16 日&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本项目的截止日期是 02/16，不过为了帮助你按计划推进，我们在 02/05 设置了一个额外加分检查点。该检查点是项目自动评分器的一个较小版本。更详细的信息会在本规格后面给出。&lt;/p&gt;
&lt;p&gt;你还需要在作业结束时推送你的 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库，以便把 Gradescope 分数传入 Beacon。后文会进一步说明。&lt;/p&gt;
&lt;p&gt;请记住，由于 Project 1 与 Lab 4 之间存在依赖关系，本项目最多只能使用 2 个 slip days。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在 Project 1 中，我们将分别使用链表和数组构建“双端队列”（Double Ended Queue）的实现，并把它们放进一个可供其他类使用的包中。这个项目大致分成两半：数据结构部分和应用部分。&lt;/p&gt;
&lt;p&gt;在项目的数据结构部分，你将创建两个 Java 文件：&lt;code&gt;LinkedListDeque.java&lt;/code&gt; 和 &lt;code&gt;ArrayDeque.java&lt;/code&gt;，并实现下文列出的 public 方法。你需要利用在 Lab 3 中掌握的随机测试和计时测试技能，自行验证这些数据结构是否正确。&lt;/p&gt;
&lt;p&gt;在项目的应用部分，你将创建 Java 文件 &lt;code&gt;MaxArrayDeque.java&lt;/code&gt;，还会使用自己的包，最终实现一个能够播放 Guitar Hero 音乐的声音合成器。你必须自行测试 &lt;code&gt;MaxArrayDeque&lt;/code&gt;；声音合成器部分的测试由课程提供。&lt;/p&gt;
&lt;p&gt;我们提供的脚手架会比较少。换句话说，我们会告诉你应该做什么，但不会告诉你具体应该怎样实现。&lt;/p&gt;
&lt;p&gt;本项目必须独立完成。请仔细阅读课程的协作与作弊政策，了解这项要求的确切含义。&lt;/p&gt;
&lt;p&gt;此外，本项目会强制检查代码风格。你必须遵守课程风格指南，否则自动评分器会扣分。&lt;/p&gt;
&lt;h2&gt;获取骨架文件&lt;/h2&gt;
&lt;p&gt;与 Project 0 一样，你应该先下载骨架文件。&lt;/p&gt;
&lt;p&gt;进入包含你个人仓库副本的文件夹。例如，如果你的登录名是 &lt;code&gt;s101&lt;/code&gt;，就进入 &lt;code&gt;sp21-s101&lt;/code&gt; 文件夹，或其任意子目录。&lt;/p&gt;
&lt;p&gt;为了确保你拥有最新的骨架文件，请使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull skeleton master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你使用的是较新的 Git 版本，可能需要运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git pull skeleton master --allow-unrelated-histories
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时应该出现一个 &lt;code&gt;proj1&lt;/code&gt; 目录，其中包含两个文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;proj1
├── deque
│   └── LinkedListDequeTest.java
└── gh2
    ├── GuitarHeroLite.java
    ├── GuitarPlayer.java
    ├── GuitarString.java
    ├── TTFAF.java
    └── TestGuitarString.java
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果遇到任何形式的错误，请停下来，仔细阅读课程 Git 指南并解决问题，或前往 Office Hours / Ed 求助。与其猜测并反复尝试 Git 命令，这样做很可能会为你省下许多麻烦。如果你发现自己正准备使用 Google 推荐的 &lt;code&gt;force push&lt;/code&gt; 一类命令，&lt;strong&gt;不要这样做&lt;/strong&gt;。即使 Stack Overflow 上的帖子说可以，也不要使用 force push。&lt;/p&gt;
&lt;p&gt;骨架中只提供了 &lt;code&gt;deque/LinkedListDequeTest.java&lt;/code&gt;，以及位于 &lt;code&gt;gh2&lt;/code&gt; 文件夹中的本项目第二部分（Guitar Hero 2）的一些骨架。&lt;code&gt;deque/LinkedListDequeTest.java&lt;/code&gt; 给出了如何编写测试来验证代码正确性的例子。我们强烈建议你运行这些测试，也编写自己的测试，因为已给出的测试并不全面。&lt;/p&gt;
&lt;p&gt;这个文件中的测试也正是检查点中用于评估 &lt;code&gt;LinkedListDeque&lt;/code&gt; 实现的测试。对于 &lt;code&gt;ArrayDeque&lt;/code&gt;，我们还会使用一些不提供给你的额外测试。关于检查点的更多信息会在后面给出。&lt;/p&gt;
&lt;p&gt;在进入 Deque API 和具体实现要求之前，我们先简要讨论包，以及本项目为什么要使用包。&lt;/p&gt;
&lt;h3&gt;包&lt;/h3&gt;
&lt;p&gt;本项目的一部分目标是使用包来分离不同的逻辑和功能。项目结束时，你将拥有两个包：提供 &lt;code&gt;Deque&lt;/code&gt; 数据结构实现的 &lt;code&gt;deque&lt;/code&gt; 包，以及实现 Guitar Hero 合成器的 &lt;code&gt;gh2&lt;/code&gt; 包。起始代码中应该已经存在这两个同名文件夹，而你的任务就是实现其中的内容。下面具体看看“包”究竟是什么。&lt;/p&gt;
&lt;p&gt;包是一组为了共同目标协同工作的 Java 类。其实我们已经在 CS 61B 中使用过包，只是此前没有明确这样称呼。例如，&lt;code&gt;org.junit&lt;/code&gt; 是一个包，其中包含许多对测试有用的类，包括我们熟悉的 &lt;code&gt;Assert&lt;/code&gt; 类；这个类中包含 &lt;code&gt;assertEquals&lt;/code&gt; 等有用的 static 方法。也就是说，在 &lt;code&gt;org.junit.Assert.assertEquals&lt;/code&gt; 中，&lt;code&gt;org.junit&lt;/code&gt; 是包名，&lt;code&gt;Assert&lt;/code&gt; 是类名，&lt;code&gt;assertEquals&lt;/code&gt; 是方法名。&lt;/p&gt;
&lt;p&gt;我们把 &lt;code&gt;org.junit.Assert.assertEquals&lt;/code&gt; 称为这个方法的“规范名称”（canonical name），把 &lt;code&gt;assertEquals&lt;/code&gt; 称为这个方法的“简单名称”（simple name）。&lt;/p&gt;
&lt;p&gt;创建包时，我们在文件顶部使用 &lt;code&gt;package&lt;/code&gt; 关键字指定代码所属的包。例如，如果想声明某个文件属于 &lt;code&gt;deque&lt;/code&gt; 包，就在文件顶部添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package deque;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果程序员想使用 &lt;code&gt;deque&lt;/code&gt; 包中的类或方法，就必须使用完整的规范名称，例如 &lt;code&gt;deque.ArrayDeque&lt;/code&gt;；或者先写 &lt;code&gt;import deque.ArrayDeque&lt;/code&gt;，之后便可以只使用简单名称 &lt;code&gt;ArrayDeque&lt;/code&gt;。因此，&lt;code&gt;import&lt;/code&gt; 语句只是让你可以使用类或方法的简单名称。&lt;/p&gt;
&lt;p&gt;通常，包名是编写代码的实体所拥有的网址倒序。例如，JUnit 库托管在 &lt;code&gt;junit.org&lt;/code&gt;，因此它的包名是 &lt;code&gt;org.junit&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;包为什么有用？关键就在“规范”这个词。只要不同程序员没有为自己的包使用同一个包名，我们就可以在不同上下文中自由使用相同的类名。例如，可能存在一个 &lt;code&gt;com.hrblock.TaxCalculator&lt;/code&gt; 类，也可能存在另一个不同的 &lt;code&gt;com.turbotax.TaxCalculator&lt;/code&gt; 类。由于我们必须使用完整规范名称，或先执行 import，因此不会在想使用一个类时意外用到另一个类。&lt;/p&gt;
&lt;p&gt;从概念上说，你可以把包理解为电脑中的不同文件夹。构建大型系统时，把它组织成不同的包是一种好做法。&lt;/p&gt;
&lt;p&gt;从现在开始，CS 61B 中的大多数代码都会属于某个包。&lt;/p&gt;
&lt;p&gt;介绍完这些内容后，下面讨论 Deque 应该具有的方法。&lt;/p&gt;
&lt;h2&gt;Deque API&lt;/h2&gt;
&lt;p&gt;双端队列与课堂上讨论过的 &lt;code&gt;SLList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt; 类非常相似。下面是 cplusplus.com 给出的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Deque（通常读作 “deck”）是 double-ended queue 的一种不规则缩写。双端队列是一种大小可动态变化的序列容器，可以在两端（前端或后端）扩张或收缩。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体而言，任何 Deque 实现都必须恰好具有以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public void addFirst(T item)&lt;/code&gt;：把类型为 &lt;code&gt;T&lt;/code&gt; 的元素添加到 Deque 前端。可以假设 &lt;code&gt;item&lt;/code&gt; 永远不会是 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void addLast(T item)&lt;/code&gt;：把类型为 &lt;code&gt;T&lt;/code&gt; 的元素添加到 Deque 后端。可以假设 &lt;code&gt;item&lt;/code&gt; 永远不会是 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean isEmpty()&lt;/code&gt;：如果 Deque 为空则返回 &lt;code&gt;true&lt;/code&gt;，否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public int size()&lt;/code&gt;：返回 Deque 中的元素数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public void printDeque()&lt;/code&gt;：从第一个元素到最后一个元素打印 Deque 中的元素，元素之间以空格分隔。打印完所有元素后，再输出一个换行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T removeFirst()&lt;/code&gt;：删除并返回 Deque 前端的元素。如果不存在这样的元素，则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T removeLast()&lt;/code&gt;：删除并返回 Deque 后端的元素。如果不存在这样的元素，则返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T get(int index)&lt;/code&gt;：获取指定索引处的元素，其中 0 表示前端，1 表示下一个元素，依此类推。如果不存在这样的元素，则返回 &lt;code&gt;null&lt;/code&gt;。不得改变 Deque！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，我们还希望两个 Deque 实现具有以下两个特殊方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public Iterator&amp;#x3C;T&gt; iterator()&lt;/code&gt;：我们要创建的 Deque 对象是可迭代的（也就是 &lt;code&gt;Iterable&amp;#x3C;T&gt;&lt;/code&gt;），因此必须提供此方法来返回一个迭代器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public boolean equals(Object o)&lt;/code&gt;：返回参数 &lt;code&gt;o&lt;/code&gt; 是否与该 Deque 相等。如果 &lt;code&gt;o&lt;/code&gt; 是一个 Deque，并且按照泛型 &lt;code&gt;T&lt;/code&gt; 的 &lt;code&gt;equals&lt;/code&gt; 方法判断，它以相同顺序包含完全相同的内容，那么 &lt;code&gt;o&lt;/code&gt; 就被视为相等。（2/12 新增：你需要使用 &lt;code&gt;instanceof&lt;/code&gt; 关键字。）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**2/12 新增：**不要让 &lt;code&gt;Deque&lt;/code&gt; 接口实现 &lt;code&gt;Iterable&lt;/code&gt;；应当只让两个实现类 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现它。若采用前一种做法，自动评分器会报告 API 错误。&lt;/p&gt;
&lt;p&gt;你会在 Lecture 11（2/12）中学习 &lt;code&gt;Iterator&lt;/code&gt;，所以现在无需担心。这一项目本来就应随着课堂和讨论课讲授更多内容而一点一点完成，也是练习本课程所学知识的绝佳机会。&lt;/p&gt;
&lt;p&gt;你的类必须能够接受任何泛型类型，而不只是整数。关于创建和使用泛型数据结构的信息，请参阅 Lecture 5，尤其注意最后一张幻灯片中关于泛型的经验规则。&lt;/p&gt;
&lt;p&gt;在本项目中，你将为 Deque 接口提供两种实现：一种由链表支持，另一种由可调整大小的数组支持。&lt;/p&gt;
&lt;h2&gt;项目任务&lt;/h2&gt;
&lt;h3&gt;1. 链表 Deque&lt;/h3&gt;
&lt;p&gt;注意：除了迭代器之外，完成这一部分所需的一切内容都已经在 Lecture 4 和 Lecture 5（1/27 和 1/25）中讲过；迭代器会在 Lecture 11（2/12）中介绍。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;proj1/deque&lt;/code&gt; 目录中创建名为 &lt;code&gt;LinkedListDeque.java&lt;/code&gt; 的文件。确保使用特殊的 &lt;code&gt;package&lt;/code&gt; 关键字声明它属于 &lt;code&gt;deque&lt;/code&gt; 包。&lt;/p&gt;
&lt;p&gt;作为第一种 Deque 实现，你将构建基于链表的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 类。&lt;/p&gt;
&lt;p&gt;你的操作必须遵守以下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt; 和 &lt;code&gt;remove&lt;/code&gt; 操作不得涉及任何循环或递归。单次这样的操作必须花费“常数时间”，也就是执行时间不能依赖 Deque 的大小。这意味着不能使用遍历全部或大多数元素的循环。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt; 必须使用迭代，而不是递归。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt; 必须花费常数时间。&lt;/li&gt;
&lt;li&gt;使用 for-each 循环遍历 &lt;code&gt;LinkedListDeque&lt;/code&gt; 所需的时间，应与元素数量成正比。&lt;/li&gt;
&lt;li&gt;不要保留对已不在 Deque 中的元素的引用。程序在任意时刻使用的内存量必须与元素数量成正比。例如，如果先添加 10,000 个元素，再删除其中 9,999 个，最终内存使用量应与只有 1 个元素的 Deque 相当，而不是仍与 10,000 个元素相当。请记住，只有当不再存在指向某个对象的指针时，Java 垃圾回收器才会为我们“删除”它。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实现前面“Deque API”一节列出的所有方法。&lt;/p&gt;
&lt;p&gt;此外，还需要实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public LinkedListDeque()&lt;/code&gt;：创建一个空的链表 Deque。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T getRecursive(int index)&lt;/code&gt;：与 &lt;code&gt;get&lt;/code&gt; 相同，但使用递归。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如有需要，可以在 &lt;code&gt;LinkedListDeque.java&lt;/code&gt; 中添加任何 private 辅助类或辅助方法。如果这样做，请为了你自己和助教的方便，添加有帮助的 Javadoc 注释。&lt;/p&gt;
&lt;p&gt;虽然听起来很简单，但其中有许多设计问题需要考虑，你可能会发现实现比预想中更困难。请务必复习双向链表讲义，尤其是关于哨兵节点的幻灯片：双哨兵拓扑和循环哨兵拓扑。我更喜欢循环方法。&lt;/p&gt;
&lt;p&gt;实现中不得使用 Java 内置的 &lt;code&gt;LinkedList&lt;/code&gt; 数据结构，也不得使用任何 &lt;code&gt;java.util.*&lt;/code&gt; 中的数据结构。如果自动评分器检测到导入了这些数据结构，会立即给你 0 分。&lt;/p&gt;
&lt;h3&gt;2. 数组 Deque&lt;/h3&gt;
&lt;p&gt;注意：除了迭代器之外，完成这一部分所需的内容会在 Lecture 7（2/03）之前全部讲完；迭代器会在 Lecture 11（2/12）中介绍。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;proj1/deque&lt;/code&gt; 目录中创建名为 &lt;code&gt;ArrayDeque.java&lt;/code&gt; 的文件。同样，使用 &lt;code&gt;package&lt;/code&gt; 关键字告诉该文件它属于 &lt;code&gt;deque&lt;/code&gt; 包。&lt;/p&gt;
&lt;p&gt;作为第二种 Deque 实现，你将构建 &lt;code&gt;ArrayDeque&lt;/code&gt; 类。这个 Deque 必须以数组作为核心数据结构。&lt;/p&gt;
&lt;p&gt;对于该实现，你的操作必须遵守以下规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;除了调整数组大小的操作外，&lt;code&gt;add&lt;/code&gt; 和 &lt;code&gt;remove&lt;/code&gt; 必须花费常数时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt; 必须花费常数时间。&lt;/li&gt;
&lt;li&gt;数组的初始大小应为 8。&lt;/li&gt;
&lt;li&gt;程序在任意时刻使用的内存量必须与元素数量成正比。例如，如果先添加 10,000 个元素，再删除其中 9,999 个，就不应该仍然使用长度约为 10,000 的数组。对于长度为 16 或更大的数组，使用率必须始终至少达到 25%。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着，在执行一次会使数组中元素数量低于数组长度 25% 的删除操作之前，应当缩小数组。对于较小的数组，使用率可以任意低。&lt;/p&gt;
&lt;p&gt;实现前面“Deque API”一节列出的所有方法。&lt;/p&gt;
&lt;p&gt;此外，还需要实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public ArrayDeque()&lt;/code&gt;：创建一个空的数组 Deque。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如有需要，可以在 &lt;code&gt;ArrayDeque.java&lt;/code&gt; 中添加任何 private 辅助类或辅助方法。&lt;/p&gt;
&lt;p&gt;你需要以某种方式记录哪些数组索引保存着 Deque 的前端元素和后端元素。我们强烈建议在本练习中把数组视为循环数组。换句话说，如果前端元素位于位置 0，而你执行 &lt;code&gt;addFirst&lt;/code&gt;，新的前端应绕回数组末尾，因此新的 Deque 前端元素位于底层数组的最后一个位置。与非循环方法相比，这会少带来许多麻烦。&lt;/p&gt;
&lt;p&gt;更多细节可参阅 Project 1 演示幻灯片。&lt;/p&gt;
&lt;p&gt;正确调整数组大小非常棘手，需要认真思考。请尝试在纸上画出不同方案。找到正确方法可能要花不少时间，我们鼓励你与同学或助教讨论核心思路，但实际实现必须完全由你独立完成。&lt;/p&gt;
&lt;h2&gt;额外加分：检查点&lt;/h2&gt;
&lt;p&gt;为了帮助你按计划推进，本项目设有一个 2/05 截止、价值 16 额外加分的检查点。检查点自动评分器会测试 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 的基础功能。具体来说，它会使用 &lt;code&gt;LinkedListDequeTest.java&lt;/code&gt; 中给出的测试检查 &lt;code&gt;LinkedListDeque&lt;/code&gt;，并使用课程自行编写的测试检查 &lt;code&gt;ArrayDeque&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这里的“基础功能”指所有 &lt;code&gt;add&lt;/code&gt; 方法、所有 &lt;code&gt;remove&lt;/code&gt; 方法、&lt;code&gt;size&lt;/code&gt; 方法、&lt;code&gt;isEmpty&lt;/code&gt; 方法和 &lt;code&gt;get&lt;/code&gt; 方法。这意味着检查点不会测试 &lt;code&gt;equals(Object o)&lt;/code&gt; 或 &lt;code&gt;iterator()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;code&gt;isEmpty&lt;/code&gt; 从一开始就在评分器中，但我们在今天（2/5）才把这一点补进规格。不过，如果已经实现了 &lt;code&gt;size&lt;/code&gt;，这个方法应该非常简单，可以只写一行。&lt;/p&gt;
&lt;p&gt;尤其重要的是，我们在 &lt;code&gt;ArrayDeque&lt;/code&gt; 中最多只会插入 8 个元素，因此你无需为了这个检查点处理扩容。&lt;/p&gt;
&lt;p&gt;你可以选择不完成检查点，不过我们强烈建议完成它，这既能帮助你保持进度，也能获得一些额外分。&lt;/p&gt;
&lt;p&gt;作业的其余部分不会在检查点中评分。&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;测试是工业界和学术界编写代码的重要组成部分。它是一项必不可少的技能，能够避免工业中的金钱损失和危险缺陷；对你而言，也能避免丢分。学习如何编写优秀、全面的单元测试，并养成在发布代码之前始终测试代码的习惯，是 CS 61B 的核心目标之一。&lt;/p&gt;
&lt;p&gt;起始代码中提供了一个非常简单的健全性检查文件 &lt;code&gt;LinkedListDequeTest.java&lt;/code&gt;。要使用这个测试样例，必须取消样例测试中相应代码行的注释。只有在实现了某个测试所用的全部方法之后，才取消该测试的注释，否则代码无法编译。执行 main 方法即可运行测试。测试项目时，请记得可以使用 IntelliJ 中的可视化工具。&lt;/p&gt;
&lt;p&gt;你不需要提交 &lt;code&gt;LinkedListDequeTest.java&lt;/code&gt;。为了自己的利益，提交之前应为 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 编写更全面的测试。请注意，通过 &lt;code&gt;LinkedListDequeTest.java&lt;/code&gt; 中提供的测试，并不一定意味着能够通过自动评分器的全部测试或在完整自动评分器中获得满分。&lt;/p&gt;
&lt;p&gt;本项目的目标之一，是让你构建某个东西并自行评估其正确性，因此我们不希望你过度依赖完整自动评分器来验证正确性。你会获得一个每 8 小时恢复一次的自动评分器 token。这些 token 不能“叠加”；即使连续 3 天没有向自动评分器提交内容，你仍然只有一个 token。&lt;/p&gt;
&lt;p&gt;从 2/13 星期六开始，恢复时间会永久缩短为每 20 分钟一次。&lt;/p&gt;
&lt;p&gt;那么应如何验证数据结构的正确性呢？使用你在 Lab 3 中获得的技能！我们鼓励你复制为 &lt;code&gt;SList&lt;/code&gt; 和 &lt;code&gt;AList&lt;/code&gt; 编写的测试，然后把它们改造成适用于这些数据结构的测试。它们会非常相似，只需要做一些基本修改。&lt;/p&gt;
&lt;p&gt;在只能极少使用自动评分器的情况下完成整个项目，可能显得非常吓人；但如果随机测试规模足够大，你应当对实现很有信心。只用几行代码，就可以在数十万规模的数据结构上，以随机顺序调用各种随机方法。&lt;/p&gt;
&lt;p&gt;换句话说，你是在数据结构上测试大量情形，很可能覆盖所有可能的边界情况。这正是随机测试的魅力：它把构想边界情况的创造性工作交给随机性。&lt;/p&gt;
&lt;p&gt;你创建的测试不会被评分，不过本项目另有一个额外加分部分，要求你编写自己的自动评分器。详细信息见本规格后面。&lt;/p&gt;
&lt;p&gt;在实现 &lt;code&gt;Deque&lt;/code&gt; 接口和所有必需方法之前，你的代码无法在完整自动评分器上编译。因此，如果只完成了规格到当前为止的内容，应使用检查点评分器，而不是完整评分器。&lt;/p&gt;
&lt;h2&gt;额外加分：自动评分器&lt;/h2&gt;
&lt;p&gt;你可以编写自己的 Deque 自动评分器，获得 32 分额外加分。完整细节位于单独的规格中，以免让本规格更加杂乱。其截止日期与 Project 1 相同，即 2/16。额外加分作业可以使用 slip days，但请理解，工作人员在 Ed 和 Office Hours 中会优先帮助必做部分。&lt;/p&gt;
&lt;p&gt;在完成整个项目之前，我们不建议开始这项额外加分作业。现在先记住它，然后继续完成主项目。&lt;/p&gt;
&lt;h2&gt;MaxArrayDeque&lt;/h2&gt;
&lt;p&gt;在完整实现并测试 &lt;code&gt;ArrayDeque&lt;/code&gt; 的正确性后，你将构建 &lt;code&gt;MaxArrayDeque&lt;/code&gt;。&lt;code&gt;MaxArrayDeque&lt;/code&gt; 拥有 &lt;code&gt;ArrayDeque&lt;/code&gt; 的全部方法，此外还具有 2 个额外方法和一个新构造器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;public MaxArrayDeque(Comparator&amp;#x3C;T&gt; c)&lt;/code&gt;：使用给定的 &lt;code&gt;Comparator&lt;/code&gt; 创建一个 &lt;code&gt;MaxArrayDeque&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T max()&lt;/code&gt;：按照先前给出的 &lt;code&gt;Comparator&lt;/code&gt; 返回 Deque 中的最大元素。如果 &lt;code&gt;MaxArrayDeque&lt;/code&gt; 为空，只需返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public T max(Comparator&amp;#x3C;T&gt; c)&lt;/code&gt;：按照参数 &lt;code&gt;Comparator c&lt;/code&gt; 返回 Deque 中的最大元素。如果 &lt;code&gt;MaxArrayDeque&lt;/code&gt; 为空，只需返回 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;MaxArrayDeque&lt;/code&gt; 可以使用构造器中提供的 &lt;code&gt;Comparator&amp;#x3C;T&gt;&lt;/code&gt; 判断自身的最大元素，也可以使用一个与构造器中给出的比较器不同的任意 &lt;code&gt;Comparator&amp;#x3C;T&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们不关心这个类的 &lt;code&gt;equals(Object o)&lt;/code&gt; 方法，所以你可以按照自己认为最恰当的方式定义它。我们不会测试此方法。&lt;/p&gt;
&lt;p&gt;如果你发现自己准备把整个 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现复制到一个 &lt;code&gt;MaxArrayDeque&lt;/code&gt; 文件中，那就做错了。这是一项关于整洁代码的练习，而在对抗复杂性时，重复是我们最大的敌人之一。作为提示，请重新阅读本节上面的第二句话。&lt;/p&gt;
&lt;p&gt;这些额外方法没有运行时间要求，我们只关心答案是否正确。有时 &lt;code&gt;MaxArrayDeque&lt;/code&gt; 中可能有多个相等、因而同为最大值的元素；在这种情况下，可以返回其中任意一个，都会被视为正确。&lt;/p&gt;
&lt;p&gt;你也应当为这一部分编写测试。因为新增功能比较简单，所以这些测试不必像为两个 Deque 实现编写的随机测试和计时测试那样健壮。为了测试代码，你很可能会创建多个 &lt;code&gt;Comparator&amp;#x3C;T&gt;&lt;/code&gt; 类——这正是本任务的目的：练习使用 &lt;code&gt;Comparator&lt;/code&gt; 对象完成有用的事情（找出最大元素），并练习编写自己的 &lt;code&gt;Comparator&lt;/code&gt; 类。&lt;/p&gt;
&lt;p&gt;你不需要提交这些测试，但为了自己，我们仍强烈建议编写它们。&lt;/p&gt;
&lt;p&gt;在下一部分中不会使用你创建的 &lt;code&gt;MaxArrayDeque&lt;/code&gt;。它是一个独立练习。&lt;/p&gt;
&lt;h2&gt;Deque 接口&lt;/h2&gt;
&lt;p&gt;在项目最后一部分，我们将真正使用刚刚创建的数据结构来解决一个现实问题。&lt;/p&gt;
&lt;p&gt;回忆一下，我们使用以下方法定义了 &lt;code&gt;Deque&lt;/code&gt; API，也就是它的行为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void addFirst(T item)
public void addLast(T item)
public boolean isEmpty()
public int size()
public void printDeque()
public T removeFirst()
public T removeLast()
public T get(int index)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于程序依赖的是这些行为，因此无论提供的是哪种 &lt;code&gt;Deque&lt;/code&gt; 实现——&lt;code&gt;ArrayDeque&lt;/code&gt; 还是 &lt;code&gt;LinkedListDeque&lt;/code&gt;——程序都不应在意，并且都应该正常工作。为了做到这一点，我们会使用接口的力量。&lt;/p&gt;
&lt;p&gt;第一个任务会有一点繁琐，但不会花太久。&lt;/p&gt;
&lt;p&gt;新建名为 &lt;code&gt;Deque.java&lt;/code&gt; 的文件，并在其中创建一个包含上述所有方法的接口。在 IntelliJ 中使用 “New → Java Class”。IntelliJ 会假设你想创建一个类，因此要确保把 &lt;code&gt;class&lt;/code&gt; 关键字替换为 &lt;code&gt;interface&lt;/code&gt;。不要忘记声明 &lt;code&gt;Deque&lt;/code&gt; 接口属于 &lt;code&gt;deque&lt;/code&gt; 包。&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和/或 &lt;code&gt;ArrayDeque&lt;/code&gt;，在类声明行中加入 &lt;code&gt;implements Deque&amp;#x3C;T&gt;&lt;/code&gt;，让它们实现 &lt;code&gt;Deque&lt;/code&gt; 接口。如果 IntelliJ 用类似下面的错误信息对你大喊：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;The method ... of type LinkedListDeque has the same erasure as ... of type Deque but does not override it.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着你忘记在 &lt;code&gt;implements&lt;/code&gt; 行中写泛型 &lt;code&gt;T&lt;/code&gt;，也就是写成了 &lt;code&gt;implements Deque&lt;/code&gt;，而不是 &lt;code&gt;implements Deque&amp;#x3C;T&gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果你为泛型类型参数使用了 &lt;code&gt;T&lt;/code&gt; 以外的名字，就使用你自己的名字。为每个重写 &lt;code&gt;Deque&lt;/code&gt; 方法的方法添加 &lt;code&gt;@Override&lt;/code&gt; 标记。&lt;/p&gt;
&lt;p&gt;现在，在 &lt;code&gt;Deque&lt;/code&gt; 接口中为 &lt;code&gt;isEmpty()&lt;/code&gt; 提供一个 &lt;code&gt;default&lt;/code&gt; 实现：当 &lt;code&gt;size()&lt;/code&gt; 为 &lt;code&gt;0&lt;/code&gt; 时返回 &lt;code&gt;true&lt;/code&gt;。由于 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现了 &lt;code&gt;Deque&lt;/code&gt; 接口，因此有了默认 &lt;code&gt;isEmpty()&lt;/code&gt; 实现后，可以从先前实现的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDeque&lt;/code&gt; 中删除该方法。&lt;/p&gt;
&lt;p&gt;实现 &lt;code&gt;Deque&lt;/code&gt; 接口，并从 &lt;code&gt;LinkedListDeque&lt;/code&gt; 与 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现中删除 &lt;code&gt;isEmpty()&lt;/code&gt; 方法后，你的代码就可以在完整自动评分器上编译。&lt;/p&gt;
&lt;h2&gt;Guitar Hero&lt;/h2&gt;
&lt;p&gt;在项目这一部分，我们会创建另一个包，使用刚刚完成的 &lt;code&gt;deque&lt;/code&gt; 包来生成合成乐器。我们会借助自己的数据结构，实现一个能够模拟拨动吉他弦的算法。&lt;/p&gt;
&lt;h3&gt;GH2 包&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;gh2&lt;/code&gt; 包只有一个你需要编辑的主要组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GuitarString&lt;/code&gt;：一个使用 &lt;code&gt;Deque&amp;#x3C;Double&gt;&lt;/code&gt; 实现 Karplus-Strong 算法、合成吉他弦声音的类。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们已经提供了 &lt;code&gt;GuitarString&lt;/code&gt; 的骨架代码，你将在这里使用项目第一部分完成的 &lt;code&gt;deque&lt;/code&gt; 包。&lt;/p&gt;
&lt;h2&gt;GuitarString&lt;/h2&gt;
&lt;p&gt;我们要完成 &lt;code&gt;GuitarString&lt;/code&gt; 文件。它应使用 &lt;code&gt;deque&lt;/code&gt; 包来复现拨弦的声音。我们将使用 Karplus-Strong 算法；这个算法用 &lt;code&gt;Deque&lt;/code&gt; 很容易实现。&lt;/p&gt;
&lt;p&gt;Karplus-Strong 算法可以简单地概括为以下三个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用随机噪声替换 &lt;code&gt;Deque&lt;/code&gt; 中的每个元素，也就是介于 -0.5 和 0.5 之间的 &lt;code&gt;double&lt;/code&gt; 值。&lt;/li&gt;
&lt;li&gt;删除 &lt;code&gt;Deque&lt;/code&gt; 前端的 double，并将它与 &lt;code&gt;Deque&lt;/code&gt; 中下一个 double 的平均值（提示：使用 &lt;code&gt;removeFirst()&lt;/code&gt; 和 &lt;code&gt;get()&lt;/code&gt;）乘以能量衰减系数 0.996；我们把整个结果称为 &lt;code&gt;newDouble&lt;/code&gt;。然后把 &lt;code&gt;newDouble&lt;/code&gt; 添加到 &lt;code&gt;Deque&lt;/code&gt; 后端。&lt;/li&gt;
&lt;li&gt;播放步骤 2 中出队的 &lt;code&gt;double&lt;/code&gt;。回到步骤 2，并永远重复。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从图形上看，假设顶部所示 Deque 的前两个值为 0.2 和 0.4，那么我们会删除 0.2，把它与 0.4 结合得到 0.2988，将 0.2988 添加到队尾，并播放 0.2。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/865a7fa5ac91-karplus-strong.png&quot; alt=&quot;Karplus-Strong 环形缓冲区示意图&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以使用 &lt;code&gt;StdAudio.play&lt;/code&gt; 方法播放一个 &lt;code&gt;double&lt;/code&gt; 值。例如，&lt;code&gt;StdAudio.play(0.333)&lt;/code&gt; 会告诉扬声器振膜向前伸展到其总行程的三分之一；&lt;code&gt;StdAudio.play(-0.9)&lt;/code&gt; 会告诉它把自己的小心脏几乎尽可能向后拉伸。扬声器振膜的运动会移动空气；如果以漂亮的模式移动空气，经过数十亿年的进化，你的意识就会把这些扰动理解为悦耳的声音。&lt;/p&gt;
&lt;p&gt;可以阅读原页面链接了解更多。如果只执行一次 &lt;code&gt;StdAudio.play(0.9)&lt;/code&gt;，之后再也不播放任何内容，那么图中振膜就只会静止在向前行程的十分之九处。&lt;/p&gt;
&lt;p&gt;完成 &lt;code&gt;GuitarString.java&lt;/code&gt;，使其实现 Karplus-Strong 算法的步骤 1 和步骤 2。请注意，在 &lt;code&gt;GuitarString&lt;/code&gt; 构造器中，需要先用 0 填满 &lt;code&gt;Deque&lt;/code&gt; 缓冲区。步骤 3 将由 &lt;code&gt;GuitarString&lt;/code&gt; 类的客户端完成。&lt;/p&gt;
&lt;p&gt;与往常一样，请确保使用 Maven 打开项目，否则 IntelliJ 无法找到 &lt;code&gt;StdAudio&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如，提供的 &lt;code&gt;TestGuitarString&lt;/code&gt; 类包含一个样例测试 &lt;code&gt;testPluckTheAString&lt;/code&gt;，它会尝试在吉他弦上播放 A 音。运行这个测试时应该能听到 A 音。如果没有听到，请尝试运行 &lt;code&gt;testTic&lt;/code&gt; 方法并从那里开始调试。可以考虑在 &lt;code&gt;GuitarString.java&lt;/code&gt; 中添加一个 &lt;code&gt;print&lt;/code&gt; 或 &lt;code&gt;toString&lt;/code&gt; 方法，以帮助自己观察各次 tic 之间发生了什么。&lt;/p&gt;
&lt;p&gt;请注意，我们在这里说的是 &lt;code&gt;Deque&lt;/code&gt;，但没有指定使用哪一种 &lt;code&gt;Deque&lt;/code&gt; 实现。这是因为我们只需要 &lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;removeFirst&lt;/code&gt; 和 &lt;code&gt;get&lt;/code&gt; 这些操作，而任何实现 &lt;code&gt;Deque&lt;/code&gt; 的类都具备这些操作。因此，实际实现中可以自由选择 &lt;code&gt;LinkedListDeque&lt;/code&gt; 或 &lt;code&gt;ArrayDeque&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;作为一项可选但强烈推荐的练习，请思考使用二者各自的权衡，并与朋友讨论哪一种更合适，或它们是否同样适合。&lt;/p&gt;
&lt;h4&gt;GuitarHeroLite&lt;/h4&gt;
&lt;p&gt;现在你也应该能够使用 &lt;code&gt;GuitarHeroLite&lt;/code&gt; 类。运行它会提供一个图形界面，让用户（也就是你）能够使用 &lt;code&gt;gh2&lt;/code&gt; 包中的 &lt;code&gt;GuitarString&lt;/code&gt; 类交互式地播放声音。&lt;/p&gt;
&lt;p&gt;以下部分不计分。&lt;/p&gt;
&lt;p&gt;考虑创建一个与 &lt;code&gt;GuitarHeroLite&lt;/code&gt; 类似的程序 &lt;code&gt;GuitarHero&lt;/code&gt;，但它支持从 110 Hz 到 880 Hz 的半音阶共 37 个音。使用下面的 37 个按键表示键盘，从最低音到最高音：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String keyboard = &quot;q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/&apos; &quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种键盘排列模仿钢琴键盘：“白键”位于 qwerty 和 zxcv 行，“黑键”位于键盘的 12345 和 asdf 行。&lt;/p&gt;
&lt;p&gt;字符串 &lt;code&gt;keyboard&lt;/code&gt; 的第 &lt;code&gt;i&lt;/code&gt; 个字符对应频率：&lt;/p&gt;
&lt;p&gt;$$440 \cdot 2^{(i - 24) / 12}$$&lt;/p&gt;
&lt;p&gt;因此字符 &lt;code&gt;q&lt;/code&gt; 是 110 Hz，&lt;code&gt;i&lt;/code&gt; 是 220 Hz，&lt;code&gt;v&lt;/code&gt; 是 440 Hz，空格是 880 Hz。千万不要考虑创建 37 个独立的 &lt;code&gt;GuitarString&lt;/code&gt; 变量或 37 路 &lt;code&gt;if&lt;/code&gt; 语句。应创建一个包含 37 个 &lt;code&gt;GuitarString&lt;/code&gt; 对象的数组，并使用 &lt;code&gt;keyboard.indexOf(key)&lt;/code&gt; 判断按下了哪个键。确保按下不对应这 37 个音符的按键时，程序不会崩溃。&lt;/p&gt;
&lt;h2&gt;仅供娱乐：TTFAF&lt;/h2&gt;
&lt;p&gt;当你比较确信 &lt;code&gt;GuitarString&lt;/code&gt; 工作正常之后，尝试运行 &lt;code&gt;TTFAF&lt;/code&gt;。确保声音已经打开。&lt;/p&gt;
&lt;p&gt;可以阅读 &lt;code&gt;GuitarPlayer&lt;/code&gt; 和 &lt;code&gt;TTFAF&lt;/code&gt; 类，弄清楚它们的工作方式。尤其是 &lt;code&gt;TTFAF&lt;/code&gt;，其中以注释代码的形式给出了另一种使用方式的示例。&lt;/p&gt;
&lt;h2&gt;更多娱乐内容&lt;/h2&gt;
&lt;p&gt;这一部分不计分，仅供娱乐。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**竖琴弦：**在 &lt;code&gt;gh2&lt;/code&gt; 包中创建 &lt;code&gt;Harp&lt;/code&gt; 类。在 &lt;code&gt;tic()&lt;/code&gt; 中把新值入队之前翻转其符号，会把类似吉他的声音变成类似竖琴的声音。你可能需要调整衰减系数以增强真实感；由于 &lt;code&gt;tic()&lt;/code&gt; 的这一变化会使自然共振频率减半，还需要把缓冲区大小调整为原来的某个二倍因子。&lt;/li&gt;
&lt;li&gt;**鼓：**在 &lt;code&gt;gh2&lt;/code&gt; 包中创建 &lt;code&gt;Drum&lt;/code&gt; 类。在 &lt;code&gt;tic()&lt;/code&gt; 中，以 0.5 的概率在新值入队之前翻转其符号，会产生鼓声。衰减系数 1.0（不衰减）会得到更好的声音，而且需要调整使用的频率集合。&lt;/li&gt;
&lt;li&gt;吉他通过 6 根实体琴弦播放每个音符。要模拟这一点，可以把 &lt;code&gt;GuitarString&lt;/code&gt; 实例分成 6 组；拨动一根弦时，把同一组中的其他弦全部归零。&lt;/li&gt;
&lt;li&gt;钢琴具有制音踏板，可以用来让琴弦静止。可以这样实现：在某个特定按键（例如 Shift）被按住的迭代中改变衰减系数。&lt;/li&gt;
&lt;li&gt;我们使用的是十二平均律，但当音程遵循纯律中的小整数分数时，人耳会觉得更悦耳。例如，演奏者使用铜管乐器以泛音方式演奏纯五度时，频率比是 $3/2 = 1.5$，而不是 $2^{7/12} \sim 1.498$。编写一个程序，使每一对相邻音符都采用纯律。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么它能工作&lt;/h2&gt;
&lt;p&gt;让 Karplus-Strong 算法发挥作用的两个主要组成部分，是环形缓冲区反馈机制和求平均操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**环形缓冲区反馈机制。**环形缓冲区模拟能量来回传播的介质，也就是两端固定的弦。环形缓冲区的长度决定所得声音的基频。从听觉上说，反馈机制只会加强基频及其谐波，也就是基频整数倍的频率。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;能量衰减系数（这里是 0.996）模拟波在琴弦中完成一次往返时发生的轻微能量耗散。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**求平均操作。**求平均操作是一种温和的低通滤波器，它会去除较高频率，同时允许较低频率通过，这也是“低通”名称的由来。因为该操作位于反馈路径中，所以它会逐渐削弱较高次谐波，同时保留较低次谐波；这与真实拨动吉他弦的声音非常接近。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;提交与评分&lt;/h2&gt;
&lt;p&gt;要提交项目，请添加并提交文件，然后推送到远程仓库。接着在 Gradescope 中进入相应作业并提交。&lt;/p&gt;
&lt;p&gt;在 Gradescope 完成最终提交之后，必须推送你的 snaps 仓库。&lt;/p&gt;
&lt;p&gt;在推送 snaps 仓库并提交 Snaps Gradescope 作业之前，你的 Gradescope 分数不会传入 Beacon。要推送 snaps 仓库，请运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd $SNAPS_DIR
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推送 snaps 仓库后，Gradescope 上有一项作业需要你提交 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库，方式与 Lab 1A 类似。这只适用于完整评分器，不适用于检查点或额外加分作业。&lt;/p&gt;
&lt;p&gt;即使忘记了，也可以在截止日期之后一周内完成这一步。如果超过一周仍未推送，就必须使用 slip days。&lt;/p&gt;
&lt;p&gt;**注意：**截至 2/07，我们略微修改了上面的 snaps 要求：从仅仅推送 snaps 仓库，改为推送后还要在 Gradescope 提交，这样学生能够更清楚地确认提交已经成功。&lt;/p&gt;
&lt;p&gt;整个项目价值 640 分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deque/LinkedListDeque&lt;/code&gt;：230 分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deque/ArrayDeque&lt;/code&gt;：230 分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deque/MaxArrayDeque&lt;/code&gt;：80 分&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh2/GuitarString&lt;/code&gt;：80 分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外共有 48 分额外加分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;检查点满分：16 分&lt;/li&gt;
&lt;li&gt;制作自动评分器：32 分&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h4&gt;Deque&lt;/h4&gt;
&lt;h5&gt;问：当我不知道 Deque 元素的类型时，应该怎样打印它们？&lt;/h5&gt;
&lt;p&gt;答：使用默认打印出来的字符串即可。这个字符串来自对象对 &lt;code&gt;toString()&lt;/code&gt; 的实现，我们会在本学期稍后讨论。例如，如果类中的泛型类型名叫 &lt;code&gt;Jumanji&lt;/code&gt;，要打印 &lt;code&gt;Jumanji j&lt;/code&gt;，可以调用 &lt;code&gt;System.out.print(j)&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;问：我无法让 Java 创建泛型对象数组！&lt;/h5&gt;
&lt;p&gt;答：使用课堂上见过的奇怪语法：&lt;code&gt;T[] a = (T[]) new Object[1000];&lt;/code&gt;。这里 &lt;code&gt;T&lt;/code&gt; 是泛型类型，是 “String” 或 “Integer” 等其他对象类型的占位符。&lt;/p&gt;
&lt;h5&gt;问：我这样做了，但编译器发出警告怎么办？&lt;/h5&gt;
&lt;p&gt;答：抱歉，这是 Java 设计者在为 Java 引入泛型时留下的问题，没有优雅的绕过方法。享受你的编译警告吧。我们会在几周后进一步讨论。&lt;/p&gt;
&lt;h5&gt;问：怎样让图中的箭头指向数据结构的特定字段？&lt;/h5&gt;
&lt;p&gt;在课堂图示中，箭头看起来能够指向数组中间，或者节点的特定字段。&lt;/p&gt;
&lt;p&gt;答：课堂上每当我画出指向某个对象的箭头时，指针都指向整个对象，而不是对象的某个特定字段。事实上，在 Java 中，引用不可能指向对象的字段。&lt;/p&gt;
&lt;h4&gt;Guitar Hero&lt;/h4&gt;
&lt;h5&gt;我遇到了 “class file contains wrong class” 错误。&lt;/h5&gt;
&lt;p&gt;确保所有 Java 文件顶部都有正确的 package 声明。还要确保属于 &lt;code&gt;gh2&lt;/code&gt; 包的所有内容都位于名为 &lt;code&gt;gh2&lt;/code&gt; 的文件夹中。&lt;/p&gt;
&lt;h5&gt;程序说我没有重写某个抽象方法，但我明明重写了！&lt;/h5&gt;
&lt;p&gt;很可能存在拼写错误。重写方法时应始终使用 &lt;code&gt;@Override&lt;/code&gt; 标记，这样编译器就能发现这类拼写错误。&lt;/p&gt;
&lt;h5&gt;运行提供的测试时出现 “No runnable methods”。&lt;/h5&gt;
&lt;p&gt;确保已经取消测试代码的注释，包括 &lt;code&gt;@Test&lt;/code&gt; 注解。&lt;/p&gt;
&lt;h5&gt;编译代码时，它说类型 K#1 与 K#2 不兼容，或出现类似内容。&lt;/h5&gt;
&lt;p&gt;如果正在定义内部类，请确保它没有重新声明一个新的泛型类型参数。例如，&lt;code&gt;private class MapWizard&amp;#x3C;Z&gt; implements Iterator&amp;#x3C;Z&gt;{&lt;/code&gt; 中第一个 &lt;code&gt;&amp;#x3C;Z&gt;&lt;/code&gt; 不应该存在。&lt;/p&gt;
&lt;h5&gt;我遇到了奇怪的自动评分器错误！&lt;/h5&gt;
&lt;p&gt;虽然 &lt;code&gt;GuitarString&lt;/code&gt; 是一个吉他弦模拟器，但它本身不应播放任何声音。播放操作应由 &lt;code&gt;GuitarString&lt;/code&gt; 的客户端完成。&lt;/p&gt;
&lt;p&gt;鸣谢：RingBuffer 图示来自 Wikipedia。本作业改编自 Kevin Wayne 的 Guitar Heroine 作业。&lt;/p&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;查看 Project 1 幻灯片，其中包含一些更偏视觉化的额外提示。&lt;/li&gt;
&lt;li&gt;如果卡住了，甚至不知道从哪里开始：一个很好的第一步是实现 &lt;code&gt;SLList&lt;/code&gt; 和/或 &lt;code&gt;AList&lt;/code&gt;。为了最高效率，可以与一两个或三个朋友一起练习这些课堂数据结构。&lt;/li&gt;
&lt;li&gt;一次只做一点。一次写大量代码只会带来痛苦，而且只会带来痛苦。如果写了太多内容并感到不知所措，请把不必要的部分注释掉。&lt;/li&gt;
&lt;li&gt;如果第一次尝试进展很糟，不要害怕丢弃代码并重新开始。每个类实际需要的代码并没有那么多；课程参考实现的每个 &lt;code&gt;.java&lt;/code&gt; 文件大约 130 行，包括所有注释和空白行。&lt;/li&gt;
&lt;li&gt;对于 &lt;code&gt;ArrayDeque&lt;/code&gt;，可以先完全不实现扩缩容，直到确认没有扩缩容时的代码正确。调整大小是一项性能优化，也是获得满分的必需要求。&lt;/li&gt;
&lt;li&gt;在尝试编写代码之前，先在纸上画出数据结构的样子。如果能找到愿意配合的朋友，让对方发出命令，而你尝试把每一步都画出来。试着设计一些能够暴露实现问题的操作。&lt;/li&gt;
&lt;li&gt;仔细考虑数据结构从空变为某个非零大小（例如 4 个元素），再回到 0，然后再次变为非零大小时会发生什么。这是常见疏漏。&lt;/li&gt;
&lt;li&gt;一旦理解哨兵节点，它们会让生活轻松很多。&lt;/li&gt;
&lt;li&gt;循环数据结构可能需要一段时间才能理解，但会让两种实现都轻松很多，尤其是 &lt;code&gt;ArrayDeque&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;考虑编写辅助函数完成计算数组索引等小任务。例如，在课程的 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现中，编写了一个 &lt;code&gt;int minusOne(int index)&lt;/code&gt; 函数，用来计算给定索引“前一个”位置的索引。&lt;/li&gt;
&lt;li&gt;考虑使用 Lab 2 中安装的 Java Visualizer，在调试器逐步执行时可视化你的 Deque。该工具的图标是一只带眼睛的蓝色咖啡杯，位于调试器面板中 “Console” 标签旁边。如果无法显示，请参阅 CS 61B 插件指南。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/e7065a8b9dbe-java_visualizer.png&quot; alt=&quot;Java Visualizer 示例&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj1/proj1&quot;&gt;https://sp21.datastructur.es/materials/proj/proj1/proj1&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 1 EC：自动评分器</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-1ec-autograder</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-1ec-autograder</guid><description>CS61B Spring 2021 Project 1 EC：自动评分器中文学习资料。</description><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;这个项目是可选项目，总共价值 32 分额外加分。在这个项目中，你将为 Project 1 的数据结构部分构建一个基础自动评分器。该项目可能颇具挑战，并且属于额外加分内容，因此在 Office Hours 和 Ed 上会被安排为最低帮助优先级。&lt;/p&gt;
&lt;p&gt;在骨架中，我们为 Project 1 额外加分提供了以下文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;student/StudentArrayDeque.java&lt;/code&gt;：一个有缺陷的 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tester/ArrayDequeSolution.java&lt;/code&gt;：一个正确的 &lt;code&gt;ArrayDeque&lt;/code&gt; 实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tester/AssertEqualsStringDemo.java&lt;/code&gt;：演示如何使用 &lt;code&gt;assertEquals&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tester/StudentArrayDequeLauncher.java&lt;/code&gt;：演示如何使用 &lt;code&gt;StudentArrayDeque&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你会再次注意到，我们在这里使用包，把学生实现与测试基础设施分离开来。&lt;/p&gt;
&lt;h2&gt;获取骨架文件&lt;/h2&gt;
&lt;p&gt;和以前一样，使用命令 &lt;code&gt;git pull skeleton master&lt;/code&gt; 拉取骨架。&lt;/p&gt;
&lt;h2&gt;随机测试&lt;/h2&gt;
&lt;p&gt;告诉你一个有趣的秘密：Project 1 的自动评分器在很大程度上依赖随机测试。例如，Gradescope 上的 JUnit 测试会随机调用你的 &lt;code&gt;LinkedListDeque&lt;/code&gt; 类以及课程的正确实现 &lt;code&gt;LinkedListDequeSolution&lt;/code&gt; 的方法；一旦发现任何不一致，测试就会失败，并打印出导致失败的操作序列。在项目这一部分，你将使用同样的思路，假装自己正在为该类编写自动评分器。&lt;/p&gt;
&lt;p&gt;这里还会介绍如何制作漂亮的 JUnit 错误消息。这实际上是一项非常重要的技能。如果你的测试具有容易阅读而且有帮助的错误消息，未来的雇主会非常喜欢你。&lt;/p&gt;
&lt;p&gt;开始项目时，首先应确保 IntelliJ 已经正确导入项目。尝试运行 &lt;code&gt;StudentArrayDequeLauncher.java&lt;/code&gt;。如果正常工作，你应当看到 0 到 9 之间的数字被打印出来，但顺序不一定相同。如果遇到问题，请按照 Lab 2 中的说明操作。&lt;/p&gt;
&lt;h3&gt;任务 I&lt;/h3&gt;
&lt;p&gt;接下来，在 &lt;code&gt;tester&lt;/code&gt; 包中创建名为 &lt;code&gt;TestArrayDequeEC.java&lt;/code&gt; 的 JUnit 测试文件。文件开头应包含所需的 package 声明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;package tester;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后确保添加必要的 import：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import static org.junit.Assert.*;
import org.junit.Test;
import student.StudentArrayDeque;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个文件中编写一个用 &lt;code&gt;@Test&lt;/code&gt; 注解标记的 JUnit 测试。测试方法叫什么并不重要。你的测试应随机调用 &lt;code&gt;StudentArrayDeque&lt;/code&gt; 和 &lt;code&gt;ArrayDequeSolution&lt;/code&gt; 的方法，直到二者在某个输出上产生分歧。可以使用 &lt;code&gt;StdRandom&lt;/code&gt; 库生成随机数。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;StudentArrayDequeLauncher&lt;/code&gt; 当作指导。如果复制粘贴其中的代码，请务必使用 &lt;code&gt;@source&lt;/code&gt; 标记注明来源。你在 Project 1 中编写的测试在这里也可能很有用。&lt;/p&gt;
&lt;p&gt;这项额外加分作业不会测试 &lt;code&gt;equals(Object o)&lt;/code&gt; 和 &lt;code&gt;iterator()&lt;/code&gt; 方法，因此只为其他方法编写测试。&lt;/p&gt;
&lt;p&gt;在这个项目中，Deque 的类型必须使用 &lt;code&gt;Integer&lt;/code&gt;，即 &lt;code&gt;StudentArrayDeque&amp;#x3C;Integer&gt;&lt;/code&gt;。只使用 &lt;code&gt;addFirst&lt;/code&gt;、&lt;code&gt;addLast&lt;/code&gt;、&lt;code&gt;removeFirst&lt;/code&gt; 和 &lt;code&gt;removeLast&lt;/code&gt; 方法就应能够找到错误；当然，你也可以尝试其他方法。&lt;/p&gt;
&lt;p&gt;你的测试不应引发 &lt;code&gt;NullPointerException&lt;/code&gt;。确保永远不要从空的 &lt;code&gt;ArrayDeque&lt;/code&gt; 中删除，因为 &lt;code&gt;Integer x = ad.removeFirst()&lt;/code&gt; 可能导致 &lt;code&gt;NullPointerException&lt;/code&gt;。此外，在本项目中，从 Deque 取出值时始终使用 &lt;code&gt;Integer&lt;/code&gt;，不要使用 &lt;code&gt;int&lt;/code&gt;；也就是说，不要写 &lt;code&gt;int x = ad.removeFirst()&lt;/code&gt;。为什么这会产生问题，请阅读下方“常见问题”。&lt;/p&gt;
&lt;h3&gt;任务 II&lt;/h3&gt;
&lt;p&gt;当你成功让测试稳定失败后，更棘手的部分开始了。仅仅告诉学生代码失败，只会带来眼泪、悲伤、困惑以及深夜的 Ed 帖子。因此，你要修改自动评分器，让它告诉学生真正有用的信息。&lt;/p&gt;
&lt;p&gt;为此，我们将利用 &lt;code&gt;assertEquals(message, expected, actual)&lt;/code&gt; 方法。它会向用户输出有帮助的消息。&lt;/p&gt;
&lt;p&gt;此方法如何使用，可以查看 examples 文件夹中的 &lt;code&gt;AssertEqualsStringDemo.java&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;TestArrayDequeEC.java&lt;/code&gt;，使传递给 &lt;code&gt;assertEquals&lt;/code&gt; 的 &lt;code&gt;message&lt;/code&gt; 参数包含一系列会导致 &lt;code&gt;StudentArrayDeque&lt;/code&gt; 输出错误答案的操作。&lt;/p&gt;
&lt;p&gt;传给 &lt;code&gt;assertEquals&lt;/code&gt; 的字符串消息必须是一系列方法调用，并且序列中的最后一次调用会产生错误返回值。例如，若先在前端添加 5，再在前端添加 3，随后从前端删除时得到错误值，那么传给 &lt;code&gt;assertEquals&lt;/code&gt; 的 String 消息必须&lt;strong&gt;恰好&lt;/strong&gt;是下面这样，每条命令之间有换行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;addFirst(5)
addFirst(3)
removeFirst()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要把 expected 和 actual 值放进 String 消息，因为它们会作为 &lt;code&gt;expected&lt;/code&gt; 和 &lt;code&gt;actual&lt;/code&gt; 参数单独传给 &lt;code&gt;assertEquals&lt;/code&gt;。换句话说，你的消息&lt;strong&gt;不应该&lt;/strong&gt;长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;addFirst(5)
addFirst(3)
removeFirst(), student was 3, correct was 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它也不应该长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;addFirst(5)
addFirst(3)
removeFirst(): 3
removeLast(): 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提示&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;编写一次比较整个 Deque 的测试可能不是好主意。假设你写了一个返回 &lt;code&gt;false&lt;/code&gt; 的 &lt;code&gt;compareDeques(studentDeque, solutionDeque)&lt;/code&gt; 方法。即使该方法返回 &lt;code&gt;false&lt;/code&gt;，它也没有告诉你是哪一个操作导致了失败。测试单个操作的输出要容易得多，例如比较 &lt;code&gt;student.removeFirst()&lt;/code&gt; 与 &lt;code&gt;solution.removeFirst()&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果你坚持一次比较整个 Deque，&lt;code&gt;assertEquals&lt;/code&gt; 不会按你希望的方式工作。例如，即便 &lt;code&gt;deque1&lt;/code&gt; 和 &lt;code&gt;deque2&lt;/code&gt; 中的所有元素都相同，&lt;code&gt;assertEquals(deque1, deque2)&lt;/code&gt; 也不会返回 true。如果确实想比较整个 Deque，需要编写自己的比较方法；不过老实说，这项作业完全没有必要这样做。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StdRandom&lt;/code&gt; 类是生成随机数最简单的方式。可查看其官方文档了解方法列表。&lt;/li&gt;
&lt;li&gt;本作业不需要进行任何异常捕获或抛出，我们此时还没有在 CS 61B 中学习这些内容。&lt;/li&gt;
&lt;li&gt;在执行操作的同时构建失败序列！不要等检测到失败之后才尝试构造它，那样非常困难。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h4&gt;怎样为 &lt;code&gt;printDeque()&lt;/code&gt; 编写测试？&lt;/h4&gt;
&lt;p&gt;这会相当复杂，而且我们的自动评分器也没有聪明到能够读取你的输出。请测试其他方法。如果你真的非常好奇，可以搜索 “redirect standard output”。&lt;/p&gt;
&lt;h4&gt;我遇到了 “reference to assertEquals is ambiguous” 错误。&lt;/h4&gt;
&lt;p&gt;遇到神秘错误消息时，应当始终尝试在网上搜索。请记住，成为能够自给自足的程序员是 CS 61B 的主要目标之一。Google 的第一个结果应该就足够了。&lt;/p&gt;
&lt;h4&gt;我不断遇到 &lt;code&gt;NullPointerException&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;首先，确保没有尝试从可用大小之外的位置执行 &lt;code&gt;get&lt;/code&gt;。其次，如果你写了类似 &lt;code&gt;int result = deque.removeFirst()&lt;/code&gt; 的代码，请改写为 &lt;code&gt;Integer result = deque.removeFirst()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;发生这一错误，是因为 Java 会自由地把 &lt;code&gt;Integer&lt;/code&gt;（装箱类型）转换为 &lt;code&gt;int&lt;/code&gt;（基本类型），这叫作拆箱。可是只有引用类型能够为 null；因此，如果尝试自动把 null &lt;code&gt;Integer&lt;/code&gt; 转换为 &lt;code&gt;int&lt;/code&gt;，你的代码就会得到 &lt;code&gt;NullPointerException&lt;/code&gt;。&lt;code&gt;StudentArrayDeque&lt;/code&gt; 本身有缺陷，可能错误地返回 null，从而在你的代码中触发这个问题。&lt;/p&gt;
&lt;h4&gt;自动评分器在抱怨我的失败序列。&lt;/h4&gt;
&lt;p&gt;可以想象，这个 gold project 的自动评分器是一头奇怪而复杂的野兽，因为它必须对自动评分器的输出进行自动评分。为了简化事情，失败 assert 的 String 参数必须包含失败序列，并且&lt;strong&gt;只能&lt;/strong&gt;包含失败序列；所有测试必须因为 assert 失败而失败，不能因为 null pointer exception 失败。传给 assert 语句的 String 参数中不能包含任何无关信息。&lt;/p&gt;
&lt;h4&gt;自动评分器仍然在抱怨我的失败序列。&lt;/h4&gt;
&lt;p&gt;必须包含引发失败的那个操作。例如，如果 &lt;code&gt;size()&lt;/code&gt; 返回错误值，就必须在失败序列中包含 &lt;code&gt;size()&lt;/code&gt;，因为要求提供的是“一系列方法调用，其中序列中的最后一次调用产生错误返回值”。还要确保失败序列只出现一次。&lt;/p&gt;
&lt;h4&gt;我把这些都试过了，自动评分器仍然在抱怨我的失败序列。&lt;/h4&gt;
&lt;p&gt;从在线自动评分器中复制它报告的失败序列，编写一个简单的 &lt;code&gt;Quick.java&lt;/code&gt; 文件：创建 &lt;code&gt;studentDeque&lt;/code&gt;，执行列出的操作，并打印最后一步的结果。你很可能会发现，实际结果与测试在自动评分器中报告的并不相同。最可能的原因是漏记了某个操作，或者加入了实际上没有执行的操作。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj1/proj1ec&quot;&gt;https://sp21.datastructur.es/materials/proj/proj1/proj1ec&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 2：Gitlet</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-2-gitlet</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-2-gitlet</guid><description>CS61B Spring 2021 Project 2：Gitlet中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%85%B3%E4%BA%8E%E6%9C%AC%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3%E7%9A%84%E4%B8%80%E7%82%B9%E8%AF%B4%E6%98%8E&quot;&gt;关于本说明文档的一点说明&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#gitlet-%E6%A6%82%E8%A7%88&quot;&gt;Gitlet 概览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%86%85%E9%83%A8%E7%BB%93%E6%9E%84&quot;&gt;内部结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%A1%8C%E4%B8%BA%E7%9A%84%E8%AF%A6%E7%BB%86%E8%A7%84%E8%8C%83&quot;&gt;行为的详细规范&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%80%BB%E4%BD%93%E8%A7%84%E8%8C%83&quot;&gt;总体规范&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%91%BD%E4%BB%A4&quot;&gt;命令&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#init&quot;&gt;&lt;code&gt;init&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#add&quot;&gt;&lt;code&gt;add&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#commit&quot;&gt;&lt;code&gt;commit&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rm&quot;&gt;&lt;code&gt;rm&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#log&quot;&gt;&lt;code&gt;log&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#global-log&quot;&gt;&lt;code&gt;global-log&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#find&quot;&gt;&lt;code&gt;find&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#status&quot;&gt;&lt;code&gt;status&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#checkout&quot;&gt;&lt;code&gt;checkout&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#branch&quot;&gt;&lt;code&gt;branch&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rm-branch&quot;&gt;&lt;code&gt;rm-branch&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#reset&quot;&gt;&lt;code&gt;reset&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#merge&quot;&gt;&lt;code&gt;merge&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%AA%A8%E6%9E%B6%E4%BB%A3%E7%A0%81&quot;&gt;骨架代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3&quot;&gt;设计文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AF%84%E5%88%86%E5%99%A8%E8%AF%B4%E6%98%8E&quot;&gt;评分器说明&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#checkpoint-grader&quot;&gt;阶段检查评分器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%8C%E6%95%B4%E8%AF%84%E5%88%86%E5%99%A8&quot;&gt;完整评分器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#snaps-%E8%AF%84%E5%88%86%E5%99%A8&quot;&gt;Snaps 评分器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%A2%9D%E5%A4%96%E5%8A%A0%E5%88%86&quot;&gt;额外加分&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%85%B3%E4%BA%8E%E6%9C%AC%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%85%B6%E4%BB%96%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9&quot;&gt;关于本项目的其他注意事项&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%96%87%E4%BB%B6%E5%A4%84%E7%90%86&quot;&gt;文件处理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%86%E8%8A%82&quot;&gt;序列化细节&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B5%8B%E8%AF%95&quot;&gt;测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%9C%A8%E8%AF%BE%E7%A8%8B%E7%BB%84%E5%8F%82%E8%80%83%E5%AE%9E%E7%8E%B0%E4%B8%8A%E6%B5%8B%E8%AF%95&quot;&gt;在课程组参考实现上测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%90%86%E8%A7%A3%E9%9B%86%E6%88%90%E6%B5%8B%E8%AF%95&quot;&gt;理解集成测试&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%A4%BA%E4%BE%8B%E6%B5%8B%E8%AF%95&quot;&gt;示例测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%BA%E6%B5%8B%E8%AF%95%E5%87%86%E5%A4%87%E7%8E%AF%E5%A2%83&quot;&gt;为测试准备环境&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D%E8%BE%93%E5%87%BA&quot;&gt;使用模式匹配输出&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B5%8B%E8%AF%95%E5%B0%8F%E7%BB%93&quot;&gt;测试小结&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%B0%83%E8%AF%95%E9%9B%86%E6%88%90%E6%B5%8B%E8%AF%95&quot;&gt;调试集成测试&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%89%BE%E5%88%B0%E7%9C%9F%E6%AD%A3%E9%9C%80%E8%A6%81%E8%B0%83%E8%AF%95%E7%9A%84%E9%82%A3%E4%B8%80%E6%AC%A1%E7%A8%8B%E5%BA%8F%E6%89%A7%E8%A1%8C&quot;&gt;找到真正需要调试的那一次程序执行&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%BF%9C%E7%A8%8B%E5%8A%9F%E8%83%BD%E9%A2%9D%E5%A4%96%E5%8A%A0%E5%88%86&quot;&gt;远程功能（额外加分）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%BF%9C%E7%A8%8B%E5%91%BD%E4%BB%A4&quot;&gt;远程命令&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#add-remote&quot;&gt;&lt;code&gt;add-remote&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#rm-remote&quot;&gt;&lt;code&gt;rm-remote&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#push&quot;&gt;&lt;code&gt;push&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#fetch&quot;&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#pull&quot;&gt;&lt;code&gt;pull&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#i-%E5%BA%94%E9%81%BF%E5%85%8D%E7%9A%84%E5%81%9A%E6%B3%95&quot;&gt;I. 应避免的做法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#j-%E8%87%B4%E8%B0%A2&quot;&gt;J. 致谢&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;关于本说明文档的一点说明&lt;/h2&gt;
&lt;p&gt;这份说明文档相当长。前半部分会用较为冗长、细致的方式描述你需要支持的每一条命令；后半部分主要介绍测试细节，并给出一些建议。为了帮助你消化这些内容，我们准备了许多高质量视频，用来讲解说明文档中的不同部分，并建议你应该如何、从哪里开始。所有视频都会在文档中与其内容相关的位置给出链接；为了方便，我们也在这里统一列出。&lt;/p&gt;
&lt;p&gt;请注意：其中一些视频制作于 2020 年春季，当时 Gitlet 是 Project 3，而 Capers 是 Lab 12；另一些视频会短暂提到 Hilfinger 教授版本的 CS 61B 配置，例如名为 &lt;code&gt;shared&lt;/code&gt; 的远程仓库、名为 &lt;code&gt;repo&lt;/code&gt; 的仓库等。请忽略这些内容，因为它们对本学期没有帮助。作业本身的实际内容并未改变。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=yWBzCAY_5UI&quot;&gt;Git 介绍——第 1 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=CnMpARAOhFg&quot;&gt;Git 介绍——第 2 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/fvhqn5PeU_Q&quot;&gt;第 12 讲现场课程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gitlet 介绍播放列表
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=-1gE2cNFhPA&quot;&gt;第 1 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=GfmH9_8tM5w&quot;&gt;第 2 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=dv5VdbIZKF8&quot;&gt;第 3 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=k8jwbG8bE7Y&quot;&gt;第 4 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cdn-uploads.piazza.com/attach/k5eevxebzpj25b/jqr7jm9igtc7l5/k97ipfmgmb3n/Gitlet_Slides.pdf&quot;&gt;Itai 使用的幻灯片&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=JR3OYCMv9b4&amp;#x26;t=929s&quot;&gt;Merge 概览与示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/desB3AS6aZg&quot;&gt;分支概览与示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=uMYpuQuHGu0&amp;#x26;t=752s&quot;&gt;测试&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paper.dropbox.com/doc/Gitlet-Persistence--AyM0lOEaezWrTi7gG_Pt~bXcAg-zEnTGJhtUMtGr8ILYhoab&quot;&gt;设计持久化机制（文字笔记）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2021 年春季答疑时间演示：
&lt;ul&gt;
&lt;li&gt;Gitlet 入门
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/6JVdbNZm0cM&quot;&gt;第 1 部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/1d1yOSoTVAM&quot;&gt;第 2 部分&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/G3YU9oY8PcU&quot;&gt;设计 Gitlet&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj2/gitlet-design-notes.pdf&quot;&gt;笔记&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/l0X5NgzAWYQ&quot;&gt;Merge&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;随着更多资源被制作出来，我们会继续在这里添加，因此请经常刷新页面！&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Gitlet 概览&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;/strong&gt; 在开始这个项目之前，请确保你已经完成 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab6/lab6&quot;&gt;Lab 6：Canine Capers&lt;/a&gt;。Lab 6 的目的就是作为本项目的入门练习，它会非常有助于你开始项目，并确保你的环境已经准备妥当。你还应该观看 &lt;a href=&quot;https://youtu.be/fvhqn5PeU_Q&quot;&gt;Lecture 12：Gitlet&lt;/a&gt;，其中介绍了许多对本项目很有用的思想。&lt;/p&gt;
&lt;p&gt;在这个项目中，你将实现一个版本控制系统，它模仿流行版本控制系统 Git 的一部分基本功能。不过，我们的系统更小、更简单，因此我们把它命名为 &lt;strong&gt;Gitlet&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;版本控制系统本质上是一个针对一组相关文件的备份系统。Gitlet 支持的主要功能包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;保存整个文件目录中的内容。&lt;/strong&gt;
在 Gitlet 中，这一操作称为 &lt;em&gt;committing（提交）&lt;/em&gt;，保存下来的内容本身称为 &lt;em&gt;commits（提交）&lt;/em&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;恢复一个或多个文件的某个版本，或者恢复整个提交。&lt;/strong&gt;
在 Gitlet 中，这称为 &lt;em&gt;checking out（检出）&lt;/em&gt; 这些文件或该提交。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;查看备份历史。&lt;/strong&gt;
在 Gitlet 中，你通过一种叫做 &lt;em&gt;log（日志）&lt;/em&gt; 的内容查看历史。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;维护彼此相关的提交序列，这些序列称为 &lt;em&gt;branches（分支）&lt;/em&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;把一个分支中所做的修改合并到另一个分支中。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;版本控制系统的意义在于：当你创建复杂项目——甚至并不复杂的项目——或者与其他人协作开发项目时，它能够帮助你。你会定期保存项目的不同版本。如果之后某个时间点你不小心把代码弄坏了，就可以把源码恢复到之前提交过的版本，同时不会丢失在那之后做过的所有修改。如果你的协作者在某个提交中做出了修改，你也可以把这些修改整合（&lt;em&gt;merge，合并&lt;/em&gt;）到自己的版本中。&lt;/p&gt;
&lt;p&gt;在 Gitlet 中，你并不是每次只提交单独一个文件。相反，你可以同时提交一组逻辑上相关的文件。我们喜欢把每个提交看作项目在某个时间点的完整&lt;strong&gt;快照&lt;/strong&gt;。不过，为了简化说明，本文后面的许多例子一次只修改一个文件。你只需要始终记住：每次提交完全可以同时修改多个文件。&lt;/p&gt;
&lt;p&gt;在这个项目中，把随时间产生的提交可视化会很有帮助。假设我们有一个项目，其中只有文件 &lt;code&gt;wug.txt&lt;/code&gt;。我们先往里面添加一些文本并提交，然后修改文件并提交这些修改，再修改一次并再次提交。现在，我们总共保存了这个文件的三个版本，每个版本都比前一个更晚。可以把这些提交画成下面这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/426fa433f3f6-three_commits.png&quot; alt=&quot;三个提交&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里，我们画出的箭头表示：每个提交都包含某种指向前一个提交的引用。我们把前一个提交称为当前提交的 &lt;em&gt;parent commit（父提交）&lt;/em&gt;，这个概念之后会非常重要。但现在，这张图看起来熟悉吗？没错，它就是一个链表！&lt;/p&gt;
&lt;p&gt;Gitlet 背后的核心思想是：我们可以把文件不同版本的历史画成这样的列表。这样一来，恢复文件的旧版本就非常容易。你可以想象发出这样的命令：“Gitlet，请把文件恢复到提交 2 时的状态。”Gitlet 就会前往链表中的第二个结点，恢复其中保存的文件副本，同时删除第一个结点中存在但第二个结点中不存在的文件。&lt;/p&gt;
&lt;p&gt;如果我们让 Gitlet 恢复到一个旧提交，那么链表最前端将不再反映文件的当前状态，这可能有些误导。为了解决这个问题，我们引入一种叫做 &lt;em&gt;head pointer（头指针，也写作 HEAD 指针）&lt;/em&gt; 的东西。HEAD 指针记录我们当前位于链表中的哪个位置。通常，当我们不断创建提交时，HEAD 指针会一直停留在链表最前端，表示最新提交反映了文件的当前状态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/8b22af5878e1-simple_head.png&quot; alt=&quot;简单的 HEAD 指针&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过，假设我们恢复到提交 2 时的文件状态——严格来说，这对应你之后会在规范中看到的 &lt;code&gt;reset&lt;/code&gt; 命令。我们会把 HEAD 指针向后移动来表示这一点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/45de95c2bb1b-reverted_head.png&quot; alt=&quot;回退后的 HEAD 指针&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时，我们会说自己处于 &lt;em&gt;detached head state（分离 HEAD 状态）&lt;/em&gt;。你以前可能遇到过这个说法，现在你知道它是什么意思了！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3 月 5 日修订：&lt;/strong&gt; 请注意，在 Gitlet 中实际上没有办法进入分离 HEAD 状态，因为 Gitlet 不提供能够把 HEAD 指针直接移动到某个具体提交的 &lt;code&gt;checkout&lt;/code&gt; 命令。&lt;code&gt;reset&lt;/code&gt; 命令确实会移动到指定提交，但它同时也会移动分支指针。因此，在 Gitlet 中，你永远不会处于分离 HEAD 状态。&lt;/p&gt;
&lt;p&gt;好了，如果 Gitlet 只能做到这些，它会是一个相当简单的系统。但 Gitlet 还有一项本领：它不仅能够维护文件较旧和较新的版本，还可以维护彼此&lt;strong&gt;不同的发展版本&lt;/strong&gt;。假设你正在编写一个项目，并且对于接下来如何开发有两种想法：一种叫方案 A，另一种叫方案 B。Gitlet 允许你保存两个版本，并随时在它们之间切换。用图表示，大致如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/008f3e528937-two_versions.png&quot; alt=&quot;两个版本&quot;&gt;&lt;/p&gt;
&lt;p&gt;这已经不再真正像链表，而更像一棵树。我们把它称为 &lt;em&gt;commit tree（提交树）&lt;/em&gt;。继续沿用树的比喻，其中彼此分离的不同版本就称为树的 &lt;em&gt;branch（分支）&lt;/em&gt;。你可以分别继续开发每一个版本：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/69ebaae365b0-two_developed_versions.png&quot; alt=&quot;分别开发的两个版本&quot;&gt;&lt;/p&gt;
&lt;p&gt;这棵树中有两个指针，它们分别表示两个分支各自延伸到的最远位置。在任何时刻，只有其中一个指针处于当前激活状态，而它就是所谓的 HEAD 指针。HEAD 指针是位于当前分支最前端的指针。&lt;/p&gt;
&lt;p&gt;以上就是对 Gitlet 系统的简要概览！如果你现在还没有完全理解，不必担心；上面的内容只是为了让你对系统要做什么有一个高层次的认识。下一节开始将详细说明这个项目具体要求你完成哪些内容。&lt;/p&gt;
&lt;p&gt;不过，最后还要说明一点：提交树是&lt;strong&gt;不可变的（immutable）&lt;/strong&gt;。一旦一个提交结点被创建，就永远不能销毁，也完全不能修改。我们只能向提交树中添加新内容，不能修改已有内容。这是 Gitlet 的一项重要特性！Gitlet 的目标之一，就是帮助我们保存内容，避免不小心把它们删除。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;内部结构&lt;/h2&gt;
&lt;p&gt;真实的 Git 会区分多种不同的&lt;strong&gt;对象（objects）&lt;/strong&gt;。对我们而言，重要的对象包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;blobs（数据对象）&lt;/strong&gt;：保存下来的文件内容。由于 Gitlet 会保存文件的许多版本，因此单个文件可能对应多个 blob；不同提交会跟踪其中不同的 blob。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;trees（树对象）&lt;/strong&gt;：目录结构，把名称映射到 blob 和其他树（子目录）的引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;commits（提交对象）&lt;/strong&gt;：由日志消息、其他元数据（提交日期、作者等）、对某棵树的引用，以及对父提交的引用组合而成。仓库还会维护从 &lt;em&gt;branch heads（分支头）&lt;/em&gt; 到提交引用的映射，从而让某些重要提交拥有符号名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gitlet 在 Git 的基础上做了进一步简化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 tree 的信息直接并入 commit，并且不处理子目录。因此，每个仓库只有一个“扁平”的普通文件目录。&lt;/li&gt;
&lt;li&gt;只处理引用两个父提交的合并。在真实 Git 中，一个提交可以有任意数量的父提交。&lt;/li&gt;
&lt;li&gt;元数据只包括时间戳和日志消息。因此，一个 commit 将由以下部分组成：日志消息、时间戳、从文件名到 blob 引用的映射、一个父提交引用，以及对于合并提交而言的第二个父提交引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在我们的场景中，每个对象——每个 blob 和每个 commit——都有一个唯一的整数 ID，用作该对象的引用。Git 有一项很有意思的特性：这些 ID 是&lt;strong&gt;通用的&lt;/strong&gt;。这不同于典型的 Java 实现；两个内容完全相同的对象，在所有系统上都会拥有相同 ID。也就是说，我的电脑、你的电脑以及其他任何人的电脑都会为它们计算出完全相同的 ID。&lt;/p&gt;
&lt;p&gt;对于 blob，“相同内容”意味着文件内容相同。对于 commit，它意味着元数据相同、名称到引用的映射相同，并且父提交引用也相同。因此，仓库中的对象被称为**内容可寻址（content addressable）**的对象。&lt;/p&gt;
&lt;p&gt;Git 与 Gitlet 使用相同的方法实现这一点：它们使用一种叫做 **SHA-1（Secure Hash 1，安全散列算法 1）**的密码学哈希函数。SHA-1 可以从任意字节序列生成一个 160 位整数哈希值。密码学哈希函数具有这样的性质：想找到两个不同的字节流却让它们产生相同哈希值极其困难；事实上，仅仅给出一个哈希值，再反向找到任何能够生成该哈希值的字节流也极其困难。因此，我们基本上可以假设，任意两个不同内容的对象拥有相同 SHA-1 哈希值的概率为 $2^{-160}$，约等于 $10^{-48}$。简单来说，我们直接忽略发生哈希碰撞的可能性。因此，从理论上说，这个系统存在一个根本性缺陷，但在实践中它永远不会发生！&lt;/p&gt;
&lt;p&gt;幸运的是，Java 已经提供了用于计算 SHA-1 值的库类，因此你不必处理实际算法。你只需要确保正确地为所有对象生成标识。特别是，这包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在对 commit 计算哈希时，把它的全部元数据和引用都包括进去。&lt;/li&gt;
&lt;li&gt;通过某种方式区分 commit 的哈希与 blob 的哈希。一个不错的方法，是在 &lt;code&gt;.gitlet&lt;/code&gt; 目录中设计经过认真考虑的目录结构。另一种方法，是在计算每种对象的哈希时额外加入一个单词，并让 blob 与 commit 使用不同的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺便说一下，把 SHA-1 哈希值表示成 40 个字符的十六进制字符串后，它会非常适合作为文件名，用于在 &lt;code&gt;.gitlet&lt;/code&gt; 目录中存储数据——后面还会进一步讨论。它也提供了一种方便的方法来比较两个文件（blob）是否内容相同：如果它们的 SHA-1 相同，我们就直接假定文件相同。&lt;/p&gt;
&lt;p&gt;对于 remote（远程仓库，例如本学期一直使用的 &lt;code&gt;skeleton&lt;/code&gt;），我们会直接使用其他 Gitlet 仓库。Push（推送）只意味着：把远程仓库中尚不存在的所有 commit 和 blob 复制到远程仓库，再重置一个分支引用。Pull（拉取）同理，只不过方向相反。在本项目中，远程功能属于额外加分内容，拿满基础分并不要求实现它们。&lt;/p&gt;
&lt;p&gt;借助 Java 的**序列化（serialization）**设施，把内部对象从文件中读取出来或写入文件其实相当简单。接口 &lt;code&gt;java.io.Serializable&lt;/code&gt; 本身没有任何方法，但只要一个类实现了它，Java 运行时就会自动提供一种方式，把该类对象转换为字节流，再把字节流恢复成对象。你可以使用 I/O 类 &lt;code&gt;java.io.ObjectOutputStream&lt;/code&gt; 把字节流写入文件，再使用 &lt;code&gt;java.io.ObjectInputStream&lt;/code&gt; 读取并反序列化。&lt;/p&gt;
&lt;p&gt;“序列化”一词指的是：把某个任意结构——数组、树、图等——转换为顺序排列的字节序列。你应该已经在 Lab 6 中见过并练习过序列化。这里会采用非常相似的方法，因此在处理持久化和序列化时，请把自己的 Lab 6 代码作为参考资源。&lt;/p&gt;
&lt;p&gt;下面是一张对本节所讨论结构的总结示意图。可以看到，每个 commit（矩形）都指向若干 blob（圆形），而 blob 中包含文件内容。commit 保存文件名、对这些 blob 的引用以及父提交链接。这些引用在 &lt;code&gt;.gitlet&lt;/code&gt; 目录中使用 SHA-1 哈希值来表示，也就是图中 commit 上方以及 blob 下方的小型十六进制数字。较新的 commit 包含 &lt;code&gt;wug1.txt&lt;/code&gt; 的更新版本，但它与较旧的 commit 共享 &lt;code&gt;wug2.txt&lt;/code&gt; 的同一个版本。&lt;/p&gt;
&lt;p&gt;你的 commit 类必须以某种方式存储图中展示的全部信息。认真选择内部数据结构，会让实现变得更容易或更困难，因此你非常值得花时间进行规划，思考保存一切内容的最佳方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/339213fb9911-commits-and-blobs.png&quot; alt=&quot;两个提交及其 blob&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;行为的详细规范&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;总体规范&lt;/h3&gt;
&lt;p&gt;我们对程序结构给出的唯一硬性要求是：必须存在一个名为 &lt;code&gt;gitlet.Main&lt;/code&gt; 的类，并且它必须拥有 &lt;code&gt;main&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;我们还为你提供了一些工具方法，用于完成多种主要与文件系统有关的任务，这样你可以把精力集中在项目逻辑上，而不是处理操作系统文件操作的各种特殊之处。&lt;/p&gt;
&lt;p&gt;我们还添加了两个建议使用的类：&lt;code&gt;Commit&lt;/code&gt; 和 &lt;code&gt;Repository&lt;/code&gt;，帮助你开始。你当然可以编写额外的 Java 类来支持项目；如果愿意，也可以删除我们建议的类。不过，除了 JUnit 之外，不得使用任何外部代码，也不得使用 Java 之外的其他编程语言。你可以使用任何 Java 标准库内容，以及我们提供的工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要把所有事情都写在 &lt;code&gt;Main&lt;/code&gt; 类中。&lt;/strong&gt; &lt;code&gt;Main&lt;/code&gt; 类主要应该调用 &lt;code&gt;Repository&lt;/code&gt; 类中的辅助方法。可以参考 Lab 6 中的 &lt;code&gt;CapersRepository&lt;/code&gt; 和 &lt;code&gt;Main&lt;/code&gt; 类，了解我们推荐的结构。&lt;/p&gt;
&lt;p&gt;这份说明文档的大部分内容将描述 &lt;code&gt;Gitlet.java&lt;/code&gt; 的 &lt;code&gt;main&lt;/code&gt; 方法在收到不同 Gitlet 命令行参数时必须如何反应。不过，在逐条命令说明之前，下面先给出整个项目都应满足的一些总体规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;为了让 Gitlet 工作，它需要一个位置存储文件的旧副本以及其他元数据。所有这些内容都&lt;strong&gt;必须&lt;/strong&gt;保存在名为 &lt;code&gt;.gitlet&lt;/code&gt; 的目录中，正如真实 Git 系统把此类信息保存在 &lt;code&gt;.git&lt;/code&gt; 目录中一样。名称前带 &lt;code&gt;.&lt;/code&gt; 的文件属于隐藏文件，在多数操作系统中默认不会显示。在 Unix 中，命令 &lt;code&gt;ls -a&lt;/code&gt; 会显示这些文件。&lt;/p&gt;
&lt;p&gt;如果某个位置中存在 &lt;code&gt;.gitlet&lt;/code&gt; 目录，就认为该位置已经初始化了 Gitlet 系统。大部分 Gitlet 命令——除 &lt;code&gt;init&lt;/code&gt; 之外——只需要在已经初始化 Gitlet 的目录中工作，也就是存在 &lt;code&gt;.gitlet&lt;/code&gt; 子目录的位置。不在 &lt;code&gt;.gitlet&lt;/code&gt; 目录中的文件，包括你当前从仓库中取出并编辑的文件，以及打算加入仓库的文件，统称为**工作目录（working directory）**中的文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;大多数命令都有运行时间或内存使用要求，你必须遵守这些要求。一些运行时间被描述为“相对于任何重要度量都是常数时间”。这里的重要度量包括：文件数量或文件大小的任意度量，以及提交数量的任意度量。&lt;/p&gt;
&lt;p&gt;你可以忽略序列化和反序列化所花费的时间，但有一个限制：序列化时间不能以任何方式依赖于曾经被添加、提交等操作处理过的全部文件的总大小。如果你不知道什么是序列化，请回顾 Lab 6。你也可以假设，从哈希表中取值是常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;某些命令有规定的失败情况，并且要求输出指定错误消息。后面会给出这些消息的精确格式。所有错误消息都以句号结尾；由于自动评分会进行字面匹配，请务必包括句号。&lt;/p&gt;
&lt;p&gt;如果程序遇到其中任意失败情况，就必须打印对应错误消息，并且不能修改其他任何内容。&lt;strong&gt;除列出的失败情况之外，你不需要处理其他错误情况。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;还有一些需要处理的失败情况并不属于某一条特定命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果用户没有输入任何参数，打印：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Please enter a command.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果用户输入了不存在的命令，打印：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No command with that name exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果用户输入命令时，操作数的数量或格式错误，打印：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Incorrect operands.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果用户输入了一条要求位于已初始化 Gitlet 工作目录中的命令——也就是当前目录应包含 &lt;code&gt;.gitlet&lt;/code&gt; 子目录——但当前并不在这样的目录中，打印：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Not in an initialized Gitlet directory.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;某些命令会列出它们与真实 Git 的区别。说明文档不会穷尽 Gitlet 与 Git 的&lt;strong&gt;全部&lt;/strong&gt;区别，但会指出一些较大、可能令人困惑或产生误导的区别。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;除规范明确要求输出的内容外，&lt;strong&gt;不要输出任何其他东西&lt;/strong&gt;。如果你输出了不必要的文字，自动评分器中的某些测试会失败。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如需立即退出程序，可以调用 &lt;code&gt;System.exit(0)&lt;/code&gt;。例如，如果辅助函数执行到一半发生错误，而你希望 Gitlet 立即终止，可以调用这个函数。&lt;strong&gt;注意：传入 &lt;code&gt;System.exit&lt;/code&gt; 的参数始终应该是 &lt;code&gt;0&lt;/code&gt;。&lt;/strong&gt; 在 CS 61C 中，你会学习这个参数——也叫错误码——的意义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规范把某些命令归类为“危险命令”。危险命令可能覆盖普通文件，而不仅是元数据。例如，当用户要求 Gitlet 恢复文件的旧版本时，Gitlet 可能覆盖这些文件当前的版本。这只是提醒你一下，所以在测试这些命令前请戴好头盔 :)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;命令&lt;/h2&gt;
&lt;p&gt;下面将详细介绍你必须支持的每一条命令。请记住，优秀的程序员始终关心自己使用的数据结构。在阅读这些命令时，你首先应该思考：怎样存储数据，才能方便地支持这些命令；其次应该思考：是否有机会复用已经实现的命令或辅助逻辑。提示：在 Project 2 后面的部分中，你有大量机会复用前面已经写过的代码。&lt;/p&gt;
&lt;p&gt;我们在一些命令旁列出了可能有帮助的课程讲次，但并不强制要求你使用这些课程中介绍的概念。一些更容易令人困惑的命令还配有概念测验，你绝对应该利用它们检查自己的理解。&lt;strong&gt;这些测验不计分&lt;/strong&gt;，只是帮助你在尝试实现命令之前验证理解是否正确。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;init&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main init
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 在当前目录中创建一个新的 Gitlet 版本控制系统。系统会自动从一个提交开始：这个提交不包含任何文件，其提交消息为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;initial commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必须完全如上书写，不添加标点。&lt;/p&gt;
&lt;p&gt;系统最初只有一个分支：&lt;code&gt;master&lt;/code&gt;。&lt;code&gt;master&lt;/code&gt; 一开始指向初始提交，并且 &lt;code&gt;master&lt;/code&gt; 是当前分支。初始提交的时间戳应为 UTC 时间 1970 年 1 月 1 日星期四 00:00:00，日期格式可以自行选择。这个时间称为 “Unix Epoch（Unix 纪元）”，在内部用时间值 &lt;code&gt;0&lt;/code&gt; 表示。&lt;/p&gt;
&lt;p&gt;由于 Gitlet 创建的所有仓库中的初始提交内容都完全相同，因此所有仓库会自动共享这个提交——它们的初始提交拥有相同 UID——并且所有仓库中的所有提交最终都会沿父提交追溯到它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于任何重要度量，都应为常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果当前目录中已经存在 Gitlet 版本控制系统，则中止操作。不得用新系统覆盖已有系统。输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A Gitlet version-control system already exists in the current directory.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 15 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;add&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main add [file name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 把文件当前存在的内容副本加入&lt;strong&gt;暂存区（staging area）&lt;/strong&gt;。暂存区会在 &lt;code&gt;commit&lt;/code&gt; 命令的说明中进一步介绍。正因为如此，添加文件也叫做把文件 &lt;em&gt;stage for addition（暂存以待添加）&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;如果一个已经暂存的文件再次被暂存，应该用新内容覆盖暂存区中原来的条目。暂存区必须位于 &lt;code&gt;.gitlet&lt;/code&gt; 内部的某个位置。&lt;/p&gt;
&lt;p&gt;如果文件当前的工作目录版本，与当前提交中保存的版本完全相同，则不要把它暂存为待添加；如果它已经在暂存区中，则把它从暂存区移除。比如，某个文件先被修改并执行 &lt;code&gt;add&lt;/code&gt;，然后又被改回原来的版本，就会出现这种情况。&lt;/p&gt;
&lt;p&gt;如果该文件在命令执行时被暂存为待删除——参见 &lt;code&gt;gitlet rm&lt;/code&gt;——则执行 &lt;code&gt;add&lt;/code&gt; 后，它不再处于待删除状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 最坏情况下，相对于所添加文件的大小应为线性时间，同时还可以有 $\lg N$ 的开销，其中 $N$ 是当前提交中的文件数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果文件不存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;File does not exist.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出，不能修改任何内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 20 行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt; 在真实 Git 中，可以一次添加多个文件；在 Gitlet 中，一次只能添加一个文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;建议复习的课程：&lt;/strong&gt; Lecture 16（集合、映射与抽象数据类型），Lecture 19（哈希）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;commit&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main commit [message]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 保存当前提交以及暂存区中的被跟踪文件快照，以便将来恢复，同时创建一个新提交。我们说这个提交正在**跟踪（tracking）**这些已保存文件。&lt;/p&gt;
&lt;p&gt;默认情况下，每个提交中的文件快照与它的父提交完全相同：它会保持文件版本不变，不会自动更新它们。只有某个被跟踪文件在提交时已经暂存为待添加，新提交才会更新该文件的内容；此时，新提交会保存暂存的文件版本，而不是从父提交继承的版本。&lt;/p&gt;
&lt;p&gt;如果某个暂存为待添加的文件此前没有被父提交跟踪，新提交会保存它并开始跟踪它。最后，如果当前提交所跟踪的某个文件，通过下方的 &lt;code&gt;rm&lt;/code&gt; 命令被&lt;strong&gt;暂存为待删除（staged for removal）&lt;/strong&gt;，那么新提交可以不再跟踪它。&lt;/p&gt;
&lt;p&gt;最核心的结论是：默认情况下，一个提交与它的父提交拥有相同的文件内容。暂存的添加和删除，就是对新提交所做的更新。当然，提交日期以及通常情况下的提交消息也会与父提交不同。&lt;/p&gt;
&lt;p&gt;关于 &lt;code&gt;commit&lt;/code&gt;，还有以下几点需要注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建提交后，清空暂存区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;commit&lt;/code&gt; 命令永远不会添加、修改或删除工作目录中的普通文件，&lt;code&gt;.gitlet&lt;/code&gt; 目录中的内容除外。&lt;code&gt;rm&lt;/code&gt; 命令则会删除普通文件，并把它们暂存为待删除，使它们在下一次 &lt;code&gt;commit&lt;/code&gt; 后不再被跟踪。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件被暂存为待添加或待删除之后，工作目录中对这些文件所做的任何进一步修改都会被 &lt;code&gt;commit&lt;/code&gt; 忽略。&lt;code&gt;commit&lt;/code&gt; 只修改 &lt;code&gt;.gitlet&lt;/code&gt; 目录中的内容。例如，如果你使用 Unix 的 &lt;code&gt;rm&lt;/code&gt; 命令——而不是 Gitlet 自己的同名命令——删除了一个被跟踪文件，这不会影响下一次提交；下一次提交仍然会包含该文件此前被跟踪的版本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行 &lt;code&gt;commit&lt;/code&gt; 后，新提交作为新结点加入提交树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刚刚创建的提交成为“当前提交”，HEAD 指针现在指向它。之前的 HEAD 提交成为新提交的父提交。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个提交都应包含创建它时的日期和时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个提交都有一条与之关联的日志消息，用来描述提交中文件的修改。这条消息由用户指定。整条消息在传给 &lt;code&gt;main&lt;/code&gt; 的 &lt;code&gt;args&lt;/code&gt; 数组中必须只占一个元素。若消息中含有多个单词，用户必须用引号把它包起来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;每个提交都由自己的 SHA-1 ID 标识。该 ID 的计算必须包括：提交中的文件 blob 引用、父提交引用、日志消息以及提交时间。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于提交数量的任何度量，运行时间都应为常数。相对于提交正在跟踪的所有文件的总大小，运行时间不得差于线性时间。&lt;/p&gt;
&lt;p&gt;此命令还有一项内存要求：执行提交后，&lt;code&gt;.gitlet&lt;/code&gt; 目录增加的大小不得超过提交时被暂存为待添加的文件总大小，额外元数据不计在内。这意味着，不要重复存储从父提交继承而来的文件版本。提示：blob 是内容可寻址的，请充分利用 SHA-1。&lt;/p&gt;
&lt;p&gt;你可以保存文件的完整副本；不需要担心只保存 diff（差异）之类的优化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果没有任何文件被暂存，则中止并输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No changes added to the commit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个提交都必须拥有非空白消息。如果消息为空，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Please enter a commit message.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被跟踪文件在工作目录中缺失或被修改，并不属于失败情况。请完全忽略 &lt;code&gt;.gitlet&lt;/code&gt; 目录之外的当前状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt; 在真实 Git 中，因为 merge 的存在，提交可以拥有多个父提交；真实提交还包含更多元数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 35 行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;建议复习的课程：&lt;/strong&gt; Lecture 19（集合、映射与抽象数据类型），Lecture 19（哈希）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一次提交执行前后的示意图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/33919f7aa6eb-before_and_after_commit.png&quot; alt=&quot;提交前后&quot;&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;rm&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main rm [file name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 如果文件当前被暂存为待添加，则取消它的暂存。如果当前提交正在跟踪该文件，则把它暂存为待删除；如果用户尚未手动删除工作目录中的该文件，还要从工作目录中删除它。&lt;strong&gt;除非当前提交正在跟踪它，否则不要删除该文件。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于任何重要度量，都应为常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果文件既没有被暂存，也没有被 HEAD 提交跟踪，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No reason to remove the file.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 是。不过，如果你使用课程组提供的工具方法，最多只会伤害仓库文件，不会删除目录中的所有其他文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 20 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;log&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 从当前 HEAD 提交开始，沿着提交树向后显示每个提交的信息，直到初始提交。只跟随第一父提交链接，忽略合并提交中的第二父提交。在普通 Git 中，这相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --first-parent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这组提交结点称为该提交的&lt;strong&gt;历史（history）&lt;/strong&gt;。对于历史中的每个结点，应显示提交 ID、提交创建时间以及提交消息。必须严格遵循下面的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;===
commit a0da1ea5a15ab613bf9961fd86f010cf74c7ee48
Date: Thu Nov 9 20:00:05 2017 -0800
A commit message.

===
commit 3e8bf1d794ca2e9ef8a4007275acf3751c7170ff
Date: Thu Nov 9 17:01:33 2017 -0800
Another commit message.

===
commit e881c9575d180a215d1a636545b8fd9abfb1d2bb
Date: Wed Dec 31 16:00:00 1969 -0800
initial commit

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个提交前都有 &lt;code&gt;===&lt;/code&gt;，每个提交后都有一个空行。与真实 Git 一样，每个条目显示提交对象唯一的 SHA-1 ID。&lt;/p&gt;
&lt;p&gt;提交中显示的时间戳应反映当前时区，而不是 UTC。因此，初始提交的时间戳不一定显示为 1970 年 1 月 1 日星期四 00:00:00，而可能显示成对应的太平洋标准时间。根据你居住的位置，你的时区可能不同，这没有问题。&lt;/p&gt;
&lt;p&gt;按照从新到旧的顺序显示提交，最新提交位于最上方。顺便说一下，Java 类 &lt;code&gt;java.util.Date&lt;/code&gt; 和 &lt;code&gt;java.util.Formatter&lt;/code&gt; 对获取与格式化时间很有帮助。请研究这些类，而不要尝试手动拼接日期。&lt;/p&gt;
&lt;p&gt;当然，你生成的 SHA-1 标识符会与示例不同，不必担心。测试会确认你输出的内容“看起来像”SHA-1 标识符，测试章节会进一步介绍。&lt;/p&gt;
&lt;p&gt;对于拥有两个父提交的合并提交，在第一行下方额外加入一行，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;===
commit 3e8bf1d794ca2e9ef8a4007275acf3751c7170ff
Merge: 4975af1 2c1ead1
Date: Sat Nov 11 12:30:00 2017 -0800
Merged development into master.

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Merge:&lt;/code&gt; 后面的两个十六进制数字，分别是第一父提交和第二父提交 ID 的前七位，并且顺序必须如此。第一父提交是执行 merge 时你所在分支的提交；第二父提交来自被合并进来的分支。这与普通 Git 相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于 HEAD 历史中的结点数量，应为线性时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 无。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 20 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图展示了某个特定提交的历史。如果当前分支的 HEAD 指针恰好指向该提交，&lt;code&gt;log&lt;/code&gt; 会输出图中圈出的提交信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/4023de93bf32-history.png&quot; alt=&quot;提交历史&quot;&gt;&lt;/p&gt;
&lt;p&gt;历史会忽略其他分支以及当前提交之后的未来结点。现在有了“历史”的概念，我们可以更精确地描述前面所说的提交树不可变：&lt;strong&gt;拥有某个特定 ID 的提交，其历史永远都不能发生任何改变。&lt;/strong&gt; 如果你把提交树理解成许多历史的集合，那么真正的意思就是：每一段历史都是不可变的。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;global-log&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main global-log
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 与 &lt;code&gt;log&lt;/code&gt; 类似，但显示曾经创建过的所有提交的信息。提交输出顺序无所谓。提示：&lt;code&gt;gitlet.Utils&lt;/code&gt; 中有一个很有用的方法，可以帮助你遍历目录中的文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于曾经创建的提交数量，应为线性时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 无。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 10 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;find&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main find [commit message]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 输出所有提交消息与给定消息相同的提交 ID，每行一个。如果存在多个这样的提交，则分别占一行输出。提交消息是单个操作数；如果消息中包含多个单词，应像 &lt;code&gt;commit&lt;/code&gt; 命令一样用引号包起来。&lt;/p&gt;
&lt;p&gt;提示：本命令的提示与 &lt;code&gt;global-log&lt;/code&gt; 相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于提交数量，应为线性时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果不存在这样的提交，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Found no commit with that message.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt; 真实 Git 中没有这条命令。通过对 &lt;code&gt;log&lt;/code&gt; 输出使用 &lt;code&gt;grep&lt;/code&gt;，可以实现类似效果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 15 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;status&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main status
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 显示当前存在的所有分支，并使用 &lt;code&gt;*&lt;/code&gt; 标记当前分支。同时显示哪些文件已经暂存为待添加或待删除。输出必须严格遵循以下格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;=== Branches ===
*master
other-branch

=== Staged Files ===
wug.txt
wug2.txt

=== Removed Files ===
goodbye.txt

=== Modifications Not Staged For Commit ===
junk.txt (deleted)
wug3.txt (modified)

=== Untracked Files ===
random.stuff

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后两个部分——未暂存的修改与未跟踪文件——在 2021 年春季课程中属于额外加分内容，共值 32 分。你可以把它们留空，只保留标题。&lt;/p&gt;
&lt;p&gt;各部分之间有一个空行，并且整个 &lt;code&gt;status&lt;/code&gt; 输出最后也必须有一个空行。各条目应按照 Java 字符串比较规则的字典序排列；分支名前面的星号不参与排序。&lt;/p&gt;
&lt;p&gt;如果工作目录中的某个文件符合以下任意情况，则它属于“已修改但未暂存（modified but not staged）”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前提交正在跟踪该文件；工作目录中的文件内容发生了变化，但没有被暂存。&lt;/li&gt;
&lt;li&gt;文件已经暂存为待添加，但工作目录中的当前内容与暂存内容不同。&lt;/li&gt;
&lt;li&gt;文件已经暂存为待添加，但在工作目录中被删除。&lt;/li&gt;
&lt;li&gt;文件没有被暂存为待删除；当前提交正在跟踪它，但它已从工作目录中删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一类“Untracked Files（未跟踪文件）”指的是：文件存在于工作目录中，但既没有暂存为待添加，也没有被当前提交跟踪。这也包括已经被暂存为待删除、但后来又在 Gitlet 不知情的情况下重新创建的文件。&lt;/p&gt;
&lt;p&gt;忽略可能被放入工作目录的所有子目录，因为 Gitlet 不处理子目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 运行时间应只依赖于工作目录中的数据量、暂存为待添加或待删除的文件数量，以及分支数量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 无。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 45 行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/LSgBK5RAdRwhAqKK8&quot;&gt;概念测验（不涉及分支）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/RHUiRkSrtgysC6En8&quot;&gt;概念测验（涉及分支）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;checkout&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;checkout&lt;/code&gt; 是一种较为通用的命令，它会根据参数形式完成不同操作。一共有三种使用方式。下面每一部分中都会看到三个编号，它们分别对应三种用法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;java gitlet.Main checkout -- [file name]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;java gitlet.Main checkout [commit id] -- [file name]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;java gitlet.Main checkout [branch name]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;取出 HEAD 提交中该文件的版本，并把它写入工作目录。如果工作目录中已经存在同名文件，就覆盖它。新写入的文件版本不进入暂存区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取出给定 ID 对应提交中的该文件版本，并把它写入工作目录。如果工作目录中已经存在同名文件，就覆盖它。新写入的文件版本不进入暂存区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取出给定分支 HEAD 提交中的所有文件，并把它们写入工作目录。如果工作目录中已经有这些文件，就覆盖原有版本。命令完成后，给定分支成为当前分支（HEAD）。当前分支正在跟踪、但被检出分支中不存在的所有文件都应被删除。清空暂存区；但如果用户试图检出当前分支，则按照下方失败情况处理，不能清空暂存区。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;相对于被检出文件的大小，应为线性时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于涉及整个提交快照的操作，相对于提交快照中所有文件的总大小，应为线性时间；相对于提交数量的任何度量，应为常数；相对于分支数量，也应为常数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如果文件不存在于 HEAD 提交中，中止并输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;File does not exist in that commit.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不得改变当前工作目录（CWD）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不存在给定 ID 的提交，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No commit with that id exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则，如果该文件不存在于给定提交中，输出与失败情况 1 相同的消息。不得改变当前工作目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果不存在该名称的分支，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No such branch exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果给定分支就是当前分支，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No need to checkout the current branch.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果工作目录中存在一个在当前分支中未被跟踪的文件，并且此次 checkout 会覆盖它，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;There is an untracked file in the way; delete it, or add and commit it first.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。必须在做任何其他事情之前完成此检查。不得改变当前工作目录。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt; 真实 Git 不会清空暂存区，并且会把检出的文件加入暂存区。此外，如果 checkout 会覆盖或撤销已经暂存的添加或删除，真实 Git 不会执行该操作。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;[commit id]&lt;/code&gt; 是前面介绍过的十六进制数字。真实 Git 有一项很方便的功能：可以使用唯一前缀来缩写提交 ID。例如，在不存在其他对象的 SHA-1 也以相同六位数字开头的常见情况下，可以把：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;a0da1ea5a15ab613bf9961fd86f010cf74c7ee48
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缩写为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;a0da1e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你也应该让长度少于 40 个字符的提交 ID 支持这种行为。遗憾的是，如果用朴素方式实现，使用缩短 ID 可能会减慢对象查找，使查找文件的时间相对于对象数量变成线性。因此，对于使用缩短 ID 的命令，我们不考核运行时间。&lt;/p&gt;
&lt;p&gt;不过，我们建议你查看真实 Git 仓库中的 &lt;code&gt;.git&lt;/code&gt; 目录，尤其是 &lt;code&gt;.git/objects&lt;/code&gt;，研究 Git 如何加快搜索。你可能会看到一种熟悉的数据结构，只不过它使用文件系统而不是指针来实现。&lt;/p&gt;
&lt;p&gt;只有第 3 种用法——检出完整分支——会修改暂存区。其他两种情况下，原本暂存为待添加或待删除的文件仍保持其暂存状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 是！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第 1 种：约 15 行。&lt;/li&gt;
&lt;li&gt;第 2 种：约 5 行。&lt;/li&gt;
&lt;li&gt;第 3 种：约 15 行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/mfHLnrU9VX349jnr7&quot;&gt;概念测验（不涉及分支）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/tbZuqDz7x3u41JhM6&quot;&gt;概念测验（涉及分支）&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;branch&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main branch [branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 创建一个拥有给定名称的新分支，并让它指向当前 HEAD 提交。分支本质上只是一个名称，它对应一个指向提交结点的引用——也就是一个 SHA-1 标识符。&lt;/p&gt;
&lt;p&gt;与真实 Git 一样，这条命令&lt;strong&gt;不会&lt;/strong&gt;立即切换到刚创建的新分支。在你第一次调用 &lt;code&gt;branch&lt;/code&gt; 之前，程序应当已经运行在默认分支 &lt;code&gt;master&lt;/code&gt; 上。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于任何重要度量，都应为常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果给定名称的分支已经存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A branch with that name already exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 10 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面详细看看 &lt;code&gt;branch&lt;/code&gt; 做了什么。假设当前状态如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/b4066dd1a0fa-simple_history.png&quot; alt=&quot;简单历史&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main branch cool-beans
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/1d29ffcc1fec-just_called_branch.png&quot; alt=&quot;刚刚创建分支&quot;&gt;&lt;/p&gt;
&lt;p&gt;嗯……好像没发生多少事情。接着使用下面的命令切换到新分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main checkout cool-beans
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/eb1805cded5b-just_switched_branch.png&quot; alt=&quot;刚刚切换分支&quot;&gt;&lt;/p&gt;
&lt;p&gt;怎么还是好像没发生什么？！好吧，现在我们创建一次提交：修改一些文件，然后运行 &lt;code&gt;java gitlet.Main add ...&lt;/code&gt;，再运行 &lt;code&gt;java gitlet.Main commit ...&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/403e3f0bf2f6-commit_on_branch.png&quot; alt=&quot;在分支上提交&quot;&gt;&lt;/p&gt;
&lt;p&gt;我明明听说这里会出现分叉，可我看到的仍然是一条直线。怎么回事？或许应该使用下面的命令回到另一个分支：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main checkout master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/5e45b6aaf4d6-checkout_master.png&quot; alt=&quot;检出 master&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在，再创建一次提交……&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/8ba3056c9265-branched.png&quot; alt=&quot;形成分叉&quot;&gt;&lt;/p&gt;
&lt;p&gt;呼！这就是分支的完整思想。你理解发生什么了吗？创建分支所做的全部事情，就是给我们添加一个新指针。在任何时刻，这些指针中的一个被视为当前激活指针，也叫做 HEAD 指针，图中使用 &lt;code&gt;*&lt;/code&gt; 表示。&lt;/p&gt;
&lt;p&gt;我们可以通过 &lt;code&gt;checkout [branch name]&lt;/code&gt; 切换当前激活的 HEAD 指针。每当提交时，我们都会在当前激活的 HEAD 提交后添加一个子提交，即使该提交已经存在另一个子提交。这样，一个提交可以拥有多个子提交，自然就形成了分叉行为。&lt;/p&gt;
&lt;p&gt;可以在&lt;a href=&quot;https://youtu.be/desB3AS6aZg&quot;&gt;这里&lt;/a&gt;查看分支的视频示例和概览。&lt;/p&gt;
&lt;p&gt;请&lt;strong&gt;务必&lt;/strong&gt;确保你的 &lt;code&gt;branch&lt;/code&gt;、&lt;code&gt;checkout&lt;/code&gt; 与 &lt;code&gt;commit&lt;/code&gt; 行为与上面的描述完全一致。这是 Gitlet 的核心功能，许多其他命令都依赖它。如果这些核心功能中的任何一部分损坏，自动评分器中的大量测试都会失败！&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;rm-branch&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main rm-branch [branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 删除给定名称的分支。这里只是删除与该分支关联的指针，并不意味着删除在该分支下创建的所有提交，也不删除其他相关内容。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于任何重要度量，都应为常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果给定名称的分支不存在，中止并输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A branch with that name does not exist.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果试图删除当前所在分支，中止并输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot remove the current branch.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 15 行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;reset&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main reset [commit id]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 检出给定提交所跟踪的全部文件。删除当前提交正在跟踪、但目标提交中不存在的文件。同时，把当前分支的 HEAD 移动到该提交结点。&lt;/p&gt;
&lt;p&gt;关于使用 &lt;code&gt;reset&lt;/code&gt; 后 HEAD 指针发生什么变化，可以回看开头的示例。&lt;code&gt;[commit id]&lt;/code&gt; 可以像 &lt;code&gt;checkout&lt;/code&gt; 一样使用缩写。执行后清空暂存区。&lt;/p&gt;
&lt;p&gt;从本质上讲，这条命令就是：检出任意指定提交，同时修改当前分支的 HEAD。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; 相对于给定提交快照中全部被跟踪文件的总大小，应为线性时间；相对于提交数量的任何度量，应为常数时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果不存在给定 ID 的提交，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;No commit with that id exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果工作目录中的某个文件在当前分支中未被跟踪，并且此次 &lt;code&gt;reset&lt;/code&gt; 会覆盖它，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;There is an untracked file in the way; delete it, or add and commit it first.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。必须在进行任何其他操作之前完成这项检查。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 是！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt; 这条命令最接近真实 Git 的 &lt;code&gt;--hard&lt;/code&gt; 选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git reset --hard [commit hash]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 10 行。为什么能这么短？请记住，你应该复用代码 :)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;merge&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main merge [branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 把给定分支中的文件合并到当前分支中。这个方法有些复杂，下面给出更详细的说明。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先，考虑当前分支与给定分支的 &lt;strong&gt;split point（分叉点）&lt;/strong&gt;。例如，假设 &lt;code&gt;master&lt;/code&gt; 是当前分支，而 &lt;code&gt;branch&lt;/code&gt; 是给定分支：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/2134fe4fb149-split_point.png&quot; alt=&quot;分叉点&quot;&gt;&lt;/p&gt;
&lt;p&gt;分叉点是当前分支 HEAD 与给定分支 HEAD 的一个&lt;strong&gt;最新公共祖先（latest common ancestor）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;**公共祖先（common ancestor）**是这样一个提交：从两个分支 HEAD 出发，分别沿 0 条或更多父指针路径，都能够到达它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最新公共祖先&lt;/strong&gt;是一个公共祖先，并且它不能是其他任何公共祖先的祖先。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，在上图中，最左侧提交也是 &lt;code&gt;master&lt;/code&gt; 和 &lt;code&gt;branch&lt;/code&gt; 的公共祖先，但它同时是右边紧邻提交的祖先，所以它不是最新公共祖先。&lt;/p&gt;
&lt;p&gt;如果分叉点与给定分支的 HEAD 是同一个提交，则什么也不做；合并已经完成，操作输出下面的消息并结束：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Given branch is an ancestor of the current branch.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果分叉点就是当前分支的 HEAD，则效果相当于检出给定分支，操作输出下面的消息并结束：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Current branch fast-forwarded.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则，继续执行下方步骤。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只在给定分支中被修改的文件&lt;/strong&gt;
如果某个文件从分叉点之后在给定分支中被修改，但在当前分支中没有被修改，则应把它改成给定分支中的版本——也就是从给定分支最前端的提交中检出该文件——并自动暂存。&lt;/p&gt;
&lt;p&gt;为了说得更清楚，“文件从分叉点之后在给定分支中被修改”意味着：该文件在给定分支 HEAD 提交中的内容，与它在分叉点提交中的内容不同。请记住，blob 是内容可寻址的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;只在当前分支中被修改的文件&lt;/strong&gt;
如果某个文件从分叉点之后在当前分支中被修改，但在给定分支中没有被修改，则保持当前状态不变。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;两个分支以相同方式修改的文件&lt;/strong&gt;
如果某个文件在当前分支与给定分支中以完全相同的方式被修改——也就是两边现在内容相同，或者两边都删除了它——则合并不改变该文件。&lt;/p&gt;
&lt;p&gt;如果文件在当前分支和给定分支中都被删除，但工作目录中存在同名文件，则不要碰这个工作目录文件；在合并提交中，该文件仍保持不存在，不被跟踪，也不被暂存。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分叉点不存在、只在当前分支中存在的文件&lt;/strong&gt;
保持当前状态不变。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分叉点不存在、只在给定分支中存在的文件&lt;/strong&gt;
检出该文件，并把它加入暂存区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分叉点存在、当前分支未修改、给定分支已删除的文件&lt;/strong&gt;
删除该文件，并停止跟踪，也就是把它暂存为待删除。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分叉点存在、给定分支未修改、当前分支已删除的文件&lt;/strong&gt;
继续保持该文件不存在。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;两个分支以不同方式修改的文件：发生冲突&lt;/strong&gt;
“以不同方式修改”可以指以下情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两边都修改了文件内容，但新内容彼此不同；&lt;/li&gt;
&lt;li&gt;一边修改了文件，另一边删除了文件；&lt;/li&gt;
&lt;li&gt;分叉点中不存在该文件，但当前分支和给定分支都新增了同名文件，并且内容不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;遇到这种情况时，用以下内容替换冲突文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C; HEAD
contents of file in current branch
=======
contents of file in given branch
&gt;&gt;&gt;&gt;&gt;&gt;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;contents of...&lt;/code&gt; 应替换为相应文件的实际内容，然后把结果暂存。把某个分支中已经删除的文件视为空文件。&lt;/p&gt;
&lt;p&gt;这里直接进行字符串拼接。如果文件末尾没有换行符，最终结果可能会像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C;&amp;#x3C; HEAD
contents of file in current branch=======
contents of file in given branch&gt;&gt;&gt;&gt;&gt;&gt;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这完全没问题。那些创建不符合标准的病态文件、并且分不清行终止符与行分隔符的人，只能承担相应结果。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;文件按照上述规则更新完成后，只要分叉点既不是当前分支 HEAD，也不是给定分支 HEAD，&lt;code&gt;merge&lt;/code&gt; 就会自动创建提交，其日志消息为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Merged [given branch name] into [current branch name].
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，如果合并过程中出现冲突，在终端——不是提交日志——输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Encountered a merge conflict.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合并提交与其他提交不同：它会把合并前当前分支的 HEAD 记录为&lt;strong&gt;第一父提交（first parent）&lt;/strong&gt;，并把命令行中给定、被合并进来的分支 HEAD 记录为第二父提交。&lt;/p&gt;
&lt;p&gt;可以在&lt;a href=&quot;https://www.youtube.com/watch?v=JR3OYCMv9b4&amp;#x26;t=929s&quot;&gt;这里&lt;/a&gt;查看这条命令的视频演示。&lt;/p&gt;
&lt;p&gt;顺便说一下，希望你已经注意到：提交集合最初是简单序列，后来变成树，现在终于成为完整的&lt;strong&gt;有向无环图（directed acyclic graph，DAG）&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;运行时间：&lt;/strong&gt; $O(N\lg N + D)$。其中，$N$ 是两个分支全部祖先提交的总数量，$D$ 是这些提交中所有文件的数据总量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果暂存区中存在待添加或待删除内容，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;You have uncommitted changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果给定名称的分支不存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A branch with that name does not exist.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果试图把一个分支与它自己合并，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cannot merge a branch with itself.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 &lt;code&gt;merge&lt;/code&gt; 创建的提交没有任何变化，导致普通 &lt;code&gt;commit&lt;/code&gt; 失败，则直接让正常的提交错误消息输出即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果当前工作目录中有未跟踪文件会被此次 merge 覆盖或删除，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;There is an untracked file in the way; delete it, or add and commit it first.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后退出。必须在进行任何其他操作之前完成此检查。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 是！&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;与真实 Git 的区别：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;真实 Git 在合并文件时处理得更加精细，只会在两个文件从分叉点之后都修改过的具体位置显示冲突。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果存在多个可能的分叉点，真实 Git 使用不同的规则决定选择哪一个。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;真实 Git 会强制用户先解决合并冲突，然后才能提交并完成合并。Gitlet 会直接把包含冲突标记的结果提交，因此你必须再创建一个单独提交来解决问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果工作目录中某个文件存在未暂存修改，而 merge 会修改它，真实 Git 会拒绝合并。你也可以选择实现这一检查，但课程测试不会覆盖该情况。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;课程组实现的代码行数：&lt;/strong&gt; 约 70 行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://forms.gle/Gu4FcFf1kfC7HYBa6&quot;&gt;概念测验&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;建议复习的课程：&lt;/strong&gt; Lecture 19（集合、映射与抽象数据类型），Lecture 22（图遍历）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;骨架代码&lt;/h2&gt;
&lt;p&gt;提供的骨架代码相当精简，大部分只是基本为空的类。我们在其中加入了有帮助的 Javadoc 注释，提示你可能希望在各文件中包含哪些内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你应该采用与 Capers 类似的结构：&lt;/strong&gt; &lt;code&gt;Main&lt;/code&gt; 类本身不负责大量实际工作，而只是根据 &lt;code&gt;args&lt;/code&gt; 调用其他方法。你完全可以删除其他类或添加自己的类，&lt;strong&gt;但 &lt;code&gt;Main&lt;/code&gt; 类必须保留&lt;/strong&gt;，否则测试将无法找到你的代码。&lt;/p&gt;
&lt;p&gt;如果你不知道从哪里开始，建议重新查看 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab6/lab6&quot;&gt;Lab 6：Canine Capers&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;设计文档&lt;/h2&gt;
&lt;p&gt;由于这一次你不是从一个内容丰富的骨架项目开始，&lt;strong&gt;我们要求所有人提交一份设计文档，描述自己的实现策略&lt;/strong&gt;。设计文档本身不计分，但在答疑时间或 Gitbug 中获得帮助之前，你必须拥有一份最新并且完整的设计文档。&lt;/p&gt;
&lt;p&gt;如果你没有设计文档，或者它不是最新状态、内容不完整，我们就不能帮助你。这样做同时是为了你和课程组：写出设计文档后，你就拥有了一份明确路线图，知道自己将如何处理这项作业。&lt;/p&gt;
&lt;p&gt;如果你需要帮助创建设计文档，我们当然可以帮你 :) 这里提供了&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj2/design.html&quot;&gt;一些指导原则&lt;/a&gt;，以及一个来自 Capers Lab 的&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj2/capers-example&quot;&gt;示例&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;评分器说明&lt;/h2&gt;
&lt;p&gt;Gitlet 一共有三个评分器：阶段检查评分器、完整评分器，以及 Snaps 评分器。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;阶段检查评分器&lt;/h3&gt;
&lt;p&gt;截止时间：3 月 12 日晚上 11:59，可获得 16 分额外加分。&lt;/p&gt;
&lt;p&gt;请提交到 Gradescope 上的 &lt;code&gt;Project 2: Gitlet Checkpoint&lt;/code&gt; 自动评分作业。&lt;/p&gt;
&lt;p&gt;它会测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你的程序能够成功编译。&lt;/li&gt;
&lt;li&gt;你能够通过骨架代码中的示例测试：&lt;code&gt;testing/samples/*.in&lt;/code&gt;。这些测试要求你实现：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;commit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout -- [file name]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout [commit id] -- [file name]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;log&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，它还会对以下内容发表评论，但暂时不计分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你是否通过代码风格检查。现阶段它会忽略 &lt;code&gt;TODO&lt;/code&gt; 一类的注释；最终提交时不会忽略。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些项目在最终提交中&lt;strong&gt;会&lt;/strong&gt;计分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3 月 4 日修订：&lt;/strong&gt; 编译器警告没有问题。&lt;/p&gt;
&lt;p&gt;你的 token 最大容量为 1，每 20 分钟恢复一个 token。测试失败时不会提供完整日志——也就是说，系统会告诉你失败了哪个测试，但不会给出额外消息。不过，由于这些测试文件本身已经提供，你可以直接在本地调试。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;完整评分器&lt;/h3&gt;
&lt;p&gt;截止时间：4 月 2 日晚上 11:59，共 1600 分。&lt;/p&gt;
&lt;p&gt;完整评分器是一套更加实质、更加全面的测试。你的 token 最大容量为 1，恢复速率如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2 月 20 日至 3 月 19 日：&lt;/strong&gt; 每 6 小时恢复一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 月 20 日至 3 月 26 日：&lt;/strong&gt; 每 3 小时恢复一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 月 26 日至 4 月 2 日：&lt;/strong&gt; 每 20 分钟恢复一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，与 Project 1 一样，使用评分器的次数受到限制。请善待自己，在开发过程中持续编写测试，不要过度依赖自动评分器来检查工作。&lt;/p&gt;
&lt;p&gt;与阶段检查类似，完整评分器会用英文提示说明每个测试大致在做什么，但不会提供实际 &lt;code&gt;.in&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Snaps 评分器&lt;/h3&gt;
&lt;p&gt;截止时间：4 月 9 日晚上 11:59。&lt;strong&gt;在你推送 snaps 仓库并提交到 Snaps Gradescope 作业之前，Gradescope 分数不会转移到 Beacon。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;要推送 snaps 仓库，请运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd $SNAPS_DIR
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推送 snaps 仓库后，Gradescope 上会有一项作业，要求你提交 &lt;code&gt;snaps-sp21-s***&lt;/code&gt; 仓库，与 Project 1 类似。该要求只适用于完整评分器，不适用于阶段检查或额外加分作业。&lt;/p&gt;
&lt;p&gt;即使忘记了，你也可以在截止日期之后的一周内完成。如果一周后仍未推送，就必须使用 slip days（延期天数）。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;额外加分&lt;/h3&gt;
&lt;p&gt;总共有 $16 + 32 + 64 = 112$ 分额外加分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;阶段检查：16 分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt; 命令正确输出 &lt;code&gt;Modifications Not Staged For Commit&lt;/code&gt; 与 &lt;code&gt;Untracked Files&lt;/code&gt; 两个部分：32 分。&lt;/li&gt;
&lt;li&gt;远程命令：64 分。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本说明文档剩余部分包含帮助你开始项目的各种资源，请认真阅读。&lt;strong&gt;关于测试和调试的章节会对你极其有帮助&lt;/strong&gt;，因为本项目的测试与调试方式和以前的项目不同，但也没有那么复杂。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;关于本项目的其他注意事项&lt;/h2&gt;
&lt;p&gt;呼！刚刚介绍了很多命令。不过不用担心，不是所有命令的难度都相同。每条命令旁边都标出了课程组完成该部分大约使用的代码行数——这里只计算该命令专属代码，不会重复计算多条命令共同复用的代码。&lt;/p&gt;
&lt;p&gt;你不必担心让自己的实现与课程组方案完全一致，但这些行数能够帮助你了解各命令相对需要多少时间。&lt;code&gt;merge&lt;/code&gt; 比其他命令更长，所以不要拖到最后一刻再实现！&lt;/p&gt;
&lt;p&gt;这是一个很有挑战性的项目，如果你感到迷茫，不知道从哪里开始，完全不奇怪。因此，与平时相比，你可以和其他同学进行稍微更密切的合作，但要遵守以下限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;gitlet/Main.java&lt;/code&gt; 文件开头附近的注释中，列出所有与你合作过的人。&lt;/li&gt;
&lt;li&gt;不要共享具体代码。所有合作成员都必须独立写出自己版本的算法，这样课程组才能看出各实现有所不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gitlet 的 Ed 汇总讨论串通常会变得非常长，但里面包含很多关于具体提交和实现方法的优质讨论。在这个项目中，你尤其应该利用班级人数多这一点，看看能否在汇总讨论串中找到与自己问题相似的人。你的问题极少会独特到从来没有其他人遇到过——除非它是与你个人设计有关的 Bug，这时应该提交 Gitbug。&lt;/p&gt;
&lt;p&gt;到这里，说明文档已经给了你足够的信息，可以开始项目。不过，为了进一步帮助你，还有几件事需要了解。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;文件处理&lt;/h2&gt;
&lt;p&gt;本项目需要读取和写入文件。完成这些操作时，你可能会发现 &lt;code&gt;java.io.File&lt;/code&gt; 和 &lt;code&gt;java.nio.file.Files&lt;/code&gt; 很有帮助。事实上，&lt;code&gt;java.io&lt;/code&gt; 与 &lt;code&gt;java.nio&lt;/code&gt; 包中的许多内容都可能有用。&lt;/p&gt;
&lt;p&gt;一定要阅读 &lt;code&gt;gitlet.Utils&lt;/code&gt;，看看课程组还为你编写了哪些工具。稍微深入研究这些内容，你可能会找到几个能够让本项目 I/O 部分&lt;strong&gt;简单得多&lt;/strong&gt;的方法。&lt;/p&gt;
&lt;p&gt;有一项警告：如果你发现自己在使用 reader、writer、scanner 或 stream，那么很可能已经把事情弄得比实际需要更复杂。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;序列化细节&lt;/h2&gt;
&lt;p&gt;仔细想想 Gitlet，你会发现每次运行程序只能执行一条命令。为了成功完成版本控制系统，你需要在多次命令运行之间记住整棵提交树。&lt;/p&gt;
&lt;p&gt;这意味着，你不仅要设计一组类，在程序执行期间表示 Gitlet 的内部结构；还必须在 &lt;code&gt;.gitlet&lt;/code&gt; 目录中设计一套对应的文件表示，使这些状态能够跨越程序的多次运行保存下来。&lt;/p&gt;
&lt;p&gt;正如前面所说，一个方便的方法是：把需要永久存储的运行时对象序列化到文件中。Java 运行时会负责判断哪些字段需要转换为字节，以及如何转换。&lt;/p&gt;
&lt;p&gt;你已经在 Lab 6 中完成过序列化，所以这里不会重复相关信息。如果对序列化的某个方面仍然不清楚，请重新阅读 Lab 6 说明文档的相关部分，并回看自己的代码。&lt;/p&gt;
&lt;p&gt;不过，这里有一个令人烦恼的细节需要注意：&lt;strong&gt;Java 序列化会顺着指针继续进行。&lt;/strong&gt; 也就是说，被传给 &lt;code&gt;writeObject&lt;/code&gt; 的对象会被序列化并写入，它指向的任何对象也会一起被序列化。&lt;/p&gt;
&lt;p&gt;例如，如果你在 commit 的内部表示中，直接使用指向其他 commit 对象的指针保存父提交，那么写入某个分支的 HEAD 时，会把整个提交子图中的所有 commit 和 blob 一起写进一个文件。这通常不是你想要的结果。&lt;/p&gt;
&lt;p&gt;为了避免这一点，在运行时对象中不要使用 Java 对象指针引用 commit 和 blob，而应该使用 SHA-1 哈希字符串。在 Gitlet 运行期间，可以维护一个从这些字符串到相应运行时对象的映射。程序启动后创建并填充这张映射，但永远不要把它读取自文件或写入文件。&lt;/p&gt;
&lt;p&gt;为了避免每次都查找对象所带来的麻烦和执行时间，你可能会觉得，在保存 SHA-1 字符串之外，再冗余地保存一些指向 commit 的对象指针会很方便。可以把这些指针保存在对象中，同时通过声明为 &lt;code&gt;transient&lt;/code&gt; 避免将其写入文件，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private transient MyCommitType parent1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种字段不会被序列化。对象重新读入并反序列化后，它们会被设置为默认值——对引用类型来说就是 &lt;code&gt;null&lt;/code&gt;。读取包含 transient 字段的对象后，你必须小心地把这些字段重新设置为正确值。&lt;/p&gt;
&lt;p&gt;遗憾的是，用文本编辑器查看程序生成的序列化文件，对调试没有多少帮助，因为内容使用 Java 私有序列化编码保存，会是一串难以理解的字节。&lt;/p&gt;
&lt;p&gt;因此，课程组提供了一个可能有帮助的简单调试工具：&lt;code&gt;gitlet.DumpObj&lt;/code&gt;。具体信息请查看 &lt;code&gt;gitlet/DumpObj.java&lt;/code&gt; 中的 Javadoc 注释。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;你应该完整阅读这一节；为了方便，也提供了一个&lt;a href=&quot;https://www.youtube.com/watch?v=uMYpuQuHGu0&amp;#x26;t=752s&quot;&gt;视频&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;与往常一样，测试是项目的一部分。请务必为每条命令编写自己的集成测试，覆盖规范中规定的全部功能。你也可以自由添加任何单元测试。课程组不提供单元测试，因为单元测试高度依赖你的具体实现。&lt;/p&gt;
&lt;p&gt;课程组提供了一个测试程序，让编写集成测试相对容易：&lt;code&gt;testing/tester.py&lt;/code&gt;。它会解释扩展名为 &lt;code&gt;.in&lt;/code&gt; 的测试文件。可以使用下面的命令运行全部测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望看到失败测试的更多信息，例如程序实际输出了什么，可以运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make check TESTER_FLAGS=&quot;--verbose&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果希望运行单个测试，请进入 &lt;code&gt;testing&lt;/code&gt; 子目录并运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 tester.py --verbose FILE.in ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;FILE.in ...&lt;/code&gt; 是希望检查的一个或多个具体 &lt;code&gt;.in&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;**运行这条命令时务必小心，**因为它不会重新编译你的代码。每次运行 &lt;code&gt;python&lt;/code&gt; 命令之前，都必须先通过 &lt;code&gt;make&lt;/code&gt; 编译代码。&lt;/p&gt;
&lt;p&gt;下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 tester.py --verbose --keep FILE.in
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了运行测试外，还会保留 &lt;code&gt;tester.py&lt;/code&gt; 创建的目录，这样你可以检查测试脚本检测到错误时目录中的文件。如果测试没有出错，该目录仍会保留，并包含测试结束时的最终内容。&lt;/p&gt;
&lt;p&gt;从效果上说，测试器实现了一种非常简单的&lt;strong&gt;领域特定语言（domain-specific language，DSL）&lt;/strong&gt;，它包含以下命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在测试目录中设置或删除文件；&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;java gitlet.Main&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;把 Gitlet 的输出与指定输出或描述可能输出的正则表达式进行比较；&lt;/li&gt;
&lt;li&gt;检查文件是否存在、是否不存在，以及文件内容是否正确。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 testing/tester.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不提供任何操作数，程序会输出一条消息，说明这种测试语言的使用方法。&lt;/p&gt;
&lt;p&gt;课程组在 &lt;code&gt;testing/samples&lt;/code&gt; 目录中提供了一些示例。不要把自己的测试放进该子目录中，否则你可能会混淆课程组测试与你自己的测试——而你自己的测试也可能存在 Bug！&lt;/p&gt;
&lt;p&gt;请把自己的全部 &lt;code&gt;.in&lt;/code&gt; 文件放在 &lt;code&gt;testing&lt;/code&gt; 目录下另一个名为 &lt;code&gt;student_tests&lt;/code&gt; 的文件夹中。骨架代码里这个文件夹最初为空。&lt;/p&gt;
&lt;p&gt;课程组还在 Makefile 中加入了一些内容，以适应不同人的系统设置。如果你的系统用 &lt;code&gt;python&lt;/code&gt; 而不是 &lt;code&gt;python3&lt;/code&gt; 来调用 Python 3，可以不修改 Makefile，直接使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make PYTHON=python check
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以向 &lt;code&gt;tester.py&lt;/code&gt; 传递额外参数，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make TESTER_FLAGS=&quot;--keep --verbose&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;在课程组参考实现上测试&lt;/h2&gt;
&lt;p&gt;从 2 月 28 日星期日开始，课程组提供了一种方法，让你可以使用课程组参考实现验证自己对命令的理解，也可以验证自己编写的测试！具体指南在&lt;a href=&quot;https://sp21.datastructur.es/materials/guides/staff-gitlet&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;理解集成测试&lt;/h2&gt;
&lt;p&gt;当你提交 Gitbug 或在答疑时间寻求帮助时，课程组首先会要求你提供一个失败的测试，因此学会在这个项目中编写测试至关重要。课程组投入了很多工作，让这一过程尽可能不痛苦，所以请花时间阅读本节，理解提供的测试，并学会自己编写高质量测试。&lt;/p&gt;
&lt;p&gt;集成测试的格式与 Capers 中类似。如果你不知道 Capers 集成测试——也就是 &lt;code&gt;.in&lt;/code&gt; 文件——如何工作，请先阅读 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab6/lab6&quot;&gt;Capers 说明文档&lt;/a&gt;中的相关部分。&lt;/p&gt;
&lt;p&gt;提供的测试远远谈不上全面。如果想在项目中拿到满分，你肯定需要编写自己的测试。在写测试之前，先理解整个机制如何工作。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;testing&lt;/code&gt; 目录的结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── Makefile
├── student_tests                    &amp;#x3C;==== 你的 .in 文件放在这里
├── samples                          &amp;#x3C;==== 课程组提供的示例 .in 文件
│   ├── test01-init.in               &amp;#x3C;==== 一个示例测试
│   ├── test02-basic-checkout.in
│   ├── test03-basic-log.in
│   ├── test04-prev-checkout.in
│   └── definitions.inc
├── src                              &amp;#x3C;==== 包含测试时使用的文件
│   ├── notwug.txt
│   └── wug.txt
├── runner.py                        &amp;#x3C;==== 帮助调试程序的脚本
└── tester.py                        &amp;#x3C;==== 测试程序的脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 Capers 一样，这些测试会在 &lt;code&gt;testing&lt;/code&gt; 目录中创建一个临时目录，并在其中运行 &lt;code&gt;.in&lt;/code&gt; 文件指定的命令。如果使用 &lt;code&gt;--keep&lt;/code&gt; 参数，这个临时目录会在测试结束后保留下来，供你检查。&lt;/p&gt;
&lt;p&gt;与 Capers 不同的是，这一次需要处理工作目录中文件的&lt;strong&gt;内容&lt;/strong&gt;。因此，&lt;code&gt;testing&lt;/code&gt; 文件夹中多了一个名为 &lt;code&gt;src&lt;/code&gt; 的目录。该目录保存许多预先填好内容的 &lt;code&gt;.txt&lt;/code&gt; 文件，测试会使用这些特定内容。&lt;/p&gt;
&lt;p&gt;后面会再次讨论它。现在只需要知道：&lt;code&gt;src&lt;/code&gt; 保存实际文件内容；&lt;code&gt;samples&lt;/code&gt; 保存示例测试——也就是阶段检查测试——的 &lt;code&gt;.in&lt;/code&gt; 文件。编写自己的测试时，应把它们添加到骨架中最初为空的 &lt;code&gt;student_tests&lt;/code&gt; 文件夹。&lt;/p&gt;
&lt;p&gt;Gitlet 的 &lt;code&gt;.in&lt;/code&gt; 文件拥有更多功能。下面是直接来自 &lt;code&gt;tester.py&lt;/code&gt; 的说明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# ...  A comment, producing no effect.
I FILE Include.  Replace this statement with the contents of FILE,
      interpreted relative to the directory containing the .in file.
C DIR  Create, if necessary, and switch to a subdirectory named DIR under
      the main directory for this test.  If DIR is missing, changes
      back to the default directory.  This command is principally
      intended to let you set up remote repositories.
T N    Set the timeout for gitlet commands in the rest of this test to N
      seconds.
+ NAME F
      Copy the contents of src/F into a file named NAME.
- NAME
      Delete the file named NAME.
&gt; COMMAND OPERANDS
LINE1
LINE2
...
&amp;#x3C;&amp;#x3C;&amp;#x3C;
      Run gitlet.Main with COMMAND ARGUMENTS as its parameters.  Compare
      its output with LINE1, LINE2, etc., reporting an error if there is
      &quot;sufficient&quot; discrepency.  The &amp;#x3C;&amp;#x3C;&amp;#x3C; delimiter may be followed by
      an asterisk (*), in which case, the preceding lines are treated as
      Python regular expressions and matched accordingly. The directory
      or JAR file containing the gitlet.Main program is assumed to be
      in directory DIR specifed by --progdir (default is ..).
= NAME F
      Check that the file named NAME is identical to src/F, and report an
      error if not.
* NAME
      Check that the file NAME does not exist, and report an error if it
      does.
E NAME
      Check that file or directory NAME exists, and report an error if it
      does not.
D VAR &quot;VALUE&quot;
      Defines the variable VAR to have the literal value VALUE.  VALUE is
      taken to be a raw Python string (as in r&quot;VALUE&quot;).  Substitutions are
      first applied to VALUE.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的中文解释如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;# ...&lt;/code&gt;：注释，不产生任何效果。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;I FILE&lt;/code&gt;：Include（包含）。使用 &lt;code&gt;FILE&lt;/code&gt; 的内容替换这一语句；文件路径相对于当前 &lt;code&gt;.in&lt;/code&gt; 文件所在目录解释。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;C DIR&lt;/code&gt;：必要时创建测试主目录下名为 &lt;code&gt;DIR&lt;/code&gt; 的子目录，并切换进去。如果省略 &lt;code&gt;DIR&lt;/code&gt;，则切回默认目录。主要用于设置远程仓库。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T N&lt;/code&gt;：把本测试余下部分中 Gitlet 命令的超时时间设置为 &lt;code&gt;N&lt;/code&gt; 秒。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;+ NAME F&lt;/code&gt;：把 &lt;code&gt;src/F&lt;/code&gt; 的内容复制到名为 &lt;code&gt;NAME&lt;/code&gt; 的文件中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;- NAME&lt;/code&gt;：删除名为 &lt;code&gt;NAME&lt;/code&gt; 的文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&gt; COMMAND OPERANDS ... &amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt;：以命令及参数运行 &lt;code&gt;gitlet.Main&lt;/code&gt;，然后把输出与中间的各行进行比较；如果差异足够明显则报告错误。&lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 后可以跟 &lt;code&gt;*&lt;/code&gt;，表示把前面的各行作为 Python 正则表达式进行匹配。包含 &lt;code&gt;gitlet.Main&lt;/code&gt; 的目录或 JAR 文件假定在 &lt;code&gt;--progdir&lt;/code&gt; 指定的目录中，默认为 &lt;code&gt;..&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;= NAME F&lt;/code&gt;：检查名为 &lt;code&gt;NAME&lt;/code&gt; 的文件是否与 &lt;code&gt;src/F&lt;/code&gt; 完全相同，否则报告错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;* NAME&lt;/code&gt;：检查 &lt;code&gt;NAME&lt;/code&gt; 文件不存在；如果存在则报告错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E NAME&lt;/code&gt;：检查名为 &lt;code&gt;NAME&lt;/code&gt; 的文件或目录存在；如果不存在则报告错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D VAR &quot;VALUE&quot;&lt;/code&gt;：定义变量 &lt;code&gt;VAR&lt;/code&gt;，使其字面值为 &lt;code&gt;VALUE&lt;/code&gt;。&lt;code&gt;VALUE&lt;/code&gt; 会被视为原始 Python 字符串——类似 &lt;code&gt;r&quot;VALUE&quot;&lt;/code&gt;——并且会先进行变量替换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不用担心上面提到的 Python 正则表达式。后面会说明它其实相当直接，并展示一个使用示例。&lt;/p&gt;
&lt;p&gt;下面通过一个测试，从头到尾看看会发生什么。我们分析 &lt;code&gt;test02-basic-checkout.in&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;示例测试&lt;/h3&gt;
&lt;p&gt;第一次运行这个测试时，系统会创建一个最初为空的临时目录。目录结构现在如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── Makefile
├── student_tests
├── samples
│   ├── test01-init.in
│   ├── test02-basic-checkout.in
│   ├── test03-basic-log.in
│   ├── test04-prev-checkout.in
│   └── definitions.inc
├── src
│   ├── notwug.txt
│   └── wug.txt
├── test02-basic-checkout_0          &amp;#x3C;==== 刚刚创建
├── runner.py
└── tester.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个临时目录就是本次测试执行所使用的 Gitlet 仓库，因此测试会在其中添加内容，也会在其中运行全部 Gitlet 命令。&lt;/p&gt;
&lt;p&gt;如果不删除该目录便第二次执行测试，系统会创建一个名为 &lt;code&gt;test02-basic-checkout_1&lt;/code&gt; 的新目录，依此类推。每次测试执行都使用独立目录，所以不必担心不同测试相互干扰；这种情况不会发生。&lt;/p&gt;
&lt;p&gt;测试的第一行是注释，因此忽略。&lt;/p&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; init
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令不应有任何输出，因为 &lt;code&gt;&gt;&lt;/code&gt; 开头的第一行与 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 之间没有文字。不过，正如我们所知，它应该创建 &lt;code&gt;.gitlet&lt;/code&gt; 文件夹。因此，目录结构现在是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── Makefile
├── student_tests
├── samples
│   ├── test01-init.in
│   ├── test02-basic-checkout.in
│   ├── test03-basic-log.in
│   ├── test04-prev-checkout.in
│   └── definitions.inc
├── src
│   ├── notwug.txt
│   └── wug.txt
├── test02-basic-checkout_0
│   └── .gitlet                     &amp;#x3C;==== 刚刚创建
├── runner.py
└── tester.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;+ wug.txt wug.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行使用 &lt;code&gt;+&lt;/code&gt; 命令。它会把右侧文件——来自 &lt;code&gt;src&lt;/code&gt; 目录——的内容复制到临时目录中左侧名称对应的文件中；如果该文件不存在，则创建它。&lt;/p&gt;
&lt;p&gt;这里左右两边恰好名称相同，但这不重要，因为它们位于不同目录。执行后，目录结构变为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.
├── Makefile
├── student_tests
├── samples
│   ├── test01-init.in
│   ├── test02-basic-checkout.in
│   ├── test03-basic-log.in
│   ├── test04-prev-checkout.in
│   └── definitions.inc
├── src
│   ├── notwug.txt
│   └── wug.txt
├── test02-basic-checkout_0
│   ├── .gitlet
│   └── wug.txt                     &amp;#x3C;==== 刚刚创建
├── runner.py
└── tester.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以看出 &lt;code&gt;src&lt;/code&gt; 目录的用途：它包含测试可用的文件内容，使测试能够按需要设置 Gitlet 仓库。如果希望向某个文件加入特殊内容，应把这些内容写入 &lt;code&gt;src&lt;/code&gt; 中一个名称合适的文件，然后使用与上面相同的 &lt;code&gt;+&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;参数顺序很容易弄混，所以请记住：右侧引用 &lt;code&gt;src&lt;/code&gt; 目录中的文件；左侧引用临时目录中的文件。&lt;/p&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; add wug.txt
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，它不应产生任何输出。此时，临时目录中的 &lt;code&gt;wug.txt&lt;/code&gt; 已暂存为待添加。&lt;/p&gt;
&lt;p&gt;你的 &lt;code&gt;test02-basic-checkout_0/.gitlet&lt;/code&gt; 内部目录结构很可能会发生变化，因为你必须以某种方式持久化 &lt;code&gt;wug.txt&lt;/code&gt; 已经暂存的事实。&lt;/p&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; commit &quot;added wug&quot;
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样没有输出，&lt;code&gt;.gitlet&lt;/code&gt; 内部的目录结构可能再次变化。&lt;/p&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;+ wug.txt notwug.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于临时目录中已经存在 &lt;code&gt;wug.txt&lt;/code&gt;，它的内容会被修改成 &lt;code&gt;src/notwug.txt&lt;/code&gt; 中的内容。&lt;/p&gt;
&lt;p&gt;下一部分为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; checkout -- wug.txt
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样不产生输出。不过，它应该把临时目录中 &lt;code&gt;wug.txt&lt;/code&gt; 的内容恢复为最初的内容，也就是与 &lt;code&gt;src/wug.txt&lt;/code&gt; 完全相同。下一条命令会对此进行断言：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;= wug.txt wug.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一条断言：如果左侧文件——仍然位于临时目录——与右侧文件——位于 &lt;code&gt;src&lt;/code&gt; 目录——内容不完全相同，测试脚本就会报错，指出文件内容不正确。&lt;/p&gt;
&lt;p&gt;还有另外两条可用的断言命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;E NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它断言临时目录中存在名为 &lt;code&gt;NAME&lt;/code&gt; 的文件或文件夹。它不检查内容，只检查是否存在。如果不存在，测试失败。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;* NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它断言临时目录中&lt;strong&gt;不存在&lt;/strong&gt;名为 &lt;code&gt;NAME&lt;/code&gt; 的文件或文件夹。如果存在，测试失败。&lt;/p&gt;
&lt;p&gt;这恰好是本测试的最后一行，所以测试结束。如果提供了 &lt;code&gt;--keep&lt;/code&gt; 参数，临时目录会被保留；否则它会被删除。&lt;/p&gt;
&lt;p&gt;如果你怀疑 &lt;code&gt;.gitlet&lt;/code&gt; 目录没有被正确设置，或者持久化机制存在问题，通常应保留这个目录进行检查。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;为测试准备环境&lt;/h3&gt;
&lt;p&gt;你很快会发现，测试某条命令可能需要大量重复的准备工作。例如，测试 &lt;code&gt;checkout&lt;/code&gt; 命令时，可能需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化 Gitlet 仓库。&lt;/li&gt;
&lt;li&gt;创建一个提交，其中包含某个文件的第一个版本（v1）。&lt;/li&gt;
&lt;li&gt;再创建一个提交，其中包含该文件的另一个版本（v2）。&lt;/li&gt;
&lt;li&gt;把文件检出为 v1。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果还想测试某些文件在第二个提交中未被跟踪、但在第一个提交中被跟踪的情况，准备步骤可能更多。&lt;/p&gt;
&lt;p&gt;节省时间的方法是：把所有这些准备操作放入一个文件，然后使用 &lt;code&gt;I&lt;/code&gt; 命令。假设准备内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# Initialize, add, and commit a file.
&gt; init
&amp;#x3C;&amp;#x3C;&amp;#x3C;
+ a.txt wug.txt
&gt; add a.txt
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&gt; commit &quot;a is a wug&quot;
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该把这个文件与其他测试放在 &lt;code&gt;samples&lt;/code&gt; 目录中，但扩展名使用 &lt;code&gt;.inc&lt;/code&gt;，例如命名为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;samples/commit_setup.inc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用 &lt;code&gt;.in&lt;/code&gt; 扩展名，测试脚本会把它误认为一项独立测试并尝试单独运行。&lt;/p&gt;
&lt;p&gt;在真正的测试文件中，只需要写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I commit_setup.inc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试脚本就会运行该文件中的全部命令，并继续使用它创建的临时目录。这样能让测试保持简短，更容易阅读。&lt;/p&gt;
&lt;p&gt;课程组提供了一个名为 &lt;code&gt;definitions.inc&lt;/code&gt; 的 &lt;code&gt;.inc&lt;/code&gt; 文件，其中为你设置了若干方便使用的模式。下面理解什么是模式。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;使用模式匹配输出&lt;/h3&gt;
&lt;p&gt;测试中最容易令人困惑的部分，是 &lt;code&gt;log&lt;/code&gt; 一类命令的输出。原因有几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随着你修改代码、把更多内容纳入哈希，提交 SHA 会变化，因此如果写死 SHA，就必须不断修改测试。&lt;/li&gt;
&lt;li&gt;日期每次都会变化，因为时间只会向前移动。&lt;/li&gt;
&lt;li&gt;完整写出所有内容会让测试非常长。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际上，我们并不关心精确文字是什么，只关心输出中是否存在某个 SHA，以及日期格式是否正确。因此，测试使用&lt;strong&gt;模式匹配（pattern matching）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你不需要深入理解这一概念。高层次地说，我们会为某类文本——例如提交 SHA——定义一个模式，然后只检查输出是否符合该模式，而不关心实际字母和数字是什么。&lt;/p&gt;
&lt;p&gt;下面展示怎样检查 &lt;code&gt;log&lt;/code&gt; 输出是否符合模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# First &quot;import&quot; the pattern defintions from our setup
I definitions.inc
# You would add your lines here that create commits with the
# specified messages. We&apos;ll omit this for this example.
&gt; log
===
${COMMIT_HEAD}
added wug

===
${COMMIT_HEAD}
initial commit

&amp;#x3C;&amp;#x3C;&amp;#x3C;*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这与普通 Gitlet 命令块相同，区别是结尾使用 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;*&lt;/code&gt;，告诉测试脚本使用模式。模式写成 &lt;code&gt;${PATTERN_NAME}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所有模式都定义在 &lt;code&gt;samples/definitions.inc&lt;/code&gt; 中。你不需要理解模式本身，只需要知道它匹配什么。例如，&lt;code&gt;HEADER&lt;/code&gt; 会匹配类似下面这样的提交头：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;commit fc26c386f550fc17a0d4d359d70bae33c47c54b9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只是某个随机提交 SHA。&lt;/p&gt;
&lt;p&gt;因此，在为测试创建预期输出时，只需要知道日志中有多少条记录，以及各提交消息是什么。&lt;/p&gt;
&lt;p&gt;也可以对 &lt;code&gt;status&lt;/code&gt; 命令使用类似方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I definitions.inc
# Add commands here to setup the status. We&apos;ll omit them here.
&gt; status
=== Branches ===
\*master

=== Staged Files ===
g.txt

=== Removed Files ===

=== Modifications Not Staged For Commit ===

=== Untracked Files ===
${ARBLINES}

&amp;#x3C;&amp;#x3C;&amp;#x3C;*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用的模式是 &lt;code&gt;ARBLINES&lt;/code&gt;，表示任意多行。如果你确实关心哪些文件未被跟踪，可以直接写出具体内容，不使用模式；但在这个例子中，我们可能更关心 &lt;code&gt;g.txt&lt;/code&gt; 是否被暂存为待添加。&lt;/p&gt;
&lt;p&gt;请注意 &lt;code&gt;master&lt;/code&gt; 前写的是 &lt;code&gt;\*&lt;/code&gt;。回忆一下，&lt;code&gt;status&lt;/code&gt; 命令要求在 HEAD 分支前加 &lt;code&gt;*&lt;/code&gt;。如果使用模式，就需要在预期输出中把 &lt;code&gt;*&lt;/code&gt; 写成 &lt;code&gt;\*&lt;/code&gt;。原因超出本课程范围，这叫做对星号进行“转义（escaping）”。&lt;/p&gt;
&lt;p&gt;如果不使用模式——也就是命令块以 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;&lt;/code&gt; 而不是 &lt;code&gt;&amp;#x3C;&amp;#x3C;&amp;#x3C;*&lt;/code&gt; 结束——则可以直接写 &lt;code&gt;*&lt;/code&gt;，不需要反斜杠。&lt;/p&gt;
&lt;p&gt;这些模式还能完成的最后一件事，是“保存”匹配到的一部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;警告：&lt;/strong&gt; 这可能看起来像魔法，课程组完全不在意你是否理解它如何工作。只需要知道它有效，并且可以使用。可以直接复制提供测试中的相关部分，不必从零编写。&lt;/p&gt;
&lt;p&gt;如果执行 &lt;code&gt;checkout&lt;/code&gt;，需要使用 SHA 标识符指定检出哪个提交或从哪个提交取文件。但前面使用了模式，因此创建测试时并不知道实际 SHA。这会造成问题。下面利用 &lt;code&gt;test04-prev-checkout.in&lt;/code&gt; 看看怎样“捕获”或“保存” SHA：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;I definitions.inc
# Each ${COMMIT_HEAD} captures its commit UID.
# Not shown here, but the test sets up the log by making many commits
# with specific messages.
&gt; log
===
${COMMIT_HEAD}
version 2 of wug.txt

===
${COMMIT_HEAD}
version 1 of wug.txt

===
${COMMIT_HEAD}
initial commit

&amp;#x3C;&amp;#x3C;&amp;#x3C;*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会在 &lt;code&gt;log&lt;/code&gt; 命令执行时捕获 UID（SHA）。命令运行结束后，可以使用 &lt;code&gt;D&lt;/code&gt; 命令把这些 UID 定义为变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# UID of second version
D UID2 &quot;${1}&quot;
# UID of first version
D UID1 &quot;${2}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意编号看起来是反过来的：编号从 1 开始，并从日志最上方开始。因此，当前版本——也就是第二个版本——被定义为 &lt;code&gt;${1}&lt;/code&gt;。我们不关心初始提交，所以不用捕获它的 UID。&lt;/p&gt;
&lt;p&gt;现在，可以在 checkout 中使用这个定义捕获到的 SHA：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; checkout ${UID1} -- wug.txt
&amp;#x3C;&amp;#x3C;&amp;#x3C;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着就可以添加断言，确认 checkout 是否成功。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;测试小结&lt;/h3&gt;
&lt;p&gt;测试脚本还支持许多更复杂的操作，不过上面这些已经足够让你编写非常好的测试。可以参考提供的测试开始编写，也可以在 Ed 上讨论测试某项功能的高层思路。&lt;/p&gt;
&lt;p&gt;你还可以分享自己的 &lt;code&gt;.in&lt;/code&gt; 文件，但在发布之前请确保它们正确，并添加注释，使其他学生和课程组人员能够看懂测试过程。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;调试集成测试&lt;/h2&gt;
&lt;p&gt;回忆 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab6/lab6&quot;&gt;Lab 6&lt;/a&gt; 中的内容：在这种新测试环境下，调试集成测试与以往稍有不同。&lt;code&gt;runner.py&lt;/code&gt; 脚本的工作方式与 Capers 中完全相同，因此应该阅读 Lab 6 说明文档中的相应部分，并观看那里链接的视频。&lt;/p&gt;
&lt;p&gt;下面介绍一些调试策略。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;找到真正需要调试的那一次程序执行&lt;/h3&gt;
&lt;p&gt;每个测试会多次运行你的程序，而其中每一次运行都有可能引入 Bug。首要任务，是识别真正引入 Bug 的那一次程序执行。&lt;/p&gt;
&lt;p&gt;这句话的意思是：假设你没有通过一个检查 &lt;code&gt;status&lt;/code&gt; 命令的测试。输出中只有一个文件存在差异：你的程序说它是未跟踪文件，而测试认为它应该已经暂存为待添加。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这并不意味着 &lt;code&gt;status&lt;/code&gt; 命令一定存在 Bug。&lt;/strong&gt; &lt;code&gt;status&lt;/code&gt; 当然有可能出错，但并不能保证问题就在这里。也可能是 &lt;code&gt;add&lt;/code&gt; 命令没有正确持久化“该文件已经暂存为待添加”这一事实！如果问题在 &lt;code&gt;add&lt;/code&gt; 中，那么即使 &lt;code&gt;status&lt;/code&gt; 的实现完全正确，程序仍然会失败。&lt;/p&gt;
&lt;p&gt;因此，找到正确的——也就是真正有 Bug 的——那一次执行非常重要。应该怎样做？使用 &lt;code&gt;runner.py&lt;/code&gt; 脚本逐次执行测试中的每一条程序命令，并在每次执行后查看临时目录，确认所有内容都已经正确写入文件。&lt;/p&gt;
&lt;p&gt;对于序列化对象，这会更困难，因为正如我们所知，它们的内容是一串无法直接理解的字节。对此，可以在序列化发生时检查对象是否拥有正确内容。你甚至可能发现：自己&lt;strong&gt;根本没有执行序列化&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;最终，你应该能够找到 Bug。如果仍然找不到，这时才应该前往答疑时间或提交 Gitbug。&lt;/p&gt;
&lt;p&gt;请注意：答疑时间中，每位学生最多只能获得 10 分钟帮助。如果你遇到一个很难处理的 Bug，认为助教需要超过 10 分钟才能解决，就应该改为提交 Gitbug，并提供&lt;strong&gt;尽可能多的信息&lt;/strong&gt;。Gitbug 写得越好，得到的回复就会越好、越快。&lt;/p&gt;
&lt;p&gt;不要忘记更新设计文档。课程组会拒绝处理没有最新、完整设计文档的 Gitbug。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;远程功能（额外加分）&lt;/h2&gt;
&lt;p&gt;本项目的主要目标是模仿 Git 的本地功能。这些功能很有用，因为它们让你能够备份自己的文件，并维护同一文件的多个版本。&lt;/p&gt;
&lt;p&gt;不过，Git 真正强大的地方其实在于它的&lt;strong&gt;远程功能（remote features）&lt;/strong&gt;，因为这些功能允许人们通过互联网协作。基本思想是：你和朋友可以共同开发同一个代码库。如果你修改了文件，就可以把变化发送给朋友，反之亦然。你们还可以共享由双方所有修改组成的历史。&lt;/p&gt;
&lt;p&gt;为了获得额外加分，可以实现一些基本远程命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;add-remote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rm-remote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pull&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完成这些命令可获得 64 分额外加分。&lt;/p&gt;
&lt;p&gt;在完成项目其他全部内容之前，不要尝试或规划额外加分功能。&lt;/p&gt;
&lt;p&gt;根据你在主项目中采用的设计有多灵活，这 64 分额外加分可能并不值得相应工作量。课程组当然也不期望所有人都完成它。&lt;/p&gt;
&lt;p&gt;课程组会优先帮助学生完成主项目。如果你在做额外加分，就需要比大多数学生更独立一些。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;远程命令&lt;/h2&gt;
&lt;p&gt;关于远程命令，先说明几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不考核运行时间。不过，为了自己的成长，请不要采用明显荒谬的实现。&lt;/li&gt;
&lt;li&gt;所有命令都比真实 Git 中对应命令简单得多，因此通常不会逐项列出与 Git 的具体差异，但请意识到这些差异确实存在。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面介绍各条命令。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;add-remote&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main add-remote [remote name] [name of remote directory]/.gitlet
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 使用给定的远程名称，保存给定的登录或位置信息。之后，如果使用该远程名称执行 push 或 pull，程序就会尝试使用对应的 &lt;code&gt;.gitlet&lt;/code&gt; 目录。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main add-remote other ../testing/otherdir/.gitlet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样可以编写在任何位置——无论是个人电脑还是评分程序环境——都能工作的远程仓库测试。&lt;/p&gt;
&lt;p&gt;在这些命令中始终使用正斜杠 &lt;code&gt;/&lt;/code&gt;。程序应把所有正斜杠转换为当前系统的路径分隔符：Unix 中为正斜杠，Windows 中为反斜杠。Java 很贴心地通过类变量 &lt;code&gt;java.io.File.separator&lt;/code&gt; 提供了这个字符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果给定名称的 remote 已存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A remote with that name already exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要检查用户名和服务器信息是否合法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;rm-remote&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main rm-remote [remote name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 删除与给定远程名称关联的信息。其思想是：如果以后希望修改一个已经添加的 remote，必须先删除它，再重新添加。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果给定名称的 remote 不存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A remote with that name does not exist.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;push&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main push [remote name] [remote branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 尝试把当前分支中的提交追加到给定远程仓库的给定分支末尾。具体如下。&lt;/p&gt;
&lt;p&gt;只有当远程分支的 HEAD 位于当前本地 HEAD 的历史中时，这条命令才能工作。这意味着，本地分支中存在一些位于远程分支之后的未来提交。&lt;/p&gt;
&lt;p&gt;此时，把这些未来提交追加到远程分支。然后，远程仓库应重置到被追加提交的最前端，使远程 HEAD 与本地 HEAD 相同。这个过程称为 &lt;strong&gt;fast-forwarding（快进）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果远程机器上的 Gitlet 系统存在，但没有输入的分支，则直接在远程 Gitlet 中添加该分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果远程分支的 HEAD 不在当前本地 HEAD 的历史中，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Please pull down remote changes before pushing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果远程 &lt;code&gt;.gitlet&lt;/code&gt; 目录不存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Remote directory not found.
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main fetch [remote name] [remote branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 从远程 Gitlet 仓库把提交下载到本地 Gitlet 仓库。&lt;/p&gt;
&lt;p&gt;从根本上说，它会复制远程仓库给定分支中的所有 commit 与 blob，只复制当前仓库中尚不存在的对象，并在本地 &lt;code&gt;.gitlet&lt;/code&gt; 中把它们放进一个名为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[remote name]/[remote branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;的分支中，这与真实 Git 相同。&lt;/p&gt;
&lt;p&gt;然后，让 &lt;code&gt;[remote name]/[remote branch name]&lt;/code&gt; 指向远程分支的 HEAD 提交，从而把远程分支的内容复制到当前仓库。如果本地此前不存在这个分支，就创建它。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; 如果远程 Gitlet 仓库中不存在给定分支，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;That remote does not have that branch.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果远程 &lt;code&gt;.gitlet&lt;/code&gt; 目录不存在，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Remote directory not found.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;原说明文档中的 HTML 注释指出：这里可能还需要为“remote 名称没有定义”补充失败情况，但规范正文没有正式给出对应消息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 否。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;pull&lt;/code&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;用法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java gitlet.Main pull [remote name] [remote branch name]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;描述：&lt;/strong&gt; 按照 &lt;code&gt;fetch&lt;/code&gt; 命令的行为获取分支 &lt;code&gt;[remote name]/[remote branch name]&lt;/code&gt;，然后把获取到的分支合并进当前分支。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;失败情况：&lt;/strong&gt; &lt;code&gt;fetch&lt;/code&gt; 与 &lt;code&gt;merge&lt;/code&gt; 的全部失败情况合并在一起。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;危险吗？&lt;/strong&gt; 是！&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;I. 应避免的做法&lt;/h2&gt;
&lt;p&gt;经验表明，下面这些做法会让你遭受无穷无尽的痛苦：程序无法工作、Bug 很难找到，有时甚至无法稳定复现——也就是所谓的 “Heisenbugs（海森堡 Bug）”。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;由于你可能会把各种信息——例如提交——保存在文件中，因此可能想使用看起来很方便的文件系统操作，例如列出目录内容，来依次遍历它们。&lt;/p&gt;
&lt;p&gt;请小心。&lt;code&gt;File.list&lt;/code&gt; 和 &lt;code&gt;File.listFiles&lt;/code&gt; 等方法会以&lt;strong&gt;未定义顺序&lt;/strong&gt;返回文件名。如果用这些方法实现 &lt;code&gt;log&lt;/code&gt; 命令，尤其可能得到随机结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Windows 用户尤其要注意：Unix 或 macOS 的文件分隔符是 &lt;code&gt;/&lt;/code&gt;，Windows 中则是 &lt;code&gt;\&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果程序通过手动拼接目录名、文件名以及显式的 &lt;code&gt;/&lt;/code&gt; 或 &lt;code&gt;\&lt;/code&gt; 来构造路径，那么可以确定，它至少会在其中一种系统上无法工作。&lt;/p&gt;
&lt;p&gt;Java 提供了与系统相关的文件分隔符：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.getProperty(&quot;file.separator&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用 &lt;code&gt;File&lt;/code&gt; 的多参数构造函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;序列化时要小心使用 &lt;code&gt;HashMap&lt;/code&gt;！&lt;code&gt;HashMap&lt;/code&gt; 内部元素的顺序是不确定的。解决方法是使用 &lt;code&gt;TreeMap&lt;/code&gt;，它始终保持相同顺序。更多信息见&lt;a href=&quot;https://stackoverflow.com/questions/5993752/hashmap-serialization-and-deserialization-changes&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;J. 致谢&lt;/h2&gt;
&lt;p&gt;感谢 Alicia Luengo、Josh Hug、Sarah Kim、Austin Chen、Andrew Huang、Yan Zhao、Matthew Chow，尤其是 Alan Yao、Daniel Nguyen 和 Armani Ferrante 为本项目提供反馈。也感谢 Git 如此优秀。&lt;/p&gt;
&lt;p&gt;本项目在很大程度上受到 Philip Nilsson 撰写的一篇优秀文章启发。原页面在此处使用了未能正常解析的引用标记 &lt;code&gt;[this][Nilsson Article]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;本项目由 Joseph Moghadam 创建。2015 年秋季、2017 年秋季和 2019 年秋季的修改由 Paul Hilfinger 完成。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj2/proj2&quot;&gt;https://sp21.datastructur.es/materials/proj/proj2/proj2&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 3：CS61BYoW</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-3-byow</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-3-byow</guid><description>CS61B Spring 2021 Project 3：CS61BYoW中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;引言&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#overview&quot;&gt;概述&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#skeleton-code-structure&quot;&gt;骨架代码结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#phase-1&quot;&gt;阶段 1：世界生成&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#the-default-tileset-and-tile-rendering-engine&quot;&gt;默认图块集与图块渲染引擎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#starting-your-program&quot;&gt;启动你的程序&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#phase-1-summary&quot;&gt;阶段 1 总结&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#design-document&quot;&gt;设计文档&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#design-document-guidelines&quot;&gt;设计文档指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#design-document-sections&quot;&gt;设计文档各部分&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1-classes-and-data-structures&quot;&gt;1. 类与数据结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-algorithms&quot;&gt;2. 算法&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-persistence&quot;&gt;3. 持久化&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#example&quot;&gt;示例&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#phase-2&quot;&gt;阶段 2：交互性&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#ui-user-interface-appearance&quot;&gt;UI（用户界面）外观&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ui-behavior&quot;&gt;UI 行为&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#saving-and-loading&quot;&gt;保存与加载&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#interacting-with-input-strings-and-phase-2&quot;&gt;通过输入字符串进行交互与阶段 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#ambition-score&quot;&gt;创意分数&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#270-point-primary-features&quot;&gt;270 分主要功能&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#90-point-secondary-features&quot;&gt;90 分次要功能&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#requirements-summary&quot;&gt;要求汇总&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#extra-credit&quot;&gt;额外加分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#submission&quot;&gt;提交与评分&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#checkoff-script-and-form&quot;&gt;验收脚本与表单&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#autograder-details&quot;&gt;自动评分器详情&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#phase-1-grader&quot;&gt;阶段 1 评分器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#phase-2-grader&quot;&gt;阶段 2 评分器&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#office-hours&quot;&gt;Office Hours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#faq&quot;&gt;常见问题&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;在项目 3 中，你将创建一个能够生成可探索世界的引擎。这是一个大型设计项目，需要你和一位搭档完成从构思到展示的整个开发流程。这个项目的目标，是让你学会在几乎没有起始代码的情况下管理一个规模更大的代码库，希望借此模拟类似产品开发周期的过程。&lt;/p&gt;
&lt;p&gt;因此，本项目的评分方式也会和其他项目不同。世界的设计与实现并不存在唯一的“正确答案”，所以除了一个非常宽泛的自动评分器之外，我们还会用一种类似实习或工作中绩效评估的方式来评价你。虽然这意味着评分会带有一定主观性，但我们保证会做比较友善的老板，并像任何好老板尊重认真工作的员工一样尊重你。如果你觉得评分方案不公平，请和我们沟通。&lt;/p&gt;
&lt;p&gt;这个项目需要你进行大量探索与实验。在整个过程中，经常上网搜索答案应当成为一种常规活动——但不要搜索或使用往届学期的完整解决方案。请记住，这是一个非常开放的项目，没有绝对的正确或错误答案。不过，有些实现方式和设计想法确实会比另一些更好。你很可能需要经历若干次迭代，最终才能确定一个自己满意的方案；这既是正常的，也是预期中的过程。&lt;/p&gt;
&lt;p&gt;也就是说，这个项目的核心是&lt;strong&gt;软件工程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;你不必使用课堂上讲过的任何高级数据结构或概念，例如 A*、最小生成树（MST）、并查集（Disjoint Sets）等。本项目考察的是软件工程，而不是数据结构或算法。课堂中学过的数据结构和算法可能会让你的代码显著更简单、更高效，但不要仅仅因为课程讲过，就强行把它们用进项目。只有当你确实理解并愿意在实现中使用这些工具时，再使用它们。&lt;/p&gt;
&lt;p&gt;这里有一个来自 2018 年春季学期的&lt;a href=&quot;https://www.youtube.com/playlist?list=PL8FaHk7qbOD6REWjsJd5jz9fpXO5_3ebY&amp;#x26;disable_polymer=true&quot;&gt;视频播放列表&lt;/a&gt;，其中介绍了完成本项目的一些建议。请注意，API 后来有少量变化，但其中的核心思想仍然适用。这些视频所使用的幻灯片可以在&lt;a href=&quot;https://docs.google.com/presentation/d/1XIu1L0k18A2QUlZuFYlJqjrQqSRNQp05qr1OmoBQkFE/edit?usp=sharing&quot;&gt;这里&lt;/a&gt;找到。&lt;/p&gt;
&lt;p&gt;本作业有几个关键截止时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;阶段 0——组队：&lt;strong&gt;必须在太平洋时间 3 月 31 日星期三晚上 11:59 之前提交 &lt;a href=&quot;https://forms.gle/hmobYwgEv3vCFYXs8&quot;&gt;Project 3 Partnership form&lt;/a&gt;。之后你将&lt;/strong&gt;不能&lt;/strong&gt;更换搭档。&lt;/li&gt;
&lt;li&gt;开始作业前，请先阅读并理解&lt;a href=&quot;https://sp21.datastructur.es/materials/guides/partnerships3&quot;&gt;合作指南&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;**阶段 1——世界生成：**4 月 16 日晚上 11:59 前提交到 Gradescope。&lt;/li&gt;
&lt;li&gt;**阶段 2——交互性：**4 月 27 日晚上 11:59 前提交到 Gradescope。&lt;/li&gt;
&lt;li&gt;**额外加分：**为你的世界加入有创意的机制，并在 4 月 30 日前制作一个公开的 YouTube 视频展示你的作品。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;项目阶段 2 不能使用 slip days 延迟提交，因为它会在实验课验收中由助教评分，47b 的学生也一样。额外加分部分同样不能延迟。理论上，你可以晚交阶段 1 以及配套的 Lab 12 和 Lab 13，但阶段 2 是建立在这些任务之上的，因此如果你晚交这些前置任务，基本不太可能仍然按时完成阶段 2。&lt;/p&gt;
&lt;p&gt;下面正式进入作业说明。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;接下来的四周中，你的任务是设计并实现一个基于二维图块的世界探索引擎。&lt;/p&gt;
&lt;p&gt;所谓“基于图块”，是指生成的世界由一个二维图块网格组成。所谓“世界探索引擎”，是指你的软件会创建一个世界，用户可以在这个世界里行走、探索，并与其中的对象互动。你的世界采用俯视视角。&lt;/p&gt;
&lt;p&gt;作为一个远比本项目复杂的例子，NES 游戏《Zelda II》在某些场景下就是一个基于图块的世界探索引擎，而它碰巧也是一款电子游戏：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/43bd74b00a6a-31358-zelda-ii-the-adventure-of-link-nes-screenshot-an-overhead-view.jpg&quot; alt=&quot;《Zelda II》俯视场景&quot;&gt;&lt;/p&gt;
&lt;p&gt;你构建的系统可以使用图形图块，如上图所示；也可以使用文字图块，就像&lt;a href=&quot;https://sites.google.com/site/broguegame/&quot;&gt;下面这个游戏&lt;/a&gt;一样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/d51504d10285-brogue_textbased_example.png&quot; alt=&quot;Brogue 的文字图块界面&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们会提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个图块渲染器；&lt;/li&gt;
&lt;li&gt;一小组起始图块；&lt;/li&gt;
&lt;li&gt;几个必须实现的方法的声明。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些方法会被你的世界引擎使用，也会被自动评分器调用。&lt;/p&gt;
&lt;p&gt;本项目有两个主要截止时间。到第一个截止时间时，你应当能够生成符合下文要求的随机世界。到第二个截止时间时，用户应当能够探索这个世界并与之互动。&lt;/p&gt;
&lt;p&gt;本项目最重要的目标，是让你有机会尝试管理构建大型系统时随之而来的巨大复杂度。先提醒一句：你构建的系统对普通用户来说大概不会特别好玩。对于初学者而言，三周时间远远不足以完成一款真正成熟的游戏。不过，我们仍希望你能从这个项目中获得成就感，而你生成的世界甚至可能会很漂亮。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;骨架代码结构&lt;/h3&gt;
&lt;p&gt;骨架代码中有两个你会使用的关键包：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;byow.TileEngine&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;byow.Core&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;byow.TileEngine&lt;/code&gt; 提供一些基础的渲染方法、图块的基本代码结构，其中包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TERenderer.java&lt;/code&gt;：包含与渲染有关的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TETile.java&lt;/code&gt;：用于表示世界中图块的类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tileset.java&lt;/code&gt;：课程提供的图块库。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要：不要修改 &lt;code&gt;TETile.java&lt;/code&gt; 的 &lt;code&gt;character&lt;/code&gt; 字段或 &lt;code&gt;character()&lt;/code&gt; 方法，否则可能会导致自动评分结果异常。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另一个包 &lt;code&gt;byow.Core&lt;/code&gt; 包含所有与图块本身无关的内容。我们建议你把本项目的大部分代码放在 &lt;code&gt;byow.Core&lt;/code&gt; 包中，但这不是强制要求。&lt;code&gt;byow.Core&lt;/code&gt; 包中自带以下类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RandomUtils.java&lt;/code&gt;：提供一些方便的随机数相关工具方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Main.java&lt;/code&gt;：整个系统的用户入口。它读取命令行参数，并调用 &lt;code&gt;Engine.java&lt;/code&gt; 中对应的函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Engine.java&lt;/code&gt;：包含用户与系统交互所需的两个方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;byow.Core.Engine&lt;/code&gt; 提供了两个与系统交互的方法。&lt;/p&gt;
&lt;p&gt;第一个方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public TETile[][] interactWithInputString(String input)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法接收一系列键盘输入，并在处理所有给定按键后，返回一个表示当前世界状态的二维 &lt;code&gt;TETile&lt;/code&gt; 数组。具体行为将在后文说明。&lt;/p&gt;
&lt;p&gt;第二个方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void interactWithKeyboard()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法从键盘获取输入，并在每次按键后将结果绘制到屏幕。Lab 12 会介绍如何渲染图块，Lab 13 会介绍如何获取用户输入。&lt;/p&gt;
&lt;p&gt;本项目会大量使用 &lt;code&gt;StdDraw&lt;/code&gt;。这是一个具备基本图形渲染能力的库，同时也支持键盘和鼠标点击交互。项目进行过程中，你可能需要查阅 &lt;code&gt;StdDraw&lt;/code&gt; 的 API 文档，地址在&lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/javadoc/StdDraw.html&quot;&gt;这里&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;你的项目只能使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 标准库，即从 &lt;code&gt;java.*&lt;/code&gt; 导入的内容；&lt;/li&gt;
&lt;li&gt;仓库中课程提供的库。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过，&lt;strong&gt;仅限额外加分视频&lt;/strong&gt;，你可以按需使用外部库。提交给阶段 2 自动评分器以及最终验收的版本，不能使用骨架代码以外的任何外部库。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要：除非变量带有 &lt;code&gt;final&lt;/code&gt;，否则不要使用静态变量。2018 年有很多学生因为使用非 &lt;code&gt;final&lt;/code&gt; 静态变量而遇到严重调试问题。非 &lt;code&gt;final&lt;/code&gt; 静态变量会给系统增加巨大的复杂度。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;另外，不要调用 &lt;code&gt;System.exit()&lt;/code&gt;，否则自动评分器本身也会退出并判定失败。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;阶段 1：世界生成&lt;/h3&gt;
&lt;p&gt;如上所述，本项目的第一个目标是编写一个世界生成器。你的世界必须满足以下要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;世界必须是一个二维网格，并使用课程提供的图块引擎绘制。图块引擎会在 &lt;a href=&quot;https://sp21.datastructur.es/materials/lab/lab12/lab12&quot;&gt;Lab 12&lt;/a&gt; 中介绍。&lt;/li&gt;
&lt;li&gt;世界必须使用伪随机方式生成。Lab 12 会介绍伪随机性。&lt;/li&gt;
&lt;li&gt;生成的世界必须包含彼此可区分的房间和走廊，也可以包含室外空间。&lt;/li&gt;
&lt;li&gt;至少一部分房间应当是矩形，但你也可以支持其他形状。&lt;/li&gt;
&lt;li&gt;世界生成器必须能够生成带转弯的走廊；等价地，也可以生成彼此相交的直走廊。&lt;/li&gt;
&lt;li&gt;世界中房间和走廊的数量应当是随机的。&lt;/li&gt;
&lt;li&gt;房间和走廊的位置应当是随机的。&lt;/li&gt;
&lt;li&gt;房间的宽度和高度应当是随机的。&lt;/li&gt;
&lt;li&gt;走廊宽度应为 1 或 2 个图块，长度应当随机。&lt;/li&gt;
&lt;li&gt;房间和走廊必须具有与地板在视觉上明显不同的墙壁。墙壁和地板也必须与未使用空间在视觉上明显不同。&lt;/li&gt;
&lt;li&gt;房间和走廊必须相互连通，也就是说，相邻房间或走廊的地板之间不能存在断层。&lt;/li&gt;
&lt;li&gt;所有房间都必须能够到达，不能存在完全没有入口的房间。&lt;/li&gt;
&lt;li&gt;每次生成的世界应当具有显著差异。不能总是生成同一种基本布局，仅仅改变一些容易预测的细节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面的图片展示了一个满足全部要求的世界示例。点击图片可以查看更高分辨率版本。在图中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt; 表示墙壁图块；&lt;/li&gt;
&lt;li&gt;点号表示地板图块；&lt;/li&gt;
&lt;li&gt;一段金色墙壁表示一扇上锁的门；&lt;/li&gt;
&lt;li&gt;所有未使用空间均为空白。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/cc595ade5dfd-compliant_world_example.png&quot;&gt;&lt;img src=&quot;../_assets/coursework/cc595ade5dfd-compliant_world_example.png&quot; alt=&quot;满足规范的世界示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;完成 Lab 12 后，你就可以开始编写世界生成算法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;你最后非常可能会把自己的第一版世界生成算法整个丢掉。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这很正常。在真实系统开发中，先实现几个完全不同的版本，再最终得到一个自己满意的方案，是很常见的事情。上图中的房间生成算法是作者的第三版，而且最终版本比前两个版本都简单得多。&lt;/p&gt;
&lt;p&gt;你可以上网搜索一些很酷的世界生成算法。你不能直接从现有游戏或在线图形演示中复制粘贴代码，但可以参考网络代码获得灵感。&lt;strong&gt;务必使用 &lt;code&gt;@source&lt;/code&gt; 标签标注你的参考来源。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你也可以试玩现有的二维图块游戏寻找灵感：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sites.google.com/site/broguegame/&quot;&gt;Brogue&lt;/a&gt; 是一个尤其优雅、漂亮的游戏。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.bay12games.com/dwarves/&quot;&gt;Dwarf Fortress&lt;/a&gt; 则是一个极其复杂、甚至复杂到近乎荒诞的世界生成引擎。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;默认图块集与图块渲染引擎&lt;/h4&gt;
&lt;p&gt;课程提供的图块渲染引擎接收一个二维 &lt;code&gt;TETile&lt;/code&gt; 对象数组，并将其绘制到屏幕上。暂时把这个数组称为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TETile[][] world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;world[0][0]&lt;/code&gt; 对应世界左下角的图块。&lt;/p&gt;
&lt;p&gt;第一个坐标是 x 坐标。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;world[9][0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示从左下角向右移动 9 个位置的图块。&lt;/p&gt;
&lt;p&gt;第二个坐标是 y 坐标，并且向上移动时数值增大。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;world[0][5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示从左下角向上移动 5 个位置的图块。&lt;/p&gt;
&lt;p&gt;数组中的所有值都必须非 &lt;code&gt;null&lt;/code&gt;。也就是说，在调用 &lt;code&gt;renderFrame&lt;/code&gt; 之前，务必把整个数组填满。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;请确保你真正理解世界网格的坐标方向。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你不确定，可以编写一些很短的示例程序，将简单内容绘制到网格上，以加深理解。&lt;strong&gt;如果你把 x 和 y，或者向上和向下弄反，调试过程会变得极其混乱。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们在 &lt;code&gt;Tileset.java&lt;/code&gt; 中提供了一小组默认图块，它们可以作为创建 &lt;code&gt;TETile&lt;/code&gt; 对象的良好示例。我们强烈建议你也添加自己的图块。&lt;/p&gt;
&lt;p&gt;图块引擎还支持图形图块。要使用图形图块，只需把图块图片的文件名作为 &lt;code&gt;TETile&lt;/code&gt; 构造器的第五个参数传入即可。图片必须是 16 × 16 像素，并且最好使用 PNG 格式。网上有大量适用于图块游戏的开源图块集，你可以自由使用。&lt;/p&gt;
&lt;p&gt;你创建的每个 &lt;code&gt;TETile&lt;/code&gt; 对象都应当拥有一个其他图块没有使用的唯一字符。即使你使用自己的图片来渲染图块，每个 &lt;code&gt;TETile&lt;/code&gt; 仍然必须有自己独立的字符表示。&lt;/p&gt;
&lt;p&gt;如果你没有提供文件名，或者对应文件无法打开，图块引擎会改用你提供的 Unicode 字符。这意味着，如果其他人的电脑上没有你指定位置的图片文件，你的世界仍然能够显示，只不过会使用 Unicode 字符，而不是你选择的纹理。&lt;/p&gt;
&lt;p&gt;图块渲染引擎依赖 &lt;code&gt;StdDraw&lt;/code&gt;。除非你非常清楚自己在做什么，否则不建议使用 &lt;code&gt;setXScale&lt;/code&gt;、&lt;code&gt;setYScale&lt;/code&gt; 一类的 &lt;code&gt;StdDraw&lt;/code&gt; 命令，因为这可能会大幅改变，甚至破坏系统的美——学——气——质。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;启动你的程序&lt;/h4&gt;
&lt;p&gt;程序将通过运行 &lt;code&gt;Main&lt;/code&gt; 类的 &lt;code&gt;main&lt;/code&gt; 方法启动。你会看到，这个方法会根据用户希望如何与程序交互，调用两个方法中的一个。&lt;/p&gt;
&lt;p&gt;用户可以通过命令行参数向程序描述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何生成随机世界；&lt;/li&gt;
&lt;li&gt;希望进行哪些探索操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这种情况下，&lt;code&gt;Main.main&lt;/code&gt; 会调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Core.Engine.interactWithInputString(String s)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;通过输入字符串运行 &lt;code&gt;Core.Main.main&lt;/code&gt; 时，命令行参数格式必须为：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-s inputString
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;inputString&lt;/code&gt; 就是传给 &lt;code&gt;interactWithInputString()&lt;/code&gt; 的输入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果没有提供命令行参数，则表示用户希望通过 GUI 菜单与程序交互，并用键盘探索世界。在这种情况下，&lt;code&gt;Main.main&lt;/code&gt; 会调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Core.Engine.interactWithKeyboard()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在阶段 1 中，你的项目不需要支持 &lt;code&gt;interactWithKeyboard()&lt;/code&gt;，但必须支持 &lt;code&gt;interactWithInputString()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;具体而言，你应当能够处理如下格式的输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N#######S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个 &lt;code&gt;#&lt;/code&gt; 都是一个数字；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt; 的数量可以任意多；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt; 表示请求创建一个新世界；&lt;/li&gt;
&lt;li&gt;中间的数字表示随机种子；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt; 表示种子输入已经完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们建议你在进入项目阶段 2（交互性）之前，不要实现 &lt;code&gt;Core.Engine.interactWithKeyboard()&lt;/code&gt;，不过你也可以随时提前实现。使用 &lt;code&gt;interactWithInputString&lt;/code&gt; 测试和调试世界生成器会更方便。&lt;/p&gt;
&lt;p&gt;我们也建议你在阶段 2 之前不要使用 &lt;code&gt;Core.Main.main&lt;/code&gt;。完成阶段 1 时，更简单的做法是在你最常使用的类中编写自己的 &lt;code&gt;main&lt;/code&gt; 方法，由它：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建渲染器；&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;interactWithInputString&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;绘制返回结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你编写自己的 &lt;code&gt;main&lt;/code&gt; 方法，可以忽略 &lt;code&gt;-s&lt;/code&gt; 标志，直接把输入字符串作为程序唯一的参数。&lt;/p&gt;
&lt;p&gt;要在 IntelliJ 中为 &lt;code&gt;main&lt;/code&gt; 方法提供命令行参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你已经运行过这个 &lt;code&gt;main&lt;/code&gt; 方法，请进入 &lt;strong&gt;Run → Edit Configurations&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;然后在 &lt;strong&gt;Program Arguments&lt;/strong&gt; 输入框中填写你的字符串，如下图所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/753c247a68dc-program_args.png&quot; alt=&quot;在 IntelliJ 中设置程序参数&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你还没有运行过 &lt;code&gt;main&lt;/code&gt; 方法，可以右键点击 &lt;code&gt;main&lt;/code&gt; 方法旁边的绿色箭头，然后选择 &lt;strong&gt;Modify Run Configurations&lt;/strong&gt;，进入同一个设置窗口：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/e019f853d72a-access_config.png&quot; alt=&quot;打开运行配置&quot;&gt;&lt;/p&gt;
&lt;p&gt;最后，我们建议你尽量少修改 &lt;code&gt;Core.Main&lt;/code&gt; 类。把程序中的实际工作委托给你自己创建的其他类，是更好的设计。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;Core.Engine.interactWithKeyboard()&lt;/code&gt; 时，程序必须显示一个主菜单。这个菜单至少应提供以下选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建新世界；&lt;/li&gt;
&lt;li&gt;加载以前保存的世界；&lt;/li&gt;
&lt;li&gt;退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主菜单必须能够完全通过键盘操作，并使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;：新世界（New World）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;L&lt;/code&gt;：加载世界（Load World）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Q&lt;/code&gt;：退出（Quit）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以自行添加更多选项或其他导航方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/a7824ffc91cf-mainmenu_example.png&quot; alt=&quot;主菜单示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;用户在键盘上按 &lt;code&gt;N&lt;/code&gt; 创建新世界后，程序应提示用户输入一个“随机种子”。该种子是用户选择的 &lt;code&gt;long&lt;/code&gt; 值，之后会用于随机生成世界，具体方式将在后文以及 Lab 12 中介绍。&lt;/p&gt;
&lt;p&gt;用户界面应显示用户目前已经输入的种子值。&lt;/p&gt;
&lt;p&gt;用户输入种子的最后一位数字后，应按下 &lt;code&gt;S&lt;/code&gt;，告诉系统整个种子已经输入完毕。&lt;/p&gt;
&lt;p&gt;世界生成器必须能够处理从正数开始、最大不超过以下值的任意种子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;9,223,372,036,854,775,807
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于大于这个值的种子，行为不作规定。&lt;/p&gt;
&lt;p&gt;“Load”命令的行为将在本说明后面介绍。&lt;/p&gt;
&lt;p&gt;如果系统通过 &lt;code&gt;Core.Engine.interactWithInputString()&lt;/code&gt; 启动，则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不应显示菜单；&lt;/li&gt;
&lt;li&gt;不应在屏幕上绘制任何内容；&lt;/li&gt;
&lt;li&gt;除此以外，系统应把输入字符串当作人类用户通过 &lt;code&gt;Core.Engine.interactWithKeyboard()&lt;/code&gt; 逐个按下相应按键来处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Core.Engine.interactWithInputString(&quot;N3412S&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序应使用种子 &lt;code&gt;3412&lt;/code&gt; 生成一个世界，并返回生成的二维图块数组。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;输入字符串中的字母可以是大写或小写。你的引擎必须同时接受两种形式。例如，&lt;code&gt;N&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 都应开始世界生成流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用 &lt;code&gt;interactWithInputString()&lt;/code&gt; 时，&lt;strong&gt;不应渲染任何图块，也不应播放任何声音。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你可以为用户增加额外选项，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择角色属性；&lt;/li&gt;
&lt;li&gt;指定世界生成参数；&lt;/li&gt;
&lt;li&gt;选择扮演的生物类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比如，如果你希望用户选择角色种类，可以在主菜单中增加第四个选项：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S：选择生物并创建新世界
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些额外选项可以具有你自行设计的行为。但是，&lt;code&gt;N&lt;/code&gt;、&lt;code&gt;L&lt;/code&gt; 和 &lt;code&gt;Q&lt;/code&gt; 的行为必须严格符合本说明。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;阶段 1 总结&lt;/h4&gt;
&lt;p&gt;在阶段 1 中，你应当能够向 &lt;code&gt;Main.main&lt;/code&gt; 提供输入字符串，让程序创建一个符合前文要求的世界。&lt;/p&gt;
&lt;p&gt;调试时，你应通过自己编写的 &lt;code&gt;main&lt;/code&gt; 方法渲染世界并检查结果。不过，对于自动评分器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interactWithInputString()&lt;/code&gt; 不应渲染世界；&lt;/li&gt;
&lt;li&gt;它只应返回一个 &lt;code&gt;TETile[][]&lt;/code&gt; 数组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同种子生成的世界应当在视觉上明显不同。&lt;/p&gt;
&lt;p&gt;若要在 Office Hours 或 Gitbug 中获得帮助，你还需要维护一份反映当前项目状态的设计文档。下一节将介绍设计文档。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;设计文档&lt;/h3&gt;
&lt;p&gt;由于我们没有为项目 3 提供大量骨架代码，而且项目本身非常开放，不同学生的 BYOW 实现预计会有很大差异。&lt;/p&gt;
&lt;p&gt;为了让课程工作人员能够为你提供项目支持，我们要求你维护一份反映项目当前状态的设计文档。&lt;/p&gt;
&lt;p&gt;如果你没有提供最新设计文档，我们将无法处理你的 Gitbug。若你在 Office Hours 中提出与自己实现细节有关的问题，但没有最新设计文档，我们也无法提供充分帮助。否则，远程理解你的整个设计是不现实的。&lt;/p&gt;
&lt;p&gt;因此，在提交 Gitbug 之前，请确保 &lt;code&gt;proj3&lt;/code&gt; 文件夹中的设计文档准确反映项目当前状态。&lt;/p&gt;
&lt;p&gt;请注意，这项练习的目的是帮助你，也帮助我们更好地支持你。我们强烈建议你在整个开发过程中持续更新设计文档。例如，在 Office Hours 中，有设计文档可以省去你向助教口头解释整个实现的大量时间。&lt;/p&gt;
&lt;p&gt;设计文档是软件工程师的重要工具。简而言之，设计文档描述程序的结构，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序由哪些概念模块组成；&lt;/li&gt;
&lt;li&gt;使用了哪些抽象，在面向对象程序中通常指类；&lt;/li&gt;
&lt;li&gt;每个类采用哪些实现策略，包括数据结构与算法；&lt;/li&gt;
&lt;li&gt;不同模块和抽象之间如何交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把程序设计写下来，可以帮助我们：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提前规划项目；&lt;/li&gt;
&lt;li&gt;整理思路；&lt;/li&gt;
&lt;li&gt;在编写代码前发现战略层面的设计缺陷。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，在 BYOW 这样的大型项目中，开发过程中经常需要推理多个组件之间如何相互作用，设计文档也会在此时提供帮助。&lt;/p&gt;
&lt;p&gt;开始编写任何代码之前，请按照下面的指南，为 BYOW 程序中的每项功能制定计划，并说服自己该设计是正确的。&lt;/p&gt;
&lt;p&gt;设计文档的编写是一个迭代过程。提出最初设计后，你可能会发现其中存在缺陷，这时应重新审视设计，并根据新发现更新文档。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：设计文档永远不会被评分。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不过，如果你没有方便可用且保持更新的设计文档，我们很可能无法在 Gitbug 和 Office Hours 中帮助你。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;设计文档指南&lt;/h4&gt;
&lt;p&gt;请使用下面的格式编写 BYOW 设计文档。&lt;/p&gt;
&lt;p&gt;设计文档应使用 Markdown 编写。Markdown 可以让你方便地格式化和排版文本文件。请组织好文档结构，使你自己或课程工作人员都能轻松阅读。&lt;/p&gt;
&lt;p&gt;你应当在 &lt;code&gt;proj3&lt;/code&gt; 文件夹中的 &lt;code&gt;README.md&lt;/code&gt; 文件内完成设计文档。在 IntelliJ 中打开该文件时，右侧会自动显示排版后的预览。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;设计文档各部分&lt;/h4&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;1. 类与数据结构&lt;/h5&gt;
&lt;p&gt;在这一部分列出所有类定义。对于每个类，列出它的实例变量（如果有），并简要描述每个变量及其在类中的用途。&lt;/p&gt;
&lt;p&gt;本节说明应尽可能简洁。完整解释留到后续章节。如果你发现文档太啰嗦，也可以适当缩短这一部分。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;2. 算法&lt;/h5&gt;
&lt;p&gt;这一部分用于说明你的代码如何工作。&lt;/p&gt;
&lt;p&gt;对于每个类，给出其中各个方法的高层描述。不要逐行解释代码，而应写成类似方法上方 Javadoc 注释的内容，并包含你所考虑的边界情况。&lt;/p&gt;
&lt;p&gt;课程工作人员也读过项目说明，因此不要重复或改写说明中已经写明的要求。本节应描述的是：&lt;strong&gt;你的代码如何实现说明中的要求。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你也可以在这里说明不同类如何通过各自的方法进行交互。&lt;/p&gt;
&lt;p&gt;本节长度取决于任务和设计的复杂度，但我们更偏好简单清晰的说明。以下是一些排版建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于复杂任务，例如“在随机位置添加随机尺寸的房间”，建议把任务拆分为多个部分，每个部分单独描述算法。从最简单的组件开始，逐步搭建设计。不同子任务可能属于不同类。如果某个算法调用了另一类的方法来完成子任务，请明确指出。&lt;/li&gt;
&lt;li&gt;尽量使用空白、标题或其他符号，清晰区分类名和各部分内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;3. 持久化&lt;/h5&gt;
&lt;p&gt;你应当只在完成阶段 1 后再处理这一部分。&lt;/p&gt;
&lt;p&gt;本节应描述程序如何按照说明要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存世界状态；&lt;/li&gt;
&lt;li&gt;重新加载世界状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样，请尽量保持解释清楚、简短。列出程序会接触的所有组件，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类；&lt;/li&gt;
&lt;li&gt;具体方法；&lt;/li&gt;
&lt;li&gt;可能创建的文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;示例&lt;/h4&gt;
&lt;p&gt;为了说明上述要求，我们为 Capers Lab 创建了一份&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj3/capers-example&quot;&gt;设计文档示例&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;阶段 2：交互性&lt;/h3&gt;
&lt;p&gt;在项目的第二阶段，你将加入让用户真正与世界互动的能力，同时还要为世界添加用户界面（UI）元素，让整个体验更具沉浸感，也能向用户提供更多信息。&lt;/p&gt;
&lt;p&gt;交互性要求如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用户必须能够控制某种“角色”（avatar），并使用 &lt;code&gt;W&lt;/code&gt;、&lt;code&gt;A&lt;/code&gt;、&lt;code&gt;S&lt;/code&gt;、&lt;code&gt;D&lt;/code&gt; 键让它移动。Lab 13 会介绍如何加入交互功能。&lt;/p&gt;
&lt;p&gt;这里所说的“角色”，只是指屏幕上由用户控制的某种表示。例如，在作者的项目中，角色使用一个可以四处移动的 &lt;code&gt;@&lt;/code&gt; 符号表示。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;角色必须能够以某种方式与世界互动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统必须是确定性的：对于同一个种子，只要按键序列相同，每一次运行都必须产生完全相同的行为。&lt;/p&gt;
&lt;p&gt;请注意，&lt;code&gt;Random&lt;/code&gt; 对象可以保证在相同种子下，每次生成相同的随机数序列。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为了支持保存和加载，程序需要在 &lt;code&gt;proj3&lt;/code&gt; 目录中创建一些文件。具体细节会在本说明后文和骨架代码中介绍。&lt;/p&gt;
&lt;p&gt;你创建的文件只能使用 &lt;code&gt;.txt&lt;/code&gt; 后缀，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;savefile.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不遵守这一要求，自动评分器可能会出现问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你也可以选择加入允许用户获胜或失败的游戏机制，具体请参见后文的额外加分部分。&lt;/p&gt;
&lt;p&gt;除了以上功能要求之外，系统还需要满足若干技术要求，下面会更详细地说明。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;UI（用户界面）外观&lt;/h4&gt;
&lt;p&gt;用户输入种子并按下 &lt;code&gt;S&lt;/code&gt; 后，程序应显示世界以及用户界面。&lt;/p&gt;
&lt;p&gt;项目的用户界面必须至少包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个二维图块网格，用于显示世界当前状态；&lt;/li&gt;
&lt;li&gt;一个 HUD（Heads Up Display，平视显示界面），用于提供可能对用户有帮助的额外信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HUD 最低限度必须包含一段文字，用来描述鼠标指针当前指向的图块。&lt;/p&gt;
&lt;p&gt;作为最低实现标准，下面这个简单界面显示了一个图块网格，并在 HUD 中显示鼠标指针下方图块的描述。点击图片可查看高分辨率版本：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/29b28a03dee4-UI_example0.png&quot;&gt;&lt;img src=&quot;../_assets/coursework/29b28a03dee4-UI_example0.png&quot; alt=&quot;基础 HUD 示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;你也可以自行加入其他功能。&lt;/p&gt;
&lt;p&gt;在下面的示例中，鼠标当前悬停在墙壁上，因此 HUD 右上角显示文本 &lt;code&gt;wall&lt;/code&gt;。与此同时，这个 HUD 还使用 5 颗心表示角色的“生命值”。&lt;/p&gt;
&lt;p&gt;请注意，这个示例世界并不符合前面规范中的世界要求，因为它是一个巨大、无规则的洞穴空间，而不是由走廊连接的房间。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/a8b0e6cd4507-UI_example1.png&quot;&gt;&lt;img src=&quot;../_assets/coursework/a8b0e6cd4507-UI_example1.png&quot; alt=&quot;带生命值的 HUD 示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下面这个游戏示例会在 GUI 中列出其他有效按键，并在鼠标悬停于图块上时提供更加详细的信息，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;You see grass-like fungus.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是“你看到了一种像草一样的真菌”。&lt;/p&gt;
&lt;p&gt;下图展示的是一款专业游戏，因此我们并不要求你的项目达到这种细节水平，不过我们鼓励你尝试实现一些有趣的视觉效果。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/d0397c466724-UI_example2.png&quot;&gt;&lt;img src=&quot;../_assets/coursework/d0397c466724-UI_example2.png&quot; alt=&quot;更复杂的 HUD 示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;关于如何指定 HUD 的位置，请参考 &lt;code&gt;TERenderer&lt;/code&gt; 中的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;initialize(int width, int height, int xOffset, int yOffset)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以查看 Lab 13。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;UI 行为&lt;/h4&gt;
&lt;p&gt;世界生成后，用户必须能够控制显示在世界中的某种角色。&lt;/p&gt;
&lt;p&gt;用户必须能够使用以下按键移动：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;W&lt;/code&gt;：向上；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt;：向左；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;S&lt;/code&gt;：向下；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D&lt;/code&gt;：向右。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些按键也可以同时触发其他行为，例如推动对象。&lt;/p&gt;
&lt;p&gt;你可以在引擎中加入额外按键。&lt;/p&gt;
&lt;p&gt;角色尝试走进墙壁时不应移动，程序也不能因此报错。&lt;/p&gt;
&lt;p&gt;系统必须表现为伪随机。也就是说，给定某个种子后，相同的按键序列必须产生完全相同的结果。&lt;/p&gt;
&lt;p&gt;除移动键以外，如果用户输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;:Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意包含前面的冒号，程序应当保存并退出。&lt;/p&gt;
&lt;p&gt;保存和加载行为会在下一节中说明。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;该命令必须立即保存并退出。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完成这一操作不应再要求用户按下任何按键。例如，不要在退出前询问用户是否确定。&lt;/p&gt;
&lt;p&gt;我们把“同时退出和保存”这一单一操作称为 &lt;strong&gt;quit/saving&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这个命令不区分大小写，因此：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;:q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也必须有效。&lt;/p&gt;
&lt;p&gt;此外，冒号 &lt;code&gt;:&lt;/code&gt; 后面跟着除 &lt;code&gt;Q&lt;/code&gt; 之外的其他字母时，不应产生任何效果。&lt;/p&gt;
&lt;p&gt;本项目使用 &lt;code&gt;StdDraw&lt;/code&gt; 处理用户输入。这会带来几个重要限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;StdDraw&lt;/code&gt; 不支持组合键。&lt;/p&gt;
&lt;p&gt;当我们说 &lt;code&gt;:Q&lt;/code&gt; 时，意思是先按 &lt;code&gt;:&lt;/code&gt;，再按 &lt;code&gt;Q&lt;/code&gt;，而不是同时按下两个键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;它只能识别会产生字符的按键。&lt;/p&gt;
&lt;p&gt;因此，任何 Unicode 字符都可以，但方向键、Escape 等不会产生字符的按键无法使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在某些电脑上，如果不进行较大修改，它可能不支持长按按键。例如，你不能一直按住 &lt;code&gt;E&lt;/code&gt; 来持续向东移动。&lt;/p&gt;
&lt;p&gt;如果你能找到一种同时兼容 &lt;code&gt;interactWithInputString&lt;/code&gt; 的长按实现方式，也可以自行支持。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为系统必须能够通过 &lt;code&gt;interactWithInputString&lt;/code&gt; 接收字符串输入，所以引擎不能使用真实时间。&lt;/p&gt;
&lt;p&gt;也就是说，系统不能存在任何依赖现实世界中过去多少时间的机制，因为这种时间信息无法记录在输入字符串中。这样一来，使用字符串输入和使用键盘输入就无法保证确定性。&lt;/p&gt;
&lt;p&gt;不过，记录已经经过多少个“回合”完全合理，也可能成为一个有趣的机制。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每移动一步，世界颜色逐渐变暗；&lt;/li&gt;
&lt;li&gt;用户可以按空格键等待一个回合。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以自由加入其他类似按键。&lt;/p&gt;
&lt;p&gt;如果你不知道如何开始实现用户交互，可以查看 &lt;code&gt;InputDemo.java&lt;/code&gt; 获取灵感。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;保存与加载&lt;/h4&gt;
&lt;p&gt;有时你正在探索世界，突然发现该去看 CS 61B 课程视频了。在这种情况下，保存当前进度，并在之后重新加载，就非常有用。&lt;/p&gt;
&lt;p&gt;系统必须能够：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在探索世界时保存世界状态；&lt;/li&gt;
&lt;li&gt;随后将世界加载回上次保存时的精确状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在正在运行的 Java 程序中，我们使用变量存储和读取值。&lt;/p&gt;
&lt;p&gt;请记住，程序结束后，所有变量都会离开作用域。因此，你需要通过程序创建的文件来持久化程序状态。&lt;/p&gt;
&lt;p&gt;当用户重新启动 &lt;code&gt;byow.Core.Main&lt;/code&gt; 并按下 &lt;code&gt;L&lt;/code&gt; 时，世界必须恢复到&lt;strong&gt;程序终止前完全相同的状态&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;该状态还包括随机数生成器的状态。下一节会进一步说明这一点。&lt;/p&gt;
&lt;p&gt;如果用户尝试加载，但不存在之前的存档，系统应当直接退出并关闭 UI，不能产生错误。&lt;/p&gt;
&lt;p&gt;在基础要求中，命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;:Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应当保存并完全终止程序。&lt;/p&gt;
&lt;p&gt;这意味着，包含 &lt;code&gt;:Q&lt;/code&gt; 的输入字符串在它之后不应再有任何字符。&lt;/p&gt;
&lt;p&gt;要加载一个世界，必须重新运行程序，并使用以 &lt;code&gt;L&lt;/code&gt; 开头的输入字符串。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;通过输入字符串进行交互与阶段 2&lt;/h4&gt;
&lt;p&gt;你的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Core.Engine.interactWithInputString(String s)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必须能够处理包含移动操作的输入字符串。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N543SWWWWAA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示用户：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用种子 &lt;code&gt;543&lt;/code&gt; 创建一个新世界；&lt;/li&gt;
&lt;li&gt;向上移动 4 次；&lt;/li&gt;
&lt;li&gt;向左移动 2 次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Core.Engine.interactWithInputString(&quot;N543SWWWWAA&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统应当返回一个 &lt;code&gt;TETile[][]&lt;/code&gt;，其世界状态必须和使用 &lt;code&gt;interactWithKeyboard&lt;/code&gt; 手动输入这些按键后得到的结果&lt;strong&gt;完全一致&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;由于系统对于给定种子和输入序列必须具有确定性，用户可以准确重放某次输入所发生的一切。&lt;/p&gt;
&lt;p&gt;这也便于你测试代码，并方便自动评分器验证程序。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Core.Engine.interactWithInputString(String s)&lt;/code&gt; 还必须能够处理重放字符串中的保存和加载。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N25SDDWD:Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用种子 &lt;code&gt;25&lt;/code&gt; 创建新世界；&lt;/li&gt;
&lt;li&gt;向右移动；&lt;/li&gt;
&lt;li&gt;再向右移动；&lt;/li&gt;
&lt;li&gt;向上移动；&lt;/li&gt;
&lt;li&gt;向右移动；&lt;/li&gt;
&lt;li&gt;保存并退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;该方法应返回保存时的二维 &lt;code&gt;TETile[][]&lt;/code&gt; 数组。&lt;/p&gt;
&lt;p&gt;之后，如果再次使用以下重放字符串启动引擎：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;LDDDD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统应：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;加载刚才保存的世界；&lt;/li&gt;
&lt;li&gt;向右移动 4 次；&lt;/li&gt;
&lt;li&gt;返回第 4 次移动后的二维 &lt;code&gt;TETile[][]&lt;/code&gt; 数组。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;世界在保存前后不能发生任何额外变化。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以下几种场景中，最后一次 &lt;code&gt;interactWithInputString&lt;/code&gt; 调用返回的 &lt;code&gt;TETile[][]&lt;/code&gt; 必须完全相同：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithInputString(&quot;N999SDDDWWWDDD&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithInputString(&quot;N999SDDD:Q&quot;)
interactWithInputString(&quot;LWWWDDD&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithInputString(&quot;N999SDDD:Q&quot;)
interactWithInputString(&quot;LWWW:Q&quot;)
interactWithInputString(&quot;LDDD:Q&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithInputString(&quot;N999SDDD:Q&quot;)
interactWithInputString(&quot;L:Q&quot;)
interactWithInputString(&quot;L:Q&quot;)
interactWithInputString(&quot;LWWWDDD&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原说明随后给出的含义是：如果之后使用输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;L:Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次调用 &lt;code&gt;interactWithInputString&lt;/code&gt;，系统保存并返回的世界状态，应当和前一次调用中得到的世界状态完全一致。保存和加载本身不能改变世界。&lt;/p&gt;
&lt;p&gt;你不需要处理包含多次保存的重放字符串。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N5SDD:QD:QDD:Q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不被视为有效重放字符串，因为程序应当在第一个 &lt;code&gt;:Q&lt;/code&gt; 时就已经终止。&lt;/p&gt;
&lt;p&gt;你也不需要处理无效重放字符串。&lt;/p&gt;
&lt;p&gt;可以假设自动评分器提供的每个重放字符串都会以下列形式之一开始：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N#S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;L
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;#&lt;/code&gt; 表示用户输入的种子数字。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interactWithInputString&lt;/code&gt; 的返回值不应取决于输入字符串是否以 &lt;code&gt;:Q&lt;/code&gt; 结尾。&lt;/p&gt;
&lt;p&gt;两者唯一的区别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以 &lt;code&gt;:Q&lt;/code&gt; 结尾时，方法会额外产生“保存世界状态”这一副作用；&lt;/li&gt;
&lt;li&gt;不以 &lt;code&gt;:Q&lt;/code&gt; 结尾时，不保存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;创意分数&lt;/h4&gt;
&lt;p&gt;项目总分中有 360 分取决于你自行选择实现的功能，我们把这部分称为 &lt;strong&gt;ambition score（创意分数）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;核心思想是：除了项目基础要求之外，我们希望你进一步打磨产品，并加入一些很酷的功能。&lt;/p&gt;
&lt;p&gt;下面列出的功能分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;270 分的主要功能；&lt;/li&gt;
&lt;li&gt;90 分的次要功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了获得创意分数的满分，你必须至少实现一个主要功能。&lt;/p&gt;
&lt;p&gt;创意分数这一类别最多只计 360 分。&lt;/p&gt;
&lt;p&gt;即使你实现了价值 450 分的功能，也不会因此获得额外加分。不过，如果你有时间和兴趣，仍然可以加入任意数量的功能。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;你的项目仍然必须满足前文所述的基础要求。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如，即使你允许用户用鼠标点击移动，项目仍必须支持基于键盘的移动。&lt;/p&gt;
&lt;p&gt;在部分主要功能说明下，我们提供了能够获得相应创意项目满分的 GIF，帮助消除歧义。你的实现不需要和示例完全相同。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;270 分主要功能&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;创建一个系统，使图块渲染器只显示位于角色视线范围内的图块。视线功能必须能够通过按键打开和关闭。&lt;/p&gt;
&lt;p&gt;下面的视线 GIF 是一种更复杂的实现，视线能够绕过拐角。我们也见过只在角色周围显示一个正方形亮区的版本。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/5bcb81b08cb7-line-of-sight.gif&quot;&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/5bcb81b08cb7-line-of-sight.gif&quot; alt=&quot;视线系统示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加入光源影响世界渲染效果的能力，并至少实现一个可以通过按键开关的光源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/80b341d8f449-toggle-lights.gif&quot;&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/80b341d8f449-toggle-lights.gif&quot; alt=&quot;开关灯光示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;加入使用课堂中某种搜索算法追逐角色或其他实体的实体，并提供一个开关，用于显示它们预计采用的路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/86f29a9e3c62-enemy.gif&quot;&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/86f29a9e3c62-enemy.gif&quot; alt=&quot;敌人追踪示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建“遭遇”系统：当角色与世界中的实体互动时，显示一个新的界面；遭遇结束后，将角色带回原来的界面。例如《Pokémon》中的遭遇。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/45417e6ad9f2-encounter.gif&quot;&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/45417e6ad9f2-encounter.gif&quot; alt=&quot;遭遇系统示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让用户能够“重放”最近一次存档，从创建最近一个新世界开始，以可视化方式展示之后执行的所有操作。重放最终得到的状态必须和用户加载最近存档后得到的最终状态相同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/docs-assets/cs61b/2021spring/assets/coursework/b1116d0afde9-replay.gif&quot;&gt;&lt;img src=&quot;/docs-assets/cs61b/2021spring/assets/coursework/b1116d0afde9-replay.gif&quot; alt=&quot;重放系统示例&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;让用户能够改变视角，例如第一人称、2.5D 等。&lt;/p&gt;
&lt;p&gt;我们以前从未见过有人实现这个主要功能。Nintendo 64 游戏 &lt;a href=&quot;https://www.youtube.com/watch?v=5uu2TWli-_M&quot;&gt;《Kirby 64 - The Crystal Shards》&lt;/a&gt;可以作为 2.5D 世界的示例。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h5&gt;90 分次要功能&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加入多个存档槽位。用户可以通过新的菜单选项访问它们，并使用新的键盘快捷键保存到 1 号槽位以外的位置。你仍应谨慎保留默认保存和加载行为，以满足重放字符串要求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入无需关闭并重新打开项目即可创建新世界的能力。它可以是探索过程中可使用的特殊选项；如果你把世界做成游戏，也可以在进入“游戏结束”状态时提供该功能。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加一个菜单选项，用于改变角色外观。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加一个菜单选项，让用户为角色命名，并在 HUD 中显示名字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加一个菜单选项，用于选择世界的环境或主题；也可以随机决定环境或主题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;添加一个菜单选项，将界面中的全部文字切换为其他语言。默认语言必须是英语，并且必须提供切换回英语的方式。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持在主菜单中通过鼠标点击执行所有本来可以通过按键执行的操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让引擎渲染图片，而不是 Unicode 字符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为菜单或探索界面加入很酷的音乐，并为角色与世界的互动加入音效。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在界面某处加入小地图，显示整个地图以及角色当前位置。&lt;/p&gt;
&lt;p&gt;如果你还实现了一个大于屏幕、正常情况下无法一次看到全部内容的地图，这个功能会更有趣。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入旋转世界的能力，例如把棋盘旋转 90 度，并相应调整移动按键。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 HUD 中显示真实日期和时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持用户点击任意可见方格进行移动。你需要实现某种寻路算法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持两名用户同时互动。这需要屏幕上有两个角色，它们可以移动，并且拥有彼此独立的控制方案。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持撤销一次移动，包括撤销发生在当前存档加载之前的移动。&lt;/p&gt;
&lt;p&gt;撤销移动应把世界恢复到最近一次按键之前的状态，但在重放字符串中应增加一个撤销命令，而不是从字符串中删除字符。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;要求汇总&lt;/h3&gt;
&lt;p&gt;以下是项目要求和限制的汇总。&lt;/p&gt;
&lt;p&gt;请注意，本节不能代替阅读完整说明，因为许多细节没有包含在这里。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 &lt;code&gt;interactWithKeyboard&lt;/code&gt; 时，程序必须显示一个菜单，其中包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New World（&lt;code&gt;N&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;Load（&lt;code&gt;L&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;Quit（&lt;code&gt;Q&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;菜单必须可以通过键盘操作，并且按键不区分大小写。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择 New World 后，用户应输入一个整数种子，然后按 &lt;code&gt;S&lt;/code&gt;。按下 &lt;code&gt;S&lt;/code&gt; 后，应生成并显示世界。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户输入种子时，UI 应显示目前已经输入的数字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;世界必须通过伪随机方式生成，并具有多样性。即，不同种子应生成不同世界。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有生成的世界都必须包含阶段 1 中描述的全部视觉特征。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户必须能够使用 &lt;code&gt;W&lt;/code&gt;、&lt;code&gt;A&lt;/code&gt;、&lt;code&gt;S&lt;/code&gt;、&lt;code&gt;D&lt;/code&gt; 在世界中移动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户必须能够按下 &lt;code&gt;:Q&lt;/code&gt; 退出。重新启动程序后，主菜单中的 &lt;code&gt;L&lt;/code&gt; 选项应把世界加载回退出前&lt;strong&gt;完全相同的状态&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有随机事件都必须是伪随机的。也就是说，给定同一个种子，程序行为必须确定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用户必须能够通过 &lt;code&gt;interactWithInputString&lt;/code&gt; 交互。除了获取输入和向屏幕绘制以外，其行为必须与 &lt;code&gt;interactWithKeyboard&lt;/code&gt; 相同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;interactWithInputString&lt;/code&gt; 必须返回一个 &lt;code&gt;TETile[][]&lt;/code&gt;，表示处理完输入字符串最后一个字符后世界的状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;interactWithInputString&lt;/code&gt; 必须能够像 &lt;code&gt;interactWithKeyboard&lt;/code&gt; 一样处理保存和加载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序必须使用课程提供的 &lt;code&gt;TileEngine&lt;/code&gt; 和 &lt;code&gt;StdDraw&lt;/code&gt; 显示图形。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序必须拥有 HUD，并在显示世界或图块的区域之外显示相关信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;鼠标悬停在图块上时，HUD 必须显示该图块的描述。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序不能使用真实时间。没有收到输入时，任何内容都不应自行移动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;程序必须实现创意功能分类中合计 360 分的功能，且&lt;strong&gt;至少包括一个主要功能&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;额外加分&lt;/h2&gt;
&lt;p&gt;为了获得额外加分，你应当让用户能够在世界中获胜或失败，也就是把它真正变成一个游戏。&lt;/p&gt;
&lt;p&gt;在此过程中，你还应加入 3 个“创意机制”。&lt;/p&gt;
&lt;p&gt;“机制”是指：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;游戏如何被控制；&lt;/li&gt;
&lt;li&gt;游戏结果如何计算；&lt;/li&gt;
&lt;li&gt;什么事情可以发生；&lt;/li&gt;
&lt;li&gt;什么事情不能发生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;游戏机制共同构成游戏规则，并决定可能出现的游戏状态。有趣的游戏通常拥有有趣的机制，以及能够产生大量不同游戏状态的机制交互。&lt;/p&gt;
&lt;p&gt;为了获得额外加分，我们要求至少加入 3 个“创意机制”。&lt;/p&gt;
&lt;p&gt;“创意机制”具体如何定义，由你决定。我们不会严格审查这一点，因此这部分主要依靠诚信原则。&lt;/p&gt;
&lt;p&gt;添加机制和胜负条件后，请制作一个公开的 YouTube 视频，展示你的游戏，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创意机制；&lt;/li&gt;
&lt;li&gt;获胜条件；&lt;/li&gt;
&lt;li&gt;失败条件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后通过&lt;a href=&quot;https://forms.gle/77oWsRS2GdRzUP9u9&quot;&gt;此表单&lt;/a&gt;提交视频链接。&lt;/p&gt;
&lt;p&gt;只需要一名搭档提交表单。&lt;/p&gt;
&lt;p&gt;你不必真的能够亲自通关自己的游戏。换句话说，即使游戏非常难，也没有问题。&lt;/p&gt;
&lt;p&gt;下面是一些可能的“创意机制”示例。不过，在要求的 3 个机制中，至少 2 个必须由你自己想出。&lt;/p&gt;
&lt;p&gt;这并不意味着你不能实现下面列表中的多个功能，只是其中最多只有一个可以计入你所需的 3 个创意机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;加入可以从菜单访问的背景设定，介绍游戏的世界观和故事。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为每种图块加入风味文本。当鼠标悬停在图块上时，在图块名称旁边显示。&lt;/p&gt;
&lt;p&gt;风味文本是一段简短描述，可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对图块作进一步说明；&lt;/li&gt;
&lt;li&gt;加入有趣的笑话或引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在游戏中加入有趣的彩蛋或作弊码，让它们触发一些有趣效果。例如加入&lt;a href=&quot;https://en.wikipedia.org/wiki/Konami_Code&quot;&gt;科乐美秘技&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入会在世界中游荡并破坏图块的实体。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改游戏，使世界地图由多个屏幕组成。&lt;/p&gt;
&lt;p&gt;常见做法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用楼梯前往具有不同布局的其他楼层；&lt;/li&gt;
&lt;li&gt;使用可滚动地图，当角色走到屏幕边缘时，地图随之移动。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入能够与角色互动的 NPC（Non-Player Characters，非玩家角色）。&lt;/p&gt;
&lt;p&gt;例如，可以加入 4 个追逐角色的幽灵；一旦角色被抓住，游戏结束。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在世界中加入能够传送角色的传送门。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入排行榜或高分列表。&lt;/p&gt;
&lt;p&gt;这要求游戏具有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明确目标；&lt;/li&gt;
&lt;li&gt;某种分数概念。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用户应能够输入名字，排行榜至少显示前 10 名及对应名字。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入动画。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入可以收集的物品。收集后，这些物品可以提供分数或改变角色能力。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入物品栏，用于存放角色可以使用或装备的物品。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入生命值机制，使游戏更具交互性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;提交与评分&lt;/h2&gt;
&lt;p&gt;和往常一样，本项目会在 Gradescope 上提供评分器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;请记得把搭档添加为 Gradescope 提交中的小组成员。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完成项目后，你还需要提交&lt;a href=&quot;https://forms.gle/L6WzsYFsR76UxoGY6&quot;&gt;这个表单&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;下一节会进一步介绍验收事项。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果不提交这个表单，项目验收部分将得到 0 分。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;两位搭档中只需要一人提交，但你们应当共同填写回答。&lt;/p&gt;
&lt;h3&gt;自动评分器：200 分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;阶段 1：100 分；&lt;/li&gt;
&lt;li&gt;阶段 2：100 分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;搭档互评：160 分&lt;/h3&gt;
&lt;p&gt;互评表单会在 Ed 上提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://forms.gle/SZcAi8zTX5FHEG9r5&quot;&gt;阶段 1&lt;/a&gt;：80 分；&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forms.gle/ffSJBXBEomUPLCbE6&quot;&gt;阶段 2&lt;/a&gt;：80 分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;验收演示：1240 分&lt;/h3&gt;
&lt;p&gt;通过提交&lt;a href=&quot;https://forms.gle/L6WzsYFsR76UxoGY6&quot;&gt;此表单&lt;/a&gt;完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创意功能：360 分；&lt;/li&gt;
&lt;li&gt;遵守阶段 1 和阶段 2 的基础规范：880 分。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;额外加分：32 分&lt;/h3&gt;
&lt;p&gt;你还需要&lt;strong&gt;指定一个 Commit&lt;/strong&gt;，以便课程工作人员知道应该评分哪个版本。&lt;/p&gt;
&lt;p&gt;请按照以下步骤操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;找出你希望用于演示评分的 Commit SHA。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个 SHA 必须来自你提交到 Gradescope 的同一个版本。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2021 年 4 月 23 日的补充说明指出：两者不必绝对相同，但建议使用相同 Commit，以确保它能够按照预期编译和运行。&lt;/p&gt;
&lt;p&gt;如果你为了通过自动评分器需要注释掉某些代码，而为了验收又需要取消注释，也可以使用不同 Commit。&lt;/p&gt;
&lt;p&gt;可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到 SHA。把它复制下来，保存在可靠的位置。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保该 Commit 位于截止时间之前，并从这个 Commit 运行代码，再次确认它确实是你希望评分的版本。&lt;/p&gt;
&lt;p&gt;如果 Commit 晚于截止时间，你将受到 50% 的扣分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;仔细把 SHA 粘贴进提交表单。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;不要忽略第 3 步。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果粘贴了错误的 SHA，我们会评分错误版本的代码。&lt;/p&gt;
&lt;p&gt;如果粘贴了无效 SHA，默认会评分 &lt;code&gt;origin/HEAD&lt;/code&gt; Commit，而这很可能导致迟交扣分。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;验收脚本与表单&lt;/h3&gt;
&lt;p&gt;为了尽量让验收过程保持透明，可以点击&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj3/proj3checkoff&quot;&gt;这里&lt;/a&gt;查看教师验收项目时使用的准确脚本。&lt;/p&gt;
&lt;p&gt;这一年采用异步验收。为了简化流程，每个人都需要提交一个表单，说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尝试实现了哪些创意功能；&lt;/li&gt;
&lt;li&gt;应如何使用这些功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明必须清楚而简洁，让评分者能够准确知道如何使用你的功能。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你发现功能使用方式很难解释，可以考虑简化用户使用它时需要完成的操作。&lt;/p&gt;
&lt;p&gt;如果创意功能存在任何已知特殊行为，请在表单中说明。例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;打开灯光后，需要先移动一步，灯光才会真正亮起。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同样重要的是，请再次阅读整个项目规范，并确保项目严格符合要求。由于要求很多，很容易漏掉某项规定。&lt;/p&gt;
&lt;p&gt;在提交创意功能以及使用说明前，请再次完整检查说明。&lt;/p&gt;
&lt;p&gt;准备提交前，请确保你已经：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;完成项目；&lt;/li&gt;
&lt;li&gt;再次阅读全部规范，确保没有遗漏；&lt;/li&gt;
&lt;li&gt;找到希望被评分的 Commit；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只使用 &lt;code&gt;library-sp21&lt;/code&gt; 或 &lt;code&gt;java.*&lt;/code&gt; 中的库；&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确保所有文件操作都使用与操作系统无关的路径拼接方式。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成这些检查后，就可以提交 &lt;a href=&quot;https://forms.gle/L6WzsYFsR76UxoGY6&quot;&gt;Project 3 Checkoff Form&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;最后一条“与操作系统无关的路径拼接”，意味着&lt;strong&gt;不能&lt;/strong&gt;把路径硬编码成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String pathToImage = &quot;images/image1.jpg&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为验收人员可能使用路径分隔符不同的操作系统。&lt;/p&gt;
&lt;p&gt;你应使用项目 2 Gitlet 的 &lt;code&gt;Utils.java&lt;/code&gt; 中的 &lt;code&gt;join&lt;/code&gt; 函数。&lt;/p&gt;
&lt;p&gt;为了方便起见，这里再次列出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.io.File;
import java.nio.file.Paths;

/**
 * 返回把 FIRST 和 OTHERS 拼接后得到的 File 路径，
 * 其行为类似 java.nio.file.Paths.get(String, String[])。
 */
static File join(String first, String... others) {
    return Paths.get(first, others).toFile();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;自动评分器详情&lt;/h2&gt;
&lt;p&gt;BYOW 有两个评分器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阶段 1 评分器；&lt;/li&gt;
&lt;li&gt;阶段 2 评分器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;阶段 1 评分器&lt;/h3&gt;
&lt;p&gt;截止时间：4 月 16 日晚上 11:59。&lt;/p&gt;
&lt;p&gt;分值：100 分。&lt;/p&gt;
&lt;p&gt;评分器会测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;interactWithInputString&lt;/code&gt; 是否返回一个世界；&lt;/li&gt;
&lt;li&gt;多次提供相同种子时，&lt;code&gt;interactWithInputString&lt;/code&gt; 是否重新创建相同世界；&lt;/li&gt;
&lt;li&gt;提供不同种子时，&lt;code&gt;interactWithInputString&lt;/code&gt; 是否创建不同世界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些测试中&lt;strong&gt;不会包含移动操作&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;阶段 2 评分器&lt;/h3&gt;
&lt;p&gt;截止时间：4 月 27 日晚上 11:59。&lt;/p&gt;
&lt;p&gt;分值：100 分。&lt;/p&gt;
&lt;p&gt;评分器会测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多次提供相同种子和相同移动操作时，&lt;code&gt;interactWithInputString&lt;/code&gt; 是否重新创建相同世界；&lt;/li&gt;
&lt;li&gt;提供不同种子和不同移动操作时，&lt;code&gt;interactWithInputString&lt;/code&gt; 是否创建不同世界；&lt;/li&gt;
&lt;li&gt;输入序列中包含保存和加载时，&lt;code&gt;interactWithInputString&lt;/code&gt; 是否仍能创建相同世界。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提交到自动评分器时，请记得把搭档添加为小组成员。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;Office Hours&lt;/h2&gt;
&lt;p&gt;由于本项目非常开放，课程工作人员很难像其他项目那样帮助你调试。&lt;/p&gt;
&lt;p&gt;因此，为了能够给到场学生分配足够时间，Office Hours 将采用以下流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;课程工作人员为每名学生提供的帮助最多约为 10 分钟。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设计文档必须反映程序当前状态。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;代码必须有良好文档，包括你编写的所有方法，并遵循&lt;a href=&quot;https://sp21.datastructur.es/materials/guides/style-guide.html#comments&quot;&gt;代码风格指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这样可以减少口头解释每个方法用途所花的时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果你的问题是调试问题，必须准备好：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解释正在发生什么错误；&lt;/li&gt;
&lt;li&gt;提供一个可以方便复现该 Bug 的测试或输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你只是说某个功能不能工作，却没有编写任何测试，也没有尝试使用调试器，我们不会帮助你。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;即使课程工作人员提供调试帮助，也可能只会提供高层建议，例如建议如何重新组织代码，让它更清晰、更容易调试。&lt;/p&gt;
&lt;p&gt;尝试在混乱、脆弱的代码中直接寻找 Bug，对你和助教来说都不是对时间的良好利用。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;问：我想制作一个可以探索室外或洞穴的世界，而不是一堆房间，应该怎么办？&lt;/h3&gt;
&lt;p&gt;答：没有问题。你可以使用种子为角色创建一座起始房屋，其中包含房间和走廊，并允许角色自由离开这座房屋。&lt;/p&gt;
&lt;h3&gt;问：我可以制作支持滚动或多个楼层（例如楼梯）的世界吗？&lt;/h3&gt;
&lt;p&gt;答：可以。&lt;/p&gt;
&lt;p&gt;在这种情况下，&lt;code&gt;interactWithInputString&lt;/code&gt; 应只返回输入重放字符串最后一个字符时，屏幕上可见的那一部分世界。&lt;/p&gt;
&lt;h3&gt;问：我可以让用户在创建世界前自定义角色吗？&lt;/h3&gt;
&lt;p&gt;答：可以，但你需要创建主菜单的第四个选项。&lt;/p&gt;
&lt;p&gt;项目必须准确支持本说明中描述的 API。也就是说：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;N23123S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;必须始终使用种子 &lt;code&gt;23123&lt;/code&gt; 创建新世界，不能再要求用户提供其他输入。&lt;/p&gt;
&lt;h3&gt;问：我看到两个 StdDraw 窗口，而不是一个，怎样避免？&lt;/h3&gt;
&lt;p&gt;答：确保导入的是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import edu.princeton.cs.introcs.StdDraw;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而不是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import edu.princeton.cs.algs4.StdDraw;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;问：为什么阶段 1 自动评分器提示 &lt;code&gt;Could not initialize class edu.princeton.cs.introcs.StdDraw&lt;/code&gt;？&lt;/h3&gt;
&lt;p&gt;答：你的代码中的某处，在 &lt;code&gt;interactWithInputString&lt;/code&gt; 方法执行期间尝试使用 &lt;code&gt;StdDraw&lt;/code&gt;，而这是不允许的。&lt;/p&gt;
&lt;p&gt;例如，如果调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TERenderer.initialize()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就意味着你正在使用 &lt;code&gt;StdDraw&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;interactWithInputString&lt;/code&gt; 时，不应打开任何 &lt;code&gt;StdDraw&lt;/code&gt; 窗口。&lt;/p&gt;
&lt;p&gt;我们见过一些学生的代码只会在某些种子下打开 &lt;code&gt;StdDraw&lt;/code&gt; 窗口，因此请非常仔细地检查。&lt;/p&gt;
&lt;h3&gt;问：自动评分器出现由 &lt;code&gt;Integer.parseInt&lt;/code&gt; 引起的 &lt;code&gt;NumberFormatException&lt;/code&gt;，怎么办？&lt;/h3&gt;
&lt;p&gt;答：&lt;code&gt;Random&lt;/code&gt; 类接收的是 &lt;code&gt;long&lt;/code&gt;，而我们提供的种子可能太大，无法放进 &lt;code&gt;int&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你需要使用 &lt;code&gt;Long&lt;/code&gt; 类解析种子，而不是 &lt;code&gt;Integer&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;long seed = Long.parseLong(seedString);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;问：自动评分器说我的世界彼此不够不同，但我在本地运行种子时，看到的世界明明不同，为什么？&lt;/h3&gt;
&lt;p&gt;答：检查你使用的每种图块是否都由不同字符表示。&lt;/p&gt;
&lt;p&gt;如果你创建了新图块，这一点尤其重要。&lt;/p&gt;
&lt;h3&gt;问：我的类的两个实例本应相等，但程序认为它们不相等。另外，我把这个类的实例作为 &lt;code&gt;HashMap&lt;/code&gt; 的键，却无法重新找到这个键，怎么办？&lt;/h3&gt;
&lt;p&gt;答：如果你创建了自己的类，请同时重写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;equals()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;hashCode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样才能保证相等的两个实例具有相同哈希值。&lt;/p&gt;
&lt;h3&gt;问：我正在使用 &lt;code&gt;byow/InputDemo&lt;/code&gt; 中的 &lt;code&gt;InputSource&lt;/code&gt;，但无法让 HUD 显示鼠标当前悬停的图块，怎么办？&lt;/h3&gt;
&lt;p&gt;答：&lt;code&gt;byow/InputDemo&lt;/code&gt; 只是一个演示。&lt;/p&gt;
&lt;p&gt;如果使用这种设计，你需要修改 &lt;code&gt;KeyboardSource&lt;/code&gt;，因为其中的 &lt;code&gt;getNextKey&lt;/code&gt; 方法在调用后会“阻塞”。&lt;/p&gt;
&lt;p&gt;这里的“阻塞”是指：在用户输入一个按键之前，它不会返回。&lt;/p&gt;
&lt;p&gt;因此，当用户没有移动时，该方法会一直停在循环中，什么也不做。&lt;/p&gt;
&lt;p&gt;但如果用户只移动鼠标，你就无法检查鼠标悬停在哪个图块上，因为代码仍被困在等待键盘输入的循环里。&lt;/p&gt;
&lt;p&gt;所以，如果选择这种实现方式，应修改这个循环，使它在等待按键的同时，也检查鼠标当前悬停的图块。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj3/proj3&quot;&gt;https://sp21.datastructur.es/materials/proj/proj3/proj3&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Project 3：游戏共享</title><link>https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-3-game-sharing</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/cs61b/2021spring/projects/project-3-game-sharing</guid><description>CS61B Spring 2021 Project 3：游戏共享中文学习资料。</description><content:encoded>&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;介绍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#networking&quot;&gt;网络功能&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#main&quot;&gt;&lt;code&gt;Main&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#byowserver&quot;&gt;&lt;code&gt;BYOWServer&lt;/code&gt;&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1-sendcanvasconfig&quot;&gt;1. &lt;code&gt;sendCanvasConfig&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-sendcanvas&quot;&gt;2. &lt;code&gt;sendCanvas&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-clienthaskeytyped&quot;&gt;3. &lt;code&gt;clientHasKeyTyped&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-clientnextkeytyped&quot;&gt;4. &lt;code&gt;clientNextKeyTyped&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-stopconnection&quot;&gt;5. &lt;code&gt;stopConnection&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#byowclient&quot;&gt;&lt;code&gt;BYOWClient&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#testing-your-code&quot;&gt;测试你的代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#supporting-remote-gameplay&quot;&gt;支持远程游戏&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#the-one-point-bounty&quot;&gt;一分悬赏&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#disclaimer&quot;&gt;免责声明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;作者：Boren Tsai&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们为项目 3：BYOW 新增了一项功能，使任意两名学生都可以远程游玩对方制作的游戏。&lt;/p&gt;
&lt;p&gt;为疫情期间的安全欢呼吧。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;现在有两名 61B 学生，Alice 和 Bob。Alice 和 Bob 都制作了非常棒的游戏，也都想玩对方的作品；但由于居家避疫规定，他们无法见面。&lt;/p&gt;
&lt;p&gt;Alice 非常沮丧，于是来到 Office Hours 表达自己的失望。幸运的是，助教 Arjun 接下了 Alice 的求助单，并带来了一个令人兴奋的消息：Alice 和 Bob 仍然可以玩对方的游戏！&lt;/p&gt;
&lt;p&gt;Arjun 首先让 Alice 再次从 &lt;code&gt;skeleton&lt;/code&gt; 拉取代码，然后解释说，Alice 可以使用课程提供的 &lt;code&gt;BYOWServer&lt;/code&gt; 托管游戏，而 Bob 可以使用 &lt;code&gt;BYOWClient&lt;/code&gt; 连接并游玩这个游戏。&lt;/p&gt;
&lt;p&gt;Arjun 把这种方案类比为在线游玩 Minecraft。&lt;/p&gt;
&lt;p&gt;客户端是用户实际用来玩游戏的程序。客户端负责所有本地计算，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图形；&lt;/li&gt;
&lt;li&gt;音频；&lt;/li&gt;
&lt;li&gt;UI（User Interface，用户界面）；&lt;/li&gt;
&lt;li&gt;记录用户输入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与此同时，远程服务器——也就是不在你本地电脑上的服务器——通过互联网监听并执行客户端的输入。&lt;/p&gt;
&lt;p&gt;随后，服务器把客户端正确渲染游戏状态、输出正确音频等操作所需的信息发送回客户端。&lt;/p&gt;
&lt;p&gt;更多信息可以在&lt;a href=&quot;https://technical-minecraft.fandom.com/wiki/Server/Client_Communication&quot;&gt;这里&lt;/a&gt;找到。&lt;/p&gt;
&lt;p&gt;为了让学生能够相互分享游戏，你需要在 &lt;code&gt;Engine&lt;/code&gt; 类中实现一个新方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithRemoteClient
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;网络功能&lt;/h2&gt;
&lt;p&gt;为了实现上述功能，我们在 &lt;code&gt;proj3&lt;/code&gt; 中加入了一层网络功能。&lt;/p&gt;
&lt;p&gt;核心功能已经放在 &lt;code&gt;Networking&lt;/code&gt; 文件夹中。你会在其中找到两个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BYOWClient&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BYOWServer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要：要获得这个文件夹，你需要重新从 &lt;code&gt;skeleton&lt;/code&gt; 拉取代码。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你不需要修改这两个文件中的任何一个。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如上所述，你需要在 &lt;code&gt;Engine.java&lt;/code&gt; 中实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;interactWithRemoteClient
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从方法名称可以看出，你会在这个方法中使用 &lt;code&gt;BYOWServer&lt;/code&gt; 与 &lt;code&gt;BYOWClient&lt;/code&gt; 交互。&lt;/p&gt;
&lt;p&gt;这些类已经完整实现，你不需要作任何调整。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;interactWithRemoteClient&lt;/code&gt; 接收的输入格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-p [4-digit port number]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-p [四位端口号]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Main&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;BYOW 服务器的“入口”已经存在于你的代码中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;else if (args.length == 2 &amp;#x26;&amp;#x26; args[0].equals(&quot;-p&quot;)) {
    System.out.println(&quot;Coming soon.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你需要把这条打印语句替换为支持服务器所需的代码。&lt;/p&gt;
&lt;p&gt;此外，你还需要修改 &lt;code&gt;main&lt;/code&gt; 方法的定义，使其抛出 &lt;code&gt;IOException&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下面提供我们的 &lt;code&gt;main&lt;/code&gt; 方法作为参考。代码中开头的 &lt;code&gt;+&lt;/code&gt; 用于表示需要新增的行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;+ public static void main(String[] args) throws IOException {
    if (args.length &gt; 2) {
        System.out.println(&quot;Can only have two arguments - the flag and input string&quot;);
        System.exit(0);
    } else if (args.length == 2 &amp;#x26;&amp;#x26; args[0].equals(&quot;-s&quot;)) {
        Engine engine = new Engine();
        engine.interactWithInputString(args[1]);
        System.out.println(engine.toString());
    } else if (args.length == 2 &amp;#x26;&amp;#x26; args[0].equals(&quot;-p&quot;)) {
+       Engine engine = new Engine();
+       engine.interactWithRemoteClient(args[1]);
    } else {
        Engine engine = new Engine();
        engine.interactWithKeyboard();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了把远程游戏共享加入项目，我们在下面提供了文档和进一步的实现指南。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BYOWServer&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们提供了一个 &lt;code&gt;BYOWServer&lt;/code&gt; 类，它会为你完成所有网络魔法。&lt;/p&gt;
&lt;p&gt;你不需要修改这个类，不过如果愿意，也可以自行修改。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BYOWServer&lt;/code&gt; 提供以下接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class BYOWServer {
    public BYOWServer(int port)

    public void sendCanvasConfig(int width, int height)
    public void sendCanvas()

    public boolean clientHasKeyTyped()
    public char clientNextKeyTyped()

    public void stopConnection()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;BYOWServer&lt;/code&gt; 时，必须提供一个 &lt;code&gt;port&lt;/code&gt; 端口号。&lt;/p&gt;
&lt;p&gt;端口号可以是你自行选择的任意整数。我们建议选择至少四位的端口号，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个数字完全由你自行决定。&lt;/p&gt;
&lt;p&gt;如果以后学习网络课程，例如 CS 168，你会了解端口的具体含义。&lt;/p&gt;
&lt;p&gt;实例化 &lt;code&gt;BYOWServer&lt;/code&gt; 时，它会输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server started. Waiting for client to connect…
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，构造器会一直等待，直到客户端建立连接。&lt;/p&gt;
&lt;p&gt;客户端成功连接后，构造器会输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client connected!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了与远程玩家通信，我们提供了以下方法。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;1. &lt;code&gt;sendCanvasConfig&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第一个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;sendCanvasConfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用于告诉客户端应创建多大的 &lt;code&gt;StdDraw&lt;/code&gt; 窗口。&lt;/p&gt;
&lt;p&gt;参数单位是像素，并且应与你在 &lt;code&gt;TERenderer&lt;/code&gt; 类第 35 行调用 &lt;code&gt;setCanvasSize&lt;/code&gt; 时提供的参数一致。&lt;/p&gt;
&lt;p&gt;每当调整画布大小时，应当&lt;strong&gt;恰好调用一次&lt;/strong&gt;这个函数。&lt;/p&gt;
&lt;p&gt;供参考，默认的 &lt;code&gt;StdDraw&lt;/code&gt; 画布大小为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;512 × 512 pixels
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;2. &lt;code&gt;sendCanvas&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第二个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;sendCanvas
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会把托管游戏的电脑上 &lt;code&gt;StdDraw&lt;/code&gt; 窗口中的内容发送给客户端。&lt;/p&gt;
&lt;p&gt;每次更新屏幕时都应调用它，例如在以下情况之后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StdDraw.showCanvas()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TERenderer.renderFrame(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;3. &lt;code&gt;clientHasKeyTyped&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第三个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;clientHasKeyTyped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 &lt;code&gt;StdDraw&lt;/code&gt; 中的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;hasNextKeyTyped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用类似。&lt;/p&gt;
&lt;p&gt;你应使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;clientHasKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代替：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StdDraw.hasNextKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;4. &lt;code&gt;clientNextKeyTyped&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第四个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;clientNextKeyTyped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与 &lt;code&gt;StdDraw&lt;/code&gt; 中的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;nextKeyTyped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用类似。&lt;/p&gt;
&lt;p&gt;你应使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;clientNextKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代替：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StdDraw.nextKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h4&gt;5. &lt;code&gt;stopConnection&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第五个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;stopConnection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用于通知客户端停止显示 &lt;code&gt;StdDraw&lt;/code&gt; 画布，然后终止连接。&lt;/p&gt;
&lt;p&gt;我们建议你在游戏退出前调用这个方法。&lt;/p&gt;
&lt;p&gt;我们建议创建一个行为与 &lt;code&gt;interactWithKeyboard&lt;/code&gt; 非常相似的 &lt;code&gt;interactWithRemoteClient&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p&gt;如果设计合理，可以避免代码重复。不过，你可能会发现，让代码在正确时机调用 &lt;code&gt;sendCanvas&lt;/code&gt; 会稍微有些麻烦。&lt;/p&gt;
&lt;p&gt;供参考，Josh Hug 在一年没有看过自己的 BYOW 客户端代码后，为其加入服务器功能大约花了 30 分钟。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BYOWClient&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;我们还提供了一个 &lt;code&gt;BYOWClient&lt;/code&gt; 类。&lt;/p&gt;
&lt;p&gt;这个类已经完整实现，可以直接运行。&lt;/p&gt;
&lt;p&gt;运行它的 &lt;code&gt;main&lt;/code&gt; 方法后，程序会提示你输入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IP address&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果想在本机测试，请在 &lt;code&gt;IP address&lt;/code&gt; 中输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在程序询问 &lt;code&gt;port&lt;/code&gt; 时，输入实例化 &lt;code&gt;BYOWServer&lt;/code&gt; 时使用的相同端口号。&lt;/p&gt;
&lt;p&gt;例如，如果你采用了我们的建议，端口号就是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BYOW Client. Please Enter the following information to connect to a server...
IP Address: localhost
Port (this must be a number): 4005
CONFIGURING CANVAS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;测试你的代码&lt;/h3&gt;
&lt;p&gt;完成 &lt;code&gt;interactWithRemoteClient&lt;/code&gt; 后，先使用以下命令行参数运行 &lt;code&gt;Main&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-p 4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用你自己选择的其他端口号。&lt;/p&gt;
&lt;p&gt;随后运行 &lt;code&gt;BYOWClient&lt;/code&gt;，并输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及相同的端口号。&lt;/p&gt;
&lt;p&gt;你应当看到两个 &lt;code&gt;StdDraw&lt;/code&gt; 窗口打开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个代表服务器；&lt;/li&gt;
&lt;li&gt;一个代表客户端。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果实现正确，这两个窗口应当显示完全相同的内容。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;支持远程游戏&lt;/h3&gt;
&lt;p&gt;当你成功让服务器端和客户端的两个 &lt;code&gt;StdDraw&lt;/code&gt; 窗口彼此同步后，就可以开始支持真正的远程游戏了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果你还没有实现 &lt;code&gt;interactWithRemoteClient&lt;/code&gt;，请立刻返回前面的步骤，直到完成它。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;正确实现 &lt;code&gt;interactWithRemoteClient&lt;/code&gt; 后，你就可以开始支持远程游戏。&lt;/p&gt;
&lt;p&gt;首先，你还需要下载一个非常酷的软件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ngrok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请在&lt;a href=&quot;https://ngrok.com&quot;&gt;这里&lt;/a&gt;注册账户并下载它。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;ngrok&lt;/code&gt; 可执行文件添加到 &lt;code&gt;PATH&lt;/code&gt; 环境变量中。&lt;/p&gt;
&lt;p&gt;原说明给出的终端命令是：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PATH=$PATH:[path to ngrok executable]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，如果可执行文件位于 &lt;code&gt;Downloads&lt;/code&gt; 文件夹中，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PATH=$PATH:~/Downloads/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，假设：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经把 &lt;code&gt;ngrok&lt;/code&gt; 可执行文件加入 &lt;code&gt;PATH&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;服务器使用端口 &lt;code&gt;4005&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;打开一个新终端并运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ngrok tcp 4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令会把服务器正在使用的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;localhost:4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;tcp&lt;/code&gt; 协议暴露到整个互联网，使其他人能够连接。&lt;/p&gt;
&lt;p&gt;运行后，终端应类似下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../_assets/coursework/875d5efe0663-ngrok.png&quot; alt=&quot;ngrok TCP 隧道示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;在显示 &lt;code&gt;Forwarding&lt;/code&gt; 的一行中，可以看到类似内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tcp://2.tcp.ngrok.io:17993 -&gt; localhost:4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际效果是把本机的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;localhost:4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;暴露到互联网。&lt;/p&gt;
&lt;p&gt;只要 ngrok 会话保持运行，发送到以下地址的全部信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tcp://2.tcp.ngrok.io:17993
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;都会被转发到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;localhost:4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，ngrok 隧道的信息为：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IP address: 2.tcp.ngrok.io
port: 17993
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：填写 &lt;code&gt;IP address&lt;/code&gt; 时，不需要包含 &lt;code&gt;tcp://&lt;/code&gt; 前缀。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面看看如何使用 &lt;code&gt;ngrok&lt;/code&gt;，让互联网上的其他人连接到你的 BYOW 服务器。&lt;/p&gt;
&lt;p&gt;首先，使用以下程序参数运行 &lt;code&gt;Main&lt;/code&gt; 类的 &lt;code&gt;main&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-p 4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后，在一个新终端中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ngrok tcp 4005
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 ngrok 创建的隧道的以下信息发送给朋友：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IP address&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;朋友运行 &lt;code&gt;BYOWClient&lt;/code&gt; 后，把你发送的信息输入客户端。&lt;/p&gt;
&lt;p&gt;建立连接之后——完成！&lt;/p&gt;
&lt;p&gt;你的朋友现在应该能够远程游玩你的游戏，而你则可以在服务器端观看整个过程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：这也带来了一些很有趣的可能性，例如允许服务器在玩家游玩过程中修改世界。尽情发挥吧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;一分悬赏&lt;/h3&gt;
&lt;p&gt;原则上，可以制作一个基于网页的客户端。&lt;/p&gt;
&lt;p&gt;我们目前还没有实现它。&lt;/p&gt;
&lt;p&gt;不过，如果有人创建了一个能够连接 BYOW 服务器的网站，我们将奖励你：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 bonus point
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是 &lt;strong&gt;1 分额外加分&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;未来，我们还很可能会把它作为正式课程资源使用。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;免责声明&lt;/h2&gt;
&lt;p&gt;由于保存 &lt;code&gt;StdDraw&lt;/code&gt; 画布会产生额外开销，同时网络自身也存在限制，在使用远程功能时，你很可能会遇到延迟。&lt;/p&gt;
&lt;p&gt;这是完全正常且符合预期的行为。&lt;/p&gt;
&lt;p&gt;希望你能享受游玩彼此制作的游戏。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;原始页面：&lt;a href=&quot;https://sp21.datastructur.es/materials/proj/proj3/proj3GameSharing&quot;&gt;https://sp21.datastructur.es/materials/proj/proj3/proj3GameSharing&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>渲染功能测试</title><link>https://blog.everlasting.xin/docs/render-demo</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/render-demo</guid><description>Everlasting Pages Markdown、MDX、Astro Pure 组件与高级渲染功能完整测试页。</description><content:encoded>&lt;p&gt;import { Image } from &apos;astro:assets&apos;&lt;/p&gt;
&lt;p&gt;import {
Aside,
Button,
Card,
CardList,
Collapse,
FormattedDate,
Icon,
Label,
MdxRepl,
Spoiler,
Steps,
Svg,
TabItem,
Tabs,
Timeline
} from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;import {
GithubCard,
LinkPreview,
QRCode,
Quote
} from &apos;astro-pure/advanced&apos;&lt;/p&gt;
&lt;p&gt;import { Icons as allIcons } from &apos;astro-pure/libs&apos;&lt;/p&gt;
&lt;p&gt;import avatar from &apos;@/assets/avatar.jpg&apos;&lt;/p&gt;
&lt;p&gt;这是一个用于测试 &lt;strong&gt;Everlasting Pages&lt;/strong&gt; 内容渲染能力的页面。&lt;/p&gt;
&lt;p&gt;如果以后升级 Astro、Astro Pure、Shiki、KaTeX，或者修改全局 CSS，可以重新打开本页，快速检查有没有出现渲染回归。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. 基础 Markdown&lt;/h2&gt;
&lt;h3&gt;1.1 文本样式&lt;/h3&gt;
&lt;p&gt;普通文本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;粗体文本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;斜体文本&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;粗斜体文本&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;~~删除线文本~~&lt;/p&gt;
&lt;p&gt;这是 &lt;code&gt;inline code&lt;/code&gt; 行内代码。&lt;/p&gt;
&lt;p&gt;这是一个 &lt;a href=&quot;/docs/&quot;&gt;内部链接&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这是一个 &lt;a href=&quot;https://astro.build/&quot;&gt;外部链接&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1.2 引用&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这是一段普通 Markdown 引用。&lt;/p&gt;
&lt;p&gt;引用中可以包含 &lt;strong&gt;粗体&lt;/strong&gt;、&lt;code&gt;代码&lt;/code&gt; 和其他 Markdown 内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;1.3 无序列表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Java&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;C++
&lt;ul&gt;
&lt;li&gt;STL&lt;/li&gt;
&lt;li&gt;Templates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1.4 有序列表&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建项目&lt;/li&gt;
&lt;li&gt;编写代码&lt;/li&gt;
&lt;li&gt;编译项目&lt;/li&gt;
&lt;li&gt;运行测试&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;1.5 嵌套列表&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;CS61B
&lt;ul&gt;
&lt;li&gt;Chapters
&lt;ul&gt;
&lt;li&gt;Java&lt;/li&gt;
&lt;li&gt;Lists&lt;/li&gt;
&lt;li&gt;Trees&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Labs&lt;/li&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;1.6 表格&lt;/h3&gt;
&lt;p&gt;| 功能 | 状态 | 说明 |
| --- | --- | --- |
| Markdown | ✅ | 基础内容 |
| MDX | ✅ | Astro 组件 |
| KaTeX | ✅ | 数学公式 |
| Shiki | ✅ | 代码高亮 |
| Waline | ✅ | 评论系统 |&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. 标题、锚点与 TOC&lt;/h2&gt;
&lt;p&gt;这一部分用于测试右侧文章目录、点击跳转、Heading ID、标题锚点以及滚动高亮。&lt;/p&gt;
&lt;h3&gt;二级测试标题 A&lt;/h3&gt;
&lt;p&gt;这里是 A 的内容。&lt;/p&gt;
&lt;h3&gt;二级测试标题 B&lt;/h3&gt;
&lt;p&gt;这里是 B 的内容。&lt;/p&gt;
&lt;h4&gt;四级测试标题&lt;/h4&gt;
&lt;p&gt;这里是更深一级的标题。&lt;/p&gt;
&lt;h3&gt;中文标题测试&lt;/h3&gt;
&lt;p&gt;中文标题也应该正确生成 slug，并支持 TOC 跳转。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;3. 数学公式 KaTeX&lt;/h2&gt;
&lt;h3&gt;3.1 行内公式&lt;/h3&gt;
&lt;p&gt;二分查找的时间复杂度为 $O(\log n)$。&lt;/p&gt;
&lt;p&gt;归并排序的时间复杂度为 $O(n\log n)$。&lt;/p&gt;
&lt;p&gt;欧拉公式：&lt;/p&gt;
&lt;p&gt;$e^{i\pi}+1=0$&lt;/p&gt;
&lt;h3&gt;3.2 块级公式&lt;/h3&gt;
&lt;p&gt;$$
\sum_{i=1}^{n} i = \frac{n(n+1)}{2}
$$&lt;/p&gt;
&lt;p&gt;$$
T(n)=2T\left(\frac{n}{2}\right)+O(n)
$$&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
f(x) &amp;#x26;= x^2 + 2x + 1 \
&amp;#x26;= (x+1)^2
\end{aligned}
$$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. Shiki 代码渲染&lt;/h2&gt;
&lt;h3&gt;4.1 基础代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello World!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.2 文件标题&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class HelloWorld {
    public static void main(String[] args) {
        System.out.println(&quot;Hello World!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.3 Diff&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class User {
    private String username;
    private String password; // [!code --]
    private String passwordHash; // [!code ++]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.4 单行高亮&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static int binarySearch(int[] a, int target) {
    int left = 0;
    int right = a.length - 1;

    while (left &amp;#x3C;= right) {
        int mid = left + (right - left) / 2; // [!code highlight]

        if (a[mid] == target) {
            return mid;
        }

        if (a[mid] &amp;#x3C; target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.5 多行高亮&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Example {
    public static void main(String[] args) {
        int x = 10; // [!code highlight:3]
        int y = 20;
        int sum = x + y;

        System.out.println(sum);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;4.6 长代码自动折叠&lt;/h3&gt;
&lt;p&gt;下面故意超过 15 行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class LongExample {
    private int value;

    public LongExample(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public boolean isPositive() {
        return value &gt; 0;
    }

    public boolean isNegative() {
        return value &amp;#x3C; 0;
    }

    public boolean isZero() {
        return value == 0;
    }

    public void reset() {
        this.value = 0;
    }

    public void increment() {
        this.value++;
    }

    public void decrement() {
        this.value--;
    }

    @Override
    public String toString() {
        return &quot;LongExample{&quot; +
                &quot;value=&quot; + value +
                &apos;}&apos;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;5. Card&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;Card
as=&apos;a&apos;
href=&apos;/projects/&apos;
heading=&apos;Everlasting Pages&apos;
subheading=&apos;Card 组件测试&apos;
date=&apos;2026&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里可以放普通文字、列表或者其他内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Astro&lt;/li&gt;
&lt;li&gt;Markdown&lt;/li&gt;
&lt;li&gt;MDX&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;6. Collapse&lt;/h2&gt;
&lt;p&gt;这是隐藏内容。&lt;/p&gt;
&lt;p&gt;可以在这里放：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;说明&lt;/li&gt;
&lt;li&gt;答案&lt;/li&gt;
&lt;li&gt;长代码&lt;/li&gt;
&lt;li&gt;补充资料&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(&quot;Collapse works!&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真正可折叠的内容在这里。
&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;7. Aside&lt;/h2&gt;
&lt;h3&gt;Note&lt;/h3&gt;
&lt;h3&gt;Tip&lt;/h3&gt;
&lt;h3&gt;Caution&lt;/h3&gt;
&lt;h3&gt;Danger&lt;/h3&gt;
&lt;h3&gt;Aside 中嵌套代码&lt;/h3&gt;
&lt;p&gt;Aside 里面可以继续写 Markdown。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git status
git add .
git commit -m &quot;test&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以写 &lt;strong&gt;粗体文本&lt;/strong&gt; 和 &lt;code&gt;inline code&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;8. Tabs&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;java --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 使用 PowerShell。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux 使用 Bash。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;java --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;macOS 也可以使用 Shell。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;8.1 同步 Tabs&lt;/h3&gt;
&lt;p&gt;下面两个 Tabs 使用相同的 &lt;code&gt;syncKey&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;切换其中一个后，另一个应该同步。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;9. MDX Repl&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;&amp;#x3C;MdxRepl width=&apos;80%&apos;&gt;
  &amp;#x3C;Button
    href=&apos;/&apos;
    title=&apos;这是 MdxRepl 中的 Button&apos;
    variant=&apos;ahead&apos;
  /&gt;
&amp;#x3C;/MdxRepl&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;10. CardList&lt;/h2&gt;
&lt;h3&gt;10.1 普通 CardList&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;CardList
title=&apos;CS 学习路线&apos;
list={[
{
title: &apos;Programming&apos;,
children: [
{
title: &apos;CS61A&apos;,
link: &apos;https://cs61a.org/&apos;
},
{
title: &apos;CS61B&apos;,
link: &apos;https://cs61b.org/&apos;
}
]
},
{
title: &apos;Systems&apos;,
children: [
{
title: &apos;CSAPP&apos;,
link: &apos;https://csapp.cs.cmu.edu/&apos;
}
]
}
]}
/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;10.2 可折叠 CardList&lt;/h3&gt;
&lt;p&gt;&amp;#x3C;CardList
title=&apos;点击展开列表&apos;
collapse
list={[
{
title: &apos;第一项&apos;
},
{
title: &apos;第二项&apos;
},
{
title: &apos;第三项&apos;
}
]}
/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;11. Timeline&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;Timeline
events={[
{
date: &apos;2025&apos;,
content: &apos;开始学习计算机科学&apos;
},
{
date: &apos;2026.01&apos;,
content: &apos;学习数据结构与算法&apos;
},
{
date: &apos;2026.08&apos;,
content: &apos;整理技术文档与个人博客&apos;
},
{
date: &apos;Future&apos;,
content: &apos;Keep building.&apos;
}
]}
/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;12. Steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h3&gt;创建项目&lt;/h3&gt;
&lt;p&gt;创建 Astro 项目并安装依赖。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;编写内容&lt;/h3&gt;
&lt;p&gt;使用 Markdown 或 MDX 编写文章。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;本地测试&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;构建项目&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;部署&lt;/h3&gt;
&lt;p&gt;Push 到 GitHub 后自动部署。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;13. Button&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;Button
as=&apos;div&apos;
variant=&apos;pill&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;自定义内容
&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;14. Spoiler&lt;/h2&gt;
&lt;p&gt;鼠标移动到下面内容：&lt;/p&gt;
&lt;p&gt;也可以隐藏一句话：&lt;/p&gt;
&lt;p&gt;这是一个 隐藏词语 测试。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;15. Label&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;Label
as=&apos;a&apos;
href=&apos;/rss.xml&apos;
title=&apos;RSS&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;16. SVG Loader&lt;/h2&gt;
&lt;p&gt;下面直接加载：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/assets/icons/key.svg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&amp;#x3C;Svg
src={import(&apos;@/assets/icons/key.svg?raw&apos;)}
class=&apos;size-12&apos;
aria-label=&apos;Key icon&apos;
/&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;17. Icon&lt;/h2&gt;
&lt;p&gt;单个图标测试：&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;17.1 Astro Pure 全部 Icon&lt;/h3&gt;
&lt;p&gt;点击按钮应该复制对应 Icon 名称，并弹出 Toast。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return (
  &amp;#x3C;Button
    as=&apos;button&apos;
    type=&apos;button&apos;
    class=&apos;cursor-pointer&apos;
    onclick={script}
  &gt;
    &amp;#x3C;Icon slot=&apos;before&apos; name={icon} /&gt;
    &amp;#x3C;span&gt;{icon}&amp;#x3C;/span&gt;
  &amp;#x3C;/Button&gt;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;})
}&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;18. Quote&lt;/h2&gt;
&lt;p&gt;下面会调用 &lt;code&gt;site.config.ts&lt;/code&gt; 中配置的一言 API。&lt;/p&gt;
&lt;p&gt;刷新页面后，理论上应该重新请求内容。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;19. GitHub Card&lt;/h2&gt;
&lt;p&gt;正常情况下会自动获取：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GitHub 头像&lt;/li&gt;
&lt;li&gt;Description&lt;/li&gt;
&lt;li&gt;Stars&lt;/li&gt;
&lt;li&gt;Forks&lt;/li&gt;
&lt;li&gt;License&lt;/li&gt;
&lt;li&gt;主要语言&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;20. Link Preview&lt;/h2&gt;
&lt;h3&gt;20.1 GitHub&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;20.2 Astro&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;21. QRCode&lt;/h2&gt;
&lt;p&gt;下面二维码固定指向博客首页：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;22. Toast&lt;/h2&gt;
&lt;p&gt;点击下面按钮。&lt;/p&gt;
&lt;p&gt;&amp;#x3C;Button
as=&apos;button&apos;
type=&apos;button&apos;
title=&apos;弹出 Toast&apos;
class=&apos;cursor-pointer&apos;
onclick={&lt;code&gt;document.dispatchEvent(     new CustomEvent(&apos;toast&apos;, {       detail: {         message: &apos;Toast 渲染正常！&apos;,         time: 5000       }     })   )&lt;/code&gt;}
/&gt;&lt;/p&gt;
&lt;p&gt;如果工作正常，页面上应该出现 Toast。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;23. Medium Zoom&lt;/h2&gt;
&lt;p&gt;点击下面图片测试图片放大。&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;24. 普通图片&lt;/h2&gt;
&lt;p&gt;同一张图片，不添加 &lt;code&gt;zoomable&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;25. 原生 Details&lt;/h2&gt;
&lt;p&gt;这里不依赖额外 JavaScript。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(&quot;Native details&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;26. 超长文本与 URL&lt;/h2&gt;
&lt;p&gt;用于检查 overflow。&lt;/p&gt;
&lt;p&gt;https://example.com/this/is/a/very/very/very/very/very/very/very/very/very/very/long/path/that/should/not/break/the/layout&lt;/p&gt;
&lt;p&gt;行内代码：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;27. HTML 元素&lt;/h2&gt;
&lt;h3&gt;Keyboard&lt;/h3&gt;
&lt;p&gt;按下 Ctrl + C。&lt;/p&gt;
&lt;h3&gt;Mark&lt;/h3&gt;
&lt;p&gt;这是一个 高亮文本。&lt;/p&gt;
&lt;h3&gt;Sub / Sup&lt;/h3&gt;
&lt;p&gt;H2O&lt;/p&gt;
&lt;p&gt;E = mc2&lt;/p&gt;
&lt;h3&gt;Abbreviation&lt;/h3&gt;
&lt;p&gt;HTML&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;28. 分割线&lt;/h2&gt;
&lt;p&gt;上面。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;下面。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;29. 综合嵌套渲染&lt;/h2&gt;
&lt;p&gt;下面同时使用 Tabs、代码块和数学公式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;Java&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;p&gt;$$
O(n \log n)
$$&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;print(&quot;Python&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;p&gt;$O(n)$&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;30. Collapse + 代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Main {
    public static void main(String[] args) {
        User user = new User(&quot;Everlasting&quot;);
        System.out.println(user.getName());
    }
}

class User {
    private final String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;31. Spoiler + Inline Code&lt;/h2&gt;
&lt;p&gt;问题：&lt;/p&gt;
&lt;p&gt;Java 程序的默认入口方法叫什么？&lt;/p&gt;
&lt;p&gt;答案：&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;32. Button + Icon&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;Button
as=&apos;a&apos;
href=&apos;https://github.com/everlastingQAQ/blog&apos;
target=&apos;_blank&apos;
rel=&apos;noopener noreferrer&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;#x3C;Button
as=&apos;a&apos;
href=&apos;/rss.xml&apos;&lt;/p&gt;
&lt;blockquote&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2&gt;33. 响应式布局测试&lt;/h2&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;缩放浏览器窗口，检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;手机&lt;/li&gt;
&lt;li&gt;平板&lt;/li&gt;
&lt;li&gt;桌面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三个尺寸下的布局。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;34. 深色 / 浅色主题测试&lt;/h2&gt;
&lt;p&gt;切换网站右上角主题按钮。&lt;/p&gt;
&lt;p&gt;重点检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正文&lt;/li&gt;
&lt;li&gt;链接&lt;/li&gt;
&lt;li&gt;Inline Code&lt;/li&gt;
&lt;li&gt;代码块&lt;/li&gt;
&lt;li&gt;Aside&lt;/li&gt;
&lt;li&gt;Card&lt;/li&gt;
&lt;li&gt;Collapse&lt;/li&gt;
&lt;li&gt;Tabs&lt;/li&gt;
&lt;li&gt;GitHubCard&lt;/li&gt;
&lt;li&gt;LinkPreview&lt;/li&gt;
&lt;li&gt;QRCode&lt;/li&gt;
&lt;li&gt;数学公式&lt;/li&gt;
&lt;li&gt;图片&lt;/li&gt;
&lt;li&gt;分割线&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;35. 渲染验收结论&lt;/h2&gt;
&lt;p&gt;如果本页所有区域都工作正常，可以基本确认：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Markdown&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MDX&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Astro Content Collection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;remark / rehype&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KaTeX&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shiki&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Astro Pure Components&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UnoCSS / Typography&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;↓&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;客户端交互&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一整条内容渲染链路工作正常。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;36. 测试完成&lt;/h2&gt;
&lt;p&gt;如果你能看到这一行，并且右侧 TOC 可以正常跳到这里：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Render Demo Passed.&lt;/strong&gt;&lt;/p&gt;</content:encoded></item><item><title>StdDraw 中文完整文档</title><link>https://blog.everlasting.xin/docs/stddraw</link><guid isPermaLink="true">https://blog.everlasting.xin/docs/stddraw</guid><description>Princeton StdDraw 绘图库完整中文参考文档，包含使用指南、字段说明与方法 API。</description><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原始文档：Princeton &lt;code&gt;StdDraw&lt;/code&gt; Javadoc（由 Javadoc 11.0.25 于 2026-08-21 生成）
本文为上传网页 &lt;code&gt;StdDraw.htm&lt;/code&gt; 的完整中文翻译版。类名、方法名、参数名、常量名和代码保持原样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;类 StdDraw&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;继承关系：&lt;/strong&gt; &lt;code&gt;Object&lt;/code&gt; → &lt;code&gt;StdDraw&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现的所有接口：&lt;/strong&gt; &lt;code&gt;ActionListener&lt;/code&gt;、&lt;code&gt;KeyListener&lt;/code&gt;、&lt;code&gt;MouseListener&lt;/code&gt;、&lt;code&gt;MouseMotionListener&lt;/code&gt;、&lt;code&gt;EventListener&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class StdDraw
extends Object
implements ActionListener, MouseListener, MouseMotionListener, KeyListener
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;StdDraw&lt;/code&gt; 类提供了一组静态方法，让程序能够创建绘图。它采用一个简单的图形模型，可以在计算机窗口中绘制由点、线、正方形、圆以及其他几何图形组成的图像，并将绘图保存到文件中。标准绘图库还支持文本、颜色、图片和动画，以及通过键盘和鼠标进行用户交互。&lt;/p&gt;
&lt;h3&gt;入门&lt;/h3&gt;
&lt;p&gt;要使用这个类，Java classpath 中必须包含 &lt;code&gt;StdDraw.class&lt;/code&gt;。如果你使用了官方自动安装程序，那么通常已经配置好了。否则，可以下载 &lt;a href=&quot;https://introcs.cs.princeton.edu/java/code/stdlib.jar&quot;&gt;stdlib.jar&lt;/a&gt; 并将其加入 Java classpath，或者下载 &lt;a href=&quot;https://introcs.cs.princeton.edu/java/stdlib/StdDraw.java&quot;&gt;StdDraw.java&lt;/a&gt; 并将其副本放在当前工作目录中。&lt;/p&gt;
&lt;p&gt;现在，将下面这个简短程序复制并粘贴到编辑器中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class TestStdDraw {
    public static void main(String[] args) {
        StdDraw.setPenRadius(0.05);
        StdDraw.setPenColor(StdDraw.BLUE);
        StdDraw.point(0.5, 0.5);
        StdDraw.setPenColor(StdDraw.MAGENTA);
        StdDraw.line(0.2, 0.2, 0.8, 0.2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果编译并运行这个程序，你应该会看到一个窗口，其中有一条较粗的品红色线段和一个蓝色点。这个程序展示了标准绘图中两类最主要的方法：一类负责绘制几何图形，另一类负责控制绘图参数。&lt;code&gt;StdDraw.line()&lt;/code&gt; 和 &lt;code&gt;StdDraw.point()&lt;/code&gt; 分别绘制线段和点；&lt;code&gt;StdDraw.setPenRadius()&lt;/code&gt; 和 &lt;code&gt;StdDraw.setPenColor()&lt;/code&gt; 则控制线条粗细和颜色。&lt;/p&gt;
&lt;h3&gt;点和线&lt;/h3&gt;
&lt;p&gt;可以使用以下方法绘制点和线段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;point(double x, double y)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;line(double x1, double y1, double x2, double y2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;x、y 坐标必须位于绘图区内（默认情况下为 0 到 1），否则点和线段将不可见。&lt;/p&gt;
&lt;h3&gt;正方形、圆、矩形和椭圆&lt;/h3&gt;
&lt;p&gt;可以使用以下方法绘制正方形、圆、矩形和椭圆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;circle(double x, double y, double radius)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;square(double x, double y, double halfLength)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rectangle(double x, double y, double halfWidth, double halfHeight)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些方法都以图形的位置和大小作为参数。图形的位置始终由其&lt;strong&gt;中心&lt;/strong&gt;的 x、y 坐标指定。圆的大小由半径指定；椭圆的大小由半长轴和半短轴的长度指定；正方形或矩形的大小则由半宽或半高指定。正方形和矩形采用这种参数约定，是为了与圆和椭圆保持一致，但对于刚接触的人来说可能不太符合直觉。&lt;/p&gt;
&lt;p&gt;上面的方法只绘制给定图形的轮廓。以下方法用于绘制实心版本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;filledCircle(double x, double y, double radius)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filledSquare(double x, double y, double radius)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filledRectangle(double x, double y, double halfWidth, double halfHeight)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;圆弧&lt;/h3&gt;
&lt;p&gt;可以使用以下方法绘制圆弧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;arc(double x, double y, double radius, double angle1, double angle2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;圆弧来自一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值的圆，并从 &lt;code&gt;angle1&lt;/code&gt; 延伸到 &lt;code&gt;angle2&lt;/code&gt;。按照约定，角度采用&lt;strong&gt;极角&lt;/strong&gt;表示，也就是从 x 轴起逆时针旋转的角度，单位为度。例如，&lt;code&gt;StdDraw.arc(0.0, 0.0, 1.0, 0, 90)&lt;/code&gt; 会绘制单位圆上从 3 点钟方向（0°）到 12 点钟方向（90°）的一段圆弧。&lt;/p&gt;
&lt;h3&gt;多边形&lt;/h3&gt;
&lt;p&gt;可以使用以下方法绘制多边形：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;polygon(double[] x, double[] y)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filledPolygon(double[] x, double[] y)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多边形中的各个点为 &lt;code&gt;(x[i], y[i])&lt;/code&gt;。例如，下面的代码片段绘制一个实心菱形，其四个顶点分别为 &lt;code&gt;(0.1, 0.2)&lt;/code&gt;、&lt;code&gt;(0.2, 0.3)&lt;/code&gt;、&lt;code&gt;(0.3, 0.2)&lt;/code&gt; 和 &lt;code&gt;(0.2, 0.1)&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double[] x = { 0.1, 0.2, 0.3, 0.2 };
double[] y = { 0.2, 0.3, 0.2, 0.1 };
StdDraw.filledPolygon(x, y);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;画笔大小&lt;/h3&gt;
&lt;p&gt;画笔是圆形的，因此当你把画笔半径设置为 &lt;em&gt;r&lt;/em&gt; 并绘制一个点时，得到的是半径为 &lt;em&gt;r&lt;/em&gt; 的圆。同时，线条宽度为 &lt;code&gt;2r&lt;/code&gt;，并具有圆形端点。默认画笔半径为 0.002，而且不会受到坐标缩放的影响。这个默认半径大约是默认画布宽度的 1/500，因此，如果在水平线或垂直线上均匀绘制 200 个点，你还能看出一个个独立的圆；但如果绘制 250 个这样的点，结果看起来就会像一条连续的线。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setPenRadius(double radius)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如，&lt;code&gt;StdDraw.setPenRadius(0.01)&lt;/code&gt; 会让线条粗细和点的大小变成默认值 0.002 的 5 倍。如果想绘制具有最小可能半径的点（在典型显示器上为 1 个像素），请将画笔半径设置为 0.0。&lt;/p&gt;
&lt;h3&gt;画笔颜色&lt;/h3&gt;
&lt;p&gt;所有几何图形（例如点、线和圆）都使用当前画笔颜色绘制。默认颜色为黑色。可以使用以下方法更改画笔颜色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setPenColor(int red, int green, int blue)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setPenColor(Color color)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个方法允许使用 RGB 颜色系统指定颜色。原文还提供了一个 &lt;a href=&quot;http://johndyer.name/lab/colorpicker/&quot;&gt;color picker&lt;/a&gt; 以方便寻找所需颜色。&lt;/p&gt;
&lt;p&gt;第二个方法允许使用 Java &lt;code&gt;java.awt&lt;/code&gt; 包中定义的 &lt;code&gt;Color&lt;/code&gt; 数据类型指定颜色。标准绘图库预定义了许多颜色，包括 &lt;code&gt;BLACK&lt;/code&gt;、&lt;code&gt;WHITE&lt;/code&gt;、&lt;code&gt;RED&lt;/code&gt;、&lt;code&gt;GREEN&lt;/code&gt; 和 &lt;code&gt;BLUE&lt;/code&gt;。例如，&lt;code&gt;StdDraw.setPenColor(StdDraw.RED)&lt;/code&gt; 会将画笔颜色设置为红色。&lt;/p&gt;
&lt;h3&gt;窗口标题&lt;/h3&gt;
&lt;p&gt;默认情况下，标准绘图窗口的标题为 &lt;code&gt;&quot;Standard Draw&quot;&lt;/code&gt;。可以使用以下方法修改标题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setTitle(String windowTitle)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该方法会把标准绘图窗口标题设置为指定字符串。&lt;/p&gt;
&lt;h3&gt;画布大小&lt;/h3&gt;
&lt;p&gt;默认情况下，所有绘图都在一个 512×512 的画布中完成。画布不包括窗口标题栏和窗口边框。可以使用以下方法修改画布大小：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setCanvasSize(int width, int height)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该方法会把画布大小设置为 &lt;code&gt;width × height&lt;/code&gt; 像素，同时使用默认背景色（白色）清除当前绘图。通常，这个方法只在程序刚开始时调用一次。例如，&lt;code&gt;StdDraw.setCanvasSize(800, 800)&lt;/code&gt; 会把画布大小设置为 800×800 像素。&lt;/p&gt;
&lt;h3&gt;画布缩放与坐标系&lt;/h3&gt;
&lt;p&gt;默认情况下，所有绘图都发生在单位正方形中，左下角为 &lt;code&gt;(0, 0)&lt;/code&gt;，右上角为 &lt;code&gt;(1, 1)&lt;/code&gt;。可以使用以下方法修改默认坐标系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setXscale(double xmin, double xmax)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setYscale(double ymin, double ymax)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setScale(double min, double max)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参数表示画布中要显示的最小和最大 x 坐标或 y 坐标。例如，如果希望使用默认坐标系但在周围保留一点边距，可以调用 &lt;code&gt;StdDraw.setScale(-.05, 1.05)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些方法只改变之后绘图命令所使用的坐标系，不会影响之前已经完成的绘图。它们也不会改变画布的像素尺寸。因此，如果 x 轴和 y 轴的缩放比例不同，正方形会显示为矩形，圆会显示为椭圆。&lt;/p&gt;
&lt;h3&gt;文本&lt;/h3&gt;
&lt;p&gt;可以使用以下方法给绘图添加文本注释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;text(double x, double y, String text)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text(double x, double y, String text, double degrees)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textLeft(double x, double y, String text)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textRight(double x, double y, String text)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两个方法使用当前字体绘制指定文本，并使文本中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。第二个方法还可以旋转文本。最后两个方法分别让文本在 &lt;code&gt;(x, y)&lt;/code&gt; 处左对齐或右对齐。&lt;/p&gt;
&lt;p&gt;默认字体是 16 磅 Sans Serif 字体。可以使用以下方法修改字体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setFont(Font font)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要指定字体，需要使用 Java &lt;code&gt;java.awt&lt;/code&gt; 包中定义的 &lt;code&gt;Font&lt;/code&gt; 数据类型。它允许选择字体名称、大小和样式。例如，下面的代码片段把字体设置为 60 磅 Arial 粗体。&lt;code&gt;import&lt;/code&gt; 语句使你可以直接使用 &lt;code&gt;Font&lt;/code&gt;，而不必写完整限定名 &lt;code&gt;java.awt.Font&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;import java.awt.Font;
...
Font font = new Font(&quot;Arial&quot;, Font.BOLD, 60);
StdDraw.setFont(font);
StdDraw.text(0.5, 0.5, &quot;Hello, World&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;图像&lt;/h3&gt;
&lt;p&gt;可以使用以下方法向绘图中添加图像：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;picture(double x, double y, String filename)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;picture(double x, double y, String filename, double degrees)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;picture(double x, double y, String filename, double scaledWidth, double scaledHeight)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;picture(double x, double y, String filename, double scaledWidth, double scaledHeight, double degrees)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些方法会绘制指定图像，并使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。图像必须采用受支持的文件格式（通常包括 JPEG、PNG、GIF、TIFF 和 BMP）。图像默认按照原始像素尺寸显示，与坐标系无关。也可以选择将图像逆时针旋转指定角度，或者重新缩放，使其恰好适配某个边界框。&lt;/p&gt;
&lt;h3&gt;保存到文件&lt;/h3&gt;
&lt;p&gt;可以通过菜单中的 &lt;em&gt;File → Save&lt;/em&gt; 选项把图像保存到文件，也可以在程序中使用以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;save(String filename)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;绘图可以保存为受支持的文件格式（通常包括 JPEG、PNG、GIF、TIFF 和 BMP）。&lt;/p&gt;
&lt;h3&gt;文件格式&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;StdDraw&lt;/code&gt; 类支持读取和写入 &lt;code&gt;javax.imageio&lt;/code&gt; 所支持的任何图像格式（通常包括 JPEG、PNG、GIF、TIFF 和 BMP）。JPEG、PNG、GIF、TIFF 和 BMP 对应的典型扩展名分别为 &lt;code&gt;.jpg&lt;/code&gt;、&lt;code&gt;.png&lt;/code&gt;、&lt;code&gt;.gif&lt;/code&gt;、&lt;code&gt;.tif&lt;/code&gt; 和 &lt;code&gt;.bmp&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于只包含几何图形的绘图，官方建议使用 PNG；对于包含图片的绘图，建议使用 JPEG。JPEG 文件格式不支持透明背景。&lt;/p&gt;
&lt;h3&gt;清空画布&lt;/h3&gt;
&lt;p&gt;可以使用以下方法清空整个绘图画布：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clear()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear(Color color)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个方法使用默认背景色（白色）清空画布；第二个方法允许指定背景色。例如，&lt;code&gt;StdDraw.clear(StdDraw.LIGHT_GRAY)&lt;/code&gt; 会使用一种灰色清空画布。若要让背景透明，请调用 &lt;code&gt;StdDraw.clear(StdDraw.TRANSPARENT)&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;计算机动画与双缓冲&lt;/h3&gt;
&lt;p&gt;双缓冲是标准绘图最强大的功能之一，可以用于制作计算机动画。以下方法控制对象的绘制方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enableDoubleBuffering()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;disableDoubleBuffering()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;show()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pause(int t)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;默认情况下，双缓冲处于禁用状态。这意味着只要调用 &lt;code&gt;point()&lt;/code&gt;、&lt;code&gt;line()&lt;/code&gt; 等绘图方法，结果就会立刻显示在屏幕上。&lt;/p&gt;
&lt;p&gt;调用 &lt;code&gt;enableDoubleBuffering()&lt;/code&gt; 启用双缓冲后，所有绘图都会发生在&lt;strong&gt;离屏画布（offscreen canvas）&lt;strong&gt;上，而离屏画布本身并不会显示。只有调用 &lt;code&gt;show()&lt;/code&gt; 时，绘图内容才会从离屏画布复制到屏幕画布（onscreen canvas），并显示在标准绘图窗口中。你可以把双缓冲理解为：先收集所有要求绘制的线、点、形状和文本，然后在需要时一次性&lt;/strong&gt;同时&lt;/strong&gt;显示出来。&lt;/p&gt;
&lt;p&gt;双缓冲最重要的用途是制作计算机动画：通过快速显示一系列静态图像，产生运动的错觉。制作动画时，重复以下四个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;清空离屏画布。&lt;/li&gt;
&lt;li&gt;在离屏画布上绘制对象。&lt;/li&gt;
&lt;li&gt;将离屏画布复制到屏幕画布。&lt;/li&gt;
&lt;li&gt;等待一小段时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;clear()&lt;/code&gt;、&lt;code&gt;show()&lt;/code&gt; 和 &lt;code&gt;pause(int t)&lt;/code&gt; 分别用于完成第 1、第 3 和第 4 步。&lt;/p&gt;
&lt;p&gt;例如，下面的代码片段会让两个小球沿圆周运动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;StdDraw.setScale(-2.0, +2.0);
StdDraw.enableDoubleBuffering();

for (double t = 0.0; true; t += 0.02) {
    double x = Math.sin(t);
    double y = Math.cos(t);
    StdDraw.clear();
    StdDraw.filledCircle(x, y, 0.1);
    StdDraw.filledCircle(-x, -y, 0.1);
    StdDraw.show();
    StdDraw.pause(20);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不使用双缓冲，小球在移动时会产生闪烁。&lt;/p&gt;
&lt;h3&gt;键盘与鼠标输入&lt;/h3&gt;
&lt;p&gt;标准绘图库对键盘和鼠标输入提供了非常基础的支持。它远不如大多数用户界面库强大，但也简单得多。可以使用以下方法捕获鼠标事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isMousePressed()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mouseX()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mouseY()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一个方法告诉你鼠标按钮当前是否正被按下；后两个方法返回鼠标当前位置的 x、y 坐标，使用的坐标系与画布相同（默认是单位正方形）。这些方法通常应放在动画循环中使用，并在每次轮询鼠标当前状态之前等待一小段时间。&lt;/p&gt;
&lt;p&gt;可以使用以下方法捕获键盘事件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hasNextKeyTyped()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nextKeyTyped()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isKeyPressed(int keycode)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果用户连续键入许多按键，它们会被保存在一个列表中，直到程序逐个处理。第一个方法用于判断用户是否键入了尚未被程序处理的按键；第二个方法返回用户键入的下一个尚未处理的按键，并把它从已保存按键列表中移除；第三个方法用于判断某个按键当前是否正被按下。&lt;/p&gt;
&lt;h3&gt;访问控制参数&lt;/h3&gt;
&lt;p&gt;可以使用以下方法获取当前画笔颜色、背景颜色、画笔半径和字体：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getPenColor()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getBackgroundColor()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getPenRadius()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getFont()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当你想临时修改某个控制参数，并在稍后将它恢复为原来的值时，这些方法非常有用。&lt;/p&gt;
&lt;h3&gt;边界情况&lt;/h3&gt;
&lt;p&gt;以下是一些需要注意的边界情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;允许绘制完全位于画布之外或部分位于画布之外的对象。不过，只有落在画布内部的部分才可见。&lt;/li&gt;
&lt;li&gt;由于浮点数问题，如果对象的 x 或 y 坐标远远超出画布范围（例如从 &lt;code&gt;(0.5, -10^308)&lt;/code&gt; 到 &lt;code&gt;(0.5, 10^308)&lt;/code&gt; 的线段），即使其中一部分理论上应该位于画布内，也可能无法显示。&lt;/li&gt;
&lt;li&gt;任何方法如果接收到 &lt;code&gt;null&lt;/code&gt; 参数，都会抛出 &lt;code&gt;IllegalArgumentException&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;任何方法如果接收到 &lt;code&gt;Double.NaN&lt;/code&gt;、&lt;code&gt;Double.POSITIVE_INFINITY&lt;/code&gt; 或 &lt;code&gt;Double.NEGATIVE_INFINITY&lt;/code&gt; 参数，都会抛出 &lt;code&gt;IllegalArgumentException&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;性能技巧&lt;/h3&gt;
&lt;p&gt;标准绘图库能够绘制大量数据。下面是一些技巧和建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对包含大量对象的静态绘图使用&lt;strong&gt;双缓冲&lt;/strong&gt;。也就是说，在一系列绘图命令之前调用 &lt;code&gt;enableDoubleBuffering()&lt;/code&gt;，全部绘制完成后再调用 &lt;code&gt;show()&lt;/code&gt;。在许多计算机系统上，一边创建复杂绘图一边持续刷新显示，效率可能低得令人无法接受。&lt;/li&gt;
&lt;li&gt;绘制计算机动画时，每一帧只调用一次 &lt;code&gt;show()&lt;/code&gt;，不要在每绘制一个对象之后都调用一次。&lt;/li&gt;
&lt;li&gt;如果多次使用同一个文件名调用 &lt;code&gt;picture()&lt;/code&gt;，Java 会缓存该图像文件，因此不会在每次调用时都承担重新读取文件的开销。&lt;/li&gt;
&lt;li&gt;默认情况下，&lt;code&gt;StdDraw&lt;/code&gt; 会针对显示质量优化渲染。若要针对速度优化，请调用 &lt;code&gt;setRenderingSpeed()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;已知缺陷和问题&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果图像中心点 &lt;code&gt;(x, y)&lt;/code&gt; 位于画布之外，&lt;code&gt;picture()&lt;/code&gt; 系列方法可能不会绘制图像中本应落在画布内部的那一部分。这个问题只会出现在某些系统上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;p&gt;如需更多文档，请参阅 Robert Sedgewick 和 Kevin Wayne 所著 &lt;em&gt;Computer Science: An Interdisciplinary Approach&lt;/em&gt; 的 &lt;a href=&quot;https://introcs.cs.princeton.edu/15inout&quot;&gt;Section 1.5&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;作者：&lt;/strong&gt; Robert Sedgewick、Kevin Wayne&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;字段摘要&lt;/h2&gt;
&lt;p&gt;| 修饰符和类型 | 字段 | 说明 |
|---|---|---|
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;AQUA&lt;/code&gt; | 水绿色（0, 255, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;BLACK&lt;/code&gt; | 黑色（0, 0, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;BLUE&lt;/code&gt; | 蓝色（0, 0, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;BOOK_BLUE&lt;/code&gt; | 《Introduction to Programming in Java》中使用的蓝色。它对应 Pantone 300U，RGB 值约为（9, 90, 166）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;BOOK_LIGHT_BLUE&lt;/code&gt; | 《Introduction to Programming in Java》中使用的浅蓝色。RGB 值约为（103, 198, 243）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;BOOK_RED&lt;/code&gt; | 《Algorithms, 4th edition》中使用的红色。它对应 Pantone 1805U，RGB 值约为（150, 35, 31）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;CYAN&lt;/code&gt; | 青色（0, 255, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;DARK_GRAY&lt;/code&gt; | 深灰色（64, 64, 64）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;FUCHSIA&lt;/code&gt; | 紫红色（255, 0, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;GRAY&lt;/code&gt; | 灰色（128, 128, 128）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;GREEN&lt;/code&gt; | 绿色（0, 128, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;LIGHT_GRAY&lt;/code&gt; | 浅灰色（192, 192, 192）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;LIME&lt;/code&gt; | 亮绿色（0, 255, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;MAGENTA&lt;/code&gt; | 品红色（255, 0, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;MAROON&lt;/code&gt; | 栗色（128, 0, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;NAVY&lt;/code&gt; | 海军蓝（0, 0, 128）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;OLIVE&lt;/code&gt; | 橄榄色（128, 128, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;ORANGE&lt;/code&gt; | 橙色（255, 200, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;PINK&lt;/code&gt; | 粉色（255, 175, 175）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;PRINCETON_ORANGE&lt;/code&gt; | 普林斯顿大学视觉标识中使用的橙色。它对应 PMS 158，RGB 值约为（245, 128, 37）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;PURPLE&lt;/code&gt; | 紫色（128, 0, 128）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;RED&lt;/code&gt; | 红色（255, 0, 0）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;SILVER&lt;/code&gt; | 银色（192, 192, 192）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;TEAL&lt;/code&gt; | 蓝绿色（0, 128, 128）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;TRANSPARENT&lt;/code&gt; | 100% 透明的颜色，用于透明背景。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;WHITE&lt;/code&gt; | 白色（255, 255, 255）。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;YELLOW&lt;/code&gt; | 黄色（255, 255, 0）。 |&lt;/p&gt;
&lt;h2&gt;方法摘要&lt;/h2&gt;
&lt;p&gt;| 修饰符和类型 | 方法 | 说明 |
|---|---|---|
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;actionPerformed ( ActionEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;addRenderingHints ( RenderingHints hints)&lt;/code&gt; | 将指定的渲染提示添加到当前渲染提示中。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;arc (double x, double y, double radius, double angle1, double angle2)&lt;/code&gt; | 绘制一段以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值、从 &lt;code&gt;angle1&lt;/code&gt; 到 &lt;code&gt;angle2&lt;/code&gt;（单位：度）的圆弧。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;circle (double x, double y, double radius)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值的圆。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;clear ()&lt;/code&gt; | 使用默认背景色（白色）清空屏幕。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;clear ( Color color)&lt;/code&gt; | 使用指定背景色清空屏幕。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;close ()&lt;/code&gt; | 关闭标准绘图窗口。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;disableDoubleBuffering ()&lt;/code&gt; | 禁用双缓冲。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;ellipse (double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、具有指定半长轴和半短轴的椭圆。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;enableDoubleBuffering ()&lt;/code&gt; | 启用双缓冲。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;filledCircle (double x, double y, double radius)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值的实心圆。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;filledEllipse (double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、具有指定半长轴和半短轴的实心椭圆。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;filledPolygon (double[] x, double[] y)&lt;/code&gt; | 绘制一个实心多边形，其顶点依次为 &lt;code&gt;(x₀, y₀)&lt;/code&gt;、&lt;code&gt;(x₁, y₁)&lt;/code&gt;、……、&lt;code&gt;(xₙ₋₁, yₙ₋₁)&lt;/code&gt;。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;filledRectangle (double x, double y, double halfWidth, double halfHeight)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的实心矩形。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;filledSquare (double x, double y, double halfLength)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的实心正方形。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;getBackgroundColor ()&lt;/code&gt; | 返回当前背景颜色。 |
| &lt;code&gt;static Font&lt;/code&gt; | &lt;code&gt;getFont ()&lt;/code&gt; | 返回当前字体。 |
| &lt;code&gt;static Color&lt;/code&gt; | &lt;code&gt;getPenColor ()&lt;/code&gt; | 返回当前画笔颜色。 |
| &lt;code&gt;static double&lt;/code&gt; | &lt;code&gt;getPenRadius ()&lt;/code&gt; | 返回当前画笔半径。 |
| &lt;code&gt;static RenderingHints&lt;/code&gt; | &lt;code&gt;getRenderingHints ()&lt;/code&gt; | 返回当前渲染提示的一个副本。 |
| &lt;code&gt;static boolean&lt;/code&gt; | &lt;code&gt;hasNextKeyTyped ()&lt;/code&gt; | 如果用户已经键入了一个尚未处理的按键，则返回 &lt;code&gt;true&lt;/code&gt;。 |
| &lt;code&gt;static boolean&lt;/code&gt; | &lt;code&gt;isKeyPressed (int keycode)&lt;/code&gt; | 如果给定按键当前正被按下，则返回 &lt;code&gt;true&lt;/code&gt;。 |
| &lt;code&gt;static boolean&lt;/code&gt; | &lt;code&gt;isMousePressed ()&lt;/code&gt; | 如果鼠标当前正被按下，则返回 &lt;code&gt;true&lt;/code&gt;。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;keyPressed ( KeyEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;keyReleased ( KeyEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;keyTyped ( KeyEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;line (double x0, double y0, double x1, double y1)&lt;/code&gt; | 绘制从 &lt;code&gt;(x₀, y₀)&lt;/code&gt; 到 &lt;code&gt;(x₁, y₁)&lt;/code&gt; 的线段。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;main ( String [] args)&lt;/code&gt; | 测试客户端。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseClicked ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseDragged ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseEntered ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseExited ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseMoved ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mousePressed ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;void&lt;/code&gt; | &lt;code&gt;mouseReleased ( MouseEvent event)&lt;/code&gt; | 此方法不能被直接调用。 |
| &lt;code&gt;static double&lt;/code&gt; | &lt;code&gt;mouseX ()&lt;/code&gt; | 返回鼠标的 x 坐标。 |
| &lt;code&gt;static double&lt;/code&gt; | &lt;code&gt;mouseY ()&lt;/code&gt; | 返回鼠标的 y 坐标。 |
| &lt;code&gt;static char&lt;/code&gt; | &lt;code&gt;nextKeyTyped ()&lt;/code&gt; | 返回用户键入的下一个、尚未被程序处理的按键。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;pause (int t)&lt;/code&gt; | 暂停 &lt;code&gt;t&lt;/code&gt; 毫秒。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;picture (double x, double y, String filename)&lt;/code&gt; | 绘制指定图像，并使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;picture (double x, double y, String filename, double degrees)&lt;/code&gt; | 绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并旋转指定角度。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;picture (double x, double y, String filename, double scaledWidth, double scaledHeight)&lt;/code&gt; | 绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并缩放到指定的边界框中。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;picture (double x, double y, String filename, double scaledWidth, double scaledHeight, double degrees)&lt;/code&gt; | 绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，旋转指定角度，并缩放到指定的边界框中。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;point (double x, double y)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心的点。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;polygon (double[] x, double[] y)&lt;/code&gt; | 绘制一个多边形，其顶点依次为 &lt;code&gt;(x₀, y₀)&lt;/code&gt;、&lt;code&gt;(x₁, y₁)&lt;/code&gt;、……、&lt;code&gt;(xₙ₋₁, yₙ₋₁)&lt;/code&gt;。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;rectangle (double x, double y, double halfWidth, double halfHeight)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的矩形。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetCanvasSize ()&lt;/code&gt; | 将画布（绘图区）设置为默认的 512×512 像素。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetFont ()&lt;/code&gt; | 将字体设置为默认字体（Sans Serif，无衬线，16 磅）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetPenColor ()&lt;/code&gt; | 将画笔颜色设置为默认颜色（黑色）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetPenRadius ()&lt;/code&gt; | 将画笔大小设置为默认值（0.002）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetRenderingHints ()&lt;/code&gt; | 恢复默认渲染提示。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetScale ()&lt;/code&gt; | 将 x 轴和 y 轴的刻度范围都重置为默认范围（0.0 到 1.0）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetXscale ()&lt;/code&gt; | 将 x 轴刻度范围重置为默认范围（0.0 到 1.0）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;resetYscale ()&lt;/code&gt; | 将 y 轴刻度范围重置为默认范围（0.0 到 1.0）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;save ( String filename)&lt;/code&gt; | 将绘图保存为受支持格式的文件（通常为 JPEG、PNG、GIF、TIFF 和 BMP）。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setCanvasSize (int canvasWidth, int canvasHeight)&lt;/code&gt; | 将画布（绘图区）设置为 &lt;code&gt;width × height&lt;/code&gt; 像素。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setFont ( Font font)&lt;/code&gt; | 将字体设置为指定值。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setPenColor (int red, int green, int blue)&lt;/code&gt; | 将画笔颜色设置为指定的 RGB 颜色。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setPenColor ( Color color)&lt;/code&gt; | 将画笔颜色设置为指定颜色。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setPenRadius (double radius)&lt;/code&gt; | 将画笔半径设置为指定大小。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setRenderingHints ( RenderingHints hints)&lt;/code&gt; | 用指定的渲染提示替换当前渲染提示。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setRenderingQuality ()&lt;/code&gt; | 将渲染提示设置为优先获得高质量绘图。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setRenderingSpeed ()&lt;/code&gt; | 将渲染提示设置为优先提高绘图速度。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setScale (double min, double max)&lt;/code&gt; | 将 x 轴和 y 轴的刻度范围都设置为同一个指定范围。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setTitle ( String title)&lt;/code&gt; | 将标准绘图窗口的标题设置为指定字符串。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setVisible (boolean isVisible)&lt;/code&gt; | 使绘图窗口可见或不可见。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setXscale (double min, double max)&lt;/code&gt; | 将 x 轴刻度范围设置为指定范围。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;setYscale (double min, double max)&lt;/code&gt; | 将 y 轴刻度范围设置为指定范围。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;show ()&lt;/code&gt; | 将离屏缓冲区复制到屏幕缓冲区。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;square (double x, double y, double halfLength)&lt;/code&gt; | 绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的正方形。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;text (double x, double y, String text)&lt;/code&gt; | 使用当前字体绘制给定文本字符串，并使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;text (double x, double y, String text, double degrees)&lt;/code&gt; | 使用当前字体绘制给定文本字符串，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并旋转指定角度。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;textLeft (double x, double y, String text)&lt;/code&gt; | 使用当前字体绘制给定文本字符串，并以 &lt;code&gt;(x, y)&lt;/code&gt; 为基准左对齐。 |
| &lt;code&gt;static void&lt;/code&gt; | &lt;code&gt;textRight (double x, double y, String text)&lt;/code&gt; | 使用当前字体绘制给定文本字符串，并以 &lt;code&gt;(x, y)&lt;/code&gt; 为基准右对齐。 |&lt;/p&gt;
&lt;h3&gt;从 &lt;code&gt;java.lang.Object&lt;/code&gt; 继承的方法&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;clone&lt;/code&gt;、&lt;code&gt;equals&lt;/code&gt;、&lt;code&gt;getClass&lt;/code&gt;、&lt;code&gt;hashCode&lt;/code&gt;、&lt;code&gt;notify&lt;/code&gt;、&lt;code&gt;notifyAll&lt;/code&gt;、&lt;code&gt;toString&lt;/code&gt;、&lt;code&gt;wait&lt;/code&gt;、&lt;code&gt;wait&lt;/code&gt;、&lt;code&gt;wait&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;字段详细说明&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;AQUA&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
AQUA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;水绿色（0, 255, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BLACK&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
BLACK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;黑色（0, 0, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BLUE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
BLUE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;蓝色（0, 0, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;CYAN&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
CYAN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;青色（0, 255, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;FUCHSIA&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
FUCHSIA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紫红色（255, 0, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;DARK_GRAY&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
DARK_GRAY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;深灰色（64, 64, 64）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;GRAY&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
GRAY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;灰色（128, 128, 128）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;GREEN&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
GREEN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绿色（0, 128, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;LIGHT_GRAY&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
LIGHT_GRAY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浅灰色（192, 192, 192）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;LIME&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
LIME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;亮绿色（0, 255, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MAGENTA&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
MAGENTA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;品红色（255, 0, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;MAROON&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
MAROON
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栗色（128, 0, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;NAVY&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
NAVY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;海军蓝（0, 0, 128）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;OLIVE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
OLIVE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;橄榄色（128, 128, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;ORANGE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
ORANGE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;橙色（255, 200, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;PINK&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
PINK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;粉色（255, 175, 175）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;PURPLE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
PURPLE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紫色（128, 0, 128）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;RED&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
RED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;红色（255, 0, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;SILVER&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
SILVER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;银色（192, 192, 192）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;TEAL&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
TEAL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;蓝绿色（0, 128, 128）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;WHITE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
WHITE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;白色（255, 255, 255）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;YELLOW&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
YELLOW
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;黄色（255, 255, 0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;TRANSPARENT&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
TRANSPARENT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;100% 透明的颜色，用于透明背景。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BOOK_BLUE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
BOOK_BLUE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;《Introduction to Programming in Java》中使用的蓝色。它对应 Pantone 300U，RGB 值约为（9, 90, 166）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BOOK_LIGHT_BLUE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
BOOK_LIGHT_BLUE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;《Introduction to Programming in Java》中使用的浅蓝色。RGB 值约为（103, 198, 243）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;BOOK_RED&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
BOOK_RED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;《Algorithms, 4th edition》中使用的红色。它对应 Pantone 1805U，RGB 值约为（150, 35, 31）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;PRINCETON_ORANGE&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static final
Color
PRINCETON_ORANGE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;普林斯顿大学视觉标识中使用的橙色。它对应 PMS 158，RGB 值约为（245, 128, 37）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;方法详细说明&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;public static void setVisible(boolean isVisible)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setVisible(boolean isVisible)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使绘图窗口可见或不可见。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;isVisible&lt;/code&gt;：如果为 &lt;code&gt;true&lt;/code&gt;，则使绘图窗口可见；否则隐藏绘图窗口。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void resetCanvasSize()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetCanvasSize()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画布（绘图区）设置为默认的 512×512 像素。同时会使用默认背景色（白色）清除当前绘图。通常，这个方法只在程序刚开始时调用一次。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setCanvasSize(int canvasWidth, int canvasHeight)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setCanvasSize(int canvasWidth,
int canvasHeight)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画布（绘图区）设置为 &lt;code&gt;width × height&lt;/code&gt; 像素。同时会使用默认背景色（白色）清除当前绘图。通常，这个方法只在程序刚开始时调用一次。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;canvasWidth&lt;/code&gt;：宽度（像素数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;canvasHeight&lt;/code&gt;：高度（像素数）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：除非 &lt;code&gt;canvasWidth&lt;/code&gt; 和 &lt;code&gt;canvasHeight&lt;/code&gt; 都为正数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void close()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭标准绘图窗口。这样客户端程序就可以直接终止，而不必要求用户手动关闭标准绘图窗口。在调用此方法之后再次进行绘图，会恢复之前的窗口状态。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setTitle( String title)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setTitle(
String
title)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将标准绘图窗口的标题设置为指定字符串。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;title&lt;/code&gt;：标题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;title&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void resetXscale()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetXscale()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 x 轴刻度范围重置为默认范围（0.0 到 1.0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void resetYscale()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetYscale()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 y 轴刻度范围重置为默认范围（0.0 到 1.0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void resetScale()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetScale()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 x 轴和 y 轴的刻度范围都重置为默认范围（0.0 到 1.0）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setXscale(double min, double max)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setXscale(double min,
double max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 x 轴刻度范围设置为指定范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;min&lt;/code&gt;：x 轴刻度的最小值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt;：x 轴刻度的最大值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;max == min&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;min&lt;/code&gt; 或 &lt;code&gt;max&lt;/code&gt; 中任意一个是 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void setYscale(double min, double max)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setYscale(double min,
double max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 y 轴刻度范围设置为指定范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;min&lt;/code&gt;：y 轴刻度的最小值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt;：y 轴刻度的最大值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;max == min&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;min&lt;/code&gt; 或 &lt;code&gt;max&lt;/code&gt; 中任意一个是 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void setScale(double min, double max)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setScale(double min,
double max)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 x 轴和 y 轴的刻度范围都设置为同一个指定范围。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;min&lt;/code&gt;：x 轴和 y 轴刻度的最小值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt;：x 轴和 y 轴刻度的最大值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;max == min&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;min&lt;/code&gt; 或 &lt;code&gt;max&lt;/code&gt; 中任意一个是 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void clear()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void clear()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用默认背景色（白色）清空屏幕。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void clear( Color color)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void clear(
Color
color)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用指定背景色清空屏幕。若要让背景透明，请使用 &lt;code&gt;StdDraw.TRANSPARENT&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;color&lt;/code&gt;：作为背景的颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;color&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static double getPenRadius()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double getPenRadius()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前画笔半径。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 当前画笔半径值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void resetPenRadius()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetPenRadius()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画笔大小设置为默认值（0.002）。画笔是圆形的，因此绘制的线段端点是圆头；设置画笔半径后绘制一个点，会得到一个具有指定半径的圆。画笔半径不受坐标缩放影响。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setPenRadius(double radius)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setPenRadius(double radius)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画笔半径设置为指定大小。画笔是圆形的，因此绘制的线段端点是圆头；设置画笔半径后绘制一个点，会得到一个具有指定半径的圆。画笔半径不受坐标缩放影响。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;radius&lt;/code&gt;：画笔半径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;radius&lt;/code&gt; 为负数、&lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static Color getPenColor()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static
Color
getPenColor()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前画笔颜色。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 当前画笔颜色。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static Color getBackgroundColor()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static
Color
getBackgroundColor()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前背景颜色。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 当前背景颜色。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void resetPenColor()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetPenColor()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画笔颜色设置为默认颜色（黑色）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setPenColor( Color color)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setPenColor(
Color
color)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画笔颜色设置为指定颜色。&lt;code&gt;StdDraw&lt;/code&gt; 预定义了许多画笔颜色，例如 &lt;code&gt;StdDraw.BLACK&lt;/code&gt;、&lt;code&gt;StdDraw.WHITE&lt;/code&gt;、&lt;code&gt;StdDraw.RED&lt;/code&gt;、&lt;code&gt;StdDraw.GREEN&lt;/code&gt; 和 &lt;code&gt;StdDraw.BLUE&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;color&lt;/code&gt;：画笔颜色。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;color&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void setPenColor(int red, int green, int blue)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setPenColor(int red,
int green,
int blue)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将画笔颜色设置为指定的 RGB 颜色。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;red&lt;/code&gt;：红色分量（0 到 255）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;green&lt;/code&gt;：绿色分量（0 到 255）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;blue&lt;/code&gt;：蓝色分量（0 到 255）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;red&lt;/code&gt;、&lt;code&gt;green&lt;/code&gt; 或 &lt;code&gt;blue&lt;/code&gt; 超出规定范围。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static Font getFont()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static
Font
getFont()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前字体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 当前字体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void resetFont()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetFont()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将字体设置为默认字体（Sans Serif，无衬线，16 磅）。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setFont( Font font)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setFont(
Font
font)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将字体设置为指定值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;font&lt;/code&gt;：字体。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;font&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void setRenderingQuality()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setRenderingQuality()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将渲染提示设置为优先获得高质量绘图。这是默认设置。该设置只影响之后的绘图命令；已经绘制的形状、文本或图片不会被重新渲染。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setRenderingSpeed()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setRenderingSpeed()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将渲染提示设置为优先提高绘图速度。这可能会降低抗锯齿、文本渲染和缩放图像的质量。该设置只影响之后的绘图命令；已经绘制的形状、文本或图片不会被重新渲染。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void resetRenderingHints()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void resetRenderingHints()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复默认渲染提示。这等价于调用 &lt;code&gt;setRenderingQuality()&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void setRenderingHints( RenderingHints hints)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void setRenderingHints(
RenderingHints
hints)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用指定的渲染提示替换当前渲染提示。由于 &lt;code&gt;RenderingHints&lt;/code&gt; 是可变对象，&lt;code&gt;StdDraw&lt;/code&gt; 类会对参数进行防御性复制。因此，在调用此方法后再修改传入的参数，不会影响 &lt;code&gt;StdDraw&lt;/code&gt; 实际使用的渲染提示。该设置只影响之后的绘图命令；已经绘制的形状、文本或图片不会被重新渲染。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;hints&lt;/code&gt;：渲染提示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;hints&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void addRenderingHints( RenderingHints hints)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void addRenderingHints(
RenderingHints
hints)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将指定的渲染提示添加到当前渲染提示中。如果指定渲染提示中的某个键已经存在于当前渲染提示中，则会替换该键的值。由于 &lt;code&gt;RenderingHints&lt;/code&gt; 是可变对象，&lt;code&gt;StdDraw&lt;/code&gt; 类会对参数进行防御性复制。因此，在调用此方法后再修改传入的参数，不会影响 &lt;code&gt;StdDraw&lt;/code&gt; 实际使用的渲染提示。该设置只影响之后的绘图命令；已经绘制的形状、文本或图片不会被重新渲染。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;hints&lt;/code&gt;：要添加的渲染提示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;hints&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static RenderingHints getRenderingHints()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static
RenderingHints
getRenderingHints()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回当前渲染提示的一个副本。返回对象是防御性副本；修改它不会影响 &lt;code&gt;StdDraw&lt;/code&gt; 使用的渲染提示。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 当前渲染提示的一个副本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void line(double x0, double y0, double x1, double y1)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void line(double x0,
double y0,
double x1,
double y1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制从 &lt;code&gt;(x₀, y₀)&lt;/code&gt; 到 &lt;code&gt;(x₁, y₁)&lt;/code&gt; 的线段。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x0&lt;/code&gt;：一个端点的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y0&lt;/code&gt;：一个端点的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x1&lt;/code&gt;：另一个端点的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y1&lt;/code&gt;：另一个端点的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意坐标为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void point(double x, double y)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void point(double x,
double y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心的点。这个点实际上是一个实心圆，其半径等于画笔半径。若要绘制单像素点，请先将画笔半径设为 0。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：点的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：点的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void circle(double x, double y, double radius)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void circle(double x,
double y,
double radius)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值的圆。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：圆心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：圆心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;radius&lt;/code&gt;：圆的半径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;radius&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void filledCircle(double x, double y, double radius)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void filledCircle(double x,
double y,
double radius)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值的实心圆。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：圆心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：圆心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;radius&lt;/code&gt;：圆的半径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;radius&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void ellipse(double x,
double y,
double semiMajorAxis,
double semiMinorAxis)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、具有指定半长轴和半短轴的椭圆。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：椭圆中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：椭圆中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;semiMajorAxis&lt;/code&gt;：椭圆的半长轴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;semiMinorAxis&lt;/code&gt;：椭圆的半短轴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;semiMajorAxis&lt;/code&gt; 或 &lt;code&gt;semiMinorAxis&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void filledEllipse(double x,
double y,
double semiMajorAxis,
double semiMinorAxis)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、具有指定半长轴和半短轴的实心椭圆。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：椭圆中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：椭圆中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;semiMajorAxis&lt;/code&gt;：椭圆的半长轴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;semiMinorAxis&lt;/code&gt;：椭圆的半短轴。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;semiMajorAxis&lt;/code&gt; 或 &lt;code&gt;semiMinorAxis&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void arc(double x, double y, double radius, double angle1, double angle2)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void arc(double x,
double y,
double radius,
double angle1,
double angle2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一段以 &lt;code&gt;(x, y)&lt;/code&gt; 为圆心、半径为指定值、从 &lt;code&gt;angle1&lt;/code&gt; 到 &lt;code&gt;angle2&lt;/code&gt;（单位：度）的圆弧。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：圆心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：圆心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;radius&lt;/code&gt;：圆的半径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;angle1&lt;/code&gt;：起始角度。0 表示圆弧从 3 点钟方向开始。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;angle2&lt;/code&gt;：圆弧结束角度。例如，如果希望得到 90° 的圆弧，则 &lt;code&gt;angle2&lt;/code&gt; 应为 &lt;code&gt;angle1 + 90&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;radius&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void square(double x, double y, double halfLength)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void square(double x,
double y,
double halfLength)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的正方形。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：正方形中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：正方形中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfLength&lt;/code&gt;：正方形任意一条边长度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;halfLength&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void filledSquare(double x, double y, double halfLength)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void filledSquare(double x,
double y,
double halfLength)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的实心正方形。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：正方形中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：正方形中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfLength&lt;/code&gt;：正方形任意一条边长度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;halfLength&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void rectangle(double x, double y, double halfWidth, double halfHeight)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void rectangle(double x,
double y,
double halfWidth,
double halfHeight)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的矩形。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：矩形中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：矩形中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfWidth&lt;/code&gt;：矩形宽度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfHeight&lt;/code&gt;：矩形高度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;halfWidth&lt;/code&gt; 或 &lt;code&gt;halfHeight&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void filledRectangle(double x, double y, double halfWidth, double halfHeight)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void filledRectangle(double x,
double y,
double halfWidth,
double halfHeight)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个以 &lt;code&gt;(x, y)&lt;/code&gt; 为中心、大小为指定值的实心矩形。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：矩形中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：矩形中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfWidth&lt;/code&gt;：矩形宽度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;halfHeight&lt;/code&gt;：矩形高度的一半。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;halfWidth&lt;/code&gt; 或 &lt;code&gt;halfHeight&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意参数为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void polygon(double[] x, double[] y)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void polygon(double[] x,
double[] y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个多边形，其顶点依次为 &lt;code&gt;(x₀, y₀)&lt;/code&gt;、&lt;code&gt;(x₁, y₁)&lt;/code&gt;、……、&lt;code&gt;(xₙ₋₁, yₙ₋₁)&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：多边形所有顶点 x 坐标组成的数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：多边形所有顶点 y 坐标组成的数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：除非 &lt;code&gt;x[]&lt;/code&gt; 与 &lt;code&gt;y[]&lt;/code&gt; 长度相同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意坐标为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x[]&lt;/code&gt; 或 &lt;code&gt;y[]&lt;/code&gt; 中任意一个为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void filledPolygon(double[] x, double[] y)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void filledPolygon(double[] x,
double[] y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制一个实心多边形，其顶点依次为 &lt;code&gt;(x₀, y₀)&lt;/code&gt;、&lt;code&gt;(x₁, y₁)&lt;/code&gt;、……、&lt;code&gt;(xₙ₋₁, yₙ₋₁)&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：多边形所有顶点 x 坐标组成的数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：多边形所有顶点 y 坐标组成的数组。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：除非 &lt;code&gt;x[]&lt;/code&gt; 与 &lt;code&gt;y[]&lt;/code&gt; 长度相同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果任意坐标为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x[]&lt;/code&gt; 或 &lt;code&gt;y[]&lt;/code&gt; 中任意一个为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void picture(double x, double y, String filename)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void picture(double x,
double y,
String
filename)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制指定图像，并使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。通常支持 JPEG、PNG、GIF、TIFF 和 BMP 等图像格式。作为一种优化，Java 会在文件系统层面对图片进行缓存，这会显著提高多次重绘同一图像（例如动画中）的性能。不过，如果你在首次绘制后修改了图片文件，之后的调用仍会绘制原先缓存的图片。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：图像中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：图像中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;filename&lt;/code&gt;：图像/图片的文件名，例如 &lt;code&gt;&quot;ball.gif&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果图像文件名无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void picture(double x, double y, String filename, double degrees)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void picture(double x,
double y,
String
filename,
double degrees)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并将图像旋转指定角度。通常支持 JPEG、PNG、GIF、TIFF 和 BMP 等图像格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：图像中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：图像中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;filename&lt;/code&gt;：图像/图片的文件名，例如 &lt;code&gt;&quot;ball.gif&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;degrees&lt;/code&gt;：逆时针旋转的角度数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果图像文件名无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt; 或 &lt;code&gt;degrees&lt;/code&gt; 为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;filename&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void picture(double x, double y, String filename, double scaledWidth, double scaledHeight)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void picture(double x,
double y,
String
filename,
double scaledWidth,
double scaledHeight)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并将其缩放到指定的边界框中。通常支持 JPEG、PNG、GIF、TIFF 和 BMP 等图像格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：图像中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：图像中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;filename&lt;/code&gt;：图像/图片的文件名，例如 &lt;code&gt;&quot;ball.gif&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;scaledWidth&lt;/code&gt;：缩放后图像的宽度（使用屏幕坐标）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;scaledHeight&lt;/code&gt;：缩放后图像的高度（使用屏幕坐标）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;scaledWidth&lt;/code&gt; 或 &lt;code&gt;scaledHeight&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果图像文件名无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;filename&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void picture(double x, double y, String filename, double scaledWidth, double scaledHeight, double degrees)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void picture(double x,
double y,
String
filename,
double scaledWidth,
double scaledHeight,
double degrees)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制指定图像，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，将其旋转指定角度，并缩放到指定的边界框中。通常支持 JPEG、PNG、GIF、TIFF 和 BMP 等图像格式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：图像中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：图像中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;filename&lt;/code&gt;：图像/图片的文件名，例如 &lt;code&gt;&quot;ball.gif&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;scaledWidth&lt;/code&gt;：缩放后图像的宽度（使用屏幕坐标）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;scaledHeight&lt;/code&gt;：缩放后图像的高度（使用屏幕坐标）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;degrees&lt;/code&gt;：逆时针旋转的角度数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;scaledWidth&lt;/code&gt; 或 &lt;code&gt;scaledHeight&lt;/code&gt; 中任意一个为负数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果图像文件名无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void text(double x, double y, String text)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void text(double x,
double y,
String
text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用当前字体绘制给定文本字符串，并使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：文本中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：文本中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;text&lt;/code&gt;：要绘制的文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;text&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void text(double x, double y, String text, double degrees)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void text(double x,
double y,
String
text,
double degrees)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用当前字体绘制给定文本字符串，使其中心位于 &lt;code&gt;(x, y)&lt;/code&gt;，并旋转指定角度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：文本中心的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：文本中心的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;text&lt;/code&gt;：要绘制的文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;degrees&lt;/code&gt;：逆时针旋转的角度数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;text&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt; 或 &lt;code&gt;degrees&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void textLeft(double x, double y, String text)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void textLeft(double x,
double y,
String
text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用当前字体绘制给定文本字符串，并以 &lt;code&gt;(x, y)&lt;/code&gt; 为基准左对齐。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：文本的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：文本的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;text&lt;/code&gt;：文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;text&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void textRight(double x, double y, String text)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void textRight(double x,
double y,
String
text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用当前字体绘制给定文本字符串，并以 &lt;code&gt;(x, y)&lt;/code&gt; 为基准右对齐。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;x&lt;/code&gt;：文本的 x 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;y&lt;/code&gt;：文本的 y 坐标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;text&lt;/code&gt;：要绘制的文本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;text&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;x&lt;/code&gt; 或 &lt;code&gt;y&lt;/code&gt; 中任意一个为 &lt;code&gt;NaN&lt;/code&gt; 或无穷大。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void pause(int t)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void pause(int t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;暂停 &lt;code&gt;t&lt;/code&gt; 毫秒。此方法用于支持计算机动画。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;t&lt;/code&gt;：毫秒数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;t&lt;/code&gt; 为负数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void show()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将离屏缓冲区复制到屏幕缓冲区。除非启用了双缓冲，否则没有必要调用此方法。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void enableDoubleBuffering()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void enableDoubleBuffering()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用双缓冲。此后对 &lt;code&gt;line()&lt;/code&gt;、&lt;code&gt;circle()&lt;/code&gt;、&lt;code&gt;square()&lt;/code&gt; 等绘图方法的调用都会被延迟，直到下一次调用 &lt;code&gt;show()&lt;/code&gt; 时才显示。此功能适合制作动画。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void disableDoubleBuffering()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void disableDoubleBuffering()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;禁用双缓冲。此后调用 &lt;code&gt;line()&lt;/code&gt;、&lt;code&gt;circle()&lt;/code&gt;、&lt;code&gt;square()&lt;/code&gt; 等绘图方法时，结果会立即显示在屏幕上。这是默认设置。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;public static void save( String filename)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void save(
String
filename)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将绘图保存为受支持格式的文件（通常为 JPEG、PNG、GIF、TIFF 和 BMP）。文件扩展名必须为 &lt;code&gt;.jpg&lt;/code&gt;、&lt;code&gt;.png&lt;/code&gt;、&lt;code&gt;.gif&lt;/code&gt;、&lt;code&gt;.bmp&lt;/code&gt; 或 &lt;code&gt;.tif&lt;/code&gt;。如果启用了双缓冲，保存的是屏幕缓冲区（即当前屏幕上显示的内容），而不是离屏缓冲区。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;filename&lt;/code&gt;：文件名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;filename&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;filename&lt;/code&gt; 是空字符串。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果 &lt;code&gt;filename&lt;/code&gt; 的文件类型扩展名无效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;IllegalArgumentException&lt;/code&gt;：如果无法写入文件 &lt;code&gt;filename&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void actionPerformed( ActionEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void actionPerformed(
ActionEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;ActionListener&lt;/code&gt; 中的 &lt;code&gt;actionPerformed&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static boolean isMousePressed()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean isMousePressed()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果鼠标当前正被按下，则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 如果鼠标正被按下则为 &lt;code&gt;true&lt;/code&gt;；否则为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static double mouseX()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double mouseX()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回鼠标的 x 坐标。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 鼠标的 x 坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static double mouseY()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static double mouseY()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回鼠标的 y 坐标。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 鼠标的 y 坐标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseClicked( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseClicked(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseListener&lt;/code&gt; 中的 &lt;code&gt;mouseClicked&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseEntered( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseEntered(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseListener&lt;/code&gt; 中的 &lt;code&gt;mouseEntered&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseExited( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseExited(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseListener&lt;/code&gt; 中的 &lt;code&gt;mouseExited&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mousePressed( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mousePressed(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseListener&lt;/code&gt; 中的 &lt;code&gt;mousePressed&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseReleased( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseReleased(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseListener&lt;/code&gt; 中的 &lt;code&gt;mouseReleased&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseDragged( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseDragged(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseMotionListener&lt;/code&gt; 中的 &lt;code&gt;mouseDragged&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void mouseMoved( MouseEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void mouseMoved(
MouseEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;MouseMotionListener&lt;/code&gt; 中的 &lt;code&gt;mouseMoved&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static boolean hasNextKeyTyped()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean hasNextKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用户已经键入了一个尚未处理的按键，则返回 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 如果用户已经键入了一个尚未被 &lt;code&gt;nextKeyTyped()&lt;/code&gt; 处理的按键，则为 &lt;code&gt;true&lt;/code&gt;；否则为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static char nextKeyTyped()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static char nextKeyTyped()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回用户键入的下一个、尚未被程序处理的按键。在调用本方法之前，应先调用 &lt;code&gt;hasNextKeyTyped()&lt;/code&gt;，以确保确实存在下一个待处理按键。本方法返回与所键入按键对应的 Unicode 字符（例如 &lt;code&gt;&apos;a&apos;&lt;/code&gt; 或 &lt;code&gt;&apos;A&apos;&lt;/code&gt;）。它无法识别动作键（例如 F1 和方向键）或修饰键（例如 Ctrl）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 用户键入的下一个、尚未被程序处理的按键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;抛出：&lt;/strong&gt; &lt;code&gt;NoSuchElementException&lt;/code&gt;：如果没有剩余的按键可供读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static boolean isKeyPressed(int keycode)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static boolean isKeyPressed(int keycode)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果给定按键当前正被按下，则返回 &lt;code&gt;true&lt;/code&gt;。此方法接受按键码 &lt;code&gt;keycode&lt;/code&gt;（对应一个物理按键）作为参数。它能够处理动作键（例如 F1 和方向键）以及修饰键（例如 Shift 和 Ctrl）。有关按键码的说明，请参阅 &lt;code&gt;KeyEvent&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;keycode&lt;/code&gt;：要检查是否正被按下的按键。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回：&lt;/strong&gt; 如果 &lt;code&gt;keycode&lt;/code&gt; 对应的按键当前正被按下，则为 &lt;code&gt;true&lt;/code&gt;；否则为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void keyTyped( KeyEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void keyTyped(
KeyEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;KeyListener&lt;/code&gt; 中的 &lt;code&gt;keyTyped&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void keyPressed( KeyEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void keyPressed(
KeyEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;KeyListener&lt;/code&gt; 中的 &lt;code&gt;keyPressed&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public void keyReleased( KeyEvent event)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public void keyReleased(
KeyEvent
event)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法不能被直接调用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定于：&lt;/strong&gt; 接口 &lt;code&gt;KeyListener&lt;/code&gt; 中的 &lt;code&gt;keyReleased&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;code&gt;public static void main( String [] args)&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(
String
[] args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试客户端。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;参数：&lt;/strong&gt; &lt;code&gt;args&lt;/code&gt;：命令行参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;文档信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类：&lt;/strong&gt; &lt;code&gt;StdDraw&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原始页面标题：&lt;/strong&gt; &lt;code&gt;StdDraw&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Javadoc 生成版本：&lt;/strong&gt; 11.0.25&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原始生成日期：&lt;/strong&gt; 2026-08-21&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作者：&lt;/strong&gt; Robert Sedgewick、Kevin Wayne&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>