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

简介:

原文链接译文链接,原文作者: 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("%08X", m.getModifiers()) + " " + m.getName());
21         }
22         System.out.println("--------------------------");
23         for (Method m : A.class.getMethods()) {
24             System.out.println(String.format("%08X", 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("%08X", c.getModifiers()) + " " + c.getName());
29         }
30     }
31 }

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


01 2
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("%08X", 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方法”的说法。 

目录
相关文章
|
16天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
57 4
|
27天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
45 17
|
21天前
|
Java 测试技术 Maven
Java一分钟之-PowerMock:静态方法与私有方法测试
通过本文的详细介绍,您可以使用PowerMock轻松地测试Java代码中的静态方法和私有方法。PowerMock通过扩展Mockito,提供了强大的功能,帮助开发者在复杂的测试场景中保持高效和准确的单元测试。希望本文对您的Java单元测试有所帮助。
59 2
|
1月前
|
算法 Java Linux
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
这篇文章介绍了如何使用Java的Graphics2D类在图片上合成另一个照片,并将照片切割成圆形头像的方法。
49 1
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
|
29天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
1月前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
29天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
17 2
|
29天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
17 1
|
29天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
29天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
27 1
下一篇
无影云桌面