前言:想改掉一些坏习惯吗?让咱们从 null、函数式编程以及 getter 和 setter 着手,看看如何改善代码。javascript
做为 Java 开发人员,咱们会使用一些习惯用法,典型的例子,如:返回 null 值、滥用 getter 和 setter,即便在没有必要的状况下也是如此。虽然在某些状况下,这些用法多是适当的,但一般是习惯使然,或者是咱们为了让系统正常工做的权宜之计。在本文中,咱们将讨论在 Java 初学者甚至高级开发人员中都常见的三种状况,并探究它们是如何给咱们带来麻烦的。应该指出的是,文中总结的规则并非不管什么时候都应该始终遵照的硬性要求。有时候,可能有一个很好的理由来使用这些模式解决问题,可是总的来讲,仍是应该相对的减小这些用法。首先,咱们将从 Null 这个关键字开始讨论,它也是 Java 中使用最频繁、但也是最具两面性的关键字之一。php
null 一直是开发者最好的朋友,也是最大的敌人,这在 Java 中也不例外。在高性能应用中,使用 null 是一种减小对象数量的可靠方法,它代表方法没有要返回的值。与抛出异常不一样,若是要通知客户端不能获取任何值,使用 null 是一种快速且低开销的方法,它不须要捕获整个堆栈跟踪。css
在高性能系统的环境以外,null 的存在会致使建立更繁琐的 null 返回值检查,从而破坏应用程序,并在解引用空对象时致使 NullPointerExceptions。在大多数应用程序中,返回 null 有三个主要缘由:java
表示列表中找不到元素;程序员
表示即便没有发生错误,也找不到有效值;web
表示特殊状况下的返回值。数据库
除非有任何性能方面的缘由,不然以上每一种状况都有更好的解决方案,它们不使用 null,而且强制开发人员处理出现 null 的状况。更重要的是,这些方法的客户端不会为该方法是否会在某些边缘状况下返回 null 而伤脑筋。在每种状况下,咱们将设计一种不返回 null 值的简洁方法。编程
No Elements(集合中没有元素的状况)api
在返回列表或其余集合时,一般会看到返回空集合,以代表没法找到该集合的元素。例如,咱们能够建立一个服务来管理数据库中的用户,该服务相似于如下内容(为了简洁起见,省略了一些方法和类定义):数组
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return null;
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
if (users != null) {
for (User user: users) {
System.out.println("User found: " + user.getName());
}
}
复制代码
由于咱们选择在没有用户的状况下返回 null 值,因此咱们迫使客户端在遍历用户列表以前先处理这种状况。若是咱们返回一个空列表来表示没有找到用户,那么客户端能够彻底删除空检查并像往常同样遍历用户。若是没有用户,则隐式跳过循环,而没必要手动处理这种状况;从本质上说,循环遍历用户列表的功能就像咱们为空列表和填充列表所作的那样,而不须要手动处理任何一种状况:
public class UserService {
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return Collections.emptyList();
}
else {
return Arrays.asList(usersFromDb);
}
}
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
for (User user: users) {
System.out.println("User found: " + user.getName());
}
复制代码
在上面的例子中,咱们返回的是一个不可变的空列表。这是一个可接受的解决方案,只要咱们记录该列表是不可变的而且不该该被修改(这样作可能会抛出异常)。若是列表必须是可变的,咱们能够返回一个空的可变列表,以下例所示:
public List<User> getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return new ArrayList<>(); // A mutable list
}
else {
return Arrays.asList(usersFromDb);
}
}
复制代码
通常来讲,当没有发现任何元素的时候,应遵照如下规则:
返回一个空集合(或 list、set、queue 等等)代表找不到元素。
这样作不只减小了客户端必须执行的特殊状况处理,并且还减小了接口中的不一致性(例如,咱们经常返回一个 list 对象,而不是其余对象)。
Optional Value(可选值)
不少时候,咱们但愿在没有发生错误时通知客户端不存在可选值,此时返回 null。例如,从 web 地址获取参数。在某些状况下,参数可能存在,但在其余状况下,它可能不存在。缺乏此参数并不必定表示错误,而是表示用户不须要提供该参数时包含的功能(例如排序)。若是没有参数,则返回 null;若是提供了参数,则返回参数值(为了简洁起见,删除了一些方法):
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public String getSortingValue() {
if (urlContainsSortParameter(url)) {
return extractSortParameter(url);
}
else {
return null;
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
String sortingParam = url.getSortingValue();
if (sortingParam != null) {
UserSorter sorter = UserSorter.fromParameter(sortingParam);
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
复制代码
当没有提供参数时,返回 null,客户端必须处理这种状况,可是在 getSortingValue 方法的签名中,没有任何地方声明排序值是可选的。若是方法的参数是可选的,而且在没有参数时,可能返回 null,要知道这个事实,咱们必须阅读与该方法相关的文档(若是提供了文档)。
相反,咱们可使可选性显式地返回一个 Optional 对象。正如咱们将看到的,当没有参数存在时,客户端仍然须要处理这种状况,可是如今这个需求已经明确了。更重要的是,Optional 类提供了比简单的 null 检查更多的机制来处理丢失的参数。例如,咱们可使用 Optional 类提供的查询方法(一种状态测试方法)简单地检查参数是否存在:
public class UserListUrl {
private final String url;
public UserListUrl(String url) {
this.url = url;
}
public Optional<String> getSortingValue() {
if (urlContainsSortParameter(url)) {
return Optional.of(extractSortParameter(url));
}
else {
return Optional.empty();
}
}
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
if (sortingParam.isPresent()) {
UserSorter sorter = UserSorter.fromParameter(sortingParam.get());
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}
复制代码
这与「空检查」的状况几乎相同,可是咱们已经明确了参数的可选性(即客户机在不调用 get() 的状况下没法访问参数,若是可选参数为空,则会抛出NoSuchElementException)。若是咱们不但愿根据 web 地址中的可选参数返回用户列表,而是以某种方式使用该参数,咱们可使用ifPresentOrElse 方法来这样作:
sortingParam.ifPresentOrElse(
param -> System.out.println("Parameter is :" + param),
() -> System.out.println("No parameter supplied.")
);
复制代码
这极大下降了「空检查」的影响。若是咱们但愿在没有提供参数时忽略参数,可使用 ifPresent 方法:
sortingParam.ifPresent(param -> System.out.println("Parameter is :" + param));
复制代码
在这两种状况下,使用 Optional 对象要优于返回 null 以及显式地强制客户端处理返回值可能不存在的状况,为处理这个可选值提供了更多的途径。考虑到这一点,咱们能够制定如下规则:
若是返回值是可选的,则经过返回一个 Optional 来确保客户端处理这种状况,该可选的值在找到值时包含一个值,在找不到值时为空
Special-Case Value(特殊状况值)
最后一个常见用例是特殊用例,在这种状况下没法得到正常值,客户端应该处理与其余用例不一样的极端状况。例如,假设咱们有一个命令工厂,客户端按期从命令工厂请求命令。若是没有命令能够得到,客户端应该等待 1 秒钟再请求。咱们能够经过返回一个空命令来实现这一点,客户端必须处理这个空命令,以下面的例子所示(为了简洁起见,没有显示一些方法):
public interface Command {
public void execute();
}
public class ReadCommand implements Command {
@Override
public void execute() {
System.out.println("Read");
}
}
public class WriteCommand implements Command {
@Override
public void execute() {
System.out.println("Write");
}
}
public class CommandFactory {
public Command getCommand() {
if (shouldRead()) {
return new ReadCommand();
}
else if (shouldWrite()) {
return new WriteCommand();
}
else {
return null;
}
}
}
CommandFactory factory = new CommandFactory();
while (true) {
Command command = factory.getCommand();
if (command != null) {
command.execute();
}
else {
Thread.sleep(1000);
}
}
复制代码
因为 CommandFactory 能够返回空命令,客户端有义务检查接收到的命令是否为空,若是为空,则休眠1秒。这将建立一组必须由客户端自行处理的条件逻辑。咱们能够经过建立一个「空对象」(有时称为特殊状况对象)来减小这种开销。「空对象」将在 null 场景中执行的逻辑(休眠 1 秒)封装到 null 状况下返回的对象中。对于咱们的命令示例,这意味着建立一个在执行时休眠的SleepCommand:
public class SleepCommand implements Command {
@Override
public void execute() {
Thread.sleep(1000);
}
}
public class CommandFactory {
public Command getCommand() {
if (shouldRead()) {
return new ReadCommand();
}
else if (shouldWrite()) {
return new WriteCommand();
}
else {
return new SleepCommand();
}
}
}
CommandFactory factory = new CommandFactory();
while (true) {
Command command = factory.getCommand();
command.execute();
}
复制代码
与返回空集合的状况同样,建立「空对象」容许客户端隐式处理特殊状况,就像它们是正常状况同样。但这并不老是可行的;在某些状况下,处理特殊状况的决定必须由客户作出。这能够经过容许客户端提供默认值来处理,就像使用 Optional 类同样。在 Optional 的状况下,客户端可使用 orElse 方法获取包含的值或默认值:
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElse("ASC");
复制代码
若是有一个提供的排序参数(例如,若是 Optional 包含一个值),这个值将被返回。若是不存在值,默认状况下将返回「ASC」。Optional 类还容许客户端在须要时建立默认值,以防默认建立过程开销较大(即只在须要时建立默认值):
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElseGet(() -> {
// Expensive computation
});
复制代码
结合「空对象」和默认值的用法,咱们能够设计如下规则:
若是可能,使用「空对象」处理使用 null 关键字的状况,或者容许客户端提供默认值
自从在 JDK 8 中引入了 stream 和 lambda 表达式以后,就出现了向函数式编程迁移的趋势,这理当如此。在 lambda 表达式和 stream 出现以前,执行函数式任务是很是麻烦的,而且会致使代码可读性的严重降低。例如,以下代码用传统方式过滤一个集合:
public class Foo {
private final int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Iterator<Foo> iterator = foos.iterator();
while(iterator.hasNext()) {
if (iterator.next().getValue() > 10) {
iterator.remove();
}
}
复制代码
虽然这段代码很紧凑,但它并无以一种明显的方式告诉咱们,当知足某个条件时,咱们将尝试删除集合的元素。相反,它告诉咱们,当集合中有更多的元素时将遍历集合,并将删除值大于 10 的元素(咱们能够假设正在进行筛选,可是删除元素的部分被代码的冗长所掩盖)。咱们可使用函数式编程将这个逻辑压缩为一条语句:
foos.removeIf(foo -> foo.getValue() > 10);
复制代码
这个语句不只比迭代方式更简洁,并且准确的告诉咱们它的行为。若是咱们为 predicate 命名并将其传递给 removeIf 方法,甚至可使其更具可读性:
Predicate<Foo> valueGreaterThan10 = foo -> foo.getValue() > 10;
foos.removeIf(valueGreaterThan10);
复制代码
这段代码的最后一行读起来像一个英语句子,准确地告诉咱们语句在作什么。对于看起来如此紧凑和极具可读性的代码,在任何须要迭代的状况下尝试使用函数式编程是很让人向往的,但这是一种天真的想法。并非每种状况都适合函数式编程。例如,若是咱们尝试在一副牌中打印一组花色和牌面大小的排列组合(花色和牌面大小的每一种组合),咱们能够建立如下内容(参见《Effective Java, 3rd Edition》得到这个示例的详细内容):
public static enum Suit {
CLUB, DIAMOND, HEART, SPADE;
}
public static enum Rank {
ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;
}
Collection<Suit> suits = EnumSet.allOf(Suit.class);
Collection<Rank> ranks = EnumSet.allOf(Rank.class);
suits.stream()
.forEach(suit -> {
ranks.stream().forEach(rank -> System.out.println("Suit: " + suit + ", rank: " + rank));
});
复制代码
虽然读起来并不复杂,但这种实现并非最简单的。很明显,咱们正试图强行使用 stream,而此时使用传统迭代明显更有利。若是咱们使用传统的迭代方法,咱们能够将 花色和等级的排列组合简化为:
for (Suit suit: suits) {
for (Rank rank: ranks) {
System.out.println("Suit: " + suit + ", rank: " + rank);
}
}
复制代码
这种风格虽然不那么浮华,但却直截了当得多。咱们能够很快地理解,咱们试图遍历每一个花色和等级,并将每一个等级与每一个花色配对。stream 表达式越大,函数式编程的乏味性就越明显。以Joshua Bloch 在《Effective Java, 3rd Edition》第 205 页,第 45 项中建立的如下代码片断为例,在用户提供的路径上查找字典中包含的指定长度内的全部词组:
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
}
复制代码
即便是经验最丰富的 stream 使用者也可能会对这个实现感到迷茫。短期内很难理解代码的意图,须要大量的思考才能发现上面的 stream 操做试图实现什么。这并不意味着 stream 必定很复杂或太冗长,只是由于它们不老是最好的选择。正如咱们在上面看到的,使用 removeIf 能够将一组复杂的语句简化为一个易于理解的语句。所以,咱们不该该试图用 stream 甚至 lambda 表达式替换传统迭代的每一个使用场景。相反,在决定是使用函数式编程仍是使用传统方式时,咱们应该遵循如下规则:
函数式编程和传统的迭代都有其优势和缺点:应该以简易性和可读性为准来选择
尽管在每一个可能的场景中使用 Java 最炫、最新的特性可能很让人向往,但这并不老是最好的方法。有时候,老式的功能效果反而最好。
新手程序员学到的第一件事是将与类相关的数据封装在私有字段中,并经过公共方法暴露它们。在实际使用时,经过建立 getter 来访问类的私有数据,建立 setter 来修改类的私有数据:
public class Foo {
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
复制代码
虽然这对于新程序员来讲是一个很好的学习实践,但这种作法不能未经思索就应用在中级或高级编程。在实际中一般发生的状况是,每一个私有字段都有一对 getter 和 setter 将类的内部内容暴露给外部实体。这会致使一些严重的问题,特别是在私有字段是可变的状况下。这不只是 setter 的问题,甚至在只有 getter 时也是如此。如下面的类为例,该类使用 getter 公开其惟一的字段:
public class Bar {
private Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
public Foo getFoo() {
return foo;
}
}
复制代码
因为咱们删除了 setter 方法,这么作可能看起来明智且无害,但并不是如此。假设另外一个类访问 Bar 类型的对象,并在 Bar 对象不知道的状况下更改 Foo 的底层值:
Foo foo = new Foo();
Bar bar = new Bar(foo);
// Another place in the code
bar.getFoo().setValue(-1);
复制代码
在本例中,咱们更改了 Foo 对象的底层值,而没有通知 Bar 对象。若是咱们提供的 Foo 对象的值破坏了 Bar 对象的一个不变量,这可能会致使一些严重的问题。举个例子,若是咱们有一个不变量,它表示 Foo 的值不多是负的,那么上面的代码片断将在不通知 Bar 对象的状况下静默修改这个不变量。当 Bar 对象使用它的 Foo 对象值时,事情可能会迅速向很差的方向发展,尤为是若是 Bar 对象假设这是不变的,由于它没有暴露 setter 直接从新分配它所保存的 Foo 对象。若是数据被严重更改,这甚至会致使系统失败,以下面例子所示,数组的底层数据在无心中暴露:
public class ArrayReader {
private String[] array;
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
public void read() {
for (String e: array) {
System.out.println(e);
}
}
}
public class Reader {
private ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = arrayReader;
}
public ArrayReader getArrayReader() {
return arrayReader;
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
reader.getArrayReader().setArray(null);
reader.read();
复制代码
执行此代码将致使 NullPointerException,由于当 ArrayReader 的实例对象试图遍历数组时,与该对象关联的数组为 null。这个 NullPointerException 的使人不安之处在于,它可能在对ArrayReader 进行更改好久以后才发生,甚至可能发生在彻底不一样的场景中(例如在代码的不一样部分中,甚至在不一样的线程中),这使得调试变得很是困难。
读者若是仔细考虑,可能还会注意到,咱们能够将私有的 ArrayReader 字段设置为 final,由于咱们在经过构造函数赋值以后,没有对它从新赋值的方法。虽然这看起来会使 ArrayReader 成为常量,确保咱们返回的 ArrayReader 对象不会被更改,但事实并不是如此。若是将 final 添加到字段中只能确保字段自己没有从新赋值(即,不能为该字段建立 setter)而不会阻止对象自己的状态被更改。或者咱们试图将 final 添加到 getter 方法中,这也是徒劳的,由于方法上的 final 修饰符只意味着该方法不能被子类重写。
咱们甚至能够更进一步考虑,在 Reader 的构造函数中防护性地复制 ArrayReader 对象,确保在将对象提供给 Reader 对象以后,传入该对象的对象不会被篡改。例如,应避免如下状况发生:
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
arrayReader.setArray(null); // Change arrayReader after supplying it to Reader
reader.read(); // NullPointerException thrown
复制代码
即便有了这三个更改(字段上增长 final 修饰符、getter 上增长 final 修饰符以及提供给构造函数的 ArrayReader 的防护性副本),咱们仍然没有解决问题。问题不在于咱们暴露底层数据的方式,而是由于咱们是在一开始就是错的。要解决这个问题,咱们必须中止公开类的内部数据,而是提供一种方法来更改底层数据,同时仍然遵循类不变量。下面的代码解决了这个问题,同时引入了提供的 ArrayReader 的防护性副本,并将 ArrayReader 字段标记为 final,由于没有 setter,因此应该是这样:
译注:原文的以下代码有一处错误,Reader 类中的 setArrayReaderArray 方法返回值类型应为 void,该方法是为了取代 setter,不该产生返回值。
public class ArrayReader {
public static ArrayReader copy(ArrayReader other) {
ArrayReader copy = new ArrayReader();
String[] originalArray = other.getArray();
copy.setArray(Arrays.copyOf(originalArray, originalArray.length));
return copy;
}
// ... Existing class ...
}
public class Reader {
private final ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = ArrayReader.copy(arrayReader);
}
public ArrayReader setArrayReaderArray(String[] array) {
arrayReader.setArray(Objects.requireNonNull(array));
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
reader.read();
Reader flawedReader = new Reader(arrayReader);
flawedReader.setArrayReaderArray(null); // NullPointerException thrown
复制代码
若是咱们查看这个有缺陷的读取器,它仍然会抛出 NullPointerException,但在不变量(读取时使用非空数组)被破坏时,会当即抛出该异常,而不是在稍后的某个时间。这确保了不变量的快速失效,这使得调试和找到问题的根源变得容易得多。
咱们能够进一步利用这一原则。若是不迫切须要更改类的状态,那么让类的字段彻底不可访问是一个好主意。例如,咱们能够删除全部可以修改 Reader 类实例对象状态的方法,实现 Reader 类的彻底封装:
public class Reader {
private final ArrayReader arrayReader;
public Reader(ArrayReader arrayReader) {
this.arrayReader = ArrayReader.copy(arrayReader);
}
public void read() {
arrayReader.read();
}
}
ArrayReader arrayReader = new ArrayReader();
arrayReader.setArray(new String[] {"hello", "world"});
Reader reader = new Reader(arrayReader);
// No changes can be made to the Reader after instantiation
reader.read();
复制代码
从逻辑上总结这个概念,若是可能的话,让类不可变是一个好主意。所以,在实例化对象以后,对象的状态永远不会改变。例如,咱们能够建立一个不可变的 Car 对象以下:
public class Car {
private final String make;
private final String model;
public Car(String make, String model) {
this.make = make;
this.model = model;
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
}
复制代码
须要注意的是,若是类的字段不是基本数据类型,客户端能够如前所述那样修改底层对象。所以,不可变对象应该返回这些对象的防护性副本,不容许客户端修改不可变对象的内部状态。可是请注意,防护性复制会下降性能,由于每次调用 getter 时都会建立一个新对象。对于这个缺陷,不该该过早地进行优化(忽视不可变性,以保证可能的性能提升),可是应该注意到这一点。下面的代码片断提供了一个方法返回值的防护性复制示例:
public class Transmission {
private String type;
public static Transmission copy(Transmission other) {
Transmission copy = new Transmission();
copy.setType(other.getType);
return copy;
}
public String setType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public class Car {
private final String make;
private final String model;
private final Transmission transmission;
public Car(String make, String model, Transmission transmission) {
this.make = make;
this.model = model;
this.transmission = Transmission.copy(transmission);
}
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public Transmission getTransmission() {
return Transmission.copy(transmission);
}
}
复制代码
这给咱们提示了如下原则:
使类不可变,除非迫切须要更改类的状态。不可变类的全部字段都应该标记为 private 和 final,以确保不会对字段执行从新赋值,也不会对字段的内部状态提供间接访问
不变性还带来了一些很是重要的优势,例如类可以在多线程上下文中轻松使用(即两个线程能够共享对象,而不用担忧一个线程会在另外一个线程访问该状态时更改该对象的状态)。总的来讲,在不少实际状况下咱们能够建立不可变的类,要比咱们意识到的要多不少,只是咱们习惯了添加了 getter 或 setter。
Conclusion(结论)
咱们建立的许多应用程序最终都能正常工做,可是在大量应用程序中,咱们无心引入的一些问题可能只会在最极端的状况下出现。在某些状况下,咱们作事情是出于方便,甚至是出于习惯,而不多注意这些习惯在咱们使用的场景中是否实用(或安全)。在本文中,咱们深刻研究了在实际应用中最多见的三种问题,如:空返回值、函数式编程的魅力、草率的 getter 和 setter,以及一些实用的替代方法。虽然本文中的规则不是绝对的,可是它们确实为一些在实际应用中遇到的罕见问题提供了看法,并可能有助于在从此避开一些费劲的问题。