[JAVA] 只知对象属性,不知类属性?就算类答应,static都不答应

由浅入深——Java 类、对象、static成员

对象

在面向对象的思想中,一切事物都可以认为是对象——万物皆对象,把对象定义成包含状态和行为的一个实体,存在于现实世界中并且可以与其他实体区分开来的。对象具有状态和行为;比如:想想你心仪的小姐姐,可以把这个小姐姐看作是一个对象,那么该对象有两方面的定义:状态和行为;状态,如身高,年龄,三围,头发(长发或者短发)等;行为,如调戏你、跳舞,玩手机等。

通过多个相同类型的对象的状态和行为分析,可以把对象抽象成类(class);我们把具有相同特性(状态)和行为(功能)的对象的抽象定义类,对象的抽象是类,类实例化后便是对象,类的实例是对象,类其实就是对象的数据类型,但其和基本数据类型的差异在于类是程序员为了解决某些问题而自定义的,基本数据类型是计算机中的数据存储单元。

Java 对象

在Java中,对象的状态,用成员变量来描述;对象的行为,用方法来描述;故Java中类可以这样定义,语法如下:

Java 类的语法

类定义示例代码:

类定义示例代码

定义Java 类时有一些必要的规范需要遵守:

  1. 类名一律使用英文或者国际通用的拼音符号,做到见名知义,如taobao,weixin,虽然是拼音,但却是国际通用的,可以使用;
  2. 如果类使用了public修饰符,必须保证当前java文件名称和当前类名相同,而且在一个java文件中,只能有一个public修饰的类(class);
  3. 类名首字母大写,如果类名是多个单词组成的,使用驼峰命名法,如: OperatingSystem(操作系统);

对象比较操作

先考虑下面的代码:

对象比较示例代码

上述示例代码运行结果为:

对象比较结果

为什么会出现这样的结果呢?都是同样的值,为什么会有不同的比较结果?那是因为==!=这两个比较运算符在比较基本数据类型和对象对象类型时是由区别的

  • 对于基本数据类型来说,比较的是值,也就是变量存储的数据内容;
  • 对于引用数据类型来说,比较的是对象的引用,也就是其在堆内存中的地址值,每次使用new关键字创建对象,都会在堆中新开辟一块内存空间存储新创建的对象, 并且会为该内存空间生成一个唯一的地址,故内存空间不同,内存空间的地址值也就不同。

那么哪些数据类型时基本数据类型,哪些是引用数据类型呢?

  • 基本数据类型:byte、short、char、int、long、float、double、boolean
  • 引用数据类型:除基本数据类型以外的所有数据类型都是引用数据类型,包括String和基本数据类型的封装类型;

所以,如果要对对象的值做比较,就必须要是用对象的equals()方法了;这里需要注意,equals()方法并不适用于基本数据类型,对于基本数据类型的变量来说,使用== 和 !=足够了。下面用一个例子来实践,代码如下:

"equals()方法 案例"

上述案例输出结果为:

"equals()方法案例 运行结果"

由此可看出,使用对象的equals()方法是能正确比较对象的值的,因为Integer已经自定义了equals方法了,下面是源码:

"Integer equals方法源码"

不难发现,Integer的equals()方法的底层是使用基本数据类型的值做==比较的。如果是我们自定义的类,而且没有重新定义equals()方法呢,结果又会是怎样的,一起来看看:

"没有重新定义equals()方法的案例"

输出结果为:false。

因为在Java中,有一个所有引用类型都直接或者间接继承的父类,Object;因此,也可以说在java中,所有类都是Object的子类,那么,如果我们没重新实现equals()方法,会默认调用Object的equals()方法,Object的equals()方法比较的是对象的引用,所以结果输出为false。

所以想要使用自定义对象的equals方法比较对象的值,那么就必须重新实现equals方法。

对象的打印操作

默认情况下,Java对象打印的效果是:类的名称@十六进制的hashCode,比如:

"Java对象打印的案例"

输出为:com.strlite.admin.demo.Value@79b4d0f,com.strlite.admin.demo.Value是类的名称,79b4d0f是一个十六进制的数,是对象在堆中的内存地址。

重写toString() 方法

可以通过重写toString() 方法来改变对象的打印效果:

"重写toString方法的案例"

输出为:

  • i = 13

对象的生命周期

对象的开始:每次使用new关键字创建对象,就会在内存中开辟新的空间存储对象信息,此时对象开始存在。

对象的结束:当堆中的对象,没有被任何变量所引用,此时该对象就成了垃圾,等待垃圾回收器(GC)来回收;当对象被回收后,对象被销毁,对象占用的内存空间被释放,对象的生命周期结束。

匿名对象

对象创建之后没有将其赋给某一个变量。匿名对象只是在堆中开辟一块新的内存空间,但是没有把该空间地址赋给任何变量。因为没有变量引用指向,所以匿名对象仅仅只能使用一次,一般会把匿名对象作为方法的参数传递。

  • new Integer(); // 创建的就是匿名对象

构造器

  • Integer i = new Integer();

在创建对象时使用的特殊方法,出现new 关键字之后的方法,称之为构造方法、构造器、构造函数(Constructor)

构造器的作用

  1. 用于创建对象,但是必须和 new 一起使用;比如:new Integer(13);
  2. 完成对象的初始化操作,可以创建带参数的构造器,为成员变量赋初始值;

构造器的特点

  1. 构造器的名称和当前所在类的名称相同;
  2. 构造器是一个特殊的方法,其没有定义返回类型,所有不必使用void作为返回类型。 假设需要写返回类型,也应该这样写:Integer Integer(); 但没有这样的必要;
  3. 在构造器中,不需要使用return语句,其实构造器是有返回值的,会默认返回当前创建对象的引用。

如果类中没有构造器,编译器会自动创建一个默认的无参构造器

"没有构造器的案例"

我们将上述代码经过编译,得到字节码文件,再将字节码文件反编译,反编译的结果如下:

"默认构造器案例的反编译效果"

通过反编译后的结果,不难发现,即便我们没有创建构造器,编译器也会为我们创建一个默认的,编译器创建的默认构造器有以下的特点:

  • 符合构造器特点;
  • 无参数的;
  • 无方法体;
  • 如果类没有使用public修饰, 则编译器问起创建的构造器也没有public修饰;使用了public修饰,则编译器创建的构造器也使用public修饰;

"默认构造器"

如果类中没有构造器,编译器会自动创建一个默认的无参构造器。但是,如果我们显式地定义了一个构造器,则编译器不再创建默认构造器。案例如下所示:

"编译器不再创建默认构造器"

通过上述对比,不难发现,当类中存在一个构造器时,编译器便不会创建默认的构造器,而是使用我们定义的构造器,由此可得出:在一个类中,至少存在一个构造器

static 修饰符

假如每个人都有name和age两个状态,但是不同人的name和age是不一样的;也就说name和age是属于对象的。但是在生活中有些东西并不是单单属于某一个对象的,而是属于整个类的,比如:每个人都会老去、都会死。

所以,状态和行为的所属也应该有对象和类之分。 有的状态和行为应该属于对象,不同的对象,状态和行为可以不一样;而有的状态和行为应该属于类,不属于对象。为了区别与对象的状态和行为,引入static修饰符来修饰类的状态和行为。

static修饰符表示静态的,可修饰字段、方法、内部类,其修饰的成员属于类,static修饰的资源属于类级别,区别于对象级别。static的真正作用是用来区别字段、方法、内部类、初始化代码块是属于对象还是属于类本身。

static修饰符的特点

  1. static修饰的成员(字段/方法),随着所在类的加载而加载,当JVM把字节码加载进JVM的时候,static修饰的成员已经在内存中存在了。
  2. 优先于对象的存在,对象是我们手动通过new关键字创建出来的,static成员是JVM创建的;
  3. satic修饰的成员被该类型的所有对象所共享,该类创建的任何对象都可以访问该类的static成员;
  4. 直接使用类名访问static成员因为static修饰的成员直接属于类,不属于对象,所以可以直接使用类名访问static成员.

下面我们通过一个案例来实践static关键字的使用:

"static关键字的使用案例"

static修改的变量称为常量,会长时间存在于JVM内存中,所以JVM也会为它分配一定的存储空间,以下便是static常量在jvm 中的内存模型:

"static常量在jvm 中的内存模型"

JVM会将静态变量存储在方法区中,以便于及时调用;并保证其能够长时间存储于JVM中。

类成员和实例成员的访问

类中的成员:字段,方法,内部类。

  • 类成员:使用static修饰的成员,直接属于类,通过类名.static成员来访问;
  • 实例成员:没有使用static修饰的成员,实例成员只属于对象, 通过对象来访问非static字段和非static方法;

一般情况下,类成员只能访问类成员,实例成员只能访问实例成员;但深究发现,对象其实可以访问类成员,但是底层依然使用类名访问的。

static方法

static方法中,只能调用static成员;非static方法,可以访问静态成员,也可以访问实例成员;

那什么时候定义成static的字段和方法:

  • 如果这个一个状态/行为属于整个事物(类),被所有对象所共享,就直接使用static修饰;
  • 在开发中,往往把工具方法使用static修饰,比如:数组中常用的java.util.Arrays中的方法;

如果不使用static修饰,则这些方法属于该类的对象,我们得先创建对象才能调用方法,在开发中工具对象只需要一份即可,可能创建N个对象,此时可以考虑使用单例设计模式。

"成员生命周期"

类成员的使用

好处:对对象的共享数据进行单独空间的存储,节省空间,没有必要每一个对象中都存储一份,可以直接被类名调用。

弊端:生命周期过长。

局部变量初始化

局部变量定义后,必须显式初始化后才能使用,因为JVM不会为局部变量执行初始化操作。这就意味着,定义局部变量后,JVM并未为这个变量分配内存空间。直到程序为这个变量赋值时,系统才会为局部变量分配内存,并将初始值保存到该内存中。

局部变量不属于任何类或实例,因此它是保存在其所在方法的栈帧内存中。

  • 基本数据局部变量:基本数据类型变量的值会直接保存到该变量所对应的内存中。
  • 引用数据局部变量:变量内存中存的是堆中对象的地址,通过该地址引用到该变量实际指向的堆里的对象。

栈帧内存中的变量随方法或代码块的运行结束而销毁,无须JVM回收。

一点小建议

  • 开发中应该尽量缩小变量的作用范围,如此在内存中停留时间越短,性能也就更高。
  • 合理使用static修饰,一般只有定义工具方法的时候使用;
  • static方法需要访问的变量,只有该变量确实属于类,才使用static修饰字段;
  • 尽量使用局部变量;

完结。老夫虽不正经,但老夫一身的才华

阅读 274

推荐阅读
Java温故知新
用户专栏

老夫一身的才华

2 人关注
24 篇文章
专栏主页