美文网首页Android进阶之路Android高级进阶
Android避坑指南:分享几个装13的知识点,那些隐藏在“编译

Android避坑指南:分享几个装13的知识点,那些隐藏在“编译

作者: 小小小小怪兽_666 | 来源:发表于2020-09-27 21:32 被阅读0次

今天这篇文章,将会突出几个关键词:Synthetic,BRIDG,泛型擦除相关的知识。

如果这几个词不熟悉,很正常,没关系。因为正常我们写代码是感知不到这些东西的存在的,只有一些无意中的发现、思考,才能带出这些知识点。

下面我们一起去发现和思考吧。

1、初识Synthetic

我们来看一坨代码:

public class A{
    public static void main(String[] args){
        B b = new A.B();
    }
    private static class B{

    }
}

上述代码,如果我们执行javac A.java编译,会产生几个class文件?

我们分析下:

  1. A.class 肯定有
  2. 有个静态内部类B,还有个A$B.class

两个。

实际运行你会发现有三个:

  1. A.class
  2. A$B.class
  3. A$1.class

为什么会多了个A$1.class呢?

答:&taodashuai。

为什么会多了个A$1.class呢?

其实就是为了实例化class B。

是这样的:

大家都知道,在java文件中编写的内部类,在编译成class之后,都会像是被抽出来一个单独的类一样,也就是从内部类变成了普通的类。

因为内部类B没有定义构造方法,在编译时编译器就会自动帮它生成一个,访问权限都是跟随这个类的访问权限的,比如:

  • public class B,生成的构造方法的访问权限就是public;
  • protected class B ——> protected B() {};
  • class B ——> B() {};
  • private class B ——> private B() {};

编译成class之后,内部类会以普通类的方式存在,而且构造方法又是private,那怎么能在A中实例化它呢?

编译器在编译时会:

既然原来的private构造方法无法改变,那我还可以新增一个可以让外部访问的构造方法啊!

不过,现在已经有一个无参构造方法了,再加一个的话,怎么保证不会跟现有的构造方法签名(参数类型)冲突呢?

于是,内部类A$1就诞生了!

反编译看一下就很清楚了 :

这里我们可以使用 javap -private A$B.class 来简单查看

Compiled from "A.java"
class A$B {
  private A$B(); // 这里就是编译器默认生成的构造函数
  A$B(A$1); // 这里是编译器额外生成的构造函数
}

再看看 A1 到底是个啥 javap -c A1.class

Compiled from "A.java"
class A$1 {
}

很简单,甚至连构造函数都没有,这个就是所谓的合成类(synthetic class),由编译器自行生成,常用于各种内部类。

而我们在A类里使用 new A.B() ,其实调用的就是 B类里的 AB(A1),可以使用 javap -c A.class 来看看A类里到底干了啥

Compiled from "A.java"
class A {
  A();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class A$B
       3: dup
       4: aconst_null
       5: invokespecial #3                  // Method A$B."<init>":(LA$1;)V   // 在这里调用
       8: astore_1
       9: return
}

Method AB."<init>":(LA1;)V 从这个方法注释就能很清晰的看出来调用的是 AB(A1) 构造方法,aconst_null代表其参数就是一个null。

ok,上面我大概看懂了,但是这些知识和标题中的:Synthetic有什么关系呢?

我们继续反编译看下:

javap -v A$1.class

class A$1
  minor version: 0
  major version: 52
  flags: ACC_SUPER, ACC_SYNTHETIC

可以看到 A$1,这个编译器额外生成的类,有个 flag 为:ACC_SYNTHETIC。

同理,我们想看看 A$B中,那个编译器额外生成的方法的flag:

A$B(A$1);
    descriptor: (LA$1;)V
    flags: ACC_SYNTHETIC

没错,熟悉的ACC_SYNTHETIC又出现了了。

所以,可以总结下,编译器为了方便我们编写代码,被背着我们做一些弥补性的工作,中间会产生一些类或者方法,在这些类和方法中都有ACC_SYNTHETIC标记。

你可能会有困惑,这都是编译器做的,我们平时写代码遇不到吧?

是有可能遇到的,比如你在反射获取 A$B 的构造方法时,忽然发现一个不是你写的构造方法,是不是一脸懵逼?

当然 Java 层也给大家提供了 API用于识别,该类 or 方法是否是ACC_SYNTHETIC标识的。

ok,继续。

2、再遇BRIDGE

再来看一个例子:

首先我们编写个接口:

interface Animal<T>{
    void test(T t);
}

这个接口有个实现类:

class Dog implements Animal<String>{

    @override
    public void test(String str){
    }
}

符合我们平时的写法对吧。

但是你仔细推敲一下

接口 Animal 类的泛型,在编译成 class 后,会经历泛型擦除,会变成这样:

interface Animal{
    void test(Object obj);
}

而实现类Dog里面有个方法test(String str),注意这个方法和接口类的方法参数并不一致。

那么也就是说,并没有实现接口中的方法,但是,接口的方法,实现类是必须实现的。

问题来了:

和我们理解的接口中声明的方法,实现类必须实现的理念相悖,为何可以呢?

想知道为什么,肯定离不开反编译了:

编译题目给出代码,再像上次那样javap看一下Dog的字节码:

javap -v Dog.class:

可以看到,Dog里面除了我们在代码中编写的test(String),还多了一个test(Object)(还可以看到它在checkcast之后也会调用test(String)方法),注意看这个多出来的test(Object)方法的flags:ACC_PUBLIC 、 ACC_BRIDGEACC_SYNTHETIC

第一个(ACC_PUBLIC)不用说,从第三个(ACC_SYNTHETIC)可以知道这个方法是编译器帮我们生成的,而且我们在代码中不能通过常规方式来访问。

中间的ACC_BRIDGE是什么呢?

正则表达式全局搜一下,看看在哪里声明:

emmmm,跟上次的SYNTHETIC一样,也是在Modifier里面。

这个由编译器帮我们生成的test(Object)方法,就像一座桥一样,将接口方法与我们自己实现的真实方法连接起来。

回到题目中的问题:

为什么没有手动实现接口经过泛型擦除后的test(Object)方法也能正常通过编译呢?

因为这一步编译器已经帮我们做了,在自动生成test(Object)方法后,还会加上一个ACC_BRIDGE标记,在程序中可以通过Method的isBridge方法来判断。

3、再深入一些吧

那么我们再来看一个例子:

interface TestInter<T> {
    T getName(String name);
}

实现类:

class TestImpl implements TestInter<String> {
    @Override
    public String getName(String name) {
        return null;
    }
}

这次猜我的关注点在哪?

我们反编译一下TestImpl:

public java.lang.String getName(java.lang.String);
  descriptor: (Ljava/lang/String;)Ljava/lang/String;
  flags: ACC_PUBLIC


public java.lang.Object getName(java.lang.String);
  descriptor: (Ljava/lang/String;)Ljava/lang/Object;
  flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC

又看到了ACC_BRIDGE,ACC_SYNTHETIC,已知知识。

可以看到生成的两个方法,我们转成Java的展现形式:

  1. String getName(String str)
  2. Object getName(String str);

有没有觉得奇怪?

我贴张图,你就能看明白了:

这两个方法的方法名,参数均相同,只有返回值不同,在我们Java平时编写中是不允许的。

问题来了:

为何在这个场景下,允许「一个类中有方法名,参数均相同,只有返回值不同」两个方法?

为什么在Java代码中定义多个【名字和参数都一样,返回值类型不同】的方法会报错呢?

因为Java语言层面,它的方法签名是不带返回值类型的,只有方法名和参数类型。所以即使两方法的返回值类型不同,它也一样当作重复声明了。

也可以通过一段代码来帮助理解:

public class Test {
    {
        Number n = getNumber(1);
    }

    Long getNumber(int i) {
        return 1L + i;
    }

    Double getNumber(int i) {
        return 1D + i;
    }
}

Test类里面定义了两个getNumber方法,它们的名字和参数类型都完全一样,只是一个返回Double一个返回Long。

如果我在某个地方调用getNumber方法来获取Number对象,你怎么知道我想要调用哪一个?

根本无法辨别的好吧。

那为什么class文件里就可以有多个【名字和参数都一样,返回值类型不同】的方法存在?

JVM是怎么把它们区分开来的?

因为在class常量池中,就已经有对这些方法的完整描述,包括方法的返回值类型。

这样的话,就算方法名和参数都一样,只要返回值不同,那它们也是合法的,JVM也能正确分辨出具体调用的是哪一个。

修改一下题目中的TestImpl,加个构造函数并在里面调用getName方法:

class TestImpl implements TestInter<String> {

    TestImpl() {
        getName(null);
    }

    @Override
    public String getName(String name) {
        return null;
    }
}

编译,然后javap -v TestImpl看下:

看构造方法里highlight的那一行:

invokevirtual的操作数#2,就是要调用的方法符号索引,可以对照着上面常量池中的信息来找到(其实右边的注释已经列出来了)完整的方法签名:

把它们组合起来,就是TestImpl.getName:(Ljava/lang/String;)Ljava/lang/String;了。

如果它调用的是返回值为Object的方法,那么就会有#n = #10:#12 、 #m = #3.#n,下面构造函数中的invokevirtual指令的操作数就是#m(当然了这个方法无法在代码中直接调用到,所以是看不到这种场景的)。

4、总结

能看到这里,恭喜你,大概率你是有收获的。

你可能会迷惑,为什么要掌握这些底层知识,好像我们平时开发时候完全用不到吧。

其实怎么说呢,说用不到,可能还没有遇到相关的场景,当你知道编译器编译期间会增加一些类、方法,假设你有修改字节码的需求的时候,你就可以评估到这些生成的类、方法带来的影响。

甚至,当我们在反射某个类里面方法的时候,也要注意避开编译器生成的桥接方法。

例如上例,Dog 里面在我们看来只有一个test 方法:

class Dog implements Animal<String>{

    @override
    public void test(String str){
    }
}

实际上你通过反射去取,test 方法包含两个,如果不注意就可能取错。

其次,今天这三个点,如果平时注意发现或者思考,应该也会有这样的困惑,这几个点都是有违我们平时开发中的知识的。

后续发现奇怪的东西,就尝试探索破解吧,说不定发现新大陆呢。


最后小编想说:对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

这里分享一下这段时间从朋友,大佬那里收集到的一些2019-2020BAT 面试真题解析,里面内容很多也很系统,包含了很多内容:Android 基础、Java 基础、Android 源码相关分析、常见的一些原理性问题等等,可以很好地帮助我们深刻理解Android相关知识点的原理以及面试相关知识

还有《960全网最全Android开发笔记》、《379页Android开发面试宝典》、《507页Android开发相关源码解析》等等学习资源。

相信它会给大家带来很多收获:

这里也分享给广大面试同胞们,希望每位程序猿们都能成为最好的自己~

以上内容均放在了开源项目:github 中已收录,里面包含不同方向的自学Android路线、面试题集合/面经、及系列技术文章等,资源持续更新中...

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

相关文章

网友评论

    本文标题:Android避坑指南:分享几个装13的知识点,那些隐藏在“编译

    本文链接:https://www.haomeiwen.com/subject/hmtruktx.html