异常处理

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception,或者用throws声明;
  • 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类;
  • Java使用异常来表示错误,并通过try ... catch捕获异常;
  • Java的异常是class,并且从Throwable继承;
  • 不推荐捕获了异常但不进行任何处理。

捕获异常

使用try语句定义可能出错的代码块,如果发生错误则执行catch语句定义的代码块,存在多个catch的时,按照顺序执行,当父类异常在前则不执行后面的子类异常,不管结果如何都会执行finally代码块,finally是可选的,一个catch语句也可以匹配多个非继承关系的异常。

package com.lsaiah.java;

public class ExceptionDemo01 {
    public static void main(String[] args) {
        try {
            int[] numbers = {1,2,3};
            System.out.println(numbers[10]);
        }catch (Exception e) {
            e.printStackTrace();
            System.out.println("ArrayIndexOutOfBoundsException");
        }finally {
            System.out.println("The 'try catch' is finished.");
        }
    }
}

抛出异常

  • 调用printStackTrace()可以打印异常的传播栈,对于调试非常有用;
  • 捕获异常并再次抛出新的异常时,应该持有原始异常信息;
  • 通常不要在finally中抛出异常。如果在finally中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()获取所有添加的Suppressed Exception

throw关键字与异常类型(在Java中可用的许多异常类型:ArithmeticException,FileNotFoundException,ArrayIndexOutOfBoundsException,SecurityException等)自定义错误:

package com.lsaiah.java;

public class ExceptionDemo02 {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    static void process1() {
        try {
            process2();
        } catch (NullPointerException e) {
//            throw new IllegalArgumentException();   //丢失原始异常NullPointerException
            throw new IllegalArgumentException(e);  //把原始的Exception实例传进去
        }
    }
    static void process2() {
        throw new NullPointerException();
    }
}

trycatch语句块中抛出异常,不会影响finally执行,JVM会先执行finally后抛出异常。如果再finally语句中抛出异常,那么catch语句的异常被屏蔽(Suppressed Exception)。

自定义异常

Java的异常是class,它的继承关系如下:

                     ┌───────────┐
                     │  Object   │
                     └───────────┘
                           ▲
                           │
                     ┌───────────┐
                     │ Throwable │
                     └───────────┘
                           ▲
                 ┌─────────┴─────────┐
                 │                   │
           ┌───────────┐       ┌───────────┐
           │   Error   │       │ Exception │
           └───────────┘       └───────────┘
                 ▲                   ▲
         ┌───────┘              ┌────┴──────────┐
         │                      │               │
┌─────────────────┐    ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘    └─────────────────┘└───────────┘
                                ▲
                    ┌───────────┴─────────────┐
                    │                         │
         ┌─────────────────────┐ ┌─────────────────────────┐
         │NullPointerException │ │IllegalArgumentException │...
         └─────────────────────┘ └─────────────────────────┘

Java标准库定义的常用异常包括:

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
  • 抛出异常时,尽量复用JDK已定义的异常类型;
  • 自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;
  • 自定义异常时,应该提供多种构造方法。
package com.lsaiah.java;
//根异常从RuntimeException派生并创建构造方法:
public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }
    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}
//其它业务异常从BaseException派生:
class UserNotFoundException extends BaseException{

    public UserNotFoundException(String error_message) {
        super(error_message);
    }
}

class LoginFailedException extends BaseException{
    public LoginFailedException(String error_mess) {
        super(error_mess);
    }
}
package com.lsaiah.java;
//在主类中调用异常
public class Main {
    public static void main(String[] args) {
        try {
            String token = login("admin", "pass");
            System.out.println("Token: " + token);
        }catch (LoginFailedException | UserNotFoundException e){
            e.printStackTrace();
        }
    }
    static String login(String username, String password){
        if (username.equals("admin")){
            if (password.equals("password")){
                return "xxx";
            }else {
                throw new LoginFailedException("登录失败:用户名或密码错误");
            }
        }else{
            throw new UserNotFoundException("用户不存在");
        }
    }
}

NullPointerException

  • NullPointerException是Java代码常见的逻辑错误,应当早暴露,早修复;
  • java -XX:+ShowCodeDetailsInExceptionMessages Main.java 可以启用Java 14的增强异常信息来查看NullPointerException的详细错误信息。
public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        System.out.println(p.address.city.toLowerCase());
    }
}

class Person {
    String[] name = new String[2];
    Address address = new Address();
}

class Address {
    String city;
    String street;
    String zipcode;
}

断言

  • 断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言。JVM默认关闭断言指令,要执行assert语句需要在执行时传递参数-enableassertions可简写-ea启用断言,还可以有选择地对特定地类-ea:com.itranswarp.sample.Main或特定的包-ea:com.itranswarp.sample...启用断言;
  • 对可恢复的错误不能使用断言,而应该抛出异常;
  • 断言很少被使用,更好的方法是编写单元测试。
public static void main(String[] args) {
    double x = Math.abs(-123.45);
    assert x >= 0 : "x must >= 0";
    System.out.println(x);
}

使用JDK Logging

Java标准库内置了日志包java.util.logging使用JDK Loggin,定义了7个日志级别:

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

默认级别INFO,INFO以下的日志不会被打印出来,需要在main()方法运行之前修改配置,在JVM启动时传递参数-Djava.util.logging.config.file=

  • 日志是为了替代System.out.println(),可以定义格式,重定向到文件等;
  • 日志可以存档,便于追踪问题;
  • 日志记录可以按级别分类,便于打开或关闭某些级别;
  • 可以根据配置文件调整日志,无需修改代码;
  • Java标准库提供了java.util.logging来实现日志功能。
package com.lsaiah.java;

import java.io.UnsupportedEncodingException;
import java.util.logging.Logger;

public class LoggingDemo {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger(LoggingDemo.class.getName());
        logger.info("Start Process ...");
        try {
            "".getBytes("invalidCharsetName");
        } catch (UnsupportedEncodingException e) {
            // TODO: 使用logger.severe()打印异常
            logger.severe(e.toString());
        }
        logger.info("Process end ...");
    }
}

使用Commons Logging

  • Commons Logging是使用最广泛的日志模块;
  • Commons Logging的API非常简单;
  • Commons Logging是一个第三方提供的库可以自动检测并使用其他日志模块,默认使用Log4j,如果没有再使用JDK Logging,使用前先导入commons-logging-x.x.jar,在编译javac -cp commons-logging-x.x.jar Main.java和执行java -cp .;commons-logging-x.x.jar Main时需要指定classpath。

Commons Logging定义了6个日志级别:

  • FATAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG
  • TRACE
// 在静态方法中引用Log:
public class Main {
    static final Log log = LogFactory.getLog(Main.class);

    static void foo() {
        log.info("foo");
    }
}

// 在实例方法中引用Log:
public class Person {
    protected final Log log = LogFactory.getLog(getClass());

    void foo() {
        log.info("foo");
    }
}

// 在子类中使用父类实例化的log:
public class Student extends Person {
    void bar() {
        log.info("bar");
    }
}
//使用重载方法打印出异常:error(String, Throwable)
try {
    ...
} catch (Exception e) {
    log.error("got exception!", e);
}

使用Log4j

  • 导入Log4j的Jar包,通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
  • 使用Log4j只需要把log4j2.xml和相关jar放入classpath;
  • 如果要更换Log4j,只需要移除log4j2.xml和相关jar;
  • 只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。

Log4j参考配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <!-- 定义日志格式 -->
        <Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
        <!-- 定义文件名变量 -->
        <Property name="file.err.filename">log/err.log</Property>
        <Property name="file.err.pattern">log/err.%i.log.gz</Property>
    </Properties>
    <!-- 定义Appender,即目的地 -->
    <Appenders>
        <!-- 定义输出到屏幕 -->
        <Console name="console" target="SYSTEM_OUT">
            <!-- 日志格式引用上面定义的log.pattern -->
            <PatternLayout pattern="${log.pattern}" />
        </Console>
        <!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
        <RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
            <PatternLayout pattern="${log.pattern}" />
            <Policies>
                <!-- 根据文件大小自动切割日志 -->
                <SizeBasedTriggeringPolicy size="1 MB" />
            </Policies>
            <!-- 保留最近10份 -->
            <DefaultRolloverStrategy max="10" />
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level="info">
            <!-- 对info级别的日志,输出到console -->
            <AppenderRef ref="console" level="info" />
            <!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
            <AppenderRef ref="err" level="error" />
        </Root>
    </Loggers>
</Configuration>

使用SL4J和Logback

  • SLF4J和Logback可以取代Commons Logging和Log4j,SLF4接口更方便,Logback性能更好;
  • 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。
Commons Logging SLF4J
org.apache.commons.logging.Log org.slf4j.Logger
org.apache.commons.logging.LogFactory org.slf4j.LoggerFactory

Logback配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <file>log/output.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>log/output.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>1MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

反射

反射为了解决在运行期,对某个实例一无所知的情况下调用方法。由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。这种通过Class实例获取class信息的方法称为反射(Reflection)

Class类

  • JVM为每个加载的classinterface创建了对应的Class实例来保存classinterface的所有信息;
  • 获取一个class对应的Class实例后,就可以获取该class的所有信息;
  • 通过Class实例获取class信息的方法称为反射(Reflection);
  • JVM总是动态加载class,可以在运行期根据条件来控制加载class。

获取classClass实例有三种方法:

方法一:直接通过一个class的静态变量class获取:

Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

因为Class实例在JVM中是唯一的,所以上述方法获取的Class实例是同一个实例。

  • instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,而不能对子类型比较。
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; //true,因为n是Integer类型
boolean b2 = n instanceof Number; //true,因为n是Number类型的子类
//当获取实例n时,通过n.getClass()反射获取Integer的class信息
boolean b3 = n.getClass() == Integer.class; //true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; //false,因为返回的Integer.class != Number.class
  • 如果获取到一个Class实例,可以通过该Class实例来创建对应类型class的实例。相当于new String(),通过cls.newInstance()创建的实例具有局限性,只能调用public的无参构造方法,对于带参数和非public的构造方法无法被调用。
//获取String的Class实例
Class cls = String.class;
//创建String的实例
String s = (String)cls.newInstance();
  • 利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:
// Commons Logging优先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
    factory = createLog4j();
} else {
    factory = createJdkLog();
}

boolean isClassPresent(String name) {
    try {
        Class.forName(name);
        return true;
    } catch (Exception e) {
        return false;
    }
}

访问字段

  • Java的反射API提供的Field类封装了字段的所有信息:
  • 通过Class实例的方法可以获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()
  • 通过Field实例可以获取字段信息:getName()getType()getModifiers()
  • 通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
  • 通过反射读写字段是一种非常规方法,它会破坏对象的封装。更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下获取特定字段的值。JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)

Field getField(name):根据字段名获取某个public的field(包括父类)

Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)

Field[] getFields():获取所有public的field(包括父类)

Field[] getDeclaredFields():获取当前类的所有field(不包括父类)

package com.lsaiah.java;
import java.lang.reflect.Field;

public class FieldDemo {
    public static void main(String[] args) throws Exception {
        Person05 p = new Person05("张三");    //创建class的实例p
        System.out.println(p.getName());    //通过实例.方法直接调用
        Class c = p.getClass(); //通过反射获取实例p的Class
        Field f = c.getDeclaredField("name"); //创建Field的实例f,通过p的Class调用getDeclaredField方法根据字段名获取当前类的某个field
        f.setAccessible(true); //设置private字段可访问
        Object value = f.get(p); //获取字段的值
        System.out.println(value);
        f.set(p,"李四");  //设置字段的值
        Object newValue = f.get(p);
        System.out.println(newValue);
    }
}
class Person05{
    public Person05(String name) {
        this.name = name;
    }
    public String getName() {
        return this.name;
    }
    private String name;
}

Method getMethod(name, Class...):获取某个public的Method(包括父类) Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类) Method[] getMethods():获取所有public的Method(包括父类) Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

  • Java的反射API提供的Method对象封装了方法的所有信息:
  • 通过Class实例的方法可以获取Method实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
  • 通过Method实例可以获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()
  • 通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
  • 通过设置setAccessible(true)来访问非public方法;
  • 通过反射调用方法时,仍然遵循多态原则,即总是调用实际类型的覆写方法(如果存在)。
//通过获取到Method对象调用非静态public方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args){
        String s = "Hello world";
        Method m = String.class.getMethod("substring", int.class);  //获取String substring(int)方法,参数为int
        String r = (String) m.invoke(s,6);  //调用该方法并获取结果
        System.out.println(r);
    }
}

//调用静态方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args){
        Method m = Integer.class.getMethod("parseInt", String.class);   //获取Integer.parseInt(String)方法,参数为String
        Integer n = (Integer) m.invoke(null,"12345");   //调用该静态方法并获取结果
        System.out.println(n);
    }
}

//调用非public方法
import java.lang.reflect.Method;
public class Main{
    public static void main(String[] args) {
        Person p = new Person();
        Method m = p.getClass().getDeclaredMethod("setName", String.class);
        m.setAccessible(true);
        m.invoke(p, "Bob");
        System.out.println(p.name)
    }
}
class Person{
    String name;
    private void setName(String name){
        this.name = name;
    }
}

调用构造方法

  • Constructor对象封装了构造方法的所有信息;
  • 通过Class实例的方法可以获取Constructor实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()
  • 通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。

通常调用方法使用new创建新的实例:

Person p = new Person();

通过反射创建新的实例,可以通过Class提供的newInstance()方法:

Person p = Person.class.newInstance();

但是通过反射调用构造方法具有局限性,只能调用该类public无参构造方法。有参数或者不是public的构造方法就无法使用class.newInstance()调用。

为了调用任意构造方法,Java反射的API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。

import java.lang.reflect.Constructor;
publci class Main{
    public static void main(String[] args){
        //获取构造方法Integer(int):
        Constructor cons1 = Integer.class.getConstructor(int.class);
        //调用构造方法:
        Integer n1 = (Integer) cons1.newInstance(123);
        System.out.print(n1);
        //获取构造方法Integer(String):
        Constructor cons2 = Integer.class.getConstructor(String.class);
        Integer n2 = (Integer) cons2.newInstance("456");
        System.out.println(n2);
    }
}

获取继承关系

通过Class对象可以获取继承关系:

  • Class getSuperclass():获取父类类型;
  • Class[] getInterfaces():获取当前类实现的所有接口。
  • 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

动态代理

所有interface类型的变量总是通过向上转型并指向某个实例,静态代码:

//定义接口:
public interface Hello {
    void morning(String name);
}

//编写实现类:
public class HelloWorld implements Hello {
    public void morning(String name){
        System.out.println("Good morning," + name);
    }
}

//创建实例,HelloWorld向上转型为Hello接口并调用:
Hello helo = new HelloWorld();
hello.morning("Bob");

Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;

动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main{
    public static void main(String[] args) {
        //1. 定义一个InvocationHandler实例,它负责实现接口的方法调用
        InvocationHandler handler = new InvocationHandler(){
            //3. 将返回的Object强制转换为接口
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throw Throwable {
                System.out.println(method);
                if(method.getName().equals("morning")){
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        /*
         * 2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数
         *  a. Hello.class.getClassLoader(),使用的ClassLoader,通常就是接口类的ClassLoader。
         *  b. new Class[] { Hello.class },需要实现的接口数组,至少需要传入一个接口进去
         *  c. handler,用来处理接口方法调用的InvocationHandler实例
         */
        Hello hello = (Hello) Proxy.newProxyInstance(
            Hello.class.getClassLoader(),
            new Class[] { Hello.class },
            handler);
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

注解

使用注解

  • 注解(Annotation)是Java语言用于工具处理的标注:
  • 注解可以配置参数,没有指定配置的参数使用默认值;
  • 如果参数名称是value,且只有一个参数,那么可以省略参数名称。
public class Hello {
    @Check(min=0, max=100, value=55)
    public int n;

    @Check(value=99)
    public int p;

    @Check(99) // @Check(value=99)
    public int x;

    @Check
    public int y;
}

@Check就是一个注解。第一个@Check(min=0, max=100, value=55)明确定义了三个参数,第二个@Check(value=99)只定义了一个value参数,它实际上和@Check(99)是完全一样的。最后一个@Check表示所有参数都使用默认值。

定义注解

  • Java使用@interface定义注解:
  • 可定义多个参数和默认值default,核心参数使用value名称;
  • 必须设置@Target来指定Annotation可以应用的范围;
  • 应当设置@Retention(RetentionPolicy.RUNTIME)便于运行期读取该Annotation
  • 非必须要元注解:@Inherited定义子类是否可继承父类定义的注解,仅针对@Target(ElementType.TYPE)类型的类注解有效,对interface无效。@Repeatable可定义注解是否可重复,经过@Repatable修饰的注解在某个类型声明处可以添加多个@Report注解。注解@Retention定义了注解的生命周期,仅编译期:RetentionPolicy.SOURCE,仅class文件:RetentionPolicy.CLASS,运行期:RetentionPolicy.RUNTIME

处理注解

可以在运行期通过反射读取RUNTIME类型的注解,注意不要漏写@Retention(RetentionPolicy.RUNTIME),否则运行期无法读取到该注解。

判断某个注解是否存在于ClassFieldMethodConstructor

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)

使用反射API读取Annotation:

  • Class.getAnnotation(Class)
  • Field.getAnnotation(Class)
  • Method.getAnnotation(Class)
  • Constructor.getAnnotation(Class)

使用反射API读取Annotation有两种方法。方法一是先判断Annotation是否存在,如果存在,就直接读取:

Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
    Report report = cls.getAnnotation(Report.class);
    ...
}

第二种方法是直接读取Annotation,如果Annotation不存在,将返回null

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
   ...
}

读取方法参数的Annotation可以将参数看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

要读取方法参数的注解,我们先用反射获取Method实例,然后读取方法参数的所有注解:

// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
    if (anno instanceof Range) { // @Range注解
        Range r = (Range) anno;
    }
    if (anno instanceof NotNull) { // @NotNull注解
        NotNull n = (NotNull) anno;
    }
}

如何使用注解由程序决定,如@Range注解,用来定义一个String字段的长度满足@Range参数定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
    int min() default 0;
    int max() default 255;
}

在JavaBean中,可以使用该注解:

public class Person {
    @Range(min=1, max=20)
    public String name;

    @Range(max=10)
    public String city;
}

编写一个Person实例的检查方法,它可以检查Person实例的String字段长度是否满足@Range的定义:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
    // 遍历所有Field:
    for (Field field : person.getClass().getFields()) {
        // 获取Field定义的@Range:
        Range range = field.getAnnotation(Range.class);
        // 如果@Range存在:
        if (range != null) {
            // 获取Field的值:
            Object value = field.get(person);
            // 如果值是String:
            if (value instanceof String) {
                String s = (String) value;
                // 判断值是否满足@Range的min/max:
                if (s.length() < range.min() || s.length() > range.max()) {
                    throw new IllegalArgumentException("Invalid field: " + field.getName());
                }
            }
        }
    }
}

注解练习

使用@Range注解来检查Java Bean的字段。如果字段类型是String,就检查String的长度,如果字段是int,就检查int的范围。

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
//定义注解
@Retention(RetentionPolicy.RUNTIME) //定义注解声明周期为运行时(类型的注解会被加载进JVM,并且在运行期可以被程序读取。)
@Target(ElementType.FIELD)  //定义注解的位置为字段
public @interface Range {
    int min() default 0;
    int max() default 255;
}


public class Person {
    @Range(min = 1, max = 20)
    public String name;
    @Range(max = 10)
    public String city;
    @Range(min = 1, max = 100)
    public int age;

    public Person(String name, String city, int age) {
        this.name = name;
        this.city = city;
        this.age = age;
    }
    @Override
    public String toString() {
        return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
    }
}


import java.lang.reflect.Field;

public class Main {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person("Bob", "Beijing", 20);
        Person p2 = new Person("", "Shanghai", 20);
        Person p3 = new Person("Alice", "Shanghai", 199);
        for (Person p : new Person[] { p1, p2, p3 }) {
            try {
                check(p);
                System.out.println("Person " + p + " checked ok.");
            } catch (IllegalArgumentException e) {
                System.out.println("Person " + p + " checked failed: " + e);
            }
        }
    }

    static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
        //通过 foreach循环,依次获取person这个实例的public的字段的字段名称(数据类型为Field)
        for (Field field : person.getClass().getFields()) {
            //通过Filed类的 .getAnnotation 方法来获得注解
            Range range = field.getAnnotation(Range.class);
            //判断注解是否为null
            if (range != null) {
                //使用了注解的情况下,通过Filed的 .get方法,来获取指定字段的字段值
                Object value = field.get(person);
                //如果是对 String字段使用的注解
                if (value instanceof String) {
                    String s = (String) value;
                    if (s.length()<range.min() || s.length()>range.max()) {
                        throw new IllegalArgumentException("Invalid filed: "+field.getName());
                    }
                }
                //如果是对 int字段使用的注解
                if (value instanceof Integer) {
                    int i = (int) value;
                    if (i<range.min() || i>range.max()) {
                        throw new IllegalArgumentException("Invalid filed: "+field.getName());
                    }
                }
            }
        }
    }
}

泛型

介绍

  • 泛型就是编写模板代码来适应任意类型;
  • 泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;
  • 注意泛型的继承关系:可以把ArrayList向上转型为ListT不能变!),但不能把ArrayList向上转型为ArrayListT不能变成父类)。

当数组的类型被定义后,添加不同类型的元素需要强制类型转换,可能发生误转型出现ClassCastException,对不同类型的编写不同的ArrayList不全面且麻烦,为了解决问题需要使用泛型将ArrayList变成一种新的模板ArrayList

public class ArrayList<T> {
    private T[] array;
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

T`可以是任何类型,这样只要编写一种模板就可以创建任意类型的`ArrayList
//创建可存储String的ArrayList
ArrayList<String> strList = new ArrayList<String>();
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
//创建可存储Float的ArrayList
ArrayaList<Float> floatList = new ArrayList<Float>();
//创建可存储Person的ArrayList
ArrayList<Person> personList = new ArrayList<Person>();

使用泛型

  • 使用泛型时,把泛型参数`替换为需要的class类型,例如:ArrayListArrayList`等;
  • 可以省略编译器能自动推断出的类型,例如:List list = new ArrayList<>();
  • 不指定泛型参数类型时,编译器会给出警告,且只能将`视为Object`类型;
  • 可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。
//自定义的类使用泛型接口实现Array.sort();
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Person[] ps = new Person[] {
            new Person("Bob", 61),
            new Person("Alice", 88),
            new Person("Lily", 75),
        };
        Arrays.sort(ps);
        System.out.println(Arrays.toString(ps));
    }
}
class Person implements Comparable<Person> {
    String name;
    int score;
    Person(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public int compareTo(Person other) {
        return this.name.compareTo(other.name);
    }
    public String toString() {
        return this.name + "," + this.score;
    }
}

编写泛型

  • 编写泛型时,需要定义泛型类型``;
  • 静态方法不能引用泛型类型,必须定义其他类型(例如)来实现静态泛型方法;
  • 泛型可以同时定义多种类型,例如Map
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 静态泛型方法应该使用其他类型区分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
}

擦拭法

擦拭法决定了泛型``:

  • 不能是基本类型,例如:int,因为实际类型是Object无法持有基本类型;
  • 不能获取带泛型类型的Class,例如:Pair.class,因为T类型getClass()返回同一个Class实例,在编译后都是
  • 不能判断带泛型类型的类型,例如:x instanceof Pair,因为不存在实际的类型.class,只有唯一的类.class;
  • 不能实例化T类型,例如:new T()擦拭后变成new Object(),需要借助Class参数,通过反射来实例化T类型,使用的时候也必须传入Class

泛型方法要防止重复定义方法,例如:public boolean equals(T obj)

子类可以获取父类的泛型类型``。

extends通配符

使用类似``通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似``定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

super通配符

使用类似``通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能写不能读。

使用extendssuper通配符要遵循PECS(Producer Extends Consumer Super)原则。

无限定通配符很少使用,可以用替换,同时它是所有``类型的超类。

泛型和反射

  • 部分反射API是泛型,例如:ClassConstructor
  • 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型;
  • 可以通过Array.newInstance(Class, int)创建T[]数组,需要强制转型;

同时使用泛型和可变参数时需要注意:

import java.util.Arrays;

public class Main {    
    public static void main(String[] args) {
        String[] arr = asArray("one", "two", "three");
        System.out.println(Arrays.toString(arr));
        // ClassCastException:
        String[] firstTwo = pickTwo("one", "two", "three");
        System.out.println(Arrays.toString(firstTwo));
    }
    //在pickTwo()方法内部,编译器无法检测K[]的正确类型,因此返回了Object[]
    static <K> K[] pickTwo(K k1, K k2, K k3) {
        return asArray(k1, k2);
    }
    @SafeVarargs
    static <T> T[] asArray(T... objs) {
        return objs;
    }
Last modification:May 18th, 2020 at 10:39 pm
如果觉得我的文章对你有用,请随意赞赏,感谢!