![4c0571491b0bfdd8b9d038919c2e342b.png](https://i-blog.csdnimg.cn/blog_migrate/b8bb3ac96128961e47cf2041504e398a.jpeg)
由浅入深——Java 类、对象、static成员
对象
在面向对象的思想中,一切事物都可以认为是对象——万物皆对象,把对象定义成包含状态和行为的一个实体,存在于现实世界中并且可以与其他实体区分开来的。对象具有状态和行为;比如:想想你心仪的小姐姐,可以把这个小姐姐看作是一个对象,那么该对象有两方面的定义:状态和行为;状态,如身高,年龄,三围,头发(长发或者短发)等;行为,如调戏你、跳舞,玩手机等。
类
通过多个相同类型的对象的状态和行为分析,可以把对象抽象成类(class);我们把具有相同特性(状态)和行为(功能)的对象的抽象定义类,对象的抽象是类,类实例化后便是对象,类的实例是对象,类其实就是对象的数据类型,但其和基本数据类型的差异在于类是程序员为了解决某些问题而自定义的,基本数据类型是计算机中的数据存储单元。
Java 对象
在Java中,对象的状态,用成员变量来描述;对象的行为,用方法来描述;故Java中类可以这样定义:
[修饰符] class 类名
{
成员变量(字段/Field)
... ...
方法
... ...
}
类定义示例代码:
// 类名
public class Student {
/** 状态 **/
// id
int id;
// 姓名
String name;
// 成绩
int score;
/** 行为 **/
// 通过学生id获取姓名
public void getNameById(int id) {
System.out.println("学生:" + name);
}
}
定义类须注意:
- 类名一律使用英文或者国际通用的拼音符号,做到见名知义,如taobao,weixin,虽然是拼音,但却是国际通用的,可以使用;
- 如果类使用了public修饰符,必须保证当前java文件名称和当前类名相同,而且在一个java文件中,只能有一个public修饰的类(class);
- 类名首字母大写,如果类名是多个单词组成的,使用驼峰命名法,如: OperatingSystem(操作系统);
对象比较操作
先考虑下面的代码:
public static void main(String[] args) {
int i = 13;
int j = 13;
System.out.println(i == j);
Integer k = new Integer(13);
Integer l = new Integer(13);
System.out.println(k == l);
}
以上代码运行结果为:
true
false
为什么会出现这样的结果呢?
那是因为== 和 != 这两个比较运算符 :
- 对于基本数据类型来说,比较的是值,也就是变量存储的数据内容;
- 对于引用数据类型来说,比较的是对象的引用,也就是其在堆内存中的地址值,每次使用new关键字创建对象,都会在堆中新开辟一块内存空间存储新创建的对象, 并且会为该内存空间生成一个唯一的地址,故内存空间不同,内存空间的地址值也就不同。
基本数据类型:byte、short、char、int、long、float、double,boolean;
引用数据类型:除基本数据类型以外的所有数据类型都是引用数据类型,包括String和基本数据类型的封装类型;
如果,要对对象的值做比较,就必须要是用对象的equals()方法了;这里需要注意,equals()方法并不适用于基本数据类型,对于基本数据类型的变量来说,使用 == 和 != 足够了。
考虑下面的代码:
public static void main(String[] args) {
Integer k = new Integer(13);
Integer l = new Integer(13);
System.out.println(k == l);
System.out.println(k.equals(l));
}
输出结果为:
false
true
由此可看出,使用对象的equals()方法是能正确比较对象的值的,因为Integer已经自定义了equals方法了,下面是源码:
/**
* Compares this object to the specified object. The result is
* {@code true} if and only if the argument is not
* {@code null} and is an {@code Integer} object that
* contains the same {@code int} value as this object.
*
* @param obj the object to compare with.
* @return {@code true} if the objects are the same;
* {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
不难发现,Integer的equals()方法的底层是使用基本数据类型的值做==比较的。
如果是我们自定义的类,而且没有重新定义equals()方法呢,结果又会是怎样的,一起来看看:
public class MyClass {
public static void main(String[] args) {
Value value1 = new Value();
Value value2 = new Value();
value1.i = value2.i = 13;
System.out.println(value1.equals(value2));
}
}
class Value {
int i;
}
输出结果为:false。
因为在Java中,有一个所有引用类型都直接或者间接继承的父类,Object;因此,也可以说在java中,所有类都是Object的子类,那么,如果我们没重新实现equals()方法,会默认调用Object的equals()方法,Object的equals()方法比较的是对象的引用,所以结果输出为false。
所以想要使用自定义对象的equals方法比较对象的值,那么就必须重新实现equals方法。
对象的打印操作
默认情况下,Java对象打印的效果是:类的名称@十六进制的hashCode,比如:
public class MyClass {
public static void main(String[] args) {
Value value = new Value();
System.out.println(value);
}
}
class Value {
int i;
}
输出为:com.strlite.admin.demo.Value@79b4d0f
可以通过重写toString() 方法来改变对象的打印效果:
public class MyClass {
public static void main(String[] args) {
Value value = new Value();
value.i = 13;
System.out.println(value);
}
class Value {
int i;
@Override
public String toString() {
return "i = " + i;
}
}
输出为:
i = 13
对象的生命周期
对象的开始:每次使用new关键字创建对象,就会在内存中开辟新的空间存储对象信息,此时对象开始存在。
对象的结束:当堆中的对象,没有被任何变量所引用,此时该对象就成了垃圾,等待垃圾回收器(GC)来回收;当对象被回收后,对象被销毁,对象占用的内存空间被释放,对象的生命周期结束。
匿名对象
对象创建之后没有将其赋给某一个变量。匿名对象只是在堆中开辟一块新的内存空间,但是没有把该空间地址赋给任何变量。因为没有变量引用指向,所以匿名对象仅仅只能使用一次,一般会把匿名对象作为方法的参数传递。
new Integer(); // 匿名对象
构造器
Integer i = new Integer();
在创建对象时使用的特殊方法,称之为构造方法、构造器、构造函数(Constructor)。
构造器的作用:
- 用于创建对象,但是必须和 new 一起使用;比如:new Integer(13);
- 完成对象的初始化操作,可以创建带参数的构造器,为成员变量赋初始值;
构造器的特点:
- 构造器的名称和当前所在类的名称相同;
- 构造器是一个特殊的方法,其没有定义返回类型,所有不必使用void作为返回类型。 假设需要写返回类型,也应该这样写:Integer Integer(); 但没有这样的必要;
- 在构造器中,不需要使用return语句,其实构造器是有返回值的,会默认返回当前创建对象的引用。
如果类中没有构造器,编译器会自动创建一个默认的无参构造器。
public class ConstructorDemo
{
public static void main(String[] args) {
}
}
反编译后的结果:
public class ConstructorDemo
{
public ConstructorDemo()
{
}
public static void main(String args[])
{
}
}
编译器创建的默认构造器的特点:
- 符合构造器特点;
- 无参数的;
- 无方法体;
- 如果类没有使用public修饰, 则编译器问起创建的构造器也没有public修饰;使用了public修饰,则编译器创建的构造器也使用public修饰.
![9998059004c7f2b2eba8cfa217166317.png](https://i-blog.csdnimg.cn/blog_migrate/5bfc369834c20802ba3910dfd3aa27f8.jpeg)
默认构造器
如果类中没有构造器,编译器会自动创建一个默认的无参构造器。但是,如果我们显示定义了一个构造器,则编译器不再创建默认构造器。
public class ConstructorDemo
{
// 自定义的构造器
public ConstructorDemo(int i) {
}
public static void main(String[] args) {
}
}
反编译的结果:
public class ConstructorDemo
{
public ConstructorDemo(int i)
{
}
public static void main(String args[])
{
}
}
通过上述对比,不难发现,在一个类中,至少存在一个构造器。
static 修饰符
假如每个人都有name和age两个状态,但是不同人的name和age是不一样的;也就说name和age是属于对象的。但是在生活中有些东西并不是单单属于某一个对象的,而是属于整个类的,比如:每个人都会老去、都会死。
所以,状态和行为的所属也应该有对象和类之分。 有的状态和行为应该属于对象,不同的对象,状态和行为可以不一样;而有的状态和行为应该属于类,不属于对象。为了区别与对象的状态和行为,引入static修饰符来修饰类的状态和行为。
static修饰符表示静态的,可修饰字段、方法、内部类,其修饰的成员属于类,static修饰的资源属于类级别,区别于对象级别。
static的真正作用是用来区别字段、方法、内部类、初始化代码块是属于对象还是属于类本身。
static修饰符的特点:
- static修饰的成员(字段/方法),随着所在类的加载而加载,当JVM把字节码加载进JVM的时候,static修饰的成员已经在内存中存在了。
- 优先于对象的存在,对象是我们手动通过new关键字创建出来的,static成员是JVM创建的;
- satic修饰的成员被该类型的所有对象所共享,该类创建的任何对象都可以访问该类的static成员;
- 直接使用类名访问static成员因为static修饰的成员直接属于类,不属于对象,所以可以直接使用类名访问static成员.
public class ConstructorDemo
{
// 自定义的构造器
public ConstructorDemo(int i) {
}
public static void main(String[] args) {
int count = StaticDemo.count;
System.out.println(count);
}
}
class StaticDemo
{
// 静态成员
static int count = 0;
}
上述示例代码在jvm 中是这样的:
![c2be342df783c48d501786adcc755cdc.png](https://i-blog.csdnimg.cn/blog_migrate/4dc6f9deb822ec6acb1b64fdfecb4638.jpeg)
静态成员的JVM模型
类成员和实例成员的访问
类中的成员:字段,方法,内部类。
- 类成员:使用static修饰的成员,直接属于类,通过类名.static成员来访问;
- 实例成员:没有使用static修饰的成员,实例成员只属于对象, 通过对象来访问非static字段和非static方法;
一般情况下,类成员只能访问类成员,实例成员只能访问实例成员;但深究发现,对象其实可以访问类成员,但是底层依然使用类名访问的。
static方法
在static方法中,只能调用static成员;非static方法,可以访问静态成员,也可以访问实例成员;
那什么时候定义成static的字段和方法:
- 如果这个一个状态/行为属于整个事物(类),被所有对象所共享,就直接使用static修饰;
- 在开发中,往往把工具方法使用static修饰,比如:数组中常用的java.util.Arrays中的方法;
如果不使用static修饰,则这些方法属于该类的对象,我们得先创建对象才能调用方法,在开发中工具对象只需要一份即可,可能创建N个对象,此时可以考虑使用单例设计模式。
![0fb651f23633ea3c2a4bc94e4d28a855.png](https://i-blog.csdnimg.cn/blog_migrate/0f7f0332291d53049071cb58a2411bee.jpeg)
成员生命周期
类成员的使用
好处:对对象的共享数据进行单独空间的存储,节省空间,没有必要每一个对象中都存储一份,可以直接被类名调用。
弊端:生命周期过长。
局部变量初始化
局部变量定义后,必须显式初始化后才能使用,因为JVM不会为局部变量执行初始化操作。这就意味着,定义局部变量后,JVM并未为这个变量分配内存空间。直到程序为这个变量赋值时,系统才会为局部变量分配内存,并将初始值保存到该内存中。
局部变量不属于任何类或实例,因此它是保存在其所在方法的栈帧内存中。
- 基本数据局部变量:基本数据类型变量的值会直接保存到该变量所对应的内存中。
- 引用数据局部变量:变量内存中存的是堆中对象的地址,通过该地址引用到该变量实际指向的堆里的对象。
栈帧内存中的变量随方法或代码块的运行结束而销毁,无须JVM回收。
一点小建议
- 开发中应该尽量缩小变量的作用范围,如此在内存中停留时间越短,性能也就更高。
- 合理使用static修饰,一般只有定义工具方法的时候使用;
- static方法需要访问的变量,只有该变量确实属于类,才使用static修饰字段;
- 尽量使用局部变量;
完结。