前言
在這篇文章 54 關於BeanUtils.copyProperties複製不生效 的問題思考的時候, 我曾經想過一個問題, 就是 MutableEntity 的 attr 的 properties 的可讀可寫, 是否和 MutableEntity 的方法聲明的順序有關係呢 ? (當然後面再仔細一看顯然是和方法的順序沒得關係, 因為是先讀取的getter, 然後再以getter為基準查詢setter)
然後 我看了一下 對應的獲取方法列表的地方, 發現使用的是 java.lang.Class.getMethods 來獲取方法列表
然後在 getMethods 的註釋如下
/**
* Returns an array containing {@code Method} objects reflecting all
* the public <em>member</em> methods of the class or interface represented
* by this {@code Class} object, including those declared by the class
* or interface and those inherited from superclasses and
* superinterfaces. Array classes return all the (public) member methods
* inherited from the {@code Object} class. The elements in the array
* returned are not sorted and are not in any particular order. This
* method returns an array of length 0 if this {@code Class} object
* represents a class or interface that has no public member methods, or if
* this {@code Class} object represents a primitive type or void.
*
* <p> The class initialization method {@code <clinit>} is not
* included in the returned array. If the class declares multiple public
* member methods with the same parameter types, they are all included in
* the returned array.
*
* <p> See <em>The Java Language Specification</em>, sections 8.2 and 8.4.
*
* @return the array of {@code Method} objects representing the
* public methods of this class
* @exception SecurityException
* If a security manager, <i>s</i>, is present and any of the
* following conditions is met:
*
* <ul>
*
* <li> invocation of
* {@link SecurityManager#checkMemberAccess
* s.checkMemberAccess(this, Member.PUBLIC)} denies
* access to the methods within this class
*
* <li> the caller's class loader is not the same as or an
* ancestor of the class loader for the current class and
* invocation of {@link SecurityManager#checkPackageAccess
* s.checkPackageAccess()} denies access to the package
* of this class
*
* </ul>
*
* @since JDK1.1
*/
@CallerSensitive
public Method[] getMethods() throws SecurityException {
// be very careful not to change the stack depth of this
// checkMemberAccess call for security reasons
// see java.lang.SecurityManager.checkMemberAccess
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
return copyMethods(privateGetPublicMethods());
}
方法描述上面有這麼一句 : The elements in the array returned are not sorted and are not in any particular order.
雖然説註釋説的是沒有特別的順序, 那麼在我們通常使用的 java編譯器 和 java虛擬機 中, 會不會有什麼規則呢 ?
我們這裏以 jdk1.7.40 為例來進行探討, 一下代碼截圖(測試代碼, javac, javap) 基於 jdk1.7.40, hotspot 部分代碼截圖 基於 jdk9
1. Class.getMethods 的實現
從上面的 Class.getMethos 的實現可以看到, 就是調用了 privateGetPublicMethods 然後做了一份拷貝
Class. privateGetPublicMethods 的實現如上圖, 其實就是拿 自己的public方法, 拿基類, 實現的接口的 public 方法(遞歸), 然後後面做了一些 額外的操作就返回了
Class.privateGetDeclaredMethods(boolean) 的方法實現如下
getDeclaredMethods0 是一個 native 方法
查看 Class 中註冊的本地方法信息, 這個 native 方法對應的實現是 JVM_GetClassDeclaredMethods
JVM_GetClassDeclaredMethods 的實現如下
獲取當前類對應的 instanceKlass 的方法列表, 然後採集目標方法, 入參有 want_constructor, publicOnly 進行過濾
然後截圖未截取完畢的部分就是根據方法索引獲取方法封裝 java.lang.reflect.Method 返回
那麼這裏看來 返回的方法列表其實就依賴於 instanceKlass.methods 的順序了 ?
2. instanceKlass.method的順序 ?
那麼 instanceKlass.method 又是由什麼決定的呢 ??
我們來到 創建並初始化 instanceKlass 的地方
原來這個 instanceKlass.methods 是來自於解析好的 classFileParser, 那麼我們看下 classFileParser 解析方法的這個部分呢
我們發現 這個是 methods 是直接讀取的 字節碼中按照順序讀取的方法, 那也就是説 instanceKlass.methods 的順序其實就是 字節碼中寫入的方法的順序呢 ??
3. class 中方法列表的寫出
一下調試代碼基於 : 52 一些關於java編譯器的問題 裏面的 Test11InitAndClinit
在 javac 寫出方法的時候, 是如下地方寫出的
可以看到這裏寫出是按照 methods 來寫出的, 然後 methods 依賴於 傳入的 Scope$Entry (c.members().elems)
methods 的是 c.members().elems (基於sibling的單鏈表) 的逆序的排列
4. c.members().elems 依賴什麼 ?
那麼 c.members().elems 又是怎麼被組織的呢 ?
在 Scope.enter 裏面打一個條件斷點, 我們發現我們這裏的 c.members().elems 整理方式如下
這裏是在 memerEnter 階段, 遍歷了一下 tree.defs (Test11InitAndClinit的成員列表)
然後如果是變量定義, 方法定義 需要註冊到 tree.sym.members_field裏面去
這裏的幾個變量, 方法的 enter 如下圖(依次序 : x, <init>(), <init>(int), main([LString;) )
然後這裏的 scope 又是如何和 tree.sym.members_field 關聯起來的呢 ?
然後 如果你足夠仔細的話, 你會發現一個問題, 圖1 裏面不是還有一個 <clinit> 麼 ?, 怎麼以上的截圖裏面沒有呢 ?
在 Gen.genClass 裏面有一個 normalizeDefs 會重組構造方法 和 類初始化方法
相關擴展可以參見這篇文章 : 52 一些關於java編譯器的問題
從上面 Scope.enter 的代碼可以發現, 這個單鏈表的添加元素的方式是 新來的節點作為頭結點, 鏈接原有的鏈表
所以在 c.members().elems (基於sibling的單鏈表) 裏面的順序是 : <clinit>(), main([LString;), <init>(int), <init>(), x
並且這裏 得到的答案是 c.members().elems 依賴於 tree.defs
5. tree.defs 依賴什麼 ?
tree.defs 又是怎麼來的呢 ?
從上圖可以看到, 這是解析 java代碼 中的解析 類主體的部分實現, 可以看到 tree.defs 依賴的是源代碼中的編碼順序
所以到這裏 為止 我們可以得出的結論是(可能會被打臉, 打臉後面再來修正) : 字節碼中的方法的順序是, java代碼 中方法的順序, 有一部分 方法比如這裏的 <clinit> 是加在最後的
比如 編譯器自動添加的 無參構造方法是加在 : 所有的定義的最前面
其他的特殊的情況 可能需要根據實際情況討論吧
6. javap 讀取 class 的方法的展示
可以看到 依次序讀的, 那就是和 寫的時候的順序一致
7. 理論上的東西我們上面提及了這麼多, 那麼實際泡一下?
測試代碼如下
package com.hx.test03;
import java.lang.reflect.Method;
/**
* MethodOrder
*
* @author Jerry.X.He
* @version 1.0
* @date 2020-02-28 12:07
*/
public class Test26MethodOrder {
public Test26MethodOrder() {
}
public Test26MethodOrder(String str) {
}
protected Test26MethodOrder(int x) {
}
Test26MethodOrder(long x) {
}
private Test26MethodOrder(double x) {
}
// Test26MethodOrder
public static void main(String[] args) {
// Method[] methods = Test26MethodOrder.class.getMethods();
Method[] declaredMethods = Test26MethodOrder.class.getDeclaredMethods();
// Constructor[] constructors = Test26MethodOrder.class.getConstructors();
// Constructor[] declaredConstructors = Test26MethodOrder.class.getDeclaredConstructors();
for(Method method : declaredMethods) {
System.out.println(method.getName());
}
System.out.println(" ending ... ");
}
// funcN
public void func001() {
}
// funcN
public static void func002() {
}
// funcN
protected void func003() {
}
// funcN
protected static void func004() {
}
// funcN
void func005() {
}
// funcN
static void func006() {
}
// funcN
private void func007() {
}
// funcN
private static void func008() {
}
}
上面沒有使用 Class.getMethods, 使用的是 Class.getDeclaredMethods, 是因為後者效果更加明顯, 而且 重要的調用部分 和前者幾乎一致
測試結果截圖如下
wtf ??, 怎麼測試結果 和我們上面分析 一點關係都沒有.. !!
哈哈 我其實也是走了一通上面的理論之後, 看到這個結果 有點詫異,, 所以 實踐是檢驗整理的唯一標準 啊, 沒有我們看起來那麼 "簡單"
8. classFileParser.post_process_parsed_stream方法的排序
在晚上搜索了一下, 看到了這樣一篇文章 OpenJDK中的JVM_GetClassDeclaredMethods方法返回順序 , 看了一下, 跟了一下 好像確實是忽略了這個呢(嘿嘿 加上這個之後, 不知道還沒有其他的影響因素, 先這樣吧, 後面發現再來附上)
一下內容截取了一下 方法是如何排序的, 以及相關的棧幀信息
可以發現 在 classFileParser 解析了class之後的 post_process 階段對方法進行了一次 排序, 排序的規則是 給定的方法的 name (類型為Symbol) 的地址比較大小
解析常量池的時候, 會創建所有的 字符串 對應的 Symbol(我們這裏的方法名字對應的字符串 也在其中)
分配 Symbol 採用 new 分配 分配的地址就和分配策略有關係了, 這個 我們就不深究了(水平有限, 也深究不下去了)
以上 這個問題就到這裏, 後面有了新的發現 再附上來
以上也凸顯了一些問題, 只看理論 和 實際調試起來 是兩碼事情, 如果是我能先跑兩次程序, 也就不用去跟蹤 javac 這這一方的代碼了, 也會少走一些彎路
完
參考
OpenJDK中的JVM_GetClassDeclaredMethods方法返回順序
https://www.jianshu.com/p/16b97baae1ca
jls 8.2. Class Members
https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.2
jvms Chapter 4. The class File Format
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.1