Java基础语法回顾(二)

异常处理

异常就是运行时的代码序列引起非正常情况。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版本兼容。

使用泛型的一些限制

  1. 不能实例化泛型参数,比如 new T();
  2. 静态成员(包括静态变量和方法)不可以使用泛型参数,但可以为静态方法创建泛型方法。
  3. 创建泛型类数组时,只能使用通配符。Gen<?>[] gens = new Gen<>[10];
  4. 泛型不能扩展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包中。
使用反射获取注解分为三步:

  1. 获取Class对象,Class是在java.lang包中定义的,可以通过多种方式获取class。比如getClass()方法。
  2. 获取Class对象后,可以使用它的方法获取与类声明的相关的信息,包括注解。Class提供了getMethod()、getField()、以及getConstructor(),他们分别返回Method、Field和Constructor。
  3. 使用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){
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容