如果Java没有对象,那Java会变成怎样?
1. 一切皆对象
Java语言假设我们只进行面向对象编程。所以在Java中(几乎)一切都被视为对象;在下文中,你将体会到Java特有的魅力—— 一切皆对象
2. 用引用操纵对象
每种语言都有自己操纵内存中元素的方式,但是所有的一切在Java这里都得到了简化。
你可以将一切都看作对象,但操纵对象的标识符实际上是对象的一个引用(reference)。
可以将这一切想象成用遥控器(引用),操纵电视机(对象)。只要握住这个遥控器,就能保持与电视机的连接。当有人想改变频道或减小音量时,实际操控的是遥控器(引用),再由遥控器来调控电视机(对象)。如果想在房间四处走走,同时仍能调控电视机,那么只需要携带遥控器(引用)而不是电视机(对象)! ——此例引自《Thinking in Java》
2.1 创建引用
String s; //创建字符串引用s
在这里,我们只是创建引用,并不是对象。如果此时向s发送一个消息,就会返回运行时错误。这是因为s实际上没有与任何事物相关联(即,没有电视机)。因此我们创建引用的同时便进行初始化!
String s="hello world"; //创建字符串引用s,并让s指向字符串"hello world"对象
3. 必须由你创建所有对象
3.1 new —— 给我一个新对象
一旦创建一个引用,就希望它能够与一个新的对象相关联。首先我们应该关注怎样的到一个新的对象,new关键字就是创建一个对象的秘诀!
new关键字的意思是“给我一个新的对象”,所以前面的例子就可以写成:
String s = new String("hello world");
它不仅表示“给我一个新的字符串”,而且通过提供一个初始字符串,给出了怎样产生这个String的信息。
3.2 对象存储到什么地方
在聊关于对象的存储之前,我有必要和大家介绍下Java的内存区域这个令人着迷的东西!下面我们只是简单地对Java内存区域进行介绍,后面我们会有详细讲解
3.2.1 程序计数器(Program Counter Register)
当前线程执行的字节码的 行号指示器
由于Java虚拟机的多线程是:通过线程轮流切换并分配处理器执行时间的方式来实现。在任何一个时刻,一个处理器多会只执行一条线程中的指令。因此,为了线程切换能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
唯一不会出现 OutOfMemoryError 的内存区域
3.2.2 Java 虚拟机栈(VM Stack)
虚拟机栈描述的是Java 方法执行的内存模型
*每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈出栈的过程。
局部变量表存放了编译期可预知的各种基本数据类型(boolean、byte、char、short、int、float、long、duble)、对象引用*
3.2.3 Java堆(Java Heap)
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。唯一目的就是存放对象实例
3.2.4 方法区
方法区是各个线程共享的额内存区域,主要存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
3.2.3.1 运行时常量池
运行时常量池是方法区的一部分,用于存放编译期成成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池。
3.3 特例
3.3.1 基本类型
在程序设计中经常用到一系列类型,它们需要特殊对待。之所以特殊对待,是因为new将对象存储在‘堆’中,则用new创建一个对象——特别是小的、简单的变量,往往不是很划算。因此,对于这类对象,Java采取与C++相同的方法。不用new创建对象,而是创建一个并非引用的‘自动变量’。这个变量直接存储‘值’并置于栈(Java虚拟机栈)中。
基本变量 | 大小 | 最小值 | 最大值 | 包装器类型 |
---|---|---|---|---|
boolean | —— | —— | —— | Boolean |
char | 16-bit | Unicode 0 | Unicode 2^16 -1 | Character |
byte | 8-bits | -128 | +127 | Byte |
short | 16bits | -2^15 | +2^15 -1 | Short |
int | 32bits | -2^31 | +2^31 -1 | Integer |
long | 64bits | -2^63 | +2^63 -1 | Long |
float | 32bits | IEEE754 | IEEE754 | Float |
double | 64bits | IEEE754 | IEEE754 | Double |
void | —— | —— | —— | Void |
3.3.2 高精度数字
Java提供了两个用于高精度的计算类:BigInteger 和 BigDecimal。这两个类包含的方法,提供的操作对基本类型所能执行的操作相似,也就是说,int或float的操作,也同样作用于BigInteger 或 BigDecimal。只不过必须以方法调用方式取代运算符
3.3.3 Java 中的数组
*Java会确保数组的初始化,而且不能在他的范围之外被访问。这种范围检查,是以每个数组上少量的内存开销及运行时下表检查为代价的。
当创建一个数组对象时,实际上就是创建了一个引用数组,并且每个引用都会自动被初始化为一个特定的值,该值拥有自己的关键字 null。一旦Java看到null,就知道这个引用没有指向某个对象,在使用任何引用值前,必须为其制定一个对象;如果试图使用一个还是null的引用,在运行时会报错!*
4. 永远不需要销毁对象
在大多数过程语言中,我们需要考虑很多关于变量的处理问题。如:变量需要存活多长时间?如果想销毁对象,那么什么时刻进行?变量的生命周期的混乱往往会导致大量的程序bug,下面我们将会简单了解Java是怎样替我们完成所有的清理工作
4.1 对象的作用域
大多数过程设计语言都有作用域的概念。作用域决定了在其内定义的变量的可见性和生命周期。
Java对象不具备和基本类型一样的生命周期。当用 new创建一个Java对象时,他可以存活在作用域之外。所以假如你采用代码:
{
String s = new String("a String");
} //End of scope
引用s在作用域中点就消失了。然而,s指向的String对象仍然继续占据内存空间。在上面的代码中,我们无法再这个作用域之后访问这个对象,因为对它的唯一引用已经超出了作用域的范围。
事实证明,由new创建的对象,只要你需要,就会一直保留下去。这样带来一个有趣的问题。如果Java让对象继续存在,那么靠什么才能防止这些对象填满内存空间,进而阻塞你的程序?**Java有一个垃圾回收器,用来监视用**new创建的所有对象,并辨别那些不会再被引用的对象。随后释放这些对象的内存空间。也就是说,你根本不需要担心内存回收问题,你只需要创建对象,一旦不再需要,他们就会自动消失
5. 创建新的数据类型——类
如果一切都是对象,那么是什么据定了某一类对象的外观和行为? ——引自《Thinking in Java》
5.1 class
class关键字用来表示“我准备告诉你一种新的类型的对象看起来像什么样子”
class ATypeName{
// class body goes here
}
这样就引入了一种新的类型,这样你可以用new来创建这种类型对象:
ATypeName a = new ATypeName();
5.2 字段和方法
一旦定义了一个类,就可以在类中设置两种类型的元素:字段(数据成员)和方法(成员函数)。
5.2.1 字段
字段可以是任意类型的对象,可以通过其引用与其进行通信;也可以是基本类型中的一种。如果字段是某个对象的引用,那么必须进行初始化该引用,以便使其与一个实际的对象相关联。
每个对象都有用来存储其字段的空间;普通字段不能在对象间共享。
class DataOnly{
int i;
double d;
boolean b;
} // 定义类
DataOnly data = new DataOnyly() //产生该类的对象
我们可以为字段赋值,但首先引用一个对象的成员。具体实现为:在对象引用名称之后紧跟一个句点,然后再接着是对象内部成员变量
data.i = 22;
data.d = 1.1;
data.b = false;
5.2.1.1 基本成员默认值
若类的某而过成员变量是基本类型,即使没有进行初始化,Java也会确保它获得一个默认值。
基本类型 | 默认值 |
---|---|
boolean | false |
char | null |
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
5.2.2 方法
Java的方法决定了一个对象能够接收什么样的信息 方法的基本组成包括:名称、参数、返回值和方法体。
ReturnType methodName (/*Argument List*/){
/*Method Body*/
}
- 返回类型:在调用方法之后从方法返回的值
- 参数列表:传入方法的信息类型和名称
- 方法名和参数列表唯一标识出某个方法
Java中调用方法与调用成员变量类似,对象引用.方法名称(参数列表)
object.methodName(arg1,arg2)
6. 探秘对象的创建过程
- 检查new指令参数,在常量池中匹配对应的类引用符号
- 检查该类是否已经被加载、解析、初始化过。(如果没有则进行类加载)
- 虚拟机为新生对象分配内存
3.1 “指针碰撞”
3.2 “空闲列表” - 将内存空间初始化为零值。
- 设置对象头信息
- 执行构造函数
6.1 检查new指令参数 && 判断类信息是否加载
当虚拟机遇到一条new指令时,首先会检查指令参数是否能在常量池中定位到一个类的符号引用;如果没有则会报错;检查到后则会检查这个符号引用代表的类是否已经加载、解析、和初始化过,如果没有,那就必须进行相应的类加载过程
6.2 分配内存
类加载检查通过后,虚拟机将会为新生对象分配内存。对象所需要内存的大小在类加载完成后就可完全确定,为对象分配内存空间的任务等同于把一块确定大小的内存从Java堆中划分出来,划分内存有以下两种方式:
“指针碰撞”
假设Java堆内存绝对规整,所有用过的内存放在一边,空闲内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存的过程就是把向空闲内存那边挪动一段与对象大小相等的距离。
“空闲列表”
如果Java内存不是规整的,已经使用的内存和空闲内存相互交错,这时候虚拟会维护一个列表,记录哪些内存块可用,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表记录。这种方式成为空闲列表
6.3 内存空间初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象的实例字段在Java代码中不赋值初始化就直接使用,程序能访问这些字段的数据类型所对应的零值。
6.4 设置对象头信息
接着,虚拟机要对对象头(Object Header)进行设置,如:对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄信息等。
6.5 执行构造函数
执行构造函数,为对象进行初始化