《On Java 8》阅读笔记——初始化与清理 与 集合

《On Java 8》是事实上的《Java 编程思想》第5版。
《On Java 8》中文版(兴趣人员自译)

初始化与清理

1、构造器:如果不自己写构造器,编译器会自动创建一个无参构造器。如果已经自己写了一个构造器,则默认的无参构造器不再自动创建(比如只写了一个有参数的构造器,那就没有无参构造器)。

构造器是静态方法。

构造器没有返回值,却同时也没有给你选择的余地。new 表达式虽然返回了刚创建的对象的引用,但构造器本身却没有返回任何值。

很有意思的是,下面的程序是可运行的。可见,构造器是一种特殊的方法,与返回类型为 void 的普通方法不同。

Class A(){
	A(){}
	void A(){}
	public static void main(String[] args){
		A a = new A();
		a.A();
	}
}

2、方法重载:看的时候以为是重写,看完才发现好像都把这个都忘掉了……

class A {
    A() {}
    A(int a) {}
    void print(int a) {}
    void print(double a) {}
    void print(String s) {}
}

3、基本类型的自动转换:基本类型可以自动从较小的类型转型为较大的类型。
在这里插入图片描述
在这里插入图片描述

4、this 关键字:有这么一个认知偏差,创建同一个类的两个对象,然后调用两个对象的同一个方法,编译器是怎么知道调用的哪个对象的方法?以下很直观地看到是对象调用的方法,编译器似乎就是这样知道的。

Banana a = new Banana();
Banana b = new Banana();
a.peel(1);
b.peel(2);

但实际上,在编译器底层,这里的调用是这样的:第一个参数隐密地传入了一个指向操作对象的引用。

Banana.peel(a, 1);
Banana.peel(b, 2);

如此一来,this 关键字的由来就呼之欲出了,就是这里的指向操作对象的引用,它能方便我们在方法中直接对对象进行操作。

不过 this 只能在非静态方法内部使用。这很容易理解,静态方法是与类关联的,不关对象的事。

this 有一种特殊用法:在构造器中调用其他的构造器。注意使用 this 构造器时,必须在首行调用,否则会编译错误。另外,编译器不允许在一个构造器之外的方法里调用构造器。

class Flower{
    private int num;
    private String color;
    Flower(){
        this(10);
        color = "white";
    }
    Flower(int num){
        this.num = num;
    }
}

5、垃圾回收器:只会释放 new 关键字创建的对象的内存。(往后看发现作者的本意是,调用非 java 代码而分配的那些内存,是不能被垃圾回收器自动回收的。存疑,特注)

下面两种情况,不是用 new 创建的对象。① clone() 方法:myClass.clone(),拷贝字段而生成新对象,类必需实现java.lang.Cloneable接口;② newInstance() 方法:MyClass.class.newInstance()(MyClass)Class.forname("classpath.MyClass").newInstance(),调用的无参构造器。

对于不是用 new 创建的对象,为了回收其内存,Java 允许在类中定义一个名为 finalize() 的方法。当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,真正回收对象占用的内存。

垃圾回收器的目的是回收内存,有的时候如果内存没有濒临用尽,它不会启动,因为回收这个动作本身也会占用资源。所以必须记住一点:它并不总是会如你所想的开始回收,以及调用 finalize() 。

回到 finalize() 方法,实际上,它并非是进行”普通“的清理工作的合适场所,它设计之初的目的,是回收那些通过某种创建对象方式之外的方式为对象分配的存储空间,就是将那些使用本地方法(用 Java 语言调用非 Java 语言代码的形式)而占用的内存,由 finalize() 中调用的本地方法回收。

通常,不能指望 finalize() ,你必须创建其他的"清理"方法,并明确地调用它们。这些清理方法中我们最熟悉的应该就是 close() 。

看起来 finalize() 只对很难用到的一些晦涩内存清理里有用了。不过关于它的一点我们是可以用到的:终结条件。因为垃圾回收的时候会调用这个 finalize() ,我们就可以利用这个作为终结条件,进行对象终结条件的验证,方便找到bug。虽然不确定会不会回收,但既然有一种途径能找到错误,总是保险一些。

class WaterTap
{
    String state = "on";

    void turnOff(){ state = "off"; }
    void turnOn() { state = "on";  }

    @Override
    protected void finalize() throws Throwable{
        if (state.equals("on")){
            System.out.println("Error: The tap is on!");
        }
        //通常情况下都会调用一下父类的finalize()
        super.finalize();
    }
}
public class TerminationCondition{
    public static void main(String[] args){
    	//没有被引用的对象空间会被GC回收(garbage collection)
        new WaterTap();
        //用于强制进行终结动作,不然这么小的程序肯定不会回收
        System.gc();
        //续一秒,在线回收
        try { Thread.sleep(1000);}
        catch (Exception e) {}
    }
}

太累了,后面还有些东西,作者写了好多,但是有点难理解,还是译文,就更难了。这里找了一篇挺不错的GC原理,GC是属于JVM的内容,超过了这本书的范畴,就看了一点,留坑。
不止面试-JVM垃圾回收面试题详解

至少知道了引用是对象是否被回收的重要决定点之一。

6、静态方法中不能调用非静态变量、非静态方法。因为可能还没有对象存在。

7、静态代码块:首次创建这个类的对象,或首次访问这个类的静态成员时,才会执行。属于类初始化过程。

public class Spoon {
    static int i;
    static { //静态初始化
        i = 1;
    }
}

非静态代码块:首次创建这个类的对象时,才会执行。属于实例初始化过程。

public class Spoon {
	int j;
	{ //实例初始化
		j = 2;
	}
}

8、类初始化:就是执行<clinit>()方法
(1)<clinit>()方法的首行调用父类的<clinit>方法。
(2)<clinit>()方法由静态变量显示赋值代码和静态代码块组成,静态变量显示赋值代码和静态代码块从上到下顺序执行。
(3)执行类的main()方法前先类初始化。
(4)<clinit>方法只执行一次。

实例初始化:就是执行<init>()方法
(1)<init>()方法由非静态变量显示赋值代码和非静态代码块、对应构造器代码组成。非静态变量显示赋值代码和非静态代码块从上到下顺序执行,最后执行构造器代码。
(2)每次创建实例对象,调用对应构造器,执行的都是<init>()方法。
(3)每个构造器都有一个自己的<init>()方法。
(4)<init>()方法的首行调用父类的<init>()方法。

9、初始化:创建对象时,首先在堆中划分空间,然后将对象的所有基本类型数据初始化为默认值,然后再按顺序初始化。

初始化顺序:
1、变量先于方法
2、变量定义顺序即初始化顺序。
3、静态变量先于非静态变量
4、父类先于子类。(父类初始化作为一个整体,子类初始化作为一个整体)
5、初始化块先于构造器
6、变量与初始化块的顺序由书写顺序而定。

关于变量先于方法:比如调用静态方法,则先初始化静态变量。比如调用构造器,则先初始化静态与非静态变量。由于 main() 方法是静态方法,所以会在执行前先将该类的静态变量初始化。

静态变量只初始化一次。

网上找的例子,可以用此来复习:

public class OnJava8 extends Father {
    private int i = test();

    private static int j = method();

    static {
        System.out.print("(6)");
    }

    OnJava8() {
        System.out.print("(7)");
    }

    {
        System.out.print("(8)");
    }

    public int test() {
        System.out.print("(9)");
        return 1;
    }

    public static int method() {
        System.out.print("(10)");
        return 1;
    }

    public static void main(String[] args) {
        System.out.println("\n(11)");
        OnJava8 a = new OnJava8();
    }
}


class Father {
    private int i = test();

    private static int j = method();

    static {
        System.out.print("(1)");
    }

    Father() {
        System.out.print("(2)");
    }

    {
        System.out.print("(3)");
    }

    public int test() {
        System.out.print("(4)");
        return 1;
    }

    public static int method() {
        System.out.print("(5)");
        return 1;
    }
}

输出:

(5)(1)(10)(6)
(11)
(9)(3)(2)(9)(8)(7)

没想到吧,方法重载。

10、所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员变量 length。

11、若没有重写 toString() 打印类的对象,内容是类名和一个 @ 符号以及多个十六进制数字(类名和对象的地址)。

例:[I@1b6d3586 前导的 [ 代表这是一个后面紧随的类型的数组,I 表示基本类型 int。[Ljava.lang.Integer;@1b6d3586这是 Integer 数组。

12、可变参数列表:method(Object... args)(也可以是任意具体类型)

把这样的东西作为参数,就可以放任意数量(包括0)任意类型的参数了: method(47, (float) 3.14, 11.11, new OnJava8());。在方法中,会将args视作数组。

注意:method(Object[] args)将会把参数要求为数组,而非任意个独立的参数。

编译器会使用自动装箱来匹配重载的方法,所以很有可能出现 bug :

public class OverloadingVarargs {
    static void f(float i, Character... args) {
        System.out.println("first");
    }

    static void f(Character... args) {
        System.out.println("second");
    }
    
    static void f(int... args) {
        System.out.println("third");
    }

    public static void main(String[] args) {
        f(1, 'a');
        f('a', 'b');//无法匹配
        f();//无法匹配
    }
}

13、枚举 enum 示例:
由于枚举类型的实例是常量,因此按照命名惯例,它们都用大写字母表示(如果名称中含有多个单词,使用下划线分隔)。

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 + " : " + howHot.ordinal());
    }
}
MEDIUM : 2

可以看到,编译器会自动创建 toString() 方法,以便显示某个 enum 实例的名称。还会创建 ordinal() 方法表示某个特定 enum 常量的声明顺序。另外, enum 可以在 switch case 中使用。

集合

1、集合分两种:①集合(Collection):一个独立元素的序列,这些元素都服从一条或多条规则。 ②映射(Map): 一组成对的“键值对”对象,允许使用键来查找值。

2、添加元素组:
Arrays.asList(...) 接受一个数组或是逗号分隔的元素列表(可变参数),并将其转换为 List 对象。返回一个表面是 List 对象,但底层实现是数组,所以不能进行增删,实际使用中,需要把这个返回值作为构造器的参数,以生成一个真正的 Collection 对象。
Collections.addAll(collection, ...) 接受一个 Collection 对象,以及一个数组或是一个逗号分隔的列表(可变参数),将其中元素添加到 接收的 Collection 实例中。
collection1.addAll(collection2) 只能接受一个 Collection 对象。

前两种方式中,②运行的最快,不过首先得自己创建一个Collection对象,但还是比①快多了,②比①快5倍左右。如果有现成的collection对象的话,③更快。

long startTime1 = System.nanoTime(); //获取开始时间
List<Integer> a = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
long endTime1 = System.nanoTime(); //获取结束时间
System.out.println("程序运行时间: " + (endTime1 - startTime1) + "ns");
//下同

3、①List : ArrayList、LinkedList
②Set : HashSet、TreeSet、LinkedHashSet
③Map : HashMap、TreeMap、LinkedHashMap

HashXxx 是最快的,只是无序存储;TreeXxx 通过比较结果的升序来存储;LinkedHashXxx 在保持 HashXxx 查找速度的同时按键的插入顺序保存键。

4、迭代器统一了对集合(List、Set)的访问方式。
①使用 iterator() 方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。
②使用 next() 方法获得序列中的下一个元素。
③使用 hasNext() 方法检查序列中是否还有元素。
④使用 remove() 方法将迭代器上一个返回的元素删除。

ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成,ListIterator 可以双向移动。
①可以通过调用 listIterator() 方法来生成指向 List 开头处的 ListIterator ,还可以通过调用 listIterator(n) 创建一个一开始就指向列表索引号为 n 的元素处的 ListIterator 。
②可以使用 set() 方法替换它访问过的最近一个元素。
③使用 previous() 方法获得序列中的上一个元素。
④使用 nextIndex() 和 previousIndex() 获取元素的序列号

5、

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值