1. 声明是什么?
String s = "Hello world!";
许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。
这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指
1. 声明是什么?
String s = "Hello world!";
许多人都做过这样的事情,但是,我们到底声明了什么?回答通常是:一个String,内容是“Hello world!”。这样模糊的回答通常是概念不清的根源。如果要准确的回答,一半的人大概会回答错误。
这个语句声明的是一个指向对象的引用,名为“s”,可以指向类型为String的任何对象,目前指向"Hello world!"这个String类型的对象。这就是真正发生的事情。我们并没有声明一个String对象,我们只是声明了一个只能指向String对象的引用变量。所以,如果在刚才那句语句后面,如果再运行一句:
String string = s;
我们是声明了另外一个只能指向String对象的引用,名为string,并没有第二个对象产生,string还是指向原来那个对象,也就是,和s指向同一个对象。
2. String类的特殊性
1) String s1 = “Hello”; //产生一个String ”Hello”对象,并产生该对象的一个别名s1来引用该对象
String s2 = “Hello”; //又产生一个别名s2来引用上面的”Hello”对象
s1 == s2 = true; //由于是同一个对象所以“==”返回为true
s1 = “World”; //产生一个String ”World”对象, s1的引用不再指向“Hello”而是指向对象”World”
s1 == s2 = false; //由于不是同一个对象所以“==”返回为false
s1 = “Hello”; //同上面的String s2 = “Hello”; 现在s1又指向对象”Hello”, 因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。
s1 == s2 = true; //由于是同一个对象所以“==”又返回为true了
s1 = s1 + “World”; //这时又产生一个对象”HelloWord”,s1不再指向”Hello”而是指向”HelloWord”
s1 == s2 = false; //不是一个对象当然是false拉
s1 = s1+ "a"+"b"+"c"+…; // String不停的创建对象,影响性能,这种易变的String用StringBuffer会得到更好的性能
StringBuffer s3 = new StringBuffer(“Hello”);
s3.append(“a”); //没有生成新的对象,而是将s3引用的对象内容改为”Helloa”
//说明: String类用来表示那些创建后就不会再改变的字符串,它是immutable的。而StringBuffer类用来表示内容可变的字符串,并提供了修改底层字符串的方法。
StingBuffer是一个可变的字符串,它可以被更改。同时StringBuffer是Thread safe的, 你可以放心的使用.
因为String被设计成一种安全的字符串, 避免了C/C++中的尴尬。因此在内部操作的时候会频繁的进行对象的交换, 因此它的效率不如StringBuffer。 如果需要频繁的进行字符串的增删操作的话最好用StringBuffer。 比如拼SQL文, 写共函。 另: 编绎器对String的+操作进行了一定的优化。
x = "a" + 4 + "c"
会被编绎成
x = new StringBuffer().append("a").append(4).append("c").toString()
但:
x = “a”;
x = x + 4;
x = x + “c”;
则不会被优化。 可以看出如果在一个表达式里面进行String的多次+操作会被优化, 而多个表达式的+操作不会被优化。
摘自:《Java API Using, Tips And Performance Tuning》
2) Integer、Boolean等wrapper类以及BigInteger、BigDecimal是immutable的,所以也有与String类似的地方,不过没有IntegerBuffer之类的东西。不过Float, Double比较特殊。如
T a1 = 10; //T代指Byte,Integer,Short,Long,Boolean。 注:应用了JDK5的AUTOBOXING
T a2 = 10;
if (a1 == a2)
System.out.println(true);
else
System.out.println(false);
这时总是true,和String有点类似
//Float时
Float i1 = (float)10.0;
Float i2 = (float)10.0;
if (i1==i2)
System.out.println(true);
else
System.out.println(false);
这时总是false
//Double时
Double i1 = 10.0;
Double i2 = 10.0;
if (i1==i2)
System.out.println(true);
else
System.out.println(false);
这时总是false
总之如果比较两个Wrapper类的值用equals,以免不必要的麻烦
3) 再看
String s1 = new String(“Hello”);
String s2 = new String(“Hello”);
s1 == s2 = false;
//因为new的时候JVM不管heap中有没有”Hello”对象都会产生一个新的”Hello”对象
String s3 = “Hello”; //重新创建对象”Hello”, 并令s3指向对象”Hello”
s3 == s1 = false; //不同对象当然false
String s4 = “Hello”;
s3 == s4 = true; //故伎重演,jvm清楚的知道哪些用了new,哪些没用new
3. 方法的参数传递中都是以reference传递,而primitive传递的是副本,但如果传递的是Integer、Boolean等wrapper类和String类的Object则是以immutable方式传递。示例:
import java.awt.Point;
class HelloWorld
{
public static void modifyPoint(Point pt, String j, int k, Integer m, Boolean b)
{
pt.setLocation(5,5);
j = "15";
k = 25;
m = 35;
b = true;
System.out.println("During modifyPoint " + "pt = " + pt +
" and j = " + j+ " and k = "+ k+
" and m = "+ m+ " and b = "+ b);
}
public static void main(String args[])
{
Point p = new Point(0,0);
String i = "10";
int k = 20;
Integer m = 30;
Boolean b = false;
System.out.println("Before modifyPoint " + "p = " + p +
" and i = " + i+ " and k = "+ k+
" and m = "+ m+ " and b = "+ b);
modifyPoint(p, i, k, m, b);
System.out.println("After modifyPoint " + "p = " + p +
" and i = " + i+ " and k = "+ k+
" and m = "+ m+ " and b = "+ b);
}
}
输出结果:
Before modifyPoint p = java.awt.Point[x=0,y=0] and i = 10 and k = 20 and m = 30 and b = false
During modifyPoint pt = java.awt.Point[x=5,y=5] and j = 15 and k = 25 and m = 35 and b = true
After modifyPoint p = java.awt.Point[x=5,y=5] and i = 10 and k = 20 and m = 30 and b = false
4. final作用于基本类型变量则该变量为恒常量;final作用于对象类型变量则该对象reference为恒量;final作用于方法则该方法不能被覆盖;final作用于class则该class不能被继承。
final使得被修饰的变量"不变",但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。
引用本身的不变:
final StringBuffer a=new StringBuffer("immutable");
final StringBuffer b=new StringBuffer("not immutable");
a=b;//编译期错误
引用指向的对象不变:
final StringBuffer a=new StringBuffer("immutable");
a.append(" broken!"); //编译通过
可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。
理解final问题有很重要的含义。许多程序漏洞都基于此----final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。
5. 怎样初始化
本问题讨论变量的初始化,所以先来看一下Java中有哪些种类的变量。
1). 类的属性,或者叫值域
2). 方法里的局部变量
3). 方法的参数
对于第一种变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。
primitive类型默认值
boolean: false
char: '\u0000' 对于未初始化的char c, c == ‘\u0000’ = true
byte: 0
short: 0
int: 0
long: 0
float: 0.0
double: 0.0
object reference: null
array: null
注意数组本身也是对象,所以没有初始化的数组引用在自动初始化后其值也是null。
对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。
对于第二种变量,必须明确地进行初始化。如果再没有初始化之前就试图使用它,编译器会抗议。如果初始化的语句在try块中或if块中,也必须要让它在第一次使用前一定能够得到赋值。也就是说,把初始化语句放在只有if块的条件判断语句中编译器也会抗议,因为执行的时候可能不符合if后面的判断条件,如此一来初始化语句就不会被执行了,这就违反了局部变量使用前必须初始化的规定。但如果在else块中也有初始化语句,就可以通过编译,因为无论如何,总有至少一条初始化语句会被执行,不会发生使用前未被初始化的事情。对于try-catch也是一样,如果只有在try块里才有初始化语句,编译部通过。如果在 catch或finally里也有,则可以通过编译。总之,要保证局部变量在使用之前一定被初始化了。所以,一个好的做法是在声明他们的时候就初始化他们,如果不知道要出事化成什么值好,就用上面的默认值吧!
其实第三种变量和第二种本质上是一样的,都是方法中的局部变量。只不过作为参数,肯定是被初始化过的,传入的值就是初始值,所以不需要初始化。
6. 尽量使用多态(polymorphism)特性而不是instanceof
7. 一旦不需要对象,尽量显式的使之为null
8. 对象之间的”=”赋值操作乃是赋值的reference, 即左边的对象也指向右边的对象,只是该reference多了一个别名而已。
9. “==”和equals()的区别
==操作符专门用来比较变量的值是否相等。比较好理解的一点是:
int a=10;
int b=10;
则a==b将是true。
但不好理解的地方是:
String a=new String("foo");
String b=new String("foo");
则a==b将返回false。
根据前一帖说过,对象变量其实是一个引用,它们的值是指向对象所在的内存地址,而不是对象本身。a和b都使用了new操作符,意味着将在内存中产生两个内容为"foo"的字符串,既然是“两个”,它们自然位于不同的内存地址。a和b的值其实是两个不同的内存地址的值,所以使用"=="操作符,结果会是 false。诚然,a和b所指的对象,它们的内容都是"foo",应该是“相等”,但是==操作符并不涉及到对象内容的比较。
对象内容的比较,正是equals方法做的事。
看一下Object对象的equals方法是如何实现的:
boolean equals(Object o){
return this==o;
}
Object 对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出, Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者。
看一下一个极端的类:
Class Monster{
private String content;
...
boolean equals(Object another){ return true;}
}
我覆盖了equals方法。这个实现会导致无论Monster实例内容如何,它们之间的比较永远返回true。
所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。
10. 不要依赖equals()的缺省实现
11. 一个equals()的实现模版
class Golfball
{
private String brand;
private String make;
private int compression;
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && getClass() == obj.getClass())
{
Golfball gb = (Golfball)obj; //Classes are equal, downcast.
if (brand.equals(gb.brand()) && //Compare attributes.
make.equals(gb.make()) &&
compression == gb.compression())
{
return true;
}
}
return false;
}
}
注意getClass() == obj.getClass()的限制,如果判断必须相等则无法比较基类和子类是否相等,完全不同的类不用考虑,完全没有可比性,除了特殊需要或很糟糕的程序。
12. 实现equals()应优先考虑使用getClass()
13. 如果某个基类我们自己实现了equals(),在它的子类中要覆盖此方法,最好调用super.equals()唤起base class的相关行为,然后再实现子类域的比较。
Example:
public boolean equals(Object obj)
{
if (this == obj) //1
return true;
if (obj != null && getClass() == obj.getClass() && //2
super.equals(obj)) //3
{
MyGolfball gb = (MyGolfball)obj; //Classes equal, downcast.
if (ballConstruction == gb.construction()) //Compare attrs.
return true;
}
return false;
}
14. 如果要在base class与derived class之间应运equals(),可以考虑instanceof来代替getClass()。对此论题的详细讨论参见:Practical Java, Practice 14
15. instanceof什么东西?
instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。举个例子:
String s = "I AM an Object!";
boolean isObject = s instanceof Object;
我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Object类的一个实例,显然,这是真的,所以返回true,也就是isObject的值为True。
instanceof有一些用处。比如我们写了一个处理账单的系统,其中有这样三个类:
public class Bill {//省略细节}
public class PhoneBill extends Bill {//省略细节}
public class GasBill extends Bill {//省略细节}
在处理程序里有一个方法,接受一个Bill类型的对象,计算金额。假设两种账单计算方法不同,而传入的Bill对象可能是两种中的任何一种,所以要用instanceof来判断:
public double calculate(Bill bill) {
if (bill instanceof PhoneBill) {
//计算电话账单
}
if (bill instanceof GasBill) {
//计算燃气账单
}
...
}
这样就可以用一个方法处理两种子类。
然而,这种做法通常被认为是没有好好利用面向对象中的多态性。其实上面的功能要求用方法重载完全可以实现,这是面向对象变成应有的做法,避免回到结构化编程模式。只要提供两个名字和返回值都相同,接受参数类型不同的方法就可以了:
public double calculate(PhoneBill bill) {
//计算电话账单
}
public double calculate(GasBill bill) {
//计算燃气账单
}
所以,使用instanceof在绝大多数情况下并不是推荐的做法,应当好好利用多态。
16. 认真对待异常。
1).在方法体用throws子句抛出异常时尽量包括所有出现的异常,而不是仅仅抛出base exception.
2).在super class中定义的方法抛出某个异常,如果在deriver class中要override该方法,那么overriding method必须:
a. 不抛出任何异常
b. 抛出和super class 中同样的异常
c. 抛出和super class 中异常的deriver class
如果super class中定义的方法没有抛出异常,但deriver class中的override的方法会产生异常,必须自己内部解决
3).好好利用finally功能。一般只要有finally,它总是会被执行,除非在try中用System.exit(0)或者在try块执行期间强行拔掉电源。finally被执行有三种情况:
a. 抛出异常
b. try正常结束
c. 在try中执行了return, break, continue而引起离开try的操作
尤其注意c.如果方法中在try块return 1,而在finally块return 2,则最终永远是2,因此尽量避免在try中使用return, break, continue,要么确保在finally中不会改变返回值
4).不要在循环体中使用try,因为在无JIT的JVM中将大大降低性能,而且这也是良好的编程习惯
5).不要将异常用于控制流程,而是仅仅用于会发生错误的地方
6).不要每逢出错就使用异常,尽量使用传统的方法判断变量的有效性
17. 关于不可变类(Immutable class),如String、Byte、Integer、Short、Long、Float、Double、BigInteger、BigDecimal等,它们之所以能将同一值自动地指向同一引用,实际上是它们实现了静态工厂方法。