Java面向对象之各种变量详解

在Java中一定有很多变量让大家头疼,成员变量、类变量、局部变量等等,今天就来分别认识认识他们吧!

Java面向对象之各种变量详解

前言

在 Java语言中, 根据定义变量位置的不同,可以将变量分成两大类:

  • 成员变量
  • 局部变量

成员变量和局部变量的运行机制存在很大差异,下面我们看看差异在哪.

成员变量

成员变量指的是在类里定义的变量.

局部变量指的是在方法里定义的变量.

下面我给出Java程序中的变量划分图:

Java面向对象之各种变量详解

成员变量被分为类变量和实例变量两种.

定义成员变量时没有 static 修饰符的就是实例变量.

有static修饰符的就是类变量.

其中, 类变量从该类的准备阶段起开始存在.

直到系统完全销毁这个类,类变量的作用域与这个类的生存范围相同.

而实例变量则从该类的实例被创建起开始存在,知道系统完全销毁这个实例.

实例变量的作用域与对应实例的生存范围相同.

小知识: 一个类在使用之前需要经过 类加载 / 类验证 / 类准备 / 类解析 / 类初始化 等几个阶段.

正是基于以上原因, 可以把类变量和实例变量统称为成员变量.

其中, 类变量可以理解为 类成员变量, 它作为类本身的一个成员, 与类本身共存亡.

实例变量则可以理解为 实例成员变量, 它作为实例的一个成员, 与实例共存亡.

只要类存在, 程序就可以访问该类的类变量.

在程序中通过如下语法访问:

类.类变量

只要实例存在, 程序就可以访问该实例的实例变量.

在程序中通过如下语法访问:

实例.实例变量

当然, 类变量也可以让该类的实例来访问.

通过实例访问类变量的语法如下:

实例.类变量

但由于这个实例并不拥有这个类变量.

因此它访问的并不是这个实例的变量,依然是访问它对应类的类变量.

也就是说, 如果通过一个实例修改了类变量的值, 由于这个类变量并不属于它.

而是属于它对应的类. 因此, 修改的依然是类的类变量.

与通过该类来修改类变量的结果完全相同.

这会导致该类的其它实例来访问这个类变量时也获得这个被修改过的值.

下面我写个程序, 定义了一个 Person 类, 在这个 Person 类中定义两个成员变量.

一个类变量: eyeNum
一个实例变量: name
程序通过 PersonTest 类来创建 Person 实例.

并分别通过 Person 类 和 Person 实例来访问实例变量和类变量.

class Person
{
  //定义一个类变量
  public static int eyeNum;
  //定义一个实例变量
  public String name;
}

public class PersonTest
{
  public static void main(String[] args)
  {
    //第一次主动使用 Person 类, 该类自动初始化, 则 eyeNum 变量开始起作用, 输出 0
    System.out.println("Person 的 eyeNum 类变量的值:" + Person.eyeNum);
    //创建 Person 对象
    Person p = new Person();
    //通过 Person 对象的引用 p 来访问 Person 对象的 name 实例变量
    //并通过实例访问 eyeNum 类变量
    System.out.println("p 对象的 name 变量的值是:" + p.name + "p 对象的 eyeNum 变量的值是:" + p.eyeNum);
    //直接为 name 实例变量赋值
    p.name = "孙悟空";
    //通过 p 访问 eyeNum 类变量, 依然是访问 Person 的 eyeNum 类变量
    p.eyeNum = 2;
    //再次通过 Person 对象来访问 name 实例变量 和 eyeNum 类变量
    System.out.println("p 对象的 name 变量值是:" + p.name + "p 对象的 eyeNum 变量值是:" + p.eyeNum);
    //前面通过 p 修改了 Person 的 eyeNum, 此处的 Person.eyeNum 将输出 2
    System.out.println("Person 的 eyeNum 类变量值:" + Person.eyeNum);
    Person p2 = new Person();
    //p2 访问的 eyeNum 类变量依然引用 Person 类的, 因此依然输出 2
    System.out.println("p2 对象的 eyeNum 类变量值:" + p2.eyeNum);
  }
}

从上面程序可以看出, 成员变量无须显式初始化.

只要为一个类定义了 类变量 或 实例变量.(类变量在程序开始之前,系统会自动初始化变量)
系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化.

成员变量默认初始化的赋值规则与我们之前讲的 数组动态初始化 时数组元素的赋值规则完全相同.

我们还可以得知, 类变量的作用域 比 实例变量的作用域 更大.

实例变量随实例的存在而存在.

而类变量则随类的存在而存在.

实例也可以访问类变量, 同一个类的所有实例访问类变量时.

实际上访问的是该类本身的同一个变量, 也就是说, 访问了同一片内存区.

注意!!!

Java 允许实例访问 static 修饰的类变量本身就是一个错误.

因此建议你以后看到通过实例来访问 static 修饰的类变量时, 都可以将它替换成通过类本身来访问. 这样程序的可读性 / 明确性 都会大大提高!

局部变量

局部变量根据定义形式的不同, 可以分为如下三种:

  • 形参:在方法定义签名时定义的变量,形参的作用域只在这个方法内有效。
  • 方法的局部变量:在方法体内定义的局部变量,它的作用域就是从定义该变量的地方生效,直到方法结束时失效。
  • 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,直到该代码块结束时失效。

    ​ 与成员变量不同的是, 局部变量除了形参之外, 都必需显式初始化.

也就是说, 必需先给方法局部变量和代码块局部变量指定初始值, 否则不可以访问它们. 下面我写个定义代码块局部变量的程序.

public class BlockTest
{
  public static void main(String[] args)
  {
    {
      //定义一个代码块局部变量 a
      int a;
      //下面代码将会出现错误, 因为 a 变量没有初始化
      System.out.println("代码块局部变量 a 的值:" + a);
    }
    //下面试图访问 a 变量, 但 a 变量的作用域根本无法涉及这里
    System.out.println(a);
  }
}

上面的代码是一个错误示例, 如果你写出来还要运行的话, 只能说你想得太简单了
从上面代码可以看出, 只要离开了 代码块局部变量 所在的代码块, 这个局部变量就没法用了.

对于方法局部变量, 其作用域从定义该变量开始, 直到该方法结束.

下面我写个 方法局部变量的作用域 示例.

public class MethodLocalVariableTest
{
  public static void main(String[] args)
  {
    //定义一个方法局部变量 a
    int a;
    //下面代码将会出现错误, 因为 a 变量没有初始化
    System.out.println("方法局部变量 a 的值:" + a);
  }

​ 下面说说形参.

​ 形参的作用域时整个方法体内有效, 而且形参也无须显式初始化.

​ 形参的初始化在调用该方法时由系统完成, 形参的值由方法的调用者负责指定.

在同一个类里, 成员变量的作用域是整个类内有效.

一个类里不能定义两个同名的成员变量.

就算一个是类变量, 一个是实例变量也不行.

一个方法里不能定义两个同名的成员变量.

方法局部变量与形参名也不能同名.

同一个方法中不同代码块内的代码块局部变量可以同名.

​ 如果先定义代码块局部变量, 后定义方法局部变量.

​ 前面定义的代码块局部变量与后面定义的方法局部变量是可以同名的.

Java允许局部变量和成员变量同名.

如果方法里的局部变量和成员变量同名, 局部变量会覆盖成员变量.

如果需要在这个方法里引用被覆盖的成员变量.

可以使用 this (对于实例变量) 或 类名(对于类变量) 来作为调用者.

​ 下面, 我写个程序.

public class VariableOverrideTest
{
  //定义一个 name 实例变量
  private String name = "猪八戒";
  //定义一个 price 类变量
  private static double price = 78.0;
  //主方法, 程序的入口
  public static void main(String[] args)
  {
    //方法里的局部变量, 局部变量覆盖成员变量
    int price = 65;
    //直接访问 price 变量, 将输出 price 局部变量的值.

    System.out.println(price);
    //使用类名作为 price 变量的调用者, 访问被覆盖的 类变量
    System.out.println(VariableOverrideTest.price);
    //运行 info 方法
    new VariableOverrideTest().info();
  }
  public void info()
  {
    //方法里的局部变量, 局部变量覆盖成员变量
    String name = "孙悟空";
    //直接访问 name 变量, 将输出 孙悟空
    System.out.println(name);
    //使用 this 来作为 name 的调用者, 访问 实例变量
    System.out.println(this.name);
  }
}

从上面代码可以看出, 当局部变量覆盖成员变量时.

依然可以在方法中显式指定 类名和this 作为调用者来访问被覆盖的成员变量, 这使得变成更加自由.

不过, 不过, 不过 . 你应该尽量避免这种局部变量和成员变量同名的情形. (想个名字真的有那么难么 – – )

成员变量(属性)和局部变量的区别?

  • 成员变量:
  • 成员变量定义在类中,在整个类中都可以被访问。
  • 成员变量分为类成员变量和实例成员变量,实例变量存在于对象所在的堆内存中。
  • 成员变量有默认初始值。
  • 成员变量的权限修饰符可以根据需要,任意选择一个。
    *
  • 如,public,private。
  • 局部变量:
  • 局部变量只定义在局部范围内,如,方法内,代码块等。
  • 局部变量存在于栈内存中。
  • 作用的范围结束,变量的空间会自动释放。
  • 局部变量没有默认初始值,每次必定显示初始值。
  • 局部变量声明时不指定权限修饰符。

成员变量的初始化和内存中的运行机制

当系统加载类或创建该类的实例时.

系统将自动为成员变量分配内存空间.

并在分配内存空间后, 自动为成员变量指定初始值.

下面通过代码来创建两个实例(非完整代码,能明白就行).

同时配合示意图来说明 Java 成员变量的初始化和内存中的运行机制.

//创建第一个 Person 对象
Person p1 = new Person();
//创建第二个 Person 对象
Person p2 = new Person();
//分别为两个 Person 对象的 name 实例变量赋值
p1.name = "孙悟空";
p2.name = "皮卡丘";
//分别为两个 Person 对象的 eyeNum 类变量赋值
p1.eyeNum = 2;
p2.eyeNum = 3;

下面开始解读:

当程序执行第一行代码 Person p1 = new Person(); 时
如果这行代码是第一次使用 Person 类.

则系统会加载并初始化这个类.

在类的准备阶段.

系统将会为该类的类变量分配内存空间,并指定默认初始值.

当 Person 类初始化完成后, 系统内存中的存储示意图如下:

Java面向对象之各种变量详解

Java面向对象之各种变量详解
​ 从上图可以看出.

当 Person 类初始化完成后.

系统将在堆内存中为 Person 类分配一块内存区.

在这块内存区中, 包含了 保存 eyeNum 类变量的内存.

并设置 eyeNum 的默认初始值为: 0

系统接着创建了一个 Person 对象.

并把这个 Person 对象赋给 p1 变量.

Person 对象里包含了名为 name 的实例变量.

实例变量是在创建实例时分配内存空间并指定初始值的.

​ 当创建了第一个 Person 对象后, 系统内存中的存储示意图如下:

Java面向对象之各种变量详解

Java面向对象之各种变量详解
​ 从上图可以看出, eyeNum 类变量并不属于 Person 对象.

它是属于 Person 类的.

所以创建第一个 Person 对象时并不需要为 eyeNum 类变量分配内存(废话…)
系统只为 name 实例变量分配了内存空间.

并指定默认初始值: null

接着执行 Person p2 = new Person();
代码创建第二个 Person 对象.

此时因为 Person 类已经存在于堆内存了.

所以不需要对 Person 类进行初始化(废话…Java 会那么傻么…)
创建第二个 Person 对象 与 创建第一个 Person 对象并没有什么不同.

当程序执行 p1.name = “孙悟空”; 时
将为 p1 的 name 实例变量赋值.

也就是让堆内存中的 name 指向 “孙悟空” 字符串.

我们之前说过, 字符串也是一种引用变量. 所以你懂的.

​ 执行完成后, 两个 Person 对象在内存中的存储示意图如下:

Java面向对象之各种变量详解

Java面向对象之各种变量详解
​ 从上图可以看出, name 实例变量是属于单个 Person 实例的.

因此, 修改第一个 Person 对象的 name 实例变量时仅仅与 p1 对象有关.

与 Person 类和其它 Person 对象没有任何关联.

同理, 修改第二个 Person 对象 p2 的 name 实例变量时, 也与 Person 类和其它 Person 对象无关.

直到执行 p1.eyeNum = 2 时
此时呢, 就是犯大忌了. 你拿 对象来操作类变量了. 不过为了教学演示, 我拿自己当典型.

从我们看过的图当中, 可以知道.

Person 的对象根本没有保存 eyeNum 这个变量.

通过 p1 访问的 eyeNum 类变量.

其实还是 Person 类的 eyeNum 类变量.

因此, 此时修改的是 Person 类的 eyeNum 类变量.

​ 修改成功后, 内存中的存储示意图如下:

Java面向对象之各种变量详解

Java面向对象之各种变量详解
​ 从上图可以看出.

不管通过哪个 Person 实例来访问 eyeNum 类变量.

它们访问的其实都是同一块内存.

所以就再次提醒你.

当程序需要访问 类变量时.

尽量使用类作为主调, 而不要使用对象作为主调.

这样可以避免歧义, 提高程序的可读性.

局部变量的初始化和内存中的运行机制

局部变量定义后.

必需经过显式初始化后才能使用.

系统不会为局部变量执行初始化.

这意味着,定义局部变量之后,系统并未为这个变量分配内存控件.

直到等程序为这个变量赋初始值时.

系统才会为局部变量分配内存,并将初始值保存到这块内存中去.

与成员变量不同,局部变量不属于任何类或实例.

因此它总是保存在其所在的方法的栈内存中.

如果局部变量是基本类型变量,则直接把这个变量的值保存在该变量对应的内存中.

如果局部变量是引用类型的变量,则这个变量里存放的就是地址.
通过该地址引用到该变量实际引用的对象或数组.

变量的使用规则

对于新手来说.

什么时候使用类变量?

什么时候使用实例变量?

什么时候使用方法局部变量?

什么时候使用代码块局部变量?

这种选择比较困难,如果仅仅从程序的运行结果来看,大部分时候都可以直接使用类变量或实例变量来解决问题.无须使用局部变量.

但实际上这种做法非常错误.

因为定义一个成员变量时,成员变量将被放置到堆内存中.

成员变量的作用域将扩大到类存在范围或对象存在范围,这种返回的扩大有两个害处.

Original: https://www.cnblogs.com/gaoziman/p/15900264.html
Author: 爱笑的Gao
Title: Java面向对象之各种变量详解

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/609498/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • docker安装minio

    拉取镜像 docker pull minio/minio 运行容器 docker run–net=”host”–name minio…

    Linux 2023年6月7日
    089
  • Qt-Vnc远程

    VNC简介 VNC(Virtual Network Computing)是基于RFB(Remote Frame Buffer)协议的远程系统,C/S端口默认为5900,B/S端口默…

    Linux 2023年6月8日
    0492
  • 【转】谈谈 JVM 内部锁升级过程

    一、加锁发生了什么 //System.out.println都加了锁 public void…

    Linux 2023年6月16日
    0119
  • shell ${val:0:3)含义

    ${file:0:5}:提取最左边的5个字节:/dir1${file:5:5}:提取第5个字节右边的连续5个字节:/dir2 例子: pval=12345678 r=${pval:…

    Linux 2023年5月28日
    069
  • python写一个双色球彩票计算器

    首先声明,赌博一定不是什么好事,也完全没有意义,不要指望用彩票发财。之所以写这个,其实是用来练手的,可以参考这个来预测一些其他的东西,意在抛砖引玉。 啰嗦完了,马上开始,先上伪代码…

    Linux 2023年6月6日
    0105
  • linux root用户编辑文件提示没有权限

    linux root用户编辑文件提示没有权限 感觉很奇怪,因为是root用户。于是查看了一下文件的权限,结果如下: [root@localhost elasticsearch-5….

    Linux 2023年6月8日
    0104
  • ShardingSphere-proxy-5.0.0容量范围分片的实现(五)

    一、修改配置文件config-sharding.yaml,并重启服务 # Licensed to the Apache Software Foundation (ASF) unde…

    Linux 2023年6月14日
    0143
  • JavaScript快速入门-07-异常处理与调试

    7、异常处理与调试 7.1 异常处理 7.1.1 try/catch语句 try/catch语句常用于处理JavaScript中的异常,其基本语法如下所示: try { // 可能…

    Linux 2023年6月7日
    0112
  • Firefox浏览器的一些配置

    一、在新标签页打开书签 1、打开Firefox浏览器,地址栏输入 about:config。 2、选择”接受风险并继续”。 3、搜索 browser.tab…

    Linux 2023年6月6日
    0112
  • 数据链路层 交换机的工作原理

    以太网 以太网是一种将几台电脑连接起来,能够进行通讯的技术,也就是组建所谓的”局域网”。所以以太网可以说是一种局域网技术但局域网技术并非只有以太网一种,还有…

    Linux 2023年6月6日
    0117
  • 6.22(js–>案例应用)

    (练习1)简易计算器: <html lang="en"> <head> <meta charset="UTF-8&quo…

    Linux 2023年6月7日
    0113
  • ELK收集日志之logstash使用

    一、logstash使用 1.logstah收集文件日志 不难理解,我们的日志通常都是在日志文件中存储的,所以,当我们在使用INPUT插件时,收集日志,需要使用file模块,从文件…

    Linux 2023年5月27日
    095
  • Windows 域控配置时间同步

    此功能是因内网时间与互联网时间不同步,需我们手动指定互联网NTP服务器来同步时间。一般默认情况下,加域客户端同步的是域主机的时间。如果域控的主机时间不准的话,那么域内的客户端也就随…

    Linux 2023年6月8日
    0145
  • vim的使用

    1、概述: Vim 是从 vi 发展出来的一个文本编辑器。具有代码补全、编译及错误跳转等功能 2、vim编辑器的常用命令: 图源:https://vimsky.com/articl…

    Linux 2023年5月27日
    0128
  • kubeadm 加入新master 报错

    error execution phase check-etcd: error syncing endpoints with etcd: context deadline exce…

    Linux 2023年6月14日
    0122
  • 四、vi/vim编辑器

    vi/vim 模式命令模式编辑模式末行模式home键 行首end键 行尾命令模式切换到编辑模式a 当前字符后输入A 当前行行尾输入i 当前字符前输入I 当前行行首输入o 当前行下一…

    Linux 2023年6月7日
    0107
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球