Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。
如果把代码想象成一个具有生命的个体,注解就是给这些代码的某些个体打标签
public @interface Test { }
它的形式跟接口很类似,不过前面多了一个 @ 符号。上面的代码就创建了一个名字为 Test 的注解。
你可以简单理解为创建了一张名字为 Test的标签。
@Test public class TestAnnotation { }
创建一个类 TestAnnotation,然后在类定义的地方加上 @Test就可以用 Test注解这个类了
你可以简单理解为将 Test 这张标签贴到 TestAnnotation这个类上面。
元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。
元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们
java - source被丢弃 -> class - class被丢弃 > jvm (runtime)
@Target
Target 是目标的意思,@Target 指定了注解运用的地方
你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
@Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
注解的属性也叫做成员变量。注解只有成员变量,没有方法。
需要注意的是,在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组
注解中属性可以有默认值,默认值需要用 default 关键值指定
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Test{ int id() default -1; String msg() default "Hello"; }
上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。
赋值的方式是在注解的括号内以 value=”” 形式,多个属性之前用 ,隔开
@Test(id=1,msg="hello annotation") public class TestAnnotation { }
注解与反射。
注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通过 getAnnotation() 方法来获取 Annotation 对象。
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
或者是 getAnnotations() 方法。
public Annotation[] getAnnotations() {}
前一种方法返回指定类型的注解,后一种方法返回注解到这个元素上的所有注解。
如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
@Test() public class TestDemo{ public static void main(String[] args) { boolean hasAnnotation = TestDemo.class.isAnnotationPresent(Test.class); if ( hasAnnotation ) { TestAnnotation testAnnotation = TestDemo.class.getAnnotation(Test.class); System.out.println("id:"+testAnnotation.id()); System.out.println("msg:"+testAnnotation.msg()); } } }
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性
简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
forName
静态方法 public static Class<?> forName(String className)
Class<?> klass = int.class; Class<?> classInt = Integer.TYPE;
getClass()
方法 StringBuilder str = new StringBuilder("123"); Class<?> klass = str.getClass();
一般地,我们用 instanceof
关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance()
方法来判断是否为某个类的实例,它是一个 native 方法:
public native boolean isInstance(Object obj);
通过反射来生成对象主要有两种方式。
Class<?> c = String.class; Object str = c.newInstance();
//获取String所对应的Class对象 Class<?> c = String.class; //获取String类带一个String参数的构造器 Constructor constructor = c.getConstructor(String.class); //根据构造器创建实例 Object obj = constructor.newInstance("23333"); System.out.println(obj);
得到构造器的方法
Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数, Constructor[] getConstructors() -- 获得类的所有公共构造函数 Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关) Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)
获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
public T newInstance(Object ... initargs)
获得字段信息的方法
Field getField(String name) -- 获得命名的公共字段 Field[] getFields() -- 获得类的所有公共字段 Field getDeclaredField(String name) -- 获得类声明的命名的字段 Field[] getDeclaredFields() -- 获得类声明的所有字段
获得方法信息的方法
Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法 Method[] getMethods() -- 获得类的所有公共方法 Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法 Method[] getDeclaredMethods() -- 获得类声明的所有方法
当我们从类中获取了一个方法后,我们就可以用 invoke()
方法来调用这个方法。 invoke
方法的原型为:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference
其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:
public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); }
而 newArray
方法是一个 native 方法,它在 HotSpot JVM 里的具体实现我们后边再研究,这里先把源码贴出来:
private static native Object newArray(Class<?> componentType, int length) throws NegativeArraySizeException;
依赖是类与类之间的连接,依赖关系表示一个类依赖于另一个类的定义,通俗来讲
就是一种需要,例如一个人(Person)可以买车(Car)和房子(House),Person类依赖于Car类和House类
public static void main(String ... args){ //TODO: Person person = new Person(); person.buy(new House()); person.buy(new Car()); } static class Person{ //表示依赖House public void buy(House house){} //表示依赖Car public void buy(Car car){} } static class House{ } static class Car{ }
依赖倒置是面向对象设计领域的一种软件设计原则
软件设计有 6 大设计原则,合称 SOLID
依赖倒置原则的定义如下:
不管你承认不承认,“有人的地方就有江湖”,我们都说人人平等,但是对于任何一个组织机构而言,它一定有架构的设计有职能的划分。按照职能的重要性,自然而然就有了上下之分。并且,随着模块的粒度划分不同这种上层与底层模块会进行变动,也许某一模块相对于另外一模块它是底层,但是相对于其他模块它又可能是上层
公司管理层就是上层,CEO 是整个事业群的上层,那么 CEO 职能之下就是底层。
然后,我们再以事业群为整个体系划分模块,各个部门经理以上部分是上层,那么之下的组织都可以称为底层。
由此,我们可以看到,在一个特定体系中,上层模块与底层模块可以按照决策能力高低为准绳进行划分。
那么,映射到我们软件实际开发中,一般我们也会将软件进行模块划分,比如业务层、逻辑层和数据层。
业务层中是软件真正要进行的操作,也就是 做什么 。
逻辑层是软件现阶段为了业务层的需求提供的实现细节,也就是 怎么做 。
数据层指业务层和逻辑层所需要的数据模型。
因此,如前面所总结,按照决策能力的高低进行模块划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
象如其名字一样,是一件很抽象的事物。抽象往往是相对于具体而言的,具体也可以被称为细节,当然也被称为具象。
比如:
上面可以知道,抽象可以是物也可以是行为。
具体映射到软件开发中,抽象可以是接口或者抽象类形式。
/** * Driveable 是接口,所以它是抽象 */ public interface Driveable { void drive(); }
/** * 而 Bike 实现了接口,它们被称为具体。 */ public class Bike implements Driveable { @Override public void drive() { System.out.println("Bike drive"); } }
/** * 而 Car实现了接口,它们被称为具体。 */ public class Car implements Driveable { @Override public void drive() { System.out.println("Car drive."); } }
在平常的开发中,我们大概都会这样编码。
public class Person { private Bike mBike; private Car mCar; private Train mTrain; public Person(){ mBike = new Bike(); //mCar = new Car(); // mTrain = new Train(); } public void goOut(){ System.out.println("出门啦"); mBike.drive(); //mCar.drive(); // mTrain.drive(); } public static void main(String ... args){ //TODO: Person person = new Person(); person.goOut(); } }
我们创建了一个 Person 类,它拥有一台自行车,出门的时候就骑自行车。
不过,自行车适应很短的距离。如果,我要出门逛街呢?自行车就不大合适了。于是就要改成汽车。
不过,如果我要到北京去,那么汽车也不合适了。
有没有一种方法能让 Person 的变动少一点呢?因为这是最基础的演示代码,如果工程大了,代码复杂了,Person 面对需求变动时改动的地方会更多。
而依赖倒置原则正好适用于解决这类情况。
下面,我们尝试运用依赖倒置原则对代码进行改造。
我们再次回顾下它的定义。
上层模块不应该依赖底层模块,它们都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
首先是上层模块和底层模块的拆分。
按照决策能力高低或者重要性划分,Person 属于上层模块,Bike、Car 和 Train 属于底层模块。
上层模块不应该依赖于底层模块。
public class Person { // private Bike mBike; private Car mCar; private Train mTrain; private Driveable mDriveable; public Person(){ // mBike = new Bike(); //mCar = new Car(); mDriveable = new Train(); } public void goOut(){ System.out.println("出门啦"); mDriveable.drive(); //mCar.drive(); // mTrain.drive(); } public static void main(String ... args){ //TODO: Person person = new Person(); person.goOut(); } }
可以看到,依赖倒置实质上是 面向接口编程的体现 。
控制反转 IoC 是 Inversion of Control的缩写,意思就是对于控制权的反转,对么控制权是什么控制权呢?
Person自己掌控着内部 mDriveable 的实例化。
现在,我们可以更改一种方式。将 mDriveable 的实例化移到 Person 外面。
public class Person2 { private Driveable mDriveable; public Person2(Driveable driveable){ this.mDriveable = driveable; } public void goOut(){ System.out.println("出门啦"); mDriveable.drive(); //mCar.drive(); // mTrain.drive(); } public static void main(String ... args){ //TODO: Person2 person = new Person2(new Car()); person.goOut(); } }
就这样无论出行方式怎么变化,Person 这个类都不需要更改代码了。
在上面代码中,Person 把内部依赖的创建权力移交给了 Person2这个类中的 main() 方法。也就是说 Person 只关心依赖提供的功能,但并不关心依赖的创建。
这种思想其实就是 IoC,IoC 是一种新的设计模式,它对上层模块与底层模块进行了更进一步的解耦。控制反转的意思是反转了上层模块对于底层模块的依赖控制。
比如上面代码,Person 不再亲自创建 Driveable 对象,它将依赖的实例化的权力交接给了 Person2。而 Person2在 IoC 中又指代了 IoC 容器 这个概念。
依赖注入,也经常被简称为 DI,其实在上一节中,我们已经见到了它的身影。它是一种实现 IoC 的手段。什么意思呢?
为了不因为依赖实现的变动而去修改 Person,也就是说以可能在 Driveable 实现类的改变下不改动 Person 这个类的代码,尽可能减少两者之间的耦合。我们需要采用上一节介绍的 IoC 模式来进行改写代码。
这个需要我们移交出对于依赖实例化的控制权,那么依赖怎么办?Person 无法实例化依赖了,它就需要在外部(IoC 容器)赋值给它,这个赋值的动作有个专门的术语叫做注入(injection),需要注意的是在 IoC 概念中,这个注入依赖的地方被称为 IoC 容器,但在依赖注入概念中,一般被称为注射器 (injector)。
实现依赖注入有 3 种方式:
/** * 接口方式注入 * 接口的存在,表明了一种依赖配置的能力。 */ public interface DepedencySetter { void set(Driveable driveable); }
public class Person2 implements DepedencySetter { //接口方式注入 @Override public void set(Driveable driveable) { this.mDriveable = mDriveable; } private Driveable mDriveable; //构造函数注入 public Person2(Driveable driveable){ this.mDriveable = driveable; } //setter 方式注入 public void setDriveable(Driveable mDriveable) { this.mDriveable = mDriveable; } public void goOut(){ System.out.println("出门啦"); mDriveable.drive(); //mCar.drive(); // mTrain.drive(); } public static void main(String ... args){ //TODO: Person2 person = new Person2(new Car()); person.goOut(); } }
JSR-330 是 Java 的依赖注入标准。定义了如下的术语描述依赖注入:
在标准中, 依赖是类型而不是实例/对象; 在程序中(运行时), 需要的是依赖的实例.
包 javax.inject 指定了获取对象的一种方法,该方法与构造器、工厂以及服务定位器(例如 JNDI))这些传统方法相比可以获得更好的可重用性、可测试性以及可维护性。此方法的处理过程就是大家熟知的依赖注入,它对于大多数应用是非常有价值的。
注解 @Inject
标识了可注入的构造器、方法或字段。可以用于静态或实例成员。一个可注入的成员可以被任何访问修饰符(private、package-
private、protected、public)修饰。注入顺序为构造器,字段,最后是方法。超类的字段、方法将优先于子类的字段、方法被注入。对于
同一个类的字段是不区分注入顺序的,同一个类的方法亦同
接口 Provider 用于提供类型 T 的实列。Provider 是一般情况是由注入器实现的。对于任何可注入的 T 而言,您也可以注入 Provider
class Car { @Inject Car(Provider<Seat> seatProvider) { Seat driver = seatProvider.get(); Seat passenger = seatProvider.get(); … } }
get()
用于提供一个完全构造的类型 T 的实例。
异常抛出:RuntimeException —— 当注入器在提供实例时遇到错误将抛出此异常。例如,对于一个可注入的成员 T
抛出了一个异常,注入器将包装此异常并将它抛给 get()
的调用者。调用者不应该尝试处理此类异常,因为不同注入器实现的行为不一样,即使是同一个注入器,也会因为配置不同而表现的行为不同。
用于标识限定器注解。任何人都可以定义新的限定器注解。一个限定器注解: