学习内容:
- 构造器
- 方法重载
- this 关键字
- 垃圾回收器的清理
- 初始化问题
- 枚举类型
这一章内容有一点点多,需要注意的地方也很多。下面就开始我的表演了。
1. 构造器
(1) 概念:
- 一个创建对象时被自动调用的特殊方法。
(2) 作用:
- 通过构造器,创建对象,并确保对象得到初始化。
(3) 命名:
- 构造器的名称必须与类名相同。
(4) 特殊:
- 构造器是一种特殊类型的方法,它没有返回值。但是!它与返回值为空(void)不同。
- 对于空返回值,方法本身不会自动返回什么,但是可以选择让它返回别的东西
- 对于构造器,不会返回任何东西。new 表达式返回了对新建对象的引用,但是构造器本身没有返回任何值。
- 不接受任何参数的构造器称为 默认构造器 / 无参构造器
- 如果类中没有构造器,那么编译器会自动创建 默认构造器;反之,如果已经定义了一个构造器(无论是否有参数),编译器都不会再自动创建 默认构造器。
2. 方法重载
(1) 原因
- 每个方法要有独一无二的标识符
- 构造器强制重载方法名:为了让方法名相同而形式参数不同的构造器同时存在。
(2) 重载规则:
- 具有相同的的方法名
- 必须有一个独一无二的参数类型列表(包括参数类型,以及参数类型对应的顺序)
(3) 需要注意,涉及基本类型的重载
- 常数值会被当作 int 值处理
- 如果传入实参的数据类型 小于 方法中声明的形参的数据类型,那么会将 实参的数据类型提升。
- 特殊的,对于 char 而言,如果没有恰好接收 char 参顺的方法,那么会把 char 提升至 int
- 如果传入实参的数据类型 大于 方法中声明的形参的数据类型,那么会将 实参的数据类型进行窄化转换。
3. this 关键字
(1) 作用
- 通过 this 关键字,可以在方法的内部获得当前对象的引用。
- this 只能在方法内部使用,表示对 “调用方法的那个对象” 的引用
(2) 用途 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() 方法。
比如将当前对象传递给其他方法
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 关键字
比如初始化成员变量时,避免参数重名造成混淆
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) 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) 初始化的顺序 (重点)
- 此处直接引入 对象的创建过程,加入有个名为 Dog 的类:
- 当首次创建 Dog 的对象时,或者 Dog 类的静态方法/静态域首次被访问时,Java 解释器查找类路径,定位 Dog.class 文件
- 然后载入 Dog.class(这会创建一个 Class 对象),执行有关静态初始化的所有动作。因此,静态初始化只在 Class 对象首次加载的时候进行一次
- 当用 new Dog() 创建对象的时候,首先在堆上为 Dog 对象分配存储空间
- 这块存储空间会被清零,也就自动的将 Dog 对象的所有成员变量设置成了默认值。
- 执行所有出现于成员变量定义处的初始化动作
- 执行构造器。(涉及到 第7章继承时 比较麻烦,之后会详细分析)
- 补充:
- 非静态成员变量的定义顺序决定了初始化的顺序。
- 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了,期待下一章吧。
共勉。