Java知识点小结2:对象的内存管理

news2024/9/20 5:12:31

文章目录

  • 局部变量和成员变量
    • 局部变量
    • 成员变量
  • 实例变量和类变量的初始化
    • 实例变量的初始化
    • 类变量的初始化
  • 构造器
  • 父类访问子类的实例变量和方法
    • 考一考
  • super
  • 父子类的类变量
  • final修饰符
    • final修饰变量
      • literal字符串
    • final修饰方法
    • 内部类中的局部变量

局部变量和成员变量

Java的变量可分为局部变量和成员变量。

局部变量存储在栈内存中,而成员变量是对象的一部分,存储在堆内存中。

局部变量

局部变量可分为:

  • 形参:
    public void f1(int i) {
        if (i > 0) {
            ......
        }
    }

本例中的 i 是形参,它是局部变量。

  • 方法内的局部变量:
    public void f2() {
        int j = 0;
        ......
    }

本例中的 j 是方法内的局部变量。

注意:方法内的局部变量在使用前必须显式初始化。

        // 在定义变量时初始化
        int i = 0; // OK
        
        // 在定义变量时没有初始化,但在使用前确保其有值
        int j;
        j = 0; // OK
        
        // 变量没有初始化,就直接使用,编译报错
        int k;
        System.out.println(k); // 编译错误

注:成员变量无需初始化,会自动赋初值(0、false、null等)。

  • 代码块内的局部变量:
public class MyClass {
    {
        int i = 0;
        ......
    }
    ......
}

注意:代码块内的局部变量和方法内的局部变量类似,在使用前必须显式初始化。

成员变量

成员变量又分为实例变量和类变量。

  • 实例变量:属于实例,每个实例有各自的成员变量
  • 类变量:属于类,整个类只有一个成员变量。类变量用 static 修饰
public class MyClass {
    private int i; // 实例变量
    private static int j; // 类变量
    ......
}

注: static 可以修饰类里面定义的成员,包括成员变量、方法、内部类、初始化代码块等。 static 不能修饰类、局部变量、局部内部类等。

Java变量必须“先定义,后使用”。

  • 例1:实例变量的非法前向引用:
    private int i = j + 1; // 编译错误
    private int j = 0;
  • 例2:类变量的非法前向引用:
    private static int i = j + 1; // 编译错误
    private static int j = 0;
  • 例3:实例变量可以前向引用类变量
    private int i = j + 1; // OK
    private static int j = 0;

例3没有问题,这是因为类变量是随着类初始化,而实例变量是随着实例初始化,所以类变量总是在实例变量初始化之前就已经初始化好了。

注:获取Class实例的几种方法(假定有 Person 类和其 person 实例):

  • Class.forName("Person") // 注意要用类名全称
  • Person.class
  • person.getClass()

实例变量和类变量的初始化

实例变量的初始化

实例变量可以在以下几处做初始化操作:

  • 1:定义实例变量时
  • 2:初始化块(非静态)
  • 3:构造器
public class Person {
    // 1:定义实例变量时初始化
    private String name = "Tom";

    // 2:初始化块
    {
        name = "Jerry";
    }

    public String getName() {
        return name;
    }

    // 3:构造器
    public Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        System.out.println(new Person().getName()); // Jerry
        System.out.println(new Person("LiSi").getName()); // Lisi
    }
}

1(定义时初始化)和2(初始化块)比3(构造器)要更早执行。事实上,编译器会把1和2的代码放在构造器里,并且是放在最上面。

1和2的执行顺序,是按照其代码的自然顺序。

本例中1在前2在后,所以先执行1,再执行2。

注意:如果把1和2的顺序交换一下,会不会报错呢?

    ......
    // 初始化块
    {
        name = "Jerry";
    }

    // 定义实例变量时初始化
    private String name = "Tom";
    ......

前面提到,“初始化块”和“定义时初始化”,哪个在前就先执行哪个,那么,修改之后,在执行初始化块时,会不会报错说 name 未定义呢?

答案是没有问题。这是因为创建对象时,会先把实例变量创建出来(内存角度),然后再初始化(赋值角度)。

总结:创建对象实例时,会先把实例变量创建出来(分配内存),然后再初始化其值。其中初始化块和定义时初始化,会按其本身顺序,放到构造器的最前面来执行。

类变量的初始化

类变量的初始化类似于实例变量的初始化(只不过没有构造器):

  • 定义类变量时初始化
  • 初始化块(静态)

这二者,哪个在前就先执行哪个。

public class Book {
    static {
        price = 30.0;
    }

    static double price = 20.0;

    public static void main(String[] args) {
        System.out.println(Book.price); // 20.0
    }
}

构造器

类一定有构造器(如果没有,系统会添加一个隐式的无参构造器)。

父类构造器一定会执行(从Object开始,往下一层一层的执行)。

父类构造器一定在当前类构造器的最前面执行

可分为以下几种情况:

  • 第一行代码为 super() ,显式调用父类的指定构造器
  • 第一行代码为 this() ,显式调用当前类的指定构造器(在指定构造器里,还是会先执行父类构造器)
  • 第一行代码不调用 super()this() ,则在最前面隐式调用父类的无参构造器(即 super()

注意: super()this() 只能在构造器中使用,并且只能用在第一行代码。二者最多只能调用一次,且不能同时使用。

考一考:前面提到,如果构造器里没有显式调用 super() ,则会在构造器最前面隐式调用父类的无参构造器 super() ,假如父类只有有参的构造器,会怎么样呢?

答案是会编译报错。看下面的例子:

class A {
    public A(String name) {
        System.out.println(name);
    }
}

class B extends A {
    
}

由于子类B没有构造器,系统会添加一个默认的无参构造器。在此构造器中,会调用父类A的无参构造器,而A没有无参构造器,所以编译报错。

父类访问子类的实例变量和方法

先看一下 this ,它指向的是对象本身。

前面提到,在创建子类对象时,一定会调用父类的构造器,如果父类构造器里面有 this,这个this指向的是谁呢?答案是子类对象。

class A {
    public A() {
        System.out.println("A::" + this.getClass().getName());
    }
}

class B extends A {
    public B() {
        System.out.println("B::" + this.getClass().getName());
    }
}

public class Test {
    public static void main(String[] args) {
        new B();
    }
}

运行结果如下:

A::package1.B
B::package1.B

可见,父类构造器里的this,指向的是子类对象,这是因为new的是子类对象,父类构造器是被子类对象的构造器所调用的。

下面举一些例子。

  • 例1:
class A {
    private String str1 = "hi";

    public void f() {
        System.out.println(this.str1);
    }
}

class B extends A {
    private String str2 = "hello";

    public void f() {
        System.out.println(this.str2);
    }
}

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        a1.f(); // hi

        A a2 = new B();
        a2.f(); // hello
    }
}

这个例子很典型,没有任何问题。

  • 例2:改为在父类构造器里调用 f() 方法:
class A {
    private String str1 = "hi";

    public void f() {
        System.out.println(this.str1);
    }

    public A() {
        this.f();
    }
}

class B extends A {
    private String str2 = "hello";

    public void f() {
        System.out.println(this.str2);
    }
}

public class Test {
    public static void main(String[] args) {
        new B(); // null
    }
}

奇怪的是,输出结果变成null了。

这是因为,在创建B对象时,首先调用父类的构造器,父类构造器里调用了 this.f() 方法。前面提到,this实际指向的是B对象,且 f() 方法被子类override了,所以调用的是子类的 f() 方法。而 str2 此时还没有初始化(先分配内存,再初始化,初始化是按从父类到子类的顺序操作的),所以是null值。

这个例子告诉我们,最好不要在构造器里调用实例方法。

  • 例3:子类和父类同名的成员变量
class A {
    private String str1 = "hi";

    public void f() {
        System.out.println(this.str1);
    }

    public A() {
        this.f();
    }
}

class B extends A {
    private String str1 = "hello";
}

public class Test {
    public static void main(String[] args) {
        new B(); // hi
    }
}

本例中,创建B对象时,先调用父类A的构造器,在构造器里调用 this.f() 方法,由于子类没有override该方法,所以调用的是父类的 f() 方法。问题在于,这里的 str1 是父类的还是子类的?

从实际运行结果可见,这里的 str1 是父类的。这是因为实例变量不存在override之说,this虽然指向子类,但编译期是父类类型,所以是父类的 str1

换句话说,在父类和子类里,实例变量即使同名,也是两个截然不同的变量,没有什么关联。父类不知道子类,更不可能知道子类的成员变量,所以在父类方法里,使用的一定是父类的成员变量。

想象一下,如果子类有成员变量 str2 ,而在父类的 f() 方法里使用 str2 ,显然会编译报错。

所以,获取“在父子类里同名的成员变量”时,是根据编译期的类型而定的。如果编译类型是子类,则会把父类的同名成员变量给“隐藏”掉。

这个例子告诉我们,父类成员变量和子类成员变量最好不要同名,徒增混淆。

总结:

  • this指向是对象本身。构造器(包括父类和子类构造器)里的this,指向的是实际创建的对象。
  • 通过this指针访问对象的实例变量时,由声明该变量的类型决定(编译期)
  • 通过this指针访问对象的实例方法时,由实际的对象来决定(运行期)

考一考

下面代码的输出结果是什么?

class A {
    String name = "Hi";

    public void display() {
        System.out.println(this.name);
    }
}

class B extends A {
    String name = "Hello";

    @Override
    public void display() {
        System.out.println(this.name);
    }
}

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        System.out.println(a1.name);
        a1.display();

        B b1 = new B();
        System.out.println(b1.name);
        b1.display();

        A a2 = new B();
        System.out.println(a2.name);
        a2.display();

        A a3 = b1;
        System.out.println(a3.name);
        a3.display();
    }
}

答案为:

Hi
Hi

Hello
Hello

Hi
Hello

Hi
Hello
  • a1:编译类型为A,实际类型也为A,所以name和display()都是A的
  • b1:编译类型为B,实际类型也为B,所以name和display()都是B的
  • a2:编译类型为A,实际类型为B,所以name是A的,display()是B的
  • a3:编译类型为A,实际类型为B,所以name是A的,display()是B的

本例中,为了混淆,故意令A和B的成员变量名都叫 name 。虽然a3和b1实际指向同一对象,但 a3.nameb1.name 是不同的变量(编译类型是子类时,子类的name把父类的name隐藏掉了)。如果二者叫不同的名字,代码就会清晰一些。

方法则相对简单一些:不管编译期类型是什么,只看实际对象是什么,就调用它的方法。

super

如果在子类中显式指定 super.xxxsuper.xxx() ,则会访问其父类的成员变量或方法。

super仍然是指向实际对象的,其编译类型是父类。

使用super,就可以访问被子类隐藏掉的同名父类成员变量,或者指定父类的方法。

父子类的类变量

  • 可以通过类名来访问类变量
  • 也可以通过super来访问父类的类变量

final修饰符

  • final修饰变量:变量赋初值后,不能重新赋值
  • final修饰方法:不能被子类override
  • final修饰类:不能被继承

final修饰变量

  • final修饰实例变量
    • 定义变量时指定初始值
    • 在初始化块(非静态)中指定初始值
    • 在构造器中指定初始值
  • final修饰类变量
    • 定义变量时指定初始值
    • 在初始化块(静态)中指定初始值
  • final修饰局部变量
    • 定义变量时指定初始值

注意:以实例变量为例,虽然可以在多个地方指定初始值,但是对一个变量,只能在一处指定初始值,不能在多处赋值。

用final修饰的变量,如果定义变量时指定初始值,而且这个初始值在编译期能确定下来,那么这个变量将不再是一个变量,而是会被当作“宏变量”,也就是所有出现该变量的地方,直接用其值来替换。比如:

    final int i = 123;
    final int j = 1 + 3;
    final String str = "abc" + "def";

literal字符串

Java会将literal字符串缓存在字符串池中。

        String str1 = "hello";
        String str2 = "hello";
        System.out.println(str1 == str2); // true

本例中,str1和str2指向同一个对象。

        String str1 = "hello";
        String str2 = "he" + "llo";
        System.out.println(str1 == str2); // true

本例中,str1和str2指向同一个对象,因为str2的值在编译期可确定。

final修饰方法

final修饰的方法不能被override。

class A {
    final void f() {}
}

class B extends A {
    void f() {} // 编译错误
}

但是下面这个例子是OK的:

class A {
    private final void f() {}
}

class B extends A {
    void f() {} // OK
}

这是因为父类的private方法对子类是不可见的,所以子类可以定义同名方法(不是override,不能添加 @Override 注解)。

不过,这里的final没什么意义,因为private方法本来就无法override。

内部类中的局部变量

如果在内部类中使用局部变量,则该变量默认有final修饰。

比如:

interface A {
    void f();
}

public class Test {
    public static void main(String[] args) {
        int i = 123;
        A a = new A() {
            @Override
            public void f() {
                System.out.println(i);
            }
        };
        a.f(); // 123
    }
}

本例中,在匿名内部类中使用了外部的局部变量i。

如果试图修改变量的值,则编译错误。比如:

interface A {
    void f();
}

public class Test {
    public static void main(String[] args) {
        int i = 123;
        new A() {
            @Override
            public void f() {
                i++; // 编译错误
            }
        };
    }
}

编译错误如下:

Variable 'i' is accessed from within inner class, needs to be final or effectively final

同理,如果在外部类中试图修改i的值,也会编译报错:

interface A {
    void f();
}

public class Test {
    public static void main(String[] args) {
        int i = 123;
        new A() {
            @Override
            public void f() {
                System.out.println(i); // 编译错误
            }
        };
        i++;
    }
}

Lambda也有同样的问题:

        int i = 123;

        Logger.getLogger("MyLogger").info(() -> "i = " + i); // 编译错误

        i++;

看来想在log里打印一下某个变量的值,还不太容易呢。

一个比较土的解决办法是,为了记log,再定义一个临时变量:

        int i = 123;
        int j = i;

        Logger.getLogger("MyLogger").info(() -> "i = " + j);
        
        i++;

至于为什么有此限制,是因为局部变量的作用域本来是在方法里的,但是内部类或Lambda可能产生隐式的闭包(Closure),闭包使得局部变量脱离它所在的方法而继续存在,也就是扩大了局部变量的作用域,将会引起混乱。所以Java要求内部类和Lambda所访问的局部变量必须是final的(如果不写,Java会自动加上)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2147968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于51单片机的温度电流电压检测系统(压力、电压、温度、电流、LCD1602)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机,通过DS18B20检测温度,滑动变阻器连接数模转换器模拟电流、电压,通过LCD1602显示,程序里设置温度阈值为40,电流阈值为60&am…

使用Python免费将pdf转为docx

刚刚想将pdf转换为docx文档时,居然要收费 还好我学过编程,这不得露两手 将pdf 转换为 docx 文档 的操作步骤 我这里使用的是Python语言 (1)在终端上安装 pdf2docx 是一个 Python 库,它可以将 PDF 文件转换为 Word (…

CS61C 2020计算机组成原理Lab03

Exercise 1: Familiarizing yourself with Venus .data .word 2, 4, 6, 8 n: .word 9.text main: # add t0, x0, x0# addi 是 "add immediate"(立即数加法)的缩写,表示这是一个加法指令,其中一个加数是一个立即数&am…

try语句块和异常处理

throw直接报错 int main() {if (true) throw std::runtime_error("Error!"); } runtime_error(或者其他错误类型)是一个类, 必须初始化, 需使用string或者c风格字符串初始化. throw放try里, catch会接住throw的error 大概就是[catch]-->{[throw]-->[try]}的关…

数据结构——二叉树堆的专题

1.堆的概念及结构 如果有一个关键码的集合K {K0 &#xff0c;K1 &#xff0c;K2 &#xff0c;K3…&#xff0c;K(N-1) }&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中&#xff0c;并满足&#xff1a;Ki < K2*i1且 Ki<K2*i2 ) i 0&#…

Java语言程序设计基础篇_编程练习题**18.30 (找出单词)

题目&#xff1a;**18.30 (找出单词) 编写一个程序&#xff0c;递归地找出某个目录下的所有文件中某个单词出现的次数。从命令行如下传递参数&#xff1a; java Exercise18_30 dirName word 习题思路 &#xff08;读取路径方法&#xff09;和18.28题差不多&#xff0c;把找…

电子元件制造5G智能工厂物联数字孪生平台,推进制造业数字化转型

5G智能工厂与物联数字孪生平台的融合应用&#xff0c;不仅为电容器制造业注入了新的活力&#xff0c;更为整个制造业的数字化转型树立了新的标杆。电子元件制造过程中&#xff0c;数字孪生平台通过实时监测生产线的各个环节&#xff0c;实现了生产流程的可视化监控。管理人员可…

vue中的slot插槽,彻底搞懂及使用

1、使用slot站位&#xff0c;不传内容&#xff0c;显示默认值 //父组件 import SlotChild from ./projectConstruction-child/SlotChild.vue <div><SlotChild></SlotChild> </div>//子组件 <template><div>下面是插槽内容</div><…

【爆炸】BB机,BP机,寻呼系统基础知识,物理层讲解

本页介绍寻呼系统基础知识。其中提到了寻呼机使用的数字协议并描述了数字寻呼接收器。 寻呼是一种单向通信系统。寻呼系统向携带小型电池供电设备&#xff08;称为寻呼机&#xff09;的个人广播信号或消息。这是与员工和/或客户沟通的非常重要的方式。让我们看看寻呼系统的工作…

新发布的OpenAI o1生成式AI模型在强化学习方面迈出了重要的一步

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Xv6驱动(四):CLINT

阅读材料 Xv6代码&#xff1a;memlayout.h、start.c、kernelvec.S教材5.4节 CLINT内存映射 实际上&#xff0c;CLINT还包括若干个MSIP寄存器&#xff0c;用来触发软件中断&#xff0c;但是在Xv6中不考虑软件中断&#xff0c;因此这些寄存器也不用考虑 // core local interr…

python 运行其他命令行工具,实时打印输出内容

起因&#xff0c; 目的: python 运行一个命令&#xff0c;最简洁的写法是: import os # 转换视频格式。 cmd "ffmpeg -i a1.ts -c copy a1.mp4"os.system(cmd)问题&#xff1a; 如果上面的视频比较大&#xff0c;需要运行很长时间&#xff0c;那么感觉就像是卡住…

C# 访问Access存取图片

图片存入ole字段&#xff0c;看有的代码是获取图片的字节数组转换为base64字符串&#xff0c;存入数据库&#xff1b;显示图片是把base64字符串转换为字节数组再显示&#xff1b;直接存字节数组可能还好一点&#xff1b; 插入的时候用带参数的sql写法比较好&#xff1b;用拼接…

InternVL 微调实践闯关任务

基础任务 follow 教学文档和视频使用QLoRA进行微调模型&#xff0c;复现微调效果&#xff0c;并能成功讲出梗图. 尝试使用LoRA&#xff0c;或调整xtuner的config&#xff0c;如LoRA rank&#xff0c;学习率。看模型Loss会如何变化&#xff0c;并记录调整后效果(选做&#xff…

十、数字人IP应用方案

1、背景 在当今的数字时代,随着AI技术的突飞猛进,数字人AI已经从概念走向应用,成为知识内容创作领域一股不可忽视的力量。它的出现,在很大程度上极大地提高了内容的生产效率,大有替代知识IP,成为内容IP终结者的趋势。 数字人IP,从形象到声音,与知识博主真人的相似度可…

初中生物--7.生物圈中的绿色植物(二)

绿色植物与生物圈的水循环 1.植物对水分的吸收和运输 1.植物主要通过根吸收水分。根吸收水分的主要部位是根尖的成熟区。 2.外界溶液浓度<根毛细胞溶液浓度→细胞吸水&#xff1b; 1.在这种情况下&#xff0c;根毛细胞内的溶液浓度高于外界溶液&#xff0c;因此细胞内的…

蓝星多面体foc旋钮键盘复刻问题详解

介绍&#xff1a; 本教程是针对立创开源项目 承载我所有幻想的键盘 - 立创开源硬件平台 作者是 蓝星多面体 这里我总结一下我复刻过程中的一些问题 一 <<编译环境怎么搭建&#xff1f;>> 第一步 安装vscode 下载vscode &#xff08;可以在各大应用平台…

R语言中的shiny框架

R语言中的shiny框架 Shiny 的基本概念基本用法示例常见用法示例1. 输入控件2. 输出控件3. 动态 UI4. 数据传递和反应式编程 高级功能1. 使用 shinyjs2. 使用 shinythemes Shiny 是一个 R 语言的框架&#xff0c;用于构建交互式的网页应用&#xff0c;可以让用户以最少的 HTML、…

飞驰云联FTP替代方案:安全高效文件传输的新选择

FTP协议广泛应用各行业的文件传输场景中&#xff0c;由于FTP应用获取门槛低、使用普遍&#xff0c;因此大部分企业都习惯使用FTP进行文件传输。然而面临激增的数据量和网络安全威胁的不断演变&#xff0c;FTP在传输安全性与传输性能上有所欠缺&#xff0c;无法满足企业现在的高…

2024java面试-软实力篇

为什么说简历很重要&#xff1f; 一份好的简历可以在整个申请面试以及面试过程中起到非常好的作用。 在不夸大自己能力的情 况 下&#xff0c;写出一份好的简历也是一项很棒的能力。为什么说简历很重要呢&#xff1f; 、 先从面试来说 假如你是网申&#xff0c;你的简历必然…