⭐️写在前面
这里是温文艾尔の学习之路
- 如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
- 博客主页 温文艾尔の学习小屋
- ⭐️更多文章请关注温文艾尔主页
- 文章发布日期:2022.02.08
- java学习之路!
- 欢迎各位点赞评论收藏⭐️
- 新年快乐朋友们
- jvm学习之路!
- ⭐️上一篇内容:【JVM】JVM05(从字节码角度分析i++和++i的执行流程))
所谓语法糖,其实就是指java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃)
注意以下代码的分析,借助javap工具,idea的反编译功能,idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换出中间的java源码,切记。
public class Candy1{
}
编译成class后的代码:
public class Candy1{
//这个无参构造器是编译器帮助我们加上的
public Candy1(){
super();//即调用父类Object的无参构造方法
}
}
这个特性是JDK5开始加入的,代码片段1:
public class Demo01 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
上面代码在JDK5之前是无法编译通过的,必须改写为代码片段2:
public class Demo01 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转化的事情在JDK5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转换为代码片段2
泛型也是在JDK5开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了Object类型来处理
public class Demo02 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);//实际调用的是List.add(Object e)
Integer x = list.get(0);//实际调用的是Object obj = list.get(int index);
}
}
所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作:
//需要将Object转为Integer
Integer x = ( Integer)list.get(0);
如果前面的x变量类型修改为int基本类型那么最终生成的字节码是:
//需要将Object转为Integer
int x = ((Integer)list.get(0)).intValue();
这些事情现在都不用我们自己做
擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息
Compiled from "Demo02.java"
public class com.wql.jvm.Candy.Demo02
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // com/wql/jvm/Candy/Demo02
super_class: #9 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #9.#29 // java/lang/Object."":()V
#2 = Class #30 // java/util/ArrayList
#3 = Methodref #2.#29 // java/util/ArrayList."":()V
#4 = Methodref #7.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Int
eger;
#5 = InterfaceMethodref #32.#33 // java/util/List.add:(Ljava/lang/Object;)Z
#6 = InterfaceMethodref #32.#34 // java/util/List.get:(I)Ljava/lang/Object;
#7 = Class #35 // java/lang/Integer
#8 = Class #36 // com/wql/jvm/Candy/Demo02
#9 = Class #37 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/wql/jvm/Candy/Demo02;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 list
#22 = Utf8 Ljava/util/List;
#23 = Utf8 x
#24 = Utf8 Ljava/lang/Integer;
#25 = Utf8 LocalVariableTypeTable
#26 = Utf8 Ljava/util/List<Ljava/lang/Integer;>;
#27 = Utf8 SourceFile
#28 = Utf8 Demo02.java
#29 = NameAndType #10:#11 // "":()V
#30 = Utf8 java/util/ArrayList
#31 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer;
#32 = Class #40 // java/util/List
#33 = NameAndType #41:#42 // add:(Ljava/lang/Object;)Z
#34 = NameAndType #43:#44 // get:(I)Ljava/lang/Object;
#35 = Utf8 java/lang/Integer
#36 = Utf8 com/wql/jvm/Candy/Demo02
#37 = Utf8 java/lang/Object
#38 = Utf8 valueOf
#39 = Utf8 (I)Ljava/lang/Integer;
#40 = Utf8 java/util/List
#41 = Utf8 add
#42 = Utf8 (Ljava/lang/Object;)Z
#43 = Utf8 get
#44 = Utf8 (I)Ljava/lang/Object;
{
public com.wql.jvm.Candy.Demo02();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wql/jvm/Candy/Demo02;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Lja
va/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Lja
va/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)L
java/lang/Object;
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
LineNumberTable:
line 14: 0
line 15: 8
line 16: 20
line 17: 31
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
//局部变量类型表 ,包含了我们方法参数的一些泛型信息 LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
}
SourceFile: "Demo02.java"
使用反射,仍然能够获得这些信息
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始类型 - " + parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
可变参数也是JDK5开始加入的新特性:
例如:
public class Demo03 {
public static void foo(String... args){
String[] array = args;
System.out.println(array);
}
public static void main(String[] args) {
foo("hello","java");
}
}
可变参数String… args 实是-个string[] args, 从代码中的赋值语句中就可以看出来。同样java编译器会在编译期间将上述代码变换为:
public class Demo03 {
public static void foo(String[] args){
String[] array = args;
System.out.println(array);
}
public static void main(String[] args) {
foo(new String[]{"hello","java"});
}
}
注意:
如果调用了foo(),则等价代码为foo(new String[]{}),创建了一个空的数组,而不会传递null进去
JDK5开始引入的语法糖,数组的循环:
public class Demo04 {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};//数组赋初值的简化写法也是语法糖
for (int e:array){
System.out.println(e);
}
}
}
会被编译器转换为:
public class Demo04 {
public static void main(String[] args) {
int[] array = new int[]{1,2,3,4,5};//数组赋初值的简化写法也是语法糖
for (int i = 0; i < array.length; ++i) {
int e =array[i];
System.out.println(e);
}
}
}
而集合的循环:
public class Demo05 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
System.out.println(i);
}
}
}
实际被编译器转换为对迭代器的调用:
public class Demo05 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator<Integer> iter = list.iterator();
while (iter.hasNext()){
Integer e = iter.next();
System.out.println(e);
}
}
}
注意:
foreach循环写法,能够配合数组,以及所有实现了Iterable接口的集合类一起使用,其中Iterable用来获取集合的迭代器(Iterator)
从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Demo06 {
public static void choose(String str){
switch (str){
case "hello":{
System.out.println("h");
break;
}
case "world":{
System.out.println("w");
break;
}
}
}
}
注意:
switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚会被编译器转换为:
public class Demo06 {
public static void choose(String str){
byte x = -1;
switch (str.hashCode()){
case 99162322://hello的hashCode
if (str.equals("hello")){
x=0;
}
break;
case 113318802://world的hashCode
if (str.equals("world")){
x=1;
}
}
switch (x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
可以看到,执行了两边switch,第一遍是根据字符串的hashCode和equals将字符串的转换为相应byte类型,第二遍才是利用byte执行进行比较
为什么第一遍时必须比较hashCode,有利用equals作比较呢?hashCode是为了提高效率,减少可能的比较;而equals是为了防止hashCode冲突,例如BM和C,这两个字符串的hashCode值都是2123,如果有如下代码:会被编译器转换为:
public class Demo07 {
public static void choose(String str) {
byte x = -1;
switch (str.hashCode()){
case 2123: //hashCode值相等,需要进一步用equals比较
if (str.equals("C")){
x=0;
}else if (str.equals("BM")){
x=0;
}
default:
switch (x){
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
}
switch枚举的例子,原始代码:
enum Sex {
MALE,FEMALE
}
public class Demo08 {
public static void foo(Sex sex){
switch (sex){
case MALE:
System.out.println("男");break;
case FEMALE:
System.out.println("女");break;
}
}
}
转换后代码:
public class Demo09 {
/**
* 定义一个合成类(仅jvm使用,对我们不可见)
* 用来映射枚举的ordinal与数组元素的关系
* 枚举的ordinal表示枚举对象的序号,从0开始
* 即MALE的ordinal()=0,FEMALE的ordinal()=1
*/
static class $MAP{
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()]=1;
map[Sex.FEMALE.ordinal()]=2;
}
}
public static void foo(Sex sex){
int x = $MAP.map[sex.ordinal()];
switch (x){
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
JDK7新增了枚举类,以前面的性别枚举为例:
enum Sex {
MALE,FEMALE
}
转换后代码:
public final class Sex extends Enum<Sex>{
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE",0);
FEMALE = new Sex("FEMALE",0);
$VALUES = new Sex(MALE,FEMALE);
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values(){
return $VALUES.clone();
}
public static Sex valueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}
JDK7开始新增了对需要关闭的资源处理的特殊语法try-with-resources:
try(资源变量 = 创建资源对象){
}catch(){
}
其中资源对象需要实现AutoCloseable接口,例如InputStream、OutputStream、Connection、Statement、ResultSet等接口都实现了AutoCloseable,使用try-with-resources可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:
public class Demo11 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
}catch (IOException e){
e.printStackTrace();
}
}
}
会被转换为:
public static void main(String[] args) {
try {
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
// t 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
希望在关闭异常时也有可能发生异常,try中的异常和资源关闭中的异常都想保留就需要t.addSuppressed(e2);
为什么要设计一个t.addSuppressed(e2);(添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想try-with-resources生成的finally中如果抛出了异常):
例如
public class Demo12 {
public static void main(String[] args) {
try(MyResource resource = new MyResource()) {
int i = 1/0;
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable{
public void close() throws Exception{
throw new Exception("close 异常");
}
}
外层异常为int i = 1/0;,内层异常为资源关闭时的异常
可以看到两个异常都没有丢
我们都知道,方法重写时对返回值分两种情况:
class A{
public Number m(){
return 1;
}
}
class B extends A {
@Override
//子类m方法的返回值是Integer是父类m方法返回值Number的子类
public Integer m() {
return 2;
}
}
对于子类,java编译器会做如下处理:
class B extends A {
public Integer m(){
return 2;
}
//此方法才是真正重写了父类public Number m()方法
public synthetic bridge Number m(){
//调用public Integer m()
return m();
}
}
synthetic bridge的含义是合成方法,对程序员是不可见的
桥接方法比较特殊,仅对java虚拟机可见,并且与原来的public Integer m()没有命名冲突,可以用下面反射代码来验证:
for (Method m : B.class.getDeclaredMethods()) {
System.out.println(m);
}
会输出
public java.lang.Integer test.candy.B.m()
public java.lang.Number test.candy.B.m()
源代码:
public class Demo14 {
public static void main(String[] args) {
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("ok");
}
};
}
}
转换后代码:
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
引用局部变量的匿名内部类,源代码:
public static void test(final int x){
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println("ok"+x);
}
};
}
转换后代码
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
这也同时解释了为什么匿名内部类引用局部变量时,局部变量必须是final的:因为在创建Candy11$1对象时,将x的值赋给了Candy 11 1对象的valx属性,所以x不应该再发生变化了,如果变化,那么valx属性没有机会再跟着一起变化