性能优化专题十--高效java编码

一、对常量使用静态 final

下面是位于类顶部的声明:

    static int intVal = 42;
    static String strVal = "Hello, world!";
    

编译器会生成一个名为 <clinit> 的类初始化器方法,当第一次使用该类时,系统会执行此方法。此方法会将值 42 存储到 intVal,并从类文件字符串常量表中提取 strVal 的引用。以后引用这些值时,可以通过查询字段访问它们。

我们可以使用“final”关键字加以改进:

    static final int intVal = 42;
    static final String strVal = "Hello, world!";

此类不再需要 <clinit> 方法,因为常量会进入 dex 文件中的静态字段初始化器。引用 intVal 的代码将直接使用整数值 42,并且对 strVal 的访问将使用成本相对较低的“字符串常量”指令,而非字段查询

注意:此优化仅适用于原语类型和 String 常量,不适用于任意引用类型。尽管如此,最好还是尽可能声明常量 static final

二、增强型 for 循环使用场所

对于实现 Iterable 接口的集合以及数组,可以使用增强型 for 循环(有时也称为“for-each”循环)。对于集合,系统会分配迭代器以对 hasNext() 和 next() 进行接口调用。对于 ArrayList,手写计数循环的速度快约 3 倍(有或没有 JIT),但对于其他集合,增强型 for 循环语法与使用显式迭代器完全等效。

遍历数组有以下几种替代方案:

    static class Foo {
        int splat;
    }

    Foo[] array = ...

    public void zero() {
        int sum = 0;
        for (int i = 0; i < array.length; ++i) {
            sum += array[i].splat;
        }
    }

    public void one() {
        int sum = 0;
        Foo[] localArray = array;
        int len = localArray.length;

        for (int i = 0; i < len; ++i) {
            sum += localArray[i].splat;
        }
    }

    public void two() {
        int sum = 0;
        for (Foo a : array) {
            sum += a.splat;
        }
    }
    

zero() 速度最慢,因为 JIT 还无法消除每次循环迭代都要获取数组长度这项成本。

one() 速度更快。它会将所有内容都提取到局部变量中,避免查询。只有数组长度方面具有性能优势。

对于没有 JIT 的设备,two() 速度最快;对于具有 JIT 的设备,two() 与 one() 速度难以区分。two() 使用了在 1.5 版 Java 编程语言中引入的增强型 for 循环语法。

因此,应默认使用增强型 for 循环,但对于性能关键型 ArrayList 迭代,不妨考虑使用手写计数循环。

提示:另请参阅 Josh Bloch 的《Effective Java》第 46 条。

三、对于私有内部类,考虑使用包访问权限,而非私有访问权限

请查看以下类定义:

    public class Foo {
        private class Inner {
            void stuff() {
                Foo.this.doStuff(Foo.this.mValue);
            }
        }

        private int mValue;

        public void run() {
            Inner in = new Inner();
            mValue = 27;
            in.stuff();
        }

        private void doStuff(int value) {
            System.out.println("Value is " + value);
        }
    }

对于上述代码,需要注意的是,我们定义了一个私有内部类 (Foo$Inner),它会直接访问外部类中的私有方法和私有实例字段。这是合乎规则的,并且代码会按预期输出“Value is 27”。

问题在于,虚拟机认为从 Foo$Inner 直接访问 Foo 的私有成员不符合规则,因为 Foo 和 Foo$Inner 属于不同的类,虽然 Java 语言允许内部类访问外部类的私有成员。为了消除这种差异,编译器会生成一些合成方法:

    /*package*/ static int Foo.access$100(Foo foo) {
        return foo.mValue;
    }
    /*package*/ static void Foo.access$200(Foo foo, int value) {
        foo.doStuff(value);
    }

javac Foo.java后,在执行javap -verbose  Foo.class,可以看到在Foo类中确实由编译器帮助生成了辅助方法,以访问到外部类中的私有成员变量MValue,以及私有方法doStuff()

每当需要访问外部类中的 mValue 字段或调用外部类中的 doStuff() 方法时,内部类代码就会调用这些静态方法。这意味着以上代码实际上可以归结为一种情况,那就是您通过访问器方法访问成员字段。之前我们讨论了访问器的速度比直接访问字段要慢,因此这是一个特定习惯用语会对性能产生“不可见”影响的示例。

如果您在性能关键位置 (hotspot) 使用这样的代码,则可以将内部类访问的字段和方法声明为拥有包访问权限(而非私有访问权限),从而避免产生相关开销。遗憾的是,这意味着同一软件包中的其他类可以直接访问这些字段,因此不应在公共 API 中使用此方法。

四、避免枚举,浮点数的使用。

  • 使用自定义注解代替枚举

单个枚举会使应用的 classes.dex 文件增加大约 1.0 到 1.4KB 的大小。这些增加的大小会快速累积,产生复杂的系统或共享库。如果可能,请考虑使用 @IntDef 注释和代码缩减移除枚举并将它们转换为整数。此类型转换可保留枚举的各种安全优势。

日常我们使用枚举来定义一些常量的取值,使用枚举能够确保参数的安全性。但是Android开发文档上指出,使用枚举会比使用静态变量多消耗两倍的内存,应该尽量避免在Android中使用枚举,那么枚举为什么会更消耗内存呢?下面一起分析一下。

public enum  Sex {
    MAN, WOMAN;
}

从反编译的代码来看,我们定义的枚举,编译器会将其转换成一个类,这个类继承自java.lang.Enum类,除此之外,编译器还会帮我们生成多个枚举类的实例,赋值给我们定义的枚举类型常量,并且还声明了一个枚举对象的数组,保存了所有的枚举对象。下面我们分别来计算一下采用静态变量和枚举占用内存的大小对比。
下面是反编译后的枚举类文件,可以看到明显比我们想象中的要占用更多内存空间:

public final class Sex extends Enum {
    public static Sex[] values()
    {
        return (Sex[])$VALUES.clone();
    }
 
    public static Sex valueOf(String s)
    {
        return (Sex)Enum.valueOf(com/liunian/androidbasic/enumtest/Sex, s);
    }
 
    private Sex(String s, int i)
    {
        super(s, i);
    }
 
    public static final Sex MAN;
    public static final Sex WOMAN;
    private static final Sex $VALUES[];
 
    static 
    {
        MAN = new Sex("MAN", 0);
        WOMAN = new Sex("WOMAN", 1);
        $VALUES = (new Sex[] {
            MAN, WOMAN
        });
    }
}

枚举占用内存的大小比静态变量多得多,枚举类型数据的内存优化,使用注解的方案

  • 避免使用浮点数

一般来讲,在 Android 设备上,浮点数要比整数慢约 2 倍。

在速度方面,float 和 double 在更现代的硬件上没有区别。在空间方面,double 所占空间大 2 倍。对于台式机,假定空间不是问题,您应该优先使用 double,而非 float

此外,即使对于整数,某些处理器拥有硬件乘法器,却缺少硬件除法器。在这种情况下,整数的除法和取模运算会在软件中执行;如果您要设计哈希表或要进行大量数学运算,则需要考虑这一点。

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/106310913
今日推荐