异常处理
处理异常一般有两种
- 约定返回错误码。,比如读写一个文件,返回code为0则成功,返回1表示不存在,返回2表示没权限…
- 在语言层面上提供一个异常处理机制。(java就属于第二种)
java的异常
java内置了一套异常处理机制,总是使用异常来表示错误。
异常本质也是一种class,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了
用try catch俘获异常
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:
}
从上图可以看到,Throwable
是异常错误的根,下面分为两个体系,Error
和Exception
Error
表示严重的问题,程序对此一般无能为力,比如
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
Exception
Exception
一般是运行时的错误,可以被俘获处理。如
某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
- NumberFormatException:数值类型的格式错误
- FileNotFoundException:未找到文件
- SocketException:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
- NullPointerException:对某个null的对象调用方法或字段
- IndexOutOfBoundsException:数组索引越界
Exception又分为两大类:
RuntimeException
以及它的子类;
非RuntimeException
(包括IOException、ReflectiveOperationException等等)
Java规定:
- 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
注意:编译器对RuntimeException及其子类不做强制捕获要求,不是指应用程序本身不应该捕获并处理RuntimeException。是否需要捕获,具体问题具体分析。
俘获异常
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(); // 尝试使用用默认编码}}
}
如果不俘获,编译器会报错
这是因为,getBytes方法的定义是
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {...
}
throw UnsupportedEncodingException表示,该方法可能会抛出什么错误,调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。
如果实在不想俘获,可以在外层再套一个throw xx
static byte[] toGBK(String s) throws UnsupportedEncodingException {return s.getBytes("GBK");}
但是main方法调用toGbk的时候就需要try catch了,否则还是会报错。
所有异常都可以调用e.printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。
- Java使用异常来表示错误,并通过try … catch捕获异常;
- Java的异常是class,并且从Throwable继承;
- Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
- RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;
- 不推荐捕获了异常但不进行任何处理。
俘获异常
多catch语句
可以使用多个catch语句,每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。例如:
try {process1();process2();process3();} catch (IOException e) {System.out.println("IO error");} catch (UnsupportedEncodingException e) { // 永远捕获不到System.out.println("Bad encoding");}
UnsupportedEncodingException
属于IOException
的字类,所以永远会被第一个catch俘获。
finally
,最后执行,与js类似。
JVM会先执行finally,然后抛出异常。
异常合并
try {process1();process2();process3();} catch (IOException | NumberFormatException e) { // IOException或NumberFormatExceptionSystem.out.println("Bad input");} catch (Exception e) {System.out.println("Unknown error");}
如果处理异常的结果一样,可以用|
放到一起俘获
抛出异常
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch被捕获为止:
通过printStackTrace()可以打印出方法的调用栈,类似:
java.lang.NumberFormatException: nullat 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)
抛出异常
创建某个Exception的实例;
用throw语句抛出。
void process2(String s) {if (s==null) {throw new NullPointerException();}
}
转换异常
public static void main(String[] args) {try {Integer.parseInt("abc");} catch (Exception e) {System.out.println("catched");throw new RuntimeException(e);} finally {System.out.println("finally");}}
}
需要把原来的异常当作参数传入,这样控制台打印不会丢失原有的异常。
自定义异常
Java标准库定义的常用异常包括:
Exception
│
├─ RuntimeException
│ │
│ ├─ NullPointerException
│ │
│ ├─ IndexOutOfBoundsException
│ │
│ ├─ SecurityException
│ │
│ └─ IllegalArgumentException
│ │
│ └─ NumberFormatException
│
├─ IOException
│ │
│ ├─ UnsupportedCharsetException
│ │
│ ├─ FileNotFoundException
│ │
│ └─ SocketException
│
├─ ParseException
│
├─ GeneralSecurityException
│
├─ SQLException
│
└─ TimeoutException
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。
一个常见的做法是自定义一个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);}
这样,抛出异常的时候,就可以选择合适的构造方法。
NullPointerException
NullPointerException即空指针异常,俗称NPE。如果一个对象为null,调用其方法或访问其字段就会产生NullPointerException,这个异常通常是由JVM抛出的,类似于js的 reading of undefined,从undefined访问字段。
使用断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert关键字来实现断言。
public static void main(String[] args) {double x = Math.abs(-123.45);assert x >= 0 : "x must >= 0";System.out.println(x);
}
语句assert x >= 0;即为断言,断言条件x >= 0预期为true。如果计算结果为false,则断言失败,抛出AssertionError。断言失败的时候,AssertionError会带上消息x must >= 0
Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。
使用JDK Logging
Java标准库内置了日志包java.util.logging
import java.util.logging.Level;
import java.util.logging.Logger;
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...
不仅打印出内容,还打印出执行栈,调用类,调用方法。
日志等级
SEVERE WARNING INFO CONFIG FINE FINER FINEST
默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
使用Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class Main {public static void main(String[] args) {Log log = LogFactory.getLog(Main.class);log.info("start...");log.warn("end.");}
}
使用Log4j
使用SLF4J和Logback
SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
commons logging
int score = 99;
p.setScore(score);
log.info("Set score " + score + " for Person " + p.getName() + " ok.");
SLF4j
int score = 99;
p.setScore(score);
logger.info("Set score {} for Person {} ok.", score, p.getName());
- SLF4J和Logback可以取代Commons Logging和Log4j;
- 始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。
反射
什么是反射?
反射就是Reflection
,Java的反射是指程序在运行期可以拿到一个对象的所有信息
。
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
// Main.java
import com.itranswarp.learnjava.Person;public class Main {String getFullName(Person p) {return p.getFirstName() + " " + p.getLastName();}
}
只能传入特定的Person类才能知道他的信息。
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
Class
除了基础类型 int long short byte char boolean float double 外,其他的引用类型基本都是类class。比如String
得出结论:class(包括interface)的本质是数据类型(Type)无继承关系的数据类型无法赋值:
Number n = new Double(123.456); // OK
String s = new Double(123.456); // compile error!
class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。
java在加载class的时候,会创建对应的Class
实例,比如String s = new String();这个s就是String的实例变量,通过s.getClass就可以获取到String这个Class实例,再通过String这个Class实例,可以获取到String这个类的信息。.
public final class Class {private Class() {}
}
以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:
Class cls = new Class(String);
如上,String就会创建String的Class实例。这个new Class只能JVM创建,他的构造函数是private,
所以,java每持有的一个Class的实例,都指向一种数据类型,比如String
Random
等
此外,每个Class实例,都保存该class的所有完整信息。比如name field method,类名,父类等,如果获取了某个Class实例,我们就可以通过Class实例获取到该实例对应的class的所有信息,比如String的Class实例,可以获取String上对应的方法,比如indexOf…
而
这种通过Class实例获取class信息的方法称为反射(Reflection)。
获取Class实例的三种方法
方法一:直接通过一个class的静态变量class获取:
Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:
String s = "Hello";
Class cls = s.getClass(); //通过String的实例s,再通过getClass就可以获取到String这个Class实例
关系应该是
s(String的实例变量) ->getClass(获取到String的Class实例,上面存放着class的信息) -> getName(获取class的类名)
方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
Class cls = Class.forName("java.lang.String");
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例;
==只能用于比较是否是同一个Class实例,而instanceOf则是判断是否是同一个类型,1比如属于Integet类型的变量,instanceOf Number的话也一定是true。
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
对于我们自己创建的类,也是同样的原理,比如
public class Person(){private int test;public Person(test){this.test = test}public setPerson(){}
}
jvm加载这个类的时候,会
Class cls = new Class(Person);
新建一个Person的Class 实例,后面可以通过这个Class 实例获取Person的相关信息学。
动态加载
JVM加载Class是动态加载的,也就是运行到什么类,再加载什么类。
利用JVM动态加载class的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging总是优先使用Log4j,只有当Log4j不存在时,才使用JDK的logging。利用JVM动态加载特性,大致的实现代码如下:
小结
- JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;
- 获取一个class对应的Class实例后,就可以获取该class的所有信息;
- 通过Class实例获取class信息的方法称为反射(Reflection);
- JVM总是动态加载class,可以在运行期根据条件来控制加载class。
获取属性
可以根据Class 实例获取到class的信息,比如
Field getField(name):根据字段名获取某个public的field(包括父类)
Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
Field[] getFields():获取所有public的field(包括父类)
Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
通过Class实例,可以获取到每个field对象,比如name等,就是一个field对象。如
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
Field对象,包含了一个字段的所有信息。
甚至可以获取该Field在实例上的值,设置新的值
public static void main(String[] args) throws Exception {Person p = new Person("Xiao Ming");System.out.println(p.getName()); // "Xiao Ming"Class c = p.getClass();Field f = c.getDeclaredField("name");f.setAccessible(true);f.get(p)f.set(p, "Xiao Hong");System.out.println(p.getName()); // "Xiao Hong"}
f.setAccessible(true)用来设置private的字段
f.get(实例)用来获取 实例.field
f.set(实例,value)用来设置实例.field = value
小结
- Java的反射API提供的Field类封装了字段的所有信息:
- 通过Class实例的方法可以获取Field实例:getField(),getFields(),getDeclaredField(),getDeclaredFields();
- 通过Field实例可以获取字段信息:getName(),getType(),getModifiers();
- 通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
- 通过反射读写字段是一种非常规方法,它会破坏对象的封装。
调用方法
通过Class实例能获取字段信息,自然也能获取对应的方法信息。
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方法;
- 通过反射调用方法时,仍然遵循多态原则。即通过父的Class 实例获取到的method,调用对象为子类,且子类复写了该method,那么就会调用子类的method。
构造方法
之前说过,通过类实例可以创建新的实例,如
Person p = Person.class.newInstance();
但只能调用无参数的构造函数,若想调用传入参数的构造函数,需要
public static void main(String[] args) throws Exception {// 获取构造方法Integer(int):Constructor cons1 = Integer.class.getConstructor(int.class);// 调用构造方法:Integer n1 = (Integer) cons1.newInstance(123);System.out.println(n1);// 获取构造方法Integer(String)Constructor cons2 = Integer.class.getConstructor(String.class);Integer n2 = (Integer) cons2.newInstance("456");System.out.println(n2);}
通过getConstuctor获取Constructor实例。
通过Class实例获取Constructor的方法如下:
getConstructor(Class...):获取某个public的Constructor;
getDeclaredConstructor(Class...):获取某个Constructor;
getConstructors():获取所有public的Constructor;
getDeclaredConstructors():获取所有Constructor。
注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败。
获取继承关系
之前说过,可以通过三种方法获取Class的实例,
Class cls = String.class; // 获取到String的Class 直接通过String类String s = ""; //通过String实例获取String CLass实例
Class cls = s.getClass(); // s是String,因此获取到String的ClassClass s = Class.forName("java.lang.String"); //通过传入完整类名
获取父类的Class
有了Class实例,可以获取父类Class 实例
public static void main(String[] args) throws Exception {Class i = Integer.class;Class n = i.getSuperclass();System.out.println(n);Class o = n.getSuperclass();System.out.println(o);System.out.println(o.getSuperclass());}
获取interface
public static void main(String[] args) throws Exception {Class s = Integer.class;Class[] is = s.getInterfaces();for (Class i : is) {System.out.println(i);}}
动态代理
先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。
public class Main {public static void main(String[] args) {InvocationHandler handler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(method);if (method.getName().equals("morning")) {System.out.println("Good morning, " + args[0]);}return null;}};Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), // 传入ClassLoadernew Class[] { Hello.class }, // 传入要实现的接口handler); // 传入处理调用方法的InvocationHandlerhello.morning("Bob");}
}interface Hello {void morning(String name);
}