关于java:一些Java中不为人知的特殊方法学完后面试官可能都没你知道的多

53次阅读

共计 4982 个字符,预计需要花费 13 分钟才能阅读完成。

如果你用过反射并且执行过 getDeclaredMethods 办法的话,你可能会感到很吃惊。你会发现呈现了很多源代码里没有的办法。如果你看一下这些办法的修饰符的话,可能会发现外面有些办法是 volatile 的。顺便说一句,如果在 Java 面试里问到“什么是 volatile 办法?”,你可能会吓出一身冷汗。正确的答案是没有 volatile 办法。但同时,getDeclaredMethods() 或者 getMethods() 返回的这些办法,Modifier.isVolatile(method.getModifiers()) 的后果却是 true。

一些用户遇到过这样的问题。他们发现,应用 immutator(这个我的项目摸索了 Java 的一些鲜为人知的细节)生成的 Java 代码应用 volatile 了作为办法的关键字,而这样的代码没法通过编译。后果就是这基本没法用。

这是怎么回事?syntethic 和 bridge 办法又是什么?

可见性

当你创立一个嵌套类的时候,它的公有变量和办法对下层的类是可见的。这个在不可变嵌套式 Builder 模式中用到了。这是 Java 语言标准里曾经定义好的一个行为。

package synthetic;

public class SyntheticMethodTest1 {private A aObj = new A();

    public class A {private int i;}

    private class B {private int i = aObj.i;}

    public static void main(String[] args) {SyntheticMethodTest1 me = new SyntheticMethodTest1();
        me.aObj.i = 1;
        B bObj = me.new B();
        System.out.println(bObj.i);
    }
}

JVM 是如何解决这个的?它可不晓得什么是外部类或者嵌套类的。JVM 对所有的类都厚此薄彼,它都认为是顶级类。所有类都会被编译成顶级类,而那些外部类编译完后会生成…$… class 的类文件。

$ ls -Fart
../                         SyntheticMethodTest2$A.class  MyClass.java  SyntheticMethodTest4.java  SyntheticMethodTest2.java
SyntheticMethodTest2.class  SyntheticMethodTest3.java     ./            MyClassSon.java            SyntheticMethodTest1.java

如果你创立一个外部类的话,它会被彻底编译成一个顶级类。

那这些公有变量又是如何被外部类拜访的呢?如果它们是个顶级类的公有变量(它们确实也是),那为什么别的类还能间接拜访这些变量?

javac 是这样解决这个问题的,对于任何 private 的字段,办法或者构造函数,如果它们也被其它顶层类所应用,就会生成一个 synthetic 办法。这些 synthetic 办法是用来拜访最后的公有变量 / 办法 / 构造函数的。这些办法的生成也很智能:只有的确被外部类用到了,才会生成这样的办法。

package synthetic;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class SyntheticMethodTest2 {

    public static class A {private A(){}
        private int x;
        private void x(){};
    }

    public static void main(String[] args) {A a = new A();
        a.x = 2;
        a.x();
        System.out.println(a.x);
        for (Method m : A.class.getDeclaredMethods()) {System.out.println(String.format("%08X", m.getModifiers()) + " " + m.getName());
        }
        System.out.println("--------------------------");
        for (Method m : A.class.getMethods()) {System.out.println(String.format("%08X", m.getModifiers()) + "" + m.getReturnType().getSimpleName() +" " + m.getName());
        }
        System.out.println("--------------------------");
        for(Constructor<?> c : A.class.getDeclaredConstructors() ){System.out.println(String.format("%08X", c.getModifiers()) + " " + c.getName());
        }
    }
}

这些生成的办法的名字取决于具体的实现,最初叫什么也不好说。我只能说在我运行的这个平台上,上述程序的输入是这样的:

2
00001008 access$1
00001008 access$2
00001008 access$3
00000002 x
--------------------------
00000111 void wait
00000011 void wait
00000011 void wait
00000001 boolean equals
00000001 String toString
00000101 int hashCode
00000111 Class getClass
00000111 void notify
00000111 void notifyAll
--------------------------
00000002 synthetic.SyntheticMethodTest2$A
00001000 synthetic.SyntheticMethodTest2$A

在下面这个程序中,咱们给变量 x 赋值,而后又调用了一个同名的办法。这会触发编译器生成对应的 synthetic 办法。你会看到它生成了三个办法,应该是 x 变量的 setter 和 getter 办法,以及 x() 办法对应的一个 synthetic 办法。这些办法并不存在于 getMethods 办法里返回的列表中,因为它们是 synthetic 办法,是不能间接被调用的。从这点来看,它们和公有办法差不多。

看一下 java.lang.reflect.Modifier 外面定义的常量,能够明确这些十六进制的数字代表的是什么:

00001008 SYNTHETIC|STATIC
00000002 PRIVATE
00000111 NATIVE|FINAL|PUBLIC
00000011 FINAL|PUBLIC
00000001 PUBLIC
00001000 SYNTHETIC

列表中有两个是构造方法。还有一个公有办法以及一个 synthetic 办法。存在这个公有办法是因为咱们的确定义了它。而 synthetic 办法的呈现是因为咱们从外部类调用了它外部的公有成员。到目前为止,还没有呈现过 bridge 办法。

泛型和继承

到目前为止,看起来还不错。不过咱们还没有看到”volatile”办法。

看一下 java.lang.reflect.Modifier 的源码你会发现 0x00000040 这个常量被定义了两次。一次是定义成 VOLATILE,还有一次是 BRIDGE(后者是包外部公有的,并不对外开放)。

想呈现 volatile 办法的话,写个简略的程序就行了:

package synthetic;

import java.lang.reflect.Method;
import java.util.LinkedList;

public class SyntheticMethodTest3 {

    public static class MyLink extends LinkedList {
        @Override
        public String get(int i) {return "";}
    }

    public static void main(String[] args) {for (Method m : MyLink.class.getDeclaredMethods()) {System.out.println(String.format("%08X", m.getModifiers()) + "" + m.getReturnType().getSimpleName() +" " + m.getName());
        }
    }
}

这个链表有一个返回 String 的 get(int) 办法。先别探讨代码整不整洁的问题了。这只是段示例代码而已。整洁的代码当然也会呈现同样的问题,不过越简单的代码越难定位问题罢了。

输入的后果是这样的:

00000001 String get
00001041 Object get

这里有两个 get 办法。一个是代码里的那个,另外一个是 synthetic 和 bridge 办法。用 javap 反编译后会是这样的:

public java.lang.String get(int);
  Code:
   Stack=1, Locals=2, Args_size=2
   0:   ldc     #2; //String
   2:   areturn
  LineNumberTable:
   line 12: 0

public java.lang.Object get(int);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   iload_1
   2:   invokevirtual   #3; //Method get:(I)Ljava/lang/String;
   5:   areturn

乏味的是,两个办法的签名是截然不同的,只有返回类型不同。这个在 JVM 外面是非法的,不过在 Java 语言里可不容许。bridge 的这个办法不干别的,就只是去调用了下原始的那个办法。

为什么咱们须要这个 synthetic 办法呢,谁会调用它?比方当初有段代码想要调用一个非 MyLink 类型变量的 get(int) 办法:

List<?> a = new MyLink();
        Object z = a.get(0);

它不能调用返回 String 的办法,因为 List 里没这样的办法。为了解释的更分明一点,咱们重写下 add 办法而不是 get 办法:

package synthetic;

import java.util.LinkedList;
import java.util.List;

public class SyntheticMethodTest4 {

    public static class MyLink extends LinkedList {
        @Override
        public boolean add(String s) {return true;}
    }

    public static void main(String[] args) {List a = new MyLink();
        a.add("");
        a.add(13);
    }
}

咱们会发现这个 bridge 办法

public boolean add(java.lang.Object);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   checkcast       #2; //class java/lang/String
   5:   invokevirtual   #3; //Method add:(Ljava/lang/String;)Z
   8:   ireturn

它不仅调用了原始的办法,它还进行了类型查看。这个查看是在运行时进行的,并不是由 JVM 本人来实现。正如你所想,在 18 行的中央会抛出一个异样:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at synthetic.SyntheticMethodTest4$MyLink.add(SyntheticMethodTest4.java:1)
    at synthetic.SyntheticMethodTest4.main(SyntheticMethodTest4.java:18)

下次如果你在面试中被问到 volatile 办法的话,说不定面试官晓得的还没你多:-) 私信”学习“有惊喜

正文完
 0