目录
异常
异常机制是指当程序出现错误后,程序如何处理。
异常的分类
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。
Throwable类中常用方法如下:
1. 返回异常发生时的详细信息
public string getMessage();
2. 返回异常发生时的简要描述
public string toString();
3. 返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
public string getLocalizedMessage();
4. 在控制台上打印Throwable对象封装的异常信息
public void printStackTrace();
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),类定义错误(NoClassDefFoundError),当 JVM 继续执行操作所需的内存资源不足时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。
Exception(异常):是程序本身可以处理的异常。其分两大类:运行时异常和编译时异常。
1.运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
2.编译时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
异常处理机制
try…catch、try…catch…finally
try{
可能会发生异常的代码
}catch(异常类型 异常名(变量)){
针对异常进行处理的代码
}catch(异常类型 异常名(变量)){
针对异常进行处理的代码
}finally{
释放资源代码
(无论代码块是否发生异常,finally代码块的内容都会被执行,除非try代码块中有System.exit()执行强制退出)
}
throw和throws都是在异常处理中使用的关键字
throw:指的是在方法中人为抛出一个异常对象(这个异常对象可能是自己创建的异常或者已存在的);
throws:在方法的声明上使用,表示此方法在调用时必须处理异常。
Object类中的方法
1.getClass()
获取运行时类型,返回值为当前的Class对象
2.hashCode()
返回该对象的哈希码值,是为了提高哈希表的性能(HashTable)
3.equals()
判断两个对象是否相等,在Object中equals是等价于==的,但是在String及某些类对equals进行了重写,实现不同的比较。
4.clone()
我们有时候不希望在方法里将参数改变,这时就需要在类中复写clone方法。可以用于保护权限。
如果在clone方法中调用super.clone()方法需要实现Cloneable接口,否则会抛出CloneNotSupportedException。
此方法只实现了一个浅层拷贝,对于基本类型字段成功拷贝,但是如果是嵌套对象,只做了赋值,也就是只把地址拷贝了,所以没有成功拷贝,需要自己重写clone方法进行深度拷贝。
class Student implements Cloneable{
@Override
protected native Object clone() throws CloneNotSupportedException{
return supper.clone; //浅拷贝
}
5.toString()
返回一个String字符串,用于描述当前对象的信息,可以重写返回对自己有用的信息,默认返回的是当前对象的类名+hashCode的16进制数字。
6.wait()
多线程时用到的方法,作用是让当前线程进入等待状态,同时也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒
7.notify()
多线程时用到的方法,唤醒该对象等待的某个线程
8.notifyAll()
多线程时用到的方法,唤醒该对象等待的所有线程
9.finalize()
对象在被GC释放之前一定会调用finalize方法,对象被释放前最后的挣扎,因为无法确定该方法什么时候被调用,很少使用。
final finally finaize的区别
1、final修饰符(关键字)。被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,即不能方法重写。
2、finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。
3、finalize是方法名。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
抽象类和接口的区别
接口和抽象类都不能被实例化
1.实现方法
(1)抽象类中可以有已经实现了的方法,也可以有被abstract修饰的方法(抽象方法),因为存在抽象方法,所以该类必须是抽象类。
(2)接口根本不存在方法的实现。
但是接口要求只能包含抽象方法,抽象方法是指没有实现的方法。接口就根本不能存在方法的实现。
子类使用的关键词不一样
(1)实现抽象类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。但是如果子类将抽象方法没有全部实现,就必须把自己也修饰成抽象类,交于继承它的子类来完成实现。以此类推,直到没有抽象函数。
(2)子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现。不能再推给下一代。接口中除了static、final变量,不能有其他变量。
是否有构造器
(1)抽象类可以有构造器
(2)接口不可以有构造器
可以使用的修饰符
(1)抽象方法可以有public、protected和default这些修饰符
(2)接口方法默认修饰符是public。你不可以使用其它修饰符。
速度方面
(1)抽象方法比接口速度要快
(2)接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
增加新方法对子类的影响
(1)如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此不需要改变现在的代码。抽象类可以有一些非抽象方法的存在,这些方法被称为默认实现。如果添加一个默认实现方法(不能是抽象方法),就不需要在子类中去实现,所以继承这个抽象类的子类无须改动。
(2)接口中只能添加抽象方法,当你添加了抽象方法,实现该接口的类就必须实现这个新添加的方法。
子类能继承的数量
抽象类只有单继承,接口就可以实现多继承
反射
反射就是把Java类中的各个组成部分进行解剖,并映射成一个个的Java对象,拿到这些对象后可以做一些事情。拿到映射后的构造方法,可以用它来生成对象;拿到映射后的方法,可以调用它来执行对应的方法;拿到映射后的字段,可以用它来获取或改变对应字段的值。反射是框架设计的灵魂,可以做一些抽象度比较高的底层代码。
反射的使用
1.拿到当前类的class对象
class People4{
private String name;
private People4(){
System.out.println("构造函数");
}
private People4(String name){
this.name = name;
}
private void eat(Integer a){
System.out.println("吃饭");
}
}
获取class对象有三种方法
Class c = People.class;
Class c = Class.forName("People");
People p = new People();
Class c = p.getClass();
注意:
在运行期间,一个类,只有一个Class对象产生。
三种方式常用第二种,第三种对象都有了还要反射干什么。第一种需要导入类的包,依赖太强,不导包就抛编译错误。一般都第二种,一个字符串可以传入也可写在配置文件中等多种方法。
2.通过反射获取构造方法,生成当前类的对象
//获取无参构造
Constructor constructor = peopleClass.getDeclaredConstructor(); //获取protected private public函数
constructor.setAccessible(true);//如果构造函数是私有的,修改访问权限为可访问权限
//根据无参构造函数New对象
Object object = constructor.newInstance();
Constructor con = peopleClass.getDeclaredConstructor(String.class);//获取有参函数
constructor.setAccessible(true);
Object obj = constructor.newInstance("李四");
3.通过class对象获取成员变量或者成员方法进行方法调用
//根据对象调用函数p.eat();
//1.获取方法是eat 2.根据对象object 调用eat
Method method = peopleClass.getMethod("eat");
method.setAccessible(true);
method.invoke(object);
//调用成员变量
Field field = peopleClass.getDeclaredField("setName");
field.setAccessible(true);
field.set(object,"张三");
System.out.println(field.get(object));
类加载
类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“com.lyj.load”))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
注意:
对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态变量的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。
类加载过程
1.装载阶段
装载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:
(1)启动类加载器(bootstrap classloader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类),是顶级类加载器
(2)扩展类加载器(extensions classloader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
(3)应用类加载器(application classloader):它负责在JVM启动时加载来自Java命令的classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。
类加载机制主要有三种:
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派(重点):所谓的双亲委派,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。优点:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次;具有安全性,java核心api中定义类型不会被随意替换,这样便可以防止核心API库被随意篡改。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
2.链接阶段
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
3)解析:将类的二进制数据中的符号引用替换成直接引用。
3.初始化阶段
初始化是为类的静态变量赋予正确的初始值。如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。