一、用构造器确保初始化
C++引入了构造器的概念,这是一个在创建对象时被自动调用的特殊方法。Java中也采用了构造器,并额外提供了“垃圾回收器”,对于不再使用的内存资源,垃圾回收器会自动将其释放。
在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
如何命名初始化的方法呢?有两个问题:第一,所取的任何一个名字都可能跟类的某个成员重名;第二,调用构造器是编译的责任,必须要让编译器知道应该调用哪个方法。所以,Java中采用了跟C++一样的策略,即构造器采用与类相同的名称。
public class Student {
Student(){
}
}
@Test
public void example1(){
Student stu1 = new Student();
}
在创建对象时,将会为对象分配内存空间,这就确保了你在操作对象之前,它已经被初始化了。不接受任何参数的构造器叫做默认构造器,Java中通常使用术语叫无参构造器。构造器是一特殊类型的方法,因为它没有返回值。
构造器有利于减少错误,产使代码易于阅读,从概念上讲初始化与创建是彼此独立的,但是在Java中,初始化和创建是捆绑在一起的,两者不能分离。
二、方法重载
如果想用多种方式创建一个对象,这就需要多个构造器,但由于构造器的名字跟类名相同。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。既然构造方法可以重载,那么重载也可适用其他方法。如下展示重载的构造方法:
public class Student {
private String name;
private Integer age;
Student(){
}
Student(String name, Integer age) {
this.name = name;
this.age = age;
}
}
1、区分重载方法
方法名相同的方法如何进行区分呢?那就是每个重载的方法都必须有一个独一无二的参数类型列表。
2、涉及基本类型的重载
如果有某个重载方法接受int型参数,它会被直接调用,系统默认的是int型;如果传入的数据类型小于方法中声明的形式参数类型,实际数据类型就会被提升;char型略有不同,如果无法找到接受char参数的方法,就会把char直接提升到int型。如下:
public class BaseDataType {
void print1(short x){
System.out.println("print1 short");
}
void print1(int x){
System.out.println("print1 int");
}
void print1(char x){
System.out.println("print1 char");
}
void print1(double x){
System.out.println("print1 double");
}
void print2(short x){
System.out.println("print2 short");
}
void print2(int x){
System.out.println("print2 int");
}
void print2(double x){
System.out.println("print2 double");
}
void print3(double x){
System.out.println("print3 double");
}
}
@Test
public void example2(){
BaseDataType bdt = new BaseDataType();
byte b = 5;
bdt.print1(b);
bdt.print2(b);
bdt.print3(b);
/**
* print1 short
* print2 int
* print3 double
*/
bdt.print1('x');
bdt.print2('x');
bdt.print3('x');
/**
* print1 char
* print2 int
* print3 double
*/
}
如果传入的实际参数较大,就得通过类型转换来进行强制窄化处理,否则编译器会报错。
3、以返回值能区分重载方法吗?
看如下示例:
void print4(){
}
int print4(){
return 1;
}
调用时可能是下面这样:
print4();//返回 void
print4();//返回int但这里不需要返回值
这时就无法区分重载方法,编译器也无法判断究竟应该调用哪个方法,别人阅读时也无法理解。所以根据方法的返回值来区分重载是行不通的。
三、默认构造器
默认构造器是没有形式参数的,它的作用是创建一个默认的对象,如果你的类中没有构造器,那么编译器会自动帮你创建一个默认的构造器。但是,如果你已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
没有构造器,但依然可以创建对象,如下:
public class Student {
}
@Test
public void example1(){
Student stu1 = new Student();
}
四、this关键字
如下代码,如何知道peel()方便是被a还是被b调用呢?
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana(),
b = new Banana();
a.peel(1);
b.peel(2);
}
}
class Banana{
void peel(int i){
//...
}
}
为了能用简便、面向对象的语法来编写代码(即发送消息给对象),编译器做了一些幕后工作,它暗自把“操作对象的引用”作为第一个参数传递给peel(),所以它们的调用形式如下:
Banana.peel(a,1);
Banana.peel(b,2);
这里其内部的表示形式,我们并不能这样去书写代码,它是由编译器“偷偷”来决定的,并没有标识符可用,但有一个关键字来表示,即:this。this关键字只能在方法内部使用,表示对调用方法那个对象的引用。如果在方法内部调用同一个类的另一个方法,是不需要使用this的,直接调用即可。示例如下:
void peel1(){
this.peel(1);
}
void peel2(){
peel(1);
}
它们在效果上是一样的,只是第二次调用省略了关键字this而已,在编译时,编译器会自动帮你加上。在实际编程中,如无必要一般会省略关键字,除非有一些需求明确指出对当前对象的引用时,才需要使用this关键字。如下示例,返回对当前对象的引用:
public class Leaf {
int i = 0;
Leaf increment(){//同样功能使用this的写法
i++;
return this;
}
Leaf increment(Leaf f){//同样功能不使用this的写法
f.i ++;
return f;
}
void print(){
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf f = new Leaf();
f.increment().increment().increment().print();
//i = 3
Leaf f2 = new Leaf();
f2.increment(f2).increment(f2).increment(f2).print();
//i = 3
}
}
由于increment()方法返回了对当前对象的引用,所以可以在一条语句里对同一个对象进行多次操作,而且通过对比发现这一个功能使用this写起来更简洁。
1、在构造器中调用构造器
public class Flower {
int count = 0;
String s = "初始化一";
Flower(int i){
this.count = i;
}
Flower(String s){
//由于参数的别名跟属性名相同,这里需要使用this来指明调用数据成员
this.s = s;
}
Flower(int i,String s){
this(i);//使用this关键字调用一个构造器
this.s = s;
}
Flower(){
this(50,"初始化二");
}
public static void main(String[] args) {
Flower f1 = new Flower();
System.out.println("count = " + f1.count + ",s = " + f1.s);
//count = 50,s = 初始化二
Flower f2 = new Flower(100,"初始化三");
System.out.println("count = " + f2.count + ",s = " + f2.s);
//count = 100,s = 初始化三
}
}
可以使用this调用一个构造器,但却不能同时调用两个,而且必须将构造器置于当前方法的起始处,否则将编译报错。而且只有构造器才能调用构造器,编译器禁止在当前类的其他方法中调用任何构造器。从上面的示例中,我们也看到了this关键字的另一种用法,就是在初始化数据成员时,可以使用this关键字来区分参数名跟数据成员名相同的情况。
2、static的含义
static方法就是没有this的方法,由于没有this,所以它不是通过“向对象发送消息”的方式来完成调用 的,因此你不能在静态方法内部调用任何非静态方法,但反过来可以,甚至你可以使用类名直接调用静态方法,这是static方法的最主要用途。
五、清理:终结处理和垃圾回收
1、finalize()的用途何在
Java垃圾回收器负责释放无用对象占用内存资源,但由于垃圾回收器只知道释放那些经由new分配的内存,如果你的对象并非使用new获得了一块特殊的内存区域,这里它就不知道如何释放该对象的这块特殊内存。为了应对这种情况,java允许在类中定义一个finalize()的方法,它的工作原理是:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
finalize()方法与C++中的析构函数的区别在于:
① 对象可能不会被垃圾回收;
② 垃圾回收并不等于“析构”;
③ 垃圾回收只与内存有关。即使用垃圾回收器的唯一原因是为了回收程序不再使用的内存,所以对于与垃圾回收有关的任何动作来说(尤其是finalize()方法),它们也必须同内存及其回收有关。
之所以要有finalize()方法,是由于在分配内存时采用了类似C语言的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,本地方法是在Java中调用非Java代码的方式。目前本地方法只支持C和C++,但它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成内存泄漏。free()是C和C++中的函数,所以需要在finalize()中用本地方法调用它。
2、你必须自己实施清理
由于垃圾收集机制的存在,使得Java中没有析构函数,但垃圾回收器的存在并不能完全代替析构函数,如果希望进行除了释放存储空间之外的清理工作,用户还是得明确调用某个恰当的Java方法,这就相当于析构函数,只是没有析构函数方便。
其实,无论是“垃圾回收”还是“终结”,都不保证一定会发生,如果Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。所以我们不能去直接调用finalize()方法,而是要创建其他的清理方法,并且明确的调用它们。
注意:System.gc()是用来强制进行终结动作的。
3、垃圾回收器如何工作
⑴ Java虚拟机的堆模型以及垃圾回收器在堆上的作用
在其他编程语言中,在堆上分配对象的代价十分高昂,但在Java中,垃圾回收器对于提高对象的创建速度,有明显的效果,这意味着,Java从堆分配空间的速度,可以跟其他语言从堆栈上分配空间的速度相媲美。
在某些Java虚拟机中,堆的实现像一个传送带,每分配一个新对象,它就往前移动一格,Java的堆指针只是简单地的移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,在实际过程中在簿记工作方面还有少量的额外开销,但比不上查找可用空间开销大。
其实,Java中的堆也未必完全像传送带那样工作,如果真的是那样的话,势必会导致频繁的内存页面调度(将其移进移出硬盘),频繁的页面调度会显著的影响性能,甚至耗尽内存资源。但由于有垃圾回收器的介入,垃圾回收器在工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样堆指针就可以很容易的移动到更靠近传送带的开始处,也就尽量的避免了页面错误。通过垃圾回收器对对象进行重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
⑵ 垃圾回收器的工作原理
垃圾回收器的工作思想:对于任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区的引用,这个引用链条可能会穿过数个对象层次。由此,如果从堆栈或静态存储区开始,遍历所有引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象所包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络被全部访问为止。当然,你所访问过的对象必须都是“活”的。
在这种工作思想下,Java虚拟机采用的是一种自适应的垃圾回收技术,它分为两种工作模式:
第一种工作模式叫做:停止——复制
它会先暂停程序的运行,然后将所有存活的对象从当前堆复制到另外一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按照前述的方法简单、快速的分配内存了。当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想像成有个表格,可以将旧地址映射到新地址)。
这样做会产生两个问题。一是效率会降低,因为要在两个堆中来回倒腾,从而得维护比实际需要多一倍的空间,某些Java虚拟机对此问题的处理方式是:按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间,如果对象较大,它会占用单独的块;二是在于复制,程序进入稳定状态后,可能只会产生少量垃圾,甚至没有垃圾,这时如果依然将所有内存自一处复制到另外一处,显然就会很浪费。为了避免这种情形,一些Java虚拟机会进行检查,如果没有新垃圾产生,就会转换到另外一种工作模式。
第二种工作模式叫做:标记——清扫
它也会先暂停程序的运行,它所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象将会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就得重新整理剩下的对象。
综上所述,在Java虚拟机中,内存分配以较大的块为单位。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里复制对象了,每个块都用相应的代数来记录它是否还存活。如果块在某处被引用,代数将会被增加,垃圾回收器将对上次回收动作之后新分配的块进行整理,这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作,大型对象仍然不会被复制,只是其代数会增加,内含小型对象的那些块则会被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记——清扫”方式。同时,Java虚拟机会跟踪“标记——清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止——复制”方式。这就是垃圾回收的“自适应”技术。
六、成员初始化
Java尽力保证,所有变量在使用前都能得到恰当的初始化,对于方法的局部变量,如果在使用前没有进行初始化,编译就会报错。而对于类的数据成员,如果是基本数据类型,则会为其赋一个初始值,示例如下:
public class InitialValues {
private boolean bool;
private char c;
private byte b;
private short s;
private int i;
private long l;
private float f;
private double d;
private String str;
public void printValue(){
System.out.println("boolean " + bool);
System.out.println("char " + c);//char的值为0,所以显示的会是空白
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("String " + str);
}
public static void main(String[] args) {
InitialValues values = new InitialValues();
values.printValue();
}
}
打印结果如下:
如果想为某个变量赋初值,那么在定义类的成员变量时直接为其赋值就好了(但C++中不能这么做)。
七、构造器初始化
1、初始化顺序
在类的内部,成员变量定义的顺序决定了它们初始化的顺序,它们将在任何方法(包括构造器)调用之前得到初始化。示例如下:
/**
* author Alex
* date 2018/10/29
* description 用于演示类的数据成员的初始化顺序
*/
public class OrderOfInitialization {
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
}
}
class Demo1{
Demo1(){
System.out.println("初始化Demo1");
}
}
class Demo2{
private Demo1 demo10 = new Demo1();
Demo2(){
test();
demo11 = new Demo1();
System.out.println("初始化Demo2");
}
private Demo1 demo11 = new Demo1();
void test(){
System.out.println("测试");
}
private Demo1 demo12 = new Demo1();
}
打印结果如下:
2、静态数据的初始化
静态数据初始化时,初始化的顺序是先静态对象,而后是非静态对象。静态初始化只有在必要时刻才会进行,如果不引用这个类,那么它不会被初始化,当静态对象初始化之后,它不会再次进行初始化,而非静态对象会进行再次初始化。其实,无论创建多少个此类的对象,静态数据都只占用一份存储区域。注意:static不能用于局部变量,因此它只能作用于域,如果一个域是静态的基本类型域,且没有进行初始化,它将会获得基本类型的标准初始值;如果它是一个引用对象,那么它的默认初始化值就是NULL。示例如下:
/**
* author Alex
* date 2018/10/31
* description 用于模拟静态数据初始化
*/
public class StaticInitialization {
public static void main(String[] args) {
System.out.println("现在初始化一个Demo5");
new Demo5();
}
static Demo4 demo4 = new Demo4();
static Demo5 demo5 = new Demo5();
}
class Demo3{
Demo3(){
System.out.println("初始化Demo3");
}
}
class Demo4{
static Demo3 demo3 = new Demo3();
Demo4(){
System.out.println("初始化Demo4");
}
}
class Demo5{
Demo3 demo3 = new Demo3();
static Demo3 demo33 = new Demo3();
Demo5(){
System.out.println("初始化Demo5");
}
}
打印结果如下:
3、显式的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的静态子句,有时也叫做静态块,它是一段跟在static关键字后面的代码,这段代码仅仅执行一次,当首次生成这个类的一个对象时或者首次访问属于这个类的静态数据成员时,它将被初始化。示例如下:
/**
* author Alex
* date 2018/10/31
* description 用于模拟静态块的初始化
*/
public class StaticBlockInitialization {
public static void main(String[] args) {
Demo7.demo60.f();
}
}
class Demo6{
Demo6(){
System.out.println("初始化Demo6");
}
void f(){
System.out.println("成员方法f");
}
}
class Demo7{
static Demo6 demo60;
static Demo6 demo61;
static {
demo60 = new Demo6();
demo61 = new Demo6();
}
}
打印结果如下:
八、数组初始化
数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列,它使用方括号[ ]来定义和使用。如下:
int[] arr;
1、数组的三种初始化方法
编译器不允许指定数组的大小,当你创建了一个对数组的引用,这时并没有给数组对象本身分配任何空间,为了给数组分配相应的存储空间,必须写初始化表达式。对于数组,初始化表达式可以出现在代码的任何地方,也可以使用一种特殊的初始化表达式,这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配等价于使用new。示例如下:
public static void main(String[] args) {
//int型数组进行特殊的初始化
int[] arr1 = {10,20,30,40,50};
//声明一个int型数组引用
int[] arr2;
arr2 = arr1;
for(int i=0;i<arr2.length;i++){
arr2[i] += 1;
}
for(int j=0;j<arr1.length;j++){
System.out.print(arr1[j]+" ");
}
System.out.println();
//打印结果如下:
//11 21 31 41 51
}
如上所示,在Java中允许将一个数组赋给另一个数组,这样做其实只是复制了一个引用而已,当改变数组2的值时,也改变了数组1的值,因为它们的引用指向的是同一块存储空间。length是数组的固有成员,它的计数是从0开始的,所以数组的最大下标数是length-1。
如果在编写程序时并不能确定在数组里需要多少个元素,可以用new在数组里创建元素进行初始化。示例如下:
public static void main(String[] args) {
example2();
}
static void example2(){
int[] arr;
//用47作为种子生成随机数(使用种子同一次生成的随机数是一样的,但不同次是不一样的)
Random random = new Random(47);
//生成0-10之间的int型随机数设置为数组的长度
arr = new int[random.nextInt(10)];
System.out.println("数组length = " + arr.length);
System.out.println(Arrays.toString(arr));
//打印结果如下:
//数组length = 8
//[0, 0, 0, 0, 0, 0, 0, 0]
}
注意:先声明arr,再使用arr = {}方式进行初始化是不允许的,只能在声明的同时进行初始化才可以使用int[] arr={}的形式,当然在声明的时候使用new int[]的方法进行初始化也是可以的,但必须指明数组的长度,如:int[] arr = new int[10],如果不指明长度,编译就会报错。示例如下:
public static void main(String[] args) {
example3();
}
static void example3(){
int[] arr = new int[10];
Random random = new Random(47);
for(int i=0;i<arr.length;i++){
arr[i] = random.nextInt(100);
}
System.out.println("数组length = " + arr.length);
System.out.println(Arrays.toString(arr));
//打印如果如下:
//数组length = 10
//[58, 55, 93, 61, 61, 29, 68, 0, 22, 7]
}
//这种初始化方法跟上面一种是一样的
下面来看看第三种初始化方式,其实是第一种和第二种结合的方式。示例如下:
public static void main(String[] args) {
example4();
}
static void example4(){
int[] arr = new int[]{10,20,30,40,50};
System.out.println("数组length = " + arr.length);
System.out.println(Arrays.toString(arr));
}
//打印如果如下:
//数组length = 5
//[10, 20, 30, 40, 50]
2、可变参数列表
有了可变参数,就可以不用显式的编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组,你获取的仍然是一个数组,所以可以使用foreach进行迭代。可变参数列表主要用于在不可变的参数后面有可选的尾随参数。在可变参数列表中可以使用任何类型的参数,包括基本数据类型。示例如下:
/**
* author Alex
* date 2018/11/1
* description 用于演示可变参数列表
*/
public class VariableParams {
public static void main(String[] args) {
VariableParams var = new VariableParams();
//可变参数列表
var.example2();
var.example2(1, 2, 3);
var.example2(10, 25.5, 'c', "测试");
//打印结果如下:
//1 2 3
//10 25.5 c 测试
//重载有可变参数列表的函数
var.example3("one","two");
var.example3(1,"one","two");
//打印结果如下:
//one two
//1 one two
System.exit(0);
}
void example2(Object... args) {
for (Object obj : args) {
System.out.print(obj + " ");
}
System.out.println();
}
void example3(int i,String... args){
System.out.print(i + " ");
for (String str:args){
System.out.print(str + " ");
}
System.out.println();
}
void example3(String... args){
for (String str:args){
System.out.print(str + " ");
}
System.out.println();
}
}
九、枚举类型
枚举类使用enum作为关键字,由于枚举类类型的实例是常量,因此按照命名惯例它们都是大写字母表示。同时编译器会自动创建ordinal()方法和values()方法用于操作枚举类。示例如下:
/**
* author Alex
* date 2018/11/1
* description 使用一星期来演示枚举类的使用
*/
public enum WeekEnum {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
public static void main(String[] args) {
//编译器自动创建的方法
//static方法values()可以获取枚举类的所有枚举值
//方法ordinal()可以获取当前枚举对象的索引
WeekEnum[] values = WeekEnum.values();
for(WeekEnum weekEnum:values){
System.out.print(weekEnum.ordinal() + " " + weekEnum + " ");
}
System.out.println();
//打印结果如下:
//0 MONDAY 1 TUESDAY 2 WEDNESDAY 3 THURSDAY 4 FRIDAY 5 SATURDAY 6 SUNDAY
}
}
我们还可以在枚举类中定义自己想要的关于枚举对象的描述或其他附加信息。示例如下:
/**
* author Alex
* date 2018/11/1
* description 使用一星期来演示枚举类的使用
*/
public enum WeekEnum {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");
private String notes;
WeekEnum(String notes) {
this.notes = notes;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
public static void main(String[] args) {
WeekEnum friday = WeekEnum.FRIDAY;
switch (friday.ordinal()){
case 0:
System.out.println("今天" + friday.getNotes());
break;
case 1:
System.out.println("今天" + friday.getNotes());
break;
case 2:
System.out.println("今天" + friday.getNotes());
break;
case 3:
System.out.println("今天" + friday.getNotes());
break;
case 4:
System.out.println("今天" + friday.getNotes());
break;
case 5:
System.out.println("今天" + friday.getNotes());
break;
case 6:
System.out.println("今天" + friday.getNotes());
break;
}
}
}