每个Java开发人员都应该停止做的三件事

从返回空值到过度使用getter和setter,甚至Java程序员都习惯于使用成语,即使在不需要的时候也是如此。尽管它们在某些情况下可能是适当的,但它们通常是使系统正常运行的习惯或后备力量。在本文中,我们将遍历Java开发人员(无论是新手还是高级)中的三件事,并探讨它们如何使我们陷入困境。应该注意的是 ,无论如何,这些并不是始终应遵守的严格规则。有时,使用这些模式来解决问题可能有充分的理由,但是总的来说,它们的使用量应该比现在少得多。首先,我们将以Java中最多产但双刃的关键字开始:Null。

1.返回零

空值一直是开发人员的最好的朋友,也是最大的敌人,Java中的空值也不例外。在高性能应用程序中,空值可以是减少对象数量并发出方法无返回值的可靠方式。与抛出异常(在创建异常时必须捕获整个堆栈跟踪)相反,null是一种快速,低开销的方式,向客户端发出无法获取任何值的信号。
在高性能系统之外,通过为空返回值创建更繁琐的检查并NullPointerException在取消引用空对象时引起s ,空可能会对应用程序造成破坏 。在大多数应用程序中,返回空值的原因主要有以下三个:(1)表示找不到列表元素;(2)表示即使没有发生错误,也找不到有效值;或(3) )表示特殊情况下的返回值。
除非有任何性能原因,上述每种情况都有一个更好的解决方案,该解决方案不使用null并强制开发人员处理null情况。此外,这些方法的客户也不会ing之以鼻,想知道该方法在某些情况下是否会返回null。在每种情况下,我们将设计一种更干净的方法,该方法不涉及返回空值。
没有元素
返回列表或其他集合时,通常会看到返回一个空集合,以表示找不到该集合的元素。例如,我们可以创建一个服务来管理数据库中的用户,该服务类似于以下内容(为简便起见,省略了一些方法和类定义):
public class UserService {
public List 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 = service.getUsers();
if (users != null) {
for (User user: users) {
System.out.println("User found: " + user.getName());
}
}

由于我们选择了在没有用户的情况下返回空值,因此我们迫使客户端在遍历用户列表之前先处理这种情况。如果相反,我们返回了一个空列表以表示未找到用户,则客户端可以完全删除null检查并照常遍历用户。如果没有用户,则循环将被隐式跳过,而无需手动处理。从本质上讲,遍历用户列表的功能与我们想要的空列表和填充列表的功能相同,而无需手动处理一种情况或另一种情况:
public class UserService {
public List 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 = service.getUsers();
for (User user: users) {
System.out.println("User found: " + user.getName());
}

在上述情况下,我们选择返回一个不变的空列表。只要我们证明该列表是不可变的,并且不应该对其进行修改(这样做可能会引发异常),则这是可接受的解决方案。如果列表必须可变,我们可以返回一个空的可变列表,如以下示例所示:
public List getUsers() {
User[] usersFromDb = getUsersFromDatabase();
if (usersFromDb == null) {
// No users found in database
return new ArrayList<>(); // A mutable list
}
else {
return Arrays.asList(usersFromDb);
}
}

通常,在发出找不到元素的信号时,应遵循以下规则:
返回一个空集合(或列表,集合,队列等),表示找不到任何元素
这样做不仅减少了客户端必须执行的特殊情况处理,而且还减少了我们界面中的不一致情况(即,有时返回一个列表对象,而不返回其他对象)。
可选值
很多时候,当我们希望通知客户不存在可选值但没有发生错误时,会返回空值。例如,从网址获取参数。在某些情况下,该参数可能存在,但在其他情况下,可能没有。缺少此参数并不一定表示错误,而是表示用户不希望提供该参数时所包含的功能(例如排序)。我们可以通过以下方式处理此问题:如果不存在任何参数,则返回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 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 sortingParam = url.getSortingValue();
if (sortingParam.isPresent()) {
UserSorter sorter = UserSorter.fromParameter(sortingParam.get());
return userService.getUsers(sorter);
}
else {
return userService.getUsers();
}

这几乎与null检查的情况相同,但是我们已经明确说明了参数的可选性(即,客户端不能在不调用的情况下访问参数get(),NoSuchElementException 如果可选为空,则会抛出a )。如果我们不希望基于网址中的可选参数返回用户列表,而是以某种方式使用该参数,则可以使用该 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 ,如果找到一个则返回一个包含值的值,如果找不到则返回一个 空值
特殊情况值
最后一个常见用例是特殊情况,即无法获得正常值,并且客户应处理与其他情况不同的特殊情况。例如,假设我们有一个命令工厂,客户机定期从中请求命令来完成。如果没有命令准备就绪,则客户端应等待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秒钟。这将创建一组条件逻辑,客户端必须自行处理。我们可以通过创建一个空对象 (有时称为特例对象)来减少这种开销。空对象将原本在空场景中执行的逻辑(即休眠1秒)封装到一个以空情况返回的对象中。对于我们的命令示例,这意味着创建一个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 sortingParam = url.getSortingValue();
String sort = sortingParam.orElse(“ASC”);

如果有提供的排序参数(即,如果 Optional 包含一个值),则将返回此值。如果不存在任何值, “ASC” 则默认情况下将返回。的 Optional 类还允许客户端创建需要时的默认值,在情况下,默认创建过程是昂贵的(即,默认将仅创建需要时):
UserListUrl url = new UserListUrl(“http://localhost/api/v2/users”);
Optional sortingParam = url.getSortingValue();
String sort = sortingParam.orElseGet(() -> {
// Expensive computation
});

结合使用空对象和默认值,我们可以设计以下规则:
在可能的情况下,使用空对象处理空个案,或允许客户端提供默认值

2.默认为函数式编程

自从Java开发工具包(JDK)8中引入了流和lambda以来,就一直在向功能编程迁移,这一点是正确的。在使用lambda和stream之前,执行简单的功能任务很麻烦,并且导致代码严重无法阅读。例如,以传统风格过滤集合会产生类似于以下内容的代码:
public class Foo {
private final int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Iterator iterator = foos.iterator();
while(iterator.hasNext()) {
if (iterator.next().getValue() > 10) {
iterator.remove();
}
}

尽管这段代码很紧凑,但是如果满足某些条件,它不会以明显的方式告诉我们要删除集合中的元素。相反,它告诉我们,当集合中有更多元素时,我们正在迭代一个集合,如果每个元素的值大于10,则将其删除(我们可以推测正在发生过滤,但是在代码的冗长性中它被遮盖了) 。我们可以使用函数式编程将此逻辑缩减为一个语句:
foos.removeIf(foo -> foo.getValue() > 10);

该语句不仅比其迭代替代方案简洁得多,而且还准确地告诉我们它正在尝试执行的操作。如果我们命名谓词并将其传递给removeIf 方法,我们甚至可以使其更具可读性 :
Predicate valueGreaterThan10 = foo -> foo.getValue() > 10;
foos.removeIf(valueGreaterThan10);

该摘要的最后一行看起来像是英语中的句子,可 将该语句的执行情况准确告知我们 。由于代码看起来如此紧凑和易读,因此很容易在 需要迭代的每种情况下尝试和使用函数式编程 ,但这是幼稚的哲学。并非每种情况都适合进行功能编程。例如,如果我们尝试在一组纸牌(每个西装和军衔的组合)中打印这套西装和军衔的叉积,我们可以创建以下内容(有关以下内容的详细列表,请参见Effective Java,第3版)这个例子):
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 suits = EnumSet.allOf(Suit.class);
Collection ranks = EnumSet.allOf(Rank.class);
suits.stream()
.forEach(suit -> {
ranks.stream().forEach(rank -> System.out.println("Suit: " + suit + ", rank: " + rank));
});

尽管阅读起来并不复杂,但这并不是我们可以设计的最直接的实现。显然,我们正在尝试将流引入传统迭代更为有利的领域。如果使用传统迭代,则可以将西服和军衔的叉积简化为以下内容:
for (Suit suit: suits) {
for (Rank rank: ranks) {
System.out.println("Suit: " + suit + ", rank: " + rank);
}
}

这种样式虽然不那么浮华,但更加直接。我们可以很快地看到,我们正在尝试遍历每件西服,并对每件西服进行等级排序和配对。流表达式越大,函数式编程的乏味就越明显。以Joshua Bloch在Effective Java,第3版(第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 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);
}
}
}

即使是经验最丰富的流支持者,也可能会对这种实现方式感到沮丧。代码的意图尚不清楚,需要花费大量的精力才能发现上述流操作试图实现的目的。这并不意味着流很复杂或太罗word,但是它们并不 总是 最佳选择。正如我们在上面看到的,将 removeIf 简化后的一组语句简化为一个易于理解的语句。因此,我们不应该尝试 用流甚至lambda 代替 传统迭代的每个实例。相反,在决定是进行函数编程还是使用传统途径时,我们应遵循以下规则:
函数式编程和传统迭代都有其优点和缺点:使用任何一种都会导致最简单和最易读的代码
尽管在每种可能的情况下都可能会尝试使用Java最华丽,最新的功能,但这并不总是最好的方法。有时,老式功能最有效。

3.创建不加选择的获取器和设置器

新手程序员要教的第一件事是将与类关联的数据封装在私有字段中,并通过公共方法公开它们。在实践中,这将导致创建获取器以访问类的私有数据,并创建设置器以修改类的私有数据:
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;
}
}

由于我们明智地限制删除了二传手方法,因此这种暴露似乎是无害的,但距离它还很遥远。假设另一个类访问类型的对象 Bar 并在Foo 不Bar 知道该对象的情况下 更改其基础值 :
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 对象 假定 不变量因未暴露设置器而直接将其重新赋值时,它就保持不变。 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 到字段只能确保不重新分配字段本身(即,我们无法为该字段创建设置器)。它不会阻止对象本身的状态被更改。如果我们尝试添加 final 到getter方法中,那么这也是徒劳的,因为 final 方法上的修饰符仅意味着该方法不能被子类覆盖。
我们甚至可以更进一步和防守复制的 ArrayReader 对象的构造函数Reader,确保已传递到对象的对象不能与已经供给之后被篡改 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 字段上的final 修饰符,getter上的 修饰符以及ArrayReader 提供给构造函数的防御性副本 ),我们仍然没有解决问题。 我们如何公开类的基础数据并没有发现问题 ,而实际上我们是在这样做的。为了解决这个问题,我们必须停止公开类的内部数据,而是提供一种方法来更改基础数据,同时仍要遵守类不变式。以下代码解决了这个问题,同时引入了提供的防御性副本 ArrayReader 并标记了 ArrayReader 字段的final,这是正确的,因为没有设置器:
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 仍然会抛出a,但是当不变量(读取时使用非空数组)被破坏时,它将立即抛出,而不是在以后的某个时间。这样可以确保不变式快速发生故障,从而使调试和查找问题根源变得更加容易。
我们可以将这一原理更进一步,并指出,如果没有迫切需要允许更改类状态的方法,最好使类的字段完全不可访问。例如,我们可以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);
}
}

这使我们具有以下原则:
使类不可变,除非迫切需要更改类的状态。不变类的所有字段都应标记为私有和最终字段,以确保不对字段执行任何重新分配,并且不应该提供对字段内部状态的间接访问
不变性还带来了一些非常重要的优势,例如该类易于在多线程上下文中使用的能力(即两个线程可以共享对象,而不必担心一个线程会改变对象的状态,而另一个线程则可以担心)线程正在访问该状态)。通常,我们可以创建不可变类的实例比起初要实现的实例多:很多时候,我们出于习惯添加了getter或setter。

结论

我们创建的许多应用程序最终都可以运行,但是在许多应用程序中,我们引入了一些隐秘的问题,这些问题往往在最坏的情况下蔓延开来。在某些情况下,我们出于便利甚至出于习惯来做事情,并且在我们使用它们的情况下根本不在乎这些习惯用法是否实用(或安全)。在本文中,我们研究了其中三种最常见的做法,例如空返回值,对函数编程的亲和力,粗心的getter和setter以及一些实用的替代方法。尽管本文中的规则不应被视为绝对规则,但它们确实提供了对常见做法的罕见危险的一些见解,并可能有助于避免将来的繁琐错误。

最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

发布了76 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhaozihao594/article/details/104144627