第三节、类、接口、方法和垃圾回收
一、类
1.类和对象的区别
当你创建一个类时,你创建了一种新的数据类型。你可以使用这种类型来声明该种类型的对象。要获得一个类的对象需要两步:一,你必须声明该类类型的一个变量,这个变量没有定义一个对象。实际上它只是一个能够引用对象的简单变量;二,使用new运算符创建一个对象的实际的物理拷贝,并把对于该对象的引用赋给该变量。new运算符为对象动态分配(即在运行时分配)内存空间,并返回对它的一个引用。这个引用或多或少的是new分配给对象的内存地址。在Java中,所有的类对象都必须动态分配。
让我们再次复习类和对象之间的区别:类创建一种新的数据类型,该种类型能被用来创建对象。也就是,类创建了一个逻辑的框架,该框架定义了它的成员之间的关系。当你声明类的对象时,你正在创造该类的实例。因此,类是一个逻辑构造,对象有物理的真实性(也就是对象占用内存空间)。
例如下面代码段,你认为它完成了哪些操作?
HelloWorld hw=new HelloWorld();
HelloWorld hw2=hw;
第一句:创建了一个HelloWorld类的对象,然后将该对象的引用赋给变量 hw
第二句:将对第一句创建的对象的引用(不是对象的拷贝)赋给变量 hw2,hw和hw2指向同一个对象
类可以使用的修饰符有:
public: 公共的
abstract: 声明该类为抽象类,必须由其子类来提供完整的实现
final: 声明该类为final的,则不能被其他类继承
strictfp: 使用原始的浮点运算模型
protected、private: 当一个类声明在另一个类内部时,称之为内部类 ,由于它是所属类的成员,
故可以使用protected和private,而顶层类则不能使用。
2.构造器(constructor )
我们同过new运算符动态的为一个对象分配地址,它的通用格式如下:
class-var = new classname();
其中,class-var是所创建类类型的变量。classname是被实例化的类的名字。类名后面跟的圆括号指定了类的构造器。一个类的构造器必须和该类类名准确匹配,而且是无返回类型声明的,它的隐含返回类型是该类本身。
在每次创建类实例时必须初始化所有变量是一件非常耗时和令人厌倦的工作,你希望在这些对象创建时,它能够自己初始化。Java使用类构造器来进行这个工作,使对象创建时能设置对象变量的初始状态。Java中每一个类都有一个默认的构造器(即无带参数的构造器),但是当你为某个类定义了带参数的构造器时,如果你没有再定义无参数的构造器,那么该类的默认构造器是无效的(invalid),你不能再使用它来创建新的对象。
3.继承
继承是面向对象编程技术的一块基石,因为它允许创建分等级层次的类。Java中使用extends关键字来声明,例如我们创建了一个类A,如果想创建另一个类B并且B是继承A的,那么声明如下:
class B extends A{
} // A是超类 、 B是子类
Java术语中,被继承的类叫做超类(superclass),继承超类的类叫做子类(subclass),子类是超类一个专门用途的版本,它继承了超类定义的所有的实例变量和方法(即子类隐含包括了超类所有的实例变量和方法,但不包括构造器),并且为它自己增添独特的元素。但子类不能访问超类中被声明为为private的成员。
上面说到了子类继承超类中定义的所有的实例变量和方法,但是如果字类中有方法和超类中的方法同名会出现什么情况呢?这就涉及到Java中覆盖和重载的概念:当子类中定义的方法和超类中定义的方法有相同的名称,返回类型和参数列表时,我们称子类中的方法覆盖超类的方法(overridded method),超类中的被覆盖方法被子类隐藏 (即不能通过子类访问该方法)。覆盖方法有三点需要注意的:
* 当子类方法的名称和参数列表同超类的方法相同时,那么它的返回类型就必须声明和超类该方法的返回类型相同,否则会出现编译错误。若仅有名称和(或)返回类型相同时,只是方法重载(overloaded)。
* 覆盖方法的访问修饰符的访问范围不能比超类被覆盖的方法小,比如,超类中被覆盖的方法声明为protectd的,那么子类中的覆盖方法就必须声明为protected或public的,而不能声明为private。
* 覆盖方法必须抛出和超类中被覆盖方法相同的异常 。
超类变量可以引用子类对象,即:如果类B是继承类A的,那么可以这样声明:
B b1 = new A(); Java中引用变量的类型-----而不是引用对象的类型---决定了什么成员可以被访问。也就是说,当一个子类对象的引用被赋给一个超类引用变量时,你只能访问超类中定义的部分,如果超类中有方法被子类覆盖了,那么虽然你使用的超类类型的引用变量,但你只能访问字类中的覆盖方法,不能访问超类中被覆盖的方法。
例子:
package Examples;
public class ClassDemo{ // 超类
public ClassDemo(){
}
protected String newPrint(int i){
return "["+i+"]This is the method in SuperClass!";
}
}
class SubClassDemo extends ClassDemo{ //子类
// 覆盖超类中的newPrint方法
protected String newPrint(int s){
return String.valueOf(s);
}
// 使用不同参数重载newPrint方法
public String newPrint(String s){
return s+" after ";
}
}
class OverClassDemo{
public static void main(String[] args){
SubClassDemo scd=new SubClassDemo(); //创建子类的一个对象
System.out.println(scd.newPrint(10)); //调用覆盖方法
System.out.println(scd.newPrint(“Hello”)); //调用子类中的重载方法
ClassDemo cd=scd; //将子类引用变量赋给超类引用变量
System.out.println(cd.newPrint(99)); // 这里调用的还是子类的覆盖方法
// 下面的语句则会导致编译错误
// System.out.println(cd.newPrint("KK")); //意图调用子类中的重载方法,
// 而这是访问不到,编译器会提示参数必须是int类型的错误。
}
}
当一个子类需要引用它的直接超类时,它可以用关键字super来实现。super使用在子类中,用来引用超类中不为private的成员。在类层次结构中,构造器以派生的次序调用,从超类到子类。当你创建一个子类的对象时,子类的构造器的初始化次序是:先按派生的次序隐含的使用super()调用它的每个超类的默认的构造器,然后再调用子类中声明的初始化代码。你可以使用super()显式的调用直接超类的默认构造器,但这个super()语句必须放在构造器内所有代码之前。
4.抽象类
有些情况下,你希望定义一个超类,该超类定义了一种给定结构的抽象但是不提供任何完整的方法来实现。也就是说,创建一个只定义一个被它的所有子类共享的通用形式,由每个子类自己去填写细节。
这种情况下,由于这种超类中的有没有实际意义的方法,你希望确保子类真正重载了所有必须的方法。Java中使用abstract关键字来声明需要被子类实现(即覆盖)的方法。任何含有一个或多个抽象方法的类都必须声明成抽象类,即将类声明为abstract的。抽象类没有对象,也就是说它不能通过new运算符直接实例化。
抽象类的字类必须实现超类的所有抽象方法,或者不实现而将它自己也设置成抽象类。
5.内部类:
顾名思义,内部类是定义在另一个类内部的类。
* Group classes that logically belong together
* Have access to their enclosing class’scope
内部类的特性:
* 当你
二、接口
接口定义了一系列的抽象方法和变量。Java中使用interface来声明一个接口。下面是一个接口的通用形式:
access interface name {
return-type method-name1(parameter-list);
return-type method-name2(parameter-list);
type final-varname1=value;
type final-varname2=value;
// ….
return-type method-nameN(parameter-list);
type final-varnameN=value;
}
这里access要么是public,要么就没有用修饰符。接口中的方法没有方法体,虽然没有声明为abstract的,但它们本质上是抽象方法。在接口中指定的方法没有默认的实现,每个包含接口的类必需实现所有的方法。接口中可以声明变量,它们一般是final和static型的,它们必须以常量值初始化。如果接口本身定义成public的,那么接口里的所有方法和变量都是public的。
一旦接口被定义,那么一个或多个类可以实现该接口。一个类也可以实现多个接口。一个包括implements子句的类的一般形式如下:
access class classname [extends superclass] [implements interface[,interface…] ] {
// class body
}
这里access要么是public的,要么是没有修饰符的。类中实现接口的方法必须声明成public,而且实现方法的类型必须严格与接口定义中指定的类型匹配。
你可以声明一个接口类型的引用变量,而不是实现接口的类的引用。任何实现了该接口的类的实例都可被这样一个变量引用。
例子:
package Examples;
interface Callback{ //这里声明了一个接口
void callMethod(int param);
}
class Client implements Callback{
public void callMethod(int p){ //当实现接口的一个方法时,该方法必须被声明为public
System.out.println("callMethod called with "+p);
}
void clientMethod(){ //类在实现接口的同时也可以定义自己的方法、变量
System.out.println("Client/'s own method!");
}
}
class InterfaceTest{
public static void main(String[] args){
Callback cb; //声明一个接口类型的引用变量
Client c1=new Client();
c1.callMethod(10);
cb=c1;
cb.callMethod(99); //这样cb也可以调用callMethod()方法
}
}
如果一个类包含一个接口但是不完全实现接口定义的方法,那么该类必须定义成abstract型的。
你可以使用接口来引入多个类的共享常量,这样做只需要简单的声明一个接口,该接口包含被初始化的变量既可。如果一个类实现这样一个接口,那么接口中的所有变量名都将作为常量看待。
接口可以通过使用extends继承其他接口,当一个类实现一个继承了另一个接口的接口时,它必须实现接口继承链表中定义的所有方法。
这里总结一下接口的知识:
(1)使用interface关键字,必须声明为public或者不加任何修饰符。接口的修饰符隐含决定接口内方法和变量的修饰符。
(2)实现接口的类同接口一样,修饰符要么是public的,要么没有修饰符。而且如果类中实现了某个接口中的方法,那么所有实现的方法必须声明为public的。如果有类implements某个接口,但没有实现接口中所有的方法,那么该类必须声明为abstract的。
(3)可以声明一个接口的引用变量,那么所有实现此接口的类的对象引用都可以赋给此引用变量。
(4)一个接口可以继承(extends)其他接口,实现一个继承了其他接口的接口时,必须实现该接
口中声明的和所继承的所有其他接口中的方法。
三、方法重载
在Java中,同一个类中的2个或2个以上的方法可以有同一个名字,只要它们的参数声明不同即可。 在这种情况下,该方法就被称为重载方法(overloaded method)。方法重载是Java实现多态性的一种方式。当一个重载方法被调用时,Java用参数的类型和(或)数量来表明实际调用的是哪个方法。因此,每个重载方法的参数的类型和(或)数量必须是不同的。虽然每个重载方法可以有不同的返回类型,但返回类型并不足以区分所使用的是哪个方法。当Java调用一个重载方法时,参数与调用匹配的方法被执行。
四、垃圾回收
由于使用new运算符来为对象动态地分配内存,你可能想知道这些对象是如何撤销的以及他们的内存在以后的重新分配时是如何释放的。在一些语言,例如C++中,用delete运算符来手工地释放动态分配的对象的内存。Java使用一种不同的、自动地处理重新分配内存的方法:垃圾回收(garbage collection)技术。由JVM自动进行垃圾回收。当你的程序不再引用某对象时,则该对象就会被考虑收集到垃圾收集堆里。仅因为你的程序失去对一个对象的引用,并不意味着JVM会立刻收回这个对象的内存,甚至根本不收回;Java将垃圾收集进程作为一个低优先级线程运行,JVM仅会在需要更多的内存以继续执行程序时才会进行垃圾收集。 例如:
package Examples;
public class GLDemo{
public static void main(String[] args){
String s=”Hello”
String s= s + new String(“World”); // s指向一个新的对象”HelloWorld”,
//此时字符串对象”Hello”可以被作为垃圾收集
int[] arr={3,4,5,6,7};
arr[2]=99;
arr=new in[10]; // arr指向一个新的数组,数组对象{3, 4 , 99 , 6 , 7}可以被作为垃圾收集
s=null; // 将s设置为null使我们失去了对字符串对象”HelloWorld”的引用,
// 所以这个对象也可被作为垃圾收集
}
}
* finalize()方法:
有时当撤销一个对象时,需要完成一些操作。例如,如果一个对象正在处理的是非Java资源,如文件句柄或window字符字体,这时你要确认一个对象被撤销以前要保证这些资源被释放。为处理这样的状况,Java提供了被称为收尾(finalization)的机制。使用该机制你可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。要给一个类增加收尾(finalizer),你只要定义finalize()方法即可。Java回收该类的一个对象时,就会调用这个方法。在finalize()方法中,你要指定在一个对象被撤销前必须执行的操作。
finalize()方法的通用格式如下:
protected void finalize(){}
Java会为程序中每一个对象只调用一次finalize()方法。finalize()被声明为protected,不返回任何值,而且抛出一个Throwable对象。你也可以调用自己对象的finalize()方法。如果你覆盖finalize()方法时,则一定要调用超类的此方法。
显示的调用垃圾收集程序:
分成两步:1、获取一个代表当前运行时的对象;2、调用这个对象的gc()方法。下面是这种步骤的一个程序片断:
Runtime rt=Runtime.getRuntime();
rt.gc();
Runtime对象定义了四个方法:
gc() :在这个方法返回前,JVM会已经执行了垃圾收集。
runFinalization() :在这个方法返回前,JVM已经为所有还没有运行finalize()方法的等候垃圾收集
的对象执行了finalize()方法。
totalMemory() : 这个方法返回一个int值,它包括JVM中为分配对象所提供的可用内存总数。
freeMemory() : 这个方法返回一个int值,这个数值总是小于totalMemory()的返回值。