|
类是Java语言面向对象编程的基本元素,它定义了一个对象的结构和行为。
在Java程序里,你要表达的概念封装在某个类里。一个类定义了一个对象的结构和它的功能接口,功能接口称为成员函数。当Java程序运行时,系统用类的定义创建类的实例,类的实例是真正的对象。类定义的一般形式如下: class classname extends superclassname { type instance-variable1; type instance-variable2; ... type instance-variableN; type methodname1(parameter-list) { method-body; } type methodname2(parameter-list) { method-body; } ... type methodnameN(parameter-list) { method-body; } } 这里,classname和superclassname是合法的标识符。关键词extends用来表明classname是superclassname派生的子类。有一个类叫做Object,它是所有Java类的根。如果你想定义Object的直接子类,你可以省略extends子句,编译器会自动包含它。 下面是一个简单的类的定义: class University { } 5.1 对象实例 类名可以作为变量的类型来使用,如果一个变量的类型是某个类,那么它将指向这个类的实例,称为对象实例。所有对象实例和它们的类型(某个类)的子类的实例都是相容的。就象可以把byte型的值赋给int型的变量一样,你可以把Object的子类的任何实例赋给一个Object型的变量。一个实例是类模板的单独的拷贝,带有自己的称为实例变量的数据集。每个实例也可以作为一个对象。 当你定义一个变量的类型是某个类时,它的缺省值是null,null是Object的一个实例。对象null没有值,它和整数0不同。下面这个例子中,声明变量u的类型是类University。 University u; 这里,变量u的值是null。 5.2 实例变量 Java通过在类定义的大括号里声明变量来把数据封装在一个类里。这里的变量称为实例变量。下面的例子定义了一个叫做University的类,它有两个实例变量:name和city。 class University{ String name, city; } 5.3 new操作符 操作符new用来生成一个类的实例,下面这个例子生成了类University的一个实例,存放在变量u中。 University u = new University( ); 在此例中,变量u指向这个对象,但并不真正包含这个对象。你可以用多个变量指向同一个对象。下面的例子中,创建了一个University的对象,但创建了两个指向它的变量。 University u = new University(); University u2 = u; 对u2所指向的对象的任何改动都会对u所指向的对象起作用,因为它们是同一个对象。对u和u2的赋值只是把它们指向这个对象,既没有分配内存,也没有复制这个对象的任何部分。对u的再赋值只是简单地去掉了u和原来对象的联系,并不影响对象本身,下面的例子说明了这种情况。 University u = new University( ); University u2 = u; u = null; 尽管u被赋值为null,u2仍指向原来由操作符new创建的对象。在前面的例子里,我们生成了一个对象并且指向了它两次。这就允许两个变量改变同一个对象。创建一个新的对象时,可直接对它的实例变量赋值。每个对象都有它所属类的实例变量的拷贝,每个对象的实例变量都是和其他对象的实例变量分离的,所以改变一个对象的实例变量不会影响其他对象的实例变量。下面的例子创建了两个University的对象,并对它们分别赋值: class TwoUniversity { public static void main(String args[]) { University u1 = new University( ); University u2 = new University( ); u1.name = "北京大学"; u1.city = "北京"; u2.name = "清华大学"; u2.city = "北京"; System.out.println("大 学:" + u1.name + " 城 市:" + u1.city); System.out.println("大 学:" + u2.name + " 城 市:" + u2.city); } } 这个例子创建了两个University的对象,并且对它们的name、city分别赋了不同的值,这说明这两个对象是真正分离的。下面是该程序运行后的输出结果。 C:/>java TwoUniversity 大 学:北京大学城市:北京 大 学:清华大学城市:北京 5.4 点(.)操作符 点(.)操作符用来接收一个对象的实例变量和成员函数。下面是用点操作符来接收实例变量的一般形式。 objectreference.variablename 这里objectreference是一个对象实例,variablename是这个对象里你想接收的实例变量。下面的程序段说明了怎样用点操作符来给实例变量赋值: u.name = "北京大学"; u.city = "北京"; 下面说明怎样用点操作符来得到实例变量的值: System.out.println("大学:" + u.name + " 城市:" + u.city); 通过向类University里加入一个成员函数main,我们创建了一个完整的例子,它使用了new操作符来创建一个University,用点操作符来赋值,然后打印结果。 class University { String name, city; public static void main(String args[]) { University u = new University( ); u.name = "北京大学"; u.city = "北京"; System.out.println("大学:" + u.name + " 城市:" + u.city); } } 运行这个程序后,就会得到下面的结果。 C:/>java University 大学: 北京大学城市:北京 5.5 成员函数定义 成员函数,是类的功能接口,是类定义里的一个子程序,在类的定义里和实例变量处于同一级别。你必须通过一个类的实例来调用成员函数。成员函数可以不用点操作符而直接使用实例变量。成员函数带有输入参数,具有某种类型的返回值。成员函数定义的一般形式如下: type methodname ( formal-parameter-list ) { method-body; } 这里type指的是成员函数的返回值的类型,如果没有返回值,就用无值(void)类型。methodname可以是任何合法的标识符,但不能与当前的类名相同。formal-parameter-list是用逗号分隔的类型、标识符对的序列。如果没有参数,括号里就是空的。还是用我们的University的例子,下面的成员函数用来初始化两个实例变量。成员函数是在类的大括号内定义的,和实例变量所处的范围相同。 class University { String name, city; void init(String a, String b) { name = a; city = b; } } 注意,我们这里直接给name和city赋值,而没有象以前那样用u1.name。这是因为每个成员函数都在类的个别实例内执行。我们创建的类的实例具有它自己的实例变量,所以成员函数可直接使用它们。 5.6 成员函数调用 可以用点(.)操作符来调用一个类的实例的成员函数。成员函数调用的一般形式如下: objectreference.methodname( parameter-list ); 这里,objectreference是指向某个对象的变量,methodname是objectreference所属类的一个成员函数,parameter-list是用逗号分隔的变量或表达式的序列,它们要与该成员函数的定义的参数个数及类型匹配。在这个例子里,我们可以对任何University对象调用成员函数init来给name和city赋值。下面的程序段说明了怎样完成这个工作: University u = new University( ); u.init("北京大学", "北京"); 这个例子创建了University的一个实例,存放在u中。通过点操作符来调用这个实例的init成员函数,把"北京大学"和"北京"分别传递给参数a和b。在init成员函数内部,name和city直接指向u所指向的对象的实例变量。把name赋值为"北京大学",city赋值为"北京",然后返回。在这个例子里,init被定义为无值(void)返回类型。在进行这个成员函数调用后,u指向这个name值和city值改变了的University对象。 5.7 this Java有一个特殊的实例值叫this,它用来在一个成员函数内部指向当前的对象。在前面的例子里,我们调用u.init,一旦进入init成员函数内部,this就会指向u所指向的对象。 在Java里,在同一个范围定义两个相同名字的局部变量是不可以的。有趣的是,局部变量、成员函数的参数可以和实例变量的名字相同。前面我们没有用name和city作为成员函数init的参数名字,因为这样它们在成员函数的范围里就把实例变量name和city隐藏了,即name指向参数name,隐藏了实例变量name。this让我们可以直接指向对象本身。 下面是另一个版本的init,用name和city作为参数名字,用this来接收当前对象的实例变量: void init(String name, String city) { this.name = name; this.city = city; } 下面是带有新的init初始成员函数的TwoUniversity例子。 class University { String name, city; void init(String name, String city) { this.name = name; this.city = city; } } class TwoUniversityInit { public static void main(String args[]) { University u1 = new University( ); University u2 = new University( ); u1.init("北京大学", "北京"); u2.init("清华大学", "北京"); System.out.println("大学:" + u1.name + " 城市:" + u1.city); System.out.println("大学:" + u2.name + " 城市:" + u2.city); } } 5.8 构造函数(Constructor) 每创建一个类的实例都去初始化它的所有变量是乏味的。如果一个对象在被创建时就完成了所有的初始工作,将是简单的和简洁的。因此,Java在类里提供了一个特殊的成员函数,叫做构造函数(Constructor)。 一个构造函数是对象被创建时初始对象的成员函数。它具有和它所在的类完全一样的名字。一旦定义好一个构造函数,创建对象时就会自动调用它。构造函数没有返回类型,即使是void类型也没有。这是因为一个类的构造函数的返回值的类型就是这个类本身。构造函数的任务是初始化一个对象的内部状态,所以用new操作符创建一个实例后,立刻就会得到一个清楚、可用的对象。 下面这个例子里,用构造函数取代了成员函数init。 class University { String name, city; University(String name, String city) { this.name = name; this.city = city; } } class UniversityCreate { public static void main(String args[]) { University u = new University("北京大学", "北京"); System.out.println("大 学:" + u.name + " 城 市:" + u.city); } } new语句中类名后的参数是传给构造函数的。 5.9 成员函数重载 对于几个意义相近的成员函数,有时使用相同的名字便于理解。因此,Java语言实现了成员函数重载,即可以创建几个名字相同、参数不同的成员函数。成员函数重载提供了Java的多态行为。 下面的例子用到了重载: class University { String name, city; University(String name, String city) { this.name = name; this.city = city; } University( ) { name = "北京大学"; city = "北京"; } } class UniversityCreateAlt { public static void main(String args[]) { University u = new University( ); System.out.println("大学:" + u.name + " 城市:" + u.city); } } 这个例子创建了一个University对象,调用了第二个构造函数。下面是它的运行结果。 C:/>java UniversityCreateAlt 大学:北京大学城市:北京 一个构造函数可以调用另一个构造函数来创建实例。例如: class University { String name, city; University(String name, String city) { this.name = name; this.city = city; } University( ) { this("北京大学", "北京"); } } 第二个构造函数调用了第一个构造函数来完成实例的初始化。你也可以用重载来创建一般的成员函数。下面这个例子里有University类的两个版本的samecity成员函数。samecity判断一个大学是否在一个城市里或一个大学和另一个大学是否在同一个城市里。一个成员函数用city作参数,另一个用University对象作参数。 class University { String name, city; University(String name, String city) { this.name = name; this.city = city; } boolean samecity(String city) { if (city.equals(this.city)) return true; else return false; } boolean samecity(University u) { return samecity(u.city); } } class UniversityCity { public static void main(String args[]) { String city = "上海"; University u1 = new University("北京大学","北京"); University u2 = new University("清华大学", "北京"); System.out.println("u1 = " + u1.name + ", " + u1.city); System.out.println("u2 = " + u2.name + ", " + u2.city); System.out.println("city = " + city); System.out.println("u1.samecity(u2) = " + u1.samecity(u2)); System.out.println("u1.samecity(city) = " + u1.samecity(city)); } } 下面是该程序的运行结果: C:/>java UniversityCity u1 = 北京大学, 北京 u2 = 清华大学, 北京 city = 上海 u1.samecity(u2) = true u1.samecity(city) = false 5.10 继承 第二个基本的面向对象机制是继承。继承是关于有层次关系的类之间的概念。一个类的后代可以继承它的祖先的所有变量和成员函数,就象创建自己的一样。一个类的直接父亲叫做它的超类(superclass)。一旦你创建了一个象University这样的类,创建它的子类是很简单的。一个类的子类是它的继承了实例变量和成员函数的特殊的版本。在这个例子里,我们 把University类派生为含有叫做country的第三个元素的子类。 class UniversityWorld extends University { String country; UniversityWorld(String name, String city, String country) { this.name = name; this.city = city; this.country = country; } UniversityWorld( ) { this("北京大学", "北京", "中国"); } } 关键词extends用来表示我们要创建University的子类。name和city不需再在UniversityWorld中进行声明,因为它们是从University中继承的。Java允许在UniversityWorld中声明变量name和city,但这会隐藏University中的name和city,是与使用子类的目的相矛盾的,应当避免。 在UniversityWorld的实例中,name、city和country的地位是一样的。 5.11 super 在UniversityWorld的例子里,有一段代码和它的超类University的重复,这段代码是初始化name和city的: this.name = name; this.city = city; 就象在University例子中用this指向第一个构造函数一样,在Java里有另一个变量叫做super,它直接指向超类的构造函数。 下面这个例子用super来初始化变量name和city,然后打印出这个对象的内容: class UniversityWorld extends University { String country; UniversityWorld(String name, String city, String country) { super(name, city); //调用了构造函数University(name, city) this.country = country; } public static void main(String args[]) { UniversityWorld u = new UniversityWorld("北京大学", "北京", "中国"); System.out.println("大学:" + u.name + " 城市:" + u.city + " 国家:" + u.country); } } 下面是运行结果: C:/>java UniversityWorld 大学:北京大学 城市:北京 国家:中国 5.12 成员函数的覆盖 这个University的新的子类继承了它的超类的成员函数samecity。但这个成员函数samecity判断的是两个城市的名字,这是不够的,因为有可能两个名字一样的城市属于不同的国家,我们要用同时判断城市和国家的成员函数来覆盖它。下面就是实现覆盖的例子: class University { String name, city; University(String name, String city) { this.name = name; this.city = city; } boolean samecity(String city) { if (city.equals(this.city)) return true; else return false; } boolean samecity(University u) { return samecity( u.city); } } class UniversityWorld extends University { String country; UniversityWorld(String name, String city, String country) { super(name, city); this.country = country; } boolean samecity(String city, String country) { if (city.equals(u.city) && country.equals(u.country)) return true; else return false; } boolean samecity(UniversityWorld other) { return distance(other.city, other.country); } } class UniversityWorldCity { public static void main(String args[]) { String city = "上海"; String country = "中国"; UniversityWorld u1 = new UniversityWorld("北京大学", "北京", "中国"); UniversityWorld u2 = new UniversityWorld("清华大学", "北京", "中国"); System.out.println("u1 = " + u1.name + ", " + u1.city + ", " + u1.country); System.out.println("u2 = " + u2.name + ", " + u2.city+ ", " + u2.country); System.out.println("city = " + city + ", country = " + country); System.out.println("u1.samecity(u2) = " + u1.samecity(u2)); System.out.println("u1.samecity(city, country) = " + u1.samecity(city, country)); } } 下面是输出结果: C:/>java UniversityWorldCity u1 = 北京大学, 北京, 中国 u2 = 清华大学, 北京, 中国 city = 上海, country = 中国 u1.samecity(u2) = true u1.samecity(city, country) = false 5.13 动态成员函数发送 当你用点操作符调用一个对象实例的成员函数时,对象实例所属的类在编译时要被检查,以确保调用的成员函数在该类中是存在的。在运行时对象实例可以指向所声明类型的子类的实例。在这些情况下,如果子类覆盖了要调用的成员函数,Java就用实例来决定调用哪一个成员函数。如下面的例子,两个类是子类和超类的关系,子类覆盖了超类的成员函数。 class A { void callme( ) { System.out.println("在A的callme成员函数里"); } } class B extends A { void callme( ) { System.out.println("在B的callme成员函数里"); } } class Dispatch { public static void main(String args[]) { A a = new B( ); a.callme( ); } } 有趣的是,在成员函数main里,我们把变量a声明为类型A,然后把类B的一个实例存放到它上面。我们在a上调用成员函数callme,Java编译器确定在类A确实有成员函数callme,但是在运行时,由于a事实上是B的实例,所以调用B的callme,而不调用A的。下面是运行结果: C:/>java Dispatch 在B的callme成员函数里 5.14 final 在缺省情况下,所有的成员函数和实例变量都可以被覆盖。如果你希望你的变量或成员函数不再被子类覆盖,可以把它们声明为final。这意味着将来的实例都依赖这个定义。例如: final int FILE_NEW = 1; final int FILE_OPEN = 2; final int FILE_SAVE = 3; fianl int FILE_SAVEAS = 4; final int FILE_QUIT = 5; final变量用大写标识符是一个一般的约定。 5.15 静态 如果你想要创建一个可以在实例的外部调用的成员函数,那么你只需声明它为静态的(static),它就会正常运行。静态成员函数只能直接调用其他静态成员函数,而不能以任何方式使用this或super。你也可以把变量声明为静态的。如果你想初始化一个静态变量,你可以用static声明一个恰好在类调用时执行一次的程序块。下面的例子是一个带有一个静态成员函数,几个静态变量,和一个静态初始块的类。 class Static { static int a = 3; static int b; static void method(int x) { System.out.println("x = " + x); System.out.println("a = " + a); System.out.println("b = " + b); } static { System.out.println("静态初始块"); b = a * 4; } public static void main(String args[]) { method(42); } } 一旦这个类被调用,所有的静态变量都被初始化,a被赋为3,然后运行static块,这将打印出一段消息,并且把b赋为a*4,即12。然后解释器调用main成员函数,它调用了成员函数method,参数x为42。这三个println语句打印了两个静态变量a、b和局部变量x。下面是运行结果: C:/>java Static 静态初始块 x = 42 a = 3 b = 12 一个静态成员函数可以通过它所属的类名来调用。象调用实例变量一样,你可以用点操作符通过类名来调用静态成员函数和静态变量。Java就是这样实现了全局函数和全局变量。下面的例子里,我们创建了带有一个静态成员函数和两个静态变量的类。第二个类可以通过名字直接来调用第一个类的静态成员函数和静态变量。 class staticClass { static int a = 42; static int b = 99; static void callme( ) { System.out.println("a = " + a); } } class StaticByName { public static void main(String args[]) { StaticClass.callme( ); System.out.println("b = " + staticClass.b); } } 下面是运行结果: C:/>java staticByName a = 42 b = 99 5.16 抽象 有时你需要定义一个给出抽象结构、但不给出每个成员函数的完整实现的类。如果某个成员函数没有完整实现,必须要由子类来覆盖,你可把它声明为抽象(abstract)型。含有抽象型成员函数的类必须声明为抽象的。为了把一个类声明为抽象的,你只需在类定义的class关键词前放置关键词abstract。这些类不能直接用new操作符生成实例,因为它们的完整实现还没有定义。你不能定义抽象的构造函数或抽象的静态成员函数。抽象类的子类或者实现了它的超类的所有抽象的成员函数,或者也被声明为抽象的。 下面例子是一个带有抽象成员函数的类,其后是一个实现了该成员函数的类。 abstract class A { abstract void callme( ); void metoo( ) { system.out.println("在A的metoo成员函数里"); } } class B extends A { void callme( ) { System.out.println("在B的callme成员函数里"); } } class Abstract { public static void main(String args[]) { A a = new B( ); a.callme( ); a.metoo( ); } } 下面是运行结果: C:/>java Abstract 在B的callme成员函数里 在A的metoo成员函数里 本章小结 1. 类是Java语言面向对象编程的基本元素,它定义了一个对象的结构和功能。 2. Java通过在类定义的大括号里声明变量来把数据封装在一个类里,这里的变量称为实例变量。 3. 成员函数,是类的功能接口,是类定义里的一个子程序,在类的定义里和实例变量处于同一级别。 |