基础2

异常处理

Java的异常

在计算机程序运行的过程当中,老是会出现各类各样的错误。java

有一些错误是用户形成的,好比,但愿用户输入一个int类型的年龄,可是用户的输入是abc程序员

// 假设用户输入了abc:
String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!

程序想要读写某个文件的内容,可是用户已经把它删除了:数组

// 用户删除了该文件:
String t = readFile("C:\\abc.txt"); // FileNotFoundException!

Java内置了一套异常处理机制,老是使用异常来表示错误。安全

异常是一种class,所以它自己带有类型信息。异常能够在任何地方抛出,但只须要在上层捕获,这样就和方法调用分离了:网络

try {
    String s = processFile(“C:\\test.txt”);
    // ok:
} catch (FileNotFoundException e) {
    // file not found:
} catch (SecurityException e) {
    // no read permission:
} catch (IOException e) {
    // io error:
} catch (Exception e) {
    // other error:
}

由于Java的异常是class,它的继承关系以下:框架

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

从继承关系可知:Throwable是异常体系的根,它继承自ObjectThrowable有两个体系:ErrorExceptionError表示严重的错误,程序对此通常无能为力,例如:ide

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:没法加载某个Class
  • StackOverflowError:栈溢出

Exception则是运行时的错误,它能够被捕获并处理。工具

某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:单元测试

  • NumberFormatException:数值类型的格式错误
  • FileNotFoundException:未找到文件
  • SocketException:读取网络失败

还有一些异常是程序逻辑编写不对形成的,应该修复程序自己。例如:测试

  • NullPointerException:对某个null的对象调用方法或字段
  • IndexOutOfBoundsException:数组索引越界

Exception又分为两大类:

  1. RuntimeException以及它的子类;
  2. RuntimeException(包括IOExceptionReflectiveOperationException等等)

Java规定:

  • 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
  • 不须要捕获的异常,包括Error及其子类,RuntimeException及其子类。

捕获异常

捕获异常使用try...catch语句,把可能发生异常的代码放到try {...}中,而后使用catch捕获对应的Exception及其子类

public class Main {
    public static void main(String[] args) {
        byte[] bs = toGBK("中文");
        System.out.println(Arrays.toString(bs));
    }

    static byte[] toGBK(String s) {
        try {
            // 用指定编码转换String为byte[]:
            return s.getBytes("GBK");
        } catch (UnsupportedEncodingException e) {
            // 若是系统不支持GBK编码,会捕获到UnsupportedEncodingException:
            System.out.println(e); // 打印异常信息
            return s.getBytes(); // 尝试使用用默认编码
        }
    }
}

若是咱们不捕获UnsupportedEncodingException,会出现编译失败的问题

编译器会报错,错误信息相似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,而且准确地指出须要捕获的语句是return s.getBytes("GBK");。意思是说,像UnsupportedEncodingException这样的Checked Exception,必须被捕获。

这是由于String.getBytes(String)方法定义是:

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
    ...
}

在方法定义的时候,使用throws Xxx表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,不然编译器会报错。

小结

Java使用异常来表示错误,并经过try ... catch捕获异常;

Java的异常是class,而且从Throwable继承;

Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;

RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;

不推荐捕获了异常但不进行任何处理。

捕获异常

在Java中,凡是可能抛出异常的语句,均可以用try ... catch捕获。把可能发生异常的语句放在try { ... }中,而后使用catch捕获对应的Exception及其子类。

多catch语句

可使用多个catch语句,每一个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,而后_再也不_继续匹配。

简单地说就是:多个catch语句只有一个能被执行。例如:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println(e);
    } catch (NumberFormatException e) {
        System.out.println(e);
    }
}

存在多个catch的时候,catch的顺序很是重要:子类必须写在前面。例如:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (IOException e) {
        System.out.println("IO error");
    } catch (UnsupportedEncodingException e) { // 永远捕获不到
        System.out.println("Bad encoding");
    }
}

对于上面的代码,UnsupportedEncodingException异常是永远捕获不到的,由于它是IOException的子类。当抛出UnsupportedEncodingException异常时,会被catch (IOException e) { ... }捕获并执行。

finally语句块保证有无错误都会执行。上述代码能够改写以下:

public static void main(String[] args) {
    try {
        process1();
        process2();
        process3();
    } catch (UnsupportedEncodingException e) {
        System.out.println("Bad encoding");
    } catch (IOException e) {
        System.out.println("IO error");
    } finally {
        System.out.println("END");
    }
}

抛出异常

异常的传播

当某个方法抛出了异常时,若是当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try ... catch被捕获为止:

public class Main {
    public static void main(String[] args) {
        try {
            process1();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void process1() {
        process2();
    }

    static void process2() {
        Integer.parseInt(null); // 会抛出NumberFormatException
    }
}

经过printStackTrace()能够打印出方法的调用栈,相似:

java.lang.NumberFormatException: null
    at java.base/java.lang.Integer.parseInt(Integer.java:614)
    at java.base/java.lang.Integer.parseInt(Integer.java:770)
    at Main.process2(Main.java:16)
    at Main.process1(Main.java:12)
    at Main.main(Main.java:5)

printStackTrace()对于调试错误很是有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:

  1. main()调用process1()
  2. process1()调用process2()
  3. process2()调用Integer.parseInt(String)
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)

查看Integer.java源码可知,抛出异常的方法代码以下:

public static int parseInt(String s, int radix) throws NumberFormatException {
    if (s == null) {
        throw new NumberFormatException("null");
    }
    ...
}

而且,每层调用均给出了源代码的行号,可直接定位。

抛出异常

当发生错误时,例如,用户输入了非法的字符,咱们就能够抛出异常。

如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:

  1. 建立某个Exception的实例;
  2. throw语句抛出。

下面是一个例子:

void process2(String s) {
    if (s==null) {
        NullPointerException e = new NullPointerException();
        throw e;
    }
}

实际上,绝大部分抛出异常的代码都会合并写成一行:

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

若是一个方法捕获了某个异常后,又在catch子句中抛出新的异常,就至关于把抛出的异常类型“转换”了:

void process1(String s) {
    try {
        process2();
    } catch (NullPointerException e) {
        throw new IllegalArgumentException();
    }
}

void process2(String s) {
    if (s==null) {
        throw new NullPointerException();
    }
}

自定义异常

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

Exception
│
├─ RuntimeException
│  │
│  ├─ NullPointerException
│  │
│  ├─ IndexOutOfBoundsException
│  │
│  ├─ SecurityException
│  │
│  └─ IllegalArgumentException
│     │
│     └─ NumberFormatException
│
├─ IOException
│  │
│  ├─ UnsupportedCharsetException
│  │
│  ├─ FileNotFoundException
│  │
│  └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException

当咱们在代码中须要抛出异常时,尽可能使用JDK已定义的异常类型。例如,参数检查不合法,应该抛出IllegalArgumentException

static void process1(int age) {
    if (age <= 0) {
        throw new IllegalArgumentException();
    }
}

在一个大型项目中,能够自定义新的异常类型,可是,保持一个合理的异常继承体系是很是重要的。

一个常见的作法是自定义一个BaseException做为“根异常”,而后,派生出各类业务类型的异常。

BaseException须要从一个适合的Exception派生,一般建议从RuntimeException派生:

public class BaseException extends RuntimeException {
}

其余业务类型的异常就能够从BaseException派生:

public class UserNotFoundException extends BaseException {
}

public class LoginFailedException extends BaseException {
}

...

自定义的BaseException应该提供多个构造方法:

public class BaseException extends RuntimeException {
    public BaseException() {
        super();
    }

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

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }
}

上述构造方法实际上都是原样照抄RuntimeException。这样,抛出异常的时候,就能够选择合适的构造方法。经过IDE能够根据父类快速生成子类的构造方法。

小结

抛出异常时,尽可能复用JDK已定义的异常类型;

自定义异常体系时,推荐从RuntimeException派生“根异常”,再派生出业务异常;

自定义异常时,应该提供多种构造方法。

NullPointerException

在全部的RuntimeException异常中,Java程序员最熟悉的恐怕就是NullPointerException了。

NullPointerException即空指针异常,俗称NPE。若是一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常一般是由JVM抛出的,例如:

public class Main {
    public static void main(String[] args) {
        String s = null;
        System.out.println(s.toLowerCase());
    }
}

指针这个概念实际上源自C语言,Java语言中并没有指针。咱们定义的变量其实是引用,Null Pointer更确切地说是Null Reference,不过二者区别不大。

处理NullPointerException

若是遇到NullPointerException,咱们应该如何处理?首先,必须明确,NullPointerException是一种代码逻辑错误,遇到NullPointerException,遵循原则是早暴露,早修复,严禁使用catch来隐藏这种编码错误:

// 错误示例: 捕获NullPointerException
try {
    transferMoney(from, to, amount);
} catch (NullPointerException e) {
}

好的编码习惯能够极大地下降NullPointerException的产生,例如:

成员变量在定义时初始化:

public class Person {
    private String name = "";
}

使用空字符串""而不是默认的null可避免不少NullPointerException,编写业务逻辑时,用空字符串""表示未填写比null安全得多。

返回空字符串""、空数组而不是null

public String[] readLinesFromFile(String file) {
    if (getFileSize(file) == 0) {
        // 返回空数组而不是null:
        return new String[0];
    }
    ...
}

这样可使得调用方无需检查结果是否为null

使用断言

断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。

咱们先看一个例子:

public static void main(String[] args) {
    double x = Math.abs(-123.45);
    assert x >= 0;
    System.out.println(x);
}

语句assert x >= 0;即为断言,断言条件x >= 0预期为true。若是计算结果为false,则断言失败,抛出AssertionError

使用assert语句时,还能够添加一个可选的断言消息:

assert x >= 0 : "x must >= 0";

这样,断言失败的时候,AssertionError会带上消息x must >= 0,更加便于调试。

Java断言的特色是:断言失败时会抛出AssertionError,致使程序结束退出。所以,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。

对于可恢复的程序错误,不该该使用断言。例如:

void sort(int[] arr) {
    assert arr != null;
}

应该抛出异常并在上层捕获:

void sort(int[] arr) {
    if (x == null) {
        throw new IllegalArgumentException("array cannot be null");
    }
}

小结

断言是一种调试方式,断言失败会抛出AssertionError,只能在开发和测试阶段启用断言;

对可恢复的错误不能使用断言,而应该抛出异常;

断言不多被使用,更好的方法是编写单元测试。

使用JDK Logging

在编写程序的过程当中,发现程序运行结果与预期不符,怎么办?固然是用System.out.println()打印出执行过程当中的某些变量,观察每一步的结果与代码逻辑是否符合,而后有针对性地修改代码。

代码改好了怎么办?固然是删除没有用的System.out.println()语句了。

若是改代码又改出问题怎么办?再加上System.out.println()

反复这么搞几回,很快你们就发现使用System.out.println()很是麻烦。

怎么办?

解决方法是使用日志。

那什么是日志?日志就是Logging,它的目的是为了取代System.out.println()

输出日志,而不是用System.out.println(),有如下几个好处:

  1. 能够设置输出样式,避免本身每次都写"ERROR: " + var
  2. 能够设置输出级别,禁止某些级别输出。例如,只输出错误日志;
  3. 能够被重定向到文件,这样能够在程序运行结束后查看日志;
  4. 能够按包名控制日志级别,只输出某些包打的日志;
  5. 能够……

由于Java标准库内置了日志包java.util.logging,咱们能够直接用。先看一个简单的例子:

public class Hello {
    public static void main(String[] args) {
        Logger logger = Logger.getGlobal();
        logger.info("start process...");
        logger.warning("memory is running out...");
        logger.fine("ignored.");
        logger.severe("process will be terminated...");
    }
}

运行上述代码,获得相似以下的输出:

Mar 02, 2019 6:32:13 PM Hello main
INFO: start process...
Mar 02, 2019 6:32:13 PM Hello main
WARNING: memory is running out...
Mar 02, 2019 6:32:13 PM Hello main
SEVERE: process will be terminated...

对比可见,使用日志最大的好处是,它自动打印了时间、调用类、调用方法等不少有用的信息。

再仔细观察发现,4条日志,只打印了3条,logger.fine()没有打印。这是由于,日志的输出能够设定级别。JDK的Logging定义了7个日志级别,从严重到普通:

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

由于默认级别是INFO,所以,INFO级别如下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就能够屏蔽掉不少调试相关的日志输出。

使用Commons Logging

和Java标准库提供的日志不一样,Commons Logging是一个第三方日志库,它是由Apache建立的日志模块。

Commons Logging的特点是,它能够挂接不一样的日志系统,并经过配置文件指定挂接的日志系统。默认状况下,Commons Loggin自动搜索并使用Log4j(Log4j是另外一个流行的日志系统),若是没有找到Log4j,再使用JDK Logging。

反射

什么是反射?

反射就是Reflection,Java的反射是指程序在运行期能够拿到一个对象的全部信息。

正常状况下,若是咱们要调用一个对象的方法,或者访问一个对象的字段,一般会传入对象实例:

// Main.java
import com.itranswarp.learnjava.Person;

public class Main {
    String getFullName(Person p) {
        return p.getFirstName() + " " + p.getLastName();
    }
}

可是,若是不能得到Person类,只有一个Object实例,好比这样:

String getFullName(Object obj) {
    return ???
}

怎么办?有童鞋会说:强制转型啊!

String getFullName(Object obj) {
    Person p = (Person) obj;
    return p.getFirstName() + " " + p.getLastName();
}

强制转型的时候,你会发现一个问题:编译上面的代码,仍然须要引用Person类。否则,去掉import语句,你看能不能编译经过?

因此,反射是为了解决在运行期,对某个实例一无所知的状况下,如何调用其方法。

Class类

JVM为每一个加载的classinterface建立了对应的Class实例来保存classinterface的全部信息;
获取一个class对应的Class实例后,就能够获取该class的全部信息;
经过Class实例获取class信息的方法称为反射(Reflection);
JVM老是动态加载class,能够在运行期根据条件来控制加载class。

访问字段

Java的反射API提供的Field类封装了字段的全部信息:
经过Class实例的方法能够获取Field实例:getField()getFields()getDeclaredField()getDeclaredFields()
经过Field实例能够获取字段信息:getName()getType()getModifiers()
经过Field实例能够读取或设置某个对象的字段,若是存在访问限制,要首先调用setAccessible(true)来访问非public字段。
经过反射读写字段是一种很是规方法,它会破坏对象的封装。

调用方法

当咱们获取到一个Method对象时,就能够对它进行调用。咱们如下面的代码为例:

String s = "Hello world";
String r = s.substring(6); // "world"

若是用反射来调用substring方法,须要如下代码:

// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);

Java的反射API提供的Method对象封装了方法的全部信息:
经过Class实例的方法能够获取Method实例:getMethod()getMethods()getDeclaredMethod()getDeclaredMethods()
经过Method实例能够获取方法信息:getName()getReturnType()getParameterTypes()getModifiers()
经过Method实例能够调用某个对象的方法:Object invoke(Object instance, Object... parameters)
经过设置setAccessible(true)来访问非public方法;
经过反射调用方法时,仍然遵循多态原则。

调用构造方法

咱们一般使用new操做符建立新的实例:

Person p = new Person();

若是经过反射来建立新的实例,能够调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();

Constructor对象封装了构造方法的全部信息;

经过Class实例的方法能够获取Constructor实例:getConstructor()getConstructors()getDeclaredConstructor()getDeclaredConstructors()

经过Constructor实例能够建立一个实例对象:newInstance(Object... parameters); 经过设置setAccessible(true)来访问非public构造方法。

注解

使用注解

注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”:

// this is a component:
@Resource("hello")
public class Hello {
    @Inject
    int n;

    @PostConstruct
    public void hello(@Param String name) {
        System.out.println(name);
    }

    @Override
    public String toString() {
        return "Hello";
    }
}

注释会被编译器直接忽略,注解则能够被编译器打包进入class文件,所以,注解是一种用做标注的“元数据”。

注解的做用

从JVM的角度看,注解自己对代码逻辑没有任何影响,如何使用注解彻底由工具决定。

Java的注解能够分为三类:

第一类是由编译器使用的注解,例如:

  • @Override:让编译器检查该方法是否正确地实现了覆写;
  • @SuppressWarnings:告诉编译器忽略此处代码产生的警告。

这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。

第二类是由工具处理.class文件使用的注解,好比有些工具会在加载class的时候,对class作动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,通常咱们没必要本身处理。

第三类是在程序运行期可以读取的注解,它们在加载后一直存在于JVM中,这也是最经常使用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

定义一个注解时,还能够定义配置参数。配置参数能够包括:

  • 全部基本类型;
  • String;
  • 枚举类型;
  • 基本类型、String、Class以及枚举的数组。

由于配置参数必须是常量,因此,上述限制保证了注解在定义时就已经肯定了每一个参数的值。

注解的配置参数能够有默认值,缺乏某个配置参数时将使用默认值。

此外,大部分注解会有一个名为value的配置参数,对此参数赋值,能够只写常量,至关于省略了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表示全部参数都使用默认值。

小结

注解(Annotation)是Java语言用于工具处理的标注:

注解能够配置参数,没有指定配置的参数使用默认值;

若是参数名称是value,且只有一个参数,那么能够省略参数名称。

定义注解

Java语言使用@interface语法来定义注解(Annotation),它的格式以下:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

注解的参数相似无参数方法,能够用default设定一个默认值(强烈推荐)。最经常使用的参数应当命名为value

元注解

元注解

有一些注解能够修饰其余注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,咱们只须要使用元注解,一般不须要本身去编写元注解。

@Target

最经常使用的元注解是@Target。使用@Target能够定义Annotation可以被应用于源码的哪些位置:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER

例如,定义注解@Report可用在方法上,咱们必须添加一个@Target(ElementType.METHOD)

@Target(ElementType.METHOD)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

定义注解@Report可用在方法或字段上,能够把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }

@Target({
    ElementType.METHOD,
    ElementType.FIELD
})
public @interface Report {
    ...
}

实际上@Target定义的valueElementType[]数组,只有一个元素时,能够省略数组的写法。

@Retention

另外一个重要的元注解@Retention定义了Annotation的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅class文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

若是@Retention不存在,则该Annotation默认为CLASS。由于一般咱们自定义的Annotation都是RUNTIME,因此,务必要加上@Retention(RetentionPolicy.RUNTIME)这个元注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

@Repeatable

使用@Repeatable这个元注解能够定义Annotation是否可重复。这个注解应用不是特别普遍。

@Inherited

使用@Inherited定义子类是否可继承父类定义的Annotation@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,而且仅针对class的继承,对interface的继承无效:

@Inherited
@Target(ElementType.TYPE)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

在使用的时候,若是一个类用到了@Report

@Report(type=1)
public class Person {
}

则它的子类默认也定义了该注解:

public class Student extends Person {
}

如何定义Annotation

咱们总结一下定义Annotation的步骤:

第一步,用@interface定义注解:

public @interface Report {
}

第二步,添加参数、默认值:

public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

把最经常使用的参数定义为value(),推荐全部参数都尽可能设置默认值。

第三步,用元注解配置注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
    int type() default 0;
    String level() default "info";
    String value() default "";
}

其中,必须设置@Target@Retention@Retention通常设置为RUNTIME,由于咱们自定义的注解一般要求在运行期读取。通常状况下,没必要写@Inherited@Repeatable

小结

Java使用@interface定义注解:

可定义多个参数和默认值,核心参数使用value名称;

必须设置@Target来指定Annotation能够应用的范围;

应当设置@Retention(RetentionPolicy.RUNTIME)便于运行期读取该Annotation

处理注解

能够在运行期经过反射读取RUNTIME类型的注解,注意千万不要漏写@Retention(RetentionPolicy.RUNTIME),不然运行期没法读取到该注解。

能够经过程序处理注解来实现相应的功能:

  • 对JavaBean的属性值按规则进行检查;
  • JUnit会自动运行@Test标记的测试方法。

注解如何使用,彻底由程序本身决定。例如,JUnit是一个测试框架,它会自动运行全部标记为@Test的方法。

咱们来看一个@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注解,配合check()方法,就能够完成Person实例的检查。注意检查逻辑彻底是咱们本身编写的,JVM不会自动给注解添加任何额外的逻辑。

泛型

泛型是一种“代码模板”,能够用一套代码套用各类类型。

image.png

什么是泛型

在讲解什么是泛型以前,咱们先观察Java标准库提供的ArrayList,它能够看做“可变长度”的数组,由于用起来比数组更方便。

实际上ArrayList内部就是一个Object[]数组,配合存储一个当前分配的长度,就能够充当“可变数组”:

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

若是用上述ArrayList存储String类型,会有这么几个缺点:

  • 须要强制转型;
  • 不方便,易出错。

例如,代码必须这么写:

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);

很容易出现ClassCastException,由于容易“误转型”:

list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

要解决上述问题,咱们能够为String单独编写一种ArrayList

public class StringArrayList {
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也必定是String,不须要强制转型,由于编译器会强制检查放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不容许放入非String类型:
list.add(new Integer(123));

问题暂时解决。

然而,新的问题是,若是要存储Integer,还须要为Integer单独编写一种ArrayList

public class IntegerArrayList {
    private Integer[] array;
    private int size;
    public void add(Integer e) {...}
    public void remove(int index) {...}
    public Integer get(int index) {...}
}

实际上,还须要为其余全部class单独编写一种ArrayList

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList
  • ...

这是不可能的,JDK的class就有上千个,并且它还不知道其余人编写的class。

为了解决新的问题,咱们必须把ArrayList变成一种模板:ArrayList<T>,代码以下:

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能够是任何class。这样一来,咱们就实现了:编写一次模版,能够建立任意类型的ArrayList

// 建立能够存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 建立能够存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 建立能够存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

所以,泛型就是定义一种模板,例如ArrayList<T>,而后在代码中为用到的类建立对应的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!

这样一来,既实现了编写一次,万能匹配,又经过编译器保证了类型安全:这就是泛型。

向上转型

在Java标准库中的ArrayList<T>实现了List<T>接口,它能够向上转型为List<T>

public class ArrayList<T> implements List<T> {
    ...
}

List<String> list = new ArrayList<String>();

即类型ArrayList<T>能够向上转型为List<T>

要_特别注意_:不能把ArrayList<Integer>向上转型为ArrayList<Number>List<Number>

这是为何呢?假设ArrayList<Integer>能够向上转型为ArrayList<Number>,观察一下代码:

// 建立ArrayList<Integer>类型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一个Integer:
integerList.add(new Integer(123));
// “向上转型”为ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一个Float,由于Float也是Number:
numberList.add(new Float(12.34));
// 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!

咱们把一个ArrayList<Integer>转型为ArrayList<Number>类型后,这个ArrayList<Number>就能够接受Float类型,由于FloatNumber的子类。可是,ArrayList<Number>实际上和ArrayList<Integer>是同一个对象,也就是ArrayList<Integer>类型,它不可能接受Float类型, 因此在获取Integer的时候将产生ClassCastException

实际上,编译器为了不这种错误,根本就不容许把ArrayList<Integer>转型为ArrayList<Number>

ArrayList<Integer>和ArrayList<Number>二者彻底没有继承关系。

小结

泛型就是编写模板代码来适应任意类型;

泛型的好处是使用时没必要对类型进行强制转换,它经过编译器对类型进行检查;

注意泛型的继承关系:能够把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)。

使用泛型

使用ArrayList时,若是不定义泛型类型时,泛型类型实际上就是Object

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把<T>看成Object使用,没有发挥泛型的优点。

当咱们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

当咱们定义泛型类型<Number>后,List<T>的泛型接口变为强类型List<Number>

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);

编译器若是能自动推断出泛型类型,就能够省略后面的泛型类型。例如,对于下面的代码:

List<Number> list = new ArrayList<Number>();

编译器看到泛型类型List<Number>就能够自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,所以,能够把代码简写为:

// 能够省略后面的Number,编译器能够自动推断泛型类型:
List<Number> list = new ArrayList<>();

泛型接口

除了ArrayList<T>使用了泛型,还能够在接口中使用泛型。例如,Arrays.sort(Object[])能够对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:

public interface Comparable<T> {
    /**
     * 返回负数: 当前实例比参数o小
     * 返回0: 当前实例与参数o相等
     * 返回正数: 当前实例比参数o大
     */
    int compareTo(T o);
}

小结

使用泛型时,把泛型参数<T>替换为须要的class类型,例如:ArrayList<String>ArrayList<Number>等;

能够省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();

不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;

能够在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

编写泛型

编写泛型类比普通类要复杂。一般来讲,泛型类通常用在集合类中,例如ArrayList<T>,咱们不多须要编写泛型类。

若是咱们确实须要编写一个泛型类,那么,应该如何编写它?

能够按照如下步骤来编写一个泛型类。

首先,按照某种类型,例如:String,来编写类:

public class Pair {
    private String first;
    private String last;
    public Pair(String first, String last) {
        this.first = first;
        this.last = last;
    }
    public String getFirst() {
        return first;
    }
    public String getLast() {
        return last;
    }
}

最后,把特定类型String替换为T,并申明<T>

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() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

熟练后便可直接从T开始编写。

静态方法

编写泛型类时,要特别注意,泛型类型<T>不能用于静态方法。

多个泛型类型

泛型还能够定义多种类型。例如,咱们但愿Pair不老是存储两个类型同样的对象,就可使用类型<T, K>

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}

使用的时候,须要指出两种类型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另外一种类型。

小结

编写泛型时,须要定义泛型类型<T>

静态方法不能引用泛型类型<T>,必须定义其余类型(例如<K>)来实现静态泛型方法;

泛型能够同时定义多种类型,例如Map<K, V>

擦拭法

Java的泛型是由编译器在编译时实行的,编译器内部永远把全部类型T视为Object处理,可是,在须要转型的时候,编译器会根据T的类型自动为咱们实行安全地强制转型。

了解了Java泛型的实现方式——擦拭法,咱们就知道了Java泛型的局限:

局限一:<T>不能是基本类型,例如int,由于实际类型是ObjectObject类型没法持有基本类型:

Pair<int> p = new Pair<>(1, 2); // compile error!

局限二:没法取得带泛型的Class

小结

Java的泛型是采用擦拭法实现的;

擦拭法决定了泛型<T>

  • 不能是基本类型,例如:int
  • 不能获取带泛型类型的Class,例如:Pair<String>.class
  • 不能判断带泛型类型的类型,例如:x instanceof Pair<String>
  • 不能实例化T类型,例如:new T()

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

子类能够获取父类的泛型类型<T>

集合

集合类型是Java标准库中被使用最多的类型。

集合简介

什么是集合(Collection)?集合就是“由若干个肯定的元素所构成的总体”。例如,5只小兔构成的集合。

在数学中,咱们常常遇到集合的概念。例如:

  • 有限集合:

    • 一个班全部的同窗构成的集合;
    • 一个网站全部的商品构成的集合;
    • ...
  • 无限集合:

    • 全体天然数集合:1,2,3,……
    • 有理数集合;
    • 实数集合;
    • ...

为何要在计算机中引入集合呢?这是为了便于处理一组相似的数据,例如:

  • 计算全部同窗的总成绩和平均成绩;
  • 列举全部的商品名称和价格;
  • ……

在Java中,若是一个Java对象能够在内部持有若干其余Java对象,并对外提供访问接口,咱们把这种Java对象称为集合。很显然,Java的数组能够看做是一种集合:

String[] ss = new String[10]; // 能够持有10个String对象
ss[0] = "Hello"; // 能够放入String对象
String first = ss[0]; // 能够获取String对象

既然Java提供了数组这种数据类型,能够充当集合,那么,咱们为何还须要其余集合类?这是由于数组有以下限制:

  • 数组初始化后大小不可变;
  • 数组只能按索引顺序存取。

所以,咱们须要各类不一样类型的集合类来处理不一样的数据,例如:

  • 可变大小的顺序链表;
  • 保证无重复元素的集合;
  • ...

Collection

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外全部其余集合类的根接口。Java的java.util包主要提供了如下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的StudentList
  • Set:一种保证没有重复元素的集合,例如,全部无重复名称的StudentSet
  • Map:一种经过键值(key-value)查找的映射表集合,例如,根据Studentname查找对应StudentMap

Java集合的设计有几个特色:一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayListLinkedList等,二是支持泛型,咱们能够限制在一个集合中只能放入同一种数据类型的元素,例如:

List<String> list = new ArrayList<>(); // 只能放入String类型

最后,Java访问集合老是经过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。

因为Java的集合设计很是久远,中间经历过大规模改进,咱们要注意到有一小部分集合类是遗留类,不该该继续使用:

  • Hashtable:一种线程安全的Map实现;
  • Vector:一种线程安全的List实现;
  • Stack:基于Vector实现的LIFO的栈。

还有一小部分接口是遗留接口,也不该该继续使用:

  • Enumeration<E>:已被Iterator<E>取代。

小结

Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括ListSetMap。Java集合使用统一的Iterator遍历,尽可能不要使用遗留接口。

使用List

在集合类中,List是最基础的一种集合:它是一种有序列表。

List的行为和数组几乎彻底相同:List内部按照放入元素的前后顺序存放,每一个元素均可以经过索引肯定本身的位置,List的索引和数组同样,从0开始。

数组和List相似,也是有序结构,若是咱们使用数组,在添加和删除元素的时候,会很是不方便。例如,从一个已有的数组{'A', 'B', 'C', 'D', 'E'}中删除索引为2的元素:

┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │   │
└───┴───┴───┴───┴───┴───┘
              │   │
          ┌───┘   │
          │   ┌───┘
          │   │
          ▼   ▼
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ D │ E │   │   │
└───┴───┴───┴───┴───┴───┘

这个“删除”操做其实是把'C'后面的元素依次往前挪一个位置,而“添加”操做其实是把指定位置之后的元素都依次向后挪一个位置,腾出来的位置给新加的元素。这两种操做,用数组实现很是麻烦。

所以,在实际应用中,须要增删元素的有序列表,咱们使用最多的是ArrayList。实际上,ArrayList在内部使用了数组来存储全部元素。例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │   │
└───┴───┴───┴───┴───┴───┘

当添加一个元素并指定索引到ArrayList时,ArrayList自动移动须要移动的元素:

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │   │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

而后,往内部指定索引的数组位置添加一个元素,而后把size1

size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

继续添加元素,可是数组已满,没有空闲位置的时候,ArrayList先建立一个更大的新数组,而后把旧数组的全部元素复制到新数组,紧接着用新数组取代旧数组:

size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

如今,新数组就有了空位,能够继续添加一个元素到数组末尾,同时size1

size=7
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ G │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

可见,ArrayList把添加和删除的操做封装起来,让咱们操做List相似于操做数组,却不用关心内部元素如何移动。

咱们考察List<E>接口,能够看到几个主要的接口方法:

  • 在末尾添加一个元素:boolean add(E e)
  • 在指定索引添加一个元素:boolean add(int index, E e)
  • 删除指定索引的元素:int remove(int index)
  • 删除某个元素:int remove(Object e)
  • 获取指定索引的元素:E get(int index)
  • 获取链表大小(包含元素的个数):int size()

可是,实现List接口并不是只能经过数组(即ArrayList的实现方式)来实现,另外一种LinkedList经过“链表”也实现了List接口。在LinkedList中,它的内部每一个元素都指向下一个元素:

┌───┬───┐   ┌───┬───┐   ┌───┬───┐   ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │   │
        └───┴───┘   └───┴───┘   └───┴───┘   └───┴───┘

一般状况下,咱们老是优先使用ArrayList

相关文章
相关标签/搜索