Java 开发者应该改掉的 3 种不良习惯

前言:想改掉一些坏习惯吗?让我们从 null、函数式编程以及 getter 和 setter 着手,看看如何改善代码。
作为 Java 开发人员,我们会使用一些习惯用法,典型的例子,如:返回 null 值、滥用 getter 和 setter,即使在没有必要的情况下也是如此。虽然在某些情况下,这些用法可能是适当的,但通常是习惯使然,或者是我们为了让系统正常工作的权宜之计。在本文中,我们将讨论在 Java 初学者甚至高级开发人员中都常见的三种情况,并探究它们是如何给我们带来麻烦的。应该指出的是,文中总结的规则并不是无论何时都应该始终遵守的硬性要求。有时候,可能有一个很好的理由来使用这些模式解决问题,但是总的来说,还是应该相对的减少这些用法。首先,我们将从 Null 这个关键字开始讨论,它也是 Java 中使用最频繁、但也是最具两面性的关键字之一。
1. Returning Null(返回 Null)
null 一直是开发者最好的朋友,也是最大的敌人,这在 Java 中也不例外。在高性能应用中,使用 null 是一种减少对象数量的可靠方法,它表明方法没有要返回的值。与抛出异常不同,如果要通知客户端不能获取任何值,使用 null 是一种快速且低开销的方法,它不需要捕获整个堆栈跟踪。
在高性能系统的环境之外,null 的存在会导致创建更繁琐的 null 返回值检查,从而破坏应用程序,并在解引用空对象时导致 NullPointerExceptions。在大多数应用程序中,返回 null 有三个主要原因:

  • 表示列表中找不到元素;

  • 表示即使没有发生错误,也找不到有效值;

  • 表示特殊情况下的返回值。
    除非有任何性能方面的原因,否则以上每一种情况都有更好的解决方案,它们不使用 null,并且强制开发人员处理出现 null 的情况。更重要的是,这些方法的客户端不会为该方法是否会在某些边缘情况下返回 null 而伤脑筋。在每种情况下,我们将设计一种不返回 null 值的简洁方法。
    No Elements(集合中没有元素的情况)
    在返回列表或其他集合时,通常会看到返回空集合,以表明无法找到该集合的元素。例如,我们可以创建一个服务来管理数据库中的用户,该服务类似于以下内容(为了简洁起见,省略了一些方法和类定义):

    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 来确保客户端处理这种情况,该可选的值在找到值时包含一个值,在找不到值时为空
3. Creating Indiscriminate Getters and Setters(滥用 getter 和 setter)
新手程序员学到的第一件事是将与类相关的数据封装在私有字段中,并通过公共方法暴露它们。在实际使用时,通过创建 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

使类不可变,除非迫切需要更改类的状态。不可变类的所有字段都应该标记为 private 和 final,以确保不会对字段执行重新赋值,也不会对字段的内部状态提供间接访问

不变性还带来了一些非常重要的优点,例如类能够在多线程上下文中轻松使用(即两个线程可以共享对象,而不用担心一个线程会在另一个线程访问该状态时更改该对象的状态)。总的来说,在很多实际情况下我们可以创建不可变的类,要比我们意识到的要多很多,只是我们习惯了添加了 getter 或 setter。
Conclusion(结论)
我们创建的许多应用程序最终都能正常工作,但是在大量应用程序中,我们无意引入的一些问题可能只会在最极端的情况下出现。在某些情况下,我们做事情是出于方便,甚至是出于习惯,而很少注意这些习惯在我们使用的场景中是否实用(或安全)。在本文中,我们深入研究了在实际应用中最常见的三种问题,如:空返回值、函数式编程的魅力、草率的 getter 和 setter,以及一些实用的替代方法。虽然本文中的规则不是绝对的,但是它们确实为一些在实际应用中遇到的罕见问题提供了见解,并可能有助于在今后避开一些费劲的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值