您的位置:首页 > 编程语言 > Java开发

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

2018-08-07 15:31 274 查看
版权声明:本文出自 whdAlive 的博客,转载必须注明出处 https://blog.csdn.net/whdAlive/article/details/81482196

学习内容:

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

    共勉。

    阅读更多
  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: