Java语法糖系列一:可变长度参数和foreach循环

96
Eric新之助
1.7 2016.12.05 15:31* 字数 1272

目录:
Java语法糖系列一:可变长度参数和foreach循环
http://www.jianshu.com/p/628568f94ef8

Java语法糖系列二:自动装箱/拆箱和条件编译
http://www.jianshu.com/p/946b3c4a5db6

Java语法糖系列三:泛型与类型擦除
http://www.jianshu.com/p/4de08deb6ba4

Java语法糖系列四:枚举类型
http://www.jianshu.com/p/ae09363fe734

Java语法糖系列五:内部类和闭包
http://www.jianshu.com/p/f55b11a4cec2


好久没做总结,打算挖个坑整理一下Java语法糖相关的知识。文字、说明等知识均来源于互联网和自己的见解,例子都是自己写的,侵删,欢迎讨论~

语法糖

语法糖(Syntactic Sugar),也叫糖衣语法,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语。指的是,在计算机语言中添加某种语法,这种语法能使程序员更方便的使用语言开发程序,同时增强程序代码的可读性,避免出错的机会。

几乎每种语言都提供语法糖,它只是编译器实现的一些小把戏罢了,编译期间以特定的字节码或者特定的方式对这些语法做一些处理,开发者就可以直接方便地使用了。这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高性能、或能提升语法的严谨性、或能减少编码出错的机会。Java提供给了用户大量的语法糖,比如泛型、自动装箱、自动拆箱、foreach循环、变长参数、内部类、枚举类、断言(assert)等

学习语法糖原理最好的办法就是反编译看源码~

可变长度参数

看以下代码

public static void main(String[] args){
    String [] params=new String[]{
            "111","222","333","444"
    };
    print("AAA","BBB","CCC","DDD");
    print(params);
}

 
public static void print(String... params)
{
    System.out.println();
    for (int i = 0; i < params.length; i++)
    {
        System.out.print(params[i]+"~");
    }
}

print方法的参数的意思是表示传入的params个数是不定的,代码的运行结果:

AAA~BBB~CCC~DDD~
111~222~333~444~

我用数组遍历的方式把参数遍历出来了,同时print方法也接受数组参数,这说明了可变参数是利用数组实现的。查看编译出来的源码:

public static void main(String[] paramArrayOfString)
 {
    String[] arrayOfString = { "111", "222", "333", "444" };

    print(new String[] { "AAA", "BBB", "CCC", "DDD" });
    print(arrayOfString);
  }

  public static void print(String[] paramArrayOfString)
  {
    System.out.println();
    for (int i = 0; i < paramArrayOfString.length; ++i)
    {
      System.out.print(paramArrayOfString[i] + "~");
    }
 }

发现print方法的参数部分由String... params 变成了 String[] paramArrayOfString
数组参数,说明可变长度参数是用数组实现的。

最后,注意一点,可变长度参数必须作为方法参数列表中的的最后一个参数且方法参数列表中只能有一个可变长度参数。

foreach循环原理

public static void print(String... params)
{
    System.out.println();
    for (String s:params)
    {
        System.out.print(s+"~");
    }
}

把上面的print函数换成foreach循环,查看编译出来的源码

 public static void print(String[] paramArrayOfString)
  {
        System.out.println();
        String[] arrayOfString = paramArrayOfString; 
     int i = arrayOfString.length; 
     for (int j = 0; j < i; ++j) { 
        String str = arrayOfString[j];
          System.out.print(str + "~");
        }
  }

发现foreach部分被替换成了普通的for循环,说明对于数组,foreach是用普通for循环实现的。

如果遍历的对象不是数组,而是List、Map等有实现迭代器Iterable接口的容器又是怎么实现的呢?再看一个例子

public static void main(String[] args){
    // TODO Auto-generated method stub
    List<String> list = new ArrayList<String>();
    list.add("AAA");
    list.add("BBB");
    
    for(String l : list)
    {
        System.out.print(l+"~");
    }
    
    Set<String> set=new HashSet<String>();
    set.add("CCC");
    set.add("DDD");

    for(String s : set)
    {
        System.out.print(s+"~");
    }
}
}

查看编译出来的源码

public static void main(String[] paramArrayOfString)
{

    ArrayList localArrayList = new ArrayList();
        localArrayList.add("AAA");
    localArrayList.add("BBB");
    localArrayList.add("CCC");
    localArrayList.add("DDD");

for (Object localObject1 = localArrayList.iterator();
 ((Iterator)localObject1).hasNext(); ) 
{ 
      localObject2 = (String)((Iterator)localObject1).next();
      System.out.print(((String)localObject2) + "~");
}

    localObject1 = new HashSet();
    ((Set)localObject1).add("AAA");
    ((Set)localObject1).add("BBB");
    ((Set)localObject1).add("CCC");
    ((Set)localObject1).add("DDD");

for (Object localObject2 = ((Set)localObject1).iterator(); 
((Iterator)localObject2).hasNext(); )
 { 
    String str = (String)((Iterator)localObject2).next();
    System.out.print(str + "~");
}
}

List和Set的foreach都被编译成用迭代器遍历的形式了,说明在对有实现Iterable接口的对象采用foreach语法糖的话,编译器会将这个for关键字转化为对目标的迭代器使用。
所以如果想要自己自定义的类可以采用foreach语法糖就要实现Iterable接口了。

一点拓展

ArrayList除了支持线性访问(sequential access)外还支持随机访问外(random access)

这是因为arrayList还实现了RandomAccess接口,而Map、Set等没有。
查看JDK关于RandomAccess接口的说明如下,版本是JDK1.8

It is recognized that the distinction between random and sequential

  • access is often fuzzy. For example, some <tt>List</tt> implementations
  • provide asymptotically linear access times if they get huge, but constant
  • access times in practice. Such a <tt>List</tt> implementation
  • should generally implement this interface. As a rule of thumb, a
  • <tt>List</tt> implementation should implement this interface if,
  • for typical instances of the class, this loop:
  • for (int i=0, n=list.size(); i < n; i++)
    
  •     list.get(i);
    
  • runs faster than this loop:
  • for (Iterator i=list.iterator(); i.hasNext(); )
    
  •     i.next();
    
  • @since 1.4

我们直接看最重要的部分,从JDK1.4开始,根据经验,对于实现了RandomAccess接口的List,如ArrayList、CopyOnWriteArrayList, RoleList, RoleUnresolvedList, Stack, Vector,直接使用for循环遍历runs faster than 迭代器遍历。

其实如果看过ArrayList源码的同学也可以注意到:ArrayList底层是采用数组实现的,如果采用Iterator遍历,那么还要创建许多指针去执行这些值(比如next();hasNext())等,这样必然会增加内存开销以及执行效率。

简单测试了一下,空遍历10万条数据不做其他任何操作,对于ArrayList用foreach遍历和for遍历耗时分别是5ms和1ms。例子都好简单就不贴出来了。

Java