一、类加载器

1.1 类的加载

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载,类的连接,类的初始化这三个步骤来对类进行初始化。如果不出现意外情况,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者类初始化。

  • 一个类的生命周期包括了 “加载”、“验证”、“准备”、“解析”、“初始化”、“使用”、“卸载” 这七个阶段,
  • 一般我们只研究前五个阶段,这五个阶段又可以分为 “加载”、“连接(准备,验证,解析)” 、 “初始化”

1.1.1 类的加载

用于加载二进制数据,类的加载主要做三件事情:

  • 找到类文件(通过类的全限定名来获取定义此类的二进制字节流)
    首先会根据各种途径(比如网络下载、数据库提取、从jar,zip中读取等)获取类的二进制数据
  • 放入方法区(将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构)
    • 把获取到的二进制数据读入内存,存储在运行时数据区的方法区,
    • 这些二进制数据所代表的存储结构会被转化为方法区中运行时的数据结构
  • 开个入口(生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口)
    • 在方法区中创建相应的class对象(这里的class对象与平时所说的对象是不一样的,
    • 当使用new创建实例对象时,就会通过class对象在堆中创建实例对象)用来封装保存在方法区内的数据结构

面试题


创建对象时类的加载顺序

  • 先加载父类,执行父类静态代码块,父类静态变量与方法,父类成员变量与方法,创建父类对象
  • 加载子类,执行子类静态代码块,子类静态变量与方法,子类成员变量与方法,创建子类对象

1.1.2 类的连接

  • 验证阶段:用于检验被加载的类是否有正确的内部结构,并和其他类协调一致

文件格式验证:验证字节流是否符合class文件的规范,确保输入的字节流能正确解析并存储到方法区

元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合规范要求。
字节码验证:这个阶段是比较复杂的,通过数据流和控制流分析,对类的方法体进行校验,确保程序的合法性
符号引用验证:这里的符号引用不单单指类的,也包括方法。
发生在符号引用转为直接引用的时候,也就是解析阶段,
对常量池中各种符号引用的信息进行匹配性校验,确保解析动作正确执行

  • 准备阶段:负责为类的类变量分配内存,并设置默认初始化值

需要注意的是:

  • 静态变量只会给默认值。
    • 例:public static int value = 123; // 此时赋给value的值是0,不是123。
  • 静态常量(static final修饰的)则会直接赋值。
    • 例:public static final int value = 123; // 此时赋给value的值是123。
  • 解析阶段:将类的二进制数据中的符号引用替换为直接引用

1.1.3 类的初始化

类的初始化的主要工作是为静态变量赋程序设定的初值。

1.1.4 类的初始化步骤

  • 假如类还未被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还未被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句
  • 注意:在执行第2个步骤的时候,系统对直接父类的初始化步骤也遵循初始化步骤1-3

1.1.5 类的初始化时机

  • 创建类的实例
  • 调用类的类方法
  • 访问类或者接口的类变量,或者为该类变量赋值
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
  • 初始化某个类的子类
  • 直接使用java命令来运行某个主类

1.2 类加载器

1.2.1 概念

负责将类的对应.class文件加载到内存中,并为之生成对应的 java.lang.Class 对象。虽然我们不用过分关心类加载机制,但是了解这个机制我们就能更好的理解程序的运行

1.2.2 类加载机制

  • 全盘负责

    就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托

    就是当一个类加载器负责加载某个Class时,先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制

    保证所有加载过的Class都会被缓存,当程序需要使用某个Class对象时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存储到缓存区

1.2.3 双亲委派机制

  • 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  • 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载, 一直到bootstrp ClassLoader.
  • 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

这样做的好处是什么?

1.避免重复加载。当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
2.为了安全。避免核心类,比如String被替换。

1.2.4 Java中的内置类加载器

当JVM启动的时候,Java缺省开始使用如下三种类加载器

  • BootstrapClassLoader

    引导类加载器, 用来加载 Java 的核心库,是用原生代码来实现的,
    并不继承自 java.lang.ClassLoader。它负责将jre/lib下面的核心类库.

  • PlatformClassLoader

    平台类加载器可以看到所有平台类 ,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类

  • SystemClassLoader

    它也被称为应用程序类加载器(ApplicationClassLoader) ,与平台类加载器不同。 系统类加载器通常用于定义应用程序类路径,模块路径和JDK特定工具上的类

  • UserClassLoader

    自定义类加载器

类加载器的继承关系:System的父加载器为Platform,而Platform的父加载器为Bootstrap

面试题


常用类加载器

  • BootstrapClassLoader
    引导类加载器, 用来加载 Java 的核心库,是用原生代码来实现的,
    并不继承自 java.lang.ClassLoader。它负责将jre/lib下面的核心类库.
  • PlatformClassLoader
    平台类加载器可以看到所有平台类 ,平台类包括由平台类加载器或其祖先定义的Java SE平台API,其实现类和JDK特定的运行时类
  • SystemClassLoader
    它也被称为应用程序类加载器(ApplicationClassLoader) ,与平台类加载器不同。 系统类加载器通常用于定义应用程序类路径,模块路径和JDK特定工具上的类
  • UserClassLoader
    自定义类加载器

二、反射

2.1 概念

在类的加载之后,会对每一个类生成对应的class对象并扎起缓存中进行存储,反射就是通过代码的书写,对每一个类声明称的class对象进行操作,在运行过程中获取一个类的变量和方法信息。然后通过获取到的信息来创建对象,调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展

2.2 class对象的获取

2.2.1 通过类名获取

  • 类名.class属性
	//通过类名获取
	//所有的类都有class属性  用于获取该类对应的Class对象
	Class<Student> c1=Student.class;

2.2.2 通过已有类的对象获取

  • 对象名.getClass()方法
	//通过已有对象获取
	//所有对象都继承Object 提供了getClass方法获取对应的Class对象
	Student s=new Student();
	Class c2 = s.getClass();

2.2.3 通过类的全路径名获取

  • Class.forName(全类名)方法
		//通过类的全路径名获取
		//通过Class提供的静态方法 通过类的全路径字符串加载对应的类
		Class c3 = Class.forName("com.yunhe.day0706.Student");
public class Student{
	private String name;
	private int age;
	private Student(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public Student(int age) {
		super();
		this.age = age;
	}
	private Student(String name) {
		super();
		this.name = name;
	}
	public Student() {
		super();
	}
	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;
	}
	private void test(){
		
	}
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}
}

2.3 获取构造方法并使用

2.3.1 Class类获取构造方法对象

方法名 说明
Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组
Constructor getConstructor(Class<?>… parameterTypes) 返回单个公共构造方法对象
Constructor getDeclaredConstructor(Class<?>… parameterTypes) 返回单个构造方法对象
		//通过类的全路径名获取
		//通过Class提供的静态方法 通过类的全路径字符串加载对应的类
		Class<?> c3 = Class.forName("com.yunhe.day0706.Student");
		//获取构造方法并执行
		//获取公开的构造方法
		Constructor[] constructors = c3.getConstructors();
		//获取当前类所有构造方法
		Constructor[] declaredConstructors = c3.getDeclaredConstructors();
		//获取指定参数的构造方法只能获取public修饰的构造方法
		Constructor<?> constructor = c3.getConstructor(int.class);
		//获取指定参数的构造方法 可以获取私有的构造方法
		Constructor<?> declaredConstructor = c3.getDeclaredConstructor(String.class,int.class);
		

2.3.2 用于创建对象的方法

方法名 说明
T newInstance(Object…initargs) 根据指定的构造方法创建对象
void setAccessible(boolean bool) 设置私有构造方法可以使用
		//在获取构造方法对象之后调用对应构造方法对象方法创建对象
		Object o1 = constructor.newInstance(18);
		System.out.println(o1);
		
		//私有的构造方法使用前必须进行授权操作
		declaredConstructor.setAccessible(true);
		Object o2 = declaredConstructor.newInstance("张三",18);
		System.out.println(o2);

2.4 获取属性并使用

2.4.1 Class类获取成员变量对象的方法

方法名 说明
Field[] getFields() 返回所有公共成员变量对象的数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 返回单个公共成员变量对象
Field getDeclaredField(String name) 返回单个成员变量对象
	//通过类的全路径名获取
    //通过Class提供的静态方法 通过类的全路径字符串加载对应的类
	Class<?> c3 = Class.forName("com.yunhe.day0706.Student");
	//获取变量并进行赋值
	//获取当前类中public修饰的属性(包含父类)
	Field[] fields = c3.getFields();

	//获取当前类中所有的属性(不包含父类变量包含私有属性)
	Field[] declaredFields = c3.getDeclaredFields();
	
	//根据属性名获取公开的属性
    Field field = c3.getField("abc");
 
    //获取当前类中指定属性名的属性对象(包含私有)
	Field declaredField = c3.getDeclaredField("name");

2.4.2 用于使用的方法

方法名 说明
void set(Object obj,Object value) 给obj对象的成员变量赋值为value
void get(Object obj) 获取obj对象的成员变量
void setAccessible(boolean bool) 设置私有构造方法可以使用
		//与构造方法不同,构造方法就是创建一个新的对象
		//但是对属性操作时 一般是对具体的属性进行赋值与取值
		//所以需要先创建对象之后才能对其属性进行操作
		Object o = c3.newInstance();//调用class类对象无参构造方法创建对象
		//调用属性赋值方法 为指定对象的该属性赋值
		field.set(o, 123);
		//调用属性取值方法 获取指定对象该属性的值
		Object object = field.get(o);

		declaredField.setAccessible(true);
		declaredField.set(o,"张三");
		System.out.println(o);

2.5 获取方法并使用

2.5.1 Class类获取成员方法对象的方法

方法名 说明
Method[] getMethods() 返回所有公共成员方法对象的数组,包括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括继承的
Method getMethod(String name, Class<?>… parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回单个成员方法对象
	
	//获取当前类中指定属性名的属性对象(包含私有)
	Field declaredField = c3.getDeclaredField("name");
	//获取方法
	//获取当前class对象代表类中所有公开的方法(包含继承的方法)
	Method[] methods = c3.getMethods();
	
	//获取当前class对象代表类中书写的方法(不包含继承的方法)
	Method[] declaredMethods = c3.getDeclaredMethods();
	 
	//获取当前class对象代表类的指定公开方法
	Method method = c3.getMethod("setName",String.class);
	
	//获取当前class对象代表类的指定方法(书写在类中的方法)
    Method declaredMethod = c3.getDeclaredMethod("test");

2.5.2 用于使用的方法

方法名 说明
Object invoke(Object obj,Object… args) 调用obj对象的成员方法,参数是args,返回值是Object类型
void setAccessible(boolean bool) 设置私有构造方法可以使用
	//执行对应方法对象
    //与属性对象使用相同,在执行方法时也需要具体的执行方法的对象
    Object o11 = c3.newInstance();//调用class类对象无参构造方法创建对象
    //传入调用此方法的对象 以及调用对象传入的参数直接执行
    method.invoke(o11, "李四");
    
    //私有方法执行之前需要进行赋权
    declaredMethod.setAccessible(true);
    declaredMethod.invoke(o11);
    
    System.out.println(o11);

三、接口中的新特性

3.1 概念

在jdk8以后对于接口提供了带有方法体方法书写的规范

  • JDK1.7之前
    • 抽象方法
    • 常量
  • JDK1.8
    • 默认方法
    • 静态方法
  • JDK1.9
    • 私有方法

3.2 默认方法的书写

默认方法:使用 default 修饰,不可省略,供子类调用或者子类重写。

3.2.1 默认方法定义格式

public default 返回值类型  方法的名称(参数列表){
    方法体
}
	//使用default关键字修饰的 书写在接口中的方法 称之为默认方法
	 public default void a(){
		System.out.println("默认方法a");
	}

3.2.2 默认方法的使用

  • 基本使用方式

使用default关键字修饰
实现类在实现默认方法所在接口后可以直接调用对应的方法,也可以重写对应方法

public interface MyInterface {
	//使用default关键字修饰的 书写在接口中的方法 称之为默认方法
	default public void a(){
		System.out.println("默认方法a");
	}
}
class MyClass implements MyInterface{
	public static void main(String[] args) {
		MyClass m=new MyClass();
		//实现类对象可以像继承一样直接调用方法
		m.a();
	}
}
  • 注意事项

如果一个实现类实现的多个接口拥有同名的默认方法,那么实现类必须重写,在重写的方法体中决定调用那个默认方法或都不调用自己书写

public class MyClass implements A,B{
	//如果实现多个接口中拥有相同的默认方法
	//必须重写并决定调用方
	@Override
	public void a() {
		A.super.a();//调用A接口的默认方法
		B.super.a();//调用B接口的默认方法
	}
}

interface A{
	default public void a(){
		System.out.println("A.a");
	}
}
interface B{
	default public void a(){
		System.out.println("B.a");
	}
}

3.3 静态方法的书写

静态方法:使用 static 修饰,供接口直接调用。
静态与.class文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用

3.3.1 静态方法定义格式

public static 返回值类型 方法名称(参数列表){
    方法体
}
interface MyInterface{
	//使用static修饰静态方法书写在接口中
	public static void a(){
		System.out.println("静态方法a");
	}
}

3.3.2 静态方法的使用

public class MyClass implements A,B{

	public static void main(String[] args) {
		//可以直接使用接口名.方法直接调用静态方法
		//实现类无法使用接口的静态方法
		//一个类可以实现多个接口,如果多个接口中有相同的静态方法,就区分不开,
		//使用的是哪个接口中的静态方法,所以只允许使用接口名.静态方法(参数列表) 使用
		A.a(); 
		B.a();
		MyClass m=new MyClass();
		//m.a();//报错  没有获取接口的静态方法
	}
}

interface A{
	  public static void a(){
		System.out.println("A.a");
	}
}
interface B{
	  public static void a(){
		System.out.println("B.a");
	}
}

四、Lambda表达式

4.1 概念

Lambda 表达式是 Java8 新增的重要特性,Lambda 使 Java 具有了类似函数式编程的风格,其实 Lambda 表达式也是一个 “语法糖”,其实质也是由编译器根据表达式推断最终生成原始语法的字节码方式。

4.2 Lambda表达式书写

  • 格式

    (形式参数) -> { 代码块 }

  • 格式解析

    • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可

    • ->:由英文中画线和大于符号组成,固定写法。代表指向动作

    • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容

4.3 Lambda表达式练习

4.3.1 Lambda表达式使用前提

  • 有一个接口

  • 接口中有且仅有一个抽象方法

4.3.2 抽象方法是无参无返回值

  • 代码实现
public class LambdaInterFace {	
	public static void main(String[] args) {
		
		LambdaA a=()->{
			System.out.println("方法a执行");
		};
		a.a();
	}
}
//无参无返回值
interface LambdaA{
	void a();
}

4.3.3 抽象方法是无参有返回值

  • 代码实现
public class LambdaInterFace {	
	public static void main(String[] args) {
		//如果方法有返回值 无需书写返回值 
		//lambda表达式会自动根据方法推断出返回值的类型
		//只需要在方法体中书写return返回对应数据即可
		LambdaB b=()->{
			return 111;
		};
		System.out.println(b.b());
	}
}
//无参有返回值
interface LambdaB{
	int b();
}

4.3.4 抽象方法是有参无返回值

  • 代码实现
public class LambdaInterFace {	
	public static void main(String[] args) {
		//如果方法有参数可以直接在()中声明参数类型进行定义
		//并且Lambda表达式在一定情况下还可以自动推测参数类
		LambdaC c=(asd)->{
			System.out.println(asd);
		};
		c.c(123456);
	}}
//有参无返回值
interface LambdaC{
	void c(int x);
}

4.3.5 抽象方法是有参有返回值

  • 代码实现
public class LambdaInterFace {	
	public static void main(String[] args) {
		LambdaD d=(int x,String y,double z)->{
			return x+y+z;
		};
		System.out.println(d.d(1, "a", 2.0));
	}
}
//有参有返回值
interface LambdaD{
	String d(int x,String y,double z);
}

4.4 Lambda表达式的省略模式

4.4.1 省略规则

  • 参数类型可以省略。但是有多个参数的情况下,不能只省略一个
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,和return关键字

4.4.2 Lambda简化写法

  • 如果方法没有参数列表()也不能省略
()->{
    for(int i=0;i<10;i++){
        System.out.println(i);
    }
}
  • 如果lambda表达式可以推导出参数类型,可以不再声明类型
(a)->{
	System.out.println(a);
};
  • 如果方法只有一个参数,而且这个参数的类型可以推导出来,也可以连括号都省略
a->{
	System.out.println(a);
};
  • lambda表达式也可以不声明返回类型,lambda表达式的返回类型总是可以由上下文推导得出
(x,y,z)->{
	return x+y+z;
};
  • lambda表达式如果方法体只有一句话,那么{}也可以省略
a->System.out.println(a);

4.5 Lambda表达式的注意事项

  • 使用Lambda必须要有接口,并且要求接口中有且仅有一个抽象方法
  • 必须有上下文环境,才能推导出Lambda对应的接口
    • 根据局部变量的赋值得知Lambda对应的接口

    Runnable r = () -> System.out.println(“Lambda表达式”);

    • 根据调用方法的参数得知Lambda对应的接口

    new Thread(() -> System.out.println(“Lambda表达式”)).start();

4.6 lambda表达式的优缺点

  • 优点:
    1、简洁
    2、非常容易并行计算
    3、可能代表未来的编程趋势

  • 缺点:
    1、如果不用并行计算,lambda表达式的计算速度一般不如for循环快(并行计算有时候也需要预热才会显示出效率优势)
    2、不容易调试
    3、如果其他人也没学过lambda表达式,代码不容易让其他语言的程序员看懂


每日一点点进步
不进则退

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐