第五章 初始化与清理

第五章 初始化和清理

1. Java中类的初始化

  1. 对象的初始化:在对象可操作前之前通过调用构造器进行初始化

    class DemoClass{
        DemoClass(/*xxx*/){
            //构造器名称与类型名称相同,没有返回值
        }
    }
    
  2. 方法重载:在同一个类中的方法形参不同但是方法名称相同的方法是重载。

    为什么不将返回值类型纳入重载的考察范围,因为返回值可以进行手动忽略(调用方法,但忽略返回值;这种调用方式称为:为了副作用而调用),所以返回值不一定有效,所以不能纳入作为判断重载的依据。

  3. 使用基本类型作为参数的重载:如果有不需要通过类型自动转化就能运行的重载方法,则运行该方法,反之使用需要最少转化的重载

    注意,这里使用并不清晰,所以要避免进行有多个功能都是最小转换的重载定义

  4. Java中的默认构造器就是直接将对象对应的内存空间完全置零(而数据中的0会被解释为基础类型的自动化初始值以及引用类型的null

  5. 为什么定义了一个构造器之后,默认构造器就会失效:因为如果不失效则提供了一种破坏对象的创建逻辑的创建方式,影响创建对象的完整性与安全性

  6. 本书中可以理解成: static 方法是没有 this的方法 static 是为类而创建的,不要任何对象。

    • static 方法中不能调用 非static 方法,可以访问其他static 方法和 static 属性; 而 非static 方法可以调用 static 方法和 static 属性;
    • 如果代码中出现了大量的 static 方法,则该重新考虑自己的设计

2. Java中对象的清理:终结处理与垃圾回收

  1. Java中提供一个接口(finalize())用于进行终结处理,其正常使用的环境局限在Java中通过**本地方法机制** 调用了C或者C++对象的情况。由于本地方法中创建的对象没法使用 JavaGC进行垃圾回收,所以需要手动设置一个 finalize() 函数,在进行 GC 的时候首先进行自定义的终结处理步骤。

  2. 终结处理接口在对象被触发GC的时候发生调用吗,还有一个特殊的用途,在用于对象回收的时候验证对象是否处于可回收状态(可用于对bug的发现)。

    • 注意: 由于 finalize() 是在触发GC 的时候自动运行,所以当内存足够大时,可能永远也无法触发 GC , 因此不进行强制 GC 的时候,这个函数是否被调用是不确定的。
    class Book{
        // 归还状态
        private boolean isReturned = false;
        
        @Override
        protected void finalize(){
            if(!isReturned){
                 System.err.println("Error: 书籍未被归还记录就已经被删除");
            }
        }
    }
    
    public class MainClass{
        public static void main(String[] args){
            Book novel = new Book();
            //... 某些业务操作
            novel = new Book(); // 覆盖原本的对象, 使得其可以被回收 
            System.gc(); // 强制系统进行一次回收操作
        }
    }
    /**
    output:
    Error:书籍未被归还记录就已经被删除
    */
    
  3. Java 中是没有析构函数的,内存的使用会被 GC 系统自动回收,但是某些情况下需要在对象析构前进行一些重要的操作,例如:文件关闭、网络连接关闭等。这些功能在 Java 中需要以普通功能的形式在程序逻辑中进行手动处理,不能依赖于 GC 。例如文件的 close() 函数就是这一类

  4. Java 中的 GC 机制

    1. Java中 GC 功能的主要效果有两个:找到不再使用的对象进行删除对于正在使用的对象进行重新组织以获得连续的内存空间
    2. 引用计数:简单且很慢的回收机制,没有在 Java中使用
      • 基本逻辑: 在每一个对象上都有一个计数器,当发生引用时,计数器就加1,如果引用被覆盖或者删除时则计数器减1。当一个对象的计数器为0时,就进行回收。
      • 设计问题:当出现对象之间循环引用时,被循环的整个环的计数器都无法为0,也就无法被析构,造成了实质性的内存泄漏。虽然可以通过其他计数判断是否成环,但是由于进行判断成环的图遍历效率过低,所以不能进行使用。
    3. Java 实际使用的 GC 的基本思想
      1. 有意义的对象是活动的对象,活动的对象意味着从目前的变量中可以通过某种方式访问到它。
      2. 要找出所有的活动对象,则进行以一个栈中变量为开始节点的广度优先的图便利
      3. 对于每个被找到的活动对象则在上面打上记号,基于所有最新打上记号的对象,寻找其引用没有打上记号的下一批对象。
      4. 由于是通过打记号的方式进行遍历,不涉及路径的判断,所以对于循环引用可以进行很好的处理。
    4. Java 的自适应垃圾回收技术:
      1. 上面的基本思想只实现了找到未使用的对象并删除的功能,没有完成使得空余内存合并为连续内存的任务
      2. 自适应垃圾回收技术是一种根据不同情况使用不同的内存整理方式的技术,使得在获得较好内存整理效果的同时不会占用过多的运行志愿
      3. 自适应垃圾回收的第一种策略是:停止-复制策略:
        1. 当有大量垃圾积累或者内存碎片过多的时候,使用本策略。
        2. 先暂停正在运行的进程,标记所有的活动对象,在内存中新建一个空白堆,将所有的活动对象一个接着一个复制到空白堆中,对虚拟机中所有的引用变量进行重新映射,删除原先堆中的所有数据。
        3. 这种垃圾回收机制是前台的垃圾回收,非常消耗资源。为了提高这种垃圾回收的效率,将整个程序的堆分为几个小堆,对每个小堆进行单独的垃圾回收,提高内存的利用率和并行度。
      4. 自适应垃圾回收的第二种策略是:标记-删除策略:
        1. 当程序进入到稳定运行状态的时候,可能只会有较少的内存垃圾出现,甚至没有内存垃圾,这个时候使用需要复制所有活动对象的停止复制就非常不划算。
        2. 虽然在清理相同大小的内存空间时,标记-删除策略相比停止-复制更慢,但是由于其不进行活动对象的复制,所以当垃圾很少的时候其相比停止-复制而言效率更高。
        3. 其算法同样是以广度优先遍历的方式对所有的活动对象进行标记,当标记完成后,对于所有不活动的对象进行删除。虽然会暂停程序并产生内存碎片,但是由于垃圾总量很少,所以对于程序运行影响不大
      5. Java自适应垃圾回收策略的转换:
        1. 当出现较大的内存不足或者是较多的内存碎片的时候,通过暂停-复制策略进行垃圾回收。
        2. 当长时间没有进行垃圾回收或者垃圾总量较少的时候,使用标记-删除策略进行垃圾回收。
    5. Java 其他提高运行效率的机制:
      1. Java Just in time 编译技术:通过将本地程序全部或者部分转化为机器码减少代码的大小,提高运行效率(因为类解释执行会需要很多不必要的代码)
      2. Java 惰性评估机制:编译器只在需要的时候才进行源码的编译,从来不执行的代码不会编译为机器码。新版Java 的HotSpot 技术也是基于此进行修改的:代码每次执行的时候,根据被执行到的内容进行动态的编译和替换,代码运行次数越多,编译器就越清楚那部分代码更重要,也就能更优化对应代码 - 代码执行的次数越多,代码优化程度越高,代码运行速度越快

3. Java 中成员初始化

  1. 类成员可以在定义的时候附上默认值,这被称为指定初始化

  2. 在进行类成员指定初始化的时候,可以调用本类型的函数进行初始化(包括static和非static)。

    class Trial1{
        private int a = 1;
        private int b = a+1;
        private int c = func();
        private int d = staticfunc(this);
        
        private int func(){
            return this.a + this.b;
        }
        private static int staticfunc(Trial1 self){
            return self.a + self.b;
        }
    }
    
  3. 对于静态对象的默认值遵循基础类型以及引用类型的自动化初始值,也就是如果是对象,其初始值为null。

  4. 需要注意的是:静态字段的初始化遵循必要原则,如果某个类没有被任何的实例化并且没有调用该静态字段的代码,则这个字段不会进行初始化。只在第一次实例化对象或者在第一次调用字段的时候进行初始化

  5. 在一个对象内部的初始化顺序为:静态成员和static块 --> 普通成员和非static 块 --> 构造函数

  6. 详细的实例化顺序为:

    1. Java 解释器寻找类路径
    2. 载入.class 文件并执行静态对象初始化
    3. 对于非静态对象分配内存空间
    4. 对空间进行清零
    5. 执行处于字段定义中的指定初始化
    6. 执行构造器
    7. 返回被实例化的对象引用
  7. static块 初始化:

    public class Trial{
        static int i;
        static { // 这个是静态子句,用于进行静态对象的复杂初始化. 相对于指定初始化的复杂版
            i = 100;
            System.out.println(i);
        }
    
  8. 非static块 初始化:

    public class Trial{
        int i;
        DefinedClass d;
        { // 和非静态变量的指定初始化一致, 在构造函数之前进行运行
            i = 100;
            if(i == 100){
                d = new DefinedClass("sample input");
            }else{
                d = new DefinedClass();
            }
        }
    }
    

4. Java中数组对象的初始化

  1. 数组虽然看起来像是个基础数据类型,但是其实际是基于Object的对象

    // 虽然 int a[] 也有效, 但是 int[] a 更符合实际
    int[] a = {1,2,3,4,5};
    // 等效于以下代码:
    int[] a = new int[5];
    a[0] = 1;
    a[1] = 2;
    a[2] = 3;
    a[3] = 4;
    a[4] = 5;
    //或者这么写: 
    int[] c = new int[]{1,2,3,4,5};
    // 注意: 如果使用了大括号进行赋值, 则不能在方括号中填写长度
     
    // 如果使用对象的数组可以进行Autoboxing
    Integer[] b = new Integer[]{
                        new Integer(1),
                        new Integer(2),
                        new Integer(3), 
                        4, // 如果不进行boxing会自动转换为Integer类型
                        new Integer(5), // 最后一个逗号会自动忽略
                };
    
  2. 基于数组对象实现可变参数列表

    public static void printArray1(Object[] args){
        for(Object o : args){
            System.out.print(o+" ");
        }
        System.out.println();
    }
    public static void printArray2(Object... args){
        for(Object o : args){
            System.out.print(o+" ");
        }
        System.out.println();
    }
    // printArray1 和 printArray2 在效果上是完全等效的, 只是调用和定义的语法不同
     
    public static void main(String[] args){
        printArray1((Object[])new Integer[]{1,2,3,4});
        // printArray1(new Integer(1),new Integer(2),new Integer(3),new Integer(4)); // 这种写法不正确
    	//printArray1(); // 这种写法不正确
        
        printArray2((Object[])new Integer[]{1,2,3,4});
        printArray2(new Integer(1),new Integer(2),new Integer(3),new Integer(4));
        printArray2();
    }
    
  3. 由于可变参数导致的模糊性问题:

    static void f(float f, Character... args){...}
    static void f(Character... args){...}
    public static void main(String[] args){
        f(1,'a'); // 因为有不需要转换的函数, 所以没有报错
        f('a','b'); // ERROR: JavaMainTest 中的方法 f(float,java.lang.Character...) 和 JavaMainTest 中的方法 f(java.lang.Character...) 都匹配
        // 因为 (char)'a' -> (int)97 -> (float)97.0 有自动转换功能 -- 来自Autoboxing
    }
    // 应该改写为: 通过非可变参数确定功能调用, 避免autoboxing
    static void f(float f, Character... args){...}
    static void f(Character c, Character... args){...}
    
  4. 所以在 Java 中使用可变参数需要非常谨慎,避免与其他功能产生模糊。

5. 枚举类型与switch

  1. 简单示例

    //: Spiciness.java
    public enum Spiciness{
        NOT, MILD, MEDIUM, HOT, FLAMING
    }
    //:~
     
    //: Main.java
    //...
    public static void main(String[] args){
        Spiciness tmp = Spiciness.HOT;
        System.out.println(tmp); // output: HOT //会直接打印出常量名称
        for(Spiciness s: Spiciness.values()){ // 遍历所有常量
            System.out.println(s + ", ordinal" + s.ordinal());
            // 打印出常量名称以及对应的整形数值
            // 这里的整形数值是从0开始的递增整数
        }
        
        // 将枚举类型用于switch条件
        switch(tmp){
            case NOT:
                //...
                break;
            case MILD:
            case MEDIUM:
                //...
                break;
            case HOT:
            case FLAMING:
            default: 
                // ...
        }
    }
    //:~
    

参考:Java编程思想(Thinking in Java)通篇阅读笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值