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 上。

posted @ 2021-07-18 23:13  你的雷哥  阅读(1472)  评论(0编辑  收藏  举报