泛型
从Java5开始,引入了泛型。如:
List strList = new ArrayList();
从Java7开始,允许泛型的菱形语法,即后边的类型参数可以省略。
List strList = new ArrayList<>();
Java可以推断出具体的泛型类型。
菱形语法对原有的泛型并没有改变,只是更好地简化了泛型编程。
深入泛型
所谓泛型,就是允许在定义类,接口和方法时使用类型参数,这个类型形参将在声明变量、创建对象和调用方法时动态地指定(即传入实际类型参数,也可称为类型参数)。
允许在定义接口和类时声明形参,类型形参在整个接口、类体内可当成类型使用。
public class Apple<E> {
private E info;
public Apple(E info) {
this.info = info;
}
public E getInfo() {
return info;
}
public void setInfo(E info) {
this.info = info;
}
public static void main (String[] args) {
Apple<String> a1 = new Apple<String>("苹果");
System.out.println(a1.getInfo());
Apple<Double> a2 = new Apple<>(5.32);
System.out.println(a2.getInfo());
}
}
当创建带有泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明.
从泛型类派生子类
当创建了带泛型声明的接口,父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口,父类时不能再包含类型形参.例如,下面的代码就是错误的:
public class A extends Apple{}//Apple类不能跟类型参数.
必须时实参,可以改为:
public class A extends Apple{} (1)
也可以是:
public calsss A extends Apple{} (2)
当两种情况有点差别,(1)已经声明了类中的类型,(2)未指定类中的未知类型(这种情况可能会出现报黄).
在静态方法,静态初始化块或者静态变量的声明和初始化中不允许使用类型形参.
如:
public class R<T>{
//下面代码是错误的,不能在静态变量声明中使用类型形参
static T info;
T age;
public void foo(T msg);
//下面的代码错误,不能再静态方法中使用类型形参
public static void bar(T msg){}
}
由于系统中并不会真正生成泛型类,所以instanceOf运算符后不能使用泛型类.例如,下面代码是错误的
cs instanceOf ArrayList<String>
不能带类型参数.
类型通配符
如果Foo是Bar的一个子类(子类或者子接口),G是具有泛型声明的类或者接口,G并不是G的子类型
使用类型通配符
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作List<?>.意思是元素类型位置的List.这个问号(?)被称为通配符.
public void test (List(?) c){
for(int i = 0 ; i < c.size(); i++){
System.out.println(c.get(i));
}
}
可以把任何List的当成参数使用,程序依然可以访问c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object.
但这种带通配符的List仅便是他是各种类型的List的父类,并不能把元素加入到其中.例如,如下代码会引起编译错误.
List<?> c = new ArrayList<>();
c.add(new Object());
设定类型通配符的上限
表示如:List<? entends Shape>
例如Circle,Rectangle是Shape的子类,List<? extends Shape>(表示所有Shape类泛型List的父类)可以当作List
由于通配符还是不能确定具体的类型,因此不能把Shape对象或其子类的对象加入这个泛型集合中.下面代码就是错误的:
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes.add(0,new Rectangle());
}
设定类型形参的上限
Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型参数时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类.
public class Banana<E extends Number> {
E col;
public E getCol() {
return col;
}
public void setCol(E col) {
this.col = col;
}
public static void main(String[] args) {
Banana<Integer> b = new Banana<Integer>();
Banana<Double> b1 = new Banana<Double>();
//下边编译错误
Banana<String> s1 = new Banana<String>()
}
}
在一种更极端的情况下,程序需要为类型形参设定多个上限(最多有一个父类上限,可以有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口.如
public class Apple<T extends Number & java.io.Serializable>{...}
所有接口必须位于类上限之后.
泛型方法
还有一种情况,定义类,接口时没有使用类型形参,但定义方法时想自己定义类型形参.
所谓的泛型方法,就是在声明方法定义时定义一个或多个类型参数.泛型方法的用法格式如下:
修饰符 <T,S> 返回值类型 方法名(形参列表)
{…}
例子:
static <T> void fromArrayToCollection(T[] a,Collection<T> c){
for(T o : a){
c.add(o);
}
}
static <T> void test(Collection<? extends T> from,Collection<T> to){...}
上边实例代码中定义了一个泛型形参,这个泛型形参就可以在该方法内当成普通类型使用.但使用时不用定义形参.但是时不要让编译器混淆;
泛型方法和通配符的区别
如果效果是可以在不同的调用点传入不同的实际类型.对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的.
泛型方法允许类型形参被用来表示方法的一个或多个参数之间类型依赖关系,或者方法返回值与参数之间的类型依赖关系,如果没有这样的类型依赖关系,就不应该使用泛型方法.(形参中a依赖b的类型,或者是返回值依赖b的类型,这个时候就不能用通配符,应该用泛型方法);
如果有需要时,也可以同时使用泛型方法和通配符,如Java的Collections.copy()方法:
public class Collections{
public static <T> void copy(List<? super T> dest,List<? extends T> src)
}
super时下限 ? super T 代表是T或者是T的父类
JDK在定义src形参类型是定义的是类型通配符,? extends T,编译器不能确定是什么类型.这是因为该方法无须向src集合中添加元素,也无须修改src集合里的元素,但?super T可以确定是T或者T的父类,只要保证这些就能存进去.
Java7的"菱形"语法与泛型构造器
正如泛型方法一样,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器.
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来"推断"类型形参的类型,而且程序员也可以显式地为构造器形参指定实际的类型.
例如:
class Foo{
public <T> Foo(T t){
System.out.println(t);
}
}
//泛型构造器中的T参数为String
new Foo("疯狂Java讲义");
//泛型构造器参数时Integer
new Foo(200);
//显式指定泛型构造器的T参数是String
//传给Foo构造器的实参也是String对象
new <String> Foo ("疯狂Java讲义");
使用泛型构造器
public class MyClass<E>{
public <T> MyClass(T t){
System.out.println("t参数的值为:" + t);
}
public static void main(String[] args) {
//MyClass类声明了E形参是String类型
//泛型构造器中声明的T形参是Integer类型
MyClass<String> mcl = new MyClass<>(5);
//显式声明泛型构造器中声明的T形参是Integer类型
MyClass<String> mcl2 = new <Integer> MyClass<String>(5);
//显式声明构造器泛型的T的形参时就不能使用"菱形"语法,下边编译错误
MyClass<String> mcl3 = new <Integer> MyClass<>(5);
}
}
上面程序中粗体字代码既制定了泛型构造器中的类型形参是Integer,又想使用"菱形"语法,所以这行代码无法通过编译.
设定通配符下限
? super T 指定下限为T
public static <T> T copy (Collection<? super T> dest,Collection<T> src) {
T last = null;
for(T ele :src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> ln = new ArrayList<Number>();
List<Integer> li = new ArrayList<Integer>();
Integer last = copy(ln,li);
}
泛型方法与方法重载
由于泛型方法既有通配符上限,也有通配符下限,因此相同方法的相似表示,从而形成重载;但如果调用,由于两个方法都适合,就会发生报错.
例如:
public class MyUtills {
public static <T> void copy(Collection<T> dest,Collection<? extends T> src){...}
public static <T> T copy(Collection<? super T> dest,Collection<T> src){...}
}
上面的两个方法的参数列表存在区别,形成重载
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln,li);
上面程序中调用copy()方法,但这个copy()方法,但这个方法可以匹配上面两个方法。编译器无法确定这行代码想调用哪个copy()方法,所以这行代码将引起编译错误。
Java8改进的类型推断
Java8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。
- 可通过调用方法的上下文推断类型的目标类型。
- 可在方法调用链中,将推断得到的类型参数传递到最后一个方法。
public class MyClass2<E> {
public static <Z> MyClass2<Z> nil(){return null;}
public static <Z> MyClass<Z> cons(Z head,MyClass2<Z> tail) {
return null;
}
E head() {
return null;
}
public static void main(String[] args) {
//可以通过方法赋值的目标来判断类型参数为String
MyClass2<String> ls = MyClass2.nil();
//无须使用下面语句在调用nil()方法时指定类型参数的类型
MyClass2<String> mu = MyClass2.<String>nil();
//可调用cons()方法所需的参数类型来推断类型的参数是Integer
MyClass2.cons(42, MyClass2.nil());
//而不用使用下面语句在调用nil()方法时指定类型参数的类型
MyClass2.cons(42, MyClass2.<Integer>nil());
}
}
擦除和转换
如果把一个有泛型的信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号内的类型信息都将被扔掉.比如一个List被转换为List,则List对集合元素的类型检查变成了类型参数的上限(即Object).
class Apple<T extends Number>{
T size;
public Apple(){}
public void setSize(T size){
this.size = size;
}
public T getSize(){
return this.size;
}
public static void main(String[] args){
Apple<Integer> a = new Apple<>(6);
Integer as = a.getSize();
Apple b = a;
Number size1 = b.getSize();//类型将变成上限
}
}
泛型和数组
Java泛型有一个很重要的设计原则–如果一段代码在编译时没有提出"[unchecked]"为经检查的转换警告,则程序在运行时不会引发ClassCastException异常.
正是基于这个原因,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符.但可以声明元素类型包含类型变量或类型形参的数组.也就是说只能声明List[]形式的数组,但不能创建ArrayList[10]这样的数组对象.能声明但不能创建;
但可以new ArrayList[10],这时Java会出现[unchecked]警告,因此下边的代码可能会出错;
使用通配符时,?没有上限
List<?>[] list = new ArrayList<?>();
则这时返回的就时Object类型,需要进行类型转换;
主要原因时因为 Object[] 是 String[] 的父类,而List 不是 List的父类
异常处理
Java7提供了多异常捕获:
以往的异常捕获是:
try {...
} catch(Exception e1){
} catch(Execttion e2){
}
Java7以后可以使用多异常捕获
使用一个catch块捕获多种类型的异常需要注意如下两个地方:
- 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开;
- 捕获多种类型的异常时,异常变量有隐式的final修饰,因此不能对异常变量重新赋值.
try {
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("你输入的两个数相除的结果是: " + c);
}
catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie)
{
System.out.println("程序发生了数组越界,数字格式异常,算术异常之一");
//捕获多异常时,异常变量默认有final修饰
//所以下面代码有错
ie = new ArithmeticException("test")
}
catch ( Exception e )
{
System.out.println("未知异常");
//捕获一种类型的异常时,异常变量没有final修饰
//所以下面代码是正确的
e = new RuntimeException("test");
}
访问异常信息
所有的异常对象都包含了如下几个常用方法:
- getMessage():返回该异常的详细描述字符串.
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出.
- printStackTrace(PrintStream s);将异常的跟踪栈信息输出到指定输出流.
- getStackTrace();返回该异常的跟踪栈信息.
finally块
异常处理语句结构中只有try块是必须的,也就是说,如果没有try块,则不能有后面的catch块和finally块.但catch块和finally块至少出现其中一个.finally块在catch块后边.
在通常情况下,不要在final块中使用如return或throw等导致方法终止的语句,一旦使用,会导致try块,catch块中的return,throw语句失效.
执行流程,程序在try块或者catch块中遇到return或者是throw代码,会导致方法立即结束.如果有finally块时,当遇到throw或return时并没有结束该方法,而是去执行finally块,当finally块执行完毕后再跳回来执行try或者是throw代码块中的return或throw语句.但如果finally块里使用了return或throw等导致方法终止的语句,finally块已经中止了方法,系统将不会跳回去执行try块,catch块里的任何代码.
异常处理可以进行嵌套,即在代码块中再写异常处理的代码;但没有必要使用超过两次的嵌套层次,层次太深,可读性降低.
Java7的自动关闭资源的try语句
一般用finally块来关闭资源会显得特别臃肿,Java7改变了这中局面.
Java7增强了try语句的功能–它允许在try关键字后紧跟一个圆括号,圆括号可以声明,初始化一个或多个资源,当try语句结束后自动关闭这些资源.
例如:
try(
BufferedReader br = new BufferedReader(new FildReader("AutoClaseTest.java"));
printStream ps = new PrintStream(new FileOutputStream(a.txt))
){
System.out.println(br.readLine());
ps.println ("庄生晓梦迷蝴蝶");
}
相当于隐式含有了finally块,因此此代码块中既没有catch块,也没有finally块.
能字段关闭的前提是实现了(AutoCloseable接口或者是Closeable,Closeable是AutoCloseable的子接口,java7基本改写了所有的IO和JDBC需要的各种资源类);
Checked异常和Runtime异常体系
对于Checked异常的处理,有如下两种方式:
- 明确如何处理该异常,程序应该使用try…catch块来捕获异常,然后在对应的catch块中处理异常.
- 当前方法不知道如何处理异常,应该在定义该方法时声明抛出异常.
使用throws声明抛出异常
当前方法不知道如何处理这种类型的异常时,抛出来让上级进行处理.如果main方法也不知道如何处理这种异常,也可以使用throws声明抛出异常,该异常交给JVM处理.JVN处理异常的方法是,打印异常的跟踪栈信息,并中止程序运行.
语法:
throws ExceptionClass1,ExceptonClass2...
抛出异常的方法,在被重写时有一个限制:子类方法声明抛出的异常应该时父类方法声明抛出的异常类型的子类或相同.
自定义异常
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类.定义异常类时,通常需要提供两个构造器:一个是无参构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息.
例如:
public class AucationException extends Exception{
public AuctionException(){}
public AuctionException(String msg){super(msg)}
}
catch和throw同时使用
很多情况下方法只会处理异常的一部分,这个时候就可以在catch代码块中通过throw抛出异常,并通知调用方法再次处理抛出去的异常.
Java7增强的throw语句
Java7之后Java编译器能准确检测到抛出类的具体类.
public static void main (String[] args)
//throws Exception Java7之前只能抛出Exception
throws FileNotFoundException //Java7之后更加精确
{
try
{
new FileOutputStream("a.txt");
}
catch(Exception ex) //实际上只需要FileNotFoundException
{
ex.printStackTrace();
throw ex;
}
}
异常链
程序一般需要分层:1,表现层:用户界面;2,中间层:实现业务逻辑;3,持久层:保存数据
当放生SQLException时我们不希望用户看见这层的异常.
我们常用的做法是在中间层捕获到SQLException,然后抛出一个给用户提示的一个异常.
这中链式处理也被称为"异常链";
JDK1.4之前,程序员必须自己编写代码来保存原始信息.从JDK1.4后,所有Throwable的子类的构造器都可以接受一个case对象作为参数.这个参数就用来表示原始异常.这样就可以把原始异常传递给新的异常.这样即使抛出了新的异常,你也可以通过这个异常链追踪到异常最初发生的未知.
例如:
catch(SQLException sqle){
throw new SalException(sqle);
}
catch(Exception e){
throw new SalException(e)
}
从JKD1.4以后,Throwable基类已有了一个可以接受Exception参数的方法,所以可以采用如下代码来定义SalException类:
public class SalException extends Exception {
public SalException(){}
public SalException(String msg){
super(msg);
}
public SqlException(Throwable t){
super(t)
}
}
Annotation(注解)
从JDK5开始,Java增加了对元数据(MetaData)的支持,也就是Annotation。
Annotation提供了一种为程序元素设置元数据的方法,从某些方面来看,Annotation就像修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在Annotation的"name=value"对中。
Annutation是一个接口,程序可以通过反射来获取指定程序元素的Annotation对象,然后通过Annotation对象来取得注解里的元数据。
Annotation不影响程序代码的执行,无论增加、删除Annotation,代码都始终如一地执行。如果希望让程序中的Annotation在运行时起到一定的作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,访问和处理Annotation的工具统称APT(Annotation processing Tool)
基本Annotation
Annotation必须使用工具来处理,工具负责提取Annotation里包含的元数据,工具还会根据这些元数据增加额外的功能。
@Override 限定重写父类方法
只能修饰方法
@Deprecated 标示已过期
修饰方法、类、接口
@SuppressWarnings 抑制编译器警告
通常情况下,如果程序中使用没有泛型限制的集合将会引起编译器警告,为了避免这种警告,可以使用@SuppressWarnings修饰
Java7的“堆污染”警告与@SafeVarargs
把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生“堆污染”;
在有些时候,开发者不希望看到这个警告,则可以使用如下三种方式“抑制”这个警告。
- 使用@SafeVarargs修饰引发该警告的方法或构造器。
- 使用@SuppressWarnings(“unchecked”)修饰
- 编译时使用-Xlint:varargs选项
@FunctionalInterface Java8的函数式接口
JDK的元Annotation
JDK除了在java.lang下提供了5个基本的Annotation之外,还在java.lang.annotation包下提供了6个Meta Annotation(元Annotation),其中5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用于定义Java8新增的重复注解。
@Retention
@Retention只能用于修饰Annotation定义,用于指定被修饰的Annotation就可以保留多长时间(保留的范围),@Retention包含了一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。
value成员变量的值只能是如下三个:
- RetentionPolicy.CLASS:编译器将把Annotation记录在class文件中,当运行java程序时,JVM不可获取Annotation信息,默认值。
- RetentionPolicy.RUNTIME:编译器把Annotation记录在class文件中。当运行Java程序时,JVM也可获取Annotation信息,程序可以通过反射过去该Annotation信息。
- RetentionPolicy.SOURCE:Annotation只保留在源代码中,编译器直接丢失这种Annotation。
如果需要通过反射获取注解信息,就需要使用value属性为RetentionPolicy.RUNTIME的@Retention。
当变量名为value时,可以直接在Annotation后的括号里指定该成员变量的值,无须使用name = value的形式。
@Target
用于指定被修饰的Annotation能用于修饰哪些程序单元。
@Target元Annotation也包含了一个名为value的成员变量,该成员变量的值只能是如下几个。
- ElementType.ANNOTATION_TYPE:指定该策略的Annotation只能修饰Annotation。
- ElementType.CONSTRUCTOR:指定该策略的Annotation只能修饰构造器。
- ElementType.FIELD:指定该策略的Annotation只能修饰成员变量。
- ElementType.LOCAL_VARIABLE:指定该策略的Annotation只能修饰局部变量。
- ElementType.METHOD:指定该策略的Annotation只能修饰方法定义。
- ElementType.PACKAGE:指定该策略的Annotation只能修饰包定义。
- ElementType.PARAMETER:指定该策略的Annotation可以修饰参数。
- ElementType.TYPE:指定该策略的Annotation可以修饰类、接口(包括注解类型)或枚举类型。
使用@Documented
修饰Annotation,指定该元Annotation修饰的Annotation将被javadoc工具提起成文档,如果定义Annotation类时使用了@Document修饰,则所有使用该Annotion修饰的程序元素的API文档中将会包含该Annotation说明。
使用@Inherited
指定被它修饰的Annotation将具有继承性。如果某个类使用了@Xxx注解(定义该注解时使用了@Inherited修饰)修饰,那么它的子类子类也将被@Xxx修饰。
自定义Annotation
定义新的Annotation类型使用@interface关键字,与定义一个接口非常像。
默认情况下可以修饰任何程序元素。
Annotation的成员变量在Annotation定义中以无形参的方法形式来声明,其方法的名称和返回值定义了该成员变量的名称和类型。
也可以在定义Annotation的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用default关键字。
使用时,如果没有指定默认值时应该设定变量。
public @interface Mytest {
String className();
String name() default "张三";
}
@Mytest(className = "AnnotationTest")
public class AnnotationTest {
@Mytest(className = "123")
public void info() {
}
}
使用@interface定义的Annotation的确非常像定义一个注解接口,这个注解接口继承了Annotation接口,这一点可以通过反射看到注解接口定义的方法。
Annotation可以根据是否包含成员变量,分为两类:
- 标记Annotation:没有定义成员变量的Annotation类型。这种Annotation进利用自身的存在与否来提供信息,如@override;
- 元数据Annotation:包含成员变量的Annotation,因为它们可以接受更多的元数据,所以也被称为元数据Annotation。
提取Annotation信息
使用Annotation修饰了类、方法、成员变量等成员之后,这些Annotation不会自己生效,必须有开发者提供相应的工具并处理Annotation信息。
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。java5在java.lang.reflect包下新增了AnnotationElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类:
- Class:类定义
- Constructor:构造器定义
- Field:类的成员变量定义
- Method:类的方法定义
- Package:类的包定义
java.lang.reflect包下主要包含了一些实现反射功能的工具类,从jdk5开始,提供了反射API增加了读取运行时Annotation的能力。只有当定义Annotation时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该Annotation才会在运行时可见,JVM才会在装载*.class文件时读取在class文件中的Annotation。
程序通过反射获取某个类的AnnotationElement对象之后,程序就可以调用该对象的如下几个方法来访问Annotation信息。
- A getAnnotation(Class annotationClass):返回该程序元素上催在的,指定类型的注解,如果该类型的注解不存在,则返回null;
- A getDeclaredAnnotation(Class annotationClass):这时Java8新增的方法,该方法尝试获取直接修饰该程序元素,指定类型的Annotation.如果该类型的注解不存在,则返回null.
- Annotation[] getAnnotations();返回该程序上存在的所有注解.
- Annotation[] getDeclaredAnnotations();返回直接修饰该程序元素的所有Annotation,可以是重复注解.
- boolan isAnnotationPresent(Class<? extends Annotation> annotationClass);判断该程序元素上是否存在指定类型的注解,如果存在则返回true,否则返回false.
- A[] getAnnotationByType(Class annotationClass):该方法的功能与前一个相似,但由于Java8增加了重复注解功能,因此需要使用该方法获取修饰该程序元素,指定类型的多个Annotation.
- A[] getDeclaredAnnotationByType(Class annotationClass):该方法与上方法相似,但由于Java8增加了重复注解功能,一次需要使用该方法获取直接修饰该程序元素,指定顶类型的多个Annotation.
public static void main(String[] args) throws NoSuchMethodException, SecurityException, ClassNotFoundException {
Annotation[] annotations = Class.forName("com.cheng.annotation.AnnotationTest").getMethod("info").getAnnotations();
System.out.println(Arrays.toString(annotations));
//[@com.cheng.annotation.Mytest(name=张三, className=123)]
Annotation[] annotations1 = Class.forName("com.cheng.annotation.AnnotationTest").getMethod("info").getDeclaredAnnotations();
System.out.println(Arrays.toString(annotations1));
//[@com.cheng.annotation.Mytest(name=张三, className=123)]
Annotation[] annotations2 = Class.forName("com.cheng.annotation.AnnotationTest").getMethod("info").getDeclaredAnnotationsByType(Mytest.class);
System.out.println(Arrays.toString(annotations2));
//[@com.cheng.annotation.Mytest(name=张三, className=123)]
//本代码的基础是在Mytest上标注@Retention(RetentionPolicy.RUNTIME)
}
如果想获取注解中的元数据,可以通过强制类型转换获取注解类,然后通过抽象方法获取;
for(Annotation an : annotations2) {
if(an instanceof Mytest) {
System.out.println(((Mytest) an).name());
System.out.println(((Mytest) an).className());
}
}
使用Annotation
介绍两个使用Annotation的例子,第一个Annotation Testable没有任何成员变量,仅仅是一个标志Annotation,它的作用是标记那些方法是可测试的.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Testable {
}
public class MyTest1 {
//使用@Testable注解指定该方法是可测试的
@Testable
public static void m1() {
}
public static void m2() {
}
@Testable
public static void m3() {
throw new IllegalArgumentException("参数处出错了");
}
public static void m4() {
}
@Testable
public static void m5() {
}
public static void m6() {
}
@Testable
public static void m7() {
throw new RuntimeException("程序业务出现异常");
}
public static void m8() {
}
}
仅仅使用注解来标记程序元素对程序来说是不会有任何影响的,这也是Java注解的一条重要原则.为了让程序中的注解起作用,接下来必须为这些注解提供一个注解工具;
该工具方法会分析clazz参数所代表的类,并运行该类里使用@Testable修饰的方法
public class ProcessorTest {
public static void process(String clazz) throws SecurityException, ClassNotFoundException {
int passed = 0;
int failed = 0;
for(Method m : Class.forName(clazz).getMethods()) {
if(m.isAnnotationPresent(Testable.class)) {
try {
m.invoke(null);
passed ++;
} catch (Exception e) {
System.out.println("方法" + m +"运行异常" +
e.getCause());
failed ++;
}
}
}
System.out.println("供运行了:" + (passed + failed) +"个方法,其中:\n 失败了"
+ failed +"个,\n" + "成功了:" + passed + "个!");
}
public static void main(String[] args) throws SecurityException, ClassNotFoundException {
process("com.cheng.annotation.MyTest1");
}
}
通过这个允许解说可以看出,程序中的@Testable起作用了,Mytest类里以@Testbale注解修饰的方法都被测试了.
Java8新增的重复注解
在Java8之前,同一个程序元素前最多只能使用一个相同类型的Annotation;如果需要在同一个元素前使用多个相同类型的Annotation,则必须使用Annotation"容器".
开发重复注解需要使用@Repeatable修饰,使用@Repeatable时必须为value成员变量指定值,该成员变量的值应该时一个"容器"注解,该容器注解可以包含多个被@Repeatable修饰的Annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(value = FkTags.class)
public @interface FkTag {
}
----------------------------------------
@Retention(RetentionPolicy.RUNTIME)//不能小于FkTag的范围,不然没有意义
@Target(ElementType.TYPE)
public @interface FkTags {
FkTag[] value();
}
----------------------------------------
//传统方式
@FkTags(value = {@FkTag(age = 3),
@FkTag(name = "任妤程",age = 2)})
public class FkTagTest {
}
----------------------------------------
//Java8之后
@FkTag(name = "任妤程",age = 2)
@FkTag(age = 3)
public class FkTagTest {
}
Java8新增的TypeAnnotation
Java8为ElementType枚举增加了TYPE_PARAMETER,TYPE_USE两个枚举值,这样就允许定义枚举时使用@Target(ElementType.TYPE_USE)修饰,这种注解被称为Type Annotation(类型注解),可用在任何用到类型的地方,比如:
- 创建对象(用new关键字创建)
- 类型转换
- 使用implements实现接口
- 使用throws声明抛出异常
这写情形都会用到类型,因此可以使用类型注解来修饰.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface NotNull {
}
-------------------------------
public class TypeAnnotationTest implements @NotNull Serializable{
private static final long serialVersionUID = 1L;
public static void main(@NotNull String[] args) {
String string = new @NotNull String("ABC");
}
}
java8本身并没有提供对Type Annotation执行检查的框架,但第三方有。
编译处理Annotation
APT是一种注解处理工具,它对源码文件进行处理,并找出源文件所包含的Annotation信息,然后针对Annotation信息进行额外的处理。使用APT可以根据文件中的Annotation生成额外的源文件和其他文件,APT会编译生成的源代码和原来的源文件,将它们一起生成class文件。
通过注解可以在java源文件中放置一些Annotation,然后使用APT工具就可以根据Annotation生成另一份XML文件。