资讯专栏INFORMATION COLUMN

《Java编程思想》笔记5.初始化与清理

wangym / 1153人阅读

摘要:点击进入我的博客初始化和清理是编程安全的两个问题。延续了中的构造器确保初始化,并引入了垃圾回收器管理和释放内存。用构造方法确保初始化构造方法和类名相同。用于强制进行终结动作。载入该文件,静态数据进行初始化,执行静态代码块。

点击进入我的博客

初始化和清理是编程安全的两个问题。Java延续了C++中的构造器确保初始化,并引入了“垃圾回收器”管理和释放内存。

5.1 用构造方法确保初始化

构造方法和类名相同。原因是尽量减少和成员名称冲突;并且调用构造方法是编译器的责任,名称相同编译器才会知道应该调用哪个方法。

由于构造方法和类名相同,所以构造方法不适应于首字母小写的约定规范。

构造方法有默认的无参构造方法,也可以带参数。

构造方法没有返回值,请注意这跟void不同。

5.2 方法重载

方法重载是指方法名相同,但是参数的个数、类型和顺序不同。

由于构造方法必须和类名相同,即方法名已经确定,但想要用多种方式(参数)创建一个对象,就必须引入方法重载

方法重载不仅适用于构造方法,还适用于其他方法。

warningfunc(int i, String str)func(String str, int i)参数顺序不同构成重载,但请尽量避免这种写法。

构成重载深层次的原因:只要这两个方法编译器能区分开来,在调用的时候知道要调用的是哪一个,不会产生混淆,这两个方法就构成重载。

基本类型的重载

对于byteshortintfloatdouble如果找不到对应基本类型方法,则会按照向上转化的路线找匹配的方法

如果是char,如果找不到对应的基本类型方法,直接从int向上找匹配的方法。

    public static void print(char c) {
        System.out.println("char: " + c);
    }

    public static void print(byte b) {
        System.out.println("byte: " + b);
    }

    public static void print(short s) {
        System.out.println("short: " + s);
    }

    public static void print(int i) {
        System.out.println("int: " + i);
    }

    public static void print(long l) {
        System.out.println("long: " + l);
    }

    public static void print(float f) {
        System.out.println("float: " + f);
    }

    public static void print(double d) {
        System.out.println("double: " + d);
    }
返回值不同无法区分两个方法
void f() {};
boolean f() {
    return true;
};
// 只调用f()无法区分是哪个方法
5.3 默认构造器

如果你的类中没有构造器,则编译器会帮你自动创建一个默认构造器。可以通过反编译.class文件来验证这一点。

如果你自己定义了一个构造方法,则编译器不会帮你创建默认构造器。

5.4 this关键字

下述代码中,有两个对象a1a2,按照面向过程的函数形式,在执行func()函数的时候,怎么知道是被a1a2调用呢?为了能用面向对象的语法来编写代码,编译器做了一些幕后工作。它暗自把“所操作的对象”作为第一个参数传递给func()函数,即func(a1)。这是内部的表示形式,我们并不能这样写代码。

public class Test {
    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
        a1.func();
        a2.func();
    }
}

class A {
    void func() {}
}

this关键字只能在方法内部使用,表示对“调用方法的那个对象的引用。”

有人喜欢将this放到每个方法调用和字段引用前,千万不要这么做!

当需要返回当前对象的引用时,可以通过return this;

5.4.1 在构造器中调用构造器

可以通过this(params);来调用其他的构造方法

可以通过this调用一个其他的构造方法,但不能调用两个及以上

通过this调用其他的构造方法必须放到该构造方法的第一行

构造方法不能通过this调用自己

    public Test(int i) {
        System.out.println("Test " + i);
    }

    public Test(String str) {
        System.out.println("Test " + str);
    }

    // (1)
    public Test() {
        this(1);
        // this("imbug");
        System.out.println("Test");
    }

    public static void main(String[] args) {
        Test test = new Test();
    }
5.4.2 static方法

static方法中不能使用this关键字

static方法中不能调用非静态方法,反之则可以

5.5 清理:终结处理和垃圾回收

Java的垃圾回收器(GC)负责回收无用对象占据的内存资源

假定你的对象(不是通过new)获得了一块“特殊”的内存区域,由于GC只知道new分配的内存,所以它不知道如何释放该对象的“特殊”内存区域。为了应付这种情况,Java允许在类中定义一个名为finalize()的方法。

5.5.1 finalize()方法

一旦GC准备释放对象的存储空间,首先调用该方法;并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。即调用该方法但时候,对象还没有被回收。

finalize()方法不是C++中的析够方法

在C++中对象一定会被销毁(代码无Bug),但是在Java里的对象并非总是被垃圾回收。

垃圾回收只与内存相关,也就是说使用GC的唯一原因是为了回收程序不再使用的内存。

上述讨论了,对象可能会获得一块“特殊”的内存区域,这主要发生在JNI本地方法的情况下,本地方法是在Java中使用非Java代码的方式。非Java代码可能会调用C的malloc()来分配存储空间,而且除了free()方法否则其存储空间将得不到释放,从而造成内存泄漏。此时就可以在finalize()中调用free()方法,清理本地对象。

不建议用finalize方法完成“非内存资源”的清理工作,但也可以作为确保某些非内存资源(如Socket、文件等)释放的一个补充。

System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不保证一定会执行。

用户可以手动调用对象的finalize方法,但并不影响GC对finalize的行为,即没有卵用~

finalize()执行流程

5.5.2 你必须实施清理

Java不允许创建局部对象(即堆栈上的对象),必须使用new创建对象。

无论是“垃圾回收”还是“终结”,都不保证一定会发生。

5.5.3 终结条件

如果某个对象的内存可以被安全释放了,例如对象代表了一个打开的文件,那么回收内存前必须保证文件关闭。这个在finalize()中可以检验文件的状态。

System.gc()用于强制进行终结动作。

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        // if(文件未安全关闭)
        System.out.println("error");
    }

    public static void main(String[] args) {
        func();
        System.gc();
    }

    public static void func() {
        Test t1 = new Test();
        Test t2 = new Test();
    }
5.5.4 GC如何工作

更详细内容请看JVM工作原理!!!

GC会整理堆内存空间,因此导致new新建对象时的内存分配速度

引用计数

每个对象都含有一个计数器,当引用连接至对象时+1,引用离开作用域或被置为null时-1。GC遍历全部对象,发现计数器为0的时候就会释放其内存。

优点:简单

缺点:慢、循环引用问题、对象应该被回收但引用计数不为零

引用计数只是为了说明GC的工作方式,但实际上似乎没有任何Java虚拟机实现过。

根搜索算法

原理:每个“活”的对象,一定能追溯到其存活在堆栈或静态存储区之中的引用。

方法:从堆栈和静态存储区开始,遍历所有引用;然后追踪它所引用的对象,然后是这些对象包含的所有对象,反复进行直至“根源于堆栈和静态存储区的引用”所形成的网络被全部访问完为止

停止-复制算法

先暂停程序的运行,然后将全部活的对象从当前堆复制到另一个堆,没有复制的都是垃圾;新堆里的对象在内存中时连续的

不属于后台回收模式,因为要暂停程序的运行

把对象从一个堆复制到另一个堆时,所有指向它们的引用都必须要修正。

效率低的原因(1):需要两个分离的堆,因此需要两倍的内存空间

效率低的原因(2):程序稳定后垃圾很少,即需要存活的对象远大于垃圾数量,此时复制到另一个堆非常浪费。

标记-清扫算法

用根搜索算法找到所有存活的对象并标记(此过程不回收),当全部标记工作完成的时候,清理所有没有标记的对象

缺点(1):导致内存空间不连续

缺点(2):也会暂停程序

分代算法

JVM中,内存以较大的“块”为单位;如果对象比较大,它会占据多带带的块;有了块之后,GC就可以在回收的时候往废弃的块中拷贝对象了

每个块用相应的代数(generation count)来记录是否存活;如果块在某处被引用,其代数会增加;GC会对上次回收动作之后新分配的块进行整理

GC会定期进行完整的清理动作,大型对象不会被复制但是其代数会增加;小型对象的那些块则被复制并整理

自适应、分代的、停止复制、标记清扫方式

JVM会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到标记-清扫模式;同样,JVM会跟踪标记-清扫的效果,要是堆空间出现很多碎片,就会切换回停止-复制模式。

其他附加技术

即使编译器(Just-In-Time JIT):可以把程序全部或部分翻译成机器码来提高运行速度。当需要装载某个类时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择:

让即时编译器编译所有代码:这种操作散落在整个程序的声明周期内,累加起来耗时更长;会增加可执行代码的长度,造成页面调度

惰性评估:意思是即时编译器只在必要的时候才编译代码,这样,从不会被执行的代码也许就压根不会被JIT所编译。

5.6 成员初始化

Java尽量保证:所有变量使用前一定会初始化
局部变量:不会自动初始化,而是编译错误
类成员变量:类的每个基本类型数据成员都保证会有初始值;引用类型为null

指定初始化

定义类成员变量的时候给它赋值——(1)

通过调用某个方法来提供初值——(2)

注意:(2)、(3)不能颠倒顺序,因为存在向前引用。

缺点:这种方式所有成员有相同的属性

public class Test {
    // (1)
    int a = 10;
    // (2)
    int i = f();
    // (3)
    int j = g(i);
    int g(int n) {
        return n;
    }
    int f() {
        return 1;
    }

    public static void main(String[] args) {
        Test t = new Test();
    }
}
5.7 构造器初始化

无法阻止自动初始化的进行,它发生在构造器被调用之前!

5.7.1 初始化顺序

遍历定义的先后顺序决定了初始化的顺序。

5.7.2 静态数据的初始化

静态数据跟非静态数据的默认初值是一致的。

先初始化静态对象,然后初始化非静态对象。

静态初始化只有在必要的时候执行,如创建第一个该类对象或调用静态方法的时候执行。

对象创建过程

在调用该类的静态方法或者首次new对象(构造器其实也是静态方法)的时候,Java解释器查找类路径定位到该类的.class文件。

载入该.class文件,静态数据进行初始化,执行静态代码块。

new对象创建对象的时候,首先在堆内存中为此对象分配足够的内存空间。

把此存储空间清零,即所有非静态基本数据类型置为0,对象类型置为null

执行非静态数据初始化动作。

执行构造器。

5.7.3 显式的静态初始化

即静态代码块。

在调用该类的静态方法或者首次new对象的时候执行,即和静态数据初始化相同的条件,但是发生在静态数据初始化之后

5.7.4 非静态实例初始化

Java中也有被称为实例初始化的语法,用来初始化每一个对象的非静态变量。

实例初始代码块和成员变量的初始化顺序是按照遍历的先后顺序执行的,但两者执行都在构造方法之前。即如果(1)、(2)位置改变,输出会变成213。

这种语法对于支持“匿名内部类”的初始化是必须的

    // (1)
    {
        System.out.println(1);
    }
    // (2)    
    int i = func();
    int func() {
        System.out.println(2);
        return 2;
    }
    // (3)
    Test() {
        System.out.println(3);
    }

    public static void main(String[] args) {
        new Test(); // output 123
    }
5.8 数组初始化

int[] arrint arr[]这两种写法都可以,但更推荐前者。

为了给数组创建相应的内存空间,必须初始化数组的大小;或者初始化的时候直接初始化数组的值(int[] arr = {1, 2, 3}),此时存储空间的分配由编译器负责。

所有数组都有一个固定成员length获知成员数量,但不可以修改这个值。

数组坐标从0开始。

数组中的元素会自动初始化为空值。

5.8.1 可变参数列表

void func(String... args) {}

可变参数列表可以接受不传任何参数,即func()是可行的。

可变列表与自动包装机制可以和谐相处

数组的class

基本数据类型:class、空格、多个(值为数组维数)[、对应数据类型的标识

对象类型:class、空格、多个(值为数组维数)[、大写L、对应数据类型的全路径、;

        System.out.println(new int[0].getClass()); // class [I
        System.out.println(new Integer[0].getClass()); // class [Ljava.lang.Integer;
        System.out.println(new long[0].getClass()); // class [J
        System.out.println(new double[0].getClass()); // class [D
        System.out.println(new int[0][0].getClass()); // class [[I
        System.out.println(new int[0][0][0].getClass()); // class [[[I
        System.out.println(new String[0].getClass()); // class [Ljava.lang.String;
        System.out.println(new String[0][0].getClass()); // class [[Ljava.lang.String;
可变类型引起的重载问题

此段代码编译失败,因为编译器发现有多个方法可以调用。

    public static void main(String[] args) {
        func(1, "a");
        func("a", "b");
    }

    static void func(int i, Character... args) {
        System.out.println("first");
    }

    static void func(Character... args) {
        System.out.println("second");
    }
5.9 枚举类型

枚举常量命名规范:全部大写字母用下划线分割

枚举会自动创建toString()方法

会自动创建ordinal()方法,用来表示枚举常量的声明顺序

枚举可以在switch中使用

public class Test {
    public static void main(String[] args) {
        Color green = Color.GREEN;
        Color red = Color.RED;
        System.out.println(green + "   " + green.ordinal());
        System.out.println(red + "   " + red.ordinal());
    }
}

enum Color {
    RED,
    GREEN;
}
反编译后的Color类

在代码中Enum禁止继承

// final class 禁止继承
final class Color extends Enum
{

    public static Color[] values()
    {
        return (Color[])$VALUES.clone();
    }

    public static Color valueOf(String name)
    {
        return (Color)Enum.valueOf(s2/Color, name);
    }
    // 私有构造方法,所以无法用new创建对象
    private Color(String s, int i)
    {
        super(s, i);
    }

    public static final Color RED;
    public static final Color GREEN;
    private static final Color $VALUES[];

    static 
    {
        RED = new Color("RED", 0);
        GREEN = new Color("GREEN", 1);
        $VALUES = (new Color[] {
            RED, GREEN
        });
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72184.html

相关文章

  • Java编程思想笔记8.多态

    摘要:多态的作用是消除类型之间的耦合关系。编写构造器准则用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。 点击进入我的博客 在面向对象的程序设计语言中,多态是继数据抽象(封装)和继承之后的第三种基本特征。多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态的作用是消除类型之间的耦合关系。 8.1 再论向上转型 对象既可以作为它自己的本类使用,也可以作为它的...

    chinafgj 评论0 收藏0
  • Java编程思想笔记7.复用类

    摘要:在类的构造方法中。对基类构造器的调用必须放到子类构造器的第一行。约定用大写字母下划线命名规范空白空白指被声明为但又未给定初值的域,但可以在构造方法必须在域的定义处代码块或构造器中对进行赋值。 点击进入我的博客 复用代码是Java众多引人注目的功能之一,但要成为极具革命性的语言,仅仅能够复制代码并对之加以改变是不够的,它还必须能够做更多的事情。 7.1 组合 组合语法 就是在当前类中产...

    ningwang 评论0 收藏0
  • Java编程思想笔记01------一切都是对象

    摘要:一引用操纵对象在的世界里,一切都被视为对象。特点创建程序时,需要知道存储在栈内所有数据的确切生命周期,以便上下移动堆栈指针。因为,指向同一块内存空间除了通过对象引用静态变量,我们还可以通过类直接引用静态变量 一、引用操纵对象 在Java的世界里,一切都被视为对象。操纵的标识符实际上是对象的引用, 例如:遥控器与电视的关系。 可以在没有对象关联的情况下,拥有一个引用。没有电视机,也可以拥...

    yuanxin 评论0 收藏0
  • Java编程思想笔记12.通过异常处理错误

    摘要:一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。这种在编译时被强制检查的异常称为被检查的异常。通过获取原始异常。构造器对于在构造阶段可能会抛出异常,并要求清理的类,最安全的做法是使用嵌套的子句。 点击进入我的博客 Java异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更自信:你的应用中没有未处理的错误。 12.1 概念 异...

    Vultr 评论0 收藏0
  • Java编程思想学习录(连载之:始化清理

    摘要:注本文首发于公众号,可长按或扫描下面的小心心来订阅关于构造器与初始化无参构造器默认构造器自己未写编译器帮忙自动创建的若自行定义了构造器无论参数有否,编译器便停止默认创建动作类里的对象引用默认初始化为,基本类型初始化为构造器也是类的静态方法四 showImg(https://segmentfault.com/img/remote/1460000015723687); 注: 本文首发于 ...

    betacat 评论0 收藏0

发表评论

0条评论

wangym

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<