java基礎_創建對象的五種方式_觸發類初始化的五種方式_內部類_Class類和Object類_反射

一:使用new關鍵字
這是最常見也是最簡單的創建對象的方式了。通過這種方式,我們可以調用任意的構造函數(無參的和帶參數的)。

public static void main(String[] args) {
        People people = new People();
    }

 


二:使用Class類的newInstance方法
利用反射來獲取class對象調用newInstance方法創建對象,其調用了無參的構造函數,所以類必須有public無參構造函數纔行

如下可以正常運行,因爲People有默認的無參構造器

public class People {
    public static void main(String[] args) {
        try {
            People people = (People) Class.forName("People").newInstance();
            System.out.println(people);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

如下運行報錯,因爲People沒有提供無參構造器

public class People {
    public People(String a) {
    }

    public static void main(String[] args) {
        try {
            People people = (People) Class.forName("People").newInstance();
            System.out.println(people);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

三:使用Constructor類的newInstance方法
和Class類的newInstance方法很像。但是它可以調用任意構造函數創建對象,包括私有的。(所以即使你私有了構造函數,還是可以創建對象的)

Constructor<People> constructor = (Constructor<People>) Class.forName("People").getConstructor();
People people1 = constructor.newInstance();

 


此種方法和方法二的newInstance方法就是大家所說的反射。事實上Class的newInstance方法內部調用Constructor的newInstance方法。這也是衆多框架,如Spring、hibernate、Struts等使用後者的原因。

四:使用clone方法
調用一個對象的clone方法,jvm就會創建一個新的對象,將前面對象的內容全部拷貝進去。用clone方法創建對象並不會調用任何構造函數。
但是要使用clone方法,我們需要先實現Cloneable接口並實現其定義的clone方法。

public class People implements Cloneable {
    public static void main(String[] args) {
        People people1 = new People();
        try {
            People people2 = (People) people1.clone();
            System.out.println(people1);
            System.out.println(people2);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

輸出

People@4554617c
People@74a14482
1
2
五:使用反序列化
當我們序列化和反序列化一個對象,jvm會給我們創建一個單獨的對象。在反序列化時,jvm創建對象並不會調用任何構造函數
但是爲了反序列化一個對象,我們需要讓我們的類實現Serializable接口

public class People implements Serializable {
    public static void main(String[] args) {
        People people1 = new People();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(people1);

            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            People people2 = (People) objectInputStream.readObject();

            System.out.println(people1);
            System.out.println(people2);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

輸出

People@14ae5a5
People@7f31245a

觸發類初始化的五種方式

一:類的生命週期
類從被加載到虛擬機內存開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用、卸載七個階段,其中驗證、準備、解析這三個階段統稱爲鏈接。
加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類加載過程必須按照這種順序按部就班的開始。

二:類加載時機
什麼情況下需要開始類加載過程的第一個階段:加載 呢?Java虛擬機規範中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。
但是對於初始化階段,虛擬機規範則是嚴格規定了有且只有如下五種情況必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始)

遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
對於這5鍾會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行爲稱爲對一個類進行主動引用,下面我們來看一下。

1、使用new關鍵字

public class MyTest {
    public static void main(String[] args) {
        People people = new People();
    }
    public static class People {
        static {
            System.out.println("People 類被初始化");
        }
    }
}

 


運行輸出:

People 類被初始化

2、調用本類靜態變量或靜態方法

public class MyTest {
    public static void main(String[] args) {
        People.getAge();
    }
    public static class People {
        static {
            System.out.println("People 類被初始化");
        }
        public static int age = 100;

        public static int getAge() {
            return age;
        }
    }
}

 

運行輸出:

People 類被初始化

3、使用反射獲取類信息

public class MyTest {
    public static void main(String[] args) {
        try {
            Class.forName("MyTest$People");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    public static class People {
        static {
            System.out.println("People 類被初始化");
        }
    }
}

運行輸出:

People 類被初始化

4、子類初始化時

public class MyTest {
    public static void main(String[] args) {
        Son son = new Son();
    }
    public static class People {
        static {
            System.out.println("People 類被初始化");
        }
    }
    public static class Son extends People {
        static {
            System.out.println("Son 類被初始化");
        }
    }
}

運行輸出

People 類被初始化
Son 類被初始化

5、包含main()主函數的類

public class MyTest {
    static {
        System.out.println("MyTest 類被初始化");
    }
    public static void main(String[] args) {

    }
}

運行輸出

MyTest 類被初始化

三:類的被動引用
除了上面5種引用類的方式稱爲主動引用方式之外的其它方式全是被動引用。被動引用不會觸發類的初始化
我們看幾個被動引用的例子

被動引用方式一:通過子類引用父類的靜態字段,不會導致子類初始化

public class MyTest {
    public static void main(String[] args) {
        System.out.println(Son.a);
    }
    public static class Father {
        public static int a = 0;

        static {
            System.out.println("Father 類被初始化");
        }
    }
    public static class Son extends Father {
        static {
            System.out.println("Son 類被初始化");
        }
    }
}

輸出結果

Father 類被初始化
0

被動引用方式二:通過數組定義來引用類,不會觸發此類的初始化

public class MyTest {
    public static void main(String[] args) {
        Father[] fathers = new Father[10];
    }
    public static class Father {
        static {
            System.out.println("Father 類被初始化");
        }
    }
}

運行後什麼也沒輸出

被動引用方式三:常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類初始化

public class MyTest {
    public static void main(String[] args) {
        System.out.println(Father.m);
    }
    public static class Father {
        static {
            System.out.println("Father 類被初始化");
        }
        public static final int m = 0;
    }
}

運行結果輸出

0

內部類

一、什麼是內部類

 

內部類顧名思義就是在類的內部再定義一個類,內部類依賴於外部類而存在,內部類可以分爲成員內部類、靜態內部類、局部內部類、匿名內部類。

內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class文件,但是前面冠以外部類的類名和$符號 。

外部類的訪問權限只能是public或包訪問權限,而內部類的訪問權限可以是public、private、protected、包訪問權限都可以。

二、成員內部類


一:成員內部類不屬於外部類,它是屬於外部類實例化對象的,創建成員內部類需要依附於外部類的實例化對象。

public class Outer {
private class Inner {
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.new Inner();
}
}

 



二、成員內部類可以訪問外部類的成員變量和成員方法,是因爲成員內部類通過構造方法拷貝了一份外部類的實例

public class Outer {
private int a = 100;
private class Inner {
int b = a;
}
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.new Inner();
System.out.println(inner.b);
}
}

 

運行輸出:100

我們來看一下編譯後的class文件

class Outer$Inner {
int b;

private Outer$Inner(Outer var1) {
this.this$0 = var1;
this.b = Outer.access$000(this.this$0);
}
}

我們看到編譯後Inner class會自動生成帶外部類對象引用參數的構造函數,並且通過創建內部類對象時把外部類對象引用拷貝一份用於訪問外部類實例的成員。

三、成員內部類中是不可以定義靜態變量和靜態方法的,但是可以定義靜態常量

成員內部類是類實例的一部分不是類的一部分,而靜態變量是屬於類的。成員內部類是需要依賴外部類的實例,而靜態變量是不需要依賴外部類的實例的,這裏是矛盾的,所以編譯器在編譯的時候是不允許成員內部類定義靜態變量的。
成員內部類是可以定義靜態常量的,因爲常量放在內存中常量池,它的機制與變量是不同的,編譯時,加載常量是不需要加載類的,所以就沒有上面那種矛盾。

public class Outer {
private class Inner {
public static final int a = 100;
}
public static void main(String[] args) {
System.out.println(Inner.a);
}
}

 


輸出:100

三、局部內部類


局部內部類是定義在一個方法或者一個作用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。
局部內部類就像是方法裏面的一個局部變量一樣,是不能有public、protected、private以及static修飾符的。

public class Outer {
public int test() {
int a = 0;
class Inner {
int b = a;
}
Inner inner = new Inner();
return inner.b;
}
public static void main(String[] args) {
Outer outer = new Outer();
System.out.println(outer.test());
}
}

 

編譯後會生成兩個class文件,Outer.class和Outer$1Inner.clas。我們來反編譯看一下。

import Outer.1Inner;

public class Outer {
public Outer() {
}

public int test() {
byte var1 = 0;
1Inner var2 = new 1Inner(this, var1);
return var2.b;
}

public static void main(String[] var0) {
Outer var1 = new Outer();
System.out.println(var1.test());
}
}

class Outer$1Inner {
int b;

Outer$1Inner(Outer var1, int var2) {
this.this$0 = var1;
this.val$a = var2;
this.b = this.val$a;
}
}

 

我們可以看到,局部內部類默認生成一個帶有兩個參數的構造函數,參數一是傳的外部類對象的引用,這個也很好理解,前面我們說過,內部類默認拷貝了一份外部類的引用。參數二傳的是局部變量a,也就是說,內部類引用局部變量是通過構造函數傳進來的。

四、匿名內部類


匿名內部類就是沒有名字的內部類,匿名內部類是局部內部類的一種特殊形式。

匿名內部類是唯一 一種沒有構造器的類。
匿名內部類裏面不能定義靜態變量、方法和類。
一個匿名內部類一定是在new的後面,用其隱含實現一個接口或實現一個類。
我們來看一個匿名內部類

public class Outer {
public static void main(String[] args) {
int x = 100;
ITestInterface iTestInterface = new ITestInterface() {
@Override
public int test() {
int y = x;
return y;
}
};
}
public interface ITestInterface {
int test();
}
}

 

編譯後會生成3個class文件,分別是Outer.class,Outer$ITestInterface.class,Outer$1.class,所以匿名內部類編譯後的類名字是 外部類+$+數字組成的。
我們繼續來看編譯後的代碼

public class Outer {
public Outer() {
}

public static void main(String[] args) {
final int x = 100;//編譯期自動加上final
Outer.ITestInterface var10000 = new Outer.ITestInterface() {
public int test() {
int y = x;
return y;
}
};
}

public interface ITestInterface {
int test();
}
}

 


我們看變量x之前多了一個final,這是編譯期自動爲我們加上的,在JDK1.8之後編譯期纔會自動加上final,在1.8之前編譯期會強制我們加上final的,否則會編譯不通過。爲什麼要加final呢?

如果匿名內部類使用了局部變量,那麼編譯器會將使用的值拷貝一份,作爲構造函數的一個參數傳遞進來(構造函數是編譯器自動添加)。因爲局部變量在方法或者代碼塊執行完畢,就會被銷燬,所以編譯器在編譯的時候,就拷貝了一份局部變量存儲的字面值或者地址值,這樣局部變量被銷燬時,匿名內部類依然擁有之前傳遞進來的值。現在我們從語義上來理解下Java設計者的考慮:假如傳遞到匿名內部類的局部變量,不加final修飾,那麼意味着局部變量可以改變,這就意味着匿名內部類裏面值的變更和外部的變量的變更不能同步,雖然內部類持有的是局部變量值的拷貝,但是語義應該保持一致,語義保持一致的前提是值要能同步,因爲java編譯器的設計無法提供兩邊同步變更的機制,所以直接鎖死,不允許內外變更。

五、靜態內部類


靜態內部類是不需要依賴於外部類對象的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非static成員變量或者方法,因爲在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非static成員就會產生矛盾,因爲外部類的非static成員必須依附於具體的對象。

public class Outer {
private static int a = 0;

public static class Inner {
private static int b = a;
}
}

 

Class類和Object類

在Java的世界裏,一切皆是對象,所有的對象都是繼承於Object類,而記錄對象的類型的信息是由Class類來完成的,下面就讓我們來具體瞭解一下Class類和Object類。

一:Class類
每個類的運行時的類型信息就是用Class對象表示的,它包含了與類有關的信息,其實我們的實例對象就通過Class對象來創建的。Java使用Class對象執行其RTTI(運行時類型識別,Run-Time Type Identification),多態是基於RTTI實現的。

每一個類都有Class對象,基本類型 ( byte, char, short, int, long, float, double and boolean)有Class對象,數組有Class對象,就連關鍵字void也有Class對象(void.class),Class對象對應着java.lang.Class類,如果說類是對象抽象和集合的話,那麼Class類就是對類的抽象和集合。

Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象。一個類被加載到內存並供我們使用需要經歷如下三個階段:

 

 

 

什麼時候觸發類的加載,請參考上一篇文章:喫透Java基礎三:觸發類初始化的五種方式

獲取Class對象的三種方式

  • Class.forName(“類的全限定名”)
  • 實例對象.getClass()
  • 類名.class (類字面常量)


Class類重要方法

類信息
私有構造函數,表示外部不能創建,只能由虛擬機默認去創建。

public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
/*
* 私有構造函數,只能由虛擬機去創建
*/
private Class(ClassLoader loader) {
classLoader = loader;
}

public String toString() {
return (isInterface() ? "interface " : (isPrimitive() ? "" : "class "))
+ getName();
}

}

通過forName()方法獲取一個類的Class信息

/**
* 返回與給定字符串名稱的類或接口相關聯的類對象

/**
 * 返回與給定字符串名稱的類或接口相關聯的類對象
 */
public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
    
/**
 * 使用給定的類加載器返回與給定字符串名稱的類或接口相關聯的類對象
 */
public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }
    
/**
 *
 */
private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

 



通過newInstance可以創建一個該類的新的實例化對象

public T newInstance()
        throws InstantiationException, IllegalAccessException
    {
        if (System.getSecurityManager() != null) {
            checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
        }

        // NOTE: the following code may not be strictly correct under
        // the current Java memory model.

        // Constructor lookup
        if (cachedConstructor == null) {
            if (this == Class.class) {
                throw new IllegalAccessException(
                    "Can not call newInstance() on the Class for java.lang.Class"
                );
            }
            try {
                Class<?>[] empty = {};
                final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
                // Disable accessibility checks on the constructor
                // since we have to do the security check here anyway
                // (the stack depth is wrong for the Constructor's
                // security check to work)
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                                c.setAccessible(true);
                                return null;
                            }
                        });
                cachedConstructor = c;
            } catch (NoSuchMethodException e) {
                throw (InstantiationException)
                    new InstantiationException(getName()).initCause(e);
            }
        }
        Constructor<T> tmpConstructor = cachedConstructor;
        // Security check (same as in java.lang.reflect.Constructor)
        int modifiers = tmpConstructor.getModifiers();
        if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            if (newInstanceCallerCache != caller) {
                Reflection.ensureMemberAccess(caller, this, null, modifiers);
                newInstanceCallerCache = caller;
            }
        }
        // Run constructor
        try {
            return tmpConstructor.newInstance((Object[])null);
        } catch (InvocationTargetException e) {
            Unsafe.getUnsafe().throwException(e.getTargetException());
            // Not reached
            return null;
        }
    }

 


二:Object類
Java中所有對象默認繼承Object類,可以說,Object類是所有對象的祖先。Object類中一共定義12個方法,其中7個native方法,5個普通方法。

 

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }
    public final native Class<?> getClass();
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;
    public final native void wait(long timeout) throws InterruptedException;
    public final native void notify();
    public final native void notifyAll();

    public boolean equals(Object obj) {
        return (this == obj);
    }
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    protected void finalize() throws Throwable { }
    

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
        if (nanos > 0) {
            timeout++;
        }
        wait(timeout);
    }
    public final void wait() throws InterruptedException {
        wait(0);
    }
}

 

registerNatives()
registerNatives函數前面有native關鍵字修飾,Java中,用native關鍵字修飾的函數表明該方法的實現並不是在Java中去完成,而是由C/C++去完成,並被編譯成了.dll,由Java去調用。

getClass()
getClass()也是一個native方法,返回的是此Object對象的類對象/運行時類對象Class<?>。效果與Object.class相同

hashCode()
返回對象的哈希碼值。 支持這種方法是爲了散列表,如HashMap提供的那樣 。

只要在執行Java應用程序時多次在同一個對象上調用該方法, hashCode方法必須始終返回相同的整數
如果根據equals(Object)方法兩個對象相等,則在兩個對象中的每個對象上調用hashCode方法必須產生相同的整數結果。
不要求如果兩個對象根據equals(java.lang.Object)方法不相等,那麼在兩個對象中的每個對象上調用hashCode方法必須產生不同的整數結果。 但是,程序員應該意識到,爲不等對象生成不同的整數結果可能會提高哈希表的性能。
clone()
clone方法實現的是淺拷貝,只拷貝當前對象,並且在堆中分配新的空間,放這個複製的對象。但是對象如果裏面有其他類的子對象,那麼就不會拷貝到新的對象中。
深拷貝與淺拷貝

淺拷貝
淺拷貝是按位拷貝對象,它會創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。
如果屬性是基本類型,拷貝的就是基本類型的值;如果屬性是內存地址(引用類型),
拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。

深拷貝
深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的內存。當對象和它所引用的對象一起拷貝時即發生深拷貝。
深拷貝相比於淺拷貝速度較慢並且花銷較大。
現在爲了要在clone對象時進行深拷貝, 那麼就要Clonable接口,覆蓋並實現clone方法,
除了調用父類中的clone方法得到新的對象, 還要將該類中的引用變量也clone出來。
如果只是用Object中默認的clone方法,是淺拷貝的。

wait() notify() notifAll()

wait():調用此方法所在的當前線程等待,直到在其他線程上調用此對象的notify()/notifyAll()方法。

wait(long timeout)/wait(long timeout, int nanos):調用此方法所在的當前線程等待,直到在其他線程上調用此對象的notify()/notifyAll()方法,或超過指定的超時時間量。

notify()/notifyAll():喚醒在此對象監視器上等待的單個線程/所有線程。

equals(Object obj)

判斷兩個對象是否相等,equals方法在非空對象引用上實現等價關係:

  • 自反性 :對於任何非空的參考值x , x.equals(x)應該返回true 。
  • 對稱性 :對於任何非空引用值x和y , x.equals(y)應該返回true當且僅當y.equals(x)回報true 。
  • 傳遞性 :對於任何非空引用值x , y和z ,如果x.equals(y)回報true個y.equals(z)回報true ,然後x.equals(z)應該返回true 。
  • 一致性 :對於任何非空引用值x和y ,多次調用x.equals(y)始終返回true或始終返回false ,沒有設置中使用的信息equals比較上的對象被修改。
  • 對於任何非空的參考值x , x.equals(null)應該返回false 。

toString()

返回對象的字符串表示形式,getClass()返回對象的類對象,getClassName()以String形式返回類對象的名稱(含包名)。Integer.toHexString(hashCode())則是以對象的哈希碼爲實參,以16進制無符號整數形式返回此哈希碼的字符串表示形式。

finalize()

一個子類覆蓋了finalize方法,當垃圾回收此對象之前會調用此方法,但是隻會調用一次,加入這次在finalize()方法中讓此對象有引用指向他,那麼下次再回收此對象時就不會調用此方法。

反射

一:什麼是反射
Java 反射機制可以讓我們在編譯期(Compile Time)之外的運行期(Runtime)檢查類,接口,變量以及方法的信息。反射還可以讓我們在運行期實例化對象,調用方法,通過調用 get/set 方法獲取變量的值。

很多人都認爲反射在實際的 Java 開發應用中並不廣泛,其實不然。當我們在使用 IDE(如 Eclipse,IDEA)時,當我們輸入一個對象或類並想調用它的屬性或方法時,一按點號,編譯器就會自動列出它的屬性或方法,這裏就會用到反射。

反射最重要的用途就是開發各種通用框架。很多框架(比如 Spring)都是配置化的(比如通過 XML 文件配置 Bean),爲了保證框架的通用性,它們可能需要根據配置文件加載不同的對象或類,調用不同的方法,這個時候就必須用到反射,運行時動態加載需要加載的對象。

二:反射應用
使用 Java 反射機制可以在運行時期檢查 Java 類的信息,檢查 Java 類的信息往往是你在使用 Java 反射機制的時候所做的第一件事情,通過獲取類的信息你可以獲取以下相關的內容:Class對象、類名、修飾符、包信息、父類、實現的接口、構造器、方法、變量、註解等等。

1、Class 對象
檢查一個類的信息之前,你首先需要獲取類的 Class 對象。Java 中的所有類型包括基本類型(int, long, float等等),即使是數組都有與之關聯的 Class 類的對象,獲取Class對象有如下三種方式:

Class class1 = Class.forName("MyTest");
Class class2 = new MyTest().getClass();
Class class3 = MyTest.class;



2、類名
可以從 Class 對象中獲取兩個版本的類名

Class class1 = Class.forName("MyTest");
//獲取類的全限定名稱(包含包名)
class1.getName();

//獲取類的名稱(不包含包名)
class1.getSimpleName();

3、修飾符
可以通過 Class 對象來訪問一個類的修飾符, 即public,private,static 等等的關鍵字,你可以使用如下方法來獲取類的修飾符:

Class class1 = Class.forName("MyTest");
int flag = class1.getModifiers();


修飾符都被包裝成一個int類型的數字,這樣每個修飾符都是一個位標識(flag bit),這個位標識可以設置和清除修飾符的類型。 可以使用 java.lang.reflect.Modifier 類中的方法來檢查修飾符的類型:

Modifier.isAbstract(int modifiers);
Modifier.isFinal(int modifiers);
Modifier.isInterface(int modifiers);
Modifier.isNative(int modifiers);
Modifier.isPrivate(int modifiers);
Modifier.isProtected(int modifiers);
Modifier.isPublic(int modifiers);
Modifier.isStatic(int modifiers);
Modifier.isStrict(int modifiers);
Modifier.isSynchronized(int modifiers);
Modifier.isTransient(int modifiers);
Modifier.isVolatile(int modifiers);

4、包信息
可以使用 Class 對象通過如下的方式獲取包信息:

Class class1 = Class.forName("MyTest");
Package pak = class1.getPackage();


通過 Package 對象你可以獲取包的相關信息,比如包名。

5、父類
通過 Class 對象你可以訪問類的父類

Class class1 = Class.forName("MyTest");
Class superClass = class1.getSuperclass();


可以得到 superclass 對象其實就是一個 Class 類的實例,所以你可以繼續在這個對象上進行反射操作。

6、實現的接口
可以通過如下方式獲取指定類所實現的接口集合:

Class class1 = Class.forName("MyTest");
Class[] interfaces = class1.getInterfaces();

由於一個類可以實現多個接口,因此 getInterfaces(); 方法返回一個 Class 數組,在 Java 中接口同樣有對應的 Class 對象。 注意:getInterfaces() 方法僅僅只返回當前類所實現的接口。當前類的父類如果實現了接口,這些接口是不會在返回的 Class 集合中的,儘管實際上當前類其實已經實現了父類接口。

7、構造器

//獲取每一個聲明爲公有的(Public)構造方法
Class class1 = Class.forName("MyTest");
Constructor[] constructors = class1.getConstructors();

//獲取指定參數類型的構造函數
Constructor constructor = class1.getConstructor(new Class[]{String.class});

//獲取構造函數的參數
Constructor constructor = class1.getConstructor(new Class[]{String.class});
Class[] typeParameters = constructor.getParameterTypes();

//利用 Constructor 對象實例化一個類
Constructor constructor = class1.getConstructor(new Class[]{String.class});
MyTest myTest=(MyTest) constructor.newInstance("aa");

 


8、變量
使用 Java 反射機制你可以運行期檢查一個類的變量信息(成員變量)或者獲取或者設置變量的值。通過使用 java.lang.reflect.Field 類就可以實現上述功能

//Field 對象數組包含了指定類中聲明爲公有的(public)的所有變量集合
Class class1 = Class.forName("MyTest");
Field[] fields = class1.getFields();

//獲取指定的變量
Field field = class1.getField("device");

//獲取變量名稱
System.out.println(field.getName());

//變量類型
System.out.println(field.getType());

//獲取或設置(get/set)變量值,如果變量是靜態變量的話(public static)那麼在調用 Field.get()/Field.set()
//方法的時候傳入 null 做爲參數而不用傳遞擁有該變量的類的實例。
Class class1 = Class.forName("MyTest");
Field field = class1.getField("device");
MyTest myTest = (MyTest) class1.newInstance();
Object fieldObject = field.get(myTest);
field.set(myTest, fieldObject);

//訪問私有變量
//Class.getField(String name)和 Class.getFields()只會返回公有的變量,無法獲取私有變量
//要想獲取私有變量你可以調用 Class.getDeclaredField(String name)方法或者 Class.getDeclaredFields()方法
Class class1 = Class.forName("MyTest");
Field field = class1.getDeclaredField("privateString");

field.setAccessible(true);

MyTest myTest = (MyTest) class1.newInstance();
String fileText = (String) field.get(myTest);

 

field.setAccessible(true)這行代碼,通過調用 setAccessible()方法會關閉指定類 Field 實例的反射訪問檢查,這行代碼執行之後不論是私有的、受保護的以及包訪問的作用域,你都可以在任何地方訪問,即使你不在他的訪問權限作用域之內。但是你如果你用一般代碼來訪問這些不在你權限作用域之內的代碼依然是不可以的,在編譯的時候就會報錯。

9、方法

//Method 對象數組包含了指定類中聲明爲公有的(public)的所有變量集合
Class class1 = Class.forName("MyTest");
Method[] methods = class1.getMethods();

//知道你要調用方法的具體參數類型,你就可以直接通過參數類型來獲取指定的方法
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("doSomething",new Class[]{String.class});

//方法參數以及返回類型
Class[] parameterTypes = method.getParameterTypes();
Class returnType = method.getReturnType();

//通過 Method 對象調用方法
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("doSomething", new Class[]{String.class});
MyTest myTest = (MyTest) class1.newInstance();
Object returnValue = method.invoke(myTest,"parameter");

//訪問私有方法
//訪問一個私有方法你需要調用 Class.getDeclaredMethod(String name, Class[]
//parameterTypes)或者 Class.getDeclaredMethods() 方法。 Class.getMethod(String name, Class[]
//parameterTypes)和 Class.getMethods()方法,只會返回公有的方法,無法獲取私有方法
Class class1 = Class.forName("MyTest");
Method method = class1.getDeclaredMethod("doSomething", null);
method.setAccessible(true);
MyTest myTest = (MyTest) class1.newInstance();
String returnValue = (String) method.invoke(myTest,null);

Method.invoke(Object target, Object … parameters)方法第一個參數是你要調用方法的對象,如果是一個靜態方法調用的話則可以用 null 代替指定對象作爲 invoke()的參數,在上面這個例子中,如果 doSomething 不是靜態方法的話,你就要傳入有效的 MyObject 實例而不是 null。 Method.invoke(Object target, Object … parameters)方法的第二個參數是一個可變參數列表,但是你必須要傳入與你要調用方法的形參一一對應的實參。就像上個例子那樣,方法需要 String 類型的參數,那我們必須要傳入一個字符串。

10、註解
註解是 Java 5 的一個新特性。註解是插入你代碼中的一種註釋或者說是一種元數據(meta data)。這些註解信息可以在編譯期使用預編譯工具進行處理(pre-compiler tools),也可以在運行期使用 Java 反射機制進行處理,下面定義一個MyAnnotation註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)

public @interface MyAnnotation {
public String name();
public String value();
}

在 interface 前面的@符號表名這是一個註解,一旦你定義了一個註解之後你就可以將其應用到你的代碼中,就像之前我們的那個例子那樣。 在註解定義中的兩個指示@Retention(RetentionPolicy.RUNTIME)和@Target(ElementType.TYPE),說明了這個註解該如何使用。 @Retention(RetentionPolicy.RUNTIME)表示這個註解可以在運行期通過反射訪問。如果你沒有在註解定義的時候使用這個指示那麼這個註解的信息不會保留到運行期,這樣反射就無法獲取它的信息。 @Target(ElementType.TYPE) 表示這個註解只能用在類型上面(比如類跟接口)。你同樣可以把Type改爲Field或者Method,或者你可以不用這個指示,這樣的話你的註解在類,方法和變量上就都可以使用了。

//獲取類註解
Class class1 = Class.forName("MyTest");
Annotation annotation = class1.getAnnotation(MyAnnotation.class);

//方法註解
Method method = class1.getMethod("doSomething", null);
Annotation[] annotations = method.getAnnotations();
Annotation annotation1 = method.getAnnotation(MyAnnotation.class);

//參數註解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Class[] parameterTypes = method.getParameterTypes();
int i=0;
for(Annotation[] annotations : parameterAnnotations){
Class parameterType = parameterTypes[i++];
for(Annotation annotation : annotations){
if(annotation instanceof MyAnnotation){
MyAnnotation myAnnotation = (MyAnnotation) annotation;
System.out.println("param: " + parameterType.getName());
System.out.println("name : " + myAnnotation.name());
System.out.println("value: " + myAnnotation.value());
}
}
}

//變量註解
Annotation[] annotations = field.getDeclaredAnnotations();
Annotation annotation = field.getAnnotation(MyAnnotation.class);


11、泛型
我常常在一些文章以及論壇中讀到說 Java 泛型信息在編譯期被擦除所以你無法在運行期獲得有關泛型的信息。其實這種說法並不完全正確的,在一些情況下是可以在運行期獲取到泛型的信息。

Java 在編譯時會在字節碼裏指令集之外的地方保留部分泛型信息,泛型接口、類、方法定義上的所有泛型、成員變量聲明處的泛型都會被保留類型信息,其他地方的泛型信息都會被擦除。

Java 的泛型機制雖然在編譯期間進行了擦除,但是在編譯 Java 源代碼成 class 文件中還是保存了泛型相關的信息,這些信息被保存在 class 字節碼的常量池中,使用了泛型的代碼處會生成一個 signature 簽名字段,通過簽名 signature 字段指明這個常量池的地址,JDK 提供了方法去讀取這些泛型信息的方法,然後再借助反射就可以獲得泛型參數的具體類型

所以獲取泛型參數類型的實質就是通過 Class 類的 getGenericSuperClass() 方法返回一個 ParameterizedType 對象(對於 Object、接口和原始類型返回 null,對於數組 class 返回 Object.class),ParameterizedType 表示帶有泛型參數類型的 Java 類型,JDK1.5 引入泛型後 Java 中所有的 Class 都實現了 Type 接口,ParameterizedType 繼承了 Type 接口,所有包含泛型的 Class 類都會自動實現這個接口。

泛型的擦除機制實際上擦除的是除結構化信息外的所有東西(結構化信息指與類結構相關的信息,而不是與程序執行流程有關的,即與類及其字段和方法的類型參數相關的元數據都會被保留下來通過反射獲取到)。

public class MyTest {
    public List<String> myList = new ArrayList<>();

    public List<String> getMyList(List<String> myList) {
        return myList;
    }
 }
    • 泛型方法返回類型
      如果你獲得了 java.lang.reflect.Method 對象,那麼你就可以獲取到這個方法的泛型返回類型信息
Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("getMyList", List.class);
Type returnType = method.getGenericReturnType();
if(returnType instanceof ParameterizedType){
   ParameterizedType type = (ParameterizedType) returnType;
   Type[] typeArguments = type.getActualTypeArguments();
   for(Type typeArgument : typeArguments){
      System.out.println((Class) typeArgument);
   }
}

輸出:class java.lang.String

泛型方法參數類型

Class class1 = Class.forName("MyTest");
Method method = class1.getMethod("getMyList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for(Type genericParameterType : genericParameterTypes){
if(genericParameterType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericParameterType;
Type[] parameterArgTypes = aType.getActualTypeArguments();
for(Type parameterArgType : parameterArgTypes){
System.out.println((Class) parameterArgType);
}
}
}


輸出:class java.lang.String

泛型變量類型

Class class1 = Class.forName("MyTest");
Field field = class1.getField("myList");
Type genericFieldType = field.getGenericType();
if(genericFieldType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericFieldType;
Type[] fieldArgTypes = aType.getActualTypeArguments();
for(Type fieldArgType : fieldArgTypes){
Class fieldArgClass = (Class) fieldArgType;
System.out.println((Class) fieldArgType);
}
}


輸出:class java.lang.String

12、數組

Java 反射機制通過 java.lang.reflect.Array 這個類來處理數組。

//創建一個數組
int[] intArray = (int[]) Array.newInstance(int.class, 3);

//訪問數組
Array.set(intArray, 0, 123);
Array.set(intArray, 1, 456);
Array.set(intArray, 2, 789);

//獲取數組的 Class 對象
Class intArray = Class.forName("[I");

//獲取數組的成員類型
Class class1 = Class.forName("[I");
Class classType = class1.getComponentType();
System.out.println(classType);

13、動態代理
利用Java反射機制你可以在運行期動態的創建接口的實現。 java.lang.reflect.Proxy 類就可以實現這一功能。這個類的名字(譯者注:Proxy 意思爲代理)就是爲什麼把動態接口實現叫做動態代理。動態的代理的用途十分廣泛,比如數據庫連接和事物管理(transaction management)還有單元測試時用到的動態 mock 對象以及 AOP 中的方法攔截功能等等都使用到了動態代理。

創建代理
可以通過使用 Proxy.newProxyInstance()方法創建動態代理。 newProxyInstance()方法有三個參數: 1、類加載器(ClassLoader)用來加載動態代理類。 2、一個要實現的接口的數組。 3、一個 InvocationHandler 把所有方法的調用都轉到代理上。

public interface InvocationHandler{
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

public class MyInvocationHandler implements InvocationHandler{

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {

}
}

invoke()方法中的 Method 對象參數代表了被動態代理的接口中要調用的方法,從這個 method 對象中你可以獲取到這個方法名字,方法的參數,參數類型等等信息。

Object 數組參數包含了被動態代理的方法需要的方法參數。注意:原生數據類型(如int,long等等)方法參數傳入等價的包裝對象(如Integer, Long等等)。

InvocationHandler handler = new MyInvocationHandler();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[] { MyInterface.class },
handler);

執行完這段代碼之後,變量 proxy 包含一個 MyInterface 接口的的動態實現。所有對 proxy 的調用都被轉向到實現了 InvocationHandler 接口的 handler 上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章