异常处理
异常就是运行时的代码序列引起非正常情况。Java提供的异常机制是通过异常对象来描述错误,当引起异常的情况时,就创建一个能够描述该异常的对象,并在引起错误的方法中将其抛出。针对异常情况,我们可以自己处理异常,也可以继续传递异常,但是在某一点都需要捕获并处理异常。
异常可以由Java运行时系统抛出(违反了Java语言规则),也可以是我们通过代码抛出(手动报告某些错误)。
异常关键字:try、catch、finally、throw和throws。
try{
//可能发生异常的代码块
}catch(ExceptionType1 e1){
//捕获该类异常,并且进行处理
}catch(ExceptionTyp2 e2){
//捕获该类异常,并且进行处理
}finally{
//无论异常是否发生,最后都会执行finally语句块
}
异常类型
Java中所有异常都是内置类Throwable的子类,所以Throwable是所有异常类的顶级父类。Throwable下面有两个子类,它们对应着Java两类不同的异常类型,分别是Exception和Error。
Exception是我们使用最多的异常类,我们可以使用它捕获异常,也可以通过继承它来实现自己的异常类。
Error异常一般是Java运行时系统使用,它表示运行环境本身出现了错误,但是这类异常我们是无法处理的,它们只是为了响应灾难性的失败而创建的。比如堆栈溢出就属于这类异常。
tyr-catch运行原理
如果当异常发生时,我们没有捕获所发生的异常,最终将会由默认处理程序进行处理,默认处理程序会显示一个错误描述字符串,输出异常发生点的堆栈跟踪信息并最终终止程序执行。
如果我们使用try-catch来捕获异常,它能给我们带来两点好处:
- 允许我们根据业务修复错误。
- 防止程序自动停止。
try{
a = 1/0;
System.out.println("1");
}catch(Exception e){
System.out.println("2");
}
System.out.println("3");
//输出结果
1
3
当try中抛出异常时,程序会立即跳出try语句块,进入catch语句块,执行完catch语句块之后,会跳出整个try-catch语句,然后执行try-catch后面的语句。所以try语句块中发生异常点后面的语句是永远不会执行的。
Throwabl重写了Object类中的toString()方法,会返回包含描述异常信息的字符串,所以我们在catch中其实也可以打印出e。
当单个代码块出现多种异常时,可以使用多条catch语句块来捕获,当发生异常时会从上向下检索是否有对应的异常能够匹配,如果匹配到就会忽略下面的catch语句。所以我们要把所有子类异常放在前面,父类异常放在后面,因为父类异常能够捕获所有子类的异常。
在JDK7之后提供了使用一个catch捕获多个异常的使用方式:
try{
}catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
System.out.println(e)
}
throw和throws
我们不仅能够捕获Java系统抛出的异常,我们也可以在程序中显示的抛出异常。
throw ThrowableInstance;
ThrowInstance必须要是Throwable或其子类的实例对象。ThrowInstance对象一般通过new关键字创建,或者通过catch中的参数获取。
throw语句之后的语句都不会被执行,检查最近的try语句是否有与其匹配的catch语句,如果没有就继续向上抛。
throw new NullPointerException("demo");
大多数异常类都会至少有两个构造函数,无参构造函数和带一个参数的构造函数。第二种参数就是异常描述信息的字符串,可以通过直接打印异常获取,也可以通过getMessage()方法获取。
当我们抛出Error、RuntimeException以及子类时,如果方法本身不能处理这种异常,就需要使用throws关键字列出该方法可能抛出的异常列表。这样做的目标是使调用者能够对这种异常有准备。对于RuntimeException这类异常被称为未经检查的异常,因为编译器不检查方法是否被处理或抛出这类异常,所以不需要指定这类异常。
class A{
static throwExcep() throws IllegalAccessException{
throw new IllegalAccessException("a");
}
pulic static void main(String[] args){
try{
throwExcep();
}catch(IllegalAccessException e){
System.out.println(e)
}
}
}
除了在抛出方法上使用throws抛出异常类型,还需要调用者使用try-catch语句块来捕获这类异常。
自定义异常
我们也可以创建自己的异常类型,用来处理特定应用程序的情况。我们只需要继承Exception类或Throwable类,然后编写需要的构造方法即可,不需要实现任何方法。实际上Exception也没有实现任何方法,它的所有方法都是从Throwable继承来的。
class MyException extends Exception{
private String msg;
MyException(){
super();
}
MyException(String msg){
this.msg = msg;
}
public String toString(){
return "MyException[" + msg + "]"
}
}
链式异常
在JDK1.4之后Java增加了链式异常机制,也就是可以设置引起当前异常的异常是什么,这个链可以是任意层次的。我们可以通过构造方法添加链式异常,可以通过initCause(Throwable caseExc)来设置异常连,但是这个方法只能调用一次。
Class MyException extends Exception{
MyException(Throwable throwExc){
throwExc = new NullInportException("null");
}
}
public class A{
public staic void main(){
b();
}
static void b(){
NullPointerException e = new NullPointerException("top layer");
e.initCause(new ArithmeticException("cause"));
throw e;
}
}
对于那些需要查看异常背后原因的情况,链式异常是完美的解决方案。
枚举
从JDK5之后Java提供对枚举的支持,枚举就是具有一系列名称的常量(可以使用final来代替使用)。枚举的本质是一种类类型,它可以有构造函数、实例变量和方法,但是不能使用new去实例化枚举。
public enum Number{
ONE,TWO,THREE,FOUR,FIVE
}
枚举中定义的标识符ONE、TWO这些称为枚举常量,每个枚举常量被隐式的声明为Number的public、static和final的成员,也就说这些常量属于“自类型化的”,它们的类型就是定义它们的枚举。
Number num = Number.ONE;
if(num == Number.ONE){
//判断两个枚举常量是否相等
}
枚举可以用在switch语句中,case语句是枚举常量值,不需要使用枚举类名标识,因为switch表达式已经隐式的指定了case常量的类型。
switch(num){
case ONE:
//...
case TWO:
//...
}
values()和valueOf()方法
枚举类中有两个常用的方法,就是values()和valueOf()。values()方法会以数据的形式将枚举类中的所有枚举常量值列出来。valuesOf(String str)会返回与str字符串对应的枚举常量。
public static enum-type[] values();
public static enum-type valueOf(String str);
枚举是类类型
枚举本身是类类型,所以可以为其创建构造方法、实例变量和方法,它甚至可以实现接口。
public enum Number{
ONE(1),TWO(2),THREE(3),FOUR(4),FIVE(5)
private int price;
public Number(int price){
this.price = price;
}
public int getPrice(){
return price;
}
}
Number num;
Number.ONE.getPrice();
枚举类虽然不能使用new关键字创建实例,但是每个枚举常量都是枚举类的实例,所以在创建每个枚举常量的时候都会调用构造方法。当我们创建一个枚举常量时,每个枚举常量值都会调用一次构造函数,因为每个枚举常量都是枚举类的一个对象,所以它们的实例变量都是一个个副本,它们之间并没有任何联系。
Enum类
枚举本身有两条限制:第一,枚举不能继承其它类;第二:枚举不能是超类。所以这就意味着枚举不能扩展。
虽然枚举不能继承其它类,但是所有枚举类都默认继承了超类java.lang.Enum,这个类中定义了所有枚举类可以使用的一些方法。比如ordinal()获取枚举的序列值(从0开始),compareTo()比较相同类型枚举值的序列号。
类型封装器和自动拆装箱
类型封装器
java出于性能考虑,将常用的类型使用基本类型来表示。基本类型不是对象层次的组成,所以它们不继承Object类。但是有许多场景需要以对象的形式来来表示基本类型,比如Java中定义的标准数据结构(比如集合类型)、不能通过引用类型为方法传递基本类型等。
为了解决这些场景,Java为八种基本类型分别定义了对应的类型封装器:Character、Boolean、Byte、Short、Integer、Long、Float和Double。
每种类型封装器都定义从基本类型到封装类型的构造函数,也定义从封装类型到基本类型的方法。
//Character(char ch)
Character ch1 = new Character('a');
char ch2 = ch1.charValue();
//Boolean(boolean bol)和Boolean(String bol)
Boolean bo1 = new Boolean(true);
boolean bo2 = bo1.booleanValue();
对于所有数值类型的类封装器都继承自Number抽象类,Number中定义了以不同数据类型返回基本类型的方法:
byte byteValue();
shot shortValue();
int intValue();
long longValue();
float floatValue();
double doubleValue();
数值类型的封装器也都包含两类构造方法:对应的基本类型和字符串。
Integer(int num)
Integer(String num)
由基本类型到类型封装器的过程称之为装箱,由类型封装器到基本的类型过程称之为拆箱。
//装箱
Interger num = new Integer(123);
//拆箱
int num2 = num.intValue();
自动拆装箱
由于频繁调用拆装箱方法用起来很费事,Java在JDK5提供了自动拆装箱的功能,就不需要我们频繁的创建类型封装器和调用类型封装器方法了。只要我们需要基本类型对应的对象,就会自动的将基本类型装箱到与之等价的类型封装器中,这个过程叫做自动装箱。当我们需要基本类型时,会自动将类型封装器转换成基本类型,这个过程叫做自动拆箱。
//有了自动拆装箱机制,就可以轻易使用了
Integer ob = 100;
int num = ob;
自动拆装箱不仅用于简单的赋值,在任何需要基本类型与封装类型转换的时候,它都会自动进行拆装箱转换,比如方法调用。
public class A{
static int getNum(Integer num){
//自动拆箱
return num;
}
public static void main(String[] args){
//自动装箱
getNum(100);
}
}
当在表达式中,数值类型对象会被自动拆箱,然后进行表达式计算。
Integer ob = 1;
//这个过程会先自动拆箱,然后加1,最后在自动装箱为Integer
++ob;
Integer num1 = 100;
Double num2 = 12.5;
//num1自动拆箱后进行了类型转换
num2 = num2 + num1;
自动拆装箱除了提供便利外,还能防止错误的发生。比如下面将Integer类型手动拆箱为byte类型,导致数据出错。
Integer num = 1000;
int n = num.byteValue();
//n=-24
自动拆装箱的警告
虽然Java提供了自动拆装箱机制,但是我们应该在只能需要用类型封装器的时候使用它,一般情况下不应该使用。因为自动拆装箱的过程会增加开销,导致同样的代码比基本类型效率低很多。比如下面:
Integer a,b,c;
a = 1;
b = 2;
c = a + b;
System.out.printn(c)
这时候完全没有必要使用类型封装器,这样的代码效率更低。我们应该在一般情况下限制类型封装器的使用。
泛型
泛型是JDK5引入的,泛型在两方面改变了Java:第一,定义了新的语法元素;第二,改变了许多核心API中的类和方法。使用泛型可以以类型安全的方式创建需要兼容各种类型的类、接口和方法,比如许多算法虽然操作类型不同,但是算法逻辑是相同的,通过使用泛型可以达到定义一次算法,可以独立于特定的数据类型的使用。
泛型又称为为参数化类型,因为类、接口或方法中的类型,是通过方法传递的方式传递过来的,使用泛型的类、接口和方法,称为泛型类、泛型接口和泛型方法。
在泛型之前,是通过使用Object引用来操作各种类型的对象,但是使用Object存在两个问题:第一,需要手动强制类型转换;第二,匹配类型是不安全的,容易产生运行时错误。而这两点正也是泛型的优点。
单类型参数的泛型
class Gen<T>{
T ob;
Gen(T ob){
this.ob = ob;
}
T getOb(){
return ob;
}
}
T就是类型参数的名称,它本身是一个占位符,当创建对象时,将需要传递参数传递给该类型参数名称。类型参数定义在<>中,整个类体都可以使用参数类型。
Gen<Integer> ob = new Gen<Integer>();
将Integer类型作为类型参数传递给Gen类,这样Gen类所操作的类型都是Integer类型了。当创建泛型类时,Java编译器实际上没有传递不同版本的Gen类,它在编译期间就将所有泛型信息移除了,将其替换成特定的类型,从而使代码看似创建了不同版本的Gen类,但实际上他们虽然都是Gen的对象,但是它们实际不是相同的类型。移除泛型信息的过程,称为擦除。
对于泛型类有两点需要注意:1. 泛型只能使用引用类型,所以对于基本类型,需要使用类型封装器。2. 基于不同类型参数的泛型类型是不同的,这也是类型安全的有效保证机制
之所以使用泛型能够保证类型安全,因为这个过程消除了手动输入类型转换以及运行时进行类型检查。比如使用Object,在编译过程中编译器并不知道使用的数据类型,所以在取数据时需要进行类型转换,并且在使用中如果类型不匹配直接在运行时跑错。而使用泛型,可以将运行错误提前到编译时错误,这就是泛型的主要优势。
带有多个类型参数的泛型
泛型可以使用多个类型参数,它们之间只需要使用逗号分隔。
class TwoGen<T,V>{
T ob1;
V ob2;
TwoGen(T ob1,V ob2){
this.ob1 = ob1;
this.ob2 = ob2;
}
}
#创建对象
TwoGen<Integer,String> gen = new TwoGen<Integer,String>(123,"Hello");
创建泛型的一般形式:
class class-name<type-param-list> {
#创建对象一般形式
class-name<type-param-list> var-name = new class-name<type-param-list>(cons-arg-list);
在JDK7开始,可以使用简短方式创建泛型类的对象了,不需要在创建对象时指定泛型参数,因为它可以通过赋值变量来判定。
TwoGen gen = new TwoGen<>(123,"hello")
有界类型
使用前面的泛型定义可以传递任意类型的参数,但是有时候需要对传递的类型参数进行限定,比如该泛型类只能操作数值类型,因为它定义了一些涉及数值类型的操作。
class Stats<T extends Number>{
T[] nums;
Stats(T nums){
this.nums = nums;
}
double average(){
double sum = 0;
for(T num: nums){
sum += num.doubleValue();//Number类定义了该方法
}
return sum;
}
}
T extends Number 这种定义就是有界类型,也就是定义了能够使用的类型必须是Number及其子类。
#定义有界类型
<T extends superClass>
除了使用类作为边界外,还可以使用接口,并且可以指定多个接口,所以边界只能包括一个超类和多接口。比如下面T必须是MyClass类或其子类,并且实现了MyInterface接口。
class<T extends MyClass & MyInterface>
使用通配符
类型安全虽然有用,但是有时候会影响可以接受的数据结构。比如我要比较Integer类型和Double类型的平均值,使用上面的泛型就很难做到,因为一个方法如果使用泛型参数,那么它只能接受一种类型,也就是你所创建的数据类型类。但使用通配符就可以解决这类问题:
boolean sameAvg(Stats<?> ob){
if(average() == ob.average)
return true;
else
return false;
}
通配符使用?标识,通配符不会影响能够创建什么类型的Stats对象,这是因为通配符只能简单匹配有效的数据类型,比如上面Status是Number类及其子类,那么通配符只能匹配这些类。
有界通配符
我们也可以为通配符进行界定,因为在很多时候我们不需要通配符来匹配任意类型。有界的通配符可以为类型参数指定上界或者下界,从而限制方法的操作(通配符只能在方法中定义)。
//定义上界,只能包含superclass及其子类
<? extends superclass>
//定义下界,只能包含subclassd的超类
<? super subclass>
比如只能比较double类型:
boolean sameAvg(Stats<? extends Double> ob){
if(average() == ob.average)
return true;
else
return false;
}
创建泛型方法
上面的创建完泛型类之后,我们可以在方法中使用传递的参数类型。我们也可以独立于泛型类,创建一个或多个泛型方法,这些泛型方法可以在非泛型类中创建。
<type-param-list> return-type method-name(param-list){}
泛型方法实例,对泛型参数T进行了界定,只能是实现了Comparable接口的类型(可被排序的对象),泛型参数V是T类型或者T类型的子集。
public class GenMethDemo {
static <T extends Comparable<T>,V extends T> boolean isIn(T x,V[] y){
for(V v : y){
if(x.equals(v))
return true;
}
return false;
}
}
//使用泛型方法
GenMethDemo.isIn(2,Arrays.asList(1,2,3));
调用泛型方法可以直接传递参数,不需要指定类型,因为参数类型能够自动辨别。当然,我们也可以显示的指定,但是没有意义。
GenMethDemo.<Integer,Integer>isIn(2,Arrays.asList(1,2,3));
泛型方法可以是静态的,也可以不是,这个没有限定。
我们不仅可以定义普通泛型方法,还可以定义泛型的构造方法。
public classs GenCons{
private double val;
<T extends Number> GenCons(T arg){
val = arg.doubleValue();
}
}
泛型接口
除了可以定义泛型泛型类和泛型方法外,还可以定义泛型接口。泛型接口的定义和泛型类相似。
interface MinMax<T extends Comparable<T>>{
T min();
T max();
}
class MyClass<T extends Comparable<T>> implements MinMax{
...
}
这里需要注意的是,泛型接口的实现类必须需要与接口指定相同的界限,一旦建立这个界限,就不需要再在implements子句中指定了。
如果类实现了泛型接口,那么类也必须是泛型化的,因为它至少需要带有被传递给接口的类型参数。除非类实现了某种具体的泛型接口。
class MyClass implements MinMax<Integer>{
}
泛型接口有两个优势:一是可以针对不同类型的数据进行实现;二是为接口的数据类型设置限制条件。
泛型类的层次
泛型类也可以像普通类那样有层次关系,因此,泛型类可以作为超类或子类。如果超类是泛型类,那么所有子类都需要向上传递超类所需要的所有泛型参数(子类也可以定义自己的类型参数)。
class Gen<T>{}
class Gen2<T,V> extends Gen<T>{}
泛型类也可以继承非泛型类。
泛型类也可以使用instanceof运算符进行运行时比较:
Gen2<Integer,String> a = new Gen2<>();
if(a instanceof Gen<?>) //指定使用通配符,运行时不知道泛型类型信息
当两个泛型实例的类型相互兼容切它们的类型参数相同时,可以使用强制类型转换,
子类可以重写泛型方法。
擦除
Java使用擦除来实现的泛型。擦除原理:编译Java代码时,所有泛型信息都会被移除(擦错)。使用它们的界定类型替换为类型参数,如果没有指定界定类型,就使用Object。然后使用类型转换,以保持和指定的类型参数兼容。
所以运行时并没有类型参数语法,它只是一种源代码的机制。
之所以这样做的原因是,要和之前的Java版本兼容。
使用泛型的一些限制
- 不能实例化泛型参数,比如 new T();
- 静态成员(包括静态变量和方法)不可以使用泛型参数,但可以为静态方法创建泛型方法。
- 创建泛型类数组时,只能使用通配符。Gen<?>[] gens = new Gen<>[10];
- 泛型不能扩展Throwable,就是不能创建泛型异常类。
lambda表达式
对Java产生深远影响的两个功能就是JDK5增加的泛型和JDK8新增的lambda表达式。lambda作为新的语法元素,提升了java语言的表达能力,并且流线化了一些常用结构的实现方式。
什么是lambda表达式
lambda表达式本身是一个匿名方法,但是这个方法不能独立执行,它需要实现由函数式接口定义的方法。所以lambda表达式会产生一个匿名类。lambda表达式也常被称为闭包。
函数式接口是仅包含一个抽象方法的接口。这个方法指明了接口的用途,所以函数式接口通常表示单个动作。
lambda表达式使用操作符:->。左侧指定了lambda表达式所需要的所有参数(如果不需要参数,则使用空参数列表)。右侧指定了lambda体,就是要执行的动作,lambda体可以是单独的一个表达式,也可以是一个代码块。
() -> Math.random() * 100;
//n参数通过类型推断得到,也可以显示定义 (int n)
(n) - > n % 2 == 0
上面说了lambda表达式不是独立执行的(直接执行上面语句是错误的),因为lambda表达式是作为函数式接口中的抽象方法实现,这个函数式接口定义了lambda表达式的目标类型。我们只有在这个目标类型的上下文中(可以理解成接口定义的变量),才能使用lambda表达式
//只有一个方法的函数式接口
interface MyNumber{
double getValue();
}
//自动创建实现了函数式接口的一个匿名类实例,而该接口的方法实现使用lambda表示
MyNumber number = () -> 100.01;
System.out.println(number)
函数式接口可以执行任何与其兼容的lambda表达式,也就说可以有多种实现该接口的方法。
interface NumberTest{
boolean test(int x,int y);
}
NumberTest test = (n,d) -> n % d == 0;
System.out.println(test(2,3));
对于多个参数的lambda,如果想要显示声明一个参数的类型,那么必须为所有的参数类型显示声明。
NumberTest test = (int n,int d) -> n % d == 0;
lambda块体
lambda体可以包含单个表达式,称为表达式lambda;也可以是一个代码块,称为块lambda表达式。块lambda必须显示使用return语句返回值,因为块lambda不是一个单独表达式。
MyNumber sum = () -> {
int s = 0;
for(int i=0; i< 100;i++)
s += i;
return s;
}
泛型函数式接口
lambda表达式自身不能指定类型参数,但是可以定义泛型函数式接口,lambda表达式由泛型接口的引用类型来决定。
interface SomeFunc<T>{
T fun(T t);
}
SomeFunc<String> reverse = (str) - >{
...
}
SomeFunc<Integer> sum = (n) -> {
...
}
作为参数传递lambda表达式
lambda表达式的另一个常用方法就是作为参数传递给方法,这样就可以将可执行代码作为参数传递给方法,极大的增强了Java的表达力。
需要注意,将lambda表达式作为参数传递,那么接受lambda表达式的形参需要和该lambda表达式兼容的函数式接口。
interface StringFunc{
String func(String sr);
}
class Test{
//接受的是一个lambda表达式
stattic String strOp(StringFun fun,String str){
return fun.func(str);
}
public static void main(String[] args){
StringFunc fun1 = (str) -> {
...
}
String opResult = strOp(fun1,"hello");
String opResult2 = strOp((str) -> n.toUpperCase(),"hello")
}
}
lambda表达式抛出异常
lambda表达式也可以抛出异常,但是需要注意的是,如果抛出的异常是经检查的异常,那么必须在函数式接口中通过throws定义了。
interface StringFunc{
//抛出自定义的空异常
String func(String sr) throws EmptyException;
}
String str = (str) ->{
...
throw new EmptyException();
}
lambda表达式变量捕获
lambda表达式能够访问外层作用域定义的变量,比如外层定义的实例或者静态变量。
但是lambda表达式在使用外层作用域中的局部变量时,不能修改局部变量,因为它被认为是final类型的变量了。不仅不能在lambda表达式中修改,也不能在lambda表达式外修改。
int sum = 0;
public void test() {
TestFunc testFunc = () -> {
//因为sum是全局变量,所以修改没有问题
sum++;
};
}
public void test() {
int sum = 0;
TestFunc testFunc = () -> {
//不能修改局部变量
//sum++;
};
sum++;//如果lambda表达式使用了该变量,这里也不能修改
}
这种称为变量捕获,捕获的变量被默认设置为final类型了。
方法引用
lambda还有另一个重要的特性就是方法引用,方法引用提供了一种引用方法但是不执行方法的方式。这个特性需要和lambda一起使用,因为要引用的方法必须能够和函数式接口中的方法兼容。也就说任一普通方法,只要和函数式接口的方法兼容,就都可以作为引用方法传递给函数式类型的参数的方法。相当于我们将这个方法作为() -> {} lambda表达式使用了。
方法引用可以是静态方法引用或实例方法。
interface StringFun{
String func(String n);
}
class StringOps{
static String strReverse(String str) {
...
}
}
class Demo{
static String stringOp(StringFun fun,String str){
fun.func(str);
}
public static void main(String[] args) {
//将StringOps.strReverse()当做lambda使用
String result = stringOp(StringOps::strReverse(),"Hello");
}
}
也可以是实例方法
objRef :: methodName
还有一种,就是直接使用类名和实例方法,这样我们要引用的方法需要比函数式接口中的方法参数少一个,因为默认会将这个引用对象作为参数传递过去。这里要和静态方法进行区别。
ClassName :: instanceMethodName
除了可以使用普通方法引用,还可以使用构造函数引用。
interface MyFunc{
MyClass func(int n);
}
class MyClass{
MyClass(){
...
}
MyClass(int n){
}
}
class Main{
public static void main(String[] args) {
//构造方法应用 className :: new
MyFunc myFunc = MyClass :: new
myFunc.func(100);
}
}
预定义的函数式接口
JDK8中包含了一个新包java.util.function,里面提供了一些预定义的函数式接口。
//TODO
注解/元数据
在JDK5上,Java支持了在源文件中嵌入补充信息,这些补充信息对程序运行没有任何改变。一般用于开发和部署期间,各种工具使用这类信息。比如源代码生成器可以处理注解信息。
注解通过基于接口机制创建:
@interface MyAnno{
String str();
int val();
}
@告诉编译器这里声明的是注解类型,注解实体中包含了两个方法,和接口一样,这里只是声明方法,不做具体实现。具体实现由使用注解的地方实现。
注解不能包含extends子句,但是所有字节都自动扩展了java.lang.annotation.Annotation接口。
在声明完注解后,我们就可以来注解声明了(使用注解声明)。在JDK8之前注解只能用于声明,在JDK8中添加了使用注解类型的功能。
使用注解
在所有类型的声明中都可以与注解进行关联,比如类、方法、域变量、参数以及枚举常量,都可以带有注解。
@MyAnno(str = "hello", val = 100)
class MyClass{
}
应用注解时,需要为注解成员提供值,从这里看这些更像是变量不像抽象方法。
注解保留策略
注解保留策略决定了在什么时候丢弃注解。java定义了三种策略,他们被封装到java.lang.annotation.RetentionPolicy中。
- SOURCE: 只在源文件中保留,编译期间会被丢弃。
- CLASS: 在编译时被存储到.class文件中,但是在运行时JVM不能得到这些注解。
- RUNTIME: 编译时存储到.class文件中,并且运行时可以通过JVM获取到这些注解。RUNTIME提供了最永久的注解。
注意:局部变量的注解不能存储到.class文件中
保留策略通过内置注解@Retention指定:
@Retention(retention-policy)
retention-policy必须是RetentionPolicy中的常量,如果没有指定,默认就是CLASS。
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnno{
...
}
运行时通过反射获取注解
我们除了在其它开发和部署工具上使用注解,对于RUNTIME保留策略的注解,可以在运行时通过反射来查询注解。反射能够获取运行时类相关信息,反射API位于java.lang.reflect包中。
使用反射获取注解分为三步:
- 获取Class对象,Class是在java.lang包中定义的,可以通过多种方式获取class。比如getClass()方法。
- 获取Class对象后,可以使用它的方法获取与类声明的相关的信息,包括注解。Class提供了getMethod()、getField()、以及getConstructor(),他们分别返回Method、Field和Constructor。
- 使用Method、Field或Constructor对象调用getAnnotation()方法,就可以获取与对象关联的注解了。
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnno{
String str();
int val();
}
class Test{
@MyAnno(str = "hello", val = 100)
public static myMethod(int num){
Test test = new Test();
Class<?> clazz = test.getClass();
//如果方法有参数,需要指定参数类型
Method m = clazz.getMethod("myMethod",int.class);
MyAnno myannno = m.getAnnotation(MyAnno.class);
System.out.println(myanno.str())
}
}
为注解使用默认值
在定义注解的时候,我们可以为注解指定默认值,当使用注解时候,如果没有为注解指定值,就会使用默认值。
@interface MyAnno{
String str() default "hello";
int val() default 100;
}
//使用
@MyAnno()
@MyAnno(str = "test")
@MyAnno(val = 1)
@MyAnno(str = "test",val = 1)
还有一类特殊使用注解,就是单成员注解。如果注解中只有一个成员,我们可以将成员名称定义为value,这时候在使用时候就可以不指定成员名称了。
@interface MyAnno{
String value();
}
@MyAnno("hello")
class Test{
如果有多个成员,并且其它所有成员都有默认值的话,也可以这样使用。不过如果显示使用默认值的时候,这时候在调用的时候也要显示value=xxxx。
标记注解
标记注解时特殊类型的注解,他没有成员(定义完注解明后后就完事了)。标记注解的唯一目的就是标记。确定标记注解的最好方式就是使用反射java.lang.reflect.AnnotatedElement类中的isAnnotationPreset()方法判断。
@interface MyAnno{
}
内置注解
Java提供了许多内置注解,大部分都是专用注解,但是有9个用于一般目的。java.lang.annotation中的@Retention、@Documented、@Target、@Inherited,java.lang中的@Override、@Deprecated、@FunctionalInterface、@SafeVarargs和@SuppressWarnning。
- @Retention:只能用于注解其它注解,用于指定保留策略。
- @Documented:标记注解,只能注解其它注解,用于提示其它工具,被注解的注解将被文档化。
- @Target:只能注解其它注解,用来表示注解的声明类型(它告诉你它标记的这个注解是用来干什么的,使用ElementType),如果声明该注解用于类的。
- @Inherited:标记注解,只能注解其它注解。并且@Inherited只能影响类声明的注解,会将超类的注解被子类继承。
- @Override:标记注解,只能用于方法。使用@Override注解的方法,必须重写超类中的方法。它用于确保超类方法被重写,而不是简单的重载。
- @Deprecatd:标记注解,用于指示是过时的了。
- @FunctionalInterface: JDK8新增注解的标记注解,用于接口。指出被注解的接口是函数式接口。
- @SafeVarags:标记注解,只能用于方法和构造函数,指示没有发生于可变长度参数相关的不安全动作。
- @SuppressWarnings:抑制一个或多个编译器可能会报告的警告,就是我们IDEA中黄色表示的部分。
类型注解
JDK8新增了使用注解的地方,就是类型注解,也就是说注解可以用于标识类型。比如返回类型、this、强制转换、被继承的类以及throws子句或者注解泛型。类型注解需要使用@Target(ElementType.TYPE_USE)标识。
class Test{
//注解this
int method(@TypeAnno Test this,int i,int j){
}