随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
初始化和清理(cleanup)是涉及安全的两个问题。初始化时,忘记初始化时许多错误的来源,还有就是不知道如何初始化库的构件。清理:当使用完元素,不进行释放,资源耗尽,造成内存泄露的问题。
JAVA采用了构造器和垃圾回收器来解决问题。
5.1 用构造器确保初始化
创造对象时,如果类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
class Rock{
Rock(){
System.out.print("Rock") ;
}
}
现在创建对象时,就会为对象分配存储空间,并调用相应的构造器。
- 构造器的名称必须和类名完全相同。
- 如果Rock(i)是Rock类的唯一构造器,那么只能用它来构造类。
- 在JAVA中,初始化和创建捆绑在一起,两者不能分离。
- 无返回值。
5.2 方法重载
一方面,想要通过具体的语境推断出含义,调用同名不同操作的方法。
在Java中,构造器是强制重载方法名的另一个原因。构造器的名字和类名相同,那么岂不是只有一种方式创造对象? 有了方法重载,就能让方法名相同而形式参数不同的构造器存在。
class Rock{
Rock(){
System.out.print("Rock") ;
}
Rock(int i){
System.out.print("Rock") ;
}
}
5.2.1 区分方法重载
- 独一无二的参数类型列表:1.参数不同。2.参数相同,顺序不同也区分。
5.2.2 涉及基本类型的重载
- 如果某这个基本类型的重载,使用这个。
- 如果传入的数据类型小于方法中声明的形式参数类型,实际数据类型就会被提升。char类型特殊,若找不到,会提升至int型。
- 如果传入的数据类型大于方法中声明的形式参数类型,必须通过类型转换的窄化转换,即显式转换,否则编译器会报错。
- 无法通过返回值来区分重载方法。
举个例子:
void f(){}
int f(){return 1;}
//这种情况好区分
int x=f();
//如果是这样就没办法区分了。因为有的时候我只在乎方法调用,忽略返回值
f();
稍微总结一下,一开始引入了安全的问题,涉及到初始化和清理的问题。初始化就引入了构造器,在创建让构造器去初始化,防止初始化错误的问题。构造器的名字又和类名相同,想要不同方式创造的对象,由此引入方法重载。
5.3 默认构造器
顾名思义,即默认对象,即没有形式参数。若无构造器,编译器自动创建一个。
- 如果定义了一个构造器,编译器不会帮你自动创建默认构造器
class Bird2(){
Bird2(int i){}
Bird2(double i){}
public static void main(String[] args){
Bird2 a=new Bird2(); //这样会报错,因为没有这样的构造器。
Bird2 a=new Bird2(5);
}
}
5.4 this关键字
class Banana{
void peel(int i){}
public static void main(String[] args){
Banana a=new Banana();
Banana b=new Banana();
a.peel(1);
b.peel(2);
}
}
如何确定peel()方法是被a调用还是被b调用呢?
这里涉及到前面所说的-----发送消息给对象。编译器做了一些幕后工作,它暗自把“所操作对象的引用”作为第一个参数传递给peel().
peel(a,1);
peel(b,2);
这是内部的表示形式。可以帮你了解实际发生的事情。
想要在方法内部获得对当前对象的引用使用this即可,表示“调用方法的那个对象”。
//按照我的理解peel可以变成这样,所以可以在方法内部使用this.
void peel(Banana this,int i){
}
this关键字对于将当前对象传递给其他方法很有用
class Person{
public void eat(Apple apple){
Apple peeled=apple.getPeeled();
System.out.println("Yumy");
}
}
class Peeler{
static Apple peel(Apple apple){
//..... remove peel
return apple;
}
class apple{
Apple getPeeled(){ return Peeler.peel(this);} //这里用的是当前对象的引用
}
public class PassingThis{
public static void main(String[] args){
new Person().eat(new Apple());
}
}
}
这种代码的设计应该是我所欠缺的,如果让我设计,我可能在eat()中直接调用Peeler.peel()方法。但是仔细想想,按照它的这种设计是不是更合理呢?我在apple类中借助外部类削苹果,而不是在Person类中做这样的操作。
5.4.1 在构造器中调用构造器
如果想要在一个构造器中调用另一个构造器,以避免重复代码,可以使用this.
class Bird2(){
Bird2(int i){}
Bird2(double i){ this(5);}
}
- 两点:可以用this调用一个构造器,不能够调用两次。
- 调用构造器方法只能放在开头。并且只能在构造器中调用。
第一点其实是由第二点推出来的,假设调用两次,但是只能放在开头,总有一次不在开头,所以报错。
5.4.2 static的含义
了解this关键字之后,就能更全面地理解static方法的含义。static方法就是没有this的方法,在static方法的内部就不能调用非静态方法。
恍然大悟,static方法没有this引用,this代表调用方法的当前对象,没有当前的对象,如何调用引用对象的数据和方法呢 ?所以不能调用非静态方法。
举个例子:
class Person{
static A(int i){
C(i); //静态方法A,这里调用同一个类下的C方法,C是一个普通方法,编译器报错。因为C()等价于this.C(),而this不存在。
}
void B(int i){
C(i); //这里可以这么写,因为C()等价于this.C()。而普通方法在创建对象时生成,this表示调用B()的对象,故调用成功。
}
void C(int i){}
}
5.5 清理:终结处理和垃圾回收
都了解初始化的重要性,但常常会忘记清理工作。java提供垃圾回收器,负责回收无用对象占据的内存资源。
finalize()方法用于:假定你的对象(并非new)获得一块“特殊”的内存区域,垃圾回收器只知道释放那些经由new分配的内存,不知道如何释放这块特殊内存,就用到了finalize()方法,这里定义如何处理的操作。
垃圾回收器:一旦准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下次回收动作发生时,才真正回收对象占用的内存。
JAVA的finalize()与析构函数不一样。和C++析构函数不同,析构函数在调用完成后会自动调用解放对象占用的内存。java对象不一定会被被垃圾回收的。
Java的finalize()函数怎么理解呢? 应该理解为假如我要清理这个对象,必须执行的操作放在这里。它可不像析构函数调用完就会被调用。可能调用也可能不会被调用,因为垃圾回收可能一直不会被发生。明白了么?析构是一定,而finalize()发生只是一种可能,它追求的是一种:假如发生了,我该怎么做。并且它使用的情况是特殊的内存,new分配的内存应该不在此列。
只要程序没有接近使用完内存空间,垃圾回收都不会发生。但是程序结束后,垃圾回收器和对象的资源会被回收。
5.5.1 finalize()的用途何在
此时,读者已经明白了不该将finalize()作为通用的清理方法。因为它只处理特殊内存,即非new分配。真正用途是?
- 垃圾回收只与内存有关:也就是说finalize()方法同内存回收有关。
如果对象中含有其他对象,是否需要finalize()去释放? 不需要,因为它只处理通过某种创建对象方式以外的方式为对象分配了储存空间的情况。这种情况垃圾回收即可处理。
到底什么是特殊情况:
就是在使用“本地方法”的情况下,本地方法是一种在java中调用非java代码的方式,一般来讲是c和C++,如C的malloc()函数分配储存空间,只能free()函数来释放空间,这需要将free()函数在finalize()中调用。
这是非普通的清理工作,普通的清理工作放在哪里执行合适?
5.5.2 必须实施清理
一般来讲,清理工作是不需要的,垃圾回收已经处理好了,这也是析构函数不需要存在的理由。但是对于垃圾回收来讲,并不是万能的(而且不能调用finalize()),当需要进行清理工作时,还是需要明确调用某个恰当的java方法。
5.5.3 终结条件
finalize()方法不能用于一般的清理方法。但是有一个有趣的用法----对象终结条件的验证。
想一个情况,当对某个对象不再感兴趣---也就是要被处理时,这个对象应该处于某种状态,使它占用的内存可以被安全的释放。这个某种状态,其实就是能够通过终结条件的验证。这个用法的作用在于,某次finalize()让缺陷被人发现,就可以找到问题所在。
class Book {
boolean checkout = false;
Book(Boolean checkout) {
this.checkout = checkout;
}
void checkIn() {
this.checkout = false;
}
protected void finalize() throws Throwable {
if (checkout) {
System.out.println("Error:checked out");
super.finalize();
}
}
}
public class TerminationCondition{
public static void main(String[] args){
Book a=new Book(true);
a.checkIn();
new Book(true);
System.gc();
}
}
这个例子说的是所有的BOOK对象需要回收的时候,应该被checkIn过,这才是对象可以被回收的状态。 如果没有被checkIn,是不该被回收的。如果被这样回收了,其实在程序内就存在了隐患。那么在finalize()中进行检查, 我们就可以在程序发生错误的时候,偶然会看到这条消息,找到问题的所在:没有合理的清理对象。这其实是一个预警信息, 你这个对象没有正常回收哦,可能会出问题的。说白了,就是验证对象可不可以回收了,不可以的话,要做清理工作。
再举个例子,容易出错的地方:
public class BankTermination {
public static void main(String[] args){
Bank a=new Bank(true);
Bank b=new Bank(true);
a.logOut();
//a=new Bank(true);
new Bank(true);
System.gc();
}
}
class Bank{
Boolean logIn=false;
Bank(Boolean logIn){
this.logIn=logIn;
}
void logOut(){
this.logIn=false;
}
protected void finalize() throws Throwable {
if(logIn){
System.out.println(this.toString()+"Error: Your account is login");
super.finalize();
}
}
}
这个例子里,你调试会发现我finalize()起效的只有new Bank(true); 这个没有被引用的对象,而a\b\logOut()方法没什么用,原因就是一直说的垃圾回收回收的无用的对象,也就是无引用的对象。除非使用注释掉的话,它就会回收a对象了。之前一直在看,但是理解不深。
5.5.4 垃圾回收器如何工作
结合博客 http://www.cnblogs.com/andy-zcx/p/5522836.html
在之前的程序语言中,在堆上分配对象的代价十分高昂,原因在于查找可用空间的时间长。大家也会觉得JAVA在堆上分配对象也高昂。但其实不是的,JAVA由于其垃圾回收机制提高了分配对象的效率。下面会介绍为什么JAVA从堆分配空间的速度和其他语言在堆栈上分配空间的速度相媲美。
举例C++,想象成一个院子,每个对象有自己的地盘,一段时间后对象被销毁,地盘需要重用,这时候我们新分配的对象就需要查找可用空间花费时间。
而JAVA不同,JAVA可以看作一条传送带,分配对象就像队列一样,不停的前进一格一格,所以在分配效率上比的上C++在堆栈上分配空间的消息。
但是这样分配存在问题,在于会导致频繁的页面调度---将其频繁移进移出硬盘,影响性能。但是有了垃圾回收旧不一样的了,垃圾回收机制会一面回收空间,一面会使空间紧凑排列,问题迎刃而解。通过垃圾回收的重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
再来具体理解垃圾回收的几种机制
1.引用计数
每个对象有一个引用记数器。被引用加1,当引用离开作用域或被置为null时,引用计数减1.垃圾回收会在含有所有对象的列表上遍历,当发现某个引用记数器为0,释放空间。
优点:简单
缺点:循环引用无法解决、速度很慢、开销持续整个程序生命周期。
举例:
class A{
B b=new B();
}
class B{
A a=new A();
}
public class Test {
public static void main(String[] args){
B b=new B();
A a=new A();
b=null;
a=null;
}
}
对象应该被回收,但是引用计数却不为0,无法回收。
5.6 成员初始化
在JAVA中,面对局部变量没有被初始化时会得到编译错误的消息。这是因为可能程序猿因为粗心忘记了初始化,如果使用默认值就掩盖了这种错误。
而对于类的数据成员,JAVA保证每一个基本类型成员都有初始化值,如果是一个引用类型,就会得到null。
5.6.1 指定初始化
如何想为某个变量赋初值,该怎么做?
- 在定义类成员变量的地方为其赋值。
- 调用方法来提供初值
5.7 构造器初始化
可以使用构造器来进行初始化。但是无法阻止自动初始化的进行,它将在构造器被调用之前发生。
举个例子
class Counter{
int i;
Counter(){i=7;}
}
这里变量i会先初始化为0,然后在调用构造器后,变成7.
5.7.1 初始化顺序
在类的内部,变量定义的先后顺序决定了初始化的顺序,假如变量分布在各处,也是按照从上向下的顺序初始化。
例子:
class Window{
Window(int marker){
System.out.println("Window("+marker+")");
}
}
class House{
Window w1=new Window(1);
House(){
System.out.println("House()");
w3=new Window(33);
}
Window w2=new Window(2);
void f(){
System.out.println("f()");
}
Window w3=new Window(3);
}
public class OrderOfInitialization {
public static void main(String[] args){
House h=new House();
h.f();
}
}
这个例子非常的清晰,变量定义散落,但是在构造器调用之前,已经按照顺序进行了调用。然后才是构造器调用和方法调用。
5.7.2 静态数据的初始化
无论创建多少个对象,静态数据只占用一份储存区域。static 关键字不能应用于局部变量,即只能作为类的数据成员。