Java最佳实践和技巧

Share

此资源包含由Toptal网络成员提供的Java最佳实践和Java技巧的集合.

该资源包含由Toptal网络成员提供的Java最佳实践和技巧的集合. As such, 本页将定期更新,以包含更多信息并涵盖新兴的Java技术. This is a community driven project, 所以我们也鼓励你们做出贡献, 我们期待着你们的反馈.

Java是最可移植的语言之一, 与Java一样,它可以构建高度分布式的web应用程序, a sophisticated desktop application, 甚至是在手持设备上运行的强大的移动应用程序.

有关Java的更多信息,请查看total资源页面 common mistakes, a Java hiring guide, Java job description and Java interview questions.

始终检查参数先决条件

As a part of the “fail fast” approach,公共方法在执行任何逻辑之前应该始终验证输入参数. 这样可以避免以后出现错误,因为错误的原因很难找到.

Since version 1.4, Java has supported the assert keyword for this purpose. For example:

public void setAge(int age) {
    assert age >= 0 : "Age must be positive";
    …
}

使用断言的一个优点是,可以在运行时禁用它们,以避免在生产系统中产生任何性能影响.

Another approach is to use Google Guava, where you can use Preconditions 类,它提供静态方法. 这样,您可以获得更准确的异常,因为断言只抛出泛型 AssertionError. The following example will throw an IllegalArgumentException:

public void setAge(int age) {
    Preconditions.checkArgument(assert age >= 0, "Age must be positive");
    …
}

In a similar way, you could use Preconditions.checkState(…) 来验证post条件,这会抛出 IllegalStateException. 有关先决条件方法的完整列表,请查看 Guava documentation.

检查参数是否为空对于前提条件验证特别有用. 检查所有不能使用的参数是一个很好的做法 null. 另一个好的做法是用标记可空参数 javax.annotation.Nullable annotation. This way, your code is more readable. Adding Nullable 注释到可以返回的方法 null values is also a good practice.

updateCustomer(int id, String name, @Nullable String address)
    Preconditions.checkknotnull (name, "名称是必填的.");
    …
}

如果您正在使用Java 7或更高版本,则可以使用 Objects.requireNonNull(…), which has the same functionality.

updateCustomer(int id, String name, @Nullable String address)
    Objects.requireNonNull(name, "名称是必填的.");
    …
}

从Toptal获取最新的开发人员更新.

Subscription implies consent to our privacy policy

编写更好文档的几个技巧

编写更好文档的一些简单建议:

  • 为每个公共类和方法编写Javadoc注释(也许可以省略setter和getter).
  • 避免不相关和明显的评论. 在setter上说“设置名称”的注释不会提供额外的信息.
  • 解释类的目的和行为.
  • 解释方法契约和期望的行为. 哪些输入参数值是有效的? 什么值可以期望作为输出? 在哪些情况下会抛出异常? 一个好的方法是记录前后条件.
  • 为类、方法和变量使用有意义的、自文档化的名称.
  • 避免方法中不必要的注释, 特别是因为随着时间的推移,注释和代码很容易彼此不同步. If you write too many comments, 也许最好将方法拆分为自文档私有方法. 例如,不添加注释 double finalPrice = price + (price * taxRate); // computes taxes,一个更好的方法是创建一个私有方法,它更容易阅读: double finalPrice = computeTax(price, taxRate);

How to Properly Use Utility Classes

实用程序类是不打算实例化的类. Instead, 它们通常为泛型功能提供静态方法, 它们就像OOP中的全局函数. 因此,如果您编写了太多实用程序类,您应该检查您的编码设计. 然而,有时您需要编写一些实用程序类,一个很好的例子是泛型函数. 另一个例子是内部领域特定语言(DSL). When implementing a DSL in Java, 您通常不遵循OOP原则并使用静态方法编写类, 因为我们的目标是使语法更短, rather than reusable components.

However, 如果您想要编写一个实用程序类, 考虑以下建议:

  • Make the constructor private. 这样,类就不能被实例化.
  • Make the class final. 即使私有构造函数将限制子类中的实例化, 有人可以创建一个子类并添加更多的静态方法.
  • 根据其方法提供的功能创建内聚的实用程序类. Generic names like MvcUtils or CommonUtils usually lead to a bad design.

不要过度使用反思

反射是一个很酷的Java特性,因为它允许查询类结构和动态调用方法, object instantiation, or setting field values. 该特性在框架和库中广泛使用,用于构建在不同类中工作的泛型代码.

拥有动态执行代码的能力是非常强大的. 但是,如果在代码中使用过多的反射,则可能会危及代码的可维护性. 考虑下面的简单示例:您需要将字段从一个Java Bean复制到另一个具有相同名称字段的Java Bean.

public class PersonEntity {
	private String firstName;
	private String lastName;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
	return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

public class PersonDTO {
	private String firstName;
	private String lastName;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

这里一个可行的选择是遍历getter并使用反射复制值. For the sake of simplicity, 我假设源对象中的getter返回的类型与目标中的setter完全相同. 这样的实现看起来像这样:

copyWithReflection(对象源,对象目标)抛出 
		IllegalAccessException,
		IllegalArgumentException,
		InvocationTargetException,
		NoSuchMethodException,
		SecurityException {
	requireNonNull(source, "source不能为null");
	requireNonNull(target, "target不能为空");

	for (Method getter : source.getClass().getMethods()) {
		if (getter.getName().startsWith(GETTER_PREFIX)) {
			try {
			Method setter = target.getClass()
					.getMethod(SETTER_PREFIX + getter.getName()
					.substring(GETTER_PREFIX.length()),
							getter.getReturnType());
			setter.invoke(target, getter.invoke(source));
			} catch (NoSuchMethodException e) {
				//没有设置,setter不存在.
			}
		}
	}
}

乍一看,这似乎是个好主意. 您可以使用相同的逻辑来复制不同类中的值. 但是,如果将来只需要复制一些字段,会发生什么呢? 这里一个可行的选择是向getter添加注释,但这会增加不必要的复杂性. 更糟糕的是,如果将来需要某种类型的数据转换会发生什么? 如果您更改了源中的字段,但忘记将其添加到目标中,会发生什么?

也许一个更好更简单的方法就是显式地复制属性:

public void copyWithoutReflection(PersonEntity源,PersonDTO目标){
	target.setFirstName(source.getFirstName());
	target.setLastName(source.getLastName());
}

这种方法的优点是更简单, more readable, 处理更少的异常并维护方法名的编译时检查. 而且它更灵活,因为getter和setter不需要有相同的名称.

第一代依赖注入器,比如 Spring, 使用基于xml的配置和反射来设置组件, 因此丢失了编译时类型检查. 后来,更高级的注入器,如 Guice and Dagger 添加了基于java的配置,以添加更多的编译时检查,但仍然使用反射. Dagger 2使用了100%基于代码生成的方法, 消除了对反射的需要,并为注入器设置提供了编译时验证.

避免在大循环中串接字符串

When you add two string in a loop (for, while, do-while), concatenating them using the + 操作符导致内存浪费并增加性能时间. 这是因为每次添加一个新的String对象时都会创建一个新的String对象. 相反,最佳实践是使用 StringBuilder class.

Let’s show it with the examples:

//Bad Practice
String output = "";
for (String s : largeArray) {
    output = output  + s;
}

如果我们将这个Java片段添加到字节码中,然后生成的字节码反编译回来, we will get the following code:

String output = "";
for( String s : largeArray ) { 
    output = new StringBuilder().append(output).append(s).toString(); 
} 
return output;

As we can see, Java used an internal StringBuilder, and in the loop, 每次执行两个操作.

因此,一个更好的解决方案是首先使用StringBuilder:

//Best Practice
StringBuilder sb = new StringBuilder();
for (String s : largeArray) {
    sb.append(s);
}
String output = sb.toString();

为了证实这一点,我测试了两种场景(I.e.,使用' + '操作符加上StringBuilder). Here are the results:

Loop lengthUsing String '+' Using String Builder Append
 Trial 1Trial 2Trial 3Trial 4Average (ms) Trial 1Trial 2Trial 3Trial 4Average (ms)
10003248426246 4553344043
5000105128122113117 2952433640
10000222262262278256 3672455552
3000016891713174716861708.75 4353545350.75
5000044544445434543574400.25 4263656358.25
7000065676933694270746879 5172616963.25
900001026310688106981051610541.25 5255726160


StringBuilder graph

Testing PC:

  • Intel Core i5 M 540 @ 2.53GHz
  • 8GB RAM
  • Windows 10 Pro
  • Java 1.8

正如我们所看到的,结果证实了最初的断言.

To learn more, Laurent 使用Java 8中的反汇编字节码详细解释了该过程.

Contributors

Akinola Olayinka

Java/Android开发人员(购物及互联网服务)

Akinola是一个Java和Android爱好者, 并且是一名拥有超过三年经验的自由开发者.

使用泛型强制执行编译时类型检查

Java作为一种强类型语言,提供了一个泛型类型系统. 设计您的类如何使用泛型类型可能既困难又乏味, but it offers significant advantages. 结果代码更加清晰,因为您不再需要大多数强制转换操作. Also, the code will be safer, 因为更多的类型验证将在编译时完成, thus avoiding ClassCastException errors at runtime.

但是,如前所述,泛型类型可能很复杂. 由于泛型是一个很大的主题,请额外阅读 Java documentation is a good idea.

处理泛型的不变性

让我们开始解释方差的含义. In object-oriented (OO) programming, 将超类视为比子类更泛型的类型是很自然的. 我们可以将超类视为“更大”的类型,因为它们可以包含比特定子类更多的类. For example, java.lang.Number,可以被认为是比 java.lang.Integer, since it can also contain java.lang.Double, java.lang.Float, and so on.

虽然这在标准的OO继承中非常简单, 在处理更复杂的类型系统时,这可能会变得有点困难, 比如由Java泛型定义的. 需要定义特定的概念来指定哪个类型比另一个类型大.

According to Wikipedia, four categories can be used: * Type A is covariant to B if A >= B. 这意味着,a类型的变量可以保存B的实例. * Type A is contravariant to B if A <= B. 这几乎与前一点相反. * A type is bivariant if it is both covariant and contravariant at the same time. This is not supported by Java. * A type is invariant 如果它不符合前面的任何定义.

在Java中,默认情况下泛型是不变的(与协变的数组相反). 这种不连贯会导致混乱. 例如,下面的代码在Java中是有效的:

字符串stringArray[] ={“嗨”、“如何”、“是”,“它”,“走?" };
Object objectArray[] = stringArray;

To explain this a bit: String[] is a subtype of the Object[]. 这种方法的问题是它可能导致运行时异常. 例如,执行下面这行代码将生成一个 java.lang.ArrayStoreException exception:

objectArray[0] = 123; 

To avoid this, 默认情况下,泛型类型不是协变的, this way, 编译器可以防止这类错误. 下面的代码将在第二行产生编译时错误:

List stringList = new ArrayList<>();
List objectList = stringList;


但是,如果我们需要使用更泛型或更具体的类型参数,会发生什么呢? Here is where extends and super keywords come into play.

With extends 可以指定类型参数可以是扩展指定类型的任何类. For example:

List integerList = new ArrayList<>();
List numberList = integerList;

This code block is valid. 这会有和数组一样的问题吗? 不会,因为Java编译器可以在编译时检查类型参数. So, while integerList.add(Integer.valueOf(123)); is valid, numberList.add(Integer.valueOf(123)); will produce a compilation error. 但是,读取一个元素并从元素中请求一个方法 Number class is perfectly valid:

numberList.get(0).intValue();

This is valid because, 即使编译器不确切知道哪个类是参数化的, it knows that it must be a Number subclass. 因此,可以调用这个类中找到的任何方法.

In a similar way, the super 关键字表示类参数可以是指定类的任何超类.

处理原始类型和类型擦除

另一个值得考虑的主题是原始类型和类型擦除. 在Java 1中引入泛型时.5, Java实现了一种称为“类型擦除”的解决方案,以保持与旧版本Java的向后兼容性. 这意味着编译器将从变量类型中删除有关泛型的信息, method parameter types, and method return types. 编译器还会在需要时自动添加强制转换操作. 中查找有关类型擦除如何工作的更多信息 online Java tutorial

Type erasure enables the use of raw types. 在声明变量时使用原始类型. 使用这种声明,您可以省略泛型类型声明. 例如,编译以下代码(带有警告):

List integerList = new ArrayList<>();
List rawList = integerList;

Note that rawList 缺少泛型类型定义,因为它是原始类型. 不鼓励使用原始类型,因为它可能导致类似于处理数组时可能发生的问题. 下面的代码行是有效的,但是会产生一个 java.lang.ClassCastException exception.

rawList.add("Hi");

大多数情况下,您不会注意到类型擦除,但有时您需要注意它. For example, some frameworks, like Guice, 强制您在运行时使用子类来保持泛型类型信息, to do generic-type bindings. 在Guice中,如果需要使用泛型类型指定绑定,则需要扩展 TypeLiteral class:

bind(new TypeLiteral>(){})
    .to(SomeImplementation.class);

这样做的原因是类型擦除. As explained before, Java从方法参数类型中删除泛型信息, but it is kept when subclassing, 因此通过反思是可以得到的.

Note that .NET设计人员在这样的平台上实现泛型时采用了不同的方法. There is no type erasure there. For example, Ninject (a .. NET依赖注入器)允许这样设置绑定:

this.Bind().To();

这是因为关于类型参数的信息 IWeapon 方法调用时在运行时维护的 Bind method.

Avoid Object Mutability

在Java中,对象可以是可变的,也可以是不可变的. 可变意味着对象状态和变量值可以在其生命周期中更改, while immutability means the opposite; the state can’t be changed once the object is created. It is important to remember object mutability could be a problem; it might become hard to track where and when the object state changed since many variables can point to the same instance. 因此,您应该尽可能避免可变性.

一个好的做法(即使不能保证不变性)是生成所有字段 final:

public class Person {
  private final int id;
  private final String name;

  public Person(int id, String name) {
    this.id = id;
    this.name = Preconditions.checkNotNull(name);
  }
  …
}

这样,Java编译器强制您在构造函数中为每个函数设置一个值 final field. 即使您有多个构造函数,编译器也会跟踪所有路径以确保这一点 final fields will be initialized.

Dependency injectors, like Spring or Guice,允许实现不同的注入模型,使用 setter, field, constructor. 为了保持对象的不变性,最好使用基于构造函数的依赖注入器.

学习使用实用程序类来正确实现“equals()”和“hashCode()”方法

如果要使用Java提供的不同集合类, 您应该熟悉实现的需求 equals() and hashCode() methods. 或者,您可以在 Object class javadoc. 实现这些方法可能会很繁琐. 大多数ide都提供代码生成功能,但生成的方法可能过于冗长. Typically, when you change the class structure, 您需要重新生成这些方法, 并且您将丢失手动进行的任何更改. 此外,它可能容易出错,因为您需要再次选择所有涉及的字段. Here’s an example of Eclipse-generated methods:

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	Result = prime * Result + ((key == null) ? 0 : key.hashCode());
	结果=质数*结果+ ((value == null) ? 0 : value.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	MyClass other = (MyClass) obj;
	if (key == null) {
		if (other.key != null)
			return false;
	} else if (!key.equals(other.key))
		return false;
	if (value == null) {
		if (other.value != null)
			return false;
	} else if (!value.equals(other.value))
		return false;
	return true;
}

Luckily, Java 7 and Google Guava 提供实用程序类以简化此类任务. Java 7 provides two static methods: Objects.hash() for building hash codes and Objects.equals() for comparing objects for equality.

例如,你可以这样做:

@Override
public int hashCode() {
  return Objects.hash(key, value);
}

@Override
public boolean equals(Object o) {
  if (this == o) {
    return true;
  }
  if (o == null || getClass() != o.getClass()) {
    return false;
  }
  MyClass that = (MyClass) o;
  return Objects.equals(key, that.key)
      && Objects.equals(value, that.value);
}
  • Objects.hash() 根据作为参数提供的对象的哈希码计算哈希码.
  • Objects.equals() 比较两个对象,进行所有要避免的空值验证 NullPointerExceptions.

如前所述,如果您使用的是java 8之前的版本,可以使用Guava的版本 com.google.common.base.Objects 类,它提供了类似的方法.

Submit a tip

提交的问题和答案将被审查和编辑, 并可能会或可能不会选择张贴, at the sole discretion of Toptal, LLC.

* All fields are required

Toptal Connects the Top 3% 世界各地的自由职业人才.

Join the Toptal community.