语法糖(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。例子都好简单就不贴出来了。
自动装箱/拆箱
自动拆箱/装箱是在编译期,依据代码的语法,决定是否进行拆箱和装箱动作。
装箱过程:把基本类型用它们对应的包装类型进行包装,使基本类型具有对象特征。
拆箱过程:与装箱过程相反,把包装类型转换成基本类型。
public static void main(String[] args){
int i=1;
Integer a = 1;
Integer b = 1;
Long c = 1L;
System.out.println(a == b);
System.out.println(a.equals(i));
System.out.println(c.equals(a));
}
结果是
true
true
false
编译出来的代码如下
public static void main(String[] paramArrayOfString)
{
int i = 1;
Integer localInteger1 = Integer.valueOf(1);
Integer localInteger2 = Integer.valueOf(1);
Long localLong = Long.valueOf(1L);
System.out.println(localInteger1 == localInteger2);
System.out.println(localInteger1.equals(Integer.valueOf(i)));
System.out.println(localLong.equals(localInteger1));
}
可以看到在自动装箱的时候,Java虚拟机会自动调用Integer的valueOf方法;
在自动拆箱的时候,Java虚拟机会自动调用Integer的intValue方法。这就是自动拆箱和自动装箱的原理。
对于包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们的equals()方法不处理数据转型类型,应在这种情况避免使用包装类。
枚举
在JVM字节码文件结构中,并没有“枚举”这个类型。
Java的枚举类型,会在编译期被编译成一个普通的类。直接看代码吧:
package lab;public enum JavaSugarEnum {
Enum1(1, "Enum1"),
Enum2(2, "Enum2"),
Enum3(3, "Enum3");private int key;private String value;
public int getKey() {
return key;
}
public String getValue() {
return value;
}
private JavaSugarEnum(int key, String value){
this.key = key;
this.value = value;
}
public String get(int key) {
for(JavaSugarEnum item : JavaSugarEnum.values()){
if(item.getKey() == key)
return item.getValue();
}
return "";
}}
编译出来的代码:
package lab;
public enum JavaSugarEnum
{
Enum1, Enum2, Enum3;
private int key;
private String value;
public int getKey()
{
return this.key;
}
public String getValue() {
return this.value;
}
public String get(int paramInt)
{
JavaSugarEnum[] arrayOfJavaSugarEnum = values(); int i = arrayOfJavaSugarEnum.length; for (int j = 0; j < i; ++j) {
JavaSugarEnum localJavaSugarEnum =arrayOfJavaSugarEnum[j];
if (localJavaSugarEnum.getKey() == paramInt)
return localJavaSugarEnum.getValue();
}
return "";
}
}
用JD打开class文,代码并没有什么变化,唯一引起注意的是
JavaSugarEnum.values()
变成了
JavaSugarEnum[] arrayOfJavaSugarEnum = values();
说明编译出来的字节码会有values()函数,那我们用javap命令再反编译一下看一看字节码
javap -c JavaSugarEnum
去掉汇编代码得到:
Compiled from "JavaSugarEnum.java"
public final class lab.JavaSugarEnum extends java.lang.Enum{
public static final lab.JavaSugarEnum Enum1;
public static final lab.JavaSugarEnum Enum2;
public static final lab.JavaSugarEnum Enum3;
public static lab.JavaSugarEnum[] values();
public static lab.JavaSugarEnum valueOf(java.lang.String);
public int getKey();
public java.lang.String getValue();
public java.lang.String get(int);static {};
}
可以看到Java枚举编译后实际上是生成了一个类,该类继承了 java.lang.Enum<E>,并添加了一个返回枚举数组的values()方法和valueOf()方法。
所以Enum类型就是一个语法糖,编译器帮我们做了语法的解析和编译。
内部类
Java的内部类也是一个语法糖,它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。
内部类分为四种:成员内部类、局部内部类、匿名内部类、静态内部类。但本篇不是谈论四种内部类的用法的,只讲内部类一些值得注意的地方。
为什么要使用内部类
在《Think in java》中有这样一句话:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
因为Java不支持多继承,支持实现多个接口。但有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。
成员内部类
成员内部类也是最普通的内部类,它是外围类的一个成员,所以他是可以无限制的访问外围类的所有 成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。在成员内部类中要注意两点
·
成员内部类中不能存在任何static的变量和方法;
·
·
成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。例子略。
·
局部内部类和匿名内部类
局部内部类是嵌套在方法和作用域内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
而匿名内部类也可以说是局部内部类的一种,有时候一个类只使用一次,就可以用匿名内部类,告诉GC只用一次就可以回收了,同时也可以简化代码和方便地定义回调
需要注意的是局部内部类和匿名内部类引用外部变量时,外部的变量需要是final 的:
abstract class InnerClass {public abstract void print();
}public class Outer {public void test1(final String s1) {// 参数必须是final
//成员内部类
InnerClass c = new InnerClass() {
public void print() {
System.out.println(s1);
}
};
c.print();
}
public void test2(final String s2) {// 参数必须是final
//匿名内部类
new Outer() { //名字可以跟外部类一样
public void print() {
System.out.println(s2);
}
}.print();
} public static void main(String[] args) {
Outer o=new Outer();
o.test1("inner1");
o.test2("inner2");
}
}
为什么匿名内部类和局部内部类引用外部的变量必要是final的呢?
直接看编译出来的源码吧
InnerClass:
abstract class InnerClass
{
public abstract void print();
}
Outer.class:
`import java.io.PrintStream;
public class Outer{
public void test1(String paramString)
{
Outer.1 local1 = new InnerClass(this, paramString) {
public void print() {
System.out.println(this.val$s1);//引用了s1变量
}
};
local1.print(); }
public void test2(String paramString) {new Outer(this, paramString) {//名字可以一样
public void print() {
System.out.println(this.val$s2);
}
}
.print();
}
public static void main(String[] paramArrayOfString)
{
Outer localOuter = new Outer();
localOuter.test1("inner1");
localOuter.test2("inner2");
}
}
局部内部类Outer$1.class:
import java.io.PrintStream;class 1 extends InnerClass{
public void print()
{
System.out.println(this.val$s1);//引用了s1变量
}
}
匿名内部类Outer$2.class:
import java.io.PrintStream;class 2 extends Outer{
public void print()
{
System.out.println(this.val$s2);//引用了s2变量
}
}
看源码两个内部类各编译出了一个独立的class文件,也就是说Outer$1和Outer$2的生命周期是对象级别的,而变量s1、s2是方法级别的,方法运行完了变量就销毁了,而局部内部类对象Outer$1和Outer$2还可能一直存在(只能没有人再引用该对象时,它才会被GC回收),它不会随着方法运行结束就马上死亡。这时可能会出现了一种"荒唐"的结果:局部内部类对象inner_object要访问一个已不存在的局部变量s1、s2!
也有人说:当方法调用完了,内部类也不可能再被访问到了,照理内部类对象也应该成为了垃圾。
别忘了Java还有反射,而且在多线程的情况下完全有可能主线程的方法运行结束,而内部类还在运行,例如:
public void execute() {
final int s = 10;
class InnerClass {
public void execute() {
new Thread() {
@Override
public void run() {
try {
Thread.currentThread().sleep(2000);
System.out.println(s);
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
new InnerClass().execute();
System.out.println("主方法已经over");
}
为什么把变量定义为final就能避免上述问题?
看stackoverflow上的一个讨论
http://stackoverflow.com/questions/3910324/why-java-inner-classes-require-final-outer-instance-variables
stackoverflow里最高票的答案说到,当主方法结束时,局部变量会被cleaned up 而内部类可能还在运行。当局部变量声明为final时,当使用已被cleaned up的局部变量时会把局部变量替换成常量:
The compiler can then just replace the use of lastPrice and price in the anonymous class with the values of the constants (at compile time, ofcourse)
也就是说当变量是final时,编译器会将final局部变量"复制"一份,复制品直接作为局部内部中的数据成员.这样,当局部内部类访问局部变量时,其实真正访问的是这个局部变量的"复制品"。因此:当运行栈中的真正的局部变量死亡时,局部内部类对象仍可以访问局部变量(其实访问的是"复制品")。而且,由于被final修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致。给人的感觉:好像是局部变量的"生命期"延长了。
这就是java的闭包。
最后贴两段关于闭包的笔记,来源于网络:
闭包是个什么东西呢?
Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长。闭包跟面向对象是一棵树上的两条枝,实现的功能是等价的。
Java中闭包带来的问题
在Java的经典著作《Effective Java》、《Java Concurrency in Practice》里,都提到:匿名函数里的变量引用,也叫做变量引用泄露,会导致线程安全问题,因此在Java8之前,如果在匿名类内部引用函数局部变量,必须将其声明为final,即不可变对象。(Python和Javascript从一开始就是为单线程而生的语言,一般也不会考虑这样的问题,所以它的外部变量是可以任意修改的)。
而java8的lambda 表达式之所以不用写final,是因为Java8这里加了一个语法糖:在lambda表达式以及匿名类内部,如果引用某局部变量,则直接将其视为final。本质并没有改变。
静态内部类
略
浅谈JAVA语法糖
最新推荐文章于 2022-10-29 10:11:43 发布