数组排序
要想对数值型数组进行排序, 可以使用 Arrays类中的 sort 方法:
int[] a = new int[10000]; ... Arrays.sort(a);
Arrays.sort();的源码
public static void sort(Object[] a) { if(LegacyMergeSort.userRequested) { legacyMergeSort(a); } else { ComparableTimSort.sort(a, 0, a.length, null, 0, 0); } } //LegacyMergeSort.userRequested源码 static final class LegacyMergeSort { private static final boolean userRequested = AccessController.doPrivileged(new GetBooleanAction("java.util.Arrays.useLegacyMergeSort")).booleanValue(); } //legacyMergeSort(a)源码 private static void legacyMergeSort(Object[] a) { Object[] aux = a.clone(); mergeSort(aux, a, 0, a.length, 0); } //ComparableTimSort.sort()源码 static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) { assert a != null && lo >= 0 && lo <= hi && hi <= a.length; int nRemaining = hi - lo; if (nRemaining < 2) return; // Arrays of size 0 and 1 are always sorted // If array is small, do a "mini-TimSort" with no merges if (nRemaining < MIN_MERGE) { int initRunLen = countRunAndMakeAscending(a, lo, hi); binarySort(a, lo, hi, lo + initRunLen); return; }
不规则数组
到目前为止,读者所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实 际存在着一些细微的差异, 而这正是 Java 的优势所在:Java 实际上没有多维数组,只有一维 数组。多维数组被解释为“ 数组的数组。
/** * This program demonstrates a triangular array. * @version 1.20 2004-02-10 * @author Cay Horstmann */ public class LotteryArray { public static void main(String[] args) { final int NMAX = 10; // allocate triangular array int[][] odds = new int[NMAX + 1][]; for (int n = 0; n <= NMAX; n++) odds[n] = new int[n + 1]; // fill triangular array for (int n = 0; n < odds.length; n++) for (int k = 0; k < odds[n].length; k++) { /* * compute binomial coefficient n*(n-1)*(n-2)*...*(n-k+1)/(1*2*3*...*k) */ int lotteryOdds = 1; for (int i = 1; i <= k; i++) lotteryOdds = lotteryOdds * (n - i + 1) / i; odds[n][k] = lotteryOdds; } // print triangular array for (int[] row : odds) { for (int odd : row) System.out.printf("%4d", odd); System.out.println(); } } }
第四章 对象与类别



面向对象程序设计概述
面向对象程序设计(简称 OOP) 是当今主流的程序设计范型, 它已经取代了 20 世纪 70 年代的“ 结构化” 过程化程序设计开发技术。Java 是完全面向对象的, 必须熟悉 OOP 才能 够编写 Java 程序。
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。 Pascal 语言的设计者 Niklaus Wirth将其著作命 名为《算法 + 数据结构 = 程序》,在 Wirth命名的书名中, 算法是第一位的,数据结构是第二位的,这就明确地表述了程序员的X作方式。 t先要确定如何操作数据, 然后再决定如何组织数 据, 以便于数据操作。 而 OOP 却调换了这个次序, 将数据放在第 •位,然后再考虑操作数 据的算法。
类
类(class) 是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的切割机,将对 象想象为小甜饼。由类构造(construct) 对象的过程称为创建类的实例 (instance)。
封装(encapsulation, 有时称为数据隐藏)是与对象有关的一个重要概念。从形式上看, 封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象 中的数据称为实例域( instance field), 操纵数据的过程称为方法(method )。对于每个特定的 类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态(state)。 无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。
在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类 中,只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立另外一个 类的过程称为继承(inheritance)。
对象
要想使用 OOP, —定要清楚对象的三个主要特性:
•对象的行为(behavior)— —可以对对象施加哪些操作,或可以对对象施加哪些方法?
•对象的状态(state)— —当施加那些方法时,对象如何响应?
•对象标识(identity)— —如何辨别具有相同行为与状态的不同对象?
对象的状态可能会 随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调用方法实现 。
(如果不经过方法调用就可以改变对象状态,只能说明封装性遭到了破坏)
识别类
传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序。在面向对象程序设计 时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是:首先从 设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
• 类:class。
• 对象:Object, instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。 • 对象和类的关系:
• 特殊到一般,具体到抽象。
• 类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
• 类是用于描述同一类形的对象的一个抽象的概念,类中定义了这一类对象所应具有的静态和动 态属性。
• JDK提供了很多类供编程人员使用,编程人员也可定义自己的类。
类之间的关系
在类之间, 最常见的关系有
•依赖( “ uses-a”)
•聚合( “ has-a”)
•继承( “ is-a”)
依赖(dependence), 即“ uses-a” 关系, 是一种最明显的、最常见的关系。
应该尽可能地将相互依赖的类减至最少。如果类 A 不知道 B 的存在, 它就不会关心 B 的任何改变(这意味着 B 的改变不会导致 A 产生任何bug)。用软件工程的术语来说,就是 让类之间的耦合度最小。
聚合(aggregation), 即“ has-a” 关系, 是一种具体且易于理解的关系。
聚合关系意味着类 A 的对象包含类 B 的对象。

继承(inheritance), 即“ is-a” 关系, 是一种用于表示特殊与一般关系的。
一般而言, 如果类 A 扩展类 B, 类 A 不但包含从类 B 继承的方法,还会 拥有一些额外的功能。
使用预定义类
在程序中,可以使用 Math 类的方法, 如 Math, random, 并只需要知道方法名和参数(如果有的话),而不必了解它的具体实现过程。这正是 封装的关键所在,当然所有类都是这样。但遗憾的是,Math类只封装了功能,它不需要也不 必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。
对象与对象变量
在 Java 程序设计语言中, 使用构造器(constructor) 构造新实例。构造器是一种特殊的方法, 用来构造并初始化对象。
构造器的名字应该与类名相同。因此 Date类的构造器名为 Date。要想构造一个 Date 对 象,需要在构造器前面加上 new 操作符,如下所示:
new Date()
这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。 如果需要的话,也可以将这个对象传递给一个方法:
System.out.printTn(new Date());
或者, 也可以将一个方法应用于刚刚创建的对象。Date 类中有一个 toString方法。这 个方法将返回日期的字符串描述。下面的语句可以说明如何将 toString方法应用于新构造的 Date 对象上。
String s = new Date().toString();
在这两个例子中, 构造的对象仅使用了一次。通常, 希望构造的对象可以多次使用, 因 此,需要将对象存放在一个变量中:
Date birthday = new Date();
在对象与对象变量之间存在着一个重要的区别。例如, 语句
Date deadline; // deadline doesn't refer to any object
定义了一个对象变量 deadline, 它可以引用 Date 类型的对象。但是,一定要认识到: 变量 deadline 不是一个对象, 实际上也没有引用对象。此时,不能将任何 Date 方法应用于这个变 量上。语句
s = deadline.toString(); // not yet 将产生编译错误。
必须首先初始化变量 deadline, 这里有两个选择。当然,可以用新构造的对象初始化这 个变量: deadline = new Date(); 也让这个变量引用一个已存在的对象:
deadline = birthday;
现在,这两个变量引用同一个对象
Java 类库中的 LocalDate 类
类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类: 一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。
不要使用构造器来构造 LocalDate类的对象。实际上,应当使用静态工厂方法 (factory method) 代表你调用构造器。下面的表达式
LocalDate.now()
会构造一个新对象,表示构造这个对象时的日期。 可以提供年、 月和日来构造对应一个特定日期的对象:
LocalDate.of(1999, 12, 31)
当然,通常都希望将构造的对象保存在一个对象变量中:
LocalDate newYearsEve = LocalDate.of(1999, 12, 31) ;
一旦有了一个 LocalDate 对象, 可以用方法 getYear、getMonthValue 和 getDayOfMonth 得到年、月和日:
int year = newYearsEve.getYearO; // 1999 int month = newYearsEve.getMonthValueO; // 12 int day = newYearsEve.getDayOfMonth() ; // 31
看起来这似乎没有多大的意义, 因为这正是构造对象时使用的那些值。不过,有时可能 某个日期是计算得到的,你希望调用这些方法来得到更多信息。例如, plusDays 方法会得到 一个新的 LocalDate, 如果把应用这个方法的对象称为当前对象,这个新日期对象则是距当 前对象指定天数的一个新日期:
LocalDate aThousandDaysLater = newYearsEve.piusDays(1000) ; year = aThousandDaysLater.getYear();// 2002 month = aThousandDaysLater.getMonthValue(); // 09 day = aThousandDaysLater.getDayOfMonth(); // 26
LocalDate 类封装了实例域来维护所设置的日期。
更改器方法与访问器方法
再来看上一节中的 plusDays 方法调用:
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000) ;
这个调用之后 newYeareEve 会有什么变化? 它会改为 1000 天之后的日期吗? 事实上,并没 有。plusDays方法会生成一个新的 LocalDate 对象,然后把这个新对象赋给 aThousandDaysLater 变量。原来的对象不做任何改动。 我们说plusDays方法没有更改调用这个方法的对象。
Java 库的一个较早版本曾经有另一个类来处理日历,名为 GregorianCalendar。 可以如下 为这个类表示的一个日期增加 1000 天:
CregorianCalendar someDay = new CregorianCalendar(1999, 11, 31) ; // Odd feature of that class: month numbers go from 0 to 11 someDay.add(Calendar.DAY_0F_M0NTH, 1000) ;
与 LocalDate.plusDays 方法不同,GregorianCalendar.add 方法是一个更改器方法 ( mutator method ) 调用这个方法后,someDay 对象的状态会改变。可以如下査看新状态:
year = someDay.get(Calendar.YEAR) ; // 2002 month = someDay.get(Calendar.MONTH)+ 1;//09 day = someDay.get(Ca1endar.DAY_0F_M0NTH) ; //26
正是因为这个原因,我们将变量命名为someDay 而不是 newYearsEve 调用这个更改 器方法之后,它不再是新年前夜。 相 反, 只 访 问 对 象 而 不 修 改 对 象 的 方 法 有 时 称 为 访 问 器 方 法 例 如, LocalDate.getYear 和 GregorianCalendar.get 就是访问器方法。
用户自定义类
Employee 类
在 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); } // a method public String getNameO { return name; } // more methods ... }
这里将这个类的实现细节分成以下几个部分, 并分別在稍后的几节中给予介绍:
import java.util.*; /** * This program tests the Employee class. * @version 1.11 2004-02-19 * @author Cay Horstmann */ public class EmployeeTest { public static void main(String[] args) { // fill the staff array with three Employee objects Employee[] staff = new Employee[3]; staff[0] = new Employee("Carl Cracker", 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 { private String name; private double salary; private Date hireDay; public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); // GregorianCalendar uses 0 for January hireDay = calendar.getTime(); } public String getName() { return name; } public double getSalary() { return salary; } public Date getHireDay() { return hireDay; } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
多个源文件的使用
Employee.一个源文件包含了两个类。许多程序员习惯于将每一个类存在一个 单独的源文件中。例如,将 Employee类存放在文件 Employee.java中, 将 EmployeeTest 类存 放在文件 EmployeeTest.java 中。 如果喜欢这样组织文件, 将可以有两种编译源程序的方法。一种是使用通配符调用 Java编译器:
javac Employee*.java
于是,所有与通配符匹配的源文件都将被编译成类文件。或者键人下列命令:
javac EmployeeTest.java
剖析 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 意味着任何类的任何方法都可以调用这 些方法。
接下来,需要注意在 Employee 类的实例中有三个实例域用来存放将要操作的数据:
private String name; private double salary; private LocalDate hireDay;
关键字 private 确保只有 Employee类自身的方法能够访问这些实例域, 而其他类的方法不能 够读写这些域。
最后, 请注意, 有两个实例域本身就是对象: name 域是 String类对象, hireDay 域是 LocalDate 类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例域。
从构造器开始
下面先看看 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); // GregorianCalendar uses 0 for January hireDay = calendar.getTime(); }
隐式参数与显式参数
方法用于操作对象以及存取它们的实例域。例如,方法:
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
将调用这个方法的对象的 salary 实例域设置为新值。看看下面这个调用:
number007.raiseSalary(5);
它的结果将 number007.salary 域的值增加 5%。具体地说,这个调用将执行下列指令:
double raise = nuaber007.salary * 5 / 100; nuiber007.salary += raise;
raiseSalary方法有两个参数。 第一个参数称为隐式( implicit) 参数, 是出现在方法名前的 Employee 类对象。第
二个参数位于方法名后面括号中的数值,这是一个显式( explicit) 参 数
在每一个方法中, 关键字 this 表示隐式参数。 如果需要的话,可以用下列方式编写 raiseSalary 方法:
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
有些程序员更偏爱这样的风格,因为这样可以将实例域与局部变量明显地区分开来。
封装的优点
最后,再仔细地看一下非常简单的 getName方法、getSalary 方法和 getHireDay 方法。
public String getName(){ return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; }
这些都是典型的访问器方法。由于它们只返回实例域值, 因此又称为域访问器。
关键在于 name 是一个只读域。一旦在构造器中设置完毕,就没有任何一个办法可以对 它进行修改,这样来确保 name 域不会受到外界的破坏。 虽然 salary 不是只读域,但是它只能用 raiseSalary 方法修改。
特别是一旦这个域值出现 了错误, 只要调试这个方法就可以了。如果 salary 域是 public 的,破坏这个域值的捣乱者有 可能会出没在任何地方。
浙公网安备 33010602011771号