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

目录
相关文章
|
10天前
|
算法 Java Linux
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
这篇文章介绍了如何使用Java的Graphics2D类在图片上合成另一个照片,并将照片切割成圆形头像的方法。
22 1
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
|
4天前
|
Java Apache Maven
Java将word文档转换成pdf文件的方法?
【10月更文挑战第13天】Java将word文档转换成pdf文件的方法?
15 1
|
8天前
|
Java 编译器
Java“返回类型为 void 的方法不能返回一个值”解决
在 Java 中,如果一个方法的返回类型被声明为 void,那么该方法不应该包含返回值的语句。如果尝试从这样的方法中返回一个值,编译器将报错。解决办法是移除返回值语句或更改方法的返回类型。
|
1月前
|
Java
Java——方法的引用
方法引用允许将已有方法作为函数式接口的实现。使用“::”符号,需具备函数式接口,被引用的方法须存在且参数和返回值需与抽象方法一致。其分类包括:静态方法引用(类::方法名)、成员方法引用(对象::方法名、this::方法名、super::方法名)和构造方法引用(类名::new)。方法引用提高了代码的简洁性和可读性,减少了样板代码。
37 13
Java——方法的引用
|
8天前
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
17 2
|
8天前
|
Java
让星星⭐月亮告诉你,jdk1.8 Java函数式编程示例:Lambda函数/方法引用/4种内建函数式接口(功能性-/消费型/供给型/断言型)
本示例展示了Java中函数式接口的使用,包括自定义和内置的函数式接口。通过方法引用,实现对字符串操作如转换大写、数值转换等,并演示了Function、Consumer、Supplier及Predicate四种主要内置函数式接口的应用。
14 1
|
8天前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
9 1
|
13天前
|
Java 编译器
在Java中,关于final、static关键字与方法的重写和继承【易错点】
在Java中,关于final、static关键字与方法的重写和继承【易错点】
19 5
|
1月前
|
Java
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
本文介绍了Java中抽象类和抽象方法的使用,以及ArrayList的基本操作,包括添加、获取、删除元素和判断列表是否为空。
21 2
java基础(12)抽象类以及抽象方法abstract以及ArrayList对象使用
|
10天前
|
存储 算法 Java
java制作海报六:Graphics2D的RenderingHints方法参数详解,包括解决文字不清晰,抗锯齿问题
这篇文章是关于如何在Java中使用Graphics2D的RenderingHints方法来提高海报制作的图像质量和文字清晰度,包括抗锯齿和解决文字不清晰问题的技术详解。
17 0
java制作海报六:Graphics2D的RenderingHints方法参数详解,包括解决文字不清晰,抗锯齿问题