Java OO设计中的常见问题

分类: JVM 发布于:

这个与OOP编程的哲学有关, 形而上 属于一部分,形而下属于一部分。

抽象类与实现类的变量初始化顺序问题

public abstract class Widget {
	        private final int cachedWidth;
	        private final int cachedHeight;
	
	        public Widget() {
	            this.cachedWidth = width();
	            this.cachedHeight = height();
	        }
	
	        protected abstract int width();
	        protected abstract int height();
	    }

        public class SquareWidget extends Widget {
	        private final int size;
	
	        public SquareWidget(int size) {
	            this.size = size;
	        }
	
	        @Override
	        protected int width() {
	            return size;
	        }
	
	        @Override
	        protected int height() {
	            return size;
	        }
	    }

上述实现过程的输出都是 0

潜在问题

  • 子类的构造发生在父类构造函数之后,在jvm字节码的实现也反应了这个问题。 在子类的实例化过程,先调用父类constructor,再调用子类的构造函数。

Java中的枚举类型的效率问题

  • 问题

枚举类型与final 整形字段的效率比较

class Test {

    // 定义两个静态final类型的字段
    static final int ONE = 1;
    static final int TWO = 2;

    // 两个枚举类型
    enum TestEnum {ONE, TWO}

    public static void main(String[] args) {
        testEnum();
        testInteger();
        time("enum", new Runnable() {
            public void run() {
                testEnum();

            }
        });
        time("integer", new Runnable() {
            public void run() {
                testInteger();
            }
        });
    }
    // 循环 1G次 enum取值
    private static void testEnum() {
        TestEnum value = TestEnum.ONE;
        for (int i = 0; i < 1000000000; i++) {
            if (value == TestEnum.TWO) {
                System.err.println("impossible");
            }
        }
    }
    // 循环 1G次 final字段   
    private static void testInteger() {
        int value = ONE;
        for (int i = 0; i < 1000000000; i++) {
            if (value == TWO) {
                System.err.println("impossible");
            }
        }
    }

    private static void time(String name, Runnable runnable) {
        long startTime = System.currentTimeMillis();
        runnable.run();
        System.err.println(name + ": " + (System.currentTimeMillis() - startTime) + " ms");
    }
}

输出结果

enum: 11 ms
integer: 2 ms

效率差距的原因在于编译后的指令效率上

testEnum 的字节码

 getstatic #11 <com/baeldung/concurrent/callable/Test$TestEnum.ONE>
 3 astore_0
 4 iconst_0
 5 istore_1
 6 iload_1
 7 ldc #12 <1000000000>
 9 if_icmpge 33 (+24)
12 aload_0
13 getstatic #13 <com/baeldung/concurrent/callable/Test$TestEnum.TWO>
16 if_acmpne 27 (+11)
19 getstatic #14 <java/lang/System.err>
22 ldc #15 <impossible>
24 invokevirtual #16 <java/io/PrintStream.println>
27 iinc 1 by 1
30 goto 6 (-24)
33 return

testInteger 函数的字节码

 iconst_1
 1 istore_0
 2 iconst_0
 3 istore_1
 4 iload_1
 5 ldc #12 <1000000000>
 7 if_icmpge 29 (+22)
10 iload_0
11 iconst_2
12 if_icmpne 23 (+11)
15 getstatic #14 <java/lang/System.err>
18 ldc #15 <impossible>
20 invokevirtual #16 <java/io/PrintStream.println>
23 iinc 1 by 1
26 goto 4 (-22)
29 return

enum中编译后的值存储在常量池中,使用getstatic取出

getstatic #13 <com/baeldung/concurrent/callable/Test$TestEnum.TWO>

final staic 直接作为coast常量存在代码区, 从上述运行结果看 这两个指令是5倍的效率差距。

字符串的应用效率问题

𝟣 String label = “TEST”;

𝟤 String label = new String(“Test”);

以上两种获取label的方式有什么区别?

  • 编译后的字节码对比
 0 ldc #2 <TEST>  # String label = "TEST";  直接从常量池中加载

 2 astore_1
 3 new #3 <java/lang/String>  # 第二中方式new一个对象
 6 dup
 7 ldc #4 <Test> # 也从常量池中加载同一个数据
 9 invokespecial #5 <java/lang/String.<init>> # 调用构造函数
12 astore_2
13 return

可见,第一种方式明显比第一种快。

  • 第一种方式 直接从常量池中加载常量。
  • 第二种方式 先new一个对象,然后从常量池中加载构造函数用到的字符串,最后调用构造函数。