知其然,知其所以然之Java基础系列(一)

相信大家在最初接触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 stuMap = new HashMap;
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发行版中增加了自动装箱和自动拆箱,基本类型和装箱类型有三个主要区别:

  1. 基本类型只有值,两个装箱基本类型可以具有相等的值和不同的同一性。
  2. 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能之外,还有个非功能值null。
  3. 基本类型通常比装箱类型节省空间。

如果不小心这三点都会让你陷入麻烦中。

下面我们看下这个小程序:

 

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

这个程序比预计的要慢一些,为什么呢?因为它把基本类型设计成了装箱类型,变量被反复的装箱和拆箱,导致性能明显下降。

 

核实使用装箱类型呢?答案有三点:

       第一点:作为集合中的元素、键和值

       第二点:在参数化类型中,必须使用装箱基本类型作为类型参数,比如ThradLocal

       第三点:在进行反射的方法调用时,必须使用装箱基本类型

你可能感兴趣的:(java基础)