Java复习的差不多了,但是怕面试挑细碎知识点问,还是集中做一个总结吧
异常
异常也是一种类型,它是JDK对“异常事件”的一种抽象。配合throw 关键字可以抛出一个异常对象,这个异常对象如果一直不能被捕获并处理,最终将会抛给JVM,JVM执行默认行为:在标准错误输出流system.err中打印堆栈信息。
所以,如果问我什么是异常,我会说:Java的异常是对非正常事件的一种抽象,具体的异常信息通常都会创建一个异常实例,用于保存这异常的元信息。
分类
异常的顶层父类是throwable。Throw关键字底层对应athrow命令,只能抛出throwable的实例。
Throwable有两大子类: 【1】error Error一般是系统相关的错误,例如OOM、Stack Overflow等 【2】exception 异常又可以分为运行时异常runtimeException和编译期异常。 runtimeException以及它的子类都是非受检异常,无论是主动抛出这个异常还是调用了声明该异常的方法。都可以不去处理,而交给JVM去处理。JVM会调用注册在线程上未捕获异常处理器(能抛给JVM异常处理器的异常一定是运行时异常)
public static void main(String[] args) {
Thread.currentThread().setUncaughtExceptionHandler((r,e)->{
System.out.println("没事,抛者玩的");
System.out.println(r.getName()+" "+e.getMessage());
});
throw new RuntimeException();
}
通过设置异常处理器,也可以实现子线程与主线程的通信,如错误报告。
除了runtimeException以及它的子类,其他的异常(Exeption类型)都是受检异常。必须进行try-catch处理或者throws向上抛出。(当然了,如果抛给了JVM则执行默认的处理器,标准错误输出流进行输出)
常见的受检异常包括SQL异常、IO异常、文件无法找到异常、EOF异常等。 注意:Error类及其子类表示程序本身无法修复的错误,javac不会检查他们,当程序运行时出现这些异常,Java进程会直接终止——Error是非受检的。 不过Error类型一般是JDK预设好的,用户一般不会主动扩展错误类型,而用户总是选择扩展RuntimeException类型。
异常处理机制
一个异常的执行顺序: 【1】new一个异常对象 【2】中止当前正在执行的程序 【3】弹出异常对象的引用 【4】异常处理机制接管被中止的程序 【5】进入异常处理程序的catch代码块继续执行。
JVM使用**方法调用栈(method invocation stack)**来跟踪每一个线程中一系列方法的调用过程。该栈保存了每个调用方法的本地信息(如方法的局部变量)。每个线程都有一个独立的方法调用栈。Java主线程中栈底方法是main()方法。每当一个新方法被调用时,JVM将描述该方法的栈结构放入栈顶,位于栈顶的方法就是正在执行的方法。
当一个方法正常执行完毕,JVM就会从调用栈中弹出该方法的栈结构(出栈),然后继续执行栈中下一个方法。 如果在执行方法的过程中抛出了异常,则JVM必须能找到catch代码块,如果在当前方法找不到,JVM就会从调用栈中弹出该方法的栈结构,继续到下一个方法中查找合适的catch代码块。
每个方法调用栈的catch结构可以看到当前栈帧的异常处理器,如果找不到就会栈帧出栈,并寻找上一调用级别的异常处理器。最终main方法栈帧出栈后,交给JVM的未捕获异常处理器进行处理。如果没有设置则执行默认行为: 【1】调用异常对象的printStackTrace()方法,打印来自方法调用栈的异常信息 【2】如果该线程不是主线程,则终止这个线程,其他线程继续正常运行。如果是主线程,那么整个应用程序被终止。
finally
如果finally和try/catch都有返回语句,那么后者的return内容将会被覆盖掉。后者返回之前会执行finally的内容。 Finally语句不被执行的唯一情况就是:执行了system.exit()方法——用于终止当前的Java进程
如果在try和catch进行return,首先这个return的值是赋给了一个局部变量,如果finally中不存在任何return语句,那么返回的就是这个局部变量。(字面量或者引用被赋值给一个局部变量) 如果finally中存在return ,则最终返回的是finally中的return。Catch中的return X 相当于将X存放到了一个局部变量(如果X=a++也是会执行的)。
return覆盖的情况:
static int fun(){
int a =0;
int b =1;
try{
return a++;
}finally {
return a+b;
}
}
异常丢失的情况:
static int fun(){
int a =0;
int b =1;
try{
throw new RuntimeException((a++)+" ");
}finally {
return a+b;
}
}
局部变量是引用类型
int[] a={1};
try{
return a;
}finally {
a[0]=33;
}
局部变量是字面量类型
static int fun(){
int[] a={1};
try{
return a[0];
}finally {
a[0]=33;
}
}
总结:finally中不建议使用return,因为会造成“异常丢失”和“返回值内容被覆盖”
throw和throws
Throw用于抛出一个异常,写在方法体内。而throws用于声明一个方法可能产生的所有异常。 Throws更多是提醒调用者“我这个方法可能抛出哪些异常”。如果是受检异常,则必须处理,如果是运行时异常则可以不去处理。 注意:如果我们拿到的是一个受检异常,我们必须try/catch进行处理,或者throws声明。而我们拿到一个非受检异常,也可以去处理或者抛出,但是不是必要的。
public static void a() throws IOException {
}
throws 甚至可以加在一个空实现的方法上,但是如果另外一个方法b()调用了a(),a() ; 这行代码本身就会被视作一个异常,b()方法必须选择处理或者throws。而如果a()如果声明的是一个runtimeException则b()调用a()不需要进行额外处理。
换一个角度描述一下他们的关系: 如果一个方法体内throw new XXException() ,或是调用一个throws XXException的方法,他们(这一行代码)的效果相似。如果这是一个受检异常,调用方必须做出throws或try/catch。如果这是一个非受检异常,则调用方则不需要额外处理。
如果我们需要实现runnable接口对应的run()方法,则我们只能抛出运行时异常,而对应受检异常只能try/catch进行处理,因此重写一个方法时不允许抛出更严格的异常,而run方法没有throws任何异常,因此我们只剩下唯一一种选择:try/catch
@Override
public void run() {
try {
throw new IOException();
} catch (IOException e) {
e.printStackTrace();
}
}
总结:异常也是java提供的一种类型,每个异常实例代表一种错误请求,异常可以被用于throws声明或者throw抛出。
枚举
使用枚举,可以枚举可选的选项,并且限定取值的范围。例如枚举进程类的状态:就绪、运行、等待、停止。 通过enum定义的枚举,是java.lang.Enum类的子类。 Enum提供了两个成员: 【1】name。它就是枚举中定义的变量名的String形式。 【2】ordinal。序数,从0开始序号,与static块中的声明的顺序有关。compareTo就是根据ordinal进行比较的。
枚举类型的本质
enum AccountType
{
SAVING, FIXED, CURRENT;
private AccountType()
{
System.out.println("It is a account type");
}
}
枚举的底层,实际上被转换为了继承java.lang.Enum类的实体类,枚举的成员变成了对应实体类型的字段,并且生成了一个新的构造函数,如果原来定义了构造函数,则在其基础上加上两个参数,生成新的构造函数。(Enum类不能手动扩展,它内部的构造函数只能通过编译器调用)
final class AccountType extends java.lang.Enum
{
public static final AccountType SAVING;
public static final AccountType FIXED;
public static final AccountType CURRENT;
public static final AccountType[] $VALUES;
static{
SAVING = new AccountType("SAVING", 0);
FIXED = new AccountType("FIXED",1);
CURRENT = new AccountType("CURRENT", 2);
$VALUES = new AccountType[]{
SAVING, FIXED, CURRENT
};
}
private AccountType(String s,int i)
{
super(s,i);
System.out.println("It is a account type");
}
}
枚举类中的所有枚举值都是静态常量(final static),而且类被final修饰,不能被继承。 除此之外,编译器还会生成两个静态方法:values()和valueOf()。 values()用于获取枚举类中的所有变量,并且作为数组返回(其实就是返回$VALUES数组),而valueOf(String name)根据名称获取枚举变量。
values和valueOf方法都是编译器插入到生成类的静态方法,因此如果将生成类向上转型为Enum,是无法调用这两个方法的。
Enum是枚举类型的公共基本类(抽象类),不能手动继承,只能编译器帮我们继承。 能被复写的只有toString方法,其他方法都被final修饰了。
枚举类特性
枚举类和正常类没什么大的区别。可以实现接口、定义属性以及方法。但是不能继承父类,因为已经继承了Enum。
enum MyEnum{
A{
@Override
void fun() {
}
},
B{
@Override
void fun() {
}
};
abstract void fun();
}
可以定义抽象方法,但是必须提供实现。该方式可帮助不同的枚举实例表现出不同的行为(枚举的多态). 另一方面,枚举也可以作为类型声明、入参、返回值等,它还可以与switch搭配使用。
MyEnum fun(MyEnum myEnum){
switch (myEnum){
case A:
return myEnum;
}
return null;
}
总结:把枚举看作一个类型,定义的枚举值就是它的静态常量成员,并且我们的实例取值范围只能在这些声明的成员中进行选择。这个访问控制的过程是JVM保证的。因此用枚举实现单例模式也是简单且安全的。
enum Singleton{
INSTANCE;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
反射
反射,就是在运行时,拿到一个类型的元信息。从哪里拿到这些元信息?JVM的元信息统一保存在方法区,类加载阶段,class文件被载入内存,并且转换为了保存在方法区的数据结构中。同时向堆中放入一个class实例,用作元信息访问的入口。我们可以通过class.forName()、实例.getClass()、或者类型.class这三种方法拿到这个class实例。(8大基本类型和void关键字都具有class实例)
拿到class实例,可以直接使用newInstance方法通过空参构造器创建对象,或者也可以使用getConstructor()根据参数数量、类型拿到具体的构造器实例,当找到需要调用的方法时,都会复制一份而不是使用原来的实例,保证数据隔离。反射是线程安全的。
一旦我们拿到这个class实例,就找到了访问类型元数据的入口,我们可以通过这个class实例拿到构造器实例、方法实例、字段实例、注解实例等,进而我们还可以拿到字段类型、方法参数类型等。反射技术通常用于实现各种框架包括注解的解析、依赖注入、占位符替换、动态代理等。
反射的特点可以用于实现类型之间的解耦,提供系统灵活性,例如输入全限定类名,反射创建一个实例(动态加载),避免大量if/else条件(静态加载)。包括spring的依赖注入、切面编程以及解析配置文件都离不开反射特性的支持。 但是反射的性能比较低(一般配合缓存使用)、JVM无法对某些动态加载的类型进行优化,而且一定程度上破坏了Java的封装语义。另一方面,反射技术可能减低代码可读性,使得代码难以维护。
注解
注解本质上是一个接口,它需要被第三方识别并使用包括编译器、框架、甚至是注解本身 注解的作用:生成文档、被框架反射读取(如依赖注入)、编译时格式检查如override。
元注解,是被注解识别的注解。 【1】@Target用来约束注解的修饰位置。使用枚举类型ElementType,取值包括type、field、method、parameter等 【2】@Retention用于约束注解的声明周期,使用枚举类型RetentionPolicy,取值包括runtime、class、source。只有runtime会保留到运行期,被反射技术读取到,而class只会被保留在class文件中,运行时被擦除,通常用于编译器检查如@Override、@Deprecated和@SuppressWarnning 【3】@Document用于生成文档。 【4】@Inherited允许子类继承父类的注解
注解支持的元素包括8大基本类型以及对应数组类型、枚举、注解、Class、字符串。但是不支持包装类型以及其他引用类型。
注解通过和反射技术配合使用,以@value为例,用户为 name字段 修饰@value(“mike”),spring框架注入值的时候首先拿到字段集合fields,遍历每一个field,如果存在@value,那么就可以拿到这个值。此时根据field还可以拿到字段名、字段的类型type。最终spring读取value,并解析为type类型,反射注入到bean对象的name字段中。(反射的性能,一定程度上依赖类型的规模)
原理
注解底层使用到了动态代理技术。 因为注解本质上是一个接口(继承自java.lang.annotio的子接口),但是我们可以通过Class对象的getAnnotation方法获得一个annotation的实例(这就是一个代理对象,调用自定义方法的时候,最终调用的是invocationHandler的invoke方法)。
该方法会从memberValue这个string-object的map中取值(k是注解属性名称,v是具体的赋值),所有生命周期在runtime的注解都会加载进这个map,并且通过引用传递给invocationHandler实例。而invoke的执行核心就是——通过方法名返回注解的属性值
进行反射获取注解(调用getAnnotations()方法)的时候,JVM会将所有生命周期在RUNTIME的注解取出来,放入一个map中,并创建一个AnnotationInvocationHandler实例,并将注解map传递给它,最后JVM 通过动态代理的方式生成目标注解的代理类,并初始化好处理器
my annotation = review.class.getAnnotation(my.class);
my是自定义注解的代理实例,有一个AnotationInvocationHandler类型的成员h,h的成员memberValues是一个map,key是注解中的方法名(注解的属性名),value是属性的值。value或是缺省值或者用户传入的值
注解@my的实例annotation是一个代理对象的实例,这个代理对象的类型是$Proxy1 extends Proxy implements my ,并且持有AnotationInvocationHandler类型的引用。
class AnnotationInvocationHandler implements InvocationHandler, Serializable
一个注解实例本质上是一个代理类实例,通过方法名返回注解的属性。(使用这个代理类的newProxyInstance方法的时候,传入接口和AnotationInvocationHandler,最终返回一个代理实例)
|