5、初始化与清理
在C语言和C++中系统是不会自动清理垃圾,导致初始化的变量一直存在内存中,导致内存泄漏。
所以Java提出了“垃圾回收器”,对于不再使用的内存资源,垃圾回收能自动将其释放。
5.1 构造器(构造方法)确保初始化
对每一个类都需要定义一个initialize()方法,Java用户在操作对象之前会自动调用相应的构造器。
在Java中构造函数的名字和类名是相同的。
class Rock{
Rock(){
System.out.println("这是构造方法")
}
}
Rock r1 = new Rock(); //构造一个Rock对象
Rock r2 = new Rock(); //再构造一个Rock对象
注!如果不写构造器方法,则使用默认构造器,也就是无参构造器。
当写有参数构造器时,就会覆盖掉之前的无参构造器,所以需要再写一个无参构造器。
class Rock2{
Rock2(int i ){
System.out.println("Rock"+i)
}
}
Rock2 rock2 = new Rock2(5); //有参构造器需要传参
5.2 方法重载
在java中可以使用相同的方法名,但是参数不同。在进行调用时,可以根据不同的参数类型,来调用相应的方法。
重载构造方法
class Tree{
int height;
// 重载构造器方法
Tree(){
height = 0;
}
Tree(int h){
height = h
}
// 重载其他方法
void info(){
System.out.println("Tree is" + height);
}
void info(String s){
System.out.println(s + "Tree is " + height);
}
}
5.2.1 区分重载方法
有几个方法有相同的名字,java如何区分需要调用哪一个?
根据参数的数量,参数的类型,甚至参数的顺序来区分。
但是,《Thinking in Java》的作者并不建议用参数的顺序区分,因为这样会使得代码不好维护。
5.2.2 涉及基本类型的重载
基本类型能从一个“较小”的类型自动提升到一个“较大”的类型。
如果此过程涉及重载,就会造成混淆。
较小的数据类型 char、byte、short
较大的数据类型 int
对于int类型,他只会找参数为int的方法
但是char类型不一样,如果它找不到char类型的方法,就会直接提升至int型。
5.2.3 以返回值区分方法
相同的参数,但是不同的返回值是不行的
因为编译器难以区分。
void f(){}
int f(){return 1;}
//这是不可以的
5.3 默认构造器(构造方法)
如果你不写构造器(构造方法),那么编译器会自动创建一个构造方法
但是当你写了有参构造方法后,就会覆盖掉原来的默认构造方法。
5.4 this关键字
如果统一类型有两个对象,分别是a和b。如何才能让这两个对象都能调用peel()方法。
class Banana{
void peel(int i ){
...
}
}
...main方法
Banana a = new Banana();
Banana b = new Banana();
a.peel(1);
b.peel(2);
//如果只有一个peel()方法,如何知道是a还是b调用的
// Banana.peel(a,1);
// Banana.peel(b,2);
// 以上代码是看不到的,但是编译器会偷偷把对象作为参数传入进去。在对象中就可以使用this关键字来确定当前对象。
public class Leaf{
int i =0;
Leaf increment(){
i ++;
return this; //返回当前对象
}
Leaf(int i){
this.i = i;
}
}
5.4.1 在构造器(构造方法)中调用构造器(构造方法)
通过this关键字,就可以调用构造方法
注:只可以在构造方法中调用一次this的构造方法
public class Person{
int height;
String name;
Person(int height){
this.height = height;
this.name = "张三"
}
Person(String name){
this.name = name;
this(180); //调用当前对象的构造方法
}
Person(int height,String name){
this.height = height;
this.name = name;
}
Person(){
this(180,"张三");
}
}
5.4.2 static的含义
在了解this关键字之后,就能更全面地理解static方法的含义。
加了static之后,这个方法和属性只属于这个类,和某个特定的对象无关。
举个例子:对于Chinese这个类,Chinese类的任何对象(zhangsan、lisi)的国籍都是china,和任何特定的对象无关,所以国籍就是一个static的。
static方法就是没有this的方法,且static方法的内部不能调用非静态方法。
非静态方法,可以调用静态方法。
《Thinking in Java》的作者讨论关于static是否是“面向对象的问题”,因为有人认为static具有全局函数的语义。但是在设计中确实有存在的必要。
5.5 清理:终结处理和垃圾回收
在Java的类中定义一个名为finalize()的方法。
一旦垃圾回收机制准备处理这个对象,就会首先调用finalize()方法
注:finalize() 并不是C++中的析构方法。
5.5.1 finalize()的用途为何?
这里要记住一点:垃圾回收只与内存有关
所以finalize()只和内存释放有关。
但是如果在Java语言中调用非Java(C或者C++)代码,那么可能就涉及到内存问题,所以需要进行手动释放。
5.5.2 你必须实施清理
Java的垃圾回收机制,使得Java并不需要手动的进行释放内存,也不需要析构方法。
但是垃圾回收机制也不是万能的,JVM只有在面临内存耗尽的情况下,才会调用垃圾回收机制。
5.5.3 终结条件
不能指望finalize()
必须创建其他的“清理”方法,并且调用清理方法。使得finalize()的用处并不是很大。
不过finalize()有一个有趣的用法,就是判断是否该对象是否被释放掉。
5.5.4 垃圾回收器如何工作
通常人们认为在堆上分配对象代价很大,但是因为垃圾回收机制,反倒提升了对象的创建速度。
举个例子:把C++里面的堆想象成一个院子,每个对象负责自己的地盘,一段时间后,对象可能被销毁,地盘必须加以宠用。但在Java虚拟机中,堆的实现完全不同,它更像一个传送带,每分配一个对象,就往前移动一格,这就加快了分配速度。Java的“堆指针”只是简单地移动到尚未分配的区域。当然,在实际过程中在薄记工作方面还有少量额外开销,但比不上查询可用空间开销大。
上面的例子是比较粗糙的,Java的堆未必完全像传送带那有工作,若真是传送带,那么势必会导致频繁的内存页面调度(频繁地移出硬盘),页面调度会显著地影响性能,最终当创建了足够多的对象之后,内存将耗尽。其秘密在于垃圾回收器的接入,它工作时一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,避免了页面错误。
java并没有引入计数器:计数器只的是该对象被多少个指针引用过,但计数器为0使,就调用回收机制,将其回收。但是在存在循环引用的问题,使得对象不能被很好地回收。
在java中的回收依据,任何活得对象,必然能追溯到存活在堆栈或者静态区域中的引用。如果从堆栈和静态区开始,遍历所有的引用,必然能找到所有“或”的对象,对于发现的每个引用,必须追踪它所引用的对象,然后使此对象包含的所有引用,如此反复,直到“根源于堆栈和静态区的引用”所形成的网络全部被访问位置。对于没被发现的对象,就将其回收。
如何分开活着的对象和没存活的对象,这取决于不同的Java虚拟机,有一种方法名为停止-复制**的方法,先暂停程序的运行,然后将活的节点复制到另一个堆,没有被复制的全部是垃圾,将其删除。当时把对象搬到另一处,所有指针都必须要修正,这会降低效率。
如何分开活着的对象和没存活的对象,频繁的复制会造成浪费,且程序进入稳定后,只会产生少量垃圾,甚至没有垃圾。还频繁复制会降低效率,所以此时就转换另一种工作模式,标记-清扫 ,标记清扫的速度相当慢,但是只有少量垃圾的时候,效率就很快了。
标记-清扫从堆栈和静态存储区触发,遍历所有引用,找到所有存活的对象,进行标记。标记完成后,删除所有没有标记的数据,这会导致内存不连续。就必须重新整理内存对象。
由于垃圾回收效率很低,所以在内存数量很低的时候,回收机制是不会执行的。
在内存分配中是以”块“为单位,一个块中有很多个对象。在执行停止-复制的时候,就可以往废弃的块中拷贝对象。以块为整体,来判断是否存活。
总结起来,就是 自适应的、分代的、停止-复制、标记-清扫的垃圾回收机制。
Java虚拟机中有许多附加技术以提升速度,尤其于加载器操作有关,被称为JIT编译器技术。这种技术将java代码编译为class文件,直接执行class文件,提高运行效率。
编译有两种:
(1) 编译所有的代码,要花更多的时间,且class文件比较大
(2)只编译需要的代码(lazy evaluation)
5.6 成员初始化
Java尽力保证所有变量都初始化。
如果方法局部变量不初始化,就会导致编译错误。
void f(){
int i;
i ++ ; //会报错
}
对于类中的数据,如果不初始化,就会设置一个默认值。如char就设置为0等。
5.6.1 指定初始化
class Depth{ }
public class InitVal2{
boolean b = true;
char ch = 'x';
byte b = 47;
Depth d = new Depth(); //Depth 是自定义的一个类
}
通过方法来进行初始化
// 正常执行
public class Method2{
int i = f(); //f是一个方法
int f(){ return 11; }
}
// 正常执行
public class Method3{
int i = f(2); //f是一个方法
int f(int n ){ return n*11; }
}
// 正常执行
public class Method4{
int i = f();
int f(){return 11; }
int j = g(i);
int g(int n){ return n*10; }
}
// !!!!报错 , 所以顺序对执行也有很大的影响。
public class Method5{
int j = g(i);//报错
int i = f();
int f(){return 11; }
int g(int n){ return n*10; }
}
5.7 构造器(构造函数)的初始化
构造函数不能阻止初始化的进行,会先将数据初始化,再调用构造函数。
public class Counter{
int i; //先初始化
public Counter{
i = 7;//再调用构造函数
}
}
//先初始化为0,然后再调用构造函数,设置为7
5.7.1 初始化顺序
变量定义的先后顺序决定了初始化的顺序。
先初始化,再调用构造方法等。
5.7.2 静态数据的初始化
无论创建多少个对象,静态数据只占用一份存储区域。
static关键字不能应用于局部变量。
静态数据也必须初始化,没有初始化则指定默认值。
5.7.3 显式的静态初始化(静态块)
Java允许多个静态初始化动作组织成一个特殊的静态子句,也称为“静态块”
什么时候进行初始化
(1)静态数据在首次生成类的对象时
(2)首次访问静态成员时
public class Spoon{
static int i;
static{ //静态块
i = 47;
}
}
class Cup{
Cup(int i){
System.out.println("Cup构造方法"+i);
}
void f(int i) {
System.out.println(i);
}
}
class Cups{
static Cup c1;
static Cup c2;
static {
c1 = new Cup(1);
c2 = new Cup(2);
}
Cups(){
System.out.println("Cups的构造方法");
}
}
public class TestString {
public static void main(String[] args) {
System.out.println("还没开始调用");
Cups.c1.f(99);
}
}
// 输出结果
// 还没开始调用
// Cup构造方法1
// Cup构造方法2
// 99
5.7.4 非静态实例初始化
非静态实例初始化跟静态方法的初始化类似。
5.8 数组初始化
数组是具有相同数据类型的列表。
通过方括号下标操作符[ ]来定义和使用
int[] a1;
//也可以
int a1[];
这两句是一样的。
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
注意!这只是复制了一个引用,其本质是操作同一个数组。
public class TestString {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
a1[2] = 5;
System.out.println(a2[2]);
}
}
// 输出结果
// 5
如果操作数组下标,那么就会抛出数据越界的异常。
int[] a = new int[10];//初始化一个长度为10的数组
5.8.1 可变参数列表
应用于参数个数未知的场合。
由于Object是所有类型的父类,所以可以创建Object类型的数组。
Object[] list = new Object[]{
new Integer(5),new Float(3.14),new Double(11.11)
};
给方法添加可变参数
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){
printArray(1,3,5);
printArray(new Integer(2),new Float(1.1));
}
}
// 输出结果
// 1, 3, 5,
// 2, 1.1,
可变参数前也可以添加其他类型的参数
static void printArray(int a,Object... args){
for(Object obj : args){
System.out.print(obj + ", ");
}
System.out.println();
}
public static void main(String[] args){
printArray(1,3,5);
}
5.9 枚举类型
在JavaSE5之后,添加了枚举类型。
enum Spiciness{
MOT,MILD,MEDIUM,HOT,FLAMING
}
public class TestString {
public static void main(String[] args){
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
在创建enum时,编译器会自动添加一些特性。比如
(1)创建toString()方法。
(2)创建ordinal()方法,显示所在的下标。
enum Spiciness{
MOT,MILD,MEDIUM,HOT,FLAMING
}
public class TestString {
public static void main(String[] args){
for(Spiciness s : Spiciness.values()) {
System.out.println(s+" ordinal " + s.ordinal());
}
}
}
// 输出结果
// MOT ordinal 0
// MILD ordinal 1
// MEDIUM ordinal 2
// HOT ordinal 3
// FLAMING ordinal 4
enum可以在switch语句中使用
switch(degree){
case MOT: ..... ; break;
case MILD: .... ; break;
.....
default: ....;
}
5.10 总结
C++的发明人在设计C++期间,发现大量的编程错误来自于不正确的初始化。