Java那些不为人知的特殊方法

    xiaoxiao2024-05-08  108

    原文链接,译文链接,原文作者: Peter Verhas,译者:有孚,本文最早发表于deepinmind

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

    immutator的一些用户遇到过这样的问题。他们发现,使用immutator(这个项目探索了Java的一些不为人知的细节)生成的Java代码使用volatile了作为方法的关键字,而这样的代码没法通过编译。结果就是这根本没法用。

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

    可见性

    当你创建一个嵌套类的时候,它的私有变量和方法对上层的类是可见的。这个在不可变嵌套式Builder模式中用到了。这是Java语言规范里已经定义好的一个行为。

    01 package synthetic; 02 03 public class SyntheticMethodTest1 { 04 private A aObj = new A(); 05 06 public class A { 07 private int i; 08 } 09 10 private class B { 11 private int i = aObj.i; 12 } 13 14 public static void main(String[] args) { 15 SyntheticMethodTest1 me = new SyntheticMethodTest1(); 16 me.aObj.i = 1; 17 B bObj = me.new B(); 18 System.out.println(bObj.i); 19 } 20}

    JVM是如何处理这个的?它可不知道什么是内部类或者嵌套类的。JVM对所有的类都一视同仁,它都认为是顶级类。所有类都会被编译成顶级类,而那些内部类编译完后会生成…$… class的类文件。

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

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

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

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

    01 package synthetic; 02 03 import java.lang.reflect.Constructor; 04 import java.lang.reflect.Method; 05 06 public class SyntheticMethodTest2 { 07 08 public static class A { 09 private A(){} 10 private int x; 11 private void x(){}; 12 } 13 14 public static void main(String[] args) { 15 A a = new A(); 16 a.x = 2; 17 a.x(); 18 System.out.println(a.x); 19 for (Method m : A.class.getDeclaredMethods()) { 20 System.out.println(String.format("X", m.getModifiers()) + " " + m.getName()); 21 } 22 System.out.println("--------------------------"); 23 for (Method m : A.class.getMethods()) { 24 System.out.println(String.format("X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName()); 25 } 26 System.out.println("--------------------------"); 27 for( Constructor<?> c : A.class.getDeclaredConstructors() ){ 28 System.out.println(String.format("X", c.getModifiers()) + " " + c.getName()); 29 } 30 } 31}

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

    012 02 00001008 access$1 03 00001008 access$2 04 00001008 access$3 05 00000002 x 06-------------------------- 07 00000111 void wait 08 00000011 void wait 09 00000011 void wait 10 00000001 boolean equals 11 00000001 String toString 12 00000101 int hashCode 13 00000111 Class getClass 14 00000111 void notify 15 00000111 void notifyAll 16-------------------------- 17 00000002 synthetic.SyntheticMethodTest2$A 18 00001000 synthetic.SyntheticMethodTest2$A

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

    看一下java.lang.reflect.Modifier里面定义的常量,可以明白这些十六进制的数字代表的是什么:

    1 00001008 SYNTHETIC|STATIC 2 00000002 PRIVATE 3 00000111 NATIVE|FINAL|PUBLIC 4 00000011 FINAL|PUBLIC 5 00000001 PUBLIC 6 00001000 SYNTHETIC

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

    泛型和继承

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

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

    想出现volatile方法的话,写个简单的程序就行了:

    01 package synthetic; 02 03 import java.lang.reflect.Method; 04 import java.util.LinkedList; 05 06 public class SyntheticMethodTest3 { 07 08 public static class MyLink extends LinkedList { 09 @Override 10 public String get(int i) { 11 return ""; 12 } 13 } 14 15 public static void main(String[] args) { 16 17 for (Method m : MyLink.class.getDeclaredMethods()) { 18 System.out.println(String.format("X", m.getModifiers()) + " " + m.getReturnType().getSimpleName() + " " + m.getName()); 19 } 20 } 21}

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

    输出的结果是这样的:

    1 00000001 String get 2 00001041 Object get

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

    01 public java.lang.String get(int); 02 Code: 03 Stack=1, Locals=2, Args_size=2 04 0: ldc #2; //String 05 2: areturn 06 LineNumberTable: 07 line 12: 0 08 09 public java.lang.Object get(int); 10 Code: 11 Stack=2, Locals=2, Args_size=2 12 0: aload_0 13 1: iload_1 14 2: invokevirtual #3; //Method get:(I)Ljava/lang/String; 15 5: areturn

    有趣的是,两个方法的签名是一模一样的,只有返回类型不同。这个在JVM里面是合法的,不过在Java语言里可不允许。bridge的这个方法不干别的,就只是去调用了下原始的那个方法。

    为什么我们需要这个synthetic方法呢,谁会调用它?比如现在有段代码想要调用一个非MyLink类型变量的get(int)方法:

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

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

    01 package synthetic; 02 03 import java.util.LinkedList; 04 import java.util.List; 05 06 public class SyntheticMethodTest4 { 07 08 public static class MyLink extends LinkedList { 09 @Override 10 public boolean add(String s) { 11 return true; 12 } 13 } 14 15 public static void main(String[] args) { 16 List a = new MyLink(); 17 a.add(""); 18 a.add(13); 19 } 20}

    我们会发现这个bridge方法

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

    它不仅调用了原始的方法,它还进行了类型检查。这个检查是在运行时进行的,并不是由JVM自己来完成。正如你所想,在18行的地方会抛出一个异常:

    查看源代码

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

    下次如果你在面试中被问到volatile方法的话,说不定面试官知道的还没你多:-)

    译者注:其实作者说到最后也没讲完到底什么是volatile方法,其实volatile方法如篇首所说,是不存在的,所谓的volatile方法就是指bridge方法。由于在修饰符中volatile和bridge是同一个值,在之前版本的javap中存在一个BUG,一个bridge方法在反编译后会显示成volatile,所以存在”volatile方法”的说法。

    文章转自 并发编程网-ifeve.com

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)