Java基础-拓展1
自定义类加载器
为什么需要自定义类加载器
网上的大部分自定义类加载器文章,几乎都是贴一段实现代码,然后分析一两句自定义 ClassLoader 的 原理。但是我觉得首先得把为什么需要自定义加载器这个问题搞清楚,因为如果不明白它的作用的情况 下,还要去学习它显然是很让人困惑的。
首先介绍自定义类的应用场景:
- (1)加密:Java 代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将 编译后的代码用某种加密算法加密,类加密后就不能再用 Java 的 ClassLoader 去加载类了,这时就需要自 定义 ClassLoader 在加载类的时候先解密类,然后再加载。
- (2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载 器,从指定的来源加载类。
- (3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安 全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后 的字节代码,接着进行解密和验证,最后定义出在 Java 虚拟机中运行的类。
1. 双亲委派模型
在实现自己的 ClassLoader 之前,我们先了解一下系统是如何加载类的,那么就不得不介绍双亲委派模 型的实现过程
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型的工作过程如下:
- (1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加 载的类。
- (2)如果没有找到,就去委托父类加载器去加载(如代码 c = parent.loadClass(name, false)所示)。 父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托 父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父 加载器去加载。
- (3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib 里未查找到该 class),则会抛出一个异 常 ClassNotFoundException,然后再调用当前加载器的 findClass()方法进行加载。
双亲委派模型的好处:
- (1)主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String。
- (2)同时也避免了类的重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不 同的 ClassLoader 加载就是不同的两个类。
2. 自定义类加载器
- (1)从上面源码看出,调用 loadClass 时会先根据委派模型在父加载器中加载,如果加载失败,则会调 用当前加载器的 findClass 来完成加载。
- (2)因此我们自定义的类加载器只需要继承 ClassLoader,并覆盖 findClass 方法,下面是一个实际例 子,在该例中我们用自定义的类加载器去加载我们事先准备好的 class 文件。
2.1 自定义一个 People.java 类做例子
public class People {
private String name;
public People() {
}
public People(String name) {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "I am a people, my name is " + name;
}
}
2.2 自定义类加载器
自定义一个类加载器,需要继承 ClassLoader 类,并实现 findClass 方法。其中 defineClass 方法可以把二 进制流字节组成的文件转换为一个 java.lang.Class(只要二进制字节流的内容符合 Class 文件规范)。
Java 中的异常处理
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable: 有两个重 要的子类:Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大 量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执 行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误 (Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误 (Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它 们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的 应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错 误通过 Error 的子类描述。
- Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类
- RuntimeException。RuntimeException 异常由 Java 虚拟机抛出。
- NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、
- ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和
- ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable 类常用方法
- • public string getMessage():返回异常发生时的详细信息
- • public string toString():返回异常发生时的简要描述
- • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖 这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage() 返回的结果相同
- • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
异常处理总结
- • try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- • catch 块:用于处理 try 捕获到的异常。
- • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。 当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。在以下 4 种特殊 情况下,finally 块不会被执行:
- 在 finally 语句块中发生了异常。
- 在前面的代码中用了 System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭 CPU。
Java 序列化中如果有些字段不想进行序列化 怎么办
对于不想进行序列化的变量,使用 transient 关键字修饰。 transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和方法。
获取用键盘输入常用的的两种方法
- 方法 1:通过 Scanner Scanner input = new Scanner(System.in); String s = input.nextLine(); input.close();
- 方法 2:通过 BufferedReader BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String s = input.readLine();