注解
从一开始学习Java,我们就一直听到注解(Annotation),那么注解是什么呢?注解和注释是有点关系的,都有个注字,都能用来描述程序。
- 注释:用文字描述程序的,给程序员看的
- 注解:用于说明程序的,程序员既能看懂,机器也能解析。注解其实是一个接口,与类同等级,但注解并非是程序一部分,我们可以理解为标签。
注解格式是@注释名(参数值)
,注解不仅仅只能用来描述程序,也可以被程序读取,对程序做出规定的约束或者其他功能。
1.内置注解
内置注解指的是JDK帮我们编写好了的注解,这些经常用于我们的日常使用。大致有以下三个:
- @override :检测被注解标注的方法是否继承自父接口,当然也会对方法进行检测,看是否重写成功(检查方法名、参数类型等)
@Override
public String toString() {
return "Test03{}";
}
- @Deprecated: 该注解标注的内容不推荐使用,可能存在风险或者已经过时
@Deprecated
public static void test() {
System.out.println("Deprecated");
}
- @suppressWarings :压制警告,参数值是一个枚举数组,表示这个注解用在什么地方(类、接口、方法等)
@SuppressWarnings("all")
public void test2() {
ArrayList arrayList = new ArrayList();
}
public static void main(String[] args) {
test();
}
2.元注解
对于普通注解,我们是用来描述程序的,那么对于元注解顾名思义,元注解就是用来描述注解的注解,从注解的底层源码可以看出:
这个是@Override
的注解源码,其中里面利用到了@Target
和@Retention
这两个元注解。元注解十分重要,学习这个也有利于我们自己自定义注解。下面讲解一下元注解:
@Target
:表示了注解作用的地方,比如类、方法、成员变量、接口等,参数是一个枚举类型的数组.@Retention
:表示需要在什么级别保存改注解,描述了注解的生命周期(SOURCE < CLASS < RUNTIME),SOURCE表示只能存在于源代码时期,CLASS表示能存活到于字节码文件,RUNTIME表示能存活到运行时期,一般来说都是RUNTIME@Documented
:说明改注解将包含在javadoc中@Inherited
:表示子类能继承父类的该注解
3.自定义注解
如果我们要手造框架的话,自定义注解就必不可少,框架很多功能其实就是利用了注解和反射来实现的。而自定义注解,就又要用到我们上面提到的元注解,自定义注解格式大致如下:
元注解
public @interface 注解名{
//注解的参数:参数类型+参数名+圆括号
E 参数名();
}
以上,便定义了一个注解。看起来很简单对吧,但还是有一些小细节需要注意:
- 注解中定义变量的写法和Java普通写法有点不同,是以圆括号结尾,有点类似于方法的写法了。
- 如果注解定了变量,那我们使用必须赋值,但变量定义时如果有使用
default
给予了默认值,那就不用赋值。 - 如果注解里只有一个变量,且名为value,则赋值时可以不用写变量名。
public class Test {
//注解可以显示赋值,如果没有默认值,我们就必须给注解赋值
@MyAnnotation2(name = "陈平安",schools = "清华")
public void test() {
}
@My3("宁姚")
public void test2() {
}
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{
//注解的参数:参数类型+参数名
String name() default ""; //加了默认值
int age() default 0;
int id() default -1; //如果默认值为-1.则不存在
String[] schools() default {"清华","北大"};
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface My3{
String value(); //只有一个变量,且名为value
}
以上便是注解的全部内容了,注解的内容其实十分简单。但也许你也会很奇怪,怎么注解看起来这么简单,却有那么强的功能,这个其实就是要通过和反射一起结合使用,注解反射天生一对,离开了反射,注解什么都算不是。下面来讲解下反射。
反射
1.知识补充:动态语言和静态语言
在学习反射前,先进行一些知识补充:语言的精动态,学习这些将有利于我们理解反射。
- 静态语言,代表:Java、C、C++
静态语言就是在编译阶段进行类型检查,当编写源程序的时候,出现不符合语法的规范,就会提示错误,在编译时变量的数据类型即可确定。也即运行时结构不可变的语言。
int a = 123; // a是整数类型变量
a = "mooc"; // 错误:不能把字符串赋给整型变量
-
动态语言,代表:JS、PHP
所谓动态语言,就是类型检查是在运行的时候做的,变量使用之前不需要类型声明,通常变量的类型是就是被赋值的那个值的类型。运行时期机构可变。例如JavaScript等这种脚本语言就是动态语言,在编译阶段它不会判断代码是否符合规范,在运行的时候才会去判断。
a = 123 # a是整数
print a
a = 'imooc' # a变为字符串
print a
如果我们对这两种语言有一些了解,就很容易了解到它们的区别,例如JS相较于Java,语法十分不规范十分宽松。那这个和反射有什么关系呢?
Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。Java的动态性让编程的时候更加灵活。
2.反射概述
1)第一个问题:反射是什么?
其实对于理解反射是什么这一个问题,最好是深入了解Java的编译过程也就是JVM的相关知识之后,才能对反射有一个较好的理解。但也不妨影响我这个弱鸡对他的理解。
对于一个类的正常生命流程,是编译期→运行期
这样一个顺序。然而Java给我们提供了一个特殊的机制,在编译期时(上图第一个阶段加载完成后)会把我们的源代码转换成.class文件并在内存中生成一个Class对象,我们的反射便是借助这一个Class对象来实现的。
反射,我觉得最重要的是这里面的那个“反”字,说明有什么东西反过来了。如下图,我们正常编程的逻辑就是按箭头的顺序执行的。但假如我们是反射,就是反过来,就是从1到2这样的逻辑,我们是通过操作我们编写的类生成的Class对象来获取类的信息或者操作类。
总结一下,正常编程:类→正常执行,反射:类→class对象→类→执行,也就是说从类或对象中推导出Class类,然后再从Class类中获得类的信息或对类进行操作,这一行为就是反射。
2)第二个问题:反射能干什么?
上面我们曾提到Java是一门“准动态”语言,反射提供了动态的特性。同时它也是我们框架中的一个很重要的组成部分,反射是框架设计的灵魂。
class是一切反射的根源,JAVA反射机制是在运行状态中,对于任何一个类,通过反射都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。
这样说可能也不能让你了解到反射有什么作用,下面看看反射的应用就知道了。
3.反射应用
1.获取类的Class对象
注意:每一个类只有一个class对象!!!
//1.通过对象获得
Class c1 = person.getClass();
System.out.println(c1.hashCode());
//2.forName获得
Class c2 = Class.forName("com.chy.reflection.Test");
System.out.println(c2.hashCode());
//3.通过类名.class获得
Class c3 = Stu.class;
System.out.println(c3.hashCode());
//4. 基本内置类型的包装类都有一个Type属性
Class c4 = Integer.TYPE;
System.out.println(c4.hashCode());
//获得父类类型
Class c5 = c1.getSuperclass();
System.out.println(c5);
当然也不仅仅只有类才有class对象,对于基本类型、数组、接口等也有class对象。对于数组来说,只要数组类型和维度一样(与长度无关),它们都是同一个class对象。
Class c1 = Object.class; //类
Class c2 = Comparable.class; //接口
Class c3 = String[].class; //一维数组
Class c4 = int[][].class; //二维数组
Class c5 = Override.class; //注解
Class c6 = ElementType.class; //枚举
Class c7 = Integer.class; //基本数据类型
Class c8 = void.class; //void
Class c9 = Class.class; //Class
2.利用class对象创建实例
Object newInstance()
:创建实例化对象Constructor getDeclaredConstructor(Class ....args)
:获取指定参数的构造器
/*
使用.newInstance()方法
前提:
1.类中必须有一个无参构造器
2.构造器的访问权限必须足够
*/
Class c = Test.class;
Test test = (Test)c.newInstance();
/*
通过获取构造器来创造实例化对象
注意:
1.此时就可以无视构造器的访问权限
2.不一定要有无参构造器
3.可以创造指定参数的构造器
*/
Class c = Test.class;
Constructor con = c.getDeclaredConstructor(String.class,int.class);
Test test = (Test)con.newInstance("chy",18);
3.利用class对象调用方法
Method getDeclaredMethod(String 方法名,Class 参数的class对象)
:获取指定的方法void setAccessible(boolean flag)
:启动和禁用访问安全的开关,默认为false,但传参为true时,就可以调用private方法。Object invoke(Object obj,Object ...args)
:激活方法,第一个参数是要调用该方法的对象,其他是参数
class user{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Class c = user.class;
Method setName = c.getDeclaredMethod("setName", String.class);
Method getName = c.getDeclaredMethod("getName");
setName.setAccessible(true);
setName.invoke(user3,"chy");
getName.invoke(user3);
注意:一般来说setAccessible
不宜为true,因为不安全。不过一旦开启这个禁用开关,则可以提高性能,对于使用频繁的方法,应该打开这个开关。
4.利用class对象获取类的信息
Field getField(String name)
:获取类的属性,只能获取publicField getDeclaredField(String name)
:获取类的属性,包括privateString getName()
:获取类的名字(全类名)String getSimpleName()
:获取类的简单名字
//获取类的名字
System.out.println(c1.getName()); //获得包名+类名
System.out.println(c1.getSimpleName()); //获得类名
//获得类的属性
Field field = c1.getField("name");
Field field = c1.getDeclaredField("name");
Field[] fields = c1.getFields(); //只能找到public属性
fields = c1.getDeclaredFields(); //找到全部属性
for (Field field :fields) {
System.out.println(field);
}
5.利用class对象操作泛型和利用class对象操作注解
这里偷懒一下,博客上有很多的,这个其实没什么特殊的,和上面的操作都是类似的。以后有空再来补。
代理模式
相信大家都对代理模式不陌生,这里为什么会涉及到代理模式呢,因为动态代理的原理就是用到了反射,所以这里也简析一下代理模式。
代理模式是什么呢?比如我们有两个类,一个实现某种功能简称功能类,一个通过调用功能类来满足自身目的检查客户类,原来客户类和功能类是直接交互的,客户类→功能类
。
现在我们使用代理模式,就再多出一个类,代理类,它横插在客户类和功能类之间,是他们必须通过代理类来交互客户类→代理类→功能类
。这时很多人就可能会不理解了,说你这个不是多此一举吗,直接交互不好吗,还要引入一个新的类。
确实,我们平时确实不一定要用到这个模式,用这个确实是多次一举。但在某些场合的时候,使用这个代理模式就是一个极佳的 punchline 了。
下面举个例子:比如我们的WEB项目,现在需要增加一个日志功能,在我们每次调用方法的时候把它记录下来,那怎么办呢?在原来的代码上改咯?不,不能这么做,这么做不够谨慎,有可能改着改着原来代码无法运行了。这时候代理模式就来了,我们只需新建一个代理类,这个代理类不仅能保有原来的功能,也在这个基础上增加了日志功能,专业术语上这个过程叫做增强。
使用代理模式就很好地降低了耦合度,将功能分离开来。其实这个也是横向开发思想的体现。
代理模式有两种:静态代理和动态代理。
首先角色分析
- 抽象角色:一般是接口或者抽象类,也就是这里面的租房这个功能
- 真实角色:被代理的角色,相当于房东
- 代理角色:代理类,一般会添加一些附属操作来实现增强,相当于这里面的中介
- 客户:访问代理对象的类,相当于这里面的租客
1.静态代理
这里就不具体展示了,主要讲一下静态代理是什么,用代码展示一下。
//租房,抽象角色
public interface Rent {
public void rent();
}
//房东,真实角色
class Host implements Rent{
@Override
public void rent() {
System.out.println("房东卖房子...");
}
}
//代理角色,中介
class Proxy implements Rent{
private Host host;
public void setHost(Host host) {
this.host = host;
}
@Override
public void rent() {
fare(); //多了一个中介方法,可以类比成我们项目中要多添加的功能
host.rent();
}
public void fare(){
System.out.println("交中介费");
}
}
//客户角色,租客
class Client{
public static void main(String[] args) {
Host host = new Host();
Proxy proxy = new Proxy();
proxy.setHost(host);
proxy.rent();
}
}
静态代理一般很少用,较多采用的是动态代理。静态代理其实就是我们真正去写出一个代理类,不过弊端也是很明显的,如果有n个真实角色,那就要写n个代理类,太麻烦了。
众所周知,程序猿是一种懒惰的动物。
2.动态代理
相较于静态代理,动态代理就很方便,不过这个很绕很绕。首先要了解以下这个类:
-
InvocationHandler
:这个叫做调用处理程序实现的接口,每一个代理角色都有一个对应的调用处理程序,看不懂没关系,只需要知道我们是通过它的一个方法来获取代理对象(Proxy对象),然后复写这个接口里面的invoke
方法。方法一:
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
,第一个参数是类加载器,第二个参数是要代理的接口的class对象,第三个参数就是要一个InvocationHandler
对象。这个方法就会返回一个Proxy对象。方法二:
invoke
,这就是我们的代理逻辑,我们要让代理对象做什么增强都需要做什么。这个方法比较复杂,我讲不明白,大家看代码理解吧。注意:每次我们调用代理对象操作时,就会自动调用这个方法。
不懂没关系,通过例子多加理解便可。对于静态代理中的那个例子进行改造,变成动态代理,代码如下:
//租房,抽象角色
public interface Rent {
public void rent();
}
//房东,真实角色
class Host implements Rent{
@Override
public void rent() {
System.out.println("房东卖房子...");
}
}
//代理角色,中介
class ProxyInvocationHandler implements InvocationHandler {
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
//获取代理对象
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);
}
//附加方法
public void fare(){
System.out.println("交中介费");
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
//主要代理逻辑
//这个method指的是真实对象里的方法,比如rent方法
fare();
Object result = method.invoke(rent,objects);
return result;
}
}
//客户角色,租客
class Client{
public static void main(String[] args) {
Host host = new Host();
ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
//这里利用到了多态
proxyInvocationHandler.setRent(host);
//获取代理对象
Rent proxy = (Rent)proxyInvocationHandler.getProxy();
//这里虽然只是调用了rent方法,但是仍然会调用交中介费的方法
proxy.rent();
}
}
以上就是动态代理的例子,其实我们可以直接记住这个格式,实际应用时改一下一些细节即可。