【Java】變量聲明在循環(huán)體內(nèi)還是循環(huán)體外你用哪一個?
引言
最近刷知乎的時候看到一個比較有意思的問題,變量聲明在循環(huán)體內(nèi)還是循環(huán)體外?這個問題有人認(rèn)為應(yīng)該定義循環(huán)體外,不應(yīng)該定義在循環(huán)體內(nèi)。很多java代碼優(yōu)化建議都有這么一條建議:循環(huán)內(nèi)不要不斷創(chuàng)建對象引用 例如:
- for (int i = 1; i <= count; i++){
- Object obj = new Object();
- }
這種做法會導(dǎo)致內(nèi)存中有count份Object對象引用存在,count很大的話,就耗費內(nèi)存了,建議為改為:
- Object obj = null;
- for (int i = 0; i <= count; i++) {
- obj = new Object();
- }
這樣的話,內(nèi)存中只有一份Object對象引用,每次new Object()的時候,Object對象引用指向不同的Object罷了,但是內(nèi)存中只有一份,這樣就大大節(jié)省了內(nèi)存空間了。這條建議應(yīng)該也出現(xiàn)過在很多公司的代碼規(guī)范上了吧。下面我們就來分析下變量聲明在循環(huán)體內(nèi)和變量聲明循環(huán)體外的情況。
效率對比
首先我們先來看看寫在循環(huán)體內(nèi)和詢環(huán)體外的效率比對,測試代碼如下:
- /**
- * @author: 公眾號【java金融】
- * @Date:
- * @Description:
- */
- @BenchmarkMode(Mode.AverageTime) // 測試完成時間
- @OutputTimeUnit(TimeUnit.NANOSECONDS)
- @Warmup(iterations = 2) // 預(yù)熱 2 輪,每次 1s
- @Measurement(iterations = 5) // 測試 5 輪,每次 1s
- @Fork(1) // fork 1 個線程
- @State(Scope.Thread)
- public class ForEachBenchMark {
- public static void main(String[] args) throws RunnerException {
- Options opt = new OptionsBuilder()
- .include(ForEachBenchMark.class.getSimpleName())
- .result("result.json")
- .resultFormat(ResultFormatType.JSON).build();
- new Runner(opt).run();
- }
- @Param(value = {"10", "50", "100"})
- private int length;
- /**
- * 循環(huán)體外創(chuàng)建對象
- * @param blackhole
- */
- @Benchmark
- public void outsideLoop(Blackhole blackhole) {
- Object object = null;
- for (int i = 0; i < length; i++) {
- object = new Object();
- blackhole.consume(object);
- }
- }
- /**
- * 循環(huán)體內(nèi)創(chuàng)建對象
- * @param blackhole
- */
- @Benchmark
- public void insideLoop(Blackhole blackhole) {
- for (int i = 0; i < length; i++) {
- Object object = new Object();
- blackhole.consume(object);
- }
- }
- }
測試結(jié)果如下:
- Benchmark (length) Mode Cnt Score Error Units
- ForEachBenchMark.insideLoop 10 avgt 5 58.629 ± 8.857 ns/op
- ForEachBenchMark.insideLoop 50 avgt 5 293.726 ± 1.856 ns/op
- ForEachBenchMark.insideLoop 100 avgt 5 587.185 ± 40.424 ns/op
- ForEachBenchMark.outsideLoop 10 avgt 5 59.563 ± 5.057 ns/op
- ForEachBenchMark.outsideLoop 50 avgt 5 305.829 ± 27.476 ns/op
- ForEachBenchMark.outsideLoop 100 avgt 5 584.853 ± 20.289 ns/op
我們可以發(fā)現(xiàn)不管在循環(huán)外創(chuàng)建對象和循環(huán)內(nèi)創(chuàng)建對象時間幾乎都是一樣的。
字節(jié)碼對比
下面我們準(zhǔn)備兩個測試類
- public class InsideTest {
- public static int count = 100;
- public List<Object> insideLoop() {
- List<Object> list = new ArrayList<>();
- int n = 0;
- for (; ; ) {
- if (n > count) {
- break;
- }
- Object o = new Object();
- list.add(o);
- }
- Object b = 2;
- return list;
- }
- }
- public class OutsideTest {
- public static int count = 100;
- public List<Object> outsideLoop() {
- List<Object> list = new ArrayList<>();
- Object o = null;
- int n = 0;
- for (; ; ) {
- if (n > count) {
- break;
- }
- o = new Object();
- list.add(o);
- }
- Object b = 2;
- return list;
- }
這兩個編譯后字節(jié)碼幾乎一模一樣,除了循環(huán)體外(OutsideTest )常量池多了一個Object o = null變量還有的話就是LocalVariableTable有點區(qū)別,變量在循環(huán)體內(nèi)的話公用了一個變量槽(o和b變量) outsideLoop在stack frame中定義了4個slot, 而intsideLoop只定義了3個slot 在outsideLoop中,變量o和b分別占用了不同的slot,在intsideloop中,變量o和b復(fù)用一個slot。所以outsideLoop的stack frame比intsideLoop多占用1個solt內(nèi)存。執(zhí)行以下命令就可以找到字節(jié)碼中的LocalVariableTable。
- javac -g OutsideTest.java
- javap -v OutsideTest.class
- LocalVariableTable:
- Start Length Slot Name Signature
- 28 8 3 o Ljava/lang/Object;
- 0 46 0 this Lcom/workit/autoconfigure/autoconfigure/controller/InsideTest;
- 8 38 1 list Ljava/util/List;
- 10 36 2 n I
- 44 2 3 b Ljava/lang/Object;
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 49 0 this Lcom/workit/autoconfigure/autoconfigure/controller/OutsideTest;
- 8 41 1 list Ljava/util/List;
- 10 39 2 o Ljava/lang/Object;
- 12 37 3 n I
- 47 2 4 b Ljava/lang/Object;
這是比較極端的情況下有1個solt的差距,如果把上述的代碼 Object b = 2;就不會存在solt復(fù)用了。
總結(jié)
整體看下來貌似內(nèi)存和效率都差不多。從“「局部變量作用域最小化」”原則上來說,變量聲明在循環(huán)體內(nèi)更合適一點,這樣代碼的閱讀性更好。
本文轉(zhuǎn)載自微信公眾號「java金融」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系java金融公眾號。