一切皆是对象

Java编程中,最重要的不是语法,感觉是一种oop的思想。最近刚刚开始学java,但是还是感觉java这门语言在被设计的时候,还是站在解决问题的 这个角度去考虑的,并且对oop思想贯彻的非常好。相比C艹来说,java更关注解决问题,而不是效率等一些高级的问题。

Java中其实有对象和引用这两个概念,说白了,对象就是内存中的内容,而引用则可以认为是和指针一样的东西,用来标识这块内存。这样一来,在想操作这个对象的时候,我们就可以通过操作这个引用来实现。因为对象和引用两个概念是分开的,所以在Java中,引用是可以脱离对象独立存在的。如:

1
2
String s;
String a = "xuran"

s就是一个还没有和特定String类型对象关联起来的一个引用,a也是一个引用,但是已经和“xuran”这个字符串对象关联起来了。 除了上述a这个引用和对象关联起来的方式之外,还可以用一种更加通用的方式:

1
String a = new String("xuran");

new这个关键字的作用就在于创建一个新的对象。将这个对象赋值给a,就相当于将a这个引用和新建的对象关联起来。因Java对象的生存周期是其内部的GC机制来控制的,所以Java通过new关键字创建的对象,都存储于堆上。引用和对象不同,在Java语言中,一个引用变量的生命周期通常都是在程序运行前就已经确定的,这也就是为什么有了作用域这一概念的原因(我理解是这样),所以Java当中的对象引用一般都是创建在堆栈上面的。堆栈上面的创建和回收操作要比堆上面的效率更高,因为堆栈中的每一项大小都是固定的,在分配和回收的时候,只需要移动栈顶指针即可。但是堆不同,它的创建和回收都比堆栈要复杂的多。

Java中对于一些基本的数据类型的存储方式上面也比较特殊,比如int类型,是一种基本的整数类型。但是在Java中同时还提供了一种Integer的类型。前者在创建的时候,采用直接声明的方式int a = 3;这种形式的创建并非创建了一个引用变量a,而是创建了一种”自动“变量,这个变量存储于堆栈中,里面存放的是赋值给他的值。而Integer a = new Integer(3);这种形式,就和上面所说的一样,是创建了一个引用变量,并且把引用和具体的对象建立了联系。并且在Java中,基本数据类型的种类要比C艹少了很多,比如没有无符号数,也没有int8,int16这种,java中所有基本的数据类型的大小都是固定的。

Java中的引用和对象由于存储位置的不同,自然会引起一个问题,那就是当引用变量脱离它的作用域的时候,这个引用变量就是不可见的,在堆栈中就已经被回收了。但是对象是存储于堆中的,这样就会造成了内存泄漏,因为创建的对象没人用,但是还没有被回收。所以Java中有一套垃圾回收的机制来处理这些问题。

Java在自定义类型中,对于一个类中的字段,可以是基本数据类型,也可以是一个对象的引用。对于一个特定类的对象在定义之后,它所具有的字段的默认值是不同的:

  1. 如果是引用的话,那么Java将不会对它赋值一个准确的值,需要自己在构造函数中准确的初始化
  2. 如果是基本数据类型,Java会将他们赋值一个默认的0值

但是这种默认复制的行为只发生在类的成员中,在Java中,局部变量在定义的时候,如果没有赋值直接使用,也是会报错的。这一点和golang就有很大的不同,golang在这中默认赋值的行为上面是统一的。

初始化

Java中用于初始化类对象成员的方式和C艹中是一样的,默认提供了一个构造函数,但是自己可以指定带参数的,或者是重写不带参数的构造函数。一旦自定义了一种形式的构造函数之后,那么在构造这个类型的对象的时候,就必须按照这种构造函数的形式进行调用,否则会编译错误。如,你定义了带参数的构造函数,此时再想调用默认的就不行了。

对于使用构造函数来初始化类内成员的方式,需要注意一点的就是,Java对于类成员默认的初始化行为是一直存在的,即使你自己定义了构造器。举例来讲,基本数据类型的成员会变成类型对应的0值,而引用类型会被初始化为NULL。当默认的初始化行为结束之后,才会自动调用类的构造器对类成员进行第二次初始化。

除了使用构造函数可以初始化类内成员,还可以直接在类中进行初始化。如:

1
2
int a = 3;
}

静态成员的初始化比较特殊,有以下几点需要注意:

  1. 静态成员只有在需要的时候才会被初始化,”需要“的时候包括创建了类内有静态成员的对象实例,以及使用某个静态成员的时候
  2. 静态成员初始化优先于非静态成员
  3. 一个类中的静态成员,只在这个类第一个对象被创建的时候,才会被初始化。静态成员在整个程序的生命周期只初始化一次

那么这里就涉及到一个初始化顺序的问题。类内成员的初始化顺序,遵循在类中定义的顺序,但是却会在任何方法被调用之前初始化,这里是包括构造函数的。也就是说,如果类中直接初始化某几个变量,那么这些变量在构造函数调用之前就会执行它们预定的初始化操作。

在初始化一个数组的时候,如果事先知道一个数组内部的元素,我们可以直接在创建数组的时候,利用大括号初始化一个数组。这种存储的分配方式和直接使用new关键字是一样的。直接定义一个数组的引用,但是没有跟它初始化一个真正的数组对象的时候,它的值应为NULL。Java中允许数组之间的赋值行为,直接使用赋值号和C++中的浅拷贝的行为是一样的,只不过是多了一个引用指向了同一个数组对象上,所以他们的更改会相互影响。

在使用new创建一个基本数据类型的数组的时候,会自动创建一系列的基本数据类型变量,直接在堆上为其开辟了空间。但是如果是创建非基本数据类型的数组的时候,我们实际上是创建了一个引用数组,在未给这些引用链接到真是存在的对象之前,如果擅自使用数组元素,是会抛出异常的。所以,往往在创建一个非基本类型的数组之后,在使用之前还要为每一个引用都链接一个真实的对象。

方法重载

重载这个概念,在C艹里面也是存在的。简单来说,就是对名字相同的函数,赋予不同的形参列表,即形成了函数重载。在执行的时候,会根据传递参数的不同而调用不同的方法。甚至在java中,还参数顺序的不同,也可以作为一种重载的形式,这也侧面说明了,重载概念中形参列表的不同,不仅仅是在于形参的类型和个数,顺序也是衡量的目标之一。

在对方法传递基本类型参数的时候,如果目标参数的类型窄于要传递的参数,此时必须进行强制类型转换才可以进行传递。如果反过来,相当于数据类型的一个提升,是没问题的。

this && static

Java中对于成员方法的调用一般来讲,都是默认的将调用对象的引用作为第一参数传递给了该成员方法,方法内部一般如果想使用它的话,直接用this这个关键字即可。并且还可以通过this在一个构造器中调用另外一个构造器,但是这种用法需要注意的是,调用语句必须放在开头且只能嵌套一个。如果出现了形参名字和成员字段名相同的时候,为了消除歧义,就使用this来表明成员字段名。

static和this在语义上是相反的,this标明的调用当前方法的一个对象,也就是说,有this出现的方法一般是和特定对象绑定的。但是static是和具体对象无关的,它属于整个类,比较像一个类内的全局方法,任何一个对象都可以调用它,也可以通过类名直接调用。但是在static的函数内,是不能够调用非static方法的,反过来是可以的。

除了static的方法之外,还有就是static的成员字段值。一般非static的成员字段值都是跟对象绑定的,不同的对象,对于同一个类成员的字段可以有不同的值,但是static是属于整个类的,所以,任何一个对象对static成员字段值的修改,都会影响其他的对象。