类与对象
简介
类:一组相关的属性和行为的集合,是一个抽象的概念。就是对一些具有共性特征,并且行为相似的个体的描述。
对象:该类事物的具体表现形式,具体存在的个体。
成员变量:事物的属性
成员方法:事物的行为
关系:类是对象的抽象,而对象是类的具体实例
区别:类是抽象的,不占用内存,而真正根据类实例化出具体的对象,就需要占用内存空间了。
案例
需求:请使用面向对象的思想描述一下学生
//学生类
class Student{
//属性
String name; //姓名
char sex; //性别
int age; //年龄
//行为
public void learn(){
System.out.println(this.name+"正在学习");
}
public void dine(){
System.out.println(this.name+"正在吃饭");
}
}
成员变量与局部变量
什么是成员变量?类中方法外的变量
class Student{
String name; //成员变量
}
什么是局部变量?代码块,方法定义中或者方法声明的变量(方法参数)
class Student{
public void learn(){
int i= 1; //局部变量
}
}
区别
- 生命周期不同
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
- 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
- 初始化值不同
- 成员变量:有默认值(构造方法对它的值进行初始化)
- 局部变量:没有默认值,必须定义,赋值,然后才能使用
- 在内存中的位置不同
- 成员变量:在堆中
- 局部变量:在栈中
- 在类中的位置不同(上面已经解释)
为什么局部变量存在于栈中而不是堆中?
一个类可以创建 n 个不同的对象,当我们 new 一个对象后,这个对象实体,已经在堆上分配了内存空间。由于类的成员变量在不同的对象中各不相同,都需要自己各自的存储空间,所以类的成员变量会随着对象存储在堆中。类的方法是所有对象通用的,所以创建对象时,方法还未出现,只有声明,方法里面的局部变量也并没有被创建,只有等对象使用方法的时候才会被压入栈。
补充:类变量(静态变量)存在于方法区,引用类型的局部变量声明在栈,存储在堆
访问权限修饰符
访问权限 | 类 | 包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protect | √ | √ | √ | |
default | √ | √ | ||
private | √ |
解释
- public,表示所有其他类都可以访问。·
- protected,当前类或子类可以访问,同时相同包内的其他类也可以访问protected成员;
- default,默认(没有修饰符):表示本包内可以使用
- private,表示的是在本类内可以使用;
构造方法
作用:初始化对象
是Java中的一种特殊的方法,被称为 构造方法
,构造器,构造函数等。
只能在对象创建的时候调用一次,通过这个构造器,来确保每一个对象都被初始化。保证初始化的进行。
构造函数没有参数类型和返回值,它的名称要和类名保持一致,并且构造方法可以有多个。如下示例
class Apple {
int sum;
String color;
public Apple(){}
public Apple(int sum){}
public Apple(String color){}
public Apple(int sum,String color){}
}
在定义完成构造方法后,就能够创建Apple对象了
class Test {
public static void main(String[] args) {
Apple apple1 = new Apple(); //不加任何参数的构造方法被称为默认的构造方法你
Apple apple2 = new Apple(1);
Apple apple3 = new Apple("red");
Apple apple4 = new Apple(2,"color");
}
}
注意:如果类中没有定义任何构造方法,那么JVM会默认添加一个无参构造方法,但是手动定义了任何一个构造方法,JVM就不在提供默认的构造方法,你必须手动指定,否则会出现编译错误。
上图错误原因是:必须提供Apple带有int参数的构造方法,而默认的无参构造方法没有被允许使用。
初始化
Java字节代码:byte[];
Java类在 JVM
的表现形式:Class类的对象;
Java源代码到Class类对象的过程
Java源代码编译成class字节码的过程如下
class字节码解释成class类对象
- 加载:把class字节码byte[]转换成JVM中的java.lang.Class类的对象;
- 链接:Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。
- 初始化:主要是执行静态代码块和初始化静态域;
类的加载
作用:把class字节码转换成 JVM
中的java.lang.Class类的对象;
- 通过一个类的全限定名获取描述此类的二进制字节流;
- 将这个字节流所代表的静态存储结构保存为方法区的运行时数据结构;
- 在
java堆
中生成一个代表这个类的java.lang.Class
对象,作为访问方法区的入口;
类加载器分类
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过
-Xbootclasspath
参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar
)的类; - 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过
java.ext.dirs
系统变量指定路径中的类库; - 应用程序类加载器(Application ClassLoader):负责加载用户路径(
classpath
)上的类库;
双亲委派模型
当一个类加载器收到类加载任务,优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
好处:比如位于 rt.jar
包中的类 java.lang.Object
,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类。
特征:
- 层次组织结构:每个类加载器都有一个父类加载器,形成tree结构;
- 代理模式:一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成;
类的链接
Java类的链接指的是将Java类的二进制代码合并到 JVM
的运行状态之中的过程。
步骤
-
验证:确保Java类的二进制表示在结构上是完全正确的,主要包括格式验证、元数据验证、字节码验证和符号引用验证;
-
准备:创建Java类中的静态域,并将这些域的值设为默认值;
在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。private static int i = 100;
准备阶段完成后,
i
值为0,而不是100。在初始化阶段,才会把100赋值给i
。但是有个特殊情况private static final int val = 100;
在编译阶段会为
val
生成ConstantValue
属性,在准备阶段虚拟机会根据ConstantValue
属性将val
赋值为100。 -
解析:解析阶段是将常量池中的符号引用替换为直接引用的过程,解析过程可能导致其他的Java类被加载;
-
符号引用使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中。
-
直接引用可以是直接指向目标的指针、相对偏移量或则能间接定位到目标的句柄。
-
类的初始化
初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。
初始化阶段是执行类构造器clinit方法的过程,clinit
方法由类变量的赋值动作和静态语句块按照在源文件出现的顺序合并而成,该合并操作由编译器完成。
clinit方法
- 方法
clinit
对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器不会生成; - 方法
clinit
方法与实例构造器不同,不需要显式的调用父类的方法,虚拟机会保证父类的优先执行; - 为了防止多次执行
clinit
,虚拟机会确保clinit
方法在多线程环境下被正确的加锁同步执行,如果有多个线程同时初始化一个类,那么只有一个线程能够执行clinit
方法,其它线程进行阻塞等待,直到clinit
执行完成。
注意:执行接口的clinit
方法不需要先执行父接口的clinit
,只有使用父接口中定义的变量时,才会执行。
触发类初始化场景
虚拟机中严格规定了有且只有5种情况必须对类进行初始化。
- 执行new、
getstatic
、putstatic
和invokestatic
指令; - 使用reflect对类进行反射调用;
- 初始化一个类的时候,父类还没有初始化,会事先初始化父类;
- 启动虚拟机时,需要初始化包含main方法的类;
- 在
JDK1.7
中,如果java.lang.invoke.MethodHandler
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄对应的类没有进行初始化;
不会触发类初始化场景
-
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
-
定义对象数组,不会触发该类的初始化。
-
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
-
通过类名获取Class对象,不会触发类的初始化。
-
通过
Class.forName
加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 -
通过
ClassLoader
默认的loadClass
方法,也不会触发初始化动作;