Java(五)反射、克隆、泛型、语法糖、注解

文章目录

  本系列文章:
    Java(一)数据类型、变量类型、修饰符、运算符
    Java(二)分支循环、数组、字符串、方法
    Java(三)面向对象、封装继承多态、重写和重载、枚举
    Java(四)内部类、包装类、异常、日期
    Java(五)反射、克隆、泛型、语法糖、元注解
    Java(六)IO、NIO、四种引用
    Java(七)JDK1.8新特性
    Java(八)JDK1.17新特性

一、反射

1.1 反射是什么

  在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取类的信息以及动态调用对象的方法的功能称为Java语言的反射机制
  可以看出,反射是在程序运行时获取类型信息的,而用来表示运行时类型信息对应类就是Class类。实际上在Java中每个类都对应着一个Class对象,每编写并且编译一个新创建的类(.java文件)就会产生一个对应Class对象,并且这个Class对象会被保存在同名.class文件里。
  当new一个新对象或者引用静态成员变量时,Java虚拟机中的类加载器子系统会将对应Class对象加载到JVM中,然后JVM再根据这个类型信息相关的Class对象创建我们需要实例对象或者提供静态变量的引用值。
  需要注意的是:手动编写的每个Class类,无论创建多少个实例对象,在JVM中都只有一个Class对象,即在内存中每个类有且只有一个相对应的Class对象
  Class类的特点:

  1. Class类也是类的一种,与class关键字是不一样的。
  2. 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且对象保存在同名.class的文件中(字节码文件)。
  3. 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
  4. Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载。
  5. Class类的对象作用是运行时提供或获得某个对象的类型信息。

1.2 反射机制的相关类*

  与Java反射相关的类:

类名用途
Class类代表类的实体,可以获取类的属性,方法等信息
Field类代表类的成员变量(成员变量也称类的属性),可以用来获取和设置类之中的属性值
Method类代表类的方法,以用来获取类中的方法信息或者执行方法
Constructor类代表类的构造方法
1.2.1 Class类

  Class代表类的实体,在运行的Java应用程序中表示类和接口。Class类的常用方法:

	//获取类的加载器
	public ClassLoader getClassLoader()
	//返回一个数组,数组中包含该类中所有公共类和接口类的对象
	public Class<?>[] getClasses()
	//根据类名返回类的对象
	public static Class<?> forName(String className) throws ClassNotFoundException
	//获取类的完整路径名字
	public String getName()
	//创建类的实例
	public T newInstance()
	//获取类的包
	public Package getPackage()
	//获取类的名字
	public String getSimpleName()
	//获得某个公有的属性对象
	public Field getField(String name)
	//获得所有公有的属性对象
	public Field[] getFields() throws SecurityException
	//获得某个属性对象
	public Field getDeclaredField(String name) throws NoSuchFieldException, SecurityException
	//获得所有属性对象
	public Field[] getDeclaredFields() throws SecurityException
	//获得该类中与参数类型匹配的公有构造方法
	public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
	//获得该类的所有公有构造方法
	public Constructor<?>[] getConstructors() throws SecurityException
	//获得该类中与参数类型相匹配的构造方法
	public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
	//获得该类所有构造方法
	public Constructor<?>[] getDeclaredConstructors() throws SecurityException
	//获得该类某个公有的方法
	public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
	//获得该类所有公有的方法
	public Method[] getMethods() throws SecurityException
	//获得该类某个方法
	public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
	//获得该类所有方法
	public Method[] getDeclaredMethods() throws SecurityException 
1.2.2 Field类

  Field代表类的成员变量(成员变量也称为类的属性)。Field类的常用方法:

	//获取obj中对应的属性值
	public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException
	//设置obj中对应的属性值
	public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException
1.2.3 Method类

  Method代表类的方法。 Method类的常用方法:

	//传递object对象及参数调用该对象对应的方法
	public Object invoke(Object obj, Object... args) 
		throws IllegalAccessException, IllegalArgumentException,InvocationTargetException
1.2.4 Constructor类

  Constructor代表类的构造方法。 Method类的常用方法:

	//根据传递的参数,创建类的对象
	public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException

1.3 反射的基本用法

  一个简单的实例类:

public class Person {
	
	private String name;
	private int age;
	public Person(){
		
	}
	public Person(String name,int age){
		this.name = name;
		this.age = age;
	}
	public void sayHello(String str){
		System.out.println(name+" say hello");
	}
}
1.3.1 获取Class对象*

  使用反射获取Class对象时,三种方法示例:

  1. Class.forName("类的全路径")【最常用】
  2. Person.class。
  3. new Person().getClass()。

  如果是一个基本数据类型,那么可以通过Type的方式来获取Class对象,示例:Class type = Integer.TYPE。
  获取Class对象的3种方法示例:

	Class pClass1 = Class.forName("Basic.Person");
	Class pClass2 = Person.class;
	Class pClass3 = new Person().getClass();  

  这三种方式获取到的Class对象是同一个。

  在实际使用中,常用第一个。第二种需要导入类的包,依赖性强,不导包就抛编译错误。第三种方法都可以直接创建对象,自然能获取到对象中的属性和方法。

  在使用Class.forName和new Person().getClass()方式获取Class对象时,都会导致静态属性被初始化,而且只会执行一次。为了验证,可以将Person修改:

public class Person {
	
	static String name;
	private int age;
	
	static {
	    System.out.println("初始化name");
	    name = "小明";
	}
	
	public Person(){
	}
	public Person(String name,int age){
		this.name = name;
		this.age = age;
	}
	public void sayHello(String str){
		System.out.println(name+" say hello");
	}
}

  再次运行上述测试代码,会有如下打印:

初始化name

1.3.2 获取构造方法及创建对象*

  为了模拟各种构造方法,我们先将Person类稍微改一下:

package Basic;

public class Person {
			
	//(默认的构造方法)
	Person(String str){
		System.out.println("(默认)的构造方法 s = " + str);
	}
		
	//无参构造方法
	public Person(){
		System.out.println("调用了公有、无参构造方法");
	}
		
	//有一个参数的构造方法
	public Person(char name){
		System.out.println("姓名:" + name);
	}
		
	//有多个参数的构造方法
	public Person(String name ,int age){
		System.out.println("姓名:"+name+"年龄:"+ age);
	}
		
	//受保护的构造方法
	protected Person(boolean n){
		System.out.println("受保护的构造方法 n = " + n);
	}
		
	//私有构造方法
	private Person(int age){
		System.out.println("私有的构造方法   年龄:"+ age);
	}
}

  使用反射机制创建对象时,分为三步:

  1. 先获取Class对象(一般通过Class.forName方法)。
  2. 再获取构造器对象(通过调用getConstructor、getDeclaredConstructor等方法)。
  3. 获取想要的实体类对象(通过调用构造器对象的newInstance方法)。

  示例:

package Basic;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class ReflectTest {
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException  {
		//1.加载Class对象
		Class clazz = Class.forName("Basic.Person");
		
		//2.获取所有公有构造方法
		System.out.println("所有公有构造方法:");
		Constructor[] conArray = clazz.getConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
		
		System.out.println("所有的构造方法(包括:私有、受保护、默认、公有):");
		conArray = clazz.getDeclaredConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
		
		System.out.println("获取公有、无参的构造方法:");
		Constructor con = clazz.getConstructor(null);
		//1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
		//2>、返回的是描述这个无参构造函数的类对象。
		System.out.println("con = " + con);
		//调用构造方法
		Object obj = con.newInstance();
		
		//根据参数来获取对象的构造方法
		System.out.println("获取私有构造方法,并调用:");
		con = clazz.getDeclaredConstructor(int.class);
		System.out.println(con);
		//调用构造方法
		con.setAccessible(true);
		obj = con.newInstance(80);
	}
}

  测试结果为:

所有公有构造方法:
public Basic.Person(java.lang.String,int)
public Basic.Person(char)
public Basic.Person()
所有的构造方法(包括:私有、受保护、默认、公有):
private Basic.Person(int)
protected Basic.Person(boolean)
public Basic.Person(java.lang.String,int)
public Basic.Person(char)
public Basic.Person()
Basic.Person(java.lang.String)
获取公有、无参的构造方法:
con = public Basic.Person()
调用了公有、无参构造方法
获取私有构造方法,并调用:
private Basic.Person(int)
私有的构造方法 年龄:80

  在获取了构造器对象之后,就可以继续调用newInstance方法(newInstance方法的参数就是创建对象时所需要传入的初始值)创建实例类的对象。

1.3.3 操作成员变量*

  使用反射机制操作成员变量时,分为三步:

  1. 先获取Class对象(一般通过Class.forName方法)。
  2. 再获取属性(Field)对象(通过调用getField、getDeclaredField等方法)。
  3. 调用属性对象的set方法修改属性值

   此时再改一下Person类:

public class Person {
	
	public String name;
	protected int age;
	char sex;
	private String phoneNum;
	
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + ", sex=" + sex
				+ ", phoneNum=" + phoneNum + "]";
	}	
}

  示例:

package Basic;

import java.lang.reflect.Field;

public class ReflectTest {

	public static void main(String[] args) throws Exception {
		
		Class perClass = Class.forName("Basic.Person");
		/*获取字段*/
		System.out.println("获取所有公有的字段:");
		Field[] fieldArray = perClass.getFields();
		for(Field f : fieldArray){
			System.out.println(f);
		}
		System.out.println("获取所有的字段(包括私有、受保护、默认的):");
		fieldArray = perClass.getDeclaredFields();
		for(Field f : fieldArray){
			System.out.println(f);
		}
		System.out.println("获取公有字段并调用:");
		Field f = perClass.getField("name");
		System.out.println(f);
		//获取一个对象
		Object obj = perClass.getConstructor().newInstance();
		//为字段设置值
		f.set(obj, "Jack");
		//验证
		Person per = (Person)obj;
		System.out.println("验证姓名:" + per.name);
		
		
		System.out.println("获取私有字段并调用");
		f = perClass.getDeclaredField("phoneNum");
		System.out.println(f);
		//解除私有限定,设置为true后,就可以访问private属性了
		f.setAccessible(true);
		f.set(obj, "12345678910");
		System.out.println("验证电话:" + per);
	}
}

  测试结果为:

获取所有公有的字段:
public java.lang.String Basic.Person.name
获取所有的字段(包括私有、受保护、默认的):
public java.lang.String Basic.Person.name
protected int Basic.Person.age
char Basic.Person.sex
private java.lang.String Basic.Person.phoneNum
获取公有字段并调用:
public java.lang.String Basic.Person.name
验证姓名:Jack
获取私有字段并调用
private java.lang.String Basic.Person.phoneNum
验证电话:Student [name=Jack, age=0, sex=

  有上面代码也可以看出,获取到某个属性对象后,修改改属性的方式为:
f.set(obj, value),其中f为属性对象,obj为实体类对象,value为属性值。

   需要注意的是:

getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (只能获取到private的字段,并且要想访问该private字段的值,需要加上setAccessible(true))。

1.3.4 调用成员方法*

  使用反射机制调用成员方法时,分为三步:

  1. 先获取Class对象(一般通过Class.forName方法)。
  2. 再获取方法对象(通过调用getMethod、getDeclaredMethod等方法)。
  3. 调用方法对象的invoke方法

   此时再改一下Person类:

public class Person {
	
	public void methodOne(String s){
		System.out.println("调用了public methodOne方法");
	}
	protected void methodTwo(){
		System.out.println("调用了protected methodTwo方法");
	}
	void methodThree(){
		System.out.println("调用了default methodThree方法");
	}
	private String methodFour(int age){
		System.out.println("调用了private methodFour方法");
		return "1234";
	}
}

  此时测试代码:

public class ReflectTest {

	public static void main(String[] args) throws Exception {
		Class perClass = Class.forName("Basic.Person");
		//获取该对象的普通方法,包含的方法范围是当前对象及父类对象的
		//所有公共方法,比如Object中的方法
		System.out.println("获取所有的public方法:");
		perClass.getMethods();
		Method[] methodArray = perClass.getMethods();
		for(Method m : methodArray){
			System.out.println(m);
		}
		
		System.out.println("获取所有的方法:");
		methodArray = perClass.getDeclaredMethods();
		for(Method m : methodArray){
			System.out.println(m);
		}
		
		System.out.println("获取public methodOne()方法:");
		Method m = perClass.getMethod("methodOne", String.class);
		System.out.println(m);
		
		Object obj = perClass.getConstructor().newInstance();
		m.invoke(obj, "Jack");
				
		System.out.println("获取private methodFour()方法:");
		m = perClass.getDeclaredMethod("methodFour", int.class);
		System.out.println(m);
		//解除私有限定,可以访问private方法
		m.setAccessible(true);
		Object result = m.invoke(obj, 20);
		System.out.println("返回值:" + result);
				
	}
}

  测试结果为:

获取所有的public方法:
public void Basic.Person.methodOne(java.lang.String)
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 void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
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()
获取所有的方法:
public void Basic.Person.methodOne(java.lang.String)
private java.lang.String Basic.Person.methodFour(int)
protected void Basic.Person.methodTwo()
void Basic.Person.methodThree()
获取public methodOne()方法:
public void Basic.Person.methodOne(java.lang.String)
调用了public methodOne方法
获取private methodFour()方法:
private java.lang.String Basic.Person.methodFour(int)
调用了private methodFour方法
返回值:1234

1.4 反射的使用场景*

  • 1、框架
      Hibernate、Spring等框架中都有使用反射技术。比如,在Hibernate中对象持久化就需要将对象转化为实体类,然后存储到数据库中,就需要通过反射来动态创建实体类对象、获取类的属性和方法等。
  • 2、动态代理
      在Java中,代理模式非常常见。如果要在程序运行期间动态的创建代理对象,就需要通过反射获取类、方法等信息。
  • 3、注解处理器(开发用到)
      注解处理器正是通过反射实现的。通过反射技术可以解析注解信息,并执行相应的操作。
  • 4、动态的加载类
      Java中的ClassLoader就是通过反射实现的。ClassLoader可以在程序运行期间动态的加载类,从而扩展程序功能。
      在程序运行时,需要根据用户输入的类名或配置文件中的类名来动态加载类,以便进行扩展、插件化等操作。
  • 5、用于对象的拷贝
      在实际项目中,当两个对象有较多相似的属性时,就可以写一个BeanUtils之类的公共方法,用于实现对象属性值的拷贝。在实现这样的功能时,就要用到反射了,用于调用对象的get和set方法。当然现有框架也有类似的功能:BeanUtils.copyProperties。
  • 6、用于单测代码中注入成员属性值(开发用到)
      在实际项目中,还有一个场景是常常用到反射的,那就是在写单测代码时,往mock的对象中注入private修饰的变量或对象。使用spring自带的反射工具类(ReflectionTestUtils)即可,示例:
	ReflectionTestUtils.setField("xxxServiceImpl","xxxSwitch","ON");

1.5 反射的相关问题

1.5.1 Java反射创建对象效率高还是通过new创建对象的效率高

  通过new创建对象的效率比较高。通过反射时,先找查找类资源,使用类加载器创建,过程比较繁琐,所以效率较低。

1.5.2 反射机制的优缺点
  • 1、优点
      1)能够运行时动态获取类的实例,提高灵活性;
      2)与动态编译结合,示例:
	Class.forName('com.mysql.jdbc.Driver.class');//加载MySQL的驱动类
  • 2、缺点
      1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析;

解决方案:
  1、通过setAccessible(true)取消Java语言访问检查,来提升反射速度;
  2、多次创建一个类的实例时,有缓存会快很多;
  3、ReflectASM工具类,通过字节码生成的方式加快反射速度。ReflectASM是一个非常小的高性能反射库,当“修改/查询字段”、“调用方法”、“创建实例”时,会用ASM技术字节码技术动态生成一个Access Class,从而不使用传统Java反射,来提高效率。

  2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)。其实现就是通过setAccessible(true)来实现的,可以在Field和Method对象上调用该方法,用于访问private变量和调用private方法。

  • 3、ReflectASM
      ReflectASM有4个核心类,这4个核心类的用法和名称去掉Access后的Java中原生的反射类用法类似。
      AccessClassLoader:自定义ClassLoader,加载动态生成的AccessClass。
      ConstructorAccess:构造器AccessClass,用以加速创建对象。
      FieldAccess:字段AccessClass:用以加速访问字段。
      MethodAccess:用以加速访问方法。
1.5.3 反射机制的作用

  1)在运行时判定任意一个对象所属的类;
  2)在运行时构造任意一个类的对象;
  3)在运行时判定任意一个类所具有的成员变量和方法;
  4)在运行时调用任意一个对象的方法;
  5)生成动态代理。

1.5.4 ClassLoader和Class.forName的区别

  ClassLoader和Class.forName都是用来加载类的,但是这两个方法一般却又在不同的场景使用。如classLoader一般是spring容器用来加载bean的时候使用的,而classForName一般我们都是在使用数据库驱动的时候会使用该方法。
  Class.forName:

    public static Class<?> forName(String className)  throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }

    /** Called after security checks have been made. */
    private static native Class forName0(String name, boolean initialize,
					    ClassLoader loader)
	throws ClassNotFoundException;

  forName0方法的第二个参数initaline表示是否对加载的类进行初始化。我们的classforname传入的是true。所以classforname会对类进行初始化操作。
  ClassLoader:

    public Class<?> loadClass(String name) throws ClassNotFoundException {
	    return loadClass(name, false);
    }

     /*
     * @param  name
     *         The <a href="#name">binary name</a> of the class
     *
     * @param  resolve
     *         If <tt>true</tt> then resolve the class
     *
     * @return  The resulting <tt>Class</tt> object
     *
     * @throws  ClassNotFoundException
     *          If the class could not be found
     */
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
	    // First, check if the class has already been loaded
	    Class c = findLoadedClass(name);
	    if (c == null) {
	        try {
		        if (parent != null) {
		            c = parent.loadClass(name, false);
		        } else {
		            c = findBootstrapClassOrNull(name);
		        }
	         } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
             }
             if (c == null) {
	         // If still not found, then invoke findClass in order
	         // to find the class.
	         c = findClass(name);
	     }
	 }
	 if (resolve) {
	    resolveClass(c);
	 }
	 return c;
   }

  可以看出,如果传的第2个参数为true,则初始化类。而在原代码中return loadClass(name, false);传入的是false,所以loaderClass则对类不进行初始化。
  因此,两者的区别在在于一个会初始化操作,一个不会初始化操作,而在类加载过程中,初始化的作用主要是执行静态代码块。即Class.forName会执行静态代码块,而ClassLoader.loadClass不会执行静态代码块

二、创建对象的四种方式

  对象实例化的四种方式:1、通过new关键字创建对象;2、通过克隆创建对象;3、通过反射创建对象;4、通过序列化创建对象。

2.1 通过克隆创建对象

   克隆有两种:深克隆与浅克隆,主要区别在于是否支持引用类型的成员变量的复制,浅克隆是复制引用类型的成员变量的,即被克隆对象与原有对象指向同一个引用对象,深克隆相反,指向不同的引用对象

2.1.1 浅克隆

  创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
  使用clone方法的话,实体类需要实现Cloneable接口并重写clone方法(直接调用父类的clone方法即可)
  示例:

/*实体类*/
public class Person implements Cloneable{
	String name;
	int age;
	
	public Person(){	
	}
	
	public Person(String name,int age) {
		this.name = name;
	    this.age = age;
	}

	public String getName() {
	    return name;
	}

	public int getAge() {
	    return age;
	}

	public void setName(String name) {
	    this.name = name;
	}

	public void setAge(int age) {
	    this.age = age;
	}

	@Override
	protected Object clone() throws CloneNotSupportedException {
	    return super.clone();
	}

	@Override
	public String toString() {
	    String s = "姓名是:"+this.name+",年龄是:"+this.age;
	    return s;
	 }
}
/*测试类*/
public class ObjectTest {
	public static void main(String[] args) throws CloneNotSupportedException  {
		//new关键字创建的对象
		Person p1 = new Person("李元芳", 30);  
        System.out.println(p1);  //姓名是:李元芳,年龄是:30
        Person p2 = null;
        
        //调用对象的clone()方法创建对象
        p2 = (Person) p1.clone();
        System.out.println(p2);  //姓名是:李元芳,年龄是:30
        p2.setAge(51);
        p2.setName("狄仁杰");
        System.out.println(p2);  //姓名是:狄仁杰,年龄是:51
	}
}
2.1.2 深克隆

  深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象(即对象的引用所指向的对象也要克隆)。示例:

/*Person中引用的实体类*/
class Address implements Cloneable {  
    private String add;  
  
    public String getAdd() { return add; }  
  	public void setAdd(String add) { this.add = add; }  
      
    @Override  
    public Object clone() {  
        Address addr = null;  
        try{  
            addr = (Address)super.clone();  
        }catch(CloneNotSupportedException e) {  
        	e.printStackTrace();  
        }  
        return addr;  
    }   
}

public class Person implements Cloneable{
	String name;
	int age;
	Address addr;
	
	public Person(String name,Integer age) {
		this.name = name;
	    this.age = age;
	}
	
	public void setName(String name){ this.name = name; }
	public void setAge(int age){ this.age = age; }
	public Address getAddr() { return addr; }  
    public void setAddr(Address addr) { this.addr = addr; }  
	public String getName(){ return name; }
	public int getAge(){ return age; }
	
    @Override  
    public Object clone() {  
    	Person per = null;  
        try{  
        	per = (Person)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        //深度复制
        per.addr = (Address)addr.clone();     
        return per;  
    }
}

public class PersonTest {
	public static void main(String[] args) {
		Address addr = new Address(); 
		addr.setAdd("无锡市");  
		Person person1 = new Person("Jack",25);
		person1.setAge(26);
		person1.setAddr(addr);
		Person person2 = (Person)person1.clone();
		
        System.out.println("person1:" + person1.getAge()+ ",地址:" + person1.getAddr().getAdd());  
        System.out.println("person2:" + person2.getAge()+ ",地址:" + person2.getAddr().getAdd()); 
        
        person2.setAge(28);
        addr.setAdd("苏州市");  
        System.out.println("person1:" + person1.getAge()+ ",地址:" + person1.getAddr().getAdd());  
        System.out.println("person2:" + person2.getAge()+ ",地址:" + person2.getAddr().getAdd()); 
	}
}

  测试结果:

person1:26,地址:无锡市
person2:26,地址:无锡市
person1:26,地址:苏州市
person2:28,地址:无锡市

2.1.3 浅克隆和深克隆的比较*
  • 浅克隆
      在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。示例:

      实现浅克隆的方式:实现Cloneable接口,并重写clone方法即可。
  • 深克隆
      在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。示例:

      也就是说,可以通过重写clone方法的方式,来实现深克隆。在重写clone方法时,clone一份所引用的对象即可。

2.2 通过反射创建对象

  通过反射来创建对象的方式也有两种:通过Class对象和Constructor对象。

  • 1、Class对象的newInstance()
      使用Class对象的newInstance()方法来创建该Class对象对应类的实例,但是这种方法要求该Class对象对应的类有默认的空构造器。
      new是强类型校验,可以调用任何构造方法,在使用new操作时,这个类可以没被加载过。而Class类下的newInstance是弱类型,只能调用无参构造方法,如果没有默认构造方法,就抛出InstantiationException异常;如果此构造方法没有权限访问,就会爬出IllegalAccessException异常。
  • 2、调用Constructor对象的newInstance()
      先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建Class对象对应类的实例,通过这种方法可以选定构造方法创建实例。
      示例:
	//获取 Person 类的 Class 对象
	Class clazz=Class.forName("reflection.Person");
	//使用.newInstane 方法创建对象
	Person p=(Person) clazz.newInstance();
	//获取构造方法并创建对象
	Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
	//创建对象并设置属性
	Person p1=(Person) c.newInstance("李四","男",20);

2.3 序列化

2.3.1 序列化的实现*

  序列化是把内存中的对象转换为字节序列的过程,反序列化是把字节序列恢复为Java对象的过程。
  实现序列化功能的类需要实现Serializable接口。在序列化时,会用到IO流中的ObjectOutputStream和ObjectInputStream。如果对象没有实现Serializable接口,在序列化的时候会抛出NotSerializableException异常。
  使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会保存类中静态变量的状态
  如果要实现自定义序列化策略,可以通过在类中增加writeObject和readObject方法来实现
  序列化使用的场景:当Java对象需要在网络上传输或持久化到文件中时。

  对于不想进行序列化的变量,使用transient关键字修饰。transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复,transient变量的值被设为初始值,如int型的是0,对象型的是null
  transient只能修饰变量,不能修饰类和方法。

  利用序列化创建对象的示例:

/*实体类*/
public class Person implements Serializable{
	
	private static final long serialVersionUID = 1L;
	String name;
	int age;
	
	public Person(String name,Integer age) {
		this.name = name;
	    this.age = age;
	}


	@Override
	public String toString() {
	    String s = "姓名是:"+this.name+",年龄是:"+this.age;
	    return s;
	 }
}
/*测试类*/
    public static void main(String[] args) {
        String filename = "E:/serialize.txt";
        serialize(filename);
        reverse_serialize(filename);
    }

    /**
     * 序列化
     */
    public static void serialize(String filename) {
        try {
            OutputStream fos = new FileOutputStream(filename);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeInt(12345);
            oos.writeObject("Today");
            oos.writeObject(new Person("Jack",20));
            oos.close();
            fos.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*反序列化*/
    public static void reverse_serialize(String filename) {
        try {
            InputStream in = new FileInputStream(filename);
            ObjectInputStream obj = new ObjectInputStream(in);
            int i = obj.readInt();
            String str = (String)obj.readObject();
            Person person = (Person)obj.readObject();
            System.out.println(i);
            System.out.println(str);
            System.out.println(person);
            obj.close();
            in.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

  测试结果:

12345
Today
姓名是:Jack,年龄是:20

2.3.2 serialVersionUID*

  序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把JAVA对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
  但是,虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化ID是否一致,即serialVersionUID要求一致。
  在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即使InvalidCastException。这样做是为了保证安全,因为文件存储中的内容可能被篡改。
  当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,JAVA序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用。这种情况下,如果Class文件没有发生变化,就算再编译多次,serialVersionUID也不会变化的。但是,如果发生了变化,那么这个文件对应的serialVersionUID也就会发生变化。
  基于以上原理,如果我们一个类实现了Serializable接口,但是没有定义serialVersionUID,然后序列化。在序列化之后,由于某些原因,我们对该类做了变更,重新启动应用后,我们相对之前序列化过的对象进行反序列化的话就会报错。

  实现Serializable接口的类建议设置serialVersionUID字段值。如果不设置,那么每次运行时,编译器会根据类的内部实现,包括类名、接口明、方法和属性等自动生成serialVersionUID。如果类的源代码有修改,那么重新编译后serialVersionUID的取值可能会发生变化。因此实现Serializable接口的类一定要显式地定义serialVersionUID值。

2.4 复制对象的几种方式

  • 1、克隆
      实现Cloneable接口,重写clone()方法。
      即如果类中属性有自定义引用类型,只拷贝引用,不拷贝引用指向的对象,就是浅拷贝。
      如果对象的属性的Class也实现Cloneable接口,那么在克隆对象时也克隆属性,即深拷贝。
  • 2、使用现成框架进行复制(常用)
      如通过org.apache.commons中的工具类BeanUtils和PropertyUtils进行对象复制。

2.5 避免使用Java自带的序列化

  JDK提供的两个输入、输出流对象ObjectInputStream和ObjectOutputStream,它们只能对实现了Serializable接口的类的对象进行反序列化和序列化。
  JDK自带的序列化有一些缺点:

  • 1、无法跨语言
  • 2、易被攻击
      对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致hashCode方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。
  • 3、序列化后的流太大
      序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
  • 4、序列化性能太差

  由于以上种种原因,常常使用JSON框架来进行序列化与反序列化,比如FastJson。

三、泛型

  泛型,即“参数化类型”。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
  泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用Java泛型。
  泛型是JDK1.5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

3.1 泛型的特性

  泛型的特性是:只在编译阶段有效,示例:

	List<String> sList = new ArrayList<String>();
	List<Integer> iList = new ArrayList<Integer>();

	Class classStringArrayList = sList.getClass();
	Class classIntegerArrayList = iList.getClass();

	if(classStringArrayList.equals(classIntegerArrayList)){
	    System.out.println("泛型测试,类型相同");
	}

  这个测试程序的输出结果是:“泛型测试,类型相同”。这个例子说明了:在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息去掉,泛型型信息不会进入到运行时阶段。
  此处需要注意的有两点:

  • 1、泛型类型变量不能是基本数据类型
      比如,没有ArrayList< double >,只有ArrayList< Double >。因为当类型擦除后,ArrayList的原始类中的类型变量(T)替换为Object,但Object类型不能存储double值。
  • 2、泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

      上面的代码会编译报错,原因是:因为泛型类中的泛型参数的实例化是在定义泛型类型对象(例如 ArrayList< Integer > ) 的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,不能确定这个泛型参数是何种类型。

3.2 泛型的使用

  泛型可以用在类、接口和方法中。

3.2.1 泛型类

  在定义类的时候在类名的后面添加<E,K,V,A,B>,起到占位的作用,类中的方法的返回值类型和属性的类型都可以使用定义的泛型。
  泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
  这个应该是最常见的泛型的使用,比如上面提到的ArrayList:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  泛型类一般在使用时,会声明要存储的参数类型,示例:

	//示例1
	List<String> sList = new ArrayList<String>();
	List<Integer> iList = new ArrayList<Integer>();
	List<Long> lList = new ArrayList<Long>();
	sList.add("123");
	iList.add(1);
	lList.add(23L);
		
	//示例2
	//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
	//在实例化泛型类时,必须指定T的具体类型
	public class Generic<T>{
	  	private T key;
	  	public Generic(T key) {
	    	this.key = key;
	 	}
	  	public T getKey(){
	    	return key;
	 	}
	}
	//实例化泛型类
	Generic<Integer> genericInteger = new Generic<Integer>(123456);
3.2.2 泛型接口

  泛型接口的使用,与泛型类类似,比如List接口,看下List接口的定义:

	public interface List<E> extends Collection<E>

  在定义接口的时候,在接口的名称后添加泛型时,需注意:

1、子类在进行实现的时候,可以不填写泛型的类型,此时在创建具体的子类对象的时候才决定使用什么类型。
2、子类在实现泛型接口的时候,只在实现父类的接口的时候指定父类的泛型类型即可。

  示例:

public interface FanXingInterface<B> {
   public B test();
   public void test2(B b);
}

public  class  FanXingInterfaceSub implements FanXingInterface<String> {

    @Override
    public String test() {
        return null;
    }

    @Override
    public void test2(String string) {
    }
}

     FanXingInterfaceSub fxi = new FanXingInterfaceSub() ;
     fxi.test2("123");
3.2.3 泛型方法

  泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。在JDK源码中,泛型经常使用,比如HashMap.java中:

    final Node<K,V> getNode(int hash, Object key) 

  无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法
  对于一个static的方法,无法访问泛型类型的参数。如果static方法要使用泛型能力,就必须使其成为泛型方法。
  在定义方法的时候,指定方法的返回值和参数是自定义的占位符,可以是类名中的T,也可以是自定义的Q,只不过在使用Q的时候需要使用< Q>定义在返回值的前面。示例:

public class FanXingMethod<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public <Q> void show(Q q){
        System.out.println(q);
        System.out.println(t);
    }
}

        FanXingMethod<String> fxm = new FanXingMethod<>();
        fxm.setT("ttt");
        fxm.show(123);
        fxm.show(true);
3.2.4 类型擦除

  泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除
  如在代码中定义的List< Object>和List< String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。
  类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。

  Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。

3.3 泛型的上下边界

  泛型可以完全不指定所要存储的元素类型,也可以进行一些限制,比如:ArrayList <? extends ClassA> list 就表示list中存储的元素类型必须是ClassA类型或是其子类;也就是父类确定了,所有的子类都可以直接使用。

  ArrayList <? super ClassA> list 就表示list中存储的元素类型必须是ClassA类型或是其父类。也就是子类确定了,子类的所有父类都可以直接传递参数使用。

3.4 常见的泛型相关问题

3.4.1 使用泛型的好处是什么?

  泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码。
  泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现 ClassCastException。

3.4.2 Jav 的泛型是如何工作的

  泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除,并且在类型参数出现的地方插入强制转换的相关指令实现的。
  编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List< String>在运行时仅用一个List类型来表示。

3.4.3 Array中可以用泛型吗

  Array是数组,不是集合,不支持泛型。

3.4.4 什么是泛型中的限定通配符和非限定通配符 ?
  • 限定通配符
      限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它确保集合里存储的必须是T或T的子类。另一种是<? super T>它确保集合里存储的类型必须是T或T的父类。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
  • 非限定通配符
      另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
3.4.5 Java中List<?>和List< Object >之间的区别是什么?

  List<?> 是一个未知类型的 List,而List< Object >其实是任意类型的 List。你可以把 List< String >, List< Integer >赋值给 List<?>,却不能把 List< String >赋值给List< Object >。

3.4.6 List< String > 和原始类型List之间的区别?

  带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型 List 却不是类型安全的。你不能把String之外的任何其它类型的Object存入String类型的List中,而你可以把任何类型的对象存入原始List中。使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。例子:

	List listOfRawTypes = new ArrayList();
	listOfRawTypes.add("abc");
	listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常
	String item = (String) listOfRawTypes.get(0); //需要显式的类型转换
	item = (String) listOfRawTypes.get(1); //抛 ClassCastException,因为 Integer 不能被转换为 String
	List<String> listOfString = new ArrayList();
	listOfString.add("abcd");
	listOfString.add(1234); //编译错误,比在运行时抛异常要好
	item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换
3.4.7 集合不用泛型会造成什么问题?比如 List list = new ArrayList()

  集合中能添加不同类型的元素,示例:

    	List list = new ArrayList();
    	list.add(1);
    	list.add("123");
    	list.add(1.0);
    	list.forEach(System.out::println);

  结果:

1
123
1.0

四、语法糖

  语法糖就是对现有语法的一个封装。语法糖的存在主要是方便开发人员使用,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖

4.1 switch支持String和枚举

  从JDK1.7开始,switch语句开始支持String。Java中的swith自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较;对于char类型则是比较其ASCII码。对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型

  • switch(字符串)示例
public class SwitchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
        case "hello":
            System.out.println("hello");
            break;
        case "world":
            System.out.println("world");
            break;
        default:
            break;
        }
    }
}
  • switch(字符串)反编译后的内容
public class SwitchStringDemo {

    public static void main(String[] args) {
        String str;
        String string = str = "world";
        int n = -1;
        switch (string.hashCode()) {
            case 99162322: {
                if (!string.equals("hello")) break;
                n = 0;
                break;
            }
            case 113318802: {
                if (!string.equals("world")) break;
                n = 1;
            }
        }
        switch (n) {
            case 0: {
                System.out.println("hello");
                break;
            }
            case 1: {
                System.out.println("world");
                break;
            }
        }
    }
}

  字符串的switch是通过equals()和hashCode()方法来实现的,进行switch比较的实际是哈希值,然后通过使用equals方法比较进行安全检查。它的性能是不如使用枚举进行switch或者使用纯整数常量,也不是很差。

4.2 泛型和类型擦除

  虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。

4.3 自动装箱与拆箱

  自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱

  • 自动装箱
	//原始代码
	public static void main(String[] args) {
	    int i = 10;
	    Integer n = i;
	}

	//反编译后的代码
	public static void main(String args[]) {
	    int i = 10;
	    Integer n = Integer.valueOf(i);
	}
  • 自动拆箱
      原始代码:
	//原始代码
	public static void main(String[] args) {
	    Integer i = 10;
	    int n = i;
	}

	//反编译后的代码
	public static void main(String args[]) {
	    Integer i = Integer.valueOf(10);
	    int n = i.intValue();
	}

  由上面内容可知,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。

4.4 枚举

  JDK1.5中引入了枚举enum,enum就和class一样,只是一个关键字,他并不是一个类。
  简单的写一个枚举:

	public enum T {
	    SPRING,SUMMER;
	}

  反编译后的代码:

	public final class T extends Enum<T> {
	    public static final /* enum */ T SPRING = new T("SPRING", 0);
	    public static final /* enum */ T SUMMER = new T("SUMMER", 1);
	    private static final /* synthetic */ T[] $VALUES;
	
	    public static T[] values() {
	        return (T[])$VALUES.clone();
	    }
	
	    public static T valueOf(String name) {
	        return Enum.valueOf(T.class, name);
	    }
	
	    private T(String string, int n) {
	        super(string, n);
	    }
	
	    static {
	        $VALUES = new T[]{SPRING, SUMMER};
	    }
	}

  由上面代码可以看出:使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

4.5 内部类

  内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念。比如outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。

4.6 增强for循环

  也就是常见的for-each循环。

4.7 try-with-resource语句

  对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。关闭资源的常用方式就是在finally块里释放,即调用close方法。
  从JDK1.7开始,Java提供了一种更好的方式关闭资源,即使用try-with-resources语句,示例:

	public static void main(String args[]) {
	    try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hello.xml"))) {
	        String line;
	        while ((line = br.readLine()) != null) {
	            System.out.println(line);
	        }
	    } catch (IOException e) {
	        // handle exception
	    }
	}

4.8 语法糖可能出问题的场景*

4.8.1 当泛型遇到重载
	public class GenericTypes {
	
	    public static void method(List<String> list) {  
	        System.out.println("invoke method(List<String> list)");  
	    }  
	
	    public static void method(List<Integer> list) {  
	        System.out.println("invoke method(List<Integer> list)");  
	    }  
	}  

  上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List,另一个是List,但是,这段代码是编译通不过的。因为参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。

4.8.2 当泛型内包含静态变量
	public class StaticTest{
	    public static void main(String[] args) {
	        GT<Integer> gti = new GT<Integer>();
	        gti.var = 1;
	        GT<String> gts = new GT<String>();
	        gts.var = 2;
	        System.out.println(gti.var);
	    }
	
	    class GT<T> {
	        public static int var = 0;
	
	        public void nothing(T x) {
	        }
	    }
	}

  这段代码编译都无法通过,因为泛型里面不能引用静态变量。由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。

五、注解

  Annotation(注解)是Java提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的Annotatio对象,然后通过该Annotation对象来获取注解中的元数据信息。
  作用在代码里的注解有3个,分别是:

@Override:重写标记。
@Deprecated:用此注解的代码已经过时,不推荐使用。
@SuppressWarnings:该注解的作用是忽略编译器的警告。

  JDK中注解相关的类和接口都定义在java.lang.annotation包中。注解的定义和我们常见的类、接口类似,只是注解使用@interface来定义,如下定义一个名称为MyAnnotation的注解:

public @interface MyAnnotation {
}

  注解有没有参数都可以,定义参数如下:

public @interface 注解名称{
   [public] 参数类型 参数名称1() [default 参数默认值];
   [public] 参数类型 参数名称2() [default 参数默认值];
   [public] 参数类型 参数名称n() [default 参数默认值];
}

  注解中可以定义多个参数,参数的定义有以下特点:

  1. 访问修饰符必须为public,不写默认为public。
  2. 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组。
  3. 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作)。
  4. 参数名称后面的 () 不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法。
  5. default代表默认值,值必须和第2点定义的类型一致。
  6. 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

5.1 4种元注解*

  元注解的作用是负责注解其他注解。 Java5定义了4个标准的元注解,它们被用来提供对其它annotation类型作说明。

  • @Target(注解的使用范围)
      @Target说明了Annotation所修饰的对象范围。示例:
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface MyAnnotation {
}

  参数value,是ElementType类型的一个数组。ElementType:

/*注解的使用范围*/
public enum ElementType {
    /*类、接口、枚举、注解上面*/
    TYPE,
    /*字段上*/
    FIELD,
    /*方法上*/
    METHOD,
    /*方法的参数上*/
    PARAMETER,
    /*构造函数上*/
    CONSTRUCTOR,
    /*本地变量上*/
    LOCAL_VARIABLE,
    /*注解上*/
    ANNOTATION_TYPE,
    /*包上*/
    PACKAGE,
    /*类型参数上*/
    TYPE_PARAMETER,
    /*类型名称上*/
    TYPE_USE
}
  • @Retention
      用于指定注解的保留策略。示例:
//指定了 MyAnnotation 只存在于源码阶段,后面的2个阶段都会丢失
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
}

  value参数的类型为RetentionPolicy枚举。RetentionPolicy:

public enum RetentionPolicy {
    /*注解只保留在源码中,编译为字节码之后就丢失了,也就是class文件中就不存在了*/
    SOURCE,
    /*注解只保留在源码和字节码中,运行阶段会丢失*/
    CLASS,
    /*源码、字节码、运行期间都存在*/
    RUNTIME
}
  • @Documented
      用于指示将被注解的元素包含在生成的Java文档中。
  • @Inherited
      作用:如果一个类用上了@Inherited修饰的注解,那么其子类也会继承这个注解 。

5.2 注解的使用*

  将注解加载使用的目标上面,示例:

@注解名称(参数1=1,参数2=2,参数n=值n)
目标对象

  示例:

//定义一个无参注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann1 { 
}
//使用无参注解
@Ann1 
public class UseAnnotation1 {
}

//定义1个参数的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann2 { 
  String name();
}
//使用1个参数的注解
@Ann2(name = "我是zhangsan") 
public class UseAnnotation2 {
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann3 {
  String value();
}
//只有1个参数,名称为value的时候,使用时参数名称可以省略
@Ann3("我是zhangsan")
public class UseAnnotation3 {
}

//数组类型参数
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann4 {
    String[] name();
}
//name有多个值的时候,需要使用{}包含起来
@Ann4(name = {"我是123", "欢迎和我一起学spring"}) 
public class UseAnnotation4 {
    //如果name只有一个值,{}可以省略
    @Ann4(name = "如果只有一个值,{}可以省略") 
    public class T1 {
    }
}

//为参数指定默认值
//通过default为参数指定默认值,用的时候如果没有设置值,则取默认值,没有指定默认值的参数
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann5 {
    String[] name() default {"zhangsan", "spring系列"};//数组类型通过{}指定默认值
    int[] score() default 1; //数组类型参数,默认值只有一个省略了{}符号
    int age() default 30; //默认值为30
    String address(); //未指定默认值
}
@Ann5(age = 32,address = "上海") //age=32对默认值进行了覆盖,并且为address指定了值
public class UseAnnotation5 {
}

5.3 注解信息的获取*

  为了运行时能准确获取到注解的相关信息,Java在 java.lang.reflect反射包下新增了AnnotatedElement接口,它主要用于表示目前正在虚拟机中运行的程序中已使用注解的元素,通过该接口提供的方法可以利用反射技术地读取注解的信息。
  AnnotatedElement接口主要有如下几个实现类:

Class:类定义
Constructor:构造器定义
Field:类的成员变量定义
Method:类的方法定义
Package:类的包定义

  AnnotatedElement的常用方法:

//该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
//返回此元素上存在的所有注解,包括从父类继承的
Annotation[] getAnnotations();
//如果指定类型的注解存在于此元素上,则返回 true,否则返回 false
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
//返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回
//的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组
Annotation[] getDeclaredAnnotations();
  • 获取注解内容示例
      1、定义1个自定义注解。
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD,ElementType.TYPE})
public @interface MyAnnotation {
    public int age();
    public String name() default "张三";
}

  2、使用该注解。

@ControllerAdvice
@Controller
@MyAnnotation(name = "李四",age=12)
public class Test {
    public void show(){
        System.out.println("111");
    }
}

  3、获取特定类型的注解(如:MyAnnotation)。

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException {
        // 得到Class类对象
        Class<?> clazz = Class.forName("com.example.redislock.annotation.Test");

        // 获取Test类上指定类型的注解
        MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
        int age = annotation.age();
        String name = annotation.name();
        //name=李四 age=12
        System.out.println("name="+name+" age="+age);
    }
}

  4、获取所有注解。

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException {
        // 得到Class类对象
        Class<?> clazz = Class.forName("com.example.redislock.annotation.Test");
        // 获取Test类上所有的注解
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation annotation : annotations) {
            // Test类上的所有注解
            Class<? extends Annotation> annotationType = annotation.annotationType();
            System.out.println(annotationType);
        }
    }
}

  结果:

interface org.springframework.web.bind.annotation.ControllerAdvice
interface org.springframework.stereotype.Controller
interface om.example.annotion.MyAnnotation

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值