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

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

[ TOC ]

为什么是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 ;
       }

时间: 2024-10-27 12:17:28

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

java基础----&gt;java中正则表达式二

跟正则表达式相关的类有:Pattern.Matcher和String.今天我们就开始Java中正则表达式的学习. Pattern和Matcher的理解 一.正则表达式的使用方法 一般推荐使用的方式如下: Pattern pattern = Pattern.compile("^[^abc]h$"); Matcher matcher = pattern.matcher("hh"); boolean isMatch = matcher.matches(); 另外一种不能复

Java基础--Java编程规范

IsCoder 标记: Java基础,Java编程规范 摘要:Java虽然没有强制性的编程规范,但是为了便于统一,Java有不成文的编程规范,为了形成良好的编程习惯,建议熟悉并遵守Java编程规范,提高代码的阅读性. 一.Java标识符 在任何语言中,都要自己的一套标识符规则.Java的标识符包括:Java关键字.Java特殊功能的标识符.合法的用户自定义标识符.其中,我们能改变的只有自定义的标识符,和大部分计算机编程语言一样,Java标识符原始只支持ASCII的编码,但是随着Java在世界的影

[Java基础] Java线程复习笔记

先说说线程和进程,现代操作系统几乎无一例外地采用进程的概念,进程之间基本上可以认为是相互独立的,共享的资源非常少.线程可以认为是轻量级的进 程,充分地利用线程可以使得同一个进程中执行多种任务.Java是第一个在语言层面就支持线程操作的主流编程语言.和进程类似,线程也是各自独立的,有自 己的栈,自己的局部变量,自己的程序执行并行路径,但线程的独立性又没有进程那么强,它们共享内存,文件资源,以及其他进程层面的状态等.同一个进程内的 多个线程共享同样的内存空间,这也就意味着这些线程可以访问同样的变量和

Java基础--Java入门

IsCoder 标记: Java基础,Java环境配置 一.Java环境配置 Java Develop Kit(JDK安装) 系统环境变量设置(JAVA_HOME) Java源程序编辑工具 Java编译运行命令 运行经典的HelloWorld程序 1.1 JDK安装 JDK,就是甲骨文公司提供给我们的Java开发工具包,包括最常用的Javac.exe编译工具和Java.exe运行工具.需要指出的是,JDK中已经包含了JER(Java Runtime Environment,Java运行时环境),

[Java基础] Java对象内存结构

转载地址:http://www.importnew.com/1305.html 原文于2008年11月13日 发表, 2008年12月18日更新:这里还有一篇关于Java的Sizeof运算符的实用库的文章. 学C/C++出身的我,对Java有一点非常困惑,那就是缺乏计算对象占用内存大小的机制.而在C++中就可以通过sizeof运算符来获得基本类型以及类实例的大小.C和C++中的这个操作符对于指针运算.内存拷贝和IO操作都非常有用. Java中并没有一个类似的运算符.事实上,Java也不需要这种运

[Java基础] java的守护线程与非守护线程

最近重新研究Java基础知识,发现以前太多知识知识略略带过了,比较说Java的线程机制,在Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) ,(PS:以前忽略了). 估计学过Unix开发但是没有细致学习Java的同学们会疑惑了,操作系统里面是没有所谓的守护线程的概念,只有守护进程一说,但是Java语言机制是构 建在JVM的基础之上的,意思是Java平台把操作系统的底层给屏蔽起来,所以它可以在它自己的虚拟的平台里面构造出对自己有利的机制,而语言或者说

[Java基础] Java float保留两位小数或多位小数

方法1:用Math.round计算,这里返回的数字格式的. float price=89.89; int itemNum=3; float totalPrice=price*itemNum; float num=(float)(Math.round(totalPrice*100)/100);//如果要求精确4位就*10000然后/10000 方法2:用DecimalFormat 返回的是String格式的.该类对十进制进行全面的封装.像%号,千分位,小数精度.科学计算. float price=

Java基础----Java中的String和StringBuffer

String和StringBuffer String是一个特殊的对象,一旦被初始化,就不会被改变.()指的是abc 不是变量 s1. String s1="abc"; s1是一个类类型变量,"abc"是一个对象. String s2=new String("abc"); s1和s2的区别: s1在字符串常量池中创建了一个abc字符串 s2在堆中创建了两个对象一个是默认对象一个是字符串对象. ==和equals的区别 ==比较的是地址,equals

Java基础----Java API中的常用类

System:描述系统的一些信息 preperties();获取系统信息 Properties prop =new System.getProperties(); 是hashtable 的子类.用map的方法去除该类集合中的元素.该集合中存储的都是字符串,没有泛型定义. String calue=(String)prop.get(obj); System.out.println(obj+":"+value); //如何在系统中自定义一些特有信息? System.setProperty(