与使用if语句首先检查空值相比,我想知道使用try / exception处理空值的成本。
提供更多信息。有一个>获得空值的几率为50%,因为在这个应用程序中。如果没有输入数据,则通常为null ...因此尝试使用null计算是常见的。
这就是说,如果我在计算之前使用if语句首先检查null并且不首先尝试计算,或者只是抛出异常并处理它,那么它会提高性能吗?
感谢您的任何建议: - )
感谢您提供极好的发人深省的反馈意见!这是一个PSEUDOcode示例,用于澄清原始问题:
BigDecimal value1 = null //assume value1 came from DB as null
BigDecimal divisor = new BigDecimal("2.0");
try{
if(value1 != null){ //does this enhance performance?... >50% chance that value1 WILL be null
value1.divide(divisor);
}
}
catch (Exception e){
//process, log etc. the exception
//do this EVERYTIME I get null... or use an if statement
//to capture other exceptions.
}
答案 0 :(得分:14)
我建议检查null并不进行计算而不是抛出异常。
异常应该是“例外”且很少见,而不是管理控制流的方法。
我还建议您与客户就输入参数建立合同。如果允许空值拼写出来;如果它们不是,请明确应该传递的内容,默认值以及如果传递空值,您承诺返回的内容。
答案 1 :(得分:9)
如果传递null
参数是例外情况,那么我会抛出NullPointerException
。
public Result calculate(Input input) {
if (input == null) throw new NullPointerException("input");
// ...
}
如果传递null
是允许的案例,那么我将跳过计算并最终返回null
。但这在我看来没有多大意义。在第一个实例中传递null
似乎是调用代码中的错误。
无论您选择哪种方式,都应该在Javadoc中正确记录,以避免API用户出现意外。
答案 2 :(得分:3)
如果有> 50%的机会获得null,那么它几乎不是例外吗?
就个人而言,我会说,如果你期望某些事情发生,你应该适当地编码 - 在这种情况下,检查null并做任何合适的事情。我一直都知道抛出一个不太便宜的例外,但不能肯定地说。
答案 3 :(得分:3)
尝试和捕获接近“免费”但投掷可能非常昂贵。通常,VM创建者不会优化异常路径,因为它们非常特殊(应该是罕见的)。
NullPointerException表示程序员错误(RuntimeException),不应该被捕获。您应该修复代码,而不是捕获NullPointerException,而不是首先抛出异常。
更糟糕的是,如果你捕获NullPointerException并且计算代码的不同部分抛出NullPointerException而不是你想要抛出它的那个,你现在已经掩盖了一个bug。
要完全回答你的问题,我会以一种方式实现它,对其进行概要分析,然后以另一种方式实现它并对其进行分析......然后我只使用if语句来避免抛出NullPointerException而不管由于上述观点,这更快。
答案 4 :(得分:0)
我同意大多数其他回复,你应该更喜欢对try-catch进行空检查。而且我已经投了一些。
但你应尽量避免这种需要。
有一个>获得空值的几率为50%,因为在这个应用程序中。如果没有输入数据,则通常为空...所以尝试使用null进行计算是司空见惯的。
这就是你应该真正解决的问题。
提出合理的默认值,确保计算有效或避免在不提供所需输入数据的情况下调用计算。
对于涉及它们的许多标准数据类型和计算,都有合理的默认值。默认数字为0或1,具体取决于它们的含义,默认字符串和要清空的集合,以及许多计算正常工作。对于您自己制作的更复杂的对象,请考虑Null Object模式。
答案 5 :(得分:0)
如果您有任何情况导致您的程序无法处理结果或输入,那么这应该是一个错误。你应该知道你的程序可以处理什么,只允许它。关于可能处理结果的未来可能情况,但我还是建议在实际需要该结果之前将其视为错误。如果有疑问,你不知道,该程序不知道,它无法处理,你没有配备你的程序来处理它,所以它是一个错误。该计划除了停止之外什么也做不了。
对于用户输入,依靠程序最终崩溃是一个非常糟糕的主意。你不知道什么时候甚至会崩溃。它可能最终会做错事或做十件事然后崩溃它不应该做,如果输入已经过验证就不会做。
在防范自己的错误方面,这更像是一个混合包。您需要更多地关注通过测试确保工作正常,消除未知数,校对证明,确保您确切知道您的计划是如何工作的等等。您仍然偶尔会遇到内部处理可能的情况产生不良后果。
如果结果不是某个给定案例的错误,则不处理异常,则处理null,而不是异常。在这种情况下,结果不是错误。所以你处理结果而不是错误。这简直太简单了。如果您正在捕获异常,然后执行可以使用if:
执行的操作try
extra = i_need_it(extra_id)
show('extra', extra)
catch
show('add_extra')
然后那是不对的。如果你没有额外的东西,你有一个完全可以接受的行动方案。
这样做更好,它可以保持你的意图清晰,没有额外的冗长:
Something extra = i_want_it(extra_id)
if extra ==== null
show('add_extra')
else
show('extra', extra)
请注意,此处您无需任何特殊操作即可避免从其他图层捕获异常。我如何把try catch放在上面是一种不好的做法。你应该只包装抛出异常的东西:
Something extra
try
extra = i_need_it(extra_id)
if extra === null
show('add_extra')
else
show('extra', extra)
当你这样做的时候它只是将null转换为异常然后再转回。这是Yo-Yo编码。
你应该从:
开始Object i_need_it(int id) throws
直到您实际上能够实现null的处理。如果您能够实现异常处理,则可以实现null的处理。
当事实证明总是不需要时,可以添加此方法或将i_need_it更改为它(如果始终处理null):
Object|null i_want_it(int id)
另一种方法是先检查它是否存在:
bool does_it_exist(int id)
这种情况经常发生的原因是因为它通常是这样的:
if(does_it_exist(id))
Something i_need = i_need_it(id)
这往往更容易出现并发问题,可能需要更多可能不可靠的调用(IO over network)并且效率低(两个RTT而不是一个)。其他调用通常像这样合并,例如更新(如果存在),插入(如果存在),更新(如果存在或插入等),然后返回通常最初检查的结果。其中一些在有效载荷大小效率和RTT效率方面存在冲突,这也可能因许多因素而异。
然而,当您需要基于是否存在某些事物的交替行为时,它会更便宜,但您不需要对其进行处理。如果您也不必担心上述问题,那就更清楚了。
您甚至可能想要:
void it_must_exist(int id) throws
这又是有用的,因为如果你只需要确保存在某些东西,它通常比获得它更便宜。然而,您很少需要这样做,因为在大多数情况下,您会想知道是否存在某些内容以便直接使用它。
设想它的一种方式是你不会做出“做出来”的事情。当它只是在调用堆栈中明确返回一个布尔值时抛出一个异常,' i_want_it'是一个组合的' has'和'得到'实际上。
虽然有两种不同的方法可以更清楚地区分方法签名,但有时你可能需要从别的东西中传下来,如果你不采取一些歧义,最简单的方法是:
Object|null get(int id, bool require) throws
这更好,因为你将合同交给了呼叫链,而不是基于远处的行动在沙屋上建造。有一些方法可以更明确地传递你的合同,但它往往是错综复杂的YAGNI(IE,传递方法调用者)。
你应该尽早抛出异常并且你可能想要安全而不是抱歉所以误报是好的。一旦你发现它是误报,那么你就会在源头修复它。
您根本不应该处理异常。绝大多数应该击中顶部,然后调用日志和输出处理程序。其余的你通过直接传回结果并处理它来适当地修复。当你在许多用途中有一个误报时,只有这个使用。不要删除根目录中的支票,并打破许多其他仍然存在错误的情况。
Java是一种令人遗憾的语言,因为我无法通过说法传递null,或者此变量必须为非null。
当缺少这样的功能时,通常最好检查其来源的空值,例如IO,而不是每次将某些内容传递给您的某个方法时。否则,这是一个荒谬的空检查。
如果确实需要,可以应用此模式创建函数来替换参数检查的ifs。您可以将id替换为对象本身,例如:
Object i_want(Object it) throws
if(it === null)
throw
return it;
然后:
void give_me(Object it)
this.it = Lib<Object>::i_want(it)
遗憾的是,没有passthru类型。
或者:
void i_get_it(Getter<Object> g)
this.it = Lib<Object>::i_want(g.gimme())
您可能拥有特定的Getter而不是通用。
或者:
void i_need_it(Result<Object> r)
this.it = r.require()
如果你只是在边界上做(当你在控制范围外面调用无法保证非空结果的东西时),虽然最好在那里做或者任何使用记录为返回null的方法,只有那里只有它真正需要的地方,这意味着当你确实得到一个它不属于的地方(错误发生)时你就会出现这种情况。我不会很容易找到它的来源。它可以从IO传递很多,并且在尝试对其进行操作之前不会产生空指针异常。这些零点可以在系统周围传递数天甚至数月,作为等待熄灭的定时炸弹。
我自己也不会这样做但是在某些情况下我不会责怪人们实施上面的防御性编程方法,因为Java的破坏类型系统默认是松散的并且不能受到限制。在强健类型的语言中,除非您明确说明为null,否则不允许使用null。
请注意,虽然我称之为破碎,但我通常使用非常宽松的语言几十年来构建大型,复杂和关键的系统,而不必通过多余的检查来乱丢代码库。纪律和能力决定了质量。
误报是当结果或条件发生时,您认为这是一个无法由所有呼叫者处理的结果,但事实证明至少有一个呼叫者可以适当地处理它。在这种情况下,您不会处理异常,而是给调用者一个结果。这方面的例外情况很少。
Java 8具有Optional功能,但它看起来并不实用。这是一个可怕的案例,内部平台效应试图将新语法作为类实现,最终必须添加一半的现有Java语法,使其非常笨重。像往常一样的现代Java OOP,通过添加更多软糖和过度复杂的东西来解决人们想要减少软糖的每个问题。如果你真的想要像这样链接,你可能想尝试像kotlin那样直接实现这种语法的东西。
现代语言意味着您不必担心大部分内容而只是:
void i_need_it(Object it)
this.it = it
void i_want_it(Object? it)
this.it = it
现代语言甚至可以为没有返回的方法返回原始对象(用self替换void作为标准和自动返回),这样人们就可以拥有他们的花式链接或者其他任何在编程中流行的时尚。
你不能拥有一个带有基类的工厂,这个基类会给你一个Null或NotNull,因为你仍然必须通过基类,并且当你说时你会违反类型你想要NotNull。
你可能想要玩方面,静态分析等,尽管这有点像兔子洞。所有文档都应该指出是否可以返回null(尽管间接地可能会遗漏它)。
你可以创建一个像MightHave这样的课程来包装你的结果,你可以把它放在像get,require这样的方法上,如果你不喜欢静态但是想要吃一个if,尽管它也在在所有地方都装满了所有方法签名,这是一个丑陋的解决方案。它只是非常方便,作为那些罕见的异常处理案例的替代方案,由于调用图的复杂性和跨层存在的未知数量,异常似乎很有用。
一个非常罕见的情况是当你的源知道要抛出什么异常但是如果它需要被抛出但是它很难传递下来(尽管在两个层之间耦合两层需要发生任何事情的情况谨慎对待)。有些人可能也想要这个,因为它可以很容易地找到缺失物品来自何处以及需要它的位置,这是使用检查可能无法给出的东西(它们很可能在源附近失败但不保证)。应该注意,因为这些问题可能比合理的多态性,元/动态编码等问题更能说明流量控制不佳或过度复杂。
应该注意默认值或Null对象模式等内容。两者都可能最终隐藏错误并成为最好的猜测或治疗比疾病更糟糕。像NullObject模式和Optional这样的东西通常可以用来简单地关闭或者反过来传译解释器中内置的错误处理。
默认值并不总是很糟糕,但可能属于猜测的范畴。隐藏用户的错误最终将其设置为失败。默认(和消毒)总是需要仔细考虑。
诸如NullObject模式和Optional之类的东西可以被过度使用以有效地关闭空指针检查。它只是假设null永远不是一个错误,最终导致程序在做某些事情,而不是其他人,但你不知道是什么。在某些情况下,这可能会带来欢闹的结果,例如用户的信用卡是否为空。确保你是否正在使用这些东西,而不是使用它们,只是将所有代码简单地包装在忽略空指针异常的try catch中。这很常见,因为人们倾向于修复抛出异常的错误。实际上,真正的错误往往更远。您最终会遇到一个错误的IO处理错误地返回null,并且null会在程序周围传递。人们将尝试在它到达导致错误的所有位置修复它,而不是修复那个null源。
NullObject模式或MagicNoop模式有它们的位置但不常用。你不应该使用它们,直到它们立即明显地以合理的方式使用,这不会引起比它解决的问题更多的问题。有时一个noop实际上是一个错误。