一、對常量使用靜態 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
。
此外,即使對於整數,某些處理器擁有硬件乘法器,卻缺少硬件除法器。在這種情況下,整數的除法和取模運算會在軟件中執行;如果您要設計哈希表或要進行大量數學運算,則需要考慮這一點。