当前位置:网站首页 > Java基础 > 正文

java基础和对象实例



面向对象程序设计(Object-Oriented Programming, OOP)是当今的主流程序设计范型,它取代了20世纪70年代的“结构化”或过程式编程技术。由于Java是面向对象的,所以你必须熟悉OOP才能够很好地使用Java。

面向对象的程序是由对象组成的,每个对象包含用户公开的特定功能和隐藏的实现。程序中的很多对象是来自标准类库的“成品”,还有一些是自定义的。究竟是自己构造对象,还是从外界购买,这完全取决于开发项目的预算和时间但是,从根本上说,只要对象能够满足要求,就不必关心其功能是如何实现的。(我不认同这个观点)

传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程,下一步往往要考虑存储数据的适当方式。这就是Pascal语言的设计者Niklaus Wirth将其著作命名为《算法+数据结构=程序》(Algorithms + Data Structures = Programs, PrenticeHall, 1975)的原因。需要注意的是,在Wirth的这个书名中,算法是第一位的,数据结构排在第二位,这也反映了当时程序员的工作方式。首先,他们会确定操作数据的过程,然后再决定如何组织数据的结构,以便于操作数据。而OOP却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法。

对于一些规模较小的问题,将其分解为过程的做法是合适的,而对象更适合解决规模较大的问题。考虑一个简单的web浏览器,实现这个浏览器可能需要大约2000个过程,这些过程需要对一组全局数据进行操作。采用面向对象风格时,可能需要大约100个类,每个类平均包含20个方法。这种结构更易于程序员掌握,也更容易查找bug。假设一个特定对象的数据出错了,在访问这个数据项的20个方法中查找“罪魁祸首”要比在2000个过程中查找容易得多。

 

类:

(class)指定了如何构造对象。可以将类想象成制作小甜饼的模具,将对象想象为小甜饼。由一个类构造(construct)对象的过程称为创建这个类的一个实例(instance)

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

封装(encapsulation,有时称为信息隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现细节。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,一个特定对象有一组特定的实例字段值。这些值集合就是这个对象当前状态(state)。只要在对象上调用一个方法,它的状态就有可能发生改变。

实现封装的关键在于,绝对不能让其他类中的方法直接访问这个类的实例字段。程序只能通过对象的方法与对象数据进行交互。封装为对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键这意味着一个类可以完全改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。

OOP的另一个原则会让用户自定义Java类变得更为容易,这就是:可以通过扩展其他类来构建新类。事实上,Java提供了一个“神通广大的超类”,名为Object。所有其他类都扩展自这个Object类。(之后会讲Object类的内容)

扩展一个已有的类时,这个新类具有被扩展的那个类的全部属性和方法。你只需要在新类中提供适用于这个新类的新方法和实例字段。通过扩展一个类来得到另外一个类的概念称为继承(inheritance)。(之后会讲继承)

 

对象:

要想使用OOP,一定要清楚对象的三个主要特性:

·对象的行为(behavior)——可以对这个对象做哪些操作,或者可以对这个对象应用哪些方法?

·对象的状态(state)——调用那些方法时,对象会如何响应?

·对象的表示(identify)——如何区分可能有相同行为和状态的不同对象?

同一类的所有实例对象都有一种家族相似性,它们都支持相同的行为。一个对象的行为由所能调用的方法来定义。

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

但是,对象的状态并不能完全描述一个对象,因为每个对象都有一个唯一的标识(identify,或称身份)。例如,在一个订单处理系统中,任何两个订单都是不同的,即使它们订购的商品完全相同。需要注意,作为同一个类的实例,每个对象的标识总是不同的,状态也通常有所不同。

对象的这些关键特性会彼此相互影响。例如,对象的状态会影响它的行为(如果一个订单“已发货”或“已付款”,就应该拒绝要求增删商品的方法调用。反过来,如果订单是“空的”,即还没有订购任何商品,就不应该允许“发货”)。

 

识别类:

传统的过程式程序中,必须从最上面的main函数开始编写程序。设计一个面向对象系统时,则没有所谓的“最上面”因此,学习OOP的初学者常常会感觉无从下手。答案是首先从识别类开始,然后再为各个类添加方法。

识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应动词。

例如,在订单处理系统中,有这样一些名词:

·商品(Item)

·订单(Order)

·发货地址(Shipping address)

·付款(Payment)

·账号(Account)

从这些名词就可以得到类Item、Order等。

接下来查找动词。商品要添加(add)到订单中,订单可以发货(ship)或取消(cancel),另外可以对订单完成付款(apply)。对于每一个动词,如“添加”“发货”“取消”或者“完成付款”,要识别出负责完成相应动作的对象。例如,当一个新商品添加到订单中,订单对象就是负责的对象,因为它知道如何存储商品以及如何对商品进行排序。也就是说,add应该是Order类的一个方法,它接受一个Item对象作为参数。

当然,这种“名词与动词”原则只是一种经验,在创建类的时候,只有经验能帮助你确定名词和动词重要。

 

类之间的关系:

类之间最常见的关系有:、

·依赖(“uses-a”);

·聚合(“has-a”);

·继承(“is-a”)。

依赖(dependence),即“uses-a”关系,是一种最明显的也最一般的关系。例如,Order类使用了Account类,因为Order对象需要访问Account对象来查看信用状态。但是Item类不依赖于Account类,因为Item对象不需要考虑客户账户。因此,如果一个类的方法要使用或操作另一个类的对象,我们就说前一个类依赖于后一个类。

应当尽可能减少相互依赖的类。这里的关键是,如果类A不知道B的存在,它就不会关心B的任何改变(这意味着B的改变不会在A中引入bug)。用软件工程的术语来说,就是要尽可能减少类之间的耦合(coupling)。

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

【注释:有些方法学家不喜欢聚合这个概念,而更喜欢使用更一般的“关联”关系。从建模的角度看,这是可以理解的。但对于程序员来说,“has-a”关系更加形象。我喜欢使用聚合还有另一个原因:关联的标准记法不是很清楚,请参见表4-1:

 

继承(inheritance),即“is-a”关系,表示一个更特殊的类与一个更一般的类之间的关系。例如,RushOrder类继承了Order类。在特殊化的RushOrder类中包含一些用于优先处理的特殊方法,还提供了一个计算运费的不同方法;而其他的方法,如添加商品、生成账单等都是从Order类继承来的。一般而言,如果类D扩展了类C,类D会继承类C的方法,另外还会有一些额外的功能(以后会详细讲继承)。

很多程序员采用UML(Unified Modeling Language,统一建模语言)绘制类图,来描述类之间的关系。图4-2就是这样一个例子。类用矩阵表示,类之间的关系用带有各种修饰的箭头表示。表4-1给出了UML中最常见的箭头样式。

 

 

使用预定义类:

在Java中,没有类就无法做任何事情,我们前面曾经接触过几个类。然而,并不是所有的类都表现出面向对象的典型特征。以Math类为例。你已经看到,可以直接使用Math类的方法,如Math.random,而不必了解它具体是如何实现的,你只需要知道方法名和参数(如果有的话)。这正是封装的关键所在,当然所有类都是这样。但Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必考虑创建对象和初始化它们的实例字段,因为根本没有实例字段。

 

对象与对象变量:

要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。

在Java程序设计语言中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,其作用是构造并初始化对象。下面来看一个例子。标准Java库中包含一个Date类。它的对象可以描述一个时间点,例如,“December 31, 1999, 23:59:59 GMT”。

【注释:你可能会感到奇怪:为什么用类表示日期,而不是像其他语言中那样用一个内置(built-in)类型来表示?例如,Visual Basic中有一个内置的date类型,程序员可以采用#12/31/1999#格式指定日期。看起来这似乎很方便,程序员只需要使用内置的date类型,而不用考虑类。但实际上,Visual Basic这样设计合适吗?在有些地区,日期表示为月/日/年,而另外一些地区则表示为日/月/年。语言设计者是否能够预见这些问题呢?如果没有处理好这类问题,语言就有可能陷入混乱,对此感到不满的程序员也会丧失使用这种语言的热情。如果使用类,这些设计任务就交给了类库的设计者,如果类设计得不完善,那么其他程序员可以很容易地编写自己的类,改进或替代(replace)这些系统类(作为印证:Java的日期类库开始时有些混乱,现在已经重新设计了两次)。】

构造器总是与类同名。因此,Date类的构造器就名为Date。要想构造一个Date对象,需要在构造器前面去加上new操作符,如下所示:

new Date()

这个表达式会构造一个新对象。这个对象初始化为当前的日期和时间。

如果需要的话,可以将这个对象传递给一个方法:

System.out.println(new Date());

或者,可以对刚构造的对象应用一个方法。Date类中有一个toString方法。这个方法将生成日期的一个字符串描述。可以如下对新构造的Date对象应用toString方法:

String s = new Date().toString();

在这两个例子中,构造的对象仅使用了一次。通常,你可能希望保留所构造的对象从而能继续使用,为此,需要将对象存放在一个变量中:

Date rightNow = new Date();

对象与对象变量之间存在着一个重要的区别。例如,以下语句:

Date startTime; // startTime doesn't refer to any object;

定义了一个对象变量startTime,它也可以引用Date类型的对象。但是,一定要认识到:变量startTime不是一个对象,而且实际上它甚至还没有引用任何对象。此时不能在这个变量上使用任何Date方法。下面的语言:

s = startTime.toString(); // not yet

将产生编译错误。

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

startTime = new Date();

也可以设置这个变量,让它引用一个已有的对象:

startTime = rightNow;

现在,这两个变量都引用同一个对象。

要认识到重要的一点:对象变量并不实际包含一个对象,它只是引用一个对象。

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

Date startTime = new Date();

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

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

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

【C++注释:很多人错误地认为Java中的对象变量就相当于C++的引用。然而,C++中没有null引用,而且引用不能赋值。应当把Java中的对象变量看作类似于C++的对象指针(但并不是指针,java是按值调用的)。例如,

Date rightNow; // Java

实际上等同于:

Date* rightNow; // C++

一旦建立了这种关联,一切就清楚了。当然,只有使用了new调用后Date*指针才会初始化。就这一点而言,C++与Java的语法几乎是一样的。

Date* rightNow = new Date(); // C++

如果把一个变量复制到另一个变量,两个变量就指向同一个日期,即它们是同一个对象的指针。Java中的null引用对应于C++中的null指针。

所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含另一个堆对象的指针。

在C++中,指针十分令人头疼,因为它们很容易出现错误。稍不小心就会创建一个错误的指针,或者使内存管理出问题。在Java语言中,这些问题都不复存在。如果使用一个没有初始化的指针,那么运行时系统就会产生一个运行时错误,而不是生成随机的结果。另外,你不必关系内存管理问题,垃圾回收器会处理相关的事宜。

C++确实做了很大的努力,它通过支持复制构造器和赋值运算符来实现对象的自动复制。例如,一个链表(linked list)的副本是一个新链表,其内容于原始链表相同,但是有一组独立的链接。这样一来就可以适当地设计类,使它们与内置类型有复制行为。在Java中,必须使用clone方法获得一个对象的完整副本。】

 

Java类库中的LocalDate类:

在前面的例子中,我们使用了Java标准类库中的Date类。Date类的实例有一个状态,也就是一个特定的时间点。

尽管在使用Date类不必知道这一点,但时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个时间点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。UTC就是Coordinated Universal Time(国际协调时间),与大家熟悉的GMT(即Greenwich Mean Time,格林尼治时间)一样,是一种实用的科学标准时间。

但是,Date类对于处理人类记录日期的日历信息并不是很有用,如“December 31,1999”。这个特定的时间描述遵循Gregorian阳历,这是世界上大多数国家使用的日历。但是,同样的这个时间点采用中国或希伯来的阴历来描述会大不相同,倘若我们有来自火星的顾客,基于他们使用的火星历来描述这个时间点就更不一样了。

类库设计者决定将保存时间与给时间点命名分开。所以,标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用大家熟悉的日历表示法表示日期的LocalDate类。Java8引入了另外一些类来处理日期和时间的不同方面——(以后会讲)。

将时间度量与日历分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同的概念。

不要使用构造器来构造LocalDate类的对象。实际上,应当使用静态工厂方法(factory method),它会代表你调用构造器。下面的表达式:

LocalDate.now()

会构造一个新对象,表示构造这个对象时的日期。

可以提供年、月和日来构造对应一个特定日期的对象:

LocalDate.of(1999, 12, 31)

一旦有了一个LocalDate对象,可以用方法getYear、getMonthValue和getDayOfMonth得到年、月和日:

int year = newYearsEve.getYear(); // 1999
int month = newYearEve.getMonthValue(); // 12
int day = newYearEve.getDayOfMonth(); // 31

看起来这似乎没有多大的意义,因为这正是构造对象时使用的那些值。不过,有时可能有一个计算得到的日期,然后你希望调用这些方法来了解它的更多信息。例如,plusDays方法会生成一个新的LocalDate,如果把应用这个方法的对象称为当前对象,那么这个新日期对象则是距当前对象指定天数的一个新日期:

LocalDate aThousandDayLater = newYearsEve.plusDays(1000);
int year = aThousandDayLater.getYear(); // 2002
int month = aThousandDayLater.getMonthValue(); // 09
int day = aThousandDayLater.getDayOfMonth(); // 26

LocalDate类封装了一些实例字段来维护所设置的日期。如果不查看源代码,就不可能知道类内部的日期表示。当然,封装的意义就在于内部表示并不重要,重要的是类对外提供的方法。

【注释:实际上,Date类也有得到日、月、年的方法,分别是getDay、getMonth以及getYear,不过这些方法已经废弃。当类库设计者意识到某个方法最初就不该引入时就把它标记为废弃,不鼓励使用。

类库设计者意识到应当单独提供类来处理日历,不过在此之前这些方法已经是Date类的一部分了。Java1.1中引入较早的一组日历类时,Date方法被标记为舍弃废弃。虽然仍然可以在程序中使用这些方法,不过如果这样做,编译时会出现警告。最好不要使用废弃的方法,因为将来的某个类库版本很有可能会将它们完全删除。】

【提示:JDK提供了jdeprscan工具来检查你的代码中是否使用了JavaAPI已经废弃的特性。有关说明参见

http://docs.oracle.com/en/java/javase/17/docs/specs/man/jdeprscan.html】

 

更改器方法与访问器方法:

再来看上一节中的plusDays方法调用:

LocalDate aThousandDayLater = newYearsEve.plusDays(1000);

这个调用之后newYearsEve会有什么变化?它会改为1000天之后的日期吗?事实上,并没有。plusDays方法会生成一个新的LocalDate对象,然后把这个新对象赋给aThousandDaysLater变量。原来的对象不做任何改动。我们说plusDays方法没有更改(mutate)调用这个方法的对象。(这类似于第3章见过的String类的toUpperCase方法。在一个字符串上调用toUpperCase时,这个字符串仍保持不变,并返回一个包含大写字符的新字符串。)

Java库的一个较早版本曾经有另一个处理日历的类,名为GregorianCalender。可以如下为这个类表示的一个日期增加1000天:

GregorianCalendar someDay = new GregorianCalender(1999, 11, 31);
    // odd feature of that class: month numbers go from 0 to 11
someDay.add(Calendar.DAY_OF_MONTH, 1000);

与LocalDate.plusDays方法不同,GregorianCalendar.add方法是一个更改器方法(mutator method)。调用这个方法后,someDay对象的状态会改变。可以如下查看新状态:

year = someDay.get(Calendar.YEAR); // 2002
month = somDay.get(Calendar.MONTH) + 1; // 09
day = someDay.get(Calendar.DAY_OF_MONTH); // 26

正是因为这个原因,我们将变量命名为someDay而不是newYearsEve——调用这个更改器方法之后,它不再是新年前夜。

相反,只访问对象而不修改对象的方法有时称为访问器方法(accessor method)。例如:LocalDate.getYear和GregorianCalendar.get就是访问器方法。

【C++注释:在C++中,带有const后缀的方法是访问器方法;没有声明为const的方法默认为更改器方法。但是,在Java语言中,访问器方法与更改器方法在语法上没有明显区别。】

下面用一个具体应用LocalDate类的程序来结束这一节。这个程序将显示当前月的日历。

当前日期标记有一个*号。可以看到,这个程序需要知道如何计算某月份的天数以及一个给定日期是星期几。

下面来看这个程序的关键步骤。首先构造一个对象,并用当前的日期初始化。

LocalDate date = LocalDate.now();

下面获得当前的月份和日期。

然后,将date设置为这个月的第一天,并得到这一天为星期几。

date = date.minusDys(today - 1); // set to start of month
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue(); // 1 = Monday, ... , 7 = Sunday

变量weekday设置为DayOfWeek类型的对象。我们调用这个对象的getValue方法来得到对应星期几的一个数值。这会得到一个整数,这里遵循国际惯例,即周末是一周的结束,星期一就返回1,星期二返回2,依此类推。星期日则返回7。

注意,日历的第一行是缩进的,使当月第一天对应正确的星期几。下面的代码会打印表头和第一行的缩进:

System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++)
    System.out.print("  ");

现在我们来打印日历的主体。进入一个循环,其中date遍历一个月中的每一天。

每次迭代中,我们要打印日期值。如果date是当前日期,这个日期则用一个*标记。接下来,把date推进到下一天。如果到达新的一周的第一天,则换行打印:

while (date.getMonthValue() == month)
{
    System.out.printf("%3d", date.getDayOfMonth());
    if (date.getDayOfMonth() == today)
        System.out.print("*");
    else
        System.out.print("*");
    date = date.plusDays(1);
    if (date.getDayOfWeek().getValue() == 1) System.out.println();
}

什么时候结束呢?我们不知道这个月有几天,是31天、30天、29天还是28天?实际上,只要date还在当月就要继续迭代。

可以看到,利用LocalDate类可以编写一个日历程序,它能处理星期几以及各月天数不同等复杂问题。你并不需要知道LocalDate类如何计算月和星期几,只需要使用这个类的接口,也就是诸如plusDays和getDayOfWeek等方法。

这个示例程序的重点是向你展示如何使用一个类的接口来完成相当复杂的人物,而无须了解实现细节。

Java.time.LocalDate  8

·static LocalDate now()

构造一个表示当前日期的对象。

·static LocalDate of(int year, int month, int day)

构造一个表示给定日期的对象。

·int getYear()

·int getMonthValue()

·int getDayOfMonth()

得到当前日期的年、月和日。

·DayOfWeek getDayOfWeek()

得到当前日期是星期几,作为DayOfWeek类的一个实例返回。在DayOfWeek实例上调用getValue来得到1~7之间的一个数,表示这是星期几,1表示星期一,7表示星期日。

·LocalDate plusDays(int n)

·LocalDate minusDays(int n)

生成当前日期之后或之前n天的日期。

 

以下是示例代码:

import java.time.*;
import java.util.*;
public class firstWar {
    public static void main(String[] args){
        LocalDateTime today = LocalDateTime.now();
        System.out.println(today);
        System.out.print("请输入你想要查询的月份:");
        Scanner in = new Scanner(System.in);
        int userYear = in.nextInt();
        int userMonth = in.nextInt();
        int userDay = 1;
        LocalDate user = LocalDate.of(userYear,userMonth,userDay);
        DayOfWeek weekday = user.getDayOfWeek();
        int value = weekday.getValue();
        System.out.println("Mon Tue Wed Thu Fri Sat Sun");
        for (int i = 1; i < value; i++)
            System.out.print("    ");
        while (user.getMonthValue() == userMonth)
        {
            System.out.printf("%3d", user.getDayOfMonth());
            if (user.getDayOfMonth() == today.getDayOfMonth())
                System.out.print("*");
            else
                System.out.print(" ");
            user = user.plusDays(1);
            if (user.getDayOfWeek().getValue() == 1) System.out.println();
        }
        if (user.getDayOfWeek().getValue() != 1) System.out.println();
       }
    }

自定义类:

现在来学习如何编写更复杂的应用所需要的那种主力类(workhorse class)。通常,这些类没有main方法,而有自己的实例字段和实例方法。要想构建一个完整的程序,会结合使用多个类,其中只有一个类有main方法。

在Java中,最简单的类定义形式为:

class ClassName
{
    field1
    field2
    ...
    constructor1
    constructor2
    ...
    method1
    method2
    ...
}

下面看一个非常简单的Employee类,编写工资管理系统时可能会用到:

class Employee
{
   // instance fields
   private String name;
   private double salary;
   private LocalDate hireDay;

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

   // some method
   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;
   }
   
   // more method
   ...
}

这里将这个类的实现分成以下几个部分,并分别在稍后的几节中介绍。不过,先来看个程序:

import java.time.*;
public class firstWar {
    public static void main(String[] args){
        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Carl Caracker", 75000,1987,12,15);
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

        //raise everyone's salary by 5%
        for (Employee e :staff)
            e.raiseSalary(5);

        // print out information about all Employee objects
        for (Employee e : staff)
            System.out.println("name = " + e.getName() + ",salary=" + e.getSalary()+" , hireDay=" + e.getHireDay());
       }
    }

 class Employee {
     // instance fields
     private final String name;
     private double salary;
     private final LocalDate hireDay;

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

     // some method
     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;
     }

     // more method
     //...
 }

在这个程序中,我们构建了一个Employee数组,并填入了3个Employee对象:

Employee[] staff = new Employee[3];

staff[0] = new Employee("Carl Caracker", 75000,1987,12,15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

接下来,使用Employee类的raiseSalary方法将每个员工的薪水提高5%:

最后,调用getName方法、getSalary方法和getHireDay方法打印各个员工的信息:

for (Employee e : staff)
    System.out.println("name = " + e.getName()
            + ",salary=" + e.getSalary()
            +" , hireDay=" + e.getHireDay());

注意,这个示例程序中包含两个类:Employee类和带有public访问修饰符的firstWar类。firstWar类包含main方法,其中使用了前面的代码。

源文件名是firstWar.java,这是因为文件名必须与public类的名字匹配。一个源文件中只能有一个公共类,但可以有任意数目的非公共类。

接下来,编译这段源代码的时候,编译器将在目录中创建两个类文件:firstWar.class和Employee.class。

然后启动这个程序,为字节码解释器提供程序中包含main方法的那个类的类名:

java firstWar

字节码解释器开始运行firstWar类的main方法中的代码。这个代码会先后构造3个新Employee对象,并显示它们的状态。

 

使用多个源文件:

在上面的程序中,一个源文件包含了两个类。许多程序员习惯将各个类放在一个单独的源文件中。例如,将Employee类放在文件Employee.java中,而将firstWar类存放在文件firstWar.java中。

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

javac Employee*.java

这样一来,所有与通配符匹配的源文件都将被编译成类文件。或者可以简单地键入以下命令:

javac firstWar.java

你可能会感到惊讶,使用第二种方式时并没有显式地编译Employee.java。不过,当Java编译器发现firstWar.java中使用了Employee类时,它会查找名为Employee.class的文件。如果没有找到这个类文件,就会自动搜索Employee.java并编译这个文件。另外,如果Employee.java的版本较已有的Employee.class文件版本更新,Java编译器就会自动地重新编译这个文件。

【注释:如果熟悉UNIX的make工具(或者是Windows中的相应工具,如make),那么就可以认为Java编译器内置了make功能。

 

剖析Employee类:

下面将对Employee类进行剖析。

首先从这个类的方法开始。通过查看源代码会发现,这个类包含一个构造器和4个方法:

public Employee(String n, double s, int year, int month, int day)

public String getName()

public double getSalary()

public LocalDate getHireDay()

public void raiseSalary(double byPercent)

这个类的所有方法都被标记为public。关键字public意味着任何类的任何方法都可以调用这些方法(共有4种访问级别,将在后面讲)。

接下来,需要注意在Employee类的实例中有3个实例字段,用来存放将要操作的数据:

private final String name;
private double salary;
private final LocalDate hireDay;

关键字private确保只有Employee类本身的方法能够访问这些实例字段,任何其他类的方法都不能读写这些字段。

【注释:可以用public标记实例字段,但这是一种很不好的做法。public实例字段允许程序的任何部分都能对其进行读取和修改,这就完全破坏了封装。任何类的任何方法都可以修改public字段,从我们的经验来看,有些代码将利用这种做法存取权限,而这是我们最不希望看到的。因此,这里强烈建议将实例字段标记为private。】

最后,请注意,有两个实例字段本身就是对象: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类的对象时,构造器会运行,这会将实例字段初始化为所希望的初始状态。

例如,使用下面这个代码创建Employee类的一个实例时:

new Employee("Jame Bond", , 1950, 1, 1)

将如下设置实例字段:

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

构造器与其他方法有一个重要的不同。构造器总是结合new操作符来调用。不能对一个已经存在的对象调用构造器来重新设置实例字段。例如,

james.Employee("Jame Bond", , 1950, 1, 1) // ERROR

将产生编译错误。

稍后会更详细地介绍有关构造器的内容。现在只需要记住:

·构造器与类同名。

·每个类可以有一个以上的构造器。

·构造器可以有0个、1个或多个参数。

·构造器没有返回值。

·构造器总是结合new操作符一起调用。

【C++注释:Java中构造器的工作方式与C++中相同。但是,要记住所有Java对象都是在堆中构造的,构造器总是结合new操作符一起使用。C++程序员最易犯的错误就是忘记new操作符:

Employee number007("Jame Bond", , 1950, 1, 1); // C++, not Java

这条语句在C++中能够正常运行,但在Java中却不行。】

【警告:请注意,不要引入与实例字段同名的局部变量。例如,下面的构造器将不会设置name或salary实例字段:

public Employee(String n, double s, . . .)
{
    String name = n; // ERROR
    double salary = s; // ERROR
    . . .
}

这个构造器声明了局部变量name和salary。这些变量只能在构造器内部访问,它们会遮蔽(shadow)同名的实例字段。有些程序员偶尔会不假思索地写出这类代码,因为他们的手指会不自觉地增加数据类型。这种错误很难检查出来,因此,必须注意在所有地方法中都不要使用与实例字段同名的变量。】

 

用var声明局部变量:

在Java 10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无须指定类型。例如,可以不这样声明:

Employee harry = new Employee("Harry Hacker", 50000, 1989,10,1);

只需要写为:

var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

这一点很好,因为这样可以避免重复写类型名Employee。

从现在开始,倘若无须了解Java API就能从等号右边明显看出类型,在这种情况下我们都将使用var表示法。不过我们不会对数值类型使用var,如int、long或double,这样你就不用当心0、0L和0.0之间的区别。对javaAPI有了更多经验后,你可能会希望更多地使用var关键字。

注意var关键字只能用于方法中的局部变量。参数和字段的类型必须声明

 

使用null引用:

在之前我们已经了解到,对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有任何对象。

听上去这是一种处理特殊情况的便捷机制,如未知的名字或雇用日期。不过使用null值时要非常小心。

如果对null值应用一个方法,会产生一个NullPointerException异常:

这是一个很严重的错误,类似于“索引越界”异常。如果你的程序没有“捕获”异常,那么程序就会终止。正常情况下,程序并不捕获这些异常,而是依赖于程序员从一开始就不要带来异常。

【提示:程序因NullPointerException异常终止时,栈轨迹会显示问题出现在哪一行代码中。从Java17开始,错误消息会包含有null值的变量或方法名。例如,在以下调用中:

String s = e.getHireDay().toString();

错误消息会告诉你e是否为null或者getHireDay是否返回null。】

定义一个类时,最好清楚地知道哪些字段可能为null。在我们的例子中,我们不希望name或hireDay字段为null。(不用担心salary字段,这个字段是基本类型,所以不可能是null。)

hireDay字段肯定是非null的,因为它初始化为一个新的LocalDate对象。但是name可能为null,如果调用构造器时为n提供的实参是null,name就会是null。

对此有两种处理方法。“宽容”方法是把null参数转换为一个适当的非null值:

if (n == null) name = "unknown";else name = n;

Objects类对此提供了一个便利方法:

public Employee(String n, double s, int year, int month, int day) {
         name = Objects.requireNonNullElse(n, "unknown");
         . . .
     }

“严格”方法则干脆拒绝null参数:

public Employee(String n, double s, int year, int month, int day) {
         name = Objects.requireNonNull(n, "The name cannot be null");
         . . .
     }

如果用null名字构造一个Employee对象,就会产生NullPointerException异常。乍看上去这种补救方法好像不太有用,不过这种方法有两个好处:

1. 异常报告会提供这个问题的描述。

2. 异常报告会准确地指出问题所在的位置,否则NullPointerException异常会出现在其他地方,而很难追踪到真正导致问题的构造器参数。

【注释:如果要接受一个对象引用作为构造参数,就要问问自己:是不是真的希望接受可有可无的值。如果不是,那么“严格”方法更合适。】

 

隐式参数和显式参数:

方法会操作对象并访问它们的实例字段。例如,以下方法:

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

将调用这个方法的对象的salary实例字段设置为一个新值。考虑下面这个调用:

number007.raiseSalary(5);

其作用是将number007.salary字段的值增加5%。具体地说,这个调用将执行以下指令:

raiseSalary方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类型的对象。第二个参数是位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有人把隐式参数称为方法调用的目标或接收者。)

可以看到,显式参数显式地列在方法声明中,例如double byPercent。隐式参数则没有出现在方法声明中。

在每一个方法中,关键字this指示隐式参数。如果愿意,可以如下改写raiseSalary方法:

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

有些程序员更偏爱这样的风格,因为这样可以将实例字段与局部变量明显地区分开来。

【C++注释:在C++中,通常在类的外面定义方法:

void Employee::raiseSalary(double byPercent) // C++,not Java
{
    . . .
}

如果在类的内部定义方法,那么这个方法将自动成为内联(inline)方法。

class Employee
{
    . . .
    int getName() { return name; } // inline in C++
}

在Java中,所有的方法都必须在类的内部定义,但这并不表示它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。即时编译器会关注那些简短、经常调用而且没有被覆盖的方法调用,并进行优化。

 

封装的优点:

最后再仔细看一下非常简单的getName方法、getSalary方法和getHireDay方法:

public String getName() {
    return name;
}

public double getSalary() {
    return salary;
}

public LocalDate getHireDay() {
    return hireDay;
}

这些都是典型的访问器方法。由于它们只返回实例字段的值,因此又称为字段访问器(field accessor)。

如果将name、salary和hireDay字段标记为公共,而不是编写单独的访问器方法,不是更容易一些吗?

不过,name是一个只读字段,一旦在构造器中设置,就没有办法能够修改这个字段。这样我们可以确保name字段不会受到外界的破坏。

虽然salary不是只读字段,一旦在构造器中设置,就没有办法能够修改这个字段。这样我们可以确保name字段不会受到外界的破坏。

虽然salary不是只读字段,但是它只能用raiseSalary方法修改。具体地,如果这个值出现了错误,那么只需要调试这个raiseSalary方法就可以了。如果salary字段是公共字段,破坏这个字段值的罪魁祸首有可能出没在任何地方(那就很难调试了)。

有些时候,可能想要获得或设置实例字段的值,那么你需要提供下面三项内容:

·一个私有的实例字段;

·一个公共的字段访问器方法;

·一个公共的字段更改器方法。

这样做要比提供一个简单的公共实例字段复杂些,但有很多明显的好处。

首先,可以改变内部实现,而不影响该类方法之外的任何其他代码。例如,如果将存储姓名的字段改为:

那么getName方法可以改为返回

firstName + " " + lastName

这个修改对于程序的其余部分是完全不可见的。

当然,为了进行新旧数据表示之间的转换,访问器方法和更改器方法可能需要做许多工作。这将为我们带来第二个好处:更改器方法可以完成错误检查,而只对字段赋值的代码不会费心这么做。例如,setSalary方法可以检查工资是否小于0。

【警告:注意不要编写返回可变对象引用的访问器方法在本书之前的一版中,我们的Empolyee类就违反了这个设计原则,其中的getHireDay方法返回了一个Date类的对象:

class Employee
{
    private Date hireDay;
    . . .
    public Date getHireDay()
    {
        return hireDay; // BAD
    }
    . . .
}

LocalDate类没有更改器方法,与之不同,Date类有一个更改器方法setTime,可以设置毫秒数。

Date对象是可变的,这一点就破坏了封装性!考虑下面这段有问题的代码:

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 of added seniority

出错的原因很微妙。d和harry.hireDay引用同一个对象。对d调用更改器方法会自动改变这个Employee对象的私有状态!

如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。对象克隆是指存放在另一个新位置的对象副本(有关对象克隆的详细内容以后会讲)。下面是修改后的代码:

class Employee
{
    . . .
    public Date getHireDay()
    {
        return (Date) hireDay.clone(); // OK
    }
    . . .
}

这里有一个经验,如果需要返回一个可变字段的副本,就应该使用clone

 

基于类的访问权限:

从前面已经知道,方法可以访问调用这个方法的对象的私有数据。一个类的方法可以访问这个类的所有对象的私有数据,这令很多人感到奇怪。例如,下面来看用来比较两个员工的equals方法:

class Employee
{
    . . .
    public boolean equals(Employee other)
    {
        return name.equals(other.name);
    }
}

下面是一个典型的调用:

if (harry.equals(boss)). . .

这个方法访问harry的私有字段,这并不让人奇怪,不过,它还访问了boss的私有字段。这是合法的,其原因是boss是Employee类型的对象,而Employee类的方法可以访问任何Employee类型对象的私有字段。

【C++注释:C++也有同样的规则。方法可以访问所属类任何对象的私有特性(feature),而不仅限于隐式参数。

 

私有方法:

实现一个类时,我们会将所有实例字段都设置为私有字段,因为公共数据很危险。不过,方法又应该如何设置呢?尽管大多数方法都是公共的,但在某些情况下,私有方法可能很有用。有时,你可能希望将一个计算代码分解成若干个独立的辅助方法。通常,这些辅助方法不应该成为公共接口的一部分,这是因为它们往往与当前实现关系非常紧密,或者需要一个特殊协议或调用次序。最好将这样的方法实现为私有方法。

在Java中,要实现一个私有方法,只需将关键字public改为private即可。

如果将一个方法设置为私有,倘若你改变了方法的具体实现,并没有义务保证这个方法依然可用。如果数据的表示发生了变化,那么这个方法可能变得更难实现,或者不再需要;这并不重要。重点在于,只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法。

 

final实例字段:

可以将实例字段定义为final。这样的字段必须在构造对象时初始化。也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且不能再修改这个字段。例如,可以将Employee类中的name字段声明为final,因为在对象构造之后,这个值不会再改变,即没有setName方法。

. . .

}

final修饰符对于类型为基本类型或者不可变类的字段尤其有用。(如果类中的所有方法都不会改变其对象,这样的类就是不可变类。例如,String类就是不可变的。)

对于可变类,使用final修饰符可能会造成混乱。例如,考虑以下字段:

private final StringBuilder evaluation;

它在Employee构造器中初始化为

Evaluations = new StringBuilder();

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

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

 

静态字段:

如果将一个字段定义为static,那么这个字段并不出现在每个类的对象中。每个静态字段只有一个副本。可以认为静态字段属于类,而不属于单个对象。例如,假设需要为每一个员工分配唯一的表示码,这里为Empoyee类添加一个实例字段id和一个静态字段nextId:

class Employee
{
    private static int nextId = 1;
    
    private int id;
    . . .
}

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

【注释:在一些面向对象程序设计语言中,静态字段被称为类字段。术语“静态”只是沿用了C++的叫法,并无实际意义。】

在构造器中,我们为新Employee对象分配下一个可用的ID,然后将其增1:

假设我们构造了对象harry。Harry的id字段设置为静态字段nextId的当前值,并将静态字段nextId的值加1:

 

静态常量:

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

public class Math
{
    . . .
    public static final double pi = 3.;
    . . .
}

在你的程序中,可以用Math.PI来访问这个常量。

如果省略关键字static,那么PI就变成了Math类的一个实例字段。也就是说,需要通过Math类的一个对象来访问PI,并且每一个Math对象都有它自己的一个PI副本。

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

public class System
{
    . . .
    public static final PrintStream out = . . .;
    . . .
}

前面曾经多次提到过,最好不要有公共字段,因为谁都可以修改公共字段。不过,公共常量(即final字段)却没问题。因为out被声明为final,所以,不允许再将它重新赋值为另一个打印流:

System.out = new PrintStream(. . .); // ERROR--out is final

【注释:如果查看System类,就会发现有一个setOut方法可以将System.out设置为不同的流。你可能会感到奇怪,为什么这个方法可以修改final变量的值。原因在于,setOut方法是一个原生方法,而不是在Java语言中实现的。原生方法可以绕过Java语言的访问控制机制。这是一种特殊的解决方法,你自己编写程序时不要模仿这种做法。】

 

静态方法:

静态方法是不操作对象的方法。例如,Math类的pow方法就是一个静态方法。以下是表达式:

Math.pow(x, a)

会计算幂x^a。它并不使用Math对象来完成这个任务。换句话说,它没有隐式函数。

可以认为静态方法是没有this参数的方法(在一个非静态方法中,this参数指示这个方法的隐式参数。)

Employee类的静态方法不能访问id实例字段,因为它不能操作对象。但是,静态方法可以访问静态字段。下面是一个静态方法的示例:

public static int advanceId()
{
    int r = nextId; // obtain next available id
    nextId++;
    return r;
}

要调用这个方法,需要提供类名:

int n = Employee.advanceId();

这个方法可以省略关键字static吗?可以。但这样的话就需要通过对Employee类型的对象引用来这个方法。

【注释:可以使用对象调用静态方法,这是合法的。例如,如果harry是一个Employee对象,那么可以调用harry.advaneId()而不是Employee.advance()。不过,这种写法很容易造成混淆,其原因是advanceId方法计算的结果与harry毫无关系。建议使用类名而不是对象来调用静态方法

下面两种情况可以使用静态方法:

·方法不需要访问对象状态(状态指当前对象的实例字段),因为它需要的所有参数都通过显式参数提供(例如Math.pow)。

·方法只需要访问类的静态字段(例如Employee.advanceId)。

C++注释:Java中的静态字段与静态方法在功能上与C++相同。但是,语法稍有所不同。在C++中,要使用 :: 操作符访问作用域之外的静态字段或静态方法,如Math::PI。

术语“静态”有一段不寻常的历史。起初,C引入关键字static是为了表示退出一个块后依然存在的局部变量。在这种情况下,术语“静态”是有意义的:变量一直保留,当再次进入这个块时它依然存在。随后,static在C中有了第二种含义,表示不能从其他文件访问的全局变量和函数。重用关键字static只是为了避免引入一个新的关键字。最后,C++第三次重用了这个关键字,与之前赋予的含义完全无关,它指示属于类而不属于任何特定类对象的变量和函数。这与Java中这个关键字的含义相同。】

工厂方法:

静态方法还有另一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。可以如下得到不同样式的格式化对象:

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类不使用构造器来创建对象呢?有两个原因:

·无法为构造器命名。构造器的名字总是要与类名相同。但是,这里希望有两个不同的名字,分别得到货币实例和百分比实例。

·使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回DecimalFormat类的对象,这是继承NumberFormat的一个子类(继承以后再讲)。[Decimal的意思货币型;双精度;数据类型;十进制小数;十进位制]

 

main方法:

需要指出,可以调用静态方而不需要任何对象。例如,不需要构造Math类的任何对象就可以调用Math.pow。

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

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

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

{Java程序运行时,第一件事情就是试图访问main方法,因为main相等于程序的入口,如果没有main方法,程序将无法启动,main方法更是占一个独立的线程,找到main方法后,是不是就会执行mian方法块里的第一句话呢?答案是不一定

因为静态部分是依赖于类,而不是依赖于对象存在的,所以静态部分的加载优先于对象存在。

  当找到main方法后,因为main方法虽然是一个特殊的静态方法,但是还是静态方法,此时JVM会加载main方法所在的类,试图找到类中其他静态部分,即首先会找main方法所在的类。

 

执行顺序大致分类:

  1.静态属性,静态方法声明,静态块。

  2.动态属性,普通方法声明,构造块。

  3.构造方法。

1.1 静态:

  当加载一个类时,JVM会根据属性的数据类型第一时间赋默认值(一举生成的)。然后再进行静态属性初始化,并为静态属性分配内存空间,静态方法的声明,静态块的加载,没有优先级之分,按出现顺序执行,静态部分仅仅加载一次。至此为止,必要的类都已经加载完毕,对象就可以被创建了。

1.2 普通:

  当new一个对象时,此时会调用构造方法,但是在调用构造方法之前,(此刻1.1已经完成,除非被打断而暂停)执行动态属性定义并设置默认值(一举生成的)。然后动态属性初始化,分配内存,构造块,普通方法声明(只是加载,它不需要初始化,只有调用它时才分配内存,当方法执行完毕后内存立即释放),没有优先级之分,按出现顺序执行。最后进行构造方法中赋值。当再次创建一个对象,不再执行静态部分,仅仅重复执行普通部分。

  注意:如果存在继承关系,创建对象时,依然会首先进行动态属性进行定义并设默认值,然后父类的构造器才会被调用,其他一切都是先父类再子类(因为子类的static初始化可能会依赖于父类成员能否被正确初始化),如果父类还有父类,依次类推,不管你是否打算产生一个该父类的对象,这都是自然发生的。}

【提示:每一个类都可以有一个main方法。这是为类增加演示代码的一个技巧。例如,可以在Employee类添加一个main方法:

class Employee {

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

。。。
    public static void main(String[] args){ // unit test
        var e = new Employee("Romeo", 50000, 2003, 3, 31);
        e.raiseSalary(10);
        System.out.println(e.getName() + " "+e.getSalary());
    }

。。。

}

要看Employee类的演示,只需要执行

java Employee

如果Employee类是一个更大的应用的一部分,那么可以使用以下命令运行这个应用:

java Application

此时,Employee类的main永远不会执行。】

 

java.util.Objects   7

·static <T> void requireNonNull (T obj)

·static <T> void requireNonNull (T obj, String message)

·static <T> void requireNonNull (T obj, Supplier<String> messageSupplier)  8

如果obj是null,这些方法会抛出一个NullPointerException异常而没有任何消息,或者有给定的消息。(第六章会解释如何利用供应者以懒方式得到一个值。第8章会解释<T>语法。)

·static <T> T requireNonNullElse (T obj, T defaultObj)   9

·static <T> T requireNonNullElseGet (T obj, Supplier<T> defaultSupplier)   9

如果obj不为null则返回obj,或者如果obj为null则返回默认对象。

 

方法参数:

首先来回顾在程序设计语言中关于如何将参数传递到方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的变量位置(location)。所以,方法可以修改按引用传递的值,而不能修改按值传递的变量的值。“按.....调用”(call by)是一个标准的计算机科学术语,用来描述各种程序语言(不只是Java)中方法参数的行为(事实上,以前还有一种按名调用(call by name),Algol程序设计语言是最古老的高级语言之一,它就采用了按名调用方式。不过,这种传递方式已经成为历史)。

Java程序设计语言总是按值调用。也就是说,方法会得到所有参数值的一个副本。具体来说,方法不能修改传递给它的任何参数变量的内容。

例如,考虑下面的调用:

不论这个方法具体如何实现,我们知道,在这个方法调用之后,percent的值还是10。

下面再研究一下这种情况。假定一个方法试图将一个参数值增加至3倍:(triple的意思adj.三部分的;三人的;三组的;三倍的;三重的;\v.成为三倍;使增至三倍;\n.三倍的数[量];三个一组;三垒安打;)

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

然后调用这个方法:

不过,这并不起作用。调用这个方法之后,percent的值还是10。下面来看发生了什么:

1. x初始化为percent值的一个副本(也就是10)。

2. x乘以3后等于30,但是percent仍然是10。

3. 这个方法结束之后,参数变量x不再使用。

不过,有两种不同类型的方java基础和对象实例法参数:

·基本数据类型(数字、布尔值)。

·对象引用。

我们已经看到,一个方法不可能修改基本数据类型的参数,而对象参数就不同了,可以很容易地实现一个方法将一个员工的工资增至3倍:

public static void tripleValue(Employee x) // work
{
    x.raiseSalary(200);
}

如下调用这个方法时,

具体的执行过程为:

1. x初始化为harry值的一个副本,这里就是一个对象引用。

2. raiseSalary方法应用于这个对象引用。x和harry同时引用的那个Employee对象的工资提高了200%(也就是提高到三倍)

3. 方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个工资增至3倍的员工对象。

可以看到,实现方法改变对象参数的状态是完全可以的,实际上也相当常见。理由很简单,方法得到的是对象引用的副本,原来的对象引用和这个副本都引用同一个对象

很多程序设计语言(特别是C++和Pascal)提供了两种参数传递方式:按值调用和按引用调用。有些程序员(甚至有些书的作者)生成Java对对象采用的是按引用调用,实际上,这是不对的。由于这种误解很常见,所以很有必要给出一个反例来详细地说明问题。

下面来编写一个交换两个Employee对象的方法:

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

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

var a = new Employee("Alice", ...);
var b = new Employee("Bob", ...);
swap(a, b);
// does a now refer to Bob, b to Alic? Obviously, it doesn't.

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

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

最终,白费力气。方法结束时,参数变量x和y被丢弃了。原来的变量a和b仍然引用这个方法调用之前所引用的对象。

这说明:Java程序设计语言对对象采用的不是按引用调用。实际上,对象引用(object reference)是按值传递的。

下面来总结在Java中对方法参数能做什么和不能做什么:

·方法不能修改基本数据类型的参数(即数值型或布尔型)。

·方法可以改变对象参数的状态。(实例字段)

·方法不能让一个对象参数引用一个新对象。

【C++注释:C++中有按键调用和按引用调用。引用参数标有&符号。例如,可以轻松地实现void tripleValue (double &x)方法或

void swap(Employee &x, Employee &y)方法来修改它们地引用参数。】

 

对象构造

前面已经学习了如何编写简单的构造器来定义对象的初始状态。不过,由于对象构造非常重要,所以Java提供了多种编写构造器的机制。下面几节将详细介绍这些机制。

重载:

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

var message = new StringBuilder();

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

var todoList = new StringBuilder("To do: ");

这种功能叫作重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的方法名但有不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型于特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器无法匹配参数,就会产生编译时出错,这可能因为不存在匹配,或者所有重载方法中没有一个相对更好的方法(这个查找匹配的过程称为重载解析(overloading resolution))。

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

indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(string, int)

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

 

默认字段初始化:

如果在构造器中没有显示地为一个字段设置初始值,就会将它自动设置为默认值:数值将设置为0,布尔值为false,对象引用为null。有些人认为依赖默认值的做法是一种不好的编程实践。确实,如果不明确地对字段进行初始化,就会影响程序代码的可读性。

【注释:这是字段与局部变量的一个重要区别。方法中的局部变量必须明确地初始化。但是在类中,如果没有初始化类中的字段,就会自动初始化为默认值(0、false或null)。】不过在用idea时,如果没有显式的初始化或者没有显式的赋值,会直接产生编译错误。故此说法是否准确还有待考究。

例如,考虑Employee类。假定没有在构造器中指定如何初始化某些字段,默认情况下就会将salary字段初始化为0,将name和hireDay字段初始化为null。(name是String对象、hireDay是LocalDate类的一个子类对象?)

但是,这并不是一个好主意。如果有人调用getName方法或getHireDay方法,就会得到一个null引用,这可能不是他们想要的结果:

 

无参数的构造器:

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

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

如果你写的类没有构造器,就会为你提供一个无参数构造器。这个构造器将所有的实例字段设置为相应的默认值。所以,实例字段中的所有数值型数据会设置为0,所有布尔值设置为false,所有对象变量将设置为null。(在本机中并没有提供,反而报告了错误。其是否有效有待考究。)

如果类中提供了至少一个构造器,但是没有提供无参数构造器,那么构造对象时就必须提供参数,否则就是不合法的。例如,先前Employee提供了一个构造器:

public Employee(String n, double s, int year, int month, int day)

对于这个类,构造默认的员工就是不合法的。也就是说,以下调用:

e = new Employee();

将产生错误。

【警告:请记住,仅当类没有任何其他构造器的时候,你才会得到一个默认无参数构造器。编写类的时候,如果写了一个你自己的构造器,要想让这个类的使用者能够通过以下调用创建一个实例:

new ClassName()

你就必须提供一个无参数的构造器。当然,如果接受所有字段设置为默认值,则只需要提供以下代码:(本机未验证此说法,该内容有待考究。)

public ClassName()
{
}

【C++注释:C++对于构造字段有一种特殊的初始化器列表语法,如下所示:

Employee::Employee(String n , double s, int y, int m, int d) // C++
: name(n),
  salary(s),
  hireDay(y, m, d)
{
}

C++使用这种特殊语法来避免不必要地调用无参数构造器。在Java中,不需要这种语法,因为对象没有子对象,只有其它对象的引用。】

 

显式字段初始化:

通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。不论调用哪个构造器,每个实例字段都要设置为一个有意义的初始值,确保这一点总是一个好主意。

可以在类定义中直接为任何字段赋值。例如:

class Employee {
     private String name = "unknown";
     . . .
}

在执行构造器之前完成这个赋值。如果一个类的所有构造器都需要把某个特定的实例字段设置为同一个值,那么这个语法尤其有用。

初始化不一定是常量值。在下面的例子中,就是利用方法调用初始化一个字段。考虑以下Employee类,其中每个员工有一个id字段。可以使用以下方式进行初始化:

class Employee {
     private static int nextId = 1;
     private int id = getNextId();
     . . .
     public static int getNextId(){
          int num = nextId;
          nextId++;
          return num;
      }

 

 

 

参数名:

在编写很小的构造器时(这十分常见),在为参数命名时可能有些困惑。

我们通常喜欢用单个字母作为参数名:

public Employee(String n, double s) {
     name = n;
     salary = s;
}

但这样做有一个缺点:只有阅读代码才能够了解参数n和参数s的含义。

有些程序员在每个参数前面加上一个前缀“a”:

public Employee(String aName, double aSalary) {
     name = aName;
     salary = aSalary;
}

这样更好一些。读者一眼就能够看懂参数的含义。

还有一种常用的技巧,它基于这样的事实:参数变量会遮蔽同名的实例字段。例如,如果将参数命名为salary,那么salary将指示这个参数,而不是实例字段。但是,还可以用this.salary访问实例字段。回想一下,this指示隐式参数,也就是所构造的对象。下面来看一个示例:

public Employee(String Name, double Salary) {
     this.name = aName;
     this.salary = aSalary;
}

【C++注释:在C++中,经常用下画线或某个固定的字母(一般选用m或x)作为实例字段的前缀。例如,salary字段可能被命名为_salary、mSalary或xSalary。Java程序员通常不这样做。】

 

调用另一个构造器:

关键字this指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。

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

public Employee(double salary){
    // calls Employee(String, double)
    this("Employee #" + nextId, salary);
}
public Employee(String name, double salary){
    this.name = name;
    this.salary = salary;
    id = nextId;
    nextId++;
    hireDay = LocalDate.now();
}

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

采用这种方式使用this关键字非常有用,这样只需要写一次公共构造代码。

【C++注释:在Java中,this引用等价于C++中的this指针。但是,在C++中,一个构造器不能调用另一个构造器。如果在C++中想抽取出公共的初始化代码,则必须编写一个单独的方法。】

 

初始化块:

前面已经介绍过两种初始化实例字段的方法:

·在构造器中设置值;

·在声明中赋值。

实际上,Java还有第三种机制,成为初始化块(initialization block)。在一个类的声明中,可以包含任意的代码块。构造这个类的对象时,这些块就会执行。例如,

class Employee
{
    private static int nextId;

    private int id;
    private String name;
    private double salary;

    // object initialization block
    {
        id = nextId;
        nextId++;
    }
}

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

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

【注释:可以在初始化块中设置字段,即使这些字段在类后面才定义,这是合法的。但是,为了避免循环定义,不允许读取在后面初始化的字段。具体规则请参见Java语言规范的8.3.3节(http://docs.oracle.com/javase/specs)。这些规则太过复杂,让编译器的实现者都很头疼,所以较早的Java版本中这些规则的实现存在一些小错误。因此,建议总是将初始化块放在字段定义之后。】

由于初始化实例字段有多种途径,所以列出构造过程的路径可能让人很费解。下面是调用构造器时的具体处理步骤:

1. 如果构造器的第一行调用了另一个构造器。则基于所提供的参数执行第二个构造器。

2. 否则,

a) 所有的实例字段初始化为默认值(0、false或null)。

b) 按照在类声明中出现的顺序,执行所有初始化方法和初始化块。

3. 执行构造器主体代码。

当然,最好精心地组织初始化代码,以便其他程序员轻松理解,而不要求他们都是语言专家。例如,如果让类的构造器依赖于实例字段声明的顺序,那就会显得很奇怪并且容易引起错误。(不过好像这样子弄不了。可能只是举一个例子。)

可以通过提供一个初始值,或者使用一个静态的初始化块来初始化静态字段,前面已经介绍过第一种机制:

private static int nextId = 1;

如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块。

将代码放在一个块中,并标记关键字static。下面是一个示例。我们希望将员工ID的起始值赋为一个小于10000的随机整数。

private static Random generator = new Random();
// static initialization block
static
{
    nextId = generator.nextInt(10000);
}

在类第一次加载的时候,会完成静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值为0、false或null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。

【注释:让人惊讶的是,在JDK 6之前,完全可以用Java编写一个没有main方法的“Hello, World”程序。

public class Hello
{
    static
    {
        System.out.println("Hello, World");
    }
}

当用java Hello调用这个类时,就会加载这个类,静态初始化将会打印“Hello, World”。在此之后才会显示一个消息指出main未定义。从Java 7之后,java程序会首先检查是否有一个main方法。】

这个例子使用了Random类来生成随机数。从JDK 17开始,java.util.random包提供了考虑多种因素的强算法的实现。阅读java.util.random包的API文档,其中对如何选择算法给出了建议。然后通过提供算法名来得到一个实例,如下所示:

RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");

调用generator.nextInt(n)或其他RandomgGenerator方法来生成随机数。(RandomGenerator是一个接口,接口以后会讲,Random类的对象可以使用所有RandomGenerator方法。)

 

java.util.Random  1.0

·Random()

构造一个新的随机数生成器。

java.util.random.RandomGenerator  17

·int nextInt(int n)

返回一个0~n-1之间的随机数。

·static RandomGenerator of(String name)

由给定算法名生成一个随机数生成器。算法“LX28MixRandom”对大多数应用都适用。

 

对象析构与fianlize方法

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

当然,某些对象使用了内存之外的其他资源,例如,文件或使用系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用就十分重要

如果一个资源一旦使用完就需要立即关闭,那么应当提供一个close方法来完成必要的清理工作。可以在对象使用完时调用这个close方法。(以后会讲如何确保自动调用这个方法。)

如果可以等到虚拟机退出,那么可以用方法Runtime.addShutdownHook增加一个“关闭钩”(shutdown hook)。在Java 9中,可以使用Cleaner类注册一个动作,当对象不再可达时(除了清洁器还能访问,其他对象都无法访问这个对象),就会完成这个动作。在实际中这些情况很少见。可以参见API文档来了解这两种方法的详细内容。

【警告:不要使用fianlize方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃。】

 

记录:

有时,数据就只是数据,而面向对象程序设计提供的数据隐藏有些碍事。考虑一个类Point,这个类描述平面上的一个点,有x和y坐标。

当然,可以如下创建一个类:

class Point
{
    private final double x;
    private final double y;
    public Point(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
    public getX()
    {
        return x;
    }
    public getY()
    {
        return y;
    }
    public String toString()
    {
        return "Point[x = %d, y = %d]".formatted(x, y);
    }
    // More methods. . .
}

这里隐藏了x和y,然后通过获取方法来获得这些值,不过,这种做法对我们确实有好处吗?

我们将来想改变Point的实现吗?当然,还有极坐标,不过对于图形API,你可能不会使用极坐标。在实际中,平面上的一个就用x和y坐标来描述。

为了更简洁地定义这些类,JDK14引用了一个大预览特性:“记录”。最终版本在JDK16中发布。

 

记录概念:

记录(record)是一种特殊形式的类,其状态不可变,而且公共可读。可以如下将Point定义为一个记录:

record Point(double x, double y){}

其结果是有以下实例字段的类:

在Java语言规范中,一个记录的实例字段成为组件(component)。

这个类有一个构造器:

Point(double x, double y)

和以下访问器方法:

注意,访问器方法名为x和y,而不是getX和getY。(Java中实例字段可以与方法同名,这是合法的。)

【注释:Java没有遵循get约定,因为那有些麻烦。对于布尔字段,通常使用is而不是get。而且首字母大写可能有问题。如果一个类既有x字段又有X字段,会发生什么?有些程序员不太满意,因为他们原先的类不能轻松地变为记录。不过实际上,那些遗留类中,很多都是可变的,所以并不适合转换为记录。】

除了字段访问器方法,每个记录有3个自动定义的方法:toString、equals和hashCode。下一章会更多地了解这些方法。

【警告:对于这些自动提供的方法,也可以定义你自己的版本,只要它们有相同的参数和返回类型。李儒,下面的定义就是合法的:

ecord Point(double x, double y)
{
    public double x() { return y;} // BAD
}

不过,这并不是一个好主意。】

可以为一个记录增加你自己的方法:

record Point(double x, double y)
{
    public double distanceFromOrigin()
    {
        return Math.hypot(x, y);
    }
}

与所有其它类一样,记录可以有静态字段和方法:

record Point(double x, double y)
{
    public static Pont ORIGIN = new Point(0, 0);
    public static double distance(Point p, Point q)
    {
        return Math.hypot(p.x - q.x, p.y - q.y);
    }
    . . .
}

不过不能为记录增加实例字段:

record Point(double x, double y)
{
    private double r; // ERROR
    . . .
}

【警告:记录的实例字段自动为final字段。不过,它们可能是可变对象的引用。

record PointInTime(double x, double y, Date when) {}

这样记录实例将是可变的:

如果希望记录实例是不可变的,那么字段就不能使用可变的类型。】

【提示:对于完全由一组变量表示的不可变数据,要使用记录而不是类。如果数据是可变的,或者数据表示可能随时间改变,则使用类。记录更易读、更高效,而且在并发程序中更安全。】

构造器:标准、自定义和简洁:

自动定义地设置所有实例字段地构造器称为标准构造器(canonical constrctor)。

还可以定义另外的自定义构造器(custom constructor)。这种构造器的第一个语句必须调用另一个构造器,所以最终会调用标准构造器。下面来看一个例子:

record Point(double x, double y)
{
    public Point() { this(0, 0); }
}

这个记录有两个构造器:标准构造器和一个生成原点的无参数构造器。

如果标准构造器需要完成额外的工作,那么可以提供你自己的实现:

record Range(int from, int to)
{
    public Range(int from, int to)
    {
        if (from <= to)
        {
            this.from = from;
            this.to = to;
        }
        else
        {
            this.from = to;
            this.to = from;
        }
    }
}

不过,实现标准构造器时,建议使用一种简洁(compact)形式。不用指定参数列表:

record Range(int from, int to)
{
    public Range // Compact form
    {
        if (from > to) // Swap the bounds
        {
            int temp = from;
            from = to;
            to = temp;
       }
    }
}

简洁形式的主体是标准构造器的“前奏”它只是在为实例字段this.from和this.to赋值之前修改参数变量from和to不能在简洁构造器的主体中读取或修改实例字段

 

包:

java允许使用包(package)将类组织在一个集合中。借助包可以方便地组织你的代码并将你自己的代码与其他人提供的代码库分开。下面我们将介绍如何使用和创建包。

包名:

使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地提供了Employee类,只要他们将自己的类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,可以使用一个intel net(因特网)域名(这显然是唯一的)以逆序的形式作为包名,然后对于不同的项目使用不同的子包。例如,考虑域名horstmann.com。如果逆序来写,就得到了包名com.horstmann。然后可以追加一个项目名,如com.horstmann.corejava。如果再把Employee类放在这个包里,那么这个类的“完全限定”名就是com.horstmann.corejava.Employee。

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

 

类的导入:

一个类可以使用所属包(这个类所在的包)中的所有类,以及其他包中的公共类(public class)。

我们可以采用两个方式访问另一个包中的公共类。第一种方式是使用完全限定名(fully qualified name),也就是包名后面跟着类名。例如:

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

这显然很繁琐。更简单且更常用的方式是使用import语句。import语句的关键是可以提供一种简写方式来引用包中各个类。一旦增加了import语句,在使用类时,就不必写出类的全名了。

可以使用import语句导入一个特定的类或整个包。Import语句应该位于源文件的顶部(但位于package语句的后面)。例如,可以使用下面这条语句导入java.time包中的所有类。

import java.time.*;

然后,就可以使用:

LocalDate today = LocalDate.now();

而不需要在前面加上包前缀。

还可以导入一个包中的特定类:

import java.time.LocalDate;

java.time.*的语法比较简单,对代码的规模也没有任何负面影响。不过,如果能够明确地指出所导入地类,那么代码的读者就能够更加准确地知道你使用了哪些类。

【提示:在Eclipse中,可以使用菜单选项Source—>Organize Imports。诸如import java.util.*;等包语句将会自动扩展为一组特定的导入语句,如:

这是一个十分便捷的特性。】

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

在大多数情况下,可以只导入你需要的包,并无须过多考虑。但在发生命名冲突的时候,就要注意包了。例如,java.util和java.sql包都有Date类。假设在程序种导入了这两个包:

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

Date today; // ERROR--java.util.Date or java.sql.Date?

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

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

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

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

【C++注释:C++程序员有时会将import与#include弄混。实际上,这两者之间并没有共同之处。在C++中,必须使用#include来包含外部特性的声明,这是因为,除了正在编译的文件以及显式包含的头文件,C++编译器不会查看任何其他文件。Java编译器则不同,只要你告诉它文件在哪里,它很乐于查看其他文件。

在Java中,通过显式地给出完整地类名,如java.util.Date,可以完全避免使用import机制;而在C++中,则无法避免使用#include指令。

import语句唯一的好处是简捷。可以使用简短的名字而不是完整的包名来引用一个类。例如,在import java.util.*(或import java.util.Date)语句之后,可以只用Date来引用java.util.Date类。

在C++中,与包机制类似的是命名空间(namespace)特性。可以认为Java中的package和import语句类似于C++中的namespace和using指令。】

 

静态导入:

有一种import语句允许导入静态方法和静态字段,而不只是类。

例如,如果在源文件最上面添加一条指令:

import static java.lang.System.*;

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

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

import static java.lang.System.out;

实际上,是否有很多程序员想要用简写System.out或System.exit,这一点很让人怀疑。这样写出的diamagnetic看起来不太清晰。不过,

sqrt(pow(x, 2) + pow(y, 2))

看起来则比

Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))

简洁得多。

 

包访问:

前面已经见过访问修饰符pubic和private。标记为public的部分可以由任意类使用;标记为private的部分只能由定义它们的类使用。如果没有指定public或private,这个部分(类、方法或变量)可以由同一个包中的所有方法访问。(大概是默认之类的)

下面再来一个例子,一个程序中,并没有将Employee类定义为公共类,因此只有在通过一个包(在此指无名包)中的其他类(例如EmployeeTest)可以访问这个类。对于类来说,这种默认方式是合乎情理的。但是,对于变量来说就有些不适宜了,变量必须显式地标记为private,不然的话就默认为包可访问。显然,这样会破坏封装性。问题是人们经常忘记键入关键字private。以java.awt包中的Window类为例(java.awt包是JDK提供的源代码的一部分):

public class Window extends Container{
    String warningString;
    ...
}

请注意,这里的warningString变量不是private!这意味这java.awt包中的所有类的方法都可以访问该变量,并将它设置为任意值(例如,”Trust me!”)。实际上,只有Window类的方法访问这个变量,因此本应该将它设置为私有变量才合适。可能是程序员敲代码时匆忙之中忘记private修饰符了?也可能没有人关心这个问题?不仅如此,这个类还陆续增加了一些新的字段,而其中大约又一半也不是私有的。

这可能会成为一个问题。在默认情况下,包不是封闭的实体。也就是说,任何人都可以向包中添加更多的类。当然,有恶意或糟糕的程序员很可能利用包访问添加一些能修改变量的代码。例如,在Java程序设计语言的早期版本中,只需要将以下这条语句房子啊类文件的开头,就可以很容易地在java.awt包中混入其他类:

Package java.awt;

然后,把得到的类文件放置在类路径上某处的java/awt子目录下,这样就可以修改警告字符串。

从1.2版开始,JDK的实现者修改了类加载器,明确地禁止加载包名以“java.”开头的用户自定义的类!当然,用户自定义的类无法从这种保护中受益。另一种机制是让JAR文件声明包为密封的(sealed),以防止第三方修改,但这种机制已经过时。现在应当使用模块封装包。(卷II第九章会讲模块)。

  • 上一篇: 有vf做基础学java
  • 下一篇: java script零基础
  • 版权声明


    相关文章:

  • 有vf做基础学java2024-11-10 17:42:02
  • 粉笔java零基础就业2024-11-10 17:42:02
  • springboot要java基础吗2024-11-10 17:42:02
  • java版基础按键教程2024-11-10 17:42:02
  • java的基础代码做动漫人物2024-11-10 17:42:02
  • java script零基础2024-11-10 17:42:02
  • 导入java基础包出错2024-11-10 17:42:02
  • java基础都有什么2024-11-10 17:42:02
  • java web 基础 面试2024-11-10 17:42:02
  • java基础综合练习2024-11-10 17:42:02