在类之间,最常见的关系有:
- 依赖(“uses-a”):一个类的方法操纵另一个类的对象
- 聚合(“has-a”):一个类的对象包含另一个类的对象
- 继承(“is-a”):特殊对象与一般对象
构造器:一种特殊的方法,用来构造并初始化对象。(类似C++的构造函数?)
构造器的名字应与类名相同。
使用new构造一个对象:
new Date();
可以将这个对象传递给一个方法:
System.out.println(new Date());
可以将一个方法应用到刚刚创建的对象上:
String s = new Date().toString();
当希望多次使用构造出的对象时,可将其保存在一个变量中:
Date birthday = new Date();
对象和对象变量
Date deadline; // deadline doesn't refer to any object
该语句定义了一个对象变量deadline,它可以引用Date类型对象。但它不是一个对象,因此不能将Date方法应用于这个变量:
s = deadline.toString(); // 错误!
一个对象变量没有实际包含一个对象,仅仅引用了一个对象:
deadline = birthday; // 两个变量引用同一个对象
Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。
可显式将对象变量设置为null,表示目前没有引用任何对象:
deadline = null;
...
if (deadline != null)
System.out.println(deadline);
如果将一个方法应用于一个值为null的对象上,将返回一个运行时错误:
birthday = null;
String s = birthday.toString(); // runtime error!
GregorianCalendar类
Date类的实例有一个状态,即特定的时间点。
GregorianCalendar类用来表示大家熟悉的日历表示法。
GregorianCalendar类扩展了一个更加通用的Calendar类,它描述了日历的一般属性。
Date类只提供了少量的方法用来比较两个时间点,如before和after:
if (today.before(birthday))
System.out.println("Still time to shop for a gift.");
GregorianCalendar类包含的方法比Date类要多得多,特别是有几个很有用的构造器。
new GreogrianCalendar()
构造一个新对象,用于表示对象构造时的日期和时间。
还可以通过提供年、月、日构造一个表示某个特定日期午夜的日历对象:
new GregorianClendar(1999, 11, 31) // 月份从0计数,11表示十二月
new GregorianCalendar(1999, Calendar.DECEMBER, 31)
还可以设置时间:
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)
查询GregorianCalendar类的信息时通常需要使用Calendar类中定义的一些常量:
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);
调用set方法,可以改变对象的状态:
deadline.set(Calendar.YEAR, 2001);
deadline.set(Calendar.MONTH, Calendar.APRIL);
deadline.set(Calendar.DAY_OF_MONTH, 15);
可同时设置年、月、日:
deadline.set(2001, Calendar.APRIL, 5);
可以为给定的日期对象增加天数、星期数、月份等:
deadline.add(Calendar.MONTH, 3); // move deadline by 3 months
GregorianCalendar类有getTime方法和setTime方法,用来获得和设置日历的时间点:
Date time = calendar.getTime();
calendar.setTime(time);
这些方法使用在GregorianCalendar类和Date类之间的转换:
GregorianCalendar calendar = new GreogrianCalendar(year, month, day);
Date hireDay = calendar.getTime();
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);
示例:打印当前月的日历
import java.text.DateFormatSymbols;
import java.util.*;
public class CalendarTest
{
public static void main(String[] args)
{
// 设置地区(不同地区有着不同的日期表示习惯)
Locale.setDefault(Locale.US);
GregorianCalendar d = new GregorianCalendar();
int today = d.get(Calendar.DAY_OF_MONTH);
int month = d.get(Calendar.MONTH);
d.set(Calendar.DAY_OF_MONTH, 1); // 设置d为当月第一天
int weekday = d.get(Calendar.DAY_OF_WEEK);
int firstDayOfWeek = d.getFirstDayOfWeek(); // 当前地区星期的起始日
int indent = 0;
while (weekday != firstDayOfWeek)
{
indent++;
d.add(Calendar.DAY_OF_MONTH, -1);
weekday = d.get(Calendar.DAY_OF_WEEK);
}
// 获取表示星期几个英文缩写
String[] weekdayNames = new DateFormatSymbols().getShortWeekdays();
do
{
System.out.printf("%4s", weekdayNames[weekday]);
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
} while (weekday != firstDayOfWeek);
System.out.println();
for (int i = 1; i <= indent; i++)
System.out.print(" ");
d.set(Calendar.DAY_OF_MONTH, 1);
do
{
int day = d.get(Calendar.DAY_OF_MONTH);
System.out.printf("%3d", day);
if (day == today) System.out.print("*");
else System.out.print(" ");
d.add(Calendar.DAY_OF_MONTH, 1);
weekday = d.get(Calendar.DAY_OF_WEEK);
if (weekday == firstDayOfWeek) System.out.println();
} while (d.get(Calendar.MONTH) == month);
if (weekday != firstDayOfWeek) System.out.println();
}
}
用户自定义类
最简单的类定义形式:
class ClassName
{
constructor1
constructor2
...
method1
method2
...
field1
field2
...
}
下面是一个非常简单的Employee类:
class Employee
{
// constructor
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();
}
// a method
public String getName()
{
return name;
}
// more methods
...
// instance fields
private String name;
private double salary;
private Date hireDay;
}
多个源文件的使用
一个源文件可包含多个类,也可将每个类存放在单独的原文件中,如Employee类存放在Employee.java,EmployeeTest类存放在EmployeeTest.java。
这样组织文件,有两种编译方法:
1. 使用通配符:
javac Employee*.java
2. 只编译主源文件:
javac EmployeeTest.java
当编译器发现使用了Employee类时会查找Employee.class文件,若未找到,就会查找Employee.java,并对其进行编译。
与C++不同,Java中所有的方法都必须的类内定义。
Final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域,并且在后面的操作中,不能够再对它进行修改。如:
class Employee
{
...
private final String name;
}
final修饰符大都应用于基本数据类型域,或不可变类的域。
对于可变类,使用final修饰符可能会对读者造成混乱,如:
private final Date hiredate;
仅仅意味着存储在hiredate变量中的对象引用在对象构造之后不能被改变,而并不意味着hiredate对象是一个常量。任何方法都可以对hiredate引用的对象调用setTime更改器。
静态域和静态方法
如果将域定义为static,每个类中只有一个这样的域。而每个对象对于所有的实例域却都有自己的一份拷贝。
假定需要给每一个雇员赋予一个惟一的标识码,添加一个实例域id和一个静态域nextId:
class Employee
{
...
private int id;
private static int nextId = 1; // 属于类,不属于任何对象
}
现在,每一个对象都有一个自己的id域,但这个类的所有实例将共享一个nextId域。
静态常量
public class Math
{
...
public static final double PI = 3.14159265358979323846;
...
}
在程序中,可以使用math.PI获得这个常量。
如果关键字static被省略,PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。
静态方法
静态方法是一种不能向对象实施操作的方法。如Math类的pow方法:
Math.pow(x, a)
计算x的a次方。运算时,不使用任何Math对象。
在下面两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供。
- 一个方法只需要访问类的静态域。
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); // prints $0.10
System.out.println(percentFormatter.format(x)); // prints 10%
NumberFormat类不利用构造器完成这些操作的原因:
- 无法命名构造器。
- 当使用构造器时,无法改变所构造的对象类型。
- 基本数据类型(数字型和布尔型)
- 对象引用
public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
}
public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
}
总结:
- 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能实现让对象参数引用一个新的对象。
对象构造
重载:多个方法可以有相同的名字,不同的参数(返回类型不起作用)。
默认构造器:没有参数的构造器
public Employee()
{
name = "";
salary = 0;
hireDay = new Date();
}
一个类没有任何构造器,则系统会提供一个默认构造器,将所有实例域设置为默认值。
一个类若有至少一个构造器,但没有默认构造器,则系统不提供默认构造器,若构造对象时不提供构造参数则被视为不合法。
可以在类定义域中,直接将一个值赋给任何域。在执行构造器之前,先执行赋值操作。初始值不一定是常量,也可以调用方法:
class Employee
{
...
static int assignId()
{
int r = nextId;
nextId++;
return r;
}
...
private int id = assignId();
}
C++中,不能直接初始化实例域。
Java中,木有初始化列表。
参数名
三种风格:
// 参数用单个字母,编写小构造器时常用
public Employee(String n, double s)
{
name = n;
salary = s;
}
// 参数前加前缀“a”,这样更易阅读
public Employee(String aName, double aSalary)
{
name = aName;
salary = aSalary;
}
// 参数屏蔽实例域,使用this访问
public Employee(String name, double aSalary)
{
this.name = name;
this.salary = salary;
}
调用另一个构造器
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String, double)构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只编写一次即可。
初始化块
在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行:
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;
...
// object initialization block
{
id = nextId;
nextId++;
}
}
该示例中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。(这种机制不常用)
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
静态的初始化块
如果对类的静态域进行初始化的代码比较复杂,那么可使用静态的初始化块:
// static initialization block
static
{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
在类第一次加载时,会进行静态域的初始化。所有静态初始化语句以及静态初始化块都将依照类定义的顺序执行。
对象析构与finalize方法
Java有自动的垃圾回收器,不需要人工回收内存。
某些对象使用了内存之外的其他资源,当资源不再需要时,需要则需将其回收。
可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。
包(package)
Java允许使用包将类组织起来。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
使用包的主要原因是确保类名的惟一性。
类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类。
可以采用两种方式访问另一个包中的公有类。
1. 每个类名前添加完整的包名:
java.util.Date today = new java.util.Date();
2. 使用import语句导入一个特定的类或者整个包:
import java.util.*;
然后就可使用:
Date today = new Date();
C++中,与包机制类似的是命名空间(namespace),不是头文件(#include)。
静态导入
import不仅可以导入类,还可以导入静态方法和静态域:
import static java.lang.System.*;
import static java.lang.System.out;
静态导入不利于代码的清晰度。但是有两个实际的应用:
- 算术函数。
- 笨重的常量。
package com.horstmann.corejava;
public class Employee
{
...
}
若不加入package语句,这个源文件中的类就放在一个默认包(deafult package)中。
- 把类放到一个目录中,如/home/user/classdir。
- 将JAR文件放在一个目录中,如/home/user/archives。
- 设置类路径(class path)。类路径是所有包含类文件的路径的集合。
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg.java
或者
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg.java
- 包
- 公有类与接口
- 公有的和受保护的方法
- 公有的和受保护的域
例:
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary(e.g. 10 = 10%)
* @return the amount of the raise
*/
public double raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
域注释。只需要对公有域(通常指的是静态常量)建立文档。
通用注释
- @author name
- @version text
- @since text(始于条目)
- @deprecated text(不再使用注释)
- @see reference(在“see also”部分增加一个超级链接),reference可以选择下列情形之一
package.class#feature label
<a href="...">label<a>
"text"
第一种最常见,如:
@see com.horstmann.corejava.Employee#raiseSalary(double)
建立一个链接到com.horstmann,corejava,Employee类的raiseSalary(double)方法的超链接。
- 如果愿意的话,可以在注释中的任何位置放置指向其他类或方法的超链接,以及插入一个专用标记,如:
{@link package.class#feature label}
包与概述注释
要想产生包注释,就需要在每一个包目录中添加一个单独的文件,可以有如下两种选择:
- 提供一个以package.html命名的HTML文件。在标记<BODY>...</BODY>之间的所有文本都会被抽取出来。
- 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/**和*/界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
javadoc -d docDirectory nameOfPackage
或对于多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage1 nameOfPackage2...
如果文件在默认包中,应运行:
javadoc -d docDirectory *.java
- 一定要将数据设计为私有
- 一定要对数据初始化
- 不要在类中使用过多的基本数据类型
- 不是所有的域都需要独立的域访问器或域更改器
- 使用标准格式进行类的定义
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 公有访问特性部分
- 包作用域访问特性部分
- 私有访问特性部分
- 实例方法
- 静态方法
- 实例域
- 静态域