Java 之路 (五) -- 初始化和清理(构造器与初始化、方法重载、this、垃圾回收器、枚举类型)

学习内容:

  • 构造器
  • 方法重载
  • this 关键字
  • 垃圾回收器的清理
  • 初始化问题
  • 枚举类型

这一章内容有一点点多,需要注意的地方也很多。下面就开始我的表演了。


1. 构造器

(1) 概念:

  • 一个创建对象时被自动调用的特殊方法

(2) 作用:

  • 通过构造器,创建对象,并确保对象得到初始化。

(3) 命名:

  • 构造器的名称必须与类名相同。

(4) 特殊:

  • 构造器是一种特殊类型的方法,它没有返回值。但是!它与返回值为空(void)不同。
    • 对于空返回值,方法本身不会自动返回什么,但是可以选择让它返回别的东西
    • 对于构造器,不会返回任何东西。new 表达式返回了对新建对象的引用,但是构造器本身没有返回任何值
  • 不接受任何参数的构造器称为 默认构造器 / 无参构造器
  • 如果类中没有构造器,那么编译器会自动创建 默认构造器;反之,如果已经定义了一个构造器(无论是否有参数),编译器都不会再自动创建 默认构造器。

2. 方法重载

(1) 原因

  • 每个方法要有独一无二的标识符
  • 构造器强制重载方法名:为了让方法名相同而形式参数不同的构造器同时存在。

(2) 重载规则:

  • 具有相同的的方法名
  • 必须有一个独一无二的参数类型列表(包括参数类型,以及参数类型对应的顺序)

(3) 需要注意,涉及基本类型的重载

  • 常数值会被当作 int 值处理
  • 如果传入实参的数据类型 小于 方法中声明的形参的数据类型,那么会将 实参的数据类型提升
    • 特殊的,对于 char 而言,如果没有恰好接收 char 参顺的方法,那么会把 char 提升至 int
  • 如果传入实参的数据类型 大于 方法中声明的形参的数据类型,那么会将 实参的数据类型进行窄化转换

3. this 关键字

(1) 作用

  • 通过 this 关键字,可以在方法的内部获得当前对象的引用。
  • this 只能在方法内部使用,表示对 “调用方法的那个对象” 的引用

(2) 用途 1 - 需要明确指出当前对象的引用

  1. 比如需要返回这个引用

    public class Leaf{
       int i=0;
       Leaf increment(){
           i++;
           return this;
       }
       void print(){
           System.out.println("i = " + i);
       }
       public static void main(String[] args){
           Leaf x = new Leaf();
           x.increment().increment().print();
       } 
    }
    
    //结果为 i = 3
    
    //分析
    //因为 increment()方法中返回了 对象的引用,所以才可以连缀多个 increment() 方法。
  2. 比如将当前对象传递给其他方法

    class person{
       public void eat(Apple apple){
           Apple peeled = apple.getPeeled();
           Ssytem.out.println("Yummy");
       }
    }
    
    class Peeler{
       static Apple peel(Apple apple){
           // ... remove peel
           return apple; // Peeled
       }
    }
    class Apple{
       Apple getPeeled(){
           return Peeler.peel(this);
       }
    }
    public class PassingThis{
       public static void main(String[] args){
           new Person().eat(new Apple());
       }
    }
    
    //输出为 Yummy
    
    //分析
    //Apple 需要调用 Peeler.peel() 方法,为了将自身传递给这个外部方法, Apple 必须使用 this 关键字
  3. 比如初始化成员变量时,避免参数重名造成混淆

    public class Person{
       String name;
       public Person(String name){
           this.name = name;
       }
    }
    
    //this.name 指的是 Person 类的 name 这个成员变量
    //name 指的是 接收的 String 参数 name

(3) 用途 2 - 在构造器中调用构造器

  • 通过 this,可以在一个构造器中调用另一个构造器,避免重复代码

  • 一般来说,单独的 this 关键字指的是 “当前对象”,表示引用;如果为 this 添加函数列表,这就产生了对符合此参数列表的某个构造器的明确调用。

    public class Person{
      String name;
      int age;
      public Person(String name){
          this.name = name;
      }
      public Person(String name,int age){
          this(name);
          this.age = age;
          System.out.println("name : " + name + "; age :" + age);
      }
    }
    
    //如果此时调用 
    Person person = new Person("whadlive",21);
    //那么输出的结果是 name : whdalive; age : 21
    
    //原因
    //首先 new Person("whdalive,21) 调用了 Person(String name,int age) 这个构造器
    //然后内部又通过 this(name) 调用了 Person(String name)。

(4) 关于 static 的问题;

  • static 方法就是没有 this 的方法 – 因为 static 属于 类,而非对象,自然不存在引用,即没有 this
  • static 方法内部不能调用非静态方法;反过来是可以的。

4. 清理:终结处理和垃圾回收

写在最前面,很重要:

  1. 对象可能不被垃圾回收
  2. 垃圾回收并不等于”析构”
  3. 垃圾回收只与内存有关

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

  • 特殊情况:

    假定对象(并非使用 new) 获得了一块特殊的内存区域(比如在 Java 中使用 C 并且通过 malloc 分配空间),而 垃圾回收器只能释放由 new 分配的内存,所以此时这块特殊的内存区域无法释放。

    应对方法:Java 中定义了 finalize() 方法

    • 当垃圾回收器准备好释放对象占用的存储空间,首先会调用 finalize() 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。也就是说,我们可以通过 finalize() 方法做一些重要的清理工作。(比如在 finalize() 方法中去调用 C 语言的 free() )
  • 坑点

    • 垃圾回收(垃圾回收有关的任何行为) 不能保证一定会发生

    我们无法控制垃圾回收的时机,前面第 3 点提到了,垃圾回收只与内存有关,如果 jvm 并未面临内存耗尽,它是不会浪费时间执行垃圾回收以恢复内存的。因此我们不能将 finalize() 作为通用的清理方法,我们需要创建其他的一些方法去进行清理。

    • 关于 System.gc()

    首要记住一点:System.gc() 不能保证执行垃圾回收,原因还是由于 垃圾回收只和内存有关。

    这个方法的作用只是提醒 JVM:开发者希望进行一次垃圾回收,但是否执行垃圾回收全看 虚拟机的脸色。

(3)终结条件

  • 对象处于某种状态,使它使用的内存可以被安全的释放

(4) 垃圾回收器如何工作?(需要好好消化)

首先提个问题:在堆上分配内存代价很高,但是由于垃圾回收器的存在,在java中,在堆中分配内存的速度甚至可以与其他语言在栈上的速度向媲美. 为什么?

因为java的垃圾回收器一方面会释放空间,一方面会进行内存碎片整理. 所以java创建对象的时候,在堆上分配内存只需要将堆指针移动一下,就像在栈上那样。

  • 垃圾回收机制 - 引用计数法(并非 Java 使用)

    每个对象都有一个引用计数器,如果有一个引用变量连接到该对象时,则该对象的引用计数器加 1;当引用离开作用域或者被置为 null 的时候,引用计时器减 1 。如果引用计数器为 0,则判定该对象失活。(经常会被立即清理)。但是如果出现循环引用的时候,单纯靠引用计数器就不行了.。

  • Java 采用的垃圾回收机制的思想:

    所有活的对象不管是被引用了多少层,一定可以追溯到存活在堆栈或者静态存储区之中的引用。对于发现的每个引用,追踪它引用的对象,寻找此对象包含的所有引用,反复进行,直到 ”根源于堆栈和静态存储区的引用“所形成的网络全部被访问为止。这样就找到了所有”活“的对象。

  • Java 采用的 自适应 的垃圾回收技术。

    在上面思想的基础下,关于如何处理找到的存活对象,取决于不同的 jvm 实现。

    有一种做法为 停止-复制

    • 简单来说就是 先暂停程序,但后将所有存活的对象复制到另外一个堆中,没有被复制的全是垃圾。当对象被复制到新的堆中时,紧凑排列。 当对象从一个堆被复制到另外一个堆之后,指向它的引用就必须被修正,静态存储区和栈上的引用可以直接被修正.,但可能还有其他指向这些对象的引用,会在之后的遍历中被找到并修正。
    • 这种方式效率低,存在两个问题:
    • 开销变大,增加了一个堆,在两个分离的堆之间来回操作
    • 复制的问题,程序稳定之后,只有少量垃圾,全部将内存复制一遍很浪费。
    • 解决方法:
    • 针对 开销大的问题:按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间 。
    • 针对 复制的问题:jvm 进行检查,没有新垃圾产生的话,转换到另一种工作模式 标记-清扫,这也是为什么说 java 是 自适应 的垃圾回收。

    关于 标记-清扫

    • 思路:同样是从堆栈和静态存储区出发,遍历所有引用,进而找出所有存活的对象。每当找到一个存活对象,就给它一个比奥及,这个过程中不会回收任何对象。当全部标记工作完成的时候,才开始清理动作。清理过程中,没有标记的对象被释放,并不进行复制。这样,剩下的堆空间是不连续的,如果需要连续空间,则需要重新整理剩下对象。
    • 同样的,也需要在程序暂停的时候才能进行。
  • 进一步解释 自适应

    • 前置知识:内存分配以较大的 块 为单位,如果对象较大就会占用单独的块。

    • 细节:停止-复制 严格来说要先把所有存活对象从旧堆复制到新堆,然后才能释放旧对象,这将导致大量内存复制行为。 在分配 块 之后,垃圾回收器可以往废弃的 块 中拷贝对象,每个 块 有相应的 代数generation count 来记录它是否存活。通常如果块在某处被引用,代数 会增加;垃圾回收器将对上次回收动作之后的新分配的 块 进行整理。

    同时,垃圾回收器会定期进行完整的整理动作–大型对象不会被复制(只是增加 代数),内含小型对象的那些 块 则被复制并整理。

    个人理解,这种做法就是避免复制大块内存,只复制一些小的对象。

    Java 虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器效率降低,则切换到 标记-清扫 模式。同样如果 标记-清扫 模式的效率降低的话,就切换回 停止-复制 模式。


5. 初始化

(1) 类的成员变量 & 局部变量:

  • 对于类的成员变量:
    • 如果是基本数据类型:未初始化,则会默认设置初值(具体的值见 Java 之路 (二) – 一切都是对象 )
    • 如果是对象引用:未初始化,则会默认设置为 null
  • 局部变量未初始化就使用,会报错。

(2) 初始化的顺序 (重点)

  1. 此处直接引入 对象的创建过程,加入有个名为 Dog 的类:
    1. 当首次创建 Dog 的对象时,或者 Dog 类的静态方法/静态域首次被访问时,Java 解释器查找类路径,定位 Dog.class 文件
    2. 然后载入 Dog.class(这会创建一个 Class 对象),执行有关静态初始化的所有动作。因此,静态初始化只在 Class 对象首次加载的时候进行一次
    3. 当用 new Dog() 创建对象的时候,首先在堆上为 Dog 对象分配存储空间
    4. 这块存储空间会被清零,也就自动的将 Dog 对象的所有成员变量设置成了默认值。
    5. 执行所有出现于成员变量定义处的初始化动作
    6. 执行构造器。(涉及到 第7章继承时 比较麻烦,之后会详细分析)
  2. 补充:
    1. 非静态成员变量的定义顺序决定了初始化的顺序。
    2. static 不会改变成员变量未初始化的默认值

(3) 关于数组的初始化

  • 关于数组

    //对于基本数据类型:
    //
    //此时只定义了一个数组,同时拥有的只是对数组的引用
    int[] a1;
    int a1[];
    
    
    //两种初始化形式
    //1.先创建,后分别对数组元素初始化
    
    int[] a1 = new int[space];//此时定义的同时,在数据里创建了 固定个数的元素,一旦个数固定,不能修改,此时 数组中的元素全部初始化为 默认值(由类型决定,此处为 int 的默认值 0)
    a1[0] = 1;a1[2]=2;...
    
    //2.也可以通过如下方式,创建的同时进行初始化
    int[] a1 = {1,2,3,4,5};
    
    
    //对于非基本类型的数组
    //假定有一个 Person 类
    
    //两种形式
    //1.先创建,后分别对数组元素初始化
    Person[] people = new People[space];//此时创建的是一个引用数组,该数组中的元素都是 Person 类型的空引用。
    //需要对 元素进行初始化之后才可以使用,否则会发生异常
    people[0] = new People();
    
    //2.创建同时初始化
    People[] people = {new People(),new People()}
    
  • 需要强调一个知识点:可变参数列表

    • 应用于参数个数或类型未知的场合。
    public class Main {
        public void printf(String... args) {
            for (String s : args) {
                System.out.println(s);
            }
        }   
    }
    • 语法: “类型” + “…” + “空格” + “参数名称”

    • 指定参数时,实际上编译器会帮我们填充数组,这样我们获取的仍旧是一个数组。


6. 枚举

本章只涉及一些 枚举 的概念,具体在 Java 中的特性在原书 第 19 章,留待日后整理。

(1) 枚举,即 enum,在 Java SE5 中加入。

(2) enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这时一种非常有用的功能。

(3) enum 是一个类,我们只需要把他用作一种创建数据类型的方式,然后直接将所得到的类型拿来使用即可。

(4) 简单示例:

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

//通过以下调用,即可获得 MEDIUM 这个值。
Spiciness sp = Spiciness.MEDIUM;

(5) 问题

  • 在 Effective java 中,认为 枚举 代替常量是一个非常安全的方法。
  • 但是学 Android 的过程中,发现 Google 官方不建议使用 枚举。
    • 原因是因为 占内存
    • 因为 反编译之后,会发现 枚举对象的变量 全部会以 static final 形式存在。(由网上的分析文章得来,并未亲自实践过)

总结

这一章算是真正接触到 Java 这门语言了(也许吧),虽然都很基础,但也是属于必须掌握的知识。

另外强调关于 垃圾回收的部分,这一章只讲了理论性的东西,然而现在回头看,只了解这些是不够的。毕竟出门动辄都是从源码层问 垃圾回收是怎么实现得,hhh,累觉不爱,所以还是要再深入了解。

不多BB了,期待下一章吧。

共勉。

你可能感兴趣的:(Java,基础,Java,之路)