第四章
1.面向对象程序设计概述
在类之间,最常见的关系有:依赖(Dependence (“uses–a”))
聚合(Aggregation (“has–a”))
继承(Inheritance (“is–a”))
以订单处理系统为例,系统中的类有项目(Item),订单(Order),帐户(Account)等:
依赖,是最常见的关系,如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。如Order类使用Account类是因为Order对象要访问Account对象查看信用状态,但Item类不依赖于Account类。应尽量将相互依赖的类减至最少,即让类的耦合度最小。聚合,意味着包含。如一个Order对象包含一些Item对象。
继承,表示特殊与一般的关系。
注:常用UML绘制描述类之间关系的类图。
2.使用现有类
(1)对象与对象变量
以Date类为例,要构造一个Date对象,需要在构造函数前加上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类型的对象。但是,变量deadline不是一个对象,实际上也没有引用对象。要使用该变量,必须先初始化,可以新构造,也可以引用已存在的对象(此时两个变量引用同一个对象)。
一个对象变量并没有实际包含一个变量,而仅仅引用一个对象。
在java中,任何对象变量的值都是对存储在另外一个地方的第一对象的引用。new操作符的返回值也是一个引用。
可以显式的将变量置为null,表示没有引用任何对象,但变量不会自动初始化为null。
C++注释:
a. Java对象变量与C++的引用有区别:在C++中没有空引用,且引用不能被赋值。可以将java中的对象变量看作C++中的对象指针。如:
Date birthday; // Java
实际等同于
Date* birthday; // C++
b. 所有的java对象都存储在堆中。当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。
c. java语言中,不用担心向C++中的指针问题。
d. C++中可以通过拷贝构造函数和复制操作符来实现对象的自动拷贝。如一个链表的拷贝结果将得到一个内容相同的新链表,但却是一组独立的链接。这使得同样的拷贝行为内置在类中成为可能。在java中,必须使用clone()方法获得对象的完整拷贝。
(2)java类库中的GregorianCalendar类
Date类的时间是用一个距离固定时间的毫秒数(可正可负)来表示的,该时间点就是所谓的纪元(epoch),是UTC时间1970年1月1日00:00:00。
标准java类库还有一个日历表示法的类GregorianCalendar。实际上,GregorianCalendar扩展了一个更加通用的Calendar类。
Date类只提供了很少的方法来比较两个时间点,如before和after分别表示一个时间点是否早于或晚于零一个时间点。(事实上,Date类还有getDay,getMonth及getYear等方法,但不推荐使用。)
GregorianCalendar的构造函数很有用,有下面的形式:
new GregorianCalendar()//当前时间
new GregorianCalendar(1999, 11, 31)//使用年月日构造,时间是00:00:00。但要注意,月份从0开始计数,所以这里的11表示的是12月份
new GregorianCalendar(1999, Calendar.DECEMBER, 31)//同上,月份用常量表示,更清晰
new GregorianCalendar(1999, Calendar.DECEMBER, 31, 23, 59, 59)//日期和时间一起设置
(3)更改器方法与访问器方法
查询当前时间的年月日等信息,要用到GregorianCalendar类的get()方法:
GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendar.MONTH);
int weekday = now.get(Calendar.DAY_OF_WEEK);
设置年月日等信息要用到set()方法:
deadline.set(2001, Calendar.APRIL, 15);
还可以为给定的日期对象增加天数、星期数、月份等:
deadline.add(Calendar.MONTH, 3); // move deadline by 3 months
如果传递的是负数,则日期往后移。
C++注释:在C++中,带const后缀的方法是访问器方法;默认为更改器方法。但在java语言中,访问器方法与更改器方法在语法上没有明显区别。
通常习惯在访问器方法前加前缀get,更改器方法前加前缀set。如getTime和setTime来获取和设置时间点:
Date time = calendar.getTime();
calendar.setTime(time);
可从GregorianCalendar获取Date对象:
GregorianCalendar calendar = new GregorianCalendar(year, month, day);
Date hireDay = calendar.getTime();
也可用Date设置GregorianCalendar对象:
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(hireDay);
int year = calendar.get(Calendar.YEAR);
getFirstDayOfWeek()方法获得当前地区星期的起始日(如美国为星期天Calendar.SUNDAY,欧洲为星期一 Calendar.Monday)
如果要打印表示星期几名称的日历头,可以使用DateFormatSymbols类:
String [] weekdayNames = new DateFormatSymbols().getShortWeekdays();
API - java.util.GregorianCalendar 1.1
GregorianCalendar()
constructs a calendar object that represents the current time in the default time zone with the default locale.
GregorianCalendar(int year, int month, int day)
GregorianCalendar(int year, int month, int day, int hour, int minutes, int seconds)
constructs a Gregorian calendar with the given date and time.
Parameters: year the year of the date
month the month of the date. This value is 0-based; for example, 0 for January
day the day of the month
hour the hour (between 0 and 23)
minutes the minutes (between 0 and 59)
seconds the seconds (between 0 and 59)
int get(int field)
gets the value of a particular field.
Parameters: field one of Calendar.ERA, Calendar.YEAR, Calendar.MONTH,Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH, Calendar.DAY_OF_MONTH, Calendar.DAY_OF_YEAR, Calendar.DAY_OF_WEEK, Calendar.DAY_OF_WEEK_IN_MONTH,Calendar.AM_PM, Calendar.HOUR, Calendar.HOUR_OF_DAY ,
Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND,Calendar.ZONE_OFFSET, Calendar.DST_OFFSET
void set(int field, int value)
sets the value of a particular field.
Parameters: field one of the constants accepted by get
value the new value
void set(int year, int month, int day)
void set(int year, int month, int day, int hour, int minutes, int seconds)
sets the fields to new values.
Parameters: year the year of the date
month the month of the date. This value is 0-based; for example, 0 for January
day the day of the month
hour the hour (between 0 and 23)
minutes the minutes (between 0 and 59)
seconds the seconds (between 0 and 59)
void add(int field, int amount)
is a date arithmetic method. Adds the specified amount of time to the given time field. For example, to add 7 days to the current calendar date, call c.add(Calendar.DAY_OF_MONTH, 7).
Parameters: field the field to modify (using one of the constants documented in the get method)
amount the amount by which the field should be changed (can be negative)
int getFirstDayOfWeek()
gets the first day of the week in the locale of the current user, for example, Calendar.SUNDAY in the United States.
void setTime(Date time)
sets this calendar to the given point in time.
Parameters: time a point in time
Date getTime()
gets the point in time that is represented by the current value of this calendar object.
API - java.text.DateFormatSymbols 1.1
String[] getShortWeekdays()
String[] getShortMonths()
String[] getWeekdays()
String[] getMonths()
gets the names of the weekdays or months in the current locale. Uses Calendar weekday and month constants as array index values.
3.用户自定义类
(1)多个源文件
如,在一个源文件中自定义Employee类(雇员信息)和EmployeeTest类(public类,包含main函数,用来运行),源文件名要定义为EmployeeTest.java,因为文件名必须与public类的名字匹配。则编译时,编译器将在目录下创建2个类文件:Employee.class和EmployeeTest.class。
也可以将两个类讯在单独的源文件中,这样就有2个源文件:Employee.java和EmployeeTest.java。如果这样,可以有2中编译文件的方式,一种是使用通配符:
javac Employee*.java
也可以:
javac EmployeeTest.java
当编译器发现EmployeeTest类使用了Employee类时会查找名为Employee.class的文件,如果没有找到,则自动搜索Employee.java并进行编译。而且,如果Employee.java版本较已有的Employee.class版本新,则会自动重新编译Employee.java。
(2)构造器
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();
}
创建Employee类的实例:
new Employee("James Bond", 100000, 1950, 1, 1);
构造器需要注意:
构造器与类同名;
每个类可以有1个以上的构造器;
构造器可以有0个、1个或1个以上的参数;
构造器没有返回值;
构造器总是伴着new操作一起调用;
C++注释:
java构造器的工作方式与C++一样。但要记住,所有的java对象都是在堆中构造的,构造器总是随着new操作符一起使用,如下面的语句:
Employee number007("James Bond", 100000, 1950, 1, 1);
// C++, not Java
可在C++中运行,但java不可以。
(3)隐式参数与显式参数
方法用于操作对象以及存取它们的实例域。如Employee类中有方法:
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
将对象中的salary实例域设置为新值。如下面的调用:
number007.raiseSalary(5);
raiseSalary()方法有2个参数。第一个参数被称为隐式(implicit)参数,是出现在方法名前的Employee对象,第二个参数位于括号中,是一个显示(explicit)参数。
在每一个方法中,关键字this表示隐式参数。如果需要的话,可以这样写:
public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
C++注释:
在C++中,通常在类的外面定义方法(如在头文件中声明,而在cpp文件中定义),如果在类的内部(头文件中)定义方法,该方法自动成为内联方法(inline)。而在java中,所有方法都必须在类的内部定义,。是否将某个方法设置为内联方法是java虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。
(4)封装的优点
在类中,有时需要获得或设置实例域的值。此时,应提供下面三项内容:
一个私有(private)的数据域;
一个公有的域访问器方法;
一个公有的域更改器方法;
警告:注意不要编写返回引用可变对象的访问器方法。如在Employee类中加入getHireDay()方法:
class Employee
{
. . .
public Date getHireDay()
{
return hireDay;
}
. . .
private Date hireDay;
}
这样会破环封装性!如下面的代码:
Employee harry = . . .;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);
// let's give Harry ten years added seniority
由于java中的引用相当于C++中的指针,这里的d和harry.hireDay引用同一个变量。对d调用更改器方法就可以自动改变这个雇员对象的私有状态!
如果要返回可变对象的引用,应该使用clone,这样得到存放在另一个位置的副本:
class Employee
{
. . .
public Date getHireDay()
{
return (Date) hireDay.clone();
}
. . .
}
(5)final实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能再对它进行修改。例如,可以将Employee类中的name域声明为final,因为在对象构建之后,这个值不会再被修改,即没有setName()方法。
private final String name;
final修饰符大都应用于基本数据(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如String类)。对于可变的类,使用final修饰符可能会对读者造成混乱,如:
private final Date hiredate;
仅仅意味着存储在hiredate变量中的对象引用(C++中的指针?)在对象构造之后不能被改变,而并不意味着hiredate对象是一个常量。任何方法都可以对hiredate引用的对象调用setTime()更改器。
4.静态域与静态方法
之前的示例中,main方法都被标记为static修饰符。
(1)静态域
如果将域定义为static,与C++的静态变量相同。静态域是属于类的,不是属于独立对象,即时没有对象,静态域也存在。
(2)静态常量
类中静态常量使用的比较多,如:
public class Math
{
. . .
public static final double PI = 3.14159265358979323846;
. . .
}
在程序中,可用Math.PI获取这个常量。如果关键字static被省略,PI就变成了Math类的一个实例域,需要通过Math类对象来访问,且每一个对象都有一个PI拷贝。
还有一个多次使用的静态常量是System.out:
public class System
{
. . .
public static final PrintStream out = . . .;
. . .
}
前面提到最好不要将域设计为public,然而公有常量(即final域)却没问题。因为out被声明为final,所以不允许再将其他的打印流赋给它,如:
System.out = new PrintStream(. . .); // ERROR--out is final
注释:如果查看System类,会发现有一个setOut方法,可以将System.out设置为不同的流。这是因为setOut是一个本地方法,不是用java语言实现的,可以绕过java语言的存取控制机制。
(3)静态方法
静态方法是一种不能向对象实施操作的方法。例如:
Math.pow(x, a)
计算x的a次幂。在运算时,不使用任何Math对象,即没有隐式参数。
因为静态方法不能操作对象,所以不能在静态方法中访问实例域。但是可以访问自身类中的静态域。如在Employee类中定义:
public static int getNextId()
{
return nextId; // returns static field
}
可以通过类名调用该方法:
int n = Employee.getNextId();
注释:也可以通过对象调用静态方法,但建议使用类名调用,以免混淆。
在下面两种情况使用静态方法:
一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(如Math.pow);
一个方法只需要访问类的静态域(如Employee.getNextId)。
C++注释:
java中的静态域与静态方法在功能上与C++相同,但语法上有所区别。
(4)Factory方法
不太懂,先抄下来(5)main方法
main方法也是一个静态方法,不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需的对象。提示:每个类可以有一个main方法。这是一个常用的对类进行单元测试的方法。如果在Employee类中添加main方法,在需要独立的测试Employee类时,只需执行:
java Employee
如果该类是大型应用程序Application的一部分,则可以用下面的语句运行程序:
java Application
且Employee类的main方法不会执行。
5.方法参数
参数传递给方法分为:
值调用(call by value)--方法接收的是调用者提供的值
引用调用(call by reference)--方法接收的是调用者提供的变量地址
java语言总是采用值调用。也就是说,方法得到的是所有参数值的一个拷贝,且方法不能修改传递给它的任何参数变量的内容(这里的内容相对基本数据类型来说是数据值,对于对象引用来说是指针地址)。
方法参数共有2种类型:
基本数据类型(数字,布尔值)
对象引用
注意:一个方法不能修改一个基本数据类型的参数;而对象引用作为参数时,作为参数的对象引用和方法中的拷贝(也是一个对象引用)同时引用一个对象,在方法中可对拷贝作出更改,当方法结束后,拷贝不再使用,而作为参数的对象引用继续引用被更改过的对象。(java中的对象变量相当于C++中的对象指针)
总结在java中方法参数的使用情况:
一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)
一个方法可以改变一个对象参数的状态
一个方法不能实现让对象参数引用一个新的对象(对象引用a相当于指针,在方法中得到的是对象引用a的拷贝b,相当于一个新的指针b,改变这个新的指针b指向的地址是影响不到参数a的,只有改变指针b指向的内容才会影响参数b,即改变对象参数a的状态)
6.对象构造
java中提供了多种编写构造器的方式。
(1)重载
与C++相同。
(2)默认域初始化
如果构造器中没有显式的给域赋初值,则会自动赋默认值:数值为0,布尔值为false,对象引用为null。这是域和局部变量的主要不同。
(3)默认构造器
默认构造器是指没有参数的构造器。如果编写类时没有提供构造器,系统会提供一个默认构造器,将所有实例域初始化为默认值(数值为0,布尔值为false,对象引用为null)。如果类中提供了至少一个构造器,但没有提供默认构造器,则构造对象时必须提供构造参数,如Employee类只提供了一个构造器:
Employee(String name, double salary, int y, int m, int d)
则下面的调用会出现错误:
e = new Employee();
(4)显式域初始化
可以在类定义中,直接将一个值赋给任何域,如:
class Employee
{
. . .
private String name = "";
}
在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定实例域时,这种方式特别有用。
初始值不一定是常量。在下面的例子中,可以调用方法对域进行初始化。仔细看一下Employee类,其中每个雇员有一个id域,可以使用以下方法初始化:
class Employee
{
. . .
static int assignId()
{
int r = nextId;
nextId++;
return r;
}
. . .
private int id = assignId();
}
C++注释:在C++中,不能直接初始化实例域,所有域必须在构造器中设置,但是有构造器有一个初始化列表的语法。
(5)参数名
java程序员常在每个参数前面加上一个前缀"a",这样可以和实例域分开。
C++注释:C++中,通常在实例域前加前缀(如m_),java程序员通常不这样。
(6)调用另一个构造器
关键字this的一个用处是引用方法的隐式参数。this还有另一个含义:
如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器,如:
public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String, double)。
C++注释:在java中,this引用等价于C++的this指针,但在C++中一个构造器不能调用另一个构造器。
(7)初始化块
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;
. . .
// object initialization block
{
id = nextId;
nextId++;
}
}
其中,无论用哪个构造器构造对象,id域都在初始化块中被初始化。首先运行初始化块,再运行构造器的主体部分。
注意:为了避免循环定义,建议将初始化块放在域定义之后。
调用构造器的具体处理步骤:
1)所有数据域被初始化为默认值(0、false或null);
2)按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块;
3)如果构造器第一行调用了另一个构造器,则执行另一个构造器主体;
4)执行这个构造器的主体。
可以通过提供一个初始化值,或使用一个静态的初始化块来对静态域进行初始化。前面介绍过第一种,如:
static int nextId = 1;
如果对静态域初始化代码较复杂,可以静态的初始化块:
// static initialization block
static
{
Random generator = new Random();
nextId = generator.nextInt(10000);
}
在类第一次加载的时候,将会进行静态域的初始化。如果不显式设置,默认值将是0、false或null。所有静态初始化语句和静态初始化块都将按照类定义的顺序执行。
注释:可以用静态初始化块的方式写一个没有main方法的"hello,world":
public class Hello
{
static
{
System.out.println("Hello, World");
}
}
静态初始化块打印"Hello, World"之后,会得到"main is not defined"的错误信息,可以在静态初始化块的尾部调用System.exit(0)来避免。
API - java.util.Random 1.0
Random()
constructs a new random number generator.
int nextInt(int n) 1.2
returns a random number between 0 and n – 1.
(8)对象析构与finalize方法(不太明白)
java不需要人工回收内存,不支持析构器。如果某些对象使用了内存之外的其他资源,例如文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得格外重要。
如果某个资源需要在使用完毕后立刻关闭,那么需要由人工管理。可以应用一个类似dispose或close的方法完成相应的清理工作。特别需要说明,如果一个类使用了这样的方法,当对象不再使用时一定要调用它。
7.包
借助于包可以方便的组织自己的类,并将自己写的代码与别人提供的代码库分开管理。
标准的java类库分布在多个包中,包括java.lang,java.util和java.net等。标准的java包具有一个层次结构。如同硬盘的目录嵌套一样,也可使用嵌套层次组织包。所有标准的java包都处于java和javax包层次中。
使用包的主要原因是确保类名的唯一性,不同的包中的类可以有相同的名称。为保证包名的绝对唯一性,Sun公司建议将公司的互联网域名以逆序的形式作为包名,如com.horstmann,这个包还可进一步划分成子包,如com.horstmann.corejava。
从编译器的角度看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系,每一个都有独立的类集合。
(1)类的导入
一个类可以使用所属包中的所有类,及其他包中的公有类。可以有2中方式访问其他包中的公有类。第一种是用完整包名:
java.util.Date today = new java.util.Date();//此时可以不用import语句
还可用用import语句导入一个特定的类或整个包。import语句应位于源文件的顶部(但位于package语句的后面)。如:
import java.util.*;Date today = new Date();
也可以导入包中的特定类:
import java.util.Date;
需要注意的是只能用*导入一个包,而不能使用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?
可以增加一个特定的import语句来解决:
import java.util.*;
import java.sql.*;
import java.util.Date;
如果2个Date都需要使用,则只能使用完整的包名:
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);
在包中定义类是编译器(compiler)的工作,类文件中的字节码肯定使用完整的包名来引用其他类。
C++注释:
在C++中,必须使用#include将外部特性的声明加入进来,这是因为C++编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。java编译器可以查看其它文件的内部,只要告诉它到哪里查看就可以了。
在C++中,与包机制类似的是命名空间namespace。java中package与import语句类似于C++中的namespace和using指令(directive)。
(2)静态导入
从jaca SE 5.0开始,import不仅可以导入类,还增加了导入静态方法和静态域的功能。
例如,如果在源文件顶部,添加:
import static java.lang.System.*;
就可以使用System类的静态方法和静态域,不必加类型前缀:
out.println("Goodbye, World!"); // i.e., System.out
exit(0); // i.e., System.exit
另外,还可以导入特定的方法或域:
import static java.lang.System.out;
不过,很少有人用System.out或System.exit。有两个比较实际的应用:
算术函数:对Math类使用静态导入后,可以用:
sqrt(pow(x, 2) + pow(y, 2)) //Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
笨重的常量:要使用大量带有冗长名字的常量,应该使用静态导入,如:
if (d.get(DAY_OF_WEEK) == MONDAY) //if (d.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY)
(3)将类加入包中
要将类放入包中,必须将包的名字放在源文件的开头,包中定义类的代码之前,如:
package com.horstmann.corejava;
public class Employee
{
. . .
}
如果没有在源文件中设置包,这个源文件中的类就被放置在一个默认包(default package,是一个没有名字的包)中。
将包中的文件放到与完整包名匹配的子目录中。编译器也会将类文件放在相同的目录结构中。
例如,有2个源文件PackageTest.java(包含main方法,放在默认包中)和Employee.java(放在com.horstmann.corejava包中),则目录结构如下:
要编译这个程序,只需改变基目录(base directory),并运行下面的命令,编译器就会自动查找到Employee.java并编译:
javac PackageTest.java
如果不使用默认包,而是将类分别放在不同的包中,则目录结构如下:
在这种情况下,仍然要从基目录编译和运行类,即包含com目录:
javac com/mycompany/PayrollApp.java
java com.mycompany.PayrollApp
注意:编译器对文件进行操作,而java解释器加载类(带有.分隔符)。
警告:编译器在编译源文件的时候不检查目录结构,即不检查自身是否在package语句中的包所对应的目录中。但最终的程序会无法运行,因为虚拟机找不到类文件。
(4)包作用域
标记为public的部分可以被任意的类使用;标记为private的部分职能被定义它们的类使用。如果没有指定public或private,这个部分(类,方法或变量)可以被同一个包中的所有方法访问,这就是包作用域。
8.类路径
类存储在文件系统的子目录中,类的路径必须与包名匹配。
另外,类文件也可存储在jar(java归档)文件中。在一个jar文件中,可以包含多个压缩形式的类文件和子目录。在程序中用到第三方的库文件时,通常会给出一个或多个需要包含的jar文件。JDK也提供了许多jar,如jre/lib目录下包含数千个类库文件。
提示:jar文件使用zip格式组织文件和子目录。
为了让类能够让多个程序共享,需要以下几点:
1)把类放到一个目录a中,这个目录是包树状结构的基目录。如果将类添加到其中,类文件必须位于基目录下的包树状目录中;
2)将jar文件放在一个目录b中;
3)设置类路径(calss path)。类路径是所有包含类文件的路径的集合。
在windows中,类路径的不同项目之间用";"分隔(下面的"."表示当前目录):
c:\classdir;.;c:\archives\archive.jar
类路径包括:基目录,当前目录(.),还有jar文件。
从java SE 6开始,可以在jar文件目录中指定通配符,如:
c:\classdir;.;c:\archives\*
在归档文件中的所有jar文件(但不包括.class文件)都包含在类路径中。
由于运行时库文件(rt.jar和在jre/lib与jre/lib/ext目录下的一些其他的jar文件)会被自动搜索,所以不必将他们显式的列在类路径中。
警告:javac编译器总是会在当前目录中查找文件,而java虚拟机仅在类路径中有"."目录的时候才查看当前目录。如果没有设置类路径,那么会用默认的类路径(包含"."目录)。然而如果设置了类路径却忘记了包含"."目录,程序仍然可以通过编译,但不能运行。
设置类路径的方法:
1)首选:最好用-classpath或-cp选项指定类路径:
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg.java
所有指令书写在一行中。可以放在一个shell脚本或批处理文件中。
2)也可以通过设置CLASSPATH环境变量完成这个操作,格式如下:
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar
知道推出shell位置,类路径设置均有效。
警告:不要将CLASSPATH环境变量设置为永久不变的值。
9.文档注释
……
10.类设计技巧
有一些技巧可以使设计出来的类更具有OOP特性:
(1)一定要将数据设计为私有
(2)一定要对数据初始化
(3)不要在类中使用过多的基本数据类型(用类代替多个相关的基本数据类型)
(4)不是所有的域都需要独立的域访问器和域更改器
(5)使用标准格式进行类的定义(没有公认最好的风格,重要的是代码风格要一致)
采用以下顺序书写类的内容:
公有访问特性部分 -- 包作用域访问特性部分 -- 私有访问特性部分
且在每一步分钟,都按照下列顺序列出:
实例方法 -- 静态方法 -- 实例域 -- 静态域
(6)将职责过多的类进行分解
(7)类名和方法名要能够体现它们的职责