Java最佳实践

内容纲要

img

计算机编程中,最佳实践是许多开发人员遵循的一组非正式规则,以提高软件质量、可读性和可维护性。在应用程序长时间保持使用的情况下,最佳实践尤其有益,这样它最初是由一个团队开发的,然后由不同的人组成的维护团队进行维护。

本教程将提供Java最佳实践的概述,以及每个条目的解释,包括Java编程的顶级最佳实践列表中的每一项。

Java编程最佳实践概览

虽然Java最佳实践的完整列表可能很长,但对于那些正在努力提高代码质量的编码人员来说,有几个被认为是一个很好的起点,包括使用适当的命名规范、使类成员私有化、避免使用空的catch块、避免内存泄漏以及正确地注释代码块:

  • 使用适当的命名规范
  • 类成员设置为私有
  • 在长数字文字中使用下划线
  • 避免空的catch
  • 使用StringBuilderStringBuffer进行字符串连接
  • 避免冗余初始化
  • 使用增强型for循环代替带计数器的for循环
  • 正确处理空指针异常
  • FloatDouble:哪一个是正确的选择?
  • 使用单引号和双引号
  • 避免内存泄漏
  • 返回空集合而不是返回Null元素
  • 高效使用字符串
  • 避免创建不必要的对象
  • 正确注释代码

Java中的类成员应该是私有的

在Java中,类的成员越不可访问,越好!第一步是使用private访问修饰符。目标是促进理想的封装,这是面向对象编程(OOP)的基本概念之一。太多时候,新的开发人员没有正确地为类分配访问修饰符,或者倾向于将它们设置为public以使事情更容易。

考虑以下字段被设置为public的类:

public class BandMember {
  public String name;
  public String instrument;
}

在这里,类的封装性被破坏了,因为任何人都可以直接更改这些值,如下所示:

BandMember billy = new BandMember();
billy.name = "George";
billy.instrument = "drums";

使用private访问修饰符与类成员一起可以将字段隐藏起来,防止用户通过setter方法之外的方式更改数据:

public class BandMember {
  private String name;
  private String instrument;

  public void setName(String name) {
    this.name = name;
  }
  public void setInstrument(String instrument)
    this.instrument = instrument;
  }
}

setter方法中也是放置验证代码和/或管理任务(如增加计数器)的理想位置。

在长数字文字中使用下划线

得益于Java 7的更新,开发人员现在可以在长数字字面量中使用下划线(_),以提高可读性。以下是在允许下划线之前一些长数字字面量的示例:

int minUploadSize = 05437326;
long debitBalance = 5000000000000000L;
float pi = 3.141592653589F;

我想您会同意下划线使值更易读:

int minUploadSize = 05_437_326;
long debitBalance = 5_000_000_000_000_000L;
float pi = 3.141_592_653_589F;

避免空的Catch块

在Java中,把catch块留空是非常不好的习惯,有两个原因:一是它可能导致程序默默地失败,二是程序可能会继续运行而不会发生任何异常。这两种结果都会使调试变得非常困难

考虑以下程序,它从命令行参数中计算两个数字的和:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;

    try {
      a = Integer.parseInt(args[0]);
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
    }

    int sum = a + b;

    System.out.println(a + " + " + b + " = " + sum);
  }
}

Java的parseInt()方法会抛出NumberFormatException异常,需要开发人员在其调用周围使用try/catch块来捕获异常。不幸的是,这位开发人员选择忽略抛出的异常!因此,传入无效参数,例如“45y”,将导致关联的变量被赋为其类型的默认值,对于int类型来说是0

img

通常,在捕获异常时,程序员应采取以下三个行动中的一个或多个:

  1. 开发人员最起码应该通知用户异常情况,要么让他们重新输入无效的值,要么让他们知道程序必须提前终止。
  2. 使用 JDK LoggingLog4J 记录异常日志。
  3. 将异常封装并作为一个新的、更适合应用程序的异常重新抛出。

以下是重新编写后的Sum应用程序,用于通知用户输入无效并因此终止程序:

public class Sum {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;

    try {
      a = Integer.parseInt(args[0]);
    } catch (NumberFormatException ex) {
      System.out.println(args[0] + " is not a number. Aborting...");
      return;
    }

    try {
      b = Integer.parseInt(args[1]);
    } catch (NumberFormatException ex) {
      System.out.println(args[1] + " is not a number. Aborting...");
      return;
    }

    int sum = a + b;

    System.out.println(a + " + " + b + " = " + sum);
  }
}

以下是我们观察到的结果:

img

使用StringBuilder或StringBuffer进行字符串拼接

“+” 运算符是在 Java 中快速和简便地组合字符串的方法。在 Hibernate 和 JPA 时代之前,对象是手动持久化的,通过从头开始构建 SQL INSERT 语句来实现!以下是一个存储一些用户数据的示例:

String sql = "Insert Into Users (name, age)";
       sql += " values ('" + user.getName();
       sql += "', '" + user.getage();
       sql += "')";

很遗憾,当像上面那样连接多个字符串时,Java编译器必须创建多个中间字符串对象,然后将它们合并成最终连接的字符串。

相反,我们应该使用StringBuilderStringBuffer类。两者都包含函数,可以连接字符串而无需创建中间String对象,从而节省处理时间和不必要的内存使用。

以前的代码可以使用 StringBuilder 重写,如下所示:

StringBuilder sqlSb = new StringBuilder("Insert Into Users (name, age)");
sqlSb.append(" values ('").append(user.getName());
sqlSb.append("', '").append(user.getage());
sqlSb.append("')");
String sqlSb = sqlSb.toString();

这对开发者来说可能要费点事儿,但是这样做非常值得!

StringBuffer 与 StringBuilder

虽然StringBufferStringBuilder类都比“+”运算符更可取,但它们并不相同。StringBuilderStringBuffer更快,但不是线程安全的。因此,在非多线程环境中进行字符串操作时,应使用StringBuilder;否则,请使用StringBuffer类。

避免冗余初始化

尽管某些语言如TypeScript强烈建议在声明时初始化变量,但在Java中并非总是必要的,因为它在声明时将默认初始化值(如0falsenull)分配给变量。

因此,Java的最佳实践是要知道成员变量的默认初始化值,除非您想将它们设置为除默认值以外的其他值,否则不要显式初始化变量。

以下是一个计算从11000的自然数之和的短程序。请注意,只有部分变量被初始化:

class VariableInitializationExample {
  public static void main(String[] args) {

    // automatically set to 0
    int sum;
    final numberOfIterations = 1000;

    // Set the loop counter to 1
    for (int i = 1; i &= numberOfIterations; ++i) {
      sum += i;
    }

    System.out.println("Sum = " + sum);
  }
}

使用增强型for循环代替需要计数器的for循环

尽管 for 循环在某些情况下很有用,但是计数器变量可能会引起错误。例如,计数器变量可能会在稍后的代码中被无意中更改。即使从 1 而不是从 0 开始索引,也可能导致意外行为。出于这些原因,for-each 循环(也称为增强型 for 循环)可能是更好的选择。

考虑以下代码:

String[] names = {"Rob", "John", "George", "Steve"};
for (int i = 0; i < names.length; i++) {
  System.out.println(names[i]);
}

在这里,变量i既是循环计数器,又是数组names的索引。尽管这个循环只是打印每个名称,但如果下面有修改i的代码,就会变得棘手。我们可以通过使用下面所示的for-each循环轻松避开整个问题:

for (String name : names) {
  System.out.println(name);
}

使用增强的for循环,出错的机会要少得多!

合理处理空指针异常

空指针异常在Java中是一个非常常见的问题,可能是由于其面向对象的设计所致。当您试图在 Null 对象引用上调用方法时,就会发生 Null Pointer 异常。这通常发生在在类实例化之前调用实例方法的情况下,如下例所示:

Office office;

// later in the code...
Employee[] employees = office.getEmployees();

虽然你无法完全消除 Null Pointer Exceptions,但有方法可以将其最小化。一种方法是在调用对象的方法之前检查对象是否为 Null。以下是使用三元运算符的示例:

Office office;

// later in the code...
Employee[] employees = office == null ? 0 : office.getEmployees();

你可能还想抛出自己的异常:

Office office;
Employee[] employees;

// later in the code...
if (office == null) {
  throw new CustomApplicationException("Office can't be null!");
} else {
  employees = office.getEmployees();
}

Float或Double:应该使用哪个?

浮点数和双精度数是相似的类型,因此许多开发人员不确定该选择哪种类型。两者都处理浮点数,但具有非常不同的特性。例如,float的大小为32位,而double分配了64位的内存空间,因此double可以处理比float大得多的小数。然后有一个精度问题:float只能容纳7位精度。极小的指数大小意味着一些位是不可避免的丢失的。相比之下,double为指数分配了更多的位数,允许它处理高达15位精度。

因此,当速度比准确性更重要时,通常建议使用float。尽管大多数程序不涉及大量计算,但在数学密集型应用中,精度差异可能非常显著。当需要的小数位数已知时,float也是一个不错的选择。当精度非常重要时,double应该是你的首选。只需记住,Java强制使用double作为处理浮点数的默认数据类型,因此您可能需要附加字母"f"来明确表示float,例如,1.2f

单引号和双引号在字符串连接中的使用

在Java中,双引号(“)用于保存字符串,单引号用于字符(由char类型表示)。当我们尝试使用+连接运算符连接字符时,可能会出现问题。问题在于使用+连接字符会将char的值转换为ascii,从而产生数字输出。以下是一些示例代码,以说明这一点:

char a, b, c;
a = 'a';
b = 'b';
c = 'c';

str = a + b + c; // not "abc", but 294!

就像字符串连接最好使用StringBuilderStringBuffer类一样,字符连接也是如此!在上面的代码中,变量abc可以通过以下方式组合成一个字符串:

new StringBuilder().append(a).append(b).append(c).toString()

我们可以在下面观察到期望的结果:

img

避免Java中的内存泄漏

在Java中,开发人员并没有太多关于内存管理的控制权,因为Java通过垃圾回收自动地进行内存管理。尽管如此,有一些Java最佳实践可以帮助开发人员避免内存泄漏,例如:

  • 避免创建不必要的对象。
  • 避免使用"+"运算符进行字符串连接。
  • 避免在会话中存储大量数据。
  • 在会话不再使用时,及时让会话超时。
  • 避免使用静态对象,因为它们在整个应用程序的生命周期内存在。
  • 在与数据库交互时,不要忘记在finally块中关闭ResultSetStatementsConnection对象。

返回空集合而不是null引用。

你知道吗,null引用经常被称为软件开发中最严重的错误吗?1965年,Tony Hoare在设计面向对象语言(OOP)的引用的第一个全面类型系统时发明了null引用。后来在2009年的一次会议上,Hoare为自己的发明道歉,承认他的创造“导致了无数的错误、漏洞和系统崩溃,在过去四十年中可能造成了数十亿美元的痛苦和损失。”

在Java中,通常最好返回空值而不是null,特别是当返回集合、可枚举对象或对象时更为重要。尽管你自己的代码可能会处理返回的null值,但其他开发人员可能会忘记编写空值检查,甚至没有意识到null是可能的返回值!

以下是一些Java代码,它以ArrayList的形式获取库存中的书籍列表。但是,如果列表为空,则返回一个空列表:

private final List<Book> booksInStock = ...

public List<Book> getInStockBooks() {
  return booksInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(booksInStock);
}

这使得方法的调用者可以在不必首先检查null引用的情况下迭代列表:

(Book book: getInStockBooks()) {
 // do something with books
}

Java 中字符串的高效使用

我们已经讨论了使用 + 连接操作符可能产生的副作用,但还有其他一些方法可以更有效地使用字符串,以避免浪费内存和处理器周期。例如,在实例化 String 对象时,通常最好直接创建 String 而不是使用构造函数。原因是什么?使用直接创建 String 比使用构造函数更快(更不用说更少的代码!)。

这里是在Java中创建字符串的两种等效方式:直接创建和使用构造函数:

// directly
String str = "abc";
// using a constructor
char data[] = {'a', 'b', 'c'};
String str = new String(data);

虽然两种方法都是等效的,但直接创建字符串的方式更好。

Java中不必要的对象创建

你知道吗?在Java中,对象的创建是消耗内存最多的操作之一。因此,在没有充分理由的情况下,最好避免创建对象,并仅在绝对必要时才这样做。

那么,如何将这个实践起来呢?具有讽刺意味的是,像我们上面看到的直接创建字符串就是避免不必要创建对象的一种方式!

以下是一个更复杂的示例:

以下是一个Person类的例子,它包括一个isBabyBoomer()方法,用于判断此人是否属于“婴儿潮”年龄段,出生于1946年至1964年之间:

public class Person {
  private final Date birthDate;

  public boolean isBabyBoomer() {
    // Unnecessary allocation of expensive object!
    Calendar gmtCal =
        Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();

    return birthDate.compareTo(boomStart) >= 0 &&
           birthDate.compareTo(boomEnd)   <  0;
  }
}

isBabyBoomer()方法每次被调用都会创建一个新的CalendarTimeZone和两个Date实例,这是不必要的。纠正这种低效的方法之一是使用静态初始化器,以便只在初始化时创建CalendarTimeZoneDate对象,而不是每次调用isBabyBoomer()方法。

class Person {
  private final Date birthDate;

  // The starting and ending dates of the baby boom.
  private static final Date BOOM_START;
  private static final Date BOOM_END;

  static {
    Calendar gmtCal =
      Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_START = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_END = gmtCal.getTime();
  }

  public boolean isBabyBoomer() {
    return birthDate.compareTo(BOOM_START) >= 0 &&
       birthDate.compareTo(BOOM_END)       <  0;
  }
}

Java中适当的注释

清晰简洁的注释在阅读其他开发人员的代码时非常有用。以下是写出高质量注释的几个指南:

  1. 注释不应该重复代码。
  2. 好的注释不能弥补代码不清晰的问题。
  3. 如果您无法编写清晰的注释,则代码可能存在问题。
  4. 在注释中解释不符合惯用方式的代码。
  5. 在最有用的地方包含指向外部参考文献的链接。
  6. 在修复错误时添加注释。
  7. 使用注释标记未完成的实现,通常使用标记“TODO:”开头。

总结

在本文中,我们了解了15个Java最佳实践,并探讨了类成员封装、在冗长的数字字面值中使用下划线、避免空catch块、正确完成字符串连接、如何避免冗余初始化以及使用增强的for循环。


【注】本文译自: Java Best Practices | Developer.com

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注