游戏系统会有大量的配置信息,包括从数据库中读取的配置,还有从各种文件中读取的配置,都伴随着抛出NullPointerException的可能,如果能够减少空指针,系统会更加稳定。
Java8引入了Optional来处理可选数据,把一个有可能缺失的数据放入到一个容器里面,作为Null引用的替代品。
一、能够尽快地检测出错误,方便查找原因。
//Java8的Optional源码
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
创建Optional的时候,就会检测是否存在Null引用,能够快速地定位到数据缺失的源头,虽然会抛出NullPointerException,但是这里抛出空指针是有意义的。
二、从Optional中获取数据,必然要对缺失的数据做处理,要么抛出异常,要么使用默认值代替缺失的数据。
//判断数据是否Null指针
if(object ==null){
//do something
}
Java8之前,处理空指针要编写大量的判空代码,不仅容易遗漏,而且影响开发效率。
Java8的Option提供了一些有用的方法:
//使用默认值
public T orElse(T other) {
return value != null ? value : other;
}
//使用默认值
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
//抛出异常
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
Java8不强迫使用Optional的人如何使用它,容易被误用,甚至有时候并没有比抛出NullpointerException好多少。
我认为使用Java语言做研发必须要遵循3个基本的原则:
1、本不应该出现的错误或者不应该继续执行的地方直接抛异常。
2、重要的逻辑,要记录详细的错误信息。
3、检查参数合法性,而不是让错误发生。
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
本不应该出现的错误或者不应该继续执行的地方抛出异常是非常合理的,但是NullPointerException和NoSuchElementException没有提供任何有价值的错误信息,也无法知道数据缺失的具体原因。
Optional的get()抛出的异常其实很没用,调用get()要保证数据不为null,除非是先调用了isPresent()方法来检查object!=null,否则不要使用这个方法。
Optional的orElseThrow()方法虽然能够抛出指定的异常,但是不适用于可选数据,比如要判断Map里面的某个key的value是否存在。
public static String getFromConcurrentMap(String key) {
String v = concurrentMapA.get(key);
if (v == null) {
v = new String(key);
concurrentMapA.putIfAbsent(key, v);
}
return v;
}
对于Map来说,value==null只是说明value不在Map里面,这不是一种错误,不应该抛出异常。
Java8的Optional.empty()没有提供任何有助于解决问题的错误信息,除了能够表示数据缺失外,没有起到什么作用。
修复bug只靠Optional.empty()没有什么效率可言,Java8提供peek()方法来调试数据缺失。
Stream.empty()//仅仅为了模拟错误
.peek(e -> System.out.println("Stream value: " + e))
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filter1 value: " + e))
.filter(e -> e.contains("a"))
.peek(e -> System.out.println("Filter2 value: " + e))
.findFirst();
findFirst()返回Optional.empty(),peek()方法可以确定是哪一步返回了空的流,但是这种查找bug的方式非常糟糕:
1、线上的系统一般不会遗留这种调试性的代码,如果测试不够充分,问题就不能被及时发现。
2、需要修改源代码才能找出问题的原因,可能会引入新的bug,不及时地删除peek()方法会降低系统的性能。
再来看一下一个例子:
public static Optional<Double> inverse(Double x){
return x==0?Optional.empty():Optional.of(1/x)
}
inverse(Double x)方法就是除法运算,x==0不能被作为除数是共识,这种本不应该出现的错误,应该抛出异常,Optional.empty()把这种错误掩盖了,而且无法知道问题是x==0导致的。
到此,可能我们都会得出一个结论,不是Optional难以正确地使用,而是Optional确实无法准确地表示数据缺失。
数据缺失的根本原因其实只有两个:
1、错误。
异常、逻辑错误、人为疏忽、配置缺失等,当错误发生时,人们习惯用null去表示无法返回的数据。
2、可选数据。
根据可选数据的存在与否,执行不同的逻辑分支。
Java8的Optional能够很好地表示可选数据,但是对于错误导致的数据缺失,无法提供有用的信息,如果仅仅使用Exception来表示错误,又会失去了使用Optional的所有好处。
一个可行的方案,就是定义三种对象来描述数据缺失:Success、Failure、Empty。
编写一个基类ResultOption来代表返回值类:
public abstract class ResultOption<V> {
@SuppressWarnings("rawtypes")
private static ResultOption empty = new Empty();
private ResultOption() {}
// 空对象
private static class Empty<V> extends ResultOption<V> {
//to do
}
// fail对象
private static class Failure<V> extends ResultOption<V> {
//to do
}
// success对象
private static class Success<V> extends ResultOption<V> {
//to do
}
}
Success要包含需要的返回值value:
// success对象
private static class Success<V> extends ResultOption<V> {
private final V value;
private Success(V value) {
this.value = value;
}
}
Failure类代表错误,一般的游戏系统都会定义ResultCode类来表示游戏某些操作的结果,加入ResultCode表示错误的结果码:
// fail对象
private static class Failure<V> extends ResultOption<V> {
private final ResultCode resultCode;
//to do
}
ResultCode代表游戏逻辑的结果码,仅仅只有一个结果码的信息是不够的,而且系统的错误和异常,也需要用一个对象来存储,加入detail表示详细的错误信息:
// fail对象
private static class Failure<V> extends ResultOption<V> {
private final ResultCode resultCode;
//如果要返回额外的信息,可以往detail里面塞数据
private final String detail;
private Failure(ResultCode resultCode) {
this.resultCode = resultCode;
this.detail = resultCode.name();
}
}
要判断数据缺失的状态,加入isSuccess()和isNotSuccess()方法:
public boolean isSuccess() {
return this instanceof Success;
}
public boolean isNotSuccess() {
return !(this instanceof Success);
}
从ResultOption获取数据:
public abstract V getOrElseThrow();
public abstract V getOrElseThrow(Exception exception);
public abstract V getOrElse(final V defaultValue);
public abstract V getOrElse(final Supplier<V> defaultValue);
// consumer就是作用,可以修改外界的任何东西
public abstract void ifSuccess(Consumer<V> effect);
public abstract void ifSuccessOrThrow(Consumer<V> effect);
//数据缺失的时候返回具体错误信息
public abstract ResultOption<String> ifSuccessOrDetail(Consumer<V> effect);
//数据缺失的时候返回具体的异常对象,而不是直接抛出异常
public abstract ResultOption<RuntimeException> ifSuccessOrException(Consumer<V> effect);
//数据缺失的时候返回具体的错误码
public abstract ResultOption<ResultCode> ifSuccessOrResultCode(Consumer<V> effect);
成功执行某个操作之后,把返回的数据作为另一个操作的输入,并且返回两个操作的结果。
public abstract ResultCode andThen(Function<V, ResultCode> f);
获取结果码和详细信息:
public abstract ResultCode getCode();
public abstract String getDetail();
应用ResultOption去处理实际的游戏业务逻辑:
//在玩家的角色上方显示某个已经获得的荣耀称号
ResultCode resultCode = ResultOption.of(mgr.getTitleInfoByBaseId(baseTitleId), "can not find this title").andThen(titleInfo -> {
//设置为使用状态
titleInfo.setState(TitleUseState.USED.getType());
//更新内存状态
mgr.save(titleInfo);
//通知外观改变
Players.getInstance().broadcastShapeChange(roleId);
if (myTitleConfigId.isSuccess()) {
AttributeUtil.updateAddOrModify(roleId, AttrLayerType.ROLE_TITLE, Arrays.asList(myTitleConfigId.getOrElseThrow(), titleInfo));
} else {
AttributeUtil.updateAddOrModify(roleId, AttrLayerType.ROLE_TITLE, titleInfo);
}
return ResultCode.SUCCESS;
});
完整的ResultOption代码请看Java8与游戏开发(七)。