异样解决
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
是异样体系的根,它继承自Object
。Throwable
有两个体系:Error
和Exception
,Error
示意重大的谬误,程序对此个别无能为力,例如:
OutOfMemoryError
:内存耗尽NoClassDefFoundError
:无奈加载某个ClassStackOverflowError
:栈溢出
而Exception
则是运行时的谬误,它能够被捕捉并解决。
某些异样是利用程序逻辑解决的一部分,应该捕捉并解决。例如:
NumberFormatException
:数值类型的格局谬误FileNotFoundException
:未找到文件SocketException
:读取网络失败
还有一些异样是程序逻辑编写不对造成的,应该修复程序自身。例如:
NullPointerException
:对某个null
的对象调用办法或字段IndexOutOfBoundsException
:数组索引越界
Exception
又分为两大类:
RuntimeException
以及它的子类;- 非
RuntimeException
(包含IOException
、ReflectiveOperationException
等等)
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
办法中被抛出的,从下往上看,调用档次顺次是:
main()
调用process1()
;process1()
调用process2()
;process2()
调用Integer.parseInt(String)
;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()
办法,抛出异样分两步:
- 创立某个
Exception
的实例; - 用
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()
,有以下几个益处:
- 能够设置输入款式,防止本人每次都写
"ERROR: " + var
; - 能够设置输入级别,禁止某些级别输入。例如,只输入谬误日志;
- 能够被重定向到文件,这样能够在程序运行完结后查看日志;
- 能够按包名管制日志级别,只输入某些包打的日志;
- 能够……
因为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为每个加载的class
及interface
创立了对应的Class
实例来保留class
及interface
的所有信息;
获取一个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
定义的value
是ElementType[]
数组,只有一个元素时,能够省略数组的写法。
@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不会主动给注解增加任何额定的逻辑。
泛型
泛型是一种“代码模板”,能够用一套代码套用各种类型。
什么是泛型
在解说什么是泛型之前,咱们先察看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
类型,因为Float
是Number
的子类。然而,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
,因为理论类型是Object
,Object
类型无奈持有根本类型:
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
:一种有序列表的汇合,例如,按索引排列的Student
的List
;Set
:一种保障没有反复元素的汇合,例如,所有无反复名称的Student
的Set
;Map
:一种通过键值(key-value)查找的映射表汇合,例如,依据Student
的name
查找对应Student
的Map
。
Java汇合的设计有几个特点:一是实现了接口和实现类相拆散,例如,有序表的接口是List
,具体的实现类有ArrayList
,LinkedList
等,二是反对泛型,咱们能够限度在一个汇合中只能放入同一种数据类型的元素,例如:
List<String> list = new ArrayList<>(); // 只能放入String类型
最初,Java拜访汇合总是通过对立的形式——迭代器(Iterator)来实现,它最显著的益处在于无需晓得汇合外部元素是按什么形式存储的。
因为Java的汇合设计十分长远,两头经验过大规模改良,咱们要留神到有一小部分汇合类是遗留类,不应该持续应用:
Hashtable
:一种线程平安的Map
实现;Vector
:一种线程平安的List
实现;Stack
:基于Vector
实现的LIFO
的栈。
还有一小部分接口是遗留接口,也不应该持续应用:
Enumeration<E>
:已被Iterator<E>
取代。
小结
Java的汇合类定义在java.util
包中,反对泛型,次要提供了3种汇合类,包含List
,Set
和Map
。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 │
└───┴───┴───┴───┴───┴───┘
而后,往外部指定索引的数组地位增加一个元素,而后把size
加1
:
size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘
持续增加元素,然而数组已满,没有闲暇地位的时候,ArrayList
先创立一个更大的新数组,而后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:
size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
当初,新数组就有了空位,能够持续增加一个元素到数组开端,同时size
加1
:
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
。
发表回复