JAVA学习笔记(3)对象与类

面向对象程序设计概述

Java 是完全面向对象的 , 必须熟悉 OOP 才能够编写 Java 程序 。面向对象的程序是由对象组成的 , 每个对象包含对用户公开的特定功能部分和隐藏的实现部分 。 程序中的很多对象来自标准库, 还有一些是自定义的 。 但是 , 从根本上说, 只要对象能够满足要求, 就不必关心其功能的具体实现过程 。 在 OOP 中 , 不必关心对象的具体实现 , 只要能够满足用户的需求即可 。

在 Wirth 命名的书名中 , 算法是第一位的 , 数据结构是第二位的,而 OOP 却调换了这个次序, 将数据放在第一位,然后考虑操作数据的算法 。

类 ( class ) 是构造对象的模板或蓝图。由类构造 (construct) 对象的过程称为创建类的实例 (instance) 。

正如前面所看到的 , 用 Java 编写的所有代码都位于某个类的内部 : 标准的 Java 库提供了几千个类, 可以用于用户界面设计、 日期、 日历和网络程序设计 。 尽管如此 , 还是需要在Java 程序中创建一些自己的类 , 以便描述应用程序所对应的问题域中的对象 。

封装 ( encapsulation , 有时称为数据隐藏 ) 是与对象有关的一个重要概念 。 从形式上看 ,封装不过是将数据和行为组合在一个包中 , 并对对象的使用者隐藏了数据的实现方式 。对象的数据称为实例域 (instance field ), 操纵数据的过程称为方法 (method)。对于每个特定的类实例 ( 对象 ) 都有一组特定的实例域值 。这些值的集合就是这个对象的当前状态 ( state ) 。无论何时 , 只要向对象发送一个消息 , 它的状态就有可能发生改变 。

OOP 的另一个原则会让用户自定义 Java 类变得轻而易举, 这就是 : 可以通过扩展一个类来建立另外一个新的类 。 事实上 , 在 Java 中 , 所有的类都源自于一个 “ 神通广大的超类 ” ,它就是 Object 。

在扩展一个已有的类时 , 这个扩展后的新类具有所扩展的类的全部属性和方法 。

对象

同一个类的所有对象实例, 由于支持相同的行为而具有家族式的相似性 。

此外 , 每个对象都保存着描述当前特征的信息 。 这就是对象的状态 。 对象的状态可能会随着时间而发生改变, 但这种改变不会是自发的 。( 如果不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏 )。

需要注意作为一个类的实例 , 每个对象的标识永远是不同的, 状态常常也存在着差异 。

识别类

传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序 。 在面向对象程序设计时没有所谓的 “ 顶部 ” 。答案是 : 首先从设计类开始, 然后再往每个类中添加方法 。

例如 , 在订单处理系统中 , 有这样一些名词 :
• 商品 ( Item )
• 订单 ( Order)
• 送货地址(Shippingaddress)
• 付款 ( Payment )
• 账户(Account)

例如, 当一个新的商品添加到订单中时, 那个订单对象就是被指定的对象 , 因为它知道如何存储商品以及如何对商品进行排序 。 也就是说, add 应该是 Order 类的一个方法,而 Item 对象是一个参数 。

类之间的关系

在类之间, 最常见的关系有
依赖 ( “ uses - a ” )
聚合 ( “ has - a ” )
继承 (“ is - a ” )

依赖 ( dependence ) , 即 “ uses - a ” 关系, 是一种最明显的 、 最常见的关系。 例如 , Order类使用 Account 类是因为 Order 对象需要访问 Account 对象查看信用状态 。但是 Item 类不依赖于 Account 类, 这是因为 Item 对象与客户账户无关 。 因此 , 如果一个类的方法操纵另一个类的对象, 我们就说一个类依赖于另一个类 。

聚合 ( aggregation ) , 即 “ has - a ” 关系 , 是一种具体且易于理解的关系 。 例如, 一个Order 对象包含一些 Item 对象 。 聚合关系意味着类 A 的对象包含类 B 的对象 。

继承 ( inheritance ) , 即 “ is - a ” 关系 , 是一种用于表示特殊与一般关系的 。 例如 , RushOrder类由Order类继承而来 。 在具有特殊性的 RushOrder 类中包含了一些用于优先处理的特殊方法 , 以及一个计算运费的不同方法 ; 而其他的方法, 如添加商品 生成账单等都是从Order 类继承来的 。一般而言, 如果类 A 扩展类 B , 类 A 不但包含从类 B 继承的方法 , 还会拥有一些额外的功能。

使用预定义的类

在 Java 中, 没有类就无法做任何事情,然而, 并不是所有的类都具有面向对象特征 。 例如 , Math 类 。在程序中 , 可以使用 Math 类的方法, 如 Math.random ,并只需要知道方法名和参数,而不必了解它的具体实现过程 。但遗憾的是, Math 类只封装了功能, 它不需要也不必隐藏数据 。 由于没有数据, 因此也不必担心生成对象以及初始化实例域。

对象与对象变量

**要想使用对象 , 就必须首先构造对象, 并指定其初始状态 。**在 Java 程序设计语言中 , 使 用 构 造 器 ( constructor ) 构造新实例 。 构造器是一种特殊的方法 , 用来构造并初始化对象 。例如,

String s = new Date().toString () ;
System.out.println(new Date());

在这两个例子中 , 构造的对象仅使用了一次 。 通常 , 希望构造的对象可以多次使用, 因此, 需要将对象存放在一个变量中 :

Date birthday = new Date() ;

图 4 - 3 显示了引用新构造的对象变量 birthday 。

在这里插入图片描述
在对象与对象变量之间存在着一个重要的区别 。 例如, 语句

Date deadline ;

定义了一个对象变量 deadline , 它 可 以 引 用 Date 类型的对象 。 但是, 一定要认识到 : 变量deadline 不是一个对象 , 实际上也没有引用对象 。 此时, 不能将任何 Date 方法应用于这个变量上 。

语句

s = deadline.toString();

将产生编译错误 。

必须首先初始化变量 deadline , 这里有两个选择 。 当然 , 可以用新构造的对象初始化这个变量 :

deadline = new Date () ;

也让这个变量引用一个已存在的对象 :

deadline = birthday;

现在 , 这两个变量引用同一个对象。
在这里插入图片描述
一定要认识到 : 一个对象变量并没有实际包含一个对象, 而仅仅引用一个对象 。(本质是一个的指针,但是Java摒弃了指针的概念,改成了引用,本质上是指针指向对象)

在 Java 中, 任何对象变量的值都是对存储在另外一个地方的一个对象的引用 。 new 操作符的返回值也是一个引用 。 下列语句 :

Date deadline = new Date () ;

有两个部分 。 表达式 new Date () 构造了一个 Date 类型的对象 , 并且它的值是对新创建对象的引用 。 这个引用存储在变量 deadline 中 。

可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象 。

deadline = null ;
if ( deadline ! = null )
. .
System.out.println (deadline) ;

如果将一个方法应用于一个值为 null 的对象上, 那么就会产生运行时错误 。

birthday = null ;
String s = birthday toStringQ ; // runtime error !

局部变量(方法内的变量)不会自动地初始化为 null , 而必须通过调用 new 或将它们设置为 null 进行初始化。

很多人错误地认为 Java 对象变量与 C ++ 的引用类似 。 然而 , 在 C ++ 中没有空引用, 并且引用不能被赋值 。 可以将 Java 的对象变量看作 C ++ 的对象指针 。 例如 ,

Date birthday; // Java

实际上 , 等同于

Date * birthday; // C++

用户自定义类

要想创建一个完整的程序, 应该将若干类组合在一起, 其中只有一个类有 main 方法 。

Employee类

import java.time.LocalDate;

public class Employee {
	private String name ;
	private double salary ;
	private LocalDate hireDay;
	public Employee(String n , double s , int year , int month , int day ){
		name = n ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
	}
	
	public String getName(){
		return name ;
	}
	
	public double getSalary() {
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	
	public void raiseSalary (double byPercent)
	{
		double raise = salary * byPercent / 100;
		salary += raise;
	}
	
	@Override
	public String toString() {
		String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
		return s;
	}
	
}
public class EmployTest {
	
		public static final double CM_PER_INCH = 2.54 ;
		
		public static void main(String[] args)throws Exception {
			Employee[] staff = new Employee[]{
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8)	
			};
			
			for(Employee ee : staff) {
				System.out.println(ee);
			}
		}

}

接下来, 当编译这段源代码的时候(不要加package,否则会按照当前目录下寻找包,会出错) , 编译器将在目录下创建两个类文件 : EmployeeTest .class 和Employee.class

得到结果:

name = liutao , salary = 30000.0 , hireDay = 2020-10-08
name = liutao , salary = 30000.0 , hireDay = 2020-10-08
name = liutao , salary = 30000.0 , hireDay = 2020-10-08

多源文件使用

一个源文件包含了两个类 。例如 , 将 Employee 类存放在文件 Employee . java 中, 将 EmployeeTest 类存放在文件 EmployeeTest. java 中 。

如果使用这样组织文件, 将可以有两种编译源程序的方法 。 一种是使用通配符调用 Java编译器 :

javac Employee * . java

于是, 所有与通配符匹配的源文件都将被编译成类文件 。 或者键人下列命令 :

javac EmployeeTest . java

不要加上包声明,否则很麻烦,自己建立的java文件使用javac会自动在当前目录下寻找,加上package就会在运行时在当前目录下寻找package。

当 Java 编译器发现 EmployeeTestjava 使用丫 Employee 类时会查找名为 Employee . class 的文件 。 如果没有找到这个文件 , 就会自动地搜索 Employeejava , 然后, 对它进行编译 。 更重要的是 : 如果 Employee ,java 版本较已有的 Empl 0 yee . dass 文件版本新 , Java 编译器就会自动地重新编译这个文件 。

剖析Employee类

这个类包含一个构造器和 4 个方法。

这个类的所有方法都被标记为 public 。 关键字 public 意味着任何类的任何方法都可以调用这些方法。

关键字 private 确保只有 Employee 类自身的方法能够访问这些实例域 , 而其他类的方法不能够读写这些域 。

有两个实例域本身就是对象 : name 域是 String 类对象 , hireDay 域是LocalDate 类对象 。 这种情形十分常见 : 类通常包括类型属于某个类类型的实例域 。本质是对象中存的是引用变量。

构造器

可以看到, 构造器与类同名 。 在构造 Employee 类的对象时, 构造器会运行 , 以便将实例域初始化为所希望的状态 。

public Employee(String n , double s , int year , int month , int day ){
		name = n ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
	}

例如 , 当使用下面这条代码创建 Employee 类实例时 :

new Eraployee ( " ] ames Bond " , 100000 , 1950 , 1 , 1)

将会把实例域设置为 :

name = " James Bond " ;
salary = 100000;
hireDay = LocalDate . of (1950 , 1 , 1 ) ; 	// January 1 , 1950

构造器与其他的方法有一个重要的不同 。 构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的 。

例如,

janes . EmployeeCJames Bond " , 250000 , 1950 , 1 , 1) // ERROR

将产生编译错误 。

构造器的相关属性:

• 构造器与类同名
• 每个类可以有一个以上的构造器
• 构造器可以有 0 个 、 1 个或多个参数
• 构造器没有返回值
• 构造器总是伴随着 new 操作一起调用

隐式参数与显式参数

方法用于操作对象以及存取它们的实例域 。 例如 , 方法 :

public void raiseSalary (double byPercent )
{
double raise = salary * byPercent / 100 ;
salary += raise ;
}

将调用这个方法的对象的 salary 实例域设置为新值 。 看看下面这个调用:

number007 . raiseSalary ( 5) ;

它的结果将 number007.salary 域的值增加 5% 。 具体地说, 这个调用将执行下列指令 :

double raise = nuaber007.salary * 5 / 100 ;
nuiber007.salary + = raise;

raiseSalary 方法有两个参数 。

**第一个参数称为隐式 ( implicit ) 参数 , 是出现在方法名前的Employee 类对象 。隐式参数就是不明显参与方法计算,但是又影响方法的计算结果。**有些人把隐式参数称为方法调用的目标或接收者

在每一个方法中 , 关键字 this 表示隐式参数 。this就是引用调用函数的对象的引用变量。

第二个参数位于方法名后面括号中的数值 , 这是一个显式 ( explicit ) 参数

也就是说this和引用调用方法对象的引用变量是相同的。

可以用下列方式编写raiseSalary 方法 :

public void raiseSalary ( double byPercent )
{
double raise = this . sal ary * byPercent / 100;
this . sal ary + = raise ;
}

因为这样可以将实例域与局部变量明显地区分开来 。

封装的优点

仔细地看一下非常简单的 getName 方法 、 getSalary 方法和 getHireDay 方法 。这些都是典型的访问器方法 。 由于它们只返回实例域值 , 因此又称为域访问器 。

关键在于 name 是一个只读域 。 一旦在构造器中设置完毕 , 就没有任何一个办法可以对它进行修改 , 这样来确保 name 域不会受到外界的破坏 。

虽然 salary 不是只读域 , 但是它只能用 raiseSalary 方法修改 。 特别是一旦这个域值出现了错误, 只要调试这个方法就可以了 。 如果 salary 域是 public 的, 破坏这个域值的捣乱者有可能会出没在任何地方 。

在有些时候, 需要获得或设置实例域的值 。 因此 , 应该提供下面三项内容 :

一 私有的数据域 ;
一 公有的域访问器方法 ;
一个公有的域更改器方法 。

首先, 可以改变内部实现, 除了该类的方法之外, 不会影响其他代码 。

如, 如果将存储名字的域改为 :

String firstName ;
String lastName ;

那么 getName 方法可以改为返回

firstName + " " + lastName

对于这点改变 , 程序的其他部分完全不可见 。

final 实例域

可以将实例域定义为 final 。 构建对象时必须初始化这样的域 。 也就是说, 必须确保在每一个构造器执行之后 , 这个域的值被设置, 并且在后面的操作中 , 不能够再对它进行修改 。

例如, 可以将 Employee 类中的 name 域声明为 final , 因为在对象构建之后, 这个值不会再被修改, 即没有 setName 方法 。

class Employee
{
	private final String name ;
	...
}

final 修饰符大都应用于基本 ( primitive ) 类型域 , 或不可变 ( immutable ) 类的域 ( 如果类中的每个方法都不会改变其对象 , 这种类就是不可变的类 。 例如 , String 类就是一个不可变的类 ) 。

对于可变的类, 使用 final 修饰符可能会对读者造成混乱 。 例如 ,

private final StringBuiIcier evaluations ;

在 Employee 构造器中会初始化为

evaluations = new StringBuilder () ;

final 关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其他 StringBuilder对象 。 不过这个对象可以更改 :

public void giveGoldStarO
{
evaluations.append ( LocalDate now () + " : Gold star ! \ n" ) ;
}

静态域和静态方法

静态域

如果将域定义为 static , 每个类中只有一个这样的域 。 而每一个对象对于所有的实例域却都有自己的一份拷贝 。 例如 , 假定需要给每一个雇员賦予唯一的标识码 。 这里给 Employee类添加一个实例域 id 和一个静态域 nextld :

class Employee
{
private static int nextld = 1;
private int id;

}

现在, 每一个雇员对象都有一个自己的 id 域 , 但这个类的所有实例将共享一个 nextld域 。 换句话说, 如果有 1000 个 Employee 类的对象, 则有 1000 个实例域 id 。 但是 , 只有一个静态域 nextld 。 即使没有一个雇员对象 , 静态域 nextld 也存在 。 它属于类, 而不属于任何独立的对象 。

在绝大多数的面向对象程序设计语言中, 静态域被称为类域 。术语 “static ” 只是沿用了 C ++ 的叫法 , 并无实际意义 。

下面实现一个简单的方法 :

public void setId()
{
	id = nextId ;
	nextId++ ;
}

假定为 harry 设定雇员标识码 :

harry.setld() ;

harry 的 id 域被设置为静态域 nextld 当前的值 , 并且静态域 nextld 的值加 1 。

静态常量

静态变量使用得比较少, 但静态常量却使用得比较多 。 例如, 在 Math 类中定义了一个静态常量 :

public class Math
{
	...
	public static final double PI = 3.14159265358979323846 ;
	...
}

在程序中, 可以采用 Math .PI 的形式获得这个常量 。
如果关键字 static 被省略 , PI 就变成了 Math 类的一个实例域 。 需要通过 Math 类的对象访问 PI , 并且每一个 Math 对象都有它自己的一份 PI 拷贝 。

另一个多次使用的静态常量是 System . out 。 它在 System 类中声明 :

public cl ass System
{
...
public static final PrintStream out =...;
...
}

前面曾经提到过 , 由于每个类对象都可以对公有域进行修改 , 所以, 最好不要将域设计为 public 。 然而 , 公有常量 ( 即 final 域 ) 却没问题 。 因为 out 被声明为 final , 所以 , 不允许再将其他打印流赋值给它 。

如果查看一下 System 类 , 就会发现有一个 setOut 方法 , 它可以将 System . out 设置为不同的流 。

静态方法

静态方法是一种不能向?象?施操作的方法 。 例如, Math类的 pow 方法就是一个方法 。 表达式

Math . pow (x , a )

计算X^a,在运算时, 不使用任何 Math 对象。 也就是没有隐式参数,没有this参数,因为调用该方法的是类而非对象。

Employee 的静态方法不能访问Id的实例域, 因为它不能操作对象 。 但是 ,静态方法可以访问自身类中的静态域 。 下面是使用这种方法的一个示例:

public static int getNextld()
{
	return nextld ; // returns static field
}

可以通过类名调用这个方法 :

int n = Employee.getNextld() ;

这个方法可以省略static,但是,需要通过Employee类对象的引用调用这个方法,毫无意义。

main方法

需要注意, 不需要使用对象调用静态方法 。 例如 , 不需要构造 Math 类对象就可以调用Math.pow 。

同理 , main 方法也是一个静态方法 。

public class Application
{
	public static void main ( StringD args )
	{
		//
		construct objects here
		...
	}
}

main 方法不对任何对象进行操作 。 事实上 , 在启动程序时还没有任何一个对象 。 静态的main 方法将执行并创建程序所需要的对象 。

每一个类可以有一个 main 方法 。 这是一个常用于对类进行单元测试的技巧 。 例如,可以在Employee 类中添加一个 main 方法

import java.time.LocalDate;

public class Employee {
	private static int nextId = 1;
	private int id;
	private String name ;
	private double salary ;
	private LocalDate hireDay;
	
	public Employee(String n , double s , int year , int month , int day ){
		name = n ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
		id = 0;
	}
	
	public void setld()
	{
		this.id = Employee.nextId ;
		Employee.nextId++;
	}
	
	public String getName(){
		return name ;
	}
	
	public double getSalary() {
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	
	public void raiseSalary(double byPercent)
	{
		double raise = salary * byPercent / 100;
		this.salary += raise;
		System.out.println(salary);
	}
	
	@Override
	public String toString() {
		String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
		return s;
	}
	
	public static int getNextId() {
		return Employee.nextId;
	}
	
	public static void main(String[] args) {
		Employee e = new Employee("liutao", 30000.0, 2020, 10, 8);
		System.out.println(e.getName() + " " + e.getSalary() );
	}
	
}
import java.time.LocalDate;

public class EmployTest {
	
		public static final double CM_PER_INCH = 2.54 ;
		
		public static void main(String[] args)throws Exception {
			Employee[] staff = new Employee[]{
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8)	
			};
			
			for(Employee ee : staff) {
				System.out.println(ee);
			}
			
			System.out.println(LocalDate.of(2008, 8, 18));
			
		}

}

以上两个类都有主类,通过IDE和命令行都可以分别运行。

方法参数

按值调用 ( call by value ) 表示方法接收的是调用者提供的值 。 而按引用调用 ( call by reference )表示方法接收的是调用者提供的变量地址 。一个方法可以修改传递引用所对应的变量值 , 而不能修改传递值调用所对应的变量值 。

Java 程序设计语言总是采用按值调用 。 也就是说 , 方法得到的是所有参数值的一个拷贝, 特别是, 方法不能修改传递给它的任何参数变量的内容 。

例如 , 考虑下面的调用 :

double percent = 10;
harry.raiseSalary(percent) :

不必理睬这个方法的具体实现 , 在方法调用之后 , percent 的值还是 10 。

假定一个方法试图将一个参数值增加至 3 倍 :

public static void tripieValue ( double x) // doesn ' t work
{
	x = 3 * x;
}

然后调用这个方法 :

double percent = 10;
tripieValue(percent) ;

不过, 并没有做到这一点调用这个方法之后 , percent 的值还是 10 。

具体的执行过程:
1 ) x 被初始化为 percent 值的一个拷贝 ( 也就是percent = 10 )
2 ) x 被乘以 3 后等于 30 。 但是 percent 仍然是 10
3 ) 这个方法结束之后, 参数变量 X 不再使用

在这里插入图片描述

然而, 方法参数共有两种类型:基本数据类型(数字,布尔值),引用数据类型。

一个方法不可能修改一个基本数据类型的参数 。而对象引用作为参数就不同了 , 可以很容易地利用下面这个方法实现将一个雇员的薪金提高两倍的操作 :

public static void tripleSalary(Employee x)
{
	x.raiseSa1ary(200) ;
}

当调用

harry = new Employee (...);
tripleSalary(harry);

具体的执行过程为 :
1 ) x 被初始化为 harry 值的拷贝, 这里是一个对象的引用 。
2 ) raiseSalary 方法应用于这个对象引用 。 x 和 harry 同时引用的那个 Employee 对象的薪金提高了 200% 。
3 ) 方法结束后 , 参数变量 x 不再使用 。 当然, 对象变量 harry 继续引用那个薪金增至 3倍的雇员对象 。

在这里插入图片描述

实现一个改变对象参数状态的方法并不是一件难事 。 理由很简单, 方法得到的是对象引用的拷贝, 对象引用及其他的拷贝同时引用同一个对象 。

有些程序员认为 Java 程序设计语言对对象采用的是引用调用,实际上, 这种理解是不对的 。 由于这种误解具有一定的普遍性 , 所以下面给出一个反例来详细地阐述一下这个问题 。

首先, 编写一个交换两个雇员对象的方法 :

public static void swap ( Employee x , Employee y ) // doesn ' t work
{
	Employee temp = x;
	x = y;
	y = temp ;
}

如果 Java 对对象采用的是按引用调用 , 那么这个方法就应该能够实现交换数据的效果 :

Employee a = new Employee("Alice", ...);
Employee b = new Employee("Bob",... );
swap ( a , b ) ;

但是 , 方法并没有改变存储在变量 a 和 b 中的对象引用 。 swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝, 这个方法交换的是这两个拷贝 。

Employee temp = x;
x = y ;
y = temp ;
// now x refers to Bob , y to Alice

最终 , 白费力气 。 在方法结束时参数变量 X 和 y 被丢弃了 。 原来的变量 a 和 b 仍然引用这个方法调用之前所引用的对象。这个过程说明 : Java 程序设计语言对对象采用的不是引用调用, 实际上 , 对象引用是按值传递的 。Java中的方法参数都是传递值无论式基本数据类型还是引用数据类型,都是的拷贝,只能通过引用来修改对象状态,而无法改变引用指向的对象和基本数据类型。
在这里插入图片描述

对象构造

由于对象构造非常重要, 所以 Java 提供了多种编写构造器的机制 。

重载

有些类有多个构造器 。 例如 , 可以如下构造一个空的 StringBuilder 对象 :

StringBuilder messages = new StringBuilder();

或者 , 可以指定一个初始字符串 :

StringBuilder sd = new StringBuilder("://");

这种特征叫做重载(overloading)。如果多个方法 ( 比如 , StringBuilder 构造器方法)有相同的名字,不同的参数, 便产生了重载 。编译器必须挑选出具体执行哪个方法 , 它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法 。如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好 。

Java 允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名 ( signature ) 。 例如 , String 类有 4 个称为 indexOf 的公有方法 。它们的签名是
indexOf ( int )
indexOf ( int , int )
indexOf (String)
indexOf ( String , int )

返回类型不是方法签名的一部分 。 也就是说 , 不能有两个名字相同 、 参数类型也相同却返回不同类型值的方法 。

默认域初始化

**如果在构造器中没有显式地给域赋予初值 , 那么就会被自动地赋为默认值 : 数值为 0 、布尔值为 false 、 对象引用为 null 。**只有缺少程序设计经验的人才会这样做。

假定没有在构造器中对某些域进行初始化 , 就会默认地将 salary 域初始化为 0 , 将 name 和 hireDay 域初始化为 null 。

但是 , 这并不是一种良好的编程习惯 。 如果此时调用 getName 方法或 getHireDay 方法 ,则会得到一个 null 引用。

LocalDate h = harry getHi reDay() ;
int year = h.getYear() ;	 // throws exception if h is null

无参数的构造器

很多类都包含一个无参数的构造函数, 对象由无参数构造函数创建时 , 其状态会设置为适当的默认值 。 例如 , 以下是 Employee 类的无参数构造函数 :

public Employee()
{
	name = "";
	salary = 0;
	hireDay = LocalDate .now () ;
}

如果在编写一个类时没有编写构造器 , 那么系统就会提供一个无参数构造器 。 这个构造器将所有的实例域设置为默认值 。 于是, 实例域中的数值型数据设置为 0 、 布尔型数据设置为 false 、 所有对象变量将设置为 null 。

如果类中提供了至少一个构造器 , 但是没有提供无参数的构造器 , 则在构造对象时如果没有提供参数就会被视为不合法

例如 , 在程序清单 4-2 中的 Employee 类提供了一个简单的构造器 :

Employee ( String name , double salary , int y , int m , int d)

对于这个类 , 构造默认的雇员属于不合法 。也就是 , 调用

e = new Eraployee();

将会产生错误 。

仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器如果在编写类的时候, 给出了一个构造器, 哪怕是很简单的, 要想让这个类的用户能够采用下列方式构造实例 :

new ClassName();

就必须提供一个默认的构造器 ( 即不带参数的构造器 )。 当然, 如果希望所有域被赋予默认值 , 可以采用下列格式 :

public ClassName()
{
}

显式初始化

通过重载类的构造器方法, 可以采用多种形式设置类的实例域的初始状态 。 确保不管怎样调用构造器, 每个实例域都可以被设置为一个有意义的初值 , 这是一种很好的设计习惯。

可以在类定义中, 直接将一个值赋给任何域 。例如 :

class Employee
{
private String name = “”;

}

在执行构造器之前 , 先执行赋值操作 。 当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时 , 这种方式特别有用 。

在 C++ 中, 不能直接初始化类的实例域 。 所有的域必须在构造器中设置 。

构造函数参数名设置

参数变量用同样的名字将实例域屏蔽起来 。 例如 , 如果将参数命名为 salary , salary 将引用这个参数 , 而不是实例域 。但是 , 可以采用 this ,salary 的形式访问实例域 。 this 指示隐式参数, 也就是所构造的对象 。 下面是一个示例 :

public Employee (String name , double salary )
{
	this.name = name;
	this.salary = salary;
}

调用另一个构造器

关键字 this 引用方法的隐式参数 。 然而, 这个关键字还有另外一个含义 。

如果构造器的第一个语句形如 this (. . .) , 这个构造器将调用同一个类的另一个构造器 。 下面是一个典型的例子 :

public Employee (double s)
{
/ / calls Employee (String , double )
this (" Employee #" + nextld , s ) ;
nextld+ + ;
}

当调用 new Employee( 60000) 时, Employee( double ) 构造器将调用 Employee(String , double)构造器。

采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可 。

初始化块

两种初始化数据域的方法 :

  1. 在构造器中设置值
  2. 在声明中赋值

Java 还有第三种机制 , 称为初始化块,在一个类的声明中 ,可以包含多个代码块 。 只要构造类的对象 , 这些块就会被执行 。

例如 ,

public class Employee {
	public static int nextId = 1;
	private int id;
	private String name ;
	private double salary ;
	private LocalDate hireDay;
	
	{
		System.out.println("代码块");
		this.id = Employee.nextId;
		Employee.nextId ++ ;
	}
	
	public Employee() {	
	}
	
	public Employee(String name , double s , int year , int month , int day ){
		System.out.println("有参构造函数");
		this.name = name ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
	}
	...
}

在这个示例中, 无论使用哪个构造器构造对象, id 域都在对象初始化块中被初始化 。 首先运行初始化块, 然后才运行构造器的主体部分 。每一次使用构造器,都会运行初始化块。

这种机制不是必需的 , 也不常见 。 通常会直接将初始化代码放在构造器中 。

下面是调用构造器的具体处理步骤 :

1 ) 所有数据域被初始化为默认值( 0 、false 或 null )。
2 ) 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块 。
3 ) 如果构造器第一行调用了第二个构造器, 则执行第二个构造器主体。
4 ) 执行这个构造器的主体 。

可以通过提供一个初始化值 , 或者使用一个静态的初始化块来对静态域进行初始化 。

private static int nextld = 1 ;

如果对类的静态域进行初始化的代码比较复杂, 那么可以使用静态的初始化块 。

将代码放在一个块中, 并标记关键字 static 。 下面是一个示例 。 其功能是将雇员 ID 的起始值赋予一个小于 10000 的随机整数 。

static {
		System.out.println("静态代码块");
		Random generator = new Random();
		nextId = generator.nextInt(10000) ;
	}

在类第一次加载的时候, 将会进行静态域的初始化 。 与实例域一样, 除非将它们显式地设置成其他值, 否则默认的初始值是 0 、 false 或 null 。 所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行 。只能使用静态变量和自定义的变量,不能够直接使用实例变量。

import java.time.LocalDate;
import java.util.Random;

public class Employee {
	public static int nextId = 1;
	private int id;
	private String name ;
	private double salary ;
	private LocalDate hireDay;
	
	static {
		System.out.println("静态代码块");
		Random generator = new Random();
		nextId = generator.nextInt(10000) ;
	}
	
	{
		System.out.println("代码块");
		this.id = Employee.nextId;
		Employee.nextId ++ ;
	}
	
	public Employee() {	
	}
	
	public Employee(String name , double s , int year , int month , int day ){
		System.out.println("有参构造函数");
		this.name = name ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
	}
	
	public void setld()
	{
		this.id = Employee.nextId ;
		Employee.nextId++;
	}
	
	public String getName(){
		return name ;
	}
	
	public double getSalary() {
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	
	public void raiseSalary(double byPercent)
	{
		double raise = salary * byPercent / 100;
		this.salary += raise;
		System.out.println(salary);
	}
	
	@Override
	public String toString() {
		String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
		return s;
	}
	
	public static int getNextId() {
		return Employee.nextId;
	}
	
	public int getId() {
		return this.id;
	}
}

对象析构与 finalize 方法

有些面向对象的程序设计语言, 特别是 C ++ , 有显式的析构器方法 , 其中放置一些当对象不再使用时需要执行的清理代码 。 在析构器中 , 最常见的操作是回收分配给对象的存储空间 。 由于 Java 有自动的垃圾回收器 , 不需要人工回收内存, 所以 Java 不支持析构器 。

如果某个资源需要在使用完毕后立刻被关闭, 那么就需要由人工来管理 。 对象用完时,可以应用一个 close 方法来完成相应的清理操作 。

Java 允许使用包 ( package )将类组织起来 。 借助于包可以方便地组织自己的代码, 并将自己的代码与别人提供的代码库分开管理 。

标准的 Java 类库分布在多个包中 , 包括 java . lang 、 java . util 和 java . net 等 。 标准的 Java包具有一个层次结构 。 如同硬盘的目录嵌套一样, 也可以使用嵌套层次组织包 。 所有标准的Java 包都处于 java 和 javax 包层次中 。

使用包的主要原因是确保类名的唯一性 。 假如两个程序员不约而同地建立了Employee类 。 只要将这些类放置在不同的包中, 就不会产生冲突 。 事实上 , 为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名 (这显然是独一无二的) 以逆序的形式作为包名, 并且对于不同的项目使用不同的子包 。例如, horstmann.com 是本书作者之一注册的域名 。逆序形式为com.horstmann 。 这个包还可以被进一步地划分成子包 , 如 com.horstmann.corejava 。

从编译器的角度来看 , 嵌套的包之间没有任何关系 。 例如, java. util 包与 java.util. jar 包毫无关系 。 每一个都拥有独立的类集合 。

类的导入

第一种方式是在每个类名之前添加完整的包名 。

例如 :

java.time.LocalDate today = java.time.LocalDate.now() ;

这显然很繁琐 。 更简单且更常用的方式是使用 import 语句 。 import 语句是一种引用包含在包中的类的简明描述 。 一旦使用了 import 语句, 在使用类时, 就不必写出包的全名了 。

import java . util . * ;

然后 , 就可以使用

LocalDate today = LocalDate.now() ;

而无须在前面加上包前缀 。 还可以导人一个包中的特定类 :

import java.time.LocalDate ;

java .time.* 的语法比较简单 , 对代码的大小也没有任何负面影响 。 当然 , 如果能够明确地指出所导人的类 , 将会使代码的读者更加准确地知道加载了哪些类 。

但是, 需要注意的是, 只能使用星号 ( * ) 导入一个包, 而不能使用 import java . 或import java . * . 导入以 java 为前缀的所有包 。**

在大多数情况下 , 只导入所需的包 , 并不必过多地理睬它们 。 但在发生命名冲突的时候, 就不能不注意包的名字了 。 例如 , java .util 和 java . sql 包都有日期 ( Date ) 类。 如果在程序中导入了这两个包 :

import java.util.* ;
import java.sql.* ;

在程序使用 Date 类的时候, 就会出现一个编译错误 :

Date today; 		/ / Error java.util.Date or java.sql.Date?

此时编译器无法确定程序使用的是哪一个 Date 类 。可以采用增加一个特定的import 语句来解决这个问题 :

import java.util.* ;
import java.sql.* ;
import java.util.Date ;

如果这两个 Date 类都需要使用 , 又该怎么办呢 ? 答案是 , 在每个类名的前面加上完整的包名。

java.util.Date deadline = new java.util.Date () ;
java.sql.Date today = new java.sql.Date( ... ) ;

在包中定位类是编译器 ( compiler ) 的工作 。 类文件中的字节码肯定使用完整的包名来引用其他类 。

静态导入

import 语句不仅可以导人类, 还增加了导人静态方法和静态域的功能 。

例如, 如果在源文件的顶部, 添加一条指令 :

import static java.lang.System.*;

就可以使用 System 类的静态方法和静态域, 而不必加类名前缀 :

out.println("Goodbye,World!"); 
exit (0) ; 

另外, 还可以导入特定的方法或域 :

import static java.lang.System.out ;

将类放入包中

要想将一个类放人包中 , 就必须将包的名字放在源文件的开头, 包中定义类的代码之前 。 例如,

package com.horstmann.corejava;
public class Employee
{
	...;
}

如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包( defaulf package ) 中 。默认包是一个没有名字的包 。 没有指明所在包,定义的所有类都在默认包中 。

将包中的文件放到与完整的包名匹配的子目录中 。 例如, com.horstmann.corejava 包中的所有源文件应该被放置在子目录 com/horstmann/corejava,编译器将类文件也放在相同的目录结构中 。

案例:

在这里插入图片描述

package javabase;

import java.time.LocalDate;
import java.util.Arrays;
import static java.lang.System.*;
public class EmployTest {

		public static final double CM_PER_INCH = 2.54 ;
		public static void main(String[] args)throws Exception {
			Employee[] staff = new Employee[]{
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8),
					new Employee("liutao", 30000.0, 2020, 10, 8)	
			};
			for(Employee ee : staff) {
				System.out.println(ee.getId());
			}
			out.println(Employee.nextId);
		}
}
package javabase;

import java.time.LocalDate;
import java.util.Random;

public class Employee {
	public static int nextId = 1;
	private int id;
	private String name ;
	private double salary ;
	private LocalDate hireDay;
	
	static {
		System.out.println("静态代码块");
		Random generator = new Random();
		nextId = generator.nextInt(10000) ;
	}
	
	{
		System.out.println("代码块");
		this.id = Employee.nextId;
		Employee.nextId ++ ;
	}
	
	public Employee() {	
	}
	
	public Employee(String name , double s , int year , int month , int day ){
		System.out.println("有参构造函数");
		this.name = name ;
		salary = s ;
		hireDay = LocalDate.of( year , month , day ) ;
	}
	
	public void setld()
	{
		this.id = Employee.nextId ;
		Employee.nextId++;
	}
	
	public String getName(){
		return name ;
	}
	
	public double getSalary() {
		return salary;
	}
	
	public LocalDate getHireDay()
	{
		return hireDay;
	}
	
	public void raiseSalary(double byPercent)
	{
		double raise = salary * byPercent / 100;
		this.salary += raise;
		System.out.println(salary);
	}
	
	@Override
	public String toString() {
		String s = "name = " + this.getName ()+ " , salary = " + this.getSalary()+ " , hireDay = " + this.getHireDay();
		return s;
	}
	
	public static int getNextId() {
		return Employee.nextId;
	}
	
	public int getId() {
		return this.id;
	}
}

在基目录(显示包目录的这一层目录)执行命令:

javac javabase/EmployTest.java		// 运行主类的代码,可以自动编译依赖的类
java javabase.EmployTest	// 会根据包声明,来在当前路径寻找包,如果没有在正确位置输入,当前位置找不到包,就会出现寻找主类错误

编译器在编译源文件的时候不检查目录结构 。 例如 , 假定有一个源文件开头有下列语句 :

package con.myconpany ;

即使这个源文件没有在子目录 com/ mycompany 下 , 也可以进行编译 。 如果它不依赖于其他包 , 就不会出现编译错误 。 但是 , 最终的程序将无法运行 , 除非先将所有类文件移到正确的位置上 。 如果包与目录不匹配, 虚拟机就找不到类。

包作用域

标记为 public 的部分可以被任意的类使用 ; 标记为 private 的部分只能被定义它们的类使用 。 如果没有指定 public 或 private , 这 部分 ( 类 、方法或变量 )可以被同一个包中的所有方法访问 。

假设没有将 Employee 类定义为公有类 ,因此只有在同一个包 ( 在此是默认包) 中的其他类可以访问, 例如 EmployeeTest 就可以访问。

对于类来说, 这种默认是合乎情理的。但是 , 对于变量来说就有些不适宜了 , 因此变量必须显式地标记为 private , 不然的话将默认为包可见 。显然, 这样做会破坏封装性 。 问题主要出于人们经常忘记键人关键字 private 。

在 java.awt 包中的 Window 类就是一个典型的示例 。 java.awt 包是 JDK 提供的部分源代码 :

public class Window extends Container
{
	String warningString ;
}

这里的 wamingString 变量不是 private ! 这 意 味 着 java.awt 包中的所有类的方法都可以访问该变量, 并将它设置为任意值 ( 例如 , “ Trust me ! ” )。 实际上 , 只有 Window类的方法访问它, 因此应该将它设置为私有变量 。程序员匆忙之中忘记键人private 修饰符了。

从 1.2 版开始, JDK 的实现者修改了类加载器 , 明确地禁止加载用户自定义的 、 包名以 “ java . ” 开始的类! 当然, 用户自定义的类无法从这种保护中受益 。 然而 , 可以通过包密封 ( package sealing ) 机制来解决将各种包混杂在一起的问题 。 如果将一个包密封起来 , 就不能再向这个包添加类了(jar 包) 。

类路径(classpath)

类存储在文件系统的子目录中 。 类的路径必须与包名匹配 。

另外, 类文件也可以存储在 JAR ( Java 归档 )文件中 。 在一个 JAR 文件中 , 可以包含多个压缩形式的类文件和子目录 , 这样既可以节省又可以改善性能 。 在程序中用到第三方( third - party ) 的库文件时, 通常会给出一个或多个需要包含的 JAR 文件 。 JDK 也提供了许多的 JAR 文件 , 例如 , 在 jre / lib / rt . jar 中包含数千个类库文件 。

JAR 文件使用 ZIP 格式组织文件和子目录。 可以使用所有 ZIP 实用程序查看内部的 rt . jar 以及其他的 JAR 文件 。

为了使类能够被多个程序共享, 需要做到下面几点 :

1 ) 把类放到一个目录中, 例如 / home / user / classdir 。 需要注意, 这个目录是包树状结构的基目录 。 如果希望将 com . horstmann . corejava . Employee 类添加到其中, 这个 Employee . class类文件就必须位于子目录 / home / user / classdir / com / horstmann/corejava 中 。

2 ) 将 JAR 文件放在一个目录中 , 例如 : / home / user / archives 。

3 ) 设置类路径 ( classpath ) 。 类路径是所有包含类文件的路径的集合 。

在 UNIX 环境中, 类路径中的不同项目之间采用冒号 (一) 分隔,其中.是当前目录 :

/home/user/classdir: . : / home /user/archives/archive.jar

由于运行时库文件 ( rt . jar 和在 jre / lib 与 jre / lib/ ext 目录下的一些其他的 JAR 文件 ) 会被自动地搜索, 所以不必将它们显式地列在类路径中 。

javac 编译器总是在当前的目录中查找文件, 但 Java 虚拟机仅在类路径中有“.“目录的时候才查看当前目录如果没有设置类路径, 那也并不会产生什么问题, 默认的类路径包含”."目录。然而如果设置了类路径却忘记了包含目录, 则程序仍然可以通过编译, 但不能运行 。

类路径所列出的目录和归档文件是搜寻类的起始点 。 下面看一个类路径示例 :

/home/user/classdir:.: /home/user/archives/archive.jar

假定虚拟机要搜寻com.horstmann.corejava.Employee 类文件 。 它首先要查看存储在 jre/lib 和 jre/lib/ext 目录下的归档文件中所存放的系统类文件。显然 , 在那里找不到相应的类文件 , 然后再查看类路径 。然后查找以下文件 :

/home/user/classdir/com/horstmann/ corejava /Employee.class
com/horstmann/corejava/ Employee. class 	// 从当前路径开始
com/horstmann/corejava/Employee.class inside /home/user/archives/archive.jar

编译器定位文件要比虚拟机复杂得多 。 如果引用了一个类, 而没有指出这个类所在的包 , 那么编译器将首先查找包含这个类的包, 并询查所有的 import 指令, 确定其中是否包含了被引用的类 。 例如, 假定源文件包含指令 :

import java . util * ;
import com . horstmann . corejava . * ;

并且源代码引用了 Employee 类 。 编译器将试图查找 java.lang . Employee ( 因为 java.lang 包被默认导入 )、java.util.Employee 、com.horstmann.corejava.Employee 和当前包中的 Employee 。

对这个类路径的所有位置中所列出的每一个类进行逐一查看 。
如果找到了一个以上的类, 就会产生编译错误 (因为类必须是唯一的, 而 import 语句的次序却无关紧要 )。

编译器的任务不止这些 , 它还要查看源文件 ( Source files ) 是否比类文件新 。 如果是这样的话 , 那么源文件就会被自动地重新编译 。仅可以导人其他包中的公有类 。 一个源文件只能包含一个公有类, 并且文件名必须与公有类匹配 。 因此 , 编译器很容易定位公有类所在的源文件 。 当然, 也可以从当前包中导入非公有类 。 这些类有可能定义在与类名不同的源文件中 。如果从当前包中导人一个类 , 编译器就要搜索当前包中的所有源文件 , 以便确定哪个源文件定义了这个类 。

设置类路径

最好采用 - classpath ( 或 - cp ) 选项指定类路径 :

java - classpath /home/user/dassdir :.: / home/user/archives/archive.jar Mydog

整个指令应该书写在一行中 。 将这样一个长的命令行放在一个 shell 脚本或一个批处理文件中是一个不错的主意 。

利用 dasspath 选项设置类路径是首选的方法 , 也可以通过设置 CLASSPATH 环境变量完成这个操作 。 其详细情况依赖于所使用的 shell 。在 Bourne Again shell ( bash ) 中 , 命令格式如下 :

export CLASSPATH = / home / user / classdir : . : / home / user / archives / archive . jar

直到退出 shell 为止, 类路径设置均有效 。

文档注释

JDK 包含一个很有用的工具 , 叫做 javadoc , 它可以由源文件生成一个 HTML 文档 。

如果在源代码中添加以专用的定界符 /** 开始的注释 , 那么可以很容易地生成一个看上去具有专业水准的文档 。 这是一种很好的方式, 因为这种方式可以将代码与注释保存在一个地方 。 如果将文档存人一个独立的文件中 , 就有可能会随着时间的推移 , 出现代码和注释不一致的问题 。 然而 , 由于文档注释与源代码在同一个文件中, 在修改源代码的同时 , 重新运行 javadoc 就可以轻而易举地保持两者的一致性 。

注释的插入

javadoc 实用程序 ( utility ) 从下面几个特性中抽取信息 :
• 包
• 公有类与接口
• 公有的和受保护的构造器及方法
• 公有的和受保护的域

应该为上面几部分编写注释 、 注释应该放置在所描述特性的前面 。 注释以 /** 开始, 并以 * / 结束 。

每个 /** . . . */ 文档注释在标记之后紧跟着自由格式文本 ( free - form text ) 。 标记由 @ 开始, 如 @ author 或 @ param 。

自由格式文本的第一句应该是一个概要性的句子 。 javadoc 实用程序自动地将这些句子抽取出来形成概要页 。

在自由格式文本中, 可以使用 HTML 修饰符 , 例如 , 用于强调的 < em > … < / eitf > 、 用于着重强调的 < strong > … < / stroiig > 以及包含图像的 < img … • > 等 。 不过, 一定不要使用 < hl > 或< hr > , 因为它们会与文档的格式产生冲突 。 若要键入等宽代码, 需使用 { @ code … } 而不是< code > . … </ code>

类注释

类注释必须放在 import 语句之后, 类定义之前 。

下面是一个类注释的例子 :

/**
* A {©code Card} object represents a playing card , such
* as "Queen of Hearts" . A card has a suit ( Diamond , Heart ,
* Spade or Club) and a value (1 = Ace , 2 . . . 10 , 11 = Jack ,
* 12 = Queen , 13 = King )
* /
public class Card
{
...
}

方法注释

每一个方法注释必须放在所描述的方法之前 。 除了通用标记之外, 还可以使用下面的标记 :

• @ param 变量描述
这个标记将对当前方法的 “ param ” ( 参数 ) 部分添加一个条目 。 这个描述可以占据多行 , 并可以使用 HTML 标记 。 一个方法的所有 @ param 标记必须放在一起 。
• @ return 描述
这个标记将对当前方法添加 “ return ” (返回 ) 部分 。 这个描述可以跨越多行 , 并可以使用 HTML 标记 。
• ©throws 类描述
这个标记将添加一个注释 , 用于表示这个方法有可能抛出异常 。

下面是一个方法注释的示例 :

/**
* Raises the salary of an employee .
* @ param byPercent the percentage by which to raise the salary ( e.g. 10 means 10%)
* @ return the amount of the raise
* /
* 
public double raiseSal ary ( double byPercent)
{
double raise = salary * byPercent / 100 ;
salary += raise ;
}

域注释

只需要对公有域 ( 通常指的是静态常量 ) 建立文档 。 例如 ,

/ * *
* The "Hearts " card suits
* /
public static final int HEARTS = 1 ;

通用注释

下面的标记可以用在类文档的注释中 。

• @ author 姓名
这个标记将产生一个"author " ( 作者) 条目 。 可以使用多个 @author 标记 , 每个 @author 标记对应一个作者。

• @version 文本
这个标记将产生一个 “ version ” ( 版本 ) 条目 。 这里的文本可以是对当前版本的任何描述 。

下面的标记可以用于所有的文档注释中 。

• @ since文本
这个标记将产生一个 “ since ” ( 始于 ) 条目 。这里的 text 可以是对引人特性的版本描述 。 例如, © since version 1.7 10

• @ deprecated
这个标记将对类 、 方法或变量添加一个不再使用的注释 。 文本中给出了取代的建议。
例如,
@ deprecated Use < code > setVIsible (true)< /code > instead

通过 @ see 和 @ link 标记, 可以使用超级链接 , 链接到 javadoc 文档的相关部分或外部文档 。

• ® see 引用
这个标记将在 “ see also ” 部分增加一个超级链接 。 它可以用于类中 , 也可以用于方法中 。

只要提供类 、 方法或变量的名字 , javadoc 就在文档中插入一个超链接 。 例如 ,

@ see com . horstraann . corejava . Employee # raiseSalary (double)

建立一个链接到 com.horstmann.corejava.Employee 类的 raiseSalary ( double) 方法的超链接 。 可以省略包名, 甚至把包名和类名都省去, 此时 , 链接将定位于当前包或当前类。

需要注意, 一定要使用井号 ( #),而不要使用句号 ( . ) 分隔类名与方法名, 或类名与变量名 。Java 编译器本身可以熟练地断定句点在分隔包 、 子包 、 类 、 内部类与方法和变量时的不同含义 。 但是 javadoc 实用程序就没有这么聪明了 , 因此必须对它提供帮助 。

如果 @ see 标记后面有一个 < 字符 , 就需要指定一个超链接 。 可以超链接到任何URL 。 例如 :

@ see < a href = " www . horstmann . com / corejava . htinl "> The Core ] ava home page < /a>

在上述各种情况下 , 都可以指定一个可选的标签 ( label ) 作为链接锚 ( link anchor ) o如果省略了 label , 用户看到的锚的名称就是目标代码名或 URL 。

包与概述注释

可以直接将类 、 方法和变量的注释放置在 Java 源文件中 , 只要用 /** . . . * / 文档注释界定就可以了 。 但是 , 要想产生包注释 , 就需要在每一个包目录中添加一个单独的文件 。 可以有如下两个选择 :

1 ) 提供一个以 package . html 命名的 HTML 文件 。 在标记 < body > < / body > 之间的所有文本都会被抽取出来 。

2 ) 提供一个以 package - info . java 命名的 Java 文件 。 这个文件必须包含一个初始的以 /**和 * / 界定的 Javadoc 注释 , 跟随在一个包语句之后 。 它不应该包含更多的代码或注释 。

还可以为所有的源文件提供一个概述性的注释 。 这个注释将被放置在一个名为 overview .html 的文件中 , 这个文件位于包含所有源文件的父目录中 。 标记 < body > . . . < / body > 之间的所有文本将被抽取出来 。 当用户从导航栏中选择 “ Overview ” 时, 就会显示出这些注释内容 。

注释的抽取

这里, 假设 HTML 文件将被存放在目录 docDirectory 下 。 执行以下步骤 :
1 ) 切换到包含想要生成文档的源文件目录 。 如果有嵌套的包要生成文档, 例如com.horstmann.corejava , 就必须切换到包含子目录 com 的目录(如果存在 overview.html 文件的话, 这也是它的所在目录 ) 。

2 ) 如果是一个包 , 应该运行命令 :

javadoc - d docDirectory nameOfPackage

或对于多个包生成文档, 运行 :

javadoc - d docDirectory nameOfPackage1 nameOfPackage . . .

如果文件在默认包中 , 就应该运行 :

javadoc -d docDirectory*.java

类设计技巧

1 . 一定要保证数据私有

这是最重要的 ; 绝对不要破坏封装性 。 有时候 , 需要编写一个访问器方法或更改器方法 ,但是最好还是保持实例域的私有性 。 很多惨痛的经验告诉我们 , 数据的表示形式很可能会改变 , 但它们的使用方式却不会经常发生变化 。 当数据保持私有时 , 它们的表示形式的变化不会对类的使用者产生影响 , 即使出现 bug 也易于检测 。

2 . 一定要对数据初始化

Java 不对局部变量进行初始化 , 但是会对对象的实例域进行初始化 。 最好不要依赖于系统的默认值 , 而是应该显式地初始化所有的数据 , 具体的初始化方式可以是提供默认值, 也可以是在所有构造器中设置默认值 。

3 . 不要在类中使用过多的基本类型
就是说, 用其他的类代替多个相关的基本类型的使用 。 这样会使类更加易于理解且易于修改 。 例如, 用一个称为 Address 的新的类替换一个 Customer 类中以下的实例域 :

private String street ;
private String city;
private String state;
private int zip;

这样 , 可以很容易处理地址的变化, 例如, 需要增加对国际地址的处理 。

4 . 不是所有的域都需要独立的域访问器和域更改器
或许, 需要获得或设置雇员的薪金 。 而一旦构造了雇员对象, 就应该禁止更改雇用日期 , 并且在对象中 , 常常包含一些不希望别人获得或设置的实例域 , 例如, 在 Address 类中 ,存放州缩写的数组 。

5 . 将职责过多的类进行分解
这样说似乎有点含糊不清, 究竟多少算是 “ 过多 ” ? 每个人的看法不同 。 但是, 如果明显地可以将一个复杂的类分解成两个更为简单的类, 就应该将其分解 ( 但另一方面, 也不要走极端 。 设计 10 个类 , 每个类只有一个方法 , 显然有些矫枉过正了)。

下面是一个反面的设计示例 。

public class CardDeck // bad design
{
private int 口 value ;
private int[] suit ;

public CardDeck () { . . }
public void shuffle()  { • • • }
public  int getTopValue()  { . . . }
public int getTopSuit()  { . . . }
public  void draw() {. }
}

实际上 , 这个类实现了两个独立的概念 : 一副牌 (含有 shuffle 方法和 draw 方法) 和一张牌 ( 含有查看面值和花色的方法 ) 。 另外 , 引入一个表示单张牌的 Card 类 。 现在有两个类 ,每个类完成自己的职责 :

public cl ass CardDeck
{
private Card[] cards ;
public CardDeck() {  . . }
public void shuffle () { . . . }
public Card getTop(){ . . . }
public void draw() { . . }
}

public class Card
{
private int value ;
private int suit ;
public Card ( int aValue , int aSuit ) {
public int getValue() { . . }
}
public int getSuit() {
}
}

6 . 类名和方法名要能够体现它们的职责

与变量应该有一个能够反映其含义的名字一样, 类也应该如此 ( 在标准类库中 , 也存在着一些含义不明确的例子 , 如 : Date 类实际上是一个用于描述时间的类 ) 。

  1. 优先使用不可变的类

LocalDate 类以及 java . time 包中的其他类是不可变的–没有方法能修改对象的状态 。类似 plusDays 的方法并不是更改对象 , 而是返回状态已修改的新对象 。
更改对象的问题在于 , 如果多个线程试图同时更新一个对象, 就会发生并发更改 。 其结果是不可预料的 。 如果类是不可变的, 就可以安全地在多个线程间共享其对象 。
因此 , 要尽可能让类是不可变的 , 这是一个很好的想法 。 对于表示值的类 , 如一个字符串或一个时间点 , 这尤其容易 。 计算会生成新值, 而不是更新原来的值 。
当然, 并不是所有类都应当是不可变的 。 如果员工加薪时让 raiseSalary 方法返回一个新的 Employee 对象 , 这会很奇怪 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值