《On Java 8》(Java编程思想第五版)学习笔记 第六章初始化和清理

第六章 初始化和清理

  • 两个安全性问题:初始化清理
    • C 语言中很多的 bug 都是因为程序员忘记初始化导致的。
    • 忘记清理会造成资源滞留不会被回收,占用内存。
  • 解决方法:
    • Java 采用了构造器来初始化
    • 另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。

6.1 利用构造器初始化

  • Java 会自动调用对象的构造器方法,从而保证初始化。
  • 构造器名称和类名一样。
  • 如果不手写构造器,默认有一个无参构造器。如果自己写了构造器,那么构造器只有你所写的。
  • 构造器没有返回值。
package chapter06;

public class Rock {

    Rock(){
        System.out.println("无参构造器");
    }

    Rock(String str){
        System.out.println("有参构造器"+str);
    }

    public static void main(String[] args) {
        new Rock();
        new Rock("hello world");
    }
}

在这里插入图片描述

  • 当创建一个对象时:new Rock() ,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。

6.2 方法重载

  • 一个类中多个方法的方法名相同,但参数列表不同,形成重载。
package chapter06;

public class Dog {

    public Dog(){
        System.out.println("狗");
    }
    //构造器重载
    public Dog(String str){
        System.out.println("狗"+str);
    }

    public void eat(){
        System.out.println("木阿木啊");
    }
    //方法重载
    public void eat(String str){
        System.out.println("木阿木啊"+str);
    }
}

6.2.1 区分重载方法

  • 有一条简单的规则:每个被重载的方法必须有独一无二的参数列表
  • 参数列表:参数种类、参数个数、参数顺序,都可区分重载的方法。
package chapter06;
public class Dog {
    //形参个数都是一个,但类型不同构成重载
    public void eat(long i){ }
    public void eat(String str){ }
    
    //数据类型相同,但形参的个数不同,构成重载
    public void eat(int i){
    }
    public void eat(int i,int j){
    }
    
    //调换两个形参的书序,也形成了重载
    public void eat(int i,String str){ }
    public void eat(String str,int i){ }
}

6.2.2 重载与基本类型

  • 基本类型可以自动从小的类型转为大的类型
package chapter06;
public class Dog1 {

//    void m(byte i){}
//    void m(short i){}
//    void m(int i){}
    void m(long i){
        System.out.println("long"+i);
    }
    void m(float i){
        System.out.println("float"+i);
    }
    void m(double i){
        System.out.println("double"+i);
    }
//    void m(char i){}
    void m(boolean i){
        System.out.println("boolean"+i);
    }

    public static void main(String[] args){
        //低转高
        //当我们调用 m(10) 方法时,程序会使用上面哪一个呢?
        new Dog1().m(10);  //用的是void m(int i); 因为整型数据默认为int类型
        //如果将上面方法的m(int i) 注释掉,那么则用哪个?
        new Dog1().m(10);  //用的是void m(long i); 因为找不到一致的数据类型时,则整型数据会自动向上转型为long.
        //int数据会先看有没有匹配int的方法,没有则找有无long ,没有则float,最后是double.都没有则编译报错.
        // int -> long -> float -> double.

        new Dog1().m('a');
        //char数据类型时, 先看有没有匹配char的,没有则向上转 (char类型参与运算时 会转为int类型再运算)
        // char -> int ->long ->float->double

        new Dog1().m(false); //查找有无boolean类型的方法 没有编译报错

        //如果想使用 short byte ,则需要调用方法时 显示转化 (因此存在精度丢失)
        new Dog1().m((byte)97);
        new Dog1().m((short)97);
    }
}

6.2.3 返回值的重载

  • 方法返回值无法区分重载方法。
    • 因为:下面这个看似重载的方法。方法名相同、参数列表也相同,只有返回值不同。但当你只想调用方法中的打印功能时,不在乎返回值,编译器会不知道该调用哪个。
package chapter06;
public class Dog {
    public int eat(int i){  //编译报错
        System.out.println("yes");
        return 0;
    }
    public String eat(int i){
        System.out.println("no");
        return "";
    }
}

6.3 无参构造器

  • 如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。

6.4 this关键字

  • this 关键字只能在非静态方法内部使用。
  • 如果你在一个类的方法里调用该类的其他方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。
package chapter06;
public class Apricot {

    void pick(){}
    
    void pit(){
        this.pick();//没必要加this
        pick();
    }
}
  • this 关键字只用在一些必须显式使用当前对象引用的特殊场合

6.4.1 在构造器中调用构造器

package chapter06;

public class Flower {

    public Flower(){
        this("*******"); //调用有参的构造器,并且代码必须在第一行
        //this(100)//错误 一个构造中只能调用一次
    }
    public Flower(String str){
        System.out.println("flower"+str);
    }
    public Flower(int i){
		System.out.println("flower"+i):
	}
}

6.4.2 static的含义

  • static 是与类绑定的,而不是对象。
  • static方法中不会存在 this。
  • 你不能在静态方法中调用非静态方法(反之可以)。静态方法是
    为类而创建的,不需要任何对象。

6.5 垃圾回收器

  • Java 中有垃圾回收器回收无用对象占用的内存。
  • 但垃圾回收器无法回收不是通过new 分配的内存。为此,Java 允许在类中定义一个名为 finalize()的方法。

6.5.1 finalize()的用途

  • finalize() 工作原理 :一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。
  • 之所以有 finalize() 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法(本地方法)。因此,不会过多使用 finalize() 方法。

6.5.2 你必须实施清理

6.5.3 终结条件

  • finalize() 一个有趣的用法,它不依赖于每次都要调用 finalize(),这就是对象终结条件的验证。
package chapter06;

public class Book {
    boolean checkedOut = false;
    public Book(boolean checkedOut){
        this.checkedOut = checkedOut;
    }
    void checkIn(){
        checkedOut = false;
    }
    @Override
    protected void finalize(){
        if(checkedOut){
            System.out.println("Error: checked out");
        }
    }
}

package chapter06;
public class TerminationCondition {
    public static void main(String[] args) {
        Book book1 = new Book(true);
        book1.checkIn();
        new Book(true); //这本书没有登记,被finalize()调用时发现。
        System.gc();
    }
}
  • 输出:Error: checked out
    要是没有 finalize() 方法来验证终结条件,将会很难发现这个 bug。

6.5.4 垃圾回收器如何工作

  • 堆:堆是利用完全二叉树的结构来维护一组数据,然后进行相关操作
  • 栈:顺序栈,链式栈。
  • 垃圾回收机制:
    • 引用计数:
      • 每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间。
      • 缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。
    • 停止-复制(stop-and-copy):
      • 顾名思义,这需要先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。
      • 效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。其二:在于一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。
    • 标记-清扫(mark-and-sweep):
      • “标记-清扫” 所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。每当找到一个存活对象,就给对象设一个标记,当标记过程完成后,没有标记的对象将被释放,不会发生任何复制动作。标记-清扫” 后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。

6.6 成员初始化

  • 成员变量:编译器默认赋予初始值 。
    • boolean false
      char
      byte 0
      short 0
      int 0
      long 0
      float 0.0
      double 0.0
      对象 null
  • 局部变量:必须手动赋予初值

6.6.1 指定初始化

public class MethodInit3 {
  //- int j = g(i); // Illegal forward reference
  int i = f();
  int f() {
  	return 11; 
  }
  int g(int n) {
  	return n * 10; 
  }
}
  • 上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对 “向前引用” 发出了警告。

6.7 构造器初始化

  • 可以用构造器进行初始化,这种方式给了你更大的灵活性。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。
public class Counter {
   int i;
   Counter() {
   	i = 7; 
   }
}
  • i 首先会被初始化为 0,然后变为 7

6.7.1 初始化的顺序

  • 成员变量的初始化在构造器之前

6.7.2 静态数据的初始化

  • 无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。
  • 初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对 象。

6.7.3 显示的静态初始化

public class Spoon {
	static int i;
	static {
		i = 47; 
		} 
	}
  • 静态初始化只会执行一次。

6.7.4 非静态实例初始化

public class Mugs {
	Mug mug1;
	Mug mug2;
	{ // [1]
		mug1 = new Mug(1);
		mug2 = new Mug(2);
		System.out.println("mug1 & mug2 initialized");
	}
	Mugs() {
		System.out.println("Mugs()");
	}
}
  • 实例初始化子句是在两个构造器之 前执行的。

6.8 数组初始化

  • 数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
int[] a1 = {1, 2, 3, 4, 5};

public class ArraysOfPrimitives {
	public static void main(String[] args) {
		int[] a1 = {1, 2, 3, 4, 5};
		int[] a2;
		a2 = a1;  //只是将a1的引用复制给了a2,a1 a2指向的是一个数组
		for (int i = 0; i < a2.length; i++) {
			a2[i] += 1; }
		for (int i = 0; i < a1.length; i++) {
			System.out.println("a1[" + i + "] = " + a1[i]);
		} 
	} 
}
输出:
a1[0] = 2;
a1[1] = 3;
a1[2] = 4;
a1[3] = 5;
a1[4] = 6;

6.8.1 动态数组创建

   int[] a;
   Random rand = new Random(47);
   a = new int[rand.nextInt(20)];
  • 数组的大小是通过 Random.nextInt() 随机确定的

6.8.2 可变参数列表

public class NewVarArgs {
	static void printArray(Object... args) {
		for (Object obj: args) {
			System.out.print(obj + " ");
		}
	System.out.println();
}
	public static void main(String[] args) {
		// Can take individual elements:
		printArray(47, (float) 3.14, 11.11);
		printArray(47, 3.14F, 11.11);
		printArray("one", "two", "three");
		printArray(new A(), new A(), new A());
		// Or an array:
		printArray((Object[]) new Integer[] {1, 2, 3, 4});
		printArray(); // Empty list is OK
	} 
}
输出:
47 3.14 11.11
47 3.14 11.11
one two three
A@15db9742 A@6d06d69c A@7852e922
1 2 3 4
  • 有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组

6.9 枚举类型

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

public class SimpleEnumUse {
   public static void main(String[] args) {
   	Spiciness howHot = Spiciness.MEDIUM;
   	System.out.println(howHot);
   } 
}
输出:
MEDIUM
  • 可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。
  • 由于 switch 是在有限的可能值集合中选择,因此它与 enum 是绝佳的组合。

6.10 本章小结

  • 构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象)
  • 垃圾回收器会自动地释放所有对象的内存,极大地简化了编程,并加强了内存管理上的安全性。然而,垃圾回收器确实增加了运行时开销,速度问题仍然是它涉足某些特定编程领域的障碍。
  • 由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过组合或继承创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值