类与对象
java是一种面向对象的开发语言。java程序是由类与对象组成的。类与对象之间有什么关系呢?
类是构造对象的蓝图或模板。由类构造对象的过程,称之为创建类的实例。可知对象就是类的一种实例或具体实现。为什么为选用java语言做开发,这种面向对象的语言对开发有什么好处?
首先,从设计上,对一个问题,你可以暂且不管它的具体实现是什么,先把它抽象成一个对象,问题中涉及到的数据,变成对象中的实例域,求解问题的方法,变成对象中的方法,这样做可以分清要对哪些数据进行操作,逻辑上比较清晰。
其次,当问题的求解较为复杂时,可能有很多子方法,这时,我们可以按照模块化的方式,将子方法抽取出来,依次实现,这样可以很快求解出来,且不易出错,即使出错,也知道它错在哪个地方(某个方法错了,一目了然,相比较几百行代码而言)。
最后,如果求解问题时,发现与曾经求解的问题相似,是不是可以把之前求解的方法拿过来,然后在此基础上修改,方便许多。
在上面求解问题的过程中涉及到的一些技术:封装,继承
封装:类中的方法不能直接操作其他类的数据,而只能通过其他类实例化的对象调用方法,从而操作数据。
继承:一个类在另一个类的基础上扩展,扩展后的类具有被扩展类的方法和属性。
对象与对象变量
要想使用对象,首先得构造对象,java中通过构造器构造并初始化对象。构造器的名字与类名相同,例如Date类,想要构造一个Date对象
new Date();
这个表达式构造了一个表示当前日期的对象。当然,可以把对象保存在一个对象变量中。
Date date=new Date();
这里我要解释一下对象与对象变量的区别
Date date2;//date doesn‘t refer to any object
//此时date仅仅是一个对象变量,没有引用对象,它也不是一个对象,所以不能
//调用Date类的方法,就好比一个变量仅仅声明却没有初始化,所以不能使用。
//初始化有两种
date2=new Date();
date2=date;//date,date2引用同一变量
对象变量只是引用对象,并不是包含对象。
java中任何一个变量都是对存储在另外一个地方的对象的引用。
Date date=new Date();
new Date()返回一个Date对象的引用,这个引用存在了date变量中。
如果把一个对象变量设置为空,表明此时变量没有引用任何对象。
date=null;
和c++不同
c++中引用不能为空,且不能被赋值。其实java中的引用类似与c++中的指针。
自定义类
首先,自定义一个类
public class EmployeeTest
{
public static void main(String[] args)
{
Employee[] staff=new Employe[3];
staff[0]=new Employee("zhang",5000,2016,5,1);
staff[1]=new Employee("li",5000,2016,5,1);
staff[2]=new Employee("wang",5000,2016,5,1);
for(Employee e:staff)
{
e.raiseSalary(5);
}
for(Employee e:staff)
{
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
class Employee
{
//构造器
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
GregorianCalendar calendar=new GregorianCalendar(year,month-1,day);
hireDay=calendar.getTime();
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public void raiseSalary(double byPercent)
{
double raise=salary*byPercent/100;
salary+=raise;
}
private String name;
private double salary;
private Date hireDay;
}
}
构造器
public Employee(String n,double s,int year,int month,int day)
{
name=n;
salary=s;
GregorianCalendar calendar=new GregorianCalendar(year,month-1,day);
hireDay=calendar.getTime();
}
构造器与类同名,在构造(实例化)对象时,构造器被执行,初始化实例域。
new Employee("zhang",5000,2016,5,1);
构造器只能被new,而不能用当做方法来调用
staff.Employee("zhang",5000,2016,5,1);//错误
Note:
构造器与类同名
每个类可以有一个以上的构造器
构造器可以有0个或0个以上的参数
构造器没有返回值
构造器前面加new关键字,不能被对象调用
public Employee(String n,double s,int year,int month,int day)
{
String name=n;//错误
double salary=s;//错误
...
}
局部变量会覆盖掉实例域。
封装的优点
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public Date getHireDay()
{
return hireDay;
}
这些都是典型的访问器方法。由于它们只返回实例域值,因此又被称为域访问器。将name、 salary和hireDay域标记为public,以此来取代独立的访问器方法会不会更容易些呢?
关键在于name是一个只读域。一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保name域不会受到外界的干扰。
虽然salary不是只读域,但是它只能用raiseSalary方法修改。特别是一旦这个域值出现了错误,只要调试这个方法就可以了。如果salary域public的,破坏这个域值的捣乱者有可能会出没在任何地方。
在有些时候,需要获得或设置实例域的值。因此,应该提供下面三项内容:
? 一个私有的数据域;
? 一个公有的域访问器方法;
? 一个公有的域更改器方法。
这样做要比提供一个简单的公有数据域复杂些,但是却有着下列明显的好处:
1)可以改变内部实现,除了该类的方法之外,不会影响其他代码。
例如,如果将存储名字的域改为:
String firstName;
String lastName;
getName方法改为
return firstName+" "+lastName;
对于这点改变,程序的其他部分完全不可见。
当然,为了进行新旧数据表示之间的转换,访问器方法和更改器方法有可能需要做许多工作。但是,这将为我们带来了第二点好处。
2)更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。
例如, setSalary方法可以检查薪金是否小于0。
final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。例如,可以将Employee类中的name域声明为final,因为在对象构建之后,这个值不会再被修改,即没有setName方法。
class Employee
{
private final String name;
...
}
final修饰符大都应用于基本数据( primitive)类型域,或不可变( immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如, String类就是一个不可变的类)。对于可变的类,使用final修饰符可能会对读者造成混乱。例如,
private final Date date;
仅仅意味着存储在date变量中的对象引用在对象构造之后不能被改变,而并不意味着date 对象是一个常量。任何方法都可以对date引用的对象调用setTime更改器。
静态域与静态方法
在前面给出的示例程序中, main方法都被标记为static修饰符。下面讨论一下这个修饰符的含义。
静态域(类域)
如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。例如,假定需要给每一个雇员赋予惟一的标识码。这里给Employee类添加一个实例域id和一个静态域nextId:
class Employee
{
private int id;
private static int nextId=1;
}
现在,每一个雇员对象都有一个自己的id域,但这个类的所有实例将共享一个nextId域。换句话说,如果有1000个Employee类的对象,则有1000个实例域id。但是,只有一个静态域nextId。即使没有一个雇员对象,静态域nextId也存在。它属于类,而不属于任何独立的对象。
静态常量
静态变量使用得比较少,但静态常量却使用得比较多。例如,在Math类中定义了一个静态常量:
public class Math
{
public static final double PI=3.1415...;
...
}
在程序中,可以采用Math.PI的形式获得这个常量。
如果关键字static被省略, PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。
另一个多次使用的静态常量是System.out。它在System类中声明:
public class System
{
public static final PrintStream out=...;
}
前面曾经提到过,由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为public。然而,公有常量(即final域)却没问题。因为out被声明为final,所以,不允许再将其他打印流赋给它:
System.out=new PrintStream();//错,out is final
静态方法
静态方法是一种不能向对象实施操作的方法。例如, Math类的pow方法就是一个静态方法。表达式Math.pow(x,a)
计算幂。在运算时,不使用任何Math对象。换句话说,没有隐式的参数。
可以认为静态方法是没有this参数的方法(在一个非静态的方法中, this参数表示这个方法的隐式参数)。
因为静态方法不能操作对象,所以不能在静态方法中访问实例域。但是,静态方法可以访问自身类中的静态域。下面是使用这种静态方法的一个示例:
public static int getNextId()
{
return nextId;//nextId is static field
}
可以通过类名调用这个方法:
int id=Employee.getNextId();
这个方法可以省略关键字static吗?答案是肯定的。但是,需要通过Employee类对象的引用调用这个方法。
Note:
可以使用对象调用静态方法。例如,如果harry是一个Employee对象,可以用
harry.getNextId( )(或者this.getNextId(),或省略this,直接getNextId())代替Employee.getnextId( )。不过,这种方式很容易造成混淆,其原因是getNextId方法计算的结果与harry毫无关系。我们建议使用类名,而不是对象来调用静态方法。
在下面两种情况下使用静态方法:
? 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如: Math.pow)。
? 一个方法只需要访问类的静态域(例如: Employee.getNextId)
main方法
不需要使用对象调用静态方法。例如,不需要构造Math类对象就可以调用Math.pow。
main方法也是一个静态方法。main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。
Note:
每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧。
方法参数
参数传递给方法(函数)的两种形式:
值调用( call by value):表示方法接收的是调用者提供的值。
而引用调用( call by reference):表示方法接收的是调用者提供的变量地址。
Note:
一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
Java程序设计语言总是采用值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
例如,考虑下面的调用:
double percent=10;
harry.raiseSalary(percent);
不必理睬这个方法的具体实现,在方法调用之后, percent的值还是10。
下面再仔细地研究一下这种情况。假定一个方法试图将一个参数值增加至3倍:
public static void tripleValue(double x)
{
x=x*3;
}
调用这个方法
double percent=10;
tripleValue(percent);
percent的值还是10,看一下执行过程
1) x被初始化为percent值的一个拷贝(也就是10)。
2) x被乘以3后等于30。但是percent仍然是10(如图)。
3)这个方法结束之后,参数变量x不再使用。
然而,参数类型有两种
? 基本数据类型 (数字、布尔值)。
? 对象引用。
一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不
同了,可以很容易地利用下面这个方法实现将一个雇员的薪金提高两倍的操作:
public static void tripleSalary(Employee x)
{
x.raiseSalary(200);
}
当调用
harry=new Employee(...);
tripleSalary(harry);
时,具体的执行过程为:
1) x被初始化为harry值的拷贝,这里是一个对象的引用。
2) raiseSalary方法应用于这个对象引用。 x和harry同时引用的那个Employee对象的薪金提高了200%。
3)方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个薪金增至3倍的雇员对象(如图4-7所示)。
误区:引用调用
首先,编写一个交换两个雇员对象的方法
public static void swap(Employee x,Employee y)
{
Employee temp=x;
x=y;
y=temp;
}
如果Java程序设计语言对对象采用的是引用调用,那么这个方法就应该能够实现交换数据的效果:
Employee a=new Employee("A",...);
Employee b=new Employee("B",...);
swap(a,b);
但是,方法并没有改变存储在变量a和b中的对象引用。 swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝。相当于
//x refers to A,y to B
Employee temp=x;
x=y;
y=temp;
//now x refers to B,y to A
最终,白费力气。在方法结束时参数变量x和y被丢弃了。原来的变量a和b仍然引用这个方法调用之前所引用的对象(如图4-8所示)。
这个过程说明: Java程序设计语言对对象采用的不是引用调用,实际上, 对象引用进行的是值传递。
下面总结一下在Java程序设计语言中,方法参数的使用情况:
? 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
? 一个方法可以改变一个对象参数的状态。
? 一个方法不能实现让对象参数引用一个新的对象。
对象构造
对象重载
Employee staff=new Employee();
Employee staff1=new Employee(...);
重载( overloading):如果多个方法,有相同的名字、不同的参数,便产生了重载。
Note
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名( signature)。例如, String类有4个称为indexOf的公有方法。它们的签名是
indexOf(int)
indexOf(int,int)
indexOf(String)
indexOf(String,int)
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。
默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为flase、对象引用为null。然而,只有缺少程序设计经验的人才会这样做。确实,如果不明确地对域进行初始化,就会影响程序代码的可读性。
Note:
这是域与局部变量的主要不同点。必须明确地初始化方法中的局部变量。但是,
如果没有初始化类中的域,将会被初始化为默认值( 0、 false或null)。
例如,仔细看一下Employee类。假定没有在构造器中对某些域进行初始化,就会默认地将salary域初始化为0,将name、 hireDay域初始化为null。
但是,这并不是一种良好的编程习惯。如果此时调用getName方法或getHireDay方法,则会得到一个null引用,这应该不是我们所希望的结果:
harry=new Employee();//默认构造方法,域初始化默认值
Date date=harry.getHireDay();
calendar.setTime(date);//空指针异常
默认构造器
所谓默认构造器是指没有参数的构造器。例如, Employee类的默认构造器:
public Employee()
{
name="";
salary=0;
hireDay=new Date();
}
如果在编写一个类时没有编写构造器,那么系统就会提供一个默认构造器。这个默认构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。
如果类中提供了至少一个构造器,但是没有提供默认的构造器,则在构造对象时如果没有提供构造参数就会被视为不合法。例如,在例4-2中的Employee类提供了一个简单的构造器:
Employee(String name,double salary,int y,int m,int d)
对于这个类,构造默认的雇员属于不合法。也就是,调用
e=new Employee();
将会产生错误。
Note
请记住,仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。
如果在编写类的时候,给出了一个构造器,哪怕是很简单的,要想让这个类的用户能够采用下列方式构造实例:
new ClassName()
就必须提供一个默认的构造器(即不带参数的构造器)。当然,如果希望所有域被赋予默认值,可以采用下列格式:
public ClassName()
{
}
显式域初始化
由于类的构造器方法可以重载,所以可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值。这是一种很好的设计习惯。可以在类定义中,直接将一个值赋给任何域。例如:
class Employee
{
...
private String name="";
...
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量。在下面的例子中,可以调用方法对域进行初始化。仔细看一下Employee类,其中每个雇员有一个id域。可以使用下列方式进行初始化:
class Employee
{
...
public int assignId()
{
int r=nextId;
nextId++;
return r;
}
...
private int id=assignId();
}
Note:
在C++中,不能直接初始化实例域。所有的域必须在构造器中设置。但是,有
一个特殊的初始化器列表语法,如下所示:
Employee:Employee(String name,double salary,int ,int m,int d):name(n),salary(s),hireDay(y,m,d)
{
}
C++使用这种特殊的语法来调用域构造器。在Java中没有这种必要,因为对象没有子对象,只有指向其他对象的指针。
调用另一个构造器(this使用技巧)
关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:
public Employee(double s)
{
this("Employee#"+nextId,s)
nextId++;
}
当调用new Employee(60000)时, Employee(double)构造器将调用Employee(String, double)构造器。
初始化块
前面已经讲过两种初始化数据域的方法:
? 在构造器中设置值
? 在声明中赋值
实际上, Java还有第三种机制,称为初始化块( initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如,
class Employee
{
public Employee(String n,double s)
{
name=n;
salary=s;
}
public Employee()
{
name="";
salary=0;
}
...
private static int nextId;
private int id;
private String name;
private double salary;
...
//initialization block
{
id=nextId;
nextId++;
}
}
在这个示例中,无论使用哪个构造器构造对象, id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必须的,也不常见。通常,直接将初始化代码放在构造器中。
即使域定义在类的后半部分,在初始化块中仍然可以为它设置值。但是,为了避免循环定义,不要读取在后面初始化的域。
由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱。下面是调用构造器的具体处理步骤:
1)所有数据域被初始化为默认值( 0、 false或null)。
2)按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
3)如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
4)执行这个构造器的主体。
当然,应该精心地组织好初始化代码,这样有利于其他程序员的理解。例如,如果让类的构造器行为依赖于数据域声明的顺序,
那就会显得很奇怪并且容易引起错误。可以通过提供一个初始化值,或者使用一个静态的初始化块来对静态域进行初始化。前面已经介绍过第一种机制:
static int nextId=1;
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字static。
下面是一个示例。其功能是将雇员ID的起始值赋予一个小于10 000的随机整数。
static{
Random generator=new Random();
nextId=generator.nextId(10000);
}
在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值是 0、 false或null。
所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
上面说了那么多,到底实例域、静态域代码块、代码块、构造函数执行顺序是怎样的?
public class HelloA {
public HelloA(){
System.out.println("HelloA构造函数执行了");
}
{
System.out.println("HelloA代码块执行了");
}
static
{
System.out.println("HelloA static代码块执行了");
}
//private String str="HelloA实例域执行了";
}
public class HelloB extends HelloA {
public HelloB() {
this(s);
System.out.println("HelloB构造函数执行了");
}
public HelloB(String s) {
System.out.println("HelloB(String)构造函数执行了"+s);
}
{
System.out.println("HelloB代码块执行了");
}
static {
s="hhhhh";
System.out.println("HelloB static代码块执行了");
}
public static void main(String[] args) {
new HelloB();
}
private static String s;
}
/*
* 其中涉及:静态初始化代码块、构造代码块、构造方法 当涉及到继承时,按照如下顺序执行:
* 1、执行父类的静态代码块
* static {
* System.out.println("static A");
* }
* 输出:static A
* 2、执行子类的静态代码块
* static {
* System.out.println("static B");
* }
* 输出:static B
* 3、执行父类的构造代码块
* {
* System.out.println("I’m A class");
* }
* 输出:I‘m A class
* 4、执行父类的构造函数
* public HelloA() { }
* 输出:无
* 5、执行子类的构造代码块
* { System.out.println("I’m B class"); }
* 输出:I‘m B class
* 6、执行子类的构造函数
* public HelloB() { }
* 输出:无
*
* 那么,最后的输出为: static A static B I‘m A class I‘m B class
*/
对象析构与finalize方法
Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。
可以为任何一个类添加finalize方法。 finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。