zoukankan      html  css  js  c++  java
  • Java编程思想:第5章 初始化与清理

    随着计算机革命的发展,“不安全”的编程方式已经逐渐成为编程代价高昂的主因之一。

    初始化和清理,正是涉及安全的俩个问题。

    5.1 用构造器确保初始化

    构造器名与类名相同,没有返回值

    5.2 方法重载

    构造器的重载与方法重载

    5.2.1 区分重载的方法

    参数列表的不同(不同顺序也是重载,但不建议这么做)

    5.2.2 涉及基本类型的重载

      void print(char c){System.out.println("The character is "+c); }
        void print(byte b){System.out.println("The byte is "+b);}
        void print(short s){System.out.println("The short is "+s);}
        void print(int i){System.out.println("The int is "+i);}
        void print(long l){System.out.println("The long is "+l);}
        void print(float f){System.out.println("The float is "+f);}
        void print(double d){System.out.println("The double is "+d); }

    1)如果直接传入数值,如 print(5)则5会被当做 int x = 5; print(x)去寻找最合适的print(int)

    2)常量之外的类型会去寻找最合适的类型,没有同类型,但是有更大类型时会类型提升

    3)char是一个特例,如果没有print(char)则char当做int,执行print(int)

    4)需要用cast才能放入小类型的方法里

    5.2.3 用返回值区分重载方法

    void f(){}

    int f(){return 1;}

    这样是不可以的,因为很多时候我们不需要返回值,只是调用而已,那么我们会写:

    f();这样是无法区分它是哪个方法的

    5.3 默认构造器

    如果类里没有构造器,编译器会帮你弄一个。

    有了就不会帮你造一个了。

    5.4 this关键字

    可以用于指代调用对象方法时的“当前对象”,可以解决参数和域重名问题。

    5.4.1 构造器中调用构造器

    this(参数);

    必须放于第一行,且最多只能调用一个。

    5.4.2 static的含义

    static方法就是没有this的方法,它属于类,不属于对象。

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

    Java垃圾回收器负责回收由new创建并且不再被使用的对象。

    特殊情况是:如果一个对象以非new方式获得了一块内存区域,GC就不知道怎么回收这块特殊的内存。Java提供了finalize()方法,它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正回收对象内存。所以finalize()方法可以在垃圾回收时做一些重要的清理工作。

    //此处疑惑感谢@Launcher的细心解答。

    //---------------解答开始

    也就是你有这样一个 class,伪码:

    class A{

       IntPtr m_pBuffer;

        A(){

              m_pBuffer = malloc(1000);

         }

         void finalize(){

                   free(_M_pBuffer);

           }
    }

    A 对象没有用 new ,而是使用 malloc 获得了一块特殊的内存区 m_pBuffer。

    A a = new A(); // 还是使用 new 获得对象 A

    但是对象 a 本身不用 new 获得的特殊内存区 m_pBuffer,不会被 GC 释放,所以你需要实现 finalize,在里面手动释放。

    //---------------解答结束

    这里有一个误区:

    finalize() 与 C++中的析构函数(销毁对象时必须要用到这个函数)相同。

    这是不正确的,C++中对象一定会被销毁,即析构函数一定被调用,而Java里的对象并不总是被垃圾回收。换句话说:

    1)对象可能不被垃圾回收

    2)垃圾回收不等于析构

    5.5.1 finalize()用途何在

    3)垃圾回收只与内存有关。

    Java中无论对象是如何创建的,GC都会回收它的内存,对finalize()的需要限制到一种特殊情况:用了创建对象以外的方式,为对象分配了存储空间。

    之所以要有finalize(),是由于在分配内存时可能采用了类似C语言的做法,这种情况主要发生在使用Java调用本地代码,如用C的malloc()函数系列来分配存储空间,所以需要在finalize()里调用本地代码,使用free()来释放这些内存。

    所以不要过多使用finalize(),它不是进行普通清理工作的合适场所。

    5.5.2 你必须实施清理

    C++中所有对象都会被销毁。包括局部对象(在栈上创建的)和用new创建的对象(delete调用析构函数)。

    Java不允许创建局部对象,必须使用new创建,也没有delete可用,释放对象内存只能由GC来完成,但是它并不保证一定去回收。如果希望在进行释放存储空间之外的清理操作,得明确在finalize()里使用,这等于析构了,但是没析构方便。

    如果JVM没有面临内存耗尽的情况,它是不会浪费时间和资源去执行垃圾回收以恢复内存的。

    5.5.3 终结条件

    因为finalize()方法是在垃圾回收前执行的,所以复写finalize方法可以检测"逻辑上"的这个对象是否应该被终结。借此来发现程序中是否有缺陷。如:某对象代表了一个打开着的文件,而我们规定这种对象回收时必须为关闭状态。为了检测程序是否有缺陷,我们可以复写finalize方法检测是否打开状态,然后在某次finalize中查看是否存在问题,虽然finalize并不一定会调用,但是只要调用了一次我们就知道是否有问题,这才是关键。

    我们可以利用System.gc()来建议做GC操作,但JVM有可能不听我们的。

    5.5.4 垃圾回收器如何工作

    在以前所用过的程序语言中,在堆上分配对象的代价十分高昂,因此容易有JAVA中的对象在堆上分配方式也非常高昂。但实际上垃圾回收器对提高对象创建的速度有非常明显的效果。Java中堆内存的分配方式就像传送带一样,通过移动“堆指针”来完成内存分配,而其中最关键的是垃圾回收器在工作时,一边回收空间,一边使堆中对象紧密排列,从而保证了“堆指针”可以很容易分配内存,实现了高速的无限空间的可供分配堆模型。

    先看其他系统的垃圾回收机制模型:

    1.引用计数,简单但很慢的垃圾回收技术。且难以处理循环引用问题。

    2.更快的模式,从堆栈和静态存储区开始,用引用向下搜索所有对象,以及对象间引用,保证每个活的对象都被搜索到且避免了循环引用问题。

    1和2都是寻找“活”着对象的方式,在基于2的方式之下,Java虚拟机采用“自适应”技术。

    如何处理寻找到的存活对象,取决于不同Java虚拟机的实现。

    A.停止-复制 stop-and-copy, 暂停程序的运行,然后将所有存活的对象从当前堆复制到新的堆里,并且紧密排列,然后就可以简单直接的分配新空间了。当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。问题一:对于这种“复制式”回收器而言,效率会降低,需要在2个堆里倒腾,还要维护比实际需要多一倍的空间。某些Java虚拟机的解决方法是,按需从堆里分配几块较大的内存,复制动作发生在这些大块内存之前。问题二:对于复制,程序进入稳定状态之后,只会产生少量垃圾,甚至没有垃圾,尽管如此,复制式回收器仍然会把所有内存从一处复制到另一处,很浪费。一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式这种模式叫 标记-清扫。一般用途而言,标记清扫的效率很低,但是如果垃圾很少甚至没有,它的速度就很快了。

    B.标记清扫 mark-and-clean, 所依据的思路同样是从堆栈和静态存储区出发,遍历所有引用,进而找出所有存活对象。每当它找到一个存活对象,就会给这个对象一个标记,这个过程不会回收任何对象。只有当标记工作全部完成之后,清理动作才会开始。清理过程中没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器希望得到连续空间的话就得重新整理剩下的对象。

    这里讨论内存分配以较大的“块”为单位的Java虚拟机。严格意义来说,停止-复制是要求释放垃圾对象之前,必须把对象从一个堆复制到新堆里的,这将导致大量的内存复制。此处有了块之后,垃圾回收器就可以往“废弃”的块里复制对象了,每个块有自己的代数记录它是否存活(废弃)。如果块在某处被引用,代数增加,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作-大型对象仍然不会被复制,内含小型对象的那些块则被复制并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器效率很低的话,就会切换到“标记-清扫”方式;同样虚拟机会跟踪“标记-清扫”的效果,要是堆空间出现很多碎片,就会切换回“停止-复制”方式。这就是自适应技术。

    Java虚拟机还有很多其他用于提升速度的附加技术。尤其是与加载器操作有关的,被称为“即时Just-In-Time,JIT”编译器技术。这种技术可以把程序部分或全部翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。当需要装载某个类时,编译器会先找到class文件,然后把字节码装入内存。此时有两种方案可供选择,一是让即时编译器编译所有代码,缺点是这种加载动作散落在整个程序生命周期内,累加起来要花费更多时间;并且会增加可执行代码长度,导致页面调度,从而降低程序运行速度。二是采用惰性评估,执行频繁的方法和代码会采用即时编译,执行次数越多速度越快。

    5.6 成员初始化

    Java尽力保证:所有变量在使用前都得到恰当的初始化。对于方法中的局部变量,以编译错误形式贯彻这种保证。对类成员以默认值执行初始化。

    5.6.1 指定初始化

    可以在申明变量时直接以赋值符号完成初始化。在针对类成员变量初始化时,如果直接给出值,会使得所有对象有相同的值,有时是我们希望的,但有时显得不够灵活。

    5.7 构造器初始化

    利用构造器来初始化可以得到更大的灵活性。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用前发生。在构造器赋值成员变量之前,变量已经被赋予了默认值。----编译器不会强制我们在构造器初始化,因为初始化早已得到了保证。

    5.7.1 初始化顺序

    变量定义的顺序决定了初始化顺序。即使变量可能定义在构造器或其他方法之后,但仍旧会在任何方法(包括构造器)被调用之前得到初始化。

    5.7.2 静态数据的初始化

    动作执行于类加载时,且只执行一次。总结对象创建的过程,假设有个Dog类

    1.构造器是静态方法,只是没有static。所以当首次调用构造器,或首次使用Dog类的静态域或方法时,Java解释器必须找到类路径,定位Dog.class文件。

    2.然后载入class文件,有关静态初始化的所有动作都会被执行。

    3.当用new Dog()创建对象的时候,首先将堆上为Dog对象分配足够的存储空间。

    4.这块存储空间会被清零,这就自动的将Dog对象中所有基本类型设置为默认值0和false,引用类型设置为null

    5.按申明顺序执行所有出现于字段定义处的初始化动作。

    6.执行构造器。

    5.7.3 显式的静态初始化

    即在static块里完成初始化

    5.7.4 非静态实例的初始化

    即无static的块里完成初始化

    总结:

    参照Java 类加载、调用构造器、执行方法的过程

    5.8 数组初始化

     int[] a1;

    如何初始化?

    1. Integer[] a1 = {1,2,3};//只能在申明处用这种方式初始化

    2. Integer[] a1 = new Integer[]{1,2,3};

    3. Integer[] a1;

     a1 = a2;

    可以用赋值让2个数组引用指向同一个数组对象。所以允许存在Integer[] a1;这种只有引用没有指向的情况

    注意:所有数组都有一个固定成员,可以通过它获取数组里包含了多少元素,但是不能修改它。这个成员就是length,我们只能访问0-(length-1)范围,下标检查是自动进行的,相比于C和C++用这些开销来获得安全是值得的。

    5.8.1 可变参数列表

    在参数列表的末尾可以选择传入0-N个参数,会自动变成数组,可以作为可选参数存在

    5.9 枚举类型

     Java SE5开始添加了enum关键字用于创建枚举类型。功能比C、C++完善得多。

    public enum Spiciness{

      NOT, MILD, MEDIUM, HOT, FLAMING

    }

    示例创建了一个名为Spiciness的枚举类型,它有5个常量实例。

    使用时:

    Spiciness howHot = Spiciness.MEDIUM;

    System.out.println(howHot);

    --> MEDIUM

    当创建了enum时,编译器会自动添加一些有用的特性:

    1.toString()用于显示实例名字

    2.ordinal()显示该实例常量声明顺序,从0开始

    3.静态的values() 方法,按照声明顺序产生一个全值数组

    注意:

    A.enum本身并不是新的数据类型,它属于编译器行为,本质上还是类,有自己的方法

    用类分析器分析后:

    public final class study.core.反射.Spiciness extends java.lang.Enum{
        //域
        public static final study.core.反射.Spiciness NOT;
        public static final study.core.反射.Spiciness MILD;
        public static final study.core.反射.Spiciness MEDIUM;
        public static final study.core.反射.Spiciness HOT;
        public static final study.core.反射.Spiciness FLAMING;
        private static final study.core.反射.Spiciness[] ENUM$VALUES;
        //构造器
        private Spiciness(java.lang.String, int);
        //方法
        public static study.core.反射.Spiciness[] values( );
        public static study.core.反射.Spiciness valueOf(java.lang.String);
    }
    B.enum可以配合switch使用

  • 相关阅读:
    url protocol
    wpf webbrowser取消js报错
    c#端口扫描器wpf+socket
    c#协变 抗变
    MTK刷机快捷键
    iTextCharp c#
    wince可用的7-zip
    直播平台搭建与相关资料
    pyinstall
    面向对象常见的术语
  • 原文地址:https://www.cnblogs.com/superzhao/p/4711485.html
Copyright © 2011-2022 走看看