3.2 编写Java代码
讨论Java所有相关的OOP功能后,就可以着手编写能够做些事情的类了。本节讨论一些可引导你完成这个过程的主题:
- 运算符;
- 普通的老式Java对象(POJO);
- 数组;
- 泛型和集合;
- 循环;
- 异常;
- 线程;
- lambda。
3.2.1 运算符
下表总结了Java语言中一些最重要的运算符。请注意,除这里列出的运算符外,Java还支持其他运算符。这里没有列出在其他所有流行的编程语言中都很常见的运算符,如+、-、>、>=、<和<=。
| 运算符 | 描述 |
|---|---|
value++
value--
| 返回value,再将value的值加1或减1 |
++value
--value
| 将value的值加1或减1,再返回value的新值 |
!
| 逻辑NOT运算符 |
%
| 计算除法运算的余数 |
instanceof
| 返回一个布尔值,指出传入的对象是否是指定类或接口的实例 |
==
!=
| 相等和不等 |
&&
||
| 逻辑AND和OR运算符 |
=
| 赋值 |
+=
-=
*=
/=
%=
| 这些运算符计算新值并将结果直接赋给变量 |
下面是一些运算符使用示例:
class OperatorDemo {public static void main(String[] args) {int i = 0;System.out.println(i++);System.out.println(++i);System.out.println(i += 10);}}
运行时,这些代码在控制台中打印如下输出:
0212
由于int变量i是在方法中声明的,因此必须显式地初始化(在这里,其初始值为0)方法中的第二行代码打印i的当前值(0),再默默地将其增加到1。接下来的一行代码将变量i的值加1,再打印它(因此打印的为2)。最后将i的值加10,并打印结果(12)。
3.2.2 条件检查
条件检查方式有两种:
if…else语句;switch…case语句。if…else语句
在Java中,if和if…else语句没有什么让人意外的地方。每个条件都必须返回一个布尔结果,而else部分是可选的:
if (condition) {} else if (condition) {} else {}
可像下面这样使用逻辑AND(&&)和OR (||)运算符来指定条件:
if (i > 25 || i == -1) {}
用于基本类型变量时,==运算符与预期一致,但用于对象时,它检查两个对象引用是否相同,而不是比较它们的内容(属性)。因此,要检查String变量的内容,必须使用来自java.lang.Object类并经过重写的方法equals(),而不能使用运算符==:
String foo = "hello";String bar = "world";if (!foo.equals(bar))System.out.println("Not equal!");
大多数IDE都能够识别将运算符
==用于String可能是错误的做法,并使用String的方法equals来重写代码。
switch…case语句
与很多其他的编程语言一样,Java也支持switch语句,下面是一个简单的示例:
int value = 3;String s = "";switch (value) {case 1:s = "One";break;case 2:case 3:s = "Two or three";break;default:s = "Something else";System.out.println(s);
对于switch语句的用法,有几点需要说明。
- 指定的表达式的结果可以是整数,也可以是字符串。
- 在
case语句中指定的值必须是在编译阶段能够确定的,因此非final变量不能用来指定这种值。 - 必须在
case语句中添加一条break语句来跳到switch语句末尾,否则将接着执行下一条case语句。
3.2.3 POJO
最初,没有支持Java语言的框架,开发人员大多自己编写类。在Java的发展过程中,引入了很多框架,而其中很多都要求类实现特定的接口或扩展框架提供的类。这导致类与特定的框架紧密耦合,而编写的代码无法在使用其他框架的项目中重用。对于这样的情形,并非每个人都感到满意,因此不久后一种新趋势开始流行:重回普通的老式Java对象(Plain Old Java Objects,POJO)。当前,很多流行的框架都支持POJO:

一个类如果满足如下条件,就被视为真正的POJO:
- 没有扩展类,也没有实现接口;
- 是可修改的;
- 包含一个不接受任何参数的公有构造函数;
- 使用公有方法来存储和获取私有变量的值。
即便需要扩展类或实现接口,遵守POJO的其他设计规则也是不错的选择。POJO不是必须遵守的规则,而是一种约定。
下面就是一个POJO类:
class POJO {private int value = 0;public POJO() {}public int getValue() {return value;}public void setValue(int value) {this.value = value;}}
在POJO类的实例中,可使用被称为属性的方法来存储和获取值。对于每个属性,都遵守如下约定:
- 其值存储在私有变量中;
- 有一个用于返回其值的公有获取方法;
- 有一个用于存储其值的公有设置方法。
获取方法(getter)的名称以get打头,后面通常是变量名。如果变量是布尔类型,获取方法通常以is(而不是get)打头。设置方法(setter)的名称通常以set打头,而不管变量是哪种类型。
通常,再添加一个公有的重载构造函数,它将POJO类的所有属性(这里是value)作为参数:
public POJO(int value) {this.value = value;}
在大多数IDE中,都只需单击一个按钮就能生成POJO或给既有POJO添加属性。
3.2.4 数组
Java提供了内置的数组支持。要声明数组,可在类型或变量名后添加[],并使用关键字new来创建数组并设置其长度:
int[] intArray1 = new int[2];int intArray2[] = new int[2];
在Java中,必须在创建数组时显式地设置其长度。对于基本类型数组,其元素的默认初始值为0(数值类型)或false(布尔类型),而对于引用类型数组,其元素的默认初始值为null。
与很多其他流行的编程语言一样,索引是从零开始的。在前面的示例中,数组intArray1和intArray2的索引可以是0或1。如果使用的索引超出了范围,将引发运行阶段异常:
intArray1[0] = 10;intArray1[1] = 20;
要获取数组的长度,可使用数组提供的只读变量length:
System.out.println(intArray1.length);
上述代码将在控制台中打印2。数组缺少一些方便的功能,例如,它们没有重写方法toString(),因此使用方法System.out.println打印数组变量intArray1,得到的将是Object提供的默认输出,如[I@659e0bfd,这没有提供有关数组内容的任何信息。
java.util包中有一个实用的Arrays类,这个类包含很多便利的静态方法。如果你要将数组转换为集合类的实例、在数组中查找元素、对数组进行排序等,建议你参阅API文档。下面是一个使用java.util.Arrays类的示例:
System.out.println(java.util.Arrays.toString(intArray1));
这将打印[10, 20]。
可在声明数组的同时对其进行初始化:
int[] intArray = { 10, 20, 30 };
在这个示例中,Java编译器将自动声明一个包含3个元素的int数组,并将每个元素都初始化为指定的值。
3.2.5 泛型和集合
前一章讨论了集合,因为它们对JVM来说非常重要。刚引入Java时,集合类只能存储java.lang.Object对象。鉴于JVM中的每个对象都可向上转换为java.lang.Object实例,因此集合一直能够存储任何类型的对象。这种灵活性也有缺点:要访问类成员,必须先向下转换对象。如果不小心添加了一个不同类型的对象,这可能导致运行阶段错误。下面的示例程序能够通过编译,但运行时将导致错误:
import java.util.ArrayList;class ClassCastExceptionExample {public static void main(String[] args) {ArrayList list = new ArrayList();list.add(new Integer(123));list.add("This is not an integer");Integer i = (Integer)list.get(0);i = (Integer)list.get(1); // 运行阶段异常!!!}}
不能将java.lang.String实例转换为java.lang.Integer实例,因此将第二个元素(索引为1)转换为整数时,引发了ClassCastException异常。
为确保某些类只能用于存储固定的类型(由开发人员指定),在Java语言中添加了泛型。例如,可创建一个只能存储java.lang.Integer实例的ArrayList对象,这样如果代码试图在这个ArrayList对象中添加其他对象,编译器将拒绝编译。泛型是一个复杂的主题,这里只讨论其基本用法。鉴于后面的示例将使用ArrayList,因此先来看下面的类图,其中显示了ArrayList实现的一些接口。

<E>表示相应的接口(和实现它的类)支持泛型。可将E视为使用List时将指定的类型的别名。根据约定,使用单个字母(这里是E)来表示元素。要声明一个只能存储java.lang.Integer实例的java.util.ArrayList实例,可使用一个指向List接口的引用:
import java.util.ArrayList;import java.util.List;class GenericsExample {public static void main(String[] args) {List<Integer> listWithIntegers = new ArrayList<>();listWithIntegers.add(new Integer(1));}}
为指定所需的类型(这里是Integer类),在接口类型List后面添加了。创建ArrayList的实例时,无需再指定Integer类,而只需添加<>即可。现在,如果你试图添加不能向上转换为Integer的类的实例,编译器将拒绝对代码进行编译。如果不使用泛型,这种错误只能到运行阶段才能检测到,因此程序员大都认为这是一种进步。
我们让引用类型变量listWithIntegers指向接口java.util.List,而不是ArrayList类,虽然并非必须这样做。java.util.List是一个泛型接口,ArrayList以及Collection API中的其他数据结构都实现了它。这样做是一种不错的约定,因为通过这样做,可将ArrayList替换为实现了接口java.util.List的其他数据结构,而无需对其他代码做任何修改。
在JVM领域,隐藏实现细节是一种非常好的设计选择,而接口和抽象类让这成为可能。
下面来看一个使用泛型HashMap的示例。HashMap是一个位于java.util包中的类,实现了更通用的接口java.util.Map。同样,这里也将把引用变量的类型声明为Map接口,以隐藏这样的设计信息,即我们使用的是HashMap类。我们先来看看接口Map:
public interface Map<K,V>
从中可知,Map支持泛型,且要求指定两种类型——K和V(它们分别代表键和值)。下面来创建一个将String键映射到Integer值的HashMap实例:
import java.util.HashMap;import java.util.Map;class GenericsExample {public static void main(String[] args) {Map<String, Integer> map = new HashMap<>();map.put("one", new Integer(1));map.put("ten", new Integer(10));System.out.println(map.get("one"));}}
这将向控制台打印Integer值1。
泛型只适用于对象,指定基本类型值将导致编译错误。处理泛型时,编译器不会将基本类型值自动装箱,也不会将对象自动拆箱。
3.2.6 循环
数组和集合很好,但仅当你能够遍历它们时才如此。Java提供了如下内置的循环结构:
for循环;while循环。
for循环
Java有两种for循环:
- 常规
for循环(使用计数器); - 改进的
for循环(用于对象)。
(1) 常规for循环
常规for循环的语法如下:
for (int i=0; i < 10; i++) {System.out.println(i);}
这将打印0-9,每个数字各占一行。
与其他语言中一样,for循环由三部分组成。
- 第一部分初始化计数器。
- 第二部分包含每次迭代开始前都将检查的表达式。如果这个表达式返回false,将停止迭代。
- 最后一部分是每次迭代后都将调用的语句。
每部分都是可选的,但分隔各部分的分号必不可少。一种不同寻常的结构是无限循环,可通过将每部分都设置为空来创建:
for (;;) {}
可使用关键字break来提前结束for循环。要结束当前迭代并进入下一次迭代,可使用关键字continue:
for (int i=0; i < 4; i++) {if (i == 1)continue;if (i == 3)break;System.out.println(i);}
上述for循环在i为1时跳到下一次迭代,并在i为3时结束,因此它只向控制台打印0和2。
(2) 改进的for循环
改进的for循环只能用于数组和实现了泛型接口java.lang.Iterable的对象。大多数Collection API类都实现了这个接口。下面是一个将这种for循环用于数组的示例:
String[] stringArray = { "One", "Two", "Three" };for (String s: stringArray) {System.out.println(s);}
这将打印One、Two和Three,每个字符串各占一行。推荐你尽可能使用这种for循环,因为这样可提高代码的可读性。
(3) while循环
在Java中,while循环类似于下面这样:
int i = 10;while (i < 10) {System.out.println(i);i++;}
其中的表达式的结果必须为布尔值。这个示例什么都不会打印,因为int变量i的值为10,导致表达式的结果为false,所以根本就不会进入循环。
与for循环一样,while循环也可使用关键字break来提前结束,还可使用关键字continue来跳到下一次迭代。
(4) do…while循环
do…while循环很像while循环,唯一的差别是它在迭代结束后再评估表达式。在任何情况下,都将进入这种循环:
int i = 10;do {System.out.println(i);i++;} while (i < 10);
这将打印10。在第一次迭代结束后对表达式进行评估;由于11大于10,因此表达式返回false,循环就此结束。
与for循环和while循环一样,do…while循环也可使用关键字break来提前结束,还可使用关键字continue来跳到下一次迭代。
3.2.7 异常
异常在前一章讨论过。要处理异常,必须将代码放在try和catch块之间。可指定多种要处理的异常。下面的类图列出了几种异常:

下面的示例代码故意引发了除零错误:
try {System.out.println(0/0);System.out.println("exit");} catch (NullPointerException e) {System.out.println("NULL POINTER EXCEPTION!");} catch (ArithmeticException e) {System.out.println(e.getMessage());e.printStackTrace();} catch (Exception e) {System.out.println("DIFFERENT ERROR");}
这将打印如下内容:
/ by zerojava.lang.ArithmeticException: / by zeroat JavaApplication8.main(JavaApplication8.java:4)
注意,没有打印文本exit,因为相应的语句还未执行就引发了异常。表达式0/0引发异常ArithmeticException后,JVM分析错误处理程序中的所有catch块。由于引发的异常ArithmeticException不是NullPointerException的子类,因此忽略第一个catch块。第二个catch块与这个异常完全匹配,因此执行这个catch块,打印这个异常的消息和栈跟踪。
如果引发的异常不是NullPointerException或ArithmeticException,也不是它们的子类,JVM将指定第三个catch块,因为异常通常是java.lang.Exception的子类。
如果这些catch块都与引发的异常不匹配,JVM将查看方法的调用者。如果其中有try…catch块,就对其进行分析。如果找到匹配的catch块,就执行它。如果调用者没有try…catch块,就查看调用者的调用者,直到找遍调用栈。如果没有try…catch块能够处理错误,程序将崩溃。
必须按正确的顺序放置catch块:将捕获最具体的异常类的catch块放在最前面,然后是捕获Exception子类的catch块,最后是捕获Exception本身的catch块。下面的示例不能通过编译:
try {Integer i = null;System.out.println(i.toString());} catch (Exception e) {// ...} catch (NullPointerException e) {// 不能通过编译!}
因为异常NullPointerException是Exception类的子类,Java编译器知道不可能到达捕获NullPointerException的catch块,所以拒绝编译这些代码。
可添加可选的finally块。在任何情况下,finally块中的语句都会执行,即便引发了异常:
try {throw new Exception("oops");} catch (Exception e) {System.out.println("Exception!");} finally {System.out.println("FINALLY!");}
这将打印如下内容:
Exception!FINALLY!
运行阶段异常
在方法可引发的异常方面,Java的要求不同寻常:方法可引发任何属于java.lang.RuntimeException的子类的异常。从本章前面的类图可知,NullPointerException和ArithmeticException都是RuntimeException的子类。
要引发不是java.lang.RuntimeException的子类的异常,方法必须显式地列出它们。来看一下接口java.sql.ResultSet的一个方法的声明:
public int getInt(String column) throws SQLException
这个方法将一个表示列名的String作为参数,并返回从数据库中检索到的int值。这个方法的签名告诉Java编译器,它可能引发SQLException异常,这个异常是java.lang.Exception的子类,但不是java.lang.RuntimeException的子类。现在,Java编译器知道,这个方法可能引发SQLException异常。
重写方法时,无论方法来自具体类、抽象类还是接口,均可:
- 选择不引发任何异常,为此可根本不添加关键字
throws; - 使用关键字
throws接受原始方法的全部或部分异常; - 将
throws列表中的部分或全部异常替换为其子类。
除非原始方法可能引发的异常为RuntimeException,否则重写后的方法不能引发其他异常。
3.2.8 线程
这里只介绍最简单的并发编程:启动多个线程。
在支持线程方面,接口java.lang.Runnable扮演着至关重要的角色。这个接口很简单,只包含一个方法:

如果一个类实现了这个接口并提供了方法run()的实现,就可在独立的线程中启动。来看一个简单的示例。
首先,有一个实现了接口Runnable的类,能够在不同的线程中运行:
class SleepyClass implements Runnable {private int number;public SleepyClass(int number) { this.number = number; }@Overridepublic void run() {System.out.println("Thread " + number + " started!");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread " + number + " ended!");}}
接下来,有另一个类,它在两个线程中运行前述代码:
public class ThreadsDemo {public static void main(String[] args) {Thread thread1 = new Thread(new SleepyClass(1));Thread thread2 = new Thread(new SleepyClass(2));thread1.start();thread2.start();}}
这个程序运行时,输出可能是这样的:
Thread 1 started!Thread 2 started!Thread 2 ended!Thread 1 ended!
对线程的启动顺序,JVM不做任何保证,即不保证线程1先开始、先结束。如果CPU内核足够多,可能每个线程都会在不同的CPU内核中运行。
SleepyClass类展示了Java开发人员经常会遇到的与异常处理相关的问题。Thread类的方法sleep可能引发异常InterruptedException,这种异常不是RuntimeException的子类。我们不能在方法run()的声明中添加throws InterruptedException,因为接口Runnable的方法run()不会引发这种异常或其子类。因此,我们选择在run()中处理这种异常。
并发编程很难。程序员必须确保多个线程不会同时修改同一个变量,否则可能引发竞态条件,因而导致数据受损和微妙的bug。之所以会出现这些问题,是因为对变量的操作大都不是原子性的,为完成一个操作必须执行多条CPU指令。在一个操作还在进行时,另一个线程将开始对同一个变量执行操作。
Java提供了一个用于方法的非访问限定符——synchronized,可确保方法不会被多个线程同时调用:其他线程等待当前线程调用完方法,再依次调用这个方法:
public synchronized void synchronizedMethod() {}
线程调用完方法后,JVM将确保相应的锁得以释放,即便方法引发了异常。
不推荐大量地使用限定符
synchronized,因为锁定线程是一种开销很大的操作,可能严重降低程序的性能。
如果你对并发编程感兴趣,请详细研究java.util.concurrent包中的类。
3.2.9 lambda
在Java 8新增的功能中,lambda可能是最受欢迎的,它们让你能够将函数像变量一样传递给方法。在本书介绍的很多语言中,都可使用lambda,因为很多其他的语言都提供了内置的lambda支持。
要向函数传递lambda,得先创建一个函数式接口(functional interface)。函数式接口是只包含一个抽象方法的普通接口。Java自带了大量支持lambda的接口,其中之一是接口java.lang.Runnable。由于这个接口只包含抽象方法run(),因此非常适合用来支持lambda。如果你只想运行一个线程,可直接向Thread实例传递一个匿名的lambda函数:
public class LambdaDemo {public static void main(String[] args) {Thread thread1 = new Thread( () -> {try {Thread.sleep(3000);} catch (InterruptedException e) {}});thread1.start();}}
乍一看,其中的语法可能令人迷惑。考虑到接口java.lang.Runnable的方法run()不接受任何参数,我们首先指定一对空括号(()),再指定一个箭头(->),它告诉编译器接下来是一个lambda。
在这个代码块中,像通常那样编写将使用线程来运行的函数的代码。
在下一章,我们将使用更复杂的接受参数的lambda。
