学习像Java这样的编程语言的基础知识是成为一名优秀程序员的重要组成部分,但是正是这些小细节让我们从优秀的程序员成长为伟大的工匠。就像木工理解凿子和凿子的细微差别,职业拳击手理解平衡和杠杆的错综复杂一样,我们必须理解提供最重要结果的小方面。
你可能还喜欢:4 Techniques for Writing Better Java
在这个条目中编写更好的Java的技术系列中,我们将深入研究Java语言中经常被忽略的三个方面。首先,我们来看看valueOf
方法以及如何尽可能避免这些方法。接下来,我们将遵循同样的思路,探索instanceof
关键字以及如何避免滥用此功能。
最后,我们将看看何时何地抛出异常以获得最大的效率,以及在正确的地方抛出异常如何能够区分设计良好的类和调试噩梦。
我们鼓励感兴趣的读者阅读本系列的其他文章,了解更多利用Java语言更有效地解决问题的12种方法:
valueOf
可能的话像Java这样的强类型语言的最大好处之一是编译器可以在编译时强制执行我们的意图。通过将一个类型应用于每一条数据,我们对该数据的性质做出了明确的陈述。
例如,如果我们将变量定义为int
,我们声明变量不能大于231- 1且不能小于-231。随着面向对象编程的引入,我们可以通过创建类和实例化该类的对象来定义新的性质。例如,我们可以定义一个Address
使用特定的Address
状态:
public class Address {
private final String name;
private final String street;
public Address(String name, String street) {
this.name = name;
this.street = street;
}
public String getName() {
return name;
}
public String getStreet() {
return street;
}
}
Address someAddress = new Address("John Doe", "117 Spartan Way");
这种强类型是核心OOP概念的基础,例如多态性和动态调度。这些概念结合在一起产生了一个应用程序,它在Java执行程序之前明确地陈述了我们的意图。虽然强类型在许多情况下可能是乏味的,但在许多情况下,它允许我们在部署应用程序之前知道我们的意图在逻辑上是否合理。例如,如果我们试图传递一个int
值为aname
或者street
当实例化我们的Address
对象(即,new Address(1, 2)
),Java编译器会抱怨:
xxxxxxxxxx
error: incompatible types: int cannot be converted to String
Address someAddress = new Address(1, 2);
我们打算有一个String
代表我们的name
和street
值,但是我们提供了int
。因为编译器不能将int
作为String
(即,int
至少不具有String
),编译器抛出一个错误并拒绝编译我们的应用程序。
当创建更大的程序员时,这种类型安全是一个必要的工具,成百上千的类通过它们之间的关系相互作用并完成相当复杂的任务。
尽管Java是一种强类型的语言,但是仍然有办法避免这种类型检查。最常见的方法之一是使用String
对象来表示所有数据。例如,经常会看到以下JavaScript对象符号(JSON)数据作为表示状态转移(REST)请求或响应的主体发送:
xxxxxxxxxx
{
"name": "John Doe",
"accountValue": "100"
}
起初,似乎很明显accountValue
字段是一个int
值,甚至可能是long
,但这里有一个更可疑的问题。虽然我们在这个响应中看到的值是一个整数,但是它能够有吗String
价值。例如,没有什么能阻止这个身体:
xxxxxxxxxx
{
"name": "John Doe",
"accountValue": "unknown"
}
这现在变成了一个更加棘手的解析问题。我们问题的根源是:什么样的价值观能够accountValue
接受挑战。从类型的角度来看,它可以是由String
班级。实际上,我们知道这个值应是一个整数,但是在语义上,不能保证它将小心点。
这个问题变得更加糟糕,因为我们忽略了关于自然的决策accountValue
应用程序的其余部分。例如,我们可以使用一个简单的普通旧Java对象来反序列化这个JSON:
xxxxxxxxxx
public class Account {
private String name;
private String accountValue;
public String getAccountValue() {
return accountValue;
}
// ...getters & setters...
}
当我们应用程序中的另一个类调用Account#getAccountValue
,aString
被返回。这String
没有提到accountValue
(例如,它的最大值或最小值可以是多少,或者它在数值上比另一个账户值更高还是更低)。解决此问题的一个快速方法是将String
立即变成int
或者long
(在这种情况下,我们将使用long
由于其更高的精度):
xxxxxxxxxx
public class Account {
private String name;
private String accountValue;
public long getAccountValueAsLong() {
return Long.valueOf(accountValue);
}
// ...getters & setters...
}
使用这种方法,我们封装了accountValue
字段并隐藏其精确表示。从外面看,另一个班级会认为accountValue
是一个long
,自getAccountValueAsLong
返回along
。这是面向对象程序设计的一个很好的应用,但是它只是推迟了问题。我们知道accountValue
应该只是一个long
,但这并不能阻止它成为String
价值。
另外,由于实际的accountValue
我们的JSON中的字段是String
,我们仍然需要提供一个getAccountValue
返回一个String
,我们必须供应setAccountValue
aString
进行反序列化(不需要其他欺骗)。
例如,如果accountValue
在我们的JSON中设置为未知,调用Long.valueOf("unknown")
会导致以下错误:
xxxxxxxxxx
Exception in thread "main" java.lang.NumberFormatException: For input string: "unknown"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.valueOf(Long.java:803)
at Main.main(Main.java:5)
需要注意的是,这个异常发生在运行时,而不是编译时:编译器在编译期间没有足够的信息来推断提供给Long.valueOf
不会成为String
它可以安全地转换成long
。相反,在执行期间,当"unknown"
被传递给Long.valueOf
,导致我们的应用程序突然退出。
由于我们决定代表accountValue
作为String
,我们现在已经将类型检查从编译时延迟到运行时。我们没有允许编译器对我们程序的源代码执行静态类型检查分析,而是将这种检查推迟到运行时,其中Long.valueOf
现在负责实现类型检查。
即使我们试图抓住这个NumberFormatException
在我们的getAccountValue
方法,我们现在负责决定当遇到不能转换为的数字时该做什么long
。我们可能会认为这是不可能的,因为每个人肯定都知道账户值是一个数字。
然而,我们认为这是错误的,因为在我们的JSON中,甚至在我们的代码中,都没有记录long
应该是。我们的类型accountValue
字段是String
,没有什么能阻止不熟悉我们应用程序的用户正确设置accountValue
敬一杯String
。
相反,我们应该改变我们的JSON来正确地表示我们的意图:
xxxxxxxxxx
{
"name": "John Doe",
"accountValue": 100
}
这使我们能够正确地表示accountValue
在我们的Account
类别:
xxxxxxxxxx
public class Account {
private String name;
private long accountValue;
// ...getters & setters...
}
当我们应用程序中的其他类现在访问Account
对象,他们知道accountValue
将是有效的long
。这使得他们能够根据long
。例如,另一个类可以很容易地确定accountValue
通过将该值与0
:getAccountValue() < 0
。
有些情况下valueOf
方法调用(例如Long.valueOf
)是必需的。如果JSON表示超出了我们的控制,我们将别无选择,只能处理accountValue
作为String
。在这种情况下,我们应该立即转换accountValue
敬一杯long
并确保所有其他类都以long
,不是一个String
。一种方法是包装解析后的帐户:
xxxxxxxxxx
public class AccountState {
private String name;
private String accountValue;
// ...getters & setters...
}
public class Account {
private String name;
private long accountValue;
public Account(AccountState state) {
this.name = state.getName();
this.accountValue = extractAccountValue(state.getAccountValue());
}
private static long extractAccountValue(String value) {
try {
return Long.valueOf(value);
}
catch (NumberFormatException e) {
return 0;
}
}
// ...getters & setters...
}
AccountState state = // ...parse JSON into AccountState object
Account account = new Account(state);
account.getAccountValue() > 0;
但是请注意,我们必须有意识地决定设置什么值accountValue
当一个非long
即使一开始就不应该提供。
因此,在可能的情况下,我们应该避免使用valueOf
转换的方法String
一些原始值,包括:
Integer.valueOf
Long.valueOf
Float.valueOf
Double.valueOf
Boolean.valueOf
它们包含在一个应用程序中应该被认为是一种代码气味,表明我们有一个String
应该用另一种数据类型来表示。
instanceof
可能的话类似于valueOf
,该instanceof
关键字提供了绕过Java编译器的类型检查系统的机会。虽然在某些情况下(尤其是在处理低级代码或使用反射时)instanceof
可能是必要的,但它应该被视为一种代码气味。这很可能是一个信号,表明我们正在跳过严格的类型检查(因此失去了Java类型检查系统的好处)。
在许多情况下,instanceof
用于将已知超类型的对象安全地转换为所需子类型的对象(称为downcasting)。在自定义类中实现equals方法时,这种向下转换很常见:
xxxxxxxxxx
public class Foo {
private int value;
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
else if (!(obj instanceof Foo)) {
return false;
}
else {
Foo other = (Foo) obj;
return value == other.value;
}
}
}
首先检查是否obj
是的实例Foo
类别(即,obj instanceof Foo
),我们确保我们正在向下预测一个实际的Foo
反对Foo
。这就是所谓的安全,或检查,下降。如果在将某个对象转换为该类型之前,我们没有检查该对象的实现类型,则会发生未检查的向下转换:
xxxxxxxxxx
public interface Vehicle {}
public class Boat implements Vehicle {}
public class Truck implements Vehicle {}
public class Foo {
public void doSomething(Vehicle vehicle) {
Truck truck = (Truck) vehicle;
System.out.println(truck);
}
}
Foo foo = new Foo();
Vehicle vehicle = new Truck();
foo.doSomething(vehicle);
我们知道这个剧组是安全的,因为我们知道推理的我们提供的doSomething
对象的实现类型为Truck
。不过,我们可以提供一个Boat
对象:
xxxxxxxxxx
Foo foo = new Foo();
Vehicle vehicle = new Boat();
foo.doSomething(vehicle);
这样做会导致以下错误:
xxxxxxxxxx
Exception in thread "main" java.lang.ClassCastException: class Boat cannot be cast to class Truck (Boat and Truck are in unnamed module of loader 'app')
at Foo.doSomething(Application.java:16)
at Application.main(Application.java:29)
请注意,这是一个运行时异常,因为编译器无法推断vehicle
将一直持续到运行时。在我们的例子中,我们知道实现类型,因为我们已经静态地设置了它,但是有时在编译时不能确定实现类型:
xxxxxxxxxx
public class Bar {
public Vehicle createVehicle() {
int random = // randomly select 0 or 1
if (random == 0) {
return new Truck();
}
else {
return new Boat();
}
}
}
Foo foo = new Foo();
Bar bar = new Bar();
foo.doSomething(bar.createVehicle());
在这种情况下,实现类型是基于一些只能在运行时知道的标准(例如,用户输入或随机数生成器)随机确定的。因此,这种检查必须推迟到运行时,如果执行了不安全的向下转换,就会引发异常。这个问题非常普遍,以至于在使用泛型执行不安全的向下转换时,Java甚至会发出警告(由于Java使用了unreified generic types):
xxxxxxxxxx
public class Foo {
public void doSomething(List<?> list) {
List<Foo> foos = (List<Foo>) list;
}
}
在这种情况下,该(List<Foo>
)列表向下转换将出现以下警告:
xxxxxxxxxx
Type safety: Unchecked cast from List<capture#1-of ?> to List<Foo>
因为向下转换可能会导致问题,所以使用有检查的向下转换总是一个好主意instanceof
只要有可能。此外,我们应该避免使用instanceof
总之。在许多情况下,instanceof
调用被用作适当多态性的替代。
例如,以下是的常见用法instanceof
:
xxxxxxxxxx
public interface Vehicle {}
public class Boat implements Vehicle {
public void engagePropeller() {
// ...
}
}
public class Truck implements Vehicle {
public void engageAxel() {
// ...
}
}
public class VehicleDriver {
public void drive(Vehicle vehicle) {
if (vehicle instanceof Boat) {
Boat boat = (Boat) vehicle;
boat.engagePropeller();
}
else if (vehicle instanceof Truck) {
Truck truck = (Truck) vehicle;
truck.engageAxel();
}
}
}
本质上,我们正试图治疗每一个Vehicle
实现类型(即,Boat
和Truck
)不同。这正是多态性的用例。而不是检查Vehicle
穿过instanceof
和执行向下转换,我们可以在Vehicle
接口被调用drive
并让每个实现类型执行适当的方法。
xxxxxxxxxx
public interface Vehicle {
public void drive();
}
public class Boat implements Vehicle {
public void drive() {
engagePropeller();
}
public void engagePropeller() {
// ...
}
}
public class Truck implements Vehicle {
public void drive() {
engageAxel();
}
public void engageAxel() {
// ...
}
}
public class VehicleDriver {
public void drive(Vehicle vehicle) {
vehicle.drive();
}
}
虽然可能有一些特定的情况instanceof
是必要的(例如实现equals
方法),一般来说,instanceof
应避免检查和不安全的下导管。相反,我们应该使用多态性来根据对象的实现类型改变行为。
当运行时必须进行错误检查时,最好尽早抛出异常。在大型复杂环境中,对象在一个线程中被实例化,在另一个线程中被调用,不适当的异常处理会导致调试噩梦。在许多情况下,不恰当的异常处理的结果可能是微妙和隐蔽的。
例如,假设我们有以下类:
xxxxxxxxxx
public class SecurityManager {
private final SecurityTransactionRepository repo;
public SecurityManager(SecurityTransactionRepository repo) {
this.repo = repo;
}
public Optional<SecurityTransaction> findTransactionById(long id) {
return repo.findById(id);
}
}
这门课似乎很简单:它吸收了SecurityTransactionRepository
对象,并推迟查找SecurityTransaction
对象添加到此存储库中。我们可以执行findTransactionsById
仅仅通过向SecurityManager
构造函数并调用findTransactionById
方法:
xxxxxxxxxx
SecurityTransactionRepository repo = // create repository...
SecurityManager manager = new SecurityManager(repo);
Optional<SecurityTransaction> transaction = manager.findTransactionById(1);
当事情没有按计划进行时,问题就开始出现了。例如,如果我们的repo
对象是null
?在这种情况下,我们的呼叫顺序将达到以下内容:
xxxxxxxxxx
SecurityManager manager = new SecurityManager(null);
Optional<SecurityTransaction> transaction = manager.findTransactionById(1);
如果我们尝试执行这段代码,我们将看到如下堆栈跟踪:
xxxxxxxxxx
Exception in thread "main" java.lang.NullPointerException
at SecurityManager.findTransactionById(Application.java:25)
at Application.main(Application.java:33)
这NullPointerException
(NPE)发生是因为我们的SecurityTransactionRepository
我们传递到SecurityManager
构造是null
。一旦findTransactionById
方法试图遵从null
SecurityTransactionRepository
对象NullPointerException
被抛出。防止此异常发生的一个简单方法是检查null
repo
对象:
xxxxxxxxxx
public class SecurityManager {
private final SecurityTransactionRepository repo;
public SecurityManager(SecurityTransactionRepository repo) {
this.repo = repo;
}
public Optional<SecurityTransaction> findTransactionById(long id) {
if (repo == null) {
return Optional.empty();
}
else {
return repo.findById(id);
}
}
}
如果我们用一个null
SecurityTransactionRepository
对象传递给SecurityManager
构造函数,我们对findTransactionById
方法现在导致空的Optional
对象。虽然我们已经解决了NPE问题,但我们引入了一个更微妙的问题,它可能会反过来伤害我们。
假设我们的SecurityManager
对象是在一个地方创建的,并且findTransactionById
方法在另一个地方调用。另外,假设findTransactionById
方法被调用的时间很晚,可能在SecurityManager
对象已被构造。
这在Spring应用程序和OSGi应用程序中很常见,在这些应用程序中,beans或服务被创建并连接到设备的完全不同的部分。在OSGi,一个服务可以在一个包中创建,并作为一个服务注入到一个完全不同的包中。
在这种情况下,如果我们试图执行findTransactionById
方法与null
SecurityTransactionRepository
对象,空的Optional
会被归还。如果我们试着调试为什么SecurityTransaction
对象没有被发现,我们看到它要么是因为没有这样SecurityTransaction
存在(但我们期望它存在)或因为SecurityTransactionRepository
存在null
。
一旦我们发现SecurityTransactionRepository
存在null
,我们知道它一定被传入了SecurityManager
构造函数为null
(因为它被限定为final
)。
虽然这个故障排除过程非常标准,但我们的调试在这一点上遇到了重大障碍。我们需要找到这个SecurityManager
对象是用null
构造函数参数。如果这个实例化过程发生在另一个项目或包中,或者发生在几分钟、几小时甚至几天前,我们现在必须搜索整个应用程序以找到根本原因。
如果不止一个地方实例化,我们可以在调试过程中添加另一个扳手SecurityManager
物体。在这种情况下,哪一个实例化了我们的SecurityManager
带着null
SecurityTransactionRepository
?所有这些问题都源于一个简单的事实:我们没有处理一个null
SecurityTransactionRepository
在正确的位置。
我们不想要我们的SecurityManager
用一个null
SecurityTransactionRepository
因此,我们应该明确声明,在SecurityManager
。为此,我们可以使用Objects#requireNonNull
方法,如果传递给它的参数为null
如果参数不是,则返回传递给它的参数null.
xxxxxxxxxx
public class SecurityManager {
private final SecurityTransactionRepository repo;
public SecurityManager(SecurityTransactionRepository repo) {
this.repo = Objects.requireNonNull(repo);
}
public Optional<SecurityTransaction> findTransactionById(long id) {
return repo.findById(id);
}
}
如果我们用null
SecurityTransactionRepository
,我们将看到一个NPE被抛出,但这一次,异常源自SecurityManager
:
xxxxxxxxxx
this.repo = Objects.requireNonNull(repo);
这确保了如果我们的应用程序启动null
SecurityTransactionManager
对象提供给的构造函数SecurityManager
对象,应用程序将从问题的根源抛出一个错误。使用报告的堆栈跟踪,我们可以找到SecurityManager
构造函数:
xxxxxxxxxx
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:221)
at SecurityManager.<init>(Application.java:22)
at Application.main(Application.java:32)
对于上面的堆栈跟踪:
xxxxxxxxxx
Application.main(Application.java:32)
这种移动的概念null
——检入构造函数与概念密切相关Design by Contract (DBC)。在这种技术中,每种方法都有以下几个方面:
例如,前提条件可能是某些数据可供使用,而后置条件可能是方法的结果如果自身相乘,则必须等于提供给方法的参数(即平方根函数)。虽然DBC可以被过度形式化,我们可以使用不变量的概念来表达我们的null
-检查。当我们检查SecurityTransactionRepository
提供给我们的对象SecurityManager
构造函数不是null
,我们知道如果SecurityManager
对象已成功构造,则SecurityTransactionRepository
永远不会null
。
自从SecurityTransactionManager
标记为final
,构造函数是唯一可以设置其值的机制。如果提供给构造函数的值是非null
,那么我们知道repo
字段将是非null
为了这个物体的生命。因此,我们永远不需要再次检查回购字段是否为null
:我们假设它是非null
。这相当于我们生活中的一个不变量SecurityManager
班级。因此,如果检查repo
对象是null
在我们的内心findTransactionById
定义。相反,我们假设它不是null
在课堂上。
一般来说,最好在错误发生时尽快抛出异常。将异常推迟到稍后的时间将会混淆调试过程,浪费开发人员试图找到问题根源的时间。就我们而言SecurityManager
,错误在于null
SecurityTransactionManager
传递给它的构造函数,因此,应该在构造函数中抛出NPE。如果我们期望SecurityTransactionRepository
可能是null
(因此在构造函数中抛出一个NPE是不正确的),我们应该使用一个机制,比如Optional
或者Null Object pattern。
在本文中,我们研究了valueOf
每个装箱的基本类中包含的方法,以及如何避免在使用这些方法时产生陷阱。接下来,我们看了instanceof
方法以及如何避免使用它,除非完全必要。
最后,我们研究了在代码的正确位置抛出异常是如何区分设计良好的类和微妙的陷阱的。虽然这些细节可能会隐藏在用Java编写代码的日常工作中,但正是这些小细节决定了优秀程序员和优秀工匠的区别。
7 Tips to Write Better Java Code You Should Know
A Systematic Approach to Write Better Code With OOP Concepts