类的加载
类的加载概述
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。
加载:就是指将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。
连接:
验证 是否有正确的内部结构,并和其他类协调一致
准备 负责为类的静态成员分配内存,并设置默认初始化值
解析 将类的二进制数据中的符号引用替换为直接引用
初始化:就是基本对象的初始化步骤,可以看下这篇(点此传送门)
加载时机
- 创建类的实例
- 访问类的静态变量,或者为静态变量赋值
- 调用类的静态方法
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
- 初始化某个类的子类
- 直接使用java.exe命令来运行某个主类
类加载器
概述
负责将.class文件加载到内存中,并为之生成对应的Class对象。虽然我们不需要关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行。
分类及其作用
Bootstrap ClassLoader 根类加载器
也被称为引导类加载器,负责Java核心类的加载。比如System,String等。在JDK中JRE的lib目录下rt.jar文件中
Extension ClassLoader 扩展类加载器
负责JRE的扩展目录中jar包的加载。在JDK中JRE的lib目录下ext目录
Sysetm ClassLoader 系统类加载器
负责在JVM启动时加载来自java命令的class文件,以及classpath环境变量所指定的jar包和类路径
反射
反射概述
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。
而解剖使用的就是Class类中的方法,所以先要获取到每一个字节码文件对应的Class类型的对象。
获取Class对象三种方式:
- Object类的getClass()方法
- 静态属性class
- Class类中静态方法forName()
package info.test;
public class Person {//Person类,用于下面的练习
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void show() {
System.out.println("show 一下");
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
三种方式的代码:
public static void main(String[] args) throws ClassNotFoundException {
//第一种获取方式
Person person = new Person();
Class clazz1 = person.getClass();
//第二种获取方式
Class clazz2 = Person.class;
//第三种获取方式
Class clazz3 = Class.forName("info.test.Person");
}
反射的三个阶段:
上面的读取配置文件啥意思?看如下代码
interface Fruit {
public void squeeze();
}
class Apple implements Fruit {
@Override
public void squeeze() {
System.out.println("榨出一杯苹果汁");
}
}
class Orange implements Fruit {
@Override
public void squeeze() {
System.out.println("榨出一杯橘子汁");
}
}
class Juicer {
public void run(Fruit f) {
f.squeeze();
}
}
读取配置文件实现类的创建
public class Test {
public static void main(String[] args) throws Exception {
Juicer juicer = new Juicer();
//普通方式
juicer.run(new Apple());
juicer.run(new Orange());
System.out.println("----------");
//配置文件读取方式
InputStream is = Test.class.getClassLoader().getResourceAsStream("config.properties");
BufferedReader br = new BufferedReader(new InputStreamReader(is));
Class<?> clazz = Class.forName(br.readLine());
Fruit fruit = (Fruit)clazz.newInstance();
juicer.run(fruit);
}
}
注:config.properties 文件在src目录下
虽然下面使用配置文件读取的方式比上面的普通代码要多,而且繁琐。虽然写的时候有点费事,但是其灵活性比上面的代码大大增多了,当我们需要一杯橘子汁时,普通方法需要在源文件中进行修改,而现在只需使用配置文件修改即可,配置文件的修改是符合开闭原则的,而前面则违反了开闭原则。
反射获取构造方法
首先,来看一下我们Class类中直接创建对象。
public T newInstance() throws InstantiationException,IllegalAccessException
创建此 Class 对象所表示的类的一个新实例。
如同用一个带有一个空参数列表的 new 表达式实例化该类。如果该类尚未初始化,则初始化这个类。
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
Person person = (Person) clazz.newInstance();
person.setAge(23);
person.setName("张三");
System.out.println(person);
}
//outPut:Person [name=张三, age=23]
通过上面的API可以发现,该方法会调用空参构造,假设我们注释掉Person类中的构造会怎样?
Exception in thread "main" java.lang.InstantiationException: info.test.Person
那么它会给你一个异常。那就再来查看我们的API,肯定有其他的方式创建
public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
//返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。
以下是Constructor内方法
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
//使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
//使用Constructor来实现clazz.newInstance()
Constructor c = clazz.getConstructor();
Person p = (Person) c.newInstance();
//使用Constructor来实现有参构造创建对象
c = clazz.getConstructor(String.class,int.class); //获取有参构造
p = (Person) c.newInstance("张三",23);//通过有参构造创建对象
}
这样就使用有参构造创建了一个对象,在getConstructor方法中,你可以一个都不给(无参构造),也可以给Class对象(Class 对象要按其形参声明顺序排列)。还有在时Constructor还是在反射阶段(操作字节码),所以传进去的也是需要其字节码。而到了下面的newInstance就到了创建对象的阶段,需传进去实际的参数值。
反射获取成员变量
public Field getField(String name) throws NoSuchFieldException, SecurityException
//返回一个 Field 对象,它反映此 Class 对象所表示的类或接口的指定公共成员字段
以下是Field内方法
public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException
//将指定对象变量上此 Field 对象表示的字段设置为指定的新值。
//如果底层字段的类型为基本类型,则对新值进行自动解包。
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
Person p = new Person("李四",24);
Field f = clazz.getField("name");
f.set(p, "张三");
System.out.println(p);
}
// Exception in thread "main" java.lang.NoSuchFieldException: name
可以发现,竟然报错了,明明我们有name字段的,为什么会找不到?仔细看看API的说明指定公共成员字段,再看看我们上面Person的字段(private)的。不过,在反射面前,一切都是赤裸裸的,我们可以暴力获取,但是单单暴力获取后直接修改值还是报错(java.lang.IllegalAccessException),我们还需给它去除权限才可正常使用。举个小例子:如果你想让某个人听令于你,那么你抓到他也没用,他不会情愿给你干活,还需给它进行洗脑后才可(哈~)
public Field getDeclaredField(String name) throws NoSuchFieldException,SecurityException
//返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段
以下为AccessibleObject内方法
其子类有:Constructor, Field, Method
public void setAccessible(boolean flag) throws SecurityException
//值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。(去除私有权限)
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
Person p = new Person("李四",24);
// Field f = clazz.getField("name");获取姓名字段
Field f = clazz.getDeclaredField("name");//暴力反射获取字段
f.setAccessible(true);//去除私有权限
f.set(p, "张三");//修改姓名的值
System.out.println(p);
}
// Person [name=张三, age=24]
同样的,我们也可暴力获取构造或者方法。
反射获取方法
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
//返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法
//name为方法名,parameterTypes 参数是按其形参声明顺序排列的 Class 对象的一个数组
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException
//返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法(暴力获取)
以下为Method内方法
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
//对带有指定参数的指定对象调用由此 Method 对象表示的底层方法
//如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null
//如果底层方法所需的形参数为 0,则所提供的 args 数组长度可以为 0 或 null
public static void main(String[] args) throws Exception {
Class clazz = Person.class;
Person p = new Person("李四",24);
Method m = clazz.getMethod("setName",String.class);//获取setName方法
m.invoke(p,"张三"); //由p调用,参数为 张三
System.out.println(p);
//静态方法的调用
m = clazz.getMethod("show");
m.invoke(null); //静态方法调用所以第一个参数为null,由于方法无参数,后面可省略也可写null
}
/*outPut:
* Person [name=张三, age=24]
* show 一下
*/
由于上述方法都为公共的,可以直接获取,若是私有方法,需使用暴力获取并去除权限
反射小习一:
ArrayList<Integer>的一个对象,在这个集合中添加一个字符串数据,如何实现呢?
泛型其实是在set()处的类型检验,而在get()处的类型转换。由于泛型擦除(点此传送门),那么在运行过程中,一切都只是Object,所以此处我们可通过反射处理。
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<>();
list.add(123);
//list.add("234"); 编译错误
Class clazz = list.getClass();
Method m = clazz.getMethod("add", Object.class); //获取 add 方法,实际字节码文件中其参数就是Object(泛型擦除)
m.invoke(list, "234");//调用方法,插入字符串 "234"
System.out.println(list);
}
/*outPut:
* [123, 234]
*/
反射小习二:
使用反射写一个通用的方法来设置某个对象的某个属性为指定的值
public static void setProperty(Object obj, String propertyName,Object value) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
Class clazz = obj.getClass();//获取字节码对象
Field f = clazz.getDeclaredField(propertyName);//暴力反射获取字段
f.setAccessible(true);//去除权限
f.set(obj, value);//设置值
}
反射小习三:
已知有如下该类:
package xxx.xxx.test;
public class Demo {
public void run() {
System.out.println("go! go! go!");
}
}
使用读取配置文件(配置类的完整名称)的方式获取其类的完整名称并加载该类,使用反射运行run方法
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("config.properties")));
String name = br.readLine();//读取配置文件
Class clazz = Class.forName(name);//获取字节码对象
Demo demo = (Demo) clazz.newInstance();//创建新实例
Method m = clazz.getMethod("run");//获取run方法
m.invoke(demo);//调用
}
注:config.properties 文件在项目根目录下。
动态代理
动态代理概述:
代理:本来应该自己做的事情,请了别人来做,被请的人就是代理对象。(春节回家买票让人代买)
动态代理:在程序运行过程中产生的这个对象,而程序运行过程中产生对象其实就是我们刚才反射讲解的内容,所以,动态代理其实就是通过反射来生成一个代理
在Java中java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口就可以生成动态代理对象。JDK提供的代理只能针对接口做代理。我们有更强大的代理cglib,Proxy类中的方法创建动态代理类对象。
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
最终会调用InvocationHandler的方法
InvocationHandler Object invoke(Object proxy,Method method,Object[] args)
public interface IStudent {
public void login();
public void submit();
}
public class Student implements IStudent {
@Override
public void login() {
System.out.println("登录");
}
@Override
public void submit() {
System.out.println("提交");
}
}
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("权限校验");//方法执行前
method.invoke(target, args); //执行被代理target对象的方法
System.out.println("日志记录");//方法执行后
return null;
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();//未代理
stu.login();
stu.submit();
System.out.println("-----------");
MyInvocationHandler m = new MyInvocationHandler(stu);//使用代理
IStudent istu = (IStudent)Proxy.newProxyInstance(stu.getClass().getClassLoader(), stu.getClass().getInterfaces(), m);
istu.login();
istu.submit();
}
}
看完上面的例子,你一定对动态代理产生了极大的疑惑。其实如果只需会用动态代理,你发现上面的代码其实就是模板一样,很多东西都是定死的,照葫芦画瓢即可。但若想深入了解动态代理,我这里转载了一片关于动态代理的,可以帮助我们解决更多的疑惑,至少我自己看完感觉明了多了(点此传送门)
JDK5新特性
1,自动拆装箱
2,泛型
3,可变参数
4,静态导入
5,增强for循环
6,互斥锁
7,枚举
自己实现枚举类
枚举概述
是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内。举例:一周只有7天,一年只有12个月等。
单例类是一个类只有一个实例。那么多例类就是一个类有多个实例,但不是无限个数的实例,而是有限个数的实例。这才能是枚举类。
//自定义枚举实现一
class Week_1 {
public static final Week_1 MON = new Week_1();
public static final Week_1 TUE = new Week_1();
public static final Week_1 WED = new Week_1();
private Week_1() {} //私有构造,不让其他类创建本类对象
}
//自定义枚举实现二
class Week_2 {
public static final Week_2 MON = new Week_2("星期一");
public static final Week_2 TUE = new Week_2("星期二");
public static final Week_2 WED = new Week_2("星期三");
private String name;
private Week_2(String name) {//同样还是需要私有构造
this.name = name;
}
public String getName() {
return name;
}
}
//自定义枚举实现三
abstract class Week_3 {
public static final Week_3 MON = new Week_3("星期一") {
@Override//使用匿名内部类方式创建
public void show() {
System.out.println("今天是星期一");
}
};
public static final Week_3 TUE = new Week_3("星期二") {
@Override
public void show() {
System.out.println("今天是星期二");
}
};
public static final Week_3 WED = new Week_3("星期三") {
@Override
public void show() {
System.out.println("今天是星期三");
}
};
private String name;
private Week_3(String name) {//私有构造
this.name = name;
}
public String getName() {
return name;
}
public abstract void show();//抽象方法
}
测试
public static void main(String[] args) throws Exception {
Week_1 mon = Week_1.MON;
System.out.println(mon); //info.test.Week_1@15db9742
Week_2 tue = Week_2.TUE;
System.out.println(tue.getName());//星期二
Week_3 wed = Week_3.WED;
wed.show();//今天是星期三
}
enum实现枚举类
简单概述:
定义枚举类要用关键字enum,所有枚举类都是Enum的子类
枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略。
枚举类可以有构造器,但必须是private的,它默认的也是private的。
枚举类也可以有抽象方法,但是枚举项必须重写该方法
枚举在switch语句中的使用
//枚举实现一
public enum Week_1 {
MON,TUE,WED;
}
//枚举实现二
public enum Week_2 {
MON("星期一"),TUE("星期二"),WED("星期三");
private String name;
private Week_2(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//枚举实现三
public enum Week_3 {
MON("星期一") {
@Override
public void show() {
System.out.println("今天是星期一");
}
},TUE("星期二") {
@Override
public void show() {
System.out.println("今天是星期二");
}
},WED("星期三") {
@Override
public void show() {
System.out.println("今天是星期三");
}
};
private String name;
private Week_3(String name) {
this.name = name;
}
public String getName() {
return name;
}
public abstract void show();
}
测试:
public static void main(String[] args) throws Exception {
Week_1 mon = Week_1.MON;
System.out.println(mon); //MON
Week_2 tue = Week_2.TUE;
System.out.println(tue.getName());//星期二
Week_3 wed = Week_3.WED;
wed.show();//今天是星期三
switch(tue) {
case MON :
System.out.println("星期一啦~");
break;
case TUE :
System.out.println("星期二啦~");
break;
}//星期二啦~
}
上面说了,所有枚举类都是Enum的子类。那么我们来看看该类中有什么常见方法。
//返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。 相当于编号
public final int ordinal()
//比较此枚举与指定对象的顺序
public final int compareTo(E o)
//返回此枚举常量的名称,在其枚举声明中对其进行声明
public final String name()
//返回枚举常量的名称,它包含在声明中。可以重写此方法
public String toString()
//返回带指定名称的指定枚举类型的枚举常量
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)
//此方法虽然在JDK文档中查找不到,但每个枚举类都具有该方法,它遍历枚举类的所有枚举值非常方便
values() 方法
public static void main(String[] args) throws Exception {
Week_1 mon = Week_1.MON;
Week_1 tue = Week_1.TUE;
System.out.println(mon.ordinal());//枚举项都是有编号的
System.out.println(tue.ordinal());
System.out.println("---------");
System.out.println(mon.compareTo(tue));//比较的是编号
System.out.println("---------");
System.out.println(mon.name());//获取实例名称
System.out.println(mon.toString());//调用toString方法
System.out.println("---------");
Week_2 wed = Week_2.valueOf(Week_2.class, "WED");//通过字节码对象获取枚举项
System.out.println(wed);
System.out.println("---------");
Week_3[] arr = Week_3.values();
for (Week_3 week : arr) {
System.out.println(week);
}
}
结果:
0
1
---------
-1
---------
MON
MON
---------
WED
---------
MON
TUE
WED
JDK7新特性
A:二进制字面量
B:数字字面量可以出现下划线
C:switch 语句可以用字符串
D:泛型简化,菱形泛型
E:异常的多个catch合并,每个异常用或|
F:try-with-resources 语句
public static void main(String[] args) throws Exception {
//二进制字面量
System.out.println(0b111); //结果为 7
//数字字面量可以出现下划线,该特性只为方便观察
System.out.println(100_000);//结果为 1000000
//剩余特性不在罗列,之前的学习都碰到过
}
JDK8新特性
1.接口中可以定义有方法体的方法,如果是非静态,必须用default修饰。如果是静态的就不用了
interface test {
public default void run() {
System.out.println("gogogo!");
}
public static void show() {
System.out.println("show!");
}
}
2.局部内部类在访问他所在方法中的局部变量必须用final修饰,而JDK8中可不使用final(其默认是final)
public void run() {
int x = 10;//编译未报错
class Inner {
public void method() {
System.out.println(x);
}
}
Inner inner = new Inner();
inner.method();
}
为啥JDK7以及以前版本需要final修饰呢?
因为当调用这个方法时,局部变量如果没有用final修饰,他的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量也会消失,那么如果局部内部类对象还没有马上消失想用这个局部变量,就没有了,如果用final修饰会在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用。