总览
当您第一次学习开发时,您会看到关于不同功能的过分笼统的陈述,对于设计,性能,清晰度,可维护性来说,都是不好的,感觉就像是黑客,或者他们只是不喜欢。
这可能会得到现实世界经验的支持,在实际经验中,删除功能的使用可以改善代码。 有时这是因为开发人员不知道如何正确使用该功能,或者该功能固有地容易出错(取决于您是否喜欢它)
当时尚或您的团队改变并且此功能变得很好甚至是首选方法时,这令人感到不安。
在这篇文章中,我研究了人们喜欢讨厌的某些功能,以及为什么我认为使用正确的功能,它们应该成为有益的力量。 功能不像是/否,好/坏,就像许多人相信的那样。
检查异常
对于开发人员不喜欢考虑错误处理的程度,我经常感到惊讶。 新开发人员甚至不喜欢阅读错误消息。 这是艰苦的工作,他们抱怨应用程序崩溃,“它不起作用”。 他们不知道为什么当错误消息和堆栈转储经常告诉他们如果他们只能看到线索的确切原因时,会引发异常。 当我出于跟踪目的而写出堆栈跟踪信息时,许多人只会在没有错误的情况下看到日志形状像崩溃一样。 读取错误消息是一种技巧,起初它可能会让人不知所措。
同样,也经常避免以有用的方式处理异常。 我不知道该异常该怎么办,我宁愿记录该异常并假装它没有发生,或者只是炸掉,然后让操作人员或GUI用户处理错误的能力最弱。
结果,许多经验丰富的开发人员都讨厌检查异常。 但是,我听到的越多,我越高兴Java检查了异常,因为我坚信Java确实会发现忽略异常非常容易,并且只要不被它们烦恼就让应用程序死亡。
检查异常当然可以被过度使用。 问题应该是抛出检查异常时。 我是否想通过迫使他们对错误处理进行一点思考来惹恼开发人员调用代码? 如果答案是肯定的,则抛出一个已检查的异常。
恕我直言,这是lambda设计的失败,因为它无法透明地处理检查的异常。 例如,作为自然的代码块,可以抛出未处理的异常,就像处理未经检查的异常和错误一样。 但是,考虑到lambda和函数式编程的历史,它们根本不喜欢副作用,更不用说快捷错误处理了,这也就不足为奇了。
通过重新抛出已检查的异常,就好像它是未检查的异常一样,您可以绕过lambda的限制。 之所以可以这样做是因为JVM没有检查异常的概念,它像泛型一样是编译时检查。 我的首选方法是使用Unsafe.rethrowException,但还有3种其他方式可以做到这一点。 尽管Thread.currentThread()。stop(e)总是很安全,但它不再在Java 8中工作。
Thread.currentThread()。stop(e)不安全吗?
当Thread.stop(Throwable)方法可能导致另一个线程在代码的随机部分触发异常时,它是不安全的。 这可能是一部分代码中未检查到的异常,也可能是抛出该异常的原因,该异常捕获在线程的某些部分,但其他部分则使您不知道它会做什么。
但是,它不安全的主要原因是,它可能会使原子操作处于不一致状态的代码锁定部分的同步状态,从而以微妙且不可测试的方式破坏内存。
更令人困惑的是,Throwable的堆栈跟踪与实际引发异常的线程的堆栈跟踪不匹配。
但是Thread.currentThread()。stop(e)呢? 这触发当前线程在当前行上引发异常。 这并不比仅使用throw异常执行正在执行的编译器无法检查的操作更糟。 问题在于,编译器并不总是知道您在做什么,以及它是否真的安全。 对于泛型,这被归类为“未经检查的强制转换”,这是一个警告,您可以通过注释禁用它。 Java不能很好地支持带有检查异常的同类操作,因此您最终会使用hack,或者更糟糕的是将真正的检查异常隐藏为运行时异常,这意味着调用者无法正确处理它。
使用
对我来说,这是一个新的“规则”。 我知道它来自哪里,但是该规则比应该应用的地方有更多的例外。 让我们首先考虑所有可以使用重载static
的上下文。
- 静态可变字段
- 静态不可变字段(指向不变的对象的最终原始或最终字段)
- 静态方法。
- 静态类(没有隐式引用外部实例)
- 静态初始化程序块。
我同意使用静态可变字段可能是新手错误,或者是有可能避免的东西。 如果您看到静态字段在构造函数中被更改,则几乎可以肯定是一个错误(即使没有,我也会避免),我相信这是避免所有静态语句的原因。
但是,在所有其他情况下,使用static不仅性能更高,而且更清楚。 它表明此字段对于每个实例都不同,或者该方法或类并不隐含地依赖于实例。
简而言之,static是好的,可变的static字段是例外,而不是规则。
Singletons不好吗?
单例的问题来自两个方向。 它们是有效的全局可变状态,使其难以维护或封装(例如在单元测试中),并且支持自动装配。 也就是说,任何组件都可以访问它,从而使您的依赖项变得不清楚并且难以管理。 由于这些原因,一些开发人员讨厌它们。
但是,遵循良好的依赖关系注入是一种应该应用于所有组件(无论是否单例)的方法,并且应该避免通过单例避免全局可变状态。
如果排除全局状态和自连接组件,则剩下的Singleton是不可变的,并通过依赖项注入传递,在这种情况下,它们可以正常工作。 我用于实现策略的一种常见模式是将枚举与一个实现接口的实例一起使用。
enum MyComparator implements Comparator {INSTANCE;public int compare(MyObject o1, MyObject o2) {// something a bit too complicated to put in a lambda}}
该实例可以通过依赖项注入作为Comparator的实现传递,并且没有可变状态,可以在线程和单元测试之间安全地使用。
我可以让一个库或框架为我做一件非常简单的事情吗?
库和框架可以为您节省大量时间和精力,使您自己的代码执行在其他地方已经可以使用的操作。
即使您想编写自己的代码,我也强烈建议您了解现有库和框架的功能,以便您可以从中学习。 自己编写它不是避免理解任何现有解决方案的捷径。 一位记者曾经绝望地写到一位有抱负的记者。 不喜欢读书,只喜欢写作。 在软件开发中也是如此。
但是,我已经看到(在Stackoverflow上)开发人员竭尽全力避免将自己的代码用于琐碎的示例。 他们觉得如果使用库,它必须比他们编写的任何东西都要好。 问题在于它是假定的。 添加库并不会增加复杂性,您对库有很好的了解,而且您将不需要学习编写可以信任的代码。
一些开发人员使用框架来帮助学习实际的方法论。 实际上,开发人员通常会使用纯Java进行依赖注入的框架,但是他们要么不信任自己,要么不信任自己的团队来这样做。
在高性能空间中,代码越简单,应用程序所做的工作就越少,使用更少的活动部件进行维护就越容易,并且运行速度也就越快。 您需要使用最少的库和框架,这些库和框架应相当容易理解,以便使您的系统发挥最佳性能。
用双钱赚钱不好吗?
在不考虑舍入的情况下使用分数将为您带来意想不到的结果。 从正面看,对于双精度数,通常显然是错误的,例如10.99999999999998,而不是11。
有些人认为BigDecimal是解决方案。 但是,问题在于BigDecimal拥有自己的陷阱,很难进行验证/读取/写入,但如果没有,最糟糕的情况是看起来正确。 举个例子:
double d = 1.0 / 3 * 3 + 0.01;BigDecimal bd1 = BigDecimal.valueOf(1.0).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(3)).add(BigDecimal.valueOf(0.01)).setScale(2, BigDecimal.ROUND_HALF_UP);BigDecimal bd2 = BigDecimal.valueOf(1.0).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(3).add(BigDecimal.valueOf(0.01))).setScale(2, BigDecimal.ROUND_HALF_UP);System.out.println("d: " + d);System.out.println("bd1: " + bd1);System.out.println("bd2: " + bd2);
这将产生三个不同的结果。 通过观察,哪一个产生正确的结果? 您能说出bd1和bd2之间的区别吗?
打印:
d: 1.01
bd1: 1.00
bd2: 0.99
您可以从输出中看到哪个错误吗? 实际上答案应该是1.01。
BigDecimal的另一个难题是equals和compareTo的行为不同。 当compareTo()返回0时,equals()可以为false。即,在BigDecimal 1.0中,等于1.00时为false,因为比例不同。
BigDecimal的问题在于,您获得的代码通常更难于理解,并且会产生看起来不正确的错误结果。 BigDecimal的速度明显较慢,并且会产生大量垃圾。 (这在Java 8的每个版本中都在改进)在某些情况下,BigDecimal是最好的解决方案,但并非如某些人所反对那样是给定的。
如果BigDecimal不是一个很好的选择,还有其他吗? int和long通常以固定的精度使用,例如,整数而不是分数。 这有一些挑战,您必须记住小数位在哪里。 如果Java支持值类型,将它们用作金钱包装器并为您提供更多安全性可能是有意义的,但是处理整数原语的控制,明确性和性能。
使用
对于NullPointerException
Java的开发人员来说,重复获得NullPointerException
是一种消耗体力的体验。 我真的必须为Java中数组中的每个对象,每个元素创建一个新实例吗? 其他语言则不需要这样做,因为通常是通过嵌入式数据结构来完成的。 (Java正在考虑的事物)
即使是经验丰富的Java开发人员也难以处理null
值,并将该语言中的null视为一个大错误。 恕我直言,问题是替代品往往差得多。 例如不是NPE的NULL对象,但也许应该已经初始化为其他对象。 在Java 8中,Optional是一个很好的添加,它使对非结果的处理更加清晰。 我认为这对那些与NullPointerException斗争的人很有用,因为它迫使您考虑可能根本没有结果。 这不能解决未初始化字段的问题。
我个人不喜欢它,因为它解决了一个问题,可以通过正确处理null来解决更广泛的问题,但是我认为对于许多人来说,这是一种改进。
一个常见的问题是; 我应该如何知道变量为空? 这是我心中错误的方法。 应该是,为什么要假设它不能为null? 如果您不能回答该问题,则必须假定它可以为null,并且如果不进行检查,NPE也就不会感到惊讶。
您可能会争辩说Java可以使用更多的语法糖来制作处理Elvis运算符之类的用于null清除程序的代码,但是我认为问题在于开发人员并未充分考虑null值。 例如,在打开它之前是否检查枚举变量是否为null? (我认为应该有一个case null
的case null
:在switch中,但不存在或陷入default
:但事实并非如此)
快速编写代码有多重要?
Java不是一种简洁的语言,并且没有IDE来为您编写一半的代码,如果您花了一整天时间编写代码,那么编写esp确实会很痛苦。
但这就是开发人员整日不做的事情吗? 实际上,他们没有。 开发人员不会花费很多时间来编写代码,他们会花费90%(对于新代码)到99%(对于遗留代码)来理解问题 。
你可能会说; 我整天和以后写了1000行代码,然后重新编写代码(通常使代码更短),一段时间后我修复了代码。但是,尽管您仍然想起这段代码,但是如果您只是编写最后需要的代码(或者从打印输出中完成),然后将其除以在项目上花费的总时间,从头到尾,您可能会发现实际上每天少于100行代码,每天可能少于10行。
因此,如果那段时间不是写最终产品,那您实际上在做什么? 据了解,最终用户需要什么,以及实施该解决方案需要什么。
曾经有人告诉我; 如果您在错误的位置钻Kong,则无论钻Kong多快,多大,有多深或有多少Kong都无所谓。
结论
我听到了从初学者到杰出开发人员的观点,声称您不应该/我无法想象为什么/如果您使用X,您应该被解雇,应该只使用Y。我发现这样的陈述很少100%准确。 通常,要么存在边际情况,有时在非常常见的情况下,这种陈述具有误导性或完全不正确。
我会以怀疑的态度对待任何这样的广泛评论,而且他们常常发现一旦看到其他人的观点不一致,他们就必须限定所说的内容。
翻译自: https://www.javacodegeeks.com/2015/05/what-are-the-bad-features-of-java.html