我是初学者,我一直都认为重复代码是不好的。但是,似乎为了不这样做,通常需要额外的方法调用。假设我有以下课程
public class BinarySearchTree<E extends Comparable<E>>{
private BinaryTree<E> root;
private final BinaryTree<E> EMPTY = new BinaryTree<E>();
private int count;
private Comparator<E> ordering;
public BinarySearchTree(Comparator<E> order){
ordering = order;
clear();
}
public void clear(){
root = EMPTY;
count = 0;
}
}
将clear()方法中的两行复制并粘贴到构造函数中而不是调用实际方法,对我来说是否更为理想?如果是这样,它会产生多大的差异?如果我的构造函数进行了10次方法调用,每次调用只是将一个实例变量设置为一个值,该怎么办什么是最好的编程实践?
答案 0 :(得分:67)
将clear()方法中的两行复制并粘贴到构造函数中而不是调用实际方法,对我来说是否更为理想?
编译器可以执行该优化。 JVM也是如此。编译器编写者和JVM作者使用的术语是“内联扩展”。
如果是这样,它会产生多大的差异?
测量它。通常,你会发现它没有任何区别。如果你认为这是一个性能热点,那你就是在错误的地方;这就是你需要测量它的原因。
如果我的构造函数进行了10次方法调用,每次调用只是将一个实例变量设置为值?
,该怎么办?
同样,这取决于生成的字节码和Java虚拟机执行的任何运行时优化。如果编译器/ JVM可以内联方法调用,它将执行优化以避免在运行时创建新堆栈帧的开销。
什么是最好的编程习惯?
避免过早优化。最佳做法是编写可读且设计良好的代码,然后针对应用程序中的性能热点进行优化。
答案 1 :(得分:18)
其他人对优化的看法绝对正确。
从性能的角度来看,没有理由来内联方法。如果这是性能问题,JVM中的JIT将内联它。在java中,方法调用非常接近自由,因此不值得考虑它。
话虽如此,这里有一个不同的问题。也就是说,是错误的编程习惯,从构造函数中调用可重写的方法(即,不是final
,static
或private
的方法)。 (Effective Java,2nd Ed。,p.89,标题为“继承的设计和文档或禁止它”的项目)
如果有人添加一个名为BinarySearchTree
的{{1}}子类,会使用以下代码覆盖所有公共方法,会发生什么:
LoggingBinarySearchTree
然后public void clear(){
this.callLog.addCall("clear");
super.clear();
}
永远不会被构建!问题是,当LoggingBinarySearchTree
构造函数正在运行时,this.callLog
将为null
,但被调用的BinarySearchTree
是被覆盖的clear
,您将获得NullPointerException
1}}。
请注意,Java和C ++在这里有所不同:在C ++中,调用virtual
方法的超类构造函数最终会调用超类中定义的那个,而不是被覆盖的类。在两种语言之间切换的人有时会忘记这一点。
考虑到这一点,我认为从构造函数调用时内联clear
方法可能更简洁,但通常在Java中你应该继续使用所有方法你想要的电话。
答案 2 :(得分:5)
我肯定会保持原样。如果更改clear()
逻辑怎么办?找到复制2行代码的所有地方是不切实际的。
答案 3 :(得分:3)
最佳做法是测量两次并切一次。
一旦浪费时间优化,你再也无法取回它了! (所以先测量它,然后问问自己是否值得优化。你节省多少实际时间?)
在这种情况下,Java VM可能已经在进行您正在讨论的优化。
答案 4 :(得分:3)
一般来说(作为初学者,这意味着永远!)你永远不应该像你正在考虑的那样进行微观优化。总是喜欢这样的事情的可读性。
为什么呢?因为编译器/热点会在运行中为您进行这些类型的优化,还有很多甚至更多。如果有的话,当你尝试沿着这些行进行优化时(虽然不是这种情况),你可能会让事情变得更慢。 Hotspot了解常见的编程习惯用法,如果你自己尝试进行优化,可能无法理解你想要做的事情,因此它无法对其进行优化。
还有更高的维护成本。如果你开始重复代码,那么维护将会更加努力,这可能比你想象的要麻烦得多!
顺便说一句,你可能会在你的编码生活中得到一些你需要进行低级别优化的点 - 但是如果你达到这些点,你肯定会知道什么时候到来。如果不这样做,如果需要,您可以随时返回并进行优化。
答案 5 :(得分:2)
方法调用的 cost 是堆栈帧的创建(和处理)以及一些额外的字节代码表达式,如果您需要将值传递给方法。
答案 6 :(得分:1)
我遵循的模式是这个方法是否满足以下条件之一:
如果以上任何一种情况属实,那么它应该用它自己的方法包装。
答案 7 :(得分:1)
保持clear()
方法有助于提高可读性。拥有不可维护的代码会更加昂贵。
答案 8 :(得分:1)
优化编译器通常可以很好地消除这些“额外”操作的冗余;在许多情况下,“优化”代码和代码之间的区别只是按照您想要的方式编写,并且通过优化编译器运行是没有的;也就是说,优化编译器通常做得和你一样好,而且它可以在不造成源代码任何降级的情况下完成。事实上,很多时候,“手动优化”代码最终效率低,因为编译器在进行优化时会考虑很多事情。将代码保留为可读格式,不要担心以后的优化。
“过早优化是其根源 所有的邪恶。“ - 唐纳德克努特
答案 9 :(得分:0)
我不会担心方法调用,但方法的逻辑。如果它是关键系统,并且系统需要“快速”,那么我会考虑优化需要很长时间才能执行的代码。
答案 10 :(得分:0)
鉴于现代计算机的记忆,这是非常便宜的。将代码分解为方法总是更好,这样有人可以快速阅读最新情况。如果错误仅限于具有几行主体的单个方法,它还有助于缩小代码中的错误。
答案 11 :(得分:0)
正如其他人所说,方法调用的成本是微不足道的,因为编译器会为你优化它。
也就是说,从构造函数中对实例方法进行方法调用存在危险。您冒着以后更新实例方法的风险,因此它可能会尝试使用构造函数尚未启动的实例变量。也就是说,您不一定要将构造活动与构造函数分开。
另一个问题 - 你的clear()方法将root设置为EMPTY,它在创建对象时初始化。如果然后将节点添加到EMPTY,然后调用clear(),则不会重置根节点。这是你想要的行为吗?