第4章 对象和类
1.面向对象
类:构造对象的模板,创建类的实例:由类构造对象的过程,封装,继承;
对象:对象的特性——对象的行为,对象的状态,对象的标识;
类之间的关系:
依赖(“user-a”),一个类的方法操纵另一个类的对象
聚合(“has-a”),类A的对象包含类B的对象
继承(“is-a”),类A 拓展了类B
2.使用预定义类
并不是所有的类都具有面向对象的特征,例如Math类;
对象不会自动被初始化为null,而必须通过调用new或将它们设置为null进行初始化(可以将java对象看成是c++的对象指针);
3.用户自定义类
主力类:类中没有main方法,有自定义的实例域和实例方法;
在一个源文件中,只能有一个共有类,但可以有任意数目的非公有类;
多个源文件的使用: 编若两个类单独存在于两个源文件,则编译 javac Employee*.java 或者 javac EmployeeTest.java (会自动搜索Employee.java)
构造器: 构造器与类同名, 每个类可以有一个以上的构造器, 构造器可以有0个,1个或多参数, 构造器没有返回值, 伴随new操作一起使用;
隐式参数和显式参数: 关键字this表示隐式参数;import java.util.*;
1 public class StaticTest 2 { 3 public static void main(String[] args) 4 { 5 // fill the staff array with three Employee objects 6 Employee[] staff = new Employee[3]; 7 8 staff[0] = new Employee("Tom", 40000); 9 staff[1] = new Employee("Dick", 60000); 10 staff[2] = new Employee("Harry", 65000); 11 12 // print out information about all Employee objects 13 for (Employee e : staff) 14 { 15 e.setId(); 16 System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary=" 17 + e.getSalary()); 18 } 19 20 int n = Employee.getNextId(); // calls static method 21 System.out.println("Next available id=" + n); 22 } 23 } 24 25 class Employee 26 { 27 private String name; 28 private double salary; 29 private int id; 30 private static int nextId = 1; 31 public Employee(String n, double s) 32 { 33 name = n; 34 salary = s; 35 id = 0; 36 } 37 38 public String getName() 39 { 40 return name; 41 } 42 43 public double getSalary() 44 { 45 return salary; 46 } 47 48 public int getId() 49 { 50 return id; 51 } 52 53 public void setId() 54 { 55 id = nextId; // set id to next available id 56 nextId++; 57 } 58 59 public static int getNextId() 60 { 61 return nextId; // returns static field 62 } 63 64 public static void main(String[] args) // unit test 65 { 66 Employee e = new Employee("Harry", 50000); 67 System.out.println(e.getName() + " " + e.getSalary()); 68 } 69 70 71 }
4.静态域和静态方法
静态域:若将域定义为static,每个类中只有一个这样的域。而每个对象对于所有的实例域都有一份自己的拷贝;属于类,不属于任何独立的对象;
静态方法:静态方法是一种不能向对象实施操作的方法,因此不能访问实例域,但可以访问自身类中的静态域。
使用静态方法的情形:
1、一个方法不需要访问对象状态,其所有参数都通过显示参数提供。如Math.pow(x,a)
2、一个方法只需要访问静态域。
main方法:main方法也是一个静态方法;
每一个类都可以有一个main方法,进行单元测试; 此例中,java Employee 进行独立测试, java StaticTest,则Employee类中的main方法不会被执行
5.方法参数
java采用按值传输;
一个方法不能修改一个基本数据类型的参数(即数值型和布尔型);
一个方法可以改变一个对象参数的状态;
一个方法不能让对象参数引用一个新的对象;
1 public class ParamTest 2 { 3 public static void main(String[] args) 4 { 5 /* 6 * Test 1: Methods can't modify numeric parameters 7 */ 8 System.out.println("Testing tripleValue:"); 9 double percent = 10; 10 System.out.println("Before: percent=" + percent); 11 tripleValue(percent); 12 System.out.println("After: percent=" + percent); 13 14 /* 15 * Test 2: Methods can change the state of object parameters 16 */ 17 System.out.println("\nTesting tripleSalary:"); 18 Employee harry = new Employee("Harry", 50000); 19 System.out.println("Before: salary=" + harry.getSalary()); 20 tripleSalary(harry); 21 System.out.println("After: salary=" + harry.getSalary()); 22 23 /* 24 * Test 3: Methods can't attach new objects to object parameters 25 */ 26 System.out.println("\nTesting swap:"); 27 Employee a = new Employee("Alice", 70000); 28 Employee b = new Employee("Bob", 60000); 29 System.out.println("Before: a=" + a.getName()); 30 System.out.println("Before: b=" + b.getName()); 31 swap(a, b); 32 System.out.println("After: a=" + a.getName()); 33 System.out.println("After: b=" + b.getName()); 34 } 35 36 public static void tripleValue(double x) // doesn't work 37 { 38 x = 3 * x; 39 System.out.println("End of method: x=" + x); 40 } 41 42 public static void tripleSalary(Employee x) // works 43 { 44 x.raiseSalary(200); 45 System.out.println("End of method: salary=" + x.getSalary()); 46 } 47 48 public static void swap(Employee x, Employee y) 49 { 50 Employee temp = x; 51 x = y; 52 y = temp; 53 System.out.println("End of method: x=" + x.getName()); 54 System.out.println("End of method: y=" + y.getName()); 55 } 56 } 57 58 class Employee // simplified Employee class 59 { 60 private String name; 61 private double salary; 62 63 public Employee(String n, double s) 64 { 65 name = n; 66 salary = s; 67 } 68 69 public String getName() 70 { 71 return name; 72 } 73 74 public double getSalary() 75 { 76 return salary; 77 } 78 79 public void raiseSalary(double byPercent) 80 { 81 double raise = salary * byPercent / 100; 82 salary += raise; 83 } 84 85 86 }
Java中只有按值传递,没有按引用传递!
方法参数有两种类型:
(1) 基本数据类型(数字,布尔型)
1 ... ... 2 //定义了一个改变参数值的函数 3 public static void changeValue(int x) { 4 x = x *2; 5 } 6 ... ... 7 //调用该函数 8 int num = 5; 9 System.out.println(num); 10 changeValue(num); 11 System.out.println(num); 12 ... ... 13
值传递的过程:
num作为参数传递给changeValue()方法时,是将内存空间中num所指向的那个存储单元中存放的值,即"5",传送给了changeValue()方法中的x变量,而这个x变量也在内存空间中分配了一个存储单元,这个时候,就把num的值5传送给了这个存储单元中。此后,在changeValue()方法中对x的一切操作都是针对x所指向的这个存储单元,与num所指向的那个存储单元没有关系了!自然,在函数调用之后,num所指向的存储单元的值还是没有发生变化,这就是所谓的“值传递”!值传递的精髓是:传递的是存储单元中的内容,而非地址或者引用!
(2)对象引用
1 ... ... 2 class person { 3 public static String name = "Jack"; 4 ... ... 5 } 6 ... ... 7 //定义一个改变对象属性的方法 8 public static void changeName(Person p) { 9 p.name = "Rose"; 10 } 11 ... ... 12 public static void main(String[] args) { 13 //定义一个Person对象,person是这个对象的引用 14 Person person = new Person(); 15 //先显示这个对象的name属性 16 System.out.println(person.name); 17 //调用changeName(Person p)方法 18 changeName(person); 19 //再显示这个对象的name属性,看是否发生了变化 20 System.out.println(person.name); 21 } 22
第一次显示:“Jack”
第二次显示:“Rose”
Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。
主函数中new 了一个对象Person,实际分配了两个对象:新创建的Person类的实体对象,和指向该对象的引用变量person。
【注意:在java中,新创建的实体对象在堆内存中开辟空间,而引用变量在栈内存中开辟空间】
正如如上图所示,左侧是堆空间,用来分配内存给新创建的实体对象,红色框是新建的Person类的实体对象,000012是该实体对象的起始地址;而右侧是栈空间,用来给引用变量和一些临时变量分配内存,新实体对象的引用person就在其中,可以看到它的存储单元的内容是000012,记录的正是新建Person类实体对象的起始地址,也就是说它指向该实体对象。
调用了changeName()方法,person作为对象参数传入该方法,但是大家特别注意,它传入的是什么!!!person引用变量将自己的存储单元的内容传给了changeName()方法的p变量!也就是将实体对象的地址传给了p变量,从此,在changeName()方法中对p的一切操作都是针对p所指向的这个存储单元,与person引用变量所指向的那个存储单元再没有关系了!
p所指向的那个存储单元中的内容是实体对象的地址,使得p也指向了该实体对象,所以才能改变对象内部的属性!
6.对象的构造
重载:方法签名包括(方法名,参数类型,但是不包括返回类型),java允许重载任何方法,而不只是构造器方法;
默认域参数: 如果在构造器中没有显示的给域赋予初值,会自动赋予默认值(数值为0,布尔值为false,对象引用为null);
无参数的构造器: 若没有编写构造器,系统提供默认的无参数构造器,将所有实例域设置为默认值;
显示域初始化:
参数名: 参数变量用同样的名字将实例域屏蔽起来,此时可以采用this访问实例域
1 public Employee(String name , double salary) 2 { 3 this.name = name; 4 this.salary =salary; 5 }
调用另一个构造器:如果构造器的第一个语句形如this(...),这个构造器将调用另一个类的另一个构造器;
1 public Employee (double s) 2 { 3 //call Employee(String,double) 4 this("Employee#" + naxtId, s); 5 nextId++; 6 }
初始化数据域的三种方法:
1). 在构造器中设置之;
2). 在声明中赋值;
3). 初始化块(程序先运行初始化块,再运行构造器的主体部分)
调用构造器的步骤:
1). 所以数据域被初始化为默认值(0,false,null);
2). 按照在类声明中出现的顺序,依次执行所有域初始化语句和初始化块;
3). 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体;
4). 执行这个构造器的主体;
1 import java.util.*; 2 3 /** 4 * 重载构造器 5 * this(...)调用另一个构造器 6 * 无参数构造器 7 *对象初始化块 8 *静态初始化块 9 *实例域初始化块 10 */ 11 public class ConstructorTest 12 { 13 public static void main(String[] args) 14 { 15 // fill the staff array with three Employee objects 16 Employee[] staff = new Employee[3]; 17 18 staff[0] = new Employee("Harry", 40000); 19 staff[1] = new Employee(60000); 20 staff[2] = new Employee(); 21 22 // print out information about all Employee objects 23 for (Employee e : staff) 24 System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary=" 25 + e.getSalary()); 26 } 27 } 28 29 class Employee 30 { 31 32 private static int nextId; 33 34 private int id; 35 private String name = ""; // instance field initialization 36 private double salary; 37 38 // static initialization block 39 static 40 { 41 Random generator = new Random(); 42 // set nextId to a random number between 0 and 9999 43 nextId = generator.nextInt(10000); 44 } 45 46 // object initialization block 47 { 48 id = nextId; 49 nextId++; 50 } 51 52 // three overloaded constructors 53 public Employee(String n, double s) 54 { 55 name = n; 56 salary = s; 57 } 58 59 public Employee(double s) 60 { 61 // calls the Employee(String, double) constructor 62 this("Employee #" + nextId, s); 63 } 64 65 // the default constructor 66 public Employee() 67 { 68 // name initialized to ""--see below 69 // salary not explicitly set--initialized to 0 70 // id initialized in initialization block 71 } 72 73 public String getName() 74 { 75 return name; 76 } 77 78 public double getSalary() 79 { 80 return salary; 81 } 82 83 public int getId() 84 { 85 return id; 86 } 87 88 }
7. 包
包的作用
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
Java使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。
包语句的语法格式为:
package pkg1[.pkg2[.pkg3…]];
例如,一个Something.java 文件它的内容
package net.java.util public class Something{ ... }
那么它的路径应该是 net/java/util/Something.java 这样保存的。 package(包)的作用是把不同的java程序分类保存,更方便的被其他java程序调用。
一个包(package)可以定义为一组相互联系的类型(类、接口、枚举和注释),为这些类型提供访问保护和命名空间管理的功能。
以下是一些Java中的包:
- java.lang-打包基础的类
- java.io-包含输入输出功能的函数
开发者可以自己把一组类和接口等打包,并定义自己的package。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注释等是相关的。
由于package创建了新的命名空间(namespace),所以不会跟其他package中的任何名字产生命名冲突。使用包这种机制,更容易实现访问控制,并且让定位相关类更加简单。
创建包
创建package的时候,你需要为这个package取一个合适的名字。之后,如果其他的一个源文件包含了这个包提供的类、接口、枚举或者注释类型的时候,都必须将这个package的声明放在这个源文件的开头。包声明应该在源文件的第一行,每个源文件只能有一个包声明,这个文件中的每个类型都应用于它。如果一个源文件中没有使用包声明,那么其中的类,函数,枚举,注释等将被放在一个无名的包(unnamed package)中。
通常使用小写的字母来命名避免与类、接口名字的冲突。
在animals包中加入一个接口(interface):
/* 文件名: Animal.java */ package animals; interface Animal { public void eat(); public void travel(); }
接下来,在同一个包中加入该接口的实现:
package animals; /* 文件名 : MammalInt.java */ public class MammalInt implements Animal{ public void eat(){ System.out.println("Mammal eats"); } public void travel(){ System.out.println("Mammal travels"); } public int noOfLegs(){ return 0; } public static void main(String args[]){ MammalInt m = new MammalInt(); m.eat(); m.travel(); } }
然后,编译这两个文件,并把他们放在一个叫做animals的子目录中。 用下面的命令来运行:
$ mkdir animals $ cp Animal.class MammalInt.class animals $ java animals/MammalInt Mammal eats Mammal travel
import关键字
为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用"import"语句可完成此功能。在 java 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为:
import package1[.package2…].(classname|*);
如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。
下面的payroll包已经包含了Employee类,接下来向payroll包中添加一个Boss类。Boss类引用Employee类的时候可以不用使用payroll前缀,Boss类的实例如下。
package payroll; public class Boss { public void payEmployee(Employee e) { e.mailCheck(); } }
如果Boss类不在payroll包中又会怎样?Boss类必须使用下面几种方法之一来引用其他包中的类
1).使用类全名描述,例如:
payroll.Employee
2).用import关键字引入,使用通配符"*"
import payroll.*;
3).使用import关键字引入Employee类
import payroll.Employee;
注意:
类文件中可以包含任意数量的import声明。import声明必须在包声明之后,类声明之前。
package的目录结构
类放在包中会有两种主要的结果:
- 包名成为类名的一部分,正如我们前面讨论的一样。
- 包名必须与相应的字节码所在的目录结构相吻合。
下面是管理你自己java中文件的一种简单方式:
将类、接口等类型的源码放在一个文本中,这个文件的名字就是这个类型的名字,并以.java作为扩展名。例如:
// 文件名 : Car.java package vehicle; public class Car { // 类实现 }
接下来,把源文件放在一个目录中,这个目录要对应类所在包的名字。
....\vehicle\Car.java
现在,正确的类名和路径将会是如下样子:
-
类名 -> vehicle.Car
-
路径名 -> vehicle\Car.java (in windows)
通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是apple.com,所有的包名都以com.apple开头。包名中的每一个部分对应一个子目录。
例如:这个公司有一个com.apple.computers的包,这个包包含一个叫做Dell.java的源文件,那么相应的,应该有如下面的一连串子目录:
....\com\apple\computers\Dell.java
编译的时候,编译器为包中定义的每个类、接口等类型各创建一个不同的输出文件,输出文件的名字就是这个类型的名字,并加上.class作为扩展后缀。 例如:
// 文件名: Dell.java package com.apple.computers; public class Dell{ } class Ups{ }
现在,我们用-d选项来编译这个文件,如下:
$javac -d . Dell.java
这样会像下面这样放置编译了的文件:
.\com\apple\computers\Dell.class.\com\apple\computers\Ups.class
你可以像下面这样来导入所有 \com\apple\computers\中定义的类、接口等:
import com.apple.computers.*;
编译之后的.class文件应该和.java源文件一样,它们放置的目录应该跟包的名字对应起来。但是,并不要求.class文件的路径跟相应的.java的路径一样。你可以分开来安排源码和类的目录。
<path-one>\sources\com\apple\computers\Dell.java
<path-two>\classes\com\apple\computers\Dell.class
这样,你可以将你的类目录分享给其他的编程人员,而不用透露自己的源码。用这种方法管理源码和类文件可以让编译器和java虚拟机(JVM)可以找到你程序中使用的所有类型。
类目录的绝对路径叫做class path。设置在系统变量CLASSPATH中。编译器和java虚拟机通过将package名字加到class path后来构造.class文件的路径。
<path- two>\classes是class path,package名字是com.apple.computers,而编译器和JVM会在 <path-two>\classes\com\apple\compters中找.class文件。
一个class path可能会包含好几个路径。多路径应该用分隔符分开。默认情况下,编译器和JVM查找当前目录。JAR文件按包含Java平台相关的类,所以他们的目录默认放在了class path中。
设置CLASSPATH系统变量
用下面的命令显示当前的CLASSPATH变量:
- Windows平台(DOS 命令行下)-> C:\> set CLASSPATH
- UNIX平台(Bourne shell下)-> % echo $CLASSPATH
删除当前CLASSPATH变量内容:
- Windows平台(DOS 命令行下)-> C:\> set CLASSPATH=
- UNIX平台(Bourne shell下)-> % unset CLASSPATH; export CLASSPATH
设置CLASSPATH变量:
-
- Windows平台(DOS 命令行下)-> set CLASSPATH=C:\users\jack\java\classes
- UNIX平台(Bourne shell下)-> % CLASSPATH=/home/jack/java/classes; export CLASSPATH
8. 文档注释
java只是三种注释方式。前两种分别是// 和/* */,第三种被称作说明注释,它以/** 开始,以 */结束;
说明注释允许你在程序中嵌入关于程序的信息。你可以使用javadoc工具软件来生成信息,并输出到HTML文件中;
javadoc 标签
javadoc工具软件识别以下标签:
标签 | 描述 | 示例 |
---|---|---|
@author | 标识一个类的作者 | @author description |
@deprecated | 指名一个过期的类或成员 | @deprecated description |
{@docRoot} | 指明当前文档根目录的路径 | Directory Path |
@exception | 标志一个类抛出的异常 | @exception exception-name explanation |
{@inheritDoc} | 从直接父类继承的注释 | Inherits a comment from the immediate surperclass. |
{@link} | 插入一个到另一个主题的链接 | {@link name text} |
{@linkplain} | 插入一个到另一个主题的链接,但是该链接显示纯文本字体 | Inserts an in-line link to another topic. |
@param | 说明一个方法的参数 | @param parameter-name explanation |
@return | 说明返回值类型 | @return explanation |
@see | 指定一个到另一个主题的链接 | @see anchor |
@serial | 说明一个序列化属性 | @serial description |
@serialData | 说明通过writeObject( ) 和 writeExternal( )方法写的数据 | @serialData description |
@serialField | 说明一个ObjectStreamField组件 | @serialField name type description |
@since | 标记当引入一个特定的变化时 | @since release |
@throws | 和 @exception标签一样. | The @throws tag has the same meaning as the @exception tag. |
{@value} | 显示常量的值,该常量必须是static属性。 | Displays the value of a constant, which must be a static field. |
@version | 指定类的版本 | @version info |
文档注释
在开始的/**之后,第一行或几行是关于类、变量和方法的主要描述。之后,你可以包含一个或多个何种各样的@标签。每一个@标签必须在一个新行的开始或者在一行的开始紧跟星号(*).多个相同类型的标签应该放成一组。例如,如果你有三个@see标签,可以将它们一个接一个的放在一起。
下面是一个类的说明注释的示例:
/*** This class draws a bar chart. * @author Zara Ali * @version 1.2 */
javadoc输出什么
javadoc工具将你Java程序的源代码作为输入,输出一些包含你程序注释的HTML文件;
每一个类的信息将在独自的HTML文件里。javadoc也可以输出继承的树形结构和索引;
由于javadoc的实现不同,工作也可能不同,你需要检查你的Java开发系统的版本等细节,选择合适的Javadoc版本;
下面是一个使用说明注释的简单实例。注意每一个注释都在它描述的项目的前面。
在经过javadoc处理之后,SquareNum类的注释将在SquareNum.html中找到。
import java.io.*; /** * This class demonstrates documentation comments. * @author Ayan Amhed * @version 1.2 */ public class SquareNum { /** * This method returns the square of num. * This is a multiline description. You can use * as many lines as you like. * @param num The value to be squared. * @return num squared. */ public double square(double num) { return num * num; } /** * This method inputs a number from the user. * @return The value input as a double. * @exception IOException On input error. * @see IOException */ public double getNumber() throws IOException { InputStreamReader isr = new InputStreamReader(System.in); BufferedReader inData = new BufferedReader(isr); String str; str = inData.readLine(); return (new Double(str)).doubleValue(); } /** * This method demonstrates square(). * @param args Unused. * @return Nothing. * @exception IOException On input error. * @see IOException */ public static void main(String args[]) throws IOException { SquareNum ob = new SquareNum(); double val; System.out.println("Enter value to be squared: "); val = ob.getNumber(); val = ob.square(val); System.out.println("Squared value is " + val); } }
如下,使用javadoc工具处理SquareNum.java文件:
$ javadoc SquareNum.java Loading source file SquareNum.java... Constructing Javadoc information... Standard Doclet version 1.5.0_13 Building tree for all the packages and classes... Generating SquareNum.html... SquareNum.java:39: warning - @return tag cannot be used\ in method with void return type. Generating package-frame.html... Generating package-summary.html... Generating package-tree.html... Generating constant-values.html... Building index for all the packages and classes... Generating overview-tree.html... Generating index-all.html... Generating deprecated-list.html... Building index for all classes... Generating allclasses-frame.html... Generating allclasses-noframe.html... Generating index.html... Generating help-doc.html... Generating stylesheet.css... 1 warning $
9.类的设计技巧
保证数据私有;
要对数据初始化;
不要在类中使用过多的基本类型;
不是所有的域都需要独立的域访问器和域更改器;
将职责过多的类进行分解;
类名和方法名要能够体现他们的职责;