5. 初始化与清理
面向对象(OOP)与面向过程
二者都是一种思想,面向对象是相对于面向过程而言的。面向过程,强调的是功能行为。面向对象,将功能封装进对象,强调具备了功能的对象。
面向对象更加强调运用人类在日常的思维逻辑中采用的思想方法与原则,如抽象、分类、继承、聚合、多态等。
面向对象的三大特征
封装 (Encapsulation)
继承 (Inheritance)
多态 (Polymorphism)
类与类之间的关系:
关联,继承,聚集,组合。
类是对一类事物描述,是抽象的、概念上的定义,好比汽车的图纸
对象是实际存在的该类事物的每个个体,因而也称实例(instance),好比一辆辆具体的汽车
面向对象程序设计的重点是类的设计
定义类其实是定义类中的成员(成员变量和成员方法)
5.1 类的成员变量
属 性:对应类中的成员变量
行 为:对应类中的成员方法
Field = 属性 = 成员变量,Method = (成员)方法 = 函数
面向对象思想的落地法则:
1. 设计类,并设计类的成员(成员变量与成员方法)
2. 通过类,来创建类的对象(类的实例化)
3. 通过“对象.属性”或“对象.方法”来调用,完成相应的功能
类及类的构成成分:属性,方法,构造器,代码块,内部类
类的属性(成员变量)
成员变量vs局部变量
都遵循变量声明的格式,都有作用域
不同:
1.声明的位置不一样,:成员变量声明在类里,方法外
局部变量:声明在方法里,方法的形参部分,代码块内。
2.成员变量的修饰符有四个:public,private,protected 缺省
局部变量没有修饰符,与所在的方法修饰符一致。
3.初始化值:一定会有初始化值。
成员变量:如果在声明的时候,不显示的赋值,那么不同的数据类型会有默认的初始化值 int, short , byte
→
→
0 , float, double
→
→
0.0 char
→
→
空格 Boolean
→
→
false ,string
→
→
null
局部变量一定要显示赋值,(没有默认的初始化值,除了形参以外)
4.二者在内存中存放的位置:成员变量存在于堆空间中,局部变量在栈空间中。
5.2 用构造器确保初始化
为每个类都定义一个初始化方法,该方法提醒在使用其对象之前要调用该方法。在java中,通过提供构造器,类的设计者可以确保每个对象都可以得到初始化。关于这个初始化方法的命名,存在两个问题:所取得任何名字都可能与类的某个成员名称相冲突;调用构造器是编译器的责任,必须让编译器知道应该调用哪个方法。所以采用了:构造器采用和类相同的名称。例如:
class Rock{
Rock(){
}
}
这样创建对象的时候,将会为对象分配存储空间,并调用相应的构造器,确保你在操作对象之前,它已经被初始化了
没有任何参数的构造器叫做默认构造器,即“无参构造器”。设计类的时候,不显示声明类的构造器的话,程序会默认提供一个无参构造器;一旦显示定义了类的构造器,那么默认的构造器就不再提供。其标准语法格式为:
修饰符 类名(参数列表){
初始化语句;
}
构造器的相关特征:
它具有与类相同的名称
它不声明返回值类型。(与声明为void不同)
不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值
根据参数不同,构造器可以分为如下两类:
隐式无参构造器(系统默认提供)
显式定义一个或多个构造器(无参、有参)
注 意:
Java语言中,每个类都至少有一个构造器
默认构造器的修饰符与所属类的修饰符一致
一旦显式定义了构造器,则系统不再提供默认构造器
一个类可以创建多个重载的构造器
父类的构造器不可被子类继承
类中属性赋值的先后顺序:1.属性的默认初始化2数据的显示赋值3.通过构造器给属性初始化4.创建对象后,通过对象。方法的形式给属性赋值
5.3 方法重载
方法区:含字符串常量
静态域:声明为static的变量
类的方法:提供某种功能的实现
1) 实例:
public void eat(){}
public String getName(){}
public void setName(String n){}
格式: 权限修饰符 返回值类型(void:无返回值) 方法名(形参){}
2)关于返回值类型:void:此方法不需要返回值
有返回值的方法:在方法的最后有return 语句。
3)方法内可以调用本类的其他方法或者属性,但是不能在方法内再定义方法。
当创建一个对象,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取得名字。通过使用名字,你可以引用所有的对象和方法。
相同的词表达不同的含义——即重载。在java中,方法的重载的格式等规定的一大原因是构造器造成的。构造器的名字由类名所决定,只能有一个构造器名,如果想要用多种方式创建一个对象怎么办?这个时候可以使用方法名相同但是参数列表不同的构造器来实现,即实现了方法的重载。
例如:
class Rock{
int i;
Rock(){
}
Rock(int value){
this.i = value;
}
Rock(int a,int b){
//.....
}
//.....
}
方法重载的特点:
与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。
例如:
public class Main{
public static void main(String[] args) {
Main m = new Main();
m.find();
m.find(1);
m.find(2,4);
}
public void find(){
System.out.println("ss");
}
void find(int i){
System.out.println(i);
}
int find(int i,int b){
System.out.println(b);
return i;
}
}
方法重载时候,要注意传入的实际参数和方法中的形参的比较。如果实参小于形参,实参会被提升。如果实参大于形参,没有进行类型转换,则出现异常。例如将上述例句中的void find(int i)
改为void find(byte i)
,看看是否出现异常。
5.4 this关键字
如果有同一类型的两个对象,分别为a,b。如何才能让这两个对象都能调用同一个方法?如何知道该方法是被哪一个对象调用?为此有一个专门的关键字:this。this关键字只能在方法的内部使用,表示对“调用方法的那个对象”的引用。this在构造器中使用,表示该构造器正在初始化的那个对象。
this表示当前对象或者当前正在创建的对象,可以调用类的属性,方法和构造器。
那么什么时候使用this关键字?
- 当在方法内需要用到调用该方法的对象时,就用this
- 当形参与成员变量重名的时候,如果方法内部需要使用成员变量,必须添加this来表明该变量是类成员
- 任意方法内,如果使用当前类的成员变量或者成员方法可以在其前面添加this,增加可读性。
public class Person{
int age;
String name;
public Person(int age,String name){
this.age = age;
this.name = name;
}
}
5.4.1在构造器中调用构造器
可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。this关键字可以做到这一点。
通常this都是代表“这个对象”或者“当前对象”,本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,就有了不同的含义,产生了对符合此参数列表的构造器的调用,这样,调用其他构造器就有了直接的途径。
class Person{ // 定义Person类
private String name ;
private int age ;
public Person(){ // 无参构造
System.out.println("新对象实例化") ;
}
public Person(String name){
this(); // 调用本类中的无参构造方法
this.name = name ;
}
public Person(String name,int age){
this(name) ; // 调用有一个参数的构造方法
this.age = age;
}
public String getInfo(){
return "姓名:" + name + ",年龄:" + age ;
}
}
注意事项:
- 使用this()必须放在构造器的首行!
- 使用this调用本类中其他的构造器,保证至少有一个构造器是不用this的。
5.4.2 static
static方法就是没有this的方法。在static方法内部是不能调用非静态方法,反过来可以。而且在没有任何对象的前提下,仅仅通过类本身来调用static方法。这是因为非静态方法在调用前要先创建该方法所属类的对象,而static则不用,static方法调用非静态方法时可能还未创建对象,所以不能通过static方法来调用非静态方法。
5.5 清理:终结处理和垃圾回收
java有垃圾收集器负责回收无用对象占据内存资源。但也有特殊情况:假定你的对象(并非使用new)获得了一块特殊的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块特殊内存。为了应对这种情况,java允许在类中定义一个名为finalize()方法。它的工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用器finalize()方法,并且在下一次垃圾回收动作发生时才会真正回收对象占用的内存。可以用finalize()方法,在垃圾回收时刻做一些重要的清理工作。
5.5.1 finalize()用途
finalize()不是作为通用的清理方法,关于finalize()方法的真正用途需牢记:垃圾回收只和内存有关。即使用垃圾回收器的唯一原因是回收程序不再使用的内存,对于垃圾回收相关的任何行为来说(尤其是finalize()方法),它们也必须同内存及回收有关。无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存,则这就将finalize()方法的需求限制到了一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。
这种特殊情况是怎么来的?之所以要有fianlize()方法,是由于在分配内存时可能采用了类似C语言中的做法,而非java中的通常做法。这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在java中调用非java代码的方式。本地方法目前只支持C和C++,但他们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非java代码中,也许会调用C的malloc()函数,系列在分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成内存泄露。而free()函数是C和C++中的函数,所以需要在finalize()中用本地方法调用它。
记住,无论是垃圾回收还是终结,都不保证一定会发生。如果java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
5.5.3 终结条件
通常,不能指望finalize(),必须创建其他的“清理”方法,关于finalize()方法,还有一个有趣的用法,叫做“对象终结条件的验证”。
当对某个对象不再感兴趣——也就是可以被清理。这个对象应该处于某种状态,使它占用的内存可以被安全地释放。例如,要是对象代表了一个打开的文件,在对象被回收前程序员应该关闭这个文件,只要对对象中存在没有被适当清理的部分,程序就存在隐晦的缺陷。finalize()可以用来最终发现这个情况。
示例:
//: initialization/TerminationCondition.java
// Using finalize() to detect an object that
// hasn't been properly cleaned up.
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error: checked out");
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper cleanup:
novel.checkIn();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();
}
} /* Output:
Error: checked out
*///:~
本例的终结条件是:所有book对象被当作垃圾回收前都应该签入(check in)。但在main方法中,由于程序员的错误,有一本书未被签入。要是没有finalize()方法来验证终结条件,很难发现这种错误。
备注:System.gc()
用于强制进行垃圾终结动作。
关于Java中垃圾收集器的工作原理等详细见《深入理解JVM》
5.6 成员初始化
java尽力保证:所有变量在使用之前都能够得到恰当的初始化。在使用变量时,注意判断变量是成员变量还是局部变量。如果是局部变量,在使用的时候没有显示的赋予初始值,会出错。
5.6.1 指定初始化
如果想为某个变量赋值,直接的方法就是在定义变量的地方为其赋值,或者用构造器与方法初始化非基本类型的对象,或者调用方法来初始化某些变量。(关于变量的初始化要注意初始化程序的顺序)
//: initialization/MethodInit2.java
public class MethodInit2 {
int i = f();
int j = g(i);
int f() { return 11; }
int g(int n) { return n * 10; }
} ///:~
5.7 构造器初始化
可以用构造器进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,但要牢记:无法阻止自动初始化的进行,他将在构造器被调用前发生。例如:
public class Counter{
int i;
Counter(){i=7}
//.....
}
这里i首先会被置0,然后变成7.对于所有的基本类型和对象的引用,包括在定义时已经指定初值的变量,这种情况都成立。
关于变量的初始化顺序如前面所述:1.属性的默认初始化2数据的显示赋值3.通过构造器给属性初始化4.创建对象后,通过对象.方法的形式给属性赋值
5.7.2 静态数据初始化
无论创建多少个对象,静态数据都只占一份存储内存区域。static关键字不能应用于局部变量,因此它只能作用与域。如果一个域是静态的,且没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始值就是null。
例如:
//: initialization/StaticInitialization.java
// Specifying initial values in a class definition.
import static net.mindview.util.Print.*;
class Bowl {
Bowl(int marker) {
print("Bowl(" + marker + ")");
}
void f1(int marker) {
print("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
print("Table()");
bowl2.f1(1);
}
void f2(int marker) {
print("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
print("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
print("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
print("Creating new Cupboard() in main");
new Cupboard();
print("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
} /* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*///:~
由输出可见,静态初始化只有在必要时刻才会进行。如果不创建table对象,也不引用table.b1或者table.b2,那么静态的Bowl b1和b2永远都不会被创建。
初始化顺序是先初始化静态对象,后为非静态对象。
5.7.3 显式的静态初始化
java允许将多个静态初始化动作组织成一个特殊的“静态子句”(静态块),例如:
public class Spoon{
static int i;
static{
i = 47;
}
}
即一段跟在static关键字后面的代码,与其他静态初始化动作一样,这段代码仅执行一次:首次生成这个类的一个对象时,首次访问属于那个类的静态数据成员时:
5.7.4 非静态实例初始化
实例:
//: initialization/Mugs.java
// Java "Instance Initialization."
import static net.mindview.util.Print.*;
class Mug {
Mug(int marker) {
print("Mug(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1 & mug2 initialized");
}
Mugs() {
print("Mugs()");
}
Mugs(int i) {
print("Mugs(int)");
}
public static void main(String[] args) {
print("Inside main()");
new Mugs();
print("new Mugs() completed");
new Mugs(1);
print("new Mugs(1) completed");
}
} /* Output:
Inside main()
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed
*///:~
5.8 数组初始化
数组是相同类型,用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过[]下标操作符来定义和使用的。定义一个数组:
int []a1
或者int a1[]
两种格式都可以。
上述方式只是定义了数组的引用,并没有给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。关于数组的初始化可以有如下几种方式:
1.直接赋值(在已经数组的具体元素时)
int []a1={1,2,3,4,5};
2.静态初始化(分配空间和赋值同时进行)
为数组分配空间
数组变量=new 数据类型[长度];
静态初始化:int a[] =new int[]{3,9,5};
3.动态初始化(只为数组申请分配空间)
int []a=new int[5];
如果不确定数组中的具体元素,可以使用第三种方式初始化。为数组分配好内存空间,每个数组元素都会有自己的默认初始值,int是0,boolean类型是false等等,与成员变量相同。
前述的是一维数组,如果是二维数组或不规则二维数组呢。
二维数组
声明与申请存储空间:
int mat[][];
mat = new int[2][3];
Or int mat[][]=new int[2][3];
Or 直接赋值 int mat[][]={{1,2,3},{4,5,6}};
Mat.length//返回二维数组的长度,即二维数组的行数
Mat[0].length//返回一维数组的长度,即二维数组的列数
数组一旦初始化,长度是不可变的。
不规则的二维数组。
int mat[][];
mat=new int[2][]; //申请第一维的存储空间,即数组的行数
mat[0]=new int[2]; //申请第二维的存储空间],每行的列数
mat[1]=new int[3];
其他操作与前述类似
5.8.1 可变参数列表
在java SE5中,添加了可变得参数列表,等价于数组
//下面采用数组形参来定义方法
public static void test(int a ,String[] books);
//以可变个数形参来定义方法
public static void test(int a ,String…books);
格式:形参:数据类型…形参名
该方法与同名的方法之间构成重载。
在调用可变个数的形参时,个数是0到无穷。
可变参数方法的使用与方法参数部分使用数组是一致的,即上述两种方法是一致的。
方法的参数部分有可变形参,需要放在形参声明的最后
5.9 枚举类型
在Java SE5中添加了enum关键字,使得我们在需要群组并使用枚举类型集的时候,可以很方便的处理。例如:
public enum Spiciness{
NOT,MILD,MEDIUM,HOT,FLAMING
}
这里创建了一个名为Spiciness的枚举类型,它具有5个具名值,由于枚举类型的实例是常量,因此按照命名惯例它们都是用大写字母表示。
为了使用enum,需要创建一个该类型的引用,将其赋值给某个实例;
//: initialization/SimpleEnumUse.java
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
} /* Output:
MEDIUM
*///:~
当你创建enum时,编译器会自动添加一些有用的特性,例如:它会创建toString()方法,显示某个enum实例的名字,编译器还会创建ordinal()方法,表示某个特定enum常量的声明顺序,以及static values()方法,按照声明顺序,产生由这些常量值构成的数组:
/: initialization/EnumOrder.java
public class EnumOrder {
public static void main(String[] args) {
for(Spiciness s : Spiciness.values())
System.out.println(s + ", ordinal " + s.ordinal());
}
} /* Output:
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
*///:~
在很大程度上,你是可以将enum当做任何类来处理。enum有一个特别实用的特性,即可以在是switch语句内使用:
//: initialization/Burrito.java
public class Burrito {
Spiciness degree;
public Burrito(Spiciness degree) { this.degree = degree;}
public void describe() {
System.out.print("This burrito is ");
switch(degree) {
case NOT: System.out.println("not spicy at all.");
break;
case MILD:
case MEDIUM: System.out.println("a little hot.");
break;
case HOT:
case FLAMING:
default: System.out.println("maybe too hot.");
}
}
public static void main(String[] args) {
Burrito
plain = new Burrito(Spiciness.NOT),
greenChile = new Burrito(Spiciness.MEDIUM),
jalapeno = new Burrito(Spiciness.HOT);
plain.describe();
greenChile.describe();
jalapeno.describe();
}
} /* Output:
This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.
*///:~
上述是关于enum的基本使用,关于详细的enum在后面19章用到具体讨论。