char
类型使用单引号'
,且仅有一个字符,要和双引号"
的字符串类型区分开;
var关键字:var sb = new StringBuilder():编译器会根据赋值语句自动推断出变量
sb
的类型是StringBuilder
移位运算:>>:不断地÷2、<<:不断地×2;
>>>:符号位也会跟着移动;
~:非运算
^:异或运算
自动转型:小类型转为大类型,如:short-->int
强制转型:short s = (short) i:这里的i原本是int型,注意若超过范围就会保留低位的两个字节,而抛弃两个高字节。
浮点数0.1
在计算机中就无法精确表示,因为十进制的0.1
换算成二进制是一个无限循环小数,很显然,无论使用float
还是double
,都只能存储一个0.1
的近似值。但是,0.5
这个浮点数又可以精确地表示。
所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数。
char字符类型可以用u+Unicode表示:char c3 = 'u0041';
转义字符:
如果用+
连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接;
printf:格式化输出
例:System.out.printf("%.4f ", d); // 显示4位小数3.1416
%为占位符,有多少个占位符就要传入多少个数
例:System.out.printf("n=%d, hex=%08x", n, n); // 注意,两个%占位符必须传入两个数
if条件:if...else if....else.....:如果前面if匹配到了后面的逻辑不会运行
引用类型判断可以用equals()、基本类型用==;
引用类型equals和==的区别;“==”会比较两者是否为同个内存里对应的同个地址;而equals()只比较其“值”是否一致
数组排序:
JDK提供了Array.sort(),这个方法实际上是改变变量所指向的数组内容,而不是修改原有数组;
冒泡排序:每一轮循环后,最大的一个数被交换到末尾;
命令行参数:
命令行参数类型是String[]
数组;
命令行参数由JVM接收用户输入并传给main
方法;
例子:
public class Main { public static void main(String[] args) { for (String arg : args) { if ("-version".equals(arg)) { System.out.println("v 1.0"); break; } } } }
编译:javac Main.java
运行:java Main -version
可变参数,用类型...表示,如:String... names;这样传入的参数个数可根据自己定义;但是传入的值不能是null,因为传入0个参数时,接收到的实际值是一个空数组而不是null
如果是用String[] names表示的话,传入参数时要先构造String[],如:new String[] {"xxx","xxx"};这里是可以传入null值的;
参数传递:
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
instanceof
实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null
,那么对任何instanceof
的判断都为false
。
向上转型,用爸爸指向儿子,那么调用爸爸一定成功,因为爸爸会的儿子都会;
向下转型,用儿子指向爸爸,这个时候要看爸爸代表的是自己还是儿子,如果是自己那就转换失败,因为儿子有些会的爸爸并不会;
包含有抽象类方法的类无法被实例,且这个类也需要必需申明为抽象类;
接口:所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来
接口可以继承接口,而实例化的只能是某个接口的子类;如常用的:List list = new ArraysList();用list接口引用ArraysList子类的实例:
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例;
因为静态方法属于class
而不属于实例,因此,静态方法内部,无法访问this
变量,也无法访问实例字段,它只能访问静态字段。
可直接用class名.静态字段来访问静态字段;
静态方法和静态字段大同小异
interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉
包作用域是指一个类允许访问同一个package
的没有public
、private
修饰的class
,以及没有public
、protected
、private
修饰的字段和方法,包名必须完全一致,包没有父子关系
定义在一个class
内部的class
称为嵌套类(nested class
)
classpath:classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
classpath
的设定方法有两种:
在系统环境变量中设置classpath
环境变量,不推荐;
在启动JVM时设置classpath
变量,推荐。
没有设置系统环境变量,也没有传入-cp
参数,那么JVM默认的classpath
为.
,即当前目录
jar包:
jar包还可以包含一个特殊的/META-INF/MANIFEST.MF
文件,MANIFEST.MF
是纯文本,可以指定Main-Class
和其它信息。JVM会自动读取这个MANIFEST.MF
文件,如果存在Main-Class
,我们就不必在命令行指定启动的类名,而是用更方便的命令:
java -jar hello.jar
jar包还可以包含其它jar包,这个时候,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型项目中,不可能手动编写MANIFEST.MF
文件,再手动创建zip包。Java社区提供了大量的开源构建工具,例如Maven,可以非常方便地创建jar包。
自己开发程序,包括自己java程序所生成的jar文件、依赖的第三方jar文件以及JVM自带的jar文件:rt.jar;所以运行程序命令一般是:
java -cp app.jar:a.jar:b.jar:c.jar com.xxx.xxx
注意:JVM自带的标准库rt.jar不要写到classpath中,写了反而会干扰JVM的正常运行。
String工具类::
去除空白字符:trim(),去除 的空白字符;strip(),相比trim还可以去除类似中文空格的字符;
isEmpty():是否为空;
isBlank():是否只包含空白字符
String.join(String,String[]):用String字符串连接String[]字符数组形成新的字符串;
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串,例:
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
有几个占位符后面就传入几个参数
类型转换:
valueOf():这是一个重载方法,编译器会根据参数自动选择合适的方法;例:
String.valueOf(123); // "123" String.valueOf(45.67); // "45.67" String.valueOf(true); // "true" String.valueOf(new Object()); // 类似java.lang.Object@636be97c
实际上字符串在String
内部是通过一个char[]
数组表示的,因此字符char想转换成字符串String可以直接
char[] cs = "Hello".toCharArray(); // String -> char[] String s = new String(cs);
如果修改了char[]
数组,String
并不会改变:
public class Main { public static void main(String[] args) { char[] cs = "Hello".toCharArray(); String s = new String(cs); System.out.println(s); cs[0] = 'X'; System.out.println(s); } }
这是因为通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。
int n1 = Integer.parseInt("123");//String-->int
boolean b1 = Boolean.parseBoolean("true"); // true//String-->boolean
当通过引用传入参数数值时,为了防止外部的代码可以自主改动已初始化的值,可以在传入时使用Object.clone(),这样就只有在重新实例化对象时才会将对象传入值改动更新,例:
public class Main { public static void main(String[] args) { int[] scores = new int[] { 88, 77, 51, 66 }; Score s = new Score(scores); s.printScores();//[88,77,51,66] scores[2] = 99; s.printScores();//[88,77,51,66] s = new Score(scores); s.printScores();//[88,77,99,66] } } class Score { private int[] scores; public Score(int[] scores) { this.scores = scores.clone(); } public void printScores() { System.out.println(Arrays.toString(scores)); } }
字符编码:
ASCII
编码:一套英文字母、数字和常用符号的编码,占用一个字节,范围从0到127
GB2312
编码:加上中文后一个字节的ASCII编码明显不够用了,这个占用2个字节,为了区分ASCII编码,第一个字节的最高位始终是1;
为了统一全球所有语言的编码,全球统一码联盟发布了Unicode
编码,Unicode
编码需要两个或者更多字节表示;
UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码;
Java的String
和char
在内存中总是以Unicode编码表示。
包装类型:
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null;
将基本类型转变为引用类型,我们可以定义一个只含有该基本类型的字段,如int:
public class Integer { private int value; public Integer(int value) { this.value = value; } public int intValue() { return this.value; } }
定义好后就可以进行相互转换:
Integer n = null; Integer n2 = new Integer(99); int n3 = n2.intValue();
java给我们对基本数据类型都提供了各自的一个包装类;且提供了两大方法:
静态方法:Integer.valueof(int i):基本类转为引用类
intValue()方法:引用类转为基本类
不仅如此,还帮助我们自动在Int和Integer之间进行转换:
Integer n = 100; // 编译器自动使用Integer.valueOf(int) int x = n; // 编译器自动使用n.intValue()
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing),只发生在编译阶段,为的是减少代码;
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
int x1 = Integer.parseInt("100"); // 100 int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
String x3 = Integer.toString(100);//“100”,将100整数转为字符串
枚举一个javaBean类如Person的例子(使用java核心库提供的Introspector类):
public class Main { public static void main(String[] args) throws Exception { BeanInfo info = Introspector.getBeanInfo(Person.class); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { System.out.println(pd.getName()); System.out.println(" " + pd.getReadMethod()); System.out.println(" " + pd.getWriteMethod()); } } }
枚举类:可以在编译时就检查出错误,比如:超范围、类型比较等错误;
例:
public class Main { public static void main(String[] args) { Weekday day = Weekday.SUN; if (day == Weekday.SAT || day == Weekday.SUN) { System.out.println("Work at home!"); } else { System.out.println("Work at office!"); } } } enum Weekday { SUN, MON, TUE, WED, THU, FRI, SAT; }
通过关键字enum
实现的,我们只需依次列出枚举的常量名,
注意枚举类常量本身是有类型属性的,即Weekday.SUN
类型是Weekday
这里的枚举类其实也是引用类型,之所以可以用==进行比较,是因为enum
类型的每个常量在JVM中只有一个唯一实例;
由于每个枚举值都是class类,所以也提供了name()方法来返回常量名,如:
String s = Weekday.SUN.name(); // "SUN"
ordinal():返回定义常量的顺序,这也表明枚举类每个常量都是有固定的序列的,为了防止由于不小心改动枚举顺序导致的返回值和预想的不一致,毕竟编译器也无法在枚举值改变位置时报错,可以在定义枚举值时定义一个int值:如:
enum Weekday { MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0); public final int dayValue; private Weekday(int dayValue) { this.dayValue = dayValue; } }
Java使用enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
;
通过name()
获取常量定义的字符串,注意不要使用toString()
;
通过ordinal()
返回常量定义的顺序(无实质意义);
可以为enum
编写构造方法、字段和方法
enum
的构造方法要声明为private
,字段强烈建议声明为final
;
enum
适合用在switch
语句中。
BigInteger
用于表示任意大小的整数;
BigInteger
是不变类,并且继承自Number
;
将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数,常用于财务计算;
比较BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
。
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。我们创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的:
SecureRandom sr = new SecureRandom(); System.out.println(sr.nextInt(100));
异常:
所有异常都可以调用printStackTrace()
方法打印异常栈,这是一个简单有用的快速打印异常的方法。
Java使用异常来表示错误,并通过try ... catch
捕获异常;
Java的异常是class
,并且从Throwable
继承;
Error
是无需捕获的严重错误,Exception
是应该捕获的可处理的错误;
RuntimeException
无需强制捕获,非RuntimeException
(Checked Exception)需强制捕获,或者用throws
声明;
不推荐捕获了异常但不进行任何处理。
可以使用多个catch
语句,每个catch
分别捕获对应的Exception
及其子类。JVM在捕获到异常后,会从上到下匹配catch
语句,匹配到某个catch
后,执行catch
代码块,然后不再继续匹配。
多个catch
语句只有一个能被执行;存在多个catch
的时候,catch
的顺序非常重要:子类必须写在前面
一个catch
语句也可以匹配多个非继承关系的异常。用|符号隔开;
调用printStackTrace()
可以打印异常的传播栈,对于调试非常有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
例:
public class Main { public static void main(String[] args) { try { process1(); } catch (Exception e) { e.printStackTrace(); } } static void process1() { try { process2(); } catch (NullPointerException e) { throw new IllegalArgumentException(e);//传入原有的异常 } } static void process2() { throw new NullPointerException(); } }
通常不要在finally
中抛出异常。如果在finally
中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()
获取所有添加的Suppressed Exception
。
例:
public class Main { public static void main(String[] args) throws Exception { Exception origin = null; try { System.out.println(Integer.parseInt("abc")); } catch (Exception e) { origin = e; throw e; } finally { Exception e = new IllegalArgumentException(); if (origin != null) { e.addSuppressed(origin); } throw e; } } }
抛出异常时,尽量复用JDK已定义的异常类型;
自定义异常体系时,推荐从RuntimeException
派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。
NullPointerException
是Java代码常见的逻辑错误,应当早暴露,早修复;
可以启用Java 14的增强异常信息来查看NullPointerException
的详细错误信息。
但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages
参数启用它:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
可以将具体发生异常的地点定位出来;
Java标准库提供了java.util.logging
来实现日志功能。
日志的输出可以设定级别。JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
Logging系统在JVM启动时读取配置文件并完成初始化,一旦开始运行main()
方法,就无法修改配置;需要在JVM启动时传递参数-Djava.util.logging.config.file=<config-file-name>
Commons Logging是使用最广泛的日志模块;
使用Commons Logging只需要和两个类打交道,并且只有两步:
第一步,通过LogFactory
获取Log
类的实例; 第二步,使用Log
实例的方法打日志。
默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
Commons Logging定义了6个日志级别:
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO
。例:
public class Person { protected final Log log = LogFactory.getLog(getClass()); void foo() { log.info("foo"); } }
注意到实例变量log的获取方式是LogFactory.getLog(getClass())
,虽然也可以用LogFactory.getLog(Person.class)
,但是前一种方式有个非常大的好处,就是子类可以直接使用该log
实例。例如:
public class Student extends Person { void bar() { log.info("bar"); } }
Log4j:
通过Commons Logging实现日志,不需要修改代码即可使用Log4j;
使用Log4j只需要把log4j2.xml和相关jar放入classpath;
如果要更换Log4j,只需要移除log4j2.xml和相关jar;
只有扩展Log4j时,才需要引用Log4j的接口(例如,将日志加密写入数据库的功能,需要自己开发)。
介绍了Commons Logging和Log4j这一对好基友,它们一个负责充当日志API,一个负责实现日志底层,搭配使用非常便于开发。
还听说过SLF4J和Logback,其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
Commons Logging的接口要打印日志,经常会用到拼接字符串,如:
log.info("Set score " + score + " for Person " + p.getName() + " ok.");
而SLF4J的日志接口改进成:
logger.info("Set score {} for Person {} ok.", score, p.getName());
SLF4J的日志接口传入的是一个带占位符的字符串,用后面的变量自动替换占位符,所以看起来更加自然。
除了int
等基本类型外,Java的其他类型全部都是class,
而class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class
类型时,将其加载进内存。
每加载一种class
,JVM就为其创建一个Class
类型的实例,并关联起来。注意:这里的Class
类型是一个名叫Class
的class
。它长这样:
public final class Class { private Class() {} }
如何获取一个class
的Class
实例?有三个方法:
方法一:直接通过一个class
的静态变量class
获取:
Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();
方法三:如果知道一个class
的完整类名,可以通过静态方法Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
因为Class
实例在JVM中是唯一的,所以,上述方法获取的Class
实例是同一个实例。可以用==
比较两个Class
实例
如果获取到了一个Class
实例,我们就可以通过该Class
实例来创建对应类型的实例:
// 获取String的Class实例: Class cls = String.class; // 创建一个String实例: String s = (String) cls.newInstance();
它的局限是:只能调用public
的无参数构造方法
动态加载:JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载
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(不包括父类)
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义。
还可以通过用Field.get(Object)
获取指定实例的指定字段的值。
通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的实例,第二个Object
参数是待修改的值。
通过反射读写字段是一种非常规方法,它会破坏对象的封装。可以调用Field.setAccessible(true)
来访问非public
字段。
我们已经能通过Class
实例获取所有Field
对象,同样的,可以通过Class
实例获取所有Method
信息。Class
类提供了以下几个方法来获取Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
获取的Method类似:
public int Student.getScore(java.lang.String)
一个Method
对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。
例:
public class Main { public static void main(String[] args) throws Exception { // 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); } }
对Method
实例调用invoke
就相当于调用该方法,invoke
的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
如果是静态方法即无需实例化对象,第一个参数直接传入null就好:
public class Main { public static void main(String[] args) throws Exception { // 获取Integer.parseInt(String)方法,参数为String: Method m = Integer.class.getMethod("parseInt", String.class); // 调用该静态方法并获取结果: Integer n = (Integer) m.invoke(null, "12345"); // 打印调用结果: System.out.println(n); } }
对于非public方法的调用跟字段一样,可以调用Field.setAccessible(true)
来访问非public方法;
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在):
如:
Method m = Person.class.getMethod("hello"); m.invoke(new Student());
实际调用的就是Student里的hello方法;相当于:
Person p = new Student(); p.hello();
通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
如果要调用有参的构造函数需先通过Class实例获取Constructor(构造方法)方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
如:
Constructor cons2 = Integer.class.getConstructor(String.class); Integer n2 = (Integer) cons2.newInstance("456");
注意Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
获取到某个class对象后,getSuperclass()可以获得其父类对象;对所有interface
的Class
调用getSuperclass()
返回的是null
getInterfaces()可以获取其接口类对象,返回的是class类型的数组;如果一个类没有实现任何interface
,那么getInterfaces()
返回空数组。
如果是两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理:
一个接口People,一个继承接口的被代理类Student,另一个继承接口的代理类Leader;实现过程
1、构建一个handler来实现InvocationHandler接口,需实现invoke方法,也是在这里实现业务的修改逻辑,且需要创建一个构造方法:public handler(Object Student){this.Student=Student}
2、调用Proxy.newProxyInstance(Student.getClassLoader(),Student.getClass,handler(实例));生成代理类;