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方法”的说法。 

时间: 2025-01-20 07:13:18

Java那些不为人知的特殊方法的相关文章

java中关于dismiss方法的使用

问题描述 java中关于dismiss方法的使用 myDialog.dismiss( )比如这条语句中是关闭一个对话框的意思吗dismiss还有哪些方面的应用 解决方案 看下这个函数的源码上面的注释信息,jdk源码上的英文注释就是很好的参考文档的. 解决方案二: 这和java语言没有关系,这只是dialog对象定义的方法罢了.你也可以写一个类,定义一个叫dismiss的方法. 在英文字面看来,dismiss就是消失的意思. 解决方案三: java中waitnotifynotifyAll的使用方法

java-关于Java的默认运行方法的机制问题。

问题描述 关于Java的默认运行方法的机制问题. timer.schedule(new MyTask()01000);这一句代码中,new了一个MyTask()的类,怎么就自动调用了里面的run()函数?新手勿喷. 解决方案 timer在到了那个指定时刻,它会反过来调用run,这是约定好的. 解决方案二: 这是类似于回调的机制吗?

java中this作为方法名的时候的问题,不知道我把它看成方法名正步正确

问题描述 java中this作为方法名的时候的问题,不知道我把它看成方法名正步正确 如下代码所示, public MyView(Context context) { this(context null); } //this在这里是方法吗,this是一个方法名吗? 解决方案 this用来调用你这个类中定义的一个构造方法 解决方案二: this不是方法名,而是Java中对当前对象的引用.例如当前对象的引用用this,父类对象的引用用super 解决方案三: 一个类中定义两个构造函数,在一个构造函数中

java的抽象类和方法

在我们所有乐器(Instrument)例子中,基础类Instrument内的方法都肯定是"伪"方法.若去调用这些方法,就会出现错误.那是由于Instrument的意图是为从它衍生出去的所有类都创建一个通用接口. 之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示.它为我们建立了一种基本形式,使我们能定义在所有衍生类里"通用"的一些东西.为阐述这个观念,另一个方法是把Instrument称为"抽象基础类"(简称"抽象

Java 库的建立方法及其实例

作者 ariesram 电子邮件地址 ariesram@linuxaid.com.cn, 或 ariesram@may10.ca 本文及本人所有文章均收集在bambi.may10.ca/~ariesram/articles/中. 本文授权给www.linuxaid.com.cn. 正文: 任何一种面向对象语言都有它的库.任何一种面向对象的语言也都离不开库的支持.用我们熟悉的 面向对象语言为例子,C++有STL,Java有API函数,具体到开发工具,Visual C++提供了MFC, Borlan

创建Java ME Math.pow()方法

使用 Java 开发移动设备应用程序时,可能需要用到特定 Java VM 所没有的数学方法.本文将专门解决 Java ME 没有"幂"方法 Math.pow() 的问题.我们将演示使用三种不同的方法开发同一个 ME 应用程序,并从中选出最佳的编程解决方案. 要讨论此问题,我们先考察整数和分数幂参数,将我们的分析限于正实数.我们将演示求整数问题和小数问题的解集相对而言比较容易(而 不考虑指数的符号).在大多数情况下,我们将使用示例问题 n = 82/3,其中我们会求出 n 的良好估计或实

JAVA基础培训(10),方法的Overload介绍

今天在项目里做事,中午休息时间,补上这个教程吧.这次我们看看Overload 的内容 . 测试代码 package lession10; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** * 老紫竹JAVA基础培训(10),方法的Overload介绍.<br> * 匹配方式为最特殊匹配,或者叫最准确匹配<br> * 如果发现多个都有相同的匹配度,则编译报错. * *

如何在Java中避免equals方法的隐藏陷阱

译者注 :你可能会觉得Java很简单,Object的equals实现也会非常简单,但是事实并不是你想象的这样,耐心的读完本文,你会发现你对Java了解的是如此的少.如果这篇文章是一份Java程序员的入职笔试,那么不知道有多少人会掉落到这样的陷阱中. 摘要 本文描述重载equals方法的技术,这种技术即使是具现类的子类增加了字段也能保证equal语义的正确性. 在<Effective Java>的第8项中,Josh Bloch描述了当继承类作为面向对象语言中的等价关系的基础问题,要保证派生类的e

Java截取字符串的方法

  本文实例讲述了Java截取字符串的方法.分享给大家供大家参考.具体实现方法如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main(String args[]) { //以该字符第一次出现,开始截取 //String str="abc.def"; //String str="abc.def.sdfsdf.fsdfd.ddddd.ggggg.ttttt"; //String str1=str.subst