程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java反射

Java反射

編輯:JAVA綜合教程

Java反射


Java中的反射庫(reflection library)中提供了一個非常豐富且精心設計的工具集,以便編寫能夠動態操縱Java代碼的程序。

能夠分析類能力的程序稱為反射(reflective)。反射的功能很強大,下面是反射的用途:

在運行中分析類的能力;在運行中查看對象,例如編寫一個toString方法供所有類使用;實現通用的數組操作代碼;利用Method對象,這個對象很像C++中的函數指針;

1 Class類的使用

Java是面向對象的語言,在Java中,所有的東西都是類(除了靜態方法和基本類型)。那麼,類是不是一個對象呢?

在Java程序運行期間,Java運行時系統時鐘為所有的對象維護一個被稱為運行時的類型標志。這個信息跟蹤著每個對象的所屬類。虛擬機利用運行時類型信息選擇相應的方法執行。

Class類保存了Java類的這些信息。官網中,對這個類型稱為類的類類型,也就是說一個類的對象。比如,有一個類Student,可以使用下面的代碼創建一個實例:

Stuent stu=new Studnet();
即stu是類Student的一個實例。那既然類也是對象,那麼Student是什麼的實例呢?Student是類Class的一個實例。

可以通過如下三種方法獲得一個類的類類型。

(1)getClass方法

如果有一個類的實例,那麼可以通過這個實例的getClass方法獲得這個類的類類型:

Class cl=stu.getClass();
(2)靜態方法forName

forName是Class的一個靜態方法,如果沒有一個類的實例,但是知道這個類的名字,可以使用這個方法獲得這個類的類類型:

Class cl=Class.forName("Student");
如果類名保存在字符串中,並可在運行中改變,就可以使用這個方法。當然,這個方法只有在參數是類名或接口名的時候才能夠執行,否則forName方法將拋出一個異常。因此,使用這個方法時,應該處理這個異常。

(3).class

第三個獲得類類型 的方法很簡單。如果T是任意的Java類型,T.class將代表匹配的類對象。比如:

Class cl1=Student.class;
Class cl2=int.class
Class cl3=Double[].class

注意,一個Class對象實際上表示一個類型,而這個類型不一定是一個類。比如,int不是類,但int.class是一個Class類型的對象。

虛擬機為每個類型管理一個Class對象。因此可以使用==運算符實現兩個類對象的比較:

if(stu.getClass()==Student.class)...
這將判斷為真。

Class中最常用的一個方法就是getName方法,這個方法將返回類的名字。

還可以通過類類型創建一個類的實例。比如:

Class cl=Student.class;
Student stu=(Student)cl.newInstance();

這就創建了一個Student實例。不過,newInstance方法使用的是默認的構造器(沒有參數的構造器)初始化新創建的對象。

使用forName和newInstance方法可以根據存儲在字符串中的類名創建一個實例:

String s="java.util.Date";
Object date=Class.forName(s).newInstance();

2 捕獲異常

在第一節中,Class類的forName方法可能會拋出一個異常,可以使用下面的代碼捕獲並處理異常:

try{
    String name=...;//get class name
    Class cl=Class.forName(name);//might throw exception
    do something with cl
}catch(Exception e){
    e.printStackTrace();
}

3 利用反射分析類的能力

下面簡要的介紹一下反射機制最重要的內容:檢查類的結構。

在java.lang.reflect包中有三個類Field、Method和Constructor分別描述類的域、方法和構造器。這三個類都有一個叫getName的方法,用來返回項目的名稱。Field類有一個getType方法,用來返回描述域所屬的Class對象。Method和Constructor類有能夠報告參數類型的方法,Method還有一個可以報告返回類型的方法。這三個類有一個getModifiers的方法,它將返回一個整數值,用不同的位開關描述public和static這樣的修飾符的使用狀況,還可以利用Modifier.toString方法將修飾符打印出來。

Class類的getFields、getMethods和getCostructors方法可以獲得類提供的public域、方法和構造器數組,其中包括超類的共有成員。Class類的getDeclatedFields、getDeclatedMethods和getDeclaredConstructors方法可以獲得類中聲明的全部域、方法和構造器,其中包括私有和受保護的成員,但不包括超類的成員。

Constructor類:

String getName():返回構造器的名字;int getModifiers():返回修飾符;int getParameterCount():返回構造器中參數的個數;Class[] getParameterTypes():返回參數的類型;

Field類:

String getName():返回域的名字;int getmodifiers():返回修飾符;ClassgetType():返回域的類類型;

Method類:

String getName():返回方法的名字;int getModifiers():返回修飾符;int getParameterCount():返回參數的個數;Class[] getParameterTypes():返回參數的類類型;ClassgetReturnType():返回返回類類型;

下面的代碼編寫了一個可以打印一個類的所有構造器、方法和域的相關信息的類:

package reflection;
import java.lang.reflect.*;
public class PrintClassInfo {
	private PrintClassInfo(){}
	public static void printClassInfo(String name){
		try{
			Class cl=Class.forName(name);
			Class supercl=cl.getSuperclass();
			String modifiers=Modifier.toString(cl.getModifiers());
			if(modifiers.length()>0)System.out.print(modifiers+" ");
			System.out.print("class "+name);
			if(supercl!=null&&supercl!=Object.class)System.out.print(" extends "+supercl.getName());
			System.out.print("\n{\nConstructors:\n");
			printConstructors(cl);
			System.out.println("Methods:");
			printMethods(cl);
			System.out.println("Fields:");
			printFields(cl);
			System.out.println("}");
		}catch(ClassNotFoundException e){
			e.printStackTrace();
		}
		System.exit(0);
	}
	public static void printConstructors(Class cl){
		Constructor[] cons=cl.getConstructors();
		for(Constructor c:cons){
			String name=c.getName();
			System.out.print("    ");
			String modifiers=Modifier.toString(c.getModifiers());
			if(modifiers.length()>0)System.out.print(modifiers+" ");
			System.out.print(name+"(");
			
			Class[] paramTypes=c.getParameterTypes();
			for(int i=0;i0)System.out.print(", ");
				System.out.print(paramTypes[i].getName());
			}
			System.out.println(");");
		}
	}
	public static void printMethods(Class cl){
		Method[] methods=cl.getDeclaredMethods();
		for(Method m:methods){
			Class retType=m.getReturnType();
			String name=m.getName();
			System.out.print("    ");
			String modifiers=Modifier.toString(m.getModifiers());
			if(modifiers.length()>0)System.out.print(modifiers+" ");
			System.out.print(retType.getName()+" "+name+"(");
			
			Class[] paramTypes=m.getParameterTypes();
			for(int i=0;i0)System.out.print(", ");
				System.out.print(paramTypes[i].getName());
			}
			System.out.println(");");
		}
	}
	public static void printFields(Class cl){
		Field[] fields=cl.getDeclaredFields();
		for(Field f:fields){
			Class type=f.getType();
			String name=f.getName();
			System.out.print("    ");
			String modifiers=Modifier.toString(f.getModifiers());
			if(modifiers.length()>0)System.out.print(modifiers+" ");
			System.out.println(type.getName()+" "+name+";");
		}
	}
}

這個類有一個靜態方法printClassInfo,用於打印類的基本信息,在方法的內部調用了printConstructors、printMethosds和printFields方法。測試代碼如下:
String name;
System.out.println("Enter the class name:");
Scanner scanner=new Scanner(System.in);
name=scanner.next();
PrintClassInfo.printClassInfo(name);
運行輸入java.lang.String,結果如下:

public final class java.lang.String
{
Constructors:
public java.lang.String([B, int, int);
public java.lang.String([B, java.nio.charset.Charset);
public java.lang.String([B, java.lang.String);
public java.lang.String([B, int, int, java.nio.charset.Charset);
public java.lang.String([B, int, int, java.lang.String);
public java.lang.String(java.lang.StringBuilder);
public java.lang.String(java.lang.StringBuffer);
public java.lang.String([B);
public java.lang.String([I, int, int);
public java.lang.String();
public java.lang.String([C);
public java.lang.String(java.lang.String);
public java.lang.String([C, int, int);
public java.lang.String([B, int);
public java.lang.String([B, int, int, int);
Methods:
public boolean equals(java.lang.Object);
public java.lang.String toString();
public int hashCode();
public int compareTo(java.lang.String);
public volatile int compareTo(java.lang.Object);
public int indexOf(java.lang.String, int);
public int indexOf(java.lang.String);
public int indexOf(int, int);
public int indexOf(int);
static int indexOf([C, int, int, [C, int, int, int);
static int indexOf([C, int, int, java.lang.String, int);
public static java.lang.String valueOf(int);
public static java.lang.String valueOf(long);
public static java.lang.String valueOf(float);
public static java.lang.String valueOf(boolean);
public static java.lang.String valueOf([C);
public static java.lang.String valueOf([C, int, int);
public static java.lang.String valueOf(java.lang.Object);
public static java.lang.String valueOf(char);
public static java.lang.String valueOf(double);
public char charAt(int);
private static void checkBounds([B, int, int);
public int codePointAt(int);
public int codePointBefore(int);
public int codePointCount(int, int);
public int compareToIgnoreCase(java.lang.String);
public java.lang.String concat(java.lang.String);
public boolean contains(java.lang.CharSequence);
public boolean contentEquals(java.lang.CharSequence);
public boolean contentEquals(java.lang.StringBuffer);
public static java.lang.String copyValueOf([C);
public static java.lang.String copyValueOf([C, int, int);
public boolean endsWith(java.lang.String);
public boolean equalsIgnoreCase(java.lang.String);
public static transient java.lang.String format(java.util.Locale, java.lang.String, [Ljava.lang.Object;);
public static transient java.lang.String format(java.lang.String, [Ljava.lang.Object;);
public void getBytes(int, int, [B, int);
public [B getBytes(java.nio.charset.Charset);
public [B getBytes(java.lang.String);
public [B getBytes();
public void getChars(int, int, [C, int);
void getChars([C, int);
private int indexOfSupplementary(int, int);
public native java.lang.String intern();
public boolean isEmpty();
public static transient java.lang.String join(java.lang.CharSequence, [Ljava.lang.CharSequence;);
public static java.lang.String join(java.lang.CharSequence, java.lang.Iterable);
public int lastIndexOf(int);
public int lastIndexOf(java.lang.String);
static int lastIndexOf([C, int, int, java.lang.String, int);
public int lastIndexOf(java.lang.String, int);
public int lastIndexOf(int, int);
static int lastIndexOf([C, int, int, [C, int, int, int);
private int lastIndexOfSupplementary(int, int);
public int length();
public boolean matches(java.lang.String);
private boolean nonSyncContentEquals(java.lang.AbstractStringBuilder);
public int offsetByCodePoints(int, int);
public boolean regionMatches(int, java.lang.String, int, int);
public boolean regionMatches(boolean, int, java.lang.String, int, int);
public java.lang.String replace(char, char);
public java.lang.String replace(java.lang.CharSequence, java.lang.CharSequence);
public java.lang.String replaceAll(java.lang.String, java.lang.String);
public java.lang.String replaceFirst(java.lang.String, java.lang.String);
public [Ljava.lang.String; split(java.lang.String);
public [Ljava.lang.String; split(java.lang.String, int);
public boolean startsWith(java.lang.String, int);
public boolean startsWith(java.lang.String);
public java.lang.CharSequence subSequence(int, int);
public java.lang.String substring(int);
public java.lang.String substring(int, int);
public [C toCharArray();
public java.lang.String toLowerCase(java.util.Locale);
public java.lang.String toLowerCase();
public java.lang.String toUpperCase();
public java.lang.String toUpperCase(java.util.Locale);
public java.lang.String trim();
Fields:
private final [C value;
private int hash;
private static final long serialVersionUID;
private static final [Ljava.io.ObjectStreamField; serialPersistentFields;
public static final java.util.Comparator CASE_INSENSITIVE_ORDER;
}

值得注意的是,這個程序可以分析Java解釋器能夠加載的任何類,而不僅僅是編譯程序時可以使用的類。

4 動態加載類

類的加載有兩種方式,一個是在編譯時加載的靜態加載類,另一個是在運行時加載的動態加載類。當我們使用new來創建一個對象時,使用的是靜態加載類,這個時候必須保證類已經實現。而Class的forName方法不但可以獲得一個類的類類型,還是一個動態加載類的方法,可以越過編譯,在運行時加載一個類。

動態加載有什麼好處麼?考慮一個場景,比如自己要實現一個辦公軟件Office,裡面有各種組件Word和Excel等。有一個Office工具可以啟動各種組件,由於現階段只有兩個組件Word和Excel,那麼Office可以這樣編寫:

 

class Office
{
	public static void main(String[] args)
	{
		if("Word".equals(args[0]))
		{
			Word w=new Word();
			w.start();
		}
		if("Excel".equals(args[0]))
		{
			Excel e=new Excel();
			e.start();
		}
	}
}

這裡,根據傳進來的參數選擇啟動哪一個組件。

這裡使用的就是靜態加載,因為使用new創建的一個對象。不過,如果開發Word的組工作較快,已經完成了開發,代碼假如如下:

class Word
{
	public void start()
	{
		System.out.println("Word running...");
	}
}

這僅僅打印一句話,但是Excel組開發較慢,還沒有開發出來,那麼Office還能用麼?當然不能,編譯Office出錯:

 

\

說找不到Excel類。這樣,整個的Office都不能使用了。

這時,就可以使用動態加載了。使用Class的forName方法動態加載一個類:

 

Word w=(Word)Class.forName(args[0]);

不過這裡有個問題,就是由於沒能事先知道首先實現的是哪個組件,所以不能強制類型轉換成Word。那怎麼辦?就可以使用接口了。定義一個接口OfficeAble,讓所有的組件實現這個接口即可:
interface OfficeAble
{
	void start();
}

新的Word類如下:
class Word implements OfficeAble
{
	public void start()
	{
		System.out.println("Word running...");
	}
}
這樣,在OfficeBetter類中就可以使用接口了:
class OfficeBetter
{
	public static void main(String[] args)
	{
		try
		{
			Class cl=Class.forName(args[0]);
			OfficeAble oa=(OfficeAble)cl.newInstance();
			oa.start();
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
	}
}
注意,forName會拋出異常,要進行捕獲並處理。

 

這時,編譯Word和OfficeBetter:

\

編譯沒有錯誤了,運行也正確。

當啟動Excel時就會報錯:

\

說沒有找到Excel類。

當Excel實現完後,只需要編譯Excel,不需要編譯OfficeBetter就可以運行:

\

結果正確。這樣,使用動態加載類,就可以隨時添加功能而不需要重新編譯。

5 在運行時使用反射分析對象

現在已經知道了如何查看任意對象的數據域名稱和類型:

(1)獲得對應的類類型;

(2)通過類類型調用getDeclaredFields方法;

利用反射機制可以查看在編譯時還不清楚的對象域。

查看對象域的關鍵方法是Field類中的get方法。如果f是一個Field類型的對象,obj是某個包含f域的類的對象,f.get(obj)將返回一個對象,其值為obj與的當前值。

既然能夠得到域的值,那麼也就能設置域的值。可以使用Field類中的set方法設置域的值,用法為f.set(obj,value),obj是包含f域的對象,value是要設置的值。

還要注意,如果f是一個私有域,那麼直接使用get方法會拋出一個IllegalAccessException異常。除非擁有訪問權限,否則Java安全機制只允許查看任意對象有哪些域而不允許得去域的值。

反射機制的默認行為受限於Java的訪問控制。然而,如果一個Java程序沒有受到安全管理器的控制,就可以覆蓋訪問控制。可以調用Field、Method或Constructor類中的setAccessible方法達到這個目的:

f.setAccessible(true);

setAccessible方法是AccessibleObject類中的一個方法,它是Field、Method和Constructor類的超類。

下面的代碼演示了使用get和set方法獲得和設置域的值:

 

Student stu=new Student("Bai",20,99);
Class cl=stu.getClass();
Field f=cl.getDeclaredField("name");
f.setAccessible(true);
String stuname=(String)f.get(stu);
System.out.println("The name is:"+stuname);
f.set(stu, "Liu");
System.out.println("The name is:"+(String)f.get(stu));
其中Student類包含String類型的name、int類型的age和double類型的score。

 

運行結果如下:

The name is:Bai
The name is:Liu

即成功獲得和設置域的值。

不過get還有一個問題,就是name是一個String,因此把它當做Object返回沒有問題。但是如果要返回double的score,double不是對象,這時可以使用Field類中的getDouble方法。此時,反射機制會自動將這個域值打包到相應的對象包裝器中。

 

Field fs=cl.getDeclaredField("score");
fs.setAccessible(true);
System.out.println("The score is:"+fs.getDouble(stu));

結果如下:

 

The score is:99.0

6 使用invoke調用任意方法

在C和C++中,可以從函數指針執行任意函數。雖然Java沒有提供函數指針,但反射機制允許調用任意方法。

和Field中的get方法類似,Method類中有個invoke方法,它允許調用包裝在當前Method對象中的方法。invoke方法的簽名是:

 

Object invoke(Object obj,Object... args);

從這裡可以看出,invoke方法有個Object類型的返回值,不過,要是調用的方法沒有返回值呢,這時invoke放回null。還有,invoke第一個參數是一個對象,第二個參數是一個可變參數,實際上就是一個Object數組。

 

在一個類中,必須知道哪些信息才能唯一確定一個方法呢?

由於Java中可以有同名的方法,因此可以使用參數來進行區分。不過要注意,返回類型不能作為區分兩個函數的依據。因此,在使用Method類中的getMethod和getDeclaredMethod方法時,要給出方法名和方法參數:

 

Method getMethod(String name,Class... parameterTypes)

下面的例子給出了invoke的使用:

 

 

import java.lang.reflect.*;
public class InvokeTest {
	public static void main(String[] args) {
		A a=new A();
		Class cl=a.getClass();
		try {
			Method m1=cl.getMethod("add", new Class[]{String.class,String.class});
			m1.invoke(a, new Object[]{"hello","world"});
			Method m2=cl.getMethod("add", int.class,int.class);
			m2.invoke(a, 1,2);
			Method m3=cl.getMethod("add");
			m3.invoke(a);
			Method m4=cl.getMethod("add", String.class,int.class);
			m4.invoke(null, "Liu",100);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
class A{
	public void add(){
		System.out.println("do nothing...");
	}
	public void add(int a,int b){
		System.out.println(a+b);
	}
	public void add(String a,String b){
		System.out.println(a.toUpperCase()+b.toUpperCase());
	}
	public static void add(String name,int age){
		System.out.println(name+" is "+age+" years old.");
	}
}

這裡定義了一個簡單的類A,裡面有四個方法名相同的方法add,其中一個是靜態方法。這四個方法的返回類型都是void,因此可以通過參數類型進行區分。在測試代碼中我們使用getMethod方法獲得add方法,其中第一個參數都是add,第二個參數給出參數類型。第二個參數是一個可變參數,即 一個數組,有兩種方法給出這個可變參數。

 

第一種,構造一個Class數組:

 

Method m1=cl.getMethod("add", new Class[]{String.class,String.class});

還可以直接列出參數,有幾個就列出幾個,用逗號(,)分隔:

 

 

Method m2=cl.getMethod("add", int.class,int.class);

獲得Method對象後,就可以調用invoke執行這個方法了。invoke有兩個參數,第一個參數給出包含這個方法的對象,第二個參數也是一個可變參數,即傳遞給要執行的方法的參數。對給出參數可以使用上面兩種方法中的一種。

 

這裡要注意的是,對於靜態方法,沒有包含它的對象,這時第一個參數置為null即可。

運行結果如下:

HELLOWORLD
3
do nothing...
Liu is 100 years old.

7 反射與泛型

我們知道,集合中的泛型使得一個集合中只能保存一種類型數據。比如ArrayList只能保存String類型,如果代碼中有add(20)就會報錯,因為20是int類型,不能轉換成String。那麼泛型是在哪個階段起作用呢?

泛型的使用在編寫代碼中起到了一種類型檢查的作用。我們知道,泛型只是編譯器的功能,Java虛擬機並沒有泛型,也就是說,是不是如果繞過編譯,就能在泛型中添加別的類型的數據呢?比如在ArrayList中添加int數據。

下面的代碼演示了這種情況:

 

import java.lang.reflect.*;
import java.util.ArrayList;
public class MethodDemo {
	public static void main(String[] args) {
		ArrayList list=new ArrayList();
		ArrayList list1=new ArrayList<>();
                System.out.println(list.getClass()==list1.getClass());
	}
}

上面的代碼定義了兩個ArrayList,其中list不是泛型,list1是泛型。我們首先檢查list和list的類類型是否相同。結果如下:

 

true

說明在編譯階段是去泛型化的。

接下來修改代碼如下:

 

import java.lang.reflect.*;
import java.util.ArrayList;
public class MethodDemo {
	public static void main(String[] args) {
		ArrayList list=new ArrayList();
		ArrayList list1=new ArrayList<>();
		
		list.add(10);
		list.add("hello");
		
		list1.add("world");
		//list1.add(20);ERROR:can't add int to ArrayList
		
		
		try {
			Method m=list1.getClass().getDeclaredMethod("add", Object.class);
			m.invoke(list1, 20);
			System.out.println(list1.size());
			System.out.println(list1);
			for(String s:list1){
				System.out.print(s+" ");
			}
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

直接在list1中使用add添加一個int數據不能成功,編譯器會報錯。之後使用Method中的getDeclaredMethod方法獲得ArrayList中的add方法,然後使用invoke調用這個方法添加一個int數據。這時,編譯器沒有報錯。打印list1的長度和內容,檢查是否添加成功。最後使用for循環遍歷list1中的內容。結果如下:

 

\

長度為2,內容也正確,說明我們跳過了編譯階段的類型檢查,在運行時成功添加了int類型數據。不過,使用for循環遍歷是報錯了,因為20不是String類型數據。

通過這個例子,可以知道,泛型會在編譯時去泛型化。同時可以通過反射在運行階段添加數據。

8 使用java.lang.reflect包中的Array類編寫泛型數組

java.lang.reflect包中的Array類可以動態創建數組。比如,可以將這個特性應用到Array類的copyOf方法實現中。

如果要給一個Student[]數組復制,可以先將Student[]轉換為Object[]數組,比如這樣:

 

public static Object[] badCopyOf(Object[] a,int newLength){
	Object[] newArray=new Object[newLength];
	System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
	return newArray;
}
不過,這樣的話,返回的類型是Object[],而Object[]不能轉化為Student[]類型。

 

為了編寫通用的方法,需要創建一個與原數組類型相同的新數組。為此,需要java.lang.reflect包中的Array類中的一些方法。

Array類中有一個newInstance方法,能夠創建新數組。這個方法要兩個參數,一個是數組的元素類型,另一個是數組的長度:

 

Object newArray=Array.newInstance(componentType,newLength);
這樣,就需要獲得數組元素的類型和數組的長度。使用Array類中的getComponentType方法可以獲得元素的類型,使用Array類中的getLength方法可以獲得數組的長度。

 

下面是完整的代碼:

 

import java.lang.reflect.*;
import java.util.Arrays;
public class CopyOfTest {
	public static void main(String[] args) {
		int[] a={1,2,3};
		a=(int[])goodCopyOf(a,10);
		System.out.println(Arrays.toString(a));
		
		String[] b={"Tom","Dick","Harry"};
		b=(String[])goodCopyOf(b,10);
		System.out.println(Arrays.toString(b));
		
		b=(String[])badCopyOf(b,10);
	}
	public static Object[] badCopyOf(Object[] a,int newLength){
		Object[] newArray=new Object[newLength];
		System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
		return newArray;
	}
	public static Object goodCopyOf(Object a,int newLength){
		Class cl=a.getClass();
		if(!cl.isArray())return null;
		Class componentType=cl.getComponentType();
		int length=Array.getLength(a);
		Object newArray=Array.newInstance(componentType, newLength);
		System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
		return newArray;
	}
}

結果如下:

\

注意,應該將goodCopyOf的參數聲明為Object類型,而不要聲明成Object[]。因為正數數組類型int[]可以轉換成Object,但不能轉換成對象數組。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved