本文参考自Java编程思想第四版,并结合自己现有知识做的一些总结。
尽管Java是基于C++的,但相比之下,Java是一种更为“纯粹”的面向对象程序设计语言。
Java语言假设我们只进行面向对象的程序设计,因为可以发现Java代码都是由一个接着一个的类组成的。可以说在Java中(几乎)一切都是对象,非对象的个例(八大基本数据类型)。
1. 引用还是指针操作对象
每一种编程语言都有操作内存中元素的方式,程序员需要注意操作元素的数据类型,是直接地操作数据,还是间接地操作数据(如C和C++中的指针)。
所有的一切在Java里都得到了简化。一切都被视为对象,因此可以采用单一固定的语法。尽管一切都看作对象,但操作元素的标识符实际上是对对象的“引用”。
上述是Java编程思想中的介绍,同时书中指出“引用”只是一个相对的解释,它在某些场景可能不被理解。
上课的时候,老师是这样跟我们解释的:首先,Java中是没有C和C++中的显示指针的,即不能直接对内存空间进行操作,但为了完成某些功能,Java中是有"隐式指针"的,它指向了某个具体的对象,对该”隐式指针"进行某些操作(给他发送消息)会作用到它指向的对象身上。
我认为两种说法都是正确的,并且二者只是表达上不一样,实际上的含义是一样的。
接下来,我们通过实例来分析一下。
定义一个Student类:
public class Student {
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
}
// s1,它既可以说是对Student对象的一个引用,也可以是一根“隐式”指针,它可以指向一个Student对象
// 无论是引用还是指针,s1都是没有初始化,由于Java编译器的严格性,下面这段代码是无法经过编译的
public static void main(String[] args) {
Student s1;
}
将s1与(1,“小明”)这个Student对象绑定,同时声明s2(Student隐式指针)指向s1。画个图理解一下:
这个时候通过s1和s2可以影响对象的值。
public static void main(String[] args) {
Student s1 = new Student(1,"小明");
Student s2 = s1;
System.out.println(s1.id);
System.out.println(s2.id);
s2.id = 2;
System.out.println(s1.id);
}
输出结果:
1
1
2
简单的解释一下:s1.id,其中.id相对于一个消息,s1收到了.id的消息需要给出id的值,但它本身没有id这个字段因此它需要到它指向的Student对象中找到id的值并给出来。那么s2.id = 2;意思就是找到s2所指向的Student对象的id字段,并将它更改为2。这个时候,若在输出s1.id的值,它将会变成2,因为s1与s2执行的是一个Student对象。
public static void main(String[] args) {
Student s1 = new Student(1,"小明");
Student s2 = s1;
s2 = new Student(2,"小明");
System.out.println(s1.id);
System.out.println(s2.id);
}
输出结果:
1
2
那为什么这个时候s1与s2的id值又不同呢,因为本质上s1与s2是互不干扰的,它们可以各自指向不同的对象。
s2 = new Student(2,“小明”);在这一句中,是=向s2发出信息,由于s2本身就可以响应这个信息,所以不需要找到它指向的对象而是s2直接指向这个新的Student对象,这样最终s1.id与s2.id的值就是不相同的。
2. 对象存储在什么位置
- 寄存器:这是最快的存储区,位于CPU内部,同时寄存器的数量是非常稀少的。寄存器根据需求进行分配,Java不能直接或间接控制寄存区。
- 堆栈:位于通用RAM(随机访问存储器中),但通过堆栈指针可以获得CPU的直接支持。堆栈指针若向下移动,则分配新的内容,若向上移动,则释放那些内存。创建程序时,Java系统必须知道存储在堆栈内的所有项的确切生命周期,以便上下移动堆栈指针。这一约束限制了程序的灵活性。所以只有部分Java数据存储在其中,包括对象引用(“隐式”指针),八大基本数据类型。
- 堆:一种通用的内存池,用于存放所有的Java对象。堆不同于堆栈的好处是:编译器不需要知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当需要一个对象时,只需要用new写一行代码即可,当执行这行代码时,会自动在堆里进行内存分配。当然高的灵活性也会付出相应的代价:用堆进行存储分配和清理可能比堆栈进行存储分配需要更多的时间。
- 常量存储:字符串常量,integer常量
- 非RAM存储:“流”对象,“持久化”对象
注意:
Java中char占两个字节,用于支持Unicode编码,同时Java所有的数值类型都有正负号,不支持无符号。
3. 字段和方法
作用域({}内为一个作用域):
{
int x = 5;
// only x
public void test(){
int p = 5;
// only p and x
}
int z = 5;
// only x and p and z
}
{
int x = 5;
public void test(){
// 这在Java中是不允许的,编译器会直接报错x已经定义过了,但在c/C++又是可以的
int x = 5;
}
}
{
// 编译器报错,x没有被初始化,这里因为x是方法内的字段,所以不会默认初始化为0,而是任意整数,
// 所以报错处理
public void test(){
int x;
}
}
{
public void test(){
String s = "123";
}
// 程序执行都这里时,虽然访问不到test中的String对象,但它仍然存在于堆中,因为JVM中的垃圾回收器
// gc没有这么快起作用,它会综合判断,在合适的时候回收test中的String对象
}
Java类的字段(数据成员)即使没有初始化,编译器也会给它一个默认值。
数据类型 | 默认值 |
boolean | false |
char | ‘\u0000’(null) |
byte | (byte)0 |
short | (short)0 |
int | 0 |
long | ol |
float | 0.0f |
double | 0.0d |
对象 | null |
**方法签名的概念:**方法名和参数列表唯一地标识某个方法。
4. static关键字
一般情况下,只有执行new来创建对象时,数据存储空间才会被分配,此时其字段和方法才能被外界访问。
但是呢,有两种情形是上述方法无法解决的:
- 希望类的某个字段能被它的所有对象共享,同时哪怕是不创建任何对象,也可以访问到该字段
- 希望某个方法不要与对象关联起来,也就是说没有创建对象也可以调用这个方法。其中最经典也是最常用的一个方法就是main()方法。
{
// 静态字段
static int a;
// 静态方法
static void test(){
}
}
注意:因为静态字段和方法没有与任何对象绑定,即在.class被加载到虚拟机时,static字段和方法就已经载入完成。这个时候,非static字段和方法还没有被载入到内存当中。因此,static方法不能调用非静态方法和字段。但是非静态方法可以调用静态方法和访问静态字段。
如何证明static字段和方法在.class被加载到虚拟机时就载入完成呢?
public class Student {
public Student(){
System.out.println("这是构造方法");
}
static {
System.out.println("这是静态字段");
}
public static void main(String[] args) {
new Student();
new Student();
}
}
new了两个Student对象,可以看到首先输出的是static语句中的"这是静态字段",并且只输出了一次,其后紧随着两次构造方法中输出的句子。
通过上述例子,很容易联想到static字段与方法会比非静态字段和方法先加载到内存中。