1. 尽量少用new 生成新对象
用new 创建类的实例时,构造函数链中所有构造函数都会被自动调用,操作速度较慢。在某些时候可复用现有对象。比如在进行大量String 操作时,可用StringBuffer 类代替String 类,以避免生成大量的对象。
一般认为,String 是不可变的,StringBuffer 和StringBuilder 是可变的,而StringBuffer 是线程安全的,而StringBuilder 不是。
先看下面的代码:
public static void main(String[] args) { for (int i = 0; i < 10; i++) { long s1 = System.nanoTime(); String s = "who" + " is " + "alex J" + "?"; System.out.println(System.nanoTime() - s1); long s2 = System.nanoTime(); StringBuilder b = new StringBuilder("who").append(" is ") .append("alex J").append("?"); System.out.println(System.nanoTime() - s2); System.out.println(s); System.out.println(b); } }
在这里,String 的速度会比StringBuilder 快,因为在JVM 中,String 对象的拼接就被解释成StringBuffer 对象的拼接,其实,在JVM 中s 就是:
String s = "who is alex J?"
但,如果是来自不同对象的拼接,情况就不一样了,下面的代码则会稍慢:
String s1 = "who"
String s2 = " is "
String s3 = "alex J?"
String s4 = s1 + s2 + s3 ;
再看这个:
String ss1 = "Hello, World";
String ss2 = new String( "Hello, World" ) ;
ss1 看起来和java 的基本数据类型的赋值操作(int=1 )类似,而ss2 看起来像是用一个字符串对象(如果“Hello World ”也是字符串对象的话)创建一个字符串对象。在前面的所提到的常量池中,有一个常量池项体的一个类型值:
CONSTANT_String 8 String 型常量
正因为这样,在常量池中有专门为String 类型设置类型值,在java 程序的编译阶段,就已经将形如(“Hello World ”)的代码作为了字符串常量存放在常量池中,如果程序有需要用到它的放,则就根据这个字符串常量在常量池中的地址去寻找,这和整型常量,浮点型常量是没有区别的:
String ss1 = "Hello, Word!" ;
int i = 4;
double d = 4.3D;
我们在后面谈到了java 的堆和栈的内容中,java 的运行时数据区分为6 个部分,其中有一个部分就是运行时常量池,在编译期,.class 文件的二进制流中的常量二进制流就会被存储到这个区域,当程序需要用到常量值时,直接在这块内存中寻找即可。
关于常量池,再扯点别的东西,下页面的例子
String A = "Hello, Word!" ;
String B = "Hello, Word!" ;
System. out .println(A == B); // true
String C = new String( "Hello, Word!" );
String D = new String( "Hello, Word!" );
System. out .println(C == D); // false
前一个的输出结构为TRUE ,因为“== ”所比较的是两个变量存储的内容,在A 和B 中所存储的都是字符串“hello world ”在常量池中的地址,当我们使用A 和B 时,就根据这个地址去常量池中寻找此地址内所存储的内容;而后一个的输出结果为FALSE ,因为C 和D 存储的是JVM 在堆中开辟的两个对象的内存地址,这两个内存地址显然是不同的,所以程序输出FALSE 。
String a = "hello" ;
String b = "word" ;
String ab = a + b;
String s = "helloword" ;
System. out .println(ab == s); // False
在这段代码中, a+b 被编译以后使用的是 StringBuilder.append(String) 方法。 JVM 会在堆中创建一个 StringBuilder 类,将 a 所指向常量池中的内容 "ab" 传入,然后调用 append(b 所指向的常量池内容 ) 完成字符串合并功能,最后将堆中 StringBuilder 对象的地址赋给变量 ab ,而 s 存储的是常量池中 "abcd" 的地址。 ab 与 s 地址当然不一样了。
String s1 = "ab" + "cd" ;
String s2 = "abcd" ;
System. out .println(s1 == s2); // true
在这段代码中, "ab"+"cd" 会直接在编译阶段就合并成常量 "abcd" ,它们并没有去堆中开辟空间,创建对象,所以相同的字符串在常量池中的地址也相同了。
StringBuffer 的使用:
StringBuffer 表示了可变的可写的字符串。
有三个构造方法 :
StringBuffer (); // 默认分配 16 个字符的空间
StringBuffer (int size); // 分配 size 个字符的空间
StringBuffer (String str); // 分配 16 个字符 +str.length() 个字符空间
可以通过 StringBuffer 的构造函数来设定它的初始化容量,这样可以明显地提升性能。这里提到的构造函数是 StringBuffer(int length) , length 参数表示当前的 StringBuffer 能保持的字符数量。也可以使用 ensureCapacity(int minimumcapacity) 方法在 StringBuffer 对象创建之后设置它的容量。首先我们看看 StringBuffer 的缺省行为,然后再找出一条更好的提升性能的途径。
StringBuffer 在内部维护一个字符数组,当你使用缺省的构造函数来创建 StringBuffer 对象的时候,因为没有设置初始化字符长度, StringBuffer 的容量被初始化为 16 个字符,也就是说缺省容量就是 16 个字符。当 StringBuffer 达到最大容量的时候,它会将自身容量增加到当前的 2 倍再加 2 ,也就是( 2* 旧值 +2 )。如果你使用缺省值,初始化之后接着往里面追加字符,在你追加到第 16 个字符的时候它会将容量增加到 34 ( 2*16+2 ),当追加到 34 个字符的时候就会将容量增加到 70 ( 2*34+2 )。无论何事只要 StringBuffer 到达它的最大容量它就不得不创建一个新的字符数组然后重新将旧字符和新字符都拷贝一遍 ―― 这也太昂贵了点。所以总是给 StringBuffer 设置一个合理的初始化容量值是错不了的,这样会带来立竿见影的性能增益。
StringBuffer 初始化过程的调整的作用由此可见一斑。所以,使用一个合适的容量值来初始化 StringBuffer 永远都是一个最佳的建议。
而以上这些分析都可以从 String 和 StringBuffer 的源码中找到答案:
//String public final class String { private final Char value[]; public String (String original) { /** * 此处为具体代码实现 * 把原字符窜original切成字符数组赋值给value[]; **/ } //StringBuffer public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence { // 继承了父类 AbstractStringBuilder 的value[] char value[]; int count; //继承父类构造器 创建一个大小为str.length + 16 的value[]数组 super(str.length() + 16); // 将str切成字符序列加入到value[]中 append(str); }
2. 使用clone() 方法生成新对象
如果一个对象实现了cloneable 接口, 就可以调用它的clone() 方法。clone() 方法不会调用任何类构造函数, 比使用new 方法创建实例速度要快。
a )理解 clone 方法
实现 Cloneable 接口可以得到语言之外的一种机制:无需调用构造器就可以创建对象。它的约定内容是:创建和返回该对象的一个拷贝。这个“拷贝”的一般含义是:
对于任何对象 x ,表达式
X . clone () ! = x 正确
X . clone (). getClass () == X . getClass () 正确
X . clone (). equals ( x ) 如果对象 x 的 equals 方法定义恰当,正确
它的精确含义是“浅复制”。
b )使用 clone 方法
用new 关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable 接口,我们就可以调用它的clone() 方法。clone() 方法不会调用任何类构造函数。在使用设计模式(Design Pattern )的场合,如果用Factory 模式创建对象,则改用clone() 方法创建新的对象实例会非常简单。
c ) 谨慎覆盖clone 方法
在《effective java 》一书中,作者提供了“谨慎地覆盖clone ”这样一条经验法则,究其原因,是因为clone 方法为我们提供了一种java 语言之外的机制,构造对象不需要构造器就可以实现,这是一种危险的行为,而且一般程序员所实现的clone 方法仅仅是对所要复制对象的一种浅复制,它并不能支持一个良好的系统应用。所以作者提出,如果需要使用clone 方法,则程序员必须提供一个行为良好的clone 方法,实现深度拷贝的功能,但既然Cloneable 接口有许多问题,一些专家干脆从来不去覆盖clone 方法,也从不条用它,除非拷贝数组。
所以,当程序员在使用clone 方法的时候,即必须在系统性能上所得到的改善和在实现clone 方法所需要提供的风险上权衡,记住一点:如果未能提供一个行为良好的受保护的(protected )clone 方法,它的子类就不能实现Cloneable 接口。
3. 尽量使用局部变量
调用方法时传递的参数及调用中创建的临时变量保存在栈(Stack) 中,速度较快。其他变量, 如静态变量、实例变量都在堆(Heap) 中创建, 速度较慢。
a) 理解堆和栈
栈与堆都是Java 用来在RAM 中存放数据的地方。与C++ 不同,Java 自动管理栈和堆,程序员不能直接地设置栈或堆,这是什么意思呢?其实,java 里提到的堆和栈应该是逻辑层面上的,是jvm 划分的,并不同于C++ 的实际运行时内存,因此根本不是一个层面上的东西,而事实上,有了JVM 的帮忙,我们更擅长处理逻辑层面上的东西,而不需要考虑繁琐的物理细节。
Java 的堆是一个运行时数据区 , 类的对象从中分配空间。这些对象通过new 、new array 、anewarray 和multianewarray 等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java 的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势从逻辑上看,可以认为比堆快,上面已经提到,java 所谓的堆和栈是逻辑层面的,在《The Java Virtual Machine Specification 》一书中,将运行时数据区分为pc 寄存器 、java 虚拟机栈 、堆 、方法区 、运行时常量池 、本地方法栈 共6 个部分,而一般认为“栈比堆快,比CPU 寄存器慢”的后半句是无意义的,因为CPU 中的寄存器是物理实体。
堆,栈,常量池的关系可以用下图表示:
4. 减少方法调用
面向对象设计的一个基本准则是通过方法间接访问对象的属性,但方法调用会占用一些开销访问。可以避免在同一个类中通过调用函数或方法(get 或set) 来设置或调用该对象的实例变量, 这比直接访问变量要慢。为了减少方法调用,可以将方法中的代码复制到调用方法的地方, 比如大量的循环中, 这样可以节省方法调用的开销。但带来性能提升的同时会牺牲代码的可读性, 可根据实际需要平衡两者关系。
5. 使用final 类和final 、static 、private 方法
带有final 修饰符的类是不可派生的。如果指定一个类为final , 则该类所有的方法都是final 。JAVA 编译器会寻找机会内联(inline) 所有的final 方法。此举能够提升程序性能。使用final 、static 、private 的方法是不能被覆盖的,JAVA 不需要在程序运行的时候动态关联实现方法, 从而节省了运行时间。