相信大家在最初接触Java基础学习的时候,也只是跟着课本上的描述学习,知其然,不知所以然,要想成为一个Java老鸟,不仅要学会怎么用,也要知道为何这么用。在Java基础系列的博客中,我会列举一系列大家日常开发中只知道会用的,却不知道为何如此这么用的那些常用知识点。
1.1 一个简单的String例子,看看个人功底如何?
下面看一段代码片段:
public static void main(String[] args){
String a = "a" + "b" + 1;
String b = "ab1";
System.out.println(a == b);
}
我们在日常开发中,对于字符串的比较是通过equals()方法进行比较,而在上面这段代码中却使用了==比较两个字符串,我们知道,在我们刚接触Java程序设计的时候,老师教我们比较两个字符串用等号是匹配不了的,那这段代码运行结果是TRUE还是FALSE呢?
运行结果:
true
这肯定会让大家很迷茫,难道我们老师的真理是错的吗?其实也不能怪老师,老师带进门,修行靠个人。
分析:
分析之前你需要知道==和equals的区别?==用于比较内存单元格上的内容,比较对象时就是比较两个对象的内存地址是否一样,对于基本类型:byte,short,int,float,long等,其实是比较它们的值是否相等;在默认情况下,不重写equals方法也是比较内存地址,String类之所以能够用equals来比较两个字符串的值,是因为它重写了equals方法。为何 a == b运行结果true,答案是编译时优化,a = "a" + "b" + 1,等号右侧全是常量,当编译器编译代码时,无需运行时知道右侧是什么值,直接将其编译成,a = "ab1",所以,a和b指向了同一个引用。为何要做如此优化呢?提高整体效率呗,能提前做的事就提前做,为何要等到要做的时候在做呢,各位,是不是呢?
补充例子,看看大家掌握的如何了?
private static String getA(){
return "a";
}
public static void main(String[] args){
String a = "a";
final String c = "a";
String b = a + "b";
String d = c + "b";
String e = getA() + "b";
String compare = "ab";
System.out.println(b == compare);
System.out.println(d == compare);
System.out.println(e == compare);
}
1.2 使用“+”拼接字符串的误区
其实“+”在拼接少量的字符串的时候,效率比append()方法效率更高,String通过“+”拼接字符串的时候,如果拼接的是对象是常量,则会在编译时合并优化,在编译阶段就完成了,无需运行时;append()更适合去拼接大量的字符串。下面我们来看一段代码片段以及编译后的代码:
编译前:
public void sample(){
String a = "a";
String b = "b";
String c = a + b + "f";
}
编译后:
public void sample(){
String a = "a";
String b = "b";
StringBuilder temp = new StringBuilder();
temp.append(a).append(b).append("f");
String c = temp.toString();
}
我们接着看下使用“+”来拼接大量字符串会带来什么结果?
String a = "";
for(int i=0; i<10000; i++){
a += i;
}
看下编译后,会怎样?
String a = "";
for(int i=0; i<10000; i++){
StringBuilder temp = new StringBuilder();
temp.append(a).append(i);
a = temp.toString();
}
在这个循环的过程中,导致a引用的值越来越来,每次拼接都会产生一个临时变量,这个就会导致产生大量的临时垃圾,随着数量的增加,垃圾空间也会越来越大,可能会导致OOM,直接导致系统宕机或者僵死,大量的垃圾也会导致新生代堆内存不足频繁进行minor GC,当新生代中的对象转移至老年代,随着老年代内存空间被占满,会直接导致full GC,依次full GC时间持续之长,运行的系统性能极速下降。
总结:在开发过程知道拼接的全是常量或者少量拼接就可以使用"+",对于大量拼接请使用append()方法。
1.3 覆盖Object的equals方法
有的时候,我们重写了equals方法,却忘了重写hashCode()方法,可能导致我们找了好久才想起我们忘了重写这个方法了,白白浪费了时间。在每个覆盖了equals方法的类中,也必须覆盖其hashCode方法。如果不这么做的话,就违反了Object.hashCode通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括:HashMap,HashSet和Hashtable。
下面我们来看一下,关于Object规范对equals和hashCode的约定吧
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个数字。
- 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
下面看一段代码:
public class Student{
private String id;
private String name;
private int age;
public Student(String id, String name, int age){
this.id = id;
this.name = name;
this.age = age;
//setter和getter方法省略
@Override
public boolean equals(Object o){
if(o == this){
return this;
}
if(o instanceof Student)
Student stu = (Student) o;
return o.getId().equals(id) && o.getName().equals(name)
&& o.getAge() == age;
}
}
假设你企图将这个类与HashMap一起用:
Map<Student, String> stuMap = new HashMap<Student, String>;
stuMap.put("2010214139", "wangf", 26);
这时候,你可能期望stuMap.get(new Student("2010214139", "wangf", 26))会返回wangf,但它实际返回的是null。由于Student类没有覆盖HashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了约定。只需为Student重写hashCode方法即可。
比如下面这种:
@Override
pulic int hashCode(){
return 31;
}
虽然这种hashCode是合法的,但每次调用都返回相同的散列码,因此,每个对象都被映射到同一个散列桶中,使散列表退化为链表。一个好的散列函数通常倾向于"为相等的对象产生不相等的散列码"。如何写一个合法的hashCode呢?
- 把某个非零的常数值,比如17,保存在名为result的int类型的变量中
- 对于对象中每个关键域f,完成以下步骤:
1)如果该域是boolean,则计算f ? 1:0;
2)如果该域是byte、char、short或者int类型,则计算(int)f;
3)如果该域是long类型,则计算(int)(f^(f>>>32));
4)如果该域是float类型,则计算Float.floatToIntBits(f);
5)如果该域是double类型,则计算Double.doubleToLongBits(),然后按照步骤3)操作
6)如果该域是一个对象引用,并且该类的equals方法地跪地equals的方式来比较域,则同样为这个域递归的调用hashCode。如果这个域为null,则返回0。
7)如果该域是一个数组,则把每一个元素当做单独的域处理。
- 按照下面的公式,把步骤2中计算得到的散列码合并到result,result = 31 * result + x;
- 返回result
下面写出Student的hashCode方法:
@Override
public int hashCode(){
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
1.4 自动装箱和自动拆箱
Java1.5发行版中增加了自动装箱和自动拆箱,基本类型和装箱类型有三个主要区别:
- 基本类型只有值,两个装箱基本类型可以具有相等的值和不同的同一性。
- 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能之外,还有个非功能值null。
- 基本类型通常比装箱类型节省空间。
如果不小心这三点都会让你陷入麻烦中。
下面我们看下这个小程序:
public class Test{
private static Integer i;
public static void main(String[] args){
if(i == 32){
System.out.println("hello");
}
}
}
事实上它并没输出hello,而是抛出了NPE,问题在于i是Integer类型,而不是int基本类型,就像所有的对象引用域一样,它的初始值都是null。
最后,看下这段代码片段:
public static void main(String[] args){
Long sum = 0;
for(long i=0; i<Integer.MAX_VALUE;i++){
sum +=i ;
}
System.out.println(sum);
}
这个程序比预计的要慢一些,为什么呢?因为它把基本类型设计成了装箱类型,变量被反复的装箱和拆箱,导致性能明显下降。
核实使用装箱类型呢?答案有三点:
第一点:作为集合中的元素、键和值
第二点:在参数化类型中,必须使用装箱基本类型作为类型参数,比如ThradLocal<Integer>
第三点:在进行反射的方法调用时,必须使用装箱基本类型