程序猿二三事之Java基础--Java SE 5增加的特性--语法篇(一)

为什么是Java SE 5?

目前已经到了JDK-8u74了,JDK7的主版本已经于2015年4月停止公开更新。

那为什么还要来说Java/JDK5呢?

Java SE在1.4(2002)趋于成熟,随着越来越多应用于开发企业应用,许多框架诞生于这个时期或走向成熟。
Java SE 5.0的发布(2004)在语法层面增加了很多特性,让开发更高效,代码更整洁。

  • 自动装箱/拆箱、泛型、注解、for循环增强、枚举、可变参数等新特性让你的小手指少敲了不少代码,可以写更优雅的实现;
  • API提供并发库大大减少并发编程的难度;
  • 虚拟机层面改进了内存模型,增加虚拟机监控和管理相关的api和工具等等。

但是,语法层面的改变对应于JVM却没有多大变化,只是编译器在编译字节码时偷偷做了手脚。
所以我们应该了解下到底编译器干了啥坏事,有助于写更合理的代码,少踩坑,掉陷阱里也得知道怎么掉的。

另外原因,目前从各种各样的项目代码看,其实多数开发人员常用的还是Java SE 5.0 的特性,甚至习惯用Java SE 1.4及以前的语法特性。
学java也有几年了,许多特性也知道个一二,但是要写下来,还是得查阅不少文章,很多东西欠缺完整性和系统性。
码农写文章(更合理说是整理资料)也是一个学习的过程。

学习一门语言,一旦实际应用于实际开发中,了解背后的原理和理念,深入了解语言的特点,有好处没坏处。

注:javac XXXXX.java 编译命令
javap -c XXXXX 反编译命令
-c 反编译
-s 输出内部类型签名 需要看方法签名时 要加上这个参数
-v 输出附加信息 会输出比较多信息 包括常量表 line number table 等信息, 但没有-s的输出内容

一、自动装箱/拆箱

1、包装类型(存在于Java 1.5之前)

Java中,类型分成两大类,基本类型(Primitive Type)和引用类型(Reference Type)。基本类型是内定的,有确定的取值范围,值占有确定的内存空间。

有八大基本类型,分成两个浮点类型(float、double),五个整型(byte, short, int, long,char), 一个布尔型(boolean)。
没看错char也是整型,在语言规范中说明,char是一个16bit无符号整形,用来表示一个UTF-16编码的单元(在Java5中对应Unicode4.0,Java8中对应Unicode6.2)。

基本类型的值不是对象,最基本的对象(Object)方法(toString, hashCode, getClass, equals等)也不能调用。

为了把基本类型当引用类型来用,具备对象的特质,JDK中定义了各种基本类型相对应的包装类。
所谓装箱,就是将基本类型的值包装成(转换-conversion)对应的包装类型的对象,拆箱,就是讲包装类型的对象,转换成基本类型的值。

装箱和拆箱:

Integer i = 100;
int j = new Integer(250);
基本类型 大小 数值范围 包装类型 默认值
boolean true, false Boolean false
byte 1字节(8bit) -2^7 – 2^7-1 Byte 0
char 2字节(16bit) \u0000–\uffff Character \u0000
short 2字节(16bit) -2^15 – 2^15-1 Short 0
int 4字节(32bit) -2^31 – 2^31-1 Integer 0
long 8字节(64bit) -2^63 – 2^63-1 Long 0
float 4字节(32bit) IEEE754 Float 0.0f
double 8字节(64bit) IEEE754 Double 0.0d

2、自动装箱/拆箱背后

前面说了,语法特性的改变并没改变JVM的实现方式,那么我们可以看看背后编译器到底干了啥事情。
下面代码和编译后的反编译结果:

       public void boxUnBox(){
              Integer i = 100;
               int j = new Integer(250);
       }

反编译结果可以看到,以上代码实际等同于以下代码的编译结果:

       public void boxUnBox(){
              Integer i = Integer. valueOf(100);

              Integer t = new Integer(250);
               int j = t .intValue();
       }

八大基本类型的装箱操作都调用的是valueOf方法,拆箱操作调用各自赌赢的xxxValue()方法,有兴趣可以试试。

3、注意==比较的陷阱

在java中,计算类型的运算符,
先来看下比较的代码编译结果:

       public void boxUnBoxCMP(){             
               Integer i = 100; 
               int j = new Integer(250);
               if(j == i ){}

               Integer h = new Integer(100);
               Integer k = new Integer(100);
               if(h == k ){}    
       }

==第一个红框==是if(j == i ) 的反编译代码
从上面的反编译结果可以看出,包装类型的单目运算符计算其实是需要通过拆箱=>计算=>装箱实现的,
而双目运算符的运算也是需要将包装类型转换成基本类型,然后再参与运算。

但是,== 的比较要牢记它的本质,如果==比较两边都是引用类型,那么比较的是引用地址,如果其中一边是基本类型,那么非引用类型的值将转换成基本类型再做比较。
==第二个红框==中是引用比较,没有转换。

4、Cache带来的坑

我们看看自动装箱的valueOf的代码吧

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache. high)
            return IntegerCache.cache[i + (-IntegerCache. low)];
        return new Integer(i);
    }

一眼就可以看到IntegerCache这个玩意,完整代码(JDK1.8的代码)如下:

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            // 根据配置获取缓存最大值,最大值配置范围 127 < h < Integer.MAX_VALUE-129
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt( integerCacheHighPropValue);
                    i = Math. max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math. min(i, Integer.MAX_VALUE - (- low) -1);
                } catch( NumberFormatException nfe ) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h ;
            cache = new Integer[(high - low) + 1];// 也许有人会疑惑为什么会有个+1,其实就是0这个数占了个坑
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k ] = new Integer(j++);
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache. high >= 127;
        }

        private IntegerCache() {}
    }

IntegerCache的意思就是将low到high的值先缓存起来,low恒定是-128, high默认是127,可以配置成127<= high <= Interger.MAX_VALUE-129
注意缓存的是Integer对象,所以是引用对象。既然是引用对象,那么==的比较就会有问题了。

        public static void trap(){
              Integer i = 100;
              Integer k = 100;

               if(i == k ){System.out.println( "i == k");}

              Integer j = 500;
              Integer h = 500;
               if(j == h ){System.out.println( "j == h");}else { System.out.println("j != h" );}

       }

输出结果是什么呢?
i == k
j != h

因为i和k都是从IntegerCache中取得的缓存对象,引用是一样的,j和h没有缓存,必须valueOf必须重新new一个Integer对象,所以引用是不等的。

类型Byte、Short、Long和Integer类似,只是没有可配置的最大缓存值,Byte所有值都被缓存了,所以不存在==的坑。
Character缓存的是0~127。
Float和Double没有缓冲,也没办法缓存。

5、建议

  • 不会参与运算的用包装, 比如数据库自增的记录ID,用Long类型
  • 参与运算的,如果计算复杂,尽量先转成基本类型,计算后再转回对应的包装类对象;特别是频繁的单目运算符,如循环中的自增自减
  • 参与比较,注意包装类的cache坑
  • 记得所有集合中只能存对象类型,基本类型都是经过装箱/拆箱的

举个不好的例子吧:

        public static Long bad(List<Integer> list){
              Long sum = 0L;
               for(Integer i : list ){
                      if(i % 2 == 0 ){
                            sum += i;
                     } else {
                            sum += i * 2;        
                     }
              }
               return sum ;
       }

有兴趣的童鞋可以反编译看下,类似于以下代码完成的事情:

        public static Long badOrigin(List<Integer> list){
              Long sum = Long. valueOf(0L);
              Iterator<Integer> it = list.iterator();
              Integer value = null;
               long sumTmp = 0L;
               while(it .hasNext()){
                      value = it.next();
                      if(value .intValue() % 2 == 0){
                            sumTmp = sum.longValue();
                            sumTmp = sumTmp+ value.intValue();
                            sum = Long. valueOf(sumTmp);
                     } else {
                            sumTmp = sum.longValue();
                            sumTmp = sumTmp+ value.intValue()*2;
                            sum = Long. valueOf(sumTmp);
                     }
              }
               return sum ;
       }

按照建议来,可以改成以下代码:

        public static Long good(List <Integer> list ){
               long sum = 0L;
               int value = 0;
               for(Integer i : list ){
                      value = i.intValue();
                      if(value % 2 == 0 ){
                            sum += value;
                     } else {
                            sum += value * 2;    
                     }
              }
               return sum ;
       }

以上反编译下看看字节码,是不是清爽多了^^

二、for循环增强

for循环增强也是1.5里的一个语法糖,让大家写for循环更加便利,再加上IDE的代码模板,非常方便

1、先看看List的for循环增强怎么写:

        public void iteratorForeach(){
              List<String> list = new ArrayList<String>();
               for (String str : list ) {
              }
       }

反编译结果如下,可以看出,其实就是调用Iterable接口的iterator方法,获得一个迭代器(Iterator), 利用迭代器进行遍历所有数据。
从这里也可以推出,只要实现Iterable接口的类型,都可以在for循环增强中使用:

比如自己实现一个只有add方法,只能通过iterator遍历的List:

        public void myListForeach(){
              MyList<String> myList = new MyList<>();
               for (String str : myList ) {

              }
        }

        public static class MyList<V> implements Iterable<V>{
               private List<V> datas = new ArrayList<>();

               public void add(V data ){
                      datas.add( data);
              }

               @Override
               public Iterator<V> iterator() {
                      final Iterator<V> it = datas .iterator();
                      return new Iterator<V>() {
                            @Override
                            public boolean hasNext() {
                                   return it .hasNext();
                           }
                            @Override
                            public V next() {
                                   return it .next();
                           }
                     };
              }      
       }

2、再看看数组类型的for循环增强怎么写:

        public void arrayForeach(){
              String[] strs = new String[10];
               for (String str : strs ) {

              }

              System. out.println();
              String str = null ;
               for(int i = 0; i < strs .length ; i ++){ //传统for循环写法
                      str = strs[ i];
              }
       }

跟传统for循环相比,数组的for增强循环更加简洁,从反编译代码中也可以看出,用到的指令序列基本上是一样的。

3、不适应的地方

这么好的东西什么情况下用不了呢? 主要是for增强循环中没能得到下标也没能得到iterator对象引用导致的。
第一种是数组或者List集合类型,需要用到下标的情况;
第二种是需要调用到Iterator接口的remove方法的情况;

三、可变参数

Java SE 5.0中增加了可变参数特性,对于以往用数组表示的参数可以调整到最后一个参数,作为可变参数定义,
调用方省去显示创建数组,可空数组可以直接可以省略:

    public static void varargs(String s, String... ss) {
    }

    public static void main(String[] args) {
        varargs("aaa" );
        varargs("aaa" , "bbb" );
        varargs("aaa" , "bbb" , "ccc" , "ddd" );
        varargs("", null) ;
        varargs("aaa" , new String[]{"abc", "ccc", "ddd" });
    }

可变参数背后编译器也是创建一个数组来传递参数的,可以方编译以上代码, varargs的方法签名中第二个参数就是一个string数组:

==注意事项:==

  • 不能有多个可变参数,并且只能是最后一个参数;
  • 因为可变参数是由数组实现的,调用方忽略可变参数时,可变参数为空数组;但是既然是数组,就可以设置成null,所以要注意空判断;
  • 如果被调用的方法,既匹配了可变参数方法,有匹配了固定参数方法,固定参数方法将被调用;
  • 尽量避免可变参数方法的重载(overload):
    • 可变参数类型与前一个参数的类型一样时,与只有可变参数类型方法重载冲突,会导致调用不明确;
    • 可变参数类型不同,但可变参数为空时,可以省略,或者设置成null,都会导致被调用方法不明确;
    • 可变参数类型是基本类型或包装类型,重载会因为自动装箱/拆箱导致调用不明确
  • override的方法参数类型和形式必须一致,不能将可变参数改成数组,虽然背后实现是一样的;
    /**不能有多个可变参数,并且只能是最后一个参数**/
    public static void varargs10(Object ... objs, String abc){ //编译出错
    }
    public static void varargs11(String abc, Object ... objs){
    }

    /**因为可变参数是由数组实现的,调用方忽略可变参数时,可变参数为长度为0的数组;但是既然是数组,就可以设置成null,所以要注意null判断;**/
    public static void varargs2Test(){
        varargs2();
        varargs2(null); //NullPointerException
    }

    public static void varargs2(String...strs){
       //strs 可能为null, 应该做 strs是否为空的判断
       for (String str : strs ) {
        }
    }

    /**如果被调用的方法,既匹配了可变参数方法,有匹配了固定参数方法,固定参数方法将被调用;**/
    public static void varargs3Test(){
       varargs3(11, 22); //varargs30
    }

    public static void varargs3(int i, int j ){
       System. out.println("varargs30" );
    }
    public static void varargs3(int i , int... arr){
       System. out.println("varargs31" );
    }

    /**可变参数类型与前一个参数的类型一样时,与只有可变参数类型方法重载冲突,会导致调用不明确;**/
    public static void varargs4Test(){
       varargs4("abc" , "def" , "ijk" ); //编译出错
    }
    public static void varargs4(String...strs){
    }
    public static void varargs4(String str, String... strs){
    }

    /**可变参数类型不同,但可变参数为空时,可以省略,或者设置成null,都会导致被调用方法不明确;**/
    public static void varargs5Test(){
       varargs5();   //编译出错
       varargs5("abc" , null); //编译出错
    }
    public static void varargs5(String str, String... strs){
    }
    public static void varargs5(String str, Integer... datas){
    }

    /**可变参数类型是基本类型或包装类型,重载会因为自动装箱/拆箱导致调用不明确**/
    public static void varargs6Test(){
       varargs6("abc" , 1, 2, 3); //编译出错
    }
    public static void varargs6(String str, int... datas){
    }
    public static void varargs6(String str, Integer... datas){
    }

    /**override的方法参数类型和形式必须一致,不能将可变参数改成数组,虽然背后实现是一样的**/
    public static void varargs7Test(){
       Sub sub = new Sub();

       Base base = sub;
       base.varargs7( "abc", "def" );
       base.varargs7();

       sub.varargs7();  //编译错误
       sub.varargs7("abc" , "def" ); //编译错误
    }

    public static interface Base {
       public void varargs7(String...strs );
    }

    public static class Sub implements Base{
       @Override
       public void varargs7(String[] strs) {
              System. out.println("varargs7" );
       }
    }

四、StringBuilder和字符串+(非1.5特性,顺便提一下而已)

JDK 5.0中增加了StringBuilder, 基本上和StringBuffer一样,但去掉了所有synchronized同步关键字。
性能上StringBuilder优于StringBuffer, 所以非并发情况下使用StringBuilder没商量。

Java中对象没有参与运算符运算的可能,也没有提供像C++那样重载运算符语法支持,不要被String的+操作欺骗了。
Java1.4中,字符串的+操作在编译器生成的字节码可以看到使用的是StringBuffer进行append,
Java5.0中,+操作改成StringBuilder的append:

       public static void sbTest(String s1, String s2){
              String str = s1 +s2 ;
       }

你可能感兴趣的:(java,java,jdk,se)