JVM(二):Java中的语法糖

时间:2022-04-27 03:32:09

JVM(二):Java中的语法糖

上文讲到在语义分析中会对Java中的语法糖进行解糖操作,因此本文就主要讲述一下Java中有哪些语法糖,每个语法糖在解糖过后的原始代码,以及这些语法糖背后的逻辑。

语法糖

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。 -----百度百科

在编程领域中,除了语法糖的概念还有语法盐,语法糖精,语法海洛因这些新奇的概念,感兴趣的读者自行Google一下,本文篇幅有限,就不展开来说了.

从百度百科的描述来看,语法糖只是作为一种更快捷的语法,其并不会改变所要实现的功能,因此本文就以Java中的语法糖来验证一下是否如此.

循环迭代

源代码如下所示

public class TestForEach {

public static void main(String[] args) {
// 验证 循环迭代
ArrayList<Integer> test = new ArrayList<Integer>();
test.add(1);
for (Integer integer : test) {
System.out.println(integer);
} Integer[] array = new Integer[test.size()];
array = test.toArray(array);
for (Integer integer : array) {
System.out.println(integer);
}
}
}

用jad反编译后如下所示

public class TestForEach
{ public TestForEach()
{
} public static void main(String args[])
{
ArrayList arraylist = new ArrayList();
arraylist.add(Integer.valueOf(1));
Integer integer;
for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(integer))
integer = (Integer)iterator.next(); Integer ainteger[] = new Integer[arraylist.size()];
ainteger = (Integer[])arraylist.toArray(ainteger);
Integer ainteger1[] = ainteger;
int i = ainteger1.length;
for(int j = 0; j < i; j++)
{
Integer integer1 = ainteger1[j];
System.out.println(integer1);
} }
}

根据上面反编译后的代码可以看出集合元素的循环迭代底层是通过迭代器来实现的.而数组的循环则是通过原始的for循环来实现的.

泛型

通过上面的代码我们还可以看出泛型这个概念在javac编译时是不存在的,编译器会将所有的泛型替换掉,在使用时,直接采用类型转换的方式来得到结果.也正是泛型的这个特征可能出现下面这个问题.

函数重载

  public void test1(ArrayList<Integer> a){}
public void test1(ArrayList<String> a){}

以上代码是无法编译通过的,因为根据上文得到的结论,泛型在编译时,会消除所有的泛型的限定,那么上面两个方法的签名都会一致,不满足函数重载的条件.

自动拆装箱

Java支持自动拆装箱,即将基本类型和其包装类型之间进行自动替换,那这种方式又是如何实现的呢.

原始代码如下所示:

// 自动拆装箱
Integer a = 1;
int b = new Integer(a);

经过反编译后,代码如下所示

Integer integer = Integer.valueOf(1);
int i = (new Integer(integer.intValue())).intValue();

可以看到Java实现基本类型 -- >包装类型,是通过XXX.valueOf()来实现的,而包装类型 --> 基本类型是通过xxxValue()来实现的.

switch

我们都知道switch--case只对int和char类型的数据有效,但从java7开始switch已经可以支持String类型了,这背后的逻辑又是什么,下面我们反编译一下代码看看其本质是如何实现的.

    String hello = "1";
switch (hello){
case "hello":
System.out.println("Hello");
break;
case "world":
System.out.println("World");
break;
default:
System.out.println("HelloWorld");
break;
}

初始代码如上所示,我们在switch中比较了字符串,根据字符串的不同来实现不同的分支,那这种逻辑是如何实现的呢.

    String s = "1";
String s1 = s;
byte byte0 = -1;
switch(s1.hashCode())
{
case 99162322:
if(s1.equals("hello"))
byte0 = 0;
break; case 113318802:
if(s1.equals("world"))
byte0 = 1;
break;
}
switch(byte0)
{
case 0: // '\0'
System.out.println("Hello");
break; case 1: // '\001'
System.out.println("World");
break; default:
System.out.println("HelloWorld");
break;
}
}

可以看到字符串是通过比较字符串的hashcode来进行比较,当两个字符串的hashCode值相同时,再通过equals()来确定其是否真正相同.

因此 Switch 比较 String 的本质还是比较 int 类型的数据。

变长参数

变长参数,即允许在方法调用时传入不定数量的参数.具体使用如下所示:

public static void unSignedArgs(String... a){
for (String s : a) {
System.out.println(s);
}
} public static void main(String[] args) {
unSignedArgs("1","3","4");
}

unSignedArgs()方法中,我们定义了一个变长参数,然后在方法调用的时候,传入3个参数.那么这种方法是如何实现的.

public static transient void unSignedArgs(String as[])
{
String as1[] = as;
int i = as1.length;
for(int j = 0; j < i; j++)
{
String s = as1[j];
System.out.println(s);
}
} public static void main(String args[])
{
unSignedArgs(new String[] {
"1", "3", "4"
});
}
}

可以看到变长参数,本质是通过数组来实现的,首先在方法定义时,将变长参数转换为了数组.然后在方法调用的时候是将传入的参数转换成数组然后再传入定义的方法中。

try-with-resource

在过去操作资源时,使用try-catch-finally语句,需要开发人员手动在finally中关闭资源。但现在官方提倡使用 try-with-resource 来操作资源,那么该语法是如何使用的呢。

源代码如下所示:

   try (FileInputStream fileInputStream = new FileInputStream("dfd")){

       fileInputStream.read();
}catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

反编译后代码如下所示:

FileInputStream fileinputstream;
Throwable throwable;
Exception exception;
fileinputstream = new FileInputStream("dfd");
throwable = null;
try
{
fileinputstream.read();
}
catch(Throwable throwable2)
{
throwable = throwable2;
throw throwable2;
}
finally
{
if(fileinputstream == null) goto _L0; else goto _L0
}
if(fileinputstream != null)
if(throwable != null)
try
{
fileinputstream.close();
}
catch(Throwable throwable1)
{
throwable.addSuppressed(throwable1);
}
else
fileinputstream.close();
break MISSING_BLOCK_LABEL_104;
if(throwable != null)
try
{
fileinputstream.close();
}
catch(Throwable throwable3)
{
throwable.addSuppressed(throwable3);
}
else
fileinputstream.close();
throw exception;
Object obj;
obj;
((FileNotFoundException) (obj)).printStackTrace();
break MISSING_BLOCK_LABEL_104;
obj;
((IOException) (obj)).printStackTrace();
}

可以看到编译器自动帮助我们进行资源的关闭,减少了编程人员出错的可能.

数值字面量

数值字面量即在多位数值中穿插入_,方便开发人员快速掌握数值的大小.

int a = 10_000;
System.out.println(a+1);

那么这个语法糖的含义是什么呢,反编译后如下所示

char c = '\u2710';
System.out.println(c + 1);

从结果可以看到编译器是不会管下划线的,其只会将数值正常的读写出来.

断言

    int a = 1;
int b = 2;
assert a == b;
System.out.println(a+b);

翻译后代码如下所示

 {
int i = 1;
byte byte0 = 2;
if(!$assertionsDisabled && i != byte0)
{
throw new AssertionError();
} else
{
System.out.println(i + byte0);
return;
}
}

从代码中可以清楚地看到断言的底层实现机制是用if语句来实现,如果条件不符合,则抛出异常.

条件编译

Java中的条件编译,是通过永真或永假if来实现的,编译器会判断条件是否符合,从而来判断是否进行编译.

源代码如下所示:

    // 条件编译
if (true){
System.out.println("true");
}else{
System.out.println("false");
}

反编译后代码如下:

 {
System.out.println("true");
}

从代码中我们可以看到,编译器对永远不会执行的代码进行了不编译的处理,从而达到了条件编译的效果.但其实笔者感觉条件编译在Java中用处不大,作用就是在不同的模式或机器下,可以编译执行不同的代码.不过有总比没有好.

内部类

在这里我们说内部类是一个语法糖,是因为其仅仅是一个编译时的概念,在编译阶段,编译器会将外部类和内部类进行编译,从而生成两个不同的文件,如下所示:

public class TestForEach {
public class Children{
}
} [260259@localhost src]$ ll
总用量 16
-rw-rw-r--. 1 260259 260259 331 5月 21 11:04 TestForEach$Children.class
-rw-rw-r--. 1 260259 260259 335 5月 21 11:04 TestForEach.class
-rw-rw-r--. 1 260259 260259 506 5月 21 11:04 TestForEach.jad
-rw-rw-r--. 1 260259 260259 2206 5月 21 11:06 TestForEach.java

反编译后,如下所示:

public class TestForEach
{
public class Children{ final TestForEach this$0; public Children(){
this$0 = TestForEach.this;
super();
}
}

枚举

枚举是一种特殊的数据接口,其中包含了一种特殊的数据接口,以key-value 的形式来存储数据,那么 enum 是一种类吗,其内部又是如何实现的呢。

首先我们定义一个枚举:

public enum testEnum {
SPRING,SUMMER,AUTUMN,WINTER
}

此时,我们对这个枚举进行反编译,

public final class testEnum extends Enum
{ public static testEnum[] values()
{
return (testEnum[])$VALUES.clone();
} public static testEnum valueOf(String s)
{
return (testEnum)Enum.valueOf(testEnum, s);
} private testEnum(String s, int i)
{
super(s, i);
} public static final testEnum SPRING;
public static final testEnum SUMMER;
public static final testEnum AUTUMN;
public static final testEnum WINTER;
private static final testEnum $VALUES[]; static
{
SPRING = new testEnum("SPRING", 0);
SUMMER = new testEnum("SUMMER", 1);
AUTUMN = new testEnum("AUTUMN", 2);
WINTER = new testEnum("WINTER", 3);
$VALUES = (new testEnum[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}

从结果,我们可以看出,首先枚举是一个编译时的概念,这也说明了其是Java的一个语法糖,编译器在编译的时候自动生成了一个类继承自Enum,同时声明为final,这也为枚举是不可继承的提供了理论基础.

lambda

lambda作为Java8新出的一个功能点其实也是一种语法糖.因为笔者这边没有Java8及以上版本的反编译工具,因此这边就不详细描述了,粗略说一下,lambda作为一个语法糖,其内部其实是通过相关的两个底层Api来实现的.

总结

在本文中,笔者介绍了Java中的12个语法糖,作为开发人员了解这些语法糖的用法以及其内部的含义,可以让我们更加高效地开发业务代码,同时也可以让我们了解编译器的优化逻辑.从而提高程序的编写效率和运行效率.

JVM(二):Java中的语法糖

文章在公众号"iceWang"第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯.

本系列文章主要借鉴自<深入分析JavaWeb技术内幕>和<深入理解Java虚拟机-JVM高级特性与最佳实践>.