跳转至

第 1 章 Java 入门

原作:Josh Hug,UC Berkeley CS61B Spring 2021 配套读本。
中文翻译版,仅供非商业学习;采用 CC BY-NC-SA 4.0 许可。
原始网站:https://joshhug.gitbooks.io/hug61b/content/



1.1 基础要点

视频讲解

先来看我们的第一个 Java 程序。运行下面的程序时,屏幕上会输出 “Hello world!”。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

对于从 Python 等语言转过来的同学,这段代码可能显得冗长得没有必要。不过这些写法都有充分理由,接下来的几周里我们会逐渐理解它们。先注意以下几个关键语法特征:

  • 整个程序由一个类声明构成,类通过关键字 public class 声明。在 Java 中,所有代码都位于类之中。
  • 真正执行的代码位于名为 main 的方法中,它被声明为 public static void main(String[] args)
  • 使用花括号 {} 表示一段代码的开始和结束。
  • 每条语句必须以分号结束。

本书不是一本完整的 Java 语法教材,因此不会逐项详讲 Java 语法。需要查阅资料时,可以参考 Paul Hilfinger 的免费电子书 A Java Reference;如果更喜欢传统教材,也可以阅读 Kathy Sierra 与 Bert Bates 合著的 Head First Java

感兴趣的话,还可以看看其他编程语言中的 Hello world!

运行 Java 程序

视频讲解

执行 Java 程序最常见的方式,是依次运行两个程序。第一个是 Java 编译器 javac,第二个是 Java 解释器 java

编译流程

例如,要运行 HelloWorld.java,先在终端输入命令 javac HelloWorld.java,然后输入 java HelloWorld。结果大致如下:

$ javac HelloWorld.java
$ java HelloWorld
Hello World! 

上面的 $ 表示终端的命令提示符。你电脑上的提示符很可能更长。

你可能会注意到,编译时命令中包含 .java,解释执行时却不写 .class。规则就是如此(TIJTWII,意为 “This Is Just The Way It Is”)。

练习 1.1.1。 在电脑上创建一个名为 HelloWorld.java 的文件,将上面的完整程序原样复制进去。尝试执行 javac HelloWorld.java。表面上看起来什么也没有发生。

但如果查看当前目录,会发现生成了一个名为 HelloWorld.class 的新文件。稍后我们会说明它是什么。

接着输入命令 java HelloWorld,终端中应该会显示 “Hello World!”。

纯属好玩。 尝试用记事本、TextEdit、Sublime、vim 或你喜欢的其他文本编辑器打开 HelloWorld.class。你会看到一大堆只有 Java 解释器才看得懂的乱码。这就是 Java 字节码,本课程不会深入讨论它。

变量与循环

视频讲解

下面的程序会输出从 0 到 9 的整数。

public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x < 10) {
            System.out.print(x + " ");
            x = x + 1;
        }
    }
}

运行程序后会看到:

$ javac HelloNumbers.java
$ java HelloNumbers
$ 0 1 2 3 4 5 6 7 8 9 

这段程序中有一些值得注意的特征:

  • 变量 x 必须先声明再使用,而且声明时必须给出类型!
  • 循环体放在花括号中,被判断的布尔表达式放在圆括号中。
  • 输出语句使用 System.out.print,而不是 System.out.println,因此输出后不会另起一行。
  • 输出语句把一个数字与一个空格连接起来,从而避免数字挤在一起。删掉空格试试看会发生什么。
  • 程序运行结束后,命令提示符会紧跟在这些数字的同一行上。如果愿意,可以在后面的练习中修复这一点。

这些特征中最重要的是:变量具有声明时确定的类型。稍后我们会回到这一点,不过先做一道练习。

练习 1.1.2。 修改 HelloNumbers,让它输出从 0 到 9 各整数的累计和。例如,输出应以 0 1 3 6 10... 开始,并以 45 结束。

另外,如果你比较在意输出格式,可以让程序在最后额外输出一个换行符。

Gradescope

本课程通过名为 Gradescope 的网站评分。如果你正在正式修读与本书配套的加州大学课程,需要在该网站提交作业并获得成绩。如果只是自学,也可以使用 Gradescope 检查答案。注册时使用课程代码 93PK75。有关 Gradescope 及作业提交方法的更多信息,请参阅 Gradescope 指南(链接稍后补充)

静态类型

Java 最重要的特征之一,是所有变量和表达式都具有所谓的 静态类型。Java 变量只能保存该类型的值,而且变量的类型一旦确定就永远不会改变。

Java 编译器的一项核心功能是执行静态类型检查。例如,假设有下面的程序:

public class HelloNumbers {
    public static void main(String[] args) {
        int x = 0;
        while (x < 10) {
            System.out.print(x + " ");
            x = x + 1;
        }
        x = "horse";
    }
}

编译后会看到:

$ javac HelloNumbers.java 
HelloNumbers.java:9: error: incompatible types: String cannot be converted to int
        x = "horse";
                ^
1 error

编译器在程序真正运行之前就直接拒绝了它。这一点非常重要,因为这意味着现实世界中任何运行该程序的人,都绝不可能在运行时遇到这个类型错误。

这与 Python 等动态类型语言形成鲜明对比:在动态类型语言中,用户可能在程序执行过程中才碰到类型错误。

除了提供额外的错误检查,静态类型还会明确告诉程序员当前处理的对象究竟是什么类型。接下来的几周里,我们会看到这一点有多重要。这也是我个人最喜欢的 Java 特性之一。

总结来说,静态类型具有以下优点:

  • 编译器会确保所有类型彼此兼容,使程序员更容易调试代码。
  • 由于代码已经被保证不存在类型错误,编译后程序的用户不会再遇到类型错误。例如,Android 应用使用 Java 编写,通常只以 .class 文件(也就是编译后的形式)发布。因为它们已经经过编译器检查,所以这类应用原则上不应因类型错误而崩溃。
  • 每个变量、参数和函数都有明确声明的类型,程序员因此更容易理解代码并进行推理。

不过静态类型也有缺点,我们会在后续章节中讨论。

补充思考题

在 Java 中可以写 System.out.println(5 + " ");。但在 Python 中,不能像上面那样写 print(5 + "horse")。为什么?

考虑以下两条 Java 语句:

String h = 5 + "horse";

以及:

int h = 5 + "horse";

第一条语句可以通过编译,第二条则会产生编译错误。Java 是强类型语言;当你声明 h 是字符串时,Java 可以把两个元素连接起来并得到字符串。但当 hint 时,Java 无法把一个数字和一个字符串连接后再得到数字。

Python 并没有事先限定结果类型,因此也无法猜测你想得到什么类型。x = 5 + "horse" 究竟应该得到数字还是字符串?Python 不知道,所以会报错。

对于 System.out.println(5 + "horse");,Java 会把参数解释为字符串拼接,因此输出结果是 5horse。更实用的例子是 System.out.println(5 + " ");,它会在 5 后输出一个空格。

System.out.println(5 + "10"); 会输出 510 还是 15?那么 System.out.println(5 + 10); 又会输出什么?

在 Java 中定义函数

视频讲解

在 Python 等语言中,函数可以在许多位置声明,包括其他函数之外。例如,下面的代码声明了一个返回两个参数中较大值的函数,然后用它计算并输出 8 与 10 中较大的那个数:

def larger(x, y):
    if x > y:
        return x
    return y

print(larger(8, 10))

由于所有 Java 代码都属于某个类,我们必须把函数定义为某个类的成员。属于类的函数通常称为“方法”。本课程中会交替使用“函数”和“方法”这两个词。与上面代码等价的 Java 程序如下:

public class LargerDemo {
    public static int larger(int x, int y) {
        if (x > y) {
            return x;
        }
        return y;
    }

    public static void main(String[] args) {
        System.out.println(larger(8, 10));
    }
}

这里的新语法是:方法通过关键字 public static 声明,它大致相当于 Python 的 def。下一章会介绍声明方法的其他方式。

这段 Java 代码显然冗长得多。你可能认为这种编程语言会拖慢开发速度;至少在短期内,确实如此。可以先把这些额外内容看成我们尚未理解的安全装备。编写小程序时,它们似乎全是多余的;但当我们开始构建大型程序时,就会逐渐理解并欣赏这些额外复杂度的价值。

打个比方,用 Python 编程有点像 Dan Osman 徒手攀登 Lover's Leap:速度可能很快,但也很危险。相比之下,Java 更像这段视频中那样使用绳索、头盔等安全设备。

代码风格、注释与 Javadoc

代码可以在许多方面体现美感:可以简洁,可以巧妙,也可以高效。初学者最容易忽视的方面之一是代码风格。刚开始编程时,人们往往只顾着让程序跑起来,而不会考虑以后是否还要重新阅读它,或者是否需要长期维护它。

本课程会非常重视代码的可读性。良好代码风格最重要的特征包括:

  • 风格一致,包括空格、变量命名、花括号风格等。
  • 大小适当,例如每行不要过宽,源文件不要过长。
  • 使用描述性名称,包括变量、函数和类。例如应使用 yeargetUserName,而不是 xf 之类缺乏含义的名称。
  • 避免重复代码:几乎不应出现两段内容大致相同、只在少数地方不同的重要代码块。
  • 在合适的位置写注释。Java 的单行注释使用 //,块注释(也叫多行注释)使用 /**/

黄金法则是:写出的代码应当让一个陌生人也能轻松理解。

这里是课程官方的代码风格指南,值得阅读。

为了让代码更简单、更容易真正理解,我们常常愿意接受轻微的性能损失。后续章节会重点展示一些例子。

注释

我们鼓励编写具有自解释性的代码,也就是选择能让人一眼看出程序意图的变量名和函数名。不过仅靠命名并不总是足够。例如,实现复杂算法时,可能仍需通过注释说明代码。注释应当谨慎使用。随着经验增长,并阅读更多他人的代码,你会逐渐形成判断:什么时候最适合添加注释。

特别需要注意的是,所有方法以及几乎所有类,都应使用所谓的 Javadoc 格式添加说明注释。Javadoc 注释的块注释开头会多一个星号,即 /**;它通常(但并非总是)还包含描述性标签。本书不会详细介绍这些标签,可以通过上面的链接了解其工作方式。

下面是不带标签的例子:

public class LargerDemo {
    /** Returns the larger of x and y. */           
    public static int larger(int x, int y) {
        if (x > y) {
            return x;
        }
        return y;
    }

    public static void main(String[] args) {
        System.out.println(larger(8, 10));
    }
}

广泛使用的 Javadoc 工具可以根据代码生成 HTML 说明文档。后续章节会看到相关例子。

接下来做什么

每章结尾都会列出使用当前已学内容可以完成的练习(如果有),并按照建议完成顺序排列。


1.2 对象

如果你以前没有使用过 Java,建议在阅读本章之前先完成 HW0 中的练习。它会介绍本书不再专门讲解的若干语法问题。

静态方法与非静态方法

静态方法

视频讲解

Java 中的所有代码都必须属于某个类(或者属于一种与类相似的结构,后面会学到)。大多数代码写在方法中。来看一个例子:

public class Dog {
    public static void makeNoise() {
        System.out.println("Bark!");
    }
}

如果尝试直接运行 Dog 类,只会得到一条错误信息:

$ java Dog
Error: Main method not found in class Dog, please define the main method as:
       public static void main(String[] args)

我们定义的 Dog 类本身不会执行任何事情。我们只是定义了 Dog 能够做的一件事,也就是发出叫声。要真正运行这个类,可以像 1.1 节那样给 Dog 添加一个 main 方法;也可以创建一个单独的 DogLauncher 类,用它来调用 Dog 类中的方法。例如:

public class DogLauncher {
    public static void main(String[] args) {
        Dog.makeNoise();
    }
}
$ java DogLauncher
Bark!

使用另一个类的类,有时称为那个类的“客户端”。因此,DogLauncherDog 的客户端。这两种技术没有绝对优劣:有些情况下,给 Dog 添加 main 方法更合适;另一些情况下,创建 DogLauncher 这样的客户端类更合适。随着课程中练习增多,两种方法各自的优势会逐渐变得清晰。

实例变量与对象实例化

视频讲解

狗与狗并不相同。有些狗会不停地尖叫,有些狗则会发出浑厚悠长的吼声,让所有听到这宏亮叫声的人心情愉悦。我们经常编写程序来模拟现实世界中的特征,而 Java 的语法正是为了方便这种模拟而设计的。

为了表示形形色色的狗,一种办法是为每一类狗分别创建一个类。

public class TinyDog {
    public static void makeNoise() {
        System.out.println("yip yip yip yip");
    }
}

public class MalamuteDog {
    public static void makeNoise() {
        System.out.println("arooooooooooooooo!");
    }
}

你以前应该已经见过:类可以被实例化,而实例可以保存数据。于是我们可以采用更自然的方法——创建 Dog 类的不同实例,并让 Dog 方法的行为取决于某一只具体 Dog 的属性。下面的类展示了这种做法:

public class Dog {
    public int weightInPounds;

    public void makeNoise() {
        if (weightInPounds < 10) {
            System.out.println("yipyipyip!");
        } else if (weightInPounds < 30) {
            System.out.println("bark. bark.");
        } else {
            System.out.println("woof!");
        }
    }    
}

下面是使用这种 Dog 的例子:

public class DogLauncher {
    public static void main(String[] args) {
        Dog d;
        d = new Dog();
        d.weightInPounds = 20;
        d.makeNoise();
    }
}

运行后,程序会创建一只体重为 20 磅的 Dog,随后这只狗会发出悦耳的 “bark. bark.”。

需要掌握的几个关键观察和术语如下:

  • Java 中的 Object(对象)是任意类的一个实例。
  • Dog 类拥有自己的变量,也称为_实例变量_或_非静态变量_。这些变量必须在类中声明;这与 Python 或 Matlab 等语言不同,后者允许在运行时添加新变量。
  • 我们在 Dog 类中创建的方法没有关键字 static。这种方法称为_实例方法_或_非静态方法_。
  • 要调用 makeNoise 方法,必须先使用 new 关键字_实例化_一只 Dog,然后让某个具体的 Dog 发出叫声。也就是说,调用的是 d.makeNoise(),而不是 Dog.makeNoise()
  • 对象实例化后,可以被_赋值_给一个已经_声明_且类型合适的变量,例如 d = new Dog();
  • 类中的变量和方法统称为类的_成员_。
  • 通过_点号记法_访问类的成员。

Java 中的构造方法

在面向对象语言中,我们通常使用_构造方法_来构造对象:

public class DogLauncher {
    public static void main(String[] args) {
        Dog d = new Dog(20);
        d.makeNoise();
    }
}

这里的实例化操作带有参数,省去了手动逐一给多个实例变量赋值的时间,也让代码更加整洁。要支持这种语法,只需像下面这样在 Dog 类中添加一个“构造方法”:

public class Dog {
    public int weightInPounds;

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

    public void makeNoise() {
        if (weightInPounds < 10) {
            System.out.println("yipyipyip!");
        } else if (weightInPounds < 30) {
            System.out.println("bark. bark.");
        } else {
            System.out.println("woof!");
        }    
    }
}

每当我们使用 new 关键字并传入一个整数参数来创建 Dog 时,签名为 public Dog(int w) 的构造方法就会被调用。如果你来自 Python,可以把构造方法理解为与 __init__ 方法非常相似的机制。

术语总结

视频讲解

数组实例化与对象数组

视频讲解

正如在 HW0 中看到的,Java 数组同样使用 new 关键字实例化。例如:

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;
    }
}

类似地,也可以创建用于保存已实例化对象的数组,例如:

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();
    }
}

注意,new 在这里以两种不同方式使用:第一次创建一个能够保存两个 Dog 对象的数组;之后两次分别创建真正的 Dog 对象。

类方法与实例方法

视频讲解

Java 允许我们定义两类方法:

  • 类方法,也就是静态方法。
  • 实例方法,也就是非静态方法。

实例方法是只能由类的某个具体实例执行的操作;静态方法则是由类本身执行的操作。两者分别适用于不同场景。例如,Math 类提供了静态方法 sqrt,因此可以这样调用:

x = Math.sqrt(100);

如果 sqrt 是实例方法,就必须使用下面这种笨拙的语法。幸运的是,sqrt 是静态方法,实际程序中不需要这样写。

Math m = new Math();
x = m.sqrt(100);

有时,一个类同时包含实例方法和静态方法是合理的。例如,假设我们希望比较两只狗。一种办法是添加一个用于比较 Dog 的静态方法:

public static Dog maxDog(Dog d1, Dog d2) {
    if (d1.weightInPounds > d2.weightInPounds) {
        return d1;
    }
    return d2;
}

例如可以这样调用:

Dog d = new Dog(15);
Dog d2 = new Dog(100);
Dog.maxDog(d, d2);

注意,这里使用类名调用方法,因为它是静态方法。

也可以把 maxDog 实现为非静态方法:

public Dog maxDog(Dog d2) {
    if (this.weightInPounds > d2.weightInPounds) {
        return this;
    }
    return d2;
}

上面使用关键字 this 表示当前对象。例如可以这样调用:

Dog d = new Dog(15);
Dog d2 = new Dog(100);
d.maxDog(d2);

这里是通过一个具体实例变量来调用方法的。

练习 1.2.1: 下面的方法会做什么?如果不确定,请亲自试一试。

public static Dog maxDog(Dog d1, Dog d2) {
    if (weightInPounds > d2.weightInPounds) {
        return this;
    }
    return d2;
}

静态变量

有时,让类拥有静态变量很有用。静态变量描述类本身固有的属性,而不是某个实例的属性。例如,可以记录狗的学名(二名法名称)是 “Canis familiaris”:

public class Dog {
    public int weightInPounds;
    public static String binomen = "Canis familiaris";
    ...
}

访问静态变量时,应当使用类名,而不是具体实例。例如应写 Dog.binomen,而不是 d.binomen

从语法上说,Java 的确允许通过实例名称访问静态变量,但这种写法风格很差、容易引起混淆;在我看来,这甚至是 Java 设计者留下的一个错误。

练习 1.2.2: 完成以下练习:

public static void main(String[] args)

视频讲解

学到这里,我们终于可以拆解并说明一直用于 main 方法的声明。逐部分来看:

  • public:到目前为止,我们的所有方法都以这个关键字开头。
  • static:它是静态方法,不与任何特定实例关联。
  • void:它没有返回值。
  • main:这是方法名称。
  • String[] args:这是传递给 main 方法的一个参数。

命令行参数

由于 main 是由 Java 解释器本身调用,而不是由另一个 Java 类调用,因此这些参数必须由解释器提供。它们通常就是命令行参数。请看下面的 ArgsDemo 程序:

public class ArgsDemo {
    public static void main(String[] args) {
        System.out.println(args[0]);
    }
}

该程序会输出第 0 个命令行参数。例如:

$ java ArgsDemo these are command line arguments
these

在上面的例子中,args 是一个字符串数组,其中各元素为 {"these", "are", "command", "line", "arguments"}

对命令行参数求和

练习 1.2.3: 假设所有命令行参数都是数字,尝试编写一个程序把它们相加。解答可以查看课程视频或 GitHub 上提供的代码。

使用库

视频讲解

程序员最重要的能力之一,是知道如何寻找并使用现有的库。在如今这个美好的时代,很多时候只要上网寻求帮助,就能省下大量编写代码和调试的工作。

本课程允许这样做,但有以下限制:

  • 不要使用课程没有提供的库。
  • 注明资料来源。
  • 不要搜索某道具体作业题或项目题的现成解答。

例如,可以搜索 “convert String integer Java”,但不可以搜索 “nbody project berkeley”。

有关合作与学术诚信政策的更多信息,请参阅课程教学大纲。

接下来做什么