倘只看书,便变成书橱
本文总结了一部分java编译期的语法糖。为了便于理解,本文不使用java字节码来推论;采用源代码和编译器编译后的伪代码来直观的表达。理解java在编译成class文件时,实际上编译器会给我们如何传递给jvm
文章中每个语法糖都有如下两段代码,第一个源代码,第二个编译器解析后的伪代码,读者可根据两段代码的差异性,理解语法糖,格式如下:
//源代码
//编译器解析后的伪代码
一、默认构造器
当我们创建一个类,如果没有编写任何构造器,编译器默认添加一个无参构造器
public class ClassName{
}
public class ClassName{
public ClassName(){
super();
}
}
二、自动拆/装箱
装箱:就是自动将基本数据类型转换为包装器类型;
拆箱:就是自动将包装器类型转换为基本数据类型;
1、编译器通过Integer.valueOf()实现基本数据类型的装箱
2、编译器通过integer.intValue()实现包装器类型的拆箱
3、如果用==比较,且一个是包装类型,一个是基本数据类型,会将包装类型拆箱
4、如果用equals比较,且一个是包装类型,一个是基本数据类型,会将基本类型装箱
public class ClassName{
public static void main(String[] args) {
Integer a = 1;
int b = a;
boolean c = a == b;
boolean d = a.equals(b);
}
}
public class ClassName{
public static void main(String[] args) {
Integer a = Integer.valueOf(1);
int b = a.intValue();
boolean c = a.intValue() == b; //需要注意NPE
boolean d = a.equals(Integer.valueOf(b));
}
}
三、范型擦除
JDK1.5开始
编译器会在编译后将范型擦除掉,但是会将范型信息存放在字节码文件模型的LocalVariableTypeTable 中
public class ClassName{
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
Integer value = list.get(0);
}
}
public class ClassName{
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(Integer.valueOf(1));
Integer value = (Integer) list.get(0);
}
}
由于范型信息被存放在了字节码文件的LocalVariableTypeTable 中 ,在方法的形参和返回值上的范型,我们可以通过反射拿到
public Set<String> method(Map<Integer,Object> map){
return null;
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = ClassName.class.getMethod("method", Map.class);
//获取参数类型
Type[] parameterTypes = method.getGenericParameterTypes();
ParameterizedType parameterType = (ParameterizedType)parameterTypes[0];
Type mapType = parameterType.getRawType(); //interface java.util.Map
//获取参数范性
Type[] actualTypeArguments = parameterType.getActualTypeArguments();
Type integerType = actualTypeArguments[0]; //class java.lang.Integer
Type objectType = actualTypeArguments[1]; //class java.lang.Object
//获取返回值类型
ParameterizedType returnType = (ParameterizedType)method.getGenericReturnType();
Type setType = returnType.getRawType(); //interface java.util.Set
//获取返回值范性
Type stringType = returnType.getActualTypeArguments()[0]; //class java.lang.String
}
四、可变参数
可变长参数的定义:使用...表示可变长参数
例如 print(String... args){ ... }
在具有可变长型参的方法中可以把参数当成数组使用
public static void method(String... args){
String[] arr = args;
}
public static void main(String[] args) {
method("a","b","c");
method();
}
public static void method(String[] args){
String[] arr = args;
}
public static void main(String[] args) {
method(new String[]{"a","b","c"});
method(new String[]{}); //无参传空数组
}
五、forech
forech根据迭代对象的类型分为两种
数组
int[] arr = {1,2,3,4,5};
for (int e : arr) {
System.out.println(e);
}
int[] arr = new int[]{1,2,3,4,5};
for (int i = 0; i < arr.length; i++) {
int e = arr[i];
System.out.println(e);
}
集合
List<Integer> list = Arrays.asList(1,2);
for (Integer e : list) {
System.out.println(e);
}
List list = Arrays.asList(Integer.valueOf(1),Integer.valueOf(2));
Iterator iterator = list.iterator();
while(iterator.hasNext()){
Integer e = (Integer) iterator.next();
System.out.println(e);
}
六、switch的字符串
switch的实现是通过跳转指令或跳转表实现的, 这取决于分支的长度
值1 | 代码入口地址 |
值2 | 代码入口地址 |
值3 | 代码入口地址 |
跳转表的特性在于key是整数且有序的,可以通过二分法快速匹配到,但JDK7开始,switch可以使用字符串和枚举。字符串与整形之前如何转换?这个功能实际上是语法糖来实现的。
public static void method(String str){
switch (str){
case "Aa":
System.out.println("Aa");
break;
case "AA":
System.out.println("AA");
break;
case "BB":
System.out.println("BB");
break;
}
}
public static void method(String str){
byte x = -1;
switch (str.hashCode()){
case 2112:
if(str.equals("Aa")){
x = 0;
}else if(str.equals("BB")){
x = 1;
}
break;
case 2080:
if(str.equals("AA")){
x=2;
}
break;
default:
}
switch (x){
case 0:
System.out.println("Aa");
break;
case 1:
System.out.println("BB");
break;
case 2:
System.out.println("AA");
break;
default:
}
}
七、switch的枚举
public static void method(SexEnum sexEnum){
switch (sexEnum){
case BOY: break;
case GIRL:break;
}
}
//在本类中生成一个静态的内部类,仅jvm识别
synthetic static class $MAP{
//将使用到的枚举,所有的选项放入一个数组中和switch的int值对应
synthetic static final int[] map = new int[SexEnum.values().length];
static{
map[SexEnum.BOY.ordinal()] = 1;
map[SexEnum.GIRL.ordinal()] = 2;
}
}
public static void method(SexEnum sexEnum){
//使用枚举的ordinal取出int值对应
switch (map[sexEnum.ordinal()]){
case 1: break;
case 2: break;
}
}
八、枚举类
jdk7开始,枚举实际是一个继承了Enum的类
enum Sex{
BOY(100),
GIRL(200);
private Integer code;
Sex(Integer code){
this.code = code;
}
}
final class Sex extends Enum<Sex>{
public static final Sex BOY;
public static final Sex GIRL;
public static final Sex[] $VALUE;
static {
BOY = new Sex("BOY",0,100);
GIRL = new Sex("GIRL",1,200);
$VALUE = new Sex[]{BOY,GIRL};
}
private Integer code;
private Sex(String name, Integer ordinal,Integer code){
super(name,ordinal);
this.code = code;
}
public static Sex[] values(){
return $VALUE.clone();
}
public static Sex valueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}
九、重写方法 - 方法桥接
abstract class A{
public abstract Number m();
}
class B extends A{
@Override
public Integer m() {
return null;
}
}
abstract class A{
public abstract Number m();
}
class B extends A{
public Integer m() {
return null;
}
/*编译器生成桥接方法,这个方法我们不可见由jvm执行
这个方法才是真正的实现父类的方法
这个方法不受同名限制
这个方法直接调用子类的同名方法
*/
@Override
public synthetic bridge Number m(){
return m();
}
}
通过打印B类的方法,我们可以验证
for (Method declaredMethod : B.class.getDeclaredMethods()) {
System.out.println(declaredMethod);
}
打印出来两个方法
public java.lang.Integer utils.B.m()
public java.lang.Number utils.B.m()
十、内部类
成员内部类
class A{
private String filed;
class B{
public void test(){
//内部类引用了外部类的成员变量
System.out.println(filed);
}
}
}
class A{
private String filed;
//将权限暴露
static String filed0(A a){
return a.filed;
}
}
class B{
//持有外部类的引用
private A a;
//创建外部类,需要传递外部类的引用
public B(A a){
this.a = a;
}
public void test(){
System.out.println(A.filed0(a));
}
}
可以看出:外部类和内部类都有相应的改动
1、在外部类存在一个公用的静态方法,将字段权限暴露出来
2、在内部类创建时,会通过构造器将外部类对象注入到自身成员变量中
3、内部类调用外部类的静态方法,获取到私有属性的值
方法内部类
class A{
public void testA(String name){
class B{
public void testB(){
//内部类引用了方法的局部变量
System.out.println(name);
}
}
new B().testB();
}
}
class A{
public void testA(String name){
//编译器注入
new B(this,name).testB();
}
}
class B{
private A a;
private String name;
//在创建内部类时,将引用的外部方法的局部变量作为内部的成员变量
public B(A a,String name){
this.a = a;
this.name = name;
}
public void testB(){
//实际是用的自己的成员变量
System.out.println(name);
}
}
其内部类的构造器上相对于成员内部类基础上多了局部变量的入参,并将其作为成员变量。
所以为什么方法内部类调用的变量要用final修饰?因为在内部类对象创建时就确定一切了。外面改了,里面就不一致了。
* 内部类中存在指针指向外部类对象,所以当大量使用 new Object(){{}}这样的语法时,要注意是否可能存在内存泄漏的危险