本文我們通過一個實際的例子來演示反射在編程中的應用,可能之前大家對反射的學習,僅僅是停留在概念層面,不知道反射究竟應用在哪,所以是一頭霧水。相信通過這篇教程,會讓你對反射有一個更深層次的認知。
概念
Java反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為Java語言的反射機制。
如何理解反射?簡單的一句話解釋,將傳統的開發思路反向逆轉。
傳統的方式是通過類創建對象:類 ---> 對象。
反射就是將這個過程逆轉,通過對象得到類:對象 ---> 類。
通過對象得到的這個類該如何表示?
使用Class類來表示,此類是Java反射的源頭,是用來描述其他類的類,Class類的每一個實例化對象就是對其他類的描述。
在Object類中定義了以下的方法,此方法將被所有子類繼承:
public final Class getClass()。
也就是說每一個類,都可以調用getClass()方法獲取對應的Class對象,用來描述目標類,我們將這個Class類叫做目標類的運行時類。
有了Class對象,能做什么?
調用Class對象的newInstance()方法,可以動態創建目標類的對象。
要求:
1)目標類必須有無參數構造方法。
2)外部方法有足夠的權限訪問目標類的構造方法。
除了動態創建目標類的對象,反射也可以動態調用對象的各種方法,訪問成員變量。
Java反射機制主要提供下面幾種用途:
-
在運行時判斷任意一個對象所屬的類
-
在運行時構造任意一個類的對象
-
在運行時判斷任意一個類所具有的成員變量和方法
-
在運行時調用任意一個對象的方法
反射相關的主要API
java.lang.Class:描述一個類。
java.lang.reflect.Method:描述類的方法。
java.lang.reflect.Field:描述類的成員變量。
java.lang.reflect.Constructor:描述類的構造方法。
Class用來描述目標類的結構,叫做目標類的運行時類。
Class的常用方法:
方法 | 描述 |
---|---|
public Class<?>[] getInterfaces() | 返回運行時類實現的全部接口。 |
public Class<? Super T> getSuperclass() | 返回運行時類的父類。 |
public Constructor<T>[] getConstructors() | 返回運行時類的public構造方法。 |
public Constructor<T>[] getDeclaredConstructors() | 返回運行時類的全部構造方法。 |
public Method[] getMethods() | 返回運行時類的public方法。 |
public Method[] getDeclaredMethods() | 返回運行時類的全部方法。 |
public Field[] getFields() | 返回運行時類的public成員變量。 |
public Field[] getDeclaredFields() | 返回運行時類的全部成員變量。 |
Method用來描述運行時類的方法。
Method的常用方法:
方法 | 描述 |
---|---|
public Class<?> getReturnType() | 返回方法的返回值類型。 |
public Class<?>[] getParameterTypes() | 返回方法的參數列表。 |
public int getModifiers() | 返回方法的訪問權限修飾符。 |
public String getName(); | 返回方法名。 |
Field用來描述運行時類的成員變量。
Field的常用方法:
方法 | 描述 |
---|---|
public int getModifiers() | 返回成員變量的訪問權限修飾符。 |
public Class<?> getType() | 返回成員變量的數據類型。 |
public String getName() | 返回成員變量的名稱。 |
反射的應用
反射在實際中的應用主要是動態創建對象,動態調用對象的方法。
1.創建對象:
調用Class類的newInstance()方法創建對象。
2.調用指定方法:
(1)通過Class類的getMethod(String name,Class…parameterTypes)方法獲取一個Method對象,並設置此方法操作時所需要的參數類型。
(2)調用Object invoke(Object obj, Object[] args)方法,並向方法中傳遞目標obj對象的參數信息。
首先看一個簡單的例子,通過這個例子來理解Java的反射機制是如何工作的。
package com.chenHao.reflection; import java.lang.reflect.Method; /** * Java 反射練習。 * * @author chenHao */ public class ForNameTest { /** * 入口函數。 * * @param args * 參數 * @throws Exception * 錯誤信息 */ public static void main(String[] args) throws Exception { // 獲得Class Class<?> cls = Class.forName("java.lang.String"); // 通過Class獲得所對應對象的方法 Method[] methods = cls.getMethods(); // 輸出每個方法名 for (Method method : methods) { System.out.println(method); } } }
輸出如下結果:
public boolean java.lang.String.equals(java.lang.Object) public java.lang.String java.lang.String.toString() public int java.lang.String.hashCode() public int java.lang.String.compareTo(java.lang.String) public int java.lang.String.compareTo(java.lang.Object) public int java.lang.String.indexOf(int) public int java.lang.String.indexOf(int,int) public int java.lang.String.indexOf(java.lang.String) public int java.lang.String.indexOf(java.lang.String,int) public static java.lang.String java.lang.String.valueOf(int) public static java.lang.String java.lang.String.valueOf(char) public static java.lang.String java.lang.String.valueOf(boolean) public static java.lang.String java.lang.String.valueOf(float) public static java.lang.String java.lang.String.valueOf(char[],int,int) public static java.lang.String java.lang.String.valueOf(double) public static java.lang.String java.lang.String.valueOf(char[]) public static java.lang.String java.lang.String.valueOf(java.lang.Object) public static java.lang.String java.lang.String.valueOf(long) public char java.lang.String.charAt(int) public int java.lang.String.codePointAt(int) public int java.lang.String.codePointBefore(int) public int java.lang.String.codePointCount(int,int) public int java.lang.String.compareToIgnoreCase(java.lang.String) public java.lang.String java.lang.String.concat(java.lang.String) public boolean java.lang.String.contains(java.lang.CharSequence) public boolean java.lang.String.contentEquals(java.lang.CharSequence) public boolean java.lang.String.contentEquals(java.lang.StringBuffer) public static java.lang.String java.lang.String.copyValueOf(char[]) public static java.lang.String java.lang.String.copyValueOf(char[],int,int) public boolean java.lang.String.endsWith(java.lang.String) public boolean java.lang.String.equalsIgnoreCase(java.lang.String) public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[]) public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[]) public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException public void java.lang.String.getBytes(int,int,byte[],int) public byte[] java.lang.String.getBytes() public byte[] java.lang.String.getBytes(java.nio.charset.Charset) public void java.lang.String.getChars(int,int,char[],int) public native java.lang.String java.lang.String.intern() public boolean java.lang.String.isEmpty() public int java.lang.String.lastIndexOf(java.lang.String) public int java.lang.String.lastIndexOf(int,int) public int java.lang.String.lastIndexOf(int) public int java.lang.String.lastIndexOf(java.lang.String,int) public int java.lang.String.length() public boolean java.lang.String.matches(java.lang.String) public int java.lang.String.offsetByCodePoints(int,int) public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int) public boolean java.lang.String.regionMatches(int,java.lang.String,int,int) public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence) public java.lang.String java.lang.String.replace(char,char) public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String) public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String) public java.lang.String[] java.lang.String.split(java.lang.String) public java.lang.String[] java.lang.String.split(java.lang.String,int) public boolean java.lang.String.startsWith(java.lang.String) public boolean java.lang.String.startsWith(java.lang.String,int) public java.lang.CharSequence java.lang.String.subSequence(int,int) public java.lang.String java.lang.String.substring(int) public java.lang.String java.lang.String.substring(int,int) public char[] java.lang.String.toCharArray() public java.lang.String java.lang.String.toLowerCase() public java.lang.String java.lang.String.toLowerCase(java.util.Locale) public java.lang.String java.lang.String.toUpperCase() public java.lang.String java.lang.String.toUpperCase(java.util.Locale) public java.lang.String java.lang.String.trim() public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException public final void java.lang.Object.wait() throws java.lang.InterruptedException public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException public final native java.lang.Class java.lang.Object.getClass() public final native void java.lang.Object.notify() public final native void java.lang.Object.notifyAll()
這樣就列出了java.lang.String類的所有方法名、及其限制符、返回類型及拋出的異常。這個程序使用Class類forName方法載入指定的類,然后調用getMethods方法返回指定類的方法列表。java.lang.reflect.Method用來表述某個類中的單一方法。
使用java的反射機制,一般需要遵循三步:
-
獲得你想操作類的Class對象
-
通過第一步獲得的Class對象去取得操作類的方法或是屬性名
-
操作第二步取得的方法或是屬性
Java運行的時候,某個類無論生成多少個對象,他們都會對應同一個Class對象,它表示正在運行程序中的類和接口。如何取得操作類的Class對象,常用的有三種方式:
-
調用Class的靜態方法forName,如上例;
-
使用類的.class語法,如:Class cls = String.class;
-
調用對象的getClass方法,如:String str = "abc";Class cls = str .getClass();
下面將通過實例講述如何通過前面所訴的三步來執行某對象的某個方法:
1 package com.chenHao.reflection; 2 3 import java.lang.reflect.Method; 4 5 /** 6 * Java 反射練習。 7 * 8 * @author chenHao 9 */ 10 public class ReflectionTest { 11 public static void main(String[] args) throws Exception { 12 DisPlay disPlay = new DisPlay(); 13 // 獲得Class 14 Class<?> cls = disPlay.getClass(); 15 // 通過Class獲得DisPlay類的show方法 16 Method method = cls.getMethod("show", String.class); 17 // 調用show方法 18 method.invoke(disPlay, "chenHao"); 19 } 20 } 21 22 class DisPlay { 23 public void show(String name) { 24 System.out.println("Hello :" + name); 25 } 26 }
前面說過,Java程序的每個類都會有個Class對象與之對應。Java反射的第一步就是獲得這個Class對象,如代碼14行。當然,每個類的方法也必有一個Method對象與之對應。要通過反射的方式調用這個方法,就要首先獲得這個方法的Method對象,如代碼16行,然后用Method對象反過來調用這個方法,如代碼18行。
注意:16行getMethod方法的第一個參數是方法名,第二個是此方法的參數類型,如果是多個參數,接着添加參數就可以了,因為getMethod是可變參數方法。執行18行代碼的invoke方法,其實也就是執行show方法,注意invoke的第一個參數,是DisPlay類的一個對象,也就是調用DisPlay類哪個對象的show方法,第二個參數是給show方法傳遞的參數。類型和個數一定要與16行的getMethod方法一直。
上例講述了如何通過反射調用某個類的方法,下面將再通過一個實例講述如何在運行時創建類的一個對象,如何通過反射給某個類的屬性賦值:
1 package com.chenHao.reflection; 2 3 import java.lang.reflect.Field; 4 5 /** 6 * Java 反射之屬性練習。 7 * 8 * @author chenHao 9 */ 10 public class ReflectionTest { 11 public static void main(String[] args) throws Exception { 12 // 建立學生對象 13 Student student = new Student(); 14 // 為學生對象賦值 15 student.setStuName("chenHao"); 16 student.setStuAge(24); 17 // 建立拷貝目標對象 18 Student destStudent = (Student) copyBean(student); 19 // 輸出拷貝結果 20 System.out.println(destStudent.getStuName() + ":" 21 + destStudent.getStuAge()); 22 } 23 24 /** 25 * 拷貝學生對象信息。 26 * 27 * @param from 28 * 拷貝源對象 29 * @param dest 30 * 拷貝目標對象 31 * @throws Exception 32 * 例外 33 */ 34 private static Object copyBean(Object from) throws Exception { 35 // 取得拷貝源對象的Class對象 36 Class<?> fromClass = from.getClass(); 37 // 取得拷貝源對象的屬性列表 38 Field[] fromFields = fromClass.getDeclaredFields(); 39 // 取得拷貝目標對象的Class對象 40 Object ints = fromClass.newInstance(); 41 for (Field fromField : fromFields) { 42 // 設置屬性的可訪問性 43 fromField.setAccessible(true); 44 // 將拷貝源對象的屬性的值賦給拷貝目標對象相應的屬性 45 fromField.set(ints, fromField.get(from)); 46 } 47 48 return ints; 49 } 50 } 51 52 /** 53 * 學生類。 54 */ 55 class Student { 56 /** 姓名 */ 57 private String stuName; 58 /** 年齡 */ 59 private int stuAge; 60 61 public String getStuName() { 62 return stuName; 63 } 64 65 public void setStuName(String stuName) { 66 this.stuName = stuName; 67 } 68 69 public int getStuAge() { 70 return stuAge; 71 } 72 73 public void setStuAge(int stuAge) { 74 this.stuAge = stuAge; 75 } 76 }
注意:Field提供了get和set方法獲取和設置屬性的值,但是由於屬性是私有類型,所以需要設置屬性的可訪問性為true,如代碼50~51行。也可以在為整個fields設置可訪問性,在40行下面使用AccessibleObject的靜態方法setAccessible,如:AccessibleObject.setAccessible(fromFields, true);
簡化版的MyBatis工具
需求:
創建一個查詢數據庫的工具類,自動將SQL語句查詢出的結果集,封裝成不同的對象返回,一個簡化版的MyBatis工具。
思路:
工具類查詢方法的參數列表:Connection對象,SQL語句,目標運行時類對象clazz,數據表的id值。
1.通過Connection對象,SQL語句,id值查詢出對應的結果集。
2.利用反射機制調用clazz的無參構造方法創建目標對象。
3.獲取clazz的Filed,即目標類的所有成員變量。
4.找到與成員變量名相同的結果集字段,並獲取字段值。
5.通過成員變量名找到對應的setter方法。
6.利用反射機制調用setter方法完成賦值。
實現步驟:
1.導入mysql驅動,c3p0數據源相關jar包。
2.創建c3p0-config.xml。
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <named-config name="testc3p0"> <!-- 指定連接數據源的基本屬性 --> <property name="user">root</property> <property name="password">root</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/school?useUnicode=true&characterEncoding=UTF-8</property> <!-- 若數據庫中連接數不足時, 一次向數據庫服務器申請多少個連接 --> <property name="acquireIncrement">5</property> <!-- 初始化數據庫連接池時連接的數量 --> <property name="initialPoolSize">5</property> <!-- 數據庫連接池中的最小的數據庫連接數 --> <property name="minPoolSize">5</property> <!-- 數據庫連接池中的最大的數據庫連接數 --> <property name="maxPoolSize">10</property> <!-- C3P0 數據庫連接池可以維護的 Statement 的個數 --> <property name="maxStatements">20</property> <!-- 每個連接同時可以使用的 Statement 對象的個數 --> <property name="maxStatementsPerConnection">5</property> </named-config> </c3p0-config>
3.創建數據表student,user。
DROP TABLE IF EXISTS `student`; CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(11) DEFAULT NULL, `address` varchar(11) DEFAULT NULL, `tel` varchar(255) DEFAULT NULL, `score` double(11,1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
4.創建實體類Student,User。
package com.southwind.entity; public class Student { private int id; private String name; private String address; private String tel; private double score; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public double getScore() { return score; } public void setScore(double score) { this.score = score; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", address=" + address + ", tel=" + tel + ", score=" + score + "]"; } }
package com.southwind.entity; public class User { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User [id=" + id + ", name=" + name + ", age=" + age + "]"; } }
5.創建JDBCTools工具類。
package com.southwind.util; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.mchange.v2.c3p0.ComboPooledDataSource; public class JDBCTools { private static ComboPooledDataSource dataSource; static{ dataSource = new ComboPooledDataSource("testc3p0"); } /** * 獲取Connection * @return */ public static Connection getConnection(){ try { return dataSource.getConnection(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } /** * 釋放資源 * @param conn * @param stmt * @param rs */ public static void release(Connection conn,Statement stmt,ResultSet rs){ if(conn != null){ try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(stmt != null){ try { stmt.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(rs != null){ try { rs.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
6.創建數據庫查詢工具類MyQueryRunner,核心代碼。
package com.southwind.util; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; /** * 通用工具類 * @author southwind * */ public class MyQueryRunner { /** * 將結果集動態封裝成對象 * @param conn * @param sql * @param clazz * @param id * @return */ public Object query(Connection conn,String sql,Class clazz,int id){ PreparedStatement pstmt = null; ResultSet rs = null; Object obj = null; try { pstmt = conn.prepareStatement(sql); pstmt.setInt(1, id); rs = pstmt.executeQuery(); obj = clazz.newInstance(); if(rs.next()){ //遍歷實體類屬性集合,依次將結果集中的值賦給屬性 Field[] fields = clazz.getDeclaredFields(); //獲取ResultSet數據 ResultSetMetaData rsmd = rs.getMetaData(); for(int i = 0; i < fields.length; i++){ Object value = setFieldValueByResultSet(fields[i],rsmd,rs); //通過屬性名找到對應的setter方法 String name = fields[i].getName(); name = name.substring(0, 1).toUpperCase() + name.substring(1); String MethodName = "set"+name; Method methodObj = clazz.getMethod(MethodName,fields[i].getType()); //調用setter方法完成賦值 methodObj.invoke(obj, value); } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return obj; } /** * 根據將結果集中的值賦給對應的屬性 * @param field * @param rsmd * @param rs * @return */ public Object setFieldValueByResultSet(Field field,ResultSetMetaData rsmd,ResultSet rs){ Object result = null; try { int count = rsmd.getColumnCount(); for(int i=1;i<=count;i++){ //找到與屬性名相同的結果集字段 if(field.getName().equals(rsmd.getColumnName(i))){ //獲取屬性的數據類型 String type = field.getType().getName(); switch (type) { case "int": result = rs.getInt(field.getName()); break; case "java.lang.String": result = rs.getString(field.getName()); break; case "double": result = rs.getDouble(field.getName()); break; } } } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return result; } }
7.測試
通過id查詢student表,調用工具方法,直接返回Student對象。
package com.southwind.test; import java.sql.Connection; import com.southwind.entity.Student; import com.southwind.util.MyQueryRunner; import com.southwind.util.JDBCTools; public class Test { public static void main(String[] args) { Connection conn = JDBCTools.getConnection(); String sql = "select * from student where id = ?"; MyQueryRunner myQueryRunner = new MyQueryRunner(); Student student = (Student) myQueryRunner.query(conn, sql, Student.class, 1); System.out.println(student); } }
通過id查詢user表,調用工具方法,直接返回User對象。
package com.southwind.test; import java.sql.Connection; import com.southwind.entity.User; import com.southwind.util.MyQueryRunner; import com.southwind.util.JDBCTools; public class Test { public static void main(String[] args) { Connection conn = JDBCTools.getConnection(); String sql = "select * from users where id = ?"; MyQueryRunner myQueryRunner = new MyQueryRunner(); User user = (User) myQueryRunner.query(conn, sql, User.class, 30); System.out.println(user); } }
至此,Java反射機制的常用機能(運行時調用對象的方法、類屬性的使用、創類類的對象)已經介紹完了。
補充:在獲得類的方法、屬性、構造函數時,會有getXXX和getgetDeclaredXXX兩種對應的方法。之間的區別在於前者返回的是訪問權限為public的方法和屬性,包括父類中的;但后者返回的是所有訪問權限的方法和屬性,不包括父類的。