目录
导读:上节我们拜访了对象村,我们不禁产生很多疑问。怎么做到遍地都是对象的呢?对象村又是如何出现的?下面就由我来进行揭秘。
前言:
对象有生有死。你必须为对象的生命循环周期负责。你决定着对象何时创建,如何创建,也决定着何时销毁对象。其实你不是真的自消灭对象,只是声明要放弃它而巳。一旦它被放弃了,冷血无情的垃圾收集器(GC)就会将它蒸发掉、回收对象所占用的内存空间。如果你要编写Java程序,就必须创建对象。早晚你得将它们释放掉,不然就会出现内存不足的问题。这一章会讨论对象如何创建、存在于何处以及如何让保存和抛弃更有效率。这代表我们会述及堆、栈、范围、构造器、超级构造器、空引用等。注意:内容含有死亡成份,12岁以下儿童需由家长陪同观赏
1.对象村的秘密(对象在内存的实现)
1.1 内存的好兄弟“堆”与“栈”
在Java中,堆(heap)和栈(stack)是两种不同的内存区域,用于存储程序运行时的不同类型的数据和对象。
1. 栈(Stack):
- 栈是一种线性数据结构,遵循"先进后出"(Last-In-First-Out, LIFO)的原则。栈中的数据主要包括基本数据类型的变量和对象的引用。
- 栈内存用于存储局部变量、方法参数、方法调用以及方法返回时的临时数据。当一个方法被调用时,会在栈上创建一个称为"栈帧"的区域,用于存储该方法的局部变量和方法调用信息。栈帧在方法执行完毕后被销毁。
- 栈的大小在程序运行时是固定的,由Java虚拟机(JVM)进行分配和管理。如果栈的内存空间不足,会抛出 StackOverflowError 错误。
2. 堆(Heap):
- 堆是用于存储动态分配的对象的一块内存区域。堆是Java中最大的一块内存区域,也是最复杂的一块内存区域。
- 所有的类实例(对象)和数组对象都存储在堆上。
- 堆的大小在程序运行时是可变的。Java虚拟机根据堆的使用情况动态地分配和回收内存。
- 堆的内存分配由垃圾回收器(Garbage Collector)负责,它自动扫描并回收不再使用的对象,使可用内存得到重复利用。
通常情况下,栈的内存空间比较小,用于存储局部变量等临时数据。而堆的内存空间比较大,用于存储动态创建的对象。在Java中,基本类型的变量(如int、boolean等)和对象的引用被存储在栈上,而对象的实际数据(包括成员变量)则存储在堆上。
了解栈和堆的原理对于理解Java的内存管理和内存分配非常重要,它们的不同特点也直接影响到程序的性能和内存使用情况。
如图所示:
实例变量:
- 实例变量是定义在类中,方法外的变量。
- 每个实例变量属于该类的每个实例对象,每个对象都有自己的实例变量副本。
- 实例变量在对象创建时被初始化,并在对象的整个生命周期内都存在。
- 实例变量可以被类中的任何方法、构造方法或块使用。
- 访问实例变量时,必须通过对象引用来访问。
示例代码如下:
public class MyClass {
// 实例变量
private String name; // 字符串类型的实例变量
public void setName(String name) {
this.name = name;
}
public void printName() {
System.out.println("Name: " + name);
}
}
局部变量:
- 局部变量是在方法、构造方法或块中定义的变量,只在其所在的作用域内可见。
- 局部变量仅在其所在的方法或块执行期间存在,并且在方法或块的执行结束后被销毁。
- 在方法中定义的局部变量在每次方法调用时都会重新创建,每个方法调用都会为其分配内存空间。
- 访问局部变量时,只能在其声明的作用域内访问。
示例代码如下:
public class MyClass {
public void printNumber(int number) {
// 局部变量
String message = "The number is: "; // 字符串类型的局部变量
System.out.println(message + number);
}
}
1.1.1方法喜欢玩泰山压顶
当你调用一个方法时,该方法会放在调用栈的栈顶,实际被堆上栈的是堆栈块。它带有方法的状态,包括执行到哪一行程序以及所有的局部变量的值。
栈顶上的方法是目前正在执行的方法,方法会一直待在这里直到执行完毕,如果foo() 方法调用bar()方法则bar()方法会放在foo()方法的上面。
1.1.2 stack的实现
下边有三个方法,第一个方法在执行过程中会调用第二个方法,第二个会调用第三个。每个方法都在内容中声明一个局部变量,而go()方法还有声明一个参数(这代表go()方法有两个局部变量)。
public void doStuff(){
boolean b = true;
go(4);
}
public void go(int x){
int z = x + 24;
crazy();
//假设还有很多代码
}
public void crazy(){
char = 'a';
}
如图
1.2栈上的对象引用
1.2.1有关对象局部变量
要记得非primitive的变量只是保存对象的引用而已,而不是对象本身。你已经知道对象存在于何处——堆。不论对象是否声明或创建,如果局部变量是个对该对象的引用,只有变量本身会放在栈上
对象本身只会存在于堆上。
primitive:在Java中,primitive(原始类型)是指Java语言中的基本数据类型。Java的原始类型包括boolean、char、byte、short、int、long、float和double。这些原始类型是Java语言的基本构建块,用于存储简单的数据值。
public class stackRef{
public void foof(){
barf();
}
public void barf(){
Duck d = new Duck(24);
}
}
要点:
我们关心栈 与堆这两种内存空间。
实例变量是声明在类中方法之外的地方。
局部变量声明在方法或方法的参数上。
所有局部变量都存在于栈上相对应的堆栈块中。
对象引用变量 与primitive主数据类型变量都是放在栈上。不管是实例变量或局部变量, 对象本身都会在堆上。
1.2.2 如果局部变量生存在栈上,那么实例变量呢?
当你要新建一个CellPhone()时,Java必须在堆上帮CellPhone找一个位置。这会需要多少空间呢?足以存放该对象所有实例变量的空间。没错,实例变量存在于对象所属的堆空间上。
记住对象的实例变量的值是存放于该对象中。如果实例变量全都是primitive主数据类型的,则Java会依据primitive主数据类型的大小为该实例变量留下空间。int需要32位,long需要64位,依此类推。Java并不在乎私有变量的值,不管是32或32,000,000的int 都会占用32位
1.2.3创建对象的奇迹
三个步骤的回顾:声明、创建、赋值
1.3对象的消亡
1.局部变量只会存活在声明该变量的方法中
局部变量只会存活在声明该变量的方法中
public void read(){
int s = 42;
//'s'只能用在此方法中
//当方法结束时
// s会完全消失
变量s只能用在read()方法中。换句话说,此变量的范围只会在所属方法的范围内。其余的程序代码完全见不到s。
2.实例变量的寿命与对象相同。 如果对象还活着,则实例变量也会是活的。
public class Life {
int size;
public void setSize (int s) {
size = s;
//'s'会在方法结束时
//消失,但size在类中
//到处都可用
此时s变量(这次是方法的参数)的范国同样也只限制在所属的setSize()这方法中。
1.3.1"life"与“scope”的差别
Lite
只要变量的堆栈块还存在于堆栈上,局部变量就算活着。也就是说,活到方法执行完毕为止。
Scope
局部变量的范围只限于声明它的方法之内.当此方法调用别的方法时,该变量还活着,但不在目前的范围内。执行其他方法完毕返回时,范围也就跟着回来。
public void doStuff(){
boolean b=true;
go(4) ;
}
public void go(int x){
int z = x + 24;
crazy();
//这里有更多的代码
}
public void crazy(){
char c = 'a';
}
当局部变量活着的时候,它的状态会被保存。只要doStuff()还在堆栈上,b变量就会保持它的值。但b变量只能在doStuffO待在栈顶时才能使用。也就是说局部变量只能在声明它的方法在执行中才能被使用
1.3.2杀死对象的三位杀手
有3种方法可以释放(杀死)对象的引用:
当最后一个引用消失时,对象就会变成可回收的。
1 引用永久性的离开它的范围。void go() {
Life z=new Life() ;}
2 引用被赋值到其他的对象上.
Life z = new Life() ;
z = new Life();
3 直接将引用设定为null。
Life z = new Life();
z = null;
对象杀手1号
引用永久性的离开他的范围
public class StackRef{
public void foof(){
barf();
}
public void barf(){
Duck d = new Duck;
}
}
对象杀手三号
引用被赋值到其他对象
public class ReRef{
Duck d = new Duck();
public void go(){
d = new Duck;
}
}
如下图:
对象杀手三号
直接将引用设定为null
public class ReRef {
Duck d = new Duck();
public void go() {
d = null;
}
}
那null又是什么呢?
null的真相
当你把引用设为null时,你就等于是抹除遥控器的功能。换句话说,你会拿到一个没有电视的遥控器。null是代表“空” 的字节组合(实际上是什么只有Java虚拟机才会知道)。如果你真地按下这种逼控器上的按钮,什么事情也不会发生。但在Java.上,你是不能对null引用按钮的。因为Java虚拟机会知道(这是运行期,不是编译时的错误)你期待喵喵叫,但是却没有Cat可以执行!
对null引用使用圆点运算符会在执行期遇到NullPointerException这样的错误。后而会有讨论异常的章节。
2.和事佬在对象村的处境(static成员)
2.1 和事佬的处事方式(static的特性)
通过以下三个代码对学生进行描述,如下所示:
public class Student{
// ...
public static void main(String[] args) {
Student s1 = new Student("Li leilei", "男", 18, 3.8);
Student s2 = new Student("Han MeiMei", "女", 19, 4.0);
Student s3 = new Student("Jim", "男", 18, 2.6);
}
}
得到以下三个对象的特征
2.2 static修饰成员变量
static修饰的成员变量,称为静态成员变量,静态成员变量最大的特性:不属于某个具体的对象,是所有对象所共享的。
【静态成员变量特性】1. 不属于某个具体的对象,是类的属性,所有对象共享的,不存储在某个对象的空间中2. 既可以通过对象访问,也可以通过类名访问,但一般更推荐使用类名访问3. 类变量存储在方法区当中4. 生命周期伴随类的一生(即:随类的加载而创建,随类的卸载而销毁)
public class Student{
public String name;
public String gender;
public int age;
public double score;
public static String classRoom = "Bit306";
// ...
public static void main(String[] args) {
// 静态成员变量可以直接通过类名访问
System.out.println(Student.classRoom);
Student s1 = new Student("Li leilei", "男", 18, 3.8);
Student s2 = new Student("Han MeiMei", "女", 19, 4.0);
Student s3 = new Student("Jim", "男", 18, 2.6);
// 也可以通过对象访问:但是classRoom是三个对象共享的
System.out.println(s1.classRoom);
System.out.println(s2.classRoom);
System.out.println(s3.classRoom);
}
}
静态成员变量可以直接通过类名访问
2.3 static修饰成员方法
被private修饰的:只有自己知道,其他人都不知道(封装会进行详细讲解)
一般类中的数据成员都设置为private,而成员方法设置为public,那设置之后,Student类中classRoom属性如何在类外访问呢?
public class Student{
private String name;
private String gender;
private int age;
private double score;
private static String classRoom = "Bit306";
// ...
}
public class TestStudent {
public static void main(String[] args) {
System.out.println(Student.classRoom);
}
}
编译失败:
Error:(10, 35) java: classRoom 在 extend01.Student 中是 private 访问控制
那static属性应该如何访问呢?
Java中,被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的。静态成员一般是通过静态方法来访问的。
public class Student{
// ...
private static String classRoom = "Bit306";
// ...
public static String getClassRoom(){
return classRoom;
}
}
public class TestStudent {
public static void main(String[] args) {
System.out.println(Student.getClassRoom());
}
}
输出:Bit306
1. 不属于某个具体的对象,是类方法2. 可以通过对象调用,也可以通过类名.静态方法名(...)方式调用,更推荐使用后者3. 不能在静态方法中(直接)访问任何非静态成员变量
无法从静态上下文引用非静态变量name
4. 静态方法中不能调用任何非静态方法,因为非静态方法有this参数,在静态方法中调用时候无法传递this引用
5. 静态方法无法重写,不能用来实现多态(此处大家暂时不用管,后序多态位置详细讲解)
2.4 static成员变量初始化
注意:静态成员变量一般不会放在构造方法中来初始化,构造方法中初始化的是与对象相关的实例属性
静态成员变量的初始化分为两种:就地初始化 和 静态代码块初始化。
1. 就地初始化
就地初始化指的是:在定义时直接给出初始值
public class Student{
private String name;
private String gender;
private int age;
private double score;
private static String classRoom = "Bit306";
// ...
}
2. 静态代码块初始化
那什么是代码块呢?继续往后看 :) ~~~
3.代码块
3.1代码块概念以及分类
使用 {} 定义的一段代码称为代码块。根据代码块定义的位置以及关键字,又可分为以下四种:
普通代码块构造块静态块同步代码块(后续讲解多线程部分再谈)
3.2 普通代码块
普通代码块:定义在方法中的代码块
public class Main{
public static void main(String[] args) {
{ //直接使用{}定义,普通方法块
int x = 10 ;
System.out.println("x1 = " +x);
}
int x = 100 ;
System.out.println("x2 = " +x);//外面用不了内部的x
}
}
// 执行结果
x1 = 10
x2 = 100
这种用法较少见
3.3 构造代码块
构造块:定义在类中的代码块(不加修饰符)。也叫:实例代码块。构造代码块一般用于初始化实例成员变量。
public class Student{
//实例成员变量
private String name;
private String gender;
private int age;
private double score;
public Student() {
System.out.println("I am Student init()!");
}
//实例代码块
{
this.name = "bit";
this.age = 12;
this.sex = "man";
System.out.println("I am instance init()!");
}
public void show(){
System.out.println("name: "+name+" age: "+age+" sex: "+sex);
}
}
public class Main {
public static void main(String[] args) {
Student stu = new Student();
stu.show();
}
}
// 运行结果
I am instance init()!
I am Student init()!
name: bit age: 12 sex: man
构造代码块在方法的外面,类的里面
实例代码块比构造方法先执行(和放在哪里没有关系)
3.4 静态代码块
使用static定义的代码块称为静态代码块。一般用于初始化静态成员变量。
public class Student{
private String name;
private String gender;
private int age;
private double score;
private static String classRoom;
//实例代码块
{
this.name = "bit";
this.age = 12;
this.gender = "man";
System.out.println("实例代码块");
}
// 静态代码块
static {
classRoom = "bit306";
System.out.println("静态代码块");
}
public Student(){
System.out.println("构造代码块");
}
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
}
}
注意事项
静态代码块不管生成多少个对象,其只会执行一次静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的如果一个类中包含多个静态代码块,在编译代码时,编译器会按照定义的先后次序依次执行(合并)实例代码块只有在创建对象时才会执行
实例化两个对象:
当没有实例化对象时:
结尾:
以上就是全部内容,送一句话该大家:
让我们如大自然般悠然自在地生活一天吧,
别因为有坚果外壳或者蚊子翅膀落在铁轨上而翻了车。
让我们该起床时就赶紧起床,
该休息时就安心休息,
保持安宁而没有烦扰的心态;
身边的人要来就让他来,要去就让他去,
让钟声回荡,让孩子哭喊—
下定决心好好地过一天。