java核心技术卷I 第4-5章

第四章 对象于类

这一章将主要介绍如下内容:

● 面向对象程序设计

● 如何创建标准Java类库中的类对象

● 如何编写自己的类

4.1 面向对象程序设计概述

  • 面向对象程序设计(简称OOP)是当今主流的程序设计范型,它已经取代了20世纪70年代的“结构化”过程化程序设计开发技术。Java是完全面向对象的,必须熟悉OOP才能够编写Java程序。

  • 面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和**隐藏的实现部分。**程序中的很多对象来自标准库,还有一些是自定义的。究竟是自己构造对象,还是从外界购买对象完全取决于开发项目的预算和时间。

  • 从根本上说,只要对象能够满足要求,就不必关心其功能的具体实现过程。在OOP中,不必关心对象的具体实现,只要能够满足用户的需求即可

  • 传统的结构化程序设计首先要确定如何操作数据,然后再决定如何组织数据,以便于数据操作。而OOP却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法。

  • 对于一些规模较小的问题,将其分解为过程的开发方式比较理想。**而面向对象更加适用于解决规模较大的问题。**后者更易于程序员掌握,也容易找到bug。假设给定对象的数据出错了,在访问过这个数据项的方法中查找错误要比在过程中查找容易得多。

4.1.1 类

  • 类(class)是构造对象的模板或蓝图
  • 由类构造(construct)对象的过程称为创建类的实例(instance)。
  • 要在Java程序中创建一些自己的类,以便描述应用程序所对应的问题域中的对象。
  • 封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。
  • 对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值这些值的集合就是这个对象的当前状态(state)。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。\
  • 实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他对象就不会知道或介意所发生的变化。
  • OOP的另一个原则会让用户自定义Java类变得轻而易举,这就是:可以通过扩展一个类来建立另外一个新的类。事实上,在Java中,所有的类都源自于一个“神通广大的超类”,它就是Object。
  • 在扩展一个已有的类时,这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中,只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立另外一个类的过程称为继承(inheritance),

4.1.2 对象

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

    ● 对象的行为(behavior)——可以对对象施加哪些操作,或可以对对象施加哪些方法?

    ● 对象的状态(state)——当施加那些方法时,对象如何响应

    ● 对象标识(identity)——如何辨别具有相同行为与状态的不同对象?

    同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性**。对象的行为是用可调用的方法**定义的。

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

  • 但是,对象的状态不能完全描述一个对象。每个对象都有一个唯一的身份(identity)。例如,在一个订单处理系统中,任何两个订单都存在着不同之处,即使所订购的货物完全相同也是如此。需要注意,作为一个类的实例,每个对象的标识永远是不同的,状态常常也存在着差异。

  • **对象的这些关键特性在彼此之间相互影响着。**例如,对象的状态影响它的行为(如果一个订单“已送货”或“已付款”,就应该拒绝调用具有增删订单中订单项的方法。反过来,如果订单是“空的”,即还没有加入预订的物品,这个订单就不应该进入“已送货”状态)

4.1.3 设计类

  • 传统的过程化程序设计,必须从顶部的main函数开始编写程序。在面向对象程序设计时没有所谓的“顶部”。对于学习OOP的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。
  • 设计类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
    • 例如,在订单处理系统中,有这样一些名词:
    • ● 商品(Item)
    • ● 订单(Order)
    • ● 送货地址(Shipping address)
    • ● 付款(Payment)
    • ● 账户(Account)
    • 这些名词很可能成为类Item、Order等。
  • 接下来,查看动词:商品被添加到订单中,订单被发送或取消,订单货款被支付。对于每一个动词如:“添加”、“发送”、“取消”以及“支付”,都要标识出主要负责完成相应动作的对象。例如,当一个新的商品添加到订单中时,那个订单对象就是被指定的对象,因为它知道如何存储商品以及如何对商品进行排序。也就是说,add应该是Order类的一个方法,而Item对象是一个参数。
  • 当然,所谓“找名词与动词”原则只是一种经验,在创建类的时候,哪些名词和动词是重要的完全取决于个人的开发经验。

4.1.4 类之间的关系

  • 在类之间,最常见的关系有

    • ● 依赖(“uses-a”)
    • ● 聚合(“has-a”)
    • ● 继承(“is-a”)
  • 依赖(dependence),即“uses-a”关系,是一种最明显的、最常见的关系。如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖的类减至最少。如果类A不知道B的存在,它就不会关心B的任何改变(这意味着B的改变不会导致A产生任何bug)。用软件工程的术语来说,就是让类之间的耦合度最小。

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

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

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

4.2 使用预定义类

  • 在Java中,没有类就无法做任何事情,我们前面曾经接触过几个类。然而,并不是所有的类都具有面向对象特征。例如,Math类。Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。

4.2.1 对象与对象变量

  • 对象引用(对象变量):Demo demo//在栈内存中创建对象引用

    引用类型表示某个类型的变量,其值一般为某个地址值。

    返回类型包括了基本类型和引用类型

  • 对象:new Demo() //在堆内存中创建对象

  • 对象引用指向(引用)对象:Demo demo = new Demo();

  • 对象引用可以不指向对象,一个对象可被多个对象引用引用。

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

  • 在Java程序设计语言中,使用构造器(constructor)构造新实例**。构造器是一种特殊的方法,用来构造并初始化对象。**

    • 构造器的名字应该与类名相同。因此Date类的构造器名为Date。要想构造一个Date对象,需要在构造器前面加上new操作符,如下所示:

      new Date() 这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。

    • 为了构造的对象可以多次使用,因此,需要将对象存放在一个变量中:

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

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

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

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

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

  • 一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。 new操作符的返回值也是一个引用。

  • 下列语句:

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

  • 可以显式地将对象变量设置为null,表明这个**对象变量目前没有引用任何对象。**如果将一个方法应用于一个值为null的对象上,那么就会产生运行时错误。

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

  • 可以将Java的对象变量看作C++的对象指针

  • 所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。

4.2.2 Java类库中的LocalDate类

  • 在前面的例子中,已经使用了Java标准类库中的Date类。Date类的实例有一个状态,即特定的时间点。

  • 时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。UTC是Coordinated Universal Time的缩写,与大家熟悉的GMT(即Greenwich Mean Time,格林威治时间)一样,是一种具有实践意义的科学标准时间。

  • Date类所提供的日期处理并没有太大的用途。Java类库的设计者认为:像“December 31, 1999, 23:59:59”这样的日期表示法只是阳历的固有习惯。但是,同一时间点采用中国的农历表示和采用希伯来的阴历表示就很不一样,对于火星历来说就更不可想象了。

  • 所以类库设计者决定将保存时间与给时间点命名(时间对应的日历日期 如农历日期 阳历日期等)分开。所以标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用来**表示大家熟悉的日历表示法的LocalDate类。Java SE 8引入了另外一些类来处理日期和时间的不同方面——有关内容参见卷Ⅱ第6章。将时间与日历(或日期)**分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同的概念。

  • LocalDate类封装了实例域来维护所设置的日期。不要使用构造器来构造LocalDate类的对象。实际上,应当使用静态工厂方法(factory method)代表你调用构造器。 LocalDate.now() 会构造一个新对象,表示构造这个对象时的日期。

    可以提供年、月和日来构造对应一个特定日期的对象: LocalDate.of(1999,12,31)

    当然,通常都希望将构造的对象保存在一个对象变量中:

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

  • plusDays方法会得到一个新的LocalDate,如果把应用这个方法的对象称为当前对象,这个新日期对象则是距当前对象指定天数的一个新日期:

    LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);

    plusDays方法会生成一个新的LocalDate对象,然后把这个新对象赋给aThousandDaysLater变量。原来的对象不做任何改动。

  • 实际上,Date类还有getDay、getMonth以及getYear等方法,**然而并不推荐使用这些方法。**当类库设计者意识到某个方法不应该存在时,就把它标记为不鼓励使用。

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

更改器方法:

  • 调用这个方法后,对象的状态会改变,如GregorianCalendar.add方法
  • 与之相对的是没有更改调用这个方法的对象,例子是上一节中的plusDays方法,plusDays方法会生成一个新的LocalDate对象,然后把这个新对象赋给aThousandDaysLater变量原来的对象不做任何改动。

访问器方法:

  • 只访问对象而不修改对象的方法有时称为访问器方法,如getXxx()…

java.time.LocalDate 8 API

● static LocalTime now()

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

● static LocalTime of(int year, int month, int day)

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

● int getYear()

● int getMonthValue()

● int getDayOfMonth()

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

● DayOfWeek getDayOfWeek()

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

● LocalDate plusDays(int n)

● LocalDate minusDays(int n)

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

4.3 用户自定义类

  • 现在开始学习如何设计复杂应用程序所需要的各种主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例域和实例方法。要想创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。

  • 在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。

  • 源文件名必须与public类的名字相匹配。

  • 源文件有多少类,编译源文件时将创建多少个对应的类文件。

4.3.2 多个源文件的使用

  • 有两种编译源程序的方法。一种是使用通配符调用Java编译器,一种直接编译main函数所在文件

  • 第一种中所有与通配符匹配的源文件都将被编译成类文件。
  • 第二种中java编译器发现该源文件中使用了其他类,会先查找class文件,没有则自动搜索并编译java文件。更重要的是,如果java源文件的版本较对应的class文件版本新,Java编译器就会自动地重新编译这个文件。

4.3.3 剖析Employee类

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

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

  • Employee类的实例中有三个实例域用来存放将要操作的数据:

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

  • 可以用public标记实例域,但这是一种极为不提倡的做法。public数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。

4.3.4 从构造器开始

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

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

  • ● 构造器与类同名

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

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

    构造器没有返回值

    ● 构造器总是伴随着new操作一起调用

  • 警告:

    • 不要在构造器中定义与实例域重名的局部变量
    • 这个构造器声明了局部变量name和salary。这些变量只能在构造器内部访问。这些变量屏蔽了同名的实例域
    • 必须注意在所有的方法中不要命名与实例域同名的变量。(可用this关键字指明当前类对象实例域)

4.3.5 隐式参数与显式参数

  • 方法调用: employee.raiseSalary(5);

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

  • 在每一个方法中,关键字this表示隐式参数。

  • 在Java中,所有的方法都必须在类的内部定义

4.3.6 封装的优点

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

  • 这些都是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。

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

    • 一个私有的数据域
    • 一个公有的域访问器方法;
    • 一个公有的域更改器方法。
  • 更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。

  • 警告:

    • 不要编写返回引用可变对象的访问器方法。

    • 在Employee类中就违反了这个设计原则,其中的getHireDay方法返回了一个Date类对象:

  • Date类有一个更改器方法setTime,可以在这里设置毫秒数。Date对象是可变的(对象中的数据域等可改变),这一点就破坏了封装性!请看下面这段代码:

    出错的原因很微妙。d和harry.hireDay引用同一个对象(请参见图4-5)。对d调用更改器方法就可以自动地改变这个雇员对象的私有状态! 对被封装的类实例来说其数据域不是私有的了。

  • 如果需要返回一个可变对象的引用应该首先对它进行克隆(clone)。对象clone是指存放在另一个位置上的对象副本

  • 如果需要返回一个可变数据域的拷贝,就应该使用clone。

  • 可变数据类型 :当该数据类型的对应变量中的值发生了改变,那么它对应的内存地址不发生改变,大部分引用类型(除String外)就是可变数据类型

  • 不可变数据类型:当该数据类型的对应变量的值发生了改变,那么它对应的内存地址也会发生改变,基本数据类型

4.3.7基于类的访问权限

  • 方法可以访问所调用对象的私有数据

4.3.8 私有方法

  • 通常,这些方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private的。
  • 只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能依赖它。

4.3.9 final实例域

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

  • final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域

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

    在Employee构造器中会初始化为

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

4.4 静态域与静态方法

4.4.1 静态域

  • 如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。
  • 静态域属于类,而不属于任何独立的对象,被对象共用。

4.4.2 静态常量

  • 静态变量使用得比较少,但静态常量却使用得比较多。可通过类名直接调用常量,无需创建对象。

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

  • 由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为public。然而,公有常量(即final域)却没问题,其不可再赋值。

4.4.3 静态方法

  • 静态方法是一种不能向对象(当前类对象)实施操作的方法。换句话说,没有隐式的参数。可以认为静态方法是没有this参数的方法
  • 不能访问实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。
  • 在下面两种情况下使用静态方法:
    • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
    • 一个方法只需要访问类的静态域(例如:Employee.getNextId)。

4.4.4 工厂方法

  • 使用静态工厂方法(factory method)来构造对象。类似LocalDate和NumberFormat。

  • 静态工厂方法一般用来构建不同状态的对象。

    • LocalDate now = LocalDate.now();//当前时间
      LocalDate of = LocalDate.of(2020, 11, 11);//具体某个时间
      
  • 相比于构造器,工厂方法可以改变所构造的对象类型,如返回子类。

4.4.5 main方法

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

4.5 方法参数

  • 参数传递给方法(或函数)的一些专业术语。
  • 按值调用(callby value)表示方法接收的是调用者提供的值,也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
  • 引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
  • Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
  • 一个方法可以修改引用调用中引用中对应的值,不可以修改按值调用所对应的变量值。
  • 方法参数共有两种类型:
    • 基本数据类型(数字、布尔值)。一个方法不可能修改一个基本数据类型的参数。
    • 对象引用。对象引用拷贝后,原对象引用依然不变故也为按值调用。
  • 下面总结一下Java中方法参数的使用情况:
    • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
    • 一个方法可以改变一个对象参数的状态(原对象引用不变)
    • 一个方法不能让对象参数引用一个新的对象。

4.6 对象构造

  • Java提供了多种编写构造器的机制

4.6.1 重载

  • 类可以有多个构造器,这种特征叫做重载(overloading)。如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载
  • 重载解析(overloading resolution):编译器通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。
  • 方法的签名:方法名以及参数类型,用来完整地描述一个方法。
  • 返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。

4.6.2 默认域初始化

  • 如果在构造器中没有显式地给域赋予初值(且域未在构造器执行前初始化),那么就会被**自动地赋为默认值:**数值为0、布尔值为false、对象引用为null。只有缺少程序设计经验的人才会这样做。
  • 域可以不初始化,局部变量一定要明确地初始化。

4.6.3 无参数的构造器

  • 如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。即域的默认初始化
  • 警告:仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。

4.6.4 显式域初始化

  • 可以在构造器中对实例域进行相应初始化,即设置实例域状态。
  • 也可以在类定义中,直接将一个值赋给实例域,即在构造器执行前,先进行赋值操作,常用来把相同的值赋予某个实例域。即在声明中赋值。

4.6.5 参数名

  • 常用方法: 参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为salary, salary将引用这个参数,而不是实例域。但是,可以采用this. salary的形式访问实例域。关键字this引用方法的隐式参数。

4.6.6 调用另一个构造器

  • 如果构造器中的第一个语句为this(…)语句,将调用同一个类的另一个构造器。

4.6.7 初始化块

  • 初始化块(即代码块)可用来初始化数据域

  • 无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。**首先运行初始化块,然后才运行构造器的主体部分。**同时建议将初始化块放在域定义之后。

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

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

  • 类加载时,静态域就初始化。实例域需创建对象时才初始化。域除非有设置值,否则有默认的初始值0、false或null。

  • 静态初始化语句以及静态代码块执行顺序为在类中定义的顺序。

调用构造器后执行顺序:

1)所有数据域被初始化为默认值(0、false或null)。

2)按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。3)如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。

4)执行这个构造器的主体。

java.util.Random 1.0 API

● Random( )

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

● int nextInt(int n) 1.2

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

4.6.8 对象析构与finalize方法

  • 在析构器中,最常见的操作是回收分配给对象的存储空间。由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。
  • 可以为任何一个类添加finalize方法finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
  • 如果某个资源需要在使用完毕后立刻被关闭,那么就需要由人工来管理。**对象用完时,可以应用一个close方法来完成相应的清理操作。**7.2.5节会介绍如何确保这个方法自动得到调用。

4.7 包

  • Java允许使用包(package)将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
  • 标准的Java包具有一个层次结构
  • 使用包的主要原因是确保类名的唯一性,包内类名唯一。
  • 包内可以划分子包,每个都有独立的类集。

4.7.1 类的导入

  • 一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)。
  • 两种方式访问另一个包中的公有类。
    • 给每个类名前添加完整的包名
    • 使用import语句导入一个特定的类或者整个包
    • import语句是一种引用包含在包中的类的简明描述。import语句应该位于源文件的顶部(但位于package语句的后面)
  • 只能使用星号( * )导入一个包,而不能使用import java.*或import java. * .*导入以java为前缀的所有包。
  • java.lang包被默认导入
  • 引用不同包中的具有相同类名的类时,需在该类名前面加上完整包名。
  • 在包中定位类是编译器(compiler)的工作。类文件中的字节码肯定使用完整的包名来引用其他类。

4.7.2 静态导入

  • import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
  • 例如:就可以使用System类的静态方法和静态域,这个方法可不加类名前缀。
  • 另外,还可以导入特定的方法或域:这个方法也可不加类名前缀。

4.7.3 将类放入包中

  • 将类放入包中必须将包的名字放在源文件的开头

  • 如果没有在源文件中放置package语句,这个源文件中的类就被放置在一个默认包(defaulf package)中。

  • 将包中的文件放到与完整的包名匹配的子目录中,编译器将类文件也放在相同的目录结构中。换句话说,目录结构如下所示:

  • 需要注意,编译器对文件(带有文件分隔符和扩展名.java的文件)进行操作。

  • 而Java解释器加载类(带有.分隔符)。

  • 警告:编译器编译源文件时不检查目录结构,即不检查package子句,即使源文件不在对应的子目录下,也可以编译,不依赖其他包的情况下,不会出现编译错误,如果依赖其他包,此时无法找到其他包。但最终的程序无法运行,因为运行类时,源文件中所定义的包与当前目录不匹配,虚拟机找不到类。

4.7.4 包作用域

  • 标记为public的部分(类,方法,属性)可以被任意类使用
  • 标记为private的部分只能被定义他们的类使用。
  • 如果没有被标记为public或private,这个部分可以被同一包下的所有类访问。
  • 变量必须标记为private,不然为包可见,破坏了封装性。

4.8 类路径

  • 类存储在文件系统的子目录下即存储在对应包下。类的路径必须与包名匹配

  • 类文件也可以归档在JAR(java归档)文件中,jar文件包含多个压缩形式的目录结构和类文件。

  • JAR文件使用ZIP格式组织文件和子目录。可以使用zip程序查看。

  • 类能够被多个程序共享,需做到以下几点:

    • 先设置包树状结构的基目录:如/home/user/classdir

    • 以基目录为基础上构建包以及源文件:如/home/user/classdir/com/liu/corejava

    • 引入所需要的类库,将JAR文件放在一个目录中,如/home/user/archives 一般放在基目录的上级某个目录下。

    • 设置类路径(引入的类库和当前项目的)

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

      unix环境下 不同项目的类路径用冒号(:)分隔,windows用分号( ; )

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

  • 类路径所列出的目录和归档文件是搜寻类的起始点。

    • 假如有 java com.horstmann.corejava.Employee命令,则有以下执行顺序:

      • 首先要查看存储在jre/lib和jre/lib/ext目录下的归档文件中所存放的系统类文件,查找相应的类文件。显然找不到
      • 然后在/home/user/classdir/com/horstmann/corejava/Employee.class 查找
      • 再找不到就com/horstmann/corejava/Employee.class从当前目录开始
      • 最后/home/user/archives/archive.jar内部找com/horstmann/corejava/Employee.class
    • 警告:虚拟机不会递归查找class文件。
  • javac编译器搜索的是文件路径,和环境变量classpath无关。

    但在编译有引用其他类的文件时,会自动从类路径下(类路径加包路径)搜索引用的class文件,**搜索到才会编译成功。**如假定源文件包含指令:并引用了

    四个包路径(当前包,java.util.*,java.lang. *,com.horstmann.corejava. *)编译器会在类路径下对应包位置进行搜索,

    编译未交代文件路径时,默认为当前目录下。

    javac HelloWorld.java 编译当前目录下的java源文件

    javac d:\myjava\HelloWorld.java

    且编译后,在.java同路径目录下生成class文件。

  • **java虚拟机搜索的是类文件,严格地说是类,搜索路径由类路径classpath决定,且有先后顺序。**java虚拟机在类路径中有"."目录是才查看当前目录。

    如果没有设置类路径,javac和java虚拟机不会产生问题,因为默认的类路径包含"."目录,设置类路径时,也需要添加 . 目录,当前目录(打开dos的目录)才会生效。

    虚拟机不会递归搜索classpath定义的路径。

总结:存在包结构时在基目录下进行javac命令,javac 命令让编译器运行,指定对哪个源文件编译,编译器会先查看类路径下(对应包路径)是否有对应类文件,如果没有或着有但没当前源文件新,则会进行编译,编译过程,编译器会先在类路径下找到对应的引用类,找到才能编译成功,编译成功后会在对应的包路径下生成class文件,然后通过java命令让虚拟机以类路径为基准执行class文件。

4.8.1设置类路径

  • 1.编译时指定:javac -classpath /home/user/project/javaclass:.:/user/local/common/sql.jar MyDog.java

  • 2.set CLASSPATH来改变环境变量

    windows在dos下面设置.

    set CLASSPATH=D:/user/class;.;

    linux通过shell可以设置

    export CLASSPATH=/home/user/class:.:

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

  • 3.运行class文件时指定

4.9 文档注释

  • javadoc : 由源文件生成的一个html文档。
  • 在源代码中添加以专用的定界符/**开始的注释,那么可以很容易地生成一个看上去具有专业水准的文档,其将代码与注释保存在一个地方
  • 由于文档注释与源代码在同一个文件中,在修改源代码的同时,重新运行javadoc就可以轻而易举地保持两者的一致性。

4.9.1 注释的插入

  • javadoc实用程序(utility)从下面几个特性中抽取信息

    • 公有类与接口
    • 公有的和受保护的构造器及方法
    • 公有的和受保护的域
  • 应该为上面几部分编写注释。注释应该放置在所描述特性的前面。注释以/**开始,并以 * /结束。
    • 每个/** . . . */文档注释标记之后紧跟着自由格式文本,标记由@开始,如@author或@param。
    • 自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些句子抽取出来形成概要页
    • 在自由格式文本中,可以使用HTML修饰符
  • 如果文档中有到其他文件的链接,就应该将这些文件放到子目录doc-files中。javadoc实用程序将从**源目录拷贝这些目录及其中的文件到文档目录中。在链接中需要使用doc-files目录,例如:<img src=“doc-files/uml.png”alt=“UMLdiagram”>。**TODO 怎么用?

4.9.2 类注释

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

4.9.3 方法注释

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

    • @param变量描述

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

4.9.4 域注释

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

4.9.5 通用注释

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

  • @author姓名

    • 这个标记将产生一个“author”(作者)条目。可以使用多个@author标记,每个@author标记对应一个作者。
  • @version文本
    • 这个标记将产生一个“version”(版本)条目。这里的文本可以是对当前版本的任何描述。

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

  • @since文本

    • 这个标记将产生一个“since”(始于)条目。这里的文本可以是对引入特性的版本描述。例如,@sinceversion 1.7.1。
  • @deprecated文本

    • 这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。例如,@deprecated Use < code> setVisible(true) instead,通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档
  • @see引用

    • 这个标记将在“see also”部分增加一个超级链接。它可以用于中,也可以用于方法中。这里的引用可以选择下列情形之一:

      • 第一种情况是最常见的。只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接。

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

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

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

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

      • 如果@see标记后面有一个双引号(")字符,文本就会显示在“see also”部分(javadoc命令产生的文档中的介绍)。例如,

      • 可以为一个特性添加多个@see标记,但必须将它们放在一起。如果愿意的话,还可以在注释中的任何位置放置指向其他类或方法的超级链接,以及插入一个专用的标签

4.9.6 包与概述注释

  • 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:

    • 提供一个以package.html命名的HTML文件。在标记…之间的所有文本都会被抽取出来。
    • 提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/* *和 */界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
  • 还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview. html的文件中,这个文件位于包含所有源文件的父目录中。标记… 之间的所有文本将被抽取出来。当用户从javadoc的导航栏中选择“Overview”时,就会显示出这些注释内容。

4.9.7 注释的抽取

这里,假设HTML文件将被存放在目录docDirectory下。执行以下步骤:

  • 切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,例如com. horstmann.corejava,就必须切换到包含子目录com的目录如果存在overview.html文件的话,这也是它的所在目录)。

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

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

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

  • 如果**省略了-d docDirectory选项,那HTML文件就会被提取到当前目录下。**这样有可能会带来混乱,因此不提倡这种做法。

  • 可以使用多种形式的命令行选项对javadoc程序进行调整。例如,可以使用**-author和-version选项在文档中包含@author和@version标记**(默认情况下,这些标记会被省略)。另一个很有用的选项是-link,用来为文档中的标准类添加超链接。

  • 如果使用-linksource选项,则每个源文件被转换为HTML(不对代码着色,但包含行编号),并且每个类和方法名将转变为指向源代码的超链接。

  • 主要还是要去看文档,有关其他的选项,请查阅javadoc实用程序的联机文档,http://docs.oracle.com/javase /8/docs/guides/javadoc。

4.10 类设计技巧

  • 1.保证数据的私有性

  • 2.一定要对数据初始化,实例域虽然会默认初始化,但不应该依赖系统的默认值,而应该显示初始化。

  • 3.不要在类中使用过多的基本类型,可将过多的基本类型用类封装起来。

  • 4.域不一定需要修改器和访问器,有些实例域不希望别人获得或修改。

  • 5.如果一个复杂的类对应的实体可以再细分成多个实体,可将实体封装成类,将复杂的类分解为简单的类。

  • 6.类名和方法名应该体现出他们的功能,给类命名一般应采用名词(可用形容词和动名词修饰)。

  • 7.优先使用不可变的类,不可变类:方法不能改变对象的状态,其返回对象的方法一般是返回已修改的新对象。jdk内部自带的很多都是不可变类 如:Integer、String、Long和LocalDate等

    可变类:开发中创建的大部分类。

    优点:避免发生并发修改,让结果不可预料,不可变类可以让多个线程共享其对象。

    不可变类的设计方法:

    1.类和类变量添加final修饰符保证类不能被继承和成员变量不可改变

    2.所有成员变量必须私有

    3.不提供改变成员变量的方法,包括setter

    4.通过构造器初始化所有成员,并进行深拷贝。

    public final class MyImmutableDemo {  private final int[] myArray;  public MyImmutableDemo(int[] array) {  this.myArray = array.clone();   //深拷贝会创建一个新内存保存传入的值。}
    }
    

    5.getter方法,不返回对象本身,而是克隆对象,并返回对象的拷贝。

第五章 继承

先通过例子来说明一些定义

Employee e = new Manager();

对象变量e的**(引用)类型**为Employee,表示其可以引用Employee及其子类类型的对象,即e是Employee对象的对象变量。Manager表示对象变量的实际引用类型。

引用类型为Employee的对象变量 e 也可看成Employee对象。

5.1 类、超类和子类

  • 继承的明显特征:is-a 如Manager is a Employee Manager extends Employee

5.1.1 定义子类

  • extends关键字表示继承,且为公有继承。表明正在构造的新类派生于一个已存在的类
  • 已存在的类可称为:超类、基类或父类;新类称为:子类、派生类或孩子类。
  • 子类一般比超类封装更多数据,拥有更多功能
  • 设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中

5.1.2 覆盖方法

  • 应主动为覆盖的方法添加@Override声明

  • 超类中不适用于子类的方法,子类可以覆盖(重写)。

  • 尽管子类都有对应的父类私有域,但子类不能直接访问,需要借助父类的公有接口。

  • 子类方法中调用父类的**同名方法(非构造方法)**可以用super关键字,格式:super.methodName()

  • 注:super与this引用不是类似的概念,因为super不是一个对象的引用,它只是一个指示编译器调用超类方法的关键字。

     static class Manager extends Employee{Manager manager;Employee employee;@Overridepublic void money() {manager = this;employee = super;//'.' expectedSystem.out.println(100);}
    
  • 警告:**在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。**特别是,如果超类方法是public,子类方法一定要声明为public。如果低于父类方法的可见性,编译器将会把它解释为试图提供更严格的访问权限。//TODO why

5.1.3 子类构造器

  • 在子类的构造器中,可用调用父类构造器来构造父类对象。
  • super(params)无需加方法名即可调用对应的父类构造器
  • 子类构造器中如果没有显示地调用父类构造器,则会自动地调用父类默认(无参)构造器 即super();
  • 注:父类必须有对应地构造器,否则编译器会报错。
  • this用途:1.引用隐式参数 2.调用该类其他构造器。
  • super用途:1.调用超类方法 2.调用超类构造器
  • 调用构造器的语句只能作为另一个构造器中的第一条语句出现。

多态与动态绑定

  • 一个对象变量可以引用多种类型的对象的现象 称为多态。
  • 运行时能够自动给选择调用哪个方法的现象 称为动态绑定

5.1.4 继承层次

  • 由一个公共超类派生出来的所有类的集合 称为 继承层次(inheritance hierarchy)。

  • 从某个特定的类到其祖先的路径 称为 该类的继承链(inheritance chain)。
  • 一个祖先类可以拥有多个子孙继承链

5.1.5 多态

  • java中 对象变量是多态的,对象变量可以引用该变量声明类型下的所有子类对象。

  • 对象变量调用的方法,只**能调用引用类型E中的方法,不能调用所引用对象所特有的方法。**调用同一方法时会优先使用所引用的对象中的方法。

    public static void main(String[] args) {Employee m = new Manager();m.money();m.level();//Cannot resolve method 'level' in 'Employee'}static class Employee{public void money(){System.out.println(10);}}static class Manager extends Employee{@Overridepublic void money() {System.out.println(100);}public void level(){System.out.println("level");}}
    
  • 对子类数组的引用可以转换成对超类数组的引用,无需强制类型转换。

    Manager[] managers = new Manager[100];Employee[] employees = managers;
    

    切记不可这样:

      employees[0] = new Employee();//为索引为0的员工重新引用一个员工对象 但其他员工皆为经理 java.lang.ArrayStoreException 数组中元素引用对象类型不同引起了元素存储空间格式不同。
    

    我们把一个普通雇员擅自归入经理行列中了

    所有数组都要牢记创建时初始化的对象类型。数组元素引用改变时,引用对象需为改创建时初始化的对象类型。

5.1.6 理解方法调用

下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:

  • 1)编译器查看对象的对象类型和方法名。

    • 编译器会列举出对应对象的类中的名字相同的方法所有超类中访问属性为public且名字相同的方法。(超类私有方法不可访问)
  • 2)编译器查看调用方法时提供的参数类型,如果在所有方法名相同的方法中找到完全匹配参数类型的一个方法,就选择该方法执行,这个过程被称为**重载解析。**由于存在类型转换(int转double,int转long等),该过程可能很复杂,如果没有找到参数类型匹配的方法,或者经过类型转换后有多个方法与之匹配,就会报错。

  • 方法签名:方法名和参数列表

    • 如果子类中定义了与超类签名相同的的方法,那么子类中的该方法就覆盖了父类中这个相同签名的方法。
    • 返回类型不是签名的一部分,因此,覆盖方法时,需保证返回类型的兼容性一般子类方法的返回类型可以定义为父类方法返回类型的子类或者相同的类型。
  • 3)private、static、final修饰的方法或着构造器,编译器可以准确的知道调用哪个,这种调用方法称为静态绑定

    与之对应的动态绑定:调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定

  • 4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与所引用对象的实际类型最合适的那个类的方法。

  • 每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机会预先为每个类创建一个方法表,其中列出了所有的方法签名和实际调用的方法。虚拟机调用方法时查这个表就行了,

    Employee的方法表(这里不包括Object类中的方法)

    Manager方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的,还有一个方法是新增加的。

    定义方法表后manage.getSalary()的解析过程

    • 1)虚拟机提取manager的实际类型的方法表。这里提取Manager
    • 2)搜索出定义相同签名的对应类,这里为Manager,此时虚拟机已经知道调用哪个方法
    • 3)虚拟机调用该类对象对应的方法

动态绑定可以通过变量的实际类型来选择调用对应的方法,且无需对现存代码进行修改就能对程序进行扩展。

5.1.7 阻止继承:final类和方法

  • 希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。
  • 类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)。
  • 域也可以被声明为final,对于final域来说,构造对象之后就不允许改变他们的值了。
  • 方法或类声明为final主要目的是:确保它们不会在子类中改变语义
  • 类声明为final后,只有其中的方法自动地成为final,而不包括域。

5.1.8 强制类型转换

  • 有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用

  • 进行类型转换的唯一原因是:**在暂时忽视对象变量的引用类型之后,使用对象的全部功能。**即引用类型从Employee 转为 Manager 便于使用Manager中的全部功能。

  • 在Java中,每个对象变量都属于一个类型。类型描述了这个变量所引用的以及能够引用的对象类型。

  • 在将值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类引用变量必须进行类型转换,这样才能够通过运行时的检查。

  • 但如果这个超类引用变量引用的不是对应的子类对象时,允许时将会报错并产生一个ClassCastException异常。

  • 因此,应该养成这样一个良好的程序设计习惯:在进行类型转换之前,先查看一下对象变量引用对象的类型判断是否能够成功地转换。这个过程简单地使用instanceof操作符就可以实现。

  • instanceof 操作符:判断所引用对象是否为某个类或其子类

    即判断是否满足 is a 关系

  • 综上:

    • 对象引用实际类型为子类,而引用类型为父类的,转为父类引用类型需强制转换。
    • 在将引用类型为超类的转换为子类之前,应使用instanceof进行检查。
  • x为null x instancof C 不会产生异常,因为null表示不引用任何对象,即不引用C类型的对象。

  • 多态中,一般只有使用子类中的特定方法才会进行类型转换

  • 在一般情况下,应该尽量少用类型转换和instanceof运算符。

5.1.9 抽象类

  • 继承层次结构中,位于上层的类更具有通用性,甚至可能更加抽象。通用属性和方法可以放置在通用的超类中。

  • 通用的超类一般不会被实例化,对于子类继承超类且有自己实现的超类方法,超类可以将该方法抽象,表示该方法是抽象的,无具体实现。

  • 包含一个或多个抽象方法的类本身必须被声明为抽象的。

  • 除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如,Person类还保存着姓名和一个返回姓名的具体方法。

  • 建议将通用的域和方法(不管是否抽象)放在超类(不管是否是抽象类)中。

  • 抽象方法充当着占位的角色,它们的具体实现在子类中。

  • 扩展抽象类可以有两种选择。

    • 1.抽象方法在抽象类中为未定义完全时,这样子类必须也标记为抽象类。
    • 2.抽象类定义全部的抽象方法后,子类就不是抽象的了。
    • 例如,通过扩展抽象Person类,并实现getDescription方法来定义Student类。由于在Student类中不再含有抽象方法,所以不必将这个类声明为抽象的。
  • 类即使不含抽象方法,也可以将类声明为抽象类。

  • 继承抽象类的非抽象子类必须实现对应的抽象方法。

  • 抽象类不能被实例化,但抽象类的对象变量可以引用非抽象子类的对象。

    且只有抽象类的非抽象子类可以通过调用父类构造器(super())创建抽象类对象。

  • 对象数组并不会实例化对象,而只是声明了若干个特定引用类型的引用变量的空间,且对象数组元素默认初始化为null

    下面代码中,抽象类Person并未实例化,对应的引用变量只能引用非抽象子类对象。

5.1.10 受保护访问

  • 一般,最好将类中的域标记为private,而方法标记为public。任何声明为private的内容对其他类都是不可见的。
  • 有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。可将这些方法声明为protected。使得子类只能获得访问受保护域的权利。
  • 事实上,Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。
    • 1)仅对本类可见——private。
    • 2)对所有类可见——public。
    • 3)对本包和所有子类可见——protected。
    • 4)对本包可见——默认(很遗憾),不需要修饰符。

5.2 Object:所有类的超类

Objects工具类对object(各个对象)中的常用方法进行封装,如封装equals和hashCode,当访问Object类中常用方法时,可以利用Objects工具类进行访问。

  • Object类是Java中所有类的始祖,类默认继承Object类。
  • 在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类(封装了多个数据的内存块也算对象)。

5.2.1 equals方法

  • 在Object类中,这个方法将判断两个对象是否具有相同的引用(地址)

  • 然而,对于多数类来说,这种判断并没有什么意义,因为一般意义的相等是判断两个对象的状态是否相等。

  • 常在类中重写equals方法来判断两个对象的状态是否相等。如

  • 为了防备name或hireDay可能为null的情况,需要使用Objects.equals方法。如果两个参数都为null,Objects.equals(a, b)调用将返回true;如果其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。语句要改写为

  • 子类中定义equals方法时,首先调用父类的equals,父类的equals方法检测成功则表示对应超类状态相等,然后再比较子类中的实例域。

5.2.2 相等测试与继承

  • 隐式和显式的参数不属于同一个类,equals方法将如何处理呢?这是一个很有争议的问题。在前面的例子中,如果发现类不匹配,equals方法就返回false。但是,许多程序员却喜欢使用instanceof进行检测(常用在由超类决定相等的概念,让不同子类对象可以相等,只要对应的父类域相同即可,无需比较子类域):

Java语言规范要求equals方法具有下面的特性:

1)自反性:对于任何非空引用x, x.equals(x)应该返回true。

2)对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。

3)传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true, x.equals(z)也应该返回true。

4)一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

5)对于任意非空引用x, x.equals(null)应该返回false。

用getClass() 还是 instanceof 进行比较

  • 如果子类能够拥有自己的相等概念,则**对称性需求将强制采用getClass进行检测。**如要求两个对象域相等以及对应父类的域相等才算相等。//TODO 什么时候用

  • 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。如将雇员id作为相等的检测指标,且适用于所有子类,就可以使用instanceof,且应将Employee.equals声明为final,让子类用父类的比较方法。

  • 下面给出编写一个完美的equals方法的建议:

    • 1)显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。

    • 2)检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。

    • 3)检测this与otherObject是否引用同一个对象:

    • 4)比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:

      如果所有的子类都拥有统一的语义,就使用instanceof检测:

    • 5)将otherObject转换为相应的类类型变量:

    • 6)对需要的比较域进行比较。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。

    • 如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。且需比较子类中的域

    • 数组类型的域,可以使用静态的Arrays.equals(a,b)方法检测相应的数组元素是否相等。参数类型非基本类型时需重写equals方法。

java.util.Arrays.static Boolean equals(type[ ] a, type[ ] b)

​ 如果两个数组长度相同,并且在对应的位置上数据元素也均相同,将返回true。数组的元素类型可以是Object、int、long、short、char、byte、boolean、float或double。

java.util.Objects.static boolean equals(Object a, Object b)

如果a和b都为null,返回true;如果只有其中之一为null,则返回false;否则返回a.equals(b)。

5.2.3 hashCode方法

  • 散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。

  • 如果x和y是两个不同的对象,x.hashCode( )与y.hashCode( )基本上不会相同。

  • hash code可以由对象状态(内容)得出,也可由地址得出。

  • String重写了hashCode方法,String的hashcode由内容得出,即相同内容的String由相同的hashcode。

  • Object类中的hashCode方法默认将hashcode的值由对象的存储地址得出。

  • 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到HashList中。

  • hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

  • 还可以做得更好。首先,最好使用null安全的方法Objects.hashCode。如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。另外,使用静态方法Double.hashCode来避免创建Double对象:

  • 还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参数。这个方法会调用Arrays.hashCode,其对各个参数实现了类似Objects.hashCode的功能,并组合这些散列值。

  • Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode( )就必须与y.hashCode( )具有相同的值。例如,如果用定义的Employee.equals比较雇员的ID,那么hashCode方法就需要hash化ID,而不是雇员的姓名或存储地址。

  • 如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码这个散列码由数组元素的散列码组成

5.2.4 toString方法

  • object中用于返回表示对象值的字符串。

  • 绝大多数的toString方法都遵循这样的格式:类名+方括号括起来的域值。

  • 可设置成getClass().getName()获得类名。

  • 子类也应定义自己的toString方法,并将子类域的描述添加进去,如果超类使用getClass().getName(),子类可调用super.toString()

  • 输出以下内容

  • 对象于一个字符串通过"+"连接,编译器会自动调用toString方法,来获得该对象的字符串描述。

  • 调用x.toString( )的地方可以用""+x替代。这条语句将一个空串与x的字符串表示相连接。

  • 如果x是任意一个对象,并调用println方法就会直接地调用x.toString( ),并打印输出得到的字符串。

  • 所用类都有继承于Object类的toString方法,其输出对象所属的类名和散列码。

  • 警告:数组虽然继承了toString方法,但未重写,一九按旧格式输出。

    生成字符串“[I@1a46e30”(前缀[I表明是一个整型数组)。

    可通过调用静态方法Arrrays.toString修正

  • 打印多维数组需调用 Arrays.deepToString方法。

  • 强烈建议为自定义的每一个类增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

5.3 泛型数组列表

  • 解决数组空间浪费的问题,使用ArrayList,它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

  • ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList< Employee >。

  • 构造数组列表时可以省略右边的类型参数

  • 使用add方法可以将元素添加到数组列表中,默认添加到列表尾部(第一个无内容的位置)。

  • 该数组列表管理着对象引用。且如果调用add时内部数组已经满了,数组列表会自动创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

  • 如果能够估计出数组可能存储的元素数量,可以在填充数组前调用ensureCapacity方法:staff.ensureCapacity(100);或着将初始容量传递给构造器:ArrayList< Employee > staff = new ArrayList<>(100); 这个方法调用将分配一个包含100个对象的内部数组。然后调用100次add,而不用重新分配空间。

  • size方法可以返回数组列表中包含的实际元素个数 staff.size(),并不是封装数组的长度。

  • 如果确认数组列表的中元素个数不再变化,可以调用timeToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。

5.3.1 访问数组列表元素

  • 使用get和set方法实现访问和改变数组元素的操作。

  • 设置第i个元素,可以使用staff.set(i,harry) 等价于对封装的数组的第i个元素赋值。

  • set方法只能替换数组中已存在的元素内容,并不能新增,添加新元素只能使用add方法。

    只有i小于等于数组列表的大小时(实际含有元素的个数),才能调用list.set(i,x)

  • get方法也是只能获取数组中存在元素的位置上对应的内容。

  • 可使用toArray方法将数组列表元素拷贝到一个数组中,

  • 还可以在数组列表的中间(已有内容的所封装的数组序列中)插入元素,使用带索引参数的add方法。

    插入一个新元素,位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重新分配存储空间。

  • 从数组列表中间删除一个元素,可用remove方法

    位于这个位置之后的所有元素都向前移动一个位置,并且数组列表的大小(size())减1。

  • 对数组列表进行更新操作时,效率比较低(移动元素)。但如果数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表了。

  • 可以使用“for each”循环遍历数组列表:一般不在for each循环中改变数组列表中的内容

  • 数组列表相比数组特点:

    • 不必指出数组大小
    • 使用add添加元素到数组中
    • 使用size()替代length求持有元素的个数。
    • 使用get(i)访问元素。

5.3.2 类型化与原始数组列表的兼容性

  • 将一个类型化ArrayList赋给一个原始ArrayList,不会出现任何错误信息或警告,因为虚拟机的完整性绝对没有受到威胁。在这种情形下,既没有降低安全性,也没有受益于编译时的检查。

    ArrayList<Employee> employees = new ArrayList<>();ArrayList objects = employees;
    
  • 相反地,将一个原始ArrayList赋给一个类型化ArrayList会得到一个警告。

            ArrayList<Employee> employees;ArrayList objects = new ArrayList();employees = objects;//Unchecked assignment: 'java.util.ArrayList' to 'java.util.ArrayList<day02.Employee>'
    

    使用类型转换并不能避免出现警告。同样指出类型转换有误

            ArrayList<Employee> employees;        ArrayList objects = new ArrayList();        employees = (ArrayList<Employee>)objects;//Unchecked cast: 'java.util.ArrayList' to 'java.util.ArrayList<day02.Employee>'
    

    这就是Java中不尽如人意的参数化类型的限制所带来的结果。鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象就将所有的类型化数组列表转换成原始ArrayList对象。 在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。

  • 一旦能确保不会造成严重的后果,可以用**@SuppressWarnings(“unchecked”)标注来标记这个变量能够接受类型转换,如下所示**

5.4 对象包装器与自动装箱

  • 基本类型也可以转换为对象。所有的基本类型都有对应的类供其转换。

  • 这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)。对象包装器类是不可变的类,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

  • 数组列表中的类型参数不允许是基本类型,可用对应的包装器来代替。

  • 由于每个值分别包装在对象中,所以ArrayList的效率远远低于int[ ]数组。因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。

  • 有一个很有用的特性,从而更加便于添加int类型的元素到ArrayList中

    这种变换被称为自动装箱(autoboxing)。

  • 相反地,当将一个Integer对象赋给一个int值时,将会自动地拆箱

  • 算式表达式中也能自动装箱和拆箱,如

    编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,最后再将结果装箱。

  • 包装器对象是一个对象,其会指向一个存储区域,区域内封装了基本类型的数据。含有相同基本类型数据数值的包装器一般地址不同,==运算符进行比较时一般不会成立,但**比较包装的内容时可用equals方法进行比较,**包装器中重写了equals方法。

  • 自动装箱规范要求byte <=127,且**-128到127之间的short和int会被包装到固定的对象中。**

    Integer a = 100;        Integer b = 100;        System.out.println(a == b);//true
    
  • 自动装箱还有几点需要说明。首先,由于包装器类引用可以为null,所以自动装箱有可能会抛出一个NullPointerException

  • 条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double:

    装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

  • 可利用包装类中静态方法将字符串转为对应基本类型的数值

    这里与Integer对象没有任何关系,parseInt是一个静态方法。但Integer类是放置这个方法的一个好地方。

  • 警告:包装器类中没有修改数值的方法,因为包装器为不可变类,包含在包装器中的内容不会改变。

  • 如果希望有一个能储存可修改数值的对象,就需要使用在org.omg.CORBA包中定义的持有者(holder)类型,包括IntHolder、BooleanHolder等。每个持有者类型都包含一个公有!域值,通过**它可以访问存储在其中的值。**可以通过该对象编写一个能修改数值的方法。

5.5 参数数量可变的方法

  • 现在的版本提供了可以用可变的参数数量调用的方法

  • 上面两条调用语句虽然参数数量不同但调用的是同一个方法。

  • 这里的省略号…是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)

  • 实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[ ]数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。现在将扫描fmt字符串,并将第i个格式说明符与args[i]的值匹配起来。

  • 编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱:

  • 允许将一个数组传递给可变参数方法的最后一个参数。

    即允许将最后一个参数为数组的方法重新定义为可变参数的方法。

    两者等价:

    public static void main(String[] args) {}public static void main(String ... args){}
    

5.6 枚举类

  • 这个枚举类刚好有四个实例(枚举常量),此后尽量不要通过new构造新对象。

  • 因此,在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“= =”就可以了。

  • 需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。下面是一个示例:

  • **所有的枚举类型都是Enum类的子类。**它们继承了这个类的许多方法。其中最有用的一个是toString,这个方法能够返回枚举常量名。例如,Size.SMALL.toString( )将返回字符串“SMALL”。

  • toString的逆方法是静态方法valueOf

    将s设置成Size.SMALL。

  • 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举常量的数组。//TODO Enum类没有

    返回包含元素Size.SMALL, Size.MEDIUM, Size.LARGE和Size.EXTRA_LARGE的数组。

  • ordinal方法返回enum声明中枚举常量的位置位置从0开始计数。例如:Size. MEDIUM. ordinal()返回1。

5.7 反射

  • 反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans中,它是Java组件的体系结构(有关JavaBeans的详细内容在卷Ⅱ中阐述)。
  • 特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。
  • 能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:
    • ● 在运行时分析类的能力。
    • ● 在运行时查看对象,例如,编写一个toString方法供所有类使用。
    • ● 实现通用的数组操作代码。
    • ● 利用Method对象,这个对象很像C++中的函数指针。
  • 反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。

5.7.1 Class类

  • 在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类虚拟机利用运行时类型信息选择相应的类中的方法执行

  • 然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class。Object类中的getClass( )方法将会返回一个Class类型的实例。

  • 一个Class对象将表示一个特定类型的属性。最常用的Class方法是getName。这个方法将返回类的名字。如果类在一个包里,包的名字也作为类名的一部分

  • 可以调用静态方法forName获得类名(全路径包名)对应的Class对象

    如果类名保存在字符串中,并可在运行中改变,就可以使用这个方法。当然,这个方法只有在className是类名或接口名时才能够执行。否则,forName方法将抛出一个checkedexception(已检查异常)。无论何时使用这个方法,都应该提供一个异常处理器(exception handler)。

  • 获得Class类对象的第三种方法非常简单。如果T是任意的Java类型(或void关键字), T.class将代表匹配的类对象。(在包中的需导包,会默认加上包前缀)

  • 请注意,一个Class对象实际上表示的是一个类型,而这个类型未必一定是一种类。例如,int不是类,但int.class是一个Class类型的对象。

  • 注:Class类实际上是一个泛型类。例如,Employee.class的类型是Class。没有说明这个问题的原因是:它将已经抽象的概念更加复杂化了。在大多数实际问题中,可以忽略类型参数,而使用原始的Class类

  • 鉴于历史原因,getName方法在应用于数组类型的时候会返回一个很奇怪的名字

  • 虚拟机为每个类型管理一个Class对象。因此,可以利用==运算符实现两个类对象比较的操作。例如,

  • newInstance( ),可以用来动态地创建一个类的实例。例如,newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。

  • 将forName与newInstance配合起来使用,可以根据存储在字符串中的类名创建一个对象。

  • 如果需要用有参构造器创建实例,必须使用Constructor类中的newInstance方法。

5.7.2 捕获异常

  • 当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个“捕获”异常的处理器(handler)对异常情况进行处理。 如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息,其中给出了异常的类型。

  • 异常有两种类型:未检查异常和已检查异常。对于已检查异常(运行前编译器可以检查出来),编译器将会检查是否提供了处理器(未提供则无法运行)。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。

    应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。

  • 并不是所有的错误都是可以避免的。如果竭尽全力还是发生了异常,编译器就要求提供一个处理器。Class.forName方法就是一个抛出已检查异常的例子。

    可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码

  • 如果类名不存在,则将跳过try块中的剩余代码,程序直接进入catch子句(这里,利用Throwable类的printStackTrace方法打印出栈的轨迹。Throwable是Exception类的超类)。如果try块中没有抛出任何异常,那么会跳过catch子句的处理器代码。

  • 对于已检查异常,只需要提供一个异常处理器。可以很容易地发现会抛出已检查异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告。

5.7.3 利用反射分析类的能力

  • 反射机制最重要的内容——检查类的结构

  • java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器

    • 三个类都有getName方法,用于放回对应项目名称
    • Field类中getType方法,返回域所属类型的Class对象
    • Method类和Constructor类 有能返回参数类型的Class对象数组的方法 getParameterTypes
    • Method类中还有报告返回类型的Class对象方法。getReturnType
    • 构造器无返回类型,只有方法名,且方法名为所构造类型对应Class对象的Name
    • 三个类还有 getModifiers的方法,用于返回一个表示描述当前项目修饰符使用状况的整型数值。
    • 可用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。如isPublic、isPrivate
    • 我们需要做的全部工作就是调用Modifier类的相应方法,并对返回的整型数值进行分析,另外,还可以利用Modifier.toString方法将修饰符打印出来。
  • Class类中的getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员

  • Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

  • Class类还可以获取该类型对应直接父类类型的Class对象的方法

    Class getSuperclass()

5.7.4 在运行时使用反射分析对象

  • 如何查看任意对象的数据域名称和类型:

  • 进一步查看数据域的实际内容。当然,在编写程序时,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。

  • 查看对象域的关键方法是Field类中的get方法。

    如果f是一个Field类型的对象, obj是某个包含f域的类的对象f.get(obj)将返回一个对象其值为obj中对应域的当前值。这样说起来显得有点抽象,这里看一看下面这个示例的运行。

  • 上面代码存在问题,因为name为私有域,访问私有域即调用get方法将抛出IllegalAccessException。除非拥有访问权限,否则Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。

  • 反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method或Constructor对象的setAccessible方法。例如,

  • setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和相似机制提供的。

  • get方法还有一个需解决的问题,get默认放回Object类型,如果域为基本类型,要返回基本类型可用形如getDouble方法,也可用get方法,因为反射机制会自动将这个域的值打包到对应的包装器。

  • 可以获得就可以设置。调用f.set(obj, value)可以将obj对象的f域设置成新值。

  • 判断Class对象是否是基本类型isPrimitive()

  • 利用反射解析对象内容,这是一种公认的提供toString方法的手段ObjectAnalyzer.java

5.7.5 使用反射编写泛型数组代码

  • java.lang.reflect包中的Array类允许动态地创建数组

  • Arrays类中的copyOf方法常用于扩展已经填满的数组。

  • 利用Array类来编写这样一个通用的方法。

    这段代码返回的数组类型是对象数组(Object[ ])类型,这是由于使用下面这行代码创建的数组:

  • 一个对象数组不能转换成雇员数组(Employee[ ])。如果这样做,则在运行时Java将会产生ClassCastException异常。前面已经看到,Java数组会记住每个元素的类型,即创建数组时new表达式中使用的元素类型。

    将一个Employee[ ]临时地转换成Object[ ]数组,然后再把它转换回来是可以的,但一个从开始就是Object[ ]的数组却永远不能转换成Employee[ ]数组。

  • 编写这类通用的数组代码,需要能够创建与原数组类型相同的新数组。为此,需要java. lang.reflect包中Array类的一些方法。其中最关键的是Array类中的静态方法newInstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度。

  • 获得新数组元素类型,就需要进行以下工作:

    • 1)首先获得a数组的类对象。
    • 2)确认它是一个数组。
    • 3)使用Class类(只能定义表示数组的类对象)的getComponentType方法确定数组对应的类型。
  • 为了能够实现上述操作,应该将goodCopyOf的参数声明为Object类型,而不要声明为对象型数组(Object[ ])。整型数组类型int[]可以被转换成Object,但不能转换成对象数组,数组本身就是一个类,继承于Object

5.7.6 调用任意方法

  • 反射机制允许你调用任意方法。

  • 回忆一下利用Field类的get方法查看对象域的过程。与之类似,在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。

  • invoke方法的签名是:

    第一个参数是隐式参数(对象),其余的对象提供了显式参数

  • 对于静态方法,第一个参数可以被忽略,即可以将它设置为null。

  • 如果返回类型是基本类型,invoke方法会返回其包装器类型

  • 例如,假设m2表示Employee类的getSalary方法,那么返回的对象实际上是一个Double,必须相应地完成类型转换。可以使用自动拆箱将它转换为一个double:

  • 可通过调用getDeclareMethods方法,然后对返回的Method对象数组进行查找,直到发现想要的方法为止。也可以通过调用Class类中的getMethod方法得到想要的方法。有可能存在方法名相同的方法,故需提供对应的参数类型。

    getMethod的签名是:

    例如:

  • 具体例子:

    • 先将方法抽出封装为Method对象

      Method sqrt = Math.class.getMethod("sqrt", double.class);
      
    • 传入具体对象和参数执行方法,且需将返回类型转换

      double y = (Double) f.invoke(null, x);
      
  • 果在调用方法的时候提供了一个错误的参数,那么invoke方法将会抛出一个异常。

  • 另外,invoke的参数和返回值必须是Object类型的。这就意味着必须进行多次的类型转换。这样做将会使编译器错过检查代码的机会。因此,等到测试阶段才会发现这些错误,找到并改正它们将会更加困难。不仅如此,使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些。

  • 有鉴于此,建议仅在必要的时候才使用Method对象,而最好使用接口以及Java SE 8中的lambda表达式。

  • 建议Java开发者不要使用Method对象的回调功能。使用接口进行回调会使得代码的执行速度更快,更易于维护。

5.8 继承的设计技巧

  • 公共操作和域放在超类

    • 这就是为什么将姓名域放在Person类中,而没有将它放在Employee和Student类中的原因。
    • 一般子类没有特有的域。
  • 不要使用受保护的域

    protected机制并不能够带来更好的保护

    • 第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。
    • 第二,在Java程序设计语言中,在同一个包中的所有类都可以访问proteced域,而不管它是否为这个类的子类。

    不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

  • 使用继承实现“is-a”关系

    • 确保子类是父类的一种。
  • 除非所有继承的方法都有意义,否则不要使用继承

  • 在覆盖方法时,不要改变预期的行为

    • 在覆盖一个方法的时候,不应该毫无原由地改变行为的内涵
    • 关键在于,在覆盖子类中的方法时,不要偏离最初的设计想法
  • 使用多态,而非类型信息

    • 无论什么时候,对于下面这种形式的代码

      都应该考虑使用多态性。

      action1与action2表示的是相同的概念吗?如果是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后,就可以调用

      以便使用多态性提供的动态分派机制执行相应的动作。

      使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展

  • 不要过多地使用反射

    • 反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。
    • 反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

java核心技术卷I 第4-5章相关推荐

  1. Java核心技术卷阅读随笔--第3章【Java 的基本程序设计结构】

    3.1 一个简单的Java应用程序 下面看一个最简单的 Java 应用程序,它只发送一条消息到控制台窗口中: 复制代码 public class FirstSample { public static ...

  2. Java核心技术卷2 高级特性 学习笔记(5)

    参考:Java核心技术卷2 高级特性  第九章 Java技术提供了三种确保安全的机制: 语言设计特性(对数组的边界进行检查,无不受检查的类型转换,无指针算法等). 访问控制机制,用于控制代码能够执行的 ...

  3. java12章_【有书共读】java核心技术卷1--第12章

    ==============java核心技术卷1第12章----Swing用户界面组件===========主要内容:1 swing和模型-视图-控制器设计模式2布局管理 3文本输入4选择组件 5菜单 ...

  4. Java 核心技术卷 II(第 8 版) – 读书笔记 – 第 1 章(下)

    22.一旦获得了一个 Charset,就可以在 Java 的 Unicode 和指定的编码格式之间进行转化,下面以 GBK 和 Unicode 之间做为例子. 从 Unicode 到 GBK: imp ...

  5. 《Java 核心技术 卷1》 笔记 第11章 异常、日志、断言和调试

    出现不可预计的问题时,需要进行如下处理: 报告错误 保存操作结果 允许用户退出 本章解决的问题: 验证程序正确性 记录程序错误 调试技巧 11.1 处理异常 程序出现错误时应该: 返回安全状态,能让用 ...

  6. java核心技术卷I 第1-3章 笔记

    java核心技术卷I 第1-3章 本书将详细介绍下列内容: ● 面向对象程序设计 ● 反射与代理 ● 接口与内部类 ● 异常处理 ● 泛型程序设计 ● 集合框架 ● 事件监听器模型 ● 使用Swing ...

  7. 《Java 核心技术 卷1》 笔记 第五章 继承(3)

    5.1.6 抽象类 有时候我们无法说出具体是什么,只能用于标识一个类型,比如图形,就可作为抽象类.虽然无法具体描述图形,但是图形通常都有面积.周长.这种时候就可用抽象类标识. 抽象类使用abstrac ...

  8. java实现图形界面输入半径求圆面积_【读】Java核心技术卷1

    阅读原文:[读]Java核心技术卷1 看到这本书时,我的内心是崩溃的,卷1就700多页,但是这本书是很多前辈所推荐的,想必其中必有精华所在,硬着头皮上吧. 如何阅读本书 拿到书的第一眼肯定去看目录,大 ...

  9. JAVA基础----终弄清java核心技术卷1中的int fourthBitFromRight = (n 0b1000 ) / 0b1000;是怎么回事了。。。

    一个关于位与运算的问题 对于<JAVA核心技术 卷1>(第11版) page43 第三章 3.5.8一节中有个描述如下: 如果n是一个整数变量,而二进制表示的n从右边数第四位1,则 int ...

  10. 《Java核心技术 卷Ⅰ》读书笔记一

    Java核心技术·卷 I(原书第10版) 作者: [美] 凯.S.霍斯特曼(Cay S. Horstmann) 出版社: 机械工业出版社 原作名: Core Java Volume I - Funda ...

最新文章

  1. 启动tomcat时jmx port被占用
  2. Android开发常用开源框架3
  3. 【深度学习】从R-CNN到Mask R-CNN的思维跃迁
  4. git url地址无效_如何同步多个 git 远程仓库
  5. CentOS 7下安装Logstash ELK Stack 日志管理系统(上)
  6. Hadoop之HDFS概述
  7. python web server_Python实现简易版的Web服务器(推荐)
  8. 计算机 ieee access,计算机 | IEEE Access 诚邀专刊稿件 (IF:3.557)
  9. 配电柜测试软件,低压配电柜测试方法及流程.docx
  10. linux下gcc编译使用opencv的源文件时报错的处理:undefined reference to symbol '_ZNSsD1Ev@@GLIBCXX_3.4'
  11. pagehelper工具类_PageHelper最佳实践
  12. luogu P1503 鬼子进村
  13. WPF引用外部类库中的资源文件提示不能找到的解决方法
  14. JSP表单提交中文乱码解决方案
  15. python英语单词库app_英语单词库【英语单词库英语头条】- 英语单词库知识点 - 中企动力...
  16. 龙芯pmon启动流程概述
  17. json datasource使用
  18. etc 文件夹下放什么内容
  19. 西南大学计算机辅助设计试题,西南大学 1906 课程名称:(9123)《计算机辅助设计》机考 答案-奥...
  20. Apache虚拟主机配置

热门文章

  1. MRI_Made_Easy 磁共振成像原理-物理基础5
  2. arcgis点连线_ArcGIS中,一个点集里的点两两连线,比如有4个点,就连6条线
  3. c语言题模板大全,C语言试题库完整版整理版
  4. php 万能密码,网络安全系列之十 万能密码登录网站后台
  5. node 简繁体转换_简体繁体转换
  6. autocad 职称计算机,2015职称计算机考试《AutoCAD》考点总结(1)
  7. 网吧无盘服务器进u盘启动,利用U盘启动在网吧免费上网
  8. 计算机藏应用,应用隐藏大师计算器
  9. AdGuard Home 使用设置以及DNS测速软件
  10. 博弈论 | 三姬分金与囚徒困境