欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-04 23:45:53
...

一、对常量使用静态 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()

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

每当需要访问外部类中的 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

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