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
常量。虽然它为这些常量提供了可打印的字符串,但是它会导致性能问题,因为它依赖于字符串的比较操作,所以这种模式也是我们不期望的。 从类型安全性和程序可读性两方面考虑,int
和String
枚举模式的缺点就显露出来了。
// 定义一个枚举类
// 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
标签:java,String,int,System,语法,static,public,out From: https://www.cnblogs.com/ah-grass/p/16647686.html