Web服务返回一个巨大的XML,我需要访问它的深层嵌套字段。例如:
return wsObject.getFoo().getBar().getBaz().getInt()
问题是,getFoo()
,getBar()
,getBaz()
可能都返回null
。
但是,如果我在所有情况下检查null
,代码将变得非常冗长且难以阅读。此外,我可能会错过某些领域的支票。
if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();
写作是否可以接受
try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
return -1;
}
还是会被视为反模式?
捕捉NullPointerException
是一件非常棘手的问题,因为它们几乎可以在任何地方发生。很容易从一个bug中获取一个,偶然捕获并继续好像一切正常,从而隐藏一个真正的问题。处理它是如此棘手,所以最好完全避免。 (例如,考虑一下null Integer
的自动拆箱。)
我建议您使用Optional
类。当您想要处理存在或不存在的值时,这通常是最佳方法。
使用它你可以像这样写你的代码:
public Optional<Integer> m(Ws wsObject) {
return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
.map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
.map(b -> b.getBaz())
.map(b -> b.getInt());
// Add this if you want to return an -1 int instead of an empty optional if any is null
// .orElse(-1);
// Or this if you want to throw an exception instead
// .orElseThrow(SomeApplicationException::new);
}
使用Optional
s代替null
可能缺少的值使得这一事实对读者来说非常明显和清晰,类型系统将确保您不会意外忘记它。
您还可以更方便地访问使用这些值的方法,例如map
和orElse
。
但是也要考虑它是否是中间方法返回null的有效结果,或者是否是错误的标志。如果它始终是一个错误,那么抛出异常可能比返回一个特殊值,或者中间方法本身抛出异常更好。
另一方面,如果中间方法中缺少的值有效,也许您可以为它们切换到Optional
s?
然后你可以像这样使用它们:
public Optional<Integer> mo(Ws wsObject) {
return wsObject.getFoo()
.flatMap(f -> f.getBar())
.flatMap(b -> b.getBaz())
.flatMap(b -> b.getInt());
}
我可以想到不使用Optional
的唯一原因是,如果这是代码的一个真正性能关键部分,并且垃圾收集开销是一个问题。这是因为每次执行代码时都会分配一些Optional
对象,而VM可能无法优化这些对象。在这种情况下,您的原始if测试可能会更好。
正如其他人所说,尊重得墨忒耳法则绝对是解决方案的一部分。另一部分,尽可能改变这些链式方法,使他们无法返回null
。您可以避免返回null
,而是返回一个空的String
,一个空的Collection
,或其他一些虚拟对象,这意味着或做任何调用者对null
做的事情。
我想添加一个关注错误含义的答案。空例外本身并不提供任何意义的完整错误。所以我建议避免直接与他们打交道。
有成千上万的情况你的代码可能出错:无法连接到数据库,IO异常,网络错误......如果你一个接一个地处理它们(比如这里的空检查),那就太麻烦了。
在代码中:
wsObject.getFoo().getBar().getBaz().getInt();
即使你知道哪个字段为空,你也不知道出了什么问题。也许吧是空的,但它是否有望?或者是数据错误?想想看你的代码的人
就像在xenteros的回答中一样,我建议使用自定义未经检查的异常。例如,在这种情况下:Foo可以为null(有效数据),但Bar和Baz永远不应为null(无效数据)
代码可以重写:
void myFunction()
{
try
{
if (wsObject.getFoo() == null)
{
throw new FooNotExistException();
}
return wsObject.getFoo().getBar().getBaz().getInt();
}
catch (Exception ex)
{
log.error(ex.Message, ex); // Write log to track whatever exception happening
throw new OperationFailedException("The requested operation failed")
}
}
void Main()
{
try
{
myFunction();
}
catch(FooNotExistException)
{
// Show error: "Your foo does not exist, please check"
}
catch(OperationFailedException)
{
// Show error: "Operation failed, please contact our support"
}
}
NullPointerException
是一个运行时异常,所以一般来说不建议捕获它,但要避免它。
您必须在任何想要调用方法的地方捕获异常(或者它将在堆栈中向上传播)。然而,如果在你的情况下,你可以继续使用值为-1的结果,并且你确定它不会传播,因为你没有使用任何可能为null的“碎片”,那么对我来说似乎是正确的抓住它
编辑:
我同意来自@xenteros的后来的answer,它最好是启动你自己的异常而不是返回-1你可以称之为InvalidXMLException
。
自昨天以来一直关注此帖。
我一直评论/投票评论说,捕捉NPE是坏事。这就是我一直这样做的原因。
package com.todelete;
public class Test {
public static void main(String[] args) {
Address address = new Address();
address.setSomeCrap(null);
Person person = new Person();
person.setAddress(address);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
try {
System.out.println(person.getAddress().getSomeCrap().getCrap());
} catch (NullPointerException npe) {
}
}
long endTime = System.currentTimeMillis();
System.out.println((endTime - startTime) / 1000F);
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
if (person != null) {
Address address1 = person.getAddress();
if (address1 != null) {
SomeCrap someCrap2 = address1.getSomeCrap();
if (someCrap2 != null) {
System.out.println(someCrap2.getCrap());
}
}
}
}
long endTime1 = System.currentTimeMillis();
System.out.println((endTime1 - startTime1) / 1000F);
}
}
public class Person {
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
package com.todelete;
public class Address {
private SomeCrap someCrap;
public SomeCrap getSomeCrap() {
return someCrap;
}
public void setSomeCrap(SomeCrap someCrap) {
this.someCrap = someCrap;
}
}
package com.todelete;
public class SomeCrap {
private String crap;
public String getCrap() {
return crap;
}
public void setCrap(String crap) {
this.crap = crap;
}
}
产量
3.216
0.002
我在这里看到一个明显的赢家。如果检查比捕获异常要便宜得多。我已经看到了Java-8的做法。考虑到70%的当前应用程序仍在Java-7上运行,我正在添加这个答案。
底线对于任何关键任务应用,处理NPE成本很高。
如果效率是一个问题,那么应该考虑“捕获”选项。如果'catch'不能被使用,因为它会传播(如'SCouto'所述),那么使用局部变量来避免多次调用方法getFoo()
,getBar()
和getBaz()
。
值得考虑创建自己的例外。我们称之为MyOperationFailedException。你可以抛出它而不是返回一个值。结果将是相同的 - 您将退出该函数,但您不会返回硬编码值-1,这是Java反模式。在Java中,我们使用Exceptions。
try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
throw new MyOperationFailedException();
}
编辑:
根据评论中的讨论,让我在之前的想法中添加一些内容。在此代码中有两种可能性。一个是你接受null而另一个是,这是一个错误。
如果它是一个错误而且它发生了,当断点不够时,您可以使用其他结构调试代码以进行调试。
如果它是可接受的,你不关心这个null出现的位置。如果你这样做,你肯定不应该链接这些请求。
你拥有的方法很冗长,但非常易读。如果我是一个新的开发人员来到你的代码库,我可以很快看到你在做什么。大多数其他答案(包括捕获异常)似乎并没有使事情更具可读性,而且有些人认为它的可读性更低。
鉴于您可能无法控制生成的源并假设您确实需要在此处访问一些深度嵌套的字段,那么我建议使用方法包装每个深层嵌套的访问。
private int getFooBarBazInt() {
if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();
}
如果您发现自己编写了很多这些方法,或者如果您发现自己想要制作这些公共静态方法,那么我会创建一个单独的对象模型,嵌套您想要的方式,只有您关心的字段,并从Web转换将对象模型服务到对象模型。
当您与远程Web服务进行通信时,通常会有一个“远程域”和“应用程序域”,并在两者之间切换。远程域通常受Web协议的限制(例如,您无法在纯RESTful服务中来回发送辅助方法,并且深层嵌套的对象模型通常可以避免多个API调用)因此不适合直接用于你的客户。
例如:
public static class MyFoo {
private int barBazInt;
public MyFoo(Foo foo) {
this.barBazInt = parseBarBazInt();
}
public int getBarBazInt() {
return barBazInt;
}
private int parseFooBarBazInt(Foo foo) {
if (foo() == null) return -1;
if (foo().getBar() == null) return -1;
if (foo().getBar().getBaz() == null) return -1;
return foo().getBar().getBaz().getInt();
}
}
return wsObject.getFooBarBazInt();
通过应用得墨忒耳定律,
class WsObject
{
FooObject foo;
..
Integer getFooBarBazInt()
{
if(foo != null) return foo.getBarBazInt();
else return null;
}
}
class FooObject
{
BarObject bar;
..
Integer getBarBazInt()
{
if(bar != null) return bar.getBazInt();
else return null;
}
}
class BarObject
{
BazObject baz;
..
Integer getBazInt()
{
if(baz != null) return baz.getInt();
else return null;
}
}
class BazObject
{
Integer myInt;
..
Integer getInt()
{
return myInt;
}
}
给出与其他所有人不同的答案。
我建议你检查
NULL
s中的if
。
原因:
我们不应该为我们的计划留下一次机会。 NullPointer由系统生成。无法预测系统生成的异常的行为。当您已经有自己的方法处理它时,不应该将程序交给System。并将Exception处理机制用于额外的安全性。!!
为了使您的代码易于阅读,请尝试检查以下条件:
if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null)
return -1;
else
return wsObject.getFoo().getBar().getBaz().getInt();
编辑:
在这里你需要在一些变量中存储这些值
wsObject.getFoo()
,wsObject.getFoo().getBar()
,wsObject.getFoo().getBar().getBaz()
。我不是这样做的,因为我不知道该函数的返回类型。
任何建议将不胜感激.. !!
我写了一个名为Snag
的类,它允许您定义在对象树中导航的路径。以下是其使用示例:
Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();
意味着实例ENGINE_NAME
会在传递给它的实例上有效地调用Car?.getEngine()?.getName()
,如果任何引用返回null
,则返回null
:
final String name = ENGINE_NAME.get(firstCar);
它没有在Maven上发布,但是如果有人发现它有用它的here(当然没有保证!)
这有点基本但似乎可以完成这项工作。显然,对于支持安全导航或Optional
的更新版本的Java和其他JVM语言来说,它已经过时了。
我建议考虑Objects.requireNonNull(T obj, String message)
。您可以为每个异常构建带有详细消息的链,例如
requireNonNull(requireNonNull(requireNonNull(
wsObject, "wsObject is null")
.getFoo(), "getFoo() is null")
.getBar(), "getBar() is null");
我建议你不要使用像-1
这样的特殊返回值。这不是Java风格。 Java设计了异常机制,以避免使用来自C语言的这种老式方法。
投掷NullPointerException
也不是最好的选择。您可以提供自己的异常(检查以确保它将由用户处理或未经检查以更简单的方式处理它)或使用您正在使用的XML解析器的特定异常。
假设类结构确实不受我们的控制,就像在这种情况下一样,我认为按照问题中的建议捕捉NPE确实是一个合理的解决方案,除非性能是一个主要问题。一个小的改进可能是包装throw / catch逻辑以避免混乱:
static <T> T get(Supplier<T> supplier, T defaultValue) {
try {
return supplier.get();
} catch (NullPointerException e) {
return defaultValue;
}
}
现在你可以简单地做:
return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);
正如Tom在评论中已经指出的那样,
以下声明违反了Law of Demeter,
wsObject.getFoo().getBar().getBaz().getInt()
你想要的是int
,你可以从Foo
得到它。得墨忒耳法则说,永远不要和陌生人说话。对于您的情况,您可以隐藏Foo
和Bar
引擎盖下的实际实现。
现在,您可以在Foo
中创建方法以从int
获取Baz
。最终,Foo
将有Bar
和Bar
我们可以访问Int
而不直接将Baz
暴露给Foo
。因此,空检查可能会划分为不同的类,并且只在类之间共享所需的属性。
我的回答几乎和@janki一样,但我想略微修改代码片段,如下所示:
if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null)
return wsObject.getFoo().getBar().getBaz().getInt();
else
return something or throw exception;
如果该对象有可能为null,您也可以为wsObject
添加空检查。
为了提高可读性,您可能希望使用多个变量,例如
Foo theFoo;
Bar theBar;
Baz theBaz;
theFoo = wsObject.getFoo();
if ( theFoo == null ) {
// Exit.
}
theBar = theFoo.getBar();
if ( theBar == null ) {
// Exit.
}
theBaz = theBar.getBaz();
if ( theBaz == null ) {
// Exit.
}
return theBaz.getInt();
你说有些方法“可能会返回null
”但不说在什么情况下他们会返回null
。你说你抓住NullPointerException
,但你没有说你抓住它的原因。缺乏信息表明您没有清楚地了解什么是例外,以及为什么它们优于替代品。
考虑一个用于执行操作的类方法,但该方法无法保证它将执行操作,因为它无法控制的情况(实际上是the case for all methods in Java)。我们称之为该方法并返回。调用该方法的代码需要知道它是否成功。它怎么知道的?它如何构建以应对成功或失败的两种可能性?
使用异常,我们可以编写成功的方法作为后置条件。如果方法返回,则表示成功。如果它抛出异常,它就失败了。这是一个清晰的大胜利。我们可以编写清楚处理正常成功案例的代码,并将所有错误处理代码移到catch
子句中。经常发现方法不成功的方式或原因的细节对调用者来说并不重要,因此可以使用相同的catch
子句来处理几种类型的失败。并且经常发生一个方法根本不需要捕获异常,但可以只允许它们传播给它的调用者。程序错误造成的例外情况属于后一类;当有bug时,很少有方法可以做出适当的反应。
那么,那些返回null
的方法。
null
值是否表示代码中存在错误?如果是这样,你根本不应该捕获异常。而你的代码不应该试图猜测自己。只要在假设它可行的情况下写出简洁明了的内容。方法链调用是否简洁明了?然后只需使用它们。null
值是否表示您的程序输入无效?如果是这样的话,NullPointerException
不是抛出的合适例外,因为它通常用于指示错误。您可能想要抛出从IllegalArgumentException
(如果您需要unchecked exception)或IOException
(如果您想要检查异常)派生的自定义异常。当输入无效时,您的程序是否需要提供详细的语法错误消息?如果是这样,检查每个方法的null
返回值然后抛出适当的诊断异常是你唯一能做的事情。如果您的程序不需要提供详细的诊断,那么将方法调用链接在一起,捕获任何NullPointerException
然后抛出您的自定义异常是最清晰,最简洁的。其中一个答案声称链式方法调用违反了Law of Demeter,因此很糟糕。这种说法是错误的。
不要抓住NullPointerException
。你不知道它来自哪里(我知道你的情况不太可能,但也许还有别的东西扔了)而且它很慢。您想要访问指定的字段,为此,每隔一个字段必须不为空。这是检查每个领域的完美有效理由。我可能会在一个中检查它,然后创建一个可读性的方法。正如其他人指出的那样已经返回-1是非常古老的学校,但我不知道你是否有理由(例如与另一个系统交谈)。
public int callService() {
...
if(isValid(wsObject)){
return wsObject.getFoo().getBar().getBaz().getInt();
}
return -1;
}
public boolean isValid(WsObject wsObject) {
if(wsObject.getFoo() != null &&
wsObject.getFoo().getBar() != null &&
wsObject.getFoo().getBar().getBaz() != null) {
return true;
}
return false;
}
编辑:由于WsObject可能只是一个数据结构(检查https://stackoverflow.com/a/26021695/1528880),因此它不符合Demeter法则是值得商榷的。
如果您不想重构代码并且可以使用Java 8,则可以使用方法引用。
首先是一个简单的演示(原谅静态内部类)
public class JavaApplication14
{
static class Baz
{
private final int _int;
public Baz(int value){ _int = value; }
public int getInt(){ return _int; }
}
static class Bar
{
private final Baz _baz;
public Bar(Baz baz){ _baz = baz; }
public Baz getBar(){ return _baz; }
}
static class Foo
{
private final Bar _bar;
public Foo(Bar bar){ _bar = bar; }
public Bar getBar(){ return _bar; }
}
static class WSObject
{
private final Foo _foo;
public WSObject(Foo foo){ _foo = foo; }
public Foo getFoo(){ return _foo; }
}
interface Getter<T, R>
{
R get(T value);
}
static class GetterResult<R>
{
public R result;
public int lastIndex;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args)
{
WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
WSObject wsObjectNull = new WSObject(new Foo(null));
GetterResult<Integer> intResult
= getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);
GetterResult<Integer> intResult2
= getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);
System.out.println(intResult.result);
System.out.println(intResult.lastIndex);
System.out.println();
System.out.println(intResult2.result);
System.out.println(intResult2.lastIndex);
// TODO code application logic here
}
public static <R, V1, V2, V3, V4> GetterResult<R>
getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
{
GetterResult result = new GetterResult<>();
Object tmp = value;
if (tmp == null)
return result;
tmp = g1.get((V1)tmp);
result.lastIndex++;
if (tmp == null)
return result;
tmp = g2.get((V2)tmp);
result.lastIndex++;
if (tmp == null)
return result;
tmp = g3.get((V3)tmp);
result.lastIndex++;
if (tmp == null)
return result;
tmp = g4.get((V4)tmp);
result.lastIndex++;
result.result = (R)tmp;
return result;
}
}
产量
241 4
空值 2
接口Getter
只是一个功能接口,你可以使用任何等价物。
GetterResult
类,访问器为了清晰而被剥离,保存getter链的结果,如果有的话,或者最后一个getter的索引被调用。
方法getterChain
是一个简单的样板代码片段,可以自动生成(或在需要时手动生成)。
我构造了代码,以便重复块是不言而喻的。
这不是一个完美的解决方案,因为您仍需要为每个吸气剂数定义一个getterChain
重载。
我会重构代码,但是如果不能并且你发现自己经常使用长getter链,那么你可以考虑建立一个带有从2到10个getter的重载的类。