在计算机程序运行的过程当中,老是会出现各类各样的错误。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
表示严重的错误,程序对此通常无能为力,例如:ide
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
分别捕获对应的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
派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。
在全部的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
,遵循原则是早暴露,早修复,严禁使用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
,只能在开发和测试阶段启用断言;
对可恢复的错误不能使用断言,而应该抛出异常;
断言不多被使用,更好的方法是编写单元测试。
在编写程序的过程当中,发现程序运行结果与预期不符,怎么办?固然是用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个日志级别,从严重到普通:
由于默认级别是INFO,所以,INFO级别如下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就能够屏蔽掉不少调试相关的日志输出。
和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
语句,你看能不能编译经过?
因此,反射是为了解决在运行期,对某个实例一无所知的状况下,如何调用其方法。
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并不会识别该注解)。
定义一个注解时,还能够定义配置参数。配置参数能够包括:
由于配置参数必须是常量,因此,上述限制保证了注解在定义时就已经肯定了每一个参数的值。
注解的配置参数能够有默认值,缺乏某个配置参数时将使用默认值。
此外,大部分注解会有一个名为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
能够定义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
定义了Annotation
的生命周期:
RetentionPolicy.SOURCE
;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
这个元注解能够定义Annotation
是否可重复。这个注解应用不是特别普遍。
使用@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
的步骤:
第一步,用@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)
,不然运行期没法读取到该注解。
能够经过程序处理注解来实现相应的功能:
@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
:
这是不可能的,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只小兔构成的集合。
在数学中,咱们常常遇到集合的概念。例如:
有限集合:
无限集合:
为何要在计算机中引入集合呢?这是为了便于处理一组相似的数据,例如:
在Java中,若是一个Java对象能够在内部持有若干其余Java对象,并对外提供访问接口,咱们把这种Java对象称为集合。很显然,Java的数组能够看做是一种集合:
String[] ss = new String[10]; // 能够持有10个String对象 ss[0] = "Hello"; // 能够放入String对象 String first = ss[0]; // 能够获取String对象
既然Java提供了数组这种数据类型,能够充当集合,那么,咱们为何还须要其余集合类?这是由于数组有以下限制:
所以,咱们须要各类不一样类型的集合类来处理不一样的数据,例如:
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
的索引和数组同样,从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
。