posts - 2,comments - 0,views - 490

Java 语法糖

语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin(彼得·兰丁) 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用,当然也仅仅是方便程序员的使用,因为java虚拟机并不支持语法糖,因此在语法糖在编译阶段就被还原成简单的基础语法结构。
语法糖就是对现有语法的一个封装。简而言之,语法糖让程序更加简洁,有更高的可读性

在编程领域,除了语法糖,还有语法盐和语法糖精的说法,有兴趣可以百度了解一下。

解语法糖

语法糖的存在主要是方便开发人员使用。但是,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖
因此,Java中的语法糖只存在于编译期,在编译器将 .java 源文件编译成 .class 字节码时,有一个步骤就是调用desugar(),这个方法就是负责解语法糖而实现的。
如果将语法糖先编译后(解语法糖)在反编译成.java文件,那么所写的语法糖就不会存在了。

Java 中最常用的语法糖主要有switch语句支持String与枚举、泛型和类型擦除、自动装箱与拆箱、方法边长参数、枚举、内部类、条件编译、断言、数值字面量、增强for循环、try-with-resources语句、Lambda表达式等。

糖块一:switch 支持String与枚举

在JDK1.5之前,switch循环只支持byte short char int四种数据类型.

JDK1.5 在switch循环中增加了枚举类与byte short char int的包装类,对四个包装类的支持是因为java编译器在底层手动进行拆箱,而对枚举类的支持是因为枚举类有一个ordinal方法,该方法实际上是一个int类型的数值.

jdk1.7开始支持String类型,但实际上String类型有一个hashCode算法,结果也是int类型.而byte short char类型可以在不损失精度的情况下向上转型成int类型.所以总的来说,可以认为switch中只支持int.

java 对switch的支持

public class SwitchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}

反编译后内容:

public class SwitchStringDemo {
public static void main(String[] args) {
String str;
String string = str = "world";
int n = -1;
switch (string.hashCode()) {
case 99162322: {
if (!string.equals("hello")) break;
n = 0;
break;
}
case 113318802: {
if (!string.equals("world")) break;
n = 1;
}
}
switch (n) {
case 0: {
System.out.println("hello");
break;
}
case 1: {
System.out.println("world");
break;
}
}
}
}

字符串的switch是通过equals()和hashCode()方法来实现的。

进行switch比较的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。

糖块二、泛型和类型擦除

很多语言都是支持泛型的,但是不同的编译器对于泛型的处理方式是不同的。

通常情况下,一个编译器处理泛型有两种方式:Code specialization和Code sharing。

C++和C#是使用Code specialization的处理机制,而Java使用的是Code sharing的机制

Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

也就是说,对于Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。

类型擦除的主要过程如下:

  • 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  • 移除所有的类型参数。
Map<String, String> map = new HashMap<String, String>();
map.put("name", "JourWon");
map.put("wechat", "JourWon");
map.put("blog", "https://blog.csdn.net/ThinkWon");
// 解语法糖后
Map map = new HashMap();
map.put("name", "JourWon");
map.put("wechat", "JourWon");
map.put("blog", "https://blog.csdn.net/ThinkWon");

以下两个方法编译出错

import java.util.List;
public class FanxingTest{
public void method(List<String> list){
System.out.println("List String");
}
public void method(List<Integer> list){
System.out.println("List Int");
}
}

糖块三、自动装箱与拆箱

自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱

原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。

自动装箱:

public static void main(String[] args) {
int i = 10;
Integer n = i;
}
// 反编译后
public static void main(String args[]) {
int i = 10;
Integer n = Integer.valueOf(i);
}

自动拆箱:

public static void main(String[] args) {
Integer i = 10;
int n = i;
}
// 反编译后
public static void main(String args[]) {
Integer i = Integer.valueOf(10);
int n = i.intValue();
}

从反编译得到内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。

糖块四 、方法变长参数

可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。

public static void main(String[] args) {
print("aa", "bb", "cc");
}
public static void print(String... strs) {
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
}
// 反编译后
public static void main(String[] args) {
print(new String[]{"aa", "bb", "cc"});
}
public static void print(String[] strs) {
for (int i = 0; i < strs.length; i++)
System.out.println(strs[i]);
}

从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法时传递实参的个数,然后再把参数值全部放到这个数组当中,再把这个数组作为参数传递到被调用的方法中。

糖块五 、枚举

为什么说枚举也是语法糖?

Java SE5提供了一种新的类型-Java的枚举类型

未有枚举前,定义季节

//分别用1 表示春天,2表示夏天,3表示秋天,4表示冬天。
public class Season {
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int AUTUMN = 3;
public static final int WINTER = 4;
}

这种方法称作int枚举模式。可这种模式有什么问题呢,我们都用了那么久了,应该没问题的。通常我们写出来的代码都会考虑它的安全性易用性可读性。 首先我们来考虑一下它的类型安全性。当然这种模式不是类型安全的。比如说我们设计一个函数,要求传入春夏秋冬的某个值。但是使用int类型,我们无法保证传入的值为合法。

private String getChineseSeason(int season){
StringBuffer result = new StringBuffer();
switch(season){
case Season.SPRING :
result.append("春天");
break;
case Season.SUMMER :
result.append("夏天");
break;
case Season.AUTUMN :
result.append("秋天");
break;
case Season.WINTER :
result.append("冬天");
break;
default :
result.append("地球没有的季节");
break;
}
return result.toString();
}
public void doSomething(){
System.out.println(this.getChineseSeason(Season.SPRING));//这是正常的场景
System.out.println(this.getChineseSeason(5));//这个却是不正常的场景,这就导致了类型不安全问题
}

接下来我们来考虑一下这种模式的可读性。使用枚举的大多数场合,我都需要方便得到枚举类型的字符串表达式。如果将int枚举常量打印出来,我们所见到的就是一组数字,这是没什么太大的用处。我们可能会想到使用String常量代替int常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。 从类型安全性程序可读性两方面考虑,intString枚举模式的缺点就显露出来了。

// 定义一个枚举类
// enum就和class一样,只是一个关键字,他并不是一个类
public enum T {
SPRING,SUMMER;
}
// 反编译
// 通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。
public final class T extends Enum<T> {
public static final /* enum */ T SPRING = new T("SPRING", 0);
public static final /* enum */ T SUMMER = new T("SUMMER", 1);
private static final /* synthetic */ T[] $VALUES;
public static T[] values() {
return (T[])$VALUES.clone();
}
public static T valueOf(String name) {
return Enum.valueOf(T.class, name);
}
private T(String string, int n) {
super(string, n);
}
static {
$VALUES = new T[]{SPRING, SUMMER};
}
}

糖块六 、内部类

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。

内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念。

outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。

当jad对OutterClass.class文件进行反编译的时候,它会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad文件

public class OutterClass {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public static void main(String[] args) {
}
class InnerClass {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

糖块七 、条件编译

—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

public class ConditionalCompilation {
public static void main(String[] args) {
final boolean DEBUG = true;
if(DEBUG) {
System.out.println("Hello, DEBUG!");
}
final boolean ONLINE = false;
if(ONLINE){
System.out.println("Hello, ONLINE!");
}
}
}

反编译后代码如下:

public class ConditionalCompilation {
public static void main(String[] args) {
boolean DEBUG = true;
System.out.println("Hello, DEBUG!");
boolean ONLINE = false;
}

首先,我们发现,在反编译后的代码中没有System.out.println(“Hello, ONLINE!”);,这其实就是条件编译。

所以,Java语法的条件编译,是通过判断条件为常量的if语句实现的。根据if判断条件的真假,编译器直接把分支为false的代码块消除。

通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个Java类的结构或者类的属性上进行条件编译。

糖块八 、断言

在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略)。

如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。

格式:

assert <布尔表达式>;
assert <布尔表达式> : <错误信息>;

assert 后面跟个冒号表达式。如果冒烟前为true,则冒号后面的被忽略,否则抛出AssertionError,错误内容为冒号后面的内容。

含有断言的代码:

public class AssertTest {
public static void main(String args[]) {
int a = 1;
int b = 1;
assert a == b;
System.out.println("CSDN-ThinkWon");
assert a != b : "ThinkWon";
System.out.println("博客:https://blog.csdn.net/ThinkWon");
}
}

反编译后代码如下:

public class AssertTest {
static final /* synthetic */ boolean $assertionsDisabled;
public static void main(String[] args) {
boolean a = true;
boolean b = true;
if (!$assertionsDisabled && a != b) {
throw new AssertionError();
}
System.out.println("CSDN-ThinkWon");
if (!$assertionsDisabled && a == b) {
throw new AssertionError((Object)"ThinkWon");
}
System.out.println("\u535a\u5ba2:https://blog.csdn.net/ThinkWon");
}
static {
$assertionsDisabled = !AssertTest.class.desiredAssertionStatus();
}
}

很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。

其实断言的底层实现就是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果断言结果为false,则程序抛出AssertError来打断程序的执行。

糖块九 、数值字面量

在java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

比如:

public class Test {
public static void main(String[] args) {
int i = 10_000;
System.out.println(i);
}
}
// 反编译后:
// 反编译后就是把_删除了。也就是说编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。
public class Test {
public static void main(String[] args) {
int i = 10000;
System.out.println(i);
}
}

糖块十 、增强for循环

for-each的实现原理其实就是使用了普通的for循环和迭代器。

public static void main(String args[]) {
String[] strs = {"aa", "bb", "cc"};
for (String s : strs) {
System.out.println(s);
}
System.out.println();
List<String> strList = Arrays.asList(strs);
for (String s : strList) {
System.out.println(s);
}
}

反编译后:

public static void main(String args[]) {
String[] strs;
String[] arrstring = strs = new String[]{"aa", "bb", "cc"};
int n = arrstring.length;
for (int i = 0; i < n; ++i) {
String s = arrstring[i];
System.out.println(s);
}
System.out.println();
List<String> strList = Arrays.asList(strs);
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}
}

糖块十一 、try-with-resource语句

Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。

关闭资源的常用方式就是在finally块里释放,即调用close方法。比如,我们经常会写这样的代码:

public static void main(String args[]) {
BufferedReader br = null;
try {
String line;
br = new BufferedReader(new FileReader("d:\\hello.xml"));
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
// handle exception
}
}
}

从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:

public static void main(String args[]) {
try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hello.xml"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
}
}

看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。

反编译以上代码,看下他的背后原理:

public static void main(String[] args) {
try {
BufferedReader br = new BufferedReader(new FileReader("d:\\ hello.xml"));
Throwable throwable = null;
try {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Throwable line) {
throwable = line;
throw line;
} finally {
if (br != null) {
if (throwable != null) {
try {
br.close();
} catch (Throwable line) {
throwable.addSuppressed(line);
}
} else {
br.close();
}
}
}
} catch (IOException br) {
// empty catch block
}
}

其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。

所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。

糖块十二、Lambda表达式

Labmda表达式不是匿名内部类的语法糖(内部类在编译之后会有两个class文件,但是,包含Labmda表达式的类编译后只有一个文件),但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的Labmda相关api。

/**
* lambda表达式
* option: --decodelambdas false
*/
public void lambdaTest() {
String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};
List<String> list = Arrays.asList(qingshanli);
// 使用lambda表达式以及函数操作
list.forEach((str) -> System.out.print(str + "; "));
// 在JDK8中使用双冒号操作符
list.forEach(System.out::println);
}

lambda表达式参考:https://blog.csdn.net/weixin_43691058/article/details/113816211

posted on   园中小小草  阅读(295)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示