Java编程思想笔记二:初始化与清理


C++ 引入了构造器的概念,这是一个在创建对象时被自动调用的特殊方法。Java 中也采用了构造器,并额外提供了“垃圾回收器”,对于不再使用的内存资源,垃圾回收器能自动将其释放。《Java编程思想》花费大量篇幅介绍了对象的初始化和清理,所以下面的内容也比较多,但其实这些只是关于初始化和清理的冰山一角,后面还会有更深入的内容。

一、Java 的构造器

Java 在创建一个对象时,会自动调用构造器对对象进行初始化。构造器有如下特点:

  1. 构造器和对象同名;
  2. 不带任何参数的构造器称为默认构造器(无参构造器),构造器也可带形式参数,以便指定如何创建对象;
  3. 在对象中如果不主动定义构造器,编译器会自动帮我买创建一个默认构造器;
    4. 如果已经定义了一个构造器(无论是否有参),编译器就不会帮你自动创建默认构造器;
  4. 一个对象中可以包含多个构造器,要用到方法重载

第 4 点很重要,之前遇到一个同事给我提供了一个接口让我调用,他定义的接口返回结构体 Response 里定义了一个带参数的构造器供其他调用方使用:

public class Response {
    Response(DatasourceParam param) {
        // ...
    }

    @JsonProperty("ResourceId")
    private String resourceId;
}

可是我只想简单实用(也满足大多数人的使用习惯):

Response rsp = new Response();

由于双方是两个项目,自己的项目里编译并不会报错,只有在运行时调用的时候才暴露出这个问题,提示缺少 Response 的无参构造器,所以,我们定义接口返回对象时,如果自定义了带有参数的构造器,没有特殊条件限制的话,最好再定义一个无参构造器,方便调用方使用

二、方法重载

概念

方法重载是指一个对象里定义了多个名称相同、但是参数类型列表不同的方法(可以是类型不同,也可以是顺序不同)。值得注意的是,返回值不同不能作为方法重载的依据。例如我定义了两个方法:

// 方法1
void f() {}
// 方法2
Integar f() { reture 1; }

我如果这样调用:

f();

这可能是调用的是方法1,本身就没有返回值;也可能是调用的方法2,忽略了返回值。编译器不知道我们到底调用的是哪个方法,在编译时就会报错。

类型提升

很多时候,定义的重载方法参数如果是基本类型,实际调用者传入的参数类型和定义不一致,这又会发生什么呢?

例如:

public class PrimitiveOverloading {
    void f1() { printnb("f1()"); }
    void f1(float x) { printnb("f1(float)"); }
    void f1(long x) { printnb("f1(long)"); }

    void f2() { printnb("f2()"); }
    void f2(float x) { printnb("f2(float)"); }
    void f2(int x) { printnb("f2(int)"); }

    void testInt() {
        int i = 1;
        f1(1);
    }

    void testChar() {
        char x = 'x';
        f1(x);
    }

    /*
    void testLongError() {
        long l = 1L;
        f2(l);
    }
    */

    void testLong() {
        long l = 1L;
        f2((int)l);
    }
}

public static void main(String[] args) {
    PrimitiveOverloading pol = new PrimitiveOverloading();
    pol.testInt();
    pol.testChar();
    // pol.testLongError();
    pol.testLong();
}

f1(long)
f1(long)
f2(int)

testInt() 的方法 f1(1) 调用了重载函数 f1(long),没错,**如果传入的数据类型小于方法中声明的形参类型,实际数据类型就会被提升。**int 类型会被提升为 long 调用 f1(long)。

但是,testChar() 方法中的 f1(x) 也调用了重载函数 f1(long),这是为什么?《Java编程思想笔记一:基本概念》已经提到过,char、byte 和 short 如果在找不到恰好接收对应类型的方法时,会先提升至 int 型。本例中 char 先提升至 int,然后 int 型又提升至 long 型。

那么,如果传入的数据类型大于方法中声明的形参类型,会出现什么情况呢?

我尝试去掉注释调用 testLongError(),编译报错了,提示“不兼容类型,从 long 转换到 int 可能会有损失……”。只有先显式的进行类型转换,转换为某个重载函数匹配的类型,才能通过,如案例中的 testLong() 方法。

三、this 关键字

为什么需要 this 这个东西

首先要搞明白下面这个问题:如果同一类型 Banana 的两个对象 a 和 b,都调用这个类的同一个方法 peel,编译器在执行类的方法时是如何知道当前执行的对象是 a 还是 b 的?

class Banana { void peel(int i) { /* ... */ } }

public class BananaPeel {
    public static void main(String[] args) {
        Banana a = new Banana(),
               b = new Banana();
        a.peel(1);
        b.peel(2);
    }
}

简单直观的理解,编译器背后在调用 a.peel(1) 和 b.peel(2) 时是这样做的:

Banana.peel(a, 1);
Banana.peel(b, 2);

编译器会“偷偷”把这个对象的引用透传到方法内部,这样编译器在方法调用时就知道具体是哪个对象调用的了。

但是这是编译器偷偷干的事情,程序员可不能这么干,那如果你想让某个方法返回执行对象本身时,怎么办?-- this 关键字就为我们提供了这个功能,this 就可以代表调用对象本身,你就可以这样写:

public class Leaf {
    int i = 0;
    Leaf increment() {
        i++;
        return this;
    }
}

this 有哪些用途

  1. this 关键字只能在方法内部使用;
  2. 返回当前对象;
  3. 构造器中调用构造器,以避免重复代码;
  4. 方法入参和数据成员名称形同时,使用 this 指明数据成员,避免歧义。

下面是一个构造器中调用构造器的示例:

public class Flower {
    int petalCount = 0;
    Flower(int petals) {
        petalCount = petals;
    }
    Flower(String s, int petals) {
        this(petals);
    }
}

注意,构造器必须至于最开始处,否则编译器会报错!

static 关键字

static 方法(静态方法)就是没有 this 的方法,具有全局函数的语义。在 static 方法内部不能调用非静态方法。

四、清理

Java 如何清理内存

Java 的资源清理可以概括为三种情况:

  1. 基本类型的内存分配在堆栈上,不需要清理,编译器会自动清理;
  2. 对象都是通过 new 来创建的,内存分配在堆上,需要垃圾回收器清理;
  3. 通过其他方式在堆上分配的内存,需要主动调用 finalize() 方法清理。

对于第 2 条,我们要记住,当我们不再需要某个对象时,垃圾回收器并不会立即回收。只要程序没有濒临空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直都没释放创建的任何对象的存储空间,则随着程序的退出,这些资源也会全部交还给操作系统。

对于第 3 条,只有在 Java 中调用非 Java 代码时才会发生,例如调用 C 的 malloc() 函数分配了内存,那就需要在 finalize() 方法中调用 C 的 free() 函数来释放内存,否则会发生内存泄露。

垃圾回收器如何工作

Java 虚拟机中的堆结构就像一个传送带,最上面有一个“堆指针”,每分配一个对象“堆指针”就会往前移动一格,但是如果一直这样简单进行,堆指针可能会频繁跨页,会导致频繁的页面调度。所以 Java 虚拟机工作时,一面回收空间,一面使堆中的对象紧密排列,这样“堆指针”才更容易移动到传送带开始处。

1.最简单的垃圾回收技术

有很多系统采用 “引用计数法” 进行垃圾回收,即每个对象都包含一个引用计数器,当有引用连接至对象引用数加1,引用离开作用域引用数减1,垃圾回收器发现对象的引用数为0时回收对象。

但是这种技术存在一个问题:无法解决循环引用的问题。

有一种解决循环引用的方法,就是追溯每个存活对象,直至找到其存活在堆栈或静态存储区中的引用;然后反过来,遍历所有的引用,追踪它所引用的对象;然后遍历对象包含的所有引用……

这样多次反复,直到 “存活在堆栈或静态存储区中的引用” 所形成的网络全部被访问为止。Java 虚拟机正式基于这种思想,设计了多种工作模式,按需进行切换。

2.垃圾回收器工作模式

  • 停止-复制(stop-and-copy)

先暂停程序运行,将所有存活的对象从当前堆复制到了一个堆,紧密排列,没有复制的全部时垃圾。但是有两个问题:

1.需要多一倍的空间,复制需要在两块分离的堆之间来回倒腾,效率很低;
2.如果程序只产生少量垃圾,这样的持续复制很浪费。

为解决问题1,有些虚拟机的处理方式是:按需从堆中分配几块较大内存,复制仅发生在这些较大内存之间。为解决问题2,有些虚拟机又引入了标记-清扫的工作模式。

  • 标记-清扫(mark-and-sweep)

遍历所有引用,标记所有存活的对象,没有被标记的对象就是需要回收的。

  • 代数(generation count)

上述停止-复制模式中,为了解决问题1,将对象拷贝限制在几块较大内存之间,对于一些较大的对象,甚至会单独占用一个“块”。Java 虚拟机对每个“块”都有相应的代数来记录它是否还存活,通常如果块在某处被引用,代数就会增加。

垃圾回收器会定期进行完整的清理动作,大型对象不会被复制,只是增加其代数值;包含小型对象的那些块则被复制并整理。

Java 虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就会切换到“标记-清扫”模式;同样,Java虚拟机也会监视“标记-清扫”模式,如果堆空间出现很多碎片,就会切换回“停止-复制”模式。所以,我们可以称 Java 垃圾回收器为“自适应的、分代的、停止-复制、标记-清扫”式垃圾回收器。

即时编译器(Just-In-Time,JIT)

Java 虚拟机中提供的附加技术,用以提升速度。它可以把程序全部或部分翻译成本地机器码,程序运行速度因此提升。当需要装载某个类时(通常是在为该类创建第一个对象),编译器会先找到其 .class 文件,然后将该类的字节码装入内存。

新版 JDK 中的 Java HotSpot 技术就是采用类型方法,代码每次被执行的时候都会做一些优化,所以执行次数越多速度越快。

五、成员初始化

Java 对变量的初始化原则

  1. 基本类型强制程序员提供初始值;
  2. 类的每个基本类型数据成员保证都会有一个初始值;
  3. 类里定义的对象引用,如果不进行初始化,系统会初始化为 null。

构造器初始化顺序

  1. 在类内部,变量定义的先后顺序决定了初始化顺序,即使变量散步于方法定义之间,仍然会在方法调用之前被初始化;
  2. 在类内部,如果存在静态对象,则先初始化静态对象,后初始化“非静态”对象。

对象的创建过程

以名为 Dog 的类为例:

  1. 即使没有使用 static 关键字,构造器实际也是静态方法。因此,首次创建类型为 Dog 的对象时,或者 Dog 类的静态方法/静态域被访问时,Java 解释器必须找到 Dog.class 文件;
  2. 载入 Dog.class 文件,执行所有静态初始化的动作(因此,静态初始化只在 class 对象首次加载的时候进行一次);
  3. 用 new Dog() 创建对象,在堆上为 Dog 对象分配内存;
  4. 这块存储空间清零,为所有基本类型数据设置默认值;
  5. 执行所有出现于字段定义处的初始化动作;
  6. 执行构造器(这里如果涉及继承,将很复杂)。

六、枚举类型

Java 也有枚举类型(enum),而且功能比 C/C++更加完备。

由于枚举类型的实例是常量,因此按照命名惯性它们都用大写字母表示(如果一个名字有多个单词,之间用下划线隔开)。

enum 可以和 switch 搭配使用:

public enum Spiciness {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

public class Burrito {
    Spiciness degree;
    public void describe() {
        switch(degree) {
            case NOT:
                System.out.println("not spicy at all");
                break;
            case MILD:
            case MEDIUM:
                System.out.println("a little hot");
                break;
            case HOT:
            case FLAMING:
            default:
                System.out.println("maybe too hot");
                break;
        }
    }
}

后面还会记录更多的学习内容,同步分享到公众号 @红薯的Java私房菜 欢迎关注。
红薯的Java私房菜
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红薯的Java私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值